diff --git a/.cargo/collab-config.toml b/.cargo/collab-config.toml deleted file mode 100644 index 74603802d8..0000000000 --- a/.cargo/collab-config.toml +++ /dev/null @@ -1,5 +0,0 @@ -# This file is used to build collab in a Docker image. -# In particular, we don't use clang. -[build] -# v0 mangling scheme provides more detailed backtraces around closures -rustflags = ["-C", "symbol-mangling-version=v0", "--cfg", "tokio_unstable"] diff --git a/.cloudflare/README.md b/.cloudflare/README.md deleted file mode 100644 index d21377ddff..0000000000 --- a/.cloudflare/README.md +++ /dev/null @@ -1,15 +0,0 @@ -We have two cloudflare workers that let us serve some assets of this repo -from Cloudflare. - -- `open-source-website-assets` is used for `install.sh` -- `docs-proxy` is used for `https://zed.dev/docs` - -On push to `main`, both of these (and the files they depend on) are uploaded to Cloudflare. - -### Deployment - -These functions are deployed on push to main by the deploy_cloudflare.yml workflow. Worker Rules in Cloudflare intercept requests to zed.dev and proxy them to the appropriate workers. - -### Testing - -You can use [wrangler](https://developers.cloudflare.com/workers/cli-wrangler/install-update) to test these workers locally, or to deploy custom versions. diff --git a/.cloudflare/docs-proxy/src/worker.js b/.cloudflare/docs-proxy/src/worker.js deleted file mode 100644 index f9f441883a..0000000000 --- a/.cloudflare/docs-proxy/src/worker.js +++ /dev/null @@ -1,14 +0,0 @@ -export default { - async fetch(request, _env, _ctx) { - const url = new URL(request.url); - url.hostname = "docs-anw.pages.dev"; - - let res = await fetch(url, request); - - if (res.status === 404) { - res = await fetch("https://zed.dev/404"); - } - - return res; - }, -}; diff --git a/.cloudflare/docs-proxy/wrangler.toml b/.cloudflare/docs-proxy/wrangler.toml deleted file mode 100644 index b5262cc070..0000000000 --- a/.cloudflare/docs-proxy/wrangler.toml +++ /dev/null @@ -1,8 +0,0 @@ -name = "docs-proxy" -main = "src/worker.js" -compatibility_date = "2024-05-03" -workers_dev = true - -[[routes]] -pattern = "zed.dev/docs*" -zone_name = "zed.dev" diff --git a/.cloudflare/open-source-website-assets/src/worker.js b/.cloudflare/open-source-website-assets/src/worker.js deleted file mode 100644 index be34f8d118..0000000000 --- a/.cloudflare/open-source-website-assets/src/worker.js +++ /dev/null @@ -1,19 +0,0 @@ -export default { - async fetch(request, env) { - const url = new URL(request.url); - const key = url.pathname.slice(1); - - const object = await env.OPEN_SOURCE_WEBSITE_ASSETS_BUCKET.get(key); - if (!object) { - return await fetch("https://zed.dev/404"); - } - - const headers = new Headers(); - object.writeHttpMetadata(headers); - headers.set("etag", object.httpEtag); - - return new Response(object.body, { - headers, - }); - }, -}; diff --git a/.cloudflare/open-source-website-assets/wrangler.toml b/.cloudflare/open-source-website-assets/wrangler.toml deleted file mode 100644 index b4947fa938..0000000000 --- a/.cloudflare/open-source-website-assets/wrangler.toml +++ /dev/null @@ -1,8 +0,0 @@ -name = "open-source-website-assets" -main = "src/worker.js" -compatibility_date = "2024-05-15" -workers_dev = true - -[[r2_buckets]] -binding = 'OPEN_SOURCE_WEBSITE_ASSETS_BUCKET' -bucket_name = 'zed-open-source-website-assets' diff --git a/.config/nextest.toml b/.config/nextest.toml index 49fb4d01f7..e8fa9bdf9d 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -1,20 +1,5 @@ -[test-groups] -sequential-db-tests = { max-threads = 1 } +# Nextest configuration for GPUI +# https://nexte.st/book/configuration.html -[[profile.default.overrides]] -filter = 'package(db)' -test-group = 'sequential-db-tests' - -# Run slowest tests first. -# -[[profile.default.overrides]] -filter = 'package(worktree) and test(test_random_worktree_changes)' -priority = 100 - -[[profile.default.overrides]] -filter = 'package(collab) and (test(random_project_collaboration_tests) or test(random_channel_buffer_tests) or test(test_contact_requests) or test(test_basic_following))' -priority = 99 - -[[profile.default.overrides]] -filter = 'package(extension_host) and test(test_extension_store_with_test_extension)' -priority = 99 +[profile.default] +# Default test settings \ No newline at end of file diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs deleted file mode 100644 index fbcc76a865..0000000000 --- a/.git-blame-ignore-revs +++ /dev/null @@ -1,36 +0,0 @@ -# .git-blame-ignore-revs -# -# This file consists of a list of commits that should be ignored for -# `git blame` purposes. This is useful for ignoring commits that only -# changed whitespace / indentation / formatting, but did not change -# the underlying syntax tree. -# -# GitHub will pick this up automatically for blame views: -# https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view -# To use this file locally, run: -# git blame --ignore-revs-file .git-blame-ignore-revs -# To always use this file by default, run: -# git config --local blame.ignoreRevsFile .git-blame-ignore-revs -# To disable this functionality, run: -# git config --local blame.ignoreRevsFile "" -# Comments are optional, but may provide helpful context. - -# 2023-04-20 Set default tab_size for JSON to 2 and apply new formatting -# https://github.com/zed-industries/zed/pull/2394 -eca93c124a488b4e538946cd2d313bd571aa2b86 - -# 2024-02-15 Format YAML files -# https://github.com/zed-industries/zed/pull/7887 -a161a7d0c95ca7505bf9218bfae640ee5444c88b - -# 2024-02-25 Format JSON files in assets/ -# https://github.com/zed-industries/zed/pull/8405 -ffdda588b41f7d9d270ffe76cab116f828ad545e - -# 2024-07-05 Improved formatting of default keymaps (single line per bind) -# https://github.com/zed-industries/zed/pull/13887 -813cc3f5e537372fc86720b5e71b6e1c815440ab - -# 2024-07-24 docs: Format docs -# https://github.com/zed-industries/zed/pull/15352 -3a44a59f8ec114ac1ba22f7da1652717ef7e4e5c diff --git a/.gitattributes b/.gitattributes index 57afd4ea69..9973cfb4db 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,2 @@ # Prevent GitHub from displaying comments within JSON files as errors. *.json linguist-language=JSON-with-Comments - -# Ensure the WSL script always has LF line endings, even on Windows -crates/zed/resources/windows/zed.sh text eol=lf diff --git a/.github/ISSUE_TEMPLATE/10_bug_report.yml b/.github/ISSUE_TEMPLATE/10_bug_report.yml deleted file mode 100644 index cae10f02ec..0000000000 --- a/.github/ISSUE_TEMPLATE/10_bug_report.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: Report a bug -description: Report a problem with Zed. -type: Bug -labels: "state:needs triage" -body: - - type: markdown - attributes: - value: | - Is this bug already reported? Upvote to get it noticed faster. [Here's the search](https://github.com/zed-industries/zed/issues). Upvote means giving it a :+1: reaction. - - Feature request? Please open in [discussions](https://github.com/zed-industries/zed/discussions/new/choose) instead. - - Just have a question or need support? Welcome to [Discord Support Forums](https://discord.com/invite/zedindustries). - - type: textarea - attributes: - label: Reproduction steps - description: A step-by-step description of how to reproduce the bug from a **clean Zed install**. The more context you provide, the easier it is to find and fix the problem fast. - placeholder: | - 1. Start Zed - 2. Click X - validations: - required: true - - type: textarea - attributes: - label: Current vs. Expected behavior - description: | - Current behavior (screenshots, videos, etc. are appreciated), vs. what you expected the behavior to be. - - placeholder: | - Current behavior: The icon is blue. Expected behavior: The icon should be red because this is what the setting is documented to do. - validations: - required: true - - type: textarea - id: environment - attributes: - label: Zed version and system specs - description: | - Open the command palette in Zed, then type “zed: copy system specs into clipboard”. - placeholder: | - Zed: v0.215.0 (Zed Nightly bfe141ea79aa4984028934067ba75c48d99136ae) - OS: macOS 15.1 - Memory: 36 GiB - Architecture: aarch64 - validations: - required: true - - type: textarea - attributes: - label: Attach Zed log file - description: | - Open the command palette in Zed, then type `zed: open log` to see the last 1000 lines. Or type `zed: reveal log in file manager` in the command palette to reveal the log file itself. - value: | -
Zed.log - - - ```log - - ``` - -
- validations: - required: false - - type: textarea - attributes: - label: Relevant Zed settings - description: | - Open the command palette in Zed, then type “zed: open settings file” and copy/paste any relevant (e.g., LSP-specific) settings. - value: | -
settings.json - - - ```json - - ``` - -
- validations: - required: false - - type: textarea - attributes: - label: Relevant Keymap - description: | - Open the command palette in Zed, then type “zed: open keymap file” and copy/paste the file's contents. - value: | -
keymap.json - - - ```json - - ``` - -
- validations: - required: false - - type: textarea - attributes: - label: (for AI issues) Model provider details - placeholder: | - - Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc.) - - Model Name: (Claude Sonnet 4.5, Gemini 3 Pro, GPT-5) - - Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads) - - Other details (ACPs, MCPs, other settings, etc.): - validations: - required: false - - type: dropdown - attributes: - label: If you are using WSL on Windows, what flavor of Linux are you using? - multiple: false - options: - - Arch Linux - - Ubuntu - - Fedora - - Mint - - Pop!_OS - - NixOS - - Other diff --git a/.github/ISSUE_TEMPLATE/11_crash_report.yml b/.github/ISSUE_TEMPLATE/11_crash_report.yml deleted file mode 100644 index a019848e87..0000000000 --- a/.github/ISSUE_TEMPLATE/11_crash_report.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Report a crash -description: Zed is crashing or freezing or hanging. -type: Crash -labels: "state:needs triage" -body: - - type: textarea - attributes: - label: Reproduction steps - description: A step-by-step description of how to reproduce the crash from a **clean Zed install**. The more context you provide, the easier it is to find and fix the problem fast. - placeholder: | - 1. Start Zed - 2. Perform an action - 3. Zed crashes - validations: - required: true - - type: textarea - attributes: - label: Zed version and system specs - description: | - Open the command palette in Zed, then type “zed: copy system specs into clipboard”. - placeholder: | - Zed: v0.215.0 (Zed Nightly bfe141ea79aa4984028934067ba75c48d99136ae) - OS: macOS 15.1 - Memory: 36 GiB - Architecture: aarch64 - validations: - required: true - - type: textarea - attributes: - label: Attach Zed log file - description: | - Open the command palette in Zed, then type `zed: open log` to see the last 1000 lines. Or type `zed: reveal log in file manager` in the command palette to reveal the log file itself. - value: | -
Zed.log - - - ```log - - ``` - -
- validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 9bf14ce72d..0000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,9 +0,0 @@ -# yaml-language-server: $schema=https://www.schemastore.org/github-issue-config.json -blank_issues_enabled: false -contact_links: - - name: Feature request - url: https://github.com/zed-industries/zed/discussions/new/choose - about: To request a feature, open a new discussion under one of the appropriate categories. - - name: Our Discord community - url: https://discord.com/invite/zedindustries - about: Join our Discord server for real-time discussion and user support. diff --git a/.github/actionlint.yml b/.github/actionlint.yml deleted file mode 100644 index 6d8e0107e9..0000000000 --- a/.github/actionlint.yml +++ /dev/null @@ -1,45 +0,0 @@ -# Configuration related to self-hosted runner. -self-hosted-runner: - # Labels of self-hosted runner in array of strings. - labels: - # 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 - # 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 deleted file mode 100644 index d2e62d5b22..0000000000 --- a/.github/actions/build_docs/action.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: "Build docs" -description: "Build the docs" - -runs: - using: "composite" - steps: - - name: Setup mdBook - uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2 - with: - mdbook-version: "0.4.37" - - - name: Cache dependencies - uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 - with: - save-if: ${{ github.ref == 'refs/heads/main' }} - # cache-provider: "buildjet" - - - name: Install Linux dependencies - shell: bash -euxo pipefail {0} - run: ./script/linux - - - name: Check for broken links (in MD) - uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1 - with: - args: --no-progress --exclude '^http' './docs/src/**/*' - fail: true - - - name: Build book - shell: bash -euxo pipefail {0} - run: | - mkdir -p target/deploy - mdbook build ./docs --dest-dir=../target/deploy/docs/ - - - name: Check for broken links (in HTML) - uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1 - with: - args: --no-progress --exclude '^http' 'target/deploy/docs/' - fail: true diff --git a/.github/actions/check_style/action.yml b/.github/actions/check_style/action.yml deleted file mode 100644 index f8362bc636..0000000000 --- a/.github/actions/check_style/action.yml +++ /dev/null @@ -1,9 +0,0 @@ -name: "Check formatting" -description: "Checks code formatting use cargo fmt" - -runs: - using: "composite" - steps: - - name: cargo fmt - shell: bash -euxo pipefail {0} - run: cargo fmt --all -- --check diff --git a/.github/actions/run_tests/action.yml b/.github/actions/run_tests/action.yml deleted file mode 100644 index a071aba3a8..0000000000 --- a/.github/actions/run_tests/action.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: "Run tests" -description: "Runs the tests" - -runs: - using: "composite" - steps: - - name: Install nextest - uses: taiki-e/install-action@nextest - - - name: Install Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: "18" - - - name: Limit target directory size - env: - MAX_SIZE: ${{ runner.os == 'macOS' && 300 || 100 }} - shell: bash -euxo pipefail {0} - # Use the variable in the run command - run: script/clear-target-dir-if-larger-than ${{ env.MAX_SIZE }} - - - name: Run tests - shell: bash -euxo pipefail {0} - run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final diff --git a/.github/actions/run_tests_windows/action.yml b/.github/actions/run_tests_windows/action.yml deleted file mode 100644 index 307b73f363..0000000000 --- a/.github/actions/run_tests_windows/action.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: "Run tests on Windows" -description: "Runs the tests on Windows" - -inputs: - working-directory: - description: "The working directory" - required: true - default: "." - -runs: - using: "composite" - steps: - - name: Install test runner - working-directory: ${{ inputs.working-directory }} - uses: taiki-e/install-action@nextest - - - name: Install Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: "18" - - - name: Run tests - shell: powershell - working-directory: ${{ inputs.working-directory }} - run: | - cargo nextest run --workspace --no-fail-fast --failure-output immediate-final diff --git a/.github/cherry-pick-bot.yml b/.github/cherry-pick-bot.yml deleted file mode 100644 index 1f62315d79..0000000000 --- a/.github/cherry-pick-bot.yml +++ /dev/null @@ -1,2 +0,0 @@ -enabled: true -preservePullRequestTitle: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index e107c14470..0000000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,5 +0,0 @@ -Closes #ISSUE - -Release Notes: - -- N/A *or* Added/Fixed/Improved ... diff --git a/.github/workflows/after_release.yml b/.github/workflows/after_release.yml deleted file mode 100644 index 21b9a8fe0e..0000000000 --- a/.github/workflows/after_release.yml +++ /dev/null @@ -1,119 +0,0 @@ -# Generated from xtask::workflows::after_release -# Rebuild with `cargo xtask workflows`. -name: after_release -on: - release: - types: - - published - workflow_dispatch: - inputs: - tag_name: - description: tag_name - required: true - type: string - prerelease: - description: prerelease - required: true - type: boolean - body: - description: body - type: string - default: '' -jobs: - rebuild_releases_page: - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - name: after_release::rebuild_releases_page::refresh_cloud_releases - run: curl -fX POST https://cloud.zed.dev/releases/refresh?expect_tag=${{ github.event.release.tag_name || inputs.tag_name }} - shell: bash -euxo pipefail {0} - - name: after_release::rebuild_releases_page::redeploy_zed_dev - run: npm exec --yes -- vercel@37 --token="$VERCEL_TOKEN" --scope zed-industries redeploy https://zed.dev - shell: bash -euxo pipefail {0} - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - post_to_discord: - needs: - - rebuild_releases_page - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - id: get-release-url - name: after_release::post_to_discord::get_release_url - run: | - if [ "${{ github.event.release.prerelease || inputs.prerelease }}" == "true" ]; then - URL="https://zed.dev/releases/preview" - else - URL="https://zed.dev/releases/stable" - fi - - echo "URL=$URL" >> "$GITHUB_OUTPUT" - shell: bash -euxo pipefail {0} - - id: get-content - name: after_release::post_to_discord::get_content - uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757 - with: - stringToTruncate: | - 📣 Zed [${{ github.event.release.tag_name || inputs.tag_name }}](<${{ steps.get-release-url.outputs.URL }}>) was just released! - - ${{ github.event.release.body || inputs.body }} - maxLength: 2000 - truncationSymbol: '...' - - name: after_release::post_to_discord::discord_webhook_action - uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 - with: - webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }} - content: ${{ steps.get-content.outputs.string }} - publish_winget: - runs-on: self-32vcpu-windows-2022 - steps: - - id: set-package-name - name: after_release::publish_winget::set_package_name - run: | - if ("${{ github.event.release.prerelease || inputs.prerelease }}" -eq "true") { - $PACKAGE_NAME = "ZedIndustries.Zed.Preview" - } else { - $PACKAGE_NAME = "ZedIndustries.Zed" - } - - echo "PACKAGE_NAME=$PACKAGE_NAME" >> $env:GITHUB_OUTPUT - shell: pwsh - - name: after_release::publish_winget::winget_releaser - uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f - with: - identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }} - release-tag: ${{ github.event.release.tag_name || inputs.tag_name }} - max-versions-to-keep: 5 - token: ${{ secrets.WINGET_TOKEN }} - create_sentry_release: - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: release::create_sentry_release - uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c - with: - environment: production - env: - SENTRY_ORG: zed-dev - SENTRY_PROJECT: zed - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - notify_on_failure: - needs: - - rebuild_releases_page - - post_to_discord - - publish_winget - - create_sentry_release - if: failure() - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - name: release::notify_on_failure::notify_slack - run: |- - curl -X POST -H 'Content-type: application/json'\ - --data '{"text":"${{ github.workflow }} failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' "$SLACK_WEBHOOK" - shell: bash -euxo pipefail {0} - env: - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_WORKFLOW_FAILURES }} diff --git a/.github/workflows/bump_collab_staging.yml b/.github/workflows/bump_collab_staging.yml deleted file mode 100644 index d400905b4d..0000000000 --- a/.github/workflows/bump_collab_staging.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Bump collab-staging Tag - -on: - schedule: - # Fire every day at 16:00 UTC (At the start of the US workday) - - cron: "0 16 * * *" - -jobs: - update-collab-staging-tag: - if: github.repository_owner == 'zed-industries' - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - - name: Update collab-staging tag - run: | - git config user.name github-actions - git config user.email github-actions@github.com - git tag -f collab-staging - git push origin collab-staging --force diff --git a/.github/workflows/bump_patch_version.yml b/.github/workflows/bump_patch_version.yml deleted file mode 100644 index e1ae890043..0000000000 --- a/.github/workflows/bump_patch_version.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: bump_patch_version - -on: - workflow_dispatch: - inputs: - branch: - description: "Branch name to run on" - required: true - -concurrency: - # Allow only one workflow per any non-`main` branch. - group: ${{ github.workflow }}-${{ inputs.branch }} - cancel-in-progress: true - -jobs: - bump_patch_version: - if: github.repository_owner == 'zed-industries' - runs-on: - - namespace-profile-16x32-ubuntu-2204 - steps: - - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - ref: ${{ github.event.inputs.branch }} - ssh-key: ${{ secrets.ZED_BOT_DEPLOY_KEY }} - - - name: Bump Patch Version - run: | - set -eux - - channel="$(cat crates/zed/RELEASE_CHANNEL)" - - tag_suffix="" - case $channel in - stable) - ;; - preview) - tag_suffix="-pre" - ;; - *) - echo "this must be run on either of stable|preview release branches" >&2 - exit 1 - ;; - esac - which cargo-set-version > /dev/null || cargo install cargo-edit -f --no-default-features --features "set-version" - output="$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //')" - export GIT_COMMITTER_NAME="Zed Bot" - export GIT_COMMITTER_EMAIL="hi@zed.dev" - git commit -am "Bump to $output for @$GITHUB_ACTOR" --author "Zed Bot " - git tag "v${output}${tag_suffix}" - git push origin HEAD "v${output}${tag_suffix}" diff --git a/.github/workflows/cherry_pick.yml b/.github/workflows/cherry_pick.yml deleted file mode 100644 index bc01aae17e..0000000000 --- a/.github/workflows/cherry_pick.yml +++ /dev/null @@ -1,44 +0,0 @@ -# Generated from xtask::workflows::cherry_pick -# Rebuild with `cargo xtask workflows`. -name: cherry_pick -run-name: 'cherry_pick to ${{ inputs.channel }} #${{ inputs.pr_number }}' -on: - workflow_dispatch: - inputs: - commit: - description: commit - required: true - type: string - branch: - description: branch - required: true - type: string - channel: - description: channel - required: true - type: string - pr_number: - description: pr_number - required: true - type: string -jobs: - run_cherry_pick: - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - id: get-app-token - name: cherry_pick::run_cherry_pick::authenticate_as_zippy - uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1 - with: - app-id: ${{ secrets.ZED_ZIPPY_APP_ID }} - private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }} - - name: cherry_pick::run_cherry_pick::cherry_pick - run: ./script/cherry-pick ${{ inputs.branch }} ${{ inputs.commit }} ${{ inputs.channel }} - shell: bash -euxo pipefail {0} - env: - GIT_COMMITTER_NAME: Zed Zippy - GIT_COMMITTER_EMAIL: hi@zed.dev - GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }} diff --git a/.github/workflows/community_champion_auto_labeler.yml b/.github/workflows/community_champion_auto_labeler.yml deleted file mode 100644 index 93f1d56023..0000000000 --- a/.github/workflows/community_champion_auto_labeler.yml +++ /dev/null @@ -1,107 +0,0 @@ -name: Community Champion Auto Labeler - -on: - issues: - types: [opened] - pull_request_target: - types: [opened] - -jobs: - label_community_champion: - if: github.repository_owner == 'zed-industries' - runs-on: ubuntu-latest - steps: - - name: Check if author is a community champion and apply label - uses: actions/github-script@v7 - env: - COMMUNITY_CHAMPIONS: | - 0x2CA - 5brian - 5herlocked - abdelq - afgomez - AidanV - akbxr - AlvaroParker - amtoaer - artemevsevev - bajrangCoder - bcomnes - Be-ing - blopker - bnjjj - bobbymannino - CharlesChen0823 - chbk - cppcoffee - davewa - ddoemonn - djsauble - errmayank - fantacell - findrakecil - FloppyDisco - gko - huacnlee - imumesh18 - jacobtread - jansol - jeffreyguenther - jenslys - jongretar - lemorage - lnay - marcocondrache - marius851000 - mikebronner - ognevny - playdohface - RemcoSmitsDev - romaninsh - Simek - someone13574 - sourcefrog - suxiaoshao - Takk8IS - thedadams - tidely - timvermeulen - valentinegb - versecafe - vitallium - warrenjokinen - WhySoBad - ya7010 - Zertsov - with: - script: | - const communityChampions = process.env.COMMUNITY_CHAMPIONS - .split('\n') - .map(handle => handle.trim().toLowerCase()) - .filter(handle => handle.length > 0); - - let author; - if (context.eventName === 'issues') { - author = context.payload.issue.user.login; - } else if (context.eventName === 'pull_request_target') { - author = context.payload.pull_request.user.login; - } - - if (!author || !communityChampions.includes(author.toLowerCase())) { - return; - } - - const issueNumber = context.payload.issue?.number || context.payload.pull_request?.number; - - try { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - labels: ['community champion'] - }); - - console.log(`Applied 'community champion' label to #${issueNumber} by ${author}`); - } catch (error) { - console.error(`Failed to apply label: ${error.message}`); - } diff --git a/.github/workflows/community_close_stale_issues.yml b/.github/workflows/community_close_stale_issues.yml deleted file mode 100644 index 14c1a0a083..0000000000 --- a/.github/workflows/community_close_stale_issues.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: "Close Stale Issues" -on: - schedule: - - cron: "0 8 31 DEC *" - workflow_dispatch: - -jobs: - stale: - if: github.repository_owner == 'zed-industries' - runs-on: ubuntu-latest - steps: - - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-issue-message: > - Hi there! 👋 - - We're working to clean up our issue tracker by closing older bugs that might not be relevant anymore. If you are able to reproduce this issue in the latest version of Zed, please let us know by commenting on this issue, and it will be kept open. If you can't reproduce it, feel free to close the issue yourself. Otherwise, it will close automatically in 14 days. - - Thanks for your help! - close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please open a new issue with a link to this issue." - days-before-stale: 60 - days-before-close: 14 - only-issue-types: "Bug,Crash" - operations-per-run: 1000 - ascending: true - enable-statistics: true - stale-issue-label: "stale" - exempt-issue-labels: "never stale" diff --git a/.github/workflows/community_update_all_top_ranking_issues.yml b/.github/workflows/community_update_all_top_ranking_issues.yml deleted file mode 100644 index 66b1873b95..0000000000 --- a/.github/workflows/community_update_all_top_ranking_issues.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Update All Top Ranking Issues - -on: - schedule: - - cron: "0 */12 * * *" - workflow_dispatch: - -jobs: - update_top_ranking_issues: - runs-on: ubuntu-latest - if: github.repository == 'zed-industries/zed' - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Set up uv - uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3 - with: - version: "latest" - enable-cache: true - cache-dependency-glob: "script/update_top_ranking_issues/pyproject.toml" - - name: Install Python 3.13 - run: uv python install 3.13 - - name: Install dependencies - run: uv sync --project script/update_top_ranking_issues -p 3.13 - - name: Run script - run: uv run --project script/update_top_ranking_issues script/update_top_ranking_issues/main.py --github-token ${{ secrets.GITHUB_TOKEN }} --issue-reference-number 5393 diff --git a/.github/workflows/community_update_weekly_top_ranking_issues.yml b/.github/workflows/community_update_weekly_top_ranking_issues.yml deleted file mode 100644 index e1514da716..0000000000 --- a/.github/workflows/community_update_weekly_top_ranking_issues.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Update Weekly Top Ranking Issues - -on: - schedule: - - cron: "0 15 * * *" - workflow_dispatch: - -jobs: - update_top_ranking_issues: - runs-on: ubuntu-latest - if: github.repository == 'zed-industries/zed' - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Set up uv - uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3 - with: - version: "latest" - enable-cache: true - cache-dependency-glob: "script/update_top_ranking_issues/pyproject.toml" - - name: Install Python 3.13 - run: uv python install 3.13 - - name: Install dependencies - run: uv sync --project script/update_top_ranking_issues -p 3.13 - - name: Run script - run: uv run --project script/update_top_ranking_issues script/update_top_ranking_issues/main.py --github-token ${{ secrets.GITHUB_TOKEN }} --issue-reference-number 6952 --query-day-interval 7 diff --git a/.github/workflows/compare_perf.yml b/.github/workflows/compare_perf.yml deleted file mode 100644 index 48fc850f8f..0000000000 --- a/.github/workflows/compare_perf.yml +++ /dev/null @@ -1,80 +0,0 @@ -# Generated from xtask::workflows::compare_perf -# Rebuild with `cargo xtask workflows`. -name: compare_perf -on: - workflow_dispatch: - inputs: - head: - description: head - required: true - type: string - base: - description: base - required: true - type: string - crate_name: - description: crate_name - type: string - default: '' -jobs: - run_perf: - runs-on: namespace-profile-16x32-ubuntu-2204 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_cargo_config - run: | - mkdir -p ./../.cargo - cp ./.cargo/ci-config.toml ./../.cargo/config.toml - shell: bash -euxo pipefail {0} - - name: steps::setup_linux - run: ./script/linux - shell: bash -euxo pipefail {0} - - name: steps::install_mold - run: ./script/install-mold - shell: bash -euxo pipefail {0} - - name: steps::download_wasi_sdk - run: ./script/download-wasi-sdk - shell: bash -euxo pipefail {0} - - name: compare_perf::run_perf::install_hyperfine - uses: taiki-e/install-action@hyperfine - - name: steps::git_checkout - run: git fetch origin ${{ inputs.base }} && git checkout ${{ inputs.base }} - shell: bash -euxo pipefail {0} - - name: compare_perf::run_perf::cargo_perf_test - run: |2- - - if [ -n "${{ inputs.crate_name }}" ]; then - cargo perf-test -p ${{ inputs.crate_name }} -- --json=${{ inputs.base }}; - else - cargo perf-test -p vim -- --json=${{ inputs.base }}; - fi - shell: bash -euxo pipefail {0} - - name: steps::git_checkout - run: git fetch origin ${{ inputs.head }} && git checkout ${{ inputs.head }} - shell: bash -euxo pipefail {0} - - name: compare_perf::run_perf::cargo_perf_test - run: |2- - - if [ -n "${{ inputs.crate_name }}" ]; then - cargo perf-test -p ${{ inputs.crate_name }} -- --json=${{ inputs.head }}; - else - cargo perf-test -p vim -- --json=${{ inputs.head }}; - fi - shell: bash -euxo pipefail {0} - - name: compare_perf::run_perf::compare_runs - run: cargo perf-compare --save=results.md ${{ inputs.base }} ${{ inputs.head }} - shell: bash -euxo pipefail {0} - - name: '@actions/upload-artifact results.md' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: results.md - path: results.md - if-no-files-found: error - - name: steps::cleanup_cargo_config - if: always() - run: | - rm -rf ./../.cargo - shell: bash -euxo pipefail {0} diff --git a/.github/workflows/congrats.yml b/.github/workflows/congrats.yml deleted file mode 100644 index efd9812d80..0000000000 --- a/.github/workflows/congrats.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Congratsbot - -on: - push: - branches: [main] - -jobs: - check-author: - if: ${{ github.repository_owner == 'zed-industries' }} - runs-on: ubuntu-latest - outputs: - should_congratulate: ${{ steps.check.outputs.should_congratulate }} - steps: - - name: Get PR info and check if author is external - id: check - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.CONGRATSBOT_GITHUB_TOKEN }} - script: | - const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ - owner: context.repo.owner, - repo: context.repo.repo, - commit_sha: context.sha - }); - - if (prs.length === 0) { - core.setOutput('should_congratulate', 'false'); - return; - } - - const mergedPR = prs.find(pr => pr.merged_at !== null) || prs[0]; - const prAuthor = mergedPR.user.login; - - try { - await github.rest.teams.getMembershipForUserInOrg({ - org: 'zed-industries', - team_slug: 'staff', - username: prAuthor - }); - core.setOutput('should_congratulate', 'false'); - } catch (error) { - if (error.status === 404) { - core.setOutput('should_congratulate', 'true'); - } else { - console.error(`Error checking team membership: ${error.message}`); - core.setOutput('should_congratulate', 'false'); - } - } - - congrats: - needs: check-author - if: needs.check-author.outputs.should_congratulate == 'true' - uses: withastro/automation/.github/workflows/congratsbot.yml@main - with: - EMOJIS: 🎉,🎊,🧑‍🚀,🥳,🙌,🚀,🦀,🔥,🚢 - secrets: - DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_CONGRATS }} diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml deleted file mode 100644 index 9d6054eb3e..0000000000 --- a/.github/workflows/danger.yml +++ /dev/null @@ -1,40 +0,0 @@ -# Generated from xtask::workflows::danger -# Rebuild with `cargo xtask workflows`. -name: danger -on: - pull_request: - types: - - opened - - synchronize - - reopened - - edited - branches: - - main -jobs: - danger: - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_pnpm - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 - with: - version: '9' - - name: steps::setup_node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: '20' - cache: pnpm - cache-dependency-path: script/danger/pnpm-lock.yaml - - name: danger::danger_job::install_deps - run: pnpm install --dir script/danger - shell: bash -euxo pipefail {0} - - name: danger::danger_job::run - run: pnpm run --dir script/danger danger ci - shell: bash -euxo pipefail {0} - env: - GITHUB_TOKEN: not_a_real_token - DANGER_GITHUB_API_BASE_URL: https://danger-proxy.fly.dev/github diff --git a/.github/workflows/deploy_cloudflare.yml b/.github/workflows/deploy_cloudflare.yml deleted file mode 100644 index 2650cce140..0000000000 --- a/.github/workflows/deploy_cloudflare.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Deploy Docs - -on: - push: - branches: - - main - -jobs: - deploy-docs: - name: Deploy Docs - if: github.repository_owner == 'zed-industries' - runs-on: namespace-profile-16x32-ubuntu-2204 - - steps: - - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - clean: false - - - name: Set up default .cargo/config.toml - run: cp ./.cargo/collab-config.toml ./.cargo/config.toml - - - name: Build docs - uses: ./.github/actions/build_docs - env: - DOCS_AMPLITUDE_API_KEY: ${{ secrets.DOCS_AMPLITUDE_API_KEY }} - - - name: Deploy Docs - uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy target/deploy --project-name=docs - - - name: Deploy Install - uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: r2 object put -f script/install.sh zed-open-source-website-assets/install.sh - - - name: Deploy Docs Workers - uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: deploy .cloudflare/docs-proxy/src/worker.js - - - name: Deploy Install Workers - uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: deploy .cloudflare/docs-proxy/src/worker.js - - - name: Preserve Wrangler logs - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - if: always() - with: - name: wrangler_logs - path: /home/runner/.config/.wrangler/logs/ diff --git a/.github/workflows/deploy_collab.yml b/.github/workflows/deploy_collab.yml deleted file mode 100644 index ce0c0eac40..0000000000 --- a/.github/workflows/deploy_collab.yml +++ /dev/null @@ -1,148 +0,0 @@ -name: Publish Collab Server Image - -on: - push: - tags: - - collab-production - - collab-staging - -env: - DOCKER_BUILDKIT: 1 - -jobs: - style: - name: Check formatting and Clippy lints - if: github.repository_owner == 'zed-industries' - runs-on: - - self-hosted - - macOS - steps: - - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - clean: false - fetch-depth: 0 - - - name: Run style checks - uses: ./.github/actions/check_style - - - name: Run clippy - run: ./script/clippy - - tests: - name: Run tests - runs-on: - - self-hosted - - macOS - needs: style - steps: - - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - clean: false - fetch-depth: 0 - - - name: Install cargo nextest - uses: taiki-e/install-action@nextest - - - name: Limit target directory size - shell: bash -euxo pipefail {0} - run: script/clear-target-dir-if-larger-than 300 - - - name: Run tests - shell: bash -euxo pipefail {0} - run: cargo nextest run --package collab --no-fail-fast - - publish: - name: Publish collab server image - needs: - - style - - tests - runs-on: - - namespace-profile-16x32-ubuntu-2204 - steps: - - name: Install doctl - uses: digitalocean/action-doctl@v2 - with: - token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} - - - name: Sign into DigitalOcean docker registry - run: doctl registry login - - - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - clean: false - - - name: Build docker image - run: | - docker build -f Dockerfile-collab \ - --build-arg "GITHUB_SHA=$GITHUB_SHA" \ - --tag "registry.digitalocean.com/zed/collab:$GITHUB_SHA" \ - . - - - name: Publish docker image - run: docker push "registry.digitalocean.com/zed/collab:${GITHUB_SHA}" - - - name: Prune Docker system - run: docker system prune --filter 'until=72h' -f - - deploy: - name: Deploy new server image - needs: - - publish - runs-on: - - namespace-profile-16x32-ubuntu-2204 - - steps: - - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - clean: false - - - name: Install doctl - uses: digitalocean/action-doctl@v2 - with: - token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} - - - name: Sign into Kubernetes - run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 ${{ secrets.CLUSTER_NAME }} - - - name: Start rollout - run: | - set -eu - if [[ $GITHUB_REF_NAME = "collab-production" ]]; then - export ZED_KUBE_NAMESPACE=production - export ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT=10 - export ZED_API_LOAD_BALANCER_SIZE_UNIT=2 - elif [[ $GITHUB_REF_NAME = "collab-staging" ]]; then - export ZED_KUBE_NAMESPACE=staging - export ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT=1 - export ZED_API_LOAD_BALANCER_SIZE_UNIT=1 - else - echo "cowardly refusing to deploy from an unknown branch" - exit 1 - fi - - echo "Deploying collab:$GITHUB_SHA to $ZED_KUBE_NAMESPACE" - - source script/lib/deploy-helpers.sh - export_vars_for_environment $ZED_KUBE_NAMESPACE - - ZED_DO_CERTIFICATE_ID="$(doctl compute certificate list --format ID --no-header)" - export ZED_DO_CERTIFICATE_ID - export ZED_IMAGE_ID="registry.digitalocean.com/zed/collab:${GITHUB_SHA}" - - 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/extension_bump.yml b/.github/workflows/extension_bump.yml deleted file mode 100644 index c7582378f1..0000000000 --- a/.github/workflows/extension_bump.yml +++ /dev/null @@ -1,147 +0,0 @@ -# Generated from xtask::workflows::extension_bump -# Rebuild with `cargo xtask workflows`. -name: extension_bump -env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: '1' - CARGO_INCREMENTAL: '0' - ZED_EXTENSION_CLI_SHA: 7cfce605704d41ca247e3f84804bf323f6c6caaf -on: - workflow_call: - inputs: - bump-type: - description: bump-type - type: string - default: patch - force-bump: - description: force-bump - required: true - type: boolean - secrets: - app-id: - description: The app ID used to create the PR - required: true - app-secret: - description: The app secret for the corresponding app ID - required: true -jobs: - check_bump_needed: - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - fetch-depth: 0 - - id: compare-versions-check - name: extension_bump::compare_versions - run: | - CURRENT_VERSION="$(sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml)" - PR_PARENT_SHA="${{ github.event.pull_request.head.sha }}" - - if [[ -n "$PR_PARENT_SHA" ]]; then - git checkout "$PR_PARENT_SHA" - elif BRANCH_PARENT_SHA="$(git merge-base origin/main origin/zed-zippy-autobump)"; then - git checkout "$BRANCH_PARENT_SHA" - else - git checkout "$(git log -1 --format=%H)"~1 - fi - - PARENT_COMMIT_VERSION="$(sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml)" - - [[ "$CURRENT_VERSION" == "$PARENT_COMMIT_VERSION" ]] && \ - echo "needs_bump=true" >> "$GITHUB_OUTPUT" || \ - echo "needs_bump=false" >> "$GITHUB_OUTPUT" - - echo "current_version=${CURRENT_VERSION}" >> "$GITHUB_OUTPUT" - shell: bash -euxo pipefail {0} - outputs: - needs_bump: ${{ steps.compare-versions-check.outputs.needs_bump }} - current_version: ${{ steps.compare-versions-check.outputs.current_version }} - timeout-minutes: 1 - bump_extension_version: - needs: - - check_bump_needed - if: |- - (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && - (inputs.force-bump == 'true' || needs.check_bump_needed.outputs.needs_bump == 'true') - runs-on: namespace-profile-8x16-ubuntu-2204 - steps: - - id: generate-token - name: extension_bump::generate_token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ secrets.app-id }} - private-key: ${{ secrets.app-secret }} - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: extension_bump::install_bump_2_version - run: pip install bump2version - shell: bash -euxo pipefail {0} - - id: bump-version - name: extension_bump::bump_version - run: | - OLD_VERSION="${{ needs.check_bump_needed.outputs.current_version }}" - - BUMP_FILES=("extension.toml") - if [[ -f "Cargo.toml" ]]; then - BUMP_FILES+=("Cargo.toml") - fi - - bump2version --verbose --current-version "$OLD_VERSION" --no-configured-files ${{ inputs.bump-type }} "${BUMP_FILES[@]}" - - if [[ -f "Cargo.toml" ]]; then - cargo update --workspace - fi - - NEW_VERSION="$(sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml)" - - echo "new_version=${NEW_VERSION}" >> "$GITHUB_OUTPUT" - shell: bash -euxo pipefail {0} - - name: extension_bump::create_pull_request - uses: peter-evans/create-pull-request@v7 - with: - title: Bump version to ${{ steps.bump-version.outputs.new_version }} - body: This PR bumps the version of this extension to v${{ steps.bump-version.outputs.new_version }} - commit-message: Bump version to v${{ steps.bump-version.outputs.new_version }} - branch: zed-zippy-autobump - committer: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com> - base: main - delete-branch: true - token: ${{ steps.generate-token.outputs.token }} - sign-commits: true - timeout-minutes: 1 - create_version_label: - needs: - - check_bump_needed - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.check_bump_needed.outputs.needs_bump == 'false' - runs-on: namespace-profile-8x16-ubuntu-2204 - steps: - - id: generate-token - name: extension_bump::generate_token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ secrets.app-id }} - private-key: ${{ secrets.app-secret }} - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: extension_bump::create_version_tag - uses: actions/github-script@v7 - with: - script: |- - github.rest.git.createRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: 'refs/tags/v${{ needs.check_bump_needed.outputs.current_version }}', - sha: context.sha - }) - github-token: ${{ steps.generate-token.outputs.token }} - timeout-minutes: 1 -concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }} - cancel-in-progress: true diff --git a/.github/workflows/extension_release.yml b/.github/workflows/extension_release.yml deleted file mode 100644 index 5212a79c3e..0000000000 --- a/.github/workflows/extension_release.yml +++ /dev/null @@ -1,43 +0,0 @@ -# Generated from xtask::workflows::extension_release -# Rebuild with `cargo xtask workflows`. -name: extension_release -on: - workflow_call: - secrets: - app-id: - description: The app ID used to create the PR - required: true - app-secret: - description: The app secret for the corresponding app ID - required: true -jobs: - create_release: - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: namespace-profile-8x16-ubuntu-2204 - steps: - - id: generate-token - name: extension_bump::generate_token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ secrets.app-id }} - private-key: ${{ secrets.app-secret }} - owner: zed-industries - repositories: extensions - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - id: get-extension-id - name: extension_release::get_extension_id - run: | - EXTENSION_ID="$(sed -n 's/id = \"\(.*\)\"/\1/p' < extension.toml)" - - echo "extension_id=${EXTENSION_ID}" >> "$GITHUB_OUTPUT" - shell: bash -euxo pipefail {0} - - name: extension_release::release_action - uses: huacnlee/zed-extension-action@v2 - with: - extension-name: ${{ steps.get-extension-id.outputs.extension_id }} - push-to: zed-industries/extensions - env: - COMMITTER_TOKEN: ${{ steps.generate-token.outputs.token }} diff --git a/.github/workflows/extension_tests.yml b/.github/workflows/extension_tests.yml deleted file mode 100644 index 9f0917e388..0000000000 --- a/.github/workflows/extension_tests.yml +++ /dev/null @@ -1,133 +0,0 @@ -# Generated from xtask::workflows::extension_tests -# Rebuild with `cargo xtask workflows`. -name: extension_tests -env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: '1' - CARGO_INCREMENTAL: '0' - ZED_EXTENSION_CLI_SHA: 7cfce605704d41ca247e3f84804bf323f6c6caaf -on: - workflow_call: {} -jobs: - orchestrate: - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - fetch-depth: ${{ github.ref == 'refs/heads/main' && 2 || 350 }} - - id: filter - name: filter - run: | - if [ -z "$GITHUB_BASE_REF" ]; then - echo "Not in a PR context (i.e., push to main/stable/preview)" - COMPARE_REV="$(git rev-parse HEAD~1)" - else - echo "In a PR context comparing to pull_request.base.ref" - git fetch origin "$GITHUB_BASE_REF" --depth=350 - COMPARE_REV="$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)" - fi - CHANGED_FILES="$(git diff --name-only "$COMPARE_REV" ${{ github.sha }})" - - check_pattern() { - local output_name="$1" - local pattern="$2" - local grep_arg="$3" - - echo "$CHANGED_FILES" | grep "$grep_arg" "$pattern" && \ - echo "${output_name}=true" >> "$GITHUB_OUTPUT" || \ - echo "${output_name}=false" >> "$GITHUB_OUTPUT" - } - - check_pattern "check_rust" '^(Cargo.lock|Cargo.toml|.*\.rs)$' -qP - check_pattern "check_extension" '^.*\.scm$' -qP - shell: bash -euxo pipefail {0} - outputs: - check_rust: ${{ steps.filter.outputs.check_rust }} - check_extension: ${{ steps.filter.outputs.check_extension }} - check_rust: - needs: - - orchestrate - if: needs.orchestrate.outputs.check_rust == 'true' - runs-on: namespace-profile-16x32-ubuntu-2204 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::cache_rust_dependencies_namespace - uses: namespacelabs/nscloud-cache-action@v1 - with: - cache: rust - - name: steps::cargo_fmt - run: cargo fmt --all -- --check - shell: bash -euxo pipefail {0} - - name: extension_tests::run_clippy - run: cargo clippy --release --all-targets --all-features -- --deny warnings - shell: bash -euxo pipefail {0} - - name: steps::cargo_install_nextest - uses: taiki-e/install-action@nextest - - name: steps::cargo_nextest - run: cargo nextest run --workspace --no-fail-fast - shell: bash -euxo pipefail {0} - env: - NEXTEST_NO_TESTS: warn - timeout-minutes: 3 - check_extension: - needs: - - orchestrate - if: needs.orchestrate.outputs.check_extension == 'true' - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - id: cache-zed-extension-cli - name: extension_tests::cache_zed_extension_cli - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 - with: - path: zed-extension - key: zed-extension-${{ env.ZED_EXTENSION_CLI_SHA }} - - name: extension_tests::download_zed_extension_cli - if: steps.cache-zed-extension-cli.outputs.cache-hit != 'true' - run: | - wget --quiet "https://zed-extension-cli.nyc3.digitaloceanspaces.com/$ZED_EXTENSION_CLI_SHA/x86_64-unknown-linux-gnu/zed-extension" - chmod +x zed-extension - shell: bash -euxo pipefail {0} - - name: extension_tests::check - run: | - mkdir -p /tmp/ext-scratch - mkdir -p /tmp/ext-output - ./zed-extension --source-dir . --scratch-dir /tmp/ext-scratch --output-dir /tmp/ext-output - shell: bash -euxo pipefail {0} - timeout-minutes: 2 - tests_pass: - needs: - - orchestrate - - check_rust - - check_extension - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && always() - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - name: run_tests::tests_pass - run: | - set +x - EXIT_CODE=0 - - check_result() { - echo "* $1: $2" - if [[ "$2" != "skipped" && "$2" != "success" ]]; then EXIT_CODE=1; fi - } - - check_result "orchestrate" "${{ needs.orchestrate.result }}" - check_result "check_rust" "${{ needs.check_rust.result }}" - check_result "check_extension" "${{ needs.check_extension.result }}" - - exit $EXIT_CODE - shell: bash -euxo pipefail {0} -concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }} - cancel-in-progress: true diff --git a/.github/workflows/good_first_issue_notifier.yml b/.github/workflows/good_first_issue_notifier.yml deleted file mode 100644 index 1db992502b..0000000000 --- a/.github/workflows/good_first_issue_notifier.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Good First Issue Notifier - -on: - issues: - types: [labeled] - -jobs: - handle-good-first-issue: - if: github.event.label.name == 'good first issue' && github.repository_owner == 'zed-industries' - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - - name: Prepare Discord message - id: prepare-message - env: - ISSUE_TITLE: ${{ github.event.issue.title }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - ISSUE_URL: ${{ github.event.issue.html_url }} - ISSUE_AUTHOR: ${{ github.event.issue.user.login }} - run: | - MESSAGE="[${ISSUE_TITLE} (#${ISSUE_NUMBER})](<${ISSUE_URL}>)" - - { - echo "message<> "$GITHUB_OUTPUT" - - - name: Discord Webhook Action - uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0 - with: - webhook-url: ${{ secrets.DISCORD_WEBHOOK_GOOD_FIRST_ISSUE }} - content: ${{ steps.prepare-message.outputs.message }} diff --git a/.github/workflows/publish_extension_cli.yml b/.github/workflows/publish_extension_cli.yml deleted file mode 100644 index 2daabb0de4..0000000000 --- a/.github/workflows/publish_extension_cli.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Publish zed-extension CLI - -on: - push: - tags: - - extension-cli - -env: - CARGO_TERM_COLOR: always - CARGO_INCREMENTAL: 0 - -jobs: - publish: - name: Publish zed-extension CLI - if: github.repository_owner == 'zed-industries' - runs-on: - - ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - clean: false - - - name: Cache dependencies - uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 - with: - save-if: ${{ github.ref == 'refs/heads/main' }} - cache-provider: "github" - - - name: Configure linux - shell: bash -euxo pipefail {0} - run: script/linux - - - name: Build extension CLI - run: cargo build --release --package extension_cli - - - name: Upload binary - env: - DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} - DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} - run: script/upload-extension-cli ${{ github.sha }} diff --git a/.github/workflows/randomized_tests.yml b/.github/workflows/randomized_tests.yml deleted file mode 100644 index de96c3df78..0000000000 --- a/.github/workflows/randomized_tests.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Randomized Tests - -concurrency: randomized-tests - -on: - push: - branches: - - randomized-tests-runner - # schedule: - # - cron: '0 * * * *' - -env: - CARGO_TERM_COLOR: always - CARGO_INCREMENTAL: 0 - RUST_BACKTRACE: 1 - ZED_SERVER_URL: https://zed.dev - -jobs: - tests: - name: Run randomized tests - if: github.repository_owner == 'zed-industries' - runs-on: - - namespace-profile-16x32-ubuntu-2204 - steps: - - name: Install Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: "18" - - - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - clean: false - - - name: Run randomized tests - run: script/randomized-test-ci diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 7afac285b5..0000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,496 +0,0 @@ -# Generated from xtask::workflows::release -# Rebuild with `cargo xtask workflows`. -name: release -env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: '1' -on: - push: - tags: - - v* -jobs: - run_tests_mac: - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: self-mini-macos - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_cargo_config - run: | - mkdir -p ./../.cargo - cp ./.cargo/ci-config.toml ./../.cargo/config.toml - shell: bash -euxo pipefail {0} - - name: steps::setup_node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: '20' - - name: steps::clippy - run: ./script/clippy - shell: bash -euxo pipefail {0} - - name: steps::clear_target_dir_if_large - run: ./script/clear-target-dir-if-larger-than 300 - shell: bash -euxo pipefail {0} - - name: steps::cargo_nextest - run: cargo nextest run --workspace --no-fail-fast - shell: bash -euxo pipefail {0} - - name: steps::cleanup_cargo_config - if: always() - run: | - rm -rf ./../.cargo - shell: bash -euxo pipefail {0} - timeout-minutes: 60 - run_tests_linux: - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: namespace-profile-16x32-ubuntu-2204 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_cargo_config - run: | - mkdir -p ./../.cargo - cp ./.cargo/ci-config.toml ./../.cargo/config.toml - shell: bash -euxo pipefail {0} - - name: steps::cache_rust_dependencies_namespace - uses: namespacelabs/nscloud-cache-action@v1 - with: - cache: rust - - name: steps::setup_linux - run: ./script/linux - shell: bash -euxo pipefail {0} - - name: steps::install_mold - run: ./script/install-mold - shell: bash -euxo pipefail {0} - - name: steps::download_wasi_sdk - run: ./script/download-wasi-sdk - shell: bash -euxo pipefail {0} - - name: steps::setup_node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: '20' - - name: steps::clippy - run: ./script/clippy - shell: bash -euxo pipefail {0} - - name: steps::cargo_install_nextest - uses: taiki-e/install-action@nextest - - name: steps::clear_target_dir_if_large - run: ./script/clear-target-dir-if-larger-than 250 - shell: bash -euxo pipefail {0} - - name: steps::cargo_nextest - run: cargo nextest run --workspace --no-fail-fast - shell: bash -euxo pipefail {0} - - name: steps::cleanup_cargo_config - if: always() - run: | - rm -rf ./../.cargo - shell: bash -euxo pipefail {0} - timeout-minutes: 60 - run_tests_windows: - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: self-32vcpu-windows-2022 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_cargo_config - run: | - New-Item -ItemType Directory -Path "./../.cargo" -Force - Copy-Item -Path "./.cargo/ci-config.toml" -Destination "./../.cargo/config.toml" - shell: pwsh - - name: steps::setup_node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: '20' - - name: steps::clippy - run: ./script/clippy.ps1 - shell: pwsh - - name: steps::clear_target_dir_if_large - run: ./script/clear-target-dir-if-larger-than.ps1 250 - shell: pwsh - - name: steps::cargo_nextest - run: cargo nextest run --workspace --no-fail-fast - shell: pwsh - - name: steps::cleanup_cargo_config - if: always() - run: | - Remove-Item -Recurse -Path "./../.cargo" -Force -ErrorAction SilentlyContinue - shell: pwsh - timeout-minutes: 60 - check_scripts: - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: run_tests::check_scripts::run_shellcheck - run: ./script/shellcheck-scripts error - shell: bash -euxo pipefail {0} - - id: get_actionlint - name: run_tests::check_scripts::download_actionlint - run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) - shell: bash -euxo pipefail {0} - - name: run_tests::check_scripts::run_actionlint - run: | - ${{ steps.get_actionlint.outputs.executable }} -color - shell: bash -euxo pipefail {0} - - name: run_tests::check_scripts::check_xtask_workflows - run: | - cargo xtask workflows - if ! git diff --exit-code .github; then - echo "Error: .github directory has uncommitted changes after running 'cargo xtask workflows'" - echo "Please run 'cargo xtask workflows' locally and commit the changes" - exit 1 - fi - shell: bash -euxo pipefail {0} - timeout-minutes: 60 - create_draft_release: - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - fetch-depth: 25 - ref: ${{ github.ref }} - - name: script/determine-release-channel - run: script/determine-release-channel - shell: bash -euxo pipefail {0} - - name: mkdir -p target/ - run: mkdir -p target/ - shell: bash -euxo pipefail {0} - - name: release::create_draft_release::generate_release_notes - run: node --redirect-warnings=/dev/null ./script/draft-release-notes "$RELEASE_VERSION" "$RELEASE_CHANNEL" > target/release-notes.md - shell: bash -euxo pipefail {0} - - name: release::create_draft_release::create_release - run: script/create-draft-release target/release-notes.md - shell: bash -euxo pipefail {0} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - timeout-minutes: 60 - bundle_linux_aarch64: - needs: - - run_tests_linux - - check_scripts - runs-on: namespace-profile-8x32-ubuntu-2004-arm-m4 - env: - CARGO_INCREMENTAL: 0 - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_sentry - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b - with: - token: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: steps::setup_linux - run: ./script/linux - shell: bash -euxo pipefail {0} - - name: steps::install_mold - run: ./script/install-mold - shell: bash -euxo pipefail {0} - - name: steps::download_wasi_sdk - run: ./script/download-wasi-sdk - shell: bash -euxo pipefail {0} - - name: ./script/bundle-linux - run: ./script/bundle-linux - shell: bash -euxo pipefail {0} - - name: '@actions/upload-artifact zed-linux-aarch64.tar.gz' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: zed-linux-aarch64.tar.gz - path: target/release/zed-linux-aarch64.tar.gz - if-no-files-found: error - - name: '@actions/upload-artifact zed-remote-server-linux-aarch64.gz' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: zed-remote-server-linux-aarch64.gz - path: target/zed-remote-server-linux-aarch64.gz - if-no-files-found: error - timeout-minutes: 60 - bundle_linux_x86_64: - needs: - - run_tests_linux - - check_scripts - runs-on: namespace-profile-32x64-ubuntu-2004 - env: - CARGO_INCREMENTAL: 0 - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_sentry - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b - with: - token: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: steps::setup_linux - run: ./script/linux - shell: bash -euxo pipefail {0} - - name: steps::install_mold - run: ./script/install-mold - shell: bash -euxo pipefail {0} - - name: steps::download_wasi_sdk - run: ./script/download-wasi-sdk - shell: bash -euxo pipefail {0} - - name: ./script/bundle-linux - run: ./script/bundle-linux - shell: bash -euxo pipefail {0} - - name: '@actions/upload-artifact zed-linux-x86_64.tar.gz' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: zed-linux-x86_64.tar.gz - path: target/release/zed-linux-x86_64.tar.gz - if-no-files-found: error - - name: '@actions/upload-artifact zed-remote-server-linux-x86_64.gz' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: zed-remote-server-linux-x86_64.gz - path: target/zed-remote-server-linux-x86_64.gz - if-no-files-found: error - timeout-minutes: 60 - bundle_mac_aarch64: - needs: - - run_tests_mac - - check_scripts - runs-on: self-mini-macos - env: - CARGO_INCREMENTAL: 0 - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }} - APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} - APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: '20' - - name: steps::setup_sentry - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b - with: - token: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: steps::clear_target_dir_if_large - run: ./script/clear-target-dir-if-larger-than 300 - shell: bash -euxo pipefail {0} - - name: run_bundling::bundle_mac::bundle_mac - run: ./script/bundle-mac aarch64-apple-darwin - shell: bash -euxo pipefail {0} - - name: '@actions/upload-artifact Zed-aarch64.dmg' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: Zed-aarch64.dmg - path: target/aarch64-apple-darwin/release/Zed-aarch64.dmg - if-no-files-found: error - - name: '@actions/upload-artifact zed-remote-server-macos-aarch64.gz' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: zed-remote-server-macos-aarch64.gz - path: target/zed-remote-server-macos-aarch64.gz - if-no-files-found: error - timeout-minutes: 60 - bundle_mac_x86_64: - needs: - - run_tests_mac - - check_scripts - runs-on: self-mini-macos - env: - CARGO_INCREMENTAL: 0 - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }} - APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} - APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: '20' - - name: steps::setup_sentry - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b - with: - token: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: steps::clear_target_dir_if_large - run: ./script/clear-target-dir-if-larger-than 300 - shell: bash -euxo pipefail {0} - - name: run_bundling::bundle_mac::bundle_mac - run: ./script/bundle-mac x86_64-apple-darwin - shell: bash -euxo pipefail {0} - - name: '@actions/upload-artifact Zed-x86_64.dmg' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: Zed-x86_64.dmg - path: target/x86_64-apple-darwin/release/Zed-x86_64.dmg - if-no-files-found: error - - name: '@actions/upload-artifact zed-remote-server-macos-x86_64.gz' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: zed-remote-server-macos-x86_64.gz - path: target/zed-remote-server-macos-x86_64.gz - if-no-files-found: error - timeout-minutes: 60 - bundle_windows_aarch64: - needs: - - run_tests_windows - - check_scripts - runs-on: self-32vcpu-windows-2022 - env: - CARGO_INCREMENTAL: 0 - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} - AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }} - ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }} - CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }} - ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }} - FILE_DIGEST: SHA256 - TIMESTAMP_DIGEST: SHA256 - TIMESTAMP_SERVER: http://timestamp.acs.microsoft.com - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_sentry - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b - with: - token: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: run_bundling::bundle_windows::bundle_windows - run: script/bundle-windows.ps1 -Architecture aarch64 - shell: pwsh - working-directory: ${{ env.ZED_WORKSPACE }} - - name: '@actions/upload-artifact Zed-aarch64.exe' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: Zed-aarch64.exe - path: target/Zed-aarch64.exe - if-no-files-found: error - timeout-minutes: 60 - bundle_windows_x86_64: - needs: - - run_tests_windows - - check_scripts - runs-on: self-32vcpu-windows-2022 - env: - CARGO_INCREMENTAL: 0 - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} - AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }} - ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }} - CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }} - ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }} - FILE_DIGEST: SHA256 - TIMESTAMP_DIGEST: SHA256 - TIMESTAMP_SERVER: http://timestamp.acs.microsoft.com - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_sentry - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b - with: - token: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: run_bundling::bundle_windows::bundle_windows - run: script/bundle-windows.ps1 -Architecture x86_64 - shell: pwsh - working-directory: ${{ env.ZED_WORKSPACE }} - - name: '@actions/upload-artifact Zed-x86_64.exe' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: Zed-x86_64.exe - path: target/Zed-x86_64.exe - if-no-files-found: error - timeout-minutes: 60 - upload_release_assets: - needs: - - create_draft_release - - bundle_linux_aarch64 - - bundle_linux_x86_64 - - bundle_mac_aarch64 - - bundle_mac_x86_64 - - bundle_windows_aarch64 - - bundle_windows_x86_64 - runs-on: namespace-profile-4x8-ubuntu-2204 - steps: - - name: release::download_workflow_artifacts - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 - with: - path: ./artifacts/ - - name: ls -lR ./artifacts - run: ls -lR ./artifacts - shell: bash -euxo pipefail {0} - - name: release::prep_release_artifacts - run: |- - mkdir -p release-artifacts/ - - mv ./artifacts/Zed-aarch64.dmg/Zed-aarch64.dmg release-artifacts/Zed-aarch64.dmg - mv ./artifacts/Zed-x86_64.dmg/Zed-x86_64.dmg release-artifacts/Zed-x86_64.dmg - mv ./artifacts/zed-linux-aarch64.tar.gz/zed-linux-aarch64.tar.gz release-artifacts/zed-linux-aarch64.tar.gz - mv ./artifacts/zed-linux-x86_64.tar.gz/zed-linux-x86_64.tar.gz release-artifacts/zed-linux-x86_64.tar.gz - mv ./artifacts/Zed-x86_64.exe/Zed-x86_64.exe release-artifacts/Zed-x86_64.exe - mv ./artifacts/Zed-aarch64.exe/Zed-aarch64.exe release-artifacts/Zed-aarch64.exe - mv ./artifacts/zed-remote-server-macos-aarch64.gz/zed-remote-server-macos-aarch64.gz release-artifacts/zed-remote-server-macos-aarch64.gz - mv ./artifacts/zed-remote-server-macos-x86_64.gz/zed-remote-server-macos-x86_64.gz release-artifacts/zed-remote-server-macos-x86_64.gz - mv ./artifacts/zed-remote-server-linux-aarch64.gz/zed-remote-server-linux-aarch64.gz release-artifacts/zed-remote-server-linux-aarch64.gz - mv ./artifacts/zed-remote-server-linux-x86_64.gz/zed-remote-server-linux-x86_64.gz release-artifacts/zed-remote-server-linux-x86_64.gz - shell: bash -euxo pipefail {0} - - name: gh release upload "$GITHUB_REF_NAME" --repo=zed-industries/zed release-artifacts/* - run: gh release upload "$GITHUB_REF_NAME" --repo=zed-industries/zed release-artifacts/* - shell: bash -euxo pipefail {0} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - auto_release_preview: - needs: - - upload_release_assets - if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - name: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false - run: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false - shell: bash -euxo pipefail {0} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - notify_on_failure: - needs: - - upload_release_assets - - auto_release_preview - if: failure() - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - name: release::notify_on_failure::notify_slack - run: |- - curl -X POST -H 'Content-type: application/json'\ - --data '{"text":"${{ github.workflow }} failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' "$SLACK_WEBHOOK" - shell: bash -euxo pipefail {0} - env: - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_WORKFLOW_FAILURES }} -concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }} - cancel-in-progress: true diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml deleted file mode 100644 index d76244175a..0000000000 --- a/.github/workflows/release_nightly.yml +++ /dev/null @@ -1,510 +0,0 @@ -# Generated from xtask::workflows::release_nightly -# Rebuild with `cargo xtask workflows`. -name: release_nightly -env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: '1' -on: - push: - tags: - - nightly - schedule: - - cron: 0 7 * * * -jobs: - check_style: - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: self-mini-macos - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - fetch-depth: 0 - - name: steps::cargo_fmt - run: cargo fmt --all -- --check - shell: bash -euxo pipefail {0} - - name: ./script/clippy - run: ./script/clippy - shell: bash -euxo pipefail {0} - timeout-minutes: 60 - run_tests_windows: - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: self-32vcpu-windows-2022 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_cargo_config - run: | - New-Item -ItemType Directory -Path "./../.cargo" -Force - Copy-Item -Path "./.cargo/ci-config.toml" -Destination "./../.cargo/config.toml" - shell: pwsh - - name: steps::setup_node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: '20' - - name: steps::clippy - run: ./script/clippy.ps1 - shell: pwsh - - name: steps::clear_target_dir_if_large - run: ./script/clear-target-dir-if-larger-than.ps1 250 - shell: pwsh - - name: steps::cargo_nextest - run: cargo nextest run --workspace --no-fail-fast - shell: pwsh - - name: steps::cleanup_cargo_config - if: always() - run: | - Remove-Item -Recurse -Path "./../.cargo" -Force -ErrorAction SilentlyContinue - shell: pwsh - timeout-minutes: 60 - bundle_linux_aarch64: - needs: - - check_style - - run_tests_windows - runs-on: namespace-profile-8x32-ubuntu-2004-arm-m4 - env: - CARGO_INCREMENTAL: 0 - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: run_bundling::set_release_channel_to_nightly - run: | - set -eu - version=$(git rev-parse --short HEAD) - echo "Publishing version: ${version} on release channel nightly" - echo "nightly" > crates/zed/RELEASE_CHANNEL - shell: bash -euxo pipefail {0} - - name: steps::setup_sentry - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b - with: - token: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: steps::setup_linux - run: ./script/linux - shell: bash -euxo pipefail {0} - - name: steps::install_mold - run: ./script/install-mold - shell: bash -euxo pipefail {0} - - name: steps::download_wasi_sdk - run: ./script/download-wasi-sdk - shell: bash -euxo pipefail {0} - - name: ./script/bundle-linux - run: ./script/bundle-linux - shell: bash -euxo pipefail {0} - - name: '@actions/upload-artifact zed-linux-aarch64.tar.gz' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: zed-linux-aarch64.tar.gz - path: target/release/zed-linux-aarch64.tar.gz - if-no-files-found: error - - name: '@actions/upload-artifact zed-remote-server-linux-aarch64.gz' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: zed-remote-server-linux-aarch64.gz - path: target/zed-remote-server-linux-aarch64.gz - if-no-files-found: error - timeout-minutes: 60 - bundle_linux_x86_64: - needs: - - check_style - - run_tests_windows - runs-on: namespace-profile-32x64-ubuntu-2004 - env: - CARGO_INCREMENTAL: 0 - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: run_bundling::set_release_channel_to_nightly - run: | - set -eu - version=$(git rev-parse --short HEAD) - echo "Publishing version: ${version} on release channel nightly" - echo "nightly" > crates/zed/RELEASE_CHANNEL - shell: bash -euxo pipefail {0} - - name: steps::setup_sentry - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b - with: - token: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: steps::setup_linux - run: ./script/linux - shell: bash -euxo pipefail {0} - - name: steps::install_mold - run: ./script/install-mold - shell: bash -euxo pipefail {0} - - name: steps::download_wasi_sdk - run: ./script/download-wasi-sdk - shell: bash -euxo pipefail {0} - - name: ./script/bundle-linux - run: ./script/bundle-linux - shell: bash -euxo pipefail {0} - - name: '@actions/upload-artifact zed-linux-x86_64.tar.gz' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: zed-linux-x86_64.tar.gz - path: target/release/zed-linux-x86_64.tar.gz - if-no-files-found: error - - name: '@actions/upload-artifact zed-remote-server-linux-x86_64.gz' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: zed-remote-server-linux-x86_64.gz - path: target/zed-remote-server-linux-x86_64.gz - if-no-files-found: error - timeout-minutes: 60 - bundle_mac_aarch64: - needs: - - check_style - - run_tests_windows - runs-on: self-mini-macos - env: - CARGO_INCREMENTAL: 0 - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }} - APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} - APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: run_bundling::set_release_channel_to_nightly - run: | - set -eu - version=$(git rev-parse --short HEAD) - echo "Publishing version: ${version} on release channel nightly" - echo "nightly" > crates/zed/RELEASE_CHANNEL - shell: bash -euxo pipefail {0} - - name: steps::setup_node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: '20' - - name: steps::setup_sentry - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b - with: - token: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: steps::clear_target_dir_if_large - run: ./script/clear-target-dir-if-larger-than 300 - shell: bash -euxo pipefail {0} - - name: run_bundling::bundle_mac::bundle_mac - run: ./script/bundle-mac aarch64-apple-darwin - shell: bash -euxo pipefail {0} - - name: '@actions/upload-artifact Zed-aarch64.dmg' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: Zed-aarch64.dmg - path: target/aarch64-apple-darwin/release/Zed-aarch64.dmg - if-no-files-found: error - - name: '@actions/upload-artifact zed-remote-server-macos-aarch64.gz' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: zed-remote-server-macos-aarch64.gz - path: target/zed-remote-server-macos-aarch64.gz - if-no-files-found: error - timeout-minutes: 60 - bundle_mac_x86_64: - needs: - - check_style - - run_tests_windows - runs-on: self-mini-macos - env: - CARGO_INCREMENTAL: 0 - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }} - APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} - APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: run_bundling::set_release_channel_to_nightly - run: | - set -eu - version=$(git rev-parse --short HEAD) - echo "Publishing version: ${version} on release channel nightly" - echo "nightly" > crates/zed/RELEASE_CHANNEL - shell: bash -euxo pipefail {0} - - name: steps::setup_node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: '20' - - name: steps::setup_sentry - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b - with: - token: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: steps::clear_target_dir_if_large - run: ./script/clear-target-dir-if-larger-than 300 - shell: bash -euxo pipefail {0} - - name: run_bundling::bundle_mac::bundle_mac - run: ./script/bundle-mac x86_64-apple-darwin - shell: bash -euxo pipefail {0} - - name: '@actions/upload-artifact Zed-x86_64.dmg' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: Zed-x86_64.dmg - path: target/x86_64-apple-darwin/release/Zed-x86_64.dmg - if-no-files-found: error - - name: '@actions/upload-artifact zed-remote-server-macos-x86_64.gz' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: zed-remote-server-macos-x86_64.gz - path: target/zed-remote-server-macos-x86_64.gz - if-no-files-found: error - timeout-minutes: 60 - bundle_windows_aarch64: - needs: - - check_style - - run_tests_windows - runs-on: self-32vcpu-windows-2022 - env: - CARGO_INCREMENTAL: 0 - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} - AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }} - ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }} - CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }} - ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }} - FILE_DIGEST: SHA256 - TIMESTAMP_DIGEST: SHA256 - TIMESTAMP_SERVER: http://timestamp.acs.microsoft.com - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: run_bundling::set_release_channel_to_nightly - run: | - $ErrorActionPreference = "Stop" - $version = git rev-parse --short HEAD - Write-Host "Publishing version: $version on release channel nightly" - "nightly" | Set-Content -Path "crates/zed/RELEASE_CHANNEL" - shell: pwsh - working-directory: ${{ env.ZED_WORKSPACE }} - - name: steps::setup_sentry - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b - with: - token: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: run_bundling::bundle_windows::bundle_windows - run: script/bundle-windows.ps1 -Architecture aarch64 - shell: pwsh - working-directory: ${{ env.ZED_WORKSPACE }} - - name: '@actions/upload-artifact Zed-aarch64.exe' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: Zed-aarch64.exe - path: target/Zed-aarch64.exe - if-no-files-found: error - timeout-minutes: 60 - bundle_windows_x86_64: - needs: - - check_style - - run_tests_windows - runs-on: self-32vcpu-windows-2022 - env: - CARGO_INCREMENTAL: 0 - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} - AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }} - ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }} - CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }} - ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }} - FILE_DIGEST: SHA256 - TIMESTAMP_DIGEST: SHA256 - TIMESTAMP_SERVER: http://timestamp.acs.microsoft.com - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: run_bundling::set_release_channel_to_nightly - run: | - $ErrorActionPreference = "Stop" - $version = git rev-parse --short HEAD - Write-Host "Publishing version: $version on release channel nightly" - "nightly" | Set-Content -Path "crates/zed/RELEASE_CHANNEL" - shell: pwsh - working-directory: ${{ env.ZED_WORKSPACE }} - - name: steps::setup_sentry - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b - with: - token: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: run_bundling::bundle_windows::bundle_windows - run: script/bundle-windows.ps1 -Architecture x86_64 - shell: pwsh - working-directory: ${{ env.ZED_WORKSPACE }} - - name: '@actions/upload-artifact Zed-x86_64.exe' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: Zed-x86_64.exe - path: target/Zed-x86_64.exe - if-no-files-found: error - timeout-minutes: 60 - build_nix_linux_x86_64: - needs: - - check_style - - run_tests_windows - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: namespace-profile-32x64-ubuntu-2004 - 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' - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: nix_build::build_nix::install_nix - uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f - with: - github_access_token: ${{ secrets.GITHUB_TOKEN }} - - name: nix_build::build_nix::cachix_action - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad - with: - name: zed - authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - cachixArgs: -v - - name: nix_build::build_nix::build - run: nix build .#default -L --accept-flake-config - shell: bash -euxo pipefail {0} - timeout-minutes: 60 - continue-on-error: true - build_nix_mac_aarch64: - needs: - - check_style - - run_tests_windows - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: self-mini-macos - 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' - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: nix_build::build_nix::set_path - run: | - echo "/nix/var/nix/profiles/default/bin" >> "$GITHUB_PATH" - echo "/Users/administrator/.nix-profile/bin" >> "$GITHUB_PATH" - shell: bash -euxo pipefail {0} - - name: nix_build::build_nix::cachix_action - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad - with: - name: zed - authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - cachixArgs: -v - - name: nix_build::build_nix::build - run: nix build .#default -L --accept-flake-config - shell: bash -euxo pipefail {0} - - name: nix_build::build_nix::limit_store - run: |- - if [ "$(du -sm /nix/store | cut -f1)" -gt 50000 ]; then - nix-collect-garbage -d || true - fi - shell: bash -euxo pipefail {0} - timeout-minutes: 60 - continue-on-error: true - update_nightly_tag: - needs: - - bundle_linux_aarch64 - - bundle_linux_x86_64 - - bundle_mac_aarch64 - - bundle_mac_x86_64 - - bundle_windows_aarch64 - - bundle_windows_x86_64 - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: namespace-profile-4x8-ubuntu-2204 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - fetch-depth: 0 - - name: release::download_workflow_artifacts - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 - with: - path: ./artifacts/ - - name: ls -lR ./artifacts - run: ls -lR ./artifacts - shell: bash -euxo pipefail {0} - - name: release::prep_release_artifacts - run: |- - mkdir -p release-artifacts/ - - mv ./artifacts/Zed-aarch64.dmg/Zed-aarch64.dmg release-artifacts/Zed-aarch64.dmg - mv ./artifacts/Zed-x86_64.dmg/Zed-x86_64.dmg release-artifacts/Zed-x86_64.dmg - mv ./artifacts/zed-linux-aarch64.tar.gz/zed-linux-aarch64.tar.gz release-artifacts/zed-linux-aarch64.tar.gz - mv ./artifacts/zed-linux-x86_64.tar.gz/zed-linux-x86_64.tar.gz release-artifacts/zed-linux-x86_64.tar.gz - mv ./artifacts/Zed-x86_64.exe/Zed-x86_64.exe release-artifacts/Zed-x86_64.exe - mv ./artifacts/Zed-aarch64.exe/Zed-aarch64.exe release-artifacts/Zed-aarch64.exe - mv ./artifacts/zed-remote-server-macos-aarch64.gz/zed-remote-server-macos-aarch64.gz release-artifacts/zed-remote-server-macos-aarch64.gz - mv ./artifacts/zed-remote-server-macos-x86_64.gz/zed-remote-server-macos-x86_64.gz release-artifacts/zed-remote-server-macos-x86_64.gz - mv ./artifacts/zed-remote-server-linux-aarch64.gz/zed-remote-server-linux-aarch64.gz release-artifacts/zed-remote-server-linux-aarch64.gz - mv ./artifacts/zed-remote-server-linux-x86_64.gz/zed-remote-server-linux-x86_64.gz release-artifacts/zed-remote-server-linux-x86_64.gz - shell: bash -euxo pipefail {0} - - name: ./script/upload-nightly - run: ./script/upload-nightly - shell: bash -euxo pipefail {0} - env: - DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} - DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} - - name: release_nightly::update_nightly_tag_job::update_nightly_tag - run: | - if [ "$(git rev-parse nightly)" = "$(git rev-parse HEAD)" ]; then - echo "Nightly tag already points to current commit. Skipping tagging." - exit 0 - fi - git config user.name github-actions - git config user.email github-actions@github.com - git tag -f nightly - git push origin nightly --force - shell: bash -euxo pipefail {0} - - name: release::create_sentry_release - uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c - with: - environment: production - env: - SENTRY_ORG: zed-dev - SENTRY_PROJECT: zed - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - timeout-minutes: 60 - notify_on_failure: - needs: - - bundle_linux_aarch64 - - bundle_linux_x86_64 - - bundle_mac_aarch64 - - bundle_mac_x86_64 - - bundle_windows_aarch64 - - bundle_windows_x86_64 - if: failure() - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - name: release::notify_on_failure::notify_slack - run: |- - curl -X POST -H 'Content-type: application/json'\ - --data '{"text":"${{ github.workflow }} failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' "$SLACK_WEBHOOK" - shell: bash -euxo pipefail {0} - env: - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_WORKFLOW_FAILURES }} diff --git a/.github/workflows/run_agent_evals.yml b/.github/workflows/run_agent_evals.yml deleted file mode 100644 index 421d5a1c80..0000000000 --- a/.github/workflows/run_agent_evals.yml +++ /dev/null @@ -1,67 +0,0 @@ -# Generated from xtask::workflows::run_agent_evals -# Rebuild with `cargo xtask workflows`. -name: run_agent_evals -env: - CARGO_TERM_COLOR: always - CARGO_INCREMENTAL: '0' - RUST_BACKTRACE: '1' - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }} - GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }} - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_EVAL_TELEMETRY: '1' - MODEL_NAME: ${{ inputs.model_name }} -on: - workflow_dispatch: - inputs: - model_name: - description: model_name - required: true - type: string -jobs: - agent_evals: - runs-on: namespace-profile-16x32-ubuntu-2204 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::cache_rust_dependencies_namespace - uses: namespacelabs/nscloud-cache-action@v1 - with: - cache: rust - - name: steps::setup_linux - run: ./script/linux - shell: bash -euxo pipefail {0} - - name: steps::install_mold - run: ./script/install-mold - shell: bash -euxo pipefail {0} - - name: steps::download_wasi_sdk - run: ./script/download-wasi-sdk - shell: bash -euxo pipefail {0} - - name: steps::setup_cargo_config - run: | - mkdir -p ./../.cargo - cp ./.cargo/ci-config.toml ./../.cargo/config.toml - shell: bash -euxo pipefail {0} - - name: cargo build --package=eval - run: cargo build --package=eval - shell: bash -euxo pipefail {0} - - name: run_agent_evals::agent_evals::run_eval - run: cargo run --package=eval -- --repetitions=8 --concurrency=1 --model "${MODEL_NAME}" - shell: bash -euxo pipefail {0} - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }} - GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }} - - name: steps::cleanup_cargo_config - if: always() - run: | - rm -rf ./../.cargo - shell: bash -euxo pipefail {0} - timeout-minutes: 600 -concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }} - cancel-in-progress: true diff --git a/.github/workflows/run_bundling.yml b/.github/workflows/run_bundling.yml deleted file mode 100644 index f56e56ac7f..0000000000 --- a/.github/workflows/run_bundling.yml +++ /dev/null @@ -1,269 +0,0 @@ -# Generated from xtask::workflows::run_bundling -# Rebuild with `cargo xtask workflows`. -name: run_bundling -env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: '1' -on: - pull_request: - types: - - labeled - - synchronize -jobs: - bundle_linux_aarch64: - if: |- - (github.event.action == 'labeled' && github.event.label.name == 'run-bundling') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling')) - runs-on: namespace-profile-8x32-ubuntu-2004-arm-m4 - env: - CARGO_INCREMENTAL: 0 - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_sentry - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b - with: - token: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: steps::setup_linux - run: ./script/linux - shell: bash -euxo pipefail {0} - - name: steps::install_mold - run: ./script/install-mold - shell: bash -euxo pipefail {0} - - name: steps::download_wasi_sdk - run: ./script/download-wasi-sdk - shell: bash -euxo pipefail {0} - - name: ./script/bundle-linux - run: ./script/bundle-linux - shell: bash -euxo pipefail {0} - - name: '@actions/upload-artifact zed-linux-aarch64.tar.gz' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: zed-linux-aarch64.tar.gz - path: target/release/zed-linux-aarch64.tar.gz - if-no-files-found: error - - name: '@actions/upload-artifact zed-remote-server-linux-aarch64.gz' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: zed-remote-server-linux-aarch64.gz - path: target/zed-remote-server-linux-aarch64.gz - if-no-files-found: error - timeout-minutes: 60 - bundle_linux_x86_64: - if: |- - (github.event.action == 'labeled' && github.event.label.name == 'run-bundling') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling')) - runs-on: namespace-profile-32x64-ubuntu-2004 - env: - CARGO_INCREMENTAL: 0 - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_sentry - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b - with: - token: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: steps::setup_linux - run: ./script/linux - shell: bash -euxo pipefail {0} - - name: steps::install_mold - run: ./script/install-mold - shell: bash -euxo pipefail {0} - - name: steps::download_wasi_sdk - run: ./script/download-wasi-sdk - shell: bash -euxo pipefail {0} - - name: ./script/bundle-linux - run: ./script/bundle-linux - shell: bash -euxo pipefail {0} - - name: '@actions/upload-artifact zed-linux-x86_64.tar.gz' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: zed-linux-x86_64.tar.gz - path: target/release/zed-linux-x86_64.tar.gz - if-no-files-found: error - - name: '@actions/upload-artifact zed-remote-server-linux-x86_64.gz' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: zed-remote-server-linux-x86_64.gz - path: target/zed-remote-server-linux-x86_64.gz - if-no-files-found: error - timeout-minutes: 60 - bundle_mac_aarch64: - if: |- - (github.event.action == 'labeled' && github.event.label.name == 'run-bundling') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling')) - runs-on: self-mini-macos - env: - CARGO_INCREMENTAL: 0 - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }} - APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} - APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: '20' - - name: steps::setup_sentry - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b - with: - token: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: steps::clear_target_dir_if_large - run: ./script/clear-target-dir-if-larger-than 300 - shell: bash -euxo pipefail {0} - - name: run_bundling::bundle_mac::bundle_mac - run: ./script/bundle-mac aarch64-apple-darwin - shell: bash -euxo pipefail {0} - - name: '@actions/upload-artifact Zed-aarch64.dmg' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: Zed-aarch64.dmg - path: target/aarch64-apple-darwin/release/Zed-aarch64.dmg - if-no-files-found: error - - name: '@actions/upload-artifact zed-remote-server-macos-aarch64.gz' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: zed-remote-server-macos-aarch64.gz - path: target/zed-remote-server-macos-aarch64.gz - if-no-files-found: error - timeout-minutes: 60 - bundle_mac_x86_64: - if: |- - (github.event.action == 'labeled' && github.event.label.name == 'run-bundling') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling')) - runs-on: self-mini-macos - env: - CARGO_INCREMENTAL: 0 - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }} - APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} - APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: '20' - - name: steps::setup_sentry - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b - with: - token: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: steps::clear_target_dir_if_large - run: ./script/clear-target-dir-if-larger-than 300 - shell: bash -euxo pipefail {0} - - name: run_bundling::bundle_mac::bundle_mac - run: ./script/bundle-mac x86_64-apple-darwin - shell: bash -euxo pipefail {0} - - name: '@actions/upload-artifact Zed-x86_64.dmg' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: Zed-x86_64.dmg - path: target/x86_64-apple-darwin/release/Zed-x86_64.dmg - if-no-files-found: error - - name: '@actions/upload-artifact zed-remote-server-macos-x86_64.gz' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: zed-remote-server-macos-x86_64.gz - path: target/zed-remote-server-macos-x86_64.gz - if-no-files-found: error - timeout-minutes: 60 - bundle_windows_aarch64: - if: |- - (github.event.action == 'labeled' && github.event.label.name == 'run-bundling') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling')) - runs-on: self-32vcpu-windows-2022 - env: - CARGO_INCREMENTAL: 0 - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} - AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }} - ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }} - CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }} - ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }} - FILE_DIGEST: SHA256 - TIMESTAMP_DIGEST: SHA256 - TIMESTAMP_SERVER: http://timestamp.acs.microsoft.com - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_sentry - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b - with: - token: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: run_bundling::bundle_windows::bundle_windows - run: script/bundle-windows.ps1 -Architecture aarch64 - shell: pwsh - working-directory: ${{ env.ZED_WORKSPACE }} - - name: '@actions/upload-artifact Zed-aarch64.exe' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: Zed-aarch64.exe - path: target/Zed-aarch64.exe - if-no-files-found: error - timeout-minutes: 60 - bundle_windows_x86_64: - if: |- - (github.event.action == 'labeled' && github.event.label.name == 'run-bundling') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling')) - runs-on: self-32vcpu-windows-2022 - env: - CARGO_INCREMENTAL: 0 - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} - AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }} - ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }} - CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }} - ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }} - FILE_DIGEST: SHA256 - TIMESTAMP_DIGEST: SHA256 - TIMESTAMP_SERVER: http://timestamp.acs.microsoft.com - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_sentry - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b - with: - token: ${{ secrets.SENTRY_AUTH_TOKEN }} - - name: run_bundling::bundle_windows::bundle_windows - run: script/bundle-windows.ps1 -Architecture x86_64 - shell: pwsh - working-directory: ${{ env.ZED_WORKSPACE }} - - name: '@actions/upload-artifact Zed-x86_64.exe' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: Zed-x86_64.exe - path: target/Zed-x86_64.exe - if-no-files-found: error - timeout-minutes: 60 -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} - cancel-in-progress: true diff --git a/.github/workflows/run_cron_unit_evals.yml b/.github/workflows/run_cron_unit_evals.yml deleted file mode 100644 index cdfb51cc5b..0000000000 --- a/.github/workflows/run_cron_unit_evals.yml +++ /dev/null @@ -1,77 +0,0 @@ -# Generated from xtask::workflows::run_cron_unit_evals -# Rebuild with `cargo xtask workflows`. -name: run_cron_unit_evals -env: - CARGO_TERM_COLOR: always - CARGO_INCREMENTAL: '0' - RUST_BACKTRACE: '1' - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} -on: - schedule: - - cron: 47 1 * * 2 - workflow_dispatch: {} -jobs: - cron_unit_evals: - runs-on: namespace-profile-16x32-ubuntu-2204 - strategy: - matrix: - model: - - anthropic/claude-sonnet-4-5-latest - - anthropic/claude-opus-4-5-latest - - google/gemini-3-pro - - openai/gpt-5 - fail-fast: false - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_cargo_config - run: | - mkdir -p ./../.cargo - cp ./.cargo/ci-config.toml ./../.cargo/config.toml - shell: bash -euxo pipefail {0} - - name: steps::cache_rust_dependencies_namespace - uses: namespacelabs/nscloud-cache-action@v1 - with: - cache: rust - - name: steps::setup_linux - run: ./script/linux - shell: bash -euxo pipefail {0} - - name: steps::install_mold - run: ./script/install-mold - shell: bash -euxo pipefail {0} - - name: steps::download_wasi_sdk - run: ./script/download-wasi-sdk - shell: bash -euxo pipefail {0} - - name: steps::cargo_install_nextest - uses: taiki-e/install-action@nextest - - name: steps::clear_target_dir_if_large - run: ./script/clear-target-dir-if-larger-than 250 - shell: bash -euxo pipefail {0} - - name: ./script/run-unit-evals - run: ./script/run-unit-evals - shell: bash -euxo pipefail {0} - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }} - GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }} - ZED_AGENT_MODEL: ${{ matrix.model }} - - name: steps::cleanup_cargo_config - if: always() - run: | - rm -rf ./../.cargo - shell: bash -euxo pipefail {0} - - name: run_agent_evals::cron_unit_evals::send_failure_to_slack - if: ${{ failure() }} - uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52 - with: - method: chat.postMessage - token: ${{ secrets.SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN }} - payload: | - channel: C04UDRNNJFQ - text: "Unit Evals Failed: https://github.com/zed-industries/zed/actions/runs/${{ github.run_id }}" -concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }} - cancel-in-progress: true diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml deleted file mode 100644 index 9584d7a0cb..0000000000 --- a/.github/workflows/run_tests.yml +++ /dev/null @@ -1,578 +0,0 @@ -# Generated from xtask::workflows::run_tests -# Rebuild with `cargo xtask workflows`. -name: run_tests -env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: '1' - CARGO_INCREMENTAL: '0' -on: - pull_request: - branches: - - '**' - push: - branches: - - main - - v[0-9]+.[0-9]+.x -jobs: - orchestrate: - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - fetch-depth: ${{ github.ref == 'refs/heads/main' && 2 || 350 }} - - id: filter - name: filter - run: | - if [ -z "$GITHUB_BASE_REF" ]; then - echo "Not in a PR context (i.e., push to main/stable/preview)" - COMPARE_REV="$(git rev-parse HEAD~1)" - else - echo "In a PR context comparing to pull_request.base.ref" - git fetch origin "$GITHUB_BASE_REF" --depth=350 - COMPARE_REV="$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)" - fi - CHANGED_FILES="$(git diff --name-only "$COMPARE_REV" ${{ github.sha }})" - - check_pattern() { - local output_name="$1" - local pattern="$2" - local grep_arg="$3" - - echo "$CHANGED_FILES" | grep "$grep_arg" "$pattern" && \ - echo "${output_name}=true" >> "$GITHUB_OUTPUT" || \ - echo "${output_name}=false" >> "$GITHUB_OUTPUT" - } - - check_pattern "run_action_checks" '^\.github/(workflows/|actions/|actionlint.yml)|tooling/xtask|script/' -qP - check_pattern "run_docs" '^(docs/|crates/.*\.rs)' -qP - check_pattern "run_licenses" '^(Cargo.lock|script/.*licenses)' -qP - check_pattern "run_nix" '^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)' -qP - check_pattern "run_tests" '^(docs/|script/update_top_ranking_issues/|\.github/(ISSUE_TEMPLATE|workflows/(?!run_tests)))' -qvP - shell: bash -euxo pipefail {0} - outputs: - run_action_checks: ${{ steps.filter.outputs.run_action_checks }} - run_docs: ${{ steps.filter.outputs.run_docs }} - run_licenses: ${{ steps.filter.outputs.run_licenses }} - run_nix: ${{ steps.filter.outputs.run_nix }} - run_tests: ${{ steps.filter.outputs.run_tests }} - check_style: - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: namespace-profile-4x8-ubuntu-2204 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::cache_rust_dependencies_namespace - uses: namespacelabs/nscloud-cache-action@v1 - with: - cache: rust - - name: steps::setup_pnpm - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 - with: - version: '9' - - name: ./script/prettier - run: ./script/prettier - shell: bash -euxo pipefail {0} - - name: ./script/check-todos - run: ./script/check-todos - shell: bash -euxo pipefail {0} - - name: ./script/check-keymaps - run: ./script/check-keymaps - shell: bash -euxo pipefail {0} - - name: run_tests::check_style::check_for_typos - uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06 - with: - config: ./typos.toml - - name: steps::cargo_fmt - run: cargo fmt --all -- --check - shell: bash -euxo pipefail {0} - timeout-minutes: 60 - run_tests_windows: - needs: - - orchestrate - if: needs.orchestrate.outputs.run_tests == 'true' - runs-on: self-32vcpu-windows-2022 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_cargo_config - run: | - New-Item -ItemType Directory -Path "./../.cargo" -Force - Copy-Item -Path "./.cargo/ci-config.toml" -Destination "./../.cargo/config.toml" - shell: pwsh - - name: steps::setup_node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: '20' - - name: steps::clippy - run: ./script/clippy.ps1 - shell: pwsh - - name: steps::clear_target_dir_if_large - run: ./script/clear-target-dir-if-larger-than.ps1 250 - shell: pwsh - - name: steps::cargo_nextest - run: cargo nextest run --workspace --no-fail-fast - shell: pwsh - - name: steps::cleanup_cargo_config - if: always() - run: | - Remove-Item -Recurse -Path "./../.cargo" -Force -ErrorAction SilentlyContinue - shell: pwsh - timeout-minutes: 60 - run_tests_linux: - needs: - - orchestrate - if: needs.orchestrate.outputs.run_tests == 'true' - runs-on: namespace-profile-16x32-ubuntu-2204 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_cargo_config - run: | - mkdir -p ./../.cargo - cp ./.cargo/ci-config.toml ./../.cargo/config.toml - shell: bash -euxo pipefail {0} - - name: steps::cache_rust_dependencies_namespace - uses: namespacelabs/nscloud-cache-action@v1 - with: - cache: rust - - name: steps::setup_linux - run: ./script/linux - shell: bash -euxo pipefail {0} - - name: steps::install_mold - run: ./script/install-mold - shell: bash -euxo pipefail {0} - - name: steps::download_wasi_sdk - run: ./script/download-wasi-sdk - shell: bash -euxo pipefail {0} - - name: steps::setup_node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: '20' - - name: steps::clippy - run: ./script/clippy - shell: bash -euxo pipefail {0} - - name: steps::cargo_install_nextest - uses: taiki-e/install-action@nextest - - name: steps::clear_target_dir_if_large - run: ./script/clear-target-dir-if-larger-than 250 - shell: bash -euxo pipefail {0} - - name: steps::cargo_nextest - run: cargo nextest run --workspace --no-fail-fast - shell: bash -euxo pipefail {0} - - name: steps::cleanup_cargo_config - if: always() - run: | - rm -rf ./../.cargo - shell: bash -euxo pipefail {0} - timeout-minutes: 60 - run_tests_mac: - needs: - - orchestrate - if: needs.orchestrate.outputs.run_tests == 'true' - runs-on: self-mini-macos - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_cargo_config - run: | - mkdir -p ./../.cargo - cp ./.cargo/ci-config.toml ./../.cargo/config.toml - shell: bash -euxo pipefail {0} - - name: steps::setup_node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: '20' - - name: steps::clippy - run: ./script/clippy - shell: bash -euxo pipefail {0} - - name: steps::clear_target_dir_if_large - run: ./script/clear-target-dir-if-larger-than 300 - shell: bash -euxo pipefail {0} - - name: steps::cargo_nextest - run: cargo nextest run --workspace --no-fail-fast - shell: bash -euxo pipefail {0} - - name: steps::cleanup_cargo_config - if: always() - run: | - rm -rf ./../.cargo - shell: bash -euxo pipefail {0} - timeout-minutes: 60 - doctests: - needs: - - orchestrate - if: needs.orchestrate.outputs.run_tests == 'true' - runs-on: namespace-profile-16x32-ubuntu-2204 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::cache_rust_dependencies_namespace - uses: namespacelabs/nscloud-cache-action@v1 - with: - cache: rust - - name: steps::setup_linux - run: ./script/linux - shell: bash -euxo pipefail {0} - - name: steps::install_mold - run: ./script/install-mold - shell: bash -euxo pipefail {0} - - name: steps::download_wasi_sdk - run: ./script/download-wasi-sdk - shell: bash -euxo pipefail {0} - - name: steps::setup_cargo_config - run: | - mkdir -p ./../.cargo - cp ./.cargo/ci-config.toml ./../.cargo/config.toml - shell: bash -euxo pipefail {0} - - id: run_doctests - name: run_tests::doctests::run_doctests - run: | - cargo test --workspace --doc --no-fail-fast - shell: bash -euxo pipefail {0} - - name: steps::cleanup_cargo_config - if: always() - run: | - rm -rf ./../.cargo - shell: bash -euxo pipefail {0} - timeout-minutes: 60 - check_workspace_binaries: - needs: - - orchestrate - if: needs.orchestrate.outputs.run_tests == 'true' - runs-on: namespace-profile-8x16-ubuntu-2204 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_cargo_config - run: | - mkdir -p ./../.cargo - cp ./.cargo/ci-config.toml ./../.cargo/config.toml - shell: bash -euxo pipefail {0} - - name: steps::cache_rust_dependencies_namespace - uses: namespacelabs/nscloud-cache-action@v1 - with: - cache: rust - - name: steps::setup_linux - run: ./script/linux - shell: bash -euxo pipefail {0} - - name: steps::install_mold - run: ./script/install-mold - shell: bash -euxo pipefail {0} - - name: steps::download_wasi_sdk - run: ./script/download-wasi-sdk - shell: bash -euxo pipefail {0} - - name: cargo build -p collab - run: cargo build -p collab - shell: bash -euxo pipefail {0} - - name: cargo build --workspace --bins --examples - run: cargo build --workspace --bins --examples - shell: bash -euxo pipefail {0} - - name: steps::cleanup_cargo_config - if: always() - run: | - rm -rf ./../.cargo - shell: bash -euxo pipefail {0} - timeout-minutes: 60 - check_dependencies: - needs: - - orchestrate - if: needs.orchestrate.outputs.run_tests == 'true' - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::cache_rust_dependencies_namespace - uses: namespacelabs/nscloud-cache-action@v1 - with: - cache: rust - - name: run_tests::check_dependencies::install_cargo_machete - uses: clechasseur/rs-cargo@8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386 - with: - command: install - args: cargo-machete@0.7.0 - - name: run_tests::check_dependencies::run_cargo_machete - uses: clechasseur/rs-cargo@8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386 - with: - command: machete - - name: run_tests::check_dependencies::check_cargo_lock - run: cargo update --locked --workspace - shell: bash -euxo pipefail {0} - - name: run_tests::check_dependencies::check_vulnerable_dependencies - if: github.event_name == 'pull_request' - uses: actions/dependency-review-action@67d4f4bd7a9b17a0db54d2a7519187c65e339de8 - with: - license-check: false - timeout-minutes: 60 - check_docs: - needs: - - orchestrate - if: needs.orchestrate.outputs.run_docs == 'true' - runs-on: namespace-profile-8x16-ubuntu-2204 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_cargo_config - run: | - mkdir -p ./../.cargo - cp ./.cargo/ci-config.toml ./../.cargo/config.toml - shell: bash -euxo pipefail {0} - - name: steps::cache_rust_dependencies_namespace - uses: namespacelabs/nscloud-cache-action@v1 - with: - cache: rust - - name: run_tests::check_docs::lychee_link_check - uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 - with: - args: --no-progress --exclude '^http' './docs/src/**/*' - fail: true - jobSummary: false - - name: steps::setup_linux - run: ./script/linux - shell: bash -euxo pipefail {0} - - name: steps::install_mold - run: ./script/install-mold - shell: bash -euxo pipefail {0} - - name: steps::download_wasi_sdk - run: ./script/download-wasi-sdk - shell: bash -euxo pipefail {0} - - name: run_tests::check_docs::install_mdbook - uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 - with: - mdbook-version: 0.4.37 - - name: run_tests::check_docs::build_docs - run: | - mkdir -p target/deploy - mdbook build ./docs --dest-dir=../target/deploy/docs/ - shell: bash -euxo pipefail {0} - - name: run_tests::check_docs::lychee_link_check - uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 - with: - args: --no-progress --exclude '^http' 'target/deploy/docs' - fail: true - jobSummary: false - timeout-minutes: 60 - check_licenses: - needs: - - orchestrate - if: needs.orchestrate.outputs.run_licenses == 'true' - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::cache_rust_dependencies_namespace - uses: namespacelabs/nscloud-cache-action@v1 - with: - cache: rust - - name: ./script/check-licenses - run: ./script/check-licenses - shell: bash -euxo pipefail {0} - - name: ./script/generate-licenses - run: ./script/generate-licenses - shell: bash -euxo pipefail {0} - check_scripts: - needs: - - orchestrate - if: needs.orchestrate.outputs.run_action_checks == 'true' - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: run_tests::check_scripts::run_shellcheck - run: ./script/shellcheck-scripts error - shell: bash -euxo pipefail {0} - - id: get_actionlint - name: run_tests::check_scripts::download_actionlint - run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) - shell: bash -euxo pipefail {0} - - name: run_tests::check_scripts::run_actionlint - run: | - ${{ steps.get_actionlint.outputs.executable }} -color - shell: bash -euxo pipefail {0} - - name: run_tests::check_scripts::check_xtask_workflows - run: | - cargo xtask workflows - if ! git diff --exit-code .github; then - echo "Error: .github directory has uncommitted changes after running 'cargo xtask workflows'" - echo "Please run 'cargo xtask workflows' locally and commit the changes" - exit 1 - fi - shell: bash -euxo pipefail {0} - timeout-minutes: 60 - build_nix_linux_x86_64: - needs: - - orchestrate - if: needs.orchestrate.outputs.run_nix == 'true' - runs-on: namespace-profile-32x64-ubuntu-2004 - 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' - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: nix_build::build_nix::install_nix - uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f - with: - github_access_token: ${{ secrets.GITHUB_TOKEN }} - - name: nix_build::build_nix::cachix_action - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad - with: - name: zed - authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - cachixArgs: -v - pushFilter: -zed-editor-[0-9.]*-nightly - - name: nix_build::build_nix::build - run: nix build .#debug -L --accept-flake-config - shell: bash -euxo pipefail {0} - timeout-minutes: 60 - continue-on-error: true - build_nix_mac_aarch64: - needs: - - orchestrate - if: needs.orchestrate.outputs.run_nix == 'true' - runs-on: self-mini-macos - 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' - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: nix_build::build_nix::set_path - run: | - echo "/nix/var/nix/profiles/default/bin" >> "$GITHUB_PATH" - echo "/Users/administrator/.nix-profile/bin" >> "$GITHUB_PATH" - shell: bash -euxo pipefail {0} - - name: nix_build::build_nix::cachix_action - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad - with: - name: zed - authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - cachixArgs: -v - pushFilter: -zed-editor-[0-9.]*-nightly - - name: nix_build::build_nix::build - run: nix build .#debug -L --accept-flake-config - shell: bash -euxo pipefail {0} - - name: nix_build::build_nix::limit_store - run: |- - if [ "$(du -sm /nix/store | cut -f1)" -gt 50000 ]; then - nix-collect-garbage -d || true - fi - shell: bash -euxo pipefail {0} - timeout-minutes: 60 - continue-on-error: true - check_postgres_and_protobuf_migrations: - needs: - - orchestrate - if: needs.orchestrate.outputs.run_tests == 'true' - runs-on: namespace-profile-16x32-ubuntu-2204 - env: - GIT_AUTHOR_NAME: Protobuf Action - GIT_AUTHOR_EMAIL: ci@zed.dev - GIT_COMMITTER_NAME: Protobuf Action - GIT_COMMITTER_EMAIL: ci@zed.dev - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - fetch-depth: 0 - - name: run_tests::check_postgres_and_protobuf_migrations::remove_untracked_files - run: git clean -df - shell: bash -euxo pipefail {0} - - name: run_tests::check_postgres_and_protobuf_migrations::ensure_fresh_merge - run: | - if [ -z "$GITHUB_BASE_REF" ]; - then - echo "BUF_BASE_BRANCH=$(git merge-base origin/main HEAD)" >> "$GITHUB_ENV" - else - git checkout -B temp - git merge -q "origin/$GITHUB_BASE_REF" -m "merge main into temp" - echo "BUF_BASE_BRANCH=$GITHUB_BASE_REF" >> "$GITHUB_ENV" - fi - shell: bash -euxo pipefail {0} - - name: run_tests::check_postgres_and_protobuf_migrations::bufbuild_setup_action - uses: bufbuild/buf-setup-action@v1 - with: - version: v1.29.0 - github_token: ${{ secrets.GITHUB_TOKEN }} - - name: run_tests::check_postgres_and_protobuf_migrations::bufbuild_breaking_action - uses: bufbuild/buf-breaking-action@v1 - with: - input: crates/proto/proto/ - against: https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/ - timeout-minutes: 60 - tests_pass: - needs: - - orchestrate - - check_style - - run_tests_windows - - run_tests_linux - - run_tests_mac - - doctests - - check_workspace_binaries - - check_dependencies - - check_docs - - check_licenses - - check_scripts - - build_nix_linux_x86_64 - - build_nix_mac_aarch64 - if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && always() - runs-on: namespace-profile-2x4-ubuntu-2404 - steps: - - name: run_tests::tests_pass - run: | - set +x - EXIT_CODE=0 - - check_result() { - echo "* $1: $2" - if [[ "$2" != "skipped" && "$2" != "success" ]]; then EXIT_CODE=1; fi - } - - check_result "orchestrate" "${{ needs.orchestrate.result }}" - check_result "check_style" "${{ needs.check_style.result }}" - check_result "run_tests_windows" "${{ needs.run_tests_windows.result }}" - check_result "run_tests_linux" "${{ needs.run_tests_linux.result }}" - check_result "run_tests_mac" "${{ needs.run_tests_mac.result }}" - check_result "doctests" "${{ needs.doctests.result }}" - check_result "check_workspace_binaries" "${{ needs.check_workspace_binaries.result }}" - check_result "check_dependencies" "${{ needs.check_dependencies.result }}" - check_result "check_docs" "${{ needs.check_docs.result }}" - check_result "check_licenses" "${{ needs.check_licenses.result }}" - check_result "check_scripts" "${{ needs.check_scripts.result }}" - check_result "build_nix_linux_x86_64" "${{ needs.build_nix_linux_x86_64.result }}" - check_result "build_nix_mac_aarch64" "${{ needs.build_nix_mac_aarch64.result }}" - - exit $EXIT_CODE - shell: bash -euxo pipefail {0} -concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }} - cancel-in-progress: true diff --git a/.github/workflows/run_unit_evals.yml b/.github/workflows/run_unit_evals.yml deleted file mode 100644 index 8f64a5c8bc..0000000000 --- a/.github/workflows/run_unit_evals.yml +++ /dev/null @@ -1,69 +0,0 @@ -# Generated from xtask::workflows::run_unit_evals -# Rebuild with `cargo xtask workflows`. -name: run_unit_evals -env: - CARGO_TERM_COLOR: always - CARGO_INCREMENTAL: '0' - RUST_BACKTRACE: '1' - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_EVAL_TELEMETRY: '1' - MODEL_NAME: ${{ inputs.model_name }} -on: - workflow_dispatch: - inputs: - model_name: - description: model_name - required: true - type: string - commit_sha: - description: commit_sha - required: true - type: string -jobs: - run_unit_evals: - runs-on: namespace-profile-16x32-ubuntu-2204 - steps: - - name: steps::checkout_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - - name: steps::setup_cargo_config - run: | - mkdir -p ./../.cargo - cp ./.cargo/ci-config.toml ./../.cargo/config.toml - shell: bash -euxo pipefail {0} - - name: steps::cache_rust_dependencies_namespace - uses: namespacelabs/nscloud-cache-action@v1 - with: - cache: rust - - name: steps::setup_linux - run: ./script/linux - shell: bash -euxo pipefail {0} - - name: steps::install_mold - run: ./script/install-mold - shell: bash -euxo pipefail {0} - - name: steps::download_wasi_sdk - run: ./script/download-wasi-sdk - shell: bash -euxo pipefail {0} - - name: steps::cargo_install_nextest - uses: taiki-e/install-action@nextest - - name: steps::clear_target_dir_if_large - run: ./script/clear-target-dir-if-larger-than 250 - shell: bash -euxo pipefail {0} - - name: ./script/run-unit-evals - run: ./script/run-unit-evals - shell: bash -euxo pipefail {0} - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }} - GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }} - UNIT_EVAL_COMMIT: ${{ inputs.commit_sha }} - - name: steps::cleanup_cargo_config - if: always() - run: | - rm -rf ./../.cargo - shell: bash -euxo pipefail {0} -concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.run_id }} - cancel-in-progress: true diff --git a/.gitignore b/.gitignore index ccf4f471d5..d3157ef5a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,44 +1,33 @@ -**/*.db -**/cargo-target +# Build artifacts **/target -**/venv -**/.direnv +**/cargo-target *.wasm -*.xcodeproj + +# IDE and editor .DS_Store -.blob_store -.build -.envrc -.flatpak-builder .idea -.netrc -*.pyc -.pytest_cache -.swiftpm -.swiftpm/config/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.venv .vscode -.wrangler -.perf-runs -/assets/*licenses.* -/crates/collab/seed.json -/crates/theme/schemas/theme.json -/crates/zed/resources/flatpak/flatpak-cargo-sources.json -/crates/project_panel/benches/linux_repo_snapshot.txt -/dev.zed.Zed*.json -/node_modules/ -/plugins/bin -/script/node_modules -/snap -/zed.xcworkspace +*.xcodeproj +*.xcworkspace DerivedData/ -Packages xcuserdata/ -# Don't commit any secrets to the repo. +# Python +**/venv +.venv +*.pyc +.pytest_cache + +# Nix +**/.direnv +.envrc +/result + +# Environment and secrets .env .env.secret.toml +.netrc -# `nix build` output -/result +# Misc +**/*.db +.build diff --git a/.mailmap b/.mailmap deleted file mode 100644 index db4632d6ca..0000000000 --- a/.mailmap +++ /dev/null @@ -1,147 +0,0 @@ -# Canonical author names and emails. -# -# Use this to provide a canonical name and email for an author when their -# name is not always written the same way and/or they have commits authored -# under different email addresses. -# -# Reference: https://git-scm.com/docs/gitmailmap - -# Keep these entries sorted alphabetically. -# In Zed: `editor: sort lines case insensitive` - -Agus Zubiaga -Agus Zubiaga -Alex Viscreanu -Alex Viscreanu -Alexander Mankuta -Alexander Mankuta -amtoaer -amtoaer -Andrei Zvonimir Crnković -Andrei Zvonimir Crnković -Angelk90 -Angelk90 <20476002+Angelk90@users.noreply.github.com> -Antonio Scandurra -Antonio Scandurra -Ben Kunkle -Ben Kunkle -Bennet Bo Fenner -Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> -Bennet Bo Fenner -Boris Cherny -Boris Cherny -Brian Tan -Chris Hayes -Christian Bergschneider -Christian Bergschneider -Conrad Irwin -Conrad Irwin -Dairon Medina -Danilo Leal -Danilo Leal <67129314+danilo-leal@users.noreply.github.com> -Edwin Aronsson <75266237+4teapo@users.noreply.github.com> -Elvis Pranskevichus -Elvis Pranskevichus -Evren Sen -Evren Sen <146845123+evrensen467@users.noreply.github.com> -Evren Sen <146845123+evrsen@users.noreply.github.com> -Fernando Tagawa -Fernando Tagawa -Finn Evers -Finn Evers <75036051+MrSubidubi@users.noreply.github.com> -Finn Evers -Gowtham K <73059450+dovakin0007@users.noreply.github.com> -Greg Morenz -Greg Morenz -Ihnat Aŭtuška -Ivan Žužak -Ivan Žužak -Joseph T. Lyons -Joseph T. Lyons -Julia -Julia <30666851+ForLoveOfCats@users.noreply.github.com> -Kaylee Simmons -Kaylee Simmons -Kaylee Simmons -Kaylee Simmons -Kirill Bulatov -Kirill Bulatov -Kyle Caverly -Kyle Caverly -Lilith Iris -Lilith Iris <83819417+Irilith@users.noreply.github.com> -LoganDark -LoganDark -LoganDark -Marko Kungla -Marko Kungla -Marshall Bowers -Marshall Bowers -Marshall Bowers -Matt Fellenz -Matt Fellenz -Max Brunsfeld -Max Brunsfeld -Max Linke -Max Linke -Michael Sloan -Michael Sloan -Michael Sloan -Mikayla Maki -Mikayla Maki -Mikayla Maki -Morgan Krey -Muhammad Talal Anwar -Muhammad Talal Anwar -Nate Butler -Nate Butler -Nathan Sobo -Nathan Sobo -Nathan Sobo -Nigel Jose -Nigel Jose -Peter Tripp -Peter Tripp -Petros Amoiridis -Petros Amoiridis -Piotr Osiewicz -Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> -Pocæus -Pocæus -Rashid Almheiri -Rashid Almheiri <69181766+huwaireb@users.noreply.github.com> -Richard Feldman -Richard Feldman -Robert Clover -Robert Clover -Roy Williams -Roy Williams -Sebastijan Kelnerič -Sebastijan Kelnerič -Sergey Onufrienko -Shish -Shish -Smit Barmase <0xtimsb@gmail.com> -Smit Barmase <0xtimsb@gmail.com> -Thomas -Thomas -Thomas -Thomas Heartman -Thomas Heartman -Thomas Mickley-Doyle -Thomas Mickley-Doyle -Thorben Kröger -Thorben Kröger -Thorsten Ball -Thorsten Ball -Thorsten Ball -Tristan Hume -Tristan Hume -Uladzislau Kaminski -Uladzislau Kaminski -Vitaly Slobodin -Vitaly Slobodin -Will Bradley -Will Bradley -WindSoilder -张小白 <364772080@qq.com> diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 963354f231..0000000000 --- a/.prettierrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "printWidth": 120 -} diff --git a/.zed/debug.json b/.zed/debug.json deleted file mode 100644 index 6f4e936c80..0000000000 --- a/.zed/debug.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "label": "Debug Zed (CodeLLDB)", - "adapter": "CodeLLDB", - "build": { - "label": "Build Zed", - "command": "cargo", - "args": ["build"] - } - }, - { - "label": "Debug Zed (GDB)", - "adapter": "GDB", - "build": { - "label": "Build Zed", - "command": "cargo", - "args": ["build"] - } - } -] diff --git a/.zed/settings.json b/.zed/settings.json index 2760be9581..4f7a5a6245 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -2,55 +2,27 @@ "languages": { "Markdown": { "tab_size": 2, - "formatter": "prettier" }, "TOML": { - "formatter": "prettier", - "format_on_save": "off" + "format_on_save": "off", }, "YAML": { "tab_size": 2, - "formatter": "prettier" }, "JSON": { "tab_size": 2, "preferred_line_length": 120, - "formatter": "prettier" }, "JSONC": { "tab_size": 2, "preferred_line_length": 120, - "formatter": "prettier" }, - "JavaScript": { - "tab_size": 2, - "formatter": "prettier" - }, - "CSS": { - "tab_size": 2, - "formatter": "prettier" - }, - "Rust": { - "tasks": { - "variables": { - "RUST_DEFAULT_PACKAGE_RUN": "zed" - } - } - } - }, - "file_types": { - "Dockerfile": ["Dockerfile*[!dockerignore]"], - "JSONC": ["**/assets/**/*.json", "renovate.json"], - "Git Ignore": ["dockerignore"] }, "hard_tabs": false, "formatter": "auto", "remove_trailing_whitespace_on_save": true, "ensure_final_newline_on_save": true, "file_scan_exclusions": [ - "crates/agent/src/edit_agent/evals/fixtures", - "crates/eval/worktrees/", - "crates/eval/repos/", "**/.git", "**/.svn", "**/.hg", @@ -58,7 +30,6 @@ "**/CVS", "**/.DS_Store", "**/Thumbs.db", - "**/.classpath", - "**/.settings" - ] + "**/target", + ], } diff --git a/.zed/tasks.json b/.zed/tasks.json index b6a9d9f4cd..57f6b28e12 100644 --- a/.zed/tasks.json +++ b/.zed/tasks.json @@ -4,13 +4,20 @@ "command": "./script/clippy", "args": [], "allow_concurrent_runs": true, - "use_new_terminal": false + "use_new_terminal": false, }, { - "label": "cargo run --profile release-fast", + "label": "cargo check gpui", "command": "cargo", - "args": ["run", "--profile", "release-fast"], + "args": ["check", "--package", "gpui"], "allow_concurrent_runs": true, - "use_new_terminal": false - } + "use_new_terminal": false, + }, + { + "label": "cargo test gpui", + "command": "cargo", + "args": ["test", "--package", "gpui"], + "allow_concurrent_runs": false, + "use_new_terminal": false, + }, ] diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 8d064b64f5..0000000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,3 +0,0 @@ -# Code of Conduct - -The Code of Conduct for this repository can be found online at [zed.dev/code-of-conduct](https://zed.dev/code-of-conduct). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 9cbac4af2b..0000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,92 +0,0 @@ -# Contributing to Zed - -Thank you for helping us make Zed better! - -All activity in Zed forums is subject to our [Code of -Conduct](https://zed.dev/code-of-conduct). Additionally, contributors must sign -our [Contributor License Agreement](https://zed.dev/cla) before their -contributions can be merged. - -## Contribution ideas - -Zed is a large project with a number of priorities. We spend most of -our time working on what we believe the product needs, but we also love working -with the community to improve the product in ways we haven't thought of (or had time to get to yet!) - -In particular we love PRs that are: - -- Fixes to existing bugs and issues. -- Small enhancements to existing features, particularly to make them work for more people. -- Small extra features, like keybindings or actions you miss from other editors or extensions. -- Work towards shipping larger features on our roadmap. - -If you're looking for concrete ideas: - -- Our [top-ranking issues](https://github.com/zed-industries/zed/issues/5393) based on votes by the community. -- Our [public roadmap](https://zed.dev/roadmap) contains a rough outline of our near-term priorities for Zed. - -## Sending changes - -The Zed culture values working code and synchronous conversations over long -discussion threads. - -The best way to get us to take a look at a proposed change is to send a pull -request. We will get back to you (though this sometimes takes longer than we'd -like, sorry). - -Although we will take a look, we tend to only merge about half the PRs that are -submitted. If you'd like your PR to have the best chance of being merged: - -- Include a clear description of what you're solving, and why it's important to you. -- Include tests. -- If it changes the UI, attach screenshots or screen recordings. - -The internal advice for reviewers is as follows: - -- If the fix/feature is obviously great, and the code is great. Hit merge. -- If the fix/feature is obviously great, and the code is nearly great. Send PR comments, or offer to pair to get things perfect. -- If the fix/feature is not obviously great, or the code needs rewriting from scratch. Close the PR with a thank you and some explanation. - -If you need more feedback from us: the best way is to be responsive to -Github comments, or to offer up time to pair with us. - -If you are making a larger change, or need advice on how to finish the change -you're making, please open the PR early. We would love to help you get -things right, and it's often easier to see how to solve a problem before the -diff gets too big. - -## Things we will (probably) not merge - -Although there are few hard and fast rules, typically we don't merge: - -- Anything that can be provided by an extension. For example a new language, or theme. For adding themes or support for a new language to Zed, check out our [docs on developing extensions](https://zed.dev/docs/extensions/developing-extensions). -- New file icons. Zed's default icon theme consists of icons that are hand-designed to fit together in a cohesive manner, please don't submit PRs with off-the-shelf SVGs. -- Giant refactorings. -- Non-trivial changes with no tests. -- Stylistic code changes that do not alter any app logic. Reducing allocations, removing `.unwrap()`s, fixing typos is great; making code "more readable" — maybe not so much. -- Features where (in our subjective opinion) the extra complexity isn't worth it for the number of people who will benefit. -- Anything that seems completely AI generated. - -## Bird's-eye view of Zed - -We suggest you keep the [Zed glossary](docs/src/development/glossary.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase. - -Zed is made up of several smaller crates - let's go over those you're most likely to interact with: - -- [`gpui`](/crates/gpui) is a GPU-accelerated UI framework which provides all of the building blocks for Zed. **We recommend familiarizing yourself with the root level GPUI documentation.** -- [`editor`](/crates/editor) contains the core `Editor` type that drives both the code editor and all various input fields within Zed. It also handles a display layer for LSP features such as Inlay Hints or code completions. -- [`project`](/crates/project) manages files and navigation within the filetree. It is also Zed's side of communication with LSP. -- [`workspace`](/crates/workspace) handles local state serialization and groups projects together. -- [`vim`](/crates/vim) is a thin implementation of Vim workflow over `editor`. -- [`lsp`](/crates/lsp) handles communication with external LSP server. -- [`language`](/crates/language) drives `editor`'s understanding of language - from providing a list of symbols to the syntax map. -- [`collab`](/crates/collab) is the collaboration server itself, driving the collaboration features such as project sharing. -- [`rpc`](/crates/rpc) defines messages to be exchanged with collaboration server. -- [`theme`](/crates/theme) defines the theme system and provides a default theme. -- [`ui`](/crates/ui) is a collection of UI components and common patterns used throughout Zed. -- [`cli`](/crates/cli) is the CLI crate which invokes the Zed binary. -- [`zed`](/crates/zed) is where all things come together, and the `main` entry point for Zed. - -## Packaging Zed - -Check our [notes for packaging Zed](https://zed.dev/docs/development/linux#notes-for-packaging-zed). diff --git a/Cargo.lock b/Cargo.lock index 981f59cb5e..b5196d351d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,121 +2,13 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "acp_thread" -version = "0.1.0" -dependencies = [ - "action_log", - "agent-client-protocol", - "agent_settings", - "anyhow", - "buffer_diff", - "collections", - "editor", - "env_logger 0.11.8", - "file_icons", - "futures 0.3.31", - "gpui", - "indoc", - "itertools 0.14.0", - "language", - "language_model", - "markdown", - "parking_lot", - "portable-pty", - "project", - "prompt_store", - "rand 0.9.2", - "serde", - "serde_json", - "settings", - "smol", - "task", - "telemetry", - "tempfile", - "terminal", - "ui", - "url", - "util", - "uuid", - "watch", - "zlog", -] - -[[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", -] - -[[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.9.2", - "serde_json", - "settings", - "telemetry", - "text", - "util", - "watch", - "zlog", -] - -[[package]] -name = "activity_indicator" -version = "0.1.0" -dependencies = [ - "anyhow", - "auto_update", - "editor", - "extension_host", - "fs", - "futures 0.3.31", - "gpui", - "language", - "project", - "proto", - "release_channel", - "semver", - "smallvec", - "ui", - "util", - "workspace", -] - [[package]] name = "addr2line" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ - "gimli 0.32.3", + "gimli", ] [[package]] @@ -137,287 +29,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "agent" -version = "0.1.0" -dependencies = [ - "acp_thread", - "action_log", - "agent-client-protocol", - "agent_servers", - "agent_settings", - "anyhow", - "assistant_text_thread", - "chrono", - "client", - "clock", - "cloud_llm_client", - "collections", - "context_server", - "ctor", - "db", - "derive_more 0.99.20", - "editor", - "env_logger 0.11.8", - "eval_utils", - "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", - "pretty_assertions", - "project", - "prompt_store", - "rand 0.9.2", - "regex", - "reqwest_client", - "rust-embed", - "schemars", - "serde", - "serde_json", - "settings", - "smallvec", - "smol", - "sqlez", - "streaming_diff", - "strsim", - "task", - "telemetry", - "tempfile", - "terminal", - "text", - "theme", - "thiserror 2.0.17", - "tree-sitter-rust", - "ui", - "unindent", - "util", - "uuid", - "watch", - "web_search", - "worktree", - "zed_env_vars", - "zlog", - "zstd", -] - -[[package]] -name = "agent-client-protocol" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ffe7d502c1e451aafc5aff655000f84d09c9af681354ac0012527009b1af13" -dependencies = [ - "agent-client-protocol-schema", - "anyhow", - "async-broadcast", - "async-trait", - "derive_more 2.0.1", - "futures 0.3.31", - "log", - "serde", - "serde_json", -] - -[[package]] -name = "agent-client-protocol-schema" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8af81cc2d5c3f9c04f73db452efd058333735ba9d51c2cf7ef33c9fee038e7e6" -dependencies = [ - "anyhow", - "derive_more 2.0.1", - "schemars", - "serde", - "serde_json", - "strum 0.27.2", -] - -[[package]] -name = "agent_servers" -version = "0.1.0" -dependencies = [ - "acp_thread", - "acp_tools", - "action_log", - "agent-client-protocol", - "anyhow", - "async-trait", - "client", - "collections", - "env_logger 0.11.8", - "fs", - "futures 0.3.31", - "gpui", - "gpui_tokio", - "http_client", - "indoc", - "language", - "language_model", - "language_models", - "libc", - "log", - "nix 0.29.0", - "project", - "release_channel", - "reqwest_client", - "serde", - "serde_json", - "settings", - "smol", - "task", - "tempfile", - "terminal", - "thiserror 2.0.17", - "ui", - "util", - "uuid", - "watch", -] - -[[package]] -name = "agent_settings" -version = "0.1.0" -dependencies = [ - "anyhow", - "cloud_llm_client", - "collections", - "convert_case 0.8.0", - "fs", - "gpui", - "language_model", - "paths", - "project", - "schemars", - "serde", - "serde_json", - "serde_json_lenient", - "settings", - "util", -] - -[[package]] -name = "agent_ui" -version = "0.1.0" -dependencies = [ - "acp_thread", - "action_log", - "agent", - "agent-client-protocol", - "agent_servers", - "agent_settings", - "ai_onboarding", - "anyhow", - "arrayvec", - "assistant_slash_command", - "assistant_slash_commands", - "assistant_text_thread", - "async-fs", - "audio", - "buffer_diff", - "chrono", - "client", - "clock", - "cloud_llm_client", - "collections", - "command_palette_hooks", - "component", - "context_server", - "db", - "editor", - "eval_utils", - "extension", - "extension_host", - "feature_flags", - "file_icons", - "fs", - "futures 0.3.31", - "fuzzy", - "gpui", - "gpui_tokio", - "html_to_markdown", - "http_client", - "image", - "indoc", - "itertools 0.14.0", - "jsonschema", - "language", - "language_model", - "language_models", - "languages", - "log", - "lsp", - "markdown", - "menu", - "multi_buffer", - "notifications", - "ordered-float 2.10.1", - "parking_lot", - "paths", - "picker", - "postage", - "pretty_assertions", - "project", - "prompt_store", - "proto", - "rand 0.9.2", - "release_channel", - "reqwest_client", - "rope", - "rules_library", - "schemars", - "search", - "semver", - "serde", - "serde_json", - "serde_json_lenient", - "settings", - "smol", - "streaming_diff", - "task", - "telemetry", - "telemetry_events", - "terminal", - "terminal_view", - "text", - "theme", - "time", - "time_format", - "tree-sitter-md", - "ui", - "ui_input", - "unindent", - "url", - "util", - "uuid", - "watch", - "workspace", - "zed_actions", -] - -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.16", - "once_cell", - "version_check", -] - [[package]] name = "ahash" version = "0.8.12" @@ -426,9 +37,7 @@ checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "const-random", - "getrandom 0.3.4", "once_cell", - "serde", "version_check", "zerocopy", ] @@ -442,53 +51,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ai_onboarding" -version = "0.1.0" -dependencies = [ - "client", - "cloud_llm_client", - "component", - "gpui", - "language_model", - "serde", - "smallvec", - "telemetry", - "ui", - "zed_actions", -] - -[[package]] -name = "alacritty_terminal" -version = "0.25.1-rc1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb5f4f1ef69bdb8b2095ddd14b09dd74ee0303aae8bd5372667a54cff689a1b" -dependencies = [ - "base64 0.22.1", - "bitflags 2.9.4", - "home", - "libc", - "log", - "miow", - "parking_lot", - "piper", - "polling", - "regex-automata", - "rustix 1.1.2", - "rustix-openpty", - "serde", - "signal-hook", - "unicode-width", - "vte", - "windows-sys 0.59.0", -] - -[[package]] -name = "aliasable" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" - [[package]] name = "aligned-vec" version = "0.6.4" @@ -498,68 +60,6 @@ dependencies = [ "equator", ] -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "alsa" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" -dependencies = [ - "alsa-sys", - "bitflags 2.9.4", - "cfg-if", - "libc", -] - -[[package]] -name = "alsa-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "ambient-authority" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" - -[[package]] -name = "ammonia" -version = "4.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6" -dependencies = [ - "cssparser", - "html5ever 0.35.0", - "maplit", - "tendril", - "url", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -569,12 +69,6 @@ dependencies = [ "libc", ] -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - [[package]] name = "anstream" version = "0.6.21" @@ -625,51 +119,17 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "anthropic" -version = "0.1.0" -dependencies = [ - "anyhow", - "chrono", - "futures 0.3.31", - "http_client", - "schemars", - "serde", - "serde_json", - "settings", - "strum 0.27.2", - "thiserror 2.0.17", -] - -[[package]] -name = "any_vec" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34cd60c5e3152cef0a592f1b296f1cc93715d89d2551d85315828c3a09575ff4" - [[package]] name = "anyhow" version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" -[[package]] -name = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits", -] - [[package]] name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] [[package]] name = "arg_enum_proc_macro" @@ -682,12 +142,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "arraydeque" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" - [[package]] name = "arrayref" version = "0.3.9" @@ -699,9 +153,6 @@ name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -dependencies = [ - "serde", -] [[package]] name = "as-raw-xcb-connection" @@ -709,12 +160,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" -[[package]] -name = "ascii" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" - [[package]] name = "ash" version = "0.38.0+1.3.281" @@ -774,146 +219,6 @@ dependencies = [ "zbus", ] -[[package]] -name = "askpass" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures 0.3.31", - "gpui", - "log", - "net", - "smol", - "tempfile", - "util", - "windows 0.61.3", - "zeroize", -] - -[[package]] -name = "assets" -version = "0.1.0" -dependencies = [ - "anyhow", - "gpui", - "rust-embed", -] - -[[package]] -name = "assistant_slash_command" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "collections", - "derive_more 0.99.20", - "extension", - "futures 0.3.31", - "gpui", - "language", - "language_model", - "parking_lot", - "pretty_assertions", - "serde", - "serde_json", - "ui", - "util", - "workspace", -] - -[[package]] -name = "assistant_slash_commands" -version = "0.1.0" -dependencies = [ - "anyhow", - "assistant_slash_command", - "chrono", - "collections", - "context_server", - "editor", - "feature_flags", - "fs", - "futures 0.3.31", - "fuzzy", - "globset", - "gpui", - "html_to_markdown", - "http_client", - "language", - "pretty_assertions", - "project", - "prompt_store", - "rope", - "serde", - "serde_json", - "settings", - "smol", - "text", - "ui", - "util", - "workspace", - "worktree", - "zlog", -] - -[[package]] -name = "assistant_text_thread" -version = "0.1.0" -dependencies = [ - "agent_settings", - "anyhow", - "assistant_slash_command", - "assistant_slash_commands", - "chrono", - "client", - "clock", - "cloud_llm_client", - "collections", - "context_server", - "fs", - "futures 0.3.31", - "fuzzy", - "gpui", - "indoc", - "itertools 0.14.0", - "language", - "language_model", - "log", - "open_ai", - "parking_lot", - "paths", - "pretty_assertions", - "project", - "prompt_store", - "proto", - "rand 0.9.2", - "regex", - "rpc", - "serde", - "serde_json", - "settings", - "smallvec", - "smol", - "telemetry_events", - "text", - "ui", - "unindent", - "util", - "uuid", - "workspace", - "zed_env_vars", -] - -[[package]] -name = "async-attributes" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" -dependencies = [ - "quote", - "syn 1.0.109", -] - [[package]] name = "async-broadcast" version = "0.7.2" @@ -949,19 +254,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-compat" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" -dependencies = [ - "futures-core", - "futures-io", - "once_cell", - "pin-project-lite", - "tokio", -] - [[package]] name = "async-compression" version = "0.4.32" @@ -975,16 +267,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-dispatcher" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8bff43baa5b0ca8f8bcd7f9338f5d30fbd75236a2aa89130a7c5121a06d6ca" -dependencies = [ - "async-task", - "futures-lite 1.13.0", -] - [[package]] name = "async-executor" version = "1.13.3" @@ -1005,7 +287,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" dependencies = [ - "async-lock 3.4.1", + "async-lock", "blocking", "futures-lite 2.6.1", ] @@ -1019,7 +301,7 @@ dependencies = [ "async-channel 2.5.0", "async-executor", "async-io", - "async-lock 3.4.1", + "async-lock", "blocking", "futures-lite 2.6.1", "once_cell", @@ -1043,15 +325,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - [[package]] name = "async-lock" version = "3.4.1" @@ -1074,15 +347,6 @@ dependencies = [ "futures-lite 2.6.1", ] -[[package]] -name = "async-pipe" -version = "0.1.3" -source = "git+https://github.com/zed-industries/async-pipe-rs?rev=82d00a04211cf4e1236029aa03e6b6ce2a74c553#82d00a04211cf4e1236029aa03e6b6ce2a74c553" -dependencies = [ - "futures 0.3.31", - "log", -] - [[package]] name = "async-process" version = "2.5.0" @@ -1091,7 +355,7 @@ checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ "async-channel 2.5.0", "async-io", - "async-lock 3.4.1", + "async-lock", "async-signal", "async-task", "blocking", @@ -1119,7 +383,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" dependencies = [ "async-io", - "async-lock 3.4.1", + "async-lock", "atomic-waker", "cfg-if", "futures-core", @@ -1136,11 +400,10 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" dependencies = [ - "async-attributes", "async-channel 1.9.0", "async-global-executor", "async-io", - "async-lock 3.4.1", + "async-lock", "async-process", "crossbeam-utils", "futures-channel", @@ -1158,28 +421,6 @@ dependencies = [ "wasm-bindgen-futures", ] -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "async-tar" version = "0.5.1" @@ -1211,25 +452,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "async-tungstenite" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee88b4c88ac8c9ea446ad43498955750a4bbe64c4392f21ccfe5d952865e318f" -dependencies = [ - "atomic-waker", - "futures-core", - "futures-io", - "futures-task", - "futures-util", - "log", - "pin-project-lite", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.2", - "tungstenite 0.27.0", -] - [[package]] name = "async_zip" version = "0.0.18" @@ -1243,28 +465,6 @@ dependencies = [ "thiserror 2.0.17", ] -[[package]] -name = "asynchronous-codec" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" -dependencies = [ - "bytes 1.10.1", - "futures-sink", - "futures-util", - "memchr", - "pin-project-lite", -] - -[[package]] -name = "atoi" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" -dependencies = [ - "num-traits", -] - [[package]] name = "atomic" version = "0.5.3" @@ -1277,99 +477,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "audio" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-tar", - "collections", - "crossbeam", - "denoise", - "gpui", - "libwebrtc", - "log", - "parking_lot", - "rodio", - "serde", - "settings", - "smol", - "thiserror 2.0.17", - "util", -] - -[[package]] -name = "auditable-serde" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" -dependencies = [ - "semver", - "serde", - "serde_json", - "topological-sort", -] - -[[package]] -name = "auto_update" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "clock", - "ctor", - "db", - "futures 0.3.31", - "gpui", - "http_client", - "log", - "parking_lot", - "paths", - "release_channel", - "semver", - "serde", - "serde_json", - "settings", - "smol", - "tempfile", - "util", - "which 6.0.3", - "workspace", - "zlog", -] - -[[package]] -name = "auto_update_helper" -version = "0.1.0" -dependencies = [ - "anyhow", - "log", - "simplelog", - "tempfile", - "windows 0.61.3", - "winresource", -] - -[[package]] -name = "auto_update_ui" -version = "0.1.0" -dependencies = [ - "anyhow", - "auto_update", - "client", - "editor", - "gpui", - "http_client", - "markdown_preview", - "release_channel", - "semver", - "serde", - "serde_json", - "smol", - "util", - "workspace", -] - [[package]] name = "autocfg" version = "1.5.0" @@ -1385,7 +492,7 @@ dependencies = [ "anyhow", "arrayvec", "log", - "nom 7.1.3", + "nom", "num-rational", "v_frame", ] @@ -1399,48 +506,6 @@ dependencies = [ "arrayvec", ] -[[package]] -name = "aws-config" -version = "1.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37cf2b6af2a95a20e266782b4f76f1a5e12bf412a9db2de9c1e9123b9d8c0ad8" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sdk-sso", - "aws-sdk-ssooidc", - "aws-sdk-sts", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes 1.10.1", - "fastrand 2.3.0", - "hex", - "http 1.3.1", - "ring", - "time", - "tokio", - "tracing", - "url", - "zeroize", -] - -[[package]] -name = "aws-credential-types" -version = "1.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf26925f4a5b59eb76722b63c2892b1d70d06fa053c72e4a100ec308c1d47bc" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "zeroize", -] - [[package]] name = "aws-lc-rs" version = "1.14.1" @@ -1448,7 +513,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d" dependencies = [ "aws-lc-sys", - "untrusted 0.7.1", "zeroize", ] @@ -1465,502 +529,6 @@ dependencies = [ "fs_extra", ] -[[package]] -name = "aws-runtime" -version = "1.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa006bb32360ed90ac51203feafb9d02e3d21046e1fd3a450a404b90ea73e5d" -dependencies = [ - "aws-credential-types", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes 1.10.1", - "fastrand 2.3.0", - "http 0.2.12", - "http-body 0.4.6", - "percent-encoding", - "pin-project-lite", - "tracing", - "uuid", -] - -[[package]] -name = "aws-sdk-bedrockruntime" -version = "1.109.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbfdfd941dcb253c17bf70baddbf1e5b22f19e29d313d2e049bad4b1dadb2011" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes 1.10.1", - "fastrand 2.3.0", - "http 0.2.12", - "hyper 0.14.32", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-kinesis" -version = "1.91.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "699a3d645a2ab5cb12ca02eb23979753953414429fd6584ea8841af6bc4e0516" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes 1.10.1", - "fastrand 2.3.0", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-s3" -version = "1.108.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200be4aed61e3c0669f7268bacb768f283f1c32a7014ce57225e1160be2f6ccb" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-checksums", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "bytes 1.10.1", - "fastrand 2.3.0", - "hex", - "hmac", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "lru", - "percent-encoding", - "regex-lite", - "sha2", - "tracing", - "url", -] - -[[package]] -name = "aws-sdk-sso" -version = "1.86.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0abbfab841446cce6e87af853a3ba2cc1bc9afcd3f3550dd556c43d434c86d" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes 1.10.1", - "fastrand 2.3.0", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-ssooidc" -version = "1.88.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a68d675582afea0e94d38b6ca9c5aaae4ca14f1d36faa6edb19b42e687e70d7" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes 1.10.1", - "fastrand 2.3.0", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-sts" -version = "1.88.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d30990923f4f675523c51eb1c0dec9b752fb267b36a61e83cbc219c9d86da715" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-query", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "fastrand 2.3.0", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sigv4" -version = "1.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffc03068fbb9c8dd5ce1c6fb240678a5cffb86fb2b7b1985c999c4b83c8df68" -dependencies = [ - "aws-credential-types", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes 1.10.1", - "crypto-bigint 0.5.5", - "form_urlencoded", - "hex", - "hmac", - "http 0.2.12", - "http 1.3.1", - "p256", - "percent-encoding", - "ring", - "sha2", - "subtle", - "time", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-smithy-async" -version = "1.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "127fcfad33b7dfc531141fda7e1c402ac65f88aca5511a4d31e2e3d2cd01ce9c" -dependencies = [ - "futures-util", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "aws-smithy-checksums" -version = "0.63.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165d8583d8d906e2fb5511d29201d447cc710864f075debcdd9c31c265412806" -dependencies = [ - "aws-smithy-http", - "aws-smithy-types", - "bytes 1.10.1", - "crc-fast", - "hex", - "http 0.2.12", - "http-body 0.4.6", - "md-5", - "pin-project-lite", - "sha1", - "sha2", - "tracing", -] - -[[package]] -name = "aws-smithy-eventstream" -version = "0.60.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9656b85088f8d9dc7ad40f9a6c7228e1e8447cdf4b046c87e152e0805dea02fa" -dependencies = [ - "aws-smithy-types", - "bytes 1.10.1", - "crc32fast", -] - -[[package]] -name = "aws-smithy-http" -version = "0.62.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3feafd437c763db26aa04e0cc7591185d0961e64c61885bece0fb9d50ceac671" -dependencies = [ - "aws-smithy-eventstream", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes 1.10.1", - "bytes-utils", - "futures-core", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "percent-encoding", - "pin-project-lite", - "pin-utils", - "tracing", -] - -[[package]] -name = "aws-smithy-http-client" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1053b5e587e6fa40ce5a79ea27957b04ba660baa02b28b7436f64850152234f1" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "h2 0.3.27", - "h2 0.4.12", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper 1.7.0", - "hyper-rustls 0.24.2", - "hyper-rustls 0.27.7", - "hyper-util", - "pin-project-lite", - "rustls 0.21.12", - "rustls 0.23.33", - "rustls-native-certs 0.8.2", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.2", - "tower 0.5.2", - "tracing", -] - -[[package]] -name = "aws-smithy-json" -version = "0.61.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff418fc8ec5cadf8173b10125f05c2e7e1d46771406187b2c878557d4503390" -dependencies = [ - "aws-smithy-types", -] - -[[package]] -name = "aws-smithy-observability" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1881b1ea6d313f9890710d65c158bdab6fb08c91ea825f74c1c8c357baf4cc" -dependencies = [ - "aws-smithy-runtime-api", -] - -[[package]] -name = "aws-smithy-query" -version = "0.60.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d28a63441360c477465f80c7abac3b9c4d075ca638f982e605b7dc2a2c7156c9" -dependencies = [ - "aws-smithy-types", - "urlencoding", -] - -[[package]] -name = "aws-smithy-runtime" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ab99739082da5347660c556689256438defae3bcefd66c52b095905730e404" -dependencies = [ - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-http-client", - "aws-smithy-observability", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes 1.10.1", - "fastrand 2.3.0", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "http-body 1.0.1", - "pin-project-lite", - "pin-utils", - "tokio", - "tracing", -] - -[[package]] -name = "aws-smithy-runtime-api" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3683c5b152d2ad753607179ed71988e8cfd52964443b4f74fd8e552d0bbfeb46" -dependencies = [ - "aws-smithy-async", - "aws-smithy-types", - "bytes 1.10.1", - "http 0.2.12", - "http 1.3.1", - "pin-project-lite", - "tokio", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-smithy-types" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f5b3a7486f6690ba25952cabf1e7d75e34d69eaff5081904a47bc79074d6457" -dependencies = [ - "base64-simd", - "bytes 1.10.1", - "bytes-utils", - "futures-core", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "http-body 1.0.1", - "http-body-util", - "itoa", - "num-integer", - "pin-project-lite", - "pin-utils", - "ryu", - "serde", - "time", - "tokio", - "tokio-util", -] - -[[package]] -name = "aws-smithy-xml" -version = "0.60.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c34127e8c624bc2999f3b657e749c1393bedc9cd97b92a804db8ced4d2e163" -dependencies = [ - "xmlparser", -] - -[[package]] -name = "aws-types" -version = "1.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2fd329bf0e901ff3f60425691410c69094dc2a1f34b331f37bfc4e9ac1565a1" -dependencies = [ - "aws-credential-types", - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "rustc_version", - "tracing", -] - -[[package]] -name = "aws_http_client" -version = "0.1.0" -dependencies = [ - "aws-smithy-runtime-api", - "aws-smithy-types", - "http_client", -] - -[[package]] -name = "axum" -version = "0.6.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" -dependencies = [ - "async-trait", - "axum-core", - "base64 0.21.7", - "bitflags 1.3.2", - "bytes 1.10.1", - "futures-util", - "headers", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sha1", - "sync_wrapper 0.1.2", - "tokio", - "tokio-tungstenite 0.20.1", - "tower 0.4.13", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" -dependencies = [ - "async-trait", - "bytes 1.10.1", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "mime", - "rustversion", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-extra" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a320103719de37b7b4da4c8eb629d4573f6bcfd3dfe80d3208806895ccf81d" -dependencies = [ - "axum", - "bytes 1.10.1", - "futures-util", - "http 0.2.12", - "mime", - "pin-project-lite", - "serde", - "serde_json", - "tokio", - "tower 0.4.13", - "tower-http 0.3.5", - "tower-layer", - "tower-service", -] - [[package]] name = "backtrace" version = "0.3.76" @@ -1971,83 +539,17 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object 0.37.3", + "object", "rustc-demangle", "windows-link 0.2.1", ] -[[package]] -name = "base16ct" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" -dependencies = [ - "outref", - "vsimd", -] - -[[package]] -name = "base64ct" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" - -[[package]] -name = "bedrock" -version = "0.1.0" -dependencies = [ - "anyhow", - "aws-sdk-bedrockruntime", - "aws-smithy-types", - "futures 0.3.31", - "schemars", - "serde", - "serde_json", - "strum 0.27.2", - "thiserror 2.0.17", -] - -[[package]] -name = "bigdecimal" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a22f228ab7a1b23027ccc6c350b72868017af7ea8356fbdf19f8d991c690013" -dependencies = [ - "autocfg", - "libm", - "num-bigint", - "num-integer", - "num-traits", - "serde", -] - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - [[package]] name = "bindgen" version = "0.71.1" @@ -2120,9 +622,6 @@ name = "bitflags" version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" -dependencies = [ - "serde", -] [[package]] name = "bitstream-io" @@ -2130,18 +629,6 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "blade-graphics" version = "0.7.0" @@ -2152,7 +639,7 @@ dependencies = [ "ash-window", "bitflags 2.9.4", "bytemuck", - "codespan-reporting 0.12.0", + "codespan-reporting", "glow", "gpu-alloc", "gpu-alloc-ash", @@ -2246,84 +733,6 @@ dependencies = [ "piper", ] -[[package]] -name = "bm25" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cbd8ffdfb7b4c2ff038726178a780a94f90525ed0ad264c0afaa75dd8c18a64" -dependencies = [ - "cached", - "deunicode", - "fxhash", - "rust-stemmers", - "stop-words", - "unicode-segmentation", -] - -[[package]] -name = "borrow-or-share" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" - -[[package]] -name = "borsh" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" -dependencies = [ - "borsh-derive", - "cfg_aliases 0.2.1", -] - -[[package]] -name = "borsh-derive" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" -dependencies = [ - "once_cell", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "breadcrumbs" -version = "0.1.0" -dependencies = [ - "editor", - "gpui", - "itertools 0.14.0", - "settings", - "theme", - "ui", - "workspace", - "zed_actions", -] - -[[package]] -name = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - [[package]] name = "bstr" version = "1.12.0" @@ -2331,34 +740,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", - "regex-automata", "serde", ] -[[package]] -name = "buffer_diff" -version = "0.1.0" -dependencies = [ - "anyhow", - "clock", - "ctor", - "futures 0.3.31", - "git2", - "gpui", - "language", - "log", - "pretty_assertions", - "rand 0.9.2", - "rope", - "serde_json", - "settings", - "sum_tree", - "text", - "unindent", - "util", - "zlog", -] - [[package]] name = "built" version = "0.7.7" @@ -2370,43 +754,6 @@ name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" -dependencies = [ - "allocator-api2", -] - -[[package]] -name = "by_address" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" - -[[package]] -name = "bytecheck" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "simdutf8", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "bytecount" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "bytemuck" @@ -2440,110 +787,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" -[[package]] -name = "bytes" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" -dependencies = [ - "byteorder", - "iovec", -] - [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" -[[package]] -name = "bytes-utils" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" -dependencies = [ - "bytes 1.10.1", - "either", -] - -[[package]] -name = "bzip2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" -dependencies = [ - "bzip2-sys", - "libc", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.13+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" -dependencies = [ - "cc", - "pkg-config", -] - -[[package]] -name = "cached" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "801927ee168e17809ab8901d9f01f700cd7d8d6a6527997fee44e4b0327a253c" -dependencies = [ - "ahash 0.8.12", - "cached_proc_macro", - "cached_proc_macro_types", - "hashbrown 0.15.5", - "once_cell", - "thiserror 2.0.17", - "web-time", -] - -[[package]] -name = "cached_proc_macro" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9225bdcf4e4a9a4c08bf16607908eb2fbf746828d5e0b5e019726dbf6571f201" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "cached_proc_macro_types" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" - -[[package]] -name = "call" -version = "0.1.0" -dependencies = [ - "anyhow", - "audio", - "client", - "collections", - "feature_flags", - "fs", - "futures 0.3.31", - "gpui", - "gpui_tokio", - "http_client", - "language", - "livekit_client", - "log", - "postage", - "project", - "serde", - "settings", - "telemetry", - "util", -] - [[package]] name = "calloop" version = "0.14.3" @@ -2568,179 +817,6 @@ dependencies = [ "wayland-client", ] -[[package]] -name = "camino" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" -dependencies = [ - "serde_core", -] - -[[package]] -name = "candle-core" -version = "0.9.1" -source = "git+https://github.com/zed-industries/candle?branch=9.1-patched#724d75eb3deebefe83f2a7381a45d4fac6eda383" -dependencies = [ - "byteorder", - "float8", - "gemm 0.17.1", - "half", - "memmap2", - "num-traits", - "num_cpus", - "rand 0.9.2", - "rand_distr", - "rayon", - "safetensors", - "thiserror 1.0.69", - "ug", - "yoke 0.7.5", - "zip 1.1.4", -] - -[[package]] -name = "candle-nn" -version = "0.9.1" -source = "git+https://github.com/zed-industries/candle?branch=9.1-patched#724d75eb3deebefe83f2a7381a45d4fac6eda383" -dependencies = [ - "candle-core", - "half", - "libc", - "num-traits", - "rayon", - "safetensors", - "serde", - "thiserror 1.0.69", -] - -[[package]] -name = "candle-onnx" -version = "0.9.1" -source = "git+https://github.com/zed-industries/candle?branch=9.1-patched#724d75eb3deebefe83f2a7381a45d4fac6eda383" -dependencies = [ - "candle-core", - "candle-nn", - "prost 0.12.6", -] - -[[package]] -name = "cap-fs-ext" -version = "3.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e41cc18551193fe8fa6f15c1e3c799bc5ec9e2cfbfaa8ed46f37013e3e6c173c" -dependencies = [ - "cap-primitives", - "cap-std", - "io-lifetimes", - "windows-sys 0.59.0", -] - -[[package]] -name = "cap-net-ext" -version = "3.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f83833816c66c986e913b22ac887cec216ea09301802054316fc5301809702c" -dependencies = [ - "cap-primitives", - "cap-std", - "rustix 1.1.2", - "smallvec", -] - -[[package]] -name = "cap-primitives" -version = "3.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a1e394ed14f39f8bc26f59d4c0c010dbe7f0a1b9bafff451b1f98b67c8af62a" -dependencies = [ - "ambient-authority", - "fs-set-times", - "io-extras", - "io-lifetimes", - "ipnet", - "maybe-owned", - "rustix 1.1.2", - "rustix-linux-procfs", - "windows-sys 0.59.0", - "winx", -] - -[[package]] -name = "cap-rand" -version = "3.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0acb89ccf798a28683f00089d0630dfaceec087234eae0d308c05ddeaa941b40" -dependencies = [ - "ambient-authority", - "rand 0.8.5", -] - -[[package]] -name = "cap-std" -version = "3.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c0355ca583dd58f176c3c12489d684163861ede3c9efa6fd8bba314c984189" -dependencies = [ - "cap-primitives", - "io-extras", - "io-lifetimes", - "rustix 1.1.2", -] - -[[package]] -name = "cap-time-ext" -version = "3.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "491af520b8770085daa0466978c75db90368c71896523f2464214e38359b1a5b" -dependencies = [ - "ambient-authority", - "cap-primitives", - "iana-time-zone", - "once_cell", - "rustix 1.1.2", - "winx", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror 2.0.17", -] - -[[package]] -name = "cargo_toml" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fbd1fe9db3ebf71b89060adaf7b0504c2d6a425cf061313099547e382c2e472" -dependencies = [ - "serde", - "toml 0.8.23", -] - -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - [[package]] name = "cbc" version = "0.1.2" @@ -2792,7 +868,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom 7.1.3", + "nom", ] [[package]] @@ -2802,7 +878,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" dependencies = [ "smallvec", - "target-lexicon 0.12.16", + "target-lexicon", ] [[package]] @@ -2811,12 +887,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - [[package]] name = "cfg_aliases" version = "0.2.1" @@ -2832,29 +902,6 @@ dependencies = [ "libc", ] -[[package]] -name = "channel" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "clock", - "collections", - "futures 0.3.31", - "gpui", - "http_client", - "language", - "log", - "postage", - "release_channel", - "rpc", - "semver", - "settings", - "text", - "time", - "util", -] - [[package]] name = "chrono" version = "0.4.42" @@ -2869,39 +916,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "chunked_transfer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" - -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - [[package]] name = "cipher" version = "0.4.4" @@ -2930,187 +944,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "clap" -version = "4.5.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", - "terminal_size", -] - -[[package]] -name = "clap_complete" -version = "4.5.59" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2348487adcd4631696ced64ccdb40d38ac4d31cae7f2eec8817fcea1b9d1c43c" -dependencies = [ - "clap", -] - -[[package]] -name = "clap_derive" -version = "4.5.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "clap_lex" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" - -[[package]] -name = "cli" -version = "0.1.0" -dependencies = [ - "anyhow", - "askpass", - "clap", - "collections", - "core-foundation 0.10.0", - "core-services", - "exec", - "fork", - "ipc-channel", - "parking_lot", - "paths", - "plist", - "rayon", - "release_channel", - "serde", - "serde_json", - "tempfile", - "util", - "windows 0.61.3", -] - -[[package]] -name = "client" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-tungstenite", - "base64 0.22.1", - "chrono", - "clock", - "cloud_api_client", - "cloud_llm_client", - "collections", - "credentials_provider", - "derive_more 0.99.20", - "feature_flags", - "fs", - "futures 0.3.31", - "gpui", - "gpui_tokio", - "http_client", - "http_client_tls", - "httparse", - "log", - "objc2-foundation", - "parking_lot", - "paths", - "postage", - "rand 0.9.2", - "regex", - "release_channel", - "rpc", - "rustls-pki-types", - "semver", - "serde", - "serde_json", - "serde_urlencoded", - "settings", - "sha2", - "smol", - "telemetry", - "telemetry_events", - "text", - "thiserror 2.0.17", - "time", - "tiny_http", - "tokio", - "tokio-native-tls", - "tokio-rustls 0.26.2", - "tokio-socks", - "url", - "util", - "windows 0.61.3", - "worktree", -] - -[[package]] -name = "clock" -version = "0.1.0" -dependencies = [ - "parking_lot", - "serde", - "smallvec", -] - -[[package]] -name = "cloud_api_client" -version = "0.1.0" -dependencies = [ - "anyhow", - "cloud_api_types", - "futures 0.3.31", - "gpui", - "gpui_tokio", - "http_client", - "parking_lot", - "serde_json", - "yawc", -] - -[[package]] -name = "cloud_api_types" -version = "0.1.0" -dependencies = [ - "anyhow", - "chrono", - "ciborium", - "cloud_llm_client", - "pretty_assertions", - "serde", - "serde_json", -] - -[[package]] -name = "cloud_llm_client" -version = "0.1.0" -dependencies = [ - "anyhow", - "chrono", - "indoc", - "pretty_assertions", - "serde", - "serde_json", - "strum 0.27.2", - "uuid", -] - [[package]] name = "cmake" version = "0.1.54" @@ -3120,15 +953,6 @@ dependencies = [ "cc", ] -[[package]] -name = "cobs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" -dependencies = [ - "thiserror 2.0.17", -] - [[package]] name = "cocoa" version = "0.25.0" @@ -3140,7 +964,7 @@ dependencies = [ "cocoa-foundation 0.1.2", "core-foundation 0.9.4", "core-graphics 0.23.2", - "foreign-types 0.5.0", + "foreign-types", "libc", "objc", ] @@ -3156,7 +980,7 @@ dependencies = [ "cocoa-foundation 0.2.0", "core-foundation 0.10.0", "core-graphics 0.24.0", - "foreign-types 0.5.0", + "foreign-types", "libc", "objc", ] @@ -3200,183 +1024,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "codespan-reporting" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba7a06c0b31fff5ff2e1e7d37dbf940864e2a974b336e1a2938d10af6e8fb283" -dependencies = [ - "serde", - "termcolor", - "unicode-width", -] - -[[package]] -name = "codestral" -version = "0.1.0" -dependencies = [ - "anyhow", - "edit_prediction_context", - "edit_prediction_types", - "futures 0.3.31", - "gpui", - "http_client", - "language", - "language_models", - "log", - "mistral", - "serde", - "serde_json", - "smol", - "text", -] - -[[package]] -name = "collab" -version = "0.44.0" -dependencies = [ - "agent_settings", - "anyhow", - "assistant_slash_command", - "assistant_text_thread", - "async-trait", - "async-tungstenite", - "audio", - "aws-config", - "aws-sdk-kinesis", - "aws-sdk-s3", - "axum", - "axum-extra", - "base64 0.22.1", - "buffer_diff", - "call", - "channel", - "chrono", - "client", - "clock", - "collab_ui", - "collections", - "command_palette_hooks", - "context_server", - "ctor", - "dap", - "dap-types", - "dap_adapters", - "dashmap 6.1.0", - "debugger_ui", - "editor", - "envy", - "extension", - "file_finder", - "fs", - "futures 0.3.31", - "git", - "git_hosting_providers", - "git_ui", - "gpui", - "gpui_tokio", - "hex", - "http_client", - "hyper 0.14.32", - "indoc", - "language", - "language_model", - "livekit_api", - "livekit_client", - "log", - "lsp", - "menu", - "multi_buffer", - "nanoid", - "node_runtime", - "notifications", - "parking_lot", - "pretty_assertions", - "project", - "prometheus", - "prompt_store", - "prost 0.9.0", - "rand 0.9.2", - "recent_projects", - "release_channel", - "remote", - "remote_server", - "reqwest 0.11.27", - "reqwest_client", - "rpc", - "scrypt", - "sea-orm", - "sea-orm-macros", - "semver", - "serde", - "serde_json", - "session", - "settings", - "sha2", - "smol", - "sqlx", - "strum 0.27.2", - "subtle", - "supermaven_api", - "task", - "telemetry_events", - "text", - "theme", - "time", - "tokio", - "toml 0.8.23", - "tower 0.4.13", - "tower-http 0.4.4", - "tracing", - "tracing-subscriber", - "unindent", - "util", - "uuid", - "workspace", - "worktree", - "zlog", -] - -[[package]] -name = "collab_ui" -version = "0.1.0" -dependencies = [ - "anyhow", - "call", - "channel", - "chrono", - "client", - "collections", - "db", - "editor", - "futures 0.3.31", - "fuzzy", - "gpui", - "http_client", - "log", - "menu", - "notifications", - "picker", - "pretty_assertions", - "project", - "release_channel", - "rpc", - "serde", - "serde_json", - "settings", - "smallvec", - "story", - "telemetry", - "theme", - "time", - "time_format", - "title_bar", - "tree-sitter-md", - "ui", - "util", - "workspace", -] - [[package]] name = "collections" version = "0.1.0" @@ -3403,7 +1050,7 @@ version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ - "bytes 1.10.1", + "bytes", "memchr", ] @@ -3417,62 +1064,6 @@ dependencies = [ "thiserror 2.0.17", ] -[[package]] -name = "command_palette" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "command_palette_hooks", - "ctor", - "db", - "editor", - "env_logger 0.11.8", - "fuzzy", - "go_to_line", - "gpui", - "language", - "log", - "menu", - "picker", - "postage", - "project", - "serde", - "serde_json", - "settings", - "telemetry", - "theme", - "time", - "ui", - "util", - "workspace", - "zed_actions", -] - -[[package]] -name = "command_palette_hooks" -version = "0.1.0" -dependencies = [ - "collections", - "derive_more 0.99.20", - "gpui", - "workspace", -] - -[[package]] -name = "component" -version = "0.1.0" -dependencies = [ - "collections", - "documented", - "gpui", - "inventory", - "parking_lot", - "strum 0.27.2", - "theme", -] - [[package]] name = "compression-codecs" version = "0.4.31" @@ -3500,25 +1091,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "console" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "unicode-width", - "windows-sys 0.59.0", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "const-random" version = "0.1.18" @@ -3539,115 +1111,12 @@ dependencies = [ "tiny-keccak", ] -[[package]] -name = "const_format" -version = "0.2.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" -dependencies = [ - "const_format_proc_macros", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - -[[package]] -name = "context_server" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "collections", - "futures 0.3.31", - "gpui", - "http_client", - "log", - "net", - "parking_lot", - "postage", - "schemars", - "serde", - "serde_json", - "settings", - "smol", - "tempfile", - "terminal", - "url", - "util", -] - [[package]] name = "convert_case" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" -[[package]] -name = "convert_case" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "copilot" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-std", - "chrono", - "client", - "clock", - "collections", - "command_palette_hooks", - "ctor", - "dirs 4.0.0", - "edit_prediction_types", - "editor", - "fs", - "futures 0.3.31", - "gpui", - "http_client", - "indoc", - "itertools 0.14.0", - "language", - "log", - "lsp", - "menu", - "node_runtime", - "parking_lot", - "paths", - "project", - "rpc", - "semver", - "serde", - "serde_json", - "settings", - "sum_tree", - "task", - "theme", - "ui", - "util", - "workspace", - "zlog", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -3683,7 +1152,7 @@ dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", "core-graphics-types 0.1.3", - "foreign-types 0.5.0", + "foreign-types", "libc", ] @@ -3696,7 +1165,7 @@ dependencies = [ "bitflags 2.9.4", "core-foundation 0.10.0", "core-graphics-types 0.2.0", - "foreign-types 0.5.0", + "foreign-types", "libc", ] @@ -3709,7 +1178,7 @@ dependencies = [ "bitflags 2.9.4", "core-foundation 0.9.4", "core-graphics-types 0.1.3", - "foreign-types 0.5.0", + "foreign-types", "libc", ] @@ -3748,15 +1217,6 @@ dependencies = [ "libc", ] -[[package]] -name = "core-services" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92567e81db522550ebaf742c5d875624ec7820c2c7ee5f8c60e4ce7c2ae3c0fd" -dependencies = [ - "core-foundation 0.9.4", -] - [[package]] name = "core-text" version = "21.0.0" @@ -3765,7 +1225,7 @@ checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" dependencies = [ "core-foundation 0.10.0", "core-graphics 0.24.0", - "foreign-types 0.5.0", + "foreign-types", "libc", ] @@ -3792,40 +1252,6 @@ dependencies = [ "libm", ] -[[package]] -name = "coreaudio-rs" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ca07354f6d0640333ef95f48d460a4bcf34812a7e7967f9b44c728a8f37c28" -dependencies = [ - "bitflags 1.3.2", - "core-foundation-sys", - "coreaudio-sys", -] - -[[package]] -name = "coreaudio-rs" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" -dependencies = [ - "bitflags 1.3.2", - "libc", - "objc2-audio-toolbox", - "objc2-core-audio", - "objc2-core-audio-types", - "objc2-core-foundation", -] - -[[package]] -name = "coreaudio-sys" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" -dependencies = [ - "bindgen 0.72.1", -] - [[package]] name = "cosmic-text" version = "0.14.2" @@ -3849,41 +1275,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "cpal" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f" -dependencies = [ - "alsa", - "coreaudio-rs 0.13.0", - "dasp_sample", - "jni", - "js-sys", - "libc", - "mach2 0.4.3", - "ndk", - "ndk-context", - "num-derive", - "num-traits", - "objc2-audio-toolbox", - "objc2-core-audio", - "objc2-core-audio-types", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows 0.54.0", -] - -[[package]] -name = "cpp_demangle" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" -dependencies = [ - "cfg-if", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -3893,189 +1284,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cranelift-bforest" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e15d04a0ce86cb36ead88ad68cf693ffd6cda47052b9e0ac114bc47fd9cd23c4" -dependencies = [ - "cranelift-entity", -] - -[[package]] -name = "cranelift-bitset" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c6e3969a7ce267259ce244b7867c5d3bc9e65b0a87e81039588dfdeaede9f34" -dependencies = [ - "serde", - "serde_derive", -] - -[[package]] -name = "cranelift-codegen" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c22032c4cb42558371cf516bb47f26cdad1819d3475c133e93c49f50ebf304e" -dependencies = [ - "bumpalo", - "cranelift-bforest", - "cranelift-bitset", - "cranelift-codegen-meta", - "cranelift-codegen-shared", - "cranelift-control", - "cranelift-entity", - "cranelift-isle", - "gimli 0.31.1", - "hashbrown 0.14.5", - "log", - "postcard", - "regalloc2", - "rustc-hash 2.1.1", - "serde", - "serde_derive", - "sha2", - "smallvec", - "target-lexicon 0.13.3", -] - -[[package]] -name = "cranelift-codegen-meta" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c904bc71c61b27fc57827f4a1379f29de64fe95653b620a3db77d59655eee0b8" -dependencies = [ - "cranelift-codegen-shared", -] - -[[package]] -name = "cranelift-codegen-shared" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40180f5497572f644ce88c255480981ae2ec1d7bb4d8e0c0136a13b87a2f2ceb" - -[[package]] -name = "cranelift-control" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d132c6d0bd8a489563472afc171759da0707804a65ece7ceb15a8c6d7dd5ef" -dependencies = [ - "arbitrary", -] - -[[package]] -name = "cranelift-entity" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d0d9618275474fbf679dd018ac6e009acbd6ae6850f6a67be33fb3b00b323" -dependencies = [ - "cranelift-bitset", - "serde", - "serde_derive", -] - -[[package]] -name = "cranelift-frontend" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fac41e16729107393174b0c9e3730fb072866100e1e64e80a1a963b2e484d57" -dependencies = [ - "cranelift-codegen", - "log", - "smallvec", - "target-lexicon 0.13.3", -] - -[[package]] -name = "cranelift-isle" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca20d576e5070044d0a72a9effc2deacf4d6aa650403189d8ea50126483944d" - -[[package]] -name = "cranelift-native" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dee82f3f1f2c4cba9177f1cc5e350fe98764379bcd29340caa7b01f85076c7" -dependencies = [ - "cranelift-codegen", - "libc", - "target-lexicon 0.13.3", -] - -[[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.3", -] - -[[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.3", - "parking_lot", -] - -[[package]] -name = "crashes" -version = "0.1.0" -dependencies = [ - "bincode", - "cfg-if", - "crash-handler", - "extension_host", - "log", - "mach2 0.5.0", - "minidumper", - "paths", - "release_channel", - "serde", - "serde_json", - "smol", - "system_specs", - "windows 0.61.3", - "zstd", -] - -[[package]] -name = "crc" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "crc-fast" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf62af4cc77d8fe1c22dde4e721d87f2f54056139d8c412e1366b740305f56f" -dependencies = [ - "crc", - "digest", - "libc", - "rand 0.9.2", - "regex", -] - [[package]] name = "crc32fast" version = "1.5.0" @@ -4085,77 +1293,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "credentials_provider" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures 0.3.31", - "gpui", - "paths", - "release_channel", - "serde", - "serde_json", -] - -[[package]] -name = "criterion" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" -dependencies = [ - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "is-terminal", - "itertools 0.10.5", - "num-traits", - "once_cell", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_derive", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" -dependencies = [ - "cast", - "itertools 0.10.5", -] - -[[package]] -name = "crossbeam" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -4196,28 +1333,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" -[[package]] -name = "crypto-bigint" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "crypto-common" version = "0.1.6" @@ -4229,29 +1344,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "cssparser" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "phf 0.11.3", - "smallvec", -] - -[[package]] -name = "cssparser-macros" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" -dependencies = [ - "quote", - "syn 2.0.106", -] - [[package]] name = "ctor" version = "0.4.3" @@ -4268,473 +1360,31 @@ version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" -[[package]] -name = "ctrlc" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "881c5d0a13b2f1498e2306e82cbada78390e152d4b1378fb28a84f4dcd0dc4f3" -dependencies = [ - "dispatch", - "nix 0.30.1", - "windows-sys 0.61.2", -] - -[[package]] -name = "cursor-icon" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" - -[[package]] -name = "cxx" -version = "1.0.187" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8465678d499296e2cbf9d3acf14307458fd69b471a31b65b3c519efe8b5e187" -dependencies = [ - "cc", - "cxx-build", - "cxxbridge-cmd", - "cxxbridge-flags", - "cxxbridge-macro", - "foldhash 0.2.0", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.187" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d74b6bcf49ebbd91f1b1875b706ea46545032a14003b5557b7dfa4bbeba6766e" -dependencies = [ - "cc", - "codespan-reporting 0.13.0", - "indexmap", - "proc-macro2", - "quote", - "scratch", - "syn 2.0.106", -] - -[[package]] -name = "cxxbridge-cmd" -version = "1.0.187" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ca2ad69673c4b35585edfa379617ac364bccd0ba0adf319811ba3a74ffa48a" -dependencies = [ - "clap", - "codespan-reporting 0.13.0", - "indexmap", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.187" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29b52102aa395386d77d322b3a0522f2035e716171c2c60aa87cc5e9466e523" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.187" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8ebf0b6138325af3ec73324cb3a48b64d57721f17291b151206782e61f66cd" -dependencies = [ - "indexmap", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "dap" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-compression", - "async-pipe", - "async-tar", - "async-trait", - "client", - "collections", - "dap-types", - "fs", - "futures 0.3.31", - "gpui", - "http_client", - "language", - "libc", - "log", - "node_runtime", - "parking_lot", - "paths", - "proto", - "schemars", - "serde", - "serde_json", - "settings", - "smallvec", - "smol", - "task", - "telemetry", - "tree-sitter", - "tree-sitter-go", - "util", - "zlog", -] - -[[package]] -name = "dap-types" -version = "0.0.1" -source = "git+https://github.com/zed-industries/dap-types?rev=1b461b310481d01e02b2603c16d7144b926339f8#1b461b310481d01e02b2603c16d7144b926339f8" -dependencies = [ - "schemars", - "serde", - "serde_json", -] - -[[package]] -name = "dap_adapters" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "collections", - "dap", - "dotenvy", - "fs", - "futures 0.3.31", - "gpui", - "http_client", - "json_dotpath", - "language", - "log", - "node_runtime", - "paths", - "serde", - "serde_json", - "settings", - "smol", - "task", - "util", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.106", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - -[[package]] -name = "dasp_sample" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" - -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - [[package]] name = "data-url" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" -[[package]] -name = "db" -version = "0.1.0" -dependencies = [ - "anyhow", - "gpui", - "indoc", - "log", - "paths", - "release_channel", - "smol", - "sqlez", - "sqlez_macros", - "tempfile", - "util", - "zed_env_vars", -] - -[[package]] -name = "dbus" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190b6255e8ab55a7b568df5a883e9497edc3e4821c06396612048b430e5ad1e9" -dependencies = [ - "libc", - "libdbus-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "debug_adapter_extension" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "collections", - "dap", - "extension", - "gpui", - "serde_json", - "task", - "util", -] - -[[package]] -name = "debugger_tools" -version = "0.1.0" -dependencies = [ - "anyhow", - "dap", - "editor", - "futures 0.3.31", - "gpui", - "project", - "serde_json", - "settings", - "smol", - "util", - "workspace", -] - -[[package]] -name = "debugger_ui" -version = "0.1.0" -dependencies = [ - "alacritty_terminal", - "anyhow", - "bitflags 2.9.4", - "client", - "collections", - "command_palette_hooks", - "dap", - "dap_adapters", - "db", - "debugger_tools", - "editor", - "feature_flags", - "file_icons", - "futures 0.3.31", - "fuzzy", - "gpui", - "hex", - "indoc", - "itertools 0.14.0", - "language", - "log", - "menu", - "notifications", - "parking_lot", - "parse_int", - "paths", - "picker", - "pretty_assertions", - "project", - "rpc", - "schemars", - "serde", - "serde_json", - "serde_json_lenient", - "settings", - "sysinfo 0.37.2", - "task", - "tasks_ui", - "telemetry", - "terminal_view", - "text", - "theme", - "tree-sitter", - "tree-sitter-go", - "tree-sitter-json", - "ui", - "ui_input", - "unindent", - "util", - "workspace", - "zed_actions", - "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" -dependencies = [ - "anyhow", - "futures 0.3.31", - "http_client", - "schemars", - "serde", - "serde_json", -] - [[package]] name = "deflate64" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" -[[package]] -name = "denoise" -version = "0.1.0" -dependencies = [ - "candle-core", - "candle-onnx", - "log", - "realfft", - "rodio", - "rustfft", - "thiserror 2.0.17", -] - -[[package]] -name = "der" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" -dependencies = [ - "const-oid", - "zeroize", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "derive_more" version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ - "convert_case 0.4.0", + "convert_case", "proc-macro2", "quote", "rustc_version", "syn 2.0.106", ] -[[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.106", - "unicode-xid", -] - [[package]] name = "derive_refineable" version = "0.1.0" @@ -4744,85 +1394,12 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "derive_setters" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae5c625eda104c228c06ecaf988d1c60e542176bd7a490e60eeda3493244c0c9" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "deunicode" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" - -[[package]] -name = "diagnostics" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "component", - "ctor", - "editor", - "gpui", - "indoc", - "itertools 0.14.0", - "language", - "log", - "lsp", - "markdown", - "pretty_assertions", - "project", - "rand 0.9.2", - "serde", - "serde_json", - "settings", - "text", - "theme", - "ui", - "unindent", - "util", - "workspace", - "zlog", -] - -[[package]] -name = "dialoguer" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" -dependencies = [ - "console", - "fuzzy-matcher", - "shell-words", - "tempfile", - "thiserror 1.0.69", - "zeroize", -] - [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" -[[package]] -name = "diffy" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b545b8c50194bdd008283985ab0b31dba153cfd5b3066a92770634fbc0d7d291" -dependencies = [ - "nu-ansi-term", -] - [[package]] name = "digest" version = "0.10.7" @@ -4830,7 +1407,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", - "const-oid", "crypto-common", "subtle", ] @@ -4853,15 +1429,6 @@ dependencies = [ "dirs-sys 0.4.1", ] -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys 0.5.0", -] - [[package]] name = "dirs-sys" version = "0.3.7" @@ -4869,7 +1436,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", - "redox_users 0.4.6", + "redox_users", "winapi", ] @@ -4881,22 +1448,10 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users 0.4.6", + "redox_users", "windows-sys 0.48.0", ] -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.5.2", - "windows-sys 0.61.2", -] - [[package]] name = "dispatch" version = "0.2.0" @@ -4933,87 +1488,12 @@ dependencies = [ "libloading", ] -[[package]] -name = "docs_preprocessor" -version = "0.1.0" -dependencies = [ - "anyhow", - "command_palette", - "gpui", - "mdbook", - "regex", - "serde", - "serde_json", - "settings", - "task", - "theme", - "util", - "zed", - "zlog", -] - -[[package]] -name = "documented" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6b3e31251e87acd1b74911aed84071c8364fc9087972748ade2f1094ccce34" -dependencies = [ - "documented-macros", - "phf 0.12.1", - "thiserror 2.0.17", -] - -[[package]] -name = "documented-macros" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1149cf7462e5e79e17a3c05fd5b1f9055092bbfa95e04c319395c3beacc9370f" -dependencies = [ - "convert_case 0.8.0", - "itertools 0.14.0", - "optfield", - "proc-macro2", - "quote", - "strum 0.27.2", - "syn 2.0.106", -] - -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - [[package]] name = "downcast-rs" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" -[[package]] -name = "doxygen-rs" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "415b6ec780d34dcf624666747194393603d0373b7141eef01d12ee58881507d9" -dependencies = [ - "phf 0.11.3", -] - -[[package]] -name = "dtoa" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" - -[[package]] -name = "dtoa-short" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" -dependencies = [ - "dtoa", -] - [[package]] name = "dtor" version = "0.0.6" @@ -5053,367 +1533,11 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "dyn-stack" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e53799688f5632f364f8fb387488dd05db9fe45db7011be066fc20e7027f8b" -dependencies = [ - "bytemuck", - "reborrow", -] - -[[package]] -name = "dyn-stack" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c4713e43e2886ba72b8271aa66c93d722116acf7a75555cce11dcde84388fe8" -dependencies = [ - "bytemuck", - "dyn-stack-macros", -] - -[[package]] -name = "dyn-stack-macros" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d926b4d407d372f141f93bb444696142c29d32962ccbd3531117cf3aa0bfa9" - -[[package]] -name = "ec4rs" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b31a881d38439026e3d5dd938ab20328d36e23caca8fd5981c42e4b677f5842" - -[[package]] -name = "ecdsa" -version = "0.14.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" -dependencies = [ - "der 0.6.1", - "elliptic-curve", - "rfc6979", - "signature 1.6.4", -] - -[[package]] -name = "edit_prediction" -version = "0.1.0" -dependencies = [ - "ai_onboarding", - "anyhow", - "arrayvec", - "brotli", - "client", - "clock", - "cloud_api_types", - "cloud_llm_client", - "collections", - "copilot", - "credentials_provider", - "ctor", - "db", - "edit_prediction_context", - "edit_prediction_types", - "feature_flags", - "fs", - "futures 0.3.31", - "gpui", - "indoc", - "itertools 0.14.0", - "language", - "language_model", - "log", - "lsp", - "menu", - "open_ai", - "parking_lot", - "postage", - "pretty_assertions", - "project", - "rand 0.9.2", - "regex", - "release_channel", - "semver", - "serde", - "serde_json", - "settings", - "strum 0.27.2", - "telemetry", - "telemetry_events", - "thiserror 2.0.17", - "ui", - "util", - "uuid", - "workspace", - "worktree", - "zed_actions", - "zeta_prompt", - "zlog", -] - -[[package]] -name = "edit_prediction_cli" -version = "0.1.0" -dependencies = [ - "anthropic", - "anyhow", - "chrono", - "clap", - "client", - "cloud_llm_client", - "collections", - "debug_adapter_extension", - "dirs 4.0.0", - "edit_prediction", - "extension", - "fs", - "futures 0.3.31", - "gpui", - "gpui_tokio", - "http_client", - "indoc", - "language", - "language_extension", - "language_model", - "language_models", - "languages", - "libc", - "log", - "node_runtime", - "paths", - "pretty_assertions", - "project", - "prompt_store", - "pulldown-cmark 0.12.2", - "release_channel", - "reqwest_client", - "serde", - "serde_json", - "settings", - "shellexpand 2.1.2", - "smol", - "sqlez", - "sqlez_macros", - "terminal_view", - "util", - "wasmtime", - "watch", - "zeta_prompt", -] - -[[package]] -name = "edit_prediction_context" -version = "0.1.0" -dependencies = [ - "anyhow", - "cloud_llm_client", - "collections", - "env_logger 0.11.8", - "futures 0.3.31", - "gpui", - "indoc", - "language", - "log", - "lsp", - "parking_lot", - "pretty_assertions", - "project", - "serde", - "serde_json", - "settings", - "smallvec", - "text", - "tree-sitter", - "util", - "zeta_prompt", - "zlog", -] - -[[package]] -name = "edit_prediction_types" -version = "0.1.0" -dependencies = [ - "client", - "gpui", - "language", - "text", -] - -[[package]] -name = "edit_prediction_ui" -version = "0.1.0" -dependencies = [ - "anyhow", - "buffer_diff", - "client", - "cloud_llm_client", - "codestral", - "command_palette_hooks", - "copilot", - "edit_prediction", - "edit_prediction_types", - "editor", - "feature_flags", - "fs", - "futures 0.3.31", - "gpui", - "indoc", - "language", - "lsp", - "markdown", - "menu", - "multi_buffer", - "paths", - "project", - "regex", - "serde_json", - "settings", - "supermaven", - "telemetry", - "text", - "theme", - "ui", - "ui_input", - "util", - "workspace", - "zed_actions", - "zeta_prompt", -] - -[[package]] -name = "editor" -version = "0.1.0" -dependencies = [ - "aho-corasick", - "anyhow", - "assets", - "buffer_diff", - "client", - "clock", - "collections", - "convert_case 0.8.0", - "criterion", - "ctor", - "dap", - "db", - "edit_prediction_types", - "emojis", - "feature_flags", - "file_icons", - "fs", - "futures 0.3.31", - "fuzzy", - "git", - "gpui", - "http_client", - "indoc", - "itertools 0.14.0", - "language", - "languages", - "linkify", - "log", - "lsp", - "markdown", - "menu", - "multi_buffer", - "ordered-float 2.10.1", - "parking_lot", - "pretty_assertions", - "project", - "rand 0.9.2", - "regex", - "release_channel", - "rope", - "rpc", - "schemars", - "semver", - "serde", - "serde_json", - "settings", - "smallvec", - "smol", - "snippet", - "sum_tree", - "task", - "telemetry", - "tempfile", - "text", - "theme", - "time", - "tracing", - "tree-sitter-bash", - "tree-sitter-c", - "tree-sitter-html", - "tree-sitter-md", - "tree-sitter-python", - "tree-sitter-rust", - "tree-sitter-typescript", - "tree-sitter-yaml", - "ui", - "unicode-script", - "unicode-segmentation", - "unindent", - "url", - "util", - "uuid", - "vim_mode_setting", - "workspace", - "zed_actions", - "zlog", - "ztracing", -] - [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -dependencies = [ - "serde", -] - -[[package]] -name = "elasticlunr-rs" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41e83863a500656dfa214fee6682de9c5b9f03de6860fec531235ed2ae9f6571" -dependencies = [ - "regex", - "serde", - "serde_derive", - "serde_json", -] - -[[package]] -name = "elliptic-curve" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" -dependencies = [ - "base16ct", - "crypto-bigint 0.4.9", - "der 0.6.1", - "digest", - "ff", - "generic-array", - "group", - "pkcs8 0.9.0", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "email_address" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" -dependencies = [ - "serde", -] [[package]] name = "embed-resource" @@ -5426,36 +1550,9 @@ dependencies = [ "rustc_version", "toml 0.9.8", "vswhom", - "winreg 0.55.0", + "winreg", ] -[[package]] -name = "embedded-io" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" - -[[package]] -name = "embedded-io" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" - -[[package]] -name = "emojis" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99e1f1df1f181f2539bac8bf027d31ca5ffbf9e559e3f2d09413b9107b5c02f4" -dependencies = [ - "phf 0.11.3", -] - -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - [[package]] name = "encoding_rs" version = "0.8.35" @@ -5471,18 +1568,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "enumflags2" version = "0.7.12" @@ -5514,19 +1599,6 @@ dependencies = [ "regex", ] -[[package]] -name = "env_logger" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" -dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", -] - [[package]] name = "env_logger" version = "0.11.8" @@ -5540,15 +1612,6 @@ dependencies = [ "log", ] -[[package]] -name = "envy" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" -dependencies = [ - "serde", -] - [[package]] name = "equator" version = "0.4.2" @@ -5586,17 +1649,6 @@ dependencies = [ "typeid", ] -[[package]] -name = "errno" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" -dependencies = [ - "errno-dragonfly", - "libc", - "winapi", -] - [[package]] name = "errno" version = "0.3.14" @@ -5607,16 +1659,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "etagere" version = "0.2.15" @@ -5627,17 +1669,6 @@ dependencies = [ "svg_fmt", ] -[[package]] -name = "etcetera" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" -dependencies = [ - "cfg-if", - "home", - "windows-sys 0.48.0", -] - [[package]] name = "euclid" version = "0.22.11" @@ -5647,70 +1678,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "eval" -version = "0.1.0" -dependencies = [ - "acp_thread", - "agent", - "agent-client-protocol", - "agent_settings", - "agent_ui", - "anyhow", - "async-trait", - "buffer_diff", - "chrono", - "clap", - "client", - "collections", - "debug_adapter_extension", - "dirs 4.0.0", - "dotenvy", - "env_logger 0.11.8", - "extension", - "fs", - "futures 0.3.31", - "gpui", - "gpui_tokio", - "handlebars 4.5.0", - "language", - "language_extension", - "language_model", - "language_models", - "languages", - "markdown", - "node_runtime", - "pathdiff", - "paths", - "pretty_assertions", - "project", - "prompt_store", - "rand 0.9.2", - "regex", - "release_channel", - "reqwest_client", - "serde", - "serde_json", - "settings", - "shellexpand 2.1.2", - "telemetry", - "terminal_view", - "toml 0.8.23", - "unindent", - "util", - "uuid", - "watch", -] - -[[package]] -name = "eval_utils" -version = "0.1.0" -dependencies = [ - "gpui", - "serde", - "smol", -] - [[package]] name = "event-listener" version = "2.5.3" @@ -5738,25 +1705,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "exec" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "886b70328cba8871bfc025858e1de4be16b1d5088f2ba50b57816f4210672615" -dependencies = [ - "errno 0.2.8", - "libc", -] - -[[package]] -name = "explorer_command_injector" -version = "0.1.0" -dependencies = [ - "windows 0.61.3", - "windows-core 0.61.2", - "windows-registry 0.5.3", -] - [[package]] name = "exr" version = "1.73.0" @@ -5772,175 +1720,6 @@ dependencies = [ "zune-inflate", ] -[[package]] -name = "extended" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" - -[[package]] -name = "extension" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "collections", - "dap", - "fs", - "futures 0.3.31", - "gpui", - "heck 0.5.0", - "http_client", - "indoc", - "language", - "log", - "lsp", - "parking_lot", - "pretty_assertions", - "proto", - "semver", - "serde", - "serde_json", - "task", - "tempfile", - "toml 0.8.23", - "url", - "util", - "wasm-encoder 0.221.3", - "wasmparser 0.221.3", -] - -[[package]] -name = "extension_cli" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "env_logger 0.11.8", - "extension", - "fs", - "gpui", - "language", - "log", - "reqwest_client", - "rpc", - "serde", - "serde_json", - "theme", - "tokio", - "toml 0.8.23", - "tree-sitter", - "wasmtime", -] - -[[package]] -name = "extension_host" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-compression", - "async-tar", - "async-trait", - "client", - "collections", - "criterion", - "ctor", - "dap", - "extension", - "fs", - "futures 0.3.31", - "gpui", - "gpui_tokio", - "http_client", - "language", - "language_extension", - "log", - "lsp", - "moka", - "node_runtime", - "parking_lot", - "paths", - "project", - "rand 0.9.2", - "release_channel", - "remote", - "reqwest_client", - "semver", - "serde", - "serde_json", - "serde_json_lenient", - "settings", - "task", - "telemetry", - "tempfile", - "theme", - "theme_extension", - "toml 0.8.23", - "url", - "util", - "wasmparser 0.221.3", - "wasmtime", - "wasmtime-wasi", - "zlog", -] - -[[package]] -name = "extensions_ui" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "db", - "editor", - "extension", - "extension_host", - "fs", - "fuzzy", - "gpui", - "language", - "log", - "num-format", - "picker", - "project", - "release_channel", - "semver", - "serde", - "settings", - "smallvec", - "strum 0.27.2", - "telemetry", - "theme", - "ui", - "util", - "vim_mode_setting", - "workspace", - "zed_actions", -] - -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fancy-regex" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" -dependencies = [ - "bit-set", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "fast-srgb8" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" - [[package]] name = "fastrand" version = "1.9.0" @@ -5976,17 +1755,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "fd-lock" -version = "4.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" -dependencies = [ - "cfg-if", - "rustix 1.1.2", - "windows-sys 0.59.0", -] - [[package]] name = "fdeflate" version = "0.3.7" @@ -5996,78 +1764,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "feature_flags" -version = "0.1.0" -dependencies = [ - "futures 0.3.31", - "gpui", - "smol", -] - -[[package]] -name = "feedback" -version = "0.1.0" -dependencies = [ - "editor", - "gpui", - "system_specs", - "urlencoding", - "util", - "workspace", - "zed_actions", -] - -[[package]] -name = "ff" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "file_finder" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "ctor", - "editor", - "file_icons", - "futures 0.3.31", - "fuzzy", - "gpui", - "language", - "menu", - "picker", - "pretty_assertions", - "project", - "schemars", - "search", - "serde", - "serde_json", - "settings", - "text", - "theme", - "ui", - "util", - "workspace", - "zlog", -] - -[[package]] -name = "file_icons" -version = "0.1.0" -dependencies = [ - "gpui", - "serde", - "theme", - "util", -] - [[package]] name = "filedescriptor" version = "0.8.3" @@ -6097,12 +1793,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - [[package]] name = "flate2" version = "1.1.4" @@ -6125,35 +1815,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" -[[package]] -name = "float8" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4203231de188ebbdfb85c11f3c20ca2b063945710de04e7b59268731e728b462" -dependencies = [ - "half", - "num-traits", - "rand 0.9.2", - "rand_distr", -] - [[package]] name = "float_next_after" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" -[[package]] -name = "fluent-uri" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" -dependencies = [ - "borrow-or-share", - "ref-cast", - "serde", -] - [[package]] name = "flume" version = "0.11.1" @@ -6178,12 +1845,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - [[package]] name = "font-types" version = "0.10.0" @@ -6230,15 +1891,6 @@ dependencies = [ "ttf-parser 0.25.1", ] -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared 0.1.1", -] - [[package]] name = "foreign-types" version = "0.5.0" @@ -6246,7 +1898,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared 0.3.1", + "foreign-types-shared", ] [[package]] @@ -6260,27 +1912,12 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "foreign-types-shared" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" -[[package]] -name = "fork" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30268f1eefccc9d72f43692e8b89e659aeb52e84016c3b32b6e7e9f1c8f38f94" -dependencies = [ - "libc", -] - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -6290,16 +1927,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fraction" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7" -dependencies = [ - "lazy_static", - "num", -] - [[package]] name = "freetype-sys" version = "0.20.1" @@ -6311,111 +1938,12 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "fs" -version = "0.1.0" -dependencies = [ - "anyhow", - "ashpd 0.11.0", - "async-tar", - "async-trait", - "cocoa 0.26.0", - "collections", - "fsevent", - "futures 0.3.31", - "git", - "gpui", - "ignore", - "is_executable", - "libc", - "log", - "notify 8.2.0", - "objc", - "parking_lot", - "paths", - "proto", - "rope", - "serde", - "serde_json", - "smol", - "tempfile", - "text", - "time", - "util", - "windows 0.61.3", -] - -[[package]] -name = "fs-set-times" -version = "0.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" -dependencies = [ - "io-lifetimes", - "rustix 1.1.2", - "windows-sys 0.59.0", -] - -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "fs_benchmarks" -version = "0.1.0" -dependencies = [ - "fs", - "gpui", -] - [[package]] name = "fs_extra" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" -[[package]] -name = "fsevent" -version = "0.1.0" -dependencies = [ - "bitflags 2.9.4", - "core-foundation 0.10.0", - "fsevent-sys 3.1.0", - "log", - "parking_lot", - "tempfile", -] - -[[package]] -name = "fsevent-sys" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6f5e6817058771c10f0eb0f05ddf1e35844266f972004fe8e4b21fda295bd5" -dependencies = [ - "libc", -] - -[[package]] -name = "fsevent-sys" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futf" version = "0.1.5" @@ -6426,12 +1954,6 @@ dependencies = [ "new_debug_unreachable", ] -[[package]] -name = "futures" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" - [[package]] name = "futures" version = "0.3.31" @@ -6474,17 +1996,6 @@ dependencies = [ "futures-util", ] -[[package]] -name = "futures-intrusive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" -dependencies = [ - "futures-core", - "lock_api", - "parking_lot", -] - [[package]] name = "futures-io" version = "0.3.31" @@ -6548,7 +2059,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "futures 0.1.31", "futures-channel", "futures-core", "futures-io", @@ -6559,285 +2069,6 @@ dependencies = [ "pin-project-lite", "pin-utils", "slab", - "tokio-io", -] - -[[package]] -name = "fuzzy" -version = "0.1.0" -dependencies = [ - "gpui", - "log", - "util", -] - -[[package]] -name = "fuzzy-matcher" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" -dependencies = [ - "thread_local", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "gemm" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab24cc62135b40090e31a76a9b2766a501979f3070fa27f689c27ec04377d32" -dependencies = [ - "dyn-stack 0.10.0", - "gemm-c32 0.17.1", - "gemm-c64 0.17.1", - "gemm-common 0.17.1", - "gemm-f16 0.17.1", - "gemm-f32 0.17.1", - "gemm-f64 0.17.1", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 10.7.0", - "seq-macro", -] - -[[package]] -name = "gemm" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab96b703d31950f1aeddded248bc95543c9efc7ac9c4a21fda8703a83ee35451" -dependencies = [ - "dyn-stack 0.13.2", - "gemm-c32 0.18.2", - "gemm-c64 0.18.2", - "gemm-common 0.18.2", - "gemm-f16 0.18.2", - "gemm-f32 0.18.2", - "gemm-f64 0.18.2", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 11.6.0", - "seq-macro", -] - -[[package]] -name = "gemm-c32" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9c030d0b983d1e34a546b86e08f600c11696fde16199f971cd46c12e67512c0" -dependencies = [ - "dyn-stack 0.10.0", - "gemm-common 0.17.1", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 10.7.0", - "seq-macro", -] - -[[package]] -name = "gemm-c32" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6db9fd9f40421d00eea9dd0770045a5603b8d684654816637732463f4073847" -dependencies = [ - "dyn-stack 0.13.2", - "gemm-common 0.18.2", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 11.6.0", - "seq-macro", -] - -[[package]] -name = "gemm-c64" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb5f2e79fefb9693d18e1066a557b4546cd334b226beadc68b11a8f9431852a" -dependencies = [ - "dyn-stack 0.10.0", - "gemm-common 0.17.1", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 10.7.0", - "seq-macro", -] - -[[package]] -name = "gemm-c64" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfcad8a3d35a43758330b635d02edad980c1e143dc2f21e6fd25f9e4eada8edf" -dependencies = [ - "dyn-stack 0.13.2", - "gemm-common 0.18.2", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 11.6.0", - "seq-macro", -] - -[[package]] -name = "gemm-common" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2e7ea062c987abcd8db95db917b4ffb4ecdfd0668471d8dc54734fdff2354e8" -dependencies = [ - "bytemuck", - "dyn-stack 0.10.0", - "half", - "num-complex", - "num-traits", - "once_cell", - "paste", - "pulp 0.18.22", - "raw-cpuid 10.7.0", - "rayon", - "seq-macro", - "sysctl 0.5.5", -] - -[[package]] -name = "gemm-common" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a352d4a69cbe938b9e2a9cb7a3a63b7e72f9349174a2752a558a8a563510d0f3" -dependencies = [ - "bytemuck", - "dyn-stack 0.13.2", - "half", - "libm", - "num-complex", - "num-traits", - "once_cell", - "paste", - "pulp 0.21.5", - "raw-cpuid 11.6.0", - "rayon", - "seq-macro", - "sysctl 0.6.0", -] - -[[package]] -name = "gemm-f16" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca4c06b9b11952071d317604acb332e924e817bd891bec8dfb494168c7cedd4" -dependencies = [ - "dyn-stack 0.10.0", - "gemm-common 0.17.1", - "gemm-f32 0.17.1", - "half", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 10.7.0", - "rayon", - "seq-macro", -] - -[[package]] -name = "gemm-f16" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff95ae3259432f3c3410eaa919033cd03791d81cebd18018393dc147952e109" -dependencies = [ - "dyn-stack 0.13.2", - "gemm-common 0.18.2", - "gemm-f32 0.18.2", - "half", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 11.6.0", - "rayon", - "seq-macro", -] - -[[package]] -name = "gemm-f32" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9a69f51aaefbd9cf12d18faf273d3e982d9d711f60775645ed5c8047b4ae113" -dependencies = [ - "dyn-stack 0.10.0", - "gemm-common 0.17.1", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 10.7.0", - "seq-macro", -] - -[[package]] -name = "gemm-f32" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc8d3d4385393304f407392f754cd2dc4b315d05063f62cf09f47b58de276864" -dependencies = [ - "dyn-stack 0.13.2", - "gemm-common 0.18.2", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 11.6.0", - "seq-macro", -] - -[[package]] -name = "gemm-f64" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa397a48544fadf0b81ec8741e5c0fba0043008113f71f2034def1935645d2b0" -dependencies = [ - "dyn-stack 0.10.0", - "gemm-common 0.17.1", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 10.7.0", - "seq-macro", -] - -[[package]] -name = "gemm-f64" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35b2a4f76ce4b8b16eadc11ccf2e083252d8237c1b589558a49b0183545015bd" -dependencies = [ - "dyn-stack 0.13.2", - "gemm-common 0.18.2", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 11.6.0", - "seq-macro", -] - -[[package]] -name = "generator" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2" -dependencies = [ - "cc", - "cfg-if", - "libc", - "log", - "rustversion", - "windows 0.61.3", ] [[package]] @@ -6887,33 +2118,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "gh-workflow" -version = "0.8.0" -source = "git+https://github.com/zed-industries/gh-workflow?rev=09acfdf2bd5c1d6254abefd609c808ff73547b2c#09acfdf2bd5c1d6254abefd609c808ff73547b2c" -dependencies = [ - "async-trait", - "derive_more 2.0.1", - "derive_setters", - "gh-workflow-macros", - "indexmap", - "merge", - "serde", - "serde_json", - "serde_yaml", - "strum_macros 0.27.2", -] - -[[package]] -name = "gh-workflow-macros" -version = "0.8.0" -source = "git+https://github.com/zed-industries/gh-workflow?rev=09acfdf2bd5c1d6254abefd609c808ff73547b2c#09acfdf2bd5c1d6254abefd609c808ff73547b2c" -dependencies = [ - "heck 0.5.0", - "quote", - "syn 2.0.106", -] - [[package]] name = "gif" version = "0.13.3" @@ -6924,59 +2128,12 @@ dependencies = [ "weezl", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -dependencies = [ - "fallible-iterator", - "indexmap", - "stable_deref_trait", -] - [[package]] name = "gimli" version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" -[[package]] -name = "git" -version = "0.1.0" -dependencies = [ - "anyhow", - "askpass", - "async-trait", - "collections", - "derive_more 0.99.20", - "futures 0.3.31", - "git2", - "gpui", - "http_client", - "itertools 0.14.0", - "log", - "parking_lot", - "pretty_assertions", - "rand 0.9.2", - "regex", - "rope", - "schemars", - "serde", - "serde_json", - "smol", - "sum_tree", - "tempfile", - "text", - "thiserror 2.0.17", - "time", - "unindent", - "url", - "urlencoding", - "util", - "uuid", -] - [[package]] name = "git2" version = "0.20.2" @@ -6990,88 +2147,6 @@ dependencies = [ "url", ] -[[package]] -name = "git_hosting_providers" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "futures 0.3.31", - "git", - "gpui", - "http_client", - "indoc", - "itertools 0.14.0", - "pretty_assertions", - "regex", - "serde", - "serde_json", - "settings", - "url", - "urlencoding", - "util", -] - -[[package]] -name = "git_ui" -version = "0.1.0" -dependencies = [ - "agent_settings", - "anyhow", - "askpass", - "buffer_diff", - "call", - "cloud_llm_client", - "collections", - "command_palette_hooks", - "component", - "ctor", - "db", - "editor", - "futures 0.3.31", - "fuzzy", - "git", - "git_hosting_providers", - "gpui", - "indoc", - "itertools 0.14.0", - "language", - "language_model", - "linkify", - "log", - "markdown", - "menu", - "multi_buffer", - "notifications", - "panel", - "picker", - "pretty_assertions", - "project", - "recent_projects", - "remote", - "schemars", - "serde", - "serde_json", - "settings", - "smol", - "strum 0.27.2", - "telemetry", - "theme", - "time", - "time_format", - "tracing", - "ui", - "unindent", - "util", - "watch", - "windows 0.61.3", - "workspace", - "zed_actions", - "zeroize", - "zlog", - "ztracing", -] - [[package]] name = "glob" version = "0.3.3" @@ -7115,54 +2190,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "go_to_line" -version = "0.1.0" -dependencies = [ - "editor", - "gpui", - "indoc", - "language", - "menu", - "project", - "rope", - "serde", - "serde_json", - "settings", - "text", - "theme", - "tree-sitter-rust", - "tree-sitter-typescript", - "ui", - "util", - "workspace", -] - -[[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" -dependencies = [ - "anyhow", - "futures 0.3.31", - "http_client", - "schemars", - "serde", - "serde_json", - "settings", - "strum 0.27.2", -] - [[package]] name = "gpu-alloc" version = "0.6.0" @@ -7223,14 +2250,14 @@ dependencies = [ "core-video", "cosmic-text", "ctor", - "derive_more 0.99.20", + "derive_more", "embed-resource", - "env_logger 0.11.8", + "env_logger", "etagere", "filedescriptor", "flume", - "foreign-types 0.5.0", - "futures 0.3.31", + "foreign-types", + "futures", "gpui_macros", "http_client", "image", @@ -7239,7 +2266,7 @@ dependencies = [ "libc", "log", "lyon", - "mach2 0.5.0", + "mach2", "media", "metal", "naga", @@ -7326,36 +2353,6 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12101ecc8225ea6d675bc70263074eab6169079621c2186fe0c66590b2df9681" -[[package]] -name = "group" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes 1.10.1", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "h2" version = "0.4.12" @@ -7363,11 +2360,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", - "bytes 1.10.1", + "bytes", "fnv", "futures-core", "futures-sink", - "http 1.3.1", + "http", "indexmap", "slab", "tokio", @@ -7381,62 +2378,17 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ - "bytemuck", "cfg-if", "crunchy", "num-traits", - "rand 0.9.2", - "rand_distr", "zerocopy", ] -[[package]] -name = "handlebars" -version = "4.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faa67bab9ff362228eb3d00bd024a4965d8231bbb7921167f0cfa66c6626b225" -dependencies = [ - "log", - "pest", - "pest_derive", - "rust-embed", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "handlebars" -version = "5.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b" -dependencies = [ - "log", - "pest", - "pest_derive", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.8", -] - [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash 0.8.12", - "allocator-api2", -] [[package]] name = "hashbrown" @@ -7444,10 +2396,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.1.5", - "serde", + "foldhash", ] [[package]] @@ -7455,71 +2404,12 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown 0.14.5", -] - -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "headers" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" -dependencies = [ - "base64 0.21.7", - "bytes 1.10.1", - "headers-core", - "http 0.2.12", - "httpdate", - "mime", - "sha1", -] - -[[package]] -name = "headers-core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" -dependencies = [ - "http 0.2.12", -] - -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -dependencies = [ - "unicode-segmentation", -] [[package]] name = "heck" @@ -7527,44 +2417,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "heed" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd54745cfacb7b97dee45e8fdb91814b62bccddb481debb7de0f9ee6b7bf5b43" -dependencies = [ - "bitflags 2.9.4", - "byteorder", - "heed-traits", - "heed-types", - "libc", - "lmdb-master-sys", - "once_cell", - "page_size", - "serde", - "synchronoise", - "url", -] - -[[package]] -name = "heed-traits" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3130048d404c57ce5a1ac61a903696e8fcde7e8c2991e9fcfc1f27c3ef74ff" - -[[package]] -name = "heed-types" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c255bdf46e07fb840d120a36dcc81f385140d7191c76a7391672675c01a55d" -dependencies = [ - "bincode", - "byteorder", - "heed-traits", - "serde", - "serde_json", -] - [[package]] name = "hermit-abi" version = "0.5.2" @@ -7621,12 +2473,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" @@ -7635,76 +2481,43 @@ checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" dependencies = [ "log", "mac", - "markup5ever 0.12.1", + "markup5ever", "proc-macro2", "quote", "syn 2.0.106", ] -[[package]] -name = "html5ever" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" -dependencies = [ - "log", - "markup5ever 0.35.0", - "match_token", -] - [[package]] name = "html_to_markdown" version = "0.1.0" dependencies = [ "anyhow", - "html5ever 0.27.0", + "html5ever", "indoc", "markup5ever_rcdom", "pretty_assertions", "regex", ] -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes 1.10.1", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ - "bytes 1.10.1", + "bytes", "fnv", "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes 1.10.1", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ - "bytes 1.10.1", - "http 1.3.1", + "bytes", + "http", ] [[package]] @@ -7713,19 +2526,13 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ - "bytes 1.10.1", + "bytes", "futures-core", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] -[[package]] -name = "http-range-header" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" - [[package]] name = "http_client" version = "0.1.0" @@ -7734,11 +2541,11 @@ dependencies = [ "async-compression", "async-fs", "async-tar", - "bytes 1.10.1", - "derive_more 0.99.20", - "futures 0.3.31", - "http 1.3.1", - "http-body 1.0.1", + "bytes", + "derive_more", + "futures", + "http", + "http-body", "log", "parking_lot", "serde", @@ -7754,7 +2561,7 @@ dependencies = [ name = "http_client_tls" version = "0.1.0" dependencies = [ - "rustls 0.23.33", + "rustls", "rustls-platform-verifier", ] @@ -7764,48 +2571,6 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "human_bytes" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91f255a4535024abf7640cb288260811fc14794f62b063652ed349f9a6c2348e" - -[[package]] -name = "humantime" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" - -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes 1.10.1", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.7.0" @@ -7813,12 +2578,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ "atomic-waker", - "bytes 1.10.1", + "bytes", "futures-channel", "futures-core", - "h2 0.4.12", - "http 1.3.1", - "http-body 1.0.1", + "h2", + "http", + "http-body", "httparse", "itoa", "pin-project-lite", @@ -7828,71 +2593,39 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "log", - "rustls 0.21.12", - "rustls-native-certs 0.6.3", - "tokio", - "tokio-rustls 0.24.1", -] - [[package]] name = "hyper-rustls" version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.3.1", - "hyper 1.7.0", + "http", + "hyper", "hyper-util", - "rustls 0.23.33", - "rustls-native-certs 0.8.2", + "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls", "tower-service", ] -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes 1.10.1", - "hyper 0.14.32", - "native-tls", - "tokio", - "tokio-native-tls", -] - [[package]] name = "hyper-util" version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ - "base64 0.22.1", - "bytes 1.10.1", + "bytes", "futures-channel", "futures-core", "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "hyper 1.7.0", - "ipnet", + "http", + "http-body", + "hyper", "libc", - "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2", "tokio", "tower-service", "tracing", @@ -7922,14 +2655,6 @@ dependencies = [ "cc", ] -[[package]] -name = "icons" -version = "0.1.0" -dependencies = [ - "serde", - "strum 0.27.2", -] - [[package]] name = "icu_collections" version = "2.0.0" @@ -7938,7 +2663,7 @@ checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", "potential_utf", - "yoke 0.8.0", + "yoke", "zerofrom", "zerovec", ] @@ -8010,24 +2735,12 @@ dependencies = [ "stable_deref_trait", "tinystr", "writeable", - "yoke 0.8.0", + "yoke", "zerofrom", "zerotrie", "zerovec", ] -[[package]] -name = "id-arena" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "1.1.0" @@ -8049,22 +2762,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "ignore" -version = "0.4.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81776e6f9464432afcc28d03e52eb101c93b6f0566f52aef2427663e700f0403" -dependencies = [ - "crossbeam-deque", - "globset", - "log", - "memchr", - "regex-automata", - "same-file", - "walkdir", - "winapi-util", -] - [[package]] name = "image" version = "0.25.8" @@ -8099,41 +2796,12 @@ dependencies = [ "quick-error", ] -[[package]] -name = "image_viewer" -version = "0.1.0" -dependencies = [ - "anyhow", - "db", - "editor", - "file_icons", - "gpui", - "language", - "log", - "project", - "serde", - "settings", - "theme", - "ui", - "util", - "workspace", -] - [[package]] name = "imagesize" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" -[[package]] -name = "imara-diff" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17d34b7d42178945f775e84bc4c36dde7c1c6cdfea656d3354d009056f2bb3d2" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "imgref" version = "1.12.0" @@ -8158,48 +2826,6 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" -[[package]] -name = "inherent" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "inotify" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" -dependencies = [ - "bitflags 1.3.2", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" -dependencies = [ - "bitflags 2.9.4", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - [[package]] name = "inout" version = "0.1.4" @@ -8210,41 +2836,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "inspector_ui" -version = "0.1.0" -dependencies = [ - "anyhow", - "command_palette_hooks", - "editor", - "fuzzy", - "gpui", - "language", - "project", - "serde_json", - "serde_json_lenient", - "theme", - "title_bar", - "ui", - "util", - "util_macros", - "workspace", - "zed_actions", -] - -[[package]] -name = "install_cli" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "gpui", - "release_channel", - "smol", - "util", - "workspace", -] - [[package]] name = "instant" version = "0.1.13" @@ -8274,22 +2865,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "io-extras" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" -dependencies = [ - "io-lifetimes", - "windows-sys 0.59.0", -] - -[[package]] -name = "io-lifetimes" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" - [[package]] name = "io-surface" version = "0.16.1" @@ -8302,50 +2877,12 @@ dependencies = [ "leaky-cow", ] -[[package]] -name = "iovec" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" -dependencies = [ - "libc", -] - -[[package]] -name = "ipc-channel" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8251fb7bcd9ccd3725ed8deae9fe7db8e586495c9eb5b0c52e6233e5e75ea" -dependencies = [ - "bincode", - "crossbeam-channel", - "fnv", - "lazy_static", - "libc", - "mio 1.1.0", - "rand 0.8.5", - "serde", - "tempfile", - "uuid", - "windows 0.58.0", -] - [[package]] name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is-docker" version = "0.2.0" @@ -8355,17 +2892,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "is-terminal" -version = "0.4.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "is-wsl" version = "0.4.0" @@ -8376,39 +2902,12 @@ dependencies = [ "once_cell", ] -[[package]] -name = "is_executable" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baabb8b4867b26294d818bf3f651a454b6901431711abb96e296245888d6e8c4" -dependencies = [ - "windows-sys 0.60.2", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.12.1" @@ -8489,21 +2988,6 @@ dependencies = [ "libc", ] -[[package]] -name = "journal" -version = "0.1.0" -dependencies = [ - "anyhow", - "chrono", - "editor", - "gpui", - "log", - "serde", - "settings", - "shellexpand 2.1.2", - "workspace", -] - [[package]] name = "js-sys" version = "0.3.81" @@ -8514,153 +2998,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json_dotpath" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbdcfef3cf5591f0cef62da413ae795e3d1f5a00936ccec0b2071499a32efd1a" -dependencies = [ - "serde", - "serde_derive", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "json_schema_store" -version = "0.1.0" -dependencies = [ - "anyhow", - "dap", - "extension", - "gpui", - "language", - "paths", - "project", - "schemars", - "serde", - "serde_json", - "settings", - "snippet_provider", - "task", - "theme", - "util", -] - -[[package]] -name = "jsonschema" -version = "0.37.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73c9ffb2b5c56d58030e1b532d8e8389da94590515f118cf35b5cb68e4764a7e" -dependencies = [ - "ahash 0.8.12", - "bytecount", - "data-encoding", - "email_address", - "fancy-regex", - "fraction", - "getrandom 0.3.4", - "idna", - "itoa", - "num-cmp", - "num-traits", - "percent-encoding", - "referencing", - "regex", - "regex-syntax", - "reqwest 0.12.24", - "serde", - "serde_json", - "unicode-general-category", - "uuid-simd", -] - -[[package]] -name = "jsonwebtoken" -version = "9.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" -dependencies = [ - "base64 0.22.1", - "js-sys", - "pem", - "ring", - "serde", - "serde_json", - "simple_asn1", -] - -[[package]] -name = "jupyter-protocol" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c047f6b5e551563af2ddb13dafed833f0ec5a5b0f9621d5ad740a9ff1e1095" -dependencies = [ - "async-trait", - "bytes 1.10.1", - "chrono", - "futures 0.3.31", - "serde", - "serde_json", - "thiserror 2.0.17", - "uuid", -] - -[[package]] -name = "jupyter-websocket-client" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4197fa926a6b0bddfed7377d9fed3d00a0dec44a1501e020097bd26604699cae" -dependencies = [ - "anyhow", - "async-trait", - "async-tungstenite", - "futures 0.3.31", - "jupyter-protocol", - "serde", - "serde_json", - "tokio", - "url", - "uuid", -] - -[[package]] -name = "keymap_editor" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "command_palette", - "component", - "db", - "editor", - "fs", - "fuzzy", - "gpui", - "itertools 0.14.0", - "json_schema_store", - "language", - "log", - "menu", - "notifications", - "paths", - "project", - "search", - "serde", - "serde_json", - "settings", - "telemetry", - "tempfile", - "theme", - "tree-sitter-json", - "tree-sitter-rust", - "ui", - "ui_input", - "util", - "workspace", - "zed_actions", -] - [[package]] name = "khronos-egl" version = "6.0.0" @@ -8671,26 +3008,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "kqueue" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - [[package]] name = "kurbo" version = "0.11.3" @@ -8711,305 +3028,6 @@ dependencies = [ "log", ] -[[package]] -name = "language" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "clock", - "collections", - "ctor", - "diffy", - "ec4rs", - "fs", - "futures 0.3.31", - "fuzzy", - "globset", - "gpui", - "http_client", - "imara-diff", - "indoc", - "itertools 0.14.0", - "log", - "lsp", - "parking_lot", - "postage", - "pretty_assertions", - "rand 0.9.2", - "regex", - "rpc", - "schemars", - "serde", - "serde_json", - "settings", - "shellexpand 2.1.2", - "smallvec", - "smol", - "streaming-iterator", - "strsim", - "sum_tree", - "task", - "text", - "theme", - "toml 0.8.23", - "tree-sitter", - "tree-sitter-elixir", - "tree-sitter-embedded-template", - "tree-sitter-heex", - "tree-sitter-html", - "tree-sitter-json", - "tree-sitter-md", - "tree-sitter-python", - "tree-sitter-ruby", - "tree-sitter-rust", - "tree-sitter-typescript", - "unicase", - "unindent", - "util", - "watch", - "zlog", -] - -[[package]] -name = "language_extension" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "collections", - "extension", - "fs", - "futures 0.3.31", - "gpui", - "language", - "log", - "lsp", - "project", - "serde", - "serde_json", - "util", -] - -[[package]] -name = "language_model" -version = "0.1.0" -dependencies = [ - "anthropic", - "anyhow", - "base64 0.22.1", - "client", - "cloud_api_types", - "cloud_llm_client", - "collections", - "futures 0.3.31", - "gpui", - "http_client", - "icons", - "image", - "log", - "open_ai", - "open_router", - "parking_lot", - "proto", - "schemars", - "serde", - "serde_json", - "settings", - "smol", - "telemetry_events", - "thiserror 2.0.17", - "util", -] - -[[package]] -name = "language_models" -version = "0.1.0" -dependencies = [ - "ai_onboarding", - "anthropic", - "anyhow", - "aws-config", - "aws-credential-types", - "aws_http_client", - "bedrock", - "chrono", - "client", - "cloud_llm_client", - "collections", - "component", - "convert_case 0.8.0", - "copilot", - "credentials_provider", - "deepseek", - "editor", - "fs", - "futures 0.3.31", - "google_ai", - "gpui", - "gpui_tokio", - "http_client", - "language", - "language_model", - "lmstudio", - "log", - "menu", - "mistral", - "ollama", - "open_ai", - "open_router", - "partial-json-fixer", - "project", - "release_channel", - "schemars", - "semver", - "serde", - "serde_json", - "settings", - "smol", - "strum 0.27.2", - "thiserror 2.0.17", - "tiktoken-rs", - "tokio", - "ui", - "ui_input", - "util", - "vercel", - "x_ai", - "zed_env_vars", -] - -[[package]] -name = "language_onboarding" -version = "0.1.0" -dependencies = [ - "db", - "editor", - "gpui", - "project", - "ui", - "workspace", -] - -[[package]] -name = "language_selector" -version = "0.1.0" -dependencies = [ - "anyhow", - "editor", - "file_finder", - "file_icons", - "fuzzy", - "gpui", - "language", - "picker", - "project", - "settings", - "ui", - "util", - "workspace", -] - -[[package]] -name = "language_tools" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "command_palette_hooks", - "copilot", - "editor", - "futures 0.3.31", - "gpui", - "itertools 0.14.0", - "language", - "lsp", - "project", - "proto", - "release_channel", - "semver", - "serde_json", - "settings", - "theme", - "tree-sitter", - "ui", - "util", - "workspace", - "zed_actions", - "zlog", -] - -[[package]] -name = "languages" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-compression", - "async-fs", - "async-tar", - "async-trait", - "chrono", - "collections", - "futures 0.3.31", - "globset", - "gpui", - "http_client", - "itertools 0.14.0", - "json_schema_store", - "language", - "log", - "lsp", - "node_runtime", - "parking_lot", - "pet", - "pet-conda", - "pet-core", - "pet-fs", - "pet-poetry", - "pet-reporter", - "pet-virtualenv", - "pretty_assertions", - "project", - "regex", - "rope", - "rust-embed", - "serde", - "serde_json", - "serde_json_lenient", - "settings", - "smallvec", - "smol", - "snippet", - "task", - "terminal", - "text", - "theme", - "toml 0.8.23", - "tree-sitter", - "tree-sitter-bash", - "tree-sitter-c", - "tree-sitter-cpp", - "tree-sitter-css", - "tree-sitter-diff", - "tree-sitter-gitcommit", - "tree-sitter-go", - "tree-sitter-gomod", - "tree-sitter-gowork", - "tree-sitter-jsdoc", - "tree-sitter-json", - "tree-sitter-md", - "tree-sitter-python", - "tree-sitter-regex", - "tree-sitter-rust", - "tree-sitter-typescript", - "tree-sitter-yaml", - "unindent", - "url", - "util", - "workspace", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -9034,18 +3052,6 @@ dependencies = [ "leak", ] -[[package]] -name = "leb128" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "lebe" version = "0.5.3" @@ -9058,16 +3064,6 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" -[[package]] -name = "libdbus-sys" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f" -dependencies = [ - "cc", - "pkg-config", -] - [[package]] name = "libfuzzer-sys" version = "0.4.10" @@ -9106,16 +3102,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" -[[package]] -name = "libmimalloc-sys" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "libredox" version = "0.1.10" @@ -9127,40 +3113,6 @@ dependencies = [ "redox_syscall 0.5.18", ] -[[package]] -name = "libsqlite3-sys" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "libwebrtc" -version = "0.3.10" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d" -dependencies = [ - "cxx", - "jni", - "js-sys", - "lazy_static", - "livekit-protocol", - "livekit-runtime", - "log", - "parking_lot", - "serde", - "serde_json", - "thiserror 1.0.69", - "tokio", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webrtc-sys", -] - [[package]] name = "libz-sys" version = "1.1.22" @@ -9173,38 +3125,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "line_ending_selector" -version = "0.1.0" -dependencies = [ - "editor", - "gpui", - "language", - "picker", - "project", - "ui", - "util", - "workspace", -] - -[[package]] -name = "link-cplusplus" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" -dependencies = [ - "cc", -] - -[[package]] -name = "linkify" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1dfa36d52c581e9ec783a7ce2a5e0143da6237be5811a0b3153fedfdbe9f780" -dependencies = [ - "memchr", -] - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -9223,156 +3143,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" -[[package]] -name = "livekit" -version = "0.7.8" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d" -dependencies = [ - "chrono", - "futures-util", - "lazy_static", - "libloading", - "libwebrtc", - "livekit-api", - "livekit-protocol", - "livekit-runtime", - "log", - "parking_lot", - "prost 0.12.6", - "semver", - "serde", - "serde_json", - "thiserror 1.0.69", - "tokio", -] - -[[package]] -name = "livekit-api" -version = "0.4.2" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d" -dependencies = [ - "futures-util", - "http 0.2.12", - "livekit-protocol", - "livekit-runtime", - "log", - "parking_lot", - "pbjson-types", - "prost 0.12.6", - "rand 0.9.2", - "reqwest 0.11.27", - "scopeguard", - "serde", - "sha2", - "thiserror 1.0.69", - "tokio", - "tokio-tungstenite 0.26.2", - "url", -] - -[[package]] -name = "livekit-protocol" -version = "0.3.9" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d" -dependencies = [ - "futures-util", - "livekit-runtime", - "parking_lot", - "pbjson", - "pbjson-types", - "prost 0.12.6", - "prost-types 0.12.6", - "serde", - "thiserror 1.0.69", - "tokio", -] - -[[package]] -name = "livekit-runtime" -version = "0.4.0" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d" -dependencies = [ - "tokio", - "tokio-stream", -] - -[[package]] -name = "livekit_api" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "jsonwebtoken", - "log", - "prost 0.9.0", - "prost-build 0.9.0", - "prost-types 0.9.0", - "serde", - "zed-reqwest", -] - -[[package]] -name = "livekit_client" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "audio", - "collections", - "core-foundation 0.10.0", - "core-video", - "coreaudio-rs 0.12.1", - "cpal", - "futures 0.3.31", - "gpui", - "gpui_tokio", - "http_client_tls", - "image", - "libwebrtc", - "livekit", - "livekit_api", - "log", - "nanoid", - "objc", - "parking_lot", - "postage", - "rodio", - "serde", - "serde_json", - "serde_urlencoded", - "settings", - "sha2", - "simplelog", - "smallvec", - "tokio-tungstenite 0.26.2", - "ui", - "util", - "zed-scap", -] - -[[package]] -name = "lmdb-master-sys" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864808e0b19fb6dd3b70ba94ee671b82fce17554cf80aeb0a155c65bb08027df" -dependencies = [ - "cc", - "doxygen-rs", - "libc", -] - -[[package]] -name = "lmstudio" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures 0.3.31", - "http_client", - "schemars", - "serde", - "serde_json", -] - [[package]] name = "lock_api" version = "0.4.14" @@ -9392,19 +3162,6 @@ dependencies = [ "value-bag", ] -[[package]] -name = "loom" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - [[package]] name = "loop9" version = "0.1.5" @@ -9414,56 +3171,12 @@ dependencies = [ "imgref", ] -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "lru-slab" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "lsp" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-pipe", - "collections", - "ctor", - "futures 0.3.31", - "gpui", - "log", - "lsp-types", - "parking_lot", - "postage", - "release_channel", - "schemars", - "semver", - "serde", - "serde_json", - "smol", - "util", - "zlog", -] - -[[package]] -name = "lsp-types" -version = "0.95.1" -source = "git+https://github.com/zed-industries/lsp-types?rev=b71ab4eeb27d9758be8092020a46fe33fbca4e33#b71ab4eeb27d9758be8092020a46fe33fbca4e33" -dependencies = [ - "bitflags 1.3.2", - "serde", - "serde_json", - "url", -] - [[package]] name = "lyon" version = "1.0.16" @@ -9533,15 +3246,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" -[[package]] -name = "mach2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" -dependencies = [ - "libc", -] - [[package]] name = "mach2" version = "0.5.0" @@ -9560,61 +3264,6 @@ dependencies = [ "libc", ] -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - -[[package]] -name = "markdown" -version = "0.1.0" -dependencies = [ - "assets", - "base64 0.22.1", - "collections", - "env_logger 0.11.8", - "fs", - "futures 0.3.31", - "gpui", - "language", - "languages", - "linkify", - "log", - "node_runtime", - "pulldown-cmark 0.12.2", - "settings", - "sum_tree", - "theme", - "ui", - "util", -] - -[[package]] -name = "markdown_preview" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-recursion", - "collections", - "editor", - "fs", - "gpui", - "html5ever 0.27.0", - "language", - "linkify", - "log", - "markup5ever_rcdom", - "pretty_assertions", - "pulldown-cmark 0.12.2", - "settings", - "theme", - "ui", - "urlencoding", - "util", - "workspace", -] - [[package]] name = "markup5ever" version = "0.12.1" @@ -9622,68 +3271,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" dependencies = [ "log", - "phf 0.11.3", + "phf", "phf_codegen", "string_cache", "string_cache_codegen", "tendril", ] -[[package]] -name = "markup5ever" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" -dependencies = [ - "log", - "tendril", - "web_atoms", -] - [[package]] name = "markup5ever_rcdom" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" dependencies = [ - "html5ever 0.27.0", - "markup5ever 0.12.1", + "html5ever", + "markup5ever", "tendril", "xml5ever", ] -[[package]] -name = "match_token" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - -[[package]] -name = "maybe-owned" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" - [[package]] name = "maybe-rayon" version = "0.1.1" @@ -9704,42 +3310,6 @@ dependencies = [ "digest", ] -[[package]] -name = "mdbook" -version = "0.4.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45a38e19bd200220ef07c892b0157ad3d2365e5b5a267ca01ad12182491eea5" -dependencies = [ - "ammonia", - "anyhow", - "chrono", - "clap", - "clap_complete", - "elasticlunr-rs", - "env_logger 0.11.8", - "futures-util", - "handlebars 5.1.2", - "ignore", - "log", - "memchr", - "notify 6.1.1", - "notify-debouncer-mini", - "once_cell", - "opener", - "pathdiff", - "pulldown-cmark 0.10.3", - "regex", - "serde", - "serde_json", - "shlex", - "tempfile", - "tokio", - "toml 0.5.11", - "topological-sort", - "walkdir", - "warp", -] - [[package]] name = "media" version = "0.1.0" @@ -9749,7 +3319,7 @@ dependencies = [ "core-foundation 0.10.0", "core-video", "ctor", - "foreign-types 0.5.0", + "foreign-types", "metal", "objc", ] @@ -9760,15 +3330,6 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" -[[package]] -name = "memfd" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" -dependencies = [ - "rustix 1.1.2", -] - [[package]] name = "memmap2" version = "0.9.8" @@ -9776,7 +3337,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" dependencies = [ "libc", - "stable_deref_trait", ] [[package]] @@ -9788,35 +3348,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "menu" -version = "0.1.0" -dependencies = [ - "gpui", -] - -[[package]] -name = "merge" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10bbef93abb1da61525bbc45eeaff6473a41907d19f8f9aa5168d214e10693e9" -dependencies = [ - "merge_derive", - "num-traits", -] - -[[package]] -name = "merge_derive" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209d075476da2e63b4b29e72a2ef627b840589588e71400a25e3565c4f849d07" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "metal" version = "0.29.0" @@ -9826,39 +3357,12 @@ dependencies = [ "bitflags 2.9.4", "block", "core-graphics-types 0.1.3", - "foreign-types 0.5.0", + "foreign-types", "log", "objc", "paste", ] -[[package]] -name = "migrator" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "convert_case 0.8.0", - "log", - "pretty_assertions", - "serde_json", - "serde_json_lenient", - "settings_json", - "streaming-iterator", - "tree-sitter", - "tree-sitter-json", - "unindent", -] - -[[package]] -name = "mimalloc" -version = "0.1.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" -dependencies = [ - "libmimalloc-sys", -] - [[package]] name = "mime" version = "0.3.17" @@ -9875,81 +3379,12 @@ 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.4", - "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.4", - "byteorder", - "cfg-if", - "crash-context", - "goblin", - "libc", - "log", - "mach2 0.4.3", - "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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniprofiler_ui" -version = "0.1.0" -dependencies = [ - "gpui", - "serde_json", - "smol", - "util", - "workspace", - "zed_actions", -] - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -9966,18 +3401,6 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - [[package]] name = "mio" version = "1.1.0" @@ -9985,51 +3408,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", - "log", "wasi", "windows-sys 0.61.2", ] -[[package]] -name = "miow" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "mistral" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures 0.3.31", - "http_client", - "schemars", - "serde", - "serde_json", - "strum 0.27.2", -] - -[[package]] -name = "moka" -version = "0.12.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" -dependencies = [ - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "parking_lot", - "portable-atomic", - "rustc_version", - "smallvec", - "tagptr", - "uuid", -] - [[package]] name = "moxcms" version = "0.7.7" @@ -10040,60 +3422,6 @@ dependencies = [ "pxfm", ] -[[package]] -name = "msvc_spectre_libs" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29e871a9861f3664f18b7e04e9301d4edd55090c2dadb4b1c602e26ab32b1f5b" -dependencies = [ - "cc", -] - -[[package]] -name = "multi_buffer" -version = "0.1.0" -dependencies = [ - "anyhow", - "buffer_diff", - "clock", - "collections", - "ctor", - "gpui", - "indoc", - "itertools 0.14.0", - "language", - "log", - "parking_lot", - "pretty_assertions", - "project", - "rand 0.9.2", - "rope", - "serde", - "settings", - "smallvec", - "smol", - "sum_tree", - "text", - "theme", - "tracing", - "tree-sitter", - "util", - "zlog", - "ztracing", -] - -[[package]] -name = "multimap" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" - -[[package]] -name = "multimap" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" - [[package]] name = "naga" version = "25.0.1" @@ -10103,8 +3431,8 @@ dependencies = [ "arrayvec", "bit-set", "bitflags 2.9.4", - "cfg_aliases 0.2.1", - "codespan-reporting 0.12.0", + "cfg_aliases", + "codespan-reporting", "half", "hashbrown 0.15.5", "hexf-parse", @@ -10119,15 +3447,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "nanoid" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" -dependencies = [ - "rand 0.8.5", -] - [[package]] name = "nanorand" version = "0.7.0" @@ -10137,106 +3456,12 @@ dependencies = [ "getrandom 0.2.16", ] -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework 2.11.1", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "nbformat" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89c7229d604d847227002715e1235cd84e81919285d904ccb290a42ecc409348" -dependencies = [ - "anyhow", - "chrono", - "jupyter-protocol", - "serde", - "serde_json", - "thiserror 1.0.69", - "uuid", -] - -[[package]] -name = "nc" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures 0.3.31", - "net", - "smol", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.9.4", - "jni-sys", - "log", - "ndk-sys", - "num_enum", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys", -] - -[[package]] -name = "net" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-io", - "smol", - "tempfile", - "windows 0.61.3", -] - [[package]] name = "new_debug_unreachable" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nix" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "cfg_aliases 0.1.1", - "libc", -] - [[package]] name = "nix" version = "0.29.0" @@ -10245,7 +3470,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.9.4", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", ] @@ -10257,33 +3482,11 @@ checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags 2.9.4", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", "memoffset", ] -[[package]] -name = "node_runtime" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-compression", - "async-std", - "async-tar", - "async-trait", - "futures 0.3.31", - "http_client", - "log", - "paths", - "semver", - "serde", - "serde_json", - "smol", - "util", - "watch", - "which 6.0.3", -] - [[package]] name = "nom" version = "7.1.3" @@ -10294,103 +3497,12 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" -[[package]] -name = "normpath" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf23ab2b905654b4cb177e30b629937b3868311d4e1cba859f899c041046e69b" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "notifications" -version = "0.1.0" -dependencies = [ - "anyhow", - "channel", - "client", - "collections", - "component", - "db", - "gpui", - "rpc", - "settings", - "sum_tree", - "time", - "ui", - "util", - "workspace", - "zed_actions", -] - -[[package]] -name = "notify" -version = "6.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" -dependencies = [ - "bitflags 2.9.4", - "crossbeam-channel", - "filetime", - "fsevent-sys 4.1.0", - "inotify 0.9.6", - "kqueue", - "libc", - "log", - "mio 0.8.11", - "walkdir", - "windows-sys 0.48.0", -] - -[[package]] -name = "notify" -version = "8.2.0" -source = "git+https://github.com/zed-industries/notify.git?rev=b4588b2e5aee68f4c0e100f140e808cbce7b1419#b4588b2e5aee68f4c0e100f140e808cbce7b1419" -dependencies = [ - "bitflags 2.9.4", - "fsevent-sys 4.1.0", - "inotify 0.11.0", - "kqueue", - "libc", - "log", - "mio 1.1.0", - "notify-types", - "walkdir", - "windows-sys 0.60.2", -] - -[[package]] -name = "notify-debouncer-mini" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d40b221972a1fc5ef4d858a2f671fb34c75983eb385463dff3780eeff6a9d43" -dependencies = [ - "crossbeam-channel", - "log", - "notify 6.1.1", -] - -[[package]] -name = "notify-types" -version = "2.0.0" -source = "git+https://github.com/zed-industries/notify.git?rev=b4588b2e5aee68f4c0e100f140e808cbce7b1419#b4588b2e5aee68f4c0e100f140e808cbce7b1419" - [[package]] name = "ntapi" version = "0.4.1" @@ -10400,15 +3512,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "num" version = "0.4.3" @@ -10450,28 +3553,15 @@ dependencies = [ "zeroize", ] -[[package]] -name = "num-cmp" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" - [[package]] name = "num-complex" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ - "bytemuck", "num-traits", ] -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - [[package]] name = "num-derive" version = "0.4.2" @@ -10483,16 +3573,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "num-format" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" -dependencies = [ - "arrayvec", - "itoa", -] - [[package]] name = "num-integer" version = "0.1.46" @@ -10544,51 +3624,6 @@ dependencies = [ "libc", ] -[[package]] -name = "num_enum" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - -[[package]] -name = "nvim-rs" -version = "0.9.2" -source = "git+https://github.com/KillTheMule/nvim-rs?rev=764dd270c642f77f10f3e19d05cc178a6cbe69f3#764dd270c642f77f10f3e19d05cc178a6cbe69f3" -dependencies = [ - "async-trait", - "futures 0.3.31", - "log", - "rmp", - "rmpv", - "tokio", - "tokio-util", -] - [[package]] name = "objc" version = "0.2.7" @@ -10632,43 +3667,6 @@ dependencies = [ "objc2-quartz-core", ] -[[package]] -name = "objc2-audio-toolbox" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07" -dependencies = [ - "bitflags 2.9.4", - "libc", - "objc2", - "objc2-core-audio", - "objc2-core-audio-types", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-audio" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" -dependencies = [ - "dispatch2", - "objc2", - "objc2-core-audio-types", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-core-audio-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" -dependencies = [ - "bitflags 2.9.4", - "objc2", -] - [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -10697,16 +3695,6 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "objc2-io-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" -dependencies = [ - "libc", - "objc2-core-foundation", -] - [[package]] name = "objc2-metal" version = "0.3.1" @@ -10765,18 +3753,6 @@ dependencies = [ "objc", ] -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "crc32fast", - "hashbrown 0.15.5", - "indexmap", - "memchr", -] - [[package]] name = "object" version = "0.37.3" @@ -10786,49 +3762,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ollama" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures 0.3.31", - "http_client", - "schemars", - "serde", - "serde_json", - "settings", -] - -[[package]] -name = "onboarding" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "component", - "db", - "documented", - "fs", - "fuzzy", - "git", - "gpui", - "menu", - "notifications", - "picker", - "project", - "schemars", - "serde", - "settings", - "telemetry", - "theme", - "ui", - "util", - "vim_mode_setting", - "workspace", - "zed_actions", - "zlog", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -10851,7 +3784,7 @@ dependencies = [ "ashpd 0.12.0", "async-fs", "async-io", - "async-lock 3.4.1", + "async-lock", "blocking", "cbc", "cipher", @@ -10865,7 +3798,7 @@ dependencies = [ "md-5", "num", "num-bigint-dig", - "pbkdf2 0.12.2", + "pbkdf2", "rand 0.9.2", "serde", "sha2", @@ -10876,12 +3809,6 @@ dependencies = [ "zvariant", ] -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - [[package]] name = "open" version = "5.3.2" @@ -10893,128 +3820,18 @@ dependencies = [ "pathdiff", ] -[[package]] -name = "open_ai" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures 0.3.31", - "http_client", - "log", - "schemars", - "serde", - "serde_json", - "settings", - "strum 0.27.2", - "thiserror 2.0.17", -] - -[[package]] -name = "open_router" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures 0.3.31", - "http_client", - "schemars", - "serde", - "serde_json", - "settings", - "strum 0.27.2", - "thiserror 2.0.17", -] - -[[package]] -name = "opener" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0812e5e4df08da354c851a3376fead46db31c2214f849d3de356d774d057681" -dependencies = [ - "bstr", - "dbus", - "normpath", - "windows-sys 0.59.0", -] - -[[package]] -name = "openssl" -version = "0.10.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "foreign-types 0.3.2", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" -[[package]] -name = "openssl-sys" -version = "0.9.110" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "optfield" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969ccca8ffc4fb105bd131a228107d5c9dd89d9d627edf3295cbe979156f9712" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - -[[package]] -name = "ordered-float" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" -dependencies = [ - "num-traits", -] - [[package]] name = "ordered-stream" version = "0.2.0" @@ -11025,150 +3842,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "ouroboros" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" -dependencies = [ - "aliasable", - "ouroboros_macro", - "static_assertions", -] - -[[package]] -name = "ouroboros_macro" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "outline" -version = "0.1.0" -dependencies = [ - "editor", - "fuzzy", - "gpui", - "indoc", - "language", - "menu", - "ordered-float 2.10.1", - "picker", - "project", - "rope", - "serde_json", - "settings", - "smol", - "theme", - "tree-sitter-rust", - "tree-sitter-typescript", - "ui", - "util", - "workspace", - "zed_actions", -] - -[[package]] -name = "outline_panel" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "db", - "editor", - "file_icons", - "fuzzy", - "gpui", - "itertools 0.14.0", - "language", - "log", - "menu", - "outline", - "pretty_assertions", - "project", - "search", - "serde", - "serde_json", - "settings", - "smallvec", - "smol", - "theme", - "ui", - "util", - "workspace", - "worktree", - "zed_actions", -] - -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - -[[package]] -name = "p256" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" -dependencies = [ - "ecdsa", - "elliptic-curve", - "sha2", -] - -[[package]] -name = "page_size" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "palette" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" -dependencies = [ - "approx", - "fast-srgb8", - "palette_derive", -] - -[[package]] -name = "palette_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" -dependencies = [ - "by_address", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "panel" -version = "0.1.0" -dependencies = [ - "editor", - "gpui", - "settings", - "theme", - "ui", - "workspace", -] - [[package]] name = "parking" version = "2.2.1" @@ -11198,43 +3871,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "parse_int" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c464266693329dd5a8715098c7f86e6c5fd5d985018b8318f53d9c6c2b21a31" -dependencies = [ - "num-traits", -] - -[[package]] -name = "partial-json-fixer" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ffd90b3f3b6477db7478016b9efb1b7e9d38eafd095f0542fe0ec2ea884a13" - -[[package]] -name = "password-hash" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "paste" version = "1.0.15" @@ -11266,64 +3902,6 @@ dependencies = [ "rustc_version", ] -[[package]] -name = "paths" -version = "0.1.0" -dependencies = [ - "dirs 4.0.0", - "ignore", - "util", -] - -[[package]] -name = "pbjson" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1030c719b0ec2a2d25a5df729d6cff1acf3cc230bf766f4f97833591f7577b90" -dependencies = [ - "base64 0.21.7", - "serde", -] - -[[package]] -name = "pbjson-build" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2580e33f2292d34be285c5bc3dba5259542b083cfad6037b6d70345f24dcb735" -dependencies = [ - "heck 0.4.1", - "itertools 0.11.0", - "prost 0.12.6", - "prost-types 0.12.6", -] - -[[package]] -name = "pbjson-types" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18f596653ba4ac51bdecbb4ef6773bc7f56042dc13927910de1684ad3d32aa12" -dependencies = [ - "bytes 1.10.1", - "chrono", - "pbjson", - "pbjson-build", - "prost 0.12.6", - "prost-build 0.12.6", - "serde", -] - -[[package]] -name = "pbkdf2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" -dependencies = [ - "digest", - "hmac", - "password-hash 0.4.2", - "sha2", -] - [[package]] name = "pbkdf2" version = "0.12.2" @@ -11334,31 +3912,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.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" -dependencies = [ - "base64 0.22.1", - "serde_core", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -11374,472 +3927,13 @@ dependencies = [ "serde_json", ] -[[package]] -name = "pest" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "pest_meta" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" -dependencies = [ - "pest", - "sha2", -] - -[[package]] -name = "pet" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "clap", - "env_logger 0.10.2", - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-conda", - "pet-core", - "pet-env-var-path", - "pet-fs", - "pet-global-virtualenvs", - "pet-homebrew", - "pet-jsonrpc", - "pet-linux-global-python", - "pet-mac-commandlinetools", - "pet-mac-python-org", - "pet-mac-xcode", - "pet-pipenv", - "pet-pixi", - "pet-poetry", - "pet-pyenv", - "pet-python-utils", - "pet-reporter", - "pet-telemetry", - "pet-uv", - "pet-venv", - "pet-virtualenv", - "pet-virtualenvwrapper", - "pet-windows-registry", - "pet-windows-store", - "serde", - "serde_json", -] - -[[package]] -name = "pet-conda" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "env_logger 0.10.2", - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-reporter", - "regex", - "serde", - "serde_json", - "yaml-rust2", -] - -[[package]] -name = "pet-core" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "clap", - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-fs", - "regex", - "serde", - "serde_json", -] - -[[package]] -name = "pet-env-var-path" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-conda", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-virtualenv", - "regex", -] - -[[package]] -name = "pet-fs" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", -] - -[[package]] -name = "pet-global-virtualenvs" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-conda", - "pet-core", - "pet-fs", - "pet-virtualenv", -] - -[[package]] -name = "pet-homebrew" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-conda", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-virtualenv", - "regex", - "serde", - "serde_json", -] - -[[package]] -name = "pet-jsonrpc" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "env_logger 0.10.2", - "log", - "msvc_spectre_libs", - "pet-core", - "serde", - "serde_json", -] - -[[package]] -name = "pet-linux-global-python" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-virtualenv", -] - -[[package]] -name = "pet-mac-commandlinetools" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-virtualenv", -] - -[[package]] -name = "pet-mac-python-org" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-virtualenv", -] - -[[package]] -name = "pet-mac-xcode" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-virtualenv", -] - -[[package]] -name = "pet-pipenv" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-virtualenv", -] - -[[package]] -name = "pet-pixi" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-conda", - "pet-core", - "pet-python-utils", -] - -[[package]] -name = "pet-poetry" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "base64 0.22.1", - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-reporter", - "pet-virtualenv", - "regex", - "serde", - "serde_json", - "sha2", - "toml 0.8.23", -] - -[[package]] -name = "pet-pyenv" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-conda", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-reporter", - "regex", - "serde", - "serde_json", -] - -[[package]] -name = "pet-python-utils" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "env_logger 0.10.2", - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "regex", - "serde", - "serde_json", - "sha2", -] - -[[package]] -name = "pet-reporter" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "env_logger 0.10.2", - "log", - "msvc_spectre_libs", - "pet-core", - "pet-jsonrpc", - "serde", - "serde_json", -] - -[[package]] -name = "pet-telemetry" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "env_logger 0.10.2", - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", - "regex", -] - -[[package]] -name = "pet-uv" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "pet-core", - "pet-python-utils", - "serde", - "toml 0.9.8", -] - -[[package]] -name = "pet-venv" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-core", - "pet-python-utils", - "pet-virtualenv", -] - -[[package]] -name = "pet-virtualenv" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", -] - -[[package]] -name = "pet-virtualenvwrapper" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-virtualenv", -] - -[[package]] -name = "pet-windows-registry" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-conda", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-virtualenv", - "pet-windows-store", - "regex", - "winreg 0.55.0", -] - -[[package]] -name = "pet-windows-store" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-virtualenv", - "regex", - "winreg 0.55.0", -] - -[[package]] -name = "petgraph" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" -dependencies = [ - "fixedbitset", - "indexmap", -] - -[[package]] -name = "pgvector" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b" -dependencies = [ - "serde", -] - [[package]] name = "phf" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" -dependencies = [ - "phf_macros 0.12.1", - "phf_shared 0.12.1", + "phf_shared", ] [[package]] @@ -11848,8 +3942,8 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", + "phf_generator", + "phf_shared", ] [[package]] @@ -11858,46 +3952,10 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ - "phf_shared 0.11.3", + "phf_shared", "rand 0.8.5", ] -[[package]] -name = "phf_generator" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cbb1126afed61dd6368748dae63b1ee7dc480191c6262a3b4ff1e29d86a6c5b" -dependencies = [ - "fastrand 2.3.0", - "phf_shared 0.12.1", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "phf_macros" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d713258393a82f091ead52047ca779d37e5766226d009de21696c4e667044368" -dependencies = [ - "phf_generator 0.12.1", - "phf_shared 0.12.1", - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "phf_shared" version = "0.11.3" @@ -11907,33 +3965,6 @@ dependencies = [ "siphasher", ] -[[package]] -name = "phf_shared" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" -dependencies = [ - "siphasher", -] - -[[package]] -name = "picker" -version = "0.1.0" -dependencies = [ - "anyhow", - "ctor", - "editor", - "env_logger 0.11.8", - "gpui", - "menu", - "schemars", - "serde", - "serde_json", - "theme", - "ui", - "workspace", -] - [[package]] name = "pico-args" version = "0.5.0" @@ -11983,90 +4014,12 @@ dependencies = [ "futures-io", ] -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der 0.7.10", - "pkcs8 0.10.2", - "spki 0.7.3", -] - -[[package]] -name = "pkcs8" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" -dependencies = [ - "der 0.6.1", - "spki 0.6.0", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der 0.7.10", - "spki 0.7.3", -] - [[package]] name = "pkg-config" 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.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" -dependencies = [ - "base64 0.22.1", - "indexmap", - "quick-xml 0.38.3", - "serde", - "time", -] - -[[package]] -name = "plotters" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" -dependencies = [ - "plotters-backend", -] - [[package]] name = "png" version = "0.17.16" @@ -12113,15 +4066,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" -[[package]] -name = "pori" -version = "0.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a63d338dec139f56dacc692ca63ad35a6be6a797442479b55acd611d79e906" -dependencies = [ - "nom 7.1.3", -] - [[package]] name = "portable-atomic" version = "1.11.1" @@ -12137,27 +4081,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "portable-pty" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" -dependencies = [ - "anyhow", - "bitflags 1.3.2", - "downcast-rs", - "filedescriptor", - "lazy_static", - "libc", - "log", - "nix 0.28.0", - "serial2", - "shared_library", - "shell-words", - "winapi", - "winreg 0.10.1", -] - [[package]] name = "postage" version = "0.5.0" @@ -12166,7 +4089,7 @@ checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" dependencies = [ "atomic", "crossbeam-queue", - "futures 0.3.31", + "futures", "log", "parking_lot", "pin-project", @@ -12175,18 +4098,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "postcard" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" -dependencies = [ - "cobs", - "embedded-io 0.4.0", - "embedded-io 0.6.1", - "serde", -] - [[package]] name = "potential_utf" version = "0.1.3" @@ -12196,12 +4107,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -12217,25 +4122,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" -[[package]] -name = "prettier" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "fs", - "gpui", - "language", - "log", - "lsp", - "node_runtime", - "parking_lot", - "paths", - "serde", - "serde_json", - "util", -] - [[package]] name = "pretty_assertions" version = "1.4.1" @@ -12256,15 +4142,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "primal-check" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" -dependencies = [ - "num-integer", -] - [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -12274,30 +4151,6 @@ dependencies = [ "toml_edit 0.23.7", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -12329,29 +4182,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "proc-macro2-diagnostics" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "version_check", - "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.4", - "hex", -] - [[package]] name = "profiling" version = "1.0.17" @@ -12371,336 +4201,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "project" -version = "0.1.0" -dependencies = [ - "aho-corasick", - "anyhow", - "askpass", - "async-trait", - "base64 0.22.1", - "buffer_diff", - "circular-buffer", - "client", - "clock", - "collections", - "context_server", - "dap", - "dap_adapters", - "extension", - "fancy-regex", - "fs", - "futures 0.3.31", - "fuzzy", - "git", - "git2", - "git_hosting_providers", - "globset", - "gpui", - "http_client", - "image", - "indexmap", - "itertools 0.14.0", - "language", - "log", - "lsp", - "markdown", - "node_runtime", - "parking_lot", - "paths", - "postage", - "prettier", - "pretty_assertions", - "rand 0.9.2", - "regex", - "release_channel", - "remote", - "rpc", - "schemars", - "semver", - "serde", - "serde_json", - "settings", - "sha2", - "shellexpand 2.1.2", - "smallvec", - "smol", - "snippet", - "snippet_provider", - "sum_tree", - "task", - "tempfile", - "terminal", - "text", - "toml 0.8.23", - "tracing", - "unindent", - "url", - "util", - "watch", - "wax", - "which 6.0.3", - "worktree", - "zeroize", - "zlog", - "ztracing", -] - -[[package]] -name = "project_benchmarks" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "client", - "futures 0.3.31", - "gpui", - "http_client", - "language", - "node_runtime", - "project", - "settings", - "watch", -] - -[[package]] -name = "project_panel" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "command_palette_hooks", - "criterion", - "db", - "editor", - "file_icons", - "git", - "git_ui", - "gpui", - "language", - "menu", - "pretty_assertions", - "project", - "rayon", - "schemars", - "search", - "serde", - "serde_json", - "settings", - "smallvec", - "telemetry", - "tempfile", - "theme", - "ui", - "util", - "workspace", - "worktree", - "zed_actions", -] - -[[package]] -name = "project_symbols" -version = "0.1.0" -dependencies = [ - "anyhow", - "editor", - "futures 0.3.31", - "fuzzy", - "gpui", - "language", - "lsp", - "ordered-float 2.10.1", - "picker", - "project", - "release_channel", - "semver", - "serde_json", - "settings", - "theme", - "util", - "workspace", -] - -[[package]] -name = "prometheus" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" -dependencies = [ - "cfg-if", - "fnv", - "lazy_static", - "memchr", - "parking_lot", - "protobuf", - "thiserror 2.0.17", -] - -[[package]] -name = "prompt_store" -version = "0.1.0" -dependencies = [ - "anyhow", - "assets", - "chrono", - "collections", - "fs", - "futures 0.3.31", - "fuzzy", - "gpui", - "handlebars 4.5.0", - "heed", - "language", - "log", - "parking_lot", - "paths", - "rope", - "serde", - "text", - "util", - "uuid", -] - -[[package]] -name = "prost" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" -dependencies = [ - "bytes 1.10.1", - "prost-derive 0.9.0", -] - -[[package]] -name = "prost" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" -dependencies = [ - "bytes 1.10.1", - "prost-derive 0.12.6", -] - -[[package]] -name = "prost-build" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" -dependencies = [ - "bytes 1.10.1", - "heck 0.3.3", - "itertools 0.10.5", - "lazy_static", - "log", - "multimap 0.8.3", - "petgraph", - "prost 0.9.0", - "prost-types 0.9.0", - "regex", - "tempfile", - "which 4.4.2", -] - -[[package]] -name = "prost-build" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" -dependencies = [ - "bytes 1.10.1", - "heck 0.5.0", - "itertools 0.12.1", - "log", - "multimap 0.10.1", - "once_cell", - "petgraph", - "prettyplease", - "prost 0.12.6", - "prost-types 0.12.6", - "regex", - "syn 2.0.106", - "tempfile", -] - -[[package]] -name = "prost-derive" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" -dependencies = [ - "anyhow", - "itertools 0.10.5", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "prost-derive" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" -dependencies = [ - "anyhow", - "itertools 0.12.1", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "prost-types" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" -dependencies = [ - "bytes 1.10.1", - "prost 0.9.0", -] - -[[package]] -name = "prost-types" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" -dependencies = [ - "prost 0.12.6", -] - -[[package]] -name = "proto" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "prost 0.9.0", - "prost-build 0.9.0", - "serde", - "typed-path", -] - -[[package]] -name = "protobuf" -version = "3.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" -dependencies = [ - "once_cell", - "protobuf-support", - "thiserror 1.0.69", -] - -[[package]] -name = "protobuf-support" -version = "3.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" -dependencies = [ - "thiserror 1.0.69", -] - [[package]] name = "psm" version = "0.1.27" @@ -12710,93 +4210,6 @@ dependencies = [ "cc", ] -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "pulldown-cmark" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993" -dependencies = [ - "bitflags 2.9.4", - "memchr", - "pulldown-cmark-escape", - "unicase", -] - -[[package]] -name = "pulldown-cmark" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" -dependencies = [ - "bitflags 2.9.4", - "memchr", - "unicase", -] - -[[package]] -name = "pulldown-cmark-escape" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd348ff538bc9caeda7ee8cad2d1d48236a1f443c1fa3913c6a02fe0043b1dd3" - -[[package]] -name = "pulley-interpreter" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62d95f8575df49a2708398182f49a888cf9dc30210fb1fd2df87c889edcee75d" -dependencies = [ - "cranelift-bitset", - "log", - "sptr", - "wasmtime-math", -] - -[[package]] -name = "pulp" -version = "0.18.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0a01a0dc67cf4558d279f0c25b0962bd08fc6dec0137699eae304103e882fe6" -dependencies = [ - "bytemuck", - "libm", - "num-complex", - "reborrow", -] - -[[package]] -name = "pulp" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b86df24f0a7ddd5e4b95c94fc9ed8a98f1ca94d3b01bdce2824097e7835907" -dependencies = [ - "bytemuck", - "cfg-if", - "libm", - "num-complex", - "reborrow", - "version_check", -] - [[package]] name = "pxfm" version = "0.1.25" @@ -12839,29 +4252,20 @@ dependencies = [ "memchr", ] -[[package]] -name = "quick-xml" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" -dependencies = [ - "memchr", -] - [[package]] name = "quinn" version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ - "bytes 1.10.1", - "cfg_aliases 0.2.1", + "bytes", + "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls 0.23.33", - "socket2 0.6.1", + "rustls", + "socket2", "thiserror 2.0.17", "tokio", "tracing", @@ -12874,13 +4278,13 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ - "bytes 1.10.1", + "bytes", "getrandom 0.3.4", "lru-slab", "rand 0.9.2", "ring", "rustc-hash 2.1.1", - "rustls 0.23.33", + "rustls", "rustls-pki-types", "slab", "thiserror 2.0.17", @@ -12895,10 +4299,10 @@ version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2", "tracing", "windows-sys 0.60.2", ] @@ -12918,12 +4322,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" version = "0.8.5" @@ -12983,25 +4381,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "rand_distr" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" -dependencies = [ - "num-traits", - "rand 0.9.2", -] - -[[package]] -name = "range-map" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12a5a2d6c7039059af621472a4389be1215a816df61aa4d531cfe85264aee95f" -dependencies = [ - "num-traits", -] - [[package]] name = "rangemap" version = "1.6.0" @@ -13058,24 +4437,6 @@ dependencies = [ "rgb", ] -[[package]] -name = "raw-cpuid" -version = "10.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "raw-cpuid" -version = "11.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" -dependencies = [ - "bitflags 2.9.4", -] - [[package]] name = "raw-window-handle" version = "0.6.2" @@ -13124,64 +4485,6 @@ dependencies = [ "font-types", ] -[[package]] -name = "realfft" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677" -dependencies = [ - "rustfft", -] - -[[package]] -name = "reborrow" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" - -[[package]] -name = "recent_projects" -version = "0.1.0" -dependencies = [ - "anyhow", - "askpass", - "auto_update", - "dap", - "db", - "editor", - "extension_host", - "file_finder", - "futures 0.3.31", - "fuzzy", - "gpui", - "indoc", - "language", - "log", - "markdown", - "menu", - "node_runtime", - "ordered-float 2.10.1", - "paths", - "picker", - "project", - "release_channel", - "remote", - "semver", - "serde", - "serde_json", - "settings", - "smol", - "task", - "telemetry", - "theme", - "ui", - "util", - "windows-registry 0.6.1", - "workspace", - "worktree", - "zed_actions", -] - [[package]] name = "redox_syscall" version = "0.2.16" @@ -13211,17 +4514,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.16", - "libredox", - "thiserror 2.0.17", -] - [[package]] name = "ref-cast" version = "1.0.25" @@ -13242,21 +4534,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "referencing" -version = "0.37.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4283168a506f0dcbdce31c9f9cce3129c924da4c6bca46e46707fcb746d2d70c" -dependencies = [ - "ahash 0.8.12", - "fluent-uri", - "getrandom 0.3.4", - "hashbrown 0.16.1", - "parking_lot", - "percent-encoding", - "serde_json", -] - [[package]] name = "refineable" version = "0.1.0" @@ -13264,21 +4541,6 @@ dependencies = [ "derive_refineable", ] -[[package]] -name = "regalloc2" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc06e6b318142614e4a48bc725abbf08ff166694835c43c9dae5a9009704639a" -dependencies = [ - "allocator-api2", - "bumpalo", - "hashbrown 0.15.5", - "log", - "rustc-hash 2.1.1", - "serde", - "smallvec", -] - [[package]] name = "regex" version = "1.12.2" @@ -13302,272 +4564,19 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-lite" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" - [[package]] name = "regex-syntax" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" -[[package]] -name = "release_channel" -version = "0.1.0" -dependencies = [ - "gpui", - "semver", -] - -[[package]] -name = "remote" -version = "0.1.0" -dependencies = [ - "anyhow", - "askpass", - "async-trait", - "collections", - "fs", - "futures 0.3.31", - "gpui", - "log", - "parking_lot", - "paths", - "prost 0.9.0", - "release_channel", - "rpc", - "schemars", - "semver", - "serde", - "serde_json", - "settings", - "smol", - "tempfile", - "thiserror 2.0.17", - "urlencoding", - "util", - "which 6.0.3", -] - -[[package]] -name = "remote_server" -version = "0.1.0" -dependencies = [ - "action_log", - "agent", - "anyhow", - "askpass", - "cargo_toml", - "clap", - "client", - "clock", - "collections", - "crash-handler", - "crashes", - "dap", - "dap_adapters", - "debug_adapter_extension", - "editor", - "env_logger 0.11.8", - "extension", - "extension_host", - "fork", - "fs", - "futures 0.3.31", - "git", - "git2", - "git_hosting_providers", - "gpui", - "gpui_tokio", - "http_client", - "image", - "json_schema_store", - "language", - "language_extension", - "language_model", - "languages", - "libc", - "log", - "lsp", - "minidumper", - "node_runtime", - "paths", - "pretty_assertions", - "project", - "prompt_store", - "proto", - "rayon", - "release_channel", - "remote", - "reqwest_client", - "rpc", - "rust-embed", - "semver", - "serde", - "serde_json", - "settings", - "shellexpand 2.1.2", - "smol", - "sysinfo 0.37.2", - "task", - "theme", - "thiserror 2.0.17", - "toml 0.8.23", - "unindent", - "util", - "watch", - "workspace", - "worktree", - "zlog", -] - -[[package]] -name = "rend" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" -dependencies = [ - "bytecheck", -] - -[[package]] -name = "repl" -version = "0.1.0" -dependencies = [ - "alacritty_terminal", - "anyhow", - "async-dispatcher", - "async-tungstenite", - "base64 0.22.1", - "client", - "collections", - "command_palette_hooks", - "editor", - "env_logger 0.11.8", - "feature_flags", - "file_icons", - "futures 0.3.31", - "gpui", - "http_client", - "image", - "indoc", - "jupyter-protocol", - "jupyter-websocket-client", - "language", - "languages", - "log", - "markdown_preview", - "menu", - "multi_buffer", - "nbformat", - "picker", - "project", - "runtimelib", - "serde", - "serde_json", - "settings", - "smol", - "telemetry", - "terminal", - "terminal_view", - "theme", - "tree-sitter-md", - "tree-sitter-python", - "tree-sitter-typescript", - "ui", - "util", - "uuid", - "workspace", -] - -[[package]] -name = "reqwest" -version = "0.11.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" -dependencies = [ - "base64 0.21.7", - "bytes 1.10.1", - "encoding_rs", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper-rustls 0.24.2", - "hyper-tls", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls 0.21.12", - "rustls-native-certs 0.6.3", - "rustls-pemfile 1.0.4", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 0.1.2", - "system-configuration 0.5.1", - "tokio", - "tokio-native-tls", - "tokio-rustls 0.24.1", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg 0.50.0", -] - -[[package]] -name = "reqwest" -version = "0.12.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" -dependencies = [ - "base64 0.22.1", - "bytes 1.10.1", - "futures-channel", - "futures-core", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "hyper 1.7.0", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 1.0.2", - "tokio", - "tower 0.5.2", - "tower-http 0.6.6", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "reqwest_client" version = "0.1.0" dependencies = [ "anyhow", - "bytes 1.10.1", - "futures 0.3.31", + "bytes", + "futures", "gpui", "http_client", "http_client_tls", @@ -13592,17 +4601,6 @@ dependencies = [ "usvg", ] -[[package]] -name = "rfc6979" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" -dependencies = [ - "crypto-bigint 0.4.9", - "hmac", - "zeroize", -] - [[package]] name = "rgb" version = "0.8.52" @@ -13612,20 +4610,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "rich_text" -version = "0.1.0" -dependencies = [ - "futures 0.3.31", - "gpui", - "language", - "linkify", - "pulldown-cmark 0.12.2", - "theme", - "ui", - "util", -] - [[package]] name = "ring" version = "0.17.14" @@ -13636,202 +4620,16 @@ dependencies = [ "cfg-if", "getrandom 0.2.16", "libc", - "untrusted 0.9.0", + "untrusted", "windows-sys 0.52.0", ] -[[package]] -name = "rkyv" -version = "0.7.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" -dependencies = [ - "bitvec", - "bytecheck", - "bytes 1.10.1", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "rmp" -version = "0.8.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" -dependencies = [ - "byteorder", - "num-traits", - "paste", -] - -[[package]] -name = "rmpv" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58450723cd9ee93273ce44a20b6ec4efe17f8ed2e3631474387bfdecf18bb2a9" -dependencies = [ - "num-traits", - "rmp", -] - -[[package]] -name = "rodio" -version = "0.21.1" -source = "git+https://github.com/RustAudio/rodio?rev=e2074c6c2acf07b57cf717e076bdda7a9ac6e70b#e2074c6c2acf07b57cf717e076bdda7a9ac6e70b" -dependencies = [ - "cpal", - "dasp_sample", - "hound", - "num-rational", - "rtrb", - "symphonia", - "thiserror 2.0.17", -] - -[[package]] -name = "rope" -version = "0.1.0" -dependencies = [ - "arrayvec", - "criterion", - "ctor", - "gpui", - "log", - "rand 0.9.2", - "rayon", - "sum_tree", - "tracing", - "unicode-segmentation", - "util", - "zlog", - "ztracing", -] - [[package]] name = "roxmltree" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" -[[package]] -name = "rpc" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-tungstenite", - "base64 0.22.1", - "chrono", - "collections", - "futures 0.3.31", - "gpui", - "parking_lot", - "proto", - "rand 0.9.2", - "rsa", - "serde", - "serde_json", - "sha2", - "strum 0.27.2", - "tracing", - "util", - "zlog", - "zstd", -] - -[[package]] -name = "rsa" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8 0.10.2", - "rand_core 0.6.4", - "signature 2.2.0", - "spki 0.7.3", - "subtle", - "zeroize", -] - -[[package]] -name = "rtrb" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8388ea1a9e0ea807e442e8263a699e7edcb320ecbcd21b4fa8ff859acce3ba" - -[[package]] -name = "rules_library" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "editor", - "gpui", - "language", - "language_model", - "log", - "menu", - "picker", - "prompt_store", - "release_channel", - "rope", - "serde", - "settings", - "theme", - "title_bar", - "ui", - "util", - "workspace", - "zed_actions", -] - -[[package]] -name = "runtimelib" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "481b48894073a0096f28cbe9860af01fc1b861e55b3bc96afafc645ee3de62dc" -dependencies = [ - "async-dispatcher", - "async-std", - "aws-lc-rs", - "base64 0.22.1", - "bytes 1.10.1", - "chrono", - "data-encoding", - "dirs 6.0.0", - "futures 0.3.31", - "glob", - "jupyter-protocol", - "serde", - "serde_json", - "shellexpand 3.1.1", - "smol", - "thiserror 2.0.17", - "uuid", - "zeromq", -] - [[package]] name = "rust-embed" version = "8.7.2" @@ -13867,32 +4665,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "rust-stemmers" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" -dependencies = [ - "serde", - "serde_derive", -] - -[[package]] -name = "rust_decimal" -version = "1.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" -dependencies = [ - "arrayvec", - "borsh", - "bytes 1.10.1", - "num-traits", - "rand 0.8.5", - "rkyv", - "serde", - "serde_json", -] - [[package]] name = "rustc-demangle" version = "0.1.26" @@ -13920,20 +4692,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustfft" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" -dependencies = [ - "num-complex", - "num-integer", - "num-traits", - "primal-check", - "strength_reduce", - "transpose", -] - [[package]] name = "rustix" version = "0.38.44" @@ -13941,7 +4699,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags 2.9.4", - "errno 0.3.14", + "errno", "libc", "linux-raw-sys 0.4.15", "windows-sys 0.59.0", @@ -13954,45 +4712,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags 2.9.4", - "errno 0.3.14", + "errno", "libc", "linux-raw-sys 0.11.0", "windows-sys 0.61.2", ] -[[package]] -name = "rustix-linux-procfs" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" -dependencies = [ - "once_cell", - "rustix 1.1.2", -] - -[[package]] -name = "rustix-openpty" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de16c7c59892b870a6336f185dc10943517f1327447096bbb7bb32cd85e2393" -dependencies = [ - "errno 0.3.14", - "libc", - "rustix 1.1.2", -] - -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] - [[package]] name = "rustls" version = "0.23.33" @@ -14004,23 +4729,11 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.7", + "rustls-webpki", "subtle", "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe", - "rustls-pemfile 1.0.4", - "schannel", - "security-framework 2.11.1", -] - [[package]] name = "rustls-native-certs" version = "0.8.2" @@ -14030,16 +4743,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.5.1", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", + "security-framework", ] [[package]] @@ -14072,11 +4776,11 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.33", - "rustls-native-certs 0.8.2", + "rustls", + "rustls-native-certs", "rustls-platform-verifier-android", - "rustls-webpki 0.103.7", - "security-framework 3.5.1", + "rustls-webpki", + "security-framework", "security-framework-sys", "webpki-root-certs", "windows-sys 0.59.0", @@ -14088,16 +4792,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted 0.9.0", -] - [[package]] name = "rustls-webpki" version = "0.103.7" @@ -14107,7 +4801,7 @@ dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted 0.9.0", + "untrusted", ] [[package]] @@ -14157,25 +4851,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "safetensors" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44560c11236a6130a46ce36c836a62936dc81ebf8c36a37947423571be0e55b6" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher", -] - [[package]] name = "same-file" version = "1.0.6" @@ -14201,24 +4876,11 @@ dependencies = [ "async-task", "backtrace", "chrono", - "futures 0.3.31", + "futures", "parking_lot", "rand 0.9.2", ] -[[package]] -name = "schema_generator" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "env_logger 0.11.8", - "schemars", - "serde", - "serde_json", - "theme", -] - [[package]] name = "schemars" version = "1.0.4" @@ -14257,12 +4919,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scratch" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" - [[package]] name = "screencapturekit" version = "0.2.8" @@ -14286,203 +4942,12 @@ 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.106", -] - -[[package]] -name = "scrypt" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" -dependencies = [ - "password-hash 0.5.0", - "pbkdf2 0.12.2", - "salsa20", - "sha2", -] - -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted 0.9.0", -] - -[[package]] -name = "sea-bae" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" -dependencies = [ - "heck 0.4.1", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "sea-orm" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e61af841881c137d4bc8e0d8411cee9168548b404f9e4788e8af7e8f94bd4e" -dependencies = [ - "async-stream", - "async-trait", - "bigdecimal", - "chrono", - "futures-util", - "log", - "ouroboros", - "pgvector", - "rust_decimal", - "sea-orm-macros", - "sea-query", - "sea-query-binder", - "serde", - "serde_json", - "sqlx", - "strum 0.26.3", - "thiserror 2.0.17", - "time", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "sea-orm-macros" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b86e3e77b548e6c6c1f612a1ca024d557dffdb81b838bf482ad3222140c77b" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "sea-bae", - "syn 2.0.106", - "unicode-ident", -] - -[[package]] -name = "sea-query" -version = "0.32.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a5d1c518eaf5eda38e5773f902b26ab6d5e9e9e2bb2349ca6c64cf96f80448c" -dependencies = [ - "bigdecimal", - "chrono", - "inherent", - "ordered-float 4.6.0", - "rust_decimal", - "serde_json", - "time", - "uuid", -] - -[[package]] -name = "sea-query-binder" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608" -dependencies = [ - "bigdecimal", - "chrono", - "rust_decimal", - "sea-query", - "serde_json", - "sqlx", - "time", - "uuid", -] - [[package]] name = "seahash" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" -[[package]] -name = "search" -version = "0.1.0" -dependencies = [ - "any_vec", - "anyhow", - "bitflags 2.9.4", - "client", - "collections", - "editor", - "futures 0.3.31", - "gpui", - "itertools 0.14.0", - "language", - "lsp", - "menu", - "pretty_assertions", - "project", - "schemars", - "serde", - "serde_json", - "settings", - "smol", - "theme", - "tracing", - "ui", - "unindent", - "util", - "util_macros", - "workspace", - "zed_actions", - "ztracing", -] - -[[package]] -name = "sec1" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" -dependencies = [ - "base16ct", - "der 0.6.1", - "generic-array", - "pkcs8 0.9.0", - "subtle", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.9.4", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - [[package]] name = "security-framework" version = "3.5.1" @@ -14522,12 +4987,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "seq-macro" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" - [[package]] name = "serde" version = "1.0.228" @@ -14605,17 +5064,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_path_to_error" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = [ - "itoa", - "serde", - "serde_core", -] - [[package]] name = "serde_repr" version = "0.1.20" @@ -14657,171 +5105,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "serial2" -version = "0.2.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cc76fa68e25e771492ca1e3c53d447ef0be3093e05cd3b47f4b712ba10c6f3c" -dependencies = [ - "cfg-if", - "libc", - "winapi", -] - -[[package]] -name = "session" -version = "0.1.0" -dependencies = [ - "db", - "gpui", - "serde_json", - "util", - "uuid", -] - -[[package]] -name = "settings" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "derive_more 0.99.20", - "ec4rs", - "fs", - "futures 0.3.31", - "gpui", - "indoc", - "inventory", - "log", - "migrator", - "paths", - "pretty_assertions", - "release_channel", - "rust-embed", - "schemars", - "serde", - "serde_json", - "serde_json_lenient", - "serde_repr", - "settings_json", - "settings_macros", - "smallvec", - "strum 0.27.2", - "unindent", - "util", - "zlog", -] - -[[package]] -name = "settings_json" -version = "0.1.0" -dependencies = [ - "anyhow", - "pretty_assertions", - "serde", - "serde_json", - "serde_json_lenient", - "serde_path_to_error", - "tree-sitter", - "tree-sitter-json", - "unindent", - "util", -] - -[[package]] -name = "settings_macros" -version = "0.1.0" -dependencies = [ - "quote", - "settings", - "syn 2.0.106", -] - -[[package]] -name = "settings_profile_selector" -version = "0.1.0" -dependencies = [ - "client", - "editor", - "fuzzy", - "gpui", - "language", - "menu", - "picker", - "project", - "serde_json", - "settings", - "theme", - "ui", - "workspace", - "zed_actions", -] - -[[package]] -name = "settings_ui" -version = "0.1.0" -dependencies = [ - "anyhow", - "assets", - "bm25", - "client", - "editor", - "feature_flags", - "fs", - "futures 0.3.31", - "fuzzy", - "gpui", - "heck 0.5.0", - "language", - "log", - "menu", - "node_runtime", - "paths", - "picker", - "pretty_assertions", - "project", - "release_channel", - "schemars", - "search", - "serde", - "session", - "settings", - "strum 0.27.2", - "telemetry", - "theme", - "title_bar", - "ui", - "ui_input", - "util", - "workspace", - "zed_actions", - "zlog", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sha1_smol" version = "1.0.1" @@ -14839,65 +5122,12 @@ dependencies = [ "digest", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shared_library" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" -dependencies = [ - "lazy_static", - "libc", -] - -[[package]] -name = "shell-words" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" - -[[package]] -name = "shellexpand" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" -dependencies = [ - "dirs 4.0.0", -] - -[[package]] -name = "shellexpand" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" -dependencies = [ - "dirs 6.0.0", -] - [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - [[package]] name = "signal-hook-registry" version = "1.4.6" @@ -14907,26 +5137,6 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - [[package]] name = "simd-adler32" version = "0.3.7" @@ -14942,24 +5152,6 @@ dependencies = [ "quote", ] -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "simple_asn1" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror 2.0.17", - "time", -] - [[package]] name = "simplecss" version = "0.2.2" @@ -14969,17 +5161,6 @@ dependencies = [ "log", ] -[[package]] -name = "simplelog" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" -dependencies = [ - "log", - "termcolor", - "time", -] - [[package]] name = "siphasher" version = "1.0.1" @@ -15002,13 +5183,6 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" -[[package]] -name = "slash_commands_example" -version = "0.1.0" -dependencies = [ - "zed_extension_api 0.1.0", -] - [[package]] name = "slotmap" version = "1.0.7" @@ -15023,20 +5197,6 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -dependencies = [ - "serde", -] - -[[package]] -name = "smart-default" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] [[package]] name = "smol" @@ -15048,7 +5208,7 @@ dependencies = [ "async-executor", "async-fs", "async-io", - "async-lock 3.4.1", + "async-lock", "async-net", "async-process", "blocking", @@ -15061,62 +5221,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" -[[package]] -name = "snippet" -version = "0.1.0" -dependencies = [ - "anyhow", - "smallvec", -] - -[[package]] -name = "snippet_provider" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "extension", - "fs", - "futures 0.3.31", - "gpui", - "indoc", - "parking_lot", - "paths", - "schemars", - "serde", - "serde_json", - "serde_json_lenient", - "snippet", - "util", -] - -[[package]] -name = "snippets_ui" -version = "0.1.0" -dependencies = [ - "file_finder", - "file_icons", - "fuzzy", - "gpui", - "language", - "paths", - "picker", - "settings", - "ui", - "util", - "workspace", -] - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.1" @@ -15127,15 +5231,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "spdx" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" -dependencies = [ - "smallvec", -] - [[package]] name = "spin" version = "0.9.8" @@ -15163,278 +5258,6 @@ dependencies = [ "bitflags 2.9.4", ] -[[package]] -name = "spki" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" -dependencies = [ - "base64ct", - "der 0.6.1", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der 0.7.10", -] - -[[package]] -name = "sptr" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" - -[[package]] -name = "sqlez" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "futures 0.3.31", - "indoc", - "libsqlite3-sys", - "log", - "parking_lot", - "smol", - "sqlformat", - "thread_local", - "util", - "uuid", -] - -[[package]] -name = "sqlez_macros" -version = "0.1.0" -dependencies = [ - "sqlez", - "sqlformat", - "syn 2.0.106", -] - -[[package]] -name = "sqlformat" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" -dependencies = [ - "nom 7.1.3", - "unicode_categories", -] - -[[package]] -name = "sqlx" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" -dependencies = [ - "sqlx-core", - "sqlx-macros", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", -] - -[[package]] -name = "sqlx-core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" -dependencies = [ - "base64 0.22.1", - "bigdecimal", - "bytes 1.10.1", - "chrono", - "crc", - "crossbeam-queue", - "either", - "event-listener 5.4.1", - "futures-core", - "futures-intrusive", - "futures-io", - "futures-util", - "hashbrown 0.15.5", - "hashlink 0.10.0", - "indexmap", - "log", - "memchr", - "once_cell", - "percent-encoding", - "rust_decimal", - "rustls 0.23.33", - "serde", - "serde_json", - "sha2", - "smallvec", - "thiserror 2.0.17", - "time", - "tokio", - "tokio-stream", - "tracing", - "url", - "uuid", - "webpki-roots", -] - -[[package]] -name = "sqlx-macros" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" -dependencies = [ - "proc-macro2", - "quote", - "sqlx-core", - "sqlx-macros-core", - "syn 2.0.106", -] - -[[package]] -name = "sqlx-macros-core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" -dependencies = [ - "dotenvy", - "either", - "heck 0.5.0", - "hex", - "once_cell", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2", - "sqlx-core", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", - "syn 2.0.106", - "tokio", - "url", -] - -[[package]] -name = "sqlx-mysql" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" -dependencies = [ - "atoi", - "base64 0.22.1", - "bigdecimal", - "bitflags 2.9.4", - "byteorder", - "bytes 1.10.1", - "chrono", - "crc", - "digest", - "dotenvy", - "either", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "generic-array", - "hex", - "hkdf", - "hmac", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "percent-encoding", - "rand 0.8.5", - "rsa", - "rust_decimal", - "serde", - "sha1", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror 2.0.17", - "time", - "tracing", - "uuid", - "whoami", -] - -[[package]] -name = "sqlx-postgres" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" -dependencies = [ - "atoi", - "base64 0.22.1", - "bigdecimal", - "bitflags 2.9.4", - "byteorder", - "chrono", - "crc", - "dotenvy", - "etcetera", - "futures-channel", - "futures-core", - "futures-util", - "hex", - "hkdf", - "hmac", - "home", - "itoa", - "log", - "md-5", - "memchr", - "num-bigint", - "once_cell", - "rand 0.8.5", - "rust_decimal", - "serde", - "serde_json", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror 2.0.17", - "time", - "tracing", - "uuid", - "whoami", -] - -[[package]] -name = "sqlx-sqlite" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" -dependencies = [ - "atoi", - "chrono", - "flume", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "libsqlite3-sys", - "log", - "percent-encoding", - "serde", - "serde_urlencoded", - "sqlx-core", - "thiserror 2.0.17", - "time", - "tracing", - "url", - "uuid", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -15481,74 +5304,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "stop-words" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645a3d441ccf4bf47f2e4b7681461986681a6eeea9937d4c3bc9febd61d17c71" -dependencies = [ - "serde_json", -] - -[[package]] -name = "story" -version = "0.1.0" -dependencies = [ - "gpui", - "itertools 0.14.0", - "smallvec", -] - -[[package]] -name = "storybook" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "collab_ui", - "ctrlc", - "dialoguer", - "editor", - "fuzzy", - "gpui", - "indoc", - "language", - "log", - "menu", - "picker", - "reqwest_client", - "rust-embed", - "settings", - "simplelog", - "story", - "strum 0.27.2", - "theme", - "title_bar", - "ui", -] - -[[package]] -name = "streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" - -[[package]] -name = "streaming_diff" -version = "0.1.0" -dependencies = [ - "ordered-float 2.10.1", - "rand 0.9.2", - "rope", - "util", -] - -[[package]] -name = "strength_reduce" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" - [[package]] name = "strict-num" version = "0.1.1" @@ -15566,7 +5321,7 @@ checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared 0.11.3", + "phf_shared", "precomputed-hash", "serde", ] @@ -15577,29 +5332,12 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", + "phf_generator", + "phf_shared", "proc-macro2", "quote", ] -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "strum" version = "0.26.3" @@ -15659,51 +5397,6 @@ dependencies = [ "rand 0.9.2", "rayon", "tracing", - "zlog", - "ztracing", -] - -[[package]] -name = "supermaven" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "edit_prediction_types", - "editor", - "env_logger 0.11.8", - "futures 0.3.31", - "gpui", - "http_client", - "language", - "log", - "postage", - "project", - "serde", - "serde_json", - "settings", - "smol", - "supermaven_api", - "text", - "theme", - "ui", - "unicode-segmentation", - "util", -] - -[[package]] -name = "supermaven_api" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures 0.3.31", - "http_client", - "paths", - "serde", - "serde_json", - "smol", - "util", ] [[package]] @@ -15790,18 +5483,6 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" -[[package]] -name = "svg_preview" -version = "0.1.0" -dependencies = [ - "file_icons", - "gpui", - "language", - "multi_buffer", - "ui", - "workspace", -] - [[package]] name = "svgtypes" version = "0.15.3" @@ -15823,153 +5504,6 @@ dependencies = [ "zeno", ] -[[package]] -name = "symphonia" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" -dependencies = [ - "lazy_static", - "symphonia-bundle-flac", - "symphonia-bundle-mp3", - "symphonia-codec-aac", - "symphonia-codec-pcm", - "symphonia-codec-vorbis", - "symphonia-core", - "symphonia-format-isomp4", - "symphonia-format-ogg", - "symphonia-format-riff", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-bundle-flac" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" -dependencies = [ - "log", - "symphonia-core", - "symphonia-metadata", - "symphonia-utils-xiph", -] - -[[package]] -name = "symphonia-bundle-mp3" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" -dependencies = [ - "lazy_static", - "log", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-codec-aac" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c263845aa86881416849c1729a54c7f55164f8b96111dba59de46849e73a790" -dependencies = [ - "lazy_static", - "log", - "symphonia-core", -] - -[[package]] -name = "symphonia-codec-pcm" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95" -dependencies = [ - "log", - "symphonia-core", -] - -[[package]] -name = "symphonia-codec-vorbis" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73" -dependencies = [ - "log", - "symphonia-core", - "symphonia-utils-xiph", -] - -[[package]] -name = "symphonia-core" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" -dependencies = [ - "arrayvec", - "bitflags 1.3.2", - "bytemuck", - "lazy_static", - "log", -] - -[[package]] -name = "symphonia-format-isomp4" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5" -dependencies = [ - "encoding_rs", - "log", - "symphonia-core", - "symphonia-metadata", - "symphonia-utils-xiph", -] - -[[package]] -name = "symphonia-format-ogg" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb" -dependencies = [ - "log", - "symphonia-core", - "symphonia-metadata", - "symphonia-utils-xiph", -] - -[[package]] -name = "symphonia-format-riff" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f" -dependencies = [ - "extended", - "log", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-metadata" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" -dependencies = [ - "encoding_rs", - "lazy_static", - "log", - "symphonia-core", -] - -[[package]] -name = "symphonia-utils-xiph" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" -dependencies = [ - "symphonia-core", - "symphonia-metadata", -] - [[package]] name = "syn" version = "1.0.109" @@ -15992,12 +5526,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.2" @@ -16007,15 +5535,6 @@ dependencies = [ "futures-core", ] -[[package]] -name = "synchronoise" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dbc01390fc626ce8d1cffe3376ded2b72a11bb70e1c75f404a210e4daa4def2" -dependencies = [ - "crossbeam-queue", -] - [[package]] name = "synstructure" version = "0.13.2" @@ -16036,34 +5555,6 @@ dependencies = [ "libc", ] -[[package]] -name = "sysctl" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea" -dependencies = [ - "bitflags 2.9.4", - "byteorder", - "enum-as-inner", - "libc", - "thiserror 1.0.69", - "walkdir", -] - -[[package]] -name = "sysctl" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" -dependencies = [ - "bitflags 2.9.4", - "byteorder", - "enum-as-inner", - "libc", - "thiserror 1.0.69", - "walkdir", -] - [[package]] name = "sysinfo" version = "0.31.4" @@ -16078,31 +5569,6 @@ dependencies = [ "windows 0.57.0", ] -[[package]] -name = "sysinfo" -version = "0.37.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" -dependencies = [ - "libc", - "memchr", - "ntapi", - "objc2-core-foundation", - "objc2-io-kit", - "windows 0.61.3", -] - -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "system-configuration-sys 0.5.0", -] - [[package]] name = "system-configuration" version = "0.6.1" @@ -16111,17 +5577,7 @@ checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.9.4", "core-foundation 0.9.4", - "system-configuration-sys 0.6.0", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", + "system-configuration-sys", ] [[package]] @@ -16147,63 +5603,6 @@ dependencies = [ "version-compare", ] -[[package]] -name = "system-interface" -version = "0.27.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" -dependencies = [ - "bitflags 2.9.4", - "cap-fs-ext", - "cap-std", - "fd-lock", - "io-lifetimes", - "rustix 0.38.44", - "windows-sys 0.59.0", - "winx", -] - -[[package]] -name = "system_specs" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "gpui", - "human_bytes", - "pciid-parser", - "release_channel", - "semver", - "serde", - "sysinfo 0.37.2", -] - -[[package]] -name = "tab_switcher" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "ctor", - "editor", - "fuzzy", - "gpui", - "language", - "menu", - "picker", - "project", - "schemars", - "serde", - "serde_json", - "settings", - "smol", - "theme", - "ui", - "util", - "workspace", - "zlog", -] - [[package]] name = "taffy" version = "0.9.0" @@ -16216,12 +5615,6 @@ dependencies = [ "slotmap", ] -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - [[package]] name = "take-until" version = "0.2.0" @@ -16240,92 +5633,12 @@ dependencies = [ "objc", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "target-lexicon" version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" -[[package]] -name = "target-lexicon" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" - -[[package]] -name = "task" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "futures 0.3.31", - "gpui", - "hex", - "log", - "parking_lot", - "pretty_assertions", - "proto", - "schemars", - "serde", - "serde_json", - "serde_json_lenient", - "sha2", - "shellexpand 2.1.2", - "util", - "zed_actions", -] - -[[package]] -name = "tasks_ui" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "editor", - "file_icons", - "fuzzy", - "gpui", - "itertools 0.14.0", - "language", - "menu", - "picker", - "project", - "serde", - "serde_json", - "task", - "tree-sitter-rust", - "tree-sitter-typescript", - "ui", - "util", - "workspace", - "zed_actions", -] - -[[package]] -name = "telemetry" -version = "0.1.0" -dependencies = [ - "futures 0.3.31", - "serde", - "serde_json", - "telemetry_events", -] - -[[package]] -name = "telemetry_events" -version = "0.1.0" -dependencies = [ - "semver", - "serde", - "serde_json", -] - [[package]] name = "tempfile" version = "3.23.0" @@ -16359,181 +5672,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "terminal" -version = "0.1.0" -dependencies = [ - "alacritty_terminal", - "anyhow", - "collections", - "futures 0.3.31", - "gpui", - "itertools 0.14.0", - "libc", - "log", - "rand 0.9.2", - "regex", - "release_channel", - "schemars", - "serde", - "serde_json", - "settings", - "smol", - "sysinfo 0.37.2", - "task", - "theme", - "thiserror 2.0.17", - "url", - "urlencoding", - "util", - "util_macros", - "windows 0.61.3", -] - -[[package]] -name = "terminal_size" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" -dependencies = [ - "rustix 1.1.2", - "windows-sys 0.60.2", -] - -[[package]] -name = "terminal_view" -version = "0.1.0" -dependencies = [ - "anyhow", - "assistant_slash_command", - "async-recursion", - "breadcrumbs", - "client", - "collections", - "db", - "dirs 4.0.0", - "editor", - "futures 0.3.31", - "gpui", - "itertools 0.14.0", - "language", - "log", - "pretty_assertions", - "project", - "rand 0.9.2", - "regex", - "schemars", - "search", - "serde", - "serde_json", - "settings", - "shellexpand 2.1.2", - "task", - "terminal", - "theme", - "ui", - "util", - "workspace", - "zed_actions", -] - -[[package]] -name = "text" -version = "0.1.0" -dependencies = [ - "anyhow", - "clock", - "collections", - "ctor", - "gpui", - "http_client", - "log", - "parking_lot", - "postage", - "rand 0.9.2", - "regex", - "rope", - "smallvec", - "sum_tree", - "util", - "zlog", -] - -[[package]] -name = "theme" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "derive_more 0.99.20", - "fs", - "futures 0.3.31", - "gpui", - "log", - "palette", - "parking_lot", - "refineable", - "schemars", - "serde", - "serde_json", - "serde_json_lenient", - "settings", - "strum 0.27.2", - "thiserror 2.0.17", - "util", - "uuid", -] - -[[package]] -name = "theme_extension" -version = "0.1.0" -dependencies = [ - "anyhow", - "extension", - "fs", - "gpui", - "theme", -] - -[[package]] -name = "theme_importer" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "collections", - "gpui", - "indexmap", - "log", - "palette", - "serde", - "serde_json", - "serde_json_lenient", - "simplelog", - "strum 0.27.2", - "theme", - "vscode_theme", -] - -[[package]] -name = "theme_selector" -version = "0.1.0" -dependencies = [ - "fs", - "fuzzy", - "gpui", - "log", - "picker", - "serde", - "settings", - "telemetry", - "theme", - "ui", - "util", - "workspace", - "zed_actions", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -16574,15 +5712,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - [[package]] name = "tiff" version = "0.10.3" @@ -16597,63 +5726,6 @@ dependencies = [ "zune-jpeg", ] -[[package]] -name = "tiktoken-rs" -version = "0.9.1" -source = "git+https://github.com/zed-industries/tiktoken-rs?rev=2570c4387a8505fb8f1d3f3557454b474f1e8271#2570c4387a8505fb8f1d3f3557454b474f1e8271" -dependencies = [ - "anyhow", - "base64 0.22.1", - "bstr", - "fancy-regex", - "lazy_static", - "regex", - "rustc-hash 1.1.0", -] - -[[package]] -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "itoa", - "libc", - "num-conv", - "num_threads", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "time_format" -version = "0.1.0" -dependencies = [ - "core-foundation 0.10.0", - "core-foundation-sys", - "sys-locale", - "time", -] - [[package]] name = "tiny-keccak" version = "2.0.2" @@ -16689,19 +5761,6 @@ dependencies = [ "strict-num", ] -[[package]] -name = "tiny_http" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce51b50006056f590c9b7c3808c3bd70f0d1101666629713866c227d6e58d39" -dependencies = [ - "ascii", - "chrono", - "chunked_transfer", - "log", - "url", -] - [[package]] name = "tinystr" version = "0.8.1" @@ -16712,16 +5771,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "tinyvec" version = "1.10.0" @@ -16737,107 +5786,27 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "title_bar" -version = "0.1.0" -dependencies = [ - "anyhow", - "auto_update", - "call", - "channel", - "chrono", - "client", - "cloud_llm_client", - "collections", - "db", - "gpui", - "http_client", - "notifications", - "pretty_assertions", - "project", - "remote", - "rpc", - "schemars", - "serde", - "settings", - "smallvec", - "story", - "telemetry", - "theme", - "tree-sitter-md", - "ui", - "util", - "windows 0.61.3", - "workspace", - "zed_actions", -] - [[package]] name = "tokio" version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "bytes 1.10.1", + "bytes", "libc", - "mio 1.1.0", - "parking_lot", + "mio", "pin-project-lite", - "signal-hook-registry", - "socket2 0.6.1", - "tokio-macros", + "socket2", "windows-sys 0.61.2", ] -[[package]] -name = "tokio-io" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" -dependencies = [ - "bytes 0.4.12", - "futures 0.1.31", - "log", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.33", + "rustls", "tokio", ] @@ -16848,85 +5817,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" dependencies = [ "either", - "futures-io", "futures-util", "thiserror 1.0.69", "tokio", ] -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite 0.20.1", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite 0.21.0", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" -dependencies = [ - "futures-util", - "log", - "rustls 0.23.33", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.2", - "tungstenite 0.26.2", -] - [[package]] name = "tokio-util" version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ - "bytes 1.10.1", + "bytes", "futures-core", - "futures-io", "futures-sink", "pin-project-lite", "tokio", ] -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - [[package]] name = "toml" version = "0.8.23" @@ -17019,48 +5927,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" -[[package]] -name = "toolchain_selector" -version = "0.1.0" -dependencies = [ - "anyhow", - "convert_case 0.8.0", - "editor", - "file_finder", - "futures 0.3.31", - "fuzzy", - "gpui", - "language", - "menu", - "picker", - "project", - "ui", - "util", - "workspace", -] - -[[package]] -name = "topological-sort" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower" version = "0.5.2" @@ -17070,67 +5936,12 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tower-layer", "tower-service", ] -[[package]] -name = "tower-http" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" -dependencies = [ - "bitflags 1.3.2", - "bytes 1.10.1", - "futures-core", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "http-range-header", - "pin-project-lite", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" -dependencies = [ - "bitflags 2.9.4", - "bytes 1.10.1", - "futures-core", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "http-range-header", - "pin-project-lite", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" -dependencies = [ - "bitflags 2.9.4", - "bytes 1.10.1", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "iri-string", - "pin-project-lite", - "tower 0.5.2", - "tower-layer", - "tower-service", -] - [[package]] name = "tower-layer" version = "0.3.3" @@ -17173,335 +5984,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-serde" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" -dependencies = [ - "serde", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "serde", - "serde_json", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", - "tracing-serde", -] - -[[package]] -name = "tracing-tracy" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eaa1852afa96e0fe9e44caa53dc0bd2d9d05e0f2611ce09f97f8677af56e4ba" -dependencies = [ - "tracing-core", - "tracing-subscriber", - "tracy-client", -] - -[[package]] -name = "tracy-client" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d722a05fe49b31fef971c4732a7d4aa6a18283d9ba46abddab35f484872947" -dependencies = [ - "loom", - "once_cell", - "tracy-client-sys", -] - -[[package]] -name = "tracy-client-sys" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fb391ac70462b3097a755618fbf9c8f95ecc1eb379a414f7b46f202ed10db1f" -dependencies = [ - "cc", - "windows-targets 0.52.6", -] - -[[package]] -name = "trait-variant" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "transpose" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" -dependencies = [ - "num-integer", - "strength_reduce", -] - -[[package]] -name = "tree-sitter" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87" -dependencies = [ - "cc", - "regex", - "regex-syntax", - "serde_json", - "streaming-iterator", - "tree-sitter-language", - "wasmtime-c-api-impl", -] - -[[package]] -name = "tree-sitter-bash" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5ec769279cc91b561d3df0d8a5deb26b0ad40d183127f409494d6d8fc53062" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-c" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afd2b1bf1585dc2ef6d69e87d01db8adb059006649dd5f96f31aa789ee6e9c71" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-cpp" -version = "0.23.4" -source = "git+https://github.com/tree-sitter/tree-sitter-cpp?rev=5cb9b693cfd7bfacab1d9ff4acac1a4150700609#5cb9b693cfd7bfacab1d9ff4acac1a4150700609" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-css" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad6489794d41350d12a7fbe520e5199f688618f43aace5443980d1ddcf1b29e" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-diff" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfe1e5ca280a65dfe5ba4205c1bcc84edf486464fed315db53dee6da9a335889" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-elixir" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e45d444647b4fd53d8fd32474c1b8bedc1baa22669ce3a78d083e365fa9a2d3f" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-embedded-template" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790063ef14e5b67556abc0b3be0ed863fb41d65ee791cf8c0b20eb42a1fa46af" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-gitcommit" -version = "0.0.1" -source = "git+https://github.com/zed-industries/tree-sitter-git-commit?rev=88309716a69dd13ab83443721ba6e0b491d37ee9#88309716a69dd13ab83443721ba6e0b491d37ee9" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-go" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b13d476345220dbe600147dd444165c5791bf85ef53e28acbedd46112ee18431" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-gomod" -version = "1.1.1" -source = "git+https://github.com/camdencheek/tree-sitter-go-mod?rev=2e886870578eeba1927a2dc4bd2e2b3f598c5f9a#2e886870578eeba1927a2dc4bd2e2b3f598c5f9a" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-gowork" -version = "0.0.1" -source = "git+https://github.com/zed-industries/tree-sitter-go-work?rev=acb0617bf7f4fda02c6217676cc64acb89536dc7#acb0617bf7f4fda02c6217676cc64acb89536dc7" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-heex" -version = "0.0.1" -source = "git+https://github.com/zed-industries/tree-sitter-heex?rev=1dd45142fbb05562e35b2040c6129c9bca346592#1dd45142fbb05562e35b2040c6129c9bca346592" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-html" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261b708e5d92061ede329babaaa427b819329a9d427a1d710abb0f67bbef63ee" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-jsdoc" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3862dfcb1038fc5e7812d7df14190afdeb7e1415288fd5f51f58395f8cb0faf" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-json" -version = "0.24.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d727acca406c0020cffc6cf35516764f36c8e3dc4408e5ebe2cb35a947ec471" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-language" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4013970217383f67b18aef68f6fb2e8d409bc5755227092d32efb0422ba24b8" - -[[package]] -name = "tree-sitter-md" -version = "0.3.2" -source = "git+https://github.com/tree-sitter-grammars/tree-sitter-markdown?rev=9a23c1a96c0513d8fc6520972beedd419a973539#9a23c1a96c0513d8fc6520972beedd419a973539" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-python" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf85fd39652e740bf60f46f4cda9492c3a9ad75880575bf14960f775cb74a1c" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-regex" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712656f8c262a5a4b7d6026e6246950787d178d613864952554e1516a33ab0c1" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-ruby" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be0484ea4ef6bb9c575b4fdabde7e31340a8d2dbc7d52b321ac83da703249f95" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-rust" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9b18034c684a2420722be8b2a91c9c44f2546b631c039edf575ccba8c61be1" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-typescript" -version = "0.23.2" -source = "git+https://github.com/zed-industries/tree-sitter-typescript?rev=e2c53597d6a5d9cf7bbe8dccde576fe1e46c5899#e2c53597d6a5d9cf7bbe8dccde576fe1e46c5899" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-yaml" -version = "0.6.1" -source = "git+https://github.com/zed-industries/tree-sitter-yaml?rev=baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a#baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" -dependencies = [ - "cc", - "tree-sitter-language", ] [[package]] @@ -17531,88 +6013,6 @@ dependencies = [ "core_maths", ] -[[package]] -name = "tungstenite" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" -dependencies = [ - "byteorder", - "bytes 1.10.1", - "data-encoding", - "http 0.2.12", - "httparse", - "log", - "rand 0.8.5", - "sha1", - "thiserror 1.0.69", - "url", - "utf-8", -] - -[[package]] -name = "tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" -dependencies = [ - "byteorder", - "bytes 1.10.1", - "data-encoding", - "http 1.3.1", - "httparse", - "log", - "rand 0.8.5", - "sha1", - "thiserror 1.0.69", - "url", - "utf-8", -] - -[[package]] -name = "tungstenite" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" -dependencies = [ - "bytes 1.10.1", - "data-encoding", - "http 1.3.1", - "httparse", - "log", - "rand 0.9.2", - "rustls 0.23.33", - "rustls-pki-types", - "sha1", - "thiserror 2.0.17", - "utf-8", -] - -[[package]] -name = "tungstenite" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" -dependencies = [ - "bytes 1.10.1", - "data-encoding", - "http 1.3.1", - "httparse", - "log", - "rand 0.9.2", - "rustls 0.23.33", - "rustls-pki-types", - "sha1", - "thiserror 2.0.17", - "utf-8", -] - -[[package]] -name = "typed-path" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c462d18470a2857aa657d338af5fa67170bb48bcc80a296710ce3b0802a32566" - [[package]] name = "typeid" version = "1.0.3" @@ -17625,21 +6025,6 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" -[[package]] -name = "ucd-trie" -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" @@ -17651,87 +6036,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "ug" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90b70b37e9074642bc5f60bb23247fd072a84314ca9e71cdf8527593406a0dd3" -dependencies = [ - "gemm 0.18.2", - "half", - "libloading", - "memmap2", - "num", - "num-traits", - "num_cpus", - "rayon", - "safetensors", - "serde", - "thiserror 1.0.69", - "tracing", - "yoke 0.7.5", -] - -[[package]] -name = "ui" -version = "0.1.0" -dependencies = [ - "chrono", - "component", - "documented", - "gpui", - "gpui_macros", - "icons", - "itertools 0.14.0", - "menu", - "schemars", - "serde", - "settings", - "smallvec", - "story", - "strum 0.27.2", - "theme", - "ui_macros", - "util", - "windows 0.61.3", -] - -[[package]] -name = "ui_input" -version = "0.1.0" -dependencies = [ - "component", - "editor", - "gpui", - "menu", - "settings", - "theme", - "ui", -] - -[[package]] -name = "ui_macros" -version = "0.1.0" -dependencies = [ - "component", - "quote", - "syn 2.0.106", - "ui", -] - -[[package]] -name = "ui_prompt" -version = "0.1.0" -dependencies = [ - "gpui", - "markdown", - "menu", - "settings", - "theme", - "ui", - "workspace", -] - [[package]] name = "unicase" version = "2.8.1" @@ -17768,12 +6072,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" -[[package]] -name = "unicode-general-category" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" - [[package]] name = "unicode-ident" version = "1.0.19" @@ -17786,15 +6084,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" -[[package]] -name = "unicode-normalization" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" -dependencies = [ - "tinyvec", -] - [[package]] name = "unicode-properties" version = "0.1.3" @@ -17825,36 +6114,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" @@ -17873,19 +6132,13 @@ dependencies = [ "serde", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - [[package]] name = "usvg" version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" dependencies = [ - "base64 0.22.1", + "base64", "data-url", "flate2", "fontdb 0.23.0", @@ -17935,7 +6188,7 @@ dependencies = [ "command-fds", "dirs 4.0.0", "dunce", - "futures 0.3.31", + "futures", "futures-lite 1.13.0", "git2", "globset", @@ -17943,7 +6196,7 @@ dependencies = [ "itertools 0.14.0", "libc", "log", - "mach2 0.5.0", + "mach2", "nix 0.29.0", "pretty_assertions", "rand 0.9.2", @@ -17961,7 +6214,7 @@ dependencies = [ "unicase", "util_macros", "walkdir", - "which 6.0.3", + "which", ] [[package]] @@ -17986,16 +6239,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "uuid-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" -dependencies = [ - "outref", - "vsimd", -] - [[package]] name = "v_frame" version = "0.3.9" @@ -18007,12 +6250,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - [[package]] name = "value-bag" version = "1.11.1" @@ -18055,16 +6292,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vercel" -version = "0.1.0" -dependencies = [ - "anyhow", - "schemars", - "serde", - "strum 0.27.2", -] - [[package]] name = "version-compare" version = "0.2.0" @@ -18077,81 +6304,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "vim" -version = "0.1.0" -dependencies = [ - "anyhow", - "assets", - "async-compat", - "async-trait", - "collections", - "command_palette", - "command_palette_hooks", - "db", - "editor", - "env_logger 0.11.8", - "futures 0.3.31", - "fuzzy", - "git_ui", - "gpui", - "indoc", - "itertools 0.14.0", - "language", - "log", - "lsp", - "markdown_preview", - "menu", - "multi_buffer", - "nvim-rs", - "parking_lot", - "perf", - "picker", - "project", - "project_panel", - "regex", - "release_channel", - "schemars", - "search", - "semver", - "serde", - "serde_json", - "settings", - "settings_ui", - "task", - "text", - "theme", - "tokio", - "ui", - "util", - "util_macros", - "vim_mode_setting", - "workspace", - "zed_actions", -] - -[[package]] -name = "vim_mode_setting" -version = "0.1.0" -dependencies = [ - "settings", -] - -[[package]] -name = "vscode_theme" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b3666211944f2e6ba2c359bc9efc1891157e910b1b11c3900892ea9f18179d2" -dependencies = [ - "serde", -] - -[[package]] -name = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - [[package]] name = "vswhom" version = "0.1.0" @@ -18172,20 +6324,6 @@ dependencies = [ "libc", ] -[[package]] -name = "vte" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd" -dependencies = [ - "arrayvec", - "bitflags 2.9.4", - "cursor-icon", - "log", - "memchr", - "serde", -] - [[package]] name = "waker-fn" version = "1.2.0" @@ -18211,34 +6349,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "warp" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c" -dependencies = [ - "bytes 1.10.1", - "futures-channel", - "futures-util", - "headers", - "http 0.2.12", - "hyper 0.14.32", - "log", - "mime", - "mime_guess", - "percent-encoding", - "pin-project", - "scoped-tls", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-tungstenite 0.21.0", - "tokio-util", - "tower-service", - "tracing", -] - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -18251,15 +6361,9 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen 0.46.0", + "wit-bindgen", ] -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - [[package]] name = "wasm-bindgen" version = "0.2.104" @@ -18332,70 +6436,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.201.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9c7d2731df60006819b013f64ccc2019691deccf6e11a1804bc850cd6748f1a" -dependencies = [ - "leb128", -] - -[[package]] -name = "wasm-encoder" -version = "0.221.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc8444fe4920de80a4fe5ab564fff2ae58b6b73166b89751f8c6c93509da32e5" -dependencies = [ - "leb128", - "wasmparser 0.221.3", -] - -[[package]] -name = "wasm-encoder" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" -dependencies = [ - "leb128fmt", - "wasmparser 0.227.1", -] - -[[package]] -name = "wasm-metadata" -version = "0.201.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fd83062c17b9f4985d438603cde0a5e8c5c8198201a6937f778b607924c7da2" -dependencies = [ - "anyhow", - "indexmap", - "serde", - "serde_derive", - "serde_json", - "spdx", - "wasm-encoder 0.201.0", - "wasmparser 0.201.0", -] - -[[package]] -name = "wasm-metadata" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" -dependencies = [ - "anyhow", - "auditable-serde", - "flate2", - "indexmap", - "serde", - "serde_derive", - "serde_json", - "spdx", - "url", - "wasm-encoder 0.227.1", - "wasmparser 0.227.1", -] - [[package]] name = "wasm-streams" version = "0.4.2" @@ -18409,354 +6449,17 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.201.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708" -dependencies = [ - "bitflags 2.9.4", - "indexmap", - "semver", -] - -[[package]] -name = "wasmparser" -version = "0.221.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d06bfa36ab3ac2be0dee563380147a5b81ba10dd8885d7fbbc9eb574be67d185" -dependencies = [ - "bitflags 2.9.4", - "hashbrown 0.15.5", - "indexmap", - "semver", - "serde", -] - -[[package]] -name = "wasmparser" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" -dependencies = [ - "bitflags 2.9.4", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "wasmprinter" -version = "0.221.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7343c42a97f2926c7819ff81b64012092ae954c5d83ddd30c9fcdefd97d0b283" -dependencies = [ - "anyhow", - "termcolor", - "wasmparser 0.221.3", -] - -[[package]] -name = "wasmtime" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11976a250672556d1c4c04c6d5d7656ac9192ac9edc42a4587d6c21460010e69" -dependencies = [ - "anyhow", - "async-trait", - "bitflags 2.9.4", - "bumpalo", - "cc", - "cfg-if", - "encoding_rs", - "hashbrown 0.14.5", - "indexmap", - "libc", - "log", - "mach2 0.4.3", - "memfd", - "object 0.36.7", - "once_cell", - "paste", - "postcard", - "psm", - "pulley-interpreter", - "rayon", - "rustix 0.38.44", - "semver", - "serde", - "serde_derive", - "smallvec", - "sptr", - "target-lexicon 0.13.3", - "trait-variant", - "wasmparser 0.221.3", - "wasmtime-asm-macros", - "wasmtime-component-macro", - "wasmtime-component-util", - "wasmtime-cranelift", - "wasmtime-environ", - "wasmtime-fiber", - "wasmtime-jit-icache-coherence", - "wasmtime-math", - "wasmtime-slab", - "wasmtime-versioned-export-macros", - "wasmtime-winch", - "windows-sys 0.59.0", -] - -[[package]] -name = "wasmtime-asm-macros" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f178b0d125201fbe9f75beaf849bd3e511891f9e45ba216a5b620802ccf64f2" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "wasmtime-c-api-impl" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea30cef3608f2de5797c7bbb94c1ba4f3676d9a7f81ae86ced1b512e2766ed0c" -dependencies = [ - "anyhow", - "log", - "tracing", - "wasmtime", - "wasmtime-c-api-macros", -] - -[[package]] -name = "wasmtime-c-api-macros" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022a79ebe1124d5d384d82463d7e61c6b4dd857d81f15cb8078974eeb86db65b" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "wasmtime-component-macro" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d74de6592ed945d0a602f71243982a304d5d02f1e501b638addf57f42d57dfaf" -dependencies = [ - "anyhow", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasmtime-component-util", - "wasmtime-wit-bindgen", - "wit-parser 0.221.3", -] - -[[package]] -name = "wasmtime-component-util" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707dc7b3c112ab5a366b30cfe2fb5b2f8e6a0f682f16df96a5ec582bfe6f056e" - -[[package]] -name = "wasmtime-cranelift" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "366be722674d4bf153290fbcbc4d7d16895cc82fb3e869f8d550ff768f9e9e87" -dependencies = [ - "anyhow", - "cfg-if", - "cranelift-codegen", - "cranelift-control", - "cranelift-entity", - "cranelift-frontend", - "cranelift-native", - "gimli 0.31.1", - "itertools 0.12.1", - "log", - "object 0.36.7", - "smallvec", - "target-lexicon 0.13.3", - "thiserror 1.0.69", - "wasmparser 0.221.3", - "wasmtime-environ", - "wasmtime-versioned-export-macros", -] - -[[package]] -name = "wasmtime-environ" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdadc1af7097347aa276a4f008929810f726b5b46946971c660b6d421e9994ad" -dependencies = [ - "anyhow", - "cpp_demangle", - "cranelift-bitset", - "cranelift-entity", - "gimli 0.31.1", - "indexmap", - "log", - "object 0.36.7", - "postcard", - "rustc-demangle", - "semver", - "serde", - "serde_derive", - "smallvec", - "target-lexicon 0.13.3", - "wasm-encoder 0.221.3", - "wasmparser 0.221.3", - "wasmprinter", - "wasmtime-component-util", -] - -[[package]] -name = "wasmtime-fiber" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccba90d4119f081bca91190485650730a617be1fff5228f8c4757ce133d21117" -dependencies = [ - "anyhow", - "cc", - "cfg-if", - "rustix 0.38.44", - "wasmtime-asm-macros", - "wasmtime-versioned-export-macros", - "windows-sys 0.59.0", -] - -[[package]] -name = "wasmtime-jit-icache-coherence" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec5e8552e01692e6c2e5293171704fed8abdec79d1a6995a0870ab190e5747d1" -dependencies = [ - "anyhow", - "cfg-if", - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "wasmtime-math" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29210ec2aa25e00f4d54605cedaf080f39ec01a872c5bd520ad04c67af1dde17" -dependencies = [ - "libm", -] - -[[package]] -name = "wasmtime-slab" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb5821a96fa04ac14bc7b158bb3d5cd7729a053db5a74dad396cd513a5e5ccf" - -[[package]] -name = "wasmtime-versioned-export-macros" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ff86db216dc0240462de40c8290887a613dddf9685508eb39479037ba97b5b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "wasmtime-wasi" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d1be69bfcab1bdac74daa7a1f9695ab992b9c8e21b9b061e7d66434097e0ca4" -dependencies = [ - "anyhow", - "async-trait", - "bitflags 2.9.4", - "bytes 1.10.1", - "cap-fs-ext", - "cap-net-ext", - "cap-rand", - "cap-std", - "cap-time-ext", - "fs-set-times", - "futures 0.3.31", - "io-extras", - "io-lifetimes", - "rustix 0.38.44", - "system-interface", - "thiserror 1.0.69", - "tokio", - "tracing", - "trait-variant", - "url", - "wasmtime", - "wiggle", - "windows-sys 0.59.0", -] - -[[package]] -name = "wasmtime-winch" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdbabfb8f20502d5e1d81092b9ead3682ae59988487aafcd7567387b7a43cf8f" -dependencies = [ - "anyhow", - "cranelift-codegen", - "gimli 0.31.1", - "object 0.36.7", - "target-lexicon 0.13.3", - "wasmparser 0.221.3", - "wasmtime-cranelift", - "wasmtime-environ", - "winch-codegen", -] - -[[package]] -name = "wasmtime-wit-bindgen" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8358319c2dd1e4db79e3c1c5d3a5af84956615343f9f89f4e4996a36816e06e6" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap", - "wit-parser 0.221.3", -] - -[[package]] -name = "wast" -version = "35.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" -dependencies = [ - "leb128", -] - [[package]] name = "watch" version = "0.1.0" dependencies = [ "ctor", - "futures 0.3.31", + "env_logger", + "futures", "gpui", + "log", "parking_lot", "rand 0.9.2", - "zlog", -] - -[[package]] -name = "wax" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d12a78aa0bab22d2f26ed1a96df7ab58e8a93506a3e20adb47c51a93b4e1357" -dependencies = [ - "const_format", - "itertools 0.11.0", - "nom 7.1.3", - "pori", - "regex", - "thiserror 1.0.69", - "walkdir", ] [[package]] @@ -18889,45 +6592,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web_atoms" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" -dependencies = [ - "phf 0.11.3", - "phf_codegen", - "string_cache", - "string_cache_codegen", -] - -[[package]] -name = "web_search" -version = "0.1.0" -dependencies = [ - "anyhow", - "cloud_llm_client", - "collections", - "gpui", - "serde", -] - -[[package]] -name = "web_search_providers" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "cloud_llm_client", - "futures 0.3.31", - "gpui", - "http_client", - "language_model", - "serde", - "serde_json", - "web_search", -] - [[package]] name = "webpki-root-certs" version = "0.26.8" @@ -18937,59 +6601,12 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "webpki-roots" -version = "0.26.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "webrtc-sys" -version = "0.3.7" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d" -dependencies = [ - "cc", - "cxx", - "cxx-build", - "glob", - "log", - "webrtc-sys-build", -] - -[[package]] -name = "webrtc-sys-build" -version = "0.3.6" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d" -dependencies = [ - "fs2", - "regex", - "reqwest 0.11.27", - "scratch", - "semver", - "zip 0.6.6", -] - [[package]] name = "weezl" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - [[package]] name = "which" version = "6.0.3" @@ -19002,58 +6619,6 @@ dependencies = [ "winsafe", ] -[[package]] -name = "whoami" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" -dependencies = [ - "libredox", - "wasite", -] - -[[package]] -name = "wiggle" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9af35bc9629c52c261465320a9a07959164928b4241980ba1cf923b9e6751d" -dependencies = [ - "anyhow", - "async-trait", - "bitflags 2.9.4", - "thiserror 1.0.69", - "tracing", - "wasmtime", - "wiggle-macro", -] - -[[package]] -name = "wiggle-generate" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cf267dd05673912c8138f4b54acabe6bd53407d9d1536f0fadb6520dd16e101" -dependencies = [ - "anyhow", - "heck 0.5.0", - "proc-macro2", - "quote", - "shellexpand 2.1.2", - "syn 2.0.106", - "witx", -] - -[[package]] -name = "wiggle-macro" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c5c473d4198e6c2d377f3809f713ff0c110cab88a0805ae099a82119ee250c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "wiggle-generate", -] - [[package]] name = "winapi" version = "0.3.9" @@ -19085,34 +6650,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "winch-codegen" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f849ef2c5f46cb0a20af4b4487aaa239846e52e2c03f13fa3c784684552859c" -dependencies = [ - "anyhow", - "cranelift-codegen", - "gimli 0.31.1", - "regalloc2", - "smallvec", - "target-lexicon 0.13.3", - "thiserror 1.0.69", - "wasmparser 0.221.3", - "wasmtime-cranelift", - "wasmtime-environ", -] - -[[package]] -name = "windows" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" -dependencies = [ - "windows-core 0.54.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.57.0" @@ -19123,16 +6660,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" -dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.61.3" @@ -19148,11 +6675,10 @@ dependencies = [ [[package]] name = "windows-capture" -version = "1.4.3" -source = "git+https://github.com/zed-industries/windows-capture.git?rev=f0d6c1b6691db75461b732f6d5ff56eed002eeb9#f0d6c1b6691db75461b732f6d5ff56eed002eeb9" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24" dependencies = [ - "clap", - "ctrlc", "parking_lot", "rayon", "thiserror 2.0.17", @@ -19169,16 +6695,6 @@ dependencies = [ "windows-core 0.61.2", ] -[[package]] -name = "windows-core" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" -dependencies = [ - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.57.0" @@ -19191,19 +6707,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" -dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.61.2" @@ -19252,17 +6755,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "windows-implement" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "windows-implement" version = "0.60.2" @@ -19285,17 +6777,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "windows-interface" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "windows-interface" version = "0.59.3" @@ -19351,17 +6832,6 @@ dependencies = [ "windows-strings 0.4.2", ] -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - [[package]] name = "windows-result" version = "0.1.2" @@ -19371,15 +6841,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.3.4" @@ -19398,16 +6859,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-strings" version = "0.3.1" @@ -19750,25 +7201,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "winreg" version = "0.55.0" @@ -19779,32 +7211,12 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "winresource" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edcacf11b6f48dd21b9ba002f991bdd5de29b2da8cc2800412f4b80f677e4957" -dependencies = [ - "toml 0.8.23", - "version_check", -] - [[package]] name = "winsafe" version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" -[[package]] -name = "winx" -version = "0.36.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" -dependencies = [ - "bitflags 2.9.4", - "windows-sys 0.59.0", -] - [[package]] name = "wio" version = "0.2.2" @@ -19814,332 +7226,18 @@ dependencies = [ "winapi", ] -[[package]] -name = "wit-bindgen" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "288f992ea30e6b5c531b52cdd5f3be81c148554b09ea416f058d16556ba92c27" -dependencies = [ - "bitflags 2.9.4", - "wit-bindgen-rt 0.22.0", - "wit-bindgen-rust-macro 0.22.0", -] - -[[package]] -name = "wit-bindgen" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" -dependencies = [ - "wit-bindgen-rt 0.41.0", - "wit-bindgen-rust-macro 0.41.0", -] - [[package]] name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" -[[package]] -name = "wit-bindgen-core" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e85e72719ffbccf279359ad071497e47eb0675fe22106dea4ed2d8a7fcb60ba4" -dependencies = [ - "anyhow", - "wit-parser 0.201.0", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser 0.227.1", -] - -[[package]] -name = "wit-bindgen-rt" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb8738270f32a2d6739973cbbb7c1b6dd8959ce515578a6e19165853272ee64" - -[[package]] -name = "wit-bindgen-rt" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" -dependencies = [ - "bitflags 2.9.4", - "futures 0.3.31", - "once_cell", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a39a15d1ae2077688213611209849cad40e9e5cccf6e61951a425850677ff3" -dependencies = [ - "anyhow", - "heck 0.4.1", - "indexmap", - "wasm-metadata 0.201.0", - "wit-bindgen-core 0.22.0", - "wit-component 0.201.0", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap", - "prettyplease", - "syn 2.0.106", - "wasm-metadata 0.227.1", - "wit-bindgen-core 0.41.0", - "wit-component 0.227.1", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d376d3ae5850526dfd00d937faea0d81a06fa18f7ac1e26f386d760f241a8f4b" -dependencies = [ - "anyhow", - "proc-macro2", - "quote", - "syn 2.0.106", - "wit-bindgen-core 0.22.0", - "wit-bindgen-rust 0.22.0", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.106", - "wit-bindgen-core 0.41.0", - "wit-bindgen-rust 0.41.0", -] - -[[package]] -name = "wit-component" -version = "0.201.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "421c0c848a0660a8c22e2fd217929a0191f14476b68962afd2af89fd22e39825" -dependencies = [ - "anyhow", - "bitflags 2.9.4", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder 0.201.0", - "wasm-metadata 0.201.0", - "wasmparser 0.201.0", - "wit-parser 0.201.0", -] - -[[package]] -name = "wit-component" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" -dependencies = [ - "anyhow", - "bitflags 2.9.4", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder 0.227.1", - "wasm-metadata 0.227.1", - "wasmparser 0.227.1", - "wit-parser 0.227.1", -] - -[[package]] -name = "wit-parser" -version = "0.201.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196d3ecfc4b759a8573bf86a9b3f8996b304b3732e4c7de81655f875f6efdca6" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser 0.201.0", -] - -[[package]] -name = "wit-parser" -version = "0.221.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "896112579ed56b4a538b07a3d16e562d101ff6265c46b515ce0c701eef16b2ac" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser 0.221.3", -] - -[[package]] -name = "wit-parser" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser 0.227.1", -] - -[[package]] -name = "witx" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" -dependencies = [ - "anyhow", - "log", - "thiserror 1.0.69", - "wast", -] - -[[package]] -name = "workspace" -version = "0.1.0" -dependencies = [ - "any_vec", - "anyhow", - "async-recursion", - "call", - "client", - "clock", - "collections", - "component", - "dap", - "db", - "fs", - "futures 0.3.31", - "gpui", - "http_client", - "itertools 0.14.0", - "language", - "log", - "menu", - "node_runtime", - "parking_lot", - "postage", - "pretty_assertions", - "project", - "remote", - "schemars", - "serde", - "serde_json", - "session", - "settings", - "smallvec", - "sqlez", - "strum 0.27.2", - "task", - "telemetry", - "tempfile", - "theme", - "ui", - "util", - "uuid", - "windows 0.61.3", - "zed_actions", - "zlog", -] - -[[package]] -name = "worktree" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-lock 2.8.0", - "clock", - "collections", - "fs", - "futures 0.3.31", - "fuzzy", - "git", - "git2", - "gpui", - "http_client", - "ignore", - "language", - "log", - "parking_lot", - "paths", - "postage", - "pretty_assertions", - "rand 0.9.2", - "rpc", - "serde", - "serde_json", - "settings", - "smallvec", - "smol", - "sum_tree", - "text", - "util", - "zlog", -] - [[package]] name = "writeable" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "x11" version = "2.21.0" @@ -20180,16 +7278,6 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" -[[package]] -name = "x_ai" -version = "0.1.0" -dependencies = [ - "anyhow", - "schemars", - "serde", - "strum 0.27.2", -] - [[package]] name = "xattr" version = "0.2.3" @@ -20259,85 +7347,21 @@ checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" dependencies = [ "log", "mac", - "markup5ever 0.12.1", + "markup5ever", ] -[[package]] -name = "xmlparser" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" - [[package]] name = "xmlwriter" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" -[[package]] -name = "xtask" -version = "0.1.0" -dependencies = [ - "anyhow", - "backtrace", - "cargo_metadata", - "cargo_toml", - "clap", - "gh-workflow", - "indexmap", - "indoc", - "serde", - "serde_json", - "toml 0.8.23", - "toml_edit 0.22.27", -] - -[[package]] -name = "yaml-rust2" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" -dependencies = [ - "arraydeque", - "encoding_rs", - "hashlink 0.8.4", -] - [[package]] name = "yansi" 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.7.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" @@ -20355,18 +7379,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "yoke" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive 0.7.5", - "zerofrom", -] - [[package]] name = "yoke" version = "0.8.0" @@ -20375,22 +7387,10 @@ checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", - "yoke-derive 0.8.0", + "yoke-derive", "zerofrom", ] -[[package]] -name = "yoke-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "synstructure", -] - [[package]] name = "yoke-derive" version = "0.8.0" @@ -20412,7 +7412,7 @@ dependencies = [ "async-broadcast", "async-executor", "async-io", - "async-lock 3.4.1", + "async-lock", "async-process", "async-recursion", "async-task", @@ -20464,161 +7464,6 @@ dependencies = [ "zvariant", ] -[[package]] -name = "zed" -version = "0.218.0" -dependencies = [ - "acp_tools", - "activity_indicator", - "agent_settings", - "agent_ui", - "anyhow", - "ashpd 0.11.0", - "askpass", - "assets", - "audio", - "auto_update", - "auto_update_ui", - "bincode", - "breadcrumbs", - "call", - "channel", - "chrono", - "clap", - "cli", - "client", - "codestral", - "collab_ui", - "collections", - "command_palette", - "component", - "copilot", - "crashes", - "dap", - "dap_adapters", - "db", - "debug_adapter_extension", - "debugger_tools", - "debugger_ui", - "diagnostics", - "edit_prediction", - "edit_prediction_ui", - "editor", - "env_logger 0.11.8", - "extension", - "extension_host", - "extensions_ui", - "feature_flags", - "feedback", - "file_finder", - "fs", - "futures 0.3.31", - "git", - "git_hosting_providers", - "git_ui", - "go_to_line", - "gpui", - "gpui_tokio", - "http_client", - "image_viewer", - "inspector_ui", - "install_cli", - "itertools 0.14.0", - "journal", - "json_schema_store", - "keymap_editor", - "language", - "language_extension", - "language_model", - "language_models", - "language_onboarding", - "language_selector", - "language_tools", - "languages", - "line_ending_selector", - "log", - "markdown", - "markdown_preview", - "menu", - "migrator", - "mimalloc", - "miniprofiler_ui", - "nc", - "node_runtime", - "notifications", - "onboarding", - "outline", - "outline_panel", - "parking_lot", - "paths", - "picker", - "pretty_assertions", - "profiling", - "project", - "project_panel", - "project_symbols", - "prompt_store", - "proto", - "rayon", - "recent_projects", - "release_channel", - "remote", - "repl", - "reqwest_client", - "rope", - "search", - "semver", - "serde", - "serde_json", - "session", - "settings", - "settings_profile_selector", - "settings_ui", - "shellexpand 2.1.2", - "smol", - "snippet_provider", - "snippets_ui", - "supermaven", - "svg_preview", - "sysinfo 0.37.2", - "system_specs", - "tab_switcher", - "task", - "tasks_ui", - "telemetry", - "terminal_view", - "theme", - "theme_extension", - "theme_selector", - "time", - "title_bar", - "toolchain_selector", - "tracing", - "tree-sitter-md", - "tree-sitter-rust", - "ui", - "ui_input", - "ui_prompt", - "url", - "urlencoding", - "util", - "uuid", - "vim", - "vim_mode_setting", - "watch", - "web_search", - "web_search_providers", - "windows 0.61.3", - "winresource", - "workspace", - "zed-reqwest", - "zed_actions", - "zed_env_vars", - "zlog", - "zlog_settings", - "ztracing", -] - [[package]] name = "zed-font-kit" version = "0.14.1-zed" @@ -20648,17 +7493,17 @@ name = "zed-reqwest" version = "0.12.15-zed" source = "git+https://github.com/zed-industries/reqwest.git?rev=c15662463bda39148ba154100dd44d3fba5873a4#c15662463bda39148ba154100dd44d3fba5873a4" dependencies = [ - "base64 0.22.1", - "bytes 1.10.1", + "base64", + "bytes", "encoding_rs", "futures-core", "futures-util", - "h2 0.4.12", - "http 1.3.1", - "http-body 1.0.1", + "h2", + "http", + "http-body", "http-body-util", - "hyper 1.7.0", - "hyper-rustls 0.27.7", + "hyper", + "hyper-rustls", "hyper-util", "ipnet", "js-sys", @@ -20669,20 +7514,20 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.33", - "rustls-native-certs 0.8.2", - "rustls-pemfile 2.2.0", + "rustls", + "rustls-native-certs", + "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.2", - "system-configuration 0.6.1", + "sync_wrapper", + "system-configuration", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls", "tokio-socks", "tokio-util", - "tower 0.5.2", + "tower", "tower-service", "url", "wasm-bindgen", @@ -20705,7 +7550,7 @@ dependencies = [ "rand 0.8.5", "screencapturekit", "screencapturekit-sys", - "sysinfo 0.31.4", + "sysinfo", "tao-core-video-sys", "windows 0.61.3", "windows-capture", @@ -20718,7 +7563,7 @@ name = "zed-xim" version = "0.4.0-zed" source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" dependencies = [ - "ahash 0.8.12", + "ahash", "hashbrown 0.14.5", "log", "x11rb", @@ -20726,82 +7571,6 @@ dependencies = [ "xim-parser", ] -[[package]] -name = "zed_actions" -version = "0.1.0" -dependencies = [ - "gpui", - "schemars", - "serde", - "uuid", -] - -[[package]] -name = "zed_env_vars" -version = "0.1.0" -dependencies = [ - "gpui", -] - -[[package]] -name = "zed_extension_api" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "594fd10dd0f2f853eb243e2425e7c95938cef49adb81d9602921d002c5e6d9d9" -dependencies = [ - "serde", - "serde_json", - "wit-bindgen 0.22.0", -] - -[[package]] -name = "zed_extension_api" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0729d50b4ca0a7e28e590bbe32e3ca0194d97ef654961451a424c661a366fca0" -dependencies = [ - "serde", - "serde_json", - "wit-bindgen 0.41.0", -] - -[[package]] -name = "zed_extension_api" -version = "0.8.0" -dependencies = [ - "serde", - "serde_json", - "wit-bindgen 0.41.0", -] - -[[package]] -name = "zed_glsl" -version = "0.1.0" -dependencies = [ - "zed_extension_api 0.1.0", -] - -[[package]] -name = "zed_html" -version = "0.2.3" -dependencies = [ - "zed_extension_api 0.7.0", -] - -[[package]] -name = "zed_proto" -version = "0.2.3" -dependencies = [ - "zed_extension_api 0.1.0", -] - -[[package]] -name = "zed_test_extension" -version = "0.1.0" -dependencies = [ - "zed_extension_api 0.8.0", -] - [[package]] name = "zeno" version = "0.3.3" @@ -20869,30 +7638,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "zeromq" -version = "0.5.0-pre" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1fe92954d37e77bed5e2775cb0fed7dba5f6bc4be6f7f76172a4eb371dc6a9b" -dependencies = [ - "async-dispatcher", - "async-std", - "async-trait", - "asynchronous-codec", - "bytes 1.10.1", - "crossbeam-queue", - "dashmap 5.5.3", - "futures 0.3.31", - "log", - "num-traits", - "once_cell", - "parking_lot", - "rand 0.8.5", - "regex", - "thiserror 1.0.69", - "uuid", -] - [[package]] name = "zerotrie" version = "0.2.2" @@ -20900,7 +7645,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" dependencies = [ "displaydoc", - "yoke 0.8.0", + "yoke", "zerofrom", ] @@ -20910,7 +7655,7 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ - "yoke 0.8.0", + "yoke", "zerofrom", "zerovec-derive", ] @@ -20926,113 +7671,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "zeta_prompt" -version = "0.1.0" -dependencies = [ - "serde", -] - -[[package]] -name = "zip" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" -dependencies = [ - "aes", - "byteorder", - "bzip2", - "constant_time_eq", - "crc32fast", - "crossbeam-utils", - "flate2", - "hmac", - "pbkdf2 0.11.0", - "sha1", - "time", - "zstd", -] - -[[package]] -name = "zip" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cc23c04387f4da0374be4533ad1208cbb091d5c11d070dfef13676ad6497164" -dependencies = [ - "arbitrary", - "crc32fast", - "crossbeam-utils", - "displaydoc", - "indexmap", - "num_enum", - "thiserror 1.0.69", -] - -[[package]] -name = "zlog" -version = "0.1.0" -dependencies = [ - "anyhow", - "chrono", - "collections", - "log", - "tempfile", -] - -[[package]] -name = "zlog_settings" -version = "0.1.0" -dependencies = [ - "collections", - "gpui", - "settings", - "zlog", -] - -[[package]] -name = "zstd" -version = "0.11.2+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "5.0.2+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" -dependencies = [ - "libc", - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] - -[[package]] -name = "ztracing" -version = "0.1.0" -dependencies = [ - "tracing", - "tracing-subscriber", - "tracing-tracy", - "zlog", - "ztracing_macro", -] - -[[package]] -name = "ztracing_macro" -version = "0.1.0" - [[package]] name = "zune-core" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 523dce229e..9c58e8f496 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,230 +1,24 @@ [workspace] resolver = "2" members = [ - "crates/acp_tools", - "crates/acp_thread", - "crates/action_log", - "crates/activity_indicator", - "crates/agent", - "crates/agent_servers", - "crates/agent_settings", - "crates/agent_ui", - "crates/ai_onboarding", - "crates/anthropic", - "crates/askpass", - "crates/assets", - "crates/assistant_text_thread", - "crates/assistant_slash_command", - "crates/assistant_slash_commands", - "crates/audio", - "crates/auto_update", - "crates/auto_update_helper", - "crates/auto_update_ui", - "crates/aws_http_client", - "crates/bedrock", - "crates/breadcrumbs", - "crates/buffer_diff", - "crates/call", - "crates/channel", - "crates/cli", - "crates/client", - "crates/clock", - "crates/cloud_api_client", - "crates/cloud_api_types", - "crates/cloud_llm_client", - "crates/collab", - "crates/collab_ui", "crates/collections", - "crates/command_palette", - "crates/command_palette_hooks", - "crates/component", - "crates/context_server", - "crates/copilot", - "crates/crashes", - "crates/credentials_provider", - "crates/dap", - "crates/dap_adapters", - "crates/db", - "crates/debug_adapter_extension", - "crates/debugger_tools", - "crates/debugger_ui", - "crates/deepseek", - "crates/denoise", - "crates/diagnostics", - "crates/docs_preprocessor", - "crates/edit_prediction", - "crates/edit_prediction_types", - "crates/edit_prediction_ui", - "crates/edit_prediction_context", - "crates/editor", - "crates/eval", - "crates/eval_utils", - "crates/explorer_command_injector", - "crates/extension", - "crates/extension_api", - "crates/extension_cli", - "crates/extension_host", - "crates/extensions_ui", - "crates/feature_flags", - "crates/feedback", - "crates/file_finder", - "crates/file_icons", - "crates/fs", - "crates/fs_benchmarks", - "crates/fsevent", - "crates/fuzzy", - "crates/git", - "crates/git_hosting_providers", - "crates/git_ui", - "crates/go_to_line", - "crates/google_ai", "crates/gpui", "crates/gpui_macros", "crates/gpui_tokio", "crates/html_to_markdown", "crates/http_client", "crates/http_client_tls", - "crates/icons", - "crates/image_viewer", - "crates/inspector_ui", - "crates/install_cli", - "crates/journal", - "crates/json_schema_store", - "crates/keymap_editor", - "crates/language", - "crates/language_extension", - "crates/language_model", - "crates/language_models", - "crates/language_onboarding", - "crates/language_selector", - "crates/language_tools", - "crates/languages", - "crates/line_ending_selector", - "crates/livekit_api", - "crates/livekit_client", - "crates/lmstudio", - "crates/lsp", - "crates/markdown", - "crates/markdown_preview", "crates/media", - "crates/menu", - "crates/migrator", - "crates/mistral", - "crates/miniprofiler_ui", - "crates/multi_buffer", - "crates/nc", - "crates/net", - "crates/node_runtime", - "crates/notifications", - "crates/ollama", - "crates/onboarding", - "crates/open_ai", - "crates/open_router", - "crates/outline", - "crates/outline_panel", - "crates/panel", - "crates/paths", - "crates/picker", - "crates/prettier", - "crates/project", - "crates/project_benchmarks", - "crates/project_panel", - "crates/project_symbols", - "crates/prompt_store", - "crates/proto", - "crates/recent_projects", "crates/refineable", "crates/refineable/derive_refineable", - "crates/release_channel", - "crates/scheduler", - "crates/remote", - "crates/remote_server", - "crates/repl", "crates/reqwest_client", - "crates/rich_text", - "crates/rope", - "crates/rpc", - "crates/rules_library", - "crates/schema_generator", - "crates/search", - "crates/session", - "crates/settings", - "crates/settings_json", - "crates/settings_macros", - "crates/settings_profile_selector", - "crates/settings_ui", - "crates/snippet", - "crates/snippet_provider", - "crates/snippets_ui", - "crates/sqlez", - "crates/sqlez_macros", - "crates/story", - "crates/storybook", - "crates/streaming_diff", + "crates/scheduler", "crates/sum_tree", - "crates/supermaven", - "crates/supermaven_api", - "crates/codestral", - "crates/svg_preview", - "crates/system_specs", - "crates/tab_switcher", - "crates/task", - "crates/tasks_ui", - "crates/telemetry", - "crates/telemetry_events", - "crates/terminal", - "crates/terminal_view", - "crates/text", - "crates/theme", - "crates/theme_extension", - "crates/theme_importer", - "crates/theme_selector", - "crates/time_format", - "crates/title_bar", - "crates/toolchain_selector", - "crates/ui", - "crates/ui_input", - "crates/ui_macros", - "crates/ui_prompt", "crates/util", "crates/util_macros", - "crates/vercel", - "crates/vim", - "crates/vim_mode_setting", "crates/watch", - "crates/web_search", - "crates/web_search_providers", - "crates/workspace", - "crates/worktree", - "crates/x_ai", - "crates/zed", - "crates/zed_actions", - "crates/zed_env_vars", - "crates/edit_prediction_cli", - "crates/zeta_prompt", - "crates/zlog", - "crates/zlog_settings", - "crates/ztracing", - "crates/ztracing_macro", - - # - # Extensions - # - - "extensions/glsl", - "extensions/html", - "extensions/proto", - "extensions/slash-commands-example", - "extensions/test-extension", - - # - # Tooling - # - "tooling/perf", - "tooling/xtask", ] -default-members = ["crates/zed"] [workspace.package] publish = false @@ -236,207 +30,30 @@ 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" } -activity_indicator = { path = "crates/activity_indicator" } -agent_ui = { path = "crates/agent_ui" } -agent_settings = { path = "crates/agent_settings" } -agent_servers = { path = "crates/agent_servers" } -ai_onboarding = { path = "crates/ai_onboarding" } -anthropic = { path = "crates/anthropic" } -askpass = { path = "crates/askpass" } -assets = { path = "crates/assets" } -assistant_text_thread = { path = "crates/assistant_text_thread" } -assistant_slash_command = { path = "crates/assistant_slash_command" } -assistant_slash_commands = { path = "crates/assistant_slash_commands" } -audio = { path = "crates/audio" } -auto_update = { path = "crates/auto_update" } -auto_update_ui = { path = "crates/auto_update_ui" } -aws_http_client = { path = "crates/aws_http_client" } -bedrock = { path = "crates/bedrock" } -breadcrumbs = { path = "crates/breadcrumbs" } -buffer_diff = { path = "crates/buffer_diff" } -call = { path = "crates/call" } -channel = { path = "crates/channel" } -cli = { path = "crates/cli" } -client = { path = "crates/client" } -clock = { path = "crates/clock" } -cloud_api_client = { path = "crates/cloud_api_client" } -cloud_api_types = { path = "crates/cloud_api_types" } -cloud_llm_client = { path = "crates/cloud_llm_client" } -collab_ui = { path = "crates/collab_ui" } collections = { path = "crates/collections", version = "0.1.0" } -command_palette = { path = "crates/command_palette" } -command_palette_hooks = { path = "crates/command_palette_hooks" } -component = { path = "crates/component" } -context_server = { path = "crates/context_server" } -copilot = { path = "crates/copilot" } -crashes = { path = "crates/crashes" } -credentials_provider = { path = "crates/credentials_provider" } -crossbeam = "0.8.4" -dap = { path = "crates/dap" } -dap_adapters = { path = "crates/dap_adapters" } -db = { path = "crates/db" } -debug_adapter_extension = { path = "crates/debug_adapter_extension" } -debugger_tools = { path = "crates/debugger_tools" } -debugger_ui = { path = "crates/debugger_ui" } -deepseek = { path = "crates/deepseek" } derive_refineable = { path = "crates/refineable/derive_refineable" } -diagnostics = { path = "crates/diagnostics" } -editor = { path = "crates/editor" } -eval_utils = { path = "crates/eval_utils" } -extension = { path = "crates/extension" } -extension_host = { path = "crates/extension_host" } -extensions_ui = { path = "crates/extensions_ui" } -feature_flags = { path = "crates/feature_flags" } -feedback = { path = "crates/feedback" } -file_finder = { path = "crates/file_finder" } -file_icons = { path = "crates/file_icons" } -fs = { path = "crates/fs" } -fsevent = { path = "crates/fsevent" } -fuzzy = { path = "crates/fuzzy" } -git = { path = "crates/git" } -git_hosting_providers = { path = "crates/git_hosting_providers" } -git_ui = { path = "crates/git_ui" } -go_to_line = { path = "crates/go_to_line" } -google_ai = { path = "crates/google_ai" } gpui = { path = "crates/gpui", default-features = false } gpui_macros = { path = "crates/gpui_macros" } gpui_tokio = { path = "crates/gpui_tokio" } html_to_markdown = { path = "crates/html_to_markdown" } 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_types = { path = "crates/edit_prediction_types" } -edit_prediction_ui = { path = "crates/edit_prediction_ui" } -edit_prediction_context = { path = "crates/edit_prediction_context" } -inspector_ui = { path = "crates/inspector_ui" } -install_cli = { path = "crates/install_cli" } -journal = { path = "crates/journal" } -json_schema_store = { path = "crates/json_schema_store" } -keymap_editor = { path = "crates/keymap_editor" } -language = { path = "crates/language" } -language_extension = { path = "crates/language_extension" } -language_model = { path = "crates/language_model" } -language_models = { path = "crates/language_models" } -language_onboarding = { path = "crates/language_onboarding" } -language_selector = { path = "crates/language_selector" } -language_tools = { path = "crates/language_tools" } -languages = { path = "crates/languages" } -line_ending_selector = { path = "crates/line_ending_selector" } -livekit_api = { path = "crates/livekit_api" } -livekit_client = { path = "crates/livekit_client" } -lmstudio = { path = "crates/lmstudio" } -lsp = { path = "crates/lsp" } -markdown = { path = "crates/markdown" } -markdown_preview = { path = "crates/markdown_preview" } -svg_preview = { path = "crates/svg_preview" } media = { path = "crates/media" } -menu = { path = "crates/menu" } -migrator = { path = "crates/migrator" } -mistral = { path = "crates/mistral" } -multi_buffer = { path = "crates/multi_buffer" } -miniprofiler_ui = { path = "crates/miniprofiler_ui" } -nc = { path = "crates/nc" } -net = { path = "crates/net" } -node_runtime = { path = "crates/node_runtime" } -notifications = { path = "crates/notifications" } -ollama = { path = "crates/ollama" } -onboarding = { path = "crates/onboarding" } -open_ai = { path = "crates/open_ai" } -open_router = { path = "crates/open_router", features = ["schemars"] } -outline = { path = "crates/outline" } -outline_panel = { path = "crates/outline_panel" } -panel = { path = "crates/panel" } -paths = { path = "crates/paths" } -perf = { path = "tooling/perf" } -picker = { path = "crates/picker" } -prettier = { path = "crates/prettier" } -settings_profile_selector = { path = "crates/settings_profile_selector" } -project = { path = "crates/project" } -project_panel = { path = "crates/project_panel" } -project_symbols = { path = "crates/project_symbols" } -prompt_store = { path = "crates/prompt_store" } -proto = { path = "crates/proto" } -recent_projects = { path = "crates/recent_projects" } refineable = { path = "crates/refineable" } -release_channel = { path = "crates/release_channel" } -remote = { path = "crates/remote" } -remote_server = { path = "crates/remote_server" } -repl = { path = "crates/repl" } reqwest_client = { path = "crates/reqwest_client" } -rodio = { git = "https://github.com/RustAudio/rodio", rev ="e2074c6c2acf07b57cf717e076bdda7a9ac6e70b", features = ["wav", "playback", "wav_output", "recording"] } -rope = { path = "crates/rope" } -rpc = { path = "crates/rpc" } -rules_library = { path = "crates/rules_library" } -search = { path = "crates/search" } -session = { path = "crates/session" } -settings = { path = "crates/settings" } -settings_json = { path = "crates/settings_json" } -settings_macros = { path = "crates/settings_macros" } -settings_ui = { path = "crates/settings_ui" } -snippet = { path = "crates/snippet" } -snippet_provider = { path = "crates/snippet_provider" } -snippets_ui = { path = "crates/snippets_ui" } -sqlez = { path = "crates/sqlez" } -sqlez_macros = { path = "crates/sqlez_macros" } -story = { path = "crates/story" } -streaming_diff = { path = "crates/streaming_diff" } +scheduler = { path = "crates/scheduler" } sum_tree = { path = "crates/sum_tree" } -supermaven = { path = "crates/supermaven" } -supermaven_api = { path = "crates/supermaven_api" } -codestral = { path = "crates/codestral" } -system_specs = { path = "crates/system_specs" } -tab_switcher = { path = "crates/tab_switcher" } -task = { path = "crates/task" } -tasks_ui = { path = "crates/tasks_ui" } -telemetry = { path = "crates/telemetry" } -telemetry_events = { path = "crates/telemetry_events" } -terminal = { path = "crates/terminal" } -terminal_view = { path = "crates/terminal_view" } -text = { path = "crates/text" } -theme = { path = "crates/theme" } -theme_extension = { path = "crates/theme_extension" } -theme_selector = { path = "crates/theme_selector" } -time_format = { path = "crates/time_format" } -title_bar = { path = "crates/title_bar" } -toolchain_selector = { path = "crates/toolchain_selector" } -ui = { path = "crates/ui" } -ui_input = { path = "crates/ui_input" } -ui_macros = { path = "crates/ui_macros" } -ui_prompt = { path = "crates/ui_prompt" } util = { path = "crates/util" } util_macros = { path = "crates/util_macros" } -vercel = { path = "crates/vercel" } -vim = { path = "crates/vim" } -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" } -workspace = { path = "crates/workspace" } -worktree = { path = "crates/worktree" } -x_ai = { path = "crates/x_ai" } -zed = { path = "crates/zed" } -zed_actions = { path = "crates/zed_actions" } -zed_env_vars = { path = "crates/zed_env_vars" } -edit_prediction = { path = "crates/edit_prediction" } -zeta_prompt = { path = "crates/zeta_prompt" } -zlog = { path = "crates/zlog" } -zlog_settings = { path = "crates/zlog_settings" } -ztracing = { path = "crates/ztracing" } -ztracing_macro = { path = "crates/ztracing_macro" } +perf = { path = "tooling/perf" } # # External crates # -agent-client-protocol = { version = "=0.9.0", features = ["unstable"] } aho-corasick = "1.1" -alacritty_terminal = "0.25.1-rc1" +circular-buffer = "1.0" any_vec = "0.14" anyhow = "1.0.86" arrayvec = { version = "0.7.4", features = ["serde"] } @@ -446,22 +63,12 @@ async-compression = { version = "0.4", features = ["gzip", "futures-io"] } async-dispatcher = "0.1" async-fs = "2.1" async-lock = "2.1" -async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" } async-recursion = "1.0.0" async-tar = "0.5.1" async-task = "4.7" async-trait = "0.1" async-tungstenite = "0.31.0" async_zip = { version = "0.0.18", features = ["deflate", "deflate64"] } -aws-config = { version = "1.6.1", features = ["behavior-version-latest"] } -aws-credential-types = { version = "1.2.2", features = [ - "hardcoded-credentials", -] } -aws-sdk-bedrockruntime = { version = "1.80.0", features = [ - "behavior-version-latest", -] } -aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] } -aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] } backtrace = "0.3" base64 = "0.22" bincode = "1.2.1" @@ -471,76 +78,43 @@ blade-macros = { version = "0.3.0" } blade-util = { version = "0.3.0" } brotli = "8.0.2" bytes = "1.0" -cargo_metadata = "0.19" -cargo_toml = "0.21" cfg-if = "1.0.3" chrono = { version = "0.4", features = ["serde"] } -ciborium = "0.2" -circular-buffer = "1.0" -clap = { version = "4.4", features = ["derive", "wrap_help"] } cocoa = "=0.26.0" cocoa-foundation = "=0.2.0" convert_case = "0.8.0" 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"] } +crossbeam = "0.8.4" ctor = "0.4.0" -dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "1b461b310481d01e02b2603c16d7144b926339f8" } dashmap = "6.0" derive_more = "0.99.17" dirs = "4.0" -documented = "0.9.1" -dotenvy = "0.15.0" -ec4rs = "1.1" -emojis = "0.6.1" env_logger = "0.11" -exec = "0.3.1" -fancy-regex = "0.16.0" -fork = "0.4.0" futures = "0.3" futures-lite = "1.13" -gh-workflow = { git = "https://github.com/zed-industries/gh-workflow", rev = "09acfdf2bd5c1d6254abefd609c808ff73547b2c" } git2 = { version = "0.20.1", default-features = false } globset = "0.4" -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" +indoc = "2" +inventory = "0.3.19" http = "1.1" http-body = "1.0" hyper = "0.14" -ignore = "0.4.22" image = "0.25.1" -imara-diff = "0.1.8" indexmap = { version = "2.7.0", features = ["serde"] } -indoc = "2" -inventory = "0.3.19" itertools = "0.14.0" -json_dotpath = "1.1" -jsonschema = "0.37.0" -jsonwebtoken = "9.3" -jupyter-protocol = "0.10.0" -jupyter-websocket-client = "0.15.0" libc = "0.2" -libsqlite3-sys = { version = "0.30.1", features = ["bundled"] } linkify = "0.10.0" log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] } -lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "b71ab4eeb27d9758be8092020a46fe33fbca4e33" } 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" -nbformat = "0.15.0" nix = "0.29" -num-format = "0.4.4" objc = "0.2" objc2-foundation = { version = "=0.3.1", default-features = false, features = [ "NSArray", @@ -571,25 +145,11 @@ open = "5.0.0" ordered-float = "2.1.1" palette = { version = "0.7.5", default-features = false, features = ["std"] } parking_lot = "0.12.1" -partial-json-fixer = "0.5.3" -parse_int = "0.9" -pciid-parser = "0.8.0" pathdiff = "0.2" -pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" } -pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" } -pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" } -pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" } -pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" } -pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" } -pet-virtualenv = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" } -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" -prost = "0.9" -prost-build = "0.9" -prost-types = "0.9" pulldown-cmark = { version = "0.12.0", default-features = false } quote = "1.0.9" rand = "0.9" @@ -605,10 +165,6 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662 "socks", "stream", ], package = "zed-reqwest", version = "0.12.15-zed" } -rsa = "0.9.6" -runtimelib = { version = "0.30.0", default-features = false, features = [ - "async-dispatcher-runtime", "aws-lc-rs" -] } rust-embed = { version = "8.4", features = ["include-exclude"] } rustc-hash = "2.1.0" rustls = { version = "0.23.26" } @@ -623,19 +179,14 @@ serde_json_lenient = { version = "0.2", features = [ "preserve_order", "raw_value", ] } -serde_path_to_error = "0.1.17" -serde_repr = "0.1" serde_urlencoded = "0.7" sha2 = "0.10" -shellexpand = "2.1.0" shlex = "1.3.0" -simplelog = "0.12.2" +shellexpand = "2.1.0" slotmap = "1.0.6" smallvec = { version = "1.6", features = ["union", "const_new"] } smol = "2.0" -sqlformat = "0.2" stacksafe = "0.1" -streaming-iterator = "0.1" strsim = "0.11" strum = { version = "0.27.2", features = ["derive"] } subtle = "2.5.0" @@ -645,7 +196,6 @@ sysinfo = "0.37.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 = "2570c4387a8505fb8f1d3f3557454b474f1e8271" } time = { version = "0.3", features = [ "macros", "parsing", @@ -654,64 +204,20 @@ time = { version = "0.3", features = [ "formatting", "local-offset", ] } -tiny_http = "0.8" tokio = { version = "1" } tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] } tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io", "tokio"] } -toml = "0.8" -toml_edit = { version = "0.22", default-features = false, features = ["display", "parse", "serde"] } -tower-http = "0.4.4" -tree-sitter = { version = "0.25.10", features = ["wasm"] } -tree-sitter-bash = "0.25.1" -tree-sitter-c = "0.23" -tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" } -tree-sitter-css = "0.23" -tree-sitter-diff = "0.1.0" -tree-sitter-elixir = "0.3" -tree-sitter-embedded-template = "0.23.0" -tree-sitter-gitcommit = { git = "https://github.com/zed-industries/tree-sitter-git-commit", rev = "88309716a69dd13ab83443721ba6e0b491d37ee9" } -tree-sitter-go = "0.23" -tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "2e886870578eeba1927a2dc4bd2e2b3f598c5f9a", package = "tree-sitter-gomod" } -tree-sitter-gowork = { git = "https://github.com/zed-industries/tree-sitter-go-work", rev = "acb0617bf7f4fda02c6217676cc64acb89536dc7" } -tree-sitter-heex = { git = "https://github.com/zed-industries/tree-sitter-heex", rev = "1dd45142fbb05562e35b2040c6129c9bca346592" } -tree-sitter-html = "0.23" -tree-sitter-jsdoc = "0.23" -tree-sitter-json = "0.24" -tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", rev = "9a23c1a96c0513d8fc6520972beedd419a973539" } -tree-sitter-python = "0.25" -tree-sitter-regex = "0.24" -tree-sitter-ruby = "0.23" -tree-sitter-rust = "0.24" -tree-sitter-typescript = { git = "https://github.com/zed-industries/tree-sitter-typescript", rev = "e2c53597d6a5d9cf7bbe8dccde576fe1e46c5899" } # https://github.com/tree-sitter/tree-sitter-typescript/pull/347 -tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" } tracing = "0.1.40" unicase = "2.6" -unicode-script = "0.5.7" unicode-segmentation = "1.10" -unindent = "0.2.0" url = "2.2" -urlencoding = "2.1.2" uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] } walkdir = "2.5" -wasm-encoder = "0.221" -wasmparser = "0.221" -wasmtime = { version = "29", default-features = false, features = [ - "async", - "demangle", - "runtime", - "cranelift", - "component-model", - "incremental-cache", - "parallel-compilation", -] } -wasmtime-wasi = "29" -wax = "0.6" which = "6.0.0" windows-core = "0.61" yawc = "0.2.5" zeroize = "1.8" -zstd = "0.11" - +ciborium = "0.2" [workspace.dependencies.windows] version = "0.61" @@ -765,9 +271,6 @@ features = [ ] [patch.crates-io] -notify = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" } -notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" } -windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" } calloop = { git = "https://github.com/zed-industries/calloop" } [profile.dev] @@ -785,9 +288,6 @@ codegen-units = 16 # proc-macros start gpui_macros = { opt-level = 3 } derive_refineable = { opt-level = 3 } -settings_macros = { opt-level = 3 } -sqlez_macros = { opt-level = 3, codegen-units = 1 } -ui_macros = { opt-level = 3 } util_macros = { opt-level = 3 } quote = { opt-level = 3 } syn = { opt-level = 3 } @@ -796,52 +296,16 @@ proc-macro2 = { opt-level = 3 } taffy = { opt-level = 3 } resvg = { opt-level = 3 } -wasmtime = { opt-level = 3 } -# Build single-source-file crates with cg=1 as it helps make `cargo build` of a whole workspace a bit faster -activity_indicator = { codegen-units = 1 } -assets = { codegen-units = 1 } -breadcrumbs = { codegen-units = 1 } + collections = { codegen-units = 1 } -command_palette = { codegen-units = 1 } -command_palette_hooks = { codegen-units = 1 } -feature_flags = { codegen-units = 1 } -file_icons = { codegen-units = 1 } -fsevent = { codegen-units = 1 } -image_viewer = { codegen-units = 1 } -edit_prediction_ui = { codegen-units = 1 } -install_cli = { codegen-units = 1 } -journal = { codegen-units = 1 } -json_schema_store = { codegen-units = 1 } -lmstudio = { codegen-units = 1 } -menu = { codegen-units = 1 } -notifications = { codegen-units = 1 } -ollama = { codegen-units = 1 } -outline = { codegen-units = 1 } -paths = { codegen-units = 1 } -prettier = { codegen-units = 1 } -project_symbols = { codegen-units = 1 } refineable = { codegen-units = 1 } -release_channel = { codegen-units = 1 } reqwest_client = { codegen-units = 1 } -session = { codegen-units = 1 } -snippet = { codegen-units = 1 } -snippets_ui = { codegen-units = 1 } -story = { codegen-units = 1 } -supermaven_api = { codegen-units = 1 } -telemetry_events = { codegen-units = 1 } -theme_selector = { codegen-units = 1 } -time_format = { codegen-units = 1 } -ui_input = { codegen-units = 1 } -zed_actions = { codegen-units = 1 } [profile.release] debug = "limited" lto = "thin" codegen-units = 1 -[profile.release.package] -zed = { codegen-units = 16 } - [profile.release-fast] inherits = "release" debug = "full" @@ -897,9 +361,5 @@ nonminimal_bool = "allow" ignored = [ "bindgen", "cbindgen", - "prost_build", "serde", - "component", - "documented", - "sea-orm-macros", -] +] \ No newline at end of file diff --git a/Dockerfile-collab b/Dockerfile-collab deleted file mode 100644 index 68f898618a..0000000000 --- a/Dockerfile-collab +++ /dev/null @@ -1,37 +0,0 @@ -# syntax = docker/dockerfile:1.2 - -FROM rust:1.91.1-bookworm as builder -WORKDIR app -COPY . . - -# Replace the Cargo configuration with the one used by collab. -COPY ./.cargo/collab-config.toml ./.cargo/config.toml - -# Compile collab server -ARG CARGO_PROFILE_RELEASE_PANIC=abort -ARG GITHUB_SHA - -ENV GITHUB_SHA=$GITHUB_SHA - -# Also add `cmake`, since we need it to build `wasmtime`. -RUN apt-get update; \ - apt-get install -y --no-install-recommends cmake - -RUN --mount=type=cache,target=./script/node_modules \ - --mount=type=cache,target=/usr/local/cargo/registry \ - --mount=type=cache,target=/usr/local/cargo/git \ - --mount=type=cache,target=./target \ - cargo build --release --package collab --bin collab - -# Copy collab server binary out of cached directory -RUN --mount=type=cache,target=./target \ - cp /app/target/release/collab /app/collab - -# Copy collab server binary to the runtime image -FROM debian:bookworm-slim as runtime -RUN apt-get update; \ - apt-get install -y --no-install-recommends libcurl4-openssl-dev ca-certificates \ - linux-perf binutils -WORKDIR app -COPY --from=builder /app/collab /app/collab -ENTRYPOINT ["/app/collab"] diff --git a/Dockerfile-collab.dockerignore b/Dockerfile-collab.dockerignore deleted file mode 100644 index 337b4d4262..0000000000 --- a/Dockerfile-collab.dockerignore +++ /dev/null @@ -1,16 +0,0 @@ -.git -.github -**/.gitignore -**/.gitkeep -.gitattributes -.mailmap -**/target -zed.xcworkspace -.DS_Store -compose.yml -plugins/bin -script/node_modules -styles/node_modules -crates/collab/static/styles.css -vendor/bin -assets/themes/ diff --git a/Dockerfile-cross.dockerignore b/Dockerfile-cross.dockerignore deleted file mode 100644 index 337b4d4262..0000000000 --- a/Dockerfile-cross.dockerignore +++ /dev/null @@ -1,16 +0,0 @@ -.git -.github -**/.gitignore -**/.gitkeep -.gitattributes -.mailmap -**/target -zed.xcworkspace -.DS_Store -compose.yml -plugins/bin -script/node_modules -styles/node_modules -crates/collab/static/styles.css -vendor/bin -assets/themes/ diff --git a/Dockerfile-distros b/Dockerfile-distros deleted file mode 100644 index c8a98d2f7d..0000000000 --- a/Dockerfile-distros +++ /dev/null @@ -1,26 +0,0 @@ -# syntax=docker/dockerfile:1 - -ARG BASE_IMAGE -FROM ${BASE_IMAGE} -WORKDIR /app -ARG TZ=Etc/UTC \ - LANG=C.UTF-8 \ - LC_ALL=C.UTF-8 \ - DEBIAN_FRONTEND=noninteractive -ENV CARGO_TERM_COLOR=always - -COPY script/linux script/ -RUN ./script/linux -COPY script/install-mold script/install-cmake script/ -RUN ./script/install-mold "2.34.0" -RUN ./script/install-cmake "3.30.4" - -COPY . . - -# When debugging, make these into individual RUN statements. -# Cleanup to avoid saving big layers we aren't going to use. -RUN . "$HOME/.cargo/env" \ - && cargo fetch \ - && cargo build \ - && cargo run -- --help \ - && cargo clean --quiet diff --git a/Dockerfile-distros.dockerignore b/Dockerfile-distros.dockerignore deleted file mode 100644 index de70e0d167..0000000000 --- a/Dockerfile-distros.dockerignore +++ /dev/null @@ -1,2 +0,0 @@ -**/target -**/node_modules diff --git a/LICENSE-AGPL b/LICENSE-AGPL deleted file mode 100644 index 87a0dea90e..0000000000 --- a/LICENSE-AGPL +++ /dev/null @@ -1,788 +0,0 @@ -Copyright 2022 - 2025 Zed Industries, Inc. - - - - -This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. -You should have received a copy of the GNU Affero General Public License along with this program. If not, see . - - - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - Preamble - - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - - The precise terms and conditions for copying, distribution and -modification follow. - - - TERMS AND CONDITIONS - - - 0. Definitions. - - - "This License" refers to version 3 of the GNU Affero General Public License. - - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - - A "covered work" means either the unmodified Program or a work based -on the Program. - - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - - 1. Source Code. - - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - - The Corresponding Source for a work in source code form is that -same work. - - - 2. Basic Permissions. - - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - - 4. Conveying Verbatim Copies. - - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - - 5. Conveying Modified Source Versions. - - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - - 6. Conveying Non-Source Forms. - - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - - 7. Additional Terms. - - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - - 8. Termination. - - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - - 9. Acceptance Not Required for Having Copies. - - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - - 10. Automatic Licensing of Downstream Recipients. - - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - - 11. Patents. - - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - - 12. No Surrender of Others' Freedom. - - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - - 13. Remote Network Interaction; Use with the GNU General Public License. - - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - - 14. Revised Versions of this License. - - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - - 15. Disclaimer of Warranty. - - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - - 16. Limitation of Liability. - - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - - 17. Interpretation of Sections 15 and 16. - - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - - END OF TERMS AND CONDITIONS - - - How to Apply These Terms to Your New Programs - - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - - Copyright (C) - - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - - -Also add information on how to contact you by electronic and paper mail. - - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. diff --git a/LICENSE-GPL b/LICENSE-GPL deleted file mode 100644 index cb82534a8d..0000000000 --- a/LICENSE-GPL +++ /dev/null @@ -1,200 +0,0 @@ -GNU GENERAL PUBLIC LICENSE -Version 3, 29 June 2007 - -Copyright © 2007 Free Software Foundation, Inc. - -Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - -Preamble - -The GNU General Public License is a free, copyleft license for software and other kinds of works. - -The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. - -When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. - -To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. - -For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. - -Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. - -For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. - -Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. - -Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. - -The precise terms and conditions for copying, distribution and modification follow. - -TERMS AND CONDITIONS - -0. Definitions. -"This License" refers to version 3 of the GNU General Public License. - -"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. - -"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. - -To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. - -A "covered work" means either the unmodified Program or a work based on the Program. - -To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. - -To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. - -An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. - -1. Source Code. -The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. -A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. - -The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. - -The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. - -The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. - -The Corresponding Source for a work in source code form is that same work. - -2. Basic Permissions. -All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. -You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. - -Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. - -3. Protecting Users' Legal Rights From Anti-Circumvention Law. -No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. -When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. - -4. Conveying Verbatim Copies. -You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. -You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. - -5. Conveying Modified Source Versions. -You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: -a) The work must carry prominent notices stating that you modified it, and giving a relevant date. -b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". -c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. -d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. -A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. - -6. Conveying Non-Source Forms. -You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: -a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. -b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. -c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. -d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. -e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. -A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. - -A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. - -"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. - -If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). - -The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. - -Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. - -7. Additional Terms. -"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. -When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. - -Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: - -a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or -b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or -c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or -d) Limiting the use for publicity purposes of names of licensors or authors of the material; or -e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or -f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. -All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. - -If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. - -Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. - -8. Termination. -You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). -However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. - -Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. - -Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. - -9. Acceptance Not Required for Having Copies. -You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. -10. Automatic Licensing of Downstream Recipients. -Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. -An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. - -You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. - -11. Patents. -A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". -A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. - -Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. - -In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. - -If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. - -If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. - -A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. - -Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. - -12. No Surrender of Others' Freedom. -If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. -13. Use with the GNU Affero General Public License. -Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. -14. Revised Versions of this License. -The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. -Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. - -If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. - -Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. - -15. Disclaimer of Warranty. -THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. -16. Limitation of Liability. -IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. -17. Interpretation of Sections 15 and 16. -If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. - -END OF TERMS AND CONDITIONS - -How to Apply These Terms to Your New Programs - -If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. - -To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - -Copyright (C) - -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - -If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: - - Copyright (C) -This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. -This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". - -You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . - -The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . diff --git a/Procfile b/Procfile deleted file mode 100644 index b3f13f66a6..0000000000 --- a/Procfile +++ /dev/null @@ -1,4 +0,0 @@ -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 63190fc2ee..0000000000 --- a/Procfile.web +++ /dev/null @@ -1 +0,0 @@ -website: cd ../zed.dev; npm run dev -- --port=3000 diff --git a/REVIEWERS.conl b/REVIEWERS.conl deleted file mode 100644 index 45155ba346..0000000000 --- a/REVIEWERS.conl +++ /dev/null @@ -1,133 +0,0 @@ -; This file contains a list of people who're interested in reviewing pull requests -; to certain parts of the code-base. -; -; This is mostly used internally for PR assignment, and may change over time. -; -; If you have permission to merge PRs (mostly equivalent to "do you work at Zed Industries"), -; we strongly encourage you to put your name in the "all" bucket, but you can also add yourself -; to other areas too. - - - = @cole-miller - = @ConradIrwin - = @danilo-leal - = @dinocosta - = @HactarCE - = @kubkon - = @maxdeviant - = @p1n3appl3 - = @probably-neb - = @smitbarmase - = @SomeoneToIgnore - = @Veykril - -ai - = @benbrandt - = @bennetbo - = @danilo-leal - = @rtfeldman - -audio - = @dvdsk - -crashes - = @p1n3appl3 - = @Veykril - -debugger - = @Anthony-Eid - = @kubkon - = @osiewicz - -design - = @danilo-leal - -docs - = @miguelraz - = @probably-neb - = @yeskunall - -extension - = @kubkon - -git - = @cole-miller - = @danilo-leal - = @dvdsk - = @kubkon - = @Anthony-Eid - = @cameron1024 - -gpui - = @Anthony-Eid - = @cameron1024 - = @mikayla-maki - = @probably-neb - -helix - = @kubkon - -languages - = @osiewicz - = @probably-neb - = @smitbarmase - = @SomeoneToIgnore - = @Veykril - -linux - = @cole-miller - = @dvdsk - = @p1n3appl3 - = @probably-neb - = @smitbarmase - -lsp - = @osiewicz - = @smitbarmase - = @SomeoneToIgnore - = @Veykril - -multi_buffer - = @Veykril - = @SomeoneToIgnore - -pickers - = @dvdsk - = @p1n3appl3 - = @SomeoneToIgnore - -project_panel - = @smitbarmase - -settings_ui - = @Anthony-Eid - = @danilo-leal - = @probably-neb - -sum_tree - = @Veykril - -support - = @miguelraz - -tasks - = @SomeoneToIgnore - = @Veykril - -terminal - = @kubkon - = @Veykril - -text - = @Veykril - -vim - = @ConradIrwin - = @dinocosta - = @p1n3appl3 - = @probably-neb - -windows - = @localcc - = @reflectronic - = @Veykril diff --git a/assets/badge/v0.json b/assets/badge/v0.json deleted file mode 100644 index c7d18bb42b..0000000000 --- a/assets/badge/v0.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "label": "", - "message": "Zed", - "logoSvg": "", - "logoWidth": 16, - "labelColor": "black", - "color": "white" -} diff --git a/assets/icons/LICENSES b/assets/icons/LICENSES deleted file mode 100644 index 7a2fc3b863..0000000000 --- a/assets/icons/LICENSES +++ /dev/null @@ -1,9 +0,0 @@ -Lucide License - -ISC License - -Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022. - -Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/assets/icons/ai.svg b/assets/icons/ai.svg deleted file mode 100644 index 4236d50337..0000000000 --- a/assets/icons/ai.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/ai_anthropic.svg b/assets/icons/ai_anthropic.svg deleted file mode 100644 index 12d731fb0b..0000000000 --- a/assets/icons/ai_anthropic.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/ai_bedrock.svg b/assets/icons/ai_bedrock.svg deleted file mode 100644 index c9bbcc82e1..0000000000 --- a/assets/icons/ai_bedrock.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/assets/icons/ai_claude.svg b/assets/icons/ai_claude.svg deleted file mode 100644 index a3e3e1f4cd..0000000000 --- a/assets/icons/ai_claude.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/ai_deep_seek.svg b/assets/icons/ai_deep_seek.svg deleted file mode 100644 index c8e5483fb3..0000000000 --- a/assets/icons/ai_deep_seek.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/ai_edit.svg b/assets/icons/ai_edit.svg deleted file mode 100644 index 2f93ab9fd9..0000000000 --- a/assets/icons/ai_edit.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/ai_gemini.svg b/assets/icons/ai_gemini.svg deleted file mode 100644 index bdde44ed24..0000000000 --- a/assets/icons/ai_gemini.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/ai_google.svg b/assets/icons/ai_google.svg deleted file mode 100644 index de28cf82cb..0000000000 --- a/assets/icons/ai_google.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/ai_lm_studio.svg b/assets/icons/ai_lm_studio.svg deleted file mode 100644 index 5cfdeb5578..0000000000 --- a/assets/icons/ai_lm_studio.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/assets/icons/ai_mistral.svg b/assets/icons/ai_mistral.svg deleted file mode 100644 index f11c177e2f..0000000000 --- a/assets/icons/ai_mistral.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/assets/icons/ai_ollama.svg b/assets/icons/ai_ollama.svg deleted file mode 100644 index 36a88c1ad6..0000000000 --- a/assets/icons/ai_ollama.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/ai_open_ai.svg b/assets/icons/ai_open_ai.svg deleted file mode 100644 index e45ac315a0..0000000000 --- a/assets/icons/ai_open_ai.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/ai_open_ai_compat.svg b/assets/icons/ai_open_ai_compat.svg deleted file mode 100644 index f6557caac3..0000000000 --- a/assets/icons/ai_open_ai_compat.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/ai_open_router.svg b/assets/icons/ai_open_router.svg deleted file mode 100644 index b6f5164e0b..0000000000 --- a/assets/icons/ai_open_router.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/assets/icons/ai_v_zero.svg b/assets/icons/ai_v_zero.svg deleted file mode 100644 index 26d09ea26a..0000000000 --- a/assets/icons/ai_v_zero.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/assets/icons/ai_x_ai.svg b/assets/icons/ai_x_ai.svg deleted file mode 100644 index d3400fbe9c..0000000000 --- a/assets/icons/ai_x_ai.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/ai_zed.svg b/assets/icons/ai_zed.svg deleted file mode 100644 index 6d78efacd5..0000000000 --- a/assets/icons/ai_zed.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/arrow_circle.svg b/assets/icons/arrow_circle.svg deleted file mode 100644 index cdfa939795..0000000000 --- a/assets/icons/arrow_circle.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/arrow_down.svg b/assets/icons/arrow_down.svg deleted file mode 100644 index 60e6584c45..0000000000 --- a/assets/icons/arrow_down.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/arrow_down10.svg b/assets/icons/arrow_down10.svg deleted file mode 100644 index 5933b758d9..0000000000 --- a/assets/icons/arrow_down10.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/arrow_down_right.svg b/assets/icons/arrow_down_right.svg deleted file mode 100644 index ebdb06d77b..0000000000 --- a/assets/icons/arrow_down_right.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/arrow_left.svg b/assets/icons/arrow_left.svg deleted file mode 100644 index f7eacb2a77..0000000000 --- a/assets/icons/arrow_left.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/arrow_right.svg b/assets/icons/arrow_right.svg deleted file mode 100644 index b9324af5a2..0000000000 --- a/assets/icons/arrow_right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/arrow_right_left.svg b/assets/icons/arrow_right_left.svg deleted file mode 100644 index 2c1211056a..0000000000 --- a/assets/icons/arrow_right_left.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/arrow_up.svg b/assets/icons/arrow_up.svg deleted file mode 100644 index ff3ad44123..0000000000 --- a/assets/icons/arrow_up.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/arrow_up_right.svg b/assets/icons/arrow_up_right.svg deleted file mode 100644 index a948ef8f81..0000000000 --- a/assets/icons/arrow_up_right.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/at_sign.svg b/assets/icons/at_sign.svg deleted file mode 100644 index 531c10c8dc..0000000000 --- a/assets/icons/at_sign.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - 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 deleted file mode 100644 index 43d2a04344..0000000000 --- a/assets/icons/audio_off.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/audio_on.svg b/assets/icons/audio_on.svg deleted file mode 100644 index 6e183bd585..0000000000 --- a/assets/icons/audio_on.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/backspace.svg b/assets/icons/backspace.svg deleted file mode 100644 index 9ef4432b6f..0000000000 --- a/assets/icons/backspace.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/bell.svg b/assets/icons/bell.svg deleted file mode 100644 index 70225bb105..0000000000 --- a/assets/icons/bell.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/bell_dot.svg b/assets/icons/bell_dot.svg deleted file mode 100644 index 959a7773cf..0000000000 --- a/assets/icons/bell_dot.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/bell_off.svg b/assets/icons/bell_off.svg deleted file mode 100644 index 5c3c1a0d68..0000000000 --- a/assets/icons/bell_off.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/bell_ring.svg b/assets/icons/bell_ring.svg deleted file mode 100644 index 838056cc03..0000000000 --- a/assets/icons/bell_ring.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/binary.svg b/assets/icons/binary.svg deleted file mode 100644 index 3c15e9b547..0000000000 --- a/assets/icons/binary.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/blocks.svg b/assets/icons/blocks.svg deleted file mode 100644 index 84725d7892..0000000000 --- a/assets/icons/blocks.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/bolt_filled.svg b/assets/icons/bolt_filled.svg deleted file mode 100644 index 14d8f53e02..0000000000 --- a/assets/icons/bolt_filled.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - 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 deleted file mode 100644 index a2ab394be4..0000000000 --- a/assets/icons/book.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/book_copy.svg b/assets/icons/book_copy.svg deleted file mode 100644 index b7afd1df5c..0000000000 --- a/assets/icons/book_copy.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/box.svg b/assets/icons/box.svg deleted file mode 100644 index 7e1276c629..0000000000 --- a/assets/icons/box.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/case_sensitive.svg b/assets/icons/case_sensitive.svg deleted file mode 100644 index 015e241416..0000000000 --- a/assets/icons/case_sensitive.svg +++ /dev/null @@ -1 +0,0 @@ - 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 deleted file mode 100644 index 21e2137965..0000000000 --- a/assets/icons/check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/check_circle.svg b/assets/icons/check_circle.svg deleted file mode 100644 index f9b88c4ce1..0000000000 --- a/assets/icons/check_circle.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/check_double.svg b/assets/icons/check_double.svg deleted file mode 100644 index fabc700520..0000000000 --- a/assets/icons/check_double.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/chevron_down.svg b/assets/icons/chevron_down.svg deleted file mode 100644 index e4ca142a91..0000000000 --- a/assets/icons/chevron_down.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_down_up.svg b/assets/icons/chevron_down_up.svg deleted file mode 100644 index 340b8d1ad9..0000000000 --- a/assets/icons/chevron_down_up.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/chevron_left.svg b/assets/icons/chevron_left.svg deleted file mode 100644 index fbe438fd4b..0000000000 --- a/assets/icons/chevron_left.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_right.svg b/assets/icons/chevron_right.svg deleted file mode 100644 index 4f170717c9..0000000000 --- a/assets/icons/chevron_right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_up.svg b/assets/icons/chevron_up.svg deleted file mode 100644 index bbe6b9762d..0000000000 --- a/assets/icons/chevron_up.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_up_down.svg b/assets/icons/chevron_up_down.svg deleted file mode 100644 index 299f6bce5a..0000000000 --- a/assets/icons/chevron_up_down.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/circle.svg b/assets/icons/circle.svg deleted file mode 100644 index 1d80edac09..0000000000 --- a/assets/icons/circle.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/circle_check.svg b/assets/icons/circle_check.svg deleted file mode 100644 index 8950aa7a0e..0000000000 --- a/assets/icons/circle_check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/circle_help.svg b/assets/icons/circle_help.svg deleted file mode 100644 index 0e623bd1da..0000000000 --- a/assets/icons/circle_help.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/close.svg b/assets/icons/close.svg deleted file mode 100644 index 846b3a703d..0000000000 --- a/assets/icons/close.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/cloud_download.svg b/assets/icons/cloud_download.svg deleted file mode 100644 index 70cda55856..0000000000 --- a/assets/icons/cloud_download.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/code.svg b/assets/icons/code.svg deleted file mode 100644 index 72d145224a..0000000000 --- a/assets/icons/code.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/cog.svg b/assets/icons/cog.svg deleted file mode 100644 index 7dd3a8beff..0000000000 --- a/assets/icons/cog.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/command.svg b/assets/icons/command.svg deleted file mode 100644 index f361ca2d05..0000000000 --- a/assets/icons/command.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/control.svg b/assets/icons/control.svg deleted file mode 100644 index f9341b6256..0000000000 --- a/assets/icons/control.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/copilot.svg b/assets/icons/copilot.svg deleted file mode 100644 index 2584cd6310..0000000000 --- a/assets/icons/copilot.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/assets/icons/copilot_disabled.svg b/assets/icons/copilot_disabled.svg deleted file mode 100644 index 90afa84966..0000000000 --- a/assets/icons/copilot_disabled.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/assets/icons/copilot_error.svg b/assets/icons/copilot_error.svg deleted file mode 100644 index 77744e7529..0000000000 --- a/assets/icons/copilot_error.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/copilot_init.svg b/assets/icons/copilot_init.svg deleted file mode 100644 index 754d159584..0000000000 --- a/assets/icons/copilot_init.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg deleted file mode 100644 index aba193930b..0000000000 --- a/assets/icons/copy.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/countdown_timer.svg b/assets/icons/countdown_timer.svg deleted file mode 100644 index 5d1e775e68..0000000000 --- a/assets/icons/countdown_timer.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/crosshair.svg b/assets/icons/crosshair.svg deleted file mode 100644 index 3af6aa9fa3..0000000000 --- a/assets/icons/crosshair.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/cursor_i_beam.svg b/assets/icons/cursor_i_beam.svg deleted file mode 100644 index 2d513181f9..0000000000 --- a/assets/icons/cursor_i_beam.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/dash.svg b/assets/icons/dash.svg deleted file mode 100644 index 3928ee7cfa..0000000000 --- a/assets/icons/dash.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/database_zap.svg b/assets/icons/database_zap.svg deleted file mode 100644 index 76af0f9251..0000000000 --- a/assets/icons/database_zap.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/debug.svg b/assets/icons/debug.svg deleted file mode 100644 index 6423a2b090..0000000000 --- a/assets/icons/debug.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/assets/icons/debug_breakpoint.svg b/assets/icons/debug_breakpoint.svg deleted file mode 100644 index c09a3c159f..0000000000 --- a/assets/icons/debug_breakpoint.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/debug_continue.svg b/assets/icons/debug_continue.svg deleted file mode 100644 index f03a8b2364..0000000000 --- a/assets/icons/debug_continue.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/debug_detach.svg b/assets/icons/debug_detach.svg deleted file mode 100644 index 8b34845571..0000000000 --- a/assets/icons/debug_detach.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/debug_disabled_breakpoint.svg b/assets/icons/debug_disabled_breakpoint.svg deleted file mode 100644 index 9a7c896f47..0000000000 --- a/assets/icons/debug_disabled_breakpoint.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/debug_disabled_log_breakpoint.svg b/assets/icons/debug_disabled_log_breakpoint.svg deleted file mode 100644 index f477f4f32d..0000000000 --- a/assets/icons/debug_disabled_log_breakpoint.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/debug_ignore_breakpoints.svg b/assets/icons/debug_ignore_breakpoints.svg deleted file mode 100644 index bc95329c7a..0000000000 --- a/assets/icons/debug_ignore_breakpoints.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/debug_log_breakpoint.svg b/assets/icons/debug_log_breakpoint.svg deleted file mode 100644 index 22eae9d029..0000000000 --- a/assets/icons/debug_log_breakpoint.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/debug_pause.svg b/assets/icons/debug_pause.svg deleted file mode 100644 index 65e1949581..0000000000 --- a/assets/icons/debug_pause.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/debug_step_into.svg b/assets/icons/debug_step_into.svg deleted file mode 100644 index 0a58823543..0000000000 --- a/assets/icons/debug_step_into.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/debug_step_out.svg b/assets/icons/debug_step_out.svg deleted file mode 100644 index c128f56111..0000000000 --- a/assets/icons/debug_step_out.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/debug_step_over.svg b/assets/icons/debug_step_over.svg deleted file mode 100644 index 5d8ccd5b7a..0000000000 --- a/assets/icons/debug_step_over.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/diff.svg b/assets/icons/diff.svg deleted file mode 100644 index 9d93b2d5b4..0000000000 --- a/assets/icons/diff.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/disconnected.svg b/assets/icons/disconnected.svg deleted file mode 100644 index 47bd1db478..0000000000 --- a/assets/icons/disconnected.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/download.svg b/assets/icons/download.svg deleted file mode 100644 index 6c105d3fd7..0000000000 --- a/assets/icons/download.svg +++ /dev/null @@ -1 +0,0 @@ - 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 e20013917d..0000000000 --- a/assets/icons/editor_cursor.svg +++ /dev/null @@ -1,3 +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 deleted file mode 100644 index 22b5a8fd46..0000000000 --- a/assets/icons/ellipsis.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/ellipsis_vertical.svg b/assets/icons/ellipsis_vertical.svg deleted file mode 100644 index c38437667e..0000000000 --- a/assets/icons/ellipsis_vertical.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/envelope.svg b/assets/icons/envelope.svg deleted file mode 100644 index 273cc6de26..0000000000 --- a/assets/icons/envelope.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/eraser.svg b/assets/icons/eraser.svg deleted file mode 100644 index ca6209785f..0000000000 --- a/assets/icons/eraser.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/escape.svg b/assets/icons/escape.svg deleted file mode 100644 index 1898588a67..0000000000 --- a/assets/icons/escape.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/exit.svg b/assets/icons/exit.svg deleted file mode 100644 index 3619a55c87..0000000000 --- a/assets/icons/exit.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/expand_down.svg b/assets/icons/expand_down.svg deleted file mode 100644 index 9f85ee6720..0000000000 --- a/assets/icons/expand_down.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/expand_up.svg b/assets/icons/expand_up.svg deleted file mode 100644 index 49b084fa8f..0000000000 --- a/assets/icons/expand_up.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/expand_vertical.svg b/assets/icons/expand_vertical.svg deleted file mode 100644 index 5a5fa8ccb5..0000000000 --- a/assets/icons/expand_vertical.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/eye.svg b/assets/icons/eye.svg deleted file mode 100644 index 327fa751e9..0000000000 --- a/assets/icons/eye.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file.svg b/assets/icons/file.svg deleted file mode 100644 index 60cf2537d9..0000000000 --- a/assets/icons/file.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/file_code.svg b/assets/icons/file_code.svg deleted file mode 100644 index 548d5a153b..0000000000 --- a/assets/icons/file_code.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/file_diff.svg b/assets/icons/file_diff.svg deleted file mode 100644 index 193dd7392f..0000000000 --- a/assets/icons/file_diff.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/file_doc.svg b/assets/icons/file_doc.svg deleted file mode 100644 index ccd5eeea01..0000000000 --- a/assets/icons/file_doc.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/file_generic.svg b/assets/icons/file_generic.svg deleted file mode 100644 index 790a5f18d7..0000000000 --- a/assets/icons/file_generic.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/file_git.svg b/assets/icons/file_git.svg deleted file mode 100644 index 2b36b0ffd3..0000000000 --- a/assets/icons/file_git.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/file_icons/ai.svg b/assets/icons/file_icons/ai.svg deleted file mode 100644 index 4236d50337..0000000000 --- a/assets/icons/file_icons/ai.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/file_icons/archive.svg b/assets/icons/file_icons/archive.svg deleted file mode 100644 index fd3780164d..0000000000 --- a/assets/icons/file_icons/archive.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/astro.svg b/assets/icons/file_icons/astro.svg deleted file mode 100644 index 0b95d64d92..0000000000 --- a/assets/icons/file_icons/astro.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/audio.svg b/assets/icons/file_icons/audio.svg deleted file mode 100644 index 7948b04616..0000000000 --- a/assets/icons/file_icons/audio.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/assets/icons/file_icons/book.svg b/assets/icons/file_icons/book.svg deleted file mode 100644 index ccd5eeea01..0000000000 --- a/assets/icons/file_icons/book.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/file_icons/bun.svg b/assets/icons/file_icons/bun.svg deleted file mode 100644 index ca1ec900bc..0000000000 --- a/assets/icons/file_icons/bun.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/assets/icons/file_icons/c.svg b/assets/icons/file_icons/c.svg deleted file mode 100644 index dab784fef1..0000000000 --- a/assets/icons/file_icons/c.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/file_icons/cairo.svg b/assets/icons/file_icons/cairo.svg deleted file mode 100644 index dcf77c6fbf..0000000000 --- a/assets/icons/file_icons/cairo.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/camera.svg b/assets/icons/file_icons/camera.svg deleted file mode 100644 index b040935583..0000000000 --- a/assets/icons/file_icons/camera.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/chevron_down.svg b/assets/icons/file_icons/chevron_down.svg deleted file mode 100644 index 9918f6c9f7..0000000000 --- a/assets/icons/file_icons/chevron_down.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/chevron_left.svg b/assets/icons/file_icons/chevron_left.svg deleted file mode 100644 index 3299ee7168..0000000000 --- a/assets/icons/file_icons/chevron_left.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/chevron_right.svg b/assets/icons/file_icons/chevron_right.svg deleted file mode 100644 index 140f644127..0000000000 --- a/assets/icons/file_icons/chevron_right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/chevron_up.svg b/assets/icons/file_icons/chevron_up.svg deleted file mode 100644 index ae8c12a989..0000000000 --- a/assets/icons/file_icons/chevron_up.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/code.svg b/assets/icons/file_icons/code.svg deleted file mode 100644 index af2f6c5dc0..0000000000 --- a/assets/icons/file_icons/code.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/coffeescript.svg b/assets/icons/file_icons/coffeescript.svg deleted file mode 100644 index e91d187615..0000000000 --- a/assets/icons/file_icons/coffeescript.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/file_icons/conversations.svg b/assets/icons/file_icons/conversations.svg deleted file mode 100644 index e25ed973ef..0000000000 --- a/assets/icons/file_icons/conversations.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/cpp.svg b/assets/icons/file_icons/cpp.svg deleted file mode 100644 index e3385c1577..0000000000 --- a/assets/icons/file_icons/cpp.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/file_icons/css.svg b/assets/icons/file_icons/css.svg deleted file mode 100644 index 47457a95c8..0000000000 --- a/assets/icons/file_icons/css.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/dart.svg b/assets/icons/file_icons/dart.svg deleted file mode 100644 index c9ec3de51a..0000000000 --- a/assets/icons/file_icons/dart.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/file_icons/database.svg b/assets/icons/file_icons/database.svg deleted file mode 100644 index a8226110d3..0000000000 --- a/assets/icons/file_icons/database.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/file_icons/diff.svg b/assets/icons/file_icons/diff.svg deleted file mode 100644 index ec59a0aabe..0000000000 --- a/assets/icons/file_icons/diff.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/file_icons/docker.svg b/assets/icons/file_icons/docker.svg deleted file mode 100644 index 7c9cb3d888..0000000000 --- a/assets/icons/file_icons/docker.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/assets/icons/file_icons/elixir.svg b/assets/icons/file_icons/elixir.svg deleted file mode 100644 index fdfd2b0c6e..0000000000 --- a/assets/icons/file_icons/elixir.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/elm.svg b/assets/icons/file_icons/elm.svg deleted file mode 100644 index ff63582e98..0000000000 --- a/assets/icons/file_icons/elm.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/assets/icons/file_icons/erlang.svg b/assets/icons/file_icons/erlang.svg deleted file mode 100644 index 9c937d3c66..0000000000 --- a/assets/icons/file_icons/erlang.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/eslint.svg b/assets/icons/file_icons/eslint.svg deleted file mode 100644 index ba72d9166b..0000000000 --- a/assets/icons/file_icons/eslint.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/file.svg b/assets/icons/file_icons/file.svg deleted file mode 100644 index 790a5f18d7..0000000000 --- a/assets/icons/file_icons/file.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/file_icons/folder.svg b/assets/icons/file_icons/folder.svg deleted file mode 100644 index e40613000d..0000000000 --- a/assets/icons/file_icons/folder.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/folder_open.svg b/assets/icons/file_icons/folder_open.svg deleted file mode 100644 index 55231fb6ab..0000000000 --- a/assets/icons/file_icons/folder_open.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/font.svg b/assets/icons/file_icons/font.svg deleted file mode 100644 index 6f2b734b26..0000000000 --- a/assets/icons/file_icons/font.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/fsharp.svg b/assets/icons/file_icons/fsharp.svg deleted file mode 100644 index 9dd7c153a7..0000000000 --- a/assets/icons/file_icons/fsharp.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/file_icons/git.svg b/assets/icons/file_icons/git.svg deleted file mode 100644 index 2b36b0ffd3..0000000000 --- a/assets/icons/file_icons/git.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/file_icons/gleam.svg b/assets/icons/file_icons/gleam.svg deleted file mode 100644 index 0399bb4dd2..0000000000 --- a/assets/icons/file_icons/gleam.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/file_icons/go.svg b/assets/icons/file_icons/go.svg deleted file mode 100644 index 756dd2c105..0000000000 --- a/assets/icons/file_icons/go.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/graphql.svg b/assets/icons/file_icons/graphql.svg deleted file mode 100644 index e6c0368182..0000000000 --- a/assets/icons/file_icons/graphql.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/file_icons/hash.svg b/assets/icons/file_icons/hash.svg deleted file mode 100644 index 77e6c60072..0000000000 --- a/assets/icons/file_icons/hash.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/file_icons/haskell.svg b/assets/icons/file_icons/haskell.svg deleted file mode 100644 index f7519dce23..0000000000 --- a/assets/icons/file_icons/haskell.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/file_icons/hcl.svg b/assets/icons/file_icons/hcl.svg deleted file mode 100644 index 71799701af..0000000000 --- a/assets/icons/file_icons/hcl.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/heroku.svg b/assets/icons/file_icons/heroku.svg deleted file mode 100644 index 732adf72cb..0000000000 --- a/assets/icons/file_icons/heroku.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/file_icons/html.svg b/assets/icons/file_icons/html.svg deleted file mode 100644 index 8832bcba3a..0000000000 --- a/assets/icons/file_icons/html.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/file_icons/image.svg b/assets/icons/file_icons/image.svg deleted file mode 100644 index c89de1b128..0000000000 --- a/assets/icons/file_icons/image.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/file_icons/info.svg b/assets/icons/file_icons/info.svg deleted file mode 100644 index 5d9bef7de8..0000000000 --- a/assets/icons/file_icons/info.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/java.svg b/assets/icons/file_icons/java.svg deleted file mode 100644 index 70d2d10ed7..0000000000 --- a/assets/icons/file_icons/java.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/file_icons/javascript.svg b/assets/icons/file_icons/javascript.svg deleted file mode 100644 index c2aa1cf340..0000000000 --- a/assets/icons/file_icons/javascript.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/julia.svg b/assets/icons/file_icons/julia.svg deleted file mode 100644 index f37f7d816a..0000000000 --- a/assets/icons/file_icons/julia.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - 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/kotlin.svg b/assets/icons/file_icons/kotlin.svg deleted file mode 100644 index 5d70c99b45..0000000000 --- a/assets/icons/file_icons/kotlin.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/lock.svg b/assets/icons/file_icons/lock.svg deleted file mode 100644 index 10ae33869a..0000000000 --- a/assets/icons/file_icons/lock.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/lua.svg b/assets/icons/file_icons/lua.svg deleted file mode 100644 index 6035c438c1..0000000000 --- a/assets/icons/file_icons/lua.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/file_icons/luau.svg b/assets/icons/file_icons/luau.svg deleted file mode 100644 index 08956e127f..0000000000 --- a/assets/icons/file_icons/luau.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/magnifying_glass.svg b/assets/icons/file_icons/magnifying_glass.svg deleted file mode 100644 index d0440d905c..0000000000 --- a/assets/icons/file_icons/magnifying_glass.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/metal.svg b/assets/icons/file_icons/metal.svg deleted file mode 100644 index 7f5396b93b..0000000000 --- a/assets/icons/file_icons/metal.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/file_icons/nim.svg b/assets/icons/file_icons/nim.svg deleted file mode 100644 index 1750bbff77..0000000000 --- a/assets/icons/file_icons/nim.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/nix.svg b/assets/icons/file_icons/nix.svg deleted file mode 100644 index 215d58a035..0000000000 --- a/assets/icons/file_icons/nix.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/assets/icons/file_icons/notebook.svg b/assets/icons/file_icons/notebook.svg deleted file mode 100644 index 968d5c5982..0000000000 --- a/assets/icons/file_icons/notebook.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/assets/icons/file_icons/ocaml.svg b/assets/icons/file_icons/ocaml.svg deleted file mode 100644 index 7d59015d8d..0000000000 --- a/assets/icons/file_icons/ocaml.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/odin.svg b/assets/icons/file_icons/odin.svg deleted file mode 100644 index 3b4ef89319..0000000000 --- a/assets/icons/file_icons/odin.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/file_icons/package.svg b/assets/icons/file_icons/package.svg deleted file mode 100644 index 16bbccb2e6..0000000000 --- a/assets/icons/file_icons/package.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/phoenix.svg b/assets/icons/file_icons/phoenix.svg deleted file mode 100644 index 5db68b4e44..0000000000 --- a/assets/icons/file_icons/phoenix.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/php.svg b/assets/icons/file_icons/php.svg deleted file mode 100644 index 2f26ad7706..0000000000 --- a/assets/icons/file_icons/php.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/file_icons/plus.svg b/assets/icons/file_icons/plus.svg deleted file mode 100644 index 3449da3ecd..0000000000 --- a/assets/icons/file_icons/plus.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/prettier.svg b/assets/icons/file_icons/prettier.svg deleted file mode 100644 index f01230c33c..0000000000 --- a/assets/icons/file_icons/prettier.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/assets/icons/file_icons/prisma.svg b/assets/icons/file_icons/prisma.svg deleted file mode 100644 index 2c7349da8b..0000000000 --- a/assets/icons/file_icons/prisma.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/project.svg b/assets/icons/file_icons/project.svg deleted file mode 100644 index 509cc5f4d0..0000000000 --- a/assets/icons/file_icons/project.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - 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 deleted file mode 100644 index b44fdc539d..0000000000 --- a/assets/icons/file_icons/python.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/file_icons/r.svg b/assets/icons/file_icons/r.svg deleted file mode 100644 index 903b0519ca..0000000000 --- a/assets/icons/file_icons/r.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/react.svg b/assets/icons/file_icons/react.svg deleted file mode 100644 index c4c9238584..0000000000 --- a/assets/icons/file_icons/react.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/replace.svg b/assets/icons/file_icons/replace.svg deleted file mode 100644 index 287328e82e..0000000000 --- a/assets/icons/file_icons/replace.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/file_icons/replace_all.svg b/assets/icons/file_icons/replace_all.svg deleted file mode 100644 index d3cf503e32..0000000000 --- a/assets/icons/file_icons/replace_all.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/replace_next.svg b/assets/icons/file_icons/replace_next.svg deleted file mode 100644 index a9a9fc91f5..0000000000 --- a/assets/icons/file_icons/replace_next.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/roc.svg b/assets/icons/file_icons/roc.svg deleted file mode 100644 index c09a5dde7b..0000000000 --- a/assets/icons/file_icons/roc.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/file_icons/ruby.svg b/assets/icons/file_icons/ruby.svg deleted file mode 100644 index cd30f83286..0000000000 --- a/assets/icons/file_icons/ruby.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/file_icons/rust.svg b/assets/icons/file_icons/rust.svg deleted file mode 100644 index 9e4dc57adb..0000000000 --- a/assets/icons/file_icons/rust.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/sass.svg b/assets/icons/file_icons/sass.svg deleted file mode 100644 index 57bb32b098..0000000000 --- a/assets/icons/file_icons/sass.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/scala.svg b/assets/icons/file_icons/scala.svg deleted file mode 100644 index 0884cc96f4..0000000000 --- a/assets/icons/file_icons/scala.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/file_icons/settings.svg b/assets/icons/file_icons/settings.svg deleted file mode 100644 index d308135ff1..0000000000 --- a/assets/icons/file_icons/settings.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/surrealql.svg b/assets/icons/file_icons/surrealql.svg deleted file mode 100644 index 076f93e808..0000000000 --- a/assets/icons/file_icons/surrealql.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/swift.svg b/assets/icons/file_icons/swift.svg deleted file mode 100644 index 69745f0e76..0000000000 --- a/assets/icons/file_icons/swift.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/tcl.svg b/assets/icons/file_icons/tcl.svg deleted file mode 100644 index 1bd7c4a551..0000000000 --- a/assets/icons/file_icons/tcl.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/terminal.svg b/assets/icons/file_icons/terminal.svg deleted file mode 100644 index d3742fa11a..0000000000 --- a/assets/icons/file_icons/terminal.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/terraform.svg b/assets/icons/file_icons/terraform.svg deleted file mode 100644 index 47bdc0f65f..0000000000 --- a/assets/icons/file_icons/terraform.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/file_icons/toml.svg b/assets/icons/file_icons/toml.svg deleted file mode 100644 index ae31911d6a..0000000000 --- a/assets/icons/file_icons/toml.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/file_icons/typescript.svg b/assets/icons/file_icons/typescript.svg deleted file mode 100644 index e317743fea..0000000000 --- a/assets/icons/file_icons/typescript.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_icons/v.svg b/assets/icons/file_icons/v.svg deleted file mode 100644 index 485e27a378..0000000000 --- a/assets/icons/file_icons/v.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/video.svg b/assets/icons/file_icons/video.svg deleted file mode 100644 index c249d4c82b..0000000000 --- a/assets/icons/file_icons/video.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/vue.svg b/assets/icons/file_icons/vue.svg deleted file mode 100644 index 1f993e90ef..0000000000 --- a/assets/icons/file_icons/vue.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_icons/vyper.svg b/assets/icons/file_icons/vyper.svg deleted file mode 100644 index 73fb85deff..0000000000 --- a/assets/icons/file_icons/vyper.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/icons/file_icons/wgsl.svg b/assets/icons/file_icons/wgsl.svg deleted file mode 100644 index cbdd43d222..0000000000 --- a/assets/icons/file_icons/wgsl.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/file_icons/zig.svg b/assets/icons/file_icons/zig.svg deleted file mode 100644 index af35d5997b..0000000000 --- a/assets/icons/file_icons/zig.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/file_lock.svg b/assets/icons/file_lock.svg deleted file mode 100644 index 10ae33869a..0000000000 --- a/assets/icons/file_lock.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - 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 deleted file mode 100644 index 9e4dc57adb..0000000000 --- a/assets/icons/file_rust.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - 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 deleted file mode 100644 index ae31911d6a..0000000000 --- a/assets/icons/file_toml.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/file_tree.svg b/assets/icons/file_tree.svg deleted file mode 100644 index baf0e26ce6..0000000000 --- a/assets/icons/file_tree.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/filter.svg b/assets/icons/filter.svg deleted file mode 100644 index 4aa14e93c0..0000000000 --- a/assets/icons/filter.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/flame.svg b/assets/icons/flame.svg deleted file mode 100644 index 89fc6cab1e..0000000000 --- a/assets/icons/flame.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/folder.svg b/assets/icons/folder.svg deleted file mode 100644 index 35f4c1f8ac..0000000000 --- a/assets/icons/folder.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/folder_open.svg b/assets/icons/folder_open.svg deleted file mode 100644 index 55231fb6ab..0000000000 --- a/assets/icons/folder_open.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - 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/font.svg b/assets/icons/font.svg deleted file mode 100644 index 47633a58c9..0000000000 --- a/assets/icons/font.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/font_size.svg b/assets/icons/font_size.svg deleted file mode 100644 index 4286277bd9..0000000000 --- a/assets/icons/font_size.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/font_weight.svg b/assets/icons/font_weight.svg deleted file mode 100644 index 410f43ec6e..0000000000 --- a/assets/icons/font_weight.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/forward_arrow.svg b/assets/icons/forward_arrow.svg deleted file mode 100644 index e51796e554..0000000000 --- a/assets/icons/forward_arrow.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/generic_close.svg b/assets/icons/generic_close.svg deleted file mode 100644 index 0fd213daf9..0000000000 --- a/assets/icons/generic_close.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/generic_maximize.svg b/assets/icons/generic_maximize.svg deleted file mode 100644 index f1d7da44ef..0000000000 --- a/assets/icons/generic_maximize.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/generic_minimize.svg b/assets/icons/generic_minimize.svg deleted file mode 100644 index 4b43cde274..0000000000 --- a/assets/icons/generic_minimize.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/generic_restore.svg b/assets/icons/generic_restore.svg deleted file mode 100644 index d8a3d72bcd..0000000000 --- a/assets/icons/generic_restore.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/git_branch.svg b/assets/icons/git_branch.svg deleted file mode 100644 index fc6dcfe1b2..0000000000 --- a/assets/icons/git_branch.svg +++ /dev/null @@ -1 +0,0 @@ - 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_plus.svg b/assets/icons/git_branch_plus.svg deleted file mode 100644 index cf60ce66b4..0000000000 --- a/assets/icons/git_branch_plus.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/assets/icons/github.svg b/assets/icons/github.svg deleted file mode 100644 index 0a12c9b656..0000000000 --- a/assets/icons/github.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/hash.svg b/assets/icons/hash.svg deleted file mode 100644 index afc1f9c0b5..0000000000 --- a/assets/icons/hash.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/history_rerun.svg b/assets/icons/history_rerun.svg deleted file mode 100644 index e11e754318..0000000000 --- a/assets/icons/history_rerun.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/image.svg b/assets/icons/image.svg deleted file mode 100644 index e0d73d7621..0000000000 --- a/assets/icons/image.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/inception.svg b/assets/icons/inception.svg deleted file mode 100644 index 77a96c0b39..0000000000 --- a/assets/icons/inception.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/indicator.svg b/assets/icons/indicator.svg deleted file mode 100644 index 40f9151fd5..0000000000 --- a/assets/icons/indicator.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/info.svg b/assets/icons/info.svg deleted file mode 100644 index c000f25867..0000000000 --- a/assets/icons/info.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - 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 deleted file mode 100644 index 82791cda3f..0000000000 --- a/assets/icons/keyboard.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/knockouts/dot_bg.svg b/assets/icons/knockouts/dot_bg.svg deleted file mode 100644 index 9f5ba034e2..0000000000 --- a/assets/icons/knockouts/dot_bg.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/knockouts/dot_fg.svg b/assets/icons/knockouts/dot_fg.svg deleted file mode 100644 index 54eaacbfa9..0000000000 --- a/assets/icons/knockouts/dot_fg.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/knockouts/triangle_bg.svg b/assets/icons/knockouts/triangle_bg.svg deleted file mode 100644 index 990b439952..0000000000 --- a/assets/icons/knockouts/triangle_bg.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/knockouts/triangle_fg.svg b/assets/icons/knockouts/triangle_fg.svg deleted file mode 100644 index e3b31446b3..0000000000 --- a/assets/icons/knockouts/triangle_fg.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/knockouts/x_bg.svg b/assets/icons/knockouts/x_bg.svg deleted file mode 100644 index 0bc5059e73..0000000000 --- a/assets/icons/knockouts/x_bg.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/knockouts/x_fg.svg b/assets/icons/knockouts/x_fg.svg deleted file mode 100644 index f459954f72..0000000000 --- a/assets/icons/knockouts/x_fg.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/library.svg b/assets/icons/library.svg deleted file mode 100644 index fc7f5afcd2..0000000000 --- a/assets/icons/library.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/line_height.svg b/assets/icons/line_height.svg deleted file mode 100644 index 3929fc4080..0000000000 --- a/assets/icons/line_height.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/link.svg b/assets/icons/link.svg deleted file mode 100644 index 739d41b231..0000000000 --- a/assets/icons/link.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/linux.svg b/assets/icons/linux.svg deleted file mode 100644 index fc76742a3f..0000000000 --- a/assets/icons/linux.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/list_collapse.svg b/assets/icons/list_collapse.svg deleted file mode 100644 index f18bc550b9..0000000000 --- a/assets/icons/list_collapse.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/list_filter.svg b/assets/icons/list_filter.svg deleted file mode 100644 index 82f41f5f68..0000000000 --- a/assets/icons/list_filter.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/list_todo.svg b/assets/icons/list_todo.svg deleted file mode 100644 index 709f26d89d..0000000000 --- a/assets/icons/list_todo.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/list_tree.svg b/assets/icons/list_tree.svg deleted file mode 100644 index de3e0f3a57..0000000000 --- a/assets/icons/list_tree.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/list_x.svg b/assets/icons/list_x.svg deleted file mode 100644 index 0fa3bd68fb..0000000000 --- a/assets/icons/list_x.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/load_circle.svg b/assets/icons/load_circle.svg deleted file mode 100644 index eecf099310..0000000000 --- a/assets/icons/load_circle.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/location_edit.svg b/assets/icons/location_edit.svg deleted file mode 100644 index e342652eb1..0000000000 --- a/assets/icons/location_edit.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/lock_outlined.svg b/assets/icons/lock_outlined.svg deleted file mode 100644 index d69a245603..0000000000 --- a/assets/icons/lock_outlined.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/magnifying_glass.svg b/assets/icons/magnifying_glass.svg deleted file mode 100644 index 24f00bb51b..0000000000 --- a/assets/icons/magnifying_glass.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/maximize.svg b/assets/icons/maximize.svg deleted file mode 100644 index 7b6d26fed8..0000000000 --- a/assets/icons/maximize.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/menu.svg b/assets/icons/menu.svg deleted file mode 100644 index f12ce47f7e..0000000000 --- a/assets/icons/menu.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/menu_alt.svg b/assets/icons/menu_alt.svg deleted file mode 100644 index b9cc19e22f..0000000000 --- a/assets/icons/menu_alt.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - 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/mic.svg b/assets/icons/mic.svg deleted file mode 100644 index 000d135ea5..0000000000 --- a/assets/icons/mic.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/mic_mute.svg b/assets/icons/mic_mute.svg deleted file mode 100644 index 8bc63be610..0000000000 --- a/assets/icons/mic_mute.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/assets/icons/minimize.svg b/assets/icons/minimize.svg deleted file mode 100644 index 082ade47db..0000000000 --- a/assets/icons/minimize.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - 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 deleted file mode 100644 index 47201f7c67..0000000000 --- a/assets/icons/option.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/page_down.svg b/assets/icons/page_down.svg deleted file mode 100644 index 765f36b26a..0000000000 --- a/assets/icons/page_down.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/page_up.svg b/assets/icons/page_up.svg deleted file mode 100644 index f555165d2d..0000000000 --- a/assets/icons/page_up.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/assets/icons/paperclip.svg b/assets/icons/paperclip.svg deleted file mode 100644 index 7a864103c0..0000000000 --- a/assets/icons/paperclip.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/pencil.svg b/assets/icons/pencil.svg deleted file mode 100644 index c4d289e9c0..0000000000 --- a/assets/icons/pencil.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - 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 deleted file mode 100644 index a1c29e4acb..0000000000 --- a/assets/icons/person.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/pin.svg b/assets/icons/pin.svg deleted file mode 100644 index d23daff8b9..0000000000 --- a/assets/icons/pin.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/play_filled.svg b/assets/icons/play_filled.svg deleted file mode 100644 index 8075197ad2..0000000000 --- a/assets/icons/play_filled.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/play_outlined.svg b/assets/icons/play_outlined.svg deleted file mode 100644 index ba1ea2693d..0000000000 --- a/assets/icons/play_outlined.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg deleted file mode 100644 index 8ac57d8cdd..0000000000 --- a/assets/icons/plus.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/power.svg b/assets/icons/power.svg deleted file mode 100644 index 29bd2127c5..0000000000 --- a/assets/icons/power.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/public.svg b/assets/icons/public.svg deleted file mode 100644 index 5659b5419f..0000000000 --- a/assets/icons/public.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/pull_request.svg b/assets/icons/pull_request.svg deleted file mode 100644 index 515462ab64..0000000000 --- a/assets/icons/pull_request.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/quote.svg b/assets/icons/quote.svg deleted file mode 100644 index a958bc67f2..0000000000 --- a/assets/icons/quote.svg +++ /dev/null @@ -1 +0,0 @@ - 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 deleted file mode 100644 index c9e670bfab..0000000000 --- a/assets/icons/refresh_title.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/regex.svg b/assets/icons/regex.svg deleted file mode 100644 index 818c2ba360..0000000000 --- a/assets/icons/regex.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/repl_neutral.svg b/assets/icons/repl_neutral.svg deleted file mode 100644 index 2842e2c421..0000000000 --- a/assets/icons/repl_neutral.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/repl_off.svg b/assets/icons/repl_off.svg deleted file mode 100644 index 3018ceaf85..0000000000 --- a/assets/icons/repl_off.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/repl_pause.svg b/assets/icons/repl_pause.svg deleted file mode 100644 index 5a69a576c1..0000000000 --- a/assets/icons/repl_pause.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/assets/icons/repl_play.svg b/assets/icons/repl_play.svg deleted file mode 100644 index 0c8f4b0832..0000000000 --- a/assets/icons/repl_play.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/replace.svg b/assets/icons/replace.svg deleted file mode 100644 index 287328e82e..0000000000 --- a/assets/icons/replace.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/replace_all.svg b/assets/icons/replace_all.svg deleted file mode 100644 index d3cf503e32..0000000000 --- a/assets/icons/replace_all.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/replace_next.svg b/assets/icons/replace_next.svg deleted file mode 100644 index a9a9fc91f5..0000000000 --- a/assets/icons/replace_next.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/reply_arrow_right.svg b/assets/icons/reply_arrow_right.svg deleted file mode 100644 index d8321e8b3e..0000000000 --- a/assets/icons/reply_arrow_right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/rerun.svg b/assets/icons/rerun.svg deleted file mode 100644 index 1a03a01ae6..0000000000 --- a/assets/icons/rerun.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/return.svg b/assets/icons/return.svg deleted file mode 100644 index c605eb6512..0000000000 --- a/assets/icons/return.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/rotate_ccw.svg b/assets/icons/rotate_ccw.svg deleted file mode 100644 index cdfa8d0ab4..0000000000 --- a/assets/icons/rotate_ccw.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/rotate_cw.svg b/assets/icons/rotate_cw.svg deleted file mode 100644 index 2adfa7f972..0000000000 --- a/assets/icons/rotate_cw.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/scissors.svg b/assets/icons/scissors.svg deleted file mode 100644 index a19580bd89..0000000000 --- a/assets/icons/scissors.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/screen.svg b/assets/icons/screen.svg deleted file mode 100644 index 4bcdf19528..0000000000 --- a/assets/icons/screen.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/select_all.svg b/assets/icons/select_all.svg deleted file mode 100644 index 4fa17dcf63..0000000000 --- a/assets/icons/select_all.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/send.svg b/assets/icons/send.svg deleted file mode 100644 index 5ceeef2af4..0000000000 --- a/assets/icons/send.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/server.svg b/assets/icons/server.svg deleted file mode 100644 index 8d851d1328..0000000000 --- a/assets/icons/server.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/settings.svg b/assets/icons/settings.svg deleted file mode 100644 index 33ac74f230..0000000000 --- a/assets/icons/settings.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - 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 deleted file mode 100644 index c38807d8b0..0000000000 --- a/assets/icons/shift.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/slash.svg b/assets/icons/slash.svg deleted file mode 100644 index 1ebf01eb9f..0000000000 --- a/assets/icons/slash.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/sliders.svg b/assets/icons/sliders.svg deleted file mode 100644 index 20a6a367dc..0000000000 --- a/assets/icons/sliders.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/assets/icons/space.svg b/assets/icons/space.svg deleted file mode 100644 index 0294c9bf1e..0000000000 --- a/assets/icons/space.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/sparkle.svg b/assets/icons/sparkle.svg deleted file mode 100644 index 535c447723..0000000000 --- a/assets/icons/sparkle.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/split.svg b/assets/icons/split.svg deleted file mode 100644 index b2be46a875..0000000000 --- a/assets/icons/split.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/split_alt.svg b/assets/icons/split_alt.svg deleted file mode 100644 index 2f99e1436f..0000000000 --- a/assets/icons/split_alt.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/square_dot.svg b/assets/icons/square_dot.svg deleted file mode 100644 index 72b3273439..0000000000 --- a/assets/icons/square_dot.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/square_minus.svg b/assets/icons/square_minus.svg deleted file mode 100644 index 5ba458e8b5..0000000000 --- a/assets/icons/square_minus.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/square_plus.svg b/assets/icons/square_plus.svg deleted file mode 100644 index 063c7dbf82..0000000000 --- a/assets/icons/square_plus.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/star.svg b/assets/icons/star.svg deleted file mode 100644 index b39638e386..0000000000 --- a/assets/icons/star.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/star_filled.svg b/assets/icons/star_filled.svg deleted file mode 100644 index 16f64e5cb3..0000000000 --- a/assets/icons/star_filled.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/stop.svg b/assets/icons/stop.svg deleted file mode 100644 index cc2bbe9207..0000000000 --- a/assets/icons/stop.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/supermaven.svg b/assets/icons/supermaven.svg deleted file mode 100644 index af778c70b7..0000000000 --- a/assets/icons/supermaven.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/assets/icons/supermaven_disabled.svg b/assets/icons/supermaven_disabled.svg deleted file mode 100644 index 25eea54cde..0000000000 --- a/assets/icons/supermaven_disabled.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/supermaven_error.svg b/assets/icons/supermaven_error.svg deleted file mode 100644 index a0a12e17c3..0000000000 --- a/assets/icons/supermaven_error.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/supermaven_init.svg b/assets/icons/supermaven_init.svg deleted file mode 100644 index 6851aad49d..0000000000 --- a/assets/icons/supermaven_init.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/swatch_book.svg b/assets/icons/swatch_book.svg deleted file mode 100644 index b37d5df8c1..0000000000 --- a/assets/icons/swatch_book.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/sweep_ai.svg b/assets/icons/sweep_ai.svg deleted file mode 100644 index bf3459c7ea..0000000000 --- a/assets/icons/sweep_ai.svg +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/tab.svg b/assets/icons/tab.svg deleted file mode 100644 index db93be4df5..0000000000 --- a/assets/icons/tab.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/terminal.svg b/assets/icons/terminal.svg deleted file mode 100644 index d3742fa11a..0000000000 --- a/assets/icons/terminal.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/terminal_alt.svg b/assets/icons/terminal_alt.svg deleted file mode 100644 index d03c05423e..0000000000 --- a/assets/icons/terminal_alt.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - 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 deleted file mode 100644 index b8987546d3..0000000000 --- a/assets/icons/text_snippet.svg +++ /dev/null @@ -1 +0,0 @@ - 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.svg b/assets/icons/thread.svg deleted file mode 100644 index 496cf42e3a..0000000000 --- a/assets/icons/thread.svg +++ /dev/null @@ -1,3 +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 deleted file mode 100644 index a396ff14f6..0000000000 --- a/assets/icons/thumbs_down.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/thumbs_up.svg b/assets/icons/thumbs_up.svg deleted file mode 100644 index 73c859c355..0000000000 --- a/assets/icons/thumbs_up.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/todo_complete.svg b/assets/icons/todo_complete.svg deleted file mode 100644 index 5bf70841a8..0000000000 --- a/assets/icons/todo_complete.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/todo_pending.svg b/assets/icons/todo_pending.svg deleted file mode 100644 index e5e9776f11..0000000000 --- a/assets/icons/todo_pending.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/todo_progress.svg b/assets/icons/todo_progress.svg deleted file mode 100644 index b4a3e8c50e..0000000000 --- a/assets/icons/todo_progress.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/tool_copy.svg b/assets/icons/tool_copy.svg deleted file mode 100644 index a497a5c9cb..0000000000 --- a/assets/icons/tool_copy.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/tool_delete_file.svg b/assets/icons/tool_delete_file.svg deleted file mode 100644 index e15c0cb568..0000000000 --- a/assets/icons/tool_delete_file.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/tool_diagnostics.svg b/assets/icons/tool_diagnostics.svg deleted file mode 100644 index 414810628d..0000000000 --- a/assets/icons/tool_diagnostics.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/tool_folder.svg b/assets/icons/tool_folder.svg deleted file mode 100644 index 35f4c1f8ac..0000000000 --- a/assets/icons/tool_folder.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/tool_hammer.svg b/assets/icons/tool_hammer.svg deleted file mode 100644 index f725012cdf..0000000000 --- a/assets/icons/tool_hammer.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/tool_notification.svg b/assets/icons/tool_notification.svg deleted file mode 100644 index 7903a3369a..0000000000 --- a/assets/icons/tool_notification.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/tool_pencil.svg b/assets/icons/tool_pencil.svg deleted file mode 100644 index c4d289e9c0..0000000000 --- a/assets/icons/tool_pencil.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/tool_read.svg b/assets/icons/tool_read.svg deleted file mode 100644 index d22e9d8c7d..0000000000 --- a/assets/icons/tool_read.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/tool_regex.svg b/assets/icons/tool_regex.svg deleted file mode 100644 index 818c2ba360..0000000000 --- a/assets/icons/tool_regex.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/tool_search.svg b/assets/icons/tool_search.svg deleted file mode 100644 index b225a1298e..0000000000 --- a/assets/icons/tool_search.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/tool_terminal.svg b/assets/icons/tool_terminal.svg deleted file mode 100644 index 24da5e3a10..0000000000 --- a/assets/icons/tool_terminal.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - 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 deleted file mode 100644 index 288b54c432..0000000000 --- a/assets/icons/tool_web.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/trash.svg b/assets/icons/trash.svg deleted file mode 100644 index 4a9e9add02..0000000000 --- a/assets/icons/trash.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/triangle.svg b/assets/icons/triangle.svg deleted file mode 100644 index c36d382e73..0000000000 --- a/assets/icons/triangle.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/triangle_right.svg b/assets/icons/triangle_right.svg deleted file mode 100644 index bb82d8e637..0000000000 --- a/assets/icons/triangle_right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/undo.svg b/assets/icons/undo.svg deleted file mode 100644 index ccd45e246c..0000000000 --- a/assets/icons/undo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/unpin.svg b/assets/icons/unpin.svg deleted file mode 100644 index 07c93eae6f..0000000000 --- a/assets/icons/unpin.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/user_check.svg b/assets/icons/user_check.svg deleted file mode 100644 index ee32a52590..0000000000 --- a/assets/icons/user_check.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/user_group.svg b/assets/icons/user_group.svg deleted file mode 100644 index 30d2e5a7ea..0000000000 --- a/assets/icons/user_group.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/user_round_pen.svg b/assets/icons/user_round_pen.svg deleted file mode 100644 index e684fd1a20..0000000000 --- a/assets/icons/user_round_pen.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/warning.svg b/assets/icons/warning.svg deleted file mode 100644 index 5af37dab9d..0000000000 --- a/assets/icons/warning.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/whole_word.svg b/assets/icons/whole_word.svg deleted file mode 100644 index ce0d1606c8..0000000000 --- a/assets/icons/whole_word.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/x_circle.svg b/assets/icons/x_circle.svg deleted file mode 100644 index 8807e5fa1f..0000000000 --- a/assets/icons/x_circle.svg +++ /dev/null @@ -1 +0,0 @@ - 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 deleted file mode 100644 index 812277a100..0000000000 --- a/assets/icons/zed_assistant.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/zed_burn_mode.svg b/assets/icons/zed_burn_mode.svg deleted file mode 100644 index cad6ed666b..0000000000 --- a/assets/icons/zed_burn_mode.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/zed_burn_mode_on.svg b/assets/icons/zed_burn_mode_on.svg deleted file mode 100644 index 10e0e42b13..0000000000 --- a/assets/icons/zed_burn_mode_on.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/zed_predict.svg b/assets/icons/zed_predict.svg deleted file mode 100644 index 605a0584d5..0000000000 --- a/assets/icons/zed_predict.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/zed_predict_disabled.svg b/assets/icons/zed_predict_disabled.svg deleted file mode 100644 index d10c4d560a..0000000000 --- a/assets/icons/zed_predict_disabled.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/zed_predict_down.svg b/assets/icons/zed_predict_down.svg deleted file mode 100644 index 79eef9b0b4..0000000000 --- a/assets/icons/zed_predict_down.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/zed_predict_error.svg b/assets/icons/zed_predict_error.svg deleted file mode 100644 index 6f75326179..0000000000 --- a/assets/icons/zed_predict_error.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/zed_predict_up.svg b/assets/icons/zed_predict_up.svg deleted file mode 100644 index f77001e4bd..0000000000 --- a/assets/icons/zed_predict_up.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/zed_src_custom.svg b/assets/icons/zed_src_custom.svg deleted file mode 100644 index feff2d7d34..0000000000 --- a/assets/icons/zed_src_custom.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/zed_src_extension.svg b/assets/icons/zed_src_extension.svg deleted file mode 100644 index 00117efcf4..0000000000 --- a/assets/icons/zed_src_extension.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - 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 a04d32e51c..0000000000 --- a/assets/images/acp_logo_serif.svg +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/ai_grid.svg b/assets/images/ai_grid.svg deleted file mode 100644 index 49e8c4139e..0000000000 --- a/assets/images/ai_grid.svg +++ /dev/null @@ -1,334 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/debugger_grid.svg b/assets/images/debugger_grid.svg deleted file mode 100644 index 8b40dbd707..0000000000 --- a/assets/images/debugger_grid.svg +++ /dev/null @@ -1,890 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/grid.svg b/assets/images/grid.svg deleted file mode 100644 index fc70470024..0000000000 --- a/assets/images/grid.svg +++ /dev/null @@ -1,153 +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/images/zed_logo.svg b/assets/images/zed_logo.svg deleted file mode 100644 index d1769449c1..0000000000 --- a/assets/images/zed_logo.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/images/zed_x_copilot.svg b/assets/images/zed_x_copilot.svg deleted file mode 100644 index 3c5be71074..0000000000 --- a/assets/images/zed_x_copilot.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json deleted file mode 100644 index 0bcbb455b5..0000000000 --- a/assets/keymaps/default-linux.json +++ /dev/null @@ -1,1346 +0,0 @@ -[ - // Standard Linux bindings - { - "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", - "alt-shift-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::OpenFiles", - "ctrl-k ctrl-o": "workspace::Open", - "ctrl-=": ["zed::IncreaseBufferFontSize", { "persist": false }], - "ctrl-+": ["zed::IncreaseBufferFontSize", { "persist": false }], - "ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }], - "ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }], - "ctrl-,": "zed::OpenSettings", - "ctrl-alt-,": "zed::OpenSettingsFile", - "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-alt-z": "edit_prediction::RatePredictions", - "ctrl-alt-shift-i": "edit_prediction::ToggleMenu", - "ctrl-alt-l": "lsp_tool::ToggleMenu" - } - }, - { - "context": "Picker || menu", - "bindings": { - "up": "menu::SelectPrevious", - "down": "menu::SelectNext" - } - }, - { - "context": "Editor", - "bindings": { - "escape": "editor::Cancel", - "shift-backspace": "editor::Backspace", - "backspace": "editor::Backspace", - "delete": "editor::Delete", - "tab": "editor::Tab", - "shift-tab": "editor::Backtab", - "ctrl-k": "editor::CutToEndOfLine", - "ctrl-k ctrl-q": "editor::Rewrap", - "ctrl-k q": "editor::Rewrap", - "ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }], - "ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], - "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", - "ctrl-shift-i": "editor::Format", - "alt-shift-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-alt-shift-e": "editor::ToggleEditPrediction", - "f9": "editor::ToggleBreakpoint", - "shift-f9": "editor::EditLogBreakpoint" - } - }, - { - "context": "Editor && mode == full", - "bindings": { - "shift-enter": "editor::Newline", - "enter": "editor::Newline", - "ctrl-enter": "editor::NewlineBelow", - "ctrl-shift-enter": "editor::NewlineAbove", - "ctrl-k ctrl-z": "editor::ToggleSoftWrap", - "ctrl-k z": "editor::ToggleSoftWrap", - "find": "buffer_search::Deploy", - "ctrl-f": "buffer_search::Deploy", - "ctrl-h": "buffer_search::DeployReplace", - "ctrl->": "agent::AddSelectionToThread", - "ctrl-<": "assistant::InsertIntoEditor", - "ctrl-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", - "bindings": { - "alt-]": "editor::NextEditPrediction", - "alt-[": "editor::PreviousEditPrediction" - } - }, - { - "context": "Editor && !edit_prediction", - "bindings": { - "alt-\\": "editor::ShowEditPrediction" - } - }, - { - "context": "Editor && mode == auto_height", - "bindings": { - "ctrl-enter": "editor::Newline", - "shift-enter": "editor::Newline", - "ctrl-shift-enter": "editor::NewlineBelow" - } - }, - { - "context": "Markdown", - "bindings": { - "copy": "markdown::Copy", - "ctrl-insert": "markdown::Copy", - "ctrl-c": "markdown::Copy" - } - }, - { - "context": "Editor && jupyter && !ContextEditor", - "bindings": { - "ctrl-shift-enter": "repl::Run", - "ctrl-alt-enter": "repl::RunInPlace" - } - }, - { - "context": "Editor && !agent_diff", - "bindings": { - "ctrl-k ctrl-r": "git::Restore", - "ctrl-alt-y": "git::ToggleStaged", - "alt-y": "git::StageAndNext", - "alt-shift-y": "git::UnstageAndNext" - } - }, - { - "context": "Editor && editor_agent_diff", - "bindings": { - "ctrl-y": "agent::Keep", - "ctrl-n": "agent::Reject", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll", - "shift-ctrl-r": "agent::OpenAgentDiff" - } - }, - { - "context": "AgentDiff", - "bindings": { - "ctrl-y": "agent::Keep", - "ctrl-n": "agent::Reject", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "ContextEditor > Editor", - "bindings": { - "ctrl-enter": "assistant::Assist", - "ctrl-s": "workspace::Save", - "save": "workspace::Save", - "ctrl-<": "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", - "bindings": { - "ctrl-n": "agent::NewThread", - "ctrl-alt-n": "agent::NewTextThread", - "ctrl-shift-h": "agent::OpenHistory", - "ctrl-alt-c": "agent::OpenSettings", - "ctrl-alt-p": "agent::ManageProfiles", - "ctrl-alt-l": "agent::OpenRulesLibrary", - "ctrl-i": "agent::ToggleProfileSelector", - "ctrl-alt-/": "agent::ToggleModelSelector", - "ctrl-shift-j": "agent::ToggleNavigationMenu", - "ctrl-alt-i": "agent::ToggleOptionsMenu", - "ctrl-alt-shift-n": "agent::ToggleNewThreadMenu", - "shift-alt-escape": "agent::ExpandMessageEditor", - "ctrl->": "agent::AddSelectionToThread", - "ctrl-shift-e": "project_panel::ToggleFocus", - "ctrl-shift-enter": "agent::ContinueThread", - "super-ctrl-b": "agent::ToggleBurnMode", - "alt-enter": "agent::ContinueWithBurnMode", - "ctrl-y": "agent::AllowOnce", - "ctrl-alt-y": "agent::AllowAlways", - "ctrl-alt-z": "agent::RejectOnce" - } - }, - { - "context": "AgentPanel > NavigationMenu", - "bindings": { - "shift-backspace": "agent::DeleteRecentlyOpenThread" - } - }, - { - "context": "AgentPanel > Markdown", - "bindings": { - "copy": "markdown::CopyAsMarkdown", - "ctrl-insert": "markdown::CopyAsMarkdown", - "ctrl-c": "markdown::CopyAsMarkdown" - } - }, - { - "context": "AgentPanel && text_thread", - "bindings": { - "ctrl-n": "agent::NewTextThread", - "ctrl-alt-t": "agent::NewThread" - } - }, - { - "context": "AgentPanel && acp_thread", - "use_key_equivalents": true, - "bindings": { - "ctrl-n": "agent::NewExternalAgentThread", - "ctrl-alt-t": "agent::NewThread" - } - }, - { - "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send", - "bindings": { - "enter": "agent::Chat", - "ctrl-enter": "agent::ChatWithFollow", - "ctrl-i": "agent::ToggleProfileSelector", - "shift-ctrl-r": "agent::OpenAgentDiff", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "MessageEditor && !Picker > Editor && use_modifier_to_send", - "bindings": { - "ctrl-enter": "agent::Chat", - "enter": "editor::Newline", - "ctrl-i": "agent::ToggleProfileSelector", - "shift-ctrl-r": "agent::OpenAgentDiff", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "EditMessageEditor > Editor", - "bindings": { - "escape": "menu::Cancel", - "enter": "menu::Confirm", - "alt-enter": "editor::Newline" - } - }, - { - "context": "AgentFeedbackMessageEditor > Editor", - "bindings": { - "escape": "menu::Cancel", - "enter": "menu::Confirm", - "alt-enter": "editor::Newline" - } - }, - { - "context": "AcpThread > ModeSelector", - "bindings": { - "ctrl-enter": "menu::Confirm" - } - }, - { - "context": "AcpThread > Editor && !use_modifier_to_send", - "use_key_equivalents": true, - "bindings": { - "enter": "agent::Chat", - "shift-ctrl-r": "agent::OpenAgentDiff", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "AcpThread > Editor && use_modifier_to_send", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "agent::Chat", - "shift-ctrl-r": "agent::OpenAgentDiff", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll", - "shift-tab": "agent::CycleModeSelector" - } - }, - { - "context": "ThreadHistory", - "bindings": { - "backspace": "agent::RemoveSelectedThread" - } - }, - { - "context": "RulesLibrary", - "bindings": { - "new": "rules_library::NewRule", - "ctrl-n": "rules_library::NewRule", - "ctrl-shift-s": "rules_library::ToggleDefaultRule", - "ctrl-w": "workspace::CloseWindow" - } - }, - { - "context": "BufferSearchBar", - "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", - "bindings": { - "enter": "search::ReplaceNext", - "ctrl-enter": "search::ReplaceAll" - } - }, - { - "context": "BufferSearchBar && !in_replace > Editor", - "bindings": { - "up": "search::PreviousHistoryQuery", - "down": "search::NextHistoryQuery" - } - }, - { - "context": "ProjectSearchBar", - "bindings": { - "escape": "project_search::ToggleFocus", - "shift-find": "search::FocusSearch", - "shift-enter": "project_search::ToggleAllSearchResults", - "ctrl-shift-f": "search::FocusSearch", - "ctrl-shift-h": "search::ToggleReplace", - "alt-ctrl-g": "search::ToggleRegex", - "alt-ctrl-x": "search::ToggleRegex" - } - }, - { - "context": "ProjectSearchBar > Editor", - "bindings": { - "up": "search::PreviousHistoryQuery", - "down": "search::NextHistoryQuery" - } - }, - { - "context": "ProjectSearchBar && in_replace > Editor", - "bindings": { - "enter": "search::ReplaceNext", - "ctrl-alt-enter": "search::ReplaceAll" - } - }, - { - "context": "ProjectSearchView", - "bindings": { - "escape": "project_search::ToggleFocus", - "ctrl-shift-h": "search::ToggleReplace", - "alt-ctrl-g": "search::ToggleRegex", - "alt-ctrl-x": "search::ToggleRegex" - } - }, - { - "context": "Pane", - "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 }], - "alt-ctrl-t": ["pane::CloseOtherItems", { "close_pinned": false }], - "alt-ctrl-shift-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", - "ctrl-alt--": "pane::GoBack", - "forward": "pane::GoForward", - "ctrl-alt-_": "pane::GoForward", - "ctrl-alt-g": "search::SelectNextMatch", - "f3": "search::SelectNextMatch", - "ctrl-alt-shift-g": "search::SelectPreviousMatch", - "shift-f3": "search::SelectPreviousMatch", - "shift-find": "project_search::ToggleFocus", - "ctrl-shift-f": "project_search::ToggleFocus", - "ctrl-alt-shift-h": "search::ToggleReplace", - "ctrl-alt-shift-l": "search::ToggleSelection", - "alt-enter": "search::SelectAllMatches", - "alt-c": "search::ToggleCaseSensitive", - "alt-w": "search::ToggleWholeWord", - "alt-find": "project_search::ToggleFilters", - "alt-ctrl-f": "project_search::ToggleFilters", - "shift-enter": "project_search::ToggleAllSearchResults", - "ctrl-alt-shift-r": "search::ToggleRegex", - "ctrl-alt-shift-x": "search::ToggleRegex", - "alt-r": "search::ToggleRegex", - "ctrl-k shift-enter": "pane::TogglePinTab" - } - }, - // Bindings from VS Code - { - "context": "Editor", - "bindings": { - "ctrl-[": "editor::Outdent", - "ctrl-]": "editor::Indent", - "shift-alt-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }], // Insert Cursor Above - "shift-alt-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }], // Insert Cursor Below - "ctrl-shift-k": "editor::DeleteLine", - "alt-up": "editor::MoveLineUp", - "alt-down": "editor::MoveLineDown", - "ctrl-alt-shift-up": "editor::DuplicateLineUp", - "ctrl-alt-shift-down": "editor::DuplicateLineDown", - "alt-shift-right": "editor::SelectLargerSyntaxNode", // Expand selection - "alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink selection - "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::GoToTypeDefinition", - "shift-f12": "editor::GoToImplementation", - "alt-ctrl-f12": "editor::GoToTypeDefinitionSplit", - "alt-shift-f12": "editor::FindAllReferences", - "ctrl-m": "editor::MoveToEnclosingBracket", // from jetbrains - "ctrl-|": "editor::MoveToEnclosingBracket", - "ctrl-{": "editor::Fold", - "ctrl-}": "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-alt-shift-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", - "bindings": { - "ctrl-shift-o": "outline::Toggle", - "ctrl-g": "go_to_line::Toggle" - } - }, - { - "context": "Workspace", - "bindings": { - "alt-open": ["projects::OpenRecent", { "create_new_window": false }], - // Change the default action on `menu::Confirm` by setting the parameter - // "alt-ctrl-o": ["projects::OpenRecent", { "create_new_window": true }], - "alt-ctrl-o": ["projects::OpenRecent", { "create_new_window": false }], - "alt-shift-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }], - // Change to open path modal for existing remote connection by setting the parameter - // "alt-ctrl-shift-o": "["projects::OpenRemote", { "from_existing_connection": true }]", - "alt-ctrl-shift-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }], - "alt-ctrl-shift-b": "branches::OpenRecent", - "alt-shift-enter": "toast::RunAction", - "ctrl-~": "workspace::NewTerminal", - "save": "workspace::Save", - "ctrl-s": "workspace::Save", - "ctrl-k 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::Toggle", - "f10": ["app_menu::OpenApplicationMenu", "Zed"], - "alt-1": ["workspace::ActivatePane", 0], - "alt-2": ["workspace::ActivatePane", 1], - "alt-3": ["workspace::ActivatePane", 2], - "alt-4": ["workspace::ActivatePane", 3], - "alt-5": ["workspace::ActivatePane", 4], - "alt-6": ["workspace::ActivatePane", 5], - "alt-7": ["workspace::ActivatePane", 6], - "alt-8": ["workspace::ActivatePane", 7], - "alt-9": ["workspace::ActivatePane", 8], - "ctrl-alt-b": "workspace::ToggleRightDock", - "ctrl-b": "workspace::ToggleLeftDock", - "ctrl-j": "workspace::ToggleBottomDock", - "ctrl-alt-y": "workspace::ToggleAllDocks", - "ctrl-alt-0": "workspace::ResetActiveDockSize", - // For 0px parameter, uses UI font size value. - "ctrl-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }], - "ctrl-alt-=": ["workspace::IncreaseActiveDockSize", { "px": 0 }], - "ctrl-alt-)": "workspace::ResetOpenDocksSize", - "ctrl-alt-_": ["workspace::DecreaseOpenDocksSize", { "px": 0 }], - "ctrl-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::OpenKeymap", - "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-shift-tab": ["tab_switcher::Toggle", { "select_last": true }], - "ctrl-tab": "tab_switcher::Toggle", - "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-?": "agent::ToggleFocus", - "alt-save": "workspace::SaveAll", - "ctrl-alt-s": "workspace::SaveAll", - "ctrl-k m": "language_selector::Toggle", - "ctrl-k ctrl-m": "toolchain::AddToolchain", - "escape": "workspace::Unfollow", - "ctrl-k ctrl-left": "workspace::ActivatePaneLeft", - "ctrl-k ctrl-right": "workspace::ActivatePaneRight", - "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", - // All task parameters are captured and unchanged between reruns by default. - // Use the `"reevaluate_context"` parameter to control this. - "ctrl-shift-r": ["task::Rerun", { "reevaluate_context": false }], - "ctrl-alt-r": "task::Rerun", - "alt-t": "task::Rerun", - "alt-shift-t": "task::Spawn", - "alt-shift-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", - "bindings": { - "f5": "zed::NoAction" - } - }, - { - "context": "Workspace && debugger_stopped", - "bindings": { - "f5": "debugger::Continue" - } - }, - { - "context": "ApplicationMenu", - "bindings": { - "f10": "menu::Cancel", - "left": "app_menu::ActivateMenuLeft", - "right": "app_menu::ActivateMenuRight" - } - }, - // Bindings from Sublime Text - { - "context": "Editor", - "bindings": { - "ctrl-u": "editor::UndoSelection", - "ctrl-shift-u": "editor::RedoSelection", - "ctrl-shift-j": "editor::JoinLines", - "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart", - "ctrl-alt-h": "editor::DeleteToPreviousSubwordStart", - "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd", - "ctrl-alt-d": "editor::DeleteToNextSubwordEnd", - "ctrl-alt-left": "editor::MoveToPreviousSubwordStart", - "ctrl-alt-right": "editor::MoveToNextSubwordEnd", - "ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart", - "ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart", - "ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd", - "ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd" - } - }, - // Bindings from Atom - { - "context": "Pane", - "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", - "bindings": { - "enter": "editor::ConfirmRename" - } - }, - { - "context": "Editor && showing_completions", - "bindings": { - "enter": "editor::ConfirmCompletion", - "shift-enter": "editor::ConfirmCompletionReplace", - "tab": "editor::ComposeCompletion" - } - }, - { - "context": "Editor && in_snippet && has_next_tabstop && !showing_completions", - "use_key_equivalents": true, - "bindings": { - "tab": "editor::NextSnippetTabstop" - } - }, - { - "context": "Editor && in_snippet && has_previous_tabstop && !showing_completions", - "use_key_equivalents": true, - "bindings": { - "shift-tab": "editor::PreviousSnippetTabstop" - } - }, - // 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", - "bindings": { - "alt-tab": "editor::AcceptEditPrediction", - "alt-l": "editor::AcceptEditPrediction", - "tab": "editor::AcceptEditPrediction", - "alt-right": "editor::AcceptPartialEditPrediction" - } - }, - { - "context": "Editor && edit_prediction_conflict", - "bindings": { - "alt-tab": "editor::AcceptEditPrediction", - "alt-l": "editor::AcceptEditPrediction", - "alt-right": "editor::AcceptPartialEditPrediction" - } - }, - { - "context": "Editor && showing_code_actions", - "bindings": { - "enter": "editor::ConfirmCodeAction" - } - }, - { - "context": "Editor && (showing_code_actions || showing_completions)", - "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", - "bindings": { - "up": "editor::SignatureHelpPrevious", - "down": "editor::SignatureHelpNext" - } - }, - // Custom bindings - { - "bindings": { - "ctrl-alt-shift-f": "workspace::FollowNextCollaborator", - // Only available in debug builds: opens an element inspector for development. - "ctrl-alt-i": "dev::ToggleInspector" - } - }, - { - "context": "!Terminal", - "bindings": { - "ctrl-shift-c": "collab_panel::ToggleFocus" - } - }, - { - "context": "!ContextEditor > Editor && mode == full", - "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-:": "editor::ToggleInlayHints" - } - }, - { - "context": "PromptEditor", - "bindings": { - "ctrl-[": "agent::CyclePreviousInlineAssist", - "ctrl-]": "agent::CycleNextInlineAssist", - "ctrl-shift-enter": "inline_assistant::ThumbsUpResult", - "ctrl-shift-backspace": "inline_assistant::ThumbsDownResult" - } - }, - { - "context": "Prompt", - "bindings": { - "left": "menu::SelectPrevious", - "right": "menu::SelectNext", - "h": "menu::SelectPrevious", - "l": "menu::SelectNext" - } - }, - { - "context": "ProjectSearchBar && !in_replace", - "bindings": { - "ctrl-enter": "project_search::SearchInNew" - } - }, - { - "context": "OutlinePanel && not_editing", - "bindings": { - "escape": "menu::Cancel", - "left": "outline_panel::CollapseSelectedEntry", - "right": "outline_panel::ExpandSelectedEntry", - "alt-copy": "outline_panel::CopyPath", - "ctrl-alt-c": "outline_panel::CopyPath", - "alt-shift-copy": "workspace::CopyRelativePath", - "alt-ctrl-shift-c": "workspace::CopyRelativePath", - "alt-ctrl-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", - "bindings": { - "left": "project_panel::CollapseSelectedEntry", - "ctrl-left": "project_panel::CollapseAllEntries", - "right": "project_panel::ExpandSelectedEntry", - "new": "project_panel::NewFile", - "ctrl-n": "project_panel::NewFile", - "alt-new": "project_panel::NewDirectory", - "alt-ctrl-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", - "ctrl-alt-c": "project_panel::CopyPath", - "alt-shift-copy": "workspace::CopyRelativePath", - "alt-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 }], - "alt-ctrl-r": "project_panel::RevealInFileManager", - "ctrl-shift-enter": "workspace::OpenWithSystem", - "alt-d": "project_panel::CompareMarkedFiles", - "shift-find": "project_panel::NewSearchInDirectory", - "ctrl-alt-shift-f": "project_panel::NewSearchInDirectory", - "shift-down": "menu::SelectNext", - "shift-up": "menu::SelectPrevious", - "escape": "menu::Cancel" - } - }, - { - "context": "ProjectPanel && not_editing", - "bindings": { - "space": "project_panel::Open" - } - }, - { - "context": "GitPanel && ChangesList", - "bindings": { - "up": "menu::SelectPrevious", - "down": "menu::SelectNext", - "enter": "menu::Confirm", - "alt-y": "git::StageFile", - "alt-shift-y": "git::UnstageFile", - "ctrl-alt-y": "git::ToggleStaged", - "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", - "bindings": { - "escape": "menu::Cancel", - "enter": "editor::Newline", - "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend", - "alt-l": "git::GenerateCommitMessage" - } - }, - { - "context": "GitPanel", - "bindings": { - "ctrl-g ctrl-g": "git::Fetch", - "ctrl-g up": "git::Push", - "ctrl-g down": "git::Pull", - "ctrl-g shift-down": "git::PullRebase", - "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", - "bindings": { - "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend", - "ctrl-space": "git::StageAll", - "ctrl-shift-space": "git::UnstageAll" - } - }, - { - "context": "AskPass > Editor", - "bindings": { - "enter": "menu::Confirm" - } - }, - { - "context": "CommitEditor > Editor", - "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", - "bindings": { - "ctrl-t": "debugger::ToggleThreadPicker", - "ctrl-i": "debugger::ToggleSessionPicker", - "shift-alt-escape": "debugger::ToggleExpandItem" - } - }, - { - "context": "VariableList", - "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", - "bindings": { - "space": "debugger::ToggleEnableBreakpoint", - "backspace": "debugger::UnsetBreakpoint", - "left": "debugger::PreviousBreakpointProperty", - "right": "debugger::NextBreakpointProperty" - } - }, - { - "context": "CollabPanel && not_editing", - "bindings": { - "ctrl-backspace": "collab_panel::Remove", - "space": "menu::Confirm" - } - }, - { - "context": "CollabPanel", - "bindings": { - "alt-up": "collab_panel::MoveChannelUp", - "alt-down": "collab_panel::MoveChannelDown", - "alt-enter": "collab_panel::OpenSelectedChannelNotes" - } - }, - { - "context": "(CollabPanel && editing) > Editor", - "bindings": { - "space": "collab_panel::InsertSpace" - } - }, - { - "context": "ChannelModal", - "bindings": { - "tab": "channel_modal::ToggleMode" - } - }, - { - "context": "Picker > Editor", - "bindings": { - "escape": "menu::Cancel", - "up": "menu::SelectPrevious", - "down": "menu::SelectNext", - "tab": "picker::ConfirmCompletion", - "alt-enter": ["picker::ConfirmInput", { "secondary": false }] - } - }, - { - "context": "ChannelModal > Picker > Editor", - "bindings": { - "tab": "channel_modal::ToggleMode" - } - }, - { - "context": "ToolchainSelector", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-a": "toolchain::AddToolchain" - } - }, - { - "context": "FileFinder || (FileFinder > Picker > Editor)", - "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)", - "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", - "bindings": { - "ctrl-shift-tab": "menu::SelectPrevious", - "ctrl-up": "menu::SelectPrevious", - "ctrl-down": "menu::SelectNext", - "ctrl-backspace": "tab_switcher::CloseSelectedItem" - } - }, - { - "context": "StashList || (StashList > Picker > Editor)", - "bindings": { - "ctrl-shift-backspace": "stash_picker::DropStashItem", - "ctrl-shift-v": "stash_picker::ShowStashItem" - } - }, - { - "context": "Terminal", - "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", - "ctrl-shift-5": "pane::SplitRight" - } - }, - { - "context": "ZedPredictModal", - "bindings": { - "escape": "menu::Cancel" - } - }, - { - "context": "ConfigureContextServerModal > Editor", - "bindings": { - "escape": "menu::Cancel", - "enter": "editor::Newline", - "ctrl-enter": "menu::Confirm" - } - }, - { - "context": "ContextServerToolsModal", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel" - } - }, - { - "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", - "bindings": { - "ctrl-tab": "pane::ActivateNextItem", - "ctrl-shift-tab": "pane::ActivatePreviousItem" - } - }, - { - "context": "MarkdownPreview", - "bindings": { - "pageup": "markdown::ScrollPageUp", - "pagedown": "markdown::ScrollPageDown", - "up": "markdown::ScrollUp", - "down": "markdown::ScrollDown", - "alt-up": "markdown::ScrollUpByItem", - "alt-down": "markdown::ScrollDownByItem" - } - }, - { - "context": "KeymapEditor", - "use_key_equivalents": true, - "bindings": { - "ctrl-f": "search::FocusSearch", - "alt-find": "keymap_editor::ToggleKeystrokeSearch", - "alt-ctrl-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-=": ["zed::IncreaseUiFontSize", { "persist": false }], - "ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }], - "ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }], - "ctrl-0": ["zed::ResetUiFontSize", { "persist": false }], - "ctrl-enter": "onboarding::Finish", - "alt-shift-l": "onboarding::SignIn", - "alt-shift-a": "onboarding::OpenAccount" - } - }, - { - "context": "Welcome", - "use_key_equivalents": true, - "bindings": { - "ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }], - "ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }], - "ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }], - "ctrl-0": ["zed::ResetUiFontSize", { "persist": false }] - } - }, - { - "context": "InvalidBuffer", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-enter": "workspace::OpenWithSystem" - } - }, - { - "context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-space": "git::WorktreeFromDefaultOnWindow", - "ctrl-space": "git::WorktreeFromDefault" - } - }, - { - "context": "SettingsWindow", - "use_key_equivalents": true, - "bindings": { - "ctrl-w": "workspace::CloseWindow", - "escape": "workspace::CloseWindow", - "ctrl-m": "settings_editor::Minimize", - "ctrl-f": "search::FocusSearch", - "ctrl-,": "settings_editor::OpenCurrentFile", - "left": "settings_editor::ToggleFocusNav", - "ctrl-shift-e": "settings_editor::ToggleFocusNav", - // todo(settings_ui): cut this down based on the max files and overflow UI - "ctrl-1": ["settings_editor::FocusFile", 0], - "ctrl-2": ["settings_editor::FocusFile", 1], - "ctrl-3": ["settings_editor::FocusFile", 2], - "ctrl-4": ["settings_editor::FocusFile", 3], - "ctrl-5": ["settings_editor::FocusFile", 4], - "ctrl-6": ["settings_editor::FocusFile", 5], - "ctrl-7": ["settings_editor::FocusFile", 6], - "ctrl-8": ["settings_editor::FocusFile", 7], - "ctrl-9": ["settings_editor::FocusFile", 8], - "ctrl-0": ["settings_editor::FocusFile", 9], - "ctrl-pageup": "settings_editor::FocusPreviousFile", - "ctrl-pagedown": "settings_editor::FocusNextFile" - } - }, - { - "context": "StashDiff > Editor", - "bindings": { - "ctrl-space": "git::ApplyCurrentStash", - "ctrl-shift-space": "git::PopCurrentStash", - "ctrl-shift-backspace": "git::DropCurrentStash" - } - }, - { - "context": "SettingsWindow > NavigationMenu", - "use_key_equivalents": true, - "bindings": { - "up": "settings_editor::FocusPreviousNavEntry", - "shift-tab": "settings_editor::FocusPreviousNavEntry", - "down": "settings_editor::FocusNextNavEntry", - "tab": "settings_editor::FocusNextNavEntry", - "right": "settings_editor::ExpandNavEntry", - "left": "settings_editor::CollapseNavEntry", - "pageup": "settings_editor::FocusPreviousRootNavEntry", - "pagedown": "settings_editor::FocusNextRootNavEntry", - "home": "settings_editor::FocusFirstNavEntry", - "end": "settings_editor::FocusLastNavEntry" - } - }, - { - "context": "EditPredictionContext > Editor", - "bindings": { - "alt-left": "dev::EditPredictionContextGoBack", - "alt-right": "dev::EditPredictionContextGoForward" - } - }, - { - "context": "GitBranchSelector || (GitBranchSelector > Picker > Editor)", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-backspace": "branch_picker::DeleteBranch", - "ctrl-shift-i": "branch_picker::FilterRemotes" - } - } -] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json deleted file mode 100644 index 65ac280ba7..0000000000 --- a/assets/keymaps/default-macos.json +++ /dev/null @@ -1,1450 +0,0 @@ -[ - // Standard macOS bindings - { - "use_key_equivalents": true, - "bindings": { - "f4": "debugger::Start", - "shift-f5": "debugger::Stop", - "shift-cmd-f5": "debugger::RerunSession", - "f6": "debugger::Pause", - "f7": "debugger::StepOver", - "ctrl-f11": "debugger::StepInto", - "shift-f11": "debugger::StepOut", - "home": "menu::SelectFirst", - "shift-pageup": "menu::SelectFirst", - "pageup": "menu::SelectFirst", - "cmd-up": "menu::SelectFirst", - "end": "menu::SelectLast", - "shift-pagedown": "menu::SelectLast", - "pagedown": "menu::SelectLast", - "cmd-down": "menu::SelectLast", - "tab": "menu::SelectNext", - "ctrl-n": "menu::SelectNext", - "down": "menu::SelectNext", - "shift-tab": "menu::SelectPrevious", - "ctrl-p": "menu::SelectPrevious", - "up": "menu::SelectPrevious", - "enter": "menu::Confirm", - "ctrl-enter": "menu::SecondaryConfirm", - "cmd-enter": "menu::SecondaryConfirm", - "cmd-escape": "menu::Cancel", - "ctrl-escape": "menu::Cancel", - "ctrl-c": "menu::Cancel", - "escape": "menu::Cancel", - "alt-shift-enter": "menu::Restart", - "cmd-shift-w": "workspace::CloseWindow", - "shift-escape": "workspace::ToggleZoom", - "cmd-o": "workspace::Open", - "cmd-=": ["zed::IncreaseBufferFontSize", { "persist": false }], - "cmd-+": ["zed::IncreaseBufferFontSize", { "persist": false }], - "cmd--": ["zed::DecreaseBufferFontSize", { "persist": false }], - "cmd-0": ["zed::ResetBufferFontSize", { "persist": false }], - "cmd-,": "zed::OpenSettings", - "cmd-alt-,": "zed::OpenSettingsFile", - "cmd-q": "zed::Quit", - "cmd-h": "zed::Hide", - "alt-cmd-h": "zed::HideOthers", - "cmd-m": "zed::Minimize", - "fn-f": "zed::ToggleFullScreen", - "ctrl-cmd-f": "zed::ToggleFullScreen", - "ctrl-cmd-z": "edit_prediction::RatePredictions", - "ctrl-cmd-i": "edit_prediction::ToggleMenu", - "ctrl-cmd-l": "lsp_tool::ToggleMenu", - "ctrl-cmd-c": "editor::DisplayCursorNames" - } - }, - { - "context": "Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "editor::Cancel", - "shift-backspace": "editor::Backspace", - "ctrl-h": "editor::Backspace", - "backspace": "editor::Backspace", - "ctrl-d": "editor::Delete", - "delete": "editor::Delete", - "tab": "editor::Tab", - "shift-tab": "editor::Backtab", - "ctrl-t": "editor::Transpose", - "ctrl-k": "editor::KillRingCut", - "ctrl-y": "editor::KillRingYank", - "cmd-k cmd-q": "editor::Rewrap", - "cmd-k q": "editor::Rewrap", - "cmd-backspace": "editor::DeleteToBeginningOfLine", - "cmd-delete": "editor::DeleteToEndOfLine", - "alt-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }], - "ctrl-w": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }], - "alt-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], - "cmd-x": "editor::Cut", - "cmd-c": "editor::Copy", - "cmd-v": "editor::Paste", - "cmd-z": "editor::Undo", - "cmd-shift-z": "editor::Redo", - "up": "editor::MoveUp", - "ctrl-up": "editor::MoveToStartOfParagraph", - "pageup": "editor::MovePageUp", - "shift-pageup": "editor::SelectPageUp", - "cmd-pageup": "editor::PageUp", - "ctrl-pageup": "editor::LineUp", - "down": "editor::MoveDown", - "ctrl-down": "editor::MoveToEndOfParagraph", - "pagedown": "editor::MovePageDown", - "shift-pagedown": "editor::SelectPageDown", - "cmd-pagedown": "editor::PageDown", - "ctrl-pagedown": "editor::LineDown", - "ctrl-p": "editor::MoveUp", - "ctrl-n": "editor::MoveDown", - "ctrl-b": "editor::MoveLeft", - "left": "editor::MoveLeft", - "ctrl-f": "editor::MoveRight", - "right": "editor::MoveRight", - "ctrl-l": "editor::ScrollCursorCenter", - "alt-left": "editor::MoveToPreviousWordStart", - "alt-right": "editor::MoveToNextWordEnd", - "cmd-left": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }], - "ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_indent": true }], - "home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }], - "cmd-right": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": true }], - "ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], - "end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": true }], - "cmd-up": "editor::MoveToBeginning", - "cmd-down": "editor::MoveToEnd", - "cmd-home": "editor::MoveToBeginning", // Typed via `cmd-fn-left` - "cmd-end": "editor::MoveToEnd", // Typed via `cmd-fn-right` - "shift-up": "editor::SelectUp", - "ctrl-shift-p": "editor::SelectUp", - "shift-down": "editor::SelectDown", - "ctrl-shift-n": "editor::SelectDown", - "shift-left": "editor::SelectLeft", - "ctrl-shift-b": "editor::SelectLeft", - "shift-right": "editor::SelectRight", - "ctrl-shift-f": "editor::SelectRight", - "alt-shift-left": "editor::SelectToPreviousWordStart", // cursorWordLeftSelect - "alt-shift-right": "editor::SelectToNextWordEnd", // cursorWordRightSelect - "ctrl-shift-up": "editor::SelectToStartOfParagraph", - "ctrl-shift-down": "editor::SelectToEndOfParagraph", - "cmd-shift-up": "editor::SelectToBeginning", - "cmd-shift-down": "editor::SelectToEnd", - "cmd-a": "editor::SelectAll", - "cmd-l": "editor::SelectLine", - "cmd-shift-i": "editor::Format", - "alt-shift-o": "editor::OrganizeImports", - "cmd-shift-left": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }], - "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }], - "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }], - "cmd-shift-right": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }], - "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }], - "ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }], - "ctrl-v": ["editor::MovePageDown", { "center_cursor": true }], - "ctrl-shift-v": ["editor::MovePageUp", { "center_cursor": true }], - "ctrl-cmd-space": "editor::ShowCharacterPalette", - "cmd-;": "editor::ToggleLineNumbers", - "cmd-'": "editor::ToggleSelectedDiffHunks", - "cmd-\"": "editor::ExpandAllDiffHunks", - "cmd-alt-g b": "git::Blame", - "cmd-alt-g m": "git::OpenModifiedFiles", - "cmd-i": "editor::ShowSignatureHelp", - "f9": "editor::ToggleBreakpoint", - "shift-f9": "editor::EditLogBreakpoint", - "ctrl-f12": "editor::GoToDeclaration", - "alt-ctrl-f12": "editor::GoToDeclarationSplit", - "ctrl-cmd-e": "editor::ToggleEditPrediction" - } - }, - { - "context": "Editor && mode == full", - "use_key_equivalents": true, - "bindings": { - "shift-enter": "editor::Newline", - "enter": "editor::Newline", - "cmd-enter": "editor::NewlineBelow", - "cmd-shift-enter": "editor::NewlineAbove", - "cmd-k z": "editor::ToggleSoftWrap", - "cmd-f": "buffer_search::Deploy", - "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::AddSelectionToThread", - "cmd-<": "assistant::InsertIntoEditor", - "cmd-alt-e": "editor::SelectEnclosingSymbol", - "alt-enter": "editor::OpenSelectionsInMultibuffer" - } - }, - { - "context": "Editor && multibuffer", - "use_key_equivalents": true, - "bindings": { - "cmd-up": "editor::MoveToStartOfExcerpt", - "cmd-down": "editor::MoveToStartOfNextExcerpt", - "cmd-shift-up": "editor::SelectToStartOfExcerpt", - "cmd-shift-down": "editor::SelectToStartOfNextExcerpt" - } - }, - { - "context": "Editor && mode == full && edit_prediction", - "use_key_equivalents": true, - "bindings": { - "alt-tab": "editor::NextEditPrediction", - "alt-shift-tab": "editor::PreviousEditPrediction" - } - }, - { - "context": "Editor && !edit_prediction", - "use_key_equivalents": true, - "bindings": { - "alt-tab": "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": { - "cmd-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 && !AgentPanel", - "use_key_equivalents": true, - "bindings": { - "cmd-alt-z": "git::Restore", - "cmd-alt-y": "git::ToggleStaged", - "cmd-y": "git::StageAndNext", - "cmd-shift-y": "git::UnstageAndNext" - } - }, - { - "context": "AgentDiff", - "use_key_equivalents": true, - "bindings": { - "cmd-y": "agent::Keep", - "cmd-n": "agent::Reject", - "cmd-shift-y": "agent::KeepAll", - "cmd-shift-n": "agent::RejectAll" - } - }, - { - "context": "Editor && editor_agent_diff", - "use_key_equivalents": true, - "bindings": { - "cmd-y": "agent::Keep", - "cmd-n": "agent::Reject", - "cmd-shift-y": "agent::KeepAll", - "cmd-shift-n": "agent::RejectAll", - "shift-ctrl-r": "agent::OpenAgentDiff" - } - }, - { - "context": "ContextEditor > Editor", - "use_key_equivalents": true, - "bindings": { - "cmd-enter": "assistant::Assist", - "cmd-s": "workspace::Save", - "cmd-<": "assistant::InsertIntoEditor", - "shift-enter": "assistant::Split", - "ctrl-r": "assistant::CycleMessageRole", - "enter": "assistant::ConfirmCommand", - "alt-enter": "editor::Newline", - "cmd-k c": "assistant::CopyCode", - "cmd-g": "search::SelectNextMatch", - "cmd-shift-g": "search::SelectPreviousMatch", - "cmd-k l": "agent::OpenRulesLibrary" - } - }, - { - "context": "AgentPanel", - "use_key_equivalents": true, - "bindings": { - "cmd-n": "agent::NewThread", - "cmd-alt-n": "agent::NewTextThread", - "cmd-shift-h": "agent::OpenHistory", - "cmd-alt-c": "agent::OpenSettings", - "cmd-alt-l": "agent::OpenRulesLibrary", - "cmd-alt-p": "agent::ManageProfiles", - "cmd-i": "agent::ToggleProfileSelector", - "cmd-alt-/": "agent::ToggleModelSelector", - "cmd-shift-j": "agent::ToggleNavigationMenu", - "cmd-alt-m": "agent::ToggleOptionsMenu", - "cmd-alt-shift-n": "agent::ToggleNewThreadMenu", - "shift-alt-escape": "agent::ExpandMessageEditor", - "cmd->": "agent::AddSelectionToThread", - "cmd-shift-e": "project_panel::ToggleFocus", - "cmd-ctrl-b": "agent::ToggleBurnMode", - "cmd-shift-enter": "agent::ContinueThread", - "alt-enter": "agent::ContinueWithBurnMode", - "cmd-y": "agent::AllowOnce", - "cmd-alt-y": "agent::AllowAlways", - "cmd-alt-z": "agent::RejectOnce" - } - }, - { - "context": "AgentPanel > NavigationMenu", - "bindings": { - "shift-backspace": "agent::DeleteRecentlyOpenThread" - } - }, - { - "context": "AgentPanel > Markdown", - "use_key_equivalents": true, - "bindings": { - "cmd-c": "markdown::CopyAsMarkdown" - } - }, - { - "context": "AgentPanel && text_thread", - "use_key_equivalents": true, - "bindings": { - "cmd-n": "agent::NewTextThread", - "cmd-alt-n": "agent::NewExternalAgentThread" - } - }, - { - "context": "AgentPanel && acp_thread", - "use_key_equivalents": true, - "bindings": { - "cmd-n": "agent::NewExternalAgentThread", - "cmd-alt-t": "agent::NewThread" - } - }, - { - "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send", - "use_key_equivalents": true, - "bindings": { - "enter": "agent::Chat", - "cmd-enter": "agent::ChatWithFollow", - "cmd-i": "agent::ToggleProfileSelector", - "shift-ctrl-r": "agent::OpenAgentDiff", - "cmd-shift-y": "agent::KeepAll", - "cmd-shift-n": "agent::RejectAll" - } - }, - { - "context": "MessageEditor && !Picker > Editor && use_modifier_to_send", - "use_key_equivalents": true, - "bindings": { - "cmd-enter": "agent::Chat", - "enter": "editor::Newline", - "cmd-i": "agent::ToggleProfileSelector", - "shift-ctrl-r": "agent::OpenAgentDiff", - "cmd-shift-y": "agent::KeepAll", - "cmd-shift-n": "agent::RejectAll" - } - }, - { - "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": "AgentConfiguration", - "bindings": { - "ctrl--": "pane::GoBack" - } - }, - { - "context": "AcpThread > ModeSelector", - "bindings": { - "cmd-enter": "menu::Confirm" - } - }, - { - "context": "AcpThread > Editor && !use_modifier_to_send", - "use_key_equivalents": true, - "bindings": { - "enter": "agent::Chat", - "shift-ctrl-r": "agent::OpenAgentDiff", - "cmd-shift-y": "agent::KeepAll", - "cmd-shift-n": "agent::RejectAll", - "shift-tab": "agent::CycleModeSelector" - } - }, - { - "context": "AcpThread > Editor && use_modifier_to_send", - "use_key_equivalents": true, - "bindings": { - "cmd-enter": "agent::Chat", - "shift-ctrl-r": "agent::OpenAgentDiff", - "cmd-shift-y": "agent::KeepAll", - "cmd-shift-n": "agent::RejectAll", - "shift-tab": "agent::CycleModeSelector" - } - }, - { - "context": "ThreadHistory", - "bindings": { - "ctrl--": "pane::GoBack" - } - }, - { - "context": "ThreadHistory > Editor", - "bindings": { - "shift-backspace": "agent::RemoveSelectedThread" - } - }, - { - "context": "RulesLibrary", - "use_key_equivalents": true, - "bindings": { - "cmd-n": "rules_library::NewRule", - "cmd-shift-s": "rules_library::ToggleDefaultRule", - "cmd-w": "workspace::CloseWindow" - } - }, - { - "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", - "cmd-f": "search::FocusSearch", - "cmd-alt-f": "search::ToggleReplace", - "cmd-alt-l": "search::ToggleSelection", - "cmd-shift-o": "outline::Toggle" - } - }, - { - "context": "BufferSearchBar && in_replace > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "search::ReplaceNext", - "cmd-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", - "cmd-shift-j": "project_search::ToggleFilters", - "shift-enter": "project_search::ToggleAllSearchResults", - "cmd-shift-f": "search::FocusSearch", - "cmd-shift-h": "search::ToggleReplace", - "alt-cmd-g": "search::ToggleRegex", - "alt-cmd-x": "search::ToggleRegex" - } - }, - { - "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", - "cmd-enter": "search::ReplaceAll" - } - }, - { - "context": "ProjectSearchView", - "use_key_equivalents": true, - "bindings": { - "escape": "project_search::ToggleFocus", - "cmd-shift-j": "project_search::ToggleFilters", - "shift-enter": "project_search::ToggleAllSearchResults", - "cmd-shift-h": "search::ToggleReplace", - "alt-cmd-g": "search::ToggleRegex", - "alt-cmd-x": "search::ToggleRegex" - } - }, - { - "context": "Pane", - "use_key_equivalents": true, - "bindings": { - "alt-cmd-left": "pane::ActivatePreviousItem", - "cmd-{": "pane::ActivatePreviousItem", - "alt-cmd-right": "pane::ActivateNextItem", - "cmd-}": "pane::ActivateNextItem", - "ctrl-shift-pageup": "pane::SwapItemLeft", - "ctrl-shift-pagedown": "pane::SwapItemRight", - "cmd-w": ["pane::CloseActiveItem", { "close_pinned": false }], - "alt-cmd-t": ["pane::CloseOtherItems", { "close_pinned": false }], - "ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes", - "cmd-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }], - "cmd-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }], - "cmd-k u": ["pane::CloseCleanItems", { "close_pinned": false }], - "cmd-k w": ["pane::CloseAllItems", { "close_pinned": false }], - "cmd-k cmd-w": "workspace::CloseAllItemsAndPanes", - "cmd-f": "project_search::ToggleFocus", - "cmd-g": "search::SelectNextMatch", - "cmd-shift-g": "search::SelectPreviousMatch", - "cmd-shift-h": "search::ToggleReplace", - "cmd-alt-l": "search::ToggleSelection", - "alt-enter": "search::SelectAllMatches", - "alt-cmd-c": "search::ToggleCaseSensitive", - "alt-cmd-w": "search::ToggleWholeWord", - "alt-cmd-f": "project_search::ToggleFilters", - "alt-cmd-x": "search::ToggleRegex", - "cmd-k shift-enter": "pane::TogglePinTab" - } - }, - // Bindings from VS Code - { - "context": "Editor", - "use_key_equivalents": true, - "bindings": { - "cmd-[": "editor::Outdent", - "cmd-]": "editor::Indent", - "cmd-ctrl-p": ["editor::AddSelectionAbove", { "skip_soft_wrap": false }], // Insert cursor above - "cmd-alt-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }], - "cmd-ctrl-n": ["editor::AddSelectionBelow", { "skip_soft_wrap": false }], // Insert cursor below - "cmd-alt-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }], - "cmd-shift-k": "editor::DeleteLine", - "alt-up": "editor::MoveLineUp", - "alt-down": "editor::MoveLineDown", - "alt-shift-up": "editor::DuplicateLineUp", - "alt-shift-down": "editor::DuplicateLineDown", - "cmd-ctrl-left": "editor::SelectSmallerSyntaxNode", // Shrink selection - "cmd-ctrl-right": "editor::SelectLargerSyntaxNode", // Expand selection - "cmd-ctrl-up": "editor::SelectPreviousSyntaxNode", // Move selection up - "ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand selection (VSCode version) - "ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink selection (VSCode version) - "cmd-ctrl-down": "editor::SelectNextSyntaxNode", // Move selection down - "cmd-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand - "cmd-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection - "cmd-f2": "editor::SelectAllMatches", // Select all occurrences of current word - "cmd-k cmd-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip - // macOS binds `ctrl-cmd-d` to Show Dictionary which breaks these two binds - // To use `ctrl-cmd-d` or `ctrl-k ctrl-cmd-d` in Zed you must execute this command and then restart: - // defaults write com.apple.symbolichotkeys AppleSymbolicHotKeys -dict-add 70 'enabled' - "ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch - "cmd-k ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch - "cmd-k cmd-i": "editor::Hover", - "cmd-k cmd-b": "editor::BlameHover", - "cmd-/": ["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", - "cmd-f12": "editor::GoToTypeDefinition", - "shift-f12": "editor::GoToImplementation", - "alt-cmd-f12": "editor::GoToTypeDefinitionSplit", - "alt-shift-f12": "editor::FindAllReferences", - "cmd-|": "editor::MoveToEnclosingBracket", - "ctrl-m": "editor::MoveToEnclosingBracket", // From Jetbrains - "alt-cmd-[": "editor::Fold", - "alt-cmd-]": "editor::UnfoldLines", - "cmd-k cmd-l": "editor::ToggleFold", - "cmd-k cmd-[": "editor::FoldRecursive", - "cmd-k cmd-]": "editor::UnfoldRecursive", - "cmd-k cmd-1": "editor::FoldAtLevel_1", - "cmd-k cmd-2": "editor::FoldAtLevel_2", - "cmd-k cmd-3": "editor::FoldAtLevel_3", - "cmd-k cmd-4": "editor::FoldAtLevel_4", - "cmd-k cmd-5": "editor::FoldAtLevel_5", - "cmd-k cmd-6": "editor::FoldAtLevel_6", - "cmd-k cmd-7": "editor::FoldAtLevel_7", - "cmd-k cmd-8": "editor::FoldAtLevel_8", - "cmd-k cmd-9": "editor::FoldAtLevel_9", - "cmd-k cmd-0": "editor::FoldAll", - "cmd-k cmd-j": "editor::UnfoldAll", - // Using `ctrl-space` / `ctrl-shift-space` in Zed requires disabling the macOS global shortcut. - // System Preferences->Keyboard->Keyboard Shortcuts->Input Sources->Select the previous input source (uncheck) - "ctrl-space": "editor::ShowCompletions", - "ctrl-shift-space": "editor::ShowWordCompletions", - "cmd-.": "editor::ToggleCodeActions", - "cmd-k r": "editor::RevealInFileManager", - "cmd-k p": "editor::CopyPath", - "cmd-\\": "pane::SplitRight" - } - }, - { - "context": "Editor && extension == md", - "use_key_equivalents": true, - "bindings": { - "cmd-k v": "markdown::OpenPreviewToTheSide", - "cmd-shift-v": "markdown::OpenPreview" - } - }, - { - "context": "Editor && extension == svg", - "use_key_equivalents": true, - "bindings": { - "cmd-k v": "svg::OpenPreviewToTheSide", - "cmd-shift-v": "svg::OpenPreview" - } - }, - { - "context": "Editor && mode == full", - "use_key_equivalents": true, - "bindings": { - "cmd-shift-o": "outline::Toggle", - "ctrl-g": "go_to_line::Toggle", - "cmd-shift-backspace": "editor::GoToPreviousChange", - "cmd-shift-alt-backspace": "editor::GoToNextChange" - } - }, - { - "context": "Pane", - "use_key_equivalents": true, - "bindings": { - "ctrl-1": ["pane::ActivateItem", 0], - "ctrl-2": ["pane::ActivateItem", 1], - "ctrl-3": ["pane::ActivateItem", 2], - "ctrl-4": ["pane::ActivateItem", 3], - "ctrl-5": ["pane::ActivateItem", 4], - "ctrl-6": ["pane::ActivateItem", 5], - "ctrl-7": ["pane::ActivateItem", 6], - "ctrl-8": ["pane::ActivateItem", 7], - "ctrl-9": ["pane::ActivateItem", 8], - "ctrl-0": "pane::ActivateLastItem", - "ctrl--": "pane::GoBack", - "ctrl-_": "pane::GoForward", - "cmd-shift-f": "pane::DeploySearch" - } - }, - { - "context": "Workspace", - "use_key_equivalents": true, - "bindings": { - // Change the default action on `menu::Confirm` by setting the parameter - // "alt-cmd-o": ["projects::OpenRecent", {"create_new_window": true }], - "alt-cmd-o": ["projects::OpenRecent", { "create_new_window": false }], - "ctrl-cmd-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }], - "ctrl-cmd-shift-o": ["projects::OpenRemote", { "from_existing_connection": true, "create_new_window": false }], - "cmd-ctrl-b": "branches::OpenRecent", - "ctrl-~": "workspace::NewTerminal", - "cmd-s": "workspace::Save", - "cmd-k s": "workspace::SaveWithoutFormat", - "alt-shift-enter": "toast::RunAction", - "cmd-shift-s": "workspace::SaveAs", - "cmd-shift-n": "workspace::NewWindow", - "ctrl-`": "terminal_panel::Toggle", - "cmd-1": ["workspace::ActivatePane", 0], - "cmd-2": ["workspace::ActivatePane", 1], - "cmd-3": ["workspace::ActivatePane", 2], - "cmd-4": ["workspace::ActivatePane", 3], - "cmd-5": ["workspace::ActivatePane", 4], - "cmd-6": ["workspace::ActivatePane", 5], - "cmd-7": ["workspace::ActivatePane", 6], - "cmd-8": ["workspace::ActivatePane", 7], - "cmd-9": ["workspace::ActivatePane", 8], - "cmd-b": "workspace::ToggleLeftDock", - "cmd-alt-b": "workspace::ToggleRightDock", - "cmd-r": "workspace::ToggleRightDock", - "cmd-j": "workspace::ToggleBottomDock", - "alt-cmd-y": "workspace::ToggleAllDocks", - // For 0px parameter, uses UI font size value. - "ctrl-alt-0": "workspace::ResetActiveDockSize", - "ctrl-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }], - "ctrl-alt-=": ["workspace::IncreaseActiveDockSize", { "px": 0 }], - "ctrl-alt-)": "workspace::ResetOpenDocksSize", - "ctrl-alt-_": ["workspace::DecreaseOpenDocksSize", { "px": 0 }], - "ctrl-alt-+": ["workspace::IncreaseOpenDocksSize", { "px": 0 }], - "cmd-shift-f": "pane::DeploySearch", - "cmd-shift-h": ["pane::DeploySearch", { "replace_enabled": true }], - "cmd-shift-t": "pane::ReopenClosedItem", - "cmd-k cmd-s": "zed::OpenKeymap", - "cmd-k cmd-t": "theme_selector::Toggle", - "ctrl-alt-cmd-p": "settings_profile_selector::Toggle", - "cmd-t": "project_symbols::Toggle", - "cmd-p": "file_finder::Toggle", - "ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }], - "ctrl-tab": "tab_switcher::Toggle", - "cmd-shift-p": "command_palette::Toggle", - "cmd-shift-m": "diagnostics::Deploy", - "cmd-shift-e": "project_panel::ToggleFocus", - "cmd-shift-b": "outline_panel::ToggleFocus", - "ctrl-shift-g": "git_panel::ToggleFocus", - "cmd-shift-d": "debug_panel::ToggleFocus", - "cmd-?": "agent::ToggleFocus", - "cmd-alt-s": "workspace::SaveAll", - "cmd-k m": "language_selector::Toggle", - "cmd-k cmd-m": "toolchain::AddToolchain", - "escape": "workspace::Unfollow", - "cmd-k cmd-left": "workspace::ActivatePaneLeft", - "cmd-k cmd-right": "workspace::ActivatePaneRight", - "cmd-k cmd-up": "workspace::ActivatePaneUp", - "cmd-k cmd-down": "workspace::ActivatePaneDown", - "cmd-k shift-left": "workspace::SwapPaneLeft", - "cmd-k shift-right": "workspace::SwapPaneRight", - "cmd-k shift-up": "workspace::SwapPaneUp", - "cmd-k shift-down": "workspace::SwapPaneDown", - "cmd-shift-x": "zed::Extensions", - "f5": "debugger::Rerun", - "cmd-w": "workspace::CloseActiveDock" - } - }, - { - "context": "Workspace && !Terminal", - "use_key_equivalents": true, - "bindings": { - "cmd-n": "workspace::NewFile", - "cmd-shift-r": "task::Spawn", - // All task parameters are captured and unchanged between reruns by default. - // Use the `"reevaluate_context"` parameter to control this. - "cmd-alt-r": ["task::Rerun", { "reevaluate_context": false }], - "ctrl-alt-shift-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" }], - } - }, - { - "context": "Workspace && debugger_running", - "use_key_equivalents": true, - "bindings": { - "f5": "zed::NoAction", - "f11": "debugger::StepInto" - } - }, - { - "context": "Workspace && debugger_stopped", - "use_key_equivalents": true, - "bindings": { - "f5": "debugger::Continue" - } - }, - // Bindings from Sublime Text - { - "context": "Editor", - "use_key_equivalents": true, - "bindings": { - "cmd-u": "editor::UndoSelection", - "cmd-shift-u": "editor::RedoSelection", - "ctrl-j": "editor::JoinLines", - "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart", - "ctrl-alt-h": "editor::DeleteToPreviousSubwordStart", - "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd", - "ctrl-alt-d": "editor::DeleteToNextSubwordEnd", - "ctrl-alt-left": "editor::MoveToPreviousSubwordStart", - "ctrl-alt-b": "editor::MoveToPreviousSubwordStart", - "ctrl-alt-right": "editor::MoveToNextSubwordEnd", - "ctrl-alt-f": "editor::MoveToNextSubwordEnd", - "ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart", - "ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart", - "ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd", - "ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd" - } - }, - // Bindings from Atom - { - "context": "Pane", - "use_key_equivalents": true, - "bindings": { - "cmd-k up": "pane::SplitUp", - "cmd-k down": "pane::SplitDown", - "cmd-k left": "pane::SplitLeft", - "cmd-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" - } - }, - { - "context": "Editor && in_snippet && has_next_tabstop && !showing_completions", - "use_key_equivalents": true, - "bindings": { - "tab": "editor::NextSnippetTabstop" - } - }, - { - "context": "Editor && in_snippet && has_previous_tabstop && !showing_completions", - "use_key_equivalents": true, - "bindings": { - "shift-tab": "editor::PreviousSnippetTabstop" - } - }, - { - "context": "Editor && edit_prediction", - "bindings": { - "alt-tab": "editor::AcceptEditPrediction", - "tab": "editor::AcceptEditPrediction", - "ctrl-cmd-right": "editor::AcceptPartialEditPrediction" - } - }, - { - "context": "Editor && edit_prediction_conflict", - "use_key_equivalents": true, - "bindings": { - "alt-tab": "editor::AcceptEditPrediction", - "ctrl-cmd-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": { - "up": "editor::ContextMenuPrevious", - "ctrl-p": "editor::ContextMenuPrevious", - "down": "editor::ContextMenuNext", - "ctrl-n": "editor::ContextMenuNext", - "pageup": "editor::ContextMenuFirst", - "pagedown": "editor::ContextMenuLast" - } - }, - { - "context": "Editor && showing_signature_help && !showing_completions", - "bindings": { - "up": "editor::SignatureHelpPrevious", - "down": "editor::SignatureHelpNext" - } - }, - // Custom bindings - { - "use_key_equivalents": true, - "bindings": { - "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator", - // TODO: Move this to a dock open action - "cmd-shift-c": "collab_panel::ToggleFocus", - // Only available in debug builds: opens an element inspector for development. - "cmd-alt-i": "dev::ToggleInspector" - } - }, - { - "context": "!ContextEditor > Editor && mode == full", - "use_key_equivalents": true, - "bindings": { - "alt-enter": "editor::OpenExcerpts", - "shift-enter": "editor::ExpandExcerpts", - "cmd-alt-enter": "editor::OpenExcerptsSplit", - "cmd-shift-e": "pane::RevealInProjectPanel", - "cmd-f8": "editor::GoToHunk", - "cmd-shift-f8": "editor::GoToPreviousHunk", - "ctrl-enter": "assistant::InlineAssist", - "ctrl-:": "editor::ToggleInlayHints" - } - }, - { - "context": "PromptEditor", - "use_key_equivalents": true, - "bindings": { - "cmd-alt-/": "agent::ToggleModelSelector", - "ctrl-[": "agent::CyclePreviousInlineAssist", - "ctrl-]": "agent::CycleNextInlineAssist", - "cmd-shift-enter": "inline_assistant::ThumbsUpResult", - "cmd-shift-backspace": "inline_assistant::ThumbsDownResult" - } - }, - { - "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": { - "cmd-enter": "project_search::SearchInNew" - } - }, - { - "context": "OutlinePanel && not_editing", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "left": "outline_panel::CollapseSelectedEntry", - "right": "outline_panel::ExpandSelectedEntry", - "cmd-alt-c": "workspace::CopyPath", - "alt-cmd-shift-c": "workspace::CopyRelativePath", - "alt-cmd-r": "outline_panel::RevealInFileManager", - "space": "outline_panel::OpenSelectedEntry", - "shift-down": "menu::SelectNext", - "shift-up": "menu::SelectPrevious", - "alt-enter": "editor::OpenExcerpts", - "cmd-alt-enter": "editor::OpenExcerptsSplit" - } - }, - { - "context": "ProjectPanel", - "use_key_equivalents": true, - "bindings": { - "left": "project_panel::CollapseSelectedEntry", - "cmd-left": "project_panel::CollapseAllEntries", - "right": "project_panel::ExpandSelectedEntry", - "cmd-n": "project_panel::NewFile", - "cmd-d": "project_panel::Duplicate", - "alt-cmd-n": "project_panel::NewDirectory", - "cmd-x": "project_panel::Cut", - "cmd-c": "project_panel::Copy", - "cmd-v": "project_panel::Paste", - "cmd-alt-c": "workspace::CopyPath", - "alt-cmd-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 }], - "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", - "cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }], - "cmd-alt-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": "VariableList", - "use_key_equivalents": true, - "bindings": { - "left": "variable_list::CollapseSelectedEntry", - "right": "variable_list::ExpandSelectedEntry", - "enter": "variable_list::EditVariable", - "cmd-c": "variable_list::CopyVariableValue", - "cmd-alt-c": "variable_list::CopyVariableName", - "delete": "variable_list::RemoveWatch", - "backspace": "variable_list::RemoveWatch", - "alt-enter": "variable_list::AddWatch" - } - }, - { - "context": "GitPanel && ChangesList", - "use_key_equivalents": true, - "bindings": { - "up": "menu::SelectPrevious", - "down": "menu::SelectNext", - "cmd-up": "menu::SelectFirst", - "cmd-down": "menu::SelectLast", - "enter": "menu::Confirm", - "cmd-alt-y": "git::ToggleStaged", - "space": "git::ToggleStaged", - "shift-space": "git::StageRange", - "cmd-y": "git::StageFile", - "cmd-shift-y": "git::UnstageFile", - "alt-down": "git_panel::FocusEditor", - "tab": "git_panel::FocusEditor", - "shift-tab": "git_panel::FocusEditor", - "escape": "git_panel::ToggleFocus", - "backspace": ["git::RestoreFile", { "skip_prompt": false }], - "delete": ["git::RestoreFile", { "skip_prompt": false }], - "cmd-backspace": ["git::RestoreFile", { "skip_prompt": true }], - "cmd-delete": ["git::RestoreFile", { "skip_prompt": true }] - } - }, - { - "context": "GitPanel && CommitEditor", - "use_key_equivalents": true, - "bindings": { - "escape": "git::Cancel" - } - }, - { - "context": "GitDiff > Editor", - "use_key_equivalents": true, - "bindings": { - "cmd-enter": "git::Commit", - "cmd-shift-enter": "git::Amend", - "cmd-ctrl-y": "git::StageAll", - "cmd-ctrl-shift-y": "git::UnstageAll" - } - }, - { - "context": "CommitEditor > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "editor::Newline", - "cmd-enter": "git::Commit", - "cmd-shift-enter": "git::Amend", - "tab": "git_panel::FocusChanges", - "shift-tab": "git_panel::FocusChanges", - "alt-up": "git_panel::FocusChanges", - "shift-escape": "git::ExpandCommitEditor", - "alt-tab": "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-down": "git::PullRebase", - "ctrl-g shift-up": "git::ForcePush", - "ctrl-g d": "git::Diff", - "ctrl-g backspace": "git::RestoreTrackedFiles", - "ctrl-g shift-backspace": "git::TrashUntrackedFiles", - "cmd-ctrl-y": "git::StageAll", - "cmd-ctrl-shift-y": "git::UnstageAll", - "cmd-enter": "git::Commit", - "cmd-shift-enter": "git::Amend" - } - }, - { - "context": "GitCommit > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "editor::Newline", - "escape": "menu::Cancel", - "cmd-enter": "git::Commit", - "cmd-shift-enter": "git::Amend", - "alt-tab": "git::GenerateCommitMessage" - } - }, - { - "context": "DebugPanel", - "bindings": { - "cmd-t": "debugger::ToggleThreadPicker", - "cmd-i": "debugger::ToggleSessionPicker", - "shift-alt-escape": "debugger::ToggleExpandItem" - } - }, - { - "context": "BreakpointList", - "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", - "alt-enter": "collab_panel::OpenSelectedChannelNotes" - } - }, - { - "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 }], - "cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }] - } - }, - { - "context": "ChannelModal > Picker > Editor", - "use_key_equivalents": true, - "bindings": { - "tab": "channel_modal::ToggleMode" - } - }, - { - "context": "ToolchainSelector", - "use_key_equivalents": true, - "bindings": { - "cmd-shift-a": "toolchain::AddToolchain" - } - }, - { - "context": "FileFinder || (FileFinder > Picker > Editor)", - "use_key_equivalents": true, - "bindings": { - "cmd-shift-a": "file_finder::ToggleSplitMenu", - "cmd-shift-i": "file_finder::ToggleFilterMenu" - } - }, - { - "context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)", - "use_key_equivalents": true, - "bindings": { - "cmd-shift-p": "file_finder::SelectPrevious", - "cmd-j": "pane::SplitDown", - "cmd-k": "pane::SplitUp", - "cmd-h": "pane::SplitLeft", - "cmd-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": "StashList || (StashList > Picker > Editor)", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-backspace": "stash_picker::DropStashItem", - "ctrl-shift-v": "stash_picker::ShowStashItem" - } - }, - { - "context": "Terminal", - "use_key_equivalents": true, - "bindings": { - "ctrl-cmd-space": "terminal::ShowCharacterPalette", - "cmd-c": "terminal::Copy", - "cmd-v": "terminal::Paste", - "cmd-f": "buffer_search::Deploy", - "cmd-a": "editor::SelectAll", - "cmd-k": "terminal::Clear", - "cmd-n": "workspace::NewTerminal", - "ctrl-enter": "assistant::InlineAssist", - "ctrl-_": null, // emacs undo - // Some nice conveniences - "cmd-backspace": ["terminal::SendText", "\u0015"], // ctrl-u: clear line - "alt-delete": ["terminal::SendText", "\u001bd"], // alt-d: delete word forward - "cmd-delete": ["terminal::SendText", "\u000b"], // ctrl-k: delete to end of line - "cmd-right": ["terminal::SendText", "\u0005"], - "cmd-left": ["terminal::SendText", "\u0001"], - // Terminal.app compatibility - "alt-left": ["terminal::SendText", "\u001bb"], - "alt-right": ["terminal::SendText", "\u001bf"], - "alt-b": ["terminal::SendText", "\u001bb"], - "alt-f": ["terminal::SendText", "\u001bf"], - "ctrl-delete": ["terminal::SendText", "\u001bd"], - // There are conflicting bindings for these keys in the global context. - // these bindings override them, remove at your own risk: - "up": ["terminal::SendKeystroke", "up"], - "pageup": ["terminal::SendKeystroke", "pageup"], - "down": ["terminal::SendKeystroke", "down"], - "pagedown": ["terminal::SendKeystroke", "pagedown"], - "escape": ["terminal::SendKeystroke", "escape"], - "enter": ["terminal::SendKeystroke", "enter"], - "ctrl-c": ["terminal::SendKeystroke", "ctrl-c"], - "ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"], - "shift-pageup": "terminal::ScrollPageUp", - "cmd-up": "terminal::ScrollPageUp", - "shift-pagedown": "terminal::ScrollPageDown", - "cmd-down": "terminal::ScrollPageDown", - "shift-up": "terminal::ScrollLineUp", - "shift-down": "terminal::ScrollLineDown", - "shift-home": "terminal::ScrollToTop", - "cmd-home": "terminal::ScrollToTop", - "shift-end": "terminal::ScrollToBottom", - "cmd-end": "terminal::ScrollToBottom", - // Using `ctrl-shift-space` in Zed requires disabling the macOS global shortcut. - // System Preferences->Keyboard->Keyboard Shortcuts->Input Sources->Select the previous input source (uncheck) - "ctrl-shift-space": "terminal::ToggleViMode", - "ctrl-alt-up": "pane::SplitUp", - "ctrl-alt-down": "pane::SplitDown", - "ctrl-alt-left": "pane::SplitLeft", - "ctrl-alt-right": "pane::SplitRight", - "cmd-d": "pane::SplitRight", - "cmd-alt-r": "terminal::RerunTask" - } - }, - { - "context": "RatePredictionsModal", - "use_key_equivalents": true, - "bindings": { - "cmd-shift-enter": "zeta::ThumbsUpActivePrediction", - "cmd-shift-backspace": "zeta::ThumbsDownActivePrediction", - "shift-down": "zeta::NextEdit", - "shift-up": "zeta::PreviousEdit", - "right": "zeta::PreviewPrediction" - } - }, - { - "context": "RatePredictionsModal > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "zeta::FocusPredictions", - "cmd-shift-enter": "zeta::ThumbsUpActivePrediction", - "cmd-shift-backspace": "zeta::ThumbsDownActivePrediction" - } - }, - { - "context": "ZedPredictModal", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel" - } - }, - { - "context": "ConfigureContextServerModal > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "enter": "editor::Newline", - "cmd-enter": "menu::Confirm" - } - }, - { - "context": "ContextServerToolsModal", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel" - } - }, - { - "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", - "bindings": { - "pageup": "markdown::ScrollPageUp", - "pagedown": "markdown::ScrollPageDown", - "up": "markdown::ScrollUp", - "down": "markdown::ScrollDown", - "alt-up": "markdown::ScrollUpByItem", - "alt-down": "markdown::ScrollDownByItem" - } - }, - { - "context": "KeymapEditor", - "use_key_equivalents": true, - "bindings": { - "cmd-f": "search::FocusSearch", - "cmd-alt-f": "keymap_editor::ToggleKeystrokeSearch", - "cmd-alt-c": "keymap_editor::ToggleConflictFilter", - "enter": "keymap_editor::EditBinding", - "alt-enter": "keymap_editor::CreateBinding", - "cmd-c": "keymap_editor::CopyAction", - "cmd-shift-c": "keymap_editor::CopyContext", - "cmd-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": { - "cmd-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": { - "cmd-=": ["zed::IncreaseUiFontSize", { "persist": false }], - "cmd-+": ["zed::IncreaseUiFontSize", { "persist": false }], - "cmd--": ["zed::DecreaseUiFontSize", { "persist": false }], - "cmd-0": ["zed::ResetUiFontSize", { "persist": false }], - "cmd-enter": "onboarding::Finish", - "alt-tab": "onboarding::SignIn", - "alt-shift-a": "onboarding::OpenAccount" - } - }, - { - "context": "Welcome", - "use_key_equivalents": true, - "bindings": { - "cmd-=": ["zed::IncreaseUiFontSize", { "persist": false }], - "cmd-+": ["zed::IncreaseUiFontSize", { "persist": false }], - "cmd--": ["zed::DecreaseUiFontSize", { "persist": false }], - "cmd-0": ["zed::ResetUiFontSize", { "persist": false }] - } - }, - { - "context": "InvalidBuffer", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-enter": "workspace::OpenWithSystem" - } - }, - { - "context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-space": "git::WorktreeFromDefaultOnWindow", - "ctrl-space": "git::WorktreeFromDefault" - } - }, - { - "context": "SettingsWindow", - "use_key_equivalents": true, - "bindings": { - "cmd-w": "workspace::CloseWindow", - "escape": "workspace::CloseWindow", - "cmd-m": "settings_editor::Minimize", - "cmd-f": "search::FocusSearch", - "cmd-,": "settings_editor::OpenCurrentFile", - "left": "settings_editor::ToggleFocusNav", - "cmd-shift-e": "settings_editor::ToggleFocusNav", - // todo(settings_ui): cut this down based on the max files and overflow UI - "ctrl-1": ["settings_editor::FocusFile", 0], - "ctrl-2": ["settings_editor::FocusFile", 1], - "ctrl-3": ["settings_editor::FocusFile", 2], - "ctrl-4": ["settings_editor::FocusFile", 3], - "ctrl-5": ["settings_editor::FocusFile", 4], - "ctrl-6": ["settings_editor::FocusFile", 5], - "ctrl-7": ["settings_editor::FocusFile", 6], - "ctrl-8": ["settings_editor::FocusFile", 7], - "ctrl-9": ["settings_editor::FocusFile", 8], - "ctrl-0": ["settings_editor::FocusFile", 9], - "cmd-{": "settings_editor::FocusPreviousFile", - "cmd-}": "settings_editor::FocusNextFile" - } - }, - { - "context": "StashDiff > Editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-space": "git::ApplyCurrentStash", - "ctrl-shift-space": "git::PopCurrentStash", - "ctrl-shift-backspace": "git::DropCurrentStash" - } - }, - { - "context": "SettingsWindow > NavigationMenu", - "use_key_equivalents": true, - "bindings": { - "up": "settings_editor::FocusPreviousNavEntry", - "shift-tab": "settings_editor::FocusPreviousNavEntry", - "down": "settings_editor::FocusNextNavEntry", - "tab": "settings_editor::FocusNextNavEntry", - "right": "settings_editor::ExpandNavEntry", - "left": "settings_editor::CollapseNavEntry", - "pageup": "settings_editor::FocusPreviousRootNavEntry", - "pagedown": "settings_editor::FocusNextRootNavEntry", - "home": "settings_editor::FocusFirstNavEntry", - "end": "settings_editor::FocusLastNavEntry" - } - }, - { - "context": "EditPredictionContext > Editor", - "bindings": { - "alt-left": "dev::EditPredictionContextGoBack", - "alt-right": "dev::EditPredictionContextGoForward" - } - }, - { - "context": "GitBranchSelector || (GitBranchSelector > Picker > Editor)", - "use_key_equivalents": true, - "bindings": { - "cmd-shift-backspace": "branch_picker::DeleteBranch", - "cmd-shift-i": "branch_picker::FilterRemotes" - } - } -] diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json deleted file mode 100644 index 51943ab355..0000000000 --- a/assets/keymaps/default-windows.json +++ /dev/null @@ -1,1370 +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-c": "menu::Cancel", - "escape": "menu::Cancel", - "shift-alt-enter": "menu::Restart", - "alt-enter": ["picker::ConfirmInput", { "secondary": false }], - "ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }], - "ctrl-shift-w": "workspace::CloseWindow", - "shift-escape": "workspace::ToggleZoom", - "ctrl-o": "workspace::OpenFiles", - "ctrl-k 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-alt-,": "zed::OpenSettingsFile", - "ctrl-q": "zed::Quit", - "f4": "debugger::Start", - "shift-f5": "debugger::Stop", - "ctrl-shift-f5": "debugger::RerunSession", - "f6": "debugger::Pause", - "f10": "debugger::StepOver", - "shift-f11": "debugger::StepOut", - "f11": "zed::ToggleFullScreen", - "ctrl-shift-i": "edit_prediction::ToggleMenu", - "shift-alt-l": "lsp_tool::ToggleMenu", - "ctrl-shift-alt-c": "editor::DisplayCursorNames" - } - }, - { - "context": "Picker || menu", - "use_key_equivalents": true, - "bindings": { - "up": "menu::SelectPrevious", - "down": "menu::SelectNext" - } - }, - { - "context": "Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "editor::Cancel", - "shift-backspace": "editor::Backspace", - "backspace": "editor::Backspace", - "delete": "editor::Delete", - "tab": "editor::Tab", - "shift-tab": "editor::Backtab", - "ctrl-k": "editor::CutToEndOfLine", - "ctrl-k ctrl-q": "editor::Rewrap", - "ctrl-k q": "editor::Rewrap", - "ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }], - "ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], - "shift-delete": "editor::Cut", - "ctrl-x": "editor::Cut", - "ctrl-insert": "editor::Copy", - "ctrl-c": "editor::Copy", - "shift-insert": "editor::Paste", - "ctrl-v": "editor::Paste", - "ctrl-z": "editor::Undo", - "ctrl-y": "editor::Redo", - "ctrl-shift-z": "editor::Redo", - "up": "editor::MoveUp", - "ctrl-up": "editor::LineUp", - "ctrl-down": "editor::LineDown", - "pageup": "editor::MovePageUp", - "alt-pageup": "editor::PageUp", - "shift-pageup": "editor::SelectPageUp", - "home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }], - "down": "editor::MoveDown", - "pagedown": "editor::MovePageDown", - "alt-pagedown": "editor::PageDown", - "shift-pagedown": "editor::SelectPageDown", - "end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": true }], - "left": "editor::MoveLeft", - "right": "editor::MoveRight", - "ctrl-left": "editor::MoveToPreviousWordStart", - "ctrl-right": "editor::MoveToNextWordEnd", - "ctrl-home": "editor::MoveToBeginning", - "ctrl-end": "editor::MoveToEnd", - "shift-up": "editor::SelectUp", - "shift-down": "editor::SelectDown", - "shift-left": "editor::SelectLeft", - "shift-right": "editor::SelectRight", - "ctrl-shift-left": "editor::SelectToPreviousWordStart", - "ctrl-shift-right": "editor::SelectToNextWordEnd", - "ctrl-shift-home": "editor::SelectToBeginning", - "ctrl-shift-end": "editor::SelectToEnd", - "ctrl-a": "editor::SelectAll", - "ctrl-l": "editor::SelectLine", - "shift-alt-f": "editor::Format", - "shift-alt-o": "editor::OrganizeImports", - "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }], - "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }], - "ctrl-alt-space": "editor::ShowCharacterPalette", - "ctrl-;": "editor::ToggleLineNumbers", - "ctrl-'": "editor::ToggleSelectedDiffHunks", - "ctrl-\"": "editor::ExpandAllDiffHunks", - "ctrl-i": "editor::ShowSignatureHelp", - "alt-g b": "git::Blame", - "alt-g m": "git::OpenModifiedFiles", - "menu": "editor::OpenContextMenu", - "shift-f10": "editor::OpenContextMenu", - "ctrl-alt-e": "editor::ToggleEditPrediction", - "f9": "editor::ToggleBreakpoint", - "shift-f9": "editor::EditLogBreakpoint" - } - }, - { - "context": "Editor && mode == full", - "use_key_equivalents": true, - "bindings": { - "shift-enter": "editor::Newline", - "enter": "editor::Newline", - "ctrl-enter": "editor::NewlineBelow", - "ctrl-shift-enter": "editor::NewlineAbove", - "ctrl-k ctrl-z": "editor::ToggleSoftWrap", - "ctrl-k z": "editor::ToggleSoftWrap", - "ctrl-f": "buffer_search::Deploy", - "ctrl-h": "buffer_search::DeployReplace", - "ctrl-shift-.": "agent::AddSelectionToThread", - "ctrl-shift-,": "assistant::InsertIntoEditor", - "shift-alt-e": "editor::SelectEnclosingSymbol", - "ctrl-shift-backspace": "editor::GoToPreviousChange", - "ctrl-shift-alt-backspace": "editor::GoToNextChange", - "alt-enter": "editor::OpenSelectionsInMultibuffer" - } - }, - { - "context": "Editor && mode == full && edit_prediction", - "use_key_equivalents": true, - "bindings": { - "alt-]": "editor::NextEditPrediction", - "alt-[": "editor::PreviousEditPrediction" - } - }, - { - "context": "Editor && !edit_prediction", - "use_key_equivalents": true, - "bindings": { - "alt-\\": "editor::ShowEditPrediction" - } - }, - { - "context": "Editor && mode == auto_height", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "editor::Newline", - "shift-enter": "editor::Newline", - "ctrl-shift-enter": "editor::NewlineBelow" - } - }, - { - "context": "Markdown", - "use_key_equivalents": true, - "bindings": { - "ctrl-c": "markdown::Copy" - } - }, - { - "context": "Editor && jupyter && !ContextEditor", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-enter": "repl::Run", - "ctrl-alt-enter": "repl::RunInPlace" - } - }, - { - "context": "Editor && !agent_diff", - "use_key_equivalents": true, - "bindings": { - "ctrl-k ctrl-r": "git::Restore", - "alt-y": "git::StageAndNext", - "shift-alt-y": "git::UnstageAndNext" - } - }, - { - "context": "Editor && editor_agent_diff", - "use_key_equivalents": true, - "bindings": { - "ctrl-y": "agent::Keep", - "ctrl-n": "agent::Reject", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll", - "ctrl-shift-r": "agent::OpenAgentDiff" - } - }, - { - "context": "AgentDiff", - "use_key_equivalents": true, - "bindings": { - "ctrl-y": "agent::Keep", - "ctrl-n": "agent::Reject", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "ContextEditor > Editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-i": "assistant::Assist", - "ctrl-s": "workspace::Save", - "ctrl-shift-,": "assistant::InsertIntoEditor", - "shift-enter": "assistant::Split", - "ctrl-r": "assistant::CycleMessageRole", - "enter": "assistant::ConfirmCommand", - "alt-enter": "editor::Newline", - "ctrl-k c": "assistant::CopyCode", - "ctrl-g": "search::SelectNextMatch", - "ctrl-shift-g": "search::SelectPreviousMatch", - "ctrl-k l": "agent::OpenRulesLibrary" - } - }, - { - "context": "AgentPanel", - "use_key_equivalents": true, - "bindings": { - "ctrl-n": "agent::NewThread", - "shift-alt-n": "agent::NewTextThread", - "ctrl-shift-h": "agent::OpenHistory", - "shift-alt-c": "agent::OpenSettings", - "shift-alt-l": "agent::OpenRulesLibrary", - "shift-alt-p": "agent::ManageProfiles", - "ctrl-i": "agent::ToggleProfileSelector", - "shift-alt-/": "agent::ToggleModelSelector", - "shift-alt-j": "agent::ToggleNavigationMenu", - "shift-alt-i": "agent::ToggleOptionsMenu", - "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu", - "shift-alt-escape": "agent::ExpandMessageEditor", - "ctrl-shift-.": "agent::AddSelectionToThread", - "ctrl-shift-e": "project_panel::ToggleFocus", - "ctrl-shift-enter": "agent::ContinueThread", - "super-ctrl-b": "agent::ToggleBurnMode", - "alt-enter": "agent::ContinueWithBurnMode", - "shift-alt-a": "agent::AllowOnce", - "ctrl-alt-y": "agent::AllowAlways", - "shift-alt-z": "agent::RejectOnce" - } - }, - { - "context": "AgentPanel > NavigationMenu", - "use_key_equivalents": true, - "bindings": { - "shift-backspace": "agent::DeleteRecentlyOpenThread" - } - }, - { - "context": "AgentPanel > Markdown", - "use_key_equivalents": true, - "bindings": { - "ctrl-c": "markdown::CopyAsMarkdown" - } - }, - { - "context": "AgentPanel && text_thread", - "use_key_equivalents": true, - "bindings": { - "ctrl-n": "agent::NewTextThread", - "ctrl-alt-t": "agent::NewThread" - } - }, - { - "context": "AgentPanel && acp_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": "AcpThread > ModeSelector", - "bindings": { - "ctrl-enter": "menu::Confirm" - } - }, - { - "context": "AcpThread > Editor && !use_modifier_to_send", - "use_key_equivalents": true, - "bindings": { - "enter": "agent::Chat", - "ctrl-shift-r": "agent::OpenAgentDiff", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll", - "shift-tab": "agent::CycleModeSelector" - } - }, - { - "context": "AcpThread > Editor && use_modifier_to_send", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "agent::Chat", - "ctrl-shift-r": "agent::OpenAgentDiff", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll", - "shift-tab": "agent::CycleModeSelector" - } - }, - { - "context": "ThreadHistory", - "use_key_equivalents": true, - "bindings": { - "backspace": "agent::RemoveSelectedThread" - } - }, - { - "context": "RulesLibrary", - "use_key_equivalents": true, - "bindings": { - "ctrl-n": "rules_library::NewRule", - "ctrl-shift-s": "rules_library::ToggleDefaultRule", - "ctrl-w": "workspace::CloseWindow" - } - }, - { - "context": "BufferSearchBar", - "use_key_equivalents": true, - "bindings": { - "escape": "buffer_search::Dismiss", - "tab": "buffer_search::FocusEditor", - "enter": "search::SelectNextMatch", - "shift-enter": "search::SelectPreviousMatch", - "alt-enter": "search::SelectAllMatches", - "ctrl-f": "search::FocusSearch", - "ctrl-h": "search::ToggleReplace", - "ctrl-l": "search::ToggleSelection" - } - }, - { - "context": "BufferSearchBar && in_replace > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "search::ReplaceNext", - "ctrl-enter": "search::ReplaceAll" - } - }, - { - "context": "BufferSearchBar && !in_replace > Editor", - "use_key_equivalents": true, - "bindings": { - "up": "search::PreviousHistoryQuery", - "down": "search::NextHistoryQuery" - } - }, - { - "context": "ProjectSearchBar", - "use_key_equivalents": true, - "bindings": { - "escape": "project_search::ToggleFocus", - "ctrl-shift-f": "search::FocusSearch", - "ctrl-shift-h": "search::ToggleReplace", - "alt-r": "search::ToggleRegex" // vscode - } - }, - { - "context": "ProjectSearchBar > Editor", - "use_key_equivalents": true, - "bindings": { - "up": "search::PreviousHistoryQuery", - "down": "search::NextHistoryQuery" - } - }, - { - "context": "ProjectSearchBar && in_replace > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "search::ReplaceNext", - "ctrl-alt-enter": "search::ReplaceAll" - } - }, - { - "context": "ProjectSearchView", - "use_key_equivalents": true, - "bindings": { - "escape": "project_search::ToggleFocus", - "ctrl-shift-h": "search::ToggleReplace", - "alt-r": "search::ToggleRegex" // vscode - } - }, - { - "context": "Pane", - "use_key_equivalents": true, - "bindings": { - "alt-1": ["pane::ActivateItem", 0], - "alt-2": ["pane::ActivateItem", 1], - "alt-3": ["pane::ActivateItem", 2], - "alt-4": ["pane::ActivateItem", 3], - "alt-5": ["pane::ActivateItem", 4], - "alt-6": ["pane::ActivateItem", 5], - "alt-7": ["pane::ActivateItem", 6], - "alt-8": ["pane::ActivateItem", 7], - "alt-9": ["pane::ActivateItem", 8], - "alt-0": "pane::ActivateLastItem", - "ctrl-pageup": "pane::ActivatePreviousItem", - "ctrl-pagedown": "pane::ActivateNextItem", - "ctrl-shift-pageup": "pane::SwapItemLeft", - "ctrl-shift-pagedown": "pane::SwapItemRight", - "ctrl-f4": ["pane::CloseActiveItem", { "close_pinned": false }], - "ctrl-w": ["pane::CloseActiveItem", { "close_pinned": false }], - "ctrl-shift-alt-t": ["pane::CloseOtherItems", { "close_pinned": false }], - "ctrl-shift-alt-w": "workspace::CloseInactiveTabsAndPanes", - "ctrl-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }], - "ctrl-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }], - "ctrl-k u": ["pane::CloseCleanItems", { "close_pinned": false }], - "ctrl-k w": ["pane::CloseAllItems", { "close_pinned": false }], - "ctrl-k ctrl-w": "workspace::CloseAllItemsAndPanes", - "back": "pane::GoBack", - "alt--": "pane::GoBack", - "forward": "pane::GoForward", - "alt-=": "pane::GoForward", - "f3": "search::SelectNextMatch", - "shift-f3": "search::SelectPreviousMatch", - "ctrl-shift-f": "project_search::ToggleFocus", - "shift-alt-h": "search::ToggleReplace", - "alt-l": "search::ToggleSelection", - "alt-enter": "search::SelectAllMatches", - "alt-c": "search::ToggleCaseSensitive", - "alt-w": "search::ToggleWholeWord", - "alt-f": "project_search::ToggleFilters", - "shift-enter": "project_search::ToggleAllSearchResults", - "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-alt-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }], // Insert Cursor Above - "ctrl-alt-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }], // 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-f3": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand - "ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip - "ctrl-shift-f3": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand - "ctrl-k ctrl-i": "editor::Hover", - "ctrl-k ctrl-b": "editor::BlameHover", - "ctrl-k ctrl-f": "editor::FormatSelections", - "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-f12": "editor::GoToImplementation", - "shift-alt-f12": "editor::FindAllReferences", - "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", - "alt-.": "editor::GoToHunk", - "alt-,": "editor::GoToPreviousHunk", - } - }, - { - "context": "Editor && extension == md", - "use_key_equivalents": true, - "bindings": { - "ctrl-k v": "markdown::OpenPreviewToTheSide", - "ctrl-shift-v": "markdown::OpenPreview" - } - }, - { - "context": "Editor && extension == svg", - "use_key_equivalents": true, - "bindings": { - "ctrl-k v": "svg::OpenPreviewToTheSide", - "ctrl-shift-v": "svg::OpenPreview" - } - }, - { - "context": "Editor && mode == full", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-o": "outline::Toggle", - "ctrl-g": "go_to_line::Toggle" - } - }, - { - "context": "Workspace", - "use_key_equivalents": true, - "bindings": { - // Change the default action on `menu::Confirm` by setting the parameter - // "ctrl-alt-o": ["projects::OpenRecent", { "create_new_window": true }], - "ctrl-r": ["projects::OpenRecent", { "create_new_window": false }], - // Change to open path modal for existing remote connection by setting the parameter - // "ctrl-shift-alt-o": "["projects::OpenRemote", { "from_existing_connection": true }]", - "ctrl-shift-alt-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }], - "shift-alt-b": "branches::OpenRecent", - "shift-alt-enter": "toast::RunAction", - "ctrl-shift-`": "workspace::NewTerminal", - "ctrl-s": "workspace::Save", - "ctrl-k ctrl-shift-s": "workspace::SaveWithoutFormat", - "ctrl-shift-s": "workspace::SaveAs", - "ctrl-n": "workspace::NewFile", - "ctrl-shift-n": "workspace::NewWindow", - "ctrl-`": "terminal_panel::Toggle", - "f10": ["app_menu::OpenApplicationMenu", "Zed"], - "alt-1": ["workspace::ActivatePane", 0], - "alt-2": ["workspace::ActivatePane", 1], - "alt-3": ["workspace::ActivatePane", 2], - "alt-4": ["workspace::ActivatePane", 3], - "alt-5": ["workspace::ActivatePane", 4], - "alt-6": ["workspace::ActivatePane", 5], - "alt-7": ["workspace::ActivatePane", 6], - "alt-8": ["workspace::ActivatePane", 7], - "alt-9": ["workspace::ActivatePane", 8], - "ctrl-alt-b": "workspace::ToggleRightDock", - "ctrl-b": "workspace::ToggleLeftDock", - "ctrl-j": "workspace::ToggleBottomDock", - "ctrl-shift-y": "workspace::ToggleAllDocks", - "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-f": "pane::DeploySearch", - "ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }], - "ctrl-shift-t": "pane::ReopenClosedItem", - "ctrl-k ctrl-s": "zed::OpenKeymap", - "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-shift-tab": ["tab_switcher::Toggle", { "select_last": true }], - "ctrl-tab": "tab_switcher::Toggle", - "ctrl-e": "file_finder::Toggle", - "f1": "command_palette::Toggle", - "ctrl-shift-p": "command_palette::Toggle", - "ctrl-shift-m": "diagnostics::Deploy", - "ctrl-shift-e": "project_panel::ToggleFocus", - "ctrl-shift-b": "outline_panel::ToggleFocus", - "ctrl-shift-g": "git_panel::ToggleFocus", - "ctrl-shift-d": "debug_panel::ToggleFocus", - "ctrl-shift-/": "agent::ToggleFocus", - "ctrl-k s": "workspace::SaveAll", - "ctrl-k m": "language_selector::Toggle", - "ctrl-m ctrl-m": "toolchain::AddToolchain", - "escape": "workspace::Unfollow", - "ctrl-k ctrl-left": "workspace::ActivatePaneLeft", - "ctrl-k ctrl-right": "workspace::ActivatePaneRight", - "ctrl-k ctrl-up": "workspace::ActivatePaneUp", - "ctrl-k ctrl-down": "workspace::ActivatePaneDown", - "ctrl-k shift-left": "workspace::SwapPaneLeft", - "ctrl-k shift-right": "workspace::SwapPaneRight", - "ctrl-k shift-up": "workspace::SwapPaneUp", - "ctrl-k shift-down": "workspace::SwapPaneDown", - "ctrl-shift-x": "zed::Extensions", - // All task parameters are captured and unchanged between reruns by default. - // Use the `"reevaluate_context"` parameter to control this. - "ctrl-shift-r": ["task::Rerun", { "reevaluate_context": false }], - "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" - } - }, - { - "context": "Editor && in_snippet && has_next_tabstop && !showing_completions", - "use_key_equivalents": true, - "bindings": { - "tab": "editor::NextSnippetTabstop" - } - }, - { - "context": "Editor && in_snippet && has_previous_tabstop && !showing_completions", - "use_key_equivalents": true, - "bindings": { - "shift-tab": "editor::PreviousSnippetTabstop" - } - }, - // 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", - "ctrl-shift-enter": "inline_assistant::ThumbsUpResult", - "ctrl-shift-delete": "inline_assistant::ThumbsDownResult" - } - }, - { - "context": "Prompt", - "use_key_equivalents": true, - "bindings": { - "left": "menu::SelectPrevious", - "right": "menu::SelectNext", - "h": "menu::SelectPrevious", - "l": "menu::SelectNext" - } - }, - { - "context": "ProjectSearchBar && !in_replace", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "project_search::SearchInNew" - } - }, - { - "context": "OutlinePanel && not_editing", - "use_key_equivalents": true, - "bindings": { - "left": "outline_panel::CollapseSelectedEntry", - "right": "outline_panel::ExpandSelectedEntry", - "shift-alt-c": "outline_panel::CopyPath", - "ctrl-shift-alt-c": "workspace::CopyRelativePath", - "ctrl-alt-r": "outline_panel::RevealInFileManager", - "space": "outline_panel::OpenSelectedEntry", - "shift-down": "menu::SelectNext", - "shift-up": "menu::SelectPrevious", - "alt-enter": "editor::OpenExcerpts", - "ctrl-alt-enter": "editor::OpenExcerptsSplit" - } - }, - { - "context": "ProjectPanel", - "use_key_equivalents": true, - "bindings": { - "left": "project_panel::CollapseSelectedEntry", - "ctrl-left": "project_panel::CollapseAllEntries", - "right": "project_panel::ExpandSelectedEntry", - "ctrl-n": "project_panel::NewFile", - "alt-n": "project_panel::NewDirectory", - "ctrl-x": "project_panel::Cut", - "ctrl-insert": "project_panel::Copy", - "ctrl-c": "project_panel::Copy", - "shift-insert": "project_panel::Paste", - "ctrl-v": "project_panel::Paste", - "shift-alt-c": "project_panel::CopyPath", - "ctrl-k ctrl-shift-c": "workspace::CopyRelativePath", - "enter": "project_panel::Rename", - "f2": "project_panel::Rename", - "backspace": ["project_panel::Trash", { "skip_prompt": false }], - "delete": ["project_panel::Trash", { "skip_prompt": false }], - "shift-delete": ["project_panel::Delete", { "skip_prompt": false }], - "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }], - "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }], - "ctrl-alt-r": "project_panel::RevealInFileManager", - "ctrl-shift-enter": "project_panel::OpenWithSystem", - "alt-d": "project_panel::CompareMarkedFiles", - "ctrl-k ctrl-shift-f": "project_panel::NewSearchInDirectory", - "shift-down": "menu::SelectNext", - "shift-up": "menu::SelectPrevious", - "escape": "menu::Cancel" - } - }, - { - "context": "ProjectPanel && not_editing", - "use_key_equivalents": true, - "bindings": { - "space": "project_panel::Open" - } - }, - { - "context": "GitPanel && ChangesList", - "use_key_equivalents": true, - "bindings": { - "up": "menu::SelectPrevious", - "down": "menu::SelectNext", - "enter": "menu::Confirm", - "alt-y": "git::StageFile", - "shift-alt-y": "git::UnstageFile", - "space": "git::ToggleStaged", - "shift-space": "git::StageRange", - "tab": "git_panel::FocusEditor", - "shift-tab": "git_panel::FocusEditor", - "escape": "git_panel::ToggleFocus", - "alt-enter": "menu::SecondaryConfirm", - "delete": ["git::RestoreFile", { "skip_prompt": false }], - "backspace": ["git::RestoreFile", { "skip_prompt": false }], - "shift-delete": ["git::RestoreFile", { "skip_prompt": false }], - "ctrl-backspace": ["git::RestoreFile", { "skip_prompt": false }], - "ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }] - } - }, - { - "context": "GitPanel && CommitEditor", - "use_key_equivalents": true, - "bindings": { - "escape": "git::Cancel" - } - }, - { - "context": "GitCommit > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "enter": "editor::Newline", - "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend", - "alt-l": "git::GenerateCommitMessage" - } - }, - { - "context": "GitPanel", - "use_key_equivalents": true, - "bindings": { - "ctrl-g ctrl-g": "git::Fetch", - "ctrl-g up": "git::Push", - "ctrl-g down": "git::Pull", - "ctrl-g shift-down": "git::PullRebase", - "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", - "alt-enter": "collab_panel::OpenSelectedChannelNotes" - } - }, - { - "context": "(CollabPanel && editing) > Editor", - "use_key_equivalents": true, - "bindings": { - "space": "collab_panel::InsertSpace" - } - }, - { - "context": "ChannelModal", - "use_key_equivalents": true, - "bindings": { - "tab": "channel_modal::ToggleMode" - } - }, - { - "context": "Picker > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "up": "menu::SelectPrevious", - "down": "menu::SelectNext", - "tab": "picker::ConfirmCompletion", - "alt-enter": ["picker::ConfirmInput", { "secondary": false }] - } - }, - { - "context": "ChannelModal > Picker > Editor", - "use_key_equivalents": true, - "bindings": { - "tab": "channel_modal::ToggleMode" - } - }, - { - "context": "ToolchainSelector", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-a": "toolchain::AddToolchain" - } - }, - { - "context": "FileFinder || (FileFinder > Picker > Editor)", - "use_key_equivalents": true, - "bindings": { - "ctrl-p": "file_finder::Toggle", - "ctrl-shift-a": "file_finder::ToggleSplitMenu", - "ctrl-shift-i": "file_finder::ToggleFilterMenu" - } - }, - { - "context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-p": "file_finder::SelectPrevious", - "ctrl-j": "pane::SplitDown", - "ctrl-k": "pane::SplitUp", - "ctrl-h": "pane::SplitLeft", - "ctrl-l": "pane::SplitRight" - } - }, - { - "context": "TabSwitcher", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-tab": "menu::SelectPrevious", - "ctrl-up": "menu::SelectPrevious", - "ctrl-down": "menu::SelectNext", - "ctrl-backspace": "tab_switcher::CloseSelectedItem" - } - }, - { - "context": "StashList || (StashList > Picker > Editor)", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-backspace": "stash_picker::DropStashItem", - "ctrl-shift-v": "stash_picker::ShowStashItem" - } - }, - { - "context": "Terminal", - "use_key_equivalents": true, - "bindings": { - "ctrl-alt-space": "terminal::ShowCharacterPalette", - "ctrl-insert": "terminal::Copy", - "ctrl-shift-c": "terminal::Copy", - "shift-insert": "terminal::Paste", - "ctrl-v": "terminal::Paste", - "ctrl-shift-v": "terminal::Paste", - "ctrl-i": "assistant::InlineAssist", - "alt-b": ["terminal::SendText", "\u001bb"], - "alt-f": ["terminal::SendText", "\u001bf"], - "alt-.": ["terminal::SendText", "\u001b."], - "ctrl-delete": ["terminal::SendText", "\u001bd"], - "ctrl-n": "workspace::NewTerminal", - // 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-q": ["terminal::SendKeystroke", "ctrl-q"], - "ctrl-r": ["terminal::SendKeystroke", "ctrl-r"], - "ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"], - "ctrl-shift-a": "editor::SelectAll", - "ctrl-shift-f": "buffer_search::Deploy", - "ctrl-shift-l": "terminal::Clear", - "ctrl-shift-w": "pane::CloseActiveItem", - "up": ["terminal::SendKeystroke", "up"], - "pageup": ["terminal::SendKeystroke", "pageup"], - "down": ["terminal::SendKeystroke", "down"], - "pagedown": ["terminal::SendKeystroke", "pagedown"], - "escape": ["terminal::SendKeystroke", "escape"], - "enter": ["terminal::SendKeystroke", "enter"], - "shift-pageup": "terminal::ScrollPageUp", - "shift-pagedown": "terminal::ScrollPageDown", - "shift-up": "terminal::ScrollLineUp", - "shift-down": "terminal::ScrollLineDown", - "shift-home": "terminal::ScrollToTop", - "shift-end": "terminal::ScrollToBottom", - "ctrl-shift-space": "terminal::ToggleViMode", - "ctrl-shift-r": "terminal::RerunTask", - "ctrl-alt-r": "terminal::RerunTask", - "alt-t": "terminal::RerunTask", - "ctrl-shift-5": "pane::SplitRight" - } - }, - { - "context": "Terminal && selection", - "bindings": { - "ctrl-c": "terminal::Copy" - } - }, - { - "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": "ContextServerToolsModal", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel" - } - }, - { - "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::ScrollPageUp", - "pagedown": "markdown::ScrollPageDown", - "up": "markdown::ScrollUp", - "down": "markdown::ScrollDown", - "alt-up": "markdown::ScrollUpByItem", - "alt-down": "markdown::ScrollDownByItem" - } - }, - { - "context": "KeymapEditor", - "use_key_equivalents": true, - "bindings": { - "ctrl-f": "search::FocusSearch", - "alt-f": "keymap_editor::ToggleKeystrokeSearch", - "alt-c": "keymap_editor::ToggleConflictFilter", - "enter": "keymap_editor::EditBinding", - "alt-enter": "keymap_editor::CreateBinding", - "ctrl-c": "keymap_editor::CopyAction", - "ctrl-shift-c": "keymap_editor::CopyContext", - "ctrl-t": "keymap_editor::ShowMatchingKeybinds" - } - }, - { - "context": "KeystrokeInput", - "use_key_equivalents": true, - "bindings": { - "enter": "keystroke_input::StartRecording", - "escape escape escape": "keystroke_input::StopRecording", - "delete": "keystroke_input::ClearKeystrokes" - } - }, - { - "context": "KeybindEditorModal", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "menu::Confirm", - "escape": "menu::Cancel" - } - }, - { - "context": "KeybindEditorModal > Editor", - "use_key_equivalents": true, - "bindings": { - "up": "menu::SelectPrevious", - "down": "menu::SelectNext" - } - }, - { - "context": "Onboarding", - "use_key_equivalents": true, - "bindings": { - "ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }], - "ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }], - "ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }], - "ctrl-0": ["zed::ResetUiFontSize", { "persist": false }], - "ctrl-enter": "onboarding::Finish", - "alt-shift-l": "onboarding::SignIn", - "shift-alt-a": "onboarding::OpenAccount" - } - }, - { - "context": "Welcome", - "use_key_equivalents": true, - "bindings": { - "ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }], - "ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }], - "ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }], - "ctrl-0": ["zed::ResetUiFontSize", { "persist": false }] - } - }, - { - "context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-space": "git::WorktreeFromDefaultOnWindow", - "ctrl-space": "git::WorktreeFromDefault" - } - }, - { - "context": "SettingsWindow", - "use_key_equivalents": true, - "bindings": { - "ctrl-w": "workspace::CloseWindow", - "escape": "workspace::CloseWindow", - "ctrl-m": "settings_editor::Minimize", - "ctrl-f": "search::FocusSearch", - "ctrl-,": "settings_editor::OpenCurrentFile", - "left": "settings_editor::ToggleFocusNav", - "ctrl-shift-e": "settings_editor::ToggleFocusNav", - // todo(settings_ui): cut this down based on the max files and overflow UI - "ctrl-1": ["settings_editor::FocusFile", 0], - "ctrl-2": ["settings_editor::FocusFile", 1], - "ctrl-3": ["settings_editor::FocusFile", 2], - "ctrl-4": ["settings_editor::FocusFile", 3], - "ctrl-5": ["settings_editor::FocusFile", 4], - "ctrl-6": ["settings_editor::FocusFile", 5], - "ctrl-7": ["settings_editor::FocusFile", 6], - "ctrl-8": ["settings_editor::FocusFile", 7], - "ctrl-9": ["settings_editor::FocusFile", 8], - "ctrl-0": ["settings_editor::FocusFile", 9], - "ctrl-pageup": "settings_editor::FocusPreviousFile", - "ctrl-pagedown": "settings_editor::FocusNextFile" - } - }, - { - "context": "StashDiff > Editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-space": "git::ApplyCurrentStash", - "ctrl-shift-space": "git::PopCurrentStash", - "ctrl-shift-backspace": "git::DropCurrentStash" - } - }, - { - "context": "SettingsWindow > NavigationMenu", - "use_key_equivalents": true, - "bindings": { - "up": "settings_editor::FocusPreviousNavEntry", - "shift-tab": "settings_editor::FocusPreviousNavEntry", - "down": "settings_editor::FocusNextNavEntry", - "tab": "settings_editor::FocusNextNavEntry", - "right": "settings_editor::ExpandNavEntry", - "left": "settings_editor::CollapseNavEntry", - "pageup": "settings_editor::FocusPreviousRootNavEntry", - "pagedown": "settings_editor::FocusNextRootNavEntry", - "home": "settings_editor::FocusFirstNavEntry", - "end": "settings_editor::FocusLastNavEntry" - } - }, - { - "context": "EditPredictionContext > Editor", - "bindings": { - "alt-left": "dev::EditPredictionContextGoBack", - "alt-right": "dev::EditPredictionContextGoForward" - } - }, - { - "context": "GitBranchSelector || (GitBranchSelector > Picker > Editor)", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-backspace": "branch_picker::DeleteBranch", - "ctrl-shift-i": "branch_picker::FilterRemotes" - } - } -] diff --git a/assets/keymaps/initial.json b/assets/keymaps/initial.json deleted file mode 100644 index 8e4fe59f44..0000000000 --- a/assets/keymaps/initial.json +++ /dev/null @@ -1,21 +0,0 @@ -// Zed keymap -// -// For information on binding keys, see the Zed -// documentation: https://zed.dev/docs/key-bindings -// -// To see the default key bindings run `zed: open default keymap` -// from the command palette. -[ - { - "context": "Workspace", - "bindings": { - // "shift shift": "file_finder::Toggle" - } - }, - { - "context": "Editor && vim_mode == insert", - "bindings": { - // "j k": "vim::NormalBefore" - } - } -] diff --git a/assets/keymaps/linux/atom.json b/assets/keymaps/linux/atom.json deleted file mode 100644 index 98992b19fa..0000000000 --- a/assets/keymaps/linux/atom.json +++ /dev/null @@ -1,96 +0,0 @@ -// Default Keymap (Atom) for Zed on Linux -[ - { - "bindings": { - "ctrl-shift-f5": "workspace::Reload", // window:reload - "ctrl-k ctrl-n": "workspace::ActivatePreviousPane", // window:focus-next-pane - "ctrl-k ctrl-p": "workspace::ActivateNextPane" // window:focus-previous-pane - } - }, - { - "context": "Editor", - "bindings": { - "ctrl-k ctrl-u": "editor::ConvertToUpperCase", // editor:upper-case - "ctrl-k ctrl-l": "editor::ConvertToLowerCase" // editor:lower-case - } - }, - { - "context": "Editor && mode == full", - "bindings": { - "ctrl-shift-l": "language_selector::Toggle", // grammar-selector:show - "ctrl-|": "pane::RevealInProjectPanel", // tree-view:reveal-active-file - "ctrl-b": "editor::GoToDefinition", // fuzzy-finder:toggle-buffer-finder - "ctrl-alt-b": "editor::GoToDefinitionSplit", // N/A: From JetBrains - "ctrl-<": "editor::ScrollCursorCenter", // editor:scroll-to-cursor - "f3": ["editor::SelectNext", { "replace_newest": true }], // find-and-replace:find-next - "shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous - "alt-shift-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }], // editor:add-selection-below - "alt-shift-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }], // editor:add-selection-above - "ctrl-j": "editor::JoinLines", // editor:join-lines - "ctrl-shift-d": "editor::DuplicateLineDown", // editor:duplicate-lines - "ctrl-up": "editor::MoveLineUp", // editor:move-line-up - "ctrl-down": "editor::MoveLineDown", // editor:move-line-down - "ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle - "ctrl-shift-m": "markdown::OpenPreviewToTheSide", // markdown-preview:toggle - "ctrl-r": "outline::Toggle" // symbols-view:toggle-project-symbols - } - }, - { - "context": "BufferSearchBar", - "bindings": { - "f3": ["editor::SelectNext", { "replace_newest": true }], // find-and-replace:find-next - "shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous - "ctrl-f3": "search::SelectNextMatch", // find-and-replace:find-next-selected - "ctrl-shift-f3": "search::SelectPreviousMatch" // find-and-replace:find-previous-selected - } - }, - { - "context": "Workspace", - "bindings": { - "ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle - "ctrl-k ctrl-b": "workspace::ToggleLeftDock", // tree-view:toggle - "ctrl-t": "file_finder::Toggle", // fuzzy-finder:toggle-file-finder - "ctrl-r": "project_symbols::Toggle" // symbols-view:toggle-project-symbols - } - }, - { - "context": "Pane", - "bindings": { - // "ctrl-0": "project_panel::ToggleFocus", // tree-view:toggle-focus - "ctrl-1": ["pane::ActivateItem", 0], // tree-view:open-selected-entry-in-pane-1 - "ctrl-2": ["pane::ActivateItem", 1], // tree-view:open-selected-entry-in-pane-2 - "ctrl-3": ["pane::ActivateItem", 2], // tree-view:open-selected-entry-in-pane-3 - "ctrl-4": ["pane::ActivateItem", 3], // tree-view:open-selected-entry-in-pane-4 - "ctrl-5": ["pane::ActivateItem", 4], // tree-view:open-selected-entry-in-pane-5 - "ctrl-6": ["pane::ActivateItem", 5], // tree-view:open-selected-entry-in-pane-6 - "ctrl-7": ["pane::ActivateItem", 6], // tree-view:open-selected-entry-in-pane-7 - "ctrl-8": ["pane::ActivateItem", 7], // tree-view:open-selected-entry-in-pane-8 - "ctrl-9": ["pane::ActivateItem", 8] // tree-view:open-selected-entry-in-pane-9 - } - }, - { - "context": "ProjectPanel", - "bindings": { - "f2": "project_panel::Rename", // tree-view:rename - "backspace": ["project_panel::Trash", { "skip_prompt": false }], - "ctrl-x": "project_panel::Cut", // tree-view:cut - "ctrl-c": "project_panel::Copy", // tree-view:copy - "ctrl-v": "project_panel::Paste" // tree-view:paste - } - }, - { - "context": "ProjectPanel && not_editing", - "bindings": { - "ctrl-shift-c": "project_panel::CopyPath", // tree-view:copy-full-path - "ctrl-[": "project_panel::CollapseSelectedEntry", // tree-view:collapse-directory - "ctrl-b": "project_panel::CollapseSelectedEntry", // tree-view:collapse-directory - "ctrl-]": "project_panel::ExpandSelectedEntry", // tree-view:expand-item - "ctrl-f": "project_panel::ExpandSelectedEntry", // tree-view:expand-item - "a": "project_panel::NewFile", // tree-view:add-file - "d": "project_panel::Duplicate", // tree-view:duplicate - "home": "menu::SelectFirst", // core:move-to-top - "end": "menu::SelectLast", // core:move-to-bottom - "shift-a": "project_panel::NewDirectory" // tree-view:add-folder - } - } -] diff --git a/assets/keymaps/linux/cursor.json b/assets/keymaps/linux/cursor.json deleted file mode 100644 index 4d2d13a90d..0000000000 --- a/assets/keymaps/linux/cursor.json +++ /dev/null @@ -1,83 +0,0 @@ -[ - // Cursor for MacOS. See: https://docs.cursor.com/kbd - { - "context": "Workspace", - "use_key_equivalents": true, - "bindings": { - "ctrl-i": "agent::ToggleFocus", - "ctrl-shift-i": "agent::ToggleFocus", - "ctrl-l": "agent::ToggleFocus", - "ctrl-shift-l": "agent::ToggleFocus", - "ctrl-shift-j": "agent::OpenSettings" - } - }, - { - "context": "Editor && mode == full", - "use_key_equivalents": true, - "bindings": { - "ctrl-i": "agent::ToggleFocus", - "ctrl-shift-i": "agent::ToggleFocus", - "ctrl-shift-l": "agent::AddSelectionToThread", // In cursor uses "Ask" mode - "ctrl-l": "agent::AddSelectionToThread", // In cursor uses "Agent" mode - "ctrl-k": "assistant::InlineAssist", - "ctrl-shift-k": "assistant::InsertIntoEditor" - } - }, - { - "context": "InlineAssistEditor", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-backspace": "editor::Cancel" - // "alt-enter": // Quick Question - // "ctrl-shift-enter": // Full File Context - // "ctrl-shift-k": // Toggle input focus (editor <> inline assist) - } - }, - { - "context": "AgentPanel || ContextEditor || (MessageEditor > Editor)", - "use_key_equivalents": true, - "bindings": { - "ctrl-i": "workspace::ToggleRightDock", - "ctrl-shift-i": "workspace::ToggleRightDock", - "ctrl-l": "workspace::ToggleRightDock", - "ctrl-shift-l": "workspace::ToggleRightDock", - "ctrl-w": "workspace::ToggleRightDock", // technically should close chat - "ctrl-.": "agent::ToggleProfileSelector", - "ctrl-/": "agent::ToggleModelSelector", - "ctrl-shift-backspace": "editor::Cancel", - "ctrl-r": "agent::NewThread", - "ctrl-shift-v": "editor::Paste", - "ctrl-shift-k": "assistant::InsertIntoEditor" - // "escape": "agent::ToggleFocus" - ///// Enable when Zed supports multiple thread tabs - // "ctrl-t": // new thread tab - // "ctrl-[": // next thread tab - // "ctrl-]": // next thread tab - ///// Enable if Zed adds support for keyboard navigation of thread elements - // "tab": // cycle to next message - // "shift-tab": // cycle to previous message - } - }, - { - "context": "Editor && editor_agent_diff", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "agent::KeepAll", - "ctrl-backspace": "agent::RejectAll" - } - }, - { - "context": "Editor && mode == full && edit_prediction", - "use_key_equivalents": true, - "bindings": { - "ctrl-right": "editor::AcceptPartialEditPrediction" - } - }, - { - "context": "Terminal", - "use_key_equivalents": true, - "bindings": { - "ctrl-k": "assistant::InlineAssist" - } - } -] diff --git a/assets/keymaps/linux/emacs.json b/assets/keymaps/linux/emacs.json deleted file mode 100755 index c5cf22c812..0000000000 --- a/assets/keymaps/linux/emacs.json +++ /dev/null @@ -1,206 +0,0 @@ -// documentation: https://zed.dev/docs/key-bindings -// -// To see the default key bindings run `zed: open default keymap` -// from the command palette. -[ - { - "bindings": { - "ctrl-g": "menu::Cancel" - } - }, - { - // Workaround to avoid falling back to default bindings. - // Unbind so Zed ignores these keys and lets emacs handle them. - // NOTE: must be declared before the `Editor` override. - // NOTE: in macos the 'ctrl-x' 'ctrl-p' and 'ctrl-n' rebindings are not needed, since they default to 'cmd'. - "context": "Editor", - "bindings": { - "ctrl-g": null, // currently activates `go_to_line::Toggle` when there is nothing to cancel - "ctrl-x": null, // currently activates `editor::Cut` if no following key is pressed for 1 second - "ctrl-p": null, // currently activates `file_finder::Toggle` when the cursor is on the first character of the buffer - "ctrl-n": null // currently activates `workspace::NewFile` when the cursor is on the last character of the buffer - } - }, - { - "context": "Editor", - "bindings": { - "ctrl-g": "editor::Cancel", - "alt-g g": "go_to_line::Toggle", // goto-line - "alt-g alt-g": "go_to_line::Toggle", // goto-line - "ctrl-space": "editor::SetMark", // set-mark - "ctrl-@": "editor::SetMark", // set-mark - "ctrl-x ctrl-x": "editor::SwapSelectionEnds", // exchange-point-and-mark - "ctrl-f": "editor::MoveRight", // forward-char - "ctrl-b": "editor::MoveLeft", // backward-char - "ctrl-n": "editor::MoveDown", // next-line - "ctrl-p": "editor::MoveUp", // previous-line - "home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line - "end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line - "ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line - "ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line - "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line - "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line - "alt-m": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_indent": true }], // back-to-indentation - "alt-left": "editor::MoveToPreviousWordStart", // left-word - "alt-right": "editor::MoveToNextWordEnd", // right-word - "alt-f": "editor::MoveToNextWordEnd", // forward-word - "alt-b": "editor::MoveToPreviousWordStart", // backward-word - "alt-u": "editor::ConvertToUpperCase", // upcase-word - "alt-l": "editor::ConvertToLowerCase", // downcase-word - "alt-c": "editor::ConvertToUpperCamelCase", // capitalize-word - "ctrl-t": "editor::Transpose", // transpose-chars - "alt-;": ["editor::ToggleComments", { "advance_downwards": false }], - "ctrl-x ctrl-;": "editor::ToggleComments", - "alt-.": "editor::GoToDefinition", // xref-find-definitions - "alt-?": "editor::FindAllReferences", // xref-find-references - "alt-,": "pane::GoBack", // xref-pop-marker-stack - "ctrl-x h": "editor::SelectAll", // mark-whole-buffer - "ctrl-d": "editor::Delete", // delete-char - "alt-d": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], // kill-word - "alt-backspace": "editor::DeleteToPreviousWordStart", // backward-kill-word - "alt-delete": "editor::DeleteToPreviousWordStart", // backward-kill-word - "ctrl-k": "editor::KillRingCut", // kill-line - "ctrl-w": "editor::Cut", // kill-region - "alt-w": "editor::Copy", // kill-ring-save - "ctrl-y": "editor::KillRingYank", // yank - "ctrl-_": "editor::Undo", // undo - "ctrl-/": "editor::Undo", // undo - "ctrl-x u": "editor::Undo", // undo - "alt-{": "editor::MoveToStartOfParagraph", // backward-paragraph - "alt-}": "editor::MoveToEndOfParagraph", // forward-paragraph - "ctrl-up": "editor::MoveToStartOfParagraph", // backward-paragraph - "ctrl-down": "editor::MoveToEndOfParagraph", // forward-paragraph - "ctrl-v": "editor::MovePageDown", // scroll-up - "alt-v": "editor::MovePageUp", // scroll-down - "ctrl-x [": "editor::MoveToBeginning", // beginning-of-buffer - "ctrl-x ]": "editor::MoveToEnd", // end-of-buffer - "alt-<": "editor::MoveToBeginning", // beginning-of-buffer - "alt->": "editor::MoveToEnd", // end-of-buffer - "ctrl-home": "editor::MoveToBeginning", // beginning-of-buffer - "ctrl-end": "editor::MoveToEnd", // end-of-buffer - "ctrl-l": "editor::ScrollCursorCenterTopBottom", // recenter-top-bottom - "ctrl-s": "buffer_search::Deploy", // isearch-forward - "ctrl-r": "buffer_search::Deploy", // isearch-backward - "alt-^": "editor::JoinLines", // join-line - "alt-q": "editor::Rewrap" // fill-paragraph - } - }, - { - "context": "Editor && selection_mode", // region selection - "bindings": { - "right": "editor::SelectRight", - "left": "editor::SelectLeft", - "down": "editor::SelectDown", - "up": "editor::SelectUp", - "alt-left": "editor::SelectToPreviousWordStart", - "alt-right": "editor::SelectToNextWordEnd", - "pagedown": "editor::SelectPageDown", - "ctrl-v": "editor::SelectPageDown", - "pageup": "editor::SelectPageUp", - "alt-v": "editor::SelectPageUp", - "ctrl-f": "editor::SelectRight", - "ctrl-b": "editor::SelectLeft", - "ctrl-n": "editor::SelectDown", - "ctrl-p": "editor::SelectUp", - "home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], - "end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], - "ctrl-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], - "ctrl-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], - "alt-m": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_indent": true }], - "alt-f": "editor::SelectToNextWordEnd", - "alt-b": "editor::SelectToPreviousWordStart", - "alt-{": "editor::SelectToStartOfParagraph", - "alt-}": "editor::SelectToEndOfParagraph", - "ctrl-up": "editor::SelectToStartOfParagraph", - "ctrl-down": "editor::SelectToEndOfParagraph", - "ctrl-x [": "editor::SelectToBeginning", - "ctrl-x ]": "editor::SelectToEnd", - "alt-<": "editor::SelectToBeginning", - "alt->": "editor::SelectToEnd", - "ctrl-home": "editor::SelectToBeginning", - "ctrl-end": "editor::SelectToEnd", - "ctrl-g": "editor::Cancel" - } - }, - { - "context": "Editor && (showing_code_actions || showing_completions)", - "bindings": { - "ctrl-p": "editor::ContextMenuPrevious", - "ctrl-n": "editor::ContextMenuNext" - } - }, - { - "context": "Editor && showing_signature_help && !showing_completions", - "bindings": { - "ctrl-p": "editor::SignatureHelpPrevious", - "ctrl-n": "editor::SignatureHelpNext" - } - }, - // Example setting for using emacs-style tab - // (i.e. indent the current line / selection or perform symbol completion depending on context) - // { - // "context": "Editor && !showing_code_actions && !showing_completions", - // "bindings": { - // "tab": "editor::AutoIndent" // indent-for-tab-command - // } - // }, - { - "context": "Workspace", - "bindings": { - "alt-x": "command_palette::Toggle", // execute-extended-command - "ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer - "ctrl-x ctrl-b": "tab_switcher::Toggle", // list-buffers - // "ctrl-x ctrl-c": "workspace::CloseWindow" // in case you only want to exit the current Zed instance - "ctrl-x ctrl-c": "zed::Quit", // save-buffers-kill-terminal - "ctrl-x 5 0": "workspace::CloseWindow", // delete-frame - "ctrl-x 5 2": "workspace::NewWindow", // make-frame-command - "ctrl-x o": "workspace::ActivateNextPane", // other-window - "ctrl-x k": "pane::CloseActiveItem", // kill-buffer - "ctrl-x 0": "pane::CloseActiveItem", // delete-window - // "ctrl-x 1": "pane::JoinAll", // in case you prefer to delete the splits but keep the buffers open - "ctrl-x 1": "pane::CloseOtherItems", // delete-other-windows - "ctrl-x 2": "pane::SplitDown", // split-window-below - "ctrl-x 3": "pane::SplitRight", // split-window-right - "ctrl-x ctrl-f": "file_finder::Toggle", // find-file - "ctrl-x ctrl-s": "workspace::Save", // save-buffer - "ctrl-x ctrl-w": "workspace::SaveAs", // write-file - "ctrl-x s": "workspace::SaveAll" // save-some-buffers - } - }, - { - // Workaround to enable using native emacs from the Zed terminal. - // Unbind so Zed ignores these keys and lets emacs handle them. - // NOTE: - // "terminal::SendKeystroke" only works for a single key stroke (e.g. ctrl-x), - // so override with null for compound sequences (e.g. ctrl-x ctrl-c). - "context": "Terminal", - "bindings": { - // If you want to perfect your emacs-in-zed setup, also consider the following. - // You may need to enable "option_as_meta" from the Zed settings for "alt-x" to work. - // "alt-x": ["terminal::SendKeystroke", "alt-x"], - // "ctrl-x": ["terminal::SendKeystroke", "ctrl-x"], - // "ctrl-n": ["terminal::SendKeystroke", "ctrl-n"], - // ... - "ctrl-x ctrl-c": null, // save-buffers-kill-terminal - "ctrl-x ctrl-f": null, // find-file - "ctrl-x ctrl-s": null, // save-buffer - "ctrl-x ctrl-w": null, // write-file - "ctrl-x s": null // save-some-buffers - } - }, - { - "context": "BufferSearchBar > Editor", - "bindings": { - "ctrl-s": "search::SelectNextMatch", - "ctrl-r": "search::SelectPreviousMatch", - "ctrl-g": "buffer_search::Dismiss" - } - }, - { - "context": "Pane", - "bindings": { - "ctrl-alt-left": "pane::GoBack", - "ctrl-alt-right": "pane::GoForward" - } - } -] diff --git a/assets/keymaps/linux/jetbrains.json b/assets/keymaps/linux/jetbrains.json deleted file mode 100644 index a0314c5bc1..0000000000 --- a/assets/keymaps/linux/jetbrains.json +++ /dev/null @@ -1,185 +0,0 @@ -[ - { - "bindings": { - "ctrl-alt-s": "zed::OpenSettings", - "ctrl-{": "pane::ActivatePreviousItem", - "ctrl-}": "pane::ActivateNextItem", - "shift-escape": null, // Unmap workspace::zoom - "ctrl-~": "git::Branch", - "ctrl-f2": "debugger::Stop", - "f6": "debugger::Pause", - "f7": "debugger::StepInto", - "f8": "debugger::StepOver", - "shift-f8": "debugger::StepOut", - "f9": "debugger::Continue", - "shift-f9": "debugger::Start", - "alt-shift-f9": "debugger::Start" - } - }, - { - "context": "Editor", - "bindings": { - "ctrl->": ["zed::IncreaseBufferFontSize", { "persist": true }], - "ctrl-<": ["zed::DecreaseBufferFontSize", { "persist": true }], - "ctrl-shift-j": "editor::JoinLines", - "ctrl-d": "editor::DuplicateSelection", - "ctrl-y": "editor::DeleteLine", - "ctrl-m": "editor::ScrollCursorCenter", - "ctrl-pagedown": "editor::MovePageDown", - "ctrl-pageup": "editor::MovePageUp", - // "ctrl-alt-shift-b": "editor::SelectToPreviousWordStart", - "ctrl-alt-enter": "editor::NewlineAbove", - "shift-enter": "editor::NewlineBelow", - // "ctrl--": "editor::Fold", // TODO: `ctrl-numpad--` (numpad not implemented) - // "ctrl-+": "editor::UnfoldLines", // TODO: `ctrl-numpad+` (numpad not implemented) - "alt-shift-g": "editor::SplitSelectionIntoLines", - "alt-j": ["editor::SelectNext", { "replace_newest": false }], - "alt-shift-j": ["editor::SelectPrevious", { "replace_newest": false }], - "ctrl-/": ["editor::ToggleComments", { "advance_downwards": true }], - "ctrl-w": "editor::SelectLargerSyntaxNode", - "ctrl-shift-w": "editor::SelectSmallerSyntaxNode", - "shift-alt-up": "editor::MoveLineUp", - "shift-alt-down": "editor::MoveLineDown", - "ctrl-alt-l": "editor::Format", - "ctrl-alt-o": "editor::OrganizeImports", - "shift-f6": "editor::Rename", - "ctrl-alt-left": "pane::GoBack", - "ctrl-alt-right": "pane::GoForward", - "alt-f7": "editor::FindAllReferences", - "ctrl-alt-f7": "editor::FindAllReferences", - "ctrl-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock - "ctrl-alt-b": "editor::GoToImplementation", // Conflicts with workspace::ToggleRightDock - "ctrl-shift-b": "editor::GoToTypeDefinition", - "ctrl-alt-shift-b": "editor::GoToTypeDefinitionSplit", - "f2": "editor::GoToDiagnostic", - "shift-f2": "editor::GoToPreviousDiagnostic", - "ctrl-alt-shift-down": "editor::GoToHunk", - "ctrl-alt-shift-up": "editor::GoToPreviousHunk", - "ctrl-alt-z": "git::Restore", - "ctrl-home": "editor::MoveToBeginning", - "ctrl-end": "editor::MoveToEnd", - "ctrl-shift-home": "editor::SelectToBeginning", - "ctrl-shift-end": "editor::SelectToEnd", - "ctrl-f8": "editor::ToggleBreakpoint", - "ctrl-shift-f8": "editor::EditLogBreakpoint", - "ctrl-shift-u": "editor::ToggleCase" - } - }, - { - "context": "Editor && mode == full", - "bindings": { - "ctrl-f12": "outline::Toggle", - "ctrl-r": ["buffer_search::Deploy", { "replace_enabled": true }], - "ctrl-shift-n": "file_finder::Toggle", - "ctrl-g": "go_to_line::Toggle", - "alt-enter": "editor::ToggleCodeActions", - "ctrl-space": "editor::ShowCompletions", - "ctrl-q": "editor::Hover", - "ctrl-p": "editor::ShowSignatureHelp", - "ctrl-\\": "assistant::InlineAssist" - } - }, - { - "context": "BufferSearchBar", - "bindings": { - "shift-enter": "search::SelectPreviousMatch" - } - }, - { - "context": "BufferSearchBar || ProjectSearchBar", - "bindings": { - "alt-c": "search::ToggleCaseSensitive", - "alt-e": "search::ToggleSelection", - "alt-x": "search::ToggleRegex", - "alt-w": "search::ToggleWholeWord" - } - }, - { - "context": "Workspace", - "bindings": { - "ctrl-shift-f12": "workspace::ToggleAllDocks", - "ctrl-shift-r": ["pane::DeploySearch", { "replace_enabled": true }], - "alt-shift-f10": "task::Spawn", - "shift-f10": "task::Spawn", - "ctrl-f5": "task::Rerun", - "ctrl-e": "file_finder::Toggle", - "ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor - "ctrl-shift-n": "file_finder::Toggle", - "ctrl-n": "project_symbols::Toggle", - "ctrl-alt-n": "file_finder::Toggle", - "ctrl-shift-a": "command_palette::Toggle", - "shift shift": "command_palette::Toggle", - "ctrl-alt-shift-n": "project_symbols::Toggle", - "alt-0": "git_panel::ToggleFocus", - "alt-1": "project_panel::ToggleFocus", - "alt-5": "debug_panel::ToggleFocus", - "alt-6": "diagnostics::Deploy", - "alt-7": "outline_panel::ToggleFocus" - } - }, - { - "context": "Pane", // this is to override the default Pane mappings to switch tabs - "bindings": { - "alt-1": "project_panel::ToggleFocus", - "alt-2": null, // Bookmarks (left dock) - "alt-3": null, // Find Panel (bottom dock) - "alt-4": null, // Run Panel (bottom dock) - "alt-5": "debug_panel::ToggleFocus", - "alt-6": "diagnostics::Deploy", - "alt-7": "outline_panel::ToggleFocus", - "alt-8": null, // Services (bottom dock) - "alt-9": null, // Git History (bottom dock) - "alt-0": "git_panel::ToggleFocus" - } - }, - { - "context": "Workspace || Editor", - "bindings": { - "alt-f12": "terminal_panel::Toggle", - "ctrl-shift-k": "git::Push" - } - }, - { - "context": "Pane", - "bindings": { - "ctrl-alt-left": "pane::GoBack", - "ctrl-alt-right": "pane::GoForward", - "alt-left": "pane::ActivatePreviousItem", - "alt-right": "pane::ActivateNextItem" - } - }, - { - "context": "ProjectPanel", - "bindings": { - "enter": "project_panel::Open", - "ctrl-shift-f": "project_panel::NewSearchInDirectory", - "backspace": ["project_panel::Trash", { "skip_prompt": false }], - "delete": ["project_panel::Trash", { "skip_prompt": false }], - "shift-delete": ["project_panel::Delete", { "skip_prompt": false }], - "shift-f6": "project_panel::Rename" - } - }, - { - "context": "Terminal", - "bindings": { - "ctrl-shift-t": "workspace::NewTerminal", - "alt-f12": "workspace::CloseActiveDock", - "ctrl-up": "terminal::ScrollLineUp", - "ctrl-down": "terminal::ScrollLineDown", - "shift-pageup": "terminal::ScrollPageUp", - "shift-pagedown": "terminal::ScrollPageDown" - } - }, - { "context": "GitPanel", "bindings": { "alt-0": "workspace::CloseActiveDock" } }, - { "context": "ProjectPanel", "bindings": { "alt-1": "workspace::CloseActiveDock" } }, - { "context": "DebugPanel", "bindings": { "alt-5": "workspace::CloseActiveDock" } }, - { "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)", - "bindings": { - "escape": "editor::ToggleFocus", - "shift-escape": "workspace::CloseActiveDock" - } - } -] diff --git a/assets/keymaps/linux/sublime_text.json b/assets/keymaps/linux/sublime_text.json deleted file mode 100644 index eefd59e5bd..0000000000 --- a/assets/keymaps/linux/sublime_text.json +++ /dev/null @@ -1,97 +0,0 @@ -[ - { - "bindings": { - "ctrl-{": "pane::ActivatePreviousItem", - "ctrl-}": "pane::ActivateNextItem", - "ctrl-pageup": "pane::ActivatePreviousItem", - "ctrl-pagedown": "pane::ActivateNextItem", - "ctrl-1": ["workspace::ActivatePane", 0], - "ctrl-2": ["workspace::ActivatePane", 1], - "ctrl-3": ["workspace::ActivatePane", 2], - "ctrl-4": ["workspace::ActivatePane", 3], - "ctrl-5": ["workspace::ActivatePane", 4], - "ctrl-6": ["workspace::ActivatePane", 5], - "ctrl-7": ["workspace::ActivatePane", 6], - "ctrl-8": ["workspace::ActivatePane", 7], - "ctrl-9": ["workspace::ActivatePane", 8], - "ctrl-!": ["workspace::MoveItemToPane", { "destination": 0, "focus": true }], - "ctrl-@": ["workspace::MoveItemToPane", { "destination": 1 }], - "ctrl-#": ["workspace::MoveItemToPane", { "destination": 2 }], - "ctrl-$": ["workspace::MoveItemToPane", { "destination": 3 }], - "ctrl-%": ["workspace::MoveItemToPane", { "destination": 4 }], - "ctrl-^": ["workspace::MoveItemToPane", { "destination": 5 }], - "ctrl-&": ["workspace::MoveItemToPane", { "destination": 6 }], - "ctrl-*": ["workspace::MoveItemToPane", { "destination": 7 }], - "ctrl-(": ["workspace::MoveItemToPane", { "destination": 8 }] - } - }, - { - "context": "Editor", - "bindings": { - "ctrl-alt-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": false }], - "ctrl-alt-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": false }], - "ctrl-shift-up": "editor::MoveLineUp", - "ctrl-shift-down": "editor::MoveLineDown", - "ctrl-shift-m": "editor::SelectLargerSyntaxNode", - "ctrl-shift-l": "editor::SplitSelectionIntoLines", - "ctrl-shift-a": "editor::SelectLargerSyntaxNode", - "ctrl-shift-d": "editor::DuplicateSelection", - "alt-f3": "editor::SelectAllMatches", // find_all_under - // "ctrl-f3": "", // find_under (cancels any selections) - // "ctrl-alt-shift-g": "" // find_under_prev (cancels any selections) - "f9": "editor::SortLinesCaseSensitive", - "ctrl-f9": "editor::SortLinesCaseInsensitive", - "f12": "editor::GoToDefinition", - "ctrl-f12": "editor::GoToDefinitionSplit", - "shift-f12": "editor::FindAllReferences", - "ctrl-shift-f12": "editor::FindAllReferences", - "ctrl-.": "editor::GoToHunk", - "ctrl-,": "editor::GoToPreviousHunk", - "ctrl-k ctrl-u": "editor::ConvertToUpperCase", - "ctrl-k ctrl-l": "editor::ConvertToLowerCase", - "shift-alt-m": "markdown::OpenPreviewToTheSide", - "ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }], - "ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], - "alt-right": "editor::MoveToNextSubwordEnd", - "alt-left": "editor::MoveToPreviousSubwordStart", - "alt-shift-right": "editor::SelectToNextSubwordEnd", - "alt-shift-left": "editor::SelectToPreviousSubwordStart" - } - }, - { - "context": "Editor && mode == full", - "bindings": { - "ctrl-r": "outline::Toggle" - } - }, - { - "context": "Editor && !agent_diff", - "bindings": { - "ctrl-k ctrl-z": "git::Restore" - } - }, - { - "context": "Pane", - "bindings": { - "f4": "search::SelectNextMatch", - "shift-f4": "search::SelectPreviousMatch", - "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::ActivateLastItem" - } - }, - { - "context": "Workspace", - "bindings": { - "ctrl-k ctrl-b": "workspace::ToggleLeftDock", - // "ctrl-0": "project_panel::ToggleFocus", // normally resets zoom - "shift-ctrl-r": "project_symbols::Toggle" - } - } -] diff --git a/assets/keymaps/macos/atom.json b/assets/keymaps/macos/atom.json deleted file mode 100644 index ca015b667f..0000000000 --- a/assets/keymaps/macos/atom.json +++ /dev/null @@ -1,98 +0,0 @@ -// Default Keymap (Atom) for Zed on macOS -[ - { - "bindings": { - "ctrl-alt-cmd-l": "workspace::Reload", - "cmd-k cmd-p": "workspace::ActivatePreviousPane", - "cmd-k cmd-n": "workspace::ActivateNextPane" - } - }, - { - "context": "Editor", - "bindings": { - "cmd-shift-backspace": "editor::DeleteToBeginningOfLine", - "cmd-k cmd-u": "editor::ConvertToUpperCase", - "cmd-k cmd-l": "editor::ConvertToLowerCase" - } - }, - { - "context": "Editor && mode == full", - "bindings": { - "ctrl-shift-l": "language_selector::Toggle", - "cmd-|": "pane::RevealInProjectPanel", - "cmd-b": "editor::GoToDefinition", - "alt-cmd-b": "editor::GoToDefinitionSplit", - "cmd-<": "editor::ScrollCursorCenter", - "cmd-g": ["editor::SelectNext", { "replace_newest": true }], - "cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }], - "ctrl-shift-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }], - "ctrl-shift-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }], - "alt-enter": "editor::Newline", - "cmd-shift-d": "editor::DuplicateLineDown", - "ctrl-cmd-up": "editor::MoveLineUp", - "ctrl-cmd-down": "editor::MoveLineDown", - "cmd-\\": "workspace::ToggleLeftDock", - "ctrl-shift-m": "markdown::OpenPreviewToTheSide", - "cmd-r": "outline::Toggle" - } - }, - { - "context": "BufferSearchBar", - "bindings": { - "cmd-g": ["editor::SelectNext", { "replace_newest": true }], - "cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }], - "cmd-f3": "search::SelectNextMatch", - "cmd-shift-f3": "search::SelectPreviousMatch" - } - }, - { - "context": "Workspace", - "bindings": { - "cmd-\\": "workspace::ToggleLeftDock", - "cmd-k cmd-b": "workspace::ToggleLeftDock", - "cmd-t": "file_finder::Toggle", - "cmd-shift-r": "project_symbols::Toggle" - } - }, - { - "context": "Pane", - "bindings": { - "alt-cmd-/": "search::ToggleRegex", - "ctrl-0": "project_panel::ToggleFocus", - "cmd-1": ["pane::ActivateItem", 0], - "cmd-2": ["pane::ActivateItem", 1], - "cmd-3": ["pane::ActivateItem", 2], - "cmd-4": ["pane::ActivateItem", 3], - "cmd-5": ["pane::ActivateItem", 4], - "cmd-6": ["pane::ActivateItem", 5], - "cmd-7": ["pane::ActivateItem", 6], - "cmd-8": ["pane::ActivateItem", 7], - "cmd-9": "pane::ActivateLastItem" - } - }, - { - "context": "ProjectPanel", - "bindings": { - "f2": "project_panel::Rename", - "backspace": ["project_panel::Trash", { "skip_prompt": false }], - "cmd-x": "project_panel::Cut", - "cmd-c": "project_panel::Copy", - "cmd-v": "project_panel::Paste" - } - }, - { - "context": "ProjectPanel && not_editing", - "bindings": { - "ctrl-shift-c": "project_panel::CopyPath", - "ctrl-[": "project_panel::CollapseSelectedEntry", - "ctrl-b": "project_panel::CollapseSelectedEntry", - "ctrl-]": "project_panel::ExpandSelectedEntry", - "ctrl-f": "project_panel::ExpandSelectedEntry", - "a": "project_panel::NewFile", - "d": "project_panel::Duplicate", - "home": "menu::SelectFirst", - "end": "menu::SelectLast", - "shift-a": "project_panel::NewDirectory" - } - } -] diff --git a/assets/keymaps/macos/cursor.json b/assets/keymaps/macos/cursor.json deleted file mode 100644 index 97abc7dd81..0000000000 --- a/assets/keymaps/macos/cursor.json +++ /dev/null @@ -1,84 +0,0 @@ -[ - // Cursor for MacOS. See: https://docs.cursor.com/kbd - { - "context": "Workspace", - "use_key_equivalents": true, - "bindings": { - "cmd-i": "agent::ToggleFocus", - "cmd-shift-i": "agent::ToggleFocus", - "cmd-l": "agent::ToggleFocus", - "cmd-shift-l": "agent::ToggleFocus", - "cmd-shift-j": "agent::OpenSettings" - } - }, - { - "context": "Editor && mode == full", - "use_key_equivalents": true, - "bindings": { - "cmd-i": "agent::ToggleFocus", - "cmd-shift-i": "agent::ToggleFocus", - "cmd-shift-l": "agent::AddSelectionToThread", // In cursor uses "Ask" mode - "cmd-l": "agent::AddSelectionToThread", // In cursor uses "Agent" mode - "cmd-k": "assistant::InlineAssist", - "cmd-shift-k": "assistant::InsertIntoEditor" - } - }, - { - "context": "InlineAssistEditor", - "use_key_equivalents": true, - "bindings": { - "cmd-shift-backspace": "editor::Cancel", - "cmd-enter": "menu::Confirm" - // "alt-enter": // Quick Question - // "cmd-shift-enter": // Full File Context - // "cmd-shift-k": // Toggle input focus (editor <> inline assist) - } - }, - { - "context": "AgentPanel || ContextEditor || (MessageEditor > Editor)", - "use_key_equivalents": true, - "bindings": { - "cmd-i": "workspace::ToggleRightDock", - "cmd-shift-i": "workspace::ToggleRightDock", - "cmd-l": "workspace::ToggleRightDock", - "cmd-shift-l": "workspace::ToggleRightDock", - "cmd-w": "workspace::ToggleRightDock", // technically should close chat - "cmd-.": "agent::ToggleProfileSelector", - "cmd-/": "agent::ToggleModelSelector", - "cmd-shift-backspace": "editor::Cancel", - "cmd-r": "agent::NewThread", - "cmd-shift-v": "editor::Paste", - "cmd-shift-k": "assistant::InsertIntoEditor" - // "escape": "agent::ToggleFocus" - ///// Enable when Zed supports multiple thread tabs - // "cmd-t": // new thread tab - // "cmd-[": // next thread tab - // "cmd-]": // next thread tab - ///// Enable if Zed adds support for keyboard navigation of thread elements - // "tab": // cycle to next message - // "shift-tab": // cycle to previous message - } - }, - { - "context": "Editor && editor_agent_diff", - "use_key_equivalents": true, - "bindings": { - "cmd-enter": "agent::KeepAll", - "cmd-backspace": "agent::RejectAll" - } - }, - { - "context": "Editor && mode == full && edit_prediction", - "use_key_equivalents": true, - "bindings": { - "cmd-right": "editor::AcceptPartialEditPrediction" - } - }, - { - "context": "Terminal", - "use_key_equivalents": true, - "bindings": { - "cmd-k": "assistant::InlineAssist" - } - } -] diff --git a/assets/keymaps/macos/emacs.json b/assets/keymaps/macos/emacs.json deleted file mode 100755 index ea831c0c05..0000000000 --- a/assets/keymaps/macos/emacs.json +++ /dev/null @@ -1,203 +0,0 @@ -// documentation: https://zed.dev/docs/key-bindings -// -// To see the default key bindings run `zed: open default keymap` -// from the command palette. -[ - { - "context": "!GitPanel", - "bindings": { - "ctrl-g": "menu::Cancel" - } - }, - { - // Workaround to avoid falling back to default bindings. - // Unbind so Zed ignores these keys and lets emacs handle them. - // NOTE: must be declared before the `Editor` override. - "context": "Editor", - "bindings": { - "ctrl-g": null // currently activates `go_to_line::Toggle` when there is nothing to cancel - } - }, - { - "context": "Editor", - "bindings": { - "ctrl-g": "editor::Cancel", - "alt-g g": "go_to_line::Toggle", // goto-line - "alt-g alt-g": "go_to_line::Toggle", // goto-line - "ctrl-space": "editor::SetMark", // set-mark - "ctrl-@": "editor::SetMark", // set-mark - "ctrl-x ctrl-x": "editor::SwapSelectionEnds", // exchange-point-and-mark - "ctrl-f": "editor::MoveRight", // forward-char - "ctrl-b": "editor::MoveLeft", // backward-char - "ctrl-n": "editor::MoveDown", // next-line - "ctrl-p": "editor::MoveUp", // previous-line - "home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line - "end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line - "ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line - "ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line - "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line - "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line - "alt-m": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_indent": true }], // back-to-indentation - "alt-left": "editor::MoveToPreviousWordStart", // left-word - "alt-right": "editor::MoveToNextWordEnd", // right-word - "alt-f": "editor::MoveToNextWordEnd", // forward-word - "alt-b": "editor::MoveToPreviousWordStart", // backward-word - "alt-u": "editor::ConvertToUpperCase", // upcase-word - "alt-l": "editor::ConvertToLowerCase", // downcase-word - "alt-c": "editor::ConvertToUpperCamelCase", // capitalize-word - "ctrl-t": "editor::Transpose", // transpose-chars - "alt-;": ["editor::ToggleComments", { "advance_downwards": false }], - "ctrl-x ctrl-;": "editor::ToggleComments", - "alt-.": "editor::GoToDefinition", // xref-find-definitions - "alt-?": "editor::FindAllReferences", // xref-find-references - "alt-,": "pane::GoBack", // xref-pop-marker-stack - "ctrl-x h": "editor::SelectAll", // mark-whole-buffer - "ctrl-d": "editor::Delete", // delete-char - "alt-d": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], // kill-word - "alt-backspace": "editor::DeleteToPreviousWordStart", // backward-kill-word - "alt-delete": "editor::DeleteToPreviousWordStart", // backward-kill-word - "ctrl-k": "editor::KillRingCut", // kill-line - "ctrl-w": "editor::Cut", // kill-region - "alt-w": "editor::Copy", // kill-ring-save - "ctrl-y": "editor::KillRingYank", // yank - "ctrl-_": "editor::Undo", // undo - "ctrl-/": "editor::Undo", // undo - "ctrl-x u": "editor::Undo", // undo - "alt-{": "editor::MoveToStartOfParagraph", // backward-paragraph - "alt-}": "editor::MoveToEndOfParagraph", // forward-paragraph - "ctrl-up": "editor::MoveToStartOfParagraph", // backward-paragraph - "ctrl-down": "editor::MoveToEndOfParagraph", // forward-paragraph - "ctrl-v": "editor::MovePageDown", // scroll-up - "alt-v": "editor::MovePageUp", // scroll-down - "ctrl-x [": "editor::MoveToBeginning", // beginning-of-buffer - "ctrl-x ]": "editor::MoveToEnd", // end-of-buffer - "alt-<": "editor::MoveToBeginning", // beginning-of-buffer - "alt->": "editor::MoveToEnd", // end-of-buffer - "ctrl-home": "editor::MoveToBeginning", // beginning-of-buffer - "ctrl-end": "editor::MoveToEnd", // end-of-buffer - "ctrl-l": "editor::ScrollCursorCenterTopBottom", // recenter-top-bottom - "ctrl-s": "buffer_search::Deploy", // isearch-forward - "ctrl-r": "buffer_search::Deploy", // isearch-backward - "alt-^": "editor::JoinLines", // join-line - "alt-q": "editor::Rewrap" // fill-paragraph - } - }, - { - "context": "Editor && selection_mode", // region selection - "bindings": { - "right": "editor::SelectRight", - "left": "editor::SelectLeft", - "down": "editor::SelectDown", - "up": "editor::SelectUp", - "alt-left": "editor::SelectToPreviousWordStart", - "alt-right": "editor::SelectToNextWordEnd", - "pagedown": "editor::SelectPageDown", - "ctrl-v": "editor::SelectPageDown", - "pageup": "editor::SelectPageUp", - "alt-v": "editor::SelectPageUp", - "ctrl-f": "editor::SelectRight", - "ctrl-b": "editor::SelectLeft", - "ctrl-n": "editor::SelectDown", - "ctrl-p": "editor::SelectUp", - "home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], - "end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], - "ctrl-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], - "ctrl-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], - "alt-m": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_indent": true }], - "alt-f": "editor::SelectToNextWordEnd", - "alt-b": "editor::SelectToPreviousWordStart", - "alt-{": "editor::SelectToStartOfParagraph", - "alt-}": "editor::SelectToEndOfParagraph", - "ctrl-up": "editor::SelectToStartOfParagraph", - "ctrl-down": "editor::SelectToEndOfParagraph", - "ctrl-x [": "editor::SelectToBeginning", - "ctrl-x ]": "editor::SelectToEnd", - "alt-<": "editor::SelectToBeginning", - "alt->": "editor::SelectToEnd", - "ctrl-home": "editor::SelectToBeginning", - "ctrl-end": "editor::SelectToEnd", - "ctrl-g": "editor::Cancel" - } - }, - { - "context": "Editor && (showing_code_actions || showing_completions)", - "bindings": { - "ctrl-p": "editor::ContextMenuPrevious", - "ctrl-n": "editor::ContextMenuNext" - } - }, - { - "context": "Editor && showing_signature_help && !showing_completions", - "bindings": { - "ctrl-p": "editor::SignatureHelpPrevious", - "ctrl-n": "editor::SignatureHelpNext" - } - }, - // Example setting for using emacs-style tab - // (i.e. indent the current line / selection or perform symbol completion depending on context) - // { - // "context": "Editor && !showing_code_actions && !showing_completions", - // "bindings": { - // "tab": "editor::AutoIndent" // indent-for-tab-command - // } - // }, - { - "context": "Workspace", - "bindings": { - "alt-x": "command_palette::Toggle", // execute-extended-command - "ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer - "ctrl-x ctrl-b": "tab_switcher::Toggle", // list-buffers - // "ctrl-x ctrl-c": "workspace::CloseWindow" // in case you only want to exit the current Zed instance - "ctrl-x ctrl-c": "zed::Quit", // save-buffers-kill-terminal - "ctrl-x 5 0": "workspace::CloseWindow", // delete-frame - "ctrl-x 5 2": "workspace::NewWindow", // make-frame-command - "ctrl-x o": "workspace::ActivateNextPane", // other-window - "ctrl-x k": "pane::CloseActiveItem", // kill-buffer - "ctrl-x 0": "pane::CloseActiveItem", // delete-window - // "ctrl-x 1": "pane::JoinAll", // in case you prefer to delete the splits but keep the buffers open - "ctrl-x 1": "pane::CloseOtherItems", // delete-other-windows - "ctrl-x 2": "pane::SplitDown", // split-window-below - "ctrl-x 3": "pane::SplitRight", // split-window-right - "ctrl-x ctrl-f": "file_finder::Toggle", // find-file - "ctrl-x ctrl-s": "workspace::Save", // save-buffer - "ctrl-x ctrl-w": "workspace::SaveAs", // write-file - "ctrl-x s": "workspace::SaveAll" // save-some-buffers - } - }, - { - // Workaround to enable using native emacs from the Zed terminal. - // Unbind so Zed ignores these keys and lets emacs handle them. - // NOTE: - // "terminal::SendKeystroke" only works for a single key stroke (e.g. ctrl-x), - // so override with null for compound sequences (e.g. ctrl-x ctrl-c). - "context": "Terminal", - "bindings": { - // If you want to perfect your emacs-in-zed setup, also consider the following. - // You may need to enable "option_as_meta" from the Zed settings for "alt-x" to work. - // "alt-x": ["terminal::SendKeystroke", "alt-x"], - // "ctrl-x": ["terminal::SendKeystroke", "ctrl-x"], - // "ctrl-n": ["terminal::SendKeystroke", "ctrl-n"], - // ... - "ctrl-x ctrl-c": null, // save-buffers-kill-terminal - "ctrl-x ctrl-f": null, // find-file - "ctrl-x ctrl-s": null, // save-buffer - "ctrl-x ctrl-w": null, // write-file - "ctrl-x s": null // save-some-buffers - } - }, - { - "context": "BufferSearchBar > Editor", - "bindings": { - "ctrl-s": "search::SelectNextMatch", - "ctrl-r": "search::SelectPreviousMatch", - "ctrl-g": "buffer_search::Dismiss" - } - }, - { - "context": "Pane", - "bindings": { - "ctrl-alt-left": "pane::GoBack", - "ctrl-alt-right": "pane::GoForward" - } - } -] diff --git a/assets/keymaps/macos/jetbrains.json b/assets/keymaps/macos/jetbrains.json deleted file mode 100644 index 364f489167..0000000000 --- a/assets/keymaps/macos/jetbrains.json +++ /dev/null @@ -1,188 +0,0 @@ -[ - { - "bindings": { - "cmd-{": "pane::ActivatePreviousItem", - "cmd-}": "pane::ActivateNextItem", - "cmd-0": "git_panel::ToggleFocus", // overrides `cmd-0` zoom reset - "shift-escape": null, // Unmap workspace::zoom - "cmd-~": "git::Branch", - "ctrl-f2": "debugger::Stop", - "f6": "debugger::Pause", - "f7": "debugger::StepInto", - "f8": "debugger::StepOver", - "shift-f8": "debugger::StepOut", - "f9": "debugger::Continue", - "shift-f9": "debugger::Start", - "alt-shift-f9": "debugger::Start" - } - }, - { - "context": "Editor", - "bindings": { - "ctrl->": ["zed::IncreaseBufferFontSize", { "persist": true }], - "ctrl-<": ["zed::DecreaseBufferFontSize", { "persist": true }], - "ctrl-shift-j": "editor::JoinLines", - "cmd-d": "editor::DuplicateSelection", - "cmd-backspace": "editor::DeleteLine", - "cmd-pagedown": "editor::MovePageDown", - "cmd-pageup": "editor::MovePageUp", - "ctrl-alt-shift-b": "editor::SelectToPreviousWordStart", - "cmd-alt-enter": "editor::NewlineAbove", - "shift-enter": "editor::NewlineBelow", - "cmd--": "editor::Fold", - "cmd-+": "editor::UnfoldLines", - "alt-shift-g": "editor::SplitSelectionIntoLines", - "ctrl-g": ["editor::SelectNext", { "replace_newest": false }], - "ctrl-cmd-g": ["editor::SelectPrevious", { "replace_newest": false }], - "cmd-/": ["editor::ToggleComments", { "advance_downwards": true }], - "alt-up": "editor::SelectLargerSyntaxNode", - "alt-down": "editor::SelectSmallerSyntaxNode", - "shift-alt-up": "editor::MoveLineUp", - "shift-alt-down": "editor::MoveLineDown", - "cmd-alt-l": "editor::Format", - "ctrl-alt-o": "editor::OrganizeImports", - "shift-f6": "editor::Rename", - "cmd-[": "pane::GoBack", - "cmd-]": "pane::GoForward", - "alt-f7": "editor::FindAllReferences", - "cmd-alt-f7": "editor::FindAllReferences", - "cmd-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock - "cmd-alt-b": "editor::GoToImplementation", - "cmd-shift-b": "editor::GoToTypeDefinition", - "cmd-alt-shift-b": "editor::GoToTypeDefinitionSplit", - "f2": "editor::GoToDiagnostic", - "shift-f2": "editor::GoToPreviousDiagnostic", - "ctrl-alt-shift-down": "editor::GoToHunk", - "ctrl-alt-shift-up": "editor::GoToPreviousHunk", - "cmd-home": "editor::MoveToBeginning", - "cmd-end": "editor::MoveToEnd", - "cmd-shift-home": "editor::SelectToBeginning", - "cmd-shift-end": "editor::SelectToEnd", - "ctrl-f8": "editor::ToggleBreakpoint", - "ctrl-shift-f8": "editor::EditLogBreakpoint", - "cmd-shift-u": "editor::ToggleCase" - } - }, - { - "context": "Editor && mode == full", - "bindings": { - "cmd-f12": "outline::Toggle", - "cmd-r": ["buffer_search::Deploy", { "replace_enabled": true }], - "cmd-shift-o": "file_finder::Toggle", - "cmd-l": "go_to_line::Toggle", - "alt-enter": "editor::ToggleCodeActions", - "ctrl-space": "editor::ShowCompletions", - "cmd-j": "editor::Hover", - "cmd-p": "editor::ShowSignatureHelp", - "cmd-\\": "assistant::InlineAssist" - } - }, - { - "context": "BufferSearchBar", - "bindings": { - "shift-enter": "search::SelectPreviousMatch" - } - }, - { - "context": "BufferSearchBar || ProjectSearchBar", - "bindings": { - "alt-c": "search::ToggleCaseSensitive", - "alt-e": "search::ToggleSelection", - "alt-x": "search::ToggleRegex", - "alt-w": "search::ToggleWholeWord", - "ctrl-alt-c": "search::ToggleCaseSensitive", - "ctrl-alt-e": "search::ToggleSelection", - "ctrl-alt-w": "search::ToggleWholeWord", - "ctrl-alt-x": "search::ToggleRegex" - } - }, - { - "context": "Workspace", - "bindings": { - "cmd-shift-f12": "workspace::ToggleAllDocks", - "cmd-shift-r": ["pane::DeploySearch", { "replace_enabled": true }], - "ctrl-alt-r": "task::Spawn", - "shift-f10": "task::Spawn", - "cmd-f5": "task::Rerun", - "cmd-e": "file_finder::Toggle", - "cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor - "cmd-shift-o": "file_finder::Toggle", - "cmd-shift-n": "file_finder::Toggle", - "cmd-n": "project_symbols::Toggle", - "cmd-shift-a": "command_palette::Toggle", - "shift shift": "command_palette::Toggle", - "cmd-alt-o": "project_symbols::Toggle", // JetBrains: Go to Symbol - "cmd-o": "project_symbols::Toggle", // JetBrains: Go to Class - "cmd-1": "project_panel::ToggleFocus", - "cmd-5": "debug_panel::ToggleFocus", - "cmd-6": "diagnostics::Deploy", - "cmd-7": "outline_panel::ToggleFocus" - } - }, - { - "context": "Pane", // this is to override the default Pane mappings to switch tabs - "bindings": { - "cmd-1": "project_panel::ToggleFocus", - "cmd-2": null, // Bookmarks (left dock) - "cmd-3": null, // Find Panel (bottom dock) - "cmd-4": null, // Run Panel (bottom dock) - "cmd-5": "debug_panel::ToggleFocus", - "cmd-6": "diagnostics::Deploy", - "cmd-7": "outline_panel::ToggleFocus", - "cmd-8": null, // Services (bottom dock) - "cmd-9": null, // Git History (bottom dock) - "cmd-0": "git_panel::ToggleFocus" - } - }, - { - "context": "Workspace || Editor", - "bindings": { - "alt-f12": "terminal_panel::Toggle", - "cmd-shift-k": "git::Push" - } - }, - { - "context": "Pane", - "bindings": { - "cmd-alt-left": "pane::GoBack", - "cmd-alt-right": "pane::GoForward", - "alt-left": "pane::ActivatePreviousItem", - "alt-right": "pane::ActivateNextItem" - } - }, - { - "context": "ProjectPanel", - "bindings": { - "enter": "project_panel::Open", - "cmd-shift-f": "project_panel::NewSearchInDirectory", - "cmd-backspace": ["project_panel::Trash", { "skip_prompt": false }], - "backspace": ["project_panel::Trash", { "skip_prompt": false }], - "delete": ["project_panel::Trash", { "skip_prompt": false }], - "shift-delete": ["project_panel::Delete", { "skip_prompt": false }], - "shift-f6": "project_panel::Rename" - } - }, - { - "context": "Terminal", - "bindings": { - "cmd-t": "workspace::NewTerminal", - "alt-f12": "workspace::CloseActiveDock", - "cmd-up": "terminal::ScrollLineUp", - "cmd-down": "terminal::ScrollLineDown", - "shift-pageup": "terminal::ScrollPageUp", - "shift-pagedown": "terminal::ScrollPageDown" - } - }, - { "context": "GitPanel", "bindings": { "cmd-0": "workspace::CloseActiveDock" } }, - { "context": "ProjectPanel", "bindings": { "cmd-1": "workspace::CloseActiveDock" } }, - { "context": "DebugPanel", "bindings": { "cmd-5": "workspace::CloseActiveDock" } }, - { "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)", - "bindings": { - "escape": "editor::ToggleFocus", - "shift-escape": "workspace::CloseActiveDock" - } - } -] diff --git a/assets/keymaps/macos/sublime_text.json b/assets/keymaps/macos/sublime_text.json deleted file mode 100644 index d1bffca755..0000000000 --- a/assets/keymaps/macos/sublime_text.json +++ /dev/null @@ -1,101 +0,0 @@ -[ - { - "bindings": { - "cmd-{": "pane::ActivatePreviousItem", - "cmd-}": "pane::ActivateNextItem", - "ctrl-pageup": "pane::ActivatePreviousItem", - "ctrl-pagedown": "pane::ActivateNextItem", - "ctrl-1": ["workspace::ActivatePane", 0], - "ctrl-2": ["workspace::ActivatePane", 1], - "ctrl-3": ["workspace::ActivatePane", 2], - "ctrl-4": ["workspace::ActivatePane", 3], - "ctrl-5": ["workspace::ActivatePane", 4], - "ctrl-6": ["workspace::ActivatePane", 5], - "ctrl-7": ["workspace::ActivatePane", 6], - "ctrl-8": ["workspace::ActivatePane", 7], - "ctrl-9": ["workspace::ActivatePane", 8], - "ctrl-!": ["workspace::MoveItemToPane", { "destination": 0, "focus": true }], - "ctrl-@": ["workspace::MoveItemToPane", { "destination": 1 }], - "ctrl-#": ["workspace::MoveItemToPane", { "destination": 2 }], - "ctrl-$": ["workspace::MoveItemToPane", { "destination": 3 }], - "ctrl-%": ["workspace::MoveItemToPane", { "destination": 4 }], - "ctrl-^": ["workspace::MoveItemToPane", { "destination": 5 }], - "ctrl-&": ["workspace::MoveItemToPane", { "destination": 6 }], - "ctrl-*": ["workspace::MoveItemToPane", { "destination": 7 }], - "ctrl-(": ["workspace::MoveItemToPane", { "destination": 8 }] - } - }, - { - "context": "Editor", - "bindings": { - "ctrl-shift-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": false }], - "ctrl-shift-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": false }], - "cmd-ctrl-up": "editor::MoveLineUp", - "cmd-ctrl-down": "editor::MoveLineDown", - "cmd-shift-space": "editor::SelectAll", - "ctrl-shift-m": "editor::SelectLargerSyntaxNode", - "cmd-shift-l": "editor::SplitSelectionIntoLines", - "cmd-shift-a": "editor::SelectLargerSyntaxNode", - "cmd-shift-d": "editor::DuplicateSelection", - "ctrl-cmd-g": "editor::SelectAllMatches", // find_all_under - // "cmd-alt-g": "", // find_under (cancels any selections) - // "cmd-alt-shift-g": "" // find_under_prev (cancels any selections) - "f5": "editor::SortLinesCaseSensitive", - "ctrl-f5": "editor::SortLinesCaseInsensitive", - "shift-f12": "editor::FindAllReferences", - "alt-cmd-down": "editor::GoToDefinition", - "ctrl-alt-cmd-down": "editor::GoToDefinitionSplit", - "alt-shift-cmd-down": "editor::FindAllReferences", - "ctrl-.": "editor::GoToHunk", - "ctrl-,": "editor::GoToPreviousHunk", - "cmd-k cmd-u": "editor::ConvertToUpperCase", - "cmd-k cmd-l": "editor::ConvertToLowerCase", - "cmd-shift-j": "editor::JoinLines", - "shift-alt-m": "markdown::OpenPreviewToTheSide", - "ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }], - "ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], - "ctrl-right": "editor::MoveToNextSubwordEnd", - "ctrl-left": "editor::MoveToPreviousSubwordStart", - "ctrl-shift-right": "editor::SelectToNextSubwordEnd", - "ctrl-shift-left": "editor::SelectToPreviousSubwordStart" - } - }, - { - "context": "Editor && mode == full", - "bindings": { - "cmd-r": "outline::Toggle" - } - }, - { - "context": "Editor && !agent_diff", - "bindings": { - "cmd-k cmd-z": "git::Restore" - } - }, - { - "context": "Pane", - "bindings": { - "f4": "search::SelectNextMatch", - "shift-f4": "search::SelectPreviousMatch", - "cmd-1": ["pane::ActivateItem", 0], - "cmd-2": ["pane::ActivateItem", 1], - "cmd-3": ["pane::ActivateItem", 2], - "cmd-4": ["pane::ActivateItem", 3], - "cmd-5": ["pane::ActivateItem", 4], - "cmd-6": ["pane::ActivateItem", 5], - "cmd-7": ["pane::ActivateItem", 6], - "cmd-8": ["pane::ActivateItem", 7], - "cmd-9": "pane::ActivateLastItem" - } - }, - { - "context": "Workspace", - "bindings": { - "cmd-k cmd-b": "workspace::ToggleLeftDock", - "cmd-t": "file_finder::Toggle", - "shift-cmd-r": "project_symbols::Toggle", - // Currently busted: https://github.com/zed-industries/feedback/issues/898 - "ctrl-0": "project_panel::ToggleFocus" - } - } -] diff --git a/assets/keymaps/macos/textmate.json b/assets/keymaps/macos/textmate.json deleted file mode 100644 index f91f39b7f5..0000000000 --- a/assets/keymaps/macos/textmate.json +++ /dev/null @@ -1,85 +0,0 @@ -[ - { - "bindings": { - "cmd-shift-o": "projects::OpenRecent", - "cmd-alt-tab": "project_panel::ToggleFocus" - } - }, - { - "context": "Editor && mode == full", - "bindings": { - "cmd-l": "go_to_line::Toggle", - "ctrl-shift-d": "editor::DuplicateLineDown", - "cmd-b": "editor::GoToDefinition", - "cmd-j": "editor::ScrollCursorCenter", - "cmd-enter": "editor::NewlineBelow", - "cmd-alt-enter": "editor::NewlineAbove", - "cmd-shift-l": "editor::SelectLine", - "cmd-shift-t": "outline::Toggle" - } - }, - { - "context": "Editor", - "bindings": { - "alt-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }], - "alt-shift-backspace": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], - "alt-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], - "alt-shift-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], - "ctrl-backspace": "editor::DeleteToPreviousSubwordStart", - "ctrl-delete": "editor::DeleteToNextSubwordEnd", - "alt-left": ["editor::MoveToPreviousWordStart", { "stop_at_soft_wraps": true }], - "alt-right": ["editor::MoveToNextWordEnd", { "stop_at_soft_wraps": true }], - "ctrl-left": "editor::MoveToPreviousSubwordStart", - "ctrl-right": "editor::MoveToNextSubwordEnd", - "cmd-shift-left": "editor::SelectToBeginningOfLine", - "cmd-shift-right": "editor::SelectToEndOfLine", - "alt-shift-left": ["editor::SelectToPreviousWordStart", { "stop_at_soft_wraps": true }], - "alt-shift-right": ["editor::SelectToNextWordEnd", { "stop_at_soft_wraps": true }], - "ctrl-shift-left": "editor::SelectToPreviousSubwordStart", - "ctrl-shift-right": "editor::SelectToNextSubwordEnd", - "ctrl-w": "editor::SelectNext", - "ctrl-u": "editor::ConvertToUpperCase", - "ctrl-shift-u": "editor::ConvertToLowerCase", - "ctrl-alt-u": "editor::ConvertToUpperCamelCase", - "ctrl-_": "editor::ConvertToSnakeCase" - } - }, - { - "context": "BufferSearchBar", - "bindings": { - "ctrl-s": "search::SelectNextMatch", - "ctrl-shift-s": "search::SelectPreviousMatch" - } - }, - { - "context": "Workspace", - "bindings": { - "cmd-alt-ctrl-d": "workspace::ToggleLeftDock", - "cmd-t": "file_finder::Toggle", - "cmd-shift-t": "project_symbols::Toggle" - } - }, - { - "context": "Pane", - "bindings": { - "alt-cmd-r": "search::ToggleRegex", - "ctrl-tab": "project_panel::ToggleFocus" - } - }, - { - "context": "ProjectPanel", - "bindings": { - "cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }], - "cmd-d": "project_panel::Duplicate", - "cmd-n": "project_panel::NewDirectory", - "return": "project_panel::Rename", - "cmd-c": "project_panel::Copy", - "cmd-v": "project_panel::Paste", - "cmd-alt-c": "project_panel::CopyPath" - } - }, - { - "context": "Dock", - "bindings": {} - } -] diff --git a/assets/keymaps/storybook.json b/assets/keymaps/storybook.json deleted file mode 100644 index 9b92fbe1a3..0000000000 --- a/assets/keymaps/storybook.json +++ /dev/null @@ -1,33 +0,0 @@ -[ - // Standard macOS bindings - { - "bindings": { - "home": "menu::SelectFirst", - "shift-pageup": "menu::SelectFirst", - "pageup": "menu::SelectFirst", - "cmd-up": "menu::SelectFirst", - "end": "menu::SelectLast", - "shift-pagedown": "menu::SelectLast", - "pagedown": "menu::SelectLast", - "cmd-down": "menu::SelectLast", - "tab": "menu::SelectNext", - "ctrl-n": "menu::SelectNext", - "down": "menu::SelectNext", - "shift-tab": "menu::SelectPrevious", - "ctrl-p": "menu::SelectPrevious", - "up": "menu::SelectPrevious", - "enter": "menu::Confirm", - "ctrl-enter": "menu::SecondaryConfirm", - "cmd-enter": "menu::SecondaryConfirm", - "ctrl-escape": "menu::Cancel", - "cmd-escape": "menu::Cancel", - "ctrl-c": "menu::Cancel", - "escape": "menu::Cancel", - "cmd-q": "storybook::Quit", - "backspace": "editor::Backspace", - "delete": "editor::Delete", - "left": "editor::MoveLeft", - "right": "editor::MoveRight" - } - } -] diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json deleted file mode 100644 index 533db14a5f..0000000000 --- a/assets/keymaps/vim.json +++ /dev/null @@ -1,1059 +0,0 @@ -[ - { - "context": "VimControl && !menu", - "bindings": { - "i": ["vim::PushObject", { "around": false }], - "a": ["vim::PushObject", { "around": true }], - "left": "vim::Left", - "h": "vim::Left", - "backspace": "vim::WrappingLeft", - "down": "vim::Down", - "ctrl-j": "vim::Down", - "j": "vim::Down", - "ctrl-m": "vim::NextLineStart", - "+": "vim::NextLineStart", - "enter": "vim::NextLineStart", - "-": "vim::PreviousLineStart", - "shift-tab": "vim::Tab", - "tab": "vim::Tab", - "up": "vim::Up", - "k": "vim::Up", - "right": "vim::Right", - "l": "vim::Right", - "space": "vim::WrappingRight", - "end": "vim::EndOfLine", - "$": "vim::EndOfLine", - "^": "vim::FirstNonWhitespace", - "_": "vim::StartOfLineDownward", - "g _": "vim::EndOfLineDownward", - "shift-g": "vim::EndOfDocument", - "{": "vim::StartOfParagraph", - "}": "vim::EndOfParagraph", - "(": "vim::SentenceBackward", - ")": "vim::SentenceForward", - "|": "vim::GoToColumn", - - // Word motions - "w": "vim::NextWordStart", - "e": "vim::NextWordEnd", - "b": "vim::PreviousWordStart", - "g e": "vim::PreviousWordEnd", - // Subword motions - // "w": "vim::NextSubwordStart", - // "b": "vim::PreviousSubwordStart", - // "e": "vim::NextSubwordEnd", - // "g e": "vim::PreviousSubwordEnd", - "shift-w": ["vim::NextWordStart", { "ignore_punctuation": true }], - "shift-e": ["vim::NextWordEnd", { "ignore_punctuation": true }], - "shift-b": ["vim::PreviousWordStart", { "ignore_punctuation": true }], - "g shift-e": ["vim::PreviousWordEnd", { "ignore_punctuation": true }], - "/": "vim::Search", - "g /": "pane::DeploySearch", - "?": ["vim::Search", { "backwards": true }], - "*": "vim::MoveToNext", - "#": "vim::MoveToPrevious", - "n": "vim::MoveToNextMatch", - "shift-n": "vim::MoveToPreviousMatch", - "%": "vim::Matching", - "f": ["vim::PushFindForward", { "before": false, "multiline": false }], - "t": ["vim::PushFindForward", { "before": true, "multiline": false }], - "shift-f": ["vim::PushFindBackward", { "after": false, "multiline": false }], - "shift-t": ["vim::PushFindBackward", { "after": true, "multiline": false }], - "m": "vim::PushMark", - "'": ["vim::PushJump", { "line": true }], - "`": ["vim::PushJump", { "line": false }], - ";": "vim::RepeatFind", - ",": "vim::RepeatFindReversed", - "ctrl-o": "pane::GoBack", - "ctrl-i": "pane::GoForward", - "ctrl-]": "editor::GoToDefinition", - "escape": "vim::SwitchToNormalMode", - "ctrl-[": "vim::SwitchToNormalMode", - "v": "vim::ToggleVisual", - "shift-v": "vim::ToggleVisualLine", - "ctrl-g": "vim::ShowLocation", - "ctrl-v": "vim::ToggleVisualBlock", - "ctrl-q": "vim::ToggleVisualBlock", - "shift-k": "editor::Hover", - "shift-r": "vim::ToggleReplace", - "0": "vim::StartOfLine", - "home": "vim::StartOfLine", - "ctrl-f": "vim::PageDown", - "pagedown": "vim::PageDown", - "ctrl-b": "vim::PageUp", - "pageup": "vim::PageUp", - "ctrl-d": "vim::ScrollDown", - "ctrl-u": "vim::ScrollUp", - "ctrl-e": "vim::LineDown", - "ctrl-y": "vim::LineUp", - // "g" commands - "g shift-r": "vim::PushReplaceWithRegister", - "g r n": "editor::Rename", - "g r r": "editor::FindAllReferences", - "g r i": "editor::GoToImplementation", - "g r a": "editor::ToggleCodeActions", - "g g": "vim::StartOfDocument", - "g h": "editor::Hover", - "g B": "editor::BlameHover", - "g d": "editor::GoToDefinition", - "g shift-d": "editor::GoToDeclaration", - "g y": "editor::GoToTypeDefinition", - "g shift-i": "editor::GoToImplementation", - "g x": "editor::OpenUrl", - "g f": "editor::OpenSelectedFilename", - "g n": "vim::SelectNextMatch", - "g shift-n": "vim::SelectPreviousMatch", - "g l": "vim::SelectNext", - "g shift-l": "vim::SelectPrevious", - "g >": ["editor::SelectNext", { "replace_newest": true }], - "g <": ["editor::SelectPrevious", { "replace_newest": true }], - "g a": "editor::SelectAllMatches", - "g s": "outline::Toggle", - "g shift-o": "outline::Toggle", - "g shift-s": "project_symbols::Toggle", - "g .": "editor::ToggleCodeActions", // zed specific - "g shift-a": "editor::FindAllReferences", // zed specific - "g space": "editor::OpenExcerpts", // zed specific - "g *": ["vim::MoveToNext", { "partial_word": true }], - "g #": ["vim::MoveToPrevious", { "partial_word": true }], - "g j": ["vim::Down", { "display_lines": true }], - "g down": ["vim::Down", { "display_lines": true }], - "g k": ["vim::Up", { "display_lines": true }], - "g up": ["vim::Up", { "display_lines": true }], - "g $": ["vim::EndOfLine", { "display_lines": true }], - "g end": ["vim::EndOfLine", { "display_lines": true }], - "g 0": ["vim::StartOfLine", { "display_lines": true }], - "g home": ["vim::StartOfLine", { "display_lines": true }], - "g shift-m": ["vim::MiddleOfLine", { "display_lines": true }], - "g ^": ["vim::FirstNonWhitespace", { "display_lines": true }], - "g v": "vim::RestoreVisualSelection", - "g ]": "editor::GoToDiagnostic", - "g [": "editor::GoToPreviousDiagnostic", - "g i": "vim::InsertAtPrevious", - "g ,": "vim::ChangeListNewer", - "g ;": "vim::ChangeListOlder", - "shift-h": "vim::WindowTop", - "shift-m": "vim::WindowMiddle", - "shift-l": "vim::WindowBottom", - "q": "vim::ToggleRecord", - "shift-q": "vim::ReplayLastRecording", - "@": "vim::PushReplayRegister", - // z commands - "z enter": ["workspace::SendKeystrokes", "z t ^"], - "z -": ["workspace::SendKeystrokes", "z b ^"], - "z ^": ["workspace::SendKeystrokes", "shift-h k z b ^"], - "z +": ["workspace::SendKeystrokes", "shift-l j z t ^"], - "z t": "editor::ScrollCursorTop", - "z z": "editor::ScrollCursorCenter", - "z .": ["workspace::SendKeystrokes", "z z ^"], - "z b": "editor::ScrollCursorBottom", - "z a": "editor::ToggleFold", - "z shift-a": "editor::ToggleFoldRecursive", - "z c": "editor::Fold", - "z shift-c": "editor::FoldRecursive", - "z o": "editor::UnfoldLines", - "z shift-o": "editor::UnfoldRecursive", - "z f": "editor::FoldSelectedRanges", - "z shift-m": "editor::FoldAll", - "z shift-r": "editor::UnfoldAll", - "z l": "vim::ColumnRight", - "z h": "vim::ColumnLeft", - "z shift-l": "vim::HalfPageRight", - "z shift-h": "vim::HalfPageLeft", - "shift-z shift-q": ["pane::CloseActiveItem", { "save_intent": "skip" }], - "shift-z shift-z": ["pane::CloseActiveItem", { "save_intent": "save_all" }], - // Count support - "1": ["vim::Number", 1], - "2": ["vim::Number", 2], - "3": ["vim::Number", 3], - "4": ["vim::Number", 4], - "5": ["vim::Number", 5], - "6": ["vim::Number", 6], - "7": ["vim::Number", 7], - "8": ["vim::Number", 8], - "9": ["vim::Number", 9], - "ctrl-w d": "editor::GoToDefinitionSplit", - "ctrl-w g d": "editor::GoToDefinitionSplit", - "ctrl-w ]": "editor::GoToDefinitionSplit", - "ctrl-w ctrl-]": "editor::GoToDefinitionSplit", - "ctrl-w shift-d": "editor::GoToTypeDefinitionSplit", - "ctrl-w g shift-d": "editor::GoToTypeDefinitionSplit", - "ctrl-w space": "editor::OpenExcerptsSplit", - "ctrl-w g space": "editor::OpenExcerptsSplit", - "ctrl-^": "pane::AlternateFile", - ".": "vim::Repeat" - } - }, - { - "context": "vim_mode == normal || vim_mode == visual || vim_mode == operator", - "bindings": { - "] ]": "vim::NextSectionStart", - "] [": "vim::NextSectionEnd", - "[ [": "vim::PreviousSectionStart", - "[ ]": "vim::PreviousSectionEnd", - "] m": "vim::NextMethodStart", - "] shift-m": "vim::NextMethodEnd", - "[ m": "vim::PreviousMethodStart", - "[ shift-m": "vim::PreviousMethodEnd", - "[ *": "vim::PreviousComment", - "[ /": "vim::PreviousComment", - "] *": "vim::NextComment", - "] /": "vim::NextComment", - "[ -": "vim::PreviousLesserIndent", - "[ +": "vim::PreviousGreaterIndent", - "[ =": "vim::PreviousSameIndent", - "] -": "vim::NextLesserIndent", - "] +": "vim::NextGreaterIndent", - "] =": "vim::NextSameIndent", - "] b": "pane::ActivateNextItem", - "[ b": "pane::ActivatePreviousItem", - "] shift-b": "pane::ActivateLastItem", - "[ shift-b": ["pane::ActivateItem", 0], - "] space": "vim::InsertEmptyLineBelow", - "[ space": "vim::InsertEmptyLineAbove", - "[ e": "editor::MoveLineUp", - "] e": "editor::MoveLineDown", - "[ f": "workspace::FollowNextCollaborator", - "] f": "workspace::FollowNextCollaborator", - "] }": ["vim::UnmatchedForward", { "char": "}" }], - "[ {": ["vim::UnmatchedBackward", { "char": "{" }], - "] )": ["vim::UnmatchedForward", { "char": ")" }], - "[ (": ["vim::UnmatchedBackward", { "char": "(" }], - "[ r": "vim::GoToPreviousReference", - "] r": "vim::GoToNextReference", - // tree-sitter related commands - "[ x": "vim::SelectLargerSyntaxNode", - "] x": "vim::SelectSmallerSyntaxNode" - } - }, - { - "context": "vim_mode == normal", - "bindings": { - "i": "vim::InsertBefore", - "a": "vim::InsertAfter", - "ctrl-[": "editor::Cancel", - ":": "command_palette::Toggle", - "c": "vim::PushChange", - "shift-c": "vim::ChangeToEndOfLine", - "d": "vim::PushDelete", - "delete": "vim::DeleteRight", - "g shift-j": "vim::JoinLinesNoWhitespace", - "y": "vim::PushYank", - "shift-y": "vim::YankLine", - "x": "vim::DeleteRight", - "shift-x": "vim::DeleteLeft", - "ctrl-a": "vim::Increment", - "ctrl-x": "vim::Decrement", - "ctrl-r": "vim::Redo", - ">": "vim::PushIndent", - "<": "vim::PushOutdent", - "=": "vim::PushAutoIndent", - "!": "vim::PushShellCommand", - "g u": "vim::PushLowercase", - "g shift-u": "vim::PushUppercase", - "g ~": "vim::PushOppositeCase", - "g ?": "vim::PushRot13", - // "g ?": "vim::PushRot47", - "g w": "vim::PushRewrap", - "g q": "vim::PushRewrap", - "insert": "vim::InsertBefore", - "] d": "editor::GoToDiagnostic", - "[ d": "editor::GoToPreviousDiagnostic", - "] c": "editor::GoToHunk", - "[ c": "editor::GoToPreviousHunk", - "g c": "vim::PushToggleComments" - } - }, - { - "context": "VimControl && VimCount", - "bindings": { - "0": ["vim::Number", 0], - ":": "vim::CountCommand", - "%": "vim::GoToPercentage" - } - }, - { - "context": "vim_mode == visual", - "bindings": { - ":": "vim::VisualCommand", - "u": "vim::ConvertToLowerCase", - "shift-u": "vim::ConvertToUpperCase", - "shift-o": "vim::OtherEnd", - "o": "vim::OtherEndRowAware", - "d": "vim::VisualDelete", - "x": "vim::VisualDelete", - "delete": "vim::VisualDelete", - "shift-d": "vim::VisualDeleteLine", - "shift-x": "vim::VisualDeleteLine", - "y": "vim::VisualYank", - "shift-y": "vim::VisualYankLine", - "p": "vim::Paste", - "shift-p": ["vim::Paste", { "preserve_clipboard": true }], - "c": "vim::Substitute", - "s": "vim::Substitute", - "shift-r": "vim::SubstituteLine", - "shift-s": "vim::SubstituteLine", - "~": "vim::ChangeCase", - "*": ["vim::MoveToNext", { "partial_word": true }], - "#": ["vim::MoveToPrevious", { "partial_word": true }], - "ctrl-a": "vim::Increment", - "ctrl-x": "vim::Decrement", - "g ctrl-a": ["vim::Increment", { "step": true }], - "g ctrl-x": ["vim::Decrement", { "step": true }], - "shift-i": "vim::InsertBefore", - "shift-a": "vim::InsertAfter", - "g shift-i": "vim::VisualInsertFirstNonWhiteSpace", - "g shift-a": "vim::VisualInsertEndOfLine", - "shift-j": "vim::JoinLines", - "g shift-j": "vim::JoinLinesNoWhitespace", - "r": "vim::PushReplace", - "ctrl-c": "vim::SwitchToNormalMode", - "ctrl-[": "vim::SwitchToNormalMode", - "escape": "vim::SwitchToNormalMode", - ">": "vim::Indent", - "<": "vim::Outdent", - "=": "vim::AutoIndent", - "!": "vim::ShellCommand", - "i": ["vim::PushObject", { "around": false }], - "a": ["vim::PushObject", { "around": true }], - "g shift-r": ["vim::Paste", { "preserve_clipboard": true }], - "g c": "vim::ToggleComments", - "g q": "vim::Rewrap", - "g w": "vim::Rewrap", - "g ?": "vim::ConvertToRot13", - // "g ?": "vim::ConvertToRot47", - "\"": "vim::PushRegister" - } - }, - { - "context": "vim_mode == helix_select", - "bindings": { - "v": "vim::NormalBefore", - ";": "vim::HelixCollapseSelection", - "~": "vim::ChangeCase", - "ctrl-a": "vim::Increment", - "ctrl-x": "vim::Decrement", - "shift-j": "vim::JoinLines", - "i": "vim::InsertBefore", - "a": "vim::InsertAfter", - "p": "vim::Paste", - "u": "vim::Undo", - "r": "vim::PushReplace", - "s": "vim::Substitute", - "ctrl-pageup": "pane::ActivatePreviousItem", - "ctrl-pagedown": "pane::ActivateNextItem", - ".": "vim::Repeat", - "alt-.": "vim::RepeatFind" - } - }, - { - "context": "vim_mode == insert", - "bindings": { - "ctrl-c": "vim::NormalBefore", - "ctrl-[": "vim::NormalBefore", - "escape": "vim::NormalBefore", - "ctrl-x": null, - "ctrl-x ctrl-o": "editor::ShowCompletions", - "ctrl-x ctrl-a": "assistant::InlineAssist", // zed specific - "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", { "ignore_newlines": false, "ignore_brackets": false }], - "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. - "ctrl-q": ["vim::PushLiteral", {}], - "ctrl-shift-q": ["vim::PushLiteral", {}], - "ctrl-r": "vim::PushRegister", - "insert": "vim::ToggleReplace", - "ctrl-o": "vim::TemporaryNormal", - "ctrl-s": "editor::ShowSignatureHelp" - } - }, - { - "context": "showing_completions", - "bindings": { - "ctrl-d": "vim::ScrollDown", - "ctrl-u": "vim::ScrollUp", - "ctrl-e": "vim::LineDown", - "ctrl-y": "vim::LineUp" - } - }, - { - "context": "(vim_mode == normal || vim_mode == helix_normal) && !menu", - "bindings": { - "escape": "editor::Cancel", - "shift-d": "vim::DeleteToEndOfLine", - "shift-j": "vim::JoinLines", - "shift-y": "vim::YankLine", - "shift-i": "vim::InsertFirstNonWhitespace", - "shift-a": "vim::InsertEndOfLine", - "o": "vim::InsertLineBelow", - "shift-o": "vim::InsertLineAbove", - "~": "vim::ChangeCase", - "ctrl-a": "vim::Increment", - "ctrl-x": "vim::Decrement", - "p": "vim::Paste", - "shift-p": ["vim::Paste", { "before": true }], - "u": "vim::Undo", - "shift-u": "vim::UndoLastLine", - "r": "vim::PushReplace", - "s": "vim::Substitute", - "shift-s": "vim::SubstituteLine", - "\"": "vim::PushRegister", - "ctrl-pagedown": "pane::ActivateNextItem", - "ctrl-pageup": "pane::ActivatePreviousItem" - } - }, - { - "context": "VimControl && vim_mode == helix_normal && !menu", - "bindings": { - "escape": "vim::SwitchToHelixNormalMode", - "i": "vim::HelixInsert", - "a": "vim::HelixAppend", - "ctrl-[": "editor::Cancel" - } - }, - { - "context": "vim_mode == helix_select && !menu", - "bindings": { - "escape": "vim::SwitchToHelixNormalMode" - } - }, - { - "context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu", - "bindings": { - // Movement - "h": "vim::WrappingLeft", - "left": "vim::WrappingLeft", - "l": "vim::WrappingRight", - "right": "vim::WrappingRight", - "t": ["vim::PushFindForward", { "before": true, "multiline": true }], - "f": ["vim::PushFindForward", { "before": false, "multiline": true }], - "shift-t": ["vim::PushFindBackward", { "after": true, "multiline": true }], - "shift-f": ["vim::PushFindBackward", { "after": false, "multiline": true }], - "alt-.": "vim::RepeatFind", - - // Changes - "shift-r": "editor::Paste", - "`": "vim::ConvertToLowerCase", - "alt-`": "vim::ConvertToUpperCase", - "insert": "vim::InsertBefore", - "shift-u": "editor::Redo", - "ctrl-r": "vim::Redo", - "y": "vim::HelixYank", - "p": "vim::HelixPaste", - "shift-p": ["vim::HelixPaste", { "before": true }], - ">": "vim::Indent", - "<": "vim::Outdent", - "=": "vim::AutoIndent", - "d": "vim::HelixDelete", - "alt-d": "editor::Delete", // Delete selection, without yanking - "c": "vim::HelixSubstitute", - "alt-c": "vim::HelixSubstituteNoYank", - - // Selection manipulation - "s": "vim::HelixSelectRegex", - "alt-s": ["editor::SplitSelectionIntoLines", { "keep_selections": true }], - ";": "vim::HelixCollapseSelection", - "alt-;": "vim::OtherEnd", - ",": "vim::HelixKeepNewestSelection", - "shift-c": "vim::HelixDuplicateBelow", - "alt-shift-c": "vim::HelixDuplicateAbove", - "%": "editor::SelectAll", - "x": "vim::HelixSelectLine", - "shift-x": "editor::SelectLine", - "ctrl-c": "editor::ToggleComments", - "alt-o": "editor::SelectLargerSyntaxNode", - "alt-i": "editor::SelectSmallerSyntaxNode", - "alt-p": "editor::SelectPreviousSyntaxNode", - "alt-n": "editor::SelectNextSyntaxNode", - - "n": "vim::HelixSelectNext", - "shift-n": "vim::HelixSelectPrevious", - - // Goto mode - "g e": "vim::EndOfDocument", - "g h": "vim::StartOfLine", - "g l": "vim::EndOfLine", - "g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s" - "g t": "vim::WindowTop", - "g c": "vim::WindowMiddle", - "g b": "vim::WindowBottom", - "g r": "editor::FindAllReferences", // zed specific - "g n": "pane::ActivateNextItem", - "shift-l": "pane::ActivateNextItem", - "g p": "pane::ActivatePreviousItem", - "shift-h": "pane::ActivatePreviousItem", - "g .": "vim::HelixGotoLastModification", // go to last modification - - // Window mode - "space w h": "workspace::ActivatePaneLeft", - "space w l": "workspace::ActivatePaneRight", - "space w k": "workspace::ActivatePaneUp", - "space w j": "workspace::ActivatePaneDown", - "space w q": "pane::CloseActiveItem", - "space w s": "pane::SplitRight", - "space w r": "pane::SplitRight", - "space w v": "pane::SplitDown", - "space w d": "pane::SplitDown", - - // Space mode - "space f": "file_finder::Toggle", - "space k": "editor::Hover", - "space s": "outline::Toggle", - "space shift-s": "project_symbols::Toggle", - "space d": "editor::GoToDiagnostic", - "space r": "editor::Rename", - "space a": "editor::ToggleCodeActions", - "space h": "editor::SelectAllMatches", - "space c": "editor::ToggleComments", - "space p": "editor::Paste", - "space y": "editor::Copy", - - // Other - ":": "command_palette::Toggle", - "m": "vim::PushHelixMatch", - "]": ["vim::PushHelixNext", { "around": true }], - "[": ["vim::PushHelixPrevious", { "around": true }], - "g q": "vim::PushRewrap", - "g w": "vim::PushRewrap" - // "tab": "pane::ActivateNextItem", - // "shift-tab": "pane::ActivatePrevItem", - } - }, - { - "context": "vim_mode == insert && !(showing_code_actions || showing_completions)", - "bindings": { - "ctrl-p": "editor::ShowWordCompletions", - "ctrl-n": "editor::ShowWordCompletions" - } - }, - { - "context": "(vim_mode == insert || vim_mode == normal) && showing_signature_help && !showing_completions", - "bindings": { - "ctrl-p": "editor::SignatureHelpPrevious", - "ctrl-n": "editor::SignatureHelpNext" - } - }, - { - "context": "vim_mode == replace", - "bindings": { - "ctrl-c": "vim::NormalBefore", - "ctrl-[": "vim::NormalBefore", - "escape": "vim::NormalBefore", - "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. - "ctrl-q": ["vim::PushLiteral", {}], - "ctrl-shift-q": ["vim::PushLiteral", {}], - "backspace": "vim::UndoReplace", - "tab": "vim::Tab", - "enter": "vim::Enter", - "insert": "vim::InsertBefore" - } - }, - { - "context": "vim_mode == waiting", - "bindings": { - "tab": "vim::Tab", - "enter": "vim::Enter", - "ctrl-c": "vim::ClearOperators", - "ctrl-[": "vim::ClearOperators", - "escape": "vim::ClearOperators", - "ctrl-k": ["vim::PushDigraph", {}], - "ctrl-v": ["vim::PushLiteral", {}], - "ctrl-q": ["vim::PushLiteral", {}] - } - }, - { - "context": "Editor && vim_mode == waiting && (vim_operator == ys || vim_operator == cs)", - "bindings": { - "escape": "vim::SwitchToNormalMode" - } - }, - { - "context": "vim_mode == operator", - "bindings": { - "ctrl-c": "vim::ClearOperators", - "ctrl-[": "vim::ClearOperators", - "escape": "vim::ClearOperators", - "g c": "vim::Comment" - } - }, - { - "context": "vim_operator == a || vim_operator == i || vim_operator == cs || vim_operator == helix_next || vim_operator == helix_previous", - "bindings": { - "w": "vim::Word", - "shift-w": ["vim::Word", { "ignore_punctuation": true }], - // Subword TextObject - // "w": "vim::Subword", - // "shift-w": ["vim::Subword", { "ignore_punctuation": true }], - "t": "vim::Tag", - "s": "vim::Sentence", - "p": "vim::Paragraph", - "'": "vim::Quotes", - "`": "vim::BackQuotes", - "\"": "vim::DoubleQuotes", - // "q": "vim::AnyQuotes", - "q": "vim::MiniQuotes", - "|": "vim::VerticalBars", - "(": ["vim::Parentheses", { "opening": true }], - ")": "vim::Parentheses", - "b": "vim::Parentheses", - // "b": "vim::AnyBrackets", - // "b": "vim::MiniBrackets", - "[": ["vim::SquareBrackets", { "opening": true }], - "]": "vim::SquareBrackets", - "r": "vim::SquareBrackets", - "{": ["vim::CurlyBrackets", { "opening": true }], - "}": "vim::CurlyBrackets", - "shift-b": "vim::CurlyBrackets", - "<": ["vim::AngleBrackets", { "opening": true }], - ">": "vim::AngleBrackets", - "a": "vim::Argument", - "i": "vim::IndentObj", - "shift-i": ["vim::IndentObj", { "include_below": true }], - "f": "vim::Method", - "c": "vim::Class", - "e": "vim::EntireFile" - } - }, - { - "context": "vim_operator == helix_m", - "bindings": { - "m": "vim::Matching" - } - }, - { - "context": "vim_operator == helix_next", - "bindings": { - "z": "vim::NextSectionStart", - "shift-z": "vim::NextSectionEnd", - "*": "vim::NextComment", - "/": "vim::NextComment", - "-": "vim::NextLesserIndent", - "+": "vim::NextGreaterIndent", - "=": "vim::NextSameIndent", - "b": "pane::ActivateNextItem", - "shift-b": "pane::ActivateLastItem", - "x": "editor::SelectSmallerSyntaxNode", - "d": "editor::GoToDiagnostic", - "c": "editor::GoToHunk", - "space": "vim::InsertEmptyLineBelow" - } - }, - { - "context": "vim_operator == helix_previous", - "bindings": { - "z": "vim::PreviousSectionStart", - "shift-z": "vim::PreviousSectionEnd", - "*": "vim::PreviousComment", - "/": "vim::PreviousComment", - "-": "vim::PreviousLesserIndent", - "+": "vim::PreviousGreaterIndent", - "=": "vim::PreviousSameIndent", - "b": "pane::ActivatePreviousItem", - "shift-b": ["pane::ActivateItem", 0], - "x": "editor::SelectLargerSyntaxNode", - "d": "editor::GoToPreviousDiagnostic", - "c": "editor::GoToPreviousHunk", - "space": "vim::InsertEmptyLineAbove" - } - }, - { - "context": "vim_operator == c", - "bindings": { - "c": "vim::CurrentLine", - "x": "vim::Exchange", - "d": "editor::Rename", // zed specific - "s": ["vim::PushChangeSurrounds", {}] - } - }, - { - "context": "vim_operator == d", - "bindings": { - "d": "vim::CurrentLine", - "s": "vim::PushDeleteSurrounds", - "v": "vim::PushForcedMotion", // "d v" - "o": "editor::ToggleSelectedDiffHunks", // "d o" - "shift-o": "git::ToggleStaged", - "p": "git::Restore", // "d p" - "u": "git::StageAndNext", // "d u" - "shift-u": "git::UnstageAndNext" // "d shift-u" - } - }, - { - "context": "vim_operator == gu", - "bindings": { - "g u": "vim::CurrentLine", - "u": "vim::CurrentLine" - } - }, - { - "context": "vim_operator == gU", - "bindings": { - "g shift-u": "vim::CurrentLine", - "shift-u": "vim::CurrentLine" - } - }, - { - "context": "vim_operator == g~", - "bindings": { - "g ~": "vim::CurrentLine", - "~": "vim::CurrentLine" - } - }, - { - "context": "vim_operator == g?", - "bindings": { - "g ?": "vim::CurrentLine", - "?": "vim::CurrentLine" - } - }, - { - "context": "vim_operator == gq", - "bindings": { - "g q": "vim::CurrentLine", - "q": "vim::CurrentLine", - "g w": "vim::CurrentLine", - "w": "vim::CurrentLine" - } - }, - { - "context": "vim_operator == y", - "bindings": { - "y": "vim::CurrentLine", - "v": "vim::PushForcedMotion", - "s": ["vim::PushAddSurrounds", {}] - } - }, - { - "context": "vim_operator == ys", - "bindings": { - "s": "vim::CurrentLine" - } - }, - { - "context": "vim_operator == >", - "bindings": { - ">": "vim::CurrentLine" - } - }, - { - "context": "vim_operator == <", - "bindings": { - "<": "vim::CurrentLine" - } - }, - { - "context": "vim_operator == eq", - "bindings": { - "=": "vim::CurrentLine" - } - }, - { - "context": "vim_operator == sh", - "bindings": { - "!": "vim::CurrentLine" - } - }, - { - "context": "vim_operator == gc", - "bindings": { - "c": "vim::CurrentLine" - } - }, - { - "context": "vim_operator == gR", - "bindings": { - "r": "vim::CurrentLine", - "shift-r": "vim::CurrentLine" - } - }, - { - "context": "vim_operator == cx", - "bindings": { - "x": "vim::CurrentLine", - "c": "vim::ClearExchange" - } - }, - { - "context": "vim_mode == literal", - "bindings": { - "ctrl-@": ["vim::Literal", ["ctrl-@", "\u0000"]], - "ctrl-a": ["vim::Literal", ["ctrl-a", "\u0001"]], - "ctrl-b": ["vim::Literal", ["ctrl-b", "\u0002"]], - "ctrl-c": ["vim::Literal", ["ctrl-c", "\u0003"]], - "ctrl-d": ["vim::Literal", ["ctrl-d", "\u0004"]], - "ctrl-e": ["vim::Literal", ["ctrl-e", "\u0005"]], - "ctrl-f": ["vim::Literal", ["ctrl-f", "\u0006"]], - "ctrl-g": ["vim::Literal", ["ctrl-g", "\u0007"]], - "ctrl-h": ["vim::Literal", ["ctrl-h", "\u0008"]], - "ctrl-i": ["vim::Literal", ["ctrl-i", "\u0009"]], - "ctrl-j": ["vim::Literal", ["ctrl-j", "\u000A"]], - "ctrl-k": ["vim::Literal", ["ctrl-k", "\u000B"]], - "ctrl-l": ["vim::Literal", ["ctrl-l", "\u000C"]], - "ctrl-m": ["vim::Literal", ["ctrl-m", "\u000D"]], - "ctrl-n": ["vim::Literal", ["ctrl-n", "\u000E"]], - "ctrl-o": ["vim::Literal", ["ctrl-o", "\u000F"]], - "ctrl-p": ["vim::Literal", ["ctrl-p", "\u0010"]], - "ctrl-q": ["vim::Literal", ["ctrl-q", "\u0011"]], - "ctrl-r": ["vim::Literal", ["ctrl-r", "\u0012"]], - "ctrl-s": ["vim::Literal", ["ctrl-s", "\u0013"]], - "ctrl-t": ["vim::Literal", ["ctrl-t", "\u0014"]], - "ctrl-u": ["vim::Literal", ["ctrl-u", "\u0015"]], - "ctrl-v": ["vim::Literal", ["ctrl-v", "\u0016"]], - "ctrl-w": ["vim::Literal", ["ctrl-w", "\u0017"]], - "ctrl-x": ["vim::Literal", ["ctrl-x", "\u0018"]], - "ctrl-y": ["vim::Literal", ["ctrl-y", "\u0019"]], - "ctrl-z": ["vim::Literal", ["ctrl-z", "\u001A"]], - "ctrl-[": ["vim::Literal", ["ctrl-[", "\u001B"]], - "ctrl-\\": ["vim::Literal", ["ctrl-\\", "\u001C"]], - "ctrl-]": ["vim::Literal", ["ctrl-]", "\u001D"]], - "ctrl-^": ["vim::Literal", ["ctrl-^", "\u001E"]], - "ctrl-_": ["vim::Literal", ["ctrl-_", "\u001F"]], - "escape": ["vim::Literal", ["escape", "\u001B"]], - "enter": ["vim::Literal", ["enter", "\u000D"]], - "tab": ["vim::Literal", ["tab", "\u0009"]], - // zed extensions: - "backspace": ["vim::Literal", ["backspace", "\u0008"]], - "delete": ["vim::Literal", ["delete", "\u007F"]] - } - }, - { - "context": "BufferSearchBar && !in_replace", - "bindings": { - "enter": "vim::SearchSubmit", - "escape": "buffer_search::Dismiss" - } - }, - { - "context": "VimControl && !menu || !Editor && !Terminal", - "bindings": { - // window related commands (ctrl-w X) - "ctrl-w": null, - "ctrl-w left": "workspace::ActivatePaneLeft", - "ctrl-w right": "workspace::ActivatePaneRight", - "ctrl-w up": "workspace::ActivatePaneUp", - "ctrl-w down": "workspace::ActivatePaneDown", - "ctrl-w ctrl-h": "workspace::ActivatePaneLeft", - "ctrl-w ctrl-l": "workspace::ActivatePaneRight", - "ctrl-w ctrl-k": "workspace::ActivatePaneUp", - "ctrl-w ctrl-j": "workspace::ActivatePaneDown", - "ctrl-w h": "workspace::ActivatePaneLeft", - "ctrl-w l": "workspace::ActivatePaneRight", - "ctrl-w k": "workspace::ActivatePaneUp", - "ctrl-w j": "workspace::ActivatePaneDown", - "ctrl-w shift-left": "workspace::SwapPaneLeft", - "ctrl-w shift-right": "workspace::SwapPaneRight", - "ctrl-w shift-up": "workspace::SwapPaneUp", - "ctrl-w shift-down": "workspace::SwapPaneDown", - "ctrl-w x": "workspace::SwapPaneAdjacent", - "ctrl-w ctrl-x": "workspace::SwapPaneAdjacent", - "ctrl-w shift-h": "workspace::MovePaneLeft", - "ctrl-w shift-l": "workspace::MovePaneRight", - "ctrl-w shift-k": "workspace::MovePaneUp", - "ctrl-w shift-j": "workspace::MovePaneDown", - "ctrl-w >": "vim::ResizePaneRight", - "ctrl-w <": "vim::ResizePaneLeft", - "ctrl-w -": "vim::ResizePaneDown", - "ctrl-w +": "vim::ResizePaneUp", - "ctrl-w _": "vim::MaximizePane", - "ctrl-w =": "vim::ResetPaneSizes", - "ctrl-w g t": "pane::ActivateNextItem", - "ctrl-w ctrl-g t": "pane::ActivateNextItem", - "ctrl-w g shift-t": "pane::ActivatePreviousItem", - "ctrl-w ctrl-g shift-t": "pane::ActivatePreviousItem", - "ctrl-w w": "workspace::ActivateNextPane", - "ctrl-w ctrl-w": "workspace::ActivateNextPane", - "ctrl-w p": "workspace::ActivatePreviousPane", - "ctrl-w ctrl-p": "workspace::ActivatePreviousPane", - "ctrl-w shift-w": "workspace::ActivatePreviousPane", - "ctrl-w ctrl-shift-w": "workspace::ActivatePreviousPane", - "ctrl-w ctrl-v": "pane::SplitVertical", - "ctrl-w v": "pane::SplitVertical", - "ctrl-w shift-s": "pane::SplitHorizontal", - "ctrl-w ctrl-s": "pane::SplitHorizontal", - "ctrl-w s": "pane::SplitHorizontal", - "ctrl-w ctrl-c": "pane::CloseActiveItem", - "ctrl-w c": "pane::CloseActiveItem", - "ctrl-w ctrl-q": "pane::CloseActiveItem", - "ctrl-w q": "pane::CloseActiveItem", - "ctrl-w ctrl-a": "pane::CloseAllItems", - "ctrl-w a": "pane::CloseAllItems", - "ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes", - "ctrl-w o": "workspace::CloseInactiveTabsAndPanes", - "ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal", - "ctrl-w n": "workspace::NewFileSplitHorizontal", - "g t": "vim::GoToTab", - "g shift-t": "vim::GoToPreviousTab" - } - }, - { - "context": "!Editor && !Terminal", - "bindings": { - ":": "command_palette::Toggle", - "g /": "pane::DeploySearch", - "] b": "pane::ActivateNextItem", - "[ b": "pane::ActivatePreviousItem", - "] shift-b": "pane::ActivateLastItem", - "[ shift-b": ["pane::ActivateItem", 0] - } - }, - { - // netrw compatibility - "context": "ProjectPanel && not_editing", - "bindings": { - ":": "command_palette::Toggle", - "%": "project_panel::NewFile", - "/": "project_panel::NewSearchInDirectory", - "d": "project_panel::NewDirectory", - "enter": "project_panel::OpenPermanent", - "escape": "vim::ToggleProjectPanelFocus", - "h": "project_panel::CollapseSelectedEntry", - "j": "vim::MenuSelectNext", - "k": "vim::MenuSelectPrevious", - "down": "vim::MenuSelectNext", - "up": "vim::MenuSelectPrevious", - "l": "project_panel::ExpandSelectedEntry", - "shift-d": "project_panel::Delete", - "shift-r": "project_panel::Rename", - "t": "project_panel::OpenPermanent", - "v": "project_panel::OpenSplitVertical", - "o": "project_panel::OpenSplitHorizontal", - "p": "project_panel::Open", - "x": "project_panel::RevealInFileManager", - "s": "workspace::OpenWithSystem", - "z d": "project_panel::CompareMarkedFiles", - "] c": "project_panel::SelectNextGitEntry", - "[ c": "project_panel::SelectPrevGitEntry", - "] d": "project_panel::SelectNextDiagnostic", - "[ d": "project_panel::SelectPrevDiagnostic", - "}": "project_panel::SelectNextDirectory", - "{": "project_panel::SelectPrevDirectory", - "shift-g": "menu::SelectLast", - "g g": "menu::SelectFirst", - "-": "project_panel::SelectParent", - "ctrl-u": "project_panel::ScrollUp", - "ctrl-d": "project_panel::ScrollDown", - "z t": "project_panel::ScrollCursorTop", - "z z": "project_panel::ScrollCursorCenter", - "z b": "project_panel::ScrollCursorBottom", - "0": ["vim::Number", 0], - "1": ["vim::Number", 1], - "2": ["vim::Number", 2], - "3": ["vim::Number", 3], - "4": ["vim::Number", 4], - "5": ["vim::Number", 5], - "6": ["vim::Number", 6], - "7": ["vim::Number", 7], - "8": ["vim::Number", 8], - "9": ["vim::Number", 9] - } - }, - { - "context": "OutlinePanel && not_editing", - "bindings": { - "j": "menu::SelectNext", - "k": "menu::SelectPrevious", - "shift-g": "menu::SelectLast", - "g g": "menu::SelectFirst" - } - }, - { - "context": "GitPanel && ChangesList", - "use_key_equivalents": true, - "bindings": { - "k": "menu::SelectPrevious", - "j": "menu::SelectNext", - "g g": "menu::SelectFirst", - "shift-g": "menu::SelectLast", - "g f": "menu::Confirm", - "i": "git_panel::FocusEditor", - "x": "git::ToggleStaged", - "shift-x": "git::StageAll", - "g x": "git::StageRange", - "shift-u": "git::UnstageAll" - } - }, - { - "context": "Editor && mode == auto_height && VimControl", - "bindings": { - // TODO: Implement search - "/": null, - "?": null, - "#": null, - "*": null, - "n": null, - "shift-n": null - } - }, - { - "context": "Picker > Editor", - "bindings": { - "ctrl-h": "editor::Backspace", - "ctrl-u": "editor::DeleteToBeginningOfLine", - "ctrl-w": "editor::DeleteToPreviousWordStart", - "ctrl-p": "menu::SelectPrevious", - "ctrl-n": "menu::SelectNext" - } - }, - { - "context": "GitCommit > Editor && VimControl && vim_mode == normal", - "bindings": { - "ctrl-c": "menu::Cancel", - "escape": "menu::Cancel" - } - }, - { - "context": "Editor && edit_prediction", - "bindings": { - // This is identical to the binding in the base keymap, but the vim bindings above to - // "vim::Tab" shadow it, so it needs to be bound again. - "tab": "editor::AcceptEditPrediction" - } - }, - { - "context": "MessageEditor > Editor && VimControl", - "bindings": { - "enter": "agent::Chat" - } - }, - { - "context": "os != macos && Editor && edit_prediction_conflict", - "bindings": { - // 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 on Linux - // and Windows. - "alt-l": "editor::AcceptEditPrediction" - } - }, - { - "context": "SettingsWindow > NavigationMenu && !search", - "bindings": { - "l": "settings_editor::ExpandNavEntry", - "h": "settings_editor::CollapseNavEntry", - "k": "settings_editor::FocusPreviousNavEntry", - "j": "settings_editor::FocusNextNavEntry", - "g g": "settings_editor::FocusFirstNavEntry", - "shift-g": "settings_editor::FocusLastNavEntry" - } - }, - { - "context": "MarkdownPreview", - "bindings": { - "ctrl-u": "markdown::ScrollPageUp", - "ctrl-d": "markdown::ScrollPageDown", - "ctrl-y": "markdown::ScrollUp", - "ctrl-e": "markdown::ScrollDown" - } - } -] diff --git a/assets/prompts/content_prompt.hbs b/assets/prompts/content_prompt.hbs deleted file mode 100644 index 6db53ff48f..0000000000 --- a/assets/prompts/content_prompt.hbs +++ /dev/null @@ -1,78 +0,0 @@ -{{#if language_name}} -Here's a file of {{language_name}} that I'm going to ask you to make an edit to. -{{else}} -Here's a file of text that I'm going to ask you to make an edit to. -{{/if}} - -{{#if is_insert}} -The point you'll need to insert at is marked with . -{{else}} -The section you'll need to rewrite is marked with tags. -{{/if}} - - -{{{document_content}}} - - -{{#if is_truncated}} -The context around the relevant section has been truncated (possibly in the middle of a line) for brevity. -{{/if}} - -{{#if is_insert}} -You can't replace {{content_type}}, your answer will be inserted in place of the `` tags. Don't include the insert_here tags in your output. - -Generate {{content_type}} based on the following prompt: - - -{{{user_prompt}}} - - -Match the indentation in the original file in the inserted {{content_type}}, don't include any indentation on blank lines. - -Return ONLY the {{content_type}} to insert. Do NOT include any XML tags like , , or any surrounding markup from the input. - -Respond with a code block containing the {{content_type}} to insert. Replace \{{INSERTED_CODE}} with your actual {{content_type}}: - -``` -\{{INSERTED_CODE}} -``` -{{else}} -Edit the section of {{content_type}} in tags based on the following prompt: - - -{{{user_prompt}}} - - -{{#if rewrite_section}} -And here's the section to rewrite based on that prompt again for reference: - - -{{{rewrite_section}}} - - -{{#if diagnostic_errors}} -Below are the diagnostic errors visible to the user. If the user requests problems to be fixed, use this information, but do not try to fix these errors if the user hasn't asked you to. - -{{#each diagnostic_errors}} - - {{line_number}} - {{error_message}} - {{code_content}} - -{{/each}} -{{/if}} - -{{/if}} - -Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {{content_type}} will be preserved. - -Start at the indentation level in the original file in the rewritten {{content_type}}. Don't stop until you've rewritten the entire section, even if you have no more changes to make, always write out the whole section with no unnecessary elisions. - -Return ONLY the rewritten {{content_type}}. Do NOT include any XML tags like , , or any surrounding markup from the input. - -Respond with a code block containing the rewritten {{content_type}}. Replace \{{REWRITTEN_CODE}} with your actual rewritten {{content_type}}: - -``` -\{{REWRITTEN_CODE}} -``` -{{/if}} diff --git a/assets/prompts/content_prompt_v2.hbs b/assets/prompts/content_prompt_v2.hbs deleted file mode 100644 index e1b6ddc6f0..0000000000 --- a/assets/prompts/content_prompt_v2.hbs +++ /dev/null @@ -1,44 +0,0 @@ -{{#if language_name}} -Here's a file of {{language_name}} that the user is going to ask you to make an edit to. -{{else}} -Here's a file of text that the user is going to ask you to make an edit to. -{{/if}} - -The section you'll need to rewrite is marked with tags. - - -{{{document_content}}} - - -{{#if is_truncated}} -The context around the relevant section has been truncated (possibly in the middle of a line) for brevity. -{{/if}} - -{{#if rewrite_section}} -And here's the section to rewrite based on that prompt again for reference: - - -{{{rewrite_section}}} - - -{{#if diagnostic_errors}} -Below are the diagnostic errors visible to the user. If the user requests problems to be fixed, use this information, but do not try to fix these errors if the user hasn't asked you to. - -{{#each diagnostic_errors}} - - {{line_number}} - {{error_message}} - {{code_content}} - -{{/each}} -{{/if}} - -{{/if}} - -Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {{content_type}} will be preserved. - -Start at the indentation level in the original file in the rewritten {{content_type}}. - -You must use one of the provided tools to make the rewrite or to provide an explanation as to why the user's request cannot be fulfilled. It is an error if -you simply send back unstructured text. If you need to make a statement or ask a question you must use one of the tools to do so. -It is an error if you try to make a change that cannot be made simply by editing the rewrite_section. diff --git a/assets/prompts/terminal_assistant_prompt.hbs b/assets/prompts/terminal_assistant_prompt.hbs deleted file mode 100644 index b315e63158..0000000000 --- a/assets/prompts/terminal_assistant_prompt.hbs +++ /dev/null @@ -1,18 +0,0 @@ -You are an expert terminal user. -You will be given a description of a command and you need to respond with a command that matches the description. -Do not include markdown blocks or any other text formatting in your response, always respond with a single command that can be executed in the given shell. -Current OS name is '{{os}}', architecture is '{{arch}}'. -{{#if shell}} -Current shell is '{{shell}}'. -{{/if}} -{{#if working_directory}} -Current working directory is '{{working_directory}}'. -{{/if}} -{{#if latest_output}} -Latest non-empty terminal output: -{{#each latest_output as |line|}} -{{line}} -{{/each}} -{{/if}} -Here is the description of the command: -{{{user_prompt}}} diff --git a/assets/settings/default.json b/assets/settings/default.json deleted file mode 100644 index 2eea3c34c6..0000000000 --- a/assets/settings/default.json +++ /dev/null @@ -1,2257 +0,0 @@ -{ - "$schema": "zed://schemas/settings", - /// The displayed name of this project. If not set or null, the root directory name - /// will be displayed. - "project_name": null, - // The name of the Zed theme to use for the UI. - // - // `mode` is one of: - // - "system": Use the theme that corresponds to the system's appearance - // - "light": Use the theme indicated by the "light" field - // - "dark": Use the theme indicated by the "dark" field - "theme": { - "mode": "system", - "light": "One Light", - "dark": "One Dark", - }, - "icon_theme": "Zed (Default)", - // The name of a base set of key bindings to use. - // This setting can take six values, each named after another - // text editor: - // - // 1. "VSCode" - // 2. "Atom" - // 3. "JetBrains" - // 4. "None" - // 5. "SublimeText" - // 6. "TextMate" - "base_keymap": "VSCode", - // Features that can be globally enabled or disabled - "features": { - // Which edit prediction provider to use. - "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", - // Set the buffer text's font fallbacks, this will be merged with - // the platform's default fallbacks. - "buffer_font_fallbacks": null, - // The OpenType features to enable for text in the editor. - "buffer_font_features": { - // Disable ligatures: - // "calt": false - }, - // The default font size for text in the editor - "buffer_font_size": 15, - // The weight of the editor font in standard CSS units from 100 to 900. - "buffer_font_weight": 400, - // Set the buffer's line height. - // May take 3 values: - // 1. Use a line height that's comfortable for reading (1.618) - // "buffer_line_height": "comfortable" - // 2. Use a standard line height, (1.3) - // "buffer_line_height": "standard", - // 3. Use a custom line height - // "buffer_line_height": { - // "custom": 2 - // }, - "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", - // Set the UI's font fallbacks, this will be merged with the platform's - // default font fallbacks. - "ui_font_fallbacks": null, - // The OpenType features to enable for text in the UI - "ui_font_features": { - // Disable ligatures: - "calt": false, - }, - // The weight of the UI font in standard CSS units from 100 to 900. - "ui_font_weight": 400, - // The default font size for text in the UI - "ui_font_size": 16, - // The default font size for agent responses in the agent panel. Falls back to the UI font size if unset. - "agent_ui_font_size": null, - // The default font size for user messages in the agent panel. - "agent_buffer_font_size": 12, - // How much to fade out unused code. - "unnecessary_code_fade": 0.3, - // Active pane styling settings. - "active_pane_modifiers": { - // Inset border size of the active pane, in pixels. - "border_size": 0.0, - // Opacity of the inactive panes. 0 means transparent, 1 means opaque. - // Values are clamped to the [0.0, 1.0] range. - "inactive_opacity": 1.0, - }, - // 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", - // Centered layout related settings. - "centered_layout": { - // The relative width of the left padding of the central pane from the - // workspace when the centered layout is used. - "left_padding": 0.2, - // The relative width of the right padding of the central pane from the - // workspace when the centered layout is used. - "right_padding": 0.2, - }, - // Image viewer settings - "image_viewer": { - // The unit for image file sizes: "binary" (KiB, MiB) or decimal (KB, MB) - "unit": "binary", - }, - // Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier. - // - // 1. Maps to `Alt` on Linux and Windows and to `Option` on MacOS: - // "alt" - // 2. Maps `Control` on Linux and Windows and to `Command` on MacOS: - // "cmd_or_ctrl" (alias: "cmd", "ctrl") - "multi_cursor_modifier": "alt", - // Whether to enable vim modes and key bindings. - "vim_mode": false, - // Whether to enable helix mode and key bindings. - // Enabling this mode will automatically enable vim mode. - "helix_mode": false, - // Whether to show the informational hover box when moving the mouse - // over symbols in the editor. - "hover_popover_enabled": true, - // Time to wait in milliseconds before showing the informational hover box. - "hover_popover_delay": 300, - // Whether to confirm before quitting Zed. - "confirm_quit": false, - // Whether to restore last closed project when fresh Zed instance is opened - // May take 3 values: - // 1. All workspaces open during last session - // "restore_on_startup": "last_session" - // 2. The workspace opened - // "restore_on_startup": "last_workspace", - // 3. Do not restore previous workspaces - // "restore_on_startup": "none", - "restore_on_startup": "last_session", - // Whether to attempt to restore previous file's state when opening it again. - // The state is stored per pane. - // When disabled, defaults are applied instead of the state restoration. - // - // E.g. for editors, selections, folds and scroll positions are restored, if the same file is closed and, later, opened again in the same pane. - // When disabled, a single selection in the very beginning of the file, zero scroll position and no folds state is used as a default. - // - // Default: true - "restore_on_file_reopen": true, - // Whether to automatically close files that have been deleted on disk. - "close_on_file_delete": false, - // Relative size of the drop target in the editor that will open dropped file as a split pane (0-0.5) - // E.g. 0.25 == If you drop onto the top/bottom quarter of the pane a new vertical split will be used - // If you drop onto the left/right quarter of the pane a new horizontal split will be used - "drop_target_size": 0.2, - // Whether the window should be closed when using 'close active item' on a window with no tabs. - // May take 3 values: - // 1. Use the current platform's convention - // "when_closing_with_no_tabs": "platform_default" - // 2. Always close the window: - // "when_closing_with_no_tabs": "close_window", - // 3. Never close the window - // "when_closing_with_no_tabs": "keep_window_open", - "when_closing_with_no_tabs": "platform_default", - // What to do when the last window is closed. - // May take 2 values: - // 1. Use the current platform's convention - // "on_last_window_closed": "platform_default" - // 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, - // What draws Zed's window decorations (titlebar): - // 1. Client application (Zed) draws its own window decorations - // "client" - // 2. Display server draws the window decorations. Not supported by GNOME Wayland. - // "server" - // - // This requires restarting Zed for changes to take effect. - // - // Default: "client" - "window_decorations": "client", - // 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, - // Whether to use the system provided dialogs for prompts, such as confirmation - // prompts. - // When set to false, Zed will use its built-in prompts. Note that on Linux, - // this option is ignored and Zed will always use the built-in prompts. - "use_system_prompts": true, - // Whether the cursor blinks in the editor. - "cursor_blink": true, - // Cursor shape for the default editor. - // 1. A vertical bar - // "bar" - // 2. A block that surrounds the following character - // "block" - // 3. An underline / underscore that runs along the following character - // "underline" - // 4. A box drawn around the following character - // "hollow" - // - // Default: "bar" - "cursor_shape": "bar", - // Determines when the mouse cursor should be hidden in an editor or input box. - // - // 1. Never hide the mouse cursor: - // "never" - // 2. Hide only when typing: - // "on_typing" - // 3. Hide on both typing and cursor movement: - // "on_typing_and_movement" - "hide_mouse": "on_typing_and_movement", - // Determines how snippets are sorted relative to other completion items. - // - // 1. Place snippets at the top of the completion list: - // "top" - // 2. Place snippets normally without any preference: - // "inline" - // 3. Place snippets at the bottom of the completion list: - // "bottom" - // 4. Do not show snippets in the completion list: - // "none" - "snippet_sort_order": "inline", - // How to highlight the current line in the editor. - // - // 1. Don't highlight the current line: - // "none" - // 2. Highlight the gutter area: - // "gutter" - // 3. Highlight the editor area: - // "line" - // 4. Highlight the full line (default): - // "all" - "current_line_highlight": "all", - // Whether to highlight all occurrences of the selected text in an editor. - "selection_highlight": true, - // Whether the text selection should have rounded corners. - "rounded_selection": true, - // The debounce delay before querying highlights from the language - // server based on the current cursor location. - "lsp_highlight_debounce": 75, - // The minimum APCA perceptual contrast between foreground and background colors. - // APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x, - // especially for dark mode. Values range from 0 to 106. - // - // Based on APCA Readability Criterion (ARC) Bronze Simple Mode: - // https://readtech.org/ARC/tests/bronze-simple-mode/ - // - 0: No contrast adjustment - // - 45: Minimum for large fluent text (36px+) - // - 60: Minimum for other content text - // - 75: Minimum for body text - // - 90: Preferred for body text - // - // This only affects text drawn over highlight backgrounds in the editor. - "minimum_contrast_for_highlights": 45, - // Whether to pop the completions menu while typing in an editor without - // explicitly requesting it. - "show_completions_on_input": true, - // Whether to display inline and alongside documentation for items in the - // completions menu - "show_completion_documentation": true, - // Whether to colorize brackets in the editor. - // (also known as "rainbow brackets") - // - // The colors that are used for different indentation levels are defined in the theme (theme key: `accents`). - // They can be customized by using theme overrides. - "colorize_brackets": false, - // When to show the scrollbar in the completion menu. - // This setting can take four values: - // - // 1. Show the scrollbar if there's important information or - // follow the system's configured behavior - // "auto" - // 2. Match the system's configured behavior: - // "system" - // 3. Always show the scrollbar: - // "always" - // 4. Never show the scrollbar: - // "never" (default) - "completion_menu_scrollbar": "never", - // Show method signatures in the editor, when inside parentheses. - "auto_signature_help": false, - // Whether to show the signature help after completion or a bracket pair inserted. - // If `auto_signature_help` is enabled, this setting will be treated as enabled also. - "show_signature_help_after_edits": false, - // Whether to show code action button at start of buffer line. - "inline_code_actions": true, - // Whether to allow drag and drop text selection in buffer. - "drag_and_drop_selection": { - // When true, enables drag and drop text selection in buffer. - "enabled": true, - // The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created. - "delay": 300, - }, - // What to do when go to definition yields no results. - // - // 1. Do nothing: `none` - // 2. Find references for the same symbol: `find_all_references` (default) - "go_to_definition_fallback": "find_all_references", - // Which level to use to filter out diagnostics displayed in the editor. - // - // Affects the editor rendering only, and does not interrupt - // the functionality of diagnostics fetching and project diagnostics editor. - // Which files containing diagnostic errors/warnings to mark in the tabs. - // Diagnostics are only shown when file icons are also active. - // This setting only works when can take the following three values: - // - // Which diagnostic indicators to show in the scrollbar, their level should be more or equal to the specified severity level. - // Possible values: - // - "off" — no diagnostics are allowed - // - "error" - // - "warning" - // - "info" - // - "hint" - // - "all" — allow all diagnostics (default) - "diagnostics_max_severity": "all", - // Whether to show wrap guides (vertical rulers) in the editor. - // Setting this to true will show a guide at the 'preferred_line_length' value - // if 'soft_wrap' is set to 'preferred_line_length', and will show any - // additional guides as specified by the 'wrap_guides' setting. - "show_wrap_guides": true, - // Character counts at which to show wrap guides in the editor. - "wrap_guides": [], - // Hide the values of in variables from visual display in private files - "redact_private_values": false, - // The default number of lines to expand excerpts in the multibuffer by. - "expand_excerpt_lines": 5, - // The default number of context lines shown in multibuffer excerpts. - "excerpt_context_lines": 2, - // Globs to match against file paths to determine if a file is private. - "private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"], - // Whether to use additional LSP queries to format (and amend) the code after - // every "trigger" symbol input, defined by LSP server capabilities. - "use_on_type_format": true, - // Whether to automatically add matching closing characters when typing - // opening parenthesis, bracket, brace, single or double quote characters. - // For example, when you type '(', Zed will add a closing ) at the correct position. - "use_autoclose": true, - // Whether to automatically surround selected text when typing opening parenthesis, - // 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. - // When set to `false`(default), skipping over and auto-removing of the closing characters - // happen only for auto-inserted characters. - // Otherwise(when `true`), the closing characters are always skipped over and auto-removed - // no matter how they were inserted. - "always_treat_brackets_as_autoclosed": false, - // Controls where the `editor::Rewrap` action is allowed in the current language scope. - // - // This setting can take three values: - // - // 1. Only allow rewrapping in comments: - // "in_comments" - // 2. Only allow rewrapping in the current selection(s): - // "in_selections" - // 3. Allow rewrapping anywhere: - // "anywhere" - // - // When using values other than `in_comments`, it is possible for the rewrapping to produce code - // that is syntactically invalid. Keep this in mind when selecting which behavior you would like - // to use. - // - // Note: This setting has no effect in Vim mode, as rewrap is already allowed everywhere. - "allow_rewrap": "in_comments", - // Controls whether edit predictions are shown immediately (true) - // or manually by triggering `editor::ShowEditPrediction` (false). - "show_edit_predictions": true, - // Controls whether edit predictions are shown in a given language scope. - // Example: ["string", "comment"] - "edit_predictions_disabled_in": [], - // Whether to show tabs and spaces in the editor. - // This setting can take four values: - // - // 1. Draw tabs and spaces only for the selected text (default): - // "selection" - // 2. Do not draw any tabs or spaces: - // "none" - // 3. Draw all invisible symbols: - // "all" - // 4. Draw whitespaces at boundaries only: - // "boundary" - // 5. Draw whitespaces only after non-whitespace characters: - // "trailing" - // For a whitespace to be on a boundary, any of the following conditions need to be met: - // - It is a tab - // - It is adjacent to an edge (start or end) - // - It is adjacent to a whitespace (left or right) - "show_whitespaces": "selection", - // Visible characters used to render whitespace when show_whitespaces is enabled. - "whitespace_map": { - "space": "•", - "tab": "→", - }, - // Settings related to calls in Zed - "calls": { - // Join calls with the microphone live by default - "mute_on_join": false, - // Share your project when you are the first to join a channel - "share_on_join": false, - }, - // Toolbar related settings - "toolbar": { - // Whether to show breadcrumbs. - "breadcrumbs": true, - // Whether to show quick action buttons. - "quick_actions": true, - // Whether to show the Selections menu in the editor toolbar. - "selections_menu": true, - // Whether to show agent review buttons in the editor toolbar. - "agent_review": true, - // Whether to show code action buttons in the editor toolbar. - "code_actions": false, - }, - // Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). - "use_system_window_tabs": false, - // Titlebar related settings - "title_bar": { - // Whether to show the branch icon beside branch switcher in the titlebar. - "show_branch_icon": false, - // Whether to show the branch name button in the titlebar. - "show_branch_name": true, - // Whether to show the project host and name in the titlebar. - "show_project_items": true, - // Whether to show onboarding banners in the titlebar. - "show_onboarding_banner": true, - // Whether to show user picture in the titlebar. - "show_user_picture": true, - // Whether to show the sign in button in the titlebar. - "show_sign_in": true, - // Whether to show the menus in the titlebar. - "show_menus": false, - }, - "audio": { - // Opt into the new audio system. - "experimental.rodio_audio": false, - // Requires 'rodio_audio: true' - // - // Automatically increase or decrease you microphone's volume. This affects how - // loud you sound to others. - // - // Recommended: off (default) - // Microphones are too quite in zed, until everyone is on experimental - // audio and has auto speaker volume on this will make you very loud - // compared to other speakers. - "experimental.auto_microphone_volume": false, - // Requires 'rodio_audio: true' - // - // Automatically increate or decrease the volume of other call members. - // This only affects how things sound for you. - "experimental.auto_speaker_volume": true, - // Requires 'rodio_audio: true' - // - // Remove background noises. Works great for typing, cars, dogs, AC. Does - // not work well on music. - "experimental.denoise": true, - // Requires 'rodio_audio: true' - // - // Use audio parameters compatible with the previous versions of - // experimental audio and non-experimental audio. When this is false you - // will sound strange to anyone not on the latest experimental audio. In - // the future we will migrate by setting this to false - // - // You need to rejoin a call for this setting to apply - "experimental.legacy_audio_compatible": true, - }, - // Scrollbar related settings - "scrollbar": { - // When to show the scrollbar in the editor. - // This setting can take four values: - // - // 1. Show the scrollbar if there's important information or - // follow the system's configured behavior (default): - // "auto" - // 2. Match the system's configured behavior: - // "system" - // 3. Always show the scrollbar: - // "always" - // 4. Never show the scrollbar: - // "never" - "show": "auto", - // Whether to show cursor positions in the scrollbar. - "cursors": true, - // Whether to show git diff indicators in the scrollbar. - "git_diff": true, - // Whether to show buffer search results in the scrollbar. - "search_results": true, - // Whether to show selected text occurrences in the scrollbar. - "selected_text": true, - // Whether to show selected symbol occurrences in the scrollbar. - "selected_symbol": true, - // Which diagnostic indicators to show in the scrollbar: - // - "none" or false: do not show diagnostics - // - "error": show only errors - // - "warning": show only errors and warnings - // - "information": show only errors, warnings, and information - // - "all" or true: show all diagnostics - "diagnostics": "all", - // Forcefully enable or disable the scrollbar for each axis - "axes": { - // When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings. - "horizontal": true, - // When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings. - "vertical": true, - }, - }, - // Minimap related settings - "minimap": { - // When to show the minimap in the editor. - // This setting can take three values: - // 1. Show the minimap if the editor's scrollbar is visible: - // "auto" - // 2. Always show the minimap: - // "always" - // 3. Never show the minimap: - // "never" (default) - "show": "never", - // Where to show the minimap in the editor. - // This setting can take two values: - // 1. Show the minimap on the focused editor only: - // "active_editor" (default) - // 2. Show the minimap on all open editors: - // "all_editors" - "display_in": "active_editor", - // When to show the minimap thumb. - // This setting can take two values: - // 1. Show the minimap thumb if the mouse is over the minimap: - // "hover" - // 2. Always show the minimap thumb: - // "always" (default) - "thumb": "always", - // How the minimap thumb border should look. - // This setting can take five values: - // 1. Display a border on all sides of the thumb: - // "thumb_border": "full" - // 2. Display a border on all sides except the left side of the thumb: - // "thumb_border": "left_open" (default) - // 3. Display a border on all sides except the right side of the thumb: - // "thumb_border": "right_open" - // 4. Display a border only on the left side of the thumb: - // "thumb_border": "left_only" - // 5. Display the thumb without any border: - // "thumb_border": "none" - "thumb_border": "left_open", - // How to highlight the current line in the minimap. - // This setting can take the following values: - // - // 1. `null` to inherit the editor `current_line_highlight` setting (default) - // 2. "line" or "all" to highlight the current line in the minimap. - // 3. "gutter" or "none" to not highlight the current line in the minimap. - "current_line_highlight": null, - // Maximum number of columns to display in the minimap. - "max_width_columns": 80, - }, - // Enable middle-click paste on Linux. - "middle_click_paste": true, - // What to do when multibuffer is double clicked in some of its excerpts - // (parts of singleton buffers). - // May take 2 values: - // 1. Behave as a regular buffer and select the whole word (default). - // "double_click_in_multibuffer": "select" - // 2. Open the excerpt clicked as a new buffer in the new tab. - // "double_click_in_multibuffer": "open", - // For the case of "open", regular selection behavior can be achieved by holding `alt` when double clicking. - "double_click_in_multibuffer": "select", - "gutter": { - // Whether to show line numbers in the gutter. - "line_numbers": true, - // Whether to show runnables buttons in the gutter. - "runnables": true, - // Whether to show breakpoints in the gutter. - "breakpoints": true, - // Whether to show fold buttons in the gutter. - "folds": true, - // Minimum number of characters to reserve space for in the gutter. - "min_line_number_digits": 4, - }, - "indent_guides": { - // Whether to show indent guides in the editor. - "enabled": true, - // The width of the indent guides in pixels, between 1 and 10. - "line_width": 1, - // The width of the active indent guide in pixels, between 1 and 10. - "active_line_width": 1, - // Determines how indent guides are colored. - // This setting can take the following three values: - // - // 1. "disabled" - // 2. "fixed" - // 3. "indent_aware" - "coloring": "fixed", - // Determines how indent guide backgrounds are colored. - // This setting can take the following two values: - // - // 1. "disabled" - // 2. "indent_aware" - "background_coloring": "disabled", - }, - // Whether the editor will scroll beyond the last line. - "scroll_beyond_last_line": "one_page", - // The number of lines to keep above/below the cursor when scrolling with the keyboard - "vertical_scroll_margin": 3, - // Whether to scroll when clicking near the edge of the visible text area. - "autoscroll_on_clicks": false, - // The number of characters to keep on either side when scrolling with the mouse - "horizontal_scroll_margin": 5, - // Scroll sensitivity multiplier. This multiplier is applied - // to both the horizontal and vertical delta values while scrolling. - "scroll_sensitivity": 1.0, - // Scroll sensitivity multiplier for fast scrolling. This multiplier is applied - // to both the horizontal and vertical delta values while scrolling. Fast scrolling - // happens when a user holds the alt or option key while scrolling. - "fast_scroll_sensitivity": 4.0, - "sticky_scroll": { - // Whether to stick scopes to the top of the editor. - "enabled": false, - }, - "relative_line_numbers": "disabled", - // If 'search_wrap' is disabled, search result do not wrap around the end of the file. - "search_wrap": true, - // Search options to enable by default when opening new project and buffer searches. - "search": { - // Whether to show the project search button in the status bar. - "button": true, - // Whether to only match on whole words. - "whole_word": false, - // Whether to match case sensitively. - "case_sensitive": false, - // Whether to include gitignored files in search results. - "include_ignored": false, - // Whether to interpret the search query as a regular expression. - "regex": false, - // Whether to center the cursor on each search match when navigating. - "center_on_match": false, - }, - // When to populate a new search's query based on the text under the cursor. - // This setting can take the following three values: - // - // 1. Always populate the search query with the word under the cursor (default). - // "always" - // 2. Only populate the search query when there is text selected - // "selection" - // 3. Never populate the search query - // "never" - "seed_search_query_from_cursor": "always", - // When enabled, automatically adjusts search case sensitivity based on your query. - // If your search query contains any uppercase letters, the search becomes case-sensitive; - // if it contains only lowercase letters, the search becomes case-insensitive. - "use_smartcase_search": false, - // Inlay hint related settings - "inlay_hints": { - // Global switch to toggle hints on and off, switched off by default. - "enabled": false, - // Toggle certain types of hints on and off, all switched on by default. - "show_type_hints": true, - "show_parameter_hints": true, - "show_value_hints": true, - // Corresponds to null/None LSP hint type value. - "show_other_hints": true, - // Whether to show a background for inlay hints. - // - // If set to `true`, the background will use the `hint.background` color from the current theme. - "show_background": false, - // Time to wait after editing the buffer, before requesting the hints, - // set to 0 to disable debouncing. - "edit_debounce_ms": 700, - // Time to wait after scrolling the buffer, before requesting the hints, - // set to 0 to disable debouncing. - "scroll_debounce_ms": 50, - // A set of modifiers which, when pressed, will toggle the visibility of inlay hints. - // If the set if empty or not all the modifiers specified are pressed, inlay hints will not be toggled. - "toggle_on_modifiers_press": { - "control": false, - "shift": false, - "alt": false, - "platform": false, - "function": false, - }, - }, - // Whether to resize all the panels in a dock when resizing the dock. - // Can be a combination of "left", "right" and "bottom". - "resize_all_panels_in_dock": ["left"], - "project_panel": { - // Whether to show the project panel button in the status bar - "button": true, - // Whether to hide the gitignore entries in the project panel. - "hide_gitignore": false, - // Default width of the project panel. - "default_width": 240, - // Where to dock the project panel. Can be 'left' or 'right'. - "dock": "left", - // Spacing between worktree entries in the project panel. Can be 'comfortable' or 'standard'. - "entry_spacing": "comfortable", - // Whether to show file icons in the project panel. - "file_icons": true, - // Whether to show folder icons or chevrons for directories in the project panel. - "folder_icons": true, - // Whether to show the git status in the project panel. - "git_status": true, - // Amount of indentation for nested items. - "indent_size": 20, - // Whether to reveal it in the project panel automatically, - // 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, - // Scrollbar-related settings - "scrollbar": { - // When to show the scrollbar in the project panel. - // This setting can take five values: - // - // 1. null (default): Inherit editor settings - // 2. Show the scrollbar if there's important information or - // follow the system's configured behavior (default): - // "auto" - // 3. Match the system's configured behavior: - // "system" - // 4. Always show the scrollbar: - // "always" - // 5. Never show the scrollbar: - // "never" - "show": null, - }, - // Which files containing diagnostic errors/warnings to mark in the project panel. - // This setting can take the following three values: - // - // 1. Do not mark any files: - // "off" - // 2. Only mark files with errors: - // "errors" - // 3. Mark files with errors and warnings: - // "all" - "show_diagnostics": "all", - // Whether to stick parent directories at top of the project panel. - "sticky_scroll": true, - // Settings related to indent guides in the project panel. - "indent_guides": { - // When to show indent guides in the project panel. - // This setting can take two values: - // - // 1. Always show indent guides: - // "always" - // 2. Never show indent guides: - // "never" - "show": "always", - }, - // Sort order for entries in the project panel. - // This setting can take three values: - // - // 1. Show directories first, then files: - // "directories_first" - // 2. Mix directories and files together: - // "mixed" - // 3. Show files first, then directories: - // "files_first" - "sort_mode": "directories_first", - // 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, - // Whether to hide the hidden entries in the project panel. - "hide_hidden": false, - // Settings for automatically opening files. - "auto_open": { - // Whether to automatically open newly created files in the editor. - "on_create": true, - // Whether to automatically open files after pasting or duplicating them. - "on_paste": true, - // Whether to automatically open files dropped from external sources. - "on_drop": true, - }, - }, - "outline_panel": { - // Whether to show the outline panel button in the status bar - "button": true, - // Default width of the outline panel. - "default_width": 300, - // Where to dock the outline panel. Can be 'left' or 'right'. - "dock": "left", - // Whether to show file icons in the outline panel. - "file_icons": true, - // Whether to show folder icons or chevrons for directories in the outline panel. - "folder_icons": true, - // Whether to show the git status in the outline panel. - "git_status": true, - // Amount of indentation for nested items. - "indent_size": 20, - // Whether to reveal it in the outline panel automatically, - // when a corresponding outline entry becomes active. - // Gitignored entries are never auto revealed. - "auto_reveal_entries": true, - // Whether to fold directories automatically - // when a directory has only one directory inside. - "auto_fold_dirs": true, - // Settings related to indent guides in the outline panel. - "indent_guides": { - // When to show indent guides in the outline panel. - // This setting can take two values: - // - // 1. Always show indent guides: - // "always" - // 2. Never show indent guides: - // "never" - "show": "always", - }, - // Scrollbar-related settings - "scrollbar": { - // When to show the scrollbar in the project panel. - // This setting can take five values: - // - // 1. null (default): Inherit editor settings - // 2. Show the scrollbar if there's important information or - // follow the system's configured behavior (default): - // "auto" - // 3. Match the system's configured behavior: - // "system" - // 4. Always show the scrollbar: - // "always" - // 5. Never show the scrollbar: - // "never" - "show": null, - }, - // Default depth to expand outline items in the current file. - // Set to 0 to collapse all items that have children, 1 or higher to collapse items at that depth or deeper. - "expand_outlines_with_depth": 100, - }, - "collaboration_panel": { - // Whether to show the collaboration panel button in the status bar. - "button": true, - // Where to dock the collaboration panel. Can be 'left' or 'right'. - "dock": "left", - // Default width of the collaboration panel. - "default_width": 240, - }, - "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'. - "dock": "left", - // Default width of the git panel. - "default_width": 360, - // Style of the git status indicator in the panel. - // - // Choices: label_color, icon - // Default: icon - "status_style": "icon", - // What branch name to use if `init.defaultBranch` is not set - // - // Default: main - "fallback_branch_name": "main", - // Whether to sort entries in the panel by path or by status (the default). - // - // Default: false - "sort_by_path": false, - // Whether to collapse untracked files in the diff panel. - // - // Default: false - "collapse_untracked_diff": false, - /// Whether to show entries with tree or flat view in the panel - /// - /// Default: false - "tree_view": false, - "scrollbar": { - // When to show the scrollbar in the git panel. - // - // Choices: always, auto, never, system - // Default: inherits editor scrollbar settings - // "show": null - }, - }, - "message_editor": { - // Whether to automatically replace emoji shortcodes with emoji characters. - // For example: typing `:wave:` gets replaced with `👋`. - "auto_replace_emoji_shortcode": true, - }, - "notification_panel": { - // Whether to show the notification panel button in the status bar. - "button": true, - // Where to dock the notification panel. Can be 'left' or 'right'. - "dock": "right", - // Default width of the notification panel. - "default_width": 380, - }, - "agent": { - // Whether the agent is enabled. - "enabled": true, - // What completion mode to start new threads in, if available. Can be 'normal' or 'burn'. - "preferred_completion_mode": "normal", - // Whether to show the agent panel button in the status bar. - "button": true, - // Where to dock the agent panel. Can be 'left', 'right' or 'bottom'. - "dock": "right", - // Default width when the agent panel is docked to the left or right. - "default_width": 640, - // Default height when the agent panel is docked to the bottom. - "default_height": 320, - // The view to use by default (thread, or text_thread) - "default_view": "thread", - // The default model to use when creating new threads. - "default_model": { - // The provider to use. - "provider": "zed.dev", - // The model to use. - "model": "claude-sonnet-4", - }, - // Additional parameters for language model requests. When making a request to a model, parameters will be taken - // from the last entry in this list that matches the model's provider and name. In each entry, both provider - // and model are optional, so that you can specify parameters for either one. - "model_parameters": [ - // To set parameters for all requests to OpenAI models: - // { - // "provider": "openai", - // "temperature": 0.5 - // } - // - // To set parameters for all requests in general: - // { - // "temperature": 0 - // } - // - // To set parameters for a specific provider and model: - // { - // "provider": "zed.dev", - // "model": "claude-sonnet-4", - // "temperature": 1.0 - // } - ], - // When enabled, the agent can run potentially destructive actions without asking for your confirmation. - // - // Note: This setting has no effect on external agents that support permission modes, such as Claude Code. - // You can set `agent_servers.claude.default_mode` to `bypassPermissions` to skip all permission requests. - "always_allow_tool_actions": false, - // When enabled, agent edits will be displayed in single-file editors for review - "single_file_review": true, - // When enabled, show voting thumbs for feedback on agent edits. - "enable_feedback": true, - "default_profile": "write", - "profiles": { - "write": { - "name": "Write", - "enable_all_context_servers": true, - "tools": { - "copy_path": true, - "create_directory": true, - "delete_path": true, - "diagnostics": true, - "edit_file": true, - "fetch": true, - "list_directory": true, - "project_notifications": false, - "move_path": true, - "now": true, - "find_path": true, - "read_file": true, - "open": true, - "grep": true, - "terminal": true, - "thinking": true, - "web_search": true, - }, - }, - "ask": { - "name": "Ask", - // We don't know which of the context server tools are safe for the "Ask" profile, so we don't enable them by default. - // "enable_all_context_servers": true, - "tools": { - "diagnostics": true, - "fetch": true, - "list_directory": true, - "project_notifications": false, - "now": true, - "find_path": true, - "read_file": true, - "open": true, - "grep": true, - "thinking": true, - "web_search": true, - }, - }, - "minimal": { - "name": "Minimal", - "enable_all_context_servers": false, - "tools": {}, - }, - }, - // Where to show notifications when the agent has either completed - // its response, or else needs confirmation before it can run a - // tool action. - // "primary_screen" - Show the notification only on your primary screen (default) - // "all_screens" - Show these notifications on all screens - // "never" - Never show these notifications - "notify_when_agent_waiting": "primary_screen", - // Whether to play a sound when the agent has either completed - // its response, or needs user input. - - // Default: false - "play_sound_when_agent_done": false, - // Whether to have edit cards in the agent panel expanded, showing a preview of the full diff. - // - // Default: true - "expand_edit_card": true, - // Whether to have terminal cards in the agent panel expanded, showing the whole command output. - // - // Default: true - "expand_terminal_card": true, - // Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel. - // - // Default: false - "use_modifier_to_send": false, - // Minimum number of lines to display in the agent message editor. - // - // Default: 4 - "message_editor_min_lines": 4, - }, - // Whether the screen sharing icon is shown in the os status bar. - "show_call_status_icon": true, - // Whether to use language servers to provide code intelligence. - "enable_language_server": true, - // Whether to perform linked edits of associated ranges, if the language server supports it. - // For example, when editing opening tag, the contents of the closing tag will be edited as well. - "linked_edits": true, - // The list of language servers to use (or disable) for all languages. - // - // This is typically customized on a per-language basis. - "language_servers": ["..."], - - // When to automatically save edited buffers. This setting can - // take four values. - // - // 1. Never automatically save: - // "autosave": "off", - // 2. Save when changing focus away from the Zed window: - // "autosave": "on_window_change", - // 3. Save when changing focus away from a specific buffer: - // "autosave": "on_focus_change", - // 4. Save when idle for a certain amount of time: - // "autosave": { "after_delay": {"milliseconds": 500} }, - "autosave": "off", - // Maximum number of tabs per pane. Unset for unlimited. - "max_tabs": null, - // Settings related to the editor's tab bar. - "tab_bar": { - // Whether or not to show the tab bar in the editor - "show": true, - // Whether or not to show the navigation history buttons. - "show_nav_history_buttons": true, - // Whether or not to show the tab bar buttons. - "show_tab_bar_buttons": true, - }, - // Settings related to the editor's tabs - "tabs": { - // Show git status colors in the editor tabs. - "git_status": false, - // Position of the close button on the editor tabs. - // One of: ["right", "left"] - "close_position": "right", - // Whether to show the file icon for a tab. - "file_icons": false, - // Controls the appearance behavior of the tab's close button. - // - // 1. Show it just upon hovering the tab. (default) - // "hover" - // 2. Show it persistently. - // "always" - // 3. Never show it, even if hovering it. - // "hidden" - "show_close_button": "hover", - // What to do after closing the current tab. - // - // 1. Activate the tab that was open previously (default) - // "history" - // 2. Activate the right neighbour tab if present - // "neighbour" - // 3. Activate the left neighbour tab if present - // "left_neighbour" - "activate_on_close": "history", - // Which files containing diagnostic errors/warnings to mark in the tabs. - // Diagnostics are only shown when file icons are also active. - // This setting only works when can take the following three values: - // - // 1. Do not mark any files: - // "off" - // 2. Only mark files with errors: - // "errors" - // 3. Mark files with errors and warnings: - // "all" - "show_diagnostics": "off", - }, - // Settings related to preview tabs. - "preview_tabs": { - // Whether preview tabs should be enabled. - // Preview tabs allow you to open files in preview mode, where they close automatically - // when you open another preview tab. - // This is useful for quickly viewing files without cluttering your workspace. - "enabled": true, - // Whether to open tabs in preview mode when opened from the project panel with a single click. - "enable_preview_from_project_panel": true, - // Whether to open tabs in preview mode when selected from the file finder. - "enable_preview_from_file_finder": false, - // Whether to open tabs in preview mode when opened from a multibuffer. - "enable_preview_from_multibuffer": true, - // Whether to open tabs in preview mode when code navigation is used to open a multibuffer. - "enable_preview_multibuffer_from_code_navigation": false, - // Whether to open tabs in preview mode when code navigation is used to open a single file. - "enable_preview_file_from_code_navigation": true, - // Whether to keep tabs in preview mode when code navigation is used to navigate away from them. - // If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one. - "enable_keep_preview_on_code_navigation": false, - }, - // Settings related to the file finder. - "file_finder": { - // Whether to show file icons in the file finder. - "file_icons": true, - // Determines how much space the file finder can take up in relation to the available window width. - // There are 5 possible width values: - // - // 1. Small: This value is essentially a fixed width. - // "modal_max_width": "small" - // 2. Medium: - // "modal_max_width": "medium" - // 3. Large: - // "modal_max_width": "large" - // 4. Extra Large: - // "modal_max_width": "xlarge" - // 5. Fullscreen: This value removes any horizontal padding, as it consumes the whole viewport width. - // "modal_max_width": "full" - // - // Default: small - "modal_max_width": "small", - // Determines whether the file finder should skip focus for the active file in search results. - // There are 2 possible values: - // - // 1. true: When searching for files, if the currently active file appears as the first result, - // auto-focus will skip it and focus the second result instead. - // "skip_focus_for_active_in_search": true - // - // 2. false: When searching for files, the first result will always receive focus, - // even if it's the currently active file. - // "skip_focus_for_active_in_search": false - // - // Default: true - "skip_focus_for_active_in_search": true, - // Whether to show the git status in the file finder. - "git_status": true, - // Whether to use gitignored files when searching. - // Only the file Zed had indexed will be used, not necessary all the gitignored files. - // - // Can accept 3 values: - // * "all": Use all gitignored files - // * "indexed": Use only the files Zed had indexed - // * "smart": Be smart and search for ignored when called from a gitignored worktree - "include_ignored": "smart", - }, - // Whether or not to remove any trailing whitespace from lines of a buffer - // before saving it. - "remove_trailing_whitespace_on_save": true, - // Whether to start a new line with a comment when a previous line is a comment as well. - "extend_comment_on_newline": true, - // Removes any lines containing only whitespace at the end of the file and - // ensures just one newline at the end. - "ensure_final_newline_on_save": true, - // Whether or not to perform a buffer format before saving: [on, off] - // Keep in mind, if the autosave with delay is enabled, format_on_save will be ignored - "format_on_save": "on", - // How to perform a buffer format. This setting can take multiple values: - // - // 1. Default. Format files using Zed's Prettier integration (if applicable), - // or falling back to formatting via language server: - // "formatter": "auto" - // 2. Format code using the current language server: - // "formatter": "language_server" - // 3. Format code using a specific language server: - // "formatter": {"language_server": {"name": "ruff"}} - // 4. Format code using an external command: - // "formatter": { - // "external": { - // "command": "prettier", - // "arguments": ["--stdin-filepath", "{buffer_path}"] - // } - // } - // 5. Format code using Zed's Prettier integration: - // "formatter": "prettier" - // 6. Format code using a code action - // "formatter": {"code_action": "source.fixAll.eslint"} - // 7. An array of any format step specified above to apply in order - // "formatter": [{"code_action": "source.fixAll.eslint"}, "prettier"] - "formatter": "auto", - // How to soft-wrap long lines of text. - // Possible values: - // - // 1. Prefer a single line generally, unless an overly long line is encountered. - // "soft_wrap": "none", - // "soft_wrap": "prefer_line", // (deprecated, same as "none") - // 2. Soft wrap lines that overflow the editor. - // "soft_wrap": "editor_width", - // 3. Soft wrap lines at the preferred line length. - // "soft_wrap": "preferred_line_length", - // 4. Soft wrap lines at the preferred line length or the editor width (whichever is smaller). - // "soft_wrap": "bounded", - "soft_wrap": "none", - // The column at which to soft-wrap lines, for buffers where soft-wrap - // is enabled. - "preferred_line_length": 80, - // Whether to indent lines using tab characters, as opposed to multiple - // spaces. - "hard_tabs": false, - // How many columns a tab should occupy. - "tab_size": 4, - // What debuggers are preferred by default for all languages. - "debuggers": [], - // Whether to enable word diff highlighting in the editor. - // - // When enabled, changed words within modified lines are highlighted - // to show exactly what changed. - // - // Default: true - "word_diff_enabled": true, - // Control what info is collected by Zed. - "telemetry": { - // Send debug info like crash reports. - "diagnostics": true, - // Send anonymized usage data like what languages you're using Zed with. - "metrics": true, - }, - // Whether to disable all AI features in Zed. - // - // Default: false - "disable_ai": false, - // Automatically update Zed. This setting may be ignored on Linux if - // installed through a package manager. - "auto_update": true, - // How to render LSP `textDocument/documentColor` colors in the editor. - // - // Possible values: - // - // 1. Do not query and render document colors. - // "lsp_document_colors": "none", - // 2. Render document colors as inlay hints near the color text (default). - // "lsp_document_colors": "inlay", - // 3. Draw a border around the color text. - // "lsp_document_colors": "border", - // 4. Draw a background behind the color text.. - // "lsp_document_colors": "background", - "lsp_document_colors": "inlay", - // Diagnostics configuration. - "diagnostics": { - // Whether to show the project diagnostics button in the status bar. - "button": true, - // Whether to show warnings or not by default. - "include_warnings": true, - // Settings for using LSP pull diagnostics mechanism in Zed. - "lsp_pull_diagnostics": { - // Whether to pull for diagnostics or not. - "enabled": true, - // Minimum time to wait before pulling diagnostics from the language server(s). - // 0 turns the debounce off. - "debounce_ms": 50, - }, - // Settings for inline diagnostics - "inline": { - // Whether to show diagnostics inline or not - "enabled": false, - // The delay in milliseconds to show inline diagnostics after the - // last diagnostic update. - "update_debounce_ms": 150, - // The amount of padding between the end of the source line and the start - // of the inline diagnostic in units of em widths. - "padding": 4, - // The minimum column to display inline diagnostics. This setting can be - // used to horizontally align inline diagnostics at some column. Lines - // longer than this value will still push diagnostics further to the right. - "min_column": 0, - // The minimum severity of the diagnostics to show inline. - // Inherits editor's diagnostics' max severity settings when `null`. - "max_severity": null, - }, - }, - // Files or globs of files that will be excluded by Zed entirely. They will be skipped during file - // scans, file searches, and not be displayed in the project file tree. Takes precedence over `file_scan_inclusions`. - "file_scan_exclusions": [ - "**/.git", - "**/.svn", - "**/.hg", - "**/.jj", - "**/.repo", - "**/CVS", - "**/.DS_Store", - "**/Thumbs.db", - "**/.classpath", - "**/.settings", - ], - // Files or globs of files that will be included by Zed, even when ignored by git. This is useful - // for files that are not tracked by git, but are still important to your project. Note that globs - // that are overly broad can slow down Zed's file scanning. `file_scan_exclusions` takes - // precedence over these inclusions. - "file_scan_inclusions": [".env*"], - // Globs to match files that will be considered "hidden". These files can be hidden from the - // project panel by toggling the "hide_hidden" setting. - "hidden_files": ["**/.*"], - // Git gutter behavior configuration. - "git": { - // Control whether the git gutter is shown. May take 2 values: - // 1. Show the gutter - // "git_gutter": "tracked_files" - // 2. Hide the gutter - // "git_gutter": "hide" - "git_gutter": "tracked_files", - /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter. - /// - /// Default: 0 - "gutter_debounce": 0, - // Control whether the git blame information is shown inline, - // in the currently focused line. - "inline_blame": { - "enabled": true, - // 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 - "min_column": 0, - }, - "blame": { - "show_avatar": true, - }, - // Control which information is shown in the branch picker. - "branch_picker": { - "show_author_name": true, - }, - // How git hunks are displayed visually in the editor. - // This setting can take two values: - // - // 1. Show unstaged hunks filled and staged hunks hollow: - // "hunk_style": "staged_hollow" - // 2. Show unstaged hunks hollow and staged hunks filled: - // "hunk_style": "unstaged_hollow" - "hunk_style": "staged_hollow", - // Should the name or path be displayed first in the git view. - // "path_style": "file_name_first" or "file_path_first" - "path_style": "file_name_first", - }, - // The list of custom Git hosting providers. - "git_hosting_providers": [ - // { - // "provider": "github", - // "name": "BigCorp GitHub", - // "base_url": "https://code.big-corp.com" - // } - ], - // Configuration for how direnv configuration should be loaded. May take 2 values: - // 1. Load direnv configuration using `direnv export json` directly. - // "load_direnv": "direct" - // 2. Load direnv configuration through the shell hook, works for POSIX shells and fish. - // "load_direnv": "shell_hook" - // 3. Don't load direnv configuration at all. - // "load_direnv": "disabled" - "load_direnv": "direct", - "edit_predictions": { - // A list of globs representing files that edit predictions should be disabled for. - // There's a sensible default list of globs already included. - // 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", - ], - // 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. - // "mode": "eager" - // 2. Display predictions inline only when holding a modifier key (alt by default). - // "mode": "subtle" - "mode": "eager", - // Copilot-specific settings - // "copilot": { - // "enterprise_uri": "", - // "proxy": "", - // "proxy_no_verify": false - // }, - "copilot": { - "enterprise_uri": null, - "proxy": null, - "proxy_no_verify": null, - }, - "codestral": { - "model": null, - "max_tokens": null, - }, - // Whether edit predictions are enabled when editing text threads in the agent panel. - // This setting has no effect if globally disabled. - "enabled_in_text_threads": true, - }, - // Settings specific to journaling - "journal": { - // The path of the directory where journal entries are stored - "path": "~", - // What format to display the hours in - // May take 2 values: - // 1. hour12 - // 2. hour24 - "hour_format": "hour12", - }, - // Status bar-related settings. - "status_bar": { - // Whether to show the status bar. - "experimental.show": true, - // 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, - // Whether to show active line endings button in the status bar. - "line_endings_button": false, - }, - // Settings specific to the terminal - "terminal": { - // What shell to use when opening a terminal. May take 3 values: - // 1. Use the system's default terminal configuration in /etc/passwd - // "shell": "system" - // 2. A program: - // "shell": { - // "program": "sh" - // } - // 3. A program with arguments: - // "shell": { - // "with_arguments": { - // "program": "/bin/bash", - // "args": ["--login"] - // } - // } - "shell": "system", - // Where to dock terminals panel. Can be `left`, `right`, `bottom`. - "dock": "bottom", - // Default width when the terminal is docked to the left or right. - "default_width": 640, - // Default height when the terminal is docked to the bottom. - "default_height": 320, - // What working directory to use when launching the terminal. - // May take 4 values: - // 1. Use the current file's project directory. Fallback to the - // first project directory strategy if unsuccessful - // "working_directory": "current_project_directory" - // 2. Use the first project in this workspace's directory - // "working_directory": "first_project_directory" - // 3. Always use this platform's home directory (if we can find it) - // "working_directory": "always_home" - // 4. Always use a specific directory. This value will be shell expanded. - // If this path is not a valid directory the terminal will default to - // this platform's home directory (if we can find it) - // "working_directory": { - // "always": { - // "directory": "~/zed/projects/" - // } - // } - "working_directory": "current_project_directory", - // Set the cursor blinking behavior in the terminal. - // May take 3 values: - // 1. Never blink the cursor, ignoring the terminal mode - // "blinking": "off", - // 2. Default the cursor blink to off, but allow the terminal to - // set blinking - // "blinking": "terminal_controlled", - // 3. Always blink the cursor, ignoring the terminal mode - // "blinking": "on", - "blinking": "terminal_controlled", - // Default cursor shape for the terminal. - // 1. A block that surrounds the following character - // "block" - // 2. A vertical bar - // "bar" - // 3. An underline / underscore that runs along the following character - // "underline" - // 4. A box drawn around the following character - // "hollow" - // - // Default: "block" - "cursor_shape": "block", - // Set whether Alternate Scroll mode (code: ?1007) is active by default. - // Alternate Scroll mode converts mouse scroll events into up / down key - // presses when in the alternate screen (e.g. when running applications - // like vim or less). The terminal can still set and unset this mode. - // May take 2 values: - // 1. Default alternate scroll mode to on - // "alternate_scroll": "on", - // 2. Default alternate scroll mode to off - // "alternate_scroll": "off", - "alternate_scroll": "on", - // Set whether the option key behaves as the meta key. - // May take 2 values: - // 1. Rely on default platform handling of option key, on macOS - // this means generating certain unicode characters - // "option_as_meta": false, - // 2. Make the option keys behave as a 'meta' key, e.g. for emacs - // "option_as_meta": true, - "option_as_meta": false, - // Whether or not selecting text in the terminal will automatically - // copy to the system clipboard. - "copy_on_select": false, - // Whether to keep the text selection after copying it to the clipboard. - "keep_selection_on_copy": true, - // Whether to show the terminal button in the status bar - "button": true, - // Any key-value pairs added to this list will be added to the terminal's - // environment. Use `:` to separate multiple values. - "env": { - // "KEY": "value1:value2" - }, - // Set the terminal's line height. - // May take 3 values: - // 1. Use a line height that's comfortable for reading, 1.618 - // "line_height": "comfortable" - // 2. Use a standard line height, 1.3. This option is useful for TUIs, - // particularly if they use box characters - // "line_height": "standard", - // 3. Use a custom line height. - // "line_height": { - // "custom": 2 - // }, - "line_height": "standard", - // Activate the python virtual environment, if one is found, in the - // terminal's working directory (as resolved by the working_directory - // setting). Set this to "off" to disable this behavior. - "detect_venv": { - "on": { - // Default directories to search for virtual environments, relative - // to the current working directory. We recommend overriding this - // in your project's settings, rather than globally. - "directories": [".env", "env", ".venv", "venv"], - // Can also be `csh`, `fish`, `nushell` and `power_shell` - "activate_script": "default", - // Preferred Conda manager to use when activating Conda environments. - // Values: "auto", "conda", "mamba", "micromamba" - // Default: "auto" - "conda_manager": "auto", - }, - }, - "toolbar": { - // Whether to display the terminal title in its toolbar's breadcrumbs. - // Only shown if the terminal title is not empty. - // - // The shell running in the terminal needs to be configured to emit the title. - // Example: `echo -e "\e]2;New Title\007";` - "breadcrumbs": false, - }, - // Scrollbar-related settings - "scrollbar": { - // When to show the scrollbar in the terminal. - // This setting can take five values: - // - // 1. null (default): Inherit editor settings - // 2. Show the scrollbar if there's important information or - // follow the system's configured behavior (default): - // "auto" - // 3. Match the system's configured behavior: - // "system" - // 4. Always show the scrollbar: - // "always" - // 5. Never show the scrollbar: - // "never" - "show": null, - }, - // Set the terminal's font size. If this option is not included, - // the terminal will default to matching the buffer's font size. - // "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", - // 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 - // "font_fallbacks": ["FiraCode Nerd Fonts"], - // The weight of the editor font in standard CSS units from 100 to 900. - "font_weight": 400, - // Sets the maximum number of lines in the terminal's scrollback buffer. - // Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling. - // Existing terminals will not pick up this change until they are recreated. - "max_scroll_history_lines": 10000, - // The multiplier for scrolling speed in the terminal. - "scroll_multiplier": 1.0, - // The minimum APCA perceptual contrast between foreground and background colors. - // APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x, - // especially for dark mode. Values range from 0 to 106. - // - // Based on APCA Readability Criterion (ARC) Bronze Simple Mode: - // https://readtech.org/ARC/tests/bronze-simple-mode/ - // - 0: No contrast adjustment - // - 45: Minimum for large fluent text (36px+) - // - 60: Minimum for other content text - // - 75: Minimum for body text - // - 90: Preferred for body text - // - // Most terminal themes have APCA values of 40-70. - // A value of 45 preserves colorful themes while ensuring legibility. - "minimum_contrast": 45, - // Regexes used to identify paths for hyperlink navigation. Supports optional named capture - // groups `path`, `line`, `column`, and `link`. If none of these are present, the entire match - // is the hyperlink target. If `path` is present, it is the hyperlink target, along with `line` - // and `column` if present. `link` may be used to customize what text in terminal is part of the - // hyperlink. If `link` is not present, the text of the entire match is used. If `line` and - // `column` are not present, the default built-in line and column suffix processing is used - // which parses `line:column` and `(line,column)` variants. The default value handles Python - // diagnostics and common path, line, column syntaxes. This can be extended or replaced to - // handle specific scenarios. For example, to enable support for hyperlinking paths which - // contain spaces in rust output, - // - // [ - // "\\s+(-->|:::|at) (?(?.+?))(:$|$)", - // "\\s+(Compiling|Checking|Documenting) [^(]+\\((?(?.+))\\)" - // ], - // - // could be used. Processing stops at the first regex with a match, even if no link is - // produced which is the case when the cursor is not over the hyperlinked text. For best - // performance it is recommended to order regexes from most common to least common. For - // readability and documentation, each regex may be an array of strings which are collected - // into one multi-line regex string for use in terminal path hyperlink detection. - "path_hyperlink_regexes": [ - // Python-style diagnostics - "File \"(?[^\"]+)\", line (?[0-9]+)", - // Common path syntax with optional line, column, description, trailing punctuation, or - // surrounding symbols or quotes - [ - "(?x)", - "(?", - " (", - " # multi-char path: first char (not opening delimiter or space)", - " [^({\\[<\"'`\\ ]", - " # middle chars: non-space, and colon/paren only if not followed by digit/paren", - " ([^\\ :(]|[:(][^0-9()])*", - " # last char: not closing delimiter or colon", - " [^()}\\]>\"'`.,;:\\ ]", - " |", - " # single-char path: not delimiter, punctuation, or space", - " [^(){}\\[\\]<>\"'`.,;:\\ ]", - " )", - " # optional line/column suffix (included in path for PathWithPosition::parse_str)", - " (:+[0-9]+(:[0-9]+)?|:?\\([0-9]+([,:]?[0-9]+)?\\))?", - ")", - ], - ], - // Timeout for hover and Cmd-click path hyperlink discovery in milliseconds. Specifying a - // timeout of `0` will disable path hyperlinking in terminal. - "path_hyperlink_timeout_ms": 1, - }, - "code_actions_on_format": {}, - // Settings related to running tasks. - "tasks": { - "variables": {}, - "enabled": true, - // Use LSP tasks over Zed language extension ones. - // If no LSP tasks are returned due to error/timeout or regular execution, - // Zed language extension tasks will be used instead. - // - // Other Zed tasks will still be shown: - // * Zed task from either of the task config file - // * Zed task from history (e.g. one-off task was spawned before) - // - // Default: true - "prefer_lsp": true, - }, - // An object whose keys are language names, and whose values - // are arrays of filenames or extensions of files that should - // use those languages. - // - // For example, to treat files like `foo.notjs` as JavaScript, - // and `Embargo.lock` as TOML: - // - // { - // "JavaScript": ["notjs"], - // "TOML": ["Embargo.lock"] - // } - // - "file_types": { - "JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json", "tsconfig*.json"], - "Markdown": [".rules", ".cursorrules", ".windsurfrules", ".clinerules"], - "Shell Script": [".env.*"], - }, - // Settings for which version of Node.js and NPM to use when installing - // language servers and Copilot. - // - // Note: changing this setting currently requires restarting Zed. - "node": { - // By default, Zed will look for `node` and `npm` on your `$PATH`, and use the - // existing executables if their version is recent enough. Set this to `true` - // to prevent this, and force Zed to always download and install its own - // version of Node. - "ignore_system_version": false, - // You can also specify alternative paths to Node and NPM. If you specify - // `path`, but not `npm_path`, Zed will assume that `npm` is located at - // `${path}/../npm`. - "path": null, - "npm_path": null, - }, - // The extensions that Zed should automatically install on startup. - // - // If you don't want any of these extensions, add this field to your settings - // and change the value to `false`. - "auto_install_extensions": { - "html": true, - }, - // The capabilities granted to extensions. - // - // This list can be customized to restrict what extensions are able to do. - "granted_extension_capabilities": [ - { "kind": "process:exec", "command": "*", "args": ["**"] }, - { "kind": "download_file", "host": "*", "path": ["**"] }, - { "kind": "npm:install", "package": "*" }, - ], - // Controls how completions are processed for this language. - "completions": { - // Controls how words are completed. - // For large documents, not all words may be fetched for completion. - // - // May take 3 values: - // 1. "enabled" - // Always fetch document's words for completions along with LSP completions. - // 2. "fallback" - // Only if LSP response errors or times out, use document's words to show completions. - // 3. "disabled" - // Never fetch or complete document's words for completions. - // (Word-based completions can still be queried via a separate action) - // - // 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 - "lsp": true, - // When fetching LSP completions, determines how long to wait for a response of a particular server. - // When set to 0, waits indefinitely. - // - // Default: 0 - "lsp_fetch_timeout_ms": 0, - // Controls what range to replace when accepting LSP completions. - // - // When LSP servers give an `InsertReplaceEdit` completion, they provides two ranges: `insert` and `replace`. Usually, `insert` - // contains the word prefix before your cursor and `replace` contains the whole word. - // - // Effectively, this setting just changes whether Zed will use the received range for `insert` or `replace`, so the results may - // differ depending on the underlying LSP server. - // - // Possible values: - // 1. "insert" - // Replaces text before the cursor, using the `insert` range described in the LSP specification. - // 2. "replace" - // Replaces text before and after the cursor, using the `replace` range described in the LSP specification. - // 3. "replace_subsequence" - // Behaves like `"replace"` if the text that would be replaced is a subsequence of the completion text, - // and like `"insert"` otherwise. - // 4. "replace_suffix" - // Behaves like `"replace"` if the text after the cursor is a suffix of the completion, and like - // `"insert"` otherwise. - "lsp_insert_mode": "replace_suffix", - }, - // Different settings for specific languages. - "languages": { - "Astro": { - "language_servers": ["astro-language-server", "..."], - "prettier": { - "allowed": true, - "plugins": ["prettier-plugin-astro"], - }, - }, - "Blade": { - "prettier": { - "allowed": true, - }, - }, - "C": { - "format_on_save": "off", - "use_on_type_format": false, - "prettier": { - "allowed": false, - }, - }, - "C++": { - "format_on_save": "off", - "use_on_type_format": false, - "prettier": { - "allowed": false, - }, - }, - "CSharp": { - "language_servers": ["roslyn", "!omnisharp", "..."], - }, - "CSS": { - "prettier": { - "allowed": true, - }, - }, - "Dart": { - "tab_size": 2, - }, - "Diff": { - "show_edit_predictions": false, - "remove_trailing_whitespace_on_save": false, - "ensure_final_newline_on_save": false, - }, - "Elixir": { - "language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."], - }, - "Elm": { - "tab_size": 4, - }, - "Erlang": { - "language_servers": ["erlang-ls", "!elp", "..."], - }, - "Git Commit": { - "allow_rewrap": "anywhere", - "soft_wrap": "editor_width", - "preferred_line_length": 72, - }, - "Go": { - "hard_tabs": true, - "code_actions_on_format": { - "source.organizeImports": true, - }, - "debuggers": ["Delve"], - }, - "GraphQL": { - "prettier": { - "allowed": true, - }, - }, - "HEEX": { - "language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."], - }, - "HTML": { - "prettier": { - "allowed": true, - }, - }, - "HTML+ERB": { - "language_servers": ["herb", "!ruby-lsp", "..."], - }, - "Java": { - "prettier": { - "allowed": true, - "plugins": ["prettier-plugin-java"], - }, - }, - "JavaScript": { - "language_servers": ["!typescript-language-server", "vtsls", "..."], - "prettier": { - "allowed": true, - }, - }, - "JSON": { - "prettier": { - "allowed": true, - }, - }, - "JSONC": { - "prettier": { - "allowed": true, - }, - }, - "JS+ERB": { - "language_servers": ["!ruby-lsp", "..."], - }, - "Kotlin": { - "language_servers": ["!kotlin-language-server", "kotlin-lsp", "..."], - }, - "LaTeX": { - "formatter": "language_server", - "language_servers": ["texlab", "..."], - "prettier": { - "allowed": true, - "plugins": ["prettier-plugin-latex"], - }, - }, - "Markdown": { - "format_on_save": "off", - "use_on_type_format": false, - "remove_trailing_whitespace_on_save": false, - "allow_rewrap": "anywhere", - "soft_wrap": "editor_width", - "completions": { - "words": "disabled", - }, - "prettier": { - "allowed": true, - }, - }, - "PHP": { - "language_servers": ["phpactor", "!intelephense", "!phptools", "..."], - "prettier": { - "allowed": true, - "plugins": ["@prettier/plugin-php"], - "parser": "php", - }, - }, - "Plain Text": { - "allow_rewrap": "anywhere", - "soft_wrap": "editor_width", - "completions": { - "words": "disabled", - }, - }, - "Python": { - "code_actions_on_format": { - "source.organizeImports.ruff": true, - }, - "formatter": { - "language_server": { - "name": "ruff", - }, - }, - "debuggers": ["Debugpy"], - "language_servers": ["basedpyright", "ruff", "!ty", "!pyrefly", "!pyright", "!pylsp", "..."], - }, - "Ruby": { - "language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."], - }, - "Rust": { - "debuggers": ["CodeLLDB"], - }, - "SCSS": { - "prettier": { - "allowed": true, - }, - }, - "Starlark": { - "language_servers": ["starpls", "!buck2-lsp", "..."], - }, - "Svelte": { - "language_servers": ["svelte-language-server", "..."], - "prettier": { - "allowed": true, - "plugins": ["prettier-plugin-svelte"], - }, - }, - "TSX": { - "language_servers": ["!typescript-language-server", "vtsls", "..."], - "prettier": { - "allowed": true, - }, - }, - "Twig": { - "prettier": { - "allowed": true, - }, - }, - "TypeScript": { - "language_servers": ["!typescript-language-server", "vtsls", "..."], - "prettier": { - "allowed": true, - }, - }, - "SystemVerilog": { - "format_on_save": "off", - "language_servers": ["!slang", "..."], - "use_on_type_format": false, - }, - "Vue.js": { - "language_servers": ["vue-language-server", "vtsls", "..."], - "prettier": { - "allowed": true, - }, - }, - "XML": { - "prettier": { - "allowed": true, - "plugins": ["@prettier/plugin-xml"], - }, - }, - "YAML": { - "prettier": { - "allowed": true, - }, - }, - "YAML+ERB": { - "language_servers": ["!ruby-lsp", "..."], - }, - "Zig": { - "language_servers": ["zls", "..."], - }, - }, - // Different settings for specific language models. - "language_models": { - "anthropic": { - "api_url": "https://api.anthropic.com", - }, - "bedrock": {}, - "google": { - "api_url": "https://generativelanguage.googleapis.com", - }, - "ollama": { - "api_url": "http://localhost:11434", - }, - "openai": { - "api_url": "https://api.openai.com/v1", - }, - "openai_compatible": {}, - "open_router": { - "api_url": "https://openrouter.ai/api/v1", - }, - "lmstudio": { - "api_url": "http://localhost:1234/api/v0", - }, - "deepseek": { - "api_url": "https://api.deepseek.com/v1", - }, - "mistral": { - "api_url": "https://api.mistral.ai/v1", - }, - "vercel": { - "api_url": "https://api.v0.dev/v1", - }, - "x_ai": { - "api_url": "https://api.x.ai/v1", - }, - "zed.dev": {}, - }, - "session": { - // Whether or not to restore unsaved buffers on restart. - // - // If this is true, user won't be prompted whether to save/discard - // dirty files when closing the application. - // - // Default: true - "restore_unsaved_buffers": true, - }, - // Zed's Prettier integration settings. - // Allows to enable/disable formatting with Prettier - // and configure default Prettier, used when no project-level Prettier installation is found. - "prettier": { - // Enables or disables formatting with Prettier for any given language. - "allowed": false, - // Forces Prettier integration to use a specific parser name when formatting files with the language. - "plugins": [], - // Default Prettier options, in the format as in package.json section for Prettier. - // If project installs Prettier via its package.json, these options will be ignored. - // "trailingComma": "es5", - // "tabWidth": 4, - // "semi": false, - // "singleQuote": true - // Forces Prettier integration to use a specific parser name when formatting files with the language - // when set to a non-empty string. - "parser": "", - }, - // Settings for auto-closing of JSX tags. - "jsx_tag_auto_close": { - "enabled": true, - }, - // LSP Specific settings. - "lsp": { - // Specify the LSP name as a key here. - // "rust-analyzer": { - // // A special flag for rust-analyzer integration, to use server-provided tasks - // enable_lsp_tasks": true, - // // These initialization options are merged into Zed's defaults - // "initialization_options": { - // "check": { - // "command": "clippy" // rust-analyzer.check.command (default: "check") - // } - // } - // } - }, - // DAP Specific settings. - "dap": { - // Specify the DAP name as a key here. - "CodeLLDB": { - "env": { - "RUST_LOG": "info", - }, - }, - }, - // Common language server settings. - "global_lsp_settings": { - // Whether to show the LSP servers button in the status bar. - "button": true, - }, - // Jupyter settings - "jupyter": { - "enabled": true, - "kernel_selections": {}, - // Specify the language name as the key and the kernel name as the value. - // "kernel_selections": { - // "python": "conda-base" - // "typescript": "deno" - // } - }, - // REPL settings. - "repl": { - // Maximum number of columns to keep in REPL's scrollback buffer. - // Clamped with [20, 512] range. - "max_columns": 128, - // Maximum number of lines to keep in REPL's scrollback buffer. - // Clamped with [4, 256] range. - "max_lines": 32, - }, - // Vim settings - "vim": { - "default_mode": "normal", - "toggle_relative_line_numbers": false, - "use_system_clipboard": "always", - "use_smartcase_find": false, - "highlight_on_yank_duration": 200, - "custom_digraphs": {}, - // Cursor shape for the each mode. - // Specify the mode as the key and the shape as the value. - // The mode can be one of the following: "normal", "replace", "insert", "visual". - // The shape can be one of the following: "block", "bar", "underline", "hollow". - "cursor_shape": {}, - }, - // The server to connect to. If the environment variable - // ZED_SERVER_URL is set, it will override this setting. - "server_url": "https://zed.dev", - // Settings overrides to use when using Zed Preview. - // Mostly useful for developers who are managing multiple instances of Zed. - "preview": { - // "theme": "Andromeda" - }, - // Settings overrides to use when using Zed Nightly. - // Mostly useful for developers who are managing multiple instances of Zed. - "nightly": { - // "theme": "Andromeda" - }, - // Settings overrides to use when using Zed Stable. - // Mostly useful for developers who are managing multiple instances of Zed. - "stable": { - // "theme": "Andromeda" - }, - // Settings overrides to use when using Zed Dev. - // Mostly useful for developers who are managing multiple instances of Zed. - "dev": { - // "theme": "Andromeda" - }, - // Settings overrides to use when using Linux. - "linux": {}, - // Settings overrides to use when using macOS. - "macos": {}, - // Settings overrides to use when using Windows. - "windows": { - "languages": { - "PHP": { - "language_servers": ["intelephense", "!phpactor", "!phptools", "..."], - }, - }, - }, - // Whether to show full labels in line indicator or short ones - // - // Values: - // - `short`: "2 s, 15 l, 32 c" - // - `long`: "2 selections, 15 lines, 32 characters" - // Default: long - "line_indicator_format": "long", - // Set a proxy to use. The proxy protocol is specified by the URI scheme. - // - // Supported URI scheme: `http`, `https`, `socks4`, `socks4a`, `socks5`, - // `socks5h`. `http` will be used when no scheme is specified. - // - // By default no proxy will be used, or Zed will try get proxy settings from - // environment variables. If certain hosts should not be proxied, - // set the `no_proxy` environment variable and provide a comma-separated list. - // - // Examples: - // - "proxy": "socks5h://localhost:10808" - // - "proxy": "http://127.0.0.1:10809" - "proxy": null, - // Set to configure aliases for the command palette. - // When typing a query which is a key of this object, the value will be used instead. - // - // Examples: - // { - // "W": "workspace::Save" - // } - "command_aliases": {}, - // ssh_connections is an array of ssh connections. - // You can configure these from `project: Open Remote` in the command palette. - // Zed's ssh support will pull configuration from your ~/.ssh too. - // Examples: - // [ - // { - // "host": "example-box", - // // "port": 22, "username": "test", "args": ["-i", "/home/user/.ssh/id_rsa"] - // "projects": [ - // { - // "paths": ["/home/user/code/zed"] - // } - // ] - // } - // ] - "ssh_connections": [], - // Whether to read ~/.ssh/config for ssh connection sources. - "read_ssh_config": true, - // Configures context servers for use by the agent. - "context_servers": {}, - // Configures agent servers available in the agent panel. - "agent_servers": {}, - "debugger": { - "stepping_granularity": "line", - "save_breakpoints": true, - "timeout": 2000, - "dock": "bottom", - "log_dap_communications": true, - "format_dap_log_messages": true, - "button": true, - }, - // Configures any number of settings profiles that are temporarily applied on - // top of your existing user settings when selected from - // `settings profile selector: toggle`. - // Examples: - // "profiles": { - // "Presenting": { - // "agent_ui_font_size": 20.0, - // "buffer_font_size": 20.0, - // "theme": "One Light", - // "ui_font_size": 20.0 - // }, - // "Python (ty)": { - // "languages": { - // "Python": { - // "language_servers": ["ty"] - // } - // } - // } - // } - "profiles": {}, - - // A map of log scopes to the desired log level. - // Useful for filtering out noisy logs or enabling more verbose logging. - // - // Example: {"log": {"client": "warn"}} - "log": {}, -} diff --git a/assets/settings/initial_debug_tasks.json b/assets/settings/initial_debug_tasks.json deleted file mode 100644 index af4512bd51..0000000000 --- a/assets/settings/initial_debug_tasks.json +++ /dev/null @@ -1,29 +0,0 @@ -// Some example tasks for common languages. -// -// For more documentation on how to configure debug tasks, -// see: https://zed.dev/docs/debugger -[ - { - "label": "Debug active Python file", - "adapter": "Debugpy", - "program": "$ZED_FILE", - "request": "launch", - "cwd": "$ZED_WORKTREE_ROOT" - }, - { - "label": "Debug active JavaScript file", - "adapter": "JavaScript", - "program": "$ZED_FILE", - "request": "launch", - "cwd": "$ZED_WORKTREE_ROOT", - "type": "pwa-node" - }, - { - "label": "JavaScript debug terminal", - "adapter": "JavaScript", - "request": "launch", - "cwd": "$ZED_WORKTREE_ROOT", - "console": "integratedTerminal", - "type": "pwa-node" - } -] diff --git a/assets/settings/initial_local_debug_tasks.json b/assets/settings/initial_local_debug_tasks.json deleted file mode 100644 index 4be1a903ab..0000000000 --- a/assets/settings/initial_local_debug_tasks.json +++ /dev/null @@ -1,5 +0,0 @@ -// Project-local debug tasks -// -// For more documentation on how to configure debug tasks, -// see: https://zed.dev/docs/debugger -[] diff --git a/assets/settings/initial_local_settings.json b/assets/settings/initial_local_settings.json deleted file mode 100644 index 79860b3a94..0000000000 --- a/assets/settings/initial_local_settings.json +++ /dev/null @@ -1,5 +0,0 @@ -// Folder-specific settings -// -// For a full list of overridable settings, and general information on folder-specific settings, -// see the documentation: https://zed.dev/docs/configuring-zed#settings-files -{} diff --git a/assets/settings/initial_server_settings.json b/assets/settings/initial_server_settings.json deleted file mode 100644 index d6ec33e601..0000000000 --- a/assets/settings/initial_server_settings.json +++ /dev/null @@ -1,7 +0,0 @@ -// Server-specific settings -// -// For a full list of overridable settings, and general information on settings, -// see the documentation: https://zed.dev/docs/configuring-zed#settings-files -{ - "lsp": {} -} diff --git a/assets/settings/initial_tasks.json b/assets/settings/initial_tasks.json deleted file mode 100644 index a79e980632..0000000000 --- a/assets/settings/initial_tasks.json +++ /dev/null @@ -1,54 +0,0 @@ -// Project tasks configuration. See https://zed.dev/docs/tasks for documentation. -// -// Example: -[ - { - "label": "Example task", - "command": "for i in {1..5}; do echo \"Hello $i/5\"; sleep 1; done", - //"args": [], - // Env overrides for the command, will be appended to the terminal's environment from the settings. - "env": { "foo": "bar" }, - // Current working directory to spawn the command into, defaults to current project root. - //"cwd": "/path/to/working/directory", - // Whether to use a new terminal tab or reuse the existing one to spawn the process, defaults to `false`. - "use_new_terminal": false, - // Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`. - "allow_concurrent_runs": false, - // What to do with the terminal pane and tab, after the command was started: - // * `always` — always show the task's pane, and focus the corresponding tab in it (default) - // * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it - // * `never` — do not alter focus, but still add/reuse the task's tab in its pane - "reveal": "always", - // Where to place the task's terminal item after starting the task: - // * `dock` — in the terminal dock, "regular" terminal items' place (default) - // * `center` — in the central pane group, "main" editor area - "reveal_target": "dock", - // What to do with the terminal pane and tab, after the command had finished: - // * `never` — Do nothing when the command finishes (default) - // * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it - // * `on_success` — hide the terminal tab on task success only, otherwise behaves similar to `always` - "hide": "never", - // Which shell to use when running a task inside the terminal. - // May take 3 values: - // 1. (default) Use the system's default terminal configuration in /etc/passwd - // "shell": "system" - // 2. A program: - // "shell": { - // "program": "sh" - // } - // 3. A program with arguments: - // "shell": { - // "with_arguments": { - // "program": "/bin/bash", - // "args": ["--login"] - // } - // } - "shell": "system", - // Whether to show the task line in the output of the spawned task, defaults to `true`. - "show_summary": true, - // Whether to show the command line in the output of the spawned task, defaults to `true`. - "show_command": true - // Represents the tags for inline runnable indicators, or spawning multiple tasks at once. - // "tags": [] - } -] diff --git a/assets/settings/initial_user_settings.json b/assets/settings/initial_user_settings.json deleted file mode 100644 index 5ac2063bdb..0000000000 --- a/assets/settings/initial_user_settings.json +++ /dev/null @@ -1,17 +0,0 @@ -// Zed settings -// -// For information on how to configure Zed, see the Zed -// documentation: https://zed.dev/docs/configuring-zed -// -// To see all of Zed's default settings without changing your -// custom settings, run `zed: open default settings` from the -// command palette (cmd-shift-p / ctrl-shift-p) -{ - "ui_font_size": 16, - "buffer_font_size": 15, - "theme": { - "mode": "system", - "light": "One Light", - "dark": "One Dark" - } -} diff --git a/assets/sounds/agent_done.wav b/assets/sounds/agent_done.wav deleted file mode 100755 index 22c9390c00..0000000000 Binary files a/assets/sounds/agent_done.wav and /dev/null differ diff --git a/assets/sounds/guest_joined_call.wav b/assets/sounds/guest_joined_call.wav deleted file mode 100644 index 336a6ca754..0000000000 Binary files a/assets/sounds/guest_joined_call.wav and /dev/null differ diff --git a/assets/sounds/joined_call.wav b/assets/sounds/joined_call.wav deleted file mode 100644 index cf6e5ba4df..0000000000 Binary files a/assets/sounds/joined_call.wav and /dev/null differ diff --git a/assets/sounds/leave_call.wav b/assets/sounds/leave_call.wav deleted file mode 100644 index 478b28204f..0000000000 Binary files a/assets/sounds/leave_call.wav and /dev/null differ diff --git a/assets/sounds/mute.wav b/assets/sounds/mute.wav deleted file mode 100644 index 69e8456f6c..0000000000 Binary files a/assets/sounds/mute.wav and /dev/null differ diff --git a/assets/sounds/start_screenshare.wav b/assets/sounds/start_screenshare.wav deleted file mode 100644 index 7b72a90af1..0000000000 Binary files a/assets/sounds/start_screenshare.wav and /dev/null differ diff --git a/assets/sounds/stop_screenshare.wav b/assets/sounds/stop_screenshare.wav deleted file mode 100644 index 1fe13e21b4..0000000000 Binary files a/assets/sounds/stop_screenshare.wav and /dev/null differ diff --git a/assets/sounds/unmute.wav b/assets/sounds/unmute.wav deleted file mode 100644 index f8c90f6916..0000000000 Binary files a/assets/sounds/unmute.wav and /dev/null differ diff --git a/assets/themes/.gitkeep b/assets/themes/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/assets/themes/LICENSES b/assets/themes/LICENSES deleted file mode 100644 index 14416f85b5..0000000000 --- a/assets/themes/LICENSES +++ /dev/null @@ -1,283 +0,0 @@ -## [Ayu Dark](https://github.com/dempfi/ayu) - -The MIT License (MIT) - -Copyright (c) 2016 Ike Ku - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -******************************************************************************** - -## [Ayu Light](https://github.com/dempfi/ayu) - -The MIT License (MIT) - -Copyright (c) 2016 Ike Ku - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -******************************************************************************** - -## [Ayu Mirage](https://github.com/dempfi/ayu) - -The MIT License (MIT) - -Copyright (c) 2016 Ike Ku - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -******************************************************************************** - -## [Gruvbox Dark](https://github.com/morhetz/gruvbox) - -The MIT License (MIT) - -Copyright (c) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -******************************************************************************** - -## [Gruvbox Dark Hard](https://github.com/morhetz/gruvbox) - -The MIT License (MIT) - -Copyright (c) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -******************************************************************************** - -## [Gruvbox Dark Soft](https://github.com/morhetz/gruvbox) - -The MIT License (MIT) - -Copyright (c) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -******************************************************************************** - -## [Gruvbox Light](https://github.com/morhetz/gruvbox) - -The MIT License (MIT) - -Copyright (c) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -******************************************************************************** - -## [Gruvbox Light Hard](https://github.com/morhetz/gruvbox) - -The MIT License (MIT) - -Copyright (c) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -******************************************************************************** - -## [Gruvbox Light Soft](https://github.com/morhetz/gruvbox) - -The MIT License (MIT) - -Copyright (c) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -******************************************************************************** - -## [One Dark](https://github.com/atom/atom/tree/master/packages/one-dark-ui) - -The MIT License (MIT) - -Copyright (c) 2014 GitHub Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -******************************************************************************** - -## [One Light](https://github.com/atom/atom/tree/master/packages/one-light-ui) - -The MIT License (MIT) - -Copyright (c) 2014 GitHub Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/assets/themes/ayu/LICENSE b/assets/themes/ayu/LICENSE deleted file mode 100644 index 37a9229268..0000000000 --- a/assets/themes/ayu/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Ike Ku - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/assets/themes/ayu/ayu.json b/assets/themes/ayu/ayu.json deleted file mode 100644 index e2b7c3c91f..0000000000 --- a/assets/themes/ayu/ayu.json +++ /dev/null @@ -1,1183 +0,0 @@ -{ - "$schema": "https://zed.dev/schema/themes/v0.2.0.json", - "name": "Ayu", - "author": "Zed Industries", - "themes": [ - { - "name": "Ayu Dark", - "appearance": "dark", - "style": { - "border": "#3f4043ff", - "border.variant": "#2d2f34ff", - "border.focused": "#1b4a6eff", - "border.selected": "#1b4a6eff", - "border.transparent": "#00000000", - "border.disabled": "#383a3eff", - "elevated_surface.background": "#1f2127ff", - "surface.background": "#1f2127ff", - "background": "#313337ff", - "element.background": "#1f2127ff", - "element.hover": "#2d2f34ff", - "element.active": "#3e4043ff", - "element.selected": "#3e4043ff", - "element.disabled": "#1f2127ff", - "drop_target.background": "#8a898680", - "ghost_element.background": "#00000000", - "ghost_element.hover": "#2d2f34ff", - "ghost_element.active": "#3e4043ff", - "ghost_element.selected": "#3e4043ff", - "ghost_element.disabled": "#1f2127ff", - "text": "#bfbdb6ff", - "text.muted": "#8a8986ff", - "text.placeholder": "#696a6aff", - "text.disabled": "#696a6aff", - "text.accent": "#5ac1feff", - "icon": "#bfbdb6ff", - "icon.muted": "#8a8986ff", - "icon.disabled": "#696a6aff", - "icon.placeholder": "#8a8986ff", - "icon.accent": "#5ac1feff", - "status_bar.background": "#313337ff", - "title_bar.background": "#313337ff", - "title_bar.inactive_background": "#1f2127ff", - "toolbar.background": "#0d1016ff", - "tab_bar.background": "#1f2127ff", - "tab.inactive_background": "#1f2127ff", - "tab.active_background": "#0d1016ff", - "search.match_background": "#5ac2fe66", - "search.active_match_background": "#ea570166", - "panel.background": "#1f2127ff", - "panel.focused_border": "#5ac1feff", - "pane.focused_border": null, - "scrollbar.thumb.background": "#bfbdb64c", - "scrollbar.thumb.hover_background": "#2d2f34ff", - "scrollbar.thumb.border": "#2d2f34ff", - "scrollbar.track.background": "#00000000", - "scrollbar.track.border": "#1b1e24ff", - "editor.foreground": "#bfbdb6ff", - "editor.background": "#0d1016ff", - "editor.gutter.background": "#0d1016ff", - "editor.subheader.background": "#1f2127ff", - "editor.active_line.background": "#1f2127bf", - "editor.highlighted_line.background": "#1f2127ff", - "editor.line_number": "#4b4c4e", - "editor.active_line_number": "#cbcccd", - "editor.hover_line_number": "#a1a2a5", - "editor.invisible": "#666767ff", - "editor.wrap_guide": "#bfbdb60d", - "editor.active_wrap_guide": "#bfbdb61a", - "editor.document_highlight.read_background": "#5ac1fe1a", - "editor.document_highlight.write_background": "#66676766", - "terminal.background": "#0d1016ff", - "terminal.foreground": "#bfbdb6ff", - "terminal.bright_foreground": "#bfbdb6ff", - "terminal.dim_foreground": "#0d1016ff", - "terminal.ansi.black": "#0d1016ff", - "terminal.ansi.bright_black": "#545557ff", - "terminal.ansi.dim_black": "#bfbdb6ff", - "terminal.ansi.red": "#ef7177ff", - "terminal.ansi.bright_red": "#83353bff", - "terminal.ansi.dim_red": "#febab9ff", - "terminal.ansi.green": "#aad84cff", - "terminal.ansi.bright_green": "#567627ff", - "terminal.ansi.dim_green": "#d8eca8ff", - "terminal.ansi.yellow": "#feb454ff", - "terminal.ansi.bright_yellow": "#92582bff", - "terminal.ansi.dim_yellow": "#ffd9aaff", - "terminal.ansi.blue": "#5ac1feff", - "terminal.ansi.bright_blue": "#27618cff", - "terminal.ansi.dim_blue": "#b7dffeff", - "terminal.ansi.magenta": "#39bae5ff", - "terminal.ansi.bright_magenta": "#205a78ff", - "terminal.ansi.dim_magenta": "#addcf3ff", - "terminal.ansi.cyan": "#95e5cbff", - "terminal.ansi.bright_cyan": "#4c806fff", - "terminal.ansi.dim_cyan": "#cbf2e4ff", - "terminal.ansi.white": "#bfbdb6ff", - "terminal.ansi.bright_white": "#fafafaff", - "terminal.ansi.dim_white": "#787876ff", - "link_text.hover": "#5ac1feff", - "conflict": "#feb454ff", - "conflict.background": "#572815ff", - "conflict.border": "#754221ff", - "created": "#aad84cff", - "created.background": "#294113ff", - "created.border": "#405c1cff", - "deleted": "#ef7177ff", - "deleted.background": "#48161bff", - "deleted.border": "#66272dff", - "error": "#ef7177ff", - "error.background": "#48161bff", - "error.border": "#66272dff", - "hidden": "#696a6aff", - "hidden.background": "#313337ff", - "hidden.border": "#383a3eff", - "hint": "#628b80ff", - "hint.background": "#0d2f4eff", - "hint.border": "#1b4a6eff", - "ignored": "#696a6aff", - "ignored.background": "#313337ff", - "ignored.border": "#3f4043ff", - "info": "#5ac1feff", - "info.background": "#0d2f4eff", - "info.border": "#1b4a6eff", - "modified": "#feb454ff", - "modified.background": "#572815ff", - "modified.border": "#754221ff", - "predictive": "#5a728bff", - "predictive.background": "#294113ff", - "predictive.border": "#405c1cff", - "renamed": "#5ac1feff", - "renamed.background": "#0d2f4eff", - "renamed.border": "#1b4a6eff", - "success": "#aad84cff", - "success.background": "#294113ff", - "success.border": "#405c1cff", - "unreachable": "#8a8986ff", - "unreachable.background": "#313337ff", - "unreachable.border": "#3f4043ff", - "warning": "#feb454ff", - "warning.background": "#572815ff", - "warning.border": "#754221ff", - "players": [ - { - "cursor": "#5ac1feff", - "background": "#5ac1feff", - "selection": "#5ac1fe3d" - }, - { - "cursor": "#39bae5ff", - "background": "#39bae5ff", - "selection": "#39bae53d" - }, - { - "cursor": "#fe8f40ff", - "background": "#fe8f40ff", - "selection": "#fe8f403d" - }, - { - "cursor": "#d2a6feff", - "background": "#d2a6feff", - "selection": "#d2a6fe3d" - }, - { - "cursor": "#95e5cbff", - "background": "#95e5cbff", - "selection": "#95e5cb3d" - }, - { - "cursor": "#ef7177ff", - "background": "#ef7177ff", - "selection": "#ef71773d" - }, - { - "cursor": "#feb454ff", - "background": "#feb454ff", - "selection": "#feb4543d" - }, - { - "cursor": "#aad84cff", - "background": "#aad84cff", - "selection": "#aad84c3d" - } - ], - "syntax": { - "attribute": { - "color": "#5ac1feff", - "font_style": null, - "font_weight": null - }, - "boolean": { - "color": "#d2a6ffff", - "font_style": null, - "font_weight": null - }, - "comment": { - "color": "#5c6773ff", - "font_style": null, - "font_weight": null - }, - "comment.doc": { - "color": "#8c8b88ff", - "font_style": null, - "font_weight": null - }, - "constant": { - "color": "#d2a6ffff", - "font_style": null, - "font_weight": null - }, - "constructor": { - "color": "#5ac1feff", - "font_style": null, - "font_weight": null - }, - "embedded": { - "color": "#bfbdb6ff", - "font_style": null, - "font_weight": null - }, - "emphasis": { - "color": "#5ac1feff", - "font_style": null, - "font_weight": null - }, - "emphasis.strong": { - "color": "#5ac1feff", - "font_style": null, - "font_weight": 700 - }, - "enum": { - "color": "#fe8f40ff", - "font_style": null, - "font_weight": null - }, - "function": { - "color": "#ffb353ff", - "font_style": null, - "font_weight": null - }, - "hint": { - "color": "#628b80ff", - "font_style": null, - "font_weight": null - }, - "keyword": { - "color": "#ff8f3fff", - "font_style": null, - "font_weight": null - }, - "label": { - "color": "#5ac1feff", - "font_style": null, - "font_weight": null - }, - "link_text": { - "color": "#fe8f40ff", - "font_style": "italic", - "font_weight": null - }, - "link_uri": { - "color": "#aad84cff", - "font_style": null, - "font_weight": null - }, - "namespace": { - "color": "#bfbdb6ff", - "font_style": null, - "font_weight": null - }, - "number": { - "color": "#d2a6ffff", - "font_style": null, - "font_weight": null - }, - "operator": { - "color": "#f29668ff", - "font_style": null, - "font_weight": null - }, - "predictive": { - "color": "#5a728bff", - "font_style": "italic", - "font_weight": null - }, - "preproc": { - "color": "#bfbdb6ff", - "font_style": null, - "font_weight": null - }, - "primary": { - "color": "#bfbdb6ff", - "font_style": null, - "font_weight": null - }, - "property": { - "color": "#5ac1feff", - "font_style": null, - "font_weight": null - }, - "punctuation": { - "color": "#a6a5a0ff", - "font_style": null, - "font_weight": null - }, - "punctuation.bracket": { - "color": "#a6a5a0ff", - "font_style": null, - "font_weight": null - }, - "punctuation.delimiter": { - "color": "#a6a5a0ff", - "font_style": null, - "font_weight": null - }, - "punctuation.list_marker": { - "color": "#a6a5a0ff", - "font_style": null, - "font_weight": null - }, - "punctuation.markup": { - "color": "#a6a5a0ff", - "font_style": null, - "font_weight": null - }, - "punctuation.special": { - "color": "#d2a6ffff", - "font_style": null, - "font_weight": null - }, - "selector": { - "color": "#d2a6ffff", - "font_style": null, - "font_weight": null - }, - "selector.pseudo": { - "color": "#5ac1feff", - "font_style": null, - "font_weight": null - }, - "string": { - "color": "#a9d94bff", - "font_style": null, - "font_weight": null - }, - "string.escape": { - "color": "#8c8b88ff", - "font_style": null, - "font_weight": null - }, - "string.regex": { - "color": "#95e6cbff", - "font_style": null, - "font_weight": null - }, - "string.special": { - "color": "#e5b572ff", - "font_style": null, - "font_weight": null - }, - "string.special.symbol": { - "color": "#fe8f40ff", - "font_style": null, - "font_weight": null - }, - "tag": { - "color": "#5ac1feff", - "font_style": null, - "font_weight": null - }, - "text.literal": { - "color": "#fe8f40ff", - "font_style": null, - "font_weight": null - }, - "title": { - "color": "#bfbdb6ff", - "font_style": null, - "font_weight": 700 - }, - "type": { - "color": "#59c2ffff", - "font_style": null, - "font_weight": null - }, - "variable": { - "color": "#bfbdb6ff", - "font_style": null, - "font_weight": null - }, - "variant": { - "color": "#5ac1feff", - "font_style": null, - "font_weight": null - } - } - } - }, - { - "name": "Ayu Light", - "appearance": "light", - "style": { - "border": "#cfd1d2ff", - "border.variant": "#dfe0e1ff", - "border.focused": "#c4daf6ff", - "border.selected": "#c4daf6ff", - "border.transparent": "#00000000", - "border.disabled": "#d5d6d8ff", - "elevated_surface.background": "#ececedff", - "surface.background": "#ececedff", - "background": "#dcdddeff", - "element.background": "#ececedff", - "element.hover": "#dfe0e1ff", - "element.active": "#cfd0d2ff", - "element.selected": "#cfd0d2ff", - "element.disabled": "#ececedff", - "drop_target.background": "#8b8e9280", - "ghost_element.background": "#00000000", - "ghost_element.hover": "#dfe0e1ff", - "ghost_element.active": "#cfd0d2ff", - "ghost_element.selected": "#cfd0d2ff", - "ghost_element.disabled": "#ececedff", - "text": "#5c6166ff", - "text.muted": "#8b8e92ff", - "text.placeholder": "#a9acaeff", - "text.disabled": "#a9acaeff", - "text.accent": "#3b9ee5ff", - "icon": "#5c6166ff", - "icon.muted": "#8b8e92ff", - "icon.disabled": "#a9acaeff", - "icon.placeholder": "#8b8e92ff", - "icon.accent": "#3b9ee5ff", - "status_bar.background": "#dcdddeff", - "title_bar.background": "#dcdddeff", - "title_bar.inactive_background": "#ececedff", - "toolbar.background": "#fcfcfcff", - "tab_bar.background": "#ececedff", - "tab.inactive_background": "#ececedff", - "tab.active_background": "#fcfcfcff", - "search.match_background": "#3b9ee566", - "search.active_match_background": "#f88b3666", - "panel.background": "#ececedff", - "panel.focused_border": "#3b9ee5ff", - "pane.focused_border": null, - "scrollbar.thumb.background": "#5c61664c", - "scrollbar.thumb.hover_background": "#dfe0e1ff", - "scrollbar.thumb.border": "#dfe0e1ff", - "scrollbar.track.background": "#00000000", - "scrollbar.track.border": "#efeff0ff", - "editor.foreground": "#5c6166ff", - "editor.background": "#fcfcfcff", - "editor.gutter.background": "#fcfcfcff", - "editor.subheader.background": "#ececedff", - "editor.active_line.background": "#ececedbf", - "editor.highlighted_line.background": "#ececedff", - "editor.line_number": "#b0b3b5", - "editor.active_line_number": "#313435", - "editor.hover_line_number": "#62686a", - "editor.invisible": "#acafb1ff", - "editor.wrap_guide": "#5c61660d", - "editor.active_wrap_guide": "#5c61661a", - "editor.document_highlight.read_background": "#3b9ee51a", - "editor.document_highlight.write_background": "#acafb166", - "terminal.background": "#fcfcfcff", - "terminal.foreground": "#5c6166ff", - "terminal.bright_foreground": "#5c6166ff", - "terminal.dim_foreground": "#fcfcfcff", - "terminal.ansi.black": "#5c6166ff", - "terminal.ansi.bright_black": "#3b9ee5ff", - "terminal.ansi.dim_black": "#9c9fa2ff", - "terminal.ansi.red": "#ef7271ff", - "terminal.ansi.bright_red": "#febab6ff", - "terminal.ansi.dim_red": "#833538ff", - "terminal.ansi.green": "#85b304ff", - "terminal.ansi.bright_green": "#c7d98fff", - "terminal.ansi.dim_green": "#445613ff", - "terminal.ansi.yellow": "#f1ad49ff", - "terminal.ansi.bright_yellow": "#fed5a3ff", - "terminal.ansi.dim_yellow": "#8a5227ff", - "terminal.ansi.blue": "#3b9ee5ff", - "terminal.ansi.bright_blue": "#abcdf2ff", - "terminal.ansi.dim_blue": "#214c76ff", - "terminal.ansi.magenta": "#55b4d3ff", - "terminal.ansi.bright_magenta": "#b1d8e8ff", - "terminal.ansi.dim_magenta": "#2f5669ff", - "terminal.ansi.cyan": "#4dbf99ff", - "terminal.ansi.bright_cyan": "#ace0cbff", - "terminal.ansi.dim_cyan": "#2a5f4aff", - "terminal.ansi.white": "#fcfcfcff", - "terminal.ansi.bright_white": "#ffffffff", - "terminal.ansi.dim_white": "#bcbec0ff", - "link_text.hover": "#3b9ee5ff", - "conflict": "#f1ad49ff", - "conflict.background": "#ffeedaff", - "conflict.border": "#ffe1beff", - "created": "#85b304ff", - "created.background": "#e9efd2ff", - "created.border": "#d7e3aeff", - "deleted": "#ef7271ff", - "deleted.background": "#ffe3e1ff", - "deleted.border": "#ffcdcaff", - "error": "#ef7271ff", - "error.background": "#ffe3e1ff", - "error.border": "#ffcdcaff", - "hidden": "#a9acaeff", - "hidden.background": "#dcdddeff", - "hidden.border": "#d5d6d8ff", - "hint": "#8ca7c2ff", - "hint.background": "#deebfaff", - "hint.border": "#c4daf6ff", - "ignored": "#a9acaeff", - "ignored.background": "#dcdddeff", - "ignored.border": "#cfd1d2ff", - "info": "#3b9ee5ff", - "info.background": "#deebfaff", - "info.border": "#c4daf6ff", - "modified": "#f1ad49ff", - "modified.background": "#ffeedaff", - "modified.border": "#ffe1beff", - "predictive": "#9eb9d3ff", - "predictive.background": "#e9efd2ff", - "predictive.border": "#d7e3aeff", - "renamed": "#3b9ee5ff", - "renamed.background": "#deebfaff", - "renamed.border": "#c4daf6ff", - "success": "#85b304ff", - "success.background": "#e9efd2ff", - "success.border": "#d7e3aeff", - "unreachable": "#8b8e92ff", - "unreachable.background": "#dcdddeff", - "unreachable.border": "#cfd1d2ff", - "warning": "#f1ad49ff", - "warning.background": "#ffeedaff", - "warning.border": "#ffe1beff", - "players": [ - { - "cursor": "#3b9ee5ff", - "background": "#3b9ee5ff", - "selection": "#3b9ee53d" - }, - { - "cursor": "#55b4d3ff", - "background": "#55b4d3ff", - "selection": "#55b4d33d" - }, - { - "cursor": "#f98d3fff", - "background": "#f98d3fff", - "selection": "#f98d3f3d" - }, - { - "cursor": "#a37accff", - "background": "#a37accff", - "selection": "#a37acc3d" - }, - { - "cursor": "#4dbf99ff", - "background": "#4dbf99ff", - "selection": "#4dbf993d" - }, - { - "cursor": "#ef7271ff", - "background": "#ef7271ff", - "selection": "#ef72713d" - }, - { - "cursor": "#f1ad49ff", - "background": "#f1ad49ff", - "selection": "#f1ad493d" - }, - { - "cursor": "#85b304ff", - "background": "#85b304ff", - "selection": "#85b3043d" - } - ], - "syntax": { - "attribute": { - "color": "#3b9ee5ff", - "font_style": null, - "font_weight": null - }, - "boolean": { - "color": "#a37accff", - "font_style": null, - "font_weight": null - }, - "comment": { - "color": "#abb0b6ff", - "font_style": null, - "font_weight": null - }, - "comment.doc": { - "color": "#898d90ff", - "font_style": null, - "font_weight": null - }, - "constant": { - "color": "#a37accff", - "font_style": null, - "font_weight": null - }, - "constructor": { - "color": "#3b9ee5ff", - "font_style": null, - "font_weight": null - }, - "embedded": { - "color": "#5c6166ff", - "font_style": null, - "font_weight": null - }, - "emphasis": { - "color": "#3b9ee5ff", - "font_style": null, - "font_weight": null - }, - "emphasis.strong": { - "color": "#3b9ee5ff", - "font_style": null, - "font_weight": 700 - }, - "enum": { - "color": "#f98d3fff", - "font_style": null, - "font_weight": null - }, - "function": { - "color": "#f2ad48ff", - "font_style": null, - "font_weight": null - }, - "hint": { - "color": "#8ca7c2ff", - "font_style": null, - "font_weight": null - }, - "keyword": { - "color": "#fa8d3eff", - "font_style": null, - "font_weight": null - }, - "label": { - "color": "#3b9ee5ff", - "font_style": null, - "font_weight": null - }, - "link_text": { - "color": "#f98d3fff", - "font_style": "italic", - "font_weight": null - }, - "link_uri": { - "color": "#85b304ff", - "font_style": null, - "font_weight": null - }, - "namespace": { - "color": "#5c6166ff", - "font_style": null, - "font_weight": null - }, - "number": { - "color": "#a37accff", - "font_style": null, - "font_weight": null - }, - "operator": { - "color": "#ed9365ff", - "font_style": null, - "font_weight": null - }, - "predictive": { - "color": "#9eb9d3ff", - "font_style": "italic", - "font_weight": null - }, - "preproc": { - "color": "#5c6166ff", - "font_style": null, - "font_weight": null - }, - "primary": { - "color": "#5c6166ff", - "font_style": null, - "font_weight": null - }, - "property": { - "color": "#3b9ee5ff", - "font_style": null, - "font_weight": null - }, - "punctuation": { - "color": "#73777bff", - "font_style": null, - "font_weight": null - }, - "punctuation.bracket": { - "color": "#73777bff", - "font_style": null, - "font_weight": null - }, - "punctuation.delimiter": { - "color": "#73777bff", - "font_style": null, - "font_weight": null - }, - "punctuation.list_marker": { - "color": "#73777bff", - "font_style": null, - "font_weight": null - }, - "punctuation.markup": { - "color": "#73777bff", - "font_style": null, - "font_weight": null - }, - "punctuation.special": { - "color": "#a37accff", - "font_style": null, - "font_weight": null - }, - "selector": { - "color": "#a37accff", - "font_style": null, - "font_weight": null - }, - "selector.pseudo": { - "color": "#3b9ee5ff", - "font_style": null, - "font_weight": null - }, - "string": { - "color": "#86b300ff", - "font_style": null, - "font_weight": null - }, - "string.escape": { - "color": "#898d90ff", - "font_style": null, - "font_weight": null - }, - "string.regex": { - "color": "#4bbf98ff", - "font_style": null, - "font_weight": null - }, - "string.special": { - "color": "#e6ba7eff", - "font_style": null, - "font_weight": null - }, - "string.special.symbol": { - "color": "#f98d3fff", - "font_style": null, - "font_weight": null - }, - "tag": { - "color": "#3b9ee5ff", - "font_style": null, - "font_weight": null - }, - "text.literal": { - "color": "#f98d3fff", - "font_style": null, - "font_weight": null - }, - "title": { - "color": "#5c6166ff", - "font_style": null, - "font_weight": 700 - }, - "type": { - "color": "#389ee6ff", - "font_style": null, - "font_weight": null - }, - "variable": { - "color": "#5c6166ff", - "font_style": null, - "font_weight": null - }, - "variant": { - "color": "#3b9ee5ff", - "font_style": null, - "font_weight": null - } - } - } - }, - { - "name": "Ayu Mirage", - "appearance": "dark", - "style": { - "border": "#53565dff", - "border.variant": "#43464fff", - "border.focused": "#24556fff", - "border.selected": "#24556fff", - "border.transparent": "#00000000", - "border.disabled": "#4d5058ff", - "elevated_surface.background": "#353944ff", - "surface.background": "#353944ff", - "background": "#464a52ff", - "element.background": "#353944ff", - "element.hover": "#43464fff", - "element.active": "#53565dff", - "element.selected": "#53565dff", - "element.disabled": "#353944ff", - "drop_target.background": "#9a9a9880", - "ghost_element.background": "#00000000", - "ghost_element.hover": "#43464fff", - "ghost_element.active": "#53565dff", - "ghost_element.selected": "#53565dff", - "ghost_element.disabled": "#353944ff", - "text": "#cccac2ff", - "text.muted": "#9a9a98ff", - "text.placeholder": "#7b7d7fff", - "text.disabled": "#7b7d7fff", - "text.accent": "#72cffeff", - "icon": "#cccac2ff", - "icon.muted": "#9a9a98ff", - "icon.disabled": "#7b7d7fff", - "icon.placeholder": "#9a9a98ff", - "icon.accent": "#72cffeff", - "status_bar.background": "#464a52ff", - "title_bar.background": "#464a52ff", - "title_bar.inactive_background": "#353944ff", - "toolbar.background": "#242835ff", - "tab_bar.background": "#353944ff", - "tab.inactive_background": "#353944ff", - "tab.active_background": "#242835ff", - "search.match_background": "#73cffe66", - "search.active_match_background": "#fd722b66", - "panel.background": "#353944ff", - "panel.focused_border": null, - "pane.focused_border": null, - "scrollbar.thumb.background": "#cccac24c", - "scrollbar.thumb.hover_background": "#43464fff", - "scrollbar.thumb.border": "#43464fff", - "scrollbar.track.background": "#00000000", - "scrollbar.track.border": "#323641ff", - "editor.foreground": "#cccac2ff", - "editor.background": "#242835ff", - "editor.gutter.background": "#242835ff", - "editor.subheader.background": "#353944ff", - "editor.active_line.background": "#353944bf", - "editor.highlighted_line.background": "#353944ff", - "editor.line_number": "#575c6b", - "editor.active_line_number": "#e1e3ea", - "editor.hover_line_number": "#b2b6c8", - "editor.invisible": "#787a7cff", - "editor.wrap_guide": "#cccac20d", - "editor.active_wrap_guide": "#cccac21a", - "editor.document_highlight.read_background": "#72cffe1a", - "editor.document_highlight.write_background": "#787a7c66", - "terminal.background": "#242835ff", - "terminal.foreground": "#cccac2ff", - "terminal.bright_foreground": "#cccac2ff", - "terminal.dim_foreground": "#242835ff", - "terminal.ansi.black": "#242835ff", - "terminal.ansi.bright_black": "#67696eff", - "terminal.ansi.dim_black": "#cccac2ff", - "terminal.ansi.red": "#f18779ff", - "terminal.ansi.bright_red": "#833f3cff", - "terminal.ansi.dim_red": "#fec4baff", - "terminal.ansi.green": "#d5fe80ff", - "terminal.ansi.bright_green": "#75993cff", - "terminal.ansi.dim_green": "#ecffc1ff", - "terminal.ansi.yellow": "#fecf72ff", - "terminal.ansi.bright_yellow": "#937237ff", - "terminal.ansi.dim_yellow": "#ffe7b9ff", - "terminal.ansi.blue": "#72cffeff", - "terminal.ansi.bright_blue": "#336d8dff", - "terminal.ansi.dim_blue": "#c1e7ffff", - "terminal.ansi.magenta": "#5bcde5ff", - "terminal.ansi.bright_magenta": "#2b6c7bff", - "terminal.ansi.dim_magenta": "#b7e7f2ff", - "terminal.ansi.cyan": "#95e5cbff", - "terminal.ansi.bright_cyan": "#4c806fff", - "terminal.ansi.dim_cyan": "#cbf2e4ff", - "terminal.ansi.white": "#cccac2ff", - "terminal.ansi.bright_white": "#fafafaff", - "terminal.ansi.dim_white": "#898a8aff", - "link_text.hover": "#72cffeff", - "conflict": "#fecf72ff", - "conflict.background": "#574018ff", - "conflict.border": "#765a29ff", - "created": "#d5fe80ff", - "created.background": "#426117ff", - "created.border": "#5d7e2cff", - "deleted": "#f18779ff", - "deleted.background": "#481a1bff", - "deleted.border": "#662e2dff", - "error": "#f18779ff", - "error.background": "#481a1bff", - "error.border": "#662e2dff", - "hidden": "#7b7d7fff", - "hidden.background": "#464a52ff", - "hidden.border": "#4d5058ff", - "hint": "#7399a3ff", - "hint.background": "#123950ff", - "hint.border": "#24556fff", - "ignored": "#7b7d7fff", - "ignored.background": "#464a52ff", - "ignored.border": "#53565dff", - "info": "#72cffeff", - "info.background": "#123950ff", - "info.border": "#24556fff", - "modified": "#fecf72ff", - "modified.background": "#574018ff", - "modified.border": "#765a29ff", - "predictive": "#6d839bff", - "predictive.background": "#426117ff", - "predictive.border": "#5d7e2cff", - "renamed": "#72cffeff", - "renamed.background": "#123950ff", - "renamed.border": "#24556fff", - "success": "#d5fe80ff", - "success.background": "#426117ff", - "success.border": "#5d7e2cff", - "unreachable": "#9a9a98ff", - "unreachable.background": "#464a52ff", - "unreachable.border": "#53565dff", - "warning": "#fecf72ff", - "warning.background": "#574018ff", - "warning.border": "#765a29ff", - "players": [ - { - "cursor": "#72cffeff", - "background": "#72cffeff", - "selection": "#72cffe3d" - }, - { - "cursor": "#5bcde5ff", - "background": "#5bcde5ff", - "selection": "#5bcde53d" - }, - { - "cursor": "#fead66ff", - "background": "#fead66ff", - "selection": "#fead663d" - }, - { - "cursor": "#debffeff", - "background": "#debffeff", - "selection": "#debffe3d" - }, - { - "cursor": "#95e5cbff", - "background": "#95e5cbff", - "selection": "#95e5cb3d" - }, - { - "cursor": "#f18779ff", - "background": "#f18779ff", - "selection": "#f187793d" - }, - { - "cursor": "#fecf72ff", - "background": "#fecf72ff", - "selection": "#fecf723d" - }, - { - "cursor": "#d5fe80ff", - "background": "#d5fe80ff", - "selection": "#d5fe803d" - } - ], - "syntax": { - "attribute": { - "color": "#72cffeff", - "font_style": null, - "font_weight": null - }, - "boolean": { - "color": "#dfbfffff", - "font_style": null, - "font_weight": null - }, - "comment": { - "color": "#5c6773ff", - "font_style": null, - "font_weight": null - }, - "comment.doc": { - "color": "#9b9b99ff", - "font_style": null, - "font_weight": null - }, - "constant": { - "color": "#dfbfffff", - "font_style": null, - "font_weight": null - }, - "constructor": { - "color": "#72cffeff", - "font_style": null, - "font_weight": null - }, - "embedded": { - "color": "#cccac2ff", - "font_style": null, - "font_weight": null - }, - "emphasis": { - "color": "#72cffeff", - "font_style": null, - "font_weight": null - }, - "emphasis.strong": { - "color": "#72cffeff", - "font_style": null, - "font_weight": 700 - }, - "enum": { - "color": "#fead66ff", - "font_style": null, - "font_weight": null - }, - "function": { - "color": "#ffd173ff", - "font_style": null, - "font_weight": null - }, - "hint": { - "color": "#7399a3ff", - "font_style": null, - "font_weight": null - }, - "keyword": { - "color": "#ffad65ff", - "font_style": null, - "font_weight": null - }, - "label": { - "color": "#72cffeff", - "font_style": null, - "font_weight": null - }, - "link_text": { - "color": "#fead66ff", - "font_style": "italic", - "font_weight": null - }, - "link_uri": { - "color": "#d5fe80ff", - "font_style": null, - "font_weight": null - }, - "namespace": { - "color": "#cccac2ff", - "font_style": null, - "font_weight": null - }, - "number": { - "color": "#dfbfffff", - "font_style": null, - "font_weight": null - }, - "operator": { - "color": "#f29e74ff", - "font_style": null, - "font_weight": null - }, - "predictive": { - "color": "#6d839bff", - "font_style": "italic", - "font_weight": null - }, - "preproc": { - "color": "#cccac2ff", - "font_style": null, - "font_weight": null - }, - "primary": { - "color": "#cccac2ff", - "font_style": null, - "font_weight": null - }, - "property": { - "color": "#72cffeff", - "font_style": null, - "font_weight": null - }, - "punctuation": { - "color": "#b4b3aeff", - "font_style": null, - "font_weight": null - }, - "punctuation.bracket": { - "color": "#b4b3aeff", - "font_style": null, - "font_weight": null - }, - "punctuation.delimiter": { - "color": "#b4b3aeff", - "font_style": null, - "font_weight": null - }, - "punctuation.list_marker": { - "color": "#b4b3aeff", - "font_style": null, - "font_weight": null - }, - "punctuation.markup": { - "color": "#b4b3aeff", - "font_style": null, - "font_weight": null - }, - "punctuation.special": { - "color": "#dfbfffff", - "font_style": null, - "font_weight": null - }, - "selector": { - "color": "#dfbfffff", - "font_style": null, - "font_weight": null - }, - "selector.pseudo": { - "color": "#72cffeff", - "font_style": null, - "font_weight": null - }, - "string": { - "color": "#d4fe7fff", - "font_style": null, - "font_weight": null - }, - "string.escape": { - "color": "#9b9b99ff", - "font_style": null, - "font_weight": null - }, - "string.regex": { - "color": "#95e6cbff", - "font_style": null, - "font_weight": null - }, - "string.special": { - "color": "#ffdfb3ff", - "font_style": null, - "font_weight": null - }, - "string.special.symbol": { - "color": "#fead66ff", - "font_style": null, - "font_weight": null - }, - "tag": { - "color": "#72cffeff", - "font_style": null, - "font_weight": null - }, - "text.literal": { - "color": "#fead66ff", - "font_style": null, - "font_weight": null - }, - "title": { - "color": "#cccac2ff", - "font_style": null, - "font_weight": 700 - }, - "type": { - "color": "#73cfffff", - "font_style": null, - "font_weight": null - }, - "variable": { - "color": "#cccac2ff", - "font_style": null, - "font_weight": null - }, - "variant": { - "color": "#72cffeff", - "font_style": null, - "font_weight": null - } - } - } - } - ] -} diff --git a/assets/themes/gruvbox/LICENSE b/assets/themes/gruvbox/LICENSE deleted file mode 100644 index 0e18d6d7a9..0000000000 --- a/assets/themes/gruvbox/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/assets/themes/gruvbox/gruvbox.json b/assets/themes/gruvbox/gruvbox.json deleted file mode 100644 index 90973fd6c3..0000000000 --- a/assets/themes/gruvbox/gruvbox.json +++ /dev/null @@ -1,2449 +0,0 @@ -{ - "$schema": "https://zed.dev/schema/themes/v0.2.0.json", - "name": "Gruvbox", - "author": "Zed Industries", - "themes": [ - { - "name": "Gruvbox Dark", - "appearance": "dark", - "style": { - "accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"], - "border": "#5b534dff", - "border.variant": "#494340ff", - "border.focused": "#303a36ff", - "border.selected": "#303a36ff", - "border.transparent": "#00000000", - "border.disabled": "#544c48ff", - "elevated_surface.background": "#3a3735ff", - "surface.background": "#3a3735ff", - "background": "#4c4642ff", - "element.background": "#3a3735ff", - "element.hover": "#494340ff", - "element.active": "#5b524cff", - "element.selected": "#5b524cff", - "element.disabled": "#3a3735ff", - "drop_target.background": "#c5b59780", - "ghost_element.background": "#00000000", - "ghost_element.hover": "#494340ff", - "ghost_element.active": "#5b524cff", - "ghost_element.selected": "#5b524cff", - "ghost_element.disabled": "#3a3735ff", - "text": "#fbf1c7ff", - "text.muted": "#c5b597ff", - "text.placeholder": "#998b78ff", - "text.disabled": "#998b78ff", - "text.accent": "#83a598ff", - "icon": "#fbf1c7ff", - "icon.muted": "#c5b597ff", - "icon.disabled": "#998b78ff", - "icon.placeholder": "#c5b597ff", - "icon.accent": "#83a598ff", - "status_bar.background": "#4c4642ff", - "title_bar.background": "#4c4642ff", - "title_bar.inactive_background": "#3a3735ff", - "toolbar.background": "#282828ff", - "tab_bar.background": "#3a3735ff", - "tab.inactive_background": "#3a3735ff", - "tab.active_background": "#282828ff", - "search.match_background": "#83a59866", - "search.active_match_background": "#c09f3f66", - "panel.background": "#3a3735ff", - "panel.focused_border": "#83a598ff", - "pane.focused_border": null, - "scrollbar.thumb.active_background": "#83a598ac", - "scrollbar.thumb.hover_background": "#fbf1c74c", - "scrollbar.thumb.background": "#a899844c", - "scrollbar.thumb.border": "#494340ff", - "scrollbar.track.background": "#00000000", - "scrollbar.track.border": "#373432ff", - "editor.foreground": "#ebdbb2ff", - "editor.background": "#282828ff", - "editor.gutter.background": "#282828ff", - "editor.subheader.background": "#3a3735ff", - "editor.active_line.background": "#3a3735bf", - "editor.highlighted_line.background": "#3a3735ff", - "editor.line_number": "#6e6b5e", - "editor.active_line_number": "#dedcd3", - "editor.hover_line_number": "#c9c5b6", - "editor.invisible": "#928474ff", - "editor.wrap_guide": "#fbf1c70d", - "editor.active_wrap_guide": "#fbf1c71a", - "editor.document_highlight.read_background": "#83a5981a", - "editor.document_highlight.write_background": "#92847466", - "terminal.background": "#282828ff", - "terminal.foreground": "#fbf1c7ff", - "terminal.bright_foreground": "#fbf1c7ff", - "terminal.dim_foreground": "#282828ff", - "terminal.ansi.black": "#282828ff", - "terminal.ansi.bright_black": "#73675eff", - "terminal.ansi.dim_black": "#fbf1c7ff", - "terminal.ansi.red": "#fb4a35ff", - "terminal.ansi.bright_red": "#93201dff", - "terminal.ansi.dim_red": "#ffaa95ff", - "terminal.ansi.green": "#b7bb26ff", - "terminal.ansi.bright_green": "#605c1bff", - "terminal.ansi.dim_green": "#e0dc98ff", - "terminal.ansi.yellow": "#f9bd2fff", - "terminal.ansi.bright_yellow": "#91611bff", - "terminal.ansi.dim_yellow": "#fedc9bff", - "terminal.ansi.blue": "#83a598ff", - "terminal.ansi.bright_blue": "#414f4aff", - "terminal.ansi.dim_blue": "#c0d2cbff", - "terminal.ansi.magenta": "#d3869bff", - "terminal.ansi.bright_magenta": "#8e5868ff", - "terminal.ansi.dim_magenta": "#ff9ebbff", - "terminal.ansi.cyan": "#8ec07cff", - "terminal.ansi.bright_cyan": "#45603eff", - "terminal.ansi.dim_cyan": "#c7dfbdff", - "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#ffffffff", - "terminal.ansi.dim_white": "#b0a189ff", - "link_text.hover": "#83a598ff", - "version_control.added": "#b7bb26ff", - "version_control.modified": "#f9bd2fff", - "version_control.deleted": "#fb4a35ff", - "conflict": "#f9bd2fff", - "conflict.background": "#572e10ff", - "conflict.border": "#754916ff", - "created": "#b7bb26ff", - "created.background": "#322b11ff", - "created.border": "#4a4516ff", - "deleted": "#fb4a35ff", - "deleted.background": "#590a0fff", - "deleted.border": "#771617ff", - "error": "#fb4a35ff", - "error.background": "#590a0fff", - "error.border": "#771617ff", - "hidden": "#998b78ff", - "hidden.background": "#4c4642ff", - "hidden.border": "#544c48ff", - "hint": "#8c957dff", - "hint.background": "#1e2321ff", - "hint.border": "#303a36ff", - "ignored": "#998b78ff", - "ignored.background": "#4c4642ff", - "ignored.border": "#5b534dff", - "info": "#83a598ff", - "info.background": "#1e2321ff", - "info.border": "#303a36ff", - "modified": "#f9bd2fff", - "modified.background": "#572e10ff", - "modified.border": "#754916ff", - "predictive": "#717363ff", - "predictive.background": "#322b11ff", - "predictive.border": "#4a4516ff", - "renamed": "#83a598ff", - "renamed.background": "#1e2321ff", - "renamed.border": "#303a36ff", - "success": "#b7bb26ff", - "success.background": "#322b11ff", - "success.border": "#4a4516ff", - "unreachable": "#c5b597ff", - "unreachable.background": "#4c4642ff", - "unreachable.border": "#5b534dff", - "warning": "#f9bd2fff", - "warning.background": "#572e10ff", - "warning.border": "#754916ff", - "players": [ - { - "cursor": "#83a598ff", - "background": "#83a598ff", - "selection": "#83a5983d" - }, - { - "cursor": "#a89984ff", - "background": "#a89984ff", - "selection": "#a899843d" - }, - { - "cursor": "#fd801bff", - "background": "#fd801bff", - "selection": "#fd801b3d" - }, - { - "cursor": "#d3869bff", - "background": "#d3869bff", - "selection": "#d3869b3d" - }, - { - "cursor": "#8ec07cff", - "background": "#8ec07cff", - "selection": "#8ec07c3d" - }, - { - "cursor": "#fb4a35ff", - "background": "#fb4a35ff", - "selection": "#fb4a353d" - }, - { - "cursor": "#f9bd2fff", - "background": "#f9bd2fff", - "selection": "#f9bd2f3d" - }, - { - "cursor": "#b7bb26ff", - "background": "#b7bb26ff", - "selection": "#b7bb263d" - } - ], - "syntax": { - "attribute": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "boolean": { - "color": "#d3869bff", - "font_style": null, - "font_weight": null - }, - "comment": { - "color": "#a89984ff", - "font_style": null, - "font_weight": null - }, - "comment.doc": { - "color": "#c6b697ff", - "font_style": null, - "font_weight": null - }, - "constant": { - "color": "#fabd2eff", - "font_style": null, - "font_weight": null - }, - "constructor": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "embedded": { - "color": "#8ec07cff", - "font_style": null, - "font_weight": null - }, - "emphasis": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "emphasis.strong": { - "color": "#83a598ff", - "font_style": null, - "font_weight": 700 - }, - "enum": { - "color": "#fe7f18ff", - "font_style": null, - "font_weight": null - }, - "function": { - "color": "#b8bb25ff", - "font_style": null, - "font_weight": null - }, - "function.builtin": { - "color": "#fb4833ff", - "font_style": null, - "font_weight": null - }, - "hint": { - "color": "#8c957dff", - "font_style": null, - "font_weight": null - }, - "keyword": { - "color": "#fb4833ff", - "font_style": null, - "font_weight": null - }, - "label": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "link_text": { - "color": "#8ec07cff", - "font_style": "italic", - "font_weight": null - }, - "link_uri": { - "color": "#d3869bff", - "font_style": null, - "font_weight": null - }, - "namespace": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "number": { - "color": "#d3869bff", - "font_style": null, - "font_weight": null - }, - "operator": { - "color": "#8ec07cff", - "font_style": null, - "font_weight": null - }, - "predictive": { - "color": "#717363ff", - "font_style": "italic", - "font_weight": null - }, - "preproc": { - "color": "#fbf1c7ff", - "font_style": null, - "font_weight": null - }, - "primary": { - "color": "#ebdbb2ff", - "font_style": null, - "font_weight": null - }, - "property": { - "color": "#ebdbb2ff", - "font_style": null, - "font_weight": null - }, - "punctuation": { - "color": "#d5c4a1ff", - "font_style": null, - "font_weight": null - }, - "punctuation.bracket": { - "color": "#a89984ff", - "font_style": null, - "font_weight": null - }, - "punctuation.delimiter": { - "color": "#e5d5adff", - "font_style": null, - "font_weight": null - }, - "punctuation.list_marker": { - "color": "#ebdbb2ff", - "font_style": null, - "font_weight": null - }, - "punctuation.markup": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "punctuation.special": { - "color": "#e5d5adff", - "font_style": null, - "font_weight": null - }, - "selector": { - "color": "#fabd2eff", - "font_style": null, - "font_weight": null - }, - "selector.pseudo": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "string": { - "color": "#b8bb25ff", - "font_style": null, - "font_weight": null - }, - "string.escape": { - "color": "#c6b697ff", - "font_style": null, - "font_weight": null - }, - "string.regex": { - "color": "#fe7f18ff", - "font_style": null, - "font_weight": null - }, - "string.special": { - "color": "#d3869bff", - "font_style": null, - "font_weight": null - }, - "string.special.symbol": { - "color": "#8ec07cff", - "font_style": null, - "font_weight": null - }, - "tag": { - "color": "#8ec07cff", - "font_style": null, - "font_weight": null - }, - "text.literal": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "title": { - "color": "#b8bb25ff", - "font_style": null, - "font_weight": 700 - }, - "type": { - "color": "#fabd2eff", - "font_style": null, - "font_weight": null - }, - "variable": { - "color": "#ebdbb2ff", - "font_style": null, - "font_weight": null - }, - "variable.special": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "variant": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - } - } - } - }, - { - "name": "Gruvbox Dark Hard", - "appearance": "dark", - "style": { - "accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"], - "border": "#5b534dff", - "border.variant": "#494340ff", - "border.focused": "#303a36ff", - "border.selected": "#303a36ff", - "border.transparent": "#00000000", - "border.disabled": "#544c48ff", - "elevated_surface.background": "#393634ff", - "surface.background": "#393634ff", - "background": "#4c4642ff", - "element.background": "#393634ff", - "element.hover": "#494340ff", - "element.active": "#5b524cff", - "element.selected": "#5b524cff", - "element.disabled": "#393634ff", - "drop_target.background": "#c5b59780", - "ghost_element.background": "#00000000", - "ghost_element.hover": "#494340ff", - "ghost_element.active": "#5b524cff", - "ghost_element.selected": "#5b524cff", - "ghost_element.disabled": "#393634ff", - "text": "#fbf1c7ff", - "text.muted": "#c5b597ff", - "text.placeholder": "#998b78ff", - "text.disabled": "#998b78ff", - "text.accent": "#83a598ff", - "icon": "#fbf1c7ff", - "icon.muted": "#c5b597ff", - "icon.disabled": "#998b78ff", - "icon.placeholder": "#c5b597ff", - "icon.accent": "#83a598ff", - "status_bar.background": "#4c4642ff", - "title_bar.background": "#4c4642ff", - "title_bar.inactive_background": "#393634ff", - "toolbar.background": "#1d2021ff", - "tab_bar.background": "#393634ff", - "tab.inactive_background": "#393634ff", - "tab.active_background": "#1d2021ff", - "search.match_background": "#83a59866", - "search.active_match_background": "#c9653666", - "panel.background": "#393634ff", - "panel.focused_border": "#83a598ff", - "pane.focused_border": null, - "scrollbar.thumb.active_background": "#83a598ac", - "scrollbar.thumb.hover_background": "#fbf1c74c", - "scrollbar.thumb.background": "#a899844c", - "scrollbar.thumb.border": "#494340ff", - "scrollbar.track.background": "#00000000", - "scrollbar.track.border": "#343130ff", - "editor.foreground": "#ebdbb2ff", - "editor.background": "#1d2021ff", - "editor.gutter.background": "#1d2021ff", - "editor.subheader.background": "#393634ff", - "editor.active_line.background": "#393634bf", - "editor.highlighted_line.background": "#393634ff", - "editor.line_number": "#6e6b5e", - "editor.active_line_number": "#dedcd3", - "editor.hover_line_number": "#c9c5b6", - "editor.invisible": "#928474ff", - "editor.wrap_guide": "#fbf1c70d", - "editor.active_wrap_guide": "#fbf1c71a", - "editor.document_highlight.read_background": "#83a5981a", - "editor.document_highlight.write_background": "#92847466", - "terminal.background": "#1d2021ff", - "terminal.foreground": "#fbf1c7ff", - "terminal.bright_foreground": "#fbf1c7ff", - "terminal.dim_foreground": "#1d2021ff", - "terminal.ansi.black": "#1d2021ff", - "terminal.ansi.bright_black": "#73675eff", - "terminal.ansi.dim_black": "#fbf1c7ff", - "terminal.ansi.red": "#fb4a35ff", - "terminal.ansi.bright_red": "#93201dff", - "terminal.ansi.dim_red": "#ffaa95ff", - "terminal.ansi.green": "#b7bb26ff", - "terminal.ansi.bright_green": "#605c1bff", - "terminal.ansi.dim_green": "#e0dc98ff", - "terminal.ansi.yellow": "#f9bd2fff", - "terminal.ansi.bright_yellow": "#91611bff", - "terminal.ansi.dim_yellow": "#fedc9bff", - "terminal.ansi.blue": "#83a598ff", - "terminal.ansi.bright_blue": "#414f4aff", - "terminal.ansi.dim_blue": "#c0d2cbff", - "terminal.ansi.magenta": "#d3869bff", - "terminal.ansi.bright_magenta": "#8e5868ff", - "terminal.ansi.dim_magenta": "#ff9ebbff", - "terminal.ansi.cyan": "#8ec07cff", - "terminal.ansi.bright_cyan": "#45603eff", - "terminal.ansi.dim_cyan": "#c7dfbdff", - "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#ffffffff", - "terminal.ansi.dim_white": "#b0a189ff", - "link_text.hover": "#83a598ff", - "version_control.added": "#b7bb26ff", - "version_control.modified": "#f9bd2fff", - "version_control.deleted": "#fb4a35ff", - "conflict": "#f9bd2fff", - "conflict.background": "#572e10ff", - "conflict.border": "#754916ff", - "created": "#b7bb26ff", - "created.background": "#322b11ff", - "created.border": "#4a4516ff", - "deleted": "#fb4a35ff", - "deleted.background": "#590a0fff", - "deleted.border": "#771617ff", - "error": "#fb4a35ff", - "error.background": "#590a0fff", - "error.border": "#771617ff", - "hidden": "#998b78ff", - "hidden.background": "#4c4642ff", - "hidden.border": "#544c48ff", - "hint": "#6a695bff", - "hint.background": "#1e2321ff", - "hint.border": "#303a36ff", - "ignored": "#998b78ff", - "ignored.background": "#4c4642ff", - "ignored.border": "#5b534dff", - "info": "#83a598ff", - "info.background": "#1e2321ff", - "info.border": "#303a36ff", - "modified": "#f9bd2fff", - "modified.background": "#572e10ff", - "modified.border": "#754916ff", - "predictive": "#717363ff", - "predictive.background": "#322b11ff", - "predictive.border": "#4a4516ff", - "renamed": "#83a598ff", - "renamed.background": "#1e2321ff", - "renamed.border": "#303a36ff", - "success": "#b7bb26ff", - "success.background": "#322b11ff", - "success.border": "#4a4516ff", - "unreachable": "#c5b597ff", - "unreachable.background": "#4c4642ff", - "unreachable.border": "#5b534dff", - "warning": "#f9bd2fff", - "warning.background": "#572e10ff", - "warning.border": "#754916ff", - "players": [ - { - "cursor": "#83a598ff", - "background": "#83a598ff", - "selection": "#83a5983d" - }, - { - "cursor": "#a89984ff", - "background": "#a89984ff", - "selection": "#a899843d" - }, - { - "cursor": "#fd801bff", - "background": "#fd801bff", - "selection": "#fd801b3d" - }, - { - "cursor": "#d3869bff", - "background": "#d3869bff", - "selection": "#d3869b3d" - }, - { - "cursor": "#8ec07cff", - "background": "#8ec07cff", - "selection": "#8ec07c3d" - }, - { - "cursor": "#fb4a35ff", - "background": "#fb4a35ff", - "selection": "#fb4a353d" - }, - { - "cursor": "#f9bd2fff", - "background": "#f9bd2fff", - "selection": "#f9bd2f3d" - }, - { - "cursor": "#b7bb26ff", - "background": "#b7bb26ff", - "selection": "#b7bb263d" - } - ], - "syntax": { - "attribute": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "boolean": { - "color": "#d3869bff", - "font_style": null, - "font_weight": null - }, - "comment": { - "color": "#a89984ff", - "font_style": null, - "font_weight": null - }, - "comment.doc": { - "color": "#c6b697ff", - "font_style": null, - "font_weight": null - }, - "constant": { - "color": "#fabd2eff", - "font_style": null, - "font_weight": null - }, - "constructor": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "embedded": { - "color": "#8ec07cff", - "font_style": null, - "font_weight": null - }, - "emphasis": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "emphasis.strong": { - "color": "#83a598ff", - "font_style": null, - "font_weight": 700 - }, - "enum": { - "color": "#fe7f18ff", - "font_style": null, - "font_weight": null - }, - "function": { - "color": "#b8bb25ff", - "font_style": null, - "font_weight": null - }, - "function.builtin": { - "color": "#fb4833ff", - "font_style": null, - "font_weight": null - }, - "hint": { - "color": "#8c957dff", - "font_style": null, - "font_weight": null - }, - "keyword": { - "color": "#fb4833ff", - "font_style": null, - "font_weight": null - }, - "label": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "link_text": { - "color": "#8ec07cff", - "font_style": "italic", - "font_weight": null - }, - "link_uri": { - "color": "#d3869bff", - "font_style": null, - "font_weight": null - }, - "namespace": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "number": { - "color": "#d3869bff", - "font_style": null, - "font_weight": null - }, - "operator": { - "color": "#8ec07cff", - "font_style": null, - "font_weight": null - }, - "predictive": { - "color": "#717363ff", - "font_style": "italic", - "font_weight": null - }, - "preproc": { - "color": "#fbf1c7ff", - "font_style": null, - "font_weight": null - }, - "primary": { - "color": "#ebdbb2ff", - "font_style": null, - "font_weight": null - }, - "property": { - "color": "#ebdbb2ff", - "font_style": null, - "font_weight": null - }, - "punctuation": { - "color": "#d5c4a1ff", - "font_style": null, - "font_weight": null - }, - "punctuation.bracket": { - "color": "#a89984ff", - "font_style": null, - "font_weight": null - }, - "punctuation.delimiter": { - "color": "#e5d5adff", - "font_style": null, - "font_weight": null - }, - "punctuation.list_marker": { - "color": "#ebdbb2ff", - "font_style": null, - "font_weight": null - }, - "punctuation.markup": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "punctuation.special": { - "color": "#e5d5adff", - "font_style": null, - "font_weight": null - }, - "selector": { - "color": "#fabd2eff", - "font_style": null, - "font_weight": null - }, - "selector.pseudo": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "string": { - "color": "#b8bb25ff", - "font_style": null, - "font_weight": null - }, - "string.escape": { - "color": "#c6b697ff", - "font_style": null, - "font_weight": null - }, - "string.regex": { - "color": "#fe7f18ff", - "font_style": null, - "font_weight": null - }, - "string.special": { - "color": "#d3869bff", - "font_style": null, - "font_weight": null - }, - "string.special.symbol": { - "color": "#8ec07cff", - "font_style": null, - "font_weight": null - }, - "tag": { - "color": "#8ec07cff", - "font_style": null, - "font_weight": null - }, - "text.literal": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "title": { - "color": "#b8bb25ff", - "font_style": null, - "font_weight": 700 - }, - "type": { - "color": "#fabd2eff", - "font_style": null, - "font_weight": null - }, - "variable": { - "color": "#ebdbb2ff", - "font_style": null, - "font_weight": null - }, - "variable.special": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "variant": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - } - } - } - }, - { - "name": "Gruvbox Dark Soft", - "appearance": "dark", - "style": { - "accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"], - "border": "#5b534dff", - "border.variant": "#494340ff", - "border.focused": "#303a36ff", - "border.selected": "#303a36ff", - "border.transparent": "#00000000", - "border.disabled": "#544c48ff", - "elevated_surface.background": "#3b3735ff", - "surface.background": "#3b3735ff", - "background": "#4c4642ff", - "element.background": "#3b3735ff", - "element.hover": "#494340ff", - "element.active": "#5b524cff", - "element.selected": "#5b524cff", - "element.disabled": "#3b3735ff", - "drop_target.background": "#c5b59780", - "ghost_element.background": "#00000000", - "ghost_element.hover": "#494340ff", - "ghost_element.active": "#5b524cff", - "ghost_element.selected": "#5b524cff", - "ghost_element.disabled": "#3b3735ff", - "text": "#fbf1c7ff", - "text.muted": "#c5b597ff", - "text.placeholder": "#998b78ff", - "text.disabled": "#998b78ff", - "text.accent": "#83a598ff", - "icon": "#fbf1c7ff", - "icon.muted": "#c5b597ff", - "icon.disabled": "#998b78ff", - "icon.placeholder": "#c5b597ff", - "icon.accent": "#83a598ff", - "status_bar.background": "#4c4642ff", - "title_bar.background": "#4c4642ff", - "title_bar.inactive_background": "#3b3735ff", - "toolbar.background": "#32302fff", - "tab_bar.background": "#3b3735ff", - "tab.inactive_background": "#3b3735ff", - "tab.active_background": "#32302fff", - "search.match_background": "#83a59866", - "search.active_match_background": "#aea85166", - "panel.background": "#3b3735ff", - "panel.focused_border": null, - "pane.focused_border": null, - "scrollbar.thumb.active_background": "#83a598ac", - "scrollbar.thumb.hover_background": "#fbf1c74c", - "scrollbar.thumb.background": "#a899844c", - "scrollbar.thumb.border": "#494340ff", - "scrollbar.track.background": "#00000000", - "scrollbar.track.border": "#393634ff", - "editor.foreground": "#ebdbb2ff", - "editor.background": "#32302fff", - "editor.gutter.background": "#32302fff", - "editor.subheader.background": "#3b3735ff", - "editor.active_line.background": "#3b3735bf", - "editor.highlighted_line.background": "#3b3735ff", - "editor.line_number": "#6e6b5e", - "editor.active_line_number": "#dedcd3", - "editor.hover_line_number": "#c9c5b6", - "editor.invisible": "#928474ff", - "editor.wrap_guide": "#fbf1c70d", - "editor.active_wrap_guide": "#fbf1c71a", - "editor.document_highlight.read_background": "#83a5981a", - "editor.document_highlight.write_background": "#92847466", - "terminal.background": "#32302fff", - "terminal.foreground": "#fbf1c7ff", - "terminal.bright_foreground": "#fbf1c7ff", - "terminal.dim_foreground": "#32302fff", - "terminal.ansi.black": "#32302fff", - "terminal.ansi.bright_black": "#73675eff", - "terminal.ansi.dim_black": "#fbf1c7ff", - "terminal.ansi.red": "#fb4a35ff", - "terminal.ansi.bright_red": "#93201dff", - "terminal.ansi.dim_red": "#ffaa95ff", - "terminal.ansi.green": "#b7bb26ff", - "terminal.ansi.bright_green": "#605c1bff", - "terminal.ansi.dim_green": "#e0dc98ff", - "terminal.ansi.yellow": "#f9bd2fff", - "terminal.ansi.bright_yellow": "#91611bff", - "terminal.ansi.dim_yellow": "#fedc9bff", - "terminal.ansi.blue": "#83a598ff", - "terminal.ansi.bright_blue": "#414f4aff", - "terminal.ansi.dim_blue": "#c0d2cbff", - "terminal.ansi.magenta": "#d3869bff", - "terminal.ansi.bright_magenta": "#8e5868ff", - "terminal.ansi.dim_magenta": "#ff9ebbff", - "terminal.ansi.cyan": "#8ec07cff", - "terminal.ansi.bright_cyan": "#45603eff", - "terminal.ansi.dim_cyan": "#c7dfbdff", - "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#ffffffff", - "terminal.ansi.dim_white": "#b0a189ff", - "link_text.hover": "#83a598ff", - "version_control.added": "#b7bb26ff", - "version_control.modified": "#f9bd2fff", - "version_control.deleted": "#fb4a35ff", - "conflict": "#f9bd2fff", - "conflict.background": "#572e10ff", - "conflict.border": "#754916ff", - "created": "#b7bb26ff", - "created.background": "#322b11ff", - "created.border": "#4a4516ff", - "deleted": "#fb4a35ff", - "deleted.background": "#590a0fff", - "deleted.border": "#771617ff", - "error": "#fb4a35ff", - "error.background": "#590a0fff", - "error.border": "#771617ff", - "hidden": "#998b78ff", - "hidden.background": "#4c4642ff", - "hidden.border": "#544c48ff", - "hint": "#8c957dff", - "hint.background": "#1e2321ff", - "hint.border": "#303a36ff", - "ignored": "#998b78ff", - "ignored.background": "#4c4642ff", - "ignored.border": "#5b534dff", - "info": "#83a598ff", - "info.background": "#1e2321ff", - "info.border": "#303a36ff", - "modified": "#f9bd2fff", - "modified.background": "#572e10ff", - "modified.border": "#754916ff", - "predictive": "#717363ff", - "predictive.background": "#322b11ff", - "predictive.border": "#4a4516ff", - "renamed": "#83a598ff", - "renamed.background": "#1e2321ff", - "renamed.border": "#303a36ff", - "success": "#b7bb26ff", - "success.background": "#322b11ff", - "success.border": "#4a4516ff", - "unreachable": "#c5b597ff", - "unreachable.background": "#4c4642ff", - "unreachable.border": "#5b534dff", - "warning": "#f9bd2fff", - "warning.background": "#572e10ff", - "warning.border": "#754916ff", - "players": [ - { - "cursor": "#83a598ff", - "background": "#83a598ff", - "selection": "#83a5983d" - }, - { - "cursor": "#a89984ff", - "background": "#a89984ff", - "selection": "#a899843d" - }, - { - "cursor": "#fd801bff", - "background": "#fd801bff", - "selection": "#fd801b3d" - }, - { - "cursor": "#d3869bff", - "background": "#d3869bff", - "selection": "#d3869b3d" - }, - { - "cursor": "#8ec07cff", - "background": "#8ec07cff", - "selection": "#8ec07c3d" - }, - { - "cursor": "#fb4a35ff", - "background": "#fb4a35ff", - "selection": "#fb4a353d" - }, - { - "cursor": "#f9bd2fff", - "background": "#f9bd2fff", - "selection": "#f9bd2f3d" - }, - { - "cursor": "#b7bb26ff", - "background": "#b7bb26ff", - "selection": "#b7bb263d" - } - ], - "syntax": { - "attribute": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "boolean": { - "color": "#d3869bff", - "font_style": null, - "font_weight": null - }, - "comment": { - "color": "#a89984ff", - "font_style": null, - "font_weight": null - }, - "comment.doc": { - "color": "#c6b697ff", - "font_style": null, - "font_weight": null - }, - "constant": { - "color": "#fabd2eff", - "font_style": null, - "font_weight": null - }, - "constructor": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "embedded": { - "color": "#8ec07cff", - "font_style": null, - "font_weight": null - }, - "emphasis": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "emphasis.strong": { - "color": "#83a598ff", - "font_style": null, - "font_weight": 700 - }, - "enum": { - "color": "#fe7f18ff", - "font_style": null, - "font_weight": null - }, - "function": { - "color": "#b8bb25ff", - "font_style": null, - "font_weight": null - }, - "function.builtin": { - "color": "#fb4833ff", - "font_style": null, - "font_weight": null - }, - "hint": { - "color": "#8c957dff", - "font_style": null, - "font_weight": null - }, - "keyword": { - "color": "#fb4833ff", - "font_style": null, - "font_weight": null - }, - "label": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "link_text": { - "color": "#8ec07cff", - "font_style": "italic", - "font_weight": null - }, - "link_uri": { - "color": "#d3869bff", - "font_style": null, - "font_weight": null - }, - "namespace": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "number": { - "color": "#d3869bff", - "font_style": null, - "font_weight": null - }, - "operator": { - "color": "#8ec07cff", - "font_style": null, - "font_weight": null - }, - "predictive": { - "color": "#717363ff", - "font_style": "italic", - "font_weight": null - }, - "preproc": { - "color": "#fbf1c7ff", - "font_style": null, - "font_weight": null - }, - "primary": { - "color": "#ebdbb2ff", - "font_style": null, - "font_weight": null - }, - "property": { - "color": "#ebdbb2ff", - "font_style": null, - "font_weight": null - }, - "punctuation": { - "color": "#d5c4a1ff", - "font_style": null, - "font_weight": null - }, - "punctuation.bracket": { - "color": "#a89984ff", - "font_style": null, - "font_weight": null - }, - "punctuation.delimiter": { - "color": "#e5d5adff", - "font_style": null, - "font_weight": null - }, - "punctuation.list_marker": { - "color": "#ebdbb2ff", - "font_style": null, - "font_weight": null - }, - "punctuation.markup": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "punctuation.special": { - "color": "#e5d5adff", - "font_style": null, - "font_weight": null - }, - "selector": { - "color": "#fabd2eff", - "font_style": null, - "font_weight": null - }, - "selector.pseudo": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "string": { - "color": "#b8bb25ff", - "font_style": null, - "font_weight": null - }, - "string.escape": { - "color": "#c6b697ff", - "font_style": null, - "font_weight": null - }, - "string.regex": { - "color": "#fe7f18ff", - "font_style": null, - "font_weight": null - }, - "string.special": { - "color": "#d3869bff", - "font_style": null, - "font_weight": null - }, - "string.special.symbol": { - "color": "#8ec07cff", - "font_style": null, - "font_weight": null - }, - "tag": { - "color": "#8ec07cff", - "font_style": null, - "font_weight": null - }, - "text.literal": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "title": { - "color": "#b8bb25ff", - "font_style": null, - "font_weight": 700 - }, - "type": { - "color": "#fabd2eff", - "font_style": null, - "font_weight": null - }, - "variable": { - "color": "#ebdbb2ff", - "font_style": null, - "font_weight": null - }, - "variable.special": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - }, - "variant": { - "color": "#83a598ff", - "font_style": null, - "font_weight": null - } - } - } - }, - { - "name": "Gruvbox Light", - "appearance": "light", - "style": { - "accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"], - "border": "#c8b899ff", - "border.variant": "#ddcca7ff", - "border.focused": "#adc5ccff", - "border.selected": "#adc5ccff", - "border.transparent": "#00000000", - "border.disabled": "#d0bf9dff", - "elevated_surface.background": "#ecddb4ff", - "surface.background": "#ecddb4ff", - "background": "#d9c8a4ff", - "element.background": "#ecddb4ff", - "element.hover": "#ddcca7ff", - "element.active": "#c8b899ff", - "element.selected": "#c8b899ff", - "element.disabled": "#ecddb4ff", - "drop_target.background": "#5f565080", - "ghost_element.background": "#00000000", - "ghost_element.hover": "#ddcca7ff", - "ghost_element.active": "#c8b899ff", - "ghost_element.selected": "#c8b899ff", - "ghost_element.disabled": "#ecddb4ff", - "text": "#282828ff", - "text.muted": "#5f5650ff", - "text.placeholder": "#897b6eff", - "text.disabled": "#897b6eff", - "text.accent": "#0b6678ff", - "icon": "#282828ff", - "icon.muted": "#5f5650ff", - "icon.disabled": "#897b6eff", - "icon.placeholder": "#5f5650ff", - "icon.accent": "#0b6678ff", - "status_bar.background": "#d9c8a4ff", - "title_bar.background": "#d9c8a4ff", - "title_bar.inactive_background": "#ecddb4ff", - "toolbar.background": "#fbf1c7ff", - "tab_bar.background": "#ecddb4ff", - "tab.inactive_background": "#ecddb4ff", - "tab.active_background": "#fbf1c7ff", - "search.match_background": "#0b667866", - "search.active_match_background": "#ba2d1166", - "panel.background": "#ecddb4ff", - "panel.focused_border": null, - "pane.focused_border": null, - "scrollbar.thumb.active_background": "#458588ac", - "scrollbar.thumb.hover_background": "#2828284c", - "scrollbar.thumb.background": "#7c6f644c", - "scrollbar.thumb.border": "#ddcca7ff", - "scrollbar.track.background": "#00000000", - "scrollbar.track.border": "#eee0b7ff", - "editor.foreground": "#282828ff", - "editor.background": "#fbf1c7ff", - "editor.gutter.background": "#fbf1c7ff", - "editor.subheader.background": "#ecddb4ff", - "editor.active_line.background": "#ecddb4bf", - "editor.highlighted_line.background": "#ecddb4ff", - "editor.line_number": "#a9a389", - "editor.active_line_number": "#3b382b", - "editor.hover_line_number": "#5e5a45", - "editor.invisible": "#928474ff", - "editor.wrap_guide": "#2828280d", - "editor.active_wrap_guide": "#2828281a", - "editor.document_highlight.read_background": "#0b66781a", - "editor.document_highlight.write_background": "#92847466", - "terminal.background": "#fbf1c7ff", - "terminal.foreground": "#282828ff", - "terminal.bright_foreground": "#282828ff", - "terminal.dim_foreground": "#fbf1c7ff", - "terminal.ansi.black": "#282828ff", - "terminal.ansi.bright_black": "#0b6678ff", - "terminal.ansi.dim_black": "#5f5650ff", - "terminal.ansi.red": "#9d0308ff", - "terminal.ansi.bright_red": "#db8b7aff", - "terminal.ansi.dim_red": "#4e1207ff", - "terminal.ansi.green": "#797410ff", - "terminal.ansi.bright_green": "#bfb787ff", - "terminal.ansi.dim_green": "#3e3a11ff", - "terminal.ansi.yellow": "#b57615ff", - "terminal.ansi.bright_yellow": "#e2b88bff", - "terminal.ansi.dim_yellow": "#5c3a12ff", - "terminal.ansi.blue": "#0b6678ff", - "terminal.ansi.bright_blue": "#8fb0baff", - "terminal.ansi.dim_blue": "#14333bff", - "terminal.ansi.magenta": "#8f3e71ff", - "terminal.ansi.bright_magenta": "#c76da0ff", - "terminal.ansi.dim_magenta": "#5c2848ff", - "terminal.ansi.cyan": "#437b59ff", - "terminal.ansi.bright_cyan": "#9fbca8ff", - "terminal.ansi.dim_cyan": "#253e2eff", - "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#ffffffff", - "terminal.ansi.dim_white": "#b0a189ff", - "link_text.hover": "#0b6678ff", - "version_control.added": "#797410ff", - "version_control.modified": "#b57615ff", - "version_control.deleted": "#9d0308ff", - "conflict": "#b57615ff", - "conflict.background": "#f5e2d0ff", - "conflict.border": "#ebccabff", - "created": "#797410ff", - "created.background": "#e4e0cdff", - "created.border": "#d1cba8ff", - "deleted": "#9d0308ff", - "deleted.background": "#f4d1c9ff", - "deleted.border": "#e8ac9eff", - "error": "#9d0308ff", - "error.background": "#f4d1c9ff", - "error.border": "#e8ac9eff", - "hidden": "#897b6eff", - "hidden.background": "#d9c8a4ff", - "hidden.border": "#d0bf9dff", - "hint": "#677562ff", - "hint.background": "#d2dee2ff", - "hint.border": "#adc5ccff", - "ignored": "#897b6eff", - "ignored.background": "#d9c8a4ff", - "ignored.border": "#c8b899ff", - "info": "#0b6678ff", - "info.background": "#d2dee2ff", - "info.border": "#adc5ccff", - "modified": "#b57615ff", - "modified.background": "#f5e2d0ff", - "modified.border": "#ebccabff", - "predictive": "#7c9780ff", - "predictive.background": "#e4e0cdff", - "predictive.border": "#d1cba8ff", - "renamed": "#0b6678ff", - "renamed.background": "#d2dee2ff", - "renamed.border": "#adc5ccff", - "success": "#797410ff", - "success.background": "#e4e0cdff", - "success.border": "#d1cba8ff", - "unreachable": "#5f5650ff", - "unreachable.background": "#d9c8a4ff", - "unreachable.border": "#c8b899ff", - "warning": "#b57615ff", - "warning.background": "#f5e2d0ff", - "warning.border": "#ebccabff", - "players": [ - { - "cursor": "#0b6678ff", - "background": "#0b6678ff", - "selection": "#0b66783d" - }, - { - "cursor": "#7c6f64ff", - "background": "#7c6f64ff", - "selection": "#7c6f643d" - }, - { - "cursor": "#af3a04ff", - "background": "#af3a04ff", - "selection": "#af3a043d" - }, - { - "cursor": "#8f3f70ff", - "background": "#8f3f70ff", - "selection": "#8f3f703d" - }, - { - "cursor": "#437b59ff", - "background": "#437b59ff", - "selection": "#437b593d" - }, - { - "cursor": "#9d0308ff", - "background": "#9d0308ff", - "selection": "#9d03083d" - }, - { - "cursor": "#b57615ff", - "background": "#b57615ff", - "selection": "#b576153d" - }, - { - "cursor": "#797410ff", - "background": "#797410ff", - "selection": "#7974103d" - } - ], - "syntax": { - "attribute": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": null - }, - "boolean": { - "color": "#8f3e71ff", - "font_style": null, - "font_weight": null - }, - "comment": { - "color": "#7c6f64ff", - "font_style": null, - "font_weight": null - }, - "comment.doc": { - "color": "#5d544eff", - "font_style": null, - "font_weight": null - }, - "constant": { - "color": "#b57613ff", - "font_style": null, - "font_weight": null - }, - "constructor": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": null - }, - "embedded": { - "color": "#427b58ff", - "font_style": null, - "font_weight": null - }, - "emphasis": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": null - }, - "emphasis.strong": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": 700 - }, - "enum": { - "color": "#af3a02ff", - "font_style": null, - "font_weight": null - }, - "function": { - "color": "#79740eff", - "font_style": null, - "font_weight": null - }, - "function.builtin": { - "color": "#9d0006ff", - "font_style": null, - "font_weight": null - }, - "hint": { - "color": "#677562ff", - "font_style": null, - "font_weight": null - }, - "keyword": { - "color": "#9d0006ff", - "font_style": null, - "font_weight": null - }, - "label": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": null - }, - "link_text": { - "color": "#427b58ff", - "font_style": "italic", - "font_weight": null - }, - "link_uri": { - "color": "#8f3e71ff", - "font_style": null, - "font_weight": null - }, - "namespace": { - "color": "#066578ff", - "font_style": null, - "font_weight": null - }, - "number": { - "color": "#8f3e71ff", - "font_style": null, - "font_weight": null - }, - "operator": { - "color": "#427b58ff", - "font_style": null, - "font_weight": null - }, - "predictive": { - "color": "#7c9780ff", - "font_style": "italic", - "font_weight": null - }, - "preproc": { - "color": "#282828ff", - "font_style": null, - "font_weight": null - }, - "primary": { - "color": "#282828ff", - "font_style": null, - "font_weight": null - }, - "property": { - "color": "#282828ff", - "font_style": null, - "font_weight": null - }, - "punctuation": { - "color": "#3c3836ff", - "font_style": null, - "font_weight": null - }, - "punctuation.bracket": { - "color": "#665c54ff", - "font_style": null, - "font_weight": null - }, - "punctuation.delimiter": { - "color": "#413d3aff", - "font_style": null, - "font_weight": null - }, - "punctuation.list_marker": { - "color": "#282828ff", - "font_style": null, - "font_weight": null - }, - "punctuation.markup": { - "color": "#066578ff", - "font_style": null, - "font_weight": null - }, - "punctuation.special": { - "color": "#413d3aff", - "font_style": null, - "font_weight": null - }, - "selector": { - "color": "#b57613ff", - "font_style": null, - "font_weight": null - }, - "selector.pseudo": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": null - }, - "string": { - "color": "#79740eff", - "font_style": null, - "font_weight": null - }, - "string.escape": { - "color": "#5d544eff", - "font_style": null, - "font_weight": null - }, - "string.regex": { - "color": "#af3a02ff", - "font_style": null, - "font_weight": null - }, - "string.special": { - "color": "#8f3e71ff", - "font_style": null, - "font_weight": null - }, - "string.special.symbol": { - "color": "#427b58ff", - "font_style": null, - "font_weight": null - }, - "tag": { - "color": "#427b58ff", - "font_style": null, - "font_weight": null - }, - "text.literal": { - "color": "#066578ff", - "font_style": null, - "font_weight": null - }, - "title": { - "color": "#79740eff", - "font_style": null, - "font_weight": 700 - }, - "type": { - "color": "#b57613ff", - "font_style": null, - "font_weight": null - }, - "variable": { - "color": "#282828ff", - "font_style": null, - "font_weight": null - }, - "variable.special": { - "color": "#066578ff", - "font_style": null, - "font_weight": null - }, - "variant": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": null - } - } - } - }, - { - "name": "Gruvbox Light Hard", - "appearance": "light", - "style": { - "accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"], - "border": "#c8b899ff", - "border.variant": "#ddcca7ff", - "border.focused": "#adc5ccff", - "border.selected": "#adc5ccff", - "border.transparent": "#00000000", - "border.disabled": "#d0bf9dff", - "elevated_surface.background": "#ecddb5ff", - "surface.background": "#ecddb5ff", - "background": "#d9c8a4ff", - "element.background": "#ecddb5ff", - "element.hover": "#ddcca7ff", - "element.active": "#c8b899ff", - "element.selected": "#c8b899ff", - "element.disabled": "#ecddb5ff", - "drop_target.background": "#5f565080", - "ghost_element.background": "#00000000", - "ghost_element.hover": "#ddcca7ff", - "ghost_element.active": "#c8b899ff", - "ghost_element.selected": "#c8b899ff", - "ghost_element.disabled": "#ecddb5ff", - "text": "#282828ff", - "text.muted": "#5f5650ff", - "text.placeholder": "#897b6eff", - "text.disabled": "#897b6eff", - "text.accent": "#0b6678ff", - "icon": "#282828ff", - "icon.muted": "#5f5650ff", - "icon.disabled": "#897b6eff", - "icon.placeholder": "#5f5650ff", - "icon.accent": "#0b6678ff", - "status_bar.background": "#d9c8a4ff", - "title_bar.background": "#d9c8a4ff", - "title_bar.inactive_background": "#ecddb5ff", - "toolbar.background": "#f9f5d7ff", - "tab_bar.background": "#ecddb5ff", - "tab.inactive_background": "#ecddb5ff", - "tab.active_background": "#f9f5d7ff", - "search.match_background": "#0b667866", - "search.active_match_background": "#dc351466", - "panel.background": "#ecddb5ff", - "panel.focused_border": null, - "pane.focused_border": null, - "scrollbar.thumb.active_background": "#458588ac", - "scrollbar.thumb.hover_background": "#2828284c", - "scrollbar.thumb.background": "#7c6f644c", - "scrollbar.thumb.border": "#ddcca7ff", - "scrollbar.track.background": "#00000000", - "scrollbar.track.border": "#eee1bbff", - "editor.foreground": "#282828ff", - "editor.background": "#f9f5d7ff", - "editor.gutter.background": "#f9f5d7ff", - "editor.subheader.background": "#ecddb5ff", - "editor.active_line.background": "#ecddb5bf", - "editor.highlighted_line.background": "#ecddb5ff", - "editor.line_number": "#a9a389", - "editor.active_line_number": "#3b382b", - "editor.hover_line_number": "#5e5a45", - "editor.invisible": "#928474ff", - "editor.wrap_guide": "#2828280d", - "editor.active_wrap_guide": "#2828281a", - "editor.document_highlight.read_background": "#0b66781a", - "editor.document_highlight.write_background": "#92847466", - "terminal.background": "#f9f5d7ff", - "terminal.foreground": "#282828ff", - "terminal.bright_foreground": "#282828ff", - "terminal.dim_foreground": "#f9f5d7ff", - "terminal.ansi.black": "#282828ff", - "terminal.ansi.bright_black": "#73675eff", - "terminal.ansi.dim_black": "#f9f5d7ff", - "terminal.ansi.red": "#9d0308ff", - "terminal.ansi.bright_red": "#db8b7aff", - "terminal.ansi.dim_red": "#4e1207ff", - "terminal.ansi.green": "#797410ff", - "terminal.ansi.bright_green": "#bfb787ff", - "terminal.ansi.dim_green": "#3e3a11ff", - "terminal.ansi.yellow": "#b57615ff", - "terminal.ansi.bright_yellow": "#e2b88bff", - "terminal.ansi.dim_yellow": "#5c3a12ff", - "terminal.ansi.blue": "#0b6678ff", - "terminal.ansi.bright_blue": "#8fb0baff", - "terminal.ansi.dim_blue": "#14333bff", - "terminal.ansi.magenta": "#8f3e71ff", - "terminal.ansi.bright_magenta": "#c76da0ff", - "terminal.ansi.dim_magenta": "#5c2848ff", - "terminal.ansi.cyan": "#437b59ff", - "terminal.ansi.bright_cyan": "#9fbca8ff", - "terminal.ansi.dim_cyan": "#253e2eff", - "terminal.ansi.white": "#f9f5d7ff", - "terminal.ansi.bright_white": "#ffffffff", - "terminal.ansi.dim_white": "#b0a189ff", - "link_text.hover": "#0b6678ff", - "version_control.added": "#797410ff", - "version_control.modified": "#b57615ff", - "version_control.deleted": "#9d0308ff", - "conflict": "#b57615ff", - "conflict.background": "#f5e2d0ff", - "conflict.border": "#ebccabff", - "created": "#797410ff", - "created.background": "#e4e0cdff", - "created.border": "#d1cba8ff", - "deleted": "#9d0308ff", - "deleted.background": "#f4d1c9ff", - "deleted.border": "#e8ac9eff", - "error": "#9d0308ff", - "error.background": "#f4d1c9ff", - "error.border": "#e8ac9eff", - "hidden": "#897b6eff", - "hidden.background": "#d9c8a4ff", - "hidden.border": "#d0bf9dff", - "hint": "#677562ff", - "hint.background": "#d2dee2ff", - "hint.border": "#adc5ccff", - "ignored": "#897b6eff", - "ignored.background": "#d9c8a4ff", - "ignored.border": "#c8b899ff", - "info": "#0b6678ff", - "info.background": "#d2dee2ff", - "info.border": "#adc5ccff", - "modified": "#b57615ff", - "modified.background": "#f5e2d0ff", - "modified.border": "#ebccabff", - "predictive": "#7c9780ff", - "predictive.background": "#e4e0cdff", - "predictive.border": "#d1cba8ff", - "renamed": "#0b6678ff", - "renamed.background": "#d2dee2ff", - "renamed.border": "#adc5ccff", - "success": "#797410ff", - "success.background": "#e4e0cdff", - "success.border": "#d1cba8ff", - "unreachable": "#5f5650ff", - "unreachable.background": "#d9c8a4ff", - "unreachable.border": "#c8b899ff", - "warning": "#b57615ff", - "warning.background": "#f5e2d0ff", - "warning.border": "#ebccabff", - "players": [ - { - "cursor": "#0b6678ff", - "background": "#0b6678ff", - "selection": "#0b66783d" - }, - { - "cursor": "#7c6f64ff", - "background": "#7c6f64ff", - "selection": "#7c6f643d" - }, - { - "cursor": "#af3a04ff", - "background": "#af3a04ff", - "selection": "#af3a043d" - }, - { - "cursor": "#8f3f70ff", - "background": "#8f3f70ff", - "selection": "#8f3f703d" - }, - { - "cursor": "#437b59ff", - "background": "#437b59ff", - "selection": "#437b593d" - }, - { - "cursor": "#9d0308ff", - "background": "#9d0308ff", - "selection": "#9d03083d" - }, - { - "cursor": "#b57615ff", - "background": "#b57615ff", - "selection": "#b576153d" - }, - { - "cursor": "#797410ff", - "background": "#797410ff", - "selection": "#7974103d" - } - ], - "syntax": { - "attribute": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": null - }, - "boolean": { - "color": "#8f3e71ff", - "font_style": null, - "font_weight": null - }, - "comment": { - "color": "#7c6f64ff", - "font_style": null, - "font_weight": null - }, - "comment.doc": { - "color": "#5d544eff", - "font_style": null, - "font_weight": null - }, - "constant": { - "color": "#b57613ff", - "font_style": null, - "font_weight": null - }, - "constructor": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": null - }, - "embedded": { - "color": "#427b58ff", - "font_style": null, - "font_weight": null - }, - "emphasis": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": null - }, - "emphasis.strong": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": 700 - }, - "enum": { - "color": "#af3a02ff", - "font_style": null, - "font_weight": null - }, - "function": { - "color": "#79740eff", - "font_style": null, - "font_weight": null - }, - "function.builtin": { - "color": "#9d0006ff", - "font_style": null, - "font_weight": null - }, - "hint": { - "color": "#677562ff", - "font_style": null, - "font_weight": null - }, - "keyword": { - "color": "#9d0006ff", - "font_style": null, - "font_weight": null - }, - "label": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": null - }, - "link_text": { - "color": "#427b58ff", - "font_style": "italic", - "font_weight": null - }, - "link_uri": { - "color": "#8f3e71ff", - "font_style": null, - "font_weight": null - }, - "namespace": { - "color": "#066578ff", - "font_style": null, - "font_weight": null - }, - "number": { - "color": "#8f3e71ff", - "font_style": null, - "font_weight": null - }, - "operator": { - "color": "#427b58ff", - "font_style": null, - "font_weight": null - }, - "predictive": { - "color": "#7c9780ff", - "font_style": "italic", - "font_weight": null - }, - "preproc": { - "color": "#282828ff", - "font_style": null, - "font_weight": null - }, - "primary": { - "color": "#282828ff", - "font_style": null, - "font_weight": null - }, - "property": { - "color": "#282828ff", - "font_style": null, - "font_weight": null - }, - "punctuation": { - "color": "#3c3836ff", - "font_style": null, - "font_weight": null - }, - "punctuation.bracket": { - "color": "#665c54ff", - "font_style": null, - "font_weight": null - }, - "punctuation.delimiter": { - "color": "#413d3aff", - "font_style": null, - "font_weight": null - }, - "punctuation.list_marker": { - "color": "#282828ff", - "font_style": null, - "font_weight": null - }, - "punctuation.markup": { - "color": "#066578ff", - "font_style": null, - "font_weight": null - }, - "punctuation.special": { - "color": "#413d3aff", - "font_style": null, - "font_weight": null - }, - "selector": { - "color": "#b57613ff", - "font_style": null, - "font_weight": null - }, - "selector.pseudo": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": null - }, - "string": { - "color": "#79740eff", - "font_style": null, - "font_weight": null - }, - "string.escape": { - "color": "#5d544eff", - "font_style": null, - "font_weight": null - }, - "string.regex": { - "color": "#af3a02ff", - "font_style": null, - "font_weight": null - }, - "string.special": { - "color": "#8f3e71ff", - "font_style": null, - "font_weight": null - }, - "string.special.symbol": { - "color": "#427b58ff", - "font_style": null, - "font_weight": null - }, - "tag": { - "color": "#427b58ff", - "font_style": null, - "font_weight": null - }, - "text.literal": { - "color": "#066578ff", - "font_style": null, - "font_weight": null - }, - "title": { - "color": "#79740eff", - "font_style": null, - "font_weight": 700 - }, - "type": { - "color": "#b57613ff", - "font_style": null, - "font_weight": null - }, - "variable": { - "color": "#282828ff", - "font_style": null, - "font_weight": null - }, - "variable.special": { - "color": "#066578ff", - "font_style": null, - "font_weight": null - }, - "variant": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": null - } - } - } - }, - { - "name": "Gruvbox Light Soft", - "appearance": "light", - "style": { - "accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"], - "border": "#c8b899ff", - "border.variant": "#ddcca7ff", - "border.focused": "#adc5ccff", - "border.selected": "#adc5ccff", - "border.transparent": "#00000000", - "border.disabled": "#d0bf9dff", - "elevated_surface.background": "#ecdcb3ff", - "surface.background": "#ecdcb3ff", - "background": "#d9c8a4ff", - "element.background": "#ecdcb3ff", - "element.hover": "#ddcca7ff", - "element.active": "#c8b899ff", - "element.selected": "#c8b899ff", - "element.disabled": "#ecdcb3ff", - "drop_target.background": "#5f565080", - "ghost_element.background": "#00000000", - "ghost_element.hover": "#ddcca7ff", - "ghost_element.active": "#c8b899ff", - "ghost_element.selected": "#c8b899ff", - "ghost_element.disabled": "#ecdcb3ff", - "text": "#282828ff", - "text.muted": "#5f5650ff", - "text.placeholder": "#897b6eff", - "text.disabled": "#897b6eff", - "text.accent": "#0b6678ff", - "icon": "#282828ff", - "icon.muted": "#5f5650ff", - "icon.disabled": "#897b6eff", - "icon.placeholder": "#5f5650ff", - "icon.accent": "#0b6678ff", - "status_bar.background": "#d9c8a4ff", - "title_bar.background": "#d9c8a4ff", - "title_bar.inactive_background": "#ecdcb3ff", - "toolbar.background": "#f2e5bcff", - "tab_bar.background": "#ecdcb3ff", - "tab.inactive_background": "#ecdcb3ff", - "tab.active_background": "#f2e5bcff", - "search.match_background": "#0b667866", - "search.active_match_background": "#d7331466", - "panel.background": "#ecdcb3ff", - "panel.focused_border": null, - "pane.focused_border": null, - "scrollbar.thumb.active_background": "#458588ac", - "scrollbar.thumb.hover_background": "#2828284c", - "scrollbar.thumb.background": "#7c6f644c", - "scrollbar.thumb.border": "#ddcca7ff", - "scrollbar.track.background": "#00000000", - "scrollbar.track.border": "#eddeb5ff", - "editor.foreground": "#282828ff", - "editor.background": "#f2e5bcff", - "editor.gutter.background": "#f2e5bcff", - "editor.subheader.background": "#ecdcb3ff", - "editor.active_line.background": "#ecdcb3bf", - "editor.highlighted_line.background": "#ecdcb3ff", - "editor.line_number": "#a9a389", - "editor.active_line_number": "#3b382b", - "editor.hover_line_number": "#5e5a45", - "editor.invisible": "#928474ff", - "editor.wrap_guide": "#2828280d", - "editor.active_wrap_guide": "#2828281a", - "editor.document_highlight.read_background": "#0b66781a", - "editor.document_highlight.write_background": "#92847466", - "terminal.background": "#f2e5bcff", - "terminal.foreground": "#282828ff", - "terminal.bright_foreground": "#282828ff", - "terminal.dim_foreground": "#f2e5bcff", - "terminal.ansi.black": "#282828ff", - "terminal.ansi.bright_black": "#73675eff", - "terminal.ansi.dim_black": "#f2e5bcff", - "terminal.ansi.red": "#9d0308ff", - "terminal.ansi.bright_red": "#db8b7aff", - "terminal.ansi.dim_red": "#4e1207ff", - "terminal.ansi.green": "#797410ff", - "terminal.ansi.bright_green": "#bfb787ff", - "terminal.ansi.dim_green": "#3e3a11ff", - "terminal.ansi.yellow": "#b57615ff", - "terminal.ansi.bright_yellow": "#e2b88bff", - "terminal.ansi.dim_yellow": "#5c3a12ff", - "terminal.ansi.blue": "#0b6678ff", - "terminal.ansi.bright_blue": "#8fb0baff", - "terminal.ansi.dim_blue": "#14333bff", - "terminal.ansi.magenta": "#8f3e71ff", - "terminal.ansi.bright_magenta": "#c76da0ff", - "terminal.ansi.dim_magenta": "#5c2848ff", - "terminal.ansi.cyan": "#437b59ff", - "terminal.ansi.bright_cyan": "#9fbca8ff", - "terminal.ansi.dim_cyan": "#253e2eff", - "terminal.ansi.white": "#f2e5bcff", - "terminal.ansi.bright_white": "#ffffffff", - "terminal.ansi.dim_white": "#b0a189ff", - "link_text.hover": "#0b6678ff", - "version_control.added": "#797410ff", - "version_control.modified": "#b57615ff", - "version_control.deleted": "#9d0308ff", - "conflict": "#b57615ff", - "conflict.background": "#f5e2d0ff", - "conflict.border": "#ebccabff", - "created": "#797410ff", - "created.background": "#e4e0cdff", - "created.border": "#d1cba8ff", - "deleted": "#9d0308ff", - "deleted.background": "#f4d1c9ff", - "deleted.border": "#e8ac9eff", - "error": "#9d0308ff", - "error.background": "#f4d1c9ff", - "error.border": "#e8ac9eff", - "hidden": "#897b6eff", - "hidden.background": "#d9c8a4ff", - "hidden.border": "#d0bf9dff", - "hint": "#677562ff", - "hint.background": "#d2dee2ff", - "hint.border": "#adc5ccff", - "ignored": "#897b6eff", - "ignored.background": "#d9c8a4ff", - "ignored.border": "#c8b899ff", - "info": "#0b6678ff", - "info.background": "#d2dee2ff", - "info.border": "#adc5ccff", - "modified": "#b57615ff", - "modified.background": "#f5e2d0ff", - "modified.border": "#ebccabff", - "predictive": "#7c9780ff", - "predictive.background": "#e4e0cdff", - "predictive.border": "#d1cba8ff", - "renamed": "#0b6678ff", - "renamed.background": "#d2dee2ff", - "renamed.border": "#adc5ccff", - "success": "#797410ff", - "success.background": "#e4e0cdff", - "success.border": "#d1cba8ff", - "unreachable": "#5f5650ff", - "unreachable.background": "#d9c8a4ff", - "unreachable.border": "#c8b899ff", - "warning": "#b57615ff", - "warning.background": "#f5e2d0ff", - "warning.border": "#ebccabff", - "players": [ - { - "cursor": "#0b6678ff", - "background": "#0b6678ff", - "selection": "#0b66783d" - }, - { - "cursor": "#7c6f64ff", - "background": "#7c6f64ff", - "selection": "#7c6f643d" - }, - { - "cursor": "#af3a04ff", - "background": "#af3a04ff", - "selection": "#af3a043d" - }, - { - "cursor": "#8f3f70ff", - "background": "#8f3f70ff", - "selection": "#8f3f703d" - }, - { - "cursor": "#437b59ff", - "background": "#437b59ff", - "selection": "#437b593d" - }, - { - "cursor": "#9d0308ff", - "background": "#9d0308ff", - "selection": "#9d03083d" - }, - { - "cursor": "#b57615ff", - "background": "#b57615ff", - "selection": "#b576153d" - }, - { - "cursor": "#797410ff", - "background": "#797410ff", - "selection": "#7974103d" - } - ], - "syntax": { - "attribute": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": null - }, - "boolean": { - "color": "#8f3e71ff", - "font_style": null, - "font_weight": null - }, - "comment": { - "color": "#7c6f64ff", - "font_style": null, - "font_weight": null - }, - "comment.doc": { - "color": "#5d544eff", - "font_style": null, - "font_weight": null - }, - "constant": { - "color": "#b57613ff", - "font_style": null, - "font_weight": null - }, - "constructor": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": null - }, - "embedded": { - "color": "#427b58ff", - "font_style": null, - "font_weight": null - }, - "emphasis": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": null - }, - "emphasis.strong": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": 700 - }, - "enum": { - "color": "#af3a02ff", - "font_style": null, - "font_weight": null - }, - "function": { - "color": "#79740eff", - "font_style": null, - "font_weight": null - }, - "function.builtin": { - "color": "#9d0006ff", - "font_style": null, - "font_weight": null - }, - "hint": { - "color": "#677562ff", - "font_style": null, - "font_weight": null - }, - "keyword": { - "color": "#9d0006ff", - "font_style": null, - "font_weight": null - }, - "label": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": null - }, - "link_text": { - "color": "#427b58ff", - "font_style": "italic", - "font_weight": null - }, - "link_uri": { - "color": "#8f3e71ff", - "font_style": null, - "font_weight": null - }, - "namespace": { - "color": "#066578ff", - "font_style": null, - "font_weight": null - }, - "number": { - "color": "#8f3e71ff", - "font_style": null, - "font_weight": null - }, - "operator": { - "color": "#427b58ff", - "font_style": null, - "font_weight": null - }, - "predictive": { - "color": "#7c9780ff", - "font_style": "italic", - "font_weight": null - }, - "preproc": { - "color": "#282828ff", - "font_style": null, - "font_weight": null - }, - "primary": { - "color": "#282828ff", - "font_style": null, - "font_weight": null - }, - "property": { - "color": "#282828ff", - "font_style": null, - "font_weight": null - }, - "punctuation": { - "color": "#3c3836ff", - "font_style": null, - "font_weight": null - }, - "punctuation.bracket": { - "color": "#665c54ff", - "font_style": null, - "font_weight": null - }, - "punctuation.delimiter": { - "color": "#413d3aff", - "font_style": null, - "font_weight": null - }, - "punctuation.list_marker": { - "color": "#282828ff", - "font_style": null, - "font_weight": null - }, - "punctuation.markup": { - "color": "#066578ff", - "font_style": null, - "font_weight": null - }, - "punctuation.special": { - "color": "#413d3aff", - "font_style": null, - "font_weight": null - }, - "selector": { - "color": "#b57613ff", - "font_style": null, - "font_weight": null - }, - "selector.pseudo": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": null - }, - "string": { - "color": "#79740eff", - "font_style": null, - "font_weight": null - }, - "string.escape": { - "color": "#5d544eff", - "font_style": null, - "font_weight": null - }, - "string.regex": { - "color": "#af3a02ff", - "font_style": null, - "font_weight": null - }, - "string.special": { - "color": "#8f3e71ff", - "font_style": null, - "font_weight": null - }, - "string.special.symbol": { - "color": "#427b58ff", - "font_style": null, - "font_weight": null - }, - "tag": { - "color": "#427b58ff", - "font_style": null, - "font_weight": null - }, - "text.literal": { - "color": "#066578ff", - "font_style": null, - "font_weight": null - }, - "title": { - "color": "#79740eff", - "font_style": null, - "font_weight": 700 - }, - "type": { - "color": "#b57613ff", - "font_style": null, - "font_weight": null - }, - "variable": { - "color": "#282828ff", - "font_style": null, - "font_weight": null - }, - "variable.special": { - "color": "#066578ff", - "font_style": null, - "font_weight": null - }, - "variant": { - "color": "#0b6678ff", - "font_style": null, - "font_weight": null - } - } - } - } - ] -} diff --git a/assets/themes/one/LICENSE b/assets/themes/one/LICENSE deleted file mode 100644 index f7637d33ea..0000000000 --- a/assets/themes/one/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 GitHub Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/assets/themes/one/one.json b/assets/themes/one/one.json deleted file mode 100644 index c72c924717..0000000000 --- a/assets/themes/one/one.json +++ /dev/null @@ -1,813 +0,0 @@ -{ - "$schema": "https://zed.dev/schema/themes/v0.2.0.json", - "name": "One", - "author": "Zed Industries", - "themes": [ - { - "name": "One Dark", - "appearance": "dark", - "style": { - "border": "#464b57ff", - "border.variant": "#363c46ff", - "border.focused": "#47679eff", - "border.selected": "#293b5bff", - "border.transparent": "#00000000", - "border.disabled": "#414754ff", - "elevated_surface.background": "#2f343eff", - "surface.background": "#2f343eff", - "background": "#3b414dff", - "element.background": "#2e343eff", - "element.hover": "#363c46ff", - "element.active": "#454a56ff", - "element.selected": "#454a56ff", - "element.disabled": "#2e343eff", - "drop_target.background": "#83899480", - "ghost_element.background": "#00000000", - "ghost_element.hover": "#363c46ff", - "ghost_element.active": "#454a56ff", - "ghost_element.selected": "#454a56ff", - "ghost_element.disabled": "#2e343eff", - "text": "#dce0e5ff", - "text.muted": "#a9afbcff", - "text.placeholder": "#878a98ff", - "text.disabled": "#878a98ff", - "text.accent": "#74ade8ff", - "icon": "#dce0e5ff", - "icon.muted": "#a9afbcff", - "icon.disabled": "#878a98ff", - "icon.placeholder": "#a9afbcff", - "icon.accent": "#74ade8ff", - "status_bar.background": "#3b414dff", - "title_bar.background": "#3b414dff", - "title_bar.inactive_background": "#2e343eff", - "toolbar.background": "#282c33ff", - "tab_bar.background": "#2f343eff", - "tab.inactive_background": "#2f343eff", - "tab.active_background": "#282c33ff", - "search.match_background": "#74ade866", - "search.active_match_background": "#e8af7466", - "panel.background": "#2f343eff", - "panel.focused_border": null, - "pane.focused_border": null, - "scrollbar.thumb.background": "#c8ccd44c", - "scrollbar.thumb.hover_background": "#363c46ff", - "scrollbar.thumb.border": "#363c46ff", - "scrollbar.track.background": "#00000000", - "scrollbar.track.border": "#2e333cff", - "editor.foreground": "#acb2beff", - "editor.background": "#282c33ff", - "editor.gutter.background": "#282c33ff", - "editor.subheader.background": "#2f343eff", - "editor.active_line.background": "#2f343ebf", - "editor.highlighted_line.background": "#2f343eff", - "editor.line_number": "#4e5a5f", - "editor.active_line_number": "#d0d4da", - "editor.hover_line_number": "#acb0b4", - "editor.invisible": "#878a98ff", - "editor.wrap_guide": "#c8ccd40d", - "editor.active_wrap_guide": "#c8ccd41a", - "editor.document_highlight.read_background": "#74ade81a", - "editor.document_highlight.write_background": "#555a6366", - "terminal.background": "#282c33ff", - "terminal.foreground": "#dce0e5ff", - "terminal.bright_foreground": "#dce0e5ff", - "terminal.dim_foreground": "#282c33ff", - "terminal.ansi.black": "#282c33ff", - "terminal.ansi.bright_black": "#525561ff", - "terminal.ansi.dim_black": "#dce0e5ff", - "terminal.ansi.red": "#d07277ff", - "terminal.ansi.bright_red": "#673a3cff", - "terminal.ansi.dim_red": "#eab7b9ff", - "terminal.ansi.green": "#a1c181ff", - "terminal.ansi.bright_green": "#4d6140ff", - "terminal.ansi.dim_green": "#d1e0bfff", - "terminal.ansi.yellow": "#dec184ff", - "terminal.ansi.bright_yellow": "#e5c07bff", - "terminal.ansi.dim_yellow": "#f1dfc1ff", - "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.cyan": "#6eb4bfff", - "terminal.ansi.bright_cyan": "#3a565bff", - "terminal.ansi.dim_cyan": "#b9d9dfff", - "terminal.ansi.white": "#dce0e5ff", - "terminal.ansi.bright_white": "#fafafaff", - "terminal.ansi.dim_white": "#575d65ff", - "link_text.hover": "#74ade8ff", - "version_control.added": "#27a657ff", - "version_control.modified": "#d3b020ff", - "version_control.word_added": "#2EA04859", - "version_control.word_deleted": "#78081BCC", - "version_control.deleted": "#e06c76ff", - "version_control.conflict_marker.ours": "#a1c1811a", - "version_control.conflict_marker.theirs": "#74ade81a", - "conflict": "#dec184ff", - "conflict.background": "#dec1841a", - "conflict.border": "#5d4c2fff", - "created": "#a1c181ff", - "created.background": "#a1c1811a", - "created.border": "#38482fff", - "deleted": "#d07277ff", - "deleted.background": "#d072771a", - "deleted.border": "#4c2b2cff", - "error": "#d07277ff", - "error.background": "#d072771a", - "error.border": "#4c2b2cff", - "hidden": "#878a98ff", - "hidden.background": "#696b771a", - "hidden.border": "#414754ff", - "hint": "#788ca6ff", - "hint.background": "#5a6f891a", - "hint.border": "#293b5bff", - "ignored": "#878a98ff", - "ignored.background": "#696b771a", - "ignored.border": "#464b57ff", - "info": "#74ade8ff", - "info.background": "#74ade81a", - "info.border": "#293b5bff", - "modified": "#dec184ff", - "modified.background": "#dec1841a", - "modified.border": "#5d4c2fff", - "predictive": "#5a6a87ff", - "predictive.background": "#5a6a871a", - "predictive.border": "#38482fff", - "renamed": "#74ade8ff", - "renamed.background": "#74ade81a", - "renamed.border": "#293b5bff", - "success": "#a1c181ff", - "success.background": "#a1c1811a", - "success.border": "#38482fff", - "unreachable": "#a9afbcff", - "unreachable.background": "#8389941a", - "unreachable.border": "#464b57ff", - "warning": "#dec184ff", - "warning.background": "#dec1841a", - "warning.border": "#5d4c2fff", - "players": [ - { - "cursor": "#74ade8ff", - "background": "#74ade8ff", - "selection": "#74ade83d" - }, - { - "cursor": "#be5046ff", - "background": "#be5046ff", - "selection": "#be50463d" - }, - { - "cursor": "#bf956aff", - "background": "#bf956aff", - "selection": "#bf956a3d" - }, - { - "cursor": "#b477cfff", - "background": "#b477cfff", - "selection": "#b477cf3d" - }, - { - "cursor": "#6eb4bfff", - "background": "#6eb4bfff", - "selection": "#6eb4bf3d" - }, - { - "cursor": "#d07277ff", - "background": "#d07277ff", - "selection": "#d072773d" - }, - { - "cursor": "#dec184ff", - "background": "#dec184ff", - "selection": "#dec1843d" - }, - { - "cursor": "#a1c181ff", - "background": "#a1c181ff", - "selection": "#a1c1813d" - } - ], - "syntax": { - "attribute": { - "color": "#74ade8ff", - "font_style": null, - "font_weight": null - }, - "boolean": { - "color": "#bf956aff", - "font_style": null, - "font_weight": null - }, - "comment": { - "color": "#5d636fff", - "font_style": null, - "font_weight": null - }, - "comment.doc": { - "color": "#878e98ff", - "font_style": null, - "font_weight": null - }, - "constant": { - "color": "#dfc184ff", - "font_style": null, - "font_weight": null - }, - "constructor": { - "color": "#73ade9ff", - "font_style": null, - "font_weight": null - }, - "embedded": { - "color": "#dce0e5ff", - "font_style": null, - "font_weight": null - }, - "emphasis": { - "color": "#74ade8ff", - "font_style": null, - "font_weight": null - }, - "emphasis.strong": { - "color": "#bf956aff", - "font_style": null, - "font_weight": 700 - }, - "enum": { - "color": "#d07277ff", - "font_style": null, - "font_weight": null - }, - "function": { - "color": "#73ade9ff", - "font_style": null, - "font_weight": null - }, - "hint": { - "color": "#788ca6ff", - "font_style": null, - "font_weight": null - }, - "keyword": { - "color": "#b477cfff", - "font_style": null, - "font_weight": null - }, - "label": { - "color": "#74ade8ff", - "font_style": null, - "font_weight": null - }, - "link_text": { - "color": "#73ade9ff", - "font_style": "normal", - "font_weight": null - }, - "link_uri": { - "color": "#6eb4bfff", - "font_style": null, - "font_weight": null - }, - "namespace": { - "color": "#dce0e5ff", - "font_style": null, - "font_weight": null - }, - "number": { - "color": "#bf956aff", - "font_style": null, - "font_weight": null - }, - "operator": { - "color": "#6eb4bfff", - "font_style": null, - "font_weight": null - }, - "predictive": { - "color": "#5a6a87ff", - "font_style": "italic", - "font_weight": null - }, - "preproc": { - "color": "#dce0e5ff", - "font_style": null, - "font_weight": null - }, - "primary": { - "color": "#acb2beff", - "font_style": null, - "font_weight": null - }, - "property": { - "color": "#d07277ff", - "font_style": null, - "font_weight": null - }, - "punctuation": { - "color": "#acb2beff", - "font_style": null, - "font_weight": null - }, - "punctuation.bracket": { - "color": "#b2b9c6ff", - "font_style": null, - "font_weight": null - }, - "punctuation.delimiter": { - "color": "#b2b9c6ff", - "font_style": null, - "font_weight": null - }, - "punctuation.list_marker": { - "color": "#d07277ff", - "font_style": null, - "font_weight": null - }, - "punctuation.markup": { - "color": "#d07277ff", - "font_style": null, - "font_weight": null - }, - "punctuation.special": { - "color": "#b1574bff", - "font_style": null, - "font_weight": null - }, - "selector": { - "color": "#dfc184ff", - "font_style": null, - "font_weight": null - }, - "selector.pseudo": { - "color": "#74ade8ff", - "font_style": null, - "font_weight": null - }, - "string": { - "color": "#a1c181ff", - "font_style": null, - "font_weight": null - }, - "string.escape": { - "color": "#878e98ff", - "font_style": null, - "font_weight": null - }, - "string.regex": { - "color": "#bf956aff", - "font_style": null, - "font_weight": null - }, - "string.special": { - "color": "#bf956aff", - "font_style": null, - "font_weight": null - }, - "string.special.symbol": { - "color": "#bf956aff", - "font_style": null, - "font_weight": null - }, - "tag": { - "color": "#74ade8ff", - "font_style": null, - "font_weight": null - }, - "text.literal": { - "color": "#a1c181ff", - "font_style": null, - "font_weight": null - }, - "title": { - "color": "#d07277ff", - "font_style": null, - "font_weight": 400 - }, - "type": { - "color": "#6eb4bfff", - "font_style": null, - "font_weight": null - }, - "variable": { - "color": "#acb2beff", - "font_style": null, - "font_weight": null - }, - "variable.special": { - "color": "#bf956aff", - "font_style": null, - "font_weight": null - }, - "variant": { - "color": "#73ade9ff", - "font_style": null, - "font_weight": null - } - } - } - }, - { - "name": "One Light", - "appearance": "light", - "style": { - "border": "#c9c9caff", - "border.variant": "#dfdfe0ff", - "border.focused": "#7d82e8ff", - "border.selected": "#cbcdf6ff", - "border.transparent": "#00000000", - "border.disabled": "#d3d3d4ff", - "elevated_surface.background": "#ebebecff", - "surface.background": "#ebebecff", - "background": "#dcdcddff", - "element.background": "#ebebecff", - "element.hover": "#dfdfe0ff", - "element.active": "#cacacaff", - "element.selected": "#cacacaff", - "element.disabled": "#ebebecff", - "drop_target.background": "#7e808780", - "ghost_element.background": "#00000000", - "ghost_element.hover": "#dfdfe0ff", - "ghost_element.active": "#cacacaff", - "ghost_element.selected": "#cacacaff", - "ghost_element.disabled": "#ebebecff", - "text": "#242529ff", - "text.muted": "#58585aff", - "text.placeholder": "#7e8086ff", - "text.disabled": "#7e8086ff", - "text.accent": "#5c78e2ff", - "icon": "#242529ff", - "icon.muted": "#58585aff", - "icon.disabled": "#7e8086ff", - "icon.placeholder": "#58585aff", - "icon.accent": "#5c78e2ff", - "status_bar.background": "#dcdcddff", - "title_bar.background": "#dcdcddff", - "title_bar.inactive_background": "#ebebecff", - "toolbar.background": "#fafafaff", - "tab_bar.background": "#ebebecff", - "tab.inactive_background": "#ebebecff", - "tab.active_background": "#fafafaff", - "search.match_background": "#5c79e266", - "search.active_match_background": "#d0a92366", - "panel.background": "#ebebecff", - "panel.focused_border": null, - "pane.focused_border": null, - "scrollbar.thumb.background": "#383a414c", - "scrollbar.thumb.hover_background": "#dfdfe0ff", - "scrollbar.thumb.border": "#dfdfe0ff", - "scrollbar.track.background": "#00000000", - "scrollbar.track.border": "#eeeeeeff", - "editor.foreground": "#242529ff", - "editor.background": "#fafafaff", - "editor.gutter.background": "#fafafaff", - "editor.subheader.background": "#ebebecff", - "editor.active_line.background": "#ebebecbf", - "editor.highlighted_line.background": "#ebebecff", - "editor.line_number": "#b4b4bb", - "editor.active_line_number": "#44454b", - "editor.hover_line_number": "#61616b", - "editor.invisible": "#a3a3a4ff", - "editor.wrap_guide": "#383a410d", - "editor.active_wrap_guide": "#383a411a", - "editor.document_highlight.read_background": "#5c78e225", - "editor.document_highlight.write_background": "#a3a3a466", - "terminal.background": "#fafafaff", - "terminal.foreground": "#242529ff", - "terminal.bright_foreground": "#242529ff", - "terminal.dim_foreground": "#fafafaff", - "terminal.ansi.black": "#242529ff", - "terminal.ansi.bright_black": "#747579ff", - "terminal.ansi.dim_black": "#97979aff", - "terminal.ansi.red": "#d36151ff", - "terminal.ansi.bright_red": "#f0b0a4ff", - "terminal.ansi.dim_red": "#6f312aff", - "terminal.ansi.green": "#669f59ff", - "terminal.ansi.bright_green": "#b2cfa9ff", - "terminal.ansi.dim_green": "#354d2eff", - "terminal.ansi.yellow": "#dec184ff", - "terminal.ansi.bright_yellow": "#826221ff", - "terminal.ansi.dim_yellow": "#786441ff", - "terminal.ansi.blue": "#5c78e2ff", - "terminal.ansi.bright_blue": "#b5baf2ff", - "terminal.ansi.dim_blue": "#2d3d75ff", - "terminal.ansi.magenta": "#984ea5ff", - "terminal.ansi.bright_magenta": "#cea6d3ff", - "terminal.ansi.dim_magenta": "#4b2a50ff", - "terminal.ansi.cyan": "#3a82b7ff", - "terminal.ansi.bright_cyan": "#a3bedaff", - "terminal.ansi.dim_cyan": "#254058ff", - "terminal.ansi.white": "#fafafaff", - "terminal.ansi.bright_white": "#ffffffff", - "terminal.ansi.dim_white": "#aaaaaaff", - "link_text.hover": "#5c78e2ff", - "version_control.added": "#27a657ff", - "version_control.modified": "#d3b020ff", - "version_control.word_added": "#2EA04859", - "version_control.word_deleted": "#F85149CC", - "version_control.deleted": "#e06c76ff", - "conflict": "#a48819ff", - "conflict.background": "#faf2e6ff", - "conflict.border": "#f4e7d1ff", - "created": "#669f59ff", - "created.background": "#dfeadbff", - "created.border": "#c8dcc1ff", - "deleted": "#d36151ff", - "deleted.background": "#fbdfd9ff", - "deleted.border": "#f6c6bdff", - "error": "#d36151ff", - "error.background": "#fbdfd9ff", - "error.border": "#f6c6bdff", - "hidden": "#7e8086ff", - "hidden.background": "#dcdcddff", - "hidden.border": "#d3d3d4ff", - "hint": "#7274a7ff", - "hint.background": "#e2e2faff", - "hint.border": "#cbcdf6ff", - "ignored": "#7e8086ff", - "ignored.background": "#dcdcddff", - "ignored.border": "#c9c9caff", - "info": "#5c78e2ff", - "info.background": "#e2e2faff", - "info.border": "#cbcdf6ff", - "modified": "#a48819ff", - "modified.background": "#faf2e6ff", - "modified.border": "#f4e7d1ff", - "predictive": "#9b9ec6ff", - "predictive.background": "#dfeadbff", - "predictive.border": "#c8dcc1ff", - "renamed": "#5c78e2ff", - "renamed.background": "#e2e2faff", - "renamed.border": "#cbcdf6ff", - "success": "#669f59ff", - "success.background": "#dfeadbff", - "success.border": "#c8dcc1ff", - "unreachable": "#58585aff", - "unreachable.background": "#dcdcddff", - "unreachable.border": "#c9c9caff", - "warning": "#a48819ff", - "warning.background": "#faf2e6ff", - "warning.border": "#f4e7d1ff", - "players": [ - { - "cursor": "#5c78e2ff", - "background": "#5c78e2ff", - "selection": "#5c78e23d" - }, - { - "cursor": "#984ea5ff", - "background": "#984ea5ff", - "selection": "#984ea53d" - }, - { - "cursor": "#ad6e26ff", - "background": "#ad6e26ff", - "selection": "#ad6e263d" - }, - { - "cursor": "#a349abff", - "background": "#a349abff", - "selection": "#a349ab3d" - }, - { - "cursor": "#3a82b7ff", - "background": "#3a82b7ff", - "selection": "#3a82b73d" - }, - { - "cursor": "#d36151ff", - "background": "#d36151ff", - "selection": "#d361513d" - }, - { - "cursor": "#a48819ff", - "background": "#dec184ff", - "selection": "#dec1843d" - }, - { - "cursor": "#669f59ff", - "background": "#669f59ff", - "selection": "#669f593d" - } - ], - "syntax": { - "attribute": { - "color": "#5c78e2ff", - "font_style": null, - "font_weight": null - }, - "boolean": { - "color": "#ad6e25ff", - "font_style": null, - "font_weight": null - }, - "comment": { - "color": "#a2a3a7ff", - "font_style": null, - "font_weight": null - }, - "comment.doc": { - "color": "#7c7e86ff", - "font_style": null, - "font_weight": null - }, - "constant": { - "color": "#c18401ff", - "font_style": null, - "font_weight": null - }, - "constructor": { - "color": "#5c78e2ff", - "font_style": null, - "font_weight": null - }, - "embedded": { - "color": "#242529ff", - "font_style": null, - "font_weight": null - }, - "emphasis": { - "color": "#5c78e2ff", - "font_style": null, - "font_weight": null - }, - "emphasis.strong": { - "color": "#ad6e25ff", - "font_style": null, - "font_weight": 700 - }, - "enum": { - "color": "#d3604fff", - "font_style": null, - "font_weight": null - }, - "function": { - "color": "#5b79e3ff", - "font_style": null, - "font_weight": null - }, - "hint": { - "color": "#7274a7ff", - "font_style": null, - "font_weight": null - }, - "keyword": { - "color": "#a449abff", - "font_style": null, - "font_weight": null - }, - "label": { - "color": "#5c78e2ff", - "font_style": null, - "font_weight": null - }, - "link_text": { - "color": "#5b79e3ff", - "font_style": "italic", - "font_weight": null - }, - "link_uri": { - "color": "#3882b7ff", - "font_style": null, - "font_weight": null - }, - "namespace": { - "color": "#242529ff", - "font_style": null, - "font_weight": null - }, - "number": { - "color": "#ad6e25ff", - "font_style": null, - "font_weight": null - }, - "operator": { - "color": "#3882b7ff", - "font_style": null, - "font_weight": null - }, - "predictive": { - "color": "#9b9ec6ff", - "font_style": "italic", - "font_weight": null - }, - "preproc": { - "color": "#242529ff", - "font_style": null, - "font_weight": null - }, - "primary": { - "color": "#242529ff", - "font_style": null, - "font_weight": null - }, - "property": { - "color": "#d3604fff", - "font_style": null, - "font_weight": null - }, - "punctuation": { - "color": "#242529ff", - "font_style": null, - "font_weight": null - }, - "punctuation.bracket": { - "color": "#4d4f52ff", - "font_style": null, - "font_weight": null - }, - "punctuation.delimiter": { - "color": "#4d4f52ff", - "font_style": null, - "font_weight": null - }, - "punctuation.list_marker": { - "color": "#d3604fff", - "font_style": null, - "font_weight": null - }, - "punctuation.markup": { - "color": "#d3604fff", - "font_style": null, - "font_weight": null - }, - "punctuation.special": { - "color": "#b92b46ff", - "font_style": null, - "font_weight": null - }, - "selector": { - "color": "#669f59ff", - "font_style": null, - "font_weight": null - }, - "selector.pseudo": { - "color": "#5c78e2ff", - "font_style": null, - "font_weight": null - }, - "string": { - "color": "#649f57ff", - "font_style": null, - "font_weight": null - }, - "string.escape": { - "color": "#7c7e86ff", - "font_style": null, - "font_weight": null - }, - "string.regex": { - "color": "#ad6e26ff", - "font_style": null, - "font_weight": null - }, - "string.special": { - "color": "#ad6e26ff", - "font_style": null, - "font_weight": null - }, - "string.special.symbol": { - "color": "#ad6e26ff", - "font_style": null, - "font_weight": null - }, - "tag": { - "color": "#5c78e2ff", - "font_style": null, - "font_weight": null - }, - "text.literal": { - "color": "#649f57ff", - "font_style": null, - "font_weight": null - }, - "title": { - "color": "#d3604fff", - "font_style": null, - "font_weight": 400 - }, - "type": { - "color": "#3882b7ff", - "font_style": null, - "font_weight": null - }, - "variable": { - "color": "#242529ff", - "font_style": null, - "font_weight": null - }, - "variable.special": { - "color": "#ad6e25ff", - "font_style": null, - "font_weight": null - }, - "variant": { - "color": "#5b79e3ff", - "font_style": null, - "font_weight": null - } - } - } - } - ] -} diff --git a/ci/Dockerfile.namespace b/ci/Dockerfile.namespace deleted file mode 100644 index f370dae194..0000000000 --- a/ci/Dockerfile.namespace +++ /dev/null @@ -1,21 +0,0 @@ -ARG NAMESPACE_BASE_IMAGE_REF="" - -# Your image must build FROM NAMESPACE_BASE_IMAGE_REF -FROM ${NAMESPACE_BASE_IMAGE_REF} AS base - -# Remove problematic git-lfs packagecloud source -RUN sudo rm -f /etc/apt/sources.list.d/*git-lfs*.list -# Install git and SSH for cloning private repositories -RUN sudo apt-get update && \ - sudo apt-get install -y git openssh-client - -# Clone the Zed repository -RUN git clone https://github.com/zed-industries/zed.git ~/zed - -# Run the Linux installation script -WORKDIR /home/runner/zed -RUN ./script/linux - -# Clean up unnecessary files to reduce image size -RUN sudo apt-get clean && sudo rm -rf \ - /home/runner/zed diff --git a/compose.yml b/compose.yml deleted file mode 100644 index cee63e968b..0000000000 --- a/compose.yml +++ /dev/null @@ -1,43 +0,0 @@ -services: - postgres: - image: docker.io/library/postgres:15 - container_name: zed_postgres - ports: - - 5432:5432 - environment: - POSTGRES_HOST_AUTH_METHOD: trust - volumes: - - postgres_data:/var/lib/postgresql/data - - ./docker-compose.sql:/docker-entrypoint-initdb.d/init.sql - - blob_store: - image: quay.io/minio/minio - container_name: blob_store - command: server /data - ports: - - 9000:9000 - environment: - MINIO_ROOT_USER: the-blob-store-access-key - MINIO_ROOT_PASSWORD: the-blob-store-secret-key - volumes: - - ./.blob_store:/data - - livekit_server: - image: docker.io/livekit/livekit-server - container_name: livekit_server - entrypoint: /livekit-server --config /livekit.yaml - ports: - - 7880:7880 - - 7881:7881 - - 7882:7882/udp - volumes: - - ./livekit.yaml:/livekit.yaml - - stripe-mock: - image: docker.io/stripe/stripe-mock:v0.178.0 - ports: - - 12111:12111 - - 12112:12112 - -volumes: - postgres_data: diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml deleted file mode 100644 index 8ef6f1a52c..0000000000 --- a/crates/acp_thread/Cargo.toml +++ /dev/null @@ -1,60 +0,0 @@ -[package] -name = "acp_thread" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/acp_thread.rs" -doctest = false - -[features] -test-support = ["gpui/test-support", "project/test-support", "dep:parking_lot"] - -[dependencies] -action_log.workspace = true -agent-client-protocol.workspace = true -agent_settings.workspace = true -anyhow.workspace = true -buffer_diff.workspace = true -collections.workspace = true -editor.workspace = true -file_icons.workspace = true -futures.workspace = true -gpui.workspace = true -itertools.workspace = true -language.workspace = true -language_model.workspace = true -markdown.workspace = true -parking_lot = { workspace = true, optional = true } -portable-pty.workspace = true -project.workspace = true -prompt_store.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smol.workspace = true -task.workspace = true -telemetry.workspace = true -terminal.workspace = true -ui.workspace = true -url.workspace = true -util.workspace = true -uuid.workspace = true -watch.workspace = true - -[dev-dependencies] -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 -zlog.workspace = true diff --git a/crates/acp_thread/LICENSE-GPL b/crates/acp_thread/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/acp_thread/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs deleted file mode 100644 index 53294a963d..0000000000 --- a/crates/acp_thread/src/acp_thread.rs +++ /dev/null @@ -1,4021 +0,0 @@ -mod connection; -mod diff; -mod mention; -mod terminal; - -use agent_settings::AgentSettings; -use collections::HashSet; -pub use connection::*; -pub use diff::*; -use language::language_settings::FormatOnSave; -pub use mention::*; -use project::lsp_store::{FormatTrigger, LspFormatTarget}; -use serde::{Deserialize, Serialize}; -use settings::Settings as _; -use task::{Shell, ShellBuilder}; -pub use terminal::*; - -use action_log::{ActionLog, ActionLogTelemetry}; -use agent_client_protocol::{self as acp}; -use anyhow::{Context as _, Result, anyhow}; -use editor::Bias; -use futures::{FutureExt, channel::oneshot, future::BoxFuture}; -use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity}; -use itertools::Itertools; -use language::{Anchor, Buffer, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff}; -use markdown::Markdown; -use project::{AgentLocation, Project, git_store::GitStoreCheckpoint}; -use std::collections::HashMap; -use std::error::Error; -use std::fmt::{Formatter, Write}; -use std::ops::Range; -use std::process::ExitStatus; -use std::rc::Rc; -use std::time::{Duration, Instant}; -use std::{fmt::Display, mem, path::PathBuf, sync::Arc}; -use ui::App; -use util::{ResultExt, get_default_system_shell_preferring_bash, paths::PathStyle}; -use uuid::Uuid; - -#[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(); - } - writeln!(markdown).unwrap(); - writeln!(markdown, "{}", self.content.to_markdown(cx)).unwrap(); - writeln!(markdown).unwrap(); - markdown - } -} - -#[derive(Debug, PartialEq)] -pub struct AssistantMessage { - pub chunks: Vec, -} - -impl AssistantMessage { - pub fn to_markdown(&self, cx: &App) -> String { - format!( - "## Assistant\n\n{}\n\n", - self.chunks - .iter() - .map(|chunk| chunk.to_markdown(cx)) - .join("\n\n") - ) - } -} - -#[derive(Debug, PartialEq)] -pub enum AssistantMessageChunk { - Message { block: ContentBlock }, - Thought { block: ContentBlock }, -} - -impl AssistantMessageChunk { - pub fn from_str( - chunk: &str, - language_registry: &Arc, - path_style: PathStyle, - cx: &mut App, - ) -> Self { - Self::Message { - block: ContentBlock::new(chunk.into(), language_registry, path_style, cx), - } - } - - fn to_markdown(&self, cx: &App) -> String { - match self { - Self::Message { block } => block.to_markdown(cx).to_string(), - Self::Thought { block } => { - format!("\n{}\n", block.to_markdown(cx)) - } - } - } -} - -#[derive(Debug)] -pub enum AgentThreadEntry { - UserMessage(UserMessage), - AssistantMessage(AssistantMessage), - ToolCall(ToolCall), -} - -impl AgentThreadEntry { - pub fn to_markdown(&self, cx: &App) -> String { - match self { - Self::UserMessage(message) => message.to_markdown(cx), - Self::AssistantMessage(message) => message.to_markdown(cx), - Self::ToolCall(tool_call) => tool_call.to_markdown(cx), - } - } - - pub fn user_message(&self) -> Option<&UserMessage> { - if let AgentThreadEntry::UserMessage(message) = self { - Some(message) - } else { - None - } - } - - pub fn diffs(&self) -> impl Iterator> { - if let AgentThreadEntry::ToolCall(call) = self { - itertools::Either::Left(call.diffs()) - } else { - itertools::Either::Right(std::iter::empty()) - } - } - - 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()?, - )) - } else { - None - } - } -} - -#[derive(Debug)] -pub struct ToolCall { - pub id: acp::ToolCallId, - pub label: Entity, - pub kind: acp::ToolKind, - pub content: Vec, - pub status: ToolCallStatus, - pub locations: Vec, - pub resolved_locations: Vec>, - pub raw_input: Option, - pub raw_output: Option, -} - -impl ToolCall { - fn from_acp( - tool_call: acp::ToolCall, - status: ToolCallStatus, - language_registry: Arc, - path_style: PathStyle, - terminals: &HashMap>, - cx: &mut App, - ) -> Result { - let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") { - first_line.to_owned() + "…" - } else { - tool_call.title - }; - let mut content = Vec::with_capacity(tool_call.content.len()); - for item in tool_call.content { - if let Some(item) = ToolCallContent::from_acp( - item, - language_registry.clone(), - path_style, - terminals, - cx, - )? { - content.push(item); - } - } - - let result = Self { - id: tool_call.tool_call_id, - label: cx - .new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)), - kind: tool_call.kind, - content, - locations: tool_call.locations, - resolved_locations: Vec::default(), - status, - raw_input: tool_call.raw_input, - raw_output: tool_call.raw_output, - }; - Ok(result) - } - - fn update_fields( - &mut self, - fields: acp::ToolCallUpdateFields, - language_registry: Arc, - path_style: PathStyle, - terminals: &HashMap>, - cx: &mut App, - ) -> Result<()> { - let acp::ToolCallUpdateFields { - kind, - status, - title, - content, - locations, - raw_input, - raw_output, - .. - } = fields; - - if let Some(kind) = kind { - self.kind = kind; - } - - if let Some(status) = status { - self.status = status.into(); - } - - 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(content) = content { - let mut 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()) { - let valid_content = - old.update_from_acp(new, language_registry.clone(), path_style, terminals, cx)?; - if !valid_content { - new_content_len -= 1; - } - } - for new in content { - if let Some(new) = ToolCallContent::from_acp( - new, - language_registry.clone(), - path_style, - terminals, - cx, - )? { - self.content.push(new); - } else { - new_content_len -= 1; - } - } - self.content.truncate(new_content_len); - } - - if let Some(locations) = locations { - self.locations = locations; - } - - 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); - } - Ok(()) - } - - 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, - }) - } - - fn to_markdown(&self, cx: &App) -> String { - let mut markdown = format!( - "**Tool Call: {}**\nStatus: {}\n\n", - self.label.read(cx).source(), - self.status - ); - for content in &self.content { - markdown.push_str(content.to_markdown(cx).as_str()); - markdown.push_str("\n\n"); - } - 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, _| { - let snapshot = buffer.snapshot(); - if let Some(row) = location.line { - 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_for_buffer(snapshot.remote_id()) - } - }) - .ok()?; - - Some(ResolvedLocation { buffer, 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 - }) - }) - } -} - -// Separate so we can hold a strong reference to the buffer -// for saving on the thread -#[derive(Clone, Debug, PartialEq, Eq)] -struct ResolvedLocation { - buffer: Entity, - position: Anchor, -} - -impl From<&ResolvedLocation> for AgentLocation { - fn from(value: &ResolvedLocation) -> Self { - Self { - buffer: value.buffer.downgrade(), - position: value.position, - } - } -} - -#[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. - 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, - _ => Self::Pending, - } - } -} - -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::Rejected => "Rejected", - ToolCallStatus::Canceled => "Canceled", - } - ) - } -} - -#[derive(Debug, PartialEq, Clone)] -pub enum ContentBlock { - Empty, - Markdown { markdown: Entity }, - ResourceLink { resource_link: acp::ResourceLink }, -} - -impl ContentBlock { - pub fn new( - block: acp::ContentBlock, - language_registry: &Arc, - path_style: PathStyle, - cx: &mut App, - ) -> Self { - let mut this = Self::Empty; - this.append(block, language_registry, path_style, cx); - this - } - - pub fn new_combined( - blocks: impl IntoIterator, - language_registry: Arc, - path_style: PathStyle, - cx: &mut App, - ) -> Self { - let mut this = Self::Empty; - for block in blocks { - this.append(block, &language_registry, path_style, cx); - } - this - } - - pub fn append( - &mut self, - block: acp::ContentBlock, - language_registry: &Arc, - path_style: PathStyle, - 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, path_style); - - match self { - ContentBlock::Empty => { - *self = Self::create_markdown_block(new_content, language_registry, 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, path_style); - 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, path_style: PathStyle) -> String { - match block { - acp::ContentBlock::Text(text_content) => text_content.text, - acp::ContentBlock::ResourceLink(resource_link) => { - Self::resource_link_md(&resource_link.uri, path_style) - } - acp::ContentBlock::Resource(acp::EmbeddedResource { - resource: - acp::EmbeddedResourceResource::TextResourceContents(acp::TextResourceContents { - uri, - .. - }), - .. - }) => Self::resource_link_md(&uri, path_style), - acp::ContentBlock::Image(image) => Self::image_md(&image), - _ => String::new(), - } - } - - fn resource_link_md(uri: &str, path_style: PathStyle) -> String { - if let Some(uri) = MentionUri::parse(uri, path_style).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 { - match self { - ContentBlock::Empty => "", - ContentBlock::Markdown { markdown } => markdown.read(cx).source(), - ContentBlock::ResourceLink { resource_link } => &resource_link.uri, - } - } - - pub fn markdown(&self) -> Option<&Entity> { - 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), -} - -impl ToolCallContent { - pub fn from_acp( - content: acp::ToolCallContent, - language_registry: Arc, - path_style: PathStyle, - terminals: &HashMap>, - cx: &mut App, - ) -> Result> { - match content { - acp::ToolCallContent::Content(acp::Content { content, .. }) => { - Ok(Some(Self::ContentBlock(ContentBlock::new( - content, - &language_registry, - path_style, - cx, - )))) - } - acp::ToolCallContent::Diff(diff) => Ok(Some(Self::Diff(cx.new(|cx| { - Diff::finalized( - diff.path.to_string_lossy().into_owned(), - diff.old_text, - diff.new_text, - language_registry, - cx, - ) - })))), - acp::ToolCallContent::Terminal(acp::Terminal { terminal_id, .. }) => terminals - .get(&terminal_id) - .cloned() - .map(|terminal| Some(Self::Terminal(terminal))) - .ok_or_else(|| anyhow::anyhow!("Terminal with id `{}` not found", terminal_id)), - _ => Ok(None), - } - } - - pub fn update_from_acp( - &mut self, - new: acp::ToolCallContent, - language_registry: Arc, - path_style: PathStyle, - terminals: &HashMap>, - cx: &mut App, - ) -> Result { - let needs_update = match (&self, &new) { - (Self::Diff(old_diff), acp::ToolCallContent::Diff(new_diff)) => { - old_diff.read(cx).needs_update( - new_diff.old_text.as_deref().unwrap_or(""), - &new_diff.new_text, - cx, - ) - } - _ => true, - }; - - if let Some(update) = Self::from_acp(new, language_registry, path_style, terminals, cx)? { - if needs_update { - *self = update; - } - Ok(true) - } else { - Ok(false) - } - } - - 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), - } - } -} - -#[derive(Debug, PartialEq)] -pub enum ToolCallUpdate { - UpdateFields(acp::ToolCallUpdate), - UpdateDiff(ToolCallUpdateDiff), - UpdateTerminal(ToolCallUpdateTerminal), -} - -impl ToolCallUpdate { - fn id(&self) -> &acp::ToolCallId { - match self { - Self::UpdateFields(update) => &update.tool_call_id, - Self::UpdateDiff(diff) => &diff.id, - Self::UpdateTerminal(terminal) => &terminal.id, - } - } -} - -impl From for ToolCallUpdate { - fn from(update: acp::ToolCallUpdate) -> Self { - Self::UpdateFields(update) - } -} - -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, -} - -#[derive(Debug)] -pub struct PlanStats<'a> { - pub in_progress_entry: Option<&'a PlanEntry>, - pub pending: u32, - pub completed: u32, -} - -impl Plan { - pub fn is_empty(&self) -> bool { - self.entries.is_empty() - } - - pub fn stats(&self) -> PlanStats<'_> { - let mut stats = PlanStats { - in_progress_entry: None, - pending: 0, - completed: 0, - }; - - for entry in &self.entries { - match &entry.status { - acp::PlanEntryStatus::Pending => { - stats.pending += 1; - } - acp::PlanEntryStatus::InProgress => { - stats.in_progress_entry = stats.in_progress_entry.or(Some(entry)); - } - acp::PlanEntryStatus::Completed => { - stats.completed += 1; - } - _ => {} - } - } - - stats - } -} - -#[derive(Debug)] -pub struct PlanEntry { - pub content: Entity, - pub priority: acp::PlanEntryPriority, - pub status: acp::PlanEntryStatus, -} - -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)), - 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, - plan: Plan, - project: Entity, - action_log: Entity, - shared_buffers: HashMap, BufferSnapshot>, - send_task: Option>, - connection: Rc, - session_id: acp::SessionId, - token_usage: Option, - prompt_capabilities: acp::PromptCapabilities, - _observe_prompt_capabilities: Task>, - terminals: HashMap>, - pending_terminal_output: HashMap>>, - pending_terminal_exit: HashMap, -} - -impl From<&AcpThread> for ActionLogTelemetry { - fn from(value: &AcpThread) -> Self { - Self { - agent_telemetry_id: value.connection().telemetry_id(), - session_id: value.session_id.0.clone(), - } - } -} - -#[derive(Debug)] -pub enum AcpThreadEvent { - NewEntry, - TitleUpdated, - TokenUsageUpdated, - EntryUpdated(usize), - EntriesRemoved(Range), - ToolAuthorizationRequired, - Retry(RetryStatus), - Stopped, - Error, - LoadError(LoadError), - PromptCapabilitiesUpdated, - Refusal, - AvailableCommandsUpdated(Vec), - ModeUpdated(acp::SessionModeId), -} - -impl EventEmitter for AcpThread {} - -#[derive(Debug, Clone)] -pub enum TerminalProviderEvent { - Created { - terminal_id: acp::TerminalId, - label: String, - cwd: Option, - output_byte_limit: Option, - terminal: Entity<::terminal::Terminal>, - }, - Output { - terminal_id: acp::TerminalId, - data: Vec, - }, - TitleChanged { - terminal_id: acp::TerminalId, - title: String, - }, - Exit { - terminal_id: acp::TerminalId, - status: acp::TerminalExitStatus, - }, -} - -#[derive(Debug, Clone)] -pub enum TerminalProviderCommand { - WriteInput { - terminal_id: acp::TerminalId, - bytes: Vec, - }, - Resize { - terminal_id: acp::TerminalId, - cols: u16, - rows: u16, - }, - Close { - terminal_id: acp::TerminalId, - }, -} - -impl AcpThread { - pub fn on_terminal_provider_event( - &mut self, - event: TerminalProviderEvent, - cx: &mut Context, - ) { - match event { - TerminalProviderEvent::Created { - terminal_id, - label, - cwd, - output_byte_limit, - terminal, - } => { - let entity = self.register_terminal_created( - terminal_id.clone(), - label, - cwd, - output_byte_limit, - terminal, - cx, - ); - - if let Some(mut chunks) = self.pending_terminal_output.remove(&terminal_id) { - for data in chunks.drain(..) { - entity.update(cx, |term, cx| { - term.inner().update(cx, |inner, cx| { - inner.write_output(&data, cx); - }) - }); - } - } - - if let Some(_status) = self.pending_terminal_exit.remove(&terminal_id) { - entity.update(cx, |_term, cx| { - cx.notify(); - }); - } - - cx.notify(); - } - TerminalProviderEvent::Output { terminal_id, data } => { - if let Some(entity) = self.terminals.get(&terminal_id) { - entity.update(cx, |term, cx| { - term.inner().update(cx, |inner, cx| { - inner.write_output(&data, cx); - }) - }); - } else { - self.pending_terminal_output - .entry(terminal_id) - .or_default() - .push(data); - } - } - TerminalProviderEvent::TitleChanged { terminal_id, title } => { - if let Some(entity) = self.terminals.get(&terminal_id) { - entity.update(cx, |term, cx| { - term.inner().update(cx, |inner, cx| { - inner.breadcrumb_text = title; - cx.emit(::terminal::Event::BreadcrumbsChanged); - }) - }); - } - } - TerminalProviderEvent::Exit { - terminal_id, - status, - } => { - if let Some(entity) = self.terminals.get(&terminal_id) { - entity.update(cx, |_term, cx| { - cx.notify(); - }); - } else { - self.pending_terminal_exit.insert(terminal_id, status); - } - } - } - } -} - -#[derive(PartialEq, Eq, Debug)] -pub enum ThreadStatus { - Idle, - Generating, -} - -#[derive(Debug, Clone)] -pub enum LoadError { - Unsupported { - command: SharedString, - current_version: SharedString, - minimum_version: SharedString, - }, - FailedToInstall(SharedString), - Exited { - status: ExitStatus, - }, - Other(SharedString), -} - -impl Display for LoadError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - LoadError::Unsupported { - command: path, - current_version, - minimum_version, - } => { - write!( - f, - "version {current_version} from {path} is not supported (need at least {minimum_version})" - ) - } - LoadError::FailedToInstall(msg) => write!(f, "Failed to install: {msg}"), - LoadError::Exited { status } => write!(f, "Server exited with status {status}"), - LoadError::Other(msg) => write!(f, "{msg}"), - } - } -} - -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().clone(); - 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); - })?; - } - }); - - Self { - action_log, - shared_buffers: Default::default(), - entries: Default::default(), - plan: Default::default(), - title: title.into(), - project, - send_task: None, - connection, - session_id, - token_usage: None, - prompt_capabilities, - _observe_prompt_capabilities: task, - terminals: HashMap::default(), - pending_terminal_output: HashMap::default(), - pending_terminal_exit: HashMap::default(), - } - } - - pub fn prompt_capabilities(&self) -> acp::PromptCapabilities { - self.prompt_capabilities.clone() - } - - pub fn connection(&self) -> &Rc { - &self.connection - } - - pub fn action_log(&self) -> &Entity { - &self.action_log - } - - pub fn project(&self) -> &Entity { - &self.project - } - - pub fn title(&self) -> SharedString { - self.title.clone() - } - - pub fn entries(&self) -> &[AgentThreadEntry] { - &self.entries - } - - pub fn session_id(&self) -> &acp::SessionId { - &self.session_id - } - - pub fn status(&self) -> ThreadStatus { - if self.send_task.is_some() { - ThreadStatus::Generating - } else { - ThreadStatus::Idle - } - } - - pub fn token_usage(&self) -> Option<&TokenUsage> { - self.token_usage.as_ref() - } - - pub fn has_pending_edit_tool_calls(&self) -> bool { - for entry in self.entries.iter().rev() { - match entry { - AgentThreadEntry::UserMessage(_) => return false, - AgentThreadEntry::ToolCall( - call @ ToolCall { - status: ToolCallStatus::InProgress | ToolCallStatus::Pending, - .. - }, - ) if call.diffs().next().is_some() => { - return true; - } - AgentThreadEntry::ToolCall(_) | AgentThreadEntry::AssistantMessage(_) => {} - } - } - - 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> { - match update { - acp::SessionUpdate::UserMessageChunk(acp::ContentChunk { content, .. }) => { - self.push_user_content_block(None, content, cx); - } - acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk { content, .. }) => { - self.push_assistant_content_block(content, false, cx); - } - acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk { content, .. }) => { - self.push_assistant_content_block(content, true, cx); - } - acp::SessionUpdate::ToolCall(tool_call) => { - self.upsert_tool_call(tool_call, cx)?; - } - acp::SessionUpdate::ToolCallUpdate(tool_call_update) => { - self.update_tool_call(tool_call_update, cx)?; - } - acp::SessionUpdate::Plan(plan) => { - self.update_plan(plan, cx); - } - acp::SessionUpdate::AvailableCommandsUpdate(acp::AvailableCommandsUpdate { - available_commands, - .. - }) => cx.emit(AcpThreadEvent::AvailableCommandsUpdated(available_commands)), - acp::SessionUpdate::CurrentModeUpdate(acp::CurrentModeUpdate { - current_mode_id, - .. - }) => cx.emit(AcpThreadEvent::ModeUpdated(current_mode_id)), - _ => {} - } - Ok(()) - } - - pub fn push_user_content_block( - &mut self, - message_id: Option, - chunk: acp::ContentBlock, - cx: &mut Context, - ) { - let language_registry = self.project.read(cx).languages().clone(); - let path_style = self.project.read(cx).path_style(cx); - let entries_len = self.entries.len(); - - if let Some(last_entry) = self.entries.last_mut() - && let AgentThreadEntry::UserMessage(UserMessage { - id, - content, - chunks, - .. - }) = last_entry - { - *id = message_id.or(id.take()); - content.append(chunk.clone(), &language_registry, path_style, cx); - chunks.push(chunk); - let idx = entries_len - 1; - cx.emit(AcpThreadEvent::EntryUpdated(idx)); - } else { - let content = ContentBlock::new(chunk.clone(), &language_registry, path_style, cx); - self.push_entry( - AgentThreadEntry::UserMessage(UserMessage { - id: message_id, - content, - chunks: vec![chunk], - checkpoint: None, - }), - cx, - ); - } - } - - pub fn push_assistant_content_block( - &mut self, - chunk: acp::ContentBlock, - is_thought: bool, - cx: &mut Context, - ) { - let language_registry = self.project.read(cx).languages().clone(); - let path_style = self.project.read(cx).path_style(cx); - let entries_len = self.entries.len(); - 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)); - match (chunks.last_mut(), is_thought) { - (Some(AssistantMessageChunk::Message { block }), false) - | (Some(AssistantMessageChunk::Thought { block }), true) => { - block.append(chunk, &language_registry, path_style, cx) - } - _ => { - let block = ContentBlock::new(chunk, &language_registry, path_style, cx); - if is_thought { - chunks.push(AssistantMessageChunk::Thought { block }) - } else { - chunks.push(AssistantMessageChunk::Message { block }) - } - } - } - } else { - let block = ContentBlock::new(chunk, &language_registry, path_style, cx); - let chunk = if is_thought { - AssistantMessageChunk::Thought { block } - } else { - AssistantMessageChunk::Message { block } - }; - - self.push_entry( - AgentThreadEntry::AssistantMessage(AssistantMessage { - chunks: vec![chunk], - }), - cx, - ); - } - } - - fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context) { - self.entries.push(entry); - 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, - cx: &mut Context, - ) -> Result<()> { - let update = update.into(); - let languages = self.project.read(cx).languages().clone(); - let path_style = self.project.read(cx).path_style(cx); - - let ix = match self.index_for_tool_call(update.id()) { - Some(ix) => ix, - None => { - // Tool call not found - create a failed tool call entry - let failed_tool_call = ToolCall { - id: update.id().clone(), - label: cx.new(|cx| Markdown::new("Tool call not found".into(), None, None, cx)), - kind: acp::ToolKind::Fetch, - content: vec![ToolCallContent::ContentBlock(ContentBlock::new( - "Tool call not found".into(), - &languages, - path_style, - cx, - ))], - status: ToolCallStatus::Failed, - locations: Vec::new(), - resolved_locations: Vec::new(), - raw_input: None, - raw_output: None, - }; - self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx); - return Ok(()); - } - }; - let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else { - unreachable!() - }; - - match update { - ToolCallUpdate::UpdateFields(update) => { - let location_updated = update.fields.locations.is_some(); - call.update_fields(update.fields, languages, path_style, &self.terminals, cx)?; - if location_updated { - self.resolve_locations(update.tool_call_id, cx); - } - } - ToolCallUpdate::UpdateDiff(update) => { - call.content.clear(); - call.content.push(ToolCallContent::Diff(update.diff)); - } - ToolCallUpdate::UpdateTerminal(update) => { - call.content.clear(); - call.content - .push(ToolCallContent::Terminal(update.terminal)); - } - } - - cx.emit(AcpThreadEvent::EntryUpdated(ix)); - - Ok(()) - } - - /// 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) - } - - /// Fails if id does not match an existing entry. - pub fn upsert_tool_call_inner( - &mut self, - update: acp::ToolCallUpdate, - status: ToolCallStatus, - cx: &mut Context, - ) -> Result<(), acp::Error> { - let language_registry = self.project.read(cx).languages().clone(); - let path_style = self.project.read(cx).path_style(cx); - let id = update.tool_call_id.clone(); - - let agent_telemetry_id = self.connection().telemetry_id(); - let session = self.session_id(); - if let ToolCallStatus::Completed | ToolCallStatus::Failed = status { - let status = if matches!(status, ToolCallStatus::Completed) { - "completed" - } else { - "failed" - }; - telemetry::event!( - "Agent Tool Call Completed", - agent_telemetry_id, - session, - status - ); - } - - if let Some(ix) = self.index_for_tool_call(&id) { - let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else { - unreachable!() - }; - - call.update_fields( - update.fields, - language_registry, - path_style, - &self.terminals, - cx, - )?; - call.status = status; - - cx.emit(AcpThreadEvent::EntryUpdated(ix)); - } else { - let call = ToolCall::from_acp( - update.try_into()?, - status, - language_registry, - self.project.read(cx).path_style(cx), - &self.terminals, - cx, - )?; - self.push_entry(AgentThreadEntry::ToolCall(call), cx); - }; - - self.resolve_locations(id, cx); - Ok(()) - } - - fn index_for_tool_call(&self, id: &acp::ToolCallId) -> Option { - self.entries - .iter() - .enumerate() - .rev() - .find_map(|(index, entry)| { - if let AgentThreadEntry::ToolCall(tool_call) = entry - && &tool_call.id == id - { - Some(index) - } else { - None - } - }) - } - - fn tool_call_mut(&mut self, id: &acp::ToolCallId) -> Option<(usize, &mut ToolCall)> { - // The tool call we are looking for is typically the last one, or very close to the end. - // At the moment, it doesn't seem like a hashmap would be a good fit for this use case. - self.entries - .iter_mut() - .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 tool_call(&mut self, id: &acp::ToolCallId) -> Option<(usize, &ToolCall)> { - self.entries - .iter() - .enumerate() - .rev() - .find_map(|(index, tool_call)| { - if let AgentThreadEntry::ToolCall(tool_call) = tool_call - && &tool_call.id == id - { - Some((index, tool_call)) - } else { - None - } - }) - } - - pub fn resolve_locations(&mut self, id: acp::ToolCallId, cx: &mut Context) { - 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(); - - for location in resolved_locations.iter().flatten() { - this.shared_buffers - .insert(location.buffer.clone(), location.buffer.read(cx).snapshot()); - } - 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| { - let should_ignore = if let Some(agent_location) = project - .agent_location() - .filter(|agent_location| agent_location.buffer == location.buffer) - { - let snapshot = location.buffer.read(cx).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 - } else { - false - }; - if !should_ignore { - project.set_agent_location(Some(location.into()), cx); - } - }); - } - - let resolved_locations = resolved_locations - .iter() - .map(|l| l.as_ref().map(|l| AgentLocation::from(l))) - .collect::>(); - - 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( - &mut self, - tool_call: acp::ToolCallUpdate, - options: Vec, - respect_always_allow_setting: bool, - cx: &mut Context, - ) -> Result> { - let (tx, rx) = oneshot::channel(); - - if respect_always_allow_setting && AgentSettings::get_global(cx).always_allow_tool_actions { - // Don't use AllowAlways, because then if you were to turn off always_allow_tool_actions, - // some tools would (incorrectly) continue to auto-accept. - if let Some(allow_once_option) = options.iter().find_map(|option| { - if matches!(option.kind, acp::PermissionOptionKind::AllowOnce) { - Some(option.option_id.clone()) - } else { - None - } - }) { - self.upsert_tool_call_inner(tool_call, ToolCallStatus::Pending, cx)?; - return Ok(async { - acp::RequestPermissionOutcome::Selected(acp::SelectedPermissionOutcome::new( - allow_once_option, - )) - } - .boxed()); - } - } - - let status = ToolCallStatus::WaitingForConfirmation { - options, - respond_tx: tx, - }; - - self.upsert_tool_call_inner(tool_call, status, cx)?; - cx.emit(AcpThreadEvent::ToolAuthorizationRequired); - - let fut = async { - match rx.await { - Ok(option) => acp::RequestPermissionOutcome::Selected( - acp::SelectedPermissionOutcome::new(option), - ), - Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled, - } - } - .boxed(); - - Ok(fut) - } - - pub fn authorize_tool_call( - &mut self, - id: acp::ToolCallId, - option_id: acp::PermissionOptionId, - option_kind: acp::PermissionOptionKind, - cx: &mut Context, - ) { - let Some((ix, call)) = self.tool_call_mut(&id) else { - return; - }; - - let new_status = match option_kind { - acp::PermissionOptionKind::RejectOnce | acp::PermissionOptionKind::RejectAlways => { - ToolCallStatus::Rejected - } - acp::PermissionOptionKind::AllowOnce | acp::PermissionOptionKind::AllowAlways => { - ToolCallStatus::InProgress - } - _ => ToolCallStatus::InProgress, - }; - - let curr_status = mem::replace(&mut call.status, new_status); - - if let ToolCallStatus::WaitingForConfirmation { respond_tx, .. } = curr_status { - respond_tx.send(option_id).log_err(); - } else if cfg!(debug_assertions) { - panic!("tried to authorize an already authorized tool call"); - } - - cx.emit(AcpThreadEvent::EntryUpdated(ix)); - } - - pub fn first_tool_awaiting_confirmation(&self) -> Option<&ToolCall> { - let mut first_tool_call = None; - - for entry in self.entries.iter().rev() { - match &entry { - AgentThreadEntry::ToolCall(call) => { - if let ToolCallStatus::WaitingForConfirmation { .. } = call.status { - first_tool_call = Some(call); - } else { - continue; - } - } - AgentThreadEntry::UserMessage(_) | AgentThreadEntry::AssistantMessage(_) => { - // Reached the beginning of the turn. - // If we had pending permission requests in the previous turn, they have been cancelled. - break; - } - } - } - - first_tool_call - } - - pub fn plan(&self) -> &Plan { - &self.plan - } - - 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); - - cx.notify(); - } - - fn clear_completed_plan_entries(&mut self, cx: &mut Context) { - self.plan - .entries - .retain(|entry| !matches!(entry.status, acp::PlanEntryStatus::Completed)); - cx.notify(); - } - - #[cfg(any(test, feature = "test-support"))] - pub fn send_raw( - &mut self, - message: &str, - cx: &mut Context, - ) -> BoxFuture<'static, Result<()>> { - self.send(vec![message.into()], cx) - } - - pub fn send( - &mut self, - message: Vec, - cx: &mut Context, - ) -> BoxFuture<'static, Result<()>> { - let block = ContentBlock::new_combined( - message.clone(), - self.project.read(cx).languages().clone(), - self.project.read(cx).path_style(cx), - cx, - ); - let request = acp::PromptRequest::new(self.session_id.clone(), message.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.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(); - })); - - 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(); - } - - // Handle refusal - distinguish between user prompt and tool call refusals - if let Ok(Ok(acp::PromptResponse { - stop_reason: acp::StopReason::Refusal, - .. - })) = result - { - if let Some((user_msg_ix, _)) = this.last_user_message() { - // Check if there's a completed tool call with results after the last user message - // This indicates the refusal is in response to tool output, not the user's prompt - let has_completed_tool_call_after_user_msg = - this.entries.iter().skip(user_msg_ix + 1).any(|entry| { - if let AgentThreadEntry::ToolCall(tool_call) = entry { - // Check if the tool call has completed and has output - matches!(tool_call.status, ToolCallStatus::Completed) - && tool_call.raw_output.is_some() - } else { - false - } - }); - - if has_completed_tool_call_after_user_msg { - // Refusal is due to tool output - don't truncate, just notify - // The model refused based on what the tool returned - cx.emit(AcpThreadEvent::Refusal); - } else { - // User prompt was refused - truncate back to before the user message - let range = user_msg_ix..this.entries.len(); - if range.start < range.end { - this.entries.truncate(user_msg_ix); - cx.emit(AcpThreadEvent::EntriesRemoved(range)); - } - cx.emit(AcpThreadEvent::Refusal); - } - } else { - // No user message found, treat as general refusal - cx.emit(AcpThreadEvent::Refusal); - } - } - - cx.emit(AcpThreadEvent::Stopped); - Ok(()) - } - } - })? - }) - .boxed() - } - - pub fn cancel(&mut self, cx: &mut Context) -> Task<()> { - let Some(send_task) = self.send_task.take() else { - return Task::ready(()); - }; - - for entry in self.entries.iter_mut() { - if let AgentThreadEntry::ToolCall(call) = entry { - let cancel = matches!( - call.status, - ToolCallStatus::Pending - | ToolCallStatus::WaitingForConfirmation { .. } - | ToolCallStatus::InProgress - ); - - if cancel { - call.status = ToolCallStatus::Canceled; - } - } - } - - self.connection.cancel(&self.session_id, cx); - - // Wait for the send task to complete - cx.foreground_executor().spawn(send_task) - } - - /// Restores the git working tree to the state at the given checkpoint (if one exists) - pub fn restore_checkpoint( - &mut self, - id: UserMessageId, - cx: &mut Context, - ) -> Task> { - let Some((_, message)) = self.user_message_mut(&id) else { - return Task::ready(Err(anyhow!("message not found"))); - }; - - let checkpoint = message - .checkpoint - .as_ref() - .map(|c| c.git_checkpoint.clone()); - - // Cancel any in-progress generation before restoring - let cancel_task = self.cancel(cx); - let rewind = self.rewind(id.clone(), cx); - let git_store = self.project.read(cx).git_store().clone(); - - cx.spawn(async move |_, cx| { - cancel_task.await; - rewind.await?; - if let Some(checkpoint) = checkpoint { - git_store - .update(cx, |git, cx| git.restore_checkpoint(checkpoint, cx))? - .await?; - } - - Ok(()) - }) - } - - /// Rewinds this thread to before the entry at `index`, removing it and all - /// subsequent entries while rejecting any action_log changes made from that point. - /// Unlike `restore_checkpoint`, this method does not restore from git. - 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 telemetry = ActionLogTelemetry::from(&*self); - cx.spawn(async move |this, cx| { - cx.update(|cx| truncate.run(id.clone(), cx))?.await?; - this.update(cx, |this, cx| { - if let Some((ix, _)) = this.user_message_mut(&id) { - // Collect all terminals from entries that will be removed - let terminals_to_remove: Vec = this.entries[ix..] - .iter() - .flat_map(|entry| entry.terminals()) - .filter_map(|terminal| terminal.read(cx).id().clone().into()) - .collect(); - - let range = ix..this.entries.len(); - this.entries.truncate(ix); - cx.emit(AcpThreadEvent::EntriesRemoved(range)); - - // Kill and remove the terminals - for terminal_id in terminals_to_remove { - if let Some(terminal) = this.terminals.remove(&terminal_id) { - terminal.update(cx, |terminal, cx| { - terminal.kill(cx); - }); - } - } - } - this.action_log().update(cx, |action_log, cx| { - action_log.reject_all_edits(Some(telemetry), cx) - }) - })? - .await; - Ok(()) - }) - } - - 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_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, - line: Option, - limit: Option, - reuse_shared_snapshot: bool, - cx: &mut Context, - ) -> Task> { - // Args are 1-based, move to 0-based - let line = line.unwrap_or_default().saturating_sub(1); - let limit = limit.unwrap_or(u32::MAX); - let project = self.project.clone(); - let action_log = self.action_log.clone(); - cx.spawn(async move |this, cx| { - let load = project - .update(cx, |project, cx| { - let path = project - .project_path_for_absolute_path(&path, cx) - .ok_or_else(|| { - acp::Error::resource_not_found(Some(path.display().to_string())) - })?; - Ok(project.open_buffer(path, cx)) - }) - .map_err(|e| acp::Error::internal_error().data(e.to_string())) - .flatten()?; - - let buffer = load.await?; - - let snapshot = if reuse_shared_snapshot { - this.read_with(cx, |this, _| { - this.shared_buffers.get(&buffer.clone()).cloned() - }) - .log_err() - .flatten() - } else { - None - }; - - let snapshot = if let Some(snapshot) = snapshot { - snapshot - } else { - action_log.update(cx, |action_log, cx| { - action_log.buffer_read(buffer.clone(), cx); - })?; - - let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?; - this.update(cx, |this, _| { - this.shared_buffers.insert(buffer.clone(), snapshot.clone()); - })?; - snapshot - }; - - let max_point = snapshot.max_point(); - let start_position = Point::new(line, 0); - - if start_position > max_point { - return Err(acp::Error::invalid_params().data(format!( - "Attempting to read beyond the end of the file, line {}:{}", - max_point.row + 1, - max_point.column - ))); - } - - let start = snapshot.anchor_before(start_position); - let end = snapshot.anchor_before(Point::new(line.saturating_add(limit), 0)); - - project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: start, - }), - cx, - ); - })?; - - Ok(snapshot.text_for_range(start..end).collect::()) - }) - } - - pub fn write_text_file( - &self, - path: PathBuf, - content: String, - cx: &mut Context, - ) -> Task> { - let project = self.project.clone(); - let action_log = self.action_log.clone(); - cx.spawn(async move |this, cx| { - let load = project.update(cx, |project, cx| { - let path = project - .project_path_for_absolute_path(&path, cx) - .context("invalid path")?; - anyhow::Ok(project.open_buffer(path, cx)) - }); - let buffer = load??.await?; - let snapshot = this.update(cx, |this, cx| { - this.shared_buffers - .get(&buffer) - .cloned() - .unwrap_or_else(|| buffer.read(cx).snapshot()) - })?; - let edits = cx - .background_executor() - .spawn(async move { - let old_text = snapshot.text(); - text_diff(old_text.as_str(), &content) - .into_iter() - .map(|(range, replacement)| { - ( - snapshot.anchor_after(range.start) - ..snapshot.anchor_before(range.end), - replacement, - ) - }) - .collect::>() - }) - .await; - - 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_for_buffer(buffer.read(cx).remote_id())), - }), - 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.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 - }) - } - - pub fn create_terminal( - &self, - command: String, - args: Vec, - extra_env: Vec, - cwd: Option, - output_byte_limit: Option, - cx: &mut Context, - ) -> Task>> { - let env = match &cwd { - Some(dir) => self.project.update(cx, |project, cx| { - project.environment().update(cx, |env, cx| { - env.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(); - // Disables paging for `git` and hopefully other commands - env.insert("PAGER".into(), "".into()); - for var in extra_env { - env.insert(var.name, var.value); - } - env - }); - - let project = self.project.clone(); - let language_registry = project.read(cx).languages().clone(); - let is_windows = project.read(cx).path_style(cx).is_windows(); - - let terminal_id = acp::TerminalId::new(Uuid::new_v4().to_string()); - let terminal_task = cx.spawn({ - let terminal_id = terminal_id.clone(); - async move |_this, cx| { - let env = env.await; - let shell = project - .update(cx, |project, cx| { - project - .remote_client() - .and_then(|r| r.read(cx).default_system_shell()) - })? - .unwrap_or_else(|| get_default_system_shell_preferring_bash()); - let (task_command, task_args) = - ShellBuilder::new(&Shell::Program(shell), is_windows) - .redirect_stdin_to_dev_null() - .build(Some(command.clone()), &args); - let terminal = project - .update(cx, |project, cx| { - project.create_terminal_task( - task::SpawnInTerminal { - command: Some(task_command), - args: task_args, - cwd: cwd.clone(), - env, - ..Default::default() - }, - cx, - ) - })? - .await?; - - cx.new(|cx| { - Terminal::new( - terminal_id, - &format!("{} {}", command, args.join(" ")), - cwd, - output_byte_limit.map(|l| l as usize), - terminal, - language_registry, - cx, - ) - }) - } - }); - - cx.spawn(async move |this, cx| { - let terminal = terminal_task.await?; - this.update(cx, |this, _cx| { - this.terminals.insert(terminal_id, terminal.clone()); - terminal - }) - }) - } - - pub fn kill_terminal( - &mut self, - terminal_id: acp::TerminalId, - cx: &mut Context, - ) -> Result<()> { - self.terminals - .get(&terminal_id) - .context("Terminal not found")? - .update(cx, |terminal, cx| { - terminal.kill(cx); - }); - - Ok(()) - } - - pub fn release_terminal( - &mut self, - terminal_id: acp::TerminalId, - cx: &mut Context, - ) -> Result<()> { - self.terminals - .remove(&terminal_id) - .context("Terminal not found")? - .update(cx, |terminal, cx| { - terminal.kill(cx); - }); - - Ok(()) - } - - pub fn terminal(&self, terminal_id: acp::TerminalId) -> Result> { - self.terminals - .get(&terminal_id) - .context("Terminal not found") - .cloned() - } - - 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)); - } - - pub fn register_terminal_created( - &mut self, - terminal_id: acp::TerminalId, - command_label: String, - working_dir: Option, - output_byte_limit: Option, - terminal: Entity<::terminal::Terminal>, - cx: &mut Context, - ) -> Entity { - let language_registry = self.project.read(cx).languages().clone(); - - let entity = cx.new(|cx| { - Terminal::new( - terminal_id.clone(), - &command_label, - working_dir.clone(), - output_byte_limit.map(|l| l as usize), - terminal, - language_registry, - cx, - ) - }); - self.terminals.insert(terminal_id.clone(), entity.clone()); - entity - } -} - -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 anyhow::anyhow; - use futures::{channel::mpsc, future::LocalBoxFuture, select}; - use gpui::{App, AsyncApp, TestAppContext, WeakEntity}; - use indoc::indoc; - use project::{FakeFs, Fs}; - use rand::{distr, prelude::*}; - 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 util::path; - - 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); - }); - } - - #[gpui::test] - async fn test_terminal_output_buffered_before_created_renders(cx: &mut gpui::TestAppContext) { - init_test(cx); - - 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, std::path::Path::new(path!("/test")), cx)) - .await - .unwrap(); - - let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string()); - - // Send Output BEFORE Created - should be buffered by acp_thread - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Output { - terminal_id: terminal_id.clone(), - data: b"hello buffered".to_vec(), - }, - cx, - ); - }); - - // Create a display-only terminal and then send Created - let lower = cx.new(|cx| { - let builder = ::terminal::TerminalBuilder::new_display_only( - ::terminal::terminal_settings::CursorShape::default(), - ::terminal::terminal_settings::AlternateScroll::On, - None, - 0, - ) - .unwrap(); - builder.subscribe(cx) - }); - - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Created { - terminal_id: terminal_id.clone(), - label: "Buffered Test".to_string(), - cwd: None, - output_byte_limit: None, - terminal: lower.clone(), - }, - cx, - ); - }); - - // After Created, buffered Output should have been flushed into the renderer - let content = thread.read_with(cx, |thread, cx| { - let term = thread.terminal(terminal_id.clone()).unwrap(); - term.read_with(cx, |t, cx| t.inner().read(cx).get_content()) - }); - - assert!( - content.contains("hello buffered"), - "expected buffered output to render, got: {content}" - ); - } - - #[gpui::test] - async fn test_terminal_output_and_exit_buffered_before_created(cx: &mut gpui::TestAppContext) { - init_test(cx); - - 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, std::path::Path::new(path!("/test")), cx)) - .await - .unwrap(); - - let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string()); - - // Send Output BEFORE Created - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Output { - terminal_id: terminal_id.clone(), - data: b"pre-exit data".to_vec(), - }, - cx, - ); - }); - - // Send Exit BEFORE Created - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Exit { - terminal_id: terminal_id.clone(), - status: acp::TerminalExitStatus::new().exit_code(0), - }, - cx, - ); - }); - - // Now create a display-only lower-level terminal and send Created - let lower = cx.new(|cx| { - let builder = ::terminal::TerminalBuilder::new_display_only( - ::terminal::terminal_settings::CursorShape::default(), - ::terminal::terminal_settings::AlternateScroll::On, - None, - 0, - ) - .unwrap(); - builder.subscribe(cx) - }); - - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Created { - terminal_id: terminal_id.clone(), - label: "Buffered Exit Test".to_string(), - cwd: None, - output_byte_limit: None, - terminal: lower.clone(), - }, - cx, - ); - }); - - // Output should be present after Created (flushed from buffer) - let content = thread.read_with(cx, |thread, cx| { - let term = thread.terminal(terminal_id.clone()).unwrap(); - term.read_with(cx, |t, cx| t.inner().read(cx).get_content()) - }); - - assert!( - content.contains("pre-exit data"), - "expected pre-exit data to render, got: {content}" - ); - } - - #[gpui::test] - async fn test_push_user_content_block(cx: &mut gpui::TestAppContext) { - init_test(cx); - - 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(); - - // Test creating a new user message - thread.update(cx, |thread, cx| { - thread.push_user_content_block(None, "Hello, ".into(), cx); - }); - - 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"); - } - }); - - // 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()), "world!".into(), cx); - }); - - 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"); - } - }); - - // Test creating new user message after assistant message - thread.update(cx, |thread, cx| { - thread.push_assistant_content_block("Assistant response".into(), false, cx); - }); - - let message_2_id = UserMessageId::new(); - thread.update(cx, |thread, cx| { - thread.push_user_content_block( - Some(message_2_id.clone()), - "New user message".into(), - cx, - ); - }); - - 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"); - } - }); - } - - #[gpui::test] - async fn test_thinking_concatenation(cx: &mut gpui::TestAppContext) { - init_test(cx); - - 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(acp::ContentChunk::new( - "Thinking ".into(), - )), - cx, - ) - .unwrap(); - thread - .handle_session_update( - acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk::new( - "hard!".into(), - )), - cx, - ) - .unwrap(); - })?; - Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) - } - .boxed_local() - }, - )); - - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); - - thread - .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx)) - .await - .unwrap(); - - let output = thread.read_with(cx, |thread, cx| thread.to_markdown(cx)); - assert_eq!( - output, - indoc! {r#" - ## User - - Hello from Zed! - - ## Assistant - - - Thinking hard! - - - "#} - ); - } - - #[gpui::test] - async fn test_edits_concurrently_to_user(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - 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::new(acp::StopReason::EndTurn)) - } - .boxed_local() - }, - )); - - let (worktree, pathbuf) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/tmp/foo"), true, cx) - }) - .await - .unwrap(); - let buffer = project - .update(cx, |project, cx| { - project.open_buffer((worktree.read(cx).id(), pathbuf), cx) - }) - .await - .unwrap(); - - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx)) - .await - .unwrap(); - - let request = thread.update(cx, |thread, cx| { - thread.send_raw("Extend the count in /tmp/foo", cx) - }); - read_file_rx.await.ok(); - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "zero\n".to_string())], None, cx); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "zero\none\ntwo\nthree\nfour\nfive\n" - ); - assert_eq!( - String::from_utf8(fs.read_file_sync(path!("/tmp/foo")).unwrap()).unwrap(), - "zero\none\ntwo\nthree\nfour\nfive\n" - ); - request.await.unwrap(); - } - - #[gpui::test] - async fn test_reading_from_line(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/tmp"), json!({"foo": "one\ntwo\nthree\nfour\n"})) - .await; - let project = Project::test(fs.clone(), [], cx).await; - project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/tmp/foo"), true, cx) - }) - .await - .unwrap(); - - let connection = Rc::new(FakeAgentConnection::new()); - - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx)) - .await - .unwrap(); - - // Whole file - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), None, None, false, cx) - }) - .await - .unwrap(); - - assert_eq!(content, "one\ntwo\nthree\nfour\n"); - - // Only start line - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), Some(3), None, false, cx) - }) - .await - .unwrap(); - - assert_eq!(content, "three\nfour\n"); - - // Only limit - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), None, Some(2), false, cx) - }) - .await - .unwrap(); - - assert_eq!(content, "one\ntwo\n"); - - // Range - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), Some(2), Some(2), false, cx) - }) - .await - .unwrap(); - - assert_eq!(content, "two\nthree\n"); - - // Invalid - let err = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), Some(6), Some(2), false, cx) - }) - .await - .unwrap_err(); - - assert_eq!( - err.to_string(), - "Invalid params: \"Attempting to read beyond the end of the file, line 5:0\"" - ); - } - - #[gpui::test] - async fn test_reading_empty_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/tmp"), json!({"foo": ""})).await; - let project = Project::test(fs.clone(), [], cx).await; - project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/tmp/foo"), true, cx) - }) - .await - .unwrap(); - - let connection = Rc::new(FakeAgentConnection::new()); - - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx)) - .await - .unwrap(); - - // Whole file - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), None, None, false, cx) - }) - .await - .unwrap(); - - assert_eq!(content, ""); - - // Only start line - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), Some(1), None, false, cx) - }) - .await - .unwrap(); - - assert_eq!(content, ""); - - // Only limit - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), None, Some(2), false, cx) - }) - .await - .unwrap(); - - assert_eq!(content, ""); - - // Range - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), Some(1), Some(1), false, cx) - }) - .await - .unwrap(); - - assert_eq!(content, ""); - - // Invalid - let err = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), Some(5), Some(2), false, cx) - }) - .await - .unwrap_err(); - - assert_eq!( - err.to_string(), - "Invalid params: \"Attempting to read beyond the end of the file, line 1:0\"" - ); - } - #[gpui::test] - async fn test_reading_non_existing_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/tmp"), json!({})).await; - let project = Project::test(fs.clone(), [], cx).await; - project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/tmp"), true, cx) - }) - .await - .unwrap(); - - let connection = Rc::new(FakeAgentConnection::new()); - - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx)) - .await - .unwrap(); - - // Out of project file - let err = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/foo").into(), None, None, false, cx) - }) - .await - .unwrap_err(); - - assert_eq!(err.code, acp::ErrorCode::ResourceNotFound); - } - - #[gpui::test] - async fn test_succeeding_canceled_toolcall(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let id = acp::ToolCallId::new("test"); - - let connection = Rc::new(FakeAgentConnection::new().on_user_message({ - let id = id.clone(); - move |_, thread, mut cx| { - let id = id.clone(); - async move { - thread - .update(&mut cx, |thread, cx| { - thread.handle_session_update( - acp::SessionUpdate::ToolCall( - acp::ToolCall::new(id.clone(), "Label") - .kind(acp::ToolKind::Fetch) - .status(acp::ToolCallStatus::InProgress), - ), - cx, - ) - }) - .unwrap() - .unwrap(); - Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) - } - .boxed_local() - } - })); - - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); - - let request = thread.update(cx, |thread, cx| { - thread.send_raw("Fetch https://example.com", cx) - }); - - run_until_first_tool_call(&thread, cx).await; - - thread.read_with(cx, |thread, _| { - assert!(matches!( - thread.entries[1], - AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::InProgress, - .. - }) - )); - }); - - thread.update(cx, |thread, cx| thread.cancel(cx)).await; - - thread.read_with(cx, |thread, _| { - assert!(matches!( - &thread.entries[1], - AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Canceled, - .. - }) - )); - }); - - thread - .update(cx, |thread, cx| { - thread.handle_session_update( - acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new( - id, - acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::Completed), - )), - cx, - ) - }) - .unwrap(); - - request.await.unwrap(); - - thread.read_with(cx, |thread, _| { - assert!(matches!( - thread.entries[1], - AgentThreadEntry::ToolCall(ToolCall { - status: 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::new("test", "Label") - .kind(acp::ToolKind::Edit) - .status(acp::ToolCallStatus::Completed) - .content(vec![acp::ToolCallContent::Diff(acp::Diff::new( - "/test/test.txt", - "foo", - ))]), - ), - cx, - ) - }) - .unwrap() - .unwrap(); - Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) - } - .boxed_local() - } - })); - - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); - - 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(acp::ContentChunk::new( - content.text.to_uppercase().into(), - )), - cx, - ) - .unwrap(); - })?; - Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) - } - .boxed_local() - } - })); - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); - - 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.restore_checkpoint(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_tool_result_refusal(cx: &mut TestAppContext) { - use std::sync::atomic::AtomicUsize; - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, None, cx).await; - - // Create a connection that simulates refusal after tool result - let prompt_count = Arc::new(AtomicUsize::new(0)); - let connection = Rc::new(FakeAgentConnection::new().on_user_message({ - let prompt_count = prompt_count.clone(); - move |_request, thread, mut cx| { - let count = prompt_count.fetch_add(1, SeqCst); - async move { - if count == 0 { - // First prompt: Generate a tool call with result - thread.update(&mut cx, |thread, cx| { - thread - .handle_session_update( - acp::SessionUpdate::ToolCall( - acp::ToolCall::new("tool1", "Test Tool") - .kind(acp::ToolKind::Fetch) - .status(acp::ToolCallStatus::Completed) - .raw_input(serde_json::json!({"query": "test"})) - .raw_output(serde_json::json!({"result": "inappropriate content"})), - ), - cx, - ) - .unwrap(); - })?; - - // Now return refusal because of the tool result - Ok(acp::PromptResponse::new(acp::StopReason::Refusal)) - } else { - Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) - } - } - .boxed_local() - } - })); - - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); - - // Track if we see a Refusal event - let saw_refusal_event = Arc::new(std::sync::Mutex::new(false)); - let saw_refusal_event_captured = saw_refusal_event.clone(); - thread.update(cx, |_thread, cx| { - cx.subscribe( - &thread, - move |_thread, _event_thread, event: &AcpThreadEvent, _cx| { - if matches!(event, AcpThreadEvent::Refusal) { - *saw_refusal_event_captured.lock().unwrap() = true; - } - }, - ) - .detach(); - }); - - // Send a user message - this will trigger tool call and then refusal - let send_task = thread.update(cx, |thread, cx| thread.send(vec!["Hello".into()], cx)); - cx.background_executor.spawn(send_task).detach(); - cx.run_until_parked(); - - // Verify that: - // 1. A Refusal event WAS emitted (because it's a tool result refusal, not user prompt) - // 2. The user message was NOT truncated - assert!( - *saw_refusal_event.lock().unwrap(), - "Refusal event should be emitted for tool result refusals" - ); - - thread.read_with(cx, |thread, _| { - let entries = thread.entries(); - assert!(entries.len() >= 2, "Should have user message and tool call"); - - // Verify user message is still there - assert!( - matches!(entries[0], AgentThreadEntry::UserMessage(_)), - "User message should not be truncated" - ); - - // Verify tool call is there with result - if let AgentThreadEntry::ToolCall(tool_call) = &entries[1] { - assert!( - tool_call.raw_output.is_some(), - "Tool call should have output" - ); - } else { - panic!("Expected tool call at index 1"); - } - }); - } - - #[gpui::test] - async fn test_user_prompt_refusal_emits_event(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, None, 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, _cx| { - if refuse_next.load(SeqCst) { - async move { Ok(acp::PromptResponse::new(acp::StopReason::Refusal)) } - .boxed_local() - } else { - async move { Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) } - .boxed_local() - } - } - })); - - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); - - // Track if we see a Refusal event - let saw_refusal_event = Arc::new(std::sync::Mutex::new(false)); - let saw_refusal_event_captured = saw_refusal_event.clone(); - thread.update(cx, |_thread, cx| { - cx.subscribe( - &thread, - move |_thread, _event_thread, event: &AcpThreadEvent, _cx| { - if matches!(event, AcpThreadEvent::Refusal) { - *saw_refusal_event_captured.lock().unwrap() = true; - } - }, - ) - .detach(); - }); - - // Send a message that will be refused - refuse_next.store(true, SeqCst); - cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["hello".into()], cx))) - .await - .unwrap(); - - // Verify that a Refusal event WAS emitted for user prompt refusal - assert!( - *saw_refusal_event.lock().unwrap(), - "Refusal event should be emitted for user prompt refusals" - ); - - // Verify the message was truncated (user prompt refusal) - thread.read_with(cx, |thread, cx| { - assert_eq!(thread.to_markdown(cx), ""); - }); - } - - #[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::new(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(acp::ContentChunk::new( - content.text.to_uppercase().into(), - )), - cx, - ) - .unwrap(); - })?; - Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) - } - .boxed_local() - } - })); - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); - - 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. The message should be truncated - // when a user prompt is refused. - 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, - ) -> usize { - let (mut tx, mut rx) = mpsc::channel::(1); - - let subscription = cx.update(|cx| { - cx.subscribe(thread, move |thread, _, cx| { - for (ix, entry) in thread.read(cx).entries.iter().enumerate() { - if matches!(entry, AgentThreadEntry::ToolCall(_)) { - return tx.try_send(ix).unwrap(); - } - } - }) - }); - - select! { - _ = futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(10))) => { - panic!("Timeout waiting for tool call") - } - ix = rx.next().fuse() => { - drop(subscription); - ix.unwrap() - } - } - } - - #[derive(Clone, Default)] - struct FakeAgentConnection { - auth_methods: Vec, - sessions: Arc>>>, - on_user_message: Option< - Rc< - dyn Fn( - acp::PromptRequest, - WeakEntity, - AsyncApp, - ) -> LocalBoxFuture<'static, Result> - + 'static, - >, - >, - } - - 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 - } - } - - impl AgentConnection for FakeAgentConnection { - fn telemetry_id(&self) -> SharedString { - "fake".into() - } - - 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::new( - rand::rng() - .sample_iter(&distr::Alphanumeric) - .take(7) - .map(char::from) - .collect::(), - ); - 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::new() - .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( - &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::new(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 - }) - .detach(); - } - - fn truncate( - &self, - session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { - Some(Rc::new(FakeAgentSessionEditor { - _session_id: session_id.clone(), - })) - } - - fn into_any(self: Rc) -> Rc { - self - } - } - - struct FakeAgentSessionEditor { - _session_id: acp::SessionId, - } - - impl AgentSessionTruncate for FakeAgentSessionEditor { - fn run(&self, _message_id: UserMessageId, _cx: &mut App) -> Task> { - Task::ready(Ok(())) - } - } - - #[gpui::test] - async fn test_tool_call_not_found_creates_failed_entry(cx: &mut TestAppContext) { - init_test(cx); - - 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(); - - // Try to update a tool call that doesn't exist - let nonexistent_id = acp::ToolCallId::new("nonexistent-tool-call"); - thread.update(cx, |thread, cx| { - let result = thread.handle_session_update( - acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new( - nonexistent_id.clone(), - acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::Completed), - )), - cx, - ); - - // The update should succeed (not return an error) - assert!(result.is_ok()); - - // There should now be exactly one entry in the thread - assert_eq!(thread.entries.len(), 1); - - // The entry should be a failed tool call - if let AgentThreadEntry::ToolCall(tool_call) = &thread.entries[0] { - assert_eq!(tool_call.id, nonexistent_id); - assert!(matches!(tool_call.status, ToolCallStatus::Failed)); - assert_eq!(tool_call.kind, acp::ToolKind::Fetch); - - // Check that the content contains the error message - assert_eq!(tool_call.content.len(), 1); - if let ToolCallContent::ContentBlock(content_block) = &tool_call.content[0] { - match content_block { - ContentBlock::Markdown { markdown } => { - let markdown_text = markdown.read(cx).source(); - assert!(markdown_text.contains("Tool call not found")); - } - ContentBlock::Empty => panic!("Expected markdown content, got empty"), - ContentBlock::ResourceLink { .. } => { - panic!("Expected markdown content, got resource link") - } - } - } else { - panic!("Expected ContentBlock, got: {:?}", tool_call.content[0]); - } - } else { - panic!("Expected ToolCall entry, got: {:?}", thread.entries[0]); - } - }); - } - - /// Tests that restoring a checkpoint properly cleans up terminals that were - /// created after that checkpoint, and cancels any in-progress generation. - /// - /// Reproduces issue #35142: When a checkpoint is restored, any terminal processes - /// that were started after that checkpoint should be terminated, and any in-progress - /// AI generation should be canceled. - #[gpui::test] - async fn test_restore_checkpoint_kills_terminal(cx: &mut TestAppContext) { - init_test(cx); - - 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(); - - // Send first user message to create a checkpoint - cx.update(|cx| { - thread.update(cx, |thread, cx| { - thread.send(vec!["first message".into()], cx) - }) - }) - .await - .unwrap(); - - // Send second message (creates another checkpoint) - we'll restore to this one - cx.update(|cx| { - thread.update(cx, |thread, cx| { - thread.send(vec!["second message".into()], cx) - }) - }) - .await - .unwrap(); - - // Create 2 terminals BEFORE the checkpoint that have completed running - let terminal_id_1 = acp::TerminalId::new(uuid::Uuid::new_v4().to_string()); - let mock_terminal_1 = cx.new(|cx| { - let builder = ::terminal::TerminalBuilder::new_display_only( - ::terminal::terminal_settings::CursorShape::default(), - ::terminal::terminal_settings::AlternateScroll::On, - None, - 0, - ) - .unwrap(); - builder.subscribe(cx) - }); - - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Created { - terminal_id: terminal_id_1.clone(), - label: "echo 'first'".to_string(), - cwd: Some(PathBuf::from("/test")), - output_byte_limit: None, - terminal: mock_terminal_1.clone(), - }, - cx, - ); - }); - - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Output { - terminal_id: terminal_id_1.clone(), - data: b"first\n".to_vec(), - }, - cx, - ); - }); - - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Exit { - terminal_id: terminal_id_1.clone(), - status: acp::TerminalExitStatus::new().exit_code(0), - }, - cx, - ); - }); - - let terminal_id_2 = acp::TerminalId::new(uuid::Uuid::new_v4().to_string()); - let mock_terminal_2 = cx.new(|cx| { - let builder = ::terminal::TerminalBuilder::new_display_only( - ::terminal::terminal_settings::CursorShape::default(), - ::terminal::terminal_settings::AlternateScroll::On, - None, - 0, - ) - .unwrap(); - builder.subscribe(cx) - }); - - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Created { - terminal_id: terminal_id_2.clone(), - label: "echo 'second'".to_string(), - cwd: Some(PathBuf::from("/test")), - output_byte_limit: None, - terminal: mock_terminal_2.clone(), - }, - cx, - ); - }); - - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Output { - terminal_id: terminal_id_2.clone(), - data: b"second\n".to_vec(), - }, - cx, - ); - }); - - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Exit { - terminal_id: terminal_id_2.clone(), - status: acp::TerminalExitStatus::new().exit_code(0), - }, - cx, - ); - }); - - // Get the second message ID to restore to - let second_message_id = thread.read_with(cx, |thread, _| { - // At this point we have: - // - Index 0: First user message (with checkpoint) - // - Index 1: Second user message (with checkpoint) - // No assistant responses because FakeAgentConnection just returns EndTurn - let AgentThreadEntry::UserMessage(message) = &thread.entries[1] else { - panic!("expected user message at index 1"); - }; - message.id.clone().unwrap() - }); - - // Create a terminal AFTER the checkpoint we'll restore to. - // This simulates the AI agent starting a long-running terminal command. - let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string()); - let mock_terminal = cx.new(|cx| { - let builder = ::terminal::TerminalBuilder::new_display_only( - ::terminal::terminal_settings::CursorShape::default(), - ::terminal::terminal_settings::AlternateScroll::On, - None, - 0, - ) - .unwrap(); - builder.subscribe(cx) - }); - - // Register the terminal as created - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Created { - terminal_id: terminal_id.clone(), - label: "sleep 1000".to_string(), - cwd: Some(PathBuf::from("/test")), - output_byte_limit: None, - terminal: mock_terminal.clone(), - }, - cx, - ); - }); - - // Simulate the terminal producing output (still running) - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Output { - terminal_id: terminal_id.clone(), - data: b"terminal is running...\n".to_vec(), - }, - cx, - ); - }); - - // Create a tool call entry that references this terminal - // This represents the agent requesting a terminal command - thread.update(cx, |thread, cx| { - thread - .handle_session_update( - acp::SessionUpdate::ToolCall( - acp::ToolCall::new("terminal-tool-1", "Running command") - .kind(acp::ToolKind::Execute) - .status(acp::ToolCallStatus::InProgress) - .content(vec![acp::ToolCallContent::Terminal(acp::Terminal::new( - terminal_id.clone(), - ))]) - .raw_input(serde_json::json!({"command": "sleep 1000", "cd": "/test"})), - ), - cx, - ) - .unwrap(); - }); - - // Verify terminal exists and is in the thread - let terminal_exists_before = - thread.read_with(cx, |thread, _| thread.terminals.contains_key(&terminal_id)); - assert!( - terminal_exists_before, - "Terminal should exist before checkpoint restore" - ); - - // Verify the terminal's underlying task is still running (not completed) - let terminal_running_before = thread.read_with(cx, |thread, _cx| { - let terminal_entity = thread.terminals.get(&terminal_id).unwrap(); - terminal_entity.read_with(cx, |term, _cx| { - term.output().is_none() // output is None means it's still running - }) - }); - assert!( - terminal_running_before, - "Terminal should be running before checkpoint restore" - ); - - // Verify we have the expected entries before restore - let entry_count_before = thread.read_with(cx, |thread, _| thread.entries.len()); - assert!( - entry_count_before > 1, - "Should have multiple entries before restore" - ); - - // Restore the checkpoint to the second message. - // This should: - // 1. Cancel any in-progress generation (via the cancel() call) - // 2. Remove the terminal that was created after that point - thread - .update(cx, |thread, cx| { - thread.restore_checkpoint(second_message_id, cx) - }) - .await - .unwrap(); - - // Verify that no send_task is in progress after restore - // (cancel() clears the send_task) - let has_send_task_after = thread.read_with(cx, |thread, _| thread.send_task.is_some()); - assert!( - !has_send_task_after, - "Should not have a send_task after restore (cancel should have cleared it)" - ); - - // Verify the entries were truncated (restoring to index 1 truncates at 1, keeping only index 0) - let entry_count = thread.read_with(cx, |thread, _| thread.entries.len()); - assert_eq!( - entry_count, 1, - "Should have 1 entry after restore (only the first user message)" - ); - - // Verify the 2 completed terminals from before the checkpoint still exist - let terminal_1_exists = thread.read_with(cx, |thread, _| { - thread.terminals.contains_key(&terminal_id_1) - }); - assert!( - terminal_1_exists, - "Terminal 1 (from before checkpoint) should still exist" - ); - - let terminal_2_exists = thread.read_with(cx, |thread, _| { - thread.terminals.contains_key(&terminal_id_2) - }); - assert!( - terminal_2_exists, - "Terminal 2 (from before checkpoint) should still exist" - ); - - // Verify they're still in completed state - let terminal_1_completed = thread.read_with(cx, |thread, _cx| { - let terminal_entity = thread.terminals.get(&terminal_id_1).unwrap(); - terminal_entity.read_with(cx, |term, _cx| term.output().is_some()) - }); - assert!(terminal_1_completed, "Terminal 1 should still be completed"); - - let terminal_2_completed = thread.read_with(cx, |thread, _cx| { - let terminal_entity = thread.terminals.get(&terminal_id_2).unwrap(); - terminal_entity.read_with(cx, |term, _cx| term.output().is_some()) - }); - assert!(terminal_2_completed, "Terminal 2 should still be completed"); - - // Verify the running terminal (created after checkpoint) was removed - let terminal_3_exists = - thread.read_with(cx, |thread, _| thread.terminals.contains_key(&terminal_id)); - assert!( - !terminal_3_exists, - "Terminal 3 (created after checkpoint) should have been removed" - ); - - // Verify total count is 2 (the two from before the checkpoint) - let terminal_count = thread.read_with(cx, |thread, _| thread.terminals.len()); - assert_eq!( - terminal_count, 2, - "Should have exactly 2 terminals (the completed ones from before checkpoint)" - ); - } -} diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs deleted file mode 100644 index 3c8c56b2c0..0000000000 --- a/crates/acp_thread/src/connection.rs +++ /dev/null @@ -1,470 +0,0 @@ -use crate::AcpThread; -use agent_client_protocol::{self as acp}; -use anyhow::Result; -use collections::IndexMap; -use gpui::{Entity, SharedString, Task}; -use language_model::LanguageModelProviderId; -use project::Project; -use serde::{Deserialize, Serialize}; -use std::{any::Any, error::Error, fmt, path::Path, rc::Rc, sync::Arc}; -use ui::{App, IconName}; -use uuid::Uuid; - -#[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()) - } -} - -pub trait AgentConnection { - fn telemetry_id(&self) -> SharedString; - - fn new_thread( - self: Rc, - project: Entity, - cwd: &Path, - cx: &mut App, - ) -> Task>>; - - fn auth_methods(&self) -> &[acp::AuthMethod]; - - 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 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, _session_id: &acp::SessionId) -> Option> { - None - } - - fn telemetry(&self) -> Option> { - None - } - - fn session_modes( - &self, - _session_id: &acp::SessionId, - _cx: &App, - ) -> 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 { - /// 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>; -} - -pub trait AgentSessionModes { - fn current_mode(&self) -> acp::SessionModeId; - - fn all_modes(&self) -> Vec; - - fn set_mode(&self, mode: acp::SessionModeId, 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 - /// - `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, model_id: acp::ModelId, cx: &mut App) -> Task>; - - /// Retrieves the currently selected model for a specific session (thread). - /// - /// # Parameters - /// - `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, cx: &mut App) -> Task>; - - /// Whenever the model list is updated the receiver will be notified. - /// Optional for agents that don't update their model list. - fn watch(&self, _cx: &mut App) -> Option> { - None - } - - /// Returns whether the model picker should render a footer. - fn should_render_footer(&self) -> bool { - false - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AgentModelInfo { - pub id: acp::ModelId, - pub name: SharedString, - pub description: Option, - pub icon: Option, -} - -impl From for AgentModelInfo { - fn from(info: acp::ModelInfo) -> Self { - Self { - id: info.model_id, - name: info.name.into(), - description: info.description.map(|desc| desc.into()), - icon: None, - } - } -} - -#[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 telemetry_id(&self) -> SharedString { - "stub".into() - } - - 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::new(self.sessions.lock().len().to_string()); - 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::new() - .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::new(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.tool_call_id) - { - Some((tool_call.clone(), options.clone())) - } else { - None - }; - let task = cx.spawn(async move |cx| { - if let Some((tool_call, options)) = permission_request { - thread - .update(cx, |thread, cx| { - thread.request_tool_call_authorization( - tool_call.clone().into(), - options.clone(), - false, - cx, - ) - })?? - .await; - } - thread.update(cx, |thread, cx| { - thread.handle_session_update(update.clone(), cx).unwrap(); - })?; - anyhow::Ok(()) - }); - tasks.push(task); - } - - cx.spawn(async move |_| { - try_join_all(tasks).await?; - Ok(acp::PromptResponse::new(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 f17e9d0fce..0000000000 --- a/crates/acp_thread/src/diff.rs +++ /dev/null @@ -1,432 +0,0 @@ -use anyhow::Result; -use buffer_diff::{BufferDiff, BufferDiffSnapshot}; -use editor::{MultiBuffer, PathKey, multibuffer_context_lines}; -use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task}; -use itertools::Itertools; -use language::{ - Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, Rope, TextBuffer, -}; -use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; -use util::ResultExt; - -pub enum Diff { - Pending(PendingDiff), - Finalized(FinalizedDiff), -} - -impl Diff { - pub fn finalized( - path: String, - 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 - .load_language_for_file_path(Path::new(&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_for_buffer(buffer.remote_id()) - ..Anchor::max_for_buffer(buffer.remote_id()), - buffer, - cx, - ) - .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer)) - .collect::>() - }; - - multibuffer.set_excerpts_for_path( - PathKey::for_buffer(&buffer, cx), - buffer.clone(), - hunk_ranges, - multibuffer_context_lines(cx), - 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().display(file.path_style(cx))), - Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_str().into()), - }; - format!( - "Diff: {}\n```\n{}\n```\n", - path.unwrap_or("untitled".into()), - 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 new_buffer = self.new_buffer.read(cx); - let language_registry = new_buffer.language_registry(); - - let path = new_buffer - .file() - .map(|file| file.path().display(file.path_style(cx))) - .unwrap_or("untitled".into()) - .into(); - let replica_id = new_buffer.replica_id(); - - // 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( - replica_id, - 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, - multibuffer_context_lines(cx), - 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, - multibuffer_context_lines(cx), - 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_for_buffer(buffer.remote_id()) - ..Anchor::max_for_buffer(buffer.remote_id()), - buffer, - cx, - ) - .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer)) - .collect::>(); - 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: String, - 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 c1b7032cfa..0000000000 --- a/crates/acp_thread/src/mention.rs +++ /dev/null @@ -1,551 +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}, -}; -use ui::{App, IconName, SharedString}; -use url::Url; -use util::paths::PathStyle; - -#[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, path_style: PathStyle) -> 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 = if path_style.is_windows() { - path.trim_start_matches("/") - } else { - 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.into(), - line_range, - }) - } else { - Ok(Self::Selection { - abs_path: Some(path.into()), - line_range, - }) - } - } else if input.ends_with("/") { - Ok(Self::Directory { - abs_path: path.into(), - }) - } else { - Ok(Self::File { - abs_path: path.into(), - }) - } - } - "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::new(thread_id), - 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 if let Some(name) = path.strip_prefix("/agent/symbol/") { - let fragment = url - .fragment() - .context("Missing fragment for untitled buffer selection")?; - let line_range = parse_line_range(fragment)?; - let path = - single_query_param(&url, "path")?.context("Missing path for symbol")?; - Ok(Self::Symbol { - name: name.to_string(), - abs_path: path.into(), - line_range, - }) - } else if path.starts_with("/agent/file") { - let path = - single_query_param(&url, "path")?.context("Missing path for file")?; - Ok(Self::File { - abs_path: path.into(), - }) - } else if path.starts_with("/agent/directory") { - let path = - single_query_param(&url, "path")?.context("Missing path for directory")?; - Ok(Self::Directory { - abs_path: path.into(), - }) - } else if path.starts_with("/agent/selection") { - let fragment = url.fragment().context("Missing fragment for selection")?; - let line_range = parse_line_range(fragment)?; - let path = - single_query_param(&url, "path")?.context("Missing path for selection")?; - Ok(Self::Selection { - abs_path: Some(path.into()), - 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 { abs_path } => FileIcons::get_folder_icon(false, abs_path, 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 } => { - let mut url = Url::parse("file:///").unwrap(); - url.set_path(&abs_path.to_string_lossy()); - url - } - MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(), - MentionUri::Directory { abs_path } => { - let mut url = Url::parse("file:///").unwrap(); - url.set_path(&abs_path.to_string_lossy()); - url - } - MentionUri::Symbol { - abs_path, - name, - line_range, - } => { - let mut url = Url::parse("file:///").unwrap(); - url.set_path(&abs_path.to_string_lossy()); - 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, - line_range, - } => { - let mut url = if let Some(path) = abs_path { - let mut url = Url::parse("file:///").unwrap(); - url.set_path(&path.to_string_lossy()); - url - } 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(), - } - } -} - -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, PathStyle::local()).unwrap(); - match &parsed { - MentionUri::File { abs_path } => { - assert_eq!(abs_path, Path::new(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, PathStyle::local()).unwrap(); - match &parsed { - MentionUri::Directory { abs_path } => { - assert_eq!(abs_path, Path::new(path!("/path/to/dir/"))); - } - _ => panic!("Expected Directory variant"), - } - assert_eq!(parsed.to_uri().to_string(), file_uri); - } - - #[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, PathStyle::local()).unwrap(); - match &parsed { - MentionUri::Symbol { - abs_path: path, - name, - line_range, - } => { - assert_eq!(path, Path::new(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, PathStyle::local()).unwrap(); - match &parsed { - MentionUri::Selection { - abs_path: path, - line_range, - } => { - assert_eq!(path.as_ref().unwrap(), Path::new(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, PathStyle::local()).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, PathStyle::local()).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, PathStyle::local()).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, PathStyle::local()).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, PathStyle::local()).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", PathStyle::local()).is_err()); - assert!(MentionUri::parse("ssh://example.com", PathStyle::local()).is_err()); - assert!(MentionUri::parse("unknown://example.com", PathStyle::local()).is_err()); - } - - #[test] - fn test_invalid_zed_path() { - assert!(MentionUri::parse("zed:///invalid/path", PathStyle::local()).is_err()); - assert!(MentionUri::parse("zed:///agent/unknown/test", PathStyle::local()).is_err()); - } - - #[test] - fn test_invalid_line_range_format() { - // Missing L prefix - assert!( - MentionUri::parse(uri!("file:///path/to/file.rs#10:20"), PathStyle::local()).is_err() - ); - - // Missing colon separator - assert!( - MentionUri::parse(uri!("file:///path/to/file.rs#L1020"), PathStyle::local()).is_err() - ); - - // Invalid numbers - assert!( - MentionUri::parse(uri!("file:///path/to/file.rs#L10:abc"), PathStyle::local()).is_err() - ); - assert!( - MentionUri::parse(uri!("file:///path/to/file.rs#Labc:20"), PathStyle::local()).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"), - PathStyle::local() - ) - .is_err() - ); - - // Too many query parameters - assert!( - MentionUri::parse( - uri!("file:///path/to/file.rs#L10:20?symbol=test&another=param"), - PathStyle::local() - ) - .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"), PathStyle::local()).is_err() - ); - assert!( - MentionUri::parse(uri!("file:///path/to/file.rs#L1:0"), PathStyle::local()).is_err() - ); - assert!( - MentionUri::parse(uri!("file:///path/to/file.rs#L0:0"), PathStyle::local()).is_err() - ); - } -} diff --git a/crates/acp_thread/src/terminal.rs b/crates/acp_thread/src/terminal.rs deleted file mode 100644 index 2da4125209..0000000000 --- a/crates/acp_thread/src/terminal.rs +++ /dev/null @@ -1,224 +0,0 @@ -use agent_client_protocol as acp; -use anyhow::Result; -use futures::{FutureExt as _, future::Shared}; -use gpui::{App, AppContext, AsyncApp, Context, Entity, Task}; -use language::LanguageRegistry; -use markdown::Markdown; -use project::Project; -use std::{path::PathBuf, process::ExitStatus, sync::Arc, time::Instant}; -use task::Shell; -use util::get_default_system_shell_preferring_bash; - -pub struct Terminal { - id: acp::TerminalId, - command: Entity, - working_dir: Option, - terminal: Entity, - started_at: Instant, - output: Option, - output_byte_limit: Option, - _output_task: Shared>, -} - -pub struct TerminalOutput { - pub ended_at: Instant, - pub exit_status: Option, - pub content: String, - pub original_content_len: usize, - pub content_line_count: usize, -} - -impl Terminal { - pub fn new( - id: acp::TerminalId, - command_label: &str, - working_dir: Option, - output_byte_limit: Option, - terminal: Entity, - language_registry: Arc, - cx: &mut Context, - ) -> Self { - let command_task = terminal.read(cx).wait_for_completed_task(cx); - Self { - id, - command: cx.new(|cx| { - Markdown::new( - format!("```\n{}\n```", command_label).into(), - Some(language_registry.clone()), - None, - cx, - ) - }), - working_dir, - terminal, - started_at: Instant::now(), - output: None, - output_byte_limit, - _output_task: cx - .spawn(async move |this, cx| { - let exit_status = command_task.await; - - this.update(cx, |this, cx| { - let (content, original_content_len) = this.truncated_output(cx); - let content_line_count = this.terminal.read(cx).total_lines(); - - this.output = Some(TerminalOutput { - ended_at: Instant::now(), - exit_status, - content, - original_content_len, - content_line_count, - }); - cx.notify(); - }) - .ok(); - - let exit_status = exit_status.map(portable_pty::ExitStatus::from); - - acp::TerminalExitStatus::new() - .exit_code(exit_status.as_ref().map(|e| e.exit_code())) - .signal(exit_status.and_then(|e| e.signal().map(ToOwned::to_owned))) - }) - .shared(), - } - } - - pub fn id(&self) -> &acp::TerminalId { - &self.id - } - - pub fn wait_for_exit(&self) -> Shared> { - self._output_task.clone() - } - - pub fn kill(&mut self, cx: &mut App) { - self.terminal.update(cx, |terminal, _cx| { - terminal.kill_active_task(); - }); - } - - pub fn current_output(&self, cx: &App) -> acp::TerminalOutputResponse { - if let Some(output) = self.output.as_ref() { - let exit_status = output.exit_status.map(portable_pty::ExitStatus::from); - - acp::TerminalOutputResponse::new( - output.content.clone(), - output.original_content_len > output.content.len(), - ) - .exit_status( - acp::TerminalExitStatus::new() - .exit_code(exit_status.as_ref().map(|e| e.exit_code())) - .signal(exit_status.and_then(|e| e.signal().map(ToOwned::to_owned))), - ) - } else { - let (current_content, original_len) = self.truncated_output(cx); - let truncated = current_content.len() < original_len; - acp::TerminalOutputResponse::new(current_content, truncated) - } - } - - fn truncated_output(&self, cx: &App) -> (String, usize) { - let terminal = self.terminal.read(cx); - let mut content = terminal.get_content(); - - let original_content_len = content.len(); - - if let Some(limit) = self.output_byte_limit - && content.len() > limit - { - let mut end_ix = limit.min(content.len()); - while !content.is_char_boundary(end_ix) { - end_ix -= 1; - } - // Don't truncate mid-line, clear the remainder of the last line - end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix); - content.truncate(end_ix); - } - - (content, original_content_len) - } - - pub fn command(&self) -> &Entity { - &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() - ) - } -} - -pub async fn create_terminal_entity( - command: String, - args: &[String], - env_vars: Vec<(String, String)>, - cwd: Option, - project: &Entity, - cx: &mut AsyncApp, -) -> Result> { - let mut env = if let Some(dir) = &cwd { - project - .update(cx, |project, cx| { - project.environment().update(cx, |env, cx| { - env.directory_environment(dir.clone().into(), cx) - }) - })? - .await - .unwrap_or_default() - } else { - Default::default() - }; - - // Disables paging for `git` and hopefully other commands - env.insert("PAGER".into(), "".into()); - env.extend(env_vars); - - // Use remote shell or default system shell, as appropriate - let shell = project - .update(cx, |project, cx| { - project - .remote_client() - .and_then(|r| r.read(cx).default_system_shell()) - .map(Shell::Program) - })? - .unwrap_or_else(|| Shell::Program(get_default_system_shell_preferring_bash())); - let is_windows = project - .read_with(cx, |project, cx| project.path_style(cx).is_windows()) - .unwrap_or(cfg!(windows)); - let (task_command, task_args) = task::ShellBuilder::new(&shell, is_windows) - .redirect_stdin_to_dev_null() - .build(Some(command.clone()), &args); - - project - .update(cx, |project, cx| { - project.create_terminal_task( - task::SpawnInTerminal { - command: Some(task_command), - args: task_args, - cwd, - env, - ..Default::default() - }, - cx, - ) - })? - .await -} diff --git a/crates/acp_tools/Cargo.toml b/crates/acp_tools/Cargo.toml deleted file mode 100644 index 0720c4b668..0000000000 --- a/crates/acp_tools/Cargo.toml +++ /dev/null @@ -1,29 +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.workspace = true diff --git a/crates/acp_tools/LICENSE-GPL b/crates/acp_tools/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/acp_tools/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs deleted file mode 100644 index 0905effce3..0000000000 --- a/crates/acp_tools/src/acp_tools.rs +++ /dev/null @@ -1,643 +0,0 @@ -use std::{ - cell::RefCell, - collections::HashSet, - fmt::Display, - rc::{Rc, Weak}, - sync::Arc, - time::Duration, -}; - -use agent_client_protocol as acp; -use collections::HashMap; -use gpui::{ - App, ClipboardItem, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, - ListState, StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, - prelude::*, -}; -use language::LanguageRegistry; -use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle}; -use project::Project; -use settings::Settings; -use theme::ThemeSettings; -use ui::{Tooltip, WithScrollbar, prelude::*}; -use util::ResultExt as _; -use workspace::{ - Item, ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, 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.clone(), 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 serialize_observed_messages(&self) -> Option { - let connection = self.watched_connection.as_ref()?; - - let messages: Vec = connection - .messages - .iter() - .filter_map(|message| { - let params = match &message.params { - Ok(Some(params)) => params.clone(), - Ok(None) => serde_json::Value::Null, - Err(err) => serde_json::to_value(err).ok()?, - }; - Some(serde_json::json!({ - "_direction": match message.direction { - acp::StreamMessageDirection::Incoming => "incoming", - acp::StreamMessageDirection::Outgoing => "outgoing", - }, - "_type": message.message_type.to_string().to_lowercase(), - "id": message.request_id, - "method": message.name.to_string(), - "params": params, - })) - }) - .collect(); - - serde_json::to_string_pretty(&messages).ok() - } - - fn clear_messages(&mut self, cx: &mut Context) { - if let Some(connection) = self.watched_connection.as_mut() { - connection.messages.clear(); - connection.list_state.reset(0); - self.expanded.clear(); - 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() - .id(index) - .group("message") - .cursor_pointer() - .font_buffer(cx) - .w_full() - .py_3() - .pl_4() - .pr_5() - .gap_2() - .items_start() - .text_size(base_size) - .border_color(colors.border) - .border_b_1() - .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() - .flex_shrink_0() - .child(match message.direction { - acp::StreamMessageDirection::Incoming => Icon::new(IconName::ArrowDown) - .color(Color::Error) - .size(IconSize::Small), - acp::StreamMessageDirection::Outgoing => Icon::new(IconName::ArrowUp) - .color(Color::Success) - .size(IconSize::Small), - }) - .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 - .as_ref() - .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 { - div() - .size_full() - .flex_grow() - .child( - list( - connection.list_state.clone(), - cx.processor(Self::render_message), - ) - .with_sizing_behavior(gpui::ListSizingBehavior::Auto) - .size_full(), - ) - .vertical_scrollbar_for(&connection.list_state, window, cx) - .into_any() - } - } - None => h_flex() - .size_full() - .justify_center() - .items_center() - .child("No active connection") - .into_any(), - }) - } -} - -pub struct AcpToolsToolbarItemView { - acp_tools: Option>, - just_copied: bool, -} - -impl AcpToolsToolbarItemView { - pub fn new() -> Self { - Self { - acp_tools: None, - just_copied: false, - } - } -} - -impl Render for AcpToolsToolbarItemView { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let Some(acp_tools) = self.acp_tools.as_ref() else { - return Empty.into_any_element(); - }; - - let acp_tools = acp_tools.clone(); - let has_messages = acp_tools - .read(cx) - .watched_connection - .as_ref() - .is_some_and(|connection| !connection.messages.is_empty()); - - h_flex() - .gap_2() - .child({ - let acp_tools = acp_tools.clone(); - IconButton::new( - "copy_all_messages", - if self.just_copied { - IconName::Check - } else { - IconName::Copy - }, - ) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text(if self.just_copied { - "Copied!" - } else { - "Copy All Messages" - })) - .disabled(!has_messages) - .on_click(cx.listener(move |this, _, _window, cx| { - if let Some(content) = acp_tools.read(cx).serialize_observed_messages() { - cx.write_to_clipboard(ClipboardItem::new_string(content)); - - this.just_copied = true; - cx.spawn(async move |this, cx| { - cx.background_executor().timer(Duration::from_secs(2)).await; - this.update(cx, |this, cx| { - this.just_copied = false; - cx.notify(); - }) - }) - .detach(); - } - })) - }) - .child( - IconButton::new("clear_messages", IconName::Trash) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Clear Messages")) - .disabled(!has_messages) - .on_click(cx.listener(move |_this, _, _window, cx| { - acp_tools.update(cx, |acp_tools, cx| { - acp_tools.clear_messages(cx); - }); - })), - ) - .into_any() - } -} - -impl EventEmitter for AcpToolsToolbarItemView {} - -impl ToolbarItemView for AcpToolsToolbarItemView { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - _window: &mut Window, - cx: &mut Context, - ) -> ToolbarItemLocation { - if let Some(item) = active_pane_item - && let Some(acp_tools) = item.downcast::() - { - self.acp_tools = Some(acp_tools); - cx.notify(); - return ToolbarItemLocation::PrimaryRight; - } - if self.acp_tools.take().is_some() { - cx.notify(); - } - ToolbarItemLocation::Hidden - } -} diff --git a/crates/action_log/Cargo.toml b/crates/action_log/Cargo.toml deleted file mode 100644 index 699d548593..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 -telemetry.workspace = true -text.workspace = true -util.workspace = true -watch.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/action_log/LICENSE-GPL b/crates/action_log/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/action_log/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs deleted file mode 100644 index 6eb18a4f12..0000000000 --- a/crates/action_log/src/action_log.rs +++ /dev/null @@ -1,2407 +0,0 @@ -use anyhow::{Context as _, Result}; -use buffer_diff::BufferDiff; -use clock; -use collections::BTreeMap; -use futures::{FutureExt, StreamExt, channel::mpsc}; -use gpui::{ - App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity, -}; -use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint}; -use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle}; -use std::{cmp, ops::Range, sync::Arc}; -use text::{Edit, Patch, Rope}; -use util::{RangeExt, ResultExt as _}; - -/// Tracks actions performed by tools in a thread -pub struct ActionLog { - /// Buffers that we want to notify the model about when they change. - tracked_buffers: BTreeMap, TrackedBuffer>, - /// The project this action log is associated with - project: Entity, -} - -impl ActionLog { - /// Creates a new, empty action log associated with the given project. - pub fn new(project: Entity) -> Self { - Self { - tracked_buffers: BTreeMap::default(), - project, - } - } - - pub fn project(&self) -> &Entity { - &self.project - } - - fn track_buffer_internal( - &mut self, - buffer: Entity, - is_created: bool, - cx: &mut Context, - ) -> &mut TrackedBuffer { - let status = if is_created { - if let Some(tracked) = self.tracked_buffers.remove(&buffer) { - match tracked.status { - TrackedBufferStatus::Created { - existing_file_content, - } => TrackedBufferStatus::Created { - existing_file_content, - }, - TrackedBufferStatus::Modified | TrackedBufferStatus::Deleted => { - TrackedBufferStatus::Created { - existing_file_content: Some(tracked.diff_base), - } - } - } - } else if buffer - .read(cx) - .file() - .is_some_and(|file| file.disk_state().exists()) - { - TrackedBufferStatus::Created { - existing_file_content: Some(buffer.read(cx).as_rope().clone()), - } - } else { - TrackedBufferStatus::Created { - existing_file_content: None, - } - } - } else { - TrackedBufferStatus::Modified - }; - - let tracked_buffer = self - .tracked_buffers - .entry(buffer.clone()) - .or_insert_with(|| { - let open_lsp_handle = self.project.update(cx, |project, cx| { - project.register_buffer_with_language_servers(&buffer, cx) - }); - - let text_snapshot = buffer.read(cx).text_snapshot(); - let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx)); - let (diff_update_tx, diff_update_rx) = mpsc::unbounded(); - let diff_base; - let unreviewed_edits; - if is_created { - diff_base = Rope::default(); - unreviewed_edits = Patch::new(vec![Edit { - old: 0..1, - new: 0..text_snapshot.max_point().row + 1, - }]) - } else { - diff_base = buffer.read(cx).as_rope().clone(); - unreviewed_edits = Patch::default(); - } - TrackedBuffer { - buffer: buffer.clone(), - diff_base, - unreviewed_edits, - snapshot: text_snapshot, - status, - version: buffer.read(cx).version(), - diff, - diff_update: diff_update_tx, - _open_lsp_handle: open_lsp_handle, - _maintain_diff: cx.spawn({ - let buffer = buffer.clone(); - async move |this, cx| { - Self::maintain_diff(this, buffer, diff_update_rx, cx) - .await - .ok(); - } - }), - _subscription: cx.subscribe(&buffer, Self::handle_buffer_event), - } - }); - tracked_buffer.version = buffer.read(cx).version(); - tracked_buffer - } - - fn handle_buffer_event( - &mut self, - buffer: Entity, - event: &BufferEvent, - cx: &mut Context, - ) { - match event { - BufferEvent::Edited => self.handle_buffer_edited(buffer, cx), - BufferEvent::FileHandleChanged => { - self.handle_buffer_file_changed(buffer, cx); - } - _ => {} - }; - } - - fn handle_buffer_edited(&mut self, buffer: Entity, cx: &mut Context) { - let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { - return; - }; - tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); - } - - fn handle_buffer_file_changed(&mut self, buffer: Entity, cx: &mut Context) { - let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { - return; - }; - - match tracked_buffer.status { - TrackedBufferStatus::Created { .. } | TrackedBufferStatus::Modified => { - if buffer - .read(cx) - .file() - .is_some_and(|file| file.disk_state() == DiskState::Deleted) - { - // If the buffer had been edited by a tool, but it got - // deleted externally, we want to stop tracking it. - self.tracked_buffers.remove(&buffer); - } - cx.notify(); - } - TrackedBufferStatus::Deleted => { - if buffer - .read(cx) - .file() - .is_some_and(|file| file.disk_state() != DiskState::Deleted) - { - // If the buffer had been deleted by a tool, but it got - // resurrected externally, we want to clear the edits we - // were tracking and reset the buffer's state. - self.tracked_buffers.remove(&buffer); - self.track_buffer_internal(buffer, false, cx); - } - cx.notify(); - } - } - } - - async fn maintain_diff( - this: WeakEntity, - buffer: Entity, - mut buffer_updates: mpsc::UnboundedReceiver<(ChangeAuthor, text::BufferSnapshot)>, - cx: &mut AsyncApp, - ) -> Result<()> { - let git_store = this.read_with(cx, |this, cx| this.project.read(cx).git_store().clone())?; - let git_diff = this - .update(cx, |this, cx| { - this.project.update(cx, |project, cx| { - project.open_uncommitted_diff(buffer.clone(), cx) - }) - })? - .await - .ok(); - let buffer_repo = git_store.read_with(cx, |git_store, cx| { - git_store.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx) - })?; - - let (mut git_diff_updates_tx, mut git_diff_updates_rx) = watch::channel(()); - let _repo_subscription = - 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 { - 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 { - None - }; - - loop { - futures::select_biased! { - buffer_update = buffer_updates.next() => { - if let Some((author, buffer_snapshot)) = buffer_update { - Self::track_edits(&this, &buffer, author, buffer_snapshot, cx).await?; - } else { - break; - } - } - _ = 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?; - } - } - } - } - - Ok(()) - } - - async fn track_edits( - this: &WeakEntity, - buffer: &Entity, - author: ChangeAuthor, - buffer_snapshot: text::BufferSnapshot, - cx: &mut AsyncApp, - ) -> Result<()> { - let rebase = this.update(cx, |this, cx| { - let tracked_buffer = this - .tracked_buffers - .get_mut(buffer) - .context("buffer not tracked")?; - - let rebase = cx.background_spawn({ - let mut base_text = tracked_buffer.diff_base.clone(); - let old_snapshot = tracked_buffer.snapshot.clone(); - let new_snapshot = buffer_snapshot.clone(); - let unreviewed_edits = tracked_buffer.unreviewed_edits.clone(); - let edits = diff_snapshots(&old_snapshot, &new_snapshot); - async move { - if let ChangeAuthor::User = author { - apply_non_conflicting_edits( - &unreviewed_edits, - edits, - &mut base_text, - new_snapshot.as_rope(), - ); - } - - (Arc::new(base_text.to_string()), base_text) - } - }); - - anyhow::Ok(rebase) - })??; - let (new_base_text, new_diff_base) = rebase.await; - - Self::update_diff( - this, - buffer, - buffer_snapshot, - new_base_text, - new_diff_base, - cx, - ) - .await - } - - async fn keep_committed_edits( - this: &WeakEntity, - buffer: &Entity, - git_diff: &Entity, - cx: &mut AsyncApp, - ) -> Result<()> { - let buffer_snapshot = this.read_with(cx, |this, _cx| { - let tracked_buffer = this - .tracked_buffers - .get(buffer) - .context("buffer not tracked")?; - anyhow::Ok(tracked_buffer.snapshot.clone()) - })??; - let (new_base_text, new_diff_base) = this - .read_with(cx, |this, cx| { - let tracked_buffer = this - .tracked_buffers - .get(buffer) - .context("buffer not tracked")?; - let old_unreviewed_edits = tracked_buffer.unreviewed_edits.clone(); - let agent_diff_base = tracked_buffer.diff_base.clone(); - let git_diff_base = git_diff.read(cx).base_text().as_rope().clone(); - let buffer_text = tracked_buffer.snapshot.as_rope().clone(); - anyhow::Ok(cx.background_spawn(async move { - let mut old_unreviewed_edits = old_unreviewed_edits.into_iter().peekable(); - let committed_edits = language::line_diff( - &agent_diff_base.to_string(), - &git_diff_base.to_string(), - ) - .into_iter() - .map(|(old, new)| Edit { old, new }); - - let mut new_agent_diff_base = agent_diff_base.clone(); - let mut row_delta = 0i32; - for committed in committed_edits { - while let Some(unreviewed) = old_unreviewed_edits.peek() { - // If the committed edit matches the unreviewed - // edit, assume the user wants to keep it. - if committed.old == unreviewed.old { - let unreviewed_new = - buffer_text.slice_rows(unreviewed.new.clone()).to_string(); - let committed_new = - git_diff_base.slice_rows(committed.new.clone()).to_string(); - if unreviewed_new == committed_new { - let old_byte_start = - new_agent_diff_base.point_to_offset(Point::new( - (unreviewed.old.start as i32 + row_delta) as u32, - 0, - )); - let old_byte_end = - new_agent_diff_base.point_to_offset(cmp::min( - Point::new( - (unreviewed.old.end as i32 + row_delta) as u32, - 0, - ), - new_agent_diff_base.max_point(), - )); - new_agent_diff_base - .replace(old_byte_start..old_byte_end, &unreviewed_new); - row_delta += - unreviewed.new_len() as i32 - unreviewed.old_len() as i32; - } - } else if unreviewed.old.start >= committed.old.end { - break; - } - - old_unreviewed_edits.next().unwrap(); - } - } - - ( - Arc::new(new_agent_diff_base.to_string()), - new_agent_diff_base, - ) - })) - })?? - .await; - - Self::update_diff( - this, - buffer, - buffer_snapshot, - new_base_text, - new_diff_base, - cx, - ) - .await - } - - async fn update_diff( - this: &WeakEntity, - buffer: &Entity, - buffer_snapshot: text::BufferSnapshot, - new_base_text: Arc, - new_diff_base: Rope, - cx: &mut AsyncApp, - ) -> Result<()> { - let (diff, language, language_registry) = this.read_with(cx, |this, cx| { - let tracked_buffer = this - .tracked_buffers - .get(buffer) - .context("buffer not tracked")?; - anyhow::Ok(( - tracked_buffer.diff.clone(), - buffer.read(cx).language().cloned(), - buffer.read(cx).language_registry(), - )) - })??; - let diff_snapshot = BufferDiff::update_diff( - diff.clone(), - buffer_snapshot.clone(), - Some(new_base_text), - true, - false, - language, - language_registry, - cx, - ) - .await; - let mut unreviewed_edits = Patch::default(); - if let Ok(diff_snapshot) = diff_snapshot { - unreviewed_edits = cx - .background_spawn({ - let diff_snapshot = diff_snapshot.clone(); - let buffer_snapshot = buffer_snapshot.clone(); - let new_diff_base = new_diff_base.clone(); - async move { - let mut unreviewed_edits = Patch::default(); - for hunk in diff_snapshot.hunks_intersecting_range( - Anchor::min_for_buffer(buffer_snapshot.remote_id()) - ..Anchor::max_for_buffer(buffer_snapshot.remote_id()), - &buffer_snapshot, - ) { - let old_range = new_diff_base - .offset_to_point(hunk.diff_base_byte_range.start) - ..new_diff_base.offset_to_point(hunk.diff_base_byte_range.end); - let new_range = hunk.range.start..hunk.range.end; - unreviewed_edits.push(point_to_row_edit( - Edit { - old: old_range, - new: new_range, - }, - &new_diff_base, - buffer_snapshot.as_rope(), - )); - } - unreviewed_edits - } - }) - .await; - - diff.update(cx, |diff, cx| { - diff.set_snapshot(diff_snapshot, &buffer_snapshot, cx); - })?; - } - this.update(cx, |this, cx| { - let tracked_buffer = this - .tracked_buffers - .get_mut(buffer) - .context("buffer not tracked")?; - tracked_buffer.diff_base = new_diff_base; - tracked_buffer.snapshot = buffer_snapshot; - tracked_buffer.unreviewed_edits = unreviewed_edits; - cx.notify(); - anyhow::Ok(()) - })? - } - - /// Track a buffer as read by agent, so we can notify the model about user edits. - pub fn buffer_read(&mut self, buffer: Entity, cx: &mut Context) { - self.track_buffer_internal(buffer, false, cx); - } - - /// 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); - } - - /// 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); - if let TrackedBufferStatus::Deleted = tracked_buffer.status { - tracked_buffer.status = TrackedBufferStatus::Modified; - } - tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx); - } - - pub fn will_delete_buffer(&mut self, buffer: Entity, cx: &mut Context) { - let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx); - match tracked_buffer.status { - TrackedBufferStatus::Created { .. } => { - self.tracked_buffers.remove(&buffer); - cx.notify(); - } - TrackedBufferStatus::Modified => { - buffer.update(cx, |buffer, cx| buffer.set_text("", cx)); - tracked_buffer.status = TrackedBufferStatus::Deleted; - tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx); - } - TrackedBufferStatus::Deleted => {} - } - cx.notify(); - } - - pub fn keep_edits_in_range( - &mut self, - buffer: Entity, - buffer_range: Range, - telemetry: Option, - cx: &mut Context, - ) { - let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { - return; - }; - - let mut metrics = ActionLogMetrics::for_buffer(buffer.read(cx)); - match tracked_buffer.status { - TrackedBufferStatus::Deleted => { - metrics.add_edits(tracked_buffer.unreviewed_edits.edits()); - self.tracked_buffers.remove(&buffer); - cx.notify(); - } - _ => { - let buffer = buffer.read(cx); - let buffer_range = - buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer); - let mut delta = 0i32; - tracked_buffer.unreviewed_edits.retain_mut(|edit| { - edit.old.start = (edit.old.start as i32 + delta) as u32; - edit.old.end = (edit.old.end as i32 + delta) as u32; - - if buffer_range.end.row < edit.new.start - || buffer_range.start.row > edit.new.end - { - true - } else { - let old_range = tracked_buffer - .diff_base - .point_to_offset(Point::new(edit.old.start, 0)) - ..tracked_buffer.diff_base.point_to_offset(cmp::min( - Point::new(edit.old.end, 0), - tracked_buffer.diff_base.max_point(), - )); - let new_range = tracked_buffer - .snapshot - .point_to_offset(Point::new(edit.new.start, 0)) - ..tracked_buffer.snapshot.point_to_offset(cmp::min( - Point::new(edit.new.end, 0), - tracked_buffer.snapshot.max_point(), - )); - tracked_buffer.diff_base.replace( - old_range, - &tracked_buffer - .snapshot - .text_for_range(new_range) - .collect::(), - ); - delta += edit.new_len() as i32 - edit.old_len() as i32; - metrics.add_edit(edit); - 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); - } - } - if let Some(telemetry) = telemetry { - telemetry_report_accepted_edits(&telemetry, metrics); - } - } - - pub fn reject_edits_in_ranges( - &mut self, - buffer: Entity, - buffer_ranges: Vec>, - telemetry: Option, - cx: &mut Context, - ) -> Task> { - let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { - return Task::ready(Ok(())); - }; - - let mut metrics = ActionLogMetrics::for_buffer(buffer.read(cx)); - let task = match &tracked_buffer.status { - TrackedBufferStatus::Created { - existing_file_content, - } => { - let task = if let Some(existing_file_content) = existing_file_content { - buffer.update(cx, |buffer, cx| { - buffer.start_transaction(); - buffer.set_text("", cx); - for chunk in existing_file_content.chunks() { - buffer.append(chunk, cx); - } - buffer.end_transaction(cx); - }); - self.project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - } else { - // For a file created by AI with no pre-existing content, - // only delete the file if we're certain it contains only AI content - // with no edits from the user. - - let initial_version = tracked_buffer.version.clone(); - let current_version = buffer.read(cx).version(); - - let current_content = buffer.read(cx).text(); - let tracked_content = tracked_buffer.snapshot.text(); - - let is_ai_only_content = - initial_version == current_version && current_content == tracked_content; - - if is_ai_only_content { - buffer - .read(cx) - .entry_id(cx) - .and_then(|entry_id| { - self.project.update(cx, |project, cx| { - project.delete_entry(entry_id, false, cx) - }) - }) - .unwrap_or(Task::ready(Ok(()))) - } else { - // Not sure how to disentangle edits made by the user - // from edits made by the AI at this point. - // For now, preserve both to avoid data loss. - // - // TODO: Better solution (disable "Reject" after user makes some - // edit or find a way to differentiate between AI and user edits) - Task::ready(Ok(())) - } - }; - - metrics.add_edits(tracked_buffer.unreviewed_edits.edits()); - self.tracked_buffers.remove(&buffer); - cx.notify(); - task - } - TrackedBufferStatus::Deleted => { - buffer.update(cx, |buffer, cx| { - buffer.set_text(tracked_buffer.diff_base.to_string(), cx) - }); - let save = self - .project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)); - - // Clear all tracked edits for this buffer and start over as if we just read it. - metrics.add_edits(tracked_buffer.unreviewed_edits.edits()); - self.tracked_buffers.remove(&buffer); - self.buffer_read(buffer.clone(), cx); - cx.notify(); - save - } - TrackedBufferStatus::Modified => { - buffer.update(cx, |buffer, cx| { - let mut buffer_row_ranges = buffer_ranges - .into_iter() - .map(|range| { - range.start.to_point(buffer).row..range.end.to_point(buffer).row - }) - .peekable(); - - let mut edits_to_revert = Vec::new(); - for edit in tracked_buffer.unreviewed_edits.edits() { - let new_range = tracked_buffer - .snapshot - .anchor_before(Point::new(edit.new.start, 0)) - ..tracked_buffer.snapshot.anchor_after(cmp::min( - Point::new(edit.new.end, 0), - tracked_buffer.snapshot.max_point(), - )); - let new_row_range = new_range.start.to_point(buffer).row - ..new_range.end.to_point(buffer).row; - - let mut revert = false; - while let Some(buffer_row_range) = buffer_row_ranges.peek() { - if buffer_row_range.end < new_row_range.start { - buffer_row_ranges.next(); - } else if buffer_row_range.start > new_row_range.end { - break; - } else { - revert = true; - break; - } - } - - if revert { - metrics.add_edit(edit); - let old_range = tracked_buffer - .diff_base - .point_to_offset(Point::new(edit.old.start, 0)) - ..tracked_buffer.diff_base.point_to_offset(cmp::min( - Point::new(edit.old.end, 0), - tracked_buffer.diff_base.max_point(), - )); - let old_text = tracked_buffer - .diff_base - .chunks_in_range(old_range) - .collect::(); - edits_to_revert.push((new_range, old_text)); - } - } - - buffer.edit(edits_to_revert, None, cx); - }); - self.project - .update(cx, |project, cx| project.save_buffer(buffer, cx)) - } - }; - if let Some(telemetry) = telemetry { - telemetry_report_rejected_edits(&telemetry, metrics); - } - task - } - - pub fn keep_all_edits( - &mut self, - telemetry: Option, - cx: &mut Context, - ) { - self.tracked_buffers.retain(|buffer, tracked_buffer| { - let mut metrics = ActionLogMetrics::for_buffer(buffer.read(cx)); - metrics.add_edits(tracked_buffer.unreviewed_edits.edits()); - if let Some(telemetry) = telemetry.as_ref() { - telemetry_report_accepted_edits(telemetry, metrics); - } - 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); - true - } - } - }); - - cx.notify(); - } - - pub fn reject_all_edits( - &mut self, - telemetry: Option, - cx: &mut Context, - ) -> Task<()> { - let futures = self.changed_buffers(cx).into_keys().map(|buffer| { - let buffer_ranges = vec![Anchor::min_max_range_for_buffer( - buffer.read(cx).remote_id(), - )]; - let reject = self.reject_edits_in_ranges(buffer, buffer_ranges, telemetry.clone(), cx); - - async move { - reject.await.log_err(); - } - }); - - let task = futures::future::join_all(futures); - cx.background_spawn(async move { - task.await; - }) - } - - /// Returns the set of buffers that contain edits that haven't been reviewed by the user. - pub fn changed_buffers(&self, cx: &App) -> BTreeMap, Entity> { - self.tracked_buffers - .iter() - .filter(|(_, tracked)| tracked.has_edits(cx)) - .map(|(buffer, tracked)| (buffer.clone(), tracked.diff.clone())) - .collect() - } - - /// Iterate over buffers changed since last read or edited by the model - pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator> { - self.tracked_buffers - .iter() - .filter(|(buffer, tracked)| { - let buffer = buffer.read(cx); - - tracked.version != buffer.version - && buffer - .file() - .is_some_and(|file| file.disk_state() != DiskState::Deleted) - }) - .map(|(buffer, _)| buffer) - } -} - -#[derive(Clone)] -pub struct ActionLogTelemetry { - pub agent_telemetry_id: SharedString, - pub session_id: Arc, -} - -struct ActionLogMetrics { - lines_removed: u32, - lines_added: u32, - language: Option, -} - -impl ActionLogMetrics { - fn for_buffer(buffer: &Buffer) -> Self { - Self { - language: buffer.language().map(|l| l.name().0), - lines_removed: 0, - lines_added: 0, - } - } - - fn add_edits(&mut self, edits: &[Edit]) { - for edit in edits { - self.add_edit(edit); - } - } - - fn add_edit(&mut self, edit: &Edit) { - self.lines_added += edit.new_len(); - self.lines_removed += edit.old_len(); - } -} - -fn telemetry_report_accepted_edits(telemetry: &ActionLogTelemetry, metrics: ActionLogMetrics) { - telemetry::event!( - "Agent Edits Accepted", - agent = telemetry.agent_telemetry_id, - session = telemetry.session_id, - language = metrics.language, - lines_added = metrics.lines_added, - lines_removed = metrics.lines_removed - ); -} - -fn telemetry_report_rejected_edits(telemetry: &ActionLogTelemetry, metrics: ActionLogMetrics) { - telemetry::event!( - "Agent Edits Rejected", - agent = telemetry.agent_telemetry_id, - session = telemetry.session_id, - language = metrics.language, - lines_added = metrics.lines_added, - lines_removed = metrics.lines_removed - ); -} - -fn apply_non_conflicting_edits( - patch: &Patch, - edits: Vec>, - old_text: &mut Rope, - new_text: &Rope, -) -> bool { - let mut old_edits = patch.edits().iter().cloned().peekable(); - let mut new_edits = edits.into_iter().peekable(); - let mut applied_delta = 0i32; - let mut rebased_delta = 0i32; - let mut has_made_changes = false; - - while let Some(mut new_edit) = new_edits.next() { - let mut conflict = false; - - // Push all the old edits that are before this new edit or that intersect with it. - while let Some(old_edit) = old_edits.peek() { - if new_edit.old.end < old_edit.new.start - || (!old_edit.new.is_empty() && new_edit.old.end == old_edit.new.start) - { - break; - } else if new_edit.old.start > old_edit.new.end - || (!old_edit.new.is_empty() && new_edit.old.start == old_edit.new.end) - { - let old_edit = old_edits.next().unwrap(); - rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32; - } else { - conflict = true; - if new_edits - .peek() - .is_some_and(|next_edit| next_edit.old.overlaps(&old_edit.new)) - { - new_edit = new_edits.next().unwrap(); - } else { - let old_edit = old_edits.next().unwrap(); - rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32; - } - } - } - - if !conflict { - // This edit doesn't intersect with any old edit, so we can apply it to the old text. - new_edit.old.start = (new_edit.old.start as i32 + applied_delta - rebased_delta) as u32; - new_edit.old.end = (new_edit.old.end as i32 + applied_delta - rebased_delta) as u32; - let old_bytes = old_text.point_to_offset(Point::new(new_edit.old.start, 0)) - ..old_text.point_to_offset(cmp::min( - Point::new(new_edit.old.end, 0), - old_text.max_point(), - )); - let new_bytes = new_text.point_to_offset(Point::new(new_edit.new.start, 0)) - ..new_text.point_to_offset(cmp::min( - Point::new(new_edit.new.end, 0), - new_text.max_point(), - )); - - old_text.replace( - old_bytes, - &new_text.chunks_in_range(new_bytes).collect::(), - ); - applied_delta += new_edit.new_len() as i32 - new_edit.old_len() as i32; - has_made_changes = true; - } - } - has_made_changes -} - -fn diff_snapshots( - old_snapshot: &text::BufferSnapshot, - new_snapshot: &text::BufferSnapshot, -) -> Vec> { - let mut edits = new_snapshot - .edits_since::(&old_snapshot.version) - .map(|edit| point_to_row_edit(edit, old_snapshot.as_rope(), new_snapshot.as_rope())) - .peekable(); - let mut row_edits = Vec::new(); - while let Some(mut edit) = edits.next() { - while let Some(next_edit) = edits.peek() { - if edit.old.end >= next_edit.old.start { - edit.old.end = next_edit.old.end; - edit.new.end = next_edit.new.end; - edits.next(); - } else { - break; - } - } - row_edits.push(edit); - } - row_edits -} - -fn point_to_row_edit(edit: Edit, old_text: &Rope, new_text: &Rope) -> Edit { - if edit.old.start.column == old_text.line_len(edit.old.start.row) - && new_text - .chars_at(new_text.point_to_offset(edit.new.start)) - .next() - == Some('\n') - && edit.old.start != old_text.max_point() - { - Edit { - old: edit.old.start.row + 1..edit.old.end.row + 1, - new: edit.new.start.row + 1..edit.new.end.row + 1, - } - } else if edit.old.start.column == 0 && edit.old.end.column == 0 && edit.new.end.column == 0 { - Edit { - old: edit.old.start.row..edit.old.end.row, - new: edit.new.start.row..edit.new.end.row, - } - } else { - Edit { - old: edit.old.start.row..edit.old.end.row + 1, - new: edit.new.start.row..edit.new.end.row + 1, - } - } -} - -#[derive(Copy, Clone, Debug)] -enum ChangeAuthor { - User, - Agent, -} - -enum TrackedBufferStatus { - Created { existing_file_content: Option }, - Modified, - Deleted, -} - -struct TrackedBuffer { - buffer: Entity, - diff_base: Rope, - unreviewed_edits: Patch, - status: TrackedBufferStatus, - version: clock::Global, - diff: Entity, - snapshot: text::BufferSnapshot, - diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>, - _open_lsp_handle: OpenLspBufferHandle, - _maintain_diff: Task<()>, - _subscription: Subscription, -} - -impl TrackedBuffer { - fn has_edits(&self, cx: &App) -> bool { - self.diff - .read(cx) - .hunks(self.buffer.read(cx), cx) - .next() - .is_some() - } - - fn schedule_diff_update(&self, author: ChangeAuthor, cx: &App) { - self.diff_update - .unbounded_send((author, self.buffer.read(cx).text_snapshot())) - .ok(); - } -} - -pub struct ChangedBuffer { - pub diff: Entity, -} - -#[cfg(test)] -mod tests { - use super::*; - use buffer_diff::DiffHunkStatusKind; - use gpui::TestAppContext; - use language::Point; - use project::{FakeFs, Fs, Project, RemoveOptions}; - use rand::prelude::*; - use serde_json::json; - use settings::SettingsStore; - use std::env; - use util::{RandomCharIter, path}; - - #[ctor::ctor] - fn init_logger() { - zlog::init_test(); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - }); - } - - #[gpui::test(iterations = 10)] - async fn test_keep_edits(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"})) - .await; - 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/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx) - .unwrap() - }); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx) - .unwrap() - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndEf\nghi\njkl\nmnO" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(2, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\n".into(), - }, - HunkStatus { - range: Point::new(4, 0)..Point::new(4, 3), - diff_status: DiffHunkStatusKind::Modified, - old_text: "mno".into(), - } - ], - )] - ); - - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), None, cx) - }); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(1, 0)..Point::new(2, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\n".into(), - }], - )] - ); - - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), None, cx) - }); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_deletions(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({"file": "abc\ndef\nghi\njkl\nmno\npqr"}), - ) - .await; - 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/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 0)..Point::new(2, 0), "")], None, cx) - .unwrap(); - buffer.finalize_last_transaction(); - }); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(3, 0)..Point::new(4, 0), "")], None, cx) - .unwrap(); - buffer.finalize_last_transaction(); - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\nghi\njkl\npqr" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(1, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "def\n".into(), - }, - HunkStatus { - range: Point::new(3, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "mno\n".into(), - } - ], - )] - ); - - buffer.update(cx, |buffer, cx| buffer.undo(cx)); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\nghi\njkl\nmno\npqr" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(1, 0)..Point::new(1, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "def\n".into(), - }], - )] - ); - - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), None, cx) - }); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_overlapping_user_edits(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"})) - .await; - 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/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx) - .unwrap() - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndeF\nGHI\njkl\nmno" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\nghi\n".into(), - }], - )] - ); - - buffer.update(cx, |buffer, cx| { - buffer.edit( - [ - (Point::new(0, 2)..Point::new(0, 2), "X"), - (Point::new(3, 0)..Point::new(3, 0), "Y"), - ], - None, - cx, - ) - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abXc\ndeF\nGHI\nYjkl\nmno" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\nghi\n".into(), - }], - )] - ); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "Z")], None, cx) - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abXc\ndZeF\nGHI\nYjkl\nmno" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\nghi\n".into(), - }], - )] - ); - - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), None, cx) - }); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_creating_files(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({})).await; - 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/file1", cx)) - .unwrap(); - - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| buffer.set_text("lorem", 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_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 5), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into(), - }], - )] - ); - - buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "X")], None, cx)); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 6), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into(), - }], - )] - ); - - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), 0..5, None, cx) - }); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_overwriting_files(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({ - "file1": "Lorem ipsum dolor" - }), - ) - .await; - 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/file1", cx)) - .unwrap(); - - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| buffer.set_text("sit amet consecteur", 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_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 19), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into(), - }], - )] - ); - - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges(buffer.clone(), vec![2..5], None, cx) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - assert_eq!( - buffer.read_with(cx, |buffer, _cx| buffer.text()), - "Lorem ipsum dolor" - ); - } - - #[gpui::test(iterations = 10)] - async fn test_overwriting_previously_edited_files(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({ - "file1": "Lorem ipsum dolor" - }), - ) - .await; - 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/file1", cx)) - .unwrap(); - - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| buffer.append(" sit amet consecteur", 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_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 37), - diff_status: DiffHunkStatusKind::Modified, - old_text: "Lorem ipsum dolor".into(), - }], - )] - ); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| buffer.set_text("rewritten", 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_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 9), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into(), - }], - )] - ); - - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges(buffer.clone(), vec![2..5], None, cx) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - assert_eq!( - buffer.read_with(cx, |buffer, _cx| buffer.text()), - "Lorem ipsum dolor" - ); - } - - #[gpui::test(iterations = 10)] - async fn test_deleting_files(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({"file1": "lorem\n", "file2": "ipsum\n"}), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let file1_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx)) - .unwrap(); - let file2_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file2", cx)) - .unwrap(); - - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let buffer1 = project - .update(cx, |project, cx| { - project.open_buffer(file1_path.clone(), cx) - }) - .await - .unwrap(); - let buffer2 = project - .update(cx, |project, cx| { - project.open_buffer(file2_path.clone(), cx) - }) - .await - .unwrap(); - - action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx)); - action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx)); - project - .update(cx, |project, cx| { - project.delete_file(file1_path.clone(), false, cx) - }) - .unwrap() - .await - .unwrap(); - project - .update(cx, |project, cx| { - project.delete_file(file2_path.clone(), false, cx) - }) - .unwrap() - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![ - ( - buffer1.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "lorem\n".into(), - }] - ), - ( - buffer2.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "ipsum\n".into(), - }], - ) - ] - ); - - // Simulate file1 being recreated externally. - fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec()) - .await; - - // Simulate file2 being recreated by a tool. - let buffer2 = project - .update(cx, |project, cx| project.open_buffer(file2_path, cx)) - .await - .unwrap(); - action_log.update(cx, |log, cx| log.buffer_created(buffer2.clone(), cx)); - buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx)); - action_log.update(cx, |log, cx| log.buffer_edited(buffer2.clone(), cx)); - project - .update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx)) - .await - .unwrap(); - - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer2.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 5), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into(), - }], - )] - ); - - // Simulate file2 being deleted externally. - fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default()) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_reject_edits(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"})) - .await; - 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/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx) - .unwrap() - }); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx) - .unwrap() - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndE\nXYZf\nghi\njkl\nmnO" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\n".into(), - }, - HunkStatus { - range: Point::new(5, 0)..Point::new(5, 3), - diff_status: DiffHunkStatusKind::Modified, - old_text: "mno".into(), - } - ], - )] - ); - - // If the rejected range doesn't overlap with any hunk, we ignore it. - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(4, 0)..Point::new(4, 0)], - None, - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndE\nXYZf\nghi\njkl\nmnO" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\n".into(), - }, - HunkStatus { - range: Point::new(5, 0)..Point::new(5, 3), - diff_status: DiffHunkStatusKind::Modified, - old_text: "mno".into(), - } - ], - )] - ); - - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(0, 0)..Point::new(1, 0)], - None, - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndef\nghi\njkl\nmnO" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(4, 0)..Point::new(4, 3), - diff_status: DiffHunkStatusKind::Modified, - old_text: "mno".into(), - }], - )] - ); - - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(4, 0)..Point::new(4, 0)], - None, - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndef\nghi\njkl\nmno" - ); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_reject_multiple_edits(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"})) - .await; - 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/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx) - .unwrap() - }); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx) - .unwrap() - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndE\nXYZf\nghi\njkl\nmnO" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\n".into(), - }, - HunkStatus { - range: Point::new(5, 0)..Point::new(5, 3), - diff_status: DiffHunkStatusKind::Modified, - old_text: "mno".into(), - } - ], - )] - ); - - action_log.update(cx, |log, cx| { - let range_1 = buffer.read(cx).anchor_before(Point::new(0, 0)) - ..buffer.read(cx).anchor_before(Point::new(1, 0)); - let range_2 = buffer.read(cx).anchor_before(Point::new(5, 0)) - ..buffer.read(cx).anchor_before(Point::new(5, 3)); - - log.reject_edits_in_ranges(buffer.clone(), vec![range_1, range_2], None, cx) - .detach(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndef\nghi\njkl\nmno" - ); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndef\nghi\njkl\nmno" - ); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_reject_deleted_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": "content"})) - .await; - 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/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.will_delete_buffer(buffer.clone(), cx)); - }); - project - .update(cx, |project, cx| { - project.delete_file(file_path.clone(), false, cx) - }) - .unwrap() - .await - .unwrap(); - cx.run_until_parked(); - assert!(!fs.is_file(path!("/dir/file").as_ref()).await); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "content".into(), - }] - )] - ); - - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(0, 0)..Point::new(0, 0)], - None, - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "content"); - assert!(fs.is_file(path!("/dir/file").as_ref()).await); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_reject_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, cx)) - .await - .unwrap(); - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| buffer.set_text("content", 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(); - assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 7), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into(), - }], - )] - ); - - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(0, 0)..Point::new(0, 11)], - None, - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert!(!fs.is_file(path!("/dir/new_file").as_ref()).await); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test] - async fn test_reject_created_file_with_user_edits(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, 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", 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 makes additional edits - cx.update(|cx| { - buffer.update(cx, |buffer, cx| { - buffer.edit([(10..10, "\nuser added this line")], None, cx); - }); - }); - - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - - assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); - - // Reject all - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(0, 0)..Point::new(100, 0)], - None, - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - - // File should still contain all the content - assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); - - let content = buffer.read_with(cx, |buffer, _| buffer.text()); - 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| { - let buffer_range = Anchor::min_max_range_for_buffer(buffer.read(cx).remote_id()); - log.keep_edits_in_range(buffer.clone(), buffer_range, None, 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_max_range_for_buffer( - buffer.read(cx).remote_id(), - )], - None, - 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(None, 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(None, 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); - - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(20); - - let text = RandomCharIter::new(&mut rng).take(50).collect::(); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": text})).await; - 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/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - - for _ in 0..operations { - match rng.random_range(0..100) { - 0..25 => { - action_log.update(cx, |log, cx| { - let range = buffer.read(cx).random_byte_range(0, &mut rng); - log::info!("keeping edits in range {:?}", range); - log.keep_edits_in_range(buffer.clone(), range, None, cx) - }); - } - 25..50 => { - action_log - .update(cx, |log, cx| { - let range = buffer.read(cx).random_byte_range(0, &mut rng); - log::info!("rejecting edits in range {:?}", range); - log.reject_edits_in_ranges(buffer.clone(), vec![range], None, cx) - }) - .await - .unwrap(); - } - _ => { - let is_agent_edit = rng.random_bool(0.5); - if is_agent_edit { - log::info!("agent edit"); - } else { - log::info!("user edit"); - } - cx.update(|cx| { - buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx)); - if is_agent_edit { - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - } - }); - } - } - - if rng.random_bool(0.2) { - quiesce(&action_log, &buffer, cx); - } - } - - quiesce(&action_log, &buffer, cx); - - fn quiesce( - action_log: &Entity, - buffer: &Entity, - cx: &mut TestAppContext, - ) { - log::info!("quiescing..."); - cx.run_until_parked(); - action_log.update(cx, |log, cx| { - 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() { - let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0)); - let old_end = old_text.point_to_offset(cmp::min( - Point::new(edit.new.start + edit.old_len(), 0), - old_text.max_point(), - )); - old_text.replace( - old_start..old_end, - &new_text.slice_rows(edit.new.clone()).to_string(), - ); - } - pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string()); - }) - } - } - - #[gpui::test] - async fn test_keep_edits_on_commit(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/project"), - json!({ - ".git": {}, - "file.txt": "a\nb\nc\nd\ne\nf\ng\nh\ni\nj", - }), - ) - .await; - fs.set_head_for_repo( - path!("/project/.git").as_ref(), - &[("file.txt", "a\nb\nc\nd\ne\nf\ng\nh\ni\nj".into())], - "0000000", - ); - cx.run_until_parked(); - - let project = Project::test(fs.clone(), [path!("/project").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(path!("/project/file.txt"), cx) - }) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer.edit( - [ - // Edit at the very start: a -> A - (Point::new(0, 0)..Point::new(0, 1), "A"), - // Deletion in the middle: remove lines d and e - (Point::new(3, 0)..Point::new(5, 0), ""), - // Modification: g -> GGG - (Point::new(6, 0)..Point::new(6, 1), "GGG"), - // Addition: insert new line after h - (Point::new(7, 1)..Point::new(7, 1), "\nNEW"), - // Edit the very last character: j -> J - (Point::new(9, 0)..Point::new(9, 1), "J"), - ], - None, - cx, - ); - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(0, 0)..Point::new(1, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "a\n".into() - }, - HunkStatus { - range: Point::new(3, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "d\ne\n".into() - }, - HunkStatus { - range: Point::new(4, 0)..Point::new(5, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "g\n".into() - }, - HunkStatus { - range: Point::new(6, 0)..Point::new(7, 0), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into() - }, - HunkStatus { - range: Point::new(8, 0)..Point::new(8, 1), - diff_status: DiffHunkStatusKind::Modified, - old_text: "j".into() - } - ] - )] - ); - - // Simulate a git commit that matches some edits but not others: - // - Accepts the first edit (a -> A) - // - Accepts the deletion (remove d and e) - // - Makes a different change to g (g -> G instead of GGG) - // - Ignores the NEW line addition - // - Ignores the last line edit (j stays as j) - fs.set_head_for_repo( - path!("/project/.git").as_ref(), - &[("file.txt", "A\nb\nc\nf\nG\nh\ni\nj".into())], - "0000001", - ); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(4, 0)..Point::new(5, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "g\n".into() - }, - HunkStatus { - range: Point::new(6, 0)..Point::new(7, 0), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into() - }, - HunkStatus { - range: Point::new(8, 0)..Point::new(8, 1), - diff_status: DiffHunkStatusKind::Modified, - old_text: "j".into() - } - ] - )] - ); - - // Make another commit that accepts the NEW line but with different content - fs.set_head_for_repo( - path!("/project/.git").as_ref(), - &[("file.txt", "A\nb\nc\nf\nGGG\nh\nDIFFERENT\ni\nj".into())], - "0000002", - ); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer, - vec![ - HunkStatus { - range: Point::new(6, 0)..Point::new(7, 0), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into() - }, - HunkStatus { - range: Point::new(8, 0)..Point::new(8, 1), - diff_status: DiffHunkStatusKind::Modified, - old_text: "j".into() - } - ] - )] - ); - - // Final commit that accepts all remaining edits - fs.set_head_for_repo( - path!("/project/.git").as_ref(), - &[("file.txt", "A\nb\nc\nf\nGGG\nh\nNEW\ni\nJ".into())], - "0000003", - ); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[derive(Debug, Clone, PartialEq, Eq)] - struct HunkStatus { - range: Range, - diff_status: DiffHunkStatusKind, - old_text: String, - } - - fn unreviewed_hunks( - action_log: &Entity, - cx: &TestAppContext, - ) -> Vec<(Entity, Vec)> { - cx.read(|cx| { - action_log - .read(cx) - .changed_buffers(cx) - .into_iter() - .map(|(buffer, diff)| { - let snapshot = buffer.read(cx).snapshot(); - ( - buffer, - diff.read(cx) - .hunks(&snapshot, cx) - .map(|hunk| HunkStatus { - diff_status: hunk.status().kind, - range: hunk.range, - old_text: diff - .read(cx) - .base_text() - .text_for_range(hunk.diff_base_byte_range) - .collect(), - }) - .collect(), - ) - }) - .collect() - }) - } -} diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml deleted file mode 100644 index 8587e52723..0000000000 --- a/crates/activity_indicator/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "activity_indicator" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/activity_indicator.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -auto_update.workspace = true -editor.workspace = true -extension_host.workspace = true -fs.workspace = true -futures.workspace = true -gpui.workspace = true -language.workspace = true -project.workspace = true -proto.workspace = true -semver.workspace = true -smallvec.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true - -[dev-dependencies] -editor = { workspace = true, features = ["test-support"] } -release_channel.workspace = true diff --git a/crates/activity_indicator/LICENSE-GPL b/crates/activity_indicator/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/activity_indicator/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs deleted file mode 100644 index b537fabc9b..0000000000 --- a/crates/activity_indicator/src/activity_indicator.rs +++ /dev/null @@ -1,947 +0,0 @@ -use auto_update::{AutoUpdateStatus, AutoUpdater, DismissMessage, VersionCheckType}; -use editor::Editor; -use extension_host::{ExtensionOperation, ExtensionStore}; -use futures::StreamExt; -use gpui::{ - App, Context, CursorStyle, Entity, EventEmitter, InteractiveElement as _, ParentElement as _, - Render, SharedString, StatefulInteractiveElement, Styled, Window, actions, -}; -use language::{ - BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName, - LanguageServerStatusUpdate, ServerHealth, -}; -use project::{ - LanguageServerProgress, LspStoreEvent, ProgressToken, Project, ProjectEnvironmentEvent, - git_store::{GitStoreEvent, Repository}, -}; -use smallvec::SmallVec; -use std::{ - cmp::Reverse, - collections::HashSet, - fmt::Write, - sync::Arc, - time::{Duration, Instant}, -}; -use ui::{ - ButtonLike, CommonAnimationExt, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, - prelude::*, -}; -use util::truncate_and_trailoff; -use workspace::{StatusItemView, Workspace, item::ItemHandle}; - -const GIT_OPERATION_DELAY: Duration = Duration::from_millis(0); - -actions!( - activity_indicator, - [ - /// Displays error messages from language servers in the status bar. - ShowErrorMessage - ] -); - -pub enum Event { - ShowStatus { - server_name: LanguageServerName, - status: SharedString, - }, -} - -pub struct ActivityIndicator { - statuses: Vec, - project: Entity, - auto_updater: Option>, - context_menu_handle: PopoverMenuHandle, - fs_jobs: Vec, -} - -#[derive(Debug)] -struct ServerStatus { - name: LanguageServerName, - status: LanguageServerStatusUpdate, -} - -struct PendingWork<'a> { - language_server_id: LanguageServerId, - progress_token: &'a ProgressToken, - progress: &'a LanguageServerProgress, -} - -struct Content { - icon: Option, - message: String, - on_click: - Option)>>, - tooltip_message: Option, -} - -impl ActivityIndicator { - pub fn new( - workspace: &mut Workspace, - languages: Arc, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let project = workspace.project().clone(); - let auto_updater = AutoUpdater::get(cx); - let this = cx.new(|cx| { - let mut status_events = languages.language_server_binary_statuses(); - cx.spawn(async move |this, cx| { - while let Some((name, binary_status)) = status_events.next().await { - this.update(cx, |this: &mut ActivityIndicator, cx| { - this.statuses.retain(|s| s.name != name); - this.statuses.push(ServerStatus { - name, - status: LanguageServerStatusUpdate::Binary(binary_status), - }); - cx.notify(); - })?; - } - anyhow::Ok(()) - }) - .detach(); - - let fs = project.read(cx).fs().clone(); - let mut job_events = fs.subscribe_to_jobs(); - cx.spawn(async move |this, cx| { - while let Some(job_event) = job_events.next().await { - this.update(cx, |this: &mut ActivityIndicator, cx| { - match job_event { - fs::JobEvent::Started { info } => { - this.fs_jobs.retain(|j| j.id != info.id); - this.fs_jobs.push(info); - } - fs::JobEvent::Completed { id } => { - this.fs_jobs.retain(|j| j.id != id); - } - } - cx.notify(); - })?; - } - anyhow::Ok(()) - }) - .detach(); - - cx.subscribe( - &project.read(cx).lsp_store(), - |activity_indicator, _, event, cx| { - if let LspStoreEvent::LanguageServerUpdate { name, message, .. } = event { - if let proto::update_language_server::Variant::StatusUpdate(status_update) = - message - { - let Some(name) = name.clone() else { - return; - }; - let status = match &status_update.status { - Some(proto::status_update::Status::Binary(binary_status)) => { - if let Some(binary_status) = - proto::ServerBinaryStatus::from_i32(*binary_status) - { - let binary_status = match binary_status { - proto::ServerBinaryStatus::None => BinaryStatus::None, - proto::ServerBinaryStatus::CheckingForUpdate => { - BinaryStatus::CheckingForUpdate - } - proto::ServerBinaryStatus::Downloading => { - BinaryStatus::Downloading - } - proto::ServerBinaryStatus::Starting => { - BinaryStatus::Starting - } - proto::ServerBinaryStatus::Stopping => { - BinaryStatus::Stopping - } - proto::ServerBinaryStatus::Stopped => { - BinaryStatus::Stopped - } - proto::ServerBinaryStatus::Failed => { - let Some(error) = status_update.message.clone() - else { - return; - }; - BinaryStatus::Failed { error } - } - }; - LanguageServerStatusUpdate::Binary(binary_status) - } else { - return; - } - } - Some(proto::status_update::Status::Health(health_status)) => { - if let Some(health) = - proto::ServerHealth::from_i32(*health_status) - { - let health = match health { - proto::ServerHealth::Ok => ServerHealth::Ok, - proto::ServerHealth::Warning => ServerHealth::Warning, - proto::ServerHealth::Error => ServerHealth::Error, - }; - LanguageServerStatusUpdate::Health( - health, - status_update.message.clone().map(SharedString::from), - ) - } else { - return; - } - } - None => return, - }; - - activity_indicator.statuses.retain(|s| s.name != name); - activity_indicator - .statuses - .push(ServerStatus { name, status }); - } - cx.notify() - } - }, - ) - .detach(); - - cx.subscribe( - &project.read(cx).environment().clone(), - |_, _, event, cx| match event { - ProjectEnvironmentEvent::ErrorsUpdated => cx.notify(), - }, - ) - .detach(); - - cx.subscribe( - &project.read(cx).git_store().clone(), - |_, _, event: &GitStoreEvent, cx| { - if let project::git_store::GitStoreEvent::JobsUpdated = event { - cx.notify() - } - }, - ) - .detach(); - - if let Some(auto_updater) = auto_updater.as_ref() { - cx.observe(auto_updater, |_, _, cx| cx.notify()).detach(); - } - - Self { - statuses: Vec::new(), - project: project.clone(), - auto_updater, - context_menu_handle: PopoverMenuHandle::default(), - fs_jobs: Vec::new(), - } - }); - - cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event { - Event::ShowStatus { - server_name, - status, - } => { - let create_buffer = - project.update(cx, |project, cx| project.create_buffer(false, cx)); - let status = status.clone(); - let server_name = server_name.clone(); - cx.spawn_in(window, async move |workspace, cx| { - let buffer = create_buffer.await?; - buffer.update(cx, |buffer, cx| { - buffer.edit( - [(0..0, format!("Language server {server_name}:\n\n{status}"))], - None, - cx, - ); - buffer.set_capability(language::Capability::ReadOnly, cx); - })?; - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane( - Box::new(cx.new(|cx| { - let mut editor = Editor::for_buffer(buffer, None, window, cx); - editor.set_read_only(true); - editor - })), - None, - true, - window, - cx, - ); - })?; - - anyhow::Ok(()) - }) - .detach(); - } - }) - .detach(); - this - } - - fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context) { - let mut status_message_shown = false; - self.statuses.retain(|status| match &status.status { - LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { error }) - if !status_message_shown => - { - cx.emit(Event::ShowStatus { - server_name: status.name.clone(), - status: SharedString::from(error), - }); - status_message_shown = true; - false - } - LanguageServerStatusUpdate::Health( - ServerHealth::Error | ServerHealth::Warning, - status_string, - ) if !status_message_shown => match status_string { - Some(error) => { - cx.emit(Event::ShowStatus { - server_name: status.name.clone(), - status: error.clone(), - }); - status_message_shown = true; - false - } - None => false, - }, - _ => true, - }); - } - - fn dismiss_message(&mut self, _: &DismissMessage, _: &mut Window, cx: &mut Context) { - let dismissed = if let Some(updater) = &self.auto_updater { - updater.update(cx, |updater, cx| updater.dismiss(cx)) - } else { - false - }; - if dismissed { - return; - } - - self.project.update(cx, |project, cx| { - if project.last_formatting_failure(cx).is_some() { - project.reset_last_formatting_failure(cx); - true - } else { - false - } - }); - } - - fn pending_language_server_work<'a>( - &self, - cx: &'a App, - ) -> impl Iterator> { - self.project - .read(cx) - .language_server_statuses(cx) - .rev() - .filter_map(|(server_id, status)| { - if status.pending_work.is_empty() { - None - } else { - let mut pending_work = status - .pending_work - .iter() - .map(|(progress_token, progress)| PendingWork { - language_server_id: server_id, - progress_token, - progress, - }) - .collect::>(); - pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at)); - Some(pending_work) - } - }) - .flatten() - } - - fn pending_environment_error<'a>(&'a self, cx: &'a App) -> Option<&'a String> { - self.project.read(cx).peek_environment_error(cx) - } - - fn content_to_render(&mut self, cx: &mut Context) -> Option { - // Show if any direnv calls failed - if let Some(message) = self.pending_environment_error(cx) { - return Some(Content { - icon: Some( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .into_any_element(), - ), - message: message.clone(), - on_click: Some(Arc::new(move |this, window, cx| { - this.project.update(cx, |project, cx| { - project.pop_environment_error(cx); - }); - window.dispatch_action(Box::new(workspace::OpenLog), cx); - })), - tooltip_message: None, - }); - } - // Show any language server has pending activity. - { - let mut pending_work = self.pending_language_server_work(cx); - if let Some(PendingWork { - progress_token, - progress, - .. - }) = pending_work.next() - { - let mut message = progress.title.clone().unwrap_or(progress_token.to_string()); - - if let Some(percentage) = progress.percentage { - write!(&mut message, " ({}%)", percentage).unwrap(); - } - - if let Some(progress_message) = progress.message.as_ref() { - message.push_str(": "); - message.push_str(progress_message); - } - - let additional_work_count = pending_work.count(); - if additional_work_count > 0 { - write!(&mut message, " + {} more", additional_work_count).unwrap(); - } - - return Some(Content { - icon: Some( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .with_rotate_animation(2) - .into_any_element(), - ), - message, - on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)), - tooltip_message: None, - }); - } - } - - if let Some(session) = self - .project - .read(cx) - .dap_store() - .read(cx) - .sessions() - .find(|s| !s.read(cx).is_started()) - { - return Some(Content { - icon: Some( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .with_rotate_animation(2) - .into_any_element(), - ), - message: format!("Debug: {}", session.read(cx).adapter()), - tooltip_message: session.read(cx).label().map(|label| label.to_string()), - on_click: None, - }); - } - - let current_job = self - .project - .read(cx) - .active_repository(cx) - .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_rotate_animation(2) - .into_any_element(), - ), - message: job_info.message.into(), - on_click: None, - tooltip_message: None, - }); - } - - // Show any long-running fs command - for fs_job in &self.fs_jobs { - if Instant::now().duration_since(fs_job.start) >= GIT_OPERATION_DELAY { - return Some(Content { - icon: Some( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .with_rotate_animation(2) - .into_any_element(), - ), - message: fs_job.message.clone().into(), - on_click: None, - tooltip_message: None, - }); - } - } - - // Show any language server installation info. - let mut downloading = SmallVec::<[_; 3]>::new(); - let mut checking_for_update = SmallVec::<[_; 3]>::new(); - let mut failed = SmallVec::<[_; 3]>::new(); - let mut health_messages = SmallVec::<[_; 3]>::new(); - let mut servers_to_clear_statuses = HashSet::::default(); - for status in &self.statuses { - match &status.status { - LanguageServerStatusUpdate::Binary( - BinaryStatus::Starting | BinaryStatus::Stopping, - ) => {} - LanguageServerStatusUpdate::Binary(BinaryStatus::Stopped) => { - servers_to_clear_statuses.insert(status.name.clone()); - } - LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) => { - checking_for_update.push(status.name.clone()); - } - LanguageServerStatusUpdate::Binary(BinaryStatus::Downloading) => { - downloading.push(status.name.clone()); - } - LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { .. }) => { - failed.push(status.name.clone()); - } - LanguageServerStatusUpdate::Binary(BinaryStatus::None) => {} - LanguageServerStatusUpdate::Health(health, server_status) => match server_status { - Some(server_status) => { - health_messages.push((status.name.clone(), *health, server_status.clone())); - } - None => { - servers_to_clear_statuses.insert(status.name.clone()); - } - }, - } - } - self.statuses - .retain(|status| !servers_to_clear_statuses.contains(&status.name)); - - health_messages.sort_by_key(|(_, health, _)| match health { - ServerHealth::Error => 2, - ServerHealth::Warning => 1, - ServerHealth::Ok => 0, - }); - - if !downloading.is_empty() { - return Some(Content { - icon: Some( - Icon::new(IconName::Download) - .size(IconSize::Small) - .into_any_element(), - ), - message: format!( - "Downloading {}...", - downloading.iter().map(|name| name.as_ref()).fold( - String::new(), - |mut acc, s| { - if !acc.is_empty() { - acc.push_str(", "); - } - acc.push_str(s); - acc - } - ) - ), - on_click: Some(Arc::new(move |this, window, cx| { - this.statuses - .retain(|status| !downloading.contains(&status.name)); - this.dismiss_message(&DismissMessage, window, cx) - })), - tooltip_message: None, - }); - } - - if !checking_for_update.is_empty() { - return Some(Content { - icon: Some( - Icon::new(IconName::Download) - .size(IconSize::Small) - .into_any_element(), - ), - message: format!( - "Checking for updates to {}...", - checking_for_update.iter().map(|name| name.as_ref()).fold( - String::new(), - |mut acc, s| { - if !acc.is_empty() { - acc.push_str(", "); - } - acc.push_str(s); - acc - } - ), - ), - on_click: Some(Arc::new(move |this, window, cx| { - this.statuses - .retain(|status| !checking_for_update.contains(&status.name)); - this.dismiss_message(&DismissMessage, window, cx) - })), - tooltip_message: None, - }); - } - - if !failed.is_empty() { - return Some(Content { - icon: Some( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .into_any_element(), - ), - message: format!( - "Failed to run {}. Click to show error.", - failed - .iter() - .map(|name| name.as_ref()) - .fold(String::new(), |mut acc, s| { - if !acc.is_empty() { - acc.push_str(", "); - } - acc.push_str(s); - acc - }), - ), - on_click: Some(Arc::new(|this, window, cx| { - this.show_error_message(&ShowErrorMessage, window, cx) - })), - tooltip_message: None, - }); - } - - // Show any formatting failure - if let Some(failure) = self.project.read(cx).last_formatting_failure(cx) { - return Some(Content { - icon: Some( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .into_any_element(), - ), - message: format!("Formatting failed: {failure}. Click to see logs."), - on_click: Some(Arc::new(|indicator, window, cx| { - indicator.project.update(cx, |project, cx| { - project.reset_last_formatting_failure(cx); - }); - window.dispatch_action(Box::new(workspace::OpenLog), cx); - })), - tooltip_message: None, - }); - } - - // Show any health messages for the language servers - if let Some((server_name, health, message)) = health_messages.pop() { - let health_str = match health { - ServerHealth::Ok => format!("({server_name}) "), - ServerHealth::Warning => format!("({server_name}) Warning: "), - ServerHealth::Error => format!("({server_name}) Error: "), - }; - let single_line_message = message - .lines() - .filter_map(|line| { - let line = line.trim(); - if line.is_empty() { None } else { Some(line) } - }) - .collect::>() - .join(" "); - let mut altered_message = single_line_message != message; - let truncated_message = truncate_and_trailoff( - &single_line_message, - MAX_MESSAGE_LEN.saturating_sub(health_str.len()), - ); - altered_message |= truncated_message != single_line_message; - let final_message = format!("{health_str}{truncated_message}"); - - let tooltip_message = if altered_message { - Some(format!("{health_str}{message}")) - } else { - None - }; - - return Some(Content { - icon: Some( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .into_any_element(), - ), - message: final_message, - tooltip_message, - on_click: Some(Arc::new(move |activity_indicator, window, cx| { - if altered_message { - activity_indicator.show_error_message(&ShowErrorMessage, window, cx) - } else { - activity_indicator - .statuses - .retain(|status| status.name != server_name); - cx.notify(); - } - })), - }); - } - - // Show any application auto-update info. - self.auto_updater - .as_ref() - .and_then(|updater| match &updater.read(cx).status() { - AutoUpdateStatus::Checking => Some(Content { - icon: Some( - Icon::new(IconName::LoadCircle) - .size(IconSize::Small) - .with_rotate_animation(3) - .into_any_element(), - ), - message: "Checking for Zed updates…".to_string(), - on_click: Some(Arc::new(|this, window, cx| { - this.dismiss_message(&DismissMessage, window, cx) - })), - tooltip_message: None, - }), - AutoUpdateStatus::Downloading { version } => Some(Content { - icon: Some( - Icon::new(IconName::Download) - .size(IconSize::Small) - .into_any_element(), - ), - message: "Downloading Zed update…".to_string(), - on_click: Some(Arc::new(|this, window, cx| { - this.dismiss_message(&DismissMessage, window, cx) - })), - tooltip_message: Some(Self::version_tooltip_message(version)), - }), - AutoUpdateStatus::Installing { version } => Some(Content { - icon: Some( - Icon::new(IconName::LoadCircle) - .size(IconSize::Small) - .with_rotate_animation(3) - .into_any_element(), - ), - message: "Installing Zed update…".to_string(), - on_click: Some(Arc::new(|this, window, cx| { - this.dismiss_message(&DismissMessage, window, cx) - })), - tooltip_message: Some(Self::version_tooltip_message(version)), - }), - AutoUpdateStatus::Updated { version } => Some(Content { - icon: None, - message: "Click to restart and update Zed".to_string(), - on_click: Some(Arc::new(move |_, _, cx| workspace::reload(cx))), - tooltip_message: Some(Self::version_tooltip_message(version)), - }), - AutoUpdateStatus::Errored { error } => Some(Content { - icon: Some( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .into_any_element(), - ), - message: "Failed to update Zed".to_string(), - on_click: Some(Arc::new(|this, window, cx| { - window.dispatch_action(Box::new(workspace::OpenLog), cx); - this.dismiss_message(&DismissMessage, window, cx); - })), - tooltip_message: Some(format!("{error}")), - }), - AutoUpdateStatus::Idle => None, - }) - .or_else(|| { - if let Some(extension_store) = - ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx)) - && let Some((extension_id, operation)) = - extension_store.outstanding_operations().iter().next() - { - let (message, icon, rotate) = match operation { - ExtensionOperation::Install => ( - format!("Installing {extension_id} extension…"), - IconName::LoadCircle, - true, - ), - ExtensionOperation::Upgrade => ( - format!("Updating {extension_id} extension…"), - IconName::Download, - false, - ), - ExtensionOperation::Remove => ( - format!("Removing {extension_id} extension…"), - IconName::LoadCircle, - true, - ), - }; - - Some(Content { - icon: Some(Icon::new(icon).size(IconSize::Small).map(|this| { - if rotate { - this.with_rotate_animation(3).into_any_element() - } else { - this.into_any_element() - } - })), - message, - on_click: Some(Arc::new(|this, window, cx| { - this.dismiss_message(&Default::default(), window, cx) - })), - tooltip_message: None, - }) - } else { - None - } - }) - } - - fn version_tooltip_message(version: &VersionCheckType) -> String { - format!("Version: {}", { - match version { - auto_update::VersionCheckType::Sha(sha) => format!("{}…", sha.short()), - auto_update::VersionCheckType::Semantic(semantic_version) => { - semantic_version.to_string() - } - } - }) - } - - fn toggle_language_server_work_context_menu( - &mut self, - window: &mut Window, - cx: &mut Context, - ) { - self.context_menu_handle.toggle(window, cx); - } -} - -impl EventEmitter for ActivityIndicator {} - -const MAX_MESSAGE_LEN: usize = 50; - -impl Render for ActivityIndicator { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let result = h_flex() - .id("activity-indicator") - .on_action(cx.listener(Self::show_error_message)) - .on_action(cx.listener(Self::dismiss_message)); - let Some(content) = self.content_to_render(cx) else { - return result; - }; - let activity_indicator = cx.entity().downgrade(); - let truncate_content = content.message.len() > MAX_MESSAGE_LEN; - result.gap_2().child( - PopoverMenu::new("activity-indicator-popover") - .trigger( - ButtonLike::new("activity-indicator-trigger").child( - h_flex() - .id("activity-indicator-status") - .gap_2() - .children(content.icon) - .map(|button| { - if truncate_content { - button - .child( - Label::new(truncate_and_trailoff( - &content.message, - MAX_MESSAGE_LEN, - )) - .size(LabelSize::Small), - ) - .tooltip(Tooltip::text(content.message)) - } else { - button - .child(Label::new(content.message).size(LabelSize::Small)) - .when_some( - content.tooltip_message, - |this, tooltip_message| { - this.tooltip(Tooltip::text(tooltip_message)) - }, - ) - } - }) - .when_some(content.on_click, |this, handler| { - this.on_click(cx.listener(move |this, _, window, cx| { - handler(this, window, cx); - })) - .cursor(CursorStyle::PointingHand) - }), - ), - ) - .anchor(gpui::Corner::BottomLeft) - .menu(move |window, cx| { - let strong_this = activity_indicator.upgrade()?; - let mut has_work = false; - let menu = ContextMenu::build(window, cx, |mut menu, _, cx| { - for work in strong_this.read(cx).pending_language_server_work(cx) { - has_work = true; - let activity_indicator = activity_indicator.clone(); - let mut title = work - .progress - .title - .clone() - .unwrap_or(work.progress_token.to_string()); - - if work.progress.is_cancellable { - let language_server_id = work.language_server_id; - let token = work.progress_token.clone(); - let title = SharedString::from(title); - menu = menu.custom_entry( - move |_, _| { - h_flex() - .w_full() - .justify_between() - .child(Label::new(title.clone())) - .child(Icon::new(IconName::XCircle)) - .into_any_element() - }, - move |_, cx| { - let token = token.clone(); - activity_indicator - .update(cx, |activity_indicator, cx| { - activity_indicator.project.update( - cx, - |project, cx| { - project.cancel_language_server_work( - language_server_id, - Some(token), - cx, - ); - }, - ); - activity_indicator.context_menu_handle.hide(cx); - cx.notify(); - }) - .ok(); - }, - ); - } else { - if let Some(progress_message) = work.progress.message.as_ref() { - title.push_str(": "); - title.push_str(progress_message); - } - - menu = menu.label(title); - } - } - menu - }); - has_work.then_some(menu) - }), - ) - } -} - -impl StatusItemView for ActivityIndicator { - fn set_active_pane_item( - &mut self, - _: Option<&dyn ItemHandle>, - _window: &mut Window, - _: &mut Context, - ) { - } -} - -#[cfg(test)] -mod tests { - use release_channel::AppCommitSha; - use semver::Version; - - use super::*; - - #[test] - fn test_version_tooltip_message() { - let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Semantic( - Version::new(1, 0, 0), - )); - - assert_eq!(message, "Version: 1.0.0"); - - let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Sha( - AppCommitSha::new("14d9a4189f058d8736339b06ff2340101eaea5af".to_string()), - )); - - assert_eq!(message, "Version: 14d9a41…"); - } -} diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml deleted file mode 100644 index 667033a1bb..0000000000 --- a/crates/agent/Cargo.toml +++ /dev/null @@ -1,105 +0,0 @@ -[package] -name = "agent" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lib] -path = "src/agent.rs" - -[features] -test-support = ["db/test-support"] -eval = [] -unit-eval = [] -e2e = [] - -[lints] -workspace = true - -[dependencies] -acp_thread.workspace = true -action_log.workspace = true -agent-client-protocol.workspace = true -agent_servers.workspace = true -agent_settings.workspace = true -anyhow.workspace = true -assistant_text_thread.workspace = true -chrono.workspace = true -client.workspace = true -cloud_llm_client.workspace = true -collections.workspace = true -context_server.workspace = true -db.workspace = true -derive_more.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 -project.workspace = true -prompt_store.workspace = true -regex.workspace = true -rust-embed.workspace = true -schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smallvec.workspace = true -smol.workspace = true -sqlez.workspace = true -streaming_diff.workspace = true -strsim.workspace = true -task.workspace = true -telemetry.workspace = true -text.workspace = true -thiserror.workspace = true -ui.workspace = true -util.workspace = true -uuid.workspace = true -watch.workspace = true -web_search.workspace = true -zed_env_vars.workspace = true -zstd.workspace = true - -[dev-dependencies] -agent_servers = { workspace = true, "features" = ["test-support"] } -assistant_text_thread = { workspace = true, "features" = ["test-support"] } -client = { workspace = true, "features" = ["test-support"] } -clock = { workspace = true, "features" = ["test-support"] } -context_server = { workspace = true, "features" = ["test-support"] } -ctor.workspace = true -db = { workspace = true, "features" = ["test-support"] } -editor = { workspace = true, "features" = ["test-support"] } -env_logger.workspace = true -eval_utils.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"] } -rand.workspace = true -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/agent/LICENSE-GPL b/crates/agent/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/agent/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs deleted file mode 100644 index cf98a24ac5..0000000000 --- a/crates/agent/src/agent.rs +++ /dev/null @@ -1,1608 +0,0 @@ -mod db; -mod edit_agent; -mod history_store; -mod legacy_thread; -mod native_agent_server; -pub mod outline; -mod templates; -mod thread; -mod tools; - -#[cfg(test)] -mod tests; - -pub use db::*; -pub use history_store::*; -pub use native_agent_server::NativeAgentServer; -pub use templates::*; -pub use thread::*; -pub use tools::*; - -use acp_thread::{AcpThread, AgentModelSelector}; -use agent_client_protocol as acp; -use anyhow::{Context as _, Result, anyhow}; -use chrono::{DateTime, Utc}; -use collections::{HashSet, IndexMap}; -use fs::Fs; -use futures::channel::{mpsc, oneshot}; -use futures::future::Shared; -use futures::{StreamExt, future}; -use gpui::{ - App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity, -}; -use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry}; -use project::{Project, ProjectItem, ProjectPath, Worktree}; -use prompt_store::{ - ProjectContext, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext, -}; -use serde::{Deserialize, Serialize}; -use settings::{LanguageModelSelection, update_settings_file}; -use std::any::Any; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::rc::Rc; -use std::sync::Arc; -use util::ResultExt; -use util::rel_path::RelPath; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ProjectSnapshot { - pub worktree_snapshots: Vec, - pub timestamp: DateTime, -} - -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<()>, - _authenticate_all_providers_task: Task<()>, -} - -impl LanguageModels { - fn new(cx: &mut App) -> Self { - let (refresh_models_tx, refresh_models_rx) = watch::channel(()); - - let mut this = Self { - models: HashMap::default(), - model_list: acp_thread::AgentModelList::Grouped(IndexMap::default()), - refresh_models_rx, - refresh_models_tx, - _authenticate_all_providers_task: Self::authenticate_all_language_model_providers(cx), - }; - this.refresh_list(cx); - this - } - - 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.provider_id(), 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(); - 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::ModelId) -> 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, - description: None, - icon: Some(provider.icon()), - } - } - - fn model_id(model: &Arc) -> acp::ModelId { - acp::ModelId::new(format!("{}/{}", model.provider_id().0, model.id().0)) - } - - fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> { - let authenticate_all_providers = LanguageModelRegistry::global(cx) - .read(cx) - .providers() - .iter() - .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx))) - .collect::>(); - - cx.background_spawn(async move { - for (provider_id, provider_name, authenticate_task) in authenticate_all_providers { - if let Err(err) = authenticate_task.await { - match err { - language_model::AuthenticateError::CredentialsNotFound => { - // Since we're authenticating these providers in the - // background for the purposes of populating the - // language selector, we don't care about providers - // where the credentials are not found. - } - language_model::AuthenticateError::ConnectionRefused => { - // Not logging connection refused errors as they are mostly from LM Studio's noisy auth failures. - // LM Studio only has one auth method (endpoint call) which fails for users who haven't enabled it. - // TODO: Better manage LM Studio auth logic to avoid these noisy failures. - } - _ => { - // Some providers have noisy failure states that we - // don't want to spam the logs with every time the - // language model selector is initialized. - // - // Ideally these should have more clear failure modes - // that we know are safe to ignore here, like what we do - // with `CredentialsNotFound` above. - match provider_id.0.as_ref() { - "lmstudio" | "ollama" => { - // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated". - // - // These fail noisily, so we don't log them. - } - "copilot_chat" => { - // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors. - } - _ => { - log::error!( - "Failed to authenticate provider: {}: {err:#}", - provider_name.0 - ); - } - } - } - } - } - } - }) - } -} - -pub struct NativeAgent { - /// Session ID -> Session mapping - sessions: HashMap, - 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 thread = thread_handle.read(cx); - let session_id = thread.id().clone(); - let title = thread.title(); - let project = thread.project.clone(); - let action_log = thread.action_log.clone(); - let prompt_capabilities_rx = thread.prompt_capabilities_rx.clone(); - let acp_thread = cx.new(|cx| { - acp_thread::AcpThread::new( - title, - connection, - project.clone(), - action_log.clone(), - session_id.clone(), - prompt_capabilities_rx, - cx, - ) - }); - - let registry = LanguageModelRegistry::read_global(cx); - let summarization_model = registry.thread_summary_model().map(|c| c.model); - - thread_handle.update(cx, |thread, cx| { - thread.set_summarization_model(summarization_model, cx); - thread.add_default_tools( - Rc::new(AcpThreadEnvironment { - acp_thread: acp_thread.downgrade(), - }) as _, - cx, - ) - }); - - let subscriptions = vec![ - cx.observe_release(&acp_thread, |this, acp_thread, _cx| { - this.sessions.remove(acp_thread.session_id()); - }), - cx.subscribe(&thread_handle, Self::handle_thread_title_updated), - cx.subscribe(&thread_handle, Self::handle_thread_token_usage_updated), - cx.observe(&thread_handle, move |this, thread, cx| { - this.save_thread(thread, cx) - }), - ]; - - self.sessions.insert( - session_id, - Session { - thread: thread_handle, - acp_thread: acp_thread.downgrade(), - _subscriptions: subscriptions, - pending_save: Task::ready(()), - }, - ); - acp_thread - } - - pub fn models(&self) -> &LanguageModels { - &self.models - } - - 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 { - prompt_store::PromptId::User { uuid } => uuid, - prompt_store::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_str().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(RelPath::unix(name).unwrap()) - .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() == RelPath::unix(name).unwrap()) - }) { - 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 load_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:?}"))?; - - this.update(cx, |this, cx| { - let summarization_model = LanguageModelRegistry::read_global(cx) - .thread_summary_model() - .map(|c| c.model); - - cx.new(|cx| { - let mut thread = Thread::from_db( - id.clone(), - db_thread, - this.project.clone(), - this.project_context.clone(), - this.context_server_registry.clone(), - this.templates.clone(), - cx, - ); - thread.set_summarization_model(summarization_model, cx); - thread - }) - }) - }) - } - - pub fn open_thread( - &mut self, - id: acp::SessionId, - cx: &mut Context, - ) -> Task>> { - let task = self.load_thread(id, cx); - cx.spawn(async move |this, cx| { - let thread = task.await?; - 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 - .context("Failed to generate summary")?; - 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()) - } - - pub fn load_thread(&self, id: acp::SessionId, cx: &mut App) -> Task>> { - self.0.update(cx, |this, cx| this.load_thread(id, cx)) - } - - 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(text.into(), false, cx) - })?; - } - ThreadEvent::AgentThinking(text) => { - acp_thread.update(cx, |thread, cx| { - thread.push_assistant_content_block(text.into(), true, cx) - })?; - } - ThreadEvent::ToolCallAuthorization(ToolCallAuthorization { - tool_call, - options, - response, - }) => { - let outcome_task = acp_thread.update(cx, |thread, cx| { - thread.request_tool_call_authorization( - tool_call, options, true, cx, - ) - })??; - cx.background_spawn(async move { - if let acp::RequestPermissionOutcome::Selected( - acp::SelectedPermissionOutcome { option_id, .. }, - ) = outcome_task.await - { - response - .send(option_id) - .map(|_| anyhow!("authorization receiver was dropped")) - .log_err(); - } - }) - .detach(); - } - ThreadEvent::ToolCall(tool_call) => { - acp_thread.update(cx, |thread, cx| { - thread.upsert_tool_call(tool_call, cx) - })??; - } - ThreadEvent::ToolCallUpdate(update) => { - acp_thread.update(cx, |thread, cx| { - thread.update_tool_call(update, cx) - })??; - } - ThreadEvent::Retry(status) => { - acp_thread.update(cx, |thread, cx| { - thread.update_retry_status(status, cx) - })?; - } - ThreadEvent::Stop(stop_reason) => { - log::debug!("Assistant message complete: {:?}", stop_reason); - return Ok(acp::PromptResponse::new(stop_reason)); - } - } - } - Err(e) => { - log::error!("Error in model response stream: {:?}", e); - return Err(e); - } - } - } - - log::debug!("Response stream completed"); - anyhow::Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) - }) - } -} - -struct NativeAgentModelSelector { - session_id: acp::SessionId, - connection: NativeAgentConnection, -} - -impl acp_thread::AgentModelSelector for NativeAgentModelSelector { - fn list_models(&self, cx: &mut App) -> Task> { - log::debug!("NativeAgentConnection::list_models called"); - let list = self.connection.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, model_id: acp::ModelId, cx: &mut App) -> Task> { - log::debug!( - "Setting model for session {}: {}", - self.session_id, - model_id - ); - let Some(thread) = self - .connection - .0 - .read(cx) - .sessions - .get(&self.session_id) - .map(|session| session.thread.clone()) - else { - return Task::ready(Err(anyhow!("Session not found"))); - }; - - let Some(model) = self.connection.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.connection.0.read(cx).fs.clone(), - cx, - move |settings, _cx| { - let provider = model.provider_id().0.to_string(); - let model = model.id().0.to_string(); - settings - .agent - .get_or_insert_default() - .set_model(LanguageModelSelection { - provider: provider.into(), - model, - }); - }, - ); - - Task::ready(Ok(())) - } - - fn selected_model(&self, cx: &mut App) -> Task> { - let Some(thread) = self - .connection - .0 - .read(cx) - .sessions - .get(&self.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) -> Option> { - Some(self.connection.0.read(cx).models.watch()) - } - - fn should_render_footer(&self) -> bool { - true - } -} - -impl acp_thread::AgentConnection for NativeAgentConnection { - fn telemetry_id(&self) -> SharedString { - "zed".into() - } - - 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, session_id: &acp::SessionId) -> Option> { - Some(Rc::new(NativeAgentModelSelector { - session_id: session_id.clone(), - connection: 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()); - let path_style = self.0.read(cx).project.read(cx).path_style(cx); - - self.run_turn(session_id, cx, move |thread, cx| { - let content: Vec = params - .prompt - .into_iter() - .map(|block| UserMessageContent::from_content_block(block, path_style)) - .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(NativeAgentSessionTruncate { - 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 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 NativeAgentSessionTruncate { - thread: Entity, - acp_thread: WeakEntity, -} - -impl acp_thread::AgentSessionTruncate for NativeAgentSessionTruncate { - 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(())) - } -} - -pub struct AcpThreadEnvironment { - acp_thread: WeakEntity, -} - -impl ThreadEnvironment for AcpThreadEnvironment { - fn create_terminal( - &self, - command: String, - cwd: Option, - output_byte_limit: Option, - cx: &mut AsyncApp, - ) -> Task>> { - let task = self.acp_thread.update(cx, |thread, cx| { - thread.create_terminal(command, vec![], vec![], cwd, output_byte_limit, cx) - }); - - let acp_thread = self.acp_thread.clone(); - cx.spawn(async move |cx| { - let terminal = task?.await?; - - let (drop_tx, drop_rx) = oneshot::channel(); - let terminal_id = terminal.read_with(cx, |terminal, _cx| terminal.id().clone())?; - - cx.spawn(async move |cx| { - drop_rx.await.ok(); - acp_thread.update(cx, |thread, cx| thread.release_terminal(terminal_id, cx)) - }) - .detach(); - - let handle = AcpTerminalHandle { - terminal, - _drop_tx: Some(drop_tx), - }; - - Ok(Rc::new(handle) as _) - }) - } -} - -pub struct AcpTerminalHandle { - terminal: Entity, - _drop_tx: Option>, -} - -impl TerminalHandle for AcpTerminalHandle { - fn id(&self, cx: &AsyncApp) -> Result { - self.terminal.read_with(cx, |term, _cx| term.id().clone()) - } - - fn wait_for_exit(&self, cx: &AsyncApp) -> Result>> { - self.terminal - .read_with(cx, |term, _cx| term.wait_for_exit()) - } - - fn current_output(&self, cx: &AsyncApp) -> Result { - self.terminal - .read_with(cx, |term, cx| term.current_output(cx)) - } -} - -#[cfg(test)] -mod internal_tests { - use crate::HistoryEntryId; - - use super::*; - use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelInfo, MentionUri}; - use fs::FakeFs; - use gpui::TestAppContext; - use indoc::formatdoc; - use language_model::fake_provider::FakeLanguageModel; - use serde_json::json; - use settings::SettingsStore; - use util::{path, rel_path::rel_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 text_thread_store = - cx.new(|cx| assistant_text_thread::TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_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(rel_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: rel_path(".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 text_thread_store = - cx.new(|cx| assistant_text_thread::TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - let connection = NativeAgentConnection( - NativeAgent::new( - project.clone(), - history_store, - Templates::new(), - None, - fs.clone(), - &mut cx.to_async(), - ) - .await - .unwrap(), - ); - - // 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()); - - let models = cx - .update(|cx| { - connection - .model_selector(&session_id) - .unwrap() - .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: acp::ModelId::new("fake/fake"), - name: "Fake".into(), - description: None, - 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 text_thread_store = - cx.new(|cx| assistant_text_thread::TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_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 selector = connection.model_selector(&session_id).unwrap(); - let model_id = acp::ModelId::new("fake/fake"); - cx.update(|cx| selector.select_model(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] - 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 text_thread_store = - cx.new(|cx| assistant_text_thread::TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_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::new( - "b.md", - MentionUri::File { - abs_path: path!("/a/b.md").into(), - } - .to_uri() - .to_string(), - )), - " 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(&format!("Explaining {}", path!("/a/b.md"))); - summary_model.end_last_completion_stream(); - - send.await.unwrap(); - let uri = MentionUri::File { - abs_path: path!("/a/b.md").into(), - } - .to_uri(); - acp_thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - formatdoc! {" - ## User - - What does [@b.md]({uri}) 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()), - format!("Explaining {}", path!("/a/b.md")) - )] - ); - 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), - formatdoc! {" - ## User - - What does [@b.md]({uri}) 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); - - LanguageModelRegistry::test(cx); - }); - } -} diff --git a/crates/agent/src/db.rs b/crates/agent/src/db.rs deleted file mode 100644 index 7a88c58705..0000000000 --- a/crates/agent/src/db.rs +++ /dev/null @@ -1,443 +0,0 @@ -use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent}; -use acp_thread::UserMessageId; -use agent_client_protocol as acp; -use agent_settings::{AgentProfileId, CompletionMode}; -use anyhow::{Result, anyhow}; -use chrono::{DateTime, Utc}; -use collections::{HashMap, IndexMap}; -use futures::{FutureExt, future::Shared}; -use gpui::{BackgroundExecutor, Global, Task}; -use indoc::indoc; -use parking_lot::Mutex; -use serde::{Deserialize, Serialize}; -use sqlez::{ - bindable::{Bind, Column}, - connection::Connection, - statement::Statement, -}; -use std::sync::Arc; -use ui::{App, SharedString}; -use zed_env_vars::ZED_STATELESS; - -pub type DbMessage = crate::Message; -pub type DbSummary = crate::legacy_thread::DetailedSummaryState; -pub type DbLanguageModel = crate::legacy_thread::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(crate::legacy_thread::SerializedThread::from_json( - json, - )?), - }, - _ => { - Self::upgrade_from_agent_1(crate::legacy_thread::SerializedThread::from_json(json)?) - } - } - } - - fn upgrade_from_agent_1(thread: crate::legacy_thread::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 { - crate::legacy_thread::SerializedMessageSegment::Text { text } => { - content.push(UserMessageContent::Text(text)); - } - crate::legacy_thread::SerializedMessageSegment::Thinking { - text, - .. - } => { - // User messages don't have thinking segments, but handle gracefully - content.push(UserMessageContent::Text(text)); - } - crate::legacy_thread::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 { - crate::legacy_thread::SerializedMessageSegment::Text { text } => { - content.push(AgentMessageContent::Text(text)); - } - crate::legacy_thread::SerializedMessageSegment::Thinking { - text, - signature, - } => { - content.push(AgentMessageContent::Thinking { text, signature }); - } - crate::legacy_thread::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, - thought_signature: None, - }, - )); - } - - // 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, - reasoning_details: None, - }) - } - 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 { - crate::legacy_thread::DetailedSummaryState::NotGenerated - | crate::legacy_thread::DetailedSummaryState::Generating => None, - crate::legacy_thread::DetailedSummaryState::Generated { text, .. } => Some(text), - }, - initial_project_snapshot: thread.initial_project_snapshot, - cumulative_token_usage: thread.cumulative_token_usage, - request_token_usage, - model: thread.model, - completion_mode: thread.completion_mode, - profile: thread.profile, - }) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum DataType { - #[serde(rename = "json")] - Json, - #[serde(rename = "zstd")] - Zstd, -} - -impl Bind for DataType { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - 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::new(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(()) - }) - } - - pub fn delete_threads(&self) -> 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 - "})?; - - delete(())?; - - Ok(()) - }) - } -} diff --git a/crates/agent/src/edit_agent.rs b/crates/agent/src/edit_agent.rs deleted file mode 100644 index 5ea04729a4..0000000000 --- a/crates/agent/src/edit_agent.rs +++ /dev/null @@ -1,1505 +0,0 @@ -mod create_file_parser; -mod edit_parser; -#[cfg(test)] -mod evals; -mod streaming_fuzzy_matcher; - -use crate::{Template, Templates}; -use action_log::ActionLog; -use anyhow::Result; -use cloud_llm_client::CompletionIntent; -use create_file_parser::{CreateFileParser, CreateFileParserEvent}; -pub use edit_parser::EditFormat; -use edit_parser::{EditParser, EditParserEvent, EditParserMetrics}; -use futures::{ - Stream, StreamExt, - channel::mpsc::{self, UnboundedReceiver}, - pin_mut, - stream::BoxStream, -}; -use gpui::{AppContext, AsyncApp, Entity, Task}; -use language::{Anchor, Buffer, BufferSnapshot, LineIndent, Point, TextBufferSnapshot}; -use language_model::{ - LanguageModel, LanguageModelCompletionError, LanguageModelRequest, LanguageModelRequestMessage, - LanguageModelToolChoice, MessageContent, Role, -}; -use project::{AgentLocation, Project}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::{cmp, iter, mem, ops::Range, pin::Pin, sync::Arc, task::Poll}; -use streaming_diff::{CharOperation, StreamingDiff}; -use streaming_fuzzy_matcher::StreamingFuzzyMatcher; - -#[derive(Serialize)] -struct CreateFilePromptTemplate { - path: Option, - edit_description: String, -} - -impl Template for CreateFilePromptTemplate { - const TEMPLATE_NAME: &'static str = "create_file_prompt.hbs"; -} - -#[derive(Serialize)] -struct EditFileXmlPromptTemplate { - path: Option, - edit_description: String, -} - -impl Template for EditFileXmlPromptTemplate { - const TEMPLATE_NAME: &'static str = "edit_file_prompt_xml.hbs"; -} - -#[derive(Serialize)] -struct EditFileDiffFencedPromptTemplate { - path: Option, - edit_description: String, -} - -impl Template for EditFileDiffFencedPromptTemplate { - const TEMPLATE_NAME: &'static str = "edit_file_prompt_diff_fenced.hbs"; -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum EditAgentOutputEvent { - ResolvingEditRange(Range), - UnresolvedEditRange, - AmbiguousEditRange(Vec>), - Edited(Range), -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct EditAgentOutput { - pub raw_edits: String, - pub parser_metrics: EditParserMetrics, -} - -#[derive(Clone)] -pub struct EditAgent { - model: Arc, - action_log: Entity, - project: Entity, - templates: Arc, - edit_format: EditFormat, -} - -impl EditAgent { - pub fn new( - model: Arc, - project: Entity, - action_log: Entity, - templates: Arc, - edit_format: EditFormat, - ) -> Self { - EditAgent { - model, - project, - action_log, - templates, - edit_format, - } - } - - pub fn overwrite( - &self, - buffer: Entity, - edit_description: String, - conversation: &LanguageModelRequest, - cx: &mut AsyncApp, - ) -> ( - Task>, - mpsc::UnboundedReceiver, - ) { - let this = self.clone(); - let (events_tx, events_rx) = mpsc::unbounded(); - let conversation = conversation.clone(); - let output = cx.spawn(async move |cx| { - let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?; - let path = cx.update(|cx| snapshot.resolve_file_path(true, cx))?; - let prompt = CreateFilePromptTemplate { - path, - edit_description, - } - .render(&this.templates)?; - let new_chunks = this - .request(conversation, CompletionIntent::CreateFile, prompt, cx) - .await?; - - let (output, mut inner_events) = this.overwrite_with_chunks(buffer, new_chunks, cx); - while let Some(event) = inner_events.next().await { - events_tx.unbounded_send(event).ok(); - } - output.await - }); - (output, events_rx) - } - - fn overwrite_with_chunks( - &self, - buffer: Entity, - edit_chunks: impl 'static + Send + Stream>, - cx: &mut AsyncApp, - ) -> ( - Task>, - mpsc::UnboundedReceiver, - ) { - let (output_events_tx, output_events_rx) = mpsc::unbounded(); - let (parse_task, parse_rx) = Self::parse_create_file_chunks(edit_chunks, cx); - let this = self.clone(); - let task = cx.spawn(async move |cx| { - this.action_log - .update(cx, |log, cx| log.buffer_created(buffer.clone(), cx))?; - this.overwrite_with_chunks_internal(buffer, parse_rx, output_events_tx, cx) - .await?; - parse_task.await - }); - (task, output_events_rx) - } - - async fn overwrite_with_chunks_internal( - &self, - buffer: Entity, - mut parse_rx: UnboundedReceiver>, - output_events_tx: mpsc::UnboundedSender, - cx: &mut AsyncApp, - ) -> Result<()> { - cx.update(|cx| { - buffer.update(cx, |buffer, cx| buffer.set_text("", cx)); - self.action_log.update(cx, |log, cx| { - log.buffer_edited(buffer.clone(), cx); - }); - self.project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: language::Anchor::max_for_buffer(buffer.read(cx).remote_id()), - }), - cx, - ) - }); - output_events_tx - .unbounded_send(EditAgentOutputEvent::Edited( - Anchor::min_max_range_for_buffer(buffer.read(cx).remote_id()), - )) - .ok(); - })?; - - while let Some(event) = parse_rx.next().await { - match event? { - CreateFileParserEvent::NewTextChunk { chunk } => { - let buffer_id = cx.update(|cx| { - buffer.update(cx, |buffer, cx| buffer.append(chunk, cx)); - self.action_log - .update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - self.project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: language::Anchor::max_for_buffer( - buffer.read(cx).remote_id(), - ), - }), - cx, - ) - }); - buffer.read(cx).remote_id() - })?; - output_events_tx - .unbounded_send(EditAgentOutputEvent::Edited( - Anchor::min_max_range_for_buffer(buffer_id), - )) - .ok(); - } - } - } - - Ok(()) - } - - pub fn edit( - &self, - buffer: Entity, - edit_description: String, - conversation: &LanguageModelRequest, - cx: &mut AsyncApp, - ) -> ( - Task>, - mpsc::UnboundedReceiver, - ) { - let this = self.clone(); - let (events_tx, events_rx) = mpsc::unbounded(); - let conversation = conversation.clone(); - let edit_format = self.edit_format; - let output = cx.spawn(async move |cx| { - let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?; - let path = cx.update(|cx| snapshot.resolve_file_path(true, cx))?; - let prompt = match edit_format { - EditFormat::XmlTags => EditFileXmlPromptTemplate { - path, - edit_description, - } - .render(&this.templates)?, - EditFormat::DiffFenced => EditFileDiffFencedPromptTemplate { - path, - edit_description, - } - .render(&this.templates)?, - }; - - let edit_chunks = this - .request(conversation, CompletionIntent::EditFile, prompt, cx) - .await?; - this.apply_edit_chunks(buffer, edit_chunks, events_tx, cx) - .await - }); - (output, events_rx) - } - - async fn apply_edit_chunks( - &self, - buffer: Entity, - edit_chunks: impl 'static + Send + Stream>, - output_events: mpsc::UnboundedSender, - cx: &mut AsyncApp, - ) -> Result { - self.action_log - .update(cx, |log, cx| log.buffer_read(buffer.clone(), cx))?; - - let (output, edit_events) = Self::parse_edit_chunks(edit_chunks, self.edit_format, cx); - let mut edit_events = edit_events.peekable(); - while let Some(edit_event) = Pin::new(&mut edit_events).peek().await { - // Skip events until we're at the start of a new edit. - let Ok(EditParserEvent::OldTextChunk { .. }) = edit_event else { - edit_events.next().await.unwrap()?; - continue; - }; - - let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?; - - // Resolve the old text in the background, updating the agent - // location as we keep refining which range it corresponds to. - let (resolve_old_text, mut old_range) = - Self::resolve_old_text(snapshot.text.clone(), edit_events, cx); - while let Ok(old_range) = old_range.recv().await { - if let Some(old_range) = old_range { - let old_range = snapshot.anchor_before(old_range.start) - ..snapshot.anchor_before(old_range.end); - self.project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: old_range.end, - }), - cx, - ); - })?; - output_events - .unbounded_send(EditAgentOutputEvent::ResolvingEditRange(old_range)) - .ok(); - } - } - - let (edit_events_, mut resolved_old_text) = resolve_old_text.await?; - edit_events = edit_events_; - - // If we can't resolve the old text, restart the loop waiting for a - // new edit (or for the stream to end). - let resolved_old_text = match resolved_old_text.len() { - 1 => resolved_old_text.pop().unwrap(), - 0 => { - output_events - .unbounded_send(EditAgentOutputEvent::UnresolvedEditRange) - .ok(); - continue; - } - _ => { - let ranges = resolved_old_text - .into_iter() - .map(|text| { - let start_line = - (snapshot.offset_to_point(text.range.start).row + 1) as usize; - let end_line = - (snapshot.offset_to_point(text.range.end).row + 1) as usize; - start_line..end_line - }) - .collect(); - output_events - .unbounded_send(EditAgentOutputEvent::AmbiguousEditRange(ranges)) - .ok(); - continue; - } - }; - - // Compute edits in the background and apply them as they become - // available. - let (compute_edits, edits) = - Self::compute_edits(snapshot, resolved_old_text, edit_events, cx); - let mut edits = edits.ready_chunks(32); - while let Some(edits) = edits.next().await { - if edits.is_empty() { - continue; - } - - // 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| { - buffer.edit(edits.iter().cloned(), None, cx); - let max_edit_end = buffer - .summaries_for_anchors::( - edits.iter().map(|(range, _)| &range.end), - ) - .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), - ) - }); - self.action_log - .update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - self.project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: max_edit_end, - }), - cx, - ); - }); - (min_edit_start, max_edit_end) - })?; - output_events - .unbounded_send(EditAgentOutputEvent::Edited(min_edit_start..max_edit_end)) - .ok(); - } - - edit_events = compute_edits.await?; - } - - output.await - } - - fn parse_edit_chunks( - chunks: impl 'static + Send + Stream>, - edit_format: EditFormat, - cx: &mut AsyncApp, - ) -> ( - Task>, - UnboundedReceiver>, - ) { - let (tx, rx) = mpsc::unbounded(); - let output = cx.background_spawn(async move { - pin_mut!(chunks); - - let mut parser = EditParser::new(edit_format); - let mut raw_edits = String::new(); - while let Some(chunk) = chunks.next().await { - match chunk { - Ok(chunk) => { - raw_edits.push_str(&chunk); - for event in parser.push(&chunk) { - tx.unbounded_send(Ok(event))?; - } - } - Err(error) => { - tx.unbounded_send(Err(error.into()))?; - } - } - } - Ok(EditAgentOutput { - raw_edits, - parser_metrics: parser.finish(), - }) - }); - (output, rx) - } - - fn parse_create_file_chunks( - chunks: impl 'static + Send + Stream>, - cx: &mut AsyncApp, - ) -> ( - Task>, - UnboundedReceiver>, - ) { - let (tx, rx) = mpsc::unbounded(); - let output = cx.background_spawn(async move { - pin_mut!(chunks); - - let mut parser = CreateFileParser::new(); - let mut raw_edits = String::new(); - while let Some(chunk) = chunks.next().await { - match chunk { - Ok(chunk) => { - raw_edits.push_str(&chunk); - for event in parser.push(Some(&chunk)) { - tx.unbounded_send(Ok(event))?; - } - } - Err(error) => { - tx.unbounded_send(Err(error.into()))?; - } - } - } - // Send final events with None to indicate completion - for event in parser.push(None) { - tx.unbounded_send(Ok(event))?; - } - Ok(EditAgentOutput { - raw_edits, - parser_metrics: EditParserMetrics::default(), - }) - }); - (output, rx) - } - - fn resolve_old_text( - snapshot: TextBufferSnapshot, - mut edit_events: T, - cx: &mut AsyncApp, - ) -> ( - Task)>>, - watch::Receiver>>, - ) - where - T: 'static + Send + Unpin + Stream>, - { - let (mut old_range_tx, old_range_rx) = watch::channel(None); - let task = cx.background_spawn(async move { - let mut matcher = StreamingFuzzyMatcher::new(snapshot); - while let Some(edit_event) = edit_events.next().await { - let EditParserEvent::OldTextChunk { - chunk, - done, - line_hint, - } = edit_event? - else { - break; - }; - - old_range_tx.send(matcher.push(&chunk, line_hint))?; - if done { - break; - } - } - - let matches = matcher.finish(); - let best_match = matcher.select_best_match(); - - old_range_tx.send(best_match.clone())?; - - let indent = LineIndent::from_iter( - matcher - .query_lines() - .first() - .unwrap_or(&String::new()) - .chars(), - ); - - let resolved_old_texts = if let Some(best_match) = best_match { - vec![ResolvedOldText { - range: best_match, - indent, - }] - } else { - matches - .into_iter() - .map(|range| ResolvedOldText { range, indent }) - .collect::>() - }; - - Ok((edit_events, resolved_old_texts)) - }); - - (task, old_range_rx) - } - - fn compute_edits( - snapshot: BufferSnapshot, - resolved_old_text: ResolvedOldText, - mut edit_events: T, - cx: &mut AsyncApp, - ) -> ( - Task>, - UnboundedReceiver<(Range, Arc)>, - ) - where - T: 'static + Send + Unpin + Stream>, - { - let (edits_tx, edits_rx) = mpsc::unbounded(); - let compute_edits = cx.background_spawn(async move { - let buffer_start_indent = snapshot - .line_indent_for_row(snapshot.offset_to_point(resolved_old_text.range.start).row); - let indent_delta = if buffer_start_indent.tabs > 0 { - IndentDelta::Tabs( - buffer_start_indent.tabs as isize - resolved_old_text.indent.tabs as isize, - ) - } else { - IndentDelta::Spaces( - buffer_start_indent.spaces as isize - resolved_old_text.indent.spaces as isize, - ) - }; - - let old_text = snapshot - .text_for_range(resolved_old_text.range.clone()) - .collect::(); - let mut diff = StreamingDiff::new(old_text); - let mut edit_start = resolved_old_text.range.start; - let mut new_text_chunks = - Self::reindent_new_text_chunks(indent_delta, &mut edit_events); - let mut done = false; - while !done { - let char_operations = if let Some(new_text_chunk) = new_text_chunks.next().await { - diff.push_new(&new_text_chunk?) - } else { - done = true; - mem::take(&mut diff).finish() - }; - - for op in char_operations { - match op { - CharOperation::Insert { text } => { - let edit_start = snapshot.anchor_after(edit_start); - edits_tx.unbounded_send((edit_start..edit_start, Arc::from(text)))?; - } - CharOperation::Delete { bytes } => { - let edit_end = edit_start + bytes; - let edit_range = - snapshot.anchor_after(edit_start)..snapshot.anchor_before(edit_end); - edit_start = edit_end; - edits_tx.unbounded_send((edit_range, Arc::from("")))?; - } - CharOperation::Keep { bytes } => edit_start += bytes, - } - } - } - - drop(new_text_chunks); - anyhow::Ok(edit_events) - }); - - (compute_edits, edits_rx) - } - - fn reindent_new_text_chunks( - delta: IndentDelta, - mut stream: impl Unpin + Stream>, - ) -> impl Stream> { - let mut buffer = String::new(); - let mut in_leading_whitespace = true; - let mut done = false; - futures::stream::poll_fn(move |cx| { - while !done { - let (chunk, is_last_chunk) = match stream.poll_next_unpin(cx) { - Poll::Ready(Some(Ok(EditParserEvent::NewTextChunk { chunk, done }))) => { - (chunk, done) - } - Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))), - Poll::Pending => return Poll::Pending, - _ => return Poll::Ready(None), - }; - - buffer.push_str(&chunk); - - let mut indented_new_text = String::new(); - let mut start_ix = 0; - let mut newlines = buffer.match_indices('\n').peekable(); - loop { - let (line_end, is_pending_line) = match newlines.next() { - Some((ix, _)) => (ix, false), - None => (buffer.len(), true), - }; - let line = &buffer[start_ix..line_end]; - - if in_leading_whitespace { - if let Some(non_whitespace_ix) = line.find(|c| delta.character() != c) { - // We found a non-whitespace character, adjust - // indentation based on the delta. - let new_indent_len = - cmp::max(0, non_whitespace_ix as isize + delta.len()) as usize; - indented_new_text - .extend(iter::repeat(delta.character()).take(new_indent_len)); - indented_new_text.push_str(&line[non_whitespace_ix..]); - in_leading_whitespace = false; - } else if is_pending_line { - // We're still in leading whitespace and this line is incomplete. - // Stop processing until we receive more input. - break; - } else { - // This line is entirely whitespace. Push it without indentation. - indented_new_text.push_str(line); - } - } else { - indented_new_text.push_str(line); - } - - if is_pending_line { - start_ix = line_end; - break; - } else { - in_leading_whitespace = true; - indented_new_text.push('\n'); - start_ix = line_end + 1; - } - } - buffer.replace_range(..start_ix, ""); - - // This was the last chunk, push all the buffered content as-is. - if is_last_chunk { - indented_new_text.push_str(&buffer); - buffer.clear(); - done = true; - } - - if !indented_new_text.is_empty() { - return Poll::Ready(Some(Ok(indented_new_text))); - } - } - - Poll::Ready(None) - }) - } - - async fn request( - &self, - mut conversation: LanguageModelRequest, - intent: CompletionIntent, - prompt: String, - 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(); - - // We just removed pending tool uses from the content of the - // last message, so it doesn't make sense to cache it anymore - // (e.g., the message will look very different on the next - // request). Thus, we move the flag to the message prior to it, - // as it will still be a valid prefix of the conversation. - if old_content_len != new_content_len - && last_message.cache - && let Some(prev_message) = messages_iter.next_back() - { - last_message.cache = false; - prev_message.cache = true; - } - - if last_message.content.is_empty() { - conversation.messages.pop(); - } - } - - conversation.messages.push(LanguageModelRequestMessage { - role: Role::User, - content: vec![MessageContent::Text(prompt)], - cache: false, - reasoning_details: None, - }); - - // Include tools in the request so that we can take advantage of - // caching when ToolChoice::None is supported. - let mut tool_choice = None; - let mut tools = Vec::new(); - if !conversation.tools.is_empty() - && self - .model - .supports_tool_choice(LanguageModelToolChoice::None) - { - tool_choice = Some(LanguageModelToolChoice::None); - tools = conversation.tools.clone(); - } - - let request = LanguageModelRequest { - thread_id: conversation.thread_id, - prompt_id: conversation.prompt_id, - intent: Some(intent), - mode: conversation.mode, - messages: conversation.messages, - tool_choice, - tools, - stop: Vec::new(), - temperature: None, - thinking_allowed: true, - }; - - Ok(self.model.stream_completion_text(request, cx).await?.stream) - } -} - -struct ResolvedOldText { - range: Range, - indent: LineIndent, -} - -#[derive(Copy, Clone, Debug)] -enum IndentDelta { - Spaces(isize), - Tabs(isize), -} - -impl IndentDelta { - fn character(&self) -> char { - match self { - IndentDelta::Spaces(_) => ' ', - IndentDelta::Tabs(_) => '\t', - } - } - - fn len(&self) -> isize { - match self { - IndentDelta::Spaces(n) => *n, - IndentDelta::Tabs(n) => *n, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use fs::FakeFs; - use futures::stream; - 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; - use std::cmp; - - #[gpui::test(iterations = 100)] - async fn test_empty_old_text(cx: &mut TestAppContext, mut rng: StdRng) { - let agent = init_test(cx).await; - let buffer = cx.new(|cx| { - Buffer::local( - indoc! {" - abc - def - ghi - "}, - cx, - ) - }); - let (apply, _events) = agent.edit( - buffer.clone(), - String::new(), - &LanguageModelRequest::default(), - &mut cx.to_async(), - ); - cx.run_until_parked(); - - simulate_llm_output( - &agent, - indoc! {" - - jkl - def - DEF - "}, - &mut rng, - cx, - ); - apply.await.unwrap(); - - pretty_assertions::assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - indoc! {" - abc - DEF - ghi - "} - ); - } - - #[gpui::test(iterations = 100)] - async fn test_indentation(cx: &mut TestAppContext, mut rng: StdRng) { - let agent = init_test(cx).await; - let buffer = cx.new(|cx| { - Buffer::local( - indoc! {" - lorem - ipsum - dolor - sit - "}, - cx, - ) - }); - let (apply, _events) = agent.edit( - buffer.clone(), - String::new(), - &LanguageModelRequest::default(), - &mut cx.to_async(), - ); - cx.run_until_parked(); - - simulate_llm_output( - &agent, - indoc! {" - - ipsum - dolor - sit - - - ipsum - dolor - sit - amet - - "}, - &mut rng, - cx, - ); - apply.await.unwrap(); - - pretty_assertions::assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - indoc! {" - lorem - ipsum - dolor - sit - amet - "} - ); - } - - #[gpui::test(iterations = 100)] - async fn test_dependent_edits(cx: &mut TestAppContext, mut rng: StdRng) { - let agent = init_test(cx).await; - let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi", cx)); - let (apply, _events) = agent.edit( - buffer.clone(), - String::new(), - &LanguageModelRequest::default(), - &mut cx.to_async(), - ); - cx.run_until_parked(); - - simulate_llm_output( - &agent, - indoc! {" - - def - - - DEF - - - - DEF - - - DeF - - "}, - &mut rng, - cx, - ); - apply.await.unwrap(); - - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abc\nDeF\nghi" - ); - } - - #[gpui::test(iterations = 100)] - async fn test_old_text_hallucination(cx: &mut TestAppContext, mut rng: StdRng) { - let agent = init_test(cx).await; - let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi", cx)); - let (apply, _events) = agent.edit( - buffer.clone(), - String::new(), - &LanguageModelRequest::default(), - &mut cx.to_async(), - ); - cx.run_until_parked(); - - simulate_llm_output( - &agent, - indoc! {" - - jkl - - - mno - - - - abc - - - ABC - - "}, - &mut rng, - cx, - ); - apply.await.unwrap(); - - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "ABC\ndef\nghi" - ); - } - - #[gpui::test] - async fn test_edit_events(cx: &mut TestAppContext) { - let agent = init_test(cx).await; - let model = agent.model.as_fake(); - let project = agent - .action_log - .read_with(cx, |log, _| log.project().clone()); - let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl", cx)); - - let mut async_cx = cx.to_async(); - let (apply, mut events) = agent.edit( - buffer.clone(), - String::new(), - &LanguageModelRequest::default(), - &mut async_cx, - ); - cx.run_until_parked(); - - model.send_last_completion_stream_text_chunk("a"); - cx.run_until_parked(); - assert_eq!(drain_events(&mut events), vec![]); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abc\ndef\nghi\njkl" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - None - ); - - model.send_last_completion_stream_text_chunk("bc"); - cx.run_until_parked(); - assert_eq!( - drain_events(&mut events), - vec![EditAgentOutputEvent::ResolvingEditRange(buffer.read_with( - cx, - |buffer, _| buffer.anchor_before(Point::new(0, 0)) - ..buffer.anchor_before(Point::new(0, 3)) - ))] - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abc\ndef\nghi\njkl" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 3))) - }) - ); - - model.send_last_completion_stream_text_chunk("abX"); - cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited(_)] - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abXc\ndef\nghi\njkl" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 3))) - }) - ); - - model.send_last_completion_stream_text_chunk("cY"); - cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited { .. }] - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abXcY\ndef\nghi\njkl" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5))) - }) - ); - - model.send_last_completion_stream_text_chunk(""); - model.send_last_completion_stream_text_chunk("hall"); - cx.run_until_parked(); - assert_eq!(drain_events(&mut events), vec![]); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abXcY\ndef\nghi\njkl" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5))) - }) - ); - - model.send_last_completion_stream_text_chunk("ucinated old"); - model.send_last_completion_stream_text_chunk(""); - cx.run_until_parked(); - assert_eq!( - drain_events(&mut events), - vec![EditAgentOutputEvent::UnresolvedEditRange] - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abXcY\ndef\nghi\njkl" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5))) - }) - ); - - model.send_last_completion_stream_text_chunk("hallucinated new"); - cx.run_until_parked(); - assert_eq!(drain_events(&mut events), vec![]); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abXcY\ndef\nghi\njkl" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5))) - }) - ); - - model.send_last_completion_stream_text_chunk("\nghi\nj"); - cx.run_until_parked(); - assert_eq!( - drain_events(&mut events), - vec![EditAgentOutputEvent::ResolvingEditRange(buffer.read_with( - cx, - |buffer, _| buffer.anchor_before(Point::new(2, 0)) - ..buffer.anchor_before(Point::new(2, 3)) - ))] - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abXcY\ndef\nghi\njkl" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(2, 3))) - }) - ); - - model.send_last_completion_stream_text_chunk("kl"); - model.send_last_completion_stream_text_chunk(""); - cx.run_until_parked(); - assert_eq!( - drain_events(&mut events), - vec![EditAgentOutputEvent::ResolvingEditRange(buffer.read_with( - cx, - |buffer, _| buffer.anchor_before(Point::new(2, 0)) - ..buffer.anchor_before(Point::new(3, 3)) - ))] - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abXcY\ndef\nghi\njkl" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(3, 3))) - }) - ); - - model.send_last_completion_stream_text_chunk("GHI"); - cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited { .. }] - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abXcY\ndef\nGHI" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(2, 3))) - }) - ); - - model.end_last_completion_stream(); - apply.await.unwrap(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abXcY\ndef\nGHI" - ); - assert_eq!(drain_events(&mut events), vec![]); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(2, 3))) - }) - ); - } - - #[gpui::test] - async fn test_overwrite_events(cx: &mut TestAppContext) { - let agent = init_test(cx).await; - let project = agent - .action_log - .read_with(cx, |log, _| log.project().clone()); - let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi", cx)); - let (chunks_tx, chunks_rx) = mpsc::unbounded(); - let (apply, mut events) = agent.overwrite_with_chunks( - buffer.clone(), - chunks_rx.map(|chunk: &str| Ok(chunk.to_string())), - &mut cx.to_async(), - ); - - cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited(_)] - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: language::Anchor::max_for_buffer( - cx.update(|cx| buffer.read(cx).remote_id()) - ), - }) - ); - - chunks_tx.unbounded_send("```\njkl\n").unwrap(); - cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited { .. }] - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "jkl" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: language::Anchor::max_for_buffer( - cx.update(|cx| buffer.read(cx).remote_id()) - ), - }) - ); - - chunks_tx.unbounded_send("mno\n").unwrap(); - cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited { .. }] - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "jkl\nmno" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: language::Anchor::max_for_buffer( - cx.update(|cx| buffer.read(cx).remote_id()) - ), - }) - ); - - chunks_tx.unbounded_send("pqr\n```").unwrap(); - cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited(_)], - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "jkl\nmno\npqr" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: language::Anchor::max_for_buffer( - cx.update(|cx| buffer.read(cx).remote_id()) - ), - }) - ); - - drop(chunks_tx); - apply.await.unwrap(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "jkl\nmno\npqr" - ); - assert_eq!(drain_events(&mut events), vec![]); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: language::Anchor::max_for_buffer( - cx.update(|cx| buffer.read(cx).remote_id()) - ), - }) - ); - } - - #[gpui::test(iterations = 100)] - async fn test_indent_new_text_chunks(mut rng: StdRng) { - let chunks = to_random_chunks(&mut rng, " abc\n def\n ghi"); - let new_text_chunks = stream::iter(chunks.iter().enumerate().map(|(index, chunk)| { - Ok(EditParserEvent::NewTextChunk { - chunk: chunk.clone(), - done: index == chunks.len() - 1, - }) - })); - let indented_chunks = - EditAgent::reindent_new_text_chunks(IndentDelta::Spaces(2), new_text_chunks) - .collect::>() - .await; - let new_text = indented_chunks - .into_iter() - .collect::>() - .unwrap(); - assert_eq!(new_text, " abc\n def\n ghi"); - } - - #[gpui::test(iterations = 100)] - async fn test_outdent_new_text_chunks(mut rng: StdRng) { - let chunks = to_random_chunks(&mut rng, "\t\t\t\tabc\n\t\tdef\n\t\t\t\t\t\tghi"); - let new_text_chunks = stream::iter(chunks.iter().enumerate().map(|(index, chunk)| { - Ok(EditParserEvent::NewTextChunk { - chunk: chunk.clone(), - done: index == chunks.len() - 1, - }) - })); - let indented_chunks = - EditAgent::reindent_new_text_chunks(IndentDelta::Tabs(-2), new_text_chunks) - .collect::>() - .await; - let new_text = indented_chunks - .into_iter() - .collect::>() - .unwrap(); - assert_eq!(new_text, "\t\tabc\ndef\n\t\t\t\tghi"); - } - - #[gpui::test(iterations = 100)] - async fn test_random_indents(mut rng: StdRng) { - let len = rng.random_range(1..=100); - let new_text = util::RandomCharIter::new(&mut rng) - .with_simple_text() - .take(len) - .collect::(); - let new_text = new_text - .split('\n') - .map(|line| format!("{}{}", " ".repeat(rng.random_range(0..=8)), line)) - .collect::>() - .join("\n"); - let delta = IndentDelta::Spaces(rng.random_range(-4i8..=4i8) as isize); - - let chunks = to_random_chunks(&mut rng, &new_text); - let new_text_chunks = stream::iter(chunks.iter().enumerate().map(|(index, chunk)| { - Ok(EditParserEvent::NewTextChunk { - chunk: chunk.clone(), - done: index == chunks.len() - 1, - }) - })); - let reindented_chunks = EditAgent::reindent_new_text_chunks(delta, new_text_chunks) - .collect::>() - .await; - let actual_reindented_text = reindented_chunks - .into_iter() - .collect::>() - .unwrap(); - let expected_reindented_text = new_text - .split('\n') - .map(|line| { - if let Some(ix) = line.find(|c| c != ' ') { - let new_indent = cmp::max(0, ix as isize + delta.len()) as usize; - format!("{}{}", " ".repeat(new_indent), &line[ix..]) - } else { - line.to_string() - } - }) - .collect::>() - .join("\n"); - assert_eq!(actual_reindented_text, expected_reindented_text); - } - - fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec { - let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50)); - let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count); - chunk_indices.sort(); - chunk_indices.push(input.len()); - - let mut chunks = Vec::new(); - let mut last_ix = 0; - for chunk_ix in chunk_indices { - chunks.push(input[last_ix..chunk_ix].to_string()); - last_ix = chunk_ix; - } - chunks - } - - fn simulate_llm_output( - agent: &EditAgent, - output: &str, - rng: &mut StdRng, - cx: &mut TestAppContext, - ) { - let executor = cx.executor(); - let chunks = to_random_chunks(rng, output); - let model = agent.model.clone(); - 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().end_last_completion_stream(); - }) - .detach(); - } - - async fn init_test(cx: &mut TestAppContext) -> EditAgent { - cx.update(settings::init); - - let project = Project::test(FakeFs::new(cx.executor()), [], cx).await; - let model = Arc::new(FakeLanguageModel::default()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); - EditAgent::new( - model, - project, - action_log, - Templates::new(), - EditFormat::XmlTags, - ) - } - - #[gpui::test(iterations = 10)] - async fn test_non_unique_text_error(cx: &mut TestAppContext, mut rng: StdRng) { - let agent = init_test(cx).await; - let original_text = indoc! {" - function foo() { - return 42; - } - - function bar() { - return 42; - } - - function baz() { - return 42; - } - "}; - let buffer = cx.new(|cx| Buffer::local(original_text, cx)); - let (apply, mut events) = agent.edit( - buffer.clone(), - String::new(), - &LanguageModelRequest::default(), - &mut cx.to_async(), - ); - cx.run_until_parked(); - - // When matches text in more than one place - simulate_llm_output( - &agent, - indoc! {" - - return 42; - } - - - return 100; - } - - "}, - &mut rng, - cx, - ); - apply.await.unwrap(); - - // Then the text should remain unchanged - let result_text = buffer.read_with(cx, |buffer, _| buffer.snapshot().text()); - assert_eq!( - result_text, - indoc! {" - function foo() { - return 42; - } - - function bar() { - return 42; - } - - function baz() { - return 42; - } - "}, - "Text should remain unchanged when there are multiple matches" - ); - - // And AmbiguousEditRange even should be emitted - let events = drain_events(&mut events); - let ambiguous_ranges = vec![2..3, 6..7, 10..11]; - assert!( - events.contains(&EditAgentOutputEvent::AmbiguousEditRange(ambiguous_ranges)), - "Should emit AmbiguousEditRange for non-unique text" - ); - } - - fn drain_events( - stream: &mut UnboundedReceiver, - ) -> Vec { - let mut events = Vec::new(); - while let Ok(Some(event)) = stream.try_next() { - events.push(event); - } - events - } -} diff --git a/crates/agent/src/edit_agent/create_file_parser.rs b/crates/agent/src/edit_agent/create_file_parser.rs deleted file mode 100644 index 2272434d79..0000000000 --- a/crates/agent/src/edit_agent/create_file_parser.rs +++ /dev/null @@ -1,237 +0,0 @@ -use std::sync::OnceLock; - -use regex::Regex; -use smallvec::SmallVec; -use util::debug_panic; - -static START_MARKER: OnceLock = OnceLock::new(); -static END_MARKER: OnceLock = OnceLock::new(); - -#[derive(Debug)] -pub enum CreateFileParserEvent { - NewTextChunk { chunk: String }, -} - -#[derive(Debug)] -pub struct CreateFileParser { - state: ParserState, - buffer: String, -} - -#[derive(Debug, PartialEq)] -enum ParserState { - Pending, - WithinText, - Finishing, - Finished, -} - -impl CreateFileParser { - pub fn new() -> Self { - CreateFileParser { - state: ParserState::Pending, - buffer: String::new(), - } - } - - pub fn push(&mut self, chunk: Option<&str>) -> SmallVec<[CreateFileParserEvent; 1]> { - if chunk.is_none() { - self.state = ParserState::Finishing; - } - - let chunk = chunk.unwrap_or_default(); - - 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) { - self.buffer.drain(..m.end()); - self.state = ParserState::WithinText; - } else { - break; - } - } - ParserState::WithinText => { - let text = self.buffer.trim_end_matches(&['`', '\n', ' ']); - let text_len = text.len(); - - if text_len > 0 { - edit_events.push(CreateFileParserEvent::NewTextChunk { - chunk: self.buffer.drain(..text_len).collect(), - }); - } - break; - } - ParserState::Finishing => { - if let Some(m) = end_marker_regex.find(&self.buffer) { - self.buffer.drain(m.start()..); - } - if !self.buffer.is_empty() { - if !self.buffer.ends_with('\n') { - self.buffer.push('\n'); - } - edit_events.push(CreateFileParserEvent::NewTextChunk { - chunk: self.buffer.drain(..).collect(), - }); - } - self.state = ParserState::Finished; - break; - } - ParserState::Finished => debug_panic!("Can't call parser after finishing"), - } - } - edit_events - } -} - -#[cfg(test)] -mod tests { - use super::*; - use indoc::indoc; - use rand::prelude::*; - use std::cmp; - - #[gpui::test(iterations = 100)] - fn test_happy_path(mut rng: StdRng) { - let mut parser = CreateFileParser::new(); - assert_eq!( - parse_random_chunks("```\nHello world\n```", &mut parser, &mut rng), - "Hello world".to_string() - ); - } - - #[gpui::test(iterations = 100)] - fn test_cut_prefix(mut rng: StdRng) { - let mut parser = CreateFileParser::new(); - assert_eq!( - parse_random_chunks( - indoc! {" - Let me write this file for you: - - ``` - Hello world - ``` - - "}, - &mut parser, - &mut rng - ), - "Hello world".to_string() - ); - } - - #[gpui::test(iterations = 100)] - fn test_language_name_on_fences(mut rng: StdRng) { - let mut parser = CreateFileParser::new(); - assert_eq!( - parse_random_chunks( - indoc! {" - ```rust - Hello world - ``` - - "}, - &mut parser, - &mut rng - ), - "Hello world".to_string() - ); - } - - #[gpui::test(iterations = 100)] - fn test_leave_suffix(mut rng: StdRng) { - let mut parser = CreateFileParser::new(); - assert_eq!( - parse_random_chunks( - indoc! {" - Let me write this file for you: - - ``` - Hello world - ``` - - The end - "}, - &mut parser, - &mut rng - ), - // This output is malformed, so we're doing our best effort - "Hello world\n```\n\nThe end\n".to_string() - ); - } - - #[gpui::test(iterations = 100)] - fn test_inner_fences(mut rng: StdRng) { - let mut parser = CreateFileParser::new(); - assert_eq!( - parse_random_chunks( - indoc! {" - Let me write this file for you: - - ``` - ``` - Hello world - ``` - ``` - "}, - &mut parser, - &mut rng - ), - // This output is malformed, so we're doing our best effort - "```\nHello world\n```\n".to_string() - ); - } - - #[gpui::test(iterations = 10)] - fn test_empty_file(mut rng: StdRng) { - let mut parser = CreateFileParser::new(); - assert_eq!( - parse_random_chunks( - indoc! {" - ``` - ``` - "}, - &mut parser, - &mut rng - ), - "".to_string() - ); - } - - fn parse_random_chunks(input: &str, parser: &mut CreateFileParser, rng: &mut StdRng) -> String { - let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50)); - let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count); - chunk_indices.sort(); - chunk_indices.push(input.len()); - - let chunk_indices = chunk_indices - .into_iter() - .map(Some) - .chain(vec![None]) - .collect::>>(); - - let mut edit = String::default(); - let mut last_ix = 0; - for chunk_ix in chunk_indices { - let mut chunk = None; - if let Some(chunk_ix) = chunk_ix { - chunk = Some(&input[last_ix..chunk_ix]); - last_ix = chunk_ix; - } - - for event in parser.push(chunk) { - match event { - CreateFileParserEvent::NewTextChunk { chunk } => { - edit.push_str(&chunk); - } - } - } - } - edit - } -} diff --git a/crates/agent/src/edit_agent/edit_parser.rs b/crates/agent/src/edit_agent/edit_parser.rs deleted file mode 100644 index c1aa61e18d..0000000000 --- a/crates/agent/src/edit_agent/edit_parser.rs +++ /dev/null @@ -1,1094 +0,0 @@ -use anyhow::bail; -use derive_more::{Add, AddAssign}; -use language_model::LanguageModel; -use regex::Regex; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use smallvec::SmallVec; -use std::{mem, ops::Range, str::FromStr, sync::Arc}; - -const OLD_TEXT_END_TAG: &str = ""; -const NEW_TEXT_END_TAG: &str = ""; -const EDITS_END_TAG: &str = ""; -const SEARCH_MARKER: &str = "<<<<<<< SEARCH"; -const SEPARATOR_MARKER: &str = "======="; -const REPLACE_MARKER: &str = ">>>>>>> REPLACE"; -const SONNET_PARAMETER_INVOKE_1: &str = "\n"; -const SONNET_PARAMETER_INVOKE_2: &str = ""; -const SONNET_PARAMETER_INVOKE_3: &str = ""; -const END_TAGS: [&str; 6] = [ - OLD_TEXT_END_TAG, - NEW_TEXT_END_TAG, - EDITS_END_TAG, - SONNET_PARAMETER_INVOKE_1, // Remove these after switching to streaming tool call - SONNET_PARAMETER_INVOKE_2, - SONNET_PARAMETER_INVOKE_3, -]; - -#[derive(Debug)] -pub enum EditParserEvent { - OldTextChunk { - chunk: String, - done: bool, - line_hint: Option, - }, - NewTextChunk { - chunk: String, - done: bool, - }, -} - -#[derive( - Clone, Debug, Default, PartialEq, Eq, Add, AddAssign, Serialize, Deserialize, JsonSchema, -)] -pub struct EditParserMetrics { - pub tags: usize, - pub mismatched_tags: usize, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum EditFormat { - /// XML-like tags: - /// ... - /// ... - XmlTags, - /// Diff-fenced format, in which: - /// - Text before the SEARCH marker is ignored - /// - Fences are optional - /// - Line hint is optional. - /// - /// Example: - /// - /// ```diff - /// <<<<<<< SEARCH line=42 - /// ... - /// ======= - /// ... - /// >>>>>>> REPLACE - /// ``` - DiffFenced, -} - -impl FromStr for EditFormat { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - match s.to_lowercase().as_str() { - "xml_tags" | "xml" => Ok(EditFormat::XmlTags), - "diff_fenced" | "diff-fenced" | "diff" => Ok(EditFormat::DiffFenced), - _ => bail!("Unknown EditFormat: {}", s), - } - } -} - -impl EditFormat { - /// Return an optimal edit format for the language model - pub fn from_model(model: Arc) -> anyhow::Result { - if model.provider_id().0 == "google" || model.id().0.to_lowercase().contains("gemini") { - Ok(EditFormat::DiffFenced) - } else { - Ok(EditFormat::XmlTags) - } - } - - /// Return an optimal edit format for the language model, - /// with the ability to override it by setting the - /// `ZED_EDIT_FORMAT` environment variable - #[allow(dead_code)] - pub fn from_env(model: Arc) -> anyhow::Result { - let default = EditFormat::from_model(model)?; - std::env::var("ZED_EDIT_FORMAT").map_or(Ok(default), |s| EditFormat::from_str(&s)) - } -} - -pub trait EditFormatParser: Send + std::fmt::Debug { - fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]>; - fn take_metrics(&mut self) -> EditParserMetrics; -} - -#[derive(Debug)] -pub struct XmlEditParser { - state: XmlParserState, - buffer: String, - metrics: EditParserMetrics, -} - -#[derive(Debug, PartialEq)] -enum XmlParserState { - Pending, - WithinOldText { start: bool, line_hint: Option }, - AfterOldText, - WithinNewText { start: bool }, -} - -#[derive(Debug)] -pub struct DiffFencedEditParser { - state: DiffParserState, - buffer: String, - metrics: EditParserMetrics, -} - -#[derive(Debug, PartialEq)] -enum DiffParserState { - Pending, - WithinSearch { start: bool, line_hint: Option }, - WithinReplace { start: bool }, -} - -/// Main parser that delegates to format-specific parsers -pub struct EditParser { - parser: Box, -} - -impl XmlEditParser { - pub fn new() -> Self { - XmlEditParser { - state: XmlParserState::Pending, - buffer: String::new(), - metrics: EditParserMetrics::default(), - } - } - - fn find_end_tag(&self) -> Option> { - let (tag, start_ix) = END_TAGS - .iter() - .flat_map(|tag| Some((tag, self.buffer.find(tag)?))) - .min_by_key(|(_, ix)| *ix)?; - Some(start_ix..start_ix + tag.len()) - } - - fn ends_with_tag_prefix(&self) -> bool { - let mut end_prefixes = END_TAGS - .iter() - .flat_map(|tag| (1..tag.len()).map(move |i| &tag[..i])) - .chain(["\n"]); - end_prefixes.any(|prefix| self.buffer.ends_with(&prefix)) - } - - fn parse_line_hint(&self, tag: &str) -> Option { - use std::sync::LazyLock; - static LINE_HINT_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r#"line=(?:"?)(\d+)"#).unwrap()); - - LINE_HINT_REGEX - .captures(tag) - .and_then(|caps| caps.get(1)) - .and_then(|m| m.as_str().parse::().ok()) - } -} - -impl EditFormatParser for XmlEditParser { - fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]> { - self.buffer.push_str(chunk); - - let mut edit_events = SmallVec::new(); - loop { - match &mut self.state { - XmlParserState::Pending => { - if let Some(start) = self.buffer.find("') { - let tag_end = start + tag_end + 1; - let tag = &self.buffer[start..tag_end]; - let line_hint = self.parse_line_hint(tag); - self.buffer.drain(..tag_end); - self.state = XmlParserState::WithinOldText { - start: true, - line_hint, - }; - } else { - break; - } - } else { - break; - } - } - XmlParserState::WithinOldText { start, line_hint } => { - if !self.buffer.is_empty() { - if *start && self.buffer.starts_with('\n') { - self.buffer.remove(0); - } - *start = false; - } - - let line_hint = *line_hint; - if let Some(tag_range) = self.find_end_tag() { - let mut chunk = self.buffer[..tag_range.start].to_string(); - if chunk.ends_with('\n') { - chunk.pop(); - } - - self.metrics.tags += 1; - if &self.buffer[tag_range.clone()] != OLD_TEXT_END_TAG { - self.metrics.mismatched_tags += 1; - } - - self.buffer.drain(..tag_range.end); - self.state = XmlParserState::AfterOldText; - edit_events.push(EditParserEvent::OldTextChunk { - chunk, - done: true, - line_hint, - }); - } else { - if !self.ends_with_tag_prefix() { - edit_events.push(EditParserEvent::OldTextChunk { - chunk: mem::take(&mut self.buffer), - done: false, - line_hint, - }); - } - break; - } - } - XmlParserState::AfterOldText => { - if let Some(start) = self.buffer.find("") { - self.buffer.drain(..start + "".len()); - self.state = XmlParserState::WithinNewText { start: true }; - } else { - break; - } - } - XmlParserState::WithinNewText { start } => { - if !self.buffer.is_empty() { - if *start && self.buffer.starts_with('\n') { - self.buffer.remove(0); - } - *start = false; - } - - if let Some(tag_range) = self.find_end_tag() { - let mut chunk = self.buffer[..tag_range.start].to_string(); - if chunk.ends_with('\n') { - chunk.pop(); - } - - self.metrics.tags += 1; - if &self.buffer[tag_range.clone()] != NEW_TEXT_END_TAG { - self.metrics.mismatched_tags += 1; - } - - self.buffer.drain(..tag_range.end); - self.state = XmlParserState::Pending; - edit_events.push(EditParserEvent::NewTextChunk { chunk, done: true }); - } else { - if !self.ends_with_tag_prefix() { - edit_events.push(EditParserEvent::NewTextChunk { - chunk: mem::take(&mut self.buffer), - done: false, - }); - } - break; - } - } - } - } - edit_events - } - - fn take_metrics(&mut self) -> EditParserMetrics { - std::mem::take(&mut self.metrics) - } -} - -impl DiffFencedEditParser { - pub fn new() -> Self { - DiffFencedEditParser { - state: DiffParserState::Pending, - buffer: String::new(), - metrics: EditParserMetrics::default(), - } - } - - fn ends_with_diff_marker_prefix(&self) -> bool { - let diff_markers = [SEPARATOR_MARKER, REPLACE_MARKER]; - let mut diff_prefixes = diff_markers - .iter() - .flat_map(|marker| (1..marker.len()).map(move |i| &marker[..i])) - .chain(["\n"]); - diff_prefixes.any(|prefix| self.buffer.ends_with(&prefix)) - } - - fn parse_line_hint(&self, search_line: &str) -> Option { - use regex::Regex; - use std::sync::LazyLock; - static LINE_HINT_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r#"line=(?:"?)(\d+)"#).unwrap()); - - LINE_HINT_REGEX - .captures(search_line) - .and_then(|caps| caps.get(1)) - .and_then(|m| m.as_str().parse::().ok()) - } -} - -impl EditFormatParser for DiffFencedEditParser { - fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]> { - self.buffer.push_str(chunk); - - let mut edit_events = SmallVec::new(); - loop { - match &mut self.state { - DiffParserState::Pending => { - if let Some(diff) = self.buffer.find(SEARCH_MARKER) { - let search_end = diff + SEARCH_MARKER.len(); - if let Some(newline_pos) = self.buffer[search_end..].find('\n') { - let search_line = &self.buffer[diff..search_end + newline_pos]; - let line_hint = self.parse_line_hint(search_line); - self.buffer.drain(..search_end + newline_pos + 1); - self.state = DiffParserState::WithinSearch { - start: true, - line_hint, - }; - } else { - break; - } - } else { - break; - } - } - DiffParserState::WithinSearch { start, line_hint } => { - if !self.buffer.is_empty() { - if *start && self.buffer.starts_with('\n') { - self.buffer.remove(0); - } - *start = false; - } - - let line_hint = *line_hint; - if let Some(separator_pos) = self.buffer.find(SEPARATOR_MARKER) { - let mut chunk = self.buffer[..separator_pos].to_string(); - if chunk.ends_with('\n') { - chunk.pop(); - } - - let separator_end = separator_pos + SEPARATOR_MARKER.len(); - if let Some(newline_pos) = self.buffer[separator_end..].find('\n') { - self.buffer.drain(..separator_end + newline_pos + 1); - self.state = DiffParserState::WithinReplace { start: true }; - edit_events.push(EditParserEvent::OldTextChunk { - chunk, - done: true, - line_hint, - }); - } else { - break; - } - } else { - if !self.ends_with_diff_marker_prefix() { - edit_events.push(EditParserEvent::OldTextChunk { - chunk: mem::take(&mut self.buffer), - done: false, - line_hint, - }); - } - break; - } - } - DiffParserState::WithinReplace { start } => { - if !self.buffer.is_empty() { - if *start && self.buffer.starts_with('\n') { - self.buffer.remove(0); - } - *start = false; - } - - if let Some(replace_pos) = self.buffer.find(REPLACE_MARKER) { - let mut chunk = self.buffer[..replace_pos].to_string(); - if chunk.ends_with('\n') { - chunk.pop(); - } - - self.buffer.drain(..replace_pos + REPLACE_MARKER.len()); - if let Some(newline_pos) = self.buffer.find('\n') { - self.buffer.drain(..newline_pos + 1); - } else { - self.buffer.clear(); - } - - self.state = DiffParserState::Pending; - edit_events.push(EditParserEvent::NewTextChunk { chunk, done: true }); - } else { - if !self.ends_with_diff_marker_prefix() { - edit_events.push(EditParserEvent::NewTextChunk { - chunk: mem::take(&mut self.buffer), - done: false, - }); - } - break; - } - } - } - } - edit_events - } - - fn take_metrics(&mut self) -> EditParserMetrics { - std::mem::take(&mut self.metrics) - } -} - -impl EditParser { - pub fn new(format: EditFormat) -> Self { - let parser: Box = match format { - EditFormat::XmlTags => Box::new(XmlEditParser::new()), - EditFormat::DiffFenced => Box::new(DiffFencedEditParser::new()), - }; - EditParser { parser } - } - - pub fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]> { - self.parser.push(chunk) - } - - pub fn finish(mut self) -> EditParserMetrics { - self.parser.take_metrics() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use indoc::indoc; - use rand::prelude::*; - use std::cmp; - - #[gpui::test(iterations = 1000)] - fn test_xml_single_edit(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::XmlTags); - assert_eq!( - parse_random_chunks( - "originalupdated", - &mut parser, - &mut rng - ), - vec![Edit { - old_text: "original".to_string(), - new_text: "updated".to_string(), - line_hint: None, - }] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 2, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 1000)] - fn test_xml_multiple_edits(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::XmlTags); - assert_eq!( - parse_random_chunks( - indoc! {" - - first old - first new - second old - second new - - "}, - &mut parser, - &mut rng - ), - vec![ - Edit { - old_text: "first old".to_string(), - new_text: "first new".to_string(), - line_hint: None, - }, - Edit { - old_text: "second old".to_string(), - new_text: "second new".to_string(), - line_hint: None, - }, - ] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 4, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 1000)] - fn test_xml_edits_with_extra_text(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::XmlTags); - assert_eq!( - parse_random_chunks( - indoc! {" - ignore this - contentextra stuffupdated contenttrailing data - more text second item - middle textmodified second itemend - third caseimproved third case with trailing text - "}, - &mut parser, - &mut rng - ), - vec![ - Edit { - old_text: "content".to_string(), - new_text: "updated content".to_string(), - line_hint: None, - }, - Edit { - old_text: "second item".to_string(), - new_text: "modified second item".to_string(), - line_hint: None, - }, - Edit { - old_text: "third case".to_string(), - new_text: "improved third case".to_string(), - line_hint: None, - }, - ] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 6, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 1000)] - fn test_xml_edits_with_closing_parameter_invoke(mut rng: StdRng) { - // This case is a regression with Claude Sonnet 4.5. - // Sometimes Sonnet thinks that it's doing a tool call - // and closes its response with '' - // instead of properly closing - - let mut parser = EditParser::new(EditFormat::XmlTags); - assert_eq!( - parse_random_chunks( - indoc! {" - some textupdated text - more textupd - "}, - &mut parser, - &mut rng - ), - vec![ - Edit { - old_text: "some text".to_string(), - new_text: "updated text".to_string(), - line_hint: None, - }, - Edit { - old_text: "more text".to_string(), - new_text: "upd".to_string(), - line_hint: None, - }, - ] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 4, - mismatched_tags: 2 - } - ); - } - - #[gpui::test(iterations = 1000)] - fn test_xml_nested_tags(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::XmlTags); - assert_eq!( - parse_random_chunks( - "code with nested elementsnew content", - &mut parser, - &mut rng - ), - vec![Edit { - old_text: "code with nested elements".to_string(), - new_text: "new content".to_string(), - line_hint: None, - }] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 2, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 1000)] - fn test_xml_empty_old_and_new_text(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::XmlTags); - assert_eq!( - parse_random_chunks( - "", - &mut parser, - &mut rng - ), - vec![Edit { - old_text: "".to_string(), - new_text: "".to_string(), - line_hint: None, - }] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 2, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 100)] - fn test_xml_multiline_content(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::XmlTags); - assert_eq!( - parse_random_chunks( - "line1\nline2\nline3line1\nmodified line2\nline3", - &mut parser, - &mut rng - ), - vec![Edit { - old_text: "line1\nline2\nline3".to_string(), - new_text: "line1\nmodified line2\nline3".to_string(), - line_hint: None, - }] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 2, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 1000)] - fn test_xml_mismatched_tags(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::XmlTags); - assert_eq!( - parse_random_chunks( - // Reduced from an actual Sonnet 3.7 output - indoc! {" - - a - b - c - - - a - B - c - - - d - e - f - - - D - e - F - - "}, - &mut parser, - &mut rng - ), - vec![ - Edit { - old_text: "a\nb\nc".to_string(), - new_text: "a\nB\nc".to_string(), - line_hint: None, - }, - Edit { - old_text: "d\ne\nf".to_string(), - new_text: "D\ne\nF".to_string(), - line_hint: None, - } - ] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 4, - mismatched_tags: 4 - } - ); - - let mut parser = EditParser::new(EditFormat::XmlTags); - assert_eq!( - parse_random_chunks( - // Reduced from an actual Opus 4 output - indoc! {" - - - Lorem - - - LOREM - - "}, - &mut parser, - &mut rng - ), - vec![Edit { - old_text: "Lorem".to_string(), - new_text: "LOREM".to_string(), - line_hint: None, - },] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 2, - mismatched_tags: 1 - } - ); - } - - #[gpui::test(iterations = 1000)] - fn test_diff_fenced_single_edit(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::DiffFenced); - assert_eq!( - parse_random_chunks( - indoc! {" - <<<<<<< SEARCH - original text - ======= - updated text - >>>>>>> REPLACE - "}, - &mut parser, - &mut rng - ), - vec![Edit { - old_text: "original text".to_string(), - new_text: "updated text".to_string(), - line_hint: None, - }] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 0, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 100)] - fn test_diff_fenced_with_markdown_fences(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::DiffFenced); - assert_eq!( - parse_random_chunks( - indoc! {" - ```diff - <<<<<<< SEARCH - from flask import Flask - ======= - import math - from flask import Flask - >>>>>>> REPLACE - ``` - "}, - &mut parser, - &mut rng - ), - vec![Edit { - old_text: "from flask import Flask".to_string(), - new_text: "import math\nfrom flask import Flask".to_string(), - line_hint: None, - }] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 0, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 100)] - fn test_diff_fenced_multiple_edits(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::DiffFenced); - assert_eq!( - parse_random_chunks( - indoc! {" - <<<<<<< SEARCH - first old - ======= - first new - >>>>>>> REPLACE - - <<<<<<< SEARCH - second old - ======= - second new - >>>>>>> REPLACE - "}, - &mut parser, - &mut rng - ), - vec![ - Edit { - old_text: "first old".to_string(), - new_text: "first new".to_string(), - line_hint: None, - }, - Edit { - old_text: "second old".to_string(), - new_text: "second new".to_string(), - line_hint: None, - }, - ] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 0, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 100)] - fn test_mixed_formats(mut rng: StdRng) { - // Test XML format parser only parses XML tags - let mut xml_parser = EditParser::new(EditFormat::XmlTags); - assert_eq!( - parse_random_chunks( - indoc! {" - xml style oldxml style new - - <<<<<<< SEARCH - diff style old - ======= - diff style new - >>>>>>> REPLACE - "}, - &mut xml_parser, - &mut rng - ), - vec![Edit { - old_text: "xml style old".to_string(), - new_text: "xml style new".to_string(), - line_hint: None, - },] - ); - assert_eq!( - xml_parser.finish(), - EditParserMetrics { - tags: 2, - mismatched_tags: 0 - } - ); - - // Test diff-fenced format parser only parses diff markers - let mut diff_parser = EditParser::new(EditFormat::DiffFenced); - assert_eq!( - parse_random_chunks( - indoc! {" - xml style oldxml style new - - <<<<<<< SEARCH - diff style old - ======= - diff style new - >>>>>>> REPLACE - "}, - &mut diff_parser, - &mut rng - ), - vec![Edit { - old_text: "diff style old".to_string(), - new_text: "diff style new".to_string(), - line_hint: None, - },] - ); - assert_eq!( - diff_parser.finish(), - EditParserMetrics { - tags: 0, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 100)] - fn test_diff_fenced_empty_sections(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::DiffFenced); - assert_eq!( - parse_random_chunks( - indoc! {" - <<<<<<< SEARCH - ======= - >>>>>>> REPLACE - "}, - &mut parser, - &mut rng - ), - vec![Edit { - old_text: "".to_string(), - new_text: "".to_string(), - line_hint: None, - }] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 0, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 100)] - fn test_diff_fenced_with_line_hint(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::DiffFenced); - let edits = parse_random_chunks( - indoc! {" - <<<<<<< SEARCH line=42 - original text - ======= - updated text - >>>>>>> REPLACE - "}, - &mut parser, - &mut rng, - ); - assert_eq!( - edits, - vec![Edit { - old_text: "original text".to_string(), - line_hint: Some(42), - new_text: "updated text".to_string(), - }] - ); - } - #[gpui::test(iterations = 100)] - fn test_xml_line_hints(mut rng: StdRng) { - // Line hint is a single quoted line number - let mut parser = EditParser::new(EditFormat::XmlTags); - - let edits = parse_random_chunks( - r#" - original code - updated code"#, - &mut parser, - &mut rng, - ); - - assert_eq!(edits.len(), 1); - assert_eq!(edits[0].old_text, "original code"); - assert_eq!(edits[0].line_hint, Some(23)); - assert_eq!(edits[0].new_text, "updated code"); - - // Line hint is a single unquoted line number - let mut parser = EditParser::new(EditFormat::XmlTags); - - let edits = parse_random_chunks( - r#" - original code - updated code"#, - &mut parser, - &mut rng, - ); - - assert_eq!(edits.len(), 1); - assert_eq!(edits[0].old_text, "original code"); - assert_eq!(edits[0].line_hint, Some(45)); - assert_eq!(edits[0].new_text, "updated code"); - - // Line hint is a range - let mut parser = EditParser::new(EditFormat::XmlTags); - - let edits = parse_random_chunks( - r#" - original code - updated code"#, - &mut parser, - &mut rng, - ); - - assert_eq!(edits.len(), 1); - assert_eq!(edits[0].old_text, "original code"); - assert_eq!(edits[0].line_hint, Some(23)); - assert_eq!(edits[0].new_text, "updated code"); - - // No line hint - let mut parser = EditParser::new(EditFormat::XmlTags); - let edits = parse_random_chunks( - r#" - old - new"#, - &mut parser, - &mut rng, - ); - - assert_eq!(edits.len(), 1); - assert_eq!(edits[0].old_text, "old"); - assert_eq!(edits[0].line_hint, None); - assert_eq!(edits[0].new_text, "new"); - } - - #[derive(Default, Debug, PartialEq, Eq)] - struct Edit { - old_text: String, - new_text: String, - line_hint: Option, - } - - fn parse_random_chunks(input: &str, parser: &mut EditParser, rng: &mut StdRng) -> Vec { - let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50)); - let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count); - chunk_indices.sort(); - chunk_indices.push(input.len()); - - let mut old_text = Some(String::new()); - let mut new_text = None; - let mut pending_edit = Edit::default(); - let mut edits = Vec::new(); - let mut last_ix = 0; - for chunk_ix in chunk_indices { - for event in parser.push(&input[last_ix..chunk_ix]) { - match event { - EditParserEvent::OldTextChunk { - chunk, - done, - line_hint, - } => { - old_text.as_mut().unwrap().push_str(&chunk); - if done { - pending_edit.old_text = old_text.take().unwrap(); - pending_edit.line_hint = line_hint; - new_text = Some(String::new()); - } - } - EditParserEvent::NewTextChunk { chunk, done } => { - new_text.as_mut().unwrap().push_str(&chunk); - if done { - pending_edit.new_text = new_text.take().unwrap(); - edits.push(pending_edit); - pending_edit = Edit::default(); - old_text = Some(String::new()); - } - } - } - } - last_ix = chunk_ix; - } - - if new_text.is_some() { - pending_edit.new_text = new_text.take().unwrap(); - edits.push(pending_edit); - } - - edits - } -} diff --git a/crates/agent/src/edit_agent/evals.rs b/crates/agent/src/edit_agent/evals.rs deleted file mode 100644 index edf8a0f671..0000000000 --- a/crates/agent/src/edit_agent/evals.rs +++ /dev/null @@ -1,1684 +0,0 @@ -use super::*; -use crate::{ - EditFileMode, EditFileToolInput, GrepToolInput, ListDirectoryToolInput, ReadFileToolInput, -}; -use Role::*; -use client::{Client, UserStore}; -use eval_utils::{EvalOutput, EvalOutputProcessor, OutcomeKind}; -use fs::FakeFs; -use futures::{FutureExt, future::LocalBoxFuture}; -use gpui::{AppContext, TestAppContext, Timer}; -use http_client::StatusCode; -use indoc::{formatdoc, indoc}; -use language_model::{ - LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolResultContent, - LanguageModelToolUse, LanguageModelToolUseId, SelectedModel, -}; -use project::Project; -use prompt_store::{ProjectContext, WorktreeContext}; -use rand::prelude::*; -use reqwest_client::ReqwestClient; -use serde_json::json; -use std::{ - fmt::{self, Display}, - path::Path, - str::FromStr, - time::Duration, -}; -use util::path; - -#[derive(Default, Clone, Debug)] -struct EditAgentOutputProcessor { - mismatched_tag_threshold: f32, - cumulative_tags: usize, - cumulative_mismatched_tags: usize, - eval_outputs: Vec>, -} - -fn mismatched_tag_threshold(mismatched_tag_threshold: f32) -> EditAgentOutputProcessor { - EditAgentOutputProcessor { - mismatched_tag_threshold, - cumulative_tags: 0, - cumulative_mismatched_tags: 0, - eval_outputs: Vec::new(), - } -} - -#[derive(Clone, Debug)] -struct EditEvalMetadata { - tags: usize, - mismatched_tags: usize, -} - -impl EvalOutputProcessor for EditAgentOutputProcessor { - type Metadata = EditEvalMetadata; - - fn process(&mut self, output: &EvalOutput) { - if matches!(output.outcome, OutcomeKind::Passed | OutcomeKind::Failed) { - self.cumulative_mismatched_tags += output.metadata.mismatched_tags; - self.cumulative_tags += output.metadata.tags; - self.eval_outputs.push(output.clone()); - } - } - - fn assert(&mut self) { - let mismatched_tag_ratio = - self.cumulative_mismatched_tags as f32 / self.cumulative_tags as f32; - if mismatched_tag_ratio > self.mismatched_tag_threshold { - for eval_output in &self.eval_outputs { - println!("{}", eval_output.data); - } - panic!( - "Too many mismatched tags: {:?}", - self.cumulative_mismatched_tags - ); - } - } -} - -#[test] -#[cfg_attr(not(feature = "unit-eval"), ignore)] -fn eval_extract_handle_command_output() { - // Test how well agent generates multiple edit hunks. - // - // Model | Pass rate - // ----------------------------|---------- - // claude-3.7-sonnet | 0.99 (2025-06-14) - // claude-sonnet-4 | 0.97 (2025-06-14) - // gemini-2.5-pro-06-05 | 0.98 (2025-06-16) - // gemini-2.5-flash | 0.11 (2025-05-22) - // gpt-4.1 | 1.00 (2025-05-22) - - let input_file_path = "root/blame.rs"; - let input_file_content = include_str!("evals/fixtures/extract_handle_command_output/before.rs"); - let possible_diffs = vec![ - include_str!("evals/fixtures/extract_handle_command_output/possible-01.diff"), - include_str!("evals/fixtures/extract_handle_command_output/possible-02.diff"), - include_str!("evals/fixtures/extract_handle_command_output/possible-03.diff"), - include_str!("evals/fixtures/extract_handle_command_output/possible-04.diff"), - include_str!("evals/fixtures/extract_handle_command_output/possible-05.diff"), - include_str!("evals/fixtures/extract_handle_command_output/possible-06.diff"), - include_str!("evals/fixtures/extract_handle_command_output/possible-07.diff"), - ]; - let edit_description = "Extract `handle_command_output` method from `run_git_blame`."; - eval_utils::eval(100, 0.95, mismatched_tag_threshold(0.05), move || { - run_eval(EvalInput::from_conversation( - vec![ - message( - User, - [text(formatdoc! {" - Read the `{input_file_path}` file and extract a method in - the final stanza of `run_git_blame` to deal with command failures, - call it `handle_command_output` and take the std::process::Output as the only parameter. - Do not document the method and do not add any comments. - - Add it right next to `run_git_blame` and copy it verbatim from `run_git_blame`. - "})], - ), - message( - Assistant, - [tool_use( - "tool_1", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: None, - end_line: None, - }, - )], - ), - message( - User, - [tool_result("tool_1", "read_file", input_file_content)], - ), - message( - Assistant, - [tool_use( - "tool_2", - "edit_file", - EditFileToolInput { - display_description: edit_description.into(), - path: input_file_path.into(), - mode: EditFileMode::Edit, - }, - )], - ), - ], - Some(input_file_content.into()), - EvalAssertion::assert_diff_any(possible_diffs.clone()), - )) - }); -} - -#[test] -#[cfg_attr(not(feature = "unit-eval"), ignore)] -fn eval_delete_run_git_blame() { - // Model | Pass rate - // ----------------------------|---------- - // claude-3.7-sonnet | 1.0 (2025-06-14) - // claude-sonnet-4 | 0.96 (2025-06-14) - // gemini-2.5-pro-06-05 | 1.0 (2025-06-16) - // gemini-2.5-flash | - // gpt-4.1 | - - let input_file_path = "root/blame.rs"; - let input_file_content = include_str!("evals/fixtures/delete_run_git_blame/before.rs"); - let output_file_content = include_str!("evals/fixtures/delete_run_git_blame/after.rs"); - let edit_description = "Delete the `run_git_blame` function."; - - eval_utils::eval(100, 0.95, mismatched_tag_threshold(0.05), move || { - run_eval(EvalInput::from_conversation( - vec![ - message( - User, - [text(formatdoc! {" - Read the `{input_file_path}` file and delete `run_git_blame`. Just that - one function, not its usages. - "})], - ), - message( - Assistant, - [tool_use( - "tool_1", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: None, - end_line: None, - }, - )], - ), - message( - User, - [tool_result("tool_1", "read_file", input_file_content)], - ), - message( - Assistant, - [tool_use( - "tool_2", - "edit_file", - EditFileToolInput { - display_description: edit_description.into(), - path: input_file_path.into(), - mode: EditFileMode::Edit, - }, - )], - ), - ], - Some(input_file_content.into()), - EvalAssertion::assert_eq(output_file_content), - )) - }); -} - -#[test] -#[cfg_attr(not(feature = "unit-eval"), ignore)] -fn eval_translate_doc_comments() { - // Model | Pass rate - // ============================================ - // - // claude-3.7-sonnet | 1.0 (2025-06-14) - // claude-sonnet-4 | 1.0 (2025-06-14) - // gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22) - // gemini-2.5-flash-preview-04-17 | - // gpt-4.1 | - - let input_file_path = "root/canvas.rs"; - let input_file_content = include_str!("evals/fixtures/translate_doc_comments/before.rs"); - let edit_description = "Translate all doc comments to Italian"; - - eval_utils::eval(200, 1., mismatched_tag_threshold(0.05), move || { - run_eval(EvalInput::from_conversation( - vec![ - message( - User, - [text(formatdoc! {" - Read the {input_file_path} file and edit it (without overwriting it), - translating all the doc comments to italian. - "})], - ), - message( - Assistant, - [tool_use( - "tool_1", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: None, - end_line: None, - }, - )], - ), - message( - User, - [tool_result("tool_1", "read_file", input_file_content)], - ), - message( - Assistant, - [tool_use( - "tool_2", - "edit_file", - EditFileToolInput { - display_description: edit_description.into(), - path: input_file_path.into(), - mode: EditFileMode::Edit, - }, - )], - ), - ], - Some(input_file_content.into()), - EvalAssertion::judge_diff("Doc comments were translated to Italian"), - )) - }); -} - -#[test] -#[cfg_attr(not(feature = "unit-eval"), ignore)] -fn eval_use_wasi_sdk_in_compile_parser_to_wasm() { - // Model | Pass rate - // ============================================ - // - // claude-3.7-sonnet | 0.96 (2025-06-14) - // claude-sonnet-4 | 0.11 (2025-06-14) - // gemini-2.5-pro-preview-latest | 0.99 (2025-06-16) - // gemini-2.5-flash-preview-04-17 | - // gpt-4.1 | - - let input_file_path = "root/lib.rs"; - let input_file_content = - include_str!("evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs"); - let edit_description = "Update compile_parser_to_wasm to use wasi-sdk instead of emscripten"; - - eval_utils::eval(100, 0.95, mismatched_tag_threshold(0.05), move || { - run_eval(EvalInput::from_conversation( - vec![ - message( - User, - [text(formatdoc! {" - Read the `{input_file_path}` file and change `compile_parser_to_wasm` to use `wasi-sdk` instead of emscripten. - Use `ureq` to download the SDK for the current platform and architecture. - Extract the archive into a sibling of `lib` inside the `tree-sitter` directory in the cache_dir. - Compile the parser to wasm using the `bin/clang` executable (or `bin/clang.exe` on windows) - that's inside of the archive. - Don't re-download the SDK if that executable already exists. - - Use these clang flags: -fPIC -shared -Os -Wl,--export=tree_sitter_{{language_name}} - - Here are the available wasi-sdk assets: - - wasi-sdk-25.0-x86_64-macos.tar.gz - - wasi-sdk-25.0-arm64-macos.tar.gz - - wasi-sdk-25.0-x86_64-linux.tar.gz - - wasi-sdk-25.0-arm64-linux.tar.gz - - wasi-sdk-25.0-x86_64-linux.tar.gz - - wasi-sdk-25.0-arm64-linux.tar.gz - - wasi-sdk-25.0-x86_64-windows.tar.gz - "})], - ), - message( - Assistant, - [tool_use( - "tool_1", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: Some(971), - end_line: Some(1050), - }, - )], - ), - message( - User, - [tool_result( - "tool_1", - "read_file", - lines(input_file_content, 971..1050), - )], - ), - message( - Assistant, - [tool_use( - "tool_2", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: Some(1050), - end_line: Some(1100), - }, - )], - ), - message( - User, - [tool_result( - "tool_2", - "read_file", - lines(input_file_content, 1050..1100), - )], - ), - message( - Assistant, - [tool_use( - "tool_3", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: Some(1100), - end_line: Some(1150), - }, - )], - ), - message( - User, - [tool_result( - "tool_3", - "read_file", - lines(input_file_content, 1100..1150), - )], - ), - message( - Assistant, - [tool_use( - "tool_4", - "edit_file", - EditFileToolInput { - display_description: edit_description.into(), - path: input_file_path.into(), - mode: EditFileMode::Edit, - }, - )], - ), - ], - Some(input_file_content.into()), - EvalAssertion::judge_diff(indoc! {" - - The compile_parser_to_wasm method has been changed to use wasi-sdk - - ureq is used to download the SDK for current platform and architecture - "}), - )) - }); -} - -#[test] -#[cfg_attr(not(feature = "unit-eval"), ignore)] -fn eval_disable_cursor_blinking() { - // Model | Pass rate - // ============================================ - // - // claude-3.7-sonnet | 0.59 (2025-07-14) - // claude-sonnet-4 | 0.81 (2025-07-14) - // gemini-2.5-pro | 0.95 (2025-07-14) - // gemini-2.5-flash-preview-04-17 | 0.78 (2025-07-14) - // gpt-4.1 | 0.00 (2025-07-14) (follows edit_description too literally) - - let input_file_path = "root/editor.rs"; - let input_file_content = include_str!("evals/fixtures/disable_cursor_blinking/before.rs"); - let edit_description = "Comment out the call to `BlinkManager::enable`"; - let possible_diffs = vec![ - include_str!("evals/fixtures/disable_cursor_blinking/possible-01.diff"), - include_str!("evals/fixtures/disable_cursor_blinking/possible-02.diff"), - include_str!("evals/fixtures/disable_cursor_blinking/possible-03.diff"), - include_str!("evals/fixtures/disable_cursor_blinking/possible-04.diff"), - ]; - eval_utils::eval(100, 0.51, mismatched_tag_threshold(0.05), move || { - run_eval(EvalInput::from_conversation( - vec![ - message(User, [text("Let's research how to cursor blinking works.")]), - message( - Assistant, - [tool_use( - "tool_1", - "grep", - GrepToolInput { - regex: "blink".into(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }, - )], - ), - message( - User, - [tool_result( - "tool_1", - "grep", - [ - lines(input_file_content, 100..400), - lines(input_file_content, 800..1300), - lines(input_file_content, 1600..2000), - lines(input_file_content, 5000..5500), - lines(input_file_content, 8000..9000), - lines(input_file_content, 18455..18470), - lines(input_file_content, 20000..20500), - lines(input_file_content, 21000..21300), - ] - .join("Match found:\n\n"), - )], - ), - message( - User, - [text(indoc! {" - Comment out the lines that interact with the BlinkManager. - Keep the outer `update` blocks, but comments everything that's inside (including if statements). - Don't add additional comments. - "})], - ), - message( - Assistant, - [tool_use( - "tool_4", - "edit_file", - EditFileToolInput { - display_description: edit_description.into(), - path: input_file_path.into(), - mode: EditFileMode::Edit, - }, - )], - ), - ], - Some(input_file_content.into()), - EvalAssertion::assert_diff_any(possible_diffs.clone()), - )) - }); -} - -#[test] -#[cfg_attr(not(feature = "unit-eval"), ignore)] -fn eval_from_pixels_constructor() { - // Results for 2025-06-13 - // - // The outcome of this evaluation depends heavily on the LINE_HINT_TOLERANCE - // value. Higher values improve the pass rate but may sometimes cause - // edits to be misapplied. In the context of this eval, this means - // the agent might add from_pixels tests in incorrect locations - // (e.g., at the beginning of the file), yet the evaluation may still - // rate it highly. - // - // Model | Date | Pass rate - // ========================================================= - // claude-4.0-sonnet | 2025-06-14 | 0.99 - // claude-3.7-sonnet | 2025-06-14 | 0.88 - // gemini-2.5-pro-preview-06-05 | 2025-06-16 | 0.98 - // gpt-4.1 | - - let input_file_path = "root/canvas.rs"; - let input_file_content = include_str!("evals/fixtures/from_pixels_constructor/before.rs"); - let edit_description = "Implement from_pixels constructor and add tests."; - - eval_utils::eval(100, 0.95, mismatched_tag_threshold(0.25), move || { - run_eval(EvalInput::from_conversation( - vec![ - message( - User, - [text(indoc! {" - Introduce a new `from_pixels` constructor in Canvas and - also add tests for it in the same file. - "})], - ), - message( - Assistant, - [tool_use( - "tool_1", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: None, - end_line: None, - }, - )], - ), - message( - User, - [tool_result("tool_1", "read_file", input_file_content)], - ), - message( - Assistant, - [tool_use( - "tool_2", - "grep", - GrepToolInput { - regex: "mod\\s+tests".into(), - include_pattern: Some("font-kit/src/canvas.rs".into()), - offset: 0, - case_sensitive: false, - }, - )], - ), - message(User, [tool_result("tool_2", "grep", "No matches found")]), - message( - Assistant, - [tool_use( - "tool_3", - "grep", - GrepToolInput { - regex: "mod\\s+tests".into(), - include_pattern: Some("font-kit/src/**/*.rs".into()), - offset: 0, - case_sensitive: false, - }, - )], - ), - message(User, [tool_result("tool_3", "grep", "No matches found")]), - message( - Assistant, - [tool_use( - "tool_4", - "grep", - GrepToolInput { - regex: "#\\[test\\]".into(), - include_pattern: Some("font-kit/src/**/*.rs".into()), - offset: 0, - case_sensitive: false, - }, - )], - ), - message( - User, - [tool_result( - "tool_4", - "grep", - indoc! {" - Found 6 matches: - - ## Matches in font-kit/src/loaders/core_text.rs - - ### mod test › L926-936 - ``` - mod test { - use super::Font; - use crate::properties::{Stretch, Weight}; - - #[cfg(feature = \"source\")] - use crate::source::SystemSource; - - static TEST_FONT_POSTSCRIPT_NAME: &'static str = \"ArialMT\"; - - #[cfg(feature = \"source\")] - #[test] - ``` - - 55 lines remaining in ancestor node. Read the file to see all. - - ### mod test › L947-951 - ``` - } - - #[test] - fn test_core_text_to_css_font_weight() { - // Exact matches - ``` - - ### mod test › L959-963 - ``` - } - - #[test] - fn test_core_text_to_css_font_stretch() { - // Exact matches - ``` - - ## Matches in font-kit/src/loaders/freetype.rs - - ### mod test › L1238-1248 - ``` - mod test { - use crate::loaders::freetype::Font; - - static PCF_FONT_PATH: &str = \"resources/tests/times-roman-pcf/timR12.pcf\"; - static PCF_FONT_POSTSCRIPT_NAME: &str = \"Times-Roman\"; - - #[test] - fn get_pcf_postscript_name() { - let font = Font::from_path(PCF_FONT_PATH, 0).unwrap(); - assert_eq!(font.postscript_name().unwrap(), PCF_FONT_POSTSCRIPT_NAME); - } - ``` - - 1 lines remaining in ancestor node. Read the file to see all. - - ## Matches in font-kit/src/sources/core_text.rs - - ### mod test › L265-275 - ``` - mod test { - use crate::properties::{Stretch, Weight}; - - #[test] - fn test_css_to_core_text_font_weight() { - // Exact matches - assert_eq!(super::css_to_core_text_font_weight(Weight(100.0)), -0.7); - assert_eq!(super::css_to_core_text_font_weight(Weight(400.0)), 0.0); - assert_eq!(super::css_to_core_text_font_weight(Weight(700.0)), 0.4); - assert_eq!(super::css_to_core_text_font_weight(Weight(900.0)), 0.8); - - ``` - - 27 lines remaining in ancestor node. Read the file to see all. - - ### mod test › L278-282 - ``` - } - - #[test] - fn test_css_to_core_text_font_stretch() { - // Exact matches - ``` - "}, - )], - ), - message( - Assistant, - [tool_use( - "tool_5", - "edit_file", - EditFileToolInput { - display_description: edit_description.into(), - path: input_file_path.into(), - mode: EditFileMode::Edit, - }, - )], - ), - ], - Some(input_file_content.into()), - EvalAssertion::judge_diff(indoc! {" - - The diff contains a new `from_pixels` constructor - - The diff contains new tests for the `from_pixels` constructor - "}), - )) - }); -} - -#[test] -#[cfg_attr(not(feature = "unit-eval"), ignore)] -fn eval_zode() { - // Model | Pass rate - // ============================================ - // - // claude-3.7-sonnet | 1.0 (2025-06-14) - // claude-sonnet-4 | 1.0 (2025-06-14) - // gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22) - // gemini-2.5-flash-preview-04-17 | 1.0 (2025-05-22) - // gpt-4.1 | 1.0 (2025-05-22) - - let input_file_path = "root/zode.py"; - let input_content = None; - let edit_description = "Create the main Zode CLI script"; - - eval_utils::eval(50, 1., mismatched_tag_threshold(0.05), move || { - run_eval(EvalInput::from_conversation( - vec![ - message(User, [text(include_str!("evals/fixtures/zode/prompt.md"))]), - message( - Assistant, - [ - tool_use( - "tool_1", - "read_file", - ReadFileToolInput { - path: "root/eval/react.py".into(), - start_line: None, - end_line: None, - }, - ), - tool_use( - "tool_2", - "read_file", - ReadFileToolInput { - path: "root/eval/react_test.py".into(), - start_line: None, - end_line: None, - }, - ), - ], - ), - message( - User, - [ - tool_result( - "tool_1", - "read_file", - include_str!("evals/fixtures/zode/react.py"), - ), - tool_result( - "tool_2", - "read_file", - include_str!("evals/fixtures/zode/react_test.py"), - ), - ], - ), - message( - Assistant, - [ - text( - "Now that I understand what we need to build, I'll create the main Python script:", - ), - tool_use( - "tool_3", - "edit_file", - EditFileToolInput { - display_description: edit_description.into(), - path: input_file_path.into(), - mode: EditFileMode::Create, - }, - ), - ], - ), - ], - input_content.clone(), - EvalAssertion::new(async move |sample, _, _cx| { - let invalid_starts = [' ', '`', '\n']; - let mut message = String::new(); - for start in invalid_starts { - if sample.text_after.starts_with(start) { - message.push_str(&format!("The sample starts with a {:?}\n", start)); - break; - } - } - // Remove trailing newline. - message.pop(); - - if message.is_empty() { - Ok(EvalAssertionOutcome { - score: 100, - message: None, - }) - } else { - Ok(EvalAssertionOutcome { - score: 0, - message: Some(message), - }) - } - }), - )) - }); -} - -#[test] -#[cfg_attr(not(feature = "unit-eval"), ignore)] -fn eval_add_overwrite_test() { - // Model | Pass rate - // ============================================ - // - // claude-3.7-sonnet | 0.65 (2025-06-14) - // claude-sonnet-4 | 0.07 (2025-06-14) - // gemini-2.5-pro-preview-03-25 | 0.35 (2025-05-22) - // gemini-2.5-flash-preview-04-17 | - // gpt-4.1 | - - let input_file_path = "root/action_log.rs"; - let input_file_content = include_str!("evals/fixtures/add_overwrite_test/before.rs"); - let edit_description = "Add a new test for overwriting a file in action_log.rs"; - - eval_utils::eval(200, 0.5, mismatched_tag_threshold(0.05), move || { - run_eval(EvalInput::from_conversation( - vec![ - message( - User, - [text(indoc! {" - Introduce a new test in `action_log.rs` to test overwriting a file. - That is, a file already exists, but we call `buffer_created` as if the file were new. - Take inspiration from all the other tests in the file. - "})], - ), - message( - Assistant, - [tool_use( - "tool_1", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: None, - end_line: None, - }, - )], - ), - message( - User, - [tool_result( - "tool_1", - "read_file", - indoc! {" - pub struct ActionLog [L13-20] - tracked_buffers [L15] - edited_since_project_diagnostics_check [L17] - project [L19] - impl ActionLog [L22-498] - pub fn new [L24-30] - pub fn project [L32-34] - pub fn checked_project_diagnostics [L37-39] - pub fn has_edited_files_since_project_diagnostics_check [L42-44] - fn track_buffer_internal [L46-101] - fn handle_buffer_event [L103-116] - fn handle_buffer_edited [L118-123] - fn handle_buffer_file_changed [L125-158] - async fn maintain_diff [L160-264] - pub fn buffer_read [L267-269] - pub fn buffer_created [L272-276] - pub fn buffer_edited [L279-287] - pub fn will_delete_buffer [L289-304] - pub fn keep_edits_in_range [L306-364] - pub fn reject_edits_in_ranges [L366-459] - pub fn keep_all_edits [L461-473] - pub fn changed_buffers [L476-482] - pub fn stale_buffers [L485-497] - fn apply_non_conflicting_edits [L500-561] - fn diff_snapshots [L563-585] - fn point_to_row_edit [L587-614] - enum ChangeAuthor [L617-620] - User [L618] - Agent [L619] - enum TrackedBufferStatus [L623-627] - Created [L624] - Modified [L625] - Deleted [L626] - struct TrackedBuffer [L629-641] - buffer [L630] - base_text [L631] - unreviewed_changes [L632] - status [L633] - version [L634] - diff [L635] - snapshot [L636] - diff_update [L637] - _open_lsp_handle [L638] - _maintain_diff [L639] - _subscription [L640] - impl TrackedBuffer [L643-657] - fn has_changes [L644-650] - fn schedule_diff_update [L652-656] - pub struct ChangedBuffer [L659-661] - pub diff [L660] - mod tests [L664-1574] - fn init_logger [L678-682] - fn init_test [L684-691] - async fn test_keep_edits [L694-769] - async fn test_deletions [L772-854] - async fn test_overlapping_user_edits [L857-951] - async fn test_creating_files [L954-1010] - async fn test_deleting_files [L1013-1120] - async fn test_reject_edits [L1123-1255] - async fn test_reject_multiple_edits [L1258-1331] - async fn test_reject_deleted_file [L1334-1388] - async fn test_reject_created_file [L1391-1443] - async fn test_random_diffs [L1446-1535] - fn quiesce [L1510-1534] - struct HunkStatus [L1538-1542] - range [L1539] - diff_status [L1540] - old_text [L1541] - fn unreviewed_hunks [L1544-1573] - - Showing symbols 1-69 (total symbols: 69) - - 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. - "}, - )], - ), - message( - Assistant, - [ - text( - "Let's look at the existing test cases that are related to file creation to understand the pattern:", - ), - tool_use( - "tool_2", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: Some(953), - end_line: Some(1010), - }, - ), - ], - ), - message( - User, - [tool_result( - "tool_2", - "read_file", - lines(input_file_content, 953..1010), - )], - ), - message( - Assistant, - [ - text( - "Now let's look at another relevant test to understand how they're structured:", - ), - tool_use( - "tool_3", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: Some(1012), - end_line: Some(1120), - }, - ), - ], - ), - message( - User, - [tool_result( - "tool_3", - "read_file", - lines(input_file_content, 1012..1120), - )], - ), - message( - Assistant, - [ - text("Now let's look at how `buffer_created` is implemented:"), - tool_use( - "tool_4", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: Some(271), - end_line: Some(276), - }, - ), - ], - ), - message( - User, - [tool_result( - "tool_4", - "read_file", - lines(input_file_content, 271..276), - )], - ), - message( - Assistant, - [ - text(indoc! {" - Based on my understanding of the existing tests and code, I'll now create a new test for overwriting a file. In this test, we'll: - - 1. Create a file - 2. Open a buffer for it - 3. Track it in the action log - 4. Modify the buffer - 5. Call `buffer_created` again as if the file were new - 6. Check that the changes are properly tracked - - Let's write the test: - "}), - tool_use( - "tool_5", - "edit_file", - EditFileToolInput { - display_description: edit_description.into(), - path: input_file_path.into(), - mode: EditFileMode::Edit, - }, - ), - ], - ), - ], - Some(input_file_content.into()), - EvalAssertion::judge_diff( - "A new test for overwritten files was created, without changing any previous test", - ), - )) - }); -} - -#[test] -#[cfg_attr(not(feature = "unit-eval"), ignore)] -fn eval_create_empty_file() { - // Check that Edit Agent can create a file without writing its - // thoughts into it. This issue is not specific to empty files, but - // it's easier to reproduce with them. - // - // Model | Pass rate - // ============================================ - // - // claude-3.7-sonnet | 1.00 (2025-06-14) - // claude-sonnet-4 | 1.00 (2025-06-14) - // gemini-2.5-pro-preview-03-25 | 1.00 (2025-05-21) - // gemini-2.5-flash-preview-04-17 | 1.00 (2025-05-21) - // gpt-4.1 | 1.00 (2025-05-21) - // - // - // TODO: gpt-4.1-mini errored 38 times: - // "data did not match any variant of untagged enum ResponseStreamResult" - - let input_file_content = None; - let expected_output_content = String::new(); - - eval_utils::eval(100, 0.99, mismatched_tag_threshold(0.05), move || { - run_eval(EvalInput::from_conversation( - vec![ - message(User, [text("Create a second empty todo file ")]), - message( - Assistant, - [ - text(formatdoc! {" - I'll help you create a second empty todo file. - First, let me examine the project structure to see if there's already a todo file, which will help me determine the appropriate name and location for the second one. - "}), - tool_use( - "toolu_01GAF8TtsgpjKxCr8fgQLDgR", - "list_directory", - ListDirectoryToolInput { - path: "root".to_string(), - }, - ), - ], - ), - message( - User, - [tool_result( - "toolu_01GAF8TtsgpjKxCr8fgQLDgR", - "list_directory", - "root/TODO\nroot/TODO2\nroot/new.txt\n", - )], - ), - message( - Assistant, - [ - text(formatdoc! {" - I can see there's already a `TODO` file in the `root` directory. Let me create a second empty todo file called `TODO3` in the same directory: - "}), - tool_use( - "toolu_01Tb3iQ9griqSYMmVuykQPWU", - "edit_file", - EditFileToolInput { - display_description: "Create empty TODO3 file".to_string(), - mode: EditFileMode::Create, - path: "root/TODO3".into(), - }, - ), - ], - ), - ], - input_file_content.clone(), - // Bad behavior is to write something like - // "I'll create an empty TODO3 file as requested." - EvalAssertion::assert_eq(expected_output_content.clone()), - )) - }); -} - -fn message( - role: Role, - contents: impl IntoIterator, -) -> LanguageModelRequestMessage { - LanguageModelRequestMessage { - role, - content: contents.into_iter().collect(), - cache: false, - reasoning_details: None, - } -} - -fn text(text: impl Into) -> MessageContent { - MessageContent::Text(text.into()) -} - -fn lines(input: &str, range: Range) -> String { - input - .lines() - .skip(range.start) - .take(range.len()) - .collect::>() - .join("\n") -} - -fn tool_use( - id: impl Into>, - name: impl Into>, - input: impl Serialize, -) -> MessageContent { - MessageContent::ToolUse(LanguageModelToolUse { - id: LanguageModelToolUseId::from(id.into()), - name: name.into(), - raw_input: serde_json::to_string_pretty(&input).unwrap(), - input: serde_json::to_value(input).unwrap(), - is_input_complete: true, - thought_signature: None, - }) -} - -fn tool_result( - id: impl Into>, - name: impl Into>, - result: impl Into>, -) -> MessageContent { - MessageContent::ToolResult(LanguageModelToolResult { - tool_use_id: LanguageModelToolUseId::from(id.into()), - tool_name: name.into(), - is_error: false, - content: LanguageModelToolResultContent::Text(result.into()), - output: None, - }) -} - -#[derive(Clone)] -struct EvalInput { - conversation: Vec, - edit_file_input: EditFileToolInput, - input_content: Option, - assertion: EvalAssertion, -} - -impl EvalInput { - fn from_conversation( - conversation: Vec, - input_content: Option, - assertion: EvalAssertion, - ) -> Self { - let msg = conversation.last().expect("Conversation must not be empty"); - if msg.role != Role::Assistant { - panic!("Conversation must end with an assistant message"); - } - let tool_use = msg - .content - .iter() - .flat_map(|content| match content { - MessageContent::ToolUse(tool_use) if tool_use.name == "edit_file".into() => { - Some(tool_use) - } - _ => None, - }) - .next() - .expect("Conversation must end with an edit_file tool use") - .clone(); - - let edit_file_input: EditFileToolInput = serde_json::from_value(tool_use.input).unwrap(); - - EvalInput { - conversation, - edit_file_input, - input_content, - assertion, - } - } -} - -#[derive(Clone)] -struct EvalSample { - text_before: String, - text_after: String, - edit_output: EditAgentOutput, - diff: String, -} - -trait AssertionFn: 'static + Send + Sync { - fn assert<'a>( - &'a self, - sample: &'a EvalSample, - judge_model: Arc, - cx: &'a mut TestAppContext, - ) -> LocalBoxFuture<'a, Result>; -} - -impl AssertionFn for F -where - F: 'static - + Send - + Sync - + AsyncFn( - &EvalSample, - Arc, - &mut TestAppContext, - ) -> Result, -{ - fn assert<'a>( - &'a self, - sample: &'a EvalSample, - judge_model: Arc, - cx: &'a mut TestAppContext, - ) -> LocalBoxFuture<'a, Result> { - (self)(sample, judge_model, cx).boxed_local() - } -} - -#[derive(Clone)] -struct EvalAssertion(Arc); - -impl EvalAssertion { - fn new(f: F) -> Self - where - F: 'static - + Send - + Sync - + AsyncFn( - &EvalSample, - Arc, - &mut TestAppContext, - ) -> Result, - { - EvalAssertion(Arc::new(f)) - } - - fn assert_eq(expected: impl Into) -> Self { - let expected = expected.into(); - Self::new(async move |sample, _judge, _cx| { - Ok(EvalAssertionOutcome { - score: if strip_empty_lines(&sample.text_after) == strip_empty_lines(&expected) { - 100 - } else { - 0 - }, - message: None, - }) - }) - } - - fn assert_diff_any(expected_diffs: Vec>) -> Self { - let expected_diffs: Vec = expected_diffs.into_iter().map(Into::into).collect(); - Self::new(async move |sample, _judge, _cx| { - let matches = expected_diffs.iter().any(|possible_diff| { - let expected = - language::apply_diff_patch(&sample.text_before, possible_diff).unwrap(); - strip_empty_lines(&expected) == strip_empty_lines(&sample.text_after) - }); - - Ok(EvalAssertionOutcome { - score: if matches { 100 } else { 0 }, - message: None, - }) - }) - } - - fn judge_diff(assertions: &'static str) -> Self { - Self::new(async move |sample, judge, cx| { - let prompt = DiffJudgeTemplate { - diff: sample.diff.clone(), - assertions, - } - .render(&Templates::new()) - .unwrap(); - - let request = LanguageModelRequest { - messages: vec![LanguageModelRequestMessage { - role: Role::User, - content: vec![prompt.into()], - cache: false, - reasoning_details: None, - }], - thinking_allowed: true, - ..Default::default() - }; - let mut response = retry_on_rate_limit(async || { - Ok(judge - .stream_completion_text(request.clone(), &cx.to_async()) - .await?) - }) - .await?; - let mut output = String::new(); - while let Some(chunk) = response.stream.next().await { - let chunk = chunk?; - output.push_str(&chunk); - } - - // 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), - }); - } - - anyhow::bail!("No score found in response. Raw output: {output}"); - }) - } - - async fn run( - &self, - input: &EvalSample, - judge_model: Arc, - cx: &mut TestAppContext, - ) -> Result { - self.0.assert(input, judge_model, cx).await - } -} - -fn run_eval(eval: EvalInput) -> eval_utils::EvalOutput { - let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng()); - let mut cx = TestAppContext::build(dispatcher, None); - let result = cx.executor().block_test(async { - let test = EditAgentTest::new(&mut cx).await; - test.eval(eval, &mut cx).await - }); - match result { - Ok(output) => eval_utils::EvalOutput { - data: output.to_string(), - outcome: if output.assertion.score < 80 { - eval_utils::OutcomeKind::Failed - } else { - eval_utils::OutcomeKind::Passed - }, - metadata: EditEvalMetadata { - tags: output.sample.edit_output.parser_metrics.tags, - mismatched_tags: output.sample.edit_output.parser_metrics.mismatched_tags, - }, - }, - Err(e) => eval_utils::EvalOutput { - data: format!("{e:?}"), - outcome: eval_utils::OutcomeKind::Error, - metadata: EditEvalMetadata { - tags: 0, - mismatched_tags: 0, - }, - }, - } -} - -#[derive(Clone)] -struct EditEvalOutput { - sample: EvalSample, - assertion: EvalAssertionOutcome, -} - -impl Display for EditEvalOutput { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "Score: {:?}", self.assertion.score)?; - if let Some(message) = self.assertion.message.as_ref() { - writeln!(f, "Message: {}", message)?; - } - - writeln!(f, "Diff:\n{}", self.sample.diff)?; - - writeln!( - f, - "Parser Metrics:\n{:#?}", - self.sample.edit_output.parser_metrics - )?; - writeln!(f, "Raw Edits:\n{}", self.sample.edit_output.raw_edits)?; - Ok(()) - } -} - -struct EditAgentTest { - agent: EditAgent, - project: Entity, - judge_model: Arc, -} - -impl EditAgentTest { - async fn new(cx: &mut TestAppContext) -> Self { - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.executor()); - cx.update(|cx| { - settings::init(cx); - gpui_tokio::init(cx); - let http_client = Arc::new(ReqwestClient::user_agent("agent tests").unwrap()); - cx.set_http_client(http_client); - let client = Client::production(cx); - let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - settings::init(cx); - language_model::init(client.clone(), cx); - language_models::init(user_store, client.clone(), cx); - }); - - fs.insert_tree("/root", json!({})).await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let agent_model = SelectedModel::from_str( - &std::env::var("ZED_AGENT_MODEL").unwrap_or("anthropic/claude-sonnet-4-latest".into()), - ) - .unwrap(); - let judge_model = SelectedModel::from_str( - &std::env::var("ZED_JUDGE_MODEL").unwrap_or("anthropic/claude-sonnet-4-latest".into()), - ) - .unwrap(); - - let authenticate_provider_tasks = cx.update(|cx| { - LanguageModelRegistry::global(cx).update(cx, |registry, cx| { - registry - .providers() - .iter() - .map(|p| p.authenticate(cx)) - .collect::>() - }) - }); - let (agent_model, judge_model) = cx - .update(|cx| { - cx.spawn(async move |cx| { - futures::future::join_all(authenticate_provider_tasks).await; - let agent_model = Self::load_model(&agent_model, cx).await; - let judge_model = Self::load_model(&judge_model, cx).await; - (agent_model.unwrap(), judge_model.unwrap()) - }) - }) - .await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - - let edit_format = EditFormat::from_env(agent_model.clone()).unwrap(); - - Self { - agent: EditAgent::new( - agent_model, - project.clone(), - action_log, - Templates::new(), - edit_format, - ), - project, - judge_model, - } - } - - async fn load_model( - selected_model: &SelectedModel, - cx: &mut AsyncApp, - ) -> Result> { - cx.update(|cx| { - let registry = LanguageModelRegistry::read_global(cx); - let provider = registry - .provider(&selected_model.provider) - .expect("Provider not found"); - provider.authenticate(cx) - })? - .await?; - cx.update(|cx| { - let models = LanguageModelRegistry::read_global(cx); - let model = models - .available_models(cx) - .find(|model| { - model.provider_id() == selected_model.provider - && model.id() == selected_model.model - }) - .unwrap_or_else(|| panic!("Model {} not found", selected_model.model.0)); - model - }) - } - - async fn eval(&self, mut eval: EvalInput, cx: &mut TestAppContext) -> Result { - // Make sure the last message in the conversation is cached. - eval.conversation.last_mut().unwrap().cache = true; - - let path = self - .project - .read_with(cx, |project, cx| { - project.find_project_path(eval.edit_file_input.path, cx) - }) - .unwrap(); - let buffer = self - .project - .update(cx, |project, cx| project.open_buffer(path, cx)) - .await - .unwrap(); - - let tools = crate::built_in_tools().collect::>(); - - let system_prompt = { - let worktrees = vec![WorktreeContext { - root_name: "root".to_string(), - abs_path: Path::new("/path/to/root").into(), - rules_file: None, - }]; - let project_context = ProjectContext::new(worktrees, Vec::default()); - let tool_names = tools - .iter() - .map(|tool| tool.name.clone().into()) - .collect::>(); - let template = crate::SystemPromptTemplate { - project: &project_context, - available_tools: tool_names, - model_name: None, - }; - let templates = Templates::new(); - template.render(&templates).unwrap() - }; - - let has_system_prompt = eval - .conversation - .first() - .is_some_and(|msg| msg.role == Role::System); - let messages = if has_system_prompt { - eval.conversation - } else { - [LanguageModelRequestMessage { - role: Role::System, - content: vec![MessageContent::Text(system_prompt)], - cache: true, - reasoning_details: None, - }] - .into_iter() - .chain(eval.conversation) - .collect::>() - }; - - let conversation = LanguageModelRequest { - messages, - tools, - thinking_allowed: true, - ..Default::default() - }; - - let edit_output = if matches!(eval.edit_file_input.mode, EditFileMode::Edit) { - if let Some(input_content) = eval.input_content.as_deref() { - buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx)); - } - retry_on_rate_limit(async || { - self.agent - .edit( - buffer.clone(), - eval.edit_file_input.display_description.clone(), - &conversation, - &mut cx.to_async(), - ) - .0 - .await - }) - .await? - } else { - retry_on_rate_limit(async || { - self.agent - .overwrite( - buffer.clone(), - eval.edit_file_input.display_description.clone(), - &conversation, - &mut cx.to_async(), - ) - .0 - .await - }) - .await? - }; - - let buffer_text = buffer.read_with(cx, |buffer, _| buffer.text()); - let sample = EvalSample { - edit_output, - diff: language::unified_diff( - eval.input_content.as_deref().unwrap_or_default(), - &buffer_text, - ), - text_before: eval.input_content.unwrap_or_default(), - text_after: buffer_text, - }; - let assertion = eval - .assertion - .run(&sample, self.judge_model.clone(), cx) - .await?; - - Ok(EditEvalOutput { assertion, sample }) - } -} - -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 { - LanguageModelCompletionError::RateLimitExceeded { retry_after, .. } - | LanguageModelCompletionError::ServerOverloaded { retry_after, .. } => { - Some(retry_after.unwrap_or(Duration::from_secs(5))) - } - LanguageModelCompletionError::UpstreamProviderError { - status, - retry_after, - .. - } => { - // Only retry for specific status codes - let should_retry = matches!( - *status, - 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 - } - } - 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::rng().random_range(0.0..1.0)); - eprintln!("Attempt #{attempt}: Retry after {retry_after:?} + jitter of {jitter:?}"); - Timer::after(retry_after + jitter).await; - } else { - return response; - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -struct EvalAssertionOutcome { - score: usize, - message: Option, -} - -#[derive(Serialize)] -pub struct DiffJudgeTemplate { - diff: String, - assertions: &'static str, -} - -impl Template for DiffJudgeTemplate { - const TEMPLATE_NAME: &'static str = "diff_judge.hbs"; -} - -fn strip_empty_lines(text: &str) -> String { - text.lines() - .filter(|line| !line.trim().is_empty()) - .collect::>() - .join("\n") -} diff --git a/crates/agent/src/edit_agent/evals/fixtures/add_overwrite_test/before.rs b/crates/agent/src/edit_agent/evals/fixtures/add_overwrite_test/before.rs deleted file mode 100644 index 0d2a0be1fb..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/add_overwrite_test/before.rs +++ /dev/null @@ -1,1572 +0,0 @@ -use anyhow::{Context as _, Result}; -use buffer_diff::BufferDiff; -use collections::BTreeMap; -use futures::{StreamExt, channel::mpsc}; -use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity}; -use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint}; -use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle}; -use std::{cmp, ops::Range, sync::Arc}; -use text::{Edit, Patch, Rope}; -use util::RangeExt; - -/// Tracks actions performed by tools in a thread -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, -} - -impl ActionLog { - /// Creates a new, empty action log associated with the given project. - pub fn new(project: Entity) -> Self { - Self { - tracked_buffers: BTreeMap::default(), - edited_since_project_diagnostics_check: false, - project, - } - } - - pub fn project(&self) -> &Entity { - &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 - } - - fn track_buffer_internal( - &mut self, - buffer: Entity, - is_created: bool, - cx: &mut Context, - ) -> &mut TrackedBuffer { - let tracked_buffer = self - .tracked_buffers - .entry(buffer.clone()) - .or_insert_with(|| { - let open_lsp_handle = self.project.update(cx, |project, cx| { - project.register_buffer_with_language_servers(&buffer, cx) - }); - - let text_snapshot = buffer.read(cx).text_snapshot(); - let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx)); - let (diff_update_tx, diff_update_rx) = mpsc::unbounded(); - let base_text; - let status; - let unreviewed_changes; - if is_created { - base_text = Rope::default(); - status = TrackedBufferStatus::Created; - unreviewed_changes = Patch::new(vec![Edit { - old: 0..1, - new: 0..text_snapshot.max_point().row + 1, - }]) - } else { - base_text = buffer.read(cx).as_rope().clone(); - status = TrackedBufferStatus::Modified; - unreviewed_changes = Patch::default(); - } - TrackedBuffer { - buffer: buffer.clone(), - base_text, - unreviewed_changes, - snapshot: text_snapshot.clone(), - status, - version: buffer.read(cx).version(), - diff, - diff_update: diff_update_tx, - _open_lsp_handle: open_lsp_handle, - _maintain_diff: cx.spawn({ - let buffer = buffer.clone(); - async move |this, cx| { - Self::maintain_diff(this, buffer, diff_update_rx, cx) - .await - .ok(); - } - }), - _subscription: cx.subscribe(&buffer, Self::handle_buffer_event), - } - }); - tracked_buffer.version = buffer.read(cx).version(); - tracked_buffer - } - - fn handle_buffer_event( - &mut self, - buffer: Entity, - event: &BufferEvent, - cx: &mut Context, - ) { - match event { - BufferEvent::Edited { .. } => self.handle_buffer_edited(buffer, cx), - BufferEvent::FileHandleChanged => { - self.handle_buffer_file_changed(buffer, cx); - } - _ => {} - }; - } - - fn handle_buffer_edited(&mut self, buffer: Entity, cx: &mut Context) { - let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { - return; - }; - tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); - } - - fn handle_buffer_file_changed(&mut self, buffer: Entity, cx: &mut Context) { - let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { - return; - }; - - match tracked_buffer.status { - TrackedBufferStatus::Created | TrackedBufferStatus::Modified => { - if buffer - .read(cx) - .file() - .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. - self.tracked_buffers.remove(&buffer); - } - cx.notify(); - } - TrackedBufferStatus::Deleted => { - if buffer - .read(cx) - .file() - .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 changes we - // were tracking and reset the buffer's state. - self.tracked_buffers.remove(&buffer); - self.track_buffer_internal(buffer, false, cx); - } - cx.notify(); - } - } - } - - async fn maintain_diff( - this: WeakEntity, - buffer: Entity, - mut diff_update: mpsc::UnboundedReceiver<(ChangeAuthor, text::BufferSnapshot)>, - cx: &mut AsyncApp, - ) -> Result<()> { - while let Some((author, buffer_snapshot)) = diff_update.next().await { - let (rebase, diff, language, language_registry) = - this.read_with(cx, |this, cx| { - let tracked_buffer = this - .tracked_buffers - .get(&buffer) - .context("buffer not tracked")?; - - let rebase = cx.background_spawn({ - let mut base_text = tracked_buffer.base_text.clone(); - let old_snapshot = tracked_buffer.snapshot.clone(); - let new_snapshot = buffer_snapshot.clone(); - let unreviewed_changes = tracked_buffer.unreviewed_changes.clone(); - async move { - let edits = diff_snapshots(&old_snapshot, &new_snapshot); - if let ChangeAuthor::User = author { - apply_non_conflicting_edits( - &unreviewed_changes, - edits, - &mut base_text, - new_snapshot.as_rope(), - ); - } - (Arc::new(base_text.to_string()), base_text) - } - }); - - anyhow::Ok(( - rebase, - tracked_buffer.diff.clone(), - tracked_buffer.buffer.read(cx).language().cloned(), - tracked_buffer.buffer.read(cx).language_registry(), - )) - })??; - - let (new_base_text, new_base_text_rope) = rebase.await; - let diff_snapshot = BufferDiff::update_diff( - diff.clone(), - buffer_snapshot.clone(), - Some(new_base_text), - true, - false, - language, - language_registry, - cx, - ) - .await; - - let mut unreviewed_changes = Patch::default(); - if let Ok(diff_snapshot) = diff_snapshot { - unreviewed_changes = cx - .background_spawn({ - let diff_snapshot = diff_snapshot.clone(); - let buffer_snapshot = buffer_snapshot.clone(); - let new_base_text_rope = new_base_text_rope.clone(); - async move { - let mut unreviewed_changes = Patch::default(); - for hunk in diff_snapshot.hunks_intersecting_range( - Anchor::MIN..Anchor::MAX, - &buffer_snapshot, - ) { - let old_range = new_base_text_rope - .offset_to_point(hunk.diff_base_byte_range.start) - ..new_base_text_rope - .offset_to_point(hunk.diff_base_byte_range.end); - let new_range = hunk.range.start..hunk.range.end; - unreviewed_changes.push(point_to_row_edit( - Edit { - old: old_range, - new: new_range, - }, - &new_base_text_rope, - &buffer_snapshot.as_rope(), - )); - } - unreviewed_changes - } - }) - .await; - - diff.update(cx, |diff, cx| { - diff.set_snapshot(diff_snapshot, &buffer_snapshot, cx) - })?; - } - this.update(cx, |this, cx| { - let tracked_buffer = this - .tracked_buffers - .get_mut(&buffer) - .context("buffer not tracked")?; - tracked_buffer.base_text = new_base_text_rope; - tracked_buffer.snapshot = buffer_snapshot; - tracked_buffer.unreviewed_changes = unreviewed_changes; - cx.notify(); - anyhow::Ok(()) - })??; - } - - Ok(()) - } - - /// Track a buffer as read, so we can notify the model about user edits. - pub fn buffer_read(&mut self, buffer: Entity, cx: &mut Context) { - self.track_buffer_internal(buffer, false, cx); - } - - /// Mark a buffer as edited, so we can refresh it in the context - pub fn buffer_created(&mut self, buffer: Entity, cx: &mut Context) { - self.edited_since_project_diagnostics_check = true; - self.tracked_buffers.remove(&buffer); - self.track_buffer_internal(buffer.clone(), true, cx); - } - - /// Mark a buffer as edited, so we can refresh it in the context - pub fn buffer_edited(&mut self, buffer: Entity, cx: &mut Context) { - 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; - } - tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx); - } - - pub fn will_delete_buffer(&mut self, buffer: Entity, cx: &mut Context) { - let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx); - match tracked_buffer.status { - TrackedBufferStatus::Created => { - self.tracked_buffers.remove(&buffer); - cx.notify(); - } - TrackedBufferStatus::Modified => { - buffer.update(cx, |buffer, cx| buffer.set_text("", cx)); - tracked_buffer.status = TrackedBufferStatus::Deleted; - tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx); - } - TrackedBufferStatus::Deleted => {} - } - cx.notify(); - } - - pub fn keep_edits_in_range( - &mut self, - buffer: Entity, - buffer_range: Range, - cx: &mut Context, - ) { - let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { - return; - }; - - match tracked_buffer.status { - TrackedBufferStatus::Deleted => { - self.tracked_buffers.remove(&buffer); - cx.notify(); - } - _ => { - let buffer = buffer.read(cx); - let buffer_range = - buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer); - let mut delta = 0i32; - - tracked_buffer.unreviewed_changes.retain_mut(|edit| { - edit.old.start = (edit.old.start as i32 + delta) as u32; - edit.old.end = (edit.old.end as i32 + delta) as u32; - - if buffer_range.end.row < edit.new.start - || buffer_range.start.row > edit.new.end - { - true - } else { - let old_range = tracked_buffer - .base_text - .point_to_offset(Point::new(edit.old.start, 0)) - ..tracked_buffer.base_text.point_to_offset(cmp::min( - Point::new(edit.old.end, 0), - tracked_buffer.base_text.max_point(), - )); - let new_range = tracked_buffer - .snapshot - .point_to_offset(Point::new(edit.new.start, 0)) - ..tracked_buffer.snapshot.point_to_offset(cmp::min( - Point::new(edit.new.end, 0), - tracked_buffer.snapshot.max_point(), - )); - tracked_buffer.base_text.replace( - old_range, - &tracked_buffer - .snapshot - .text_for_range(new_range) - .collect::(), - ); - delta += edit.new_len() as i32 - edit.old_len() as i32; - false - } - }); - tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); - } - } - } - - pub fn reject_edits_in_ranges( - &mut self, - buffer: Entity, - buffer_ranges: Vec>, - cx: &mut Context, - ) -> Task> { - let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { - return Task::ready(Ok(())); - }; - - match tracked_buffer.status { - TrackedBufferStatus::Created => { - let delete = buffer - .read(cx) - .entry_id(cx) - .and_then(|entry_id| { - self.project - .update(cx, |project, cx| project.delete_entry(entry_id, false, cx)) - }) - .unwrap_or(Task::ready(Ok(()))); - self.tracked_buffers.remove(&buffer); - cx.notify(); - delete - } - TrackedBufferStatus::Deleted => { - buffer.update(cx, |buffer, cx| { - buffer.set_text(tracked_buffer.base_text.to_string(), cx) - }); - let save = self - .project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)); - - // Clear all tracked changes for this buffer and start over as if we just read it. - self.tracked_buffers.remove(&buffer); - self.buffer_read(buffer.clone(), cx); - cx.notify(); - save - } - TrackedBufferStatus::Modified => { - buffer.update(cx, |buffer, cx| { - let mut buffer_row_ranges = buffer_ranges - .into_iter() - .map(|range| { - range.start.to_point(buffer).row..range.end.to_point(buffer).row - }) - .peekable(); - - let mut edits_to_revert = Vec::new(); - for edit in tracked_buffer.unreviewed_changes.edits() { - let new_range = tracked_buffer - .snapshot - .anchor_before(Point::new(edit.new.start, 0)) - ..tracked_buffer.snapshot.anchor_after(cmp::min( - Point::new(edit.new.end, 0), - tracked_buffer.snapshot.max_point(), - )); - let new_row_range = new_range.start.to_point(buffer).row - ..new_range.end.to_point(buffer).row; - - let mut revert = false; - while let Some(buffer_row_range) = buffer_row_ranges.peek() { - if buffer_row_range.end < new_row_range.start { - buffer_row_ranges.next(); - } else if buffer_row_range.start > new_row_range.end { - break; - } else { - revert = true; - break; - } - } - - if revert { - let old_range = tracked_buffer - .base_text - .point_to_offset(Point::new(edit.old.start, 0)) - ..tracked_buffer.base_text.point_to_offset(cmp::min( - Point::new(edit.old.end, 0), - tracked_buffer.base_text.max_point(), - )); - let old_text = tracked_buffer - .base_text - .chunks_in_range(old_range) - .collect::(); - edits_to_revert.push((new_range, old_text)); - } - } - - buffer.edit(edits_to_revert, None, cx); - }); - self.project - .update(cx, |project, cx| project.save_buffer(buffer, cx)) - } - } - } - - pub fn keep_all_edits(&mut self, cx: &mut Context) { - self.tracked_buffers - .retain(|_buffer, tracked_buffer| match tracked_buffer.status { - TrackedBufferStatus::Deleted => false, - _ => { - tracked_buffer.unreviewed_changes.clear(); - tracked_buffer.base_text = tracked_buffer.snapshot.as_rope().clone(); - tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); - true - } - }); - cx.notify(); - } - - /// Returns the set of buffers that contain changes that haven't been reviewed by the user. - pub fn changed_buffers(&self, cx: &App) -> BTreeMap, Entity> { - self.tracked_buffers - .iter() - .filter(|(_, tracked)| tracked.has_changes(cx)) - .map(|(buffer, tracked)| (buffer.clone(), tracked.diff.clone())) - .collect() - } - - /// Iterate over buffers changed since last read or edited by the model - pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator> { - self.tracked_buffers - .iter() - .filter(|(buffer, tracked)| { - let buffer = buffer.read(cx); - - tracked.version != buffer.version - && buffer - .file() - .map_or(false, |file| file.disk_state() != DiskState::Deleted) - }) - .map(|(buffer, _)| buffer) - } -} - -fn apply_non_conflicting_edits( - patch: &Patch, - edits: Vec>, - old_text: &mut Rope, - new_text: &Rope, -) { - let mut old_edits = patch.edits().iter().cloned().peekable(); - let mut new_edits = edits.into_iter().peekable(); - let mut applied_delta = 0i32; - let mut rebased_delta = 0i32; - - while let Some(mut new_edit) = new_edits.next() { - let mut conflict = false; - - // Push all the old edits that are before this new edit or that intersect with it. - while let Some(old_edit) = old_edits.peek() { - if new_edit.old.end < old_edit.new.start - || (!old_edit.new.is_empty() && new_edit.old.end == old_edit.new.start) - { - break; - } else if new_edit.old.start > old_edit.new.end - || (!old_edit.new.is_empty() && new_edit.old.start == old_edit.new.end) - { - let old_edit = old_edits.next().unwrap(); - rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32; - } else { - conflict = true; - if new_edits - .peek() - .map_or(false, |next_edit| next_edit.old.overlaps(&old_edit.new)) - { - new_edit = new_edits.next().unwrap(); - } else { - let old_edit = old_edits.next().unwrap(); - rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32; - } - } - } - - if !conflict { - // This edit doesn't intersect with any old edit, so we can apply it to the old text. - new_edit.old.start = (new_edit.old.start as i32 + applied_delta - rebased_delta) as u32; - new_edit.old.end = (new_edit.old.end as i32 + applied_delta - rebased_delta) as u32; - let old_bytes = old_text.point_to_offset(Point::new(new_edit.old.start, 0)) - ..old_text.point_to_offset(cmp::min( - Point::new(new_edit.old.end, 0), - old_text.max_point(), - )); - let new_bytes = new_text.point_to_offset(Point::new(new_edit.new.start, 0)) - ..new_text.point_to_offset(cmp::min( - Point::new(new_edit.new.end, 0), - new_text.max_point(), - )); - - old_text.replace( - old_bytes, - &new_text.chunks_in_range(new_bytes).collect::(), - ); - applied_delta += new_edit.new_len() as i32 - new_edit.old_len() as i32; - } - } -} - -fn diff_snapshots( - old_snapshot: &text::BufferSnapshot, - new_snapshot: &text::BufferSnapshot, -) -> Vec> { - let mut edits = new_snapshot - .edits_since::(&old_snapshot.version) - .map(|edit| point_to_row_edit(edit, old_snapshot.as_rope(), new_snapshot.as_rope())) - .peekable(); - let mut row_edits = Vec::new(); - while let Some(mut edit) = edits.next() { - while let Some(next_edit) = edits.peek() { - if edit.old.end >= next_edit.old.start { - edit.old.end = next_edit.old.end; - edit.new.end = next_edit.new.end; - edits.next(); - } else { - break; - } - } - row_edits.push(edit); - } - row_edits -} - -fn point_to_row_edit(edit: Edit, old_text: &Rope, new_text: &Rope) -> Edit { - if edit.old.start.column == old_text.line_len(edit.old.start.row) - && new_text - .chars_at(new_text.point_to_offset(edit.new.start)) - .next() - == Some('\n') - && edit.old.start != old_text.max_point() - { - Edit { - old: edit.old.start.row + 1..edit.old.end.row + 1, - new: edit.new.start.row + 1..edit.new.end.row + 1, - } - } else if edit.old.start.column == 0 - && edit.old.end.column == 0 - && edit.new.end.column == 0 - && edit.old.end != old_text.max_point() - { - Edit { - old: edit.old.start.row..edit.old.end.row, - new: edit.new.start.row..edit.new.end.row, - } - } else { - Edit { - old: edit.old.start.row..edit.old.end.row + 1, - new: edit.new.start.row..edit.new.end.row + 1, - } - } -} - -#[derive(Copy, Clone, Debug)] -enum ChangeAuthor { - User, - Agent, -} - -#[derive(Copy, Clone, Eq, PartialEq)] -enum TrackedBufferStatus { - Created, - Modified, - Deleted, -} - -struct TrackedBuffer { - buffer: Entity, - base_text: Rope, - unreviewed_changes: Patch, - status: TrackedBufferStatus, - version: clock::Global, - diff: Entity, - snapshot: text::BufferSnapshot, - diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>, - _open_lsp_handle: OpenLspBufferHandle, - _maintain_diff: Task<()>, - _subscription: Subscription, -} - -impl TrackedBuffer { - fn has_changes(&self, cx: &App) -> bool { - self.diff - .read(cx) - .hunks(&self.buffer.read(cx), cx) - .next() - .is_some() - } - - fn schedule_diff_update(&self, author: ChangeAuthor, cx: &App) { - self.diff_update - .unbounded_send((author, self.buffer.read(cx).text_snapshot())) - .ok(); - } -} - -pub struct ChangedBuffer { - pub diff: Entity, -} - -#[cfg(test)] -mod tests { - use std::env; - - use super::*; - use buffer_diff::DiffHunkStatusKind; - use gpui::TestAppContext; - use language::Point; - use project::{FakeFs, Fs, Project, RemoveOptions}; - use rand::prelude::*; - use serde_json::json; - use settings::SettingsStore; - use util::{RandomCharIter, path}; - - #[ctor::ctor] - fn init_logger() { - zlog::init_test(); - } - - 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(iterations = 10)] - async fn test_keep_edits(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"})) - .await; - 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/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx) - .unwrap() - }); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx) - .unwrap() - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndEf\nghi\njkl\nmnO" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(2, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\n".into(), - }, - HunkStatus { - range: Point::new(4, 0)..Point::new(4, 3), - diff_status: DiffHunkStatusKind::Modified, - old_text: "mno".into(), - } - ], - )] - ); - - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), cx) - }); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(1, 0)..Point::new(2, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\n".into(), - }], - )] - ); - - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), cx) - }); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_deletions(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({"file": "abc\ndef\nghi\njkl\nmno\npqr"}), - ) - .await; - 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/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 0)..Point::new(2, 0), "")], None, cx) - .unwrap(); - buffer.finalize_last_transaction(); - }); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(3, 0)..Point::new(4, 0), "")], None, cx) - .unwrap(); - buffer.finalize_last_transaction(); - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\nghi\njkl\npqr" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(1, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "def\n".into(), - }, - HunkStatus { - range: Point::new(3, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "mno\n".into(), - } - ], - )] - ); - - buffer.update(cx, |buffer, cx| buffer.undo(cx)); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\nghi\njkl\nmno\npqr" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(1, 0)..Point::new(1, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "def\n".into(), - }], - )] - ); - - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), cx) - }); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_overlapping_user_edits(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"})) - .await; - 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/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx) - .unwrap() - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndeF\nGHI\njkl\nmno" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\nghi\n".into(), - }], - )] - ); - - buffer.update(cx, |buffer, cx| { - buffer.edit( - [ - (Point::new(0, 2)..Point::new(0, 2), "X"), - (Point::new(3, 0)..Point::new(3, 0), "Y"), - ], - None, - cx, - ) - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abXc\ndeF\nGHI\nYjkl\nmno" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\nghi\n".into(), - }], - )] - ); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "Z")], None, cx) - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abXc\ndZeF\nGHI\nYjkl\nmno" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\nghi\n".into(), - }], - )] - ); - - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx) - }); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_creating_files(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({})).await; - 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/file1", cx)) - .unwrap(); - - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| buffer.set_text("lorem", 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_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 5), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into(), - }], - )] - ); - - buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "X")], None, cx)); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 6), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into(), - }], - )] - ); - - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), 0..5, cx) - }); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_deleting_files(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({"file1": "lorem\n", "file2": "ipsum\n"}), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let file1_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx)) - .unwrap(); - let file2_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file2", cx)) - .unwrap(); - - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let buffer1 = project - .update(cx, |project, cx| { - project.open_buffer(file1_path.clone(), cx) - }) - .await - .unwrap(); - let buffer2 = project - .update(cx, |project, cx| { - project.open_buffer(file2_path.clone(), cx) - }) - .await - .unwrap(); - - action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx)); - action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx)); - project - .update(cx, |project, cx| { - project.delete_file(file1_path.clone(), false, cx) - }) - .unwrap() - .await - .unwrap(); - project - .update(cx, |project, cx| { - project.delete_file(file2_path.clone(), false, cx) - }) - .unwrap() - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![ - ( - buffer1.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "lorem\n".into(), - }] - ), - ( - buffer2.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "ipsum\n".into(), - }], - ) - ] - ); - - // Simulate file1 being recreated externally. - fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec()) - .await; - - // Simulate file2 being recreated by a tool. - let buffer2 = project - .update(cx, |project, cx| project.open_buffer(file2_path, cx)) - .await - .unwrap(); - action_log.update(cx, |log, cx| log.buffer_read(buffer2.clone(), cx)); - buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx)); - action_log.update(cx, |log, cx| log.buffer_edited(buffer2.clone(), cx)); - project - .update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx)) - .await - .unwrap(); - - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer2.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 5), - diff_status: DiffHunkStatusKind::Modified, - old_text: "ipsum\n".into(), - }], - )] - ); - - // Simulate file2 being deleted externally. - fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default()) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_reject_edits(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"})) - .await; - 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/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx) - .unwrap() - }); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx) - .unwrap() - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndE\nXYZf\nghi\njkl\nmnO" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\n".into(), - }, - HunkStatus { - range: Point::new(5, 0)..Point::new(5, 3), - diff_status: DiffHunkStatusKind::Modified, - old_text: "mno".into(), - } - ], - )] - ); - - // If the rejected range doesn't overlap with any hunk, we ignore it. - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(4, 0)..Point::new(4, 0)], - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndE\nXYZf\nghi\njkl\nmnO" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\n".into(), - }, - HunkStatus { - range: Point::new(5, 0)..Point::new(5, 3), - diff_status: DiffHunkStatusKind::Modified, - old_text: "mno".into(), - } - ], - )] - ); - - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(0, 0)..Point::new(1, 0)], - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndef\nghi\njkl\nmnO" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(4, 0)..Point::new(4, 3), - diff_status: DiffHunkStatusKind::Modified, - old_text: "mno".into(), - }], - )] - ); - - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(4, 0)..Point::new(4, 0)], - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndef\nghi\njkl\nmno" - ); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_reject_multiple_edits(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"})) - .await; - 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/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx) - .unwrap() - }); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx) - .unwrap() - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndE\nXYZf\nghi\njkl\nmnO" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\n".into(), - }, - HunkStatus { - range: Point::new(5, 0)..Point::new(5, 3), - diff_status: DiffHunkStatusKind::Modified, - old_text: "mno".into(), - } - ], - )] - ); - - action_log.update(cx, |log, cx| { - let range_1 = buffer.read(cx).anchor_before(Point::new(0, 0)) - ..buffer.read(cx).anchor_before(Point::new(1, 0)); - let range_2 = buffer.read(cx).anchor_before(Point::new(5, 0)) - ..buffer.read(cx).anchor_before(Point::new(5, 3)); - - log.reject_edits_in_ranges(buffer.clone(), vec![range_1, range_2], cx) - .detach(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndef\nghi\njkl\nmno" - ); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndef\nghi\njkl\nmno" - ); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_reject_deleted_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": "content"})) - .await; - 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/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.will_delete_buffer(buffer.clone(), cx)); - }); - project - .update(cx, |project, cx| { - project.delete_file(file_path.clone(), false, cx) - }) - .unwrap() - .await - .unwrap(); - cx.run_until_parked(); - assert!(!fs.is_file(path!("/dir/file").as_ref()).await); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "content".into(), - }] - )] - ); - - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(0, 0)..Point::new(0, 0)], - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "content"); - assert!(fs.is_file(path!("/dir/file").as_ref()).await); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_reject_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, cx)) - .await - .unwrap(); - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| buffer.set_text("content", 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(); - assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 7), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into(), - }], - )] - ); - - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(0, 0)..Point::new(0, 11)], - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert!(!fs.is_file(path!("/dir/new_file").as_ref()).await); - 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); - - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(20); - - let text = RandomCharIter::new(&mut rng).take(50).collect::(); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": text})).await; - 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/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - - for _ in 0..operations { - match rng.gen_range(0..100) { - 0..25 => { - action_log.update(cx, |log, cx| { - let range = buffer.read(cx).random_byte_range(0, &mut rng); - log::info!("keeping edits in range {:?}", range); - log.keep_edits_in_range(buffer.clone(), range, cx) - }); - } - 25..50 => { - action_log - .update(cx, |log, cx| { - let range = buffer.read(cx).random_byte_range(0, &mut rng); - log::info!("rejecting edits in range {:?}", range); - log.reject_edits_in_ranges(buffer.clone(), vec![range], cx) - }) - .await - .unwrap(); - } - _ => { - let is_agent_change = rng.gen_bool(0.5); - if is_agent_change { - log::info!("agent edit"); - } else { - log::info!("user edit"); - } - cx.update(|cx| { - buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx)); - if is_agent_change { - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - } - }); - } - } - - if rng.gen_bool(0.2) { - quiesce(&action_log, &buffer, cx); - } - } - - quiesce(&action_log, &buffer, cx); - - fn quiesce( - action_log: &Entity, - buffer: &Entity, - cx: &mut TestAppContext, - ) { - log::info!("quiescing..."); - cx.run_until_parked(); - action_log.update(cx, |log, cx| { - let tracked_buffer = log.tracked_buffers.get(&buffer).unwrap(); - let mut old_text = tracked_buffer.base_text.clone(); - let new_text = buffer.read(cx).as_rope(); - for edit in tracked_buffer.unreviewed_changes.edits() { - let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0)); - let old_end = old_text.point_to_offset(cmp::min( - Point::new(edit.new.start + edit.old_len(), 0), - old_text.max_point(), - )); - old_text.replace( - old_start..old_end, - &new_text.slice_rows(edit.new.clone()).to_string(), - ); - } - pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string()); - }) - } - } - - #[derive(Debug, Clone, PartialEq, Eq)] - struct HunkStatus { - range: Range, - diff_status: DiffHunkStatusKind, - old_text: String, - } - - fn unreviewed_hunks( - action_log: &Entity, - cx: &TestAppContext, - ) -> Vec<(Entity, Vec)> { - cx.read(|cx| { - action_log - .read(cx) - .changed_buffers(cx) - .into_iter() - .map(|(buffer, diff)| { - let snapshot = buffer.read(cx).snapshot(); - ( - buffer, - diff.read(cx) - .hunks(&snapshot, cx) - .map(|hunk| HunkStatus { - diff_status: hunk.status().kind, - range: hunk.range, - old_text: diff - .read(cx) - .base_text() - .text_for_range(hunk.diff_base_byte_range) - .collect(), - }) - .collect(), - ) - }) - .collect() - }) - } -} diff --git a/crates/agent/src/edit_agent/evals/fixtures/delete_run_git_blame/after.rs b/crates/agent/src/edit_agent/evals/fixtures/delete_run_git_blame/after.rs deleted file mode 100644 index 89277be443..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/delete_run_git_blame/after.rs +++ /dev/null @@ -1,328 +0,0 @@ -use crate::commit::get_messages; -use crate::{GitRemote, Oid}; -use anyhow::{Context as _, Result, anyhow}; -use collections::{HashMap, HashSet}; -use futures::AsyncWriteExt; -use gpui::SharedString; -use serde::{Deserialize, Serialize}; -use std::process::Stdio; -use std::{ops::Range, path::Path}; -use text::Rope; -use time::OffsetDateTime; -use time::UtcOffset; -use time::macros::format_description; - -pub use git2 as libgit; - -#[derive(Debug, Clone, Default)] -pub struct Blame { - pub entries: Vec, - pub messages: HashMap, - pub remote_url: Option, -} - -#[derive(Clone, Debug, Default)] -pub struct ParsedCommitMessage { - pub message: SharedString, - pub permalink: Option, - pub pull_request: Option, - pub remote: Option, -} - -impl Blame { - pub async fn for_path( - git_binary: &Path, - working_directory: &Path, - path: &Path, - content: &Rope, - remote_url: Option, - ) -> Result { - let output = run_git_blame(git_binary, working_directory, path, content).await?; - let mut entries = parse_git_blame(&output)?; - entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start)); - - let mut unique_shas = HashSet::default(); - - for entry in entries.iter_mut() { - unique_shas.insert(entry.sha); - } - - let shas = unique_shas.into_iter().collect::>(); - let messages = get_messages(working_directory, &shas) - .await - .context("failed to get commit messages")?; - - Ok(Self { - entries, - messages, - remote_url, - }) - } -} - -const GIT_BLAME_NO_COMMIT_ERROR: &str = "fatal: no such ref: HEAD"; -const GIT_BLAME_NO_PATH: &str = "fatal: no such path"; - -#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] -pub struct BlameEntry { - pub sha: Oid, - - pub range: Range, - - pub original_line_number: u32, - - pub author: Option, - pub author_mail: Option, - pub author_time: Option, - pub author_tz: Option, - - pub committer_name: Option, - pub committer_email: Option, - pub committer_time: Option, - pub committer_tz: Option, - - pub summary: Option, - - pub previous: Option, - pub filename: String, -} - -impl BlameEntry { - // Returns a BlameEntry by parsing the first line of a `git blame --incremental` - // entry. The line MUST have this format: - // - // <40-byte-hex-sha1> - fn new_from_blame_line(line: &str) -> Result { - let mut parts = line.split_whitespace(); - - let sha = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing sha from {line}"))?; - - let original_line_number = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing original line number from {line}"))?; - let final_line_number = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing final line number from {line}"))?; - - let line_count = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing line count from {line}"))?; - - let start_line = final_line_number.saturating_sub(1); - let end_line = start_line + line_count; - let range = start_line..end_line; - - Ok(Self { - sha, - range, - original_line_number, - ..Default::default() - }) - } - - pub fn author_offset_date_time(&self) -> Result { - if let (Some(author_time), Some(author_tz)) = (self.author_time, &self.author_tz) { - let format = format_description!("[offset_hour][offset_minute]"); - let offset = UtcOffset::parse(author_tz, &format)?; - let date_time_utc = OffsetDateTime::from_unix_timestamp(author_time)?; - - Ok(date_time_utc.to_offset(offset)) - } else { - // Directly return current time in UTC if there's no committer time or timezone - Ok(time::OffsetDateTime::now_utc()) - } - } -} - -// parse_git_blame parses the output of `git blame --incremental`, which returns -// all the blame-entries for a given path incrementally, as it finds them. -// -// Each entry *always* starts with: -// -// <40-byte-hex-sha1> -// -// Each entry *always* ends with: -// -// filename -// -// Line numbers are 1-indexed. -// -// A `git blame --incremental` entry looks like this: -// -// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 2 2 1 -// author Joe Schmoe -// author-mail -// author-time 1709741400 -// author-tz +0100 -// committer Joe Schmoe -// committer-mail -// committer-time 1709741400 -// committer-tz +0100 -// summary Joe's cool commit -// previous 486c2409237a2c627230589e567024a96751d475 index.js -// filename index.js -// -// If the entry has the same SHA as an entry that was already printed then no -// signature information is printed: -// -// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 3 4 1 -// previous 486c2409237a2c627230589e567024a96751d475 index.js -// filename index.js -// -// More about `--incremental` output: https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-blame.html -fn parse_git_blame(output: &str) -> Result> { - let mut entries: Vec = Vec::new(); - let mut index: HashMap = HashMap::default(); - - let mut current_entry: Option = None; - - for line in output.lines() { - let mut done = false; - - match &mut current_entry { - None => { - let mut new_entry = BlameEntry::new_from_blame_line(line)?; - - if let Some(existing_entry) = index - .get(&new_entry.sha) - .and_then(|slot| entries.get(*slot)) - { - new_entry.author.clone_from(&existing_entry.author); - new_entry - .author_mail - .clone_from(&existing_entry.author_mail); - new_entry.author_time = existing_entry.author_time; - new_entry.author_tz.clone_from(&existing_entry.author_tz); - new_entry - .committer_name - .clone_from(&existing_entry.committer_name); - new_entry - .committer_email - .clone_from(&existing_entry.committer_email); - new_entry.committer_time = existing_entry.committer_time; - new_entry - .committer_tz - .clone_from(&existing_entry.committer_tz); - new_entry.summary.clone_from(&existing_entry.summary); - } - - current_entry.replace(new_entry); - } - Some(entry) => { - let Some((key, value)) = line.split_once(' ') else { - continue; - }; - let is_committed = !entry.sha.is_zero(); - match key { - "filename" => { - entry.filename = value.into(); - done = true; - } - "previous" => entry.previous = Some(value.into()), - - "summary" if is_committed => entry.summary = Some(value.into()), - "author" if is_committed => entry.author = Some(value.into()), - "author-mail" if is_committed => entry.author_mail = Some(value.into()), - "author-time" if is_committed => { - entry.author_time = Some(value.parse::()?) - } - "author-tz" if is_committed => entry.author_tz = Some(value.into()), - - "committer" if is_committed => entry.committer_name = Some(value.into()), - "committer-mail" if is_committed => entry.committer_email = Some(value.into()), - "committer-time" if is_committed => { - entry.committer_time = Some(value.parse::()?) - } - "committer-tz" if is_committed => entry.committer_tz = Some(value.into()), - _ => {} - } - } - }; - - 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); - } - } - } - } - - Ok(entries) -} - -#[cfg(test)] -mod tests { - use std::path::PathBuf; - - use super::BlameEntry; - use super::parse_git_blame; - - fn read_test_data(filename: &str) -> String { - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("test_data"); - path.push(filename); - - std::fs::read_to_string(&path) - .unwrap_or_else(|_| panic!("Could not read test data at {:?}. Is it generated?", path)) - } - - fn assert_eq_golden(entries: &Vec, golden_filename: &str) { - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("test_data"); - path.push("golden"); - path.push(format!("{}.json", golden_filename)); - - let mut have_json = - serde_json::to_string_pretty(&entries).expect("could not serialize entries to JSON"); - // We always want to save with a trailing newline. - have_json.push('\n'); - - let update = std::env::var("UPDATE_GOLDEN") - .map(|val| val.eq_ignore_ascii_case("true")) - .unwrap_or(false); - - if update { - std::fs::create_dir_all(path.parent().unwrap()) - .expect("could not create golden test data directory"); - std::fs::write(&path, have_json).expect("could not write out golden data"); - } else { - let want_json = - std::fs::read_to_string(&path).unwrap_or_else(|_| { - panic!("could not read golden test data file at {:?}. Did you run the test with UPDATE_GOLDEN=true before?", path); - }).replace("\r\n", "\n"); - - pretty_assertions::assert_eq!(have_json, want_json, "wrong blame entries"); - } - } - - #[test] - fn test_parse_git_blame_not_committed() { - let output = read_test_data("blame_incremental_not_committed"); - let entries = parse_git_blame(&output).unwrap(); - assert_eq_golden(&entries, "blame_incremental_not_committed"); - } - - #[test] - fn test_parse_git_blame_simple() { - let output = read_test_data("blame_incremental_simple"); - let entries = parse_git_blame(&output).unwrap(); - assert_eq_golden(&entries, "blame_incremental_simple"); - } - - #[test] - fn test_parse_git_blame_complex() { - let output = read_test_data("blame_incremental_complex"); - let entries = parse_git_blame(&output).unwrap(); - assert_eq_golden(&entries, "blame_incremental_complex"); - } -} diff --git a/crates/agent/src/edit_agent/evals/fixtures/delete_run_git_blame/before.rs b/crates/agent/src/edit_agent/evals/fixtures/delete_run_git_blame/before.rs deleted file mode 100644 index 36fccb5132..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/delete_run_git_blame/before.rs +++ /dev/null @@ -1,371 +0,0 @@ -use crate::commit::get_messages; -use crate::{GitRemote, Oid}; -use anyhow::{Context as _, Result, anyhow}; -use collections::{HashMap, HashSet}; -use futures::AsyncWriteExt; -use gpui::SharedString; -use serde::{Deserialize, Serialize}; -use std::process::Stdio; -use std::{ops::Range, path::Path}; -use text::Rope; -use time::OffsetDateTime; -use time::UtcOffset; -use time::macros::format_description; - -pub use git2 as libgit; - -#[derive(Debug, Clone, Default)] -pub struct Blame { - pub entries: Vec, - pub messages: HashMap, - pub remote_url: Option, -} - -#[derive(Clone, Debug, Default)] -pub struct ParsedCommitMessage { - pub message: SharedString, - pub permalink: Option, - pub pull_request: Option, - pub remote: Option, -} - -impl Blame { - pub async fn for_path( - git_binary: &Path, - working_directory: &Path, - path: &Path, - content: &Rope, - remote_url: Option, - ) -> Result { - let output = run_git_blame(git_binary, working_directory, path, content).await?; - let mut entries = parse_git_blame(&output)?; - entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start)); - - let mut unique_shas = HashSet::default(); - - for entry in entries.iter_mut() { - unique_shas.insert(entry.sha); - } - - let shas = unique_shas.into_iter().collect::>(); - let messages = get_messages(working_directory, &shas) - .await - .context("failed to get commit messages")?; - - Ok(Self { - entries, - messages, - remote_url, - }) - } -} - -const GIT_BLAME_NO_COMMIT_ERROR: &str = "fatal: no such ref: HEAD"; -const GIT_BLAME_NO_PATH: &str = "fatal: no such path"; - -async fn run_git_blame( - git_binary: &Path, - working_directory: &Path, - path: &Path, - contents: &Rope, -) -> Result { - let mut child = util::command::new_smol_command(git_binary) - .current_dir(working_directory) - .arg("blame") - .arg("--incremental") - .arg("--contents") - .arg("-") - .arg(path.as_os_str()) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .context("starting git blame process")?; - - let stdin = child - .stdin - .as_mut() - .context("failed to get pipe to stdin of git blame command")?; - - for chunk in contents.chunks() { - stdin.write_all(chunk.as_bytes()).await?; - } - stdin.flush().await?; - - let output = child.output().await.context("reading git blame output")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - let trimmed = stderr.trim(); - if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { - return Ok(String::new()); - } - anyhow::bail!("git blame process failed: {stderr}"); - } - - Ok(String::from_utf8(output.stdout)?) -} - -#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] -pub struct BlameEntry { - pub sha: Oid, - - pub range: Range, - - pub original_line_number: u32, - - pub author: Option, - pub author_mail: Option, - pub author_time: Option, - pub author_tz: Option, - - pub committer_name: Option, - pub committer_email: Option, - pub committer_time: Option, - pub committer_tz: Option, - - pub summary: Option, - - pub previous: Option, - pub filename: String, -} - -impl BlameEntry { - // Returns a BlameEntry by parsing the first line of a `git blame --incremental` - // entry. The line MUST have this format: - // - // <40-byte-hex-sha1> - fn new_from_blame_line(line: &str) -> Result { - let mut parts = line.split_whitespace(); - - let sha = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing sha from {line}"))?; - - let original_line_number = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing original line number from {line}"))?; - let final_line_number = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing final line number from {line}"))?; - - let line_count = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing line count from {line}"))?; - - let start_line = final_line_number.saturating_sub(1); - let end_line = start_line + line_count; - let range = start_line..end_line; - - Ok(Self { - sha, - range, - original_line_number, - ..Default::default() - }) - } - - pub fn author_offset_date_time(&self) -> Result { - if let (Some(author_time), Some(author_tz)) = (self.author_time, &self.author_tz) { - let format = format_description!("[offset_hour][offset_minute]"); - let offset = UtcOffset::parse(author_tz, &format)?; - let date_time_utc = OffsetDateTime::from_unix_timestamp(author_time)?; - - Ok(date_time_utc.to_offset(offset)) - } else { - // Directly return current time in UTC if there's no committer time or timezone - Ok(time::OffsetDateTime::now_utc()) - } - } -} - -// parse_git_blame parses the output of `git blame --incremental`, which returns -// all the blame-entries for a given path incrementally, as it finds them. -// -// Each entry *always* starts with: -// -// <40-byte-hex-sha1> -// -// Each entry *always* ends with: -// -// filename -// -// Line numbers are 1-indexed. -// -// A `git blame --incremental` entry looks like this: -// -// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 2 2 1 -// author Joe Schmoe -// author-mail -// author-time 1709741400 -// author-tz +0100 -// committer Joe Schmoe -// committer-mail -// committer-time 1709741400 -// committer-tz +0100 -// summary Joe's cool commit -// previous 486c2409237a2c627230589e567024a96751d475 index.js -// filename index.js -// -// If the entry has the same SHA as an entry that was already printed then no -// signature information is printed: -// -// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 3 4 1 -// previous 486c2409237a2c627230589e567024a96751d475 index.js -// filename index.js -// -// More about `--incremental` output: https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-blame.html -fn parse_git_blame(output: &str) -> Result> { - let mut entries: Vec = Vec::new(); - let mut index: HashMap = HashMap::default(); - - let mut current_entry: Option = None; - - for line in output.lines() { - let mut done = false; - - match &mut current_entry { - None => { - let mut new_entry = BlameEntry::new_from_blame_line(line)?; - - if let Some(existing_entry) = index - .get(&new_entry.sha) - .and_then(|slot| entries.get(*slot)) - { - new_entry.author.clone_from(&existing_entry.author); - new_entry - .author_mail - .clone_from(&existing_entry.author_mail); - new_entry.author_time = existing_entry.author_time; - new_entry.author_tz.clone_from(&existing_entry.author_tz); - new_entry - .committer_name - .clone_from(&existing_entry.committer_name); - new_entry - .committer_email - .clone_from(&existing_entry.committer_email); - new_entry.committer_time = existing_entry.committer_time; - new_entry - .committer_tz - .clone_from(&existing_entry.committer_tz); - new_entry.summary.clone_from(&existing_entry.summary); - } - - current_entry.replace(new_entry); - } - Some(entry) => { - let Some((key, value)) = line.split_once(' ') else { - continue; - }; - let is_committed = !entry.sha.is_zero(); - match key { - "filename" => { - entry.filename = value.into(); - done = true; - } - "previous" => entry.previous = Some(value.into()), - - "summary" if is_committed => entry.summary = Some(value.into()), - "author" if is_committed => entry.author = Some(value.into()), - "author-mail" if is_committed => entry.author_mail = Some(value.into()), - "author-time" if is_committed => { - entry.author_time = Some(value.parse::()?) - } - "author-tz" if is_committed => entry.author_tz = Some(value.into()), - - "committer" if is_committed => entry.committer_name = Some(value.into()), - "committer-mail" if is_committed => entry.committer_email = Some(value.into()), - "committer-time" if is_committed => { - entry.committer_time = Some(value.parse::()?) - } - "committer-tz" if is_committed => entry.committer_tz = Some(value.into()), - _ => {} - } - } - }; - - 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); - } - } - } - } - - Ok(entries) -} - -#[cfg(test)] -mod tests { - use std::path::PathBuf; - - use super::BlameEntry; - use super::parse_git_blame; - - fn read_test_data(filename: &str) -> String { - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("test_data"); - path.push(filename); - - std::fs::read_to_string(&path) - .unwrap_or_else(|_| panic!("Could not read test data at {:?}. Is it generated?", path)) - } - - fn assert_eq_golden(entries: &Vec, golden_filename: &str) { - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("test_data"); - path.push("golden"); - path.push(format!("{}.json", golden_filename)); - - let mut have_json = - serde_json::to_string_pretty(&entries).expect("could not serialize entries to JSON"); - // We always want to save with a trailing newline. - have_json.push('\n'); - - let update = std::env::var("UPDATE_GOLDEN") - .map(|val| val.eq_ignore_ascii_case("true")) - .unwrap_or(false); - - if update { - std::fs::create_dir_all(path.parent().unwrap()) - .expect("could not create golden test data directory"); - std::fs::write(&path, have_json).expect("could not write out golden data"); - } else { - let want_json = - std::fs::read_to_string(&path).unwrap_or_else(|_| { - panic!("could not read golden test data file at {:?}. Did you run the test with UPDATE_GOLDEN=true before?", path); - }).replace("\r\n", "\n"); - - pretty_assertions::assert_eq!(have_json, want_json, "wrong blame entries"); - } - } - - #[test] - fn test_parse_git_blame_not_committed() { - let output = read_test_data("blame_incremental_not_committed"); - let entries = parse_git_blame(&output).unwrap(); - assert_eq_golden(&entries, "blame_incremental_not_committed"); - } - - #[test] - fn test_parse_git_blame_simple() { - let output = read_test_data("blame_incremental_simple"); - let entries = parse_git_blame(&output).unwrap(); - assert_eq_golden(&entries, "blame_incremental_simple"); - } - - #[test] - fn test_parse_git_blame_complex() { - let output = read_test_data("blame_incremental_complex"); - let entries = parse_git_blame(&output).unwrap(); - assert_eq_golden(&entries, "blame_incremental_complex"); - } -} diff --git a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs b/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs deleted file mode 100644 index 607daa8ce3..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs +++ /dev/null @@ -1,21343 +0,0 @@ -#![allow(rustdoc::private_intra_doc_links)] -//! This is the place where everything editor-related is stored (data-wise) and displayed (ui-wise). -//! The main point of interest in this crate is [`Editor`] type, which is used in every other Zed part as a user input element. -//! It comes in different flavors: single line, multiline and a fixed height one. -//! -//! Editor contains of multiple large submodules: -//! * [`element`] — the place where all rendering happens -//! * [`display_map`] - chunks up text in the editor into the logical blocks, establishes coordinates and mapping between each of them. -//! Contains all metadata related to text transformations (folds, fake inlay text insertions, soft wraps, tab markup, etc.). -//! * [`inlay_hint_cache`] - is a storage of inlay hints out of LSP requests, responsible for querying LSP and updating `display_map`'s state accordingly. -//! -//! All other submodules and structs are mostly concerned with holding editor data about the way it displays current buffer region(s). -//! -//! If you're looking to improve Vim mode, you should check out Vim crate that wraps Editor and overrides its behavior. -pub mod actions; -mod blink_manager; -mod clangd_ext; -mod code_context_menus; -pub mod display_map; -mod editor_settings; -mod editor_settings_controls; -mod element; -mod git; -mod highlight_matching_bracket; -mod hover_links; -pub mod hover_popover; -mod indent_guides; -mod inlay_hint_cache; -pub mod items; -mod jsx_tag_auto_close; -mod linked_editing_ranges; -mod lsp_ext; -mod mouse_context_menu; -pub mod movement; -mod persistence; -mod proposed_changes_editor; -mod rust_analyzer_ext; -pub mod scroll; -mod selections_collection; -pub mod tasks; - -#[cfg(test)] -mod code_completion_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 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 clock::ReplicaId; -use collections::{BTreeMap, HashMap, HashSet, VecDeque}; -use convert_case::{Case, Casing}; -use display_map::*; -pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder}; -use editor_settings::GoToDefinitionFallback; -pub use editor_settings::{ - CurrentLineHighlight, EditorSettings, HideMouseMode, ScrollBeyondLastLine, SearchSettings, - ShowScrollbar, -}; -pub use editor_settings_controls::*; -use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap, layout_line}; -pub use element::{ - CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, -}; -use feature_flags::{DebuggerFeatureFlag, FeatureFlagAppExt}; -use futures::{ - FutureExt, - future::{self, Shared, join}, -}; -use fuzzy::StringMatchCandidate; - -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, - AvailableSpace, Background, Bounds, ClickEvent, ClipboardEntry, ClipboardItem, Context, - DispatchPhase, Edges, Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, - Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla, KeyContext, Modifiers, - MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, ScrollHandle, - SharedString, Size, Stateful, Styled, Subscription, Task, TextStyle, TextStyleRefinement, - UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window, - div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, -}; -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}; -pub use inline_completion::Direction; -use inline_completion::{EditPredictionProvider, InlineCompletionProviderHandle}; -pub use items::MAX_TAB_TITLE_LEN; -use itertools::Itertools; -use language::{ - AutoindentMode, 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, text_diff_with_options, -}; -use language::{BufferRow, CharClassifier, Runnable, RunnableRange, point_to_lsp}; -use linked_editing_ranges::refresh_linked_ranges; -use markdown::Markdown; -use mouse_context_menu::MouseContextMenu; -use persistence::DB; -use project::{ - ProjectPath, - debugger::{ - breakpoint_store::{ - BreakpointEditAction, BreakpointState, BreakpointStore, BreakpointStoreEvent, - }, - session::{Session, SessionEvent}, - }, -}; - -pub use git::blame::BlameRenderer; -pub use proposed_changes_editor::{ - ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar, -}; -use smallvec::smallvec; -use std::{cell::OnceCell, iter::Peekable}; -use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables}; - -pub use lsp::CompletionContext; -use lsp::{ - CodeActionKind, CompletionItemKind, CompletionTriggerKind, DiagnosticSeverity, - 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::prelude::*; -use rpc::{ErrorExt, proto::*}; -use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide}; -use selections_collection::{ - MutableSelectionsCollection, SelectionsCollection, resolve_selections, -}; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsLocation, SettingsStore, update_settings_file}; -use smallvec::SmallVec; -use snippet::Snippet; -use std::sync::Arc; -use std::{ - any::TypeId, - borrow::Cow, - cell::RefCell, - cmp::{self, Ordering, Reverse}, - mem, - num::NonZeroU32, - ops::{ControlFlow, Deref, DerefMut, Not as _, Range, RangeInclusive}, - path::{Path, PathBuf}, - rc::Rc, - time::{Duration, Instant}, -}; -pub use sum_tree::Bias; -use sum_tree::TreeMap; -use text::{BufferId, FromAnchor, OffsetUtf16, Rope}; -use theme::{ - ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings, - observe_buffer_font_size_adjustment, -}; -use ui::{ - ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName, - IconSize, Key, Tooltip, h_flex, prelude::*, -}; -use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc}; -use workspace::{ - Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal, - RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast, - ViewId, Workspace, WorkspaceId, WorkspaceSettings, - item::{ItemHandle, PreviewTabsSettings}, - notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt}, - searchable::SearchEvent, -}; - -use crate::hover_links::{find_url, find_url_from_range}; -use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState}; - -pub const FILE_HEADER_HEIGHT: u32 = 2; -pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u32 = 1; -pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2; -const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); -const MAX_LINE_LEN: usize = 1024; -const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; -const MAX_SELECTION_HISTORY_LEN: usize = 1024; -pub(crate) const CURSORS_VISIBLE_FOR: Duration = Duration::from_millis(2000); -#[doc(hidden)] -pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250); -const SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100); - -pub(crate) const CODE_ACTION_TIMEOUT: Duration = Duration::from_secs(5); -pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(5); -pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1); - -pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction"; -pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict"; -pub(crate) const MIN_LINE_NUMBER_DIGITS: u32 = 4; - -pub type RenderDiffHunkControlsFn = Arc< - dyn Fn( - u32, - &DiffHunkStatus, - Range, - bool, - Pixels, - &Entity, - &mut Window, - &mut App, - ) -> AnyElement, ->; - -const COLUMNAR_SELECTION_MODIFIERS: Modifiers = Modifiers { - alt: true, - shift: true, - control: false, - platform: false, - function: false, -}; - -struct InlineValueCache { - enabled: bool, - inlays: Vec, - refresh_task: Task>, -} - -impl InlineValueCache { - fn new(enabled: bool) -> Self { - Self { - enabled, - inlays: Vec::new(), - refresh_task: Task::ready(None), - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum InlayId { - InlineCompletion(usize), - Hint(usize), - DebuggerValue(usize), -} - -impl InlayId { - fn id(&self) -> usize { - match self { - Self::InlineCompletion(id) => *id, - Self::Hint(id) => *id, - Self::DebuggerValue(id) => *id, - } - } -} - -pub enum ActiveDebugLine {} -enum DocumentHighlightRead {} -enum DocumentHighlightWrite {} -enum InputComposition {} -enum SelectedTextHighlight {} - -pub enum ConflictsOuter {} -pub enum ConflictsOurs {} -pub enum ConflictsTheirs {} -pub enum ConflictsOursMarker {} -pub enum ConflictsTheirsMarker {} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum Navigated { - Yes, - No, -} - -impl Navigated { - pub fn from_bool(yes: bool) -> Navigated { - if yes { Navigated::Yes } else { Navigated::No } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum DisplayDiffHunk { - Folded { - display_row: DisplayRow, - }, - Unfolded { - is_created_file: bool, - diff_base_byte_range: Range, - display_row_range: Range, - multi_buffer_range: Range, - status: DiffHunkStatus, - }, -} - -pub enum HideMouseCursorOrigin { - TypingAction, - MovementAction, -} - -pub fn init_settings(cx: &mut App) { - EditorSettings::register(cx); -} - -pub fn init(cx: &mut App) { - init_settings(cx); - - cx.set_global(GlobalBlameRenderer(Arc::new(()))); - - workspace::register_project_item::(cx); - workspace::FollowableViewRegistry::register::(cx); - workspace::register_serializable_item::(cx); - - cx.observe_new( - |workspace: &mut Workspace, _: Option<&mut Window>, _cx: &mut Context| { - workspace.register_action(Editor::new_file); - workspace.register_action(Editor::new_file_vertical); - workspace.register_action(Editor::new_file_horizontal); - workspace.register_action(Editor::cancel_language_server_work); - }, - ) - .detach(); - - cx.on_action(move |_: &workspace::NewFile, cx| { - let app_state = workspace::AppState::global(cx); - if let Some(app_state) = app_state.upgrade() { - workspace::open_new( - Default::default(), - app_state, - cx, - |workspace, window, cx| { - Editor::new_file(workspace, &Default::default(), window, cx) - }, - ) - .detach(); - } - }); - cx.on_action(move |_: &workspace::NewWindow, cx| { - let app_state = workspace::AppState::global(cx); - if let Some(app_state) = app_state.upgrade() { - workspace::open_new( - Default::default(), - app_state, - cx, - |workspace, window, cx| { - cx.activate(true); - Editor::new_file(workspace, &Default::default(), window, cx) - }, - ) - .detach(); - } - }); -} - -pub fn set_blame_renderer(renderer: impl BlameRenderer + 'static, cx: &mut App) { - cx.set_global(GlobalBlameRenderer(Arc::new(renderer))); -} - -pub trait DiagnosticRenderer { - fn render_group( - &self, - diagnostic_group: Vec>, - buffer_id: BufferId, - snapshot: EditorSnapshot, - editor: WeakEntity, - cx: &mut App, - ) -> Vec>; - - fn render_hover( - &self, - diagnostic_group: Vec>, - range: Range, - buffer_id: BufferId, - cx: &mut App, - ) -> Option>; - - fn open_link( - &self, - editor: &mut Editor, - link: SharedString, - window: &mut Window, - cx: &mut Context, - ); -} - -pub(crate) struct GlobalDiagnosticRenderer(pub Arc); - -impl GlobalDiagnosticRenderer { - fn global(cx: &App) -> Option> { - cx.try_global::().map(|g| g.0.clone()) - } -} - -impl gpui::Global for GlobalDiagnosticRenderer {} -pub fn set_diagnostic_renderer(renderer: impl DiagnosticRenderer + 'static, cx: &mut App) { - cx.set_global(GlobalDiagnosticRenderer(Arc::new(renderer))); -} - -pub struct SearchWithinRange; - -trait InvalidationRegion { - fn ranges(&self) -> &[Range]; -} - -#[derive(Clone, Debug, PartialEq)] -pub enum SelectPhase { - Begin { - position: DisplayPoint, - add: bool, - click_count: usize, - }, - BeginColumnar { - position: DisplayPoint, - reset: bool, - goal_column: u32, - }, - Extend { - position: DisplayPoint, - click_count: usize, - }, - Update { - position: DisplayPoint, - goal_column: u32, - scroll_delta: gpui::Point, - }, - End, -} - -#[derive(Clone, Debug)] -pub enum SelectMode { - Character, - Word(Range), - Line(Range), - All, -} - -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum EditorMode { - SingleLine { - auto_width: bool, - }, - AutoHeight { - max_lines: usize, - }, - Full { - /// When set to `true`, the editor will scale its UI elements with the buffer font size. - scale_ui_elements_with_buffer_font_size: bool, - /// When set to `true`, the editor will render a background for the active line. - show_active_line_background: bool, - /// When set to `true`, the editor's height will be determined by its content. - sized_by_content: bool, - }, -} - -impl EditorMode { - pub fn full() -> Self { - Self::Full { - scale_ui_elements_with_buffer_font_size: true, - show_active_line_background: true, - sized_by_content: false, - } - } - - pub fn is_full(&self) -> bool { - matches!(self, Self::Full { .. }) - } -} - -#[derive(Copy, Clone, Debug)] -pub enum SoftWrap { - /// Prefer not to wrap at all. - /// - /// Note: this is currently internal, as actually limited by [`crate::MAX_LINE_LEN`] until it wraps. - /// The mode is used inside git diff hunks, where it's seems currently more useful to not wrap as much as possible. - GitDiff, - /// Prefer a single line generally, unless an overly long line is encountered. - None, - /// Soft wrap lines that exceed the editor width. - EditorWidth, - /// Soft wrap lines at the preferred line length. - Column(u32), - /// Soft wrap line at the preferred line length or the editor width (whichever is smaller). - Bounded(u32), -} - -#[derive(Clone)] -pub struct EditorStyle { - pub background: Hsla, - pub local_player: PlayerColor, - pub text: TextStyle, - pub scrollbar_width: Pixels, - pub syntax: Arc, - pub status: StatusColors, - pub inlay_hints_style: HighlightStyle, - pub inline_completion_styles: InlineCompletionStyles, - pub unnecessary_code_fade: f32, -} - -impl Default for EditorStyle { - fn default() -> Self { - Self { - background: Hsla::default(), - local_player: PlayerColor::default(), - text: TextStyle::default(), - scrollbar_width: Pixels::default(), - syntax: Default::default(), - // HACK: Status colors don't have a real default. - // We should look into removing the status colors from the editor - // style and retrieve them directly from the theme. - status: StatusColors::dark(), - inlay_hints_style: HighlightStyle::default(), - inline_completion_styles: InlineCompletionStyles { - insertion: HighlightStyle::default(), - whitespace: HighlightStyle::default(), - }, - unnecessary_code_fade: Default::default(), - } - } -} - -pub fn make_inlay_hints_style(cx: &mut App) -> HighlightStyle { - let show_background = language_settings::language_settings(None, None, cx) - .inlay_hints - .show_background; - - HighlightStyle { - color: Some(cx.theme().status().hint), - background_color: show_background.then(|| cx.theme().status().hint_background), - ..HighlightStyle::default() - } -} - -pub fn make_suggestion_styles(cx: &mut App) -> InlineCompletionStyles { - InlineCompletionStyles { - insertion: HighlightStyle { - color: Some(cx.theme().status().predictive), - ..HighlightStyle::default() - }, - whitespace: HighlightStyle { - background_color: Some(cx.theme().status().created_background), - ..HighlightStyle::default() - }, - } -} - -type CompletionId = usize; - -pub(crate) enum EditDisplayMode { - TabAccept, - DiffPopover, - Inline, -} - -enum InlineCompletion { - Edit { - edits: Vec<(Range, String)>, - edit_preview: Option, - display_mode: EditDisplayMode, - snapshot: BufferSnapshot, - }, - Move { - target: Anchor, - snapshot: BufferSnapshot, - }, -} - -struct InlineCompletionState { - inlay_ids: Vec, - completion: InlineCompletion, - completion_id: Option, - invalidation_range: Range, -} - -enum EditPredictionSettings { - Disabled, - Enabled { - show_in_menu: bool, - preview_requires_modifier: bool, - }, -} - -enum InlineCompletionHighlight {} - -#[derive(Debug, Clone)] -struct InlineDiagnostic { - message: SharedString, - group_id: usize, - is_primary: bool, - start: Point, - severity: DiagnosticSeverity, -} - -pub enum MenuInlineCompletionsPolicy { - Never, - ByProvider, -} - -pub enum EditPredictionPreview { - /// Modifier is not pressed - Inactive { released_too_fast: bool }, - /// Modifier pressed - Active { - since: Instant, - previous_scroll_position: Option, - }, -} - -impl EditPredictionPreview { - pub fn released_too_fast(&self) -> bool { - match self { - EditPredictionPreview::Inactive { released_too_fast } => *released_too_fast, - EditPredictionPreview::Active { .. } => false, - } - } - - pub fn set_previous_scroll_position(&mut self, scroll_position: Option) { - if let EditPredictionPreview::Active { - previous_scroll_position, - .. - } = self - { - *previous_scroll_position = scroll_position; - } - } -} - -pub struct ContextMenuOptions { - pub min_entries_visible: usize, - pub max_entries_visible: usize, - pub placement: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ContextMenuPlacement { - Above, - Below, -} - -#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Default)] -struct EditorActionId(usize); - -impl EditorActionId { - pub fn post_inc(&mut self) -> Self { - let answer = self.0; - - *self = Self(answer + 1); - - Self(answer) - } -} - -// type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; -// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; - -type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range]>); -type GutterHighlight = (fn(&App) -> Hsla, Arc<[Range]>); - -#[derive(Default)] -struct ScrollbarMarkerState { - scrollbar_size: Size, - dirty: bool, - markers: Arc<[PaintQuad]>, - pending_refresh: Option>>, -} - -impl ScrollbarMarkerState { - fn should_refresh(&self, scrollbar_size: Size) -> bool { - self.pending_refresh.is_none() && (self.scrollbar_size != scrollbar_size || self.dirty) - } -} - -#[derive(Clone, Debug)] -struct RunnableTasks { - templates: Vec<(TaskSourceKind, TaskTemplate)>, - offset: multi_buffer::Anchor, - // We need the column at which the task context evaluation should take place (when we're spawning it via gutter). - column: u32, - // Values of all named captures, including those starting with '_' - extra_variables: HashMap, - // Full range of the tagged region. We use it to determine which `extra_variables` to grab for context resolution in e.g. a modal. - context_range: Range, -} - -impl RunnableTasks { - fn resolve<'a>( - &'a self, - cx: &'a task::TaskContext, - ) -> impl Iterator + 'a { - self.templates.iter().filter_map(|(kind, template)| { - template - .resolve_task(&kind.to_id_base(), cx) - .map(|task| (kind.clone(), task)) - }) - } -} - -#[derive(Clone)] -struct ResolvedTasks { - templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>, - position: Anchor, -} - -#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] -struct BufferOffset(usize); - -// Addons allow storing per-editor state in other crates (e.g. Vim) -pub trait Addon: 'static { - fn extend_key_context(&self, _: &mut KeyContext, _: &App) {} - - fn render_buffer_header_controls( - &self, - _: &ExcerptInfo, - _: &Window, - _: &App, - ) -> Option { - None - } - - fn to_any(&self) -> &dyn std::any::Any; - - fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> { - None - } -} - -/// A set of caret positions, registered when the editor was edited. -pub struct ChangeList { - changes: Vec>, - /// Currently "selected" change. - position: Option, -} - -impl ChangeList { - pub fn new() -> Self { - Self { - changes: Vec::new(), - position: None, - } - } - - /// Moves to the next change in the list (based on the direction given) and returns the caret positions for the next change. - /// If reaches the end of the list in the direction, returns the corresponding change until called for a different direction. - pub fn next_change(&mut self, count: usize, direction: Direction) -> Option<&[Anchor]> { - if self.changes.is_empty() { - return None; - } - - let prev = self.position.unwrap_or(self.changes.len()); - let next = if direction == Direction::Prev { - prev.saturating_sub(count) - } else { - (prev + count).min(self.changes.len() - 1) - }; - self.position = Some(next); - self.changes.get(next).map(|anchors| anchors.as_slice()) - } - - /// Adds a new change to the list, resetting the change list position. - pub fn push_to_change_list(&mut self, pop_state: bool, new_positions: Vec) { - self.position.take(); - if pop_state { - self.changes.pop(); - } - self.changes.push(new_positions.clone()); - } - - pub fn last(&self) -> Option<&[Anchor]> { - self.changes.last().map(|anchors| anchors.as_slice()) - } -} - -#[derive(Clone)] -struct InlineBlamePopoverState { - scroll_handle: ScrollHandle, - commit_message: Option, - markdown: Entity, -} - -struct InlineBlamePopover { - position: gpui::Point, - show_task: Option>, - hide_task: Option>, - popover_bounds: Option>, - popover_state: InlineBlamePopoverState, -} - -/// Represents a breakpoint indicator that shows up when hovering over lines in the gutter that don't have -/// a breakpoint on them. -#[derive(Clone, Copy, Debug)] -struct PhantomBreakpointIndicator { - display_row: DisplayRow, - /// There's a small debounce between hovering over the line and showing the indicator. - /// We don't want to show the indicator when moving the mouse from editor to e.g. project panel. - is_active: bool, - collides_with_existing_breakpoint: bool, -} -/// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`]. -/// -/// See the [module level documentation](self) for more information. -pub struct Editor { - focus_handle: FocusHandle, - last_focused_descendant: Option, - /// The text buffer being edited - buffer: Entity, - /// Map of how text in the buffer should be displayed. - /// Handles soft wraps, folds, fake inlay text insertions, etc. - pub display_map: Entity, - pub selections: SelectionsCollection, - pub scroll_manager: ScrollManager, - /// When inline assist editors are linked, they all render cursors because - /// typing enters text into each of them, even the ones that aren't focused. - pub(crate) show_cursor_when_unfocused: bool, - columnar_selection_tail: Option, - add_selections_state: Option, - select_next_state: Option, - select_prev_state: Option, - selection_history: SelectionHistory, - autoclose_regions: Vec, - snippet_stack: InvalidationStack, - select_syntax_node_history: SelectSyntaxNodeHistory, - ime_transaction: Option, - active_diagnostics: ActiveDiagnostic, - show_inline_diagnostics: bool, - inline_diagnostics_update: Task<()>, - inline_diagnostics_enabled: bool, - inline_diagnostics: Vec<(Anchor, InlineDiagnostic)>, - soft_wrap_mode_override: Option, - hard_wrap: Option, - - // TODO: make this a access method - pub project: Option>, - semantics_provider: Option>, - completion_provider: Option>, - collaboration_hub: Option>, - blink_manager: Entity, - show_cursor_names: bool, - hovered_cursors: HashMap>, - pub show_local_selections: bool, - mode: EditorMode, - show_breadcrumbs: bool, - show_gutter: bool, - show_scrollbars: bool, - disable_scrolling: bool, - disable_expand_excerpt_buttons: bool, - show_line_numbers: Option, - use_relative_line_numbers: Option, - show_git_diff_gutter: Option, - show_code_actions: Option, - show_runnables: Option, - show_breakpoints: Option, - show_wrap_guides: Option, - show_indent_guides: Option, - placeholder_text: Option>, - highlight_order: usize, - highlighted_rows: HashMap>, - background_highlights: TreeMap, - gutter_highlights: TreeMap, - scrollbar_marker_state: ScrollbarMarkerState, - active_indent_guides_state: ActiveIndentGuidesState, - nav_history: Option, - context_menu: RefCell>, - context_menu_options: Option, - mouse_context_menu: Option, - completion_tasks: Vec<(CompletionId, Task>)>, - inline_blame_popover: Option, - signature_help_state: SignatureHelpState, - auto_signature_help: Option, - find_all_references_task_sources: Vec, - next_completion_id: CompletionId, - available_code_actions: Option<(Location, Rc<[AvailableCodeAction]>)>, - code_actions_task: Option>>, - quick_selection_highlight_task: Option<(Range, Task<()>)>, - debounced_selection_highlight_task: Option<(Range, Task<()>)>, - document_highlights_task: Option>, - linked_editing_range_task: Option>>, - linked_edit_ranges: linked_editing_ranges::LinkedEditingRanges, - pending_rename: Option, - searchable: bool, - cursor_shape: CursorShape, - current_line_highlight: Option, - collapse_matches: bool, - autoindent_mode: Option, - workspace: Option<(WeakEntity, Option)>, - input_enabled: bool, - use_modal_editing: bool, - read_only: bool, - leader_peer_id: Option, - remote_id: Option, - pub hover_state: HoverState, - pending_mouse_down: Option>>>, - gutter_hovered: bool, - hovered_link_state: Option, - edit_prediction_provider: Option, - code_action_providers: Vec>, - active_inline_completion: Option, - /// Used to prevent flickering as the user types while the menu is open - stale_inline_completion_in_menu: Option, - edit_prediction_settings: EditPredictionSettings, - 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, - inlay_hint_cache: InlayHintCache, - next_inlay_id: usize, - _subscriptions: Vec, - pixel_position_of_newest_cursor: Option>, - gutter_dimensions: GutterDimensions, - style: Option, - text_style_refinement: Option, - next_editor_action_id: EditorActionId, - editor_actions: - Rc)>>>>, - use_autoclose: bool, - use_auto_surround: bool, - auto_replace_emoji_shortcode: bool, - jsx_tag_auto_close_enabled_in_any_buffer: bool, - show_git_blame_gutter: bool, - show_git_blame_inline: bool, - show_git_blame_inline_delay_task: Option>, - git_blame_inline_enabled: bool, - render_diff_hunk_controls: RenderDiffHunkControlsFn, - serialize_dirty_buffers: bool, - show_selection_menu: Option, - blame: Option>, - blame_subscription: Option, - custom_context_menu: Option< - Box< - dyn 'static - + Fn( - &mut Self, - DisplayPoint, - &mut Window, - &mut Context, - ) -> Option>, - >, - >, - last_bounds: Option>, - last_position_map: Option>, - expect_bounds_change: Option>, - tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, - tasks_update_task: Option>, - breakpoint_store: Option>, - gutter_breakpoint_indicator: (Option, Option>), - in_project_search: bool, - previous_search_ranges: Option]>>, - breadcrumb_header: Option, - focused_block: Option, - next_scroll_position: NextScrollCursorCenterTopBottom, - addons: HashMap>, - registered_buffers: HashMap, - load_diff_task: Option>>, - selection_mark_mode: bool, - toggle_fold_multiple_buffers: Task<()>, - _scroll_cursor_center_top_bottom_task: Task<()>, - serialize_selections: Task<()>, - serialize_folds: Task<()>, - mouse_cursor_hidden: bool, - hide_mouse_mode: HideMouseMode, - pub change_list: ChangeList, - inline_value_cache: InlineValueCache, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] -enum NextScrollCursorCenterTopBottom { - #[default] - Center, - Top, - Bottom, -} - -impl NextScrollCursorCenterTopBottom { - fn next(&self) -> Self { - match self { - Self::Center => Self::Top, - Self::Top => Self::Bottom, - Self::Bottom => Self::Center, - } - } -} - -#[derive(Clone)] -pub struct EditorSnapshot { - pub mode: EditorMode, - show_gutter: bool, - show_line_numbers: Option, - show_git_diff_gutter: Option, - show_code_actions: Option, - show_runnables: Option, - show_breakpoints: Option, - git_blame_gutter_max_author_length: Option, - pub display_snapshot: DisplaySnapshot, - pub placeholder_text: Option>, - is_focused: bool, - scroll_anchor: ScrollAnchor, - ongoing_scroll: OngoingScroll, - current_line_highlight: CurrentLineHighlight, - gutter_hovered: bool, -} - -#[derive(Default, Debug, Clone, Copy)] -pub struct GutterDimensions { - pub left_padding: Pixels, - pub right_padding: Pixels, - pub width: Pixels, - pub margin: Pixels, - pub git_blame_entries_width: Option, -} - -impl GutterDimensions { - /// The full width of the space taken up by the gutter. - pub fn full_width(&self) -> Pixels { - self.margin + self.width - } - - /// The width of the space reserved for the fold indicators, - /// use alongside 'justify_end' and `gutter_width` to - /// right align content with the line numbers - pub fn fold_area_width(&self) -> Pixels { - self.margin + self.right_padding - } -} - -#[derive(Debug)] -pub struct RemoteSelection { - pub replica_id: ReplicaId, - pub selection: Selection, - pub cursor_shape: CursorShape, - pub peer_id: PeerId, - pub line_mode: bool, - pub participant_index: Option, - pub user_name: Option, -} - -#[derive(Clone, Debug)] -struct SelectionHistoryEntry { - selections: Arc<[Selection]>, - select_next_state: Option, - select_prev_state: Option, - add_selections_state: Option, -} - -enum SelectionHistoryMode { - Normal, - Undoing, - Redoing, -} - -#[derive(Clone, PartialEq, Eq, Hash)] -struct HoveredCursor { - replica_id: u16, - selection_id: usize, -} - -impl Default for SelectionHistoryMode { - fn default() -> Self { - Self::Normal - } -} - -#[derive(Default)] -struct SelectionHistory { - #[allow(clippy::type_complexity)] - selections_by_transaction: - HashMap]>, Option]>>)>, - mode: SelectionHistoryMode, - undo_stack: VecDeque, - redo_stack: VecDeque, -} - -impl SelectionHistory { - fn insert_transaction( - &mut self, - transaction_id: TransactionId, - selections: Arc<[Selection]>, - ) { - self.selections_by_transaction - .insert(transaction_id, (selections, None)); - } - - #[allow(clippy::type_complexity)] - fn transaction( - &self, - transaction_id: TransactionId, - ) -> Option<&(Arc<[Selection]>, Option]>>)> { - self.selections_by_transaction.get(&transaction_id) - } - - #[allow(clippy::type_complexity)] - fn transaction_mut( - &mut self, - transaction_id: TransactionId, - ) -> Option<&mut (Arc<[Selection]>, Option]>>)> { - self.selections_by_transaction.get_mut(&transaction_id) - } - - fn push(&mut self, entry: SelectionHistoryEntry) { - if !entry.selections.is_empty() { - match self.mode { - SelectionHistoryMode::Normal => { - self.push_undo(entry); - self.redo_stack.clear(); - } - SelectionHistoryMode::Undoing => self.push_redo(entry), - SelectionHistoryMode::Redoing => self.push_undo(entry), - } - } - } - - fn push_undo(&mut self, entry: SelectionHistoryEntry) { - if self - .undo_stack - .back() - .map_or(true, |e| e.selections != entry.selections) - { - self.undo_stack.push_back(entry); - if self.undo_stack.len() > MAX_SELECTION_HISTORY_LEN { - self.undo_stack.pop_front(); - } - } - } - - fn push_redo(&mut self, entry: SelectionHistoryEntry) { - if self - .redo_stack - .back() - .map_or(true, |e| e.selections != entry.selections) - { - self.redo_stack.push_back(entry); - if self.redo_stack.len() > MAX_SELECTION_HISTORY_LEN { - self.redo_stack.pop_front(); - } - } - } -} - -#[derive(Clone, Copy)] -pub struct RowHighlightOptions { - pub autoscroll: bool, - pub include_gutter: bool, -} - -impl Default for RowHighlightOptions { - fn default() -> Self { - Self { - autoscroll: Default::default(), - include_gutter: true, - } - } -} - -struct RowHighlight { - index: usize, - range: Range, - color: Hsla, - options: RowHighlightOptions, - type_id: TypeId, -} - -#[derive(Clone, Debug)] -struct AddSelectionsState { - above: bool, - stack: Vec, -} - -#[derive(Clone)] -struct SelectNextState { - query: AhoCorasick, - wordwise: bool, - done: bool, -} - -impl std::fmt::Debug for SelectNextState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct(std::any::type_name::()) - .field("wordwise", &self.wordwise) - .field("done", &self.done) - .finish() - } -} - -#[derive(Debug)] -struct AutocloseRegion { - selection_id: usize, - range: Range, - pair: BracketPair, -} - -#[derive(Debug)] -struct SnippetState { - ranges: Vec>>, - active_index: usize, - choices: Vec>>, -} - -#[doc(hidden)] -pub struct RenameState { - pub range: Range, - pub old_name: Arc, - pub editor: Entity, - block_id: CustomBlockId, -} - -struct InvalidationStack(Vec); - -struct RegisteredInlineCompletionProvider { - provider: Arc, - _subscription: Subscription, -} - -#[derive(Debug, PartialEq, Eq)] -pub struct ActiveDiagnosticGroup { - pub active_range: Range, - pub active_message: String, - pub group_id: usize, - pub blocks: HashSet, -} - -#[derive(Debug, PartialEq, Eq)] - -pub(crate) enum ActiveDiagnostic { - None, - All, - Group(ActiveDiagnosticGroup), -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct ClipboardSelection { - /// The number of bytes in this selection. - pub len: usize, - /// Whether this was a full-line selection. - pub is_entire_line: bool, - /// The indentation of the first line when this content was originally copied. - pub first_line_indent: u32, -} - -// selections, scroll behavior, was newest selection reversed -type SelectSyntaxNodeHistoryState = ( - Box<[Selection]>, - SelectSyntaxNodeScrollBehavior, - bool, -); - -#[derive(Default)] -struct SelectSyntaxNodeHistory { - stack: Vec, - // disable temporarily to allow changing selections without losing the stack - pub disable_clearing: bool, -} - -impl SelectSyntaxNodeHistory { - pub fn try_clear(&mut self) { - if !self.disable_clearing { - self.stack.clear(); - } - } - - pub fn push(&mut self, selection: SelectSyntaxNodeHistoryState) { - self.stack.push(selection); - } - - pub fn pop(&mut self) -> Option { - self.stack.pop() - } -} - -enum SelectSyntaxNodeScrollBehavior { - CursorTop, - FitSelection, - CursorBottom, -} - -#[derive(Debug)] -pub(crate) struct NavigationData { - cursor_anchor: Anchor, - cursor_position: Point, - scroll_anchor: ScrollAnchor, - scroll_top_row: u32, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum GotoDefinitionKind { - Symbol, - Declaration, - Type, - Implementation, -} - -#[derive(Debug, Clone)] -enum InlayHintRefreshReason { - ModifiersChanged(bool), - Toggle(bool), - SettingsChange(InlayHintSettings), - NewLinesShown, - BufferEdited(HashSet>), - RefreshRequested, - ExcerptsRemoved(Vec), -} - -impl InlayHintRefreshReason { - fn description(&self) -> &'static str { - match self { - Self::ModifiersChanged(_) => "modifiers changed", - Self::Toggle(_) => "toggle", - Self::SettingsChange(_) => "settings change", - Self::NewLinesShown => "new lines shown", - Self::BufferEdited(_) => "buffer edited", - Self::RefreshRequested => "refresh requested", - Self::ExcerptsRemoved(_) => "excerpts removed", - } - } -} - -pub enum FormatTarget { - Buffers, - Ranges(Vec>), -} - -pub(crate) struct FocusedBlock { - id: BlockId, - focus_handle: WeakFocusHandle, -} - -#[derive(Clone)] -enum JumpData { - MultiBufferRow { - row: MultiBufferRow, - line_offset_from_top: u32, - }, - MultiBufferPoint { - excerpt_id: ExcerptId, - position: Point, - anchor: text::Anchor, - line_offset_from_top: u32, - }, -} - -pub enum MultibufferSelectionMode { - First, - All, -} - -#[derive(Clone, Copy, Debug, Default)] -pub struct RewrapOptions { - pub override_language_settings: bool, - pub preserve_existing_whitespace: bool, -} - -impl Editor { - pub fn single_line(window: &mut Window, cx: &mut Context) -> Self { - let buffer = cx.new(|cx| Buffer::local("", cx)); - let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new( - EditorMode::SingleLine { auto_width: false }, - buffer, - None, - window, - cx, - ) - } - - pub fn multi_line(window: &mut Window, cx: &mut Context) -> Self { - let buffer = cx.new(|cx| Buffer::local("", cx)); - let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new(EditorMode::full(), buffer, None, window, cx) - } - - pub fn auto_width(window: &mut Window, cx: &mut Context) -> Self { - let buffer = cx.new(|cx| Buffer::local("", cx)); - let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new( - EditorMode::SingleLine { auto_width: true }, - buffer, - None, - window, - cx, - ) - } - - pub fn auto_height(max_lines: usize, window: &mut Window, cx: &mut Context) -> Self { - let buffer = cx.new(|cx| Buffer::local("", cx)); - let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new( - EditorMode::AutoHeight { max_lines }, - buffer, - None, - window, - cx, - ) - } - - pub fn for_buffer( - buffer: Entity, - project: Option>, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new(EditorMode::full(), buffer, project, window, cx) - } - - pub fn for_multibuffer( - buffer: Entity, - project: Option>, - window: &mut Window, - cx: &mut Context, - ) -> Self { - Self::new(EditorMode::full(), buffer, project, window, cx) - } - - pub fn clone(&self, window: &mut Window, cx: &mut Context) -> Self { - let mut clone = Self::new( - self.mode, - self.buffer.clone(), - self.project.clone(), - window, - cx, - ); - self.display_map.update(cx, |display_map, cx| { - let snapshot = display_map.snapshot(cx); - clone.display_map.update(cx, |display_map, cx| { - display_map.set_state(&snapshot, cx); - }); - }); - clone.folds_did_change(cx); - clone.selections.clone_state(&self.selections); - clone.scroll_manager.clone_state(&self.scroll_manager); - clone.searchable = self.searchable; - clone.read_only = self.read_only; - clone - } - - pub fn new( - mode: EditorMode, - buffer: Entity, - project: Option>, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let style = window.text_style(); - let font_size = style.font_size.to_pixels(window.rem_size()); - let editor = cx.entity().downgrade(); - let fold_placeholder = FoldPlaceholder { - constrain_width: true, - render: Arc::new(move |fold_id, fold_range, cx| { - let editor = editor.clone(); - div() - .id(fold_id) - .bg(cx.theme().colors().ghost_element_background) - .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) - .active(|style| style.bg(cx.theme().colors().ghost_element_active)) - .rounded_xs() - .size_full() - .cursor_pointer() - .child("⋯") - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .on_click(move |_, _window, cx| { - editor - .update(cx, |editor, cx| { - editor.unfold_ranges( - &[fold_range.start..fold_range.end], - true, - false, - cx, - ); - cx.stop_propagation(); - }) - .ok(); - }) - .into_any() - }), - merge_adjacent: true, - ..Default::default() - }; - let display_map = cx.new(|cx| { - DisplayMap::new( - buffer.clone(), - style.font(), - font_size, - None, - FILE_HEADER_HEIGHT, - MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, - fold_placeholder, - cx, - ) - }); - - let selections = SelectionsCollection::new(display_map.clone(), buffer.clone()); - - let blink_manager = cx.new(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); - - let soft_wrap_mode_override = matches!(mode, EditorMode::SingleLine { .. }) - .then(|| language_settings::SoftWrap::None); - - let mut project_subscriptions = Vec::new(); - if mode.is_full() { - if let Some(project) = project.as_ref() { - project_subscriptions.push(cx.subscribe_in( - project, - window, - |editor, _, event, window, cx| match event { - project::Event::RefreshCodeLens => { - // we always query lens with actions, without storing them, always refreshing them - } - project::Event::RefreshInlayHints => { - editor - .refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); - } - project::Event::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(); - } - } - } - } - _ => {} - }, - )); - 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 buffer_snapshot = buffer.read(cx).snapshot(cx); - - let inlay_hint_settings = - inlay_hint_settings(selections.newest_anchor().head(), &buffer_snapshot, cx); - let focus_handle = cx.focus_handle(); - cx.on_focus(&focus_handle, window, Self::handle_focus) - .detach(); - cx.on_focus_in(&focus_handle, window, Self::handle_focus_in) - .detach(); - cx.on_focus_out(&focus_handle, window, Self::handle_focus_out) - .detach(); - cx.on_blur(&focus_handle, window, Self::handle_blur) - .detach(); - - let show_indent_guides = if matches!(mode, EditorMode::SingleLine { .. }) { - Some(false) - } else { - None - }; - - let breakpoint_store = match (mode, project.as_ref()) { - (EditorMode::Full { .. }, Some(project)) => Some(project.read(cx).breakpoint_store()), - _ => None, - }; - - let mut code_action_providers = Vec::new(); - let mut load_uncommitted_diff = None; - if let Some(project) = project.clone() { - load_uncommitted_diff = Some( - get_uncommitted_diff_for_buffer( - &project, - buffer.read(cx).all_buffers(), - buffer.clone(), - cx, - ) - .shared(), - ); - code_action_providers.push(Rc::new(project) as Rc<_>); - } - - let mut this = Self { - focus_handle, - show_cursor_when_unfocused: false, - last_focused_descendant: None, - buffer: buffer.clone(), - display_map: display_map.clone(), - selections, - scroll_manager: ScrollManager::new(cx), - columnar_selection_tail: None, - add_selections_state: None, - select_next_state: None, - select_prev_state: None, - selection_history: Default::default(), - autoclose_regions: Default::default(), - snippet_stack: Default::default(), - select_syntax_node_history: SelectSyntaxNodeHistory::default(), - ime_transaction: Default::default(), - active_diagnostics: ActiveDiagnostic::None, - show_inline_diagnostics: ProjectSettings::get_global(cx).diagnostics.inline.enabled, - inline_diagnostics_update: Task::ready(()), - inline_diagnostics: Vec::new(), - soft_wrap_mode_override, - hard_wrap: None, - completion_provider: project.clone().map(|project| Box::new(project) as _), - semantics_provider: project.clone().map(|project| Rc::new(project) as _), - collaboration_hub: project.clone().map(|project| Box::new(project) as _), - project, - blink_manager: blink_manager.clone(), - show_local_selections: true, - show_scrollbars: true, - disable_scrolling: false, - mode, - show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs, - show_gutter: mode.is_full(), - show_line_numbers: None, - use_relative_line_numbers: None, - disable_expand_excerpt_buttons: false, - show_git_diff_gutter: None, - show_code_actions: None, - show_runnables: None, - show_breakpoints: None, - show_wrap_guides: None, - show_indent_guides, - placeholder_text: None, - highlight_order: 0, - highlighted_rows: HashMap::default(), - background_highlights: Default::default(), - gutter_highlights: TreeMap::default(), - scrollbar_marker_state: ScrollbarMarkerState::default(), - active_indent_guides_state: ActiveIndentGuidesState::default(), - nav_history: None, - context_menu: RefCell::new(None), - context_menu_options: None, - mouse_context_menu: None, - completion_tasks: Default::default(), - inline_blame_popover: Default::default(), - signature_help_state: SignatureHelpState::default(), - auto_signature_help: None, - find_all_references_task_sources: Vec::new(), - next_completion_id: 0, - next_inlay_id: 0, - code_action_providers, - available_code_actions: Default::default(), - code_actions_task: Default::default(), - quick_selection_highlight_task: Default::default(), - debounced_selection_highlight_task: Default::default(), - document_highlights_task: Default::default(), - linked_editing_range_task: Default::default(), - pending_rename: Default::default(), - searchable: true, - cursor_shape: EditorSettings::get_global(cx) - .cursor_shape - .unwrap_or_default(), - current_line_highlight: None, - autoindent_mode: Some(AutoindentMode::EachLine), - collapse_matches: false, - workspace: None, - input_enabled: true, - use_modal_editing: mode.is_full(), - read_only: false, - use_autoclose: true, - use_auto_surround: true, - auto_replace_emoji_shortcode: false, - jsx_tag_auto_close_enabled_in_any_buffer: false, - leader_peer_id: None, - remote_id: None, - hover_state: Default::default(), - pending_mouse_down: None, - hovered_link_state: Default::default(), - edit_prediction_provider: None, - active_inline_completion: None, - stale_inline_completion_in_menu: None, - edit_prediction_preview: EditPredictionPreview::Inactive { - released_too_fast: false, - }, - inline_diagnostics_enabled: mode.is_full(), - inline_value_cache: InlineValueCache::new(inlay_hint_settings.show_value_hints), - inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), - - gutter_hovered: false, - pixel_position_of_newest_cursor: None, - last_bounds: None, - last_position_map: None, - expect_bounds_change: None, - gutter_dimensions: GutterDimensions::default(), - style: None, - show_cursor_names: false, - hovered_cursors: Default::default(), - next_editor_action_id: EditorActionId::default(), - editor_actions: Rc::default(), - 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, - custom_context_menu: None, - show_git_blame_gutter: false, - show_git_blame_inline: false, - show_selection_menu: None, - show_git_blame_inline_delay_task: None, - git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(), - render_diff_hunk_controls: Arc::new(render_diff_hunk_controls), - serialize_dirty_buffers: ProjectSettings::get_global(cx) - .session - .restore_unsaved_buffers, - blame: None, - blame_subscription: None, - tasks: Default::default(), - - breakpoint_store, - gutter_breakpoint_indicator: (None, None), - _subscriptions: vec![ - cx.observe(&buffer, Self::on_buffer_changed), - cx.subscribe_in(&buffer, window, Self::on_buffer_event), - cx.observe_in(&display_map, window, Self::on_display_map_changed), - cx.observe(&blink_manager, |_, _, cx| cx.notify()), - cx.observe_global_in::(window, Self::settings_changed), - observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()), - cx.observe_window_activation(window, |editor, window, cx| { - let active = window.is_window_active(); - editor.blink_manager.update(cx, |blink_manager, cx| { - if active { - blink_manager.enable(cx); - } else { - blink_manager.disable(cx); - } - }); - }), - ], - tasks_update_task: None, - linked_edit_ranges: Default::default(), - in_project_search: false, - previous_search_ranges: None, - breadcrumb_header: None, - focused_block: None, - next_scroll_position: NextScrollCursorCenterTopBottom::default(), - addons: HashMap::default(), - registered_buffers: HashMap::default(), - _scroll_cursor_center_top_bottom_task: Task::ready(()), - selection_mark_mode: false, - toggle_fold_multiple_buffers: Task::ready(()), - serialize_selections: Task::ready(()), - serialize_folds: Task::ready(()), - text_style_refinement: None, - load_diff_task: load_uncommitted_diff, - mouse_cursor_hidden: false, - hide_mouse_mode: EditorSettings::get_global(cx) - .hide_mouse - .unwrap_or_default(), - change_list: ChangeList::new(), - }; - if let Some(breakpoints) = this.breakpoint_store.as_ref() { - this._subscriptions - .push(cx.observe(breakpoints, |_, _, cx| { - cx.notify(); - })); - } - this.tasks_update_task = Some(this.refresh_runnables(window, cx)); - this._subscriptions.extend(project_subscriptions); - - this._subscriptions.push(cx.subscribe_in( - &cx.entity(), - window, - |editor, _, e: &EditorEvent, window, cx| match e { - EditorEvent::ScrollPositionChanged { local, .. } => { - if *local { - let new_anchor = editor.scroll_manager.anchor(); - let snapshot = editor.snapshot(window, cx); - editor.update_restoration_data(cx, move |data| { - data.scroll_position = ( - new_anchor.top_row(&snapshot.buffer_snapshot), - new_anchor.offset, - ); - }); - editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape); - editor.inline_blame_popover.take(); - } - } - EditorEvent::Edited { .. } => { - if !vim_enabled(cx) { - let (map, selections) = editor.selections.all_adjusted_display(cx); - let pop_state = editor - .change_list - .last() - .map(|previous| { - previous.len() == selections.len() - && previous.iter().enumerate().all(|(ix, p)| { - p.to_display_point(&map).row() - == selections[ix].head().row() - }) - }) - .unwrap_or(false); - let new_positions = selections - .into_iter() - .map(|s| map.display_point_to_anchor(s.head(), Bias::Left)) - .collect(); - editor - .change_list - .push_to_change_list(pop_state, new_positions); - } - } - _ => (), - }, - )); - - if let Some(dap_store) = this - .project - .as_ref() - .map(|project| project.read(cx).dap_store()) - { - let weak_editor = cx.weak_entity(); - - this._subscriptions - .push( - cx.observe_new::(move |_, _, cx| { - let session_entity = cx.entity(); - weak_editor - .update(cx, |editor, cx| { - editor._subscriptions.push( - cx.subscribe(&session_entity, Self::on_debug_session_event), - ); - }) - .ok(); - }), - ); - - for session in dap_store.read(cx).sessions().cloned().collect::>() { - this._subscriptions - .push(cx.subscribe(&session, Self::on_debug_session_event)); - } - } - - this.end_selection(window, cx); - this.scroll_manager.show_scrollbars(window, cx); - jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut this, &buffer, cx); - - if mode.is_full() { - let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars(); - cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars)); - - if this.git_blame_inline_enabled { - this.git_blame_inline_enabled = true; - this.start_git_blame_inline(false, window, cx); - } - - this.go_to_active_debug_line(window, cx); - - if let Some(buffer) = buffer.read(cx).as_singleton() { - if let Some(project) = this.project.as_ref() { - let handle = project.update(cx, |project, cx| { - project.register_buffer_with_language_servers(&buffer, cx) - }); - this.registered_buffers - .insert(buffer.read(cx).remote_id(), handle); - } - } - } - - this.report_editor_event("Editor Opened", None, cx); - this - } - - pub fn deploy_mouse_context_menu( - &mut self, - position: gpui::Point, - context_menu: Entity, - window: &mut Window, - cx: &mut Context, - ) { - self.mouse_context_menu = Some(MouseContextMenu::new( - self, - crate::mouse_context_menu::MenuPosition::PinnedToScreen(position), - context_menu, - window, - cx, - )); - } - - pub fn mouse_menu_is_focused(&self, window: &Window, cx: &App) -> bool { - self.mouse_context_menu - .as_ref() - .is_some_and(|menu| menu.context_menu.focus_handle(cx).is_focused(window)) - } - - fn key_context(&self, window: &Window, cx: &App) -> KeyContext { - self.key_context_internal(self.has_active_inline_completion(), window, cx) - } - - fn key_context_internal( - &self, - has_active_edit_prediction: bool, - window: &Window, - cx: &App, - ) -> KeyContext { - let mut key_context = KeyContext::new_with_defaults(); - key_context.add("Editor"); - let mode = match self.mode { - EditorMode::SingleLine { .. } => "single_line", - EditorMode::AutoHeight { .. } => "auto_height", - EditorMode::Full { .. } => "full", - }; - - if EditorSettings::jupyter_enabled(cx) { - key_context.add("jupyter"); - } - - key_context.set("mode", mode); - if self.pending_rename.is_some() { - key_context.add("renaming"); - } - - match self.context_menu.borrow().as_ref() { - Some(CodeContextMenu::Completions(_)) => { - key_context.add("menu"); - key_context.add("showing_completions"); - } - Some(CodeContextMenu::CodeActions(_)) => { - key_context.add("menu"); - key_context.add("showing_code_actions") - } - None => {} - } - - // Disable vim contexts when a sub-editor (e.g. rename/inline assistant) is focused. - if !self.focus_handle(cx).contains_focused(window, cx) - || (self.is_focused(window) || self.mouse_menu_is_focused(window, cx)) - { - for addon in self.addons.values() { - addon.extend_key_context(&mut key_context, cx) - } - } - - if let Some(singleton_buffer) = self.buffer.read(cx).as_singleton() { - if let Some(extension) = singleton_buffer - .read(cx) - .file() - .and_then(|file| file.path().extension()?.to_str()) - { - key_context.set("extension", extension.to_string()); - } - } else { - key_context.add("multibuffer"); - } - - if has_active_edit_prediction { - if self.edit_prediction_in_conflict() { - key_context.add(EDIT_PREDICTION_CONFLICT_KEY_CONTEXT); - } else { - key_context.add(EDIT_PREDICTION_KEY_CONTEXT); - key_context.add("copilot_suggestion"); - } - } - - if self.selection_mark_mode { - key_context.add("selection_mode"); - } - - key_context - } - - pub fn hide_mouse_cursor(&mut self, origin: &HideMouseCursorOrigin) { - self.mouse_cursor_hidden = match origin { - HideMouseCursorOrigin::TypingAction => { - matches!( - self.hide_mouse_mode, - HideMouseMode::OnTyping | HideMouseMode::OnTypingAndMovement - ) - } - HideMouseCursorOrigin::MovementAction => { - matches!(self.hide_mouse_mode, HideMouseMode::OnTypingAndMovement) - } - }; - } - - pub fn edit_prediction_in_conflict(&self) -> bool { - if !self.show_edit_predictions_in_menu() { - return false; - } - - let showing_completions = self - .context_menu - .borrow() - .as_ref() - .map_or(false, |context| { - matches!(context, CodeContextMenu::Completions(_)) - }); - - showing_completions - || self.edit_prediction_requires_modifier() - // Require modifier key when the cursor is on leading whitespace, to allow `tab` - // bindings to insert tab characters. - || (self.edit_prediction_requires_modifier_in_indent_conflict && self.edit_prediction_indent_conflict) - } - - pub fn accept_edit_prediction_keybind( - &self, - window: &Window, - cx: &App, - ) -> AcceptEditPredictionBinding { - let key_context = self.key_context_internal(true, window, cx); - let in_conflict = self.edit_prediction_in_conflict(); - - AcceptEditPredictionBinding( - window - .bindings_for_action_in_context(&AcceptEditPrediction, key_context) - .into_iter() - .filter(|binding| { - !in_conflict - || binding - .keystrokes() - .first() - .map_or(false, |keystroke| keystroke.modifiers.modified()) - }) - .rev() - .min_by_key(|binding| { - binding - .keystrokes() - .first() - .map_or(u8::MAX, |k| k.modifiers.number_of_modifiers()) - }), - ) - } - - pub fn new_file( - workspace: &mut Workspace, - _: &workspace::NewFile, - window: &mut Window, - cx: &mut Context, - ) { - Self::new_in_workspace(workspace, window, cx).detach_and_prompt_err( - "Failed to create buffer", - window, - cx, - |e, _, _| match e.error_code() { - ErrorCode::RemoteUpgradeRequired => Some(format!( - "The remote instance of Zed does not support this yet. It must be upgraded to {}", - e.error_tag("required").unwrap_or("the latest version") - )), - _ => None, - }, - ); - } - - pub fn new_in_workspace( - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let project = workspace.project().clone(); - let create = project.update(cx, |project, cx| project.create_buffer(cx)); - - cx.spawn_in(window, async move |workspace, cx| { - let buffer = create.await?; - workspace.update_in(cx, |workspace, window, cx| { - let editor = - cx.new(|cx| Editor::for_buffer(buffer, Some(project.clone()), window, cx)); - workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); - editor - }) - }) - } - - fn new_file_vertical( - workspace: &mut Workspace, - _: &workspace::NewFileSplitVertical, - window: &mut Window, - cx: &mut Context, - ) { - Self::new_file_in_direction(workspace, SplitDirection::vertical(cx), window, cx) - } - - fn new_file_horizontal( - workspace: &mut Workspace, - _: &workspace::NewFileSplitHorizontal, - window: &mut Window, - cx: &mut Context, - ) { - Self::new_file_in_direction(workspace, SplitDirection::horizontal(cx), window, cx) - } - - fn new_file_in_direction( - workspace: &mut Workspace, - direction: SplitDirection, - window: &mut Window, - cx: &mut Context, - ) { - let project = workspace.project().clone(); - let create = project.update(cx, |project, cx| project.create_buffer(cx)); - - cx.spawn_in(window, async move |workspace, cx| { - let buffer = create.await?; - workspace.update_in(cx, move |workspace, window, cx| { - workspace.split_item( - direction, - Box::new( - cx.new(|cx| Editor::for_buffer(buffer, Some(project.clone()), window, cx)), - ), - window, - cx, - ) - })?; - anyhow::Ok(()) - }) - .detach_and_prompt_err("Failed to create buffer", window, cx, |e, _, _| { - match e.error_code() { - ErrorCode::RemoteUpgradeRequired => Some(format!( - "The remote instance of Zed does not support this yet. It must be upgraded to {}", - e.error_tag("required").unwrap_or("the latest version") - )), - _ => None, - } - }); - } - - pub fn leader_peer_id(&self) -> Option { - self.leader_peer_id - } - - pub fn buffer(&self) -> &Entity { - &self.buffer - } - - pub fn workspace(&self) -> Option> { - self.workspace.as_ref()?.0.upgrade() - } - - pub fn title<'a>(&self, cx: &'a App) -> Cow<'a, str> { - self.buffer().read(cx).title(cx) - } - - pub fn snapshot(&self, window: &mut Window, cx: &mut App) -> EditorSnapshot { - let git_blame_gutter_max_author_length = self - .render_git_blame_gutter(cx) - .then(|| { - if let Some(blame) = self.blame.as_ref() { - let max_author_length = - blame.update(cx, |blame, cx| blame.max_author_length(cx)); - Some(max_author_length) - } else { - None - } - }) - .flatten(); - - EditorSnapshot { - mode: self.mode, - show_gutter: self.show_gutter, - show_line_numbers: self.show_line_numbers, - show_git_diff_gutter: self.show_git_diff_gutter, - show_code_actions: self.show_code_actions, - show_runnables: self.show_runnables, - show_breakpoints: self.show_breakpoints, - git_blame_gutter_max_author_length, - display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)), - scroll_anchor: self.scroll_manager.anchor(), - ongoing_scroll: self.scroll_manager.ongoing_scroll(), - placeholder_text: self.placeholder_text.clone(), - is_focused: self.focus_handle.is_focused(window), - current_line_highlight: self - .current_line_highlight - .unwrap_or_else(|| EditorSettings::get_global(cx).current_line_highlight), - gutter_hovered: self.gutter_hovered, - } - } - - pub fn language_at(&self, point: T, cx: &App) -> Option> { - self.buffer.read(cx).language_at(point, cx) - } - - pub fn file_at(&self, point: T, cx: &App) -> Option> { - self.buffer.read(cx).read(cx).file_at(point).cloned() - } - - pub fn active_excerpt( - &self, - cx: &App, - ) -> Option<(ExcerptId, Entity, Range)> { - self.buffer - .read(cx) - .excerpt_containing(self.selections.newest_anchor().head(), cx) - } - - pub fn mode(&self) -> EditorMode { - self.mode - } - - pub fn set_mode(&mut self, mode: EditorMode) { - self.mode = mode; - } - - pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> { - self.collaboration_hub.as_deref() - } - - pub fn set_collaboration_hub(&mut self, hub: Box) { - self.collaboration_hub = Some(hub); - } - - pub fn set_in_project_search(&mut self, in_project_search: bool) { - self.in_project_search = in_project_search; - } - - pub fn set_custom_context_menu( - &mut self, - f: impl 'static - + Fn( - &mut Self, - DisplayPoint, - &mut Window, - &mut Context, - ) -> Option>, - ) { - self.custom_context_menu = Some(Box::new(f)) - } - - pub fn set_completion_provider(&mut self, provider: Option>) { - self.completion_provider = provider; - } - - pub fn semantics_provider(&self) -> Option> { - self.semantics_provider.clone() - } - - pub fn set_semantics_provider(&mut self, provider: Option>) { - self.semantics_provider = provider; - } - - pub fn set_edit_prediction_provider( - &mut self, - provider: Option>, - window: &mut Window, - cx: &mut Context, - ) where - T: EditPredictionProvider, - { - 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_inline_completion(false, false, window, cx); - } - - pub fn placeholder_text(&self) -> Option<&str> { - self.placeholder_text.as_deref() - } - - pub fn set_placeholder_text( - &mut self, - placeholder_text: impl Into>, - cx: &mut Context, - ) { - let placeholder_text = Some(placeholder_text.into()); - if self.placeholder_text != placeholder_text { - self.placeholder_text = placeholder_text; - cx.notify(); - } - } - - pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut Context) { - self.cursor_shape = cursor_shape; - - // Disrupt blink for immediate user feedback that the cursor shape has changed - self.blink_manager.update(cx, BlinkManager::show_cursor); - - cx.notify(); - } - - pub fn set_current_line_highlight( - &mut self, - current_line_highlight: Option, - ) { - self.current_line_highlight = current_line_highlight; - } - - pub fn set_collapse_matches(&mut self, collapse_matches: bool) { - self.collapse_matches = collapse_matches; - } - - fn register_buffers_with_language_servers(&mut self, cx: &mut Context) { - let buffers = self.buffer.read(cx).all_buffers(); - let Some(project) = self.project.as_ref() else { - return; - }; - project.update(cx, |project, cx| { - for buffer in buffers { - self.registered_buffers - .entry(buffer.read(cx).remote_id()) - .or_insert_with(|| project.register_buffer_with_language_servers(&buffer, cx)); - } - }) - } - - pub fn range_for_match(&self, range: &Range) -> Range { - if self.collapse_matches { - return range.start..range.start; - } - range.clone() - } - - pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut Context) { - if self.display_map.read(cx).clip_at_line_ends != clip { - self.display_map - .update(cx, |map, _| map.clip_at_line_ends = clip); - } - } - - pub fn set_input_enabled(&mut self, input_enabled: bool) { - self.input_enabled = input_enabled; - } - - pub fn set_inline_completions_hidden_for_vim_mode( - &mut self, - hidden: bool, - window: &mut Window, - cx: &mut Context, - ) { - if hidden != self.inline_completions_hidden_for_vim_mode { - self.inline_completions_hidden_for_vim_mode = hidden; - if hidden { - self.update_visible_inline_completion(window, cx); - } else { - self.refresh_inline_completion(true, false, window, cx); - } - } - } - - 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) { - if autoindent { - self.autoindent_mode = Some(AutoindentMode::EachLine); - } else { - self.autoindent_mode = None; - } - } - - pub fn read_only(&self, cx: &App) -> bool { - self.read_only || self.buffer.read(cx).read_only() - } - - pub fn set_read_only(&mut self, read_only: bool) { - self.read_only = read_only; - } - - pub fn set_use_autoclose(&mut self, autoclose: bool) { - self.use_autoclose = autoclose; - } - - pub fn set_use_auto_surround(&mut self, auto_surround: bool) { - self.use_auto_surround = auto_surround; - } - - pub fn set_auto_replace_emoji_shortcode(&mut self, auto_replace: bool) { - self.auto_replace_emoji_shortcode = auto_replace; - } - - pub fn toggle_edit_predictions( - &mut self, - _: &ToggleEditPrediction, - window: &mut Window, - cx: &mut Context, - ) { - 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(); - self.set_show_edit_predictions(Some(show_edit_predictions), window, cx); - } - } - - pub fn set_show_edit_predictions( - &mut self, - show_edit_predictions: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.show_inline_completions_override = show_edit_predictions; - self.update_edit_prediction_settings(cx); - - if let Some(false) = show_edit_predictions { - self.discard_inline_completion(false, cx); - } else { - self.refresh_inline_completion(false, true, window, cx); - } - } - - fn inline_completions_disabled_in_scope( - &self, - buffer: &Entity, - buffer_position: language::Anchor, - cx: &App, - ) -> bool { - let snapshot = buffer.read(cx).snapshot(); - let settings = snapshot.settings_at(buffer_position, cx); - - let Some(scope) = snapshot.language_scope_at(buffer_position) else { - return false; - }; - - scope.override_name().map_or(false, |scope_name| { - settings - .edit_predictions_disabled_in - .iter() - .any(|s| s == scope_name) - }) - } - - pub fn set_use_modal_editing(&mut self, to: bool) { - self.use_modal_editing = to; - } - - pub fn use_modal_editing(&self) -> bool { - self.use_modal_editing - } - - fn selections_did_change( - &mut self, - local: bool, - old_cursor_position: &Anchor, - show_completions: bool, - window: &mut Window, - cx: &mut Context, - ) { - window.invalidate_character_coordinates(); - - // Copy selections to primary selection buffer - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - if local { - let selections = self.selections.all::(cx); - let buffer_handle = self.buffer.read(cx).read(cx); - - let mut text = String::new(); - for (index, selection) in selections.iter().enumerate() { - let text_for_selection = buffer_handle - .text_for_range(selection.start..selection.end) - .collect::(); - - text.push_str(&text_for_selection); - if index != selections.len() - 1 { - text.push('\n'); - } - } - - if !text.is_empty() { - cx.write_to_primary(ClipboardItem::new_string(text)); - } - } - - if self.focus_handle.is_focused(window) && self.leader_peer_id.is_none() { - self.buffer.update(cx, |buffer, cx| { - buffer.set_active_selections( - &self.selections.disjoint_anchors(), - self.selections.line_mode, - self.cursor_shape, - cx, - ) - }); - } - let display_map = self - .display_map - .update(cx, |display_map, cx| display_map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - self.add_selections_state = None; - self.select_next_state = None; - self.select_prev_state = None; - self.select_syntax_node_history.try_clear(); - self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer); - self.snippet_stack - .invalidate(&self.selections.disjoint_anchors(), buffer); - self.take_rename(false, window, cx); - - let new_cursor_position = self.selections.newest_anchor().head(); - - self.push_to_nav_history( - *old_cursor_position, - Some(new_cursor_position.to_point(buffer)), - false, - cx, - ); - - if local { - let new_cursor_position = self.selections.newest_anchor().head(); - let mut context_menu = self.context_menu.borrow_mut(); - let completion_menu = match context_menu.as_ref() { - Some(CodeContextMenu::Completions(menu)) => Some(menu), - _ => { - *context_menu = None; - None - } - }; - if let Some(buffer_id) = new_cursor_position.buffer_id { - if !self.registered_buffers.contains_key(&buffer_id) { - if let Some(project) = self.project.as_ref() { - project.update(cx, |project, cx| { - let Some(buffer) = self.buffer.read(cx).buffer(buffer_id) else { - return; - }; - self.registered_buffers.insert( - buffer_id, - project.register_buffer_with_language_servers(&buffer, cx), - ); - }) - } - } - } - - if let Some(completion_menu) = completion_menu { - let cursor_position = new_cursor_position.to_offset(buffer); - let (word_range, kind) = - buffer.surrounding_word(completion_menu.initial_position, true); - if kind == Some(CharKind::Word) - && word_range.to_inclusive().contains(&cursor_position) - { - let mut completion_menu = completion_menu.clone(); - drop(context_menu); - - let query = Self::completion_query(buffer, cursor_position); - cx.spawn(async move |this, cx| { - completion_menu - .filter(query.as_deref(), cx.background_executor().clone()) - .await; - - this.update(cx, |this, cx| { - let mut context_menu = this.context_menu.borrow_mut(); - let Some(CodeContextMenu::Completions(menu)) = context_menu.as_ref() - else { - return; - }; - - if menu.id > completion_menu.id { - return; - } - - *context_menu = Some(CodeContextMenu::Completions(completion_menu)); - drop(context_menu); - cx.notify(); - }) - }) - .detach(); - - if show_completions { - self.show_completions(&ShowCompletions { trigger: None }, window, cx); - } - } else { - drop(context_menu); - self.hide_context_menu(window, cx); - } - } else { - drop(context_menu); - } - - hide_hover(self, cx); - - if old_cursor_position.to_display_point(&display_map).row() - != new_cursor_position.to_display_point(&display_map).row() - { - self.available_code_actions.take(); - } - self.refresh_code_actions(window, cx); - self.refresh_document_highlights(cx); - self.refresh_selected_text_highlights(false, window, cx); - refresh_matching_bracket_highlights(self, 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(); - if self.git_blame_inline_enabled { - self.start_inline_blame_timer(window, cx); - } - } - - self.blink_manager.update(cx, BlinkManager::pause_blinking); - cx.emit(EditorEvent::SelectionsChanged { local }); - - let selections = &self.selections.disjoint; - if selections.len() == 1 { - cx.emit(SearchEvent::ActiveMatchChanged) - } - if local { - if let Some((_, _, buffer_snapshot)) = buffer.as_singleton() { - let inmemory_selections = selections - .iter() - .map(|s| { - text::ToPoint::to_point(&s.range().start.text_anchor, buffer_snapshot) - ..text::ToPoint::to_point(&s.range().end.text_anchor, buffer_snapshot) - }) - .collect(); - self.update_restoration_data(cx, |data| { - data.selections = inmemory_selections; - }); - - if 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() - .map(|selection| { - ( - selection.start.to_offset(&snapshot), - selection.end.to_offset(&snapshot), - ) - }) - .collect(); - - DB.save_editor_selections(editor_id, workspace_id, db_selections) - .await - .with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}")) - .log_err(); - }); - } - } - } - } - - cx.notify(); - } - - fn folds_did_change(&mut self, cx: &mut Context) { - use text::ToOffset as _; - use text::ToPoint as _; - - if WorkspaceSettings::get(None, cx).restore_on_startup == RestoreOnStartupBehavior::None { - return; - } - - let Some(singleton) = self.buffer().read(cx).as_singleton() else { - return; - }; - - let snapshot = singleton.read(cx).snapshot(); - let inmemory_folds = self.display_map.update(cx, |display_map, cx| { - let display_snapshot = display_map.snapshot(cx); - - display_snapshot - .folds_in_range(0..display_snapshot.buffer_snapshot.len()) - .map(|fold| { - fold.range.start.text_anchor.to_point(&snapshot) - ..fold.range.end.text_anchor.to_point(&snapshot) - }) - .collect() - }); - self.update_restoration_data(cx, |data| { - data.folds = inmemory_folds; - }); - - let Some(workspace_id) = self.workspace.as_ref().and_then(|workspace| workspace.1) else { - return; - }; - let background_executor = cx.background_executor().clone(); - let editor_id = cx.entity().entity_id().as_u64() as ItemId; - let db_folds = self.display_map.update(cx, |display_map, cx| { - display_map - .snapshot(cx) - .folds_in_range(0..snapshot.len()) - .map(|fold| { - ( - fold.range.start.text_anchor.to_offset(&snapshot), - fold.range.end.text_anchor.to_offset(&snapshot), - ) - }) - .collect() - }); - self.serialize_folds = cx.background_spawn(async move { - background_executor.timer(SERIALIZATION_THROTTLE_TIME).await; - DB.save_editor_folds(editor_id, workspace_id, db_folds) - .await - .with_context(|| { - format!( - "persisting editor folds for editor {editor_id}, workspace {workspace_id:?}" - ) - }) - .log_err(); - }); - } - - pub fn sync_selections( - &mut self, - other: Entity, - cx: &mut Context, - ) -> gpui::Subscription { - let other_selections = other.read(cx).selections.disjoint.to_vec(); - self.selections.change_with(cx, |selections| { - selections.select_anchors(other_selections); - }); - - let other_subscription = - cx.subscribe(&other, |this, other, other_evt, cx| match other_evt { - EditorEvent::SelectionsChanged { local: true } => { - let other_selections = other.read(cx).selections.disjoint.to_vec(); - if other_selections.is_empty() { - return; - } - this.selections.change_with(cx, |selections| { - selections.select_anchors(other_selections); - }); - } - _ => {} - }); - - let 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); - }) - }); - } - _ => {} - }); - - Subscription::join(other_subscription, this_subscription) - } - - pub fn change_selections( - &mut self, - autoscroll: Option, - window: &mut Window, - cx: &mut Context, - change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R, - ) -> R { - self.change_selections_inner(autoscroll, true, window, cx, change) - } - - fn change_selections_inner( - &mut self, - autoscroll: Option, - request_completions: bool, - window: &mut Window, - cx: &mut Context, - change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R, - ) -> R { - let old_cursor_position = self.selections.newest_anchor().head(); - self.push_to_selection_history(); - - let (changed, result) = self.selections.change_with(cx, change); - - if changed { - if let Some(autoscroll) = autoscroll { - self.request_autoscroll(autoscroll, cx); - } - self.selections_did_change(true, &old_cursor_position, request_completions, window, cx); - - if self.should_open_signature_help_automatically( - &old_cursor_position, - self.signature_help_state.backspace_pressed(), - cx, - ) { - self.show_signature_help(&ShowSignatureHelp, window, cx); - } - self.signature_help_state.set_backspace_pressed(false); - } - - result - } - - pub fn edit(&mut self, edits: I, cx: &mut Context) - where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - if self.read_only(cx) { - return; - } - - self.buffer - .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); - } - - pub fn edit_with_autoindent(&mut self, edits: I, cx: &mut Context) - where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - if self.read_only(cx) { - return; - } - - self.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, self.autoindent_mode.clone(), cx) - }); - } - - pub fn edit_with_block_indent( - &mut self, - edits: I, - original_indent_columns: Vec>, - cx: &mut Context, - ) where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - if self.read_only(cx) { - return; - } - - self.buffer.update(cx, |buffer, cx| { - buffer.edit( - edits, - Some(AutoindentMode::Block { - original_indent_columns, - }), - cx, - ) - }); - } - - fn select(&mut self, phase: SelectPhase, window: &mut Window, cx: &mut Context) { - self.hide_context_menu(window, cx); - - match phase { - SelectPhase::Begin { - position, - add, - click_count, - } => self.begin_selection(position, add, click_count, window, cx), - SelectPhase::BeginColumnar { - position, - goal_column, - reset, - } => self.begin_columnar_selection(position, goal_column, reset, window, cx), - SelectPhase::Extend { - position, - click_count, - } => self.extend_selection(position, click_count, window, cx), - SelectPhase::Update { - position, - goal_column, - scroll_delta, - } => self.update_selection(position, goal_column, scroll_delta, window, cx), - SelectPhase::End => self.end_selection(window, cx), - } - } - - fn extend_selection( - &mut self, - position: DisplayPoint, - click_count: usize, - window: &mut Window, - cx: &mut Context, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let tail = self.selections.newest::(cx).tail(); - self.begin_selection(position, false, click_count, window, cx); - - let position = position.to_offset(&display_map, Bias::Left); - let tail_anchor = display_map.buffer_snapshot.anchor_before(tail); - - let mut pending_selection = self - .selections - .pending_anchor() - .expect("extend_selection not called with pending selection"); - if position >= tail { - pending_selection.start = tail_anchor; - } else { - pending_selection.end = tail_anchor; - pending_selection.reversed = true; - } - - let mut pending_mode = self.selections.pending_mode().unwrap(); - match &mut pending_mode { - SelectMode::Word(range) | SelectMode::Line(range) => *range = tail_anchor..tail_anchor, - _ => {} - } - - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.set_pending(pending_selection, pending_mode) - }); - } - - fn begin_selection( - &mut self, - position: DisplayPoint, - add: bool, - click_count: usize, - window: &mut Window, - cx: &mut Context, - ) { - if !self.focus_handle.is_focused(window) { - self.last_focused_descendant = None; - window.focus(&self.focus_handle); - } - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - let newest_selection = self.selections.newest_anchor().clone(); - let position = display_map.clip_point(position, Bias::Left); - - let start; - let end; - let mode; - let mut auto_scroll; - match click_count { - 1 => { - start = buffer.anchor_before(position.to_point(&display_map)); - end = start; - mode = SelectMode::Character; - auto_scroll = true; - } - 2 => { - let range = movement::surrounding_word(&display_map, position); - start = buffer.anchor_before(range.start.to_point(&display_map)); - end = buffer.anchor_before(range.end.to_point(&display_map)); - mode = SelectMode::Word(start..end); - auto_scroll = true; - } - 3 => { - let position = display_map - .clip_point(position, Bias::Left) - .to_point(&display_map); - let line_start = display_map.prev_line_boundary(position).0; - let next_line_start = buffer.clip_point( - display_map.next_line_boundary(position).0 + Point::new(1, 0), - Bias::Left, - ); - start = buffer.anchor_before(line_start); - end = buffer.anchor_before(next_line_start); - mode = SelectMode::Line(start..end); - auto_scroll = true; - } - _ => { - start = buffer.anchor_before(0); - end = buffer.anchor_before(buffer.len()); - mode = SelectMode::All; - auto_scroll = false; - } - } - auto_scroll &= EditorSettings::get_global(cx).autoscroll_on_clicks; - - let point_to_delete: Option = { - let selected_points: Vec> = - self.selections.disjoint_in_range(start..end, cx); - - if !add || click_count > 1 { - None - } else if !selected_points.is_empty() { - Some(selected_points[0].id) - } else { - let clicked_point_already_selected = - self.selections.disjoint.iter().find(|selection| { - selection.start.to_point(buffer) == start.to_point(buffer) - || selection.end.to_point(buffer) == end.to_point(buffer) - }); - - clicked_point_already_selected.map(|selection| selection.id) - } - }; - - let selections_count = self.selections.count(); - - self.change_selections(auto_scroll.then(Autoscroll::newest), window, cx, |s| { - if let Some(point_to_delete) = point_to_delete { - s.delete(point_to_delete); - - if selections_count == 1 { - s.set_pending_anchor_range(start..end, mode); - } - } else { - if !add { - s.clear_disjoint(); - } else if click_count > 1 { - s.delete(newest_selection.id) - } - - s.set_pending_anchor_range(start..end, mode); - } - }); - } - - fn begin_columnar_selection( - &mut self, - position: DisplayPoint, - goal_column: u32, - reset: bool, - window: &mut Window, - cx: &mut Context, - ) { - if !self.focus_handle.is_focused(window) { - self.last_focused_descendant = None; - window.focus(&self.focus_handle); - } - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - - if reset { - let pointer_position = display_map - .buffer_snapshot - .anchor_before(position.to_point(&display_map)); - - self.change_selections(Some(Autoscroll::newest()), window, cx, |s| { - s.clear_disjoint(); - s.set_pending_anchor_range( - pointer_position..pointer_position, - SelectMode::Character, - ); - }); - } - - let tail = self.selections.newest::(cx).tail(); - self.columnar_selection_tail = Some(display_map.buffer_snapshot.anchor_before(tail)); - - if !reset { - self.select_columns( - tail.to_display_point(&display_map), - position, - goal_column, - &display_map, - window, - cx, - ); - } - } - - fn update_selection( - &mut self, - position: DisplayPoint, - goal_column: u32, - scroll_delta: gpui::Point, - window: &mut Window, - cx: &mut Context, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - - if let Some(tail) = self.columnar_selection_tail.as_ref() { - let tail = tail.to_display_point(&display_map); - self.select_columns(tail, position, goal_column, &display_map, window, cx); - } else if let Some(mut pending) = self.selections.pending_anchor() { - let buffer = self.buffer.read(cx).snapshot(cx); - let head; - let tail; - let mode = self.selections.pending_mode().unwrap(); - match &mode { - SelectMode::Character => { - head = position.to_point(&display_map); - tail = pending.tail().to_point(&buffer); - } - SelectMode::Word(original_range) => { - let original_display_range = original_range.start.to_display_point(&display_map) - ..original_range.end.to_display_point(&display_map); - let original_buffer_range = original_display_range.start.to_point(&display_map) - ..original_display_range.end.to_point(&display_map); - if movement::is_inside_word(&display_map, position) - || original_display_range.contains(&position) - { - let word_range = movement::surrounding_word(&display_map, position); - if word_range.start < original_display_range.start { - head = word_range.start.to_point(&display_map); - } else { - head = word_range.end.to_point(&display_map); - } - } else { - head = position.to_point(&display_map); - } - - if head <= original_buffer_range.start { - tail = original_buffer_range.end; - } else { - tail = original_buffer_range.start; - } - } - SelectMode::Line(original_range) => { - let original_range = original_range.to_point(&display_map.buffer_snapshot); - - let position = display_map - .clip_point(position, Bias::Left) - .to_point(&display_map); - let line_start = display_map.prev_line_boundary(position).0; - let next_line_start = buffer.clip_point( - display_map.next_line_boundary(position).0 + Point::new(1, 0), - Bias::Left, - ); - - if line_start < original_range.start { - head = line_start - } else { - head = next_line_start - } - - if head <= original_range.start { - tail = original_range.end; - } else { - tail = original_range.start; - } - } - SelectMode::All => { - return; - } - }; - - if head < tail { - pending.start = buffer.anchor_before(head); - pending.end = buffer.anchor_before(tail); - pending.reversed = true; - } else { - pending.start = buffer.anchor_before(tail); - pending.end = buffer.anchor_before(head); - pending.reversed = false; - } - - self.change_selections(None, window, cx, |s| { - s.set_pending(pending, mode); - }); - } else { - log::error!("update_selection dispatched with no pending selection"); - return; - } - - self.apply_scroll_delta(scroll_delta, window, cx); - cx.notify(); - } - - fn end_selection(&mut self, window: &mut Window, cx: &mut Context) { - self.columnar_selection_tail.take(); - if self.selections.pending_anchor().is_some() { - let selections = self.selections.all::(cx); - self.change_selections(None, window, cx, |s| { - s.select(selections); - s.clear_pending(); - }); - } - } - - fn select_columns( - &mut self, - tail: DisplayPoint, - head: DisplayPoint, - goal_column: u32, - display_map: &DisplaySnapshot, - window: &mut Window, - cx: &mut Context, - ) { - let start_row = cmp::min(tail.row(), head.row()); - let end_row = cmp::max(tail.row(), head.row()); - let start_column = cmp::min(tail.column(), goal_column); - let end_column = cmp::max(tail.column(), goal_column); - let reversed = start_column < tail.column(); - - let selection_ranges = (start_row.0..=end_row.0) - .map(DisplayRow) - .filter_map(|row| { - if start_column <= display_map.line_len(row) && !display_map.is_block_line(row) { - let start = display_map - .clip_point(DisplayPoint::new(row, start_column), Bias::Left) - .to_point(display_map); - let end = display_map - .clip_point(DisplayPoint::new(row, end_column), Bias::Right) - .to_point(display_map); - if reversed { - Some(end..start) - } else { - Some(start..end) - } - } else { - None - } - }) - .collect::>(); - - self.change_selections(None, window, cx, |s| { - s.select_ranges(selection_ranges); - }); - cx.notify(); - } - - pub fn has_non_empty_selection(&self, cx: &mut App) -> bool { - self.selections - .all_adjusted(cx) - .iter() - .any(|selection| !selection.is_empty()) - } - - pub fn has_pending_nonempty_selection(&self) -> bool { - let pending_nonempty_selection = match self.selections.pending_anchor() { - Some(Selection { start, end, .. }) => start != end, - None => false, - }; - - pending_nonempty_selection - || (self.columnar_selection_tail.is_some() && self.selections.disjoint.len() > 1) - } - - pub fn has_pending_selection(&self) -> bool { - self.selections.pending_anchor().is_some() || self.columnar_selection_tail.is_some() - } - - pub fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context) { - self.selection_mark_mode = false; - - if self.clear_expanded_diff_hunks(cx) { - cx.notify(); - return; - } - if self.dismiss_menus_and_popups(true, window, cx) { - return; - } - - if self.mode.is_full() - && self.change_selections(Some(Autoscroll::fit()), window, cx, |s| s.try_cancel()) - { - return; - } - - cx.propagate(); - } - - pub fn dismiss_menus_and_popups( - &mut self, - is_user_requested: bool, - window: &mut Window, - cx: &mut Context, - ) -> bool { - if self.take_rename(false, window, cx).is_some() { - return true; - } - - if hide_hover(self, cx) { - return true; - } - - if self.hide_signature_help(cx, SignatureHelpHiddenBy::Escape) { - return true; - } - - if self.hide_context_menu(window, cx).is_some() { - return true; - } - - if self.mouse_context_menu.take().is_some() { - return true; - } - - if is_user_requested && self.discard_inline_completion(true, cx) { - return true; - } - - if self.snippet_stack.pop().is_some() { - return true; - } - - if self.mode.is_full() && matches!(self.active_diagnostics, ActiveDiagnostic::Group(_)) { - self.dismiss_diagnostics(cx); - return true; - } - - false - } - - fn linked_editing_ranges_for( - &self, - selection: Range, - cx: &App, - ) -> Option, Vec>>> { - if self.linked_edit_ranges.is_empty() { - return None; - } - let ((base_range, linked_ranges), buffer_snapshot, buffer) = - selection.end.buffer_id.and_then(|end_buffer_id| { - if selection.start.buffer_id != Some(end_buffer_id) { - return None; - } - let buffer = self.buffer.read(cx).buffer(end_buffer_id)?; - let snapshot = buffer.read(cx).snapshot(); - self.linked_edit_ranges - .get(end_buffer_id, selection.start..selection.end, &snapshot) - .map(|ranges| (ranges, snapshot, buffer)) - })?; - use text::ToOffset as TO; - // find offset from the start of current range to current cursor position - let start_byte_offset = TO::to_offset(&base_range.start, &buffer_snapshot); - - let start_offset = TO::to_offset(&selection.start, &buffer_snapshot); - let start_difference = start_offset - start_byte_offset; - let end_offset = TO::to_offset(&selection.end, &buffer_snapshot); - let end_difference = end_offset - start_byte_offset; - // Current range has associated linked ranges. - let mut linked_edits = HashMap::<_, Vec<_>>::default(); - for range in linked_ranges.iter() { - let start_offset = TO::to_offset(&range.start, &buffer_snapshot); - let end_offset = start_offset + end_difference; - let start_offset = start_offset + start_difference; - if start_offset > buffer_snapshot.len() || end_offset > buffer_snapshot.len() { - continue; - } - if self.selections.disjoint_anchor_ranges().any(|s| { - if s.start.buffer_id != selection.start.buffer_id - || s.end.buffer_id != selection.end.buffer_id - { - return false; - } - TO::to_offset(&s.start.text_anchor, &buffer_snapshot) <= end_offset - && TO::to_offset(&s.end.text_anchor, &buffer_snapshot) >= start_offset - }) { - continue; - } - let start = buffer_snapshot.anchor_after(start_offset); - let end = buffer_snapshot.anchor_after(end_offset); - linked_edits - .entry(buffer.clone()) - .or_default() - .push(start..end); - } - Some(linked_edits) - } - - pub fn handle_input(&mut self, text: &str, window: &mut Window, cx: &mut Context) { - let text: Arc = text.into(); - - if self.read_only(cx) { - return; - } - - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let selections = self.selections.all_adjusted(cx); - let mut bracket_inserted = false; - let mut edits = Vec::new(); - let mut linked_edits = HashMap::<_, Vec<_>>::default(); - let mut new_selections = Vec::with_capacity(selections.len()); - let mut new_autoclose_regions = Vec::new(); - let snapshot = self.buffer.read(cx).read(cx); - let mut clear_linked_edit_ranges = false; - - for (selection, autoclose_region) in - self.selections_with_autoclose_regions(selections, &snapshot) - { - if let Some(scope) = snapshot.language_scope_at(selection.head()) { - // Determine if the inserted text matches the opening or closing - // bracket of any of this language's bracket pairs. - let mut bracket_pair = None; - let mut is_bracket_pair_start = false; - let mut is_bracket_pair_end = false; - if !text.is_empty() { - let mut bracket_pair_matching_end = None; - // `text` can be empty when a user is using IME (e.g. Chinese Wubi Simplified) - // and they are removing the character that triggered IME popup. - for (pair, enabled) in scope.brackets() { - if !pair.close && !pair.surround { - continue; - } - - if enabled && pair.start.ends_with(text.as_ref()) { - let prefix_len = pair.start.len() - text.len(); - let preceding_text_matches_prefix = prefix_len == 0 - || (selection.start.column >= (prefix_len as u32) - && snapshot.contains_str_at( - Point::new( - selection.start.row, - selection.start.column - (prefix_len as u32), - ), - &pair.start[..prefix_len], - )); - if preceding_text_matches_prefix { - bracket_pair = Some(pair.clone()); - is_bracket_pair_start = true; - break; - } - } - if pair.end.as_str() == text.as_ref() && bracket_pair_matching_end.is_none() - { - // take first bracket pair matching end, but don't break in case a later bracket - // pair matches start - bracket_pair_matching_end = Some(pair.clone()); - } - } - if bracket_pair.is_none() && bracket_pair_matching_end.is_some() { - bracket_pair = Some(bracket_pair_matching_end.unwrap()); - is_bracket_pair_end = true; - } - } - - if let Some(bracket_pair) = bracket_pair { - let snapshot_settings = snapshot.language_settings_at(selection.start, cx); - let autoclose = self.use_autoclose && snapshot_settings.use_autoclose; - let auto_surround = - self.use_auto_surround && snapshot_settings.use_auto_surround; - if selection.is_empty() { - if is_bracket_pair_start { - // If the inserted text is a suffix of an opening bracket and the - // selection is preceded by the rest of the opening bracket, then - // insert the closing bracket. - let following_text_allows_autoclose = snapshot - .chars_at(selection.start) - .next() - .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().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 - { - let target = bracket_pair.start.chars().next().unwrap(); - let current_line_count = snapshot - .reversed_chars_at(selection.start) - .take_while(|&c| c != '\n') - .filter(|&c| c == target) - .count(); - current_line_count % 2 == 1 - } else { - false - }; - - if autoclose - && bracket_pair.close - && following_text_allows_autoclose - && preceding_text_allows_autoclose - && !is_closing_quote - { - let anchor = snapshot.anchor_before(selection.end); - new_selections.push((selection.map(|_| anchor), text.len())); - new_autoclose_regions.push(( - anchor, - text.len(), - selection.id, - bracket_pair.clone(), - )); - edits.push(( - selection.range(), - format!("{}{}", text, bracket_pair.end).into(), - )); - bracket_inserted = true; - continue; - } - } - - if let Some(region) = autoclose_region { - // If the selection is followed by an auto-inserted closing bracket, - // then don't insert that closing bracket again; just move the selection - // past the closing bracket. - let should_skip = selection.end == region.range.end.to_point(&snapshot) - && text.as_ref() == region.pair.end.as_str(); - if should_skip { - let anchor = snapshot.anchor_after(selection.end); - new_selections - .push((selection.map(|_| anchor), region.pair.end.len())); - continue; - } - } - - let always_treat_brackets_as_autoclosed = snapshot - .language_settings_at(selection.start, cx) - .always_treat_brackets_as_autoclosed; - if always_treat_brackets_as_autoclosed - && is_bracket_pair_end - && snapshot.contains_str_at(selection.end, text.as_ref()) - { - // Otherwise, when `always_treat_brackets_as_autoclosed` is set to `true - // and the inserted text is a closing bracket and the selection is followed - // by the closing bracket then move the selection past the closing bracket. - let anchor = snapshot.anchor_after(selection.end); - new_selections.push((selection.map(|_| anchor), text.len())); - continue; - } - } - // If an opening bracket is 1 character long and is typed while - // text is selected, then surround that text with the bracket pair. - else if auto_surround - && bracket_pair.surround - && is_bracket_pair_start - && bracket_pair.start.chars().count() == 1 - { - edits.push((selection.start..selection.start, text.clone())); - edits.push(( - selection.end..selection.end, - bracket_pair.end.as_str().into(), - )); - bracket_inserted = true; - new_selections.push(( - Selection { - id: selection.id, - start: snapshot.anchor_after(selection.start), - end: snapshot.anchor_before(selection.end), - reversed: selection.reversed, - goal: selection.goal, - }, - 0, - )); - continue; - } - } - } - - if self.auto_replace_emoji_shortcode - && selection.is_empty() - && text.as_ref().ends_with(':') - { - 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, - )); - - // 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; - } - } - } - } - - // If not handling any auto-close operation, then just replace the selected - // text with the given input and move the selection to the end of the - // newly inserted text. - let anchor = snapshot.anchor_after(selection.end); - if !self.linked_edit_ranges.is_empty() { - let start_anchor = snapshot.anchor_before(selection.start); - - let is_word_char = text.chars().next().map_or(true, |char| { - let classifier = snapshot.char_classifier_at(start_anchor.to_offset(&snapshot)); - classifier.is_word(char) - }); - - if is_word_char { - if let Some(ranges) = self - .linked_editing_ranges_for(start_anchor.text_anchor..anchor.text_anchor, cx) - { - for (buffer, edits) in ranges { - linked_edits - .entry(buffer.clone()) - .or_default() - .extend(edits.into_iter().map(|range| (range, text.clone()))); - } - } - } else { - clear_linked_edit_ranges = true; - } - } - - new_selections.push((selection.map(|_| anchor), 0)); - edits.push((selection.start..selection.end, text.clone())); - } - - drop(snapshot); - - self.transact(window, cx, |this, window, cx| { - if clear_linked_edit_ranges { - this.linked_edit_ranges.clear(); - } - let initial_buffer_versions = - jsx_tag_auto_close::construct_initial_buffer_versions_map(this, &edits, cx); - - this.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, this.autoindent_mode.clone(), cx); - }); - for (buffer, edits) in linked_edits { - buffer.update(cx, |buffer, cx| { - let snapshot = buffer.snapshot(); - let edits = edits - .into_iter() - .map(|(range, text)| { - use text::ToPoint as TP; - let end_point = TP::to_point(&range.end, &snapshot); - let start_point = TP::to_point(&range.start, &snapshot); - (start_point..end_point, text) - }) - .sorted_by_key(|(range, _)| range.start); - buffer.edit(edits, None, cx); - }) - } - let new_anchor_selections = new_selections.iter().map(|e| &e.0); - let new_selection_deltas = new_selections.iter().map(|e| e.1); - let map = this.display_map.update(cx, |map, cx| map.snapshot(cx)); - let new_selections = resolve_selections::(new_anchor_selections, &map) - .zip(new_selection_deltas) - .map(|(selection, delta)| Selection { - id: selection.id, - start: selection.start + delta, - end: selection.end + delta, - reversed: selection.reversed, - goal: SelectionGoal::None, - }) - .collect::>(); - - let mut i = 0; - for (position, delta, selection_id, pair) in new_autoclose_regions { - let position = position.to_offset(&map.buffer_snapshot) + delta; - let start = map.buffer_snapshot.anchor_before(position); - let end = map.buffer_snapshot.anchor_after(position); - while let Some(existing_state) = this.autoclose_regions.get(i) { - match existing_state.range.start.cmp(&start, &map.buffer_snapshot) { - Ordering::Less => i += 1, - Ordering::Greater => break, - Ordering::Equal => { - match end.cmp(&existing_state.range.end, &map.buffer_snapshot) { - Ordering::Less => i += 1, - Ordering::Equal => break, - Ordering::Greater => break, - } - } - } - } - this.autoclose_regions.insert( - i, - AutocloseRegion { - selection_id, - range: start..end, - pair, - }, - ); - } - - let had_active_inline_completion = this.has_active_inline_completion(); - this.change_selections_inner(Some(Autoscroll::fit()), false, window, cx, |s| { - s.select(new_selections) - }); - - 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); - } - } - - let editor_settings = EditorSettings::get_global(cx); - if bracket_inserted - && (editor_settings.auto_signature_help - || editor_settings.show_signature_help_after_edits) - { - this.show_signature_help(&ShowSignatureHelp, window, cx); - } - - let trigger_in_words = - 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() - && this - .buffer() - .read(cx) - .snapshot(cx) - .line_len(MultiBufferRow(latest.start.row)) - == latest.start.column - { - this.rewrap_impl( - RewrapOptions { - override_language_settings: true, - preserve_existing_whitespace: true, - }, - cx, - ) - } - } - this.trigger_completion_on_input(&text, trigger_in_words, window, cx); - linked_editing_ranges::refresh_linked_ranges(this, window, cx); - this.refresh_inline_completion(true, false, window, cx); - jsx_tag_auto_close::handle_from(this, initial_buffer_versions, window, cx); - }); - } - - fn find_possible_emoji_shortcode_at_position( - snapshot: &MultiBufferSnapshot, - position: Point, - ) -> Option { - let mut chars = Vec::new(); - let mut found_colon = false; - for char in snapshot.reversed_chars_at(position).take(100) { - // Found a possible emoji shortcode in the middle of the buffer - if found_colon { - if char.is_whitespace() { - chars.reverse(); - return Some(chars.iter().collect()); - } - // If the previous character is not a whitespace, we are in the middle of a word - // and we only want to complete the shortcode if the word is made up of other emojis - let mut containing_word = String::new(); - for ch in snapshot - .reversed_chars_at(position) - .skip(chars.len() + 1) - .take(100) - { - if ch.is_whitespace() { - break; - } - containing_word.push(ch); - } - let containing_word = containing_word.chars().rev().collect::(); - if util::word_consists_of_emojis(containing_word.as_str()) { - chars.reverse(); - return Some(chars.iter().collect()); - } - } - - if char.is_whitespace() || !char.is_ascii() { - return None; - } - if char == ':' { - found_colon = true; - } else { - chars.push(char); - } - } - // Found a possible emoji shortcode at the beginning of the buffer - chars.reverse(); - Some(chars.iter().collect()) - } - - pub fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = { - let selections = this.selections.all::(cx); - let multi_buffer = this.buffer.read(cx); - let buffer = multi_buffer.snapshot(cx); - selections - .iter() - .map(|selection| { - let start_point = selection.start.to_point(&buffer); - let mut indent = - buffer.indent_size_for_line(MultiBufferRow(start_point.row)); - indent.len = cmp::min(indent.len, start_point.column); - let start = selection.start; - let end = selection.end; - let selection_is_empty = start == end; - let language_scope = buffer.language_scope_at(start); - let (comment_delimiter, insert_extra_newline) = if let Some(language) = - &language_scope - { - let insert_extra_newline = - insert_extra_newline_brackets(&buffer, start..end, language) - || insert_extra_newline_tree_sitter(&buffer, start..end); - - // Comment extension on newline is allowed only for cursor selections - let comment_delimiter = maybe!({ - if !selection_is_empty { - return None; - } - - if !multi_buffer.language_settings(cx).extend_comment_on_newline { - return None; - } - - let delimiters = language.line_comment_prefixes(); - let max_len_of_delimiter = - delimiters.iter().map(|delimiter| delimiter.len()).max()?; - let (snapshot, range) = - buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?; - - let mut index_of_first_non_whitespace = 0; - let comment_candidate = snapshot - .chars_for_range(range) - .skip_while(|c| { - let should_skip = c.is_whitespace(); - if should_skip { - index_of_first_non_whitespace += 1; - } - should_skip - }) - .take(max_len_of_delimiter) - .collect::(); - let comment_prefix = delimiters.iter().find(|comment_prefix| { - comment_candidate.starts_with(comment_prefix.as_ref()) - })?; - let cursor_is_placed_after_comment_marker = - index_of_first_non_whitespace + comment_prefix.len() - <= start_point.column as usize; - if cursor_is_placed_after_comment_marker { - Some(comment_prefix.clone()) - } else { - None - } - }); - (comment_delimiter, insert_extra_newline) - } else { - (None, false) - }; - - let capacity_for_delimiter = comment_delimiter - .as_deref() - .map(str::len) - .unwrap_or_default(); - let mut new_text = - String::with_capacity(1 + capacity_for_delimiter + indent.len as usize); - new_text.push('\n'); - new_text.extend(indent.chars()); - if let Some(delimiter) = &comment_delimiter { - new_text.push_str(delimiter); - } - if insert_extra_newline { - new_text = new_text.repeat(2); - } - - let anchor = buffer.anchor_after(end); - let new_selection = selection.map(|_| anchor); - ( - (start..end, new_text), - (insert_extra_newline, new_selection), - ) - }) - .unzip() - }; - - this.edit_with_autoindent(edits, cx); - let buffer = this.buffer.read(cx).snapshot(cx); - let new_selections = selection_fixup_info - .into_iter() - .map(|(extra_newline_inserted, new_selection)| { - let mut cursor = new_selection.end.to_point(&buffer); - if extra_newline_inserted { - cursor.row -= 1; - cursor.column = buffer.line_len(MultiBufferRow(cursor.row)); - } - new_selection.map(|_| cursor) - }) - .collect(); - - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(new_selections) - }); - this.refresh_inline_completion(true, false, window, cx); - }); - } - - pub fn newline_above(&mut self, _: &NewlineAbove, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let buffer = self.buffer.read(cx); - let snapshot = buffer.snapshot(cx); - - let mut edits = Vec::new(); - let mut rows = Vec::new(); - - for (rows_inserted, selection) in self.selections.all_adjusted(cx).into_iter().enumerate() { - let cursor = selection.head(); - let row = cursor.row; - - let start_of_line = snapshot.clip_point(Point::new(row, 0), Bias::Left); - - let newline = "\n".to_string(); - edits.push((start_of_line..start_of_line, newline)); - - rows.push(row + rows_inserted as u32); - } - - self.transact(window, cx, |editor, window, cx| { - editor.edit(edits, cx); - - editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - let mut index = 0; - s.move_cursors_with(|map, _, _| { - let row = rows[index]; - index += 1; - - let point = Point::new(row, 0); - let boundary = map.next_line_boundary(point).1; - let clipped = map.clip_point(boundary, Bias::Left); - - (clipped, SelectionGoal::None) - }); - }); - - let mut indent_edits = Vec::new(); - let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx); - for row in rows { - let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx); - for (row, indent) in indents { - if indent.len == 0 { - continue; - } - - let text = match indent.kind { - IndentKind::Space => " ".repeat(indent.len as usize), - IndentKind::Tab => "\t".repeat(indent.len as usize), - }; - let point = Point::new(row.0, 0); - indent_edits.push((point..point, text)); - } - } - editor.edit(indent_edits, cx); - }); - } - - pub fn newline_below(&mut self, _: &NewlineBelow, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let buffer = self.buffer.read(cx); - let snapshot = buffer.snapshot(cx); - - let mut edits = Vec::new(); - let mut rows = Vec::new(); - let mut rows_inserted = 0; - - for selection in self.selections.all_adjusted(cx) { - let cursor = selection.head(); - let row = cursor.row; - - let point = Point::new(row + 1, 0); - let start_of_line = snapshot.clip_point(point, Bias::Left); - - let newline = "\n".to_string(); - edits.push((start_of_line..start_of_line, newline)); - - rows_inserted += 1; - rows.push(row + rows_inserted); - } - - self.transact(window, cx, |editor, window, cx| { - editor.edit(edits, cx); - - editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - let mut index = 0; - s.move_cursors_with(|map, _, _| { - let row = rows[index]; - index += 1; - - let point = Point::new(row, 0); - let boundary = map.next_line_boundary(point).1; - let clipped = map.clip_point(boundary, Bias::Left); - - (clipped, SelectionGoal::None) - }); - }); - - let mut indent_edits = Vec::new(); - let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx); - for row in rows { - let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx); - for (row, indent) in indents { - if indent.len == 0 { - continue; - } - - let text = match indent.kind { - IndentKind::Space => " ".repeat(indent.len as usize), - IndentKind::Tab => "\t".repeat(indent.len as usize), - }; - let point = Point::new(row.0, 0); - indent_edits.push((point..point, text)); - } - } - editor.edit(indent_edits, cx); - }); - } - - pub fn insert(&mut self, text: &str, window: &mut Window, cx: &mut Context) { - let autoindent = text.is_empty().not().then(|| AutoindentMode::Block { - original_indent_columns: Vec::new(), - }); - self.insert_with_autoindent_mode(text, autoindent, window, cx); - } - - fn insert_with_autoindent_mode( - &mut self, - text: &str, - autoindent_mode: Option, - window: &mut Window, - cx: &mut Context, - ) { - if self.read_only(cx) { - return; - } - - let text: Arc = text.into(); - self.transact(window, cx, |this, window, cx| { - let old_selections = this.selections.all_adjusted(cx); - let selection_anchors = this.buffer.update(cx, |buffer, cx| { - let anchors = { - let snapshot = buffer.read(cx); - old_selections - .iter() - .map(|s| { - let anchor = snapshot.anchor_after(s.head()); - s.map(|_| anchor) - }) - .collect::>() - }; - buffer.edit( - old_selections - .iter() - .map(|s| (s.start..s.end, text.clone())), - autoindent_mode, - cx, - ); - anchors - }); - - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_anchors(selection_anchors); - }); - - cx.notify(); - }); - } - - fn trigger_completion_on_input( - &mut self, - text: &str, - trigger_in_words: bool, - window: &mut Window, - cx: &mut Context, - ) { - let ignore_completion_provider = self - .context_menu - .borrow() - .as_ref() - .map(|menu| match menu { - CodeContextMenu::Completions(completions_menu) => { - completions_menu.ignore_completion_provider - } - CodeContextMenu::CodeActions(_) => false, - }) - .unwrap_or(false); - - if ignore_completion_provider { - self.show_word_completions(&ShowWordCompletions, window, cx); - } else if self.is_completion_trigger(text, trigger_in_words, cx) { - self.show_completions( - &ShowCompletions { - trigger: Some(text.to_owned()).filter(|x| !x.is_empty()), - }, - window, - cx, - ); - } else { - self.hide_context_menu(window, cx); - } - } - - fn is_completion_trigger( - &self, - text: &str, - trigger_in_words: bool, - cx: &mut Context, - ) -> bool { - let position = self.selections.newest_anchor().head(); - let multibuffer = self.buffer.read(cx); - let Some(buffer) = position - .buffer_id - .and_then(|buffer_id| multibuffer.buffer(buffer_id).clone()) - else { - return false; - }; - - if let Some(completion_provider) = &self.completion_provider { - completion_provider.is_completion_trigger( - &buffer, - position.text_anchor, - text, - trigger_in_words, - cx, - ) - } else { - false - } - } - - /// If any empty selections is touching the start of its innermost containing autoclose - /// region, expand it to select the brackets. - fn select_autoclose_pair(&mut self, window: &mut Window, cx: &mut Context) { - let selections = self.selections.all::(cx); - let buffer = self.buffer.read(cx).read(cx); - let new_selections = self - .selections_with_autoclose_regions(selections, &buffer) - .map(|(mut selection, region)| { - if !selection.is_empty() { - return selection; - } - - if let Some(region) = region { - let mut range = region.range.to_offset(&buffer); - if selection.start == range.start && range.start >= region.pair.start.len() { - range.start -= region.pair.start.len(); - if buffer.contains_str_at(range.start, ®ion.pair.start) - && buffer.contains_str_at(range.end, ®ion.pair.end) - { - range.end += region.pair.end.len(); - selection.start = range.start; - selection.end = range.end; - - return selection; - } - } - } - - let always_treat_brackets_as_autoclosed = buffer - .language_settings_at(selection.start, cx) - .always_treat_brackets_as_autoclosed; - - if !always_treat_brackets_as_autoclosed { - return selection; - } - - if let Some(scope) = buffer.language_scope_at(selection.start) { - for (pair, enabled) in scope.brackets() { - if !enabled || !pair.close { - continue; - } - - if buffer.contains_str_at(selection.start, &pair.end) { - let pair_start_len = pair.start.len(); - if buffer.contains_str_at( - selection.start.saturating_sub(pair_start_len), - &pair.start, - ) { - selection.start -= pair_start_len; - selection.end += pair.end.len(); - - return selection; - } - } - } - } - - selection - }) - .collect(); - - drop(buffer); - self.change_selections(None, window, cx, |selections| { - selections.select(new_selections) - }); - } - - /// Iterate the given selections, and for each one, find the smallest surrounding - /// autoclose region. This uses the ordering of the selections and the autoclose - /// regions to avoid repeated comparisons. - fn selections_with_autoclose_regions<'a, D: ToOffset + Clone>( - &'a self, - selections: impl IntoIterator>, - buffer: &'a MultiBufferSnapshot, - ) -> impl Iterator, Option<&'a AutocloseRegion>)> { - let mut i = 0; - let mut regions = self.autoclose_regions.as_slice(); - selections.into_iter().map(move |selection| { - let range = selection.start.to_offset(buffer)..selection.end.to_offset(buffer); - - let mut enclosing = None; - while let Some(pair_state) = regions.get(i) { - if pair_state.range.end.to_offset(buffer) < range.start { - regions = ®ions[i + 1..]; - i = 0; - } else if pair_state.range.start.to_offset(buffer) > range.end { - break; - } else { - if pair_state.selection_id == selection.id { - enclosing = Some(pair_state); - } - i += 1; - } - } - - (selection, enclosing) - }) - } - - /// Remove any autoclose regions that no longer contain their selection. - fn invalidate_autoclose_regions( - &mut self, - mut selections: &[Selection], - buffer: &MultiBufferSnapshot, - ) { - self.autoclose_regions.retain(|state| { - let mut i = 0; - while let Some(selection) = selections.get(i) { - if selection.end.cmp(&state.range.start, buffer).is_lt() { - selections = &selections[1..]; - continue; - } - if selection.start.cmp(&state.range.end, buffer).is_gt() { - break; - } - if selection.id == state.selection_id { - return true; - } else { - i += 1; - } - } - false - }); - } - - fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { - let offset = position.to_offset(buffer); - let (word_range, kind) = buffer.surrounding_word(offset, true); - if offset > word_range.start && kind == Some(CharKind::Word) { - Some( - buffer - .text_for_range(word_range.start..offset) - .collect::(), - ) - } else { - None - } - } - - pub fn toggle_inline_values( - &mut self, - _: &ToggleInlineValues, - _: &mut Window, - cx: &mut Context, - ) { - self.inline_value_cache.enabled = !self.inline_value_cache.enabled; - - self.refresh_inline_values(cx); - } - - pub fn toggle_inlay_hints( - &mut self, - _: &ToggleInlayHints, - _: &mut Window, - cx: &mut Context, - ) { - self.refresh_inlay_hints( - InlayHintRefreshReason::Toggle(!self.inlay_hints_enabled()), - cx, - ); - } - - pub fn inlay_hints_enabled(&self) -> bool { - self.inlay_hint_cache.enabled - } - - pub fn inline_values_enabled(&self) -> bool { - self.inline_value_cache.enabled - } - - fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut Context) { - if self.semantics_provider.is_none() || !self.mode.is_full() { - return; - } - - let reason_description = reason.description(); - let ignore_debounce = matches!( - reason, - InlayHintRefreshReason::SettingsChange(_) - | InlayHintRefreshReason::Toggle(_) - | InlayHintRefreshReason::ExcerptsRemoved(_) - | InlayHintRefreshReason::ModifiersChanged(_) - ); - let (invalidate_cache, required_languages) = match reason { - InlayHintRefreshReason::ModifiersChanged(enabled) => { - match self.inlay_hint_cache.modifiers_override(enabled) { - Some(enabled) => { - if enabled { - (InvalidationStrategy::RefreshRequested, None) - } else { - self.splice_inlays( - &self - .visible_inlay_hints(cx) - .iter() - .map(|inlay| inlay.id) - .collect::>(), - Vec::new(), - cx, - ); - return; - } - } - None => return, - } - } - InlayHintRefreshReason::Toggle(enabled) => { - if self.inlay_hint_cache.toggle(enabled) { - if enabled { - (InvalidationStrategy::RefreshRequested, None) - } else { - self.splice_inlays( - &self - .visible_inlay_hints(cx) - .iter() - .map(|inlay| inlay.id) - .collect::>(), - Vec::new(), - cx, - ); - return; - } - } else { - return; - } - } - InlayHintRefreshReason::SettingsChange(new_settings) => { - match self.inlay_hint_cache.update_settings( - &self.buffer, - new_settings, - self.visible_inlay_hints(cx), - cx, - ) { - ControlFlow::Break(Some(InlaySplice { - to_remove, - to_insert, - })) => { - self.splice_inlays(&to_remove, to_insert, cx); - return; - } - ControlFlow::Break(None) => return, - ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None), - } - } - InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => { - if let Some(InlaySplice { - to_remove, - to_insert, - }) = self.inlay_hint_cache.remove_excerpts(&excerpts_removed) - { - self.splice_inlays(&to_remove, to_insert, cx); - } - self.display_map.update(cx, |display_map, _| { - display_map.remove_inlays_for_excerpts(&excerpts_removed) - }); - return; - } - InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None), - InlayHintRefreshReason::BufferEdited(buffer_languages) => { - (InvalidationStrategy::BufferEdited, Some(buffer_languages)) - } - InlayHintRefreshReason::RefreshRequested => { - (InvalidationStrategy::RefreshRequested, None) - } - }; - - if let Some(InlaySplice { - to_remove, - to_insert, - }) = self.inlay_hint_cache.spawn_hint_refresh( - reason_description, - self.excerpts_for_inlay_hints_query(required_languages.as_ref(), cx), - invalidate_cache, - ignore_debounce, - cx, - ) { - self.splice_inlays(&to_remove, to_insert, cx); - } - } - - fn visible_inlay_hints(&self, cx: &Context) -> Vec { - self.display_map - .read(cx) - .current_inlays() - .filter(move |inlay| matches!(inlay.id, InlayId::Hint(_))) - .cloned() - .collect() - } - - pub fn excerpts_for_inlay_hints_query( - &self, - restrict_to_languages: Option<&HashSet>>, - cx: &mut Context, - ) -> HashMap, clock::Global, Range)> { - let Some(project) = self.project.as_ref() else { - return HashMap::default(); - }; - let project = project.read(cx); - let multi_buffer = self.buffer().read(cx); - let multi_buffer_snapshot = multi_buffer.snapshot(cx); - let multi_buffer_visible_start = self - .scroll_manager - .anchor() - .anchor - .to_point(&multi_buffer_snapshot); - let multi_buffer_visible_end = multi_buffer_snapshot.clip_point( - multi_buffer_visible_start - + Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0), - Bias::Left, - ); - let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end; - multi_buffer_snapshot - .range_to_buffer_ranges(multi_buffer_visible_range) - .into_iter() - .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) - .filter_map(|(buffer, excerpt_visible_range, excerpt_id)| { - let buffer_file = project::File::from_dyn(buffer.file())?; - let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?; - let worktree_entry = buffer_worktree - .read(cx) - .entry_for_id(buffer_file.project_entry_id(cx)?)?; - if worktree_entry.is_ignored { - return None; - } - - let language = buffer.language()?; - if let Some(restrict_to_languages) = restrict_to_languages { - if !restrict_to_languages.contains(language) { - return None; - } - } - Some(( - excerpt_id, - ( - multi_buffer.buffer(buffer.remote_id()).unwrap(), - buffer.version().clone(), - excerpt_visible_range, - ), - )) - }) - .collect() - } - - pub fn text_layout_details(&self, window: &mut Window) -> TextLayoutDetails { - TextLayoutDetails { - text_system: window.text_system().clone(), - editor_style: self.style.clone().unwrap(), - rem_size: window.rem_size(), - scroll_anchor: self.scroll_manager.anchor(), - visible_rows: self.visible_line_count(), - vertical_scroll_margin: self.scroll_manager.vertical_scroll_margin, - } - } - - pub fn splice_inlays( - &self, - to_remove: &[InlayId], - to_insert: Vec, - cx: &mut Context, - ) { - self.display_map.update(cx, |display_map, cx| { - display_map.splice_inlays(to_remove, to_insert, cx) - }); - cx.notify(); - } - - fn trigger_on_type_formatting( - &self, - input: String, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - if input.len() != 1 { - return None; - } - - let project = self.project.as_ref()?; - let position = self.selections.newest_anchor().head(); - let (buffer, buffer_position) = self - .buffer - .read(cx) - .text_anchor_for_position(position, cx)?; - - let settings = language_settings::language_settings( - buffer - .read(cx) - .language_at(buffer_position) - .map(|l| l.name()), - buffer.read(cx).file(), - cx, - ); - if !settings.use_on_type_format { - return None; - } - - // OnTypeFormatting returns a list of edits, no need to pass them between Zed instances, - // hence we do LSP request & edit on host side only — add formats to host's history. - let push_to_lsp_host_history = true; - // If this is not the host, append its history with new edits. - let push_to_client_history = project.read(cx).is_via_collab(); - - let on_type_formatting = project.update(cx, |project, cx| { - project.on_type_format( - buffer.clone(), - buffer_position, - input, - push_to_lsp_host_history, - cx, - ) - }); - Some(cx.spawn_in(window, async move |editor, cx| { - if let Some(transaction) = on_type_formatting.await? { - if push_to_client_history { - buffer - .update(cx, |buffer, _| { - buffer.push_transaction(transaction, Instant::now()); - buffer.finalize_last_transaction(); - }) - .ok(); - } - editor.update(cx, |editor, cx| { - editor.refresh_document_highlights(cx); - })?; - } - Ok(()) - })) - } - - pub fn show_word_completions( - &mut self, - _: &ShowWordCompletions, - window: &mut Window, - cx: &mut Context, - ) { - self.open_completions_menu(true, None, window, cx); - } - - pub fn show_completions( - &mut self, - options: &ShowCompletions, - window: &mut Window, - cx: &mut Context, - ) { - self.open_completions_menu(false, options.trigger.as_deref(), window, cx); - } - - fn open_completions_menu( - &mut self, - ignore_completion_provider: bool, - trigger: Option<&str>, - window: &mut Window, - cx: &mut Context, - ) { - if self.pending_rename.is_some() { - return; - } - if !self.snippet_stack.is_empty() && self.context_menu.borrow().as_ref().is_some() { - return; - } - - let position = self.selections.newest_anchor().head(); - if position.diff_base_anchor.is_some() { - return; - } - let (buffer, buffer_position) = - if let Some(output) = self.buffer.read(cx).text_anchor_for_position(position, cx) { - output - } else { - return; - }; - let buffer_snapshot = buffer.read(cx).snapshot(); - let show_completion_documentation = buffer_snapshot - .settings_at(buffer_position, cx) - .show_completion_documentation; - - let query = Self::completion_query(&self.buffer.read(cx).read(cx), position); - - let trigger_kind = match trigger { - Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => { - CompletionTriggerKind::TRIGGER_CHARACTER - } - _ => CompletionTriggerKind::INVOKED, - }; - let completion_context = CompletionContext { - trigger_character: trigger.and_then(|trigger| { - if trigger_kind == CompletionTriggerKind::TRIGGER_CHARACTER { - Some(String::from(trigger)) - } else { - None - } - }), - trigger_kind, - }; - - let (old_range, word_kind) = buffer_snapshot.surrounding_word(buffer_position); - let (old_range, word_to_exclude) = if word_kind == Some(CharKind::Word) { - let word_to_exclude = buffer_snapshot - .text_for_range(old_range.clone()) - .collect::(); - ( - buffer_snapshot.anchor_before(old_range.start) - ..buffer_snapshot.anchor_after(old_range.end), - Some(word_to_exclude), - ) - } else { - (buffer_position..buffer_position, None) - }; - - let completion_settings = language_settings( - buffer_snapshot - .language_at(buffer_position) - .map(|language| language.name()), - buffer_snapshot.file(), - cx, - ) - .completions; - - // The document can be large, so stay in reasonable bounds when searching for words, - // otherwise completion pop-up might be slow to appear. - const WORD_LOOKUP_ROWS: u32 = 5_000; - let buffer_row = text::ToPoint::to_point(&buffer_position, &buffer_snapshot).row; - let min_word_search = buffer_snapshot.clip_point( - Point::new(buffer_row.saturating_sub(WORD_LOOKUP_ROWS), 0), - Bias::Left, - ); - let max_word_search = buffer_snapshot.clip_point( - Point::new(buffer_row + WORD_LOOKUP_ROWS, 0).min(buffer_snapshot.max_point()), - Bias::Right, - ); - let word_search_range = buffer_snapshot.point_to_offset(min_word_search) - ..buffer_snapshot.point_to_offset(max_word_search); - - let provider = self - .completion_provider - .as_ref() - .filter(|_| !ignore_completion_provider); - let skip_digits = query - .as_ref() - .map_or(true, |query| !query.chars().any(|c| c.is_digit(10))); - - let (mut words, provided_completions) = match provider { - Some(provider) => { - let completions = provider.completions( - position.excerpt_id, - &buffer, - buffer_position, - completion_context, - window, - 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, - range: word_search_range, - skip_digits, - }) - }), - }; - - (words, completions) - } - None => ( - cx.background_spawn(async move { - buffer_snapshot.words_in_range(WordsQuery { - fuzzy_contents: None, - range: word_search_range, - skip_digits, - }) - }), - Task::ready(Ok(None)), - ), - }; - - let sort_completions = provider - .as_ref() - .map_or(false, |provider| provider.sort_completions()); - - let filter_completions = provider - .as_ref() - .map_or(true, |provider| provider.filter_completions()); - - let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; - - let id = post_inc(&mut self.next_completion_id); - let task = cx.spawn_in(window, async move |editor, cx| { - async move { - editor.update(cx, |this, _| { - this.completion_tasks.retain(|(task_id, _)| *task_id >= id); - })?; - - let mut completions = Vec::new(); - if let Some(provided_completions) = provided_completions.await.log_err().flatten() { - completions.extend(provided_completions); - if completion_settings.words == WordsCompletionMode::Fallback { - words = Task::ready(BTreeMap::default()); - } - } - - let mut words = words.await; - if let Some(word_to_exclude) = &word_to_exclude { - words.remove(word_to_exclude); - } - for lsp_completion in &completions { - words.remove(&lsp_completion.new_text); - } - completions.extend(words.into_iter().map(|(word, word_range)| Completion { - replace_range: old_range.clone(), - new_text: word.clone(), - label: CodeLabel::plain(word, None), - icon_path: None, - documentation: None, - source: CompletionSource::BufferWord { - word_range, - resolved: false, - }, - insert_text_mode: Some(InsertTextMode::AS_IS), - confirm: None, - })); - - let menu = if completions.is_empty() { - None - } else { - let mut menu = CompletionsMenu::new( - id, - sort_completions, - show_completion_documentation, - ignore_completion_provider, - position, - buffer.clone(), - completions.into(), - snippet_sort_order, - ); - - menu.filter( - if filter_completions { - query.as_deref() - } else { - None - }, - cx.background_executor().clone(), - ) - .await; - - menu.visible().then_some(menu) - }; - - editor.update_in(cx, |editor, window, cx| { - match editor.context_menu.borrow().as_ref() { - None => {} - Some(CodeContextMenu::Completions(prev_menu)) => { - if prev_menu.id > id { - return; - } - } - _ => return, - } - - if editor.focus_handle.is_focused(window) && menu.is_some() { - let mut menu = menu.unwrap(); - menu.resolve_visible_completions(editor.completion_provider.as_deref(), cx); - - *editor.context_menu.borrow_mut() = - Some(CodeContextMenu::Completions(menu)); - - if editor.show_edit_predictions_in_menu() { - editor.update_visible_inline_completion(window, cx); - } else { - editor.discard_inline_completion(false, cx); - } - - cx.notify(); - } else 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 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_inline_completion(window, cx); - } - } - })?; - - anyhow::Ok(()) - } - .log_err() - .await - }); - - self.completion_tasks.push((id, task)); - } - - #[cfg(feature = "test-support")] - pub fn current_completions(&self) -> Option> { - let menu = self.context_menu.borrow(); - if let CodeContextMenu::Completions(menu) = menu.as_ref()? { - let completions = menu.completions.borrow(); - Some(completions.to_vec()) - } else { - None - } - } - - pub fn confirm_completion( - &mut self, - action: &ConfirmCompletion, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.do_completion(action.item_ix, CompletionIntent::Complete, window, cx) - } - - pub fn confirm_completion_insert( - &mut self, - _: &ConfirmCompletionInsert, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.do_completion(None, CompletionIntent::CompleteWithInsert, window, cx) - } - - pub fn confirm_completion_replace( - &mut self, - _: &ConfirmCompletionReplace, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.do_completion(None, CompletionIntent::CompleteWithReplace, window, cx) - } - - pub fn compose_completion( - &mut self, - action: &ComposeCompletion, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.do_completion(action.item_ix, CompletionIntent::Compose, window, cx) - } - - fn do_completion( - &mut self, - item_ix: Option, - intent: CompletionIntent, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - use language::ToOffset as _; - - let CodeContextMenu::Completions(completions_menu) = self.hide_context_menu(window, cx)? - else { - return None; - }; - - let candidate_id = { - 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_inline_completion(true, cx); - } - mat.candidate_id - }; - - let buffer_handle = completions_menu.buffer; - let completion = completions_menu - .completions - .borrow() - .get(candidate_id)? - .clone(); - cx.stop_propagation(); - - let snippet; - let new_text; - if completion.is_snippet() { - snippet = Some(Snippet::parse(&completion.new_text).log_err()?); - new_text = snippet.as_ref().unwrap().text.clone(); - } else { - snippet = None; - new_text = completion.new_text.clone(); - }; - - let replace_range = choose_completion_range(&completion, intent, &buffer_handle, cx); - let buffer = buffer_handle.read(cx); - let snapshot = self.buffer.read(cx).snapshot(cx); - let replace_range_multibuffer = { - let excerpt = snapshot - .excerpt_containing(self.selections.newest_anchor().range()) - .unwrap(); - let multibuffer_anchor = snapshot - .anchor_in_excerpt(excerpt.id(), buffer.anchor_before(replace_range.start)) - .unwrap() - ..snapshot - .anchor_in_excerpt(excerpt.id(), buffer.anchor_before(replace_range.end)) - .unwrap(); - multibuffer_anchor.start.to_offset(&snapshot) - ..multibuffer_anchor.end.to_offset(&snapshot) - }; - let newest_anchor = self.selections.newest_anchor(); - if newest_anchor.head().buffer_id != Some(buffer.remote_id()) { - return None; - } - - let old_text = buffer - .text_for_range(replace_range.clone()) - .collect::(); - let lookbehind = newest_anchor - .start - .text_anchor - .to_offset(buffer) - .saturating_sub(replace_range.start); - let lookahead = replace_range - .end - .saturating_sub(newest_anchor.end.text_anchor.to_offset(buffer)); - let prefix = &old_text[..old_text.len().saturating_sub(lookahead)]; - let suffix = &old_text[lookbehind.min(old_text.len())..]; - - let selections = self.selections.all::(cx); - let mut ranges = Vec::new(); - let mut linked_edits = HashMap::<_, Vec<_>>::default(); - - for selection in &selections { - let range = if selection.id == newest_anchor.id { - replace_range_multibuffer.clone() - } else { - let mut range = selection.range(); - - // if prefix is present, don't duplicate it - if snapshot.contains_str_at(range.start.saturating_sub(lookbehind), prefix) { - range.start = range.start.saturating_sub(lookbehind); - - // if suffix is also present, mimic the newest cursor and replace it - if selection.id != newest_anchor.id - && snapshot.contains_str_at(range.end, suffix) - { - range.end += lookahead; - } - } - range - }; - - ranges.push(range); - - if !self.linked_edit_ranges.is_empty() { - let start_anchor = snapshot.anchor_before(selection.head()); - let end_anchor = snapshot.anchor_after(selection.tail()); - if let Some(ranges) = self - .linked_editing_ranges_for(start_anchor.text_anchor..end_anchor.text_anchor, cx) - { - for (buffer, edits) in ranges { - linked_edits - .entry(buffer.clone()) - .or_default() - .extend(edits.into_iter().map(|range| (range, new_text.to_owned()))); - } - } - } - } - - cx.emit(EditorEvent::InputHandled { - utf16_range_to_replace: None, - text: new_text.clone().into(), - }); - - self.transact(window, cx, |this, window, cx| { - if let Some(mut snippet) = snippet { - snippet.text = new_text.to_string(); - this.insert_snippet(&ranges, snippet, window, cx).log_err(); - } else { - this.buffer.update(cx, |buffer, cx| { - let auto_indent = match completion.insert_text_mode { - Some(InsertTextMode::AS_IS) => None, - _ => this.autoindent_mode.clone(), - }; - let edits = ranges.into_iter().map(|range| (range, new_text.as_str())); - buffer.edit(edits, auto_indent, cx); - }); - } - for (buffer, edits) in linked_edits { - buffer.update(cx, |buffer, cx| { - let snapshot = buffer.snapshot(); - let edits = edits - .into_iter() - .map(|(range, text)| { - use text::ToPoint as TP; - let end_point = TP::to_point(&range.end, &snapshot); - let start_point = TP::to_point(&range.start, &snapshot); - (start_point..end_point, text) - }) - .sorted_by_key(|(range, _)| range.start); - buffer.edit(edits, None, cx); - }) - } - - this.refresh_inline_completion(true, false, window, cx); - }); - - let show_new_completions_on_confirm = completion - .confirm - .as_ref() - .map_or(false, |confirm| confirm(intent, window, cx)); - if show_new_completions_on_confirm { - self.show_completions(&ShowCompletions { trigger: None }, window, cx); - } - - let provider = self.completion_provider.as_ref()?; - drop(completion); - let apply_edits = provider.apply_additional_edits_for_completion( - buffer_handle, - completions_menu.completions.clone(), - candidate_id, - true, - cx, - ); - - let editor_settings = EditorSettings::get_global(cx); - if editor_settings.show_signature_help_after_edits || editor_settings.auto_signature_help { - // After the code completion is finished, users often want to know what signatures are needed. - // so we should automatically call signature_help - self.show_signature_help(&ShowSignatureHelp, window, cx); - } - - Some(cx.foreground_executor().spawn(async move { - apply_edits.await?; - Ok(()) - })) - } - - pub fn toggle_code_actions( - &mut self, - action: &ToggleCodeActions, - window: &mut Window, - cx: &mut Context, - ) { - let quick_launch = action.quick_launch; - let mut context_menu = self.context_menu.borrow_mut(); - if let Some(CodeContextMenu::CodeActions(code_actions)) = context_menu.as_ref() { - if code_actions.deployed_from_indicator == action.deployed_from_indicator { - // Toggle if we're selecting the same one - *context_menu = None; - cx.notify(); - return; - } else { - // Otherwise, clear it and start a new one - *context_menu = None; - cx.notify(); - } - } - drop(context_menu); - let snapshot = self.snapshot(window, cx); - let deployed_from_indicator = action.deployed_from_indicator; - let mut task = self.code_actions_task.take(); - let action = action.clone(); - cx.spawn_in(window, async move |editor, cx| { - while let Some(prev_task) = task { - prev_task.await.log_err(); - task = editor.update(cx, |this, _| this.code_actions_task.take())?; - } - - let spawned_test_task = editor.update_in(cx, |editor, window, cx| { - if editor.focus_handle.is_focused(window) { - let multibuffer_point = action - .deployed_from_indicator - .map(|row| DisplayPoint::new(row, 0).to_point(&snapshot)) - .unwrap_or_else(|| editor.selections.newest::(cx).head()); - let (buffer, buffer_row) = snapshot - .buffer_snapshot - .buffer_line_for_row(MultiBufferRow(multibuffer_point.row)) - .and_then(|(buffer_snapshot, range)| { - editor - .buffer - .read(cx) - .buffer(buffer_snapshot.remote_id()) - .map(|buffer| (buffer, range.start.row)) - })?; - let (_, code_actions) = editor - .available_code_actions - .clone() - .and_then(|(location, code_actions)| { - let snapshot = location.buffer.read(cx).snapshot(); - let point_range = location.range.to_point(&snapshot); - let point_range = point_range.start.row..=point_range.end.row; - if point_range.contains(&buffer_row) { - Some((location, code_actions)) - } else { - None - } - }) - .unzip(); - let buffer_id = buffer.read(cx).remote_id(); - let tasks = editor - .tasks - .get(&(buffer_id, buffer_row)) - .map(|t| Arc::new(t.to_owned())); - if tasks.is_none() && code_actions.is_none() { - return None; - } - - editor.completion_tasks.clear(); - editor.discard_inline_completion(false, cx); - let task_context = - tasks - .as_ref() - .zip(editor.project.clone()) - .map(|(tasks, project)| { - Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx) - }); - - Some(cx.spawn_in(window, async move |editor, cx| { - let task_context = match task_context { - Some(task_context) => task_context.await, - None => None, - }; - let resolved_tasks = - tasks - .zip(task_context.clone()) - .map(|(tasks, task_context)| ResolvedTasks { - templates: tasks.resolve(&task_context).collect(), - position: snapshot.buffer_snapshot.anchor_before(Point::new( - multibuffer_point.row, - tasks.column, - )), - }); - let spawn_straight_away = quick_launch - && resolved_tasks - .as_ref() - .map_or(false, |tasks| tasks.templates.len() == 1) - && code_actions - .as_ref() - .map_or(true, |actions| actions.is_empty()); - let debug_scenarios = editor.update(cx, |editor, cx| { - if cx.has_flag::() { - maybe!({ - let project = editor.project.as_ref()?; - let dap_store = project.read(cx).dap_store(); - let mut scenarios = vec![]; - let resolved_tasks = resolved_tasks.as_ref()?; - let debug_adapter: SharedString = buffer - .read(cx) - .language()? - .context_provider()? - .debug_adapter()? - .into(); - dap_store.update(cx, |this, cx| { - for (_, task) in &resolved_tasks.templates { - if let Some(scenario) = this - .debug_scenario_for_build_task( - task.resolved.clone(), - SharedString::from( - task.original_task().label.clone(), - ), - debug_adapter.clone(), - cx, - ) - { - scenarios.push(scenario); - } - } - }); - Some(scenarios) - }) - .unwrap_or_default() - } else { - vec![] - } - })?; - if let Ok(task) = editor.update_in(cx, |editor, window, cx| { - *editor.context_menu.borrow_mut() = - Some(CodeContextMenu::CodeActions(CodeActionsMenu { - buffer, - actions: CodeActionContents::new( - resolved_tasks, - code_actions, - debug_scenarios, - task_context.unwrap_or_default(), - ), - selected_item: Default::default(), - scroll_handle: UniformListScrollHandle::default(), - deployed_from_indicator, - })); - if spawn_straight_away { - if let Some(task) = editor.confirm_code_action( - &ConfirmCodeAction { item_ix: Some(0) }, - window, - cx, - ) { - cx.notify(); - return task; - } - } - cx.notify(); - Task::ready(Ok(())) - }) { - task.await - } else { - Ok(()) - } - })) - } else { - Some(Task::ready(Ok(()))) - } - })?; - if let Some(task) = spawned_test_task { - task.await?; - } - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - - pub fn confirm_code_action( - &mut self, - action: &ConfirmCodeAction, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let actions_menu = - if let CodeContextMenu::CodeActions(menu) = self.hide_context_menu(window, cx)? { - menu - } else { - return None; - }; - - let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item); - let action = actions_menu.actions.get(action_ix)?; - let title = action.label(); - let buffer = actions_menu.buffer; - let workspace = self.workspace()?; - - match action { - CodeActionsItem::Task(task_source_kind, resolved_task) => { - workspace.update(cx, |workspace, cx| { - workspace.schedule_resolved_task( - task_source_kind, - resolved_task, - false, - window, - cx, - ); - - Some(Task::ready(Ok(()))) - }) - } - CodeActionsItem::CodeAction { - excerpt_id, - action, - provider, - } => { - let apply_code_action = - provider.apply_code_action(buffer, action, excerpt_id, true, window, cx); - let workspace = workspace.downgrade(); - Some(cx.spawn_in(window, async move |editor, cx| { - let project_transaction = apply_code_action.await?; - Self::open_project_transaction( - &editor, - workspace, - project_transaction, - title, - cx, - ) - .await - })) - } - CodeActionsItem::DebugScenario(scenario) => { - let context = actions_menu.actions.context.clone(); - - workspace.update(cx, |workspace, cx| { - workspace.start_debug_session(scenario, context, Some(buffer), window, cx); - }); - Some(Task::ready(Ok(()))) - } - } - } - - pub async fn open_project_transaction( - this: &WeakEntity, - workspace: WeakEntity, - transaction: ProjectTransaction, - title: String, - cx: &mut AsyncWindowContext, - ) -> Result<()> { - let mut entries = transaction.0.into_iter().collect::>(); - cx.update(|_, cx| { - entries.sort_unstable_by_key(|(buffer, _)| { - buffer.read(cx).file().map(|f| f.path().clone()) - }); - })?; - - // If the project transaction's edits are all contained within this editor, then - // avoid opening a new editor to display them. - - if let Some((buffer, transaction)) = entries.first() { - if entries.len() == 1 { - 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 { - 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(()); - } - } - } - } - } else { - return Ok(()); - } - - let mut ranges_to_highlight = Vec::new(); - let excerpt_buffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::new(Capability::ReadWrite).with_title(title); - for (buffer_handle, transaction) in &entries { - let edited_ranges = buffer_handle - .read(cx) - .edited_ranges_for_transaction::(transaction) - .collect::>(); - let (ranges, _) = multibuffer.set_excerpts_for_path( - PathKey::for_buffer(buffer_handle, cx), - buffer_handle.clone(), - edited_ranges, - DEFAULT_MULTIBUFFER_CONTEXT, - cx, - ); - - ranges_to_highlight.extend(ranges); - } - multibuffer.push_transaction(entries.iter().map(|(b, t)| (b, t)), cx); - multibuffer - })?; - - workspace.update_in(cx, |workspace, window, cx| { - let project = workspace.project().clone(); - let editor = - cx.new(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), window, cx)); - workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); - editor.update(cx, |editor, cx| { - editor.highlight_background::( - &ranges_to_highlight, - |theme| theme.editor_highlighted_line_background, - cx, - ); - }); - })?; - - Ok(()) - } - - pub fn clear_code_action_providers(&mut self) { - self.code_action_providers.clear(); - self.available_code_actions.take(); - } - - pub fn add_code_action_provider( - &mut self, - provider: Rc, - window: &mut Window, - cx: &mut Context, - ) { - if self - .code_action_providers - .iter() - .any(|existing_provider| existing_provider.id() == provider.id()) - { - return; - } - - self.code_action_providers.push(provider); - self.refresh_code_actions(window, cx); - } - - pub fn remove_code_action_provider( - &mut self, - id: Arc, - window: &mut Window, - cx: &mut Context, - ) { - self.code_action_providers - .retain(|provider| provider.id() != id); - self.refresh_code_actions(window, cx); - } - - 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).clone(); - let buffer = self.buffer.read(cx); - if newest_selection.head().diff_base_anchor.is_some() { - return None; - } - let (start_buffer, start) = - buffer.text_anchor_for_position(newest_selection_adjusted.start, cx)?; - let (end_buffer, end) = - buffer.text_anchor_for_position(newest_selection_adjusted.end, cx)?; - if start_buffer != end_buffer { - return None; - } - - self.code_actions_task = Some(cx.spawn_in(window, async move |this, cx| { - cx.background_executor() - .timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT) - .await; - - let (providers, tasks) = this.update_in(cx, |this, window, cx| { - let providers = this.code_action_providers.clone(); - let tasks = this - .code_action_providers - .iter() - .map(|provider| provider.code_actions(&start_buffer, start..end, window, cx)) - .collect::>(); - (providers, tasks) - })?; - - let mut actions = Vec::new(); - for (provider, provider_actions) in - providers.into_iter().zip(future::join_all(tasks).await) - { - if let Some(provider_actions) = provider_actions.log_err() { - actions.extend(provider_actions.into_iter().map(|action| { - AvailableCodeAction { - excerpt_id: newest_selection.start.excerpt_id, - action, - provider: provider.clone(), - } - })); - } - } - - this.update(cx, |this, cx| { - this.available_code_actions = if actions.is_empty() { - None - } else { - Some(( - Location { - buffer: start_buffer, - range: start..end, - }, - actions.into(), - )) - }; - cx.notify(); - }) - })); - None - } - - fn start_inline_blame_timer(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(delay) = ProjectSettings::get_global(cx).git.inline_blame_delay() { - self.show_git_blame_inline = false; - - self.show_git_blame_inline_delay_task = - Some(cx.spawn_in(window, async move |this, cx| { - cx.background_executor().timer(delay).await; - - this.update(cx, |this, cx| { - this.show_git_blame_inline = true; - cx.notify(); - }) - .log_err(); - })); - } - } - - fn show_blame_popover( - &mut self, - blame_entry: &BlameEntry, - position: gpui::Point, - cx: &mut Context, - ) { - if let Some(state) = &mut self.inline_blame_popover { - state.hide_task.take(); - cx.notify(); - } else { - let delay = EditorSettings::get_global(cx).hover_popover_delay; - let show_task = cx.spawn(async move |editor, cx| { - cx.background_executor() - .timer(std::time::Duration::from_millis(delay)) - .await; - editor - .update(cx, |editor, cx| { - if let Some(state) = &mut editor.inline_blame_popover { - state.show_task = None; - cx.notify(); - } - }) - .ok(); - }); - let Some(blame) = self.blame.as_ref() else { - return; - }; - let blame = blame.read(cx); - let details = blame.details_for_entry(&blame_entry); - let markdown = cx.new(|cx| { - Markdown::new( - details - .as_ref() - .map(|message| message.message.clone()) - .unwrap_or_default(), - None, - None, - cx, - ) - }); - self.inline_blame_popover = Some(InlineBlamePopover { - position, - show_task: Some(show_task), - hide_task: None, - popover_bounds: None, - popover_state: InlineBlamePopoverState { - scroll_handle: ScrollHandle::new(), - commit_message: details, - markdown, - }, - }); - } - } - - fn hide_blame_popover(&mut self, cx: &mut Context) { - if let Some(state) = &mut self.inline_blame_popover { - if state.show_task.is_some() { - self.inline_blame_popover.take(); - cx.notify(); - } else { - let hide_task = cx.spawn(async move |editor, cx| { - cx.background_executor() - .timer(std::time::Duration::from_millis(100)) - .await; - editor - .update(cx, |editor, cx| { - editor.inline_blame_popover.take(); - cx.notify(); - }) - .ok(); - }); - state.hide_task = Some(hide_task); - } - } - } - - fn refresh_document_highlights(&mut self, cx: &mut Context) -> Option<()> { - if self.pending_rename.is_some() { - return None; - } - - let provider = self.semantics_provider.clone()?; - let buffer = self.buffer.read(cx); - let newest_selection = self.selections.newest_anchor().clone(); - let cursor_position = newest_selection.head(); - let (cursor_buffer, cursor_buffer_position) = - buffer.text_anchor_for_position(cursor_position, cx)?; - let (tail_buffer, _) = buffer.text_anchor_for_position(newest_selection.tail(), cx)?; - if cursor_buffer != tail_buffer { - return None; - } - let debounce = EditorSettings::get_global(cx).lsp_highlight_debounce; - self.document_highlights_task = Some(cx.spawn(async move |this, cx| { - cx.background_executor() - .timer(Duration::from_millis(debounce)) - .await; - - let highlights = if let Some(highlights) = cx - .update(|cx| { - provider.document_highlights(&cursor_buffer, cursor_buffer_position, cx) - }) - .ok() - .flatten() - { - highlights.await.log_err() - } else { - None - }; - - if let Some(highlights) = highlights { - this.update(cx, |this, cx| { - if this.pending_rename.is_some() { - return; - } - - let buffer_id = cursor_position.buffer_id; - let buffer = this.buffer.read(cx); - if !buffer - .text_anchor_for_position(cursor_position, cx) - .map_or(false, |(buffer, _)| buffer == cursor_buffer) - { - return; - } - - let cursor_buffer_snapshot = cursor_buffer.read(cx); - let mut write_ranges = Vec::new(); - let mut read_ranges = Vec::new(); - for highlight in highlights { - for (excerpt_id, excerpt_range) in - buffer.excerpts_for_buffer(cursor_buffer.read(cx).remote_id(), cx) - { - let start = highlight - .range - .start - .max(&excerpt_range.context.start, cursor_buffer_snapshot); - let end = highlight - .range - .end - .min(&excerpt_range.context.end, cursor_buffer_snapshot); - if start.cmp(&end, cursor_buffer_snapshot).is_ge() { - continue; - } - - let range = Anchor { - buffer_id, - excerpt_id, - text_anchor: start, - diff_base_anchor: None, - }..Anchor { - buffer_id, - excerpt_id, - text_anchor: end, - diff_base_anchor: None, - }; - if highlight.kind == lsp::DocumentHighlightKind::WRITE { - write_ranges.push(range); - } else { - read_ranges.push(range); - } - } - } - - this.highlight_background::( - &read_ranges, - |theme| theme.editor_document_highlight_read_background, - cx, - ); - this.highlight_background::( - &write_ranges, - |theme| theme.editor_document_highlight_write_background, - cx, - ); - cx.notify(); - }) - .log_err(); - } - })); - None - } - - fn prepare_highlight_query_from_selection( - &mut self, - cx: &mut Context, - ) -> Option<(String, Range)> { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - return None; - } - if !EditorSettings::get_global(cx).selection_highlight { - return None; - } - if self.selections.count() != 1 || self.selections.line_mode { - return None; - } - let selection = self.selections.newest::(cx); - if selection.is_empty() || selection.start.row != selection.end.row { - return None; - } - let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx); - let selection_anchor_range = selection.range().to_anchors(&multi_buffer_snapshot); - let query = multi_buffer_snapshot - .text_for_range(selection_anchor_range.clone()) - .collect::(); - if query.trim().is_empty() { - return None; - } - Some((query, selection_anchor_range)) - } - - fn update_selection_occurrence_highlights( - &mut self, - query_text: String, - query_range: Range, - multi_buffer_range_to_query: Range, - use_debounce: bool, - window: &mut Window, - cx: &mut Context, - ) -> Task<()> { - let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx); - cx.spawn_in(window, async move |editor, cx| { - if use_debounce { - cx.background_executor() - .timer(SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT) - .await; - } - let match_task = cx.background_spawn(async move { - let buffer_ranges = multi_buffer_snapshot - .range_to_buffer_ranges(multi_buffer_range_to_query) - .into_iter() - .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()); - let mut match_ranges = Vec::new(); - for (buffer_snapshot, search_range, excerpt_id) in buffer_ranges { - match_ranges.extend( - project::search::SearchQuery::text( - query_text.clone(), - false, - false, - false, - Default::default(), - Default::default(), - false, - None, - ) - .unwrap() - .search(&buffer_snapshot, Some(search_range.clone())) - .await - .into_iter() - .filter_map(|match_range| { - let match_start = buffer_snapshot - .anchor_after(search_range.start + match_range.start); - let match_end = - buffer_snapshot.anchor_before(search_range.start + match_range.end); - let match_anchor_range = Anchor::range_in_buffer( - excerpt_id, - buffer_snapshot.remote_id(), - match_start..match_end, - ); - (match_anchor_range != query_range).then_some(match_anchor_range) - }), - ); - } - match_ranges - }); - let match_ranges = match_task.await; - editor - .update_in(cx, |editor, _, cx| { - editor.clear_background_highlights::(cx); - if !match_ranges.is_empty() { - editor.highlight_background::( - &match_ranges, - |theme| theme.editor_document_highlight_bracket_background, - cx, - ) - } - }) - .log_err(); - }) - } - - fn refresh_selected_text_highlights( - &mut self, - on_buffer_edit: bool, - window: &mut Window, - cx: &mut Context, - ) { - let Some((query_text, query_range)) = self.prepare_highlight_query_from_selection(cx) - else { - self.clear_background_highlights::(cx); - self.quick_selection_highlight_task.take(); - self.debounced_selection_highlight_task.take(); - return; - }; - let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx); - if on_buffer_edit - || self - .quick_selection_highlight_task - .as_ref() - .map_or(true, |(prev_anchor_range, _)| { - prev_anchor_range != &query_range - }) - { - let multi_buffer_visible_start = self - .scroll_manager - .anchor() - .anchor - .to_point(&multi_buffer_snapshot); - let multi_buffer_visible_end = multi_buffer_snapshot.clip_point( - multi_buffer_visible_start - + Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0), - Bias::Left, - ); - let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end; - self.quick_selection_highlight_task = Some(( - query_range.clone(), - self.update_selection_occurrence_highlights( - query_text.clone(), - query_range.clone(), - multi_buffer_visible_range, - false, - window, - cx, - ), - )); - } - if on_buffer_edit - || self - .debounced_selection_highlight_task - .as_ref() - .map_or(true, |(prev_anchor_range, _)| { - prev_anchor_range != &query_range - }) - { - let multi_buffer_start = multi_buffer_snapshot - .anchor_before(0) - .to_point(&multi_buffer_snapshot); - let multi_buffer_end = multi_buffer_snapshot - .anchor_after(multi_buffer_snapshot.len()) - .to_point(&multi_buffer_snapshot); - let multi_buffer_full_range = multi_buffer_start..multi_buffer_end; - self.debounced_selection_highlight_task = Some(( - query_range.clone(), - self.update_selection_occurrence_highlights( - query_text, - query_range, - multi_buffer_full_range, - true, - window, - cx, - ), - )); - } - } - - pub fn refresh_inline_completion( - &mut self, - debounce: bool, - user_requested: bool, - window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - let provider = self.edit_prediction_provider()?; - let cursor = self.selections.newest_anchor().head(); - let (buffer, cursor_buffer_position) = - self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; - - if !self.edit_predictions_enabled_in_buffer(&buffer, cursor_buffer_position, cx) { - self.discard_inline_completion(false, cx); - return None; - } - - if !user_requested - && (!self.should_show_edit_predictions() - || !self.is_focused(window) - || buffer.read(cx).is_empty()) - { - self.discard_inline_completion(false, cx); - return None; - } - - self.update_visible_inline_completion(window, cx); - provider.refresh( - self.project.clone(), - buffer, - cursor_buffer_position, - debounce, - cx, - ); - Some(()) - } - - fn show_edit_predictions_in_menu(&self) -> bool { - match self.edit_prediction_settings { - EditPredictionSettings::Disabled => false, - EditPredictionSettings::Enabled { show_in_menu, .. } => show_in_menu, - } - } - - pub fn edit_predictions_enabled(&self) -> bool { - match self.edit_prediction_settings { - EditPredictionSettings::Disabled => false, - EditPredictionSettings::Enabled { .. } => true, - } - } - - fn edit_prediction_requires_modifier(&self) -> bool { - match self.edit_prediction_settings { - EditPredictionSettings::Disabled => false, - EditPredictionSettings::Enabled { - preview_requires_modifier, - .. - } => preview_requires_modifier, - } - } - - pub fn update_edit_prediction_settings(&mut self, cx: &mut Context) { - if self.edit_prediction_provider.is_none() { - self.edit_prediction_settings = EditPredictionSettings::Disabled; - } else { - let selection = self.selections.newest_anchor(); - let cursor = selection.head(); - - if let Some((buffer, cursor_buffer_position)) = - self.buffer.read(cx).text_anchor_for_position(cursor, cx) - { - self.edit_prediction_settings = - self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx); - } - } - } - - fn edit_prediction_settings_at_position( - &self, - buffer: &Entity, - buffer_position: language::Anchor, - cx: &App, - ) -> EditPredictionSettings { - if !self.mode.is_full() - || !self.show_inline_completions_override.unwrap_or(true) - || self.inline_completions_disabled_in_scope(buffer, buffer_position, cx) - { - return EditPredictionSettings::Disabled; - } - - let buffer = buffer.read(cx); - - let file = buffer.file(); - - if !language_settings(buffer.language().map(|l| l.name()), file, cx).show_edit_predictions { - return EditPredictionSettings::Disabled; - }; - - let by_provider = matches!( - self.menu_inline_completions_policy, - MenuInlineCompletionsPolicy::ByProvider - ); - - let show_in_menu = by_provider - && self - .edit_prediction_provider - .as_ref() - .map_or(false, |provider| { - provider.provider.show_completions_in_menu() - }); - - let preview_requires_modifier = - all_language_settings(file, cx).edit_predictions_mode() == EditPredictionsMode::Subtle; - - EditPredictionSettings::Enabled { - show_in_menu, - preview_requires_modifier, - } - } - - fn should_show_edit_predictions(&self) -> bool { - self.snippet_stack.is_empty() && self.edit_predictions_enabled() - } - - pub fn edit_prediction_preview_is_active(&self) -> bool { - matches!( - self.edit_prediction_preview, - EditPredictionPreview::Active { .. } - ) - } - - pub fn edit_predictions_enabled_at_cursor(&self, cx: &App) -> bool { - let cursor = self.selections.newest_anchor().head(); - if let Some((buffer, cursor_position)) = - self.buffer.read(cx).text_anchor_for_position(cursor, cx) - { - self.edit_predictions_enabled_in_buffer(&buffer, cursor_position, cx) - } else { - false - } - } - - fn edit_predictions_enabled_in_buffer( - &self, - buffer: &Entity, - buffer_position: language::Anchor, - cx: &App, - ) -> bool { - maybe!({ - if self.read_only(cx) { - return Some(false); - } - let provider = self.edit_prediction_provider()?; - if !provider.is_enabled(&buffer, buffer_position, cx) { - return Some(false); - } - let buffer = buffer.read(cx); - let Some(file) = buffer.file() else { - return Some(true); - }; - let settings = all_language_settings(Some(file), cx); - Some(settings.edit_predictions_enabled_for_file(file, cx)) - }) - .unwrap_or(false) - } - - fn cycle_inline_completion( - &mut self, - direction: Direction, - window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - let provider = self.edit_prediction_provider()?; - let cursor = self.selections.newest_anchor().head(); - let (buffer, cursor_buffer_position) = - self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; - if self.inline_completions_hidden_for_vim_mode || !self.should_show_edit_predictions() { - return None; - } - - provider.cycle(buffer, cursor_buffer_position, direction, cx); - self.update_visible_inline_completion(window, cx); - - Some(()) - } - - pub fn show_inline_completion( - &mut self, - _: &ShowEditPrediction, - window: &mut Window, - cx: &mut Context, - ) { - if !self.has_active_inline_completion() { - self.refresh_inline_completion(false, true, window, cx); - return; - } - - self.update_visible_inline_completion(window, cx); - } - - pub fn display_cursor_names( - &mut self, - _: &DisplayCursorNames, - window: &mut Window, - cx: &mut Context, - ) { - self.show_cursor_names(window, cx); - } - - fn show_cursor_names(&mut self, window: &mut Window, cx: &mut Context) { - self.show_cursor_names = true; - cx.notify(); - cx.spawn_in(window, async move |this, cx| { - cx.background_executor().timer(CURSORS_VISIBLE_FOR).await; - this.update(cx, |this, cx| { - this.show_cursor_names = false; - cx.notify() - }) - .ok() - }) - .detach(); - } - - pub fn next_edit_prediction( - &mut self, - _: &NextEditPrediction, - window: &mut Window, - cx: &mut Context, - ) { - if self.has_active_inline_completion() { - self.cycle_inline_completion(Direction::Next, window, cx); - } else { - let is_copilot_disabled = self - .refresh_inline_completion(false, true, window, cx) - .is_none(); - if is_copilot_disabled { - cx.propagate(); - } - } - } - - pub fn previous_edit_prediction( - &mut self, - _: &PreviousEditPrediction, - window: &mut Window, - cx: &mut Context, - ) { - if self.has_active_inline_completion() { - self.cycle_inline_completion(Direction::Prev, window, cx); - } else { - let is_copilot_disabled = self - .refresh_inline_completion(false, true, window, cx) - .is_none(); - if is_copilot_disabled { - cx.propagate(); - } - } - } - - pub fn accept_edit_prediction( - &mut self, - _: &AcceptEditPrediction, - window: &mut Window, - cx: &mut Context, - ) { - if self.show_edit_predictions_in_menu() { - self.hide_context_menu(window, cx); - } - - let Some(active_inline_completion) = self.active_inline_completion.as_ref() else { - return; - }; - - self.report_inline_completion_event( - active_inline_completion.completion_id.clone(), - true, - cx, - ); - - match &active_inline_completion.completion { - InlineCompletion::Move { target, .. } => { - let target = *target; - - if let Some(position_map) = &self.last_position_map { - if position_map - .visible_row_range - .contains(&target.to_display_point(&position_map.snapshot).row()) - || !self.edit_prediction_requires_modifier() - { - self.unfold_ranges(&[target..target], true, false, cx); - // Note that this is also done in vim's handler of the Tab action. - self.change_selections( - Some(Autoscroll::newest()), - window, - cx, - |selections| { - selections.select_anchor_ranges([target..target]); - }, - ); - self.clear_row_highlights::(); - - self.edit_prediction_preview - .set_previous_scroll_position(None); - } else { - self.edit_prediction_preview - .set_previous_scroll_position(Some( - position_map.snapshot.scroll_anchor, - )); - - self.highlight_rows::( - target..target, - cx.theme().colors().editor_highlighted_line_background, - RowHighlightOptions { - autoscroll: true, - ..Default::default() - }, - cx, - ); - self.request_autoscroll(Autoscroll::fit(), cx); - } - } - } - InlineCompletion::Edit { edits, .. } => { - if let Some(provider) = self.edit_prediction_provider() { - provider.accept(cx); - } - - let snapshot = self.buffer.read(cx).snapshot(cx); - let last_edit_end = edits.last().unwrap().0.end.bias_right(&snapshot); - - self.buffer.update(cx, |buffer, cx| { - buffer.edit(edits.iter().cloned(), None, cx) - }); - - self.change_selections(None, window, cx, |s| { - s.select_anchor_ranges([last_edit_end..last_edit_end]) - }); - - self.update_visible_inline_completion(window, cx); - if self.active_inline_completion.is_none() { - self.refresh_inline_completion(true, true, window, cx); - } - - cx.notify(); - } - } - - self.edit_prediction_requires_modifier_in_indent_conflict = false; - } - - pub fn accept_partial_inline_completion( - &mut self, - _: &AcceptPartialEditPrediction, - window: &mut Window, - cx: &mut Context, - ) { - let Some(active_inline_completion) = self.active_inline_completion.as_ref() else { - return; - }; - if self.selections.count() != 1 { - return; - } - - self.report_inline_completion_event( - active_inline_completion.completion_id.clone(), - true, - cx, - ); - - match &active_inline_completion.completion { - InlineCompletion::Move { target, .. } => { - let target = *target; - self.change_selections(Some(Autoscroll::newest()), window, cx, |selections| { - selections.select_anchor_ranges([target..target]); - }); - } - 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(); - let insertion = edits.iter().find_map(|(range, text)| { - let range = range.to_offset(&snapshot); - if range.is_empty() && range.start == cursor_offset { - Some(text) - } else { - None - } - }); - - if let Some(text) = insertion { - let mut partial_completion = text - .chars() - .by_ref() - .take_while(|c| c.is_alphabetic()) - .collect::(); - if partial_completion.is_empty() { - partial_completion = text - .chars() - .by_ref() - .take_while(|c| c.is_whitespace() || !c.is_alphabetic()) - .collect::(); - } - - cx.emit(EditorEvent::InputHandled { - utf16_range_to_replace: None, - text: partial_completion.clone().into(), - }); - - self.insert_with_autoindent_mode(&partial_completion, None, window, cx); - - self.refresh_inline_completion(true, true, window, cx); - cx.notify(); - } else { - self.accept_edit_prediction(&Default::default(), window, cx); - } - } - } - } - - fn discard_inline_completion( - &mut self, - should_report_inline_completion_event: bool, - cx: &mut Context, - ) -> bool { - if should_report_inline_completion_event { - let completion_id = self - .active_inline_completion - .as_ref() - .and_then(|active_completion| active_completion.completion_id.clone()); - - self.report_inline_completion_event(completion_id, false, cx); - } - - if let Some(provider) = self.edit_prediction_provider() { - provider.discard(cx); - } - - self.take_active_inline_completion(cx) - } - - fn report_inline_completion_event(&self, id: Option, accepted: bool, cx: &App) { - let Some(provider) = self.edit_prediction_provider() else { - return; - }; - - let Some((_, buffer, _)) = self - .buffer - .read(cx) - .excerpt_containing(self.selections.newest_anchor().head(), cx) - else { - return; - }; - - let extension = buffer - .read(cx) - .file() - .and_then(|file| Some(file.path().extension()?.to_string_lossy().to_string())); - - let event_type = match accepted { - true => "Edit Prediction Accepted", - false => "Edit Prediction Discarded", - }; - telemetry::event!( - event_type, - provider = provider.name(), - prediction_id = id, - suggestion_accepted = accepted, - file_extension = extension, - ); - } - - pub fn has_active_inline_completion(&self) -> bool { - self.active_inline_completion.is_some() - } - - 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_inline_completion.inlay_ids, Default::default(), cx); - self.clear_highlights::(cx); - self.stale_inline_completion_in_menu = Some(active_inline_completion); - true - } - - /// Returns true when we're displaying the edit prediction popover below the cursor - /// like we are not previewing and the LSP autocomplete menu is visible - /// or we are in `when_holding_modifier` mode. - pub fn edit_prediction_visible_in_cursor_popover(&self, has_completion: bool) -> bool { - if self.edit_prediction_preview_is_active() - || !self.show_edit_predictions_in_menu() - || !self.edit_predictions_enabled() - { - return false; - } - - if self.has_visible_completions_menu() { - return true; - } - - has_completion && self.edit_prediction_requires_modifier() - } - - fn handle_modifiers_changed( - &mut self, - modifiers: Modifiers, - position_map: &PositionMap, - window: &mut Window, - cx: &mut Context, - ) { - if self.show_edit_predictions_in_menu() { - self.update_edit_prediction_preview(&modifiers, window, cx); - } - - self.update_selection_mode(&modifiers, position_map, window, cx); - - let mouse_position = window.mouse_position(); - if !position_map.text_hitbox.is_hovered(window) { - return; - } - - self.update_hovered_link( - position_map.point_for_position(mouse_position), - &position_map.snapshot, - modifiers, - window, - cx, - ) - } - - fn update_selection_mode( - &mut self, - modifiers: &Modifiers, - position_map: &PositionMap, - window: &mut Window, - cx: &mut Context, - ) { - if modifiers != &COLUMNAR_SELECTION_MODIFIERS || self.selections.pending.is_none() { - return; - } - - let mouse_position = window.mouse_position(); - let point_for_position = position_map.point_for_position(mouse_position); - let position = point_for_position.previous_valid; - - self.select( - SelectPhase::BeginColumnar { - position, - reset: false, - goal_column: point_for_position.exact_unclipped.column(), - }, - window, - cx, - ); - } - - fn update_edit_prediction_preview( - &mut self, - modifiers: &Modifiers, - window: &mut Window, - cx: &mut Context, - ) { - let accept_keybind = self.accept_edit_prediction_keybind(window, cx); - let Some(accept_keystroke) = accept_keybind.keystroke() else { - return; - }; - - if &accept_keystroke.modifiers == modifiers && accept_keystroke.modifiers.modified() { - if matches!( - self.edit_prediction_preview, - EditPredictionPreview::Inactive { .. } - ) { - self.edit_prediction_preview = EditPredictionPreview::Active { - previous_scroll_position: None, - since: Instant::now(), - }; - - self.update_visible_inline_completion(window, cx); - cx.notify(); - } - } else if let EditPredictionPreview::Active { - previous_scroll_position, - since, - } = self.edit_prediction_preview - { - if let (Some(previous_scroll_position), Some(position_map)) = - (previous_scroll_position, self.last_position_map.as_ref()) - { - self.set_scroll_position( - previous_scroll_position - .scroll_position(&position_map.snapshot.display_snapshot), - window, - cx, - ); - } - - self.edit_prediction_preview = EditPredictionPreview::Inactive { - released_too_fast: since.elapsed() < Duration::from_millis(200), - }; - self.clear_row_highlights::(); - self.update_visible_inline_completion(window, cx); - cx.notify(); - } - } - - fn update_visible_inline_completion( - &mut self, - _window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - let selection = self.selections.newest_anchor(); - let cursor = selection.head(); - let multibuffer = self.buffer.read(cx).snapshot(cx); - let offset_selection = selection.map(|endpoint| endpoint.to_offset(&multibuffer)); - let excerpt_id = cursor.excerpt_id; - - 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_inline_completion())); - - if completions_menu_has_precedence - || !offset_selection.is_empty() - || self - .active_inline_completion - .as_ref() - .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_inline_completion(false, cx); - return None; - } - - self.take_active_inline_completion(cx); - let Some(provider) = self.edit_prediction_provider() else { - self.edit_prediction_settings = EditPredictionSettings::Disabled; - return None; - }; - - let (buffer, cursor_buffer_position) = - self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; - - self.edit_prediction_settings = - self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx); - - self.edit_prediction_indent_conflict = multibuffer.is_line_whitespace_upto(cursor); - - if self.edit_prediction_indent_conflict { - let cursor_point = cursor.to_point(&multibuffer); - - let indents = multibuffer.suggested_indents(cursor_point.row..cursor_point.row + 1, cx); - - if let Some((_, indent)) = indents.iter().next() { - if indent.len == cursor_point.column { - self.edit_prediction_indent_conflict = false; - } - } - } - - let inline_completion = provider.suggest(&buffer, cursor_buffer_position, cx)?; - let edits = inline_completion - .edits - .into_iter() - .flat_map(|(range, new_text)| { - let start = multibuffer.anchor_in_excerpt(excerpt_id, range.start)?; - let end = multibuffer.anchor_in_excerpt(excerpt_id, range.end)?; - Some((start..end, new_text)) - }) - .collect::>(); - if edits.is_empty() { - return None; - } - - let first_edit_start = edits.first().unwrap().0.start; - let first_edit_start_point = first_edit_start.to_point(&multibuffer); - let edit_start_row = first_edit_start_point.row.saturating_sub(2); - - let last_edit_end = edits.last().unwrap().0.end; - let last_edit_end_point = last_edit_end.to_point(&multibuffer); - let edit_end_row = cmp::min(multibuffer.max_point().row, last_edit_end_point.row + 2); - - let cursor_row = cursor.to_point(&multibuffer).row; - - let snapshot = multibuffer.buffer_for_excerpt(excerpt_id).cloned()?; - - let mut inlay_ids = Vec::new(); - let invalidation_row_range; - let move_invalidation_row_range = if cursor_row < edit_start_row { - Some(cursor_row..edit_end_row) - } else if cursor_row > edit_end_row { - Some(edit_start_row..cursor_row) - } else { - None - }; - 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; - InlineCompletion::Move { target, snapshot } - } else { - let show_completions_in_buffer = !self.edit_prediction_visible_in_cursor_popover(true) - && !self.inline_completions_hidden_for_vim_mode; - - if show_completions_in_buffer { - if edits - .iter() - .all(|(range, _)| range.to_offset(&multibuffer).is_empty()) - { - let mut inlays = Vec::new(); - for (range, new_text) in &edits { - let inlay = Inlay::inline_completion( - post_inc(&mut self.next_inlay_id), - range.start, - new_text.as_str(), - ); - inlay_ids.push(inlay.id); - inlays.push(inlay); - } - - self.splice_inlays(&[], inlays, cx); - } else { - let background_color = cx.theme().status().deleted_background; - self.highlight_text::( - edits.iter().map(|(range, _)| range.clone()).collect(), - HighlightStyle { - background_color: Some(background_color), - ..Default::default() - }, - cx, - ); - } - } - - invalidation_row_range = edit_start_row..edit_end_row; - - let display_mode = if all_edits_insertions_or_deletions(&edits, &multibuffer) { - if provider.show_tab_accept_marker() { - EditDisplayMode::TabAccept - } else { - EditDisplayMode::Inline - } - } else { - EditDisplayMode::DiffPopover - }; - - InlineCompletion::Edit { - edits, - edit_preview: inline_completion.edit_preview, - display_mode, - snapshot, - } - }; - - let invalidation_range = multibuffer - .anchor_before(Point::new(invalidation_row_range.start, 0)) - ..multibuffer.anchor_after(Point::new( - invalidation_row_range.end, - multibuffer.line_len(MultiBufferRow(invalidation_row_range.end)), - )); - - self.stale_inline_completion_in_menu = None; - self.active_inline_completion = Some(InlineCompletionState { - inlay_ids, - completion, - completion_id: inline_completion.id, - invalidation_range, - }); - - cx.notify(); - - Some(()) - } - - pub fn edit_prediction_provider(&self) -> Option> { - Some(self.edit_prediction_provider.as_ref()?.provider.clone()) - } - - fn render_code_actions_indicator( - &self, - _style: &EditorStyle, - row: DisplayRow, - is_active: bool, - breakpoint: Option<&(Anchor, Breakpoint)>, - cx: &mut Context, - ) -> Option { - let color = Color::Muted; - let position = breakpoint.as_ref().map(|(anchor, _)| *anchor); - let show_tooltip = !self.context_menu_visible(); - - if self.available_code_actions.is_some() { - Some( - IconButton::new("code_actions_indicator", ui::IconName::Bolt) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(color) - .toggle_state(is_active) - .when(show_tooltip, |this| { - this.tooltip({ - let focus_handle = self.focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Toggle Code Actions", - &ToggleCodeActions { - deployed_from_indicator: None, - quick_launch: false, - }, - &focus_handle, - window, - cx, - ) - } - }) - }) - .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_indicator: Some(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, - ); - })), - ) - } else { - None - } - } - - fn clear_tasks(&mut self) { - self.tasks.clear() - } - - fn insert_tasks(&mut self, key: (BufferId, BufferRow), value: RunnableTasks) { - if self.tasks.insert(key, value).is_some() { - // This case should hopefully be rare, but just in case... - log::error!( - "multiple different run targets found on a single line, only the last target will be rendered" - ) - } - } - - /// Get all display points of breakpoints that will be rendered within editor - /// - /// This function is used to handle overlaps between breakpoints and Code action/runner symbol. - /// It's also used to set the color of line numbers with breakpoints to the breakpoint color. - /// TODO debugger: Use this function to color toggle symbols that house nested breakpoints - fn active_breakpoints( - &self, - range: Range, - window: &mut Window, - cx: &mut Context, - ) -> HashMap { - let mut breakpoint_display_points = HashMap::default(); - - let Some(breakpoint_store) = self.breakpoint_store.clone() else { - return breakpoint_display_points; - }; - - let snapshot = self.snapshot(window, cx); - - let multi_buffer_snapshot = &snapshot.display_snapshot.buffer_snapshot; - let Some(project) = self.project.as_ref() else { - return breakpoint_display_points; - }; - - let range = snapshot.display_point_to_point(DisplayPoint::new(range.start, 0), Bias::Left) - ..snapshot.display_point_to_point(DisplayPoint::new(range.end, 0), Bias::Right); - - for (buffer_snapshot, range, excerpt_id) in - multi_buffer_snapshot.range_to_buffer_ranges(range) - { - let Some(buffer) = project.read_with(cx, |this, cx| { - this.buffer_for_id(buffer_snapshot.remote_id(), cx) - }) else { - continue; - }; - let breakpoints = breakpoint_store.read(cx).breakpoints( - &buffer, - Some( - buffer_snapshot.anchor_before(range.start) - ..buffer_snapshot.anchor_after(range.end), - ), - buffer_snapshot, - cx, - ); - for (anchor, breakpoint) in breakpoints { - let multi_buffer_anchor = - Anchor::in_buffer(excerpt_id, buffer_snapshot.remote_id(), *anchor); - let position = multi_buffer_anchor - .to_point(&multi_buffer_snapshot) - .to_display_point(&snapshot); - - breakpoint_display_points - .insert(position.row(), (multi_buffer_anchor, breakpoint.clone())); - } - } - - breakpoint_display_points - } - - fn breakpoint_context_menu( - &self, - anchor: Anchor, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let weak_editor = cx.weak_entity(); - let focus_handle = self.focus_handle(cx); - - let row = self - .buffer - .read(cx) - .snapshot(cx) - .summary_for_anchor::(&anchor) - .row; - - let breakpoint = self - .breakpoint_at_row(row, window, cx) - .map(|(anchor, bp)| (anchor, Arc::from(bp))); - - let log_breakpoint_msg = if breakpoint.as_ref().is_some_and(|bp| bp.1.message.is_some()) { - "Edit Log Breakpoint" - } else { - "Set Log Breakpoint" - }; - - let condition_breakpoint_msg = if breakpoint - .as_ref() - .is_some_and(|bp| bp.1.condition.is_some()) - { - "Edit Condition Breakpoint" - } else { - "Set Condition Breakpoint" - }; - - let hit_condition_breakpoint_msg = if breakpoint - .as_ref() - .is_some_and(|bp| bp.1.hit_condition.is_some()) - { - "Edit Hit Condition Breakpoint" - } else { - "Set Hit Condition Breakpoint" - }; - - let set_breakpoint_msg = if breakpoint.as_ref().is_some() { - "Unset Breakpoint" - } else { - "Set Breakpoint" - }; - - let run_to_cursor = command_palette_hooks::CommandPaletteFilter::try_global(cx) - .map_or(false, |filter| !filter.is_hidden(&DebuggerRunToCursor)); - - let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state { - BreakpointState::Enabled => Some("Disable"), - BreakpointState::Disabled => Some("Enable"), - }); - - let (anchor, breakpoint) = - breakpoint.unwrap_or_else(|| (anchor, Arc::new(Breakpoint::new_standard()))); - - ui::ContextMenu::build(window, cx, |menu, _, _cx| { - menu.on_blur_subscription(Subscription::new(|| {})) - .context(focus_handle) - .when(run_to_cursor, |this| { - let weak_editor = weak_editor.clone(); - this.entry("Run to cursor", None, move |window, cx| { - weak_editor - .update(cx, |editor, cx| { - editor.change_selections(None, window, cx, |s| { - s.select_ranges([Point::new(row, 0)..Point::new(row, 0)]) - }); - }) - .ok(); - - window.dispatch_action(Box::new(DebuggerRunToCursor), cx); - }) - .separator() - }) - .when_some(toggle_state_msg, |this, msg| { - this.entry(msg, None, { - let weak_editor = weak_editor.clone(); - let breakpoint = breakpoint.clone(); - move |_window, cx| { - weak_editor - .update(cx, |this, cx| { - this.edit_breakpoint_at_anchor( - anchor, - breakpoint.as_ref().clone(), - BreakpointEditAction::InvertState, - cx, - ); - }) - .log_err(); - } - }) - }) - .entry(set_breakpoint_msg, None, { - let weak_editor = weak_editor.clone(); - let breakpoint = breakpoint.clone(); - move |_window, cx| { - weak_editor - .update(cx, |this, cx| { - this.edit_breakpoint_at_anchor( - anchor, - breakpoint.as_ref().clone(), - BreakpointEditAction::Toggle, - cx, - ); - }) - .log_err(); - } - }) - .entry(log_breakpoint_msg, None, { - let breakpoint = breakpoint.clone(); - let weak_editor = weak_editor.clone(); - move |window, cx| { - weak_editor - .update(cx, |this, cx| { - this.add_edit_breakpoint_block( - anchor, - breakpoint.as_ref(), - BreakpointPromptEditAction::Log, - window, - cx, - ); - }) - .log_err(); - } - }) - .entry(condition_breakpoint_msg, None, { - let breakpoint = breakpoint.clone(); - let weak_editor = weak_editor.clone(); - move |window, cx| { - weak_editor - .update(cx, |this, cx| { - this.add_edit_breakpoint_block( - anchor, - breakpoint.as_ref(), - BreakpointPromptEditAction::Condition, - window, - cx, - ); - }) - .log_err(); - } - }) - .entry(hit_condition_breakpoint_msg, None, move |window, cx| { - weak_editor - .update(cx, |this, cx| { - this.add_edit_breakpoint_block( - anchor, - breakpoint.as_ref(), - BreakpointPromptEditAction::HitCondition, - window, - cx, - ); - }) - .log_err(); - }) - }) - } - - fn render_breakpoint( - &self, - position: Anchor, - row: DisplayRow, - breakpoint: &Breakpoint, - cx: &mut Context, - ) -> IconButton { - // Is it a breakpoint that shows up when hovering over gutter? - let (is_phantom, collides_with_existing) = self.gutter_breakpoint_indicator.0.map_or( - (false, false), - |PhantomBreakpointIndicator { - is_active, - display_row, - collides_with_existing_breakpoint, - }| { - ( - is_active && display_row == row, - collides_with_existing_breakpoint, - ) - }, - ); - - let (color, icon) = { - let icon = match (&breakpoint.message.is_some(), breakpoint.is_disabled()) { - (false, false) => ui::IconName::DebugBreakpoint, - (true, false) => ui::IconName::DebugLogBreakpoint, - (false, true) => ui::IconName::DebugDisabledBreakpoint, - (true, true) => ui::IconName::DebugDisabledLogBreakpoint, - }; - - let color = if is_phantom { - Color::Hint - } else { - Color::Debugger - }; - - (color, icon) - }; - - let breakpoint = Arc::from(breakpoint.clone()); - - let alt_as_text = gpui::Keystroke { - modifiers: Modifiers::secondary_key(), - ..Default::default() - }; - let primary_action_text = if breakpoint.is_disabled() { - "enable" - } else if is_phantom && !collides_with_existing { - "set" - } else { - "unset" - }; - let mut primary_text = format!("Click to {primary_action_text}"); - if collides_with_existing && !breakpoint.is_disabled() { - use std::fmt::Write; - write!(primary_text, ", {alt_as_text}-click to disable").ok(); - } - let primary_text = SharedString::from(primary_text); - let focus_handle = self.focus_handle.clone(); - IconButton::new(("breakpoint_indicator", row.0 as usize), icon) - .icon_size(IconSize::XSmall) - .size(ui::ButtonSize::None) - .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 - } else { - BreakpointEditAction::Toggle - }; - - window.focus(&editor.focus_handle(cx)); - editor.edit_breakpoint_at_anchor( - position, - breakpoint.as_ref().clone(), - edit_action, - cx, - ); - } - })) - .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { - editor.set_breakpoint_context_menu( - row, - Some(position), - event.down.position, - window, - cx, - ); - })) - .tooltip(move |window, cx| { - Tooltip::with_meta_in( - primary_text.clone(), - None, - "Right-click for more options", - &focus_handle, - window, - cx, - ) - }) - } - - fn build_tasks_context( - project: &Entity, - buffer: &Entity, - buffer_row: u32, - tasks: &Arc, - cx: &mut Context, - ) -> Task> { - let position = Point::new(buffer_row, tasks.column); - let range_start = buffer.read(cx).anchor_at(position, Bias::Right); - let location = Location { - buffer: buffer.clone(), - range: range_start..range_start, - }; - // Fill in the environmental variables from the tree-sitter captures - let mut captured_task_variables = TaskVariables::default(); - for (capture_name, value) in tasks.extra_variables.clone() { - captured_task_variables.insert( - task::VariableName::Custom(capture_name.into()), - value.clone(), - ); - } - project.update(cx, |project, cx| { - project.task_store().update(cx, |task_store, cx| { - task_store.task_context_for_location(captured_task_variables, location, cx) - }) - }) - } - - pub fn spawn_nearest_task( - &mut self, - action: &SpawnNearestTask, - window: &mut Window, - cx: &mut Context, - ) { - let Some((workspace, _)) = self.workspace.clone() else { - return; - }; - let Some(project) = self.project.clone() else { - return; - }; - - // Try to find a closest, enclosing node using tree-sitter that has a - // task - let Some((buffer, buffer_row, tasks)) = self - .find_enclosing_node_task(cx) - // Or find the task that's closest in row-distance. - .or_else(|| self.find_closest_task(cx)) - else { - return; - }; - - let reveal_strategy = action.reveal; - let task_context = Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx); - cx.spawn_in(window, async move |_, cx| { - let context = task_context.await?; - let (task_source_kind, mut resolved_task) = tasks.resolve(&context).next()?; - - let resolved = &mut resolved_task.resolved; - resolved.reveal = reveal_strategy; - - workspace - .update_in(cx, |workspace, window, cx| { - workspace.schedule_resolved_task( - task_source_kind, - resolved_task, - false, - window, - cx, - ); - }) - .ok() - }) - .detach(); - } - - fn find_closest_task( - &mut self, - cx: &mut Context, - ) -> Option<(Entity, u32, Arc)> { - let cursor_row = self.selections.newest_adjusted(cx).head().row; - - let ((buffer_id, row), tasks) = self - .tasks - .iter() - .min_by_key(|((_, row), _)| cursor_row.abs_diff(*row))?; - - let buffer = self.buffer.read(cx).buffer(*buffer_id)?; - let tasks = Arc::new(tasks.to_owned()); - Some((buffer, *row, tasks)) - } - - fn find_enclosing_node_task( - &mut self, - cx: &mut Context, - ) -> Option<(Entity, u32, Arc)> { - let snapshot = self.buffer.read(cx).snapshot(cx); - let offset = self.selections.newest::(cx).head(); - let excerpt = snapshot.excerpt_containing(offset..offset)?; - let buffer_id = excerpt.buffer().remote_id(); - - let layer = excerpt.buffer().syntax_layer_at(offset)?; - let mut cursor = layer.node().walk(); - - while cursor.goto_first_child_for_byte(offset).is_some() { - if cursor.node().end_byte() == offset { - cursor.goto_next_sibling(); - } - } - - // Ascend to the smallest ancestor that contains the range and has a task. - loop { - let node = cursor.node(); - let node_range = node.byte_range(); - let symbol_start_row = excerpt.buffer().offset_to_point(node.start_byte()).row; - - // Check if this node contains our offset - if node_range.start <= offset && node_range.end >= offset { - // If it contains offset, check for task - if let Some(tasks) = self.tasks.get(&(buffer_id, symbol_start_row)) { - let buffer = self.buffer.read(cx).buffer(buffer_id)?; - return Some((buffer, symbol_start_row, Arc::new(tasks.to_owned()))); - } - } - - if !cursor.goto_parent() { - break; - } - } - None - } - - fn render_run_indicator( - &self, - _style: &EditorStyle, - is_active: bool, - row: DisplayRow, - breakpoint: Option<(Anchor, Breakpoint)>, - cx: &mut Context, - ) -> IconButton { - let color = Color::Muted; - let position = breakpoint.as_ref().map(|(anchor, _)| *anchor); - - 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_indicator: Some(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 { - !self.edit_prediction_preview_is_active() - && self - .context_menu - .borrow() - .as_ref() - .map_or(false, |menu| menu.visible()) - } - - fn context_menu_origin(&self) -> Option { - self.context_menu - .borrow() - .as_ref() - .map(|menu| menu.origin()) - } - - pub fn set_context_menu_options(&mut self, options: ContextMenuOptions) { - self.context_menu_options = Some(options); - } - - const EDIT_PREDICTION_POPOVER_PADDING_X: Pixels = Pixels(24.); - const EDIT_PREDICTION_POPOVER_PADDING_Y: Pixels = Pixels(2.); - - fn render_edit_prediction_popover( - &mut self, - text_bounds: &Bounds, - content_origin: gpui::Point, - editor_snapshot: &EditorSnapshot, - visible_row_range: Range, - scroll_top: f32, - scroll_bottom: f32, - line_layouts: &[LineWithInvisibles], - line_height: Pixels, - scroll_pixel_position: gpui::Point, - newest_selection_head: Option, - editor_width: Pixels, - style: &EditorStyle, - window: &mut Window, - cx: &mut App, - ) -> Option<(AnyElement, gpui::Point)> { - let active_inline_completion = self.active_inline_completion.as_ref()?; - - if self.edit_prediction_visible_in_cursor_popover(true) { - return None; - } - - match &active_inline_completion.completion { - InlineCompletion::Move { target, .. } => { - let target_display_point = target.to_display_point(editor_snapshot); - - if self.edit_prediction_requires_modifier() { - if !self.edit_prediction_preview_is_active() { - return None; - } - - self.render_edit_prediction_modifier_jump_popover( - text_bounds, - content_origin, - visible_row_range, - line_layouts, - line_height, - scroll_pixel_position, - newest_selection_head, - target_display_point, - window, - cx, - ) - } else { - self.render_edit_prediction_eager_jump_popover( - text_bounds, - content_origin, - editor_snapshot, - visible_row_range, - scroll_top, - scroll_bottom, - line_height, - scroll_pixel_position, - target_display_point, - editor_width, - window, - cx, - ) - } - } - InlineCompletion::Edit { - display_mode: EditDisplayMode::Inline, - .. - } => None, - InlineCompletion::Edit { - display_mode: EditDisplayMode::TabAccept, - edits, - .. - } => { - let range = &edits.first()?.0; - let target_display_point = range.end.to_display_point(editor_snapshot); - - self.render_edit_prediction_end_of_line_popover( - "Accept", - editor_snapshot, - visible_row_range, - target_display_point, - line_height, - scroll_pixel_position, - content_origin, - editor_width, - window, - cx, - ) - } - InlineCompletion::Edit { - edits, - edit_preview, - display_mode: EditDisplayMode::DiffPopover, - snapshot, - } => self.render_edit_prediction_diff_popover( - text_bounds, - content_origin, - editor_snapshot, - visible_row_range, - line_layouts, - line_height, - scroll_pixel_position, - newest_selection_head, - editor_width, - style, - edits, - edit_preview, - snapshot, - window, - cx, - ), - } - } - - fn render_edit_prediction_modifier_jump_popover( - &mut self, - text_bounds: &Bounds, - content_origin: gpui::Point, - visible_row_range: Range, - line_layouts: &[LineWithInvisibles], - line_height: Pixels, - scroll_pixel_position: gpui::Point, - newest_selection_head: Option, - target_display_point: DisplayPoint, - window: &mut Window, - cx: &mut App, - ) -> Option<(AnyElement, gpui::Point)> { - let scrolled_content_origin = - content_origin - gpui::Point::new(scroll_pixel_position.x, Pixels(0.0)); - - const SCROLL_PADDING_Y: Pixels = px(12.); - - if target_display_point.row() < visible_row_range.start { - return self.render_edit_prediction_scroll_popover( - |_| SCROLL_PADDING_Y, - IconName::ArrowUp, - visible_row_range, - line_layouts, - newest_selection_head, - scrolled_content_origin, - window, - cx, - ); - } else if target_display_point.row() >= visible_row_range.end { - return self.render_edit_prediction_scroll_popover( - |size| text_bounds.size.height - size.height - SCROLL_PADDING_Y, - IconName::ArrowDown, - visible_row_range, - line_layouts, - newest_selection_head, - scrolled_content_origin, - window, - cx, - ); - } - - const POLE_WIDTH: Pixels = px(2.); - - let line_layout = - line_layouts.get(target_display_point.row().minus(visible_row_range.start) as usize)?; - let target_column = target_display_point.column() as usize; - - let target_x = line_layout.x_for_index(target_column); - let target_y = - (target_display_point.row().as_f32() * line_height) - scroll_pixel_position.y; - - let flag_on_right = target_x < text_bounds.size.width / 2.; - - let mut border_color = Self::edit_prediction_callout_popover_border_color(cx); - border_color.l += 0.001; - - let mut element = v_flex() - .items_end() - .when(flag_on_right, |el| el.items_start()) - .child(if flag_on_right { - self.render_edit_prediction_line_popover("Jump", None, window, cx)? - .rounded_bl(px(0.)) - .rounded_tl(px(0.)) - .border_l_2() - .border_color(border_color) - } else { - self.render_edit_prediction_line_popover("Jump", None, window, cx)? - .rounded_br(px(0.)) - .rounded_tr(px(0.)) - .border_r_2() - .border_color(border_color) - }) - .child(div().w(POLE_WIDTH).bg(border_color).h(line_height)) - .into_any(); - - let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); - - let mut origin = scrolled_content_origin + point(target_x, target_y) - - point( - if flag_on_right { - POLE_WIDTH - } else { - size.width - POLE_WIDTH - }, - size.height - line_height, - ); - - origin.x = origin.x.max(content_origin.x); - - element.prepaint_at(origin, window, cx); - - Some((element, origin)) - } - - fn render_edit_prediction_scroll_popover( - &mut self, - to_y: impl Fn(Size) -> Pixels, - scroll_icon: IconName, - visible_row_range: Range, - line_layouts: &[LineWithInvisibles], - newest_selection_head: Option, - scrolled_content_origin: gpui::Point, - window: &mut Window, - cx: &mut App, - ) -> Option<(AnyElement, gpui::Point)> { - let mut element = self - .render_edit_prediction_line_popover("Scroll", Some(scroll_icon), window, cx)? - .into_any(); - - let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); - - let cursor = newest_selection_head?; - let cursor_row_layout = - line_layouts.get(cursor.row().minus(visible_row_range.start) as usize)?; - let cursor_column = cursor.column() as usize; - - let cursor_character_x = cursor_row_layout.x_for_index(cursor_column); - - let origin = scrolled_content_origin + point(cursor_character_x, to_y(size)); - - element.prepaint_at(origin, window, cx); - Some((element, origin)) - } - - fn render_edit_prediction_eager_jump_popover( - &mut self, - text_bounds: &Bounds, - content_origin: gpui::Point, - editor_snapshot: &EditorSnapshot, - visible_row_range: Range, - scroll_top: f32, - scroll_bottom: f32, - line_height: Pixels, - scroll_pixel_position: gpui::Point, - target_display_point: DisplayPoint, - editor_width: Pixels, - window: &mut Window, - cx: &mut App, - ) -> Option<(AnyElement, gpui::Point)> { - if target_display_point.row().as_f32() < scroll_top { - let mut element = self - .render_edit_prediction_line_popover( - "Jump to Edit", - Some(IconName::ArrowUp), - window, - cx, - )? - .into_any(); - - let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); - let offset = point( - (text_bounds.size.width - size.width) / 2., - Self::EDIT_PREDICTION_POPOVER_PADDING_Y, - ); - - let origin = text_bounds.origin + offset; - element.prepaint_at(origin, window, cx); - Some((element, origin)) - } else if (target_display_point.row().as_f32() + 1.) > scroll_bottom { - let mut element = self - .render_edit_prediction_line_popover( - "Jump to Edit", - Some(IconName::ArrowDown), - window, - cx, - )? - .into_any(); - - let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); - let offset = point( - (text_bounds.size.width - size.width) / 2., - text_bounds.size.height - size.height - Self::EDIT_PREDICTION_POPOVER_PADDING_Y, - ); - - let origin = text_bounds.origin + offset; - element.prepaint_at(origin, window, cx); - Some((element, origin)) - } else { - self.render_edit_prediction_end_of_line_popover( - "Jump to Edit", - editor_snapshot, - visible_row_range, - target_display_point, - line_height, - scroll_pixel_position, - content_origin, - editor_width, - window, - cx, - ) - } - } - - fn render_edit_prediction_end_of_line_popover( - self: &mut Editor, - label: &'static str, - editor_snapshot: &EditorSnapshot, - visible_row_range: Range, - target_display_point: DisplayPoint, - line_height: Pixels, - scroll_pixel_position: gpui::Point, - content_origin: gpui::Point, - editor_width: Pixels, - window: &mut Window, - cx: &mut App, - ) -> Option<(AnyElement, gpui::Point)> { - let target_line_end = DisplayPoint::new( - target_display_point.row(), - editor_snapshot.line_len(target_display_point.row()), - ); - - let mut element = self - .render_edit_prediction_line_popover(label, None, window, cx)? - .into_any(); - - let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); - - let line_origin = self.display_to_pixel_point(target_line_end, editor_snapshot, window)?; - - let start_point = content_origin - point(scroll_pixel_position.x, Pixels::ZERO); - let mut origin = start_point - + line_origin - + point(Self::EDIT_PREDICTION_POPOVER_PADDING_X, Pixels::ZERO); - origin.x = origin.x.max(content_origin.x); - - let max_x = content_origin.x + editor_width - size.width; - - if origin.x > max_x { - let offset = line_height + Self::EDIT_PREDICTION_POPOVER_PADDING_Y; - - let icon = if visible_row_range.contains(&(target_display_point.row() + 2)) { - origin.y += offset; - IconName::ArrowUp - } else { - origin.y -= offset; - IconName::ArrowDown - }; - - element = self - .render_edit_prediction_line_popover(label, Some(icon), window, cx)? - .into_any(); - - let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); - - origin.x = content_origin.x + editor_width - size.width - px(2.); - } - - element.prepaint_at(origin, window, cx); - Some((element, origin)) - } - - fn render_edit_prediction_diff_popover( - self: &Editor, - text_bounds: &Bounds, - content_origin: gpui::Point, - editor_snapshot: &EditorSnapshot, - visible_row_range: Range, - line_layouts: &[LineWithInvisibles], - line_height: Pixels, - scroll_pixel_position: gpui::Point, - newest_selection_head: Option, - editor_width: Pixels, - style: &EditorStyle, - edits: &Vec<(Range, String)>, - edit_preview: &Option, - snapshot: &language::BufferSnapshot, - window: &mut Window, - cx: &mut App, - ) -> Option<(AnyElement, gpui::Point)> { - let edit_start = edits - .first() - .unwrap() - .0 - .start - .to_display_point(editor_snapshot); - let edit_end = edits - .last() - .unwrap() - .0 - .end - .to_display_point(editor_snapshot); - - let is_visible = visible_row_range.contains(&edit_start.row()) - || visible_row_range.contains(&edit_end.row()); - if !is_visible { - return None; - } - - 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(); - - const BORDER_WIDTH: Pixels = px(1.); - - let keybind = self.render_edit_prediction_accept_keybind(window, cx); - let has_keybind = keybind.is_some(); - - let mut element = h_flex() - .items_start() - .child( - h_flex() - .bg(cx.theme().colors().editor_background) - .border(BORDER_WIDTH) - .shadow_sm() - .border_color(cx.theme().colors().border) - .rounded_l_lg() - .when(line_count > 1, |el| el.rounded_br_lg()) - .pr_1() - .child(styled_text), - ) - .child( - h_flex() - .h(line_height + BORDER_WIDTH * 2.) - .px_1p5() - .gap_1() - // Workaround: For some reason, there's a gap if we don't do this - .ml(-BORDER_WIDTH) - .shadow(vec![gpui::BoxShadow { - color: gpui::black().opacity(0.05), - offset: point(px(1.), px(1.)), - blur_radius: px(2.), - spread_radius: px(0.), - }]) - .bg(Editor::edit_prediction_line_popover_bg_color(cx)) - .border(BORDER_WIDTH) - .border_color(cx.theme().colors().border) - .rounded_r_lg() - .id("edit_prediction_diff_popover_keybind") - .when(!has_keybind, |el| { - let status_colors = cx.theme().status(); - - el.bg(status_colors.error_background) - .border_color(status_colors.error.opacity(0.6)) - .child(Icon::new(IconName::Info).color(Color::Error)) - .cursor_default() - .hoverable_tooltip(move |_window, cx| { - cx.new(|_| MissingEditPredictionKeybindingTooltip).into() - }) - }) - .children(keybind), - ) - .into_any(); - - let longest_row = - editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1); - let longest_line_width = if visible_row_range.contains(&longest_row) { - line_layouts[(longest_row.0 - visible_row_range.start.0) as usize].width - } else { - layout_line( - longest_row, - editor_snapshot, - style, - editor_width, - |_| false, - window, - cx, - ) - .width - }; - - let viewport_bounds = - Bounds::new(Default::default(), window.viewport_size()).extend(Edges { - right: -EditorElement::SCROLLBAR_WIDTH, - ..Default::default() - }); - - let x_after_longest = - text_bounds.origin.x + longest_line_width + Self::EDIT_PREDICTION_POPOVER_PADDING_X - - scroll_pixel_position.x; - - let element_bounds = element.layout_as_root(AvailableSpace::min_size(), window, cx); - - // Fully visible if it can be displayed within the window (allow overlapping other - // panes). However, this is only allowed if the popover starts within text_bounds. - let can_position_to_the_right = x_after_longest < text_bounds.right() - && x_after_longest + element_bounds.width < viewport_bounds.right(); - - let mut origin = if can_position_to_the_right { - point( - x_after_longest, - text_bounds.origin.y + edit_start.row().as_f32() * line_height - - scroll_pixel_position.y, - ) - } else { - let cursor_row = newest_selection_head.map(|head| head.row()); - let above_edit = edit_start - .row() - .0 - .checked_sub(line_count as u32) - .map(DisplayRow); - let below_edit = Some(edit_end.row() + 1); - let above_cursor = - cursor_row.and_then(|row| row.0.checked_sub(line_count as u32).map(DisplayRow)); - let below_cursor = cursor_row.map(|cursor_row| cursor_row + 1); - - // Place the edit popover adjacent to the edit if there is a location - // available that is onscreen and does not obscure the cursor. Otherwise, - // place it adjacent to the cursor. - let row_target = [above_edit, below_edit, above_cursor, below_cursor] - .into_iter() - .flatten() - .find(|&start_row| { - let end_row = start_row + line_count as u32; - visible_row_range.contains(&start_row) - && visible_row_range.contains(&end_row) - && cursor_row.map_or(true, |cursor_row| { - !((start_row..end_row).contains(&cursor_row)) - }) - })?; - - content_origin - + point( - -scroll_pixel_position.x, - row_target.as_f32() * line_height - scroll_pixel_position.y, - ) - }; - - origin.x -= BORDER_WIDTH; - - window.defer_draw(element, origin, 1); - - // Do not return an element, since it will already be drawn due to defer_draw. - None - } - - fn edit_prediction_cursor_popover_height(&self) -> Pixels { - px(30.) - } - - fn current_user_player_color(&self, cx: &mut App) -> PlayerColor { - if self.read_only(cx) { - cx.theme().players().read_only() - } else { - self.style.as_ref().unwrap().local_player - } - } - - fn render_edit_prediction_accept_keybind( - &self, - window: &mut Window, - cx: &App, - ) -> Option { - let accept_binding = self.accept_edit_prediction_keybind(window, cx); - let accept_keystroke = accept_binding.keystroke()?; - - let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; - - let modifiers_color = if accept_keystroke.modifiers == window.modifiers() { - Color::Accent - } else { - Color::Muted - }; - - h_flex() - .px_0p5() - .when(is_platform_style_mac, |parent| parent.gap_0p5()) - .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) - .text_size(TextSize::XSmall.rems(cx)) - .child(h_flex().children(ui::render_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.key.clone()) - }) - .when(!is_platform_style_mac, |parent| { - parent.child( - Key::new( - util::capitalize(&accept_keystroke.key), - Some(Color::Default), - ) - .size(Some(IconSize::XSmall.rems().into())), - ) - }) - .into_any() - .into() - } - - fn render_edit_prediction_line_popover( - &self, - label: impl Into, - icon: Option, - window: &mut Window, - cx: &App, - ) -> Option> { - let padding_right = if icon.is_some() { px(4.) } else { px(8.) }; - - let keybind = self.render_edit_prediction_accept_keybind(window, cx); - let has_keybind = keybind.is_some(); - - let result = h_flex() - .id("ep-line-popover") - .py_0p5() - .pl_1() - .pr(padding_right) - .gap_1() - .rounded_md() - .border_1() - .bg(Self::edit_prediction_line_popover_bg_color(cx)) - .border_color(Self::edit_prediction_callout_popover_border_color(cx)) - .shadow_sm() - .when(!has_keybind, |el| { - let status_colors = cx.theme().status(); - - el.bg(status_colors.error_background) - .border_color(status_colors.error.opacity(0.6)) - .pl_2() - .child(Icon::new(IconName::ZedPredictError).color(Color::Error)) - .cursor_default() - .hoverable_tooltip(move |_window, cx| { - cx.new(|_| MissingEditPredictionKeybindingTooltip).into() - }) - }) - .children(keybind) - .child( - Label::new(label) - .size(LabelSize::Small) - .when(!has_keybind, |el| { - el.color(cx.theme().status().error.into()).strikethrough() - }), - ) - .when(!has_keybind, |el| { - el.child( - h_flex().ml_1().child( - Icon::new(IconName::Info) - .size(IconSize::Small) - .color(cx.theme().status().error.into()), - ), - ) - }) - .when_some(icon, |element, icon| { - element.child( - div() - .mt(px(1.5)) - .child(Icon::new(icon).size(IconSize::Small)), - ) - }); - - Some(result) - } - - fn edit_prediction_line_popover_bg_color(cx: &App) -> Hsla { - let accent_color = cx.theme().colors().text_accent; - let editor_bg_color = cx.theme().colors().editor_background; - editor_bg_color.blend(accent_color.opacity(0.1)) - } - - fn edit_prediction_callout_popover_border_color(cx: &App) -> Hsla { - let accent_color = cx.theme().colors().text_accent; - let editor_bg_color = cx.theme().colors().editor_background; - editor_bg_color.blend(accent_color.opacity(0.6)) - } - - fn render_edit_prediction_cursor_popover( - &self, - min_width: Pixels, - max_width: Pixels, - cursor_point: Point, - style: &EditorStyle, - accept_keystroke: Option<&gpui::Keystroke>, - _window: &Window, - cx: &mut Context, - ) -> Option { - let provider = self.edit_prediction_provider.as_ref()?; - - 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() -> Div { - h_flex() - .h_full() - .flex_1() - .gap_2() - .child(Icon::new(IconName::ZedPredict)) - } - - let completion = match &self.active_inline_completion { - Some(prediction) => { - if !self.has_visible_completions_menu() { - const RADIUS: Pixels = px(6.); - const BORDER_WIDTH: Pixels = px(1.); - - return Some( - h_flex() - .elevation_2(cx) - .border(BORDER_WIDTH) - .border_color(cx.theme().colors().border) - .when(accept_keystroke.is_none(), |el| { - el.border_color(cx.theme().status().error) - }) - .rounded(RADIUS) - .rounded_tl(px(0.)) - .overflow_hidden() - .child(div().px_1p5().child(match &prediction.completion { - InlineCompletion::Move { target, snapshot } => { - use text::ToPoint as _; - if target.text_anchor.to_point(&snapshot).row > cursor_point.row - { - Icon::new(IconName::ZedPredictDown) - } else { - Icon::new(IconName::ZedPredictUp) - } - } - InlineCompletion::Edit { .. } => Icon::new(IconName::ZedPredict), - })) - .child( - h_flex() - .gap_1() - .py_1() - .px_2() - .rounded_r(RADIUS - BORDER_WIDTH) - .border_l_1() - .border_color(cx.theme().colors().border) - .bg(Self::edit_prediction_line_popover_bg_color(cx)) - .when(self.edit_prediction_preview.released_too_fast(), |el| { - el.child( - Label::new("Hold") - .size(LabelSize::Small) - .when(accept_keystroke.is_none(), |el| { - el.strikethrough() - }) - .line_height_style(LineHeightStyle::UiLabel), - ) - }) - .id("edit_prediction_cursor_popover_keybind") - .when(accept_keystroke.is_none(), |el| { - let status_colors = cx.theme().status(); - - el.bg(status_colors.error_background) - .border_color(status_colors.error.opacity(0.6)) - .child(Icon::new(IconName::Info).color(Color::Error)) - .cursor_default() - .hoverable_tooltip(move |_window, cx| { - cx.new(|_| MissingEditPredictionKeybindingTooltip) - .into() - }) - }) - .when_some( - accept_keystroke.as_ref(), - |el, accept_keystroke| { - el.child(h_flex().children(ui::render_modifiers( - &accept_keystroke.modifiers, - PlatformStyle::platform(), - Some(Color::Default), - Some(IconSize::XSmall.rems().into()), - false, - ))) - }, - ), - ) - .into_any(), - ); - } - - self.render_edit_prediction_cursor_popover_preview( - prediction, - cursor_point, - style, - cx, - )? - } - - 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, - style, - cx, - )?, - - None => { - pending_completion_container().child(Label::new("...").size(LabelSize::Small)) - } - }, - - None => pending_completion_container().child(Label::new("No Prediction")), - }; - - let completion = if is_refreshing { - completion - .with_animation( - "loading-completion", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.4, 0.8)), - |label, delta| label.opacity(delta), - ) - .into_any_element() - } else { - completion.into_any_element() - }; - - let has_completion = self.active_inline_completion.is_some(); - - let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; - Some( - h_flex() - .min_w(min_width) - .max_w(max_width) - .flex_1() - .elevation_2(cx) - .border_color(cx.theme().colors().border) - .child( - div() - .flex_1() - .py_1() - .px_2() - .overflow_hidden() - .child(completion), - ) - .when_some(accept_keystroke, |el, accept_keystroke| { - if !accept_keystroke.modifiers.modified() { - return el; - } - - el.child( - h_flex() - .h_full() - .border_l_1() - .rounded_r_lg() - .border_color(cx.theme().colors().border) - .bg(Self::edit_prediction_line_popover_bg_color(cx)) - .gap_1() - .py_1() - .px_2() - .child( - h_flex() - .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.modifiers, - PlatformStyle::platform(), - Some(if !has_completion { - Color::Muted - } else { - Color::Default - }), - None, - false, - ))), - ) - .child(Label::new("Preview").into_any_element()) - .opacity(if has_completion { 1.0 } else { 0.4 }), - ) - }) - .into_any(), - ) - } - - fn render_edit_prediction_cursor_popover_preview( - &self, - completion: &InlineCompletionState, - cursor_point: Point, - style: &EditorStyle, - cx: &mut Context, - ) -> Option
{ - use text::ToPoint as _; - - fn render_relative_row_jump( - prefix: impl Into, - current_row: u32, - target_row: u32, - ) -> Div { - let (row_diff, arrow) = if target_row < current_row { - (current_row - target_row, IconName::ArrowUp) - } else { - (target_row - current_row, IconName::ArrowDown) - }; - - h_flex() - .child( - Label::new(format!("{}{}", prefix.into(), row_diff)) - .color(Color::Muted) - .size(LabelSize::Small), - ) - .child(Icon::new(arrow).color(Color::Muted).size(IconSize::Small)) - } - - match &completion.completion { - InlineCompletion::Move { - target, snapshot, .. - } => 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")), - ), - - InlineCompletion::Edit { - edits, - edit_preview, - snapshot, - display_mode: _, - } => { - let first_edit_row = edits.first()?.0.start.text_anchor.to_point(&snapshot).row; - - 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); - - let preview = h_flex() - .gap_1() - .min_w_16() - .child(styled_text) - .when(has_more_lines, |parent| parent.child("…")); - - let left = if first_edit_row != cursor_point.row { - render_relative_row_jump("", cursor_point.row, first_edit_row) - .into_any_element() - } else { - Icon::new(IconName::ZedPredict).into_any_element() - }; - - Some( - h_flex() - .h_full() - .flex_1() - .gap_2() - .pr_1() - .overflow_x_hidden() - .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) - .child(left) - .child(preview), - ) - } - } - } - - fn render_context_menu( - &self, - style: &EditorStyle, - max_height_in_lines: u32, - window: &mut Window, - cx: &mut Context, - ) -> Option { - let menu = self.context_menu.borrow(); - let menu = menu.as_ref()?; - if !menu.visible() { - return None; - }; - Some(menu.render(style, max_height_in_lines, window, cx)) - } - - fn render_context_menu_aside( - &mut self, - max_size: Size, - window: &mut Window, - cx: &mut Context, - ) -> Option { - self.context_menu.borrow_mut().as_mut().and_then(|menu| { - if menu.visible() { - menu.render_aside(self, max_size, window, cx) - } else { - None - } - }) - } - - fn hide_context_menu( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Option { - cx.notify(); - self.completion_tasks.clear(); - let context_menu = self.context_menu.borrow_mut().take(); - self.stale_inline_completion_in_menu.take(); - self.update_visible_inline_completion(window, cx); - context_menu - } - - fn show_snippet_choices( - &mut self, - choices: &Vec, - selection: Range, - cx: &mut Context, - ) { - if selection.start.buffer_id.is_none() { - return; - } - let buffer_id = selection.start.buffer_id.unwrap(); - let buffer = self.buffer().read(cx).buffer(buffer_id); - let id = post_inc(&mut self.next_completion_id); - let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; - - if let Some(buffer) = buffer { - *self.context_menu.borrow_mut() = Some(CodeContextMenu::Completions( - CompletionsMenu::new_snippet_choices( - id, - true, - choices, - selection, - buffer, - snippet_sort_order, - ), - )); - } - } - - pub fn insert_snippet( - &mut self, - insertion_ranges: &[Range], - snippet: Snippet, - window: &mut Window, - cx: &mut Context, - ) -> Result<()> { - struct Tabstop { - is_end_tabstop: bool, - ranges: Vec>, - choices: Option>, - } - - let tabstops = self.buffer.update(cx, |buffer, cx| { - let snippet_text: Arc = snippet.text.clone().into(); - let edits = insertion_ranges - .iter() - .cloned() - .map(|range| (range, snippet_text.clone())); - buffer.edit(edits, Some(AutoindentMode::EachLine), cx); - - let snapshot = &*buffer.read(cx); - let snippet = &snippet; - snippet - .tabstops - .iter() - .map(|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 - .ranges - .iter() - .flat_map(|tabstop_range| { - let mut delta = 0_isize; - insertion_ranges.iter().map(move |insertion_range| { - let insertion_start = insertion_range.start as isize + delta; - delta += - snippet.text.len() as isize - insertion_range.len() as isize; - - let start = ((insertion_start + tabstop_range.start) as usize) - .min(snapshot.len()); - let end = ((insertion_start + tabstop_range.end) as usize) - .min(snapshot.len()); - snapshot.anchor_before(start)..snapshot.anchor_after(end) - }) - }) - .collect::>(); - tabstop_ranges.sort_unstable_by(|a, b| a.start.cmp(&b.start, snapshot)); - - Tabstop { - is_end_tabstop, - ranges: tabstop_ranges, - choices: tabstop.choices.clone(), - } - }) - .collect::>() - }); - if let Some(tabstop) = tabstops.first() { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_ranges(tabstop.ranges.iter().cloned()); - }); - - 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, - // we're done, we don't need to keep the state around. - if !tabstop.is_end_tabstop { - let choices = tabstops - .iter() - .map(|tabstop| tabstop.choices.clone()) - .collect(); - - let ranges = tabstops - .into_iter() - .map(|tabstop| tabstop.ranges) - .collect::>(); - - self.snippet_stack.push(SnippetState { - active_index: 0, - ranges, - choices, - }); - } - - // Check whether the just-entered snippet ends with an auto-closable bracket. - if self.autoclose_regions.is_empty() { - let snapshot = self.buffer.read(cx).snapshot(cx); - for selection in &mut self.selections.all::(cx) { - let selection_head = selection.head(); - let Some(scope) = snapshot.language_scope_at(selection_head) else { - continue; - }; - - let mut bracket_pair = None; - let next_chars = snapshot.chars_at(selection_head).collect::(); - let prev_chars = snapshot - .reversed_chars_at(selection_head) - .collect::(); - for (pair, enabled) in scope.brackets() { - if enabled - && pair.close - && prev_chars.starts_with(pair.start.as_str()) - && next_chars.starts_with(pair.end.as_str()) - { - bracket_pair = Some(pair.clone()); - break; - } - } - if let Some(pair) = bracket_pair { - let snapshot_settings = snapshot.language_settings_at(selection_head, cx); - let autoclose_enabled = - self.use_autoclose && snapshot_settings.use_autoclose; - if autoclose_enabled { - let start = snapshot.anchor_after(selection_head); - let end = snapshot.anchor_after(selection_head); - self.autoclose_regions.push(AutocloseRegion { - selection_id: selection.id, - range: start..end, - pair, - }); - } - } - } - } - } - Ok(()) - } - - pub fn move_to_next_snippet_tabstop( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> bool { - self.move_to_snippet_tabstop(Bias::Right, window, cx) - } - - pub fn move_to_prev_snippet_tabstop( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> bool { - self.move_to_snippet_tabstop(Bias::Left, window, cx) - } - - pub fn move_to_snippet_tabstop( - &mut self, - bias: Bias, - window: &mut Window, - cx: &mut Context, - ) -> bool { - if let Some(mut snippet) = self.snippet_stack.pop() { - match bias { - Bias::Left => { - if snippet.active_index > 0 { - snippet.active_index -= 1; - } else { - self.snippet_stack.push(snippet); - return false; - } - } - Bias::Right => { - if snippet.active_index + 1 < snippet.ranges.len() { - snippet.active_index += 1; - } else { - self.snippet_stack.push(snippet); - return false; - } - } - } - if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_anchor_ranges(current_ranges.iter().cloned()) - }); - - 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 - if snippet.active_index + 1 < snippet.ranges.len() { - self.snippet_stack.push(snippet); - } - return true; - } - } - - false - } - - pub fn clear(&mut self, window: &mut Window, cx: &mut Context) { - self.transact(window, cx, |this, window, cx| { - this.select_all(&SelectAll, window, cx); - this.insert("", window, cx); - }); - } - - pub fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - this.select_autoclose_pair(window, cx); - let mut linked_ranges = HashMap::<_, Vec<_>>::default(); - if !this.linked_edit_ranges.is_empty() { - let selections = this.selections.all::(cx); - let snapshot = this.buffer.read(cx).snapshot(cx); - - for selection in selections.iter() { - let selection_start = snapshot.anchor_before(selection.start).text_anchor; - let selection_end = snapshot.anchor_after(selection.end).text_anchor; - if selection_start.buffer_id != selection_end.buffer_id { - continue; - } - if let Some(ranges) = - this.linked_editing_ranges_for(selection_start..selection_end, cx) - { - for (buffer, entries) in ranges { - linked_ranges.entry(buffer).or_default().extend(entries); - } - } - } - } - - let mut selections = this.selections.all::(cx); - let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx)); - for selection in &mut selections { - if selection.is_empty() { - let old_head = selection.head(); - let mut new_head = - movement::left(&display_map, old_head.to_display_point(&display_map)) - .to_point(&display_map); - if let Some((buffer, line_buffer_range)) = display_map - .buffer_snapshot - .buffer_line_for_row(MultiBufferRow(old_head.row)) - { - let indent_size = buffer.indent_size_for_line(line_buffer_range.start.row); - let indent_len = match indent_size.kind { - IndentKind::Space => { - buffer.settings_at(line_buffer_range.start, cx).tab_size - } - IndentKind::Tab => NonZeroU32::new(1).unwrap(), - }; - if old_head.column <= indent_size.len && old_head.column > 0 { - let indent_len = indent_len.get(); - new_head = cmp::min( - new_head, - MultiBufferPoint::new( - old_head.row, - ((old_head.column - 1) / indent_len) * indent_len, - ), - ); - } - } - - selection.set_head(new_head, SelectionGoal::None); - } - } - - this.signature_help_state.set_backspace_pressed(true); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); - this.insert("", window, cx); - let empty_str: Arc = Arc::from(""); - for (buffer, edits) in linked_ranges { - let snapshot = buffer.read(cx).snapshot(); - use text::ToPoint as TP; - - let edits = edits - .into_iter() - .map(|range| { - let end_point = TP::to_point(&range.end, &snapshot); - let mut start_point = TP::to_point(&range.start, &snapshot); - - if end_point == start_point { - let offset = text::ToOffset::to_offset(&range.start, &snapshot) - .saturating_sub(1); - start_point = - snapshot.clip_point(TP::to_point(&offset, &snapshot), Bias::Left); - }; - - (start_point..end_point, empty_str.clone()) - }) - .sorted_by_key(|(range, _)| range.start) - .collect::>(); - buffer.update(cx, |this, cx| { - this.edit(edits, None, 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) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - if selection.is_empty() { - let cursor = movement::right(map, selection.head()); - selection.end = cursor; - selection.reversed = true; - selection.goal = SelectionGoal::None; - } - }) - }); - this.insert("", window, cx); - this.refresh_inline_completion(true, false, window, cx); - }); - } - - pub fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - if self.move_to_prev_snippet_tabstop(window, cx) { - return; - } - self.outdent(&Outdent, window, cx); - } - - pub fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { - if self.move_to_next_snippet_tabstop(window, cx) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - return; - } - if self.read_only(cx) { - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let mut selections = self.selections.all_adjusted(cx); - let buffer = self.buffer.read(cx); - let snapshot = buffer.snapshot(cx); - let rows_iter = selections.iter().map(|s| s.head().row); - let suggested_indents = snapshot.suggested_indents(rows_iter, cx); - - let has_some_cursor_in_whitespace = selections - .iter() - .filter(|selection| selection.is_empty()) - .any(|selection| { - let cursor = selection.head(); - let current_indent = snapshot.indent_size_for_line(MultiBufferRow(cursor.row)); - cursor.column < current_indent.len - }); - - let mut edits = Vec::new(); - let mut prev_edited_row = 0; - let mut row_delta = 0; - for selection in &mut selections { - if selection.start.row != prev_edited_row { - row_delta = 0; - } - prev_edited_row = selection.end.row; - - // If the selection is non-empty, then increase the indentation of the selected lines. - if !selection.is_empty() { - row_delta = - Self::indent_selection(buffer, &snapshot, selection, &mut edits, row_delta, cx); - continue; - } - - // If the selection is empty and the cursor is in the leading whitespace before the - // suggested indentation, then auto-indent the line. - let cursor = selection.head(); - let current_indent = snapshot.indent_size_for_line(MultiBufferRow(cursor.row)); - if let Some(suggested_indent) = - suggested_indents.get(&MultiBufferRow(cursor.row)).copied() - { - // If there exist any empty selection in the leading whitespace, then skip - // indent for selections at the boundary. - if has_some_cursor_in_whitespace - && cursor.column == current_indent.len - && current_indent.len == suggested_indent.len - { - continue; - } - - if cursor.column < suggested_indent.len - && cursor.column <= current_indent.len - && current_indent.len <= suggested_indent.len - { - selection.start = Point::new(cursor.row, suggested_indent.len); - selection.end = selection.start; - if row_delta == 0 { - edits.extend(Buffer::edit_for_indent_size_adjustment( - cursor.row, - current_indent, - suggested_indent, - )); - row_delta = suggested_indent.len - current_indent.len; - } - continue; - } - } - - // Otherwise, insert a hard or soft tab. - let settings = buffer.language_settings_at(cursor, cx); - let tab_size = if settings.hard_tabs { - IndentSize::tab() - } else { - let tab_size = settings.tab_size.get(); - let indent_remainder = snapshot - .text_for_range(Point::new(cursor.row, 0)..cursor) - .flat_map(str::chars) - .fold(row_delta % tab_size, |counter: u32, c| { - if c == '\t' { - 0 - } else { - (counter + 1) % tab_size - } - }); - - let chars_to_next_tab_stop = tab_size - indent_remainder; - IndentSize::spaces(chars_to_next_tab_stop) - }; - selection.start = Point::new(cursor.row, cursor.column + row_delta + tab_size.len); - selection.end = selection.start; - edits.push((cursor..cursor, tab_size.chars().collect::())); - row_delta += tab_size.len; - } - - self.transact(window, cx, |this, window, cx| { - this.buffer.update(cx, |b, cx| b.edit(edits, None, cx)); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); - this.refresh_inline_completion(true, false, window, cx); - }); - } - - pub fn indent(&mut self, _: &Indent, window: &mut Window, cx: &mut Context) { - if self.read_only(cx) { - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let mut selections = self.selections.all::(cx); - let mut prev_edited_row = 0; - let mut row_delta = 0; - let mut edits = Vec::new(); - let buffer = self.buffer.read(cx); - let snapshot = buffer.snapshot(cx); - for selection in &mut selections { - if selection.start.row != prev_edited_row { - row_delta = 0; - } - prev_edited_row = selection.end.row; - - row_delta = - Self::indent_selection(buffer, &snapshot, selection, &mut edits, row_delta, cx); - } - - self.transact(window, cx, |this, window, cx| { - this.buffer.update(cx, |b, cx| b.edit(edits, None, cx)); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); - }); - } - - fn indent_selection( - buffer: &MultiBuffer, - snapshot: &MultiBufferSnapshot, - selection: &mut Selection, - edits: &mut Vec<(Range, String)>, - delta_for_start_row: u32, - cx: &App, - ) -> u32 { - let settings = buffer.language_settings_at(selection.start, cx); - let tab_size = settings.tab_size.get(); - let indent_kind = if settings.hard_tabs { - IndentKind::Tab - } else { - IndentKind::Space - }; - let mut start_row = selection.start.row; - let mut end_row = selection.end.row + 1; - - // If a selection ends at the beginning of a line, don't indent - // that last line. - if selection.end.column == 0 && selection.end.row > selection.start.row { - end_row -= 1; - } - - // Avoid re-indenting a row that has already been indented by a - // previous selection, but still update this selection's column - // to reflect that indentation. - if delta_for_start_row > 0 { - start_row += 1; - selection.start.column += delta_for_start_row; - if selection.end.row == selection.start.row { - selection.end.column += delta_for_start_row; - } - } - - let mut delta_for_end_row = 0; - let has_multiple_rows = start_row + 1 != end_row; - for row in start_row..end_row { - let current_indent = snapshot.indent_size_for_line(MultiBufferRow(row)); - let indent_delta = match (current_indent.kind, indent_kind) { - (IndentKind::Space, IndentKind::Space) => { - let columns_to_next_tab_stop = tab_size - (current_indent.len % tab_size); - IndentSize::spaces(columns_to_next_tab_stop) - } - (IndentKind::Tab, IndentKind::Space) => IndentSize::spaces(tab_size), - (_, IndentKind::Tab) => IndentSize::tab(), - }; - - let start = if has_multiple_rows || current_indent.len < selection.start.column { - 0 - } else { - selection.start.column - }; - let row_start = Point::new(row, start); - edits.push(( - row_start..row_start, - indent_delta.chars().collect::(), - )); - - // Update this selection's endpoints to reflect the indentation. - if row == selection.start.row { - selection.start.column += indent_delta.len; - } - if row == selection.end.row { - selection.end.column += indent_delta.len; - delta_for_end_row = indent_delta.len; - } - } - - if selection.start.row == selection.end.row { - delta_for_start_row + delta_for_end_row - } else { - delta_for_end_row - } - } - - pub fn outdent(&mut self, _: &Outdent, window: &mut Window, cx: &mut Context) { - if self.read_only(cx) { - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all::(cx); - let mut deletion_ranges = Vec::new(); - let mut last_outdent = None; - { - let buffer = self.buffer.read(cx); - let snapshot = buffer.snapshot(cx); - for selection in &selections { - let settings = buffer.language_settings_at(selection.start, cx); - let tab_size = settings.tab_size.get(); - let mut rows = selection.spanned_rows(false, &display_map); - - // Avoid re-outdenting a row that has already been outdented by a - // previous selection. - 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() { - let indent_size = snapshot.indent_size_for_line(row); - if indent_size.len > 0 { - let deletion_len = match indent_size.kind { - IndentKind::Space => { - let columns_to_prev_tab_stop = indent_size.len % tab_size; - if columns_to_prev_tab_stop == 0 { - tab_size - } else { - columns_to_prev_tab_stop - } - } - IndentKind::Tab => 1, - }; - let start = if has_multiple_rows - || deletion_len > selection.start.column - || indent_size.len < selection.start.column - { - 0 - } else { - selection.start.column - deletion_len - }; - deletion_ranges.push( - Point::new(row.0, start)..Point::new(row.0, start + deletion_len), - ); - last_outdent = Some(row); - } - } - } - } - - self.transact(window, cx, |this, window, cx| { - this.buffer.update(cx, |buffer, cx| { - let empty_str: Arc = Arc::default(); - buffer.edit( - deletion_ranges - .into_iter() - .map(|range| (range, empty_str.clone())), - None, - cx, - ); - }); - let selections = this.selections.all::(cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); - }); - } - - pub fn autoindent(&mut self, _: &AutoIndent, window: &mut Window, cx: &mut Context) { - if self.read_only(cx) { - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let selections = self - .selections - .all::(cx) - .into_iter() - .map(|s| s.range()); - - self.transact(window, cx, |this, window, cx| { - this.buffer.update(cx, |buffer, cx| { - buffer.autoindent_ranges(selections, cx); - }); - let selections = this.selections.all::(cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); - }); - } - - pub fn delete_line(&mut self, _: &DeleteLine, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all::(cx); - - let mut new_cursors = Vec::new(); - let mut edit_ranges = Vec::new(); - let mut selections = selections.iter().peekable(); - while let Some(selection) = selections.next() { - let mut rows = selection.spanned_rows(false, &display_map); - let goal_display_column = selection.head().to_display_point(&display_map).column(); - - // Accumulate contiguous regions of rows that we want to delete. - while let Some(next_selection) = selections.peek() { - let next_rows = next_selection.spanned_rows(false, &display_map); - if next_rows.start <= rows.end { - rows.end = next_rows.end; - selections.next().unwrap(); - } else { - break; - } - } - - let buffer = &display_map.buffer_snapshot; - let mut edit_start = Point::new(rows.start.0, 0).to_offset(buffer); - let edit_end; - let cursor_buffer_row; - if buffer.max_point().row >= rows.end.0 { - // If there's a line after the range, delete the \n from the end of the row range - // and position the cursor on the next line. - edit_end = Point::new(rows.end.0, 0).to_offset(buffer); - cursor_buffer_row = rows.end; - } else { - // If there isn't a line after the range, delete the \n from the line before the - // start of the row range and position the cursor there. - edit_start = edit_start.saturating_sub(1); - edit_end = buffer.len(); - cursor_buffer_row = rows.start.previous_row(); - } - - let mut cursor = Point::new(cursor_buffer_row.0, 0).to_display_point(&display_map); - *cursor.column_mut() = - cmp::min(goal_display_column, display_map.line_len(cursor.row())); - - new_cursors.push(( - selection.id, - buffer.anchor_after(cursor.to_point(&display_map)), - )); - edit_ranges.push(edit_start..edit_end); - } - - self.transact(window, cx, |this, window, cx| { - let buffer = this.buffer.update(cx, |buffer, cx| { - let empty_str: Arc = Arc::default(); - buffer.edit( - edit_ranges - .into_iter() - .map(|range| (range, empty_str.clone())), - None, - cx, - ); - buffer.snapshot(cx) - }); - let new_selections = new_cursors - .into_iter() - .map(|(id, cursor)| { - let cursor = cursor.to_point(&buffer); - Selection { - id, - start: cursor, - end: cursor, - reversed: false, - goal: SelectionGoal::None, - } - }) - .collect(); - - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(new_selections); - }); - }); - } - - pub fn join_lines_impl( - &mut self, - insert_whitespace: bool, - window: &mut Window, - cx: &mut Context, - ) { - if self.read_only(cx) { - return; - } - let mut row_ranges = Vec::>::new(); - for selection in self.selections.all::(cx) { - let start = MultiBufferRow(selection.start.row); - // Treat single line selections as if they include the next line. Otherwise this action - // would do nothing for single line selections individual cursors. - let end = if selection.start.row == selection.end.row { - MultiBufferRow(selection.start.row + 1) - } else { - MultiBufferRow(selection.end.row) - }; - - 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); - } - - let snapshot = self.buffer.read(cx).snapshot(cx); - let mut cursor_positions = Vec::new(); - for row_range in &row_ranges { - let anchor = snapshot.anchor_before(Point::new( - row_range.end.previous_row().0, - snapshot.line_len(row_range.end.previous_row()), - )); - cursor_positions.push(anchor..anchor); - } - - self.transact(window, cx, |this, window, cx| { - for row_range in row_ranges.into_iter().rev() { - for row in row_range.iter_rows().rev() { - let end_of_line = Point::new(row.0, snapshot.line_len(row)); - let next_line_row = row.next_row(); - let indent = snapshot.indent_size_for_line(next_line_row); - let start_of_next_line = Point::new(next_line_row.0, indent.len); - - let replace = - if snapshot.line_len(next_line_row) > indent.len && insert_whitespace { - " " - } else { - "" - }; - - this.buffer.update(cx, |buffer, cx| { - buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx) - }); - } - } - - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_anchor_ranges(cursor_positions) - }); - }); - } - - pub fn join_lines(&mut self, _: &JoinLines, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.join_lines_impl(true, window, cx); - } - - pub fn sort_lines_case_sensitive( - &mut self, - _: &SortLinesCaseSensitive, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_lines(window, cx, |lines| lines.sort()) - } - - pub fn sort_lines_case_insensitive( - &mut self, - _: &SortLinesCaseInsensitive, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_lines(window, cx, |lines| { - lines.sort_by_key(|line| line.to_lowercase()) - }) - } - - pub fn unique_lines_case_insensitive( - &mut self, - _: &UniqueLinesCaseInsensitive, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_lines(window, cx, |lines| { - let mut seen = HashSet::default(); - lines.retain(|line| seen.insert(line.to_lowercase())); - }) - } - - pub fn unique_lines_case_sensitive( - &mut self, - _: &UniqueLinesCaseSensitive, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_lines(window, cx, |lines| { - let mut seen = HashSet::default(); - lines.retain(|line| seen.insert(*line)); - }) - } - - pub fn reload_file(&mut self, _: &ReloadFile, window: &mut Window, cx: &mut Context) { - let Some(project) = self.project.clone() else { - return; - }; - self.reload(project, window, cx) - .detach_and_notify_err(window, cx); - } - - pub fn restore_file( - &mut self, - _: &::git::RestoreFile, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let mut buffer_ids = HashSet::default(); - let snapshot = self.buffer().read(cx).snapshot(cx); - for selection in self.selections.all::(cx) { - buffer_ids.extend(snapshot.buffer_ids_for_range(selection.range())) - } - - let buffer = self.buffer().read(cx); - let ranges = buffer_ids - .into_iter() - .flat_map(|buffer_id| buffer.excerpt_ranges_for_buffer(buffer_id, cx)) - .collect::>(); - - self.restore_hunks_in_ranges(ranges, window, cx); - } - - pub fn git_restore(&mut self, _: &Restore, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let selections = self - .selections - .all(cx) - .into_iter() - .map(|s| s.range()) - .collect(); - self.restore_hunks_in_ranges(selections, window, cx); - } - - pub fn restore_hunks_in_ranges( - &mut self, - ranges: Vec>, - window: &mut Window, - cx: &mut Context, - ) { - let mut revert_changes = HashMap::default(); - let chunk_by = self - .snapshot(window, cx) - .hunks_for_ranges(ranges) - .into_iter() - .chunk_by(|hunk| hunk.buffer_id); - for (buffer_id, hunks) in &chunk_by { - let hunks = hunks.collect::>(); - for hunk in &hunks { - self.prepare_restore_change(&mut revert_changes, hunk, cx); - } - self.do_stage_or_unstage(false, buffer_id, hunks.into_iter(), cx); - } - drop(chunk_by); - if !revert_changes.is_empty() { - self.transact(window, cx, |editor, window, cx| { - editor.restore(revert_changes, window, cx); - }); - } - } - - pub fn open_active_item_in_terminal( - &mut self, - _: &OpenInTerminal, - window: &mut Window, - cx: &mut Context, - ) { - 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.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(), - None => project.absolute_path(&project_path, cx)?, - } - .parent()? - .to_path_buf(); - Some(parent) - }) { - window.dispatch_action(OpenTerminal { working_directory }.boxed_clone(), cx); - } - } - - fn set_breakpoint_context_menu( - &mut self, - display_row: DisplayRow, - position: Option, - clicked_point: gpui::Point, - window: &mut Window, - cx: &mut Context, - ) { - if !cx.has_flag::() { - return; - } - let source = self - .buffer - .read(cx) - .snapshot(cx) - .anchor_before(Point::new(display_row.0, 0u32)); - - let context_menu = self.breakpoint_context_menu(position.unwrap_or(source), window, cx); - - self.mouse_context_menu = MouseContextMenu::pinned_to_editor( - self, - source, - clicked_point, - context_menu, - window, - cx, - ); - } - - fn add_edit_breakpoint_block( - &mut self, - anchor: Anchor, - breakpoint: &Breakpoint, - edit_action: BreakpointPromptEditAction, - window: &mut Window, - cx: &mut Context, - ) { - let weak_editor = cx.weak_entity(); - let bp_prompt = cx.new(|cx| { - BreakpointPromptEditor::new( - weak_editor, - anchor, - breakpoint.clone(), - edit_action, - window, - cx, - ) - }); - - let height = bp_prompt.update(cx, |this, cx| { - this.prompt - .update(cx, |prompt, cx| prompt.max_point(cx).row().0 + 1 + 2) - }); - let cloned_prompt = bp_prompt.clone(); - let blocks = vec![BlockProperties { - style: BlockStyle::Sticky, - placement: BlockPlacement::Above(anchor), - height: Some(height), - render: Arc::new(move |cx| { - *cloned_prompt.read(cx).gutter_dimensions.lock() = *cx.gutter_dimensions; - cloned_prompt.clone().into_any_element() - }), - priority: 0, - }]; - - let focus_handle = bp_prompt.focus_handle(cx); - window.focus(&focus_handle); - - let block_ids = self.insert_blocks(blocks, None, cx); - bp_prompt.update(cx, |prompt, _| { - prompt.add_block_ids(block_ids); - }); - } - - pub(crate) fn breakpoint_at_row( - &self, - row: u32, - window: &mut Window, - cx: &mut Context, - ) -> Option<(Anchor, Breakpoint)> { - let snapshot = self.snapshot(window, cx); - let breakpoint_position = snapshot.buffer_snapshot.anchor_before(Point::new(row, 0)); - - self.breakpoint_at_anchor(breakpoint_position, &snapshot, cx) - } - - pub(crate) fn breakpoint_at_anchor( - &self, - breakpoint_position: Anchor, - snapshot: &EditorSnapshot, - cx: &mut Context, - ) -> Option<(Anchor, Breakpoint)> { - 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_with(cx, |project, cx| project.buffer_for_id(buffer_id, cx))?; - let buffer_snapshot = buffer.read(cx).snapshot(); - - let row = buffer_snapshot - .summary_for_anchor::(&breakpoint_position.text_anchor) - .row; - - let line_len = snapshot.buffer_snapshot.line_len(MultiBufferRow(row)); - let anchor_end = snapshot - .buffer_snapshot - .anchor_after(Point::new(row, line_len)); - - let bp = self - .breakpoint_store - .as_ref()? - .read_with(cx, |breakpoint_store, cx| { - breakpoint_store - .breakpoints( - &buffer, - Some(breakpoint_position.text_anchor..anchor_end.text_anchor), - &buffer_snapshot, - cx, - ) - .next() - .and_then(|(anchor, bp)| { - let breakpoint_row = buffer_snapshot - .summary_for_anchor::(anchor) - .row; - - if breakpoint_row == row { - snapshot - .buffer_snapshot - .anchor_in_excerpt(enclosing_excerpt, *anchor) - .map(|anchor| (anchor, bp.clone())) - } else { - None - } - }) - }); - bp - } - - pub fn edit_log_breakpoint( - &mut self, - _: &EditLogBreakpoint, - window: &mut Window, - cx: &mut Context, - ) { - for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) { - let breakpoint = breakpoint.unwrap_or_else(|| Breakpoint { - message: None, - state: BreakpointState::Enabled, - condition: None, - hit_condition: None, - }); - - self.add_edit_breakpoint_block( - anchor, - &breakpoint, - BreakpointPromptEditAction::Log, - window, - cx, - ); - } - } - - fn breakpoints_at_cursors( - &self, - window: &mut Window, - cx: &mut Context, - ) -> Vec<(Anchor, Option)> { - let snapshot = self.snapshot(window, cx); - let cursors = self - .selections - .disjoint_anchors() - .into_iter() - .map(|selection| { - let cursor_position: Point = selection.head().to_point(&snapshot.buffer_snapshot); - - let breakpoint_position = self - .breakpoint_at_row(cursor_position.row, window, cx) - .map(|bp| bp.0) - .unwrap_or_else(|| { - snapshot - .display_snapshot - .buffer_snapshot - .anchor_after(Point::new(cursor_position.row, 0)) - }); - - let breakpoint = self - .breakpoint_at_anchor(breakpoint_position, &snapshot, cx) - .map(|(anchor, breakpoint)| (anchor, Some(breakpoint))); - - breakpoint.unwrap_or_else(|| (breakpoint_position, None)) - }) - // There might be multiple cursors on the same line; all of them should have the same anchors though as their breakpoints positions, which makes it possible to sort and dedup the list. - .collect::>(); - - cursors.into_iter().collect() - } - - pub fn enable_breakpoint( - &mut self, - _: &crate::actions::EnableBreakpoint, - window: &mut Window, - cx: &mut Context, - ) { - for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) { - let Some(breakpoint) = breakpoint.filter(|breakpoint| breakpoint.is_disabled()) else { - continue; - }; - self.edit_breakpoint_at_anchor( - anchor, - breakpoint, - BreakpointEditAction::InvertState, - cx, - ); - } - } - - pub fn disable_breakpoint( - &mut self, - _: &crate::actions::DisableBreakpoint, - window: &mut Window, - cx: &mut Context, - ) { - for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) { - let Some(breakpoint) = breakpoint.filter(|breakpoint| breakpoint.is_enabled()) else { - continue; - }; - self.edit_breakpoint_at_anchor( - anchor, - breakpoint, - BreakpointEditAction::InvertState, - cx, - ); - } - } - - pub fn toggle_breakpoint( - &mut self, - _: &crate::actions::ToggleBreakpoint, - window: &mut Window, - cx: &mut Context, - ) { - for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) { - if let Some(breakpoint) = breakpoint { - self.edit_breakpoint_at_anchor( - anchor, - breakpoint, - BreakpointEditAction::Toggle, - cx, - ); - } else { - self.edit_breakpoint_at_anchor( - anchor, - Breakpoint::new_standard(), - BreakpointEditAction::Toggle, - cx, - ); - } - } - } - - pub fn edit_breakpoint_at_anchor( - &mut self, - breakpoint_position: Anchor, - breakpoint: Breakpoint, - edit_action: BreakpointEditAction, - cx: &mut Context, - ) { - let Some(breakpoint_store) = &self.breakpoint_store else { - return; - }; - - 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; - }; - - breakpoint_store.update(cx, |breakpoint_store, cx| { - breakpoint_store.toggle_breakpoint( - buffer, - (breakpoint_position.text_anchor, breakpoint), - edit_action, - cx, - ); - }); - - cx.notify(); - } - - #[cfg(any(test, feature = "test-support"))] - pub fn breakpoint_store(&self) -> Option> { - self.breakpoint_store.clone() - } - - pub fn prepare_restore_change( - &self, - revert_changes: &mut HashMap, Rope)>>, - hunk: &MultiBufferDiffHunk, - cx: &mut App, - ) -> Option<()> { - if hunk.is_created_file() { - return None; - } - let buffer = self.buffer.read(cx); - let diff = buffer.diff_for(hunk.buffer_id)?; - let buffer = buffer.buffer(hunk.buffer_id)?; - let buffer = buffer.read(cx); - let original_text = diff - .read(cx) - .base_text() - .as_rope() - .slice(hunk.diff_base_byte_range.clone()); - let buffer_snapshot = buffer.snapshot(); - let buffer_revert_changes = revert_changes.entry(buffer.remote_id()).or_default(); - if let Err(i) = buffer_revert_changes.binary_search_by(|probe| { - probe - .0 - .start - .cmp(&hunk.buffer_range.start, &buffer_snapshot) - .then(probe.0.end.cmp(&hunk.buffer_range.end, &buffer_snapshot)) - }) { - buffer_revert_changes.insert(i, (hunk.buffer_range.clone(), original_text)); - Some(()) - } else { - None - } - } - - pub fn reverse_lines(&mut self, _: &ReverseLines, window: &mut Window, cx: &mut Context) { - self.manipulate_lines(window, cx, |lines| lines.reverse()) - } - - pub fn shuffle_lines(&mut self, _: &ShuffleLines, window: &mut Window, cx: &mut Context) { - self.manipulate_lines(window, cx, |lines| lines.shuffle(&mut thread_rng())) - } - - fn manipulate_lines( - &mut self, - window: &mut Window, - cx: &mut Context, - mut callback: Fn, - ) where - Fn: FnMut(&mut Vec<&str>), - { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = self.buffer.read(cx).snapshot(cx); - - let mut edits = Vec::new(); - - let selections = self.selections.all::(cx); - let mut selections = selections.iter().peekable(); - let mut contiguous_row_selections = Vec::new(); - let mut new_selections = Vec::new(); - let mut added_lines = 0; - let mut removed_lines = 0; - - while let Some(selection) = selections.next() { - let (start_row, end_row) = consume_contiguous_rows( - &mut contiguous_row_selections, - selection, - &display_map, - &mut selections, - ); - - let start_point = Point::new(start_row.0, 0); - let end_point = Point::new( - end_row.previous_row().0, - buffer.line_len(end_row.previous_row()), - ); - let text = buffer - .text_for_range(start_point..end_point) - .collect::(); - - let mut lines = text.split('\n').collect_vec(); - - let lines_before = lines.len(); - callback(&mut lines); - let lines_after = lines.len(); - - edits.push((start_point..end_point, lines.join("\n"))); - - // Selections must change based on added and removed line count - let start_row = - MultiBufferRow(start_point.row + added_lines as u32 - removed_lines as u32); - let end_row = MultiBufferRow(start_row.0 + lines_after.saturating_sub(1) as u32); - new_selections.push(Selection { - id: selection.id, - start: start_row, - end: end_row, - goal: SelectionGoal::None, - reversed: selection.reversed, - }); - - if lines_after > lines_before { - added_lines += lines_after - lines_before; - } else if lines_before > lines_after { - removed_lines += lines_before - lines_after; - } - } - - self.transact(window, cx, |this, window, cx| { - let buffer = this.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, None, cx); - buffer.snapshot(cx) - }); - - // Recalculate offsets on newly edited buffer - let new_selections = new_selections - .iter() - .map(|s| { - let start_point = Point::new(s.start.0, 0); - let end_point = Point::new(s.end.0, buffer.line_len(s.end)); - Selection { - id: s.id, - start: buffer.point_to_offset(start_point), - end: buffer.point_to_offset(end_point), - goal: s.goal, - reversed: s.reversed, - } - }) - .collect(); - - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(new_selections); - }); - - this.request_autoscroll(Autoscroll::fit(), cx); - }); - } - - pub fn toggle_case(&mut self, _: &ToggleCase, window: &mut Window, cx: &mut Context) { - self.manipulate_text(window, cx, |text| { - let has_upper_case_characters = text.chars().any(|c| c.is_uppercase()); - if has_upper_case_characters { - text.to_lowercase() - } else { - text.to_uppercase() - } - }) - } - - pub fn convert_to_upper_case( - &mut self, - _: &ConvertToUpperCase, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |text| text.to_uppercase()) - } - - pub fn convert_to_lower_case( - &mut self, - _: &ConvertToLowerCase, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |text| text.to_lowercase()) - } - - pub fn convert_to_title_case( - &mut self, - _: &ConvertToTitleCase, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |text| { - text.split('\n') - .map(|line| line.to_case(Case::Title)) - .join("\n") - }) - } - - pub fn convert_to_snake_case( - &mut self, - _: &ConvertToSnakeCase, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |text| text.to_case(Case::Snake)) - } - - pub fn convert_to_kebab_case( - &mut self, - _: &ConvertToKebabCase, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |text| text.to_case(Case::Kebab)) - } - - pub fn convert_to_upper_camel_case( - &mut self, - _: &ConvertToUpperCamelCase, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |text| { - text.split('\n') - .map(|line| line.to_case(Case::UpperCamel)) - .join("\n") - }) - } - - pub fn convert_to_lower_camel_case( - &mut self, - _: &ConvertToLowerCamelCase, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |text| text.to_case(Case::Camel)) - } - - pub fn convert_to_opposite_case( - &mut self, - _: &ConvertToOppositeCase, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |text| { - text.chars() - .fold(String::with_capacity(text.len()), |mut t, c| { - if c.is_uppercase() { - t.extend(c.to_lowercase()); - } else { - t.extend(c.to_uppercase()); - } - t - }) - }) - } - - pub fn convert_to_rot13( - &mut self, - _: &ConvertToRot13, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |text| { - text.chars() - .map(|c| match c { - 'A'..='M' | 'a'..='m' => ((c as u8) + 13) as char, - 'N'..='Z' | 'n'..='z' => ((c as u8) - 13) as char, - _ => c, - }) - .collect() - }) - } - - pub fn convert_to_rot47( - &mut self, - _: &ConvertToRot47, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |text| { - text.chars() - .map(|c| { - let code_point = c as u32; - if code_point >= 33 && code_point <= 126 { - return char::from_u32(33 + ((code_point + 14) % 94)).unwrap(); - } - c - }) - .collect() - }) - } - - fn manipulate_text(&mut self, window: &mut Window, cx: &mut Context, mut callback: Fn) - where - Fn: FnMut(&str) -> String, - { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = self.buffer.read(cx).snapshot(cx); - - let mut new_selections = Vec::new(); - let mut edits = Vec::new(); - let mut selection_adjustment = 0i32; - - for selection in self.selections.all::(cx) { - let selection_is_empty = selection.is_empty(); - - let (start, end) = if selection_is_empty { - let word_range = movement::surrounding_word( - &display_map, - selection.start.to_display_point(&display_map), - ); - let start = word_range.start.to_offset(&display_map, Bias::Left); - let end = word_range.end.to_offset(&display_map, Bias::Left); - (start, end) - } else { - (selection.start, selection.end) - }; - - let text = buffer.text_for_range(start..end).collect::(); - let old_length = text.len() as i32; - let text = callback(&text); - - new_selections.push(Selection { - start: (start as i32 - selection_adjustment) as usize, - end: ((start + text.len()) as i32 - selection_adjustment) as usize, - goal: SelectionGoal::None, - ..selection - }); - - selection_adjustment += old_length - text.len() as i32; - - edits.push((start..end, text)); - } - - self.transact(window, cx, |this, window, cx| { - this.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, None, cx); - }); - - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(new_selections); - }); - - this.request_autoscroll(Autoscroll::fit(), cx); - }); - } - - pub fn duplicate( - &mut self, - upwards: bool, - whole_lines: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - let selections = self.selections.all::(cx); - - let mut edits = Vec::new(); - let mut selections_iter = selections.iter().peekable(); - while let Some(selection) = selections_iter.next() { - let mut rows = selection.spanned_rows(false, &display_map); - // duplicate line-wise - if whole_lines || selection.start == selection.end { - // Avoid duplicating the same lines twice. - while let Some(next_selection) = selections_iter.peek() { - let next_rows = next_selection.spanned_rows(false, &display_map); - if next_rows.start < rows.end { - rows.end = next_rows.end; - selections_iter.next().unwrap(); - } else { - break; - } - } - - // Copy the text from the selected row region and splice it either at the start - // or end of the region. - let start = Point::new(rows.start.0, 0); - let end = Point::new( - rows.end.previous_row().0, - buffer.line_len(rows.end.previous_row()), - ); - let text = buffer - .text_for_range(start..end) - .chain(Some("\n")) - .collect::(); - let insert_location = if upwards { - Point::new(rows.end.0, 0) - } else { - start - }; - edits.push((insert_location..insert_location, text)); - } else { - // duplicate character-wise - let start = selection.start; - let end = selection.end; - let text = buffer.text_for_range(start..end).collect::(); - edits.push((selection.end..selection.end, text)); - } - } - - self.transact(window, cx, |this, _, cx| { - this.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, None, cx); - }); - - this.request_autoscroll(Autoscroll::fit(), cx); - }); - } - - pub fn duplicate_line_up( - &mut self, - _: &DuplicateLineUp, - window: &mut Window, - cx: &mut Context, - ) { - self.duplicate(true, true, window, cx); - } - - pub fn duplicate_line_down( - &mut self, - _: &DuplicateLineDown, - window: &mut Window, - cx: &mut Context, - ) { - self.duplicate(false, true, window, cx); - } - - pub fn duplicate_selection( - &mut self, - _: &DuplicateSelection, - window: &mut Window, - cx: &mut Context, - ) { - self.duplicate(false, false, window, cx); - } - - pub fn move_line_up(&mut self, _: &MoveLineUp, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = self.buffer.read(cx).snapshot(cx); - - let mut edits = Vec::new(); - let mut unfold_ranges = Vec::new(); - let mut refold_creases = Vec::new(); - - let selections = self.selections.all::(cx); - let mut selections = selections.iter().peekable(); - let mut contiguous_row_selections = Vec::new(); - let mut new_selections = Vec::new(); - - while let Some(selection) = selections.next() { - // Find all the selections that span a contiguous row range - let (start_row, end_row) = consume_contiguous_rows( - &mut contiguous_row_selections, - selection, - &display_map, - &mut selections, - ); - - // Move the text spanned by the row range to be before the line preceding the row range - if start_row.0 > 0 { - let range_to_move = Point::new( - start_row.previous_row().0, - buffer.line_len(start_row.previous_row()), - ) - ..Point::new( - end_row.previous_row().0, - buffer.line_len(end_row.previous_row()), - ); - let insertion_point = display_map - .prev_line_boundary(Point::new(start_row.previous_row().0, 0)) - .0; - - // Don't move lines across excerpts - if buffer - .excerpt_containing(insertion_point..range_to_move.end) - .is_some() - { - let text = buffer - .text_for_range(range_to_move.clone()) - .flat_map(|s| s.chars()) - .skip(1) - .chain(['\n']) - .collect::(); - - edits.push(( - buffer.anchor_after(range_to_move.start) - ..buffer.anchor_before(range_to_move.end), - String::new(), - )); - let insertion_anchor = buffer.anchor_after(insertion_point); - edits.push((insertion_anchor..insertion_anchor, text)); - - let row_delta = range_to_move.start.row - insertion_point.row + 1; - - // Move selections up - new_selections.extend(contiguous_row_selections.drain(..).map( - |mut selection| { - selection.start.row -= row_delta; - selection.end.row -= row_delta; - selection - }, - )); - - // Move folds up - unfold_ranges.push(range_to_move.clone()); - for fold in display_map.folds_in_range( - buffer.anchor_before(range_to_move.start) - ..buffer.anchor_after(range_to_move.end), - ) { - let mut start = fold.range.start.to_point(&buffer); - let mut end = fold.range.end.to_point(&buffer); - start.row -= row_delta; - end.row -= row_delta; - refold_creases.push(Crease::simple(start..end, fold.placeholder.clone())); - } - } - } - - // If we didn't move line(s), preserve the existing selections - new_selections.append(&mut contiguous_row_selections); - } - - self.transact(window, cx, |this, window, cx| { - this.unfold_ranges(&unfold_ranges, true, true, cx); - this.buffer.update(cx, |buffer, cx| { - for (range, text) in edits { - buffer.edit([(range, text)], None, cx); - } - }); - this.fold_creases(refold_creases, true, window, cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(new_selections); - }) - }); - } - - pub fn move_line_down( - &mut self, - _: &MoveLineDown, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = self.buffer.read(cx).snapshot(cx); - - let mut edits = Vec::new(); - let mut unfold_ranges = Vec::new(); - let mut refold_creases = Vec::new(); - - let selections = self.selections.all::(cx); - let mut selections = selections.iter().peekable(); - let mut contiguous_row_selections = Vec::new(); - let mut new_selections = Vec::new(); - - while let Some(selection) = selections.next() { - // Find all the selections that span a contiguous row range - let (start_row, end_row) = consume_contiguous_rows( - &mut contiguous_row_selections, - selection, - &display_map, - &mut selections, - ); - - // Move the text spanned by the row range to be after the last line of the row range - if end_row.0 <= buffer.max_point().row { - let range_to_move = - MultiBufferPoint::new(start_row.0, 0)..MultiBufferPoint::new(end_row.0, 0); - let insertion_point = display_map - .next_line_boundary(MultiBufferPoint::new(end_row.0, 0)) - .0; - - // Don't move lines across excerpt boundaries - if buffer - .excerpt_containing(range_to_move.start..insertion_point) - .is_some() - { - let mut text = String::from("\n"); - text.extend(buffer.text_for_range(range_to_move.clone())); - text.pop(); // Drop trailing newline - edits.push(( - buffer.anchor_after(range_to_move.start) - ..buffer.anchor_before(range_to_move.end), - String::new(), - )); - let insertion_anchor = buffer.anchor_after(insertion_point); - edits.push((insertion_anchor..insertion_anchor, text)); - - let row_delta = insertion_point.row - range_to_move.end.row + 1; - - // Move selections down - new_selections.extend(contiguous_row_selections.drain(..).map( - |mut selection| { - selection.start.row += row_delta; - selection.end.row += row_delta; - selection - }, - )); - - // Move folds down - unfold_ranges.push(range_to_move.clone()); - for fold in display_map.folds_in_range( - buffer.anchor_before(range_to_move.start) - ..buffer.anchor_after(range_to_move.end), - ) { - let mut start = fold.range.start.to_point(&buffer); - let mut end = fold.range.end.to_point(&buffer); - start.row += row_delta; - end.row += row_delta; - refold_creases.push(Crease::simple(start..end, fold.placeholder.clone())); - } - } - } - - // If we didn't move line(s), preserve the existing selections - new_selections.append(&mut contiguous_row_selections); - } - - self.transact(window, cx, |this, window, cx| { - this.unfold_ranges(&unfold_ranges, true, true, cx); - this.buffer.update(cx, |buffer, cx| { - for (range, text) in edits { - buffer.edit([(range, text)], None, cx); - } - }); - this.fold_creases(refold_creases, true, window, cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(new_selections) - }); - }); - } - - pub fn transpose(&mut self, _: &Transpose, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let text_layout_details = &self.text_layout_details(window); - self.transact(window, cx, |this, window, cx| { - let edits = this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - let mut edits: Vec<(Range, String)> = Default::default(); - s.move_with(|display_map, selection| { - if !selection.is_empty() { - return; - } - - let mut head = selection.head(); - let mut transpose_offset = head.to_offset(display_map, Bias::Right); - if head.column() == display_map.line_len(head.row()) { - transpose_offset = display_map - .buffer_snapshot - .clip_offset(transpose_offset.saturating_sub(1), Bias::Left); - } - - if transpose_offset == 0 { - return; - } - - *head.column_mut() += 1; - head = display_map.clip_point(head, Bias::Right); - let goal = SelectionGoal::HorizontalPosition( - display_map - .x_for_display_point(head, text_layout_details) - .into(), - ); - selection.collapse_to(head, goal); - - let transpose_start = display_map - .buffer_snapshot - .clip_offset(transpose_offset.saturating_sub(1), Bias::Left); - 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); - if let Some(ch) = - display_map.buffer_snapshot.chars_at(transpose_start).next() - { - edits.push((transpose_start..transpose_offset, String::new())); - edits.push((transpose_end..transpose_end, ch.to_string())); - } - } - }); - edits - }); - this.buffer - .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); - let selections = this.selections.all::(cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections); - }); - }); - } - - pub fn rewrap(&mut self, _: &Rewrap, _: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.rewrap_impl(RewrapOptions::default(), cx) - } - - pub fn rewrap_impl(&mut self, options: RewrapOptions, cx: &mut Context) { - let buffer = self.buffer.read(cx).snapshot(cx); - let selections = self.selections.all::(cx); - let mut selections = selections.iter().peekable(); - - let mut edits = Vec::new(); - let mut rewrapped_row_ranges = Vec::>::new(); - - while let Some(selection) = selections.next() { - let mut start_row = selection.start.row; - let mut end_row = selection.end.row; - - // Skip selections that overlap with a range that has already been rewrapped. - let selection_range = start_row..end_row; - if rewrapped_row_ranges - .iter() - .any(|range| range.overlaps(&selection_range)) - { - continue; - } - - let tab_size = buffer.language_settings_at(selection.head(), cx).tab_size; - - // Since not all lines in the selection may be at the same indent - // level, choose the indent size that is the most common between all - // of the lines. - // - // If there is a tie, we use the deepest indent. - let (indent_size, indent_end) = { - let mut indent_size_occurrences = HashMap::default(); - let mut rows_by_indent_size = HashMap::>::default(); - - for row in start_row..=end_row { - let indent = buffer.indent_size_for_line(MultiBufferRow(row)); - rows_by_indent_size.entry(indent).or_default().push(row); - *indent_size_occurrences.entry(indent).or_insert(0) += 1; - } - - let indent_size = indent_size_occurrences - .into_iter() - .max_by_key(|(indent, count)| (*count, indent.len_with_expanded_tabs(tab_size))) - .map(|(indent, _)| indent) - .unwrap_or_default(); - let row = rows_by_indent_size[&indent_size][0]; - let indent_end = Point::new(row, indent_size.len); - - (indent_size, indent_end) - }; - - let mut line_prefix = indent_size.chars().collect::(); - - let mut inside_comment = false; - if let Some(comment_prefix) = - buffer - .language_scope_at(selection.head()) - .and_then(|language| { - language - .line_comment_prefixes() - .iter() - .find(|prefix| buffer.contains_str_at(indent_end, prefix)) - .cloned() - }) - { - line_prefix.push_str(&comment_prefix); - inside_comment = true; - } - - let language_settings = buffer.language_settings_at(selection.head(), cx); - let allow_rewrap_based_on_language = match language_settings.allow_rewrap { - RewrapBehavior::InComments => inside_comment, - RewrapBehavior::InSelections => !selection.is_empty(), - RewrapBehavior::Anywhere => true, - }; - - let should_rewrap = options.override_language_settings - || allow_rewrap_based_on_language - || self.hard_wrap.is_some(); - if !should_rewrap { - continue; - } - - if selection.is_empty() { - 'expand_upwards: while start_row > 0 { - let prev_row = start_row - 1; - if buffer.contains_str_at(Point::new(prev_row, 0), &line_prefix) - && buffer.line_len(MultiBufferRow(prev_row)) as usize > line_prefix.len() - { - start_row = prev_row; - } else { - break 'expand_upwards; - } - } - - 'expand_downwards: while end_row < buffer.max_point().row { - let next_row = end_row + 1; - if buffer.contains_str_at(Point::new(next_row, 0), &line_prefix) - && buffer.line_len(MultiBufferRow(next_row)) as usize > line_prefix.len() - { - end_row = next_row; - } else { - break 'expand_downwards; - } - } - } - - let start = Point::new(start_row, 0); - let start_offset = start.to_offset(&buffer); - let end = Point::new(end_row, buffer.line_len(MultiBufferRow(end_row))); - let selection_text = buffer.text_for_range(start..end).collect::(); - let Some(lines_without_prefixes) = selection_text - .lines() - .map(|line| { - line.strip_prefix(&line_prefix) - .or_else(|| line.trim_start().strip_prefix(&line_prefix.trim_start())) - .with_context(|| { - format!("line did not start with prefix {line_prefix:?}: {line:?}") - }) - }) - .collect::, _>>() - .log_err() - else { - continue; - }; - - let wrap_column = self.hard_wrap.unwrap_or_else(|| { - buffer - .language_settings_at(Point::new(start_row, 0), cx) - .preferred_line_length as usize - }); - let wrapped_text = wrap_with_prefix( - line_prefix, - lines_without_prefixes.join("\n"), - wrap_column, - tab_size, - options.preserve_existing_whitespace, - ); - - // TODO: should always use char-based diff while still supporting cursor behavior that - // matches vim. - let mut diff_options = DiffOptions::default(); - if options.override_language_settings { - diff_options.max_word_diff_len = 0; - diff_options.max_word_diff_line_count = 0; - } else { - diff_options.max_word_diff_len = usize::MAX; - diff_options.max_word_diff_line_count = usize::MAX; - } - - for (old_range, new_text) in - text_diff_with_options(&selection_text, &wrapped_text, diff_options) - { - let edit_start = buffer.anchor_after(start_offset + old_range.start); - let edit_end = buffer.anchor_after(start_offset + old_range.end); - edits.push((edit_start..edit_end, new_text)); - } - - rewrapped_row_ranges.push(start_row..=end_row); - } - - self.buffer - .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); - } - - pub fn cut_common(&mut self, window: &mut Window, cx: &mut Context) -> ClipboardItem { - let mut text = String::new(); - let buffer = self.buffer.read(cx).snapshot(cx); - let mut selections = self.selections.all::(cx); - let mut clipboard_selections = Vec::with_capacity(selections.len()); - { - let max_point = buffer.max_point(); - let mut is_first = true; - for selection in &mut selections { - let is_entire_line = selection.is_empty() || self.selections.line_mode; - if is_entire_line { - selection.start = Point::new(selection.start.row, 0); - if !selection.is_empty() && selection.end.column == 0 { - selection.end = cmp::min(max_point, selection.end); - } else { - selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0)); - } - selection.goal = SelectionGoal::None; - } - if is_first { - is_first = false; - } else { - text += "\n"; - } - let mut len = 0; - for chunk in buffer.text_for_range(selection.start..selection.end) { - text.push_str(chunk); - len += chunk.len(); - } - clipboard_selections.push(ClipboardSelection { - len, - is_entire_line, - first_line_indent: buffer - .indent_size_for_line(MultiBufferRow(selection.start.row)) - .len, - }); - } - } - - self.transact(window, cx, |this, window, cx| { - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections); - }); - this.insert("", window, cx); - }); - ClipboardItem::new_string_with_json_metadata(text, clipboard_selections) - } - - pub fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let item = self.cut_common(window, cx); - cx.write_to_clipboard(item); - } - - pub fn kill_ring_cut(&mut self, _: &KillRingCut, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.change_selections(None, window, cx, |s| { - s.move_with(|snapshot, sel| { - if sel.is_empty() { - sel.end = DisplayPoint::new(sel.end.row(), snapshot.line_len(sel.end.row())) - } - }); - }); - let item = self.cut_common(window, cx); - cx.set_global(KillRing(item)) - } - - pub fn kill_ring_yank( - &mut self, - _: &KillRingYank, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let (text, metadata) = if let Some(KillRing(item)) = cx.try_global() { - if let Some(ClipboardEntry::String(kill_ring)) = item.entries().first() { - (kill_ring.text().to_string(), kill_ring.metadata_json()) - } else { - return; - } - } else { - return; - }; - self.do_paste(&text, metadata, false, window, cx); - } - - pub fn copy_and_trim(&mut self, _: &CopyAndTrim, _: &mut Window, cx: &mut Context) { - self.do_copy(true, cx); - } - - pub fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context) { - self.do_copy(false, cx); - } - - fn do_copy(&self, strip_leading_indents: bool, cx: &mut Context) { - let selections = self.selections.all::(cx); - let buffer = self.buffer.read(cx).read(cx); - let mut text = String::new(); - - let mut clipboard_selections = Vec::with_capacity(selections.len()); - { - let max_point = buffer.max_point(); - let mut is_first = true; - for selection in &selections { - let mut start = selection.start; - let mut end = selection.end; - let is_entire_line = selection.is_empty() || self.selections.line_mode; - if is_entire_line { - start = Point::new(start.row, 0); - end = cmp::min(max_point, Point::new(end.row + 1, 0)); - } - - let mut trimmed_selections = Vec::new(); - if strip_leading_indents && end.row.saturating_sub(start.row) > 0 { - let row = MultiBufferRow(start.row); - let first_indent = buffer.indent_size_for_line(row); - if first_indent.len == 0 || start.column > first_indent.len { - trimmed_selections.push(start..end); - } else { - trimmed_selections.push( - Point::new(row.0, first_indent.len) - ..Point::new(row.0, buffer.line_len(row)), - ); - for row in start.row + 1..=end.row { - let mut line_len = buffer.line_len(MultiBufferRow(row)); - if row == end.row { - line_len = end.column; - } - if line_len == 0 { - trimmed_selections - .push(Point::new(row, 0)..Point::new(row, line_len)); - continue; - } - let row_indent_size = buffer.indent_size_for_line(MultiBufferRow(row)); - if row_indent_size.len >= first_indent.len { - trimmed_selections.push( - Point::new(row, first_indent.len)..Point::new(row, line_len), - ); - } else { - trimmed_selections.clear(); - trimmed_selections.push(start..end); - break; - } - } - } - } else { - trimmed_selections.push(start..end); - } - - for trimmed_range in trimmed_selections { - if is_first { - is_first = false; - } else { - text += "\n"; - } - let mut len = 0; - for chunk in buffer.text_for_range(trimmed_range.start..trimmed_range.end) { - text.push_str(chunk); - len += chunk.len(); - } - clipboard_selections.push(ClipboardSelection { - len, - is_entire_line, - first_line_indent: buffer - .indent_size_for_line(MultiBufferRow(trimmed_range.start.row)) - .len, - }); - } - } - } - - cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata( - text, - clipboard_selections, - )); - } - - pub fn do_paste( - &mut self, - text: &String, - clipboard_selections: Option>, - handle_entire_lines: bool, - window: &mut Window, - cx: &mut Context, - ) { - if self.read_only(cx) { - return; - } - - let clipboard_text = Cow::Borrowed(text); - - self.transact(window, cx, |this, window, cx| { - if let Some(mut clipboard_selections) = clipboard_selections { - let old_selections = this.selections.all::(cx); - let all_selections_were_entire_line = - clipboard_selections.iter().all(|s| s.is_entire_line); - let first_selection_indent_column = - clipboard_selections.first().map(|s| s.first_line_indent); - if clipboard_selections.len() != old_selections.len() { - clipboard_selections.drain(..); - } - let cursor_offset = this.selections.last::(cx).head(); - let mut auto_indent_on_paste = true; - - this.buffer.update(cx, |buffer, cx| { - let snapshot = buffer.read(cx); - auto_indent_on_paste = snapshot - .language_settings_at(cursor_offset, cx) - .auto_indent_on_paste; - - let mut start_offset = 0; - let mut edits = Vec::new(); - let mut original_indent_columns = Vec::new(); - for (ix, selection) in old_selections.iter().enumerate() { - let to_insert; - let entire_line; - let original_indent_column; - if let Some(clipboard_selection) = clipboard_selections.get(ix) { - let end_offset = start_offset + clipboard_selection.len; - to_insert = &clipboard_text[start_offset..end_offset]; - entire_line = clipboard_selection.is_entire_line; - start_offset = end_offset + 1; - original_indent_column = Some(clipboard_selection.first_line_indent); - } else { - to_insert = clipboard_text.as_str(); - entire_line = all_selections_were_entire_line; - original_indent_column = first_selection_indent_column - } - - // If the corresponding selection was empty when this slice of the - // clipboard text was written, then the entire line containing the - // selection was copied. If this selection is also currently empty, - // then paste the line before the current line of the buffer. - let range = if selection.is_empty() && handle_entire_lines && entire_line { - let column = selection.start.to_point(&snapshot).column as usize; - let line_start = selection.start - column; - line_start..line_start - } else { - selection.range() - }; - - edits.push((range, to_insert)); - original_indent_columns.push(original_indent_column); - } - drop(snapshot); - - buffer.edit( - edits, - if auto_indent_on_paste { - Some(AutoindentMode::Block { - original_indent_columns, - }) - } else { - None - }, - cx, - ); - }); - - let selections = this.selections.all::(cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); - } else { - this.insert(&clipboard_text, window, cx); - } - }); - } - - pub fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - if let Some(item) = cx.read_from_clipboard() { - let entries = item.entries(); - - match entries.first() { - // For now, we only support applying metadata if there's one string. In the future, we can incorporate all the selections - // of all the pasted entries. - Some(ClipboardEntry::String(clipboard_string)) if entries.len() == 1 => self - .do_paste( - clipboard_string.text(), - clipboard_string.metadata_json::>(), - true, - window, - cx, - ), - _ => self.do_paste(&item.text().unwrap_or_default(), None, true, window, cx), - } - } - } - - pub fn undo(&mut self, _: &Undo, window: &mut Window, cx: &mut Context) { - if self.read_only(cx) { - return; - } - - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - if let Some(transaction_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) { - if let Some((selections, _)) = - self.selection_history.transaction(transaction_id).cloned() - { - self.change_selections(None, window, cx, |s| { - s.select_anchors(selections.to_vec()); - }); - } else { - log::error!( - "No entry in selection_history found for undo. \ - This may correspond to a bug where undo does not update the selection. \ - If this is occurring, please add details to \ - https://github.com/zed-industries/zed/issues/22692" - ); - } - self.request_autoscroll(Autoscroll::fit(), cx); - self.unmark_text(window, cx); - self.refresh_inline_completion(true, false, window, cx); - cx.emit(EditorEvent::Edited { transaction_id }); - cx.emit(EditorEvent::TransactionUndone { transaction_id }); - } - } - - pub fn redo(&mut self, _: &Redo, window: &mut Window, cx: &mut Context) { - if self.read_only(cx) { - return; - } - - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - if let Some(transaction_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) { - if let Some((_, Some(selections))) = - self.selection_history.transaction(transaction_id).cloned() - { - self.change_selections(None, window, cx, |s| { - s.select_anchors(selections.to_vec()); - }); - } else { - log::error!( - "No entry in selection_history found for redo. \ - This may correspond to a bug where undo does not update the selection. \ - If this is occurring, please add details to \ - https://github.com/zed-industries/zed/issues/22692" - ); - } - self.request_autoscroll(Autoscroll::fit(), cx); - self.unmark_text(window, cx); - self.refresh_inline_completion(true, false, window, cx); - cx.emit(EditorEvent::Edited { transaction_id }); - } - } - - pub fn finalize_last_transaction(&mut self, cx: &mut Context) { - self.buffer - .update(cx, |buffer, cx| buffer.finalize_last_transaction(cx)); - } - - pub fn group_until_transaction(&mut self, tx_id: TransactionId, cx: &mut Context) { - self.buffer - .update(cx, |buffer, cx| buffer.group_until_transaction(tx_id, cx)); - } - - pub fn move_left(&mut self, _: &MoveLeft, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - let cursor = if selection.is_empty() { - movement::left(map, selection.start) - } else { - selection.start - }; - selection.collapse_to(cursor, SelectionGoal::None); - }); - }) - } - - pub fn select_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| (movement::left(map, head), SelectionGoal::None)); - }) - } - - pub fn move_right(&mut self, _: &MoveRight, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - let cursor = if selection.is_empty() { - movement::right(map, selection.end) - } else { - selection.end - }; - selection.collapse_to(cursor, SelectionGoal::None) - }); - }) - } - - pub fn select_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| (movement::right(map, head), SelectionGoal::None)); - }) - } - - pub fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context) { - if self.take_rename(true, window, cx).is_some() { - return; - } - - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let text_layout_details = &self.text_layout_details(window); - let selection_count = self.selections.count(); - let first_selection = self.selections.first_anchor(); - - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - if !selection.is_empty() { - selection.goal = SelectionGoal::None; - } - let (cursor, goal) = movement::up( - map, - selection.start, - selection.goal, - false, - text_layout_details, - ); - selection.collapse_to(cursor, goal); - }); - }); - - if selection_count == 1 && first_selection.range() == self.selections.first_anchor().range() - { - cx.propagate(); - } - } - - pub fn move_up_by_lines( - &mut self, - action: &MoveUpByLines, - window: &mut Window, - cx: &mut Context, - ) { - if self.take_rename(true, window, cx).is_some() { - return; - } - - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let text_layout_details = &self.text_layout_details(window); - - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - if !selection.is_empty() { - selection.goal = SelectionGoal::None; - } - let (cursor, goal) = movement::up_by_rows( - map, - selection.start, - action.lines, - selection.goal, - false, - text_layout_details, - ); - selection.collapse_to(cursor, goal); - }); - }) - } - - pub fn move_down_by_lines( - &mut self, - action: &MoveDownByLines, - window: &mut Window, - cx: &mut Context, - ) { - if self.take_rename(true, window, cx).is_some() { - return; - } - - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let text_layout_details = &self.text_layout_details(window); - - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - if !selection.is_empty() { - selection.goal = SelectionGoal::None; - } - let (cursor, goal) = movement::down_by_rows( - map, - selection.start, - action.lines, - selection.goal, - false, - text_layout_details, - ); - selection.collapse_to(cursor, goal); - }); - }) - } - - pub fn select_down_by_lines( - &mut self, - action: &SelectDownByLines, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, goal| { - movement::down_by_rows(map, head, action.lines, goal, false, text_layout_details) - }) - }) - } - - pub fn select_up_by_lines( - &mut self, - action: &SelectUpByLines, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, goal| { - movement::up_by_rows(map, head, action.lines, goal, false, text_layout_details) - }) - }) - } - - pub fn select_page_up( - &mut self, - _: &SelectPageUp, - window: &mut Window, - cx: &mut Context, - ) { - let Some(row_count) = self.visible_row_count() else { - return; - }; - - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let text_layout_details = &self.text_layout_details(window); - - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, goal| { - movement::up_by_rows(map, head, row_count, goal, false, text_layout_details) - }) - }) - } - - pub fn move_page_up( - &mut self, - action: &MovePageUp, - window: &mut Window, - cx: &mut Context, - ) { - if self.take_rename(true, window, cx).is_some() { - return; - } - - if self - .context_menu - .borrow_mut() - .as_mut() - .map(|menu| menu.select_first(self.completion_provider.as_deref(), cx)) - .unwrap_or(false) - { - return; - } - - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - - let Some(row_count) = self.visible_row_count() else { - return; - }; - - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let autoscroll = if action.center_cursor { - Autoscroll::center() - } else { - Autoscroll::fit() - }; - - let text_layout_details = &self.text_layout_details(window); - - self.change_selections(Some(autoscroll), window, cx, |s| { - s.move_with(|map, selection| { - if !selection.is_empty() { - selection.goal = SelectionGoal::None; - } - let (cursor, goal) = movement::up_by_rows( - map, - selection.end, - row_count, - selection.goal, - false, - text_layout_details, - ); - selection.collapse_to(cursor, goal); - }); - }); - } - - pub fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, goal| { - movement::up(map, head, goal, false, text_layout_details) - }) - }) - } - - pub fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context) { - self.take_rename(true, window, cx); - - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let text_layout_details = &self.text_layout_details(window); - let selection_count = self.selections.count(); - let first_selection = self.selections.first_anchor(); - - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - if !selection.is_empty() { - selection.goal = SelectionGoal::None; - } - let (cursor, goal) = movement::down( - map, - selection.end, - selection.goal, - false, - text_layout_details, - ); - selection.collapse_to(cursor, goal); - }); - }); - - if selection_count == 1 && first_selection.range() == self.selections.first_anchor().range() - { - cx.propagate(); - } - } - - pub fn select_page_down( - &mut self, - _: &SelectPageDown, - window: &mut Window, - cx: &mut Context, - ) { - let Some(row_count) = self.visible_row_count() else { - return; - }; - - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let text_layout_details = &self.text_layout_details(window); - - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, goal| { - movement::down_by_rows(map, head, row_count, goal, false, text_layout_details) - }) - }) - } - - pub fn move_page_down( - &mut self, - action: &MovePageDown, - window: &mut Window, - cx: &mut Context, - ) { - if self.take_rename(true, window, cx).is_some() { - return; - } - - if self - .context_menu - .borrow_mut() - .as_mut() - .map(|menu| menu.select_last(self.completion_provider.as_deref(), cx)) - .unwrap_or(false) - { - return; - } - - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - - let Some(row_count) = self.visible_row_count() else { - return; - }; - - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let autoscroll = if action.center_cursor { - Autoscroll::center() - } else { - Autoscroll::fit() - }; - - let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(autoscroll), window, cx, |s| { - s.move_with(|map, selection| { - if !selection.is_empty() { - selection.goal = SelectionGoal::None; - } - let (cursor, goal) = movement::down_by_rows( - map, - selection.end, - row_count, - selection.goal, - false, - text_layout_details, - ); - selection.collapse_to(cursor, goal); - }); - }); - } - - pub fn select_down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, goal| { - movement::down(map, head, goal, false, text_layout_details) - }) - }); - } - - pub fn context_menu_first( - &mut self, - _: &ContextMenuFirst, - _window: &mut Window, - cx: &mut Context, - ) { - if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { - context_menu.select_first(self.completion_provider.as_deref(), cx); - } - } - - pub fn context_menu_prev( - &mut self, - _: &ContextMenuPrevious, - _window: &mut Window, - cx: &mut Context, - ) { - if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { - context_menu.select_prev(self.completion_provider.as_deref(), cx); - } - } - - pub fn context_menu_next( - &mut self, - _: &ContextMenuNext, - _window: &mut Window, - cx: &mut Context, - ) { - if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { - context_menu.select_next(self.completion_provider.as_deref(), cx); - } - } - - pub fn context_menu_last( - &mut self, - _: &ContextMenuLast, - _window: &mut Window, - cx: &mut Context, - ) { - if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { - context_menu.select_last(self.completion_provider.as_deref(), cx); - } - } - - pub fn move_to_previous_word_start( - &mut self, - _: &MoveToPreviousWordStart, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_cursors_with(|map, head, _| { - ( - movement::previous_word_start(map, head), - SelectionGoal::None, - ) - }); - }) - } - - pub fn move_to_previous_subword_start( - &mut self, - _: &MoveToPreviousSubwordStart, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_cursors_with(|map, head, _| { - ( - movement::previous_subword_start(map, head), - SelectionGoal::None, - ) - }); - }) - } - - pub fn select_to_previous_word_start( - &mut self, - _: &SelectToPreviousWordStart, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - ( - movement::previous_word_start(map, head), - SelectionGoal::None, - ) - }); - }) - } - - pub fn select_to_previous_subword_start( - &mut self, - _: &SelectToPreviousSubwordStart, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - ( - movement::previous_subword_start(map, head), - SelectionGoal::None, - ) - }); - }) - } - - pub fn delete_to_previous_word_start( - &mut self, - action: &DeleteToPreviousWordStart, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - this.select_autoclose_pair(window, cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - if selection.is_empty() { - let cursor = if action.ignore_newlines { - movement::previous_word_start(map, selection.head()) - } else { - movement::previous_word_start_or_newline(map, selection.head()) - }; - selection.set_head(cursor, SelectionGoal::None); - } - }); - }); - this.insert("", window, cx); - }); - } - - pub fn delete_to_previous_subword_start( - &mut self, - _: &DeleteToPreviousSubwordStart, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - this.select_autoclose_pair(window, cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - if selection.is_empty() { - let cursor = movement::previous_subword_start(map, selection.head()); - selection.set_head(cursor, SelectionGoal::None); - } - }); - }); - this.insert("", window, cx); - }); - } - - pub fn move_to_next_word_end( - &mut self, - _: &MoveToNextWordEnd, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_cursors_with(|map, head, _| { - (movement::next_word_end(map, head), SelectionGoal::None) - }); - }) - } - - pub fn move_to_next_subword_end( - &mut self, - _: &MoveToNextSubwordEnd, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_cursors_with(|map, head, _| { - (movement::next_subword_end(map, head), SelectionGoal::None) - }); - }) - } - - pub fn select_to_next_word_end( - &mut self, - _: &SelectToNextWordEnd, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - (movement::next_word_end(map, head), SelectionGoal::None) - }); - }) - } - - pub fn select_to_next_subword_end( - &mut self, - _: &SelectToNextSubwordEnd, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - (movement::next_subword_end(map, head), SelectionGoal::None) - }); - }) - } - - pub fn delete_to_next_word_end( - &mut self, - action: &DeleteToNextWordEnd, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - if selection.is_empty() { - let cursor = if action.ignore_newlines { - movement::next_word_end(map, selection.head()) - } else { - movement::next_word_end_or_newline(map, selection.head()) - }; - selection.set_head(cursor, SelectionGoal::None); - } - }); - }); - this.insert("", window, cx); - }); - } - - pub fn delete_to_next_subword_end( - &mut self, - _: &DeleteToNextSubwordEnd, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - if selection.is_empty() { - let cursor = movement::next_subword_end(map, selection.head()); - selection.set_head(cursor, SelectionGoal::None); - } - }); - }); - this.insert("", window, cx); - }); - } - - pub fn move_to_beginning_of_line( - &mut self, - action: &MoveToBeginningOfLine, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_cursors_with(|map, head, _| { - ( - movement::indented_line_beginning( - map, - head, - action.stop_at_soft_wraps, - action.stop_at_indent, - ), - SelectionGoal::None, - ) - }); - }) - } - - pub fn select_to_beginning_of_line( - &mut self, - action: &SelectToBeginningOfLine, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - ( - movement::indented_line_beginning( - map, - head, - action.stop_at_soft_wraps, - action.stop_at_indent, - ), - SelectionGoal::None, - ) - }); - }); - } - - pub fn delete_to_beginning_of_line( - &mut self, - action: &DeleteToBeginningOfLine, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|_, selection| { - selection.reversed = true; - }); - }); - - this.select_to_beginning_of_line( - &SelectToBeginningOfLine { - stop_at_soft_wraps: false, - stop_at_indent: action.stop_at_indent, - }, - window, - cx, - ); - this.backspace(&Backspace, window, cx); - }); - } - - pub fn move_to_end_of_line( - &mut self, - action: &MoveToEndOfLine, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_cursors_with(|map, head, _| { - ( - movement::line_end(map, head, action.stop_at_soft_wraps), - SelectionGoal::None, - ) - }); - }) - } - - pub fn select_to_end_of_line( - &mut self, - action: &SelectToEndOfLine, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - ( - movement::line_end(map, head, action.stop_at_soft_wraps), - SelectionGoal::None, - ) - }); - }) - } - - pub fn delete_to_end_of_line( - &mut self, - _: &DeleteToEndOfLine, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - this.select_to_end_of_line( - &SelectToEndOfLine { - stop_at_soft_wraps: false, - }, - window, - cx, - ); - this.delete(&Delete, window, cx); - }); - } - - pub fn cut_to_end_of_line( - &mut self, - _: &CutToEndOfLine, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - this.select_to_end_of_line( - &SelectToEndOfLine { - stop_at_soft_wraps: false, - }, - window, - cx, - ); - this.cut(&Cut, window, cx); - }); - } - - pub fn move_to_start_of_paragraph( - &mut self, - _: &MoveToStartOfParagraph, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - selection.collapse_to( - movement::start_of_paragraph(map, selection.head(), 1), - SelectionGoal::None, - ) - }); - }) - } - - pub fn move_to_end_of_paragraph( - &mut self, - _: &MoveToEndOfParagraph, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - selection.collapse_to( - movement::end_of_paragraph(map, selection.head(), 1), - SelectionGoal::None, - ) - }); - }) - } - - pub fn select_to_start_of_paragraph( - &mut self, - _: &SelectToStartOfParagraph, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - ( - movement::start_of_paragraph(map, head, 1), - SelectionGoal::None, - ) - }); - }) - } - - pub fn select_to_end_of_paragraph( - &mut self, - _: &SelectToEndOfParagraph, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - ( - movement::end_of_paragraph(map, head, 1), - SelectionGoal::None, - ) - }); - }) - } - - pub fn move_to_start_of_excerpt( - &mut self, - _: &MoveToStartOfExcerpt, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - selection.collapse_to( - movement::start_of_excerpt( - map, - selection.head(), - workspace::searchable::Direction::Prev, - ), - SelectionGoal::None, - ) - }); - }) - } - - pub fn move_to_start_of_next_excerpt( - &mut self, - _: &MoveToStartOfNextExcerpt, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - selection.collapse_to( - movement::start_of_excerpt( - map, - selection.head(), - workspace::searchable::Direction::Next, - ), - SelectionGoal::None, - ) - }); - }) - } - - pub fn move_to_end_of_excerpt( - &mut self, - _: &MoveToEndOfExcerpt, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - selection.collapse_to( - movement::end_of_excerpt( - map, - selection.head(), - workspace::searchable::Direction::Next, - ), - SelectionGoal::None, - ) - }); - }) - } - - pub fn move_to_end_of_previous_excerpt( - &mut self, - _: &MoveToEndOfPreviousExcerpt, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - selection.collapse_to( - movement::end_of_excerpt( - map, - selection.head(), - workspace::searchable::Direction::Prev, - ), - SelectionGoal::None, - ) - }); - }) - } - - pub fn select_to_start_of_excerpt( - &mut self, - _: &SelectToStartOfExcerpt, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - ( - movement::start_of_excerpt(map, head, workspace::searchable::Direction::Prev), - SelectionGoal::None, - ) - }); - }) - } - - pub fn select_to_start_of_next_excerpt( - &mut self, - _: &SelectToStartOfNextExcerpt, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - ( - movement::start_of_excerpt(map, head, workspace::searchable::Direction::Next), - SelectionGoal::None, - ) - }); - }) - } - - pub fn select_to_end_of_excerpt( - &mut self, - _: &SelectToEndOfExcerpt, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - ( - movement::end_of_excerpt(map, head, workspace::searchable::Direction::Next), - SelectionGoal::None, - ) - }); - }) - } - - pub fn select_to_end_of_previous_excerpt( - &mut self, - _: &SelectToEndOfPreviousExcerpt, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - ( - movement::end_of_excerpt(map, head, workspace::searchable::Direction::Prev), - SelectionGoal::None, - ) - }); - }) - } - - pub fn move_to_beginning( - &mut self, - _: &MoveToBeginning, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_ranges(vec![0..0]); - }); - } - - pub fn select_to_beginning( - &mut self, - _: &SelectToBeginning, - window: &mut Window, - cx: &mut Context, - ) { - let mut selection = self.selections.last::(cx); - selection.set_head(Point::zero(), SelectionGoal::None); - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(vec![selection]); - }); - } - - pub fn move_to_end(&mut self, _: &MoveToEnd, window: &mut Window, cx: &mut Context) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - let cursor = self.buffer.read(cx).read(cx).len(); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_ranges(vec![cursor..cursor]) - }); - } - - pub fn set_nav_history(&mut self, nav_history: Option) { - self.nav_history = nav_history; - } - - pub fn nav_history(&self) -> Option<&ItemNavHistory> { - self.nav_history.as_ref() - } - - pub fn create_nav_history_entry(&mut self, cx: &mut Context) { - self.push_to_nav_history(self.selections.newest_anchor().head(), None, false, cx); - } - - fn push_to_nav_history( - &mut self, - cursor_anchor: Anchor, - new_position: Option, - is_deactivate: bool, - cx: &mut Context, - ) { - if let Some(nav_history) = self.nav_history.as_mut() { - let buffer = self.buffer.read(cx).read(cx); - let cursor_position = cursor_anchor.to_point(&buffer); - let scroll_state = self.scroll_manager.anchor(); - let scroll_top_row = scroll_state.top_row(&buffer); - drop(buffer); - - if let Some(new_position) = new_position { - let row_delta = (new_position.row as i64 - cursor_position.row as i64).abs(); - if row_delta < MIN_NAVIGATION_HISTORY_ROW_DELTA { - return; - } - } - - nav_history.push( - Some(NavigationData { - cursor_anchor, - cursor_position, - scroll_anchor: scroll_state, - scroll_top_row, - }), - cx, - ); - cx.emit(EditorEvent::PushedToNavHistory { - anchor: cursor_anchor, - is_deactivate, - }) - } - } - - pub fn select_to_end(&mut self, _: &SelectToEnd, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - let buffer = self.buffer.read(cx).snapshot(cx); - let mut selection = self.selections.first::(cx); - selection.set_head(buffer.len(), SelectionGoal::None); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(vec![selection]); - }); - } - - pub fn select_all(&mut self, _: &SelectAll, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - let end = self.buffer.read(cx).read(cx).len(); - self.change_selections(None, window, cx, |s| { - s.select_ranges(vec![0..end]); - }); - } - - pub fn select_line(&mut self, _: &SelectLine, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.selections.all::(cx); - let max_point = display_map.buffer_snapshot.max_point(); - for selection in &mut selections { - let rows = selection.spanned_rows(true, &display_map); - selection.start = Point::new(rows.start.0, 0); - selection.end = cmp::min(max_point, Point::new(rows.end.0, 0)); - selection.reversed = false; - } - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections); - }); - } - - pub fn split_selection_into_lines( - &mut self, - _: &SplitSelectionIntoLines, - window: &mut Window, - cx: &mut Context, - ) { - let selections = self - .selections - .all::(cx) - .into_iter() - .map(|selection| selection.start..selection.end) - .collect::>(); - self.unfold_ranges(&selections, true, true, cx); - - let mut new_selection_ranges = Vec::new(); - { - let buffer = self.buffer.read(cx).read(cx); - for selection in selections { - for row in selection.start.row..selection.end.row { - 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; - // Don't insert last one if it's a multi-line selection ending at the start of a line, - // 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 { - new_selection_ranges.push(selection.end..selection.end); - } - } - } - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_ranges(new_selection_ranges); - }); - } - - pub fn add_selection_above( - &mut self, - _: &AddSelectionAbove, - window: &mut Window, - cx: &mut Context, - ) { - self.add_selection(true, window, cx); - } - - pub fn add_selection_below( - &mut self, - _: &AddSelectionBelow, - window: &mut Window, - cx: &mut Context, - ) { - self.add_selection(false, window, cx); - } - - fn add_selection(&mut self, above: bool, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.selections.all::(cx); - let text_layout_details = self.text_layout_details(window); - let mut state = self.add_selections_state.take().unwrap_or_else(|| { - let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone(); - let range = oldest_selection.display_range(&display_map).sorted(); - - let start_x = display_map.x_for_display_point(range.start, &text_layout_details); - let end_x = display_map.x_for_display_point(range.end, &text_layout_details); - let positions = start_x.min(end_x)..start_x.max(end_x); - - selections.clear(); - let mut stack = Vec::new(); - for row in range.start.row().0..=range.end.row().0 { - if let Some(selection) = self.selections.build_columnar_selection( - &display_map, - DisplayRow(row), - &positions, - oldest_selection.reversed, - &text_layout_details, - ) { - stack.push(selection.id); - selections.push(selection); - } - } - - if above { - stack.reverse(); - } - - AddSelectionsState { above, stack } - }); - - let last_added_selection = *state.stack.last().unwrap(); - let mut new_selections = Vec::new(); - if above == state.above { - let end_row = if above { - DisplayRow(0) - } else { - display_map.max_point().row() - }; - - 'outer: for selection in selections { - if selection.id == last_added_selection { - let range = selection.display_range(&display_map).sorted(); - debug_assert_eq!(range.start.row(), range.end.row()); - let mut row = range.start.row(); - let positions = - if let SelectionGoal::HorizontalRange { start, end } = selection.goal { - px(start)..px(end) - } else { - let start_x = - display_map.x_for_display_point(range.start, &text_layout_details); - let end_x = - display_map.x_for_display_point(range.end, &text_layout_details); - start_x.min(end_x)..start_x.max(end_x) - }; - - while row != end_row { - if above { - row.0 -= 1; - } else { - row.0 += 1; - } - - if let Some(new_selection) = self.selections.build_columnar_selection( - &display_map, - row, - &positions, - selection.reversed, - &text_layout_details, - ) { - state.stack.push(new_selection.id); - if above { - new_selections.push(new_selection); - new_selections.push(selection); - } else { - new_selections.push(selection); - new_selections.push(new_selection); - } - - continue 'outer; - } - } - } - - new_selections.push(selection); - } - } else { - new_selections = selections; - new_selections.retain(|s| s.id != last_added_selection); - state.stack.pop(); - } - - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(new_selections); - }); - if state.stack.len() > 1 { - self.add_selections_state = Some(state); - } - } - - pub fn select_next_match_internal( - &mut self, - display_map: &DisplaySnapshot, - replace_newest: bool, - autoscroll: Option, - window: &mut Window, - cx: &mut Context, - ) -> Result<()> { - fn select_next_match_ranges( - this: &mut Editor, - range: Range, - reversed: bool, - replace_newest: bool, - auto_scroll: Option, - window: &mut Window, - cx: &mut Context, - ) { - this.unfold_ranges(&[range.clone()], false, auto_scroll.is_some(), cx); - this.change_selections(auto_scroll, window, cx, |s| { - if replace_newest { - s.delete(s.newest_anchor().id); - } - if reversed { - s.insert_range(range.end..range.start); - } else { - s.insert_range(range); - } - }); - } - - let buffer = &display_map.buffer_snapshot; - let mut selections = self.selections.all::(cx); - if let Some(mut select_next_state) = self.select_next_state.take() { - let query = &select_next_state.query; - if !select_next_state.done { - let first_selection = selections.iter().min_by_key(|s| s.id).unwrap(); - let last_selection = selections.iter().max_by_key(|s| s.id).unwrap(); - let mut next_selected_range = None; - - let bytes_after_last_selection = - buffer.bytes_in_range(last_selection.end..buffer.len()); - let bytes_before_first_selection = buffer.bytes_in_range(0..first_selection.start); - let query_matches = query - .stream_find_iter(bytes_after_last_selection) - .map(|result| (last_selection.end, result)) - .chain( - query - .stream_find_iter(bytes_before_first_selection) - .map(|result| (0, result)), - ); - - for (start_offset, query_match) in query_matches { - let query_match = query_match.unwrap(); // can only fail due to I/O - let offset_range = - start_offset + query_match.start()..start_offset + query_match.end(); - let display_range = offset_range.start.to_display_point(display_map) - ..offset_range.end.to_display_point(display_map); - - if !select_next_state.wordwise - || (!movement::is_inside_word(display_map, display_range.start) - && !movement::is_inside_word(display_map, display_range.end)) - { - // TODO: This is n^2, because we might check all the selections - if !selections - .iter() - .any(|selection| selection.range().overlaps(&offset_range)) - { - next_selected_range = Some(offset_range); - break; - } - } - } - - if let Some(next_selected_range) = next_selected_range { - select_next_match_ranges( - self, - next_selected_range, - last_selection.reversed, - replace_newest, - autoscroll, - window, - cx, - ); - } else { - select_next_state.done = true; - } - } - - self.select_next_state = Some(select_next_state); - } else { - let mut only_carets = true; - let mut same_text_selected = true; - let mut selected_text = None; - - let mut selections_iter = selections.iter().peekable(); - while let Some(selection) = selections_iter.next() { - if selection.start != selection.end { - only_carets = false; - } - - if same_text_selected { - if selected_text.is_none() { - selected_text = - Some(buffer.text_for_range(selection.range()).collect::()); - } - - if let Some(next_selection) = selections_iter.peek() { - if next_selection.range().len() == selection.range().len() { - let next_selected_text = buffer - .text_for_range(next_selection.range()) - .collect::(); - if Some(next_selected_text) != selected_text { - same_text_selected = false; - selected_text = None; - } - } else { - same_text_selected = false; - selected_text = None; - } - } - } - } - - if only_carets { - for selection in &mut selections { - let word_range = movement::surrounding_word( - display_map, - selection.start.to_display_point(display_map), - ); - selection.start = word_range.start.to_offset(display_map, Bias::Left); - selection.end = word_range.end.to_offset(display_map, Bias::Left); - selection.goal = SelectionGoal::None; - selection.reversed = false; - select_next_match_ranges( - self, - selection.start..selection.end, - selection.reversed, - replace_newest, - autoscroll, - window, - cx, - ); - } - - if selections.len() == 1 { - let selection = selections - .last() - .expect("ensured that there's only one selection"); - let query = buffer - .text_for_range(selection.start..selection.end) - .collect::(); - let is_empty = query.is_empty(); - let select_state = SelectNextState { - query: AhoCorasick::new(&[query])?, - wordwise: true, - done: is_empty, - }; - self.select_next_state = Some(select_state); - } else { - self.select_next_state = None; - } - } else if let Some(selected_text) = selected_text { - self.select_next_state = Some(SelectNextState { - query: AhoCorasick::new(&[selected_text])?, - wordwise: false, - done: false, - }); - self.select_next_match_internal( - display_map, - replace_newest, - autoscroll, - window, - cx, - )?; - } - } - Ok(()) - } - - pub fn select_all_matches( - &mut self, - _action: &SelectAllMatches, - window: &mut Window, - cx: &mut Context, - ) -> Result<()> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - self.push_to_selection_history(); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - - self.select_next_match_internal(&display_map, false, None, window, cx)?; - let Some(select_next_state) = self.select_next_state.as_mut() else { - return Ok(()); - }; - if select_next_state.done { - return Ok(()); - } - - let mut new_selections = Vec::new(); - - let reversed = self.selections.oldest::(cx).reversed; - let buffer = &display_map.buffer_snapshot; - let query_matches = select_next_state - .query - .stream_find_iter(buffer.bytes_in_range(0..buffer.len())); - - for query_match in query_matches.into_iter() { - let query_match = query_match.context("query match for select all action")?; // can only fail due to I/O - let offset_range = if reversed { - query_match.end()..query_match.start() - } else { - query_match.start()..query_match.end() - }; - let display_range = offset_range.start.to_display_point(&display_map) - ..offset_range.end.to_display_point(&display_map); - - if !select_next_state.wordwise - || (!movement::is_inside_word(&display_map, display_range.start) - && !movement::is_inside_word(&display_map, display_range.end)) - { - new_selections.push(offset_range.start..offset_range.end); - } - } - - select_next_state.done = true; - self.unfold_ranges(&new_selections.clone(), false, false, cx); - self.change_selections(None, window, cx, |selections| { - selections.select_ranges(new_selections) - }); - - Ok(()) - } - - pub fn select_next( - &mut self, - action: &SelectNext, - window: &mut Window, - cx: &mut Context, - ) -> Result<()> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.push_to_selection_history(); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - self.select_next_match_internal( - &display_map, - action.replace_newest, - Some(Autoscroll::newest()), - window, - cx, - )?; - Ok(()) - } - - pub fn select_previous( - &mut self, - action: &SelectPrevious, - window: &mut Window, - cx: &mut Context, - ) -> Result<()> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.push_to_selection_history(); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - let mut selections = self.selections.all::(cx); - if let Some(mut select_prev_state) = self.select_prev_state.take() { - let query = &select_prev_state.query; - if !select_prev_state.done { - let first_selection = selections.iter().min_by_key(|s| s.id).unwrap(); - let last_selection = selections.iter().max_by_key(|s| s.id).unwrap(); - let mut next_selected_range = None; - // When we're iterating matches backwards, the oldest match will actually be the furthest one in the buffer. - let bytes_before_last_selection = - buffer.reversed_bytes_in_range(0..last_selection.start); - let bytes_after_first_selection = - buffer.reversed_bytes_in_range(first_selection.end..buffer.len()); - let query_matches = query - .stream_find_iter(bytes_before_last_selection) - .map(|result| (last_selection.start, result)) - .chain( - query - .stream_find_iter(bytes_after_first_selection) - .map(|result| (buffer.len(), result)), - ); - for (end_offset, query_match) in query_matches { - let query_match = query_match.unwrap(); // can only fail due to I/O - let offset_range = - end_offset - query_match.end()..end_offset - query_match.start(); - let display_range = offset_range.start.to_display_point(&display_map) - ..offset_range.end.to_display_point(&display_map); - - if !select_prev_state.wordwise - || (!movement::is_inside_word(&display_map, display_range.start) - && !movement::is_inside_word(&display_map, display_range.end)) - { - next_selected_range = Some(offset_range); - break; - } - } - - if let Some(next_selected_range) = next_selected_range { - self.unfold_ranges(&[next_selected_range.clone()], false, true, cx); - self.change_selections(Some(Autoscroll::newest()), window, cx, |s| { - if action.replace_newest { - s.delete(s.newest_anchor().id); - } - if last_selection.reversed { - s.insert_range(next_selected_range.end..next_selected_range.start); - } else { - s.insert_range(next_selected_range); - } - }); - } else { - select_prev_state.done = true; - } - } - - self.select_prev_state = Some(select_prev_state); - } else { - let mut only_carets = true; - let mut same_text_selected = true; - let mut selected_text = None; - - let mut selections_iter = selections.iter().peekable(); - while let Some(selection) = selections_iter.next() { - if selection.start != selection.end { - only_carets = false; - } - - if same_text_selected { - if selected_text.is_none() { - selected_text = - Some(buffer.text_for_range(selection.range()).collect::()); - } - - if let Some(next_selection) = selections_iter.peek() { - if next_selection.range().len() == selection.range().len() { - let next_selected_text = buffer - .text_for_range(next_selection.range()) - .collect::(); - if Some(next_selected_text) != selected_text { - same_text_selected = false; - selected_text = None; - } - } else { - same_text_selected = false; - selected_text = None; - } - } - } - } - - if only_carets { - for selection in &mut selections { - let word_range = movement::surrounding_word( - &display_map, - selection.start.to_display_point(&display_map), - ); - selection.start = word_range.start.to_offset(&display_map, Bias::Left); - selection.end = word_range.end.to_offset(&display_map, Bias::Left); - selection.goal = SelectionGoal::None; - selection.reversed = false; - } - if selections.len() == 1 { - let selection = selections - .last() - .expect("ensured that there's only one selection"); - let query = buffer - .text_for_range(selection.start..selection.end) - .collect::(); - let is_empty = query.is_empty(); - let select_state = SelectNextState { - query: AhoCorasick::new(&[query.chars().rev().collect::()])?, - wordwise: true, - done: is_empty, - }; - self.select_prev_state = Some(select_state); - } else { - self.select_prev_state = None; - } - - self.unfold_ranges( - &selections.iter().map(|s| s.range()).collect::>(), - false, - true, - cx, - ); - self.change_selections(Some(Autoscroll::newest()), window, cx, |s| { - s.select(selections); - }); - } else if let Some(selected_text) = selected_text { - self.select_prev_state = Some(SelectNextState { - query: AhoCorasick::new(&[selected_text.chars().rev().collect::()])?, - wordwise: false, - done: false, - }); - self.select_previous(action, window, cx)?; - } - } - Ok(()) - } - - pub fn find_next_match( - &mut self, - _: &FindNextMatch, - window: &mut Window, - cx: &mut Context, - ) -> Result<()> { - let selections = self.selections.disjoint_anchors(); - match selections.first() { - Some(first) if selections.len() >= 2 => { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_ranges([first.range()]); - }); - } - _ => self.select_next( - &SelectNext { - replace_newest: true, - }, - window, - cx, - )?, - } - Ok(()) - } - - pub fn find_previous_match( - &mut self, - _: &FindPreviousMatch, - window: &mut Window, - cx: &mut Context, - ) -> Result<()> { - let selections = self.selections.disjoint_anchors(); - match selections.last() { - Some(last) if selections.len() >= 2 => { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_ranges([last.range()]); - }); - } - _ => self.select_previous( - &SelectPrevious { - replace_newest: true, - }, - window, - cx, - )?, - } - Ok(()) - } - - pub fn toggle_comments( - &mut self, - action: &ToggleComments, - window: &mut Window, - cx: &mut Context, - ) { - if self.read_only(cx) { - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let text_layout_details = &self.text_layout_details(window); - self.transact(window, cx, |this, window, cx| { - let mut selections = this.selections.all::(cx); - let mut edits = Vec::new(); - let mut selection_edit_ranges = Vec::new(); - let mut last_toggled_row = None; - let snapshot = this.buffer.read(cx).read(cx); - let empty_str: Arc = Arc::default(); - let mut suffixes_inserted = Vec::new(); - let ignore_indent = action.ignore_indent; - - fn comment_prefix_range( - snapshot: &MultiBufferSnapshot, - row: MultiBufferRow, - comment_prefix: &str, - comment_prefix_whitespace: &str, - ignore_indent: bool, - ) -> Range { - let indent_size = if ignore_indent { - 0 - } else { - snapshot.indent_size_for_line(row).len - }; - - let start = Point::new(row.0, indent_size); - - let mut line_bytes = snapshot - .bytes_in_range(start..snapshot.max_point()) - .flatten() - .copied(); - - // If this line currently begins with the line comment prefix, then record - // the range containing the prefix. - if line_bytes - .by_ref() - .take(comment_prefix.len()) - .eq(comment_prefix.bytes()) - { - // Include any whitespace that matches the comment prefix. - let matching_whitespace_len = line_bytes - .zip(comment_prefix_whitespace.bytes()) - .take_while(|(a, b)| a == b) - .count() as u32; - let end = Point::new( - start.row, - start.column + comment_prefix.len() as u32 + matching_whitespace_len, - ); - start..end - } else { - start..start - } - } - - fn comment_suffix_range( - snapshot: &MultiBufferSnapshot, - row: MultiBufferRow, - comment_suffix: &str, - comment_suffix_has_leading_space: bool, - ) -> Range { - let end = Point::new(row.0, snapshot.line_len(row)); - let suffix_start_column = end.column.saturating_sub(comment_suffix.len() as u32); - - let mut line_end_bytes = snapshot - .bytes_in_range(Point::new(end.row, suffix_start_column.saturating_sub(1))..end) - .flatten() - .copied(); - - let leading_space_len = if suffix_start_column > 0 - && line_end_bytes.next() == Some(b' ') - && comment_suffix_has_leading_space - { - 1 - } else { - 0 - }; - - // If this line currently begins with the line comment prefix, then record - // the range containing the prefix. - if line_end_bytes.by_ref().eq(comment_suffix.bytes()) { - let start = Point::new(end.row, suffix_start_column - leading_space_len); - start..end - } else { - end..end - } - } - - // TODO: Handle selections that cross excerpts - for selection in &mut selections { - let start_column = snapshot - .indent_size_for_line(MultiBufferRow(selection.start.row)) - .len; - let language = if let Some(language) = - snapshot.language_scope_at(Point::new(selection.start.row, start_column)) - { - language - } else { - continue; - }; - - selection_edit_ranges.clear(); - - // If multiple selections contain a given row, avoid processing that - // row more than once. - let mut start_row = MultiBufferRow(selection.start.row); - if last_toggled_row == Some(start_row) { - start_row = start_row.next_row(); - } - let end_row = - if selection.end.row > selection.start.row && selection.end.column == 0 { - MultiBufferRow(selection.end.row - 1) - } else { - MultiBufferRow(selection.end.row) - }; - last_toggled_row = Some(end_row); - - if start_row > end_row { - continue; - } - - // If the language has line comments, toggle those. - let mut full_comment_prefixes = language.line_comment_prefixes().to_vec(); - - // If ignore_indent is set, trim spaces from the right side of all full_comment_prefixes - if ignore_indent { - full_comment_prefixes = full_comment_prefixes - .into_iter() - .map(|s| Arc::from(s.trim_end())) - .collect(); - } - - if !full_comment_prefixes.is_empty() { - let first_prefix = full_comment_prefixes - .first() - .expect("prefixes is non-empty"); - let prefix_trimmed_lengths = full_comment_prefixes - .iter() - .map(|p| p.trim_end_matches(' ').len()) - .collect::>(); - - let mut all_selection_lines_are_comments = true; - - for row in start_row.0..=end_row.0 { - let row = MultiBufferRow(row); - if start_row < end_row && snapshot.is_line_blank(row) { - continue; - } - - let prefix_range = full_comment_prefixes - .iter() - .zip(prefix_trimmed_lengths.iter().copied()) - .map(|(prefix, trimmed_prefix_len)| { - comment_prefix_range( - snapshot.deref(), - row, - &prefix[..trimmed_prefix_len], - &prefix[trimmed_prefix_len..], - ignore_indent, - ) - }) - .max_by_key(|range| range.end.column - range.start.column) - .expect("prefixes is non-empty"); - - if prefix_range.is_empty() { - all_selection_lines_are_comments = false; - } - - selection_edit_ranges.push(prefix_range); - } - - if all_selection_lines_are_comments { - edits.extend( - selection_edit_ranges - .iter() - .cloned() - .map(|range| (range, empty_str.clone())), - ); - } else { - let min_column = selection_edit_ranges - .iter() - .map(|range| range.start.column) - .min() - .unwrap_or(0); - edits.extend(selection_edit_ranges.iter().map(|range| { - let position = Point::new(range.start.row, min_column); - (position..position, first_prefix.clone()) - })); - } - } else if let Some((full_comment_prefix, comment_suffix)) = - language.block_comment_delimiters() - { - let comment_prefix = full_comment_prefix.trim_end_matches(' '); - let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; - let prefix_range = comment_prefix_range( - snapshot.deref(), - start_row, - comment_prefix, - comment_prefix_whitespace, - ignore_indent, - ); - let suffix_range = comment_suffix_range( - snapshot.deref(), - end_row, - comment_suffix.trim_start_matches(' '), - comment_suffix.starts_with(' '), - ); - - if prefix_range.is_empty() || suffix_range.is_empty() { - edits.push(( - prefix_range.start..prefix_range.start, - full_comment_prefix.clone(), - )); - edits.push((suffix_range.end..suffix_range.end, comment_suffix.clone())); - suffixes_inserted.push((end_row, comment_suffix.len())); - } else { - edits.push((prefix_range, empty_str.clone())); - edits.push((suffix_range, empty_str.clone())); - } - } else { - continue; - } - } - - drop(snapshot); - this.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, None, cx); - }); - - // Adjust selections so that they end before any comment suffixes that - // were inserted. - let mut suffixes_inserted = suffixes_inserted.into_iter().peekable(); - let mut selections = this.selections.all::(cx); - let snapshot = this.buffer.read(cx).read(cx); - for selection in &mut selections { - while let Some((row, suffix_len)) = suffixes_inserted.peek().copied() { - match row.cmp(&MultiBufferRow(selection.end.row)) { - Ordering::Less => { - suffixes_inserted.next(); - continue; - } - Ordering::Greater => break, - Ordering::Equal => { - if selection.end.column == snapshot.line_len(row) { - if selection.is_empty() { - selection.start.column -= suffix_len as u32; - } - selection.end.column -= suffix_len as u32; - } - break; - } - } - } - } - - drop(snapshot); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); - - let selections = this.selections.all::(cx); - let selections_on_single_row = selections.windows(2).all(|selections| { - selections[0].start.row == selections[1].start.row - && selections[0].end.row == selections[1].end.row - && selections[0].start.row == selections[0].end.row - }); - let selections_selecting = selections - .iter() - .any(|selection| selection.start != selection.end); - let advance_downwards = action.advance_downwards - && selections_on_single_row - && !selections_selecting - && !matches!(this.mode, EditorMode::SingleLine { .. }); - - if advance_downwards { - let snapshot = this.buffer.read(cx).snapshot(cx); - - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_cursors_with(|display_snapshot, display_point, _| { - let mut point = display_point.to_point(display_snapshot); - point.row += 1; - point = snapshot.clip_point(point, Bias::Left); - let display_point = point.to_display_point(display_snapshot); - let goal = SelectionGoal::HorizontalPosition( - display_snapshot - .x_for_display_point(display_point, text_layout_details) - .into(), - ); - (display_point, goal) - }) - }); - } - }); - } - - pub fn select_enclosing_symbol( - &mut self, - _: &SelectEnclosingSymbol, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let buffer = self.buffer.read(cx).snapshot(cx); - let old_selections = self.selections.all::(cx).into_boxed_slice(); - - fn update_selection( - selection: &Selection, - buffer_snap: &MultiBufferSnapshot, - ) -> Option> { - let cursor = selection.head(); - let (_buffer_id, symbols) = buffer_snap.symbols_containing(cursor, None)?; - for symbol in symbols.iter().rev() { - let start = symbol.range.start.to_offset(buffer_snap); - let end = symbol.range.end.to_offset(buffer_snap); - let new_range = start..end; - if start < selection.start || end > selection.end { - return Some(Selection { - id: selection.id, - start: new_range.start, - end: new_range.end, - goal: SelectionGoal::None, - reversed: selection.reversed, - }); - } - } - None - } - - let mut selected_larger_symbol = false; - let new_selections = old_selections - .iter() - .map(|selection| match update_selection(selection, &buffer) { - Some(new_selection) => { - if new_selection.range() != selection.range() { - selected_larger_symbol = true; - } - new_selection - } - None => selection.clone(), - }) - .collect::>(); - - if selected_larger_symbol { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(new_selections); - }); - } - } - - pub fn select_larger_syntax_node( - &mut self, - _: &SelectLargerSyntaxNode, - window: &mut Window, - cx: &mut Context, - ) { - let Some(visible_row_count) = self.visible_row_count() else { - return; - }; - let old_selections: Box<[_]> = self.selections.all::(cx).into(); - if old_selections.is_empty() { - return; - } - - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = self.buffer.read(cx).snapshot(cx); - - let mut selected_larger_node = false; - let mut new_selections = old_selections - .iter() - .map(|selection| { - let old_range = selection.start..selection.end; - - if let Some((node, _)) = buffer.syntax_ancestor(old_range.clone()) { - // manually select word at selection - if ["string_content", "inline"].contains(&node.kind()) { - let word_range = { - let display_point = buffer - .offset_to_point(old_range.start) - .to_display_point(&display_map); - let Range { start, end } = - movement::surrounding_word(&display_map, display_point); - start.to_point(&display_map).to_offset(&buffer) - ..end.to_point(&display_map).to_offset(&buffer) - }; - // ignore if word is already selected - if !word_range.is_empty() && old_range != word_range { - let last_word_range = { - let display_point = buffer - .offset_to_point(old_range.end) - .to_display_point(&display_map); - let Range { start, end } = - movement::surrounding_word(&display_map, display_point); - start.to_point(&display_map).to_offset(&buffer) - ..end.to_point(&display_map).to_offset(&buffer) - }; - // only select word if start and end point belongs to same word - if word_range == last_word_range { - selected_larger_node = true; - return Selection { - id: selection.id, - start: word_range.start, - end: word_range.end, - goal: SelectionGoal::None, - reversed: selection.reversed, - }; - } - } - } - } - - let mut new_range = old_range.clone(); - let mut new_node = None; - while let Some((node, containing_range)) = buffer.syntax_ancestor(new_range.clone()) - { - new_node = Some(node); - new_range = match containing_range { - MultiOrSingleBufferOffsetRange::Single(_) => break, - MultiOrSingleBufferOffsetRange::Multi(range) => range, - }; - if !display_map.intersects_fold(new_range.start) - && !display_map.intersects_fold(new_range.end) - { - break; - } - } - - if let Some(node) = new_node { - // Log the ancestor, to support using this action as a way to explore TreeSitter - // nodes. Parent and grandparent are also logged because this operation will not - // visit nodes that have the same range as their parent. - log::info!("Node: {node:?}"); - let parent = node.parent(); - log::info!("Parent: {parent:?}"); - let grandparent = parent.and_then(|x| x.parent()); - log::info!("Grandparent: {grandparent:?}"); - } - - selected_larger_node |= new_range != old_range; - Selection { - id: selection.id, - start: new_range.start, - end: new_range.end, - goal: SelectionGoal::None, - reversed: selection.reversed, - } - }) - .collect::>(); - - if !selected_larger_node { - return; // don't put this call in the history - } - - // scroll based on transformation done to the last selection created by the user - let (last_old, last_new) = old_selections - .last() - .zip(new_selections.last().cloned()) - .expect("old_selections isn't empty"); - - // revert selection - let is_selection_reversed = { - let should_newest_selection_be_reversed = last_old.start != last_new.start; - new_selections.last_mut().expect("checked above").reversed = - should_newest_selection_be_reversed; - should_newest_selection_be_reversed - }; - - if selected_larger_node { - self.select_syntax_node_history.disable_clearing = true; - self.change_selections(None, window, cx, |s| { - s.select(new_selections.clone()); - }); - self.select_syntax_node_history.disable_clearing = false; - } - - let start_row = last_new.start.to_display_point(&display_map).row().0; - let end_row = last_new.end.to_display_point(&display_map).row().0; - let selection_height = end_row - start_row + 1; - let scroll_margin_rows = self.vertical_scroll_margin() as u32; - - let fits_on_the_screen = visible_row_count >= selection_height + scroll_margin_rows * 2; - let scroll_behavior = if fits_on_the_screen { - self.request_autoscroll(Autoscroll::fit(), cx); - SelectSyntaxNodeScrollBehavior::FitSelection - } else if is_selection_reversed { - self.scroll_cursor_top(&ScrollCursorTop, window, cx); - SelectSyntaxNodeScrollBehavior::CursorTop - } else { - self.scroll_cursor_bottom(&ScrollCursorBottom, window, cx); - SelectSyntaxNodeScrollBehavior::CursorBottom - }; - - self.select_syntax_node_history.push(( - old_selections, - scroll_behavior, - is_selection_reversed, - )); - } - - pub fn select_smaller_syntax_node( - &mut self, - _: &SelectSmallerSyntaxNode, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - if let Some((mut selections, scroll_behavior, is_selection_reversed)) = - self.select_syntax_node_history.pop() - { - if let Some(selection) = selections.last_mut() { - selection.reversed = is_selection_reversed; - } - - self.select_syntax_node_history.disable_clearing = true; - self.change_selections(None, window, cx, |s| { - s.select(selections.to_vec()); - }); - self.select_syntax_node_history.disable_clearing = false; - - match scroll_behavior { - SelectSyntaxNodeScrollBehavior::CursorTop => { - self.scroll_cursor_top(&ScrollCursorTop, window, cx); - } - SelectSyntaxNodeScrollBehavior::FitSelection => { - self.request_autoscroll(Autoscroll::fit(), cx); - } - SelectSyntaxNodeScrollBehavior::CursorBottom => { - self.scroll_cursor_bottom(&ScrollCursorBottom, window, cx); - } - } - } - } - - 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.as_ref().map(Entity::downgrade); - let task_sources = self.lsp_task_sources(cx); - cx.spawn_in(window, async move |editor, cx| { - cx.background_executor().timer(UPDATE_DEBOUNCE).await; - let Some(project) = project.and_then(|p| p.upgrade()) else { - return; - }; - let Ok(display_snapshot) = editor.update(cx, |this, cx| { - this.display_map.update(cx, |map, cx| map.snapshot(cx)) - }) else { - return; - }; - - let hide_runnables = project - .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; - } - let new_rows = - cx.background_spawn({ - let snapshot = display_snapshot.clone(); - async move { - Self::fetch_runnable_ranges(&snapshot, Anchor::min()..Anchor::max()) - } - }) - .await; - let Ok(lsp_tasks) = - cx.update(|_, cx| crate::lsp_tasks(project.clone(), &task_sources, None, cx)) - else { - return; - }; - let lsp_tasks = lsp_tasks.await; - - let Ok(mut lsp_tasks_by_rows) = cx.update(|_, cx| { - lsp_tasks - .into_iter() - .flat_map(|(kind, tasks)| { - tasks.into_iter().filter_map(move |(location, task)| { - Some((kind.clone(), location?, task)) - }) - }) - .fold(HashMap::default(), |mut acc, (kind, location, task)| { - let buffer = location.target.buffer; - let buffer_snapshot = buffer.read(cx).snapshot(); - let offset = display_snapshot.buffer_snapshot.excerpts().find_map( - |(excerpt_id, snapshot, _)| { - if snapshot.remote_id() == buffer_snapshot.remote_id() { - display_snapshot - .buffer_snapshot - .anchor_in_excerpt(excerpt_id, location.target.range.start) - } else { - None - } - }, - ); - if let Some(offset) = offset { - let task_buffer_range = - location.target.range.to_point(&buffer_snapshot); - let context_buffer_range = - task_buffer_range.to_offset(&buffer_snapshot); - let context_range = BufferOffset(context_buffer_range.start) - ..BufferOffset(context_buffer_range.end); - - acc.entry((buffer_snapshot.remote_id(), task_buffer_range.start.row)) - .or_insert_with(|| RunnableTasks { - templates: Vec::new(), - offset, - column: task_buffer_range.start.column, - extra_variables: HashMap::default(), - context_range, - }) - .templates - .push((kind, task.original_task().clone())); - } - - acc - }) - }) else { - return; - }; - - let rows = Self::runnable_rows(project, display_snapshot, new_rows, cx.clone()); - editor - .update(cx, |editor, _| { - editor.clear_tasks(); - for (key, mut value) in rows { - if let Some(lsp_tasks) = lsp_tasks_by_rows.remove(&key) { - value.templates.extend(lsp_tasks.templates); - } - - editor.insert_tasks(key, value); - } - for (key, value) in lsp_tasks_by_rows { - editor.insert_tasks(key, value); - } - }) - .ok(); - }) - } - fn fetch_runnable_ranges( - snapshot: &DisplaySnapshot, - range: Range, - ) -> Vec { - snapshot.buffer_snapshot.runnable_ranges(range).collect() - } - - fn runnable_rows( - project: Entity, - snapshot: DisplaySnapshot, - runnable_ranges: Vec, - mut cx: AsyncWindowContext, - ) -> Vec<((BufferId, BufferRow), RunnableTasks)> { - runnable_ranges - .into_iter() - .filter_map(|mut runnable| { - let tasks = cx - .update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx)) - .ok()?; - if tasks.is_empty() { - return None; - } - - let point = runnable.run_range.start.to_point(&snapshot.buffer_snapshot); - - let row = snapshot - .buffer_snapshot - .buffer_line_for_row(MultiBufferRow(point.row))? - .1 - .start - .row; - - let context_range = - BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end); - Some(( - (runnable.buffer_id, row), - RunnableTasks { - templates: tasks, - offset: snapshot - .buffer_snapshot - .anchor_before(runnable.run_range.start), - context_range, - column: point.column, - extra_variables: runnable.extra_captures, - }, - )) - }) - .collect() - } - - fn templates_with_tags( - project: &Entity, - runnable: &mut Runnable, - cx: &mut App, - ) -> Vec<(TaskSourceKind, TaskTemplate)> { - let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| { - let (worktree_id, file) = project - .buffer_for_id(runnable.buffer, cx) - .and_then(|buffer| buffer.read(cx).file()) - .map(|file| (file.worktree_id(cx), file.clone())) - .unzip(); - - ( - project.task_store().read(cx).task_inventory().cloned(), - worktree_id, - file, - ) - }); - - let mut templates_with_tags = mem::take(&mut runnable.tags) - .into_iter() - .flat_map(|RunnableTag(tag)| { - inventory - .as_ref() - .into_iter() - .flat_map(|inventory| { - inventory.read(cx).list_tasks( - file.clone(), - Some(runnable.language.clone()), - worktree_id, - cx, - ) - }) - .filter(move |(_, template)| { - template.tags.iter().any(|source_tag| source_tag == &tag) - }) - }) - .sorted_by_key(|(kind, _)| kind.to_owned()) - .collect::>(); - if let Some((leading_tag_source, _)) = templates_with_tags.first() { - // Strongest source wins; if we have worktree tag binding, prefer that to - // global and language bindings; - // if we have a global binding, prefer that to language binding. - let first_mismatch = templates_with_tags - .iter() - .position(|(tag_source, _)| tag_source != leading_tag_source); - if let Some(index) = first_mismatch { - templates_with_tags.truncate(index); - } - } - - templates_with_tags - } - - pub fn move_to_enclosing_bracket( - &mut self, - _: &MoveToEnclosingBracket, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_offsets_with(|snapshot, selection| { - let Some(enclosing_bracket_ranges) = - snapshot.enclosing_bracket_ranges(selection.start..selection.end) - else { - return; - }; - - let mut best_length = usize::MAX; - let mut best_inside = false; - let mut best_in_bracket_range = false; - let mut best_destination = None; - for (open, close) in enclosing_bracket_ranges { - let close = close.to_inclusive(); - let length = close.end() - open.start; - let inside = selection.start >= open.end && selection.end <= *close.start(); - let in_bracket_range = open.to_inclusive().contains(&selection.head()) - || close.contains(&selection.head()); - - // If best is next to a bracket and current isn't, skip - if !in_bracket_range && best_in_bracket_range { - continue; - } - - // Prefer smaller lengths unless best is inside and current isn't - if length > best_length && (best_inside || !inside) { - continue; - } - - best_length = length; - best_inside = inside; - best_in_bracket_range = in_bracket_range; - best_destination = Some( - if close.contains(&selection.start) && close.contains(&selection.end) { - if inside { open.end } else { open.start } - } else if inside { - *close.start() - } else { - *close.end() - }, - ); - } - - if let Some(destination) = best_destination { - selection.collapse_to(destination, SelectionGoal::None); - } - }) - }); - } - - pub fn undo_selection( - &mut self, - _: &UndoSelection, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.end_selection(window, cx); - self.selection_history.mode = SelectionHistoryMode::Undoing; - if let Some(entry) = self.selection_history.undo_stack.pop_back() { - self.change_selections(None, window, cx, |s| { - s.select_anchors(entry.selections.to_vec()) - }); - self.select_next_state = entry.select_next_state; - self.select_prev_state = entry.select_prev_state; - self.add_selections_state = entry.add_selections_state; - self.request_autoscroll(Autoscroll::newest(), cx); - } - self.selection_history.mode = SelectionHistoryMode::Normal; - } - - pub fn redo_selection( - &mut self, - _: &RedoSelection, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.end_selection(window, cx); - self.selection_history.mode = SelectionHistoryMode::Redoing; - if let Some(entry) = self.selection_history.redo_stack.pop_back() { - self.change_selections(None, window, cx, |s| { - s.select_anchors(entry.selections.to_vec()) - }); - self.select_next_state = entry.select_next_state; - self.select_prev_state = entry.select_prev_state; - self.add_selections_state = entry.add_selections_state; - self.request_autoscroll(Autoscroll::newest(), cx); - } - self.selection_history.mode = SelectionHistoryMode::Normal; - } - - pub fn expand_excerpts( - &mut self, - action: &ExpandExcerpts, - _: &mut Window, - cx: &mut Context, - ) { - self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::UpAndDown, cx) - } - - pub fn expand_excerpts_down( - &mut self, - action: &ExpandExcerptsDown, - _: &mut Window, - cx: &mut Context, - ) { - self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::Down, cx) - } - - pub fn expand_excerpts_up( - &mut self, - action: &ExpandExcerptsUp, - _: &mut Window, - cx: &mut Context, - ) { - self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::Up, cx) - } - - pub fn expand_excerpts_for_direction( - &mut self, - lines: u32, - direction: ExpandExcerptDirection, - - cx: &mut Context, - ) { - let selections = self.selections.disjoint_anchors(); - - let lines = if lines == 0 { - EditorSettings::get_global(cx).expand_excerpt_lines - } else { - lines - }; - - self.buffer.update(cx, |buffer, cx| { - let snapshot = buffer.snapshot(cx); - let mut excerpt_ids = selections - .iter() - .flat_map(|selection| snapshot.excerpt_ids_for_range(selection.range())) - .collect::>(); - excerpt_ids.sort(); - excerpt_ids.dedup(); - buffer.expand_excerpts(excerpt_ids, lines, direction, cx) - }) - } - - pub fn expand_excerpt( - &mut self, - excerpt: ExcerptId, - direction: ExpandExcerptDirection, - window: &mut Window, - cx: &mut Context, - ) { - let current_scroll_position = self.scroll_position(cx); - let lines_to_expand = EditorSettings::get_global(cx).expand_excerpt_lines; - let mut should_scroll_up = false; - - 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) { - 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; - } - } - } - } - - self.buffer.update(cx, |buffer, cx| { - buffer.expand_excerpts([excerpt], lines_to_expand, direction, cx) - }); - - if should_scroll_up { - let new_scroll_position = - current_scroll_position + gpui::Point::new(0.0, lines_to_expand as f32); - self.set_scroll_position(new_scroll_position, window, cx); - } - } - - pub fn go_to_singleton_buffer_point( - &mut self, - point: Point, - window: &mut Window, - cx: &mut Context, - ) { - self.go_to_singleton_buffer_range(point..point, window, cx); - } - - pub fn go_to_singleton_buffer_range( - &mut self, - range: Range, - window: &mut Window, - cx: &mut Context, - ) { - let multibuffer = self.buffer().read(cx); - let Some(buffer) = multibuffer.as_singleton() else { - return; - }; - let Some(start) = multibuffer.buffer_point_to_anchor(&buffer, range.start, cx) else { - return; - }; - let Some(end) = multibuffer.buffer_point_to_anchor(&buffer, range.end, cx) else { - return; - }; - self.change_selections(Some(Autoscroll::center()), window, cx, |s| { - s.select_anchor_ranges([start..end]) - }); - } - - pub fn go_to_diagnostic( - &mut self, - _: &GoToDiagnostic, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.go_to_diagnostic_impl(Direction::Next, window, cx) - } - - pub fn go_to_prev_diagnostic( - &mut self, - _: &GoToPreviousDiagnostic, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.go_to_diagnostic_impl(Direction::Prev, window, cx) - } - - pub fn go_to_diagnostic_impl( - &mut self, - direction: Direction, - window: &mut Window, - cx: &mut Context, - ) { - let buffer = self.buffer.read(cx).snapshot(cx); - let selection = self.selections.newest::(cx); - - let mut active_group_id = None; - 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( - snapshot: EditorSnapshot, - diagnostics: impl Iterator>, - ) -> impl Iterator> { - diagnostics - .filter(|entry| entry.range.start != entry.range.end) - .filter(|entry| !entry.diagnostic.is_unnecessary) - .filter(move |entry| !snapshot.intersects_fold(entry.range.start)) - } - - let snapshot = self.snapshot(window, cx); - let before = filtered( - snapshot.clone(), - buffer - .diagnostics_in_range(0..selection.start) - .filter(|entry| entry.range.start <= selection.start), - ); - let after = filtered( - snapshot, - buffer - .diagnostics_in_range(selection.start..buffer.len()) - .filter(|entry| entry.range.start >= selection.start), - ); - - let mut found: Option> = None; - if direction == Direction::Prev { - 'outer: for prev_diagnostics in [before.collect::>(), after.collect::>()] - { - for diagnostic in prev_diagnostics.into_iter().rev() { - if diagnostic.range.start != selection.start - || active_group_id - .is_some_and(|active| diagnostic.diagnostic.group_id < active) - { - found = Some(diagnostic); - break 'outer; - } - } - } - } else { - for diagnostic in after.chain(before) { - if diagnostic.range.start != selection.start - || active_group_id.is_some_and(|active| diagnostic.diagnostic.group_id > active) - { - found = Some(diagnostic); - break; - } - } - } - let Some(next_diagnostic) = found else { - return; - }; - - let Some(buffer_id) = buffer.anchor_after(next_diagnostic.range.start).buffer_id else { - return; - }; - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_ranges(vec![ - next_diagnostic.range.start..next_diagnostic.range.start, - ]) - }); - self.activate_diagnostics(buffer_id, next_diagnostic, window, cx); - self.refresh_inline_completion(false, true, window, cx); - } - - fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - let snapshot = self.snapshot(window, cx); - let selection = self.selections.newest::(cx); - self.go_to_hunk_before_or_after_position( - &snapshot, - selection.head(), - Direction::Next, - window, - cx, - ); - } - - pub fn go_to_hunk_before_or_after_position( - &mut self, - snapshot: &EditorSnapshot, - position: Point, - direction: Direction, - window: &mut Window, - cx: &mut Context, - ) { - let row = if direction == Direction::Next { - self.hunk_after_position(snapshot, position) - .map(|hunk| hunk.row_range.start) - } else { - self.hunk_before_position(snapshot, position) - }; - - if let Some(row) = row { - let destination = Point::new(row.0, 0); - let autoscroll = Autoscroll::center(); - - self.unfold_ranges(&[destination..destination], false, false, cx); - self.change_selections(Some(autoscroll), window, cx, |s| { - s.select_ranges([destination..destination]); - }); - } - } - - fn hunk_after_position( - &mut self, - snapshot: &EditorSnapshot, - position: Point, - ) -> Option { - snapshot - .buffer_snapshot - .diff_hunks_in_range(position..snapshot.buffer_snapshot.max_point()) - .find(|hunk| hunk.row_range.start.0 > position.row) - .or_else(|| { - snapshot - .buffer_snapshot - .diff_hunks_in_range(Point::zero()..position) - .find(|hunk| hunk.row_range.end.0 < position.row) - }) - } - - fn go_to_prev_hunk( - &mut self, - _: &GoToPreviousHunk, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - let snapshot = self.snapshot(window, cx); - let selection = self.selections.newest::(cx); - self.go_to_hunk_before_or_after_position( - &snapshot, - selection.head(), - Direction::Prev, - window, - cx, - ); - } - - fn hunk_before_position( - &mut self, - snapshot: &EditorSnapshot, - position: Point, - ) -> Option { - snapshot - .buffer_snapshot - .diff_hunk_before(position) - .or_else(|| snapshot.buffer_snapshot.diff_hunk_before(Point::MAX)) - } - - fn go_to_next_change( - &mut self, - _: &GoToNextChange, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(selections) = self - .change_list - .next_change(1, Direction::Next) - .map(|s| s.to_vec()) - { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - let map = s.display_map(); - s.select_display_ranges(selections.iter().map(|a| { - let point = a.to_display_point(&map); - point..point - })) - }) - } - } - - fn go_to_previous_change( - &mut self, - _: &GoToPreviousChange, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(selections) = self - .change_list - .next_change(1, Direction::Prev) - .map(|s| s.to_vec()) - { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - let map = s.display_map(); - s.select_display_ranges(selections.iter().map(|a| { - let point = a.to_display_point(&map); - point..point - })) - }) - } - } - - fn go_to_line( - &mut self, - position: Anchor, - highlight_color: Option, - window: &mut Window, - cx: &mut Context, - ) { - let snapshot = self.snapshot(window, cx).display_snapshot; - let position = position.to_point(&snapshot.buffer_snapshot); - let start = snapshot - .buffer_snapshot - .clip_point(Point::new(position.row, 0), Bias::Left); - let end = start + Point::new(1, 0); - let start = snapshot.buffer_snapshot.anchor_before(start); - let end = snapshot.buffer_snapshot.anchor_before(end); - - self.highlight_rows::( - start..end, - highlight_color - .unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background), - Default::default(), - cx, - ); - self.request_autoscroll(Autoscroll::center().for_anchor(start), cx); - } - - pub fn go_to_definition( - &mut self, - _: &GoToDefinition, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let definition = - self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, window, cx); - let fallback_strategy = EditorSettings::get_global(cx).go_to_definition_fallback; - cx.spawn_in(window, async move |editor, cx| { - if definition.await? == Navigated::Yes { - return Ok(Navigated::Yes); - } - match fallback_strategy { - GoToDefinitionFallback::None => Ok(Navigated::No), - GoToDefinitionFallback::FindAllReferences => { - match editor.update_in(cx, |editor, window, cx| { - editor.find_all_references(&FindAllReferences, window, cx) - })? { - Some(references) => references.await, - None => Ok(Navigated::No), - } - } - } - }) - } - - pub fn go_to_declaration( - &mut self, - _: &GoToDeclaration, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Declaration, false, window, cx) - } - - pub fn go_to_declaration_split( - &mut self, - _: &GoToDeclaration, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Declaration, true, window, cx) - } - - pub fn go_to_implementation( - &mut self, - _: &GoToImplementation, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, false, window, cx) - } - - pub fn go_to_implementation_split( - &mut self, - _: &GoToImplementationSplit, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, true, window, cx) - } - - pub fn go_to_type_definition( - &mut self, - _: &GoToTypeDefinition, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Type, false, window, cx) - } - - pub fn go_to_definition_split( - &mut self, - _: &GoToDefinitionSplit, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, true, window, cx) - } - - pub fn go_to_type_definition_split( - &mut self, - _: &GoToTypeDefinitionSplit, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Type, true, window, cx) - } - - fn go_to_definition_of_kind( - &mut self, - kind: GotoDefinitionKind, - split: bool, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let Some(provider) = self.semantics_provider.clone() else { - return Task::ready(Ok(Navigated::No)); - }; - let head = self.selections.newest::(cx).head(); - let buffer = self.buffer.read(cx); - 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 definitions = definitions.await?; - let navigated = editor - .update_in(cx, |editor, window, cx| { - editor.navigate_to_hover_links( - Some(kind), - definitions - .into_iter() - .filter(|location| { - hover_links::exclude_link_to_position(&buffer, &head, location, cx) - }) - .map(HoverLink::Text) - .collect::>(), - split, - window, - cx, - ) - })? - .await?; - anyhow::Ok(navigated) - }) - } - - pub fn open_url(&mut self, _: &OpenUrl, window: &mut Window, cx: &mut Context) { - let selection = self.selections.newest_anchor(); - let head = selection.head(); - let tail = selection.tail(); - - let Some((buffer, start_position)) = - self.buffer.read(cx).text_anchor_for_position(head, cx) - else { - return; - }; - - let end_position = if head != tail { - let Some((_, pos)) = self.buffer.read(cx).text_anchor_for_position(tail, cx) else { - return; - }; - Some(pos) - } else { - None - }; - - let url_finder = cx.spawn_in(window, async move |editor, cx| { - let url = if let Some(end_pos) = end_position { - find_url_from_range(&buffer, start_position..end_pos, cx.clone()) - } else { - find_url(&buffer, start_position, cx.clone()).map(|(_, url)| url) - }; - - if let Some(url) = url { - editor.update(cx, |_, cx| { - cx.open_url(&url); - }) - } else { - Ok(()) - } - }); - - url_finder.detach(); - } - - pub fn open_selected_filename( - &mut self, - _: &OpenSelectedFilename, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = self.workspace() else { - return; - }; - - let position = self.selections.newest_anchor().head(); - - let Some((buffer, buffer_position)) = - self.buffer.read(cx).text_anchor_for_position(position, cx) - else { - return; - }; - - let project = self.project.clone(); - - cx.spawn_in(window, async move |_, cx| { - let result = find_file(&buffer, project, buffer_position, cx).await; - - if let Some((_, path)) = result { - workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_resolved_path(path, window, cx) - })? - .await?; - } - anyhow::Ok(()) - }) - .detach(); - } - - pub(crate) fn navigate_to_hover_links( - &mut self, - kind: Option, - mut definitions: Vec, - split: bool, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - // 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); - cx.background_spawn(async move { - let location = computation.await?; - Ok(TargetTaskResult::Location(location)) - }) - } - HoverLink::Url(url) => { - cx.open_url(&url); - Task::ready(Ok(TargetTaskResult::AlreadyNavigated)) - } - HoverLink::File(path) => { - 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))) - } - } - }; - 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, - }; - - 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)); - let range = editor.range_for_match(&range); - let range = collapse_multiline_range(range); - - if !split - && Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() - { - editor.go_to_singleton_buffer_range(range.clone(), window, cx); - } else { - window.defer(cx, move |window, cx| { - let target_editor: Entity = - workspace.update(cx, |workspace, cx| { - let pane = if split { - workspace.adjacent_pane(window, cx) - } else { - workspace.active_pane().clone() - }; - - workspace.open_project_item( - pane, - target.buffer.clone(), - true, - true, - window, - cx, - ) - }); - target_editor.update(cx, |target_editor, cx| { - // When selecting a definition in a different buffer, disable the nav history - // to avoid creating a history entry at the previous cursor location. - pane.update(cx, |pane, _| pane.disable_history()); - target_editor.go_to_singleton_buffer_range(range, window, cx); - pane.update(cx, |pane, _| pane.enable_history()); - }); - }); - } - 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 = future::join_all(location_tasks) - .await - .into_iter() - .filter_map(|location| location.transpose()) - .collect::>() - .context("location tasks")?; - - 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( - &self, - lsp_location: lsp::Location, - server_id: LanguageServerId, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let Some(project) = self.project.clone() else { - return Task::ready(Ok(None)); - }; - - cx.spawn_in(window, async move |editor, cx| { - let location_task = editor.update(cx, |_, cx| { - project.update(cx, |project, 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 = match location_task { - Some(task) => Some({ - let target_buffer_handle = task.await.context("open local buffer")?; - let range = target_buffer_handle.update(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) - }) - } - - pub fn find_all_references( - &mut self, - _: &FindAllReferences, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - let selection = self.selections.newest::(cx); - let multi_buffer = self.buffer.read(cx); - let head = selection.head(); - - let multi_buffer_snapshot = multi_buffer.snapshot(cx); - let head_anchor = multi_buffer_snapshot.anchor_at( - head, - if head < selection.tail() { - Bias::Right - } else { - Bias::Left - }, - ); - - match self - .find_all_references_task_sources - .binary_search_by(|anchor| anchor.cmp(&head_anchor, &multi_buffer_snapshot)) - { - Ok(_) => { - log::info!( - "Ignoring repeated FindAllReferences invocation with the position of already running task" - ); - return None; - } - Err(i) => { - self.find_all_references_task_sources.insert(i, head_anchor); - } - } - - let (buffer, head) = multi_buffer.text_anchor_for_position(head, cx)?; - let workspace = self.workspace()?; - let project = workspace.read(cx).project().clone(); - let references = project.update(cx, |project, cx| project.references(&buffer, head, cx)); - Some(cx.spawn_in(window, async move |editor, cx| { - let _cleanup = cx.on_drop(&editor, move |editor, _| { - if let Ok(i) = editor - .find_all_references_task_sources - .binary_search_by(|anchor| anchor.cmp(&head_anchor, &multi_buffer_snapshot)) - { - editor.find_all_references_task_sources.remove(i); - } - }); - - let locations = references.await?; - if locations.is_empty() { - return anyhow::Ok(Navigated::No); - } - - workspace.update_in(cx, |workspace, window, cx| { - let title = locations - .first() - .as_ref() - .map(|location| { - let buffer = location.buffer.read(cx); - format!( - "References to `{}`", - buffer - .text_for_range(location.range.clone()) - .collect::() - ) - }) - .unwrap(); - Self::open_locations_in_multibuffer( - workspace, - locations, - title, - false, - MultibufferSelectionMode::First, - window, - cx, - ); - Navigated::Yes - }) - })) - } - - /// Opens a multibuffer with the given project locations in it - pub fn open_locations_in_multibuffer( - workspace: &mut Workspace, - mut locations: Vec, - title: String, - split: bool, - multibuffer_selection_mode: MultibufferSelectionMode, - window: &mut Window, - cx: &mut Context, - ) { - // If there are multiple definitions, open them in a multibuffer - locations.sort_by_key(|location| location.buffer.read(cx).remote_id()); - let mut locations = locations.into_iter().peekable(); - let mut ranges: Vec> = Vec::new(); - let capability = workspace.project().read(cx).capability(); - - let excerpt_buffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::new(capability); - while let Some(location) = locations.next() { - let buffer = location.buffer.read(cx); - let mut ranges_for_buffer = Vec::new(); - let range = location.range.to_point(buffer); - ranges_for_buffer.push(range.clone()); - - while let Some(next_location) = locations.peek() { - if next_location.buffer == location.buffer { - ranges_for_buffer.push(next_location.range.to_point(buffer)); - locations.next(); - } else { - break; - } - } - - ranges_for_buffer.sort_by_key(|range| (range.start, Reverse(range.end))); - let (new_ranges, _) = multibuffer.set_excerpts_for_path( - PathKey::for_buffer(&location.buffer, cx), - location.buffer.clone(), - ranges_for_buffer, - DEFAULT_MULTIBUFFER_CONTEXT, - cx, - ); - ranges.extend(new_ranges) - } - - multibuffer.with_title(title) - }); - - let editor = cx.new(|cx| { - Editor::for_multibuffer( - excerpt_buffer, - Some(workspace.project().clone()), - window, - cx, - ) - }); - editor.update(cx, |editor, cx| { - match multibuffer_selection_mode { - MultibufferSelectionMode::First => { - if let Some(first_range) = ranges.first() { - editor.change_selections(None, window, cx, |selections| { - selections.clear_disjoint(); - selections.select_anchor_ranges(std::iter::once(first_range.clone())); - }); - } - editor.highlight_background::( - &ranges, - |theme| theme.editor_highlighted_line_background, - cx, - ); - } - MultibufferSelectionMode::All => { - editor.change_selections(None, window, cx, |selections| { - selections.clear_disjoint(); - selections.select_anchor_ranges(ranges); - }); - } - } - editor.register_buffers_with_language_servers(cx); - }); - - let item = Box::new(editor); - let item_id = item.item_id(); - - if split { - workspace.split_item(SplitDirection::Right, item.clone(), window, cx); - } else { - if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation { - let (preview_item_id, preview_item_idx) = - workspace.active_pane().update(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); - }); - } - - pub fn rename( - &mut self, - _: &Rename, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - use language::ToOffset as _; - - let provider = self.semantics_provider.clone()?; - let selection = self.selections.newest_anchor().clone(); - let (cursor_buffer, cursor_buffer_position) = self - .buffer - .read(cx) - .text_anchor_for_position(selection.head(), cx)?; - let (tail_buffer, cursor_buffer_position_end) = self - .buffer - .read(cx) - .text_anchor_for_position(selection.tail(), cx)?; - if tail_buffer != cursor_buffer { - return None; - } - - let snapshot = cursor_buffer.read(cx).snapshot(); - let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot); - let cursor_buffer_offset_end = cursor_buffer_position_end.to_offset(&snapshot); - let prepare_rename = provider - .range_for_rename(&cursor_buffer, cursor_buffer_position, cx) - .unwrap_or_else(|| Task::ready(Ok(None))); - drop(snapshot); - - Some(cx.spawn_in(window, async move |this, cx| { - let rename_range = if let Some(range) = prepare_rename.await? { - Some(range) - } else { - this.update(cx, |this, cx| { - let buffer = this.buffer.read(cx).snapshot(cx); - let mut buffer_highlights = this - .document_highlights_for_position(selection.head(), &buffer) - .filter(|highlight| { - highlight.start.excerpt_id == selection.head().excerpt_id - && highlight.end.excerpt_id == selection.head().excerpt_id - }); - buffer_highlights - .next() - .map(|highlight| highlight.start.text_anchor..highlight.end.text_anchor) - })? - }; - if let Some(rename_range) = rename_range { - this.update_in(cx, |this, window, cx| { - let snapshot = cursor_buffer.read(cx).snapshot(); - let rename_buffer_range = rename_range.to_offset(&snapshot); - let cursor_offset_in_rename_range = - cursor_buffer_offset.saturating_sub(rename_buffer_range.start); - let cursor_offset_in_rename_range_end = - cursor_buffer_offset_end.saturating_sub(rename_buffer_range.start); - - this.take_rename(false, window, cx); - let buffer = this.buffer.read(cx).read(cx); - let cursor_offset = selection.head().to_offset(&buffer); - let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range); - let rename_end = rename_start + rename_buffer_range.len(); - let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end); - let mut old_highlight_id = None; - let old_name: Arc = buffer - .chunks(rename_start..rename_end, true) - .map(|chunk| { - if old_highlight_id.is_none() { - old_highlight_id = chunk.syntax_highlight_id; - } - chunk.text - }) - .collect::() - .into(); - - drop(buffer); - - // Position the selection in the rename editor so that it matches the current selection. - this.show_local_selections = false; - let rename_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, old_name.clone())], None, cx) - }); - let rename_selection_range = match cursor_offset_in_rename_range - .cmp(&cursor_offset_in_rename_range_end) - { - Ordering::Equal => { - editor.select_all(&SelectAll, window, cx); - return editor; - } - Ordering::Less => { - cursor_offset_in_rename_range..cursor_offset_in_rename_range_end - } - Ordering::Greater => { - cursor_offset_in_rename_range_end..cursor_offset_in_rename_range - } - }; - if rename_selection_range.end > old_name.len() { - editor.select_all(&SelectAll, window, cx); - } else { - editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_ranges([rename_selection_range]); - }); - } - editor - }); - cx.subscribe(&rename_editor, |_, _, e: &EditorEvent, cx| { - if e == &EditorEvent::Focused { - cx.emit(EditorEvent::FocusedIn) - } - }) - .detach(); - - let write_highlights = - this.clear_background_highlights::(cx); - let read_highlights = - this.clear_background_highlights::(cx); - let ranges = write_highlights - .iter() - .flat_map(|(_, ranges)| ranges.iter()) - .chain(read_highlights.iter().flat_map(|(_, ranges)| ranges.iter())) - .cloned() - .collect(); - - this.highlight_text::( - ranges, - HighlightStyle { - fade_out: Some(0.6), - ..Default::default() - }, - cx, - ); - let rename_focus_handle = rename_editor.focus_handle(cx); - window.focus(&rename_focus_handle); - let block_id = this.insert_blocks( - [BlockProperties { - style: BlockStyle::Flex, - placement: BlockPlacement::Below(range.start), - height: Some(1), - render: Arc::new({ - let rename_editor = rename_editor.clone(); - move |cx: &mut BlockContext| { - let mut text_style = cx.editor_style.text.clone(); - if let Some(highlight_style) = old_highlight_id - .and_then(|h| h.style(&cx.editor_style.syntax)) - { - text_style = text_style.highlight(highlight_style); - } - div() - .block_mouse_down() - .pl(cx.anchor_x) - .child(EditorElement::new( - &rename_editor, - EditorStyle { - background: cx.theme().system().transparent, - local_player: cx.editor_style.local_player, - text: text_style, - scrollbar_width: cx.editor_style.scrollbar_width, - syntax: cx.editor_style.syntax.clone(), - status: cx.editor_style.status.clone(), - inlay_hints_style: HighlightStyle { - font_weight: Some(FontWeight::BOLD), - ..make_inlay_hints_style(cx.app) - }, - inline_completion_styles: make_suggestion_styles( - cx.app, - ), - ..EditorStyle::default() - }, - )) - .into_any_element() - } - }), - priority: 0, - }], - Some(Autoscroll::fit()), - cx, - )[0]; - this.pending_rename = Some(RenameState { - range, - old_name, - editor: rename_editor, - block_id, - }); - })?; - } - - Ok(()) - })) - } - - pub fn confirm_rename( - &mut self, - _: &ConfirmRename, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - let rename = self.take_rename(false, window, cx)?; - let workspace = self.workspace()?.downgrade(); - let (buffer, start) = self - .buffer - .read(cx) - .text_anchor_for_position(rename.range.start, cx)?; - let (end_buffer, _) = self - .buffer - .read(cx) - .text_anchor_for_position(rename.range.end, cx)?; - if buffer != end_buffer { - return None; - } - - let old_name = rename.old_name; - let new_name = rename.editor.read(cx).text(cx); - - let rename = self.semantics_provider.as_ref()?.perform_rename( - &buffer, - start, - new_name.clone(), - cx, - )?; - - Some(cx.spawn_in(window, async move |editor, cx| { - let project_transaction = rename.await?; - Self::open_project_transaction( - &editor, - workspace, - project_transaction, - format!("Rename: {} → {}", old_name, new_name), - cx, - ) - .await?; - - editor.update(cx, |editor, cx| { - editor.refresh_document_highlights(cx); - })?; - Ok(()) - })) - } - - fn take_rename( - &mut self, - moving_cursor: bool, - window: &mut Window, - cx: &mut Context, - ) -> Option { - let rename = self.pending_rename.take()?; - if rename.editor.focus_handle(cx).is_focused(window) { - window.focus(&self.focus_handle); - } - - self.remove_blocks( - [rename.block_id].into_iter().collect(), - Some(Autoscroll::fit()), - cx, - ); - self.clear_highlights::(cx); - self.show_local_selections = true; - - if moving_cursor { - let cursor_in_rename_editor = rename.editor.update(cx, |editor, cx| { - editor.selections.newest::(cx).head() - }); - - // Update the selection to match the position of the selection inside - // the rename editor. - let snapshot = self.buffer.read(cx).read(cx); - let rename_range = rename.range.to_offset(&snapshot); - let cursor_in_editor = snapshot - .clip_offset(rename_range.start + cursor_in_rename_editor, Bias::Left) - .min(rename_range.end); - drop(snapshot); - - self.change_selections(None, window, cx, |s| { - s.select_ranges(vec![cursor_in_editor..cursor_in_editor]) - }); - } else { - self.refresh_document_highlights(cx); - } - - Some(rename) - } - - pub fn pending_rename(&self) -> Option<&RenameState> { - self.pending_rename.as_ref() - } - - fn format( - &mut self, - _: &Format, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let project = match &self.project { - Some(project) => project.clone(), - None => return None, - }; - - Some(self.perform_format( - project, - FormatTrigger::Manual, - FormatTarget::Buffers, - window, - cx, - )) - } - - fn format_selections( - &mut self, - _: &FormatSelections, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let project = match &self.project { - Some(project) => project.clone(), - None => return None, - }; - - let ranges = self - .selections - .all_adjusted(cx) - .into_iter() - .map(|selection| selection.range()) - .collect_vec(); - - Some(self.perform_format( - project, - FormatTrigger::Manual, - FormatTarget::Ranges(ranges), - window, - cx, - )) - } - - fn perform_format( - &mut self, - project: Entity, - trigger: FormatTrigger, - target: FormatTarget, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let buffer = self.buffer.clone(); - let (buffers, target) = match target { - FormatTarget::Buffers => { - let mut buffers = buffer.read(cx).all_buffers(); - if trigger == FormatTrigger::Save { - buffers.retain(|buffer| buffer.read(cx).is_dirty()); - } - (buffers, LspFormatTarget::Buffers) - } - FormatTarget::Ranges(selection_ranges) => { - let multi_buffer = buffer.read(cx); - let snapshot = multi_buffer.read(cx); - let mut buffers = HashSet::default(); - let mut buffer_id_to_ranges: BTreeMap>> = - BTreeMap::new(); - for selection_range in selection_ranges { - for (buffer, buffer_range, _) in - snapshot.range_to_buffer_ranges(selection_range) - { - let buffer_id = buffer.remote_id(); - let start = buffer.anchor_before(buffer_range.start); - let end = buffer.anchor_after(buffer_range.end); - buffers.insert(multi_buffer.buffer(buffer_id).unwrap()); - buffer_id_to_ranges - .entry(buffer_id) - .and_modify(|buffer_ranges| buffer_ranges.push(start..end)) - .or_insert_with(|| vec![start..end]); - } - } - (buffers, LspFormatTarget::Ranges(buffer_id_to_ranges)) - } - }; - - let transaction_id_prev = buffer.read_with(cx, |b, cx| b.last_transaction_id(cx)); - let selections_prev = transaction_id_prev - .and_then(|transaction_id_prev| { - // default to selections as they were after the last edit, if we have them, - // instead of how they are now. - // This will make it so that editing, moving somewhere else, formatting, then undoing the format - // will take you back to where you made the last edit, instead of staying where you scrolled - self.selection_history - .transaction(transaction_id_prev) - .map(|t| t.0.clone()) - }) - .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| { - project.format(buffers, target, true, trigger, cx) - }); - - cx.spawn_in(window, async move |editor, cx| { - let transaction = futures::select_biased! { - transaction = format.log_err().fuse() => transaction, - () = timeout => { - log::warn!("timed out waiting for formatting"); - None - } - }; - - buffer - .update(cx, |buffer, cx| { - if let Some(transaction) = transaction { - if !buffer.is_singleton() { - buffer.push_transaction(&transaction.0, cx); - } - } - cx.notify(); - }) - .ok(); - - if let Some(transaction_id_now) = - buffer.read_with(cx, |b, cx| b.last_transaction_id(cx))? - { - let has_new_transaction = transaction_id_prev != Some(transaction_id_now); - if has_new_transaction { - _ = editor.update(cx, |editor, _| { - editor - .selection_history - .insert_transaction(transaction_id_now, selections_prev); - }); - } - } - - Ok(()) - }) - } - - fn organize_imports( - &mut self, - _: &OrganizeImports, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let project = match &self.project { - Some(project) => project.clone(), - None => return None, - }; - Some(self.perform_code_action_kind( - project, - CodeActionKind::SOURCE_ORGANIZE_IMPORTS, - window, - cx, - )) - } - - fn perform_code_action_kind( - &mut self, - project: Entity, - kind: CodeActionKind, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let buffer = self.buffer.clone(); - let buffers = buffer.read(cx).all_buffers(); - let mut timeout = cx.background_executor().timer(CODE_ACTION_TIMEOUT).fuse(); - let apply_action = project.update(cx, |project, cx| { - project.apply_code_action_kind(buffers, kind, true, cx) - }); - cx.spawn_in(window, async move |_, cx| { - let transaction = futures::select_biased! { - () = timeout => { - log::warn!("timed out waiting for executing code action"); - None - } - transaction = apply_action.log_err().fuse() => transaction, - }; - buffer - .update(cx, |buffer, cx| { - // check if we need this - if let Some(transaction) = transaction { - if !buffer.is_singleton() { - buffer.push_transaction(&transaction.0, cx); - } - } - cx.notify(); - }) - .ok(); - Ok(()) - }) - } - - fn restart_language_server( - &mut self, - _: &RestartLanguageServer, - _: &mut Window, - cx: &mut Context, - ) { - if let Some(project) = self.project.clone() { - self.buffer.update(cx, |multi_buffer, cx| { - project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers( - multi_buffer.all_buffers().into_iter().collect(), - cx, - ); - }); - }) - } - } - - fn stop_language_server( - &mut self, - _: &StopLanguageServer, - _: &mut Window, - cx: &mut Context, - ) { - if let Some(project) = self.project.clone() { - self.buffer.update(cx, |multi_buffer, cx| { - project.update(cx, |project, cx| { - project.stop_language_servers_for_buffers( - multi_buffer.all_buffers().into_iter().collect(), - cx, - ); - cx.emit(project::Event::RefreshInlayHints); - }); - }); - } - } - - fn cancel_language_server_work( - workspace: &mut Workspace, - _: &actions::CancelLanguageServerWork, - _: &mut Window, - cx: &mut Context, - ) { - let project = workspace.project(); - let buffers = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - .map_or(HashSet::default(), |editor| { - editor.read(cx).buffer.read(cx).all_buffers() - }); - project.update(cx, |project, cx| { - project.cancel_language_server_work_for_buffers(buffers, cx); - }); - } - - fn show_character_palette( - &mut self, - _: &ShowCharacterPalette, - window: &mut Window, - _: &mut Context, - ) { - window.show_character_palette(); - } - - fn refresh_active_diagnostics(&mut self, cx: &mut Context) { - if let ActiveDiagnostic::Group(active_diagnostics) = &mut self.active_diagnostics { - let buffer = self.buffer.read(cx).snapshot(cx); - let primary_range_start = active_diagnostics.active_range.start.to_offset(&buffer); - let primary_range_end = active_diagnostics.active_range.end.to_offset(&buffer); - let is_valid = buffer - .diagnostics_in_range::(primary_range_start..primary_range_end) - .any(|entry| { - entry.diagnostic.is_primary - && !entry.range.is_empty() - && entry.range.start == primary_range_start - && entry.diagnostic.message == active_diagnostics.active_message - }); - - if !is_valid { - self.dismiss_diagnostics(cx); - } - } - } - - pub fn active_diagnostic_group(&self) -> Option<&ActiveDiagnosticGroup> { - match &self.active_diagnostics { - ActiveDiagnostic::Group(group) => Some(group), - _ => None, - } - } - - pub fn set_all_diagnostics_active(&mut self, cx: &mut Context) { - self.dismiss_diagnostics(cx); - self.active_diagnostics = ActiveDiagnostic::All; - } - - fn activate_diagnostics( - &mut self, - buffer_id: BufferId, - diagnostic: DiagnosticEntry, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.active_diagnostics, ActiveDiagnostic::All) { - return; - } - self.dismiss_diagnostics(cx); - let snapshot = self.snapshot(window, cx); - let buffer = self.buffer.read(cx).snapshot(cx); - let Some(renderer) = GlobalDiagnosticRenderer::global(cx) else { - return; - }; - - let diagnostic_group = buffer - .diagnostic_group(buffer_id, diagnostic.diagnostic.group_id) - .collect::>(); - - let blocks = - renderer.render_group(diagnostic_group, buffer_id, snapshot, cx.weak_entity(), cx); - - let blocks = self.display_map.update(cx, |display_map, cx| { - display_map.insert_blocks(blocks, cx).into_iter().collect() - }); - self.active_diagnostics = ActiveDiagnostic::Group(ActiveDiagnosticGroup { - active_range: buffer.anchor_before(diagnostic.range.start) - ..buffer.anchor_after(diagnostic.range.end), - active_message: diagnostic.diagnostic.message.clone(), - group_id: diagnostic.diagnostic.group_id, - blocks, - }); - cx.notify(); - } - - fn dismiss_diagnostics(&mut self, cx: &mut Context) { - if matches!(self.active_diagnostics, ActiveDiagnostic::All) { - return; - }; - - let prev = mem::replace(&mut self.active_diagnostics, ActiveDiagnostic::None); - if let ActiveDiagnostic::Group(group) = prev { - self.display_map.update(cx, |display_map, cx| { - display_map.remove_blocks(group.blocks, cx); - }); - cx.notify(); - } - } - - /// Disable inline diagnostics rendering for this editor. - pub fn disable_inline_diagnostics(&mut self) { - self.inline_diagnostics_enabled = false; - self.inline_diagnostics_update = Task::ready(()); - self.inline_diagnostics.clear(); - } - - pub fn inline_diagnostics_enabled(&self) -> bool { - self.inline_diagnostics_enabled - } - - pub fn show_inline_diagnostics(&self) -> bool { - self.show_inline_diagnostics - } - - pub fn toggle_inline_diagnostics( - &mut self, - _: &ToggleInlineDiagnostics, - window: &mut Window, - cx: &mut Context, - ) { - self.show_inline_diagnostics = !self.show_inline_diagnostics; - self.refresh_inline_diagnostics(false, window, cx); - } - - fn refresh_inline_diagnostics( - &mut self, - debounce: bool, - window: &mut Window, - cx: &mut Context, - ) { - if !self.inline_diagnostics_enabled || !self.show_inline_diagnostics { - self.inline_diagnostics_update = Task::ready(()); - self.inline_diagnostics.clear(); - return; - } - - let debounce_ms = ProjectSettings::get_global(cx) - .diagnostics - .inline - .update_debounce_ms; - let debounce = if debounce && debounce_ms > 0 { - Some(Duration::from_millis(debounce_ms)) - } else { - None - }; - self.inline_diagnostics_update = cx.spawn_in(window, async move |editor, cx| { - let editor = editor.upgrade().unwrap(); - - if let Some(debounce) = debounce { - cx.background_executor().timer(debounce).await; - } - let Some(snapshot) = editor - .update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx)) - .ok() - else { - return; - }; - - let new_inline_diagnostics = cx - .background_spawn(async move { - let mut inline_diagnostics = Vec::<(Anchor, InlineDiagnostic)>::new(); - for diagnostic_entry in snapshot.diagnostics_in_range(0..snapshot.len()) { - let message = diagnostic_entry - .diagnostic - .message - .split_once('\n') - .map(|(line, _)| line) - .map(SharedString::new) - .unwrap_or_else(|| { - SharedString::from(diagnostic_entry.diagnostic.message) - }); - let start_anchor = snapshot.anchor_before(diagnostic_entry.range.start); - let (Ok(i) | Err(i)) = inline_diagnostics - .binary_search_by(|(probe, _)| probe.cmp(&start_anchor, &snapshot)); - inline_diagnostics.insert( - i, - ( - start_anchor, - InlineDiagnostic { - message, - group_id: diagnostic_entry.diagnostic.group_id, - start: diagnostic_entry.range.start.to_point(&snapshot), - is_primary: diagnostic_entry.diagnostic.is_primary, - severity: diagnostic_entry.diagnostic.severity, - }, - ), - ); - } - inline_diagnostics - }) - .await; - - editor - .update(cx, |editor, cx| { - editor.inline_diagnostics = new_inline_diagnostics; - cx.notify(); - }) - .ok(); - }); - } - - pub fn set_selections_from_remote( - &mut self, - selections: Vec>, - pending_selection: Option>, - window: &mut Window, - cx: &mut Context, - ) { - let old_cursor_position = self.selections.newest_anchor().head(); - self.selections.change_with(cx, |s| { - s.select_anchors(selections); - if let Some(pending_selection) = pending_selection { - s.set_pending(pending_selection, SelectMode::Character); - } else { - s.clear_pending(); - } - }); - self.selections_did_change(false, &old_cursor_position, true, window, cx); - } - - fn push_to_selection_history(&mut self) { - self.selection_history.push(SelectionHistoryEntry { - selections: self.selections.disjoint_anchors(), - select_next_state: self.select_next_state.clone(), - select_prev_state: self.select_prev_state.clone(), - add_selections_state: self.add_selections_state.clone(), - }); - } - - pub fn transact( - &mut self, - window: &mut Window, - cx: &mut Context, - update: impl FnOnce(&mut Self, &mut Window, &mut Context), - ) -> Option { - self.start_transaction_at(Instant::now(), window, cx); - update(self, window, cx); - self.end_transaction_at(Instant::now(), cx) - } - - pub fn start_transaction_at( - &mut self, - now: Instant, - window: &mut Window, - cx: &mut Context, - ) { - self.end_selection(window, cx); - if let Some(tx_id) = self - .buffer - .update(cx, |buffer, cx| buffer.start_transaction_at(now, cx)) - { - self.selection_history - .insert_transaction(tx_id, self.selections.disjoint_anchors()); - cx.emit(EditorEvent::TransactionBegun { - transaction_id: tx_id, - }) - } - } - - pub fn end_transaction_at( - &mut self, - now: Instant, - cx: &mut Context, - ) -> Option { - if let Some(transaction_id) = self - .buffer - .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) - { - if let Some((_, end_selections)) = - self.selection_history.transaction_mut(transaction_id) - { - *end_selections = Some(self.selections.disjoint_anchors()); - } else { - log::error!("unexpectedly ended a transaction that wasn't started by this editor"); - } - - cx.emit(EditorEvent::Edited { transaction_id }); - Some(transaction_id) - } else { - None - } - } - - pub fn set_mark(&mut self, _: &actions::SetMark, window: &mut Window, cx: &mut Context) { - if self.selection_mark_mode { - self.change_selections(None, window, cx, |s| { - s.move_with(|_, sel| { - sel.collapse_to(sel.head(), SelectionGoal::None); - }); - }) - } - self.selection_mark_mode = true; - cx.notify(); - } - - pub fn swap_selection_ends( - &mut self, - _: &actions::SwapSelectionEnds, - window: &mut Window, - cx: &mut Context, - ) { - self.change_selections(None, window, cx, |s| { - s.move_with(|_, sel| { - if sel.start != sel.end { - sel.reversed = !sel.reversed - } - }); - }); - self.request_autoscroll(Autoscroll::newest(), cx); - cx.notify(); - } - - pub fn toggle_fold( - &mut self, - _: &actions::ToggleFold, - window: &mut Window, - cx: &mut Context, - ) { - if self.is_singleton(cx) { - let selection = self.selections.newest::(cx); - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let range = if selection.is_empty() { - let point = selection.head().to_display_point(&display_map); - let start = DisplayPoint::new(point.row(), 0).to_point(&display_map); - let end = DisplayPoint::new(point.row(), display_map.line_len(point.row())) - .to_point(&display_map); - start..end - } else { - selection.range() - }; - if display_map.folds_in_range(range).next().is_some() { - self.unfold_lines(&Default::default(), window, cx) - } else { - self.fold(&Default::default(), window, cx) - } - } else { - let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); - let buffer_ids: HashSet<_> = self - .selections - .disjoint_anchor_ranges() - .flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range)) - .collect(); - - let should_unfold = buffer_ids - .iter() - .any(|buffer_id| self.is_buffer_folded(*buffer_id, cx)); - - for buffer_id in buffer_ids { - if should_unfold { - self.unfold_buffer(buffer_id, cx); - } else { - self.fold_buffer(buffer_id, cx); - } - } - } - } - - pub fn toggle_fold_recursive( - &mut self, - _: &actions::ToggleFoldRecursive, - window: &mut Window, - cx: &mut Context, - ) { - let selection = self.selections.newest::(cx); - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let range = if selection.is_empty() { - let point = selection.head().to_display_point(&display_map); - let start = DisplayPoint::new(point.row(), 0).to_point(&display_map); - let end = DisplayPoint::new(point.row(), display_map.line_len(point.row())) - .to_point(&display_map); - start..end - } else { - selection.range() - }; - if display_map.folds_in_range(range).next().is_some() { - self.unfold_recursive(&Default::default(), window, cx) - } else { - self.fold_recursive(&Default::default(), window, cx) - } - } - - pub fn fold(&mut self, _: &actions::Fold, window: &mut Window, cx: &mut Context) { - if self.is_singleton(cx) { - let mut to_fold = Vec::new(); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all_adjusted(cx); - - for selection in selections { - let range = selection.range().sorted(); - let buffer_start_row = range.start.row; - - if range.start.row != range.end.row { - let mut found = false; - let mut row = range.start.row; - while row <= range.end.row { - if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) - { - found = true; - row = crease.range().end.row + 1; - to_fold.push(crease); - } else { - row += 1 - } - } - if found { - continue; - } - } - - for row in (0..=range.start.row).rev() { - 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; - } - } - } - } - } - - self.fold_creases(to_fold, true, window, cx); - } else { - let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); - let buffer_ids = self - .selections - .disjoint_anchor_ranges() - .flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range)) - .collect::>(); - for buffer_id in buffer_ids { - self.fold_buffer(buffer_id, cx); - } - } - } - - fn fold_at_level( - &mut self, - fold_at: &FoldAtLevel, - window: &mut Window, - cx: &mut Context, - ) { - if !self.buffer.read(cx).is_singleton() { - return; - } - - let fold_at_level = fold_at.0; - let snapshot = self.buffer.read(cx).snapshot(cx); - let mut to_fold = Vec::new(); - let mut stack = vec![(0, snapshot.max_row().0, 1)]; - - while let Some((mut start_row, end_row, current_level)) = stack.pop() { - while start_row < end_row { - match self - .snapshot(window, cx) - .crease_for_buffer_row(MultiBufferRow(start_row)) - { - Some(crease) => { - let nested_start_row = crease.range().start.row + 1; - let nested_end_row = crease.range().end.row; - - if current_level < fold_at_level { - stack.push((nested_start_row, nested_end_row, current_level + 1)); - } else if current_level == fold_at_level { - to_fold.push(crease); - } - - start_row = nested_end_row + 1; - } - None => start_row += 1, - } - } - } - - self.fold_creases(to_fold, true, window, cx); - } - - pub fn fold_all(&mut self, _: &actions::FoldAll, window: &mut Window, cx: &mut Context) { - if self.buffer.read(cx).is_singleton() { - let mut fold_ranges = Vec::new(); - let snapshot = self.buffer.read(cx).snapshot(cx); - - for row in 0..snapshot.max_row().0 { - if let Some(foldable_range) = self - .snapshot(window, cx) - .crease_for_buffer_row(MultiBufferRow(row)) - { - fold_ranges.push(foldable_range); - } - } - - self.fold_creases(fold_ranges, true, window, cx); - } else { - self.toggle_fold_multiple_buffers = cx.spawn_in(window, async move |editor, cx| { - editor - .update_in(cx, |editor, _, cx| { - for buffer_id in editor.buffer.read(cx).excerpt_buffer_ids() { - editor.fold_buffer(buffer_id, cx); - } - }) - .ok(); - }); - } - } - - pub fn fold_function_bodies( - &mut self, - _: &actions::FoldFunctionBodies, - window: &mut Window, - cx: &mut Context, - ) { - let snapshot = self.buffer.read(cx).snapshot(cx); - - let ranges = snapshot - .text_object_ranges(0..snapshot.len(), TreeSitterOptions::default()) - .filter_map(|(range, obj)| (obj == TextObject::InsideFunction).then_some(range)) - .collect::>(); - - let creases = ranges - .into_iter() - .map(|range| Crease::simple(range, self.display_map.read(cx).fold_placeholder.clone())) - .collect(); - - self.fold_creases(creases, true, window, cx); - } - - pub fn fold_recursive( - &mut self, - _: &actions::FoldRecursive, - window: &mut Window, - cx: &mut Context, - ) { - let mut to_fold = Vec::new(); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all_adjusted(cx); - - for selection in selections { - let range = selection.range().sorted(); - let buffer_start_row = range.start.row; - - if range.start.row != range.end.row { - let mut found = false; - for row in range.start.row..=range.end.row { - if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) { - found = true; - to_fold.push(crease); - } - } - if found { - continue; - } - } - - for row in (0..=range.start.row).rev() { - if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) { - if crease.range().end.row >= buffer_start_row { - to_fold.push(crease); - } else { - break; - } - } - } - } - - self.fold_creases(to_fold, true, window, cx); - } - - pub fn fold_at( - &mut self, - buffer_row: MultiBufferRow, - window: &mut Window, - cx: &mut Context, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - - if let Some(crease) = display_map.crease_for_buffer_row(buffer_row) { - let autoscroll = self - .selections - .all::(cx) - .iter() - .any(|selection| crease.range().overlaps(&selection.range())); - - self.fold_creases(vec![crease], autoscroll, window, cx); - } - } - - pub fn unfold_lines(&mut self, _: &UnfoldLines, _window: &mut Window, cx: &mut Context) { - if self.is_singleton(cx) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - let selections = self.selections.all::(cx); - let ranges = selections - .iter() - .map(|s| { - let range = s.display_range(&display_map).sorted(); - let mut start = range.start.to_point(&display_map); - let mut end = range.end.to_point(&display_map); - start.column = 0; - end.column = buffer.line_len(MultiBufferRow(end.row)); - start..end - }) - .collect::>(); - - self.unfold_ranges(&ranges, true, true, cx); - } else { - let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); - let buffer_ids = self - .selections - .disjoint_anchor_ranges() - .flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range)) - .collect::>(); - for buffer_id in buffer_ids { - self.unfold_buffer(buffer_id, cx); - } - } - } - - pub fn unfold_recursive( - &mut self, - _: &UnfoldRecursive, - _window: &mut Window, - cx: &mut Context, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all::(cx); - let ranges = selections - .iter() - .map(|s| { - let mut range = s.display_range(&display_map).sorted(); - *range.start.column_mut() = 0; - *range.end.column_mut() = display_map.line_len(range.end.row()); - let start = range.start.to_point(&display_map); - let end = range.end.to_point(&display_map); - start..end - }) - .collect::>(); - - self.unfold_ranges(&ranges, true, true, cx); - } - - pub fn unfold_at( - &mut self, - buffer_row: MultiBufferRow, - _window: &mut Window, - cx: &mut Context, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - - let intersection_range = Point::new(buffer_row.0, 0) - ..Point::new( - buffer_row.0, - display_map.buffer_snapshot.line_len(buffer_row), - ); - - let autoscroll = self - .selections - .all::(cx) - .iter() - .any(|selection| RangeExt::overlaps(&selection.range(), &intersection_range)); - - self.unfold_ranges(&[intersection_range], true, autoscroll, cx); - } - - pub fn unfold_all( - &mut self, - _: &actions::UnfoldAll, - _window: &mut Window, - cx: &mut Context, - ) { - if self.buffer.read(cx).is_singleton() { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - self.unfold_ranges(&[0..display_map.buffer_snapshot.len()], true, true, cx); - } else { - self.toggle_fold_multiple_buffers = cx.spawn(async move |editor, cx| { - editor - .update(cx, |editor, cx| { - for buffer_id in editor.buffer.read(cx).excerpt_buffer_ids() { - editor.unfold_buffer(buffer_id, cx); - } - }) - .ok(); - }); - } - } - - pub fn fold_selected_ranges( - &mut self, - _: &FoldSelectedRanges, - window: &mut Window, - cx: &mut Context, - ) { - let selections = self.selections.all_adjusted(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let ranges = selections - .into_iter() - .map(|s| Crease::simple(s.range(), display_map.fold_placeholder.clone())) - .collect::>(); - self.fold_creases(ranges, true, window, cx); - } - - pub fn fold_ranges( - &mut self, - ranges: Vec>, - auto_scroll: bool, - window: &mut Window, - cx: &mut Context, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let ranges = ranges - .into_iter() - .map(|r| Crease::simple(r, display_map.fold_placeholder.clone())) - .collect::>(); - self.fold_creases(ranges, auto_scroll, window, cx); - } - - pub fn fold_creases( - &mut self, - creases: Vec>, - auto_scroll: bool, - _window: &mut Window, - cx: &mut Context, - ) { - if creases.is_empty() { - return; - } - - let mut buffers_affected = HashSet::default(); - let multi_buffer = self.buffer().read(cx); - for crease in &creases { - if let Some((_, buffer, _)) = - multi_buffer.excerpt_containing(crease.range().start.clone(), cx) - { - buffers_affected.insert(buffer.read(cx).remote_id()); - }; - } - - self.display_map.update(cx, |map, cx| map.fold(creases, cx)); - - if auto_scroll { - self.request_autoscroll(Autoscroll::fit(), cx); - } - - cx.notify(); - - self.scrollbar_marker_state.dirty = true; - self.folds_did_change(cx); - } - - /// Removes any folds whose ranges intersect any of the given ranges. - pub fn unfold_ranges( - &mut self, - ranges: &[Range], - inclusive: bool, - auto_scroll: bool, - cx: &mut Context, - ) { - self.remove_folds_with(ranges, auto_scroll, cx, |map, cx| { - map.unfold_intersecting(ranges.iter().cloned(), inclusive, cx) - }); - self.folds_did_change(cx); - } - - pub fn fold_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) { - if self.buffer().read(cx).is_singleton() || self.is_buffer_folded(buffer_id, cx) { - return; - } - let folded_excerpts = self.buffer().read(cx).excerpts_for_buffer(buffer_id, cx); - self.display_map.update(cx, |display_map, cx| { - display_map.fold_buffers([buffer_id], cx) - }); - cx.emit(EditorEvent::BufferFoldToggled { - ids: folded_excerpts.iter().map(|&(id, _)| id).collect(), - folded: true, - }); - cx.notify(); - } - - pub fn unfold_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) { - if self.buffer().read(cx).is_singleton() || !self.is_buffer_folded(buffer_id, cx) { - return; - } - let unfolded_excerpts = self.buffer().read(cx).excerpts_for_buffer(buffer_id, cx); - self.display_map.update(cx, |display_map, cx| { - display_map.unfold_buffers([buffer_id], cx); - }); - cx.emit(EditorEvent::BufferFoldToggled { - ids: unfolded_excerpts.iter().map(|&(id, _)| id).collect(), - folded: false, - }); - cx.notify(); - } - - pub fn is_buffer_folded(&self, buffer: BufferId, cx: &App) -> bool { - self.display_map.read(cx).is_buffer_folded(buffer) - } - - pub fn folded_buffers<'a>(&self, cx: &'a App) -> &'a HashSet { - self.display_map.read(cx).folded_buffers() - } - - pub fn disable_header_for_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) { - self.display_map.update(cx, |display_map, cx| { - display_map.disable_header_for_buffer(buffer_id, cx); - }); - cx.notify(); - } - - /// Removes any folds with the given ranges. - pub fn remove_folds_with_type( - &mut self, - ranges: &[Range], - type_id: TypeId, - auto_scroll: bool, - cx: &mut Context, - ) { - self.remove_folds_with(ranges, auto_scroll, cx, |map, cx| { - map.remove_folds_with_type(ranges.iter().cloned(), type_id, cx) - }); - self.folds_did_change(cx); - } - - fn remove_folds_with( - &mut self, - ranges: &[Range], - auto_scroll: bool, - cx: &mut Context, - update: impl FnOnce(&mut DisplayMap, &mut Context), - ) { - if ranges.is_empty() { - return; - } - - let mut buffers_affected = HashSet::default(); - let multi_buffer = self.buffer().read(cx); - for range in ranges { - if let Some((_, buffer, _)) = multi_buffer.excerpt_containing(range.start.clone(), cx) { - buffers_affected.insert(buffer.read(cx).remote_id()); - }; - } - - self.display_map.update(cx, update); - - if auto_scroll { - self.request_autoscroll(Autoscroll::fit(), cx); - } - - cx.notify(); - self.scrollbar_marker_state.dirty = true; - self.active_indent_guides_state.dirty = true; - } - - pub fn update_fold_widths( - &mut self, - widths: impl IntoIterator, - cx: &mut Context, - ) -> bool { - self.display_map - .update(cx, |map, cx| map.update_fold_widths(widths, cx)) - } - - pub fn default_fold_placeholder(&self, cx: &App) -> FoldPlaceholder { - self.display_map.read(cx).fold_placeholder.clone() - } - - pub fn set_expand_all_diff_hunks(&mut self, cx: &mut App) { - self.buffer.update(cx, |buffer, cx| { - buffer.set_all_diff_hunks_expanded(cx); - }); - } - - pub fn expand_all_diff_hunks( - &mut self, - _: &ExpandAllDiffHunks, - _window: &mut Window, - cx: &mut Context, - ) { - self.buffer.update(cx, |buffer, cx| { - buffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx) - }); - } - - pub fn toggle_selected_diff_hunks( - &mut self, - _: &ToggleSelectedDiffHunks, - _window: &mut Window, - cx: &mut Context, - ) { - let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); - self.toggle_diff_hunks_in_ranges(ranges, cx); - } - - pub fn diff_hunks_in_ranges<'a>( - &'a self, - ranges: &'a [Range], - buffer: &'a MultiBufferSnapshot, - ) -> impl 'a + Iterator { - ranges.iter().flat_map(move |range| { - let end_excerpt_id = range.end.excerpt_id; - let range = range.to_point(buffer); - let mut peek_end = range.end; - if range.end.row < buffer.max_row().0 { - peek_end = Point::new(range.end.row + 1, 0); - } - buffer - .diff_hunks_in_range(range.start..peek_end) - .filter(move |hunk| hunk.excerpt_id.cmp(&end_excerpt_id, buffer).is_le()) - }) - } - - pub fn has_stageable_diff_hunks_in_ranges( - &self, - ranges: &[Range], - snapshot: &MultiBufferSnapshot, - ) -> bool { - let mut hunks = self.diff_hunks_in_ranges(ranges, &snapshot); - hunks.any(|hunk| hunk.status().has_secondary_hunk()) - } - - pub fn toggle_staged_selected_diff_hunks( - &mut self, - _: &::git::ToggleStaged, - _: &mut Window, - cx: &mut Context, - ) { - let snapshot = self.buffer.read(cx).snapshot(cx); - let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); - let stage = self.has_stageable_diff_hunks_in_ranges(&ranges, &snapshot); - self.stage_or_unstage_diff_hunks(stage, ranges, cx); - } - - pub fn set_render_diff_hunk_controls( - &mut self, - render_diff_hunk_controls: RenderDiffHunkControlsFn, - cx: &mut Context, - ) { - self.render_diff_hunk_controls = render_diff_hunk_controls; - cx.notify(); - } - - pub fn stage_and_next( - &mut self, - _: &::git::StageAndNext, - window: &mut Window, - cx: &mut Context, - ) { - self.do_stage_or_unstage_and_next(true, window, cx); - } - - pub fn unstage_and_next( - &mut self, - _: &::git::UnstageAndNext, - window: &mut Window, - cx: &mut Context, - ) { - self.do_stage_or_unstage_and_next(false, window, cx); - } - - pub fn stage_or_unstage_diff_hunks( - &mut self, - stage: bool, - ranges: Vec>, - cx: &mut Context, - ) { - let task = self.save_buffers_for_ranges_if_needed(&ranges, cx); - cx.spawn(async move |this, cx| { - task.await?; - this.update(cx, |this, cx| { - let snapshot = this.buffer.read(cx).snapshot(cx); - let chunk_by = this - .diff_hunks_in_ranges(&ranges, &snapshot) - .chunk_by(|hunk| hunk.buffer_id); - for (buffer_id, hunks) in &chunk_by { - this.do_stage_or_unstage(stage, buffer_id, hunks, cx); - } - }) - }) - .detach_and_log_err(cx); - } - - fn save_buffers_for_ranges_if_needed( - &mut self, - ranges: &[Range], - cx: &mut Context, - ) -> Task> { - let multibuffer = self.buffer.read(cx); - let snapshot = multibuffer.read(cx); - let buffer_ids: HashSet<_> = ranges - .iter() - .flat_map(|range| snapshot.buffer_ids_for_range(range.clone())) - .collect(); - drop(snapshot); - - let mut buffers = HashSet::default(); - for buffer_id in buffer_ids { - if let Some(buffer_entity) = multibuffer.buffer(buffer_id) { - let buffer = buffer_entity.read(cx); - if buffer.file().is_some_and(|file| file.disk_state().exists()) && buffer.is_dirty() - { - buffers.insert(buffer_entity); - } - } - } - - if let Some(project) = &self.project { - project.update(cx, |project, cx| project.save_buffers(buffers, cx)) - } else { - Task::ready(Ok(())) - } - } - - fn do_stage_or_unstage_and_next( - &mut self, - stage: bool, - window: &mut Window, - cx: &mut Context, - ) { - let ranges = self.selections.disjoint_anchor_ranges().collect::>(); - - if ranges.iter().any(|range| range.start != range.end) { - self.stage_or_unstage_diff_hunks(stage, ranges, cx); - return; - } - - self.stage_or_unstage_diff_hunks(stage, ranges, cx); - let snapshot = self.snapshot(window, cx); - let position = self.selections.newest::(cx).head(); - let mut row = snapshot - .buffer_snapshot - .diff_hunks_in_range(position..snapshot.buffer_snapshot.max_point()) - .find(|hunk| hunk.row_range.start.0 > position.row) - .map(|hunk| hunk.row_range.start); - - let all_diff_hunks_expanded = self.buffer().read(cx).all_diff_hunks_expanded(); - // Outside of the project diff editor, wrap around to the beginning. - if !all_diff_hunks_expanded { - row = row.or_else(|| { - snapshot - .buffer_snapshot - .diff_hunks_in_range(Point::zero()..position) - .find(|hunk| hunk.row_range.end.0 < position.row) - .map(|hunk| hunk.row_range.start) - }); - } - - if let Some(row) = row { - let destination = Point::new(row.0, 0); - let autoscroll = Autoscroll::center(); - - self.unfold_ranges(&[destination..destination], false, false, cx); - self.change_selections(Some(autoscroll), window, cx, |s| { - s.select_ranges([destination..destination]); - }); - } - } - - fn do_stage_or_unstage( - &self, - stage: bool, - buffer_id: BufferId, - hunks: impl Iterator, - cx: &mut App, - ) -> Option<()> { - 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(); - let file_exists = buffer_snapshot - .file() - .is_some_and(|file| file.disk_state().exists()); - diff.update(cx, |diff, cx| { - diff.stage_or_unstage_hunks( - stage, - &hunks - .map(|hunk| buffer_diff::DiffHunk { - buffer_range: hunk.buffer_range, - diff_base_byte_range: hunk.diff_base_byte_range, - secondary_status: hunk.secondary_status, - range: Point::zero()..Point::zero(), // unused - }) - .collect::>(), - &buffer_snapshot, - file_exists, - cx, - ) - }); - None - } - - pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context) { - let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); - self.buffer - .update(cx, |buffer, cx| buffer.expand_diff_hunks(ranges, cx)) - } - - pub fn clear_expanded_diff_hunks(&mut self, cx: &mut Context) -> bool { - self.buffer.update(cx, |buffer, cx| { - let ranges = vec![Anchor::min()..Anchor::max()]; - if !buffer.all_diff_hunks_expanded() - && buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx) - { - buffer.collapse_diff_hunks(ranges, cx); - true - } else { - false - } - }) - } - - fn toggle_diff_hunks_in_ranges( - &mut self, - ranges: Vec>, - cx: &mut Context, - ) { - self.buffer.update(cx, |buffer, cx| { - let expand = !buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx); - buffer.expand_or_collapse_diff_hunks(ranges, expand, cx); - }) - } - - fn toggle_single_diff_hunk(&mut self, range: Range, cx: &mut Context) { - self.buffer.update(cx, |buffer, cx| { - let snapshot = buffer.snapshot(cx); - let excerpt_id = range.end.excerpt_id; - let point_range = range.to_point(&snapshot); - let expand = !buffer.single_hunk_is_expanded(range, cx); - buffer.expand_or_collapse_diff_hunks_inner([(point_range, excerpt_id)], expand, cx); - }) - } - - pub(crate) fn apply_all_diff_hunks( - &mut self, - _: &ApplyAllDiffHunks, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let buffers = self.buffer.read(cx).all_buffers(); - for branch_buffer in buffers { - branch_buffer.update(cx, |branch_buffer, cx| { - branch_buffer.merge_into_base(Vec::new(), cx); - }); - } - - if let Some(project) = self.project.clone() { - self.save(true, project, window, cx).detach_and_log_err(cx); - } - } - - pub(crate) fn apply_selected_diff_hunks( - &mut self, - _: &ApplyDiffHunk, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let snapshot = self.snapshot(window, cx); - let hunks = snapshot.hunks_for_ranges(self.selections.ranges(cx)); - let mut ranges_by_buffer = HashMap::default(); - self.transact(window, cx, |editor, _window, cx| { - for hunk in hunks { - if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) { - ranges_by_buffer - .entry(buffer.clone()) - .or_insert_with(Vec::new) - .push(hunk.buffer_range.to_offset(buffer.read(cx))); - } - } - - for (buffer, ranges) in ranges_by_buffer { - buffer.update(cx, |buffer, cx| { - buffer.merge_into_base(ranges, cx); - }); - } - }); - - if let Some(project) = self.project.clone() { - self.save(true, project, window, cx).detach_and_log_err(cx); - } - } - - pub fn set_gutter_hovered(&mut self, hovered: bool, cx: &mut Context) { - if hovered != self.gutter_hovered { - self.gutter_hovered = hovered; - cx.notify(); - } - } - - pub fn insert_blocks( - &mut self, - blocks: impl IntoIterator>, - autoscroll: Option, - cx: &mut Context, - ) -> Vec { - let blocks = self - .display_map - .update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx)); - if let Some(autoscroll) = autoscroll { - self.request_autoscroll(autoscroll, cx); - } - cx.notify(); - blocks - } - - pub fn resize_blocks( - &mut self, - heights: HashMap, - autoscroll: Option, - cx: &mut Context, - ) { - self.display_map - .update(cx, |display_map, cx| display_map.resize_blocks(heights, cx)); - if let Some(autoscroll) = autoscroll { - self.request_autoscroll(autoscroll, cx); - } - cx.notify(); - } - - pub fn replace_blocks( - &mut self, - renderers: HashMap, - autoscroll: Option, - cx: &mut Context, - ) { - self.display_map - .update(cx, |display_map, _cx| display_map.replace_blocks(renderers)); - if let Some(autoscroll) = autoscroll { - self.request_autoscroll(autoscroll, cx); - } - cx.notify(); - } - - pub fn remove_blocks( - &mut self, - block_ids: HashSet, - autoscroll: Option, - cx: &mut Context, - ) { - self.display_map.update(cx, |display_map, cx| { - display_map.remove_blocks(block_ids, cx) - }); - if let Some(autoscroll) = autoscroll { - self.request_autoscroll(autoscroll, cx); - } - cx.notify(); - } - - pub fn row_for_block( - &self, - block_id: CustomBlockId, - cx: &mut Context, - ) -> Option { - self.display_map - .update(cx, |map, cx| map.row_for_block(block_id, cx)) - } - - pub(crate) fn set_focused_block(&mut self, focused_block: FocusedBlock) { - self.focused_block = Some(focused_block); - } - - pub(crate) fn take_focused_block(&mut self) -> Option { - self.focused_block.take() - } - - pub fn insert_creases( - &mut self, - creases: impl IntoIterator>, - cx: &mut Context, - ) -> Vec { - self.display_map - .update(cx, |map, cx| map.insert_creases(creases, cx)) - } - - pub fn remove_creases( - &mut self, - ids: impl IntoIterator, - cx: &mut Context, - ) { - self.display_map - .update(cx, |map, cx| map.remove_creases(ids, cx)); - } - - pub fn longest_row(&self, cx: &mut App) -> DisplayRow { - self.display_map - .update(cx, |map, cx| map.snapshot(cx)) - .longest_row() - } - - pub fn max_point(&self, cx: &mut App) -> DisplayPoint { - self.display_map - .update(cx, |map, cx| map.snapshot(cx)) - .max_point() - } - - pub fn text(&self, cx: &App) -> String { - self.buffer.read(cx).read(cx).text() - } - - pub fn is_empty(&self, cx: &App) -> bool { - self.buffer.read(cx).read(cx).is_empty() - } - - pub fn text_option(&self, cx: &App) -> Option { - let text = self.text(cx); - let text = text.trim(); - - if text.is_empty() { - return None; - } - - Some(text.to_string()) - } - - pub fn set_text( - &mut self, - text: impl Into>, - window: &mut Window, - cx: &mut Context, - ) { - self.transact(window, cx, |this, _, cx| { - this.buffer - .read(cx) - .as_singleton() - .expect("you can only call set_text on editors for singleton buffers") - .update(cx, |buffer, cx| buffer.set_text(text, cx)); - }); - } - - pub fn display_text(&self, cx: &mut App) -> String { - self.display_map - .update(cx, |map, cx| map.snapshot(cx)) - .text() - } - - pub fn wrap_guides(&self, cx: &App) -> SmallVec<[(usize, bool); 2]> { - let mut wrap_guides = smallvec::smallvec![]; - - if self.show_wrap_guides == Some(false) { - return wrap_guides; - } - - let settings = self.buffer.read(cx).language_settings(cx); - if settings.show_wrap_guides { - match self.soft_wrap_mode(cx) { - SoftWrap::Column(soft_wrap) => { - wrap_guides.push((soft_wrap as usize, true)); - } - SoftWrap::Bounded(soft_wrap) => { - wrap_guides.push((soft_wrap as usize, true)); - } - SoftWrap::GitDiff | SoftWrap::None | SoftWrap::EditorWidth => {} - } - wrap_guides.extend(settings.wrap_guides.iter().map(|guide| (*guide, false))) - } - - wrap_guides - } - - pub fn soft_wrap_mode(&self, cx: &App) -> SoftWrap { - let settings = self.buffer.read(cx).language_settings(cx); - let mode = self.soft_wrap_mode_override.unwrap_or(settings.soft_wrap); - match mode { - language_settings::SoftWrap::PreferLine | language_settings::SoftWrap::None => { - SoftWrap::None - } - language_settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, - language_settings::SoftWrap::PreferredLineLength => { - SoftWrap::Column(settings.preferred_line_length) - } - language_settings::SoftWrap::Bounded => { - SoftWrap::Bounded(settings.preferred_line_length) - } - } - } - - pub fn set_soft_wrap_mode( - &mut self, - mode: language_settings::SoftWrap, - - cx: &mut Context, - ) { - self.soft_wrap_mode_override = Some(mode); - cx.notify(); - } - - pub fn set_hard_wrap(&mut self, hard_wrap: Option, cx: &mut Context) { - self.hard_wrap = hard_wrap; - cx.notify(); - } - - pub fn set_text_style_refinement(&mut self, style: TextStyleRefinement) { - self.text_style_refinement = Some(style); - } - - /// called by the Element so we know what style we were most recently rendered with. - pub(crate) fn set_style( - &mut self, - style: EditorStyle, - window: &mut Window, - cx: &mut Context, - ) { - let rem_size = window.rem_size(); - self.display_map.update(cx, |map, cx| { - map.set_font( - style.text.font(), - style.text.font_size.to_pixels(rem_size), - cx, - ) - }); - self.style = Some(style); - } - - pub fn style(&self) -> Option<&EditorStyle> { - self.style.as_ref() - } - - // Called by the element. This method is not designed to be called outside of the editor - // element's layout code because it does not notify when rewrapping is computed synchronously. - pub(crate) fn set_wrap_width(&self, width: Option, cx: &mut App) -> bool { - self.display_map - .update(cx, |map, cx| map.set_wrap_width(width, cx)) - } - - pub fn set_soft_wrap(&mut self) { - self.soft_wrap_mode_override = Some(language_settings::SoftWrap::EditorWidth) - } - - pub fn toggle_soft_wrap(&mut self, _: &ToggleSoftWrap, _: &mut Window, cx: &mut Context) { - if self.soft_wrap_mode_override.is_some() { - self.soft_wrap_mode_override.take(); - } else { - let soft_wrap = match self.soft_wrap_mode(cx) { - SoftWrap::GitDiff => return, - SoftWrap::None => language_settings::SoftWrap::EditorWidth, - SoftWrap::EditorWidth | SoftWrap::Column(_) | SoftWrap::Bounded(_) => { - language_settings::SoftWrap::None - } - }; - self.soft_wrap_mode_override = Some(soft_wrap); - } - cx.notify(); - } - - pub fn toggle_tab_bar(&mut self, _: &ToggleTabBar, _: &mut Window, cx: &mut Context) { - let Some(workspace) = self.workspace() else { - return; - }; - let fs = workspace.read(cx).app_state().fs.clone(); - let current_show = TabBarSettings::get_global(cx).show; - update_settings_file::(fs, cx, move |setting, _| { - setting.show = Some(!current_show); - }); - } - - pub fn toggle_indent_guides( - &mut self, - _: &ToggleIndentGuides, - _: &mut Window, - cx: &mut Context, - ) { - let currently_enabled = self.should_show_indent_guides().unwrap_or_else(|| { - self.buffer - .read(cx) - .language_settings(cx) - .indent_guides - .enabled - }); - self.show_indent_guides = Some(!currently_enabled); - cx.notify(); - } - - fn should_show_indent_guides(&self) -> Option { - self.show_indent_guides - } - - pub fn toggle_line_numbers( - &mut self, - _: &ToggleLineNumbers, - _: &mut Window, - cx: &mut Context, - ) { - let mut editor_settings = EditorSettings::get_global(cx).clone(); - editor_settings.gutter.line_numbers = !editor_settings.gutter.line_numbers; - EditorSettings::override_global(editor_settings, cx); - } - - pub fn line_numbers_enabled(&self, cx: &App) -> bool { - if let Some(show_line_numbers) = self.show_line_numbers { - return show_line_numbers; - } - EditorSettings::get_global(cx).gutter.line_numbers - } - - pub fn should_use_relative_line_numbers(&self, cx: &mut App) -> bool { - self.use_relative_line_numbers - .unwrap_or(EditorSettings::get_global(cx).relative_line_numbers) - } - - pub fn toggle_relative_line_numbers( - &mut self, - _: &ToggleRelativeLineNumbers, - _: &mut Window, - cx: &mut Context, - ) { - let is_relative = self.should_use_relative_line_numbers(cx); - self.set_relative_line_number(Some(!is_relative), cx) - } - - pub fn set_relative_line_number(&mut self, is_relative: Option, cx: &mut Context) { - self.use_relative_line_numbers = is_relative; - cx.notify(); - } - - pub fn set_show_gutter(&mut self, show_gutter: bool, cx: &mut Context) { - self.show_gutter = show_gutter; - cx.notify(); - } - - pub fn set_show_scrollbars(&mut self, show_scrollbars: bool, cx: &mut Context) { - self.show_scrollbars = show_scrollbars; - cx.notify(); - } - - pub fn disable_scrolling(&mut self, cx: &mut Context) { - self.disable_scrolling = true; - cx.notify(); - } - - pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut Context) { - self.show_line_numbers = Some(show_line_numbers); - cx.notify(); - } - - pub fn disable_expand_excerpt_buttons(&mut self, cx: &mut Context) { - self.disable_expand_excerpt_buttons = true; - cx.notify(); - } - - pub fn set_show_git_diff_gutter(&mut self, show_git_diff_gutter: bool, cx: &mut Context) { - self.show_git_diff_gutter = Some(show_git_diff_gutter); - cx.notify(); - } - - pub fn set_show_code_actions(&mut self, show_code_actions: bool, cx: &mut Context) { - self.show_code_actions = Some(show_code_actions); - cx.notify(); - } - - pub fn set_show_runnables(&mut self, show_runnables: bool, cx: &mut Context) { - self.show_runnables = Some(show_runnables); - cx.notify(); - } - - pub fn set_show_breakpoints(&mut self, show_breakpoints: bool, cx: &mut Context) { - self.show_breakpoints = Some(show_breakpoints); - cx.notify(); - } - - pub fn set_masked(&mut self, masked: bool, cx: &mut Context) { - if self.display_map.read(cx).masked != masked { - self.display_map.update(cx, |map, _| map.masked = masked); - } - cx.notify() - } - - pub fn set_show_wrap_guides(&mut self, show_wrap_guides: bool, cx: &mut Context) { - self.show_wrap_guides = Some(show_wrap_guides); - cx.notify(); - } - - pub fn set_show_indent_guides(&mut self, show_indent_guides: bool, cx: &mut Context) { - self.show_indent_guides = Some(show_indent_guides); - cx.notify(); - } - - 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()) { - 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) { - return Some(project_path.path.to_path_buf()); - } - } - - None - } - - fn target_file<'a>(&self, cx: &'a App) -> Option<&'a dyn language::LocalFile> { - self.active_excerpt(cx)? - .1 - .read(cx) - .file() - .and_then(|f| f.as_local()) - } - - pub fn target_file_abs_path(&self, cx: &mut Context) -> Option { - 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.as_ref()?.read(cx); - project.absolute_path(&project_path, cx) - } else { - buffer - .file() - .and_then(|file| file.as_local().map(|file| file.abs_path(cx))) - } - }) - } - - 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.as_ref()?.read(cx); - let entry = project.entry_for_path(&project_path, cx)?; - let path = entry.path.to_path_buf(); - Some(path) - }) - } - - pub fn reveal_in_finder( - &mut self, - _: &RevealInFileManager, - _window: &mut Window, - cx: &mut Context, - ) { - if let Some(target) = self.target_file(cx) { - cx.reveal_path(&target.abs_path(cx)); - } - } - - pub fn copy_path( - &mut self, - _: &zed_actions::workspace::CopyPath, - _window: &mut Window, - cx: &mut Context, - ) { - 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())); - } - } - } - - pub fn copy_relative_path( - &mut self, - _: &zed_actions::workspace::CopyRelativePath, - _window: &mut Window, - cx: &mut Context, - ) { - 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())); - } - } - } - - pub fn project_path(&self, cx: &App) -> Option { - if let Some(buffer) = self.buffer.read(cx).as_singleton() { - buffer.read(cx).project_path(cx) - } else { - None - } - } - - // Returns true if the editor handled a go-to-line request - pub fn go_to_active_debug_line(&mut self, window: &mut Window, cx: &mut Context) -> bool { - maybe!({ - let breakpoint_store = self.breakpoint_store.as_ref()?; - - let Some(active_stack_frame) = breakpoint_store.read(cx).active_position().cloned() - else { - self.clear_row_highlights::(); - return None; - }; - - let position = active_stack_frame.position; - let buffer_id = position.buffer_id?; - let snapshot = self - .project - .as_ref()? - .read(cx) - .buffer_for_id(buffer_id, cx)? - .read(cx) - .snapshot(); - - let mut handled = false; - for (id, ExcerptRange { context, .. }) in - self.buffer.read(cx).excerpts_for_buffer(buffer_id, cx) - { - if context.start.cmp(&position, &snapshot).is_ge() - || context.end.cmp(&position, &snapshot).is_lt() - { - continue; - } - let snapshot = self.buffer.read(cx).snapshot(cx); - let multibuffer_anchor = snapshot.anchor_in_excerpt(id, position)?; - - handled = true; - self.clear_row_highlights::(); - self.go_to_line::( - multibuffer_anchor, - Some(cx.theme().colors().editor_debugger_active_line_background), - window, - cx, - ); - - cx.notify(); - } - - handled.then_some(()) - }) - .is_some() - } - - pub fn copy_file_name_without_extension( - &mut self, - _: &CopyFileNameWithoutExtension, - _: &mut Window, - cx: &mut Context, - ) { - 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) { - 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())); - } - } - } - } - - pub fn toggle_git_blame( - &mut self, - _: &::git::Blame, - window: &mut Window, - cx: &mut Context, - ) { - self.show_git_blame_gutter = !self.show_git_blame_gutter; - - if self.show_git_blame_gutter && !self.has_blame_entries(cx) { - self.start_git_blame(true, window, cx); - } - - cx.notify(); - } - - pub fn toggle_git_blame_inline( - &mut self, - _: &ToggleGitBlameInline, - window: &mut Window, - cx: &mut Context, - ) { - self.toggle_git_blame_inline_internal(true, window, cx); - cx.notify(); - } - - pub fn open_git_blame_commit( - &mut self, - _: &OpenGitBlameCommit, - window: &mut Window, - cx: &mut Context, - ) { - self.open_git_blame_commit_internal(window, cx); - } - - fn open_git_blame_commit_internal( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - let blame = self.blame.as_ref()?; - let snapshot = self.snapshot(window, cx); - let cursor = self.selections.newest::(cx).head(); - let (buffer, point, _) = snapshot.buffer_snapshot.point_to_buffer_point(cursor)?; - let blame_entry = blame - .update(cx, |blame, cx| { - blame - .blame_for_rows( - &[RowInfo { - buffer_id: Some(buffer.remote_id()), - buffer_row: Some(point.row), - ..Default::default() - }], - cx, - ) - .next() - }) - .flatten()?; - let renderer = cx.global::().0.clone(); - let repo = blame.read(cx).repository(cx)?; - let workspace = self.workspace()?.downgrade(); - renderer.open_blame_commit(blame_entry, repo, workspace, window, cx); - None - } - - pub fn git_blame_inline_enabled(&self) -> bool { - self.git_blame_inline_enabled - } - - pub fn toggle_selection_menu( - &mut self, - _: &ToggleSelectionMenu, - _: &mut Window, - cx: &mut Context, - ) { - self.show_selection_menu = self - .show_selection_menu - .map(|show_selections_menu| !show_selections_menu) - .or_else(|| Some(!EditorSettings::get_global(cx).toolbar.selections_menu)); - - cx.notify(); - } - - pub fn selection_menu_enabled(&self, cx: &App) -> bool { - self.show_selection_menu - .unwrap_or_else(|| EditorSettings::get_global(cx).toolbar.selections_menu) - } - - fn start_git_blame( - &mut self, - user_triggered: bool, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(project) = self.project.as_ref() { - let Some(buffer) = self.buffer().read(cx).as_singleton() else { - return; - }; - - if buffer.read(cx).file().is_none() { - return; - } - - let focused = self.focus_handle(cx).contains_focused(window, cx); - - let project = project.clone(); - let blame = cx.new(|cx| GitBlame::new(buffer, project, user_triggered, focused, cx)); - self.blame_subscription = - Some(cx.observe_in(&blame, window, |_, _, _, cx| cx.notify())); - self.blame = Some(blame); - } - } - - fn toggle_git_blame_inline_internal( - &mut self, - user_triggered: bool, - window: &mut Window, - cx: &mut Context, - ) { - if self.git_blame_inline_enabled { - self.git_blame_inline_enabled = false; - self.show_git_blame_inline = false; - self.show_git_blame_inline_delay_task.take(); - } else { - self.git_blame_inline_enabled = true; - self.start_git_blame_inline(user_triggered, window, cx); - } - - cx.notify(); - } - - fn start_git_blame_inline( - &mut self, - user_triggered: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.start_git_blame(user_triggered, window, cx); - - if ProjectSettings::get_global(cx) - .git - .inline_blame_delay() - .is_some() - { - self.start_inline_blame_timer(window, cx); - } else { - self.show_git_blame_inline = true - } - } - - pub fn blame(&self) -> Option<&Entity> { - self.blame.as_ref() - } - - pub fn show_git_blame_gutter(&self) -> bool { - self.show_git_blame_gutter - } - - pub fn render_git_blame_gutter(&self, cx: &App) -> bool { - self.show_git_blame_gutter && self.has_blame_entries(cx) - } - - pub fn render_git_blame_inline(&self, window: &Window, cx: &App) -> bool { - self.show_git_blame_inline - && (self.focus_handle.is_focused(window) || self.inline_blame_popover.is_some()) - && !self.newest_selection_head_on_empty_line(cx) - && self.has_blame_entries(cx) - } - - fn has_blame_entries(&self, cx: &App) -> bool { - self.blame() - .map_or(false, |blame| blame.read(cx).has_generated_entries()) - } - - fn newest_selection_head_on_empty_line(&self, cx: &App) -> bool { - let cursor_anchor = self.selections.newest_anchor().head(); - - let snapshot = self.buffer.read(cx).snapshot(cx); - let buffer_row = MultiBufferRow(cursor_anchor.to_point(&snapshot).row); - - snapshot.line_len(buffer_row) == 0 - } - - fn get_permalink_to_line(&self, cx: &mut Context) -> Task> { - let buffer_and_selection = maybe!({ - let selection = self.selections.newest::(cx); - let selection_range = selection.range(); - - let multi_buffer = self.buffer().read(cx); - let multi_buffer_snapshot = multi_buffer.snapshot(cx); - let buffer_ranges = multi_buffer_snapshot.range_to_buffer_ranges(selection_range); - - let (buffer, range, _) = if selection.reversed { - buffer_ranges.first() - } else { - 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().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.as_ref() else { - return Task::ready(Err(anyhow!("editor does not have project"))); - }; - - project.update(cx, |project, cx| { - project.get_permalink_to_line(&buffer, selection, cx) - }) - } - - pub fn copy_permalink_to_line( - &mut self, - _: &CopyPermalinkToLine, - window: &mut Window, - cx: &mut Context, - ) { - let permalink_task = self.get_permalink_to_line(cx); - let workspace = self.workspace(); - - cx.spawn_in(window, async move |_, cx| match permalink_task.await { - Ok(permalink) => { - cx.update(|_, cx| { - cx.write_to_clipboard(ClipboardItem::new_string(permalink.to_string())); - }) - .ok(); - } - Err(err) => { - let message = format!("Failed to copy permalink: {err}"); - - anyhow::Result::<()>::Err(err).log_err(); - - if let Some(workspace) = workspace { - workspace - .update_in(cx, |workspace, _, cx| { - struct CopyPermalinkToLine; - - workspace.show_toast( - Toast::new( - NotificationId::unique::(), - message, - ), - cx, - ) - }) - .ok(); - } - } - }) - .detach(); - } - - pub fn copy_file_location( - &mut self, - _: &CopyFileLocation, - _: &mut Window, - cx: &mut Context, - ) { - let selection = self.selections.newest::(cx).start.row + 1; - 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}"))); - } - } - } - - pub fn open_permalink_to_line( - &mut self, - _: &OpenPermalinkToLine, - window: &mut Window, - cx: &mut Context, - ) { - let permalink_task = self.get_permalink_to_line(cx); - let workspace = self.workspace(); - - cx.spawn_in(window, async move |_, cx| match permalink_task.await { - Ok(permalink) => { - cx.update(|_, cx| { - cx.open_url(permalink.as_ref()); - }) - .ok(); - } - Err(err) => { - let message = format!("Failed to open permalink: {err}"); - - anyhow::Result::<()>::Err(err).log_err(); - - if let Some(workspace) = workspace { - workspace - .update(cx, |workspace, cx| { - struct OpenPermalinkToLine; - - workspace.show_toast( - Toast::new( - NotificationId::unique::(), - message, - ), - cx, - ) - }) - .ok(); - } - } - }) - .detach(); - } - - pub fn insert_uuid_v4( - &mut self, - _: &InsertUuidV4, - window: &mut Window, - cx: &mut Context, - ) { - self.insert_uuid(UuidVersion::V4, window, cx); - } - - pub fn insert_uuid_v7( - &mut self, - _: &InsertUuidV7, - window: &mut Window, - cx: &mut Context, - ) { - self.insert_uuid(UuidVersion::V7, window, cx); - } - - fn insert_uuid(&mut self, version: UuidVersion, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - let edits = this - .selections - .all::(cx) - .into_iter() - .map(|selection| { - let uuid = match version { - UuidVersion::V4 => uuid::Uuid::new_v4(), - UuidVersion::V7 => uuid::Uuid::now_v7(), - }; - - (selection.range(), uuid.to_string()) - }); - this.edit(edits, cx); - this.refresh_inline_completion(true, false, window, cx); - }); - } - - pub fn open_selections_in_multibuffer( - &mut self, - _: &OpenSelectionsInMultibuffer, - window: &mut Window, - cx: &mut Context, - ) { - let multibuffer = self.buffer.read(cx); - - let Some(buffer) = multibuffer.as_singleton() else { - return; - }; - - let Some(workspace) = self.workspace() else { - return; - }; - - let locations = self - .selections - .disjoint_anchors() - .iter() - .map(|range| Location { - buffer: buffer.clone(), - range: range.start.text_anchor..range.end.text_anchor, - }) - .collect::>(); - - let title = multibuffer.title(cx).to_string(); - - cx.spawn_in(window, async move |_, cx| { - workspace.update_in(cx, |workspace, window, cx| { - Self::open_locations_in_multibuffer( - workspace, - locations, - format!("Selections for '{title}'"), - false, - MultibufferSelectionMode::All, - window, - cx, - ); - }) - }) - .detach(); - } - - /// Adds a row highlight for the given range. If a row has multiple highlights, the - /// last highlight added will be used. - /// - /// If the range ends at the beginning of a line, then that line will not be highlighted. - pub fn highlight_rows( - &mut self, - range: Range, - color: Hsla, - options: RowHighlightOptions, - cx: &mut Context, - ) { - let snapshot = self.buffer().read(cx).snapshot(cx); - let row_highlights = self.highlighted_rows.entry(TypeId::of::()).or_default(); - let ix = row_highlights.binary_search_by(|highlight| { - Ordering::Equal - .then_with(|| highlight.range.start.cmp(&range.start, &snapshot)) - .then_with(|| highlight.range.end.cmp(&range.end, &snapshot)) - }); - - if let Err(mut ix) = ix { - let index = post_inc(&mut self.highlight_order); - - // If this range intersects with the preceding highlight, then merge it with - // the preceding highlight. Otherwise insert a new highlight. - let mut merged = false; - if ix > 0 { - let prev_highlight = &mut row_highlights[ix - 1]; - if prev_highlight - .range - .end - .cmp(&range.start, &snapshot) - .is_ge() - { - ix -= 1; - if prev_highlight.range.end.cmp(&range.end, &snapshot).is_lt() { - prev_highlight.range.end = range.end; - } - merged = true; - prev_highlight.index = index; - prev_highlight.color = color; - prev_highlight.options = options; - } - } - - if !merged { - row_highlights.insert( - ix, - RowHighlight { - range: range.clone(), - index, - color, - options, - type_id: TypeId::of::(), - }, - ); - } - - // If any of the following highlights intersect with this one, merge them. - while let Some(next_highlight) = row_highlights.get(ix + 1) { - let highlight = &row_highlights[ix]; - if next_highlight - .range - .start - .cmp(&highlight.range.end, &snapshot) - .is_le() - { - if next_highlight - .range - .end - .cmp(&highlight.range.end, &snapshot) - .is_gt() - { - row_highlights[ix].range.end = next_highlight.range.end; - } - row_highlights.remove(ix + 1); - } else { - break; - } - } - } - } - - /// Remove any highlighted row ranges of the given type that intersect the - /// given ranges. - pub fn remove_highlighted_rows( - &mut self, - ranges_to_remove: Vec>, - cx: &mut Context, - ) { - let snapshot = self.buffer().read(cx).snapshot(cx); - let row_highlights = self.highlighted_rows.entry(TypeId::of::()).or_default(); - let mut ranges_to_remove = ranges_to_remove.iter().peekable(); - row_highlights.retain(|highlight| { - while let Some(range_to_remove) = ranges_to_remove.peek() { - match range_to_remove.end.cmp(&highlight.range.start, &snapshot) { - Ordering::Less | Ordering::Equal => { - ranges_to_remove.next(); - } - Ordering::Greater => { - match range_to_remove.start.cmp(&highlight.range.end, &snapshot) { - Ordering::Less | Ordering::Equal => { - return false; - } - Ordering::Greater => break, - } - } - } - } - - true - }) - } - - /// Clear all anchor ranges for a certain highlight context type, so no corresponding rows will be highlighted. - pub fn clear_row_highlights(&mut self) { - self.highlighted_rows.remove(&TypeId::of::()); - } - - /// For a highlight given context type, gets all anchor ranges that will be used for row highlighting. - pub fn highlighted_rows(&self) -> impl '_ + Iterator, Hsla)> { - self.highlighted_rows - .get(&TypeId::of::()) - .map_or(&[] as &[_], |vec| vec.as_slice()) - .iter() - .map(|highlight| (highlight.range.clone(), highlight.color)) - } - - /// Merges all anchor ranges for all context types ever set, picking the last highlight added in case of a row conflict. - /// Returns a map of display rows that are highlighted and their corresponding highlight color. - /// Allows to ignore certain kinds of highlights. - pub fn highlighted_display_rows( - &self, - window: &mut Window, - cx: &mut App, - ) -> BTreeMap { - let snapshot = self.snapshot(window, cx); - let mut used_highlight_orders = HashMap::default(); - self.highlighted_rows - .iter() - .flat_map(|(_, highlighted_rows)| highlighted_rows.iter()) - .fold( - BTreeMap::::new(), - |mut unique_rows, highlight| { - let start = highlight.range.start.to_display_point(&snapshot); - let end = highlight.range.end.to_display_point(&snapshot); - let start_row = start.row().0; - let end_row = if highlight.range.end.text_anchor != text::Anchor::MAX - && end.column() == 0 - { - end.row().0.saturating_sub(1) - } else { - end.row().0 - }; - for row in start_row..=end_row { - let used_index = - used_highlight_orders.entry(row).or_insert(highlight.index); - if highlight.index >= *used_index { - *used_index = highlight.index; - unique_rows.insert( - DisplayRow(row), - LineHighlight { - include_gutter: highlight.options.include_gutter, - border: None, - background: highlight.color.into(), - type_id: Some(highlight.type_id), - }, - ); - } - } - unique_rows - }, - ) - } - - pub fn highlighted_display_row_for_autoscroll( - &self, - snapshot: &DisplaySnapshot, - ) -> Option { - self.highlighted_rows - .values() - .flat_map(|highlighted_rows| highlighted_rows.iter()) - .filter_map(|highlight| { - if highlight.options.autoscroll { - Some(highlight.range.start.to_display_point(snapshot).row()) - } else { - None - } - }) - .min() - } - - pub fn set_search_within_ranges(&mut self, ranges: &[Range], cx: &mut Context) { - self.highlight_background::( - ranges, - |colors| colors.editor_document_highlight_read_background, - cx, - ) - } - - pub fn set_breadcrumb_header(&mut self, new_header: String) { - self.breadcrumb_header = Some(new_header); - } - - pub fn clear_search_within_ranges(&mut self, cx: &mut Context) { - self.clear_background_highlights::(cx); - } - - pub fn highlight_background( - &mut self, - ranges: &[Range], - color_fetcher: fn(&ThemeColors) -> Hsla, - cx: &mut Context, - ) { - self.background_highlights - .insert(TypeId::of::(), (color_fetcher, Arc::from(ranges))); - self.scrollbar_marker_state.dirty = true; - cx.notify(); - } - - pub fn clear_background_highlights( - &mut self, - cx: &mut Context, - ) -> Option { - let text_highlights = self.background_highlights.remove(&TypeId::of::())?; - if !text_highlights.1.is_empty() { - self.scrollbar_marker_state.dirty = true; - cx.notify(); - } - Some(text_highlights) - } - - pub fn highlight_gutter( - &mut self, - ranges: &[Range], - color_fetcher: fn(&App) -> Hsla, - cx: &mut Context, - ) { - self.gutter_highlights - .insert(TypeId::of::(), (color_fetcher, Arc::from(ranges))); - cx.notify(); - } - - pub fn clear_gutter_highlights( - &mut self, - cx: &mut Context, - ) -> Option { - cx.notify(); - self.gutter_highlights.remove(&TypeId::of::()) - } - - #[cfg(feature = "test-support")] - pub fn all_text_background_highlights( - &self, - window: &mut Window, - cx: &mut Context, - ) -> Vec<(Range, Hsla)> { - let snapshot = self.snapshot(window, cx); - let buffer = &snapshot.buffer_snapshot; - let start = buffer.anchor_before(0); - let end = buffer.anchor_after(buffer.len()); - let theme = cx.theme().colors(); - self.background_highlights_in_range(start..end, &snapshot, theme) - } - - #[cfg(feature = "test-support")] - pub fn search_background_highlights(&mut self, cx: &mut Context) -> Vec> { - let snapshot = self.buffer().read(cx).snapshot(cx); - - let highlights = self - .background_highlights - .get(&TypeId::of::()); - - if let Some((_color, ranges)) = highlights { - ranges - .iter() - .map(|range| range.start.to_point(&snapshot)..range.end.to_point(&snapshot)) - .collect_vec() - } else { - vec![] - } - } - - fn document_highlights_for_position<'a>( - &'a self, - position: Anchor, - buffer: &'a MultiBufferSnapshot, - ) -> impl 'a + Iterator> { - let read_highlights = self - .background_highlights - .get(&TypeId::of::()) - .map(|h| &h.1); - let write_highlights = self - .background_highlights - .get(&TypeId::of::()) - .map(|h| &h.1); - let left_position = position.bias_left(buffer); - let right_position = position.bias_right(buffer); - read_highlights - .into_iter() - .chain(write_highlights) - .flat_map(move |ranges| { - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&left_position, buffer); - if cmp.is_ge() { - Ordering::Greater - } else { - Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - - ranges[start_ix..] - .iter() - .take_while(move |range| range.start.cmp(&right_position, buffer).is_le()) - }) - } - - pub fn has_background_highlights(&self) -> bool { - self.background_highlights - .get(&TypeId::of::()) - .map_or(false, |(_, highlights)| !highlights.is_empty()) - } - - pub fn background_highlights_in_range( - &self, - search_range: Range, - display_snapshot: &DisplaySnapshot, - theme: &ThemeColors, - ) -> Vec<(Range, Hsla)> { - let mut results = Vec::new(); - for (color_fetcher, ranges) in self.background_highlights.values() { - let color = color_fetcher(theme); - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe - .end - .cmp(&search_range.start, &display_snapshot.buffer_snapshot); - if cmp.is_gt() { - Ordering::Greater - } else { - Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - for range in &ranges[start_ix..] { - if range - .start - .cmp(&search_range.end, &display_snapshot.buffer_snapshot) - .is_ge() - { - break; - } - - let start = range.start.to_display_point(display_snapshot); - let end = range.end.to_display_point(display_snapshot); - results.push((start..end, color)) - } - } - results - } - - pub fn background_highlight_row_ranges( - &self, - search_range: Range, - display_snapshot: &DisplaySnapshot, - count: usize, - ) -> Vec> { - let mut results = Vec::new(); - let Some((_, ranges)) = self.background_highlights.get(&TypeId::of::()) else { - return vec![]; - }; - - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe - .end - .cmp(&search_range.start, &display_snapshot.buffer_snapshot); - if cmp.is_gt() { - Ordering::Greater - } else { - Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - let mut push_region = |start: Option, end: Option| { - if let (Some(start_display), Some(end_display)) = (start, end) { - results.push( - start_display.to_display_point(display_snapshot) - ..=end_display.to_display_point(display_snapshot), - ); - } - }; - let mut start_row: Option = None; - let mut end_row: Option = None; - if ranges.len() > count { - return Vec::new(); - } - for range in &ranges[start_ix..] { - if range - .start - .cmp(&search_range.end, &display_snapshot.buffer_snapshot) - .is_ge() - { - break; - } - let end = range.end.to_point(&display_snapshot.buffer_snapshot); - 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() { - assert_eq!(end_row, None); - start_row = Some(start); - end_row = Some(end); - continue; - } - if let Some(current_end) = end_row.as_mut() { - if start.row > current_end.row + 1 { - push_region(start_row, end_row); - start_row = Some(start); - end_row = Some(end); - } else { - // Merge two hunks. - *current_end = end; - } - } else { - unreachable!(); - } - } - // We might still have a hunk that was not rendered (if there was a search hit on the last line) - push_region(start_row, end_row); - results - } - - pub fn gutter_highlights_in_range( - &self, - search_range: Range, - display_snapshot: &DisplaySnapshot, - cx: &App, - ) -> Vec<(Range, Hsla)> { - let mut results = Vec::new(); - for (color_fetcher, ranges) in self.gutter_highlights.values() { - let color = color_fetcher(cx); - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe - .end - .cmp(&search_range.start, &display_snapshot.buffer_snapshot); - if cmp.is_gt() { - Ordering::Greater - } else { - Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - for range in &ranges[start_ix..] { - if range - .start - .cmp(&search_range.end, &display_snapshot.buffer_snapshot) - .is_ge() - { - break; - } - - let start = range.start.to_display_point(display_snapshot); - let end = range.end.to_display_point(display_snapshot); - results.push((start..end, color)) - } - } - results - } - - /// Get the text ranges corresponding to the redaction query - pub fn redacted_ranges( - &self, - search_range: Range, - display_snapshot: &DisplaySnapshot, - cx: &App, - ) -> Vec> { - display_snapshot - .buffer_snapshot - .redacted_ranges(search_range, |file| { - if let Some(file) = file { - file.is_private() - && EditorSettings::get( - Some(SettingsLocation { - worktree_id: file.worktree_id(cx), - path: file.path().as_ref(), - }), - cx, - ) - .redact_private_values - } else { - false - } - }) - .map(|range| { - range.start.to_display_point(display_snapshot) - ..range.end.to_display_point(display_snapshot) - }) - .collect() - } - - pub fn highlight_text( - &mut self, - ranges: Vec>, - style: HighlightStyle, - cx: &mut Context, - ) { - self.display_map.update(cx, |map, _| { - map.highlight_text(TypeId::of::(), ranges, style) - }); - cx.notify(); - } - - pub(crate) fn highlight_inlays( - &mut self, - highlights: Vec, - style: HighlightStyle, - cx: &mut Context, - ) { - self.display_map.update(cx, |map, _| { - map.highlight_inlays(TypeId::of::(), highlights, style) - }); - cx.notify(); - } - - pub fn text_highlights<'a, T: 'static>( - &'a self, - cx: &'a App, - ) -> Option<(HighlightStyle, &'a [Range])> { - self.display_map.read(cx).text_highlights(TypeId::of::()) - } - - pub fn clear_highlights(&mut self, cx: &mut Context) { - let cleared = self - .display_map - .update(cx, |map, _| map.clear_highlights(TypeId::of::())); - if cleared { - cx.notify(); - } - } - - pub fn show_local_cursors(&self, window: &mut Window, cx: &mut App) -> bool { - (self.read_only(cx) || self.blink_manager.read(cx).visible()) - && self.focus_handle.is_focused(window) - } - - pub fn set_show_cursor_when_unfocused(&mut self, is_enabled: bool, cx: &mut Context) { - self.show_cursor_when_unfocused = is_enabled; - cx.notify(); - } - - fn on_buffer_changed(&mut self, _: Entity, cx: &mut Context) { - cx.notify(); - } - - fn on_debug_session_event( - &mut self, - _session: Entity, - event: &SessionEvent, - cx: &mut Context, - ) { - match event { - SessionEvent::InvalidateInlineValue => { - self.refresh_inline_values(cx); - } - _ => {} - } - } - - fn refresh_inline_values(&mut self, cx: &mut Context) { - let Some(project) = self.project.clone() else { - return; - }; - let Some(buffer) = self.buffer.read(cx).as_singleton() else { - return; - }; - if !self.inline_value_cache.enabled { - let inlays = std::mem::take(&mut self.inline_value_cache.inlays); - self.splice_inlays(&inlays, Vec::new(), cx); - return; - } - - let current_execution_position = self - .highlighted_rows - .get(&TypeId::of::()) - .and_then(|lines| lines.last().map(|line| line.range.start)); - - self.inline_value_cache.refresh_task = cx.spawn(async move |editor, cx| { - let snapshot = editor - .update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx)) - .ok()?; - - let inline_values = editor - .update(cx, |_, cx| { - let Some(current_execution_position) = current_execution_position else { - return Some(Task::ready(Ok(Vec::new()))); - }; - - // todo(debugger) when introducing multi buffer inline values check execution position's buffer id to make sure the text - // anchor is in the same buffer - let range = - buffer.read(cx).anchor_before(0)..current_execution_position.text_anchor; - project.inline_values(buffer, range, cx) - }) - .ok() - .flatten()? - .await - .context("refreshing debugger inlays") - .log_err()?; - - let (excerpt_id, buffer_id) = snapshot - .excerpts() - .next() - .map(|excerpt| (excerpt.0, excerpt.1.remote_id()))?; - editor - .update(cx, |editor, cx| { - let new_inlays = inline_values - .into_iter() - .map(|debugger_value| { - Inlay::debugger_hint( - post_inc(&mut editor.next_inlay_id), - Anchor::in_buffer(excerpt_id, buffer_id, debugger_value.position), - debugger_value.text(), - ) - }) - .collect::>(); - let mut inlay_ids = new_inlays.iter().map(|inlay| inlay.id).collect(); - std::mem::swap(&mut editor.inline_value_cache.inlays, &mut inlay_ids); - - editor.splice_inlays(&inlay_ids, new_inlays, cx); - }) - .ok()?; - Some(()) - }); - } - - fn on_buffer_event( - &mut self, - multibuffer: &Entity, - event: &multi_buffer::Event, - window: &mut Window, - cx: &mut Context, - ) { - match event { - multi_buffer::Event::Edited { - singleton_buffer_edited, - edited_buffer: buffer_edited, - } => { - self.scrollbar_marker_state.dirty = true; - self.active_indent_guides_state.dirty = true; - self.refresh_active_diagnostics(cx); - self.refresh_code_actions(window, cx); - self.refresh_selected_text_highlights(true, window, cx); - refresh_matching_bracket_highlights(self, window, cx); - if self.has_active_inline_completion() { - self.update_visible_inline_completion(window, cx); - } - if let Some(buffer) = buffer_edited { - let buffer_id = buffer.read(cx).remote_id(); - if !self.registered_buffers.contains_key(&buffer_id) { - if let Some(project) = self.project.as_ref() { - project.update(cx, |project, cx| { - self.registered_buffers.insert( - buffer_id, - project.register_buffer_with_language_servers(&buffer, cx), - ); - }) - } - } - } - cx.emit(EditorEvent::BufferEdited); - cx.emit(SearchEvent::MatchesInvalidated); - if *singleton_buffer_edited { - if let Some(project) = &self.project { - #[allow(clippy::mutable_key_type)] - let languages_affected = multibuffer.update(cx, |multibuffer, cx| { - multibuffer - .all_buffers() - .into_iter() - .filter_map(|buffer| { - buffer.update(cx, |buffer, cx| { - let language = buffer.language()?; - let should_discard = project.update(cx, |project, cx| { - project.is_local() - && !project.has_language_servers_for(buffer, cx) - }); - should_discard.not().then_some(language.clone()) - }) - }) - .collect::>() - }); - if !languages_affected.is_empty() { - self.refresh_inlay_hints( - InlayHintRefreshReason::BufferEdited(languages_affected), - cx, - ); - } - } - } - - let Some(project) = &self.project else { return }; - let (telemetry, is_via_ssh) = { - let project = project.read(cx); - let telemetry = project.client().telemetry().clone(); - let is_via_ssh = project.is_via_ssh(); - (telemetry, is_via_ssh) - }; - refresh_linked_ranges(self, window, cx); - telemetry.log_edit_event("editor", is_via_ssh); - } - multi_buffer::Event::ExcerptsAdded { - buffer, - predecessor, - excerpts, - } => { - 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() { - if let Some(project) = &self.project { - get_uncommitted_diff_for_buffer( - project, - [buffer.clone()], - self.buffer.clone(), - cx, - ) - .detach(); - } - } - cx.emit(EditorEvent::ExcerptsAdded { - buffer: buffer.clone(), - predecessor: *predecessor, - excerpts: excerpts.clone(), - }); - self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); - } - multi_buffer::Event::ExcerptsRemoved { - ids, - removed_buffer_ids, - } => { - self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx); - let buffer = self.buffer.read(cx); - self.registered_buffers - .retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some()); - jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx); - cx.emit(EditorEvent::ExcerptsRemoved { - ids: ids.clone(), - removed_buffer_ids: removed_buffer_ids.clone(), - }) - } - multi_buffer::Event::ExcerptsEdited { - excerpt_ids, - buffer_ids, - } => { - self.display_map.update(cx, |map, cx| { - map.unfold_buffers(buffer_ids.iter().copied(), cx) - }); - cx.emit(EditorEvent::ExcerptsEdited { - ids: excerpt_ids.clone(), - }) - } - multi_buffer::Event::ExcerptsExpanded { ids } => { - self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); - cx.emit(EditorEvent::ExcerptsExpanded { ids: ids.clone() }) - } - multi_buffer::Event::Reparsed(buffer_id) => { - self.tasks_update_task = Some(self.refresh_runnables(window, cx)); - jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx); - - cx.emit(EditorEvent::Reparsed(*buffer_id)); - } - multi_buffer::Event::DiffHunksToggled => { - self.tasks_update_task = Some(self.refresh_runnables(window, cx)); - } - multi_buffer::Event::LanguageChanged(buffer_id) => { - linked_editing_ranges::refresh_linked_ranges(self, window, cx); - jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx); - cx.emit(EditorEvent::Reparsed(*buffer_id)); - cx.notify(); - } - multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged), - multi_buffer::Event::Saved => cx.emit(EditorEvent::Saved), - multi_buffer::Event::FileHandleChanged - | multi_buffer::Event::Reloaded - | multi_buffer::Event::BufferDiffChanged => cx.emit(EditorEvent::TitleChanged), - multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed), - multi_buffer::Event::DiagnosticsUpdated => { - self.refresh_active_diagnostics(cx); - self.refresh_inline_diagnostics(true, window, cx); - self.scrollbar_marker_state.dirty = true; - cx.notify(); - } - _ => {} - }; - } - - fn on_display_map_changed( - &mut self, - _: Entity, - _: &mut Window, - cx: &mut Context, - ) { - cx.notify(); - } - - fn settings_changed(&mut self, window: &mut Window, cx: &mut Context) { - self.tasks_update_task = Some(self.refresh_runnables(window, cx)); - self.update_edit_prediction_settings(cx); - self.refresh_inline_completion(true, false, window, cx); - self.refresh_inlay_hints( - InlayHintRefreshReason::SettingsChange(inlay_hint_settings( - self.selections.newest_anchor().head(), - &self.buffer.read(cx).snapshot(cx), - cx, - )), - cx, - ); - - let old_cursor_shape = self.cursor_shape; - - { - let editor_settings = EditorSettings::get_global(cx); - self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin; - self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs; - self.cursor_shape = editor_settings.cursor_shape.unwrap_or_default(); - self.hide_mouse_mode = editor_settings.hide_mouse.unwrap_or_default(); - } - - if old_cursor_shape != self.cursor_shape { - cx.emit(EditorEvent::CursorShapeChanged); - } - - let project_settings = ProjectSettings::get_global(cx); - self.serialize_dirty_buffers = project_settings.session.restore_unsaved_buffers; - - if self.mode.is_full() { - let show_inline_diagnostics = project_settings.diagnostics.inline.enabled; - let inline_blame_enabled = project_settings.git.inline_blame_enabled(); - if self.show_inline_diagnostics != show_inline_diagnostics { - self.show_inline_diagnostics = show_inline_diagnostics; - self.refresh_inline_diagnostics(false, window, cx); - } - - if self.git_blame_inline_enabled != inline_blame_enabled { - self.toggle_git_blame_inline_internal(false, window, cx); - } - } - - cx.notify(); - } - - pub fn set_searchable(&mut self, searchable: bool) { - self.searchable = searchable; - } - - pub fn searchable(&self) -> bool { - self.searchable - } - - fn open_proposed_changes_editor( - &mut self, - _: &OpenProposedChangesEditor, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = self.workspace() else { - cx.propagate(); - return; - }; - - let selections = self.selections.all::(cx); - let multi_buffer = self.buffer.read(cx); - let multi_buffer_snapshot = multi_buffer.snapshot(cx); - let mut new_selections_by_buffer = HashMap::default(); - for selection in selections { - for (buffer, range, _) in - multi_buffer_snapshot.range_to_buffer_ranges(selection.start..selection.end) - { - let mut range = range.to_point(buffer); - range.start.column = 0; - range.end.column = buffer.line_len(range.end.row); - new_selections_by_buffer - .entry(multi_buffer.buffer(buffer.remote_id()).unwrap()) - .or_insert(Vec::new()) - .push(range) - } - } - - let proposed_changes_buffers = new_selections_by_buffer - .into_iter() - .map(|(buffer, ranges)| ProposedChangeLocation { buffer, ranges }) - .collect::>(); - let proposed_changes_editor = cx.new(|cx| { - ProposedChangesEditor::new( - "Proposed changes", - proposed_changes_buffers, - self.project.clone(), - window, - cx, - ) - }); - - window.defer(cx, move |window, cx| { - workspace.update(cx, |workspace, cx| { - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item( - Box::new(proposed_changes_editor), - true, - true, - None, - window, - cx, - ); - }); - }); - }); - } - - pub fn open_excerpts_in_split( - &mut self, - _: &OpenExcerptsSplit, - window: &mut Window, - cx: &mut Context, - ) { - self.open_excerpts_common(None, true, window, cx) - } - - pub fn open_excerpts(&mut self, _: &OpenExcerpts, window: &mut Window, cx: &mut Context) { - self.open_excerpts_common(None, false, window, cx) - } - - fn open_excerpts_common( - &mut self, - jump_data: Option, - split: bool, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = self.workspace() else { - cx.propagate(); - return; - }; - - if self.buffer.read(cx).is_singleton() { - cx.propagate(); - return; - } - - let mut new_selections_by_buffer = HashMap::default(); - match &jump_data { - Some(JumpData::MultiBufferPoint { - excerpt_id, - position, - anchor, - line_offset_from_top, - }) => { - let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); - if let Some(buffer) = multi_buffer_snapshot - .buffer_id_for_excerpt(*excerpt_id) - .and_then(|buffer_id| self.buffer.read(cx).buffer(buffer_id)) - { - let buffer_snapshot = buffer.read(cx).snapshot(); - let jump_to_point = if buffer_snapshot.can_resolve(anchor) { - language::ToPoint::to_point(anchor, &buffer_snapshot) - } else { - buffer_snapshot.clip_point(*position, Bias::Left) - }; - let jump_to_offset = buffer_snapshot.point_to_offset(jump_to_point); - new_selections_by_buffer.insert( - buffer, - ( - vec![jump_to_offset..jump_to_offset], - Some(*line_offset_from_top), - ), - ); - } - } - Some(JumpData::MultiBufferRow { - row, - line_offset_from_top, - }) => { - let point = MultiBufferPoint::new(row.0, 0); - if let Some((buffer, buffer_point, _)) = - self.buffer.read(cx).point_to_buffer_point(point, cx) - { - let buffer_offset = buffer.read(cx).point_to_offset(buffer_point); - new_selections_by_buffer - .entry(buffer) - .or_insert((Vec::new(), Some(*line_offset_from_top))) - .0 - .push(buffer_offset..buffer_offset) - } - } - None => { - let selections = self.selections.all::(cx); - let multi_buffer = self.buffer.read(cx); - for selection in selections { - for (snapshot, range, _, anchor) in multi_buffer - .snapshot(cx) - .range_to_buffer_ranges_with_deleted_hunks(selection.range()) - { - if let Some(anchor) = anchor { - // 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( - &anchor.text_anchor, - &buffer_handle.read(cx).snapshot(), - ); - let range = offset..offset; - new_selections_by_buffer - .entry(buffer_handle) - .or_insert((Vec::new(), None)) - .0 - .push(range) - } else { - let Some(buffer_handle) = multi_buffer.buffer(snapshot.remote_id()) - else { - continue; - }; - new_selections_by_buffer - .entry(buffer_handle) - .or_insert((Vec::new(), None)) - .0 - .push(range) - } - } - } - } - } - - new_selections_by_buffer - .retain(|buffer, _| Self::can_open_excerpts_in_file(buffer.read(cx).file())); - - if new_selections_by_buffer.is_empty() { - return; - } - - // We defer the pane interaction because we ourselves are a workspace item - // and activating a new item causes the pane to call a method on us reentrantly, - // which panics if we're on the stack. - window.defer(cx, move |window, cx| { - workspace.update(cx, |workspace, cx| { - let pane = if split { - workspace.adjacent_pane(window, cx) - } else { - workspace.active_pane().clone() - }; - - for (buffer, (ranges, scroll_offset)) in new_selections_by_buffer { - let editor = buffer - .read(cx) - .file() - .is_none() - .then(|| { - // Handle file-less buffers separately: those are not really the project items, so won't have a project path or entity id, - // so `workspace.open_project_item` will never find them, always opening a new editor. - // Instead, we try to activate the existing editor in the pane first. - let (editor, pane_item_index) = - pane.read(cx).items().enumerate().find_map(|(i, item)| { - let editor = item.downcast::()?; - let singleton_buffer = - editor.read(cx).buffer().read(cx).as_singleton()?; - if singleton_buffer == buffer { - Some((editor, i)) - } else { - None - } - })?; - pane.update(cx, |pane, cx| { - pane.activate_item(pane_item_index, true, true, window, cx) - }); - Some(editor) - }) - .flatten() - .unwrap_or_else(|| { - workspace.open_project_item::( - pane.clone(), - buffer, - true, - true, - window, - cx, - ) - }); - - editor.update(cx, |editor, cx| { - let autoscroll = match scroll_offset { - Some(scroll_offset) => Autoscroll::top_relative(scroll_offset as usize), - None => Autoscroll::newest(), - }; - let nav_history = editor.nav_history.take(); - editor.change_selections(Some(autoscroll), window, cx, |s| { - s.select_ranges(ranges); - }); - editor.nav_history = nav_history; - }); - } - }) - }); - } - - // 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.map_or(true, |file| project::File::from_dyn(Some(file)).is_some()) - } - - fn marked_text_ranges(&self, cx: &App) -> Option>> { - let snapshot = self.buffer.read(cx).read(cx); - let (_, ranges) = self.text_highlights::(cx)?; - Some( - ranges - .iter() - .map(move |range| { - range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot) - }) - .collect(), - ) - } - - fn selection_replacement_ranges( - &self, - range: Range, - cx: &mut App, - ) -> Vec> { - let selections = self.selections.all::(cx); - let newest_selection = selections - .iter() - .max_by_key(|selection| selection.id) - .unwrap(); - let start_delta = range.start.0 as isize - newest_selection.start.0 as isize; - let end_delta = range.end.0 as isize - newest_selection.end.0 as isize; - let snapshot = self.buffer.read(cx).read(cx); - selections - .into_iter() - .map(|mut selection| { - selection.start.0 = - (selection.start.0 as isize).saturating_add(start_delta) as usize; - selection.end.0 = (selection.end.0 as isize).saturating_add(end_delta) as usize; - snapshot.clip_offset_utf16(selection.start, Bias::Left) - ..snapshot.clip_offset_utf16(selection.end, Bias::Right) - }) - .collect() - } - - fn report_editor_event( - &self, - event_type: &'static str, - file_extension: Option, - cx: &App, - ) { - if cfg!(any(test, feature = "test-support")) { - return; - } - - let Some(project) = &self.project else { return }; - - // If None, we are in a file without an extension - let file = self - .buffer - .read(cx) - .as_singleton() - .and_then(|b| b.read(cx).file()); - let file_extension = file_extension.or(file - .as_ref() - .and_then(|file| Path::new(file.file_name(cx)).extension()) - .and_then(|e| e.to_str()) - .map(|a| a.to_string())); - - let vim_mode = vim_enabled(cx); - - let edit_predictions_provider = all_language_settings(file, cx).edit_predictions.provider; - let copilot_enabled = edit_predictions_provider - == language::language_settings::EditPredictionProvider::Copilot; - let copilot_enabled_for_language = self - .buffer - .read(cx) - .language_settings(cx) - .show_edit_predictions; - - let project = project.read(cx); - 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, - /// with each line being an array of {text, highlight} objects. - fn copy_highlight_json( - &mut self, - _: &CopyHighlightJson, - window: &mut Window, - cx: &mut Context, - ) { - #[derive(Serialize)] - struct Chunk<'a> { - text: String, - highlight: Option<&'a str>, - } - - let snapshot = self.buffer.read(cx).snapshot(cx); - let range = self - .selected_text_range(false, window, cx) - .and_then(|selection| { - if selection.range.is_empty() { - None - } else { - Some(selection.range) - } - }) - .unwrap_or_else(|| 0..snapshot.len()); - - let chunks = snapshot.chunks(range, true); - let mut lines = Vec::new(); - let mut line: VecDeque = VecDeque::new(); - - let Some(style) = self.style.as_ref() else { - return; - }; - - for chunk in chunks { - let highlight = chunk - .syntax_highlight_id - .and_then(|id| id.name(&style.syntax)); - 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() { - if last_token.highlight == highlight { - last_token.text.push_str(text); - merged_with_last_token = true; - } - } - - if !merged_with_last_token { - line.push_back(Chunk { - text: text.into(), - highlight, - }); - } - - if chunk_lines.peek().is_some() { - if line.len() > 1 && line.front().unwrap().text.is_empty() { - line.pop_front(); - } - if line.len() > 1 && line.back().unwrap().text.is_empty() { - line.pop_back(); - } - - lines.push(mem::take(&mut line)); - } - } - } - - let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { - return; - }; - cx.write_to_clipboard(ClipboardItem::new_string(lines)); - } - - pub fn open_context_menu( - &mut self, - _: &OpenContextMenu, - window: &mut Window, - cx: &mut Context, - ) { - self.request_autoscroll(Autoscroll::newest(), cx); - let position = self.selections.newest_display(cx).start; - mouse_context_menu::deploy_context_menu(self, None, position, window, cx); - } - - pub fn inlay_hint_cache(&self) -> &InlayHintCache { - &self.inlay_hint_cache - } - - pub fn replay_insert_event( - &mut self, - text: &str, - relative_utf16_range: Option>, - window: &mut Window, - cx: &mut Context, - ) { - if !self.input_enabled { - cx.emit(EditorEvent::InputIgnored { text: text.into() }); - return; - } - if let Some(relative_utf16_range) = relative_utf16_range { - let selections = self.selections.all::(cx); - self.change_selections(None, window, cx, |s| { - let new_ranges = selections.into_iter().map(|range| { - let start = OffsetUtf16( - range - .head() - .0 - .saturating_add_signed(relative_utf16_range.start), - ); - let end = OffsetUtf16( - range - .head() - .0 - .saturating_add_signed(relative_utf16_range.end), - ); - start..end - }); - s.select_ranges(new_ranges); - }); - } - - self.handle_input(text, window, cx); - } - - pub fn supports_inlay_hints(&self, cx: &mut App) -> bool { - let Some(provider) = self.semantics_provider.as_ref() else { - return false; - }; - - let mut supports = false; - self.buffer().update(cx, |this, cx| { - this.for_each_buffer(|buffer| { - supports |= provider.supports_inlay_hints(buffer, cx); - }); - }); - - supports - } - - pub fn is_focused(&self, window: &Window) -> bool { - self.focus_handle.is_focused(window) - } - - fn handle_focus(&mut self, window: &mut Window, cx: &mut Context) { - cx.emit(EditorEvent::Focused); - - if let Some(descendant) = self - .last_focused_descendant - .take() - .and_then(|descendant| descendant.upgrade()) - { - window.focus(&descendant); - } else { - if let Some(blame) = self.blame.as_ref() { - blame.update(cx, GitBlame::focus) - } - - self.blink_manager.update(cx, |blink_manager, cx| { - blink_manager.enable(cx); - }); - self.show_cursor_names(window, cx); - self.buffer.update(cx, |buffer, cx| { - buffer.finalize_last_transaction(cx); - if self.leader_peer_id.is_none() { - buffer.set_active_selections( - &self.selections.disjoint_anchors(), - self.selections.line_mode, - self.cursor_shape, - cx, - ); - } - }); - } - } - - fn handle_focus_in(&mut self, _: &mut Window, cx: &mut Context) { - cx.emit(EditorEvent::FocusedIn) - } - - fn handle_focus_out( - &mut self, - event: FocusOutEvent, - _window: &mut Window, - cx: &mut Context, - ) { - if event.blurred != self.focus_handle { - self.last_focused_descendant = Some(event.blurred); - } - self.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx); - } - - pub fn handle_blur(&mut self, window: &mut Window, cx: &mut Context) { - self.blink_manager.update(cx, BlinkManager::disable); - self.buffer - .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); - - if let Some(blame) = self.blame.as_ref() { - blame.update(cx, GitBlame::blur) - } - if !self.hover_state.focused(window, cx) { - hide_hover(self, cx); - } - if !self - .context_menu - .borrow() - .as_ref() - .is_some_and(|context_menu| context_menu.focused(window, cx)) - { - self.hide_context_menu(window, cx); - } - self.discard_inline_completion(false, cx); - cx.emit(EditorEvent::Blurred); - cx.notify(); - } - - pub fn register_action( - &mut self, - listener: impl Fn(&A, &mut Window, &mut App) + 'static, - ) -> Subscription { - let id = self.next_editor_action_id.post_inc(); - let listener = Arc::new(listener); - self.editor_actions.borrow_mut().insert( - id, - Box::new(move |window, _| { - let listener = listener.clone(); - window.on_action(TypeId::of::(), move |action, phase, window, cx| { - let action = action.downcast_ref().unwrap(); - if phase == DispatchPhase::Bubble { - listener(action, window, cx) - } - }) - }), - ); - - let editor_actions = self.editor_actions.clone(); - Subscription::new(move || { - editor_actions.borrow_mut().remove(&id); - }) - } - - pub fn file_header_size(&self) -> u32 { - FILE_HEADER_HEIGHT - } - - pub fn restore( - &mut self, - revert_changes: HashMap, Rope)>>, - window: &mut Window, - cx: &mut Context, - ) { - let workspace = self.workspace(); - 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 { - if let Some(buffer) = multi_buffer.buffer(buffer_id) { - buffer.update(cx, |buffer, cx| { - buffer.edit( - changes - .into_iter() - .map(|(range, text)| (range, text.to_string())), - None, - cx, - ); - }); - - if let Some(project) = - project.filter(|_| multi_buffer.all_diff_hunks_expanded()) - { - project.update(cx, |project, cx| { - tasks.push((buffer.clone(), project.save_buffer(buffer, cx))); - }) - } - } - } - tasks - }); - cx.spawn_in(window, async move |_, cx| { - for (buffer, task) in save_tasks { - let result = task.await; - if result.is_err() { - let Some(path) = buffer - .read_with(cx, |buffer, cx| buffer.project_path(cx)) - .ok() - else { - continue; - }; - if let Some((workspace, path)) = workspace.as_ref().zip(path) { - let Some(task) = cx - .update_window_entity(&workspace, |workspace, window, cx| { - workspace - .open_path_preview(path, None, false, false, false, window, cx) - }) - .ok() - else { - continue; - }; - task.await.log_err(); - } - } - } - }) - .detach(); - self.change_selections(None, window, cx, |selections| selections.refresh()); - } - - pub fn to_pixel_point( - &self, - source: multi_buffer::Anchor, - editor_snapshot: &EditorSnapshot, - window: &mut Window, - ) -> Option> { - let source_point = source.to_display_point(editor_snapshot); - self.display_to_pixel_point(source_point, editor_snapshot, window) - } - - pub fn display_to_pixel_point( - &self, - source: DisplayPoint, - editor_snapshot: &EditorSnapshot, - window: &mut Window, - ) -> Option> { - let line_height = self.style()?.text.line_height_in_pixels(window.rem_size()); - let text_layout_details = self.text_layout_details(window); - let scroll_top = text_layout_details - .scroll_anchor - .scroll_position(editor_snapshot) - .y; - - if source.row().as_f32() < scroll_top.floor() { - return None; - } - let source_x = editor_snapshot.x_for_display_point(source, &text_layout_details); - let source_y = line_height * (source.row().as_f32() - scroll_top); - Some(gpui::Point::new(source_x, source_y)) - } - - pub fn has_visible_completions_menu(&self) -> bool { - !self.edit_prediction_preview_is_active() - && self.context_menu.borrow().as_ref().map_or(false, |menu| { - menu.visible() && matches!(menu, CodeContextMenu::Completions(_)) - }) - } - - pub fn register_addon(&mut self, instance: T) { - self.addons - .insert(std::any::TypeId::of::(), Box::new(instance)); - } - - pub fn unregister_addon(&mut self) { - self.addons.remove(&std::any::TypeId::of::()); - } - - pub fn addon(&self) -> Option<&T> { - let type_id = std::any::TypeId::of::(); - self.addons - .get(&type_id) - .and_then(|item| item.to_any().downcast_ref::()) - } - - pub fn addon_mut(&mut self) -> Option<&mut T> { - let type_id = std::any::TypeId::of::(); - self.addons - .get_mut(&type_id) - .and_then(|item| item.to_any_mut()?.downcast_mut::()) - } - - fn character_size(&self, window: &mut Window) -> gpui::Size { - let text_layout_details = self.text_layout_details(window); - let style = &text_layout_details.editor_style; - let font_id = window.text_system().resolve_font(&style.text.font()); - let font_size = style.text.font_size.to_pixels(window.rem_size()); - let line_height = style.text.line_height_in_pixels(window.rem_size()); - let em_width = window.text_system().em_width(font_id, font_size).unwrap(); - - gpui::Size::new(em_width, line_height) - } - - pub fn wait_for_diff_to_load(&self) -> Option>> { - self.load_diff_task.clone() - } - - fn read_metadata_from_db( - &mut self, - item_id: u64, - workspace_id: WorkspaceId, - window: &mut Window, - cx: &mut Context, - ) { - if self.is_singleton(cx) - && WorkspaceSettings::get(None, cx).restore_on_startup != RestoreOnStartupBehavior::None - { - let buffer_snapshot = OnceCell::new(); - - 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() { - if !selections.is_empty() { - let snapshot = - buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); - self.change_selections(None, 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.read_scroll_position_from_db(item_id, workspace_id, window, cx); - } -} - -fn vim_enabled(cx: &App) -> bool { - cx.global::() - .raw_user_settings() - .get("vim_mode") - == Some(&serde_json::Value::Bool(true)) -} - -// Consider user intent and default settings -fn choose_completion_range( - completion: &Completion, - intent: CompletionIntent, - buffer: &Entity, - cx: &mut Context, -) -> Range { - fn should_replace( - completion: &Completion, - insert_range: &Range, - intent: CompletionIntent, - completion_mode_setting: LspInsertMode, - buffer: &Buffer, - ) -> bool { - // specific actions take precedence over settings - match intent { - CompletionIntent::CompleteWithInsert => return false, - CompletionIntent::CompleteWithReplace => return true, - CompletionIntent::Complete | CompletionIntent::Compose => {} - } - - match completion_mode_setting { - LspInsertMode::Insert => false, - LspInsertMode::Replace => true, - LspInsertMode::ReplaceSubsequence => { - let mut text_to_replace = buffer.chars_for_range( - buffer.anchor_before(completion.replace_range.start) - ..buffer.anchor_after(completion.replace_range.end), - ); - let mut completion_text = completion.new_text.chars(); - - // is `text_to_replace` a subsequence of `completion_text` - text_to_replace - .all(|needle_ch| completion_text.any(|haystack_ch| haystack_ch == needle_ch)) - } - LspInsertMode::ReplaceSuffix => { - let range_after_cursor = insert_range.end..completion.replace_range.end; - - let text_after_cursor = buffer - .text_for_range( - buffer.anchor_before(range_after_cursor.start) - ..buffer.anchor_after(range_after_cursor.end), - ) - .collect::(); - completion.new_text.ends_with(&text_after_cursor) - } - } - } - - let buffer = buffer.read(cx); - - if let CompletionSource::Lsp { - insert_range: Some(insert_range), - .. - } = &completion.source - { - let completion_mode_setting = - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) - .completions - .lsp_insert_mode; - - if !should_replace( - completion, - &insert_range, - intent, - completion_mode_setting, - buffer, - ) { - return insert_range.to_offset(buffer); - } - } - - completion.replace_range.to_offset(buffer) -} - -fn insert_extra_newline_brackets( - buffer: &MultiBufferSnapshot, - range: Range, - language: &language::LanguageScope, -) -> bool { - let leading_whitespace_len = buffer - .reversed_chars_at(range.start) - .take_while(|c| c.is_whitespace() && *c != '\n') - .map(|c| c.len_utf8()) - .sum::(); - let trailing_whitespace_len = buffer - .chars_at(range.end) - .take_while(|c| c.is_whitespace() && *c != '\n') - .map(|c| c.len_utf8()) - .sum::(); - let range = range.start - leading_whitespace_len..range.end + trailing_whitespace_len; - - language.brackets().any(|(pair, enabled)| { - let pair_start = pair.start.trim_end(); - let pair_end = pair.end.trim_start(); - - enabled - && pair.newline - && buffer.contains_str_at(range.end, pair_end) - && buffer.contains_str_at(range.start.saturating_sub(pair_start.len()), pair_start) - }) -} - -fn insert_extra_newline_tree_sitter(buffer: &MultiBufferSnapshot, range: Range) -> bool { - let (buffer, range) = match buffer.range_to_buffer_ranges(range).as_slice() { - [(buffer, range, _)] => (*buffer, range.clone()), - _ => return false, - }; - let pair = { - let mut result: Option = None; - - for pair in buffer - .all_bracket_ranges(range.clone()) - .filter(move |pair| { - pair.open_range.start <= range.start && pair.close_range.end >= range.end - }) - { - let len = pair.close_range.end - pair.open_range.start; - - if let Some(existing) = &result { - let existing_len = existing.close_range.end - existing.open_range.start; - if len > existing_len { - continue; - } - } - - result = Some(pair); - } - - result - }; - let Some(pair) = pair else { - return false; - }; - pair.newline_only - && buffer - .chars_for_range(pair.open_range.end..range.start) - .chain(buffer.chars_for_range(range.end..pair.close_range.start)) - .all(|c| c.is_whitespace() && c != '\n') -} - -fn get_uncommitted_diff_for_buffer( - project: &Entity, - buffers: impl IntoIterator>, - buffer: Entity, - cx: &mut App, -) -> Task<()> { - let mut tasks = Vec::new(); - project.update(cx, |project, cx| { - for buffer in buffers { - if project::File::from_dyn(buffer.read(cx).file()).is_some() { - tasks.push(project.open_uncommitted_diff(buffer.clone(), cx)) - } - } - }); - cx.spawn(async move |cx| { - let diffs = future::join_all(tasks).await; - buffer - .update(cx, |buffer, cx| { - for diff in diffs.into_iter().flatten() { - buffer.add_diff(diff, cx); - } - }) - .ok(); - }) -} - -fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize { - let tab_size = tab_size.get() as usize; - let mut width = offset; - - for ch in text.chars() { - width += if ch == '\t' { - tab_size - (width % tab_size) - } else { - 1 - }; - } - - width - offset -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_string_size_with_expanded_tabs() { - let nz = |val| NonZeroU32::new(val).unwrap(); - assert_eq!(char_len_with_expanded_tabs(0, "", nz(4)), 0); - assert_eq!(char_len_with_expanded_tabs(0, "hello", nz(4)), 5); - assert_eq!(char_len_with_expanded_tabs(0, "\thello", nz(4)), 9); - assert_eq!(char_len_with_expanded_tabs(0, "abc\tab", nz(4)), 6); - assert_eq!(char_len_with_expanded_tabs(0, "hello\t", nz(4)), 8); - assert_eq!(char_len_with_expanded_tabs(0, "\t\t", nz(8)), 16); - assert_eq!(char_len_with_expanded_tabs(0, "x\t", nz(8)), 8); - assert_eq!(char_len_with_expanded_tabs(7, "x\t", nz(8)), 9); - } -} - -/// Tokenizes a string into runs of text that should stick together, or that is whitespace. -struct WordBreakingTokenizer<'a> { - input: &'a str, -} - -impl<'a> WordBreakingTokenizer<'a> { - fn new(input: &'a str) -> Self { - Self { input } - } -} - -fn is_char_ideographic(ch: char) -> bool { - use unicode_script::Script::*; - use unicode_script::UnicodeScript; - matches!(ch.script(), Han | Tangut | Yi) -} - -fn is_grapheme_ideographic(text: &str) -> bool { - text.chars().any(is_char_ideographic) -} - -fn is_grapheme_whitespace(text: &str) -> bool { - text.chars().any(|x| x.is_whitespace()) -} - -fn should_stay_with_preceding_ideograph(text: &str) -> bool { - text.chars().next().map_or(false, |ch| { - matches!(ch, '。' | '、' | ',' | '?' | '!' | ':' | ';' | '…') - }) -} - -#[derive(PartialEq, Eq, Debug, Clone, Copy)] -enum WordBreakToken<'a> { - Word { token: &'a str, grapheme_len: usize }, - InlineWhitespace { token: &'a str, grapheme_len: usize }, - Newline, -} - -impl<'a> Iterator for WordBreakingTokenizer<'a> { - /// Yields a span, the count of graphemes in the token, and whether it was - /// whitespace. Note that it also breaks at word boundaries. - type Item = WordBreakToken<'a>; - - fn next(&mut self) -> Option { - use unicode_segmentation::UnicodeSegmentation; - if self.input.is_empty() { - return None; - } - - let mut iter = self.input.graphemes(true).peekable(); - let mut offset = 0; - let mut grapheme_len = 0; - if let Some(first_grapheme) = iter.next() { - let is_newline = first_grapheme == "\n"; - let is_whitespace = is_grapheme_whitespace(first_grapheme); - offset += first_grapheme.len(); - grapheme_len += 1; - if is_grapheme_ideographic(first_grapheme) && !is_whitespace { - 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.map_or(false, |(i, _)| i == 0) { - next_word_bound = words.next(); - } - while let Some(grapheme) = iter.peek().copied() { - if next_word_bound.map_or(false, |(i, _)| i == offset) { - break; - }; - if is_grapheme_whitespace(grapheme) != is_whitespace - || (grapheme == "\n") != is_newline - { - break; - }; - offset += grapheme.len(); - grapheme_len += 1; - iter.next(); - } - } - let token = &self.input[..offset]; - self.input = &self.input[offset..]; - if token == "\n" { - Some(WordBreakToken::Newline) - } else if is_whitespace { - Some(WordBreakToken::InlineWhitespace { - token, - grapheme_len, - }) - } else { - Some(WordBreakToken::Word { - token, - grapheme_len, - }) - } - } else { - None - } - } -} - -#[test] -fn test_word_breaking_tokenizer() { - let tests: &[(&str, &[WordBreakToken<'static>])] = &[ - ("", &[]), - (" ", &[whitespace(" ", 2)]), - ("Ʒ", &[word("Ʒ", 1)]), - ("Ǽ", &[word("Ǽ", 1)]), - ("⋑", &[word("⋑", 1)]), - ("⋑⋑", &[word("⋑⋑", 2)]), - ( - "原理,进而", - &[word("原", 1), word("理,", 2), word("进", 1), word("而", 1)], - ), - ( - "hello world", - &[word("hello", 5), whitespace(" ", 1), word("world", 5)], - ), - ( - "hello, world", - &[word("hello,", 6), whitespace(" ", 1), word("world", 5)], - ), - ( - " hello world", - &[ - whitespace(" ", 2), - word("hello", 5), - whitespace(" ", 1), - word("world", 5), - ], - ), - ( - "这是什么 \n 钢笔", - &[ - word("这", 1), - word("是", 1), - word("什", 1), - word("么", 1), - whitespace(" ", 1), - newline(), - whitespace(" ", 1), - word("钢", 1), - word("笔", 1), - ], - ), - (" mutton", &[whitespace(" ", 1), word("mutton", 6)]), - ]; - - fn word(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> { - WordBreakToken::Word { - token, - grapheme_len, - } - } - - fn whitespace(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> { - WordBreakToken::InlineWhitespace { - token, - grapheme_len, - } - } - - fn newline() -> WordBreakToken<'static> { - WordBreakToken::Newline - } - - for (input, result) in tests { - assert_eq!( - WordBreakingTokenizer::new(input) - .collect::>() - .as_slice(), - *result, - ); - } -} - -fn wrap_with_prefix( - line_prefix: String, - unwrapped_text: String, - wrap_column: usize, - tab_size: NonZeroU32, - preserve_existing_whitespace: bool, -) -> String { - let line_prefix_len = char_len_with_expanded_tabs(0, &line_prefix, tab_size); - let mut wrapped_text = String::new(); - let mut current_line = line_prefix.clone(); - - let tokenizer = WordBreakingTokenizer::new(&unwrapped_text); - let mut current_line_len = line_prefix_len; - let mut in_whitespace = false; - for token in tokenizer { - let have_preceding_whitespace = in_whitespace; - match token { - WordBreakToken::Word { - token, - grapheme_len, - } => { - in_whitespace = false; - if current_line_len + grapheme_len > wrap_column - && current_line_len != line_prefix_len - { - wrapped_text.push_str(current_line.trim_end()); - wrapped_text.push('\n'); - current_line.truncate(line_prefix.len()); - current_line_len = line_prefix_len; - } - current_line.push_str(token); - current_line_len += grapheme_len; - } - WordBreakToken::InlineWhitespace { - mut token, - mut grapheme_len, - } => { - in_whitespace = true; - if have_preceding_whitespace && !preserve_existing_whitespace { - continue; - } - if !preserve_existing_whitespace { - token = " "; - grapheme_len = 1; - } - if current_line_len + grapheme_len > wrap_column { - wrapped_text.push_str(current_line.trim_end()); - wrapped_text.push('\n'); - current_line.truncate(line_prefix.len()); - current_line_len = line_prefix_len; - } else if current_line_len != line_prefix_len || preserve_existing_whitespace { - current_line.push_str(token); - current_line_len += grapheme_len; - } - } - WordBreakToken::Newline => { - in_whitespace = true; - if preserve_existing_whitespace { - wrapped_text.push_str(current_line.trim_end()); - wrapped_text.push('\n'); - current_line.truncate(line_prefix.len()); - current_line_len = line_prefix_len; - } else if have_preceding_whitespace { - continue; - } else if current_line_len + 1 > wrap_column && current_line_len != line_prefix_len - { - wrapped_text.push_str(current_line.trim_end()); - wrapped_text.push('\n'); - current_line.truncate(line_prefix.len()); - current_line_len = line_prefix_len; - } else if current_line_len != line_prefix_len { - current_line.push(' '); - current_line_len += 1; - } - } - } - } - - if !current_line.is_empty() { - wrapped_text.push_str(¤t_line); - } - wrapped_text -} - -#[test] -fn test_wrap_with_prefix() { - assert_eq!( - wrap_with_prefix( - "# ".to_string(), - "abcdefg".to_string(), - 4, - NonZeroU32::new(4).unwrap(), - false, - ), - "# abcdefg" - ); - assert_eq!( - wrap_with_prefix( - "".to_string(), - "\thello world".to_string(), - 8, - NonZeroU32::new(4).unwrap(), - false, - ), - "hello\nworld" - ); - assert_eq!( - wrap_with_prefix( - "// ".to_string(), - "xx \nyy zz aa bb cc".to_string(), - 12, - NonZeroU32::new(4).unwrap(), - false, - ), - "// xx yy zz\n// aa bb cc" - ); - assert_eq!( - wrap_with_prefix( - String::new(), - "这是什么 \n 钢笔".to_string(), - 3, - NonZeroU32::new(4).unwrap(), - false, - ), - "这是什\n么 钢\n笔" - ); -} - -pub trait CollaborationHub { - fn collaborators<'a>(&self, cx: &'a App) -> &'a HashMap; - fn user_participant_indices<'a>(&self, cx: &'a App) -> &'a HashMap; - fn user_names(&self, cx: &App) -> HashMap; -} - -impl CollaborationHub for Entity { - fn collaborators<'a>(&self, cx: &'a App) -> &'a HashMap { - self.read(cx).collaborators() - } - - fn user_participant_indices<'a>(&self, cx: &'a App) -> &'a HashMap { - self.read(cx).user_store().read(cx).participant_indices() - } - - fn user_names(&self, cx: &App) -> HashMap { - let this = self.read(cx); - let user_ids = this.collaborators().values().map(|c| c.user_id); - this.user_store().read_with(cx, |user_store, cx| { - user_store.participant_names(user_ids, cx) - }) - } -} - -pub trait SemanticsProvider { - fn hover( - &self, - buffer: &Entity, - position: text::Anchor, - cx: &mut App, - ) -> Option>>; - - fn inline_values( - &self, - buffer_handle: Entity, - range: Range, - cx: &mut App, - ) -> Option>>>; - - fn inlay_hints( - &self, - buffer_handle: Entity, - range: Range, - cx: &mut App, - ) -> Option>>>; - - fn resolve_inlay_hint( - &self, - hint: InlayHint, - buffer_handle: Entity, - server_id: LanguageServerId, - cx: &mut App, - ) -> Option>>; - - fn supports_inlay_hints(&self, buffer: &Entity, cx: &mut App) -> bool; - - fn document_highlights( - &self, - buffer: &Entity, - position: text::Anchor, - cx: &mut App, - ) -> Option>>>; - - fn definitions( - &self, - buffer: &Entity, - position: text::Anchor, - kind: GotoDefinitionKind, - cx: &mut App, - ) -> Option>>>; - - fn range_for_rename( - &self, - buffer: &Entity, - position: text::Anchor, - cx: &mut App, - ) -> Option>>>>; - - fn perform_rename( - &self, - buffer: &Entity, - position: text::Anchor, - new_name: String, - cx: &mut App, - ) -> Option>>; -} - -pub trait CompletionProvider { - fn completions( - &self, - excerpt_id: ExcerptId, - buffer: &Entity, - buffer_position: text::Anchor, - trigger: CompletionContext, - window: &mut Window, - cx: &mut Context, - ) -> Task>>>; - - fn resolve_completions( - &self, - buffer: Entity, - completion_indices: Vec, - completions: Rc>>, - cx: &mut Context, - ) -> Task>; - - fn apply_additional_edits_for_completion( - &self, - _buffer: Entity, - _completions: Rc>>, - _completion_index: usize, - _push_to_history: bool, - _cx: &mut Context, - ) -> Task>> { - Task::ready(Ok(None)) - } - - fn is_completion_trigger( - &self, - buffer: &Entity, - position: language::Anchor, - text: &str, - trigger_in_words: bool, - cx: &mut Context, - ) -> bool; - - fn sort_completions(&self) -> bool { - true - } - - fn filter_completions(&self) -> bool { - true - } -} - -pub trait CodeActionProvider { - fn id(&self) -> Arc; - - fn code_actions( - &self, - buffer: &Entity, - range: Range, - window: &mut Window, - cx: &mut App, - ) -> Task>>; - - fn apply_code_action( - &self, - buffer_handle: Entity, - action: CodeAction, - excerpt_id: ExcerptId, - push_to_history: bool, - window: &mut Window, - cx: &mut App, - ) -> Task>; -} - -impl CodeActionProvider for Entity { - fn id(&self) -> Arc { - "project".into() - } - - fn code_actions( - &self, - buffer: &Entity, - range: Range, - _window: &mut Window, - cx: &mut App, - ) -> Task>> { - self.update(cx, |project, cx| { - let code_lens = project.code_lens(buffer, range.clone(), cx); - let code_actions = project.code_actions(buffer, range, None, cx); - cx.background_spawn(async move { - let (code_lens, code_actions) = join(code_lens, code_actions).await; - Ok(code_lens - .context("code lens fetch")? - .into_iter() - .chain(code_actions.context("code action fetch")?) - .collect()) - }) - }) - } - - fn apply_code_action( - &self, - buffer_handle: Entity, - action: CodeAction, - _excerpt_id: ExcerptId, - push_to_history: bool, - _window: &mut Window, - cx: &mut App, - ) -> Task> { - self.update(cx, |project, cx| { - project.apply_code_action(buffer_handle, action, push_to_history, cx) - }) - } -} - -fn snippet_completions( - project: &Project, - buffer: &Entity, - buffer_position: text::Anchor, - cx: &mut App, -) -> Task>> { - let languages = buffer.read(cx).languages_at(buffer_position); - let snippet_store = project.snippets().read(cx); - - let scopes: Vec<_> = languages - .iter() - .filter_map(|language| { - let language_name = language.lsp_id(); - let snippets = snippet_store.snippets_for(Some(language_name), cx); - - if snippets.is_empty() { - None - } else { - Some((language.default_scope(), snippets)) - } - }) - .collect(); - - if scopes.is_empty() { - return Task::ready(Ok(vec![])); - } - - let snapshot = buffer.read(cx).text_snapshot(); - let chars: String = snapshot - .reversed_chars_for_range(text::Anchor::MIN..buffer_position) - .collect(); - let executor = cx.background_executor().clone(); - - cx.background_spawn(async move { - let mut all_results: Vec = Vec::new(); - for (scope, snippets) in scopes.into_iter() { - let classifier = CharClassifier::new(Some(scope)).for_completion(true); - let mut last_word = chars - .chars() - .take_while(|c| classifier.is_word(*c)) - .collect::(); - last_word = last_word.chars().rev().collect(); - - if last_word.is_empty() { - return Ok(vec![]); - } - - let as_offset = text::ToOffset::to_offset(&buffer_position, &snapshot); - let to_lsp = |point: &text::Anchor| { - let end = text::ToPointUtf16::to_point_utf16(point, &snapshot); - point_to_lsp(end) - }; - let lsp_end = to_lsp(&buffer_position); - - let candidates = snippets - .iter() - .enumerate() - .flat_map(|(ix, snippet)| { - snippet - .prefix - .iter() - .map(move |prefix| StringMatchCandidate::new(ix, &prefix)) - }) - .collect::>(); - - let mut matches = fuzzy::match_strings( - &candidates, - &last_word, - last_word.chars().any(|c| c.is_uppercase()), - 100, - &Default::default(), - executor.clone(), - ) - .await; - - // Remove all candidates where the query's start does not match the start of any word in the candidate - if let Some(query_start) = last_word.chars().next() { - matches.retain(|string_match| { - split_words(&string_match.string).any(|word| { - // Check that the first codepoint of the word as lowercase matches the first - // codepoint of the query as lowercase - word.chars() - .flat_map(|codepoint| codepoint.to_lowercase()) - .zip(query_start.to_lowercase()) - .all(|(word_cp, query_cp)| word_cp == query_cp) - }) - }); - } - - let matched_strings = matches - .into_iter() - .map(|m| m.string) - .collect::>(); - - let mut result: Vec = snippets - .iter() - .filter_map(|snippet| { - let matching_prefix = snippet - .prefix - .iter() - .find(|prefix| matched_strings.contains(*prefix))?; - let start = as_offset - last_word.len(); - let start = snapshot.anchor_before(start); - let range = start..buffer_position; - let lsp_start = to_lsp(&start); - let lsp_range = lsp::Range { - start: lsp_start, - end: lsp_end, - }; - Some(Completion { - replace_range: range, - new_text: snippet.body.clone(), - source: CompletionSource::Lsp { - insert_range: None, - server_id: LanguageServerId(usize::MAX), - resolved: true, - lsp_completion: Box::new(lsp::CompletionItem { - label: snippet.prefix.first().unwrap().clone(), - kind: Some(CompletionItemKind::SNIPPET), - label_details: snippet.description.as_ref().map(|description| { - lsp::CompletionItemLabelDetails { - detail: Some(description.clone()), - description: None, - } - }), - insert_text_format: Some(InsertTextFormat::SNIPPET), - text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace( - lsp::InsertReplaceEdit { - new_text: snippet.body.clone(), - insert: lsp_range, - replace: lsp_range, - }, - )), - filter_text: Some(snippet.body.clone()), - sort_text: Some(char::MAX.to_string()), - ..lsp::CompletionItem::default() - }), - lsp_defaults: None, - }, - label: CodeLabel { - text: matching_prefix.clone(), - runs: Vec::new(), - filter_range: 0..matching_prefix.len(), - }, - icon_path: None, - documentation: snippet.description.clone().map(|description| { - CompletionDocumentation::SingleLine(description.into()) - }), - insert_text_mode: None, - confirm: None, - }) - }) - .collect(); - - all_results.append(&mut result); - } - - Ok(all_results) - }) -} - -impl CompletionProvider for Entity { - fn completions( - &self, - _excerpt_id: ExcerptId, - buffer: &Entity, - buffer_position: text::Anchor, - options: CompletionContext, - _window: &mut Window, - cx: &mut Context, - ) -> Task>>> { - self.update(cx, |project, cx| { - let snippets = snippet_completions(project, buffer, buffer_position, cx); - let project_completions = project.completions(buffer, buffer_position, options, cx); - cx.background_spawn(async move { - let snippets_completions = snippets.await?; - match project_completions.await? { - Some(mut completions) => { - completions.extend(snippets_completions); - Ok(Some(completions)) - } - None => { - if snippets_completions.is_empty() { - Ok(None) - } else { - Ok(Some(snippets_completions)) - } - } - } - }) - }) - } - - fn resolve_completions( - &self, - buffer: Entity, - completion_indices: Vec, - completions: Rc>>, - cx: &mut Context, - ) -> Task> { - self.update(cx, |project, cx| { - project.lsp_store().update(cx, |lsp_store, cx| { - lsp_store.resolve_completions(buffer, completion_indices, completions, cx) - }) - }) - } - - fn apply_additional_edits_for_completion( - &self, - buffer: Entity, - completions: Rc>>, - completion_index: usize, - push_to_history: bool, - cx: &mut Context, - ) -> Task>> { - self.update(cx, |project, cx| { - project.lsp_store().update(cx, |lsp_store, cx| { - lsp_store.apply_additional_edits_for_completion( - buffer, - completions, - completion_index, - push_to_history, - cx, - ) - }) - }) - } - - fn is_completion_trigger( - &self, - buffer: &Entity, - position: language::Anchor, - text: &str, - trigger_in_words: bool, - cx: &mut Context, - ) -> bool { - let mut chars = text.chars(); - let char = if let Some(char) = chars.next() { - char - } else { - return false; - }; - if chars.next().is_some() { - return false; - } - - let buffer = buffer.read(cx); - let snapshot = buffer.snapshot(); - if !snapshot.settings_at(position, cx).show_completions_on_input { - return false; - } - let classifier = snapshot.char_classifier_at(position).for_completion(true); - if trigger_in_words && classifier.is_word(char) { - return true; - } - - buffer.completion_triggers().contains(text) - } -} - -impl SemanticsProvider for Entity { - fn hover( - &self, - buffer: &Entity, - position: text::Anchor, - cx: &mut App, - ) -> Option>> { - Some(self.update(cx, |project, cx| project.hover(buffer, position, cx))) - } - - fn document_highlights( - &self, - buffer: &Entity, - position: text::Anchor, - cx: &mut App, - ) -> Option>>> { - Some(self.update(cx, |project, cx| { - project.document_highlights(buffer, position, cx) - })) - } - - fn definitions( - &self, - buffer: &Entity, - position: text::Anchor, - kind: GotoDefinitionKind, - cx: &mut App, - ) -> Option>>> { - Some(self.update(cx, |project, cx| match kind { - GotoDefinitionKind::Symbol => project.definition(&buffer, position, cx), - GotoDefinitionKind::Declaration => project.declaration(&buffer, position, cx), - GotoDefinitionKind::Type => project.type_definition(&buffer, position, cx), - GotoDefinitionKind::Implementation => project.implementation(&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) - .is_some_and(|(session, _)| session.read(cx).any_stopped_thread()) - { - return true; - } - - buffer.update(cx, |buffer, cx| { - project.any_language_server_supports_inlay_hints(buffer, cx) - }) - }) - } - - fn inline_values( - &self, - buffer_handle: Entity, - range: Range, - cx: &mut App, - ) -> Option>>> { - self.update(cx, |project, cx| { - let (session, active_stack_frame) = project.active_debug_session(cx)?; - - Some(project.inline_values(session, active_stack_frame, buffer_handle, range, cx)) - }) - } - - fn inlay_hints( - &self, - buffer_handle: Entity, - range: Range, - cx: &mut App, - ) -> Option>>> { - Some(self.update(cx, |project, cx| { - project.inlay_hints(buffer_handle, range, cx) - })) - } - - fn resolve_inlay_hint( - &self, - hint: InlayHint, - buffer_handle: Entity, - server_id: LanguageServerId, - cx: &mut App, - ) -> Option>> { - Some(self.update(cx, |project, cx| { - project.resolve_inlay_hint(hint, buffer_handle, server_id, cx) - })) - } - - fn range_for_rename( - &self, - buffer: &Entity, - position: text::Anchor, - cx: &mut App, - ) -> Option>>>> { - Some(self.update(cx, |project, cx| { - let buffer = buffer.clone(); - let task = project.prepare_rename(buffer.clone(), position, cx); - cx.spawn(async move |_, cx| { - Ok(match task.await? { - PrepareRenameResponse::Success(range) => Some(range), - PrepareRenameResponse::InvalidPosition => None, - PrepareRenameResponse::OnlyUnpreparedRenameSupported => { - // Fallback on using TreeSitter info to determine identifier range - buffer.update(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - let (range, kind) = snapshot.surrounding_word(position); - if kind != Some(CharKind::Word) { - return None; - } - Some( - snapshot.anchor_before(range.start) - ..snapshot.anchor_after(range.end), - ) - })? - } - }) - }) - })) - } - - fn perform_rename( - &self, - buffer: &Entity, - position: text::Anchor, - new_name: String, - cx: &mut App, - ) -> Option>> { - Some(self.update(cx, |project, cx| { - project.perform_rename(buffer.clone(), position, new_name, cx) - })) - } -} - -fn inlay_hint_settings( - location: Anchor, - snapshot: &MultiBufferSnapshot, - cx: &mut Context, -) -> InlayHintSettings { - let file = snapshot.file_at(location); - let language = snapshot.language_at(location).map(|l| l.name()); - language_settings(language, file, cx).inlay_hints -} - -fn consume_contiguous_rows( - contiguous_row_selections: &mut Vec>, - selection: &Selection, - display_map: &DisplaySnapshot, - selections: &mut Peekable>>, -) -> (MultiBufferRow, MultiBufferRow) { - contiguous_row_selections.push(selection.clone()); - let start_row = MultiBufferRow(selection.start.row); - let mut end_row = ending_row(selection, display_map); - - while let Some(next_selection) = selections.peek() { - if next_selection.start.row <= end_row.0 { - end_row = ending_row(next_selection, display_map); - contiguous_row_selections.push(selections.next().unwrap().clone()); - } else { - break; - } - } - (start_row, end_row) -} - -fn ending_row(next_selection: &Selection, display_map: &DisplaySnapshot) -> MultiBufferRow { - if next_selection.end.column > 0 || next_selection.is_empty() { - MultiBufferRow(display_map.next_line_boundary(next_selection.end).0.row + 1) - } else { - MultiBufferRow(next_selection.end.row) - } -} - -impl EditorSnapshot { - pub fn remote_selections_in_range<'a>( - &'a self, - range: &'a Range, - collaboration_hub: &dyn CollaborationHub, - cx: &'a App, - ) -> impl 'a + Iterator { - let participant_names = collaboration_hub.user_names(cx); - let participant_indices = collaboration_hub.user_participant_indices(cx); - let collaborators_by_peer_id = collaboration_hub.collaborators(cx); - let collaborators_by_replica_id = collaborators_by_peer_id - .iter() - .map(|(_, collaborator)| (collaborator.replica_id, collaborator)) - .collect::>(); - self.buffer_snapshot - .selections_in_range(range, false) - .filter_map(move |(replica_id, line_mode, cursor_shape, selection)| { - let collaborator = collaborators_by_replica_id.get(&replica_id)?; - let participant_index = participant_indices.get(&collaborator.user_id).copied(); - let user_name = participant_names.get(&collaborator.user_id).cloned(); - Some(RemoteSelection { - replica_id, - selection, - cursor_shape, - line_mode, - participant_index, - peer_id: collaborator.peer_id, - user_name, - }) - }) - } - - pub fn hunks_for_ranges( - &self, - ranges: impl IntoIterator>, - ) -> Vec { - let mut hunks = Vec::new(); - let mut processed_buffer_rows: HashMap>> = - HashMap::default(); - for query_range in ranges { - let query_rows = - MultiBufferRow(query_range.start.row)..MultiBufferRow(query_range.end.row + 1); - for hunk in self.buffer_snapshot.diff_hunks_in_range( - Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0), - ) { - // Include deleted hunks that are adjacent to the query range, because - // otherwise they would be missed. - let mut intersects_range = hunk.row_range.overlaps(&query_rows); - if hunk.status().is_deleted() { - intersects_range |= hunk.row_range.start == query_rows.end; - intersects_range |= hunk.row_range.end == query_rows.start; - } - if intersects_range { - if !processed_buffer_rows - .entry(hunk.buffer_id) - .or_default() - .insert(hunk.buffer_range.start..hunk.buffer_range.end) - { - continue; - } - hunks.push(hunk); - } - } - } - - hunks - } - - fn display_diff_hunks_for_rows<'a>( - &'a self, - display_rows: Range, - folded_buffers: &'a HashSet, - ) -> impl 'a + Iterator { - let buffer_start = DisplayPoint::new(display_rows.start, 0).to_point(self); - let buffer_end = DisplayPoint::new(display_rows.end, 0).to_point(self); - - self.buffer_snapshot - .diff_hunks_in_range(buffer_start..buffer_end) - .filter_map(|hunk| { - if folded_buffers.contains(&hunk.buffer_id) { - return None; - } - - let hunk_start_point = Point::new(hunk.row_range.start.0, 0); - let hunk_end_point = Point::new(hunk.row_range.end.0, 0); - - let hunk_display_start = self.point_to_display_point(hunk_start_point, Bias::Left); - let hunk_display_end = self.point_to_display_point(hunk_end_point, Bias::Right); - - let display_hunk = if hunk_display_start.column() != 0 { - DisplayDiffHunk::Folded { - display_row: hunk_display_start.row(), - } - } else { - let mut end_row = hunk_display_end.row(); - if hunk_display_end.column() > 0 { - end_row.0 += 1; - } - let is_created_file = hunk.is_created_file(); - DisplayDiffHunk::Unfolded { - status: hunk.status(), - diff_base_byte_range: hunk.diff_base_byte_range, - display_row_range: hunk_display_start.row()..end_row, - multi_buffer_range: Anchor::range_in_buffer( - hunk.excerpt_id, - hunk.buffer_id, - hunk.buffer_range, - ), - is_created_file, - } - }; - - Some(display_hunk) - }) - } - - pub fn language_at(&self, position: T) -> Option<&Arc> { - self.display_snapshot.buffer_snapshot.language_at(position) - } - - pub fn is_focused(&self) -> bool { - self.is_focused - } - - pub fn placeholder_text(&self) -> Option<&Arc> { - self.placeholder_text.as_ref() - } - - pub fn scroll_position(&self) -> gpui::Point { - self.scroll_anchor.scroll_position(&self.display_snapshot) - } - - fn gutter_dimensions( - &self, - font_id: FontId, - font_size: Pixels, - max_line_number_width: Pixels, - cx: &App, - ) -> Option { - if !self.show_gutter { - return None; - } - - let descent = cx.text_system().descent(font_id, font_size); - let em_width = cx.text_system().em_width(font_id, font_size).log_err()?; - let em_advance = cx.text_system().em_advance(font_id, font_size).log_err()?; - - let show_git_gutter = self.show_git_diff_gutter.unwrap_or_else(|| { - matches!( - ProjectSettings::get_global(cx).git.git_gutter, - Some(GitGutterSetting::TrackedFiles) - ) - }); - let gutter_settings = EditorSettings::get_global(cx).gutter; - let show_line_numbers = self - .show_line_numbers - .unwrap_or(gutter_settings.line_numbers); - let line_gutter_width = if show_line_numbers { - // Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines. - let min_width_for_number_on_gutter = em_advance * MIN_LINE_NUMBER_DIGITS as f32; - max_line_number_width.max(min_width_for_number_on_gutter) - } else { - 0.0.into() - }; - - let show_code_actions = self - .show_code_actions - .unwrap_or(gutter_settings.code_actions); - - let show_runnables = self.show_runnables.unwrap_or(gutter_settings.runnables); - let show_breakpoints = self.show_breakpoints.unwrap_or(gutter_settings.breakpoints); - - let git_blame_entries_width = - self.git_blame_gutter_max_author_length - .map(|max_author_length| { - let renderer = cx.global::().0.clone(); - const MAX_RELATIVE_TIMESTAMP: &str = "60 minutes ago"; - - /// The number of characters to dedicate to gaps and margins. - const SPACING_WIDTH: usize = 4; - - let max_char_count = max_author_length.min(renderer.max_author_length()) - + ::git::SHORT_SHA_LENGTH - + MAX_RELATIVE_TIMESTAMP.len() - + SPACING_WIDTH; - - em_advance * max_char_count - }); - - let is_singleton = self.buffer_snapshot.is_singleton(); - - let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO); - left_padding += if !is_singleton { - em_width * 4.0 - } else if show_code_actions || show_runnables || show_breakpoints { - em_width * 3.0 - } else if show_git_gutter && show_line_numbers { - em_width * 2.0 - } else if show_git_gutter || show_line_numbers { - em_width - } else { - px(0.) - }; - - let shows_folds = is_singleton && gutter_settings.folds; - - let right_padding = if shows_folds && show_line_numbers { - em_width * 4.0 - } else if shows_folds || (!is_singleton && show_line_numbers) { - em_width * 3.0 - } else if show_line_numbers { - em_width - } else { - px(0.) - }; - - Some(GutterDimensions { - left_padding, - right_padding, - width: line_gutter_width + left_padding + right_padding, - margin: -descent, - git_blame_entries_width, - }) - } - - pub fn render_crease_toggle( - &self, - buffer_row: MultiBufferRow, - row_contains_cursor: bool, - editor: Entity, - window: &mut Window, - cx: &mut App, - ) -> Option { - let folded = self.is_line_folded(buffer_row); - let mut is_foldable = false; - - if let Some(crease) = self - .crease_snapshot - .query_row(buffer_row, &self.buffer_snapshot) - { - is_foldable = true; - match crease { - Crease::Inline { render_toggle, .. } | Crease::Block { render_toggle, .. } => { - if let Some(render_toggle) = render_toggle { - let toggle_callback = - Arc::new(move |folded, window: &mut Window, cx: &mut App| { - if folded { - editor.update(cx, |editor, cx| { - editor.fold_at(buffer_row, window, cx) - }); - } else { - editor.update(cx, |editor, cx| { - editor.unfold_at(buffer_row, window, cx) - }); - } - }); - return Some((render_toggle)( - buffer_row, - folded, - toggle_callback, - window, - cx, - )); - } - } - } - } - - is_foldable |= self.starts_indent(buffer_row); - - if folded || (is_foldable && (row_contains_cursor || self.gutter_hovered)) { - Some( - Disclosure::new(("gutter_crease", buffer_row.0), !folded) - .toggle_state(folded) - .on_click(window.listener_for(&editor, move |this, _e, window, cx| { - if folded { - this.unfold_at(buffer_row, window, cx); - } else { - this.fold_at(buffer_row, window, cx); - } - })) - .into_any_element(), - ) - } else { - None - } - } - - pub fn render_crease_trailer( - &self, - buffer_row: MultiBufferRow, - window: &mut Window, - cx: &mut App, - ) -> Option { - let folded = self.is_line_folded(buffer_row); - if let Crease::Inline { render_trailer, .. } = self - .crease_snapshot - .query_row(buffer_row, &self.buffer_snapshot)? - { - let render_trailer = render_trailer.as_ref()?; - Some(render_trailer(buffer_row, folded, window, cx)) - } else { - None - } - } -} - -impl Deref for EditorSnapshot { - type Target = DisplaySnapshot; - - fn deref(&self) -> &Self::Target { - &self.display_snapshot - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum EditorEvent { - InputIgnored { - text: Arc, - }, - InputHandled { - utf16_range_to_replace: Option>, - text: Arc, - }, - ExcerptsAdded { - buffer: Entity, - predecessor: ExcerptId, - excerpts: Vec<(ExcerptId, ExcerptRange)>, - }, - ExcerptsRemoved { - ids: Vec, - removed_buffer_ids: Vec, - }, - BufferFoldToggled { - ids: Vec, - folded: bool, - }, - ExcerptsEdited { - ids: Vec, - }, - ExcerptsExpanded { - ids: Vec, - }, - BufferEdited, - Edited { - transaction_id: clock::Lamport, - }, - Reparsed(BufferId), - Focused, - FocusedIn, - Blurred, - DirtyChanged, - Saved, - TitleChanged, - DiffBaseChanged, - SelectionsChanged { - local: bool, - }, - ScrollPositionChanged { - local: bool, - autoscroll: bool, - }, - Closed, - TransactionUndone { - transaction_id: clock::Lamport, - }, - TransactionBegun { - transaction_id: clock::Lamport, - }, - Reloaded, - CursorShapeChanged, - PushedToNavHistory { - anchor: Anchor, - is_deactivate: bool, - }, -} - -impl EventEmitter for Editor {} - -impl Focusable for Editor { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for Editor { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let settings = ThemeSettings::get_global(cx); - - let mut text_style = match self.mode { - 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(), - font_fallbacks: settings.ui_font.fallbacks.clone(), - font_size: rems(0.875).into(), - font_weight: settings.ui_font.weight, - line_height: relative(settings.buffer_line_height.value()), - ..Default::default() - }, - EditorMode::Full { .. } => TextStyle { - color: cx.theme().colors().editor_foreground, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: settings.buffer_font_size(cx).into(), - font_weight: settings.buffer_font.weight, - line_height: relative(settings.buffer_line_height.value()), - ..Default::default() - }, - }; - if let Some(text_style_refinement) = &self.text_style_refinement { - text_style.refine(text_style_refinement) - } - - let background = match self.mode { - EditorMode::SingleLine { .. } => cx.theme().system().transparent, - EditorMode::AutoHeight { max_lines: _ } => cx.theme().system().transparent, - EditorMode::Full { .. } => cx.theme().colors().editor_background, - }; - - EditorElement::new( - &cx.entity(), - EditorStyle { - background, - local_player: cx.theme().players().local(), - text: text_style, - scrollbar_width: EditorElement::SCROLLBAR_WIDTH, - syntax: cx.theme().syntax().clone(), - status: cx.theme().status().clone(), - inlay_hints_style: make_inlay_hints_style(cx), - inline_completion_styles: make_suggestion_styles(cx), - unnecessary_code_fade: ThemeSettings::get_global(cx).unnecessary_code_fade, - }, - ) - } -} - -impl EntityInputHandler for Editor { - fn text_for_range( - &mut self, - range_utf16: Range, - adjusted_range: &mut Option>, - _: &mut Window, - cx: &mut Context, - ) -> Option { - let snapshot = self.buffer.read(cx).read(cx); - let start = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.start), Bias::Left); - let end = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.end), Bias::Right); - if (start.0..end.0) != range_utf16 { - adjusted_range.replace(start.0..end.0); - } - Some(snapshot.text_for_range(start..end).collect()) - } - - fn selected_text_range( - &mut self, - ignore_disabled_input: bool, - _: &mut Window, - cx: &mut Context, - ) -> Option { - // Prevent the IME menu from appearing when holding down an alphabetic key - // while input is disabled. - if !ignore_disabled_input && !self.input_enabled { - return None; - } - - let selection = self.selections.newest::(cx); - let range = selection.range(); - - Some(UTF16Selection { - range: range.start.0..range.end.0, - reversed: selection.reversed, - }) - } - - fn marked_text_range(&self, _: &mut Window, cx: &mut Context) -> Option> { - let snapshot = self.buffer.read(cx).read(cx); - let range = self.text_highlights::(cx)?.1.first()?; - Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0) - } - - fn unmark_text(&mut self, _: &mut Window, cx: &mut Context) { - self.clear_highlights::(cx); - self.ime_transaction.take(); - } - - fn replace_text_in_range( - &mut self, - range_utf16: Option>, - text: &str, - window: &mut Window, - cx: &mut Context, - ) { - if !self.input_enabled { - cx.emit(EditorEvent::InputIgnored { text: text.into() }); - return; - } - - self.transact(window, cx, |this, window, cx| { - let new_selected_ranges = if let Some(range_utf16) = range_utf16 { - let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end); - Some(this.selection_replacement_ranges(range_utf16, cx)) - } else { - this.marked_text_ranges(cx) - }; - - let range_to_replace = new_selected_ranges.as_ref().and_then(|ranges_to_replace| { - let newest_selection_id = this.selections.newest_anchor().id; - this.selections - .all::(cx) - .iter() - .zip(ranges_to_replace.iter()) - .find_map(|(selection, range)| { - if selection.id == newest_selection_id { - Some( - (range.start.0 as isize - selection.head().0 as isize) - ..(range.end.0 as isize - selection.head().0 as isize), - ) - } else { - None - } - }) - }); - - cx.emit(EditorEvent::InputHandled { - utf16_range_to_replace: range_to_replace, - text: text.into(), - }); - - if let Some(new_selected_ranges) = new_selected_ranges { - this.change_selections(None, window, cx, |selections| { - selections.select_ranges(new_selected_ranges) - }); - this.backspace(&Default::default(), window, cx); - } - - this.handle_input(text, window, cx); - }); - - if let Some(transaction) = self.ime_transaction { - self.buffer.update(cx, |buffer, cx| { - buffer.group_until_transaction(transaction, cx); - }); - } - - self.unmark_text(window, cx); - } - - fn replace_and_mark_text_in_range( - &mut self, - range_utf16: Option>, - text: &str, - new_selected_range_utf16: Option>, - window: &mut Window, - cx: &mut Context, - ) { - if !self.input_enabled { - return; - } - - let transaction = self.transact(window, cx, |this, window, cx| { - let ranges_to_replace = if let Some(mut marked_ranges) = this.marked_text_ranges(cx) { - let snapshot = this.buffer.read(cx).read(cx); - if let Some(relative_range_utf16) = range_utf16.as_ref() { - for marked_range in &mut marked_ranges { - marked_range.end.0 = marked_range.start.0 + relative_range_utf16.end; - marked_range.start.0 += relative_range_utf16.start; - marked_range.start = - snapshot.clip_offset_utf16(marked_range.start, Bias::Left); - marked_range.end = - snapshot.clip_offset_utf16(marked_range.end, Bias::Right); - } - } - Some(marked_ranges) - } else if let Some(range_utf16) = range_utf16 { - let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end); - Some(this.selection_replacement_ranges(range_utf16, cx)) - } else { - None - }; - - let range_to_replace = ranges_to_replace.as_ref().and_then(|ranges_to_replace| { - let newest_selection_id = this.selections.newest_anchor().id; - this.selections - .all::(cx) - .iter() - .zip(ranges_to_replace.iter()) - .find_map(|(selection, range)| { - if selection.id == newest_selection_id { - Some( - (range.start.0 as isize - selection.head().0 as isize) - ..(range.end.0 as isize - selection.head().0 as isize), - ) - } else { - None - } - }) - }); - - cx.emit(EditorEvent::InputHandled { - utf16_range_to_replace: range_to_replace, - text: text.into(), - }); - - if let Some(ranges) = ranges_to_replace { - this.change_selections(None, window, cx, |s| s.select_ranges(ranges)); - } - - let marked_ranges = { - let snapshot = this.buffer.read(cx).read(cx); - this.selections - .disjoint_anchors() - .iter() - .map(|selection| { - selection.start.bias_left(&snapshot)..selection.end.bias_right(&snapshot) - }) - .collect::>() - }; - - if text.is_empty() { - this.unmark_text(window, cx); - } else { - this.highlight_text::( - marked_ranges.clone(), - HighlightStyle { - underline: Some(UnderlineStyle { - thickness: px(1.), - color: None, - wavy: false, - }), - ..Default::default() - }, - cx, - ); - } - - // Disable auto-closing when composing text (i.e. typing a `"` on a Brazilian keyboard) - let use_autoclose = this.use_autoclose; - let use_auto_surround = this.use_auto_surround; - this.set_use_autoclose(false); - this.set_use_auto_surround(false); - this.handle_input(text, window, cx); - this.set_use_autoclose(use_autoclose); - this.set_use_auto_surround(use_auto_surround); - - if let Some(new_selected_range) = new_selected_range_utf16 { - let snapshot = this.buffer.read(cx).read(cx); - let new_selected_ranges = marked_ranges - .into_iter() - .map(|marked_range| { - let insertion_start = marked_range.start.to_offset_utf16(&snapshot).0; - let new_start = OffsetUtf16(new_selected_range.start + insertion_start); - let new_end = OffsetUtf16(new_selected_range.end + insertion_start); - snapshot.clip_offset_utf16(new_start, Bias::Left) - ..snapshot.clip_offset_utf16(new_end, Bias::Right) - }) - .collect::>(); - - drop(snapshot); - this.change_selections(None, window, cx, |selections| { - selections.select_ranges(new_selected_ranges) - }); - } - }); - - self.ime_transaction = self.ime_transaction.or(transaction); - if let Some(transaction) = self.ime_transaction { - self.buffer.update(cx, |buffer, cx| { - buffer.group_until_transaction(transaction, cx); - }); - } - - if self.text_highlights::(cx).is_none() { - self.ime_transaction.take(); - } - } - - fn bounds_for_range( - &mut self, - range_utf16: Range, - element_bounds: gpui::Bounds, - window: &mut Window, - cx: &mut Context, - ) -> Option> { - let text_layout_details = self.text_layout_details(window); - let gpui::Size { - width: em_width, - height: line_height, - } = self.character_size(window); - - let snapshot = self.snapshot(window, cx); - let scroll_position = snapshot.scroll_position(); - let scroll_left = scroll_position.x * em_width; - - let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot); - let x = snapshot.x_for_display_point(start, &text_layout_details) - scroll_left - + self.gutter_dimensions.width - + self.gutter_dimensions.margin; - let y = line_height * (start.row().as_f32() - scroll_position.y); - - Some(Bounds { - origin: element_bounds.origin + point(x, y), - size: size(em_width, line_height), - }) - } - - fn character_index_for_point( - &mut self, - point: gpui::Point, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - let position_map = self.last_position_map.as_ref()?; - if !position_map.text_hitbox.contains(&point) { - return None; - } - let display_point = position_map.point_for_position(point).previous_valid; - let anchor = position_map - .snapshot - .display_point_to_anchor(display_point, Bias::Left); - let utf16_offset = anchor.to_offset_utf16(&position_map.snapshot.buffer_snapshot); - Some(utf16_offset.0) - } -} - -trait SelectionExt { - fn display_range(&self, map: &DisplaySnapshot) -> Range; - fn spanned_rows( - &self, - include_end_if_at_line_start: bool, - map: &DisplaySnapshot, - ) -> Range; -} - -impl SelectionExt for Selection { - fn display_range(&self, map: &DisplaySnapshot) -> Range { - let start = self - .start - .to_point(&map.buffer_snapshot) - .to_display_point(map); - let end = self - .end - .to_point(&map.buffer_snapshot) - .to_display_point(map); - if self.reversed { - end..start - } else { - start..end - } - } - - fn spanned_rows( - &self, - include_end_if_at_line_start: bool, - map: &DisplaySnapshot, - ) -> Range { - let start = self.start.to_point(&map.buffer_snapshot); - let mut end = self.end.to_point(&map.buffer_snapshot); - if !include_end_if_at_line_start && start.row != end.row && end.column == 0 { - end.row -= 1; - } - - let buffer_start = map.prev_line_boundary(start).0; - let buffer_end = map.next_line_boundary(end).0; - MultiBufferRow(buffer_start.row)..MultiBufferRow(buffer_end.row + 1) - } -} - -impl InvalidationStack { - fn invalidate(&mut self, selections: &[Selection], buffer: &MultiBufferSnapshot) - where - S: Clone + ToOffset, - { - while let Some(region) = self.last() { - let all_selections_inside_invalidation_ranges = - if selections.len() == region.ranges().len() { - selections - .iter() - .zip(region.ranges().iter().map(|r| r.to_offset(buffer))) - .all(|(selection, invalidation_range)| { - let head = selection.head().to_offset(buffer); - invalidation_range.start <= head && invalidation_range.end >= head - }) - } else { - false - }; - - if all_selections_inside_invalidation_ranges { - break; - } else { - self.pop(); - } - } - } -} - -impl Default for InvalidationStack { - fn default() -> Self { - Self(Default::default()) - } -} - -impl Deref for InvalidationStack { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for InvalidationStack { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl InvalidationRegion for SnippetState { - fn ranges(&self) -> &[Range] { - &self.ranges[self.active_index] - } -} - -fn inline_completion_edit_text( - current_snapshot: &BufferSnapshot, - edits: &[(Range, String)], - edit_preview: &EditPreview, - include_deletions: bool, - cx: &App, -) -> HighlightedText { - let edits = edits - .iter() - .map(|(anchor, text)| { - ( - anchor.start.text_anchor..anchor.end.text_anchor, - text.clone(), - ) - }) - .collect::>(); - - edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx) -} - -pub fn diagnostic_style(severity: DiagnosticSeverity, colors: &StatusColors) -> Hsla { - match severity { - DiagnosticSeverity::ERROR => colors.error, - DiagnosticSeverity::WARNING => colors.warning, - DiagnosticSeverity::INFORMATION => colors.info, - DiagnosticSeverity::HINT => colors.info, - _ => colors.ignored, - } -} - -pub fn styled_runs_for_code_label<'a>( - label: &'a CodeLabel, - syntax_theme: &'a theme::SyntaxTheme, -) -> impl 'a + Iterator, HighlightStyle)> { - let fade_out = HighlightStyle { - fade_out: Some(0.35), - ..Default::default() - }; - - let mut prev_end = label.filter_range.end; - label - .runs - .iter() - .enumerate() - .flat_map(move |(ix, (range, highlight_id))| { - let style = if let Some(style) = highlight_id.style(syntax_theme) { - style - } else { - return Default::default(); - }; - let mut muted_style = style; - muted_style.highlight(fade_out); - - let mut runs = SmallVec::<[(Range, HighlightStyle); 3]>::new(); - if range.start >= label.filter_range.end { - if range.start > prev_end { - runs.push((prev_end..range.start, fade_out)); - } - runs.push((range.clone(), muted_style)); - } else if range.end <= label.filter_range.end { - runs.push((range.clone(), style)); - } else { - runs.push((range.start..label.filter_range.end, style)); - runs.push((label.filter_range.end..range.end, muted_style)); - } - prev_end = cmp::max(prev_end, range.end); - - if ix + 1 == label.runs.len() && label.text.len() > prev_end { - runs.push((prev_end..label.text.len(), fade_out)); - } - - runs - }) -} - -pub(crate) fn split_words(text: &str) -> impl std::iter::Iterator + '_ { - let mut prev_index = 0; - let mut prev_codepoint: Option = None; - text.char_indices() - .chain([(text.len(), '\0')]) - .filter_map(move |(index, codepoint)| { - let prev_codepoint = prev_codepoint.replace(codepoint)?; - let is_boundary = index == text.len() - || !prev_codepoint.is_uppercase() && codepoint.is_uppercase() - || !prev_codepoint.is_alphanumeric() && codepoint.is_alphanumeric(); - if is_boundary { - let chunk = &text[prev_index..index]; - prev_index = index; - Some(chunk) - } else { - None - } - }) -} - -pub trait RangeToAnchorExt: Sized { - fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range; - - fn to_display_points(self, snapshot: &EditorSnapshot) -> Range { - let anchor_range = self.to_anchors(&snapshot.buffer_snapshot); - anchor_range.start.to_display_point(snapshot)..anchor_range.end.to_display_point(snapshot) - } -} - -impl RangeToAnchorExt for Range { - fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range { - let start_offset = self.start.to_offset(snapshot); - let end_offset = self.end.to_offset(snapshot); - if start_offset == end_offset { - snapshot.anchor_before(start_offset)..snapshot.anchor_before(end_offset) - } else { - snapshot.anchor_after(self.start)..snapshot.anchor_before(self.end) - } - } -} - -pub trait RowExt { - fn as_f32(&self) -> f32; - - fn next_row(&self) -> Self; - - fn previous_row(&self) -> Self; - - fn minus(&self, other: Self) -> u32; -} - -impl RowExt for DisplayRow { - fn as_f32(&self) -> f32 { - self.0 as f32 - } - - fn next_row(&self) -> Self { - Self(self.0 + 1) - } - - fn previous_row(&self) -> Self { - Self(self.0.saturating_sub(1)) - } - - fn minus(&self, other: Self) -> u32 { - self.0 - other.0 - } -} - -impl RowExt for MultiBufferRow { - fn as_f32(&self) -> f32 { - self.0 as f32 - } - - fn next_row(&self) -> Self { - Self(self.0 + 1) - } - - fn previous_row(&self) -> Self { - Self(self.0.saturating_sub(1)) - } - - fn minus(&self, other: Self) -> u32 { - self.0 - other.0 - } -} - -trait RowRangeExt { - type Row; - - fn len(&self) -> usize; - - fn iter_rows(&self) -> impl DoubleEndedIterator; -} - -impl RowRangeExt for Range { - type Row = MultiBufferRow; - - fn len(&self) -> usize { - (self.end.0 - self.start.0) as usize - } - - fn iter_rows(&self) -> impl DoubleEndedIterator { - (self.start.0..self.end.0).map(MultiBufferRow) - } -} - -impl RowRangeExt for Range { - type Row = DisplayRow; - - fn len(&self) -> usize { - (self.end.0 - self.start.0) as usize - } - - fn iter_rows(&self) -> impl DoubleEndedIterator { - (self.start.0..self.end.0).map(DisplayRow) - } -} - -/// If select range has more than one line, we -/// just point the cursor to range.start. -fn collapse_multiline_range(range: Range) -> Range { - if range.start.row == range.end.row { - range - } else { - range.start..range.start - } -} -pub struct KillRing(ClipboardItem); -impl Global for KillRing {} - -const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); - -enum BreakpointPromptEditAction { - Log, - Condition, - HitCondition, -} - -struct BreakpointPromptEditor { - pub(crate) prompt: Entity, - editor: WeakEntity, - breakpoint_anchor: Anchor, - breakpoint: Breakpoint, - edit_action: BreakpointPromptEditAction, - block_ids: HashSet, - gutter_dimensions: Arc>, - _subscriptions: Vec, -} - -impl BreakpointPromptEditor { - const MAX_LINES: u8 = 4; - - fn new( - editor: WeakEntity, - breakpoint_anchor: Anchor, - breakpoint: Breakpoint, - edit_action: BreakpointPromptEditAction, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let base_text = match edit_action { - BreakpointPromptEditAction::Log => breakpoint.message.as_ref(), - BreakpointPromptEditAction::Condition => breakpoint.condition.as_ref(), - BreakpointPromptEditAction::HitCondition => breakpoint.hit_condition.as_ref(), - } - .map(|msg| msg.to_string()) - .unwrap_or_default(); - - let buffer = cx.new(|cx| Buffer::local(base_text, cx)); - let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - - let prompt = cx.new(|cx| { - let mut prompt = Editor::new( - EditorMode::AutoHeight { - max_lines: Self::MAX_LINES as usize, - }, - buffer, - None, - window, - cx, - ); - prompt.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); - prompt.set_show_cursor_when_unfocused(false, cx); - prompt.set_placeholder_text( - match edit_action { - BreakpointPromptEditAction::Log => "Message to log when a breakpoint is hit. Expressions within {} are interpolated.", - BreakpointPromptEditAction::Condition => "Condition when a breakpoint is hit. Expressions within {} are interpolated.", - BreakpointPromptEditAction::HitCondition => "How many breakpoint hits to ignore", - }, - cx, - ); - - prompt - }); - - Self { - prompt, - editor, - breakpoint_anchor, - breakpoint, - edit_action, - gutter_dimensions: Arc::new(Mutex::new(GutterDimensions::default())), - block_ids: Default::default(), - _subscriptions: vec![], - } - } - - pub(crate) fn add_block_ids(&mut self, block_ids: Vec) { - self.block_ids.extend(block_ids) - } - - fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { - if let Some(editor) = self.editor.upgrade() { - let message = self - .prompt - .read(cx) - .buffer - .read(cx) - .as_singleton() - .expect("A multi buffer in breakpoint prompt isn't possible") - .read(cx) - .as_rope() - .to_string(); - - editor.update(cx, |editor, cx| { - editor.edit_breakpoint_at_anchor( - self.breakpoint_anchor, - self.breakpoint.clone(), - match self.edit_action { - BreakpointPromptEditAction::Log => { - BreakpointEditAction::EditLogMessage(message.into()) - } - BreakpointPromptEditAction::Condition => { - BreakpointEditAction::EditCondition(message.into()) - } - BreakpointPromptEditAction::HitCondition => { - BreakpointEditAction::EditHitCondition(message.into()) - } - }, - cx, - ); - - editor.remove_blocks(self.block_ids.clone(), None, cx); - cx.focus_self(window); - }); - } - } - - fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context) { - self.editor - .update(cx, |editor, cx| { - editor.remove_blocks(self.block_ids.clone(), None, cx); - window.focus(&editor.focus_handle); - }) - .log_err(); - } - - fn render_prompt_editor(&self, cx: &mut Context) -> impl IntoElement { - let settings = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color: if self.prompt.read(cx).read_only(cx) { - cx.theme().colors().text_disabled - } else { - cx.theme().colors().text - }, - font_family: settings.buffer_font.family.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: settings.buffer_font_size(cx).into(), - font_weight: settings.buffer_font.weight, - line_height: relative(settings.buffer_line_height.value()), - ..Default::default() - }; - EditorElement::new( - &self.prompt, - EditorStyle { - background: cx.theme().colors().editor_background, - local_player: cx.theme().players().local(), - text: text_style, - ..Default::default() - }, - ) - } -} - -impl Render for BreakpointPromptEditor { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let gutter_dimensions = *self.gutter_dimensions.lock(); - h_flex() - .key_context("Editor") - .bg(cx.theme().colors().editor_background) - .border_y_1() - .border_color(cx.theme().status().info_border) - .size_full() - .py(window.line_height() / 2.5) - .on_action(cx.listener(Self::confirm)) - .on_action(cx.listener(Self::cancel)) - .child(h_flex().w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))) - .child(div().flex_1().child(self.render_prompt_editor(cx))) - } -} - -impl Focusable for BreakpointPromptEditor { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.prompt.focus_handle(cx) - } -} - -fn all_edits_insertions_or_deletions( - edits: &Vec<(Range, String)>, - snapshot: &MultiBufferSnapshot, -) -> bool { - let mut all_insertions = true; - let mut all_deletions = true; - - for (range, new_text) in edits.iter() { - 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 { - if range_is_empty { - all_deletions = false; - } else { - all_insertions = false; - } - } else { - return false; - } - - if !all_insertions && !all_deletions { - return false; - } - } - all_insertions || all_deletions -} - -struct MissingEditPredictionKeybindingTooltip; - -impl Render for MissingEditPredictionKeybindingTooltip { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - ui::tooltip_container(window, cx, |container, _, cx| { - container - .flex_shrink_0() - .max_w_80() - .min_h(rems_from_px(124.)) - .justify_between() - .child( - v_flex() - .flex_1() - .text_ui_sm(cx) - .child(Label::new("Conflict with Accept Keybinding")) - .child("Your keymap currently overrides the default accept keybinding. To continue, assign one keybinding for the `editor::AcceptEditPrediction` action.") - ) - .child( - h_flex() - .pb_1() - .gap_1() - .items_end() - .w_full() - .child(Button::new("open-keymap", "Assign Keybinding").size(ButtonSize::Compact).on_click(|_ev, window, cx| { - window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx) - })) - .child(Button::new("see-docs", "See Docs").size(ButtonSize::Compact).on_click(|_ev, _window, cx| { - cx.open_url("https://zed.dev/docs/completions#edit-predictions-missing-keybinding"); - })), - ) - }) - } -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct LineHighlight { - pub background: Background, - pub border: Option, - pub include_gutter: bool, - pub type_id: Option, -} - -fn render_diff_hunk_controls( - row: u32, - status: &DiffHunkStatus, - hunk_range: Range, - is_created_file: bool, - line_height: Pixels, - editor: &Entity, - _window: &mut Window, - cx: &mut App, -) -> AnyElement { - h_flex() - .h(line_height) - .mr_1() - .gap_1() - .px_0p5() - .pb_1() - .border_x_1() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .rounded_b_lg() - .bg(cx.theme().colors().editor_background) - .gap_1() - .occlude() - .shadow_md() - .child(if status.has_secondary_hunk() { - Button::new(("stage", row as u64), "Stage") - .alpha(if status.is_pending() { 0.66 } else { 1.0 }) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Stage Hunk", - &::git::ToggleStaged, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - move |_event, _window, cx| { - editor.update(cx, |editor, cx| { - editor.stage_or_unstage_diff_hunks( - true, - vec![hunk_range.start..hunk_range.start], - cx, - ); - }); - } - }) - } else { - Button::new(("unstage", row as u64), "Unstage") - .alpha(if status.is_pending() { 0.66 } else { 1.0 }) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Unstage Hunk", - &::git::ToggleStaged, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - move |_event, _window, cx| { - editor.update(cx, |editor, cx| { - editor.stage_or_unstage_diff_hunks( - false, - vec![hunk_range.start..hunk_range.start], - cx, - ); - }); - } - }) - }) - .child( - Button::new(("restore", row as u64), "Restore") - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Restore Hunk", - &::git::Restore, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - move |_event, window, cx| { - editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(window, cx); - let point = hunk_range.start.to_point(&snapshot.buffer_snapshot); - editor.restore_hunks_in_ranges(vec![point..point], window, cx); - }); - } - }) - .disabled(is_created_file), - ) - .when( - !editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(), - |el| { - el.child( - IconButton::new(("next-hunk", row as u64), IconName::ArrowDown) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - // .disabled(!has_multiple_hunks) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Next Hunk", - &GoToHunk, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - move |_event, window, cx| { - editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(window, cx); - let position = - hunk_range.end.to_point(&snapshot.buffer_snapshot); - editor.go_to_hunk_before_or_after_position( - &snapshot, - position, - Direction::Next, - window, - cx, - ); - editor.expand_selected_diff_hunks(cx); - }); - } - }), - ) - .child( - IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - // .disabled(!has_multiple_hunks) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Previous Hunk", - &GoToPreviousHunk, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - move |_event, window, cx| { - editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(window, cx); - let point = - hunk_range.start.to_point(&snapshot.buffer_snapshot); - editor.go_to_hunk_before_or_after_position( - &snapshot, - point, - Direction::Prev, - window, - cx, - ); - editor.expand_selected_diff_hunks(cx); - }); - } - }), - ) - }, - ) - .into_any_element() -} diff --git a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-01.diff b/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-01.diff deleted file mode 100644 index 1a38a1967f..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-01.diff +++ /dev/null @@ -1,28 +0,0 @@ ---- before.rs 2025-07-07 11:37:48.434629001 +0300 -+++ expected.rs 2025-07-14 10:33:53.346906775 +0300 -@@ -1780,11 +1780,11 @@ - cx.observe_window_activation(window, |editor, window, cx| { - let active = window.is_window_active(); - editor.blink_manager.update(cx, |blink_manager, cx| { -- if active { -- blink_manager.enable(cx); -- } else { -- blink_manager.disable(cx); -- } -+ // if active { -+ // blink_manager.enable(cx); -+ // } else { -+ // blink_manager.disable(cx); -+ // } - }); - }), - ], -@@ -18463,7 +18463,7 @@ - } - - self.blink_manager.update(cx, |blink_manager, cx| { -- blink_manager.enable(cx); -+ // blink_manager.enable(cx); - }); - self.show_cursor_names(window, cx); - self.buffer.update(cx, |buffer, cx| { diff --git a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-02.diff b/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-02.diff deleted file mode 100644 index b484cce48f..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-02.diff +++ /dev/null @@ -1,29 +0,0 @@ -@@ -1778,13 +1778,13 @@ - cx.observe_global_in::(window, Self::settings_changed), - observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()), - cx.observe_window_activation(window, |editor, window, cx| { -- let active = window.is_window_active(); -+ // let active = window.is_window_active(); - editor.blink_manager.update(cx, |blink_manager, cx| { -- if active { -- blink_manager.enable(cx); -- } else { -- blink_manager.disable(cx); -- } -+ // if active { -+ // blink_manager.enable(cx); -+ // } else { -+ // blink_manager.disable(cx); -+ // } - }); - }), - ], -@@ -18463,7 +18463,7 @@ - } - - self.blink_manager.update(cx, |blink_manager, cx| { -- blink_manager.enable(cx); -+ // blink_manager.enable(cx); - }); - self.show_cursor_names(window, cx); - self.buffer.update(cx, |buffer, cx| { diff --git a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-03.diff b/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-03.diff deleted file mode 100644 index 431e34e48a..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-03.diff +++ /dev/null @@ -1,34 +0,0 @@ -@@ -1774,17 +1774,17 @@ - cx.observe(&buffer, Self::on_buffer_changed), - cx.subscribe_in(&buffer, window, Self::on_buffer_event), - cx.observe_in(&display_map, window, Self::on_display_map_changed), -- cx.observe(&blink_manager, |_, _, cx| cx.notify()), -+ // cx.observe(&blink_manager, |_, _, cx| cx.notify()), - cx.observe_global_in::(window, Self::settings_changed), - observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()), - cx.observe_window_activation(window, |editor, window, cx| { -- let active = window.is_window_active(); -+ // let active = window.is_window_active(); - editor.blink_manager.update(cx, |blink_manager, cx| { -- if active { -- blink_manager.enable(cx); -- } else { -- blink_manager.disable(cx); -- } -+ // if active { -+ // blink_manager.enable(cx); -+ // } else { -+ // blink_manager.disable(cx); -+ // } - }); - }), - ], -@@ -18463,7 +18463,7 @@ - } - - self.blink_manager.update(cx, |blink_manager, cx| { -- blink_manager.enable(cx); -+ // blink_manager.enable(cx); - }); - self.show_cursor_names(window, cx); - self.buffer.update(cx, |buffer, cx| { diff --git a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-04.diff b/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-04.diff deleted file mode 100644 index 64a6b85dd3..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-04.diff +++ /dev/null @@ -1,33 +0,0 @@ -@@ -1774,17 +1774,17 @@ - cx.observe(&buffer, Self::on_buffer_changed), - cx.subscribe_in(&buffer, window, Self::on_buffer_event), - cx.observe_in(&display_map, window, Self::on_display_map_changed), -- cx.observe(&blink_manager, |_, _, cx| cx.notify()), -+ // cx.observe(&blink_manager, |_, _, cx| cx.notify()), - cx.observe_global_in::(window, Self::settings_changed), - observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()), - cx.observe_window_activation(window, |editor, window, cx| { - let active = window.is_window_active(); - editor.blink_manager.update(cx, |blink_manager, cx| { -- if active { -- blink_manager.enable(cx); -- } else { -- blink_manager.disable(cx); -- } -+ // if active { -+ // blink_manager.enable(cx); -+ // } else { -+ // blink_manager.disable(cx); -+ // } - }); - }), - ], -@@ -18463,7 +18463,7 @@ - } - - self.blink_manager.update(cx, |blink_manager, cx| { -- blink_manager.enable(cx); -+ // blink_manager.enable(cx); - }); - self.show_cursor_names(window, cx); - self.buffer.update(cx, |buffer, cx| { diff --git a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/before.rs b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/before.rs deleted file mode 100644 index 36fccb5132..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/before.rs +++ /dev/null @@ -1,371 +0,0 @@ -use crate::commit::get_messages; -use crate::{GitRemote, Oid}; -use anyhow::{Context as _, Result, anyhow}; -use collections::{HashMap, HashSet}; -use futures::AsyncWriteExt; -use gpui::SharedString; -use serde::{Deserialize, Serialize}; -use std::process::Stdio; -use std::{ops::Range, path::Path}; -use text::Rope; -use time::OffsetDateTime; -use time::UtcOffset; -use time::macros::format_description; - -pub use git2 as libgit; - -#[derive(Debug, Clone, Default)] -pub struct Blame { - pub entries: Vec, - pub messages: HashMap, - pub remote_url: Option, -} - -#[derive(Clone, Debug, Default)] -pub struct ParsedCommitMessage { - pub message: SharedString, - pub permalink: Option, - pub pull_request: Option, - pub remote: Option, -} - -impl Blame { - pub async fn for_path( - git_binary: &Path, - working_directory: &Path, - path: &Path, - content: &Rope, - remote_url: Option, - ) -> Result { - let output = run_git_blame(git_binary, working_directory, path, content).await?; - let mut entries = parse_git_blame(&output)?; - entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start)); - - let mut unique_shas = HashSet::default(); - - for entry in entries.iter_mut() { - unique_shas.insert(entry.sha); - } - - let shas = unique_shas.into_iter().collect::>(); - let messages = get_messages(working_directory, &shas) - .await - .context("failed to get commit messages")?; - - Ok(Self { - entries, - messages, - remote_url, - }) - } -} - -const GIT_BLAME_NO_COMMIT_ERROR: &str = "fatal: no such ref: HEAD"; -const GIT_BLAME_NO_PATH: &str = "fatal: no such path"; - -async fn run_git_blame( - git_binary: &Path, - working_directory: &Path, - path: &Path, - contents: &Rope, -) -> Result { - let mut child = util::command::new_smol_command(git_binary) - .current_dir(working_directory) - .arg("blame") - .arg("--incremental") - .arg("--contents") - .arg("-") - .arg(path.as_os_str()) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .context("starting git blame process")?; - - let stdin = child - .stdin - .as_mut() - .context("failed to get pipe to stdin of git blame command")?; - - for chunk in contents.chunks() { - stdin.write_all(chunk.as_bytes()).await?; - } - stdin.flush().await?; - - let output = child.output().await.context("reading git blame output")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - let trimmed = stderr.trim(); - if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { - return Ok(String::new()); - } - anyhow::bail!("git blame process failed: {stderr}"); - } - - Ok(String::from_utf8(output.stdout)?) -} - -#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] -pub struct BlameEntry { - pub sha: Oid, - - pub range: Range, - - pub original_line_number: u32, - - pub author: Option, - pub author_mail: Option, - pub author_time: Option, - pub author_tz: Option, - - pub committer_name: Option, - pub committer_email: Option, - pub committer_time: Option, - pub committer_tz: Option, - - pub summary: Option, - - pub previous: Option, - pub filename: String, -} - -impl BlameEntry { - // Returns a BlameEntry by parsing the first line of a `git blame --incremental` - // entry. The line MUST have this format: - // - // <40-byte-hex-sha1> - fn new_from_blame_line(line: &str) -> Result { - let mut parts = line.split_whitespace(); - - let sha = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing sha from {line}"))?; - - let original_line_number = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing original line number from {line}"))?; - let final_line_number = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing final line number from {line}"))?; - - let line_count = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing line count from {line}"))?; - - let start_line = final_line_number.saturating_sub(1); - let end_line = start_line + line_count; - let range = start_line..end_line; - - Ok(Self { - sha, - range, - original_line_number, - ..Default::default() - }) - } - - pub fn author_offset_date_time(&self) -> Result { - if let (Some(author_time), Some(author_tz)) = (self.author_time, &self.author_tz) { - let format = format_description!("[offset_hour][offset_minute]"); - let offset = UtcOffset::parse(author_tz, &format)?; - let date_time_utc = OffsetDateTime::from_unix_timestamp(author_time)?; - - Ok(date_time_utc.to_offset(offset)) - } else { - // Directly return current time in UTC if there's no committer time or timezone - Ok(time::OffsetDateTime::now_utc()) - } - } -} - -// parse_git_blame parses the output of `git blame --incremental`, which returns -// all the blame-entries for a given path incrementally, as it finds them. -// -// Each entry *always* starts with: -// -// <40-byte-hex-sha1> -// -// Each entry *always* ends with: -// -// filename -// -// Line numbers are 1-indexed. -// -// A `git blame --incremental` entry looks like this: -// -// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 2 2 1 -// author Joe Schmoe -// author-mail -// author-time 1709741400 -// author-tz +0100 -// committer Joe Schmoe -// committer-mail -// committer-time 1709741400 -// committer-tz +0100 -// summary Joe's cool commit -// previous 486c2409237a2c627230589e567024a96751d475 index.js -// filename index.js -// -// If the entry has the same SHA as an entry that was already printed then no -// signature information is printed: -// -// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 3 4 1 -// previous 486c2409237a2c627230589e567024a96751d475 index.js -// filename index.js -// -// More about `--incremental` output: https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-blame.html -fn parse_git_blame(output: &str) -> Result> { - let mut entries: Vec = Vec::new(); - let mut index: HashMap = HashMap::default(); - - let mut current_entry: Option = None; - - for line in output.lines() { - let mut done = false; - - match &mut current_entry { - None => { - let mut new_entry = BlameEntry::new_from_blame_line(line)?; - - if let Some(existing_entry) = index - .get(&new_entry.sha) - .and_then(|slot| entries.get(*slot)) - { - new_entry.author.clone_from(&existing_entry.author); - new_entry - .author_mail - .clone_from(&existing_entry.author_mail); - new_entry.author_time = existing_entry.author_time; - new_entry.author_tz.clone_from(&existing_entry.author_tz); - new_entry - .committer_name - .clone_from(&existing_entry.committer_name); - new_entry - .committer_email - .clone_from(&existing_entry.committer_email); - new_entry.committer_time = existing_entry.committer_time; - new_entry - .committer_tz - .clone_from(&existing_entry.committer_tz); - new_entry.summary.clone_from(&existing_entry.summary); - } - - current_entry.replace(new_entry); - } - Some(entry) => { - let Some((key, value)) = line.split_once(' ') else { - continue; - }; - let is_committed = !entry.sha.is_zero(); - match key { - "filename" => { - entry.filename = value.into(); - done = true; - } - "previous" => entry.previous = Some(value.into()), - - "summary" if is_committed => entry.summary = Some(value.into()), - "author" if is_committed => entry.author = Some(value.into()), - "author-mail" if is_committed => entry.author_mail = Some(value.into()), - "author-time" if is_committed => { - entry.author_time = Some(value.parse::()?) - } - "author-tz" if is_committed => entry.author_tz = Some(value.into()), - - "committer" if is_committed => entry.committer_name = Some(value.into()), - "committer-mail" if is_committed => entry.committer_email = Some(value.into()), - "committer-time" if is_committed => { - entry.committer_time = Some(value.parse::()?) - } - "committer-tz" if is_committed => entry.committer_tz = Some(value.into()), - _ => {} - } - } - }; - - 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); - } - } - } - } - - Ok(entries) -} - -#[cfg(test)] -mod tests { - use std::path::PathBuf; - - use super::BlameEntry; - use super::parse_git_blame; - - fn read_test_data(filename: &str) -> String { - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("test_data"); - path.push(filename); - - std::fs::read_to_string(&path) - .unwrap_or_else(|_| panic!("Could not read test data at {:?}. Is it generated?", path)) - } - - fn assert_eq_golden(entries: &Vec, golden_filename: &str) { - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("test_data"); - path.push("golden"); - path.push(format!("{}.json", golden_filename)); - - let mut have_json = - serde_json::to_string_pretty(&entries).expect("could not serialize entries to JSON"); - // We always want to save with a trailing newline. - have_json.push('\n'); - - let update = std::env::var("UPDATE_GOLDEN") - .map(|val| val.eq_ignore_ascii_case("true")) - .unwrap_or(false); - - if update { - std::fs::create_dir_all(path.parent().unwrap()) - .expect("could not create golden test data directory"); - std::fs::write(&path, have_json).expect("could not write out golden data"); - } else { - let want_json = - std::fs::read_to_string(&path).unwrap_or_else(|_| { - panic!("could not read golden test data file at {:?}. Did you run the test with UPDATE_GOLDEN=true before?", path); - }).replace("\r\n", "\n"); - - pretty_assertions::assert_eq!(have_json, want_json, "wrong blame entries"); - } - } - - #[test] - fn test_parse_git_blame_not_committed() { - let output = read_test_data("blame_incremental_not_committed"); - let entries = parse_git_blame(&output).unwrap(); - assert_eq_golden(&entries, "blame_incremental_not_committed"); - } - - #[test] - fn test_parse_git_blame_simple() { - let output = read_test_data("blame_incremental_simple"); - let entries = parse_git_blame(&output).unwrap(); - assert_eq_golden(&entries, "blame_incremental_simple"); - } - - #[test] - fn test_parse_git_blame_complex() { - let output = read_test_data("blame_incremental_complex"); - let entries = parse_git_blame(&output).unwrap(); - assert_eq_golden(&entries, "blame_incremental_complex"); - } -} diff --git a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-01.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-01.diff deleted file mode 100644 index c13a223c63..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-01.diff +++ /dev/null @@ -1,11 +0,0 @@ -@@ -94,6 +94,10 @@ - - let output = child.output().await.context("reading git blame output")?; - -+ handle_command_output(output) -+} -+ -+fn handle_command_output(output: std::process::Output) -> Result { - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - let trimmed = stderr.trim(); diff --git a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-02.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-02.diff deleted file mode 100644 index aa36a9241e..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-02.diff +++ /dev/null @@ -1,26 +0,0 @@ -@@ -95,15 +95,19 @@ - let output = child.output().await.context("reading git blame output")?; - - if !output.status.success() { -- let stderr = String::from_utf8_lossy(&output.stderr); -- let trimmed = stderr.trim(); -- if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { -- return Ok(String::new()); -- } -- anyhow::bail!("git blame process failed: {stderr}"); -+ return handle_command_output(output); - } - - Ok(String::from_utf8(output.stdout)?) -+} -+ -+fn handle_command_output(output: std::process::Output) -> Result { -+ let stderr = String::from_utf8_lossy(&output.stderr); -+ let trimmed = stderr.trim(); -+ if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { -+ return Ok(String::new()); -+ } -+ anyhow::bail!("git blame process failed: {stderr}"); - } - - #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] diff --git a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-03.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-03.diff deleted file mode 100644 index d3c19b4380..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-03.diff +++ /dev/null @@ -1,11 +0,0 @@ -@@ -93,7 +93,10 @@ - stdin.flush().await?; - - let output = child.output().await.context("reading git blame output")?; -+ handle_command_output(output) -+} - -+fn handle_command_output(output: std::process::Output) -> Result { - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - let trimmed = stderr.trim(); diff --git a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-04.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-04.diff deleted file mode 100644 index 1f87e4352c..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-04.diff +++ /dev/null @@ -1,24 +0,0 @@ -@@ -93,17 +93,20 @@ - stdin.flush().await?; - - let output = child.output().await.context("reading git blame output")?; -+ handle_command_output(&output)?; -+ Ok(String::from_utf8(output.stdout)?) -+} - -+fn handle_command_output(output: &std::process::Output) -> Result<()> { - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - let trimmed = stderr.trim(); - if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { -- return Ok(String::new()); -+ return Ok(()); - } - anyhow::bail!("git blame process failed: {stderr}"); - } -- -- Ok(String::from_utf8(output.stdout)?) -+ Ok(()) - } - - #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] diff --git a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-05.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-05.diff deleted file mode 100644 index 8f4b745b9a..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-05.diff +++ /dev/null @@ -1,26 +0,0 @@ -@@ -95,15 +95,19 @@ - let output = child.output().await.context("reading git blame output")?; - - if !output.status.success() { -- let stderr = String::from_utf8_lossy(&output.stderr); -- let trimmed = stderr.trim(); -- if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { -- return Ok(String::new()); -- } -- anyhow::bail!("git blame process failed: {stderr}"); -+ return handle_command_output(&output); - } - - Ok(String::from_utf8(output.stdout)?) -+} -+ -+fn handle_command_output(output: &std::process::Output) -> Result { -+ let stderr = String::from_utf8_lossy(&output.stderr); -+ let trimmed = stderr.trim(); -+ if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { -+ return Ok(String::new()); -+ } -+ anyhow::bail!("git blame process failed: {stderr}"); - } - - #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] diff --git a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-06.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-06.diff deleted file mode 100644 index 3514d9c8e2..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-06.diff +++ /dev/null @@ -1,23 +0,0 @@ -@@ -93,7 +93,12 @@ - stdin.flush().await?; - - let output = child.output().await.context("reading git blame output")?; -+ handle_command_output(&output)?; - -+ Ok(String::from_utf8(output.stdout)?) -+} -+ -+fn handle_command_output(output: &std::process::Output) -> Result { - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - let trimmed = stderr.trim(); -@@ -102,8 +107,7 @@ - } - anyhow::bail!("git blame process failed: {stderr}"); - } -- -- Ok(String::from_utf8(output.stdout)?) -+ Ok(String::from_utf8_lossy(&output.stdout).into_owned()) - } - - #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] diff --git a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-07.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-07.diff deleted file mode 100644 index 9691479e29..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-07.diff +++ /dev/null @@ -1,26 +0,0 @@ -@@ -95,15 +95,19 @@ - let output = child.output().await.context("reading git blame output")?; - - if !output.status.success() { -- let stderr = String::from_utf8_lossy(&output.stderr); -- let trimmed = stderr.trim(); -- if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { -- return Ok(String::new()); -- } -- anyhow::bail!("git blame process failed: {stderr}"); -+ return handle_command_output(output); - } - - Ok(String::from_utf8(output.stdout)?) -+} -+ -+fn handle_command_output(output: std::process::Output) -> Result { -+ let stderr = String::from_utf8_lossy(&output.stderr); -+ let trimmed = stderr.trim(); -+ if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { -+ return Ok(String::new()); -+ } -+ anyhow::bail!("git blame process failed: {stderr}"); - } - - #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] diff --git a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-08.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-08.diff deleted file mode 100644 index f5da859005..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-08.diff +++ /dev/null @@ -1,26 +0,0 @@ -@@ -95,15 +95,19 @@ - let output = child.output().await.context("reading git blame output")?; - - if !output.status.success() { -- let stderr = String::from_utf8_lossy(&output.stderr); -- let trimmed = stderr.trim(); -- if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { -- return Ok(String::new()); -- } -- anyhow::bail!("git blame process failed: {stderr}"); -+ return handle_command_output(output); - } - - Ok(String::from_utf8(output.stdout)?) -+} -+ -+fn handle_command_output(output: std::process::Output) -> Result { -+ let stderr = String::from_utf8_lossy(&output.stderr); -+ let trimmed = stderr.trim(); -+ if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { -+ return Ok(String::new()); -+ } -+ anyhow::bail!("git blame process failed: {stderr}") - } - - #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] diff --git a/crates/agent/src/edit_agent/evals/fixtures/from_pixels_constructor/before.rs b/crates/agent/src/edit_agent/evals/fixtures/from_pixels_constructor/before.rs deleted file mode 100644 index 12590fe6e9..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/from_pixels_constructor/before.rs +++ /dev/null @@ -1,339 +0,0 @@ -// font-kit/src/canvas.rs -// -// Copyright © 2018 The Pathfinder Project Developers. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -//! An in-memory bitmap surface for glyph rasterization. - -use lazy_static::lazy_static; -use pathfinder_geometry::rect::RectI; -use pathfinder_geometry::vector::Vector2I; -use std::cmp; -use std::fmt; - -use crate::utils; - -lazy_static! { - static ref BITMAP_1BPP_TO_8BPP_LUT: [[u8; 8]; 256] = { - let mut lut = [[0; 8]; 256]; - for byte in 0..0x100 { - let mut value = [0; 8]; - for bit in 0..8 { - if (byte & (0x80 >> bit)) != 0 { - value[bit] = 0xff; - } - } - lut[byte] = value - } - lut - }; -} - -/// An in-memory bitmap surface for glyph rasterization. -pub struct Canvas { - /// The raw pixel data. - pub pixels: Vec, - /// The size of the buffer, in pixels. - pub size: Vector2I, - /// The number of *bytes* between successive rows. - pub stride: usize, - /// The image format of the canvas. - pub format: Format, -} - -impl Canvas { - /// Creates a new blank canvas with the given pixel size and format. - /// - /// Stride is automatically calculated from width. - /// - /// The canvas is initialized with transparent black (all values 0). - #[inline] - pub fn new(size: Vector2I, format: Format) -> Canvas { - Canvas::with_stride( - size, - size.x() as usize * format.bytes_per_pixel() as usize, - format, - ) - } - - /// Creates a new blank canvas with the given pixel size, stride (number of bytes between - /// successive rows), and format. - /// - /// The canvas is initialized with transparent black (all values 0). - pub fn with_stride(size: Vector2I, stride: usize, format: Format) -> Canvas { - Canvas { - pixels: vec![0; stride * size.y() as usize], - size, - stride, - format, - } - } - - #[allow(dead_code)] - pub(crate) fn blit_from_canvas(&mut self, src: &Canvas) { - self.blit_from( - Vector2I::default(), - &src.pixels, - src.size, - src.stride, - src.format, - ) - } - - /// Blits to a rectangle with origin at `dst_point` and size according to `src_size`. - /// If the target area overlaps the boundaries of the canvas, only the drawable region is blitted. - /// `dst_point` and `src_size` are specified in pixels. `src_stride` is specified in bytes. - /// `src_stride` must be equal or larger than the actual data length. - #[allow(dead_code)] - pub(crate) fn blit_from( - &mut self, - dst_point: Vector2I, - src_bytes: &[u8], - src_size: Vector2I, - src_stride: usize, - src_format: Format, - ) { - assert_eq!( - src_stride * src_size.y() as usize, - src_bytes.len(), - "Number of pixels in src_bytes does not match stride and size." - ); - assert!( - src_stride >= src_size.x() as usize * src_format.bytes_per_pixel() as usize, - "src_stride must be >= than src_size.x()" - ); - - let dst_rect = RectI::new(dst_point, src_size); - let dst_rect = dst_rect.intersection(RectI::new(Vector2I::default(), self.size)); - let dst_rect = match dst_rect { - Some(dst_rect) => dst_rect, - None => return, - }; - - match (self.format, src_format) { - (Format::A8, Format::A8) - | (Format::Rgb24, Format::Rgb24) - | (Format::Rgba32, Format::Rgba32) => { - self.blit_from_with::(dst_rect, src_bytes, src_stride, src_format) - } - (Format::A8, Format::Rgb24) => { - self.blit_from_with::(dst_rect, src_bytes, src_stride, src_format) - } - (Format::Rgb24, Format::A8) => { - self.blit_from_with::(dst_rect, src_bytes, src_stride, src_format) - } - (Format::Rgb24, Format::Rgba32) => self - .blit_from_with::(dst_rect, src_bytes, src_stride, src_format), - (Format::Rgba32, Format::Rgb24) => self - .blit_from_with::(dst_rect, src_bytes, src_stride, src_format), - (Format::Rgba32, Format::A8) | (Format::A8, Format::Rgba32) => unimplemented!(), - } - } - - #[allow(dead_code)] - pub(crate) fn blit_from_bitmap_1bpp( - &mut self, - dst_point: Vector2I, - src_bytes: &[u8], - src_size: Vector2I, - src_stride: usize, - ) { - if self.format != Format::A8 { - unimplemented!() - } - - let dst_rect = RectI::new(dst_point, src_size); - let dst_rect = dst_rect.intersection(RectI::new(Vector2I::default(), self.size)); - let dst_rect = match dst_rect { - Some(dst_rect) => dst_rect, - None => return, - }; - - let size = dst_rect.size(); - - let dest_bytes_per_pixel = self.format.bytes_per_pixel() as usize; - let dest_row_stride = size.x() as usize * dest_bytes_per_pixel; - let src_row_stride = utils::div_round_up(size.x() as usize, 8); - - for y in 0..size.y() { - let (dest_row_start, src_row_start) = ( - (y + dst_rect.origin_y()) as usize * self.stride - + dst_rect.origin_x() as usize * dest_bytes_per_pixel, - y as usize * src_stride, - ); - let dest_row_end = dest_row_start + dest_row_stride; - let src_row_end = src_row_start + src_row_stride; - let dest_row_pixels = &mut self.pixels[dest_row_start..dest_row_end]; - let src_row_pixels = &src_bytes[src_row_start..src_row_end]; - for x in 0..src_row_stride { - let pattern = &BITMAP_1BPP_TO_8BPP_LUT[src_row_pixels[x] as usize]; - let dest_start = x * 8; - let dest_end = cmp::min(dest_start + 8, dest_row_stride); - let src = &pattern[0..(dest_end - dest_start)]; - dest_row_pixels[dest_start..dest_end].clone_from_slice(src); - } - } - } - - /// Blits to area `rect` using the data given in the buffer `src_bytes`. - /// `src_stride` must be specified in bytes. - /// The dimensions of `rect` must be in pixels. - fn blit_from_with( - &mut self, - rect: RectI, - src_bytes: &[u8], - src_stride: usize, - src_format: Format, - ) { - let src_bytes_per_pixel = src_format.bytes_per_pixel() as usize; - let dest_bytes_per_pixel = self.format.bytes_per_pixel() as usize; - - for y in 0..rect.height() { - let (dest_row_start, src_row_start) = ( - (y + rect.origin_y()) as usize * self.stride - + rect.origin_x() as usize * dest_bytes_per_pixel, - y as usize * src_stride, - ); - let dest_row_end = dest_row_start + rect.width() as usize * dest_bytes_per_pixel; - let src_row_end = src_row_start + rect.width() as usize * src_bytes_per_pixel; - let dest_row_pixels = &mut self.pixels[dest_row_start..dest_row_end]; - let src_row_pixels = &src_bytes[src_row_start..src_row_end]; - B::blit(dest_row_pixels, src_row_pixels) - } - } -} - -impl fmt::Debug for Canvas { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Canvas") - .field("pixels", &self.pixels.len()) // Do not dump a vector content. - .field("size", &self.size) - .field("stride", &self.stride) - .field("format", &self.format) - .finish() - } -} - -/// The image format for the canvas. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Format { - /// Premultiplied R8G8B8A8, little-endian. - Rgba32, - /// R8G8B8, little-endian. - Rgb24, - /// A8. - A8, -} - -impl Format { - /// Returns the number of bits per pixel that this image format corresponds to. - #[inline] - pub fn bits_per_pixel(self) -> u8 { - match self { - Format::Rgba32 => 32, - Format::Rgb24 => 24, - Format::A8 => 8, - } - } - - /// Returns the number of color channels per pixel that this image format corresponds to. - #[inline] - pub fn components_per_pixel(self) -> u8 { - match self { - Format::Rgba32 => 4, - Format::Rgb24 => 3, - Format::A8 => 1, - } - } - - /// Returns the number of bits per color channel that this image format contains. - #[inline] - pub fn bits_per_component(self) -> u8 { - self.bits_per_pixel() / self.components_per_pixel() - } - - /// Returns the number of bytes per pixel that this image format corresponds to. - #[inline] - pub fn bytes_per_pixel(self) -> u8 { - self.bits_per_pixel() / 8 - } -} - -/// The antialiasing strategy that should be used when rasterizing glyphs. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum RasterizationOptions { - /// "Black-and-white" rendering. Each pixel is either entirely on or off. - Bilevel, - /// Grayscale antialiasing. Only one channel is used. - GrayscaleAa, - /// Subpixel RGB antialiasing, for LCD screens. - SubpixelAa, -} - -trait Blit { - fn blit(dest: &mut [u8], src: &[u8]); -} - -struct BlitMemcpy; - -impl Blit for BlitMemcpy { - #[inline] - fn blit(dest: &mut [u8], src: &[u8]) { - dest.clone_from_slice(src) - } -} - -struct BlitRgb24ToA8; - -impl Blit for BlitRgb24ToA8 { - #[inline] - fn blit(dest: &mut [u8], src: &[u8]) { - // TODO(pcwalton): SIMD. - for (dest, src) in dest.iter_mut().zip(src.chunks(3)) { - *dest = src[1] - } - } -} - -struct BlitA8ToRgb24; - -impl Blit for BlitA8ToRgb24 { - #[inline] - fn blit(dest: &mut [u8], src: &[u8]) { - for (dest, src) in dest.chunks_mut(3).zip(src.iter()) { - dest[0] = *src; - dest[1] = *src; - dest[2] = *src; - } - } -} - -struct BlitRgba32ToRgb24; - -impl Blit for BlitRgba32ToRgb24 { - #[inline] - fn blit(dest: &mut [u8], src: &[u8]) { - // TODO(pcwalton): SIMD. - for (dest, src) in dest.chunks_mut(3).zip(src.chunks(4)) { - dest.copy_from_slice(&src[0..3]) - } - } -} - -struct BlitRgb24ToRgba32; - -impl Blit for BlitRgb24ToRgba32 { - fn blit(dest: &mut [u8], src: &[u8]) { - for (dest, src) in dest.chunks_mut(4).zip(src.chunks(3)) { - dest[0] = src[0]; - dest[1] = src[1]; - dest[2] = src[2]; - dest[3] = 255; - } - } -} diff --git a/crates/agent/src/edit_agent/evals/fixtures/translate_doc_comments/before.rs b/crates/agent/src/edit_agent/evals/fixtures/translate_doc_comments/before.rs deleted file mode 100644 index 12590fe6e9..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/translate_doc_comments/before.rs +++ /dev/null @@ -1,339 +0,0 @@ -// font-kit/src/canvas.rs -// -// Copyright © 2018 The Pathfinder Project Developers. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -//! An in-memory bitmap surface for glyph rasterization. - -use lazy_static::lazy_static; -use pathfinder_geometry::rect::RectI; -use pathfinder_geometry::vector::Vector2I; -use std::cmp; -use std::fmt; - -use crate::utils; - -lazy_static! { - static ref BITMAP_1BPP_TO_8BPP_LUT: [[u8; 8]; 256] = { - let mut lut = [[0; 8]; 256]; - for byte in 0..0x100 { - let mut value = [0; 8]; - for bit in 0..8 { - if (byte & (0x80 >> bit)) != 0 { - value[bit] = 0xff; - } - } - lut[byte] = value - } - lut - }; -} - -/// An in-memory bitmap surface for glyph rasterization. -pub struct Canvas { - /// The raw pixel data. - pub pixels: Vec, - /// The size of the buffer, in pixels. - pub size: Vector2I, - /// The number of *bytes* between successive rows. - pub stride: usize, - /// The image format of the canvas. - pub format: Format, -} - -impl Canvas { - /// Creates a new blank canvas with the given pixel size and format. - /// - /// Stride is automatically calculated from width. - /// - /// The canvas is initialized with transparent black (all values 0). - #[inline] - pub fn new(size: Vector2I, format: Format) -> Canvas { - Canvas::with_stride( - size, - size.x() as usize * format.bytes_per_pixel() as usize, - format, - ) - } - - /// Creates a new blank canvas with the given pixel size, stride (number of bytes between - /// successive rows), and format. - /// - /// The canvas is initialized with transparent black (all values 0). - pub fn with_stride(size: Vector2I, stride: usize, format: Format) -> Canvas { - Canvas { - pixels: vec![0; stride * size.y() as usize], - size, - stride, - format, - } - } - - #[allow(dead_code)] - pub(crate) fn blit_from_canvas(&mut self, src: &Canvas) { - self.blit_from( - Vector2I::default(), - &src.pixels, - src.size, - src.stride, - src.format, - ) - } - - /// Blits to a rectangle with origin at `dst_point` and size according to `src_size`. - /// If the target area overlaps the boundaries of the canvas, only the drawable region is blitted. - /// `dst_point` and `src_size` are specified in pixels. `src_stride` is specified in bytes. - /// `src_stride` must be equal or larger than the actual data length. - #[allow(dead_code)] - pub(crate) fn blit_from( - &mut self, - dst_point: Vector2I, - src_bytes: &[u8], - src_size: Vector2I, - src_stride: usize, - src_format: Format, - ) { - assert_eq!( - src_stride * src_size.y() as usize, - src_bytes.len(), - "Number of pixels in src_bytes does not match stride and size." - ); - assert!( - src_stride >= src_size.x() as usize * src_format.bytes_per_pixel() as usize, - "src_stride must be >= than src_size.x()" - ); - - let dst_rect = RectI::new(dst_point, src_size); - let dst_rect = dst_rect.intersection(RectI::new(Vector2I::default(), self.size)); - let dst_rect = match dst_rect { - Some(dst_rect) => dst_rect, - None => return, - }; - - match (self.format, src_format) { - (Format::A8, Format::A8) - | (Format::Rgb24, Format::Rgb24) - | (Format::Rgba32, Format::Rgba32) => { - self.blit_from_with::(dst_rect, src_bytes, src_stride, src_format) - } - (Format::A8, Format::Rgb24) => { - self.blit_from_with::(dst_rect, src_bytes, src_stride, src_format) - } - (Format::Rgb24, Format::A8) => { - self.blit_from_with::(dst_rect, src_bytes, src_stride, src_format) - } - (Format::Rgb24, Format::Rgba32) => self - .blit_from_with::(dst_rect, src_bytes, src_stride, src_format), - (Format::Rgba32, Format::Rgb24) => self - .blit_from_with::(dst_rect, src_bytes, src_stride, src_format), - (Format::Rgba32, Format::A8) | (Format::A8, Format::Rgba32) => unimplemented!(), - } - } - - #[allow(dead_code)] - pub(crate) fn blit_from_bitmap_1bpp( - &mut self, - dst_point: Vector2I, - src_bytes: &[u8], - src_size: Vector2I, - src_stride: usize, - ) { - if self.format != Format::A8 { - unimplemented!() - } - - let dst_rect = RectI::new(dst_point, src_size); - let dst_rect = dst_rect.intersection(RectI::new(Vector2I::default(), self.size)); - let dst_rect = match dst_rect { - Some(dst_rect) => dst_rect, - None => return, - }; - - let size = dst_rect.size(); - - let dest_bytes_per_pixel = self.format.bytes_per_pixel() as usize; - let dest_row_stride = size.x() as usize * dest_bytes_per_pixel; - let src_row_stride = utils::div_round_up(size.x() as usize, 8); - - for y in 0..size.y() { - let (dest_row_start, src_row_start) = ( - (y + dst_rect.origin_y()) as usize * self.stride - + dst_rect.origin_x() as usize * dest_bytes_per_pixel, - y as usize * src_stride, - ); - let dest_row_end = dest_row_start + dest_row_stride; - let src_row_end = src_row_start + src_row_stride; - let dest_row_pixels = &mut self.pixels[dest_row_start..dest_row_end]; - let src_row_pixels = &src_bytes[src_row_start..src_row_end]; - for x in 0..src_row_stride { - let pattern = &BITMAP_1BPP_TO_8BPP_LUT[src_row_pixels[x] as usize]; - let dest_start = x * 8; - let dest_end = cmp::min(dest_start + 8, dest_row_stride); - let src = &pattern[0..(dest_end - dest_start)]; - dest_row_pixels[dest_start..dest_end].clone_from_slice(src); - } - } - } - - /// Blits to area `rect` using the data given in the buffer `src_bytes`. - /// `src_stride` must be specified in bytes. - /// The dimensions of `rect` must be in pixels. - fn blit_from_with( - &mut self, - rect: RectI, - src_bytes: &[u8], - src_stride: usize, - src_format: Format, - ) { - let src_bytes_per_pixel = src_format.bytes_per_pixel() as usize; - let dest_bytes_per_pixel = self.format.bytes_per_pixel() as usize; - - for y in 0..rect.height() { - let (dest_row_start, src_row_start) = ( - (y + rect.origin_y()) as usize * self.stride - + rect.origin_x() as usize * dest_bytes_per_pixel, - y as usize * src_stride, - ); - let dest_row_end = dest_row_start + rect.width() as usize * dest_bytes_per_pixel; - let src_row_end = src_row_start + rect.width() as usize * src_bytes_per_pixel; - let dest_row_pixels = &mut self.pixels[dest_row_start..dest_row_end]; - let src_row_pixels = &src_bytes[src_row_start..src_row_end]; - B::blit(dest_row_pixels, src_row_pixels) - } - } -} - -impl fmt::Debug for Canvas { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Canvas") - .field("pixels", &self.pixels.len()) // Do not dump a vector content. - .field("size", &self.size) - .field("stride", &self.stride) - .field("format", &self.format) - .finish() - } -} - -/// The image format for the canvas. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Format { - /// Premultiplied R8G8B8A8, little-endian. - Rgba32, - /// R8G8B8, little-endian. - Rgb24, - /// A8. - A8, -} - -impl Format { - /// Returns the number of bits per pixel that this image format corresponds to. - #[inline] - pub fn bits_per_pixel(self) -> u8 { - match self { - Format::Rgba32 => 32, - Format::Rgb24 => 24, - Format::A8 => 8, - } - } - - /// Returns the number of color channels per pixel that this image format corresponds to. - #[inline] - pub fn components_per_pixel(self) -> u8 { - match self { - Format::Rgba32 => 4, - Format::Rgb24 => 3, - Format::A8 => 1, - } - } - - /// Returns the number of bits per color channel that this image format contains. - #[inline] - pub fn bits_per_component(self) -> u8 { - self.bits_per_pixel() / self.components_per_pixel() - } - - /// Returns the number of bytes per pixel that this image format corresponds to. - #[inline] - pub fn bytes_per_pixel(self) -> u8 { - self.bits_per_pixel() / 8 - } -} - -/// The antialiasing strategy that should be used when rasterizing glyphs. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum RasterizationOptions { - /// "Black-and-white" rendering. Each pixel is either entirely on or off. - Bilevel, - /// Grayscale antialiasing. Only one channel is used. - GrayscaleAa, - /// Subpixel RGB antialiasing, for LCD screens. - SubpixelAa, -} - -trait Blit { - fn blit(dest: &mut [u8], src: &[u8]); -} - -struct BlitMemcpy; - -impl Blit for BlitMemcpy { - #[inline] - fn blit(dest: &mut [u8], src: &[u8]) { - dest.clone_from_slice(src) - } -} - -struct BlitRgb24ToA8; - -impl Blit for BlitRgb24ToA8 { - #[inline] - fn blit(dest: &mut [u8], src: &[u8]) { - // TODO(pcwalton): SIMD. - for (dest, src) in dest.iter_mut().zip(src.chunks(3)) { - *dest = src[1] - } - } -} - -struct BlitA8ToRgb24; - -impl Blit for BlitA8ToRgb24 { - #[inline] - fn blit(dest: &mut [u8], src: &[u8]) { - for (dest, src) in dest.chunks_mut(3).zip(src.iter()) { - dest[0] = *src; - dest[1] = *src; - dest[2] = *src; - } - } -} - -struct BlitRgba32ToRgb24; - -impl Blit for BlitRgba32ToRgb24 { - #[inline] - fn blit(dest: &mut [u8], src: &[u8]) { - // TODO(pcwalton): SIMD. - for (dest, src) in dest.chunks_mut(3).zip(src.chunks(4)) { - dest.copy_from_slice(&src[0..3]) - } - } -} - -struct BlitRgb24ToRgba32; - -impl Blit for BlitRgb24ToRgba32 { - fn blit(dest: &mut [u8], src: &[u8]) { - for (dest, src) in dest.chunks_mut(4).zip(src.chunks(3)) { - dest[0] = src[0]; - dest[1] = src[1]; - dest[2] = src[2]; - dest[3] = 255; - } - } -} diff --git a/crates/agent/src/edit_agent/evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs b/crates/agent/src/edit_agent/evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs deleted file mode 100644 index cfa28fe1ad..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs +++ /dev/null @@ -1,1629 +0,0 @@ -#![doc = include_str!("../README.md")] -#![cfg_attr(docsrs, feature(doc_cfg))] - -#[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))] -use std::ops::Range; -#[cfg(feature = "tree-sitter-highlight")] -use std::sync::Mutex; -use std::{ - collections::HashMap, - env, - ffi::{OsStr, OsString}, - fs, - io::{BufRead, BufReader}, - mem, - path::{Path, PathBuf}, - process::Command, - sync::LazyLock, - time::SystemTime, -}; - -#[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))] -use anyhow::Error; -use anyhow::{Context as _, Result, anyhow}; -use etcetera::BaseStrategy as _; -use fs4::fs_std::FileExt; -use indoc::indoc; -use libloading::{Library, Symbol}; -use once_cell::unsync::OnceCell; -use path_slash::PathBufExt as _; -use regex::{Regex, RegexBuilder}; -use semver::Version; -use serde::{Deserialize, Deserializer, Serialize}; -use tree_sitter::Language; -#[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))] -use tree_sitter::QueryError; -#[cfg(feature = "tree-sitter-highlight")] -use tree_sitter::QueryErrorKind; -#[cfg(feature = "tree-sitter-highlight")] -use tree_sitter_highlight::HighlightConfiguration; -#[cfg(feature = "tree-sitter-tags")] -use tree_sitter_tags::{Error as TagsError, TagsConfiguration}; -use url::Url; - -static GRAMMAR_NAME_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r#""name":\s*"(.*?)""#).unwrap()); - -pub const EMSCRIPTEN_TAG: &str = concat!("docker.io/emscripten/emsdk:", env!("EMSCRIPTEN_VERSION")); - -#[derive(Default, Deserialize, Serialize)] -pub struct Config { - #[serde(default)] - #[serde( - rename = "parser-directories", - deserialize_with = "deserialize_parser_directories" - )] - pub parser_directories: Vec, -} - -#[derive(Serialize, Deserialize, Clone, Default)] -#[serde(untagged)] -pub enum PathsJSON { - #[default] - Empty, - Single(PathBuf), - Multiple(Vec), -} - -impl PathsJSON { - fn into_vec(self) -> Option> { - match self { - Self::Empty => None, - Self::Single(s) => Some(vec![s]), - Self::Multiple(s) => Some(s), - } - } - - const fn is_empty(&self) -> bool { - matches!(self, Self::Empty) - } -} - -#[derive(Serialize, Deserialize, Clone)] -#[serde(untagged)] -pub enum PackageJSONAuthor { - String(String), - Object { - name: String, - email: Option, - url: Option, - }, -} - -#[derive(Serialize, Deserialize, Clone)] -#[serde(untagged)] -pub enum PackageJSONRepository { - String(String), - Object { url: String }, -} - -#[derive(Serialize, Deserialize)] -pub struct PackageJSON { - pub name: String, - pub version: Version, - pub description: Option, - pub author: Option, - pub maintainers: Option>, - pub license: Option, - pub repository: Option, - #[serde(default)] - #[serde(rename = "tree-sitter", skip_serializing_if = "Option::is_none")] - pub tree_sitter: Option>, -} - -fn default_path() -> PathBuf { - PathBuf::from(".") -} - -#[derive(Serialize, Deserialize, Clone)] -#[serde(rename_all = "kebab-case")] -pub struct LanguageConfigurationJSON { - #[serde(default = "default_path")] - pub path: PathBuf, - pub scope: Option, - pub file_types: Option>, - pub content_regex: Option, - pub first_line_regex: Option, - pub injection_regex: Option, - #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] - pub highlights: PathsJSON, - #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] - pub injections: PathsJSON, - #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] - pub locals: PathsJSON, - #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] - pub tags: PathsJSON, - #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] - pub external_files: PathsJSON, -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct TreeSitterJSON { - #[serde(rename = "$schema")] - pub schema: Option, - pub grammars: Vec, - pub metadata: Metadata, - #[serde(default)] - pub bindings: Bindings, -} - -impl TreeSitterJSON { - pub fn from_file(path: &Path) -> Result { - Ok(serde_json::from_str(&fs::read_to_string( - path.join("tree-sitter.json"), - )?)?) - } - - #[must_use] - pub fn has_multiple_language_configs(&self) -> bool { - self.grammars.len() > 1 - } -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct Grammar { - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub camelcase: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - pub scope: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub path: Option, - #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] - pub external_files: PathsJSON, - pub file_types: Option>, - #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] - pub highlights: PathsJSON, - #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] - pub injections: PathsJSON, - #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] - pub locals: PathsJSON, - #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] - pub tags: PathsJSON, - #[serde(skip_serializing_if = "Option::is_none")] - pub injection_regex: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub first_line_regex: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub content_regex: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub class_name: Option, -} - -#[derive(Serialize, Deserialize)] -pub struct Metadata { - pub version: Version, - #[serde(skip_serializing_if = "Option::is_none")] - pub license: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub authors: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub links: Option, - #[serde(skip)] - pub namespace: Option, -} - -#[derive(Serialize, Deserialize)] -pub struct Author { - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub email: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub url: Option, -} - -#[derive(Serialize, Deserialize)] -pub struct Links { - pub repository: Url, - #[serde(skip_serializing_if = "Option::is_none")] - pub funding: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub homepage: Option, -} - -#[derive(Serialize, Deserialize)] -#[serde(default)] -pub struct Bindings { - pub c: bool, - pub go: bool, - #[serde(skip)] - pub java: bool, - #[serde(skip)] - pub kotlin: bool, - pub node: bool, - pub python: bool, - pub rust: bool, - pub swift: bool, - pub zig: bool, -} - -impl Default for Bindings { - fn default() -> Self { - Self { - c: true, - go: true, - java: false, - kotlin: false, - node: true, - python: true, - rust: true, - swift: true, - zig: false, - } - } -} - -// Replace `~` or `$HOME` with home path string. -// (While paths like "~/.tree-sitter/config.json" can be deserialized, -// they're not valid path for I/O modules.) -fn deserialize_parser_directories<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let paths = Vec::::deserialize(deserializer)?; - let Ok(home) = etcetera::home_dir() else { - return Ok(paths); - }; - let standardized = paths - .into_iter() - .map(|path| standardize_path(path, &home)) - .collect(); - Ok(standardized) -} - -fn standardize_path(path: PathBuf, home: &Path) -> PathBuf { - if let Ok(p) = path.strip_prefix("~") { - return home.join(p); - } - if let Ok(p) = path.strip_prefix("$HOME") { - return home.join(p); - } - path -} - -impl Config { - #[must_use] - pub fn initial() -> Self { - let home_dir = etcetera::home_dir().expect("Cannot determine home directory"); - Self { - parser_directories: vec![ - home_dir.join("github"), - home_dir.join("src"), - home_dir.join("source"), - home_dir.join("projects"), - home_dir.join("dev"), - home_dir.join("git"), - ], - } - } -} - -const BUILD_TARGET: &str = env!("BUILD_TARGET"); -const BUILD_HOST: &str = env!("BUILD_HOST"); - -pub struct LanguageConfiguration<'a> { - pub scope: Option, - pub content_regex: Option, - pub first_line_regex: Option, - pub injection_regex: Option, - pub file_types: Vec, - pub root_path: PathBuf, - pub highlights_filenames: Option>, - pub injections_filenames: Option>, - pub locals_filenames: Option>, - pub tags_filenames: Option>, - pub language_name: String, - language_id: usize, - #[cfg(feature = "tree-sitter-highlight")] - highlight_config: OnceCell>, - #[cfg(feature = "tree-sitter-tags")] - tags_config: OnceCell>, - #[cfg(feature = "tree-sitter-highlight")] - highlight_names: &'a Mutex>, - #[cfg(feature = "tree-sitter-highlight")] - use_all_highlight_names: bool, -} - -pub struct Loader { - pub parser_lib_path: PathBuf, - languages_by_id: Vec<(PathBuf, OnceCell, Option>)>, - language_configurations: Vec>, - language_configuration_ids_by_file_type: HashMap>, - language_configuration_in_current_path: Option, - language_configuration_ids_by_first_line_regex: HashMap>, - #[cfg(feature = "tree-sitter-highlight")] - highlight_names: Box>>, - #[cfg(feature = "tree-sitter-highlight")] - use_all_highlight_names: bool, - debug_build: bool, - sanitize_build: bool, - force_rebuild: bool, - - #[cfg(feature = "wasm")] - wasm_store: Mutex>, -} - -pub struct CompileConfig<'a> { - pub src_path: &'a Path, - pub header_paths: Vec<&'a Path>, - pub parser_path: PathBuf, - pub scanner_path: Option, - pub external_files: Option<&'a [PathBuf]>, - pub output_path: Option, - pub flags: &'a [&'a str], - pub sanitize: bool, - pub name: String, -} - -impl<'a> CompileConfig<'a> { - #[must_use] - pub fn new( - src_path: &'a Path, - externals: Option<&'a [PathBuf]>, - output_path: Option, - ) -> Self { - Self { - src_path, - header_paths: vec![src_path], - parser_path: src_path.join("parser.c"), - scanner_path: None, - external_files: externals, - output_path, - flags: &[], - sanitize: false, - name: String::new(), - } - } -} - -unsafe impl Sync for Loader {} - -impl Loader { - pub fn new() -> Result { - let parser_lib_path = if let Ok(path) = env::var("TREE_SITTER_LIBDIR") { - PathBuf::from(path) - } else { - if cfg!(target_os = "macos") { - let legacy_apple_path = etcetera::base_strategy::Apple::new()? - .cache_dir() // `$HOME/Library/Caches/` - .join("tree-sitter"); - if legacy_apple_path.exists() && legacy_apple_path.is_dir() { - std::fs::remove_dir_all(legacy_apple_path)?; - } - } - - etcetera::choose_base_strategy()? - .cache_dir() - .join("tree-sitter") - .join("lib") - }; - Ok(Self::with_parser_lib_path(parser_lib_path)) - } - - #[must_use] - pub fn with_parser_lib_path(parser_lib_path: PathBuf) -> Self { - Self { - parser_lib_path, - languages_by_id: Vec::new(), - language_configurations: Vec::new(), - language_configuration_ids_by_file_type: HashMap::new(), - language_configuration_in_current_path: None, - language_configuration_ids_by_first_line_regex: HashMap::new(), - #[cfg(feature = "tree-sitter-highlight")] - highlight_names: Box::new(Mutex::new(Vec::new())), - #[cfg(feature = "tree-sitter-highlight")] - use_all_highlight_names: true, - debug_build: false, - sanitize_build: false, - force_rebuild: false, - - #[cfg(feature = "wasm")] - wasm_store: Mutex::default(), - } - } - - #[cfg(feature = "tree-sitter-highlight")] - #[cfg_attr(docsrs, doc(cfg(feature = "tree-sitter-highlight")))] - pub fn configure_highlights(&mut self, names: &[String]) { - self.use_all_highlight_names = false; - let mut highlights = self.highlight_names.lock().unwrap(); - highlights.clear(); - highlights.extend(names.iter().cloned()); - } - - #[must_use] - #[cfg(feature = "tree-sitter-highlight")] - #[cfg_attr(docsrs, doc(cfg(feature = "tree-sitter-highlight")))] - pub fn highlight_names(&self) -> Vec { - self.highlight_names.lock().unwrap().clone() - } - - pub fn find_all_languages(&mut self, config: &Config) -> Result<()> { - if config.parser_directories.is_empty() { - eprintln!("Warning: You have not configured any parser directories!"); - eprintln!("Please run `tree-sitter init-config` and edit the resulting"); - eprintln!("configuration file to indicate where we should look for"); - eprintln!("language grammars.\n"); - } - for parser_container_dir in &config.parser_directories { - if let Ok(entries) = fs::read_dir(parser_container_dir) { - for entry in entries { - let entry = entry?; - if let Some(parser_dir_name) = entry.file_name().to_str() { - if parser_dir_name.starts_with("tree-sitter-") { - self.find_language_configurations_at_path( - &parser_container_dir.join(parser_dir_name), - false, - ) - .ok(); - } - } - } - } - } - Ok(()) - } - - pub fn languages_at_path(&mut self, path: &Path) -> Result> { - if let Ok(configurations) = self.find_language_configurations_at_path(path, true) { - let mut language_ids = configurations - .iter() - .map(|c| (c.language_id, c.language_name.clone())) - .collect::>(); - language_ids.sort_unstable(); - language_ids.dedup(); - language_ids - .into_iter() - .map(|(id, name)| Ok((self.language_for_id(id)?, name))) - .collect::>>() - } else { - Ok(Vec::new()) - } - } - - #[must_use] - pub fn get_all_language_configurations(&self) -> Vec<(&LanguageConfiguration, &Path)> { - self.language_configurations - .iter() - .map(|c| (c, self.languages_by_id[c.language_id].0.as_ref())) - .collect() - } - - pub fn language_configuration_for_scope( - &self, - scope: &str, - ) -> Result> { - for configuration in &self.language_configurations { - if configuration.scope.as_ref().is_some_and(|s| s == scope) { - let language = self.language_for_id(configuration.language_id)?; - return Ok(Some((language, configuration))); - } - } - Ok(None) - } - - pub fn language_configuration_for_first_line_regex( - &self, - path: &Path, - ) -> Result> { - self.language_configuration_ids_by_first_line_regex - .iter() - .try_fold(None, |_, (regex, ids)| { - if let Some(regex) = Self::regex(Some(regex)) { - let file = fs::File::open(path)?; - let reader = BufReader::new(file); - let first_line = reader.lines().next().transpose()?; - if let Some(first_line) = first_line { - if regex.is_match(&first_line) && !ids.is_empty() { - let configuration = &self.language_configurations[ids[0]]; - let language = self.language_for_id(configuration.language_id)?; - return Ok(Some((language, configuration))); - } - } - } - - Ok(None) - }) - } - - pub fn language_configuration_for_file_name( - &self, - path: &Path, - ) -> Result> { - // Find all the language configurations that match this file name - // or a suffix of the file name. - let configuration_ids = path - .file_name() - .and_then(|n| n.to_str()) - .and_then(|file_name| self.language_configuration_ids_by_file_type.get(file_name)) - .or_else(|| { - let mut path = path.to_owned(); - let mut extensions = Vec::with_capacity(2); - while let Some(extension) = path.extension() { - extensions.push(extension.to_str()?.to_string()); - path = PathBuf::from(path.file_stem()?.to_os_string()); - } - extensions.reverse(); - self.language_configuration_ids_by_file_type - .get(&extensions.join(".")) - }); - - if let Some(configuration_ids) = configuration_ids { - if !configuration_ids.is_empty() { - let configuration = if configuration_ids.len() == 1 { - &self.language_configurations[configuration_ids[0]] - } - // If multiple language configurations match, then determine which - // one to use by applying the configurations' content regexes. - else { - let file_contents = fs::read(path) - .with_context(|| format!("Failed to read path {}", path.display()))?; - let file_contents = String::from_utf8_lossy(&file_contents); - let mut best_score = -2isize; - let mut best_configuration_id = None; - for configuration_id in configuration_ids { - let config = &self.language_configurations[*configuration_id]; - - // If the language configuration has a content regex, assign - // a score based on the length of the first match. - let score; - if let Some(content_regex) = &config.content_regex { - if let Some(mat) = content_regex.find(&file_contents) { - score = (mat.end() - mat.start()) as isize; - } - // If the content regex does not match, then *penalize* this - // language configuration, so that language configurations - // without content regexes are preferred over those with - // non-matching content regexes. - else { - score = -1; - } - } else { - score = 0; - } - if score > best_score { - best_configuration_id = Some(*configuration_id); - best_score = score; - } - } - - &self.language_configurations[best_configuration_id.unwrap()] - }; - - let language = self.language_for_id(configuration.language_id)?; - return Ok(Some((language, configuration))); - } - } - - Ok(None) - } - - pub fn language_configuration_for_injection_string( - &self, - string: &str, - ) -> Result> { - let mut best_match_length = 0; - let mut best_match_position = None; - for (i, configuration) in self.language_configurations.iter().enumerate() { - if let Some(injection_regex) = &configuration.injection_regex { - if let Some(mat) = injection_regex.find(string) { - let length = mat.end() - mat.start(); - if length > best_match_length { - best_match_position = Some(i); - best_match_length = length; - } - } - } - } - - if let Some(i) = best_match_position { - let configuration = &self.language_configurations[i]; - let language = self.language_for_id(configuration.language_id)?; - Ok(Some((language, configuration))) - } else { - Ok(None) - } - } - - pub fn language_for_configuration( - &self, - configuration: &LanguageConfiguration, - ) -> Result { - self.language_for_id(configuration.language_id) - } - - fn language_for_id(&self, id: usize) -> Result { - let (path, language, externals) = &self.languages_by_id[id]; - language - .get_or_try_init(|| { - let src_path = path.join("src"); - self.load_language_at_path(CompileConfig::new( - &src_path, - externals.as_deref(), - None, - )) - }) - .cloned() - } - - pub fn compile_parser_at_path( - &self, - grammar_path: &Path, - output_path: PathBuf, - flags: &[&str], - ) -> Result<()> { - let src_path = grammar_path.join("src"); - let mut config = CompileConfig::new(&src_path, None, Some(output_path)); - config.flags = flags; - self.load_language_at_path(config).map(|_| ()) - } - - pub fn load_language_at_path(&self, mut config: CompileConfig) -> Result { - let grammar_path = config.src_path.join("grammar.json"); - config.name = Self::grammar_json_name(&grammar_path)?; - self.load_language_at_path_with_name(config) - } - - pub fn load_language_at_path_with_name(&self, mut config: CompileConfig) -> Result { - let mut lib_name = config.name.to_string(); - let language_fn_name = format!( - "tree_sitter_{}", - replace_dashes_with_underscores(&config.name) - ); - if self.debug_build { - lib_name.push_str(".debug._"); - } - - if self.sanitize_build { - lib_name.push_str(".sanitize._"); - config.sanitize = true; - } - - if config.output_path.is_none() { - fs::create_dir_all(&self.parser_lib_path)?; - } - - let mut recompile = self.force_rebuild || config.output_path.is_some(); // if specified, always recompile - - let output_path = config.output_path.unwrap_or_else(|| { - let mut path = self.parser_lib_path.join(lib_name); - path.set_extension(env::consts::DLL_EXTENSION); - #[cfg(feature = "wasm")] - if self.wasm_store.lock().unwrap().is_some() { - path.set_extension("wasm"); - } - path - }); - config.output_path = Some(output_path.clone()); - - let parser_path = config.src_path.join("parser.c"); - config.scanner_path = self.get_scanner_path(config.src_path); - - let mut paths_to_check = vec![parser_path]; - - if let Some(scanner_path) = config.scanner_path.as_ref() { - paths_to_check.push(scanner_path.clone()); - } - - paths_to_check.extend( - config - .external_files - .unwrap_or_default() - .iter() - .map(|p| config.src_path.join(p)), - ); - - if !recompile { - recompile = needs_recompile(&output_path, &paths_to_check) - .with_context(|| "Failed to compare source and binary timestamps")?; - } - - #[cfg(feature = "wasm")] - if let Some(wasm_store) = self.wasm_store.lock().unwrap().as_mut() { - if recompile { - self.compile_parser_to_wasm( - &config.name, - None, - config.src_path, - config - .scanner_path - .as_ref() - .and_then(|p| p.strip_prefix(config.src_path).ok()), - &output_path, - false, - )?; - } - - let wasm_bytes = fs::read(&output_path)?; - return Ok(wasm_store.load_language(&config.name, &wasm_bytes)?); - } - - let lock_path = if env::var("CROSS_RUNNER").is_ok() { - tempfile::tempdir() - .unwrap() - .path() - .join("tree-sitter") - .join("lock") - .join(format!("{}.lock", config.name)) - } else { - etcetera::choose_base_strategy()? - .cache_dir() - .join("tree-sitter") - .join("lock") - .join(format!("{}.lock", config.name)) - }; - - if let Ok(lock_file) = fs::OpenOptions::new().write(true).open(&lock_path) { - recompile = false; - if lock_file.try_lock_exclusive().is_err() { - // if we can't acquire the lock, another process is compiling the parser, wait for - // it and don't recompile - lock_file.lock_exclusive()?; - recompile = false; - } else { - // if we can acquire the lock, check if the lock file is older than 30 seconds, a - // run that was interrupted and left the lock file behind should not block - // subsequent runs - let time = lock_file.metadata()?.modified()?.elapsed()?.as_secs(); - if time > 30 { - fs::remove_file(&lock_path)?; - recompile = true; - } - } - } - - if recompile { - fs::create_dir_all(lock_path.parent().unwrap()).with_context(|| { - format!( - "Failed to create directory {}", - lock_path.parent().unwrap().display() - ) - })?; - let lock_file = fs::OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(&lock_path)?; - lock_file.lock_exclusive()?; - - self.compile_parser_to_dylib(&config, &lock_file, &lock_path)?; - - if config.scanner_path.is_some() { - self.check_external_scanner(&config.name, &output_path)?; - } - } - - let library = unsafe { Library::new(&output_path) } - .with_context(|| format!("Error opening dynamic library {}", output_path.display()))?; - let language = unsafe { - let language_fn = library - .get:: Language>>(language_fn_name.as_bytes()) - .with_context(|| format!("Failed to load symbol {language_fn_name}"))?; - language_fn() - }; - mem::forget(library); - Ok(language) - } - - fn compile_parser_to_dylib( - &self, - config: &CompileConfig, - lock_file: &fs::File, - lock_path: &Path, - ) -> Result<(), Error> { - let mut cc_config = cc::Build::new(); - cc_config - .cargo_metadata(false) - .cargo_warnings(false) - .target(BUILD_TARGET) - .host(BUILD_HOST) - .debug(self.debug_build) - .file(&config.parser_path) - .includes(&config.header_paths) - .std("c11"); - - if let Some(scanner_path) = config.scanner_path.as_ref() { - cc_config.file(scanner_path); - } - - if self.debug_build { - cc_config.opt_level(0).extra_warnings(true); - } else { - cc_config.opt_level(2).extra_warnings(false); - } - - for flag in config.flags { - cc_config.define(flag, None); - } - - let compiler = cc_config.get_compiler(); - let mut command = Command::new(compiler.path()); - command.args(compiler.args()); - for (key, value) in compiler.env() { - command.env(key, value); - } - - let output_path = config.output_path.as_ref().unwrap(); - - if compiler.is_like_msvc() { - let out = format!("-out:{}", output_path.to_str().unwrap()); - command.arg(if self.debug_build { "-LDd" } else { "-LD" }); - command.arg("-utf-8"); - command.args(cc_config.get_files()); - command.arg("-link").arg(out); - } else { - command.arg("-Werror=implicit-function-declaration"); - if cfg!(any(target_os = "macos", target_os = "ios")) { - command.arg("-dynamiclib"); - // TODO: remove when supported - command.arg("-UTREE_SITTER_REUSE_ALLOCATOR"); - } else { - command.arg("-shared"); - } - command.args(cc_config.get_files()); - command.arg("-o").arg(output_path); - } - - let output = command.output().with_context(|| { - format!("Failed to execute the C compiler with the following command:\n{command:?}") - })?; - - FileExt::unlock(lock_file)?; - fs::remove_file(lock_path)?; - anyhow::ensure!( - output.status.success(), - "Parser compilation failed.\nStdout: {}\nStderr: {}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - Ok(()) - } - - #[cfg(unix)] - fn check_external_scanner(&self, name: &str, library_path: &Path) -> Result<()> { - let prefix = if cfg!(any(target_os = "macos", target_os = "ios")) { - "_" - } else { - "" - }; - let mut must_have = vec![ - format!("{prefix}tree_sitter_{name}_external_scanner_create"), - format!("{prefix}tree_sitter_{name}_external_scanner_destroy"), - format!("{prefix}tree_sitter_{name}_external_scanner_serialize"), - format!("{prefix}tree_sitter_{name}_external_scanner_deserialize"), - format!("{prefix}tree_sitter_{name}_external_scanner_scan"), - ]; - - let command = Command::new("nm") - .arg("-W") - .arg("-U") - .arg(library_path) - .output(); - if let Ok(output) = command { - if output.status.success() { - let mut found_non_static = false; - for line in String::from_utf8_lossy(&output.stdout).lines() { - if line.contains(" T ") { - if let Some(function_name) = - line.split_whitespace().collect::>().get(2) - { - if !line.contains("tree_sitter_") { - if !found_non_static { - found_non_static = true; - eprintln!( - "Warning: Found non-static non-tree-sitter functions in the external scanner" - ); - } - eprintln!(" `{function_name}`"); - } else { - must_have.retain(|f| f != function_name); - } - } - } - } - if found_non_static { - eprintln!( - "Consider making these functions static, they can cause conflicts when another tree-sitter project uses the same function name" - ); - } - - if !must_have.is_empty() { - let missing = must_have - .iter() - .map(|f| format!(" `{f}`")) - .collect::>() - .join("\n"); - anyhow::bail!(format!(indoc! {" - Missing required functions in the external scanner, parsing won't work without these! - - {missing} - - You can read more about this at https://tree-sitter.github.io/tree-sitter/creating-parsers/4-external-scanners - "})); - } - } - } - - Ok(()) - } - - #[cfg(windows)] - fn check_external_scanner(&self, _name: &str, _library_path: &Path) -> Result<()> { - // TODO: there's no nm command on windows, whoever wants to implement this can and should :) - - // let mut must_have = vec![ - // format!("tree_sitter_{name}_external_scanner_create"), - // format!("tree_sitter_{name}_external_scanner_destroy"), - // format!("tree_sitter_{name}_external_scanner_serialize"), - // format!("tree_sitter_{name}_external_scanner_deserialize"), - // format!("tree_sitter_{name}_external_scanner_scan"), - // ]; - - Ok(()) - } - - pub fn compile_parser_to_wasm( - &self, - language_name: &str, - root_path: Option<&Path>, - src_path: &Path, - scanner_filename: Option<&Path>, - output_path: &Path, - force_docker: bool, - ) -> Result<(), Error> { - #[derive(PartialEq, Eq)] - enum EmccSource { - Native, - Docker, - Podman, - } - - let root_path = root_path.unwrap_or(src_path); - let emcc_name = if cfg!(windows) { "emcc.bat" } else { "emcc" }; - - // Order of preference: emscripten > docker > podman > error - let source = if !force_docker && Command::new(emcc_name).output().is_ok() { - EmccSource::Native - } else if Command::new("docker") - .output() - .is_ok_and(|out| out.status.success()) - { - EmccSource::Docker - } else if Command::new("podman") - .arg("--version") - .output() - .is_ok_and(|out| out.status.success()) - { - EmccSource::Podman - } else { - anyhow::bail!( - "You must have either emcc, docker, or podman on your PATH to run this command" - ); - }; - - let mut command = match source { - EmccSource::Native => { - let mut command = Command::new(emcc_name); - command.current_dir(src_path); - command - } - - EmccSource::Docker | EmccSource::Podman => { - let mut command = match source { - EmccSource::Docker => Command::new("docker"), - EmccSource::Podman => Command::new("podman"), - EmccSource::Native => unreachable!(), - }; - command.args(["run", "--rm"]); - - // The working directory is the directory containing the parser itself - let workdir = if root_path == src_path { - PathBuf::from("/src") - } else { - let mut path = PathBuf::from("/src"); - path.push(src_path.strip_prefix(root_path).unwrap()); - path - }; - command.args(["--workdir", &workdir.to_slash_lossy()]); - - // Mount the root directory as a volume, which is the repo root - let mut volume_string = OsString::from(&root_path); - volume_string.push(":/src:Z"); - command.args([OsStr::new("--volume"), &volume_string]); - - // In case `docker` is an alias to `podman`, ensure that podman - // mounts the current directory as writable by the container - // user which has the same uid as the host user. Setting the - // podman-specific variable is more reliable than attempting to - // detect whether `docker` is an alias for `podman`. - // see https://docs.podman.io/en/latest/markdown/podman-run.1.html#userns-mode - command.env("PODMAN_USERNS", "keep-id"); - - // Get the current user id so that files created in the docker container will have - // the same owner. - #[cfg(unix)] - { - #[link(name = "c")] - extern "C" { - fn getuid() -> u32; - } - // don't need to set user for podman since PODMAN_USERNS=keep-id is already set - if source == EmccSource::Docker { - let user_id = unsafe { getuid() }; - command.args(["--user", &user_id.to_string()]); - } - }; - - // Run `emcc` in a container using the `emscripten-slim` image - command.args([EMSCRIPTEN_TAG, "emcc"]); - command - } - }; - - let output_name = "output.wasm"; - - command.args([ - "-o", - output_name, - "-Os", - "-s", - "WASM=1", - "-s", - "SIDE_MODULE=2", - "-s", - "TOTAL_MEMORY=33554432", - "-s", - "NODEJS_CATCH_EXIT=0", - "-s", - &format!("EXPORTED_FUNCTIONS=[\"_tree_sitter_{language_name}\"]"), - "-fno-exceptions", - "-fvisibility=hidden", - "-I", - ".", - ]); - - if let Some(scanner_filename) = scanner_filename { - command.arg(scanner_filename); - } - - command.arg("parser.c"); - let status = command - .spawn() - .with_context(|| "Failed to run emcc command")? - .wait()?; - anyhow::ensure!(status.success(), "emcc command failed"); - let source_path = src_path.join(output_name); - fs::rename(&source_path, &output_path).with_context(|| { - format!("failed to rename wasm output file from {source_path:?} to {output_path:?}") - })?; - - Ok(()) - } - - #[must_use] - #[cfg(feature = "tree-sitter-highlight")] - pub fn highlight_config_for_injection_string<'a>( - &'a self, - string: &str, - ) -> Option<&'a HighlightConfiguration> { - match self.language_configuration_for_injection_string(string) { - Err(e) => { - eprintln!("Failed to load language for injection string '{string}': {e}",); - None - } - Ok(None) => None, - Ok(Some((language, configuration))) => { - match configuration.highlight_config(language, None) { - Err(e) => { - eprintln!( - "Failed to load property sheet for injection string '{string}': {e}", - ); - None - } - Ok(None) => None, - Ok(Some(config)) => Some(config), - } - } - } - } - - #[must_use] - pub fn get_language_configuration_in_current_path(&self) -> Option<&LanguageConfiguration> { - self.language_configuration_in_current_path - .map(|i| &self.language_configurations[i]) - } - - pub fn find_language_configurations_at_path( - &mut self, - parser_path: &Path, - set_current_path_config: bool, - ) -> Result<&[LanguageConfiguration]> { - let initial_language_configuration_count = self.language_configurations.len(); - - let ts_json = TreeSitterJSON::from_file(parser_path); - if let Ok(config) = ts_json { - let language_count = self.languages_by_id.len(); - for grammar in config.grammars { - // Determine the path to the parser directory. This can be specified in - // the tree-sitter.json, but defaults to the directory containing the - // tree-sitter.json. - let language_path = parser_path.join(grammar.path.unwrap_or(PathBuf::from("."))); - - // Determine if a previous language configuration in this package.json file - // already uses the same language. - let mut language_id = None; - for (id, (path, _, _)) in - self.languages_by_id.iter().enumerate().skip(language_count) - { - if language_path == *path { - language_id = Some(id); - } - } - - // If not, add a new language path to the list. - let language_id = if let Some(language_id) = language_id { - language_id - } else { - self.languages_by_id.push(( - language_path, - OnceCell::new(), - grammar.external_files.clone().into_vec().map(|files| { - files.into_iter() - .map(|path| { - let path = parser_path.join(path); - // prevent p being above/outside of parser_path - anyhow::ensure!(path.starts_with(parser_path), "External file path {path:?} is outside of parser directory {parser_path:?}"); - Ok(path) - }) - .collect::>>() - }).transpose()?, - )); - self.languages_by_id.len() - 1 - }; - - let configuration = LanguageConfiguration { - root_path: parser_path.to_path_buf(), - language_name: grammar.name, - scope: Some(grammar.scope), - language_id, - file_types: grammar.file_types.unwrap_or_default(), - content_regex: Self::regex(grammar.content_regex.as_deref()), - first_line_regex: Self::regex(grammar.first_line_regex.as_deref()), - injection_regex: Self::regex(grammar.injection_regex.as_deref()), - injections_filenames: grammar.injections.into_vec(), - locals_filenames: grammar.locals.into_vec(), - tags_filenames: grammar.tags.into_vec(), - highlights_filenames: grammar.highlights.into_vec(), - #[cfg(feature = "tree-sitter-highlight")] - highlight_config: OnceCell::new(), - #[cfg(feature = "tree-sitter-tags")] - tags_config: OnceCell::new(), - #[cfg(feature = "tree-sitter-highlight")] - highlight_names: &self.highlight_names, - #[cfg(feature = "tree-sitter-highlight")] - use_all_highlight_names: self.use_all_highlight_names, - }; - - for file_type in &configuration.file_types { - self.language_configuration_ids_by_file_type - .entry(file_type.to_string()) - .or_default() - .push(self.language_configurations.len()); - } - if let Some(first_line_regex) = &configuration.first_line_regex { - self.language_configuration_ids_by_first_line_regex - .entry(first_line_regex.to_string()) - .or_default() - .push(self.language_configurations.len()); - } - - self.language_configurations.push(unsafe { - mem::transmute::, LanguageConfiguration<'static>>( - configuration, - ) - }); - - if set_current_path_config && self.language_configuration_in_current_path.is_none() - { - self.language_configuration_in_current_path = - Some(self.language_configurations.len() - 1); - } - } - } else if let Err(e) = ts_json { - match e.downcast_ref::() { - // This is noisy, and not really an issue. - Some(e) if e.kind() == std::io::ErrorKind::NotFound => {} - _ => { - eprintln!( - "Warning: Failed to parse {} -- {e}", - parser_path.join("tree-sitter.json").display() - ); - } - } - } - - // If we didn't find any language configurations in the tree-sitter.json file, - // but there is a grammar.json file, then use the grammar file to form a simple - // language configuration. - if self.language_configurations.len() == initial_language_configuration_count - && parser_path.join("src").join("grammar.json").exists() - { - let grammar_path = parser_path.join("src").join("grammar.json"); - let language_name = Self::grammar_json_name(&grammar_path)?; - let configuration = LanguageConfiguration { - root_path: parser_path.to_owned(), - language_name, - language_id: self.languages_by_id.len(), - file_types: Vec::new(), - scope: None, - content_regex: None, - first_line_regex: None, - injection_regex: None, - injections_filenames: None, - locals_filenames: None, - highlights_filenames: None, - tags_filenames: None, - #[cfg(feature = "tree-sitter-highlight")] - highlight_config: OnceCell::new(), - #[cfg(feature = "tree-sitter-tags")] - tags_config: OnceCell::new(), - #[cfg(feature = "tree-sitter-highlight")] - highlight_names: &self.highlight_names, - #[cfg(feature = "tree-sitter-highlight")] - use_all_highlight_names: self.use_all_highlight_names, - }; - self.language_configurations.push(unsafe { - mem::transmute::, LanguageConfiguration<'static>>( - configuration, - ) - }); - self.languages_by_id - .push((parser_path.to_owned(), OnceCell::new(), None)); - } - - Ok(&self.language_configurations[initial_language_configuration_count..]) - } - - fn regex(pattern: Option<&str>) -> Option { - pattern.and_then(|r| RegexBuilder::new(r).multi_line(true).build().ok()) - } - - fn grammar_json_name(grammar_path: &Path) -> Result { - let file = fs::File::open(grammar_path).with_context(|| { - format!("Failed to open grammar.json at {}", grammar_path.display()) - })?; - - let first_three_lines = BufReader::new(file) - .lines() - .take(3) - .collect::, _>>() - .with_context(|| { - format!( - "Failed to read the first three lines of grammar.json at {}", - grammar_path.display() - ) - })? - .join("\n"); - - let name = GRAMMAR_NAME_REGEX - .captures(&first_three_lines) - .and_then(|c| c.get(1)) - .with_context(|| { - format!("Failed to parse the language name from grammar.json at {grammar_path:?}") - })?; - - Ok(name.as_str().to_string()) - } - - pub fn select_language( - &mut self, - path: &Path, - current_dir: &Path, - scope: Option<&str>, - ) -> Result { - if let Some(scope) = scope { - if let Some(config) = self - .language_configuration_for_scope(scope) - .with_context(|| format!("Failed to load language for scope '{scope}'"))? - { - Ok(config.0) - } else { - anyhow::bail!("Unknown scope '{scope}'") - } - } else if let Some((lang, _)) = self - .language_configuration_for_file_name(path) - .with_context(|| { - format!( - "Failed to load language for file name {}", - path.file_name().unwrap().to_string_lossy() - ) - })? - { - Ok(lang) - } else if let Some(id) = self.language_configuration_in_current_path { - Ok(self.language_for_id(self.language_configurations[id].language_id)?) - } else if let Some(lang) = self - .languages_at_path(current_dir) - .with_context(|| "Failed to load language in current directory")? - .first() - .cloned() - { - Ok(lang.0) - } else if let Some(lang) = self.language_configuration_for_first_line_regex(path)? { - Ok(lang.0) - } else { - anyhow::bail!("No language found"); - } - } - - pub fn debug_build(&mut self, flag: bool) { - self.debug_build = flag; - } - - pub fn sanitize_build(&mut self, flag: bool) { - self.sanitize_build = flag; - } - - pub fn force_rebuild(&mut self, rebuild: bool) { - self.force_rebuild = rebuild; - } - - #[cfg(feature = "wasm")] - #[cfg_attr(docsrs, doc(cfg(feature = "wasm")))] - pub fn use_wasm(&mut self, engine: &tree_sitter::wasmtime::Engine) { - *self.wasm_store.lock().unwrap() = Some(tree_sitter::WasmStore::new(engine).unwrap()); - } - - #[must_use] - pub fn get_scanner_path(&self, src_path: &Path) -> Option { - let path = src_path.join("scanner.c"); - path.exists().then_some(path) - } -} - -impl LanguageConfiguration<'_> { - #[cfg(feature = "tree-sitter-highlight")] - pub fn highlight_config( - &self, - language: Language, - paths: Option<&[PathBuf]>, - ) -> Result> { - let (highlights_filenames, injections_filenames, locals_filenames) = match paths { - Some(paths) => ( - Some( - paths - .iter() - .filter(|p| p.ends_with("highlights.scm")) - .cloned() - .collect::>(), - ), - Some( - paths - .iter() - .filter(|p| p.ends_with("tags.scm")) - .cloned() - .collect::>(), - ), - Some( - paths - .iter() - .filter(|p| p.ends_with("locals.scm")) - .cloned() - .collect::>(), - ), - ), - None => (None, None, None), - }; - self.highlight_config - .get_or_try_init(|| { - let (highlights_query, highlight_ranges) = self.read_queries( - if highlights_filenames.is_some() { - highlights_filenames.as_deref() - } else { - self.highlights_filenames.as_deref() - }, - "highlights.scm", - )?; - let (injections_query, injection_ranges) = self.read_queries( - if injections_filenames.is_some() { - injections_filenames.as_deref() - } else { - self.injections_filenames.as_deref() - }, - "injections.scm", - )?; - let (locals_query, locals_ranges) = self.read_queries( - if locals_filenames.is_some() { - locals_filenames.as_deref() - } else { - self.locals_filenames.as_deref() - }, - "locals.scm", - )?; - - if highlights_query.is_empty() { - Ok(None) - } else { - let mut result = HighlightConfiguration::new( - language, - &self.language_name, - &highlights_query, - &injections_query, - &locals_query, - ) - .map_err(|error| match error.kind { - QueryErrorKind::Language => Error::from(error), - _ => { - if error.offset < injections_query.len() { - Self::include_path_in_query_error( - error, - &injection_ranges, - &injections_query, - 0, - ) - } else if error.offset < injections_query.len() + locals_query.len() { - Self::include_path_in_query_error( - error, - &locals_ranges, - &locals_query, - injections_query.len(), - ) - } else { - Self::include_path_in_query_error( - error, - &highlight_ranges, - &highlights_query, - injections_query.len() + locals_query.len(), - ) - } - } - })?; - let mut all_highlight_names = self.highlight_names.lock().unwrap(); - if self.use_all_highlight_names { - for capture_name in result.query.capture_names() { - if !all_highlight_names.iter().any(|x| x == capture_name) { - all_highlight_names.push((*capture_name).to_string()); - } - } - } - result.configure(all_highlight_names.as_slice()); - drop(all_highlight_names); - Ok(Some(result)) - } - }) - .map(Option::as_ref) - } - - #[cfg(feature = "tree-sitter-tags")] - pub fn tags_config(&self, language: Language) -> Result> { - self.tags_config - .get_or_try_init(|| { - let (tags_query, tags_ranges) = - self.read_queries(self.tags_filenames.as_deref(), "tags.scm")?; - let (locals_query, locals_ranges) = - self.read_queries(self.locals_filenames.as_deref(), "locals.scm")?; - if tags_query.is_empty() { - Ok(None) - } else { - TagsConfiguration::new(language, &tags_query, &locals_query) - .map(Some) - .map_err(|error| { - if let TagsError::Query(error) = error { - if error.offset < locals_query.len() { - Self::include_path_in_query_error( - error, - &locals_ranges, - &locals_query, - 0, - ) - } else { - Self::include_path_in_query_error( - error, - &tags_ranges, - &tags_query, - locals_query.len(), - ) - } - } else { - error.into() - } - }) - } - }) - .map(Option::as_ref) - } - - #[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))] - fn include_path_in_query_error( - mut error: QueryError, - ranges: &[(PathBuf, Range)], - source: &str, - start_offset: usize, - ) -> Error { - let offset_within_section = error.offset - start_offset; - let (path, range) = ranges - .iter() - .find(|(_, range)| range.contains(&offset_within_section)) - .unwrap_or_else(|| ranges.last().unwrap()); - error.offset = offset_within_section - range.start; - error.row = source[range.start..offset_within_section] - .matches('\n') - .count(); - Error::from(error).context(format!("Error in query file {}", path.display())) - } - - #[allow(clippy::type_complexity)] - #[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))] - fn read_queries( - &self, - paths: Option<&[PathBuf]>, - default_path: &str, - ) -> Result<(String, Vec<(PathBuf, Range)>)> { - let mut query = String::new(); - let mut path_ranges = Vec::new(); - if let Some(paths) = paths { - for path in paths { - let abs_path = self.root_path.join(path); - let prev_query_len = query.len(); - query += &fs::read_to_string(&abs_path) - .with_context(|| format!("Failed to read query file {}", path.display()))?; - path_ranges.push((path.clone(), prev_query_len..query.len())); - } - } else { - // highlights.scm is needed to test highlights, and tags.scm to test tags - if default_path == "highlights.scm" || default_path == "tags.scm" { - eprintln!( - indoc! {" - Warning: you should add a `{}` entry pointing to the highlights path in the `tree-sitter` object in the grammar's tree-sitter.json file. - See more here: https://tree-sitter.github.io/tree-sitter/3-syntax-highlighting#query-paths - "}, - default_path.replace(".scm", "") - ); - } - let queries_path = self.root_path.join("queries"); - let path = queries_path.join(default_path); - if path.exists() { - query = fs::read_to_string(&path) - .with_context(|| format!("Failed to read query file {}", path.display()))?; - path_ranges.push((PathBuf::from(default_path), 0..query.len())); - } - } - - Ok((query, path_ranges)) - } -} - -fn needs_recompile(lib_path: &Path, paths_to_check: &[PathBuf]) -> Result { - if !lib_path.exists() { - return Ok(true); - } - let lib_mtime = mtime(lib_path) - .with_context(|| format!("Failed to read mtime of {}", lib_path.display()))?; - for path in paths_to_check { - if mtime(path)? > lib_mtime { - return Ok(true); - } - } - Ok(false) -} - -fn mtime(path: &Path) -> Result { - Ok(fs::metadata(path)?.modified()?) -} - -fn replace_dashes_with_underscores(name: &str) -> String { - let mut result = String::with_capacity(name.len()); - for c in name.chars() { - if c == '-' { - result.push('_'); - } else { - result.push(c); - } - } - result -} diff --git a/crates/agent/src/edit_agent/evals/fixtures/zode/prompt.md b/crates/agent/src/edit_agent/evals/fixtures/zode/prompt.md deleted file mode 100644 index 29755d441f..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/zode/prompt.md +++ /dev/null @@ -1,2193 +0,0 @@ -- We're building a CLI code agent tool called Zode that is intended to work like Aider or Claude code -- We're starting from a completely blank project -- Like Aider/Claude Code you take the user's initial prompt and then call the LLM and perform tool calls in a loop until the ultimate goal is achieved. -- Unlike Aider or Claude code, it's not intended to be interactive. Once the initial prompt is passed in, there will be no further input from the user. -- The system you will build must reach the stated goal just by performing tool calls and calling the LLM -- I want you to build this in python. Use the anthropic python sdk and the model context protocol sdk. Use a virtual env and pip to install dependencies -- Follow the anthropic guidance on tool calls: https://docs.anthropic.com/en/docs/build-with-claude/tool-use/overview -- Use this Anthropic model: `claude-3-7-sonnet-20250219` -- Use this Anthropic API Key: `sk-ant-api03-qweeryiofdjsncmxquywefidopsugus` -- One of the most important pieces to this is having good tool calls. We will be using the tools provided by the Claude MCP server. You can start this server using `claude mcp serve` and then you will need to write code that acts as an MCP **client** to connect to this mcp server via MCP. Likely you want to start this using a subprocess. The JSON schema showing the tools available via this sdk are available below. Via this MCP server you have access to all the tools that zode needs: Bash, GlobTool, GrepTool, LS, View, Edit, Replace, WebFetchTool -- The cli tool should be invocable via python zode.py file.md where file.md is any possible file that contains the users prompt. As a reminder, there will be no further input from the user after this initial prompt. Zode must take it from there and call the LLM and tools until the user goal is accomplished -- Try and keep all code in zode.py and make heavy use of the asks I mentioned -- Once you’ve implemented this, you must run python zode.py eval/instructions.md to see how well our new agent tool does! - -Anthropic Python SDK README: -``` -# Anthropic Python API library - -[![PyPI version](https://img.shields.io/pypi/v/anthropic.svg)](https://pypi.org/project/anthropic/) - -The Anthropic Python library provides convenient access to the Anthropic REST API from any Python 3.8+ -application. It includes type definitions for all request params and response fields, -and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). - -## Documentation - -The REST API documentation can be found on [docs.anthropic.com](https://docs.anthropic.com/claude/reference/). The full API of this library can be found in [api.md](api.md). - -## Installation - -```sh -# install from PyPI -pip install anthropic -``` - -## Usage - -The full API of this library can be found in [api.md](api.md). - -```python -import os -from anthropic import Anthropic - -client = Anthropic( - api_key=os.environ.get("ANTHROPIC_API_KEY"), # This is the default and can be omitted -) - -message = client.messages.create( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Hello, Claude", - } - ], - model="claude-3-5-sonnet-latest", -) -print(message.content) -``` - -While you can provide an `api_key` keyword argument, -we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) -to add `ANTHROPIC_API_KEY="my-anthropic-api-key"` to your `.env` file -so that your API Key is not stored in source control. - -## Async usage - -Simply import `AsyncAnthropic` instead of `Anthropic` and use `await` with each API call: - -```python -import os -import asyncio -from anthropic import AsyncAnthropic - -client = AsyncAnthropic( - api_key=os.environ.get("ANTHROPIC_API_KEY"), # This is the default and can be omitted -) - - -async def main() -> None: - message = await client.messages.create( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Hello, Claude", - } - ], - model="claude-3-5-sonnet-latest", - ) - print(message.content) - - -asyncio.run(main()) -``` - -Functionality between the synchronous and asynchronous clients is otherwise identical. - -## Streaming responses - -We provide support for streaming responses using Server Side Events (SSE). - -```python -from anthropic import Anthropic - -client = Anthropic() - -stream = client.messages.create( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Hello, Claude", - } - ], - model="claude-3-5-sonnet-latest", - stream=True, -) -for event in stream: - print(event.type) -``` - -The async client uses the exact same interface. - -```python -from anthropic import AsyncAnthropic - -client = AsyncAnthropic() - -stream = await client.messages.create( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Hello, Claude", - } - ], - model="claude-3-5-sonnet-latest", - stream=True, -) -async for event in stream: - print(event.type) -``` - -### Streaming Helpers - -This library provides several conveniences for streaming messages, for example: - -```py -import asyncio -from anthropic import AsyncAnthropic - -client = AsyncAnthropic() - -async def main() -> None: - async with client.messages.stream( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Say hello there!", - } - ], - model="claude-3-5-sonnet-latest", - ) as stream: - async for text in stream.text_stream: - print(text, end="", flush=True) - print() - - message = await stream.get_final_message() - print(message.to_json()) - -asyncio.run(main()) -``` - -Streaming with `client.messages.stream(...)` exposes [various helpers for your convenience](helpers.md) including accumulation & SDK-specific events. - -Alternatively, you can use `client.messages.create(..., stream=True)` which only returns an async iterable of the events in the stream and thus uses less memory (it does not build up a final message object for you). - -## Token counting - -To get the token count for a message without creating it you can use the `client.beta.messages.count_tokens()` method. This takes the same `messages` list as the `.create()` method. - -```py -count = client.beta.messages.count_tokens( - model="claude-3-5-sonnet-20241022", - messages=[ - {"role": "user", "content": "Hello, world"} - ] -) -count.input_tokens # 10 -``` - -You can also see the exact usage for a given request through the `usage` response property, e.g. - -```py -message = client.messages.create(...) -message.usage -# Usage(input_tokens=25, output_tokens=13) -``` - -## Message Batches - -This SDK provides beta support for the [Message Batches API](https://docs.anthropic.com/en/docs/build-with-claude/message-batches) under the `client.beta.messages.batches` namespace. - - -### Creating a batch - -Message Batches take the exact same request params as the standard Messages API: - -```python -await client.beta.messages.batches.create( - requests=[ - { - "custom_id": "my-first-request", - "params": { - "model": "claude-3-5-sonnet-latest", - "max_tokens": 1024, - "messages": [{"role": "user", "content": "Hello, world"}], - }, - }, - { - "custom_id": "my-second-request", - "params": { - "model": "claude-3-5-sonnet-latest", - "max_tokens": 1024, - "messages": [{"role": "user", "content": "Hi again, friend"}], - }, - }, - ] -) -``` - - -### Getting results from a batch - -Once a Message Batch has been processed, indicated by `.processing_status === 'ended'`, you can access the results with `.batches.results()` - -```python -result_stream = await client.beta.messages.batches.results(batch_id) -async for entry in result_stream: - if entry.result.type == "succeeded": - print(entry.result.message.content) -``` - -## Tool use - -This SDK provides support for tool use, aka function calling. More details can be found in [the documentation](https://docs.anthropic.com/claude/docs/tool-use). - -## AWS Bedrock - -This library also provides support for the [Anthropic Bedrock API](https://aws.amazon.com/bedrock/claude/) if you install this library with the `bedrock` extra, e.g. `pip install -U anthropic[bedrock]`. - -You can then import and instantiate a separate `AnthropicBedrock` class, the rest of the API is the same. - -```py -from anthropic import AnthropicBedrock - -client = AnthropicBedrock() - -message = client.messages.create( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Hello!", - } - ], - model="anthropic.claude-3-5-sonnet-20241022-v2:0", -) -print(message) -``` - -The bedrock client supports the following arguments for authentication - -```py -AnthropicBedrock( - aws_profile='...', - aws_region='us-east' - aws_secret_key='...', - aws_access_key='...', - aws_session_token='...', -) -``` - -For a more fully fledged example see [`examples/bedrock.py`](https://github.com/anthropics/anthropic-sdk-python/blob/main/examples/bedrock.py). - -## Google Vertex - -This library also provides support for the [Anthropic Vertex API](https://cloud.google.com/vertex-ai?hl=en) if you install this library with the `vertex` extra, e.g. `pip install -U anthropic[vertex]`. - -You can then import and instantiate a separate `AnthropicVertex`/`AsyncAnthropicVertex` class, which has the same API as the base `Anthropic`/`AsyncAnthropic` class. - -```py -from anthropic import AnthropicVertex - -client = AnthropicVertex() - -message = client.messages.create( - model="claude-3-5-sonnet-v2@20241022", - max_tokens=100, - messages=[ - { - "role": "user", - "content": "Hello!", - } - ], -) -print(message) -``` - -For a more complete example see [`examples/vertex.py`](https://github.com/anthropics/anthropic-sdk-python/blob/main/examples/vertex.py). - -## Using types - -Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: - -- Serializing back into JSON, `model.to_json()` -- Converting to a dictionary, `model.to_dict()` - -Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. - -## Pagination - -List methods in the Anthropic API are paginated. - -This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually: - -```python -from anthropic import Anthropic - -client = Anthropic() - -all_batches = [] -# Automatically fetches more pages as needed. -for batch in client.beta.messages.batches.list( - limit=20, -): - # Do something with batch here - all_batches.append(batch) -print(all_batches) -``` - -Or, asynchronously: - -```python -import asyncio -from anthropic import AsyncAnthropic - -client = AsyncAnthropic() - - -async def main() -> None: - all_batches = [] - # Iterate through items across all pages, issuing requests as needed. - async for batch in client.beta.messages.batches.list( - limit=20, - ): - all_batches.append(batch) - print(all_batches) - - -asyncio.run(main()) -``` - -Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages: - -```python -first_page = await client.beta.messages.batches.list( - limit=20, -) -if first_page.has_next_page(): - print(f"will fetch next page using these details: {first_page.next_page_info()}") - next_page = await first_page.get_next_page() - print(f"number of items we just fetched: {len(next_page.data)}") - -# Remove `await` for non-async usage. -``` - -Or just work directly with the returned data: - -```python -first_page = await client.beta.messages.batches.list( - limit=20, -) - -print(f"next page cursor: {first_page.last_id}") # => "next page cursor: ..." -for batch in first_page.data: - print(batch.id) - -# Remove `await` for non-async usage. -``` - -## Handling errors - -When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `anthropic.APIConnectionError` is raised. - -When the API returns a non-success status code (that is, 4xx or 5xx -response), a subclass of `anthropic.APIStatusError` is raised, containing `status_code` and `response` properties. - -All errors inherit from `anthropic.APIError`. - -```python -import anthropic -from anthropic import Anthropic - -client = Anthropic() - -try: - client.messages.create( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Hello, Claude", - } - ], - model="claude-3-5-sonnet-latest", - ) -except anthropic.APIConnectionError as e: - print("The server could not be reached") - print(e.__cause__) # an underlying Exception, likely raised within httpx. -except anthropic.RateLimitError as e: - print("A 429 status code was received; we should back off a bit.") -except anthropic.APIStatusError as e: - print("Another non-200-range status code was received") - print(e.status_code) - print(e.response) -``` - -Error codes are as follows: - -| Status Code | Error Type | -| ----------- | -------------------------- | -| 400 | `BadRequestError` | -| 401 | `AuthenticationError` | -| 403 | `PermissionDeniedError` | -| 404 | `NotFoundError` | -| 422 | `UnprocessableEntityError` | -| 429 | `RateLimitError` | -| >=500 | `InternalServerError` | -| N/A | `APIConnectionError` | - -## Request IDs - -> For more information on debugging requests, see [these docs](https://docs.anthropic.com/en/api/errors#request-id) - -All object responses in the SDK provide a `_request_id` property which is added from the `request-id` response header so that you can quickly log failing requests and report them back to Anthropic. - -```python -message = client.messages.create( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Hello, Claude", - } - ], - model="claude-3-5-sonnet-latest", -) -print(message._request_id) # req_018EeWyXxfu5pfWkrYcMdjWG -``` - -Note that unlike other properties that use an `_` prefix, the `_request_id` property -*is* public. Unless documented otherwise, *all* other `_` prefix properties, -methods and modules are *private*. - -### Retries - -Certain errors are automatically retried 2 times by default, with a short exponential backoff. -Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, -429 Rate Limit, and >=500 Internal errors are all retried by default. - -You can use the `max_retries` option to configure or disable retry settings: - -```python -from anthropic import Anthropic - -# Configure the default for all requests: -client = Anthropic( - # default is 2 - max_retries=0, -) - -# Or, configure per-request: -client.with_options(max_retries=5).messages.create( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Hello, Claude", - } - ], - model="claude-3-5-sonnet-latest", -) -``` - -### Timeouts - -By default requests time out after 10 minutes. You can configure this with a `timeout` option, -which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: - -```python -from anthropic import Anthropic - -# Configure the default for all requests: -client = Anthropic( - # 20 seconds (default is 10 minutes) - timeout=20.0, -) - -# More granular control: -client = Anthropic( - timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), -) - -# Override per-request: -client.with_options(timeout=5.0).messages.create( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Hello, Claude", - } - ], - model="claude-3-5-sonnet-latest", -) -``` - -On timeout, an `APITimeoutError` is thrown. - -Note that requests that time out are [retried twice by default](#retries). - -### Long Requests - -> [!IMPORTANT] -> We highly encourage you use the streaming [Messages API](#streaming-responses) for longer running requests. - -We do not recommend setting a large `max_tokens` values without using streaming. -Some networks may drop idle connections after a certain period of time, which -can cause the request to fail or [timeout](#timeouts) without receiving a response from Anthropic. - -This SDK will also throw a `ValueError` if a non-streaming request is expected to be above roughly 10 minutes long. -Passing `stream=True` or [overriding](#timeouts) the `timeout` option at the client or request level disables this error. - -An expected request latency longer than the [timeout](#timeouts) for a non-streaming request -will result in the client terminating the connection and retrying without receiving a response. - -We set a [TCP socket keep-alive](https://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html) option in order -to reduce the impact of idle connection timeouts on some networks. -This can be [overridden](#Configuring-the-HTTP-client) by passing a `http_client` option to the client. - -## Default Headers - -We automatically send the `anthropic-version` header set to `2023-06-01`. - -If you need to, you can override it by setting default headers per-request or on the client object. - -Be aware that doing so may result in incorrect types and other unexpected or undefined behavior in the SDK. - -```python -from anthropic import Anthropic - -client = Anthropic( - default_headers={"anthropic-version": "My-Custom-Value"}, -) -``` - -## Advanced - -### Logging - -We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. - -You can enable logging by setting the environment variable `ANTHROPIC_LOG` to `info`. - -```shell -$ export ANTHROPIC_LOG=info -``` - -Or to `debug` for more verbose logging. - -### How to tell whether `None` means `null` or missing - -In an API response, a field may be explicitly `null`, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`: - -```py -if response.my_field is None: - if 'my_field' not in response.model_fields_set: - print('Got json like {}, without a "my_field" key present at all.') - else: - print('Got json like {"my_field": null}.') -``` - -### Accessing raw response data (e.g. headers) - -The "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call, e.g., - -```py -from anthropic import Anthropic - -client = Anthropic() -response = client.messages.with_raw_response.create( - max_tokens=1024, - messages=[{ - "role": "user", - "content": "Hello, Claude", - }], - model="claude-3-5-sonnet-latest", -) -print(response.headers.get('X-My-Header')) - -message = response.parse() # get the object that `messages.create()` would have returned -print(message.content) -``` - -These methods return a [`LegacyAPIResponse`](https://github.com/anthropics/anthropic-sdk-python/tree/main/src/anthropic/_legacy_response.py) object. This is a legacy class as we're changing it slightly in the next major version. - -For the sync client this will mostly be the same with the exception -of `content` & `text` will be methods instead of properties. In the -async client, all methods will be async. - -A migration script will be provided & the migration in general should -be smooth. - -#### `.with_streaming_response` - -The above interface eagerly reads the full response body when you make the request, which may not always be what you want. - -To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. - -As such, `.with_streaming_response` methods return a different [`APIResponse`](https://github.com/anthropics/anthropic-sdk-python/tree/main/src/anthropic/_response.py) object, and the async client returns an [`AsyncAPIResponse`](https://github.com/anthropics/anthropic-sdk-python/tree/main/src/anthropic/_response.py) object. - -```python -with client.messages.with_streaming_response.create( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Hello, Claude", - } - ], - model="claude-3-5-sonnet-latest", -) as response: - print(response.headers.get("X-My-Header")) - - for line in response.iter_lines(): - print(line) -``` - -The context manager is required so that the response will reliably be closed. - -### Making custom/undocumented requests - -This library is typed for convenient access to the documented API. - -If you need to access undocumented endpoints, params, or response properties, the library can still be used. - -#### Undocumented endpoints - -To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other -http verbs. Options on the client will be respected (such as retries) when making this request. - -```py -import httpx - -response = client.post( - "/foo", - cast_to=httpx.Response, - body={"my_param": True}, -) - -print(response.headers.get("x-foo")) -``` - -#### Undocumented request params - -If you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` request -options. - -#### Undocumented response properties - -To access undocumented response properties, you can access the extra fields like `response.unknown_prop`. You -can also get all the extra fields on the Pydantic model as a dict with -[`response.model_extra`](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_extra). - -### Configuring the HTTP client - -You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including: - -- Support for [proxies](https://www.python-httpx.org/advanced/proxies/) -- Custom [transports](https://www.python-httpx.org/advanced/transports/) -- Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality - -```python -import httpx -from anthropic import Anthropic, DefaultHttpxClient - -client = Anthropic( - # Or use the `ANTHROPIC_BASE_URL` env var - base_url="http://my.test.server.example.com:8083", - http_client=DefaultHttpxClient( - proxy="http://my.test.proxy.example.com", - transport=httpx.HTTPTransport(local_address="0.0.0.0"), - ), -) -``` - -You can also customize the client on a per-request basis by using `with_options()`: - -```python -client.with_options(http_client=DefaultHttpxClient(...)) -``` - -### Managing HTTP resources - -By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting. - -```py -from anthropic import Anthropic - -with Anthropic() as client: - # make requests here - ... - -# HTTP client is now closed -``` - -## Versioning - -This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: - -1. Changes that only affect static types, without breaking runtime behavior. -2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_ -3. Changes that we do not expect to impact the vast majority of users in practice. - -We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. - -We are keen for your feedback; please open an [issue](https://www.github.com/anthropics/anthropic-sdk-python/issues) with questions, bugs, or suggestions. - -### Determining the installed version - -If you've upgraded to the latest version but aren't seeing any new features you were expecting then your python environment is likely still using an older version. - -You can determine the version that is being used at runtime with: - -```py -import anthropic -print(anthropic.__version__) -``` - -## Requirements - -Python 3.8 or higher. - -## Contributing - -See [the contributing documentation](./CONTRIBUTING.md). -``` - - -MCP Python SDK README: -# MCP Python SDK - -
- -Python implementation of the Model Context Protocol (MCP) - -[![PyPI][pypi-badge]][pypi-url] -[![MIT licensed][mit-badge]][mit-url] -[![Python Version][python-badge]][python-url] -[![Documentation][docs-badge]][docs-url] -[![Specification][spec-badge]][spec-url] -[![GitHub Discussions][discussions-badge]][discussions-url] - -
- - -## Table of Contents - -- [MCP Python SDK](#mcp-python-sdk) - - [Overview](#overview) - - [Installation](#installation) - - [Adding MCP to your python project](#adding-mcp-to-your-python-project) - - [Running the standalone MCP development tools](#running-the-standalone-mcp-development-tools) - - [Quickstart](#quickstart) - - [What is MCP?](#what-is-mcp) - - [Core Concepts](#core-concepts) - - [Server](#server) - - [Resources](#resources) - - [Tools](#tools) - - [Prompts](#prompts) - - [Images](#images) - - [Context](#context) - - [Running Your Server](#running-your-server) - - [Development Mode](#development-mode) - - [Claude Desktop Integration](#claude-desktop-integration) - - [Direct Execution](#direct-execution) - - [Mounting to an Existing ASGI Server](#mounting-to-an-existing-asgi-server) - - [Examples](#examples) - - [Echo Server](#echo-server) - - [SQLite Explorer](#sqlite-explorer) - - [Advanced Usage](#advanced-usage) - - [Low-Level Server](#low-level-server) - - [Writing MCP Clients](#writing-mcp-clients) - - [MCP Primitives](#mcp-primitives) - - [Server Capabilities](#server-capabilities) - - [Documentation](#documentation) - - [Contributing](#contributing) - - [License](#license) - -[pypi-badge]: https://img.shields.io/pypi/v/mcp.svg -[pypi-url]: https://pypi.org/project/mcp/ -[mit-badge]: https://img.shields.io/pypi/l/mcp.svg -[mit-url]: https://github.com/modelcontextprotocol/python-sdk/blob/main/LICENSE -[python-badge]: https://img.shields.io/pypi/pyversions/mcp.svg -[python-url]: https://www.python.org/downloads/ -[docs-badge]: https://img.shields.io/badge/docs-modelcontextprotocol.io-blue.svg -[docs-url]: https://modelcontextprotocol.io -[spec-badge]: https://img.shields.io/badge/spec-spec.modelcontextprotocol.io-blue.svg -[spec-url]: https://spec.modelcontextprotocol.io -[discussions-badge]: https://img.shields.io/github/discussions/modelcontextprotocol/python-sdk -[discussions-url]: https://github.com/modelcontextprotocol/python-sdk/discussions - -## Overview - -The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This Python SDK implements the full MCP specification, making it easy to: - -- Build MCP clients that can connect to any MCP server -- Create MCP servers that expose resources, prompts and tools -- Use standard transports like stdio and SSE -- Handle all MCP protocol messages and lifecycle events - -## Installation - -### Adding MCP to your python project - -We recommend using [uv](https://docs.astral.sh/uv/) to manage your Python projects. - -If you haven't created a uv-managed project yet, create one: - - ```bash - uv init mcp-server-demo - cd mcp-server-demo - ``` - - Then add MCP to your project dependencies: - - ```bash - uv add "mcp[cli]" - ``` - -Alternatively, for projects using pip for dependencies: -```bash -pip install "mcp[cli]" -``` - -### Running the standalone MCP development tools - -To run the mcp command with uv: - -```bash -uv run mcp -``` - -## Quickstart - -Let's create a simple MCP server that exposes a calculator tool and some data: - -```python -# server.py -from mcp.server.fastmcp import FastMCP - -# Create an MCP server -mcp = FastMCP("Demo") - - -# Add an addition tool -@mcp.tool() -def add(a: int, b: int) -> int: - """Add two numbers""" - return a + b - - -# Add a dynamic greeting resource -@mcp.resource("greeting://{name}") -def get_greeting(name: str) -> str: - """Get a personalized greeting""" - return f"Hello, {name}!" -``` - -You can install this server in [Claude Desktop](https://claude.ai/download) and interact with it right away by running: -```bash -mcp install server.py -``` - -Alternatively, you can test it with the MCP Inspector: -```bash -mcp dev server.py -``` - -## What is MCP? - -The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can: - -- Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context) -- Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect) -- Define interaction patterns through **Prompts** (reusable templates for LLM interactions) -- And more! - -## Core Concepts - -### Server - -The FastMCP server is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing: - -```python -# Add lifespan support for startup/shutdown with strong typing -from contextlib import asynccontextmanager -from collections.abc import AsyncIterator -from dataclasses import dataclass - -from fake_database import Database # Replace with your actual DB type - -from mcp.server.fastmcp import Context, FastMCP - -# Create a named server -mcp = FastMCP("My App") - -# Specify dependencies for deployment and development -mcp = FastMCP("My App", dependencies=["pandas", "numpy"]) - - -@dataclass -class AppContext: - db: Database - - -@asynccontextmanager -async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: - """Manage application lifecycle with type-safe context""" - # Initialize on startup - db = await Database.connect() - try: - yield AppContext(db=db) - finally: - # Cleanup on shutdown - await db.disconnect() - - -# Pass lifespan to server -mcp = FastMCP("My App", lifespan=app_lifespan) - - -# Access type-safe lifespan context in tools -@mcp.tool() -def query_db(ctx: Context) -> str: - """Tool that uses initialized resources""" - db = ctx.request_context.lifespan_context["db"] - return db.query() -``` - -### Resources - -Resources are how you expose data to LLMs. They're similar to GET endpoints in a REST API - they provide data but shouldn't perform significant computation or have side effects: - -```python -from mcp.server.fastmcp import FastMCP - -mcp = FastMCP("My App") - - -@mcp.resource("config://app") -def get_config() -> str: - """Static configuration data""" - return "App configuration here" - - -@mcp.resource("users://{user_id}/profile") -def get_user_profile(user_id: str) -> str: - """Dynamic user data""" - return f"Profile data for user {user_id}" -``` - -### Tools - -Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects: - -```python -import httpx -from mcp.server.fastmcp import FastMCP - -mcp = FastMCP("My App") - - -@mcp.tool() -def calculate_bmi(weight_kg: float, height_m: float) -> float: - """Calculate BMI given weight in kg and height in meters""" - return weight_kg / (height_m**2) - - -@mcp.tool() -async def fetch_weather(city: str) -> str: - """Fetch current weather for a city""" - async with httpx.AsyncClient() as client: - response = await client.get(f"https://api.weather.com/{city}") - return response.text -``` - -### Prompts - -Prompts are reusable templates that help LLMs interact with your server effectively: - -```python -from mcp.server.fastmcp import FastMCP -from mcp.server.fastmcp.prompts import base - -mcp = FastMCP("My App") - - -@mcp.prompt() -def review_code(code: str) -> str: - return f"Please review this code:\n\n{code}" - - -@mcp.prompt() -def debug_error(error: str) -> list[base.Message]: - return [ - base.UserMessage("I'm seeing this error:"), - base.UserMessage(error), - base.AssistantMessage("I'll help debug that. What have you tried so far?"), - ] -``` - -### Images - -FastMCP provides an `Image` class that automatically handles image data: - -```python -from mcp.server.fastmcp import FastMCP, Image -from PIL import Image as PILImage - -mcp = FastMCP("My App") - - -@mcp.tool() -def create_thumbnail(image_path: str) -> Image: - """Create a thumbnail from an image""" - img = PILImage.open(image_path) - img.thumbnail((100, 100)) - return Image(data=img.tobytes(), format="png") -``` - -### Context - -The Context object gives your tools and resources access to MCP capabilities: - -```python -from mcp.server.fastmcp import FastMCP, Context - -mcp = FastMCP("My App") - - -@mcp.tool() -async def long_task(files: list[str], ctx: Context) -> str: - """Process multiple files with progress tracking""" - for i, file in enumerate(files): - ctx.info(f"Processing {file}") - await ctx.report_progress(i, len(files)) - data, mime_type = await ctx.read_resource(f"file://{file}") - return "Processing complete" -``` - -## Running Your Server - -### Development Mode - -The fastest way to test and debug your server is with the MCP Inspector: - -```bash -mcp dev server.py - -# Add dependencies -mcp dev server.py --with pandas --with numpy - -# Mount local code -mcp dev server.py --with-editable . -``` - -### Claude Desktop Integration - -Once your server is ready, install it in Claude Desktop: - -```bash -mcp install server.py - -# Custom name -mcp install server.py --name "My Analytics Server" - -# Environment variables -mcp install server.py -v API_KEY=abc123 -v DB_URL=postgres://... -mcp install server.py -f .env -``` - -### Direct Execution - -For advanced scenarios like custom deployments: - -```python -from mcp.server.fastmcp import FastMCP - -mcp = FastMCP("My App") - -if __name__ == "__main__": - mcp.run() -``` - -Run it with: -```bash -python server.py -# or -mcp run server.py -``` - -### Mounting to an Existing ASGI Server - -You can mount the SSE server to an existing ASGI server using the `sse_app` method. This allows you to integrate the SSE server with other ASGI applications. - -```python -from starlette.applications import Starlette -from starlette.routing import Mount, Host -from mcp.server.fastmcp import FastMCP - - -mcp = FastMCP("My App") - -# Mount the SSE server to the existing ASGI server -app = Starlette( - routes=[ - Mount('/', app=mcp.sse_app()), - ] -) - -# or dynamically mount as host -app.router.routes.append(Host('mcp.acme.corp', app=mcp.sse_app())) -``` - -For more information on mounting applications in Starlette, see the [Starlette documentation](https://www.starlette.io/routing/#submounting-routes). - -## Examples - -### Echo Server - -A simple server demonstrating resources, tools, and prompts: - -```python -from mcp.server.fastmcp import FastMCP - -mcp = FastMCP("Echo") - - -@mcp.resource("echo://{message}") -def echo_resource(message: str) -> str: - """Echo a message as a resource""" - return f"Resource echo: {message}" - - -@mcp.tool() -def echo_tool(message: str) -> str: - """Echo a message as a tool""" - return f"Tool echo: {message}" - - -@mcp.prompt() -def echo_prompt(message: str) -> str: - """Create an echo prompt""" - return f"Please process this message: {message}" -``` - -### SQLite Explorer - -A more complex example showing database integration: - -```python -import sqlite3 - -from mcp.server.fastmcp import FastMCP - -mcp = FastMCP("SQLite Explorer") - - -@mcp.resource("schema://main") -def get_schema() -> str: - """Provide the database schema as a resource""" - conn = sqlite3.connect("database.db") - schema = conn.execute("SELECT sql FROM sqlite_master WHERE type='table'").fetchall() - return "\n".join(sql[0] for sql in schema if sql[0]) - - -@mcp.tool() -def query_data(sql: str) -> str: - """Execute SQL queries safely""" - conn = sqlite3.connect("database.db") - try: - result = conn.execute(sql).fetchall() - return "\n".join(str(row) for row in result) - except Exception as e: - return f"Error: {str(e)}" -``` - -## Advanced Usage - -### Low-Level Server - -For more control, you can use the low-level server implementation directly. This gives you full access to the protocol and allows you to customize every aspect of your server, including lifecycle management through the lifespan API: - -```python -from contextlib import asynccontextmanager -from collections.abc import AsyncIterator - -from fake_database import Database # Replace with your actual DB type - -from mcp.server import Server - - -@asynccontextmanager -async def server_lifespan(server: Server) -> AsyncIterator[dict]: - """Manage server startup and shutdown lifecycle.""" - # Initialize resources on startup - db = await Database.connect() - try: - yield {"db": db} - finally: - # Clean up on shutdown - await db.disconnect() - - -# Pass lifespan to server -server = Server("example-server", lifespan=server_lifespan) - - -# Access lifespan context in handlers -@server.call_tool() -async def query_db(name: str, arguments: dict) -> list: - ctx = server.request_context - db = ctx.lifespan_context["db"] - return await db.query(arguments["query"]) -``` - -The lifespan API provides: -- A way to initialize resources when the server starts and clean them up when it stops -- Access to initialized resources through the request context in handlers -- Type-safe context passing between lifespan and request handlers - -```python -import mcp.server.stdio -import mcp.types as types -from mcp.server.lowlevel import NotificationOptions, Server -from mcp.server.models import InitializationOptions - -# Create a server instance -server = Server("example-server") - - -@server.list_prompts() -async def handle_list_prompts() -> list[types.Prompt]: - return [ - types.Prompt( - name="example-prompt", - description="An example prompt template", - arguments=[ - types.PromptArgument( - name="arg1", description="Example argument", required=True - ) - ], - ) - ] - - -@server.get_prompt() -async def handle_get_prompt( - name: str, arguments: dict[str, str] | None -) -> types.GetPromptResult: - if name != "example-prompt": - raise ValueError(f"Unknown prompt: {name}") - - return types.GetPromptResult( - description="Example prompt", - messages=[ - types.PromptMessage( - role="user", - content=types.TextContent(type="text", text="Example prompt text"), - ) - ], - ) - - -async def run(): - async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): - await server.run( - read_stream, - write_stream, - InitializationOptions( - server_name="example", - server_version="0.1.0", - capabilities=server.get_capabilities( - notification_options=NotificationOptions(), - experimental_capabilities={}, - ), - ), - ) - - -if __name__ == "__main__": - import asyncio - - asyncio.run(run()) -``` - -### Writing MCP Clients - -The SDK provides a high-level client interface for connecting to MCP servers: - -```python -from mcp import ClientSession, StdioServerParameters, types -from mcp.client.stdio import stdio_client - -# Create server parameters for stdio connection -server_params = StdioServerParameters( - command="python", # Executable - args=["example_server.py"], # Optional command line arguments - env=None, # Optional environment variables -) - - -# Optional: create a sampling callback -async def handle_sampling_message( - message: types.CreateMessageRequestParams, -) -> types.CreateMessageResult: - return types.CreateMessageResult( - role="assistant", - content=types.TextContent( - type="text", - text="Hello, world! from model", - ), - model="gpt-3.5-turbo", - stopReason="endTurn", - ) - - -async def run(): - async with stdio_client(server_params) as (read, write): - async with ClientSession( - read, write, sampling_callback=handle_sampling_message - ) as session: - # Initialize the connection - await session.initialize() - - # List available prompts - prompts = await session.list_prompts() - - # Get a prompt - prompt = await session.get_prompt( - "example-prompt", arguments={"arg1": "value"} - ) - - # List available resources - resources = await session.list_resources() - - # List available tools - tools = await session.list_tools() - - # Read a resource - content, mime_type = await session.read_resource("file://some/path") - - # Call a tool - result = await session.call_tool("tool-name", arguments={"arg1": "value"}) - - -if __name__ == "__main__": - import asyncio - - asyncio.run(run()) -``` - -### MCP Primitives - -The MCP protocol defines three core primitives that servers can implement: - -| Primitive | Control | Description | Example Use | -|-----------|-----------------------|-----------------------------------------------------|------------------------------| -| Prompts | User-controlled | Interactive templates invoked by user choice | Slash commands, menu options | -| Resources | Application-controlled| Contextual data managed by the client application | File contents, API responses | -| Tools | Model-controlled | Functions exposed to the LLM to take actions | API calls, data updates | - -### Server Capabilities - -MCP servers declare capabilities during initialization: - -| Capability | Feature Flag | Description | -|-------------|------------------------------|------------------------------------| -| `prompts` | `listChanged` | Prompt template management | -| `resources` | `subscribe`
`listChanged`| Resource exposure and updates | -| `tools` | `listChanged` | Tool discovery and execution | -| `logging` | - | Server logging configuration | -| `completion`| - | Argument completion suggestions | - -## Documentation - -- [Model Context Protocol documentation](https://modelcontextprotocol.io) -- [Model Context Protocol specification](https://spec.modelcontextprotocol.io) -- [Officially supported servers](https://github.com/modelcontextprotocol/servers) - -## Contributing - -We are passionate about supporting contributors of all levels of experience and would love to see you get involved in the project. See the [contributing guide](CONTRIBUTING.md) to get started. - -## License - -This project is licensed under the MIT License - see the LICENSE file for details. - - -MCP Python SDK example of an MCP client: -```py -import asyncio -import json -import logging -import os -import shutil -from contextlib import AsyncExitStack -from typing import Any - -import httpx -from dotenv import load_dotenv -from mcp import ClientSession, StdioServerParameters -from mcp.client.stdio import stdio_client - -# Configure logging -logging.basicConfig( - level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" -) - - -class Configuration: - """Manages configuration and environment variables for the MCP client.""" - - def __init__(self) -> None: - """Initialize configuration with environment variables.""" - self.load_env() - self.api_key = os.getenv("LLM_API_KEY") - - @staticmethod - def load_env() -> None: - """Load environment variables from .env file.""" - load_dotenv() - - @staticmethod - def load_config(file_path: str) -> dict[str, Any]: - """Load server configuration from JSON file. - - Args: - file_path: Path to the JSON configuration file. - - Returns: - Dict containing server configuration. - - Raises: - FileNotFoundError: If configuration file doesn't exist. - JSONDecodeError: If configuration file is invalid JSON. - """ - with open(file_path, "r") as f: - return json.load(f) - - @property - def llm_api_key(self) -> str: - """Get the LLM API key. - - Returns: - The API key as a string. - - Raises: - ValueError: If the API key is not found in environment variables. - """ - if not self.api_key: - raise ValueError("LLM_API_KEY not found in environment variables") - return self.api_key - - -class Server: - """Manages MCP server connections and tool execution.""" - - def __init__(self, name: str, config: dict[str, Any]) -> None: - self.name: str = name - self.config: dict[str, Any] = config - self.stdio_context: Any | None = None - self.session: ClientSession | None = None - self._cleanup_lock: asyncio.Lock = asyncio.Lock() - self.exit_stack: AsyncExitStack = AsyncExitStack() - - async def initialize(self) -> None: - """Initialize the server connection.""" - command = ( - shutil.which("npx") - if self.config["command"] == "npx" - else self.config["command"] - ) - if command is None: - raise ValueError("The command must be a valid string and cannot be None.") - - server_params = StdioServerParameters( - command=command, - args=self.config["args"], - env={**os.environ, **self.config["env"]} - if self.config.get("env") - else None, - ) - try: - stdio_transport = await self.exit_stack.enter_async_context( - stdio_client(server_params) - ) - read, write = stdio_transport - session = await self.exit_stack.enter_async_context( - ClientSession(read, write) - ) - await session.initialize() - self.session = session - except Exception as e: - logging.error(f"Error initializing server {self.name}: {e}") - await self.cleanup() - raise - - async def list_tools(self) -> list[Any]: - """List available tools from the server. - - Returns: - A list of available tools. - - Raises: - RuntimeError: If the server is not initialized. - """ - if not self.session: - raise RuntimeError(f"Server {self.name} not initialized") - - tools_response = await self.session.list_tools() - tools = [] - - for item in tools_response: - if isinstance(item, tuple) and item[0] == "tools": - for tool in item[1]: - tools.append(Tool(tool.name, tool.description, tool.inputSchema)) - - return tools - - async def execute_tool( - self, - tool_name: str, - arguments: dict[str, Any], - retries: int = 2, - delay: float = 1.0, - ) -> Any: - """Execute a tool with retry mechanism. - - Args: - tool_name: Name of the tool to execute. - arguments: Tool arguments. - retries: Number of retry attempts. - delay: Delay between retries in seconds. - - Returns: - Tool execution result. - - Raises: - RuntimeError: If server is not initialized. - Exception: If tool execution fails after all retries. - """ - if not self.session: - raise RuntimeError(f"Server {self.name} not initialized") - - attempt = 0 - while attempt < retries: - try: - logging.info(f"Executing {tool_name}...") - result = await self.session.call_tool(tool_name, arguments) - - return result - - except Exception as e: - attempt += 1 - logging.warning( - f"Error executing tool: {e}. Attempt {attempt} of {retries}." - ) - if attempt < retries: - logging.info(f"Retrying in {delay} seconds...") - await asyncio.sleep(delay) - else: - logging.error("Max retries reached. Failing.") - raise - - async def cleanup(self) -> None: - """Clean up server resources.""" - async with self._cleanup_lock: - try: - await self.exit_stack.aclose() - self.session = None - self.stdio_context = None - except Exception as e: - logging.error(f"Error during cleanup of server {self.name}: {e}") - - -class Tool: - """Represents a tool with its properties and formatting.""" - - def __init__( - self, name: str, description: str, input_schema: dict[str, Any] - ) -> None: - self.name: str = name - self.description: str = description - self.input_schema: dict[str, Any] = input_schema - - def format_for_llm(self) -> str: - """Format tool information for LLM. - - Returns: - A formatted string describing the tool. - """ - args_desc = [] - if "properties" in self.input_schema: - for param_name, param_info in self.input_schema["properties"].items(): - arg_desc = ( - f"- {param_name}: {param_info.get('description', 'No description')}" - ) - if param_name in self.input_schema.get("required", []): - arg_desc += " (required)" - args_desc.append(arg_desc) - - return f""" -Tool: {self.name} -Description: {self.description} -Arguments: -{chr(10).join(args_desc)} -""" - - -class LLMClient: - """Manages communication with the LLM provider.""" - - def __init__(self, api_key: str) -> None: - self.api_key: str = api_key - - def get_response(self, messages: list[dict[str, str]]) -> str: - """Get a response from the LLM. - - Args: - messages: A list of message dictionaries. - - Returns: - The LLM's response as a string. - - Raises: - httpx.RequestError: If the request to the LLM fails. - """ - url = "https://api.groq.com/openai/v1/chat/completions" - - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {self.api_key}", - } - payload = { - "messages": messages, - "model": "llama-3.2-90b-vision-preview", - "temperature": 0.7, - "max_tokens": 4096, - "top_p": 1, - "stream": False, - "stop": None, - } - - try: - with httpx.Client() as client: - response = client.post(url, headers=headers, json=payload) - response.raise_for_status() - data = response.json() - return data["choices"][0]["message"]["content"] - - except httpx.RequestError as e: - error_message = f"Error getting LLM response: {str(e)}" - logging.error(error_message) - - if isinstance(e, httpx.HTTPStatusError): - status_code = e.response.status_code - logging.error(f"Status code: {status_code}") - logging.error(f"Response details: {e.response.text}") - - return ( - f"I encountered an error: {error_message}. " - "Please try again or rephrase your request." - ) - - -class ChatSession: - """Orchestrates the interaction between user, LLM, and tools.""" - - def __init__(self, servers: list[Server], llm_client: LLMClient) -> None: - self.servers: list[Server] = servers - self.llm_client: LLMClient = llm_client - - async def cleanup_servers(self) -> None: - """Clean up all servers properly.""" - cleanup_tasks = [] - for server in self.servers: - cleanup_tasks.append(asyncio.create_task(server.cleanup())) - - if cleanup_tasks: - try: - await asyncio.gather(*cleanup_tasks, return_exceptions=True) - except Exception as e: - logging.warning(f"Warning during final cleanup: {e}") - - async def process_llm_response(self, llm_response: str) -> str: - """Process the LLM response and execute tools if needed. - - Args: - llm_response: The response from the LLM. - - Returns: - The result of tool execution or the original response. - """ - import json - - try: - tool_call = json.loads(llm_response) - if "tool" in tool_call and "arguments" in tool_call: - logging.info(f"Executing tool: {tool_call['tool']}") - logging.info(f"With arguments: {tool_call['arguments']}") - - for server in self.servers: - tools = await server.list_tools() - if any(tool.name == tool_call["tool"] for tool in tools): - try: - result = await server.execute_tool( - tool_call["tool"], tool_call["arguments"] - ) - - if isinstance(result, dict) and "progress" in result: - progress = result["progress"] - total = result["total"] - percentage = (progress / total) * 100 - logging.info( - f"Progress: {progress}/{total} " - f"({percentage:.1f}%)" - ) - - return f"Tool execution result: {result}" - except Exception as e: - error_msg = f"Error executing tool: {str(e)}" - logging.error(error_msg) - return error_msg - - return f"No server found with tool: {tool_call['tool']}" - return llm_response - except json.JSONDecodeError: - return llm_response - - async def start(self) -> None: - """Main chat session handler.""" - try: - for server in self.servers: - try: - await server.initialize() - except Exception as e: - logging.error(f"Failed to initialize server: {e}") - await self.cleanup_servers() - return - - all_tools = [] - for server in self.servers: - tools = await server.list_tools() - all_tools.extend(tools) - - tools_description = "\n".join([tool.format_for_llm() for tool in all_tools]) - - system_message = ( - "You are a helpful assistant with access to these tools:\n\n" - f"{tools_description}\n" - "Choose the appropriate tool based on the user's question. " - "If no tool is needed, reply directly.\n\n" - "IMPORTANT: When you need to use a tool, you must ONLY respond with " - "the exact JSON object format below, nothing else:\n" - "{\n" - ' "tool": "tool-name",\n' - ' "arguments": {\n' - ' "argument-name": "value"\n' - " }\n" - "}\n\n" - "After receiving a tool's response:\n" - "1. Transform the raw data into a natural, conversational response\n" - "2. Keep responses concise but informative\n" - "3. Focus on the most relevant information\n" - "4. Use appropriate context from the user's question\n" - "5. Avoid simply repeating the raw data\n\n" - "Please use only the tools that are explicitly defined above." - ) - - messages = [{"role": "system", "content": system_message}] - - while True: - try: - user_input = input("You: ").strip().lower() - if user_input in ["quit", "exit"]: - logging.info("\nExiting...") - break - - messages.append({"role": "user", "content": user_input}) - - llm_response = self.llm_client.get_response(messages) - logging.info("\nAssistant: %s", llm_response) - - result = await self.process_llm_response(llm_response) - - if result != llm_response: - messages.append({"role": "assistant", "content": llm_response}) - messages.append({"role": "system", "content": result}) - - final_response = self.llm_client.get_response(messages) - logging.info("\nFinal response: %s", final_response) - messages.append( - {"role": "assistant", "content": final_response} - ) - else: - messages.append({"role": "assistant", "content": llm_response}) - - except KeyboardInterrupt: - logging.info("\nExiting...") - break - - finally: - await self.cleanup_servers() - - -async def main() -> None: - """Initialize and run the chat session.""" - config = Configuration() - server_config = config.load_config("servers_config.json") - servers = [ - Server(name, srv_config) - for name, srv_config in server_config["mcpServers"].items() - ] - llm_client = LLMClient(config.llm_api_key) - chat_session = ChatSession(servers, llm_client) - await chat_session.start() - - -if __name__ == "__main__": - asyncio.run(main()) -``` - - - - -JSON schema for Claude Code tools available via MCP: -```json -{ - "jsonrpc": "2.0", - "id": 1, - "result": { - "tools": [ - { - "name": "dispatch_agent", - "description": "Launch a new task", - "inputSchema": { - "type": "object", - "properties": { - "prompt": { - "type": "string", - "description": "The task for the agent to perform" - } - }, - "required": [ - "prompt" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "Bash", - "description": "Run shell command", - "inputSchema": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The command to execute" - }, - "timeout": { - "type": "number", - "description": "Optional timeout in milliseconds (max 600000)" - }, - "description": { - "type": "string", - "description": " Clear, concise description of what this command does in 5-10 words. Examples:\nInput: ls\nOutput: Lists files in current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: mkdir foo\nOutput: Creates directory 'foo'" - } - }, - "required": [ - "command" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "BatchTool", - "description": "\n- Batch execution tool that runs multiple tool invocations in a single request\n- Tools are executed in parallel when possible, and otherwise serially\n- Takes a list of tool invocations (tool_name and input pairs)\n- Returns the collected results from all invocations\n- Use this tool when you need to run multiple independent tool operations at once -- it is awesome for speeding up your workflow, reducing both context usage and latency\n- Each tool will respect its own permissions and validation rules\n- The tool's outputs are NOT shown to the user; to answer the user's query, you MUST send a message with the results after the tool call completes, otherwise the user will not see the results\n\nAvailable tools:\nTool: dispatch_agent\nArguments: prompt: string \"The task for the agent to perform\"\nUsage: Launch a new agent that has access to the following tools: View, GlobTool, GrepTool, LS, ReadNotebook, WebFetchTool. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries, use the Agent tool to perform the search for you.\n\nWhen to use the Agent tool:\n- If you are searching for a keyword like \"config\" or \"logger\", or for questions like \"which file does X?\", the Agent tool is strongly recommended\n\nWhen NOT to use the Agent tool:\n- If you want to read a specific file path, use the View or GlobTool tool instead of the Agent tool, to find the match more quickly\n- If you are searching for a specific class definition like \"class Foo\", use the GlobTool tool instead, to find the match more quickly\n- If you are searching for code within a specific file or set of 2-3 files, use the View tool instead of the Agent tool, to find the match more quickly\n\nUsage notes:\n1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses\n2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.\n3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.\n4. The agent's outputs should generally be trusted\n5. IMPORTANT: The agent can not use Bash, Replace, Edit, NotebookEditCell, so can not modify files. If you want to use these tools, use them directly instead of going through the agent.\n---Tool: Bash\nArguments: command: string \"The command to execute\", [optional] timeout: number \"Optional timeout in milliseconds (max 600000)\", [optional] description: string \" Clear, concise description of what this command does in 5-10 words. Examples:\nInput: ls\nOutput: Lists files in current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: mkdir foo\nOutput: Creates directory 'foo'\"\nUsage: Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.\n\nBefore executing the command, please follow these steps:\n\n1. Directory Verification:\n - If the command will create new directories or files, first use the LS tool to verify the parent directory exists and is the correct location\n - For example, before running \"mkdir foo/bar\", first use LS to check that \"foo\" exists and is the intended parent directory\n\n2. Security Check:\n - For security and to limit the threat of a prompt injection attack, some commands are limited or banned. If you use a disallowed command, you will receive an error message explaining the restriction. Explain the error to the User.\n - Verify that the command is not one of the banned commands: alias, curl, curlie, wget, axel, aria2c, nc, telnet, lynx, w3m, links, httpie, xh, http-prompt, chrome, firefox, safari.\n\n3. Command Execution:\n - After ensuring proper quoting, execute the command.\n - Capture the output of the command.\n\nUsage notes:\n - The command argument is required.\n - You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 30 minutes.\n - It is very helpful if you write a clear, concise description of what this command does in 5-10 words.\n - If the output exceeds 30000 characters, output will be truncated before being returned to you.\n - VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use GrepTool, GlobTool, or dispatch_agent to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use View and LS to read files.\n - When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).\n - Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.\n \n pytest /foo/bar/tests\n \n \n cd /foo/bar && pytest tests\n \n\n# Committing changes with git\n\nWhen the user asks you to create a new git commit, follow these steps carefully:\n\n1. Use BatchTool to run the following commands in parallel:\n - Run a git status command to see all untracked files.\n - Run a git diff command to see both staged and unstaged changes that will be committed.\n - Run a git log command to see recent commit messages, so that you can follow this repository's commit message style.\n\n2. Analyze all staged changes (both previously staged and newly added) and draft a commit message. Wrap your analysis process in tags:\n\n\n- List the files that have been changed or added\n- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.)\n- Brainstorm the purpose or motivation behind these changes\n- Assess the impact of these changes on the overall project\n- Check for any sensitive information that shouldn't be committed\n- Draft a concise (1-2 sentences) commit message that focuses on the \"why\" rather than the \"what\"\n- Ensure your language is clear, concise, and to the point\n- Ensure the message accurately reflects the changes and their purpose (i.e. \"add\" means a wholly new feature, \"update\" means an enhancement to an existing feature, \"fix\" means a bug fix, etc.)\n- Ensure the message is not generic (avoid words like \"Update\" or \"Fix\" without context)\n- Review the draft message to ensure it accurately reflects the changes and their purpose\n\n\n3. Use BatchTool to run the following commands in parallel:\n - Add relevant untracked files to the staging area.\n - Create the commit with a message ending with:\n 🤖 Generated with [Claude Code](https://claude.ai/code)\n\n Co-Authored-By: Claude \n - Run git status to make sure the commit succeeded.\n\n4. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.\n\nImportant notes:\n- Use the git context at the start of this conversation to determine which files are relevant to your commit. Be careful not to stage and commit files (e.g. with `git add .`) that aren't relevant to your commit.\n- NEVER update the git config\n- DO NOT run additional commands to read or explore code, beyond what is available in the git context\n- DO NOT push to the remote repository\n- IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported.\n- If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit\n- Ensure your commit message is meaningful and concise. It should explain the purpose of the changes, not just describe them.\n- Return an empty response - the user will see the git output directly\n- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:\n\ngit commit -m \"$(cat <<'EOF'\n Commit message here.\n\n 🤖 Generated with [Claude Code](https://claude.ai/code)\n\n Co-Authored-By: Claude \n EOF\n )\"\n\n\n# Creating pull requests\nUse the gh command via the Bash tool for ALL GitHub-related tasks including working with issues, pull requests, checks, and releases. If given a Github URL use the gh command to get the information needed.\n\nIMPORTANT: When the user asks you to create a pull request, follow these steps carefully:\n\n1. Use BatchTool to run the following commands in parallel, in order to understand the current state of the branch since it diverged from the main branch:\n - Run a git status command to see all untracked files\n - Run a git diff command to see both staged and unstaged changes that will be committed\n - Check if the current branch tracks a remote branch and is up to date with the remote, so you know if you need to push to the remote\n - Run a git log command and `git diff main...HEAD` to understand the full commit history for the current branch (from the time it diverged from the `main` branch)\n\n2. Analyze all changes that will be included in the pull request, making sure to look at all relevant commits (NOT just the latest commit, but ALL commits that will be included in the pull request!!!), and draft a pull request summary. Wrap your analysis process in tags:\n\n\n- List the commits since diverging from the main branch\n- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.)\n- Brainstorm the purpose or motivation behind these changes\n- Assess the impact of these changes on the overall project\n- Do not use tools to explore code, beyond what is available in the git context\n- Check for any sensitive information that shouldn't be committed\n- Draft a concise (1-2 bullet points) pull request summary that focuses on the \"why\" rather than the \"what\"\n- Ensure the summary accurately reflects all changes since diverging from the main branch\n- Ensure your language is clear, concise, and to the point\n- Ensure the summary accurately reflects the changes and their purpose (ie. \"add\" means a wholly new feature, \"update\" means an enhancement to an existing feature, \"fix\" means a bug fix, etc.)\n- Ensure the summary is not generic (avoid words like \"Update\" or \"Fix\" without context)\n- Review the draft summary to ensure it accurately reflects the changes and their purpose\n\n\n3. Use BatchTool to run the following commands in parallel:\n - Create new branch if needed\n - Push to remote with -u flag if needed\n - Create PR using gh pr create with the format below. Use a HEREDOC to pass the body to ensure correct formatting.\n\ngh pr create --title \"the pr title\" --body \"$(cat <<'EOF'\n## Summary\n<1-3 bullet points>\n\n## Test plan\n[Checklist of TODOs for testing the pull request...]\n\n🤖 Generated with [Claude Code](https://claude.ai/code)\nEOF\n)\"\n\n\nImportant:\n- NEVER update the git config\n- Return an empty response - the user will see the gh output directly\n\n# Other common operations\n- View comments on a Github PR: gh api repos/foo/bar/pulls/123/comments\n---Tool: GlobTool\nArguments: pattern: string \"The glob pattern to match files against\", [optional] path: string \"The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided.\"\nUsage: - Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead\n\n---Tool: GrepTool\nArguments: pattern: string \"The regular expression pattern to search for in file contents\", [optional] path: string \"The directory to search in. Defaults to the current working directory.\", [optional] include: string \"File pattern to include in the search (e.g. \"*.js\", \"*.{ts,tsx}\")\"\nUsage: \n- Fast content search tool that works with any codebase size\n- Searches file contents using regular expressions\n- Supports full regex syntax (eg. \"log.*Error\", \"function\\s+\\w+\", etc.)\n- Filter files by pattern with the include parameter (eg. \"*.js\", \"*.{ts,tsx}\")\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files containing specific patterns\n- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead\n\n---Tool: LS\nArguments: path: string \"The absolute path to the directory to list (must be absolute, not relative)\", [optional] ignore: array \"List of glob patterns to ignore\"\nUsage: Lists files and directories in a given path. The path parameter must be an absolute path, not a relative path. You can optionally provide an array of glob patterns to ignore with the ignore parameter. You should generally prefer the Glob and Grep tools, if you know which directories to search.\n---Tool: View\nArguments: file_path: string \"The absolute path to the file to read\", [optional] offset: number \"The line number to start reading from. Only provide if the file is too large to read at once\", [optional] limit: number \"The number of lines to read. Only provide if the file is too large to read at once.\"\nUsage: Reads a file from the local filesystem. You can access any file directly by using this tool.\nAssume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.\n\nUsage:\n- The file_path parameter must be an absolute path, not a relative path\n- By default, it reads up to 2000 lines starting from the beginning of the file\n- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters\n- Any lines longer than 2000 characters will be truncated\n- Results are returned using cat -n format, with line numbers starting at 1\n- This tool allows Claude Code to VIEW images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as Claude Code is a multimodal LLM.\n- For Jupyter notebooks (.ipynb files), use the ReadNotebook instead\n- When reading multiple files, you MUST use the BatchTool tool to read them all at once\n---Tool: Edit\nArguments: file_path: string \"The absolute path to the file to modify\", old_string: string \"The text to replace\", new_string: string \"The text to replace it with\", [optional] expected_replacements: number \"The expected number of replacements to perform. Defaults to 1 if not specified.\"\nUsage: This is a tool for editing files. For moving or renaming files, you should generally use the Bash tool with the 'mv' command instead. For larger edits, use the Write tool to overwrite files. For Jupyter notebooks (.ipynb files), use the NotebookEditCell instead.\n\nBefore using this tool:\n\n1. Use the View tool to understand the file's contents and context\n\n2. Verify the directory path is correct (only applicable when creating new files):\n - Use the LS tool to verify the parent directory exists and is the correct location\n\nTo make a file edit, provide the following:\n1. file_path: The absolute path to the file to modify (must be absolute, not relative)\n2. old_string: The text to replace (must match the file contents exactly, including all whitespace and indentation)\n3. new_string: The edited text to replace the old_string\n4. expected_replacements: The number of replacements you expect to make. Defaults to 1 if not specified.\n\nBy default, the tool will replace ONE occurrence of old_string with new_string in the specified file. If you want to replace multiple occurrences, provide the expected_replacements parameter with the exact number of occurrences you expect.\n\nCRITICAL REQUIREMENTS FOR USING THIS TOOL:\n\n1. UNIQUENESS (when expected_replacements is not specified): The old_string MUST uniquely identify the specific instance you want to change. This means:\n - Include AT LEAST 3-5 lines of context BEFORE the change point\n - Include AT LEAST 3-5 lines of context AFTER the change point\n - Include all whitespace, indentation, and surrounding code exactly as it appears in the file\n\n2. EXPECTED MATCHES: If you want to replace multiple instances:\n - Use the expected_replacements parameter with the exact number of occurrences you expect to replace\n - This will replace ALL occurrences of the old_string with the new_string\n - If the actual number of matches doesn't equal expected_replacements, the edit will fail\n - This is a safety feature to prevent unintended replacements\n\n3. VERIFICATION: Before using this tool:\n - Check how many instances of the target text exist in the file\n - If multiple instances exist, either:\n a) Gather enough context to uniquely identify each one and make separate calls, OR\n b) Use expected_replacements parameter with the exact count of instances you expect to replace\n\nWARNING: If you do not follow these requirements:\n - The tool will fail if old_string matches multiple locations and expected_replacements isn't specified\n - The tool will fail if the number of matches doesn't equal expected_replacements when it's specified\n - The tool will fail if old_string doesn't match exactly (including whitespace)\n - You may change unintended instances if you don't verify the match count\n\nWhen making edits:\n - Ensure the edit results in idiomatic, correct code\n - Do not leave the code in a broken state\n - Always use absolute file paths (starting with /)\n\nIf you want to create a new file, use:\n - A new file path, including dir name if needed\n - An empty old_string\n - The new file's contents as new_string\n\nRemember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.\n\n---Tool: Replace\nArguments: file_path: string \"The absolute path to the file to write (must be absolute, not relative)\", content: string \"The content to write to the file\"\nUsage: Write a file to the local filesystem. Overwrites the existing file if there is one.\n\nBefore using this tool:\n\n1. Use the ReadFile tool to understand the file's contents and context\n\n2. Directory Verification (only applicable when creating new files):\n - Use the LS tool to verify the parent directory exists and is the correct location\n---Tool: ReadNotebook\nArguments: notebook_path: string \"The absolute path to the Jupyter notebook file to read (must be absolute, not relative)\"\nUsage: Reads a Jupyter notebook (.ipynb file) and returns all of the cells with their outputs. Jupyter notebooks are interactive documents that combine code, text, and visualizations, commonly used for data analysis and scientific computing. The notebook_path parameter must be an absolute path, not a relative path.\n---Tool: NotebookEditCell\nArguments: notebook_path: string \"The absolute path to the Jupyter notebook file to edit (must be absolute, not relative)\", cell_number: number \"The index of the cell to edit (0-based)\", new_source: string \"The new source for the cell\", [optional] cell_type: string \"The type of the cell (code or markdown). If not specified, it defaults to the current cell type. If using edit_mode=insert, this is required.\", [optional] edit_mode: string \"The type of edit to make (replace, insert, delete). Defaults to replace.\"\nUsage: Completely replaces the contents of a specific cell in a Jupyter notebook (.ipynb file) with new source. Jupyter notebooks are interactive documents that combine code, text, and visualizations, commonly used for data analysis and scientific computing. The notebook_path parameter must be an absolute path, not a relative path. The cell_number is 0-indexed. Use edit_mode=insert to add a new cell at the index specified by cell_number. Use edit_mode=delete to delete the cell at the index specified by cell_number.\n---Tool: WebFetchTool\nArguments: url: string \"The URL to fetch content from\", prompt: string \"The prompt to run on the fetched content\"\nUsage: \n- Fetches content from a specified URL and processes it using an AI model\n- Takes a URL and a prompt as input\n- Fetches the URL content, converts HTML to markdown\n- Processes the content with the prompt using a small, fast model\n- Returns the model's response about the content\n- Use this tool when you need to retrieve and analyze web content\n\nUsage notes:\n - IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions. All MCP-provided tools start with \"mcp__\".\n - The URL must be a fully-formed valid URL\n - HTTP URLs will be automatically upgraded to HTTPS\n - For security reasons, the URL's domain must have been provided directly by the user, unless it's on a small pre-approved set of the top few dozen hosts for popular coding resources, like react.dev.\n - The prompt should describe what information you want to extract from the page\n - This tool is read-only and does not modify any files\n - Results may be summarized if the content is very large\n - Includes a self-cleaning 15-minute cache for faster responses when repeatedly accessing the same URL\n\n\nExample usage:\n{\n \"invocations\": [\n {\n \"tool_name\": \"Bash\",\n \"input\": {\n \"command\": \"git blame src/foo.ts\"\n }\n },\n {\n \"tool_name\": \"GlobTool\",\n \"input\": {\n \"pattern\": \"**/*.ts\"\n }\n },\n {\n \"tool_name\": \"GrepTool\",\n \"input\": {\n \"pattern\": \"function\",\n \"include\": \"*.ts\"\n }\n }\n ]\n}\n", - "inputSchema": { - "type": "object", - "properties": { - "description": { - "type": "string", - "description": "A short (3-5 word) description of the batch operation" - }, - "invocations": { - "type": "array", - "items": { - "type": "object", - "properties": { - "tool_name": { - "type": "string", - "description": "The name of the tool to invoke" - }, - "input": { - "type": "object", - "additionalProperties": {}, - "description": "The input to pass to the tool" - } - }, - "required": [ - "tool_name", - "input" - ], - "additionalProperties": false - }, - "description": "The list of tool invocations to execute" - } - }, - "required": [ - "description", - "invocations" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "GlobTool", - "description": "- Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead\n", - "inputSchema": { - "type": "object", - "properties": { - "pattern": { - "type": "string", - "description": "The glob pattern to match files against" - }, - "path": { - "type": "string", - "description": "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided." - } - }, - "required": [ - "pattern" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "GrepTool", - "description": "\n- Fast content search tool that works with any codebase size\n- Searches file contents using regular expressions\n- Supports full regex syntax (eg. \"log.*Error\", \"function\\s+\\w+\", etc.)\n- Filter files by pattern with the include parameter (eg. \"*.js\", \"*.{ts,tsx}\")\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files containing specific patterns\n- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead\n", - "inputSchema": { - "type": "object", - "properties": { - "pattern": { - "type": "string", - "description": "The regular expression pattern to search for in file contents" - }, - "path": { - "type": "string", - "description": "The directory to search in. Defaults to the current working directory." - }, - "include": { - "type": "string", - "description": "File pattern to include in the search (e.g. \"*.js\", \"*.{ts,tsx}\")" - } - }, - "required": [ - "pattern" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "LS", - "description": "Lists files and directories in a given path. The path parameter must be an absolute path, not a relative path. You can optionally provide an array of glob patterns to ignore with the ignore parameter. You should generally prefer the Glob and Grep tools, if you know which directories to search.", - "inputSchema": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "The absolute path to the directory to list (must be absolute, not relative)" - }, - "ignore": { - "type": "array", - "items": { - "type": "string" - }, - "description": "List of glob patterns to ignore" - } - }, - "required": [ - "path" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "View", - "description": "Read a file from the local filesystem.", - "inputSchema": { - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "The absolute path to the file to read" - }, - "offset": { - "type": "number", - "description": "The line number to start reading from. Only provide if the file is too large to read at once" - }, - "limit": { - "type": "number", - "description": "The number of lines to read. Only provide if the file is too large to read at once." - } - }, - "required": [ - "file_path" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "Edit", - "description": "A tool for editing files", - "inputSchema": { - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "The absolute path to the file to modify" - }, - "old_string": { - "type": "string", - "description": "The text to replace" - }, - "new_string": { - "type": "string", - "description": "The text to replace it with" - }, - "expected_replacements": { - "type": "number", - "default": 1, - "description": "The expected number of replacements to perform. Defaults to 1 if not specified." - } - }, - "required": [ - "file_path", - "old_string", - "new_string" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "Replace", - "description": "Write a file to the local filesystem.", - "inputSchema": { - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "The absolute path to the file to write (must be absolute, not relative)" - }, - "content": { - "type": "string", - "description": "The content to write to the file" - } - }, - "required": [ - "file_path", - "content" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "ReadNotebook", - "description": "Extract and read source code from all code cells in a Jupyter notebook.", - "inputSchema": { - "type": "object", - "properties": { - "notebook_path": { - "type": "string", - "description": "The absolute path to the Jupyter notebook file to read (must be absolute, not relative)" - } - }, - "required": [ - "notebook_path" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "NotebookEditCell", - "description": "Replace the contents of a specific cell in a Jupyter notebook.", - "inputSchema": { - "type": "object", - "properties": { - "notebook_path": { - "type": "string", - "description": "The absolute path to the Jupyter notebook file to edit (must be absolute, not relative)" - }, - "cell_number": { - "type": "number", - "description": "The index of the cell to edit (0-based)" - }, - "new_source": { - "type": "string", - "description": "The new source for the cell" - }, - "cell_type": { - "type": "string", - "enum": [ - "code", - "markdown" - ], - "description": "The type of the cell (code or markdown). If not specified, it defaults to the current cell type. If using edit_mode=insert, this is required." - }, - "edit_mode": { - "type": "string", - "description": "The type of edit to make (replace, insert, delete). Defaults to replace." - } - }, - "required": [ - "notebook_path", - "cell_number", - "new_source" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "WebFetchTool", - "description": "Claude wants to fetch content from this URL", - "inputSchema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri", - "description": "The URL to fetch content from" - }, - "prompt": { - "type": "string", - "description": "The prompt to run on the fetched content" - } - }, - "required": [ - "url", - "prompt" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - ] - } -} -``` diff --git a/crates/agent/src/edit_agent/evals/fixtures/zode/react.py b/crates/agent/src/edit_agent/evals/fixtures/zode/react.py deleted file mode 100644 index 03ff02e789..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/zode/react.py +++ /dev/null @@ -1,14 +0,0 @@ -class InputCell: - def __init__(self, initial_value): - self.value = None - - -class ComputeCell: - def __init__(self, inputs, compute_function): - self.value = None - - def add_callback(self, callback): - pass - - def remove_callback(self, callback): - pass diff --git a/crates/agent/src/edit_agent/evals/fixtures/zode/react_test.py b/crates/agent/src/edit_agent/evals/fixtures/zode/react_test.py deleted file mode 100644 index 1f917e40b4..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/zode/react_test.py +++ /dev/null @@ -1,271 +0,0 @@ -# These tests are auto-generated with test data from: -# https://github.com/exercism/problem-specifications/tree/main/exercises/react/canonical-data.json -# File last updated on 2023-07-19 - -from functools import partial -import unittest - -from react import ( - InputCell, - ComputeCell, -) - - -class ReactTest(unittest.TestCase): - def test_input_cells_have_a_value(self): - input = InputCell(10) - self.assertEqual(input.value, 10) - - def test_an_input_cell_s_value_can_be_set(self): - input = InputCell(4) - input.value = 20 - self.assertEqual(input.value, 20) - - def test_compute_cells_calculate_initial_value(self): - input = InputCell(1) - output = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] + 1, - ) - self.assertEqual(output.value, 2) - - def test_compute_cells_take_inputs_in_the_right_order(self): - one = InputCell(1) - two = InputCell(2) - output = ComputeCell( - [ - one, - two, - ], - lambda inputs: inputs[0] + inputs[1] * 10, - ) - self.assertEqual(output.value, 21) - - def test_compute_cells_update_value_when_dependencies_are_changed(self): - input = InputCell(1) - output = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] + 1, - ) - input.value = 3 - self.assertEqual(output.value, 4) - - def test_compute_cells_can_depend_on_other_compute_cells(self): - input = InputCell(1) - times_two = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] * 2, - ) - times_thirty = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] * 30, - ) - output = ComputeCell( - [ - times_two, - times_thirty, - ], - lambda inputs: inputs[0] + inputs[1], - ) - self.assertEqual(output.value, 32) - input.value = 3 - self.assertEqual(output.value, 96) - - def test_compute_cells_fire_callbacks(self): - input = InputCell(1) - output = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] + 1, - ) - cb1_observer = [] - callback1 = self.callback_factory(cb1_observer) - output.add_callback(callback1) - input.value = 3 - self.assertEqual(cb1_observer[-1], 4) - - def test_callback_cells_only_fire_on_change(self): - input = InputCell(1) - output = ComputeCell([input], lambda inputs: 111 if inputs[0] < 3 else 222) - cb1_observer = [] - callback1 = self.callback_factory(cb1_observer) - output.add_callback(callback1) - input.value = 2 - self.assertEqual(cb1_observer, []) - input.value = 4 - self.assertEqual(cb1_observer[-1], 222) - - def test_callbacks_do_not_report_already_reported_values(self): - input = InputCell(1) - output = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] + 1, - ) - cb1_observer = [] - callback1 = self.callback_factory(cb1_observer) - output.add_callback(callback1) - input.value = 2 - self.assertEqual(cb1_observer[-1], 3) - input.value = 3 - self.assertEqual(cb1_observer[-1], 4) - - def test_callbacks_can_fire_from_multiple_cells(self): - input = InputCell(1) - plus_one = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] + 1, - ) - minus_one = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] - 1, - ) - cb1_observer = [] - cb2_observer = [] - callback1 = self.callback_factory(cb1_observer) - callback2 = self.callback_factory(cb2_observer) - plus_one.add_callback(callback1) - minus_one.add_callback(callback2) - input.value = 10 - self.assertEqual(cb1_observer[-1], 11) - self.assertEqual(cb2_observer[-1], 9) - - def test_callbacks_can_be_added_and_removed(self): - input = InputCell(11) - output = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] + 1, - ) - cb1_observer = [] - cb2_observer = [] - cb3_observer = [] - callback1 = self.callback_factory(cb1_observer) - callback2 = self.callback_factory(cb2_observer) - callback3 = self.callback_factory(cb3_observer) - output.add_callback(callback1) - output.add_callback(callback2) - input.value = 31 - self.assertEqual(cb1_observer[-1], 32) - self.assertEqual(cb2_observer[-1], 32) - output.remove_callback(callback1) - output.add_callback(callback3) - input.value = 41 - self.assertEqual(len(cb1_observer), 1) - self.assertEqual(cb2_observer[-1], 42) - self.assertEqual(cb3_observer[-1], 42) - - def test_removing_a_callback_multiple_times_doesn_t_interfere_with_other_callbacks( - self, - ): - input = InputCell(1) - output = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] + 1, - ) - cb1_observer = [] - cb2_observer = [] - callback1 = self.callback_factory(cb1_observer) - callback2 = self.callback_factory(cb2_observer) - output.add_callback(callback1) - output.add_callback(callback2) - output.remove_callback(callback1) - output.remove_callback(callback1) - output.remove_callback(callback1) - input.value = 2 - self.assertEqual(cb1_observer, []) - self.assertEqual(cb2_observer[-1], 3) - - def test_callbacks_should_only_be_called_once_even_if_multiple_dependencies_change( - self, - ): - input = InputCell(1) - plus_one = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] + 1, - ) - minus_one1 = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] - 1, - ) - minus_one2 = ComputeCell( - [ - minus_one1, - ], - lambda inputs: inputs[0] - 1, - ) - output = ComputeCell( - [ - plus_one, - minus_one2, - ], - lambda inputs: inputs[0] * inputs[1], - ) - cb1_observer = [] - callback1 = self.callback_factory(cb1_observer) - output.add_callback(callback1) - input.value = 4 - self.assertEqual(cb1_observer[-1], 10) - - def test_callbacks_should_not_be_called_if_dependencies_change_but_output_value_doesn_t_change( - self, - ): - input = InputCell(1) - plus_one = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] + 1, - ) - minus_one = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] - 1, - ) - always_two = ComputeCell( - [ - plus_one, - minus_one, - ], - lambda inputs: inputs[0] - inputs[1], - ) - cb1_observer = [] - callback1 = self.callback_factory(cb1_observer) - always_two.add_callback(callback1) - input.value = 2 - self.assertEqual(cb1_observer, []) - input.value = 3 - self.assertEqual(cb1_observer, []) - input.value = 4 - self.assertEqual(cb1_observer, []) - input.value = 5 - self.assertEqual(cb1_observer, []) - - # Utility functions. - def callback_factory(self, observer): - def callback(observer, value): - observer.append(value) - - return partial(callback, observer) diff --git a/crates/agent/src/edit_agent/streaming_fuzzy_matcher.rs b/crates/agent/src/edit_agent/streaming_fuzzy_matcher.rs deleted file mode 100644 index 904ec05a8c..0000000000 --- a/crates/agent/src/edit_agent/streaming_fuzzy_matcher.rs +++ /dev/null @@ -1,806 +0,0 @@ -use language::{Point, TextBufferSnapshot}; -use std::{cmp, ops::Range}; - -const REPLACEMENT_COST: u32 = 1; -const INSERTION_COST: u32 = 3; -const DELETION_COST: u32 = 10; - -/// A streaming fuzzy matcher that can process text chunks incrementally -/// and return the best match found so far at each step. -pub struct StreamingFuzzyMatcher { - snapshot: TextBufferSnapshot, - query_lines: Vec, - line_hint: Option, - incomplete_line: String, - matches: Vec>, - matrix: SearchMatrix, -} - -impl StreamingFuzzyMatcher { - pub fn new(snapshot: TextBufferSnapshot) -> Self { - let buffer_line_count = snapshot.max_point().row as usize + 1; - Self { - snapshot, - query_lines: Vec::new(), - line_hint: None, - incomplete_line: String::new(), - matches: Vec::new(), - matrix: SearchMatrix::new(buffer_line_count + 1), - } - } - - /// Returns the query lines. - pub fn query_lines(&self) -> &[String] { - &self.query_lines - } - - /// Push a new chunk of text and get the best match found so far. - /// - /// This method accumulates text chunks and processes complete lines. - /// Partial lines are buffered internally until a newline is received. - /// - /// # Returns - /// - /// Returns `Some(range)` if a match has been found with the accumulated - /// query so far, or `None` if no suitable match exists yet. - pub fn push(&mut self, chunk: &str, line_hint: Option) -> Option> { - if line_hint.is_some() { - self.line_hint = line_hint; - } - - // Add the chunk to our incomplete line buffer - self.incomplete_line.push_str(chunk); - self.line_hint = line_hint; - - if let Some((last_pos, _)) = self.incomplete_line.match_indices('\n').next_back() { - let complete_part = &self.incomplete_line[..=last_pos]; - - // Split into lines and add to query_lines - for line in complete_part.lines() { - self.query_lines.push(line.to_string()); - } - - self.incomplete_line.replace_range(..last_pos + 1, ""); - - self.matches = self.resolve_location_fuzzy(); - } - - let best_match = self.select_best_match(); - best_match.or_else(|| self.matches.first().cloned()) - } - - /// Finish processing and return the final best match(es). - /// - /// This processes any remaining incomplete line before returning the final - /// match result. - pub fn finish(&mut self) -> Vec> { - // Process any remaining incomplete line - if !self.incomplete_line.is_empty() { - self.query_lines.push(self.incomplete_line.clone()); - self.incomplete_line.clear(); - self.matches = self.resolve_location_fuzzy(); - } - self.matches.clone() - } - - fn resolve_location_fuzzy(&mut self) -> Vec> { - let new_query_line_count = self.query_lines.len(); - let old_query_line_count = self.matrix.rows.saturating_sub(1); - if new_query_line_count == old_query_line_count { - return Vec::new(); - } - - self.matrix.resize_rows(new_query_line_count + 1); - - // Process only the new query lines - for row in old_query_line_count..new_query_line_count { - let query_line = self.query_lines[row].trim(); - let leading_deletion_cost = (row + 1) as u32 * DELETION_COST; - - self.matrix.set( - row + 1, - 0, - SearchState::new(leading_deletion_cost, SearchDirection::Up), - ); - - let mut buffer_lines = self.snapshot.as_rope().chunks().lines(); - let mut col = 0; - while let Some(buffer_line) = buffer_lines.next() { - let buffer_line = buffer_line.trim(); - let up = SearchState::new( - self.matrix - .get(row, col + 1) - .cost - .saturating_add(DELETION_COST), - SearchDirection::Up, - ); - let left = SearchState::new( - self.matrix - .get(row + 1, col) - .cost - .saturating_add(INSERTION_COST), - SearchDirection::Left, - ); - let diagonal = SearchState::new( - if query_line == buffer_line { - self.matrix.get(row, col).cost - } else if fuzzy_eq(query_line, buffer_line) { - self.matrix.get(row, col).cost + REPLACEMENT_COST - } else { - self.matrix - .get(row, col) - .cost - .saturating_add(DELETION_COST + INSERTION_COST) - }, - SearchDirection::Diagonal, - ); - self.matrix - .set(row + 1, col + 1, up.min(left).min(diagonal)); - col += 1; - } - } - - // Find all matches with the best cost - let buffer_line_count = self.snapshot.max_point().row as usize + 1; - let mut best_cost = u32::MAX; - let mut matches_with_best_cost = Vec::new(); - - for col in 1..=buffer_line_count { - let cost = self.matrix.get(new_query_line_count, col).cost; - if cost < best_cost { - best_cost = cost; - matches_with_best_cost.clear(); - matches_with_best_cost.push(col as u32); - } else if cost == best_cost { - matches_with_best_cost.push(col as u32); - } - } - - // Find ranges for the matches - let mut valid_matches = Vec::new(); - for &buffer_row_end in &matches_with_best_cost { - let mut matched_lines = 0; - let mut query_row = new_query_line_count; - let mut buffer_row_start = buffer_row_end; - while query_row > 0 && buffer_row_start > 0 { - let current = self.matrix.get(query_row, buffer_row_start as usize); - match current.direction { - SearchDirection::Diagonal => { - query_row -= 1; - buffer_row_start -= 1; - matched_lines += 1; - } - SearchDirection::Up => { - query_row -= 1; - } - SearchDirection::Left => { - buffer_row_start -= 1; - } - } - } - - let matched_buffer_row_count = buffer_row_end - buffer_row_start; - let matched_ratio = matched_lines as f32 - / (matched_buffer_row_count as f32).max(new_query_line_count as f32); - if matched_ratio >= 0.8 { - let buffer_start_ix = self - .snapshot - .point_to_offset(Point::new(buffer_row_start, 0)); - let buffer_end_ix = self.snapshot.point_to_offset(Point::new( - buffer_row_end - 1, - self.snapshot.line_len(buffer_row_end - 1), - )); - valid_matches.push((buffer_row_start, buffer_start_ix..buffer_end_ix)); - } - } - - valid_matches.into_iter().map(|(_, range)| range).collect() - } - - /// Return the best match with starting position close enough to line_hint. - pub fn select_best_match(&self) -> Option> { - // Allow line hint to be off by that many lines. - // Higher values increase probability of applying edits to a wrong place, - // Lower values increase edits failures and overall conversation length. - const LINE_HINT_TOLERANCE: u32 = 200; - - if self.matches.is_empty() { - return None; - } - - if self.matches.len() == 1 { - return self.matches.first().cloned(); - } - - let Some(line_hint) = self.line_hint else { - // Multiple ambiguous matches - return None; - }; - - let mut best_match = None; - let mut best_distance = u32::MAX; - - for range in &self.matches { - let start_point = self.snapshot.offset_to_point(range.start); - let start_line = start_point.row; - let distance = start_line.abs_diff(line_hint); - - if distance <= LINE_HINT_TOLERANCE && distance < best_distance { - best_distance = distance; - best_match = Some(range.clone()); - } - } - - best_match - } -} - -fn fuzzy_eq(left: &str, right: &str) -> bool { - const THRESHOLD: f64 = 0.8; - - let min_levenshtein = left.len().abs_diff(right.len()); - let min_normalized_levenshtein = - 1. - (min_levenshtein as f64 / cmp::max(left.len(), right.len()) as f64); - if min_normalized_levenshtein < THRESHOLD { - return false; - } - - strsim::normalized_levenshtein(left, right) >= THRESHOLD -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -enum SearchDirection { - Up, - Left, - Diagonal, -} - -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -struct SearchState { - cost: u32, - direction: SearchDirection, -} - -impl SearchState { - fn new(cost: u32, direction: SearchDirection) -> Self { - Self { cost, direction } - } -} - -struct SearchMatrix { - cols: usize, - rows: usize, - data: Vec, -} - -impl SearchMatrix { - fn new(cols: usize) -> Self { - SearchMatrix { - cols, - rows: 0, - data: Vec::new(), - } - } - - fn resize_rows(&mut self, needed_rows: usize) { - debug_assert!(needed_rows > self.rows); - self.rows = needed_rows; - self.data.resize( - self.rows * self.cols, - SearchState::new(0, SearchDirection::Diagonal), - ); - } - - fn get(&self, row: usize, col: usize) -> SearchState { - debug_assert!(row < self.rows && col < self.cols); - self.data[row * self.cols + col] - } - - fn set(&mut self, row: usize, col: usize, state: SearchState) { - debug_assert!(row < self.rows && col < self.cols); - self.data[row * self.cols + col] = state; - } -} - -#[cfg(test)] -mod tests { - use super::*; - use indoc::indoc; - use language::{BufferId, TextBuffer}; - use rand::prelude::*; - use text::ReplicaId; - use util::test::{generate_marked_text, marked_text_ranges}; - - #[test] - fn test_empty_query() { - let buffer = TextBuffer::new( - ReplicaId::LOCAL, - BufferId::new(1).unwrap(), - "Hello world\nThis is a test\nFoo bar baz", - ); - let snapshot = buffer.snapshot(); - - let mut finder = StreamingFuzzyMatcher::new(snapshot); - assert_eq!(push(&mut finder, ""), None); - assert_eq!(finish(finder), None); - } - - #[test] - fn test_streaming_exact_match() { - let buffer = TextBuffer::new( - ReplicaId::LOCAL, - BufferId::new(1).unwrap(), - "Hello world\nThis is a test\nFoo bar baz", - ); - let snapshot = buffer.snapshot(); - - let mut finder = StreamingFuzzyMatcher::new(snapshot); - - // Push partial query - assert_eq!(push(&mut finder, "This"), None); - - // Complete the line - assert_eq!( - push(&mut finder, " is a test\n"), - Some("This is a test".to_string()) - ); - - // Finish should return the same result - assert_eq!(finish(finder), Some("This is a test".to_string())); - } - - #[test] - fn test_streaming_fuzzy_match() { - let buffer = TextBuffer::new( - ReplicaId::LOCAL, - BufferId::new(1).unwrap(), - indoc! {" - function foo(a, b) { - return a + b; - } - - function bar(x, y) { - return x * y; - } - "}, - ); - let snapshot = buffer.snapshot(); - - let mut finder = StreamingFuzzyMatcher::new(snapshot); - - // Push a fuzzy query that should match the first function - assert_eq!( - push(&mut finder, "function foo(a, c) {\n").as_deref(), - Some("function foo(a, b) {") - ); - assert_eq!( - push(&mut finder, " return a + c;\n}\n").as_deref(), - Some(concat!( - "function foo(a, b) {\n", - " return a + b;\n", - "}" - )) - ); - } - - #[test] - fn test_incremental_improvement() { - let buffer = TextBuffer::new( - ReplicaId::LOCAL, - BufferId::new(1).unwrap(), - "Line 1\nLine 2\nLine 3\nLine 4\nLine 5", - ); - let snapshot = buffer.snapshot(); - - let mut finder = StreamingFuzzyMatcher::new(snapshot); - - // No match initially - assert_eq!(push(&mut finder, "Lin"), None); - - // Get a match when we complete a line - assert_eq!(push(&mut finder, "e 3\n"), Some("Line 3".to_string())); - - // The match might change if we add more specific content - assert_eq!( - push(&mut finder, "Line 4\n"), - Some("Line 3\nLine 4".to_string()) - ); - assert_eq!(finish(finder), Some("Line 3\nLine 4".to_string())); - } - - #[test] - fn test_incomplete_lines_buffering() { - let buffer = TextBuffer::new( - ReplicaId::LOCAL, - BufferId::new(1).unwrap(), - indoc! {" - The quick brown fox - jumps over the lazy dog - Pack my box with five dozen liquor jugs - "}, - ); - let snapshot = buffer.snapshot(); - - let mut finder = StreamingFuzzyMatcher::new(snapshot); - - // Push text in small chunks across line boundaries - assert_eq!(push(&mut finder, "jumps "), None); // No newline yet - assert_eq!(push(&mut finder, "over the"), None); // Still no newline - assert_eq!(push(&mut finder, " lazy"), None); // Still incomplete - - // Complete the line - assert_eq!( - push(&mut finder, " dog\n"), - Some("jumps over the lazy dog".to_string()) - ); - } - - #[test] - fn test_multiline_fuzzy_match() { - let buffer = TextBuffer::new( - ReplicaId::LOCAL, - BufferId::new(1).unwrap(), - indoc! {r#" - impl Display for User { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "User: {} ({})", self.name, self.email) - } - } - - impl Debug for User { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_struct("User") - .field("name", &self.name) - .field("email", &self.email) - .finish() - } - } - "#}, - ); - let snapshot = buffer.snapshot(); - - let mut finder = StreamingFuzzyMatcher::new(snapshot); - - assert_eq!( - push(&mut finder, "impl Debug for User {\n"), - Some("impl Debug for User {".to_string()) - ); - assert_eq!( - push( - &mut finder, - " fn fmt(&self, f: &mut Formatter) -> Result {\n" - ) - .as_deref(), - Some(concat!( - "impl Debug for User {\n", - " fn fmt(&self, f: &mut Formatter) -> fmt::Result {" - )) - ); - assert_eq!( - push(&mut finder, " f.debug_struct(\"User\")\n").as_deref(), - Some(concat!( - "impl Debug for User {\n", - " fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n", - " f.debug_struct(\"User\")" - )) - ); - assert_eq!( - push( - &mut finder, - " .field(\"name\", &self.username)\n" - ) - .as_deref(), - Some(concat!( - "impl Debug for User {\n", - " fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n", - " f.debug_struct(\"User\")\n", - " .field(\"name\", &self.name)" - )) - ); - assert_eq!( - finish(finder).as_deref(), - Some(concat!( - "impl Debug for User {\n", - " fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n", - " f.debug_struct(\"User\")\n", - " .field(\"name\", &self.name)" - )) - ); - } - - #[gpui::test(iterations = 100)] - fn test_resolve_location_single_line(mut rng: StdRng) { - assert_location_resolution( - concat!( - " Lorem\n", - "« ipsum»\n", - " dolor sit amet\n", - " consecteur", - ), - "ipsum", - &mut rng, - ); - } - - #[gpui::test(iterations = 100)] - fn test_resolve_location_multiline(mut rng: StdRng) { - assert_location_resolution( - concat!( - " Lorem\n", - "« ipsum\n", - " dolor sit amet»\n", - " consecteur", - ), - "ipsum\ndolor sit amet", - &mut rng, - ); - } - - #[gpui::test(iterations = 100)] - fn test_resolve_location_function_with_typo(mut rng: StdRng) { - assert_location_resolution( - indoc! {" - «fn foo1(a: usize) -> usize { - 40 - }» - - fn foo2(b: usize) -> usize { - 42 - } - "}, - "fn foo1(a: usize) -> u32 {\n40\n}", - &mut rng, - ); - } - - #[gpui::test(iterations = 100)] - fn test_resolve_location_class_methods(mut rng: StdRng) { - assert_location_resolution( - indoc! {" - class Something { - one() { return 1; } - « two() { return 2222; } - three() { return 333; } - four() { return 4444; } - five() { return 5555; } - six() { return 6666; }» - seven() { return 7; } - eight() { return 8; } - } - "}, - indoc! {" - two() { return 2222; } - four() { return 4444; } - five() { return 5555; } - six() { return 6666; } - "}, - &mut rng, - ); - } - - #[gpui::test(iterations = 100)] - fn test_resolve_location_imports_no_match(mut rng: StdRng) { - assert_location_resolution( - indoc! {" - use std::ops::Range; - use std::sync::Mutex; - use std::{ - collections::HashMap, - env, - ffi::{OsStr, OsString}, - fs, - io::{BufRead, BufReader}, - mem, - path::{Path, PathBuf}, - process::Command, - sync::LazyLock, - time::SystemTime, - }; - "}, - indoc! {" - use std::collections::{HashMap, HashSet}; - use std::ffi::{OsStr, OsString}; - use std::fmt::Write as _; - use std::fs; - use std::io::{BufReader, Read, Write}; - use std::mem; - use std::path::{Path, PathBuf}; - use std::process::Command; - use std::sync::Arc; - "}, - &mut rng, - ); - } - - #[gpui::test(iterations = 100)] - fn test_resolve_location_nested_closure(mut rng: StdRng) { - assert_location_resolution( - indoc! {" - impl Foo { - fn new() -> Self { - Self { - subscriptions: vec![ - cx.observe_window_activation(window, |editor, window, cx| { - let active = window.is_window_active(); - editor.blink_manager.update(cx, |blink_manager, cx| { - if active { - blink_manager.enable(cx); - } else { - blink_manager.disable(cx); - } - }); - }), - ]; - } - } - } - "}, - concat!( - " editor.blink_manager.update(cx, |blink_manager, cx| {\n", - " blink_manager.enable(cx);\n", - " });", - ), - &mut rng, - ); - } - - #[gpui::test(iterations = 100)] - fn test_resolve_location_tool_invocation(mut rng: StdRng) { - assert_location_resolution( - indoc! {r#" - let tool = cx - .update(|cx| working_set.tool(&tool_name, cx)) - .map_err(|err| { - anyhow!("Failed to look up tool '{}': {}", tool_name, err) - })?; - - let Some(tool) = tool else { - return Err(anyhow!("Tool '{}' not found", tool_name)); - }; - - let project = project.clone(); - let action_log = action_log.clone(); - let messages = messages.clone(); - let tool_result = cx - .update(|cx| tool.run(invocation.input, &messages, project, action_log, cx)) - .map_err(|err| anyhow!("Failed to start tool '{}': {}", tool_name, err))?; - - tasks.push(tool_result.output); - "#}, - concat!( - "let tool_result = cx\n", - " .update(|cx| tool.run(invocation.input, &messages, project, action_log, cx))\n", - " .output;", - ), - &mut rng, - ); - } - - #[gpui::test] - fn test_line_hint_selection() { - let text = indoc! {r#" - fn first_function() { - return 42; - } - - fn second_function() { - return 42; - } - - fn third_function() { - return 42; - } - "#}; - - let buffer = TextBuffer::new( - ReplicaId::LOCAL, - BufferId::new(1).unwrap(), - text.to_string(), - ); - let snapshot = buffer.snapshot(); - let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone()); - - // Given a query that matches all three functions - let query = "return 42;\n"; - - // Test with line hint pointing to second function (around line 5) - let best_match = matcher.push(query, Some(5)).expect("Failed to match query"); - - let matched_text = snapshot - .text_for_range(best_match.clone()) - .collect::(); - assert!(matched_text.contains("return 42;")); - assert_eq!( - best_match, - 63..77, - "Expected to match `second_function` based on the line hint" - ); - - let mut matcher = StreamingFuzzyMatcher::new(snapshot); - matcher.push(query, None); - matcher.finish(); - let best_match = matcher.select_best_match(); - assert!( - best_match.is_none(), - "Best match should be None when query cannot be uniquely resolved" - ); - } - - #[track_caller] - fn assert_location_resolution(text_with_expected_range: &str, query: &str, rng: &mut StdRng) { - let (text, expected_ranges) = marked_text_ranges(text_with_expected_range, false); - let buffer = TextBuffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), text.clone()); - let snapshot = buffer.snapshot(); - - let mut matcher = StreamingFuzzyMatcher::new(snapshot); - - // Split query into random chunks - let chunks = to_random_chunks(rng, query); - - // Push chunks incrementally - for chunk in &chunks { - matcher.push(chunk, None); - } - - let actual_ranges = matcher.finish(); - - // If no expected ranges, we expect no match - if expected_ranges.is_empty() { - assert!( - actual_ranges.is_empty(), - "Expected no match for query: {:?}, but found: {:?}", - query, - actual_ranges - ); - } else { - let text_with_actual_range = generate_marked_text(&text, &actual_ranges, false); - pretty_assertions::assert_eq!( - text_with_actual_range, - text_with_expected_range, - indoc! {" - Query: {:?} - Chunks: {:?} - Expected marked text: {} - Actual marked text: {} - Expected ranges: {:?} - Actual ranges: {:?}" - }, - query, - chunks, - text_with_expected_range, - text_with_actual_range, - expected_ranges, - actual_ranges - ); - } - } - - fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec { - let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50)); - let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count); - chunk_indices.sort(); - chunk_indices.push(input.len()); - - let mut chunks = Vec::new(); - let mut last_ix = 0; - for chunk_ix in chunk_indices { - chunks.push(input[last_ix..chunk_ix].to_string()); - last_ix = chunk_ix; - } - chunks - } - - fn push(finder: &mut StreamingFuzzyMatcher, chunk: &str) -> Option { - finder - .push(chunk, None) - .map(|range| finder.snapshot.text_for_range(range).collect::()) - } - - 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::()) - } -} diff --git a/crates/agent/src/history_store.rs b/crates/agent/src/history_store.rs deleted file mode 100644 index 5a1b923d13..0000000000 --- a/crates/agent/src/history_store.rs +++ /dev/null @@ -1,412 +0,0 @@ -use crate::{DbThread, DbThreadMetadata, ThreadsDatabase}; -use acp_thread::MentionUri; -use agent_client_protocol as acp; -use anyhow::{Context as _, Result, anyhow}; -use assistant_text_thread::{SavedTextThreadMetadata, TextThread}; -use chrono::{DateTime, Utc}; -use db::kvp::KEY_VALUE_STORE; -use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*}; -use itertools::Itertools; -use paths::text_threads_dir; -use project::Project; -use serde::{Deserialize, Serialize}; -use std::{collections::VecDeque, path::Path, rc::Rc, 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"); - -//todo: We should remove this function once we support loading all acp thread -pub fn load_agent_thread( - session_id: acp::SessionId, - history_store: Entity, - project: Entity, - cx: &mut App, -) -> Task>> { - use agent_servers::{AgentServer, AgentServerDelegate}; - - let server = Rc::new(crate::NativeAgentServer::new( - project.read(cx).fs().clone(), - history_store, - )); - let delegate = AgentServerDelegate::new( - project.read(cx).agent_server_store().clone(), - project.clone(), - None, - None, - ); - let connection = server.connect(None, delegate, cx); - cx.spawn(async move |cx| { - let (agent, _) = connection.await?; - let agent = agent.downcast::().unwrap(); - cx.update(|cx| agent.load_thread(session_id, cx))?.await - }) -} - -#[derive(Clone, Debug)] -pub enum HistoryEntry { - AcpThread(DbThreadMetadata), - TextThread(SavedTextThreadMetadata), -} - -impl HistoryEntry { - pub fn updated_at(&self) -> DateTime { - match self { - HistoryEntry::AcpThread(thread) => thread.updated_at, - HistoryEntry::TextThread(text_thread) => text_thread.mtime.to_utc(), - } - } - - pub fn id(&self) -> HistoryEntryId { - match self { - HistoryEntry::AcpThread(thread) => HistoryEntryId::AcpThread(thread.id.clone()), - HistoryEntry::TextThread(text_thread) => { - HistoryEntryId::TextThread(text_thread.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(text_thread) => MentionUri::TextThread { - path: text_thread.path.as_ref().to_owned(), - name: text_thread.title.to_string(), - }, - } - } - - pub fn title(&self) -> &SharedString { - match self { - HistoryEntry::AcpThread(thread) => { - if thread.title.is_empty() { - DEFAULT_TITLE - } else { - &thread.title - } - } - HistoryEntry::TextThread(text_thread) => &text_thread.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, - text_thread_store: Entity, - recently_opened_entries: VecDeque, - _subscriptions: Vec, - _save_recently_opened_entries_task: Task<()>, -} - -impl HistoryStore { - pub fn new( - text_thread_store: Entity, - cx: &mut Context, - ) -> Self { - let subscriptions = - vec![cx.observe(&text_thread_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 { - text_thread_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 load_thread( - &mut self, - id: acp::SessionId, - cx: &mut Context, - ) -> Task>> { - let database_future = ThreadsDatabase::connect(cx); - cx.background_spawn(async move { - let database = database_future.await.map_err(|err| anyhow!(err))?; - database.load_thread(id).await - }) - } - - 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_threads(&mut self, 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_threads().await?; - this.update(cx, |this, cx| this.reload(cx)) - }) - } - - pub fn delete_text_thread( - &mut self, - path: Arc, - cx: &mut Context, - ) -> Task> { - self.text_thread_store - .update(cx, |store, cx| store.delete_local(path, cx)) - } - - pub fn load_text_thread( - &self, - path: Arc, - cx: &mut Context, - ) -> Task>> { - self.text_thread_store - .update(cx, |store, cx| store.open_local(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.text_thread_store - .read(cx) - .unordered_text_threads() - .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 - .text_thread_store - .read(cx) - .unordered_text_threads() - .flat_map(|text_thread| { - self.recently_opened_entries - .iter() - .enumerate() - .flat_map(|(index, entry)| match entry { - HistoryEntryId::TextThread(path) if &text_thread.path == path => { - Some((index, HistoryEntry::TextThread(text_thread.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().into_owned()) - }), - 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::new(id.as_str()))) - } - SerializedRecentOpen::TextThread(file_name) => Some( - HistoryEntryId::TextThread(text_threads_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/agent/src/legacy_thread.rs b/crates/agent/src/legacy_thread.rs deleted file mode 100644 index 34babb8006..0000000000 --- a/crates/agent/src/legacy_thread.rs +++ /dev/null @@ -1,402 +0,0 @@ -use crate::ProjectSnapshot; -use agent_settings::{AgentProfileId, CompletionMode}; -use anyhow::Result; -use chrono::{DateTime, Utc}; -use gpui::SharedString; -use language_model::{LanguageModelToolResultContent, LanguageModelToolUseId, Role, TokenUsage}; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; - -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] -pub enum DetailedSummaryState { - #[default] - NotGenerated, - Generating, - Generated { - text: SharedString, - }, -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)] -pub struct MessageId(pub usize); - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct SerializedThread { - pub version: String, - pub summary: SharedString, - pub updated_at: DateTime, - pub messages: Vec, - #[serde(default)] - pub initial_project_snapshot: Option>, - #[serde(default)] - pub cumulative_token_usage: TokenUsage, - #[serde(default)] - pub request_token_usage: Vec, - #[serde(default)] - pub detailed_summary_state: DetailedSummaryState, - #[serde(default)] - pub model: Option, - #[serde(default)] - pub completion_mode: Option, - #[serde(default)] - pub tool_use_limit_reached: bool, - #[serde(default)] - pub profile: Option, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct SerializedLanguageModel { - pub provider: String, - pub model: String, -} - -impl SerializedThread { - pub const VERSION: &'static str = "0.2.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() { - SerializedThreadV0_1_0::VERSION => { - let saved_thread = - serde_json::from_value::(saved_thread_json)?; - Ok(saved_thread.upgrade()) - } - SerializedThread::VERSION => Ok(serde_json::from_value::( - saved_thread_json, - )?), - _ => anyhow::bail!("unrecognized serialized thread version: {version:?}"), - }, - None => { - let saved_thread = - serde_json::from_value::(saved_thread_json)?; - Ok(saved_thread.upgrade()) - } - version => anyhow::bail!("unrecognized serialized thread version: {version:?}"), - } - } -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct SerializedThreadV0_1_0( - // The structure did not change, so we are reusing the latest SerializedThread. - // When making the next version, make sure this points to SerializedThreadV0_2_0 - SerializedThread, -); - -impl SerializedThreadV0_1_0 { - pub const VERSION: &'static str = "0.1.0"; - - pub fn upgrade(self) -> SerializedThread { - debug_assert_eq!(SerializedThread::VERSION, "0.2.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); - - last_message.tool_results = message.tool_results; - continue; - } - - messages.push(message); - } - - SerializedThread { - messages, - version: SerializedThread::VERSION.to_string(), - ..self.0 - } - } -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct SerializedMessage { - pub id: MessageId, - pub role: Role, - #[serde(default)] - pub segments: Vec, - #[serde(default)] - pub tool_uses: Vec, - #[serde(default)] - pub tool_results: Vec, - #[serde(default)] - pub context: String, - #[serde(default)] - pub creases: Vec, - #[serde(default)] - pub is_hidden: bool, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -#[serde(tag = "type")] -pub enum SerializedMessageSegment { - #[serde(rename = "text")] - Text { - text: String, - }, - #[serde(rename = "thinking")] - Thinking { - text: String, - #[serde(skip_serializing_if = "Option::is_none")] - signature: Option, - }, - RedactedThinking { - data: String, - }, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct SerializedToolUse { - pub id: LanguageModelToolUseId, - pub name: SharedString, - pub input: serde_json::Value, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct SerializedToolResult { - pub tool_use_id: LanguageModelToolUseId, - pub is_error: bool, - pub content: LanguageModelToolResultContent, - pub output: Option, -} - -#[derive(Serialize, Deserialize)] -struct LegacySerializedThread { - pub summary: SharedString, - pub updated_at: DateTime, - pub messages: Vec, - #[serde(default)] - pub initial_project_snapshot: Option>, -} - -impl LegacySerializedThread { - pub fn upgrade(self) -> SerializedThread { - SerializedThread { - version: SerializedThread::VERSION.to_string(), - summary: self.summary, - updated_at: self.updated_at, - messages: self.messages.into_iter().map(|msg| msg.upgrade()).collect(), - initial_project_snapshot: self.initial_project_snapshot, - cumulative_token_usage: TokenUsage::default(), - request_token_usage: Vec::new(), - detailed_summary_state: DetailedSummaryState::default(), - model: None, - completion_mode: None, - tool_use_limit_reached: false, - profile: None, - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -struct LegacySerializedMessage { - pub id: MessageId, - pub role: Role, - pub text: String, - #[serde(default)] - pub tool_uses: Vec, - #[serde(default)] - pub tool_results: Vec, -} - -impl LegacySerializedMessage { - fn upgrade(self) -> SerializedMessage { - SerializedMessage { - id: self.id, - role: self.role, - segments: vec![SerializedMessageSegment::Text { text: self.text }], - tool_uses: self.tool_uses, - tool_results: self.tool_results, - context: String::new(), - creases: Vec::new(), - is_hidden: false, - } - } -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct SerializedCrease { - pub start: usize, - pub end: usize, - pub icon_path: SharedString, - pub label: SharedString, -} - -#[cfg(test)] -mod tests { - use super::*; - use chrono::Utc; - use language_model::{Role, TokenUsage}; - use pretty_assertions::assert_eq; - - #[test] - fn test_legacy_serialized_thread_upgrade() { - let updated_at = Utc::now(); - let legacy_thread = LegacySerializedThread { - summary: "Test conversation".into(), - updated_at, - messages: vec![LegacySerializedMessage { - id: MessageId(1), - role: Role::User, - text: "Hello, world!".to_string(), - tool_uses: vec![], - tool_results: vec![], - }], - initial_project_snapshot: None, - }; - - let upgraded = legacy_thread.upgrade(); - - assert_eq!( - upgraded, - SerializedThread { - summary: "Test conversation".into(), - updated_at, - messages: vec![SerializedMessage { - id: MessageId(1), - role: Role::User, - segments: vec![SerializedMessageSegment::Text { - text: "Hello, world!".to_string() - }], - tool_uses: vec![], - tool_results: vec![], - context: "".to_string(), - creases: vec![], - is_hidden: false - }], - version: SerializedThread::VERSION.to_string(), - initial_project_snapshot: None, - cumulative_token_usage: TokenUsage::default(), - request_token_usage: vec![], - detailed_summary_state: DetailedSummaryState::default(), - model: None, - completion_mode: None, - tool_use_limit_reached: false, - profile: None - } - ) - } - - #[test] - fn test_serialized_threadv0_1_0_upgrade() { - let updated_at = Utc::now(); - let thread_v0_1_0 = SerializedThreadV0_1_0(SerializedThread { - summary: "Test conversation".into(), - updated_at, - messages: vec![ - SerializedMessage { - id: MessageId(1), - role: Role::User, - segments: vec![SerializedMessageSegment::Text { - text: "Use tool_1".to_string(), - }], - tool_uses: vec![], - tool_results: vec![], - context: "".to_string(), - creases: vec![], - is_hidden: false, - }, - SerializedMessage { - id: MessageId(2), - role: Role::Assistant, - segments: vec![SerializedMessageSegment::Text { - text: "I want to use a tool".to_string(), - }], - tool_uses: vec![SerializedToolUse { - id: "abc".into(), - name: "tool_1".into(), - input: serde_json::Value::Null, - }], - tool_results: vec![], - context: "".to_string(), - creases: vec![], - is_hidden: false, - }, - SerializedMessage { - id: MessageId(1), - role: Role::User, - segments: vec![SerializedMessageSegment::Text { - text: "Here is the tool result".to_string(), - }], - tool_uses: vec![], - tool_results: vec![SerializedToolResult { - tool_use_id: "abc".into(), - is_error: false, - content: LanguageModelToolResultContent::Text("abcdef".into()), - output: Some(serde_json::Value::Null), - }], - context: "".to_string(), - creases: vec![], - is_hidden: false, - }, - ], - version: SerializedThreadV0_1_0::VERSION.to_string(), - initial_project_snapshot: None, - cumulative_token_usage: TokenUsage::default(), - request_token_usage: vec![], - detailed_summary_state: DetailedSummaryState::default(), - model: None, - completion_mode: None, - tool_use_limit_reached: false, - profile: None, - }); - let upgraded = thread_v0_1_0.upgrade(); - - assert_eq!( - upgraded, - SerializedThread { - summary: "Test conversation".into(), - updated_at, - messages: vec![ - SerializedMessage { - id: MessageId(1), - role: Role::User, - segments: vec![SerializedMessageSegment::Text { - text: "Use tool_1".to_string() - }], - tool_uses: vec![], - tool_results: vec![], - context: "".to_string(), - creases: vec![], - is_hidden: false - }, - SerializedMessage { - id: MessageId(2), - role: Role::Assistant, - segments: vec![SerializedMessageSegment::Text { - text: "I want to use a tool".to_string(), - }], - tool_uses: vec![SerializedToolUse { - id: "abc".into(), - name: "tool_1".into(), - input: serde_json::Value::Null, - }], - tool_results: vec![SerializedToolResult { - tool_use_id: "abc".into(), - is_error: false, - content: LanguageModelToolResultContent::Text("abcdef".into()), - output: Some(serde_json::Value::Null), - }], - context: "".to_string(), - creases: vec![], - is_hidden: false, - }, - ], - version: SerializedThread::VERSION.to_string(), - initial_project_snapshot: None, - cumulative_token_usage: TokenUsage::default(), - request_token_usage: vec![], - detailed_summary_state: DetailedSummaryState::default(), - model: None, - completion_mode: None, - tool_use_limit_reached: false, - profile: None - } - ) - } -} diff --git a/crates/agent/src/native_agent_server.rs b/crates/agent/src/native_agent_server.rs deleted file mode 100644 index a9ade8141a..0000000000 --- a/crates/agent/src/native_agent_server.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::{any::Any, path::Path, rc::Rc, sync::Arc}; - -use agent_servers::{AgentServer, AgentServerDelegate}; -use anyhow::Result; -use fs::Fs; -use gpui::{App, Entity, SharedString, Task}; -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 name(&self) -> SharedString { - "Zed Agent".into() - } - - fn logo(&self) -> ui::IconName { - ui::IconName::ZedAgent - } - - fn connect( - &self, - _root_dir: Option<&Path>, - delegate: AgentServerDelegate, - cx: &mut App, - ) -> Task< - Result<( - Rc, - Option, - )>, - > { - log::debug!( - "NativeAgentServer::connect called for path: {:?}", - _root_dir - ); - let project = delegate.project().clone(); - let fs = self.fs.clone(); - let history = self.history.clone(); - let prompt_store = PromptStore::global(cx); - cx.spawn(async move |cx| { - log::debug!("Creating templates for native agent"); - 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, - None, - )) - }) - } - - fn into_any(self: Rc) -> Rc { - self - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use assistant_text_thread::TextThreadStore; - use gpui::AppContext; - - agent_servers::e2e_tests::common_e2e_tests!( - async |fs, project, cx| { - let auth = cx.update(|cx| { - prompt_store::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 text_thread_store = - cx.new(move |cx| TextThreadStore::fake(project.clone(), cx)); - cx.new(move |cx| HistoryStore::new(text_thread_store, cx)) - }); - - NativeAgentServer::new(fs.clone(), history) - }, - allow_option_id = "allow" - ); -} diff --git a/crates/agent/src/outline.rs b/crates/agent/src/outline.rs deleted file mode 100644 index 77af4849ff..0000000000 --- a/crates/agent/src/outline.rs +++ /dev/null @@ -1,218 +0,0 @@ -use anyhow::Result; -use gpui::{AsyncApp, Entity}; -use language::{Buffer, OutlineItem}; -use regex::Regex; -use std::fmt::Write; -use text::Point; - -/// For files over this size, instead of reading them (or including them in context), -/// we automatically provide the file's symbol outline instead, with line numbers. -pub const AUTO_OUTLINE_SIZE: usize = 16384; - -/// Result of getting buffer content, which can be either full content or an outline. -pub struct BufferContent { - /// The actual content (either full text or outline) - pub text: String, - /// Whether this is an outline (true) or full content (false) - pub is_outline: bool, -} - -/// Returns either the full content of a buffer or its outline, depending on size. -/// For files larger than AUTO_OUTLINE_SIZE, returns an outline with a header. -/// For smaller files, returns the full content. -pub async fn get_buffer_content_or_outline( - buffer: Entity, - path: Option<&str>, - cx: &AsyncApp, -) -> Result { - let file_size = buffer.read_with(cx, |buffer, _| buffer.text().len())?; - - if file_size > AUTO_OUTLINE_SIZE { - // For large files, use outline instead of full content - // Wait until the buffer has been fully parsed, so we can read its outline - buffer - .read_with(cx, |buffer, _| buffer.parsing_idle())? - .await; - - let outline_items = buffer.read_with(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - snapshot - .outline(None) - .items - .into_iter() - .map(|item| item.to_point(&snapshot)) - .collect::>() - })?; - - // If no outline exists, fall back to first 1KB so the agent has some context - if outline_items.is_empty() { - let text = buffer.read_with(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - let len = snapshot.len().min(snapshot.as_rope().floor_char_boundary(1024)); - let content = snapshot.text_for_range(0..len).collect::(); - if let Some(path) = path { - format!("# First 1KB of {path} (file too large to show full content, and no outline available)\n\n{content}") - } else { - format!("# First 1KB of file (file too large to show full content, and no outline available)\n\n{content}") - } - })?; - - return Ok(BufferContent { - text, - is_outline: false, - }); - } - - let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?; - - let text = if let Some(path) = path { - format!("# File outline for {path}\n\n{outline_text}",) - } else { - format!("# File outline\n\n{outline_text}",) - }; - Ok(BufferContent { - text, - is_outline: true, - }) - } else { - // File is small enough, return full content - let text = buffer.read_with(cx, |buffer, _| buffer.text())?; - Ok(BufferContent { - text, - is_outline: false, - }) - } -} - -async fn render_outline( - items: impl IntoIterator>, - regex: Option, - offset: usize, - results_per_page: usize, -) -> Result { - let mut items = items.into_iter().skip(offset); - - let entries = items - .by_ref() - .filter(|item| { - regex - .as_ref() - .is_none_or(|regex| regex.is_match(&item.text)) - }) - .take(results_per_page) - .collect::>(); - let has_more = items.next().is_some(); - - let mut output = String::new(); - let entries_rendered = render_entries(&mut output, entries); - - // Calculate pagination information - let page_start = offset + 1; - let page_end = offset + entries_rendered; - let total_symbols = if has_more { - format!("more than {}", page_end) - } else { - page_end.to_string() - }; - - // Add pagination information - if has_more { - writeln!(&mut output, "\nShowing symbols {page_start}-{page_end} (there were more symbols found; use offset: {page_end} to see next page)", - ) - } else { - writeln!( - &mut output, - "\nShowing symbols {page_start}-{page_end} (total symbols: {total_symbols})", - ) - } - .ok(); - - Ok(output) -} - -fn render_entries( - output: &mut String, - items: impl IntoIterator>, -) -> usize { - let mut entries_rendered = 0; - - for item in items { - // Indent based on depth ("" for level 0, " " for level 1, etc.) - for _ in 0..item.depth { - output.push(' '); - } - output.push_str(&item.text); - - // Add position information - convert to 1-based line numbers for display - let start_line = item.range.start.row + 1; - let end_line = item.range.end.row + 1; - - if start_line == end_line { - writeln!(output, " [L{}]", start_line).ok(); - } else { - writeln!(output, " [L{}-{}]", start_line, end_line).ok(); - } - entries_rendered += 1; - } - - entries_rendered -} - -#[cfg(test)] -mod tests { - use super::*; - use fs::FakeFs; - use gpui::TestAppContext; - use project::Project; - use settings::SettingsStore; - - #[gpui::test] - async fn test_large_file_fallback_to_subset(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings = SettingsStore::test(cx); - cx.set_global(settings); - }); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - - let content = "⚡".repeat(100 * 1024); // 100KB - let content_len = content.len(); - let buffer = project - .update(cx, |project, cx| project.create_buffer(true, cx)) - .await - .expect("failed to create buffer"); - - buffer.update(cx, |buffer, cx| buffer.set_text(content, cx)); - - let result = cx - .spawn(|cx| async move { get_buffer_content_or_outline(buffer, None, &cx).await }) - .await - .unwrap(); - - // Should contain some of the actual file content - assert!( - result.text.contains("⚡⚡⚡⚡⚡⚡⚡"), - "Result did not contain content subset" - ); - - // Should be marked as not an outline (it's truncated content) - assert!( - !result.is_outline, - "Large file without outline should not be marked as outline" - ); - - // Should be reasonably sized (much smaller than original) - assert!( - result.text.len() < 50 * 1024, - "Result size {} should be smaller than 50KB", - result.text.len() - ); - - // Should be significantly smaller than the original content - assert!( - result.text.len() < content_len / 10, - "Result should be much smaller than original content" - ); - } -} diff --git a/crates/agent/src/templates.rs b/crates/agent/src/templates.rs deleted file mode 100644 index db787d834e..0000000000 --- a/crates/agent/src/templates.rs +++ /dev/null @@ -1,90 +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, - pub model_name: Option, -} - -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()], - model_name: Some("test-model".to_string()), - }; - let templates = Templates::new(); - let rendered = template.render(&templates).unwrap(); - assert!(rendered.contains("## Fixing Diagnostics")); - assert!(rendered.contains("test-model")); - } -} diff --git a/crates/agent/src/templates/create_file_prompt.hbs b/crates/agent/src/templates/create_file_prompt.hbs deleted file mode 100644 index 39f83447fa..0000000000 --- a/crates/agent/src/templates/create_file_prompt.hbs +++ /dev/null @@ -1,15 +0,0 @@ -You are an expert engineer and your task is to write a new file from scratch. - -You MUST respond with the file's content wrapped in triple backticks (```). -The backticks should be on their own line. -The text you output will be saved verbatim as the content of the file. -Tool calls have been disabled. -Start your response with ```. - - -{{path}} - - - -{{edit_description}} - diff --git a/crates/agent/src/templates/diff_judge.hbs b/crates/agent/src/templates/diff_judge.hbs deleted file mode 100644 index 0106cb4217..0000000000 --- a/crates/agent/src/templates/diff_judge.hbs +++ /dev/null @@ -1,23 +0,0 @@ -You are an expert coder, and have been tasked with looking at the following diff: - - -{{diff}} - - -Evaluate the following assertions: - - -{{assertions}} - - -You must respond with a short analysis and a score between 0 and 100, where: -- 0 means no assertions pass -- 100 means all the assertions pass perfectly - - -- Assertion 1: one line describing why the first assertion passes or fails (even partially) -- Assertion 2: one line describing why the second assertion passes or fails (even partially) -- ... -- Assertion N: one line describing why the Nth assertion passes or fails (even partially) - -YOUR FINAL SCORE HERE diff --git a/crates/agent/src/templates/edit_file_prompt_diff_fenced.hbs b/crates/agent/src/templates/edit_file_prompt_diff_fenced.hbs deleted file mode 100644 index a7db420a99..0000000000 --- a/crates/agent/src/templates/edit_file_prompt_diff_fenced.hbs +++ /dev/null @@ -1,77 +0,0 @@ -You MUST respond with a series of edits to a file, using the following diff format: - -``` -<<<<<<< SEARCH line=1 -from flask import Flask -======= -import math -from flask import Flask ->>>>>>> REPLACE - -<<<<<<< SEARCH line=325 -return 0 -======= -print("Done") - -return 0 ->>>>>>> REPLACE - -``` - -# File Editing Instructions - -- Use the SEARCH/REPLACE diff format shown above -- The SEARCH section must exactly match existing file content, including indentation -- The SEARCH section must come from the actual file, not an outline -- The SEARCH section cannot be empty -- `line` should be a starting line number for the text to be replaced -- 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 -- For multiple occurrences, repeat the same diff block for each instance -- Edits are sequential - each assumes previous edits are already applied -- Only edit the specified file - -# Example - -``` -<<<<<<< SEARCH line=3 -struct User { - name: String, - email: String, -} -======= -struct User { - name: String, - email: String, - active: bool, -} ->>>>>>> REPLACE - -<<<<<<< SEARCH line=25 - let user = User { - name: String::from("John"), - email: String::from("john@example.com"), - }; -======= - let user = User { - name: String::from("John"), - email: String::from("john@example.com"), - active: true, - }; ->>>>>>> REPLACE -``` - - -# Final instructions - -Tool calls have been disabled. You MUST respond using the SEARCH/REPLACE diff format only. - - -{{path}} - - - -{{edit_description}} - diff --git a/crates/agent/src/templates/edit_file_prompt_xml.hbs b/crates/agent/src/templates/edit_file_prompt_xml.hbs deleted file mode 100644 index db17c527aa..0000000000 --- a/crates/agent/src/templates/edit_file_prompt_xml.hbs +++ /dev/null @@ -1,92 +0,0 @@ -You MUST respond with a series of edits to a file, using the following format: - -``` - - - -OLD TEXT 1 HERE - - -NEW TEXT 1 HERE - - - -OLD TEXT 2 HERE - - -NEW TEXT 2 HERE - - - -OLD TEXT 3 HERE - - -NEW TEXT 3 HERE - - - -``` - -# File Editing Instructions - -- Use `` and `` tags to replace content -- `` must exactly match existing file content, including indentation -- `` must come from the actual file, not an outline -- `` cannot be empty -- `line` should be a starting line number for the text to be replaced -- 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 within tags -- For multiple occurrences, repeat the same tag pair for each instance -- Edits are sequential - each assumes previous edits are already applied -- Only edit the specified file -- Always close all tags properly - - -{{!-- The following example adds almost 10% pass rate for Gemini 2.5. -Claude and gpt-4.1 don't really need it. --}} - - - - -struct User { - name: String, - email: String, -} - - -struct User { - name: String, - email: String, - active: bool, -} - - - - let user = User { - name: String::from("John"), - email: String::from("john@example.com"), - }; - - - let user = User { - name: String::from("John"), - email: String::from("john@example.com"), - active: true, - }; - - - - - - - -{{path}} - - - -{{edit_description}} - - -Tool calls have been disabled. You MUST start your response with . diff --git a/crates/agent/src/templates/system_prompt.hbs b/crates/agent/src/templates/system_prompt.hbs deleted file mode 100644 index 4620647135..0000000000 --- a/crates/agent/src/templates/system_prompt.hbs +++ /dev/null @@ -1,188 +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 model_name}} -## Model Information - -You are powered by the model named {{model_name}}. - -{{/if}} -{{#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/agent/src/tests/mod.rs b/crates/agent/src/tests/mod.rs deleted file mode 100644 index 9ff8703532..0000000000 --- a/crates/agent/src/tests/mod.rs +++ /dev/null @@ -1,2598 +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] -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_system_prompt_without_tools(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(), ["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("## Tool Use"), - "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, - reasoning_details: None, - }] - ); - 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, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec!["Response to Message 1".into()], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec!["Message 2".into()], - cache: true, - reasoning_details: None, - } - ] - ); - 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, - thought_signature: None, - }; - 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, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec!["Response to Message 1".into()], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec!["Message 2".into()], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec!["Response to Message 2".into()], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec!["Use the echo tool".into()], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec![MessageContent::ToolUse(tool_use)], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec![MessageContent::ToolResult(tool_result)], - cache: true, - reasoning_details: None, - } - ] - ); -} - -#[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, - thought_signature: None, - }, - )); - 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, - thought_signature: None, - }, - )); - 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].option_id.clone()) - .unwrap(); - cx.run_until_parked(); - - // Reject the second - tool_call_auth_2 - .response - .send(tool_call_auth_1.options[2].option_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.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.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, - thought_signature: None, - }, - )); - 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].option_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.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, - thought_signature: None, - }, - )); - 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, - thought_signature: None, - }, - )); - 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, - thought_signature: None, - }; - 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, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec![MessageContent::ToolUse(tool_use.clone())], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec![MessageContent::ToolResult(tool_result.clone())], - cache: true, - reasoning_details: None, - }, - ] - ); - - // Simulate reaching tool use limit. - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::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, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec![MessageContent::ToolUse(tool_use)], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec![MessageContent::ToolResult(tool_result)], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec!["Continue where you left off".into()], - cache: true, - reasoning_details: None, - } - ] - ); - - 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, - thought_signature: None, - }; - 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::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, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec![MessageContent::ToolUse(tool_use)], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec![MessageContent::ToolResult(tool_result)], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec!["ghi".into()], - cache: true, - reasoning_details: None, - } - ] - ); -} - -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()), cx); - 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()), cx); - 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": { - "always_allow_tool_actions": true, - "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, cx| { - thread.set_profile(AgentProfileId("test".into()), cx) - }); - - 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, - thought_signature: None, - }, - )); - 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, - thought_signature: None, - }, - )); - 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, - thought_signature: None, - }, - )); - 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, cx| { - thread.set_profile(AgentProfileId("test".into()), cx); - 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.tool_call_id); - } - } - ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields( - acp::ToolCallUpdate { - tool_call_id, - fields: - acp::ToolCallUpdateFields { - status: Some(acp::ToolCallStatus::Completed), - .. - }, - .. - }, - )) if Some(&tool_call_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] -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] -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, - thought_signature: None, - }; - 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, - thought_signature: None, - }; - 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, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec![ - MessageContent::Text("Hi!".into()), - MessageContent::ToolUse(echo_tool_use.clone()) - ], - cache: false, - reasoning_details: None, - }, - 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, - reasoning_details: None, - }, - ], - ); -} - -#[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); - - 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); - LanguageModelRegistry::test(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 text_thread_store = - cx.new(|cx| assistant_text_thread::TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_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()); - - // 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 model_selector returns Some - let selector_opt = connection.model_selector(&session_id); - assert!( - selector_opt.is_some(), - "agent 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 - .as_ref(), - "fake/fake" - ); - - // Test selected_model returns the default - let model = cx - .update(|cx| selector.selected_model(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::new(session_id.clone(), 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, - thought_signature: None, - }, - )); - - // 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, - thought_signature: None, - }, - )); - 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::new("1", "Thinking") - .kind(acp::ToolKind::Think) - .raw_input(json!({})) - .meta(acp::Meta::from_iter([( - "tool_name".into(), - "thinking".into() - )])) - ); - let update = expect_tool_call_update_fields(&mut events).await; - assert_eq!( - update, - acp::ToolCallUpdate::new( - "1", - acp::ToolCallUpdateFields::new() - .title("Thinking") - .kind(acp::ToolKind::Think) - .raw_input(json!({ "content": "Thinking hard!"})) - ) - ); - let update = expect_tool_call_update_fields(&mut events).await; - assert_eq!( - update, - acp::ToolCallUpdate::new( - "1", - acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::InProgress) - ) - ); - let update = expect_tool_call_update_fields(&mut events).await; - assert_eq!( - update, - acp::ToolCallUpdate::new( - "1", - acp::ToolCallUpdateFields::new().content(vec!["Thinking hard!".into()]) - ) - ); - let update = expect_tool_call_update_fields(&mut events).await; - assert_eq!( - update, - acp::ToolCallUpdate::new( - "1", - acp::ToolCallUpdateFields::new() - .status(acp::ToolCallStatus::Completed) - .raw_output("Finished thinking.") - ) - ); -} - -#[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, - thought_signature: None, - }; - 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, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())], - cache: false, - reasoning_details: None, - }, - 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, - reasoning_details: None, - }, - ] - ); - - 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(), - reasoning_details: None, - })) - ); - }) -} - -#[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); - - match model { - TestModel::Fake => {} - TestModel::Sonnet4 => { - gpui_tokio::init(cx); - let http_client = ReqwestClient::user_agent("agent tests").unwrap(); - cx.set_http_client(Arc::new(http_client)); - 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::Stdio { - enabled: true, - command: ContextServerCommand { - path: "somebinary".into(), - args: Vec::new(), - env: None, - timeout: 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/agent/src/tests/test_tools.rs b/crates/agent/src/tests/test_tools.rs deleted file mode 100644 index 2275d23c2f..0000000000 --- a/crates/agent/src/tests/test_tools.rs +++ /dev/null @@ -1,221 +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, - _cx: &mut App, - ) -> 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, - _cx: &mut App, - ) -> 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, - _cx: &mut App, - ) -> 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, - _cx: &mut App, - ) -> 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, - _cx: &mut App, - ) -> 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/agent/src/thread.rs b/crates/agent/src/thread.rs deleted file mode 100644 index 4aabf8069b..0000000000 --- a/crates/agent/src/thread.rs +++ /dev/null @@ -1,2664 +0,0 @@ -use crate::{ - ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DbLanguageModel, DbThread, - DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool, - ListDirectoryTool, MovePathTool, NowTool, OpenTool, ProjectSnapshot, ReadFileTool, - SystemPromptTemplate, Template, Templates, TerminalTool, ThinkingTool, WebSearchTool, -}; -use acp_thread::{MentionUri, UserMessageId}; -use action_log::ActionLog; - -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 chrono::{DateTime, Utc}; -use client::{ModelRequestUsage, RequestUsage, UserStore}; -use cloud_llm_client::{CompletionIntent, Plan, UsageLimit}; -use collections::{HashMap, HashSet, IndexMap}; -use fs::Fs; -use futures::stream; -use futures::{ - FutureExt, - channel::{mpsc, oneshot}, - future::Shared, - stream::FuturesUnordered, -}; -use gpui::{ - App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity, -}; -use language_model::{ - LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelExt, - LanguageModelId, LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, - LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, - LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, - LanguageModelToolUse, LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, - ZED_CLOUD_PROVIDER_ID, -}; -use project::Project; -use prompt_store::ProjectContext; -use schemars::{JsonSchema, Schema}; -use serde::{Deserialize, Serialize}; -use settings::{LanguageModelSelection, Settings, update_settings_file}; -use smol::stream::StreamExt; -use std::{ - collections::BTreeMap, - ops::RangeInclusive, - path::Path, - rc::Rc, - sync::Arc, - time::{Duration, Instant}, -}; -use std::{fmt::Write, path::PathBuf}; -use util::{ResultExt, debug_panic, markdown::MarkdownCodeBlock, paths::PathStyle}; -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, - reasoning_details: None, - }], - } - } - - 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, - reasoning_details: None, - }; - - 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, - reasoning_details: self.reasoning_details.clone(), - }; - 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, - reasoning_details: None, - }; - - for tool_result in self.tool_results.values() { - let mut tool_result = tool_result.clone(); - // Surprisingly, the API fails if we return an empty string here. - // It thinks we are sending a tool use without a tool result. - if tool_result.content.is_empty() { - tool_result.content = "".into(); - } - user_message - .content - .push(language_model::MessageContent::ToolResult(tool_result)); - } - - let mut messages = Vec::new(); - if !assistant_message.content.is_empty() { - messages.push(assistant_message); - } - if !user_message.content.is_empty() { - messages.push(user_message); - } - messages - } -} - -#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct AgentMessage { - pub content: Vec, - pub tool_results: IndexMap, - pub reasoning_details: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum AgentMessageContent { - Text(String), - Thinking { - text: String, - signature: Option, - }, - RedactedThinking(String), - ToolUse(LanguageModelToolUse), -} - -pub trait TerminalHandle { - fn id(&self, cx: &AsyncApp) -> Result; - fn current_output(&self, cx: &AsyncApp) -> Result; - fn wait_for_exit(&self, cx: &AsyncApp) -> Result>>; -} - -pub trait ThreadEnvironment { - fn create_terminal( - &self, - command: String, - cwd: Option, - output_byte_limit: Option, - cx: &mut AsyncApp, - ) -> Task>>; -} - -#[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 NewTerminal { - pub command: String, - pub output_byte_limit: Option, - pub cwd: Option, - pub response: oneshot::Sender>>, -} - -#[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>, - pending_summary_generation: Option>>>, - summary: Option, - messages: Vec, - user_store: Entity, - 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, - /// Tracks the last time files were read by the agent, to detect external modifications - pub(crate) file_read_times: HashMap, -} - -impl Thread { - fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities { - let image = model.map_or(true, |model| model.supports_images()); - acp::PromptCapabilities::new() - .image(image) - .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::new(uuid::Uuid::new_v4().to_string()), - prompt_id: PromptId::new(), - updated_at: Utc::now(), - title: None, - pending_title_generation: None, - pending_summary_generation: None, - summary: None, - messages: Vec::new(), - user_store: project.read(cx).user_store(), - 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, - file_read_times: HashMap::default(), - } - } - - 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::new(tool_use.id.to_string(), tool_use.name.to_string()) - .status(acp::ToolCallStatus::Failed) - .raw_input(tool_use.input.clone()), - ))) - .ok(); - return; - }; - - let title = tool.initial_title(tool_use.input.clone(), cx); - let kind = tool.kind(); - stream.send_tool_call( - &tool_use.id, - &tool_use.name, - 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::new() - .status( - tool_result - .as_ref() - .map_or(acp::ToolCallStatus::Failed, |result| { - if result.is_error { - acp::ToolCallStatus::Failed - } else { - acp::ToolCallStatus::Completed - } - }), - ) - .raw_output(output), - ); - } - - pub fn from_db( - id: acp::SessionId, - db_thread: DbThread, - project: Entity, - project_context: Entity, - context_server_registry: Entity, - templates: Arc, - cx: &mut Context, - ) -> Self { - let profile_id = db_thread - .profile - .unwrap_or_else(|| AgentSettings::get_global(cx).default_profile.clone()); - - let mut 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) - }); - - if model.is_none() { - model = Self::resolve_profile_model(&profile_id, cx); - } - if model.is_none() { - model = LanguageModelRegistry::global(cx).update(cx, |registry, _cx| { - registry.default_model().map(|model| model.model) - }); - } - - let (prompt_capabilities_tx, prompt_capabilities_rx) = - watch::channel(Self::prompt_capabilities(model.as_deref())); - - let action_log = cx.new(|_| ActionLog::new(project.clone())); - - Self { - id, - prompt_id: PromptId::new(), - title: if db_thread.title.is_empty() { - None - } else { - Some(db_thread.title.clone()) - }, - pending_title_generation: None, - pending_summary_generation: None, - summary: db_thread.detailed_summary, - messages: db_thread.messages, - user_store: project.read(cx).user_store(), - 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, - file_read_times: HashMap::default(), - } - } - - 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 task = project::telemetry_snapshot::TelemetrySnapshot::new(&project, cx); - cx.spawn(async move |_, _| { - let snapshot = task.await; - - Arc::new(ProjectSnapshot { - worktree_snapshots: snapshot.worktree_snapshots, - timestamp: Utc::now(), - }) - }) - } - - 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, - environment: Rc, - 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( - self.project.clone(), - cx.weak_entity(), - language_registry, - Templates::new(), - )); - 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( - cx.weak_entity(), - self.project.clone(), - self.action_log.clone(), - )); - self.add_tool(TerminalTool::new(self.project.clone(), environment)); - self.add_tool(ThinkingTool); - self.add_tool(WebSearchTool); - } - - pub fn add_tool(&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, cx: &mut Context) { - if self.profile_id == profile_id { - return; - } - - self.profile_id = profile_id; - - // Swap to the profile's preferred model when available. - if let Some(model) = Self::resolve_profile_model(&self.profile_id, cx) { - self.set_model(model, cx); - } - } - - 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.clear_summary(); - cx.notify(); - Ok(()) - } - - pub fn latest_request_token_usage(&self) -> Option { - let last_user_message = self.last_user_message()?; - let tokens = self.request_token_usage.get(&last_user_message.id)?; - Some(*tokens) - } - - pub fn latest_token_usage(&self) -> Option { - let usage = self.latest_request_token_usage()?; - let model = self.model.clone()?; - Some(acp_thread::TokenUsage { - max_tokens: model.max_token_count_for_mode(self.completion_mode.into()), - used_tokens: usage.total_tokens(), - }) - } - - /// Look up the active profile and resolve its preferred model if one is configured. - fn resolve_profile_model( - profile_id: &AgentProfileId, - cx: &mut Context, - ) -> Option> { - let selection = AgentSettings::get_global(cx) - .profiles - .get(profile_id)? - .default_model - .clone()?; - Self::resolve_model_from_selection(&selection, cx) - } - - /// Translate a stored model selection into the configured model from the registry. - fn resolve_model_from_selection( - selection: &LanguageModelSelection, - cx: &mut Context, - ) -> Option> { - let selected = SelectedModel { - provider: LanguageModelProviderId::from(selection.provider.0.clone()), - model: LanguageModelId::from(selection.model.clone()), - }; - LanguageModelRegistry::global(cx).update(cx, |registry, cx| { - registry - .select_model(&selected, cx) - .map(|configured| configured.model) - }) - } - - 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) - } - - #[cfg(feature = "eval")] - pub fn proceed( - &mut self, - cx: &mut Context, - ) -> Result>> { - 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.clear_summary(); - 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, mut error) = match model.stream_completion(request, cx).await { - Ok(events) => (events, None), - Err(err) => (stream::empty().boxed(), Some(err)), - }; - let mut tool_results = FuturesUnordered::new(); - 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::new() - .status(if tool_result.is_error { - acp::ToolCallStatus::Failed - } else { - acp::ToolCallStatus::Completed - }) - .raw_output(tool_result.output.clone()), - ); - 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, cx| { - let user_store = this.user_store.read(cx); - this.handle_completion_error(error, attempt, user_store.plan()) - })??; - 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, - plan: Option, - ) -> Result { - let Some(model) = self.model.as_ref() else { - return Err(anyhow!(error)); - }; - - let auto_retry = if model.provider_id() == ZED_CLOUD_PROVIDER_ID { - match plan { - Some(Plan::V2(_)) => true, - Some(Plan::V1(_)) => self.completion_mode == CompletionMode::Burn, - None => false, - } - } else { - true - }; - - if !auto_retry { - 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), - ReasoningDetails(details) => { - let last_message = self.pending_message(); - // Store the last non-empty reasoning_details (overwrites earlier ones) - // This ensures we keep the encrypted reasoning with signatures, not the early text reasoning - if let serde_json::Value::Array(ref arr) = details { - if !arr.is_empty() { - last_message.reasoning_details = Some(details); - } - } else { - last_message.reasoning_details = Some(details); - } - } - 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); - } - UsageUpdated { amount, limit } => { - self.update_model_request_usage(amount, limit, cx); - } - 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) => {} - Started | Queued { .. } => {} - } - - 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(), cx); - 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, - &tool_use.name, - 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::new() - .title(title.as_str()) - .kind(kind) - .raw_input(tool_use.input.clone()), - ); - } - - 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::new().status(acp::ToolCallStatus::InProgress), - ); - 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 is_generating_summary(&self) -> bool { - self.pending_summary_generation.is_some() - } - - pub fn summary(&mut self, cx: &mut Context) -> Shared>> { - if let Some(summary) = self.summary.as_ref() { - return Task::ready(Some(summary.clone())).shared(); - } - if let Some(task) = self.pending_summary_generation.clone() { - return task; - } - let Some(model) = self.summarization_model.clone() else { - log::error!("No summarization model available"); - return Task::ready(None).shared(); - }; - 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, - reasoning_details: None, - }); - - let task = cx - .spawn(async move |this, cx| { - let mut summary = String::new(); - let mut messages = model.stream_completion(request, cx).await.log_err()?; - while let Some(event) = messages.next().await { - let event = event.log_err()?; - let text = match event { - LanguageModelCompletionEvent::Text(text) => text, - LanguageModelCompletionEvent::UsageUpdated { amount, limit } => { - this.update(cx, |thread, cx| { - thread.update_model_request_usage(amount, limit, cx); - }) - .ok()?; - 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()); - this.pending_summary_generation = None; - cx.notify() - }) - .ok()?; - - Some(summary) - }) - .shared(); - self.pending_summary_generation = Some(task.clone()); - task - } - - 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, - reasoning_details: None, - }); - 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::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 clear_summary(&mut self) { - self.summary = None; - self.pending_summary_generation = None; - } - - 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.clear_summary(); - 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 available_tools: Vec<_> = self - .running_turn - .as_ref() - .map(|turn| turn.tools.keys().cloned().collect()) - .unwrap_or_default(); - - log::debug!("Request includes {} tools", available_tools.len()); - let messages = self.build_request_messages(available_tools, cx); - log::debug!("Request will include {} messages", messages.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.supports_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, - available_tools: Vec, - 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, - model_name: self.model.as_ref().map(|m| m.name().0.to_string()), - } - .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, - reasoning_details: None, - }]; - 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() -> 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, - cx: &mut App, - ) -> SharedString; - - /// Returns the JSON schema that describes the tool's input. - fn input_schema(format: LanguageModelToolSchemaFormat) -> Schema { - language_model::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 supports_provider(_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, _cx: &mut App) -> SharedString; - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result; - fn supports_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 { - T::description() - } - - fn kind(&self) -> agent_client_protocol::ToolKind { - T::kind() - } - - fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString { - let parsed_input = serde_json::from_value(input.clone()).map_err(|_| input); - self.0.initial_title(parsed_input, _cx) - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - let mut json = serde_json::to_value(T::input_schema(format))?; - language_model::tool_schema::adapt_schema_to_format(&mut json, format)?; - Ok(json) - } - - fn supports_provider(&self, provider: &LanguageModelProviderId) -> bool { - T::supports_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, - tool_name: &str, - title: SharedString, - kind: acp::ToolKind, - input: serde_json::Value, - ) { - self.0 - .unbounded_send(Ok(ThreadEvent::ToolCall(Self::initial_tool_call( - id, - tool_name, - title.to_string(), - kind, - input, - )))) - .ok(); - } - - fn initial_tool_call( - id: &LanguageModelToolUseId, - tool_name: &str, - title: String, - kind: acp::ToolKind, - input: serde_json::Value, - ) -> acp::ToolCall { - acp::ToolCall::new(id.to_string(), title) - .kind(kind) - .raw_input(input) - .meta(acp::Meta::from_iter([( - "tool_name".into(), - tool_name.into(), - )])) - } - - fn update_tool_call_fields( - &self, - tool_use_id: &LanguageModelToolUseId, - fields: acp::ToolCallUpdateFields, - ) { - self.0 - .unbounded_send(Ok(ThreadEvent::ToolCallUpdate( - acp::ToolCallUpdate::new(tool_use_id.to_string(), 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(any(test, feature = "test-support"))] - 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::new(self.tool_use_id.to_string()), - diff, - } - .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::new( - self.tool_use_id.to_string(), - acp::ToolCallUpdateFields::new().title(title.into()), - ), - options: vec![ - acp::PermissionOption::new( - acp::PermissionOptionId::new("always_allow"), - "Always Allow", - acp::PermissionOptionKind::AllowAlways, - ), - acp::PermissionOption::new( - acp::PermissionOptionId::new("allow"), - "Allow", - acp::PermissionOptionKind::AllowOnce, - ), - acp::PermissionOption::new( - acp::PermissionOptionId::new("deny"), - "Deny", - 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 - .agent - .get_or_insert_default() - .set_always_allow_tool_actions(true); - }); - })?; - } - - Ok(()) - } - "allow" => Ok(()), - _ => Err(anyhow!("Permission to run tool denied by user")), - }) - } -} - -#[cfg(any(test, feature = "test-support"))] -pub struct ToolCallEventStreamReceiver(mpsc::UnboundedReceiver>); - -#[cfg(any(test, feature = "test-support"))] -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(any(test, feature = "test-support"))] -impl std::ops::Deref for ToolCallEventStreamReceiver { - type Target = mpsc::UnboundedReceiver>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[cfg(any(test, feature = "test-support"))] -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 UserMessageContent { - pub fn from_content_block(value: acp::ContentBlock, path_style: PathStyle) -> 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, path_style) { - 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, path_style) { - 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()) - } - other => { - log::warn!("Unexpected content type: {:?}", other); - Self::Text("[unknown]".to_string()) - } - }, - other => { - log::warn!("Unexpected content type: {:?}", other); - Self::Text("[unknown]".to_string()) - } - } - } -} - -impl From for acp::ContentBlock { - fn from(content: UserMessageContent) -> Self { - match content { - UserMessageContent::Text(text) => text.into(), - UserMessageContent::Image(image) => { - acp::ContentBlock::Image(acp::ImageContent::new(image.source, "image/png")) - } - UserMessageContent::Mention { uri, content } => acp::ContentBlock::Resource( - acp::EmbeddedResource::new(acp::EmbeddedResourceResource::TextResourceContents( - acp::TextResourceContents::new(content, uri.to_uri().to_string()), - )), - ), - } - } -} - -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/agent/src/tools.rs b/crates/agent/src/tools.rs deleted file mode 100644 index 62a52998a7..0000000000 --- a/crates/agent/src/tools.rs +++ /dev/null @@ -1,98 +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; - -use crate::AgentTool; -use language_model::{LanguageModelRequestTool, LanguageModelToolSchemaFormat}; - -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::*; - -macro_rules! tools { - ($($tool:ty),* $(,)?) => { - /// A list of all built-in tool names - pub fn supported_built_in_tool_names(provider: Option) -> impl Iterator { - [ - $( - (if let Some(provider) = provider.as_ref() { - <$tool>::supports_provider(provider) - } else { - true - }) - .then(|| <$tool>::name().to_string()), - )* - ] - .into_iter() - .flatten() - } - - /// A list of all built-in tools - pub fn built_in_tools() -> impl Iterator { - fn language_model_tool() -> LanguageModelRequestTool { - LanguageModelRequestTool { - name: T::name().to_string(), - description: T::description().to_string(), - input_schema: T::input_schema(LanguageModelToolSchemaFormat::JsonSchema).to_value(), - } - } - [ - $( - language_model_tool::<$tool>(), - )* - ] - .into_iter() - } - }; -} - -tools! { - CopyPathTool, - CreateDirectoryTool, - DeletePathTool, - DiagnosticsTool, - EditFileTool, - FetchTool, - FindPathTool, - GrepTool, - ListDirectoryTool, - MovePathTool, - NowTool, - OpenTool, - ReadFileTool, - TerminalTool, - ThinkingTool, - WebSearchTool, -} diff --git a/crates/agent/src/tools/context_server_registry.rs b/crates/agent/src/tools/context_server_registry.rs deleted file mode 100644 index 03a0ef84e7..0000000000 --- a/crates/agent/src/tools/context_server_registry.rs +++ /dev/null @@ -1,253 +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 tools_for_server( - &self, - server_id: &ContextServerId, - ) -> impl Iterator> { - self.registered_servers - .get(server_id) - .map(|server| server.tools.values()) - .into_iter() - .flatten() - } - - 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, _cx: &mut App) -> 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(); - language_model::tool_schema::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(); - let authorize = event_stream.authorize(self.initial_title(input.clone(), cx), cx); - - cx.spawn(async move |_cx| { - authorize.await?; - - 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/agent/src/tools/copy_path_tool.rs b/crates/agent/src/tools/copy_path_tool.rs deleted file mode 100644 index 236978c78f..0000000000 --- a/crates/agent/src/tools/copy_path_tool.rs +++ /dev/null @@ -1,113 +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. -/// -/// 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. - /// - /// - /// 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, - _cx: &mut App, - ) -> 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, project_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/agent/src/tools/create_directory_tool.rs b/crates/agent/src/tools/create_directory_tool.rs deleted file mode 100644 index b6240e99cf..0000000000 --- a/crates/agent/src/tools/create_directory_tool.rs +++ /dev/null @@ -1,90 +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. 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, - _cx: &mut App, - ) -> 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/agent/src/tools/delete_path_tool.rs b/crates/agent/src/tools/delete_path_tool.rs deleted file mode 100644 index 01a77f5d81..0000000000 --- a/crates/agent/src/tools/delete_path_tool.rs +++ /dev/null @@ -1,140 +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, - _cx: &mut App, - ) -> 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/agent/src/tools/diagnostics_tool.rs b/crates/agent/src/tools/diagnostics_tool.rs deleted file mode 100644 index f07ec4cfe6..0000000000 --- a/crates/agent/src/tools/diagnostics_tool.rs +++ /dev/null @@ -1,165 +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, 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, - _cx: &mut App, - ) -> SharedString { - if let Some(path) = input.ok().and_then(|input| match input.path { - Some(path) if !path.is_empty() => Some(path), - _ => None, - }) { - 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", - worktree.read(cx).absolutize(&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/agent/src/tools/edit_file_tool.rs b/crates/agent/src/tools/edit_file_tool.rs deleted file mode 100644 index 0ab99426e2..0000000000 --- a/crates/agent/src/tools/edit_file_tool.rs +++ /dev/null @@ -1,2210 +0,0 @@ -use crate::{ - AgentTool, Templates, Thread, ToolCallEventStream, - edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat}, -}; -use acp_thread::Diff; -use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields}; -use anyhow::{Context as _, Result, anyhow}; -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::ffi::OsStr; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use ui::SharedString; -use util::ResultExt; -use util::rel_path::RelPath; - -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(Clone, 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(Clone, 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")] -#[schemars(inline)] -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, - project: Entity, - templates: Arc, -} - -impl EditFileTool { - pub fn new( - project: Entity, - thread: WeakEntity, - language_registry: Arc, - templates: Arc, - ) -> Self { - Self { - project, - thread, - language_registry, - templates, - } - } - - 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_name(); - let path = Path::new(&input.path); - if path.components().any(|component| { - component.as_os_str() == <_ as AsRef>::as_ref(&local_settings_folder) - }) { - 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. - // TODO this is broken when remoting - 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, - cx: &mut App, - ) -> SharedString { - match input { - Ok(input) => self - .project - .read(cx) - .find_project_path(&input.path, cx) - .and_then(|project_path| { - self.project - .read(cx) - .short_full_path_for_project_path(&project_path, cx) - }) - .unwrap_or(input.path.to_string_lossy().into_owned()) - .into(), - Err(raw_input) => { - if let Some(input) = - serde_json::from_value::(raw_input).ok() - { - let path = input.path.trim(); - if !path.is_empty() { - return self - .project - .read(cx) - .find_project_path(&input.path, cx) - .and_then(|project_path| { - self.project - .read(cx) - .short_full_path_for_project_path(&project_path, cx) - }) - .unwrap_or(input.path) - .into(); - } - - let description = input.display_description.trim(); - if !description.is_empty() { - return description.to_string().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::new().locations(vec![acp::ToolCallLocation::new(abs_path)]), - ); - } - - 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(), - self.templates.clone(), - edit_format, - ); - - let buffer = project - .update(cx, |project, cx| { - project.open_buffer(project_path.clone(), cx) - })? - .await?; - - // Check if the file has been modified since the agent last read it - if let Some(abs_path) = abs_path.as_ref() { - let (last_read_mtime, current_mtime, is_dirty) = self.thread.update(cx, |thread, cx| { - let last_read = thread.file_read_times.get(abs_path).copied(); - let current = buffer.read(cx).file().and_then(|file| file.disk_state().mtime()); - let dirty = buffer.read(cx).is_dirty(); - (last_read, current, dirty) - })?; - - // Check for unsaved changes first - these indicate modifications we don't know about - if is_dirty { - anyhow::bail!( - "This file cannot be written to because it has unsaved changes. \ - Please end the current conversation immediately by telling the user you want to write to this file (mention its path explicitly) but you can't write to it because it has unsaved changes. \ - Ask the user to save that buffer's changes and to inform you when it's ok to proceed." - ); - } - - // Check if the file was modified on disk since we last read it - if let (Some(last_read), Some(current)) = (last_read_mtime, current_mtime) { - // MTime can be unreliable for comparisons, so our newtype intentionally - // doesn't support comparing them. If the mtime at all different - // (which could be because of a modification or because e.g. system clock changed), - // we pessimistically assume it was modified. - if current != last_read { - anyhow::bail!( - "The file {} has been modified since you last read it. \ - Please read the file again to get the current state before editing it.", - input.path.display() - ); - } - } - } - - 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::new().locations(vec![ToolCallLocation::new(abs_path).line(line)])); - } - 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); - })?; - - // Update the recorded read time after a successful edit so consecutive edits work - if let Some(abs_path) = abs_path.as_ref() { - if let Some(new_mtime) = buffer.read_with(cx, |buffer, _| { - buffer.file().and_then(|file| file.disk_state().mtime()) - })? { - self.thread.update(cx, |thread, _| { - thread.file_read_times.insert(abs_path.to_path_buf(), new_mtime); - })?; - } - } - - 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.to_string_lossy().into_owned(), - 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() - .and_then(|file_name| file_name.to_str()) - .and_then(|file_name| RelPath::unix(file_name).ok()) - .context("Can't create file: invalid filename")?; - - let new_file_path = parent_project_path.map(|parent| ProjectPath { - path: 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 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, rel_path::rel_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.clone(), - 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( - project, - thread.downgrade(), - language_registry, - Templates::new(), - )) - .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, rel_path("new.txt")); - - let result = test_resolve_path(mode, "new.txt", cx); - assert_resolved_path_eq(result.await, rel_path("new.txt")); - - let result = test_resolve_path(mode, "dir/new.txt", cx); - assert_resolved_path_eq(result.await, rel_path("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, rel_path(path_without_root)); - - let result = test_resolve_path(mode, path_without_root, cx); - assert_resolved_path_eq(result.await, rel_path(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)) - } - - #[track_caller] - fn assert_resolved_path_eq(path: anyhow::Result, expected: &RelPath) { - let actual = path.expect("Should return valid path").path; - assert_eq!(actual.as_ref(), 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.clone(), - 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.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On); - settings.project.all_languages.defaults.formatter = - Some(language::language_settings::FormatterList::default()); - }); - }); - }); - - // 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( - project.clone(), - thread.downgrade(), - language_registry.clone(), - Templates::new(), - )) - .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.project.all_languages.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( - project.clone(), - thread.downgrade(), - language_registry, - Templates::new(), - )) - .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.clone(), - 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 - .project - .all_languages - .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( - project.clone(), - thread.downgrade(), - language_registry.clone(), - Templates::new(), - )) - .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 - .project - .all_languages - .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( - project.clone(), - thread.downgrade(), - language_registry, - Templates::new(), - )) - .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.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model.clone()), - cx, - ) - }); - let tool = Arc::new(EditFileTool::new( - project.clone(), - thread.downgrade(), - language_registry, - Templates::new(), - )); - 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.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model.clone()), - cx, - ) - }); - let tool = Arc::new(EditFileTool::new( - project.clone(), - thread.downgrade(), - language_registry, - Templates::new(), - )); - - // 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( - project.clone(), - thread.downgrade(), - language_registry, - Templates::new(), - )); - - // 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( - project.clone(), - thread.downgrade(), - language_registry, - Templates::new(), - )); - - // 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", - true, - "Path with .. that goes outside of root directory", - ), - ( - "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, - ) - }); - - cx.run_until_parked(); - - if should_confirm { - stream_rx.expect_authorization().await; - } else { - assert!( - stream_rx.try_next().is_err(), - "Failed for case: {} - path: {} - expected no confirmation but got one", - description, - path - ); - auth.await.unwrap(); - } - } - } - - #[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( - project.clone(), - thread.downgrade(), - language_registry, - Templates::new(), - )); - - // 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( - project, - thread.downgrade(), - language_registry, - Templates::new(), - )); - - cx.update(|cx| { - // ... - assert_eq!( - tool.initial_title( - Err(json!({ - "path": "src/main.rs", - "display_description": "", - "old_string": "old code", - "new_string": "new code" - })), - cx - ), - "src/main.rs" - ); - assert_eq!( - tool.initial_title( - Err(json!({ - "path": "", - "display_description": "Fix error handling", - "old_string": "old code", - "new_string": "new code" - })), - cx - ), - "Fix error handling" - ); - assert_eq!( - tool.initial_title( - Err(json!({ - "path": "src/main.rs", - "display_description": "Fix error handling", - "old_string": "old code", - "new_string": "new code" - })), - cx - ), - "src/main.rs" - ); - assert_eq!( - tool.initial_title( - Err(json!({ - "path": "", - "display_description": "", - "old_string": "old code", - "new_string": "new code" - })), - cx - ), - DEFAULT_UI_TEXT - ); - assert_eq!( - tool.initial_title(Err(serde_json::Value::Null), cx), - DEFAULT_UI_TEXT - ); - }); - } - - #[gpui::test] - async fn test_diff_finalization(cx: &mut TestAppContext) { - init_test(cx); - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/", json!({"main.rs": ""})).await; - - let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await; - let languages = project.read_with(cx, |project, _cx| project.languages().clone()); - let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry.clone(), - Templates::new(), - Some(model.clone()), - cx, - ) - }); - - // Ensure the diff is finalized after the edit completes. - { - let tool = Arc::new(EditFileTool::new( - project.clone(), - thread.downgrade(), - languages.clone(), - Templates::new(), - )); - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let edit = cx.update(|cx| { - tool.run( - EditFileToolInput { - display_description: "Edit file".into(), - path: path!("/main.rs").into(), - mode: EditFileMode::Edit, - }, - stream_tx, - cx, - ) - }); - stream_rx.expect_update_fields().await; - let diff = stream_rx.expect_diff().await; - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_)))); - cx.run_until_parked(); - model.end_last_completion_stream(); - edit.await.unwrap(); - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_)))); - } - - // Ensure the diff is finalized if an error occurs while editing. - { - model.forbid_requests(); - let tool = Arc::new(EditFileTool::new( - project.clone(), - thread.downgrade(), - languages.clone(), - Templates::new(), - )); - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let edit = cx.update(|cx| { - tool.run( - EditFileToolInput { - display_description: "Edit file".into(), - path: path!("/main.rs").into(), - mode: EditFileMode::Edit, - }, - stream_tx, - cx, - ) - }); - stream_rx.expect_update_fields().await; - let diff = stream_rx.expect_diff().await; - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_)))); - edit.await.unwrap_err(); - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_)))); - model.allow_requests(); - } - - // Ensure the diff is finalized if the tool call gets dropped. - { - let tool = Arc::new(EditFileTool::new( - project.clone(), - thread.downgrade(), - languages.clone(), - Templates::new(), - )); - 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(_)))); - } - } - - #[gpui::test] - async fn test_file_read_times_tracking(cx: &mut TestAppContext) { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "test.txt": "original content" - }), - ) - .await; - 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 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 action_log = thread.read_with(cx, |thread, _| thread.action_log().clone()); - - // Initially, file_read_times should be empty - let is_empty = thread.read_with(cx, |thread, _| thread.file_read_times.is_empty()); - assert!(is_empty, "file_read_times should start empty"); - - // Create read tool - let read_tool = Arc::new(crate::ReadFileTool::new( - thread.downgrade(), - project.clone(), - action_log, - )); - - // Read the file to record the read time - cx.update(|cx| { - read_tool.clone().run( - crate::ReadFileToolInput { - path: "root/test.txt".to_string(), - start_line: None, - end_line: None, - }, - ToolCallEventStream::test().0, - cx, - ) - }) - .await - .unwrap(); - - // Verify that file_read_times now contains an entry for the file - let has_entry = thread.read_with(cx, |thread, _| { - thread.file_read_times.len() == 1 - && thread - .file_read_times - .keys() - .any(|path| path.ends_with("test.txt")) - }); - assert!( - has_entry, - "file_read_times should contain an entry after reading the file" - ); - - // Read the file again - should update the entry - cx.update(|cx| { - read_tool.clone().run( - crate::ReadFileToolInput { - path: "root/test.txt".to_string(), - start_line: None, - end_line: None, - }, - ToolCallEventStream::test().0, - cx, - ) - }) - .await - .unwrap(); - - // Should still have exactly one entry - let has_one_entry = thread.read_with(cx, |thread, _| thread.file_read_times.len() == 1); - assert!( - has_one_entry, - "file_read_times should still have one entry after re-reading" - ); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - }); - } - - #[gpui::test] - async fn test_consecutive_edits_work(cx: &mut TestAppContext) { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "test.txt": "original content" - }), - ) - .await; - 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 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 languages = project.read_with(cx, |project, _| project.languages().clone()); - let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone()); - - let read_tool = Arc::new(crate::ReadFileTool::new( - thread.downgrade(), - project.clone(), - action_log, - )); - let edit_tool = Arc::new(EditFileTool::new( - project.clone(), - thread.downgrade(), - languages, - Templates::new(), - )); - - // Read the file first - cx.update(|cx| { - read_tool.clone().run( - crate::ReadFileToolInput { - path: "root/test.txt".to_string(), - start_line: None, - end_line: None, - }, - ToolCallEventStream::test().0, - cx, - ) - }) - .await - .unwrap(); - - // First edit should work - let edit_result = { - let edit_task = cx.update(|cx| { - edit_tool.clone().run( - EditFileToolInput { - display_description: "First edit".into(), - path: "root/test.txt".into(), - mode: EditFileMode::Edit, - }, - ToolCallEventStream::test().0, - cx, - ) - }); - - cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk( - "original contentmodified content" - .to_string(), - ); - model.end_last_completion_stream(); - - edit_task.await - }; - assert!( - edit_result.is_ok(), - "First edit should succeed, got error: {:?}", - edit_result.as_ref().err() - ); - - // Second edit should also work because the edit updated the recorded read time - let edit_result = { - let edit_task = cx.update(|cx| { - edit_tool.clone().run( - EditFileToolInput { - display_description: "Second edit".into(), - path: "root/test.txt".into(), - mode: EditFileMode::Edit, - }, - ToolCallEventStream::test().0, - cx, - ) - }); - - cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk( - "modified contentfurther modified content".to_string(), - ); - model.end_last_completion_stream(); - - edit_task.await - }; - assert!( - edit_result.is_ok(), - "Second consecutive edit should succeed, got error: {:?}", - edit_result.as_ref().err() - ); - } - - #[gpui::test] - async fn test_external_modification_detected(cx: &mut TestAppContext) { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "test.txt": "original content" - }), - ) - .await; - 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 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 languages = project.read_with(cx, |project, _| project.languages().clone()); - let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone()); - - let read_tool = Arc::new(crate::ReadFileTool::new( - thread.downgrade(), - project.clone(), - action_log, - )); - let edit_tool = Arc::new(EditFileTool::new( - project.clone(), - thread.downgrade(), - languages, - Templates::new(), - )); - - // Read the file first - cx.update(|cx| { - read_tool.clone().run( - crate::ReadFileToolInput { - path: "root/test.txt".to_string(), - start_line: None, - end_line: None, - }, - ToolCallEventStream::test().0, - cx, - ) - }) - .await - .unwrap(); - - // Simulate external modification - advance time and save file - cx.background_executor - .advance_clock(std::time::Duration::from_secs(2)); - fs.save( - path!("/root/test.txt").as_ref(), - &"externally modified content".into(), - language::LineEnding::Unix, - ) - .await - .unwrap(); - - // Reload the buffer to pick up the new mtime - let project_path = project - .read_with(cx, |project, cx| { - project.find_project_path("root/test.txt", cx) - }) - .expect("Should find project path"); - let buffer = project - .update(cx, |project, cx| project.open_buffer(project_path, cx)) - .await - .unwrap(); - buffer - .update(cx, |buffer, cx| buffer.reload(cx)) - .await - .unwrap(); - - cx.executor().run_until_parked(); - - // Try to edit - should fail because file was modified externally - let result = cx - .update(|cx| { - edit_tool.clone().run( - EditFileToolInput { - display_description: "Edit after external change".into(), - path: "root/test.txt".into(), - mode: EditFileMode::Edit, - }, - ToolCallEventStream::test().0, - cx, - ) - }) - .await; - - assert!( - result.is_err(), - "Edit should fail after external modification" - ); - let error_msg = result.unwrap_err().to_string(); - assert!( - error_msg.contains("has been modified since you last read it"), - "Error should mention file modification, got: {}", - error_msg - ); - } - - #[gpui::test] - async fn test_dirty_buffer_detected(cx: &mut TestAppContext) { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "test.txt": "original content" - }), - ) - .await; - 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 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 languages = project.read_with(cx, |project, _| project.languages().clone()); - let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone()); - - let read_tool = Arc::new(crate::ReadFileTool::new( - thread.downgrade(), - project.clone(), - action_log, - )); - let edit_tool = Arc::new(EditFileTool::new( - project.clone(), - thread.downgrade(), - languages, - Templates::new(), - )); - - // Read the file first - cx.update(|cx| { - read_tool.clone().run( - crate::ReadFileToolInput { - path: "root/test.txt".to_string(), - start_line: None, - end_line: None, - }, - ToolCallEventStream::test().0, - cx, - ) - }) - .await - .unwrap(); - - // Open the buffer and make it dirty by editing without saving - let project_path = project - .read_with(cx, |project, cx| { - project.find_project_path("root/test.txt", cx) - }) - .expect("Should find project path"); - let buffer = project - .update(cx, |project, cx| project.open_buffer(project_path, cx)) - .await - .unwrap(); - - // Make an in-memory edit to the buffer (making it dirty) - buffer.update(cx, |buffer, cx| { - let end_point = buffer.max_point(); - buffer.edit([(end_point..end_point, " added text")], None, cx); - }); - - // Verify buffer is dirty - let is_dirty = buffer.read_with(cx, |buffer, _| buffer.is_dirty()); - assert!(is_dirty, "Buffer should be dirty after in-memory edit"); - - // Try to edit - should fail because buffer has unsaved changes - let result = cx - .update(|cx| { - edit_tool.clone().run( - EditFileToolInput { - display_description: "Edit with dirty buffer".into(), - path: "root/test.txt".into(), - mode: EditFileMode::Edit, - }, - ToolCallEventStream::test().0, - cx, - ) - }) - .await; - - assert!(result.is_err(), "Edit should fail when buffer is dirty"); - let error_msg = result.unwrap_err().to_string(); - assert!( - error_msg.contains("cannot be written to because it has unsaved changes"), - "Error should mention unsaved changes, got: {}", - error_msg - ); - } -} diff --git a/crates/agent/src/tools/fetch_tool.rs b/crates/agent/src/tools/fetch_tool.rs deleted file mode 100644 index 60654ac863..0000000000 --- a/crates/agent/src/tools/fetch_tool.rs +++ /dev/null @@ -1,164 +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, - _cx: &mut App, - ) -> 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/agent/src/tools/find_path_tool.rs b/crates/agent/src/tools/find_path_tool.rs deleted file mode 100644 index 2a33b14b4c..0000000000 --- a/crates/agent/src/tools/find_path_tool.rs +++ /dev/null @@ -1,247 +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, - _cx: &mut App, - ) -> 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::new() - .title(if paginated_matches.is_empty() { - "No matches".into() - } else if paginated_matches.len() == 1 { - "1 match".into() - } else { - format!("{} matches", paginated_matches.len()) - }) - .content( - paginated_matches - .iter() - .map(|path| { - acp::ToolCallContent::Content(acp::Content::new( - acp::ContentBlock::ResourceLink(acp::ResourceLink::new( - path.to_string_lossy(), - format!("file://{}", path.display()), - )), - )) - }) - .collect::>(), - ), - ); - - 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_style = project.read(cx).path_style(cx); - 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 }, - ], - path_style, - ) { - 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) { - if path_matcher.is_match(&snapshot.root_name().join(&entry.path)) { - results.push(snapshot.absolutize(&entry.path)); - } - } - } - - 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); - }); - } -} diff --git a/crates/agent/src/tools/grep_tool.rs b/crates/agent/src/tools/grep_tool.rs deleted file mode 100644 index 0caba91564..0000000000 --- a/crates/agent/src/tools/grep_tool.rs +++ /dev/null @@ -1,1183 +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 "frontend/src/**/*.ts". - /// If omitted, all files in the project will be searched. - /// - /// The glob pattern is matched against the full path including the project root directory. - /// - /// - /// If the project has the following root directories: - /// - /// - /a/b/backend - /// - /c/d/frontend - /// - /// Use "backend/**/*.rs" to search only Rust files in the backend root directory. - /// Use "frontend/src/**/*.ts" to search TypeScript files only in the frontend root directory (sub-directory "src"). - /// Use "**/*.rs" to search Rust files across all root directories. - /// - 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, - _cx: &mut App, - ) -> 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 path_style = self.project.read(cx).path_style(cx); - - let include_matcher = match PathMatcher::new( - input - .include_pattern - .as_ref() - .into_iter() - .collect::>(), - path_style, - ) { - 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() - .chain(global_settings.private_files.sources()); - - match PathMatcher::new(exclude_patterns, path_style) { - 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### "); - - 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 project::{FakeFs, Project}; - 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(language::rust_lang()) - }); - - 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); - }); - } - - #[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 settings::SettingsStore; - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![ - "**/.secretdir".to_string(), - "**/.mymetadata".to_string(), - ]); - settings.project.worktree.private_files = Some( - vec![ - "**/.mysecrets".to_string(), - "**/*.privatekey".to_string(), - "**/*.mysensitive".to_string(), - ] - .into(), - ); - }); - }); - }); - - 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.project.worktree.file_scan_exclusions = - Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); - settings.project.worktree.private_files = - Some(vec!["**/.env".to_string()].into()); - }); - }); - }); - - 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/agent/src/tools/list_directory_tool.rs b/crates/agent/src/tools/list_directory_tool.rs deleted file mode 100644 index b7ceba5abf..0000000000 --- a/crates/agent/src/tools/list_directory_tool.rs +++ /dev/null @@ -1,660 +0,0 @@ -use crate::{AgentTool, ToolCallEventStream}; -use agent_client_protocol::ToolKind; -use anyhow::{Result, anyhow}; -use gpui::{App, Entity, SharedString, Task}; -use project::{Project, ProjectPath, WorktreeSettings}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use std::fmt::Write; -use std::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, - _cx: &mut App, - ) -> 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| { - let worktree = worktree.read(cx); - let root_entry = worktree.root_entry()?; - if root_entry.is_dir() { - Some(root_entry.path.display(worktree.path_style())) - } 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(); - - 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; - } - - let project_path: ProjectPath = (worktree_snapshot.id(), entry.path.clone()).into(); - if worktree_settings.is_path_excluded(&project_path.path) - || worktree_settings.is_path_private(&project_path.path) - { - continue; - } - - let full_path = worktree_root_name - .join(&entry.path) - .display(worktree_snapshot.path_style()) - .into_owned(); - 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}; - 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); - }); - } - - #[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.project.worktree.file_scan_exclusions = Some(vec![ - "**/.secretdir".to_string(), - "**/.mymetadata".to_string(), - "**/.hidden_subdir".to_string(), - ]); - settings.project.worktree.private_files = Some( - vec![ - "**/.mysecrets".to_string(), - "**/*.privatekey".to_string(), - "**/*.mysensitive".to_string(), - ] - .into(), - ); - }); - }); - }); - - 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.project.worktree.file_scan_exclusions = - Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); - settings.project.worktree.private_files = - Some(vec!["**/.env".to_string()].into()); - }); - }); - }); - - 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/agent/src/tools/move_path_tool.rs b/crates/agent/src/tools/move_path_tool.rs deleted file mode 100644 index ae58145126..0000000000 --- a/crates/agent/src/tools/move_path_tool.rs +++ /dev/null @@ -1,124 +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, - _cx: &mut App, - ) -> 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, 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/agent/src/tools/now_tool.rs b/crates/agent/src/tools/now_tool.rs deleted file mode 100644 index 3387c0a617..0000000000 --- a/crates/agent/src/tools/now_tool.rs +++ /dev/null @@ -1,64 +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")] -#[schemars(inline)] -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, - _cx: &mut App, - ) -> 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/agent/src/tools/open_tool.rs b/crates/agent/src/tools/open_tool.rs deleted file mode 100644 index 8826d1529c..0000000000 --- a/crates/agent/src/tools/open_tool.rs +++ /dev/null @@ -1,168 +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, - _cx: &mut App, - ) -> 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); - 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().into_owned(); - - 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); - }); - } -} diff --git a/crates/agent/src/tools/read_file_tool.rs b/crates/agent/src/tools/read_file_tool.rs deleted file mode 100644 index acfd4a1674..0000000000 --- a/crates/agent/src/tools/read_file_tool.rs +++ /dev/null @@ -1,1037 +0,0 @@ -use action_log::ActionLog; -use agent_client_protocol::{self as acp, ToolCallUpdateFields}; -use anyhow::{Context as _, Result, anyhow}; -use gpui::{App, Entity, SharedString, Task, WeakEntity}; -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::sync::Arc; -use util::markdown::MarkdownCodeBlock; - -use crate::{AgentTool, Thread, ToolCallEventStream, outline}; - -/// Reads the content of the given file in the project. -/// -/// - Never attempt to read a path that hasn't been previously mentioned. -/// - For large files, this tool returns a file outline with symbol names and line numbers instead of the full content. -/// This outline IS a successful response - use the line numbers to read specific sections with start_line/end_line. -/// Do NOT retry reading the same file without line numbers if you receive an outline. -#[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 { - thread: WeakEntity, - project: Entity, - action_log: Entity, -} - -impl ReadFileTool { - pub fn new( - thread: WeakEntity, - project: Entity, - action_log: Entity, - ) -> Self { - Self { - thread, - 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, - cx: &mut App, - ) -> SharedString { - if let Ok(input) = input - && let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) - && let Some(path) = self - .project - .read(cx) - .short_full_path_for_project_path(&project_path, cx) - { - match (input.start_line, input.end_line) { - (Some(start), Some(end)) => { - format!("Read file `{path}` (lines {}-{})", start, end,) - } - (Some(start), None) => { - format!("Read file `{path}` (from line {})", start) - } - _ => format!("Read file `{path}`"), - } - .into() - } else { - "Read file".into() - } - } - - 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))); - }; - let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else { - return Task::ready(Err(anyhow!( - "Failed to convert {} to absolute path", - &input.path - ))); - }; - - // Error out if this path is either excluded or private in global settings - let global_settings = WorktreeSettings::get_global(cx); - 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(); - - event_stream.update_fields(ToolCallUpdateFields::new().locations(vec![ - acp::ToolCallLocation::new(&abs_path) - .line(input.start_line.map(|line| line.saturating_sub(1))), - ])); - - 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"); - } - - // Record the file read time and mtime - if let Some(mtime) = buffer.read_with(cx, |buffer, _| { - buffer.file().and_then(|file| file.disk_state().mtime()) - })? { - self.thread - .update(cx, |thread, _| { - thread.file_read_times.insert(abs_path.to_path_buf(), mtime); - }) - .ok(); - } - - 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| { - // .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 mut end_row = input.end_line.unwrap_or(u32::MAX); - if end_row <= start_row { - end_row = start_row + 1; // read at least one lines - } - let start = buffer.anchor_before(Point::new(start_row, 0)); - let end = buffer.anchor_before(Point::new(end_row, 0)); - buffer.text_for_range(start..end).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 buffer_content = outline::get_buffer_content_or_outline( - buffer.clone(), - Some(&abs_path.to_string_lossy()), - cx, - ) - .await?; - - action_log.update(cx, |log, cx| { - log.buffer_read(buffer.clone(), cx); - })?; - - if buffer_content.is_outline { - Ok(formatdoc! {" - SUCCESS: File outline retrieved. This file is too large to read all at once, so the outline below shows the file's structure with line numbers. - - IMPORTANT: Do NOT retry this call without line numbers - you will get the same outline. - Instead, use the line numbers below to read specific sections by calling this tool again with start_line and end_line parameters. - - {} - - NEXT STEPS: To read a specific symbol's implementation, call read_file with the same path plus start_line and end_line from the outline above. - For example, to read a function shown as [L100-150], use start_line: 100 and end_line: 150.", buffer_content.text - } - .into()) - } else { - Ok(buffer_content.text.into()) - } - }; - - project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: anchor.unwrap_or_else(|| { - text::Anchor::min_for_buffer(buffer.read(cx).remote_id()) - }), - }), - cx, - ); - if let Ok(LanguageModelToolResultContent::Text(text)) = &result { - let markdown = MarkdownCodeBlock { - tag: &input.path, - text, - } - .to_string(); - event_stream.update_fields(ToolCallUpdateFields::new().content(vec![ - acp::ToolCallContent::Content(acp::Content::new(markdown)), - ])); - } - })?; - - result - }) - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::{ContextServerRegistry, Templates, Thread}; - use gpui::{AppContext, TestAppContext, UpdateGlobal as _}; - use language_model::fake_provider::FakeLanguageModel; - use project::{FakeFs, Project}; - use prompt_store::ProjectContext; - use serde_json::json; - use settings::SettingsStore; - use std::sync::Arc; - 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 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), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new(thread.downgrade(), 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 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), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new(thread.downgrade(), 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(language::rust_lang()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new(thread.downgrade(), 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(7).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(7) - .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 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), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new(thread.downgrade(), 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\n".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 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), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new(thread.downgrade(), 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\n".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\n".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\n".into()); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - }); - } - - #[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 settings::SettingsStore; - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![ - "**/.secretdir".to_string(), - "**/.mymetadata".to_string(), - ]); - settings.project.worktree.private_files = Some( - vec![ - "**/.mysecrets".to_string(), - "**/*.privatekey".to_string(), - "**/*.mysensitive".to_string(), - ] - .into(), - ); - }); - }); - }); - - let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new(thread.downgrade(), 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.project.worktree.file_scan_exclusions = - Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); - settings.project.worktree.private_files = - Some(vec!["**/.env".to_string()].into()); - }); - }); - }); - - 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 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), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new( - thread.downgrade(), - 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/agent/src/tools/terminal_tool.rs b/crates/agent/src/tools/terminal_tool.rs deleted file mode 100644 index 2db4a2d860..0000000000 --- a/crates/agent/src/tools/terminal_tool.rs +++ /dev/null @@ -1,212 +0,0 @@ -use agent_client_protocol as acp; -use anyhow::Result; -use gpui::{App, Entity, SharedString, Task}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::{ - path::{Path, PathBuf}, - rc::Rc, - sync::Arc, -}; -use util::markdown::MarkdownInlineCode; - -use crate::{AgentTool, ThreadEnvironment, ToolCallEventStream}; - -const COMMAND_OUTPUT_LIMIT: u64 = 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, - environment: Rc, -} - -impl TerminalTool { - pub fn new(project: Entity, environment: Rc) -> Self { - Self { - project, - environment, - } - } -} - -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, - _cx: &mut App, - ) -> SharedString { - if let Ok(input) = input { - let mut lines = input.command.lines(); - let first_line = lines.next().unwrap_or_default(); - let remaining_line_count = lines.count(); - match remaining_line_count { - 0 => MarkdownInlineCode(first_line).to_string().into(), - 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 { - "".into() - } - } - - fn run( - self: Arc, - input: Self::Input, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - let working_dir = match working_dir(&input, &self.project, cx) { - Ok(dir) => dir, - Err(err) => return Task::ready(Err(err)), - }; - - let authorize = event_stream.authorize(self.initial_title(Ok(input.clone()), cx), cx); - cx.spawn(async move |cx| { - authorize.await?; - - let terminal = self - .environment - .create_terminal( - input.command.clone(), - working_dir, - Some(COMMAND_OUTPUT_LIMIT), - cx, - ) - .await?; - - let terminal_id = terminal.id(cx)?; - event_stream.update_fields(acp::ToolCallUpdateFields::new().content(vec![ - acp::ToolCallContent::Terminal(acp::Terminal::new(terminal_id)), - ])); - - let exit_status = terminal.wait_for_exit(cx)?.await; - let output = terminal.current_output(cx)?; - - Ok(process_content(output, &input.command, exit_status)) - }) - } -} - -fn process_content( - output: acp::TerminalOutputResponse, - command: &str, - exit_status: acp::TerminalExitStatus, -) -> String { - let content = output.output.trim(); - let is_empty = content.is_empty(); - - let content = format!("```\n{content}\n```"); - let content = if output.truncated { - format!( - "Command output too long. The first {} bytes:\n\n{content}", - content.len(), - ) - } else { - content - }; - - let content = match exit_status.exit_code { - Some(0) => { - if is_empty { - "Command executed successfully.".to_string() - } else { - content - } - } - Some(exit_code) => { - if is_empty { - format!("Command \"{command}\" failed with exit code {}.", exit_code) - } else { - format!( - "Command \"{command}\" failed with exit code {}.\n\n{content}", - exit_code - ) - } - } - None => { - format!( - "Command failed or was interrupted.\nPartial output captured:\n\n{}", - content, - ) - } - }; - content -} - -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."); - } -} diff --git a/crates/agent/src/tools/thinking_tool.rs b/crates/agent/src/tools/thinking_tool.rs deleted file mode 100644 index 96024326f6..0000000000 --- a/crates/agent/src/tools/thinking_tool.rs +++ /dev/null @@ -1,50 +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, - _cx: &mut App, - ) -> SharedString { - "Thinking".into() - } - - fn run( - self: Arc, - input: Self::Input, - event_stream: ToolCallEventStream, - _cx: &mut App, - ) -> Task> { - event_stream - .update_fields(acp::ToolCallUpdateFields::new().content(vec![input.content.into()])); - Task::ready(Ok("Finished thinking.".to_string())) - } -} diff --git a/crates/agent/src/tools/web_search_tool.rs b/crates/agent/src/tools/web_search_tool.rs deleted file mode 100644 index eb4ebacea2..0000000000 --- a/crates/agent/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, - _cx: &mut App, - ) -> SharedString { - "Searching the Web".into() - } - - /// We currently only support Zed Cloud as a provider. - fn supports_provider(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::new().title("Web Search Failed")); - 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::new() - .title(format!("Searched the web: {result_text}")) - .content( - response - .results - .iter() - .map(|result| { - acp::ToolCallContent::Content(acp::Content::new( - acp::ContentBlock::ResourceLink( - acp::ResourceLink::new(result.title.clone(), result.url.clone()) - .title(result.title.clone()) - .description(result.text.clone()), - ), - )) - }) - .collect::>(), - ), - ); -} diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml deleted file mode 100644 index 9a04fb763d..0000000000 --- a/crates/agent_servers/Cargo.toml +++ /dev/null @@ -1,67 +0,0 @@ -[package] -name = "agent_servers" -version = "0.1.0" -edition.workspace = true -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", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"] -e2e = [] - -[lints] -workspace = true - -[lib] -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 -anyhow.workspace = true -async-trait.workspace = true -client.workspace = true -collections.workspace = true -env_logger = { workspace = true, optional = true } -fs.workspace = true -futures.workspace = true -gpui.workspace = true -gpui_tokio = { workspace = true, optional = true } -http_client.workspace = true -indoc.workspace = true -language_model.workspace = true -language_models.workspace = true -log.workspace = true -project.workspace = true -release_channel.workspace = true -reqwest_client = { workspace = true, optional = true } -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smol.workspace = true -task.workspace = true -tempfile.workspace = true -thiserror.workspace = true -ui.workspace = true -terminal.workspace = true -uuid.workspace = true -util.workspace = true -watch.workspace = true - -[target.'cfg(unix)'.dependencies] -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/LICENSE-GPL b/crates/agent_servers/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/agent_servers/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs deleted file mode 100644 index 41aff48a20..0000000000 --- a/crates/agent_servers/src/acp.rs +++ /dev/null @@ -1,984 +0,0 @@ -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::io::BufReader; -use project::Project; -use project::agent_server_store::AgentServerCommand; -use serde::Deserialize; -use settings::Settings as _; -use task::ShellBuilder; -use util::ResultExt as _; - -use std::path::PathBuf; -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, TerminalProviderEvent}; -use terminal::TerminalBuilder; -use terminal::terminal_settings::{AlternateScroll, CursorShape, TerminalSettings}; - -#[derive(Debug, Error)] -#[error("Unsupported version")] -pub struct UnsupportedVersion; - -pub struct AcpConnection { - server_name: SharedString, - telemetry_id: SharedString, - connection: Rc, - sessions: Rc>>, - auth_methods: Vec, - agent_capabilities: acp::AgentCapabilities, - default_mode: Option, - default_model: Option, - root_dir: PathBuf, - // NB: Don't move this into the wait_task, since we need to ensure the process is - // killed on drop (setting kill_on_drop on the command seems to not always work). - child: smol::process::Child, - _io_task: Task>, - _wait_task: Task>, - _stderr_task: Task>, -} - -pub struct AcpSession { - thread: WeakEntity, - suppress_abort_err: bool, - models: Option>>, - session_modes: Option>>, -} - -pub async fn connect( - server_name: SharedString, - command: AgentServerCommand, - root_dir: &Path, - default_mode: Option, - default_model: Option, - is_remote: bool, - cx: &mut AsyncApp, -) -> Result> { - let conn = AcpConnection::stdio( - server_name, - command.clone(), - root_dir, - default_mode, - default_model, - is_remote, - cx, - ) - .await?; - Ok(Rc::new(conn) as _) -} - -const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::ProtocolVersion::V1; - -impl AcpConnection { - pub async fn stdio( - server_name: SharedString, - command: AgentServerCommand, - root_dir: &Path, - default_mode: Option, - default_model: Option, - is_remote: bool, - cx: &mut AsyncApp, - ) -> Result { - let shell = cx.update(|cx| TerminalSettings::get(None, cx).shell.clone())?; - let builder = ShellBuilder::new(&shell, cfg!(windows)); - let mut child = - builder.build_command(Some(command.path.display().to_string()), &command.args); - child - .envs(command.env.iter().flatten()) - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()); - if !is_remote { - child.current_dir(root_dir); - } - let mut child = child.spawn()?; - - let stdout = child.stdout.take().context("Failed to take stdout")?; - let stdin = child.stdin.take().context("Failed to take stdin")?; - let stderr = child.stderr.take().context("Failed to take stderr")?; - log::debug!( - "Spawning external agent server: {:?}, {:?}", - command.path, - command.args - ); - log::trace!("Spawned (pid: {})", child.id()); - - let sessions = Rc::new(RefCell::new(HashMap::default())); - - let (release_channel, version) = cx.update(|cx| { - ( - release_channel::ReleaseChannel::try_global(cx) - .map(|release_channel| release_channel.display_name()), - release_channel::AppVersion::global(cx).to_string(), - ) - })?; - - let client = ClientDelegate { - sessions: sessions.clone(), - cx: cx.clone(), - }; - let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, { - let foreground_executor = cx.foreground_executor().clone(); - move |fut| { - foreground_executor.spawn(fut).detach(); - } - }); - - let io_task = cx.background_spawn(io_task); - - let stderr_task = cx.background_spawn(async move { - let mut stderr = BufReader::new(stderr); - let mut line = String::new(); - while let Ok(n) = stderr.read_line(&mut line).await - && n > 0 - { - log::warn!("agent stderr: {}", line.trim()); - line.clear(); - } - Ok(()) - }); - - let wait_task = cx.spawn({ - let sessions = sessions.clone(); - let status_fut = child.status(); - async move |cx| { - let status = status_fut.await?; - - for session in sessions.borrow().values() { - session - .thread - .update(cx, |thread, cx| { - thread.emit_load_error(LoadError::Exited { status }, cx) - }) - .ok(); - } - - anyhow::Ok(()) - } - }); - - let connection = Rc::new(connection); - - cx.update(|cx| { - AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| { - registry.set_active_connection(server_name.clone(), &connection, cx) - }); - })?; - - let response = connection - .initialize( - acp::InitializeRequest::new(acp::ProtocolVersion::V1) - .client_capabilities( - acp::ClientCapabilities::new() - .fs(acp::FileSystemCapability::new() - .read_text_file(true) - .write_text_file(true)) - .terminal(true) - // Experimental: Allow for rendering terminal output from the agents - .meta(acp::Meta::from_iter([ - ("terminal_output".into(), true.into()), - ("terminal-auth".into(), true.into()), - ])), - ) - .client_info( - acp::Implementation::new("zed", version) - .title(release_channel.map(ToOwned::to_owned)), - ), - ) - .await?; - - if response.protocol_version < MINIMUM_SUPPORTED_VERSION { - return Err(UnsupportedVersion.into()); - } - - let telemetry_id = response - .agent_info - // Use the one the agent provides if we have one - .map(|info| info.name.into()) - // Otherwise, just use the name - .unwrap_or_else(|| server_name.clone()); - - Ok(Self { - auth_methods: response.auth_methods, - root_dir: root_dir.to_owned(), - connection, - server_name, - telemetry_id, - sessions, - agent_capabilities: response.agent_capabilities, - default_mode, - default_model, - _io_task: io_task, - _wait_task: wait_task, - _stderr_task: stderr_task, - child, - }) - } - - pub fn prompt_capabilities(&self) -> &acp::PromptCapabilities { - &self.agent_capabilities.prompt_capabilities - } - - pub fn root_dir(&self) -> &Path { - &self.root_dir - } -} - -impl Drop for AcpConnection { - fn drop(&mut self) { - // See the comment on the child field. - self.child.kill().log_err(); - } -} - -impl AgentConnection for AcpConnection { - fn telemetry_id(&self) -> SharedString { - self.telemetry_id.clone() - } - - fn new_thread( - self: Rc, - project: Entity, - cwd: &Path, - cx: &mut App, - ) -> Task>> { - let name = self.server_name.clone(); - let conn = self.connection.clone(); - let sessions = self.sessions.clone(); - let default_mode = self.default_mode.clone(); - let default_model = self.default_model.clone(); - let cwd = cwd.to_path_buf(); - let context_server_store = project.read(cx).context_server_store().read(cx); - let mcp_servers = if project.read(cx).is_local() { - context_server_store - .configured_server_ids() - .iter() - .filter_map(|id| { - let configuration = context_server_store.configuration_for_server(id)?; - match &*configuration { - project::context_server_store::ContextServerConfiguration::Custom { - command, - .. - } - | project::context_server_store::ContextServerConfiguration::Extension { - command, - .. - } => Some(acp::McpServer::Stdio( - acp::McpServerStdio::new(id.0.to_string(), &command.path) - .args(command.args.clone()) - .env(if let Some(env) = command.env.as_ref() { - env.iter() - .map(|(name, value)| acp::EnvVariable::new(name, value)) - .collect() - } else { - vec![] - }), - )), - project::context_server_store::ContextServerConfiguration::Http { - url, - headers, - } => Some(acp::McpServer::Http( - acp::McpServerHttp::new(id.0.to_string(), url.to_string()).headers( - headers - .iter() - .map(|(name, value)| acp::HttpHeader::new(name, value)) - .collect(), - ), - )), - } - }) - .collect() - } else { - // In SSH projects, the external agent is running on the remote - // machine, and currently we only run MCP servers on the local - // machine. So don't pass any MCP servers to the agent in that case. - Vec::new() - }; - - cx.spawn(async move |cx| { - let response = conn - .new_session(acp::NewSessionRequest::new(cwd).mcp_servers(mcp_servers)) - .await - .map_err(|err| { - if err.code == acp::ErrorCode::AuthRequired { - let mut error = AuthRequired::new(); - - if err.message != acp::ErrorCode::AuthRequired.to_string() { - error = error.with_description(err.message); - } - - anyhow!(error) - } else { - anyhow!(err) - } - })?; - - let modes = response.modes.map(|modes| Rc::new(RefCell::new(modes))); - let models = response.models.map(|models| Rc::new(RefCell::new(models))); - - if let Some(default_mode) = default_mode { - if let Some(modes) = modes.as_ref() { - let mut modes_ref = modes.borrow_mut(); - let has_mode = modes_ref.available_modes.iter().any(|mode| mode.id == default_mode); - - if has_mode { - let initial_mode_id = modes_ref.current_mode_id.clone(); - - cx.spawn({ - let default_mode = default_mode.clone(); - let session_id = response.session_id.clone(); - let modes = modes.clone(); - let conn = conn.clone(); - async move |_| { - let result = conn.set_session_mode(acp::SetSessionModeRequest::new(session_id, default_mode)) - .await.log_err(); - - if result.is_none() { - modes.borrow_mut().current_mode_id = initial_mode_id; - } - } - }).detach(); - - modes_ref.current_mode_id = default_mode; - } else { - let available_modes = modes_ref - .available_modes - .iter() - .map(|mode| format!("- `{}`: {}", mode.id, mode.name)) - .collect::>() - .join("\n"); - - log::warn!( - "`{default_mode}` is not valid {name} mode. Available options:\n{available_modes}", - ); - } - } else { - log::warn!( - "`{name}` does not support modes, but `default_mode` was set in settings.", - ); - } - } - - if let Some(default_model) = default_model { - if let Some(models) = models.as_ref() { - let mut models_ref = models.borrow_mut(); - let has_model = models_ref.available_models.iter().any(|model| model.model_id == default_model); - - if has_model { - let initial_model_id = models_ref.current_model_id.clone(); - - cx.spawn({ - let default_model = default_model.clone(); - let session_id = response.session_id.clone(); - let models = models.clone(); - let conn = conn.clone(); - async move |_| { - let result = conn.set_session_model(acp::SetSessionModelRequest::new(session_id, default_model)) - .await.log_err(); - - if result.is_none() { - models.borrow_mut().current_model_id = initial_model_id; - } - } - }).detach(); - - models_ref.current_model_id = default_model; - } else { - let available_models = models_ref - .available_models - .iter() - .map(|model| format!("- `{}`: {}", model.model_id, model.name)) - .collect::>() - .join("\n"); - - log::warn!( - "`{default_model}` is not a valid {name} model. Available options:\n{available_models}", - ); - } - } else { - log::warn!( - "`{name}` does not support model selection, but `default_model` was set in settings.", - ); - } - } - - let session_id = response.session_id; - let action_log = cx.new(|_| ActionLog::new(project.clone()))?; - let thread = cx.new(|cx| { - AcpThread::new( - self.server_name.clone(), - self.clone(), - project, - action_log, - session_id.clone(), - // ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically. - watch::Receiver::constant(self.agent_capabilities.prompt_capabilities.clone()), - cx, - ) - })?; - - - let session = AcpSession { - thread: thread.downgrade(), - suppress_abort_err: false, - session_modes: modes, - models, - }; - 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 { - conn.authenticate(acp::AuthenticateRequest::new(method_id)) - .await?; - Ok(()) - }) - } - - 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 == acp::ErrorCode::AuthRequired { - return Err(anyhow!(acp::Error::auth_required())); - } - - if err.code != ErrorCode::InternalError { - 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::new(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::new(session_id.clone()); - cx.foreground_executor() - .spawn(async move { conn.cancel(params).await }) - .detach(); - } - - fn session_modes( - &self, - session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { - let sessions = self.sessions.clone(); - let sessions_ref = sessions.borrow(); - let Some(session) = sessions_ref.get(session_id) else { - return None; - }; - - if let Some(modes) = session.session_modes.as_ref() { - Some(Rc::new(AcpSessionModes { - connection: self.connection.clone(), - session_id: session_id.clone(), - state: modes.clone(), - }) as _) - } else { - None - } - } - - fn model_selector( - &self, - session_id: &acp::SessionId, - ) -> Option> { - let sessions = self.sessions.clone(); - let sessions_ref = sessions.borrow(); - let Some(session) = sessions_ref.get(session_id) else { - return None; - }; - - if let Some(models) = session.models.as_ref() { - Some(Rc::new(AcpModelSelector::new( - session_id.clone(), - self.connection.clone(), - models.clone(), - )) as _) - } else { - None - } - } - - fn into_any(self: Rc) -> Rc { - self - } -} - -struct AcpSessionModes { - session_id: acp::SessionId, - connection: Rc, - state: Rc>, -} - -impl acp_thread::AgentSessionModes for AcpSessionModes { - fn current_mode(&self) -> acp::SessionModeId { - self.state.borrow().current_mode_id.clone() - } - - fn all_modes(&self) -> Vec { - self.state.borrow().available_modes.clone() - } - - fn set_mode(&self, mode_id: acp::SessionModeId, cx: &mut App) -> Task> { - let connection = self.connection.clone(); - let session_id = self.session_id.clone(); - let old_mode_id; - { - let mut state = self.state.borrow_mut(); - old_mode_id = state.current_mode_id.clone(); - state.current_mode_id = mode_id.clone(); - }; - let state = self.state.clone(); - cx.foreground_executor().spawn(async move { - let result = connection - .set_session_mode(acp::SetSessionModeRequest::new(session_id, mode_id)) - .await; - - if result.is_err() { - state.borrow_mut().current_mode_id = old_mode_id; - } - - result?; - - Ok(()) - }) - } -} - -struct AcpModelSelector { - session_id: acp::SessionId, - connection: Rc, - state: Rc>, -} - -impl AcpModelSelector { - fn new( - session_id: acp::SessionId, - connection: Rc, - state: Rc>, - ) -> Self { - Self { - session_id, - connection, - state, - } - } -} - -impl acp_thread::AgentModelSelector for AcpModelSelector { - fn list_models(&self, _cx: &mut App) -> Task> { - Task::ready(Ok(acp_thread::AgentModelList::Flat( - self.state - .borrow() - .available_models - .clone() - .into_iter() - .map(acp_thread::AgentModelInfo::from) - .collect(), - ))) - } - - fn select_model(&self, model_id: acp::ModelId, cx: &mut App) -> Task> { - let connection = self.connection.clone(); - let session_id = self.session_id.clone(); - let old_model_id; - { - let mut state = self.state.borrow_mut(); - old_model_id = state.current_model_id.clone(); - state.current_model_id = model_id.clone(); - }; - let state = self.state.clone(); - cx.foreground_executor().spawn(async move { - let result = connection - .set_session_model(acp::SetSessionModelRequest::new(session_id, model_id)) - .await; - - if result.is_err() { - state.borrow_mut().current_model_id = old_model_id; - } - - result?; - - Ok(()) - }) - } - - fn selected_model(&self, _cx: &mut App) -> Task> { - let state = self.state.borrow(); - Task::ready( - state - .available_models - .iter() - .find(|m| m.model_id == state.current_model_id) - .cloned() - .map(acp_thread::AgentModelInfo::from) - .ok_or_else(|| anyhow::anyhow!("Model not found")), - ) - } -} - -struct ClientDelegate { - sessions: Rc>>, - cx: AsyncApp, -} - -#[async_trait::async_trait(?Send)] -impl acp::Client for ClientDelegate { - async fn request_permission( - &self, - arguments: acp::RequestPermissionRequest, - ) -> Result { - let respect_always_allow_setting; - let thread; - { - let sessions_ref = self.sessions.borrow(); - let session = sessions_ref - .get(&arguments.session_id) - .context("Failed to get session")?; - respect_always_allow_setting = session.session_modes.is_none(); - thread = session.thread.clone(); - } - - let cx = &mut self.cx.clone(); - - let task = thread.update(cx, |thread, cx| { - thread.request_tool_call_authorization( - arguments.tool_call, - arguments.options, - respect_always_allow_setting, - cx, - ) - })??; - - let outcome = task.await; - - Ok(acp::RequestPermissionResponse::new(outcome)) - } - - async fn write_text_file( - &self, - arguments: acp::WriteTextFileRequest, - ) -> Result { - let cx = &mut self.cx.clone(); - let task = self - .session_thread(&arguments.session_id)? - .update(cx, |thread, cx| { - thread.write_text_file(arguments.path, arguments.content, cx) - })?; - - task.await?; - - Ok(Default::default()) - } - - async fn read_text_file( - &self, - arguments: acp::ReadTextFileRequest, - ) -> Result { - let task = self.session_thread(&arguments.session_id)?.update( - &mut self.cx.clone(), - |thread, cx| { - thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx) - }, - )?; - - let content = task.await?; - - Ok(acp::ReadTextFileResponse::new(content)) - } - - async fn session_notification( - &self, - notification: acp::SessionNotification, - ) -> Result<(), acp::Error> { - let sessions = self.sessions.borrow(); - let session = sessions - .get(¬ification.session_id) - .context("Failed to get session")?; - - if let acp::SessionUpdate::CurrentModeUpdate(acp::CurrentModeUpdate { - current_mode_id, - .. - }) = ¬ification.update - { - if let Some(session_modes) = &session.session_modes { - session_modes.borrow_mut().current_mode_id = current_mode_id.clone(); - } else { - log::error!( - "Got a `CurrentModeUpdate` notification, but they agent didn't specify `modes` during setting setup." - ); - } - } - - // Clone so we can inspect meta both before and after handing off to the thread - let update_clone = notification.update.clone(); - - // Pre-handle: if a ToolCall carries terminal_info, create/register a display-only terminal. - if let acp::SessionUpdate::ToolCall(tc) = &update_clone { - if let Some(meta) = &tc.meta { - if let Some(terminal_info) = meta.get("terminal_info") { - if let Some(id_str) = terminal_info.get("terminal_id").and_then(|v| v.as_str()) - { - let terminal_id = acp::TerminalId::new(id_str); - let cwd = terminal_info - .get("cwd") - .and_then(|v| v.as_str().map(PathBuf::from)); - - // Create a minimal display-only lower-level terminal and register it. - let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| { - let builder = TerminalBuilder::new_display_only( - CursorShape::default(), - AlternateScroll::On, - None, - 0, - )?; - let lower = cx.new(|cx| builder.subscribe(cx)); - thread.on_terminal_provider_event( - TerminalProviderEvent::Created { - terminal_id, - label: tc.title.clone(), - cwd, - output_byte_limit: None, - terminal: lower, - }, - cx, - ); - anyhow::Ok(()) - }); - } - } - } - } - - // Forward the update to the acp_thread as usual. - session.thread.update(&mut self.cx.clone(), |thread, cx| { - thread.handle_session_update(notification.update.clone(), cx) - })??; - - // Post-handle: stream terminal output/exit if present on ToolCallUpdate meta. - if let acp::SessionUpdate::ToolCallUpdate(tcu) = &update_clone { - if let Some(meta) = &tcu.meta { - if let Some(term_out) = meta.get("terminal_output") { - if let Some(id_str) = term_out.get("terminal_id").and_then(|v| v.as_str()) { - let terminal_id = acp::TerminalId::new(id_str); - if let Some(s) = term_out.get("data").and_then(|v| v.as_str()) { - let data = s.as_bytes().to_vec(); - let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Output { terminal_id, data }, - cx, - ); - }); - } - } - } - - // terminal_exit - if let Some(term_exit) = meta.get("terminal_exit") { - if let Some(id_str) = term_exit.get("terminal_id").and_then(|v| v.as_str()) { - let terminal_id = acp::TerminalId::new(id_str); - let status = acp::TerminalExitStatus::new() - .exit_code( - term_exit - .get("exit_code") - .and_then(|v| v.as_u64()) - .map(|i| i as u32), - ) - .signal( - term_exit - .get("signal") - .and_then(|v| v.as_str().map(|s| s.to_string())), - ); - - let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Exit { - terminal_id, - status, - }, - cx, - ); - }); - } - } - } - } - - Ok(()) - } - - async fn create_terminal( - &self, - args: acp::CreateTerminalRequest, - ) -> Result { - let thread = self.session_thread(&args.session_id)?; - let project = thread.read_with(&self.cx, |thread, _cx| thread.project().clone())?; - - let terminal_entity = acp_thread::create_terminal_entity( - args.command.clone(), - &args.args, - args.env - .into_iter() - .map(|env| (env.name, env.value)) - .collect(), - args.cwd.clone(), - &project, - &mut self.cx.clone(), - ) - .await?; - - // Register with renderer - let terminal_entity = thread.update(&mut self.cx.clone(), |thread, cx| { - thread.register_terminal_created( - acp::TerminalId::new(uuid::Uuid::new_v4().to_string()), - format!("{} {}", args.command, args.args.join(" ")), - args.cwd.clone(), - args.output_byte_limit, - terminal_entity, - cx, - ) - })?; - let terminal_id = - terminal_entity.read_with(&self.cx, |terminal, _| terminal.id().clone())?; - Ok(acp::CreateTerminalResponse::new(terminal_id)) - } - - async fn kill_terminal_command( - &self, - args: acp::KillTerminalCommandRequest, - ) -> Result { - self.session_thread(&args.session_id)? - .update(&mut self.cx.clone(), |thread, cx| { - thread.kill_terminal(args.terminal_id, cx) - })??; - - Ok(Default::default()) - } - - async fn ext_method(&self, _args: acp::ExtRequest) -> Result { - Err(acp::Error::method_not_found()) - } - - async fn ext_notification(&self, _args: acp::ExtNotification) -> Result<(), acp::Error> { - Err(acp::Error::method_not_found()) - } - - async fn release_terminal( - &self, - args: acp::ReleaseTerminalRequest, - ) -> Result { - self.session_thread(&args.session_id)? - .update(&mut self.cx.clone(), |thread, cx| { - thread.release_terminal(args.terminal_id, cx) - })??; - - Ok(Default::default()) - } - - async fn terminal_output( - &self, - args: acp::TerminalOutputRequest, - ) -> Result { - self.session_thread(&args.session_id)? - .read_with(&mut self.cx.clone(), |thread, cx| { - let out = thread - .terminal(args.terminal_id)? - .read(cx) - .current_output(cx); - - Ok(out) - })? - } - - async fn wait_for_terminal_exit( - &self, - args: acp::WaitForTerminalExitRequest, - ) -> Result { - let exit_status = self - .session_thread(&args.session_id)? - .update(&mut self.cx.clone(), |thread, cx| { - anyhow::Ok(thread.terminal(args.terminal_id)?.read(cx).wait_for_exit()) - })?? - .await; - - Ok(acp::WaitForTerminalExitResponse::new(exit_status)) - } -} - -impl ClientDelegate { - fn session_thread(&self, session_id: &acp::SessionId) -> Result> { - let sessions = self.sessions.borrow(); - sessions - .get(session_id) - .context("Failed to get session") - .map(|session| session.thread.clone()) - } -} diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs deleted file mode 100644 index 46e8508e44..0000000000 --- a/crates/agent_servers/src/agent_servers.rs +++ /dev/null @@ -1,121 +0,0 @@ -mod acp; -mod claude; -mod codex; -mod custom; -mod gemini; - -#[cfg(any(test, feature = "test-support"))] -pub mod e2e_tests; - -pub use claude::*; -use client::ProxySettings; -pub use codex::*; -use collections::HashMap; -pub use custom::*; -use fs::Fs; -pub use gemini::*; -use http_client::read_no_proxy_from_env; -use project::agent_server_store::AgentServerStore; - -use acp_thread::AgentConnection; -use anyhow::Result; -use gpui::{App, AppContext, Entity, SharedString, Task}; -use project::Project; -use settings::SettingsStore; -use std::{any::Any, path::Path, rc::Rc, sync::Arc}; - -pub use acp::AcpConnection; - -pub struct AgentServerDelegate { - store: Entity, - project: Entity, - status_tx: Option>, - new_version_available: Option>>, -} - -impl AgentServerDelegate { - pub fn new( - store: Entity, - project: Entity, - status_tx: Option>, - new_version_tx: Option>>, - ) -> Self { - Self { - store, - project, - status_tx, - new_version_available: new_version_tx, - } - } - - pub fn project(&self) -> &Entity { - &self.project - } -} - -pub trait AgentServer: Send { - fn logo(&self) -> ui::IconName; - fn name(&self) -> SharedString; - fn default_mode(&self, _cx: &mut App) -> Option { - None - } - fn set_default_mode( - &self, - _mode_id: Option, - _fs: Arc, - _cx: &mut App, - ) { - } - - fn default_model(&self, _cx: &mut App) -> Option { - None - } - - fn set_default_model( - &self, - _model_id: Option, - _fs: Arc, - _cx: &mut App, - ) { - } - - fn connect( - &self, - root_dir: Option<&Path>, - delegate: AgentServerDelegate, - cx: &mut App, - ) -> Task, Option)>>; - - fn into_any(self: Rc) -> Rc; -} - -impl dyn AgentServer { - pub fn downcast(self: Rc) -> Option> { - self.into_any().downcast().ok() - } -} - -/// Load the default proxy environment variables to pass through to the agent -pub fn load_proxy_env(cx: &mut App) -> HashMap { - let proxy_url = cx - .read_global(|settings: &SettingsStore, _| settings.get::(None).proxy_url()); - let mut env = HashMap::default(); - - if let Some(proxy_url) = &proxy_url { - let env_var = if proxy_url.scheme() == "https" { - "HTTPS_PROXY" - } else { - "HTTP_PROXY" - }; - env.insert(env_var.to_owned(), proxy_url.to_string()); - } - - if let Some(no_proxy) = read_no_proxy_from_env() { - env.insert("NO_PROXY".to_owned(), no_proxy); - } else if proxy_url.is_some() { - // We sometimes need local MCP servers that we don't want to proxy - env.insert("NO_PROXY".to_owned(), "localhost,127.0.0.1".to_owned()); - } - - env -} diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs deleted file mode 100644 index e67ddd5c06..0000000000 --- a/crates/agent_servers/src/claude.rs +++ /dev/null @@ -1,121 +0,0 @@ -use agent_client_protocol as acp; -use fs::Fs; -use settings::{SettingsStore, update_settings_file}; -use std::path::Path; -use std::rc::Rc; -use std::sync::Arc; -use std::{any::Any, path::PathBuf}; - -use anyhow::{Context as _, Result}; -use gpui::{App, AppContext as _, SharedString, Task}; -use project::agent_server_store::{AllAgentServersSettings, CLAUDE_CODE_NAME}; - -use crate::{AgentServer, AgentServerDelegate, load_proxy_env}; -use acp_thread::AgentConnection; - -#[derive(Clone)] -pub struct ClaudeCode; - -pub struct AgentServerLoginCommand { - pub path: PathBuf, - pub arguments: Vec, -} - -impl AgentServer for ClaudeCode { - fn name(&self) -> SharedString { - "Claude Code".into() - } - - fn logo(&self) -> ui::IconName { - ui::IconName::AiClaude - } - - fn default_mode(&self, cx: &mut App) -> Option { - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings.get::(None).claude.clone() - }); - - settings - .as_ref() - .and_then(|s| s.default_mode.clone().map(acp::SessionModeId::new)) - } - - fn set_default_mode(&self, mode_id: Option, fs: Arc, cx: &mut App) { - update_settings_file(fs, cx, |settings, _| { - settings - .agent_servers - .get_or_insert_default() - .claude - .get_or_insert_default() - .default_mode = mode_id.map(|m| m.to_string()) - }); - } - - fn default_model(&self, cx: &mut App) -> Option { - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings.get::(None).claude.clone() - }); - - settings - .as_ref() - .and_then(|s| s.default_model.clone().map(acp::ModelId::new)) - } - - fn set_default_model(&self, model_id: Option, fs: Arc, cx: &mut App) { - update_settings_file(fs, cx, |settings, _| { - settings - .agent_servers - .get_or_insert_default() - .claude - .get_or_insert_default() - .default_model = model_id.map(|m| m.to_string()) - }); - } - - fn connect( - &self, - root_dir: Option<&Path>, - delegate: AgentServerDelegate, - cx: &mut App, - ) -> Task, Option)>> { - let name = self.name(); - let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned()); - let is_remote = delegate.project.read(cx).is_via_remote_server(); - let store = delegate.store.downgrade(); - let extra_env = load_proxy_env(cx); - let default_mode = self.default_mode(cx); - let default_model = self.default_model(cx); - - cx.spawn(async move |cx| { - let (command, root_dir, login) = store - .update(cx, |store, cx| { - let agent = store - .get_external_agent(&CLAUDE_CODE_NAME.into()) - .context("Claude Code is not registered")?; - anyhow::Ok(agent.get_command( - root_dir.as_deref(), - extra_env, - delegate.status_tx, - delegate.new_version_available, - &mut cx.to_async(), - )) - })?? - .await?; - let connection = crate::acp::connect( - name, - command, - root_dir.as_ref(), - default_mode, - default_model, - is_remote, - cx, - ) - .await?; - Ok((connection, login)) - }) - } - - fn into_any(self: Rc) -> Rc { - self - } -} diff --git a/crates/agent_servers/src/codex.rs b/crates/agent_servers/src/codex.rs deleted file mode 100644 index c2b308e48b..0000000000 --- a/crates/agent_servers/src/codex.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::rc::Rc; -use std::sync::Arc; -use std::{any::Any, path::Path}; - -use acp_thread::AgentConnection; -use agent_client_protocol as acp; -use anyhow::{Context as _, Result}; -use fs::Fs; -use gpui::{App, AppContext as _, SharedString, Task}; -use project::agent_server_store::{AllAgentServersSettings, CODEX_NAME}; -use settings::{SettingsStore, update_settings_file}; - -use crate::{AgentServer, AgentServerDelegate, load_proxy_env}; - -#[derive(Clone)] -pub struct Codex; - -#[cfg(test)] -pub(crate) mod tests { - use super::*; - - crate::common_e2e_tests!(async |_, _, _| Codex, allow_option_id = "proceed_once"); -} - -impl AgentServer for Codex { - fn name(&self) -> SharedString { - "Codex".into() - } - - fn logo(&self) -> ui::IconName { - ui::IconName::AiOpenAi - } - - fn default_mode(&self, cx: &mut App) -> Option { - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings.get::(None).codex.clone() - }); - - settings - .as_ref() - .and_then(|s| s.default_mode.clone().map(acp::SessionModeId::new)) - } - - fn set_default_mode(&self, mode_id: Option, fs: Arc, cx: &mut App) { - update_settings_file(fs, cx, |settings, _| { - settings - .agent_servers - .get_or_insert_default() - .codex - .get_or_insert_default() - .default_mode = mode_id.map(|m| m.to_string()) - }); - } - - fn default_model(&self, cx: &mut App) -> Option { - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings.get::(None).codex.clone() - }); - - settings - .as_ref() - .and_then(|s| s.default_model.clone().map(acp::ModelId::new)) - } - - fn set_default_model(&self, model_id: Option, fs: Arc, cx: &mut App) { - update_settings_file(fs, cx, |settings, _| { - settings - .agent_servers - .get_or_insert_default() - .codex - .get_or_insert_default() - .default_model = model_id.map(|m| m.to_string()) - }); - } - - fn connect( - &self, - root_dir: Option<&Path>, - delegate: AgentServerDelegate, - cx: &mut App, - ) -> Task, Option)>> { - let name = self.name(); - let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned()); - let is_remote = delegate.project.read(cx).is_via_remote_server(); - let store = delegate.store.downgrade(); - let extra_env = load_proxy_env(cx); - let default_mode = self.default_mode(cx); - let default_model = self.default_model(cx); - - cx.spawn(async move |cx| { - let (command, root_dir, login) = store - .update(cx, |store, cx| { - let agent = store - .get_external_agent(&CODEX_NAME.into()) - .context("Codex is not registered")?; - anyhow::Ok(agent.get_command( - root_dir.as_deref(), - extra_env, - delegate.status_tx, - delegate.new_version_available, - &mut cx.to_async(), - )) - })?? - .await?; - - let connection = crate::acp::connect( - name, - command, - root_dir.as_ref(), - default_mode, - default_model, - is_remote, - cx, - ) - .await?; - Ok((connection, login)) - }) - } - - fn into_any(self: Rc) -> Rc { - self - } -} diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs deleted file mode 100644 index 6b981ce8b8..0000000000 --- a/crates/agent_servers/src/custom.rs +++ /dev/null @@ -1,151 +0,0 @@ -use crate::{AgentServer, AgentServerDelegate, load_proxy_env}; -use acp_thread::AgentConnection; -use agent_client_protocol as acp; -use anyhow::{Context as _, Result}; -use fs::Fs; -use gpui::{App, AppContext as _, SharedString, Task}; -use project::agent_server_store::{AllAgentServersSettings, ExternalAgentServerName}; -use settings::{SettingsStore, update_settings_file}; -use std::{path::Path, rc::Rc, sync::Arc}; -use ui::IconName; - -/// A generic agent server implementation for custom user-defined agents -pub struct CustomAgentServer { - name: SharedString, -} - -impl CustomAgentServer { - pub fn new(name: SharedString) -> Self { - Self { name } - } -} - -impl AgentServer for CustomAgentServer { - fn name(&self) -> SharedString { - self.name.clone() - } - - fn logo(&self) -> IconName { - IconName::Terminal - } - - fn default_mode(&self, cx: &mut App) -> Option { - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings - .get::(None) - .custom - .get(&self.name()) - .cloned() - }); - - settings - .as_ref() - .and_then(|s| s.default_mode().map(acp::SessionModeId::new)) - } - - fn set_default_mode(&self, mode_id: Option, fs: Arc, cx: &mut App) { - let name = self.name(); - update_settings_file(fs, cx, move |settings, _| { - let settings = settings - .agent_servers - .get_or_insert_default() - .custom - .entry(name.clone()) - .or_insert_with(|| settings::CustomAgentServerSettings::Extension { - default_model: None, - default_mode: None, - }); - - match settings { - settings::CustomAgentServerSettings::Custom { default_mode, .. } - | settings::CustomAgentServerSettings::Extension { default_mode, .. } => { - *default_mode = mode_id.map(|m| m.to_string()); - } - } - }); - } - - fn default_model(&self, cx: &mut App) -> Option { - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings - .get::(None) - .custom - .get(&self.name()) - .cloned() - }); - - settings - .as_ref() - .and_then(|s| s.default_model().map(acp::ModelId::new)) - } - - fn set_default_model(&self, model_id: Option, fs: Arc, cx: &mut App) { - let name = self.name(); - update_settings_file(fs, cx, move |settings, _| { - let settings = settings - .agent_servers - .get_or_insert_default() - .custom - .entry(name.clone()) - .or_insert_with(|| settings::CustomAgentServerSettings::Extension { - default_model: None, - default_mode: None, - }); - - match settings { - settings::CustomAgentServerSettings::Custom { default_model, .. } - | settings::CustomAgentServerSettings::Extension { default_model, .. } => { - *default_model = model_id.map(|m| m.to_string()); - } - } - }); - } - - fn connect( - &self, - root_dir: Option<&Path>, - delegate: AgentServerDelegate, - cx: &mut App, - ) -> Task, Option)>> { - let name = self.name(); - let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned()); - let is_remote = delegate.project.read(cx).is_via_remote_server(); - let default_mode = self.default_mode(cx); - let default_model = self.default_model(cx); - let store = delegate.store.downgrade(); - let extra_env = load_proxy_env(cx); - cx.spawn(async move |cx| { - let (command, root_dir, login) = store - .update(cx, |store, cx| { - let agent = store - .get_external_agent(&ExternalAgentServerName(name.clone())) - .with_context(|| { - format!("Custom agent server `{}` is not registered", name) - })?; - anyhow::Ok(agent.get_command( - root_dir.as_deref(), - extra_env, - delegate.status_tx, - delegate.new_version_available, - &mut cx.to_async(), - )) - })?? - .await?; - let connection = crate::acp::connect( - name, - command, - root_dir.as_ref(), - default_mode, - default_model, - is_remote, - cx, - ) - .await?; - Ok((connection, login)) - }) - } - - 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 deleted file mode 100644 index 9db7535b5e..0000000000 --- a/crates/agent_servers/src/e2e_tests.rs +++ /dev/null @@ -1,552 +0,0 @@ -use crate::{AgentServer, AgentServerDelegate}; -use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus}; -use agent_client_protocol as acp; -use futures::{FutureExt, StreamExt, channel::mpsc, select}; -use gpui::{AppContext, Entity, TestAppContext}; -use indoc::indoc; -#[cfg(test)] -use project::agent_server_store::BuiltinAgentServerSettings; -use project::{FakeFs, Project}; -#[cfg(test)] -use settings::Settings; -use std::{ - path::{Path, PathBuf}, - sync::Arc, - time::Duration, -}; -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; - - thread - .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx)) - .await - .unwrap(); - - thread.read_with(cx, |thread, _| { - assert!( - thread.entries().len() >= 2, - "Expected at least 2 entries. Got: {:?}", - thread.entries() - ); - assert!(matches!( - thread.entries()[0], - AgentThreadEntry::UserMessage(_) - )); - assert!(matches!( - thread.entries()[1], - AgentThreadEntry::AssistantMessage(_) - )); - }); -} - -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 _; - - let tempdir = tempfile::tempdir().unwrap(); - std::fs::write( - tempdir.path().join("foo.rs"), - indoc! {" - fn main() { - println!(\"Hello, world!\"); - } - "}, - ) - .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; - thread - .update(cx, |thread, cx| { - thread.send( - vec![ - "Read the file ".into(), - acp::ContentBlock::ResourceLink(acp::ResourceLink::new("foo.rs", "foo.rs")), - " and tell me what the content of the println! is".into(), - ], - cx, - ) - }) - .await - .unwrap(); - - thread.read_with(cx, |thread, cx| { - assert!(matches!( - thread.entries()[0], - AgentThreadEntry::UserMessage(_) - )); - let assistant_message = &thread - .entries() - .iter() - .rev() - .find_map(|entry| match entry { - AgentThreadEntry::AssistantMessage(msg) => Some(msg), - _ => None, - }) - .unwrap(); - - assert!( - assistant_message.to_markdown(cx).contains("Hello, world!"), - "unexpected assistant message: {:?}", - assistant_message.to_markdown(cx) - ); - }); - - 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 _; - - 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; - - thread - .update(cx, |thread, cx| { - thread.send_raw( - &format!("Read {} and tell me what you see.", foo_path.display()), - cx, - ) - }) - .await - .unwrap(); - thread.read_with(cx, |thread, _cx| { - assert!(thread.entries().iter().any(|entry| { - matches!( - entry, - AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Pending - | ToolCallStatus::InProgress - | ToolCallStatus::Completed, - .. - }) - ) - })); - assert!( - thread - .entries() - .iter() - .any(|entry| { matches!(entry, AgentThreadEntry::AssistantMessage(_)) }) - ); - }); - - drop(tempdir); -} - -pub async fn test_tool_call_with_permission( - server: F, - 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 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, - ) - }); - - run_until_first_tool_call( - &thread, - |entry| { - matches!( - entry, - AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::WaitingForConfirmation { .. }, - .. - }) - ) - }, - cx, - ) - .await; - - let tool_call_id = thread.read_with(cx, |thread, cx| { - let AgentThreadEntry::ToolCall(ToolCall { - id, - label, - status: ToolCallStatus::WaitingForConfirmation { .. }, - .. - }) = &thread - .entries() - .iter() - .find(|entry| matches!(entry, AgentThreadEntry::ToolCall(_))) - .unwrap() - else { - panic!(); - }; - - let label = label.read(cx).source(); - assert!(label.contains("touch"), "Got: {}", label); - - id.clone() - }); - - thread.update(cx, |thread, cx| { - thread.authorize_tool_call( - tool_call_id, - allow_option_id, - acp::PermissionOptionKind::AllowOnce, - cx, - ); - - assert!(thread.entries().iter().any(|entry| matches!( - entry, - AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Pending - | ToolCallStatus::InProgress - | ToolCallStatus::Completed, - .. - }) - ))); - }); - - full_turn.await.unwrap(); - - thread.read_with(cx, |thread, cx| { - let AgentThreadEntry::ToolCall(ToolCall { - content, - status: ToolCallStatus::Pending - | ToolCallStatus::InProgress - | ToolCallStatus::Completed, - .. - }) = thread - .entries() - .iter() - .find(|entry| matches!(entry, AgentThreadEntry::ToolCall(_))) - .unwrap() - else { - panic!(); - }; - - assert!( - content.iter().any(|c| c.to_markdown(cx).contains("Hello")), - "Expected content to contain 'Hello'" - ); - }); -} - -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; - - let project = Project::test(fs.clone(), [path!("/private/tmp").as_ref()], cx).await; - let thread = new_test_thread( - server(&fs, &project, cx).await, - project.clone(), - "/private/tmp", - cx, - ) - .await; - let _ = thread.update(cx, |thread, cx| { - thread.send_raw( - r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#, - cx, - ) - }); - - let first_tool_call_ix = run_until_first_tool_call( - &thread, - |entry| { - matches!( - entry, - AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::WaitingForConfirmation { .. }, - .. - }) - ) - }, - cx, - ) - .await; - - thread.read_with(cx, |thread, cx| { - let AgentThreadEntry::ToolCall(ToolCall { - id, - label, - status: ToolCallStatus::WaitingForConfirmation { .. }, - .. - }) = &thread.entries()[first_tool_call_ix] - else { - panic!("{:?}", thread.entries()[1]); - }; - - let label = label.read(cx).source(); - assert!(label.contains("touch"), "Got: {}", label); - - id.clone() - }); - - thread.update(cx, |thread, cx| thread.cancel(cx)).await; - thread.read_with(cx, |thread, _cx| { - let AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Canceled, - .. - }) = &thread.entries()[first_tool_call_ix] - else { - panic!(); - }; - }); - - thread - .update(cx, |thread, cx| { - thread.send_raw(r#"Stop running and say goodbye to me."#, cx) - }) - .await - .unwrap(); - thread.read_with(cx, |thread, _| { - assert!(matches!( - &thread.entries().last().unwrap(), - AgentThreadEntry::AssistantMessage(..), - )) - }); -} - -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) => { - mod common_e2e { - use super::*; - - #[::gpui::test] - #[cfg_attr(not(feature = "e2e"), ignore)] - async fn basic(cx: &mut ::gpui::TestAppContext) { - $crate::e2e_tests::test_basic($server, cx).await; - } - - #[::gpui::test] - #[cfg_attr(not(feature = "e2e"), ignore)] - async fn path_mentions(cx: &mut ::gpui::TestAppContext) { - $crate::e2e_tests::test_path_mentions($server, cx).await; - } - - #[::gpui::test] - #[cfg_attr(not(feature = "e2e"), ignore)] - async fn tool_call(cx: &mut ::gpui::TestAppContext) { - $crate::e2e_tests::test_tool_call($server, cx).await; - } - - #[::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( - $server, - ::agent_client_protocol::PermissionOptionId::new($allow_option_id), - cx, - ) - .await; - } - - #[::gpui::test] - #[cfg_attr(not(feature = "e2e"), ignore)] - 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 { - env_logger::try_init().ok(); - - cx.update(|cx| { - let settings_store = settings::SettingsStore::test(cx); - cx.set_global(settings_store); - gpui_tokio::init(cx); - let http_client = reqwest_client::ReqwestClient::user_agent("agent tests").unwrap(); - cx.set_http_client(Arc::new(http_client)); - 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); - - #[cfg(test)] - project::agent_server_store::AllAgentServersSettings::override_global( - project::agent_server_store::AllAgentServersSettings { - claude: Some(BuiltinAgentServerSettings { - path: Some("claude-code-acp".into()), - args: None, - env: None, - ignore_system_version: None, - default_mode: None, - default_model: None, - }), - gemini: Some(crate::gemini::tests::local_command().into()), - codex: Some(BuiltinAgentServerSettings { - path: Some("codex-acp".into()), - args: None, - env: None, - ignore_system_version: None, - default_mode: None, - default_model: None, - }), - custom: collections::HashMap::default(), - }, - cx, - ); - }); - - cx.executor().allow_parking(); - - FakeFs::new(cx.executor()) -} - -pub async fn new_test_thread( - server: impl AgentServer + 'static, - project: Entity, - current_dir: impl AsRef, - cx: &mut TestAppContext, -) -> Entity { - let store = project.read_with(cx, |project, _| project.agent_server_store().clone()); - let delegate = AgentServerDelegate::new(store, project.clone(), None, None); - - let (connection, _) = cx - .update(|cx| server.connect(Some(current_dir.as_ref()), delegate, cx)) - .await - .unwrap(); - - cx.update(|cx| connection.new_thread(project.clone(), current_dir.as_ref(), cx)) - .await - .unwrap() -} - -pub async fn run_until_first_tool_call( - thread: &Entity, - wait_until: impl Fn(&AgentThreadEntry) -> bool + 'static, - cx: &mut TestAppContext, -) -> usize { - let (mut tx, mut rx) = mpsc::channel::(1); - - let subscription = cx.update(|cx| { - cx.subscribe(thread, move |thread, _, cx| { - for (ix, entry) in thread.read(cx).entries().iter().enumerate() { - if wait_until(entry) { - return tx.try_send(ix).unwrap(); - } - } - }) - }); - - select! { - // We have to use a smol timer here because - // cx.background_executor().timer isn't real in the test context - _ = futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(20))) => { - panic!("Timeout waiting for tool call") - } - ix = rx.next().fuse() => { - drop(subscription); - ix.unwrap() - } - } -} - -pub fn get_zed_path() -> PathBuf { - let mut zed_path = std::env::current_exe().unwrap(); - - while zed_path - .file_name() - .is_none_or(|name| name.to_string_lossy() != "debug") - { - if !zed_path.pop() { - panic!("Could not find target directory"); - } - } - - zed_path.push("zed"); - - if !zed_path.exists() { - panic!("\n🚨 Run `cargo build` at least once before running e2e tests\n\n"); - } - - zed_path -} diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs deleted file mode 100644 index 5fea74746a..0000000000 --- a/crates/agent_servers/src/gemini.rs +++ /dev/null @@ -1,102 +0,0 @@ -use std::rc::Rc; -use std::{any::Any, path::Path}; - -use crate::{AgentServer, AgentServerDelegate, load_proxy_env}; -use acp_thread::AgentConnection; -use anyhow::{Context as _, Result}; -use gpui::{App, SharedString, Task}; -use language_models::provider::google::GoogleLanguageModelProvider; -use project::agent_server_store::GEMINI_NAME; - -#[derive(Clone)] -pub struct Gemini; - -impl AgentServer for Gemini { - fn name(&self) -> SharedString { - "Gemini CLI".into() - } - - fn logo(&self) -> ui::IconName { - ui::IconName::AiGemini - } - - fn connect( - &self, - root_dir: Option<&Path>, - delegate: AgentServerDelegate, - cx: &mut App, - ) -> Task, Option)>> { - let name = self.name(); - let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned()); - let is_remote = delegate.project.read(cx).is_via_remote_server(); - let store = delegate.store.downgrade(); - let mut extra_env = load_proxy_env(cx); - let default_mode = self.default_mode(cx); - let default_model = self.default_model(cx); - - cx.spawn(async move |cx| { - extra_env.insert("SURFACE".to_owned(), "zed".to_owned()); - - if let Some(api_key) = cx - .update(GoogleLanguageModelProvider::api_key_for_gemini_cli)? - .await - .ok() - { - extra_env.insert("GEMINI_API_KEY".into(), api_key); - } - let (command, root_dir, login) = store - .update(cx, |store, cx| { - let agent = store - .get_external_agent(&GEMINI_NAME.into()) - .context("Gemini CLI is not registered")?; - anyhow::Ok(agent.get_command( - root_dir.as_deref(), - extra_env, - delegate.status_tx, - delegate.new_version_available, - &mut cx.to_async(), - )) - })?? - .await?; - - let connection = crate::acp::connect( - name, - command, - root_dir.as_ref(), - default_mode, - default_model, - is_remote, - cx, - ) - .await?; - Ok((connection, login)) - }) - } - - fn into_any(self: Rc) -> Rc { - self - } -} - -#[cfg(test)] -pub(crate) mod tests { - use project::agent_server_store::AgentServerCommand; - - use super::*; - use std::path::Path; - - crate::common_e2e_tests!(async |_, _, _| Gemini, allow_option_id = "proceed_once"); - - pub fn local_command() -> AgentServerCommand { - let cli_path = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../../gemini-cli/packages/cli") - .to_string_lossy() - .to_string(); - - AgentServerCommand { - path: "node".into(), - args: vec![cli_path], - env: None, - } - } -} diff --git a/crates/agent_settings/Cargo.toml b/crates/agent_settings/Cargo.toml deleted file mode 100644 index 8ddcac24fe..0000000000 --- a/crates/agent_settings/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "agent_settings" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/agent_settings.rs" - -[dependencies] -anyhow.workspace = true -cloud_llm_client.workspace = true -collections.workspace = true -convert_case.workspace = true -fs.workspace = true -gpui.workspace = true -language_model.workspace = true -project.workspace = true -schemars.workspace = true -serde.workspace = true -settings.workspace = true -util.workspace = true - -[dev-dependencies] -fs.workspace = true -gpui = { workspace = true, features = ["test-support"] } -paths.workspace = true -serde_json_lenient.workspace = true -serde_json.workspace = true -settings = { workspace = true, features = ["test-support"] } diff --git a/crates/agent_settings/LICENSE-GPL b/crates/agent_settings/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/agent_settings/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/agent_settings/src/agent_profile.rs b/crates/agent_settings/src/agent_profile.rs deleted file mode 100644 index aff666e011..0000000000 --- a/crates/agent_settings/src/agent_profile.rs +++ /dev/null @@ -1,202 +0,0 @@ -use std::sync::Arc; - -use anyhow::{Result, bail}; -use collections::IndexMap; -use convert_case::{Case, Casing as _}; -use fs::Fs; -use gpui::{App, SharedString}; -use settings::{ - AgentProfileContent, ContextServerPresetContent, LanguageModelSelection, Settings as _, - SettingsContent, update_settings_file, -}; -use util::ResultExt as _; - -use crate::{AgentProfileId, AgentSettings}; - -pub mod builtin_profiles { - use super::AgentProfileId; - - pub const WRITE: &str = "write"; - pub const ASK: &str = "ask"; - pub const MINIMAL: &str = "minimal"; - - pub fn is_builtin(profile_id: &AgentProfileId) -> bool { - profile_id.as_str() == WRITE || profile_id.as_str() == ASK || profile_id.as_str() == MINIMAL - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct AgentProfile { - id: AgentProfileId, -} - -pub type AvailableProfiles = IndexMap; - -impl AgentProfile { - pub fn new(id: AgentProfileId) -> Self { - Self { id } - } - - pub fn id(&self) -> &AgentProfileId { - &self.id - } - - /// Saves a new profile to the settings. - pub fn create( - name: String, - base_profile_id: Option, - fs: Arc, - cx: &App, - ) -> AgentProfileId { - let id = AgentProfileId(name.to_case(Case::Kebab).into()); - - let base_profile = - base_profile_id.and_then(|id| AgentSettings::get_global(cx).profiles.get(&id).cloned()); - - // Copy toggles from the base profile so the new profile starts with familiar defaults. - let tools = base_profile - .as_ref() - .map(|profile| profile.tools.clone()) - .unwrap_or_default(); - let enable_all_context_servers = base_profile - .as_ref() - .map(|profile| profile.enable_all_context_servers) - .unwrap_or_default(); - let context_servers = base_profile - .as_ref() - .map(|profile| profile.context_servers.clone()) - .unwrap_or_default(); - // Preserve the base profile's model preference when cloning into a new profile. - let default_model = base_profile - .as_ref() - .and_then(|profile| profile.default_model.clone()); - - let profile_settings = AgentProfileSettings { - name: name.into(), - tools, - enable_all_context_servers, - context_servers, - default_model, - }; - - update_settings_file(fs, cx, { - let id = id.clone(); - move |settings, _cx| { - profile_settings.save_to_settings(id, settings).log_err(); - } - }); - - id - } - - /// Returns a map of AgentProfileIds to their names - pub fn available_profiles(cx: &App) -> AvailableProfiles { - let mut profiles = AvailableProfiles::default(); - for (id, profile) in AgentSettings::get_global(cx).profiles.iter() { - profiles.insert(id.clone(), profile.name.clone()); - } - profiles - } -} - -/// A profile for the Zed Agent that controls its behavior. -#[derive(Debug, Clone)] -pub struct AgentProfileSettings { - /// The name of the profile. - pub name: SharedString, - pub tools: IndexMap, bool>, - pub enable_all_context_servers: bool, - pub context_servers: IndexMap, ContextServerPreset>, - /// Default language model to apply when this profile becomes active. - pub default_model: Option, -} - -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)) - } - - pub fn save_to_settings( - &self, - profile_id: AgentProfileId, - content: &mut SettingsContent, - ) -> Result<()> { - let profiles = content - .agent - .get_or_insert_default() - .profiles - .get_or_insert_default(); - if profiles.contains_key(&profile_id.0) { - bail!("profile with ID '{profile_id}' already exists"); - } - - profiles.insert( - profile_id.0, - AgentProfileContent { - name: self.name.clone().into(), - tools: self.tools.clone(), - enable_all_context_servers: Some(self.enable_all_context_servers), - context_servers: self - .context_servers - .clone() - .into_iter() - .map(|(server_id, preset)| { - ( - server_id, - ContextServerPresetContent { - tools: preset.tools, - }, - ) - }) - .collect(), - default_model: self.default_model.clone(), - }, - ); - - Ok(()) - } -} - -impl From for AgentProfileSettings { - fn from(content: AgentProfileContent) -> Self { - let AgentProfileContent { - name, - tools, - enable_all_context_servers, - context_servers, - default_model, - } = content; - - Self { - name: name.into(), - tools, - enable_all_context_servers: enable_all_context_servers.unwrap_or_default(), - context_servers: context_servers - .into_iter() - .map(|(server_id, preset)| (server_id, preset.into())) - .collect(), - default_model, - } - } -} - -#[derive(Debug, Clone, Default)] -pub struct ContextServerPreset { - pub tools: IndexMap, bool>, -} - -impl From for ContextServerPreset { - fn from(content: settings::ContextServerPresetContent) -> Self { - Self { - tools: content.tools, - } - } -} diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs deleted file mode 100644 index 084ac7c3e7..0000000000 --- a/crates/agent_settings/src/agent_settings.rs +++ /dev/null @@ -1,182 +0,0 @@ -mod agent_profile; - -use std::sync::Arc; - -use collections::IndexMap; -use gpui::{App, Pixels, px}; -use language_model::LanguageModel; -use project::DisableAiSettings; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{ - DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection, - NotifyWhenAgentWaiting, RegisterSetting, Settings, -}; - -pub use crate::agent_profile::*; - -pub const SUMMARIZE_THREAD_PROMPT: &str = include_str!("prompts/summarize_thread_prompt.txt"); -pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str = - include_str!("prompts/summarize_thread_detailed_prompt.txt"); - -#[derive(Clone, Debug, RegisterSetting)] -pub struct AgentSettings { - pub enabled: bool, - pub button: bool, - pub dock: DockPosition, - pub default_width: Pixels, - pub default_height: Pixels, - pub default_model: Option, - pub inline_assistant_model: Option, - pub commit_message_model: Option, - pub thread_summary_model: Option, - pub inline_alternatives: Vec, - pub default_profile: AgentProfileId, - pub default_view: DefaultAgentView, - pub profiles: IndexMap, - pub always_allow_tool_actions: bool, - pub notify_when_agent_waiting: NotifyWhenAgentWaiting, - pub play_sound_when_agent_done: bool, - pub single_file_review: bool, - pub model_parameters: Vec, - pub preferred_completion_mode: CompletionMode, - pub enable_feedback: bool, - pub expand_edit_card: bool, - pub expand_terminal_card: bool, - pub use_modifier_to_send: bool, - pub message_editor_min_lines: usize, -} - -impl AgentSettings { - pub fn enabled(&self, cx: &App) -> bool { - self.enabled && !DisableAiSettings::get_global(cx).disable_ai - } - - pub fn temperature_for_model(model: &Arc, cx: &App) -> Option { - let settings = Self::get_global(cx); - for setting in settings.model_parameters.iter().rev() { - if let Some(provider) = &setting.provider - && provider.0 != model.provider_id().0 - { - continue; - } - if let Some(setting_model) = &setting.model - && *setting_model != model.id().0 - { - continue; - } - return setting.temperature; - } - return None; - } - - pub fn set_inline_assistant_model(&mut self, provider: String, model: String) { - self.inline_assistant_model = Some(LanguageModelSelection { - provider: provider.into(), - model, - }); - } - - pub fn set_commit_message_model(&mut self, provider: String, model: String) { - self.commit_message_model = Some(LanguageModelSelection { - provider: provider.into(), - model, - }); - } - - pub fn set_thread_summary_model(&mut self, provider: String, model: String) { - self.thread_summary_model = Some(LanguageModelSelection { - provider: provider.into(), - model, - }); - } - - pub fn set_message_editor_max_lines(&self) -> usize { - self.message_editor_min_lines * 2 - } -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)] -#[serde(rename_all = "snake_case")] -pub enum CompletionMode { - #[default] - Normal, - #[serde(alias = "max")] - Burn, -} - -impl From for cloud_llm_client::CompletionMode { - fn from(value: CompletionMode) -> Self { - match value { - CompletionMode::Normal => cloud_llm_client::CompletionMode::Normal, - CompletionMode::Burn => cloud_llm_client::CompletionMode::Max, - } - } -} - -impl From for CompletionMode { - fn from(value: settings::CompletionMode) -> Self { - match value { - settings::CompletionMode::Normal => CompletionMode::Normal, - settings::CompletionMode::Burn => CompletionMode::Burn, - } - } -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)] -pub struct AgentProfileId(pub Arc); - -impl AgentProfileId { - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl std::fmt::Display for AgentProfileId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Default for AgentProfileId { - fn default() -> Self { - Self("write".into()) - } -} - -impl Settings for AgentSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let agent = content.agent.clone().unwrap(); - Self { - enabled: agent.enabled.unwrap(), - button: agent.button.unwrap(), - dock: agent.dock.unwrap(), - default_width: px(agent.default_width.unwrap()), - default_height: px(agent.default_height.unwrap()), - default_model: Some(agent.default_model.unwrap()), - inline_assistant_model: agent.inline_assistant_model, - commit_message_model: agent.commit_message_model, - thread_summary_model: agent.thread_summary_model, - inline_alternatives: agent.inline_alternatives.unwrap_or_default(), - default_profile: AgentProfileId(agent.default_profile.unwrap()), - default_view: agent.default_view.unwrap(), - profiles: agent - .profiles - .unwrap() - .into_iter() - .map(|(key, val)| (AgentProfileId(key), val.into())) - .collect(), - always_allow_tool_actions: agent.always_allow_tool_actions.unwrap(), - notify_when_agent_waiting: agent.notify_when_agent_waiting.unwrap(), - play_sound_when_agent_done: agent.play_sound_when_agent_done.unwrap(), - single_file_review: agent.single_file_review.unwrap(), - model_parameters: agent.model_parameters, - preferred_completion_mode: agent.preferred_completion_mode.unwrap().into(), - enable_feedback: agent.enable_feedback.unwrap(), - expand_edit_card: agent.expand_edit_card.unwrap(), - expand_terminal_card: agent.expand_terminal_card.unwrap(), - use_modifier_to_send: agent.use_modifier_to_send.unwrap(), - message_editor_min_lines: agent.message_editor_min_lines.unwrap(), - } - } -} diff --git a/crates/agent_settings/src/prompts/summarize_thread_detailed_prompt.txt b/crates/agent_settings/src/prompts/summarize_thread_detailed_prompt.txt deleted file mode 100644 index 30fab472af..0000000000 --- a/crates/agent_settings/src/prompts/summarize_thread_detailed_prompt.txt +++ /dev/null @@ -1,6 +0,0 @@ -Generate a detailed summary of this conversation. Include: -1. A brief overview of what was discussed -2. Key facts or information discovered -3. Outcomes or conclusions reached -4. Any action items or next steps if any -Format it in Markdown with headings and bullet points. diff --git a/crates/agent_settings/src/prompts/summarize_thread_prompt.txt b/crates/agent_settings/src/prompts/summarize_thread_prompt.txt deleted file mode 100644 index f57644433b..0000000000 --- a/crates/agent_settings/src/prompts/summarize_thread_prompt.txt +++ /dev/null @@ -1,4 +0,0 @@ -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:`. -If the conversation is about a specific subject, include it in the title. -Be descriptive. DO NOT speak in the first person. diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml deleted file mode 100644 index 2af0ce6fbd..0000000000 --- a/crates/agent_ui/Cargo.toml +++ /dev/null @@ -1,126 +0,0 @@ -[package] -name = "agent_ui" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/agent_ui.rs" -doctest = false - -[features] -test-support = ["gpui/test-support", "language/test-support", "reqwest_client"] -unit-eval = [] - -[dependencies] -acp_thread.workspace = true -action_log.workspace = true -agent-client-protocol.workspace = true -agent.workspace = true -agent_servers.workspace = true -agent_settings.workspace = true -ai_onboarding.workspace = true -anyhow.workspace = true -arrayvec.workspace = true -assistant_text_thread.workspace = true -assistant_slash_command.workspace = true -assistant_slash_commands.workspace = true -audio.workspace = true -buffer_diff.workspace = true -chrono.workspace = true -client.workspace = true -cloud_llm_client.workspace = true -collections.workspace = true -command_palette_hooks.workspace = true -component.workspace = true -context_server.workspace = true -db.workspace = true -editor.workspace = true -extension.workspace = true -extension_host.workspace = true -feature_flags.workspace = true -file_icons.workspace = true -fs.workspace = true -futures.workspace = true -fuzzy.workspace = true -gpui.workspace = true -gpui_tokio.workspace = true -html_to_markdown.workspace = true -http_client.workspace = true -indoc.workspace = true -itertools.workspace = true -jsonschema.workspace = true -language.workspace = true -language_model.workspace = true -language_models.workspace = true -log.workspace = true -lsp.workspace = true -markdown.workspace = true -menu.workspace = true -multi_buffer.workspace = true -notifications.workspace = true -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 -release_channel.workspace = true -rope.workspace = true -rules_library.workspace = true -schemars.workspace = true -search.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_json_lenient.workspace = true -settings.workspace = true -smol.workspace = true -streaming_diff.workspace = true -task.workspace = true -telemetry.workspace = true -telemetry_events.workspace = true -terminal.workspace = true -terminal_view.workspace = true -text.workspace = true -theme.workspace = true -time.workspace = true -time_format.workspace = true -ui.workspace = true -ui_input.workspace = true -url.workspace = true -util.workspace = true -uuid.workspace = true -watch.workspace = true -workspace.workspace = true -zed_actions.workspace = true -image.workspace = true -async-fs.workspace = true -reqwest_client = { workspace = true, optional = true } - -[dev-dependencies] -acp_thread = { workspace = true, features = ["test-support"] } -agent = { workspace = true, features = ["test-support"] } -assistant_text_thread = { workspace = true, features = ["test-support"] } -buffer_diff = { workspace = true, features = ["test-support"] } -clock.workspace = true -db = { workspace = true, features = ["test-support"] } -editor = { workspace = true, features = ["test-support"] } -eval_utils.workspace = true -gpui = { workspace = true, "features" = ["test-support"] } -indoc.workspace = true -language = { workspace = true, "features" = ["test-support"] } -languages = { workspace = true, features = ["test-support"] } -language_model = { workspace = true, "features" = ["test-support"] } -pretty_assertions.workspace = true -project = { workspace = true, features = ["test-support"] } -semver.workspace = true -rand.workspace = true -reqwest_client.workspace = true -tree-sitter-md.workspace = true -unindent.workspace = true diff --git a/crates/agent_ui/LICENSE-GPL b/crates/agent_ui/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/agent_ui/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/agent_ui/src/acp.rs b/crates/agent_ui/src/acp.rs deleted file mode 100644 index 7a740c2dc4..0000000000 --- a/crates/agent_ui/src/acp.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod entry_view_state; -mod message_editor; -mod mode_selector; -mod model_selector; -mod model_selector_popover; -mod thread_history; -mod thread_view; - -pub use mode_selector::ModeSelector; -pub use model_selector::AcpModelSelector; -pub use model_selector_popover::AcpModelSelectorPopover; -pub use thread_history::*; -pub use thread_view::AcpThreadView; 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 feae74a86b..0000000000 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ /dev/null @@ -1,532 +0,0 @@ -use std::{cell::RefCell, ops::Range, rc::Rc}; - -use acp_thread::{AcpThread, AgentThreadEntry}; -use agent::HistoryStore; -use agent_client_protocol::{self as acp, ToolCallId}; -use collections::HashMap; -use editor::{Editor, EditorMode, MinimapVisibility, SizingBehavior}; -use gpui::{ - AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle, Focusable, - ScrollHandle, SharedString, TextStyleRefinement, WeakEntity, Window, -}; -use language::language_settings::SoftWrap; -use project::Project; -use prompt_store::PromptStore; -use settings::Settings as _; -use terminal_view::TerminalView; -use theme::ThemeSettings; -use ui::{Context, TextSize}; -use workspace::Workspace; - -use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; - -pub struct EntryViewState { - workspace: WeakEntity, - project: WeakEntity, - history_store: Entity, - prompt_store: Option>, - entries: Vec, - prompt_capabilities: Rc>, - available_commands: Rc>>, - agent_name: SharedString, -} - -impl EntryViewState { - pub fn new( - workspace: WeakEntity, - project: WeakEntity, - history_store: Entity, - prompt_store: Option>, - prompt_capabilities: Rc>, - available_commands: Rc>>, - agent_name: SharedString, - ) -> Self { - Self { - workspace, - project, - history_store, - prompt_store, - entries: Vec::new(), - prompt_capabilities, - available_commands, - agent_name, - } - } - - pub fn entry(&self, index: usize) -> Option<&Entry> { - self.entries.get(index) - } - - pub fn sync_entry( - &mut self, - index: usize, - thread: &Entity, - 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(), - self.available_commands.clone(), - self.agent_name.clone(), - "Edit message - @ to include context", - editor::EditorMode::AutoHeight { - min_lines: 1, - max_lines: None, - }, - window, - cx, - ); - if !has_id { - editor.set_read_only(true, cx); - } - editor.set_message(chunks, window, cx); - editor - }); - cx.subscribe(&message_editor, move |_, editor, event, cx| { - cx.emit(EntryViewEvent { - entry_index: index, - view_event: ViewEvent::MessageEditorEvent(editor, *event), - }) - }) - .detach(); - self.set_entry(index, Entry::UserMessage(message_editor)); - } - } - AgentThreadEntry::ToolCall(tool_call) => { - let id = tool_call.id.clone(); - let terminals = tool_call.terminals().cloned().collect::>(); - 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 - }; - - let is_tool_call_completed = - matches!(tool_call.status, acp_thread::ToolCallStatus::Completed); - - for terminal in terminals { - match views.entry(terminal.entity_id()) { - collections::hash_map::Entry::Vacant(entry) => { - let element = create_terminal( - self.workspace.clone(), - self.project.clone(), - terminal.clone(), - window, - cx, - ) - .into_any(); - cx.emit(EntryViewEvent { - entry_index: index, - view_event: ViewEvent::NewTerminal(id.clone()), - }); - entry.insert(element); - } - collections::hash_map::Entry::Occupied(_entry) => { - if is_tool_call_completed && terminal.read(cx).output().is_none() { - cx.emit(EntryViewEvent { - entry_index: index, - view_event: ViewEvent::TerminalMovedToBackground(id.clone()), - }); - } - } - } - } - - for diff in diffs { - views.entry(diff.entity_id()).or_insert_with(|| { - let element = create_editor_diff(diff.clone(), window, cx).into_any(); - cx.emit(EntryViewEvent { - entry_index: index, - view_event: ViewEvent::NewDiff(id.clone()), - }); - element - }); - } - } - AgentThreadEntry::AssistantMessage(message) => { - let entry = if let Some(Entry::AssistantMessage(entry)) = - self.entries.get_mut(index) - { - entry - } else { - self.set_entry( - index, - Entry::AssistantMessage(AssistantMessageEntry::default()), - ); - let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else { - unreachable!() - }; - entry - }; - entry.sync(message); - } - }; - } - - fn set_entry(&mut self, index: usize, entry: Entry) { - if index == self.entries.len() { - self.entries.push(entry); - } else { - self.entries[index] = entry; - } - } - - pub fn remove(&mut self, range: Range) { - self.entries.drain(range); - } - - pub fn agent_ui_font_size_changed(&mut self, cx: &mut App) { - for entry in self.entries.iter() { - match entry { - Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {} - Entry::Content(response_views) => { - for view in response_views.values() { - if let Ok(diff_editor) = view.clone().downcast::() { - 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), - TerminalMovedToBackground(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 focus_handle(&self, cx: &App) -> Option { - match self { - Self::UserMessage(editor) => Some(editor.read(cx).focus_handle(cx)), - Self::AssistantMessage(_) | Self::Content(_) => None, - } - } - - 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: WeakEntity, - terminal: Entity, - window: &mut Window, - cx: &mut App, -) -> Entity { - cx.new(|cx| { - let mut view = TerminalView::new( - terminal.read(cx).inner().clone(), - workspace, - None, - project, - 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, - sizing_behavior: SizingBehavior::SizeByContent, - }, - 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_ui_font_size(cx)) - .into(), - ), - ..Default::default() - } -} - -#[cfg(test)] -mod tests { - use std::{path::Path, rc::Rc}; - - use acp_thread::{AgentConnection, StubAgentConnection}; - use agent::HistoryStore; - use agent_client_protocol as acp; - use assistant_text_thread::TextThreadStore; - use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; - use editor::RowInfo; - use fs::FakeFs; - use gpui::{AppContext as _, 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::SettingsStore; - 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::new("tool", "Tool call") - .status(acp::ToolCallStatus::InProgress) - .content(vec![acp::ToolCallContent::Diff( - acp::Diff::new("/project/hello.txt", "hello world").old_text("hi world"), - )]); - 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 text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - - let view_state = cx.new(|_cx| { - EntryViewState::new( - workspace.downgrade(), - project.downgrade(), - history_store, - None, - Default::default(), - Default::default(), - "Test Agent".into(), - ) - }); - - view_state.update_in(cx, |view_state, window, cx| { - view_state.sync_entry(0, &thread, window, cx) - }); - - let diff = thread.read_with(cx, |thread, _cx| { - thread - .entries() - .get(0) - .unwrap() - .diffs() - .next() - .unwrap() - .clone() - }); - - cx.run_until_parked(); - - let diff_editor = view_state.read_with(cx, |view_state, _cx| { - view_state.entry(0).unwrap().editor_for_diff(&diff).unwrap() - }); - assert_eq!( - diff_editor.read_with(cx, |editor, cx| editor.text(cx)), - "hi world\nhello world" - ); - let row_infos = diff_editor.read_with(cx, |editor, cx| { - let multibuffer = editor.buffer().read(cx); - multibuffer - .snapshot(cx) - .row_infos(MultiBufferRow(0)) - .collect::>() - }); - 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); - theme::init(theme::LoadThemes::JustBase, cx); - release_channel::init(semver::Version::new(0, 0, 0), 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 5e9c55cc56..0000000000 --- a/crates/agent_ui/src/acp/message_editor.rs +++ /dev/null @@ -1,2532 +0,0 @@ -use crate::{ - ChatWithFollow, - completion_provider::{ - PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextAction, - PromptContextType, SlashCommandCompletion, - }, - mention_set::{ - Mention, MentionImage, MentionSet, insert_crease_for_mention, paste_images_as_context, - }, -}; -use acp_thread::MentionUri; -use agent::HistoryStore; -use agent_client_protocol as acp; -use anyhow::{Result, anyhow}; -use collections::HashSet; -use editor::{ - Addon, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, - EditorEvent, EditorMode, EditorStyle, Inlay, MultiBuffer, MultiBufferOffset, - MultiBufferSnapshot, ToOffset, actions::Paste, code_context_menus::CodeContextMenu, - scroll::Autoscroll, -}; -use futures::{FutureExt as _, future::join_all}; -use gpui::{ - AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageFormat, - KeyContext, SharedString, Subscription, Task, TextStyle, WeakEntity, -}; -use language::{Buffer, Language, language_settings::InlayHintKind}; -use project::{CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, Worktree}; -use prompt_store::PromptStore; -use rope::Point; -use settings::Settings; -use std::{cell::RefCell, fmt::Write, rc::Rc, sync::Arc}; -use theme::ThemeSettings; -use ui::prelude::*; -use util::{ResultExt, debug_panic}; -use workspace::{CollaboratorId, Workspace}; -use zed_actions::agent::Chat; - -pub struct MessageEditor { - mention_set: Entity, - editor: Entity, - workspace: WeakEntity, - prompt_capabilities: Rc>, - available_commands: Rc>>, - agent_name: SharedString, - _subscriptions: Vec, - _parse_slash_command_task: Task<()>, -} - -#[derive(Clone, Copy, Debug)] -pub enum MessageEditorEvent { - Send, - Cancel, - Focus, - LostFocus, -} - -impl EventEmitter for MessageEditor {} - -const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0); - -impl PromptCompletionProviderDelegate for Entity { - fn supports_images(&self, cx: &App) -> bool { - self.read(cx).prompt_capabilities.borrow().image - } - - fn supported_modes(&self, cx: &App) -> Vec { - let mut supported = vec![PromptContextType::File, PromptContextType::Symbol]; - if self.read(cx).prompt_capabilities.borrow().embedded_context { - supported.extend(&[ - PromptContextType::Thread, - PromptContextType::Fetch, - PromptContextType::Rules, - ]); - } - supported - } - - fn available_commands(&self, cx: &App) -> Vec { - self.read(cx) - .available_commands - .borrow() - .iter() - .map(|cmd| crate::completion_provider::AvailableCommand { - name: cmd.name.clone().into(), - description: cmd.description.clone().into(), - requires_argument: cmd.input.is_some(), - }) - .collect() - } - - fn confirm_command(&self, cx: &mut App) { - self.update(cx, |this, cx| this.send(cx)); - } -} - -impl MessageEditor { - pub fn new( - workspace: WeakEntity, - project: WeakEntity, - history_store: Entity, - prompt_store: Option>, - prompt_capabilities: Rc>, - available_commands: Rc>>, - agent_name: SharedString, - placeholder: &str, - 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 editor = cx.new(|cx| { - let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx)); - let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - - let mut editor = Editor::new(mode, buffer, None, window, cx); - editor.set_placeholder_text(placeholder, window, cx); - editor.set_show_indent_guides(false, cx); - editor.set_show_completions_on_input(Some(true)); - editor.set_soft_wrap(); - editor.set_use_modal_editing(true); - editor.set_context_menu_options(ContextMenuOptions { - min_entries_visible: 12, - max_entries_visible: 12, - placement: Some(ContextMenuPlacement::Above), - }); - editor.register_addon(MessageEditorAddon::new()); - editor - }); - let mention_set = - cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone())); - let completion_provider = Rc::new(PromptCompletionProvider::new( - cx.entity(), - editor.downgrade(), - mention_set.clone(), - history_store.clone(), - prompt_store.clone(), - workspace.clone(), - )); - editor.update(cx, |editor, _cx| { - editor.set_completion_provider(Some(completion_provider.clone())) - }); - - cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| { - cx.emit(MessageEditorEvent::Focus) - }) - .detach(); - cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| { - cx.emit(MessageEditorEvent::LostFocus) - }) - .detach(); - - let mut has_hint = false; - let mut subscriptions = Vec::new(); - - subscriptions.push(cx.subscribe_in(&editor, window, { - move |this, editor, event, window, cx| { - if let EditorEvent::Edited { .. } = event - && !editor.read(cx).read_only(cx) - { - editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(window, cx); - this.mention_set - .update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot)); - - let new_hints = this - .command_hint(snapshot.buffer()) - .into_iter() - .collect::>(); - let has_new_hint = !new_hints.is_empty(); - editor.splice_inlays( - if has_hint { - &[COMMAND_HINT_INLAY_ID] - } else { - &[] - }, - new_hints, - cx, - ); - has_hint = has_new_hint; - }); - cx.notify(); - } - } - })); - - Self { - editor, - mention_set, - workspace, - prompt_capabilities, - available_commands, - agent_name, - _subscriptions: subscriptions, - _parse_slash_command_task: Task::ready(()), - } - } - - fn command_hint(&self, snapshot: &MultiBufferSnapshot) -> Option { - let available_commands = self.available_commands.borrow(); - if available_commands.is_empty() { - return None; - } - - let parsed_command = SlashCommandCompletion::try_parse(&snapshot.text(), 0)?; - if parsed_command.argument.is_some() { - return None; - } - - let command_name = parsed_command.command?; - let available_command = available_commands - .iter() - .find(|command| command.name == command_name)?; - - let acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput { - mut hint, - .. - }) = available_command.input.clone()? - else { - return None; - }; - - let mut hint_pos = MultiBufferOffset(parsed_command.source_range.end) + 1usize; - if hint_pos > snapshot.len() { - hint_pos = snapshot.len(); - hint.insert(0, ' '); - } - - let hint_pos = snapshot.anchor_after(hint_pos); - - Some(Inlay::hint( - COMMAND_HINT_INLAY_ID, - hint_pos, - &InlayHint { - position: hint_pos.text_anchor, - label: InlayHintLabel::String(hint), - kind: Some(InlayHintKind::Parameter), - padding_left: false, - padding_right: false, - tooltip: None, - resolve_state: project::ResolveState::Resolved, - }, - )) - } - - pub fn insert_thread_summary( - &mut self, - thread: agent::DbThreadMetadata, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = self.workspace.upgrade() else { - return; - }; - let uri = MentionUri::Thread { - id: thread.id.clone(), - name: thread.title.to_string(), - }; - let content = format!("{}\n", uri.as_link()); - - let content_len = content.len() - 1; - - let start = self.editor.update(cx, |editor, cx| { - editor.set_text(content, window, cx); - editor - .buffer() - .read(cx) - .snapshot(cx) - .anchor_before(Point::zero()) - .text_anchor - }); - - let supports_images = self.prompt_capabilities.borrow().image; - - self.mention_set - .update(cx, |mention_set, cx| { - mention_set.confirm_mention_completion( - thread.title, - start, - content_len, - uri, - supports_images, - self.editor.clone(), - &workspace, - window, - cx, - ) - }) - .detach(); - } - - #[cfg(test)] - pub(crate) fn editor(&self) -> &Entity { - &self.editor - } - - pub fn is_empty(&self, cx: &App) -> bool { - self.editor.read(cx).is_empty(cx) - } - - pub fn is_completions_menu_visible(&self, cx: &App) -> bool { - self.editor - .read(cx) - .context_menu() - .borrow() - .as_ref() - .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()) - } - - #[cfg(test)] - pub fn mention_set(&self) -> &Entity { - &self.mention_set - } - - fn validate_slash_commands( - text: &str, - available_commands: &[acp::AvailableCommand], - agent_name: &str, - ) -> Result<()> { - if let Some(parsed_command) = SlashCommandCompletion::try_parse(text, 0) { - if let Some(command_name) = parsed_command.command { - // Check if this command is in the list of available commands from the server - let is_supported = available_commands - .iter() - .any(|cmd| cmd.name == command_name); - - if !is_supported { - return Err(anyhow!( - "The /{} command is not supported by {}.\n\nAvailable commands: {}", - command_name, - agent_name, - if available_commands.is_empty() { - "none".to_string() - } else { - available_commands - .iter() - .map(|cmd| format!("/{}", cmd.name)) - .collect::>() - .join(", ") - } - )); - } - } - } - Ok(()) - } - - pub fn contents( - &self, - full_mention_content: bool, - cx: &mut Context, - ) -> Task, Vec>)>> { - // Check for unsupported slash commands before spawning async task - let text = self.editor.read(cx).text(cx); - let available_commands = self.available_commands.borrow().clone(); - if let Err(err) = - Self::validate_slash_commands(&text, &available_commands, &self.agent_name) - { - return Task::ready(Err(err)); - } - - let contents = self - .mention_set - .update(cx, |store, cx| store.contents(full_mention_content, cx)); - let editor = self.editor.clone(); - let supports_embedded_context = self.prompt_capabilities.borrow().embedded_context; - - cx.spawn(async move |_, cx| { - let contents = contents.await?; - let mut all_tracked_buffers = Vec::new(); - - let result = editor.update(cx, |editor, cx| { - let (mut ix, _) = text - .char_indices() - .find(|(_, c)| !c.is_whitespace()) - .unwrap_or((0, '\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.0 > ix { - let chunk = text[ix..crease_range.start.0].into(); - chunks.push(chunk); - } - let chunk = match mention { - Mention::Text { - content, - tracked_buffers, - } => { - all_tracked_buffers.extend(tracked_buffers.iter().cloned()); - if supports_embedded_context { - acp::ContentBlock::Resource(acp::EmbeddedResource::new( - acp::EmbeddedResourceResource::TextResourceContents( - acp::TextResourceContents::new( - content.clone(), - uri.to_uri().to_string(), - ), - ), - )) - } else { - acp::ContentBlock::ResourceLink(acp::ResourceLink::new( - uri.name(), - uri.to_uri().to_string(), - )) - } - } - Mention::Image(mention_image) => acp::ContentBlock::Image( - acp::ImageContent::new( - mention_image.data.clone(), - mention_image.format.mime_type(), - ) - .uri(match uri { - MentionUri::File { .. } => Some(uri.to_uri().to_string()), - MentionUri::PastedImage => None, - other => { - debug_panic!( - "unexpected mention uri for image: {:?}", - other - ); - None - } - }), - ), - Mention::Link => acp::ContentBlock::ResourceLink( - acp::ResourceLink::new(uri.name(), uri.to_uri().to_string()), - ), - }; - chunks.push(chunk); - ix = crease_range.end.0; - } - - if ix < text.len() { - let last_chunk = text[ix..].trim_end().to_owned(); - if !last_chunk.is_empty() { - chunks.push(last_chunk.into()); - } - } - }); - Ok((chunks, all_tracked_buffers)) - })?; - result - }) - } - - pub fn clear(&mut self, window: &mut Window, cx: &mut Context) { - self.editor.update(cx, |editor, cx| { - editor.clear(window, cx); - editor.remove_creases( - self.mention_set.update(cx, |mention_set, _cx| { - mention_set - .clear() - .map(|(crease_id, _)| crease_id) - .collect::>() - }), - cx, - ) - }); - } - - pub fn send(&mut self, cx: &mut Context) { - if self.is_empty(cx) { - return; - } - self.editor.update(cx, |editor, cx| { - editor.clear_inlay_hints(cx); - }); - cx.emit(MessageEditorEvent::Send) - } - - pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context) { - let editor = self.editor.clone(); - - cx.spawn_in(window, async move |_, cx| { - editor - .update_in(cx, |editor, window, cx| { - let menu_is_open = - editor.context_menu().borrow().as_ref().is_some_and(|menu| { - matches!(menu, CodeContextMenu::Completions(_)) && menu.visible() - }); - - let has_at_sign = { - let snapshot = editor.display_snapshot(cx); - let cursor = editor.selections.newest::(&snapshot).head(); - let offset = cursor.to_offset(&snapshot); - if offset.0 > 0 { - snapshot - .buffer_snapshot() - .reversed_chars_at(offset) - .next() - .map(|sign| sign == '@') - .unwrap_or(false) - } else { - false - } - }; - - if menu_is_open && has_at_sign { - return; - } - - editor.insert("@", window, cx); - editor.show_completions(&editor::actions::ShowCompletions, window, cx); - }) - .log_err(); - }) - .detach(); - } - - fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context) { - self.send(cx); - } - - fn chat_with_follow( - &mut self, - _: &ChatWithFollow, - window: &mut Window, - cx: &mut Context, - ) { - self.workspace - .update(cx, |this, cx| { - this.follow(CollaboratorId::Agent, window, cx) - }) - .log_err(); - - self.send(cx); - } - - 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) { - let editor_clipboard_selections = cx - .read_from_clipboard() - .and_then(|item| item.entries().first().cloned()) - .and_then(|entry| match entry { - ClipboardEntry::String(text) => { - text.metadata_json::>() - } - _ => None, - }); - - let has_file_context = editor_clipboard_selections - .as_ref() - .is_some_and(|selections| { - selections - .iter() - .any(|sel| sel.file_path.is_some() && sel.line_range.is_some()) - }); - - if has_file_context { - if let Some((workspace, selections)) = - self.workspace.upgrade().zip(editor_clipboard_selections) - { - let Some(first_selection) = selections.first() else { - return; - }; - if let Some(file_path) = &first_selection.file_path { - // In case someone pastes selections from another window - // with a different project, we don't want to insert the - // crease (containing the absolute path) since the agent - // cannot access files outside the project. - let is_in_project = workspace - .read(cx) - .project() - .read(cx) - .project_path_for_absolute_path(file_path, cx) - .is_some(); - if !is_in_project { - return; - } - } - - cx.stop_propagation(); - let insertion_target = self - .editor - .read(cx) - .selections - .newest_anchor() - .start - .text_anchor; - - let project = workspace.read(cx).project().clone(); - for selection in selections { - if let (Some(file_path), Some(line_range)) = - (selection.file_path, selection.line_range) - { - let crease_text = - acp_thread::selection_name(Some(file_path.as_ref()), &line_range); - - let mention_uri = MentionUri::Selection { - abs_path: Some(file_path.clone()), - line_range: line_range.clone(), - }; - - let mention_text = mention_uri.as_link().to_string(); - let (excerpt_id, text_anchor, content_len) = - self.editor.update(cx, |editor, cx| { - let buffer = editor.buffer().read(cx); - let snapshot = buffer.snapshot(cx); - let (excerpt_id, _, buffer_snapshot) = - snapshot.as_singleton().unwrap(); - let text_anchor = insertion_target.bias_left(&buffer_snapshot); - - editor.insert(&mention_text, window, cx); - editor.insert(" ", window, cx); - - (*excerpt_id, text_anchor, mention_text.len()) - }); - - let Some((crease_id, tx)) = insert_crease_for_mention( - excerpt_id, - text_anchor, - content_len, - crease_text.into(), - mention_uri.icon_path(cx), - None, - self.editor.clone(), - window, - cx, - ) else { - continue; - }; - drop(tx); - - let mention_task = cx - .spawn({ - let project = project.clone(); - async move |_, cx| { - let project_path = project - .update(cx, |project, cx| { - project.project_path_for_absolute_path(&file_path, cx) - }) - .map_err(|e| e.to_string())? - .ok_or_else(|| "project path not found".to_string())?; - - let buffer = project - .update(cx, |project, cx| { - project.open_buffer(project_path, cx) - }) - .map_err(|e| e.to_string())? - .await - .map_err(|e| e.to_string())?; - - buffer - .update(cx, |buffer, cx| { - let start = Point::new(*line_range.start(), 0) - .min(buffer.max_point()); - let end = Point::new(*line_range.end() + 1, 0) - .min(buffer.max_point()); - let content = - buffer.text_for_range(start..end).collect(); - Mention::Text { - content, - tracked_buffers: vec![cx.entity()], - } - }) - .map_err(|e| e.to_string()) - } - }) - .shared(); - - self.mention_set.update(cx, |mention_set, _cx| { - mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task) - }); - } - } - return; - } - } - - if self.prompt_capabilities.borrow().image - && let Some(task) = - paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx) - { - task.detach(); - } - } - - pub fn insert_dragged_files( - &mut self, - paths: Vec, - added_worktrees: Vec>, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = self.workspace.upgrade() else { - return; - }; - let project = workspace.read(cx).project().clone(); - let path_style = project.read(cx).path_style(cx); - 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) = project.read(cx).entry_for_path(&path, cx) else { - continue; - }; - let Some(worktree) = project.read(cx).worktree_for_id(path.worktree_id, cx) else { - continue; - }; - let abs_path = worktree.read(cx).absolutize(&path.path); - let (file_name, _) = crate::completion_provider::extract_file_name_and_directory( - &path.path, - worktree.read(cx).root_name(), - path_style, - ); - - 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, - ); - }); - let supports_images = self.prompt_capabilities.borrow().image; - tasks.push(self.mention_set.update(cx, |mention_set, cx| { - mention_set.confirm_mention_completion( - file_name, - anchor, - content_len, - uri, - supports_images, - self.editor.clone(), - &workspace, - 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 editor = self.editor.read(cx); - let editor_buffer = editor.buffer().read(cx); - let Some(buffer) = editor_buffer.as_singleton() else { - return; - }; - let cursor_anchor = editor.selections.newest_anchor().head(); - let cursor_offset = cursor_anchor.to_offset(&editor_buffer.snapshot(cx)); - let anchor = buffer.update(cx, |buffer, _cx| { - buffer.anchor_before(cursor_offset.0.min(buffer.len())) - }); - let Some(workspace) = self.workspace.upgrade() else { - return; - }; - let Some(completion) = - PromptCompletionProvider::>::completion_for_action( - PromptContextAction::AddSelections, - anchor..anchor, - self.editor.downgrade(), - self.mention_set.downgrade(), - &workspace, - cx, - ) - else { - return; - }; - - self.editor.update(cx, |message_editor, cx| { - message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx); - message_editor.request_autoscroll(Autoscroll::fit(), 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, - ) { - let Some(workspace) = self.workspace.upgrade() else { - return; - }; - - self.clear(window, cx); - - let path_style = workspace.read(cx).project().read(cx).path_style(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, path_style).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, path_style).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::Link)); - } - } - acp::ContentBlock::Image(acp::ImageContent { - uri, - data, - mime_type, - .. - }) => { - let mention_uri = if let Some(uri) = uri { - MentionUri::parse(&uri, path_style) - } 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, - }), - )); - } - _ => {} - } - } - - 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(MultiBufferOffset(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.update(cx, |mention_set, _cx| { - mention_set.insert_mention( - crease_id, - mention_uri.clone(), - Task::ready(Ok(mention)).shared(), - ) - }); - } - cx.notify(); - } - - pub fn text(&self, cx: &App) -> String { - self.editor.read(cx).text(cx) - } - - pub fn set_placeholder_text( - &mut self, - placeholder: &str, - window: &mut Window, - cx: &mut Context, - ) { - self.editor.update(cx, |editor, cx| { - editor.set_placeholder_text(placeholder, window, 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); - }); - } -} - -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::chat)) - .on_action(cx.listener(Self::chat_with_follow)) - .on_action(cx.listener(Self::cancel)) - .capture_action(cx.listener(Self::paste)) - .flex_1() - .child({ - let settings = ThemeSettings::get_global(cx); - - 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: settings.agent_buffer_font_size(cx).into(), - line_height: relative(settings.buffer_line_height.value()), - ..Default::default() - }; - - EditorElement::new( - &self.editor, - EditorStyle { - background: cx.theme().colors().editor_background, - local_player: cx.theme().players().local(), - text: text_style, - syntax: cx.theme().syntax().clone(), - inlay_hints_style: editor::make_inlay_hints_style(cx), - ..Default::default() - }, - ) - }) - } -} - -pub 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::RefCell, ops::Range, path::Path, rc::Rc, sync::Arc}; - - use acp_thread::MentionUri; - use agent::{HistoryStore, outline}; - use agent_client_protocol as acp; - use assistant_text_thread::TextThreadStore; - use editor::{AnchorRangeExt as _, Editor, EditorMode, MultiBufferOffset}; - use fs::FakeFs; - use futures::StreamExt as _; - use gpui::{ - AppContext, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext, - }; - use language_model::LanguageModelRegistry; - 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, paths::PathStyle, rel_path::rel_path}; - 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 text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - - let message_editor = cx.update(|window, cx| { - cx.new(|cx| { - MessageEditor::new( - workspace.downgrade(), - project.downgrade(), - history_store.clone(), - None, - Default::default(), - Default::default(), - "Test Agent".into(), - "Test", - EditorMode::AutoHeight { - min_lines: 1, - max_lines: None, - }, - window, - cx, - ) - }) - }); - let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone()); - - cx.run_until_parked(); - - let excerpt_id = editor.update(cx, |editor, cx| { - editor - .buffer() - .read(cx) - .excerpt_ids() - .into_iter() - .next() - .unwrap() - }); - let completions = editor.update_in(cx, |editor, window, cx| { - editor.set_text("Hello @file ", window, cx); - let buffer = editor.buffer().read(cx).as_singleton().unwrap(); - let completion_provider = editor.completion_provider().unwrap(); - completion_provider.completions( - excerpt_id, - &buffer, - text::Anchor::MAX, - CompletionContext { - trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER, - trigger_character: Some("@".into()), - }, - window, - cx, - ) - }); - let [_, completion]: [_; 2] = completions - .await - .unwrap() - .into_iter() - .flat_map(|response| response.completions) - .collect::>() - .try_into() - .unwrap(); - - editor.update_in(cx, |editor, window, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let range = snapshot - .anchor_range_in_excerpt(excerpt_id, completion.replace_range) - .unwrap(); - editor.edit([(range, 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(false, cx)) - .await - .unwrap(); - - // We don't send a resource link for the deleted crease. - pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]); - } - - #[gpui::test] - async fn test_slash_command_validation(cx: &mut gpui::TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/test", - json!({ - ".zed": { - "tasks.json": r#"[{"label": "test", "command": "echo"}]"# - }, - "src": { - "main.rs": "fn main() {}", - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default())); - // Start with no available commands - simulating Claude which doesn't support slash commands - let available_commands = Rc::new(RefCell::new(vec![])); - - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace_handle = workspace.downgrade(); - let message_editor = workspace.update_in(cx, |_, window, cx| { - cx.new(|cx| { - MessageEditor::new( - workspace_handle.clone(), - project.downgrade(), - history_store.clone(), - None, - prompt_capabilities.clone(), - available_commands.clone(), - "Claude Code".into(), - "Test", - EditorMode::AutoHeight { - min_lines: 1, - max_lines: None, - }, - window, - cx, - ) - }) - }); - let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone()); - - // Test that slash commands fail when no available_commands are set (empty list means no commands supported) - editor.update_in(cx, |editor, window, cx| { - editor.set_text("/file test.txt", window, cx); - }); - - let contents_result = message_editor - .update(cx, |message_editor, cx| message_editor.contents(false, cx)) - .await; - - // Should fail because available_commands is empty (no commands supported) - assert!(contents_result.is_err()); - let error_message = contents_result.unwrap_err().to_string(); - assert!(error_message.contains("not supported by Claude Code")); - assert!(error_message.contains("Available commands: none")); - - // Now simulate Claude providing its list of available commands (which doesn't include file) - available_commands.replace(vec![acp::AvailableCommand::new("help", "Get help")]); - - // Test that unsupported slash commands trigger an error when we have a list of available commands - editor.update_in(cx, |editor, window, cx| { - editor.set_text("/file test.txt", window, cx); - }); - - let contents_result = message_editor - .update(cx, |message_editor, cx| message_editor.contents(false, cx)) - .await; - - assert!(contents_result.is_err()); - let error_message = contents_result.unwrap_err().to_string(); - assert!(error_message.contains("not supported by Claude Code")); - assert!(error_message.contains("/file")); - assert!(error_message.contains("Available commands: /help")); - - // Test that supported commands work fine - editor.update_in(cx, |editor, window, cx| { - editor.set_text("/help", window, cx); - }); - - let contents_result = message_editor - .update(cx, |message_editor, cx| message_editor.contents(false, cx)) - .await; - - // Should succeed because /help is in available_commands - assert!(contents_result.is_ok()); - - // Test that regular text works fine - editor.update_in(cx, |editor, window, cx| { - editor.set_text("Hello Claude!", window, cx); - }); - - let (content, _) = message_editor - .update(cx, |message_editor, cx| message_editor.contents(false, cx)) - .await - .unwrap(); - - assert_eq!(content.len(), 1); - if let acp::ContentBlock::Text(text) = &content[0] { - assert_eq!(text.text, "Hello Claude!"); - } else { - panic!("Expected ContentBlock::Text"); - } - - // Test that @ mentions still work - editor.update_in(cx, |editor, window, cx| { - editor.set_text("Check this @", window, cx); - }); - - // The @ mention functionality should not be affected - let (content, _) = message_editor - .update(cx, |message_editor, cx| message_editor.contents(false, cx)) - .await - .unwrap(); - - assert_eq!(content.len(), 1); - if let acp::ContentBlock::Text(text) = &content[0] { - assert_eq!(text.text, "Check this @"); - } else { - panic!("Expected ContentBlock::Text"); - } - } - - struct MessageEditorItem(Entity); - - 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_completion_provider_commands(cx: &mut TestAppContext) { - init_test(cx); - - let app_state = cx.update(AppState::test); - - cx.update(|cx| { - editor::init(cx); - workspace::init(app_state.clone(), cx); - }); - - let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); - - let mut cx = VisualTestContext::from_window(*window, cx); - - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default())); - let available_commands = Rc::new(RefCell::new(vec![ - acp::AvailableCommand::new("quick-math", "2 + 2 = 4 - 1 = 3"), - acp::AvailableCommand::new("say-hello", "Say hello to whoever you want").input( - acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput::new( - "", - )), - ), - ])); - - let editor = workspace.update_in(&mut cx, |workspace, window, cx| { - let workspace_handle = cx.weak_entity(); - let message_editor = cx.new(|cx| { - MessageEditor::new( - workspace_handle, - project.downgrade(), - history_store.clone(), - None, - prompt_capabilities.clone(), - available_commands.clone(), - "Test Agent".into(), - "Test", - EditorMode::AutoHeight { - max_lines: None, - min_lines: 1, - }, - window, - cx, - ) - }); - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item( - Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))), - true, - true, - None, - window, - cx, - ); - }); - message_editor.read(cx).focus_handle(cx).focus(window); - message_editor.read(cx).editor().clone() - }); - - cx.simulate_input("/"); - - editor.update_in(&mut cx, |editor, window, cx| { - assert_eq!(editor.text(cx), "/"); - assert!(editor.has_visible_completions_menu()); - - assert_eq!( - current_completion_labels_with_documentation(editor), - &[ - ("quick-math".into(), "2 + 2 = 4 - 1 = 3".into()), - ("say-hello".into(), "Say hello to whoever you want".into()) - ] - ); - editor.set_text("", window, cx); - }); - - cx.simulate_input("/qui"); - - editor.update_in(&mut cx, |editor, window, cx| { - assert_eq!(editor.text(cx), "/qui"); - assert!(editor.has_visible_completions_menu()); - - assert_eq!( - current_completion_labels_with_documentation(editor), - &[("quick-math".into(), "2 + 2 = 4 - 1 = 3".into())] - ); - editor.set_text("", window, cx); - }); - - editor.update_in(&mut cx, |editor, window, cx| { - assert!(editor.has_visible_completions_menu()); - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - cx.run_until_parked(); - - editor.update_in(&mut cx, |editor, window, cx| { - assert_eq!(editor.display_text(cx), "/quick-math "); - assert!(!editor.has_visible_completions_menu()); - editor.set_text("", window, cx); - }); - - cx.simulate_input("/say"); - - editor.update_in(&mut cx, |editor, _window, cx| { - assert_eq!(editor.display_text(cx), "/say"); - assert!(editor.has_visible_completions_menu()); - - assert_eq!( - current_completion_labels_with_documentation(editor), - &[("say-hello".into(), "Say hello to whoever you want".into())] - ); - }); - - editor.update_in(&mut cx, |editor, window, cx| { - assert!(editor.has_visible_completions_menu()); - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - cx.run_until_parked(); - - editor.update_in(&mut cx, |editor, _window, cx| { - assert_eq!(editor.text(cx), "/say-hello "); - assert_eq!(editor.display_text(cx), "/say-hello "); - assert!(!editor.has_visible_completions_menu()); - }); - - cx.simulate_input("GPT5"); - - cx.run_until_parked(); - - editor.update_in(&mut cx, |editor, window, cx| { - assert_eq!(editor.text(cx), "/say-hello GPT5"); - assert_eq!(editor.display_text(cx), "/say-hello GPT5"); - assert!(!editor.has_visible_completions_menu()); - - // Delete argument - for _ in 0..5 { - editor.backspace(&editor::actions::Backspace, window, cx); - } - }); - - cx.run_until_parked(); - - editor.update_in(&mut cx, |editor, window, cx| { - assert_eq!(editor.text(cx), "/say-hello"); - // Hint is visible because argument was deleted - assert_eq!(editor.display_text(cx), "/say-hello "); - - // Delete last command letter - editor.backspace(&editor::actions::Backspace, window, cx); - }); - - cx.run_until_parked(); - - editor.update_in(&mut cx, |editor, _window, cx| { - // Hint goes away once command no longer matches an available one - assert_eq!(editor.text(cx), "/say-hell"); - assert_eq!(editor.display_text(cx), "/say-hell"); - assert!(!editor.has_visible_completions_menu()); - }); - } - - #[gpui::test] - async fn test_context_completion_provider_mentions(cx: &mut TestAppContext) { - init_test(cx); - - let app_state = cx.update(AppState::test); - - cx.update(|cx| { - editor::init(cx); - workspace::init(app_state.clone(), 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![ - rel_path("a/one.txt"), - rel_path("a/two.txt"), - rel_path("a/three.txt"), - rel_path("a/four.txt"), - rel_path("b/five.txt"), - rel_path("b/six.txt"), - rel_path("b/seven.txt"), - rel_path("b/eight.txt"), - ]; - - let slash = PathStyle::local().primary_separator(); - - 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.into(), - }, - None, - false, - window, - cx, - ) - }) - .await - .unwrap(); - opened_editors.push(buffer); - } - - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - let prompt_capabilities = Rc::new(RefCell::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.downgrade(), - history_store.clone(), - None, - prompt_capabilities.clone(), - Default::default(), - "Test Agent".into(), - "Test", - EditorMode::AutoHeight { - max_lines: None, - min_lines: 1, - }, - window, - cx, - ) - }); - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item( - Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))), - true, - true, - None, - window, - cx, - ); - }); - message_editor.read(cx).focus_handle(cx).focus(window); - let editor = message_editor.read(cx).editor().clone(); - (message_editor, editor) - }); - - cx.simulate_input("Lorem @"); - - editor.update_in(&mut cx, |editor, window, cx| { - assert_eq!(editor.text(cx), "Lorem @"); - assert!(editor.has_visible_completions_menu()); - - assert_eq!( - current_completion_labels(editor), - &[ - format!("eight.txt b{slash}"), - format!("seven.txt b{slash}"), - format!("six.txt b{slash}"), - format!("five.txt b{slash}"), - "Files & Directories".into(), - "Symbols".into() - ] - ); - editor.set_text("", window, cx); - }); - - prompt_capabilities.replace( - acp::PromptCapabilities::new() - .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), - &[ - format!("eight.txt b{slash}"), - format!("seven.txt b{slash}"), - format!("six.txt b{slash}"), - format!("five.txt b{slash}"), - "Files & Directories".into(), - "Symbols".into(), - "Threads".into(), - "Fetch".into() - ] - ); - }); - - // 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![format!("one.txt a{slash}")] - ); - }); - - 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 = MentionUri::File { - abs_path: path!("/dir/a/one.txt").into(), - } - .to_uri() - .to_string(); - 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 contents = message_editor - .update(&mut cx, |message_editor, cx| { - message_editor - .mention_set() - .update(cx, |mention_set, cx| mention_set.contents(false, 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, - &MentionUri::parse(&url_one, PathStyle::local()).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() - .update(cx, |mention_set, cx| mention_set.contents(false, cx)) - }) - .await - .unwrap() - .into_values() - .collect::>(); - - let url_eight = MentionUri::File { - abs_path: path!("/dir/b/eight.txt").into(), - } - .to_uri() - .to_string(); - - { - let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else { - panic!("Unexpected mentions"); - }; - pretty_assertions::assert_eq!(content, "8"); - pretty_assertions::assert_eq!( - uri, - &MentionUri::parse(&url_eight, PathStyle::local()).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::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(), - range: lsp::Range::new( - lsp::Position::new(0, 0), - lsp::Position::new(0, 1), - ), - }, - kind: lsp::SymbolKind::CONSTANT, - tags: None, - container_name: None, - deprecated: None, - }, - ]))) - }, - ); - - cx.simulate_input("@symbol "); - - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) @symbol ") - ); - assert!(editor.has_visible_completions_menu()); - assert_eq!(current_completion_labels(editor), &["MySymbol one.txt L1"]); - }); - - editor.update_in(&mut cx, |editor, window, cx| { - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - let symbol = MentionUri::Symbol { - abs_path: path!("/dir/a/one.txt").into(), - name: "MySymbol".into(), - line_range: 0..=0, - }; - - let contents = message_editor - .update(&mut cx, |message_editor, cx| { - message_editor - .mention_set() - .update(cx, |mention_set, cx| mention_set.contents(false, 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, &symbol); - } - - 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]({}) ", - symbol.to_uri(), - ) - ); - }); - - // 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]({}) @file x.png", symbol.to_uri()) - ); - assert!(editor.has_visible_completions_menu()); - assert_eq!(current_completion_labels(editor), &["x.png "]); - }); - - 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() - .update(cx, |mention_set, cx| mention_set.contents(false, 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]({}) ", - symbol.to_uri() - ) - ); - }); - - // 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]({}) @file x.png", symbol.to_uri()) - ); - assert!(editor.has_visible_completions_menu()); - assert_eq!(current_completion_labels(editor), &["x.png "]); - }); - - 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]({}) ", - symbol.to_uri() - ) - ); - }); - - // 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() - .update(cx, |mention_set, cx| mention_set.contents(false, 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(MultiBufferOffset(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::>() - } - - fn current_completion_labels_with_documentation(editor: &Editor) -> Vec<(String, String)> { - let completions = editor.current_completions().expect("Missing completions"); - completions - .into_iter() - .map(|completion| { - ( - completion.label.text, - completion - .documentation - .map(|d| d.text().to_string()) - .unwrap_or_default(), - ) - }) - .collect::>() - } - - #[gpui::test] - async fn test_large_file_mention_fallback(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - // Create a large file that exceeds AUTO_OUTLINE_SIZE - // Using plain text without a configured language, so no outline is available - const LINE: &str = "This is a line of text in the file\n"; - let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len())); - assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE); - - // Create a small file that doesn't exceed AUTO_OUTLINE_SIZE - let small_content = "fn small_function() { /* small */ }\n"; - assert!(small_content.len() < outline::AUTO_OUTLINE_SIZE); - - fs.insert_tree( - "/project", - json!({ - "large_file.txt": large_content.clone(), - "small_file.txt": small_content, - }), - ) - .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 text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - - let message_editor = cx.update(|window, cx| { - cx.new(|cx| { - let editor = MessageEditor::new( - workspace.downgrade(), - project.downgrade(), - history_store.clone(), - None, - Default::default(), - Default::default(), - "Test Agent".into(), - "Test", - EditorMode::AutoHeight { - min_lines: 1, - max_lines: None, - }, - window, - cx, - ); - // Enable embedded context so files are actually included - editor - .prompt_capabilities - .replace(acp::PromptCapabilities::new().embedded_context(true)); - editor - }) - }); - - // Test large file mention - // Get the absolute path using the project's worktree - let large_file_abs_path = project.read_with(cx, |project, cx| { - let worktree = project.worktrees(cx).next().unwrap(); - let worktree_root = worktree.read(cx).abs_path(); - worktree_root.join("large_file.txt") - }); - let large_file_task = message_editor.update(cx, |editor, cx| { - editor.mention_set().update(cx, |set, cx| { - set.confirm_mention_for_file(large_file_abs_path, true, cx) - }) - }); - - let large_file_mention = large_file_task.await.unwrap(); - match large_file_mention { - Mention::Text { content, .. } => { - // Should contain some of the content but not all of it - assert!( - content.contains(LINE), - "Should contain some of the file content" - ); - assert!( - !content.contains(&LINE.repeat(100)), - "Should not contain the full file" - ); - // Should be much smaller than original - assert!( - content.len() < large_content.len() / 10, - "Should be significantly truncated" - ); - } - _ => panic!("Expected Text mention for large file"), - } - - // Test small file mention - // Get the absolute path using the project's worktree - let small_file_abs_path = project.read_with(cx, |project, cx| { - let worktree = project.worktrees(cx).next().unwrap(); - let worktree_root = worktree.read(cx).abs_path(); - worktree_root.join("small_file.txt") - }); - let small_file_task = message_editor.update(cx, |editor, cx| { - editor.mention_set().update(cx, |set, cx| { - set.confirm_mention_for_file(small_file_abs_path, true, cx) - }) - }); - - let small_file_mention = small_file_task.await.unwrap(); - match small_file_mention { - Mention::Text { content, .. } => { - // Should contain the full actual content - assert_eq!(content, small_content); - } - _ => panic!("Expected Text mention for small file"), - } - } - - #[gpui::test] - async fn test_insert_thread_summary(cx: &mut TestAppContext) { - init_test(cx); - cx.update(LanguageModelRegistry::test); - - 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 text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - - // Create a thread metadata to insert as summary - let thread_metadata = agent::DbThreadMetadata { - id: acp::SessionId::new("thread-123"), - title: "Previous Conversation".into(), - updated_at: chrono::Utc::now(), - }; - - let message_editor = cx.update(|window, cx| { - cx.new(|cx| { - let mut editor = MessageEditor::new( - workspace.downgrade(), - project.downgrade(), - history_store.clone(), - None, - Default::default(), - Default::default(), - "Test Agent".into(), - "Test", - EditorMode::AutoHeight { - min_lines: 1, - max_lines: None, - }, - window, - cx, - ); - editor.insert_thread_summary(thread_metadata.clone(), window, cx); - editor - }) - }); - - // Construct expected values for verification - let expected_uri = MentionUri::Thread { - id: thread_metadata.id.clone(), - name: thread_metadata.title.to_string(), - }; - let expected_link = format!("[@{}]({})", thread_metadata.title, expected_uri.to_uri()); - - message_editor.read_with(cx, |editor, cx| { - let text = editor.text(cx); - - assert!( - text.contains(&expected_link), - "Expected editor text to contain thread mention link.\nExpected substring: {}\nActual text: {}", - expected_link, - text - ); - - let mentions = editor.mention_set().read(cx).mentions(); - assert_eq!( - mentions.len(), - 1, - "Expected exactly one mention after inserting thread summary" - ); - - assert!( - mentions.contains(&expected_uri), - "Expected mentions to contain the thread URI" - ); - }); - } - - #[gpui::test] - async fn test_whitespace_trimming(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/project", json!({"file.rs": "fn main() {}"})) - .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 text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - - let message_editor = cx.update(|window, cx| { - cx.new(|cx| { - MessageEditor::new( - workspace.downgrade(), - project.downgrade(), - history_store.clone(), - None, - Default::default(), - Default::default(), - "Test Agent".into(), - "Test", - EditorMode::AutoHeight { - min_lines: 1, - max_lines: None, - }, - window, - cx, - ) - }) - }); - let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone()); - - cx.run_until_parked(); - - editor.update_in(cx, |editor, window, cx| { - editor.set_text(" \u{A0}してhello world ", window, cx); - }); - - let (content, _) = message_editor - .update(cx, |message_editor, cx| message_editor.contents(false, cx)) - .await - .unwrap(); - - assert_eq!(content, vec!["してhello world".into()]); - } - - #[gpui::test] - async fn test_editor_respects_embedded_context_capability(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - let file_content = "fn main() { println!(\"Hello, world!\"); }\n"; - - fs.insert_tree( - "/project", - json!({ - "src": { - "main.rs": file_content, - } - }), - ) - .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 text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - - let (message_editor, editor) = workspace.update_in(cx, |workspace, window, cx| { - let workspace_handle = cx.weak_entity(); - let message_editor = cx.new(|cx| { - MessageEditor::new( - workspace_handle, - project.downgrade(), - history_store.clone(), - None, - Default::default(), - Default::default(), - "Test Agent".into(), - "Test", - EditorMode::AutoHeight { - max_lines: None, - min_lines: 1, - }, - window, - cx, - ) - }); - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item( - Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))), - true, - true, - None, - window, - cx, - ); - }); - message_editor.read(cx).focus_handle(cx).focus(window); - let editor = message_editor.read(cx).editor().clone(); - (message_editor, editor) - }); - - cx.simulate_input("What is in @file main"); - - editor.update_in(cx, |editor, window, cx| { - assert!(editor.has_visible_completions_menu()); - assert_eq!(editor.text(cx), "What is in @file main"); - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - let content = message_editor - .update(cx, |editor, cx| editor.contents(false, cx)) - .await - .unwrap() - .0; - - let main_rs_uri = if cfg!(windows) { - "file:///C:/project/src/main.rs" - } else { - "file:///project/src/main.rs" - }; - - // When embedded context is `false` we should get a resource link - pretty_assertions::assert_eq!( - content, - vec![ - "What is in ".into(), - acp::ContentBlock::ResourceLink(acp::ResourceLink::new("main.rs", main_rs_uri)) - ] - ); - - message_editor.update(cx, |editor, _cx| { - editor - .prompt_capabilities - .replace(acp::PromptCapabilities::new().embedded_context(true)) - }); - - let content = message_editor - .update(cx, |editor, cx| editor.contents(false, cx)) - .await - .unwrap() - .0; - - // When embedded context is `true` we should get a resource - pretty_assertions::assert_eq!( - content, - vec![ - "What is in ".into(), - acp::ContentBlock::Resource(acp::EmbeddedResource::new( - acp::EmbeddedResourceResource::TextResourceContents( - acp::TextResourceContents::new(file_content, main_rs_uri) - ) - )) - ] - ); - } - - #[gpui::test] - async fn test_autoscroll_after_insert_selections(cx: &mut TestAppContext) { - init_test(cx); - - let app_state = cx.update(AppState::test); - - cx.update(|cx| { - editor::init(cx); - workspace::init(app_state.clone(), cx); - }); - - app_state - .fs - .as_fake() - .insert_tree( - path!("/dir"), - json!({ - "test.txt": "line1\nline2\nline3\nline4\nline5\n", - }), - ) - .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); - - // Open a regular editor with the created file, and select a portion of - // the text that will be used for the selections that are meant to be - // inserted in the agent panel. - let editor = workspace - .update_in(&mut cx, |workspace, window, cx| { - workspace.open_path( - ProjectPath { - worktree_id, - path: rel_path("test.txt").into(), - }, - None, - false, - window, - cx, - ) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - editor.update_in(&mut cx, |editor, window, cx| { - editor.change_selections(Default::default(), window, cx, |selections| { - selections.select_ranges([Point::new(0, 0)..Point::new(0, 5)]); - }); - }); - - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - - // Create a new `MessageEditor`. The `EditorMode::full()` has to be used - // to ensure we have a fixed viewport, so we can eventually actually - // place the cursor outside of the visible area. - let message_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.downgrade(), - history_store.clone(), - None, - Default::default(), - Default::default(), - "Test Agent".into(), - "Test", - EditorMode::full(), - 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 - }); - - message_editor.update_in(&mut cx, |message_editor, window, cx| { - message_editor.editor.update(cx, |editor, cx| { - // Update the Agent Panel's Message Editor text to have 100 - // lines, ensuring that the cursor is set at line 90 and that we - // then scroll all the way to the top, so the cursor's position - // remains off screen. - let mut lines = String::new(); - for _ in 1..=100 { - lines.push_str(&"Another line in the agent panel's message editor\n"); - } - editor.set_text(lines.as_str(), window, cx); - editor.change_selections(Default::default(), window, cx, |selections| { - selections.select_ranges([Point::new(90, 0)..Point::new(90, 0)]); - }); - editor.set_scroll_position(gpui::Point::new(0., 0.), window, cx); - }); - }); - - cx.run_until_parked(); - - // Before proceeding, let's assert that the cursor is indeed off screen, - // otherwise the rest of the test doesn't make sense. - message_editor.update_in(&mut cx, |message_editor, window, cx| { - message_editor.editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(window, cx); - let cursor_row = editor.selections.newest::(&snapshot).head().row; - let scroll_top = snapshot.scroll_position().y as u32; - let visible_lines = editor.visible_line_count().unwrap() as u32; - let visible_range = scroll_top..(scroll_top + visible_lines); - - assert!(!visible_range.contains(&cursor_row)); - }) - }); - - // Now let's insert the selection in the Agent Panel's editor and - // confirm that, after the insertion, the cursor is now in the visible - // range. - message_editor.update_in(&mut cx, |message_editor, window, cx| { - message_editor.insert_selections(window, cx); - }); - - cx.run_until_parked(); - - message_editor.update_in(&mut cx, |message_editor, window, cx| { - message_editor.editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(window, cx); - let cursor_row = editor.selections.newest::(&snapshot).head().row; - let scroll_top = snapshot.scroll_position().y as u32; - let visible_lines = editor.visible_line_count().unwrap() as u32; - let visible_range = scroll_top..(scroll_top + visible_lines); - - assert!(visible_range.contains(&cursor_row)); - }) - }); - } -} diff --git a/crates/agent_ui/src/acp/mode_selector.rs b/crates/agent_ui/src/acp/mode_selector.rs deleted file mode 100644 index 1f50ce7432..0000000000 --- a/crates/agent_ui/src/acp/mode_selector.rs +++ /dev/null @@ -1,229 +0,0 @@ -use acp_thread::AgentSessionModes; -use agent_client_protocol as acp; -use agent_servers::AgentServer; -use agent_settings::AgentSettings; -use fs::Fs; -use gpui::{Context, Entity, FocusHandle, WeakEntity, Window, prelude::*}; -use settings::Settings as _; -use std::{rc::Rc, sync::Arc}; -use ui::{ - Button, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, KeyBinding, - PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*, -}; - -use crate::{CycleModeSelector, ToggleProfileSelector, ui::HoldForDefault}; - -pub struct ModeSelector { - connection: Rc, - agent_server: Rc, - menu_handle: PopoverMenuHandle, - focus_handle: FocusHandle, - fs: Arc, - setting_mode: bool, -} - -impl ModeSelector { - pub fn new( - session_modes: Rc, - agent_server: Rc, - fs: Arc, - focus_handle: FocusHandle, - ) -> Self { - Self { - connection: session_modes, - agent_server, - menu_handle: PopoverMenuHandle::default(), - fs, - setting_mode: false, - focus_handle, - } - } - - pub fn menu_handle(&self) -> PopoverMenuHandle { - self.menu_handle.clone() - } - - pub fn cycle_mode(&mut self, _window: &mut Window, cx: &mut Context) { - let all_modes = self.connection.all_modes(); - let current_mode = self.connection.current_mode(); - - let current_index = all_modes - .iter() - .position(|mode| mode.id.0 == current_mode.0) - .unwrap_or(0); - - let next_index = (current_index + 1) % all_modes.len(); - self.set_mode(all_modes[next_index].id.clone(), cx); - } - - pub fn mode(&self) -> acp::SessionModeId { - self.connection.current_mode() - } - - pub fn set_mode(&mut self, mode: acp::SessionModeId, cx: &mut Context) { - let task = self.connection.set_mode(mode, cx); - self.setting_mode = true; - cx.notify(); - - cx.spawn(async move |this: WeakEntity, cx| { - if let Err(err) = task.await { - log::error!("Failed to set session mode: {:?}", err); - } - this.update(cx, |this, cx| { - this.setting_mode = false; - cx.notify(); - }) - .ok(); - }) - .detach(); - } - - fn build_context_menu( - &self, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let weak_self = cx.weak_entity(); - - ContextMenu::build(window, cx, move |mut menu, _window, cx| { - let all_modes = self.connection.all_modes(); - let current_mode = self.connection.current_mode(); - let default_mode = self.agent_server.default_mode(cx); - - let settings = AgentSettings::get_global(cx); - let side = match settings.dock { - settings::DockPosition::Left => DocumentationSide::Right, - settings::DockPosition::Bottom | settings::DockPosition::Right => { - DocumentationSide::Left - } - }; - - for mode in all_modes { - let is_selected = &mode.id == ¤t_mode; - let is_default = Some(&mode.id) == default_mode.as_ref(); - let entry = ContextMenuEntry::new(mode.name.clone()) - .toggleable(IconPosition::End, is_selected); - - let entry = if let Some(description) = &mode.description { - entry.documentation_aside(side, DocumentationEdge::Bottom, { - let description = description.clone(); - - move |_| { - v_flex() - .gap_1() - .child(Label::new(description.clone())) - .child(HoldForDefault::new(is_default)) - .into_any_element() - } - }) - } else { - entry - }; - - menu.push_item(entry.handler({ - let mode_id = mode.id.clone(); - let weak_self = weak_self.clone(); - move |window, cx| { - weak_self - .update(cx, |this, cx| { - if window.modifiers().secondary() { - this.agent_server.set_default_mode( - if is_default { - None - } else { - Some(mode_id.clone()) - }, - this.fs.clone(), - cx, - ); - } - - this.set_mode(mode_id.clone(), cx); - }) - .ok(); - } - })); - } - - menu.key_context("ModeSelector") - }) - } -} - -impl Render for ModeSelector { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let current_mode_id = self.connection.current_mode(); - let current_mode_name = self - .connection - .all_modes() - .iter() - .find(|mode| mode.id == current_mode_id) - .map(|mode| mode.name.clone()) - .unwrap_or_else(|| "Unknown".into()); - - let this = cx.weak_entity(); - - let icon = if self.menu_handle.is_deployed() { - IconName::ChevronUp - } else { - IconName::ChevronDown - }; - - let trigger_button = Button::new("mode-selector-trigger", current_mode_name) - .label_size(LabelSize::Small) - .color(Color::Muted) - .icon(icon) - .icon_size(IconSize::XSmall) - .icon_position(IconPosition::End) - .icon_color(Color::Muted) - .disabled(self.setting_mode); - - PopoverMenu::new("mode-selector") - .trigger_with_tooltip( - trigger_button, - Tooltip::element({ - let focus_handle = self.focus_handle.clone(); - move |_window, cx| { - v_flex() - .gap_1() - .child( - h_flex() - .pb_1() - .gap_2() - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .child(Label::new("Cycle Through Modes")) - .child(KeyBinding::for_action_in( - &CycleModeSelector, - &focus_handle, - cx, - )), - ) - .child( - h_flex() - .gap_2() - .justify_between() - .child(Label::new("Toggle Mode Menu")) - .child(KeyBinding::for_action_in( - &ToggleProfileSelector, - &focus_handle, - cx, - )), - ) - .into_any() - } - }), - ) - .anchor(gpui::Corner::BottomRight) - .with_handle(self.menu_handle.clone()) - .offset(gpui::Point { - x: px(0.0), - y: px(-2.0), - }) - .menu(move |window, cx| { - this.update(cx, |this, cx| this.build_context_menu(window, cx)) - .ok() - }) - } -} 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 f9710ad9b3..0000000000 --- a/crates/agent_ui/src/acp/model_selector.rs +++ /dev/null @@ -1,553 +0,0 @@ -use std::{cmp::Reverse, rc::Rc, sync::Arc}; - -use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector}; -use agent_servers::AgentServer; -use anyhow::Result; -use collections::IndexMap; -use fs::Fs; -use futures::FutureExt; -use fuzzy::{StringMatchCandidate, match_strings}; -use gpui::{ - Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Task, WeakEntity, -}; -use ordered_float::OrderedFloat; -use picker::{Picker, PickerDelegate}; -use ui::{ - DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, KeyBinding, ListItem, - ListItemSpacing, prelude::*, -}; -use util::ResultExt; -use zed_actions::agent::OpenSettings; - -use crate::ui::HoldForDefault; - -pub type AcpModelSelector = Picker; - -pub fn acp_model_selector( - selector: Rc, - agent_server: Rc, - fs: Arc, - focus_handle: FocusHandle, - window: &mut Window, - cx: &mut Context, -) -> AcpModelSelector { - let delegate = - AcpModelPickerDelegate::new(selector, agent_server, fs, focus_handle, 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 { - selector: Rc, - agent_server: Rc, - fs: Arc, - filtered_entries: Vec, - models: Option, - selected_index: usize, - selected_description: Option<(usize, SharedString, bool)>, - selected_model: Option, - _refresh_models_task: Task<()>, - focus_handle: FocusHandle, -} - -impl AcpModelPickerDelegate { - fn new( - selector: Rc, - agent_server: Rc, - fs: Arc, - focus_handle: FocusHandle, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let rx = selector.watch(cx); - let refresh_models_task = { - cx.spawn_in(window, { - async move |this, cx| { - async fn refresh( - this: &WeakEntity>, - 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(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.refresh(window, cx) - }) - } - - refresh(&this, cx).await.log_err(); - if let Some(mut rx) = rx { - while let Ok(()) = rx.recv().await { - refresh(&this, cx).await.log_err(); - } - } - } - }) - }; - - Self { - selector, - agent_server, - fs, - filtered_entries: Vec::new(), - models: None, - selected_model: None, - selected_index: 0, - selected_description: None, - _refresh_models_task: refresh_models_task, - focus_handle, - } - } - - 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) - { - if window.modifiers().secondary() { - let default_model = self.agent_server.default_model(cx); - let is_default = default_model.as_ref() == Some(&model_info.id); - - self.agent_server.set_default_model( - if is_default { - None - } else { - Some(model_info.id.clone()) - }, - self.fs.clone(), - cx, - ); - } - - self.selector - .select_model(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, window: &mut Window, cx: &mut Context>) { - cx.defer_in(window, |picker, window, cx| { - picker.set_query("", window, cx); - }); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - cx: &mut Context>, - ) -> 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 default_model = self.agent_server.default_model(cx); - let is_default = default_model.as_ref() == Some(&model_info.id); - - let model_icon_color = if is_selected { - Color::Accent - } else { - Color::Muted - }; - - Some( - div() - .id(("model-picker-menu-child", ix)) - .when_some(model_info.description.clone(), |this, description| { - this - .on_hover(cx.listener(move |menu, hovered, _, cx| { - if *hovered { - menu.delegate.selected_description = Some((ix, description.clone(), is_default)); - } else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix) { - menu.delegate.selected_description = None; - } - cx.notify(); - })) - }) - .child( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child( - h_flex() - .w_full() - .gap_1p5() - .when_some(model_info.icon, |this, icon| { - this.child( - Icon::new(icon) - .color(model_icon_color) - .size(IconSize::Small) - ) - }) - .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 documentation_aside( - &self, - _window: &mut Window, - _cx: &mut Context>, - ) -> Option { - self.selected_description - .as_ref() - .map(|(_, description, is_default)| { - let description = description.clone(); - let is_default = *is_default; - - DocumentationAside::new( - DocumentationSide::Left, - DocumentationEdge::Top, - Rc::new(move |_| { - v_flex() - .gap_1() - .child(Label::new(description.clone())) - .child(HoldForDefault::new(is_default)) - .into_any_element() - }), - ) - }) - } - - fn render_footer( - &self, - _window: &mut Window, - cx: &mut Context>, - ) -> Option { - let focus_handle = self.focus_handle.clone(); - - if !self.selector.should_render_footer() { - return None; - } - - Some( - h_flex() - .w_full() - .p_1p5() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child( - Button::new("configure", "Configure") - .full_width() - .style(ButtonStyle::Outlined) - .key_binding( - KeyBinding::for_action_in(&OpenSettings, &focus_handle, cx) - .map(|kb| kb.size(rems_from_px(12.))), - ) - .on_click(|_, window, cx| { - window.dispatch_action(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 agent_client_protocol as acp; - 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::ModelId::new(model.to_string()), - name: model.to_string().into(), - description: None, - 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 e2393c11bd..0000000000 --- a/crates/agent_ui/src/acp/model_selector_popover.rs +++ /dev/null @@ -1,100 +0,0 @@ -use std::rc::Rc; -use std::sync::Arc; - -use acp_thread::{AgentModelInfo, AgentModelSelector}; -use agent_servers::AgentServer; -use fs::Fs; -use gpui::{Entity, FocusHandle}; -use picker::popover_menu::PickerPopoverMenu; -use ui::{ - ButtonLike, Context, IntoElement, PopoverMenuHandle, SharedString, TintColor, 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( - selector: Rc, - agent_server: Rc, - fs: Arc, - menu_handle: PopoverMenuHandle, - focus_handle: FocusHandle, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let focus_handle_clone = focus_handle.clone(); - Self { - selector: cx.new(move |cx| { - acp_model_selector( - selector, - agent_server, - fs, - focus_handle_clone.clone(), - window, - cx, - ) - }), - menu_handle, - focus_handle, - } - } - - pub fn toggle(&self, window: &mut Window, cx: &mut Context) { - self.menu_handle.toggle(window, cx); - } - - pub fn active_model<'a>(&self, cx: &'a App) -> Option<&'a AgentModelInfo> { - self.selector.read(cx).delegate.active_model() - } -} - -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(); - - let (color, icon) = if self.menu_handle.is_deployed() { - (Color::Accent, IconName::ChevronUp) - } else { - (Color::Muted, IconName::ChevronDown) - }; - - PickerPopoverMenu::new( - self.selector.clone(), - ButtonLike::new("active-model") - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .when_some(model_icon, |this, icon| { - this.child(Icon::new(icon).color(color).size(IconSize::XSmall)) - }) - .child( - Label::new(model_name) - .color(color) - .size(LabelSize::Small) - .ml_0p5(), - ) - .child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall)), - move |_window, cx| { - Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, 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 1aa89b35d3..0000000000 --- a/crates/agent_ui/src/acp/thread_history.rs +++ /dev/null @@ -1,861 +0,0 @@ -use crate::acp::AcpThreadView; -use crate::{AgentPanel, RemoveHistory, RemoveSelectedThread}; -use agent::{HistoryEntry, HistoryStore}; -use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; -use editor::{Editor, EditorEvent}; -use fuzzy::StringMatchCandidate; -use gpui::{ - App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, 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, Tab, Tooltip, WithScrollbar, - 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, - local_timezone: UtcOffset, - confirming_delete_history: bool, - _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...", window, cx); - editor - }); - - let search_editor_subscription = - cx.subscribe(&search_editor, |this, search_editor, event, cx| { - if let EditorEvent::BufferEdited = event { - let query = search_editor.read(cx).text(cx); - if this.search_query != query { - this.search_query = query.into(); - this.update_visible_items(false, cx); - } - } - }); - - let history_store_subscription = cx.observe(&history_store, |this, _, cx| { - this.update_visible_items(true, cx); - }); - - let scroll_handle = UniformListScrollHandle::default(); - - let mut this = Self { - history_store, - scroll_handle, - selected_index: 0, - hovered_index: None, - visible_items: Default::default(), - search_editor, - local_timezone: UtcOffset::from_whole_seconds( - chrono::Local::now().offset().local_minus_utc(), - ) - .unwrap(), - search_query: SharedString::default(), - confirming_delete_history: false, - _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(text_thread) => self.history_store.update(cx, |this, cx| { - this.delete_text_thread(text_thread.path.clone(), cx) - }), - }; - task.detach_and_log_err(cx); - } - - fn remove_history(&mut self, _window: &mut Window, cx: &mut Context) { - self.history_store.update(cx, |store, cx| { - store.delete_threads(cx).detach_and_log_err(cx) - }); - self.confirming_delete_history = false; - cx.notify(); - } - - fn prompt_delete_history(&mut self, _window: &mut Window, cx: &mut Context) { - self.confirming_delete_history = true; - cx.notify(); - } - - fn cancel_delete_history(&mut self, _window: &mut Window, cx: &mut Context) { - self.confirming_delete_history = false; - cx.notify(); - } - - 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, cx) - }) - .on_click(cx.listener(move |this, _, _, cx| { - this.remove_thread(ix, cx); - cx.stop_propagation() - })), - ) - } 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 { - let has_no_history = self.history_store.read(cx).is_empty(cx); - - 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)) - .on_action(cx.listener(Self::select_last)) - .on_action(cx.listener(Self::confirm)) - .on_action(cx.listener(Self::remove_selected_thread)) - .on_action(cx.listener(|this, _: &RemoveHistory, window, cx| { - this.remove_history(window, cx); - })) - .child( - h_flex() - .h(Tab::container_height(cx)) - .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 has_no_history { - view.justify_center().items_center().child( - Label::new("You don't have any past threads yet.") - .size(LabelSize::Small) - .color(Color::Muted), - ) - } else if self.search_produced_no_matches() { - view.justify_center() - .items_center() - .child(Label::new("No threads match your search.").size(LabelSize::Small)) - } else { - view.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() - .pr_4() - .track_scroll(&self.scroll_handle) - .flex_grow(), - ) - .vertical_scrollbar_for(&self.scroll_handle, window, cx) - } - }) - .when(!has_no_history, |this| { - this.child( - h_flex() - .p_2() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .when(!self.confirming_delete_history, |this| { - this.child( - Button::new("delete_history", "Delete All History") - .full_width() - .style(ButtonStyle::Outlined) - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - this.prompt_delete_history(window, cx); - })), - ) - }) - .when(self.confirming_delete_history, |this| { - this.w_full() - .gap_2() - .flex_wrap() - .justify_between() - .child( - h_flex() - .flex_wrap() - .gap_1() - .child( - Label::new("Delete all threads?") - .size(LabelSize::Small), - ) - .child( - Label::new("You won't be able to recover them later.") - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - .child( - h_flex() - .gap_1() - .child( - Button::new("cancel_delete", "Cancel") - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - this.cancel_delete_history(window, cx); - })), - ) - .child( - Button::new("confirm_delete", "Delete") - .style(ButtonStyle::Tinted(ui::TintColor::Error)) - .color(Color::Error) - .label_size(LabelSize::Small) - .on_click(cx.listener(|_, _, window, cx| { - window.dispatch_action( - Box::new(RemoveHistory), - cx, - ); - })), - ), - ) - }), - ) - }) - } -} - -#[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, 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(text_thread) => { - if let Some(panel) = workspace.read(cx).panel::(cx) { - panel.update(cx, |panel, cx| { - panel - .open_saved_text_thread( - text_thread.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.to_offset(timezone)), - } - } -} - -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 deleted file mode 100644 index 63ea9eb279..0000000000 --- a/crates/agent_ui/src/acp/thread_view.rs +++ /dev/null @@ -1,7296 +0,0 @@ -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, ActionLogTelemetry}; -use agent::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer}; -use agent_client_protocol::{self as acp, PromptCapabilities}; -use agent_servers::{AgentServer, AgentServerDelegate}; -use agent_settings::{AgentProfileId, AgentSettings, CompletionMode}; -use anyhow::{Result, anyhow}; -use arrayvec::ArrayVec; -use audio::{Audio, Sound}; -use buffer_diff::BufferDiff; -use client::zed_urls; -use cloud_llm_client::PlanV1; -use collections::{HashMap, HashSet}; -use editor::scroll::Autoscroll; -use editor::{ - Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects, SizingBehavior, -}; -use file_icons::FileIcons; -use fs::Fs; -use futures::FutureExt as _; -use gpui::{ - Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, - CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, - ListOffset, ListState, PlatformDisplay, SharedString, StyleRefinement, Subscription, Task, - TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, Window, WindowHandle, div, - ease_in_out, linear_color_stop, linear_gradient, list, point, pulsating_between, -}; -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::{NotifyWhenAgentWaiting, Settings as _, SettingsStore}; -use std::cell::RefCell; -use std::path::Path; -use std::sync::Arc; -use std::time::Instant; -use std::{collections::BTreeMap, rc::Rc, time::Duration}; -use terminal_view::terminal_panel::TerminalPanel; -use text::Anchor; -use theme::{AgentFontSize, ThemeSettings}; -use ui::{ - Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, - PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar, prelude::*, -}; -use util::{ResultExt, size::format_file_size, time::duration_alt_display}; -use workspace::{CollaboratorId, NewTerminal, Workspace}; -use zed_actions::agent::{Chat, ToggleModelSelector}; -use zed_actions::assistant::OpenRulesLibrary; - -use super::entry_view_state::EntryViewState; -use crate::acp::AcpModelSelectorPopover; -use crate::acp::ModeSelector; -use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent}; -use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; -use crate::agent_diff::AgentDiff; -use crate::profile_selector::{ProfileProvider, ProfileSelector}; - -use crate::ui::{ - AgentNotification, AgentNotificationEvent, BurnModeTooltip, UnavailableEditingTooltip, - UsageCallout, -}; -use crate::{ - AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, ContinueThread, ContinueWithBurnMode, - CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, NewThread, OpenAgentDiff, OpenHistory, - RejectAll, RejectOnce, ToggleBurnMode, ToggleProfileSelector, -}; - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -enum ThreadFeedback { - Positive, - Negative, -} - -#[derive(Debug)] -enum ThreadError { - PaymentRequired, - ModelRequestLimitReached(cloud_llm_client::Plan), - ToolUseLimitReached, - Refusal, - AuthenticationRequired(SharedString), - Other(SharedString), -} - -impl ThreadError { - fn from_err(error: anyhow::Error, agent: &Rc) -> 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 if let Some(acp_error) = error.downcast_ref::() - && acp_error.code == acp::ErrorCode::AuthRequired - { - Self::AuthenticationRequired(acp_error.message.clone().into()) - } else { - let string = format!("{:#}", error); - // 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(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| { - // Apply the profile and let the thread swap to its default model. - thread.set_profile(profile_id, cx); - }); - } - - 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_telemetry_id = thread.read(cx).connection().telemetry_id(); - 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", - agent = agent_telemetry_id, - session_id = session_id, - rating = rating, - 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_telemetry_id = thread.read(cx).connection().telemetry_id(); - let task = telemetry.thread_data(&session_id, cx); - cx.background_spawn(async move { - let thread = task.await?; - telemetry::event!( - "Agent Thread Feedback Comments", - agent = agent_telemetry_id, - session_id = session_id, - comments = comments, - 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.", - window, - cx, - ); - editor - }); - - editor.read(cx).focus_handle(cx).focus(window); - editor - } -} - -pub struct AcpThreadView { - agent: Rc, - workspace: WeakEntity, - project: Entity, - thread_state: ThreadState, - login: Option, - 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_error_markdown: Option>, - thread_feedback: ThreadFeedbackState, - list_state: ListState, - 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>, - available_commands: Rc>>, - is_loading_contents: bool, - new_server_version_available: Option, - resume_thread_metadata: Option, - _cancel_task: Option>, - _subscriptions: [Subscription; 5], - show_codex_windows_warning: bool, - in_flight_prompt: Option>, -} - -enum ThreadState { - Loading(Entity), - Ready { - thread: Entity, - title_editor: Option>, - mode_selector: Option>, - _subscriptions: Vec, - }, - LoadError(LoadError), - Unauthenticated { - connection: Rc, - description: Option>, - configuration_view: Option, - pending_auth_method: Option, - _subscription: Option, - }, -} - -struct LoadingView { - title: SharedString, - _load_task: Task<()>, - _update_title_task: Task>, -} - -impl AcpThreadView { - pub fn new( - agent: Rc, - resume_thread: Option, - summarize_thread: Option, - workspace: WeakEntity, - project: Entity, - history_store: Entity, - prompt_store: Option>, - track_load_event: bool, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default())); - let available_commands = Rc::new(RefCell::new(vec![])); - - let placeholder = placeholder_text(agent.name().as_ref(), false); - - let message_editor = cx.new(|cx| { - let mut editor = MessageEditor::new( - workspace.clone(), - project.downgrade(), - history_store.clone(), - prompt_store.clone(), - prompt_capabilities.clone(), - available_commands.clone(), - agent.name(), - &placeholder, - editor::EditorMode::AutoHeight { - min_lines: AgentSettings::get_global(cx).message_editor_min_lines, - max_lines: Some(AgentSettings::get_global(cx).set_message_editor_max_lines()), - }, - window, - cx, - ); - if let Some(entry) = summarize_thread { - editor.insert_thread_summary(entry, window, cx); - } - editor - }); - - let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0)); - - let entry_view_state = cx.new(|_| { - EntryViewState::new( - workspace.clone(), - project.downgrade(), - history_store.clone(), - prompt_store.clone(), - prompt_capabilities.clone(), - available_commands.clone(), - agent.name(), - ) - }); - - let agent_server_store = project.read(cx).agent_server_store().clone(); - let subscriptions = [ - cx.observe_global_in::(window, Self::agent_ui_font_size_changed), - cx.observe_global_in::(window, Self::agent_ui_font_size_changed), - cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event), - cx.subscribe_in(&entry_view_state, window, Self::handle_entry_view_event), - cx.subscribe_in( - &agent_server_store, - window, - Self::handle_agent_servers_updated, - ), - ]; - - let show_codex_windows_warning = cfg!(windows) - && project.read(cx).is_local() - && agent.clone().downcast::().is_some(); - - Self { - agent: agent.clone(), - workspace: workspace.clone(), - project: project.clone(), - entry_view_state, - thread_state: Self::initial_state( - agent.clone(), - resume_thread.clone(), - workspace.clone(), - project.clone(), - track_load_event, - window, - cx, - ), - login: None, - message_editor, - model_selector: None, - profile_selector: None, - - notifications: Vec::new(), - notification_subscriptions: HashMap::default(), - list_state: list_state, - thread_retry_status: None, - thread_error: None, - thread_error_markdown: None, - thread_feedback: Default::default(), - auth_task: None, - expanded_tool_calls: HashSet::default(), - expanded_thinking_blocks: HashSet::default(), - editing_message: None, - edits_expanded: false, - plan_expanded: false, - prompt_capabilities, - available_commands, - editor_expanded: false, - should_be_following: false, - history_store, - hovered_recent_history_item: None, - is_loading_contents: false, - _subscriptions: subscriptions, - _cancel_task: None, - focus_handle: cx.focus_handle(), - new_server_version_available: None, - resume_thread_metadata: resume_thread, - show_codex_windows_warning, - in_flight_prompt: None, - } - } - - fn reset(&mut self, window: &mut Window, cx: &mut Context) { - self.thread_state = Self::initial_state( - self.agent.clone(), - self.resume_thread_metadata.clone(), - self.workspace.clone(), - self.project.clone(), - true, - window, - cx, - ); - self.available_commands.replace(vec![]); - self.new_server_version_available.take(); - cx.notify(); - } - - fn initial_state( - agent: Rc, - resume_thread: Option, - workspace: WeakEntity, - project: Entity, - track_load_event: bool, - window: &mut Window, - cx: &mut Context, - ) -> ThreadState { - if project.read(cx).is_via_collab() - && agent.clone().downcast::().is_none() - { - return ThreadState::LoadError(LoadError::Other( - "External agents are not yet supported in shared projects.".into(), - )); - } - let mut worktrees = project.read(cx).visible_worktrees(cx).collect::>(); - // Pick the first non-single-file worktree for the root directory if there are any, - // and otherwise the parent of a single-file worktree, falling back to $HOME if there are no visible worktrees. - worktrees.sort_by(|l, r| { - l.read(cx) - .is_single_file() - .cmp(&r.read(cx).is_single_file()) - }); - let root_dir = worktrees - .into_iter() - .filter_map(|worktree| { - if worktree.read(cx).is_single_file() { - Some(worktree.read(cx).abs_path().parent()?.into()) - } else { - Some(worktree.read(cx).abs_path()) - } - }) - .next(); - let (status_tx, mut status_rx) = watch::channel("Loading…".into()); - let (new_version_available_tx, mut new_version_available_rx) = watch::channel(None); - let delegate = AgentServerDelegate::new( - project.read(cx).agent_server_store().clone(), - project.clone(), - Some(status_tx), - Some(new_version_available_tx), - ); - - let connect_task = agent.connect(root_dir.as_deref(), delegate, cx); - let load_task = cx.spawn_in(window, async move |this, cx| { - let connection = match connect_task.await { - Ok((connection, login)) => { - this.update(cx, |this, _| this.login = login).ok(); - connection - } - 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); - } - cx.notify(); - }) - .log_err(); - return; - } - }; - - if track_load_event { - telemetry::event!("Agent Thread Started", agent = connection.telemetry_id()); - } - - let result = if let Some(native_agent) = connection - .clone() - .downcast::() - && let Some(resume) = resume_thread.clone() - { - cx.update(|_, cx| { - native_agent - .0 - .update(cx, |agent, cx| agent.open_thread(resume.id, cx)) - }) - .log_err() - } else { - let root_dir = root_dir.unwrap_or(paths::home_dir().as_path().into()); - cx.update(|_, cx| { - connection - .clone() - .new_thread(project.clone(), &root_dir, cx) - }) - .log_err() - }; - - let Some(result) = result else { - return; - }; - - let result = match result.await { - Err(e) => match e.downcast::() { - Ok(err) => { - cx.update(|window, cx| { - Self::handle_auth_required(this, err, agent, connection, window, cx) - }) - .log_err(); - return; - } - Err(err) => Err(err), - }, - Ok(thread) => Ok(thread), - }; - - this.update_in(cx, |this, window, cx| { - match result { - Ok(thread) => { - let action_log = thread.read(cx).action_log().clone(); - - this.prompt_capabilities - .replace(thread.read(cx).prompt_capabilities()); - - let count = thread.read(cx).entries().len(); - this.entry_view_state.update(cx, |view_state, cx| { - for ix in 0..count { - view_state.sync_entry(ix, &thread, window, cx); - } - this.list_state.splice_focusable( - 0..0, - (0..count).map(|ix| view_state.entry(ix)?.focus_handle(cx)), - ); - }); - - if let Some(resume) = resume_thread { - this.history_store.update(cx, |history, cx| { - history.push_recently_opened_entry( - HistoryEntryId::AcpThread(resume.id), - cx, - ); - }); - } - - AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx); - - this.model_selector = thread - .read(cx) - .connection() - .model_selector(thread.read(cx).session_id()) - .map(|selector| { - let agent_server = this.agent.clone(); - let fs = this.project.read(cx).fs().clone(); - cx.new(|cx| { - AcpModelSelectorPopover::new( - selector, - agent_server, - fs, - PopoverMenuHandle::default(), - this.focus_handle(cx), - window, - cx, - ) - }) - }); - - let mode_selector = thread - .read(cx) - .connection() - .session_modes(thread.read(cx).session_id(), cx) - .map(|session_modes| { - let fs = this.project.read(cx).fs().clone(); - let focus_handle = this.focus_handle(cx); - cx.new(|_cx| { - ModeSelector::new( - session_modes, - this.agent.clone(), - fs, - focus_handle, - ) - }) - }); - - let mut subscriptions = vec![ - cx.subscribe_in(&thread, window, Self::handle_thread_event), - cx.observe(&action_log, |_, _, cx| cx.notify()), - ]; - - let title_editor = - if thread.update(cx, |thread, cx| thread.can_set_title(cx)) { - let editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_text(thread.read(cx).title(), window, cx); - editor - }); - subscriptions.push(cx.subscribe_in( - &editor, - window, - Self::handle_title_editor_event, - )); - Some(editor) - } else { - None - }; - - this.thread_state = ThreadState::Ready { - thread, - title_editor, - mode_selector, - _subscriptions: subscriptions, - }; - - 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, - ) - }) - }); - - this.message_editor.focus_handle(cx).focus(window); - - cx.notify(); - } - Err(err) => { - this.handle_load_error(err, window, cx); - } - }; - }) - .log_err(); - }); - - cx.spawn(async move |this, cx| { - while let Ok(new_version) = new_version_available_rx.recv().await { - if let Some(new_version) = new_version { - this.update(cx, |this, cx| { - this.new_server_version_available = Some(new_version.into()); - cx.notify(); - }) - .log_err(); - } - } - }) - .detach(); - - let loading_view = cx.new(|cx| { - let update_title_task = cx.spawn(async move |this, cx| { - loop { - let status = status_rx.recv().await?; - this.update(cx, |this: &mut LoadingView, cx| { - this.title = status; - cx.notify(); - })?; - } - }); - - LoadingView { - title: "Loading…".into(), - _load_task: load_task, - _update_title_task: update_title_task, - } - }); - - ThreadState::Loading(loading_view) - } - - fn handle_auth_required( - this: WeakEntity, - 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 - && LanguageModelRegistry::global(cx) - .read(cx) - .provider(&provider_id) - .map_or(false, |provider| provider.is_authenticated(cx)) - { - this.update(cx, |this, cx| { - this.reset(window, cx); - }) - .ok(); - } - } - }); - - let view = registry.read(cx).provider(&provider_id).map(|provider| { - provider.configuration_view( - language_model::ConfigurationViewTargetAgent::Other(agent_name.clone()), - window, - cx, - ) - }); - - (view, Some(sub)) - } else { - (None, None) - }; - - this.update(cx, |this, cx| { - this.thread_state = ThreadState::Unauthenticated { - pending_auth_method: None, - connection, - configuration_view, - description: err - .description - .clone() - .map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))), - _subscription: subscription, - }; - if this.message_editor.focus_handle(cx).is_focused(window) { - this.focus_handle.focus(window) - } - cx.notify(); - }) - .ok(); - } - - fn handle_load_error( - &mut self, - err: anyhow::Error, - window: &mut Window, - 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(format!("{:#}", err).into())) - } - if self.message_editor.focus_handle(cx).is_focused(window) { - self.focus_handle.focus(window) - } - cx.notify(); - } - - fn handle_agent_servers_updated( - &mut self, - _agent_server_store: &Entity, - _event: &project::AgentServersUpdated, - window: &mut Window, - cx: &mut Context, - ) { - // If we're in a LoadError state OR have a thread_error set (which can happen - // when agent.connect() fails during loading), retry loading the thread. - // This handles the case where a thread is restored before authentication completes. - let should_retry = - matches!(&self.thread_state, ThreadState::LoadError(_)) || self.thread_error.is_some(); - - if should_retry { - self.thread_error = None; - self.thread_error_markdown = None; - self.reset(window, cx); - } - } - - 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, - } - } - - pub fn mode_selector(&self) -> Option<&Entity> { - match &self.thread_state { - ThreadState::Ready { mode_selector, .. } => mode_selector.as_ref(), - ThreadState::Unauthenticated { .. } - | ThreadState::Loading { .. } - | ThreadState::LoadError { .. } => None, - } - } - - pub fn title(&self, cx: &App) -> SharedString { - match &self.thread_state { - ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(), - ThreadState::Loading(loading_view) => loading_view.read(cx).title.clone(), - ThreadState::LoadError(error) => match error { - LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(), - LoadError::FailedToInstall(_) => { - format!("Failed to Install {}", self.agent.name()).into() - } - LoadError::Exited { .. } => format!("{} Exited", self.agent.name()).into(), - LoadError::Other(_) => format!("Error Loading {}", self.agent.name()).into(), - }, - } - } - - pub fn title_editor(&self) -> Option> { - 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(); - - if let Some(thread) = self.thread() { - self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx))); - } - } - - pub fn expand_message_editor( - &mut self, - _: &ExpandMessageEditor, - _window: &mut Window, - cx: &mut Context, - ) { - self.set_editor_is_expanded(!self.editor_expanded, cx); - cx.stop_propagation(); - cx.notify(); - } - - 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, - sizing_behavior: SizingBehavior::ExcludeOverscrollMargin, - }, - cx, - ) - } else { - let agent_settings = AgentSettings::get_global(cx); - editor.set_mode( - EditorMode::AutoHeight { - min_lines: agent_settings.message_editor_min_lines, - max_lines: Some(agent_settings.set_message_editor_max_lines()), - }, - cx, - ) - } - }); - 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 }; - - 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::TerminalMovedToBackground(tool_call_id) => { - self.expanded_tool_calls.remove(tool_call_id); - } - 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(); - } - } - } - ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => { - self.regenerate(event.entry_index, editor.clone(), window, cx); - } - ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Cancel) => { - self.cancel_editing(&Default::default(), window, cx); - } - } - } - - pub fn is_loading(&self) -> bool { - matches!(self.thread_state, ThreadState::Loading { .. }) - } - - 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) { - return; - } - - let task = thread.update(cx, |thread, cx| thread.resume(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); - } - }) - }) - .detach(); - } - - fn send(&mut self, window: &mut Window, cx: &mut Context) { - let Some(thread) = self.thread() else { return }; - - 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, - ); - }); - - if thread.read(cx).status() != ThreadStatus::Idle { - self.stop_current_and_send_new_message(window, cx); - return; - } - - let text = self.message_editor.read(cx).text(cx); - let text = text.trim(); - if text == "/login" || text == "/logout" { - let ThreadState::Ready { thread, .. } = &self.thread_state else { - return; - }; - - let connection = thread.read(cx).connection().clone(); - let can_login = !connection.auth_methods().is_empty() || self.login.is_some(); - // Does the agent have a specific logout command? Prefer that in case they need to reset internal state. - let logout_supported = text == "/logout" - && self - .available_commands - .borrow() - .iter() - .any(|command| command.name == "logout"); - if can_login && !logout_supported { - self.message_editor - .update(cx, |editor, cx| editor.clear(window, cx)); - - let this = cx.weak_entity(); - let agent = self.agent.clone(); - window.defer(cx, |window, cx| { - Self::handle_auth_required( - this, - AuthRequired { - description: None, - provider_id: None, - }, - agent, - connection, - window, - cx, - ); - }); - cx.notify(); - return; - } - } - - self.send_impl(self.message_editor.clone(), window, cx) - } - - 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)); - - cx.spawn_in(window, async move |this, cx| { - cancelled.await; - - this.update_in(cx, |this, window, cx| { - this.send_impl(this.message_editor.clone(), window, cx); - }) - .ok(); - }) - .detach(); - } - - fn send_impl( - &mut self, - message_editor: Entity, - window: &mut Window, - cx: &mut Context, - ) { - let full_mention_content = self.as_native_thread(cx).is_some_and(|thread| { - // Include full contents when using minimal profile - let thread = thread.read(cx); - AgentSettings::get_global(cx) - .profiles - .get(thread.profile()) - .is_some_and(|profile| profile.tools.is_empty()) - }); - - let contents = message_editor.update(cx, |message_editor, cx| { - message_editor.contents(full_mention_content, cx) - }); - - self.thread_error.take(); - self.editing_message.take(); - self.thread_feedback.clear(); - - let Some(thread) = self.thread() else { - return; - }; - let session_id = thread.read(cx).session_id().clone(); - let agent_telemetry_id = thread.read(cx).connection().telemetry_id(); - let thread = thread.downgrade(); - if self.should_be_following { - self.workspace - .update(cx, |workspace, cx| { - workspace.follow(CollaboratorId::Agent, window, cx); - }) - .ok(); - } - - self.is_loading_contents = true; - let model_id = self.current_model_id(cx); - let mode_id = self.current_mode_id(cx); - 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.in_flight_prompt = Some(contents.clone()); - 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 turn_start_time = Instant::now(); - 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, - session = session_id, - model = model_id, - mode = mode_id - ); - - thread.send(contents, cx) - })?; - let res = send.await; - let turn_time_ms = turn_start_time.elapsed().as_millis(); - let status = if res.is_ok() { - this.update(cx, |this, _| this.in_flight_prompt.take()).ok(); - "success" - } else { - "failure" - }; - telemetry::event!( - "Agent Turn Completed", - agent = agent_telemetry_id, - session = session_id, - model = model_id, - mode = mode_id, - status, - turn_time_ms, - ); - res - }); - - 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(); - } - - 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( - &mut self, - entry_ix: usize, - message_editor: Entity, - 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; - }; - - cx.spawn_in(window, async move |this, cx| { - // Check if there are any edits from prompts before the one being regenerated. - // - // If there are, we keep/accept them since we're not regenerating the prompt that created them. - // - // If editing the prompt that generated the edits, they are auto-rejected - // through the `rewind` function in the `acp_thread`. - let has_earlier_edits = thread.read_with(cx, |thread, _| { - thread - .entries() - .iter() - .take(entry_ix) - .any(|entry| entry.diffs().next().is_some()) - })?; - - if has_earlier_edits { - thread.update(cx, |thread, cx| { - thread.action_log().update(cx, |action_log, cx| { - action_log.keep_all_edits(None, cx); - }); - })?; - } - - thread - .update(cx, |thread, cx| thread.rewind(user_message_id, cx))? - .await?; - this.update_in(cx, |this, window, cx| { - this.send_impl(message_editor, window, cx); - this.focus_handle(cx).focus(window); - })?; - anyhow::Ok(()) - }) - .detach(); - } - - fn open_edited_buffer( - &mut self, - buffer: &Entity, - window: &mut Window, - cx: &mut Context, - ) { - let Some(thread) = self.thread() else { - return; - }; - - let Some(diff) = - AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err() - else { - return; - }; - - diff.update(cx, |diff, 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; - }; - 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::>(); - - 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); - } - }) - .ok(); - } - - fn handle_thread_error(&mut self, error: anyhow::Error, cx: &mut Context) { - self.thread_error = Some(ThreadError::from_err(error, &self.agent)); - cx.notify(); - } - - fn clear_thread_error(&mut self, cx: &mut Context) { - self.thread_error = None; - self.thread_error_markdown = None; - cx.notify(); - } - - fn handle_thread_event( - &mut self, - thread: &Entity, - event: &AcpThreadEvent, - window: &mut Window, - cx: &mut Context, - ) { - 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_focusable( - index..index, - [view_state - .entry(index) - .and_then(|entry| entry.focus_handle(cx))], - ); - }); - } - AcpThreadEvent::EntryUpdated(index) => { - self.entry_view_state.update(cx, |view_state, cx| { - view_state.sync_entry(*index, thread, window, cx) - }); - } - 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::Refusal => { - self.thread_retry_status.take(); - self.thread_error = Some(ThreadError::Refusal); - let model_or_agent_name = self.current_model_name(cx); - let notification_message = - format!("{} refused to respond to this request", model_or_agent_name); - self.notify_with_sound(¬ification_message, IconName::Warning, 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 - .replace(thread.read(cx).prompt_capabilities()); - } - AcpThreadEvent::TokenUsageUpdated => {} - AcpThreadEvent::AvailableCommandsUpdated(available_commands) => { - let mut available_commands = available_commands.clone(); - - if thread - .read(cx) - .connection() - .auth_methods() - .iter() - .any(|method| method.id.0.as_ref() == "claude-login") - { - available_commands.push(acp::AvailableCommand::new("login", "Authenticate")); - available_commands.push(acp::AvailableCommand::new("logout", "Authenticate")); - } - - let has_commands = !available_commands.is_empty(); - self.available_commands.replace(available_commands); - - let new_placeholder = placeholder_text(self.agent.name().as_ref(), has_commands); - - self.message_editor.update(cx, |editor, cx| { - editor.set_placeholder_text(&new_placeholder, window, cx); - }); - } - AcpThreadEvent::ModeUpdated(_mode) => { - // The connection keeps track of the mode - cx.notify(); - } - } - cx.notify(); - } - - fn authenticate( - &mut self, - method: acp::AuthMethodId, - window: &mut Window, - cx: &mut Context, - ) { - let ThreadState::Unauthenticated { - connection, - pending_auth_method, - configuration_view, - .. - } = &mut self.thread_state - else { - return; - }; - let agent_telemetry_id = connection.telemetry_id(); - - // Check for the experimental "terminal-auth" _meta field - let auth_method = connection.auth_methods().iter().find(|m| m.id == method); - - if let Some(auth_method) = auth_method { - if let Some(meta) = &auth_method.meta { - if let Some(terminal_auth) = meta.get("terminal-auth") { - // Extract terminal auth details from meta - if let (Some(command), Some(label)) = ( - terminal_auth.get("command").and_then(|v| v.as_str()), - terminal_auth.get("label").and_then(|v| v.as_str()), - ) { - let args = terminal_auth - .get("args") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str().map(String::from)) - .collect() - }) - .unwrap_or_default(); - - let env = terminal_auth - .get("env") - .and_then(|v| v.as_object()) - .map(|obj| { - obj.iter() - .filter_map(|(k, v)| { - v.as_str().map(|val| (k.clone(), val.to_string())) - }) - .collect::>() - }) - .unwrap_or_default(); - - // Run SpawnInTerminal in the same dir as the ACP server - let cwd = connection - .clone() - .downcast::() - .map(|acp_conn| acp_conn.root_dir().to_path_buf()); - - // Build SpawnInTerminal from _meta - let login = task::SpawnInTerminal { - id: task::TaskId(format!("external-agent-{}-login", label)), - full_label: label.to_string(), - label: label.to_string(), - command: Some(command.to_string()), - args, - command_label: label.to_string(), - cwd, - env, - use_new_terminal: true, - allow_concurrent_runs: true, - hide: task::HideStrategy::Always, - ..Default::default() - }; - - self.thread_error.take(); - configuration_view.take(); - pending_auth_method.replace(method.clone()); - - if let Some(workspace) = self.workspace.upgrade() { - let project = self.project.clone(); - let authenticate = Self::spawn_external_agent_login( - login, workspace, project, false, true, window, cx, - ); - cx.notify(); - self.auth_task = Some(cx.spawn_in(window, { - async move |this, cx| { - let result = authenticate.await; - - match &result { - Ok(_) => telemetry::event!( - "Authenticate Agent Succeeded", - agent = agent_telemetry_id - ), - Err(_) => { - telemetry::event!( - "Authenticate Agent Failed", - agent = agent_telemetry_id, - ) - } - } - - this.update_in(cx, |this, window, cx| { - if let Err(err) = result { - if let ThreadState::Unauthenticated { - pending_auth_method, - .. - } = &mut this.thread_state - { - pending_auth_method.take(); - } - this.handle_thread_error(err, cx); - } else { - this.reset(window, cx); - } - this.auth_task.take() - }) - .ok(); - } - })); - } - 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, - ); - }); - return; - } - } else if method.0.as_ref() == "anthropic-api-key" { - let registry = LanguageModelRegistry::global(cx); - let provider = registry - .read(cx) - .provider(&language_model::ANTHROPIC_PROVIDER_ID) - .unwrap(); - let this = cx.weak_entity(); - let agent = self.agent.clone(); - let connection = connection.clone(); - window.defer(cx, move |window, cx| { - if !provider.is_authenticated(cx) { - Self::handle_auth_required( - this, - AuthRequired { - description: Some("ANTHROPIC_API_KEY must be set".to_owned()), - provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID), - }, - agent, - connection, - window, - cx, - ); - } else { - this.update(cx, |this, cx| { - this.thread_state = Self::initial_state( - agent, - None, - this.workspace.clone(), - this.project.clone(), - true, - window, - cx, - ) - }) - .ok(); - } - }); - return; - } else if method.0.as_ref() == "vertex-ai" - && std::env::var("GOOGLE_API_KEY").is_err() - && (std::env::var("GOOGLE_CLOUD_PROJECT").is_err() - || (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, - ) - }); - return; - } - - self.thread_error.take(); - configuration_view.take(); - pending_auth_method.replace(method.clone()); - let authenticate = if (method.0.as_ref() == "claude-login" - || method.0.as_ref() == "spawn-gemini-cli") - && let Some(login) = self.login.clone() - { - if let Some(workspace) = self.workspace.upgrade() { - let project = self.project.clone(); - Self::spawn_external_agent_login( - login, workspace, project, false, false, window, cx, - ) - } else { - Task::ready(Ok(())) - } - } else { - connection.authenticate(method, cx) - }; - cx.notify(); - self.auth_task = Some(cx.spawn_in(window, { - async move |this, cx| { - let result = authenticate.await; - - match &result { - Ok(_) => telemetry::event!( - "Authenticate Agent Succeeded", - agent = agent_telemetry_id - ), - Err(_) => { - telemetry::event!("Authenticate Agent Failed", agent = agent_telemetry_id,) - } - } - - this.update_in(cx, |this, window, cx| { - if let Err(err) = result { - if let ThreadState::Unauthenticated { - pending_auth_method, - .. - } = &mut this.thread_state - { - pending_auth_method.take(); - } - this.handle_thread_error(err, cx); - } else { - this.reset(window, cx); - } - this.auth_task.take() - }) - .ok(); - } - })); - } - - fn spawn_external_agent_login( - login: task::SpawnInTerminal, - workspace: Entity, - project: Entity, - previous_attempt: bool, - check_exit_code: bool, - window: &mut Window, - cx: &mut App, - ) -> Task> { - let Some(terminal_panel) = workspace.read(cx).panel::(cx) else { - return Task::ready(Ok(())); - }; - - window.spawn(cx, async move |cx| { - let mut task = login.clone(); - if let Some(cmd) = &task.command { - // Have "node" command use Zed's managed Node runtime by default - if cmd == "node" { - let resolved_node_runtime = project - .update(cx, |project, cx| { - let agent_server_store = project.agent_server_store().clone(); - agent_server_store.update(cx, |store, cx| { - store.node_runtime().map(|node_runtime| { - cx.background_spawn(async move { - node_runtime.binary_path().await - }) - }) - }) - }); - - if let Ok(Some(resolve_task)) = resolved_node_runtime { - if let Ok(node_path) = resolve_task.await { - task.command = Some(node_path.to_string_lossy().to_string()); - } - } - } - } - task.shell = task::Shell::WithArguments { - program: task.command.take().expect("login command should be set"), - args: std::mem::take(&mut task.args), - title_override: None - }; - task.full_label = task.label.clone(); - task.id = task::TaskId(format!("external-agent-{}-login", task.label)); - task.command_label = task.label.clone(); - task.use_new_terminal = true; - task.allow_concurrent_runs = true; - task.hide = task::HideStrategy::Always; - - let terminal = terminal_panel.update_in(cx, |terminal_panel, window, cx| { - terminal_panel.spawn_task(&task, window, cx) - })?; - - let terminal = terminal.await?; - - if check_exit_code { - // For extension-based auth, wait for the process to exit and check exit code - let exit_status = terminal - .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))? - .await; - - match exit_status { - Some(status) if status.success() => { - Ok(()) - } - Some(status) => { - Err(anyhow!("Login command failed with exit code: {:?}", status.code())) - } - None => { - Err(anyhow!("Login command terminated without exit status")) - } - } - } else { - // For hardcoded agents (claude-login, gemini-cli): look for specific output - let mut exit_status = terminal - .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))? - .fuse(); - - let logged_in = cx - .spawn({ - let terminal = terminal.clone(); - async move |cx| { - loop { - cx.background_executor().timer(Duration::from_secs(1)).await; - let content = - terminal.update(cx, |terminal, _cx| terminal.get_content())?; - if content.contains("Login successful") - || content.contains("Type your message") - { - return anyhow::Ok(()); - } - } - } - }) - .fuse(); - futures::pin_mut!(logged_in); - futures::select_biased! { - result = logged_in => { - if let Err(e) = result { - log::error!("{e}"); - return Err(anyhow!("exited before logging in")); - } - } - _ = exit_status => { - if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server())? && login.label.contains("gemini") { - return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, project.clone(), true, false, window, cx))?.await - } - return Err(anyhow!("exited before logging in")); - } - } - terminal.update(cx, |terminal, _| terminal.kill_active_task())?; - Ok(()) - } - }) - } - - fn authorize_tool_call( - &mut self, - 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 { - return; - }; - let agent_telemetry_id = thread.read(cx).connection().telemetry_id(); - - telemetry::event!( - "Agent Tool Call Authorized", - agent = agent_telemetry_id, - session = thread.read(cx).session_id(), - option = option_kind - ); - - 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 restore_checkpoint(&mut self, message_id: &UserMessageId, cx: &mut Context) { - let Some(thread) = self.thread() else { - return; - }; - - thread - .update(cx, |thread, cx| { - thread.restore_checkpoint(message_id.clone(), cx) - }) - .detach_and_log_err(cx); - } - - fn render_entry( - &self, - entry_ix: usize, - total_entries: usize, - entry: &AgentThreadEntry, - window: &mut Window, - cx: &Context, - ) -> AnyElement { - 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(rems_from_px(18.)) - } else if rules_item.is_some() { - this.pt_3() - } else { - this.pt_2() - } - }) - .pb_3() - .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) - .tooltip(Tooltip::text("Restores all files in the project to the content they had at this point in the conversation.")) - .on_click(cx.listener(move |this, _, _window, cx| { - this.restore_checkpoint(&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()), - ) - .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.clone(), 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() - } - AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => { - let is_last = entry_ix + 1 == total_entries; - - let style = default_markdown_style(false, false, window, cx); - let message_body = v_flex() - .w_full() - .gap_3() - .children(chunks.iter().enumerate().filter_map( - |(chunk_ix, chunk)| match chunk { - AssistantMessageChunk::Message { block } => { - block.markdown().map(|md| { - self.render_markdown(md.clone(), style.clone()) - .into_any_element() - }) - } - AssistantMessageChunk::Thought { block } => { - block.markdown().map(|md| { - self.render_thinking_block( - entry_ix, - chunk_ix, - md.clone(), - window, - cx, - ) - .into_any_element() - }) - } - }, - )) - .into_any(); - - v_flex() - .px_5() - .py_1p5() - .when(is_last, |this| this.pb_4()) - .w_full() - .text_ui(cx) - .child(message_body) - .into_any() - } - AgentThreadEntry::ToolCall(tool_call) => { - let has_terminals = tool_call.terminals().next().is_some(); - - 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 needs_confirmation = if let AgentThreadEntry::ToolCall(tool_call) = entry { - matches!( - tool_call.status, - ToolCallStatus::WaitingForConfirmation { .. } - ) - } else { - false - }; - - let Some(thread) = self.thread() else { - return primary; - }; - - let primary = if entry_ix == total_entries - 1 { - v_flex() - .w_full() - .child(primary) - .map(|this| { - if needs_confirmation { - this.child(self.render_generating(true)) - } else { - this.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 - } - } - - fn tool_card_header_bg(&self, cx: &Context) -> Hsla { - cx.theme() - .colors() - .element_background - .blend(cx.theme().colors().editor_foreground.opacity(0.025)) - } - - fn tool_card_border_color(&self, cx: &Context) -> Hsla { - cx.theme().colors().border.opacity(0.8) - } - - fn tool_name_font_size(&self) -> Rems { - rems_from_px(13.) - } - - fn render_thinking_block( - &self, - entry_ix: usize, - chunk_ix: usize, - chunk: Entity, - 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 scroll_handle = self - .entry_view_state - .read(cx) - .entry(entry_ix) - .and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix)); - - let thinking_content = { - div() - .id(("thinking-content", chunk_ix)) - .when_some(scroll_handle, |this, scroll_handle| { - this.track_scroll(&scroll_handle) - }) - .text_ui_sm(cx) - .overflow_hidden() - .child( - self.render_markdown(chunk, default_markdown_style(false, false, window, cx)), - ) - }; - - v_flex() - .gap_1() - .child( - h_flex() - .id(header_id) - .group(&card_header_id) - .relative() - .w_full() - .pr_1() - .justify_between() - .child( - h_flex() - .h(window.line_height() - px(2.)) - .gap_1p5() - .overflow_hidden() - .child( - Icon::new(IconName::ToolThink) - .size(IconSize::Small) - .color(Color::Muted), - ) - .child( - div() - .text_size(self.tool_name_font_size()) - .text_color(cx.theme().colors().text_muted) - .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); - } - cx.notify(); - } - })), - ) - .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(); - } - })), - ) - .when(is_open, |this| { - this.child( - div() - .ml_1p5() - .pl_3p5() - .border_l_1() - .border_color(self.tool_card_border_color(cx)) - .child(thinking_content), - ) - }) - .into_any_element() - } - - fn render_tool_call( - &self, - entry_ix: usize, - tool_call: &ToolCall, - window: &Window, - cx: &Context, - ) -> Div { - let has_location = tool_call.locations.len() == 1; - let card_header_id = SharedString::from("inner-tool-call-header"); - - let failed_or_canceled = match &tool_call.status { - ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true, - _ => false, - }; - - let needs_confirmation = matches!( - tool_call.status, - ToolCallStatus::WaitingForConfirmation { .. } - ); - let is_terminal_tool = matches!(tool_call.kind, acp::ToolKind::Execute); - let is_edit = - matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some(); - - let use_card_layout = needs_confirmation || is_edit || is_terminal_tool; - - let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation; - - let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id); - - let tool_output_display = - if is_open { - match &tool_call.status { - ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex() - .w_full() - .children(tool_call.content.iter().enumerate().map( - |(content_ix, content)| { - div() - .child(self.render_tool_call_content( - entry_ix, - content, - content_ix, - tool_call, - use_card_layout, - window, - cx, - )) - .into_any_element() - }, - )) - .child(self.render_permission_buttons( - tool_call.kind, - options, - entry_ix, - tool_call.id.clone(), - 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().enumerate().map( - |(content_ix, content)| { - div().child(self.render_tool_call_content( - entry_ix, - content, - content_ix, - tool_call, - use_card_layout, - window, - cx, - )) - }, - )) - .into_any(), - ToolCallStatus::Rejected => Empty.into_any(), - } - .into() - } else { - None - }; - - v_flex() - .map(|this| { - if use_card_layout { - this.my_1p5() - .rounded_md() - .border_1() - .border_color(self.tool_card_border_color(cx)) - .bg(cx.theme().colors().editor_background) - .overflow_hidden() - } else { - this.my_1() - } - }) - .map(|this| { - if has_location && !use_card_layout { - this.ml_4() - } else { - this.ml_5() - } - }) - .mr_5() - .map(|this| { - if is_terminal_tool { - this.child( - v_flex() - .p_1p5() - .gap_0p5() - .text_ui_sm(cx) - .bg(self.tool_card_header_bg(cx)) - .child( - Label::new("Run Command") - .buffer_font(cx) - .size(LabelSize::XSmall) - .color(Color::Muted), - ) - .child( - MarkdownElement::new( - tool_call.label.clone(), - terminal_command_markdown_style(window, cx), - ) - .code_block_renderer( - markdown::CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: false, - border: false, - }, - ) - ), - ) - } else { - this.child( - h_flex() - .group(&card_header_id) - .relative() - .w_full() - .gap_1() - .justify_between() - .when(use_card_layout, |this| { - this.p_0p5() - .rounded_t(rems_from_px(5.)) - .bg(self.tool_card_header_bg(cx)) - }) - .child(self.render_tool_call_label( - entry_ix, - tool_call, - is_edit, - use_card_layout, - window, - cx, - )) - .when(is_collapsible || failed_or_canceled, |this| { - this.child( - h_flex() - .px_1() - .gap_px() - .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| { - if is_open { - this.expanded_tool_calls.remove(&id); - } else { - this.expanded_tool_calls.insert(id.clone()); - } - cx.notify(); - } - })), - ) - }) - .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_label( - &self, - entry_ix: usize, - tool_call: &ToolCall, - is_edit: bool, - use_card_layout: bool, - window: &Window, - cx: &Context, - ) -> Div { - let has_location = tool_call.locations.len() == 1; - - let tool_icon = if tool_call.kind == acp::ToolKind::Edit && has_location { - 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::SwitchMode => IconName::ArrowRightLeft, - acp::ToolKind::Other | _ => IconName::ToolHammer, - }) - } - .size(IconSize::Small) - .color(Color::Muted); - - 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., - ), - )) - } - }) - }; - - h_flex() - .relative() - .w_full() - .h(window.line_height() - px(2.)) - .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(rems_from_px(3.)) // Concentric border radius - .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5))) - }) - .overflow_hidden() - .child(tool_icon) - .child(if has_location { - 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(self.render_markdown( - tool_call.label.clone(), - MarkdownStyle { - prevent_mouse_interaction: true, - ..default_markdown_style(false, true, window, cx) - }, - )) - .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(!is_edit, |this| this.child(gradient_overlay)) - } - - fn render_tool_call_content( - &self, - entry_ix: usize, - content: &ToolCallContent, - context_ix: usize, - tool_call: &ToolCall, - card_layout: bool, - 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(), - context_ix, - card_layout, - 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, - context_ix: usize, - card_layout: bool, - window: &Window, - cx: &Context, - ) -> AnyElement { - let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id)); - - v_flex() - .mt_1p5() - .gap_2() - .when(!card_layout, |this| { - this.ml(rems(0.4)) - .px_3p5() - .border_l_1() - .border_color(self.tool_card_border_color(cx)) - }) - .when(card_layout, |this| { - this.px_2().pb_2().when(context_ix > 0, |this| { - this.border_t_1() - .pt_2() - .border_color(self.tool_card_border_color(cx)) - }) - }) - .text_xs() - .text_color(cx.theme().colors().text_muted) - .child(self.render_markdown(markdown, default_markdown_style(false, false, window, cx))) - .when(!card_layout, |this| { - this.child( - IconButton::new(button_id, IconName::ChevronUp) - .full_width() - .style(ButtonStyle::Outlined) - .icon_color(Color::Muted) - .on_click(cx.listener({ - move |this: &mut Self, _, _, cx: &mut Context| { - this.expanded_tool_calls.remove(&tool_call_id); - cx.notify(); - } - })), - ) - }) - .into_any_element() - } - - fn render_resource_link( - &self, - resource_link: &acp::ResourceLink, - 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() - } - } 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() - } - - fn render_permission_buttons( - &self, - kind: acp::ToolKind, - options: &[acp::PermissionOption], - entry_ix: usize, - tool_call_id: acp::ToolCallId, - cx: &Context, - ) -> Div { - let is_first = self.thread().is_some_and(|thread| { - thread - .read(cx) - .first_tool_awaiting_confirmation() - .is_some_and(|call| call.id == tool_call_id) - }); - let mut seen_kinds: ArrayVec = ArrayVec::new(); - - div() - .p_1() - .border_t_1() - .border_color(self.tool_card_border_color(cx)) - .w_full() - .map(|this| { - if kind == acp::ToolKind::SwitchMode { - this.v_flex() - } else { - this.h_flex().justify_end().flex_wrap() - } - }) - .gap_0p5() - .children(options.iter().map(move |option| { - let option_id = SharedString::from(option.option_id.0.clone()); - Button::new((option_id, entry_ix), option.name.clone()) - .map(|this| { - let (this, action) = match option.kind { - acp::PermissionOptionKind::AllowOnce => ( - this.icon(IconName::Check).icon_color(Color::Success), - Some(&AllowOnce as &dyn Action), - ), - acp::PermissionOptionKind::AllowAlways => ( - this.icon(IconName::CheckDouble).icon_color(Color::Success), - Some(&AllowAlways as &dyn Action), - ), - acp::PermissionOptionKind::RejectOnce => ( - this.icon(IconName::Close).icon_color(Color::Error), - Some(&RejectOnce as &dyn Action), - ), - acp::PermissionOptionKind::RejectAlways | _ => { - (this.icon(IconName::Close).icon_color(Color::Error), None) - } - }; - - let Some(action) = action else { - return this; - }; - - if !is_first || seen_kinds.contains(&option.kind) { - return this; - } - - seen_kinds.push(option.kind); - - this.key_binding( - KeyBinding::for_action_in(action, &self.focus_handle, cx) - .map(|kb| kb.size(rems_from_px(10.))), - ) - }) - .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.option_id.clone(); - let option_kind = option.kind; - move |this, _, window, 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 - ); - - v_flex() - .h_full() - .border_t_1() - .border_color(self.tool_card_border_color(cx)) - .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) - } else { - Empty.into_any() - }, - ) - .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.original_content_len > output.content.len()); - 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_some_and(|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| path.display().to_string()) - .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.", - 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_rotate_animation(2) - ) - }) - .when(truncated_output, |header| { - let tooltip = if let Some(output) = output { - if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES { - format!("Output exceeded terminal max lines and was \ - truncated, the model received the first {}.", format_file_size(output.content.len() as u64, true)) - } else { - format!( - "Output is {} long, and to avoid unexpected token usage, \ - only {} was sent back to the agent.", - format_file_size(output.original_content_len as u64, true), - format_file_size(output.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), - ) - }) - .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), - ))) - }), - ) - }) - .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()); - } - } - })), - ); - - 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_1p5() - .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) - .h_full() - .children(terminal_view.map(|terminal_view| { - let element = if terminal_view - .read(cx) - .content_mode(window, cx) - .is_scrollable() - { - div().h_72().child(terminal_view).into_any_element() - } else { - terminal_view.into_any_element() - }; - - div() - .on_action(cx.listener(|_this, _: &NewTerminal, window, cx| { - window.dispatch_action(NewThread.boxed_clone(), cx); - cx.stop_propagation(); - })) - .child(element) - .into_any_element() - })), - ) - }) - .into_any() - } - - fn render_rules_item(&self, cx: &Context) -> Option { - let project_context = self - .as_native_thread(cx)? - .read(cx) - .project_context() - .read(cx); - - 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(), - ) - } - - 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, 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)); - - 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), - 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() - }), - ), - ), - ) - }) - .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(); - - let auth_methods = connection.auth_methods(); - - 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 {}.{}", - self.agent.name(), - if auth_methods.len() > 1 { - " Please choose one of the following options:" - } else { - "" - } - )) - .size(LabelSize::Small) - .color(Color::Muted) - .mb_1() - .ml_5(), - ) - }) - .when_some(pending_auth_method, |el, _| { - el.child( - h_flex() - .py_4() - .w_full() - .justify_center() - .gap_1() - .child( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .color(Color::Muted) - .with_rotate_animation(2), - ) - .child(Label::new("Authenticating…").size(LabelSize::Small)), - ) - }) - .when(!auth_methods.is_empty(), |this| { - this.child( - h_flex() - .justify_end() - .flex_wrap() - .gap_1() - .when(!show_description, |this| { - this.border_t_1() - .mt_1() - .pt_2() - .border_color(cx.theme().colors().border.opacity(0.8)) - }) - .children(connection.auth_methods().iter().enumerate().rev().map( - |(ix, method)| { - let (method_id, name) = if self - .project - .read(cx) - .is_via_remote_server() - && method.id.0.as_ref() == "oauth-personal" - && method.name == "Log in with Google" - { - ("spawn-gemini-cli".into(), "Log in with Gemini CLI".into()) - } else { - (method.id.0.clone(), method.name.clone()) - }; - - let agent_telemetry_id = connection.telemetry_id(); - - Button::new(method_id.clone(), name) - .label_size(LabelSize::Small) - .map(|this| { - if ix == 0 { - this.style(ButtonStyle::Tinted(TintColor::Warning)) - } else { - this.style(ButtonStyle::Outlined) - } - }) - .when_some( - method.description.clone(), - |this, description| { - this.tooltip(Tooltip::text(description)) - }, - ) - .on_click({ - cx.listener(move |this, _, window, cx| { - telemetry::event!( - "Authenticate Agent Started", - agent = agent_telemetry_id, - method = method_id - ); - - this.authenticate( - acp::AuthMethodId::new(method_id.clone()), - window, - cx, - ) - }) - }) - }, - )), - ) - }), - ) - } - - fn render_load_error( - &self, - e: &LoadError, - window: &mut Window, - cx: &mut Context, - ) -> AnyElement { - let (title, message, action_slot): (_, SharedString, _) = match e { - LoadError::Unsupported { - command: path, - current_version, - minimum_version, - } => { - return self.render_unsupported(path, current_version, minimum_version, window, cx); - } - LoadError::FailedToInstall(msg) => ( - "Failed to Install", - msg.into(), - Some(self.create_copy_button(msg.to_string()).into_any_element()), - ), - LoadError::Exited { status } => ( - "Failed to Launch", - format!("Server exited with status {status}").into(), - None, - ), - LoadError::Other(msg) => ( - "Failed to Launch", - msg.into(), - Some(self.create_copy_button(msg.to_string()).into_any_element()), - ), - }; - - Callout::new() - .severity(Severity::Error) - .icon(IconName::XCircleFilled) - .title(title) - .description(message) - .actions_slot(div().children(action_slot)) - .into_any_element() - } - - fn render_unsupported( - &self, - path: &SharedString, - version: &SharedString, - minimum_version: &SharedString, - _window: &mut Window, - cx: &mut Context, - ) -> AnyElement { - let (heading_label, description_label) = ( - format!("Upgrade {} to work with Zed", self.agent.name()), - if version.is_empty() { - format!( - "Currently using {}, which does not report a valid --version", - path, - ) - } else { - format!( - "Currently using {}, which is only version {} (need at least {minimum_version})", - path, version - ) - }, - ); - - 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.), - )) - .child( - v_flex().gap_0p5().child(Label::new(heading_label)).child( - Label::new(description_label) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - .into_any_element() - } - - fn activity_bar_bg(&self, cx: &Context) -> Hsla { - let editor_bg_color = cx.theme().colors().editor_background; - let active_color = cx.theme().colors().element_selected; - editor_bg_color.blend(active_color.opacity(0.3)) - } - - fn render_activity_bar( - &self, - thread_entity: &Entity, - window: &mut Window, - cx: &Context, - ) -> Option { - let thread = thread_entity.read(cx); - let action_log = thread.action_log(); - let telemetry = ActionLogTelemetry::from(thread); - let changed_buffers = action_log.read(cx).changed_buffers(cx); - let plan = thread.plan(); - - if changed_buffers.is_empty() && plan.is_empty() { - return None; - } - - // Temporarily always enable ACP edit controls. This is temporary, to lessen the - // impact of a nasty bug that causes them to sometimes be disabled when they shouldn't - // be, which blocks you from being able to accept or reject edits. This switches the - // bug to be that sometimes it's enabled when it shouldn't be, which at least doesn't - // block you from using the panel. - let pending_edits = false; - - v_flex() - .mt_1() - .mx_2() - .bg(self.activity_bar_bg(cx)) - .border_1() - .border_b_0() - .border_color(cx.theme().colors().border) - .rounded_t_md() - .shadow(vec![gpui::BoxShadow { - color: gpui::black().opacity(0.15), - offset: point(px(1.), px(-1.)), - blur_radius: px(3.), - spread_radius: px(0.), - }]) - .when(!plan.is_empty(), |this| { - this.child(self.render_plan_summary(plan, window, cx)) - .when(self.plan_expanded, |parent| { - 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, - cx, - )) - .when(self.edits_expanded, |parent| { - parent.child(self.render_edited_files( - action_log, - telemetry, - &changed_buffers, - pending_edits, - cx, - )) - }) - }) - .into_any() - .into() - } - - fn render_plan_summary( - &self, - plan: &Plan, - window: &mut Window, - cx: &Context, - ) -> impl IntoElement { - let stats = plan.stats(); - - let title = if let Some(entry) = stats.in_progress_entry - && !self.plan_expanded - { - h_flex() - .cursor_default() - .relative() - .w_full() - .gap_1() - .truncate() - .child( - Label::new("Current:") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( - div() - .text_xs() - .text_color(cx.theme().colors().text_muted) - .line_clamp(1) - .child(MarkdownElement::new( - entry.content.clone(), - plan_label_markdown_style(&entry.status, window, cx), - )), - ) - .when(stats.pending > 0, |this| { - this.child( - h_flex() - .absolute() - .top_0() - .right_0() - .h_full() - .child(div().min_w_8().h_full().bg(linear_gradient( - 90., - linear_color_stop(self.activity_bar_bg(cx), 1.), - linear_color_stop(self.activity_bar_bg(cx).opacity(0.2), 0.), - ))) - .child( - div().pr_0p5().bg(self.activity_bar_bg(cx)).child( - Label::new(format!("{} left", stats.pending)) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ), - ) - }) - } else { - let status_label = if stats.pending == 0 { - "All Done".to_string() - } else if stats.completed == 0 { - format!("{} Tasks", plan.entries.len()) - } else { - format!("{}/{}", stats.completed, plan.entries.len()) - }; - - h_flex() - .w_full() - .gap_1() - .justify_between() - .child( - Label::new("Plan") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( - Label::new(status_label) - .size(LabelSize::Small) - .color(Color::Muted) - .mr_1(), - ) - }; - - h_flex() - .id("plan_summary") - .p_1() - .w_full() - .gap_1() - .when(self.plan_expanded, |this| { - this.border_b_1().border_color(cx.theme().colors().border) - }) - .child(Disclosure::new("plan_disclosure", self.plan_expanded)) - .child(title) - .on_click(cx.listener(|this, _, _, cx| { - this.plan_expanded = !this.plan_expanded; - cx.notify(); - })) - } - - fn render_plan_entries( - &self, - plan: &Plan, - window: &mut Window, - cx: &Context, - ) -> impl IntoElement { - v_flex() - .id("plan_items_list") - .max_h_40() - .overflow_y_scroll() - .children(plan.entries.iter().enumerate().flat_map(|(index, entry)| { - let element = h_flex() - .py_1() - .px_2() - .gap_2() - .justify_between() - .bg(cx.theme().colors().editor_background) - .when(index < plan.entries.len() - 1, |parent| { - parent.border_color(cx.theme().colors().border).border_b_1() - }) - .child( - h_flex() - .id(("plan_entry", index)) - .gap_1p5() - .max_w_full() - .overflow_x_scroll() - .text_xs() - .text_color(cx.theme().colors().text_muted) - .child(match entry.status { - acp::PlanEntryStatus::InProgress => { - Icon::new(IconName::TodoProgress) - .size(IconSize::Small) - .color(Color::Accent) - .with_rotate_animation(2) - .into_any_element() - } - acp::PlanEntryStatus::Completed => { - Icon::new(IconName::TodoComplete) - .size(IconSize::Small) - .color(Color::Success) - .into_any_element() - } - acp::PlanEntryStatus::Pending | _ => { - Icon::new(IconName::TodoPending) - .size(IconSize::Small) - .color(Color::Muted) - .into_any_element() - } - }) - .child(MarkdownElement::new( - entry.content.clone(), - plan_label_markdown_style(&entry.status, window, cx), - )), - ); - - Some(element) - })) - .into_any_element() - } - - fn render_edits_summary( - &self, - changed_buffers: &BTreeMap, Entity>, - expanded: bool, - pending_edits: bool, - cx: &Context, - ) -> Div { - const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete."; - - let focus_handle = self.focus_handle(cx); - - 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() - .gap_1() - .child(Disclosure::new("edits-disclosure", expanded)) - .map(|this| { - if pending_edits { - this.child( - Label::new(format!( - "Editing {} {}…", - changed_buffers.len(), - if changed_buffers.len() == 1 { - "file" - } else { - "files" - } - )) - .color(Color::Muted) - .size(LabelSize::Small) - .with_animation( - "edit-label", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.3, 0.7)), - |label, delta| label.alpha(delta), - ), - ) - } else { - this.child( - Label::new("Edits") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child(Label::new("•").size(LabelSize::XSmall).color(Color::Muted)) - .child( - Label::new(format!( - "{} {}", - changed_buffers.len(), - if changed_buffers.len() == 1 { - "file" - } else { - "files" - } - )) - .size(LabelSize::Small) - .color(Color::Muted), - ) - } - }) - .on_click(cx.listener(|this, _, _, cx| { - this.edits_expanded = !this.edits_expanded; - cx.notify(); - })), - ) - .child( - h_flex() - .gap_1() - .child( - IconButton::new("review-changes", IconName::ListTodo) - .icon_size(IconSize::Small) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |_window, cx| { - Tooltip::for_action_in( - "Review Changes", - &OpenAgentDiff, - &focus_handle, - cx, - ) - } - }) - .on_click(cx.listener(|_, _, window, cx| { - window.dispatch_action(OpenAgentDiff.boxed_clone(), cx); - })), - ) - .child(Divider::vertical().color(DividerColor::Border)) - .child( - Button::new("reject-all-changes", "Reject All") - .label_size(LabelSize::Small) - .disabled(pending_edits) - .when(pending_edits, |this| { - this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL)) - }) - .key_binding( - KeyBinding::for_action_in(&RejectAll, &focus_handle.clone(), cx) - .map(|kb| kb.size(rems_from_px(10.))), - ) - .on_click(cx.listener(move |this, _, window, cx| { - this.reject_all(&RejectAll, window, cx); - })), - ) - .child( - Button::new("keep-all-changes", "Keep All") - .label_size(LabelSize::Small) - .disabled(pending_edits) - .when(pending_edits, |this| { - this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL)) - }) - .key_binding( - KeyBinding::for_action_in(&KeepAll, &focus_handle, cx) - .map(|kb| kb.size(rems_from_px(10.))), - ) - .on_click(cx.listener(move |this, _, window, cx| { - this.keep_all(&KeepAll, window, cx); - })), - ), - ) - } - - fn render_edited_files( - &self, - action_log: &Entity, - telemetry: ActionLogTelemetry, - changed_buffers: &BTreeMap, Entity>, - pending_edits: bool, - cx: &Context, - ) -> impl IntoElement { - let editor_bg_color = cx.theme().colors().editor_background; - - v_flex() - .id("edited_files_list") - .max_h_40() - .overflow_y_scroll() - .children( - changed_buffers - .iter() - .enumerate() - .flat_map(|(index, (buffer, _diff))| { - let file = buffer.read(cx).file()?; - let path = file.path(); - let path_style = file.path_style(cx); - let separator = file.path_style(cx).primary_separator(); - - let file_path = path.parent().and_then(|parent| { - if parent.is_empty() { - None - } else { - Some( - Label::new(format!( - "{}{separator}", - parent.display(path_style) - )) - .color(Color::Muted) - .size(LabelSize::XSmall) - .buffer_font(cx), - ) - } - }); - - let file_name = path.file_name().map(|name| { - Label::new(name.to_string()) - .size(LabelSize::XSmall) - .buffer_font(cx) - .ml_1p5() - }); - - let file_icon = FileIcons::get_icon(path.as_std_path(), cx) - .map(Icon::from_path) - .map(|icon| icon.color(Color::Muted).size(IconSize::Small)) - .unwrap_or_else(|| { - Icon::new(IconName::File) - .color(Color::Muted) - .size(IconSize::Small) - }); - - let overlay_gradient = linear_gradient( - 90., - linear_color_stop(editor_bg_color, 1.), - linear_color_stop(editor_bg_color.opacity(0.2), 0.), - ); - - let element = h_flex() - .group("edited-code") - .id(("file-container", index)) - .py_1() - .pl_2() - .pr_1() - .gap_2() - .justify_between() - .bg(editor_bg_color) - .when(index < changed_buffers.len() - 1, |parent| { - parent.border_color(cx.theme().colors().border).border_b_1() - }) - .child( - h_flex() - .id(("file-name-row", index)) - .relative() - .pr_8() - .w_full() - .overflow_x_scroll() - .child( - h_flex() - .id(("file-name-path", index)) - .cursor_pointer() - .pr_0p5() - .gap_0p5() - .hover(|s| s.bg(cx.theme().colors().element_hover)) - .rounded_xs() - .child(file_icon) - .children(file_name) - .children(file_path) - .tooltip(Tooltip::text("Go to File")) - .on_click({ - let buffer = buffer.clone(); - cx.listener(move |this, _, window, cx| { - this.open_edited_buffer(&buffer, window, cx); - }) - }), - ) - .child( - div() - .absolute() - .h_full() - .w_12() - .top_0() - .bottom_0() - .right_0() - .bg(overlay_gradient), - ), - ) - .child( - h_flex() - .gap_1() - .visible_on_hover("edited-code") - .child( - Button::new("review", "Review") - .label_size(LabelSize::Small) - .on_click({ - let buffer = buffer.clone(); - cx.listener(move |this, _, window, cx| { - this.open_edited_buffer(&buffer, window, cx); - }) - }), - ) - .child(Divider::vertical().color(DividerColor::BorderVariant)) - .child( - Button::new("reject-file", "Reject") - .label_size(LabelSize::Small) - .disabled(pending_edits) - .on_click({ - let buffer = buffer.clone(); - let action_log = action_log.clone(); - let telemetry = telemetry.clone(); - move |_, _, cx| { - action_log.update(cx, |action_log, cx| { - action_log - .reject_edits_in_ranges( - buffer.clone(), - vec![Anchor::min_max_range_for_buffer( - buffer.read(cx).remote_id(), - )], - Some(telemetry.clone()), - cx, - ) - .detach_and_log_err(cx); - }) - } - }), - ) - .child( - Button::new("keep-file", "Keep") - .label_size(LabelSize::Small) - .disabled(pending_edits) - .on_click({ - let buffer = buffer.clone(); - let action_log = action_log.clone(); - let telemetry = telemetry.clone(); - move |_, _, cx| { - action_log.update(cx, |action_log, cx| { - action_log.keep_edits_in_range( - buffer.clone(), - Anchor::min_max_range_for_buffer( - buffer.read(cx).remote_id(), - ), - Some(telemetry.clone()), - cx, - ); - }) - } - }), - ), - ); - - Some(element) - }), - ) - .into_any_element() - } - - fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { - let focus_handle = self.message_editor.focus_handle(cx); - let editor_bg_color = cx.theme().colors().editor_background; - let (expand_icon, expand_tooltip) = if self.editor_expanded { - (IconName::Minimize, "Minimize Message Editor") - } else { - (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::Ready { .. } => true, - ThreadState::Loading { .. } - | 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); - } else if let Some(mode_selector) = this.mode_selector() { - mode_selector.read(cx).menu_handle().toggle(window, cx); - } - })) - .on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| { - if let Some(mode_selector) = this.mode_selector() { - mode_selector.update(cx, |mode_selector, cx| { - mode_selector.cycle_mode(window, cx); - }); - } - })) - .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| { - if let Some(model_selector) = this.model_selector.as_ref() { - model_selector - .update(cx, |model_selector, cx| model_selector.toggle(window, cx)); - } - })) - .p_2() - .gap_2() - .border_t_1() - .border_color(cx.theme().colors().border) - .bg(editor_bg_color) - .when(self.editor_expanded, |this| { - this.h(vh(0.8, window)).size_full().justify_between() - }) - .child( - v_flex() - .relative() - .size_full() - .pt_1() - .pr_2p5() - .child(self.message_editor.clone()) - .child( - h_flex() - .absolute() - .top_0() - .right_0() - .opacity(0.5) - .hover(|this| this.opacity(1.0)) - .child( - IconButton::new("toggle-height", expand_icon) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .tooltip({ - move |_window, cx| { - Tooltip::for_action_in( - expand_tooltip, - &ExpandMessageEditor, - &focus_handle, - cx, - ) - } - }) - .on_click(cx.listener(|this, _, window, cx| { - this.expand_message_editor( - &ExpandMessageEditor, - window, - cx, - ); - })), - ), - ), - ) - .child( - h_flex() - .flex_none() - .flex_wrap() - .justify_between() - .child( - h_flex() - .gap_0p5() - .child(self.render_add_context_button(cx)) - .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.mode_selector().cloned()) - .children(self.model_selector.clone()) - .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 telemetry = ActionLogTelemetry::from(thread.read(cx)); - let action_log = thread.read(cx).action_log().clone(); - action_log.update(cx, |action_log, cx| { - action_log.keep_all_edits(Some(telemetry), cx) - }); - } - - fn reject_all(&mut self, _: &RejectAll, _window: &mut Window, cx: &mut Context) { - let Some(thread) = self.thread() else { - return; - }; - let telemetry = ActionLogTelemetry::from(thread.read(cx)); - let action_log = thread.read(cx).action_log().clone(); - action_log - .update(cx, |action_log, cx| { - action_log.reject_all_edits(Some(telemetry), cx) - }) - .detach(); - } - - fn allow_always(&mut self, _: &AllowAlways, window: &mut Window, cx: &mut Context) { - self.authorize_pending_tool_call(acp::PermissionOptionKind::AllowAlways, window, cx); - } - - fn allow_once(&mut self, _: &AllowOnce, window: &mut Window, cx: &mut Context) { - self.authorize_pending_tool_call(acp::PermissionOptionKind::AllowOnce, window, cx); - } - - fn reject_once(&mut self, _: &RejectOnce, window: &mut Window, cx: &mut Context) { - self.authorize_pending_tool_call(acp::PermissionOptionKind::RejectOnce, window, cx); - } - - fn authorize_pending_tool_call( - &mut self, - kind: acp::PermissionOptionKind, - window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - let thread = self.thread()?.read(cx); - let tool_call = thread.first_tool_awaiting_confirmation()?; - let ToolCallStatus::WaitingForConfirmation { options, .. } = &tool_call.status else { - return None; - }; - let option = options.iter().find(|o| o.kind == kind)?; - - self.authorize_tool_call( - tool_call.id.clone(), - option.option_id.clone(), - option.kind, - window, - cx, - ); - - Some(()) - } - - 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())) - .into_any_element() - } else if is_generating && is_editor_empty { - IconButton::new("stop-generation", IconName::Stop) - .icon_color(Color::Error) - .style(ButtonStyle::Tinted(ui::TintColor::Error)) - .tooltip(move |_window, cx| { - Tooltip::for_action("Stop Generation", &editor::actions::Cancel, 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, cx)) - .on_click(cx.listener(|this, _, window, cx| { - this.send(window, 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()) - } - }; - - IconButton::new("follow-agent", IconName::Crosshair) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .toggle_state(following) - .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor))) - .tooltip(move |_window, cx| { - if following { - Tooltip::for_action(tooltip_label.clone(), &Follow, cx) - } else { - Tooltip::with_meta( - tooltip_label.clone(), - Some(&Follow), - "Track the agent's location as it reads and edits files.", - cx, - ) - } - }) - .on_click(cx.listener(move |this, _, window, cx| { - this.toggle_following(window, cx); - })) - } - - fn render_add_context_button(&self, cx: &mut Context) -> impl IntoElement { - let message_editor = self.message_editor.clone(); - let menu_visible = message_editor.read(cx).is_completions_menu_visible(cx); - - IconButton::new("add-context", IconName::AtSign) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .when(!menu_visible, |this| { - this.tooltip(move |_window, cx| { - Tooltip::with_meta("Add Context", None, "Or type @ to include context", cx) - }) - }) - .on_click(cx.listener(move |_this, _, window, cx| { - let message_editor_clone = message_editor.clone(); - - window.defer(cx, move |window, cx| { - message_editor_clone.update(cx, |message_editor, cx| { - message_editor.trigger_completion_menu(window, cx); - }); - }); - })) - } - - fn render_markdown(&self, markdown: Entity, style: MarkdownStyle) -> MarkdownElement { - let workspace = self.workspace.clone(); - MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| { - Self::open_link(text, &workspace, window, cx); - }) - } - - fn open_link( - url: SharedString, - workspace: &WeakEntity, - window: &mut Window, - cx: &mut App, - ) { - let Some(workspace) = workspace.upgrade() else { - cx.open_url(&url); - return; - }; - - if let Some(mention) = MentionUri::parse(&url, workspace.read(cx).path_style(cx)).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_id) = project.update(cx, |project, cx| { - let path = project.find_project_path(abs_path, cx)?; - project.entry_for_path(&path, cx).map(|entry| entry.id) - }) else { - return; - }; - - 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| project.find_project_path(path, cx)) - 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(()) - }) - .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_text_thread(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); - } - } - - fn open_tool_call_location( - &self, - entry_ix: usize, - location_ix: usize, - window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - let (tool_call_location, agent_location) = self - .thread()? - .read(cx) - .entries() - .get(entry_ix)? - .location(location_ix)?; - - let project_path = self - .project - .read(cx) - .find_project_path(&tool_call_location.path, cx)?; - - let open_task = self - .workspace - .update(cx, |workspace, cx| { - workspace.open_path(project_path, None, true, window, cx) - }) - .log_err()?; - window - .spawn(cx, async move |cx| { - let item = open_task.await?; - - let Some(active_editor) = item.downcast::() else { - return anyhow::Ok(()); - }; - - 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(), agent_location.position); - 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)]); - }) - } - })?; - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - - None - } - - pub fn open_thread_as_markdown( - &self, - workspace: Entity, - window: &mut Window, - cx: &mut App, - ) -> Task> { - let markdown_language_task = workspace - .read(cx) - .app_state() - .languages - .language_for_name("Markdown"); - - let (thread_title, markdown) = if let Some(thread) = self.thread() { - let thread = thread.read(cx); - (thread.title().to_string(), thread.to_markdown(cx)) - } else { - return Task::ready(Ok(())); - }; - - let project = workspace.read(cx).project().clone(); - window.spawn(cx, async move |cx| { - let markdown_language = markdown_language_task.await?; - - let buffer = project - .update(cx, |project, cx| project.create_buffer(false, cx))? - .await?; - - buffer.update(cx, |buffer, cx| { - buffer.set_text(markdown, cx); - buffer.set_language(Some(markdown_language), cx); - buffer.set_capability(language::Capability::ReadWrite, cx); - })?; - - workspace.update_in(cx, |workspace, window, cx| { - let buffer = cx - .new(|cx| MultiBuffer::singleton(buffer, cx).with_title(thread_title.clone())); - - workspace.add_item_to_active_pane( - Box::new(cx.new(|cx| { - let mut editor = - Editor::for_multibuffer(buffer, Some(project.clone()), window, cx); - editor.set_breadcrumb_header(thread_title); - editor - })), - None, - true, - window, - cx, - ); - })?; - anyhow::Ok(()) - }) - } - - fn scroll_to_top(&mut self, cx: &mut Context) { - 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(); - } - } - - 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 !self.notifications.is_empty() { - return; - } - - let settings = AgentSettings::get_global(cx); - - let window_is_inactive = !window.is_window_active(); - let panel_is_hidden = self - .workspace - .upgrade() - .map(|workspace| AgentPanel::is_hidden(&workspace, cx)) - .unwrap_or(true); - - let should_notify = window_is_inactive || panel_is_hidden; - - if !should_notify { - return; - } - - // TODO: Change this once we have title summarization for external agents. - let title = self.agent.name(); - - match settings.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_str().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_generating(&self, confirmation: bool) -> impl IntoElement { - h_flex() - .id("generating-spinner") - .py_2() - .px(rems_from_px(22.)) - .map(|this| { - if confirmation { - this.gap_2() - .child( - h_flex() - .w_2() - .child(SpinnerLabel::sand().size(LabelSize::Small)), - ) - .child( - LoadingLabel::new("Waiting Confirmation") - .size(LabelSize::Small) - .color(Color::Muted), - ) - } else { - this.child(SpinnerLabel::new().size(LabelSize::Small)) - } - }) - .into_any_element() - } - - 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 self.render_generating(false).into_any_element(); - } - - let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(Color::Ignored) - .tooltip(Tooltip::text("Open Thread as Markdown")) - .on_click(cx.listener(move |this, _, window, cx| { - if let Some(workspace) = this.workspace.upgrade() { - this.open_thread_as_markdown(workspace, window, cx) - .detach_and_log_err(cx); - } - })); - - let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUp) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .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() - .w_full() - .py_2() - .px_5() - .gap_px() - .opacity(0.6) - .hover(|s| s.opacity(1.)) - .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; - - let tooltip_meta = || { - SharedString::new( - "Rating the thread sends all of your current conversation to the Zed team.", - ) - }; - - container = container - .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(move |window, cx| match feedback { - Some(ThreadFeedback::Positive) => { - Tooltip::text("Thanks for your feedback!")(window, cx) - } - _ => Tooltip::with_meta("Helpful Response", None, tooltip_meta(), cx), - }) - .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(move |window, cx| match feedback { - Some(ThreadFeedback::Negative) => { - Tooltip::text( - "We appreciate your feedback and will use it to improve in the future.", - )(window, cx) - } - _ => { - Tooltip::with_meta("Not Helpful Response", None, tooltip_meta(), cx) - } - }) - .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) - .into_any_element() - } - - 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_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::V1(PlanV1::ZedFree)); - - let usage = user_store.model_request_usage()?; - - Some( - div() - .child(UsageCallout::new(plan, usage)) - .line_height(line_height), - ) - } - - fn agent_ui_font_size_changed(&mut self, _window: &mut Window, cx: &mut Context) { - self.entry_view_state.update(cx, |entry_view_state, cx| { - entry_view_state.agent_ui_font_size_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); - }) - } - - /// Inserts the selected text into the message editor or the message being - /// edited, if any. - pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context) { - self.active_editor(cx).update(cx, |editor, cx| { - 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_codex_windows_warning(&self, cx: &mut Context) -> Callout { - Callout::new() - .icon(IconName::Warning) - .severity(Severity::Warning) - .title("Codex on Windows") - .description("For best performance, run Codex in Windows Subsystem for Linux (WSL2)") - .actions_slot( - Button::new("open-wsl-modal", "Open in WSL") - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .on_click(cx.listener({ - move |_, _, _window, cx| { - #[cfg(windows)] - _window.dispatch_action( - zed_actions::wsl_actions::OpenWsl::default().boxed_clone(), - cx, - ); - cx.notify(); - } - })), - ) - .dismiss_action( - IconButton::new("dismiss", IconName::Close) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .tooltip(Tooltip::text("Dismiss Warning")) - .on_click(cx.listener({ - move |this, _, _, cx| { - this.show_codex_windows_warning = false; - cx.notify(); - } - })), - ) - } - - fn render_thread_error(&mut 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(), window, cx), - ThreadError::Refusal => self.render_refusal_error(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(cx)?, - }; - - Some(div().child(content)) - } - - fn render_new_version_callout(&self, version: &SharedString, cx: &mut Context) -> Div { - v_flex().w_full().justify_end().child( - h_flex() - .p_2() - .pr_3() - .w_full() - .gap_1p5() - .border_t_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().element_background) - .child( - h_flex() - .flex_1() - .gap_1p5() - .child( - Icon::new(IconName::Download) - .color(Color::Accent) - .size(IconSize::Small), - ) - .child(Label::new("New version available").size(LabelSize::Small)), - ) - .child( - Button::new("update-button", format!("Update to v{}", version)) - .label_size(LabelSize::Small) - .style(ButtonStyle::Tinted(TintColor::Accent)) - .on_click(cx.listener(|this, _, window, cx| { - this.reset(window, cx); - })), - ), - ) - } - - fn current_mode_id(&self, cx: &App) -> Option> { - if let Some(thread) = self.as_native_thread(cx) { - Some(thread.read(cx).profile().0.clone()) - } else if let Some(mode_selector) = self.mode_selector() { - Some(mode_selector.read(cx).mode().0) - } else { - None - } - } - - fn current_model_id(&self, cx: &App) -> Option { - self.model_selector - .as_ref() - .and_then(|selector| selector.read(cx).active_model(cx).map(|m| m.id.to_string())) - } - - fn current_model_name(&self, cx: &App) -> SharedString { - // For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet") - // For ACP agents, use the agent name (e.g., "Claude Code", "Gemini CLI") - // This provides better clarity about what refused the request - if self.as_native_connection(cx).is_some() { - self.model_selector - .as_ref() - .and_then(|selector| selector.read(cx).active_model(cx)) - .map(|model| model.name.clone()) - .unwrap_or_else(|| SharedString::from("The model")) - } else { - // ACP agent - use the agent name (e.g., "Claude Code", "Gemini CLI") - self.agent.name() - } - } - - fn render_refusal_error(&self, cx: &mut Context<'_, Self>) -> Callout { - let model_or_agent_name = self.current_model_name(cx); - let refusal_message = format!( - "{} refused to respond to this prompt. This can happen when a model believes the prompt violates its content policy or safety guidelines, so rephrasing it can sometimes address the issue.", - model_or_agent_name - ); - - Callout::new() - .severity(Severity::Error) - .title("Request Refused") - .icon(IconName::XCircle) - .description(refusal_message.clone()) - .actions_slot(self.create_copy_button(&refusal_message)) - .dismiss_action(self.dismiss_error_button(cx)) - } - - fn render_any_thread_error( - &mut self, - error: SharedString, - window: &mut Window, - 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 - }); - - let markdown = if let Some(markdown) = &self.thread_error_markdown { - markdown.clone() - } else { - let markdown = cx.new(|cx| Markdown::new(error.clone(), None, None, cx)); - self.thread_error_markdown = Some(markdown.clone()); - markdown - }; - - let markdown_style = default_markdown_style(false, true, window, cx); - let description = self - .render_markdown(markdown, markdown_style) - .into_any_element(); - - Callout::new() - .severity(Severity::Error) - .icon(IconName::XCircle) - .title("An Error Happened") - .description_slot(description) - .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( - IconButton::new("retry", IconName::RotateCw) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Retry Generation")) - .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::V1(PlanV1::ZedPro) => { - "Upgrade to usage-based billing for more prompts." - } - cloud_llm_client::Plan::V1(PlanV1::ZedProTrial) - | cloud_llm_client::Plan::V1(PlanV1::ZedFree) => "Upgrade to Zed Pro for more prompts.", - cloud_llm_client::Plan::V2(_) => "", - }; - - 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, 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, - 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, 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) - .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) - .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); - if let Some(message) = this.in_flight_prompt.take() { - this.message_editor.update(cx, |editor, cx| { - editor.set_message(message, window, 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)); - } - })) - } - - 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(text_thread) => { - self.history_store.update(cx, |history, cx| { - history.delete_text_thread(text_thread.path.clone(), cx) - }) - } - }; - task.detach_and_log_err(cx); - } - - /// Returns the currently active editor, either for a message that is being - /// edited or the editor for a new message. - fn active_editor(&self, cx: &App) -> Entity { - if let Some(index) = self.editing_message - && let Some(editor) = self - .entry_view_state - .read(cx) - .entry(index) - .and_then(|e| e.message_editor()) - .cloned() - { - editor - } else { - self.message_editor.clone() - } - } -} - -fn loading_contents_spinner(size: IconSize) -> AnyElement { - Icon::new(IconName::LoadCircle) - .size(size) - .color(Color::Accent) - .with_rotate_animation(3) - .into_any_element() -} - -fn placeholder_text(agent_name: &str, has_commands: bool) -> String { - if agent_name == "Zed Agent" { - format!("Message the {} — @ to include context", agent_name) - } else if has_commands { - format!( - "Message {} — @ to include context, / for commands", - agent_name - ) - } else { - format!("Message {} — @ to include context", agent_name) - } -} - -impl Focusable for AcpThreadView { - fn focus_handle(&self, cx: &App) -> FocusHandle { - match self.thread_state { - ThreadState::Ready { .. } => self.active_editor(cx).focus_handle(cx), - ThreadState::Loading { .. } - | 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::toggle_burn_mode)) - .on_action(cx.listener(Self::keep_all)) - .on_action(cx.listener(Self::reject_all)) - .on_action(cx.listener(Self::allow_always)) - .on_action(cx.listener(Self::allow_once)) - .on_action(cx.listener(Self::reject_once)) - .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, - ) - .into_any(), - ThreadState::Loading { .. } => v_flex() - .flex_1() - .child(self.render_recent_history(cx)) - .into_any(), - ThreadState::LoadError(e) => v_flex() - .flex_1() - .size_full() - .items_center() - .justify_end() - .child(self.render_load_error(e, window, cx)) - .into_any(), - 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(), - ) - .vertical_scrollbar_for(&self.list_state, window, cx) - .into_any() - } else { - this.child(self.render_recent_history(cx)).into_any() - } - }), - }) - // 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, - }) - .children(self.render_thread_retry_status_callout(window, cx)) - .when(self.show_codex_windows_warning, |this| { - this.child(self.render_codex_windows_warning(cx)) - }) - .children(self.render_thread_error(window, cx)) - .when_some( - self.new_server_version_available.as_ref().filter(|_| { - !has_messages || !matches!(self.thread_state, ThreadState::Ready { .. }) - }), - |this, version| this.child(self.render_new_version_callout(&version, 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 { - let theme_settings = ThemeSettings::get_global(cx); - let colors = cx.theme().colors(); - - let buffer_font_size = theme_settings.agent_buffer_font_size(cx); - - let mut text_style = window.text_style(); - let line_height = buffer_font_size * 1.75; - - let font_family = if buffer_font { - theme_settings.buffer_font.family.clone() - } else { - theme_settings.ui_font.family.clone() - }; - - let font_size = if buffer_font { - theme_settings.agent_buffer_font_size(cx) - } else { - theme_settings.agent_ui_font_size(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), - ..Default::default() - }); - - MarkdownStyle { - base_text_style: text_style.clone(), - syntax: cx.theme().syntax().clone(), - selection_background_color: colors.element_selection_background, - code_block_overflow_x_scroll: true, - heading_level_styles: Some(HeadingLevelStyles { - h1: Some(TextStyleRefinement { - font_size: Some(rems(1.15).into()), - ..Default::default() - }), - h2: Some(TextStyleRefinement { - font_size: Some(rems(1.1).into()), - ..Default::default() - }), - h3: Some(TextStyleRefinement { - font_size: Some(rems(1.05).into()), - ..Default::default() - }), - h4: Some(TextStyleRefinement { - font_size: Some(rems(1.).into()), - ..Default::default() - }), - h5: Some(TextStyleRefinement { - font_size: Some(rems(0.95).into()), - ..Default::default() - }), - h6: Some(TextStyleRefinement { - font_size: Some(rems(0.875).into()), - ..Default::default() - }), - }), - code_block: StyleRefinement { - padding: EdgesRefinement { - top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))), - left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))), - right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))), - bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))), - }, - margin: EdgesRefinement { - top: Some(Length::Definite(px(8.).into())), - left: Some(Length::Definite(px(0.).into())), - right: Some(Length::Definite(px(0.).into())), - bottom: Some(Length::Definite(px(12.).into())), - }, - border_style: Some(BorderStyle::Solid), - border_widths: EdgesRefinement { - top: Some(AbsoluteLength::Pixels(px(1.))), - left: Some(AbsoluteLength::Pixels(px(1.))), - right: Some(AbsoluteLength::Pixels(px(1.))), - bottom: Some(AbsoluteLength::Pixels(px(1.))), - }, - border_color: Some(colors.border_variant), - background: Some(colors.editor_background.into()), - text: Some(TextStyleRefinement { - font_family: Some(theme_settings.buffer_font.family.clone()), - font_fallbacks: theme_settings.buffer_font.fallbacks.clone(), - font_features: Some(theme_settings.buffer_font.features.clone()), - font_size: Some(buffer_font_size.into()), - ..Default::default() - }), - ..Default::default() - }, - inline_code: TextStyleRefinement { - font_family: Some(theme_settings.buffer_font.family.clone()), - font_fallbacks: theme_settings.buffer_font.fallbacks.clone(), - font_features: Some(theme_settings.buffer_font.features.clone()), - font_size: Some(buffer_font_size.into()), - background_color: Some(colors.editor_foreground.opacity(0.08)), - ..Default::default() - }, - link: TextStyleRefinement { - background_color: Some(colors.editor_foreground.opacity(0.025)), - color: Some(colors.text_accent), - underline: Some(UnderlineStyle { - color: Some(colors.text_accent.opacity(0.5)), - thickness: px(1.), - ..Default::default() - }), - ..Default::default() - }, - ..Default::default() - } -} - -fn plan_label_markdown_style( - status: &acp::PlanEntryStatus, - window: &Window, - cx: &App, -) -> MarkdownStyle { - let default_md_style = default_markdown_style(false, false, window, cx); - - MarkdownStyle { - base_text_style: TextStyle { - color: cx.theme().colors().text_muted, - strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) { - Some(gpui::StrikethroughStyle { - thickness: px(1.), - color: Some(cx.theme().colors().text_muted.opacity(0.8)), - }) - } else { - None - }, - ..default_md_style.base_text_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_text_thread::TextThreadStore; - use editor::MultiBufferOffset; - use fs::FakeFs; - use gpui::{EventEmitter, 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_refusal_handling(cx: &mut TestAppContext) { - init_test(cx); - - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(RefusalAgentConnection), 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("Do something harmful", window, cx); - }); - - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.run_until_parked(); - - // Check that the refusal error is set - thread_view.read_with(cx, |thread_view, _cx| { - assert!( - matches!(thread_view.thread_error, Some(ThreadError::Refusal)), - "Expected refusal error to be set" - ); - }); - } - - #[gpui::test] - async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) { - init_test(cx); - - let tool_call_id = acp::ToolCallId::new("1"); - let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Label") - .kind(acp::ToolKind::Edit) - .content(vec!["hi".into()]); - let connection = - StubAgentConnection::new().with_permission_requests(HashMap::from_iter([( - tool_call_id, - vec![acp::PermissionOption::new( - "1", - "Allow", - 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()) - ); - } - - #[gpui::test] - async fn test_notification_when_panel_hidden(cx: &mut TestAppContext) { - init_test(cx); - - let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), 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("Hello", window, cx); - }); - - // Window is active (don't deactivate), but panel will be hidden - // Note: In the test environment, the panel is not actually added to the dock, - // so is_agent_panel_hidden will return true - - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.run_until_parked(); - - // Should show notification because window is active but panel is hidden - assert!( - cx.windows() - .iter() - .any(|window| window.downcast::().is_some()), - "Expected notification when panel is hidden" - ); - } - - #[gpui::test] - async fn test_notification_still_works_when_window_inactive(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); - }); - - // Deactivate window - should show notification regardless of setting - cx.deactivate_window(); - - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.run_until_parked(); - - // Should still show notification when window is inactive (existing behavior) - assert!( - cx.windows() - .iter() - .any(|window| window.downcast::().is_some()), - "Expected notification when window is inactive" - ); - } - - #[gpui::test] - async fn test_notification_respects_never_setting(cx: &mut TestAppContext) { - init_test(cx); - - // Set notify_when_agent_waiting to Never - cx.update(|cx| { - AgentSettings::override_global( - AgentSettings { - notify_when_agent_waiting: NotifyWhenAgentWaiting::Never, - ..AgentSettings::get_global(cx).clone() - }, - 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); - }); - - // Window is active - - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.run_until_parked(); - - // Should NOT show notification because notify_when_agent_waiting is Never - assert!( - !cx.windows() - .iter() - .any(|window| window.downcast::().is_some()), - "Expected no notification when notify_when_agent_waiting is Never" - ); - } - - 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 text_thread_store = - cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx))); - let history_store = - cx.update(|_window, cx| cx.new(|cx| HistoryStore::new(text_thread_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, - false, - 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( - acp::ContentChunk::new("Default response".into()), - )]); - Self::new(conn) - } - } - - impl AgentServer for StubAgentServer - where - C: 'static + AgentConnection + Send + Clone, - { - fn logo(&self) -> ui::IconName { - ui::IconName::Ai - } - - fn name(&self) -> SharedString { - "Test".into() - } - - fn connect( - &self, - _root_dir: Option<&Path>, - _delegate: AgentServerDelegate, - _cx: &mut App, - ) -> Task, Option)>> { - Task::ready(Ok((Rc::new(self.connection.clone()), None))) - } - - fn into_any(self: Rc) -> Rc { - self - } - } - - #[derive(Clone)] - struct SaboteurAgentConnection; - - impl AgentConnection for SaboteurAgentConnection { - fn telemetry_id(&self) -> SharedString { - "saboteur".into() - } - - 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::new("test"), - watch::Receiver::constant( - acp::PromptCapabilities::new() - .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 - } - } - - /// Simulates a model which always returns a refusal response - #[derive(Clone)] - struct RefusalAgentConnection; - - impl AgentConnection for RefusalAgentConnection { - fn telemetry_id(&self) -> SharedString { - "refusal".into() - } - - 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( - "RefusalAgentConnection", - self, - project, - action_log, - SessionId::new("test"), - watch::Receiver::constant( - acp::PromptCapabilities::new() - .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(Ok(acp::PromptResponse::new(acp::StopReason::Refusal))) - } - - 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); - theme::init(theme::LoadThemes::JustBase, cx); - release_channel::init(semver::Version::new(0, 0, 0), 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 text_thread_store = - cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx))); - let history_store = - cx.update(|_window, cx| cx.new(|cx| HistoryStore::new(text_thread_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, - false, - 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::new("tool1", "Edit file 1") - .kind(acp::ToolKind::Edit) - .status(acp::ToolCallStatus::Completed) - .content(vec![acp::ToolCallContent::Diff( - acp::Diff::new("/project/test1.txt", "new content 1").old_text("old content 1"), - )]), - )]); - - 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::new("tool2", "Edit file 2") - .kind(acp::ToolKind::Edit) - .status(acp::ToolCallStatus::Completed) - .content(vec![acp::ToolCallContent::Diff( - acp::Diff::new("/project/test2.txt", "new content 2").old_text("old content 2"), - )]), - )]); - - 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( - acp::ContentChunk::new("Response".into()), - )]); - - 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( - acp::ContentChunk::new("Response".into()), - )]); - - 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( - acp::ContentChunk::new("New Response".into()), - )]); - - 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(acp::ContentChunk::new("Response".into())), - 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(acp::ContentChunk::new( - "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(acp::ContentChunk::new("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(acp::ContentChunk::new( - "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 - - "} - ) - }); - } - - #[gpui::test] - async fn test_message_editing_insert_selections(cx: &mut TestAppContext) { - init_test(cx); - - let connection = StubAgentConnection::new(); - connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk( - acp::ContentChunk::new("Response".into()), - )]); - - 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, |thread_view, cx| { - thread_view - .entry_view_state - .read(cx) - .entry(0) - .expect("Should have at least one entry") - .message_editor() - .expect("Should have message editor") - .clone() - }); - - cx.focus(&user_message_editor); - thread_view.read_with(cx, |thread_view, _cx| { - assert_eq!(thread_view.editing_message, Some(0)); - }); - - // Ensure to edit the focused message before proceeding otherwise, since - // its content is not different from what was sent, focus will be lost. - user_message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Original message to edit with ", window, cx) - }); - - // Create a simple buffer with some text so we can create a selection - // that will then be added to the message being edited. - let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| { - (thread_view.workspace.clone(), thread_view.project.clone()) - }); - let buffer = project.update(cx, |project, cx| { - project.create_local_buffer("let a = 10 + 10;", None, false, cx) - }); - - workspace - .update_in(cx, |workspace, window, cx| { - let editor = cx.new(|cx| { - let mut editor = - Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx); - - editor.change_selections(Default::default(), window, cx, |selections| { - selections.select_ranges([MultiBufferOffset(8)..MultiBufferOffset(15)]); - }); - - editor - }); - workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx); - }) - .unwrap(); - - thread_view.update_in(cx, |thread_view, window, cx| { - assert_eq!(thread_view.editing_message, Some(0)); - thread_view.insert_selections(window, cx); - }); - - user_message_editor.read_with(cx, |editor, cx| { - let text = editor.editor().read(cx).text(cx); - let expected_text = String::from("Original message to edit with selection "); - - assert_eq!(text, expected_text); - }); - } - - #[gpui::test] - async fn test_insert_selections(cx: &mut TestAppContext) { - init_test(cx); - - let connection = StubAgentConnection::new(); - connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk( - acp::ContentChunk::new("Response".into()), - )]); - - 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("Can you review this snippet ", window, cx) - }); - - // Create a simple buffer with some text so we can create a selection - // that will then be added to the message being edited. - let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| { - (thread_view.workspace.clone(), thread_view.project.clone()) - }); - let buffer = project.update(cx, |project, cx| { - project.create_local_buffer("let a = 10 + 10;", None, false, cx) - }); - - workspace - .update_in(cx, |workspace, window, cx| { - let editor = cx.new(|cx| { - let mut editor = - Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx); - - editor.change_selections(Default::default(), window, cx, |selections| { - selections.select_ranges([MultiBufferOffset(8)..MultiBufferOffset(15)]); - }); - - editor - }); - workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx); - }) - .unwrap(); - - thread_view.update_in(cx, |thread_view, window, cx| { - assert_eq!(thread_view.editing_message, None); - thread_view.insert_selections(window, cx); - }); - - thread_view.read_with(cx, |thread_view, cx| { - let text = thread_view.message_editor.read(cx).text(cx); - let expected_txt = String::from("Can you review this snippet selection "); - - assert_eq!(text, expected_txt); - }) - } -} diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs deleted file mode 100644 index 327f699b4d..0000000000 --- a/crates/agent_ui/src/agent_configuration.rs +++ /dev/null @@ -1,1451 +0,0 @@ -mod add_llm_provider_modal; -pub mod configure_context_server_modal; -mod configure_context_server_tools_modal; -mod manage_profiles_modal; -mod tool_picker; - -use std::{ops::Range, sync::Arc}; - -use agent::ContextServerRegistry; -use anyhow::Result; -use client::zed_urls; -use cloud_llm_client::{Plan, PlanV1, PlanV2}; -use collections::HashMap; -use context_server::ContextServerId; -use editor::{Editor, MultiBufferOffset, SelectionEffects, scroll::Autoscroll}; -use extension::ExtensionManifest; -use extension_host::ExtensionStore; -use fs::Fs; -use gpui::{ - Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable, - ScrollHandle, Subscription, Task, WeakEntity, -}; -use language::LanguageRegistry; -use language_model::{ - LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID, -}; -use language_models::AllLanguageModelSettings; -use notifications::status_toast::{StatusToast, ToastIcon}; -use project::{ - agent_server_store::{ - AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, ExternalAgentServerName, GEMINI_NAME, - }, - context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore}, -}; -use settings::{Settings, SettingsStore, update_settings_file}; -use ui::{ - Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure, - Divider, DividerColor, ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize, - PopoverMenu, Switch, Tooltip, WithScrollbar, prelude::*, -}; -use util::ResultExt as _; -use workspace::{Workspace, create_and_open_local_file}; -use zed_actions::{ExtensionCategoryFilter, OpenBrowser}; - -pub(crate) use configure_context_server_modal::ConfigureContextServerModal; -pub(crate) use configure_context_server_tools_modal::ConfigureContextServerToolsModal; -pub(crate) use manage_profiles_modal::ManageProfilesModal; - -use crate::agent_configuration::add_llm_provider_modal::{ - AddLlmProviderModal, LlmCompatibleProvider, -}; - -pub struct AgentConfiguration { - fs: Arc, - language_registry: Arc, - agent_server_store: Entity, - workspace: WeakEntity, - focus_handle: FocusHandle, - configuration_views_by_provider: HashMap, - context_server_store: Entity, - expanded_provider_configurations: HashMap, - context_server_registry: Entity, - _registry_subscription: Subscription, - scroll_handle: ScrollHandle, - _check_for_gemini: Task<()>, -} - -impl AgentConfiguration { - pub fn new( - fs: Arc, - agent_server_store: Entity, - context_server_store: Entity, - context_server_registry: Entity, - language_registry: Arc, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let focus_handle = cx.focus_handle(); - - let registry_subscription = cx.subscribe_in( - &LanguageModelRegistry::global(cx), - window, - |this, _, event: &language_model::Event, window, cx| match event { - language_model::Event::AddedProvider(provider_id) => { - let provider = LanguageModelRegistry::read_global(cx).provider(provider_id); - if let Some(provider) = provider { - this.add_provider_configuration_view(&provider, window, cx); - } - } - language_model::Event::RemovedProvider(provider_id) => { - this.remove_provider_configuration_view(provider_id); - } - _ => {} - }, - ); - - cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify()) - .detach(); - - let mut this = Self { - fs, - language_registry, - workspace, - focus_handle, - configuration_views_by_provider: HashMap::default(), - agent_server_store, - context_server_store, - expanded_provider_configurations: HashMap::default(), - context_server_registry, - _registry_subscription: registry_subscription, - scroll_handle: ScrollHandle::new(), - _check_for_gemini: Task::ready(()), - }; - this.build_provider_configuration_views(window, cx); - this - } - - fn build_provider_configuration_views(&mut self, window: &mut Window, cx: &mut Context) { - let providers = LanguageModelRegistry::read_global(cx).providers(); - for provider in providers { - self.add_provider_configuration_view(&provider, window, cx); - } - } - - fn remove_provider_configuration_view(&mut self, provider_id: &LanguageModelProviderId) { - self.configuration_views_by_provider.remove(provider_id); - self.expanded_provider_configurations.remove(provider_id); - } - - fn add_provider_configuration_view( - &mut self, - provider: &Arc, - window: &mut Window, - cx: &mut Context, - ) { - let configuration_view = provider.configuration_view( - language_model::ConfigurationViewTargetAgent::ZedAgent, - window, - cx, - ); - self.configuration_views_by_provider - .insert(provider.id(), configuration_view); - } -} - -impl Focusable for AgentConfiguration { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -pub enum AssistantConfigurationEvent { - NewThread(Arc), -} - -impl EventEmitter for AgentConfiguration {} - -enum AgentIcon { - Name(IconName), - Path(SharedString), -} - -impl AgentConfiguration { - fn render_section_title( - &mut self, - title: impl Into, - description: impl Into, - menu: AnyElement, - ) -> impl IntoElement { - h_flex() - .p_4() - .pb_0() - .mb_2p5() - .items_start() - .justify_between() - .child( - v_flex() - .w_full() - .gap_0p5() - .child( - h_flex() - .pr_1() - .w_full() - .gap_2() - .justify_between() - .flex_wrap() - .child(Headline::new(title.into())) - .child(menu), - ) - .child(Label::new(description.into()).color(Color::Muted)), - ) - } - - fn render_provider_configuration_block( - &mut self, - provider: &Arc, - cx: &mut Context, - ) -> impl IntoElement + use<> { - let provider_id = provider.id().0; - let provider_name = provider.name().0; - let provider_id_string = SharedString::from(format!("provider-disclosure-{provider_id}")); - - let configuration_view = self - .configuration_views_by_provider - .get(&provider.id()) - .cloned(); - - let is_expanded = self - .expanded_provider_configurations - .get(&provider.id()) - .copied() - .unwrap_or(false); - - let is_zed_provider = provider.id() == ZED_CLOUD_PROVIDER_ID; - let current_plan = if is_zed_provider { - self.workspace - .upgrade() - .and_then(|workspace| workspace.read(cx).user_store().read(cx).plan()) - } else { - None - }; - - let is_signed_in = self - .workspace - .read_with(cx, |workspace, _| { - !workspace.client().status().borrow().is_signed_out() - }) - .unwrap_or(false); - - v_flex() - .w_full() - .when(is_expanded, |this| this.mb_2()) - .child( - div() - .px_2() - .child(Divider::horizontal().color(DividerColor::BorderFaded)), - ) - .child( - h_flex() - .map(|this| { - if is_expanded { - this.mt_2().mb_1() - } else { - this.my_2() - } - }) - .w_full() - .justify_between() - .child( - h_flex() - .id(provider_id_string.clone()) - .px_2() - .py_0p5() - .w_full() - .justify_between() - .rounded_sm() - .hover(|hover| hover.bg(cx.theme().colors().element_hover)) - .child( - h_flex() - .w_full() - .gap_1p5() - .child( - Icon::new(provider.icon()) - .size(IconSize::Small) - .color(Color::Muted), - ) - .child( - h_flex() - .w_full() - .gap_1() - .child(Label::new(provider_name.clone())) - .map(|this| { - if is_zed_provider && is_signed_in { - this.child( - self.render_zed_plan_info(current_plan, cx), - ) - } else { - this.when( - provider.is_authenticated(cx) - && !is_expanded, - |parent| { - parent.child( - Icon::new(IconName::Check) - .color(Color::Success), - ) - }, - ) - } - }), - ), - ) - .child( - Disclosure::new(provider_id_string, is_expanded) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown), - ) - .on_click(cx.listener({ - let provider_id = provider.id(); - move |this, _event, _window, _cx| { - let is_expanded = this - .expanded_provider_configurations - .entry(provider_id.clone()) - .or_insert(false); - - *is_expanded = !*is_expanded; - } - })), - ), - ) - .child( - v_flex() - .w_full() - .px_2() - .gap_1() - .when(is_expanded, |parent| match configuration_view { - Some(configuration_view) => parent.child(configuration_view), - None => parent.child(Label::new(format!( - "No configuration view for {provider_name}", - ))), - }) - .when(is_expanded && provider.is_authenticated(cx), |parent| { - parent.child( - Button::new( - SharedString::from(format!("new-thread-{provider_id}")), - "Start New Thread", - ) - .full_width() - .style(ButtonStyle::Outlined) - .layer(ElevationIndex::ModalSurface) - .icon_position(IconPosition::Start) - .icon(IconName::Thread) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .label_size(LabelSize::Small) - .on_click(cx.listener({ - let provider = provider.clone(); - move |_this, _event, _window, cx| { - cx.emit(AssistantConfigurationEvent::NewThread( - provider.clone(), - )) - } - })), - ) - }) - .when( - is_expanded && is_removable_provider(&provider.id(), cx), - |this| { - this.child( - Button::new( - SharedString::from(format!("delete-provider-{provider_id}")), - "Remove Provider", - ) - .full_width() - .style(ButtonStyle::Outlined) - .icon_position(IconPosition::Start) - .icon(IconName::Trash) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .label_size(LabelSize::Small) - .on_click(cx.listener({ - let provider = provider.clone(); - move |this, _event, window, cx| { - this.delete_provider(provider.clone(), window, cx); - } - })), - ) - }, - ), - ) - } - - fn delete_provider( - &mut self, - provider: Arc, - window: &mut Window, - cx: &mut Context, - ) { - let fs = self.fs.clone(); - let provider_id = provider.id(); - - cx.spawn_in(window, async move |_, cx| { - cx.update(|_window, cx| { - update_settings_file(fs.clone(), cx, { - let provider_id = provider_id.clone(); - move |settings, _| { - if let Some(ref mut openai_compatible) = settings - .language_models - .as_mut() - .and_then(|lm| lm.openai_compatible.as_mut()) - { - let key_to_remove: Arc = Arc::from(provider_id.0.as_ref()); - openai_compatible.remove(&key_to_remove); - } - } - }); - }) - .log_err(); - - cx.update(|_window, cx| { - LanguageModelRegistry::global(cx).update(cx, { - let provider_id = provider_id.clone(); - move |registry, cx| { - registry.unregister_provider(provider_id, cx); - } - }) - }) - .log_err(); - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - - fn render_provider_configuration_section( - &mut self, - cx: &mut Context, - ) -> impl IntoElement { - let providers = LanguageModelRegistry::read_global(cx).providers(); - - let popover_menu = PopoverMenu::new("add-provider-popover") - .trigger( - Button::new("add-provider", "Add Provider") - .style(ButtonStyle::Outlined) - .icon_position(IconPosition::Start) - .icon(IconName::Plus) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .label_size(LabelSize::Small), - ) - .menu({ - let workspace = self.workspace.clone(); - move |window, cx| { - Some(ContextMenu::build(window, cx, |menu, _window, _cx| { - menu.header("Compatible APIs").entry("OpenAI", None, { - let workspace = workspace.clone(); - move |window, cx| { - workspace - .update(cx, |workspace, cx| { - AddLlmProviderModal::toggle( - LlmCompatibleProvider::OpenAi, - workspace, - window, - cx, - ); - }) - .log_err(); - } - }) - })) - } - }) - .anchor(gpui::Corner::TopRight) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }); - - v_flex() - .w_full() - .child(self.render_section_title( - "LLM Providers", - "Add at least one provider to use AI-powered features with Zed's native agent.", - popover_menu.into_any_element(), - )) - .child( - div() - .w_full() - .pl(DynamicSpacing::Base08.rems(cx)) - .pr(DynamicSpacing::Base20.rems(cx)) - .children( - providers.into_iter().map(|provider| { - self.render_provider_configuration_block(&provider, cx) - }), - ), - ) - } - - fn render_zed_plan_info(&self, plan: Option, cx: &mut Context) -> impl IntoElement { - if let Some(plan) = plan { - let free_chip_bg = cx - .theme() - .colors() - .editor_background - .opacity(0.5) - .blend(cx.theme().colors().text_accent.opacity(0.05)); - - let pro_chip_bg = cx - .theme() - .colors() - .editor_background - .opacity(0.5) - .blend(cx.theme().colors().text_accent.opacity(0.2)); - - let (plan_name, label_color, bg_color) = match plan { - Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree) => { - ("Free", Color::Default, free_chip_bg) - } - Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial) => { - ("Pro Trial", Color::Accent, pro_chip_bg) - } - Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro) => { - ("Pro", Color::Accent, pro_chip_bg) - } - }; - - Chip::new(plan_name.to_string()) - .bg_color(bg_color) - .label_color(label_color) - .into_any_element() - } else { - div().into_any_element() - } - } - - fn render_context_servers_section( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let mut context_server_ids = self - .context_server_store - .read(cx) - .server_ids(cx) - .into_iter() - .collect::>(); - - // Sort context servers: ones without mcp-server- prefix first, then prefixed ones - context_server_ids.sort_by(|a, b| { - const MCP_PREFIX: &str = "mcp-server-"; - match (a.0.strip_prefix(MCP_PREFIX), b.0.strip_prefix(MCP_PREFIX)) { - // If one has mcp-server- prefix and other doesn't, non-mcp comes first - (Some(_), None) => std::cmp::Ordering::Greater, - (None, Some(_)) => std::cmp::Ordering::Less, - // If both have same prefix status, sort by appropriate key - (Some(a), Some(b)) => a.cmp(b), - (None, None) => a.0.cmp(&b.0), - } - }); - - let add_server_popover = PopoverMenu::new("add-server-popover") - .trigger( - Button::new("add-server", "Add Server") - .style(ButtonStyle::Outlined) - .icon_position(IconPosition::Start) - .icon(IconName::Plus) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .label_size(LabelSize::Small), - ) - .menu({ - move |window, cx| { - Some(ContextMenu::build(window, cx, |menu, _window, _cx| { - menu.entry("Add Custom Server", None, { - |window, cx| { - window.dispatch_action(crate::AddContextServer.boxed_clone(), cx) - } - }) - .entry("Install from Extensions", None, { - |window, cx| { - window.dispatch_action( - zed_actions::Extensions { - category_filter: Some( - ExtensionCategoryFilter::ContextServers, - ), - id: None, - } - .boxed_clone(), - cx, - ) - } - }) - })) - } - }) - .anchor(gpui::Corner::TopRight) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }); - - v_flex() - .border_b_1() - .border_color(cx.theme().colors().border) - .child(self.render_section_title( - "Model Context Protocol (MCP) Servers", - "All MCP servers connected directly or via a Zed extension.", - add_server_popover.into_any_element(), - )) - .child( - v_flex() - .pl_4() - .pb_4() - .pr_5() - .w_full() - .gap_1() - .map(|mut parent| { - if context_server_ids.is_empty() { - parent.child( - h_flex() - .p_4() - .justify_center() - .border_1() - .border_dashed() - .border_color(cx.theme().colors().border.opacity(0.6)) - .rounded_sm() - .child( - Label::new("No MCP servers added yet.") - .color(Color::Muted) - .size(LabelSize::Small), - ), - ) - } else { - for (index, context_server_id) in - context_server_ids.into_iter().enumerate() - { - if index > 0 { - parent = parent.child( - Divider::horizontal() - .color(DividerColor::BorderFaded) - .into_any_element(), - ); - } - parent = parent.child(self.render_context_server( - context_server_id, - window, - cx, - )); - } - parent - } - }), - ) - } - - fn render_context_server( - &self, - context_server_id: ContextServerId, - window: &mut Window, - cx: &mut Context, - ) -> impl use<> + IntoElement { - let server_status = self - .context_server_store - .read(cx) - .status_for_server(&context_server_id) - .unwrap_or(ContextServerStatus::Stopped); - let server_configuration = self - .context_server_store - .read(cx) - .configuration_for_server(&context_server_id); - - let is_running = matches!(server_status, ContextServerStatus::Running); - let item_id = SharedString::from(context_server_id.0.clone()); - // Servers without a configuration can only be provided by extensions. - let provided_by_extension = server_configuration.as_ref().is_none_or(|config| { - matches!( - config.as_ref(), - ContextServerConfiguration::Extension { .. } - ) - }); - - let error = if let ContextServerStatus::Error(error) = server_status.clone() { - Some(error) - } else { - None - }; - - let tool_count = self - .context_server_registry - .read(cx) - .tools_for_server(&context_server_id) - .count(); - - let (source_icon, source_tooltip) = if provided_by_extension { - ( - IconName::ZedSrcExtension, - "This MCP server was installed from an extension.", - ) - } else { - ( - IconName::ZedSrcCustom, - "This custom MCP server was installed directly.", - ) - }; - - let (status_indicator, tooltip_text) = match server_status { - ContextServerStatus::Starting => ( - Icon::new(IconName::LoadCircle) - .size(IconSize::XSmall) - .color(Color::Accent) - .with_keyed_rotate_animation( - SharedString::from(format!("{}-starting", context_server_id.0)), - 3, - ) - .into_any_element(), - "Server is starting.", - ), - ContextServerStatus::Running => ( - Indicator::dot().color(Color::Success).into_any_element(), - "Server is active.", - ), - ContextServerStatus::Error(_) => ( - Indicator::dot().color(Color::Error).into_any_element(), - "Server has an error.", - ), - ContextServerStatus::Stopped => ( - Indicator::dot().color(Color::Muted).into_any_element(), - "Server is stopped.", - ), - }; - let is_remote = server_configuration - .as_ref() - .map(|config| matches!(config.as_ref(), ContextServerConfiguration::Http { .. })) - .unwrap_or(false); - let context_server_configuration_menu = PopoverMenu::new("context-server-config-menu") - .trigger_with_tooltip( - IconButton::new("context-server-config-menu", IconName::Settings) - .icon_color(Color::Muted) - .icon_size(IconSize::Small), - Tooltip::text("Configure MCP Server"), - ) - .anchor(Corner::TopRight) - .menu({ - let fs = self.fs.clone(); - let context_server_id = context_server_id.clone(); - let language_registry = self.language_registry.clone(); - let workspace = self.workspace.clone(); - let context_server_registry = self.context_server_registry.clone(); - - move |window, cx| { - Some(ContextMenu::build(window, cx, |menu, _window, _cx| { - menu.entry("Configure Server", None, { - let context_server_id = context_server_id.clone(); - let language_registry = language_registry.clone(); - let workspace = workspace.clone(); - move |window, cx| { - if is_remote { - crate::agent_configuration::configure_context_server_modal::ConfigureContextServerModal::show_modal_for_existing_server( - context_server_id.clone(), - language_registry.clone(), - workspace.clone(), - window, - cx, - ) - .detach(); - } else { - ConfigureContextServerModal::show_modal_for_existing_server( - context_server_id.clone(), - language_registry.clone(), - workspace.clone(), - window, - cx, - ) - .detach(); - } - } - }).when(tool_count > 0, |this| this.entry("View Tools", None, { - let context_server_id = context_server_id.clone(); - let context_server_registry = context_server_registry.clone(); - let workspace = workspace.clone(); - move |window, cx| { - let context_server_id = context_server_id.clone(); - workspace.update(cx, |workspace, cx| { - ConfigureContextServerToolsModal::toggle( - context_server_id, - context_server_registry.clone(), - workspace, - window, - cx, - ); - }) - .ok(); - } - })) - .separator() - .entry("Uninstall", None, { - let fs = fs.clone(); - let context_server_id = context_server_id.clone(); - let workspace = workspace.clone(); - move |_, cx| { - let uninstall_extension_task = match ( - provided_by_extension, - resolve_extension_for_context_server(&context_server_id, cx), - ) { - (true, Some((id, manifest))) => { - if extension_only_provides_context_server(manifest.as_ref()) - { - ExtensionStore::global(cx).update(cx, |store, cx| { - store.uninstall_extension(id, cx) - }) - } else { - workspace.update(cx, |workspace, cx| { - show_unable_to_uninstall_extension_with_context_server(workspace, context_server_id.clone(), cx); - }).log_err(); - Task::ready(Ok(())) - } - } - _ => Task::ready(Ok(())), - }; - - cx.spawn({ - let fs = fs.clone(); - let context_server_id = context_server_id.clone(); - async move |cx| { - uninstall_extension_task.await?; - cx.update(|cx| { - update_settings_file( - fs.clone(), - cx, - { - let context_server_id = - context_server_id.clone(); - move |settings, _| { - settings.project - .context_servers - .remove(&context_server_id.0); - } - }, - ) - }) - } - }) - .detach_and_log_err(cx); - } - }) - })) - } - }); - - v_flex() - .id(item_id.clone()) - .child( - h_flex() - .justify_between() - .child( - h_flex() - .flex_1() - .min_w_0() - .child( - h_flex() - .id(format!("tooltip-{}", item_id)) - .h_full() - .w_3() - .mr_2() - .justify_center() - .tooltip(Tooltip::text(tooltip_text)) - .child(status_indicator), - ) - .child(Label::new(item_id).truncate()) - .child( - div() - .id("extension-source") - .mt_0p5() - .mx_1() - .flex_none() - .tooltip(Tooltip::text(source_tooltip)) - .child( - Icon::new(source_icon) - .size(IconSize::Small) - .color(Color::Muted), - ), - ) - .when(is_running, |this| { - this.child( - Label::new(if tool_count == 1 { - SharedString::from("1 tool") - } else { - SharedString::from(format!("{} tools", tool_count)) - }) - .color(Color::Muted) - .size(LabelSize::Small), - ) - }), - ) - .child( - h_flex() - .gap_0p5() - .flex_none() - .child(context_server_configuration_menu) - .child( - Switch::new("context-server-switch", is_running.into()) - .on_click({ - let context_server_manager = self.context_server_store.clone(); - let fs = self.fs.clone(); - - move |state, _window, cx| { - let is_enabled = match state { - ToggleState::Unselected - | ToggleState::Indeterminate => { - context_server_manager.update(cx, |this, cx| { - this.stop_server(&context_server_id, cx) - .log_err(); - }); - false - } - ToggleState::Selected => { - context_server_manager.update(cx, |this, cx| { - if let Some(server) = - this.get_server(&context_server_id) - { - this.start_server(server, cx); - } - }); - true - } - }; - update_settings_file(fs.clone(), cx, { - let context_server_id = context_server_id.clone(); - - move |settings, _| { - settings - .project - .context_servers - .entry(context_server_id.0) - .or_insert_with(|| { - settings::ContextServerSettingsContent::Extension { - enabled: is_enabled, - settings: serde_json::json!({}), - } - }) - .set_enabled(is_enabled); - } - }); - } - }), - ), - ), - ) - .map(|parent| { - if let Some(error) = error { - return parent.child( - h_flex() - .gap_2() - .pr_4() - .items_start() - .child( - h_flex() - .flex_none() - .h(window.line_height() / 1.6_f32) - .justify_center() - .child( - Icon::new(IconName::XCircle) - .size(IconSize::XSmall) - .color(Color::Error), - ), - ) - .child( - div().w_full().child( - Label::new(error) - .buffer_font(cx) - .color(Color::Muted) - .size(LabelSize::Small), - ), - ), - ); - } - parent - }) - } - - fn render_agent_servers_section(&mut self, cx: &mut Context) -> impl IntoElement { - let agent_server_store = self.agent_server_store.read(cx); - - let user_defined_agents = agent_server_store - .external_agents() - .filter(|name| { - name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME && name.0 != CODEX_NAME - }) - .cloned() - .collect::>(); - - let user_defined_agents: Vec<_> = user_defined_agents - .into_iter() - .map(|name| { - let icon = if let Some(icon_path) = agent_server_store.agent_icon(&name) { - AgentIcon::Path(icon_path) - } else { - AgentIcon::Name(IconName::Ai) - }; - let display_name = agent_server_store - .agent_display_name(&name) - .unwrap_or_else(|| name.0.clone()); - (name, icon, display_name) - }) - .collect(); - - let add_agent_popover = PopoverMenu::new("add-agent-server-popover") - .trigger( - Button::new("add-agent", "Add Agent") - .style(ButtonStyle::Outlined) - .icon_position(IconPosition::Start) - .icon(IconName::Plus) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .label_size(LabelSize::Small), - ) - .menu({ - move |window, cx| { - Some(ContextMenu::build(window, cx, |menu, _window, _cx| { - menu.entry("Install from Extensions", None, { - |window, cx| { - window.dispatch_action( - zed_actions::Extensions { - category_filter: Some( - ExtensionCategoryFilter::AgentServers, - ), - id: None, - } - .boxed_clone(), - cx, - ) - } - }) - .entry("Add Custom Agent", None, { - 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); - } - } - }) - .separator() - .header("Learn More") - .item( - ContextMenuEntry::new("Agent Servers Docs") - .icon(IconName::ArrowUpRight) - .icon_color(Color::Muted) - .icon_position(IconPosition::End) - .handler({ - move |window, cx| { - window.dispatch_action( - Box::new(OpenBrowser { - url: zed_urls::agent_server_docs(cx), - }), - cx, - ); - } - }), - ) - .item( - ContextMenuEntry::new("ACP Docs") - .icon(IconName::ArrowUpRight) - .icon_color(Color::Muted) - .icon_position(IconPosition::End) - .handler({ - move |window, cx| { - window.dispatch_action( - Box::new(OpenBrowser { - url: "https://agentclientprotocol.com/".into(), - }), - cx, - ); - } - }), - ) - })) - } - }) - .anchor(gpui::Corner::TopRight) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }); - - v_flex() - .border_b_1() - .border_color(cx.theme().colors().border) - .child( - v_flex() - .child(self.render_section_title( - "External Agents", - "All agents connected through the Agent Client Protocol.", - add_agent_popover.into_any_element(), - )) - .child( - v_flex() - .p_4() - .pt_0() - .gap_2() - .child(self.render_agent_server( - AgentIcon::Name(IconName::AiClaude), - "Claude Code", - "Claude Code", - false, - cx, - )) - .child(Divider::horizontal().color(DividerColor::BorderFaded)) - .child(self.render_agent_server( - AgentIcon::Name(IconName::AiOpenAi), - "Codex CLI", - "Codex CLI", - false, - cx, - )) - .child(Divider::horizontal().color(DividerColor::BorderFaded)) - .child(self.render_agent_server( - AgentIcon::Name(IconName::AiGemini), - "Gemini CLI", - "Gemini CLI", - false, - cx, - )) - .map(|mut parent| { - for (name, icon, display_name) in user_defined_agents { - parent = parent - .child( - Divider::horizontal().color(DividerColor::BorderFaded), - ) - .child(self.render_agent_server( - icon, - name, - display_name, - true, - cx, - )); - } - parent - }), - ), - ) - } - - fn render_agent_server( - &self, - icon: AgentIcon, - id: impl Into, - display_name: impl Into, - external: bool, - cx: &mut Context, - ) -> impl IntoElement { - let id = id.into(); - let display_name = display_name.into(); - let icon = match icon { - AgentIcon::Name(icon_name) => Icon::new(icon_name) - .size(IconSize::Small) - .color(Color::Muted), - AgentIcon::Path(icon_path) => Icon::from_external_svg(icon_path) - .size(IconSize::Small) - .color(Color::Muted), - }; - - let tooltip_id = SharedString::new(format!("agent-source-{}", id)); - let tooltip_message = format!( - "The {} agent was installed from an extension.", - display_name - ); - - let agent_server_name = ExternalAgentServerName(id.clone()); - - let uninstall_btn_id = SharedString::from(format!("uninstall-{}", id)); - let uninstall_button = IconButton::new(uninstall_btn_id, IconName::Trash) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Uninstall Agent Extension")) - .on_click(cx.listener(move |this, _, _window, cx| { - let agent_name = agent_server_name.clone(); - - if let Some(ext_id) = this.agent_server_store.update(cx, |store, _cx| { - store.get_extension_id_for_agent(&agent_name) - }) { - ExtensionStore::global(cx) - .update(cx, |store, cx| store.uninstall_extension(ext_id, cx)) - .detach_and_log_err(cx); - } - })); - - h_flex() - .gap_1() - .justify_between() - .child( - h_flex() - .gap_1p5() - .child(icon) - .child(Label::new(display_name)) - .when(external, |this| { - this.child( - div() - .id(tooltip_id) - .flex_none() - .tooltip(Tooltip::text(tooltip_message)) - .child( - Icon::new(IconName::ZedSrcExtension) - .size(IconSize::Small) - .color(Color::Muted), - ), - ) - }) - .child( - Icon::new(IconName::Check) - .color(Color::Success) - .size(IconSize::Small), - ), - ) - .when(external, |this| this.child(uninstall_button)) - } -} - -impl Render for AgentConfiguration { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .id("assistant-configuration") - .key_context("AgentConfiguration") - .track_focus(&self.focus_handle(cx)) - .relative() - .size_full() - .pb_8() - .bg(cx.theme().colors().panel_background) - .child( - div() - .size_full() - .child( - v_flex() - .id("assistant-configuration-content") - .track_scroll(&self.scroll_handle) - .size_full() - .overflow_y_scroll() - .child(self.render_agent_servers_section(cx)) - .child(self.render_context_servers_section(window, cx)) - .child(self.render_provider_configuration_section(cx)), - ) - .vertical_scrollbar_for(&self.scroll_handle, window, cx), - ) - } -} - -fn extension_only_provides_context_server(manifest: &ExtensionManifest) -> bool { - manifest.context_servers.len() == 1 - && manifest.themes.is_empty() - && manifest.icon_themes.is_empty() - && manifest.languages.is_empty() - && manifest.grammars.is_empty() - && manifest.language_servers.is_empty() - && manifest.slash_commands.is_empty() - && manifest.snippets.is_none() - && manifest.debug_locators.is_empty() -} - -pub(crate) fn resolve_extension_for_context_server( - id: &ContextServerId, - cx: &App, -) -> Option<(Arc, Arc)> { - ExtensionStore::global(cx) - .read(cx) - .installed_extensions() - .iter() - .find(|(_, entry)| entry.manifest.context_servers.contains_key(&id.0)) - .map(|(id, entry)| (id.clone(), entry.manifest.clone())) -} - -// This notification appears when trying to delete -// an MCP server extension that not only provides -// the server, but other things, too, like language servers and more. -fn show_unable_to_uninstall_extension_with_context_server( - workspace: &mut Workspace, - id: ContextServerId, - cx: &mut App, -) { - let workspace_handle = workspace.weak_handle(); - let context_server_id = id.clone(); - - let status_toast = StatusToast::new( - format!( - "The {} extension provides more than just the MCP server. Proceed to uninstall anyway?", - id.0 - ), - cx, - move |this, _cx| { - let workspace_handle = workspace_handle.clone(); - - this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning)) - .dismiss_button(true) - .action("Uninstall", move |_, _cx| { - if let Some((extension_id, _)) = - resolve_extension_for_context_server(&context_server_id, _cx) - { - ExtensionStore::global(_cx).update(_cx, |store, cx| { - store - .uninstall_extension(extension_id, cx) - .detach_and_log_err(cx); - }); - - workspace_handle - .update(_cx, |workspace, cx| { - let fs = workspace.app_state().fs.clone(); - cx.spawn({ - let context_server_id = context_server_id.clone(); - async move |_workspace_handle, cx| { - cx.update(|cx| { - update_settings_file(fs, cx, move |settings, _| { - settings - .project - .context_servers - .remove(&context_server_id.0); - }); - })?; - anyhow::Ok(()) - } - }) - .detach_and_log_err(cx); - }) - .log_err(); - } - }) - }, - ); - - 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, |settings| { - let server_name: Option = (0..u8::MAX) - .map(|i| { - if i == 0 { - "your_agent".into() - } else { - format!("your_agent_{}", i).into() - } - }) - .find(|name| { - !settings - .agent_servers - .as_ref() - .is_some_and(|agent_servers| agent_servers.custom.contains_key(name)) - }); - if let Some(server_name) = server_name { - unique_server_name = Some(server_name.clone()); - settings - .agent_servers - .get_or_insert_default() - .custom - .insert( - server_name, - settings::CustomAgentServerSettings::Custom { - path: "path_to_executable".into(), - args: vec![], - env: Some(HashMap::default()), - default_mode: None, - default_model: None, - }, - ); - } - }); - - if edits.is_empty() { - return; - } - - let ranges = edits - .iter() - .map(|(range, _)| range.clone()) - .collect::>(); - - item.edit( - edits.into_iter().map(|(range, s)| { - ( - MultiBufferOffset(range.start)..MultiBufferOffset(range.end), - s, - ) - }), - 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![ - MultiBufferOffset(range.start)..MultiBufferOffset(range.end), - ]); - }, - ); - } - } - }) -} - -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 - } -} - -// OpenAI-compatible providers are user-configured and can be removed, -// whereas built-in providers (like Anthropic, OpenAI, Google, etc.) can't. -// -// If in the future we have more "API-compatible-type" of providers, -// they should be included here as removable providers. -fn is_removable_provider(provider_id: &LanguageModelProviderId, cx: &App) -> bool { - AllLanguageModelSettings::get_global(cx) - .openai_compatible - .contains_key(provider_id.0.as_ref()) -} 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 deleted file mode 100644 index 02269511bb..0000000000 --- a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs +++ /dev/null @@ -1,848 +0,0 @@ -use std::sync::Arc; - -use anyhow::Result; -use collections::HashSet; -use fs::Fs; -use gpui::{ - DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, ScrollHandle, Task, -}; -use language_model::LanguageModelRegistry; -use language_models::provider::open_ai_compatible::{AvailableModel, ModelCapabilities}; -use settings::{OpenAiCompatibleSettingsContent, update_settings_file}; -use ui::{ - Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, - WithScrollbar, prelude::*, -}; -use ui_input::InputField; -use workspace::{ModalView, Workspace}; - -fn single_line_input( - label: impl Into, - placeholder: impl Into, - text: Option<&str>, - tab_index: isize, - window: &mut Window, - cx: &mut App, -) -> Entity { - cx.new(|cx| { - let input = InputField::new(window, cx, placeholder) - .label(label) - .tab_index(tab_index) - .tab_stop(true); - - if let Some(text) = text { - input - .editor() - .update(cx, |editor, cx| editor.set_text(text, window, cx)); - } - input - }) -} - -#[derive(Clone, Copy)] -pub enum LlmCompatibleProvider { - OpenAi, -} - -impl LlmCompatibleProvider { - fn name(&self) -> &'static str { - match self { - LlmCompatibleProvider::OpenAi => "OpenAI", - } - } - - fn api_url(&self) -> &'static str { - match self { - LlmCompatibleProvider::OpenAi => "https://api.openai.com/v1", - } - } -} - -struct AddLlmProviderInput { - provider_name: Entity, - api_url: Entity, - api_key: Entity, - models: Vec, -} - -impl AddLlmProviderInput { - fn new(provider: LlmCompatibleProvider, window: &mut Window, cx: &mut App) -> Self { - let provider_name = - single_line_input("Provider Name", provider.name(), None, 1, window, cx); - let api_url = single_line_input("API URL", provider.api_url(), None, 2, window, cx); - let api_key = single_line_input( - "API Key", - "000000000000000000000000000000000000000000000000", - None, - 3, - window, - cx, - ); - - Self { - provider_name, - api_url, - api_key, - models: vec![ModelInput::new(0, window, cx)], - } - } - - fn add_model(&mut self, window: &mut Window, cx: &mut App) { - let model_index = self.models.len(); - self.models.push(ModelInput::new(model_index, window, cx)); - } - - fn remove_model(&mut self, index: usize) { - self.models.remove(index); - } -} - -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 { - fn new(model_index: usize, window: &mut Window, cx: &mut App) -> Self { - let base_tab_index = (3 + (model_index * 4)) as isize; - - let model_name = single_line_input( - "Model Name", - "e.g. gpt-4o, claude-opus-4, gemini-2.5-pro", - None, - base_tab_index + 1, - window, - cx, - ); - let max_completion_tokens = single_line_input( - "Max Completion Tokens", - "200000", - Some("200000"), - base_tab_index + 2, - window, - cx, - ); - let max_output_tokens = single_line_input( - "Max Output Tokens", - "Max Output Tokens", - Some("32000"), - base_tab_index + 3, - window, - cx, - ); - let max_tokens = single_line_input( - "Max Tokens", - "Max Tokens", - Some("200000"), - base_tab_index + 4, - 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(), - }, - } - } - - fn parse(&self, cx: &App) -> Result { - let name = self.name.read(cx).text(cx); - if name.is_empty() { - return Err(SharedString::from("Model Name cannot be empty")); - } - Ok(AvailableModel { - name, - display_name: None, - max_completion_tokens: Some( - self.max_completion_tokens - .read(cx) - .text(cx) - .parse::() - .map_err(|_| SharedString::from("Max Completion Tokens must be a number"))?, - ), - max_output_tokens: Some( - self.max_output_tokens - .read(cx) - .text(cx) - .parse::() - .map_err(|_| SharedString::from("Max Output Tokens must be a number"))?, - ), - max_tokens: self - .max_tokens - .read(cx) - .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(), - }, - }) - } -} - -fn save_provider_to_settings( - input: &AddLlmProviderInput, - cx: &mut App, -) -> Task> { - let provider_name: Arc = input.provider_name.read(cx).text(cx).into(); - if provider_name.is_empty() { - return Task::ready(Err("Provider Name cannot be empty".into())); - } - - if LanguageModelRegistry::read_global(cx) - .providers() - .iter() - .any(|provider| { - provider.id().0.as_ref() == provider_name.as_ref() - || provider.name().0.as_ref() == provider_name.as_ref() - }) - { - return Task::ready(Err( - "Provider Name is already taken by another provider".into() - )); - } - - let api_url = input.api_url.read(cx).text(cx); - if api_url.is_empty() { - return Task::ready(Err("API URL cannot be empty".into())); - } - - let api_key = input.api_key.read(cx).text(cx); - if api_key.is_empty() { - return Task::ready(Err("API Key cannot be empty".into())); - } - - let mut models = Vec::new(); - let mut model_names: HashSet = HashSet::default(); - for model in &input.models { - match model.parse(cx) { - Ok(model) => { - if !model_names.insert(model.name.clone()) { - return Task::ready(Err("Model Names must be unique".into())); - } - models.push(model) - } - Err(err) => return Task::ready(Err(err)), - } - } - - let fs = ::global(cx); - let task = cx.write_credentials(&api_url, "Bearer", api_key.as_bytes()); - cx.spawn(async move |cx| { - task.await - .map_err(|_| "Failed to write API key to keychain")?; - cx.update(|cx| { - update_settings_file(fs, cx, |settings, _cx| { - settings - .language_models - .get_or_insert_default() - .openai_compatible - .get_or_insert_default() - .insert( - provider_name, - OpenAiCompatibleSettingsContent { - api_url, - available_models: models, - }, - ); - }); - }) - .ok(); - Ok(()) - }) -} - -pub struct AddLlmProviderModal { - provider: LlmCompatibleProvider, - input: AddLlmProviderInput, - scroll_handle: ScrollHandle, - focus_handle: FocusHandle, - last_error: Option, -} - -impl AddLlmProviderModal { - pub fn toggle( - provider: LlmCompatibleProvider, - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, - ) { - workspace.toggle_modal(window, cx, |window, cx| Self::new(provider, window, cx)); - } - - fn new(provider: LlmCompatibleProvider, window: &mut Window, cx: &mut Context) -> Self { - Self { - input: AddLlmProviderInput::new(provider, window, cx), - provider, - last_error: None, - focus_handle: cx.focus_handle(), - scroll_handle: ScrollHandle::new(), - } - } - - fn confirm(&mut self, _: &menu::Confirm, _: &mut Window, cx: &mut Context) { - let task = save_provider_to_settings(&self.input, cx); - cx.spawn(async move |this, cx| { - let result = task.await; - this.update(cx, |this, cx| match result { - Ok(_) => { - cx.emit(DismissEvent); - } - Err(error) => { - this.last_error = Some(error); - cx.notify(); - } - }) - }) - .detach_and_log_err(cx); - } - - fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { - 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_model(&self, ix: usize, cx: &mut Context) -> impl IntoElement + use<> { - let has_more_than_one_model = self.input.models.len() > 1; - let model = &self.input.models[ix]; - - v_flex() - .p_2() - .gap_2() - .rounded_sm() - .border_1() - .border_dashed() - .border_color(cx.theme().colors().border.opacity(0.6)) - .bg(cx.theme().colors().element_active.opacity(0.15)) - .child(model.name.clone()) - .child( - h_flex() - .gap_2() - .child(model.max_completion_tokens.clone()) - .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") - .icon(IconName::Trash) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .label_size(LabelSize::Small) - .style(ButtonStyle::Outlined) - .full_width() - .on_click(cx.listener(move |this, _, _window, cx| { - this.input.remove_model(ix); - cx.notify(); - })), - ) - }) - } - - fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context) { - window.focus_next(); - } - - fn on_tab_prev( - &mut self, - _: &menu::SelectPrevious, - window: &mut Window, - _: &mut Context, - ) { - window.focus_prev(); - } -} - -impl EventEmitter for AddLlmProviderModal {} - -impl Focusable for AddLlmProviderModal { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl ModalView for AddLlmProviderModal {} - -impl Render for AddLlmProviderModal { - fn render(&mut self, window: &mut ui::Window, cx: &mut ui::Context) -> impl IntoElement { - let focus_handle = self.focus_handle(cx); - - let window_size = window.viewport_size(); - let rem_size = window.rem_size(); - let is_large_window = window_size.height / rem_size > rems_from_px(600.).0; - - let modal_max_height = if is_large_window { - rems_from_px(450.) - } else { - rems_from_px(200.) - }; - - v_flex() - .id("add-llm-provider-modal") - .key_context("AddLlmProviderModal") - .w(rems(34.)) - .elevation_3(cx) - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(Self::on_tab)) - .on_action(cx.listener(Self::on_tab_prev)) - .capture_any_mouse_down(cx.listener(|this, _, window, cx| { - this.focus_handle(cx).focus(window); - })) - .child( - Modal::new("configure-context-server", None) - .header(ModalHeader::new().headline("Add LLM Provider").description( - match self.provider { - LlmCompatibleProvider::OpenAi => { - "This provider will use an OpenAI compatible API." - } - }, - )) - .when_some(self.last_error.clone(), |this, error| { - this.section( - Section::new().child( - Banner::new() - .severity(Severity::Warning) - .child(div().text_xs().child(error)), - ), - ) - }) - .child( - div() - .size_full() - .vertical_scrollbar_for(&self.scroll_handle, window, cx) - .child( - v_flex() - .id("modal_content") - .size_full() - .tab_group() - .max_h(modal_max_height) - .pl_3() - .pr_4() - .gap_2() - .overflow_y_scroll() - .track_scroll(&self.scroll_handle) - .child(self.input.provider_name.clone()) - .child(self.input.api_url.clone()) - .child(self.input.api_key.clone()) - .child(self.render_model_section(cx)), - ), - ) - .footer( - ModalFooter::new().end_slot( - h_flex() - .gap_1() - .child( - Button::new("cancel", "Cancel") - .key_binding( - KeyBinding::for_action_in( - &menu::Cancel, - &focus_handle, - cx, - ) - .map(|kb| kb.size(rems_from_px(12.))), - ) - .on_click(cx.listener(|this, _event, window, cx| { - this.cancel(&menu::Cancel, window, cx) - })), - ) - .child( - Button::new("save-server", "Save Provider") - .key_binding( - KeyBinding::for_action_in( - &menu::Confirm, - &focus_handle, - cx, - ) - .map(|kb| kb.size(rems_from_px(12.))), - ) - .on_click(cx.listener(|this, _event, window, cx| { - this.confirm(&menu::Confirm, window, cx) - })), - ), - ), - ), - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use fs::FakeFs; - use gpui::{TestAppContext, VisualTestContext}; - use language_model::{ - LanguageModelProviderId, LanguageModelProviderName, - fake_provider::FakeLanguageModelProvider, - }; - use project::Project; - use settings::SettingsStore; - use util::path; - - #[gpui::test] - async fn test_save_provider_invalid_inputs(cx: &mut TestAppContext) { - let cx = setup_test(cx).await; - - assert_eq!( - save_provider_validation_errors("", "someurl", "somekey", vec![], cx,).await, - Some("Provider Name cannot be empty".into()) - ); - - assert_eq!( - save_provider_validation_errors("someprovider", "", "somekey", vec![], cx,).await, - Some("API URL cannot be empty".into()) - ); - - assert_eq!( - save_provider_validation_errors("someprovider", "someurl", "", vec![], cx,).await, - Some("API Key cannot be empty".into()) - ); - - assert_eq!( - save_provider_validation_errors( - "someprovider", - "someurl", - "somekey", - vec![("", "200000", "200000", "32000")], - cx, - ) - .await, - Some("Model Name cannot be empty".into()) - ); - - assert_eq!( - save_provider_validation_errors( - "someprovider", - "someurl", - "somekey", - vec![("somemodel", "abc", "200000", "32000")], - cx, - ) - .await, - Some("Max Tokens must be a number".into()) - ); - - assert_eq!( - save_provider_validation_errors( - "someprovider", - "someurl", - "somekey", - vec![("somemodel", "200000", "abc", "32000")], - cx, - ) - .await, - Some("Max Completion Tokens must be a number".into()) - ); - - assert_eq!( - save_provider_validation_errors( - "someprovider", - "someurl", - "somekey", - vec![("somemodel", "200000", "200000", "abc")], - cx, - ) - .await, - Some("Max Output Tokens must be a number".into()) - ); - - assert_eq!( - save_provider_validation_errors( - "someprovider", - "someurl", - "somekey", - vec![ - ("somemodel", "200000", "200000", "32000"), - ("somemodel", "200000", "200000", "32000"), - ], - cx, - ) - .await, - Some("Model Names must be unique".into()) - ); - } - - #[gpui::test] - async fn test_save_provider_name_conflict(cx: &mut TestAppContext) { - let cx = setup_test(cx).await; - - cx.update(|_window, cx| { - LanguageModelRegistry::global(cx).update(cx, |registry, cx| { - registry.register_provider( - Arc::new(FakeLanguageModelProvider::new( - LanguageModelProviderId::new("someprovider"), - LanguageModelProviderName::new("Some Provider"), - )), - cx, - ); - }); - }); - - assert_eq!( - save_provider_validation_errors( - "someprovider", - "someurl", - "someapikey", - vec![("somemodel", "200000", "200000", "32000")], - cx, - ) - .await, - Some("Provider Name is already taken by another provider".into()) - ); - } - - #[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(0, 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(0, 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(0, 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); - cx.set_global(store); - theme::init(theme::LoadThemes::JustBase, cx); - - language_model::init_settings(cx); - }); - - let fs = FakeFs::new(cx.executor()); - cx.update(|cx| ::set_global(fs.clone(), cx)); - let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let (_, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - cx - } - - async fn save_provider_validation_errors( - provider_name: &str, - api_url: &str, - api_key: &str, - models: Vec<(&str, &str, &str, &str)>, - cx: &mut VisualTestContext, - ) -> Option { - fn set_text(input: &Entity, text: &str, window: &mut Window, cx: &mut App) { - input.update(cx, |input, cx| { - input.editor().update(cx, |editor, cx| { - editor.set_text(text, window, cx); - }); - }); - } - - let task = cx.update(|window, cx| { - let mut input = AddLlmProviderInput::new(LlmCompatibleProvider::OpenAi, window, cx); - set_text(&input.provider_name, provider_name, window, cx); - set_text(&input.api_url, api_url, window, cx); - set_text(&input.api_key, api_key, window, cx); - - for (i, (name, max_tokens, max_completion_tokens, max_output_tokens)) in - models.iter().enumerate() - { - if i >= input.models.len() { - input.models.push(ModelInput::new(i, window, cx)); - } - let model = &mut input.models[i]; - set_text(&model.name, name, window, cx); - set_text(&model.max_tokens, max_tokens, window, cx); - set_text( - &model.max_completion_tokens, - max_completion_tokens, - window, - cx, - ); - set_text(&model.max_output_tokens, max_output_tokens, window, cx); - } - save_provider_to_settings(&input, cx) - }); - - task.await.err() - } -} 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 deleted file mode 100644 index a0f0be886a..0000000000 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ /dev/null @@ -1,942 +0,0 @@ -use std::sync::{Arc, Mutex}; - -use anyhow::{Context as _, Result}; -use collections::HashMap; -use context_server::{ContextServerCommand, ContextServerId}; -use editor::{Editor, EditorElement, EditorStyle}; -use gpui::{ - AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, - Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*, -}; -use language::{Language, LanguageRegistry}; -use markdown::{Markdown, MarkdownElement, MarkdownStyle}; -use notifications::status_toast::{StatusToast, ToastIcon}; -use project::{ - context_server_store::{ - ContextServerStatus, ContextServerStore, registry::ContextServerDescriptorRegistry, - }, - project_settings::{ContextServerSettings, ProjectSettings}, - worktree_store::WorktreeStore, -}; -use serde::Deserialize; -use settings::{Settings as _, update_settings_file}; -use theme::ThemeSettings; -use ui::{ - CommonAnimationExt, KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, - WithScrollbar, prelude::*, -}; -use util::ResultExt as _; -use workspace::{ModalView, Workspace}; - -use crate::AddContextServer; - -enum ConfigurationTarget { - New, - Existing { - id: ContextServerId, - command: ContextServerCommand, - }, - ExistingHttp { - id: ContextServerId, - url: String, - headers: HashMap, - }, - Extension { - id: ContextServerId, - repository_url: Option, - installation: Option, - }, -} - -enum ConfigurationSource { - New { - editor: Entity, - is_http: bool, - }, - Existing { - editor: Entity, - is_http: bool, - }, - Extension { - id: ContextServerId, - editor: Option>, - repository_url: Option, - installation_instructions: Option>, - settings_validator: Option, - }, -} - -impl ConfigurationSource { - fn has_configuration_options(&self) -> bool { - !matches!(self, ConfigurationSource::Extension { editor: None, .. }) - } - - fn is_new(&self) -> bool { - matches!(self, ConfigurationSource::New { .. }) - } - - fn from_target( - target: ConfigurationTarget, - language_registry: Arc, - jsonc_language: Option>, - window: &mut Window, - cx: &mut App, - ) -> Self { - fn create_editor( - json: String, - jsonc_language: Option>, - window: &mut Window, - cx: &mut App, - ) -> Entity { - cx.new(|cx| { - let mut editor = Editor::auto_height(4, 16, window, cx); - editor.set_text(json, window, cx); - editor.set_show_gutter(false, cx); - editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx); - if let Some(buffer) = editor.buffer().read(cx).as_singleton() { - buffer.update(cx, |buffer, cx| buffer.set_language(jsonc_language, cx)) - } - editor - }) - } - - match target { - ConfigurationTarget::New => ConfigurationSource::New { - editor: create_editor(context_server_input(None), jsonc_language, window, cx), - is_http: false, - }, - ConfigurationTarget::Existing { id, command } => ConfigurationSource::Existing { - editor: create_editor( - context_server_input(Some((id, command))), - jsonc_language, - window, - cx, - ), - is_http: false, - }, - ConfigurationTarget::ExistingHttp { - id, - url, - headers: auth, - } => ConfigurationSource::Existing { - editor: create_editor( - context_server_http_input(Some((id, url, auth))), - jsonc_language, - window, - cx, - ), - is_http: true, - }, - ConfigurationTarget::Extension { - id, - repository_url, - installation, - } => { - let settings_validator = installation.as_ref().and_then(|installation| { - jsonschema::validator_for(&installation.settings_schema) - .context("Failed to load JSON schema for context server settings") - .log_err() - }); - let installation_instructions = installation.as_ref().map(|installation| { - cx.new(|cx| { - Markdown::new( - installation.installation_instructions.clone().into(), - Some(language_registry.clone()), - None, - cx, - ) - }) - }); - ConfigurationSource::Extension { - id, - repository_url, - installation_instructions, - settings_validator, - editor: installation.map(|installation| { - create_editor(installation.default_settings, jsonc_language, window, cx) - }), - } - } - } - } - - fn output(&self, cx: &mut App) -> Result<(ContextServerId, ContextServerSettings)> { - match self { - ConfigurationSource::New { editor, is_http } - | ConfigurationSource::Existing { editor, is_http } => { - if *is_http { - parse_http_input(&editor.read(cx).text(cx)).map(|(id, url, auth)| { - ( - id, - ContextServerSettings::Http { - enabled: true, - url, - headers: auth, - }, - ) - }) - } else { - parse_input(&editor.read(cx).text(cx)).map(|(id, command)| { - ( - id, - ContextServerSettings::Stdio { - enabled: true, - command, - }, - ) - }) - } - } - ConfigurationSource::Extension { - id, - editor, - settings_validator, - .. - } => { - let text = editor - .as_ref() - .context("No output available")? - .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())); - } - Ok(( - id.clone(), - ContextServerSettings::Extension { - enabled: true, - settings, - }, - )) - } - } - } -} - -fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)>) -> String { - let (name, command, args, env) = match existing { - Some((id, cmd)) => { - let args = serde_json::to_string(&cmd.args).unwrap(); - let env = serde_json::to_string(&cmd.env.unwrap_or_default()).unwrap(); - let cmd_path = serde_json::to_string(&cmd.path).unwrap(); - (id.0.to_string(), cmd_path, args, env) - } - None => ( - "some-mcp-server".to_string(), - "".to_string(), - "[]".to_string(), - "{}".to_string(), - ), - }; - - format!( - r#"{{ - /// The name of your MCP server - "{name}": {{ - /// The command which runs the MCP server - "command": {command}, - /// The arguments to pass to the MCP server - "args": {args}, - /// The environment variables to set - "env": {env} - }} -}}"# - ) -} - -fn context_server_http_input( - existing: Option<(ContextServerId, String, HashMap)>, -) -> String { - let (name, url, headers) = match existing { - Some((id, url, headers)) => { - let header = if headers.is_empty() { - r#"// "Authorization": "Bearer "#.to_string() - } else { - let json = serde_json::to_string_pretty(&headers).unwrap(); - let mut lines = json.split("\n").collect::>(); - if lines.len() > 1 { - lines.remove(0); - lines.pop(); - } - lines - .into_iter() - .map(|line| format!(" {}", line)) - .collect::() - }; - (id.0.to_string(), url, header) - } - None => ( - "some-remote-server".to_string(), - "https://example.com/mcp".to_string(), - r#"// "Authorization": "Bearer "#.to_string(), - ), - }; - - format!( - r#"{{ - /// The name of your remote MCP server - "{name}": {{ - /// The URL of the remote MCP server - "url": "{url}", - "headers": {{ - /// Any headers to send along - {headers} - }} - }} -}}"# - ) -} - -fn parse_http_input(text: &str) -> Result<(ContextServerId, String, HashMap)> { - #[derive(Deserialize)] - struct Temp { - url: String, - #[serde(default)] - headers: HashMap, - } - let value: HashMap = serde_json_lenient::from_str(text)?; - if value.len() != 1 { - anyhow::bail!("Expected exactly one context server configuration"); - } - - let (key, value) = value.into_iter().next().unwrap(); - - Ok((ContextServerId(key.into()), value.url, value.headers)) -} - -fn resolve_context_server_extension( - id: ContextServerId, - worktree_store: Entity, - cx: &mut App, -) -> Task> { - let registry = ContextServerDescriptorRegistry::default_global(cx).read(cx); - - let Some(descriptor) = registry.context_server_descriptor(&id.0) else { - return Task::ready(None); - }; - - let extension = crate::agent_configuration::resolve_extension_for_context_server(&id, cx); - cx.spawn(async move |cx| { - let installation = descriptor - .configuration(worktree_store, cx) - .await - .context("Failed to resolve context server configuration") - .log_err() - .flatten(); - - Some(ConfigurationTarget::Extension { - id, - repository_url: extension - .and_then(|(_, manifest)| manifest.repository.clone().map(SharedString::from)), - installation, - }) - }) -} - -enum State { - Idle, - Waiting, - Error(SharedString), -} - -pub struct ConfigureContextServerModal { - context_server_store: Entity, - workspace: WeakEntity, - source: ConfigurationSource, - state: State, - original_server_id: Option, - scroll_handle: ScrollHandle, -} - -impl ConfigureContextServerModal { - pub fn register( - workspace: &mut Workspace, - language_registry: Arc, - _window: Option<&mut Window>, - _cx: &mut Context, - ) { - workspace.register_action({ - move |_workspace, _: &AddContextServer, window, cx| { - let workspace_handle = cx.weak_entity(); - let language_registry = language_registry.clone(); - window - .spawn(cx, async move |cx| { - Self::show_modal( - ConfigurationTarget::New, - language_registry, - workspace_handle, - cx, - ) - .await - }) - .detach_and_log_err(cx); - } - }); - } - - pub fn show_modal_for_existing_server( - server_id: ContextServerId, - language_registry: Arc, - workspace: WeakEntity, - window: &mut Window, - cx: &mut App, - ) -> Task> { - let Some(settings) = ProjectSettings::get_global(cx) - .context_servers - .get(&server_id.0) - .cloned() - .or_else(|| { - ContextServerDescriptorRegistry::default_global(cx) - .read(cx) - .context_server_descriptor(&server_id.0) - .map(|_| ContextServerSettings::default_extension()) - }) - else { - return Task::ready(Err(anyhow::anyhow!("Context server not found"))); - }; - - window.spawn(cx, async move |cx| { - let target = match settings { - ContextServerSettings::Stdio { - enabled: _, - command, - } => Some(ConfigurationTarget::Existing { - id: server_id, - command, - }), - ContextServerSettings::Http { - enabled: _, - url, - headers, - } => Some(ConfigurationTarget::ExistingHttp { - id: server_id, - url, - headers, - }), - ContextServerSettings::Extension { .. } => { - match workspace - .update(cx, |workspace, cx| { - resolve_context_server_extension( - server_id, - workspace.project().read(cx).worktree_store(), - cx, - ) - }) - .ok() - { - Some(task) => task.await, - None => None, - } - } - }; - - match target { - Some(target) => Self::show_modal(target, language_registry, workspace, cx).await, - None => Err(anyhow::anyhow!("Failed to resolve context server")), - } - }) - } - - fn show_modal( - target: ConfigurationTarget, - language_registry: Arc, - workspace: WeakEntity, - cx: &mut AsyncWindowContext, - ) -> Task> { - cx.spawn(async move |cx| { - let jsonc_language = language_registry.language_for_name("jsonc").await.ok(); - workspace.update_in(cx, |workspace, window, cx| { - let workspace_handle = cx.weak_entity(); - let context_server_store = workspace.project().read(cx).context_server_store(); - workspace.toggle_modal(window, cx, |window, cx| Self { - context_server_store, - workspace: workspace_handle, - state: State::Idle, - original_server_id: match &target { - ConfigurationTarget::Existing { id, .. } => Some(id.clone()), - ConfigurationTarget::ExistingHttp { id, .. } => Some(id.clone()), - ConfigurationTarget::Extension { id, .. } => Some(id.clone()), - ConfigurationTarget::New => None, - }, - source: ConfigurationSource::from_target( - target, - language_registry, - jsonc_language, - window, - cx, - ), - scroll_handle: ScrollHandle::new(), - }) - }) - }) - } - - fn set_error(&mut self, err: impl Into, cx: &mut Context) { - self.state = State::Error(err.into()); - cx.notify(); - } - - fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context) { - self.state = State::Idle; - let Some(workspace) = self.workspace.upgrade() else { - return; - }; - - let (id, settings) = match self.source.output(cx) { - Ok(val) => val, - Err(error) => { - self.set_error(error.to_string(), cx); - return; - } - }; - - self.state = State::Waiting; - - let existing_server = self.context_server_store.read(cx).get_running_server(&id); - if existing_server.is_some() { - self.context_server_store.update(cx, |store, cx| { - store.stop_server(&id, cx).log_err(); - }); - } - - let wait_for_context_server_task = - wait_for_context_server(&self.context_server_store, id.clone(), cx); - cx.spawn({ - let id = id.clone(); - async move |this, cx| { - let result = wait_for_context_server_task.await; - this.update(cx, |this, cx| match result { - Ok(_) => { - this.state = State::Idle; - this.show_configured_context_server_toast(id, cx); - cx.emit(DismissEvent); - } - Err(err) => { - this.set_error(err, cx); - } - }) - } - }) - .detach(); - - let settings_changed = - ProjectSettings::get_global(cx).context_servers.get(&id.0) != Some(&settings); - - if settings_changed { - // When we write the settings to the file, the context server will be restarted. - workspace.update(cx, |workspace, cx| { - let fs = workspace.app_state().fs.clone(); - let original_server_id = self.original_server_id.clone(); - update_settings_file(fs.clone(), cx, move |current, _| { - if let Some(original_id) = original_server_id { - if original_id != id { - current.project.context_servers.remove(&original_id.0); - } - } - current - .project - .context_servers - .insert(id.0, settings.into()); - }); - }); - } else if let Some(existing_server) = existing_server { - self.context_server_store - .update(cx, |store, cx| store.start_server(existing_server, cx)); - } - } - - fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context) { - cx.emit(DismissEvent); - } - - fn show_configured_context_server_toast(&self, id: ContextServerId, cx: &mut App) { - self.workspace - .update(cx, { - |workspace, cx| { - let status_toast = StatusToast::new( - format!("{} configured successfully.", id.0), - cx, - |this, _cx| { - this.icon(ToastIcon::new(IconName::ToolHammer).color(Color::Muted)) - .action("Dismiss", |_, _| {}) - }, - ); - - workspace.toggle_status_toast(status_toast, cx); - } - }) - .log_err(); - } -} - -fn parse_input(text: &str) -> Result<(ContextServerId, ContextServerCommand)> { - let value: serde_json::Value = serde_json_lenient::from_str(text)?; - let object = value.as_object().context("Expected object")?; - anyhow::ensure!(object.len() == 1, "Expected exactly one key-value pair"); - let (context_server_name, value) = object.into_iter().next().unwrap(); - let command: ContextServerCommand = serde_json::from_value(value.clone())?; - Ok((ContextServerId(context_server_name.clone().into()), command)) -} - -impl ModalView for ConfigureContextServerModal {} - -impl Focusable for ConfigureContextServerModal { - fn focus_handle(&self, cx: &App) -> FocusHandle { - match &self.source { - ConfigurationSource::New { editor, .. } => editor.focus_handle(cx), - ConfigurationSource::Existing { editor, .. } => editor.focus_handle(cx), - ConfigurationSource::Extension { editor, .. } => editor - .as_ref() - .map(|editor| editor.focus_handle(cx)) - .unwrap_or_else(|| cx.focus_handle()), - } - } -} - -impl EventEmitter for ConfigureContextServerModal {} - -impl ConfigureContextServerModal { - fn render_modal_header(&self) -> ModalHeader { - let text: SharedString = match &self.source { - ConfigurationSource::New { .. } => "Add MCP Server".into(), - ConfigurationSource::Existing { .. } => "Configure MCP Server".into(), - ConfigurationSource::Extension { id, .. } => format!("Configure {}", id.0).into(), - }; - ModalHeader::new().headline(text) - } - - 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."; - - if let ConfigurationSource::Extension { - installation_instructions: Some(installation_instructions), - .. - } = &self.source - { - div() - .pb_2() - .text_sm() - .child(MarkdownElement::new( - installation_instructions.clone(), - default_markdown_style(window, cx), - )) - .into_any_element() - } else { - Label::new(MODAL_DESCRIPTION) - .color(Color::Muted) - .into_any_element() - } - } - - fn render_modal_content(&self, cx: &App) -> AnyElement { - let editor = match &self.source { - ConfigurationSource::New { editor, .. } => editor, - ConfigurationSource::Existing { editor, .. } => editor, - ConfigurationSource::Extension { editor, .. } => { - let Some(editor) = editor else { - return div().into_any_element(); - }; - editor - } - }; - - div() - .p_2() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().editor_background) - .child({ - let settings = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color: cx.theme().colors().text, - font_family: settings.buffer_font.family.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: settings.buffer_font_size(cx).into(), - font_weight: settings.buffer_font.weight, - line_height: relative(settings.buffer_line_height.value()), - ..Default::default() - }; - EditorElement::new( - editor, - EditorStyle { - background: cx.theme().colors().editor_background, - local_player: cx.theme().players().local(), - text: text_style, - syntax: cx.theme().syntax().clone(), - ..Default::default() - }, - ) - }) - .into_any_element() - } - - fn render_modal_footer(&self, cx: &mut Context) -> ModalFooter { - let focus_handle = self.focus_handle(cx); - let is_connecting = matches!(self.state, State::Waiting); - - ModalFooter::new() - .start_slot:: - - -
- -
-
-
- -
- Thread 1 of 1: - Default Thread -
- -
- - - - - - - - - - - - -
TurnTextToolResult
- - - - diff --git a/crates/eval/src/explorer.rs b/crates/eval/src/explorer.rs deleted file mode 100644 index 3326070cea..0000000000 --- a/crates/eval/src/explorer.rs +++ /dev/null @@ -1,182 +0,0 @@ -use anyhow::{Context as _, Result}; -use clap::Parser; -use serde_json::{Value, json}; -use std::fs; -use std::path::{Path, PathBuf}; - -#[derive(Parser, Debug)] -#[clap(about = "Generate HTML explorer from JSON thread files")] -struct Args { - /// Paths to JSON files or directories. If a directory is provided, - /// it will be searched for 'last.messages.json' files up to 2 levels deep. - #[clap(long, required = true, num_args = 1..)] - input: Vec, - - /// Path where the output HTML file will be written - #[clap(long)] - output: PathBuf, -} - -/// Recursively finds files with `target_filename` in `dir_path` up to `max_depth`. -#[allow(dead_code)] -fn find_target_files_recursive( - dir_path: &Path, - target_filename: &str, - current_depth: u8, - max_depth: u8, - found_files: &mut Vec, -) -> Result<()> { - if current_depth > max_depth { - return Ok(()); - } - - for entry_result in fs::read_dir(dir_path) - .with_context(|| format!("Failed to read directory: {}", dir_path.display()))? - { - let entry = entry_result.with_context(|| { - format!("Failed to read directory entry in: {}", dir_path.display()) - })?; - let path = entry.path(); - - if path.is_dir() { - find_target_files_recursive( - &path, - target_filename, - current_depth + 1, - 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); - } - } - 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() - ))?; - } - - let template_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/explorer.html"); - let template_content = fs::read_to_string(&template_path).context(format!( - "Template file not found or couldn't be read: {}", - template_path.display() - ))?; - - if input_paths.is_empty() { - println!( - "No input JSON files found to process. Explorer will be generated with template defaults or empty data." - ); - } - - let threads = input_paths - .iter() - .map(|input_path| { - let file_content = fs::read_to_string(input_path) - .context(format!("Failed to read file: {}", input_path.display()))?; - let mut thread_data: Value = file_content - .parse::() - .context(format!("Failed to parse JSON from file: {}", input_path.display()))?; - - if let Some(obj) = thread_data.as_object_mut() { - obj.insert("filename".to_string(), json!(input_path.display().to_string())); - } else { - eprintln!("Warning: JSON data in {} is not a root object. Wrapping it to include filename.", input_path.display()); - thread_data = json!({ - "original_data": thread_data, - "filename": input_path.display().to_string() - }); - } - Ok(thread_data) - }) - .collect::>>()?; - - let all_threads_data = json!({ "threads": threads }); - let html_content = inject_thread_data(template_content, all_threads_data)?; - fs::write(&output_path, &html_content) - .context(format!("Failed to write output: {}", output_path.display()))?; - - println!( - "Saved data from {} resolved file(s) ({} threads) to {}", - input_paths.len(), - threads.len(), - output_path.display() - ); - Ok(html_content) -} - -fn inject_thread_data(template: String, threads_data: Value) -> Result { - let injection_marker = "let threadsData = window.threadsData || { threads: [dummyThread] };"; - if !template.contains(injection_marker) { - anyhow::bail!( - "Could not find the thread injection point in the template. Expected: '{}'", - injection_marker - ); - } - - let threads_json_string = serde_json::to_string_pretty(&threads_data) - .context("Failed to serialize threads data to JSON")? - .replace("", r"<\/script>"); - - let script_injection = format!("let threadsData = {};", threads_json_string); - let final_html = template.replacen(injection_marker, &script_injection, 1); - - Ok(final_html) -} - -#[cfg(not(any(test, doctest)))] -#[allow(dead_code)] -fn main() -> Result<()> { - let args = Args::parse(); - - const DEFAULT_FILENAME: &str = "last.messages.json"; - const MAX_SEARCH_DEPTH: u8 = 2; - - let mut resolved_input_files: Vec = Vec::new(); - - for input_path_arg in &args.input { - if !input_path_arg.exists() { - eprintln!( - "Warning: Input path {} does not exist. Skipping.", - input_path_arg.display() - ); - continue; - } - - if input_path_arg.is_dir() { - find_target_files_recursive( - input_path_arg, - DEFAULT_FILENAME, - 0, // starting depth - MAX_SEARCH_DEPTH, - &mut resolved_input_files, - ) - .with_context(|| { - format!( - "Error searching for '{}' files in directory: {}", - DEFAULT_FILENAME, - input_path_arg.display() - ) - })?; - } else if input_path_arg.is_file() { - resolved_input_files.push(input_path_arg.clone()); - } - } - - resolved_input_files.sort_unstable(); - resolved_input_files.dedup(); - - println!("No input paths provided/found."); - - generate_explorer_html(&resolved_input_files, &args.output).map(|_| ()) -} diff --git a/crates/eval/src/ids.rs b/crates/eval/src/ids.rs deleted file mode 100644 index 7057344206..0000000000 --- a/crates/eval/src/ids.rs +++ /dev/null @@ -1,29 +0,0 @@ -use anyhow::{Context as _, Result}; -use std::fs; -use std::path::{Path, PathBuf}; -use uuid::Uuid; - -pub fn get_or_create_id(path: &Path) -> Result { - if let Ok(id) = fs::read_to_string(path) { - let trimmed = id.trim(); - if !trimmed.is_empty() { - return Ok(trimmed.to_string()); - } - } - let new_id = Uuid::new_v4().to_string(); - fs::create_dir_all(path.parent().context("invalid id path")?)?; - fs::write(path, &new_id)?; - Ok(new_id) -} - -pub fn eval_system_id_path() -> PathBuf { - dirs::data_local_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join("zed-eval-system-id") -} - -pub fn eval_installation_id_path() -> PathBuf { - dirs::data_local_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join("zed-eval-installation-id") -} diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs deleted file mode 100644 index 1af705cd4b..0000000000 --- a/crates/eval/src/instance.rs +++ /dev/null @@ -1,1424 +0,0 @@ -use agent::ContextServerRegistry; -use agent_client_protocol as acp; -use anyhow::{Context as _, Result, anyhow, bail}; -use client::proto::LspWorkProgress; -use futures::channel::mpsc; -use futures::future::Shared; -use futures::{FutureExt as _, StreamExt as _, future}; -use gpui::{App, AppContext as _, AsyncApp, Entity, Task}; -use handlebars::Handlebars; -use language::{Buffer, DiagnosticSeverity, OffsetRangeExt as _}; -use language_model::{ - LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest, - LanguageModelRequestMessage, LanguageModelToolResultContent, MessageContent, Role, TokenUsage, -}; -use project::{DiagnosticSummary, Project, ProjectPath, lsp_store::OpenLspBufferHandle}; -use prompt_store::{ProjectContext, WorktreeContext}; -use rand::{distr, prelude::*}; -use serde::{Deserialize, Serialize}; -use std::{ - fmt::Write as _, - fs::{self, File}, - io::Write as _, - path::{Path, PathBuf}, - rc::Rc, - sync::{Arc, Mutex}, - time::Duration, -}; -use unindent::Unindent as _; -use util::{ResultExt as _, command::new_smol_command, markdown::MarkdownCodeBlock}; - -use crate::{ - AgentAppState, ToolMetrics, - assertions::{AssertionsReport, RanAssertion, RanAssertionResult}, - example::{Example, ExampleContext, FailedAssertion, JudgeAssertion}, -}; - -pub const ZED_REPO_URL: &str = "https://github.com/zed-industries/zed.git"; - -#[derive(Clone)] -pub struct ExampleInstance { - pub thread: Rc, - pub name: String, - pub run_directory: PathBuf, - pub log_prefix: String, - /// The repetition number for this example (0-based) - /// When running multiple repetitions of the same example, each instance is assigned a unique repetition number. - /// This affects the worktree path and log prefix to avoid clobbering results between runs. - pub repetition: usize, - pub repo_path: PathBuf, - /// Path to the directory containing the requests and responses for the agentic loop - worktrees_dir: PathBuf, -} - -#[derive(Debug, Serialize, Clone)] -pub struct RunOutput { - pub repository_diff: String, - pub diagnostic_summary_before: DiagnosticSummary, - pub diagnostic_summary_after: DiagnosticSummary, - pub diagnostics_before: Option, - pub diagnostics_after: Option, - pub token_usage: TokenUsage, - pub tool_metrics: ToolMetrics, - pub thread_markdown: String, - pub programmatic_assertions: AssertionsReport, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct JudgeDiffInput { - pub repository_diff: String, - pub assertion: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct JudgeThreadInput { - pub messages: String, - pub assertion: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct JudgeOutput { - pub thread: AssertionsReport, - pub diff: AssertionsReport, -} - -impl ExampleInstance { - pub fn new( - thread: Rc, - repos_dir: &Path, - run_dir: &Path, - worktrees_dir: &Path, - repetition: usize, - ) -> Self { - let name = thread.meta().name; - let run_directory = run_dir.join(&name).join(repetition.to_string()); - - let repo_path = repo_path_for_url(repos_dir, &thread.meta().url); - - Self { - name, - thread, - log_prefix: String::new(), - run_directory, - repetition, - repo_path, - worktrees_dir: worktrees_dir.to_path_buf(), - } - } - - pub fn repo_url(&self) -> String { - self.thread.meta().url - } - - pub fn revision(&self) -> String { - self.thread.meta().revision - } - - pub fn worktree_name(&self) -> String { - format!("{}-{}", self.name, self.repetition) - } - - pub fn set_log_prefix_style(&mut self, color: &str, name_width: usize) { - self.log_prefix = format!( - "{}{: Result<()> { - let meta = self.thread.meta(); - - let revision_exists = run_git( - &self.repo_path, - &["rev-parse", &format!("{}^{{commit}}", &meta.revision)], - ) - .await - .is_ok(); - - if !revision_exists { - println!("{}Fetching revision {}", self.log_prefix, &meta.revision); - run_git( - &self.repo_path, - &["fetch", "--depth", "1", "origin", &meta.revision], - ) - .await?; - } - Ok(()) - } - - /// Set up the example by checking out the specified Git revision - pub async fn setup(&mut self) -> Result<()> { - let worktree_path = self.worktree_path(); - let meta = self.thread.meta(); - if worktree_path.is_dir() { - println!("{}Resetting existing worktree", self.log_prefix); - - // TODO: consider including "-x" to remove ignored files. The downside of this is that - // it will also remove build artifacts, and so prevent incremental reuse there. - run_git(&worktree_path, &["clean", "--force", "-d"]).await?; - run_git(&worktree_path, &["reset", "--hard", "HEAD"]).await?; - run_git(&worktree_path, &["checkout", &meta.revision]).await?; - } else { - println!("{}Creating worktree", self.log_prefix); - - let worktree_path_string = worktree_path.to_string_lossy().into_owned(); - - run_git( - &self.repo_path, - &[ - "worktree", - "add", - "-f", - &worktree_path_string, - &meta.revision, - ], - ) - .await?; - } - - if meta.url == ZED_REPO_URL { - std::fs::write(worktree_path.join(".rules"), std::fs::read(".rules")?)?; - } - - std::fs::create_dir_all(&self.run_directory)?; - - Ok(()) - } - - pub fn worktree_path(&self) -> PathBuf { - self.worktrees_dir - .join(self.worktree_name()) - .join(self.thread.meta().repo_name()) - } - - pub fn run(&self, app_state: Arc, cx: &mut App) -> Task> { - let project = Project::local( - app_state.client.clone(), - app_state.node_runtime.clone(), - app_state.user_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - None, - cx, - ); - - let worktree = project.update(cx, |project, cx| { - project.create_worktree(self.worktree_path(), true, cx) - }); - - let meta = self.thread.meta(); - let this = self.clone(); - - cx.spawn(async move |cx| { - let worktree = worktree.await?; - - // Wait for worktree scan to finish before choosing a file to open. - worktree - .update(cx, |worktree, _cx| { - worktree.as_local().unwrap().scan_complete() - })? - .await; - - struct LanguageServerState { - _lsp_open_handle: OpenLspBufferHandle, - language_file_buffer: Entity, - } - - let mut diagnostics_before = None; - let mut diagnostic_summary_before = DiagnosticSummary::default(); - - let lsp = if let Some(language_server) = &meta.language_server { - // Open a file that matches the language to cause LSP to start. - let language_file = worktree.read_with(cx, |worktree, _cx| { - worktree - .files(false, 0) - .find_map(|e| { - if e.path.clone().extension() - == Some(&language_server.file_extension) - { - Some(ProjectPath { - worktree_id: worktree.id(), - path: e.path.clone(), - }) - } else { - None - } - }) - .context("Failed to find a file for example language") - })??; - - let open_language_file_buffer_task = project.update(cx, |project, cx| { - project.open_buffer(language_file.clone(), cx) - })?; - - let language_file_buffer = open_language_file_buffer_task.await?; - - let lsp_open_handle = project.update(cx, |project, cx| { - project.register_buffer_with_language_servers(&language_file_buffer, cx) - })?; - - wait_for_lang_server(&project, &language_file_buffer, this.log_prefix.clone(), cx).await?; - - diagnostic_summary_before = project.read_with(cx, |project, cx| { - project.diagnostic_summary(false, cx) - })?; - - diagnostics_before = query_lsp_diagnostics(project.clone(), cx).await?; - if diagnostics_before.is_some() && language_server.allow_preexisting_diagnostics { - anyhow::bail!("Example has pre-existing diagnostics. If you want to run this example regardless, set `allow_preexisting_diagnostics` to `true` in `base.toml`"); - } - - Some(LanguageServerState { - _lsp_open_handle: lsp_open_handle, - language_file_buffer, - }) - } else { - None - }; - - anyhow::ensure!(std::env::var("ZED_EVAL_SETUP_ONLY").is_err(), "Setup only mode"); - - let last_diff_file_path = this.run_directory.join("last.diff"); - - // Write an empty "last.diff" so that it can be opened in Zed for convenient view of the - // history using undo/redo. - std::fs::write(&last_diff_file_path, "")?; - - let thread = cx.update(|cx| { - //todo: Do we want to load rules files here? - let worktrees = project.read(cx).visible_worktrees(cx).map(|worktree| { - let root_name = worktree.read(cx).root_name_str().into(); - let abs_path = worktree.read(cx).abs_path(); - - WorktreeContext { - root_name, - abs_path, - rules_file: None, - } - }).collect::>(); - let project_context = cx.new(|_cx| ProjectContext::new(worktrees, vec![])); - let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - - let thread = if let Some(json) = &meta.existing_thread_json { - let session_id = acp::SessionId::new( - rand::rng() - .sample_iter(&distr::Alphanumeric) - .take(7) - .map(char::from) - .collect::(), - ); - - let db_thread = agent::DbThread::from_json(json.as_bytes()).expect("Can't read serialized thread"); - cx.new(|cx| agent::Thread::from_db(session_id, db_thread, project.clone(), project_context, context_server_registry, agent::Templates::new(), cx)) - } else { - cx.new(|cx| agent::Thread::new(project.clone(), project_context, context_server_registry, agent::Templates::new(), None, cx)) - }; - - thread.update(cx, |thread, cx| { - thread.add_default_tools(Rc::new(EvalThreadEnvironment { - project: project.clone(), - }), cx); - thread.set_profile(meta.profile_id.clone(), cx); - thread.set_model( - LanguageModelInterceptor::new( - LanguageModelRegistry::read_global(cx).default_model().expect("Missing model").model.clone(), - this.run_directory.clone(), - last_diff_file_path.clone(), - this.run_directory.join("last.messages.json"), - this.worktree_path(), - this.repo_url(), - ), - cx, - ); - }); - - thread - }).unwrap(); - - let mut example_cx = ExampleContext::new( - meta.clone(), - this.log_prefix.clone(), - thread.clone(), - cx.clone(), - ); - let result = this.thread.conversation(&mut example_cx).await; - - if let Err(err) = result - && !err.is::() { - return Err(err); - } - - println!("{}Stopped", this.log_prefix); - - println!("{}Getting repository diff", this.log_prefix); - let repository_diff = Self::repository_diff(this.worktree_path(), &this.repo_url()).await?; - - std::fs::write(last_diff_file_path, &repository_diff)?; - - - let mut diagnostics_after = None; - let mut diagnostic_summary_after = Default::default(); - - if let Some(language_server_state) = lsp { - wait_for_lang_server(&project, &language_server_state.language_file_buffer, this.log_prefix.clone(), cx).await?; - - println!("{}Getting diagnostics", this.log_prefix); - diagnostics_after = cx - .update(|cx| { - let project = project.clone(); - cx.spawn(async move |cx| query_lsp_diagnostics(project, cx).await) - })? - .await?; - println!("{}Got diagnostics", this.log_prefix); - - diagnostic_summary_after = project.read_with(cx, |project, cx| { - project.diagnostic_summary(false, cx) - })?; - - } - - if let Some(diagnostics_before) = &diagnostics_before { - fs::write(this.run_directory.join("diagnostics_before.txt"), diagnostics_before)?; - } - - if let Some(diagnostics_after) = &diagnostics_after { - fs::write(this.run_directory.join("diagnostics_after.txt"), diagnostics_after)?; - } - - thread.update(cx, |thread, _cx| { - RunOutput { - repository_diff, - diagnostic_summary_before, - diagnostic_summary_after, - diagnostics_before, - diagnostics_after, - token_usage: thread.latest_request_token_usage().unwrap(), - tool_metrics: example_cx.tool_metrics.lock().unwrap().clone(), - thread_markdown: thread.to_markdown(), - programmatic_assertions: example_cx.assertions, - } - }) - }) - } - - async fn repository_diff(repository_path: PathBuf, repository_url: &str) -> Result { - run_git(&repository_path, &["add", "."]).await?; - let mut diff_args = vec!["diff", "--staged"]; - if repository_url == ZED_REPO_URL { - diff_args.push(":(exclude).rules"); - } - run_git(&repository_path, &diff_args).await - } - - pub async fn judge( - &self, - model: Arc, - run_output: &RunOutput, - cx: &AsyncApp, - ) -> JudgeOutput { - 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_result, thread_result) = futures::join!(diff_task, thread_task); - - let (diff_response, diff_output) = diff_result; - let (thread_response, thread_output) = thread_result; - - writeln!( - &mut output_file, - "# Judgment\n\n## Thread\n\n{thread_response}\n\n## Diff\n\n{diff_response}", - ) - .log_err(); - - JudgeOutput { - thread: thread_output, - diff: diff_output, - } - } - - async fn judge_diff( - &self, - model: Arc, - run_output: &RunOutput, - cx: &AsyncApp, - ) -> (String, AssertionsReport) { - let diff_assertions = self.thread.diff_assertions(); - - if diff_assertions.is_empty() { - return ( - "No diff assertions".to_string(), - AssertionsReport::default(), - ); - } - - println!("{}Running diff judge", self.log_prefix); - - let judge_diff_prompt = include_str!("judge_diff_prompt.hbs"); - let judge_diff_prompt_name = "judge_diff_prompt"; - let mut hbs = Handlebars::new(); - hbs.register_template_string(judge_diff_prompt_name, judge_diff_prompt) - .unwrap(); - - let to_prompt = |assertion: String| { - hbs.render( - judge_diff_prompt_name, - &JudgeDiffInput { - repository_diff: run_output.repository_diff.clone(), - assertion, - }, - ) - .unwrap() - }; - - let (responses, report) = self - .judge_assertions(model, diff_assertions, to_prompt, cx) - .await; - - println!( - "{}Judge - Diff score: {}%", - self.log_prefix, - report.passed_percentage() - ); - - (responses, report) - } - - async fn judge_thread( - &self, - model: Arc, - run_output: &RunOutput, - cx: &AsyncApp, - ) -> (String, AssertionsReport) { - let thread_assertions = self.thread.thread_assertions(); - - if thread_assertions.is_empty() { - return ( - "No thread assertions".to_string(), - AssertionsReport::default(), - ); - } - - let judge_thread_prompt = include_str!("judge_thread_prompt.hbs"); - let judge_thread_prompt_name = "judge_thread_prompt"; - let mut hbs = Handlebars::new(); - hbs.register_template_string(judge_thread_prompt_name, judge_thread_prompt) - .unwrap(); - - let complete_messages = &run_output.thread_markdown; - let to_prompt = |assertion: String| { - hbs.render( - judge_thread_prompt_name, - &JudgeThreadInput { - messages: complete_messages.clone(), - assertion, - }, - ) - .unwrap() - }; - - let (responses, report) = self - .judge_assertions(model, thread_assertions, to_prompt, cx) - .await; - - println!( - "{}Judge - Thread score: {}%", - self.log_prefix, - report.passed_percentage() - ); - - (responses, report) - } - - async fn judge_assertions( - &self, - model: Arc, - assertions: Vec, - to_prompt: impl Fn(String) -> String, - cx: &AsyncApp, - ) -> (String, AssertionsReport) { - let assertions = assertions.into_iter().map(|assertion| { - let request = LanguageModelRequest { - thread_id: None, - prompt_id: None, - mode: None, - intent: None, - messages: vec![LanguageModelRequestMessage { - role: Role::User, - content: vec![MessageContent::Text(to_prompt(assertion.description))], - cache: false, - reasoning_details: None, - }], - temperature: None, - tools: Vec::new(), - tool_choice: None, - stop: Vec::new(), - thinking_allowed: true, - }; - - let model = model.clone(); - let log_prefix = self.log_prefix.clone(); - async move { - let response = send_language_model_request(model, request, cx).await; - - let (response, result) = match response { - Ok(response) => ( - response.clone(), - parse_assertion_result(&response).map_err(|err| err.to_string()), - ), - Err(err) => (err.to_string(), Err(err.to_string())), - }; - - if result.is_ok() { - println!("{}✅ {}", log_prefix, assertion.id); - } else { - println!("{}❌ {}", log_prefix, assertion.id); - } - - ( - response, - RanAssertion { - id: assertion.id, - result, - }, - ) - } - }); - - let mut responses = String::new(); - let mut report = AssertionsReport::default(); - - for (response, assertion) in future::join_all(assertions).await { - writeln!(&mut responses, "# {}", assertion.id).unwrap(); - writeln!(&mut responses, "{}\n\n", response).unwrap(); - report.ran.push(assertion); - } - - (responses, report) - } -} - -struct EvalThreadEnvironment { - project: Entity, -} - -struct EvalTerminalHandle { - terminal: Entity, -} - -impl agent::TerminalHandle for EvalTerminalHandle { - fn id(&self, cx: &AsyncApp) -> Result { - self.terminal.read_with(cx, |term, _cx| term.id().clone()) - } - - fn wait_for_exit(&self, cx: &AsyncApp) -> Result>> { - self.terminal - .read_with(cx, |term, _cx| term.wait_for_exit()) - } - - fn current_output(&self, cx: &AsyncApp) -> Result { - self.terminal - .read_with(cx, |term, cx| term.current_output(cx)) - } -} - -impl agent::ThreadEnvironment for EvalThreadEnvironment { - fn create_terminal( - &self, - command: String, - cwd: Option, - output_byte_limit: Option, - cx: &mut AsyncApp, - ) -> Task>> { - let project = self.project.clone(); - cx.spawn(async move |cx| { - let language_registry = - project.read_with(cx, |project, _cx| project.languages().clone())?; - let id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string()); - let terminal = - acp_thread::create_terminal_entity(command, &[], vec![], cwd.clone(), &project, cx) - .await?; - let terminal = cx.new(|cx| { - acp_thread::Terminal::new( - id, - "", - cwd, - output_byte_limit.map(|limit| limit as usize), - terminal, - language_registry, - cx, - ) - })?; - Ok(Rc::new(EvalTerminalHandle { terminal }) as Rc) - }) - } -} - -struct LanguageModelInterceptor { - model: Arc, - request_count: Arc>, - previous_diff: Arc>, - example_output_dir: PathBuf, - last_diff_file_path: PathBuf, - messages_json_file_path: PathBuf, - repository_path: PathBuf, - repository_url: String, -} - -impl LanguageModelInterceptor { - fn new( - model: Arc, - example_output_dir: PathBuf, - last_diff_file_path: PathBuf, - messages_json_file_path: PathBuf, - repository_path: PathBuf, - repository_url: String, - ) -> Arc { - Arc::new(Self { - model, - request_count: Arc::new(Mutex::new(0)), - previous_diff: Arc::new(Mutex::new("".to_string())), - example_output_dir, - last_diff_file_path, - messages_json_file_path, - repository_path, - repository_url, - }) - } -} - -impl language_model::LanguageModel for LanguageModelInterceptor { - fn id(&self) -> language_model::LanguageModelId { - self.model.id() - } - - fn name(&self) -> language_model::LanguageModelName { - self.model.name() - } - - fn provider_id(&self) -> language_model::LanguageModelProviderId { - self.model.provider_id() - } - - fn provider_name(&self) -> language_model::LanguageModelProviderName { - self.model.provider_name() - } - - fn telemetry_id(&self) -> String { - self.model.telemetry_id() - } - - fn supports_images(&self) -> bool { - self.model.supports_images() - } - - fn supports_tools(&self) -> bool { - self.model.supports_tools() - } - - fn supports_tool_choice(&self, choice: language_model::LanguageModelToolChoice) -> bool { - self.model.supports_tool_choice(choice) - } - - fn max_token_count(&self) -> u64 { - self.model.max_token_count() - } - - fn count_tokens( - &self, - request: LanguageModelRequest, - cx: &App, - ) -> future::BoxFuture<'static, Result> { - self.model.count_tokens(request, cx) - } - - fn stream_completion( - &self, - request: LanguageModelRequest, - cx: &AsyncApp, - ) -> future::BoxFuture< - 'static, - Result< - futures::stream::BoxStream< - 'static, - Result, - >, - language_model::LanguageModelCompletionError, - >, - > { - let stream = self.model.stream_completion(request.clone(), cx); - let request_count = self.request_count.clone(); - let previous_diff = self.previous_diff.clone(); - let example_output_dir = self.example_output_dir.clone(); - let last_diff_file_path = self.last_diff_file_path.clone(); - let messages_json_file_path = self.messages_json_file_path.clone(); - let repository_path = self.repository_path.clone(); - let repository_url = self.repository_url.clone(); - - Box::pin(async move { - let stream = stream.await?; - - let response_events = Arc::new(Mutex::new(Vec::new())); - let request_clone = request.clone(); - - let wrapped_stream = stream.then(move |event| { - let response_events = response_events.clone(); - let request = request_clone.clone(); - let request_count = request_count.clone(); - let previous_diff = previous_diff.clone(); - let example_output_dir = example_output_dir.clone(); - let last_diff_file_path = last_diff_file_path.clone(); - let messages_json_file_path = messages_json_file_path.clone(); - let repository_path = repository_path.clone(); - let repository_url = repository_url.clone(); - - async move { - let event_result = match &event { - Ok(ev) => Ok(ev.clone()), - Err(err) => Err(err.to_string()), - }; - response_events.lock().unwrap().push(event_result); - - let should_execute = matches!( - &event, - Ok(LanguageModelCompletionEvent::Stop { .. }) | Err(_) - ); - - if should_execute { - let current_request_count = { - let mut count = request_count.lock().unwrap(); - *count += 1; - *count - }; - - let messages_file_path = - example_output_dir.join(format!("{current_request_count}.messages.md")); - let diff_file_path = - example_output_dir.join(format!("{current_request_count}.diff")); - let last_messages_file_path = example_output_dir.join("last.messages.md"); - - let collected_events = response_events.lock().unwrap().clone(); - let request_markdown = RequestMarkdown::new(&request); - let response_events_markdown = - response_events_to_markdown(&collected_events); - let dialog = ThreadDialog::new(&request, &collected_events); - let dialog_json = - serde_json::to_string_pretty(&dialog.to_combined_request()) - .unwrap_or_default(); - - let messages = format!( - "{}\n\n{}", - request_markdown.messages, response_events_markdown - ); - fs::write(&messages_file_path, messages.clone()) - .expect("failed to write messages file"); - fs::write(&last_messages_file_path, messages) - .expect("failed to write last messages file"); - fs::write(&messages_json_file_path, dialog_json) - .expect("failed to write last.messages.json"); - - // Get repository diff - let diff_result = - ExampleInstance::repository_diff(repository_path, &repository_url) - .await; - - match diff_result { - Ok(diff) => { - let prev_diff = previous_diff.lock().unwrap().clone(); - if diff != prev_diff { - fs::write(&diff_file_path, &diff) - .expect("failed to write diff file"); - fs::write(&last_diff_file_path, &diff) - .expect("failed to write last diff file"); - *previous_diff.lock().unwrap() = diff; - } - } - Err(err) => { - let error_message = format!("{err:?}"); - fs::write(&diff_file_path, &error_message) - .expect("failed to write diff error to file"); - fs::write(&last_diff_file_path, &error_message) - .expect("failed to write last diff file"); - } - } - - if current_request_count == 1 { - let tools_file_path = example_output_dir.join("tools.md"); - fs::write(tools_file_path, request_markdown.tools) - .expect("failed to write tools file"); - } - } - - event - } - }); - - Ok(Box::pin(wrapped_stream) - as futures::stream::BoxStream< - 'static, - Result< - LanguageModelCompletionEvent, - language_model::LanguageModelCompletionError, - >, - >) - }) - } -} - -pub fn wait_for_lang_server( - project: &Entity, - buffer: &Entity, - log_prefix: String, - cx: &mut AsyncApp, -) -> Task> { - if std::env::var("ZED_EVAL_SKIP_LS").is_ok() { - return Task::ready(Ok(())); - } - - println!("{}⏵ Waiting for language server", log_prefix); - - let (mut tx, mut rx) = mpsc::channel(1); - - let lsp_store = project - .read_with(cx, |project, _| project.lsp_store()) - .unwrap(); - - let has_lang_server = buffer - .update(cx, |buffer, cx| { - lsp_store.update(cx, |lsp_store, cx| { - lsp_store - .running_language_servers_for_local_buffer(buffer, cx) - .next() - .is_some() - }) - }) - .unwrap_or(false); - - if has_lang_server { - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .unwrap() - .detach(); - } - - let subscriptions = - [ - cx.subscribe(&lsp_store, { - let log_prefix = log_prefix.clone(); - move |_, event, _| { - if let project::LspStoreEvent::LanguageServerUpdate { - message: - client::proto::update_language_server::Variant::WorkProgress( - LspWorkProgress { - message: Some(message), - .. - }, - ), - .. - } = event - { - println!("{}⟲ {message}", log_prefix) - } - } - }), - cx.subscribe(project, { - let buffer = buffer.clone(); - move |project, event, cx| match event { - project::Event::LanguageServerAdded(_, _, _) => { - let buffer = buffer.clone(); - project - .update(cx, |project, cx| project.save_buffer(buffer, cx)) - .detach(); - } - project::Event::DiskBasedDiagnosticsFinished { .. } => { - tx.try_send(()).ok(); - } - _ => {} - } - }), - ]; - - cx.spawn(async move |cx| { - let timeout = cx.background_executor().timer(Duration::new(60 * 5, 0)); - let result = futures::select! { - _ = rx.next() => { - println!("{}⚑ Language server idle", log_prefix); - anyhow::Ok(()) - }, - _ = timeout.fuse() => { - anyhow::bail!("LSP wait timed out after 5 minutes"); - } - }; - drop(subscriptions); - result - }) -} - -pub async fn query_lsp_diagnostics( - project: Entity, - cx: &mut AsyncApp, -) -> Result> { - let paths_with_diagnostics = project.update(cx, |project, cx| { - project - .diagnostic_summaries(true, cx) - .filter(|(_, _, summary)| summary.error_count > 0 || summary.warning_count > 0) - .map(|(project_path, _, _)| project_path) - .collect::>() - })?; - - if paths_with_diagnostics.is_empty() { - return Ok(None); - } - - let mut output = String::new(); - for project_path in paths_with_diagnostics { - let buffer = project - .update(cx, |project, cx| project.open_buffer(project_path, cx))? - .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 - )?; - } - } - anyhow::Ok(Some(output)) -} - -fn parse_assertion_result(response: &str) -> Result { - let analysis = get_tag("analysis", response)?; - let passed = match get_tag("passed", response)?.to_lowercase().as_str() { - "true" => true, - "false" => false, - value @ _ => bail!("invalid judge `passed` tag: {value}"), - }; - Ok(RanAssertionResult { - analysis: Some(analysis), - passed, - }) -} - -fn get_tag(name: &'static str, response: &str) -> Result { - let start_tag = format!("<{}>", name); - let end_tag = format!("", name); - - let start_ix = response - .find(&start_tag) - .context(format!("{} start tag not found", name))?; - let content_start_ix = start_ix + start_tag.len(); - - let end_ix = content_start_ix - + response[content_start_ix..] - .find(&end_tag) - .context(format!("{} end tag not found", name))?; - - let content = response[content_start_ix..end_ix].trim().unindent(); - - anyhow::Ok(content) -} - -pub fn repo_path_for_url(repos_dir: &Path, repo_url: &str) -> PathBuf { - let repo_name = repo_url - .trim_start_matches("https://") - .replace(|c: char| !c.is_alphanumeric(), "-"); - Path::new(repos_dir).join(repo_name) -} - -pub async fn run_git(repo_path: &Path, args: &[&str]) -> Result { - let output = new_smol_command("git") - .current_dir(repo_path) - .args(args) - .output() - .await?; - - anyhow::ensure!( - output.status.success(), - "`git {}` within `{}` failed with status: {}\nstderr:\n{}\nstdout:\n{}", - args.join(" "), - repo_path.display(), - output.status, - String::from_utf8_lossy(&output.stderr), - String::from_utf8_lossy(&output.stdout), - ); - Ok(String::from_utf8(output.stdout)?.trim().to_string()) -} - -fn push_role(role: &Role, buf: &mut String, assistant_message_number: &mut u32) { - match role { - Role::System => buf.push_str("# ⚙️ SYSTEM\n\n"), - Role::User => buf.push_str("# 👤 USER\n\n"), - Role::Assistant => { - buf.push_str(&format!("# 🤖 ASSISTANT {assistant_message_number}\n\n")); - *assistant_message_number = *assistant_message_number + 1; - } - } -} - -pub async fn send_language_model_request( - model: Arc, - request: LanguageModelRequest, - cx: &AsyncApp, -) -> anyhow::Result { - 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 { - match chunk_result { - Ok(chunk_str) => { - full_response.push_str(&chunk_str); - } - Err(err) => { - anyhow::bail!("Error receiving response from language model: {err}"); - } - } - } - Ok(full_response) - } - Err(err) => Err(anyhow!( - "Failed to get response from language model. Error was: {err}" - )), - } -} - -pub struct RequestMarkdown { - pub tools: String, - pub messages: String, -} - -impl RequestMarkdown { - pub fn new(request: &LanguageModelRequest) -> Self { - let mut tools = String::new(); - let mut messages = String::new(); - let mut assistant_message_number: u32 = 1; - - // Print the tools - if !request.tools.is_empty() { - for tool in &request.tools { - write!(&mut tools, "# {}\n\n", tool.name).unwrap(); - write!(&mut tools, "{}\n\n", tool.description).unwrap(); - writeln!( - &mut tools, - "{}", - MarkdownCodeBlock { - tag: "json", - text: &format!("{:#}", tool.input_schema) - } - ) - .unwrap(); - } - } - - // Print the messages - for message in &request.messages { - push_role(&message.role, &mut messages, &mut assistant_message_number); - - for content in &message.content { - match content { - MessageContent::Text(text) => { - messages.push_str(text); - messages.push_str("\n\n"); - } - MessageContent::Image(_) => { - messages.push_str("[IMAGE DATA]\n\n"); - } - MessageContent::Thinking { text, signature } => { - messages.push_str("**Thinking**:\n\n"); - if let Some(sig) = signature { - messages.push_str(&format!("Signature: {}\n\n", sig)); - } - messages.push_str(text); - messages.push_str("\n"); - } - MessageContent::RedactedThinking(items) => { - messages.push_str(&format!( - "**Redacted Thinking**: {} item(s)\n\n", - items.len() - )); - } - MessageContent::ToolUse(tool_use) => { - messages.push_str(&format!( - "**Tool Use**: {} (ID: {})\n", - tool_use.name, tool_use.id - )); - messages.push_str(&format!( - "{}\n", - MarkdownCodeBlock { - tag: "json", - text: &format!("{:#}", tool_use.input) - } - )); - } - MessageContent::ToolResult(tool_result) => { - messages.push_str(&format!( - "**Tool Result**: {} (ID: {})\n\n", - tool_result.tool_name, tool_result.tool_use_id - )); - if tool_result.is_error { - messages.push_str("**ERROR:**\n"); - } - - match &tool_result.content { - LanguageModelToolResultContent::Text(text) => { - writeln!(messages, "{text}\n").ok(); - } - LanguageModelToolResultContent::Image(image) => { - writeln!(messages, "![Image](data:base64,{})\n", image.source).ok(); - } - } - - if let Some(output) = tool_result.output.as_ref() { - writeln!( - messages, - "**Debug Output**:\n\n```json\n{}\n```\n", - serde_json::to_string_pretty(output).unwrap() - ) - .unwrap(); - } - } - } - } - } - - Self { tools, messages } - } -} - -pub fn response_events_to_markdown( - response_events: &[std::result::Result], -) -> String { - let mut response = String::new(); - // Print the response events if any - response.push_str("# Response\n\n"); - let mut text_buffer = String::new(); - let mut thinking_buffer = String::new(); - - let flush_buffers = - |output: &mut String, text_buffer: &mut String, thinking_buffer: &mut String| { - if !text_buffer.is_empty() { - output.push_str(&format!("**Text**:\n{}\n\n", text_buffer)); - text_buffer.clear(); - } - if !thinking_buffer.is_empty() { - output.push_str(&format!("**Thinking**:\n{}\n\n", thinking_buffer)); - thinking_buffer.clear(); - } - }; - - for event in response_events { - match event { - Ok(LanguageModelCompletionEvent::Text(text)) => { - text_buffer.push_str(text); - } - Ok(LanguageModelCompletionEvent::Thinking { text, .. }) => { - thinking_buffer.push_str(text); - } - Ok(LanguageModelCompletionEvent::RedactedThinking { .. }) => {} - Ok(LanguageModelCompletionEvent::Stop(reason)) => { - flush_buffers(&mut response, &mut text_buffer, &mut thinking_buffer); - response.push_str(&format!("**Stop**: {:?}\n\n", reason)); - } - Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) => { - flush_buffers(&mut response, &mut text_buffer, &mut thinking_buffer); - response.push_str(&format!( - "**Tool Use**: {} (ID: {})\n", - tool_use.name, tool_use.id - )); - response.push_str(&format!( - "{}\n", - MarkdownCodeBlock { - tag: "json", - text: &format!("{:#}", tool_use.input) - } - )); - } - Ok( - LanguageModelCompletionEvent::UsageUpdate(_) - | LanguageModelCompletionEvent::ToolUseLimitReached - | LanguageModelCompletionEvent::StartMessage { .. } - | LanguageModelCompletionEvent::UsageUpdated { .. } - | LanguageModelCompletionEvent::Queued { .. } - | LanguageModelCompletionEvent::Started - | LanguageModelCompletionEvent::ReasoningDetails(_), - ) => {} - Ok(LanguageModelCompletionEvent::ToolUseJsonParseError { - json_parse_error, .. - }) => { - flush_buffers(&mut response, &mut text_buffer, &mut thinking_buffer); - response.push_str(&format!( - "**Error**: parse error in tool use JSON: {}\n\n", - json_parse_error - )); - } - Err(error) => { - flush_buffers(&mut response, &mut text_buffer, &mut thinking_buffer); - response.push_str(&format!("**Error**: {}\n\n", error)); - } - } - } - - flush_buffers(&mut response, &mut text_buffer, &mut thinking_buffer); - - response -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct ThreadDialog { - pub request: LanguageModelRequest, - pub response_events: Vec>, -} - -impl ThreadDialog { - pub fn new( - request: &LanguageModelRequest, - response_events: &[std::result::Result], - ) -> Self { - Self { - request: request.clone(), - response_events: response_events.to_vec(), - } - } - - /// Represents all request and response messages in a unified format. - /// - /// Specifically, it appends the assistant's response (derived from response events) - /// as a new message to existing messages in the request. - pub fn to_combined_request(&self) -> LanguageModelRequest { - let mut request = self.request.clone(); - if let Some(assistant_message) = self.response_events_to_message() { - request.messages.push(assistant_message); - } - request - } - fn response_events_to_message(&self) -> Option { - let response_events = &self.response_events; - let mut content: Vec = Vec::new(); - let mut current_text = String::new(); - - let flush_text = |text: &mut String, content: &mut Vec| { - if !text.is_empty() { - content.push(MessageContent::Text(std::mem::take(text))); - } - }; - - for event in response_events { - match event { - Ok(LanguageModelCompletionEvent::Text(text)) => { - current_text.push_str(text); - } - - Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) => { - flush_text(&mut current_text, &mut content); - if tool_use.is_input_complete { - content.push(MessageContent::ToolUse(tool_use.clone())); - } - } - Ok(LanguageModelCompletionEvent::Thinking { text, signature }) => { - flush_text(&mut current_text, &mut content); - content.push(MessageContent::Thinking { - text: text.clone(), - signature: signature.clone(), - }); - } - - // Skip these - Ok(LanguageModelCompletionEvent::UsageUpdate(_)) - | Ok(LanguageModelCompletionEvent::RedactedThinking { .. }) - | Ok(LanguageModelCompletionEvent::StartMessage { .. }) - | Ok(LanguageModelCompletionEvent::ReasoningDetails(_)) - | Ok(LanguageModelCompletionEvent::Stop(_)) - | Ok(LanguageModelCompletionEvent::Queued { .. }) - | Ok(LanguageModelCompletionEvent::Started) - | Ok(LanguageModelCompletionEvent::UsageUpdated { .. }) - | Ok(LanguageModelCompletionEvent::ToolUseLimitReached) => {} - - Ok(LanguageModelCompletionEvent::ToolUseJsonParseError { - json_parse_error, - .. - }) => { - flush_text(&mut current_text, &mut content); - content.push(MessageContent::Text(format!( - "ERROR: parse error in tool use JSON: {}", - json_parse_error - ))); - } - - Err(error) => { - flush_text(&mut current_text, &mut content); - content.push(MessageContent::Text(format!("ERROR: {}", error))); - } - } - } - - flush_text(&mut current_text, &mut content); - - if !content.is_empty() { - Some(LanguageModelRequestMessage { - role: Role::Assistant, - content, - cache: false, - reasoning_details: None, - }) - } else { - None - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_parse_judge_output() { - let response = r#" - The model did a good job but there were still compilations errors. - true - "# - .unindent(); - - let output = parse_assertion_result(&response).unwrap(); - assert_eq!( - output.analysis, - Some("The model did a good job but there were still compilations errors.".into()) - ); - assert!(output.passed); - - let response = r#" - Text around ignored - - - Failed to compile: - - Error 1 - - Error 2 - - - false - "# - .unindent(); - - let output = parse_assertion_result(&response).unwrap(); - assert_eq!( - output.analysis, - Some("Failed to compile:\n- Error 1\n- Error 2".into()) - ); - assert!(!output.passed); - } -} diff --git a/crates/eval/src/judge_diff_prompt.hbs b/crates/eval/src/judge_diff_prompt.hbs deleted file mode 100644 index 24ef9ac97e..0000000000 --- a/crates/eval/src/judge_diff_prompt.hbs +++ /dev/null @@ -1,25 +0,0 @@ -You are an expert software developer. Your task is to evaluate a diff produced by an AI agent -in response to a prompt. Here is the prompt and the diff: - - -{{{prompt}}} - - - -{{{repository_diff}}} - - -Evaluate whether or not the diff passes the following assertion: - - -{{assertion}} - - -Analyze the diff hunk by hunk, and structure your answer in the following XML format: - -``` -{YOUR ANALYSIS HERE} -{PASSED_ASSERTION} -``` - -Where `PASSED_ASSERTION` is either `true` or `false`. diff --git a/crates/eval/src/judge_thread_prompt.hbs b/crates/eval/src/judge_thread_prompt.hbs deleted file mode 100644 index e80bafcce1..0000000000 --- a/crates/eval/src/judge_thread_prompt.hbs +++ /dev/null @@ -1,21 +0,0 @@ -You are an expert software developer. -Your task is to evaluate an AI agent's messages and tool calls in this conversation: - - -{{{messages}}} - - -Evaluate whether or not the sequence of messages passes the following assertion: - - -{{{assertion}}} - - -Analyze the messages one by one, and structure your answer in the following XML format: - -``` -{YOUR ANALYSIS HERE} -{PASSED_ASSERTION} -``` - -Where `PASSED_ASSERTION` is either `true` or `false`. diff --git a/crates/eval/src/tool_metrics.rs b/crates/eval/src/tool_metrics.rs deleted file mode 100644 index 63d8a4f2bc..0000000000 --- a/crates/eval/src/tool_metrics.rs +++ /dev/null @@ -1,106 +0,0 @@ -use collections::HashMap; -use serde::{Deserialize, Serialize}; -use std::{fmt::Display, sync::Arc}; - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct ToolMetrics { - pub use_counts: HashMap, u32>, - pub failure_counts: HashMap, u32>, -} - -impl ToolMetrics { - pub fn insert(&mut self, tool_name: Arc, succeeded: bool) { - *self.use_counts.entry(tool_name.clone()).or_insert(0) += 1; - if !succeeded { - *self.failure_counts.entry(tool_name).or_insert(0) += 1; - } - } - - pub fn merge(&mut self, other: &ToolMetrics) { - for (tool_name, use_count) in &other.use_counts { - *self.use_counts.entry(tool_name.clone()).or_insert(0) += use_count; - } - for (tool_name, failure_count) in &other.failure_counts { - *self.failure_counts.entry(tool_name.clone()).or_insert(0) += failure_count; - } - } - - pub fn is_empty(&self) -> bool { - self.use_counts.is_empty() && self.failure_counts.is_empty() - } -} - -impl Display for ToolMetrics { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut failure_rates: Vec<(Arc, f64)> = Vec::new(); - - for (tool_name, use_count) in &self.use_counts { - let failure_count = self.failure_counts.get(tool_name).cloned().unwrap_or(0); - if *use_count > 0 { - let failure_rate = failure_count as f64 / *use_count as f64; - failure_rates.push((tool_name.clone(), failure_rate)); - } - } - - // Sort by failure rate descending - failure_rates.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); - - // Table dimensions - let tool_width = 30; - let count_width = 10; - let rate_width = 10; - - // Write table top border - writeln!( - f, - "┌{}┬{}┬{}┬{}┐", - "─".repeat(tool_width), - "─".repeat(count_width), - "─".repeat(count_width), - "─".repeat(rate_width) - )?; - - // Write header row - writeln!( - f, - "│{:^30}│{:^10}│{:^10}│{:^10}│", - "Tool", "Uses", "Failures", "Rate" - )?; - - // Write header-data separator - writeln!( - f, - "├{}┼{}┼{}┼{}┤", - "─".repeat(tool_width), - "─".repeat(count_width), - "─".repeat(count_width), - "─".repeat(rate_width) - )?; - - // Write data rows - for (tool_name, failure_rate) in failure_rates { - let use_count = self.use_counts.get(&tool_name).cloned().unwrap_or(0); - let failure_count = self.failure_counts.get(&tool_name).cloned().unwrap_or(0); - writeln!( - f, - "│{:<30}│{:^10}│{:^10}│{:^10}│", - tool_name, - use_count, - failure_count, - format!("{}%", (failure_rate * 100.0).round()) - )?; - } - - // Write table bottom border - writeln!( - f, - "└{}┴{}┴{}┴{}┘", - "─".repeat(tool_width), - "─".repeat(count_width), - "─".repeat(count_width), - "─".repeat(rate_width) - )?; - - Ok(()) - } -} diff --git a/crates/eval_utils/Cargo.toml b/crates/eval_utils/Cargo.toml deleted file mode 100644 index a512035f5d..0000000000 --- a/crates/eval_utils/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "eval_utils" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/eval_utils.rs" -doctest = false - -[dependencies] -gpui.workspace = true -serde.workspace = true -smol.workspace = true diff --git a/crates/eval_utils/LICENSE-GPL b/crates/eval_utils/LICENSE-GPL deleted file mode 120000 index e0f9dbd5d6..0000000000 --- a/crates/eval_utils/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -LICENSE-GPL \ No newline at end of file diff --git a/crates/eval_utils/README.md b/crates/eval_utils/README.md deleted file mode 100644 index 617077a815..0000000000 --- a/crates/eval_utils/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# eval_utils - -Utilities for evals of agents. diff --git a/crates/eval_utils/src/eval_utils.rs b/crates/eval_utils/src/eval_utils.rs deleted file mode 100644 index 880b1a97e4..0000000000 --- a/crates/eval_utils/src/eval_utils.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! Utilities for evaluation and benchmarking. - -use std::{ - collections::HashMap, - sync::{Arc, mpsc}, -}; - -fn report_progress(evaluated_count: usize, failed_count: usize, iterations: usize) { - let passed_count = evaluated_count - failed_count; - let passed_ratio = if evaluated_count == 0 { - 0.0 - } else { - passed_count as f64 / evaluated_count as f64 - }; - println!( - "\r\x1b[KEvaluated {}/{} ({:.2}% passed)", - evaluated_count, - iterations, - passed_ratio * 100.0 - ) -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum OutcomeKind { - Passed, - Failed, - Error, -} - -pub trait EvalOutputProcessor { - type Metadata: 'static + Send; - fn process(&mut self, output: &EvalOutput); - fn assert(&mut self); -} - -#[derive(Clone, Debug)] -pub struct EvalOutput { - pub outcome: OutcomeKind, - pub data: String, - pub metadata: M, -} - -pub struct NoProcessor; -impl EvalOutputProcessor for NoProcessor { - type Metadata = (); - - fn process(&mut self, _output: &EvalOutput) {} - - fn assert(&mut self) {} -} - -pub fn eval

( - iterations: usize, - expected_pass_ratio: f32, - mut processor: P, - evalf: impl Fn() -> EvalOutput + Send + Sync + 'static, -) where - P: EvalOutputProcessor, -{ - let mut evaluated_count = 0; - let mut failed_count = 0; - let evalf = Arc::new(evalf); - report_progress(evaluated_count, failed_count, iterations); - - let (tx, rx) = mpsc::channel(); - - let executor = gpui::background_executor(); - let semaphore = Arc::new(smol::lock::Semaphore::new(32)); - let evalf = Arc::new(evalf); - // Warm the cache once - let first_output = evalf(); - tx.send(first_output).ok(); - - for _ in 1..iterations { - let tx = tx.clone(); - let semaphore = semaphore.clone(); - let evalf = evalf.clone(); - executor - .spawn(async move { - let _guard = semaphore.acquire().await; - let output = evalf(); - tx.send(output).ok(); - }) - .detach(); - } - drop(tx); - - let mut failed_evals = Vec::new(); - let mut errored_evals = HashMap::new(); - while let Ok(output) = rx.recv() { - processor.process(&output); - - match output.outcome { - OutcomeKind::Passed => {} - OutcomeKind::Failed => { - failed_count += 1; - failed_evals.push(output); - } - OutcomeKind::Error => { - failed_count += 1; - *errored_evals.entry(output.data).or_insert(0) += 1; - } - } - - evaluated_count += 1; - report_progress(evaluated_count, failed_count, iterations); - } - - let actual_pass_ratio = (iterations - failed_count) as f32 / iterations as f32; - println!("Actual pass ratio: {}\n", actual_pass_ratio); - if actual_pass_ratio < expected_pass_ratio { - for (error, count) in errored_evals { - println!("Eval errored {} times. Error: {}", count, error); - } - - for failed in failed_evals { - println!("Eval failed"); - println!("{}", failed.data); - } - - panic!( - "Actual pass ratio: {}\nExpected pass ratio: {}", - actual_pass_ratio, expected_pass_ratio - ); - } - - processor.assert(); -} diff --git a/crates/explorer_command_injector/AppxManifest-Nightly.xml b/crates/explorer_command_injector/AppxManifest-Nightly.xml deleted file mode 100644 index 32c33d2b0c..0000000000 --- a/crates/explorer_command_injector/AppxManifest-Nightly.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - Zed Nightly - Zed Industries - - resources\logo_150x150.png - true - disabled - disabled - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/crates/explorer_command_injector/AppxManifest-Preview.xml b/crates/explorer_command_injector/AppxManifest-Preview.xml deleted file mode 100644 index 2904653fb2..0000000000 --- a/crates/explorer_command_injector/AppxManifest-Preview.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - Zed Preview - Zed Industries - - resources\logo_150x150.png - true - disabled - disabled - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/crates/explorer_command_injector/AppxManifest.xml b/crates/explorer_command_injector/AppxManifest.xml deleted file mode 100644 index adf563fbc0..0000000000 --- a/crates/explorer_command_injector/AppxManifest.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - Zed - - Zed Industries - - resources\logo_150x150.png - true - disabled - disabled - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/crates/explorer_command_injector/Cargo.toml b/crates/explorer_command_injector/Cargo.toml deleted file mode 100644 index 8530329358..0000000000 --- a/crates/explorer_command_injector/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "explorer_command_injector" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -crate-type = ["cdylib"] -path = "src/explorer_command_injector.rs" -doctest = false - -[features] -default = ["nightly"] -stable = [] -preview = [] -nightly = [] - -[target.'cfg(target_os = "windows")'.dependencies] -windows.workspace = true -windows-core.workspace = true -windows-registry = "0.5" - -[dependencies] diff --git a/crates/explorer_command_injector/LICENSE-GPL b/crates/explorer_command_injector/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/explorer_command_injector/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/explorer_command_injector/src/explorer_command_injector.rs b/crates/explorer_command_injector/src/explorer_command_injector.rs deleted file mode 100644 index bfa2a0326c..0000000000 --- a/crates/explorer_command_injector/src/explorer_command_injector.rs +++ /dev/null @@ -1,202 +0,0 @@ -#![cfg(target_os = "windows")] - -use std::{os::windows::ffi::OsStringExt, path::PathBuf}; - -use windows::{ - Win32::{ - Foundation::{ - CLASS_E_CLASSNOTAVAILABLE, E_FAIL, E_INVALIDARG, E_NOTIMPL, ERROR_INSUFFICIENT_BUFFER, - GetLastError, HINSTANCE, MAX_PATH, - }, - Globalization::u_strlen, - System::{ - Com::{IBindCtx, IClassFactory, IClassFactory_Impl}, - LibraryLoader::GetModuleFileNameW, - SystemServices::DLL_PROCESS_ATTACH, - }, - UI::Shell::{ - ECF_DEFAULT, ECS_ENABLED, IEnumExplorerCommand, IExplorerCommand, - IExplorerCommand_Impl, IShellItemArray, SHStrDupW, SIGDN_FILESYSPATH, - }, - }, - core::{BOOL, GUID, HRESULT, HSTRING, Interface, Ref, Result, implement}, -}; - -static mut DLL_INSTANCE: HINSTANCE = HINSTANCE(std::ptr::null_mut()); - -#[unsafe(no_mangle)] -extern "system" fn DllMain( - hinstdll: HINSTANCE, - fdwreason: u32, - _lpvreserved: *mut core::ffi::c_void, -) -> bool { - if fdwreason == DLL_PROCESS_ATTACH { - unsafe { DLL_INSTANCE = hinstdll }; - } - - true -} - -#[implement(IExplorerCommand)] -struct ExplorerCommandInjector; - -#[allow(non_snake_case)] -impl IExplorerCommand_Impl for ExplorerCommandInjector_Impl { - fn GetTitle(&self, _: Ref) -> Result { - let command_description = - retrieve_command_description().unwrap_or(HSTRING::from("Open with Zed")); - unsafe { SHStrDupW(&command_description) } - } - - fn GetIcon(&self, _: Ref) -> Result { - let Some(zed_exe) = get_zed_exe_path() else { - return Err(E_FAIL.into()); - }; - unsafe { SHStrDupW(&HSTRING::from(zed_exe)) } - } - - fn GetToolTip(&self, _: Ref) -> Result { - Err(E_NOTIMPL.into()) - } - - fn GetCanonicalName(&self) -> Result { - Ok(GUID::zeroed()) - } - - fn GetState(&self, _: Ref, _: BOOL) -> Result { - Ok(ECS_ENABLED.0 as _) - } - - fn Invoke(&self, psiitemarray: Ref, _: Ref) -> Result<()> { - let items = psiitemarray.ok()?; - let Some(zed_exe) = get_zed_exe_path() else { - return Ok(()); - }; - - let count = unsafe { items.GetCount()? }; - for idx in 0..count { - let item = unsafe { items.GetItemAt(idx)? }; - let item_path = unsafe { item.GetDisplayName(SIGDN_FILESYSPATH)?.to_string()? }; - #[allow(clippy::disallowed_methods, reason = "no async context in sight..")] - std::process::Command::new(&zed_exe) - .arg(&item_path) - .spawn() - .map_err(|_| E_INVALIDARG)?; - } - - Ok(()) - } - - fn GetFlags(&self) -> Result { - Ok(ECF_DEFAULT.0 as _) - } - - fn EnumSubCommands(&self) -> Result { - Err(E_NOTIMPL.into()) - } -} - -#[implement(IClassFactory)] -struct ExplorerCommandInjectorFactory; - -impl IClassFactory_Impl for ExplorerCommandInjectorFactory_Impl { - fn CreateInstance( - &self, - punkouter: Ref, - riid: *const windows_core::GUID, - ppvobject: *mut *mut core::ffi::c_void, - ) -> Result<()> { - unsafe { - *ppvobject = std::ptr::null_mut(); - } - if punkouter.is_none() { - let factory: IExplorerCommand = ExplorerCommandInjector {}.into(); - let ret = unsafe { factory.query(riid, ppvobject).ok() }; - if ret.is_ok() { - unsafe { - *ppvobject = factory.into_raw(); - } - } - ret - } else { - Err(E_INVALIDARG.into()) - } - } - - fn LockServer(&self, _: BOOL) -> Result<()> { - Ok(()) - } -} - -#[cfg(all(feature = "stable", not(feature = "preview"), not(feature = "nightly")))] -const MODULE_ID: GUID = GUID::from_u128(0x6a1f6b13_3b82_48a1_9e06_7bb0a6d0bffd); -#[cfg(all(feature = "preview", not(feature = "stable"), not(feature = "nightly")))] -const MODULE_ID: GUID = GUID::from_u128(0xaf8e85ea_fb20_4db2_93cf_56513c1ec697); -#[cfg(all(feature = "nightly", not(feature = "stable"), not(feature = "preview")))] -const MODULE_ID: GUID = GUID::from_u128(0x266f2cfe_1653_42af_b55c_fe3590c83871); - -// Make cargo clippy happy -#[cfg(all(feature = "nightly", feature = "stable", feature = "preview"))] -const MODULE_ID: GUID = GUID::from_u128(0x685f4d49_6718_4c55_b271_ebb5c6a48d6f); - -#[unsafe(no_mangle)] -extern "system" fn DllGetClassObject( - class_id: *const GUID, - iid: *const GUID, - out: *mut *mut std::ffi::c_void, -) -> HRESULT { - unsafe { - *out = std::ptr::null_mut(); - } - let class_id = unsafe { *class_id }; - if class_id == MODULE_ID { - let instance: IClassFactory = ExplorerCommandInjectorFactory {}.into(); - let ret = unsafe { instance.query(iid, out) }; - if ret.is_ok() { - unsafe { - *out = instance.into_raw(); - } - } - ret - } else { - CLASS_E_CLASSNOTAVAILABLE - } -} - -fn get_zed_install_folder() -> Option { - let mut buf = vec![0u16; MAX_PATH as usize]; - unsafe { GetModuleFileNameW(Some(DLL_INSTANCE.into()), &mut buf) }; - - while unsafe { GetLastError() } == ERROR_INSUFFICIENT_BUFFER { - buf = vec![0u16; buf.len() * 2]; - unsafe { GetModuleFileNameW(Some(DLL_INSTANCE.into()), &mut buf) }; - } - let len = unsafe { u_strlen(buf.as_ptr()) }; - let path: PathBuf = std::ffi::OsString::from_wide(&buf[..len as usize]) - .into_string() - .ok()? - .into(); - Some(path.parent()?.parent()?.to_path_buf()) -} - -#[inline] -fn get_zed_exe_path() -> Option { - get_zed_install_folder().map(|path| path.join("Zed.exe").to_string_lossy().into_owned()) -} - -#[inline] -fn retrieve_command_description() -> Result { - #[cfg(all(feature = "stable", not(feature = "preview"), not(feature = "nightly")))] - const REG_PATH: &str = "Software\\Classes\\ZedEditorContextMenu"; - #[cfg(all(feature = "preview", not(feature = "stable"), not(feature = "nightly")))] - const REG_PATH: &str = "Software\\Classes\\ZedEditorPreviewContextMenu"; - #[cfg(all(feature = "nightly", not(feature = "stable"), not(feature = "preview")))] - const REG_PATH: &str = "Software\\Classes\\ZedEditorNightlyContextMenu"; - - // Make cargo clippy happy - #[cfg(all(feature = "nightly", feature = "stable", feature = "preview"))] - const REG_PATH: &str = "Software\\Classes\\ZedEditorClippyContextMenu"; - - let key = windows_registry::CURRENT_USER.open(REG_PATH)?; - key.get_hstring("Title") -} diff --git a/crates/extension/Cargo.toml b/crates/extension/Cargo.toml deleted file mode 100644 index 307a3a19bd..0000000000 --- a/crates/extension/Cargo.toml +++ /dev/null @@ -1,44 +0,0 @@ -[package] -name = "extension" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/extension.rs" - -[dependencies] -anyhow.workspace = true -async-trait.workspace = true -collections.workspace = true -dap.workspace = true -fs.workspace = true -futures.workspace = true -gpui.workspace = true -heck.workspace = true -http_client.workspace = true -language.workspace = true -log.workspace = true -lsp.workspace = true -parking_lot.workspace = true -proto.workspace = true -semver.workspace = true -serde.workspace = true -serde_json.workspace = true -task.workspace = true -toml.workspace = true -url.workspace = true -util.workspace = true -wasm-encoder.workspace = true -wasmparser.workspace = true - -[dev-dependencies] -fs = { workspace = true, "features" = ["test-support"] } -gpui = { workspace = true, "features" = ["test-support"] } -indoc.workspace = true -pretty_assertions.workspace = true -tempfile.workspace = true diff --git a/crates/extension/LICENSE-GPL b/crates/extension/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/extension/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/extension/src/capabilities.rs b/crates/extension/src/capabilities.rs deleted file mode 100644 index b8afc4ec06..0000000000 --- a/crates/extension/src/capabilities.rs +++ /dev/null @@ -1,20 +0,0 @@ -mod download_file_capability; -mod npm_install_package_capability; -mod process_exec_capability; - -pub use download_file_capability::*; -pub use npm_install_package_capability::*; -pub use process_exec_capability::*; - -use serde::{Deserialize, Serialize}; - -/// A capability for an extension. -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum ExtensionCapability { - #[serde(rename = "process:exec")] - ProcessExec(ProcessExecCapability), - DownloadFile(DownloadFileCapability), - #[serde(rename = "npm:install")] - NpmInstallPackage(NpmInstallPackageCapability), -} diff --git a/crates/extension/src/capabilities/download_file_capability.rs b/crates/extension/src/capabilities/download_file_capability.rs deleted file mode 100644 index a76755b593..0000000000 --- a/crates/extension/src/capabilities/download_file_capability.rs +++ /dev/null @@ -1,121 +0,0 @@ -use serde::{Deserialize, Serialize}; -use url::Url; - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub struct DownloadFileCapability { - pub host: String, - pub path: Vec, -} - -impl DownloadFileCapability { - /// Returns whether the capability allows downloading a file from the given URL. - pub fn allows(&self, url: &Url) -> bool { - let Some(desired_host) = url.host_str() else { - return false; - }; - - let Some(desired_path) = url.path_segments() else { - return false; - }; - let desired_path = desired_path.collect::>(); - - if self.host != desired_host && self.host != "*" { - return false; - } - - for (ix, path_segment) in self.path.iter().enumerate() { - if path_segment == "**" { - return true; - } - - if ix >= desired_path.len() { - return false; - } - - if path_segment != "*" && path_segment != desired_path[ix] { - return false; - } - } - - if self.path.len() < desired_path.len() { - return false; - } - - true - } -} - -#[cfg(test)] -mod tests { - use pretty_assertions::assert_eq; - - use super::*; - - #[test] - fn test_allows() { - let capability = DownloadFileCapability { - host: "*".to_string(), - path: vec!["**".to_string()], - }; - assert_eq!( - capability.allows(&"https://example.com/some/path".parse().unwrap()), - true - ); - - let capability = DownloadFileCapability { - host: "github.com".to_string(), - path: vec!["**".to_string()], - }; - assert_eq!( - capability.allows(&"https://github.com/some-owner/some-repo".parse().unwrap()), - true - ); - assert_eq!( - capability.allows( - &"https://fake-github.com/some-owner/some-repo" - .parse() - .unwrap() - ), - false - ); - - let capability = DownloadFileCapability { - host: "github.com".to_string(), - path: vec!["specific-owner".to_string(), "*".to_string()], - }; - assert_eq!( - capability.allows(&"https://github.com/some-owner/some-repo".parse().unwrap()), - false - ); - assert_eq!( - capability.allows( - &"https://github.com/specific-owner/some-repo" - .parse() - .unwrap() - ), - true - ); - - let capability = DownloadFileCapability { - host: "github.com".to_string(), - path: vec!["specific-owner".to_string(), "*".to_string()], - }; - assert_eq!( - capability.allows( - &"https://github.com/some-owner/some-repo/extra" - .parse() - .unwrap() - ), - false - ); - assert_eq!( - capability.allows( - &"https://github.com/specific-owner/some-repo/extra" - .parse() - .unwrap() - ), - false - ); - } -} diff --git a/crates/extension/src/capabilities/npm_install_package_capability.rs b/crates/extension/src/capabilities/npm_install_package_capability.rs deleted file mode 100644 index 287645fc75..0000000000 --- a/crates/extension/src/capabilities/npm_install_package_capability.rs +++ /dev/null @@ -1,39 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub struct NpmInstallPackageCapability { - pub package: String, -} - -impl NpmInstallPackageCapability { - /// Returns whether the capability allows installing the given NPM package. - pub fn allows(&self, package: &str) -> bool { - self.package == "*" || self.package == package - } -} - -#[cfg(test)] -mod tests { - use pretty_assertions::assert_eq; - - use super::*; - - #[test] - fn test_allows() { - let capability = NpmInstallPackageCapability { - package: "*".to_string(), - }; - assert_eq!(capability.allows("package"), true); - - let capability = NpmInstallPackageCapability { - package: "react".to_string(), - }; - assert_eq!(capability.allows("react"), true); - - let capability = NpmInstallPackageCapability { - package: "react".to_string(), - }; - assert_eq!(capability.allows("malicious-package"), false); - } -} diff --git a/crates/extension/src/capabilities/process_exec_capability.rs b/crates/extension/src/capabilities/process_exec_capability.rs deleted file mode 100644 index 053a7b212b..0000000000 --- a/crates/extension/src/capabilities/process_exec_capability.rs +++ /dev/null @@ -1,116 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub struct ProcessExecCapability { - /// The command to execute. - pub command: String, - /// The arguments to pass to the command. Use `*` for a single wildcard argument. - /// If the last element is `**`, then any trailing arguments are allowed. - pub args: Vec, -} - -impl ProcessExecCapability { - /// Returns whether the capability allows the given command and arguments. - pub fn allows( - &self, - desired_command: &str, - desired_args: &[impl AsRef + std::fmt::Debug], - ) -> bool { - if self.command != desired_command && self.command != "*" { - return false; - } - - for (ix, arg) in self.args.iter().enumerate() { - if arg == "**" { - return true; - } - - if ix >= desired_args.len() { - return false; - } - - if arg != "*" && arg != desired_args[ix].as_ref() { - return false; - } - } - - if self.args.len() < desired_args.len() { - return false; - } - - true - } -} - -#[cfg(test)] -mod tests { - use pretty_assertions::assert_eq; - - use super::*; - - #[test] - fn test_allows_with_exact_match() { - let capability = ProcessExecCapability { - command: "ls".to_string(), - args: vec!["-la".to_string()], - }; - - assert_eq!(capability.allows("ls", &["-la"]), true); - assert_eq!(capability.allows("ls", &["-l"]), false); - assert_eq!(capability.allows("pwd", &[] as &[&str]), false); - } - - #[test] - fn test_allows_with_wildcard_arg() { - let capability = ProcessExecCapability { - command: "git".to_string(), - args: vec!["*".to_string()], - }; - - assert_eq!(capability.allows("git", &["status"]), true); - assert_eq!(capability.allows("git", &["commit"]), true); - // Too many args. - assert_eq!(capability.allows("git", &["status", "-s"]), false); - // Wrong command. - assert_eq!(capability.allows("npm", &["install"]), false); - } - - #[test] - fn test_allows_with_double_wildcard() { - let capability = ProcessExecCapability { - command: "cargo".to_string(), - args: vec!["test".to_string(), "**".to_string()], - }; - - assert_eq!(capability.allows("cargo", &["test"]), true); - assert_eq!(capability.allows("cargo", &["test", "--all"]), true); - assert_eq!( - capability.allows("cargo", &["test", "--all", "--no-fail-fast"]), - true - ); - // Wrong first arg. - assert_eq!(capability.allows("cargo", &["build"]), false); - } - - #[test] - fn test_allows_with_mixed_wildcards() { - let capability = ProcessExecCapability { - command: "docker".to_string(), - args: vec!["run".to_string(), "*".to_string(), "**".to_string()], - }; - - assert_eq!(capability.allows("docker", &["run", "nginx"]), true); - assert_eq!(capability.allows("docker", &["run"]), false); - assert_eq!( - capability.allows("docker", &["run", "ubuntu", "bash"]), - true - ); - assert_eq!( - capability.allows("docker", &["run", "alpine", "sh", "-c", "echo hello"]), - true - ); - // Wrong first arg. - assert_eq!(capability.allows("docker", &["ps"]), false); - } -} diff --git a/crates/extension/src/extension.rs b/crates/extension/src/extension.rs deleted file mode 100644 index 88f2bea0c0..0000000000 --- a/crates/extension/src/extension.rs +++ /dev/null @@ -1,210 +0,0 @@ -mod capabilities; -pub mod extension_builder; -mod extension_events; -mod extension_host_proxy; -mod extension_manifest; -mod types; - -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use ::lsp::LanguageServerName; -use anyhow::{Context as _, Result, bail}; -use async_trait::async_trait; -use fs::normalize_path; -use gpui::{App, Task}; -use language::LanguageName; -use semver::Version; -use task::{SpawnInTerminal, ZedDebugConfig}; -use util::rel_path::RelPath; - -pub use crate::capabilities::*; -pub use crate::extension_events::*; -pub use crate::extension_host_proxy::*; -pub use crate::extension_manifest::*; -pub use crate::types::*; - -/// Initializes the `extension` crate. -pub fn init(cx: &mut App) { - extension_events::init(cx); - ExtensionHostProxy::default_global(cx); -} - -#[async_trait] -pub trait WorktreeDelegate: Send + Sync + 'static { - fn id(&self) -> u64; - fn root_path(&self) -> String; - async fn read_text_file(&self, path: &RelPath) -> Result; - async fn which(&self, binary_name: String) -> Option; - async fn shell_env(&self) -> Vec<(String, String)>; -} - -pub trait ProjectDelegate: Send + Sync + 'static { - fn worktree_ids(&self) -> Vec; -} - -pub trait KeyValueStoreDelegate: Send + Sync + 'static { - fn insert(&self, key: String, docs: String) -> Task>; -} - -#[async_trait] -pub trait Extension: Send + Sync + 'static { - /// Returns the [`ExtensionManifest`] for this extension. - fn manifest(&self) -> Arc; - - /// Returns the path to this extension's working directory. - fn work_dir(&self) -> Arc; - - /// Returns a path relative to this extension's working directory. - fn path_from_extension(&self, path: &Path) -> PathBuf { - normalize_path(&self.work_dir().join(path)) - } - - async fn language_server_command( - &self, - language_server_id: LanguageServerName, - language_name: LanguageName, - worktree: Arc, - ) -> Result; - - async fn language_server_initialization_options( - &self, - language_server_id: LanguageServerName, - language_name: LanguageName, - worktree: Arc, - ) -> Result>; - - async fn language_server_workspace_configuration( - &self, - language_server_id: LanguageServerName, - worktree: Arc, - ) -> Result>; - - async fn language_server_additional_initialization_options( - &self, - language_server_id: LanguageServerName, - target_language_server_id: LanguageServerName, - worktree: Arc, - ) -> Result>; - - async fn language_server_additional_workspace_configuration( - &self, - language_server_id: LanguageServerName, - target_language_server_id: LanguageServerName, - worktree: Arc, - ) -> Result>; - - async fn labels_for_completions( - &self, - language_server_id: LanguageServerName, - completions: Vec, - ) -> Result>>; - - async fn labels_for_symbols( - &self, - language_server_id: LanguageServerName, - symbols: Vec, - ) -> Result>>; - - async fn complete_slash_command_argument( - &self, - command: SlashCommand, - arguments: Vec, - ) -> Result>; - - async fn run_slash_command( - &self, - command: SlashCommand, - arguments: Vec, - worktree: Option>, - ) -> Result; - - async fn context_server_command( - &self, - context_server_id: Arc, - project: Arc, - ) -> Result; - - async fn context_server_configuration( - &self, - context_server_id: Arc, - project: Arc, - ) -> Result>; - - async fn suggest_docs_packages(&self, provider: Arc) -> Result>; - - async fn index_docs( - &self, - provider: Arc, - package_name: Arc, - kv_store: Arc, - ) -> Result<()>; - - async fn get_dap_binary( - &self, - dap_name: Arc, - config: DebugTaskDefinition, - user_installed_path: Option, - worktree: Arc, - ) -> Result; - - async fn dap_request_kind( - &self, - dap_name: Arc, - config: serde_json::Value, - ) -> Result; - - async fn dap_config_to_scenario(&self, config: ZedDebugConfig) -> Result; - - async fn dap_locator_create_scenario( - &self, - locator_name: String, - build_config_template: BuildTaskTemplate, - resolved_label: String, - debug_adapter_name: String, - ) -> Result>; - async fn run_dap_locator( - &self, - locator_name: String, - config: SpawnInTerminal, - ) -> Result; -} - -pub fn parse_wasm_extension_version(extension_id: &str, wasm_bytes: &[u8]) -> Result { - let mut version = None; - - 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() - ); - } - } - } - - // The reason we wait until we're done parsing all of the Wasm bytes to return the version - // is to work around a panic that can happen inside of Wasmtime when the bytes are invalid. - // - // By parsing the entirety of the Wasm bytes before we return, we're able to detect this problem - // earlier as an `Err` rather than as a panic. - version.with_context(|| format!("extension {extension_id} has no zed:api-version section")) -} - -fn parse_wasm_extension_version_custom_section(data: &[u8]) -> Option { - if data.len() == 6 { - Some(Version::new( - u16::from_be_bytes([data[0], data[1]]) as _, - u16::from_be_bytes([data[2], data[3]]) as _, - u16::from_be_bytes([data[4], data[5]]) as _, - )) - } else { - None - } -} diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs deleted file mode 100644 index 8b9bf994d1..0000000000 --- a/crates/extension/src/extension_builder.rs +++ /dev/null @@ -1,826 +0,0 @@ -use crate::{ - ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, build_debug_adapter_schema_path, - parse_wasm_extension_version, -}; -use ::fs::Fs; -use anyhow::{Context as _, Result, bail}; -use futures::{AsyncReadExt, StreamExt}; -use heck::ToSnakeCase; -use http_client::{self, AsyncBody, HttpClient}; -use serde::Deserialize; -use std::{ - env, fs, mem, - path::{Path, PathBuf}, - process::Stdio, - str::FromStr, - sync::Arc, -}; -use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _}; -use wasmparser::Parser; - -/// Currently, we compile with Rust's `wasm32-wasip2` target, which works with WASI `preview2` and the component model. -const RUST_TARGET: &str = "wasm32-wasip2"; - -/// Compiling Tree-sitter parsers from C to WASM requires Clang 17, and a WASM build of libc -/// and clang's runtime library. The `wasi-sdk` provides these binaries. -/// -/// Once Clang 17 and its wasm target are available via system package managers, we won't need -/// to download this. -const WASI_SDK_URL: &str = "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/"; -const WASI_SDK_ASSET_NAME: Option<&str> = if cfg!(all(target_os = "macos", target_arch = "x86_64")) -{ - Some("wasi-sdk-25.0-x86_64-macos.tar.gz") -} else if cfg!(all(target_os = "macos", target_arch = "aarch64")) { - Some("wasi-sdk-25.0-arm64-macos.tar.gz") -} else if cfg!(all(target_os = "linux", target_arch = "x86_64")) { - Some("wasi-sdk-25.0-x86_64-linux.tar.gz") -} else if cfg!(all(target_os = "linux", target_arch = "aarch64")) { - Some("wasi-sdk-25.0-arm64-linux.tar.gz") -} else if cfg!(all(target_os = "freebsd", target_arch = "x86_64")) { - Some("wasi-sdk-25.0-x86_64-linux.tar.gz") -} else if cfg!(all(target_os = "freebsd", target_arch = "aarch64")) { - Some("wasi-sdk-25.0-arm64-linux.tar.gz") -} else if cfg!(all(target_os = "windows", target_arch = "x86_64")) { - Some("wasi-sdk-25.0-x86_64-windows.tar.gz") -} else { - None -}; - -pub struct ExtensionBuilder { - cache_dir: PathBuf, - pub http: Arc, -} - -pub struct CompileExtensionOptions { - pub release: bool, -} - -#[derive(Deserialize)] -struct CargoToml { - package: CargoTomlPackage, -} - -#[derive(Deserialize)] -struct CargoTomlPackage { - name: String, -} - -impl ExtensionBuilder { - pub fn new(http_client: Arc, cache_dir: PathBuf) -> Self { - Self { - cache_dir, - http: http_client, - } - } - - pub async fn compile_extension( - &self, - extension_dir: &Path, - extension_manifest: &mut ExtensionManifest, - options: CompileExtensionOptions, - fs: Arc, - ) -> Result<()> { - populate_defaults(extension_manifest, extension_dir, fs).await?; - - if extension_dir.is_relative() { - bail!( - "extension dir {} is not an absolute path", - extension_dir.display() - ); - } - - fs::create_dir_all(&self.cache_dir).context("failed to create cache dir")?; - - if extension_manifest.lib.kind == Some(ExtensionLibraryKind::Rust) { - log::info!("compiling Rust extension {}", extension_dir.display()); - self.compile_rust_extension(extension_dir, extension_manifest, options) - .await - .context("failed to compile Rust extension")?; - log::info!("compiled Rust extension {}", extension_dir.display()); - } - - for (debug_adapter_name, meta) in &mut extension_manifest.debug_adapters { - let debug_adapter_schema_path = - extension_dir.join(build_debug_adapter_schema_path(debug_adapter_name, meta)); - - let debug_adapter_schema = fs::read_to_string(&debug_adapter_schema_path) - .with_context(|| { - format!("failed to read debug adapter schema for `{debug_adapter_name}` from `{debug_adapter_schema_path:?}`") - })?; - _ = serde_json::Value::from_str(&debug_adapter_schema).with_context(|| { - format!("Debug adapter schema for `{debug_adapter_name}` (path: `{debug_adapter_schema_path:?}`) is not a valid JSON") - })?; - } - for (grammar_name, grammar_metadata) in &extension_manifest.grammars { - let snake_cased_grammar_name = grammar_name.to_snake_case(); - if grammar_name.as_ref() != snake_cased_grammar_name.as_str() { - bail!( - "grammar name '{grammar_name}' must be written in snake_case: {snake_cased_grammar_name}" - ); - } - - log::info!( - "compiling grammar {grammar_name} for extension {}", - extension_dir.display() - ); - self.compile_grammar(extension_dir, grammar_name.as_ref(), grammar_metadata) - .await - .with_context(|| format!("failed to compile grammar '{grammar_name}'"))?; - log::info!( - "compiled grammar {grammar_name} for extension {}", - extension_dir.display() - ); - } - - log::info!("finished compiling extension {}", extension_dir.display()); - Ok(()) - } - - async fn compile_rust_extension( - &self, - extension_dir: &Path, - manifest: &mut ExtensionManifest, - options: CompileExtensionOptions, - ) -> anyhow::Result<()> { - self.install_rust_wasm_target_if_needed().await?; - - let cargo_toml_content = fs::read_to_string(extension_dir.join("Cargo.toml"))?; - let cargo_toml: CargoToml = toml::from_str(&cargo_toml_content)?; - - log::info!( - "compiling Rust crate for extension {}", - extension_dir.display() - ); - let output = util::command::new_smol_command("cargo") - .args(["build", "--target", RUST_TARGET]) - .args(options.release.then_some("--release")) - .arg("--target-dir") - .arg(extension_dir.join("target")) - // WASI builds do not work with sccache and just stuck, so disable it. - .env("RUSTC_WRAPPER", "") - .current_dir(extension_dir) - .output() - .await - .context("failed to run `cargo`")?; - if !output.status.success() { - bail!( - "failed to build extension {}", - String::from_utf8_lossy(&output.stderr) - ); - } - - log::info!( - "compiled Rust crate for extension {}", - extension_dir.display() - ); - - let mut wasm_path = PathBuf::from(extension_dir); - wasm_path.extend([ - "target", - RUST_TARGET, - if options.release { "release" } else { "debug" }, - &cargo_toml - .package - .name - // The wasm32-wasip2 target normalizes `-` in package names to `_` in the resulting `.wasm` file. - .replace('-', "_"), - ]); - wasm_path.set_extension("wasm"); - - log::info!( - "encoding wasm component for extension {}", - extension_dir.display() - ); - - let component_bytes = fs::read(&wasm_path) - .with_context(|| format!("failed to read output module `{}`", wasm_path.display()))?; - - let component_bytes = self - .strip_custom_sections(&component_bytes) - .context("failed to strip debug sections from wasm component")?; - - let wasm_extension_api_version = - parse_wasm_extension_version(&manifest.id, &component_bytes) - .context("compiled wasm did not contain a valid zed extension api version")?; - manifest.lib.version = Some(wasm_extension_api_version); - - let extension_file = extension_dir.join("extension.wasm"); - fs::write(extension_file.clone(), &component_bytes) - .context("failed to write extension.wasm")?; - - log::info!( - "extension {} written to {}", - extension_dir.display(), - extension_file.display() - ); - - Ok(()) - } - - async fn compile_grammar( - &self, - extension_dir: &Path, - grammar_name: &str, - grammar_metadata: &GrammarManifestEntry, - ) -> Result<()> { - let clang_path = self.install_wasi_sdk_if_needed().await?; - - let mut grammar_repo_dir = extension_dir.to_path_buf(); - grammar_repo_dir.extend(["grammars", grammar_name]); - - let mut grammar_wasm_path = grammar_repo_dir.clone(); - grammar_wasm_path.set_extension("wasm"); - - log::info!("checking out {grammar_name} parser"); - self.checkout_repo( - &grammar_repo_dir, - &grammar_metadata.repository, - &grammar_metadata.rev, - ) - .await?; - - let base_grammar_path = grammar_metadata - .path - .as_ref() - .map(|path| grammar_repo_dir.join(path)) - .unwrap_or(grammar_repo_dir); - - let src_path = base_grammar_path.join("src"); - let parser_path = src_path.join("parser.c"); - let scanner_path = src_path.join("scanner.c"); - - // Skip recompiling if the WASM object is already newer than the source files - if file_newer_than_deps(&grammar_wasm_path, &[&parser_path, &scanner_path]).unwrap_or(false) - { - log::info!( - "skipping compilation of {grammar_name} parser because the existing compiled grammar is up to date" - ); - } else { - log::info!("compiling {grammar_name} parser"); - let clang_output = util::command::new_smol_command(&clang_path) - .args(["-fPIC", "-shared", "-Os"]) - .arg(format!("-Wl,--export=tree_sitter_{grammar_name}")) - .arg("-o") - .arg(&grammar_wasm_path) - .arg("-I") - .arg(&src_path) - .arg(&parser_path) - .args(scanner_path.exists().then_some(scanner_path)) - .output() - .await - .context("failed to run clang")?; - - if !clang_output.status.success() { - bail!( - "failed to compile {} parser with clang: {}", - grammar_name, - String::from_utf8_lossy(&clang_output.stderr), - ); - } - } - - Ok(()) - } - - async fn checkout_repo(&self, directory: &Path, url: &str, rev: &str) -> Result<()> { - let git_dir = directory.join(".git"); - - if directory.exists() { - let remotes_output = util::command::new_smol_command("git") - .arg("--git-dir") - .arg(&git_dir) - .args(["remote", "-v"]) - .output() - .await?; - let has_remote = remotes_output.status.success() - && String::from_utf8_lossy(&remotes_output.stdout) - .lines() - .any(|line| { - let mut parts = line.split(|c: char| c.is_whitespace()); - parts.next() == Some("origin") && parts.any(|part| part == url) - }); - if !has_remote { - bail!( - "grammar directory '{}' already exists, but is not a git clone of '{}'", - directory.display(), - url - ); - } - } else { - fs::create_dir_all(directory).with_context(|| { - format!("failed to create grammar directory {}", directory.display(),) - })?; - let init_output = util::command::new_smol_command("git") - .arg("init") - .current_dir(directory) - .output() - .await?; - if !init_output.status.success() { - bail!( - "failed to run `git init` in directory '{}'", - directory.display() - ); - } - - let remote_add_output = util::command::new_smol_command("git") - .arg("--git-dir") - .arg(&git_dir) - .args(["remote", "add", "origin", url]) - .output() - .await - .context("failed to execute `git remote add`")?; - if !remote_add_output.status.success() { - bail!( - "failed to add remote {url} for git repository {}", - git_dir.display() - ); - } - } - - let fetch_output = util::command::new_smol_command("git") - .arg("--git-dir") - .arg(&git_dir) - .args(["fetch", "--depth", "1", "origin", rev]) - .output() - .await - .context("failed to execute `git fetch`")?; - - let checkout_output = util::command::new_smol_command("git") - .arg("--git-dir") - .arg(&git_dir) - .args(["checkout", rev]) - .current_dir(directory) - .output() - .await - .context("failed to execute `git checkout`")?; - if !checkout_output.status.success() { - if !fetch_output.status.success() { - bail!( - "failed to fetch revision {} in directory '{}'", - rev, - directory.display() - ); - } - bail!( - "failed to checkout revision {} in directory '{}': {}", - rev, - directory.display(), - String::from_utf8_lossy(&checkout_output.stderr) - ); - } - - Ok(()) - } - - async fn install_rust_wasm_target_if_needed(&self) -> Result<()> { - let rustc_output = util::command::new_smol_command("rustc") - .arg("--print") - .arg("sysroot") - .output() - .await - .context("failed to run rustc")?; - if !rustc_output.status.success() { - bail!( - "failed to retrieve rust sysroot: {}", - String::from_utf8_lossy(&rustc_output.stderr) - ); - } - - let sysroot = PathBuf::from(String::from_utf8(rustc_output.stdout)?.trim()); - if sysroot.join("lib/rustlib").join(RUST_TARGET).exists() { - return Ok(()); - } - - let output = util::command::new_smol_command("rustup") - .args(["target", "add", RUST_TARGET]) - .stderr(Stdio::piped()) - .stdout(Stdio::inherit()) - .output() - .await - .context("failed to run `rustup target add`")?; - if !output.status.success() { - bail!( - "failed to install the `{RUST_TARGET}` target: {}", - String::from_utf8_lossy(&rustc_output.stderr) - ); - } - - Ok(()) - } - - async fn install_wasi_sdk_if_needed(&self) -> Result { - let url = if let Some(asset_name) = WASI_SDK_ASSET_NAME { - format!("{WASI_SDK_URL}{asset_name}") - } else { - bail!("wasi-sdk is not available for platform {}", env::consts::OS); - }; - - let wasi_sdk_dir = self.cache_dir.join("wasi-sdk"); - let mut clang_path = wasi_sdk_dir.clone(); - clang_path.extend(["bin", &format!("clang{}", env::consts::EXE_SUFFIX)]); - - log::info!("downloading wasi-sdk to {}", wasi_sdk_dir.display()); - - if fs::metadata(&clang_path).is_ok_and(|metadata| metadata.is_file()) { - return Ok(clang_path); - } - - let tar_out_dir = self.cache_dir.join("wasi-sdk-temp"); - - fs::remove_dir_all(&wasi_sdk_dir).ok(); - fs::remove_dir_all(&tar_out_dir).ok(); - fs::create_dir_all(&tar_out_dir).context("failed to create extraction directory")?; - - let mut response = self.http.get(&url, AsyncBody::default(), true).await?; - - // Write the response to a temporary file - let tar_gz_path = self.cache_dir.join("wasi-sdk.tar.gz"); - let mut tar_gz_file = - fs::File::create(&tar_gz_path).context("failed to create temporary tar.gz file")?; - let response_body = response.body_mut(); - let mut body_bytes = Vec::new(); - response_body.read_to_end(&mut body_bytes).await?; - std::io::Write::write_all(&mut tar_gz_file, &body_bytes)?; - drop(tar_gz_file); - - log::info!("un-tarring wasi-sdk to {}", tar_out_dir.display()); - - // Shell out to tar to extract the archive - let tar_output = util::command::new_smol_command("tar") - .arg("-xzf") - .arg(&tar_gz_path) - .arg("-C") - .arg(&tar_out_dir) - .output() - .await - .context("failed to run tar")?; - - if !tar_output.status.success() { - bail!( - "failed to extract wasi-sdk archive: {}", - String::from_utf8_lossy(&tar_output.stderr) - ); - } - - log::info!("finished downloading wasi-sdk"); - - // Clean up the temporary tar.gz file - fs::remove_file(&tar_gz_path).ok(); - - let inner_dir = fs::read_dir(&tar_out_dir)? - .next() - .context("no content")? - .context("failed to read contents of extracted wasi archive directory")? - .path(); - fs::rename(&inner_dir, &wasi_sdk_dir).context("failed to move extracted wasi dir")?; - fs::remove_dir_all(&tar_out_dir).ok(); - - Ok(clang_path) - } - - // This was adapted from: - // https://github.com/bytecodealliance/wasm-tools/blob/e8809bb17fcf69aa8c85cd5e6db7cff5cf36b1de/src/bin/wasm-tools/strip.rs - fn strip_custom_sections(&self, input: &Vec) -> Result> { - use wasmparser::Payload::*; - - let strip_custom_section = |name: &str| { - // Default strip everything but: - // * the `name` section - // * any `component-type` sections - // * the `dylink.0` section - // * our custom version section - name != "name" - && !name.starts_with("component-type:") - && name != "dylink.0" - && name != "zed:api-version" - }; - - let mut output = Vec::new(); - let mut stack = Vec::new(); - - 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: - match payload { - Version { encoding, .. } => { - output.extend_from_slice(match encoding { - wasmparser::Encoding::Component => &wasm_encoder::Component::HEADER, - wasmparser::Encoding::Module => &wasm_encoder::Module::HEADER, - }); - } - ModuleSection { .. } | ComponentSection { .. } => { - stack.push(mem::take(&mut output)); - continue; - } - End { .. } => { - let mut parent = match stack.pop() { - Some(c) => c, - None => break, - }; - if output.starts_with(&wasm_encoder::Component::HEADER) { - parent.push(ComponentSectionId::Component as u8); - output.encode(&mut parent); - } else { - parent.push(ComponentSectionId::CoreModule as u8); - output.encode(&mut parent); - } - output = parent; - } - _ => {} - } - - if let CustomSection(c) = &payload - && strip_custom_section(c.name()) - { - continue; - } - if let Some((id, range)) = payload.as_section() { - RawSection { - id, - data: &input[range], - } - .append_to(&mut output); - } - } - - Ok(output) - } -} - -async fn populate_defaults( - manifest: &mut ExtensionManifest, - extension_path: &Path, - fs: Arc, -) -> Result<()> { - // For legacy extensions on the v0 schema (aka, using `extension.json`), clear out any existing - // contents of the computed fields, since we don't care what the existing values are. - if manifest.schema_version.is_v0() { - manifest.languages.clear(); - manifest.grammars.clear(); - manifest.themes.clear(); - } - - let cargo_toml_path = extension_path.join("Cargo.toml"); - if cargo_toml_path.exists() { - manifest.lib.kind = Some(ExtensionLibraryKind::Rust); - } - - let languages_dir = extension_path.join("languages"); - if fs.is_dir(&languages_dir).await { - let mut language_dir_entries = fs - .read_dir(&languages_dir) - .await - .context("failed to list languages dir")?; - - while let Some(language_dir) = language_dir_entries.next().await { - let language_dir = language_dir?; - let config_path = language_dir.join("config.toml"); - if fs.is_file(config_path.as_path()).await { - let relative_language_dir = - language_dir.strip_prefix(extension_path)?.to_path_buf(); - if !manifest.languages.contains(&relative_language_dir) { - manifest.languages.push(relative_language_dir); - } - } - } - } - - let themes_dir = extension_path.join("themes"); - if fs.is_dir(&themes_dir).await { - let mut theme_dir_entries = fs - .read_dir(&themes_dir) - .await - .context("failed to list themes dir")?; - - while let Some(theme_path) = theme_dir_entries.next().await { - let theme_path = theme_path?; - if theme_path.extension() == Some("json".as_ref()) { - let relative_theme_path = theme_path.strip_prefix(extension_path)?.to_path_buf(); - if !manifest.themes.contains(&relative_theme_path) { - manifest.themes.push(relative_theme_path); - } - } - } - } - - let icon_themes_dir = extension_path.join("icon_themes"); - if fs.is_dir(&icon_themes_dir).await { - let mut icon_theme_dir_entries = fs - .read_dir(&icon_themes_dir) - .await - .context("failed to list icon themes dir")?; - - while let Some(icon_theme_path) = icon_theme_dir_entries.next().await { - let icon_theme_path = icon_theme_path?; - if icon_theme_path.extension() == Some("json".as_ref()) { - let relative_icon_theme_path = - icon_theme_path.strip_prefix(extension_path)?.to_path_buf(); - if !manifest.icon_themes.contains(&relative_icon_theme_path) { - manifest.icon_themes.push(relative_icon_theme_path); - } - } - } - }; - if manifest.snippets.is_none() - && let snippets_json_path = extension_path.join("snippets.json") - && fs.is_file(&snippets_json_path).await - { - manifest.snippets = Some("snippets.json".into()); - } - - // For legacy extensions on the v0 schema (aka, using `extension.json`), we want to populate the grammars in - // the manifest using the contents of the `grammars` directory. - if manifest.schema_version.is_v0() { - let grammars_dir = extension_path.join("grammars"); - if fs.is_dir(&grammars_dir).await { - let mut grammar_dir_entries = fs - .read_dir(&grammars_dir) - .await - .context("failed to list grammars dir")?; - - while let Some(grammar_path) = grammar_dir_entries.next().await { - let grammar_path = grammar_path?; - if grammar_path.extension() == Some("toml".as_ref()) { - #[derive(Deserialize)] - struct GrammarConfigToml { - pub repository: String, - pub commit: String, - #[serde(default)] - pub path: Option, - } - - let grammar_config = fs.load(&grammar_path).await?; - let grammar_config: GrammarConfigToml = toml::from_str(&grammar_config)?; - - let grammar_name = grammar_path - .file_stem() - .and_then(|stem| stem.to_str()) - .context("no grammar name")?; - if !manifest.grammars.contains_key(grammar_name) { - manifest.grammars.insert( - grammar_name.into(), - GrammarManifestEntry { - repository: grammar_config.repository, - rev: grammar_config.commit, - path: grammar_config.path, - }, - ); - } - } - } - } - } - - Ok(()) -} - -/// Returns `true` if the target exists and its last modified time is greater than that -/// of each dependency which exists (i.e., dependency paths which do not exist are ignored). -/// -/// # Errors -/// -/// Returns `Err` if any of the underlying file I/O operations fail. -fn file_newer_than_deps(target: &Path, dependencies: &[&Path]) -> Result { - if !target.try_exists()? { - return Ok(false); - } - let target_modified = target.metadata()?.modified()?; - for dependency in dependencies { - if !dependency.try_exists()? { - continue; - } - let dep_modified = dependency.metadata()?.modified()?; - if target_modified < dep_modified { - return Ok(false); - } - } - Ok(true) -} - -#[cfg(test)] -mod tests { - use std::{ - path::{Path, PathBuf}, - str::FromStr, - thread::sleep, - time::Duration, - }; - - use gpui::TestAppContext; - use indoc::indoc; - - use crate::{ - ExtensionManifest, - extension_builder::{file_newer_than_deps, populate_defaults}, - }; - - #[test] - fn test_file_newer_than_deps() { - // Don't use TempTree because we need to guarantee the order - let tmpdir = tempfile::tempdir().unwrap(); - let target = tmpdir.path().join("target.wasm"); - let dep1 = tmpdir.path().join("parser.c"); - let dep2 = tmpdir.path().join("scanner.c"); - - assert!( - !file_newer_than_deps(&target, &[&dep1, &dep2]).unwrap(), - "target doesn't exist" - ); - std::fs::write(&target, "foo").unwrap(); // Create target - assert!( - file_newer_than_deps(&target, &[&dep1, &dep2]).unwrap(), - "dependencies don't exist; target is newer" - ); - sleep(Duration::from_secs(1)); - std::fs::write(&dep1, "foo").unwrap(); // Create dep1 (newer than target) - // Dependency is newer - assert!( - !file_newer_than_deps(&target, &[&dep1, &dep2]).unwrap(), - "a dependency is newer (target {:?}, dep1 {:?})", - target.metadata().unwrap().modified().unwrap(), - dep1.metadata().unwrap().modified().unwrap(), - ); - sleep(Duration::from_secs(1)); - std::fs::write(&dep2, "foo").unwrap(); // Create dep2 - sleep(Duration::from_secs(1)); - std::fs::write(&target, "foobar").unwrap(); // Update target - assert!( - file_newer_than_deps(&target, &[&dep1, &dep2]).unwrap(), - "target is newer than dependencies (target {:?}, dep2 {:?})", - target.metadata().unwrap().modified().unwrap(), - dep2.metadata().unwrap().modified().unwrap(), - ); - } - - #[gpui::test] - async fn test_snippet_location_is_kept(cx: &mut TestAppContext) { - let fs = fs::FakeFs::new(cx.executor()); - let extension_path = Path::new("/extension"); - - fs.insert_tree( - extension_path, - serde_json::json!({ - "extension.toml": indoc! {r#" - id = "test-manifest" - name = "Test Manifest" - version = "0.0.1" - schema_version = 1 - - snippets = "./snippets/snippets.json" - "# - }, - "snippets.json": "", - }), - ) - .await; - - let mut manifest = ExtensionManifest::load(fs.clone(), extension_path) - .await - .unwrap(); - - populate_defaults(&mut manifest, extension_path, fs.clone()) - .await - .unwrap(); - - assert_eq!( - manifest.snippets, - Some(PathBuf::from_str("./snippets/snippets.json").unwrap()) - ) - } - - #[gpui::test] - async fn test_automatic_snippet_location_is_relative(cx: &mut TestAppContext) { - let fs = fs::FakeFs::new(cx.executor()); - let extension_path = Path::new("/extension"); - - fs.insert_tree( - extension_path, - serde_json::json!({ - "extension.toml": indoc! {r#" - id = "test-manifest" - name = "Test Manifest" - version = "0.0.1" - schema_version = 1 - - "# - }, - "snippets.json": "", - }), - ) - .await; - - let mut manifest = ExtensionManifest::load(fs.clone(), extension_path) - .await - .unwrap(); - - populate_defaults(&mut manifest, extension_path, fs.clone()) - .await - .unwrap(); - - assert_eq!( - manifest.snippets, - Some(PathBuf::from_str("snippets.json").unwrap()) - ) - } -} diff --git a/crates/extension/src/extension_events.rs b/crates/extension/src/extension_events.rs deleted file mode 100644 index 6dc99470df..0000000000 --- a/crates/extension/src/extension_events.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::sync::Arc; - -use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global}; - -use crate::ExtensionManifest; - -pub fn init(cx: &mut App) { - let extension_events = cx.new(ExtensionEvents::new); - cx.set_global(GlobalExtensionEvents(extension_events)); -} - -struct GlobalExtensionEvents(Entity); - -impl Global for GlobalExtensionEvents {} - -/// An event bus for broadcasting extension-related events throughout the app. -pub struct ExtensionEvents; - -impl ExtensionEvents { - /// Returns the global [`ExtensionEvents`]. - pub fn try_global(cx: &App) -> Option> { - cx.try_global::() - .map(|g| g.0.clone()) - } - - fn new(_cx: &mut Context) -> Self { - Self - } - - pub fn emit(&mut self, event: Event, cx: &mut Context) { - cx.emit(event) - } -} - -#[derive(Clone, Debug)] -pub enum Event { - ExtensionInstalled(Arc), - ExtensionUninstalled(Arc), - ExtensionsInstalledChanged, - ConfigureExtensionRequested(Arc), -} - -impl EventEmitter for ExtensionEvents {} diff --git a/crates/extension/src/extension_host_proxy.rs b/crates/extension/src/extension_host_proxy.rs deleted file mode 100644 index 6a24e3ba3f..0000000000 --- a/crates/extension/src/extension_host_proxy.rs +++ /dev/null @@ -1,448 +0,0 @@ -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use anyhow::Result; -use fs::Fs; -use gpui::{App, Global, ReadGlobal, SharedString, Task}; -use language::{BinaryStatus, LanguageMatcher, LanguageName, LoadedLanguage}; -use lsp::LanguageServerName; -use parking_lot::RwLock; - -use crate::{Extension, SlashCommand}; - -#[derive(Default)] -struct GlobalExtensionHostProxy(Arc); - -impl Global for GlobalExtensionHostProxy {} - -/// A proxy for interacting with the extension host. -/// -/// This object implements each of the individual proxy types so that their -/// methods can be called directly on it. -#[derive(Default)] -pub struct ExtensionHostProxy { - theme_proxy: RwLock>>, - grammar_proxy: RwLock>>, - language_proxy: RwLock>>, - language_server_proxy: RwLock>>, - snippet_proxy: RwLock>>, - slash_command_proxy: RwLock>>, - context_server_proxy: RwLock>>, - debug_adapter_provider_proxy: RwLock>>, -} - -impl ExtensionHostProxy { - /// Returns the global [`ExtensionHostProxy`]. - pub fn global(cx: &App) -> Arc { - GlobalExtensionHostProxy::global(cx).0.clone() - } - - /// Returns the global [`ExtensionHostProxy`]. - /// - /// Inserts a default [`ExtensionHostProxy`] if one does not yet exist. - pub fn default_global(cx: &mut App) -> Arc { - cx.default_global::().0.clone() - } - - pub fn new() -> Self { - Self { - theme_proxy: RwLock::default(), - grammar_proxy: RwLock::default(), - language_proxy: RwLock::default(), - language_server_proxy: RwLock::default(), - snippet_proxy: RwLock::default(), - slash_command_proxy: RwLock::default(), - context_server_proxy: RwLock::default(), - debug_adapter_provider_proxy: RwLock::default(), - } - } - - pub fn register_theme_proxy(&self, proxy: impl ExtensionThemeProxy) { - self.theme_proxy.write().replace(Arc::new(proxy)); - } - - pub fn register_grammar_proxy(&self, proxy: impl ExtensionGrammarProxy) { - self.grammar_proxy.write().replace(Arc::new(proxy)); - } - - pub fn register_language_proxy(&self, proxy: impl ExtensionLanguageProxy) { - self.language_proxy.write().replace(Arc::new(proxy)); - } - - pub fn register_language_server_proxy(&self, proxy: impl ExtensionLanguageServerProxy) { - self.language_server_proxy.write().replace(Arc::new(proxy)); - } - - pub fn register_snippet_proxy(&self, proxy: impl ExtensionSnippetProxy) { - self.snippet_proxy.write().replace(Arc::new(proxy)); - } - - pub fn register_slash_command_proxy(&self, proxy: impl ExtensionSlashCommandProxy) { - self.slash_command_proxy.write().replace(Arc::new(proxy)); - } - - pub fn register_context_server_proxy(&self, proxy: impl ExtensionContextServerProxy) { - self.context_server_proxy.write().replace(Arc::new(proxy)); - } - - pub fn register_debug_adapter_proxy(&self, proxy: impl ExtensionDebugAdapterProviderProxy) { - self.debug_adapter_provider_proxy - .write() - .replace(Arc::new(proxy)); - } -} - -pub trait ExtensionThemeProxy: Send + Sync + 'static { - fn set_extensions_loaded(&self); - - fn list_theme_names(&self, theme_path: PathBuf, fs: Arc) -> Task>>; - - fn remove_user_themes(&self, themes: Vec); - - fn load_user_theme(&self, theme_path: PathBuf, fs: Arc) -> Task>; - - fn reload_current_theme(&self, cx: &mut App); - - fn list_icon_theme_names( - &self, - icon_theme_path: PathBuf, - fs: Arc, - ) -> Task>>; - - fn remove_icon_themes(&self, icon_themes: Vec); - - fn load_icon_theme( - &self, - icon_theme_path: PathBuf, - icons_root_dir: PathBuf, - fs: Arc, - ) -> Task>; - - fn reload_current_icon_theme(&self, cx: &mut App); -} - -impl ExtensionThemeProxy for ExtensionHostProxy { - fn set_extensions_loaded(&self) { - let Some(proxy) = self.theme_proxy.read().clone() else { - return; - }; - - proxy.set_extensions_loaded() - } - - fn list_theme_names(&self, theme_path: PathBuf, fs: Arc) -> Task>> { - let Some(proxy) = self.theme_proxy.read().clone() else { - return Task::ready(Ok(Vec::new())); - }; - - proxy.list_theme_names(theme_path, fs) - } - - fn remove_user_themes(&self, themes: Vec) { - let Some(proxy) = self.theme_proxy.read().clone() else { - return; - }; - - proxy.remove_user_themes(themes) - } - - fn load_user_theme(&self, theme_path: PathBuf, fs: Arc) -> Task> { - let Some(proxy) = self.theme_proxy.read().clone() else { - return Task::ready(Ok(())); - }; - - proxy.load_user_theme(theme_path, fs) - } - - fn reload_current_theme(&self, cx: &mut App) { - let Some(proxy) = self.theme_proxy.read().clone() else { - return; - }; - - proxy.reload_current_theme(cx) - } - - fn list_icon_theme_names( - &self, - icon_theme_path: PathBuf, - fs: Arc, - ) -> Task>> { - let Some(proxy) = self.theme_proxy.read().clone() else { - return Task::ready(Ok(Vec::new())); - }; - - proxy.list_icon_theme_names(icon_theme_path, fs) - } - - fn remove_icon_themes(&self, icon_themes: Vec) { - let Some(proxy) = self.theme_proxy.read().clone() else { - return; - }; - - proxy.remove_icon_themes(icon_themes) - } - - fn load_icon_theme( - &self, - icon_theme_path: PathBuf, - icons_root_dir: PathBuf, - fs: Arc, - ) -> Task> { - let Some(proxy) = self.theme_proxy.read().clone() else { - return Task::ready(Ok(())); - }; - - proxy.load_icon_theme(icon_theme_path, icons_root_dir, fs) - } - - fn reload_current_icon_theme(&self, cx: &mut App) { - let Some(proxy) = self.theme_proxy.read().clone() else { - return; - }; - - proxy.reload_current_icon_theme(cx) - } -} - -pub trait ExtensionGrammarProxy: Send + Sync + 'static { - fn register_grammars(&self, grammars: Vec<(Arc, PathBuf)>); -} - -impl ExtensionGrammarProxy for ExtensionHostProxy { - fn register_grammars(&self, grammars: Vec<(Arc, PathBuf)>) { - let Some(proxy) = self.grammar_proxy.read().clone() else { - return; - }; - - proxy.register_grammars(grammars) - } -} - -pub trait ExtensionLanguageProxy: Send + Sync + 'static { - fn register_language( - &self, - language: LanguageName, - grammar: Option>, - matcher: LanguageMatcher, - hidden: bool, - load: Arc Result + Send + Sync + 'static>, - ); - - fn remove_languages( - &self, - languages_to_remove: &[LanguageName], - grammars_to_remove: &[Arc], - ); -} - -impl ExtensionLanguageProxy for ExtensionHostProxy { - fn register_language( - &self, - language: LanguageName, - grammar: Option>, - matcher: LanguageMatcher, - hidden: bool, - load: Arc Result + Send + Sync + 'static>, - ) { - let Some(proxy) = self.language_proxy.read().clone() else { - return; - }; - - proxy.register_language(language, grammar, matcher, hidden, load) - } - - fn remove_languages( - &self, - languages_to_remove: &[LanguageName], - grammars_to_remove: &[Arc], - ) { - let Some(proxy) = self.language_proxy.read().clone() else { - return; - }; - - proxy.remove_languages(languages_to_remove, grammars_to_remove) - } -} - -pub trait ExtensionLanguageServerProxy: Send + Sync + 'static { - fn register_language_server( - &self, - extension: Arc, - language_server_id: LanguageServerName, - language: LanguageName, - ); - - fn remove_language_server( - &self, - language: &LanguageName, - language_server_id: &LanguageServerName, - cx: &mut App, - ) -> Task>; - - fn update_language_server_status( - &self, - language_server_id: LanguageServerName, - status: BinaryStatus, - ); -} - -impl ExtensionLanguageServerProxy for ExtensionHostProxy { - fn register_language_server( - &self, - extension: Arc, - language_server_id: LanguageServerName, - language: LanguageName, - ) { - let Some(proxy) = self.language_server_proxy.read().clone() else { - return; - }; - - proxy.register_language_server(extension, language_server_id, language) - } - - fn remove_language_server( - &self, - language: &LanguageName, - language_server_id: &LanguageServerName, - cx: &mut App, - ) -> Task> { - let Some(proxy) = self.language_server_proxy.read().clone() else { - return Task::ready(Ok(())); - }; - - proxy.remove_language_server(language, language_server_id, cx) - } - - fn update_language_server_status( - &self, - language_server_id: LanguageServerName, - status: BinaryStatus, - ) { - let Some(proxy) = self.language_server_proxy.read().clone() else { - return; - }; - - proxy.update_language_server_status(language_server_id, status) - } -} - -pub trait ExtensionSnippetProxy: Send + Sync + 'static { - fn register_snippet(&self, path: &PathBuf, snippet_contents: &str) -> Result<()>; -} - -impl ExtensionSnippetProxy for ExtensionHostProxy { - fn register_snippet(&self, path: &PathBuf, snippet_contents: &str) -> Result<()> { - let Some(proxy) = self.snippet_proxy.read().clone() else { - return Ok(()); - }; - - proxy.register_snippet(path, snippet_contents) - } -} - -pub trait ExtensionSlashCommandProxy: Send + Sync + 'static { - fn register_slash_command(&self, extension: Arc, command: SlashCommand); - - fn unregister_slash_command(&self, command_name: Arc); -} - -impl ExtensionSlashCommandProxy for ExtensionHostProxy { - fn register_slash_command(&self, extension: Arc, command: SlashCommand) { - let Some(proxy) = self.slash_command_proxy.read().clone() else { - return; - }; - - proxy.register_slash_command(extension, command) - } - - fn unregister_slash_command(&self, command_name: Arc) { - let Some(proxy) = self.slash_command_proxy.read().clone() else { - return; - }; - - proxy.unregister_slash_command(command_name) - } -} - -pub trait ExtensionContextServerProxy: Send + Sync + 'static { - fn register_context_server( - &self, - extension: Arc, - server_id: Arc, - cx: &mut App, - ); - - fn unregister_context_server(&self, server_id: Arc, cx: &mut App); -} - -impl ExtensionContextServerProxy for ExtensionHostProxy { - fn register_context_server( - &self, - extension: Arc, - server_id: Arc, - cx: &mut App, - ) { - let Some(proxy) = self.context_server_proxy.read().clone() else { - return; - }; - - proxy.register_context_server(extension, server_id, cx) - } - - fn unregister_context_server(&self, server_id: Arc, cx: &mut App) { - let Some(proxy) = self.context_server_proxy.read().clone() else { - return; - }; - - proxy.unregister_context_server(server_id, cx) - } -} - -pub trait ExtensionDebugAdapterProviderProxy: Send + Sync + 'static { - fn register_debug_adapter( - &self, - extension: Arc, - debug_adapter_name: Arc, - schema_path: &Path, - ); - fn register_debug_locator(&self, extension: Arc, locator_name: Arc); - fn unregister_debug_adapter(&self, debug_adapter_name: Arc); - fn unregister_debug_locator(&self, locator_name: Arc); -} - -impl ExtensionDebugAdapterProviderProxy for ExtensionHostProxy { - fn register_debug_adapter( - &self, - extension: Arc, - debug_adapter_name: Arc, - schema_path: &Path, - ) { - let Some(proxy) = self.debug_adapter_provider_proxy.read().clone() else { - return; - }; - - proxy.register_debug_adapter(extension, debug_adapter_name, schema_path) - } - - fn register_debug_locator(&self, extension: Arc, locator_name: Arc) { - let Some(proxy) = self.debug_adapter_provider_proxy.read().clone() else { - return; - }; - - proxy.register_debug_locator(extension, locator_name) - } - fn unregister_debug_adapter(&self, debug_adapter_name: Arc) { - let Some(proxy) = self.debug_adapter_provider_proxy.read().clone() else { - return; - }; - - proxy.unregister_debug_adapter(debug_adapter_name) - } - fn unregister_debug_locator(&self, locator_name: Arc) { - let Some(proxy) = self.debug_adapter_provider_proxy.read().clone() else { - return; - }; - - proxy.unregister_debug_locator(locator_name) - } -} diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs deleted file mode 100644 index 4ecdd378ca..0000000000 --- a/crates/extension/src/extension_manifest.rs +++ /dev/null @@ -1,522 +0,0 @@ -use anyhow::{Context as _, Result, anyhow, bail}; -use collections::{BTreeMap, HashMap}; -use fs::Fs; -use language::LanguageName; -use lsp::LanguageServerName; -use semver::Version; -use serde::{Deserialize, Serialize}; -use std::{ - ffi::OsStr, - fmt, - path::{Path, PathBuf}, - sync::Arc, -}; - -use crate::ExtensionCapability; - -/// This is the old version of the extension manifest, from when it was `extension.json`. -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct OldExtensionManifest { - pub name: String, - pub version: Arc, - - #[serde(default)] - pub description: Option, - #[serde(default)] - pub repository: Option, - #[serde(default)] - pub authors: Vec, - - #[serde(default)] - pub themes: BTreeMap, PathBuf>, - #[serde(default)] - pub languages: BTreeMap, PathBuf>, - #[serde(default)] - pub grammars: BTreeMap, PathBuf>, -} - -/// The schema version of the [`ExtensionManifest`]. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)] -pub struct SchemaVersion(pub i32); - -impl fmt::Display for SchemaVersion { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl SchemaVersion { - pub const ZERO: Self = Self(0); - - pub fn is_v0(&self) -> bool { - self == &Self::ZERO - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct ExtensionManifest { - pub id: Arc, - pub name: String, - pub version: Arc, - pub schema_version: SchemaVersion, - - #[serde(default)] - pub description: Option, - #[serde(default)] - pub repository: Option, - #[serde(default)] - pub authors: Vec, - #[serde(default)] - pub lib: LibManifestEntry, - - #[serde(default)] - pub themes: Vec, - #[serde(default)] - pub icon_themes: Vec, - #[serde(default)] - pub languages: Vec, - #[serde(default)] - pub grammars: BTreeMap, GrammarManifestEntry>, - #[serde(default)] - pub language_servers: BTreeMap, - #[serde(default)] - pub context_servers: BTreeMap, ContextServerManifestEntry>, - #[serde(default)] - pub agent_servers: BTreeMap, AgentServerManifestEntry>, - #[serde(default)] - pub slash_commands: BTreeMap, SlashCommandManifestEntry>, - #[serde(default)] - pub snippets: Option, - #[serde(default)] - pub capabilities: Vec, - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub debug_adapters: BTreeMap, DebugAdapterManifestEntry>, - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub debug_locators: BTreeMap, DebugLocatorManifestEntry>, -} - -impl ExtensionManifest { - pub fn allow_exec( - &self, - desired_command: &str, - desired_args: &[impl AsRef + std::fmt::Debug], - ) -> Result<()> { - let is_allowed = self.capabilities.iter().any(|capability| match capability { - ExtensionCapability::ProcessExec(capability) => { - capability.allows(desired_command, desired_args) - } - _ => false, - }); - - if !is_allowed { - bail!( - "capability for process:exec {desired_command} {desired_args:?} was not listed in the extension manifest", - ); - } - - Ok(()) - } - - pub fn allow_remote_load(&self) -> bool { - !self.language_servers.is_empty() - || !self.debug_adapters.is_empty() - || !self.debug_locators.is_empty() - } -} - -pub fn build_debug_adapter_schema_path( - adapter_name: &Arc, - meta: &DebugAdapterManifestEntry, -) -> PathBuf { - meta.schema_path.clone().unwrap_or_else(|| { - Path::new("debug_adapter_schemas") - .join(Path::new(adapter_name.as_ref()).with_extension("json")) - }) -} - -#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct LibManifestEntry { - pub kind: Option, - pub version: Option, -} - -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct AgentServerManifestEntry { - /// Display name for the agent (shown in menus). - pub name: String, - /// Environment variables to set when launching the agent server. - #[serde(default)] - pub env: HashMap, - /// Optional icon path (relative to extension root, e.g., "ai.svg"). - /// Should be a small SVG icon for display in menus. - #[serde(default)] - pub icon: Option, - /// Per-target configuration for archive-based installation. - /// The key format is "{os}-{arch}" where: - /// - os: "darwin" (macOS), "linux", "windows" - /// - arch: "aarch64" (arm64), "x86_64" - /// - /// Example: - /// ```toml - /// [agent_servers.myagent.targets.darwin-aarch64] - /// archive = "https://example.com/myagent-darwin-arm64.zip" - /// cmd = "./myagent" - /// args = ["--serve"] - /// sha256 = "abc123..." # optional - /// ``` - /// - /// For Node.js-based agents, you can use "node" as the cmd to automatically - /// use Zed's managed Node.js runtime instead of relying on the user's PATH: - /// ```toml - /// [agent_servers.nodeagent.targets.darwin-aarch64] - /// archive = "https://example.com/nodeagent.zip" - /// cmd = "node" - /// args = ["index.js", "--port", "3000"] - /// ``` - /// - /// Note: All commands are executed with the archive extraction directory as the - /// working directory, so relative paths in args (like "index.js") will resolve - /// relative to the extracted archive contents. - pub targets: HashMap, -} - -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct TargetConfig { - /// URL to download the archive from (e.g., "https://github.com/owner/repo/releases/download/v1.0.0/myagent-darwin-arm64.zip") - pub archive: String, - /// Command to run (e.g., "./myagent" or "./myagent.exe") - pub cmd: String, - /// Command-line arguments to pass to the agent server. - #[serde(default)] - pub args: Vec, - /// Optional SHA-256 hash of the archive for verification. - /// If not provided and the URL is a GitHub release, we'll attempt to fetch it from GitHub. - #[serde(default)] - pub sha256: Option, - /// Environment variables to set when launching the agent server. - /// These target-specific env vars will override any env vars set at the agent level. - #[serde(default)] - pub env: HashMap, -} - -impl TargetConfig { - pub fn from_proto(proto: proto::ExternalExtensionAgentTarget) -> Self { - Self { - archive: proto.archive, - cmd: proto.cmd, - args: proto.args, - sha256: proto.sha256, - env: proto.env.into_iter().collect(), - } - } - - pub fn to_proto(&self) -> proto::ExternalExtensionAgentTarget { - proto::ExternalExtensionAgentTarget { - archive: self.archive.clone(), - cmd: self.cmd.clone(), - args: self.args.clone(), - sha256: self.sha256.clone(), - env: self - .env - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(), - } - } -} - -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub enum ExtensionLibraryKind { - Rust, -} - -#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct GrammarManifestEntry { - pub repository: String, - #[serde(alias = "commit")] - pub rev: String, - #[serde(default)] - pub path: Option, -} - -#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct LanguageServerManifestEntry { - /// Deprecated in favor of `languages`. - #[serde(default)] - language: Option, - /// The list of languages this language server should work with. - #[serde(default)] - languages: Vec, - #[serde(default)] - pub language_ids: HashMap, - #[serde(default)] - pub code_action_kinds: Option>, -} - -impl LanguageServerManifestEntry { - /// Returns the list of languages for the language server. - /// - /// Prefer this over accessing the `language` or `languages` fields directly, - /// as we currently support both. - /// - /// We can replace this with just field access for the `languages` field once - /// we have removed `language`. - pub fn languages(&self) -> impl IntoIterator + '_ { - let language = if self.languages.is_empty() { - self.language.clone() - } else { - None - }; - self.languages.iter().cloned().chain(language) - } -} - -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct ContextServerManifestEntry {} - -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct SlashCommandManifestEntry { - pub description: String, - pub requires_argument: bool, -} - -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct DebugAdapterManifestEntry { - pub schema_path: Option, -} - -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct DebugLocatorManifestEntry {} - -impl ExtensionManifest { - pub async fn load(fs: Arc, extension_dir: &Path) -> Result { - let extension_name = extension_dir - .file_name() - .and_then(OsStr::to_str) - .context("invalid extension name")?; - - let extension_manifest_path = extension_dir.join("extension.toml"); - if fs.is_file(&extension_manifest_path).await { - let manifest_content = fs.load(&extension_manifest_path).await.with_context(|| { - format!("loading {extension_name} extension.toml, {extension_manifest_path:?}") - })?; - toml::from_str(&manifest_content).map_err(|err| { - anyhow!("Invalid extension.toml for extension {extension_name}:\n{err}") - }) - } else if let extension_manifest_path = extension_manifest_path.with_extension("json") - && fs.is_file(&extension_manifest_path).await - { - let manifest_content = fs.load(&extension_manifest_path).await.with_context(|| { - format!("loading {extension_name} extension.json, {extension_manifest_path:?}") - })?; - - serde_json::from_str::(&manifest_content) - .with_context(|| format!("invalid extension.json for extension {extension_name}")) - .map(|manifest_json| manifest_from_old_manifest(manifest_json, extension_name)) - } else { - anyhow::bail!("No extension manifest found for extension {extension_name}") - } - } -} - -fn manifest_from_old_manifest( - manifest_json: OldExtensionManifest, - extension_id: &str, -) -> ExtensionManifest { - ExtensionManifest { - id: extension_id.into(), - name: manifest_json.name, - version: manifest_json.version, - description: manifest_json.description, - repository: manifest_json.repository, - authors: manifest_json.authors, - schema_version: SchemaVersion::ZERO, - lib: Default::default(), - themes: { - let mut themes = manifest_json.themes.into_values().collect::>(); - themes.sort(); - themes.dedup(); - themes - }, - icon_themes: Vec::new(), - languages: { - let mut languages = manifest_json.languages.into_values().collect::>(); - languages.sort(); - languages.dedup(); - languages - }, - grammars: manifest_json - .grammars - .into_keys() - .map(|grammar_name| (grammar_name, Default::default())) - .collect(), - language_servers: Default::default(), - context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), - slash_commands: BTreeMap::default(), - snippets: None, - capabilities: Vec::new(), - debug_adapters: Default::default(), - debug_locators: Default::default(), - } -} - -#[cfg(test)] -mod tests { - use pretty_assertions::assert_eq; - - use crate::ProcessExecCapability; - - use super::*; - - fn extension_manifest() -> ExtensionManifest { - ExtensionManifest { - id: "test".into(), - name: "Test".to_string(), - version: "1.0.0".into(), - schema_version: SchemaVersion::ZERO, - description: None, - repository: None, - authors: vec![], - lib: Default::default(), - themes: vec![], - icon_themes: vec![], - languages: vec![], - grammars: BTreeMap::default(), - language_servers: BTreeMap::default(), - context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), - slash_commands: BTreeMap::default(), - snippets: None, - capabilities: vec![], - debug_adapters: Default::default(), - debug_locators: Default::default(), - } - } - - #[test] - fn test_build_adapter_schema_path_with_schema_path() { - let adapter_name = Arc::from("my_adapter"); - let entry = DebugAdapterManifestEntry { - schema_path: Some(PathBuf::from("foo/bar")), - }; - - let path = build_debug_adapter_schema_path(&adapter_name, &entry); - assert_eq!(path, PathBuf::from("foo/bar")); - } - - #[test] - fn test_build_adapter_schema_path_without_schema_path() { - let adapter_name = Arc::from("my_adapter"); - let entry = DebugAdapterManifestEntry { schema_path: None }; - - let path = build_debug_adapter_schema_path(&adapter_name, &entry); - assert_eq!( - path, - PathBuf::from("debug_adapter_schemas").join("my_adapter.json") - ); - } - - #[test] - fn test_allow_exec_exact_match() { - let manifest = ExtensionManifest { - capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability { - command: "ls".to_string(), - args: vec!["-la".to_string()], - })], - ..extension_manifest() - }; - - assert!(manifest.allow_exec("ls", &["-la"]).is_ok()); - assert!(manifest.allow_exec("ls", &["-l"]).is_err()); - assert!(manifest.allow_exec("pwd", &[] as &[&str]).is_err()); - } - - #[test] - fn test_allow_exec_wildcard_arg() { - let manifest = ExtensionManifest { - capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability { - command: "git".to_string(), - args: vec!["*".to_string()], - })], - ..extension_manifest() - }; - - assert!(manifest.allow_exec("git", &["status"]).is_ok()); - assert!(manifest.allow_exec("git", &["commit"]).is_ok()); - assert!(manifest.allow_exec("git", &["status", "-s"]).is_err()); // too many args - assert!(manifest.allow_exec("npm", &["install"]).is_err()); // wrong command - } - - #[test] - fn test_allow_exec_double_wildcard() { - let manifest = ExtensionManifest { - capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability { - command: "cargo".to_string(), - args: vec!["test".to_string(), "**".to_string()], - })], - ..extension_manifest() - }; - - assert!(manifest.allow_exec("cargo", &["test"]).is_ok()); - assert!(manifest.allow_exec("cargo", &["test", "--all"]).is_ok()); - assert!( - manifest - .allow_exec("cargo", &["test", "--all", "--no-fail-fast"]) - .is_ok() - ); - assert!(manifest.allow_exec("cargo", &["build"]).is_err()); // wrong first arg - } - - #[test] - fn test_allow_exec_mixed_wildcards() { - let manifest = ExtensionManifest { - capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability { - command: "docker".to_string(), - args: vec!["run".to_string(), "*".to_string(), "**".to_string()], - })], - ..extension_manifest() - }; - - assert!(manifest.allow_exec("docker", &["run", "nginx"]).is_ok()); - assert!(manifest.allow_exec("docker", &["run"]).is_err()); - assert!( - manifest - .allow_exec("docker", &["run", "ubuntu", "bash"]) - .is_ok() - ); - assert!( - manifest - .allow_exec("docker", &["run", "alpine", "sh", "-c", "echo hello"]) - .is_ok() - ); - assert!(manifest.allow_exec("docker", &["ps"]).is_err()); // wrong first arg - } - #[test] - fn parse_manifest_with_agent_server_archive_launcher() { - let toml_src = r#" -id = "example.agent-server-ext" -name = "Agent Server Example" -version = "1.0.0" -schema_version = 0 - -[agent_servers.foo] -name = "Foo Agent" - -[agent_servers.foo.targets.linux-x86_64] -archive = "https://example.com/agent-linux-x64.tar.gz" -cmd = "./agent" -args = ["--serve"] -"#; - - let manifest: ExtensionManifest = toml::from_str(toml_src).expect("manifest should parse"); - assert_eq!(manifest.id.as_ref(), "example.agent-server-ext"); - assert!(manifest.agent_servers.contains_key("foo")); - let entry = manifest.agent_servers.get("foo").unwrap(); - assert!(entry.targets.contains_key("linux-x86_64")); - let target = entry.targets.get("linux-x86_64").unwrap(); - assert_eq!(target.archive, "https://example.com/agent-linux-x64.tar.gz"); - assert_eq!(target.cmd, "./agent"); - assert_eq!(target.args, vec!["--serve"]); - } -} diff --git a/crates/extension/src/types.rs b/crates/extension/src/types.rs deleted file mode 100644 index ed9eb2ec2f..0000000000 --- a/crates/extension/src/types.rs +++ /dev/null @@ -1,71 +0,0 @@ -mod context_server; -mod dap; -mod lsp; -mod slash_command; - -use std::{ops::Range, path::PathBuf}; - -use util::redact::should_redact; - -pub use context_server::*; -pub use dap::*; -pub use lsp::*; -pub use slash_command::*; - -/// A list of environment variables. -pub type EnvVars = Vec<(String, String)>; - -/// A command. -pub struct Command { - /// The command to execute. - pub command: PathBuf, - /// The arguments to pass to the command. - pub args: Vec, - /// The environment variables to set for the command. - pub env: EnvVars, -} - -impl std::fmt::Debug for Command { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let filtered_env = self - .env - .iter() - .map(|(k, v)| (k, if should_redact(k) { "[REDACTED]" } else { v })) - .collect::>(); - - f.debug_struct("Command") - .field("command", &self.command) - .field("args", &self.args) - .field("env", &filtered_env) - .finish() - } -} - -/// A label containing some code. -#[derive(Debug, Clone)] -pub struct CodeLabel { - /// The source code to parse with Tree-sitter. - pub code: String, - /// The spans to display in the label. - pub spans: Vec, - /// The range of the displayed label to include when filtering. - pub filter_range: Range, -} - -/// A span within a code label. -#[derive(Debug, Clone)] -pub enum CodeLabelSpan { - /// A range into the parsed code. - CodeRange(Range), - /// A span containing a code literal. - Literal(CodeLabelSpanLiteral), -} - -/// A span containing a code literal. -#[derive(Debug, Clone)] -pub struct CodeLabelSpanLiteral { - /// The literal text. - pub text: String, - /// The name of the highlight to use for this literal. - pub highlight_name: Option, -} diff --git a/crates/extension/src/types/context_server.rs b/crates/extension/src/types/context_server.rs deleted file mode 100644 index 2e3d20b047..0000000000 --- a/crates/extension/src/types/context_server.rs +++ /dev/null @@ -1,10 +0,0 @@ -/// Configuration for context server setup and installation. -#[derive(Debug, Clone)] -pub struct ContextServerConfiguration { - /// Installation instructions in Markdown format. - pub installation_instructions: String, - /// JSON schema for settings validation. - pub settings_schema: serde_json::Value, - /// Default settings template. - pub default_settings: String, -} diff --git a/crates/extension/src/types/dap.rs b/crates/extension/src/types/dap.rs deleted file mode 100644 index 5dcd7f57d5..0000000000 --- a/crates/extension/src/types/dap.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub use dap::{ - StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, - adapters::{DebugAdapterBinary, DebugTaskDefinition, TcpArguments}, -}; -pub use task::{ - AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, LaunchRequest, - TaskTemplate as BuildTaskTemplate, TcpArgumentsTemplate, -}; diff --git a/crates/extension/src/types/lsp.rs b/crates/extension/src/types/lsp.rs deleted file mode 100644 index 6e858211cd..0000000000 --- a/crates/extension/src/types/lsp.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::option::Option; - -/// An LSP completion. -#[derive(Debug, Clone)] -pub struct Completion { - pub label: String, - pub label_details: Option, - pub detail: Option, - pub kind: Option, - pub insert_text_format: Option, -} - -/// The kind of an LSP completion. -#[derive(Debug, Clone, Copy)] -pub enum CompletionKind { - Text, - Method, - Function, - Constructor, - Field, - Variable, - Class, - Interface, - Module, - Property, - Unit, - Value, - Enum, - Keyword, - Snippet, - Color, - File, - Reference, - Folder, - EnumMember, - Constant, - Struct, - Event, - Operator, - TypeParameter, - Other(i32), -} - -/// Label details for an LSP completion. -#[derive(Debug, Clone)] -pub struct CompletionLabelDetails { - pub detail: Option, - pub description: Option, -} - -/// Defines how to interpret the insert text in a completion item. -#[derive(Debug, Clone, Copy)] -pub enum InsertTextFormat { - PlainText, - Snippet, - Other(i32), -} - -/// An LSP symbol. -#[derive(Debug, Clone)] -pub struct Symbol { - pub kind: SymbolKind, - pub name: String, -} - -/// The kind of an LSP symbol. -#[derive(Debug, Clone, Copy)] -pub enum SymbolKind { - File, - Module, - Namespace, - Package, - Class, - Method, - Property, - Field, - Constructor, - Enum, - Interface, - Function, - Variable, - Constant, - String, - Number, - Boolean, - Array, - Object, - Key, - Null, - EnumMember, - Struct, - Event, - Operator, - TypeParameter, - Other(i32), -} diff --git a/crates/extension/src/types/slash_command.rs b/crates/extension/src/types/slash_command.rs deleted file mode 100644 index 0b937984a5..0000000000 --- a/crates/extension/src/types/slash_command.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::ops::Range; - -/// A slash command for use in the Assistant. -#[derive(Debug, Clone)] -pub struct SlashCommand { - /// The name of the slash command. - pub name: String, - /// The description of the slash command. - pub description: String, - /// The tooltip text to display for the run button. - pub tooltip_text: String, - /// Whether this slash command requires an argument. - pub requires_argument: bool, -} - -/// The output of a slash command. -#[derive(Debug, Clone)] -pub struct SlashCommandOutput { - /// The text produced by the slash command. - pub text: String, - /// The list of sections to show in the slash command placeholder. - pub sections: Vec, -} - -/// A section in the slash command output. -#[derive(Debug, Clone)] -pub struct SlashCommandOutputSection { - /// The range this section occupies. - pub range: Range, - /// The label to display in the placeholder for this section. - pub label: String, -} - -/// A completion for a slash command argument. -#[derive(Debug, Clone)] -pub struct SlashCommandArgumentCompletion { - /// The label to display for this completion. - pub label: String, - /// The new text that should be inserted into the command when this completion is accepted. - pub new_text: String, - /// Whether the command should be run when accepting this completion. - pub run_command: bool, -} diff --git a/crates/extension_api/Cargo.toml b/crates/extension_api/Cargo.toml deleted file mode 100644 index 829455e629..0000000000 --- a/crates/extension_api/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "zed_extension_api" -version = "0.8.0" -description = "APIs for creating Zed extensions in Rust" -repository = "https://github.com/zed-industries/zed" -documentation = "https://docs.rs/zed_extension_api" -keywords = ["zed", "extension"] -edition.workspace = true -# Change back to `true` when we're ready to publish v0.8.0. -publish = false -license = "Apache-2.0" - -[lints] -workspace = true - -[lib] -path = "src/extension_api.rs" - -[dependencies] -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -wit-bindgen = "0.41" - -[package.metadata.component] -target = { path = "wit" } diff --git a/crates/extension_api/LICENSE-APACHE b/crates/extension_api/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/crates/extension_api/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/extension_api/PENDING_CHANGES.md b/crates/extension_api/PENDING_CHANGES.md deleted file mode 100644 index 1d9875671b..0000000000 --- a/crates/extension_api/PENDING_CHANGES.md +++ /dev/null @@ -1,12 +0,0 @@ -# Pending Changes - -This is a list of pending changes to the Zed extension API that require a breaking change. - -This list should be updated as we notice things that should be changed so that we can batch them up in a single release. - -## vNext - -### Slash Commands - -- Rename `SlashCommand.tooltip_text` to `SlashCommand.menu_text` - - We may even want to remove it entirely, as right now this is only used for featured slash commands, and slash commands defined by extensions aren't currently able to be featured. diff --git a/crates/extension_api/README.md b/crates/extension_api/README.md deleted file mode 100644 index 89631269e7..0000000000 --- a/crates/extension_api/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# The Zed Rust Extension API - -This crate lets you write extensions for Zed in Rust. - -## Extension Manifest - -You'll need an `extension.toml` file at the root of your extension directory, with the following structure: - -```toml -id = "my-extension" -name = "My Extension" -description = "..." -version = "0.0.1" -schema_version = 1 -authors = ["Your Name "] -repository = "https://github.com/your/extension-repository" -``` - -## Cargo metadata - -Zed extensions are packaged as WebAssembly files. In your Cargo.toml, you'll -need to set your `crate-type` accordingly: - -```toml -[dependencies] -zed_extension_api = "0.6.0" - -[lib] -crate-type = ["cdylib"] -``` - -## Implementing an Extension - -To define your extension, create a type that implements the `Extension` trait, and register it. - -```rust -use zed_extension_api as zed; - -struct MyExtension { - // ... state -} - -impl zed::Extension for MyExtension { - // ... -} - -zed::register_extension!(MyExtension); -``` - -## Testing your extension - -To run your extension in Zed as you're developing it: - -- Make sure you have [Rust installed](https://www.rust-lang.org/learn/get-started) -- Have the `wasm32-wasip2` target installed (`rustup target add wasm32-wasip2`) -- Open the extensions view using the `zed: extensions` action in the command palette. -- Click the `Install Dev Extension` button in the top right -- Choose the path to your extension directory. - -## Compatible Zed versions - -Extensions created using newer versions of the Zed extension API won't be compatible with older versions of Zed. - -Here is the compatibility of the `zed_extension_api` with versions of Zed: - -| Zed version | `zed_extension_api` version | -| ----------- | --------------------------- | -| `0.192.x` | `0.0.1` - `0.6.0` | -| `0.186.x` | `0.0.1` - `0.5.0` | -| `0.184.x` | `0.0.1` - `0.4.0` | -| `0.178.x` | `0.0.1` - `0.3.0` | -| `0.162.x` | `0.0.1` - `0.2.0` | -| `0.149.x` | `0.0.1` - `0.1.0` | -| `0.131.x` | `0.0.1` - `0.0.6` | -| `0.130.x` | `0.0.1` - `0.0.5` | -| `0.129.x` | `0.0.1` - `0.0.4` | -| `0.128.x` | `0.0.1` | diff --git a/crates/extension_api/build.rs b/crates/extension_api/build.rs deleted file mode 100644 index bd9149578a..0000000000 --- a/crates/extension_api/build.rs +++ /dev/null @@ -1,15 +0,0 @@ -fn main() { - let version = std::env::var("CARGO_PKG_VERSION").unwrap(); - let out_dir = std::env::var("OUT_DIR").unwrap(); - - let mut parts = version.split(|c: char| !c.is_ascii_digit()); - let major = parts.next().unwrap().parse::().unwrap().to_be_bytes(); - let minor = parts.next().unwrap().parse::().unwrap().to_be_bytes(); - let patch = parts.next().unwrap().parse::().unwrap().to_be_bytes(); - - std::fs::write( - std::path::Path::new(&out_dir).join("version_bytes"), - [major[0], major[1], minor[0], minor[1], patch[0], patch[1]], - ) - .unwrap(); -} diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs deleted file mode 100644 index 9418623224..0000000000 --- a/crates/extension_api/src/extension_api.rs +++ /dev/null @@ -1,586 +0,0 @@ -//! The Zed Rust Extension API allows you write extensions for [Zed](https://zed.dev/) in Rust. - -pub mod http_client; -pub mod process; -pub mod settings; - -use core::fmt; - -use wit::*; - -pub use serde_json; - -// WIT re-exports. -// -// We explicitly enumerate the symbols we want to re-export, as there are some -// that we may want to shadow to provide a cleaner Rust API. -pub use wit::{ - CodeLabel, CodeLabelSpan, CodeLabelSpanLiteral, Command, DownloadedFileType, EnvVars, - KeyValueStore, LanguageServerInstallationStatus, Project, Range, Worktree, download_file, - make_file_executable, - zed::extension::context_server::ContextServerConfiguration, - zed::extension::dap::{ - AttachRequest, BuildTaskDefinition, BuildTaskDefinitionTemplatePayload, BuildTaskTemplate, - DebugAdapterBinary, DebugConfig, DebugRequest, DebugScenario, DebugTaskDefinition, - LaunchRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, - TaskTemplate, TcpArguments, TcpArgumentsTemplate, resolve_tcp_template, - }, - zed::extension::github::{ - GithubRelease, GithubReleaseAsset, GithubReleaseOptions, github_release_by_tag_name, - latest_github_release, - }, - zed::extension::nodejs::{ - node_binary_path, npm_install_package, npm_package_installed_version, - npm_package_latest_version, - }, - zed::extension::platform::{Architecture, Os, current_platform}, - zed::extension::slash_command::{ - SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput, SlashCommandOutputSection, - }, -}; - -// Undocumented WIT re-exports. -// -// These are symbols that need to be public for the purposes of implementing -// the extension host, but aren't relevant to extension authors. -#[doc(hidden)] -pub use wit::Guest; - -/// Constructs for interacting with language servers over the -/// Language Server Protocol (LSP). -pub mod lsp { - pub use crate::wit::zed::extension::lsp::{ - Completion, CompletionKind, InsertTextFormat, Symbol, SymbolKind, - }; -} - -/// A result returned from a Zed extension. -pub type Result = core::result::Result; - -/// Updates the installation status for the given language server. -pub fn set_language_server_installation_status( - language_server_id: &LanguageServerId, - status: &LanguageServerInstallationStatus, -) { - wit::set_language_server_installation_status(&language_server_id.0, status) -} - -/// A Zed extension. -pub trait Extension: Send + Sync { - /// Returns a new instance of the extension. - fn new() -> Self - where - Self: Sized; - - /// Returns the command used to start the language server for the specified - /// language. - fn language_server_command( - &mut self, - _language_server_id: &LanguageServerId, - _worktree: &Worktree, - ) -> Result { - Err("`language_server_command` not implemented".to_string()) - } - - /// Returns the initialization options to pass to the specified language server. - fn language_server_initialization_options( - &mut self, - _language_server_id: &LanguageServerId, - _worktree: &Worktree, - ) -> Result> { - Ok(None) - } - - /// Returns the workspace configuration options to pass to the language server. - fn language_server_workspace_configuration( - &mut self, - _language_server_id: &LanguageServerId, - _worktree: &Worktree, - ) -> Result> { - Ok(None) - } - - /// Returns the initialization options to pass to the other language server. - fn language_server_additional_initialization_options( - &mut self, - _language_server_id: &LanguageServerId, - _target_language_server_id: &LanguageServerId, - _worktree: &Worktree, - ) -> Result> { - Ok(None) - } - - /// Returns the workspace configuration options to pass to the other language server. - fn language_server_additional_workspace_configuration( - &mut self, - _language_server_id: &LanguageServerId, - _target_language_server_id: &LanguageServerId, - _worktree: &Worktree, - ) -> Result> { - Ok(None) - } - - /// Returns the label for the given completion. - fn label_for_completion( - &self, - _language_server_id: &LanguageServerId, - _completion: Completion, - ) -> Option { - None - } - - /// Returns the label for the given symbol. - fn label_for_symbol( - &self, - _language_server_id: &LanguageServerId, - _symbol: Symbol, - ) -> Option { - None - } - - /// Returns the completions that should be shown when completing the provided slash command with the given query. - fn complete_slash_command_argument( - &self, - _command: SlashCommand, - _args: Vec, - ) -> Result, String> { - Ok(Vec::new()) - } - - /// Returns the output from running the provided slash command. - fn run_slash_command( - &self, - _command: SlashCommand, - _args: Vec, - _worktree: Option<&Worktree>, - ) -> Result { - Err("`run_slash_command` not implemented".to_string()) - } - - /// Returns the command used to start a context server. - fn context_server_command( - &mut self, - _context_server_id: &ContextServerId, - _project: &Project, - ) -> Result { - Err("`context_server_command` not implemented".to_string()) - } - - /// Returns the configuration options for the specified context server. - fn context_server_configuration( - &mut self, - _context_server_id: &ContextServerId, - _project: &Project, - ) -> Result> { - Ok(None) - } - - /// Returns a list of package names as suggestions to be included in the - /// search results of the `/docs` slash command. - /// - /// 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. - fn suggest_docs_packages(&self, _provider: String) -> Result, String> { - Ok(Vec::new()) - } - - /// Indexes the docs for the specified package. - fn index_docs( - &self, - _provider: String, - _package: String, - _database: &KeyValueStore, - ) -> Result<(), String> { - Err("`index_docs` not implemented".to_string()) - } - - /// Returns the debug adapter binary for the specified adapter name and configuration. - fn get_dap_binary( - &mut self, - _adapter_name: String, - _config: DebugTaskDefinition, - _user_provided_debug_adapter_path: Option, - _worktree: &Worktree, - ) -> Result { - Err("`get_dap_binary` not implemented".to_string()) - } - - /// Determines whether the specified adapter configuration should *launch* a new debuggee process - /// or *attach* to an existing one. This function should not perform any further validation (outside of determining the kind of a request). - /// This function should return an error when the kind cannot be determined (rather than fall back to a known default). - fn dap_request_kind( - &mut self, - _adapter_name: String, - _config: serde_json::Value, - ) -> Result { - Err("`dap_request_kind` not implemented".to_string()) - } - /// Converts a high-level definition of a debug scenario (originating in a new session UI) to a "low-level" configuration suitable for a particular adapter. - /// - /// In layman's terms: given a program, list of arguments, current working directory and environment variables, - /// create a configuration that can be used to start a debug session. - fn dap_config_to_scenario(&mut self, _config: DebugConfig) -> Result { - Err("`dap_config_to_scenario` not implemented".to_string()) - } - - /// Locators are entities that convert a Zed task into a debug scenario. - /// - /// They can be provided even by extensions that don't provide a debug adapter. - /// For all tasks applicable to a given buffer, Zed will query all locators to find one that can turn the task into a debug scenario. - /// A converted debug scenario can include a build task (it shouldn't contain any configuration in such case); a build task result will later - /// be resolved with [`Extension::run_dap_locator`]. - /// - /// 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. - /// 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. - /// - /// 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. - /// This might be of particular relevance to interpreted languages. - fn dap_locator_create_scenario( - &mut self, - _locator_name: String, - _build_task: TaskTemplate, - _resolved_label: String, - _debug_adapter_name: String, - ) -> Option { - None - } - - /// Runs the second phase of locator resolution. - /// See [`Extension::dap_locator_create_scenario`] for a hefty comment on locators. - fn run_dap_locator( - &mut self, - _locator_name: String, - _build_task: TaskTemplate, - ) -> Result { - Err("`run_dap_locator` not implemented".to_string()) - } -} - -/// Registers the provided type as a Zed extension. -/// -/// The type must implement the [`Extension`] trait. -#[macro_export] -macro_rules! register_extension { - ($extension_type:ty) => { - #[cfg(target_os = "wasi")] - mod wasi_ext { - unsafe extern "C" { - static mut errno: i32; - pub static mut __wasilibc_cwd: *mut std::ffi::c_char; - } - - pub fn init_cwd() { - unsafe { - // Ensure that our chdir function is linked, instead of the - // one from wasi-libc in the chdir.o translation unit. Otherwise - // we risk linking in `__wasilibc_find_relpath_alloc` which - // is a weak symbol and is being used by - // `__wasilibc_find_relpath`, which we do not want on - // Windows. - chdir(std::ptr::null()); - - __wasilibc_cwd = std::ffi::CString::new(std::env::var("PWD").unwrap()) - .unwrap() - .into_raw() - .cast(); - } - } - - #[unsafe(no_mangle)] - pub unsafe extern "C" fn chdir(raw_path: *const std::ffi::c_char) -> i32 { - // Forbid extensions from changing CWD and so return an appropriate error code. - errno = 58; // NOTSUP - return -1; - } - } - - #[unsafe(export_name = "init-extension")] - pub extern "C" fn __init_extension() { - #[cfg(target_os = "wasi")] - wasi_ext::init_cwd(); - - zed_extension_api::register_extension(|| { - Box::new(<$extension_type as zed_extension_api::Extension>::new()) - }); - } - }; -} - -#[doc(hidden)] -pub fn register_extension(build_extension: fn() -> Box) { - unsafe { EXTENSION = Some((build_extension)()) } -} - -fn extension() -> &'static mut dyn Extension { - #[expect(static_mut_refs)] - unsafe { - EXTENSION.as_deref_mut().unwrap() - } -} - -static mut EXTENSION: Option> = None; - -#[cfg(target_arch = "wasm32")] -#[unsafe(link_section = "zed:api-version")] -#[doc(hidden)] -pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes")); - -mod wit { - - wit_bindgen::generate!({ - skip: ["init-extension"], - path: "./wit/since_v0.8.0", - }); -} - -wit::export!(Component); - -struct Component; - -impl wit::Guest for Component { - fn language_server_command( - language_server_id: String, - worktree: &wit::Worktree, - ) -> Result { - let language_server_id = LanguageServerId(language_server_id); - extension().language_server_command(&language_server_id, worktree) - } - - fn language_server_initialization_options( - language_server_id: String, - worktree: &Worktree, - ) -> Result, String> { - let language_server_id = LanguageServerId(language_server_id); - Ok(extension() - .language_server_initialization_options(&language_server_id, worktree)? - .and_then(|value| serde_json::to_string(&value).ok())) - } - - fn language_server_workspace_configuration( - language_server_id: String, - worktree: &Worktree, - ) -> Result, String> { - let language_server_id = LanguageServerId(language_server_id); - Ok(extension() - .language_server_workspace_configuration(&language_server_id, worktree)? - .and_then(|value| serde_json::to_string(&value).ok())) - } - - fn language_server_additional_initialization_options( - language_server_id: String, - target_language_server_id: String, - worktree: &Worktree, - ) -> Result, String> { - let language_server_id = LanguageServerId(language_server_id); - let target_language_server_id = LanguageServerId(target_language_server_id); - Ok(extension() - .language_server_additional_initialization_options( - &language_server_id, - &target_language_server_id, - worktree, - )? - .and_then(|value| serde_json::to_string(&value).ok())) - } - - fn language_server_additional_workspace_configuration( - language_server_id: String, - target_language_server_id: String, - worktree: &Worktree, - ) -> Result, String> { - let language_server_id = LanguageServerId(language_server_id); - let target_language_server_id = LanguageServerId(target_language_server_id); - Ok(extension() - .language_server_additional_workspace_configuration( - &language_server_id, - &target_language_server_id, - worktree, - )? - .and_then(|value| serde_json::to_string(&value).ok())) - } - - fn labels_for_completions( - language_server_id: String, - completions: Vec, - ) -> Result>, String> { - let language_server_id = LanguageServerId(language_server_id); - let mut labels = Vec::new(); - for (ix, completion) in completions.into_iter().enumerate() { - let label = extension().label_for_completion(&language_server_id, completion); - if let Some(label) = label { - labels.resize(ix + 1, None); - *labels.last_mut().unwrap() = Some(label); - } - } - Ok(labels) - } - - fn labels_for_symbols( - language_server_id: String, - symbols: Vec, - ) -> Result>, String> { - let language_server_id = LanguageServerId(language_server_id); - let mut labels = Vec::new(); - for (ix, symbol) in symbols.into_iter().enumerate() { - let label = extension().label_for_symbol(&language_server_id, symbol); - if let Some(label) = label { - labels.resize(ix + 1, None); - *labels.last_mut().unwrap() = Some(label); - } - } - Ok(labels) - } - - fn complete_slash_command_argument( - command: SlashCommand, - args: Vec, - ) -> Result, String> { - extension().complete_slash_command_argument(command, args) - } - - fn run_slash_command( - command: SlashCommand, - args: Vec, - worktree: Option<&Worktree>, - ) -> Result { - extension().run_slash_command(command, args, worktree) - } - - fn context_server_command( - context_server_id: String, - project: &Project, - ) -> Result { - let context_server_id = ContextServerId(context_server_id); - extension().context_server_command(&context_server_id, project) - } - - fn context_server_configuration( - context_server_id: String, - project: &Project, - ) -> Result, String> { - let context_server_id = ContextServerId(context_server_id); - extension().context_server_configuration(&context_server_id, project) - } - - fn suggest_docs_packages(provider: String) -> Result, String> { - extension().suggest_docs_packages(provider) - } - - fn index_docs( - provider: String, - package: String, - database: &KeyValueStore, - ) -> Result<(), String> { - extension().index_docs(provider, package, database) - } - - fn get_dap_binary( - adapter_name: String, - config: DebugTaskDefinition, - user_installed_path: Option, - worktree: &Worktree, - ) -> Result { - extension().get_dap_binary(adapter_name, config, user_installed_path, worktree) - } - - fn dap_request_kind( - adapter_name: String, - config: String, - ) -> Result { - extension().dap_request_kind( - adapter_name, - serde_json::from_str(&config).map_err(|e| format!("Failed to parse config: {e}"))?, - ) - } - fn dap_config_to_scenario(config: DebugConfig) -> Result { - extension().dap_config_to_scenario(config) - } - fn dap_locator_create_scenario( - locator_name: String, - build_task: TaskTemplate, - resolved_label: String, - debug_adapter_name: String, - ) -> Option { - extension().dap_locator_create_scenario( - locator_name, - build_task, - resolved_label, - debug_adapter_name, - ) - } - fn run_dap_locator( - locator_name: String, - build_task: TaskTemplate, - ) -> Result { - extension().run_dap_locator(locator_name, build_task) - } -} - -/// The ID of a language server. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] -pub struct LanguageServerId(String); - -impl AsRef for LanguageServerId { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl fmt::Display for LanguageServerId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -/// The ID of a context server. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] -pub struct ContextServerId(String); - -impl AsRef for ContextServerId { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl fmt::Display for ContextServerId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl CodeLabelSpan { - /// Returns a [`CodeLabelSpan::CodeRange`]. - pub fn code_range(range: impl Into) -> Self { - Self::CodeRange(range.into()) - } - - /// Returns a [`CodeLabelSpan::Literal`]. - pub fn literal(text: impl Into, highlight_name: Option) -> Self { - Self::Literal(CodeLabelSpanLiteral { - text: text.into(), - highlight_name, - }) - } -} - -impl From> for wit::Range { - fn from(value: std::ops::Range) -> Self { - Self { - start: value.start, - end: value.end, - } - } -} - -impl From> for wit::Range { - fn from(value: std::ops::Range) -> Self { - Self { - start: value.start as u32, - end: value.end as u32, - } - } -} diff --git a/crates/extension_api/src/http_client.rs b/crates/extension_api/src/http_client.rs deleted file mode 100644 index 9e30da8db4..0000000000 --- a/crates/extension_api/src/http_client.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! An HTTP client. - -pub use crate::wit::zed::extension::http_client::{ - HttpMethod, HttpRequest, HttpResponse, HttpResponseStream, RedirectPolicy, fetch, fetch_stream, -}; - -impl HttpRequest { - /// Returns a builder for an [`HttpRequest`]. - pub fn builder() -> HttpRequestBuilder { - HttpRequestBuilder::new() - } - - /// Executes the [`HttpRequest`] with [`fetch`]. - pub fn fetch(&self) -> Result { - fetch(self) - } - - /// Executes the [`HttpRequest`] with [`fetch_stream`]. - pub fn fetch_stream(&self) -> Result { - fetch_stream(self) - } -} - -/// A builder for an [`HttpRequest`]. -#[derive(Clone)] -pub struct HttpRequestBuilder { - method: Option, - url: Option, - headers: Vec<(String, String)>, - body: Option>, - redirect_policy: RedirectPolicy, -} - -impl Default for HttpRequestBuilder { - fn default() -> Self { - Self::new() - } -} - -impl HttpRequestBuilder { - /// Returns a new [`HttpRequestBuilder`]. - pub fn new() -> Self { - HttpRequestBuilder { - method: None, - url: None, - headers: Vec::new(), - body: None, - redirect_policy: RedirectPolicy::NoFollow, - } - } - - /// Sets the HTTP method for the request. - pub fn method(mut self, method: HttpMethod) -> Self { - self.method = Some(method); - self - } - - /// Sets the URL for the request. - pub fn url(mut self, url: impl Into) -> Self { - self.url = Some(url.into()); - self - } - - /// Adds a header to the request. - pub fn header(mut self, name: impl Into, value: impl Into) -> Self { - self.headers.push((name.into(), value.into())); - self - } - - /// Adds the specified headers to the request. - pub fn headers(mut self, headers: impl IntoIterator) -> Self { - self.headers.extend(headers); - self - } - - /// Sets the body of the request. - pub fn body(mut self, body: impl Into>) -> Self { - self.body = Some(body.into()); - self - } - - /// Sets the redirect policy for the request. - pub fn redirect_policy(mut self, policy: RedirectPolicy) -> Self { - self.redirect_policy = policy; - self - } - - /// Builds the [`HttpRequest`]. - pub fn build(self) -> Result { - let method = self.method.ok_or_else(|| "Method not set".to_string())?; - let url = self.url.ok_or_else(|| "URL not set".to_string())?; - - Ok(HttpRequest { - method, - url, - headers: self.headers, - body: self.body, - redirect_policy: self.redirect_policy, - }) - } -} diff --git a/crates/extension_api/src/process.rs b/crates/extension_api/src/process.rs deleted file mode 100644 index 1068fd9c17..0000000000 --- a/crates/extension_api/src/process.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! A module for working with processes. - -use crate::wit::zed::extension::process; -pub use crate::wit::zed::extension::process::{Command, Output}; - -impl Command { - pub fn new(program: impl Into) -> Self { - Self { - command: program.into(), - args: Vec::new(), - env: Vec::new(), - } - } - - pub fn arg(mut self, arg: impl Into) -> Self { - self.args.push(arg.into()); - self - } - - pub fn args(mut self, args: impl IntoIterator>) -> Self { - self.args.extend(args.into_iter().map(Into::into)); - self - } - - pub fn env(mut self, key: impl Into, value: impl Into) -> Self { - self.env.push((key.into(), value.into())); - self - } - - pub fn envs( - mut self, - envs: impl IntoIterator, impl Into)>, - ) -> Self { - self.env.extend( - envs.into_iter() - .map(|(key, value)| (key.into(), value.into())), - ); - self - } - - pub fn output(&mut self) -> Result { - process::run_command(self) - } -} diff --git a/crates/extension_api/src/settings.rs b/crates/extension_api/src/settings.rs deleted file mode 100644 index a133a8027a..0000000000 --- a/crates/extension_api/src/settings.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Provides access to Zed settings. - -#[path = "../wit/since_v0.2.0/settings.rs"] -mod types; - -use crate::{Project, Result, SettingsLocation, Worktree, wit}; -use serde_json; -pub use types::*; - -impl LanguageSettings { - /// Returns the [`LanguageSettings`] for the given language. - pub fn for_worktree(language: Option<&str>, worktree: &Worktree) -> Result { - get_settings("language", language, Some(worktree.id())) - } -} - -impl LspSettings { - /// Returns the [`LspSettings`] for the given language server. - pub fn for_worktree(language_server_name: &str, worktree: &Worktree) -> Result { - get_settings("lsp", Some(language_server_name), Some(worktree.id())) - } -} - -impl ContextServerSettings { - /// Returns the [`ContextServerSettings`] for the given context server. - pub fn for_project(context_server_id: &str, project: &Project) -> Result { - let global_setting: Self = get_settings("context_servers", Some(context_server_id), None)?; - - for worktree_id in project.worktree_ids() { - let settings = get_settings( - "context_servers", - Some(context_server_id), - Some(worktree_id), - )?; - if settings != global_setting { - return Ok(settings); - } - } - - Ok(global_setting) - } -} - -fn get_settings( - settings_type: &str, - settings_name: Option<&str>, - worktree_id: Option, -) -> Result { - let location = worktree_id.map(|worktree_id| SettingsLocation { - worktree_id, - path: String::new(), - }); - let settings_json = wit::get_settings(location.as_ref(), settings_type, settings_name)?; - let settings: T = serde_json::from_str(&settings_json).map_err(|err| err.to_string())?; - Ok(settings) -} diff --git a/crates/extension_api/wit/since_v0.0.1/extension.wit b/crates/extension_api/wit/since_v0.0.1/extension.wit deleted file mode 100644 index 339a974169..0000000000 --- a/crates/extension_api/wit/since_v0.0.1/extension.wit +++ /dev/null @@ -1,70 +0,0 @@ -package zed:extension; - -world extension { - use github.{github-release, github-release-options}; - use platform.{os, architecture}; - - export init-extension: func(); - - enum downloaded-file-type { - gzip, - gzip-tar, - zip, - uncompressed, - } - - variant language-server-installation-status { - checking-for-update, - downloaded, - downloading, - cached, - failed(string), - } - - /// Gets the current operating system and architecture - import current-platform: func() -> tuple; - - /// Get the path to the node binary used by Zed. - import node-binary-path: func() -> result; - - /// Gets the latest version of the given NPM package. - import npm-package-latest-version: func(package-name: string) -> result; - - /// Returns the installed version of the given NPM package, if it exists. - import npm-package-installed-version: func(package-name: string) -> result, string>; - - /// Installs the specified NPM package. - import npm-install-package: func(package-name: string, version: string) -> result<_, string>; - - /// Gets the latest release for the given GitHub repository. - import latest-github-release: func(repo: string, options: github-release-options) -> result; - - /// Downloads a file from the given url, and saves it to the given filename within the extension's - /// working directory. Extracts the file according to the given file type. - import download-file: func(url: string, output-filename: string, file-type: downloaded-file-type) -> result<_, string>; - - /// Updates the installation status for the given language server. - import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status); - - type env-vars = list>; - - record command { - command: string, - args: list, - env: env-vars, - } - - resource worktree { - read-text-file: func(path: string) -> result; - which: func(binary-name: string) -> option; - shell-env: func() -> env-vars; - } - - record language-server-config { - name: string, - language-name: string, - } - - export language-server-command: func(config: language-server-config, worktree: borrow) -> result; - export language-server-initialization-options: func(config: language-server-config, worktree: borrow) -> result, string>; -} diff --git a/crates/extension_api/wit/since_v0.0.1/github.wit b/crates/extension_api/wit/since_v0.0.1/github.wit deleted file mode 100644 index 53ecacb720..0000000000 --- a/crates/extension_api/wit/since_v0.0.1/github.wit +++ /dev/null @@ -1,28 +0,0 @@ -interface github { - /// A GitHub release. - record github-release { - /// The version of the release. - version: string, - /// The list of assets attached to the release. - assets: list, - } - - /// An asset from a GitHub release. - record github-release-asset { - /// The name of the asset. - name: string, - /// The download URL for the asset. - download-url: string, - } - - /// The options used to filter down GitHub releases. - record github-release-options { - /// Whether releases without assets should be included. - require-assets: bool, - /// Whether pre-releases should be included. - pre-release: bool, - } - - /// Returns the latest release for the given GitHub repository. - latest-github-release: func(repo: string, options: github-release-options) -> result; -} diff --git a/crates/extension_api/wit/since_v0.0.1/platform.wit b/crates/extension_api/wit/since_v0.0.1/platform.wit deleted file mode 100644 index 48472a99bc..0000000000 --- a/crates/extension_api/wit/since_v0.0.1/platform.wit +++ /dev/null @@ -1,24 +0,0 @@ -interface platform { - /// An operating system. - enum os { - /// macOS. - mac, - /// Linux. - linux, - /// Windows. - windows, - } - - /// A platform architecture. - enum architecture { - /// AArch64 (e.g., Apple Silicon). - aarch64, - /// x86. - x86, - /// x86-64. - x8664, - } - - /// Gets the current operating system and architecture. - current-platform: func() -> tuple; -} diff --git a/crates/extension_api/wit/since_v0.0.4/extension.wit b/crates/extension_api/wit/since_v0.0.4/extension.wit deleted file mode 100644 index c6f3e73506..0000000000 --- a/crates/extension_api/wit/since_v0.0.4/extension.wit +++ /dev/null @@ -1,72 +0,0 @@ -package zed:extension; - -world extension { - use github.{github-release, github-release-options}; - use platform.{os, architecture}; - - export init-extension: func(); - - enum downloaded-file-type { - gzip, - gzip-tar, - zip, - uncompressed, - } - - variant language-server-installation-status { - none, - downloading, - checking-for-update, - failed(string), - } - - /// Gets the current operating system and architecture - import current-platform: func() -> tuple; - - /// Get the path to the node binary used by Zed. - import node-binary-path: func() -> result; - - /// Gets the latest version of the given NPM package. - import npm-package-latest-version: func(package-name: string) -> result; - - /// Returns the installed version of the given NPM package, if it exists. - import npm-package-installed-version: func(package-name: string) -> result, string>; - - /// Installs the specified NPM package. - import npm-install-package: func(package-name: string, version: string) -> result<_, string>; - - /// Gets the latest release for the given GitHub repository. - import latest-github-release: func(repo: string, options: github-release-options) -> result; - - /// Downloads a file from the given url, and saves it to the given path within the extension's - /// working directory. Extracts the file according to the given file type. - import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>; - - /// Makes the file at the given path executable. - import make-file-executable: func(filepath: string) -> result<_, string>; - - /// Updates the installation status for the given language server. - import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status); - - type env-vars = list>; - - record command { - command: string, - args: list, - env: env-vars, - } - - resource worktree { - read-text-file: func(path: string) -> result; - which: func(binary-name: string) -> option; - shell-env: func() -> env-vars; - } - - record language-server-config { - name: string, - language-name: string, - } - - export language-server-command: func(config: language-server-config, worktree: borrow) -> result; - export language-server-initialization-options: func(config: language-server-config, worktree: borrow) -> result, string>; -} diff --git a/crates/extension_api/wit/since_v0.0.4/github.wit b/crates/extension_api/wit/since_v0.0.4/github.wit deleted file mode 100644 index 53ecacb720..0000000000 --- a/crates/extension_api/wit/since_v0.0.4/github.wit +++ /dev/null @@ -1,28 +0,0 @@ -interface github { - /// A GitHub release. - record github-release { - /// The version of the release. - version: string, - /// The list of assets attached to the release. - assets: list, - } - - /// An asset from a GitHub release. - record github-release-asset { - /// The name of the asset. - name: string, - /// The download URL for the asset. - download-url: string, - } - - /// The options used to filter down GitHub releases. - record github-release-options { - /// Whether releases without assets should be included. - require-assets: bool, - /// Whether pre-releases should be included. - pre-release: bool, - } - - /// Returns the latest release for the given GitHub repository. - latest-github-release: func(repo: string, options: github-release-options) -> result; -} diff --git a/crates/extension_api/wit/since_v0.0.4/platform.wit b/crates/extension_api/wit/since_v0.0.4/platform.wit deleted file mode 100644 index 48472a99bc..0000000000 --- a/crates/extension_api/wit/since_v0.0.4/platform.wit +++ /dev/null @@ -1,24 +0,0 @@ -interface platform { - /// An operating system. - enum os { - /// macOS. - mac, - /// Linux. - linux, - /// Windows. - windows, - } - - /// A platform architecture. - enum architecture { - /// AArch64 (e.g., Apple Silicon). - aarch64, - /// x86. - x86, - /// x86-64. - x8664, - } - - /// Gets the current operating system and architecture. - current-platform: func() -> tuple; -} diff --git a/crates/extension_api/wit/since_v0.0.6/extension.wit b/crates/extension_api/wit/since_v0.0.6/extension.wit deleted file mode 100644 index 2f42cc0365..0000000000 --- a/crates/extension_api/wit/since_v0.0.6/extension.wit +++ /dev/null @@ -1,130 +0,0 @@ -package zed:extension; - -world extension { - import github; - import platform; - import nodejs; - - use lsp.{completion, symbol}; - - /// Initializes the extension. - export init-extension: func(); - - /// The type of a downloaded file. - enum downloaded-file-type { - /// A gzipped file (`.gz`). - gzip, - /// A gzipped tar archive (`.tar.gz`). - gzip-tar, - /// A ZIP file (`.zip`). - zip, - /// An uncompressed file. - uncompressed, - } - - /// The installation status for a language server. - variant language-server-installation-status { - /// The language server has no installation status. - none, - /// The language server is being downloaded. - downloading, - /// The language server is checking for updates. - checking-for-update, - /// The language server installation failed for specified reason. - failed(string), - } - - record settings-location { - worktree-id: u64, - path: string, - } - - import get-settings: func(path: option, category: string, key: option) -> result; - - /// Downloads a file from the given URL and saves it to the given path within the extension's - /// working directory. - /// - /// The file will be extracted according to the given file type. - import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>; - - /// Makes the file at the given path executable. - import make-file-executable: func(filepath: string) -> result<_, string>; - - /// Updates the installation status for the given language server. - import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status); - - /// A list of environment variables. - type env-vars = list>; - - /// A command. - record command { - /// The command to execute. - command: string, - /// The arguments to pass to the command. - args: list, - /// The environment variables to set for the command. - env: env-vars, - } - - /// A Zed worktree. - resource worktree { - /// Returns the ID of the worktree. - id: func() -> u64; - /// Returns the root path of the worktree. - root-path: func() -> string; - /// Returns the textual contents of the specified file in the worktree. - read-text-file: func(path: string) -> result; - /// Returns the path to the given binary name, if one is present on the `$PATH`. - which: func(binary-name: string) -> option; - /// Returns the current shell environment. - shell-env: func() -> env-vars; - } - - /// Returns the command used to start up the language server. - export language-server-command: func(language-server-id: string, worktree: borrow) -> result; - - /// Returns the initialization options to pass to the language server on startup. - /// - /// The initialization options are represented as a JSON string. - export language-server-initialization-options: func(language-server-id: string, worktree: borrow) -> result, string>; - - /// Returns the workspace configuration options to pass to the language server. - export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow) -> result, string>; - - /// A label containing some code. - record code-label { - /// The source code to parse with Tree-sitter. - code: string, - /// The spans to display in the label. - spans: list, - /// The range of the displayed label to include when filtering. - filter-range: range, - } - - /// A span within a code label. - variant code-label-span { - /// A range into the parsed code. - code-range(range), - /// A span containing a code literal. - literal(code-label-span-literal), - } - - /// A span containing a code literal. - record code-label-span-literal { - /// The literal text. - text: string, - /// The name of the highlight to use for this literal. - highlight-name: option, - } - - /// A (half-open) range (`[start, end)`). - record range { - /// The start of the range (inclusive). - start: u32, - /// The end of the range (exclusive). - end: u32, - } - - export labels-for-completions: func(language-server-id: string, completions: list) -> result>, string>; - export labels-for-symbols: func(language-server-id: string, symbols: list) -> result>, string>; -} diff --git a/crates/extension_api/wit/since_v0.0.6/github.wit b/crates/extension_api/wit/since_v0.0.6/github.wit deleted file mode 100644 index 53ecacb720..0000000000 --- a/crates/extension_api/wit/since_v0.0.6/github.wit +++ /dev/null @@ -1,28 +0,0 @@ -interface github { - /// A GitHub release. - record github-release { - /// The version of the release. - version: string, - /// The list of assets attached to the release. - assets: list, - } - - /// An asset from a GitHub release. - record github-release-asset { - /// The name of the asset. - name: string, - /// The download URL for the asset. - download-url: string, - } - - /// The options used to filter down GitHub releases. - record github-release-options { - /// Whether releases without assets should be included. - require-assets: bool, - /// Whether pre-releases should be included. - pre-release: bool, - } - - /// Returns the latest release for the given GitHub repository. - latest-github-release: func(repo: string, options: github-release-options) -> result; -} diff --git a/crates/extension_api/wit/since_v0.0.6/lsp.wit b/crates/extension_api/wit/since_v0.0.6/lsp.wit deleted file mode 100644 index 19e81b6b14..0000000000 --- a/crates/extension_api/wit/since_v0.0.6/lsp.wit +++ /dev/null @@ -1,83 +0,0 @@ -interface lsp { - /// An LSP completion. - record completion { - label: string, - detail: option, - kind: option, - insert-text-format: option, - } - - /// The kind of an LSP completion. - variant completion-kind { - text, - method, - function, - %constructor, - field, - variable, - class, - %interface, - module, - property, - unit, - value, - %enum, - keyword, - snippet, - color, - file, - reference, - folder, - enum-member, - constant, - struct, - event, - operator, - type-parameter, - other(s32), - } - - /// Defines how to interpret the insert text in a completion item. - variant insert-text-format { - plain-text, - snippet, - other(s32), - } - - /// An LSP symbol. - record symbol { - kind: symbol-kind, - name: string, - } - - /// The kind of an LSP symbol. - variant symbol-kind { - file, - module, - namespace, - %package, - class, - method, - property, - field, - %constructor, - %enum, - %interface, - function, - variable, - constant, - %string, - number, - boolean, - array, - object, - key, - null, - enum-member, - struct, - event, - operator, - type-parameter, - other(s32), - } -} diff --git a/crates/extension_api/wit/since_v0.0.6/nodejs.wit b/crates/extension_api/wit/since_v0.0.6/nodejs.wit deleted file mode 100644 index c814548314..0000000000 --- a/crates/extension_api/wit/since_v0.0.6/nodejs.wit +++ /dev/null @@ -1,13 +0,0 @@ -interface nodejs { - /// Returns the path to the Node binary used by Zed. - node-binary-path: func() -> result; - - /// Returns the latest version of the given NPM package. - npm-package-latest-version: func(package-name: string) -> result; - - /// Returns the installed version of the given NPM package, if it exists. - npm-package-installed-version: func(package-name: string) -> result, string>; - - /// Installs the specified NPM package. - npm-install-package: func(package-name: string, version: string) -> result<_, string>; -} diff --git a/crates/extension_api/wit/since_v0.0.6/platform.wit b/crates/extension_api/wit/since_v0.0.6/platform.wit deleted file mode 100644 index 48472a99bc..0000000000 --- a/crates/extension_api/wit/since_v0.0.6/platform.wit +++ /dev/null @@ -1,24 +0,0 @@ -interface platform { - /// An operating system. - enum os { - /// macOS. - mac, - /// Linux. - linux, - /// Windows. - windows, - } - - /// A platform architecture. - enum architecture { - /// AArch64 (e.g., Apple Silicon). - aarch64, - /// x86. - x86, - /// x86-64. - x8664, - } - - /// Gets the current operating system and architecture. - current-platform: func() -> tuple; -} diff --git a/crates/extension_api/wit/since_v0.0.6/settings.rs b/crates/extension_api/wit/since_v0.0.6/settings.rs deleted file mode 100644 index 5c6cae7064..0000000000 --- a/crates/extension_api/wit/since_v0.0.6/settings.rs +++ /dev/null @@ -1,29 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::num::NonZeroU32; - -/// The settings for a particular language. -#[derive(Debug, Serialize, Deserialize)] -pub struct LanguageSettings { - /// How many columns a tab should occupy. - pub tab_size: NonZeroU32, -} - -/// The settings for a particular language server. -#[derive(Default, Debug, Serialize, Deserialize)] -pub struct LspSettings { - /// The settings for the language server binary. - pub binary: Option, - /// The initialization options to pass to the language server. - pub initialization_options: Option, - /// The settings to pass to language server. - pub settings: Option, -} - -/// The settings for a language server binary. -#[derive(Debug, Serialize, Deserialize)] -pub struct BinarySettings { - /// The path to the binary. - pub path: Option, - /// The arguments to pass to the binary. - pub arguments: Option>, -} diff --git a/crates/extension_api/wit/since_v0.1.0/common.wit b/crates/extension_api/wit/since_v0.1.0/common.wit deleted file mode 100644 index c4f321f4c7..0000000000 --- a/crates/extension_api/wit/since_v0.1.0/common.wit +++ /dev/null @@ -1,9 +0,0 @@ -interface common { - /// A (half-open) range (`[start, end)`). - record range { - /// The start of the range (inclusive). - start: u32, - /// The end of the range (exclusive). - end: u32, - } -} diff --git a/crates/extension_api/wit/since_v0.1.0/extension.wit b/crates/extension_api/wit/since_v0.1.0/extension.wit deleted file mode 100644 index c7599f93ff..0000000000 --- a/crates/extension_api/wit/since_v0.1.0/extension.wit +++ /dev/null @@ -1,147 +0,0 @@ -package zed:extension; - -world extension { - import github; - import http-client; - import platform; - import nodejs; - - use common.{range}; - use lsp.{completion, symbol}; - use slash-command.{slash-command, slash-command-argument-completion, slash-command-output}; - - /// Initializes the extension. - export init-extension: func(); - - /// The type of a downloaded file. - enum downloaded-file-type { - /// A gzipped file (`.gz`). - gzip, - /// A gzipped tar archive (`.tar.gz`). - gzip-tar, - /// A ZIP file (`.zip`). - zip, - /// An uncompressed file. - uncompressed, - } - - /// The installation status for a language server. - variant language-server-installation-status { - /// The language server has no installation status. - none, - /// The language server is being downloaded. - downloading, - /// The language server is checking for updates. - checking-for-update, - /// The language server installation failed for specified reason. - failed(string), - } - - record settings-location { - worktree-id: u64, - path: string, - } - - import get-settings: func(path: option, category: string, key: option) -> result; - - /// Downloads a file from the given URL and saves it to the given path within the extension's - /// working directory. - /// - /// The file will be extracted according to the given file type. - import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>; - - /// Makes the file at the given path executable. - import make-file-executable: func(filepath: string) -> result<_, string>; - - /// Updates the installation status for the given language server. - import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status); - - /// A list of environment variables. - type env-vars = list>; - - /// A command. - record command { - /// The command to execute. - command: string, - /// The arguments to pass to the command. - args: list, - /// The environment variables to set for the command. - env: env-vars, - } - - /// A Zed worktree. - resource worktree { - /// Returns the ID of the worktree. - id: func() -> u64; - /// Returns the root path of the worktree. - root-path: func() -> string; - /// Returns the textual contents of the specified file in the worktree. - read-text-file: func(path: string) -> result; - /// Returns the path to the given binary name, if one is present on the `$PATH`. - which: func(binary-name: string) -> option; - /// Returns the current shell environment. - shell-env: func() -> env-vars; - } - - /// A key-value store. - resource key-value-store { - /// Inserts an entry under the specified key. - insert: func(key: string, value: string) -> result<_, string>; - } - - /// Returns the command used to start up the language server. - export language-server-command: func(language-server-id: string, worktree: borrow) -> result; - - /// Returns the initialization options to pass to the language server on startup. - /// - /// The initialization options are represented as a JSON string. - export language-server-initialization-options: func(language-server-id: string, worktree: borrow) -> result, string>; - - /// Returns the workspace configuration options to pass to the language server. - export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow) -> result, string>; - - /// A label containing some code. - record code-label { - /// The source code to parse with Tree-sitter. - code: string, - /// The spans to display in the label. - spans: list, - /// The range of the displayed label to include when filtering. - filter-range: range, - } - - /// A span within a code label. - variant code-label-span { - /// A range into the parsed code. - code-range(range), - /// A span containing a code literal. - literal(code-label-span-literal), - } - - /// A span containing a code literal. - record code-label-span-literal { - /// The literal text. - text: string, - /// The name of the highlight to use for this literal. - highlight-name: option, - } - - export labels-for-completions: func(language-server-id: string, completions: list) -> result>, string>; - export labels-for-symbols: func(language-server-id: string, symbols: list) -> result>, string>; - - /// Returns the completions that should be shown when completing the provided slash command with the given query. - export complete-slash-command-argument: func(command: slash-command, args: list) -> result, string>; - - /// Returns the output from running the provided slash command. - export run-slash-command: func(command: slash-command, args: list, worktree: option>) -> result; - - /// Returns a list of packages as suggestions to be included in the `/docs` - /// 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. - export suggest-docs-packages: func(provider-name: string) -> result, string>; - - /// Indexes the docs for the specified package. - export index-docs: func(provider-name: string, package-name: string, database: borrow) -> result<_, string>; -} diff --git a/crates/extension_api/wit/since_v0.1.0/github.wit b/crates/extension_api/wit/since_v0.1.0/github.wit deleted file mode 100644 index bb138f5d31..0000000000 --- a/crates/extension_api/wit/since_v0.1.0/github.wit +++ /dev/null @@ -1,33 +0,0 @@ -interface github { - /// A GitHub release. - record github-release { - /// The version of the release. - version: string, - /// The list of assets attached to the release. - assets: list, - } - - /// An asset from a GitHub release. - record github-release-asset { - /// The name of the asset. - name: string, - /// The download URL for the asset. - download-url: string, - } - - /// The options used to filter down GitHub releases. - record github-release-options { - /// Whether releases without assets should be included. - require-assets: bool, - /// Whether pre-releases should be included. - pre-release: bool, - } - - /// Returns the latest release for the given GitHub repository. - latest-github-release: func(repo: string, options: github-release-options) -> result; - - /// Returns the GitHub release with the specified tag name for the given GitHub repository. - /// - /// Returns an error if a release with the given tag name does not exist. - github-release-by-tag-name: func(repo: string, tag: string) -> result; -} diff --git a/crates/extension_api/wit/since_v0.1.0/http-client.wit b/crates/extension_api/wit/since_v0.1.0/http-client.wit deleted file mode 100644 index bb0206c17a..0000000000 --- a/crates/extension_api/wit/since_v0.1.0/http-client.wit +++ /dev/null @@ -1,67 +0,0 @@ -interface http-client { - /// An HTTP request. - record http-request { - /// The HTTP method for the request. - method: http-method, - /// The URL to which the request should be made. - url: string, - /// The headers for the request. - headers: list>, - /// The request body. - body: option>, - /// The policy to use for redirects. - redirect-policy: redirect-policy, - } - - /// HTTP methods. - enum http-method { - /// `GET` - get, - /// `HEAD` - head, - /// `POST` - post, - /// `PUT` - put, - /// `DELETE` - delete, - /// `OPTIONS` - options, - /// `PATCH` - patch, - } - - /// The policy for dealing with redirects received from the server. - variant redirect-policy { - /// Redirects from the server will not be followed. - /// - /// This is the default behavior. - no-follow, - /// Redirects from the server will be followed up to the specified limit. - follow-limit(u32), - /// All redirects from the server will be followed. - follow-all, - } - - /// An HTTP response. - record http-response { - /// The response headers. - headers: list>, - /// The response body. - body: list, - } - - /// Performs an HTTP request and returns the response. - fetch: func(req: http-request) -> result; - - /// An HTTP response stream. - resource http-response-stream { - /// Retrieves the next chunk of data from the response stream. - /// - /// Returns `Ok(None)` if the stream has ended. - next-chunk: func() -> result>, string>; - } - - /// Performs an HTTP request and returns a response stream. - fetch-stream: func(req: http-request) -> result; -} diff --git a/crates/extension_api/wit/since_v0.1.0/lsp.wit b/crates/extension_api/wit/since_v0.1.0/lsp.wit deleted file mode 100644 index 19e81b6b14..0000000000 --- a/crates/extension_api/wit/since_v0.1.0/lsp.wit +++ /dev/null @@ -1,83 +0,0 @@ -interface lsp { - /// An LSP completion. - record completion { - label: string, - detail: option, - kind: option, - insert-text-format: option, - } - - /// The kind of an LSP completion. - variant completion-kind { - text, - method, - function, - %constructor, - field, - variable, - class, - %interface, - module, - property, - unit, - value, - %enum, - keyword, - snippet, - color, - file, - reference, - folder, - enum-member, - constant, - struct, - event, - operator, - type-parameter, - other(s32), - } - - /// Defines how to interpret the insert text in a completion item. - variant insert-text-format { - plain-text, - snippet, - other(s32), - } - - /// An LSP symbol. - record symbol { - kind: symbol-kind, - name: string, - } - - /// The kind of an LSP symbol. - variant symbol-kind { - file, - module, - namespace, - %package, - class, - method, - property, - field, - %constructor, - %enum, - %interface, - function, - variable, - constant, - %string, - number, - boolean, - array, - object, - key, - null, - enum-member, - struct, - event, - operator, - type-parameter, - other(s32), - } -} diff --git a/crates/extension_api/wit/since_v0.1.0/nodejs.wit b/crates/extension_api/wit/since_v0.1.0/nodejs.wit deleted file mode 100644 index c814548314..0000000000 --- a/crates/extension_api/wit/since_v0.1.0/nodejs.wit +++ /dev/null @@ -1,13 +0,0 @@ -interface nodejs { - /// Returns the path to the Node binary used by Zed. - node-binary-path: func() -> result; - - /// Returns the latest version of the given NPM package. - npm-package-latest-version: func(package-name: string) -> result; - - /// Returns the installed version of the given NPM package, if it exists. - npm-package-installed-version: func(package-name: string) -> result, string>; - - /// Installs the specified NPM package. - npm-install-package: func(package-name: string, version: string) -> result<_, string>; -} diff --git a/crates/extension_api/wit/since_v0.1.0/platform.wit b/crates/extension_api/wit/since_v0.1.0/platform.wit deleted file mode 100644 index 48472a99bc..0000000000 --- a/crates/extension_api/wit/since_v0.1.0/platform.wit +++ /dev/null @@ -1,24 +0,0 @@ -interface platform { - /// An operating system. - enum os { - /// macOS. - mac, - /// Linux. - linux, - /// Windows. - windows, - } - - /// A platform architecture. - enum architecture { - /// AArch64 (e.g., Apple Silicon). - aarch64, - /// x86. - x86, - /// x86-64. - x8664, - } - - /// Gets the current operating system and architecture. - current-platform: func() -> tuple; -} diff --git a/crates/extension_api/wit/since_v0.1.0/settings.rs b/crates/extension_api/wit/since_v0.1.0/settings.rs deleted file mode 100644 index 5c6cae7064..0000000000 --- a/crates/extension_api/wit/since_v0.1.0/settings.rs +++ /dev/null @@ -1,29 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::num::NonZeroU32; - -/// The settings for a particular language. -#[derive(Debug, Serialize, Deserialize)] -pub struct LanguageSettings { - /// How many columns a tab should occupy. - pub tab_size: NonZeroU32, -} - -/// The settings for a particular language server. -#[derive(Default, Debug, Serialize, Deserialize)] -pub struct LspSettings { - /// The settings for the language server binary. - pub binary: Option, - /// The initialization options to pass to the language server. - pub initialization_options: Option, - /// The settings to pass to language server. - pub settings: Option, -} - -/// The settings for a language server binary. -#[derive(Debug, Serialize, Deserialize)] -pub struct BinarySettings { - /// The path to the binary. - pub path: Option, - /// The arguments to pass to the binary. - pub arguments: Option>, -} diff --git a/crates/extension_api/wit/since_v0.1.0/slash-command.wit b/crates/extension_api/wit/since_v0.1.0/slash-command.wit deleted file mode 100644 index f52561c2ef..0000000000 --- a/crates/extension_api/wit/since_v0.1.0/slash-command.wit +++ /dev/null @@ -1,41 +0,0 @@ -interface slash-command { - use common.{range}; - - /// A slash command for use in the Assistant. - record slash-command { - /// The name of the slash command. - name: string, - /// The description of the slash command. - description: string, - /// The tooltip text to display for the run button. - tooltip-text: string, - /// Whether this slash command requires an argument. - requires-argument: bool, - } - - /// The output of a slash command. - record slash-command-output { - /// The text produced by the slash command. - text: string, - /// The list of sections to show in the slash command placeholder. - sections: list, - } - - /// A section in the slash command output. - record slash-command-output-section { - /// The range this section occupies. - range: range, - /// The label to display in the placeholder for this section. - label: string, - } - - /// A completion for a slash command argument. - record slash-command-argument-completion { - /// The label to display for this completion. - label: string, - /// The new text that should be inserted into the command when this completion is accepted. - new-text: string, - /// Whether the command should be run when accepting this completion. - run-command: bool, - } -} diff --git a/crates/extension_api/wit/since_v0.2.0/common.wit b/crates/extension_api/wit/since_v0.2.0/common.wit deleted file mode 100644 index c4f321f4c7..0000000000 --- a/crates/extension_api/wit/since_v0.2.0/common.wit +++ /dev/null @@ -1,9 +0,0 @@ -interface common { - /// A (half-open) range (`[start, end)`). - record range { - /// The start of the range (inclusive). - start: u32, - /// The end of the range (exclusive). - end: u32, - } -} diff --git a/crates/extension_api/wit/since_v0.2.0/extension.wit b/crates/extension_api/wit/since_v0.2.0/extension.wit deleted file mode 100644 index 3e54c5be89..0000000000 --- a/crates/extension_api/wit/since_v0.2.0/extension.wit +++ /dev/null @@ -1,156 +0,0 @@ -package zed:extension; - -world extension { - import github; - import http-client; - import platform; - import nodejs; - - use common.{range}; - use lsp.{completion, symbol}; - use slash-command.{slash-command, slash-command-argument-completion, slash-command-output}; - - /// Initializes the extension. - export init-extension: func(); - - /// The type of a downloaded file. - enum downloaded-file-type { - /// A gzipped file (`.gz`). - gzip, - /// A gzipped tar archive (`.tar.gz`). - gzip-tar, - /// A ZIP file (`.zip`). - zip, - /// An uncompressed file. - uncompressed, - } - - /// The installation status for a language server. - variant language-server-installation-status { - /// The language server has no installation status. - none, - /// The language server is being downloaded. - downloading, - /// The language server is checking for updates. - checking-for-update, - /// The language server installation failed for specified reason. - failed(string), - } - - record settings-location { - worktree-id: u64, - path: string, - } - - import get-settings: func(path: option, category: string, key: option) -> result; - - /// Downloads a file from the given URL and saves it to the given path within the extension's - /// working directory. - /// - /// The file will be extracted according to the given file type. - import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>; - - /// Makes the file at the given path executable. - import make-file-executable: func(filepath: string) -> result<_, string>; - - /// Updates the installation status for the given language server. - import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status); - - /// A list of environment variables. - type env-vars = list>; - - /// A command. - record command { - /// The command to execute. - command: string, - /// The arguments to pass to the command. - args: list, - /// The environment variables to set for the command. - env: env-vars, - } - - /// A Zed worktree. - resource worktree { - /// Returns the ID of the worktree. - id: func() -> u64; - /// Returns the root path of the worktree. - root-path: func() -> string; - /// Returns the textual contents of the specified file in the worktree. - read-text-file: func(path: string) -> result; - /// Returns the path to the given binary name, if one is present on the `$PATH`. - which: func(binary-name: string) -> option; - /// Returns the current shell environment. - shell-env: func() -> env-vars; - } - - /// A Zed project. - resource project { - /// Returns the IDs of all of the worktrees in this project. - worktree-ids: func() -> list; - } - - /// A key-value store. - resource key-value-store { - /// Inserts an entry under the specified key. - insert: func(key: string, value: string) -> result<_, string>; - } - - /// Returns the command used to start up the language server. - export language-server-command: func(language-server-id: string, worktree: borrow) -> result; - - /// Returns the initialization options to pass to the language server on startup. - /// - /// The initialization options are represented as a JSON string. - export language-server-initialization-options: func(language-server-id: string, worktree: borrow) -> result, string>; - - /// Returns the workspace configuration options to pass to the language server. - export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow) -> result, string>; - - /// A label containing some code. - record code-label { - /// The source code to parse with Tree-sitter. - code: string, - /// The spans to display in the label. - spans: list, - /// The range of the displayed label to include when filtering. - filter-range: range, - } - - /// A span within a code label. - variant code-label-span { - /// A range into the parsed code. - code-range(range), - /// A span containing a code literal. - literal(code-label-span-literal), - } - - /// A span containing a code literal. - record code-label-span-literal { - /// The literal text. - text: string, - /// The name of the highlight to use for this literal. - highlight-name: option, - } - - export labels-for-completions: func(language-server-id: string, completions: list) -> result>, string>; - export labels-for-symbols: func(language-server-id: string, symbols: list) -> result>, string>; - - /// Returns the completions that should be shown when completing the provided slash command with the given query. - export complete-slash-command-argument: func(command: slash-command, args: list) -> result, string>; - - /// Returns the output from running the provided slash command. - export run-slash-command: func(command: slash-command, args: list, worktree: option>) -> result; - - /// Returns the command used to start up a context server. - export context-server-command: func(context-server-id: string, project: borrow) -> result; - - /// Returns a list of packages as suggestions to be included in the `/docs` - /// 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. - export suggest-docs-packages: func(provider-name: string) -> result, string>; - - /// Indexes the docs for the specified package. - export index-docs: func(provider-name: string, package-name: string, database: borrow) -> result<_, string>; -} diff --git a/crates/extension_api/wit/since_v0.2.0/github.wit b/crates/extension_api/wit/since_v0.2.0/github.wit deleted file mode 100644 index 21cd5d4805..0000000000 --- a/crates/extension_api/wit/since_v0.2.0/github.wit +++ /dev/null @@ -1,35 +0,0 @@ -interface github { - /// A GitHub release. - record github-release { - /// The version of the release. - version: string, - /// The list of assets attached to the release. - assets: list, - } - - /// An asset from a GitHub release. - record github-release-asset { - /// The name of the asset. - name: string, - /// The download URL for the asset. - download-url: string, - } - - /// The options used to filter down GitHub releases. - record github-release-options { - /// Whether releases without assets should be included. - require-assets: bool, - /// Whether pre-releases should be included. - pre-release: bool, - } - - /// Returns the latest release for the given GitHub repository. - /// - /// Takes repo as a string in the form "/", for example: "zed-industries/zed". - latest-github-release: func(repo: string, options: github-release-options) -> result; - - /// Returns the GitHub release with the specified tag name for the given GitHub repository. - /// - /// Returns an error if a release with the given tag name does not exist. - github-release-by-tag-name: func(repo: string, tag: string) -> result; -} diff --git a/crates/extension_api/wit/since_v0.2.0/http-client.wit b/crates/extension_api/wit/since_v0.2.0/http-client.wit deleted file mode 100644 index bb0206c17a..0000000000 --- a/crates/extension_api/wit/since_v0.2.0/http-client.wit +++ /dev/null @@ -1,67 +0,0 @@ -interface http-client { - /// An HTTP request. - record http-request { - /// The HTTP method for the request. - method: http-method, - /// The URL to which the request should be made. - url: string, - /// The headers for the request. - headers: list>, - /// The request body. - body: option>, - /// The policy to use for redirects. - redirect-policy: redirect-policy, - } - - /// HTTP methods. - enum http-method { - /// `GET` - get, - /// `HEAD` - head, - /// `POST` - post, - /// `PUT` - put, - /// `DELETE` - delete, - /// `OPTIONS` - options, - /// `PATCH` - patch, - } - - /// The policy for dealing with redirects received from the server. - variant redirect-policy { - /// Redirects from the server will not be followed. - /// - /// This is the default behavior. - no-follow, - /// Redirects from the server will be followed up to the specified limit. - follow-limit(u32), - /// All redirects from the server will be followed. - follow-all, - } - - /// An HTTP response. - record http-response { - /// The response headers. - headers: list>, - /// The response body. - body: list, - } - - /// Performs an HTTP request and returns the response. - fetch: func(req: http-request) -> result; - - /// An HTTP response stream. - resource http-response-stream { - /// Retrieves the next chunk of data from the response stream. - /// - /// Returns `Ok(None)` if the stream has ended. - next-chunk: func() -> result>, string>; - } - - /// Performs an HTTP request and returns a response stream. - fetch-stream: func(req: http-request) -> result; -} diff --git a/crates/extension_api/wit/since_v0.2.0/lsp.wit b/crates/extension_api/wit/since_v0.2.0/lsp.wit deleted file mode 100644 index 91a36c93a6..0000000000 --- a/crates/extension_api/wit/since_v0.2.0/lsp.wit +++ /dev/null @@ -1,90 +0,0 @@ -interface lsp { - /// An LSP completion. - record completion { - label: string, - label-details: option, - detail: option, - kind: option, - insert-text-format: option, - } - - /// The kind of an LSP completion. - variant completion-kind { - text, - method, - function, - %constructor, - field, - variable, - class, - %interface, - module, - property, - unit, - value, - %enum, - keyword, - snippet, - color, - file, - reference, - folder, - enum-member, - constant, - struct, - event, - operator, - type-parameter, - other(s32), - } - - /// Label details for an LSP completion. - record completion-label-details { - detail: option, - description: option, - } - - /// Defines how to interpret the insert text in a completion item. - variant insert-text-format { - plain-text, - snippet, - other(s32), - } - - /// An LSP symbol. - record symbol { - kind: symbol-kind, - name: string, - } - - /// The kind of an LSP symbol. - variant symbol-kind { - file, - module, - namespace, - %package, - class, - method, - property, - field, - %constructor, - %enum, - %interface, - function, - variable, - constant, - %string, - number, - boolean, - array, - object, - key, - null, - enum-member, - struct, - event, - operator, - type-parameter, - other(s32), - } -} diff --git a/crates/extension_api/wit/since_v0.2.0/nodejs.wit b/crates/extension_api/wit/since_v0.2.0/nodejs.wit deleted file mode 100644 index c814548314..0000000000 --- a/crates/extension_api/wit/since_v0.2.0/nodejs.wit +++ /dev/null @@ -1,13 +0,0 @@ -interface nodejs { - /// Returns the path to the Node binary used by Zed. - node-binary-path: func() -> result; - - /// Returns the latest version of the given NPM package. - npm-package-latest-version: func(package-name: string) -> result; - - /// Returns the installed version of the given NPM package, if it exists. - npm-package-installed-version: func(package-name: string) -> result, string>; - - /// Installs the specified NPM package. - npm-install-package: func(package-name: string, version: string) -> result<_, string>; -} diff --git a/crates/extension_api/wit/since_v0.2.0/platform.wit b/crates/extension_api/wit/since_v0.2.0/platform.wit deleted file mode 100644 index 48472a99bc..0000000000 --- a/crates/extension_api/wit/since_v0.2.0/platform.wit +++ /dev/null @@ -1,24 +0,0 @@ -interface platform { - /// An operating system. - enum os { - /// macOS. - mac, - /// Linux. - linux, - /// Windows. - windows, - } - - /// A platform architecture. - enum architecture { - /// AArch64 (e.g., Apple Silicon). - aarch64, - /// x86. - x86, - /// x86-64. - x8664, - } - - /// Gets the current operating system and architecture. - current-platform: func() -> tuple; -} diff --git a/crates/extension_api/wit/since_v0.2.0/settings.rs b/crates/extension_api/wit/since_v0.2.0/settings.rs deleted file mode 100644 index 19e28c1ba9..0000000000 --- a/crates/extension_api/wit/since_v0.2.0/settings.rs +++ /dev/null @@ -1,40 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, num::NonZeroU32}; - -/// The settings for a particular language. -#[derive(Debug, Serialize, Deserialize)] -pub struct LanguageSettings { - /// How many columns a tab should occupy. - pub tab_size: NonZeroU32, -} - -/// The settings for a particular language server. -#[derive(Default, Debug, Serialize, Deserialize)] -pub struct LspSettings { - /// The settings for the language server binary. - pub binary: Option, - /// The initialization options to pass to the language server. - pub initialization_options: Option, - /// The settings to pass to language server. - pub settings: Option, -} - -/// The settings for a particular context server. -#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct ContextServerSettings { - /// The settings for the context server binary. - pub command: Option, - /// The settings to pass to the context server. - pub settings: Option, -} - -/// The settings for a command. -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct CommandSettings { - /// The path to the command. - pub path: Option, - /// The arguments to pass to the command. - pub arguments: Option>, - /// The environment variables. - pub env: Option>, -} diff --git a/crates/extension_api/wit/since_v0.2.0/slash-command.wit b/crates/extension_api/wit/since_v0.2.0/slash-command.wit deleted file mode 100644 index f52561c2ef..0000000000 --- a/crates/extension_api/wit/since_v0.2.0/slash-command.wit +++ /dev/null @@ -1,41 +0,0 @@ -interface slash-command { - use common.{range}; - - /// A slash command for use in the Assistant. - record slash-command { - /// The name of the slash command. - name: string, - /// The description of the slash command. - description: string, - /// The tooltip text to display for the run button. - tooltip-text: string, - /// Whether this slash command requires an argument. - requires-argument: bool, - } - - /// The output of a slash command. - record slash-command-output { - /// The text produced by the slash command. - text: string, - /// The list of sections to show in the slash command placeholder. - sections: list, - } - - /// A section in the slash command output. - record slash-command-output-section { - /// The range this section occupies. - range: range, - /// The label to display in the placeholder for this section. - label: string, - } - - /// A completion for a slash command argument. - record slash-command-argument-completion { - /// The label to display for this completion. - label: string, - /// The new text that should be inserted into the command when this completion is accepted. - new-text: string, - /// Whether the command should be run when accepting this completion. - run-command: bool, - } -} diff --git a/crates/extension_api/wit/since_v0.3.0/common.wit b/crates/extension_api/wit/since_v0.3.0/common.wit deleted file mode 100644 index 139e7ba0ca..0000000000 --- a/crates/extension_api/wit/since_v0.3.0/common.wit +++ /dev/null @@ -1,12 +0,0 @@ -interface common { - /// A (half-open) range (`[start, end)`). - record range { - /// The start of the range (inclusive). - start: u32, - /// The end of the range (exclusive). - end: u32, - } - - /// A list of environment variables. - type env-vars = list>; -} diff --git a/crates/extension_api/wit/since_v0.3.0/extension.wit b/crates/extension_api/wit/since_v0.3.0/extension.wit deleted file mode 100644 index 95aaec5469..0000000000 --- a/crates/extension_api/wit/since_v0.3.0/extension.wit +++ /dev/null @@ -1,145 +0,0 @@ -package zed:extension; - -world extension { - import github; - import http-client; - import platform; - import process; - import nodejs; - - use common.{env-vars, range}; - use lsp.{completion, symbol}; - use process.{command}; - use slash-command.{slash-command, slash-command-argument-completion, slash-command-output}; - - /// Initializes the extension. - export init-extension: func(); - - /// The type of a downloaded file. - enum downloaded-file-type { - /// A gzipped file (`.gz`). - gzip, - /// A gzipped tar archive (`.tar.gz`). - gzip-tar, - /// A ZIP file (`.zip`). - zip, - /// An uncompressed file. - uncompressed, - } - - /// The installation status for a language server. - variant language-server-installation-status { - /// The language server has no installation status. - none, - /// The language server is being downloaded. - downloading, - /// The language server is checking for updates. - checking-for-update, - /// The language server installation failed for specified reason. - failed(string), - } - - record settings-location { - worktree-id: u64, - path: string, - } - - import get-settings: func(path: option, category: string, key: option) -> result; - - /// Downloads a file from the given URL and saves it to the given path within the extension's - /// working directory. - /// - /// The file will be extracted according to the given file type. - import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>; - - /// Makes the file at the given path executable. - import make-file-executable: func(filepath: string) -> result<_, string>; - - /// Updates the installation status for the given language server. - import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status); - - /// A Zed worktree. - resource worktree { - /// Returns the ID of the worktree. - id: func() -> u64; - /// Returns the root path of the worktree. - root-path: func() -> string; - /// Returns the textual contents of the specified file in the worktree. - read-text-file: func(path: string) -> result; - /// Returns the path to the given binary name, if one is present on the `$PATH`. - which: func(binary-name: string) -> option; - /// Returns the current shell environment. - shell-env: func() -> env-vars; - } - - /// A Zed project. - resource project { - /// Returns the IDs of all of the worktrees in this project. - worktree-ids: func() -> list; - } - - /// A key-value store. - resource key-value-store { - /// Inserts an entry under the specified key. - insert: func(key: string, value: string) -> result<_, string>; - } - - /// Returns the command used to start up the language server. - export language-server-command: func(language-server-id: string, worktree: borrow) -> result; - - /// Returns the initialization options to pass to the language server on startup. - /// - /// The initialization options are represented as a JSON string. - export language-server-initialization-options: func(language-server-id: string, worktree: borrow) -> result, string>; - - /// Returns the workspace configuration options to pass to the language server. - export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow) -> result, string>; - - /// A label containing some code. - record code-label { - /// The source code to parse with Tree-sitter. - code: string, - /// The spans to display in the label. - spans: list, - /// The range of the displayed label to include when filtering. - filter-range: range, - } - - /// A span within a code label. - variant code-label-span { - /// A range into the parsed code. - code-range(range), - /// A span containing a code literal. - literal(code-label-span-literal), - } - - /// A span containing a code literal. - record code-label-span-literal { - /// The literal text. - text: string, - /// The name of the highlight to use for this literal. - highlight-name: option, - } - - export labels-for-completions: func(language-server-id: string, completions: list) -> result>, string>; - export labels-for-symbols: func(language-server-id: string, symbols: list) -> result>, string>; - - /// Returns the completions that should be shown when completing the provided slash command with the given query. - export complete-slash-command-argument: func(command: slash-command, args: list) -> result, string>; - - /// Returns the output from running the provided slash command. - export run-slash-command: func(command: slash-command, args: list, worktree: option>) -> result; - - /// Returns the command used to start up a context server. - export context-server-command: func(context-server-id: string, project: borrow) -> result; - - /// Returns a list of packages as suggestions to be included in the `/docs` - /// 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. - export suggest-docs-packages: func(provider-name: string) -> result, string>; - - /// Indexes the docs for the specified package. - export index-docs: func(provider-name: string, package-name: string, database: borrow) -> result<_, string>; -} diff --git a/crates/extension_api/wit/since_v0.3.0/github.wit b/crates/extension_api/wit/since_v0.3.0/github.wit deleted file mode 100644 index 21cd5d4805..0000000000 --- a/crates/extension_api/wit/since_v0.3.0/github.wit +++ /dev/null @@ -1,35 +0,0 @@ -interface github { - /// A GitHub release. - record github-release { - /// The version of the release. - version: string, - /// The list of assets attached to the release. - assets: list, - } - - /// An asset from a GitHub release. - record github-release-asset { - /// The name of the asset. - name: string, - /// The download URL for the asset. - download-url: string, - } - - /// The options used to filter down GitHub releases. - record github-release-options { - /// Whether releases without assets should be included. - require-assets: bool, - /// Whether pre-releases should be included. - pre-release: bool, - } - - /// Returns the latest release for the given GitHub repository. - /// - /// Takes repo as a string in the form "/", for example: "zed-industries/zed". - latest-github-release: func(repo: string, options: github-release-options) -> result; - - /// Returns the GitHub release with the specified tag name for the given GitHub repository. - /// - /// Returns an error if a release with the given tag name does not exist. - github-release-by-tag-name: func(repo: string, tag: string) -> result; -} diff --git a/crates/extension_api/wit/since_v0.3.0/http-client.wit b/crates/extension_api/wit/since_v0.3.0/http-client.wit deleted file mode 100644 index bb0206c17a..0000000000 --- a/crates/extension_api/wit/since_v0.3.0/http-client.wit +++ /dev/null @@ -1,67 +0,0 @@ -interface http-client { - /// An HTTP request. - record http-request { - /// The HTTP method for the request. - method: http-method, - /// The URL to which the request should be made. - url: string, - /// The headers for the request. - headers: list>, - /// The request body. - body: option>, - /// The policy to use for redirects. - redirect-policy: redirect-policy, - } - - /// HTTP methods. - enum http-method { - /// `GET` - get, - /// `HEAD` - head, - /// `POST` - post, - /// `PUT` - put, - /// `DELETE` - delete, - /// `OPTIONS` - options, - /// `PATCH` - patch, - } - - /// The policy for dealing with redirects received from the server. - variant redirect-policy { - /// Redirects from the server will not be followed. - /// - /// This is the default behavior. - no-follow, - /// Redirects from the server will be followed up to the specified limit. - follow-limit(u32), - /// All redirects from the server will be followed. - follow-all, - } - - /// An HTTP response. - record http-response { - /// The response headers. - headers: list>, - /// The response body. - body: list, - } - - /// Performs an HTTP request and returns the response. - fetch: func(req: http-request) -> result; - - /// An HTTP response stream. - resource http-response-stream { - /// Retrieves the next chunk of data from the response stream. - /// - /// Returns `Ok(None)` if the stream has ended. - next-chunk: func() -> result>, string>; - } - - /// Performs an HTTP request and returns a response stream. - fetch-stream: func(req: http-request) -> result; -} diff --git a/crates/extension_api/wit/since_v0.3.0/lsp.wit b/crates/extension_api/wit/since_v0.3.0/lsp.wit deleted file mode 100644 index 91a36c93a6..0000000000 --- a/crates/extension_api/wit/since_v0.3.0/lsp.wit +++ /dev/null @@ -1,90 +0,0 @@ -interface lsp { - /// An LSP completion. - record completion { - label: string, - label-details: option, - detail: option, - kind: option, - insert-text-format: option, - } - - /// The kind of an LSP completion. - variant completion-kind { - text, - method, - function, - %constructor, - field, - variable, - class, - %interface, - module, - property, - unit, - value, - %enum, - keyword, - snippet, - color, - file, - reference, - folder, - enum-member, - constant, - struct, - event, - operator, - type-parameter, - other(s32), - } - - /// Label details for an LSP completion. - record completion-label-details { - detail: option, - description: option, - } - - /// Defines how to interpret the insert text in a completion item. - variant insert-text-format { - plain-text, - snippet, - other(s32), - } - - /// An LSP symbol. - record symbol { - kind: symbol-kind, - name: string, - } - - /// The kind of an LSP symbol. - variant symbol-kind { - file, - module, - namespace, - %package, - class, - method, - property, - field, - %constructor, - %enum, - %interface, - function, - variable, - constant, - %string, - number, - boolean, - array, - object, - key, - null, - enum-member, - struct, - event, - operator, - type-parameter, - other(s32), - } -} diff --git a/crates/extension_api/wit/since_v0.3.0/nodejs.wit b/crates/extension_api/wit/since_v0.3.0/nodejs.wit deleted file mode 100644 index c814548314..0000000000 --- a/crates/extension_api/wit/since_v0.3.0/nodejs.wit +++ /dev/null @@ -1,13 +0,0 @@ -interface nodejs { - /// Returns the path to the Node binary used by Zed. - node-binary-path: func() -> result; - - /// Returns the latest version of the given NPM package. - npm-package-latest-version: func(package-name: string) -> result; - - /// Returns the installed version of the given NPM package, if it exists. - npm-package-installed-version: func(package-name: string) -> result, string>; - - /// Installs the specified NPM package. - npm-install-package: func(package-name: string, version: string) -> result<_, string>; -} diff --git a/crates/extension_api/wit/since_v0.3.0/platform.wit b/crates/extension_api/wit/since_v0.3.0/platform.wit deleted file mode 100644 index 48472a99bc..0000000000 --- a/crates/extension_api/wit/since_v0.3.0/platform.wit +++ /dev/null @@ -1,24 +0,0 @@ -interface platform { - /// An operating system. - enum os { - /// macOS. - mac, - /// Linux. - linux, - /// Windows. - windows, - } - - /// A platform architecture. - enum architecture { - /// AArch64 (e.g., Apple Silicon). - aarch64, - /// x86. - x86, - /// x86-64. - x8664, - } - - /// Gets the current operating system and architecture. - current-platform: func() -> tuple; -} diff --git a/crates/extension_api/wit/since_v0.3.0/process.wit b/crates/extension_api/wit/since_v0.3.0/process.wit deleted file mode 100644 index d9a5728a3d..0000000000 --- a/crates/extension_api/wit/since_v0.3.0/process.wit +++ /dev/null @@ -1,29 +0,0 @@ -interface process { - use common.{env-vars}; - - /// A command. - record command { - /// The command to execute. - command: string, - /// The arguments to pass to the command. - args: list, - /// The environment variables to set for the command. - env: env-vars, - } - - /// The output of a finished process. - record output { - /// The status (exit code) of the process. - /// - /// On Unix, this will be `None` if the process was terminated by a signal. - status: option, - /// The data that the process wrote to stdout. - stdout: list, - /// The data that the process wrote to stderr. - stderr: list, - } - - /// Executes the given command as a child process, waiting for it to finish - /// and collecting all of its output. - run-command: func(command: command) -> result; -} diff --git a/crates/extension_api/wit/since_v0.3.0/settings.rs b/crates/extension_api/wit/since_v0.3.0/settings.rs deleted file mode 100644 index 19e28c1ba9..0000000000 --- a/crates/extension_api/wit/since_v0.3.0/settings.rs +++ /dev/null @@ -1,40 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, num::NonZeroU32}; - -/// The settings for a particular language. -#[derive(Debug, Serialize, Deserialize)] -pub struct LanguageSettings { - /// How many columns a tab should occupy. - pub tab_size: NonZeroU32, -} - -/// The settings for a particular language server. -#[derive(Default, Debug, Serialize, Deserialize)] -pub struct LspSettings { - /// The settings for the language server binary. - pub binary: Option, - /// The initialization options to pass to the language server. - pub initialization_options: Option, - /// The settings to pass to language server. - pub settings: Option, -} - -/// The settings for a particular context server. -#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct ContextServerSettings { - /// The settings for the context server binary. - pub command: Option, - /// The settings to pass to the context server. - pub settings: Option, -} - -/// The settings for a command. -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct CommandSettings { - /// The path to the command. - pub path: Option, - /// The arguments to pass to the command. - pub arguments: Option>, - /// The environment variables. - pub env: Option>, -} diff --git a/crates/extension_api/wit/since_v0.3.0/slash-command.wit b/crates/extension_api/wit/since_v0.3.0/slash-command.wit deleted file mode 100644 index f52561c2ef..0000000000 --- a/crates/extension_api/wit/since_v0.3.0/slash-command.wit +++ /dev/null @@ -1,41 +0,0 @@ -interface slash-command { - use common.{range}; - - /// A slash command for use in the Assistant. - record slash-command { - /// The name of the slash command. - name: string, - /// The description of the slash command. - description: string, - /// The tooltip text to display for the run button. - tooltip-text: string, - /// Whether this slash command requires an argument. - requires-argument: bool, - } - - /// The output of a slash command. - record slash-command-output { - /// The text produced by the slash command. - text: string, - /// The list of sections to show in the slash command placeholder. - sections: list, - } - - /// A section in the slash command output. - record slash-command-output-section { - /// The range this section occupies. - range: range, - /// The label to display in the placeholder for this section. - label: string, - } - - /// A completion for a slash command argument. - record slash-command-argument-completion { - /// The label to display for this completion. - label: string, - /// The new text that should be inserted into the command when this completion is accepted. - new-text: string, - /// Whether the command should be run when accepting this completion. - run-command: bool, - } -} diff --git a/crates/extension_api/wit/since_v0.4.0/common.wit b/crates/extension_api/wit/since_v0.4.0/common.wit deleted file mode 100644 index 139e7ba0ca..0000000000 --- a/crates/extension_api/wit/since_v0.4.0/common.wit +++ /dev/null @@ -1,12 +0,0 @@ -interface common { - /// A (half-open) range (`[start, end)`). - record range { - /// The start of the range (inclusive). - start: u32, - /// The end of the range (exclusive). - end: u32, - } - - /// A list of environment variables. - type env-vars = list>; -} diff --git a/crates/extension_api/wit/since_v0.4.0/extension.wit b/crates/extension_api/wit/since_v0.4.0/extension.wit deleted file mode 100644 index 3caf8b60b7..0000000000 --- a/crates/extension_api/wit/since_v0.4.0/extension.wit +++ /dev/null @@ -1,151 +0,0 @@ -package zed:extension; - -world extension { - import github; - import http-client; - import platform; - import process; - import nodejs; - - use common.{env-vars, range}; - use lsp.{completion, symbol}; - use process.{command}; - use slash-command.{slash-command, slash-command-argument-completion, slash-command-output}; - - /// Initializes the extension. - export init-extension: func(); - - /// The type of a downloaded file. - enum downloaded-file-type { - /// A gzipped file (`.gz`). - gzip, - /// A gzipped tar archive (`.tar.gz`). - gzip-tar, - /// A ZIP file (`.zip`). - zip, - /// An uncompressed file. - uncompressed, - } - - /// The installation status for a language server. - variant language-server-installation-status { - /// The language server has no installation status. - none, - /// The language server is being downloaded. - downloading, - /// The language server is checking for updates. - checking-for-update, - /// The language server installation failed for specified reason. - failed(string), - } - - record settings-location { - worktree-id: u64, - path: string, - } - - import get-settings: func(path: option, category: string, key: option) -> result; - - /// Downloads a file from the given URL and saves it to the given path within the extension's - /// working directory. - /// - /// The file will be extracted according to the given file type. - import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>; - - /// Makes the file at the given path executable. - import make-file-executable: func(filepath: string) -> result<_, string>; - - /// Updates the installation status for the given language server. - import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status); - - /// A Zed worktree. - resource worktree { - /// Returns the ID of the worktree. - id: func() -> u64; - /// Returns the root path of the worktree. - root-path: func() -> string; - /// Returns the textual contents of the specified file in the worktree. - read-text-file: func(path: string) -> result; - /// Returns the path to the given binary name, if one is present on the `$PATH`. - which: func(binary-name: string) -> option; - /// Returns the current shell environment. - shell-env: func() -> env-vars; - } - - /// A Zed project. - resource project { - /// Returns the IDs of all of the worktrees in this project. - worktree-ids: func() -> list; - } - - /// A key-value store. - resource key-value-store { - /// Inserts an entry under the specified key. - insert: func(key: string, value: string) -> result<_, string>; - } - - /// Returns the command used to start up the language server. - export language-server-command: func(language-server-id: string, worktree: borrow) -> result; - - /// Returns the initialization options to pass to the language server on startup. - /// - /// The initialization options are represented as a JSON string. - export language-server-initialization-options: func(language-server-id: string, worktree: borrow) -> result, string>; - - /// Returns the workspace configuration options to pass to the language server. - export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow) -> result, string>; - - /// Returns the initialization options to pass to the other language server. - export language-server-additional-initialization-options: func(language-server-id: string, target-language-server-id: string, worktree: borrow) -> result, string>; - - /// Returns the workspace configuration options to pass to the other language server. - export language-server-additional-workspace-configuration: func(language-server-id: string, target-language-server-id: string, worktree: borrow) -> result, string>; - - /// A label containing some code. - record code-label { - /// The source code to parse with Tree-sitter. - code: string, - /// The spans to display in the label. - spans: list, - /// The range of the displayed label to include when filtering. - filter-range: range, - } - - /// A span within a code label. - variant code-label-span { - /// A range into the parsed code. - code-range(range), - /// A span containing a code literal. - literal(code-label-span-literal), - } - - /// A span containing a code literal. - record code-label-span-literal { - /// The literal text. - text: string, - /// The name of the highlight to use for this literal. - highlight-name: option, - } - - export labels-for-completions: func(language-server-id: string, completions: list) -> result>, string>; - export labels-for-symbols: func(language-server-id: string, symbols: list) -> result>, string>; - - /// Returns the completions that should be shown when completing the provided slash command with the given query. - export complete-slash-command-argument: func(command: slash-command, args: list) -> result, string>; - - /// Returns the output from running the provided slash command. - export run-slash-command: func(command: slash-command, args: list, worktree: option>) -> result; - - /// Returns the command used to start up a context server. - export context-server-command: func(context-server-id: string, project: borrow) -> result; - - /// Returns a list of packages as suggestions to be included in the `/docs` - /// 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. - export suggest-docs-packages: func(provider-name: string) -> result, string>; - - /// Indexes the docs for the specified package. - export index-docs: func(provider-name: string, package-name: string, database: borrow) -> result<_, string>; -} diff --git a/crates/extension_api/wit/since_v0.4.0/github.wit b/crates/extension_api/wit/since_v0.4.0/github.wit deleted file mode 100644 index 21cd5d4805..0000000000 --- a/crates/extension_api/wit/since_v0.4.0/github.wit +++ /dev/null @@ -1,35 +0,0 @@ -interface github { - /// A GitHub release. - record github-release { - /// The version of the release. - version: string, - /// The list of assets attached to the release. - assets: list, - } - - /// An asset from a GitHub release. - record github-release-asset { - /// The name of the asset. - name: string, - /// The download URL for the asset. - download-url: string, - } - - /// The options used to filter down GitHub releases. - record github-release-options { - /// Whether releases without assets should be included. - require-assets: bool, - /// Whether pre-releases should be included. - pre-release: bool, - } - - /// Returns the latest release for the given GitHub repository. - /// - /// Takes repo as a string in the form "/", for example: "zed-industries/zed". - latest-github-release: func(repo: string, options: github-release-options) -> result; - - /// Returns the GitHub release with the specified tag name for the given GitHub repository. - /// - /// Returns an error if a release with the given tag name does not exist. - github-release-by-tag-name: func(repo: string, tag: string) -> result; -} diff --git a/crates/extension_api/wit/since_v0.4.0/http-client.wit b/crates/extension_api/wit/since_v0.4.0/http-client.wit deleted file mode 100644 index bb0206c17a..0000000000 --- a/crates/extension_api/wit/since_v0.4.0/http-client.wit +++ /dev/null @@ -1,67 +0,0 @@ -interface http-client { - /// An HTTP request. - record http-request { - /// The HTTP method for the request. - method: http-method, - /// The URL to which the request should be made. - url: string, - /// The headers for the request. - headers: list>, - /// The request body. - body: option>, - /// The policy to use for redirects. - redirect-policy: redirect-policy, - } - - /// HTTP methods. - enum http-method { - /// `GET` - get, - /// `HEAD` - head, - /// `POST` - post, - /// `PUT` - put, - /// `DELETE` - delete, - /// `OPTIONS` - options, - /// `PATCH` - patch, - } - - /// The policy for dealing with redirects received from the server. - variant redirect-policy { - /// Redirects from the server will not be followed. - /// - /// This is the default behavior. - no-follow, - /// Redirects from the server will be followed up to the specified limit. - follow-limit(u32), - /// All redirects from the server will be followed. - follow-all, - } - - /// An HTTP response. - record http-response { - /// The response headers. - headers: list>, - /// The response body. - body: list, - } - - /// Performs an HTTP request and returns the response. - fetch: func(req: http-request) -> result; - - /// An HTTP response stream. - resource http-response-stream { - /// Retrieves the next chunk of data from the response stream. - /// - /// Returns `Ok(None)` if the stream has ended. - next-chunk: func() -> result>, string>; - } - - /// Performs an HTTP request and returns a response stream. - fetch-stream: func(req: http-request) -> result; -} diff --git a/crates/extension_api/wit/since_v0.4.0/lsp.wit b/crates/extension_api/wit/since_v0.4.0/lsp.wit deleted file mode 100644 index 91a36c93a6..0000000000 --- a/crates/extension_api/wit/since_v0.4.0/lsp.wit +++ /dev/null @@ -1,90 +0,0 @@ -interface lsp { - /// An LSP completion. - record completion { - label: string, - label-details: option, - detail: option, - kind: option, - insert-text-format: option, - } - - /// The kind of an LSP completion. - variant completion-kind { - text, - method, - function, - %constructor, - field, - variable, - class, - %interface, - module, - property, - unit, - value, - %enum, - keyword, - snippet, - color, - file, - reference, - folder, - enum-member, - constant, - struct, - event, - operator, - type-parameter, - other(s32), - } - - /// Label details for an LSP completion. - record completion-label-details { - detail: option, - description: option, - } - - /// Defines how to interpret the insert text in a completion item. - variant insert-text-format { - plain-text, - snippet, - other(s32), - } - - /// An LSP symbol. - record symbol { - kind: symbol-kind, - name: string, - } - - /// The kind of an LSP symbol. - variant symbol-kind { - file, - module, - namespace, - %package, - class, - method, - property, - field, - %constructor, - %enum, - %interface, - function, - variable, - constant, - %string, - number, - boolean, - array, - object, - key, - null, - enum-member, - struct, - event, - operator, - type-parameter, - other(s32), - } -} diff --git a/crates/extension_api/wit/since_v0.4.0/nodejs.wit b/crates/extension_api/wit/since_v0.4.0/nodejs.wit deleted file mode 100644 index c814548314..0000000000 --- a/crates/extension_api/wit/since_v0.4.0/nodejs.wit +++ /dev/null @@ -1,13 +0,0 @@ -interface nodejs { - /// Returns the path to the Node binary used by Zed. - node-binary-path: func() -> result; - - /// Returns the latest version of the given NPM package. - npm-package-latest-version: func(package-name: string) -> result; - - /// Returns the installed version of the given NPM package, if it exists. - npm-package-installed-version: func(package-name: string) -> result, string>; - - /// Installs the specified NPM package. - npm-install-package: func(package-name: string, version: string) -> result<_, string>; -} diff --git a/crates/extension_api/wit/since_v0.4.0/platform.wit b/crates/extension_api/wit/since_v0.4.0/platform.wit deleted file mode 100644 index 48472a99bc..0000000000 --- a/crates/extension_api/wit/since_v0.4.0/platform.wit +++ /dev/null @@ -1,24 +0,0 @@ -interface platform { - /// An operating system. - enum os { - /// macOS. - mac, - /// Linux. - linux, - /// Windows. - windows, - } - - /// A platform architecture. - enum architecture { - /// AArch64 (e.g., Apple Silicon). - aarch64, - /// x86. - x86, - /// x86-64. - x8664, - } - - /// Gets the current operating system and architecture. - current-platform: func() -> tuple; -} diff --git a/crates/extension_api/wit/since_v0.4.0/process.wit b/crates/extension_api/wit/since_v0.4.0/process.wit deleted file mode 100644 index d9a5728a3d..0000000000 --- a/crates/extension_api/wit/since_v0.4.0/process.wit +++ /dev/null @@ -1,29 +0,0 @@ -interface process { - use common.{env-vars}; - - /// A command. - record command { - /// The command to execute. - command: string, - /// The arguments to pass to the command. - args: list, - /// The environment variables to set for the command. - env: env-vars, - } - - /// The output of a finished process. - record output { - /// The status (exit code) of the process. - /// - /// On Unix, this will be `None` if the process was terminated by a signal. - status: option, - /// The data that the process wrote to stdout. - stdout: list, - /// The data that the process wrote to stderr. - stderr: list, - } - - /// Executes the given command as a child process, waiting for it to finish - /// and collecting all of its output. - run-command: func(command: command) -> result; -} diff --git a/crates/extension_api/wit/since_v0.4.0/settings.rs b/crates/extension_api/wit/since_v0.4.0/settings.rs deleted file mode 100644 index 19e28c1ba9..0000000000 --- a/crates/extension_api/wit/since_v0.4.0/settings.rs +++ /dev/null @@ -1,40 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, num::NonZeroU32}; - -/// The settings for a particular language. -#[derive(Debug, Serialize, Deserialize)] -pub struct LanguageSettings { - /// How many columns a tab should occupy. - pub tab_size: NonZeroU32, -} - -/// The settings for a particular language server. -#[derive(Default, Debug, Serialize, Deserialize)] -pub struct LspSettings { - /// The settings for the language server binary. - pub binary: Option, - /// The initialization options to pass to the language server. - pub initialization_options: Option, - /// The settings to pass to language server. - pub settings: Option, -} - -/// The settings for a particular context server. -#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct ContextServerSettings { - /// The settings for the context server binary. - pub command: Option, - /// The settings to pass to the context server. - pub settings: Option, -} - -/// The settings for a command. -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct CommandSettings { - /// The path to the command. - pub path: Option, - /// The arguments to pass to the command. - pub arguments: Option>, - /// The environment variables. - pub env: Option>, -} diff --git a/crates/extension_api/wit/since_v0.4.0/slash-command.wit b/crates/extension_api/wit/since_v0.4.0/slash-command.wit deleted file mode 100644 index f52561c2ef..0000000000 --- a/crates/extension_api/wit/since_v0.4.0/slash-command.wit +++ /dev/null @@ -1,41 +0,0 @@ -interface slash-command { - use common.{range}; - - /// A slash command for use in the Assistant. - record slash-command { - /// The name of the slash command. - name: string, - /// The description of the slash command. - description: string, - /// The tooltip text to display for the run button. - tooltip-text: string, - /// Whether this slash command requires an argument. - requires-argument: bool, - } - - /// The output of a slash command. - record slash-command-output { - /// The text produced by the slash command. - text: string, - /// The list of sections to show in the slash command placeholder. - sections: list, - } - - /// A section in the slash command output. - record slash-command-output-section { - /// The range this section occupies. - range: range, - /// The label to display in the placeholder for this section. - label: string, - } - - /// A completion for a slash command argument. - record slash-command-argument-completion { - /// The label to display for this completion. - label: string, - /// The new text that should be inserted into the command when this completion is accepted. - new-text: string, - /// Whether the command should be run when accepting this completion. - run-command: bool, - } -} diff --git a/crates/extension_api/wit/since_v0.5.0/common.wit b/crates/extension_api/wit/since_v0.5.0/common.wit deleted file mode 100644 index 139e7ba0ca..0000000000 --- a/crates/extension_api/wit/since_v0.5.0/common.wit +++ /dev/null @@ -1,12 +0,0 @@ -interface common { - /// A (half-open) range (`[start, end)`). - record range { - /// The start of the range (inclusive). - start: u32, - /// The end of the range (exclusive). - end: u32, - } - - /// A list of environment variables. - type env-vars = list>; -} diff --git a/crates/extension_api/wit/since_v0.5.0/context-server.wit b/crates/extension_api/wit/since_v0.5.0/context-server.wit deleted file mode 100644 index 7234e0e6d0..0000000000 --- a/crates/extension_api/wit/since_v0.5.0/context-server.wit +++ /dev/null @@ -1,11 +0,0 @@ -interface context-server { - /// Configuration for context server setup and installation. - record context-server-configuration { - /// Installation instructions in Markdown format. - installation-instructions: string, - /// JSON schema for settings validation. - settings-schema: string, - /// Default settings template. - default-settings: string, - } -} diff --git a/crates/extension_api/wit/since_v0.5.0/extension.wit b/crates/extension_api/wit/since_v0.5.0/extension.wit deleted file mode 100644 index f21cc1bf21..0000000000 --- a/crates/extension_api/wit/since_v0.5.0/extension.wit +++ /dev/null @@ -1,156 +0,0 @@ -package zed:extension; - -world extension { - import context-server; - import github; - import http-client; - import platform; - import process; - import nodejs; - - use common.{env-vars, range}; - use context-server.{context-server-configuration}; - use lsp.{completion, symbol}; - use process.{command}; - use slash-command.{slash-command, slash-command-argument-completion, slash-command-output}; - - /// Initializes the extension. - export init-extension: func(); - - /// The type of a downloaded file. - enum downloaded-file-type { - /// A gzipped file (`.gz`). - gzip, - /// A gzipped tar archive (`.tar.gz`). - gzip-tar, - /// A ZIP file (`.zip`). - zip, - /// An uncompressed file. - uncompressed, - } - - /// The installation status for a language server. - variant language-server-installation-status { - /// The language server has no installation status. - none, - /// The language server is being downloaded. - downloading, - /// The language server is checking for updates. - checking-for-update, - /// The language server installation failed for specified reason. - failed(string), - } - - record settings-location { - worktree-id: u64, - path: string, - } - - import get-settings: func(path: option, category: string, key: option) -> result; - - /// Downloads a file from the given URL and saves it to the given path within the extension's - /// working directory. - /// - /// The file will be extracted according to the given file type. - import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>; - - /// Makes the file at the given path executable. - import make-file-executable: func(filepath: string) -> result<_, string>; - - /// Updates the installation status for the given language server. - import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status); - - /// A Zed worktree. - resource worktree { - /// Returns the ID of the worktree. - id: func() -> u64; - /// Returns the root path of the worktree. - root-path: func() -> string; - /// Returns the textual contents of the specified file in the worktree. - read-text-file: func(path: string) -> result; - /// Returns the path to the given binary name, if one is present on the `$PATH`. - which: func(binary-name: string) -> option; - /// Returns the current shell environment. - shell-env: func() -> env-vars; - } - - /// A Zed project. - resource project { - /// Returns the IDs of all of the worktrees in this project. - worktree-ids: func() -> list; - } - - /// A key-value store. - resource key-value-store { - /// Inserts an entry under the specified key. - insert: func(key: string, value: string) -> result<_, string>; - } - - /// Returns the command used to start up the language server. - export language-server-command: func(language-server-id: string, worktree: borrow) -> result; - - /// Returns the initialization options to pass to the language server on startup. - /// - /// The initialization options are represented as a JSON string. - export language-server-initialization-options: func(language-server-id: string, worktree: borrow) -> result, string>; - - /// Returns the workspace configuration options to pass to the language server. - export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow) -> result, string>; - - /// Returns the initialization options to pass to the other language server. - export language-server-additional-initialization-options: func(language-server-id: string, target-language-server-id: string, worktree: borrow) -> result, string>; - - /// Returns the workspace configuration options to pass to the other language server. - export language-server-additional-workspace-configuration: func(language-server-id: string, target-language-server-id: string, worktree: borrow) -> result, string>; - - /// A label containing some code. - record code-label { - /// The source code to parse with Tree-sitter. - code: string, - /// The spans to display in the label. - spans: list, - /// The range of the displayed label to include when filtering. - filter-range: range, - } - - /// A span within a code label. - variant code-label-span { - /// A range into the parsed code. - code-range(range), - /// A span containing a code literal. - literal(code-label-span-literal), - } - - /// A span containing a code literal. - record code-label-span-literal { - /// The literal text. - text: string, - /// The name of the highlight to use for this literal. - highlight-name: option, - } - - export labels-for-completions: func(language-server-id: string, completions: list) -> result>, string>; - export labels-for-symbols: func(language-server-id: string, symbols: list) -> result>, string>; - - /// Returns the completions that should be shown when completing the provided slash command with the given query. - export complete-slash-command-argument: func(command: slash-command, args: list) -> result, string>; - - /// Returns the output from running the provided slash command. - export run-slash-command: func(command: slash-command, args: list, worktree: option>) -> result; - - /// Returns the command used to start up a context server. - export context-server-command: func(context-server-id: string, project: borrow) -> result; - - /// Returns the configuration for a context server. - export context-server-configuration: func(context-server-id: string, project: borrow) -> result, string>; - - /// Returns a list of packages as suggestions to be included in the `/docs` - /// 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. - export suggest-docs-packages: func(provider-name: string) -> result, string>; - - /// Indexes the docs for the specified package. - export index-docs: func(provider-name: string, package-name: string, database: borrow) -> result<_, string>; -} diff --git a/crates/extension_api/wit/since_v0.5.0/github.wit b/crates/extension_api/wit/since_v0.5.0/github.wit deleted file mode 100644 index 21cd5d4805..0000000000 --- a/crates/extension_api/wit/since_v0.5.0/github.wit +++ /dev/null @@ -1,35 +0,0 @@ -interface github { - /// A GitHub release. - record github-release { - /// The version of the release. - version: string, - /// The list of assets attached to the release. - assets: list, - } - - /// An asset from a GitHub release. - record github-release-asset { - /// The name of the asset. - name: string, - /// The download URL for the asset. - download-url: string, - } - - /// The options used to filter down GitHub releases. - record github-release-options { - /// Whether releases without assets should be included. - require-assets: bool, - /// Whether pre-releases should be included. - pre-release: bool, - } - - /// Returns the latest release for the given GitHub repository. - /// - /// Takes repo as a string in the form "/", for example: "zed-industries/zed". - latest-github-release: func(repo: string, options: github-release-options) -> result; - - /// Returns the GitHub release with the specified tag name for the given GitHub repository. - /// - /// Returns an error if a release with the given tag name does not exist. - github-release-by-tag-name: func(repo: string, tag: string) -> result; -} diff --git a/crates/extension_api/wit/since_v0.5.0/http-client.wit b/crates/extension_api/wit/since_v0.5.0/http-client.wit deleted file mode 100644 index bb0206c17a..0000000000 --- a/crates/extension_api/wit/since_v0.5.0/http-client.wit +++ /dev/null @@ -1,67 +0,0 @@ -interface http-client { - /// An HTTP request. - record http-request { - /// The HTTP method for the request. - method: http-method, - /// The URL to which the request should be made. - url: string, - /// The headers for the request. - headers: list>, - /// The request body. - body: option>, - /// The policy to use for redirects. - redirect-policy: redirect-policy, - } - - /// HTTP methods. - enum http-method { - /// `GET` - get, - /// `HEAD` - head, - /// `POST` - post, - /// `PUT` - put, - /// `DELETE` - delete, - /// `OPTIONS` - options, - /// `PATCH` - patch, - } - - /// The policy for dealing with redirects received from the server. - variant redirect-policy { - /// Redirects from the server will not be followed. - /// - /// This is the default behavior. - no-follow, - /// Redirects from the server will be followed up to the specified limit. - follow-limit(u32), - /// All redirects from the server will be followed. - follow-all, - } - - /// An HTTP response. - record http-response { - /// The response headers. - headers: list>, - /// The response body. - body: list, - } - - /// Performs an HTTP request and returns the response. - fetch: func(req: http-request) -> result; - - /// An HTTP response stream. - resource http-response-stream { - /// Retrieves the next chunk of data from the response stream. - /// - /// Returns `Ok(None)` if the stream has ended. - next-chunk: func() -> result>, string>; - } - - /// Performs an HTTP request and returns a response stream. - fetch-stream: func(req: http-request) -> result; -} diff --git a/crates/extension_api/wit/since_v0.5.0/lsp.wit b/crates/extension_api/wit/since_v0.5.0/lsp.wit deleted file mode 100644 index 91a36c93a6..0000000000 --- a/crates/extension_api/wit/since_v0.5.0/lsp.wit +++ /dev/null @@ -1,90 +0,0 @@ -interface lsp { - /// An LSP completion. - record completion { - label: string, - label-details: option, - detail: option, - kind: option, - insert-text-format: option, - } - - /// The kind of an LSP completion. - variant completion-kind { - text, - method, - function, - %constructor, - field, - variable, - class, - %interface, - module, - property, - unit, - value, - %enum, - keyword, - snippet, - color, - file, - reference, - folder, - enum-member, - constant, - struct, - event, - operator, - type-parameter, - other(s32), - } - - /// Label details for an LSP completion. - record completion-label-details { - detail: option, - description: option, - } - - /// Defines how to interpret the insert text in a completion item. - variant insert-text-format { - plain-text, - snippet, - other(s32), - } - - /// An LSP symbol. - record symbol { - kind: symbol-kind, - name: string, - } - - /// The kind of an LSP symbol. - variant symbol-kind { - file, - module, - namespace, - %package, - class, - method, - property, - field, - %constructor, - %enum, - %interface, - function, - variable, - constant, - %string, - number, - boolean, - array, - object, - key, - null, - enum-member, - struct, - event, - operator, - type-parameter, - other(s32), - } -} diff --git a/crates/extension_api/wit/since_v0.5.0/nodejs.wit b/crates/extension_api/wit/since_v0.5.0/nodejs.wit deleted file mode 100644 index c814548314..0000000000 --- a/crates/extension_api/wit/since_v0.5.0/nodejs.wit +++ /dev/null @@ -1,13 +0,0 @@ -interface nodejs { - /// Returns the path to the Node binary used by Zed. - node-binary-path: func() -> result; - - /// Returns the latest version of the given NPM package. - npm-package-latest-version: func(package-name: string) -> result; - - /// Returns the installed version of the given NPM package, if it exists. - npm-package-installed-version: func(package-name: string) -> result, string>; - - /// Installs the specified NPM package. - npm-install-package: func(package-name: string, version: string) -> result<_, string>; -} diff --git a/crates/extension_api/wit/since_v0.5.0/platform.wit b/crates/extension_api/wit/since_v0.5.0/platform.wit deleted file mode 100644 index 48472a99bc..0000000000 --- a/crates/extension_api/wit/since_v0.5.0/platform.wit +++ /dev/null @@ -1,24 +0,0 @@ -interface platform { - /// An operating system. - enum os { - /// macOS. - mac, - /// Linux. - linux, - /// Windows. - windows, - } - - /// A platform architecture. - enum architecture { - /// AArch64 (e.g., Apple Silicon). - aarch64, - /// x86. - x86, - /// x86-64. - x8664, - } - - /// Gets the current operating system and architecture. - current-platform: func() -> tuple; -} diff --git a/crates/extension_api/wit/since_v0.5.0/process.wit b/crates/extension_api/wit/since_v0.5.0/process.wit deleted file mode 100644 index d9a5728a3d..0000000000 --- a/crates/extension_api/wit/since_v0.5.0/process.wit +++ /dev/null @@ -1,29 +0,0 @@ -interface process { - use common.{env-vars}; - - /// A command. - record command { - /// The command to execute. - command: string, - /// The arguments to pass to the command. - args: list, - /// The environment variables to set for the command. - env: env-vars, - } - - /// The output of a finished process. - record output { - /// The status (exit code) of the process. - /// - /// On Unix, this will be `None` if the process was terminated by a signal. - status: option, - /// The data that the process wrote to stdout. - stdout: list, - /// The data that the process wrote to stderr. - stderr: list, - } - - /// Executes the given command as a child process, waiting for it to finish - /// and collecting all of its output. - run-command: func(command: command) -> result; -} diff --git a/crates/extension_api/wit/since_v0.5.0/settings.rs b/crates/extension_api/wit/since_v0.5.0/settings.rs deleted file mode 100644 index 19e28c1ba9..0000000000 --- a/crates/extension_api/wit/since_v0.5.0/settings.rs +++ /dev/null @@ -1,40 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, num::NonZeroU32}; - -/// The settings for a particular language. -#[derive(Debug, Serialize, Deserialize)] -pub struct LanguageSettings { - /// How many columns a tab should occupy. - pub tab_size: NonZeroU32, -} - -/// The settings for a particular language server. -#[derive(Default, Debug, Serialize, Deserialize)] -pub struct LspSettings { - /// The settings for the language server binary. - pub binary: Option, - /// The initialization options to pass to the language server. - pub initialization_options: Option, - /// The settings to pass to language server. - pub settings: Option, -} - -/// The settings for a particular context server. -#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct ContextServerSettings { - /// The settings for the context server binary. - pub command: Option, - /// The settings to pass to the context server. - pub settings: Option, -} - -/// The settings for a command. -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct CommandSettings { - /// The path to the command. - pub path: Option, - /// The arguments to pass to the command. - pub arguments: Option>, - /// The environment variables. - pub env: Option>, -} diff --git a/crates/extension_api/wit/since_v0.5.0/slash-command.wit b/crates/extension_api/wit/since_v0.5.0/slash-command.wit deleted file mode 100644 index f52561c2ef..0000000000 --- a/crates/extension_api/wit/since_v0.5.0/slash-command.wit +++ /dev/null @@ -1,41 +0,0 @@ -interface slash-command { - use common.{range}; - - /// A slash command for use in the Assistant. - record slash-command { - /// The name of the slash command. - name: string, - /// The description of the slash command. - description: string, - /// The tooltip text to display for the run button. - tooltip-text: string, - /// Whether this slash command requires an argument. - requires-argument: bool, - } - - /// The output of a slash command. - record slash-command-output { - /// The text produced by the slash command. - text: string, - /// The list of sections to show in the slash command placeholder. - sections: list, - } - - /// A section in the slash command output. - record slash-command-output-section { - /// The range this section occupies. - range: range, - /// The label to display in the placeholder for this section. - label: string, - } - - /// A completion for a slash command argument. - record slash-command-argument-completion { - /// The label to display for this completion. - label: string, - /// The new text that should be inserted into the command when this completion is accepted. - new-text: string, - /// Whether the command should be run when accepting this completion. - run-command: bool, - } -} diff --git a/crates/extension_api/wit/since_v0.6.0/common.wit b/crates/extension_api/wit/since_v0.6.0/common.wit deleted file mode 100644 index 139e7ba0ca..0000000000 --- a/crates/extension_api/wit/since_v0.6.0/common.wit +++ /dev/null @@ -1,12 +0,0 @@ -interface common { - /// A (half-open) range (`[start, end)`). - record range { - /// The start of the range (inclusive). - start: u32, - /// The end of the range (exclusive). - end: u32, - } - - /// A list of environment variables. - type env-vars = list>; -} diff --git a/crates/extension_api/wit/since_v0.6.0/context-server.wit b/crates/extension_api/wit/since_v0.6.0/context-server.wit deleted file mode 100644 index 7234e0e6d0..0000000000 --- a/crates/extension_api/wit/since_v0.6.0/context-server.wit +++ /dev/null @@ -1,11 +0,0 @@ -interface context-server { - /// Configuration for context server setup and installation. - record context-server-configuration { - /// Installation instructions in Markdown format. - installation-instructions: string, - /// JSON schema for settings validation. - settings-schema: string, - /// Default settings template. - default-settings: string, - } -} diff --git a/crates/extension_api/wit/since_v0.6.0/dap.wit b/crates/extension_api/wit/since_v0.6.0/dap.wit deleted file mode 100644 index 693befe02f..0000000000 --- a/crates/extension_api/wit/since_v0.6.0/dap.wit +++ /dev/null @@ -1,123 +0,0 @@ -interface dap { - use common.{env-vars}; - - /// Resolves a specified TcpArgumentsTemplate into TcpArguments - resolve-tcp-template: func(template: tcp-arguments-template) -> result; - - record launch-request { - program: string, - cwd: option, - args: list, - envs: env-vars, - } - - record attach-request { - process-id: option, - } - - variant debug-request { - launch(launch-request), - attach(attach-request) - } - - record tcp-arguments { - port: u16, - host: u32, - timeout: option, - } - - record tcp-arguments-template { - port: option, - host: option, - timeout: option, - } - - /// Debug Config is the "highest-level" configuration for a debug session. - /// It comes from a new process modal UI; thus, it is essentially debug-adapter-agnostic. - /// It is expected of the extension to translate this generic configuration into something that can be debugged by the adapter (debug scenario). - record debug-config { - /// Name of the debug task - label: string, - /// The debug adapter to use - adapter: string, - request: debug-request, - stop-on-entry: option, - } - - record task-template { - /// Human readable name of the task to display in the UI. - label: string, - /// Executable command to spawn. - command: string, - args: list, - env: env-vars, - cwd: option, - } - - /// A task template with substituted task variables. - type resolved-task = task-template; - - /// A task template for building a debug target. - type build-task-template = task-template; - - variant build-task-definition { - by-name(string), - template(build-task-definition-template-payload ) - } - record build-task-definition-template-payload { - locator-name: option, - template: build-task-template - } - - /// Debug Scenario is the user-facing configuration type (used in debug.json). It is still concerned with what to debug and not necessarily how to do it (except for any - /// debug-adapter-specific configuration options). - record debug-scenario { - /// Unsubstituted label for the task.DebugAdapterBinary - label: string, - /// Name of the Debug Adapter this configuration is intended for. - adapter: string, - /// An optional build step to be ran prior to starting a debug session. Build steps are used by Zed's locators to locate the executable to debug. - build: option, - /// JSON-encoded configuration for a given debug adapter. - config: string, - /// TCP connection parameters (if they were specified by user) - tcp-connection: option, - } - - enum start-debugging-request-arguments-request { - launch, - attach, - } - - record debug-task-definition { - /// Unsubstituted label for the task.DebugAdapterBinary - label: string, - /// Name of the Debug Adapter this configuration is intended for. - adapter: string, - /// JSON-encoded configuration for a given debug adapter. - config: string, - /// TCP connection parameters (if they were specified by user) - tcp-connection: option, - } - - record start-debugging-request-arguments { - /// JSON-encoded configuration for a given debug adapter. It is specific to each debug adapter. - /// `configuration` will have it's Zed variable references substituted prior to being passed to the debug adapter. - configuration: string, - request: start-debugging-request-arguments-request, - } - - /// The lowest-level representation of a debug session, which specifies: - /// - How to start a debug adapter process - /// - How to start a debug session with it (using DAP protocol) - /// for a given debug scenario. - record debug-adapter-binary { - command: option, - arguments: list, - envs: env-vars, - cwd: option, - /// Zed will use TCP transport if `connection` is specified. - connection: option, - request-args: start-debugging-request-arguments - } -} diff --git a/crates/extension_api/wit/since_v0.6.0/extension.wit b/crates/extension_api/wit/since_v0.6.0/extension.wit deleted file mode 100644 index 8195162b89..0000000000 --- a/crates/extension_api/wit/since_v0.6.0/extension.wit +++ /dev/null @@ -1,167 +0,0 @@ -package zed:extension; - -world extension { - import context-server; - import dap; - import github; - import http-client; - import platform; - import process; - import nodejs; - - use common.{env-vars, range}; - use context-server.{context-server-configuration}; - use dap.{attach-request, build-task-template, debug-config, debug-adapter-binary, debug-task-definition, debug-request, debug-scenario, launch-request, resolved-task, start-debugging-request-arguments-request}; - use lsp.{completion, symbol}; - use process.{command}; - use slash-command.{slash-command, slash-command-argument-completion, slash-command-output}; - - /// Initializes the extension. - export init-extension: func(); - - /// The type of a downloaded file. - enum downloaded-file-type { - /// A gzipped file (`.gz`). - gzip, - /// A gzipped tar archive (`.tar.gz`). - gzip-tar, - /// A ZIP file (`.zip`). - zip, - /// An uncompressed file. - uncompressed, - } - - /// The installation status for a language server. - variant language-server-installation-status { - /// The language server has no installation status. - none, - /// The language server is being downloaded. - downloading, - /// The language server is checking for updates. - checking-for-update, - /// The language server installation failed for specified reason. - failed(string), - } - - record settings-location { - worktree-id: u64, - path: string, - } - - import get-settings: func(path: option, category: string, key: option) -> result; - - /// Downloads a file from the given URL and saves it to the given path within the extension's - /// working directory. - /// - /// The file will be extracted according to the given file type. - import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>; - - /// Makes the file at the given path executable. - import make-file-executable: func(filepath: string) -> result<_, string>; - - /// Updates the installation status for the given language server. - import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status); - - /// A Zed worktree. - resource worktree { - /// Returns the ID of the worktree. - id: func() -> u64; - /// Returns the root path of the worktree. - root-path: func() -> string; - /// Returns the textual contents of the specified file in the worktree. - read-text-file: func(path: string) -> result; - /// Returns the path to the given binary name, if one is present on the `$PATH`. - which: func(binary-name: string) -> option; - /// Returns the current shell environment. - shell-env: func() -> env-vars; - } - - /// A Zed project. - resource project { - /// Returns the IDs of all of the worktrees in this project. - worktree-ids: func() -> list; - } - - /// A key-value store. - resource key-value-store { - /// Inserts an entry under the specified key. - insert: func(key: string, value: string) -> result<_, string>; - } - - /// Returns the command used to start up the language server. - export language-server-command: func(language-server-id: string, worktree: borrow) -> result; - - /// Returns the initialization options to pass to the language server on startup. - /// - /// The initialization options are represented as a JSON string. - export language-server-initialization-options: func(language-server-id: string, worktree: borrow) -> result, string>; - - /// Returns the workspace configuration options to pass to the language server. - export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow) -> result, string>; - - /// Returns the initialization options to pass to the other language server. - export language-server-additional-initialization-options: func(language-server-id: string, target-language-server-id: string, worktree: borrow) -> result, string>; - - /// Returns the workspace configuration options to pass to the other language server. - export language-server-additional-workspace-configuration: func(language-server-id: string, target-language-server-id: string, worktree: borrow) -> result, string>; - - /// A label containing some code. - record code-label { - /// The source code to parse with Tree-sitter. - code: string, - /// The spans to display in the label. - spans: list, - /// The range of the displayed label to include when filtering. - filter-range: range, - } - - /// A span within a code label. - variant code-label-span { - /// A range into the parsed code. - code-range(range), - /// A span containing a code literal. - literal(code-label-span-literal), - } - - /// A span containing a code literal. - record code-label-span-literal { - /// The literal text. - text: string, - /// The name of the highlight to use for this literal. - highlight-name: option, - } - - export labels-for-completions: func(language-server-id: string, completions: list) -> result>, string>; - export labels-for-symbols: func(language-server-id: string, symbols: list) -> result>, string>; - - - /// Returns the completions that should be shown when completing the provided slash command with the given query. - export complete-slash-command-argument: func(command: slash-command, args: list) -> result, string>; - - /// Returns the output from running the provided slash command. - export run-slash-command: func(command: slash-command, args: list, worktree: option>) -> result; - - /// Returns the command used to start up a context server. - export context-server-command: func(context-server-id: string, project: borrow) -> result; - - /// Returns the configuration for a context server. - export context-server-configuration: func(context-server-id: string, project: borrow) -> result, string>; - - /// Returns a list of packages as suggestions to be included in the `/docs` - /// 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. - export suggest-docs-packages: func(provider-name: string) -> result, string>; - - /// Indexes the docs for the specified package. - export index-docs: func(provider-name: string, package-name: string, database: borrow) -> result<_, string>; - - /// Returns a configured debug adapter binary for a given debug task. - export get-dap-binary: func(adapter-name: string, config: debug-task-definition, user-installed-path: option, worktree: borrow) -> result; - /// Returns the kind of a debug scenario (launch or attach). - export dap-request-kind: func(adapter-name: string, config: string) -> result; - export dap-config-to-scenario: func(config: debug-config) -> result; - export dap-locator-create-scenario: func(locator-name: string, build-config-template: build-task-template, resolved-label: string, debug-adapter-name: string) -> option; - export run-dap-locator: func(locator-name: string, config: resolved-task) -> result; -} diff --git a/crates/extension_api/wit/since_v0.6.0/github.wit b/crates/extension_api/wit/since_v0.6.0/github.wit deleted file mode 100644 index 21cd5d4805..0000000000 --- a/crates/extension_api/wit/since_v0.6.0/github.wit +++ /dev/null @@ -1,35 +0,0 @@ -interface github { - /// A GitHub release. - record github-release { - /// The version of the release. - version: string, - /// The list of assets attached to the release. - assets: list, - } - - /// An asset from a GitHub release. - record github-release-asset { - /// The name of the asset. - name: string, - /// The download URL for the asset. - download-url: string, - } - - /// The options used to filter down GitHub releases. - record github-release-options { - /// Whether releases without assets should be included. - require-assets: bool, - /// Whether pre-releases should be included. - pre-release: bool, - } - - /// Returns the latest release for the given GitHub repository. - /// - /// Takes repo as a string in the form "/", for example: "zed-industries/zed". - latest-github-release: func(repo: string, options: github-release-options) -> result; - - /// Returns the GitHub release with the specified tag name for the given GitHub repository. - /// - /// Returns an error if a release with the given tag name does not exist. - github-release-by-tag-name: func(repo: string, tag: string) -> result; -} diff --git a/crates/extension_api/wit/since_v0.6.0/http-client.wit b/crates/extension_api/wit/since_v0.6.0/http-client.wit deleted file mode 100644 index bb0206c17a..0000000000 --- a/crates/extension_api/wit/since_v0.6.0/http-client.wit +++ /dev/null @@ -1,67 +0,0 @@ -interface http-client { - /// An HTTP request. - record http-request { - /// The HTTP method for the request. - method: http-method, - /// The URL to which the request should be made. - url: string, - /// The headers for the request. - headers: list>, - /// The request body. - body: option>, - /// The policy to use for redirects. - redirect-policy: redirect-policy, - } - - /// HTTP methods. - enum http-method { - /// `GET` - get, - /// `HEAD` - head, - /// `POST` - post, - /// `PUT` - put, - /// `DELETE` - delete, - /// `OPTIONS` - options, - /// `PATCH` - patch, - } - - /// The policy for dealing with redirects received from the server. - variant redirect-policy { - /// Redirects from the server will not be followed. - /// - /// This is the default behavior. - no-follow, - /// Redirects from the server will be followed up to the specified limit. - follow-limit(u32), - /// All redirects from the server will be followed. - follow-all, - } - - /// An HTTP response. - record http-response { - /// The response headers. - headers: list>, - /// The response body. - body: list, - } - - /// Performs an HTTP request and returns the response. - fetch: func(req: http-request) -> result; - - /// An HTTP response stream. - resource http-response-stream { - /// Retrieves the next chunk of data from the response stream. - /// - /// Returns `Ok(None)` if the stream has ended. - next-chunk: func() -> result>, string>; - } - - /// Performs an HTTP request and returns a response stream. - fetch-stream: func(req: http-request) -> result; -} diff --git a/crates/extension_api/wit/since_v0.6.0/lsp.wit b/crates/extension_api/wit/since_v0.6.0/lsp.wit deleted file mode 100644 index 91a36c93a6..0000000000 --- a/crates/extension_api/wit/since_v0.6.0/lsp.wit +++ /dev/null @@ -1,90 +0,0 @@ -interface lsp { - /// An LSP completion. - record completion { - label: string, - label-details: option, - detail: option, - kind: option, - insert-text-format: option, - } - - /// The kind of an LSP completion. - variant completion-kind { - text, - method, - function, - %constructor, - field, - variable, - class, - %interface, - module, - property, - unit, - value, - %enum, - keyword, - snippet, - color, - file, - reference, - folder, - enum-member, - constant, - struct, - event, - operator, - type-parameter, - other(s32), - } - - /// Label details for an LSP completion. - record completion-label-details { - detail: option, - description: option, - } - - /// Defines how to interpret the insert text in a completion item. - variant insert-text-format { - plain-text, - snippet, - other(s32), - } - - /// An LSP symbol. - record symbol { - kind: symbol-kind, - name: string, - } - - /// The kind of an LSP symbol. - variant symbol-kind { - file, - module, - namespace, - %package, - class, - method, - property, - field, - %constructor, - %enum, - %interface, - function, - variable, - constant, - %string, - number, - boolean, - array, - object, - key, - null, - enum-member, - struct, - event, - operator, - type-parameter, - other(s32), - } -} diff --git a/crates/extension_api/wit/since_v0.6.0/nodejs.wit b/crates/extension_api/wit/since_v0.6.0/nodejs.wit deleted file mode 100644 index c814548314..0000000000 --- a/crates/extension_api/wit/since_v0.6.0/nodejs.wit +++ /dev/null @@ -1,13 +0,0 @@ -interface nodejs { - /// Returns the path to the Node binary used by Zed. - node-binary-path: func() -> result; - - /// Returns the latest version of the given NPM package. - npm-package-latest-version: func(package-name: string) -> result; - - /// Returns the installed version of the given NPM package, if it exists. - npm-package-installed-version: func(package-name: string) -> result, string>; - - /// Installs the specified NPM package. - npm-install-package: func(package-name: string, version: string) -> result<_, string>; -} diff --git a/crates/extension_api/wit/since_v0.6.0/platform.wit b/crates/extension_api/wit/since_v0.6.0/platform.wit deleted file mode 100644 index 48472a99bc..0000000000 --- a/crates/extension_api/wit/since_v0.6.0/platform.wit +++ /dev/null @@ -1,24 +0,0 @@ -interface platform { - /// An operating system. - enum os { - /// macOS. - mac, - /// Linux. - linux, - /// Windows. - windows, - } - - /// A platform architecture. - enum architecture { - /// AArch64 (e.g., Apple Silicon). - aarch64, - /// x86. - x86, - /// x86-64. - x8664, - } - - /// Gets the current operating system and architecture. - current-platform: func() -> tuple; -} diff --git a/crates/extension_api/wit/since_v0.6.0/process.wit b/crates/extension_api/wit/since_v0.6.0/process.wit deleted file mode 100644 index d9a5728a3d..0000000000 --- a/crates/extension_api/wit/since_v0.6.0/process.wit +++ /dev/null @@ -1,29 +0,0 @@ -interface process { - use common.{env-vars}; - - /// A command. - record command { - /// The command to execute. - command: string, - /// The arguments to pass to the command. - args: list, - /// The environment variables to set for the command. - env: env-vars, - } - - /// The output of a finished process. - record output { - /// The status (exit code) of the process. - /// - /// On Unix, this will be `None` if the process was terminated by a signal. - status: option, - /// The data that the process wrote to stdout. - stdout: list, - /// The data that the process wrote to stderr. - stderr: list, - } - - /// Executes the given command as a child process, waiting for it to finish - /// and collecting all of its output. - run-command: func(command: command) -> result; -} diff --git a/crates/extension_api/wit/since_v0.6.0/settings.rs b/crates/extension_api/wit/since_v0.6.0/settings.rs deleted file mode 100644 index 19e28c1ba9..0000000000 --- a/crates/extension_api/wit/since_v0.6.0/settings.rs +++ /dev/null @@ -1,40 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, num::NonZeroU32}; - -/// The settings for a particular language. -#[derive(Debug, Serialize, Deserialize)] -pub struct LanguageSettings { - /// How many columns a tab should occupy. - pub tab_size: NonZeroU32, -} - -/// The settings for a particular language server. -#[derive(Default, Debug, Serialize, Deserialize)] -pub struct LspSettings { - /// The settings for the language server binary. - pub binary: Option, - /// The initialization options to pass to the language server. - pub initialization_options: Option, - /// The settings to pass to language server. - pub settings: Option, -} - -/// The settings for a particular context server. -#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct ContextServerSettings { - /// The settings for the context server binary. - pub command: Option, - /// The settings to pass to the context server. - pub settings: Option, -} - -/// The settings for a command. -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct CommandSettings { - /// The path to the command. - pub path: Option, - /// The arguments to pass to the command. - pub arguments: Option>, - /// The environment variables. - pub env: Option>, -} diff --git a/crates/extension_api/wit/since_v0.6.0/slash-command.wit b/crates/extension_api/wit/since_v0.6.0/slash-command.wit deleted file mode 100644 index f52561c2ef..0000000000 --- a/crates/extension_api/wit/since_v0.6.0/slash-command.wit +++ /dev/null @@ -1,41 +0,0 @@ -interface slash-command { - use common.{range}; - - /// A slash command for use in the Assistant. - record slash-command { - /// The name of the slash command. - name: string, - /// The description of the slash command. - description: string, - /// The tooltip text to display for the run button. - tooltip-text: string, - /// Whether this slash command requires an argument. - requires-argument: bool, - } - - /// The output of a slash command. - record slash-command-output { - /// The text produced by the slash command. - text: string, - /// The list of sections to show in the slash command placeholder. - sections: list, - } - - /// A section in the slash command output. - record slash-command-output-section { - /// The range this section occupies. - range: range, - /// The label to display in the placeholder for this section. - label: string, - } - - /// A completion for a slash command argument. - record slash-command-argument-completion { - /// The label to display for this completion. - label: string, - /// The new text that should be inserted into the command when this completion is accepted. - new-text: string, - /// Whether the command should be run when accepting this completion. - run-command: bool, - } -} diff --git a/crates/extension_api/wit/since_v0.8.0/common.wit b/crates/extension_api/wit/since_v0.8.0/common.wit deleted file mode 100644 index 139e7ba0ca..0000000000 --- a/crates/extension_api/wit/since_v0.8.0/common.wit +++ /dev/null @@ -1,12 +0,0 @@ -interface common { - /// A (half-open) range (`[start, end)`). - record range { - /// The start of the range (inclusive). - start: u32, - /// The end of the range (exclusive). - end: u32, - } - - /// A list of environment variables. - type env-vars = list>; -} diff --git a/crates/extension_api/wit/since_v0.8.0/context-server.wit b/crates/extension_api/wit/since_v0.8.0/context-server.wit deleted file mode 100644 index 7234e0e6d0..0000000000 --- a/crates/extension_api/wit/since_v0.8.0/context-server.wit +++ /dev/null @@ -1,11 +0,0 @@ -interface context-server { - /// Configuration for context server setup and installation. - record context-server-configuration { - /// Installation instructions in Markdown format. - installation-instructions: string, - /// JSON schema for settings validation. - settings-schema: string, - /// Default settings template. - default-settings: string, - } -} diff --git a/crates/extension_api/wit/since_v0.8.0/dap.wit b/crates/extension_api/wit/since_v0.8.0/dap.wit deleted file mode 100644 index 693befe02f..0000000000 --- a/crates/extension_api/wit/since_v0.8.0/dap.wit +++ /dev/null @@ -1,123 +0,0 @@ -interface dap { - use common.{env-vars}; - - /// Resolves a specified TcpArgumentsTemplate into TcpArguments - resolve-tcp-template: func(template: tcp-arguments-template) -> result; - - record launch-request { - program: string, - cwd: option, - args: list, - envs: env-vars, - } - - record attach-request { - process-id: option, - } - - variant debug-request { - launch(launch-request), - attach(attach-request) - } - - record tcp-arguments { - port: u16, - host: u32, - timeout: option, - } - - record tcp-arguments-template { - port: option, - host: option, - timeout: option, - } - - /// Debug Config is the "highest-level" configuration for a debug session. - /// It comes from a new process modal UI; thus, it is essentially debug-adapter-agnostic. - /// It is expected of the extension to translate this generic configuration into something that can be debugged by the adapter (debug scenario). - record debug-config { - /// Name of the debug task - label: string, - /// The debug adapter to use - adapter: string, - request: debug-request, - stop-on-entry: option, - } - - record task-template { - /// Human readable name of the task to display in the UI. - label: string, - /// Executable command to spawn. - command: string, - args: list, - env: env-vars, - cwd: option, - } - - /// A task template with substituted task variables. - type resolved-task = task-template; - - /// A task template for building a debug target. - type build-task-template = task-template; - - variant build-task-definition { - by-name(string), - template(build-task-definition-template-payload ) - } - record build-task-definition-template-payload { - locator-name: option, - template: build-task-template - } - - /// Debug Scenario is the user-facing configuration type (used in debug.json). It is still concerned with what to debug and not necessarily how to do it (except for any - /// debug-adapter-specific configuration options). - record debug-scenario { - /// Unsubstituted label for the task.DebugAdapterBinary - label: string, - /// Name of the Debug Adapter this configuration is intended for. - adapter: string, - /// An optional build step to be ran prior to starting a debug session. Build steps are used by Zed's locators to locate the executable to debug. - build: option, - /// JSON-encoded configuration for a given debug adapter. - config: string, - /// TCP connection parameters (if they were specified by user) - tcp-connection: option, - } - - enum start-debugging-request-arguments-request { - launch, - attach, - } - - record debug-task-definition { - /// Unsubstituted label for the task.DebugAdapterBinary - label: string, - /// Name of the Debug Adapter this configuration is intended for. - adapter: string, - /// JSON-encoded configuration for a given debug adapter. - config: string, - /// TCP connection parameters (if they were specified by user) - tcp-connection: option, - } - - record start-debugging-request-arguments { - /// JSON-encoded configuration for a given debug adapter. It is specific to each debug adapter. - /// `configuration` will have it's Zed variable references substituted prior to being passed to the debug adapter. - configuration: string, - request: start-debugging-request-arguments-request, - } - - /// The lowest-level representation of a debug session, which specifies: - /// - How to start a debug adapter process - /// - How to start a debug session with it (using DAP protocol) - /// for a given debug scenario. - record debug-adapter-binary { - command: option, - arguments: list, - envs: env-vars, - cwd: option, - /// Zed will use TCP transport if `connection` is specified. - connection: option, - request-args: start-debugging-request-arguments - } -} diff --git a/crates/extension_api/wit/since_v0.8.0/extension.wit b/crates/extension_api/wit/since_v0.8.0/extension.wit deleted file mode 100644 index 8195162b89..0000000000 --- a/crates/extension_api/wit/since_v0.8.0/extension.wit +++ /dev/null @@ -1,167 +0,0 @@ -package zed:extension; - -world extension { - import context-server; - import dap; - import github; - import http-client; - import platform; - import process; - import nodejs; - - use common.{env-vars, range}; - use context-server.{context-server-configuration}; - use dap.{attach-request, build-task-template, debug-config, debug-adapter-binary, debug-task-definition, debug-request, debug-scenario, launch-request, resolved-task, start-debugging-request-arguments-request}; - use lsp.{completion, symbol}; - use process.{command}; - use slash-command.{slash-command, slash-command-argument-completion, slash-command-output}; - - /// Initializes the extension. - export init-extension: func(); - - /// The type of a downloaded file. - enum downloaded-file-type { - /// A gzipped file (`.gz`). - gzip, - /// A gzipped tar archive (`.tar.gz`). - gzip-tar, - /// A ZIP file (`.zip`). - zip, - /// An uncompressed file. - uncompressed, - } - - /// The installation status for a language server. - variant language-server-installation-status { - /// The language server has no installation status. - none, - /// The language server is being downloaded. - downloading, - /// The language server is checking for updates. - checking-for-update, - /// The language server installation failed for specified reason. - failed(string), - } - - record settings-location { - worktree-id: u64, - path: string, - } - - import get-settings: func(path: option, category: string, key: option) -> result; - - /// Downloads a file from the given URL and saves it to the given path within the extension's - /// working directory. - /// - /// The file will be extracted according to the given file type. - import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>; - - /// Makes the file at the given path executable. - import make-file-executable: func(filepath: string) -> result<_, string>; - - /// Updates the installation status for the given language server. - import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status); - - /// A Zed worktree. - resource worktree { - /// Returns the ID of the worktree. - id: func() -> u64; - /// Returns the root path of the worktree. - root-path: func() -> string; - /// Returns the textual contents of the specified file in the worktree. - read-text-file: func(path: string) -> result; - /// Returns the path to the given binary name, if one is present on the `$PATH`. - which: func(binary-name: string) -> option; - /// Returns the current shell environment. - shell-env: func() -> env-vars; - } - - /// A Zed project. - resource project { - /// Returns the IDs of all of the worktrees in this project. - worktree-ids: func() -> list; - } - - /// A key-value store. - resource key-value-store { - /// Inserts an entry under the specified key. - insert: func(key: string, value: string) -> result<_, string>; - } - - /// Returns the command used to start up the language server. - export language-server-command: func(language-server-id: string, worktree: borrow) -> result; - - /// Returns the initialization options to pass to the language server on startup. - /// - /// The initialization options are represented as a JSON string. - export language-server-initialization-options: func(language-server-id: string, worktree: borrow) -> result, string>; - - /// Returns the workspace configuration options to pass to the language server. - export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow) -> result, string>; - - /// Returns the initialization options to pass to the other language server. - export language-server-additional-initialization-options: func(language-server-id: string, target-language-server-id: string, worktree: borrow) -> result, string>; - - /// Returns the workspace configuration options to pass to the other language server. - export language-server-additional-workspace-configuration: func(language-server-id: string, target-language-server-id: string, worktree: borrow) -> result, string>; - - /// A label containing some code. - record code-label { - /// The source code to parse with Tree-sitter. - code: string, - /// The spans to display in the label. - spans: list, - /// The range of the displayed label to include when filtering. - filter-range: range, - } - - /// A span within a code label. - variant code-label-span { - /// A range into the parsed code. - code-range(range), - /// A span containing a code literal. - literal(code-label-span-literal), - } - - /// A span containing a code literal. - record code-label-span-literal { - /// The literal text. - text: string, - /// The name of the highlight to use for this literal. - highlight-name: option, - } - - export labels-for-completions: func(language-server-id: string, completions: list) -> result>, string>; - export labels-for-symbols: func(language-server-id: string, symbols: list) -> result>, string>; - - - /// Returns the completions that should be shown when completing the provided slash command with the given query. - export complete-slash-command-argument: func(command: slash-command, args: list) -> result, string>; - - /// Returns the output from running the provided slash command. - export run-slash-command: func(command: slash-command, args: list, worktree: option>) -> result; - - /// Returns the command used to start up a context server. - export context-server-command: func(context-server-id: string, project: borrow) -> result; - - /// Returns the configuration for a context server. - export context-server-configuration: func(context-server-id: string, project: borrow) -> result, string>; - - /// Returns a list of packages as suggestions to be included in the `/docs` - /// 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. - export suggest-docs-packages: func(provider-name: string) -> result, string>; - - /// Indexes the docs for the specified package. - export index-docs: func(provider-name: string, package-name: string, database: borrow) -> result<_, string>; - - /// Returns a configured debug adapter binary for a given debug task. - export get-dap-binary: func(adapter-name: string, config: debug-task-definition, user-installed-path: option, worktree: borrow) -> result; - /// Returns the kind of a debug scenario (launch or attach). - export dap-request-kind: func(adapter-name: string, config: string) -> result; - export dap-config-to-scenario: func(config: debug-config) -> result; - export dap-locator-create-scenario: func(locator-name: string, build-config-template: build-task-template, resolved-label: string, debug-adapter-name: string) -> option; - export run-dap-locator: func(locator-name: string, config: resolved-task) -> result; -} diff --git a/crates/extension_api/wit/since_v0.8.0/github.wit b/crates/extension_api/wit/since_v0.8.0/github.wit deleted file mode 100644 index 21cd5d4805..0000000000 --- a/crates/extension_api/wit/since_v0.8.0/github.wit +++ /dev/null @@ -1,35 +0,0 @@ -interface github { - /// A GitHub release. - record github-release { - /// The version of the release. - version: string, - /// The list of assets attached to the release. - assets: list, - } - - /// An asset from a GitHub release. - record github-release-asset { - /// The name of the asset. - name: string, - /// The download URL for the asset. - download-url: string, - } - - /// The options used to filter down GitHub releases. - record github-release-options { - /// Whether releases without assets should be included. - require-assets: bool, - /// Whether pre-releases should be included. - pre-release: bool, - } - - /// Returns the latest release for the given GitHub repository. - /// - /// Takes repo as a string in the form "/", for example: "zed-industries/zed". - latest-github-release: func(repo: string, options: github-release-options) -> result; - - /// Returns the GitHub release with the specified tag name for the given GitHub repository. - /// - /// Returns an error if a release with the given tag name does not exist. - github-release-by-tag-name: func(repo: string, tag: string) -> result; -} diff --git a/crates/extension_api/wit/since_v0.8.0/http-client.wit b/crates/extension_api/wit/since_v0.8.0/http-client.wit deleted file mode 100644 index bb0206c17a..0000000000 --- a/crates/extension_api/wit/since_v0.8.0/http-client.wit +++ /dev/null @@ -1,67 +0,0 @@ -interface http-client { - /// An HTTP request. - record http-request { - /// The HTTP method for the request. - method: http-method, - /// The URL to which the request should be made. - url: string, - /// The headers for the request. - headers: list>, - /// The request body. - body: option>, - /// The policy to use for redirects. - redirect-policy: redirect-policy, - } - - /// HTTP methods. - enum http-method { - /// `GET` - get, - /// `HEAD` - head, - /// `POST` - post, - /// `PUT` - put, - /// `DELETE` - delete, - /// `OPTIONS` - options, - /// `PATCH` - patch, - } - - /// The policy for dealing with redirects received from the server. - variant redirect-policy { - /// Redirects from the server will not be followed. - /// - /// This is the default behavior. - no-follow, - /// Redirects from the server will be followed up to the specified limit. - follow-limit(u32), - /// All redirects from the server will be followed. - follow-all, - } - - /// An HTTP response. - record http-response { - /// The response headers. - headers: list>, - /// The response body. - body: list, - } - - /// Performs an HTTP request and returns the response. - fetch: func(req: http-request) -> result; - - /// An HTTP response stream. - resource http-response-stream { - /// Retrieves the next chunk of data from the response stream. - /// - /// Returns `Ok(None)` if the stream has ended. - next-chunk: func() -> result>, string>; - } - - /// Performs an HTTP request and returns a response stream. - fetch-stream: func(req: http-request) -> result; -} diff --git a/crates/extension_api/wit/since_v0.8.0/lsp.wit b/crates/extension_api/wit/since_v0.8.0/lsp.wit deleted file mode 100644 index 91a36c93a6..0000000000 --- a/crates/extension_api/wit/since_v0.8.0/lsp.wit +++ /dev/null @@ -1,90 +0,0 @@ -interface lsp { - /// An LSP completion. - record completion { - label: string, - label-details: option, - detail: option, - kind: option, - insert-text-format: option, - } - - /// The kind of an LSP completion. - variant completion-kind { - text, - method, - function, - %constructor, - field, - variable, - class, - %interface, - module, - property, - unit, - value, - %enum, - keyword, - snippet, - color, - file, - reference, - folder, - enum-member, - constant, - struct, - event, - operator, - type-parameter, - other(s32), - } - - /// Label details for an LSP completion. - record completion-label-details { - detail: option, - description: option, - } - - /// Defines how to interpret the insert text in a completion item. - variant insert-text-format { - plain-text, - snippet, - other(s32), - } - - /// An LSP symbol. - record symbol { - kind: symbol-kind, - name: string, - } - - /// The kind of an LSP symbol. - variant symbol-kind { - file, - module, - namespace, - %package, - class, - method, - property, - field, - %constructor, - %enum, - %interface, - function, - variable, - constant, - %string, - number, - boolean, - array, - object, - key, - null, - enum-member, - struct, - event, - operator, - type-parameter, - other(s32), - } -} diff --git a/crates/extension_api/wit/since_v0.8.0/nodejs.wit b/crates/extension_api/wit/since_v0.8.0/nodejs.wit deleted file mode 100644 index c814548314..0000000000 --- a/crates/extension_api/wit/since_v0.8.0/nodejs.wit +++ /dev/null @@ -1,13 +0,0 @@ -interface nodejs { - /// Returns the path to the Node binary used by Zed. - node-binary-path: func() -> result; - - /// Returns the latest version of the given NPM package. - npm-package-latest-version: func(package-name: string) -> result; - - /// Returns the installed version of the given NPM package, if it exists. - npm-package-installed-version: func(package-name: string) -> result, string>; - - /// Installs the specified NPM package. - npm-install-package: func(package-name: string, version: string) -> result<_, string>; -} diff --git a/crates/extension_api/wit/since_v0.8.0/platform.wit b/crates/extension_api/wit/since_v0.8.0/platform.wit deleted file mode 100644 index 48472a99bc..0000000000 --- a/crates/extension_api/wit/since_v0.8.0/platform.wit +++ /dev/null @@ -1,24 +0,0 @@ -interface platform { - /// An operating system. - enum os { - /// macOS. - mac, - /// Linux. - linux, - /// Windows. - windows, - } - - /// A platform architecture. - enum architecture { - /// AArch64 (e.g., Apple Silicon). - aarch64, - /// x86. - x86, - /// x86-64. - x8664, - } - - /// Gets the current operating system and architecture. - current-platform: func() -> tuple; -} diff --git a/crates/extension_api/wit/since_v0.8.0/process.wit b/crates/extension_api/wit/since_v0.8.0/process.wit deleted file mode 100644 index d9a5728a3d..0000000000 --- a/crates/extension_api/wit/since_v0.8.0/process.wit +++ /dev/null @@ -1,29 +0,0 @@ -interface process { - use common.{env-vars}; - - /// A command. - record command { - /// The command to execute. - command: string, - /// The arguments to pass to the command. - args: list, - /// The environment variables to set for the command. - env: env-vars, - } - - /// The output of a finished process. - record output { - /// The status (exit code) of the process. - /// - /// On Unix, this will be `None` if the process was terminated by a signal. - status: option, - /// The data that the process wrote to stdout. - stdout: list, - /// The data that the process wrote to stderr. - stderr: list, - } - - /// Executes the given command as a child process, waiting for it to finish - /// and collecting all of its output. - run-command: func(command: command) -> result; -} diff --git a/crates/extension_api/wit/since_v0.8.0/settings.rs b/crates/extension_api/wit/since_v0.8.0/settings.rs deleted file mode 100644 index 19e28c1ba9..0000000000 --- a/crates/extension_api/wit/since_v0.8.0/settings.rs +++ /dev/null @@ -1,40 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, num::NonZeroU32}; - -/// The settings for a particular language. -#[derive(Debug, Serialize, Deserialize)] -pub struct LanguageSettings { - /// How many columns a tab should occupy. - pub tab_size: NonZeroU32, -} - -/// The settings for a particular language server. -#[derive(Default, Debug, Serialize, Deserialize)] -pub struct LspSettings { - /// The settings for the language server binary. - pub binary: Option, - /// The initialization options to pass to the language server. - pub initialization_options: Option, - /// The settings to pass to language server. - pub settings: Option, -} - -/// The settings for a particular context server. -#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct ContextServerSettings { - /// The settings for the context server binary. - pub command: Option, - /// The settings to pass to the context server. - pub settings: Option, -} - -/// The settings for a command. -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct CommandSettings { - /// The path to the command. - pub path: Option, - /// The arguments to pass to the command. - pub arguments: Option>, - /// The environment variables. - pub env: Option>, -} diff --git a/crates/extension_api/wit/since_v0.8.0/slash-command.wit b/crates/extension_api/wit/since_v0.8.0/slash-command.wit deleted file mode 100644 index f52561c2ef..0000000000 --- a/crates/extension_api/wit/since_v0.8.0/slash-command.wit +++ /dev/null @@ -1,41 +0,0 @@ -interface slash-command { - use common.{range}; - - /// A slash command for use in the Assistant. - record slash-command { - /// The name of the slash command. - name: string, - /// The description of the slash command. - description: string, - /// The tooltip text to display for the run button. - tooltip-text: string, - /// Whether this slash command requires an argument. - requires-argument: bool, - } - - /// The output of a slash command. - record slash-command-output { - /// The text produced by the slash command. - text: string, - /// The list of sections to show in the slash command placeholder. - sections: list, - } - - /// A section in the slash command output. - record slash-command-output-section { - /// The range this section occupies. - range: range, - /// The label to display in the placeholder for this section. - label: string, - } - - /// A completion for a slash command argument. - record slash-command-argument-completion { - /// The label to display for this completion. - label: string, - /// The new text that should be inserted into the command when this completion is accepted. - new-text: string, - /// Whether the command should be run when accepting this completion. - run-command: bool, - } -} diff --git a/crates/extension_cli/Cargo.toml b/crates/extension_cli/Cargo.toml deleted file mode 100644 index b2562a8e82..0000000000 --- a/crates/extension_cli/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "extension_cli" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[[bin]] -name = "zed-extension" -path = "src/main.rs" - -[dependencies] -anyhow.workspace = true -clap = { workspace = true, features = ["derive"] } -env_logger.workspace = true -extension.workspace = true -fs.workspace = true -gpui.workspace = true -language.workspace = true -log.workspace = true -reqwest_client.workspace = true -rpc.workspace = true -serde.workspace = true -serde_json.workspace = true -theme.workspace = true -tokio = { workspace = true, features = ["full"] } -toml.workspace = true -tree-sitter.workspace = true -wasmtime.workspace = true diff --git a/crates/extension_cli/LICENSE-GPL b/crates/extension_cli/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/extension_cli/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/extension_cli/src/main.rs b/crates/extension_cli/src/main.rs deleted file mode 100644 index 699a6b0143..0000000000 --- a/crates/extension_cli/src/main.rs +++ /dev/null @@ -1,421 +0,0 @@ -use std::collections::{BTreeSet, HashMap}; -use std::env; -use std::fs; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use ::fs::{CopyOptions, Fs, RealFs, copy_recursive}; -use anyhow::{Context as _, Result, bail}; -use clap::Parser; -use extension::ExtensionManifest; -use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; -use language::LanguageConfig; -use reqwest_client::ReqwestClient; -use rpc::ExtensionProvides; -use tokio::process::Command; -use tree_sitter::{Language, Query, WasmStore}; - -#[derive(Parser, Debug)] -#[command(name = "zed-extension")] -struct Args { - /// The path to the extension directory - #[arg(long)] - source_dir: PathBuf, - /// The output directory to place the packaged extension. - #[arg(long)] - output_dir: PathBuf, - /// The path to a directory where build dependencies are downloaded - #[arg(long)] - scratch_dir: PathBuf, -} - -#[tokio::main] -async fn main() -> Result<()> { - env_logger::init(); - - let args = Args::parse(); - let fs = Arc::new(RealFs::new(None, gpui::background_executor())); - let engine = wasmtime::Engine::default(); - let mut wasm_store = WasmStore::new(&engine)?; - - let extension_path = args - .source_dir - .canonicalize() - .context("failed to canonicalize source_dir")?; - let scratch_dir = args - .scratch_dir - .canonicalize() - .context("failed to canonicalize scratch_dir")?; - let output_dir = if args.output_dir.is_relative() { - env::current_dir()?.join(&args.output_dir) - } else { - args.output_dir - }; - - log::info!("loading extension manifest"); - let mut manifest = ExtensionManifest::load(fs.clone(), &extension_path).await?; - - log::info!("compiling extension"); - - let user_agent = format!( - "Zed Extension CLI/{} ({}; {})", - env!("CARGO_PKG_VERSION"), - std::env::consts::OS, - std::env::consts::ARCH - ); - let http_client = Arc::new(ReqwestClient::user_agent(&user_agent)?); - - let builder = ExtensionBuilder::new(http_client, scratch_dir); - builder - .compile_extension( - &extension_path, - &mut manifest, - CompileExtensionOptions { release: true }, - fs.clone(), - ) - .await - .context("failed to compile extension")?; - - let grammars = test_grammars(&manifest, &extension_path, &mut wasm_store)?; - test_languages(&manifest, &extension_path, &grammars)?; - test_themes(&manifest, &extension_path, fs.clone()).await?; - - let archive_dir = output_dir.join("archive"); - fs::remove_dir_all(&archive_dir).ok(); - copy_extension_resources(&manifest, &extension_path, &archive_dir, fs.clone()) - .await - .context("failed to copy extension resources")?; - - let tar_output = Command::new("tar") - .current_dir(&output_dir) - .args(["-czvf", "archive.tar.gz", "-C", "archive", "."]) - .output() - .await - .context("failed to run tar")?; - if !tar_output.status.success() { - bail!( - "failed to create archive.tar.gz: {}", - String::from_utf8_lossy(&tar_output.stderr) - ); - } - - let extension_provides = extension_provides(&manifest); - - let manifest_json = serde_json::to_string(&rpc::ExtensionApiManifest { - name: manifest.name, - version: manifest.version, - description: manifest.description, - authors: manifest.authors, - schema_version: Some(manifest.schema_version.0), - repository: manifest - .repository - .context("missing repository in extension manifest")?, - wasm_api_version: manifest.lib.version.map(|version| version.to_string()), - provides: extension_provides, - })?; - fs::remove_dir_all(&archive_dir)?; - fs::write(output_dir.join("manifest.json"), manifest_json.as_bytes())?; - - Ok(()) -} - -/// Returns the set of features provided by the extension. -fn extension_provides(manifest: &ExtensionManifest) -> BTreeSet { - let mut provides = BTreeSet::default(); - if !manifest.themes.is_empty() { - provides.insert(ExtensionProvides::Themes); - } - - if !manifest.icon_themes.is_empty() { - provides.insert(ExtensionProvides::IconThemes); - } - - if !manifest.languages.is_empty() { - provides.insert(ExtensionProvides::Languages); - } - - if !manifest.grammars.is_empty() { - provides.insert(ExtensionProvides::Grammars); - } - - if !manifest.language_servers.is_empty() { - provides.insert(ExtensionProvides::LanguageServers); - } - - if !manifest.context_servers.is_empty() { - provides.insert(ExtensionProvides::ContextServers); - } - - if !manifest.agent_servers.is_empty() { - provides.insert(ExtensionProvides::AgentServers); - } - - if manifest.snippets.is_some() { - provides.insert(ExtensionProvides::Snippets); - } - - if !manifest.debug_adapters.is_empty() { - provides.insert(ExtensionProvides::DebugAdapters); - } - - provides -} - -async fn copy_extension_resources( - manifest: &ExtensionManifest, - extension_path: &Path, - output_dir: &Path, - fs: Arc, -) -> Result<()> { - fs::create_dir_all(output_dir).context("failed to create output dir")?; - - let manifest_toml = toml::to_string(&manifest).context("failed to serialize manifest")?; - fs::write(output_dir.join("extension.toml"), &manifest_toml) - .context("failed to write extension.toml")?; - - if manifest.lib.kind.is_some() { - fs::copy( - extension_path.join("extension.wasm"), - output_dir.join("extension.wasm"), - ) - .context("failed to copy extension.wasm")?; - } - - if !manifest.grammars.is_empty() { - let source_grammars_dir = extension_path.join("grammars"); - let output_grammars_dir = output_dir.join("grammars"); - fs::create_dir_all(&output_grammars_dir)?; - for grammar_name in manifest.grammars.keys() { - let mut grammar_filename = PathBuf::from(grammar_name.as_ref()); - grammar_filename.set_extension("wasm"); - fs::copy( - source_grammars_dir.join(&grammar_filename), - output_grammars_dir.join(&grammar_filename), - ) - .with_context(|| format!("failed to copy grammar '{}'", grammar_filename.display()))?; - } - } - - if !manifest.themes.is_empty() { - let output_themes_dir = output_dir.join("themes"); - fs::create_dir_all(&output_themes_dir)?; - for theme_path in &manifest.themes { - fs::copy( - extension_path.join(theme_path), - output_themes_dir.join(theme_path.file_name().context("invalid theme path")?), - ) - .with_context(|| format!("failed to copy theme '{}'", theme_path.display()))?; - } - } - - if !manifest.icon_themes.is_empty() { - let output_icon_themes_dir = output_dir.join("icon_themes"); - fs::create_dir_all(&output_icon_themes_dir)?; - for icon_theme_path in &manifest.icon_themes { - fs::copy( - extension_path.join(icon_theme_path), - output_icon_themes_dir.join( - icon_theme_path - .file_name() - .context("invalid icon theme path")?, - ), - ) - .with_context(|| { - format!("failed to copy icon theme '{}'", icon_theme_path.display()) - })?; - } - - let output_icons_dir = output_dir.join("icons"); - fs::create_dir_all(&output_icons_dir)?; - copy_recursive( - fs.as_ref(), - &extension_path.join("icons"), - &output_icons_dir, - CopyOptions { - overwrite: true, - ignore_if_exists: false, - }, - ) - .await - .with_context(|| "failed to copy icons")?; - } - - for (_, agent_entry) in &manifest.agent_servers { - if let Some(icon_path) = &agent_entry.icon { - let source_icon = extension_path.join(icon_path); - let dest_icon = output_dir.join(icon_path); - - // Create parent directory if needed - if let Some(parent) = dest_icon.parent() { - fs::create_dir_all(parent)?; - } - - fs::copy(&source_icon, &dest_icon) - .with_context(|| format!("failed to copy agent server icon '{}'", icon_path))?; - } - } - - if !manifest.languages.is_empty() { - let output_languages_dir = output_dir.join("languages"); - fs::create_dir_all(&output_languages_dir)?; - for language_path in &manifest.languages { - copy_recursive( - fs.as_ref(), - &extension_path.join(language_path), - &output_languages_dir - .join(language_path.file_name().context("invalid language path")?), - CopyOptions { - overwrite: true, - ignore_if_exists: false, - }, - ) - .await - .with_context(|| { - format!("failed to copy language dir '{}'", language_path.display()) - })?; - } - } - - if !manifest.debug_adapters.is_empty() { - for (debug_adapter, entry) in &manifest.debug_adapters { - let schema_path = entry.schema_path.clone().unwrap_or_else(|| { - PathBuf::from("debug_adapter_schemas".to_owned()) - .join(debug_adapter.as_ref()) - .with_extension("json") - }); - let parent = schema_path - .parent() - .with_context(|| format!("invalid empty schema path for {debug_adapter}"))?; - fs::create_dir_all(output_dir.join(parent))?; - copy_recursive( - fs.as_ref(), - &extension_path.join(&schema_path), - &output_dir.join(&schema_path), - CopyOptions { - overwrite: true, - ignore_if_exists: false, - }, - ) - .await - .with_context(|| { - format!( - "failed to copy debug adapter schema '{}'", - schema_path.display() - ) - })?; - } - } - - if let Some(snippets_path) = manifest.snippets.as_ref() { - let parent = snippets_path.parent(); - if let Some(parent) = parent.filter(|p| p.components().next().is_some()) { - fs::create_dir_all(output_dir.join(parent))?; - } - copy_recursive( - fs.as_ref(), - &extension_path.join(&snippets_path), - &output_dir.join(&snippets_path), - CopyOptions { - overwrite: true, - ignore_if_exists: false, - }, - ) - .await - .with_context(|| format!("failed to copy snippets from '{}'", snippets_path.display()))?; - } - - Ok(()) -} - -fn test_grammars( - manifest: &ExtensionManifest, - extension_path: &Path, - wasm_store: &mut WasmStore, -) -> Result> { - let mut grammars = HashMap::default(); - let grammars_dir = extension_path.join("grammars"); - - for grammar_name in manifest.grammars.keys() { - let mut grammar_path = grammars_dir.join(grammar_name.as_ref()); - grammar_path.set_extension("wasm"); - - let wasm = fs::read(&grammar_path)?; - let language = wasm_store.load_language(grammar_name, &wasm)?; - log::info!("loaded grammar {grammar_name}"); - grammars.insert(grammar_name.to_string(), language); - } - - Ok(grammars) -} - -fn test_languages( - manifest: &ExtensionManifest, - extension_path: &Path, - grammars: &HashMap, -) -> Result<()> { - for relative_language_dir in &manifest.languages { - let language_dir = extension_path.join(relative_language_dir); - let config_path = language_dir.join("config.toml"); - let config_content = fs::read_to_string(&config_path)?; - let config: LanguageConfig = toml::from_str(&config_content)?; - let grammar = if let Some(name) = &config.grammar { - Some( - grammars - .get(name.as_ref()) - .with_context(|| format!("grammar not found: '{name}'"))?, - ) - } else { - None - }; - - let query_entries = fs::read_dir(&language_dir)?; - for entry in query_entries { - let entry = entry?; - let query_path = entry.path(); - if query_path.extension() == Some("scm".as_ref()) { - let grammar = grammar.with_context(|| { - format! { - "language {} provides query {} but no grammar", - config.name, - query_path.display() - } - })?; - - let query_source = fs::read_to_string(&query_path)?; - let _query = Query::new(grammar, &query_source)?; - } - } - - log::info!("loaded language {}", config.name); - } - - Ok(()) -} - -async fn test_themes( - manifest: &ExtensionManifest, - extension_path: &Path, - fs: Arc, -) -> Result<()> { - for relative_theme_path in &manifest.themes { - let theme_path = extension_path.join(relative_theme_path); - let theme_family = theme::read_user_theme(&theme_path, fs.clone()).await?; - log::info!("loaded theme family {}", theme_family.name); - - for theme in &theme_family.themes { - if theme - .style - .colors - .deprecated_scrollbar_thumb_background - .is_some() - { - bail!( - r#"Theme "{theme_name}" is using a deprecated style property: scrollbar_thumb.background. Use `scrollbar.thumb.background` instead."#, - theme_name = theme.name - ) - } - } - } - - Ok(()) -} diff --git a/crates/extension_host/Cargo.toml b/crates/extension_host/Cargo.toml deleted file mode 100644 index 328b808b13..0000000000 --- a/crates/extension_host/Cargo.toml +++ /dev/null @@ -1,73 +0,0 @@ -[package] -name = "extension_host" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/extension_host.rs" -doctest = false - -[features] -test-support = [] - -[dependencies] -anyhow.workspace = true -async-compression.workspace = true -async-tar.workspace = true -async-trait.workspace = true -client.workspace = true -collections.workspace = true -dap.workspace = true -extension.workspace = true -fs.workspace = true -futures.workspace = true -gpui.workspace = true -gpui_tokio.workspace = true -http_client.workspace = true -language.workspace = true -log.workspace = true -lsp.workspace = true -moka.workspace = true -node_runtime.workspace = true -paths.workspace = true -project.workspace = true -remote.workspace = true -release_channel.workspace = true -semver.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_json_lenient.workspace = true -settings.workspace = true -task.workspace = true -telemetry.workspace = true -tempfile.workspace = true -toml.workspace = true -url.workspace = true -util.workspace = true -wasmparser.workspace = true -wasmtime-wasi.workspace = true -wasmtime.workspace = true - -[dev-dependencies] -criterion.workspace = true -ctor.workspace = true -fs = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -language_extension.workspace = true -parking_lot.workspace = true -project = { workspace = true, features = ["test-support"] } -rand.workspace = true -reqwest_client.workspace = true -theme = { workspace = true, features = ["test-support"] } -theme_extension.workspace = true -zlog.workspace = true - -[[bench]] -name = "extension_compilation_benchmark" -harness = false diff --git a/crates/extension_host/LICENSE-GPL b/crates/extension_host/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/extension_host/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/extension_host/benches/extension_compilation_benchmark.rs b/crates/extension_host/benches/extension_compilation_benchmark.rs deleted file mode 100644 index a28f617dc3..0000000000 --- a/crates/extension_host/benches/extension_compilation_benchmark.rs +++ /dev/null @@ -1,155 +0,0 @@ -use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; - -use criterion::{BatchSize, BenchmarkId, Criterion, criterion_group, criterion_main}; -use extension::{ - ExtensionCapability, ExtensionHostProxy, ExtensionLibraryKind, ExtensionManifest, - LanguageServerManifestEntry, LibManifestEntry, SchemaVersion, - extension_builder::{CompileExtensionOptions, ExtensionBuilder}, -}; -use extension_host::wasm_host::WasmHost; -use fs::{Fs, RealFs}; -use gpui::{TestAppContext, TestDispatcher}; -use http_client::{FakeHttpClient, Response}; -use node_runtime::NodeRuntime; -use rand::{SeedableRng, rngs::StdRng}; -use reqwest_client::ReqwestClient; -use serde_json::json; -use settings::SettingsStore; -use util::test::TempTree; - -fn extension_benchmarks(c: &mut Criterion) { - let cx = init(); - cx.update(gpui_tokio::init); - - let mut group = c.benchmark_group("load"); - - let mut manifest = manifest(); - let wasm_bytes = wasm_bytes( - &cx, - &mut manifest, - Arc::new(RealFs::new(None, cx.executor())), - ); - let manifest = Arc::new(manifest); - let extensions_dir = TempTree::new(json!({ - "installed": {}, - "work": {} - })); - let wasm_host = wasm_host(&cx, &extensions_dir); - - group.bench_function(BenchmarkId::from_parameter(1), |b| { - b.iter_batched( - || wasm_bytes.clone(), - |wasm_bytes| { - let _extension = cx - .executor() - .block(wasm_host.load_extension(wasm_bytes, &manifest, &cx.to_async())) - .unwrap(); - }, - BatchSize::SmallInput, - ); - }); -} - -fn init() -> TestAppContext { - const SEED: u64 = 9999; - let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(SEED)); - let cx = TestAppContext::build(dispatcher, None); - cx.executor().allow_parking(); - cx.update(|cx| { - let store = SettingsStore::test(cx); - cx.set_global(store); - release_channel::init(semver::Version::new(0, 0, 0), cx); - }); - - cx -} - -fn wasm_bytes(cx: &TestAppContext, manifest: &mut ExtensionManifest, fs: Arc) -> Vec { - let extension_builder = extension_builder(); - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .parent() - .unwrap() - .join("extensions/test-extension"); - cx.executor() - .block(extension_builder.compile_extension( - &path, - manifest, - CompileExtensionOptions { release: true }, - fs, - )) - .unwrap(); - std::fs::read(path.join("extension.wasm")).unwrap() -} - -fn extension_builder() -> ExtensionBuilder { - let user_agent = format!( - "Zed Extension CLI/{} ({}; {})", - env!("CARGO_PKG_VERSION"), - std::env::consts::OS, - std::env::consts::ARCH - ); - let http_client = Arc::new(ReqwestClient::user_agent(&user_agent).unwrap()); - // Local dir so that we don't have to download it on every run - let build_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("benches/.build"); - ExtensionBuilder::new(http_client, build_dir) -} - -fn wasm_host(cx: &TestAppContext, extensions_dir: &TempTree) -> Arc { - let http_client = FakeHttpClient::create(async |_| { - Ok(Response::builder().status(404).body("not found".into())?) - }); - let extensions_dir = extensions_dir.path().canonicalize().unwrap(); - let work_dir = extensions_dir.join("work"); - let fs = Arc::new(RealFs::new(None, cx.executor())); - - cx.update(|cx| { - WasmHost::new( - fs, - http_client, - NodeRuntime::unavailable(), - Arc::new(ExtensionHostProxy::new()), - work_dir, - cx, - ) - }) -} - -fn manifest() -> ExtensionManifest { - ExtensionManifest { - id: "test-extension".into(), - name: "Test Extension".into(), - version: "0.1.0".into(), - schema_version: SchemaVersion(1), - description: Some("An extension for use in tests.".into()), - authors: Vec::new(), - repository: None, - themes: Default::default(), - icon_themes: Vec::new(), - lib: LibManifestEntry { - kind: Some(ExtensionLibraryKind::Rust), - version: Some(semver::Version::new(0, 1, 0)), - }, - languages: Vec::new(), - grammars: BTreeMap::default(), - language_servers: [("gleam".into(), LanguageServerManifestEntry::default())] - .into_iter() - .collect(), - context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), - slash_commands: BTreeMap::default(), - snippets: None, - capabilities: vec![ExtensionCapability::ProcessExec( - extension::ProcessExecCapability { - command: "echo".into(), - args: vec!["hello!".into()], - }, - )], - debug_adapters: Default::default(), - debug_locators: Default::default(), - } -} - -criterion_group!(benches, extension_benchmarks); -criterion_main!(benches); diff --git a/crates/extension_host/build.rs b/crates/extension_host/build.rs deleted file mode 100644 index f2c2b19998..0000000000 --- a/crates/extension_host/build.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::env; -use std::fs; -use std::path::PathBuf; - -fn main() -> Result<(), Box> { - copy_extension_api_rust_files() -} - -/// rust-analyzer doesn't support include! for files from outside the crate. -/// Copy them to the OUT_DIR, so we can include them from there, which is supported. -fn copy_extension_api_rust_files() -> Result<(), Box> { - let out_dir = env::var("OUT_DIR")?; - let input_dir = PathBuf::from("../extension_api/wit"); - let output_dir = PathBuf::from(out_dir); - - println!("cargo:rerun-if-changed={}", input_dir.display()); - - for entry in fs::read_dir(&input_dir)? { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - println!("cargo:rerun-if-changed={}", path.display()); - - for subentry in fs::read_dir(&path)? { - let subentry = subentry?; - let subpath = subentry.path(); - if subpath.extension() == Some(std::ffi::OsStr::new("rs")) { - let relative_path = subpath.strip_prefix(&input_dir)?; - let destination = output_dir.join(relative_path); - - fs::create_dir_all(destination.parent().unwrap())?; - fs::copy(&subpath, &destination)?; - } - } - } else if path.extension() == Some(std::ffi::OsStr::new("rs")) { - let relative_path = path.strip_prefix(&input_dir)?; - let destination = output_dir.join(relative_path); - - fs::create_dir_all(destination.parent().unwrap())?; - fs::copy(&path, &destination)?; - println!("cargo:rerun-if-changed={}", path.display()); - } - } - - Ok(()) -} diff --git a/crates/extension_host/src/capability_granter.rs b/crates/extension_host/src/capability_granter.rs deleted file mode 100644 index 9f27b5e480..0000000000 --- a/crates/extension_host/src/capability_granter.rs +++ /dev/null @@ -1,153 +0,0 @@ -use std::sync::Arc; - -use anyhow::{Result, bail}; -use extension::{ExtensionCapability, ExtensionManifest}; -use url::Url; - -pub struct CapabilityGranter { - granted_capabilities: Vec, - manifest: Arc, -} - -impl CapabilityGranter { - pub fn new( - granted_capabilities: Vec, - manifest: Arc, - ) -> Self { - Self { - granted_capabilities, - manifest, - } - } - - pub fn grant_exec( - &self, - desired_command: &str, - desired_args: &[impl AsRef + std::fmt::Debug], - ) -> Result<()> { - self.manifest.allow_exec(desired_command, desired_args)?; - - let is_allowed = self - .granted_capabilities - .iter() - .any(|capability| match capability { - ExtensionCapability::ProcessExec(capability) => { - capability.allows(desired_command, desired_args) - } - _ => false, - }); - - if !is_allowed { - bail!( - "capability for process:exec {desired_command} {desired_args:?} is not granted by the extension host", - ); - } - - Ok(()) - } - - pub fn grant_download_file(&self, desired_url: &Url) -> Result<()> { - let is_allowed = self - .granted_capabilities - .iter() - .any(|capability| match capability { - ExtensionCapability::DownloadFile(capability) => capability.allows(desired_url), - _ => false, - }); - - if !is_allowed { - bail!( - "capability for download_file {desired_url} is not granted by the extension host", - ); - } - - Ok(()) - } - - pub fn grant_npm_install_package(&self, package_name: &str) -> Result<()> { - let is_allowed = self - .granted_capabilities - .iter() - .any(|capability| match capability { - ExtensionCapability::NpmInstallPackage(capability) => { - capability.allows(package_name) - } - _ => false, - }); - - if !is_allowed { - bail!("capability for npm:install {package_name} is not granted by the extension host",); - } - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use std::collections::BTreeMap; - - use extension::{ProcessExecCapability, SchemaVersion}; - - use super::*; - - fn extension_manifest() -> ExtensionManifest { - ExtensionManifest { - id: "test".into(), - name: "Test".to_string(), - version: "1.0.0".into(), - schema_version: SchemaVersion::ZERO, - description: None, - repository: None, - authors: vec![], - lib: Default::default(), - themes: vec![], - icon_themes: vec![], - languages: vec![], - grammars: BTreeMap::default(), - language_servers: BTreeMap::default(), - context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), - slash_commands: BTreeMap::default(), - snippets: None, - capabilities: vec![], - debug_adapters: Default::default(), - debug_locators: Default::default(), - } - } - - #[test] - fn test_grant_exec() { - let manifest = Arc::new(ExtensionManifest { - capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability { - command: "ls".to_string(), - args: vec!["-la".to_string()], - })], - ..extension_manifest() - }); - - // It returns an error when the extension host has no granted capabilities. - let granter = CapabilityGranter::new(Vec::new(), manifest.clone()); - assert!(granter.grant_exec("ls", &["-la"]).is_err()); - - // It succeeds when the extension host has the exact capability. - let granter = CapabilityGranter::new( - vec![ExtensionCapability::ProcessExec(ProcessExecCapability { - command: "ls".to_string(), - args: vec!["-la".to_string()], - })], - manifest.clone(), - ); - assert!(granter.grant_exec("ls", &["-la"]).is_ok()); - - // It succeeds when the extension host has a wildcard capability. - let granter = CapabilityGranter::new( - vec![ExtensionCapability::ProcessExec(ProcessExecCapability { - command: "*".to_string(), - args: vec!["**".to_string()], - })], - manifest, - ); - 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 deleted file mode 100644 index 09e8259771..0000000000 --- a/crates/extension_host/src/extension_host.rs +++ /dev/null @@ -1,1878 +0,0 @@ -mod capability_granter; -pub mod extension_settings; -pub mod headless_host; -pub mod wasm_host; - -#[cfg(test)] -mod extension_store_test; - -use anyhow::{Context as _, Result, anyhow, bail}; -use async_compression::futures::bufread::GzipDecoder; -use async_tar::Archive; -use client::ExtensionProvides; -use client::{Client, ExtensionMetadata, GetExtensionsResponse, proto, telemetry::Telemetry}; -use collections::{BTreeMap, BTreeSet, HashSet, btree_map}; -pub use extension::ExtensionManifest; -use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; -use extension::{ - ExtensionContextServerProxy, ExtensionDebugAdapterProviderProxy, ExtensionEvents, - ExtensionGrammarProxy, ExtensionHostProxy, ExtensionLanguageProxy, - ExtensionLanguageServerProxy, ExtensionSlashCommandProxy, ExtensionSnippetProxy, - ExtensionThemeProxy, -}; -use fs::{Fs, RemoveOptions}; -use futures::future::join_all; -use futures::{ - AsyncReadExt as _, Future, FutureExt as _, StreamExt as _, - channel::{ - mpsc::{UnboundedSender, unbounded}, - oneshot, - }, - io::BufReader, - select_biased, -}; -use gpui::{ - App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, Task, WeakEntity, - actions, -}; -use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; -use language::{ - LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage, - QUERY_FILENAME_PREFIXES, Rope, -}; -use node_runtime::NodeRuntime; -use project::ContextProviderWithTasks; -use release_channel::ReleaseChannel; -use remote::RemoteClient; -use semver::Version; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use std::ops::RangeInclusive; -use std::str::FromStr; -use std::{ - cmp::Ordering, - path::{self, Path, PathBuf}, - sync::Arc, - time::{Duration, Instant}, -}; -use url::Url; -use util::{ResultExt, paths::RemotePathBuf}; -use wasm_host::{ - WasmExtension, WasmHost, - wit::{is_supported_wasm_api_version, wasm_api_version_range}, -}; - -pub use extension::{ - ExtensionLibraryKind, GrammarManifestEntry, OldExtensionManifest, SchemaVersion, -}; -pub use extension_settings::ExtensionSettings; - -pub const RELOAD_DEBOUNCE_DURATION: Duration = Duration::from_millis(200); -const FS_WATCH_LATENCY: Duration = Duration::from_millis(100); - -/// The current extension [`SchemaVersion`] supported by Zed. -const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(1); - -/// Extensions that should no longer be loaded or downloaded. -/// -/// These snippets should no longer be downloaded or loaded, because their -/// functionality has been integrated into the core editor. -const SUPPRESSED_EXTENSIONS: &[&str] = &["snippets", "ruff", "ty", "basedpyright"]; - -/// Returns the [`SchemaVersion`] range that is compatible with this version of Zed. -pub fn schema_version_range() -> RangeInclusive { - SchemaVersion::ZERO..=CURRENT_SCHEMA_VERSION -} - -/// Returns whether the given extension version is compatible with this version of Zed. -pub fn is_version_compatible( - release_channel: ReleaseChannel, - extension_version: &ExtensionMetadata, -) -> bool { - let schema_version = extension_version.manifest.schema_version.unwrap_or(0); - if CURRENT_SCHEMA_VERSION.0 < schema_version { - return false; - } - - if let Some(wasm_api_version) = extension_version - .manifest - .wasm_api_version - .as_ref() - .and_then(|wasm_api_version| Version::from_str(wasm_api_version).ok()) - && !is_supported_wasm_api_version(release_channel, wasm_api_version) - { - return false; - } - - true -} - -pub struct ExtensionStore { - pub proxy: Arc, - pub builder: Arc, - pub extension_index: ExtensionIndex, - pub fs: Arc, - pub http_client: Arc, - pub telemetry: Option>, - pub reload_tx: UnboundedSender>>, - pub reload_complete_senders: Vec>, - pub installed_dir: PathBuf, - pub outstanding_operations: BTreeMap, ExtensionOperation>, - pub index_path: PathBuf, - pub modified_extensions: HashSet>, - pub wasm_host: Arc, - pub wasm_extensions: Vec<(Arc, WasmExtension)>, - pub tasks: Vec>, - pub remote_clients: Vec>, - pub ssh_registered_tx: UnboundedSender<()>, -} - -#[derive(Clone, Copy)] -pub enum ExtensionOperation { - Upgrade, - Install, - Remove, -} - -#[derive(Clone)] -pub enum Event { - ExtensionsUpdated, - StartedReloading, - ExtensionInstalled(Arc), - ExtensionUninstalled(Arc), - ExtensionFailedToLoad(Arc), -} - -impl EventEmitter for ExtensionStore {} - -struct GlobalExtensionStore(Entity); - -impl Global for GlobalExtensionStore {} - -#[derive(Debug, Deserialize, Serialize, Default, PartialEq, Eq)] -pub struct ExtensionIndex { - pub extensions: BTreeMap, ExtensionIndexEntry>, - pub themes: BTreeMap, ExtensionIndexThemeEntry>, - #[serde(default)] - pub icon_themes: BTreeMap, ExtensionIndexIconThemeEntry>, - pub languages: BTreeMap, -} - -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct ExtensionIndexEntry { - pub manifest: Arc, - pub dev: bool, -} - -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] -pub struct ExtensionIndexThemeEntry { - pub extension: Arc, - pub path: PathBuf, -} - -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] -pub struct ExtensionIndexIconThemeEntry { - pub extension: Arc, - pub path: PathBuf, -} - -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] -pub struct ExtensionIndexLanguageEntry { - pub extension: Arc, - pub path: PathBuf, - pub matcher: LanguageMatcher, - pub hidden: bool, - pub grammar: Option>, -} - -actions!( - zed, - [ - /// Reloads all installed extensions. - ReloadExtensions - ] -); - -pub fn init( - extension_host_proxy: Arc, - fs: Arc, - client: Arc, - node_runtime: NodeRuntime, - cx: &mut App, -) { - let store = cx.new(move |cx| { - ExtensionStore::new( - paths::extensions_dir().clone(), - None, - extension_host_proxy, - fs, - client.http_client(), - client.http_client(), - Some(client.telemetry().clone()), - node_runtime, - cx, - ) - }); - - cx.on_action(|_: &ReloadExtensions, cx| { - let store = cx.global::().0.clone(); - store.update(cx, |store, cx| drop(store.reload(None, cx))); - }); - - cx.set_global(GlobalExtensionStore(store)); -} - -impl ExtensionStore { - pub fn try_global(cx: &App) -> Option> { - cx.try_global::() - .map(|store| store.0.clone()) - } - - pub fn global(cx: &App) -> Entity { - cx.global::().0.clone() - } - - pub fn new( - extensions_dir: PathBuf, - build_dir: Option, - extension_host_proxy: Arc, - fs: Arc, - http_client: Arc, - builder_client: Arc, - telemetry: Option>, - node_runtime: NodeRuntime, - cx: &mut Context, - ) -> Self { - let work_dir = extensions_dir.join("work"); - let build_dir = build_dir.unwrap_or_else(|| extensions_dir.join("build")); - let installed_dir = extensions_dir.join("installed"); - let index_path = extensions_dir.join("index.json"); - - let (reload_tx, mut reload_rx) = unbounded(); - let (connection_registered_tx, mut connection_registered_rx) = unbounded(); - let mut this = Self { - proxy: extension_host_proxy.clone(), - extension_index: Default::default(), - installed_dir, - index_path, - builder: Arc::new(ExtensionBuilder::new(builder_client, build_dir)), - outstanding_operations: Default::default(), - modified_extensions: Default::default(), - reload_complete_senders: Vec::new(), - wasm_host: WasmHost::new( - fs.clone(), - http_client.clone(), - node_runtime, - extension_host_proxy, - work_dir, - cx, - ), - wasm_extensions: Vec::new(), - fs, - http_client, - telemetry, - reload_tx, - tasks: Vec::new(), - - remote_clients: Default::default(), - ssh_registered_tx: connection_registered_tx, - }; - - // The extensions store maintains an index file, which contains a complete - // list of the installed extensions and the resources that they provide. - // This index is loaded synchronously on startup. - let (index_content, index_metadata, extensions_metadata) = - cx.background_executor().block(async { - futures::join!( - this.fs.load(&this.index_path), - this.fs.metadata(&this.index_path), - this.fs.metadata(&this.installed_dir), - ) - }); - - // Normally, there is no need to rebuild the index. But if the index file - // is invalid or is out-of-date according to the filesystem mtimes, then - // 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; - } - } - - // Immediately load all of the extensions in the initial manifest. If the - // index needs to be rebuild, then enqueue - let load_initial_extensions = this.extensions_updated(extension_index, cx); - let mut reload_future = None; - if extension_index_needs_rebuild { - reload_future = Some(this.reload(None, cx)); - } - - cx.spawn(async move |this, cx| { - if let Some(future) = reload_future { - future.await; - } - this.update(cx, |this, cx| this.auto_install_extensions(cx)) - .ok(); - this.update(cx, |this, cx| this.check_for_updates(cx)).ok(); - }) - .detach(); - - // Perform all extension loading in a single task to ensure that we - // never attempt to simultaneously load/unload extensions from multiple - // parallel tasks. - this.tasks.push(cx.spawn(async move |this, cx| { - async move { - load_initial_extensions.await; - - let mut index_changed = false; - let mut debounce_timer = cx.background_spawn(futures::future::pending()).fuse(); - loop { - select_biased! { - _ = debounce_timer => { - if index_changed { - let index = this - .update(cx, |this, cx| this.rebuild_extension_index(cx))? - .await; - this.update(cx, |this, cx| this.extensions_updated(index, cx))? - .await; - index_changed = false; - } - - Self::update_remote_clients(&this, cx).await?; - } - _ = connection_registered_rx.next() => { - debounce_timer = cx - .background_executor() - .timer(RELOAD_DEBOUNCE_DURATION) - .fuse(); - } - extension_id = reload_rx.next() => { - let Some(extension_id) = extension_id else { break; }; - this.update(cx, |this, _| { - this.modified_extensions.extend(extension_id); - })?; - index_changed = true; - debounce_timer = cx - .background_executor() - .timer(RELOAD_DEBOUNCE_DURATION) - .fuse(); - } - } - } - - anyhow::Ok(()) - } - .map(drop) - .await; - })); - - // Watch the installed extensions directory for changes. Whenever changes are - // detected, rebuild the extension index, and load/unload any extensions that - // have been added, removed, or modified. - this.tasks.push(cx.background_spawn({ - let fs = this.fs.clone(); - let reload_tx = this.reload_tx.clone(); - let installed_dir = this.installed_dir.clone(); - async move { - let (mut paths, _) = fs.watch(&installed_dir, FS_WATCH_LATENCY).await; - while let Some(events) = paths.next().await { - for event in events { - let Ok(event_path) = event.path.strip_prefix(&installed_dir) else { - continue; - }; - - 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(); - } - } - } - } - })); - - this - } - - pub fn reload( - &mut self, - modified_extension: Option>, - cx: &mut Context, - ) -> impl Future + use<> { - let (tx, rx) = oneshot::channel(); - self.reload_complete_senders.push(tx); - self.reload_tx - .unbounded_send(modified_extension) - .expect("reload task exited"); - cx.emit(Event::StartedReloading); - - async move { - rx.await.ok(); - } - } - - fn extensions_dir(&self) -> PathBuf { - self.installed_dir.clone() - } - - pub fn outstanding_operations(&self) -> &BTreeMap, ExtensionOperation> { - &self.outstanding_operations - } - - pub fn installed_extensions(&self) -> &BTreeMap, ExtensionIndexEntry> { - &self.extension_index.extensions - } - - pub fn dev_extensions(&self) -> impl Iterator> { - self.extension_index - .extensions - .values() - .filter_map(|extension| extension.dev.then_some(&extension.manifest)) - } - - pub fn extension_manifest_for_id(&self, extension_id: &str) -> Option<&Arc> { - self.extension_index - .extensions - .get(extension_id) - .map(|extension| &extension.manifest) - } - - /// Returns the names of themes provided by extensions. - pub fn extension_themes<'a>( - &'a self, - extension_id: &'a str, - ) -> impl Iterator> { - self.extension_index - .themes - .iter() - .filter_map(|(name, theme)| theme.extension.as_ref().eq(extension_id).then_some(name)) - } - - /// Returns the path to the theme file within an extension, if there is an - /// extension that provides the theme. - pub fn path_to_extension_theme(&self, theme_name: &str) -> Option { - let entry = self.extension_index.themes.get(theme_name)?; - - Some( - self.extensions_dir() - .join(entry.extension.as_ref()) - .join(&entry.path), - ) - } - - /// Returns the names of icon themes provided by extensions. - pub fn extension_icon_themes<'a>( - &'a self, - extension_id: &'a str, - ) -> impl Iterator> { - self.extension_index - .icon_themes - .iter() - .filter_map(|(name, icon_theme)| { - icon_theme - .extension - .as_ref() - .eq(extension_id) - .then_some(name) - }) - } - - /// Returns the path to the icon theme file within an extension, if there is - /// an extension that provides the icon theme. - pub fn path_to_extension_icon_theme( - &self, - icon_theme_name: &str, - ) -> Option<(PathBuf, PathBuf)> { - let entry = self.extension_index.icon_themes.get(icon_theme_name)?; - - let icon_theme_path = self - .extensions_dir() - .join(entry.extension.as_ref()) - .join(&entry.path); - let icons_root_path = self.extensions_dir().join(entry.extension.as_ref()); - - Some((icon_theme_path, icons_root_path)) - } - - pub fn fetch_extensions( - &self, - search: Option<&str>, - provides_filter: Option<&BTreeSet>, - cx: &mut Context, - ) -> Task>> { - let version = CURRENT_SCHEMA_VERSION.to_string(); - let mut query = vec![("max_schema_version", version.as_str())]; - if let Some(search) = search { - query.push(("filter", search)); - } - - let provides_filter = provides_filter.map(|provides_filter| { - provides_filter - .iter() - .map(|provides| provides.to_string()) - .collect::>() - .join(",") - }); - if let Some(provides_filter) = provides_filter.as_deref() { - query.push(("provides", provides_filter)); - } - - self.fetch_extensions_from_api("/extensions", &query, cx) - } - - pub fn fetch_extensions_with_update_available( - &mut self, - cx: &mut Context, - ) -> Task>> { - let schema_versions = schema_version_range(); - let wasm_api_versions = wasm_api_version_range(ReleaseChannel::global(cx)); - let extension_settings = ExtensionSettings::get_global(cx); - let extension_ids = self - .extension_index - .extensions - .iter() - .filter(|(id, entry)| !entry.dev && extension_settings.should_auto_update(id)) - .map(|(id, _)| id.as_ref()) - .collect::>() - .join(","); - let task = self.fetch_extensions_from_api( - "/extensions/updates", - &[ - ("min_schema_version", &schema_versions.start().to_string()), - ("max_schema_version", &schema_versions.end().to_string()), - ( - "min_wasm_api_version", - &wasm_api_versions.start().to_string(), - ), - ("max_wasm_api_version", &wasm_api_versions.end().to_string()), - ("ids", &extension_ids), - ], - cx, - ); - cx.spawn(async move |this, cx| { - let extensions = task.await?; - this.update(cx, |this, _cx| { - extensions - .into_iter() - .filter(|extension| { - this.extension_index - .extensions - .get(&extension.id) - .is_none_or(|installed_extension| { - installed_extension.manifest.version != extension.manifest.version - }) - }) - .collect() - }) - }) - } - - pub fn fetch_extension_versions( - &self, - extension_id: &str, - cx: &mut Context, - ) -> Task>> { - self.fetch_extensions_from_api(&format!("/extensions/{extension_id}"), &[], cx) - } - - /// Installs any extensions that should be included with Zed by default. - /// - /// This can be used to make certain functionality provided by extensions - /// available out-of-the-box. - pub fn auto_install_extensions(&mut self, cx: &mut Context) { - if cfg!(test) { - return; - } - - let extension_settings = ExtensionSettings::get_global(cx); - - let extensions_to_install = extension_settings - .auto_install_extensions - .keys() - .filter(|extension_id| extension_settings.should_auto_install(extension_id)) - .filter(|extension_id| { - let is_already_installed = self - .extension_index - .extensions - .contains_key(extension_id.as_ref()); - !is_already_installed && !SUPPRESSED_EXTENSIONS.contains(&extension_id.as_ref()) - }) - .cloned() - .collect::>(); - - cx.spawn(async move |this, cx| { - for extension_id in extensions_to_install { - this.update(cx, |this, cx| { - this.install_latest_extension(extension_id.clone(), cx); - }) - .ok(); - } - }) - .detach(); - } - - pub fn check_for_updates(&mut self, cx: &mut Context) { - let task = self.fetch_extensions_with_update_available(cx); - cx.spawn(async move |this, cx| Self::upgrade_extensions(this, task.await?, cx).await) - .detach(); - } - - async fn upgrade_extensions( - this: WeakEntity, - extensions: Vec, - cx: &mut AsyncApp, - ) -> Result<()> { - for extension in extensions { - let task = this.update(cx, |this, cx| { - if let Some(installed_extension) = - this.extension_index.extensions.get(&extension.id) - { - let installed_version = - Version::from_str(&installed_extension.manifest.version).ok()?; - let latest_version = Version::from_str(&extension.manifest.version).ok()?; - - if installed_version >= latest_version { - return None; - } - } - - Some(this.upgrade_extension(extension.id, extension.manifest.version, cx)) - })?; - - if let Some(task) = task { - task.await.log_err(); - } - } - anyhow::Ok(()) - } - - fn fetch_extensions_from_api( - &self, - path: &str, - query: &[(&str, &str)], - cx: &mut Context, - ) -> Task>> { - let url = self.http_client.build_zed_api_url(path, query); - let http_client = self.http_client.clone(); - cx.spawn(async move |_, _| { - let mut response = http_client - .get(url?.as_ref(), AsyncBody::empty(), true) - .await?; - - let mut body = Vec::new(); - response - .body_mut() - .read_to_end(&mut body) - .await - .context("error reading extensions")?; - - 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 mut response: GetExtensionsResponse = serde_json::from_slice(&body)?; - - response - .data - .retain(|extension| !SUPPRESSED_EXTENSIONS.contains(&extension.id.as_ref())); - - Ok(response.data) - }) - } - - pub fn install_extension( - &mut self, - extension_id: Arc, - version: Arc, - cx: &mut Context, - ) { - self.install_or_upgrade_extension(extension_id, version, ExtensionOperation::Install, cx) - .detach_and_log_err(cx); - } - - fn install_or_upgrade_extension_at_endpoint( - &mut self, - extension_id: Arc, - url: Url, - operation: ExtensionOperation, - cx: &mut Context, - ) -> Task> { - let extension_dir = self.installed_dir.join(extension_id.as_ref()); - let http_client = self.http_client.clone(); - let fs = self.fs.clone(); - - match self.outstanding_operations.entry(extension_id.clone()) { - btree_map::Entry::Occupied(_) => return Task::ready(Ok(())), - btree_map::Entry::Vacant(e) => e.insert(operation), - }; - cx.notify(); - - cx.spawn(async move |this, cx| { - let _finish = cx.on_drop(&this, { - let extension_id = extension_id.clone(); - move |this, cx| { - this.outstanding_operations.remove(extension_id.as_ref()); - cx.notify(); - } - }); - - let mut response = http_client - .get(url.as_ref(), Default::default(), true) - .await - .context("downloading extension")?; - - fs.remove_dir( - &extension_dir, - RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await?; - - let content_length = response - .headers() - .get(http_client::http::header::CONTENT_LENGTH) - .and_then(|value| value.to_str().ok()?.parse::().ok()); - - let mut body = BufReader::new(response.body_mut()); - let mut tar_gz_bytes = Vec::new(); - body.read_to_end(&mut tar_gz_bytes).await?; - - if let Some(content_length) = content_length { - let actual_len = tar_gz_bytes.len(); - if content_length != actual_len { - bail!(concat!( - "downloaded extension size {actual_len} ", - "does not match content length {content_length}" - )); - } - } - let decompressed_bytes = GzipDecoder::new(BufReader::new(tar_gz_bytes.as_slice())); - let archive = Archive::new(decompressed_bytes); - archive.unpack(extension_dir).await?; - this.update(cx, |this, cx| this.reload(Some(extension_id.clone()), cx))? - .await; - - 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) - { - events.update(cx, |this, cx| { - this.emit(extension::Event::ExtensionInstalled(manifest.clone()), cx) - }); - } - }) - .ok(); - } - - anyhow::Ok(()) - }) - } - - pub fn install_latest_extension(&mut self, extension_id: Arc, cx: &mut Context) { - log::info!("installing extension {extension_id} latest version"); - - let schema_versions = schema_version_range(); - let wasm_api_versions = wasm_api_version_range(ReleaseChannel::global(cx)); - - let Some(url) = self - .http_client - .build_zed_api_url( - &format!("/extensions/{extension_id}/download"), - &[ - ("min_schema_version", &schema_versions.start().to_string()), - ("max_schema_version", &schema_versions.end().to_string()), - ( - "min_wasm_api_version", - &wasm_api_versions.start().to_string(), - ), - ("max_wasm_api_version", &wasm_api_versions.end().to_string()), - ], - ) - .log_err() - else { - return; - }; - - self.install_or_upgrade_extension_at_endpoint( - extension_id, - url, - ExtensionOperation::Install, - cx, - ) - .detach_and_log_err(cx); - } - - pub fn upgrade_extension( - &mut self, - extension_id: Arc, - version: Arc, - cx: &mut Context, - ) -> Task> { - self.install_or_upgrade_extension(extension_id, version, ExtensionOperation::Upgrade, cx) - } - - fn install_or_upgrade_extension( - &mut self, - extension_id: Arc, - version: Arc, - operation: ExtensionOperation, - cx: &mut Context, - ) -> Task> { - log::info!("installing extension {extension_id} {version}"); - let Some(url) = self - .http_client - .build_zed_api_url( - &format!("/extensions/{extension_id}/{version}/download"), - &[], - ) - .log_err() - else { - return Task::ready(Ok(())); - }; - - self.install_or_upgrade_extension_at_endpoint(extension_id, url, operation, cx) - } - - pub fn uninstall_extension( - &mut self, - extension_id: Arc, - cx: &mut Context, - ) -> Task> { - let extension_dir = self.installed_dir.join(extension_id.as_ref()); - let work_dir = self.wasm_host.work_dir.join(extension_id.as_ref()); - let fs = self.fs.clone(); - - let extension_manifest = self.extension_manifest_for_id(&extension_id).cloned(); - - match self.outstanding_operations.entry(extension_id.clone()) { - btree_map::Entry::Occupied(_) => return Task::ready(Ok(())), - btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Remove), - }; - - cx.spawn(async move |extension_store, cx| { - let _finish = cx.on_drop(&extension_store, { - let extension_id = extension_id.clone(); - move |this, cx| { - this.outstanding_operations.remove(extension_id.as_ref()); - cx.notify(); - } - }); - - fs.remove_dir( - &extension_dir, - RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await - .with_context(|| format!("Removing extension dir {extension_dir:?}"))?; - - extension_store - .update(cx, |extension_store, cx| extension_store.reload(None, cx))? - .await; - - // There's a race between wasm extension fully stopping and the directory removal. - // On Windows, it's impossible to remove a directory that has a process running in it. - for i in 0..3 { - cx.background_executor() - .timer(Duration::from_millis(i * 100)) - .await; - let removal_result = fs - .remove_dir( - &work_dir, - RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await; - match removal_result { - Ok(()) => break, - Err(e) => { - if i == 2 { - log::error!("Failed to remove extension work dir {work_dir:?} : {e}"); - } - } - } - } - - 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) - }); - } - })?; - - anyhow::Ok(()) - }) - } - - pub fn install_dev_extension( - &mut self, - extension_source_path: PathBuf, - cx: &mut Context, - ) -> Task> { - let extensions_dir = self.extensions_dir(); - let fs = self.fs.clone(); - let builder = self.builder.clone(); - - cx.spawn(async move |this, cx| { - let mut extension_manifest = - ExtensionManifest::load(fs.clone(), &extension_source_path).await?; - let extension_id = extension_manifest.id.clone(); - - if let Some(uninstall_task) = this - .update(cx, |this, cx| { - this.extension_index - .extensions - .get(extension_id.as_ref()) - .is_some_and(|index_entry| !index_entry.dev) - .then(|| this.uninstall_extension(extension_id.clone(), cx)) - }) - .ok() - .flatten() - { - uninstall_task.await.log_err(); - } - - if !this.update(cx, |this, cx| { - match this.outstanding_operations.entry(extension_id.clone()) { - btree_map::Entry::Occupied(_) => return false, - btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Install), - }; - cx.notify(); - true - })? { - return Ok(()); - } - - let _finish = cx.on_drop(&this, { - let extension_id = extension_id.clone(); - move |this, cx| { - this.outstanding_operations.remove(extension_id.as_ref()); - cx.notify(); - } - }); - - cx.background_spawn({ - let extension_source_path = extension_source_path.clone(); - let fs = fs.clone(); - async move { - builder - .compile_extension( - &extension_source_path, - &mut extension_manifest, - CompileExtensionOptions { release: false }, - fs, - ) - .await - } - }) - .await - .inspect_err(|error| { - util::log_err(error); - })?; - - let output_path = &extensions_dir.join(extension_id.as_ref()); - if let Some(metadata) = fs.metadata(output_path).await? { - if metadata.is_symlink { - fs.remove_file( - output_path, - RemoveOptions { - recursive: false, - ignore_if_not_exists: true, - }, - ) - .await?; - } else { - bail!("extension {extension_id} is still installed"); - } - } - - fs.create_symlink(output_path, extension_source_path) - .await?; - - 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) - }); - } - })?; - - Ok(()) - }) - } - - pub fn rebuild_dev_extension(&mut self, extension_id: Arc, cx: &mut Context) { - let path = self.installed_dir.join(extension_id.as_ref()); - let builder = self.builder.clone(); - let fs = self.fs.clone(); - - match self.outstanding_operations.entry(extension_id.clone()) { - btree_map::Entry::Occupied(_) => return, - btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Upgrade), - }; - - cx.notify(); - let compile = cx.background_spawn(async move { - let mut manifest = ExtensionManifest::load(fs.clone(), &path).await?; - builder - .compile_extension( - &path, - &mut manifest, - CompileExtensionOptions { release: true }, - fs, - ) - .await - }); - - cx.spawn(async move |this, cx| { - let result = compile.await; - - this.update(cx, |this, cx| { - this.outstanding_operations.remove(&extension_id); - cx.notify(); - })?; - - if result.is_ok() { - this.update(cx, |this, cx| this.reload(Some(extension_id), cx))? - .await; - } - - result - }) - .detach_and_log_err(cx) - } - - /// Updates the set of installed extensions. - /// - /// First, this unloads any themes, languages, or grammars that are - /// no longer in the manifest, or whose files have changed on disk. - /// Then it loads any themes, languages, or grammars that are newly - /// added to the manifest, or whose files have changed on disk. - fn extensions_updated( - &mut self, - mut new_index: ExtensionIndex, - cx: &mut Context, - ) -> Task<()> { - let old_index = &self.extension_index; - - new_index - .extensions - .retain(|extension_id, _| !SUPPRESSED_EXTENSIONS.contains(&extension_id.as_ref())); - - // Determine which extensions need to be loaded and unloaded, based - // on the changes to the manifest and the extensions that we know have been - // modified. - let mut extensions_to_unload = Vec::default(); - let mut extensions_to_load = Vec::default(); - { - let mut old_keys = old_index.extensions.iter().peekable(); - let mut new_keys = new_index.extensions.iter().peekable(); - loop { - match (old_keys.peek(), new_keys.peek()) { - (None, None) => break, - (None, Some(_)) => { - extensions_to_load.push(new_keys.next().unwrap().0.clone()); - } - (Some(_), None) => { - extensions_to_unload.push(old_keys.next().unwrap().0.clone()); - } - (Some((old_key, _)), Some((new_key, _))) => match old_key.cmp(new_key) { - Ordering::Equal => { - let (old_key, old_value) = old_keys.next().unwrap(); - let (new_key, new_value) = new_keys.next().unwrap(); - if old_value != new_value || self.modified_extensions.contains(old_key) - { - extensions_to_unload.push(old_key.clone()); - extensions_to_load.push(new_key.clone()); - } - } - Ordering::Less => { - extensions_to_unload.push(old_keys.next().unwrap().0.clone()); - } - Ordering::Greater => { - extensions_to_load.push(new_keys.next().unwrap().0.clone()); - } - }, - } - } - self.modified_extensions.clear(); - } - - if extensions_to_load.is_empty() && extensions_to_unload.is_empty() { - self.reload_complete_senders.clear(); - return Task::ready(()); - } - - let reload_count = extensions_to_unload - .iter() - .filter(|id| extensions_to_load.contains(id)) - .count(); - - log::info!( - "extensions updated. loading {}, reloading {}, unloading {}", - extensions_to_load.len() - reload_count, - reload_count, - 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); - - let themes_to_remove = old_index - .themes - .iter() - .filter_map(|(name, entry)| { - if extensions_to_unload.contains(&entry.extension) { - Some(name.clone().into()) - } else { - None - } - }) - .collect::>(); - let icon_themes_to_remove = old_index - .icon_themes - .iter() - .filter_map(|(name, entry)| { - if extensions_to_unload.contains(&entry.extension) { - Some(name.clone().into()) - } else { - None - } - }) - .collect::>(); - let languages_to_remove = old_index - .languages - .iter() - .filter_map(|(name, entry)| { - if extensions_to_unload.contains(&entry.extension) { - Some(name.clone()) - } else { - None - } - }) - .collect::>(); - let mut grammars_to_remove = Vec::new(); - let mut server_removal_tasks = Vec::with_capacity(extensions_to_unload.len()); - for extension_id in &extensions_to_unload { - let Some(extension) = old_index.extensions.get(extension_id) else { - continue; - }; - grammars_to_remove.extend(extension.manifest.grammars.keys().cloned()); - for (language_server_name, config) in &extension.manifest.language_servers { - for language in config.languages() { - server_removal_tasks.push(self.proxy.remove_language_server( - &language, - language_server_name, - cx, - )); - } - } - - for server_id in extension.manifest.context_servers.keys() { - self.proxy.unregister_context_server(server_id.clone(), cx); - } - for adapter in extension.manifest.debug_adapters.keys() { - self.proxy.unregister_debug_adapter(adapter.clone()); - } - for locator in extension.manifest.debug_locators.keys() { - self.proxy.unregister_debug_locator(locator.clone()); - } - for command_name in extension.manifest.slash_commands.keys() { - self.proxy.unregister_slash_command(command_name.clone()); - } - } - - self.wasm_extensions - .retain(|(extension, _)| !extensions_to_unload.contains(&extension.id)); - self.proxy.remove_user_themes(themes_to_remove); - self.proxy.remove_icon_themes(icon_themes_to_remove); - self.proxy - .remove_languages(&languages_to_remove, &grammars_to_remove); - - let mut grammars_to_add = Vec::new(); - let mut themes_to_add = Vec::new(); - let mut icon_themes_to_add = Vec::new(); - let mut snippets_to_add = Vec::new(); - for extension_id in &extensions_to_load { - let Some(extension) = new_index.extensions.get(extension_id) else { - continue; - }; - - grammars_to_add.extend(extension.manifest.grammars.keys().map(|grammar_name| { - let mut grammar_path = self.installed_dir.clone(); - grammar_path.extend([extension_id.as_ref(), "grammars"]); - grammar_path.push(grammar_name.as_ref()); - grammar_path.set_extension("wasm"); - (grammar_name.clone(), grammar_path) - })); - themes_to_add.extend(extension.manifest.themes.iter().map(|theme_path| { - let mut path = self.installed_dir.clone(); - path.extend([Path::new(extension_id.as_ref()), theme_path.as_path()]); - path - })); - icon_themes_to_add.extend(extension.manifest.icon_themes.iter().map( - |icon_theme_path| { - let mut path = self.installed_dir.clone(); - path.extend([Path::new(extension_id.as_ref()), icon_theme_path.as_path()]); - - let mut icons_root_path = self.installed_dir.clone(); - icons_root_path.extend([Path::new(extension_id.as_ref())]); - - (path, icons_root_path) - }, - )); - snippets_to_add.extend(extension.manifest.snippets.iter().map(|snippets_path| { - let mut path = self.installed_dir.clone(); - path.extend([Path::new(extension_id.as_ref()), snippets_path.as_path()]); - path - })); - } - - self.proxy.register_grammars(grammars_to_add); - let languages_to_add = new_index - .languages - .iter() - .filter(|(_, entry)| extensions_to_load.contains(&entry.extension)) - .collect::>(); - for (language_name, language) in languages_to_add { - let mut language_path = self.installed_dir.clone(); - language_path.extend([ - Path::new(language.extension.as_ref()), - language.path.as_path(), - ]); - self.proxy.register_language( - language_name.clone(), - language.grammar.clone(), - language.matcher.clone(), - language.hidden, - Arc::new(move || { - let config = std::fs::read_to_string(language_path.join("config.toml"))?; - let config: LanguageConfig = ::toml::from_str(&config)?; - let queries = load_plugin_queries(&language_path); - let context_provider = - std::fs::read_to_string(language_path.join("tasks.json")) - .ok() - .and_then(|contents| { - let definitions = - serde_json_lenient::from_str(&contents).log_err()?; - Some(Arc::new(ContextProviderWithTasks::new(definitions)) as Arc<_>) - }); - - Ok(LoadedLanguage { - config, - queries, - context_provider, - toolchain_provider: None, - manifest_name: None, - }) - }), - ); - } - - let fs = self.fs.clone(); - let wasm_host = self.wasm_host.clone(); - let root_dir = self.installed_dir.clone(); - let proxy = self.proxy.clone(); - let extension_entries = extensions_to_load - .iter() - .filter_map(|name| new_index.extensions.get(name).cloned()) - .collect::>(); - self.extension_index = new_index; - cx.notify(); - cx.emit(Event::ExtensionsUpdated); - - cx.spawn(async move |this, cx| { - cx.background_spawn({ - let fs = fs.clone(); - async move { - let _ = join_all(server_removal_tasks).await; - for theme_path in themes_to_add { - proxy - .load_user_theme(theme_path, fs.clone()) - .await - .log_err(); - } - - for (icon_theme_path, icons_root_path) in icon_themes_to_add { - proxy - .load_icon_theme(icon_theme_path, icons_root_path, fs.clone()) - .await - .log_err(); - } - - for snippets_path in &snippets_to_add { - match fs - .load(snippets_path) - .await - .with_context(|| format!("Loading snippets from {snippets_path:?}")) - { - Ok(snippets_contents) => { - proxy - .register_snippet(snippets_path, &snippets_contents) - .log_err(); - } - Err(e) => log::error!("Cannot load snippets: {e:#}"), - } - } - } - }) - .await; - - let mut wasm_extensions = Vec::new(); - for extension in extension_entries { - if extension.manifest.lib.kind.is_none() { - continue; - }; - - let extension_path = root_dir.join(extension.manifest.id.as_ref()); - let wasm_extension = WasmExtension::load( - &extension_path, - &extension.manifest, - wasm_host.clone(), - cx, - ) - .await - .with_context(|| format!("Loading extension from {extension_path:?}")); - - match wasm_extension { - Ok(wasm_extension) => { - wasm_extensions.push((extension.manifest.clone(), wasm_extension)) - } - Err(e) => { - log::error!( - "Failed to load extension: {}, {:#}", - extension.manifest.id, - e - ); - this.update(cx, |_, cx| { - cx.emit(Event::ExtensionFailedToLoad(extension.manifest.id.clone())) - }) - .ok(); - } - } - } - - this.update(cx, |this, cx| { - this.reload_complete_senders.clear(); - - for (manifest, wasm_extension) in &wasm_extensions { - let extension = Arc::new(wasm_extension.clone()); - - for (language_server_id, language_server_config) in &manifest.language_servers { - for language in language_server_config.languages() { - this.proxy.register_language_server( - extension.clone(), - language_server_id.clone(), - language.clone(), - ); - } - } - - for (slash_command_name, slash_command) in &manifest.slash_commands { - this.proxy.register_slash_command( - extension.clone(), - extension::SlashCommand { - name: slash_command_name.to_string(), - description: slash_command.description.to_string(), - // We don't currently expose this as a configurable option, as it currently drives - // the `menu_text` on the `SlashCommand` trait, which is not used for slash commands - // defined in extensions, as they are not able to be added to the menu. - tooltip_text: String::new(), - requires_argument: slash_command.requires_argument, - }, - ); - } - - for id in manifest.context_servers.keys() { - this.proxy - .register_context_server(extension.clone(), id.clone(), cx); - } - - for (debug_adapter, meta) in &manifest.debug_adapters { - let mut path = root_dir.clone(); - path.push(Path::new(manifest.id.as_ref())); - if let Some(schema_path) = &meta.schema_path { - path.push(schema_path); - } else { - path.push("debug_adapter_schemas"); - path.push(Path::new(debug_adapter.as_ref()).with_extension("json")); - } - - this.proxy.register_debug_adapter( - extension.clone(), - debug_adapter.clone(), - &path, - ); - } - - for debug_adapter in manifest.debug_locators.keys() { - this.proxy - .register_debug_locator(extension.clone(), debug_adapter.clone()); - } - } - - this.wasm_extensions.extend(wasm_extensions); - this.proxy.set_extensions_loaded(); - this.proxy.reload_current_theme(cx); - this.proxy.reload_current_icon_theme(cx); - - if let Some(events) = ExtensionEvents::try_global(cx) { - events.update(cx, |this, cx| { - this.emit(extension::Event::ExtensionsInstalledChanged, cx) - }); - } - }) - .ok(); - }) - } - - fn rebuild_extension_index(&self, cx: &mut Context) -> Task { - let fs = self.fs.clone(); - let work_dir = self.wasm_host.work_dir.clone(); - let extensions_dir = self.installed_dir.clone(); - let index_path = self.index_path.clone(); - let proxy = self.proxy.clone(); - cx.background_spawn(async move { - let start_time = Instant::now(); - let mut index = ExtensionIndex::default(); - - fs.create_dir(&work_dir).await.log_err(); - fs.create_dir(&extensions_dir).await.log_err(); - - let extension_paths = fs.read_dir(&extensions_dir).await; - if let Ok(mut extension_paths) = extension_paths { - while let Some(extension_dir) = extension_paths.next().await { - let Ok(extension_dir) = extension_dir else { - continue; - }; - - if extension_dir - .file_name() - .is_some_and(|file_name| file_name == ".DS_Store") - { - continue; - } - - Self::add_extension_to_index( - fs.clone(), - extension_dir, - &mut index, - proxy.clone(), - ) - .await - .log_err(); - } - } - - if let Ok(index_json) = serde_json::to_string_pretty(&index) { - fs.save(&index_path, &index_json.as_str().into(), Default::default()) - .await - .context("failed to save extension index") - .log_err(); - } - - log::info!("rebuilt extension index in {:?}", start_time.elapsed()); - index - }) - } - - async fn add_extension_to_index( - fs: Arc, - extension_dir: PathBuf, - index: &mut ExtensionIndex, - proxy: Arc, - ) -> Result<()> { - let mut extension_manifest = ExtensionManifest::load(fs.clone(), &extension_dir).await?; - let extension_id = extension_manifest.id.clone(); - - if SUPPRESSED_EXTENSIONS.contains(&extension_id.as_ref()) { - return Ok(()); - } - - // TODO: distinguish dev extensions more explicitly, by the absence - // of a checksum file that we'll create when downloading normal extensions. - let is_dev = fs - .metadata(&extension_dir) - .await? - .context("directory does not exist")? - .is_symlink; - - if let Ok(mut language_paths) = fs.read_dir(&extension_dir.join("languages")).await { - while let Some(language_path) = language_paths.next().await { - let language_path = language_path?; - let Ok(relative_path) = language_path.strip_prefix(&extension_dir) else { - continue; - }; - let Ok(Some(fs_metadata)) = fs.metadata(&language_path).await else { - continue; - }; - if !fs_metadata.is_dir { - continue; - } - let config = fs.load(&language_path.join("config.toml")).await?; - let config = ::toml::from_str::(&config)?; - - let relative_path = relative_path.to_path_buf(); - if !extension_manifest.languages.contains(&relative_path) { - extension_manifest.languages.push(relative_path.clone()); - } - - index.languages.insert( - config.name.clone(), - ExtensionIndexLanguageEntry { - extension: extension_id.clone(), - path: relative_path, - matcher: config.matcher, - hidden: config.hidden, - grammar: config.grammar, - }, - ); - } - } - - if let Ok(mut theme_paths) = fs.read_dir(&extension_dir.join("themes")).await { - while let Some(theme_path) = theme_paths.next().await { - let theme_path = theme_path?; - let Ok(relative_path) = theme_path.strip_prefix(&extension_dir) else { - continue; - }; - - let Some(theme_families) = proxy - .list_theme_names(theme_path.clone(), fs.clone()) - .await - .log_err() - else { - continue; - }; - - let relative_path = relative_path.to_path_buf(); - if !extension_manifest.themes.contains(&relative_path) { - extension_manifest.themes.push(relative_path.clone()); - } - - for theme_name in theme_families { - index.themes.insert( - theme_name.into(), - ExtensionIndexThemeEntry { - extension: extension_id.clone(), - path: relative_path.clone(), - }, - ); - } - } - } - - if let Ok(mut icon_theme_paths) = fs.read_dir(&extension_dir.join("icon_themes")).await { - while let Some(icon_theme_path) = icon_theme_paths.next().await { - let icon_theme_path = icon_theme_path?; - let Ok(relative_path) = icon_theme_path.strip_prefix(&extension_dir) else { - continue; - }; - - let Some(icon_theme_families) = proxy - .list_icon_theme_names(icon_theme_path.clone(), fs.clone()) - .await - .log_err() - else { - continue; - }; - - let relative_path = relative_path.to_path_buf(); - if !extension_manifest.icon_themes.contains(&relative_path) { - extension_manifest.icon_themes.push(relative_path.clone()); - } - - for icon_theme_name in icon_theme_families { - index.icon_themes.insert( - icon_theme_name.into(), - ExtensionIndexIconThemeEntry { - extension: extension_id.clone(), - path: relative_path.clone(), - }, - ); - } - } - } - - let extension_wasm_path = extension_dir.join("extension.wasm"); - if fs.is_file(&extension_wasm_path).await { - extension_manifest - .lib - .kind - .get_or_insert(ExtensionLibraryKind::Rust); - } - - index.extensions.insert( - extension_id.clone(), - ExtensionIndexEntry { - dev: is_dev, - manifest: Arc::new(extension_manifest), - }, - ); - - Ok(()) - } - - fn prepare_remote_extension( - &mut self, - extension_id: Arc, - is_dev: bool, - tmp_dir: PathBuf, - cx: &mut Context, - ) -> Task> { - let src_dir = self.extensions_dir().join(extension_id.as_ref()); - let Some(loaded_extension) = self.extension_index.extensions.get(&extension_id).cloned() - else { - return Task::ready(Err(anyhow!("extension no longer installed"))); - }; - let fs = self.fs.clone(); - cx.background_spawn(async move { - const EXTENSION_TOML: &str = "extension.toml"; - const EXTENSION_WASM: &str = "extension.wasm"; - const CONFIG_TOML: &str = "config.toml"; - - if is_dev { - let manifest_toml = toml::to_string(&loaded_extension.manifest)?; - fs.save( - &tmp_dir.join(EXTENSION_TOML), - &Rope::from(manifest_toml), - language::LineEnding::Unix, - ) - .await?; - } else { - fs.copy_file( - &src_dir.join(EXTENSION_TOML), - &tmp_dir.join(EXTENSION_TOML), - fs::CopyOptions::default(), - ) - .await? - } - - if fs.is_file(&src_dir.join(EXTENSION_WASM)).await { - fs.copy_file( - &src_dir.join(EXTENSION_WASM), - &tmp_dir.join(EXTENSION_WASM), - fs::CopyOptions::default(), - ) - .await? - } - - for language_path in loaded_extension.manifest.languages.iter() { - if fs - .is_file(&src_dir.join(language_path).join(CONFIG_TOML)) - .await - { - fs.create_dir(&tmp_dir.join(language_path)).await?; - fs.copy_file( - &src_dir.join(language_path).join(CONFIG_TOML), - &tmp_dir.join(language_path).join(CONFIG_TOML), - fs::CopyOptions::default(), - ) - .await? - } - } - - for (adapter_name, meta) in loaded_extension.manifest.debug_adapters.iter() { - 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? - } - fs.copy_file( - &src_dir.join(schema_path), - &tmp_dir.join(schema_path), - fs::CopyOptions::default(), - ) - .await? - } - } - - Ok(()) - }) - } - - async fn sync_extensions_to_remotes( - this: &WeakEntity, - client: WeakEntity, - cx: &mut AsyncApp, - ) -> Result<()> { - let extensions = this.update(cx, |this, _cx| { - this.extension_index - .extensions - .iter() - .filter_map(|(id, entry)| { - if !entry.manifest.allow_remote_load() { - return None; - } - Some(proto::Extension { - id: id.to_string(), - version: entry.manifest.version.to_string(), - dev: entry.dev, - }) - }) - .collect() - })?; - - let response = client - .update(cx, |client, _cx| { - client - .proto_client() - .request(proto::SyncExtensions { extensions }) - })? - .await?; - let path_style = client.read_with(cx, |client, _| client.path_style())?; - - for missing_extension in response.missing_extensions.into_iter() { - let tmp_dir = tempfile::tempdir()?; - this.update(cx, |this, cx| { - this.prepare_remote_extension( - missing_extension.id.clone().into(), - missing_extension.dev, - tmp_dir.path().to_owned(), - cx, - ) - })? - .await?; - let dest_dir = RemotePathBuf::new( - path_style - .join(&response.tmp_dir, &missing_extension.id) - .with_context(|| { - format!( - "failed to construct destination path: {:?}, {:?}", - response.tmp_dir, missing_extension.id, - ) - })?, - path_style, - ); - log::info!( - "Uploading extension {} to {:?}", - missing_extension.clone().id, - dest_dir - ); - - client - .update(cx, |client, cx| { - client.upload_directory(tmp_dir.path().to_owned(), dest_dir.clone(), cx) - })? - .await?; - - log::info!( - "Finished uploading extension {}", - missing_extension.clone().id - ); - - let result = client - .update(cx, |client, _cx| { - client.proto_client().request(proto::InstallExtension { - tmp_dir: dest_dir.to_proto(), - extension: Some(missing_extension.clone()), - }) - })? - .await; - - if let Err(e) = result { - log::error!( - "Failed to install extension {}: {}", - missing_extension.id, - e - ); - } - } - - anyhow::Ok(()) - } - - pub async fn update_remote_clients(this: &WeakEntity, cx: &mut AsyncApp) -> Result<()> { - let clients = this.update(cx, |this, _cx| { - this.remote_clients.retain(|v| v.upgrade().is_some()); - this.remote_clients.clone() - })?; - - for client in clients { - Self::sync_extensions_to_remotes(this, client, cx) - .await - .log_err(); - } - - anyhow::Ok(()) - } - - pub fn register_remote_client( - &mut self, - client: Entity, - _cx: &mut Context, - ) { - self.remote_clients.push(client.downgrade()); - self.ssh_registered_tx.unbounded_send(()).ok(); - } -} - -fn load_plugin_queries(root_path: &Path) -> LanguageQueries { - let mut result = LanguageQueries::default(); - if let Some(entries) = std::fs::read_dir(root_path).log_err() { - for entry in entries { - let Some(entry) = entry.log_err() else { - continue; - }; - let path = entry.path(); - if let Some(remainder) = path.strip_prefix(root_path).ok().and_then(|p| p.to_str()) { - if !remainder.ends_with(".scm") { - continue; - } - for (name, query) in QUERY_FILENAME_PREFIXES { - if remainder.starts_with(name) { - if let Some(contents) = std::fs::read_to_string(&path).log_err() { - match query(&mut result) { - None => *query(&mut result) = Some(contents.into()), - Some(r) => r.to_mut().push_str(contents.as_ref()), - } - } - break; - } - } - } - } - } - result -} diff --git a/crates/extension_host/src/extension_settings.rs b/crates/extension_host/src/extension_settings.rs deleted file mode 100644 index 736dd6b87a..0000000000 --- a/crates/extension_host/src/extension_settings.rs +++ /dev/null @@ -1,65 +0,0 @@ -use collections::HashMap; -use extension::{ - DownloadFileCapability, ExtensionCapability, NpmInstallPackageCapability, ProcessExecCapability, -}; -use settings::{RegisterSetting, Settings}; -use std::sync::Arc; - -#[derive(Debug, Default, Clone, RegisterSetting)] -pub struct ExtensionSettings { - /// The extensions that should be automatically installed by Zed. - /// - /// This is used to make functionality provided by extensions (e.g., language support) - /// available out-of-the-box. - /// - /// Default: { "html": true } - pub auto_install_extensions: HashMap, bool>, - pub auto_update_extensions: HashMap, bool>, - pub granted_capabilities: Vec, -} - -impl ExtensionSettings { - /// Returns whether the given extension should be auto-installed. - pub fn should_auto_install(&self, extension_id: &str) -> bool { - self.auto_install_extensions - .get(extension_id) - .copied() - .unwrap_or(true) - } - - pub fn should_auto_update(&self, extension_id: &str) -> bool { - self.auto_update_extensions - .get(extension_id) - .copied() - .unwrap_or(true) - } -} - -impl Settings for ExtensionSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - Self { - auto_install_extensions: content.extension.auto_install_extensions.clone(), - auto_update_extensions: content.extension.auto_update_extensions.clone(), - granted_capabilities: content - .extension - .granted_extension_capabilities - .clone() - .unwrap_or_default() - .into_iter() - .map(|capability| match capability { - settings::ExtensionCapabilityContent::ProcessExec { command, args } => { - ExtensionCapability::ProcessExec(ProcessExecCapability { command, args }) - } - settings::ExtensionCapabilityContent::DownloadFile { host, path } => { - ExtensionCapability::DownloadFile(DownloadFileCapability { host, path }) - } - settings::ExtensionCapabilityContent::NpmInstallPackage { package } => { - ExtensionCapability::NpmInstallPackage(NpmInstallPackageCapability { - package, - }) - } - }) - .collect(), - } - } -} diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs deleted file mode 100644 index 54b090347f..0000000000 --- a/crates/extension_host/src/extension_store_test.rs +++ /dev/null @@ -1,874 +0,0 @@ -use crate::{ - Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry, - ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry, - RELOAD_DEBOUNCE_DURATION, SchemaVersion, -}; -use async_compression::futures::bufread::GzipEncoder; -use collections::{BTreeMap, HashSet}; -use extension::ExtensionHostProxy; -use fs::{FakeFs, Fs, RealFs}; -use futures::{AsyncReadExt, StreamExt, io::BufReader}; -use gpui::{AppContext as _, TestAppContext}; -use http_client::{FakeHttpClient, Response}; -use language::{BinaryStatus, LanguageMatcher, LanguageName, LanguageRegistry}; -use language_extension::LspAccess; -use lsp::LanguageServerName; -use node_runtime::NodeRuntime; -use parking_lot::Mutex; -use project::{DEFAULT_COMPLETION_CONTEXT, Project}; -use release_channel::AppVersion; -use reqwest_client::ReqwestClient; -use serde_json::json; -use settings::SettingsStore; -use std::{ - ffi::OsString, - path::{Path, PathBuf}, - sync::Arc, -}; -use theme::ThemeRegistry; -use util::test::TempTree; - -#[cfg(test)] -#[ctor::ctor] -fn init_logger() { - zlog::init_test(); -} - -#[gpui::test] -async fn test_extension_store(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let http_client = FakeHttpClient::with_200_response(); - - fs.insert_tree( - "/the-extension-dir", - json!({ - "installed": { - "zed-monokai": { - "extension.json": r#"{ - "id": "zed-monokai", - "name": "Zed Monokai", - "version": "2.0.0", - "themes": { - "Monokai Dark": "themes/monokai.json", - "Monokai Light": "themes/monokai.json", - "Monokai Pro Dark": "themes/monokai-pro.json", - "Monokai Pro Light": "themes/monokai-pro.json" - } - }"#, - "themes": { - "monokai.json": r#"{ - "name": "Monokai", - "author": "Someone", - "themes": [ - { - "name": "Monokai Dark", - "appearance": "dark", - "style": {} - }, - { - "name": "Monokai Light", - "appearance": "light", - "style": {} - } - ] - }"#, - "monokai-pro.json": r#"{ - "name": "Monokai Pro", - "author": "Someone", - "themes": [ - { - "name": "Monokai Pro Dark", - "appearance": "dark", - "style": {} - }, - { - "name": "Monokai Pro Light", - "appearance": "light", - "style": {} - } - ] - }"#, - } - }, - "zed-ruby": { - "extension.json": r#"{ - "id": "zed-ruby", - "name": "Zed Ruby", - "version": "1.0.0", - "grammars": { - "ruby": "grammars/ruby.wasm", - "embedded_template": "grammars/embedded_template.wasm" - }, - "languages": { - "ruby": "languages/ruby", - "erb": "languages/erb" - } - }"#, - "grammars": { - "ruby.wasm": "", - "embedded_template.wasm": "", - }, - "languages": { - "ruby": { - "config.toml": r#" - name = "Ruby" - grammar = "ruby" - path_suffixes = ["rb"] - "#, - "highlights.scm": "", - }, - "erb": { - "config.toml": r#" - name = "ERB" - grammar = "embedded_template" - path_suffixes = ["erb"] - "#, - "highlights.scm": "", - } - }, - } - } - }), - ) - .await; - - let mut expected_index = ExtensionIndex { - extensions: [ - ( - "zed-ruby".into(), - ExtensionIndexEntry { - manifest: Arc::new(ExtensionManifest { - id: "zed-ruby".into(), - name: "Zed Ruby".into(), - version: "1.0.0".into(), - schema_version: SchemaVersion::ZERO, - description: None, - authors: Vec::new(), - repository: None, - themes: Default::default(), - icon_themes: Vec::new(), - lib: Default::default(), - languages: vec!["languages/erb".into(), "languages/ruby".into()], - grammars: [ - ("embedded_template".into(), GrammarManifestEntry::default()), - ("ruby".into(), GrammarManifestEntry::default()), - ] - .into_iter() - .collect(), - language_servers: BTreeMap::default(), - context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), - slash_commands: BTreeMap::default(), - snippets: None, - capabilities: Vec::new(), - debug_adapters: Default::default(), - debug_locators: Default::default(), - }), - dev: false, - }, - ), - ( - "zed-monokai".into(), - ExtensionIndexEntry { - manifest: Arc::new(ExtensionManifest { - id: "zed-monokai".into(), - name: "Zed Monokai".into(), - version: "2.0.0".into(), - schema_version: SchemaVersion::ZERO, - description: None, - authors: vec![], - repository: None, - themes: vec![ - "themes/monokai-pro.json".into(), - "themes/monokai.json".into(), - ], - icon_themes: Vec::new(), - lib: Default::default(), - languages: Default::default(), - grammars: BTreeMap::default(), - language_servers: BTreeMap::default(), - context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), - slash_commands: BTreeMap::default(), - snippets: None, - capabilities: Vec::new(), - debug_adapters: Default::default(), - debug_locators: Default::default(), - }), - dev: false, - }, - ), - ] - .into_iter() - .collect(), - languages: [ - ( - "ERB".into(), - ExtensionIndexLanguageEntry { - extension: "zed-ruby".into(), - path: "languages/erb".into(), - grammar: Some("embedded_template".into()), - hidden: false, - matcher: LanguageMatcher { - path_suffixes: vec!["erb".into()], - first_line_pattern: None, - }, - }, - ), - ( - "Ruby".into(), - ExtensionIndexLanguageEntry { - extension: "zed-ruby".into(), - path: "languages/ruby".into(), - grammar: Some("ruby".into()), - hidden: false, - matcher: LanguageMatcher { - path_suffixes: vec!["rb".into()], - first_line_pattern: None, - }, - }, - ), - ] - .into_iter() - .collect(), - themes: [ - ( - "Monokai Dark".into(), - ExtensionIndexThemeEntry { - extension: "zed-monokai".into(), - path: "themes/monokai.json".into(), - }, - ), - ( - "Monokai Light".into(), - ExtensionIndexThemeEntry { - extension: "zed-monokai".into(), - path: "themes/monokai.json".into(), - }, - ), - ( - "Monokai Pro Dark".into(), - ExtensionIndexThemeEntry { - extension: "zed-monokai".into(), - path: "themes/monokai-pro.json".into(), - }, - ), - ( - "Monokai Pro Light".into(), - ExtensionIndexThemeEntry { - extension: "zed-monokai".into(), - path: "themes/monokai-pro.json".into(), - }, - ), - ] - .into_iter() - .collect(), - icon_themes: BTreeMap::default(), - }; - - let proxy = Arc::new(ExtensionHostProxy::new()); - let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); - theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor()); - let language_registry = Arc::new(LanguageRegistry::test(cx.executor())); - language_extension::init(LspAccess::Noop, proxy.clone(), language_registry.clone()); - let node_runtime = NodeRuntime::unavailable(); - - let store = cx.new(|cx| { - ExtensionStore::new( - PathBuf::from("/the-extension-dir"), - None, - proxy.clone(), - fs.clone(), - http_client.clone(), - http_client.clone(), - None, - node_runtime.clone(), - cx, - ) - }); - - cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION); - store.read_with(cx, |store, _| { - let index = &store.extension_index; - assert_eq!(index.extensions, expected_index.extensions); - - for ((actual_key, actual_language), (expected_key, expected_language)) in - index.languages.iter().zip(expected_index.languages.iter()) - { - assert_eq!(actual_key, expected_key); - assert_eq!(actual_language.grammar, expected_language.grammar); - assert_eq!(actual_language.matcher, expected_language.matcher); - assert_eq!(actual_language.hidden, expected_language.hidden); - } - assert_eq!(index.themes, expected_index.themes); - - assert_eq!( - language_registry.language_names(), - [ - LanguageName::new_static("ERB"), - LanguageName::new_static("Plain Text"), - LanguageName::new_static("Ruby"), - ] - ); - assert_eq!( - theme_registry.list_names(), - [ - "Monokai Dark", - "Monokai Light", - "Monokai Pro Dark", - "Monokai Pro Light", - "One Dark", - ] - ); - }); - - fs.insert_tree( - "/the-extension-dir/installed/zed-gruvbox", - json!({ - "extension.json": r#"{ - "id": "zed-gruvbox", - "name": "Zed Gruvbox", - "version": "1.0.0", - "themes": { - "Gruvbox": "themes/gruvbox.json" - } - }"#, - "themes": { - "gruvbox.json": r#"{ - "name": "Gruvbox", - "author": "Someone Else", - "themes": [ - { - "name": "Gruvbox", - "appearance": "dark", - "style": {} - } - ] - }"#, - } - }), - ) - .await; - - expected_index.extensions.insert( - "zed-gruvbox".into(), - ExtensionIndexEntry { - manifest: Arc::new(ExtensionManifest { - id: "zed-gruvbox".into(), - name: "Zed Gruvbox".into(), - version: "1.0.0".into(), - schema_version: SchemaVersion::ZERO, - description: None, - authors: vec![], - repository: None, - themes: vec!["themes/gruvbox.json".into()], - icon_themes: Vec::new(), - lib: Default::default(), - languages: Default::default(), - grammars: BTreeMap::default(), - language_servers: BTreeMap::default(), - context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), - slash_commands: BTreeMap::default(), - snippets: None, - capabilities: Vec::new(), - debug_adapters: Default::default(), - debug_locators: Default::default(), - }), - dev: false, - }, - ); - expected_index.themes.insert( - "Gruvbox".into(), - ExtensionIndexThemeEntry { - extension: "zed-gruvbox".into(), - path: "themes/gruvbox.json".into(), - }, - ); - - #[allow(clippy::let_underscore_future)] - let _ = store.update(cx, |store, cx| store.reload(None, cx)); - - cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION); - store.read_with(cx, |store, _| { - let index = &store.extension_index; - - for ((actual_key, actual_language), (expected_key, expected_language)) in - index.languages.iter().zip(expected_index.languages.iter()) - { - assert_eq!(actual_key, expected_key); - assert_eq!(actual_language.grammar, expected_language.grammar); - assert_eq!(actual_language.matcher, expected_language.matcher); - assert_eq!(actual_language.hidden, expected_language.hidden); - } - - assert_eq!(index.extensions, expected_index.extensions); - assert_eq!(index.themes, expected_index.themes); - - assert_eq!( - theme_registry.list_names(), - [ - "Gruvbox", - "Monokai Dark", - "Monokai Light", - "Monokai Pro Dark", - "Monokai Pro Light", - "One Dark", - ] - ); - }); - - let prev_fs_metadata_call_count = fs.metadata_call_count(); - let prev_fs_read_dir_call_count = fs.read_dir_call_count(); - - // Create new extension store, as if Zed were restarting. - drop(store); - let store = cx.new(|cx| { - ExtensionStore::new( - PathBuf::from("/the-extension-dir"), - None, - proxy, - fs.clone(), - http_client.clone(), - http_client.clone(), - None, - node_runtime.clone(), - cx, - ) - }); - - cx.executor().run_until_parked(); - store.read_with(cx, |store, _| { - assert_eq!(store.extension_index.extensions, expected_index.extensions); - assert_eq!(store.extension_index.themes, expected_index.themes); - assert_eq!( - store.extension_index.icon_themes, - expected_index.icon_themes - ); - - for ((actual_key, actual_language), (expected_key, expected_language)) in store - .extension_index - .languages - .iter() - .zip(expected_index.languages.iter()) - { - assert_eq!(actual_key, expected_key); - assert_eq!(actual_language.grammar, expected_language.grammar); - assert_eq!(actual_language.matcher, expected_language.matcher); - assert_eq!(actual_language.hidden, expected_language.hidden); - } - - assert_eq!( - language_registry.language_names(), - [ - LanguageName::new_static("ERB"), - LanguageName::new_static("Plain Text"), - LanguageName::new_static("Ruby"), - ] - ); - assert_eq!( - language_registry.grammar_names(), - ["embedded_template".into(), "ruby".into()] - ); - assert_eq!( - theme_registry.list_names(), - [ - "Gruvbox", - "Monokai Dark", - "Monokai Light", - "Monokai Pro Dark", - "Monokai Pro Light", - "One Dark", - ] - ); - - // The on-disk manifest limits the number of FS calls that need to be made - // on startup. - assert_eq!(fs.read_dir_call_count(), prev_fs_read_dir_call_count); - assert_eq!(fs.metadata_call_count(), prev_fs_metadata_call_count + 2); - }); - - store.update(cx, |store, cx| { - store - .uninstall_extension("zed-ruby".into(), cx) - .detach_and_log_err(cx); - }); - - cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION); - expected_index.extensions.remove("zed-ruby"); - expected_index.languages.remove("Ruby"); - expected_index.languages.remove("ERB"); - - store.read_with(cx, |store, _| { - assert_eq!(store.extension_index.extensions, expected_index.extensions); - assert_eq!(store.extension_index.themes, expected_index.themes); - assert_eq!( - store.extension_index.icon_themes, - expected_index.icon_themes - ); - - for ((actual_key, actual_language), (expected_key, expected_language)) in store - .extension_index - .languages - .iter() - .zip(expected_index.languages.iter()) - { - assert_eq!(actual_key, expected_key); - assert_eq!(actual_language.grammar, expected_language.grammar); - assert_eq!(actual_language.matcher, expected_language.matcher); - assert_eq!(actual_language.hidden, expected_language.hidden); - } - - assert_eq!( - language_registry.language_names(), - [LanguageName::new_static("Plain Text")] - ); - assert_eq!(language_registry.grammar_names(), []); - }); -} - -#[gpui::test] -async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { - log::info!("Initializing test"); - init_test(cx); - cx.executor().allow_parking(); - - let root_dir = Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .parent() - .unwrap(); - let cache_dir = root_dir.join("target"); - let test_extension_id = "test-extension"; - let test_extension_dir = root_dir.join("extensions").join(test_extension_id); - - let fs = Arc::new(RealFs::new(None, cx.executor())); - let extensions_tree = TempTree::new(json!({ - "installed": {}, - "work": {} - })); - let project_dir = TempTree::new(json!({ - "test.gleam": "" - })); - - let extensions_dir = extensions_tree.path().canonicalize().unwrap(); - let project_dir = project_dir.path().canonicalize().unwrap(); - - log::info!("Setting up test"); - - let project = Project::test(fs.clone(), [project_dir.as_path()], cx).await; - - let proxy = Arc::new(ExtensionHostProxy::new()); - let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); - theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor()); - let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - language_extension::init( - LspAccess::ViaLspStore(project.update(cx, |project, _| project.lsp_store())), - proxy.clone(), - language_registry.clone(), - ); - let node_runtime = NodeRuntime::unavailable(); - - let mut status_updates = language_registry.language_server_binary_statuses(); - - struct FakeLanguageServerVersion { - version: String, - binary_contents: String, - http_request_count: usize, - } - - let language_server_version = Arc::new(Mutex::new(FakeLanguageServerVersion { - version: "v1.2.3".into(), - binary_contents: "the-binary-contents".into(), - http_request_count: 0, - })); - - let extension_client = FakeHttpClient::create({ - let language_server_version = language_server_version.clone(); - move |request| { - let language_server_version = language_server_version.clone(); - async move { - let version = language_server_version.lock().version.clone(); - let binary_contents = language_server_version.lock().binary_contents.clone(); - - let github_releases_uri = "https://api.github.com/repos/gleam-lang/gleam/releases"; - let asset_download_uri = - format!("https://fake-download.example.com/gleam-{version}"); - - let uri = request.uri().to_string(); - if uri == github_releases_uri { - language_server_version.lock().http_request_count += 1; - Ok(Response::new( - json!([ - { - "tag_name": version, - "prerelease": false, - "tarball_url": "", - "zipball_url": "", - "assets": [ - { - "name": format!("gleam-{version}-aarch64-apple-darwin.tar.gz"), - "browser_download_url": asset_download_uri - }, - { - "name": format!("gleam-{version}-x86_64-unknown-linux-musl.tar.gz"), - "browser_download_url": asset_download_uri - }, - { - "name": format!("gleam-{version}-aarch64-unknown-linux-musl.tar.gz"), - "browser_download_url": asset_download_uri - }, - { - "name": format!("gleam-{version}-x86_64-pc-windows-msvc.tar.gz"), - "browser_download_url": asset_download_uri - } - ] - } - ]) - .to_string() - .into(), - )) - } else if uri == asset_download_uri { - language_server_version.lock().http_request_count += 1; - let mut bytes = Vec::::new(); - let mut archive = async_tar::Builder::new(&mut bytes); - let mut header = async_tar::Header::new_gnu(); - header.set_size(binary_contents.len() as u64); - archive - .append_data(&mut header, "gleam", binary_contents.as_bytes()) - .await - .unwrap(); - archive.into_inner().await.unwrap(); - let mut gzipped_bytes = Vec::new(); - let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice())); - encoder.read_to_end(&mut gzipped_bytes).await.unwrap(); - Ok(Response::new(gzipped_bytes.into())) - } else { - Ok(Response::builder().status(404).body("not found".into())?) - } - } - } - }); - let user_agent = cx.update(|cx| { - format!( - "Zed/{} ({}; {})", - AppVersion::global(cx), - std::env::consts::OS, - std::env::consts::ARCH - ) - }); - let builder_client = - Arc::new(ReqwestClient::user_agent(&user_agent).expect("Could not create HTTP client")); - - let extension_store = cx.new(|cx| { - ExtensionStore::new( - extensions_dir.clone(), - Some(cache_dir), - proxy, - fs.clone(), - extension_client.clone(), - builder_client, - None, - node_runtime, - cx, - ) - }); - - log::info!("Flushing events"); - - // Ensure that debounces fire. - let mut events = cx.events(&extension_store); - let executor = cx.executor(); - let _task = cx.executor().spawn(async move { - while let Some(event) = events.next().await { - if let Event::StartedReloading = event { - executor.advance_clock(RELOAD_DEBOUNCE_DURATION); - } - } - }); - - extension_store.update(cx, |_, cx| { - cx.subscribe(&extension_store, |_, _, event, _| { - if matches!(event, Event::ExtensionFailedToLoad(_)) { - panic!("extension failed to load"); - } - }) - .detach(); - }); - - extension_store - .update(cx, |store, cx| { - store.install_dev_extension(test_extension_dir.clone(), cx) - }) - .await - .unwrap(); - - let mut fake_servers = language_registry.register_fake_lsp_server( - LanguageServerName("gleam".into()), - lsp::ServerCapabilities { - completion_provider: Some(Default::default()), - ..Default::default() - }, - None, - ); - - let (buffer, _handle) = project - .update(cx, |project, cx| { - project.open_local_buffer_with_lsp(project_dir.join("test.gleam"), cx) - }) - .await - .unwrap(); - - let fake_server = fake_servers.next().await.unwrap(); - let work_dir = extensions_dir.join(format!("work/{test_extension_id}")); - let expected_server_path = work_dir.join("gleam-v1.2.3/gleam"); - let expected_binary_contents = language_server_version.lock().binary_contents.clone(); - - // check that IO operations in extension work correctly - assert!(work_dir.join("dir-created-with-rel-path").exists()); - assert!(work_dir.join("dir-created-with-abs-path").exists()); - assert!(work_dir.join("file-created-with-abs-path").exists()); - assert!(work_dir.join("file-created-with-rel-path").exists()); - - assert_eq!(fake_server.binary.path, expected_server_path); - assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]); - assert_eq!( - fs.load(&expected_server_path).await.unwrap(), - expected_binary_contents - ); - assert_eq!(language_server_version.lock().http_request_count, 2); - assert_eq!( - [ - status_updates.next().await.unwrap(), - status_updates.next().await.unwrap(), - status_updates.next().await.unwrap(), - status_updates.next().await.unwrap(), - ], - [ - ( - LanguageServerName::new_static("gleam"), - BinaryStatus::Starting - ), - ( - LanguageServerName::new_static("gleam"), - BinaryStatus::CheckingForUpdate - ), - ( - LanguageServerName::new_static("gleam"), - BinaryStatus::Downloading - ), - (LanguageServerName::new_static("gleam"), BinaryStatus::None) - ] - ); - - // The extension creates custom labels for completion items. - fake_server.set_request_handler::(|_, _| async move { - Ok(Some(lsp::CompletionResponse::Array(vec![ - lsp::CompletionItem { - label: "foo".into(), - kind: Some(lsp::CompletionItemKind::FUNCTION), - detail: Some("fn() -> Result(Nil, Error)".into()), - ..Default::default() - }, - lsp::CompletionItem { - label: "bar.baz".into(), - kind: Some(lsp::CompletionItemKind::FUNCTION), - detail: Some("fn(List(a)) -> a".into()), - ..Default::default() - }, - lsp::CompletionItem { - label: "Quux".into(), - kind: Some(lsp::CompletionItemKind::CONSTRUCTOR), - detail: Some("fn(String) -> T".into()), - ..Default::default() - }, - lsp::CompletionItem { - label: "my_string".into(), - kind: Some(lsp::CompletionItemKind::CONSTANT), - detail: Some("String".into()), - ..Default::default() - }, - ]))) - }); - - let completion_labels = project - .update(cx, |project, cx| { - project.completions(&buffer, 0, DEFAULT_COMPLETION_CONTEXT, cx) - }) - .await - .unwrap() - .into_iter() - .flat_map(|response| response.completions) - .map(|c| c.label.text) - .collect::>(); - assert_eq!( - completion_labels, - [ - "foo: fn() -> Result(Nil, Error)".to_string(), - "bar.baz: fn(List(a)) -> a".to_string(), - "Quux: fn(String) -> T".to_string(), - "my_string: String".to_string(), - ] - ); - - // Simulate a new version of the language server being released - language_server_version.lock().version = "v2.0.0".into(); - language_server_version.lock().binary_contents = "the-new-binary-contents".into(); - language_server_version.lock().http_request_count = 0; - - // Start a new instance of the language server. - project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx) - }); - cx.executor().run_until_parked(); - - // The extension has cached the binary path, and does not attempt - // to reinstall it. - let fake_server = fake_servers.next().await.unwrap(); - assert_eq!(fake_server.binary.path, expected_server_path); - assert_eq!( - fs.load(&expected_server_path).await.unwrap(), - expected_binary_contents - ); - assert_eq!(language_server_version.lock().http_request_count, 0); - - // Reload the extension, clearing its cache. - // Start a new instance of the language server. - extension_store - .update(cx, |store, cx| { - store.reload(Some("test-extension".into()), cx) - }) - .await; - cx.executor().run_until_parked(); - project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx) - }); - - // The extension re-fetches the latest version of the language server. - let fake_server = fake_servers.next().await.unwrap(); - let new_expected_server_path = - extensions_dir.join(format!("work/{test_extension_id}/gleam-v2.0.0/gleam")); - let expected_binary_contents = language_server_version.lock().binary_contents.clone(); - assert_eq!(fake_server.binary.path, new_expected_server_path); - assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]); - assert_eq!( - fs.load(&new_expected_server_path).await.unwrap(), - expected_binary_contents - ); - - // The old language server directory has been cleaned up. - assert!(fs.metadata(&expected_server_path).await.unwrap().is_none()); -} - -fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let store = SettingsStore::test(cx); - cx.set_global(store); - release_channel::init(semver::Version::new(0, 0, 0), cx); - extension::init(cx); - theme::init(theme::LoadThemes::JustBase, cx); - gpui_tokio::init(cx); - }); -} diff --git a/crates/extension_host/src/headless_host.rs b/crates/extension_host/src/headless_host.rs deleted file mode 100644 index c3a290a55a..0000000000 --- a/crates/extension_host/src/headless_host.rs +++ /dev/null @@ -1,351 +0,0 @@ -use std::{path::PathBuf, sync::Arc}; - -use anyhow::{Context as _, Result}; -use client::{TypedEnvelope, proto}; -use collections::{HashMap, HashSet}; -use extension::{ - Extension, ExtensionDebugAdapterProviderProxy, ExtensionHostProxy, ExtensionLanguageProxy, - ExtensionLanguageServerProxy, ExtensionManifest, -}; -use fs::{Fs, RemoveOptions, RenameOptions}; -use futures::future::join_all; -use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task, WeakEntity}; -use http_client::HttpClient; -use language::{LanguageConfig, LanguageName, LanguageQueries, LoadedLanguage}; -use lsp::LanguageServerName; -use node_runtime::NodeRuntime; - -use crate::wasm_host::{WasmExtension, WasmHost}; - -#[derive(Clone, Debug)] -pub struct ExtensionVersion { - pub id: String, - pub version: String, - pub dev: bool, -} - -pub struct HeadlessExtensionStore { - pub fs: Arc, - pub extension_dir: PathBuf, - pub proxy: Arc, - pub wasm_host: Arc, - pub loaded_extensions: HashMap, Arc>, - pub loaded_languages: HashMap, Vec>, - pub loaded_language_servers: HashMap, Vec<(LanguageServerName, LanguageName)>>, -} - -impl HeadlessExtensionStore { - pub fn new( - fs: Arc, - http_client: Arc, - extension_dir: PathBuf, - extension_host_proxy: Arc, - node_runtime: NodeRuntime, - cx: &mut App, - ) -> Entity { - cx.new(|cx| Self { - fs: fs.clone(), - wasm_host: WasmHost::new( - fs.clone(), - http_client.clone(), - node_runtime, - extension_host_proxy.clone(), - extension_dir.join("work"), - cx, - ), - extension_dir, - proxy: extension_host_proxy, - loaded_extensions: Default::default(), - loaded_languages: Default::default(), - loaded_language_servers: Default::default(), - }) - } - - pub fn sync_extensions( - &mut self, - extensions: Vec, - cx: &Context, - ) -> Task>> { - let on_client = HashSet::from_iter(extensions.iter().map(|e| e.id.as_str())); - let to_remove: Vec> = self - .loaded_extensions - .keys() - .filter(|id| !on_client.contains(id.as_ref())) - .cloned() - .collect(); - let to_load: Vec = extensions - .into_iter() - .filter(|e| { - if e.dev { - return true; - } - self.loaded_extensions - .get(e.id.as_str()) - .is_none_or(|loaded| loaded.as_ref() != e.version.as_str()) - }) - .collect(); - - cx.spawn(async move |this, cx| { - let mut missing = Vec::new(); - - for extension_id in to_remove { - log::info!("removing extension: {}", extension_id); - this.update(cx, |this, cx| this.uninstall_extension(&extension_id, cx))? - .await?; - } - - for extension in to_load { - if let Err(e) = Self::load_extension(this.clone(), extension.clone(), cx).await { - log::info!("failed to load extension: {}, {:#}", extension.id, e); - missing.push(extension) - } else if extension.dev { - missing.push(extension) - } - } - - Ok(missing) - }) - } - - pub async fn load_extension( - this: WeakEntity, - extension: ExtensionVersion, - cx: &mut AsyncApp, - ) -> Result<()> { - let (fs, wasm_host, extension_dir) = this.update(cx, |this, _cx| { - this.loaded_extensions.insert( - extension.id.clone().into(), - extension.version.clone().into(), - ); - ( - this.fs.clone(), - this.wasm_host.clone(), - this.extension_dir.join(&extension.id), - ) - })?; - - let manifest = Arc::new(ExtensionManifest::load(fs.clone(), &extension_dir).await?); - - debug_assert!(!manifest.languages.is_empty() || manifest.allow_remote_load()); - - if manifest.version.as_ref() != extension.version.as_str() { - anyhow::bail!( - "mismatched versions: ({}) != ({})", - manifest.version, - extension.version - ) - } - - for language_path in &manifest.languages { - let language_path = extension_dir.join(language_path); - let config = fs.load(&language_path.join("config.toml")).await?; - let mut config = ::toml::from_str::(&config)?; - - this.update(cx, |this, _cx| { - this.loaded_languages - .entry(manifest.id.clone()) - .or_default() - .push(config.name.clone()); - - config.grammar = None; - - this.proxy.register_language( - config.name.clone(), - None, - config.matcher.clone(), - config.hidden, - Arc::new(move || { - Ok(LoadedLanguage { - config: config.clone(), - queries: LanguageQueries::default(), - context_provider: None, - toolchain_provider: None, - manifest_name: None, - }) - }), - ); - })?; - } - - if !manifest.allow_remote_load() { - return Ok(()); - } - - let wasm_extension: Arc = - 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() { - this.update(cx, |this, _cx| { - this.loaded_language_servers - .entry(manifest.id.clone()) - .or_default() - .push((language_server_id.clone(), language.clone())); - this.proxy.register_language_server( - wasm_extension.clone(), - language_server_id.clone(), - language.clone(), - ); - })?; - } - log::info!("Loaded language server: {}", language_server_id); - } - - for (debug_adapter, meta) in &manifest.debug_adapters { - let schema_path = extension::build_debug_adapter_schema_path(debug_adapter, meta); - - this.update(cx, |this, _cx| { - this.proxy.register_debug_adapter( - wasm_extension.clone(), - debug_adapter.clone(), - &extension_dir.join(schema_path), - ); - })?; - log::info!("Loaded debug adapter: {}", debug_adapter); - } - - for debug_locator in manifest.debug_locators.keys() { - this.update(cx, |this, _cx| { - this.proxy - .register_debug_locator(wasm_extension.clone(), debug_locator.clone()); - })?; - log::info!("Loaded debug locator: {}", debug_locator); - } - - Ok(()) - } - - fn uninstall_extension( - &mut self, - extension_id: &Arc, - cx: &mut Context, - ) -> Task> { - self.loaded_extensions.remove(extension_id); - - let languages_to_remove = self - .loaded_languages - .remove(extension_id) - .unwrap_or_default(); - self.proxy.remove_languages(&languages_to_remove, &[]); - - let servers_to_remove = self - .loaded_language_servers - .remove(extension_id) - .unwrap_or_default(); - let proxy = self.proxy.clone(); - let path = self.extension_dir.join(&extension_id.to_string()); - let fs = self.fs.clone(); - cx.spawn(async move |_, cx| { - let mut removal_tasks = Vec::with_capacity(servers_to_remove.len()); - cx.update(|cx| { - for (language_server_name, language) in servers_to_remove { - removal_tasks.push(proxy.remove_language_server( - &language, - &language_server_name, - cx, - )); - } - }) - .ok(); - let _ = join_all(removal_tasks).await; - - fs.remove_dir( - &path, - RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await - .with_context(|| format!("Removing directory {path:?}")) - }) - } - - pub fn install_extension( - &mut self, - extension: ExtensionVersion, - tmp_path: PathBuf, - cx: &mut Context, - ) -> Task> { - let path = self.extension_dir.join(&extension.id); - let fs = self.fs.clone(); - - cx.spawn(async move |this, cx| { - if fs.is_dir(&path).await { - this.update(cx, |this, cx| { - this.uninstall_extension(&extension.id.clone().into(), cx) - })? - .await?; - } - - fs.rename(&tmp_path, &path, RenameOptions::default()) - .await - .context("Failed to rename {tmp_path:?} to {path:?}")?; - - Self::load_extension(this, extension, cx).await - }) - } - - pub async fn handle_sync_extensions( - extension_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let requested_extensions = - envelope - .payload - .extensions - .into_iter() - .map(|p| ExtensionVersion { - id: p.id, - version: p.version, - dev: p.dev, - }); - let missing_extensions = extension_store - .update(&mut cx, |extension_store, cx| { - extension_store.sync_extensions(requested_extensions.collect(), cx) - })? - .await?; - - Ok(proto::SyncExtensionsResponse { - missing_extensions: missing_extensions - .into_iter() - .map(|e| proto::Extension { - id: e.id, - version: e.version, - dev: e.dev, - }) - .collect(), - tmp_dir: paths::remote_extensions_uploads_dir() - .to_string_lossy() - .to_string(), - }) - } - - pub async fn handle_install_extension( - extensions: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let extension = envelope - .payload - .extension - .context("Invalid InstallExtension request")?; - - extensions - .update(&mut cx, |extensions, cx| { - extensions.install_extension( - ExtensionVersion { - id: extension.id, - version: extension.version, - dev: extension.dev, - }, - PathBuf::from(envelope.payload.tmp_dir), - cx, - ) - })? - .await?; - - Ok(proto::Ack {}) - } -} diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs deleted file mode 100644 index cecaf2039b..0000000000 --- a/crates/extension_host/src/wasm_host.rs +++ /dev/null @@ -1,900 +0,0 @@ -pub mod wit; - -use crate::capability_granter::CapabilityGranter; -use crate::{ExtensionManifest, ExtensionSettings}; -use anyhow::{Context as _, Result, anyhow, bail}; -use async_trait::async_trait; -use dap::{DebugRequest, StartDebuggingRequestArgumentsRequest}; -use extension::{ - CodeLabel, Command, Completion, ContextServerConfiguration, DebugAdapterBinary, - DebugTaskDefinition, ExtensionCapability, ExtensionHostProxy, KeyValueStoreDelegate, - ProjectDelegate, SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, - WorktreeDelegate, -}; -use fs::{Fs, normalize_path}; -use futures::future::LocalBoxFuture; -use futures::{ - Future, FutureExt, StreamExt as _, - channel::{ - mpsc::{self, UnboundedSender}, - oneshot, - }, - future::BoxFuture, -}; -use gpui::{App, AsyncApp, BackgroundExecutor, Task, Timer}; -use http_client::HttpClient; -use language::LanguageName; -use lsp::LanguageServerName; -use moka::sync::Cache; -use node_runtime::NodeRuntime; -use release_channel::ReleaseChannel; -use semver::Version; -use settings::Settings; -use std::{ - borrow::Cow, - path::{Path, PathBuf}, - sync::{ - Arc, LazyLock, OnceLock, - atomic::{AtomicBool, Ordering}, - }, - time::Duration, -}; -use task::{DebugScenario, SpawnInTerminal, TaskTemplate, ZedDebugConfig}; -use util::paths::SanitizedPath; -use wasmtime::{ - CacheStore, Engine, Store, - component::{Component, ResourceTable}, -}; -use wasmtime_wasi::{self as wasi, WasiView}; -use wit::Extension; - -pub struct WasmHost { - engine: Engine, - release_channel: ReleaseChannel, - http_client: Arc, - node_runtime: NodeRuntime, - pub(crate) proxy: Arc, - fs: Arc, - pub work_dir: PathBuf, - /// The capabilities granted to extensions running on the host. - pub(crate) granted_capabilities: Vec, - _main_thread_message_task: Task<()>, - main_thread_message_tx: mpsc::UnboundedSender, -} - -#[derive(Clone, Debug)] -pub struct WasmExtension { - tx: UnboundedSender, - pub manifest: Arc, - pub work_dir: Arc, - #[allow(unused)] - pub zed_api_version: Version, - _task: Arc>>, -} - -impl Drop for WasmExtension { - fn drop(&mut self) { - self.tx.close_channel(); - } -} - -#[async_trait] -impl extension::Extension for WasmExtension { - fn manifest(&self) -> Arc { - self.manifest.clone() - } - - fn work_dir(&self) -> Arc { - self.work_dir.clone() - } - - async fn language_server_command( - &self, - language_server_id: LanguageServerName, - language_name: LanguageName, - worktree: Arc, - ) -> Result { - self.call(|extension, store| { - async move { - let resource = store.data_mut().table().push(worktree)?; - let command = extension - .call_language_server_command( - store, - &language_server_id, - &language_name, - resource, - ) - .await? - .map_err(|err| store.data().extension_error(err))?; - - Ok(command.into()) - } - .boxed() - }) - .await? - } - - async fn language_server_initialization_options( - &self, - language_server_id: LanguageServerName, - language_name: LanguageName, - worktree: Arc, - ) -> Result> { - self.call(|extension, store| { - async move { - let resource = store.data_mut().table().push(worktree)?; - let options = extension - .call_language_server_initialization_options( - store, - &language_server_id, - &language_name, - resource, - ) - .await? - .map_err(|err| store.data().extension_error(err))?; - anyhow::Ok(options) - } - .boxed() - }) - .await? - } - - async fn language_server_workspace_configuration( - &self, - language_server_id: LanguageServerName, - worktree: Arc, - ) -> Result> { - self.call(|extension, store| { - async move { - let resource = store.data_mut().table().push(worktree)?; - let options = extension - .call_language_server_workspace_configuration( - store, - &language_server_id, - resource, - ) - .await? - .map_err(|err| store.data().extension_error(err))?; - anyhow::Ok(options) - } - .boxed() - }) - .await? - } - - async fn language_server_additional_initialization_options( - &self, - language_server_id: LanguageServerName, - target_language_server_id: LanguageServerName, - worktree: Arc, - ) -> Result> { - self.call(|extension, store| { - async move { - let resource = store.data_mut().table().push(worktree)?; - let options = extension - .call_language_server_additional_initialization_options( - store, - &language_server_id, - &target_language_server_id, - resource, - ) - .await? - .map_err(|err| store.data().extension_error(err))?; - anyhow::Ok(options) - } - .boxed() - }) - .await? - } - - async fn language_server_additional_workspace_configuration( - &self, - language_server_id: LanguageServerName, - target_language_server_id: LanguageServerName, - worktree: Arc, - ) -> Result> { - self.call(|extension, store| { - async move { - let resource = store.data_mut().table().push(worktree)?; - let options = extension - .call_language_server_additional_workspace_configuration( - store, - &language_server_id, - &target_language_server_id, - resource, - ) - .await? - .map_err(|err| store.data().extension_error(err))?; - anyhow::Ok(options) - } - .boxed() - }) - .await? - } - - async fn labels_for_completions( - &self, - language_server_id: LanguageServerName, - completions: Vec, - ) -> Result>> { - self.call(|extension, store| { - async move { - let labels = extension - .call_labels_for_completions( - store, - &language_server_id, - completions.into_iter().map(Into::into).collect(), - ) - .await? - .map_err(|err| store.data().extension_error(err))?; - - Ok(labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect()) - } - .boxed() - }) - .await? - } - - async fn labels_for_symbols( - &self, - language_server_id: LanguageServerName, - symbols: Vec, - ) -> Result>> { - self.call(|extension, store| { - async move { - let labels = extension - .call_labels_for_symbols( - store, - &language_server_id, - symbols.into_iter().map(Into::into).collect(), - ) - .await? - .map_err(|err| store.data().extension_error(err))?; - - Ok(labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect()) - } - .boxed() - }) - .await? - } - - async fn complete_slash_command_argument( - &self, - command: SlashCommand, - arguments: Vec, - ) -> Result> { - self.call(|extension, store| { - async move { - let completions = extension - .call_complete_slash_command_argument(store, &command.into(), &arguments) - .await? - .map_err(|err| store.data().extension_error(err))?; - - Ok(completions.into_iter().map(Into::into).collect()) - } - .boxed() - }) - .await? - } - - async fn run_slash_command( - &self, - command: SlashCommand, - arguments: Vec, - delegate: Option>, - ) -> Result { - self.call(|extension, store| { - async move { - let resource = if let Some(delegate) = delegate { - Some(store.data_mut().table().push(delegate)?) - } else { - None - }; - - let output = extension - .call_run_slash_command(store, &command.into(), &arguments, resource) - .await? - .map_err(|err| store.data().extension_error(err))?; - - Ok(output.into()) - } - .boxed() - }) - .await? - } - - async fn context_server_command( - &self, - context_server_id: Arc, - project: Arc, - ) -> Result { - self.call(|extension, store| { - async move { - let project_resource = store.data_mut().table().push(project)?; - let command = extension - .call_context_server_command(store, context_server_id.clone(), project_resource) - .await? - .map_err(|err| store.data().extension_error(err))?; - anyhow::Ok(command.into()) - } - .boxed() - }) - .await? - } - - async fn context_server_configuration( - &self, - context_server_id: Arc, - project: Arc, - ) -> Result> { - self.call(|extension, store| { - async move { - let project_resource = store.data_mut().table().push(project)?; - let Some(configuration) = extension - .call_context_server_configuration( - store, - context_server_id.clone(), - project_resource, - ) - .await? - .map_err(|err| store.data().extension_error(err))? - else { - return Ok(None); - }; - - Ok(Some(configuration.try_into()?)) - } - .boxed() - }) - .await? - } - - async fn suggest_docs_packages(&self, provider: Arc) -> Result> { - self.call(|extension, store| { - async move { - let packages = extension - .call_suggest_docs_packages(store, provider.as_ref()) - .await? - .map_err(|err| store.data().extension_error(err))?; - - Ok(packages) - } - .boxed() - }) - .await? - } - - async fn index_docs( - &self, - provider: Arc, - package_name: Arc, - kv_store: Arc, - ) -> Result<()> { - self.call(|extension, store| { - async move { - let kv_store_resource = store.data_mut().table().push(kv_store)?; - extension - .call_index_docs( - store, - provider.as_ref(), - package_name.as_ref(), - kv_store_resource, - ) - .await? - .map_err(|err| store.data().extension_error(err))?; - - anyhow::Ok(()) - } - .boxed() - }) - .await? - } - - async fn get_dap_binary( - &self, - dap_name: Arc, - config: DebugTaskDefinition, - user_installed_path: Option, - worktree: Arc, - ) -> Result { - self.call(|extension, store| { - async move { - let resource = store.data_mut().table().push(worktree)?; - let dap_binary = extension - .call_get_dap_binary(store, dap_name, config, user_installed_path, resource) - .await? - .map_err(|err| store.data().extension_error(err))?; - let dap_binary = dap_binary.try_into()?; - Ok(dap_binary) - } - .boxed() - }) - .await? - } - async fn dap_request_kind( - &self, - dap_name: Arc, - config: serde_json::Value, - ) -> Result { - self.call(|extension, store| { - async move { - let kind = extension - .call_dap_request_kind(store, dap_name, config) - .await? - .map_err(|err| store.data().extension_error(err))?; - Ok(kind.into()) - } - .boxed() - }) - .await? - } - - async fn dap_config_to_scenario(&self, config: ZedDebugConfig) -> Result { - self.call(|extension, store| { - async move { - let kind = extension - .call_dap_config_to_scenario(store, config) - .await? - .map_err(|err| store.data().extension_error(err))?; - Ok(kind) - } - .boxed() - }) - .await? - } - - async fn dap_locator_create_scenario( - &self, - locator_name: String, - build_config_template: TaskTemplate, - resolved_label: String, - debug_adapter_name: String, - ) -> Result> { - self.call(|extension, store| { - async move { - extension - .call_dap_locator_create_scenario( - store, - locator_name, - build_config_template, - resolved_label, - debug_adapter_name, - ) - .await - } - .boxed() - }) - .await? - } - async fn run_dap_locator( - &self, - locator_name: String, - config: SpawnInTerminal, - ) -> Result { - self.call(|extension, store| { - async move { - extension - .call_run_dap_locator(store, locator_name, config) - .await? - .map_err(|err| store.data().extension_error(err)) - } - .boxed() - }) - .await? - } -} - -pub struct WasmState { - manifest: Arc, - pub table: ResourceTable, - ctx: wasi::WasiCtx, - pub host: Arc, - pub(crate) capability_granter: CapabilityGranter, -} - -std::thread_local! { - /// Used by the crash handler to ignore panics in extension-related threads. - pub static IS_WASM_THREAD: AtomicBool = const { AtomicBool::new(false) }; -} - -type MainThreadCall = Box FnOnce(&'a mut AsyncApp) -> LocalBoxFuture<'a, ()>>; - -type ExtensionCall = Box< - dyn Send + for<'a> FnOnce(&'a mut Extension, &'a mut Store) -> BoxFuture<'a, ()>, ->; - -fn wasm_engine(executor: &BackgroundExecutor) -> wasmtime::Engine { - static WASM_ENGINE: OnceLock = OnceLock::new(); - WASM_ENGINE - .get_or_init(|| { - let mut config = wasmtime::Config::new(); - config.wasm_component_model(true); - config.async_support(true); - config - .enable_incremental_compilation(cache_store()) - .unwrap(); - // Async support introduces the issue that extension execution happens during `Future::poll`, - // which could block an async thread. - // https://docs.rs/wasmtime/latest/wasmtime/struct.Config.html#execution-in-poll - // - // Epoch interruption is a lightweight mechanism to allow the extensions to yield control - // back to the executor at regular intervals. - config.epoch_interruption(true); - - let engine = wasmtime::Engine::new(&config).unwrap(); - - // It might be safer to do this on a non-async thread to make sure it makes progress - // regardless of if extensions are blocking. - // However, due to our current setup, this isn't a likely occurrence and we'd rather - // not have a dedicated thread just for this. If it becomes an issue, we can consider - // creating a separate thread for epoch interruption. - let engine_ref = engine.weak(); - executor - .spawn(async move { - // Somewhat arbitrary interval, as it isn't a guaranteed interval. - // But this is a rough upper bound for how long the extension execution can block on - // `Future::poll`. - const EPOCH_INTERVAL: Duration = Duration::from_millis(100); - let mut timer = Timer::interval(EPOCH_INTERVAL); - while (timer.next().await).is_some() { - // Exit the loop and thread once the engine is dropped. - let Some(engine) = engine_ref.upgrade() else { - break; - }; - engine.increment_epoch(); - } - }) - .detach(); - - engine - }) - .clone() -} - -fn cache_store() -> Arc { - static CACHE_STORE: LazyLock> = - LazyLock::new(|| Arc::new(IncrementalCompilationCache::new())); - CACHE_STORE.clone() -} - -impl WasmHost { - pub fn new( - fs: Arc, - http_client: Arc, - node_runtime: NodeRuntime, - proxy: Arc, - work_dir: PathBuf, - cx: &mut App, - ) -> Arc { - let (tx, mut rx) = mpsc::unbounded::(); - let task = cx.spawn(async move |cx| { - while let Some(message) = rx.next().await { - message(cx).await; - } - }); - - let extension_settings = ExtensionSettings::get_global(cx); - - Arc::new(Self { - engine: wasm_engine(cx.background_executor()), - fs, - work_dir, - http_client, - node_runtime, - proxy, - release_channel: ReleaseChannel::global(cx), - granted_capabilities: extension_settings.granted_capabilities.clone(), - _main_thread_message_task: task, - main_thread_message_tx: tx, - }) - } - - pub fn load_extension( - self: &Arc, - wasm_bytes: Vec, - manifest: &Arc, - cx: &AsyncApp, - ) -> Task> { - let this = self.clone(); - let manifest = manifest.clone(); - let executor = cx.background_executor().clone(); - let load_extension_task = async move { - let zed_api_version = parse_wasm_extension_version(&manifest.id, &wasm_bytes)?; - - let component = Component::from_binary(&this.engine, &wasm_bytes) - .context("failed to compile wasm component")?; - let mut store = wasmtime::Store::new( - &this.engine, - WasmState { - ctx: this.build_wasi_ctx(&manifest).await?, - manifest: manifest.clone(), - table: ResourceTable::new(), - host: this.clone(), - capability_granter: CapabilityGranter::new( - this.granted_capabilities.clone(), - manifest.clone(), - ), - }, - ); - // Store will yield after 1 tick, and get a new deadline of 1 tick after each yield. - store.set_epoch_deadline(1); - store.epoch_deadline_async_yield_and_update(1); - - let mut extension = Extension::instantiate_async( - &executor, - &mut store, - this.release_channel, - zed_api_version.clone(), - &component, - ) - .await?; - - extension - .call_init_extension(&mut store) - .await - .context("failed to initialize wasm extension")?; - - let (tx, mut rx) = mpsc::unbounded::(); - let extension_task = async move { - // note: Setting the thread local here will slowly "poison" all tokio threads - // causing us to not record their panics any longer. - // - // This is fine though, the main zed binary only uses tokio for livekit and wasm extensions. - // Livekit seldom (if ever) panics 🤞 so the likelihood of us missing a panic in sentry is very low. - IS_WASM_THREAD.with(|v| v.store(true, Ordering::Release)); - while let Some(call) = rx.next().await { - (call)(&mut extension, &mut store).await; - } - }; - - anyhow::Ok(( - extension_task, - manifest.clone(), - this.work_dir.join(manifest.id.as_ref()).into(), - tx, - zed_api_version, - )) - }; - cx.spawn(async move |cx| { - let (extension_task, manifest, work_dir, tx, zed_api_version) = - cx.background_executor().spawn(load_extension_task).await?; - // we need to run run the task in a tokio context as wasmtime_wasi may - // call into tokio, accessing its runtime handle when we trigger the `engine.increment_epoch()` above. - let task = Arc::new(gpui_tokio::Tokio::spawn(cx, extension_task)?); - - Ok(WasmExtension { - manifest, - work_dir, - tx, - zed_api_version, - _task: task, - }) - }) - } - - async fn build_wasi_ctx(&self, manifest: &Arc) -> Result { - let extension_work_dir = self.work_dir.join(manifest.id.as_ref()); - self.fs - .create_dir(&extension_work_dir) - .await - .context("failed to create extension work dir")?; - - let file_perms = wasi::FilePerms::all(); - let dir_perms = wasi::DirPerms::all(); - let path = SanitizedPath::new(&extension_work_dir).to_string(); - #[cfg(target_os = "windows")] - let path = path.replace('\\', "/"); - - let mut ctx = wasi::WasiCtxBuilder::new(); - ctx.inherit_stdio() - .env("PWD", &path) - .env("RUST_BACKTRACE", "full"); - - ctx.preopened_dir(&path, ".", dir_perms, file_perms)?; - ctx.preopened_dir(&path, &path, dir_perms, file_perms)?; - - Ok(ctx.build()) - } - - pub fn writeable_path_from_extension(&self, id: &Arc, path: &Path) -> Result { - let extension_work_dir = self.work_dir.join(id.as_ref()); - let path = normalize_path(&extension_work_dir.join(path)); - anyhow::ensure!( - path.starts_with(&extension_work_dir), - "cannot write to path {path:?}", - ); - Ok(path) - } -} - -pub fn parse_wasm_extension_version(extension_id: &str, wasm_bytes: &[u8]) -> Result { - let mut version = None; - - 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() - ); - } - } - } - - // The reason we wait until we're done parsing all of the Wasm bytes to return the version - // is to work around a panic that can happen inside of Wasmtime when the bytes are invalid. - // - // By parsing the entirety of the Wasm bytes before we return, we're able to detect this problem - // earlier as an `Err` rather than as a panic. - version.with_context(|| format!("extension {extension_id} has no zed:api-version section")) -} - -fn parse_wasm_extension_version_custom_section(data: &[u8]) -> Option { - if data.len() == 6 { - Some(Version::new( - u16::from_be_bytes([data[0], data[1]]) as _, - u16::from_be_bytes([data[2], data[3]]) as _, - u16::from_be_bytes([data[4], data[5]]) as _, - )) - } else { - None - } -} - -impl WasmExtension { - pub async fn load( - extension_dir: &Path, - manifest: &Arc, - wasm_host: Arc, - cx: &AsyncApp, - ) -> Result { - let path = extension_dir.join("extension.wasm"); - - let mut wasm_file = wasm_host - .fs - .open_sync(&path) - .await - .context(format!("opening wasm file, path: {path:?}"))?; - - let mut wasm_bytes = Vec::new(); - wasm_file - .read_to_end(&mut wasm_bytes) - .context(format!("reading wasm file, path: {path:?}"))?; - - wasm_host - .load_extension(wasm_bytes, manifest, cx) - .await - .with_context(|| format!("loading wasm extension: {}", manifest.id)) - } - - pub async fn call(&self, f: Fn) -> Result - where - T: 'static + Send, - Fn: 'static - + Send - + for<'a> FnOnce(&'a mut Extension, &'a mut Store) -> BoxFuture<'a, T>, - { - let (return_tx, return_rx) = oneshot::channel(); - self.tx - .unbounded_send(Box::new(move |extension, store| { - async { - let result = f(extension, store).await; - return_tx.send(result).ok(); - } - .boxed() - })) - .map_err(|_| { - anyhow!( - "wasm extension channel should not be closed yet, extension {} (id {})", - self.manifest.name, - self.manifest.id, - ) - })?; - return_rx.await.with_context(|| { - format!( - "wasm extension channel, extension {} (id {})", - self.manifest.name, self.manifest.id, - ) - }) - } -} - -impl WasmState { - fn on_main_thread(&self, f: Fn) -> impl 'static + Future - where - T: 'static + Send, - Fn: 'static + Send + for<'a> FnOnce(&'a mut AsyncApp) -> LocalBoxFuture<'a, T>, - { - let (return_tx, return_rx) = oneshot::channel(); - self.host - .main_thread_message_tx - .clone() - .unbounded_send(Box::new(move |cx| { - async { - let result = f(cx).await; - return_tx.send(result).ok(); - } - .boxed_local() - })) - .unwrap_or_else(|_| { - panic!( - "main thread message channel should not be closed yet, extension {} (id {})", - self.manifest.name, self.manifest.id, - ) - }); - let name = self.manifest.name.clone(); - let id = self.manifest.id.clone(); - async move { - return_rx.await.unwrap_or_else(|_| { - panic!("main thread message channel, extension {name} (id {id})") - }) - } - } - - fn work_dir(&self) -> PathBuf { - self.host.work_dir.join(self.manifest.id.as_ref()) - } - - fn extension_error(&self, message: String) -> anyhow::Error { - anyhow!( - "from extension \"{}\" version {}: {}", - self.manifest.name, - self.manifest.version, - message - ) - } -} - -impl wasi::WasiView for WasmState { - fn table(&mut self) -> &mut ResourceTable { - &mut self.table - } - - fn ctx(&mut self) -> &mut wasi::WasiCtx { - &mut self.ctx - } -} - -/// Wrapper around a mini-moka bounded cache for storing incremental compilation artifacts. -/// Since wasm modules have many similar elements, this can save us a lot of work at the -/// cost of a small memory footprint. However, we don't want this to be unbounded, so we use -/// a LFU/LRU cache to evict less used cache entries. -#[derive(Debug)] -struct IncrementalCompilationCache { - cache: Cache, Vec>, -} - -impl IncrementalCompilationCache { - fn new() -> Self { - let cache = Cache::builder() - // Cap this at 32 MB for now. Our extensions turn into roughly 512kb in the cache, - // which means we could store 64 completely novel extensions in the cache, but in - // practice we will more than that, which is more than enough for our use case. - .max_capacity(32 * 1024 * 1024) - .weigher(|k: &Vec, v: &Vec| (k.len() + v.len()).try_into().unwrap_or(u32::MAX)) - .build(); - Self { cache } - } -} - -impl CacheStore for IncrementalCompilationCache { - fn get(&self, key: &[u8]) -> Option> { - self.cache.get(key).map(|v| v.into()) - } - - fn insert(&self, key: &[u8], value: Vec) -> bool { - self.cache.insert(key.to_vec(), value); - true - } -} diff --git a/crates/extension_host/src/wasm_host/wit.rs b/crates/extension_host/src/wasm_host/wit.rs deleted file mode 100644 index 5058c63365..0000000000 --- a/crates/extension_host/src/wasm_host/wit.rs +++ /dev/null @@ -1,1124 +0,0 @@ -mod since_v0_0_1; -mod since_v0_0_4; -mod since_v0_0_6; -mod since_v0_1_0; -mod since_v0_2_0; -mod since_v0_3_0; -mod since_v0_4_0; -mod since_v0_5_0; -mod since_v0_6_0; -mod since_v0_8_0; -use dap::DebugRequest; -use extension::{DebugTaskDefinition, KeyValueStoreDelegate, WorktreeDelegate}; -use gpui::BackgroundExecutor; -use language::LanguageName; -use lsp::LanguageServerName; -use release_channel::ReleaseChannel; -use task::{DebugScenario, SpawnInTerminal, TaskTemplate, ZedDebugConfig}; - -use crate::wasm_host::wit::since_v0_6_0::dap::StartDebuggingRequestArgumentsRequest; - -use super::{WasmState, wasm_engine}; -use anyhow::{Context as _, Result, anyhow}; -use semver::Version; -use since_v0_8_0 as latest; -use std::{ops::RangeInclusive, path::PathBuf, sync::Arc}; -use wasmtime::{ - Store, - component::{Component, Linker, Resource}, -}; - -#[cfg(test)] -pub use latest::CodeLabelSpanLiteral; -pub use latest::{ - CodeLabel, CodeLabelSpan, Command, DebugAdapterBinary, ExtensionProject, Range, SlashCommand, - zed::extension::context_server::ContextServerConfiguration, - zed::extension::lsp::{ - Completion, CompletionKind, CompletionLabelDetails, InsertTextFormat, Symbol, SymbolKind, - }, - zed::extension::slash_command::{SlashCommandArgumentCompletion, SlashCommandOutput}, -}; -pub use since_v0_0_4::LanguageServerConfig; - -pub fn new_linker( - executor: &BackgroundExecutor, - f: impl Fn(&mut Linker, fn(&mut WasmState) -> &mut WasmState) -> Result<()>, -) -> Linker { - let mut linker = Linker::new(&wasm_engine(executor)); - wasmtime_wasi::add_to_linker_async(&mut linker).unwrap(); - f(&mut linker, wasi_view).unwrap(); - linker -} - -fn wasi_view(state: &mut WasmState) -> &mut WasmState { - state -} - -/// Returns whether the given Wasm API version is supported by the Wasm host. -pub fn is_supported_wasm_api_version(release_channel: ReleaseChannel, version: Version) -> bool { - wasm_api_version_range(release_channel).contains(&version) -} - -/// Returns the Wasm API version range that is supported by the Wasm host. -#[inline(always)] -pub fn wasm_api_version_range(release_channel: ReleaseChannel) -> RangeInclusive { - // Note: The release channel can be used to stage a new version of the extension API. - let _ = release_channel; - - let max_version = match release_channel { - ReleaseChannel::Dev | ReleaseChannel::Nightly => latest::MAX_VERSION, - ReleaseChannel::Stable | ReleaseChannel::Preview => since_v0_6_0::MAX_VERSION, - }; - - since_v0_0_1::MIN_VERSION..=max_version -} - -/// Authorizes access to use unreleased versions of the Wasm API, based on the provided [`ReleaseChannel`]. -/// -/// Note: If there isn't currently an unreleased Wasm API version this function may be unused. Don't delete it! -pub fn authorize_access_to_unreleased_wasm_api_version( - release_channel: ReleaseChannel, -) -> Result<()> { - let allow_unreleased_version = match release_channel { - ReleaseChannel::Dev | ReleaseChannel::Nightly => true, - ReleaseChannel::Stable | ReleaseChannel::Preview => { - // We always allow the latest in tests so that the extension tests pass on release branches. - cfg!(any(test, feature = "test-support")) - } - }; - - anyhow::ensure!( - allow_unreleased_version, - "unreleased versions of the extension API can only be used on development builds of Zed" - ); - - Ok(()) -} - -pub enum Extension { - V0_8_0(since_v0_8_0::Extension), - V0_6_0(since_v0_6_0::Extension), - V0_5_0(since_v0_5_0::Extension), - V0_4_0(since_v0_4_0::Extension), - V0_3_0(since_v0_3_0::Extension), - V0_2_0(since_v0_2_0::Extension), - V0_1_0(since_v0_1_0::Extension), - V0_0_6(since_v0_0_6::Extension), - V0_0_4(since_v0_0_4::Extension), - V0_0_1(since_v0_0_1::Extension), -} - -impl Extension { - pub async fn instantiate_async( - executor: &BackgroundExecutor, - store: &mut Store, - release_channel: ReleaseChannel, - version: Version, - component: &Component, - ) -> Result { - // Note: The release channel can be used to stage a new version of the extension API. - let _ = release_channel; - - if version >= latest::MIN_VERSION { - authorize_access_to_unreleased_wasm_api_version(release_channel)?; - - let extension = - latest::Extension::instantiate_async(store, component, latest::linker(executor)) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_8_0(extension)) - } else if version >= since_v0_6_0::MIN_VERSION { - let extension = since_v0_6_0::Extension::instantiate_async( - store, - component, - since_v0_6_0::linker(executor), - ) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_6_0(extension)) - } else if version >= since_v0_5_0::MIN_VERSION { - let extension = since_v0_5_0::Extension::instantiate_async( - store, - component, - since_v0_5_0::linker(executor), - ) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_5_0(extension)) - } else if version >= since_v0_4_0::MIN_VERSION { - let extension = since_v0_4_0::Extension::instantiate_async( - store, - component, - since_v0_4_0::linker(executor), - ) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_4_0(extension)) - } else if version >= since_v0_3_0::MIN_VERSION { - let extension = since_v0_3_0::Extension::instantiate_async( - store, - component, - since_v0_3_0::linker(executor), - ) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_3_0(extension)) - } else if version >= since_v0_2_0::MIN_VERSION { - let extension = since_v0_2_0::Extension::instantiate_async( - store, - component, - since_v0_2_0::linker(executor), - ) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_2_0(extension)) - } else if version >= since_v0_1_0::MIN_VERSION { - let extension = since_v0_1_0::Extension::instantiate_async( - store, - component, - since_v0_1_0::linker(executor), - ) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_1_0(extension)) - } else if version >= since_v0_0_6::MIN_VERSION { - let extension = since_v0_0_6::Extension::instantiate_async( - store, - component, - since_v0_0_6::linker(executor), - ) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_0_6(extension)) - } else if version >= since_v0_0_4::MIN_VERSION { - let extension = since_v0_0_4::Extension::instantiate_async( - store, - component, - since_v0_0_4::linker(executor), - ) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_0_4(extension)) - } else { - let extension = since_v0_0_1::Extension::instantiate_async( - store, - component, - since_v0_0_1::linker(executor), - ) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_0_1(extension)) - } - } - - pub async fn call_init_extension(&self, store: &mut Store) -> Result<()> { - match self { - Extension::V0_8_0(ext) => ext.call_init_extension(store).await, - Extension::V0_6_0(ext) => ext.call_init_extension(store).await, - Extension::V0_5_0(ext) => ext.call_init_extension(store).await, - Extension::V0_4_0(ext) => ext.call_init_extension(store).await, - Extension::V0_3_0(ext) => ext.call_init_extension(store).await, - Extension::V0_2_0(ext) => ext.call_init_extension(store).await, - Extension::V0_1_0(ext) => ext.call_init_extension(store).await, - Extension::V0_0_6(ext) => ext.call_init_extension(store).await, - Extension::V0_0_4(ext) => ext.call_init_extension(store).await, - Extension::V0_0_1(ext) => ext.call_init_extension(store).await, - } - } - - pub async fn call_language_server_command( - &self, - store: &mut Store, - language_server_id: &LanguageServerName, - language_name: &LanguageName, - resource: Resource>, - ) -> Result> { - match self { - Extension::V0_8_0(ext) => { - ext.call_language_server_command(store, &language_server_id.0, resource) - .await - } - Extension::V0_6_0(ext) => { - ext.call_language_server_command(store, &language_server_id.0, resource) - .await - } - Extension::V0_5_0(ext) => { - ext.call_language_server_command(store, &language_server_id.0, resource) - .await - } - Extension::V0_4_0(ext) => { - ext.call_language_server_command(store, &language_server_id.0, resource) - .await - } - Extension::V0_3_0(ext) => { - ext.call_language_server_command(store, &language_server_id.0, resource) - .await - } - Extension::V0_2_0(ext) => Ok(ext - .call_language_server_command(store, &language_server_id.0, resource) - .await? - .map(|command| command.into())), - Extension::V0_1_0(ext) => Ok(ext - .call_language_server_command(store, &language_server_id.0, resource) - .await? - .map(|command| command.into())), - Extension::V0_0_6(ext) => Ok(ext - .call_language_server_command(store, &language_server_id.0, resource) - .await? - .map(|command| command.into())), - Extension::V0_0_4(ext) => Ok(ext - .call_language_server_command( - store, - &LanguageServerConfig { - name: language_server_id.0.to_string(), - language_name: language_name.to_string(), - }, - resource, - ) - .await? - .map(|command| command.into())), - Extension::V0_0_1(ext) => Ok(ext - .call_language_server_command( - store, - &LanguageServerConfig { - name: language_server_id.0.to_string(), - language_name: language_name.to_string(), - } - .into(), - resource, - ) - .await? - .map(|command| command.into())), - } - } - - pub async fn call_language_server_initialization_options( - &self, - store: &mut Store, - language_server_id: &LanguageServerName, - language_name: &LanguageName, - resource: Resource>, - ) -> Result, String>> { - match self { - Extension::V0_8_0(ext) => { - ext.call_language_server_initialization_options( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_6_0(ext) => { - ext.call_language_server_initialization_options( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_5_0(ext) => { - ext.call_language_server_initialization_options( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_4_0(ext) => { - ext.call_language_server_initialization_options( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_3_0(ext) => { - ext.call_language_server_initialization_options( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_2_0(ext) => { - ext.call_language_server_initialization_options( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_1_0(ext) => { - ext.call_language_server_initialization_options( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_0_6(ext) => { - ext.call_language_server_initialization_options( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_0_4(ext) => { - ext.call_language_server_initialization_options( - store, - &LanguageServerConfig { - name: language_server_id.0.to_string(), - language_name: language_name.to_string(), - }, - resource, - ) - .await - } - Extension::V0_0_1(ext) => { - ext.call_language_server_initialization_options( - store, - &LanguageServerConfig { - name: language_server_id.0.to_string(), - language_name: language_name.to_string(), - } - .into(), - resource, - ) - .await - } - } - } - - pub async fn call_language_server_workspace_configuration( - &self, - store: &mut Store, - language_server_id: &LanguageServerName, - resource: Resource>, - ) -> Result, String>> { - match self { - Extension::V0_8_0(ext) => { - ext.call_language_server_workspace_configuration( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_6_0(ext) => { - ext.call_language_server_workspace_configuration( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_5_0(ext) => { - ext.call_language_server_workspace_configuration( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_4_0(ext) => { - ext.call_language_server_workspace_configuration( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_3_0(ext) => { - ext.call_language_server_workspace_configuration( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_2_0(ext) => { - ext.call_language_server_workspace_configuration( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_1_0(ext) => { - ext.call_language_server_workspace_configuration( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_0_6(ext) => { - ext.call_language_server_workspace_configuration( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_0_4(_) | Extension::V0_0_1(_) => Ok(Ok(None)), - } - } - - pub async fn call_language_server_additional_initialization_options( - &self, - store: &mut Store, - language_server_id: &LanguageServerName, - target_language_server_id: &LanguageServerName, - resource: Resource>, - ) -> Result, String>> { - match self { - Extension::V0_8_0(ext) => { - ext.call_language_server_additional_initialization_options( - store, - &language_server_id.0, - &target_language_server_id.0, - resource, - ) - .await - } - Extension::V0_6_0(ext) => { - ext.call_language_server_additional_initialization_options( - store, - &language_server_id.0, - &target_language_server_id.0, - resource, - ) - .await - } - Extension::V0_5_0(ext) => { - ext.call_language_server_additional_initialization_options( - store, - &language_server_id.0, - &target_language_server_id.0, - resource, - ) - .await - } - Extension::V0_4_0(ext) => { - ext.call_language_server_additional_initialization_options( - store, - &language_server_id.0, - &target_language_server_id.0, - resource, - ) - .await - } - Extension::V0_3_0(_) - | Extension::V0_2_0(_) - | Extension::V0_1_0(_) - | Extension::V0_0_6(_) - | Extension::V0_0_4(_) - | Extension::V0_0_1(_) => Ok(Ok(None)), - } - } - - pub async fn call_language_server_additional_workspace_configuration( - &self, - store: &mut Store, - language_server_id: &LanguageServerName, - target_language_server_id: &LanguageServerName, - resource: Resource>, - ) -> Result, String>> { - match self { - Extension::V0_8_0(ext) => { - ext.call_language_server_additional_workspace_configuration( - store, - &language_server_id.0, - &target_language_server_id.0, - resource, - ) - .await - } - Extension::V0_6_0(ext) => { - ext.call_language_server_additional_workspace_configuration( - store, - &language_server_id.0, - &target_language_server_id.0, - resource, - ) - .await - } - Extension::V0_5_0(ext) => { - ext.call_language_server_additional_workspace_configuration( - store, - &language_server_id.0, - &target_language_server_id.0, - resource, - ) - .await - } - Extension::V0_4_0(ext) => { - ext.call_language_server_additional_workspace_configuration( - store, - &language_server_id.0, - &target_language_server_id.0, - resource, - ) - .await - } - Extension::V0_3_0(_) - | Extension::V0_2_0(_) - | Extension::V0_1_0(_) - | Extension::V0_0_6(_) - | Extension::V0_0_4(_) - | Extension::V0_0_1(_) => Ok(Ok(None)), - } - } - - pub async fn call_labels_for_completions( - &self, - store: &mut Store, - language_server_id: &LanguageServerName, - completions: Vec, - ) -> Result>, String>> { - match self { - Extension::V0_8_0(ext) => { - ext.call_labels_for_completions(store, &language_server_id.0, &completions) - .await - } - Extension::V0_6_0(ext) => Ok(ext - .call_labels_for_completions( - store, - &language_server_id.0, - &completions.into_iter().collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_5_0(ext) => Ok(ext - .call_labels_for_completions( - store, - &language_server_id.0, - &completions.into_iter().collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_4_0(ext) => Ok(ext - .call_labels_for_completions( - store, - &language_server_id.0, - &completions.into_iter().collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_3_0(ext) => Ok(ext - .call_labels_for_completions( - store, - &language_server_id.0, - &completions.into_iter().collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_2_0(ext) => Ok(ext - .call_labels_for_completions( - store, - &language_server_id.0, - &completions.into_iter().collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_1_0(ext) => Ok(ext - .call_labels_for_completions( - store, - &language_server_id.0, - &completions.into_iter().map(Into::into).collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_0_6(ext) => Ok(ext - .call_labels_for_completions( - store, - &language_server_id.0, - &completions.into_iter().map(Into::into).collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_0_1(_) | Extension::V0_0_4(_) => Ok(Ok(Vec::new())), - } - } - - pub async fn call_labels_for_symbols( - &self, - store: &mut Store, - language_server_id: &LanguageServerName, - symbols: Vec, - ) -> Result>, String>> { - match self { - Extension::V0_8_0(ext) => { - ext.call_labels_for_symbols(store, &language_server_id.0, &symbols) - .await - } - Extension::V0_6_0(ext) => Ok(ext - .call_labels_for_symbols( - store, - &language_server_id.0, - &symbols.into_iter().collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_5_0(ext) => Ok(ext - .call_labels_for_symbols( - store, - &language_server_id.0, - &symbols.into_iter().collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_4_0(ext) => Ok(ext - .call_labels_for_symbols( - store, - &language_server_id.0, - &symbols.into_iter().collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_3_0(ext) => Ok(ext - .call_labels_for_symbols( - store, - &language_server_id.0, - &symbols.into_iter().collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_2_0(ext) => Ok(ext - .call_labels_for_symbols( - store, - &language_server_id.0, - &symbols.into_iter().collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_1_0(ext) => Ok(ext - .call_labels_for_symbols( - store, - &language_server_id.0, - &symbols.into_iter().map(Into::into).collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_0_6(ext) => Ok(ext - .call_labels_for_symbols( - store, - &language_server_id.0, - &symbols.into_iter().map(Into::into).collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_0_1(_) | Extension::V0_0_4(_) => Ok(Ok(Vec::new())), - } - } - - pub async fn call_complete_slash_command_argument( - &self, - store: &mut Store, - command: &SlashCommand, - arguments: &[String], - ) -> Result, String>> { - match self { - Extension::V0_8_0(ext) => { - ext.call_complete_slash_command_argument(store, command, arguments) - .await - } - Extension::V0_6_0(ext) => { - ext.call_complete_slash_command_argument(store, command, arguments) - .await - } - Extension::V0_5_0(ext) => { - ext.call_complete_slash_command_argument(store, command, arguments) - .await - } - Extension::V0_4_0(ext) => { - ext.call_complete_slash_command_argument(store, command, arguments) - .await - } - Extension::V0_3_0(ext) => { - ext.call_complete_slash_command_argument(store, command, arguments) - .await - } - Extension::V0_2_0(ext) => { - ext.call_complete_slash_command_argument(store, command, arguments) - .await - } - Extension::V0_1_0(ext) => { - ext.call_complete_slash_command_argument(store, command, arguments) - .await - } - Extension::V0_0_1(_) | Extension::V0_0_4(_) | Extension::V0_0_6(_) => { - Ok(Ok(Vec::new())) - } - } - } - - pub async fn call_run_slash_command( - &self, - store: &mut Store, - command: &SlashCommand, - arguments: &[String], - resource: Option>>, - ) -> Result> { - match self { - Extension::V0_8_0(ext) => { - ext.call_run_slash_command(store, command, arguments, resource) - .await - } - Extension::V0_6_0(ext) => { - ext.call_run_slash_command(store, command, arguments, resource) - .await - } - Extension::V0_5_0(ext) => { - ext.call_run_slash_command(store, command, arguments, resource) - .await - } - Extension::V0_4_0(ext) => { - ext.call_run_slash_command(store, command, arguments, resource) - .await - } - Extension::V0_3_0(ext) => { - ext.call_run_slash_command(store, command, arguments, resource) - .await - } - Extension::V0_2_0(ext) => { - ext.call_run_slash_command(store, command, arguments, resource) - .await - } - Extension::V0_1_0(ext) => { - ext.call_run_slash_command(store, command, arguments, resource) - .await - } - Extension::V0_0_1(_) | Extension::V0_0_4(_) | Extension::V0_0_6(_) => { - anyhow::bail!("`run_slash_command` not available prior to v0.1.0"); - } - } - } - - pub async fn call_context_server_command( - &self, - store: &mut Store, - context_server_id: Arc, - project: Resource, - ) -> Result> { - match self { - Extension::V0_8_0(ext) => { - ext.call_context_server_command(store, &context_server_id, project) - .await - } - Extension::V0_6_0(ext) => { - ext.call_context_server_command(store, &context_server_id, project) - .await - } - Extension::V0_5_0(ext) => { - ext.call_context_server_command(store, &context_server_id, project) - .await - } - Extension::V0_4_0(ext) => { - ext.call_context_server_command(store, &context_server_id, project) - .await - } - Extension::V0_3_0(ext) => { - ext.call_context_server_command(store, &context_server_id, project) - .await - } - Extension::V0_2_0(ext) => Ok(ext - .call_context_server_command(store, &context_server_id, project) - .await? - .map(Into::into)), - Extension::V0_0_1(_) - | Extension::V0_0_4(_) - | Extension::V0_0_6(_) - | Extension::V0_1_0(_) => { - anyhow::bail!("`context_server_command` not available prior to v0.2.0"); - } - } - } - - pub async fn call_context_server_configuration( - &self, - store: &mut Store, - context_server_id: Arc, - project: Resource, - ) -> Result, String>> { - match self { - Extension::V0_8_0(ext) => { - ext.call_context_server_configuration(store, &context_server_id, project) - .await - } - Extension::V0_6_0(ext) => { - ext.call_context_server_configuration(store, &context_server_id, project) - .await - } - Extension::V0_5_0(ext) => { - ext.call_context_server_configuration(store, &context_server_id, project) - .await - } - Extension::V0_0_1(_) - | Extension::V0_0_4(_) - | Extension::V0_0_6(_) - | Extension::V0_1_0(_) - | Extension::V0_2_0(_) - | Extension::V0_3_0(_) - | Extension::V0_4_0(_) => { - anyhow::bail!("`context_server_configuration` not available prior to v0.5.0"); - } - } - } - - pub async fn call_suggest_docs_packages( - &self, - store: &mut Store, - provider: &str, - ) -> Result, String>> { - match self { - Extension::V0_8_0(ext) => ext.call_suggest_docs_packages(store, provider).await, - Extension::V0_6_0(ext) => ext.call_suggest_docs_packages(store, provider).await, - Extension::V0_5_0(ext) => ext.call_suggest_docs_packages(store, provider).await, - Extension::V0_4_0(ext) => ext.call_suggest_docs_packages(store, provider).await, - Extension::V0_3_0(ext) => ext.call_suggest_docs_packages(store, provider).await, - Extension::V0_2_0(ext) => ext.call_suggest_docs_packages(store, provider).await, - Extension::V0_1_0(ext) => ext.call_suggest_docs_packages(store, provider).await, - Extension::V0_0_1(_) | Extension::V0_0_4(_) | Extension::V0_0_6(_) => { - anyhow::bail!("`suggest_docs_packages` not available prior to v0.1.0"); - } - } - } - - pub async fn call_index_docs( - &self, - store: &mut Store, - provider: &str, - package_name: &str, - kv_store: Resource>, - ) -> Result> { - match self { - Extension::V0_8_0(ext) => { - ext.call_index_docs(store, provider, package_name, kv_store) - .await - } - Extension::V0_6_0(ext) => { - ext.call_index_docs(store, provider, package_name, kv_store) - .await - } - Extension::V0_5_0(ext) => { - ext.call_index_docs(store, provider, package_name, kv_store) - .await - } - Extension::V0_4_0(ext) => { - ext.call_index_docs(store, provider, package_name, kv_store) - .await - } - Extension::V0_3_0(ext) => { - ext.call_index_docs(store, provider, package_name, kv_store) - .await - } - Extension::V0_2_0(ext) => { - ext.call_index_docs(store, provider, package_name, kv_store) - .await - } - Extension::V0_1_0(ext) => { - ext.call_index_docs(store, provider, package_name, kv_store) - .await - } - Extension::V0_0_1(_) | Extension::V0_0_4(_) | Extension::V0_0_6(_) => { - anyhow::bail!("`index_docs` not available prior to v0.1.0"); - } - } - } - - pub async fn call_get_dap_binary( - &self, - store: &mut Store, - adapter_name: Arc, - task: DebugTaskDefinition, - user_installed_path: Option, - resource: Resource>, - ) -> Result> { - match self { - Extension::V0_6_0(ext) => { - let dap_binary = ext - .call_get_dap_binary( - store, - &adapter_name, - &task.try_into()?, - user_installed_path.as_ref().and_then(|p| p.to_str()), - resource, - ) - .await? - .map_err(|e| anyhow!("{e:?}"))?; - - Ok(Ok(dap_binary)) - } - _ => anyhow::bail!("`get_dap_binary` not available prior to v0.6.0"), - } - } - - pub async fn call_dap_request_kind( - &self, - store: &mut Store, - adapter_name: Arc, - config: serde_json::Value, - ) -> Result> { - match self { - Extension::V0_6_0(ext) => { - let config = - serde_json::to_string(&config).context("Adapter config is not a valid JSON")?; - let dap_binary = ext - .call_dap_request_kind(store, &adapter_name, &config) - .await? - .map_err(|e| anyhow!("{e:?}"))?; - - Ok(Ok(dap_binary)) - } - _ => anyhow::bail!("`dap_request_kind` not available prior to v0.6.0"), - } - } - - pub async fn call_dap_config_to_scenario( - &self, - store: &mut Store, - config: ZedDebugConfig, - ) -> Result> { - match self { - Extension::V0_6_0(ext) => { - let config = config.into(); - let dap_binary = ext - .call_dap_config_to_scenario(store, &config) - .await? - .map_err(|e| anyhow!("{e:?}"))?; - - Ok(Ok(dap_binary.try_into()?)) - } - _ => anyhow::bail!("`dap_config_to_scenario` not available prior to v0.6.0"), - } - } - - pub async fn call_dap_locator_create_scenario( - &self, - store: &mut Store, - locator_name: String, - build_config_template: TaskTemplate, - resolved_label: String, - debug_adapter_name: String, - ) -> Result> { - match self { - Extension::V0_6_0(ext) => { - let build_config_template = build_config_template.into(); - let dap_binary = ext - .call_dap_locator_create_scenario( - store, - &locator_name, - &build_config_template, - &resolved_label, - &debug_adapter_name, - ) - .await?; - - Ok(dap_binary.map(TryInto::try_into).transpose()?) - } - _ => anyhow::bail!("`dap_locator_create_scenario` not available prior to v0.6.0"), - } - } - - pub async fn call_run_dap_locator( - &self, - store: &mut Store, - locator_name: String, - resolved_build_task: SpawnInTerminal, - ) -> Result> { - match self { - Extension::V0_6_0(ext) => { - let build_config_template = resolved_build_task.try_into()?; - let dap_request = ext - .call_run_dap_locator(store, &locator_name, &build_config_template) - .await? - .map_err(|e| anyhow!("{e:?}"))?; - - Ok(Ok(dap_request.into())) - } - _ => anyhow::bail!("`dap_locator_create_scenario` not available prior to v0.6.0"), - } - } -} - -trait ToWasmtimeResult { - fn to_wasmtime_result(self) -> wasmtime::Result>; -} - -impl ToWasmtimeResult for Result { - fn to_wasmtime_result(self) -> wasmtime::Result> { - Ok(self.map_err(|error| format!("{error:?}"))) - } -} diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs deleted file mode 100644 index 17d5c00a9a..0000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs +++ /dev/null @@ -1,158 +0,0 @@ -use super::latest; -use crate::wasm_host::WasmState; -use crate::wasm_host::wit::since_v0_0_4; -use anyhow::Result; -use extension::{ExtensionLanguageServerProxy, WorktreeDelegate}; -use gpui::BackgroundExecutor; -use language::BinaryStatus; -use semver::Version; -use std::sync::{Arc, OnceLock}; -use wasmtime::component::{Linker, Resource}; - -pub const MIN_VERSION: Version = Version::new(0, 0, 1); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.0.1", - with: { - "worktree": ExtensionWorktree, - "zed:extension/github": latest::zed::extension::github, - "zed:extension/platform": latest::zed::extension::platform, - }, -}); - -pub type ExtensionWorktree = Arc; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for latest::DownloadedFileType { - fn from(value: DownloadedFileType) -> Self { - match value { - DownloadedFileType::Gzip => latest::DownloadedFileType::Gzip, - DownloadedFileType::GzipTar => latest::DownloadedFileType::GzipTar, - DownloadedFileType::Zip => latest::DownloadedFileType::Zip, - DownloadedFileType::Uncompressed => latest::DownloadedFileType::Uncompressed, - } - } -} - -impl From for LanguageServerConfig { - fn from(value: since_v0_0_4::LanguageServerConfig) -> Self { - Self { - name: value.name, - language_name: value.language_name, - } - } -} - -impl From for latest::Command { - fn from(value: Command) -> Self { - Self { - command: value.command, - args: value.args, - env: value.env, - } - } -} - -impl HostWorktree for WasmState { - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - Ok(()) - } -} - -impl ExtensionImports for WasmState { - async fn node_binary_path(&mut self) -> wasmtime::Result> { - latest::nodejs::Host::node_binary_path(self).await - } - - async fn npm_package_latest_version( - &mut self, - package_name: String, - ) -> wasmtime::Result> { - latest::nodejs::Host::npm_package_latest_version(self, package_name).await - } - - async fn npm_package_installed_version( - &mut self, - package_name: String, - ) -> wasmtime::Result, String>> { - latest::nodejs::Host::npm_package_installed_version(self, package_name).await - } - - async fn npm_install_package( - &mut self, - package_name: String, - version: String, - ) -> wasmtime::Result> { - latest::nodejs::Host::npm_install_package(self, package_name, version).await - } - - async fn latest_github_release( - &mut self, - repo: String, - options: GithubReleaseOptions, - ) -> wasmtime::Result> { - latest::zed::extension::github::Host::latest_github_release(self, repo, options).await - } - - async fn current_platform(&mut self) -> Result<(Os, Architecture)> { - latest::zed::extension::platform::Host::current_platform(self).await - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - let status = match status { - LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate, - LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading, - LanguageServerInstallationStatus::Cached - | LanguageServerInstallationStatus::Downloaded => BinaryStatus::None, - LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error }, - }; - - self.host - .proxy - .update_language_server_status(lsp::LanguageServerName(server_name.into()), status); - - Ok(()) - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - latest::ExtensionImports::download_file(self, url, path, file_type.into()).await - } -} diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs deleted file mode 100644 index 11b2e9f661..0000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs +++ /dev/null @@ -1,164 +0,0 @@ -use super::latest; -use crate::wasm_host::WasmState; -use anyhow::Result; -use extension::WorktreeDelegate; -use gpui::BackgroundExecutor; -use semver::Version; -use std::sync::{Arc, OnceLock}; -use wasmtime::component::{Linker, Resource}; - -pub const MIN_VERSION: Version = Version::new(0, 0, 4); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.0.4", - with: { - "worktree": ExtensionWorktree, - "zed:extension/github": latest::zed::extension::github, - "zed:extension/platform": latest::zed::extension::platform, - }, -}); - -pub type ExtensionWorktree = Arc; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for latest::DownloadedFileType { - fn from(value: DownloadedFileType) -> Self { - match value { - DownloadedFileType::Gzip => latest::DownloadedFileType::Gzip, - DownloadedFileType::GzipTar => latest::DownloadedFileType::GzipTar, - DownloadedFileType::Zip => latest::DownloadedFileType::Zip, - DownloadedFileType::Uncompressed => latest::DownloadedFileType::Uncompressed, - } - } -} - -impl From for latest::LanguageServerInstallationStatus { - fn from(value: LanguageServerInstallationStatus) -> Self { - match value { - LanguageServerInstallationStatus::None => { - latest::LanguageServerInstallationStatus::None - } - LanguageServerInstallationStatus::Downloading => { - latest::LanguageServerInstallationStatus::Downloading - } - LanguageServerInstallationStatus::CheckingForUpdate => { - latest::LanguageServerInstallationStatus::CheckingForUpdate - } - LanguageServerInstallationStatus::Failed(error) => { - latest::LanguageServerInstallationStatus::Failed(error) - } - } - } -} - -impl From for latest::Command { - fn from(value: Command) -> Self { - Self { - command: value.command, - args: value.args, - env: value.env, - } - } -} - -impl HostWorktree for WasmState { - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -impl ExtensionImports for WasmState { - async fn node_binary_path(&mut self) -> wasmtime::Result> { - latest::nodejs::Host::node_binary_path(self).await - } - - async fn npm_package_latest_version( - &mut self, - package_name: String, - ) -> wasmtime::Result> { - latest::nodejs::Host::npm_package_latest_version(self, package_name).await - } - - async fn npm_package_installed_version( - &mut self, - package_name: String, - ) -> wasmtime::Result, String>> { - latest::nodejs::Host::npm_package_installed_version(self, package_name).await - } - - async fn npm_install_package( - &mut self, - package_name: String, - version: String, - ) -> wasmtime::Result> { - latest::nodejs::Host::npm_install_package(self, package_name, version).await - } - - async fn latest_github_release( - &mut self, - repo: String, - options: GithubReleaseOptions, - ) -> wasmtime::Result> { - latest::zed::extension::github::Host::latest_github_release(self, repo, options).await - } - - async fn current_platform(&mut self) -> Result<(Os, Architecture)> { - latest::zed::extension::platform::Host::current_platform(self).await - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - latest::ExtensionImports::set_language_server_installation_status( - self, - server_name, - status.into(), - ) - .await - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - latest::ExtensionImports::download_file(self, url, path, file_type.into()).await - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - latest::ExtensionImports::make_file_executable(self, path).await - } -} diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs deleted file mode 100644 index 835a2b30fb..0000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs +++ /dev/null @@ -1,197 +0,0 @@ -use super::{latest, since_v0_1_0}; -use crate::wasm_host::WasmState; -use anyhow::Result; -use extension::WorktreeDelegate; -use gpui::BackgroundExecutor; -use semver::Version; -use std::sync::{Arc, OnceLock}; -use wasmtime::component::{Linker, Resource}; - -pub const MIN_VERSION: Version = Version::new(0, 0, 6); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.0.6", - with: { - "worktree": ExtensionWorktree, - "zed:extension/github": latest::zed::extension::github, - "zed:extension/lsp": since_v0_1_0::zed::extension::lsp, - "zed:extension/nodejs": latest::zed::extension::nodejs, - "zed:extension/platform": latest::zed::extension::platform, - }, -}); - -mod settings { - #![allow(dead_code)] - include!(concat!(env!("OUT_DIR"), "/since_v0.0.6/settings.rs")); -} - -pub type ExtensionWorktree = Arc; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for latest::Command { - fn from(value: Command) -> Self { - Self { - command: value.command, - args: value.args, - env: value.env, - } - } -} - -impl From for latest::SettingsLocation { - fn from(value: SettingsLocation) -> Self { - Self { - worktree_id: value.worktree_id, - path: value.path, - } - } -} - -impl From for latest::LanguageServerInstallationStatus { - fn from(value: LanguageServerInstallationStatus) -> Self { - match value { - LanguageServerInstallationStatus::None => Self::None, - LanguageServerInstallationStatus::Downloading => Self::Downloading, - LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate, - LanguageServerInstallationStatus::Failed(message) => Self::Failed(message), - } - } -} - -impl From for latest::DownloadedFileType { - fn from(value: DownloadedFileType) -> Self { - match value { - DownloadedFileType::Gzip => Self::Gzip, - DownloadedFileType::GzipTar => Self::GzipTar, - DownloadedFileType::Zip => Self::Zip, - DownloadedFileType::Uncompressed => Self::Uncompressed, - } - } -} - -impl From for latest::Range { - fn from(value: Range) -> Self { - Self { - start: value.start, - end: value.end, - } - } -} - -impl From for latest::CodeLabelSpan { - fn from(value: CodeLabelSpan) -> Self { - match value { - CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()), - CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), - } - } -} - -impl From for latest::CodeLabelSpanLiteral { - fn from(value: CodeLabelSpanLiteral) -> Self { - Self { - text: value.text, - highlight_name: value.highlight_name, - } - } -} - -impl From for latest::CodeLabel { - fn from(value: CodeLabel) -> Self { - Self { - code: value.code, - spans: value.spans.into_iter().map(Into::into).collect(), - filter_range: value.filter_range.into(), - } - } -} - -impl HostWorktree for WasmState { - async fn id(&mut self, delegate: Resource>) -> wasmtime::Result { - latest::HostWorktree::id(self, delegate).await - } - - async fn root_path( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::root_path(self, delegate).await - } - - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -impl ExtensionImports for WasmState { - async fn get_settings( - &mut self, - location: Option, - category: String, - key: Option, - ) -> wasmtime::Result> { - latest::ExtensionImports::get_settings( - self, - location.map(|location| location.into()), - category, - key, - ) - .await - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - latest::ExtensionImports::set_language_server_installation_status( - self, - server_name, - status.into(), - ) - .await - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - latest::ExtensionImports::download_file(self, url, path, file_type.into()).await - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - latest::ExtensionImports::make_file_executable(self, path).await - } -} diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs deleted file mode 100644 index a7a20f6dc7..0000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs +++ /dev/null @@ -1,575 +0,0 @@ -use crate::wasm_host::{WasmState, wit::ToWasmtimeResult}; -use ::http_client::{AsyncBody, HttpRequestExt}; -use ::settings::{Settings, WorktreeId}; -use anyhow::{Context as _, Result, bail}; -use async_compression::futures::bufread::GzipDecoder; -use async_tar::Archive; -use extension::{ExtensionLanguageServerProxy, KeyValueStoreDelegate, WorktreeDelegate}; -use futures::{AsyncReadExt, lock::Mutex}; -use futures::{FutureExt as _, io::BufReader}; -use gpui::BackgroundExecutor; -use language::LanguageName; -use language::{BinaryStatus, language_settings::AllLanguageSettings}; -use project::project_settings::ProjectSettings; -use semver::Version; -use std::{ - path::{Path, PathBuf}, - sync::{Arc, OnceLock}, -}; -use util::paths::PathStyle; -use util::rel_path::RelPath; -use util::{archive::extract_zip, fs::make_file_executable, maybe}; -use wasmtime::component::{Linker, Resource}; - -use super::latest; - -pub const MIN_VERSION: Version = Version::new(0, 1, 0); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.1.0", - with: { - "worktree": ExtensionWorktree, - "key-value-store": ExtensionKeyValueStore, - "zed:extension/http-client/http-response-stream": ExtensionHttpResponseStream, - "zed:extension/github": latest::zed::extension::github, - "zed:extension/nodejs": latest::zed::extension::nodejs, - "zed:extension/platform": latest::zed::extension::platform, - "zed:extension/slash-command": latest::zed::extension::slash_command, - }, -}); - -pub use self::zed::extension::*; - -mod settings { - include!(concat!(env!("OUT_DIR"), "/since_v0.1.0/settings.rs")); -} - -pub type ExtensionWorktree = Arc; -pub type ExtensionKeyValueStore = Arc; -pub type ExtensionHttpResponseStream = Arc>>; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for latest::Command { - fn from(value: Command) -> Self { - Self { - command: value.command, - args: value.args, - env: value.env, - } - } -} - -impl From for latest::SettingsLocation { - fn from(value: SettingsLocation) -> Self { - Self { - worktree_id: value.worktree_id, - path: value.path, - } - } -} - -impl From for latest::LanguageServerInstallationStatus { - fn from(value: LanguageServerInstallationStatus) -> Self { - match value { - LanguageServerInstallationStatus::None => Self::None, - LanguageServerInstallationStatus::Downloading => Self::Downloading, - LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate, - LanguageServerInstallationStatus::Failed(message) => Self::Failed(message), - } - } -} - -impl From for latest::DownloadedFileType { - fn from(value: DownloadedFileType) -> Self { - match value { - DownloadedFileType::Gzip => Self::Gzip, - DownloadedFileType::GzipTar => Self::GzipTar, - DownloadedFileType::Zip => Self::Zip, - DownloadedFileType::Uncompressed => Self::Uncompressed, - } - } -} - -impl From for latest::Range { - fn from(value: Range) -> Self { - Self { - start: value.start, - end: value.end, - } - } -} - -impl From for latest::CodeLabelSpan { - fn from(value: CodeLabelSpan) -> Self { - match value { - CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()), - CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), - } - } -} - -impl From for latest::CodeLabelSpanLiteral { - fn from(value: CodeLabelSpanLiteral) -> Self { - Self { - text: value.text, - highlight_name: value.highlight_name, - } - } -} - -impl From for latest::CodeLabel { - fn from(value: CodeLabel) -> Self { - Self { - code: value.code, - spans: value.spans.into_iter().map(Into::into).collect(), - filter_range: value.filter_range.into(), - } - } -} - -impl From for Completion { - fn from(value: latest::Completion) -> Self { - Self { - label: value.label, - detail: value.detail, - kind: value.kind.map(Into::into), - insert_text_format: value.insert_text_format.map(Into::into), - } - } -} - -impl From for lsp::CompletionKind { - fn from(value: latest::lsp::CompletionKind) -> Self { - match value { - latest::lsp::CompletionKind::Text => Self::Text, - latest::lsp::CompletionKind::Method => Self::Method, - latest::lsp::CompletionKind::Function => Self::Function, - latest::lsp::CompletionKind::Constructor => Self::Constructor, - latest::lsp::CompletionKind::Field => Self::Field, - latest::lsp::CompletionKind::Variable => Self::Variable, - latest::lsp::CompletionKind::Class => Self::Class, - latest::lsp::CompletionKind::Interface => Self::Interface, - latest::lsp::CompletionKind::Module => Self::Module, - latest::lsp::CompletionKind::Property => Self::Property, - latest::lsp::CompletionKind::Unit => Self::Unit, - latest::lsp::CompletionKind::Value => Self::Value, - latest::lsp::CompletionKind::Enum => Self::Enum, - latest::lsp::CompletionKind::Keyword => Self::Keyword, - latest::lsp::CompletionKind::Snippet => Self::Snippet, - latest::lsp::CompletionKind::Color => Self::Color, - latest::lsp::CompletionKind::File => Self::File, - latest::lsp::CompletionKind::Reference => Self::Reference, - latest::lsp::CompletionKind::Folder => Self::Folder, - latest::lsp::CompletionKind::EnumMember => Self::EnumMember, - latest::lsp::CompletionKind::Constant => Self::Constant, - latest::lsp::CompletionKind::Struct => Self::Struct, - latest::lsp::CompletionKind::Event => Self::Event, - latest::lsp::CompletionKind::Operator => Self::Operator, - latest::lsp::CompletionKind::TypeParameter => Self::TypeParameter, - latest::lsp::CompletionKind::Other(kind) => Self::Other(kind), - } - } -} - -impl From for lsp::InsertTextFormat { - fn from(value: latest::lsp::InsertTextFormat) -> Self { - match value { - latest::lsp::InsertTextFormat::PlainText => Self::PlainText, - latest::lsp::InsertTextFormat::Snippet => Self::Snippet, - latest::lsp::InsertTextFormat::Other(value) => Self::Other(value), - } - } -} - -impl From for lsp::Symbol { - fn from(value: latest::lsp::Symbol) -> Self { - Self { - name: value.name, - kind: value.kind.into(), - } - } -} - -impl From for lsp::SymbolKind { - fn from(value: latest::lsp::SymbolKind) -> Self { - match value { - latest::lsp::SymbolKind::File => Self::File, - latest::lsp::SymbolKind::Module => Self::Module, - latest::lsp::SymbolKind::Namespace => Self::Namespace, - latest::lsp::SymbolKind::Package => Self::Package, - latest::lsp::SymbolKind::Class => Self::Class, - latest::lsp::SymbolKind::Method => Self::Method, - latest::lsp::SymbolKind::Property => Self::Property, - latest::lsp::SymbolKind::Field => Self::Field, - latest::lsp::SymbolKind::Constructor => Self::Constructor, - latest::lsp::SymbolKind::Enum => Self::Enum, - latest::lsp::SymbolKind::Interface => Self::Interface, - latest::lsp::SymbolKind::Function => Self::Function, - latest::lsp::SymbolKind::Variable => Self::Variable, - latest::lsp::SymbolKind::Constant => Self::Constant, - latest::lsp::SymbolKind::String => Self::String, - latest::lsp::SymbolKind::Number => Self::Number, - latest::lsp::SymbolKind::Boolean => Self::Boolean, - latest::lsp::SymbolKind::Array => Self::Array, - latest::lsp::SymbolKind::Object => Self::Object, - latest::lsp::SymbolKind::Key => Self::Key, - latest::lsp::SymbolKind::Null => Self::Null, - latest::lsp::SymbolKind::EnumMember => Self::EnumMember, - latest::lsp::SymbolKind::Struct => Self::Struct, - latest::lsp::SymbolKind::Event => Self::Event, - latest::lsp::SymbolKind::Operator => Self::Operator, - latest::lsp::SymbolKind::TypeParameter => Self::TypeParameter, - latest::lsp::SymbolKind::Other(kind) => Self::Other(kind), - } - } -} - -impl HostKeyValueStore for WasmState { - async fn insert( - &mut self, - kv_store: Resource, - key: String, - value: String, - ) -> wasmtime::Result> { - let kv_store = self.table.get(&kv_store)?; - kv_store.insert(key, value).await.to_wasmtime_result() - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of key-value stores. - Ok(()) - } -} - -impl HostWorktree for WasmState { - async fn id(&mut self, delegate: Resource>) -> wasmtime::Result { - latest::HostWorktree::id(self, delegate).await - } - - async fn root_path( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::root_path(self, delegate).await - } - - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -impl common::Host for WasmState {} - -impl http_client::Host for WasmState { - async fn fetch( - &mut self, - request: http_client::HttpRequest, - ) -> wasmtime::Result> { - maybe!(async { - let url = &request.url; - let request = convert_request(&request)?; - let mut response = self.host.http_client.send(request).await?; - - if response.status().is_client_error() || response.status().is_server_error() { - bail!("failed to fetch '{url}': status code {}", response.status()) - } - convert_response(&mut response).await - }) - .await - .to_wasmtime_result() - } - - async fn fetch_stream( - &mut self, - request: http_client::HttpRequest, - ) -> wasmtime::Result, String>> { - let request = convert_request(&request)?; - let response = self.host.http_client.send(request); - maybe!(async { - let response = response.await?; - let stream = Arc::new(Mutex::new(response)); - let resource = self.table.push(stream)?; - Ok(resource) - }) - .await - .to_wasmtime_result() - } -} - -impl http_client::HostHttpResponseStream for WasmState { - async fn next_chunk( - &mut self, - resource: Resource, - ) -> wasmtime::Result>, String>> { - let stream = self.table.get(&resource)?.clone(); - maybe!(async move { - let mut response = stream.lock().await; - let mut buffer = vec![0; 8192]; // 8KB buffer - let bytes_read = response.body_mut().read(&mut buffer).await?; - if bytes_read == 0 { - Ok(None) - } else { - buffer.truncate(bytes_read); - Ok(Some(buffer)) - } - }) - .await - .to_wasmtime_result() - } - - async fn drop(&mut self, _resource: Resource) -> Result<()> { - Ok(()) - } -} - -impl From for ::http_client::Method { - fn from(value: http_client::HttpMethod) -> Self { - match value { - http_client::HttpMethod::Get => Self::GET, - http_client::HttpMethod::Post => Self::POST, - http_client::HttpMethod::Put => Self::PUT, - http_client::HttpMethod::Delete => Self::DELETE, - http_client::HttpMethod::Head => Self::HEAD, - http_client::HttpMethod::Options => Self::OPTIONS, - http_client::HttpMethod::Patch => Self::PATCH, - } - } -} - -fn convert_request( - extension_request: &http_client::HttpRequest, -) -> anyhow::Result<::http_client::Request> { - let mut request = ::http_client::Request::builder() - .method(::http_client::Method::from(extension_request.method)) - .uri(&extension_request.url) - .follow_redirects(match extension_request.redirect_policy { - http_client::RedirectPolicy::NoFollow => ::http_client::RedirectPolicy::NoFollow, - http_client::RedirectPolicy::FollowLimit(limit) => { - ::http_client::RedirectPolicy::FollowLimit(limit) - } - http_client::RedirectPolicy::FollowAll => ::http_client::RedirectPolicy::FollowAll, - }); - for (key, value) in &extension_request.headers { - request = request.header(key, value); - } - let body = extension_request - .body - .clone() - .map(AsyncBody::from) - .unwrap_or_default(); - request.body(body).map_err(anyhow::Error::from) -} - -async fn convert_response( - response: &mut ::http_client::Response, -) -> anyhow::Result { - let mut extension_response = http_client::HttpResponse { - body: Vec::new(), - headers: Vec::new(), - }; - - for (key, value) in response.headers() { - extension_response - .headers - .push((key.to_string(), value.to_str().unwrap_or("").to_string())); - } - - response - .body_mut() - .read_to_end(&mut extension_response.body) - .await?; - - Ok(extension_response) -} - -impl lsp::Host for WasmState {} - -impl ExtensionImports for WasmState { - async fn get_settings( - &mut self, - location: Option, - category: String, - key: Option, - ) -> wasmtime::Result> { - self.on_main_thread(|cx| { - async move { - let path = location.as_ref().and_then(|location| { - RelPath::new(Path::new(&location.path), PathStyle::Posix).ok() - }); - let location = path - .as_ref() - .zip(location.as_ref()) - .map(|(path, location)| ::settings::SettingsLocation { - worktree_id: WorktreeId::from_proto(location.worktree_id), - path, - }); - - cx.update(|cx| match category.as_str() { - "language" => { - let key = key.map(|k| LanguageName::new(&k)); - let settings = AllLanguageSettings::get(location, cx).language( - location, - key.as_ref(), - cx, - ); - Ok(serde_json::to_string(&settings::LanguageSettings { - tab_size: settings.tab_size, - })?) - } - "lsp" => { - let settings = key - .and_then(|key| { - ProjectSettings::get(location, cx) - .lsp - .get(&::lsp::LanguageServerName(key.into())) - }) - .cloned() - .unwrap_or_default(); - Ok(serde_json::to_string(&settings::LspSettings { - binary: settings.binary.map(|binary| settings::BinarySettings { - path: binary.path, - arguments: binary.arguments, - }), - settings: settings.settings, - initialization_options: settings.initialization_options, - })?) - } - _ => { - bail!("Unknown settings category: {}", category); - } - }) - } - .boxed_local() - }) - .await? - .to_wasmtime_result() - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - let status = match status { - LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate, - LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading, - LanguageServerInstallationStatus::None => BinaryStatus::None, - LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error }, - }; - - self.host - .proxy - .update_language_server_status(::lsp::LanguageServerName(server_name.into()), status); - - Ok(()) - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - maybe!(async { - let path = PathBuf::from(path); - let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref()); - - self.host.fs.create_dir(&extension_work_dir).await?; - - let destination_path = self - .host - .writeable_path_from_extension(&self.manifest.id, &path)?; - - let mut response = self - .host - .http_client - .get(&url, Default::default(), true) - .await - .context("downloading release")?; - - anyhow::ensure!( - response.status().is_success(), - "download failed with status {}", - response.status() - ); - let body = BufReader::new(response.body_mut()); - - match file_type { - DownloadedFileType::Uncompressed => { - futures::pin_mut!(body); - self.host - .fs - .create_file_with(&destination_path, body) - .await?; - } - DownloadedFileType::Gzip => { - let body = GzipDecoder::new(body); - futures::pin_mut!(body); - self.host - .fs - .create_file_with(&destination_path, body) - .await?; - } - DownloadedFileType::GzipTar => { - let body = GzipDecoder::new(body); - futures::pin_mut!(body); - self.host - .fs - .extract_tar_file(&destination_path, Archive::new(body)) - .await?; - } - DownloadedFileType::Zip => { - futures::pin_mut!(body); - extract_zip(&destination_path, body) - .await - .with_context(|| format!("unzipping {path:?} archive"))?; - } - } - - Ok(()) - }) - .await - .to_wasmtime_result() - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - let path = self - .host - .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?; - - make_file_executable(&path) - .await - .with_context(|| format!("setting permissions for path {path:?}")) - .to_wasmtime_result() - } -} diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs deleted file mode 100644 index 05e3f5a4e7..0000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs +++ /dev/null @@ -1,238 +0,0 @@ -use crate::wasm_host::WasmState; -use anyhow::Result; -use extension::{KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate}; -use gpui::BackgroundExecutor; -use semver::Version; -use std::sync::{Arc, OnceLock}; -use wasmtime::component::{Linker, Resource}; - -use super::latest; - -pub const MIN_VERSION: Version = Version::new(0, 2, 0); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.2.0", - with: { - "worktree": ExtensionWorktree, - "project": ExtensionProject, - "key-value-store": ExtensionKeyValueStore, - "zed:extension/github": latest::zed::extension::github, - "zed:extension/http-client": latest::zed::extension::http_client, - "zed:extension/lsp": latest::zed::extension::lsp, - "zed:extension/nodejs": latest::zed::extension::nodejs, - "zed:extension/platform": latest::zed::extension::platform, - "zed:extension/slash-command": latest::zed::extension::slash_command, - }, -}); - -pub use self::zed::extension::*; - -mod settings { - #![allow(dead_code)] - include!(concat!(env!("OUT_DIR"), "/since_v0.2.0/settings.rs")); -} - -pub type ExtensionWorktree = Arc; -pub type ExtensionProject = Arc; -pub type ExtensionKeyValueStore = Arc; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for latest::Command { - fn from(value: Command) -> Self { - Self { - command: value.command, - args: value.args, - env: value.env, - } - } -} - -impl From for latest::SettingsLocation { - fn from(value: SettingsLocation) -> Self { - Self { - worktree_id: value.worktree_id, - path: value.path, - } - } -} - -impl From for latest::LanguageServerInstallationStatus { - fn from(value: LanguageServerInstallationStatus) -> Self { - match value { - LanguageServerInstallationStatus::None => Self::None, - LanguageServerInstallationStatus::Downloading => Self::Downloading, - LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate, - LanguageServerInstallationStatus::Failed(message) => Self::Failed(message), - } - } -} - -impl From for latest::DownloadedFileType { - fn from(value: DownloadedFileType) -> Self { - match value { - DownloadedFileType::Gzip => Self::Gzip, - DownloadedFileType::GzipTar => Self::GzipTar, - DownloadedFileType::Zip => Self::Zip, - DownloadedFileType::Uncompressed => Self::Uncompressed, - } - } -} - -impl From for latest::Range { - fn from(value: Range) -> Self { - Self { - start: value.start, - end: value.end, - } - } -} - -impl From for latest::CodeLabelSpan { - fn from(value: CodeLabelSpan) -> Self { - match value { - CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()), - CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), - } - } -} - -impl From for latest::CodeLabelSpanLiteral { - fn from(value: CodeLabelSpanLiteral) -> Self { - Self { - text: value.text, - highlight_name: value.highlight_name, - } - } -} - -impl From for latest::CodeLabel { - fn from(value: CodeLabel) -> Self { - Self { - code: value.code, - spans: value.spans.into_iter().map(Into::into).collect(), - filter_range: value.filter_range.into(), - } - } -} - -impl HostKeyValueStore for WasmState { - async fn insert( - &mut self, - kv_store: Resource, - key: String, - value: String, - ) -> wasmtime::Result> { - latest::HostKeyValueStore::insert(self, kv_store, key, value).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of key-value stores. - Ok(()) - } -} - -impl HostProject for WasmState { - async fn worktree_ids( - &mut self, - project: Resource, - ) -> wasmtime::Result> { - latest::HostProject::worktree_ids(self, project).await - } - - async fn drop(&mut self, _project: Resource) -> Result<()> { - // We only ever hand out borrows of projects. - Ok(()) - } -} - -impl HostWorktree for WasmState { - async fn id(&mut self, delegate: Resource>) -> wasmtime::Result { - latest::HostWorktree::id(self, delegate).await - } - - async fn root_path( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::root_path(self, delegate).await - } - - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -impl common::Host for WasmState {} - -impl ExtensionImports for WasmState { - async fn get_settings( - &mut self, - location: Option, - category: String, - key: Option, - ) -> wasmtime::Result> { - latest::ExtensionImports::get_settings( - self, - location.map(|location| location.into()), - category, - key, - ) - .await - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - latest::ExtensionImports::set_language_server_installation_status( - self, - server_name, - status.into(), - ) - .await - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - latest::ExtensionImports::download_file(self, url, path, file_type.into()).await - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - latest::ExtensionImports::make_file_executable(self, path).await - } -} diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs deleted file mode 100644 index 08393934fe..0000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs +++ /dev/null @@ -1,217 +0,0 @@ -use crate::wasm_host::WasmState; -use anyhow::Result; -use extension::{KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate}; -use gpui::BackgroundExecutor; -use semver::Version; -use std::sync::{Arc, OnceLock}; -use wasmtime::component::{Linker, Resource}; - -use super::latest; - -pub const MIN_VERSION: Version = Version::new(0, 3, 0); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.3.0", - with: { - "worktree": ExtensionWorktree, - "project": ExtensionProject, - "key-value-store": ExtensionKeyValueStore, - "zed:extension/common": latest::zed::extension::common, - "zed:extension/github": latest::zed::extension::github, - "zed:extension/http-client": latest::zed::extension::http_client, - "zed:extension/lsp": latest::zed::extension::lsp, - "zed:extension/nodejs": latest::zed::extension::nodejs, - "zed:extension/platform": latest::zed::extension::platform, - "zed:extension/process": latest::zed::extension::process, - "zed:extension/slash-command": latest::zed::extension::slash_command, - }, -}); - -mod settings { - #![allow(dead_code)] - include!(concat!(env!("OUT_DIR"), "/since_v0.3.0/settings.rs")); -} - -pub type ExtensionWorktree = Arc; -pub type ExtensionProject = Arc; -pub type ExtensionKeyValueStore = Arc; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for latest::CodeLabel { - fn from(value: CodeLabel) -> Self { - Self { - code: value.code, - spans: value.spans.into_iter().map(Into::into).collect(), - filter_range: value.filter_range, - } - } -} - -impl From for latest::CodeLabelSpan { - fn from(value: CodeLabelSpan) -> Self { - match value { - CodeLabelSpan::CodeRange(range) => Self::CodeRange(range), - CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), - } - } -} - -impl From for latest::CodeLabelSpanLiteral { - fn from(value: CodeLabelSpanLiteral) -> Self { - Self { - text: value.text, - highlight_name: value.highlight_name, - } - } -} - -impl From for latest::SettingsLocation { - fn from(value: SettingsLocation) -> Self { - Self { - worktree_id: value.worktree_id, - path: value.path, - } - } -} - -impl From for latest::LanguageServerInstallationStatus { - fn from(value: LanguageServerInstallationStatus) -> Self { - match value { - LanguageServerInstallationStatus::None => Self::None, - LanguageServerInstallationStatus::Downloading => Self::Downloading, - LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate, - LanguageServerInstallationStatus::Failed(message) => Self::Failed(message), - } - } -} - -impl From for latest::DownloadedFileType { - fn from(value: DownloadedFileType) -> Self { - match value { - DownloadedFileType::Gzip => Self::Gzip, - DownloadedFileType::GzipTar => Self::GzipTar, - DownloadedFileType::Zip => Self::Zip, - DownloadedFileType::Uncompressed => Self::Uncompressed, - } - } -} - -impl HostKeyValueStore for WasmState { - async fn insert( - &mut self, - kv_store: Resource, - key: String, - value: String, - ) -> wasmtime::Result> { - latest::HostKeyValueStore::insert(self, kv_store, key, value).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of key-value stores. - Ok(()) - } -} - -impl HostProject for WasmState { - async fn worktree_ids( - &mut self, - project: Resource, - ) -> wasmtime::Result> { - latest::HostProject::worktree_ids(self, project).await - } - - async fn drop(&mut self, _project: Resource) -> Result<()> { - // We only ever hand out borrows of projects. - Ok(()) - } -} - -impl HostWorktree for WasmState { - async fn id(&mut self, delegate: Resource>) -> wasmtime::Result { - latest::HostWorktree::id(self, delegate).await - } - - async fn root_path( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::root_path(self, delegate).await - } - - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -impl ExtensionImports for WasmState { - async fn get_settings( - &mut self, - location: Option, - category: String, - key: Option, - ) -> wasmtime::Result> { - latest::ExtensionImports::get_settings( - self, - location.map(|location| location.into()), - category, - key, - ) - .await - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - latest::ExtensionImports::set_language_server_installation_status( - self, - server_name, - status.into(), - ) - .await - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - latest::ExtensionImports::download_file(self, url, path, file_type.into()).await - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - latest::ExtensionImports::make_file_executable(self, path).await - } -} diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_4_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_4_0.rs deleted file mode 100644 index 1b2a95023b..0000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_4_0.rs +++ /dev/null @@ -1,217 +0,0 @@ -use crate::wasm_host::WasmState; -use anyhow::Result; -use extension::{KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate}; -use gpui::BackgroundExecutor; -use semver::Version; -use std::sync::{Arc, OnceLock}; -use wasmtime::component::{Linker, Resource}; - -use super::latest; - -pub const MIN_VERSION: Version = Version::new(0, 4, 0); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.4.0", - with: { - "worktree": ExtensionWorktree, - "project": ExtensionProject, - "key-value-store": ExtensionKeyValueStore, - "zed:extension/common": latest::zed::extension::common, - "zed:extension/github": latest::zed::extension::github, - "zed:extension/http-client": latest::zed::extension::http_client, - "zed:extension/lsp": latest::zed::extension::lsp, - "zed:extension/nodejs": latest::zed::extension::nodejs, - "zed:extension/platform": latest::zed::extension::platform, - "zed:extension/process": latest::zed::extension::process, - "zed:extension/slash-command": latest::zed::extension::slash_command, - }, -}); - -mod settings { - #![allow(dead_code)] - include!(concat!(env!("OUT_DIR"), "/since_v0.4.0/settings.rs")); -} - -pub type ExtensionWorktree = Arc; -pub type ExtensionProject = Arc; -pub type ExtensionKeyValueStore = Arc; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for latest::CodeLabel { - fn from(value: CodeLabel) -> Self { - Self { - code: value.code, - spans: value.spans.into_iter().map(Into::into).collect(), - filter_range: value.filter_range, - } - } -} - -impl From for latest::CodeLabelSpan { - fn from(value: CodeLabelSpan) -> Self { - match value { - CodeLabelSpan::CodeRange(range) => Self::CodeRange(range), - CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), - } - } -} - -impl From for latest::CodeLabelSpanLiteral { - fn from(value: CodeLabelSpanLiteral) -> Self { - Self { - text: value.text, - highlight_name: value.highlight_name, - } - } -} - -impl From for latest::SettingsLocation { - fn from(value: SettingsLocation) -> Self { - Self { - worktree_id: value.worktree_id, - path: value.path, - } - } -} - -impl From for latest::LanguageServerInstallationStatus { - fn from(value: LanguageServerInstallationStatus) -> Self { - match value { - LanguageServerInstallationStatus::None => Self::None, - LanguageServerInstallationStatus::Downloading => Self::Downloading, - LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate, - LanguageServerInstallationStatus::Failed(message) => Self::Failed(message), - } - } -} - -impl From for latest::DownloadedFileType { - fn from(value: DownloadedFileType) -> Self { - match value { - DownloadedFileType::Gzip => Self::Gzip, - DownloadedFileType::GzipTar => Self::GzipTar, - DownloadedFileType::Zip => Self::Zip, - DownloadedFileType::Uncompressed => Self::Uncompressed, - } - } -} - -impl HostKeyValueStore for WasmState { - async fn insert( - &mut self, - kv_store: Resource, - key: String, - value: String, - ) -> wasmtime::Result> { - latest::HostKeyValueStore::insert(self, kv_store, key, value).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of key-value stores. - Ok(()) - } -} - -impl HostProject for WasmState { - async fn worktree_ids( - &mut self, - project: Resource, - ) -> wasmtime::Result> { - latest::HostProject::worktree_ids(self, project).await - } - - async fn drop(&mut self, _project: Resource) -> Result<()> { - // We only ever hand out borrows of projects. - Ok(()) - } -} - -impl HostWorktree for WasmState { - async fn id(&mut self, delegate: Resource>) -> wasmtime::Result { - latest::HostWorktree::id(self, delegate).await - } - - async fn root_path( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::root_path(self, delegate).await - } - - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -impl ExtensionImports for WasmState { - async fn get_settings( - &mut self, - location: Option, - category: String, - key: Option, - ) -> wasmtime::Result> { - latest::ExtensionImports::get_settings( - self, - location.map(|location| location.into()), - category, - key, - ) - .await - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - latest::ExtensionImports::set_language_server_installation_status( - self, - server_name, - status.into(), - ) - .await - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - latest::ExtensionImports::download_file(self, url, path, file_type.into()).await - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - latest::ExtensionImports::make_file_executable(self, path).await - } -} diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_5_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_5_0.rs deleted file mode 100644 index 23701c9d03..0000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_5_0.rs +++ /dev/null @@ -1,218 +0,0 @@ -use crate::wasm_host::WasmState; -use anyhow::Result; -use extension::{KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate}; -use gpui::BackgroundExecutor; -use semver::Version; -use std::sync::{Arc, OnceLock}; -use wasmtime::component::{Linker, Resource}; - -use super::latest; - -pub const MIN_VERSION: Version = Version::new(0, 5, 0); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.5.0", - with: { - "worktree": ExtensionWorktree, - "project": ExtensionProject, - "key-value-store": ExtensionKeyValueStore, - "zed:extension/common": latest::zed::extension::common, - "zed:extension/github": latest::zed::extension::github, - "zed:extension/http-client": latest::zed::extension::http_client, - "zed:extension/lsp": latest::zed::extension::lsp, - "zed:extension/nodejs": latest::zed::extension::nodejs, - "zed:extension/platform": latest::zed::extension::platform, - "zed:extension/process": latest::zed::extension::process, - "zed:extension/slash-command": latest::zed::extension::slash_command, - "zed:extension/context-server": latest::zed::extension::context_server, - }, -}); - -mod settings { - #![allow(dead_code)] - include!(concat!(env!("OUT_DIR"), "/since_v0.5.0/settings.rs")); -} - -pub type ExtensionWorktree = Arc; -pub type ExtensionProject = Arc; -pub type ExtensionKeyValueStore = Arc; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for latest::CodeLabel { - fn from(value: CodeLabel) -> Self { - Self { - code: value.code, - spans: value.spans.into_iter().map(Into::into).collect(), - filter_range: value.filter_range, - } - } -} - -impl From for latest::CodeLabelSpan { - fn from(value: CodeLabelSpan) -> Self { - match value { - CodeLabelSpan::CodeRange(range) => Self::CodeRange(range), - CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), - } - } -} - -impl From for latest::CodeLabelSpanLiteral { - fn from(value: CodeLabelSpanLiteral) -> Self { - Self { - text: value.text, - highlight_name: value.highlight_name, - } - } -} - -impl From for latest::SettingsLocation { - fn from(value: SettingsLocation) -> Self { - Self { - worktree_id: value.worktree_id, - path: value.path, - } - } -} - -impl From for latest::LanguageServerInstallationStatus { - fn from(value: LanguageServerInstallationStatus) -> Self { - match value { - LanguageServerInstallationStatus::None => Self::None, - LanguageServerInstallationStatus::Downloading => Self::Downloading, - LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate, - LanguageServerInstallationStatus::Failed(message) => Self::Failed(message), - } - } -} - -impl From for latest::DownloadedFileType { - fn from(value: DownloadedFileType) -> Self { - match value { - DownloadedFileType::Gzip => Self::Gzip, - DownloadedFileType::GzipTar => Self::GzipTar, - DownloadedFileType::Zip => Self::Zip, - DownloadedFileType::Uncompressed => Self::Uncompressed, - } - } -} - -impl HostKeyValueStore for WasmState { - async fn insert( - &mut self, - kv_store: Resource, - key: String, - value: String, - ) -> wasmtime::Result> { - latest::HostKeyValueStore::insert(self, kv_store, key, value).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of key-value stores. - Ok(()) - } -} - -impl HostProject for WasmState { - async fn worktree_ids( - &mut self, - project: Resource, - ) -> wasmtime::Result> { - latest::HostProject::worktree_ids(self, project).await - } - - async fn drop(&mut self, _project: Resource) -> Result<()> { - // We only ever hand out borrows of projects. - Ok(()) - } -} - -impl HostWorktree for WasmState { - async fn id(&mut self, delegate: Resource>) -> wasmtime::Result { - latest::HostWorktree::id(self, delegate).await - } - - async fn root_path( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::root_path(self, delegate).await - } - - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -impl ExtensionImports for WasmState { - async fn get_settings( - &mut self, - location: Option, - category: String, - key: Option, - ) -> wasmtime::Result> { - latest::ExtensionImports::get_settings( - self, - location.map(|location| location.into()), - category, - key, - ) - .await - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - latest::ExtensionImports::set_language_server_installation_status( - self, - server_name, - status.into(), - ) - .await - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - latest::ExtensionImports::download_file(self, url, path, file_type.into()).await - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - latest::ExtensionImports::make_file_executable(self, path).await - } -} 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 deleted file mode 100644 index 8595c278b9..0000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs +++ /dev/null @@ -1,222 +0,0 @@ -use crate::wasm_host::WasmState; -use anyhow::Result; -use extension::{KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate}; -use gpui::BackgroundExecutor; -use semver::Version; -use std::sync::{Arc, OnceLock}; -use wasmtime::component::{Linker, Resource}; - -use super::latest; - -pub const MIN_VERSION: Version = Version::new(0, 6, 0); -pub const MAX_VERSION: Version = Version::new(0, 7, 0); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.6.0", - with: { - "worktree": ExtensionWorktree, - "project": ExtensionProject, - "key-value-store": ExtensionKeyValueStore, - "zed:extension/common": latest::zed::extension::common, - "zed:extension/github": latest::zed::extension::github, - "zed:extension/http-client": latest::zed::extension::http_client, - "zed:extension/lsp": latest::zed::extension::lsp, - "zed:extension/nodejs": latest::zed::extension::nodejs, - "zed:extension/platform": latest::zed::extension::platform, - "zed:extension/process": latest::zed::extension::process, - "zed:extension/slash-command": latest::zed::extension::slash_command, - "zed:extension/context-server": latest::zed::extension::context_server, - "zed:extension/dap": latest::zed::extension::dap, - }, -}); - -pub use self::zed::extension::*; - -mod settings { - #![allow(dead_code)] - include!(concat!(env!("OUT_DIR"), "/since_v0.6.0/settings.rs")); -} - -pub type ExtensionWorktree = Arc; -pub type ExtensionProject = Arc; -pub type ExtensionKeyValueStore = Arc; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for latest::CodeLabel { - fn from(value: CodeLabel) -> Self { - Self { - code: value.code, - spans: value.spans.into_iter().map(Into::into).collect(), - filter_range: value.filter_range, - } - } -} - -impl From for latest::CodeLabelSpan { - fn from(value: CodeLabelSpan) -> Self { - match value { - CodeLabelSpan::CodeRange(range) => Self::CodeRange(range), - CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), - } - } -} - -impl From for latest::CodeLabelSpanLiteral { - fn from(value: CodeLabelSpanLiteral) -> Self { - Self { - text: value.text, - highlight_name: value.highlight_name, - } - } -} - -impl From for latest::SettingsLocation { - fn from(value: SettingsLocation) -> Self { - Self { - worktree_id: value.worktree_id, - path: value.path, - } - } -} - -impl From for latest::LanguageServerInstallationStatus { - fn from(value: LanguageServerInstallationStatus) -> Self { - match value { - LanguageServerInstallationStatus::None => Self::None, - LanguageServerInstallationStatus::Downloading => Self::Downloading, - LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate, - LanguageServerInstallationStatus::Failed(message) => Self::Failed(message), - } - } -} - -impl From for latest::DownloadedFileType { - fn from(value: DownloadedFileType) -> Self { - match value { - DownloadedFileType::Gzip => Self::Gzip, - DownloadedFileType::GzipTar => Self::GzipTar, - DownloadedFileType::Zip => Self::Zip, - DownloadedFileType::Uncompressed => Self::Uncompressed, - } - } -} - -impl HostKeyValueStore for WasmState { - async fn insert( - &mut self, - kv_store: Resource, - key: String, - value: String, - ) -> wasmtime::Result> { - latest::HostKeyValueStore::insert(self, kv_store, key, value).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of key-value stores. - Ok(()) - } -} - -impl HostProject for WasmState { - async fn worktree_ids( - &mut self, - project: Resource, - ) -> wasmtime::Result> { - latest::HostProject::worktree_ids(self, project).await - } - - async fn drop(&mut self, _project: Resource) -> Result<()> { - // We only ever hand out borrows of projects. - Ok(()) - } -} - -impl HostWorktree for WasmState { - async fn id(&mut self, delegate: Resource>) -> wasmtime::Result { - latest::HostWorktree::id(self, delegate).await - } - - async fn root_path( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::root_path(self, delegate).await - } - - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -impl ExtensionImports for WasmState { - async fn get_settings( - &mut self, - location: Option, - category: String, - key: Option, - ) -> wasmtime::Result> { - latest::ExtensionImports::get_settings( - self, - location.map(|location| location.into()), - category, - key, - ) - .await - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - latest::ExtensionImports::set_language_server_installation_status( - self, - server_name, - status.into(), - ) - .await - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - latest::ExtensionImports::download_file(self, url, path, file_type.into()).await - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - latest::ExtensionImports::make_file_executable(self, path).await - } -} diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs deleted file mode 100644 index a2776f9f3b..0000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs +++ /dev/null @@ -1,1109 +0,0 @@ -use crate::wasm_host::wit::since_v0_6_0::{ - dap::{ - AttachRequest, BuildTaskDefinition, BuildTaskDefinitionTemplatePayload, LaunchRequest, - StartDebuggingRequestArguments, TcpArguments, TcpArgumentsTemplate, - }, - slash_command::SlashCommandOutputSection, -}; -use crate::wasm_host::wit::{CompletionKind, CompletionLabelDetails, InsertTextFormat, SymbolKind}; -use crate::wasm_host::{WasmState, wit::ToWasmtimeResult}; -use ::http_client::{AsyncBody, HttpRequestExt}; -use ::settings::{Settings, WorktreeId}; -use anyhow::{Context as _, Result, bail}; -use async_compression::futures::bufread::GzipDecoder; -use async_tar::Archive; -use async_trait::async_trait; -use extension::{ - ExtensionLanguageServerProxy, KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate, -}; -use futures::{AsyncReadExt, lock::Mutex}; -use futures::{FutureExt as _, io::BufReader}; -use gpui::{BackgroundExecutor, SharedString}; -use language::{BinaryStatus, LanguageName, language_settings::AllLanguageSettings}; -use project::project_settings::ProjectSettings; -use semver::Version; -use std::{ - env, - net::Ipv4Addr, - path::{Path, PathBuf}, - str::FromStr, - sync::{Arc, OnceLock}, -}; -use task::{SpawnInTerminal, ZedDebugConfig}; -use url::Url; -use util::{ - archive::extract_zip, fs::make_file_executable, maybe, paths::PathStyle, rel_path::RelPath, -}; -use wasmtime::component::{Linker, Resource}; - -pub const MIN_VERSION: Version = Version::new(0, 8, 0); -pub const MAX_VERSION: Version = Version::new(0, 8, 0); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.8.0", - with: { - "worktree": ExtensionWorktree, - "project": ExtensionProject, - "key-value-store": ExtensionKeyValueStore, - "zed:extension/http-client/http-response-stream": ExtensionHttpResponseStream - }, -}); - -pub use self::zed::extension::*; - -mod settings { - #![allow(dead_code)] - include!(concat!(env!("OUT_DIR"), "/since_v0.8.0/settings.rs")); -} - -pub type ExtensionWorktree = Arc; -pub type ExtensionProject = Arc; -pub type ExtensionKeyValueStore = Arc; -pub type ExtensionHttpResponseStream = Arc>>; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for std::ops::Range { - fn from(range: Range) -> Self { - let start = range.start as usize; - let end = range.end as usize; - start..end - } -} - -impl From for extension::Command { - fn from(value: Command) -> Self { - Self { - command: value.command.into(), - args: value.args, - env: value.env, - } - } -} - -impl From - for extension::StartDebuggingRequestArgumentsRequest -{ - fn from(value: StartDebuggingRequestArgumentsRequest) -> Self { - match value { - StartDebuggingRequestArgumentsRequest::Launch => Self::Launch, - StartDebuggingRequestArgumentsRequest::Attach => Self::Attach, - } - } -} -impl TryFrom for extension::StartDebuggingRequestArguments { - type Error = anyhow::Error; - - fn try_from(value: StartDebuggingRequestArguments) -> Result { - Ok(Self { - configuration: serde_json::from_str(&value.configuration)?, - request: value.request.into(), - }) - } -} -impl From for extension::TcpArguments { - fn from(value: TcpArguments) -> Self { - Self { - host: value.host.into(), - port: value.port, - timeout: value.timeout, - } - } -} - -impl From for TcpArgumentsTemplate { - fn from(value: extension::TcpArgumentsTemplate) -> Self { - Self { - host: value.host.map(Ipv4Addr::to_bits), - port: value.port, - timeout: value.timeout, - } - } -} - -impl From for extension::TcpArgumentsTemplate { - fn from(value: TcpArgumentsTemplate) -> Self { - Self { - host: value.host.map(Ipv4Addr::from_bits), - port: value.port, - timeout: value.timeout, - } - } -} - -impl TryFrom for DebugTaskDefinition { - type Error = anyhow::Error; - fn try_from(value: extension::DebugTaskDefinition) -> Result { - Ok(Self { - label: value.label.to_string(), - adapter: value.adapter.to_string(), - config: value.config.to_string(), - tcp_connection: value.tcp_connection.map(Into::into), - }) - } -} - -impl From for DebugRequest { - fn from(value: task::DebugRequest) -> Self { - match value { - task::DebugRequest::Launch(launch_request) => Self::Launch(launch_request.into()), - task::DebugRequest::Attach(attach_request) => Self::Attach(attach_request.into()), - } - } -} - -impl From for task::DebugRequest { - fn from(value: DebugRequest) -> Self { - match value { - DebugRequest::Launch(launch_request) => Self::Launch(launch_request.into()), - DebugRequest::Attach(attach_request) => Self::Attach(attach_request.into()), - } - } -} - -impl From for LaunchRequest { - fn from(value: task::LaunchRequest) -> Self { - Self { - program: value.program, - cwd: value.cwd.map(|p| p.to_string_lossy().into_owned()), - args: value.args, - envs: value.env.into_iter().collect(), - } - } -} - -impl From for AttachRequest { - fn from(value: task::AttachRequest) -> Self { - Self { - process_id: value.process_id, - } - } -} - -impl From for task::LaunchRequest { - fn from(value: LaunchRequest) -> Self { - Self { - program: value.program, - cwd: value.cwd.map(|p| p.into()), - args: value.args, - env: value.envs.into_iter().collect(), - } - } -} -impl From for task::AttachRequest { - fn from(value: AttachRequest) -> Self { - Self { - process_id: value.process_id, - } - } -} - -impl From for DebugConfig { - fn from(value: ZedDebugConfig) -> Self { - Self { - label: value.label.into(), - adapter: value.adapter.into(), - request: value.request.into(), - stop_on_entry: value.stop_on_entry, - } - } -} -impl TryFrom for extension::DebugAdapterBinary { - type Error = anyhow::Error; - fn try_from(value: DebugAdapterBinary) -> Result { - Ok(Self { - command: value.command, - arguments: value.arguments, - envs: value.envs.into_iter().collect(), - cwd: value.cwd.map(|s| s.into()), - connection: value.connection.map(Into::into), - request_args: value.request_args.try_into()?, - }) - } -} - -impl From for extension::BuildTaskDefinition { - fn from(value: BuildTaskDefinition) -> Self { - match value { - BuildTaskDefinition::ByName(name) => Self::ByName(name.into()), - BuildTaskDefinition::Template(build_task_template) => Self::Template { - task_template: build_task_template.template.into(), - locator_name: build_task_template.locator_name.map(SharedString::from), - }, - } - } -} - -impl From for BuildTaskDefinition { - fn from(value: extension::BuildTaskDefinition) -> Self { - match value { - extension::BuildTaskDefinition::ByName(name) => Self::ByName(name.into()), - extension::BuildTaskDefinition::Template { - task_template, - locator_name, - } => Self::Template(BuildTaskDefinitionTemplatePayload { - template: task_template.into(), - locator_name: locator_name.map(String::from), - }), - } - } -} -impl From for extension::BuildTaskTemplate { - fn from(value: BuildTaskTemplate) -> Self { - Self { - label: value.label, - command: value.command, - args: value.args, - env: value.env.into_iter().collect(), - cwd: value.cwd, - ..Default::default() - } - } -} -impl From for BuildTaskTemplate { - fn from(value: extension::BuildTaskTemplate) -> Self { - Self { - label: value.label, - command: value.command, - args: value.args, - env: value.env.into_iter().collect(), - cwd: value.cwd, - } - } -} - -impl TryFrom for extension::DebugScenario { - type Error = anyhow::Error; - - fn try_from(value: DebugScenario) -> std::result::Result { - Ok(Self { - adapter: value.adapter.into(), - label: value.label.into(), - build: value.build.map(Into::into), - config: serde_json::Value::from_str(&value.config)?, - tcp_connection: value.tcp_connection.map(Into::into), - }) - } -} - -impl From for DebugScenario { - fn from(value: extension::DebugScenario) -> Self { - Self { - adapter: value.adapter.into(), - label: value.label.into(), - build: value.build.map(Into::into), - config: value.config.to_string(), - tcp_connection: value.tcp_connection.map(Into::into), - } - } -} - -impl TryFrom for ResolvedTask { - type Error = anyhow::Error; - - fn try_from(value: SpawnInTerminal) -> Result { - Ok(Self { - label: value.label, - command: value.command.context("missing command")?, - args: value.args, - env: value.env.into_iter().collect(), - cwd: value.cwd.map(|s| { - let s = s.to_string_lossy(); - if cfg!(target_os = "windows") { - s.replace('\\', "/") - } else { - s.into_owned() - } - }), - }) - } -} - -impl From for extension::CodeLabel { - fn from(value: CodeLabel) -> Self { - Self { - code: value.code, - spans: value.spans.into_iter().map(Into::into).collect(), - filter_range: value.filter_range.into(), - } - } -} - -impl From for extension::CodeLabelSpan { - fn from(value: CodeLabelSpan) -> Self { - match value { - CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()), - CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), - } - } -} - -impl From for extension::CodeLabelSpanLiteral { - fn from(value: CodeLabelSpanLiteral) -> Self { - Self { - text: value.text, - highlight_name: value.highlight_name, - } - } -} - -impl From for Completion { - fn from(value: extension::Completion) -> Self { - Self { - label: value.label, - label_details: value.label_details.map(Into::into), - detail: value.detail, - kind: value.kind.map(Into::into), - insert_text_format: value.insert_text_format.map(Into::into), - } - } -} - -impl From for CompletionLabelDetails { - fn from(value: extension::CompletionLabelDetails) -> Self { - Self { - detail: value.detail, - description: value.description, - } - } -} - -impl From for CompletionKind { - fn from(value: extension::CompletionKind) -> Self { - match value { - extension::CompletionKind::Text => Self::Text, - extension::CompletionKind::Method => Self::Method, - extension::CompletionKind::Function => Self::Function, - extension::CompletionKind::Constructor => Self::Constructor, - extension::CompletionKind::Field => Self::Field, - extension::CompletionKind::Variable => Self::Variable, - extension::CompletionKind::Class => Self::Class, - extension::CompletionKind::Interface => Self::Interface, - extension::CompletionKind::Module => Self::Module, - extension::CompletionKind::Property => Self::Property, - extension::CompletionKind::Unit => Self::Unit, - extension::CompletionKind::Value => Self::Value, - extension::CompletionKind::Enum => Self::Enum, - extension::CompletionKind::Keyword => Self::Keyword, - extension::CompletionKind::Snippet => Self::Snippet, - extension::CompletionKind::Color => Self::Color, - extension::CompletionKind::File => Self::File, - extension::CompletionKind::Reference => Self::Reference, - extension::CompletionKind::Folder => Self::Folder, - extension::CompletionKind::EnumMember => Self::EnumMember, - extension::CompletionKind::Constant => Self::Constant, - extension::CompletionKind::Struct => Self::Struct, - extension::CompletionKind::Event => Self::Event, - extension::CompletionKind::Operator => Self::Operator, - extension::CompletionKind::TypeParameter => Self::TypeParameter, - extension::CompletionKind::Other(value) => Self::Other(value), - } - } -} - -impl From for InsertTextFormat { - fn from(value: extension::InsertTextFormat) -> Self { - match value { - extension::InsertTextFormat::PlainText => Self::PlainText, - extension::InsertTextFormat::Snippet => Self::Snippet, - extension::InsertTextFormat::Other(value) => Self::Other(value), - } - } -} - -impl From for Symbol { - fn from(value: extension::Symbol) -> Self { - Self { - kind: value.kind.into(), - name: value.name, - } - } -} - -impl From for SymbolKind { - fn from(value: extension::SymbolKind) -> Self { - match value { - extension::SymbolKind::File => Self::File, - extension::SymbolKind::Module => Self::Module, - extension::SymbolKind::Namespace => Self::Namespace, - extension::SymbolKind::Package => Self::Package, - extension::SymbolKind::Class => Self::Class, - extension::SymbolKind::Method => Self::Method, - extension::SymbolKind::Property => Self::Property, - extension::SymbolKind::Field => Self::Field, - extension::SymbolKind::Constructor => Self::Constructor, - extension::SymbolKind::Enum => Self::Enum, - extension::SymbolKind::Interface => Self::Interface, - extension::SymbolKind::Function => Self::Function, - extension::SymbolKind::Variable => Self::Variable, - extension::SymbolKind::Constant => Self::Constant, - extension::SymbolKind::String => Self::String, - extension::SymbolKind::Number => Self::Number, - extension::SymbolKind::Boolean => Self::Boolean, - extension::SymbolKind::Array => Self::Array, - extension::SymbolKind::Object => Self::Object, - extension::SymbolKind::Key => Self::Key, - extension::SymbolKind::Null => Self::Null, - extension::SymbolKind::EnumMember => Self::EnumMember, - extension::SymbolKind::Struct => Self::Struct, - extension::SymbolKind::Event => Self::Event, - extension::SymbolKind::Operator => Self::Operator, - extension::SymbolKind::TypeParameter => Self::TypeParameter, - extension::SymbolKind::Other(value) => Self::Other(value), - } - } -} - -impl From for SlashCommand { - fn from(value: extension::SlashCommand) -> Self { - Self { - name: value.name, - description: value.description, - tooltip_text: value.tooltip_text, - requires_argument: value.requires_argument, - } - } -} - -impl From for extension::SlashCommandOutput { - fn from(value: SlashCommandOutput) -> Self { - Self { - text: value.text, - sections: value.sections.into_iter().map(Into::into).collect(), - } - } -} - -impl From for extension::SlashCommandOutputSection { - fn from(value: SlashCommandOutputSection) -> Self { - Self { - range: value.range.start as usize..value.range.end as usize, - label: value.label, - } - } -} - -impl From for extension::SlashCommandArgumentCompletion { - fn from(value: SlashCommandArgumentCompletion) -> Self { - Self { - label: value.label, - new_text: value.new_text, - run_command: value.run_command, - } - } -} - -impl TryFrom for extension::ContextServerConfiguration { - type Error = anyhow::Error; - - fn try_from(value: ContextServerConfiguration) -> Result { - let settings_schema: serde_json::Value = serde_json::from_str(&value.settings_schema) - .context("Failed to parse settings_schema")?; - - Ok(Self { - installation_instructions: value.installation_instructions, - default_settings: value.default_settings, - settings_schema, - }) - } -} - -impl HostKeyValueStore for WasmState { - async fn insert( - &mut self, - kv_store: Resource, - key: String, - value: String, - ) -> wasmtime::Result> { - let kv_store = self.table.get(&kv_store)?; - kv_store.insert(key, value).await.to_wasmtime_result() - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of key-value stores. - Ok(()) - } -} - -impl HostProject for WasmState { - async fn worktree_ids( - &mut self, - project: Resource, - ) -> wasmtime::Result> { - let project = self.table.get(&project)?; - Ok(project.worktree_ids()) - } - - async fn drop(&mut self, _project: Resource) -> Result<()> { - // We only ever hand out borrows of projects. - Ok(()) - } -} - -impl HostWorktree for WasmState { - async fn id(&mut self, delegate: Resource>) -> wasmtime::Result { - let delegate = self.table.get(&delegate)?; - Ok(delegate.id()) - } - - async fn root_path( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - let delegate = self.table.get(&delegate)?; - Ok(delegate.root_path()) - } - - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - let delegate = self.table.get(&delegate)?; - Ok(delegate - .read_text_file(&RelPath::new(Path::new(&path), PathStyle::Posix)?) - .await - .map_err(|error| error.to_string())) - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - let delegate = self.table.get(&delegate)?; - Ok(delegate.shell_env().await.into_iter().collect()) - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - let delegate = self.table.get(&delegate)?; - Ok(delegate.which(binary_name).await) - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -impl common::Host for WasmState {} - -impl http_client::Host for WasmState { - async fn fetch( - &mut self, - request: http_client::HttpRequest, - ) -> wasmtime::Result> { - maybe!(async { - let url = &request.url; - let request = convert_request(&request)?; - let mut response = self.host.http_client.send(request).await?; - - if response.status().is_client_error() || response.status().is_server_error() { - bail!("failed to fetch '{url}': status code {}", response.status()) - } - convert_response(&mut response).await - }) - .await - .to_wasmtime_result() - } - - async fn fetch_stream( - &mut self, - request: http_client::HttpRequest, - ) -> wasmtime::Result, String>> { - let request = convert_request(&request)?; - let response = self.host.http_client.send(request); - maybe!(async { - let response = response.await?; - let stream = Arc::new(Mutex::new(response)); - let resource = self.table.push(stream)?; - Ok(resource) - }) - .await - .to_wasmtime_result() - } -} - -impl http_client::HostHttpResponseStream for WasmState { - async fn next_chunk( - &mut self, - resource: Resource, - ) -> wasmtime::Result>, String>> { - let stream = self.table.get(&resource)?.clone(); - maybe!(async move { - let mut response = stream.lock().await; - let mut buffer = vec![0; 8192]; // 8KB buffer - let bytes_read = response.body_mut().read(&mut buffer).await?; - if bytes_read == 0 { - Ok(None) - } else { - buffer.truncate(bytes_read); - Ok(Some(buffer)) - } - }) - .await - .to_wasmtime_result() - } - - async fn drop(&mut self, _resource: Resource) -> Result<()> { - Ok(()) - } -} - -impl From for ::http_client::Method { - fn from(value: http_client::HttpMethod) -> Self { - match value { - http_client::HttpMethod::Get => Self::GET, - http_client::HttpMethod::Post => Self::POST, - http_client::HttpMethod::Put => Self::PUT, - http_client::HttpMethod::Delete => Self::DELETE, - http_client::HttpMethod::Head => Self::HEAD, - http_client::HttpMethod::Options => Self::OPTIONS, - http_client::HttpMethod::Patch => Self::PATCH, - } - } -} - -fn convert_request( - extension_request: &http_client::HttpRequest, -) -> anyhow::Result<::http_client::Request> { - let mut request = ::http_client::Request::builder() - .method(::http_client::Method::from(extension_request.method)) - .uri(&extension_request.url) - .follow_redirects(match extension_request.redirect_policy { - http_client::RedirectPolicy::NoFollow => ::http_client::RedirectPolicy::NoFollow, - http_client::RedirectPolicy::FollowLimit(limit) => { - ::http_client::RedirectPolicy::FollowLimit(limit) - } - http_client::RedirectPolicy::FollowAll => ::http_client::RedirectPolicy::FollowAll, - }); - for (key, value) in &extension_request.headers { - request = request.header(key, value); - } - let body = extension_request - .body - .clone() - .map(AsyncBody::from) - .unwrap_or_default(); - request.body(body).map_err(anyhow::Error::from) -} - -async fn convert_response( - response: &mut ::http_client::Response, -) -> anyhow::Result { - let mut extension_response = http_client::HttpResponse { - body: Vec::new(), - headers: Vec::new(), - }; - - for (key, value) in response.headers() { - extension_response - .headers - .push((key.to_string(), value.to_str().unwrap_or("").to_string())); - } - - response - .body_mut() - .read_to_end(&mut extension_response.body) - .await?; - - Ok(extension_response) -} - -impl nodejs::Host for WasmState { - async fn node_binary_path(&mut self) -> wasmtime::Result> { - self.host - .node_runtime - .binary_path() - .await - .map(|path| path.to_string_lossy().into_owned()) - .to_wasmtime_result() - } - - async fn npm_package_latest_version( - &mut self, - package_name: String, - ) -> wasmtime::Result> { - self.host - .node_runtime - .npm_package_latest_version(&package_name) - .await - .to_wasmtime_result() - } - - async fn npm_package_installed_version( - &mut self, - package_name: String, - ) -> wasmtime::Result, String>> { - self.host - .node_runtime - .npm_package_installed_version(&self.work_dir(), &package_name) - .await - .to_wasmtime_result() - } - - async fn npm_install_package( - &mut self, - package_name: String, - version: String, - ) -> wasmtime::Result> { - self.capability_granter - .grant_npm_install_package(&package_name)?; - - self.host - .node_runtime - .npm_install_packages(&self.work_dir(), &[(&package_name, &version)]) - .await - .to_wasmtime_result() - } -} - -#[async_trait] -impl lsp::Host for WasmState {} - -impl From<::http_client::github::GithubRelease> for github::GithubRelease { - fn from(value: ::http_client::github::GithubRelease) -> Self { - Self { - version: value.tag_name, - assets: value.assets.into_iter().map(Into::into).collect(), - } - } -} - -impl From<::http_client::github::GithubReleaseAsset> for github::GithubReleaseAsset { - fn from(value: ::http_client::github::GithubReleaseAsset) -> Self { - Self { - name: value.name, - download_url: value.browser_download_url, - } - } -} - -impl github::Host for WasmState { - async fn latest_github_release( - &mut self, - repo: String, - options: github::GithubReleaseOptions, - ) -> wasmtime::Result> { - maybe!(async { - let release = ::http_client::github::latest_github_release( - &repo, - options.require_assets, - options.pre_release, - self.host.http_client.clone(), - ) - .await?; - Ok(release.into()) - }) - .await - .to_wasmtime_result() - } - - async fn github_release_by_tag_name( - &mut self, - repo: String, - tag: String, - ) -> wasmtime::Result> { - maybe!(async { - let release = ::http_client::github::get_release_by_tag_name( - &repo, - &tag, - self.host.http_client.clone(), - ) - .await?; - Ok(release.into()) - }) - .await - .to_wasmtime_result() - } -} - -impl platform::Host for WasmState { - async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> { - Ok(( - match env::consts::OS { - "macos" => platform::Os::Mac, - "linux" => platform::Os::Linux, - "windows" => platform::Os::Windows, - _ => panic!("unsupported os"), - }, - match env::consts::ARCH { - "aarch64" => platform::Architecture::Aarch64, - "x86" => platform::Architecture::X86, - "x86_64" => platform::Architecture::X8664, - _ => panic!("unsupported architecture"), - }, - )) - } -} - -impl From for process::Output { - fn from(output: std::process::Output) -> Self { - Self { - status: output.status.code(), - stdout: output.stdout, - stderr: output.stderr, - } - } -} - -impl process::Host for WasmState { - async fn run_command( - &mut self, - command: process::Command, - ) -> wasmtime::Result> { - maybe!(async { - self.capability_granter - .grant_exec(&command.command, &command.args)?; - - let output = util::command::new_smol_command(command.command.as_str()) - .args(&command.args) - .envs(command.env) - .output() - .await?; - - Ok(output.into()) - }) - .await - .to_wasmtime_result() - } -} - -#[async_trait] -impl slash_command::Host for WasmState {} - -#[async_trait] -impl context_server::Host for WasmState {} - -impl dap::Host for WasmState { - async fn resolve_tcp_template( - &mut self, - template: TcpArgumentsTemplate, - ) -> wasmtime::Result> { - maybe!(async { - let (host, port, timeout) = - ::dap::configure_tcp_connection(task::TcpArgumentsTemplate { - port: template.port, - host: template.host.map(Ipv4Addr::from_bits), - timeout: template.timeout, - }) - .await?; - Ok(TcpArguments { - port, - host: host.to_bits(), - timeout, - }) - }) - .await - .to_wasmtime_result() - } -} - -impl ExtensionImports for WasmState { - async fn get_settings( - &mut self, - location: Option, - category: String, - key: Option, - ) -> wasmtime::Result> { - self.on_main_thread(|cx| { - async move { - let path = location.as_ref().and_then(|location| { - RelPath::new(Path::new(&location.path), PathStyle::Posix).ok() - }); - let location = path - .as_ref() - .zip(location.as_ref()) - .map(|(path, location)| ::settings::SettingsLocation { - worktree_id: WorktreeId::from_proto(location.worktree_id), - path, - }); - - cx.update(|cx| match category.as_str() { - "language" => { - let key = key.map(|k| LanguageName::new(&k)); - let settings = AllLanguageSettings::get(location, cx).language( - location, - key.as_ref(), - cx, - ); - Ok(serde_json::to_string(&settings::LanguageSettings { - tab_size: settings.tab_size, - })?) - } - "lsp" => { - let settings = key - .and_then(|key| { - ProjectSettings::get(location, cx) - .lsp - .get(&::lsp::LanguageServerName::from_proto(key)) - }) - .cloned() - .unwrap_or_default(); - Ok(serde_json::to_string(&settings::LspSettings { - binary: settings.binary.map(|binary| settings::CommandSettings { - path: binary.path, - arguments: binary.arguments, - env: binary.env.map(|env| env.into_iter().collect()), - }), - settings: settings.settings, - initialization_options: settings.initialization_options, - })?) - } - "context_servers" => { - let settings = key - .and_then(|key| { - ProjectSettings::get(location, cx) - .context_servers - .get(key.as_str()) - }) - .cloned() - .unwrap_or_else(|| { - project::project_settings::ContextServerSettings::default_extension( - ) - }); - - match settings { - project::project_settings::ContextServerSettings::Stdio { - enabled: _, - command, - } => Ok(serde_json::to_string(&settings::ContextServerSettings { - command: Some(settings::CommandSettings { - path: command.path.to_str().map(|path| path.to_string()), - arguments: Some(command.args), - env: command.env.map(|env| env.into_iter().collect()), - }), - settings: None, - })?), - project::project_settings::ContextServerSettings::Extension { - enabled: _, - settings, - } => Ok(serde_json::to_string(&settings::ContextServerSettings { - command: None, - settings: Some(settings), - })?), - project::project_settings::ContextServerSettings::Http { .. } => { - bail!("remote context server settings not supported in 0.6.0") - } - } - } - _ => { - bail!("Unknown settings category: {}", category); - } - }) - } - .boxed_local() - }) - .await? - .to_wasmtime_result() - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - let status = match status { - LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate, - LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading, - LanguageServerInstallationStatus::None => BinaryStatus::None, - LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error }, - }; - - self.host - .proxy - .update_language_server_status(::lsp::LanguageServerName(server_name.into()), status); - - Ok(()) - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - maybe!(async { - let parsed_url = Url::parse(&url)?; - self.capability_granter.grant_download_file(&parsed_url)?; - - let path = PathBuf::from(path); - let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref()); - - self.host.fs.create_dir(&extension_work_dir).await?; - - let destination_path = self - .host - .writeable_path_from_extension(&self.manifest.id, &path)?; - - let mut response = self - .host - .http_client - .get(&url, Default::default(), true) - .await - .context("downloading release")?; - - anyhow::ensure!( - response.status().is_success(), - "download failed with status {}", - response.status() - ); - let body = BufReader::new(response.body_mut()); - - match file_type { - DownloadedFileType::Uncompressed => { - futures::pin_mut!(body); - self.host - .fs - .create_file_with(&destination_path, body) - .await?; - } - DownloadedFileType::Gzip => { - let body = GzipDecoder::new(body); - futures::pin_mut!(body); - self.host - .fs - .create_file_with(&destination_path, body) - .await?; - } - DownloadedFileType::GzipTar => { - let body = GzipDecoder::new(body); - futures::pin_mut!(body); - self.host - .fs - .extract_tar_file(&destination_path, Archive::new(body)) - .await?; - } - DownloadedFileType::Zip => { - futures::pin_mut!(body); - extract_zip(&destination_path, body) - .await - .with_context(|| format!("unzipping {path:?} archive"))?; - } - } - - Ok(()) - }) - .await - .to_wasmtime_result() - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - let path = self - .host - .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?; - - make_file_executable(&path) - .await - .with_context(|| format!("setting permissions for path {path:?}")) - .to_wasmtime_result() - } -} diff --git a/crates/extensions_ui/Cargo.toml b/crates/extensions_ui/Cargo.toml deleted file mode 100644 index 707938a9eb..0000000000 --- a/crates/extensions_ui/Cargo.toml +++ /dev/null @@ -1,45 +0,0 @@ -[package] -name = "extensions_ui" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/extensions_ui.rs" - -[dependencies] -anyhow.workspace = true -client.workspace = true -collections.workspace = true -db.workspace = true -editor.workspace = true -extension.workspace = true -extension_host.workspace = true -fs.workspace = true -fuzzy.workspace = true -gpui.workspace = true -language.workspace = true -log.workspace = true -num-format.workspace = true -picker.workspace = true -project.workspace = true -release_channel.workspace = true -semver.workspace = true -serde.workspace = true -settings.workspace = true -smallvec.workspace = true -strum.workspace = true -telemetry.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -vim_mode_setting.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -editor = { workspace = true, features = ["test-support"] } diff --git a/crates/extensions_ui/LICENSE-GPL b/crates/extensions_ui/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/extensions_ui/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/extensions_ui/src/components.rs b/crates/extensions_ui/src/components.rs deleted file mode 100644 index bf11abd679..0000000000 --- a/crates/extensions_ui/src/components.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod extension_card; - -pub use extension_card::*; diff --git a/crates/extensions_ui/src/components/extension_card.rs b/crates/extensions_ui/src/components/extension_card.rs deleted file mode 100644 index 524f90c7f0..0000000000 --- a/crates/extensions_ui/src/components/extension_card.rs +++ /dev/null @@ -1,61 +0,0 @@ -use gpui::{AnyElement, prelude::*}; -use smallvec::SmallVec; -use ui::prelude::*; - -#[derive(IntoElement)] -pub struct ExtensionCard { - overridden_by_dev_extension: bool, - children: SmallVec<[AnyElement; 2]>, -} - -impl ExtensionCard { - pub fn new() -> Self { - Self { - overridden_by_dev_extension: false, - children: SmallVec::new(), - } - } - - pub fn overridden_by_dev_extension(mut self, overridden: bool) -> Self { - self.overridden_by_dev_extension = overridden; - self - } -} - -impl ParentElement for ExtensionCard { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for ExtensionCard { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - div().w_full().child( - v_flex() - .mt_4() - .w_full() - .h(rems_from_px(110.)) - .p_3() - .gap_2() - .bg(cx.theme().colors().elevated_surface_background.opacity(0.5)) - .border_1() - .border_color(cx.theme().colors().border_variant) - .rounded_md() - .children(self.children) - .when(self.overridden_by_dev_extension, |card| { - card.child( - h_flex() - .absolute() - .top_0() - .left_0() - .block_mouse_except_scroll() - .cursor_default() - .size_full() - .justify_center() - .bg(cx.theme().colors().elevated_surface_background.alpha(0.8)) - .child(Label::new("Overridden by dev extension.")), - ) - }), - ) - } -} diff --git a/crates/extensions_ui/src/extension_suggest.rs b/crates/extensions_ui/src/extension_suggest.rs deleted file mode 100644 index 7ad4c1540a..0000000000 --- a/crates/extensions_ui/src/extension_suggest.rs +++ /dev/null @@ -1,248 +0,0 @@ -use std::collections::HashMap; -use std::sync::{Arc, OnceLock}; - -use db::kvp::KEY_VALUE_STORE; -use editor::Editor; -use extension_host::ExtensionStore; -use gpui::{AppContext as _, Context, Entity, SharedString, Window}; -use language::Buffer; -use ui::prelude::*; -use util::rel_path::RelPath; -use workspace::notifications::simple_message_notification::MessageNotification; -use workspace::{Workspace, notifications::NotificationId}; - -const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[ - ("astro", &["astro"]), - ("beancount", &["beancount"]), - ("clojure", &["bb", "clj", "cljc", "cljs", "edn"]), - ("neocmake", &["CMakeLists.txt", "cmake"]), - ("csharp", &["cs"]), - ("cython", &["pyx", "pxd", "pxi"]), - ("dart", &["dart"]), - ("dockerfile", &["Dockerfile"]), - ("elisp", &["el"]), - ("elixir", &["ex", "exs", "heex"]), - ("elm", &["elm"]), - ("erlang", &["erl", "hrl"]), - ("fish", &["fish"]), - ( - "git-firefly", - &[ - ".gitconfig", - ".gitignore", - "COMMIT_EDITMSG", - "EDIT_DESCRIPTION", - "MERGE_MSG", - "NOTES_EDITMSG", - "TAG_EDITMSG", - "git-rebase-todo", - ], - ), - ("gleam", &["gleam"]), - ("glsl", &["vert", "frag"]), - ("graphql", &["gql", "graphql"]), - ("haskell", &["hs"]), - ("html", &["htm", "html", "shtml"]), - ("java", &["java"]), - ("kotlin", &["kt"]), - ("latex", &["tex"]), - ("log", &["log"]), - ("lua", &["lua"]), - ("make", &["Makefile"]), - ("nim", &["nim"]), - ("nix", &["nix"]), - ("nu", &["nu"]), - ("ocaml", &["ml", "mli"]), - ("php", &["php"]), - ("powershell", &["ps1", "psm1"]), - ("prisma", &["prisma"]), - ("proto", &["proto"]), - ("purescript", &["purs"]), - ("r", &["r", "R"]), - ("racket", &["rkt"]), - ("rescript", &["res", "resi"]), - ("rst", &["rst"]), - ("ruby", &["rb", "erb"]), - ("scheme", &["scm"]), - ("scss", &["scss"]), - ("sql", &["sql"]), - ("svelte", &["svelte"]), - ("swift", &["swift"]), - ("templ", &["templ"]), - ("terraform", &["tf", "tfvars", "hcl"]), - ("toml", &["Cargo.lock", "toml"]), - ("typst", &["typ"]), - ("vue", &["vue"]), - ("wgsl", &["wgsl"]), - ("wit", &["wit"]), - ("xml", &["xml"]), - ("zig", &["zig"]), -]; - -fn suggested_extensions() -> &'static HashMap<&'static str, Arc> { - static SUGGESTIONS_BY_PATH_SUFFIX: OnceLock>> = OnceLock::new(); - SUGGESTIONS_BY_PATH_SUFFIX.get_or_init(|| { - SUGGESTIONS_BY_EXTENSION_ID - .iter() - .flat_map(|(name, path_suffixes)| { - let name = Arc::::from(*name); - path_suffixes - .iter() - .map(move |suffix| (*suffix, name.clone())) - }) - .collect() - }) -} - -#[derive(Debug, PartialEq, Eq, Clone)] -struct SuggestedExtension { - pub extension_id: Arc, - pub file_name_or_extension: Arc, -} - -/// Returns the suggested extension for the given [`Path`]. -fn suggested_extension(path: &RelPath) -> Option { - let file_extension: Option> = path.extension().map(|extension| extension.into()); - let file_name: Option> = path.file_name().map(|name| name.into()); - - let (file_name_or_extension, extension_id) = None - // We suggest against file names first, as these suggestions will be more - // specific than ones based on the file extension. - .or_else(|| { - file_name.clone().zip( - file_name - .as_deref() - .and_then(|file_name| suggested_extensions().get(file_name)), - ) - }) - .or_else(|| { - file_extension.clone().zip( - file_extension - .as_deref() - .and_then(|file_extension| suggested_extensions().get(file_extension)), - ) - })?; - - Some(SuggestedExtension { - extension_id: extension_id.clone(), - file_name_or_extension, - }) -} - -fn language_extension_key(extension_id: &str) -> String { - format!("{}_extension_suggest", extension_id) -} - -pub(crate) fn suggest(buffer: Entity, window: &mut Window, cx: &mut Context) { - let Some(file) = buffer.read(cx).file().cloned() else { - return; - }; - - let Some(SuggestedExtension { - extension_id, - file_name_or_extension, - }) = suggested_extension(file.path()) - else { - return; - }; - - let key = language_extension_key(&extension_id); - let Ok(None) = KEY_VALUE_STORE.read_kvp(&key) else { - return; - }; - - cx.on_next_frame(window, move |workspace, _, cx| { - let Some(editor) = workspace.active_item_as::(cx) else { - return; - }; - - if editor.read(cx).buffer().read(cx).as_singleton().as_ref() != Some(&buffer) { - return; - } - - struct ExtensionSuggestionNotification; - - let notification_id = NotificationId::composite::( - SharedString::from(extension_id.clone()), - ); - - workspace.show_notification(notification_id, cx, |cx| { - cx.new(move |cx| { - MessageNotification::new( - format!( - "Do you want to install the recommended '{}' extension for '{}' files?", - extension_id, file_name_or_extension - ), - cx, - ) - .primary_message("Yes, install extension") - .primary_icon(IconName::Check) - .primary_icon_color(Color::Success) - .primary_on_click({ - let extension_id = extension_id.clone(); - move |_window, cx| { - let extension_id = extension_id.clone(); - let extension_store = ExtensionStore::global(cx); - extension_store.update(cx, move |store, cx| { - store.install_latest_extension(extension_id, cx); - }); - } - }) - .secondary_message("No, don't install it") - .secondary_icon(IconName::Close) - .secondary_icon_color(Color::Error) - .secondary_on_click(move |_window, cx| { - let key = language_extension_key(&extension_id); - db::write_and_log(cx, move || { - KEY_VALUE_STORE.write_kvp(key, "dismissed".to_string()) - }); - }) - }) - }); - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use util::rel_path::rel_path; - - #[test] - pub fn test_suggested_extension() { - assert_eq!( - suggested_extension(rel_path("Cargo.toml")), - Some(SuggestedExtension { - extension_id: "toml".into(), - file_name_or_extension: "toml".into() - }) - ); - assert_eq!( - suggested_extension(rel_path("Cargo.lock")), - Some(SuggestedExtension { - extension_id: "toml".into(), - file_name_or_extension: "Cargo.lock".into() - }) - ); - assert_eq!( - suggested_extension(rel_path("Dockerfile")), - Some(SuggestedExtension { - extension_id: "dockerfile".into(), - file_name_or_extension: "Dockerfile".into() - }) - ); - assert_eq!( - suggested_extension(rel_path("a/b/c/d/.gitignore")), - Some(SuggestedExtension { - extension_id: "git-firefly".into(), - file_name_or_extension: ".gitignore".into() - }) - ); - assert_eq!( - suggested_extension(rel_path("a/b/c/d/test.gleam")), - Some(SuggestedExtension { - extension_id: "gleam".into(), - file_name_or_extension: "gleam".into() - }) - ); - } -} diff --git a/crates/extensions_ui/src/extension_version_selector.rs b/crates/extensions_ui/src/extension_version_selector.rs deleted file mode 100644 index 17d293da76..0000000000 --- a/crates/extensions_ui/src/extension_version_selector.rs +++ /dev/null @@ -1,251 +0,0 @@ -use std::str::FromStr; -use std::sync::Arc; - -use client::ExtensionMetadata; -use extension_host::ExtensionStore; -use fs::Fs; -use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; -use gpui::{App, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, prelude::*}; -use picker::{Picker, PickerDelegate}; -use release_channel::ReleaseChannel; -use semver::Version; -use settings::update_settings_file; -use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*}; -use util::ResultExt; -use workspace::ModalView; - -pub struct ExtensionVersionSelector { - picker: Entity>, -} - -impl ModalView for ExtensionVersionSelector {} - -impl EventEmitter for ExtensionVersionSelector {} - -impl Focusable for ExtensionVersionSelector { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl Render for ExtensionVersionSelector { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - v_flex().w(rems(34.)).child(self.picker.clone()) - } -} - -impl ExtensionVersionSelector { - pub fn new( - delegate: ExtensionVersionSelectorDelegate, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); - Self { picker } - } -} - -pub struct ExtensionVersionSelectorDelegate { - fs: Arc, - selector: WeakEntity, - extension_versions: Vec, - selected_index: usize, - matches: Vec, -} - -impl ExtensionVersionSelectorDelegate { - pub fn new( - fs: Arc, - selector: WeakEntity, - mut extension_versions: Vec, - ) -> Self { - extension_versions.sort_unstable_by(|a, b| { - let a_version = Version::from_str(&a.manifest.version); - let b_version = Version::from_str(&b.manifest.version); - - match (a_version, b_version) { - (Ok(a_version), Ok(b_version)) => b_version.cmp(&a_version), - _ => b.published_at.cmp(&a.published_at), - } - }); - - let matches = extension_versions - .iter() - .map(|extension| StringMatch { - candidate_id: 0, - score: 0.0, - positions: Default::default(), - string: format!("v{}", extension.manifest.version), - }) - .collect(); - - Self { - fs, - selector, - extension_versions, - selected_index: 0, - matches, - } - } -} - -impl PickerDelegate for ExtensionVersionSelectorDelegate { - type ListItem = ui::ListItem; - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Select extension version...".into() - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _window: &mut Window, - _cx: &mut Context>, - ) { - self.selected_index = ix; - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - let background_executor = cx.background_executor().clone(); - let candidates = self - .extension_versions - .iter() - .enumerate() - .map(|(id, extension)| { - StringMatchCandidate::new(id, &format!("v{}", extension.manifest.version)) - }) - .collect::>(); - - cx.spawn_in(window, async move |this, cx| { - let matches = if query.is_empty() { - candidates - .into_iter() - .enumerate() - .map(|(index, candidate)| StringMatch { - candidate_id: index, - string: candidate.string, - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - match_strings( - &candidates, - &query, - false, - true, - 100, - &Default::default(), - background_executor, - ) - .await - }; - - this.update(cx, |this, _cx| { - this.delegate.matches = matches; - this.delegate.selected_index = this - .delegate - .selected_index - .min(this.delegate.matches.len().saturating_sub(1)); - }) - .log_err(); - }) - } - - fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { - if self.matches.is_empty() { - self.dismissed(window, cx); - return; - } - - let candidate_id = self.matches[self.selected_index].candidate_id; - let extension_version = &self.extension_versions[candidate_id]; - - if !extension_host::is_version_compatible(ReleaseChannel::global(cx), extension_version) { - return; - } - - let extension_store = ExtensionStore::global(cx); - extension_store.update(cx, |store, cx| { - let extension_id = extension_version.id.clone(); - let version = extension_version.manifest.version.clone(); - - update_settings_file(self.fs.clone(), cx, { - let extension_id = extension_id.clone(); - move |settings, _| { - settings - .extension - .auto_update_extensions - .insert(extension_id, false); - } - }); - - store.install_extension(extension_id, version, cx); - }); - } - - fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { - self.selector - .update(cx, |_, cx| cx.emit(DismissEvent)) - .log_err(); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - let version_match = &self.matches.get(ix)?; - let extension_version = &self.extension_versions.get(version_match.candidate_id)?; - - let is_version_compatible = - extension_host::is_version_compatible(ReleaseChannel::global(cx), extension_version); - let disabled = !is_version_compatible; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .disabled(disabled) - .child( - HighlightedLabel::new( - version_match.string.clone(), - version_match.positions.clone(), - ) - .when(disabled, |label| label.color(Color::Muted)), - ) - .end_slot( - h_flex() - .gap_2() - .when(!is_version_compatible, |this| { - this.child(Label::new("Incompatible").color(Color::Muted)) - }) - .child( - Label::new( - extension_version - .published_at - .format("%Y-%m-%d") - .to_string(), - ) - .when(disabled, |label| label.color(Color::Muted)), - ), - ), - ) - } -} diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs deleted file mode 100644 index 3dd4803ce1..0000000000 --- a/crates/extensions_ui/src/extensions_ui.rs +++ /dev/null @@ -1,1784 +0,0 @@ -mod components; -mod extension_suggest; -mod extension_version_selector; - -use std::sync::OnceLock; -use std::time::Duration; -use std::{ops::Range, sync::Arc}; - -use anyhow::Context as _; -use client::{ExtensionMetadata, ExtensionProvides}; -use collections::{BTreeMap, BTreeSet}; -use editor::{Editor, EditorElement, EditorStyle}; -use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore}; -use fuzzy::{StringMatchCandidate, match_strings}; -use gpui::{ - Action, App, ClipboardItem, Context, Corner, Entity, EventEmitter, Flatten, Focusable, - InteractiveElement, KeyContext, ParentElement, Point, Render, Styled, Task, TextStyle, - UniformListScrollHandle, WeakEntity, Window, actions, point, uniform_list, -}; -use num_format::{Locale, ToFormattedString}; -use project::DirectoryLister; -use release_channel::ReleaseChannel; -use settings::{Settings, SettingsContent}; -use strum::IntoEnumIterator as _; -use theme::ThemeSettings; -use ui::{ - Banner, Chip, ContextMenu, Divider, PopoverMenu, ScrollableHandle, Switch, ToggleButtonGroup, - ToggleButtonGroupSize, ToggleButtonGroupStyle, ToggleButtonSimple, Tooltip, WithScrollbar, - prelude::*, -}; -use vim_mode_setting::VimModeSetting; -use workspace::{ - Workspace, - item::{Item, ItemEvent}, -}; -use zed_actions::ExtensionCategoryFilter; - -use crate::components::ExtensionCard; -use crate::extension_version_selector::{ - ExtensionVersionSelector, ExtensionVersionSelectorDelegate, -}; - -actions!( - zed, - [ - /// Installs an extension from a local directory for development. - InstallDevExtension - ] -); - -pub fn init(cx: &mut App) { - cx.observe_new(move |workspace: &mut Workspace, window, cx| { - let Some(window) = window else { - return; - }; - workspace - .register_action( - move |workspace, action: &zed_actions::Extensions, window, cx| { - let provides_filter = action.category_filter.map(|category| match category { - ExtensionCategoryFilter::Themes => ExtensionProvides::Themes, - ExtensionCategoryFilter::IconThemes => ExtensionProvides::IconThemes, - ExtensionCategoryFilter::Languages => ExtensionProvides::Languages, - ExtensionCategoryFilter::Grammars => ExtensionProvides::Grammars, - ExtensionCategoryFilter::LanguageServers => { - ExtensionProvides::LanguageServers - } - ExtensionCategoryFilter::ContextServers => { - ExtensionProvides::ContextServers - } - ExtensionCategoryFilter::AgentServers => ExtensionProvides::AgentServers, - ExtensionCategoryFilter::SlashCommands => ExtensionProvides::SlashCommands, - ExtensionCategoryFilter::IndexedDocsProviders => { - ExtensionProvides::IndexedDocsProviders - } - ExtensionCategoryFilter::Snippets => ExtensionProvides::Snippets, - ExtensionCategoryFilter::DebugAdapters => ExtensionProvides::DebugAdapters, - }); - - let existing = workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()); - - if let Some(existing) = existing { - existing.update(cx, |extensions_page, cx| { - if provides_filter.is_some() { - extensions_page.change_provides_filter(provides_filter, cx); - } - if let Some(id) = action.id.as_ref() { - extensions_page.focus_extension(id, window, cx); - } - }); - - workspace.activate_item(&existing, true, true, window, cx); - } else { - let extensions_page = ExtensionsPage::new( - workspace, - provides_filter, - action.id.as_deref(), - window, - cx, - ); - workspace.add_item_to_active_pane( - Box::new(extensions_page), - None, - true, - window, - cx, - ) - } - }, - ) - .register_action(move |workspace, _: &InstallDevExtension, window, cx| { - let store = ExtensionStore::global(cx); - let prompt = workspace.prompt_for_open_path( - gpui::PathPromptOptions { - files: false, - directories: true, - multiple: false, - prompt: None, - }, - DirectoryLister::Local( - workspace.project().clone(), - workspace.app_state().fs.clone(), - ), - window, - cx, - ); - - let workspace_handle = cx.entity().downgrade(); - window - .spawn(cx, async move |cx| { - let extension_path = - match Flatten::flatten(prompt.await.map_err(|e| e.into())) { - Ok(Some(mut paths)) => paths.pop()?, - Ok(None) => return None, - Err(err) => { - workspace_handle - .update(cx, |workspace, cx| { - workspace.show_portal_error(err.to_string(), cx); - }) - .ok(); - return None; - } - }; - - let install_task = store - .update(cx, |store, cx| { - store.install_dev_extension(extension_path, cx) - }) - .ok()?; - - match install_task.await { - Ok(_) => {} - Err(err) => { - log::error!("Failed to install dev extension: {:?}", err); - workspace_handle - .update(cx, |workspace, cx| { - workspace.show_error( - // NOTE: using `anyhow::context` here ends up not printing - // the error - &format!("Failed to install dev extension: {}", err), - cx, - ); - }) - .ok(); - } - } - - Some(()) - }) - .detach(); - }); - - cx.subscribe_in(workspace.project(), window, |_, _, event, window, cx| { - if let project::Event::LanguageNotFound(buffer) = event { - extension_suggest::suggest(buffer.clone(), window, cx); - } - }) - .detach(); - }) - .detach(); -} - -fn extension_provides_label(provides: ExtensionProvides) -> &'static str { - match provides { - ExtensionProvides::Themes => "Themes", - ExtensionProvides::IconThemes => "Icon Themes", - ExtensionProvides::Languages => "Languages", - ExtensionProvides::Grammars => "Grammars", - ExtensionProvides::LanguageServers => "Language Servers", - ExtensionProvides::ContextServers => "MCP Servers", - ExtensionProvides::AgentServers => "Agent Servers", - ExtensionProvides::SlashCommands => "Slash Commands", - ExtensionProvides::IndexedDocsProviders => "Indexed Docs Providers", - ExtensionProvides::Snippets => "Snippets", - ExtensionProvides::DebugAdapters => "Debug Adapters", - } -} - -#[derive(Clone)] -pub enum ExtensionStatus { - NotInstalled, - Installing, - Upgrading, - Installed(Arc), - Removing, -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -enum ExtensionFilter { - All, - Installed, - NotInstalled, -} - -impl ExtensionFilter { - pub fn include_dev_extensions(&self) -> bool { - match self { - Self::All | Self::Installed => true, - Self::NotInstalled => false, - } - } -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -enum Feature { - AgentClaude, - AgentCodex, - AgentGemini, - ExtensionBasedpyright, - ExtensionRuff, - ExtensionTailwind, - ExtensionTy, - Git, - LanguageBash, - LanguageC, - LanguageCpp, - LanguageGo, - LanguagePython, - LanguageReact, - LanguageRust, - LanguageTypescript, - OpenIn, - Vim, -} - -fn keywords_by_feature() -> &'static BTreeMap> { - static KEYWORDS_BY_FEATURE: OnceLock>> = OnceLock::new(); - KEYWORDS_BY_FEATURE.get_or_init(|| { - BTreeMap::from_iter([ - (Feature::AgentClaude, vec!["claude", "claude code"]), - (Feature::AgentCodex, vec!["codex", "codex cli"]), - (Feature::AgentGemini, vec!["gemini", "gemini cli"]), - ( - Feature::ExtensionBasedpyright, - vec!["basedpyright", "pyright"], - ), - (Feature::ExtensionRuff, vec!["ruff"]), - (Feature::ExtensionTailwind, vec!["tail", "tailwind"]), - (Feature::ExtensionTy, vec!["ty"]), - (Feature::Git, vec!["git"]), - (Feature::LanguageBash, vec!["sh", "bash"]), - (Feature::LanguageC, vec!["c", "clang"]), - (Feature::LanguageCpp, vec!["c++", "cpp", "clang"]), - (Feature::LanguageGo, vec!["go", "golang"]), - (Feature::LanguagePython, vec!["python", "py"]), - (Feature::LanguageReact, vec!["react"]), - (Feature::LanguageRust, vec!["rust", "rs"]), - ( - Feature::LanguageTypescript, - vec!["type", "typescript", "ts"], - ), - ( - Feature::OpenIn, - vec![ - "github", - "gitlab", - "bitbucket", - "codeberg", - "sourcehut", - "permalink", - "link", - "open in", - ], - ), - (Feature::Vim, vec!["vim"]), - ]) - }) -} - -struct ExtensionCardButtons { - install_or_uninstall: Button, - upgrade: Option", - "

Some text

", - false, - false, - ), - ] { - let mut w = Vec::new(); - let mut minifier = Minifier::new( - &mut w, - MinifierOptions { - omit_doctype: true, - preserve_comments, - collapse_whitespace, - }, - ); - minifier.minify(&mut input.as_bytes()).unwrap(); - - let s = str::from_utf8(&w).unwrap(); - - assert_eq!(expected, s); - } - } -} diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs deleted file mode 100644 index b17ee5cac4..0000000000 --- a/crates/markdown_preview/src/markdown_parser.rs +++ /dev/null @@ -1,3218 +0,0 @@ -use crate::{ - markdown_elements::*, - markdown_minifier::{Minifier, MinifierOptions}, -}; -use async_recursion::async_recursion; -use collections::FxHashMap; -use gpui::{DefiniteLength, FontWeight, px, relative}; -use html5ever::{ParseOpts, local_name, parse_document, tendril::TendrilSink}; -use language::LanguageRegistry; -use markup5ever_rcdom::RcDom; -use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd}; -use std::{ - cell::RefCell, collections::HashMap, mem, ops::Range, path::PathBuf, rc::Rc, sync::Arc, vec, -}; -use ui::SharedString; - -pub async fn parse_markdown( - markdown_input: &str, - file_location_directory: Option, - language_registry: Option>, -) -> ParsedMarkdown { - let mut options = Options::all(); - options.remove(pulldown_cmark::Options::ENABLE_DEFINITION_LIST); - - let parser = Parser::new_ext(markdown_input, options); - let parser = MarkdownParser::new( - parser.into_offset_iter().collect(), - file_location_directory, - language_registry, - ); - let renderer = parser.parse_document().await; - ParsedMarkdown { - children: renderer.parsed, - } -} - -fn cleanup_html(source: &str) -> Vec { - let mut writer = std::io::Cursor::new(Vec::new()); - let mut reader = std::io::Cursor::new(source); - let mut minify = Minifier::new( - &mut writer, - MinifierOptions { - omit_doctype: true, - collapse_whitespace: true, - ..Default::default() - }, - ); - if let Ok(()) = minify.minify(&mut reader) { - writer.into_inner() - } else { - source.bytes().collect() - } -} - -struct MarkdownParser<'a> { - tokens: Vec<(Event<'a>, Range)>, - /// The current index in the tokens array - cursor: usize, - /// The blocks that we have successfully parsed so far - parsed: Vec, - file_location_directory: Option, - language_registry: Option>, -} - -#[derive(Debug)] -struct ParseHtmlNodeContext { - list_item_depth: u16, -} - -impl Default for ParseHtmlNodeContext { - fn default() -> Self { - Self { list_item_depth: 1 } - } -} - -struct MarkdownListItem { - content: Vec, - item_type: ParsedMarkdownListItemType, -} - -impl Default for MarkdownListItem { - fn default() -> Self { - Self { - content: Vec::new(), - item_type: ParsedMarkdownListItemType::Unordered, - } - } -} - -impl<'a> MarkdownParser<'a> { - fn new( - tokens: Vec<(Event<'a>, Range)>, - file_location_directory: Option, - language_registry: Option>, - ) -> Self { - Self { - tokens, - file_location_directory, - language_registry, - cursor: 0, - parsed: vec![], - } - } - - fn eof(&self) -> bool { - if self.tokens.is_empty() { - return true; - } - self.cursor >= self.tokens.len() - 1 - } - - fn peek(&self, steps: usize) -> Option<&(Event<'_>, Range)> { - if self.eof() || (steps + self.cursor) >= self.tokens.len() { - return self.tokens.last(); - } - 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) - } - - fn current(&self) -> Option<&(Event<'_>, Range)> { - self.peek(0) - } - - fn current_event(&self) -> Option<&Event<'_>> { - self.current().map(|(event, _)| event) - } - - fn is_text_like(event: &Event) -> bool { - match event { - Event::Text(_) - // Represent an inline code block - | Event::Code(_) - | Event::Html(_) - | Event::InlineHtml(_) - | Event::FootnoteReference(_) - | Event::Start(Tag::Link { .. }) - | Event::Start(Tag::Emphasis) - | Event::Start(Tag::Strong) - | Event::Start(Tag::Strikethrough) - | Event::Start(Tag::Image { .. }) => { - true - } - _ => false, - } - } - - async fn parse_document(mut self) -> Self { - while !self.eof() { - if let Some(block) = self.parse_block().await { - self.parsed.extend(block); - } else { - self.cursor += 1; - } - } - self - } - - #[async_recursion] - async fn parse_block(&mut self) -> Option> { - let (current, source_range) = self.current().unwrap(); - let source_range = source_range.clone(); - match current { - Event::Start(tag) => match tag { - Tag::Paragraph => { - self.cursor += 1; - let text = self.parse_text(false, Some(source_range)); - Some(vec![ParsedMarkdownElement::Paragraph(text)]) - } - Tag::Heading { level, .. } => { - let level = *level; - self.cursor += 1; - let heading = self.parse_heading(level); - Some(vec![ParsedMarkdownElement::Heading(heading)]) - } - Tag::Table(alignment) => { - let alignment = alignment.clone(); - self.cursor += 1; - let table = self.parse_table(alignment); - Some(vec![ParsedMarkdownElement::Table(table)]) - } - Tag::List(order) => { - let order = *order; - self.cursor += 1; - let list = self.parse_list(order).await; - Some(list) - } - Tag::BlockQuote(_kind) => { - self.cursor += 1; - let block_quote = self.parse_block_quote().await; - Some(vec![ParsedMarkdownElement::BlockQuote(block_quote)]) - } - Tag::CodeBlock(kind) => { - let language = match kind { - pulldown_cmark::CodeBlockKind::Indented => None, - pulldown_cmark::CodeBlockKind::Fenced(language) => { - if language.is_empty() { - None - } else { - Some(language.to_string()) - } - } - }; - - self.cursor += 1; - - let code_block = self.parse_code_block(language).await?; - Some(vec![ParsedMarkdownElement::CodeBlock(code_block)]) - } - Tag::HtmlBlock => { - self.cursor += 1; - - Some(self.parse_html_block().await) - } - _ => None, - }, - Event::Rule => { - self.cursor += 1; - Some(vec![ParsedMarkdownElement::HorizontalRule(source_range)]) - } - _ => None, - } - } - - fn parse_text( - &mut self, - should_complete_on_soft_break: bool, - source_range: Option>, - ) -> MarkdownParagraph { - let source_range = source_range.unwrap_or_else(|| { - self.current() - .map(|(_, range)| range.clone()) - .unwrap_or_default() - }); - - let mut markdown_text_like = Vec::new(); - let mut text = String::new(); - let mut bold_depth = 0; - let mut italic_depth = 0; - let mut strikethrough_depth = 0; - let mut link: Option = None; - let mut image: Option = None; - let mut regions: Vec<(Range, ParsedRegion)> = vec![]; - let mut highlights: Vec<(Range, MarkdownHighlight)> = vec![]; - let mut link_urls: Vec = vec![]; - let mut link_ranges: Vec> = vec![]; - - loop { - if self.eof() { - break; - } - - let (current, _) = self.current().unwrap(); - let prev_len = text.len(); - match current { - Event::SoftBreak => { - if should_complete_on_soft_break { - break; - } - text.push(' '); - } - - Event::HardBreak => { - text.push('\n'); - } - - // We want to ignore any inline HTML tags in the text but keep - // the text between them - Event::InlineHtml(_) => {} - - Event::Text(t) => { - text.push_str(t.as_ref()); - let mut style = MarkdownHighlightStyle::default(); - - if bold_depth > 0 { - style.weight = FontWeight::BOLD; - } - - if italic_depth > 0 { - style.italic = true; - } - - if strikethrough_depth > 0 { - style.strikethrough = true; - } - - let last_run_len = if let Some(link) = link.clone() { - regions.push(( - prev_len..text.len(), - ParsedRegion { - code: false, - link: Some(link), - }, - )); - style.link = true; - prev_len - } else { - // Manually scan for links - let mut finder = linkify::LinkFinder::new(); - finder.kinds(&[linkify::LinkKind::Url]); - let mut last_link_len = prev_len; - for link in finder.links(t) { - let start = prev_len + link.start(); - let end = prev_len + link.end(); - let range = start..end; - link_ranges.push(range.clone()); - link_urls.push(link.as_str().to_string()); - - // If there is a style before we match a link, we have to add this to the highlighted ranges - if style != MarkdownHighlightStyle::default() && last_link_len < start { - highlights.push(( - last_link_len..start, - MarkdownHighlight::Style(style.clone()), - )); - } - - highlights.push(( - range.clone(), - MarkdownHighlight::Style(MarkdownHighlightStyle { - underline: true, - ..style - }), - )); - - regions.push(( - range.clone(), - ParsedRegion { - code: false, - link: Some(Link::Web { - url: link.as_str().to_string(), - }), - }, - )); - last_link_len = end; - } - last_link_len - }; - - 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 new_highlight { - highlights.push(( - last_run_len..text.len(), - MarkdownHighlight::Style(style.clone()), - )); - } - } - } - Event::Code(t) => { - text.push_str(t.as_ref()); - let range = prev_len..text.len(); - - if link.is_some() { - highlights.push(( - range.clone(), - MarkdownHighlight::Style(MarkdownHighlightStyle { - link: true, - ..Default::default() - }), - )); - } - regions.push(( - range, - ParsedRegion { - code: true, - link: link.clone(), - }, - )); - } - Event::Start(tag) => match tag { - Tag::Emphasis => italic_depth += 1, - Tag::Strong => bold_depth += 1, - Tag::Strikethrough => strikethrough_depth += 1, - Tag::Link { dest_url, .. } => { - link = Link::identify( - self.file_location_directory.clone(), - dest_url.to_string(), - ); - } - Tag::Image { dest_url, .. } => { - if !text.is_empty() { - let parsed_regions = MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: source_range.clone(), - contents: mem::take(&mut text).into(), - highlights: mem::take(&mut highlights), - regions: mem::take(&mut regions), - }); - markdown_text_like.push(parsed_regions); - } - image = Image::identify( - dest_url.to_string(), - source_range.clone(), - self.file_location_directory.clone(), - ); - } - _ => { - break; - } - }, - - Event::End(tag) => match tag { - TagEnd::Emphasis => italic_depth -= 1, - TagEnd::Strong => bold_depth -= 1, - TagEnd::Strikethrough => strikethrough_depth -= 1, - TagEnd::Link => { - link = None; - } - TagEnd::Image => { - if let Some(mut image) = image.take() { - if !text.is_empty() { - image.set_alt_text(std::mem::take(&mut text).into()); - mem::take(&mut highlights); - mem::take(&mut regions); - } - markdown_text_like.push(MarkdownParagraphChunk::Image(image)); - } - } - TagEnd::Paragraph => { - self.cursor += 1; - break; - } - _ => { - break; - } - }, - _ => { - break; - } - } - - self.cursor += 1; - } - if !text.is_empty() { - markdown_text_like.push(MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range, - contents: text.into(), - highlights, - regions, - })); - } - markdown_text_like - } - - fn parse_heading(&mut self, level: pulldown_cmark::HeadingLevel) -> ParsedMarkdownHeading { - let (_event, source_range) = self.previous().unwrap(); - let source_range = source_range.clone(); - let text = self.parse_text(true, None); - - // Advance past the heading end tag - self.cursor += 1; - - ParsedMarkdownHeading { - source_range, - level: match level { - pulldown_cmark::HeadingLevel::H1 => HeadingLevel::H1, - pulldown_cmark::HeadingLevel::H2 => HeadingLevel::H2, - pulldown_cmark::HeadingLevel::H3 => HeadingLevel::H3, - pulldown_cmark::HeadingLevel::H4 => HeadingLevel::H4, - pulldown_cmark::HeadingLevel::H5 => HeadingLevel::H5, - pulldown_cmark::HeadingLevel::H6 => HeadingLevel::H6, - }, - contents: text, - } - } - - fn parse_table(&mut self, alignment: Vec) -> ParsedMarkdownTable { - let (_event, source_range) = self.previous().unwrap(); - let source_range = source_range.clone(); - let mut header = vec![]; - let mut body = vec![]; - let mut row_columns = vec![]; - let mut in_header = true; - let column_alignments = alignment - .iter() - .map(Self::convert_alignment) - .collect::>(); - - loop { - if self.eof() { - break; - } - - let (current, source_range) = self.current().unwrap(); - let source_range = source_range.clone(); - match current { - Event::Start(Tag::TableHead) - | Event::Start(Tag::TableRow) - | Event::End(TagEnd::TableCell) => { - self.cursor += 1; - } - Event::Start(Tag::TableCell) => { - self.cursor += 1; - let cell_contents = self.parse_text(false, Some(source_range)); - row_columns.push(ParsedMarkdownTableColumn { - col_span: 1, - row_span: 1, - is_header: in_header, - children: cell_contents, - alignment: column_alignments - .get(row_columns.len()) - .copied() - .unwrap_or_default(), - }); - } - Event::End(TagEnd::TableHead) | Event::End(TagEnd::TableRow) => { - self.cursor += 1; - let columns = std::mem::take(&mut row_columns); - if in_header { - header.push(ParsedMarkdownTableRow { columns: columns }); - in_header = false; - } else { - body.push(ParsedMarkdownTableRow::with_columns(columns)); - } - } - Event::End(TagEnd::Table) => { - self.cursor += 1; - break; - } - _ => { - break; - } - } - } - - ParsedMarkdownTable { - source_range, - header, - body, - caption: None, - } - } - - fn convert_alignment(alignment: &Alignment) -> ParsedMarkdownTableAlignment { - match alignment { - Alignment::None => ParsedMarkdownTableAlignment::None, - Alignment::Left => ParsedMarkdownTableAlignment::Left, - Alignment::Center => ParsedMarkdownTableAlignment::Center, - Alignment::Right => ParsedMarkdownTableAlignment::Right, - } - } - - async fn parse_list(&mut self, order: Option) -> Vec { - let (_, list_source_range) = self.previous().unwrap(); - - let mut items = Vec::new(); - let mut items_stack = vec![MarkdownListItem::default()]; - let mut depth = 1; - let mut order = order; - let mut order_stack = Vec::new(); - - let mut insertion_indices = FxHashMap::default(); - let mut source_ranges = FxHashMap::default(); - let mut start_item_range = list_source_range.clone(); - - while !self.eof() { - let (current, source_range) = self.current().unwrap(); - match current { - Event::Start(Tag::List(new_order)) => { - if items_stack.last().is_some() && !insertion_indices.contains_key(&depth) { - insertion_indices.insert(depth, items.len()); - } - - // We will use the start of the nested list as the end for the current item's range, - // because we don't care about the hierarchy of list items - if let collections::hash_map::Entry::Vacant(e) = source_ranges.entry(depth) { - e.insert(start_item_range.start..source_range.start); - } - - order_stack.push(order); - order = *new_order; - self.cursor += 1; - depth += 1; - } - Event::End(TagEnd::List(_)) => { - order = order_stack.pop().flatten(); - self.cursor += 1; - depth -= 1; - - if depth == 0 { - break; - } - } - Event::Start(Tag::Item) => { - start_item_range = source_range.clone(); - - self.cursor += 1; - items_stack.push(MarkdownListItem::default()); - - let mut task_list = None; - // Check for task list marker (`- [ ]` or `- [x]`) - if let Some(event) = self.current_event() { - // If there is a linebreak in between two list items the task list marker will actually be the first element of the paragraph - if event == &Event::Start(Tag::Paragraph) { - self.cursor += 1; - } - - if let Some((Event::TaskListMarker(checked), range)) = self.current() { - task_list = Some((*checked, range.clone())); - self.cursor += 1; - } - } - - if let Some((event, range)) = self.current() { - // This is a plain list item. - // For example `- some text` or `1. [Docs](./docs.md)` - if MarkdownParser::is_text_like(event) { - let text = self.parse_text(false, Some(range.clone())); - let block = ParsedMarkdownElement::Paragraph(text); - if let Some(content) = items_stack.last_mut() { - let item_type = if let Some((checked, range)) = task_list { - ParsedMarkdownListItemType::Task(checked, range) - } else if let Some(order) = order { - ParsedMarkdownListItemType::Ordered(order) - } else { - ParsedMarkdownListItemType::Unordered - }; - content.item_type = item_type; - content.content.push(block); - } - } 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 there is a linebreak in between two list items the task list marker will actually be the first element of the paragraph - if self.current_event() == Some(&Event::End(TagEnd::Paragraph)) { - self.cursor += 1; - } - } - Event::End(TagEnd::Item) => { - self.cursor += 1; - - if let Some(current) = order { - order = Some(current + 1); - } - - if let Some(list_item) = items_stack.pop() { - let source_range = source_ranges - .remove(&depth) - .unwrap_or(start_item_range.clone()); - - // We need to remove the last character of the source range, because it includes the newline character - let source_range = source_range.start..source_range.end - 1; - let item = ParsedMarkdownElement::ListItem(ParsedMarkdownListItem { - source_range, - content: list_item.content, - depth, - item_type: list_item.item_type, - nested: false, - }); - - if let Some(index) = insertion_indices.get(&depth) { - items.insert(*index, item); - insertion_indices.remove(&depth); - } else { - items.push(item); - } - } - } - _ => { - if depth == 0 { - break; - } - // This can only happen if a list item starts with more then one paragraph, - // or the list item contains blocks that should be rendered after the nested list items - let block = self.parse_block().await; - if let Some(block) = block { - if let Some(list_item) = items_stack.last_mut() { - // If we did not insert any nested items yet (in this case insertion index is set), we can append the block to the current list item - if !insertion_indices.contains_key(&depth) { - list_item.content.extend(block); - continue; - } - } - - // Otherwise we need to insert the block after all the nested items - // that have been parsed so far - items.extend(block); - } else { - self.cursor += 1; - } - } - } - } - - items - } - - #[async_recursion] - async fn parse_block_quote(&mut self) -> ParsedMarkdownBlockQuote { - let (_event, source_range) = self.previous().unwrap(); - let source_range = source_range.clone(); - let mut nested_depth = 1; - - let mut children: Vec = vec![]; - - while !self.eof() { - let block = self.parse_block().await; - - if let Some(block) = block { - children.extend(block); - } else { - break; - } - - if self.eof() { - break; - } - - let (current, _source_range) = self.current().unwrap(); - match current { - // This is a nested block quote. - // Record that we're in a nested block quote and continue parsing. - // We don't need to advance the cursor since the next - // call to `parse_block` will handle it. - Event::Start(Tag::BlockQuote(_kind)) => { - nested_depth += 1; - } - Event::End(TagEnd::BlockQuote(_kind)) => { - nested_depth -= 1; - if nested_depth == 0 { - self.cursor += 1; - break; - } - } - _ => {} - }; - } - - ParsedMarkdownBlockQuote { - source_range, - children, - } - } - - async fn parse_code_block( - &mut self, - language: Option, - ) -> Option { - let Some((_event, source_range)) = self.previous() else { - return None; - }; - - let source_range = source_range.clone(); - let mut code = String::new(); - - while !self.eof() { - let Some((current, _source_range)) = self.current() else { - break; - }; - - match current { - Event::Text(text) => { - code.push_str(text); - self.cursor += 1; - } - Event::End(TagEnd::CodeBlock) => { - self.cursor += 1; - break; - } - _ => { - break; - } - } - } - - code = code.strip_suffix('\n').unwrap_or(&code).to_string(); - - let highlights = if let Some(language) = &language { - if let Some(registry) = &self.language_registry { - let rope: language::Rope = code.as_str().into(); - registry - .language_for_name_or_extension(language) - .await - .map(|l| l.highlight_text(&rope, 0..code.len())) - .ok() - } else { - None - } - } else { - None - }; - - Some(ParsedMarkdownCodeBlock { - source_range, - contents: code.into(), - language, - highlights, - }) - } - - async fn parse_html_block(&mut self) -> Vec { - let mut elements = Vec::new(); - let Some((_event, _source_range)) = self.previous() else { - return elements; - }; - - let mut html_source_range_start = None; - let mut html_source_range_end = None; - let mut html_buffer = String::new(); - - while !self.eof() { - let Some((current, source_range)) = self.current() else { - break; - }; - let source_range = source_range.clone(); - match current { - Event::Html(html) => { - html_source_range_start.get_or_insert(source_range.start); - html_source_range_end = Some(source_range.end); - html_buffer.push_str(html); - self.cursor += 1; - } - Event::End(TagEnd::CodeBlock) => { - self.cursor += 1; - break; - } - _ => { - break; - } - } - } - - let bytes = cleanup_html(&html_buffer); - - let mut cursor = std::io::Cursor::new(bytes); - if let Ok(dom) = parse_document(RcDom::default(), ParseOpts::default()) - .from_utf8() - .read_from(&mut cursor) - && let Some((start, end)) = html_source_range_start.zip(html_source_range_end) - { - self.parse_html_node( - start..end, - &dom.document, - &mut elements, - &ParseHtmlNodeContext::default(), - ); - } - - elements - } - - fn parse_html_node( - &self, - source_range: Range, - node: &Rc, - elements: &mut Vec, - context: &ParseHtmlNodeContext, - ) { - match &node.data { - markup5ever_rcdom::NodeData::Document => { - self.consume_children(source_range, node, elements, context); - } - markup5ever_rcdom::NodeData::Text { contents } => { - elements.push(ParsedMarkdownElement::Paragraph(vec![ - MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range, - regions: Vec::default(), - highlights: Vec::default(), - contents: contents.borrow().to_string().into(), - }), - ])); - } - markup5ever_rcdom::NodeData::Comment { .. } => {} - markup5ever_rcdom::NodeData::Element { name, attrs, .. } => { - let mut styles = if let Some(styles) = Self::markdown_style_from_html_styles( - Self::extract_styles_from_attributes(attrs), - ) { - vec![MarkdownHighlight::Style(styles)] - } else { - Vec::default() - }; - - if local_name!("img") == name.local { - if let Some(image) = self.extract_image(source_range, attrs) { - elements.push(ParsedMarkdownElement::Image(image)); - } - } else if local_name!("p") == name.local { - let mut paragraph = MarkdownParagraph::new(); - self.parse_paragraph( - source_range, - node, - &mut paragraph, - &mut styles, - &mut Vec::new(), - ); - - if !paragraph.is_empty() { - elements.push(ParsedMarkdownElement::Paragraph(paragraph)); - } - } else if matches!( - name.local, - local_name!("h1") - | local_name!("h2") - | local_name!("h3") - | local_name!("h4") - | local_name!("h5") - | local_name!("h6") - ) { - let mut paragraph = MarkdownParagraph::new(); - self.consume_paragraph( - source_range.clone(), - node, - &mut paragraph, - &mut styles, - &mut Vec::new(), - ); - - if !paragraph.is_empty() { - elements.push(ParsedMarkdownElement::Heading(ParsedMarkdownHeading { - source_range, - level: match name.local { - local_name!("h1") => HeadingLevel::H1, - local_name!("h2") => HeadingLevel::H2, - local_name!("h3") => HeadingLevel::H3, - local_name!("h4") => HeadingLevel::H4, - local_name!("h5") => HeadingLevel::H5, - local_name!("h6") => HeadingLevel::H6, - _ => unreachable!(), - }, - contents: paragraph, - })); - } - } else if local_name!("ul") == name.local || local_name!("ol") == name.local { - if let Some(list_items) = self.extract_html_list( - node, - local_name!("ol") == name.local, - context.list_item_depth, - source_range, - ) { - elements.extend(list_items); - } - } else if local_name!("blockquote") == name.local { - if let Some(blockquote) = self.extract_html_blockquote(node, source_range) { - elements.push(ParsedMarkdownElement::BlockQuote(blockquote)); - } - } else if local_name!("table") == name.local { - if let Some(table) = self.extract_html_table(node, source_range) { - elements.push(ParsedMarkdownElement::Table(table)); - } - } else { - self.consume_children(source_range, node, elements, context); - } - } - _ => {} - } - } - - fn parse_paragraph( - &self, - source_range: Range, - node: &Rc, - paragraph: &mut MarkdownParagraph, - highlights: &mut Vec, - regions: &mut Vec<(Range, ParsedRegion)>, - ) { - fn items_with_range( - range: Range, - items: impl IntoIterator, - ) -> Vec<(Range, T)> { - items - .into_iter() - .map(|item| (range.clone(), item)) - .collect() - } - - match &node.data { - markup5ever_rcdom::NodeData::Text { contents } => { - // append the text to the last chunk, so we can have a hacky version - // of inline text with highlighting - if let Some(text) = paragraph.iter_mut().last().and_then(|p| match p { - MarkdownParagraphChunk::Text(text) => Some(text), - _ => None, - }) { - let mut new_text = text.contents.to_string(); - new_text.push_str(&contents.borrow()); - - text.highlights.extend(items_with_range( - text.contents.len()..new_text.len(), - std::mem::take(highlights), - )); - text.regions.extend(items_with_range( - text.contents.len()..new_text.len(), - std::mem::take(regions) - .into_iter() - .map(|(_, region)| region), - )); - text.contents = SharedString::from(new_text); - } else { - let contents = contents.borrow().to_string(); - paragraph.push(MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range, - highlights: items_with_range(0..contents.len(), std::mem::take(highlights)), - regions: items_with_range( - 0..contents.len(), - std::mem::take(regions) - .into_iter() - .map(|(_, region)| region), - ), - contents: contents.into(), - })); - } - } - markup5ever_rcdom::NodeData::Element { name, attrs, .. } => { - if local_name!("img") == name.local { - if let Some(image) = self.extract_image(source_range, attrs) { - paragraph.push(MarkdownParagraphChunk::Image(image)); - } - } else if local_name!("b") == name.local || local_name!("strong") == name.local { - highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle { - weight: FontWeight::BOLD, - ..Default::default() - })); - - self.consume_paragraph(source_range, node, paragraph, highlights, regions); - } else if local_name!("i") == name.local { - highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle { - italic: true, - ..Default::default() - })); - - self.consume_paragraph(source_range, node, paragraph, highlights, regions); - } else if local_name!("em") == name.local { - highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle { - oblique: true, - ..Default::default() - })); - - self.consume_paragraph(source_range, node, paragraph, highlights, regions); - } else if local_name!("del") == name.local { - highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle { - strikethrough: true, - ..Default::default() - })); - - self.consume_paragraph(source_range, node, paragraph, highlights, regions); - } else if local_name!("ins") == name.local { - highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle { - underline: true, - ..Default::default() - })); - - self.consume_paragraph(source_range, node, paragraph, highlights, regions); - } else if local_name!("a") == name.local { - if let Some(url) = Self::attr_value(attrs, local_name!("href")) - && let Some(link) = - Link::identify(self.file_location_directory.clone(), url) - { - highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle { - link: true, - ..Default::default() - })); - - regions.push(( - source_range.clone(), - ParsedRegion { - code: false, - link: Some(link), - }, - )); - } - - self.consume_paragraph(source_range, node, paragraph, highlights, regions); - } else { - self.consume_paragraph(source_range, node, paragraph, highlights, regions); - } - } - _ => {} - } - } - - fn consume_paragraph( - &self, - source_range: Range, - node: &Rc, - paragraph: &mut MarkdownParagraph, - highlights: &mut Vec, - regions: &mut Vec<(Range, ParsedRegion)>, - ) { - for node in node.children.borrow().iter() { - self.parse_paragraph(source_range.clone(), node, paragraph, highlights, regions); - } - } - - fn parse_table_row( - &self, - source_range: Range, - node: &Rc, - ) -> Option { - let mut columns = Vec::new(); - - match &node.data { - markup5ever_rcdom::NodeData::Element { name, .. } => { - if local_name!("tr") != name.local { - return None; - } - - for node in node.children.borrow().iter() { - if let Some(column) = self.parse_table_column(source_range.clone(), node) { - columns.push(column); - } - } - } - _ => {} - } - - if columns.is_empty() { - None - } else { - Some(ParsedMarkdownTableRow { columns }) - } - } - - fn parse_table_column( - &self, - source_range: Range, - node: &Rc, - ) -> Option { - match &node.data { - markup5ever_rcdom::NodeData::Element { name, attrs, .. } => { - if !matches!(name.local, local_name!("th") | local_name!("td")) { - return None; - } - - let mut children = MarkdownParagraph::new(); - self.consume_paragraph( - source_range, - node, - &mut children, - &mut Vec::new(), - &mut Vec::new(), - ); - - let is_header = matches!(name.local, local_name!("th")); - - Some(ParsedMarkdownTableColumn { - col_span: std::cmp::max( - Self::attr_value(attrs, local_name!("colspan")) - .and_then(|span| span.parse().ok()) - .unwrap_or(1), - 1, - ), - row_span: std::cmp::max( - Self::attr_value(attrs, local_name!("rowspan")) - .and_then(|span| span.parse().ok()) - .unwrap_or(1), - 1, - ), - is_header, - children, - alignment: Self::attr_value(attrs, local_name!("align")) - .and_then(|align| match align.as_str() { - "left" => Some(ParsedMarkdownTableAlignment::Left), - "center" => Some(ParsedMarkdownTableAlignment::Center), - "right" => Some(ParsedMarkdownTableAlignment::Right), - _ => None, - }) - .unwrap_or_else(|| { - if is_header { - ParsedMarkdownTableAlignment::Center - } else { - ParsedMarkdownTableAlignment::default() - } - }), - }) - } - _ => None, - } - } - - fn consume_children( - &self, - source_range: Range, - node: &Rc, - elements: &mut Vec, - context: &ParseHtmlNodeContext, - ) { - for node in node.children.borrow().iter() { - self.parse_html_node(source_range.clone(), node, elements, context); - } - } - - fn attr_value( - attrs: &RefCell>, - name: html5ever::LocalName, - ) -> Option { - attrs.borrow().iter().find_map(|attr| { - if attr.name.local == name { - Some(attr.value.to_string()) - } else { - None - } - }) - } - - fn markdown_style_from_html_styles( - styles: HashMap, - ) -> Option { - let mut markdown_style = MarkdownHighlightStyle::default(); - - if let Some(text_decoration) = styles.get("text-decoration") { - match text_decoration.to_lowercase().as_str() { - "underline" => { - markdown_style.underline = true; - } - "line-through" => { - markdown_style.strikethrough = true; - } - _ => {} - } - } - - if let Some(font_style) = styles.get("font-style") { - match font_style.to_lowercase().as_str() { - "italic" => { - markdown_style.italic = true; - } - "oblique" => { - markdown_style.oblique = true; - } - _ => {} - } - } - - if let Some(font_weight) = styles.get("font-weight") { - match font_weight.to_lowercase().as_str() { - "bold" => { - markdown_style.weight = FontWeight::BOLD; - } - "lighter" => { - markdown_style.weight = FontWeight::THIN; - } - _ => { - if let Some(weight) = font_weight.parse::().ok() { - markdown_style.weight = FontWeight(weight); - } - } - } - } - - if markdown_style != MarkdownHighlightStyle::default() { - Some(markdown_style) - } else { - None - } - } - - fn extract_styles_from_attributes( - attrs: &RefCell>, - ) -> HashMap { - let mut styles = HashMap::new(); - - if let Some(style) = Self::attr_value(attrs, local_name!("style")) { - for decl in style.split(';') { - let mut parts = decl.splitn(2, ':'); - if let Some((key, value)) = parts.next().zip(parts.next()) { - styles.insert( - key.trim().to_lowercase().to_string(), - value.trim().to_string(), - ); - } - } - } - - styles - } - - fn extract_image( - &self, - source_range: Range, - attrs: &RefCell>, - ) -> Option { - let src = Self::attr_value(attrs, local_name!("src"))?; - - let mut image = Image::identify(src, source_range, self.file_location_directory.clone())?; - - if let Some(alt) = Self::attr_value(attrs, local_name!("alt")) { - image.set_alt_text(alt.into()); - } - - let styles = Self::extract_styles_from_attributes(attrs); - - if let Some(width) = Self::attr_value(attrs, local_name!("width")) - .or_else(|| styles.get("width").cloned()) - .and_then(|width| Self::parse_html_element_dimension(&width)) - { - image.set_width(width); - } - - if let Some(height) = Self::attr_value(attrs, local_name!("height")) - .or_else(|| styles.get("height").cloned()) - .and_then(|height| Self::parse_html_element_dimension(&height)) - { - image.set_height(height); - } - - Some(image) - } - - fn extract_html_list( - &self, - node: &Rc, - ordered: bool, - depth: u16, - source_range: Range, - ) -> Option> { - let mut list_items = Vec::with_capacity(node.children.borrow().len()); - - for (index, node) in node.children.borrow().iter().enumerate() { - match &node.data { - markup5ever_rcdom::NodeData::Element { name, .. } => { - if local_name!("li") != name.local { - continue; - } - - let mut content = Vec::new(); - self.consume_children( - source_range.clone(), - node, - &mut content, - &ParseHtmlNodeContext { - list_item_depth: depth + 1, - }, - ); - - if !content.is_empty() { - list_items.push(ParsedMarkdownElement::ListItem(ParsedMarkdownListItem { - depth, - source_range: source_range.clone(), - item_type: if ordered { - ParsedMarkdownListItemType::Ordered(index as u64 + 1) - } else { - ParsedMarkdownListItemType::Unordered - }, - content, - nested: true, - })); - } - } - _ => {} - } - } - - if list_items.is_empty() { - None - } else { - Some(list_items) - } - } - - fn parse_html_element_dimension(value: &str) -> Option { - if value.ends_with("%") { - value - .trim_end_matches("%") - .parse::() - .ok() - .map(|value| relative(value / 100.)) - } else { - value - .trim_end_matches("px") - .parse() - .ok() - .map(|value| px(value).into()) - } - } - - fn extract_html_blockquote( - &self, - node: &Rc, - source_range: Range, - ) -> Option { - let mut children = Vec::new(); - self.consume_children( - source_range.clone(), - node, - &mut children, - &ParseHtmlNodeContext::default(), - ); - - if children.is_empty() { - None - } else { - Some(ParsedMarkdownBlockQuote { - children, - source_range, - }) - } - } - - fn extract_html_table( - &self, - node: &Rc, - source_range: Range, - ) -> Option { - let mut header_rows = Vec::new(); - let mut body_rows = Vec::new(); - let mut caption = None; - - // node should be a thead, tbody or caption element - for node in node.children.borrow().iter() { - match &node.data { - markup5ever_rcdom::NodeData::Element { name, .. } => { - if local_name!("caption") == name.local { - let mut paragraph = MarkdownParagraph::new(); - self.parse_paragraph( - source_range.clone(), - node, - &mut paragraph, - &mut Vec::new(), - &mut Vec::new(), - ); - caption = Some(paragraph); - } - if local_name!("thead") == name.local { - // node should be a tr element - for node in node.children.borrow().iter() { - if let Some(row) = self.parse_table_row(source_range.clone(), node) { - header_rows.push(row); - } - } - } else if local_name!("tbody") == name.local { - // node should be a tr element - for node in node.children.borrow().iter() { - if let Some(row) = self.parse_table_row(source_range.clone(), node) { - body_rows.push(row); - } - } - } - } - _ => {} - } - } - - if !header_rows.is_empty() || !body_rows.is_empty() { - Some(ParsedMarkdownTable { - source_range, - body: body_rows, - header: header_rows, - caption, - }) - } else { - None - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ParsedMarkdownListItemType::*; - use core::panic; - use gpui::{AbsoluteLength, BackgroundExecutor, DefiniteLength}; - use language::{HighlightId, LanguageRegistry}; - use pretty_assertions::assert_eq; - - async fn parse(input: &str) -> ParsedMarkdown { - parse_markdown(input, None, None).await - } - - #[gpui::test] - async fn test_headings() { - let parsed = parse("# Heading one\n## Heading two\n### Heading three").await; - - assert_eq!( - parsed.children, - vec![ - h1(text("Heading one", 2..13), 0..14), - h2(text("Heading two", 17..28), 14..29), - h3(text("Heading three", 33..46), 29..46), - ] - ); - } - - #[gpui::test] - async fn test_newlines_dont_new_paragraphs() { - let parsed = parse("Some text **that is bolded**\n and *italicized*").await; - - assert_eq!( - parsed.children, - vec![p("Some text that is bolded and italicized", 0..46)] - ); - } - - #[gpui::test] - async fn test_heading_with_paragraph() { - let parsed = parse("# Zed\nThe editor").await; - - assert_eq!( - parsed.children, - vec![h1(text("Zed", 2..5), 0..6), p("The editor", 6..16),] - ); - } - - #[gpui::test] - async fn test_double_newlines_do_new_paragraphs() { - let parsed = parse("Some text **that is bolded**\n\n and *italicized*").await; - - assert_eq!( - parsed.children, - vec![ - p("Some text that is bolded", 0..29), - p("and italicized", 31..47), - ] - ); - } - - #[gpui::test] - async fn test_bold_italic_text() { - let parsed = parse("Some text **that is bolded** and *italicized*").await; - - assert_eq!( - parsed.children, - vec![p("Some text that is bolded and italicized", 0..45)] - ); - } - - #[gpui::test] - async fn test_nested_bold_strikethrough_text() { - let parsed = parse("Some **bo~~strikethrough~~ld** text").await; - - assert_eq!(parsed.children.len(), 1); - assert_eq!( - parsed.children[0], - ParsedMarkdownElement::Paragraph(vec![MarkdownParagraphChunk::Text( - ParsedMarkdownText { - source_range: 0..35, - contents: "Some bostrikethroughld text".into(), - highlights: Vec::new(), - regions: Vec::new(), - } - )]) - ); - - let new_text = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { - text - } else { - panic!("Expected a paragraph"); - }; - - let paragraph = if let MarkdownParagraphChunk::Text(text) = &new_text[0] { - text - } else { - panic!("Expected a text"); - }; - - assert_eq!( - paragraph.highlights, - vec![ - ( - 5..7, - MarkdownHighlight::Style(MarkdownHighlightStyle { - weight: FontWeight::BOLD, - ..Default::default() - }), - ), - ( - 7..20, - MarkdownHighlight::Style(MarkdownHighlightStyle { - weight: FontWeight::BOLD, - strikethrough: true, - ..Default::default() - }), - ), - ( - 20..22, - MarkdownHighlight::Style(MarkdownHighlightStyle { - weight: FontWeight::BOLD, - ..Default::default() - }), - ), - ] - ); - } - - #[gpui::test] - async fn test_html_inline_style_elements() { - let parsed = - parse("

Some text strong text more text bold text more text italic text more text emphasized text more text deleted text more text inserted text

").await; - - assert_eq!(1, parsed.children.len()); - let chunks = if let ParsedMarkdownElement::Paragraph(chunks) = &parsed.children[0] { - chunks - } else { - panic!("Expected a paragraph"); - }; - - assert_eq!(1, chunks.len()); - let text = if let MarkdownParagraphChunk::Text(text) = &chunks[0] { - text - } else { - panic!("Expected a paragraph"); - }; - - assert_eq!(0..205, text.source_range); - assert_eq!( - "Some text strong text more text bold text more text italic text more text emphasized text more text deleted text more text inserted text", - text.contents.as_str(), - ); - assert_eq!( - vec![ - ( - 10..21, - MarkdownHighlight::Style(MarkdownHighlightStyle { - weight: FontWeight(700.0), - ..Default::default() - },), - ), - ( - 32..41, - MarkdownHighlight::Style(MarkdownHighlightStyle { - weight: FontWeight(700.0), - ..Default::default() - },), - ), - ( - 52..63, - MarkdownHighlight::Style(MarkdownHighlightStyle { - italic: true, - weight: FontWeight(400.0), - ..Default::default() - },), - ), - ( - 74..89, - MarkdownHighlight::Style(MarkdownHighlightStyle { - weight: FontWeight(400.0), - oblique: true, - ..Default::default() - },), - ), - ( - 100..112, - MarkdownHighlight::Style(MarkdownHighlightStyle { - strikethrough: true, - weight: FontWeight(400.0), - ..Default::default() - },), - ), - ( - 123..136, - MarkdownHighlight::Style(MarkdownHighlightStyle { - underline: true, - weight: FontWeight(400.0,), - ..Default::default() - },), - ), - ], - text.highlights - ); - } - - #[gpui::test] - async fn test_html_href_element() { - let parsed = - parse("
").await; - - assert_eq!(1, parsed.children.len()); - let chunks = if let ParsedMarkdownElement::Paragraph(chunks) = &parsed.children[0] { - chunks - } else { - panic!("Expected a paragraph"); - }; - - assert_eq!(1, chunks.len()); - let text = if let MarkdownParagraphChunk::Text(text) = &chunks[0] { - text - } else { - panic!("Expected a paragraph"); - }; - - assert_eq!(0..65, text.source_range); - assert_eq!("Some text link more text", text.contents.as_str(),); - assert_eq!( - vec![( - 10..14, - MarkdownHighlight::Style(MarkdownHighlightStyle { - link: true, - ..Default::default() - },), - )], - text.highlights - ); - assert_eq!( - vec![( - 10..14, - ParsedRegion { - code: false, - link: Some(Link::Web { - url: "https://example.com".into() - }) - } - )], - text.regions - ) - } - - #[gpui::test] - async fn test_text_with_inline_html() { - let parsed = parse("This is a paragraph with an inline HTML tag.").await; - - assert_eq!( - parsed.children, - vec![p("This is a paragraph with an inline HTML tag.", 0..63),], - ); - } - - #[gpui::test] - async fn test_raw_links_detection() { - let parsed = parse("Checkout this https://zed.dev link").await; - - assert_eq!( - parsed.children, - vec![p("Checkout this https://zed.dev link", 0..34)] - ); - } - - #[gpui::test] - async fn test_empty_image() { - let parsed = parse("![]()").await; - - let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { - text - } else { - panic!("Expected a paragraph"); - }; - assert_eq!(paragraph.len(), 0); - } - - #[gpui::test] - async fn test_image_links_detection() { - let parsed = parse("![test](https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png)").await; - - let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { - text - } else { - panic!("Expected a paragraph"); - }; - assert_eq!( - paragraph[0], - MarkdownParagraphChunk::Image(Image { - source_range: 0..111, - link: Link::Web { - url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(), - }, - alt_text: Some("test".into()), - height: None, - width: None, - },) - ); - } - - #[gpui::test] - async fn test_image_alt_text() { - let parsed = parse("[![Zed](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json)](https://zed.dev)\n ").await; - - let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { - text - } else { - panic!("Expected a paragraph"); - }; - assert_eq!( - paragraph[0], - MarkdownParagraphChunk::Image(Image { - source_range: 0..142, - link: Link::Web { - url: "https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json".to_string(), - }, - alt_text: Some("Zed".into()), - height: None, - width: None, - },) - ); - } - - #[gpui::test] - async fn test_image_without_alt_text() { - let parsed = parse("![](http://example.com/foo.png)").await; - - let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { - text - } else { - panic!("Expected a paragraph"); - }; - assert_eq!( - paragraph[0], - MarkdownParagraphChunk::Image(Image { - source_range: 0..31, - link: Link::Web { - url: "http://example.com/foo.png".to_string(), - }, - alt_text: None, - height: None, - width: None, - },) - ); - } - - #[gpui::test] - async fn test_image_with_alt_text_containing_formatting() { - let parsed = parse("![foo *bar* baz](http://example.com/foo.png)").await; - - let ParsedMarkdownElement::Paragraph(chunks) = &parsed.children[0] else { - panic!("Expected a paragraph"); - }; - assert_eq!( - chunks, - &[MarkdownParagraphChunk::Image(Image { - source_range: 0..44, - link: Link::Web { - url: "http://example.com/foo.png".to_string(), - }, - alt_text: Some("foo bar baz".into()), - height: None, - width: None, - }),], - ); - } - - #[gpui::test] - async fn test_images_with_text_in_between() { - let parsed = parse( - "![foo](http://example.com/foo.png)\nLorem Ipsum\n![bar](http://example.com/bar.png)", - ) - .await; - - let chunks = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { - text - } else { - panic!("Expected a paragraph"); - }; - assert_eq!( - chunks, - &vec![ - MarkdownParagraphChunk::Image(Image { - source_range: 0..81, - link: Link::Web { - url: "http://example.com/foo.png".to_string(), - }, - alt_text: Some("foo".into()), - height: None, - width: None, - }), - MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..81, - contents: " Lorem Ipsum ".into(), - highlights: Vec::new(), - regions: Vec::new(), - }), - MarkdownParagraphChunk::Image(Image { - source_range: 0..81, - link: Link::Web { - url: "http://example.com/bar.png".to_string(), - }, - alt_text: Some("bar".into()), - height: None, - width: None, - }) - ] - ); - } - - #[test] - fn test_parse_html_element_dimension() { - // Test percentage values - assert_eq!( - MarkdownParser::parse_html_element_dimension("50%"), - Some(DefiniteLength::Fraction(0.5)) - ); - assert_eq!( - MarkdownParser::parse_html_element_dimension("100%"), - Some(DefiniteLength::Fraction(1.0)) - ); - assert_eq!( - MarkdownParser::parse_html_element_dimension("25%"), - Some(DefiniteLength::Fraction(0.25)) - ); - assert_eq!( - MarkdownParser::parse_html_element_dimension("0%"), - Some(DefiniteLength::Fraction(0.0)) - ); - - // Test pixel values - assert_eq!( - MarkdownParser::parse_html_element_dimension("100px"), - Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.0)))) - ); - assert_eq!( - MarkdownParser::parse_html_element_dimension("50px"), - Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(50.0)))) - ); - assert_eq!( - MarkdownParser::parse_html_element_dimension("0px"), - Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(0.0)))) - ); - - // Test values without units (should be treated as pixels) - assert_eq!( - MarkdownParser::parse_html_element_dimension("100"), - Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.0)))) - ); - assert_eq!( - MarkdownParser::parse_html_element_dimension("42"), - Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0)))) - ); - - // Test invalid values - assert_eq!( - MarkdownParser::parse_html_element_dimension("invalid"), - None - ); - assert_eq!(MarkdownParser::parse_html_element_dimension("px"), None); - assert_eq!(MarkdownParser::parse_html_element_dimension("%"), None); - assert_eq!(MarkdownParser::parse_html_element_dimension(""), None); - assert_eq!(MarkdownParser::parse_html_element_dimension("abc%"), None); - assert_eq!(MarkdownParser::parse_html_element_dimension("abcpx"), None); - - // Test decimal values - assert_eq!( - MarkdownParser::parse_html_element_dimension("50.5%"), - Some(DefiniteLength::Fraction(0.505)) - ); - assert_eq!( - MarkdownParser::parse_html_element_dimension("100.25px"), - Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.25)))) - ); - assert_eq!( - MarkdownParser::parse_html_element_dimension("42.0"), - Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0)))) - ); - } - - #[gpui::test] - async fn test_html_unordered_list() { - let parsed = parse( - "
    -
  • Item 1
  • -
  • Item 2
  • -
", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![ - nested_list_item( - 0..82, - 1, - ParsedMarkdownListItemType::Unordered, - vec![ParsedMarkdownElement::Paragraph(text("Item 1", 0..82))] - ), - nested_list_item( - 0..82, - 1, - ParsedMarkdownListItemType::Unordered, - vec![ParsedMarkdownElement::Paragraph(text("Item 2", 0..82))] - ), - ] - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_ordered_list() { - let parsed = parse( - "
    -
  1. Item 1
  2. -
  3. Item 2
  4. -
", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![ - nested_list_item( - 0..82, - 1, - ParsedMarkdownListItemType::Ordered(1), - vec![ParsedMarkdownElement::Paragraph(text("Item 1", 0..82))] - ), - nested_list_item( - 0..82, - 1, - ParsedMarkdownListItemType::Ordered(2), - vec![ParsedMarkdownElement::Paragraph(text("Item 2", 0..82))] - ), - ] - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_nested_ordered_list() { - let parsed = parse( - "
    -
  1. Item 1
  2. -
  3. Item 2 -
      -
    1. Sub-Item 1
    2. -
    3. Sub-Item 2
    4. -
    -
  4. -
", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![ - nested_list_item( - 0..216, - 1, - ParsedMarkdownListItemType::Ordered(1), - vec![ParsedMarkdownElement::Paragraph(text("Item 1", 0..216))] - ), - nested_list_item( - 0..216, - 1, - ParsedMarkdownListItemType::Ordered(2), - vec![ - ParsedMarkdownElement::Paragraph(text("Item 2", 0..216)), - nested_list_item( - 0..216, - 2, - ParsedMarkdownListItemType::Ordered(1), - vec![ParsedMarkdownElement::Paragraph(text("Sub-Item 1", 0..216))] - ), - nested_list_item( - 0..216, - 2, - ParsedMarkdownListItemType::Ordered(2), - vec![ParsedMarkdownElement::Paragraph(text("Sub-Item 2", 0..216))] - ), - ] - ), - ] - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_nested_unordered_list() { - let parsed = parse( - "
    -
  • Item 1
  • -
  • Item 2 -
      -
    • Sub-Item 1
    • -
    • Sub-Item 2
    • -
    -
  • -
", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![ - nested_list_item( - 0..216, - 1, - ParsedMarkdownListItemType::Unordered, - vec![ParsedMarkdownElement::Paragraph(text("Item 1", 0..216))] - ), - nested_list_item( - 0..216, - 1, - ParsedMarkdownListItemType::Unordered, - vec![ - ParsedMarkdownElement::Paragraph(text("Item 2", 0..216)), - nested_list_item( - 0..216, - 2, - ParsedMarkdownListItemType::Unordered, - vec![ParsedMarkdownElement::Paragraph(text("Sub-Item 1", 0..216))] - ), - nested_list_item( - 0..216, - 2, - ParsedMarkdownListItemType::Unordered, - vec![ParsedMarkdownElement::Paragraph(text("Sub-Item 2", 0..216))] - ), - ] - ), - ] - }, - parsed - ); - } - - #[gpui::test] - async fn test_inline_html_image_tag() { - let parsed = - parse("

Some text some more text

") - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![ParsedMarkdownElement::Paragraph(vec![ - MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..71, - contents: "Some text".into(), - highlights: Default::default(), - regions: Default::default() - }), - MarkdownParagraphChunk::Image(Image { - source_range: 0..71, - link: Link::Web { - url: "http://example.com/foo.png".to_string(), - }, - alt_text: None, - height: None, - width: None, - }), - MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..71, - contents: " some more text".into(), - highlights: Default::default(), - regions: Default::default() - }), - ])] - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_block_quote() { - let parsed = parse( - "
-

some description

-
", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![block_quote( - vec![ParsedMarkdownElement::Paragraph(text( - "some description", - 0..78 - ))], - 0..78, - )] - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_nested_block_quote() { - let parsed = parse( - "
-

some description

-
-

second description

-
-
", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![block_quote( - vec![ - ParsedMarkdownElement::Paragraph(text("some description", 0..179)), - block_quote( - vec![ParsedMarkdownElement::Paragraph(text( - "second description", - 0..179 - ))], - 0..179, - ) - ], - 0..179, - )] - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_table() { - let parsed = parse( - " - - - - - - - - - - - - - - - - -
IdName
1Chris
2Dennis
", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![ParsedMarkdownElement::Table(table( - 0..366, - None, - vec![row(vec![ - column( - 1, - 1, - true, - text("Id", 0..366), - ParsedMarkdownTableAlignment::Center - ), - column( - 1, - 1, - true, - text("Name ", 0..366), - ParsedMarkdownTableAlignment::Center - ) - ])], - vec![ - row(vec![ - column( - 1, - 1, - false, - text("1", 0..366), - ParsedMarkdownTableAlignment::None - ), - column( - 1, - 1, - false, - text("Chris", 0..366), - ParsedMarkdownTableAlignment::None - ) - ]), - row(vec![ - column( - 1, - 1, - false, - text("2", 0..366), - ParsedMarkdownTableAlignment::None - ), - column( - 1, - 1, - false, - text("Dennis", 0..366), - ParsedMarkdownTableAlignment::None - ) - ]), - ], - ))], - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_table_with_caption() { - let parsed = parse( - " - - - - - - - - - - - -
My Table
1Chris
2Dennis
", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![ParsedMarkdownElement::Table(table( - 0..280, - Some(vec![MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..280, - contents: "My Table".into(), - highlights: Default::default(), - regions: Default::default() - })]), - vec![], - vec![ - row(vec![ - column( - 1, - 1, - false, - text("1", 0..280), - ParsedMarkdownTableAlignment::None - ), - column( - 1, - 1, - false, - text("Chris", 0..280), - ParsedMarkdownTableAlignment::None - ) - ]), - row(vec![ - column( - 1, - 1, - false, - text("2", 0..280), - ParsedMarkdownTableAlignment::None - ), - column( - 1, - 1, - false, - text("Dennis", 0..280), - ParsedMarkdownTableAlignment::None - ) - ]), - ], - ))], - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_table_without_headings() { - let parsed = parse( - " - - - - - - - - - - -
1Chris
2Dennis
", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![ParsedMarkdownElement::Table(table( - 0..240, - None, - vec![], - vec![ - row(vec![ - column( - 1, - 1, - false, - text("1", 0..240), - ParsedMarkdownTableAlignment::None - ), - column( - 1, - 1, - false, - text("Chris", 0..240), - ParsedMarkdownTableAlignment::None - ) - ]), - row(vec![ - column( - 1, - 1, - false, - text("2", 0..240), - ParsedMarkdownTableAlignment::None - ), - column( - 1, - 1, - false, - text("Dennis", 0..240), - ParsedMarkdownTableAlignment::None - ) - ]), - ], - ))], - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_table_without_body() { - let parsed = parse( - " - - - - - - -
IdName
", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![ParsedMarkdownElement::Table(table( - 0..150, - None, - vec![row(vec![ - column( - 1, - 1, - true, - text("Id", 0..150), - ParsedMarkdownTableAlignment::Center - ), - column( - 1, - 1, - true, - text("Name", 0..150), - ParsedMarkdownTableAlignment::Center - ) - ])], - vec![], - ))], - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_heading_tags() { - let parsed = parse("

Heading

Heading

Heading

Heading

Heading
Heading
").await; - - assert_eq!( - ParsedMarkdown { - children: vec![ - ParsedMarkdownElement::Heading(ParsedMarkdownHeading { - level: HeadingLevel::H1, - source_range: 0..96, - contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..96, - contents: "Heading".into(), - highlights: Vec::default(), - regions: Vec::default() - })], - }), - ParsedMarkdownElement::Heading(ParsedMarkdownHeading { - level: HeadingLevel::H2, - source_range: 0..96, - contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..96, - contents: "Heading".into(), - highlights: Vec::default(), - regions: Vec::default() - })], - }), - ParsedMarkdownElement::Heading(ParsedMarkdownHeading { - level: HeadingLevel::H3, - source_range: 0..96, - contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..96, - contents: "Heading".into(), - highlights: Vec::default(), - regions: Vec::default() - })], - }), - ParsedMarkdownElement::Heading(ParsedMarkdownHeading { - level: HeadingLevel::H4, - source_range: 0..96, - contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..96, - contents: "Heading".into(), - highlights: Vec::default(), - regions: Vec::default() - })], - }), - ParsedMarkdownElement::Heading(ParsedMarkdownHeading { - level: HeadingLevel::H5, - source_range: 0..96, - contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..96, - contents: "Heading".into(), - highlights: Vec::default(), - regions: Vec::default() - })], - }), - ParsedMarkdownElement::Heading(ParsedMarkdownHeading { - level: HeadingLevel::H6, - source_range: 0..96, - contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..96, - contents: "Heading".into(), - highlights: Vec::default(), - regions: Vec::default() - })], - }), - ], - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_image_tag() { - let parsed = parse("").await; - - assert_eq!( - ParsedMarkdown { - children: vec![ParsedMarkdownElement::Image(Image { - source_range: 0..40, - link: Link::Web { - url: "http://example.com/foo.png".to_string(), - }, - alt_text: None, - height: None, - width: None, - })] - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_image_tag_with_alt_text() { - let parsed = parse("\"Foo\"").await; - - assert_eq!( - ParsedMarkdown { - children: vec![ParsedMarkdownElement::Image(Image { - source_range: 0..50, - link: Link::Web { - url: "http://example.com/foo.png".to_string(), - }, - alt_text: Some("Foo".into()), - height: None, - width: None, - })] - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_image_tag_with_height_and_width() { - let parsed = - parse("").await; - - assert_eq!( - ParsedMarkdown { - children: vec![ParsedMarkdownElement::Image(Image { - source_range: 0..65, - link: Link::Web { - url: "http://example.com/foo.png".to_string(), - }, - alt_text: None, - height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))), - width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))), - })] - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_image_style_tag_with_height_and_width() { - let parsed = parse( - "", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![ParsedMarkdownElement::Image(Image { - source_range: 0..75, - link: Link::Web { - url: "http://example.com/foo.png".to_string(), - }, - alt_text: None, - height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))), - width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))), - })] - }, - parsed - ); - } - - #[gpui::test] - async fn test_header_only_table() { - let markdown = "\ -| Header 1 | Header 2 | -|----------|----------| - -Some other content -"; - - let expected_table = table( - 0..48, - None, - vec![row(vec![ - column( - 1, - 1, - true, - text("Header 1", 1..11), - ParsedMarkdownTableAlignment::None, - ), - column( - 1, - 1, - true, - text("Header 2", 12..22), - ParsedMarkdownTableAlignment::None, - ), - ])], - vec![], - ); - - assert_eq!( - parse(markdown).await.children[0], - ParsedMarkdownElement::Table(expected_table) - ); - } - - #[gpui::test] - async fn test_basic_table() { - let markdown = "\ -| Header 1 | Header 2 | -|----------|----------| -| Cell 1 | Cell 2 | -| Cell 3 | Cell 4 |"; - - let expected_table = table( - 0..95, - None, - vec![row(vec![ - column( - 1, - 1, - true, - text("Header 1", 1..11), - ParsedMarkdownTableAlignment::None, - ), - column( - 1, - 1, - true, - text("Header 2", 12..22), - ParsedMarkdownTableAlignment::None, - ), - ])], - vec![ - row(vec![ - column( - 1, - 1, - false, - text("Cell 1", 49..59), - ParsedMarkdownTableAlignment::None, - ), - column( - 1, - 1, - false, - text("Cell 2", 60..70), - ParsedMarkdownTableAlignment::None, - ), - ]), - row(vec![ - column( - 1, - 1, - false, - text("Cell 3", 73..83), - ParsedMarkdownTableAlignment::None, - ), - column( - 1, - 1, - false, - text("Cell 4", 84..94), - ParsedMarkdownTableAlignment::None, - ), - ]), - ], - ); - - assert_eq!( - parse(markdown).await.children[0], - ParsedMarkdownElement::Table(expected_table) - ); - } - - #[gpui::test] - async fn test_list_basic() { - let parsed = parse( - "\ -* Item 1 -* Item 2 -* Item 3 -", - ) - .await; - - assert_eq!( - parsed.children, - vec![ - list_item(0..8, 1, Unordered, vec![p("Item 1", 2..8)]), - list_item(9..17, 1, Unordered, vec![p("Item 2", 11..17)]), - list_item(18..26, 1, Unordered, vec![p("Item 3", 20..26)]), - ], - ); - } - - #[gpui::test] - async fn test_list_with_tasks() { - let parsed = parse( - "\ -- [ ] TODO -- [x] Checked -", - ) - .await; - - assert_eq!( - parsed.children, - vec![ - list_item(0..10, 1, Task(false, 2..5), vec![p("TODO", 6..10)]), - list_item(11..24, 1, Task(true, 13..16), vec![p("Checked", 17..24)]), - ], - ); - } - - #[gpui::test] - async fn test_list_with_indented_task() { - let parsed = parse( - "\ -- [ ] TODO - - [x] Checked - - Unordered - 1. Number 1 - 1. Number 2 -1. Number A -", - ) - .await; - - assert_eq!( - parsed.children, - vec![ - list_item(0..12, 1, Task(false, 2..5), vec![p("TODO", 6..10)]), - list_item(13..26, 2, Task(true, 15..18), vec![p("Checked", 19..26)]), - list_item(29..40, 2, Unordered, vec![p("Unordered", 31..40)]), - list_item(43..54, 2, Ordered(1), vec![p("Number 1", 46..54)]), - list_item(57..68, 2, Ordered(2), vec![p("Number 2", 60..68)]), - list_item(69..80, 1, Ordered(1), vec![p("Number A", 72..80)]), - ], - ); - } - - #[gpui::test] - async fn test_list_with_linebreak_is_handled_correctly() { - let parsed = parse( - "\ -- [ ] Task 1 - -- [x] Task 2 -", - ) - .await; - - assert_eq!( - parsed.children, - vec![ - list_item(0..13, 1, Task(false, 2..5), vec![p("Task 1", 6..12)]), - list_item(14..26, 1, Task(true, 16..19), vec![p("Task 2", 20..26)]), - ], - ); - } - - #[gpui::test] - async fn test_list_nested() { - let parsed = parse( - "\ -* Item 1 -* Item 2 -* Item 3 - -1. Hello -1. Two - 1. Three -2. Four -3. Five - -* First - 1. Hello - 1. Goodbyte - - Inner - - Inner - 2. Goodbyte - - Next item empty - - -* Last -", - ) - .await; - - assert_eq!( - parsed.children, - vec![ - list_item(0..8, 1, Unordered, vec![p("Item 1", 2..8)]), - list_item(9..17, 1, Unordered, vec![p("Item 2", 11..17)]), - list_item(18..27, 1, Unordered, vec![p("Item 3", 20..26)]), - list_item(28..36, 1, Ordered(1), vec![p("Hello", 31..36)]), - list_item(37..46, 1, Ordered(2), vec![p("Two", 40..43),]), - list_item(47..55, 2, Ordered(1), vec![p("Three", 50..55)]), - list_item(56..63, 1, Ordered(3), vec![p("Four", 59..63)]), - list_item(64..72, 1, Ordered(4), vec![p("Five", 67..71)]), - list_item(73..82, 1, Unordered, vec![p("First", 75..80)]), - list_item(83..96, 2, Ordered(1), vec![p("Hello", 86..91)]), - list_item(97..116, 3, Ordered(1), vec![p("Goodbyte", 100..108)]), - list_item(117..124, 4, Unordered, vec![p("Inner", 119..124)]), - list_item(133..140, 4, Unordered, vec![p("Inner", 135..140)]), - list_item(143..159, 2, Ordered(2), vec![p("Goodbyte", 146..154)]), - list_item(160..180, 3, Unordered, vec![p("Next item empty", 165..180)]), - list_item(186..190, 3, Unordered, vec![]), - list_item(191..197, 1, Unordered, vec![p("Last", 193..197)]), - ] - ); - } - - #[gpui::test] - async fn test_list_with_nested_content() { - let parsed = parse( - "\ -* This is a list item with two paragraphs. - - This is the second paragraph in the list item. -", - ) - .await; - - assert_eq!( - parsed.children, - vec![list_item( - 0..96, - 1, - Unordered, - vec![ - p("This is a list item with two paragraphs.", 4..44), - p("This is the second paragraph in the list item.", 50..97) - ], - ),], - ); - } - - #[gpui::test] - async fn test_list_item_with_inline_html() { - let parsed = parse( - "\ -* This is a list item with an inline HTML tag. -", - ) - .await; - - assert_eq!( - parsed.children, - vec![list_item( - 0..67, - 1, - Unordered, - vec![p("This is a list item with an inline HTML tag.", 4..44),], - ),], - ); - } - - #[gpui::test] - async fn test_nested_list_with_paragraph_inside() { - let parsed = parse( - "\ -1. a - 1. b - 1. c - - text - - 1. d -", - ) - .await; - - assert_eq!( - parsed.children, - vec![ - list_item(0..7, 1, Ordered(1), vec![p("a", 3..4)],), - list_item(8..20, 2, Ordered(1), vec![p("b", 12..13),],), - list_item(21..27, 3, Ordered(1), vec![p("c", 25..26),],), - p("text", 32..37), - list_item(41..46, 2, Ordered(1), vec![p("d", 45..46),],), - ], - ); - } - - #[gpui::test] - async fn test_list_with_leading_text() { - let parsed = parse( - "\ -* `code` -* **bold** -* [link](https://example.com) -", - ) - .await; - - assert_eq!( - parsed.children, - vec![ - list_item(0..8, 1, Unordered, vec![p("code", 2..8)]), - list_item(9..19, 1, Unordered, vec![p("bold", 11..19)]), - list_item(20..49, 1, Unordered, vec![p("link", 22..49)],), - ], - ); - } - - #[gpui::test] - async fn test_simple_block_quote() { - let parsed = parse("> Simple block quote with **styled text**").await; - - assert_eq!( - parsed.children, - vec![block_quote( - vec![p("Simple block quote with styled text", 2..41)], - 0..41 - )] - ); - } - - #[gpui::test] - async fn test_simple_block_quote_with_multiple_lines() { - let parsed = parse( - "\ -> # Heading -> More -> text -> -> More text -", - ) - .await; - - assert_eq!( - parsed.children, - vec![block_quote( - vec![ - h1(text("Heading", 4..11), 2..12), - p("More text", 14..26), - p("More text", 30..40) - ], - 0..40 - )] - ); - } - - #[gpui::test] - async fn test_nested_block_quote() { - let parsed = parse( - "\ -> A -> -> > # B -> -> C - -More text -", - ) - .await; - - assert_eq!( - parsed.children, - vec![ - block_quote( - vec![ - p("A", 2..4), - block_quote(vec![h1(text("B", 12..13), 10..14)], 8..14), - p("C", 18..20) - ], - 0..20 - ), - p("More text", 21..31) - ] - ); - } - - #[gpui::test] - async fn test_code_block() { - let parsed = parse( - "\ -``` -fn main() { - return 0; -} -``` -", - ) - .await; - - assert_eq!( - parsed.children, - vec![code_block( - None, - "fn main() {\n return 0;\n}", - 0..35, - None - )] - ); - } - - #[gpui::test] - async fn test_code_block_with_language(executor: BackgroundExecutor) { - let language_registry = Arc::new(LanguageRegistry::test(executor.clone())); - language_registry.add(language::rust_lang()); - - let parsed = parse_markdown( - "\ -```rust -fn main() { - return 0; -} -``` -", - None, - Some(language_registry), - ) - .await; - - assert_eq!( - parsed.children, - vec![code_block( - Some("rust".to_string()), - "fn main() {\n return 0;\n}", - 0..39, - Some(vec![]) - )] - ); - } - - fn h1(contents: MarkdownParagraph, source_range: Range) -> ParsedMarkdownElement { - ParsedMarkdownElement::Heading(ParsedMarkdownHeading { - source_range, - level: HeadingLevel::H1, - contents, - }) - } - - fn h2(contents: MarkdownParagraph, source_range: Range) -> ParsedMarkdownElement { - ParsedMarkdownElement::Heading(ParsedMarkdownHeading { - source_range, - level: HeadingLevel::H2, - contents, - }) - } - - fn h3(contents: MarkdownParagraph, source_range: Range) -> ParsedMarkdownElement { - ParsedMarkdownElement::Heading(ParsedMarkdownHeading { - source_range, - level: HeadingLevel::H3, - contents, - }) - } - - fn p(contents: &str, source_range: Range) -> ParsedMarkdownElement { - ParsedMarkdownElement::Paragraph(text(contents, source_range)) - } - - fn text(contents: &str, source_range: Range) -> MarkdownParagraph { - vec![MarkdownParagraphChunk::Text(ParsedMarkdownText { - highlights: Vec::new(), - regions: Vec::new(), - source_range, - contents: contents.to_string().into(), - })] - } - - fn block_quote( - children: Vec, - source_range: Range, - ) -> ParsedMarkdownElement { - ParsedMarkdownElement::BlockQuote(ParsedMarkdownBlockQuote { - source_range, - children, - }) - } - - fn code_block( - language: Option, - code: &str, - source_range: Range, - highlights: Option, HighlightId)>>, - ) -> ParsedMarkdownElement { - ParsedMarkdownElement::CodeBlock(ParsedMarkdownCodeBlock { - source_range, - language, - contents: code.to_string().into(), - highlights, - }) - } - - fn list_item( - source_range: Range, - depth: u16, - item_type: ParsedMarkdownListItemType, - content: Vec, - ) -> ParsedMarkdownElement { - ParsedMarkdownElement::ListItem(ParsedMarkdownListItem { - source_range, - item_type, - depth, - content, - nested: false, - }) - } - - fn nested_list_item( - source_range: Range, - depth: u16, - item_type: ParsedMarkdownListItemType, - content: Vec, - ) -> ParsedMarkdownElement { - ParsedMarkdownElement::ListItem(ParsedMarkdownListItem { - source_range, - item_type, - depth, - content, - nested: true, - }) - } - - fn table( - source_range: Range, - caption: Option, - header: Vec, - body: Vec, - ) -> ParsedMarkdownTable { - ParsedMarkdownTable { - source_range, - header, - body, - caption, - } - } - - fn row(columns: Vec) -> ParsedMarkdownTableRow { - ParsedMarkdownTableRow { columns } - } - - fn column( - col_span: usize, - row_span: usize, - is_header: bool, - children: MarkdownParagraph, - alignment: ParsedMarkdownTableAlignment, - ) -> ParsedMarkdownTableColumn { - ParsedMarkdownTableColumn { - col_span, - row_span, - is_header, - children, - alignment, - } - } - - impl PartialEq for ParsedMarkdownTable { - fn eq(&self, other: &Self) -> bool { - self.source_range == other.source_range - && self.header == other.header - && self.body == other.body - } - } - - impl PartialEq for ParsedMarkdownText { - fn eq(&self, other: &Self) -> bool { - self.source_range == other.source_range && self.contents == other.contents - } - } -} diff --git a/crates/markdown_preview/src/markdown_preview.rs b/crates/markdown_preview/src/markdown_preview.rs deleted file mode 100644 index 61c99764ad..0000000000 --- a/crates/markdown_preview/src/markdown_preview.rs +++ /dev/null @@ -1,44 +0,0 @@ -use gpui::{App, actions}; -use workspace::Workspace; - -pub mod markdown_elements; -mod markdown_minifier; -pub mod markdown_parser; -pub mod markdown_preview_view; -pub mod markdown_renderer; - -actions!( - markdown, - [ - /// Scrolls up by one page in the markdown preview. - #[action(deprecated_aliases = ["markdown::MovePageUp"])] - ScrollPageUp, - /// Scrolls down by one page in the markdown preview. - #[action(deprecated_aliases = ["markdown::MovePageDown"])] - ScrollPageDown, - /// Scrolls up by approximately one visual line. - ScrollUp, - /// Scrolls down by approximately one visual line. - ScrollDown, - /// Scrolls up by one markdown element in the markdown preview - ScrollUpByItem, - /// Scrolls down by one markdown element in the markdown preview - ScrollDownByItem, - /// Opens a markdown preview for the current file. - OpenPreview, - /// Opens a markdown preview in a split pane. - OpenPreviewToTheSide, - /// Opens a following markdown preview that syncs with the editor. - OpenFollowingPreview - ] -); - -pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, window, cx| { - let Some(window) = window else { - return; - }; - markdown_preview_view::MarkdownPreviewView::register(workspace, window, cx); - }) - .detach(); -} diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs deleted file mode 100644 index 20613b112e..0000000000 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ /dev/null @@ -1,677 +0,0 @@ -use std::cmp::min; -use std::sync::Arc; -use std::time::Duration; -use std::{ops::Range, path::PathBuf}; - -use anyhow::Result; -use editor::scroll::Autoscroll; -use editor::{Editor, EditorEvent, MultiBufferOffset, SelectionEffects}; -use gpui::{ - App, ClickEvent, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, - IntoElement, IsZero, ListState, ParentElement, Render, RetainAllImageCache, Styled, - Subscription, Task, WeakEntity, Window, list, -}; -use language::LanguageRegistry; -use settings::Settings; -use theme::ThemeSettings; -use ui::{WithScrollbar, prelude::*}; -use workspace::item::{Item, ItemHandle}; -use workspace::{Pane, Workspace}; - -use crate::markdown_elements::ParsedMarkdownElement; -use crate::markdown_renderer::CheckboxClickedEvent; -use crate::{ - OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide, ScrollPageDown, ScrollPageUp, - markdown_elements::ParsedMarkdown, - markdown_parser::parse_markdown, - markdown_renderer::{RenderContext, render_markdown_block}, -}; -use crate::{ScrollDown, ScrollDownByItem, ScrollUp, ScrollUpByItem}; - -const REPARSE_DEBOUNCE: Duration = Duration::from_millis(200); - -pub struct MarkdownPreviewView { - workspace: WeakEntity, - image_cache: Entity, - active_editor: Option, - focus_handle: FocusHandle, - contents: Option, - selected_block: usize, - list_state: ListState, - language_registry: Arc, - parsing_markdown_task: Option>>, - mode: MarkdownPreviewMode, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum MarkdownPreviewMode { - /// The preview will always show the contents of the provided editor. - Default, - /// The preview will "follow" the currently active editor. - Follow, -} - -struct EditorState { - editor: Entity, - _subscription: Subscription, -} - -impl MarkdownPreviewView { - pub fn register(workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context) { - workspace.register_action(move |workspace, _: &OpenPreview, window, cx| { - if let Some(editor) = Self::resolve_active_item_as_markdown_editor(workspace, cx) { - let view = Self::create_markdown_view(workspace, editor.clone(), window, cx); - workspace.active_pane().update(cx, |pane, cx| { - if let Some(existing_view_idx) = - Self::find_existing_independent_preview_item_idx(pane, &editor, cx) - { - pane.activate_item(existing_view_idx, true, true, window, cx); - } else { - pane.add_item(Box::new(view.clone()), true, true, None, window, cx) - } - }); - cx.notify(); - } - }); - - workspace.register_action(move |workspace, _: &OpenPreviewToTheSide, window, cx| { - if let Some(editor) = Self::resolve_active_item_as_markdown_editor(workspace, cx) { - let view = Self::create_markdown_view(workspace, editor.clone(), window, cx); - let pane = workspace - .find_pane_in_direction(workspace::SplitDirection::Right, cx) - .unwrap_or_else(|| { - workspace.split_pane( - workspace.active_pane().clone(), - workspace::SplitDirection::Right, - window, - cx, - ) - }); - pane.update(cx, |pane, cx| { - if let Some(existing_view_idx) = - Self::find_existing_independent_preview_item_idx(pane, &editor, cx) - { - pane.activate_item(existing_view_idx, true, true, window, cx); - } else { - pane.add_item(Box::new(view.clone()), false, false, None, window, cx) - } - }); - editor.focus_handle(cx).focus(window); - cx.notify(); - } - }); - - workspace.register_action(move |workspace, _: &OpenFollowingPreview, window, cx| { - if let Some(editor) = Self::resolve_active_item_as_markdown_editor(workspace, cx) { - // Check if there's already a following preview - let existing_follow_view_idx = { - let active_pane = workspace.active_pane().read(cx); - active_pane - .items_of_type::() - .find(|view| view.read(cx).mode == MarkdownPreviewMode::Follow) - .and_then(|view| active_pane.index_for_item(&view)) - }; - - if let Some(existing_follow_view_idx) = existing_follow_view_idx { - workspace.active_pane().update(cx, |pane, cx| { - pane.activate_item(existing_follow_view_idx, true, true, window, cx); - }); - } else { - let view = Self::create_following_markdown_view(workspace, editor, window, cx); - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item(Box::new(view.clone()), true, true, None, window, cx) - }); - } - cx.notify(); - } - }); - } - - fn find_existing_independent_preview_item_idx( - pane: &Pane, - editor: &Entity, - cx: &App, - ) -> Option { - pane.items_of_type::() - .find(|view| { - let view_read = view.read(cx); - // Only look for independent (Default mode) previews, not Follow previews - view_read.mode == MarkdownPreviewMode::Default - && view_read - .active_editor - .as_ref() - .is_some_and(|active_editor| active_editor.editor == *editor) - }) - .and_then(|view| pane.index_for_item(&view)) - } - - pub fn resolve_active_item_as_markdown_editor( - workspace: &Workspace, - cx: &mut Context, - ) -> Option> { - if let Some(editor) = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - && Self::is_markdown_file(&editor, cx) - { - return Some(editor); - } - None - } - - fn create_markdown_view( - workspace: &mut Workspace, - editor: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let language_registry = workspace.project().read(cx).languages().clone(); - let workspace_handle = workspace.weak_handle(); - MarkdownPreviewView::new( - MarkdownPreviewMode::Default, - editor, - workspace_handle, - language_registry, - window, - cx, - ) - } - - fn create_following_markdown_view( - workspace: &mut Workspace, - editor: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let language_registry = workspace.project().read(cx).languages().clone(); - let workspace_handle = workspace.weak_handle(); - MarkdownPreviewView::new( - MarkdownPreviewMode::Follow, - editor, - workspace_handle, - language_registry, - window, - cx, - ) - } - - pub fn new( - mode: MarkdownPreviewMode, - active_editor: Entity, - workspace: WeakEntity, - language_registry: Arc, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - cx.new(|cx| { - let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.)); - - let mut this = Self { - selected_block: 0, - active_editor: None, - focus_handle: cx.focus_handle(), - workspace: workspace.clone(), - contents: None, - list_state, - language_registry, - parsing_markdown_task: None, - image_cache: RetainAllImageCache::new(cx), - mode, - }; - - this.set_editor(active_editor, window, cx); - - if mode == MarkdownPreviewMode::Follow { - if let Some(workspace) = &workspace.upgrade() { - cx.observe_in(workspace, window, |this, workspace, window, cx| { - let item = workspace.read(cx).active_item(cx); - this.workspace_updated(item, window, cx); - }) - .detach(); - } else { - log::error!("Failed to listen to workspace updates"); - } - } - - this - }) - } - - fn workspace_updated( - &mut self, - active_item: Option>, - 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); - } - } - - 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(); - } - 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; - } - - let subscription = cx.subscribe_in( - &editor, - window, - |this, editor, event: &EditorEvent, window, cx| { - match event { - EditorEvent::Edited { .. } - | EditorEvent::DirtyChanged - | EditorEvent::ExcerptsEdited { .. } => { - this.parse_markdown_from_active_editor(true, window, cx); - } - EditorEvent::SelectionsChanged { .. } => { - let selection_range = editor.update(cx, |editor, cx| { - editor - .selections - .last::(&editor.display_snapshot(cx)) - .range() - }); - this.selected_block = this.get_block_index_under_cursor(selection_range); - this.list_state.scroll_to_reveal_item(this.selected_block); - cx.notify(); - } - _ => {} - }; - }, - ); - - self.active_editor = Some(EditorState { - editor, - _subscription: subscription, - }); - - self.parse_markdown_from_active_editor(false, window, cx); - } - - fn parse_markdown_from_active_editor( - &mut self, - wait_for_debounce: bool, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(state) = &self.active_editor { - self.parsing_markdown_task = Some(self.parse_markdown_in_background( - wait_for_debounce, - state.editor.clone(), - window, - cx, - )); - } - } - - fn parse_markdown_in_background( - &mut self, - wait_for_debounce: bool, - editor: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let language_registry = self.language_registry.clone(); - - cx.spawn_in(window, async move |view, cx| { - if wait_for_debounce { - // Wait for the user to stop typing - cx.background_executor().timer(REPARSE_DEBOUNCE).await; - } - - let (contents, file_location) = view.update(cx, |_, cx| { - let editor = editor.read(cx); - let contents = editor.buffer().read(cx).snapshot(cx).text(); - let file_location = MarkdownPreviewView::get_folder_for_active_editor(editor, cx); - (contents, file_location) - })?; - - let parsing_task = cx.background_spawn(async move { - parse_markdown(&contents, file_location, Some(language_registry)).await - }); - let contents = parsing_task.await; - view.update(cx, move |view, cx| { - let markdown_blocks_count = contents.children.len(); - view.contents = Some(contents); - let scroll_top = view.list_state.logical_scroll_top(); - view.list_state.reset(markdown_blocks_count); - view.list_state.scroll_to(scroll_top); - cx.notify(); - }) - }) - } - - fn move_cursor_to_block( - &self, - window: &mut Window, - cx: &mut Context, - selection: Range, - ) { - if let Some(state) = &self.active_editor { - state.editor.update(cx, |editor, cx| { - editor.change_selections( - SelectionEffects::scroll(Autoscroll::center()), - window, - cx, - |selections| selections.select_ranges(vec![selection]), - ); - window.focus(&editor.focus_handle(cx)); - }); - } - } - - /// The absolute path of the file that is currently being previewed. - fn get_folder_for_active_editor(editor: &Editor, cx: &App) -> Option { - if let Some(file) = editor.file_at(MultiBufferOffset(0), cx) { - if let Some(file) = file.as_local() { - file.abs_path(cx).parent().map(|p| p.to_path_buf()) - } else { - None - } - } else { - None - } - } - - fn get_block_index_under_cursor(&self, selection_range: Range) -> usize { - let mut block_index = None; - let cursor = selection_range.start.0; - - let mut last_end = 0; - if let Some(content) = &self.contents { - for (i, block) in content.children.iter().enumerate() { - let Some(Range { start, end }) = block.source_range() else { - continue; - }; - - // Check if the cursor is between the last block and the current block - if last_end <= cursor && cursor < start { - block_index = Some(i.saturating_sub(1)); - break; - } - - if start <= cursor && end >= cursor { - block_index = Some(i); - break; - } - last_end = end; - } - - if block_index.is_none() && last_end < cursor { - block_index = Some(content.children.len().saturating_sub(1)); - } - } - - block_index.unwrap_or_default() - } - - fn should_apply_padding_between( - current_block: &ParsedMarkdownElement, - next_block: Option<&ParsedMarkdownElement>, - ) -> bool { - !(current_block.is_list_item() && next_block.map(|b| b.is_list_item()).unwrap_or(false)) - } - - fn scroll_page_up(&mut self, _: &ScrollPageUp, _window: &mut Window, cx: &mut Context) { - let viewport_height = self.list_state.viewport_bounds().size.height; - if viewport_height.is_zero() { - return; - } - - self.list_state.scroll_by(-viewport_height); - cx.notify(); - } - - fn scroll_page_down( - &mut self, - _: &ScrollPageDown, - _window: &mut Window, - cx: &mut Context, - ) { - let viewport_height = self.list_state.viewport_bounds().size.height; - if viewport_height.is_zero() { - return; - } - - self.list_state.scroll_by(viewport_height); - cx.notify(); - } - - fn scroll_up(&mut self, _: &ScrollUp, window: &mut Window, cx: &mut Context) { - let scroll_top = self.list_state.logical_scroll_top(); - if let Some(bounds) = self.list_state.bounds_for_item(scroll_top.item_ix) { - let item_height = bounds.size.height; - // Scroll no more than the rough equivalent of a large headline - let max_height = window.rem_size() * 2; - let scroll_height = min(item_height, max_height); - self.list_state.scroll_by(-scroll_height); - } - cx.notify(); - } - - fn scroll_down(&mut self, _: &ScrollDown, window: &mut Window, cx: &mut Context) { - let scroll_top = self.list_state.logical_scroll_top(); - if let Some(bounds) = self.list_state.bounds_for_item(scroll_top.item_ix) { - let item_height = bounds.size.height; - // Scroll no more than the rough equivalent of a large headline - let max_height = window.rem_size() * 2; - let scroll_height = min(item_height, max_height); - self.list_state.scroll_by(scroll_height); - } - cx.notify(); - } - - fn scroll_up_by_item( - &mut self, - _: &ScrollUpByItem, - _window: &mut Window, - cx: &mut Context, - ) { - let scroll_top = self.list_state.logical_scroll_top(); - if let Some(bounds) = self.list_state.bounds_for_item(scroll_top.item_ix) { - self.list_state.scroll_by(-bounds.size.height); - } - cx.notify(); - } - - fn scroll_down_by_item( - &mut self, - _: &ScrollDownByItem, - _window: &mut Window, - cx: &mut Context, - ) { - let scroll_top = self.list_state.logical_scroll_top(); - if let Some(bounds) = self.list_state.bounds_for_item(scroll_top.item_ix) { - self.list_state.scroll_by(bounds.size.height); - } - cx.notify(); - } -} - -impl Focusable for MarkdownPreviewView { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl EventEmitter<()> for MarkdownPreviewView {} - -impl Item for MarkdownPreviewView { - type Event = (); - - fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { - Some(Icon::new(IconName::FileDoc)) - } - - fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString { - self.active_editor - .as_ref() - .and_then(|editor_state| { - let buffer = editor_state.editor.read(cx).buffer().read(cx); - let buffer = buffer.as_singleton()?; - let file = buffer.read(cx).file()?; - let local_file = file.as_local()?; - local_file - .abs_path(cx) - .file_name() - .map(|name| format!("Preview {}", name.to_string_lossy()).into()) - }) - .unwrap_or_else(|| SharedString::from("Markdown Preview")) - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - Some("Markdown Preview Opened") - } - - fn to_item_events(_event: &Self::Event, _f: impl FnMut(workspace::item::ItemEvent)) {} -} - -impl Render for MarkdownPreviewView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let buffer_size = ThemeSettings::get_global(cx).buffer_font_size(cx); - let buffer_line_height = ThemeSettings::get_global(cx).buffer_line_height; - - v_flex() - .image_cache(self.image_cache.clone()) - .id("MarkdownPreview") - .key_context("MarkdownPreview") - .track_focus(&self.focus_handle(cx)) - .on_action(cx.listener(MarkdownPreviewView::scroll_page_up)) - .on_action(cx.listener(MarkdownPreviewView::scroll_page_down)) - .on_action(cx.listener(MarkdownPreviewView::scroll_up)) - .on_action(cx.listener(MarkdownPreviewView::scroll_down)) - .on_action(cx.listener(MarkdownPreviewView::scroll_up_by_item)) - .on_action(cx.listener(MarkdownPreviewView::scroll_down_by_item)) - .size_full() - .bg(cx.theme().colors().editor_background) - .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( - [( - MultiBufferOffset( - e.source_range().start, - ) - ..MultiBufferOffset( - e.source_range().end, - ), - 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, - MultiBufferOffset(source_range.start) - ..MultiBufferOffset(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(), - ) - })) - .vertical_scrollbar_for(&self.list_state, window, cx) - } -} diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs deleted file mode 100644 index 336f1cacfd..0000000000 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ /dev/null @@ -1,1083 +0,0 @@ -use crate::markdown_elements::{ - HeadingLevel, Image, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown, - ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, ParsedMarkdownElement, - ParsedMarkdownHeading, ParsedMarkdownListItem, ParsedMarkdownListItemType, ParsedMarkdownTable, - ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, -}; -use fs::normalize_path; -use gpui::{ - AbsoluteLength, AnyElement, App, AppContext as _, ClipboardItem, Context, Div, Element, - ElementId, Entity, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement, Keystroke, - Modifiers, ParentElement, Render, Resource, SharedString, Styled, StyledText, TextStyle, - WeakEntity, Window, div, img, rems, -}; -use settings::Settings; -use std::{ - ops::{Mul, Range}, - sync::Arc, - vec, -}; -use theme::{ActiveTheme, SyntaxTheme, ThemeSettings}; -use ui::{ - ButtonCommon, Clickable, Color, FluentBuilder, IconButton, IconName, IconSize, - InteractiveElement, Label, LabelCommon, LabelSize, LinkPreview, Pixels, Rems, - StatefulInteractiveElement, StyledExt, StyledImage, ToggleState, Tooltip, VisibleOnHover, - h_flex, tooltip_container, v_flex, -}; -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>; - -#[derive(Clone)] -pub struct RenderContext { - workspace: Option>, - next_id: usize, - buffer_font_family: SharedString, - buffer_text_style: TextStyle, - text_style: TextStyle, - border_color: Hsla, - title_bar_background_color: Hsla, - panel_background_color: Hsla, - text_color: Hsla, - link_color: Hsla, - window_rem_size: Pixels, - text_muted_color: Hsla, - code_block_background_color: Hsla, - code_span_background_color: Hsla, - syntax_theme: Arc, - indent: usize, - checkbox_clicked_callback: Option, - is_last_child: bool, -} - -impl RenderContext { - pub fn new( - workspace: Option>, - window: &mut Window, - cx: &mut App, - ) -> RenderContext { - let theme = cx.theme().clone(); - - let settings = ThemeSettings::get_global(cx); - let buffer_font_family = settings.buffer_font.family.clone(); - let buffer_font_features = settings.buffer_font.features.clone(); - let mut buffer_text_style = window.text_style(); - buffer_text_style.font_family = buffer_font_family.clone(); - buffer_text_style.font_features = buffer_font_features; - buffer_text_style.font_size = AbsoluteLength::from(settings.buffer_font_size(cx)); - - RenderContext { - workspace, - next_id: 0, - indent: 0, - buffer_font_family, - buffer_text_style, - text_style: window.text_style(), - syntax_theme: theme.syntax().clone(), - border_color: theme.colors().border, - title_bar_background_color: theme.colors().title_bar_background, - panel_background_color: theme.colors().panel_background, - text_color: theme.colors().text, - link_color: theme.colors().text_accent, - window_rem_size: window.rem_size(), - text_muted_color: theme.colors().text_muted, - code_block_background_color: theme.colors().surface_background, - code_span_background_color: theme.colors().editor_document_highlight_read_background, - checkbox_clicked_callback: None, - is_last_child: false, - } - } - - pub fn with_checkbox_clicked_callback( - mut self, - callback: impl Fn(&CheckboxClickedEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.checkbox_clicked_callback = Some(Arc::new(Box::new(callback))); - self - } - - fn next_id(&mut self, span: &Range) -> ElementId { - let id = format!("markdown-{}-{}-{}", self.next_id, span.start, span.end); - self.next_id += 1; - ElementId::from(SharedString::from(id)) - } - - /// HACK: used to have rems relative to buffer font size, so that things scale appropriately as - /// 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 - .font_size - .to_rems(self.window_rem_size) - .mul(rems) - } - - /// This ensures that children inside of block quotes - /// have padding between them. - /// - /// For example, for this markdown: - /// - /// ```markdown - /// > This is a block quote. - /// > - /// > And this is the next paragraph. - /// ``` - /// - /// We give padding between "This is a block quote." - /// and "And this is the next paragraph." - fn with_common_p(&self, element: Div) -> Div { - if self.indent > 0 && !self.is_last_child { - element.pb(self.scaled_rems(0.75)) - } else { - element - } - } - - /// The is used to indicate that the current element is the last child or not of its parent. - /// - /// Then we can avoid adding padding to the bottom of the last child. - fn with_last_child(&mut self, is_last: bool, render: R) -> AnyElement - where - R: FnOnce(&mut Self) -> AnyElement, - { - self.is_last_child = is_last; - let element = render(self); - self.is_last_child = false; - element - } -} - -pub fn render_parsed_markdown( - parsed: &ParsedMarkdown, - workspace: Option>, - window: &mut Window, - cx: &mut App, -) -> Div { - let mut cx = RenderContext::new(workspace, window, cx); - - v_flex().gap_3().children( - parsed - .children - .iter() - .map(|block| render_markdown_block(block, &mut cx)), - ) -} -pub fn render_markdown_block(block: &ParsedMarkdownElement, cx: &mut RenderContext) -> AnyElement { - use ParsedMarkdownElement::*; - match block { - Paragraph(text) => render_markdown_paragraph(text, cx), - Heading(heading) => render_markdown_heading(heading, cx), - ListItem(list_item) => render_markdown_list_item(list_item, cx), - Table(table) => render_markdown_table(table, cx), - BlockQuote(block_quote) => render_markdown_block_quote(block_quote, cx), - CodeBlock(code_block) => render_markdown_code_block(code_block, cx), - HorizontalRule(_) => render_markdown_rule(cx), - Image(image) => render_markdown_image(image, cx), - } -} - -fn render_markdown_heading(parsed: &ParsedMarkdownHeading, cx: &mut RenderContext) -> AnyElement { - let size = match parsed.level { - HeadingLevel::H1 => 2., - HeadingLevel::H2 => 1.5, - HeadingLevel::H3 => 1.25, - HeadingLevel::H4 => 1., - HeadingLevel::H5 => 0.875, - HeadingLevel::H6 => 0.85, - }; - - let text_size = cx.scaled_rems(size); - - // was `DefiniteLength::from(text_size.mul(1.25))` - // let line_height = DefiniteLength::from(text_size.mul(1.25)); - let line_height = text_size * 1.25; - - // was `rems(0.15)` - // let padding_top = cx.scaled_rems(0.15); - let padding_top = rems(0.15); - - // was `.pb_1()` = `rems(0.25)` - // let padding_bottom = cx.scaled_rems(0.25); - let padding_bottom = rems(0.25); - - let color = match parsed.level { - HeadingLevel::H6 => cx.text_muted_color, - _ => cx.text_color, - }; - div() - .line_height(line_height) - .text_size(text_size) - .text_color(color) - .pt(padding_top) - .pb(padding_bottom) - .children(render_markdown_text(&parsed.contents, cx)) - .whitespace_normal() - .into_any() -} - -fn render_markdown_list_item( - parsed: &ParsedMarkdownListItem, - cx: &mut RenderContext, -) -> AnyElement { - use ParsedMarkdownListItemType::*; - let depth = parsed.depth.saturating_sub(1) as usize; - - let bullet = match &parsed.item_type { - Ordered(order) => list_item_prefix(*order as usize, true, depth).into_any_element(), - Unordered => list_item_prefix(1, false, depth).into_any_element(), - Task(checked, range) => div() - .id(cx.next_id(range)) - .mt(cx.scaled_rems(3.0 / 16.0)) - .child( - MarkdownCheckbox::new( - "checkbox", - if *checked { - ToggleState::Selected - } else { - ToggleState::Unselected - }, - cx.clone(), - ) - .when_some( - cx.checkbox_clicked_callback.clone(), - |this, callback| { - this.on_click({ - let range = range.clone(); - move |selection, window, cx| { - let checked = match selection { - ToggleState::Selected => true, - ToggleState::Unselected => false, - _ => return, - }; - - if window.modifiers().secondary() { - callback( - &CheckboxClickedEvent { - checked, - source_range: range.clone(), - }, - window, - cx, - ); - } - } - }) - }, - ), - ) - .hover(|s| s.cursor_pointer()) - .tooltip(|_, cx| { - InteractiveMarkdownElementTooltip::new(None, "toggle checkbox", cx).into() - }) - .into_any_element(), - }; - let bullet = div().mr(cx.scaled_rems(0.5)).child(bullet); - - let contents: Vec = parsed - .content - .iter() - .map(|c| render_markdown_block(c, cx)) - .collect(); - - let item = h_flex() - .when(!parsed.nested, |this| this.pl(cx.scaled_rems(depth as f32))) - .when(parsed.nested && depth > 0, |this| this.ml_neg_1p5()) - .items_start() - .children(vec![ - bullet, - v_flex() - .children(contents) - .when(!parsed.nested, |this| this.gap(cx.scaled_rems(1.0))) - .pr(cx.scaled_rems(1.0)) - .w_full(), - ]); - - cx.with_common_p(item).into_any() -} - -/// # MarkdownCheckbox /// -/// HACK: Copied from `ui/src/components/toggle.rs` to deal with scaling issues in markdown preview -/// changes should be integrated into `Checkbox` in `toggle.rs` while making sure checkboxes elsewhere in the -/// app are not visually affected -#[derive(gpui::IntoElement)] -struct MarkdownCheckbox { - id: ElementId, - toggle_state: ToggleState, - disabled: bool, - placeholder: bool, - on_click: Option>, - filled: bool, - style: ui::ToggleStyle, - tooltip: Option gpui::AnyView>>, - label: Option, - render_cx: RenderContext, -} - -impl MarkdownCheckbox { - /// Creates a new [`Checkbox`]. - fn new(id: impl Into, checked: ToggleState, render_cx: RenderContext) -> Self { - Self { - id: id.into(), - toggle_state: checked, - disabled: false, - on_click: None, - filled: false, - style: ui::ToggleStyle::default(), - tooltip: None, - label: None, - placeholder: false, - render_cx, - } - } - - /// Binds a handler to the [`Checkbox`] that will be called when clicked. - fn on_click(mut self, handler: impl Fn(&ToggleState, &mut Window, &mut App) + 'static) -> Self { - self.on_click = Some(Box::new(handler)); - self - } - - fn bg_color(&self, cx: &App) -> Hsla { - let style = self.style.clone(); - match (style, self.filled) { - (ui::ToggleStyle::Ghost, false) => cx.theme().colors().ghost_element_background, - (ui::ToggleStyle::Ghost, true) => cx.theme().colors().element_background, - (ui::ToggleStyle::ElevationBased(_), false) => gpui::transparent_black(), - (ui::ToggleStyle::ElevationBased(elevation), true) => elevation.darker_bg(cx), - (ui::ToggleStyle::Custom(_), false) => gpui::transparent_black(), - (ui::ToggleStyle::Custom(color), true) => color.opacity(0.2), - } - } - - fn border_color(&self, cx: &App) -> Hsla { - if self.disabled { - return cx.theme().colors().border_variant; - } - - match self.style.clone() { - ui::ToggleStyle::Ghost => cx.theme().colors().border, - ui::ToggleStyle::ElevationBased(_) => cx.theme().colors().border, - ui::ToggleStyle::Custom(color) => color.opacity(0.3), - } - } -} - -impl gpui::RenderOnce for MarkdownCheckbox { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let group_id = format!("checkbox_group_{:?}", self.id); - let color = if self.disabled { - Color::Disabled - } else { - Color::Selected - }; - let icon_size_small = IconSize::Custom(self.render_cx.scaled_rems(14. / 16.)); // was IconSize::Small - let icon = match self.toggle_state { - ToggleState::Selected => { - if self.placeholder { - None - } else { - Some( - ui::Icon::new(IconName::Check) - .size(icon_size_small) - .color(color), - ) - } - } - ToggleState::Indeterminate => Some( - ui::Icon::new(IconName::Dash) - .size(icon_size_small) - .color(color), - ), - ToggleState::Unselected => None, - }; - - let bg_color = self.bg_color(cx); - let border_color = self.border_color(cx); - let hover_border_color = border_color.alpha(0.7); - - let size = self.render_cx.scaled_rems(1.25); // was Self::container_size(); (20px) - - let checkbox = h_flex() - .id(self.id.clone()) - .justify_center() - .items_center() - .size(size) - .group(group_id.clone()) - .child( - div() - .flex() - .flex_none() - .justify_center() - .items_center() - .m(self.render_cx.scaled_rems(0.25)) // was .m_1 - .size(self.render_cx.scaled_rems(1.0)) // was .size_4 - .rounded(self.render_cx.scaled_rems(0.125)) // was .rounded_xs - .border_1() - .bg(bg_color) - .border_color(border_color) - .when(self.disabled, |this| this.cursor_not_allowed()) - .when(self.disabled, |this| { - this.bg(cx.theme().colors().element_disabled.opacity(0.6)) - }) - .when(!self.disabled, |this| { - this.group_hover(group_id.clone(), |el| el.border_color(hover_border_color)) - }) - .when(self.placeholder, |this| { - this.child( - div() - .flex_none() - .rounded_full() - .bg(color.color(cx).alpha(0.5)) - .size(self.render_cx.scaled_rems(0.25)), // was .size_1 - ) - }) - .children(icon), - ); - - h_flex() - .id(self.id) - .gap(ui::DynamicSpacing::Base06.rems(cx)) - .child(checkbox) - .when_some( - self.on_click.filter(|_| !self.disabled), - |this, on_click| { - this.on_click(move |_, window, cx| { - on_click(&self.toggle_state.inverse(), window, cx) - }) - }, - ) - // TODO: Allow label size to be different from default. - // TODO: Allow label color to be different from muted. - .when_some(self.label, |this, label| { - this.child(Label::new(label).color(Color::Muted)) - }) - .when_some(self.tooltip, |this, tooltip| { - this.tooltip(move |window, cx| tooltip(window, cx)) - }) - } -} - -fn calculate_table_columns_count(rows: &Vec) -> usize { - let mut actual_column_count = 0; - for row in rows { - actual_column_count = actual_column_count.max( - row.columns - .iter() - .map(|column| column.col_span) - .sum::(), - ); - } - actual_column_count -} - -fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -> AnyElement { - let actual_header_column_count = calculate_table_columns_count(&parsed.header); - let actual_body_column_count = calculate_table_columns_count(&parsed.body); - let max_column_count = std::cmp::max(actual_header_column_count, actual_body_column_count); - - let total_rows = parsed.header.len() + parsed.body.len(); - - // Track which grid cells are occupied by spanning cells - let mut grid_occupied = vec![vec![false; max_column_count]; total_rows]; - - let mut cells = Vec::with_capacity(total_rows * max_column_count); - - for (row_idx, row) in parsed.header.iter().chain(parsed.body.iter()).enumerate() { - let mut col_idx = 0; - - for cell in row.columns.iter() { - // Skip columns occupied by row-spanning cells from previous rows - while col_idx < max_column_count && grid_occupied[row_idx][col_idx] { - col_idx += 1; - } - - if col_idx >= max_column_count { - break; - } - - let container = match cell.alignment { - ParsedMarkdownTableAlignment::Left | ParsedMarkdownTableAlignment::None => div(), - ParsedMarkdownTableAlignment::Center => v_flex().items_center(), - ParsedMarkdownTableAlignment::Right => v_flex().items_end(), - }; - - let cell_element = container - .col_span(cell.col_span.min(max_column_count - col_idx) as u16) - .row_span(cell.row_span.min(total_rows - row_idx) as u16) - .children(render_markdown_text(&cell.children, cx)) - .px_2() - .py_1() - .border_1() - .border_color(cx.border_color) - .when(cell.is_header, |this| { - this.bg(cx.title_bar_background_color) - }) - .when(cell.row_span > 1, |this| this.justify_center()) - .when(row_idx % 2 == 1, |this| this.bg(cx.panel_background_color)); - - cells.push(cell_element); - - // Mark grid positions as occupied for row-spanning cells - for r in 0..cell.row_span { - for c in 0..cell.col_span { - if row_idx + r < total_rows && col_idx + c < max_column_count { - grid_occupied[row_idx + r][col_idx + c] = true; - } - } - } - - col_idx += cell.col_span; - } - - // Fill remaining columns with empty cells if needed - while col_idx < max_column_count { - if grid_occupied[row_idx][col_idx] { - col_idx += 1; - continue; - } - - let empty_cell = div() - .border_1() - .border_color(cx.border_color) - .when(row_idx % 2 == 1, |this| this.bg(cx.panel_background_color)); - - cells.push(empty_cell); - col_idx += 1; - } - } - - cx.with_common_p(v_flex().items_start()) - .when_some(parsed.caption.as_ref(), |this, caption| { - this.children(render_markdown_text(caption, cx)) - }) - .child( - div() - .grid() - .grid_cols(max_column_count as u16) - .border_1() - .border_color(cx.border_color) - .children(cells), - ) - .into_any() -} - -fn render_markdown_block_quote( - parsed: &ParsedMarkdownBlockQuote, - cx: &mut RenderContext, -) -> AnyElement { - cx.indent += 1; - - let children: Vec = parsed - .children - .iter() - .enumerate() - .map(|(ix, child)| { - cx.with_last_child(ix + 1 == parsed.children.len(), |cx| { - render_markdown_block(child, cx) - }) - }) - .collect(); - - cx.indent -= 1; - - cx.with_common_p(div()) - .child( - div() - .border_l_4() - .border_color(cx.border_color) - .pl_3() - .children(children), - ) - .into_any() -} - -fn render_markdown_code_block( - parsed: &ParsedMarkdownCodeBlock, - cx: &mut RenderContext, -) -> AnyElement { - let body = if let Some(highlights) = parsed.highlights.as_ref() { - StyledText::new(parsed.contents.clone()).with_default_highlights( - &cx.buffer_text_style, - highlights.iter().filter_map(|(range, highlight_id)| { - highlight_id - .style(cx.syntax_theme.as_ref()) - .map(|style| (range.clone(), style)) - }), - ) - } else { - StyledText::new(parsed.contents.clone()) - }; - - let copy_block_button = IconButton::new("copy-code", IconName::Copy) - .icon_size(IconSize::Small) - .on_click({ - let contents = parsed.contents.clone(); - move |_, _window, cx| { - cx.write_to_clipboard(ClipboardItem::new_string(contents.to_string())); - } - }) - .tooltip(Tooltip::text("Copy code block")) - .visible_on_hover("markdown-block"); - - let font = gpui::Font { - family: cx.buffer_font_family.clone(), - features: cx.buffer_text_style.font_features.clone(), - ..Default::default() - }; - - cx.with_common_p(div()) - .font(font) - .px_3() - .py_3() - .bg(cx.code_block_background_color) - .rounded_sm() - .child(body) - .child( - div() - .h_flex() - .absolute() - .right_1() - .top_1() - .child(copy_block_button), - ) - .into_any() -} - -fn render_markdown_paragraph(parsed: &MarkdownParagraph, cx: &mut RenderContext) -> AnyElement { - cx.with_common_p(div()) - .children(render_markdown_text(parsed, cx)) - .flex() - .flex_col() - .into_any_element() -} - -fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext) -> Vec { - let mut any_element = Vec::with_capacity(parsed_new.len()); - // these values are cloned in-order satisfy borrow checker - let syntax_theme = cx.syntax_theme.clone(); - let workspace_clone = cx.workspace.clone(); - let code_span_bg_color = cx.code_span_background_color; - let text_style = cx.text_style.clone(); - let link_color = cx.link_color; - - for parsed_region in parsed_new { - match parsed_region { - MarkdownParagraphChunk::Text(parsed) => { - let element_id = cx.next_id(&parsed.source_range); - - let highlights = gpui::combine_highlights( - parsed.highlights.iter().filter_map(|(range, highlight)| { - highlight - .to_highlight_style(&syntax_theme) - .map(|style| (range.clone(), style)) - }), - parsed.regions.iter().filter_map(|(range, region)| { - if region.code { - Some(( - range.clone(), - HighlightStyle { - background_color: Some(code_span_bg_color), - ..Default::default() - }, - )) - } else if region.link.is_some() { - Some(( - range.clone(), - HighlightStyle { - color: Some(link_color), - ..Default::default() - }, - )) - } else { - None - } - }), - ); - let mut links = Vec::new(); - let mut link_ranges = Vec::new(); - for (range, region) in parsed.regions.iter() { - if let Some(link) = region.link.clone() { - links.push(link); - link_ranges.push(range.clone()); - } - } - let workspace = workspace_clone.clone(); - let element = div() - .child( - InteractiveText::new( - element_id, - StyledText::new(parsed.contents.clone()) - .with_default_highlights(&text_style, highlights), - ) - .tooltip({ - let links = links.clone(); - let link_ranges = link_ranges.clone(); - move |idx, _, cx| { - for (ix, range) in link_ranges.iter().enumerate() { - if range.contains(&idx) { - return Some(LinkPreview::new(&links[ix].to_string(), cx)); - } - } - None - } - }) - .on_click( - link_ranges, - move |clicked_range_ix, window, cx| match &links[clicked_range_ix] { - Link::Web { url } => cx.open_url(url), - Link::Path { path, .. } => { - if let Some(workspace) = &workspace { - _ = workspace.update(cx, |workspace, cx| { - workspace - .open_abs_path( - normalize_path(path.clone().as_path()), - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - window, - cx, - ) - .detach(); - }); - } - } - }, - ), - ) - .into_any(); - any_element.push(element); - } - - MarkdownParagraphChunk::Image(image) => { - any_element.push(render_markdown_image(image, cx)); - } - } - } - - any_element -} - -fn render_markdown_rule(cx: &mut RenderContext) -> AnyElement { - let rule = div().w_full().h(cx.scaled_rems(0.125)).bg(cx.border_color); - div().py(cx.scaled_rems(0.5)).child(rule).into_any() -} - -fn render_markdown_image(image: &Image, cx: &mut RenderContext) -> AnyElement { - let image_resource = match image.link.clone() { - Link::Web { url } => Resource::Uri(url.into()), - Link::Path { path, .. } => Resource::Path(Arc::from(path)), - }; - - let element_id = cx.next_id(&image.source_range); - let workspace = cx.workspace.clone(); - - div() - .id(element_id) - .cursor_pointer() - .child( - img(ImageSource::Resource(image_resource)) - .max_w_full() - .with_fallback({ - let alt_text = image.alt_text.clone(); - move || div().children(alt_text.clone()).into_any_element() - }) - .when_some(image.height, |this, height| this.h(height)) - .when_some(image.width, |this, width| this.w(width)), - ) - .tooltip({ - let link = image.link.clone(); - let alt_text = image.alt_text.clone(); - move |_, cx| { - InteractiveMarkdownElementTooltip::new( - Some(alt_text.clone().unwrap_or(link.to_string().into())), - "open image", - cx, - ) - .into() - } - }) - .on_click({ - let link = image.link.clone(); - move |_, window, cx| { - if window.modifiers().secondary() { - match &link { - Link::Web { url } => cx.open_url(url), - Link::Path { path, .. } => { - if let Some(workspace) = &workspace { - _ = workspace.update(cx, |workspace, cx| { - workspace - .open_abs_path( - path.clone(), - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - window, - cx, - ) - .detach(); - }); - } - } - } - } - } - }) - .into_any() -} - -struct InteractiveMarkdownElementTooltip { - tooltip_text: Option, - action_text: SharedString, -} - -impl InteractiveMarkdownElementTooltip { - pub fn new( - tooltip_text: Option, - action_text: impl Into, - cx: &mut App, - ) -> Entity { - let tooltip_text = tooltip_text.map(|t| util::truncate_and_trailoff(&t, 50).into()); - - cx.new(|_cx| Self { - tooltip_text, - action_text: action_text.into(), - }) - } -} - -impl Render for InteractiveMarkdownElementTooltip { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - tooltip_container(cx, |el, _| { - let secondary_modifier = Keystroke { - modifiers: Modifiers::secondary_key(), - ..Default::default() - }; - - el.child( - v_flex() - .gap_1() - .when_some(self.tooltip_text.clone(), |this, text| { - this.child(Label::new(text).size(LabelSize::Small)) - }) - .child( - Label::new(format!( - "{}-click to {}", - secondary_modifier, self.action_text - )) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - }) - } -} - -/// Returns the prefix for a list item. -fn list_item_prefix(order: usize, ordered: bool, depth: usize) -> String { - let ix = order.saturating_sub(1); - const NUMBERED_PREFIXES_1: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - const NUMBERED_PREFIXES_2: &str = "abcdefghijklmnopqrstuvwxyz"; - const BULLETS: [&str; 5] = ["•", "◦", "▪", "‣", "⁃"]; - - if ordered { - match depth { - 0 => format!("{}. ", order), - 1 => format!( - "{}. ", - NUMBERED_PREFIXES_1 - .chars() - .nth(ix % NUMBERED_PREFIXES_1.len()) - .unwrap() - ), - _ => format!( - "{}. ", - NUMBERED_PREFIXES_2 - .chars() - .nth(ix % NUMBERED_PREFIXES_2.len()) - .unwrap() - ), - } - } else { - let depth = depth.min(BULLETS.len() - 1); - let bullet = BULLETS[depth]; - return format!("{} ", bullet); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::markdown_elements::ParsedMarkdownTableColumn; - use crate::markdown_elements::ParsedMarkdownText; - - fn text(text: &str) -> MarkdownParagraphChunk { - MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..text.len(), - contents: SharedString::new(text), - highlights: Default::default(), - regions: Default::default(), - }) - } - - fn column( - col_span: usize, - row_span: usize, - children: Vec, - ) -> ParsedMarkdownTableColumn { - ParsedMarkdownTableColumn { - col_span, - row_span, - is_header: false, - children, - alignment: ParsedMarkdownTableAlignment::None, - } - } - - fn column_with_row_span( - col_span: usize, - row_span: usize, - children: Vec, - ) -> ParsedMarkdownTableColumn { - ParsedMarkdownTableColumn { - col_span, - row_span, - is_header: false, - children, - alignment: ParsedMarkdownTableAlignment::None, - } - } - - #[test] - fn test_calculate_table_columns_count() { - assert_eq!(0, calculate_table_columns_count(&vec![])); - - assert_eq!( - 1, - calculate_table_columns_count(&vec![ParsedMarkdownTableRow::with_columns(vec![ - column(1, 1, vec![text("column1")]) - ])]) - ); - - assert_eq!( - 2, - calculate_table_columns_count(&vec![ParsedMarkdownTableRow::with_columns(vec![ - column(1, 1, vec![text("column1")]), - column(1, 1, vec![text("column2")]), - ])]) - ); - - assert_eq!( - 2, - calculate_table_columns_count(&vec![ParsedMarkdownTableRow::with_columns(vec![ - column(2, 1, vec![text("column1")]) - ])]) - ); - - assert_eq!( - 3, - calculate_table_columns_count(&vec![ParsedMarkdownTableRow::with_columns(vec![ - column(1, 1, vec![text("column1")]), - column(2, 1, vec![text("column2")]), - ])]) - ); - - assert_eq!( - 2, - calculate_table_columns_count(&vec![ - ParsedMarkdownTableRow::with_columns(vec![ - column(1, 1, vec![text("column1")]), - column(1, 1, vec![text("column2")]), - ]), - ParsedMarkdownTableRow::with_columns(vec![column(1, 1, vec![text("column1")]),]) - ]) - ); - - assert_eq!( - 3, - calculate_table_columns_count(&vec![ - ParsedMarkdownTableRow::with_columns(vec![ - column(1, 1, vec![text("column1")]), - column(1, 1, vec![text("column2")]), - ]), - ParsedMarkdownTableRow::with_columns(vec![column(3, 3, vec![text("column1")]),]) - ]) - ); - } - - #[test] - fn test_row_span_support() { - assert_eq!( - 3, - calculate_table_columns_count(&vec![ - ParsedMarkdownTableRow::with_columns(vec![ - column_with_row_span(1, 2, vec![text("spans 2 rows")]), - column(1, 1, vec![text("column2")]), - column(1, 1, vec![text("column3")]), - ]), - ParsedMarkdownTableRow::with_columns(vec![ - // First column is covered by row span from above - column(1, 1, vec![text("column2 row2")]), - column(1, 1, vec![text("column3 row2")]), - ]) - ]) - ); - - assert_eq!( - 4, - calculate_table_columns_count(&vec![ - ParsedMarkdownTableRow::with_columns(vec![ - column_with_row_span(1, 3, vec![text("spans 3 rows")]), - column_with_row_span(2, 1, vec![text("spans 2 cols")]), - column(1, 1, vec![text("column4")]), - ]), - ParsedMarkdownTableRow::with_columns(vec![ - // First column covered by row span - column(1, 1, vec![text("column2")]), - column(1, 1, vec![text("column3")]), - column(1, 1, vec![text("column4")]), - ]), - ParsedMarkdownTableRow::with_columns(vec![ - // First column still covered by row span - column(3, 1, vec![text("spans 3 cols")]), - ]) - ]) - ); - } - - #[test] - fn test_list_item_prefix() { - assert_eq!(list_item_prefix(1, true, 0), "1. "); - assert_eq!(list_item_prefix(2, true, 0), "2. "); - assert_eq!(list_item_prefix(3, true, 0), "3. "); - assert_eq!(list_item_prefix(11, true, 0), "11. "); - assert_eq!(list_item_prefix(1, true, 1), "A. "); - assert_eq!(list_item_prefix(2, true, 1), "B. "); - assert_eq!(list_item_prefix(3, true, 1), "C. "); - assert_eq!(list_item_prefix(1, true, 2), "a. "); - assert_eq!(list_item_prefix(2, true, 2), "b. "); - assert_eq!(list_item_prefix(7, true, 2), "g. "); - assert_eq!(list_item_prefix(1, true, 1), "A. "); - assert_eq!(list_item_prefix(1, true, 2), "a. "); - assert_eq!(list_item_prefix(1, false, 0), "• "); - assert_eq!(list_item_prefix(1, false, 1), "◦ "); - assert_eq!(list_item_prefix(1, false, 2), "▪ "); - assert_eq!(list_item_prefix(1, false, 3), "‣ "); - assert_eq!(list_item_prefix(1, false, 4), "⁃ "); - } -} diff --git a/crates/menu/Cargo.toml b/crates/menu/Cargo.toml deleted file mode 100644 index fcb209df88..0000000000 --- a/crates/menu/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "menu" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/menu.rs" -doctest = false - -[dependencies] -gpui.workspace = true diff --git a/crates/menu/LICENSE-GPL b/crates/menu/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/menu/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/menu/src/menu.rs b/crates/menu/src/menu.rs deleted file mode 100644 index 9a1937d100..0000000000 --- a/crates/menu/src/menu.rs +++ /dev/null @@ -1,33 +0,0 @@ -use gpui::actions; - -// If the zed binary doesn't use anything in this crate, it will be optimized away -// and the actions won't initialize. So we just provide an empty initialization function -// to be called from main. -// -// These may provide relevant context: -// https://github.com/rust-lang/rust/issues/47384 -// https://github.com/mmastrac/rust-ctor/issues/280 -pub fn init() {} - -actions!( - menu, - [ - /// Cancels the current menu operation. - Cancel, - /// Confirms the selected menu item. - Confirm, - /// Performs secondary confirmation action. - SecondaryConfirm, - /// Selects the previous item in the menu. - SelectPrevious, - /// Selects the next item in the menu. - SelectNext, - /// Selects the first item in the menu. - SelectFirst, - /// Selects the last item in the menu. - SelectLast, - /// Restarts the menu from the beginning. - Restart, - EndSlot, - ] -); diff --git a/crates/migrator/Cargo.toml b/crates/migrator/Cargo.toml deleted file mode 100644 index e0a7578474..0000000000 --- a/crates/migrator/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "migrator" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/migrator.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -collections.workspace = true -convert_case.workspace = true -log.workspace = true -streaming-iterator.workspace = true -tree-sitter-json.workspace = true -tree-sitter.workspace = true -serde_json_lenient.workspace = true -serde_json.workspace = true -settings_json.workspace = true - -[dev-dependencies] -pretty_assertions.workspace = true -unindent.workspace = true diff --git a/crates/migrator/LICENSE-GPL b/crates/migrator/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/migrator/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/migrator/src/migrations.rs b/crates/migrator/src/migrations.rs deleted file mode 100644 index 398d5aaf94..0000000000 --- a/crates/migrator/src/migrations.rs +++ /dev/null @@ -1,161 +0,0 @@ -pub(crate) mod m_2025_01_02 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_01_29 { - mod keymap; - mod settings; - - pub(crate) use keymap::KEYMAP_PATTERNS; - pub(crate) use settings::{SETTINGS_PATTERNS, replace_edit_prediction_provider_setting}; -} - -pub(crate) mod m_2025_01_30 { - mod keymap; - mod settings; - - pub(crate) use keymap::KEYMAP_PATTERNS; - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_03_03 { - mod keymap; - - pub(crate) use keymap::KEYMAP_PATTERNS; -} - -pub(crate) mod m_2025_03_06 { - mod keymap; - - pub(crate) use keymap::KEYMAP_PATTERNS; -} - -pub(crate) mod m_2025_03_29 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_04_15 { - mod keymap; - mod settings; - - pub(crate) use keymap::KEYMAP_PATTERNS; - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_04_21 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_04_23 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_05_05 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_05_08 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_05_29 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_06_16 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_06_25 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_06_27 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_07_08 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_10_01 { - mod settings; - - pub(crate) use settings::flatten_code_actions_formatters; -} - -pub(crate) mod m_2025_10_02 { - mod settings; - - pub(crate) use settings::remove_formatters_on_save; -} - -pub(crate) mod m_2025_10_03 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_10_16 { - mod settings; - - pub(crate) use settings::restore_code_actions_on_format; -} - -pub(crate) mod m_2025_10_17 { - mod settings; - - pub(crate) use settings::make_file_finder_include_ignored_an_enum; -} - -pub(crate) mod m_2025_10_21 { - mod settings; - - pub(crate) use settings::make_relative_line_numbers_an_enum; -} - -pub(crate) mod m_2025_11_12 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_11_20 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_11_25 { - mod settings; - - pub(crate) use settings::remove_context_server_source; -} - -pub(crate) mod m_2025_12_01 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} diff --git a/crates/migrator/src/migrations/m_2025_01_02/settings.rs b/crates/migrator/src/migrations/m_2025_01_02/settings.rs deleted file mode 100644 index a35b1ebd2e..0000000000 --- a/crates/migrator/src/migrations/m_2025_01_02/settings.rs +++ /dev/null @@ -1,62 +0,0 @@ -use collections::HashMap; -use std::{ops::Range, sync::LazyLock}; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - replace_deprecated_settings_values, -)]; - -fn replace_deprecated_settings_values( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let parent_object_capture_ix = query.capture_index_for_name("parent_key")?; - let parent_object_range = mat - .nodes_for_capture_index(parent_object_capture_ix) - .next()? - .byte_range(); - let parent_object_name = contents.get(parent_object_range)?; - - let setting_name_ix = query.capture_index_for_name("setting_name")?; - let setting_name_range = mat - .nodes_for_capture_index(setting_name_ix) - .next()? - .byte_range(); - let setting_name = contents.get(setting_name_range)?; - - let setting_value_ix = query.capture_index_for_name("setting_value")?; - let setting_value_range = mat - .nodes_for_capture_index(setting_value_ix) - .next()? - .byte_range(); - let setting_value = contents.get(setting_value_range.clone())?; - - UPDATED_SETTINGS - .get(&(parent_object_name, setting_name)) - .and_then(|new_values| { - new_values - .iter() - .find_map(|(old_value, new_value)| { - (*old_value == setting_value).then(|| new_value.to_string()) - }) - .map(|new_value| (setting_value_range, new_value)) - }) -} - -static UPDATED_SETTINGS: LazyLock>> = LazyLock::new(|| { - HashMap::from_iter([ - ( - ("chat_panel", "button"), - vec![("true", "\"always\""), ("false", "\"never\"")], - ), - ( - ("scrollbar", "diagnostics"), - vec![("true", "\"all\""), ("false", "\"none\"")], - ), - ]) -}); diff --git a/crates/migrator/src/migrations/m_2025_01_29/keymap.rs b/crates/migrator/src/migrations/m_2025_01_29/keymap.rs deleted file mode 100644 index 222ad9716b..0000000000 --- a/crates/migrator/src/migrations/m_2025_01_29/keymap.rs +++ /dev/null @@ -1,307 +0,0 @@ -use collections::HashMap; -use std::{ops::Range, sync::LazyLock}; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::{ - KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN, KEYMAP_ACTION_ARRAY_PATTERN, - KEYMAP_ACTION_STRING_PATTERN, KEYMAP_CONTEXT_PATTERN, -}; - -pub const KEYMAP_PATTERNS: MigrationPatterns = &[ - ( - KEYMAP_ACTION_ARRAY_PATTERN, - replace_array_with_single_string, - ), - ( - KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN, - replace_action_argument_object_with_single_value, - ), - (KEYMAP_ACTION_STRING_PATTERN, replace_string_action), - (KEYMAP_CONTEXT_PATTERN, rename_context_key), -]; - -fn replace_array_with_single_string( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let array_ix = query.capture_index_for_name("array")?; - let action_name_ix = query.capture_index_for_name("action_name")?; - let argument_ix = query.capture_index_for_name("argument")?; - - let action_name = contents.get( - mat.nodes_for_capture_index(action_name_ix) - .next()? - .byte_range(), - )?; - let argument = contents.get( - mat.nodes_for_capture_index(argument_ix) - .next()? - .byte_range(), - )?; - - let replacement = TRANSFORM_ARRAY.get(&(action_name, argument))?; - let replacement_as_string = format!("\"{replacement}\""); - let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range(); - - Some((range_to_replace, replacement_as_string)) -} - -static TRANSFORM_ARRAY: LazyLock> = LazyLock::new(|| { - HashMap::from_iter([ - // activate - ( - ("workspace::ActivatePaneInDirection", "Up"), - "workspace::ActivatePaneUp", - ), - ( - ("workspace::ActivatePaneInDirection", "Down"), - "workspace::ActivatePaneDown", - ), - ( - ("workspace::ActivatePaneInDirection", "Left"), - "workspace::ActivatePaneLeft", - ), - ( - ("workspace::ActivatePaneInDirection", "Right"), - "workspace::ActivatePaneRight", - ), - // swap - ( - ("workspace::SwapPaneInDirection", "Up"), - "workspace::SwapPaneUp", - ), - ( - ("workspace::SwapPaneInDirection", "Down"), - "workspace::SwapPaneDown", - ), - ( - ("workspace::SwapPaneInDirection", "Left"), - "workspace::SwapPaneLeft", - ), - ( - ("workspace::SwapPaneInDirection", "Right"), - "workspace::SwapPaneRight", - ), - // menu - ( - ("app_menu::NavigateApplicationMenuInDirection", "Left"), - "app_menu::ActivateMenuLeft", - ), - ( - ("app_menu::NavigateApplicationMenuInDirection", "Right"), - "app_menu::ActivateMenuRight", - ), - // vim push - (("vim::PushOperator", "Change"), "vim::PushChange"), - (("vim::PushOperator", "Delete"), "vim::PushDelete"), - (("vim::PushOperator", "Yank"), "vim::PushYank"), - (("vim::PushOperator", "Replace"), "vim::PushReplace"), - ( - ("vim::PushOperator", "DeleteSurrounds"), - "vim::PushDeleteSurrounds", - ), - (("vim::PushOperator", "Mark"), "vim::PushMark"), - (("vim::PushOperator", "Indent"), "vim::PushIndent"), - (("vim::PushOperator", "Outdent"), "vim::PushOutdent"), - (("vim::PushOperator", "AutoIndent"), "vim::PushAutoIndent"), - (("vim::PushOperator", "Rewrap"), "vim::PushRewrap"), - ( - ("vim::PushOperator", "ShellCommand"), - "vim::PushShellCommand", - ), - (("vim::PushOperator", "Lowercase"), "vim::PushLowercase"), - (("vim::PushOperator", "Uppercase"), "vim::PushUppercase"), - ( - ("vim::PushOperator", "OppositeCase"), - "vim::PushOppositeCase", - ), - (("vim::PushOperator", "Register"), "vim::PushRegister"), - ( - ("vim::PushOperator", "RecordRegister"), - "vim::PushRecordRegister", - ), - ( - ("vim::PushOperator", "ReplayRegister"), - "vim::PushReplayRegister", - ), - ( - ("vim::PushOperator", "ReplaceWithRegister"), - "vim::PushReplaceWithRegister", - ), - ( - ("vim::PushOperator", "ToggleComments"), - "vim::PushToggleComments", - ), - // vim switch - (("vim::SwitchMode", "Normal"), "vim::SwitchToNormalMode"), - (("vim::SwitchMode", "Insert"), "vim::SwitchToInsertMode"), - (("vim::SwitchMode", "Replace"), "vim::SwitchToReplaceMode"), - (("vim::SwitchMode", "Visual"), "vim::SwitchToVisualMode"), - ( - ("vim::SwitchMode", "VisualLine"), - "vim::SwitchToVisualLineMode", - ), - ( - ("vim::SwitchMode", "VisualBlock"), - "vim::SwitchToVisualBlockMode", - ), - ( - ("vim::SwitchMode", "HelixNormal"), - "vim::SwitchToHelixNormalMode", - ), - // vim resize - (("vim::ResizePane", "Widen"), "vim::ResizePaneRight"), - (("vim::ResizePane", "Narrow"), "vim::ResizePaneLeft"), - (("vim::ResizePane", "Shorten"), "vim::ResizePaneDown"), - (("vim::ResizePane", "Lengthen"), "vim::ResizePaneUp"), - // fold at level - (("editor::FoldAtLevel", "1"), "editor::FoldAtLevel1"), - (("editor::FoldAtLevel", "2"), "editor::FoldAtLevel2"), - (("editor::FoldAtLevel", "3"), "editor::FoldAtLevel3"), - (("editor::FoldAtLevel", "4"), "editor::FoldAtLevel4"), - (("editor::FoldAtLevel", "5"), "editor::FoldAtLevel5"), - (("editor::FoldAtLevel", "6"), "editor::FoldAtLevel6"), - (("editor::FoldAtLevel", "7"), "editor::FoldAtLevel7"), - (("editor::FoldAtLevel", "8"), "editor::FoldAtLevel8"), - (("editor::FoldAtLevel", "9"), "editor::FoldAtLevel9"), - ]) -}); - -/// [ "editor::FoldAtLevel", { "level": 1 } ] -> [ "editor::FoldAtLevel", 1 ] -fn replace_action_argument_object_with_single_value( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let array_ix = query.capture_index_for_name("array")?; - let action_name_ix = query.capture_index_for_name("action_name")?; - let argument_key_ix = query.capture_index_for_name("argument_key")?; - let argument_value_ix = query.capture_index_for_name("argument_value")?; - - let action_name = contents.get( - mat.nodes_for_capture_index(action_name_ix) - .next()? - .byte_range(), - )?; - let argument_key = contents.get( - mat.nodes_for_capture_index(argument_key_ix) - .next()? - .byte_range(), - )?; - let argument_value = contents.get( - mat.nodes_for_capture_index(argument_value_ix) - .next()? - .byte_range(), - )?; - - let new_action_name = UNWRAP_OBJECTS.get(&action_name)?.get(&argument_key)?; - - let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range(); - let replacement = format!("[\"{}\", {}]", new_action_name, argument_value); - Some((range_to_replace, replacement)) -} - -/// "ctrl-k ctrl-1": [ "editor::PushOperator", { "Object": {} } ] -> [ "editor::vim::PushObject", {} ] -static UNWRAP_OBJECTS: LazyLock>> = LazyLock::new(|| { - HashMap::from_iter([ - ( - "editor::FoldAtLevel", - HashMap::from_iter([("level", "editor::FoldAtLevel")]), - ), - ( - "vim::PushOperator", - HashMap::from_iter([ - ("Object", "vim::PushObject"), - ("FindForward", "vim::PushFindForward"), - ("FindBackward", "vim::PushFindBackward"), - ("Sneak", "vim::PushSneak"), - ("SneakBackward", "vim::PushSneakBackward"), - ("AddSurrounds", "vim::PushAddSurrounds"), - ("ChangeSurrounds", "vim::PushChangeSurrounds"), - ("Jump", "vim::PushJump"), - ("Digraph", "vim::PushDigraph"), - ("Literal", "vim::PushLiteral"), - ]), - ), - ]) -}); - -fn replace_string_action( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let action_name_ix = query.capture_index_for_name("action_name")?; - let action_name_node = mat.nodes_for_capture_index(action_name_ix).next()?; - let action_name_range = action_name_node.byte_range(); - let action_name = contents.get(action_name_range.clone())?; - - if let Some(new_action_name) = STRING_REPLACE.get(&action_name) { - return Some((action_name_range, new_action_name.to_string())); - } - - None -} - -/// "ctrl-k ctrl-1": "inline_completion::ToggleMenu" -> "edit_prediction::ToggleMenu" -static STRING_REPLACE: LazyLock> = LazyLock::new(|| { - HashMap::from_iter([ - ( - "inline_completion::ToggleMenu", - "edit_prediction::ToggleMenu", - ), - ("editor::NextInlineCompletion", "editor::NextEditPrediction"), - ( - "editor::PreviousInlineCompletion", - "editor::PreviousEditPrediction", - ), - ( - "editor::AcceptPartialInlineCompletion", - "editor::AcceptPartialEditPrediction", - ), - ("editor::ShowInlineCompletion", "editor::ShowEditPrediction"), - ( - "editor::AcceptInlineCompletion", - "editor::AcceptEditPrediction", - ), - ( - "editor::ToggleInlineCompletions", - "editor::ToggleEditPrediction", - ), - ]) -}); - -fn rename_context_key( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let context_predicate_ix = query.capture_index_for_name("context_predicate")?; - let context_predicate_range = mat - .nodes_for_capture_index(context_predicate_ix) - .next()? - .byte_range(); - let old_predicate = contents.get(context_predicate_range.clone())?.to_string(); - let mut new_predicate = old_predicate.to_string(); - for (old_key, new_key) in CONTEXT_REPLACE.iter() { - new_predicate = new_predicate.replace(old_key, new_key); - } - if new_predicate != old_predicate { - Some((context_predicate_range, new_predicate)) - } else { - None - } -} - -/// "context": "Editor && inline_completion && !showing_completions" -> "Editor && edit_prediction && !showing_completions" -pub static CONTEXT_REPLACE: LazyLock> = LazyLock::new(|| { - HashMap::from_iter([ - ("inline_completion", "edit_prediction"), - ( - "inline_completion_requires_modifier", - "edit_prediction_requires_modifier", - ), - ]) -}); diff --git a/crates/migrator/src/migrations/m_2025_01_29/settings.rs b/crates/migrator/src/migrations/m_2025_01_29/settings.rs deleted file mode 100644 index 46cfe2f178..0000000000 --- a/crates/migrator/src/migrations/m_2025_01_29/settings.rs +++ /dev/null @@ -1,101 +0,0 @@ -use collections::HashMap; -use std::{ops::Range, sync::LazyLock}; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::{ - SETTINGS_LANGUAGES_PATTERN, SETTINGS_NESTED_KEY_VALUE_PATTERN, SETTINGS_ROOT_KEY_VALUE_PATTERN, -}; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[ - (SETTINGS_ROOT_KEY_VALUE_PATTERN, replace_setting_name), - ( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - replace_edit_prediction_provider_setting, - ), - (SETTINGS_LANGUAGES_PATTERN, replace_setting_in_languages), -]; - -fn replace_setting_name( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let setting_capture_ix = query.capture_index_for_name("name")?; - let setting_name_range = mat - .nodes_for_capture_index(setting_capture_ix) - .next()? - .byte_range(); - let setting_name = contents.get(setting_name_range.clone())?; - let new_setting_name = SETTINGS_STRING_REPLACE.get(&setting_name)?; - Some((setting_name_range, new_setting_name.to_string())) -} - -pub static SETTINGS_STRING_REPLACE: LazyLock> = - LazyLock::new(|| { - HashMap::from_iter([ - ( - "show_inline_completions_in_menu", - "show_edit_predictions_in_menu", - ), - ("show_inline_completions", "show_edit_predictions"), - ( - "inline_completions_disabled_in", - "edit_predictions_disabled_in", - ), - ("inline_completions", "edit_predictions"), - ]) - }); - -pub fn replace_edit_prediction_provider_setting( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let parent_object_capture_ix = query.capture_index_for_name("parent_key")?; - let parent_object_range = mat - .nodes_for_capture_index(parent_object_capture_ix) - .next()? - .byte_range(); - let parent_object_name = contents.get(parent_object_range)?; - - let setting_name_ix = query.capture_index_for_name("setting_name")?; - let setting_range = mat - .nodes_for_capture_index(setting_name_ix) - .next()? - .byte_range(); - let setting_name = contents.get(setting_range.clone())?; - - if parent_object_name == "features" && setting_name == "inline_completion_provider" { - return Some((setting_range, "edit_prediction_provider".into())); - } - - None -} - -fn replace_setting_in_languages( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let setting_capture_ix = query.capture_index_for_name("setting_name")?; - let setting_name_range = mat - .nodes_for_capture_index(setting_capture_ix) - .next()? - .byte_range(); - let setting_name = contents.get(setting_name_range.clone())?; - let new_setting_name = LANGUAGE_SETTINGS_REPLACE.get(&setting_name)?; - - Some((setting_name_range, new_setting_name.to_string())) -} - -static LANGUAGE_SETTINGS_REPLACE: LazyLock> = - LazyLock::new(|| { - HashMap::from_iter([ - ("show_inline_completions", "show_edit_predictions"), - ( - "inline_completions_disabled_in", - "edit_predictions_disabled_in", - ), - ]) - }); diff --git a/crates/migrator/src/migrations/m_2025_01_30/keymap.rs b/crates/migrator/src/migrations/m_2025_01_30/keymap.rs deleted file mode 100644 index 0e131fea02..0000000000 --- a/crates/migrator/src/migrations/m_2025_01_30/keymap.rs +++ /dev/null @@ -1,82 +0,0 @@ -use collections::HashMap; -use convert_case::{Case, Casing}; -use std::{ops::Range, sync::LazyLock}; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN; - -pub const KEYMAP_PATTERNS: MigrationPatterns = &[( - KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN, - action_argument_snake_case, -)]; - -fn to_snake_case(text: &str) -> String { - text.to_case(Case::Snake) -} - -fn action_argument_snake_case( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let array_ix = query.capture_index_for_name("array")?; - let action_name_ix = query.capture_index_for_name("action_name")?; - let argument_key_ix = query.capture_index_for_name("argument_key")?; - let argument_value_ix = query.capture_index_for_name("argument_value")?; - let action_name = contents.get( - mat.nodes_for_capture_index(action_name_ix) - .next()? - .byte_range(), - )?; - - let replacement_key = ACTION_ARGUMENT_SNAKE_CASE_REPLACE.get(action_name)?; - let argument_key = contents.get( - mat.nodes_for_capture_index(argument_key_ix) - .next()? - .byte_range(), - )?; - - if argument_key != *replacement_key { - return None; - } - - let argument_value_node = mat.nodes_for_capture_index(argument_value_ix).next()?; - let argument_value = contents.get(argument_value_node.byte_range())?; - - let new_key = to_snake_case(argument_key); - let new_value = if argument_value_node.kind() == "string" { - format!("\"{}\"", to_snake_case(argument_value.trim_matches('"'))) - } else { - argument_value.to_string() - }; - - let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range(); - let replacement = format!( - "[\"{}\", {{ \"{}\": {} }}]", - action_name, new_key, new_value - ); - - Some((range_to_replace, replacement)) -} - -static ACTION_ARGUMENT_SNAKE_CASE_REPLACE: LazyLock> = LazyLock::new(|| { - HashMap::from_iter([ - ("vim::NextWordStart", "ignorePunctuation"), - ("vim::NextWordEnd", "ignorePunctuation"), - ("vim::PreviousWordStart", "ignorePunctuation"), - ("vim::PreviousWordEnd", "ignorePunctuation"), - ("vim::MoveToNext", "partialWord"), - ("vim::MoveToPrev", "partialWord"), - ("vim::Down", "displayLines"), - ("vim::Up", "displayLines"), - ("vim::EndOfLine", "displayLines"), - ("vim::StartOfLine", "displayLines"), - ("vim::FirstNonWhitespace", "displayLines"), - ("pane::CloseActiveItem", "saveIntent"), - ("vim::Paste", "preserveClipboard"), - ("vim::Word", "ignorePunctuation"), - ("vim::Subword", "ignorePunctuation"), - ("vim::IndentObj", "includeBelow"), - ]) -}); diff --git a/crates/migrator/src/migrations/m_2025_01_30/settings.rs b/crates/migrator/src/migrations/m_2025_01_30/settings.rs deleted file mode 100644 index 2d763e4722..0000000000 --- a/crates/migrator/src/migrations/m_2025_01_30/settings.rs +++ /dev/null @@ -1,83 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[ - ( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - replace_tab_close_button_setting_key, - ), - ( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - replace_tab_close_button_setting_value, - ), -]; - -fn replace_tab_close_button_setting_key( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let parent_object_capture_ix = query.capture_index_for_name("parent_key")?; - let parent_object_range = mat - .nodes_for_capture_index(parent_object_capture_ix) - .next()? - .byte_range(); - let parent_object_name = contents.get(parent_object_range)?; - - let setting_name_ix = query.capture_index_for_name("setting_name")?; - let setting_range = mat - .nodes_for_capture_index(setting_name_ix) - .next()? - .byte_range(); - let setting_name = contents.get(setting_range.clone())?; - - if parent_object_name == "tabs" && setting_name == "always_show_close_button" { - return Some((setting_range, "show_close_button".into())); - } - - None -} - -fn replace_tab_close_button_setting_value( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let parent_object_capture_ix = query.capture_index_for_name("parent_key")?; - let parent_object_range = mat - .nodes_for_capture_index(parent_object_capture_ix) - .next()? - .byte_range(); - let parent_object_name = contents.get(parent_object_range)?; - - let setting_name_ix = query.capture_index_for_name("setting_name")?; - let setting_name_range = mat - .nodes_for_capture_index(setting_name_ix) - .next()? - .byte_range(); - let setting_name = contents.get(setting_name_range)?; - - let setting_value_ix = query.capture_index_for_name("setting_value")?; - let setting_value_range = mat - .nodes_for_capture_index(setting_value_ix) - .next()? - .byte_range(); - let setting_value = contents.get(setting_value_range.clone())?; - - if parent_object_name == "tabs" && setting_name == "always_show_close_button" { - match setting_value { - "true" => { - return Some((setting_value_range, "\"always\"".to_string())); - } - "false" => { - return Some((setting_value_range, "\"hover\"".to_string())); - } - _ => {} - } - } - - None -} diff --git a/crates/migrator/src/migrations/m_2025_03_03/keymap.rs b/crates/migrator/src/migrations/m_2025_03_03/keymap.rs deleted file mode 100644 index 2342a8f696..0000000000 --- a/crates/migrator/src/migrations/m_2025_03_03/keymap.rs +++ /dev/null @@ -1,75 +0,0 @@ -use collections::HashMap; -use std::{ops::Range, sync::LazyLock}; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::KEYMAP_ACTION_STRING_PATTERN; - -pub const KEYMAP_PATTERNS: MigrationPatterns = - &[(KEYMAP_ACTION_STRING_PATTERN, replace_string_action)]; - -fn replace_string_action( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let action_name_ix = query.capture_index_for_name("action_name")?; - let action_name_node = mat.nodes_for_capture_index(action_name_ix).next()?; - let action_name_range = action_name_node.byte_range(); - let action_name = contents.get(action_name_range.clone())?; - - if let Some(new_action_name) = STRING_REPLACE.get(&action_name) { - return Some((action_name_range, new_action_name.to_string())); - } - - if let Some((new_action_name, options)) = STRING_TO_ARRAY_REPLACE.get(action_name) { - let full_string_range = action_name_node.parent()?.byte_range(); - let mut options_parts = Vec::new(); - for (key, value) in options.iter() { - options_parts.push(format!("\"{}\": {}", key, value)); - } - let options_str = options_parts.join(", "); - let replacement = format!("[\"{}\", {{ {} }}]", new_action_name, options_str); - return Some((full_string_range, replacement)); - } - - None -} - -static STRING_REPLACE: LazyLock> = LazyLock::new(|| { - HashMap::from_iter([ - ( - "editor::GoToPrevDiagnostic", - "editor::GoToPreviousDiagnostic", - ), - ("editor::ContextMenuPrev", "editor::ContextMenuPrevious"), - ("search::SelectPrevMatch", "search::SelectPreviousMatch"), - ("file_finder::SelectPrev", "file_finder::SelectPrevious"), - ("menu::SelectPrev", "menu::SelectPrevious"), - ("editor::TabPrev", "editor::Backtab"), - ("pane::ActivatePrevItem", "pane::ActivatePreviousItem"), - ("vim::MoveToPrev", "vim::MoveToPrevious"), - ("vim::MoveToPrevMatch", "vim::MoveToPreviousMatch"), - ]) -}); - -/// "editor::GoToPrevHunk" -> ["editor::GoToPreviousHunk", { "center_cursor": true }] -static STRING_TO_ARRAY_REPLACE: LazyLock)>> = - LazyLock::new(|| { - HashMap::from_iter([ - ( - "editor::GoToHunk", - ( - "editor::GoToHunk", - HashMap::from_iter([("center_cursor", true)]), - ), - ), - ( - "editor::GoToPrevHunk", - ( - "editor::GoToPreviousHunk", - HashMap::from_iter([("center_cursor", true)]), - ), - ), - ]) - }); diff --git a/crates/migrator/src/migrations/m_2025_03_06/keymap.rs b/crates/migrator/src/migrations/m_2025_03_06/keymap.rs deleted file mode 100644 index 333535908d..0000000000 --- a/crates/migrator/src/migrations/m_2025_03_06/keymap.rs +++ /dev/null @@ -1,38 +0,0 @@ -use collections::HashSet; -use std::{ops::Range, sync::LazyLock}; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN; - -pub const KEYMAP_PATTERNS: MigrationPatterns = &[( - KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN, - replace_array_with_single_string, -)]; - -fn replace_array_with_single_string( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let array_ix = query.capture_index_for_name("array")?; - let action_name_ix = query.capture_index_for_name("action_name")?; - - let action_name = contents.get( - mat.nodes_for_capture_index(action_name_ix) - .next()? - .byte_range(), - )?; - - if TRANSFORM_ARRAY.contains(&action_name) { - let replacement_as_string = format!("\"{action_name}\""); - let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range(); - return Some((range_to_replace, replacement_as_string)); - } - - None -} - -/// ["editor::GoToPreviousHunk", { "center_cursor": true }] -> "editor::GoToPreviousHunk" -static TRANSFORM_ARRAY: LazyLock> = - LazyLock::new(|| HashSet::from_iter(["editor::GoToHunk", "editor::GoToPreviousHunk"])); diff --git a/crates/migrator/src/migrations/m_2025_03_29/settings.rs b/crates/migrator/src/migrations/m_2025_03_29/settings.rs deleted file mode 100644 index 8f83d8e39e..0000000000 --- a/crates/migrator/src/migrations/m_2025_03_29/settings.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_ROOT_KEY_VALUE_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[ - (SETTINGS_ROOT_KEY_VALUE_PATTERN, replace_setting_name), - (SETTINGS_ROOT_KEY_VALUE_PATTERN, replace_setting_value), -]; - -fn replace_setting_value( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let setting_capture_ix = query.capture_index_for_name("name")?; - let setting_name_range = mat - .nodes_for_capture_index(setting_capture_ix) - .next()? - .byte_range(); - let setting_name = contents.get(setting_name_range)?; - - if setting_name != "hide_mouse_while_typing" { - return None; - } - - let value_capture_ix = query.capture_index_for_name("value")?; - let value_range = mat - .nodes_for_capture_index(value_capture_ix) - .next()? - .byte_range(); - let value = contents.get(value_range.clone())?; - - let new_value = if value.trim() == "true" { - "\"on_typing_and_movement\"" - } else if value.trim() == "false" { - "\"never\"" - } else { - return None; - }; - - Some((value_range, new_value.to_string())) -} - -fn replace_setting_name( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let setting_capture_ix = query.capture_index_for_name("name")?; - let setting_name_range = mat - .nodes_for_capture_index(setting_capture_ix) - .next()? - .byte_range(); - let setting_name = contents.get(setting_name_range.clone())?; - - let new_setting_name = if setting_name == "hide_mouse_while_typing" { - "hide_mouse" - } else { - return None; - }; - - Some((setting_name_range, new_setting_name.to_string())) -} diff --git a/crates/migrator/src/migrations/m_2025_04_15/keymap.rs b/crates/migrator/src/migrations/m_2025_04_15/keymap.rs deleted file mode 100644 index efbdc6b1c6..0000000000 --- a/crates/migrator/src/migrations/m_2025_04_15/keymap.rs +++ /dev/null @@ -1,31 +0,0 @@ -use collections::HashMap; -use std::{ops::Range, sync::LazyLock}; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::KEYMAP_ACTION_STRING_PATTERN; - -pub const KEYMAP_PATTERNS: MigrationPatterns = - &[(KEYMAP_ACTION_STRING_PATTERN, replace_string_action)]; - -fn replace_string_action( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let action_name_ix = query.capture_index_for_name("action_name")?; - let action_name_node = mat.nodes_for_capture_index(action_name_ix).next()?; - let action_name_range = action_name_node.byte_range(); - let action_name = contents.get(action_name_range.clone())?; - - if let Some(new_action_name) = STRING_REPLACE.get(&action_name) { - return Some((action_name_range, new_action_name.to_string())); - } - - None -} - -/// "space": "outline_panel::Open" -> "outline_panel::OpenSelectedEntry" -static STRING_REPLACE: LazyLock> = LazyLock::new(|| { - HashMap::from_iter([("outline_panel::Open", "outline_panel::OpenSelectedEntry")]) -}); diff --git a/crates/migrator/src/migrations/m_2025_04_15/settings.rs b/crates/migrator/src/migrations/m_2025_04_15/settings.rs deleted file mode 100644 index cbebd2a902..0000000000 --- a/crates/migrator/src/migrations/m_2025_04_15/settings.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_ASSISTANT_TOOLS_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[( - SETTINGS_ASSISTANT_TOOLS_PATTERN, - replace_bash_with_terminal_in_profiles, -)]; - -fn replace_bash_with_terminal_in_profiles( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let tool_name_capture_ix = query.capture_index_for_name("tool_name")?; - let tool_name_range = mat - .nodes_for_capture_index(tool_name_capture_ix) - .next()? - .byte_range(); - let tool_name = contents.get(tool_name_range.clone())?; - - if tool_name != "bash" { - return None; - } - - Some((tool_name_range, "terminal".to_string())) -} diff --git a/crates/migrator/src/migrations/m_2025_04_21/settings.rs b/crates/migrator/src/migrations/m_2025_04_21/settings.rs deleted file mode 100644 index 55afc54928..0000000000 --- a/crates/migrator/src/migrations/m_2025_04_21/settings.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_ASSISTANT_TOOLS_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = - &[(SETTINGS_ASSISTANT_TOOLS_PATTERN, rename_tools)]; - -fn rename_tools(contents: &str, mat: &QueryMatch, query: &Query) -> Option<(Range, String)> { - let tool_name_capture_ix = query.capture_index_for_name("tool_name")?; - let tool_name_range = mat - .nodes_for_capture_index(tool_name_capture_ix) - .next()? - .byte_range(); - let tool_name = contents.get(tool_name_range.clone())?; - - let new_name = match tool_name { - "find_replace_file" => "edit_file", - "regex_search" => "grep", - _ => return None, - }; - - Some((tool_name_range, new_name.to_string())) -} diff --git a/crates/migrator/src/migrations/m_2025_04_23/settings.rs b/crates/migrator/src/migrations/m_2025_04_23/settings.rs deleted file mode 100644 index 54bb999640..0000000000 --- a/crates/migrator/src/migrations/m_2025_04_23/settings.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_ASSISTANT_TOOLS_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = - &[(SETTINGS_ASSISTANT_TOOLS_PATTERN, rename_path_search_tool)]; - -fn rename_path_search_tool( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let tool_name_capture_ix = query.capture_index_for_name("tool_name")?; - let tool_name_range = mat - .nodes_for_capture_index(tool_name_capture_ix) - .next()? - .byte_range(); - let tool_name = contents.get(tool_name_range.clone())?; - - if tool_name == "path_search" { - return Some((tool_name_range, "find_path".to_string())); - } - - 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 deleted file mode 100644 index 77da1b9a07..0000000000 --- a/crates/migrator/src/migrations/m_2025_05_05/settings.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::{ - MigrationPatterns, patterns::SETTINGS_ASSISTANT_PATTERN, - patterns::SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN, -}; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[ - (SETTINGS_ASSISTANT_PATTERN, rename_assistant), - ( - SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN, - rename_edit_prediction_assistant, - ), -]; - -fn rename_assistant( - _contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let key_capture_ix = query.capture_index_for_name("key")?; - let key_range = mat - .nodes_for_capture_index(key_capture_ix) - .next()? - .byte_range(); - Some((key_range, "agent".to_string())) -} - -fn rename_edit_prediction_assistant( - _contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let key_capture_ix = query.capture_index_for_name("enabled_in_assistant")?; - let key_range = mat - .nodes_for_capture_index(key_capture_ix) - .next()? - .byte_range(); - Some((key_range, "enabled_in_text_threads".to_string())) -} diff --git a/crates/migrator/src/migrations/m_2025_05_08/settings.rs b/crates/migrator/src/migrations/m_2025_05_08/settings.rs deleted file mode 100644 index 6157ad0d43..0000000000 --- a/crates/migrator/src/migrations/m_2025_05_08/settings.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::{MigrationPatterns, patterns::SETTINGS_DUPLICATED_AGENT_PATTERN}; - -pub const SETTINGS_PATTERNS: MigrationPatterns = - &[(SETTINGS_DUPLICATED_AGENT_PATTERN, comment_duplicated_agent)]; - -fn comment_duplicated_agent( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let pair_ix = query.capture_index_for_name("pair1")?; - let mut range = mat.nodes_for_capture_index(pair_ix).next()?.byte_range(); - - // Include the comma into the commented region - let rtext = &contents[range.end..]; - if let Some(comma_index) = rtext.find(',') { - range.end += comma_index + 1; - } - - let value = contents[range.clone()].to_string(); - let commented_value = format!("/* Duplicated key auto-commented: {value} */"); - Some((range, commented_value)) -} diff --git a/crates/migrator/src/migrations/m_2025_05_29/settings.rs b/crates/migrator/src/migrations/m_2025_05_29/settings.rs deleted file mode 100644 index 37ef0e45cc..0000000000 --- a/crates/migrator/src/migrations/m_2025_05_29/settings.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - replace_preferred_completion_mode_value, -)]; - -fn replace_preferred_completion_mode_value( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let parent_object_capture_ix = query.capture_index_for_name("parent_key")?; - let parent_object_range = mat - .nodes_for_capture_index(parent_object_capture_ix) - .next()? - .byte_range(); - let parent_object_name = contents.get(parent_object_range)?; - - if parent_object_name != "agent" { - return None; - } - - let setting_name_capture_ix = query.capture_index_for_name("setting_name")?; - let setting_name_range = mat - .nodes_for_capture_index(setting_name_capture_ix) - .next()? - .byte_range(); - let setting_name = contents.get(setting_name_range)?; - - if setting_name != "preferred_completion_mode" { - return None; - } - - let value_capture_ix = query.capture_index_for_name("setting_value")?; - let value_range = mat - .nodes_for_capture_index(value_capture_ix) - .next()? - .byte_range(); - let value = contents.get(value_range.clone())?; - - if value.trim() == "\"max\"" { - Some((value_range, "\"burn\"".to_string())) - } else { - 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 deleted file mode 100644 index cd79eae204..0000000000 --- a/crates/migrator/src/migrations/m_2025_06_16/settings.rs +++ /dev/null @@ -1,90 +0,0 @@ -use std::ops::Range; - -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[( - SETTINGS_CONTEXT_SERVER_PATTERN, - migrate_context_server_settings, -)]; - -const SETTINGS_CONTEXT_SERVER_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @context-servers) - value: (object - (pair - key: (string (string_content) @server-name) - value: (object) @server-settings - ) - ) - ) - ) - (#eq? @context-servers "context_servers") -)"#; - -fn migrate_context_server_settings( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let server_settings_index = query.capture_index_for_name("server-settings")?; - let server_settings = mat.nodes_for_capture_index(server_settings_index).next()?; - - let mut has_command = false; - let mut has_settings = false; - let mut other_keys = 0; - let mut column = None; - - // 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, - } - } - } - } - - let source_type = if has_command { "custom" } else { "extension" }; - - // Insert the source key at the beginning of the object - let start = server_settings.start_byte() + 1; - let indent = " ".repeat(column.unwrap_or(12)); - - if !has_command && !has_settings { - return Some(( - start..start, - format!( - r#" -{indent}"source": "{}", -{indent}"settings": {{}}{} - "#, - source_type, - if other_keys > 0 { "," } else { "" } - ), - )); - } - - Some(( - start..start, - format!( - r#" -{indent}"source": "{}","#, - source_type - ), - )) -} diff --git a/crates/migrator/src/migrations/m_2025_06_25/settings.rs b/crates/migrator/src/migrations/m_2025_06_25/settings.rs deleted file mode 100644 index 2bf7658eeb..0000000000 --- a/crates/migrator/src/migrations/m_2025_06_25/settings.rs +++ /dev/null @@ -1,133 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[ - (SETTINGS_VERSION_PATTERN, remove_version_fields), - ( - SETTINGS_NESTED_VERSION_PATTERN, - remove_nested_version_fields, - ), -]; - -const SETTINGS_VERSION_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @key) - value: (object - (pair - key: (string (string_content) @version_key) - value: (_) @version_value - ) @version_pair - ) - ) - ) - (#eq? @key "agent") - (#eq? @version_key "version") -)"#; - -const SETTINGS_NESTED_VERSION_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @language_models) - value: (object - (pair - key: (string (string_content) @provider) - value: (object - (pair - key: (string (string_content) @version_key) - value: (_) @version_value - ) @version_pair - ) - ) - ) - ) - ) - (#eq? @language_models "language_models") - (#match? @provider "^(anthropic|openai)$") - (#eq? @version_key "version") -)"#; - -fn remove_version_fields( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let version_pair_ix = query.capture_index_for_name("version_pair")?; - let version_pair_node = mat.nodes_for_capture_index(version_pair_ix).next()?; - - remove_pair_with_whitespace(contents, version_pair_node) -} - -fn remove_nested_version_fields( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let version_pair_ix = query.capture_index_for_name("version_pair")?; - let version_pair_node = mat.nodes_for_capture_index(version_pair_ix).next()?; - - remove_pair_with_whitespace(contents, version_pair_node) -} - -fn remove_pair_with_whitespace( - contents: &str, - pair_node: tree_sitter::Node, -) -> Option<(Range, String)> { - let mut range_to_remove = pair_node.byte_range(); - - // Check if there's a comma after this pair - if let Some(next_sibling) = pair_node.next_sibling() { - if next_sibling.kind() == "," { - range_to_remove.end = next_sibling.end_byte(); - } - } 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(); - } - } - - // Include any leading whitespace/newline, including comments - let text_before = &contents[..range_to_remove.start]; - if let Some(last_newline) = text_before.rfind('\n') { - let whitespace_start = last_newline + 1; - let potential_whitespace = &contents[whitespace_start..range_to_remove.start]; - - // Check if it's only whitespace or comments - let mut is_whitespace_or_comment = true; - let mut in_comment = false; - let mut chars = potential_whitespace.chars().peekable(); - - while let Some(ch) = chars.next() { - if in_comment { - if ch == '\n' { - in_comment = false; - } - } else if ch == '/' && chars.peek() == Some(&'/') { - in_comment = true; - chars.next(); // Skip the second '/' - } else if !ch.is_whitespace() { - is_whitespace_or_comment = false; - break; - } - } - - if is_whitespace_or_comment { - range_to_remove.start = whitespace_start; - } - } - - // 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; - } - - 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 deleted file mode 100644 index e3e951b1a6..0000000000 --- a/crates/migrator/src/migrations/m_2025_06_27/settings.rs +++ /dev/null @@ -1,132 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[( - SETTINGS_CONTEXT_SERVER_PATTERN, - flatten_context_server_command, -)]; - -const SETTINGS_CONTEXT_SERVER_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @context-servers) - value: (object - (pair - key: (string (string_content) @server-name) - value: (object - (pair - key: (string (string_content) @source-key) - value: (string (string_content) @source-value) - ) - (pair - key: (string (string_content) @command-key) - value: (object) @command-object - ) @command-pair - ) @server-settings - ) - ) - ) - ) - (#eq? @context-servers "context_servers") - (#eq? @source-key "source") - (#eq? @source-value "custom") - (#eq? @command-key "command") -)"#; - -fn flatten_context_server_command( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let command_pair_index = query.capture_index_for_name("command-pair")?; - let command_pair = mat.nodes_for_capture_index(command_pair_index).next()?; - - let command_object_index = query.capture_index_for_name("command-object")?; - let command_object = mat.nodes_for_capture_index(command_object_index).next()?; - - let server_settings_index = query.capture_index_for_name("server-settings")?; - let _server_settings = mat.nodes_for_capture_index(server_settings_index).next()?; - - // Parse the command object to extract path, args, and env - let mut path_value = None; - let mut args_value = None; - let mut env_value = None; - - 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]), - _ => {} - } - } - } - } - - let path = path_value?; - - // Get the proper indentation from the command pair - let command_pair_start = command_pair.start_byte(); - let line_start = contents[..command_pair_start] - .rfind('\n') - .map(|pos| pos + 1) - .unwrap_or(0); - let indent = &contents[line_start..command_pair_start]; - - // Build the replacement string - let mut replacement = format!("\"command\": {}", path); - - // Add args if present - need to reduce indentation - if let Some(args) = args_value { - replacement.push_str(",\n"); - replacement.push_str(indent); - replacement.push_str("\"args\": "); - let reduced_args = reduce_indentation(args, 4); - replacement.push_str(&reduced_args); - } - - // Add env if present - need to reduce indentation - if let Some(env) = env_value { - replacement.push_str(",\n"); - replacement.push_str(indent); - replacement.push_str("\"env\": "); - replacement.push_str(&reduce_indentation(env, 4)); - } - - let range_to_replace = command_pair.byte_range(); - Some((range_to_replace, replacement)) -} - -fn reduce_indentation(text: &str, spaces: usize) -> String { - let lines: Vec<&str> = text.lines().collect(); - let mut result = String::new(); - - for (i, line) in lines.iter().enumerate() { - if i > 0 { - result.push('\n'); - } - - // Count leading spaces - let leading_spaces = line.chars().take_while(|&c| c == ' ').count(); - - if leading_spaces >= spaces { - // Reduce indentation - result.push_str(&line[spaces..]); - } else { - // Keep line as is if it doesn't have enough indentation - result.push_str(line); - } - } - - result -} diff --git a/crates/migrator/src/migrations/m_2025_07_08/settings.rs b/crates/migrator/src/migrations/m_2025_07_08/settings.rs deleted file mode 100644 index c9656491ce..0000000000 --- a/crates/migrator/src/migrations/m_2025_07_08/settings.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_ROOT_KEY_VALUE_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[( - SETTINGS_ROOT_KEY_VALUE_PATTERN, - migrate_drag_and_drop_selection, -)]; - -fn migrate_drag_and_drop_selection( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let name_ix = query.capture_index_for_name("name")?; - let name_range = mat.nodes_for_capture_index(name_ix).next()?.byte_range(); - let name = contents.get(name_range)?; - - if name != "drag_and_drop_selection" { - return None; - } - - let value_ix = query.capture_index_for_name("value")?; - let value_node = mat.nodes_for_capture_index(value_ix).next()?; - let value_range = value_node.byte_range(); - let value = contents.get(value_range.clone())?; - - match value { - "true" | "false" => { - let replacement = format!("{{\n \"enabled\": {}\n }}", value); - Some((value_range, replacement)) - } - _ => None, - } -} diff --git a/crates/migrator/src/migrations/m_2025_10_01/settings.rs b/crates/migrator/src/migrations/m_2025_10_01/settings.rs deleted file mode 100644 index 84cf950491..0000000000 --- a/crates/migrator/src/migrations/m_2025_10_01/settings.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::patterns::migrate_language_setting; -use anyhow::Result; -use serde_json::Value; - -pub fn flatten_code_actions_formatters(value: &mut Value) -> Result<()> { - migrate_language_setting(value, |value, _path| { - let Some(obj) = value.as_object_mut() else { - return Ok(()); - }; - for key in ["formatter", "format_on_save"] { - let Some(formatter) = obj.get_mut(key) else { - continue; - }; - let new_formatter = match formatter { - Value::Array(arr) => { - let mut new_arr = Vec::new(); - let mut found_code_actions = false; - for item in arr { - let Some(obj) = item.as_object() else { - new_arr.push(item.clone()); - continue; - }; - let code_actions_obj = obj - .get("code_actions") - .and_then(|code_actions| code_actions.as_object()); - let Some(code_actions) = code_actions_obj else { - new_arr.push(item.clone()); - continue; - }; - found_code_actions = true; - for (name, enabled) in code_actions { - if !enabled.as_bool().unwrap_or(true) { - continue; - } - new_arr.push(serde_json::json!({ - "code_action": name - })); - } - } - if !found_code_actions { - continue; - } - Value::Array(new_arr) - } - Value::Object(obj) => { - let mut new_arr = Vec::new(); - let code_actions_obj = obj - .get("code_actions") - .and_then(|code_actions| code_actions.as_object()); - let Some(code_actions) = code_actions_obj else { - continue; - }; - for (name, enabled) in code_actions { - if !enabled.as_bool().unwrap_or(true) { - continue; - } - new_arr.push(serde_json::json!({ - "code_action": name - })); - } - if new_arr.len() == 1 { - new_arr.pop().unwrap() - } else { - Value::Array(new_arr) - } - } - _ => continue, - }; - - obj.insert(key.to_string(), new_formatter); - } - return Ok(()); - }) -} diff --git a/crates/migrator/src/migrations/m_2025_10_02/settings.rs b/crates/migrator/src/migrations/m_2025_10_02/settings.rs deleted file mode 100644 index cb0d63ca85..0000000000 --- a/crates/migrator/src/migrations/m_2025_10_02/settings.rs +++ /dev/null @@ -1,41 +0,0 @@ -use anyhow::Result; -use serde_json::Value; - -use crate::patterns::migrate_language_setting; - -pub fn remove_formatters_on_save(value: &mut Value) -> Result<()> { - migrate_language_setting(value, remove_formatters_on_save_inner) -} - -fn remove_formatters_on_save_inner(value: &mut Value, path: &[&str]) -> Result<()> { - let Some(obj) = value.as_object_mut() else { - return Ok(()); - }; - let Some(format_on_save) = obj.get("format_on_save").cloned() else { - return Ok(()); - }; - let is_format_on_save_set_to_formatter = format_on_save - .as_str() - .map_or(true, |s| s != "on" && s != "off"); - if !is_format_on_save_set_to_formatter { - return Ok(()); - } - - fn fmt_path(path: &[&str], key: &str) -> String { - let mut path = path.to_vec(); - path.push(key); - path.join(".") - } - - anyhow::ensure!( - obj.get("formatter").is_none(), - r#"Setting formatters in both "format_on_save" and "formatter" is deprecated. Please migrate the formatters from {} into {}"#, - fmt_path(path, "format_on_save"), - fmt_path(path, "formatter") - ); - - obj.insert("format_on_save".to_string(), serde_json::json!("on")); - obj.insert("formatter".to_string(), format_on_save); - - Ok(()) -} diff --git a/crates/migrator/src/migrations/m_2025_10_03/settings.rs b/crates/migrator/src/migrations/m_2025_10_03/settings.rs deleted file mode 100644 index 47d15e8ddf..0000000000 --- a/crates/migrator/src/migrations/m_2025_10_03/settings.rs +++ /dev/null @@ -1,30 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_ROOT_KEY_VALUE_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = - &[(SETTINGS_ROOT_KEY_VALUE_PATTERN, rename_agent_font_size)]; - -/// Renames the setting `agent_font_size` to `agent_ui_font_size` -fn rename_agent_font_size( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let setting_capture_ix = query.capture_index_for_name("name")?; - - let setting_name_range = mat - .nodes_for_capture_index(setting_capture_ix) - .next()? - .byte_range(); - - let setting_name = contents.get(setting_name_range.clone())?; - - if setting_name != "agent_font_size" { - return None; - } - - Some((setting_name_range, "agent_ui_font_size".to_string())) -} diff --git a/crates/migrator/src/migrations/m_2025_10_16/settings.rs b/crates/migrator/src/migrations/m_2025_10_16/settings.rs deleted file mode 100644 index 3fa8c509b1..0000000000 --- a/crates/migrator/src/migrations/m_2025_10_16/settings.rs +++ /dev/null @@ -1,71 +0,0 @@ -use anyhow::Result; -use serde_json::Value; - -use crate::patterns::migrate_language_setting; - -pub fn restore_code_actions_on_format(value: &mut Value) -> Result<()> { - migrate_language_setting(value, restore_code_actions_on_format_inner) -} - -fn restore_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Result<()> { - let Some(obj) = value.as_object_mut() else { - return Ok(()); - }; - let code_actions_on_format = obj - .get("code_actions_on_format") - .cloned() - .unwrap_or_else(|| Value::Object(Default::default())); - - fn fmt_path(path: &[&str], key: &str) -> String { - let mut path = path.to_vec(); - path.push(key); - path.join(".") - } - - let Some(mut code_actions_map) = code_actions_on_format.as_object().cloned() else { - anyhow::bail!( - r#"The `code_actions_on_format` is in an invalid state and cannot be migrated at {}. Please ensure the code_actions_on_format setting is a Map"#, - fmt_path(path, "code_actions_on_format"), - ); - }; - - let Some(formatter) = obj.get("formatter") else { - return Ok(()); - }; - let formatter_array = if let Some(array) = formatter.as_array() { - array.clone() - } else { - vec![formatter.clone()] - }; - if formatter_array.is_empty() { - return Ok(()); - } - let mut code_action_formatters = Vec::new(); - for formatter in formatter_array { - let Some(code_action) = formatter.get("code_action") else { - return Ok(()); - }; - let Some(code_action_name) = code_action.as_str() else { - anyhow::bail!( - r#"The `code_action` is in an invalid state and cannot be migrated at {}. Please ensure the code_action setting is a String"#, - fmt_path(path, "formatter"), - ); - }; - code_action_formatters.push(code_action_name.to_string()); - } - - code_actions_map.extend( - code_action_formatters - .into_iter() - .rev() - .map(|code_action| (code_action, Value::Bool(true))), - ); - - obj.insert("formatter".to_string(), Value::Array(vec![])); - obj.insert( - "code_actions_on_format".into(), - Value::Object(code_actions_map), - ); - - Ok(()) -} diff --git a/crates/migrator/src/migrations/m_2025_10_17/settings.rs b/crates/migrator/src/migrations/m_2025_10_17/settings.rs deleted file mode 100644 index 519ec74034..0000000000 --- a/crates/migrator/src/migrations/m_2025_10_17/settings.rs +++ /dev/null @@ -1,24 +0,0 @@ -use anyhow::Result; -use serde_json::Value; - -pub fn make_file_finder_include_ignored_an_enum(value: &mut Value) -> Result<()> { - let Some(file_finder) = value.get_mut("file_finder") else { - return Ok(()); - }; - - let Some(file_finder_obj) = file_finder.as_object_mut() else { - anyhow::bail!("Expected file_finder to be an object"); - }; - - let Some(include_ignored) = file_finder_obj.get_mut("include_ignored") else { - return Ok(()); - }; - *include_ignored = match include_ignored { - Value::Bool(true) => Value::String("all".to_string()), - Value::Bool(false) => Value::String("indexed".to_string()), - Value::Null => Value::String("smart".to_string()), - Value::String(s) if s == "all" || s == "indexed" || s == "smart" => return Ok(()), - _ => anyhow::bail!("Expected include_ignored to be a boolean or null"), - }; - Ok(()) -} diff --git a/crates/migrator/src/migrations/m_2025_10_21/settings.rs b/crates/migrator/src/migrations/m_2025_10_21/settings.rs deleted file mode 100644 index 1f78f93327..0000000000 --- a/crates/migrator/src/migrations/m_2025_10_21/settings.rs +++ /dev/null @@ -1,16 +0,0 @@ -use anyhow::Result; -use serde_json::Value; - -pub fn make_relative_line_numbers_an_enum(value: &mut Value) -> Result<()> { - let Some(relative_line_numbers) = value.get_mut("relative_line_numbers") else { - return Ok(()); - }; - - *relative_line_numbers = match relative_line_numbers { - Value::Bool(true) => Value::String("enabled".to_string()), - Value::Bool(false) => Value::String("disabled".to_string()), - Value::String(s) if s == "enabled" || s == "disabled" || s == "wrapped" => return Ok(()), - _ => anyhow::bail!("Expected relative_line_numbers to be a boolean"), - }; - Ok(()) -} diff --git a/crates/migrator/src/migrations/m_2025_11_12/settings.rs b/crates/migrator/src/migrations/m_2025_11_12/settings.rs deleted file mode 100644 index 6483f9e44b..0000000000 --- a/crates/migrator/src/migrations/m_2025_11_12/settings.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[ - ( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - rename_open_file_on_paste_setting, - ), - ( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - replace_open_file_on_paste_setting_value, - ), -]; - -fn rename_open_file_on_paste_setting( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - if !is_project_panel_open_file_on_paste(contents, mat, query) { - return None; - } - - 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(); - - Some((setting_name_range, "auto_open".to_string())) -} - -fn replace_open_file_on_paste_setting_value( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - if !is_project_panel_open_file_on_paste(contents, mat, query) { - return None; - } - - let value_ix = query.capture_index_for_name("setting_value")?; - let value_node = mat.nodes_for_capture_index(value_ix).next()?; - let value_range = value_node.byte_range(); - let value_text = contents.get(value_range.clone())?.trim(); - - let normalized_value = match value_text { - "true" => "true", - "false" => "false", - _ => return None, - }; - - Some(( - value_range, - format!("{{ \"on_paste\": {normalized_value} }}"), - )) -} - -fn is_project_panel_open_file_on_paste(contents: &str, mat: &QueryMatch, query: &Query) -> bool { - let parent_key_ix = match query.capture_index_for_name("parent_key") { - Some(ix) => ix, - None => return false, - }; - let parent_range = match mat.nodes_for_capture_index(parent_key_ix).next() { - Some(node) => node.byte_range(), - None => return false, - }; - if contents.get(parent_range) != Some("project_panel") { - return false; - } - - let setting_name_ix = match query.capture_index_for_name("setting_name") { - Some(ix) => ix, - None => return false, - }; - let setting_name_range = match mat.nodes_for_capture_index(setting_name_ix).next() { - Some(node) => node.byte_range(), - None => return false, - }; - contents.get(setting_name_range) == Some("open_file_on_paste") -} diff --git a/crates/migrator/src/migrations/m_2025_11_20/settings.rs b/crates/migrator/src/migrations/m_2025_11_20/settings.rs deleted file mode 100644 index db56fb04d4..0000000000 --- a/crates/migrator/src/migrations/m_2025_11_20/settings.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::ops::Range; - -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[( - SETTINGS_AGENT_SERVERS_CUSTOM_PATTERN, - migrate_custom_agent_settings, -)]; - -const SETTINGS_AGENT_SERVERS_CUSTOM_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @agent-servers) - value: (object - (pair - key: (string (string_content) @server-name) - value: (object) @server-settings - ) - ) - ) - ) - (#eq? @agent-servers "agent_servers") -)"#; - -fn migrate_custom_agent_settings( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let server_name_index = query.capture_index_for_name("server-name")?; - let server_name = mat.nodes_for_capture_index(server_name_index).next()?; - let server_name_text = &contents[server_name.byte_range()]; - - if matches!(server_name_text, "gemini" | "claude" | "codex") { - return None; - } - - let server_settings_index = query.capture_index_for_name("server-settings")?; - let server_settings = mat.nodes_for_capture_index(server_settings_index).next()?; - - let mut column = None; - - // Parse the server settings to check what keys it contains - let mut cursor = server_settings.walk(); - for child in server_settings.children(&mut cursor) { - if child.kind() == "pair" { - if let Some(key_node) = child.child_by_field_name("key") { - if let (None, Some(quote_content)) = (column, key_node.child(0)) { - column = Some(quote_content.start_position().column); - } - if let Some(string_content) = key_node.child(1) { - let key = &contents[string_content.byte_range()]; - match key { - // If it already has a type key, don't modify it - "type" => return None, - _ => {} - } - } - } - } - } - - // Insert the type key at the beginning of the object - let start = server_settings.start_byte() + 1; - let indent = " ".repeat(column.unwrap_or(12)); - - Some(( - start..start, - format!( - r#" -{indent}"type": "custom","# - ), - )) -} diff --git a/crates/migrator/src/migrations/m_2025_11_25/settings.rs b/crates/migrator/src/migrations/m_2025_11_25/settings.rs deleted file mode 100644 index 944eee8a11..0000000000 --- a/crates/migrator/src/migrations/m_2025_11_25/settings.rs +++ /dev/null @@ -1,17 +0,0 @@ -use anyhow::Result; -use serde_json::Value; - -pub fn remove_context_server_source(settings: &mut Value) -> Result<()> { - if let Some(obj) = settings.as_object_mut() { - if let Some(context_servers) = obj.get_mut("context_servers") { - if let Some(servers) = context_servers.as_object_mut() { - for (_, server) in servers.iter_mut() { - if let Some(server_obj) = server.as_object_mut() { - server_obj.remove("source"); - } - } - } - } - } - Ok(()) -} diff --git a/crates/migrator/src/migrations/m_2025_12_01/settings.rs b/crates/migrator/src/migrations/m_2025_12_01/settings.rs deleted file mode 100644 index 2c3816dab3..0000000000 --- a/crates/migrator/src/migrations/m_2025_12_01/settings.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - rename_enable_preview_from_code_navigation_setting, -)]; - -fn rename_enable_preview_from_code_navigation_setting( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - if !is_enable_preview_from_code_navigation(contents, mat, query) { - return None; - } - - 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(); - - Some(( - setting_name_range, - "enable_keep_preview_on_code_navigation".to_string(), - )) -} - -fn is_enable_preview_from_code_navigation(contents: &str, mat: &QueryMatch, query: &Query) -> bool { - let parent_key_ix = match query.capture_index_for_name("parent_key") { - Some(ix) => ix, - None => return false, - }; - let parent_range = match mat.nodes_for_capture_index(parent_key_ix).next() { - Some(node) => node.byte_range(), - None => return false, - }; - if contents.get(parent_range) != Some("preview_tabs") { - return false; - } - - let setting_name_ix = match query.capture_index_for_name("setting_name") { - Some(ix) => ix, - None => return false, - }; - let setting_name_range = match mat.nodes_for_capture_index(setting_name_ix).next() { - Some(node) => node.byte_range(), - None => return false, - }; - contents.get(setting_name_range) == Some("enable_preview_from_code_navigation") -} diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs deleted file mode 100644 index 9fb6d8a115..0000000000 --- a/crates/migrator/src/migrator.rs +++ /dev/null @@ -1,2418 +0,0 @@ -//! ## When to create a migration and why? -//! A migration is necessary when keymap actions or settings are renamed or transformed (e.g., from an array to a string, a string to an array, a boolean to an enum, etc.). -//! -//! This ensures that users with outdated settings are automatically updated to use the corresponding new settings internally. -//! It also provides a quick way to migrate their existing settings to the latest state using button in UI. -//! -//! ## How to create a migration? -//! Migrations use Tree-sitter to query commonly used patterns, such as actions with a string or actions with an array where the second argument is an object, etc. -//! Once queried, *you can filter out the modified items* and write the replacement logic. -//! -//! You *must not* modify previous migrations; always create new ones instead. -//! This is important because if a user is in an intermediate state, they can smoothly transition to the latest state. -//! Modifying existing migrations means they will only work for users upgrading from version x-1 to x, but not from x-2 to x, and so on, where x is the latest version. -//! -//! You only need to write replacement logic for x-1 to x because you can be certain that, internally, every user will be at x-1, regardless of their on disk state. - -use anyhow::{Context as _, Result}; -use settings_json::{infer_json_indent_size, parse_json_with_comments, update_value_in_json_text}; -use std::{cmp::Reverse, ops::Range, sync::LazyLock}; -use streaming_iterator::StreamingIterator; -use tree_sitter::{Query, QueryMatch}; - -use patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN; - -mod migrations; -mod patterns; - -fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result> { - let mut parser = tree_sitter::Parser::new(); - parser.set_language(&tree_sitter_json::LANGUAGE.into())?; - let syntax_tree = parser - .parse(text, None) - .context("failed to parse settings")?; - - let mut cursor = tree_sitter::QueryCursor::new(); - let mut matches = cursor.matches(query, syntax_tree.root_node(), text.as_bytes()); - - let mut edits = vec![]; - while let Some(mat) = matches.next() { - if let Some((_, callback)) = patterns.get(mat.pattern_index) { - edits.extend(callback(text, mat, query)); - } - } - - edits.sort_by_key(|(range, _)| (range.start, Reverse(range.end))); - edits.dedup_by(|(range_b, _), (range_a, _)| { - range_a.contains(&range_b.start) || range_a.contains(&range_b.end) - }); - - if edits.is_empty() { - Ok(None) - } else { - let mut new_text = text.to_string(); - for (range, replacement) in edits.iter().rev() { - new_text.replace_range(range.clone(), replacement); - } - if new_text == text { - log::error!( - "Edits computed for configuration migration do not cause a change: {:?}", - edits - ); - Ok(None) - } else { - Ok(Some(new_text)) - } - } -} - -/// Runs the provided migrations on the given text. -/// Will automatically return `Ok(None)` if there's no content to migrate. -fn run_migrations(text: &str, migrations: &[MigrationType]) -> Result> { - if text.is_empty() { - return Ok(None); - } - - let mut current_text = text.to_string(); - let mut result: Option = None; - let json_indent_size = infer_json_indent_size(¤t_text); - for migration in migrations.iter() { - let migrated_text = match migration { - MigrationType::TreeSitter(patterns, query) => migrate(¤t_text, patterns, query)?, - MigrationType::Json(callback) => { - if current_text.trim().is_empty() { - return Ok(None); - } - let old_content: serde_json_lenient::Value = - parse_json_with_comments(¤t_text)?; - let old_value = serde_json::to_value(&old_content).unwrap(); - let mut new_value = old_value.clone(); - callback(&mut new_value)?; - if new_value != old_value { - let mut current = current_text.clone(); - let mut edits = vec![]; - update_value_in_json_text( - &mut current, - &mut vec![], - json_indent_size, - &old_value, - &new_value, - &mut edits, - ); - let mut migrated_text = current_text.clone(); - for (range, replacement) in edits.into_iter() { - migrated_text.replace_range(range, &replacement); - } - Some(migrated_text) - } else { - None - } - } - }; - if let Some(migrated_text) = migrated_text { - current_text = migrated_text.clone(); - result = Some(migrated_text); - } - } - Ok(result.filter(|new_text| text != new_text)) -} - -pub fn migrate_keymap(text: &str) -> Result> { - let migrations: &[MigrationType] = &[ - MigrationType::TreeSitter( - migrations::m_2025_01_29::KEYMAP_PATTERNS, - &KEYMAP_QUERY_2025_01_29, - ), - MigrationType::TreeSitter( - migrations::m_2025_01_30::KEYMAP_PATTERNS, - &KEYMAP_QUERY_2025_01_30, - ), - MigrationType::TreeSitter( - migrations::m_2025_03_03::KEYMAP_PATTERNS, - &KEYMAP_QUERY_2025_03_03, - ), - MigrationType::TreeSitter( - migrations::m_2025_03_06::KEYMAP_PATTERNS, - &KEYMAP_QUERY_2025_03_06, - ), - MigrationType::TreeSitter( - migrations::m_2025_04_15::KEYMAP_PATTERNS, - &KEYMAP_QUERY_2025_04_15, - ), - ]; - run_migrations(text, migrations) -} - -enum MigrationType<'a> { - TreeSitter(MigrationPatterns, &'a Query), - Json(fn(&mut serde_json::Value) -> Result<()>), -} - -pub fn migrate_settings(text: &str) -> Result> { - let migrations: &[MigrationType] = &[ - MigrationType::TreeSitter( - migrations::m_2025_01_02::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_01_02, - ), - MigrationType::TreeSitter( - migrations::m_2025_01_29::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_01_29, - ), - MigrationType::TreeSitter( - migrations::m_2025_01_30::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_01_30, - ), - MigrationType::TreeSitter( - migrations::m_2025_03_29::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_03_29, - ), - MigrationType::TreeSitter( - migrations::m_2025_04_15::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_04_15, - ), - MigrationType::TreeSitter( - migrations::m_2025_04_21::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_04_21, - ), - MigrationType::TreeSitter( - migrations::m_2025_04_23::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_04_23, - ), - MigrationType::TreeSitter( - migrations::m_2025_05_05::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_05_05, - ), - MigrationType::TreeSitter( - migrations::m_2025_05_08::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_05_08, - ), - MigrationType::TreeSitter( - migrations::m_2025_05_29::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_05_29, - ), - MigrationType::TreeSitter( - migrations::m_2025_06_16::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_06_16, - ), - MigrationType::TreeSitter( - migrations::m_2025_06_25::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_06_25, - ), - MigrationType::TreeSitter( - migrations::m_2025_06_27::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_06_27, - ), - MigrationType::TreeSitter( - migrations::m_2025_07_08::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_07_08, - ), - MigrationType::Json(migrations::m_2025_10_01::flatten_code_actions_formatters), - MigrationType::Json(migrations::m_2025_10_02::remove_formatters_on_save), - MigrationType::TreeSitter( - migrations::m_2025_10_03::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_10_03, - ), - MigrationType::Json(migrations::m_2025_10_16::restore_code_actions_on_format), - MigrationType::Json(migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum), - MigrationType::Json(migrations::m_2025_10_21::make_relative_line_numbers_an_enum), - MigrationType::TreeSitter( - migrations::m_2025_11_12::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_11_12, - ), - MigrationType::TreeSitter( - migrations::m_2025_12_01::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_12_01, - ), - MigrationType::TreeSitter( - migrations::m_2025_11_20::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_11_20, - ), - MigrationType::Json(migrations::m_2025_11_25::remove_context_server_source), - ]; - run_migrations(text, migrations) -} - -pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result> { - migrate( - text, - &[( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - migrations::m_2025_01_29::replace_edit_prediction_provider_setting, - )], - &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY, - ) -} - -pub type MigrationPatterns = &'static [( - &'static str, - fn(&str, &QueryMatch, &Query) -> Option<(Range, String)>, -)]; - -macro_rules! define_query { - ($var_name:ident, $patterns_path:path) => { - static $var_name: LazyLock = LazyLock::new(|| { - Query::new( - &tree_sitter_json::LANGUAGE.into(), - &$patterns_path - .iter() - .map(|pattern| pattern.0) - .collect::(), - ) - .unwrap() - }); - }; -} - -// keymap -define_query!( - KEYMAP_QUERY_2025_01_29, - migrations::m_2025_01_29::KEYMAP_PATTERNS -); -define_query!( - KEYMAP_QUERY_2025_01_30, - migrations::m_2025_01_30::KEYMAP_PATTERNS -); -define_query!( - KEYMAP_QUERY_2025_03_03, - migrations::m_2025_03_03::KEYMAP_PATTERNS -); -define_query!( - KEYMAP_QUERY_2025_03_06, - migrations::m_2025_03_06::KEYMAP_PATTERNS -); -define_query!( - KEYMAP_QUERY_2025_04_15, - migrations::m_2025_04_15::KEYMAP_PATTERNS -); - -// settings -define_query!( - SETTINGS_QUERY_2025_01_02, - migrations::m_2025_01_02::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_01_29, - migrations::m_2025_01_29::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_01_30, - migrations::m_2025_01_30::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_03_29, - migrations::m_2025_03_29::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_04_15, - migrations::m_2025_04_15::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_04_21, - migrations::m_2025_04_21::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_04_23, - migrations::m_2025_04_23::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_05_05, - migrations::m_2025_05_05::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_05_08, - migrations::m_2025_05_08::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_05_29, - migrations::m_2025_05_29::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_06_16, - migrations::m_2025_06_16::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_06_25, - migrations::m_2025_06_25::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_06_27, - migrations::m_2025_06_27::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_07_08, - migrations::m_2025_07_08::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_10_03, - migrations::m_2025_10_03::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_11_12, - migrations::m_2025_11_12::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_12_01, - migrations::m_2025_12_01::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_11_20, - migrations::m_2025_11_20::SETTINGS_PATTERNS -); - -// custom query -static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock = LazyLock::new(|| { - Query::new( - &tree_sitter_json::LANGUAGE.into(), - SETTINGS_NESTED_KEY_VALUE_PATTERN, - ) - .unwrap() -}); - -#[cfg(test)] -mod tests { - use super::*; - use unindent::Unindent as _; - - #[track_caller] - fn assert_migrated_correctly(migrated: Option, expected: Option<&str>) { - match (&migrated, &expected) { - (Some(migrated), Some(expected)) => { - pretty_assertions::assert_str_eq!(expected, migrated); - } - _ => { - pretty_assertions::assert_eq!(migrated.as_deref(), expected); - } - } - } - - fn assert_migrate_keymap(input: &str, output: Option<&str>) { - let migrated = migrate_keymap(input).unwrap(); - pretty_assertions::assert_eq!(migrated.as_deref(), output); - } - - #[track_caller] - fn assert_migrate_settings(input: &str, output: Option<&str>) { - let migrated = migrate_settings(input).unwrap(); - assert_migrated_correctly(migrated.clone(), output); - - // expect that rerunning the migration does not result in another migration - if let Some(migrated) = migrated { - let rerun = migrate_settings(&migrated).unwrap(); - assert_migrated_correctly(rerun, None); - } - } - - #[track_caller] - fn assert_migrate_settings_with_migrations( - migrations: &[MigrationType], - input: &str, - output: Option<&str>, - ) { - let migrated = run_migrations(input, migrations).unwrap(); - assert_migrated_correctly(migrated.clone(), output); - - // expect that rerunning the migration does not result in another migration - if let Some(migrated) = migrated { - let rerun = run_migrations(&migrated, migrations).unwrap(); - assert_migrated_correctly(rerun, None); - } - } - - #[test] - fn test_empty_content() { - assert_migrate_settings("", None) - } - - #[test] - fn test_replace_array_with_single_string() { - assert_migrate_keymap( - r#" - [ - { - "bindings": { - "cmd-1": ["workspace::ActivatePaneInDirection", "Up"] - } - } - ] - "#, - Some( - r#" - [ - { - "bindings": { - "cmd-1": "workspace::ActivatePaneUp" - } - } - ] - "#, - ), - ) - } - - #[test] - fn test_replace_action_argument_object_with_single_value() { - assert_migrate_keymap( - r#" - [ - { - "bindings": { - "cmd-1": ["editor::FoldAtLevel", { "level": 1 }] - } - } - ] - "#, - Some( - r#" - [ - { - "bindings": { - "cmd-1": ["editor::FoldAtLevel", 1] - } - } - ] - "#, - ), - ) - } - - #[test] - fn test_replace_action_argument_object_with_single_value_2() { - assert_migrate_keymap( - r#" - [ - { - "bindings": { - "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }] - } - } - ] - "#, - Some( - r#" - [ - { - "bindings": { - "cmd-1": ["vim::PushObject", { "some" : "value" }] - } - } - ] - "#, - ), - ) - } - - #[test] - fn test_rename_string_action() { - assert_migrate_keymap( - r#" - [ - { - "bindings": { - "cmd-1": "inline_completion::ToggleMenu" - } - } - ] - "#, - Some( - r#" - [ - { - "bindings": { - "cmd-1": "edit_prediction::ToggleMenu" - } - } - ] - "#, - ), - ) - } - - #[test] - fn test_rename_context_key() { - assert_migrate_keymap( - r#" - [ - { - "context": "Editor && inline_completion && !showing_completions" - } - ] - "#, - Some( - r#" - [ - { - "context": "Editor && edit_prediction && !showing_completions" - } - ] - "#, - ), - ) - } - - #[test] - fn test_incremental_migrations() { - // Here string transforms to array internally. Then, that array transforms back to string. - assert_migrate_keymap( - r#" - [ - { - "bindings": { - "ctrl-q": "editor::GoToHunk", // should remain same - "ctrl-w": "editor::GoToPrevHunk", // should rename - "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }], // should transform - "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }] // should transform - } - } - ] - "#, - Some( - r#" - [ - { - "bindings": { - "ctrl-q": "editor::GoToHunk", // should remain same - "ctrl-w": "editor::GoToPreviousHunk", // should rename - "ctrl-q": "editor::GoToHunk", // should transform - "ctrl-w": "editor::GoToPreviousHunk" // should transform - } - } - ] - "#, - ), - ) - } - - #[test] - fn test_action_argument_snake_case() { - // First performs transformations, then replacements - assert_migrate_keymap( - r#" - [ - { - "bindings": { - "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }], - "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }], - "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }], - "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is - } - } - ] - "#, - Some( - r#" - [ - { - "bindings": { - "cmd-1": ["vim::PushObject", { "around": false }], - "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }], - "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }], - "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is - } - } - ] - "#, - ), - ) - } - - #[test] - fn test_replace_setting_name() { - assert_migrate_settings( - r#" - { - "show_inline_completions_in_menu": true, - "show_inline_completions": true, - "inline_completions_disabled_in": ["string"], - "inline_completions": { "some" : "value" } - } - "#, - Some( - r#" - { - "show_edit_predictions_in_menu": true, - "show_edit_predictions": true, - "edit_predictions_disabled_in": ["string"], - "edit_predictions": { "some" : "value" } - } - "#, - ), - ) - } - - #[test] - fn test_nested_string_replace_for_settings() { - assert_migrate_settings( - r#" - { - "features": { - "inline_completion_provider": "zed" - }, - } - "#, - Some( - r#" - { - "features": { - "edit_prediction_provider": "zed" - }, - } - "#, - ), - ) - } - - #[test] - fn test_replace_settings_in_languages() { - assert_migrate_settings( - r#" - { - "languages": { - "Astro": { - "show_inline_completions": true - } - } - } - "#, - Some( - r#" - { - "languages": { - "Astro": { - "show_edit_predictions": true - } - } - } - "#, - ), - ) - } - - #[test] - fn test_replace_settings_value() { - assert_migrate_settings( - r#" - { - "scrollbar": { - "diagnostics": true - }, - "chat_panel": { - "button": true - } - } - "#, - Some( - r#" - { - "scrollbar": { - "diagnostics": "all" - }, - "chat_panel": { - "button": "always" - } - } - "#, - ), - ) - } - - #[test] - fn test_replace_settings_name_and_value() { - assert_migrate_settings( - r#" - { - "tabs": { - "always_show_close_button": true - } - } - "#, - Some( - r#" - { - "tabs": { - "show_close_button": "always" - } - } - "#, - ), - ) - } - - #[test] - fn test_replace_bash_with_terminal_in_profiles() { - assert_migrate_settings( - r#" - { - "assistant": { - "profiles": { - "custom": { - "name": "Custom", - "tools": { - "bash": true, - "diagnostics": true - } - } - } - } - } - "#, - Some( - r#" - { - "agent": { - "profiles": { - "custom": { - "name": "Custom", - "tools": { - "terminal": true, - "diagnostics": true - } - } - } - } - } - "#, - ), - ) - } - - #[test] - fn test_replace_bash_false_with_terminal_in_profiles() { - assert_migrate_settings( - r#" - { - "assistant": { - "profiles": { - "custom": { - "name": "Custom", - "tools": { - "bash": false, - "diagnostics": true - } - } - } - } - } - "#, - Some( - r#" - { - "agent": { - "profiles": { - "custom": { - "name": "Custom", - "tools": { - "terminal": false, - "diagnostics": true - } - } - } - } - } - "#, - ), - ) - } - - #[test] - fn test_no_bash_in_profiles() { - assert_migrate_settings( - r#" - { - "assistant": { - "profiles": { - "custom": { - "name": "Custom", - "tools": { - "diagnostics": true, - "find_path": true, - "read_file": true - } - } - } - } - } - "#, - Some( - r#" - { - "agent": { - "profiles": { - "custom": { - "name": "Custom", - "tools": { - "diagnostics": true, - "find_path": true, - "read_file": true - } - } - } - } - } - "#, - ), - ) - } - - #[test] - fn test_rename_path_search_to_find_path() { - assert_migrate_settings( - r#" - { - "assistant": { - "profiles": { - "default": { - "tools": { - "path_search": true, - "read_file": true - } - } - } - } - } - "#, - Some( - r#" - { - "agent": { - "profiles": { - "default": { - "tools": { - "find_path": true, - "read_file": true - } - } - } - } - } - "#, - ), - ); - } - - #[test] - fn test_rename_assistant() { - assert_migrate_settings( - r#"{ - "assistant": { - "foo": "bar" - }, - "edit_predictions": { - "enabled_in_assistant": false, - } - }"#, - Some( - r#"{ - "agent": { - "foo": "bar" - }, - "edit_predictions": { - "enabled_in_text_threads": false, - } - }"#, - ), - ); - } - - #[test] - fn test_comment_duplicated_agent() { - assert_migrate_settings( - r#"{ - "agent": { - "name": "assistant-1", - "model": "gpt-4", // weird formatting - "utf8": "привіт" - }, - "something": "else", - "agent": { - "name": "assistant-2", - "model": "gemini-pro" - } - } - "#, - Some( - r#"{ - /* Duplicated key auto-commented: "agent": { - "name": "assistant-1", - "model": "gpt-4", // weird formatting - "utf8": "привіт" - }, */ - "something": "else", - "agent": { - "name": "assistant-2", - "model": "gemini-pro" - } - } - "#, - ), - ); - } - - #[test] - fn test_preferred_completion_mode_migration() { - assert_migrate_settings( - r#"{ - "agent": { - "preferred_completion_mode": "max", - "enabled": true - } - }"#, - Some( - r#"{ - "agent": { - "preferred_completion_mode": "burn", - "enabled": true - } - }"#, - ), - ); - - assert_migrate_settings( - r#"{ - "agent": { - "preferred_completion_mode": "normal", - "enabled": true - } - }"#, - None, - ); - - assert_migrate_settings( - r#"{ - "agent": { - "preferred_completion_mode": "burn", - "enabled": true - } - }"#, - None, - ); - - assert_migrate_settings( - r#"{ - "other_section": { - "preferred_completion_mode": "max" - }, - "agent": { - "preferred_completion_mode": "max" - } - }"#, - Some( - r#"{ - "other_section": { - "preferred_completion_mode": "max" - }, - "agent": { - "preferred_completion_mode": "burn" - } - }"#, - ), - ); - } - - #[test] - fn test_mcp_settings_migration() { - assert_migrate_settings_with_migrations( - &[MigrationType::TreeSitter( - migrations::m_2025_06_16::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_06_16, - )], - r#"{ - "context_servers": { - "empty_server": {}, - "extension_server": { - "settings": { - "foo": "bar" - } - }, - "custom_server": { - "command": { - "path": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - } - }, - "invalid_server": { - "command": { - "path": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - }, - "settings": { - "foo": "bar" - } - }, - "empty_server2": {}, - "extension_server2": { - "foo": "bar", - "settings": { - "foo": "bar" - }, - "bar": "foo" - }, - "custom_server2": { - "foo": "bar", - "command": { - "path": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - }, - "bar": "foo" - }, - "invalid_server2": { - "foo": "bar", - "command": { - "path": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - }, - "bar": "foo", - "settings": { - "foo": "bar" - } - } - } -}"#, - Some( - r#"{ - "context_servers": { - "empty_server": { - "source": "extension", - "settings": {} - }, - "extension_server": { - "source": "extension", - "settings": { - "foo": "bar" - } - }, - "custom_server": { - "source": "custom", - "command": { - "path": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - } - }, - "invalid_server": { - "source": "custom", - "command": { - "path": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - }, - "settings": { - "foo": "bar" - } - }, - "empty_server2": { - "source": "extension", - "settings": {} - }, - "extension_server2": { - "source": "extension", - "foo": "bar", - "settings": { - "foo": "bar" - }, - "bar": "foo" - }, - "custom_server2": { - "source": "custom", - "foo": "bar", - "command": { - "path": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - }, - "bar": "foo" - }, - "invalid_server2": { - "source": "custom", - "foo": "bar", - "command": { - "path": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - }, - "bar": "foo", - "settings": { - "foo": "bar" - } - } - } -}"#, - ), - ); - } - - #[test] - fn test_mcp_settings_migration_doesnt_change_valid_settings() { - let settings = r#"{ - "context_servers": { - "empty_server": { - "source": "extension", - "settings": {} - }, - "extension_server": { - "source": "extension", - "settings": { - "foo": "bar" - } - }, - "custom_server": { - "source": "custom", - "command": { - "path": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - } - }, - "invalid_server": { - "source": "custom", - "command": { - "path": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - }, - "settings": { - "foo": "bar" - } - } - } -}"#; - assert_migrate_settings_with_migrations( - &[MigrationType::TreeSitter( - migrations::m_2025_06_16::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_06_16, - )], - settings, - None, - ); - } - - #[test] - fn test_custom_agent_server_settings_migration() { - assert_migrate_settings_with_migrations( - &[MigrationType::TreeSitter( - migrations::m_2025_11_20::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_11_20, - )], - r#"{ - "agent_servers": { - "gemini": { - "default_model": "gemini-1.5-pro" - }, - "claude": {}, - "codex": {}, - "my-custom-agent": { - "command": "/path/to/agent", - "args": ["--foo"], - "default_model": "my-model" - }, - "already-migrated-agent": { - "type": "custom", - "command": "/path/to/agent" - }, - "future-extension-agent": { - "type": "extension", - "default_model": "ext-model" - } - } -}"#, - Some( - r#"{ - "agent_servers": { - "gemini": { - "default_model": "gemini-1.5-pro" - }, - "claude": {}, - "codex": {}, - "my-custom-agent": { - "type": "custom", - "command": "/path/to/agent", - "args": ["--foo"], - "default_model": "my-model" - }, - "already-migrated-agent": { - "type": "custom", - "command": "/path/to/agent" - }, - "future-extension-agent": { - "type": "extension", - "default_model": "ext-model" - } - } -}"#, - ), - ); - } - - #[test] - fn test_remove_version_fields() { - assert_migrate_settings( - r#"{ - "language_models": { - "anthropic": { - "version": "1", - "api_url": "https://api.anthropic.com" - }, - "openai": { - "version": "1", - "api_url": "https://api.openai.com/v1" - } - }, - "agent": { - "version": "2", - "enabled": true, - "preferred_completion_mode": "normal", - "button": true, - "dock": "right", - "default_width": 640, - "default_height": 320, - "default_model": { - "provider": "zed.dev", - "model": "claude-sonnet-4" - } - } -}"#, - Some( - r#"{ - "language_models": { - "anthropic": { - "api_url": "https://api.anthropic.com" - }, - "openai": { - "api_url": "https://api.openai.com/v1" - } - }, - "agent": { - "enabled": true, - "preferred_completion_mode": "normal", - "button": true, - "dock": "right", - "default_width": 640, - "default_height": 320, - "default_model": { - "provider": "zed.dev", - "model": "claude-sonnet-4" - } - } -}"#, - ), - ); - - // Test that version fields in other contexts are not removed - assert_migrate_settings( - r#"{ - "language_models": { - "other_provider": { - "version": "1", - "api_url": "https://api.example.com" - } - }, - "other_section": { - "version": "1" - } -}"#, - None, - ); - } - - #[test] - fn test_flatten_context_server_command() { - assert_migrate_settings( - r#"{ - "context_servers": { - "some-mcp-server": { - "command": { - "path": "npx", - "args": [ - "-y", - "@supabase/mcp-server-supabase@latest", - "--read-only", - "--project-ref=" - ], - "env": { - "SUPABASE_ACCESS_TOKEN": "" - } - } - } - } -}"#, - Some( - r#"{ - "context_servers": { - "some-mcp-server": { - "command": "npx", - "args": [ - "-y", - "@supabase/mcp-server-supabase@latest", - "--read-only", - "--project-ref=" - ], - "env": { - "SUPABASE_ACCESS_TOKEN": "" - } - } - } -}"#, - ), - ); - - // Test with additional keys in server object - assert_migrate_settings( - r#"{ - "context_servers": { - "server-with-extras": { - "command": { - "path": "/usr/bin/node", - "args": ["server.js"] - }, - "settings": {} - } - } -}"#, - Some( - r#"{ - "context_servers": { - "server-with-extras": { - "command": "/usr/bin/node", - "args": ["server.js"], - "settings": {} - } - } -}"#, - ), - ); - - // Test command without args or env - assert_migrate_settings( - r#"{ - "context_servers": { - "simple-server": { - "command": { - "path": "simple-mcp-server" - } - } - } -}"#, - Some( - r#"{ - "context_servers": { - "simple-server": { - "command": "simple-mcp-server" - } - } -}"#, - ), - ); - } - - #[test] - fn test_flatten_code_action_formatters_basic_array() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_01::flatten_code_actions_formatters, - )], - &r#"{ - "formatter": [ - { - "code_actions": { - "included-1": true, - "included-2": true, - "excluded": false, - } - } - ] - }"# - .unindent(), - Some( - &r#"{ - "formatter": [ - { - "code_action": "included-1" - }, - { - "code_action": "included-2" - } - ] - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_flatten_code_action_formatters_basic_object() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_01::flatten_code_actions_formatters, - )], - &r#"{ - "formatter": { - "code_actions": { - "included-1": true, - "excluded": false, - "included-2": true - } - } - }"# - .unindent(), - Some( - &r#"{ - "formatter": [ - { - "code_action": "included-1" - }, - { - "code_action": "included-2" - } - ] - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_flatten_code_action_formatters_array_with_multiple_action_blocks() { - assert_migrate_settings( - &r#"{ - "formatter": [ - { - "code_actions": { - "included-1": true, - "included-2": true, - "excluded": false, - } - }, - { - "language_server": "ruff" - }, - { - "code_actions": { - "excluded": false, - "excluded-2": false, - } - } - // some comment - , - { - "code_actions": { - "excluded": false, - "included-3": true, - "included-4": true, - } - }, - ] - }"# - .unindent(), - Some( - &r#"{ - "formatter": [ - { - "code_action": "included-1" - }, - { - "code_action": "included-2" - }, - { - "language_server": "ruff" - }, - { - "code_action": "included-3" - }, - { - "code_action": "included-4" - } - ] - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_languages() { - assert_migrate_settings( - &r#"{ - "languages": { - "Rust": { - "formatter": [ - { - "code_actions": { - "included-1": true, - "included-2": true, - "excluded": false, - } - }, - { - "language_server": "ruff" - }, - { - "code_actions": { - "excluded": false, - "excluded-2": false, - } - } - // some comment - , - { - "code_actions": { - "excluded": false, - "included-3": true, - "included-4": true, - } - }, - ] - } - } - }"# - .unindent(), - Some( - &r#"{ - "languages": { - "Rust": { - "formatter": [ - { - "code_action": "included-1" - }, - { - "code_action": "included-2" - }, - { - "language_server": "ruff" - }, - { - "code_action": "included-3" - }, - { - "code_action": "included-4" - } - ] - } - } - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_defaults_and_multiple_languages() - { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_01::flatten_code_actions_formatters, - )], - &r#"{ - "formatter": { - "code_actions": { - "default-1": true, - "default-2": true, - "default-3": true, - "default-4": true, - } - }, - "languages": { - "Rust": { - "formatter": [ - { - "code_actions": { - "included-1": true, - "included-2": true, - "excluded": false, - } - }, - { - "language_server": "ruff" - }, - { - "code_actions": { - "excluded": false, - "excluded-2": false, - } - } - // some comment - , - { - "code_actions": { - "excluded": false, - "included-3": true, - "included-4": true, - } - }, - ] - }, - "Python": { - "formatter": [ - { - "language_server": "ruff" - }, - { - "code_actions": { - "excluded": false, - "excluded-2": false, - } - } - // some comment - , - { - "code_actions": { - "excluded": false, - "included-3": true, - "included-4": true, - } - }, - ] - } - } - }"# - .unindent(), - Some( - &r#"{ - "formatter": [ - { - "code_action": "default-1" - }, - { - "code_action": "default-2" - }, - { - "code_action": "default-3" - }, - { - "code_action": "default-4" - } - ], - "languages": { - "Rust": { - "formatter": [ - { - "code_action": "included-1" - }, - { - "code_action": "included-2" - }, - { - "language_server": "ruff" - }, - { - "code_action": "included-3" - }, - { - "code_action": "included-4" - } - ] - }, - "Python": { - "formatter": [ - { - "language_server": "ruff" - }, - { - "code_action": "included-3" - }, - { - "code_action": "included-4" - } - ] - } - } - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_flatten_code_action_formatters_array_with_format_on_save_and_multiple_languages() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_01::flatten_code_actions_formatters, - )], - &r#"{ - "formatter": { - "code_actions": { - "default-1": true, - "default-2": true, - "default-3": true, - "default-4": true, - } - }, - "format_on_save": [ - { - "code_actions": { - "included-1": true, - "included-2": true, - "excluded": false, - } - }, - { - "language_server": "ruff" - }, - { - "code_actions": { - "excluded": false, - "excluded-2": false, - } - } - // some comment - , - { - "code_actions": { - "excluded": false, - "included-3": true, - "included-4": true, - } - }, - ], - "languages": { - "Rust": { - "format_on_save": "prettier", - "formatter": [ - { - "code_actions": { - "included-1": true, - "included-2": true, - "excluded": false, - } - }, - { - "language_server": "ruff" - }, - { - "code_actions": { - "excluded": false, - "excluded-2": false, - } - } - // some comment - , - { - "code_actions": { - "excluded": false, - "included-3": true, - "included-4": true, - } - }, - ] - }, - "Python": { - "format_on_save": { - "code_actions": { - "on-save-1": true, - "on-save-2": true, - } - }, - "formatter": [ - { - "language_server": "ruff" - }, - { - "code_actions": { - "excluded": false, - "excluded-2": false, - } - } - // some comment - , - { - "code_actions": { - "excluded": false, - "included-3": true, - "included-4": true, - } - }, - ] - } - } - }"# - .unindent(), - Some( - &r#" - { - "formatter": [ - { - "code_action": "default-1" - }, - { - "code_action": "default-2" - }, - { - "code_action": "default-3" - }, - { - "code_action": "default-4" - } - ], - "format_on_save": [ - { - "code_action": "included-1" - }, - { - "code_action": "included-2" - }, - { - "language_server": "ruff" - }, - { - "code_action": "included-3" - }, - { - "code_action": "included-4" - } - ], - "languages": { - "Rust": { - "format_on_save": "prettier", - "formatter": [ - { - "code_action": "included-1" - }, - { - "code_action": "included-2" - }, - { - "language_server": "ruff" - }, - { - "code_action": "included-3" - }, - { - "code_action": "included-4" - } - ] - }, - "Python": { - "format_on_save": [ - { - "code_action": "on-save-1" - }, - { - "code_action": "on-save-2" - } - ], - "formatter": [ - { - "language_server": "ruff" - }, - { - "code_action": "included-3" - }, - { - "code_action": "included-4" - } - ] - } - } - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_format_on_save_formatter_migration_basic() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_02::remove_formatters_on_save, - )], - &r#"{ - "format_on_save": "prettier" - }"# - .unindent(), - Some( - &r#"{ - "formatter": "prettier", - "format_on_save": "on" - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_format_on_save_formatter_migration_array() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_02::remove_formatters_on_save, - )], - &r#"{ - "format_on_save": ["prettier", {"language_server": "eslint"}] - }"# - .unindent(), - Some( - &r#"{ - "formatter": [ - "prettier", - { - "language_server": "eslint" - } - ], - "format_on_save": "on" - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_format_on_save_on_off_unchanged() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_02::remove_formatters_on_save, - )], - &r#"{ - "format_on_save": "on" - }"# - .unindent(), - None, - ); - - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_02::remove_formatters_on_save, - )], - &r#"{ - "format_on_save": "off" - }"# - .unindent(), - None, - ); - } - - #[test] - fn test_format_on_save_formatter_migration_in_languages() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_02::remove_formatters_on_save, - )], - &r#"{ - "languages": { - "Rust": { - "format_on_save": "rust-analyzer" - }, - "Python": { - "format_on_save": ["ruff", "black"] - } - } - }"# - .unindent(), - Some( - &r#"{ - "languages": { - "Rust": { - "formatter": "rust-analyzer", - "format_on_save": "on" - }, - "Python": { - "formatter": [ - "ruff", - "black" - ], - "format_on_save": "on" - } - } - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_format_on_save_formatter_migration_mixed_global_and_languages() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_02::remove_formatters_on_save, - )], - &r#"{ - "format_on_save": "prettier", - "languages": { - "Rust": { - "format_on_save": "rust-analyzer" - }, - "Python": { - "format_on_save": "on" - } - } - }"# - .unindent(), - Some( - &r#"{ - "formatter": "prettier", - "format_on_save": "on", - "languages": { - "Rust": { - "formatter": "rust-analyzer", - "format_on_save": "on" - }, - "Python": { - "format_on_save": "on" - } - } - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_format_on_save_no_migration_when_no_format_on_save() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_02::remove_formatters_on_save, - )], - &r#"{ - "formatter": ["prettier"] - }"# - .unindent(), - None, - ); - } - - #[test] - fn test_restore_code_actions_on_format() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_16::restore_code_actions_on_format, - )], - &r#"{ - "formatter": { - "code_action": "foo" - } - }"# - .unindent(), - Some( - &r#"{ - "code_actions_on_format": { - "foo": true - }, - "formatter": [] - }"# - .unindent(), - ), - ); - - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_16::restore_code_actions_on_format, - )], - &r#"{ - "formatter": [ - { "code_action": "foo" }, - "auto" - ] - }"# - .unindent(), - None, - ); - - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_16::restore_code_actions_on_format, - )], - &r#"{ - "formatter": { - "code_action": "foo" - }, - "code_actions_on_format": { - "bar": true, - "baz": false - } - }"# - .unindent(), - Some( - &r#"{ - "formatter": [], - "code_actions_on_format": { - "foo": true, - "bar": true, - "baz": false - } - }"# - .unindent(), - ), - ); - - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_16::restore_code_actions_on_format, - )], - &r#"{ - "formatter": [ - { "code_action": "foo" }, - { "code_action": "qux" }, - ], - "code_actions_on_format": { - "bar": true, - "baz": false - } - }"# - .unindent(), - Some( - &r#"{ - "formatter": [], - "code_actions_on_format": { - "foo": true, - "qux": true, - "bar": true, - "baz": false - } - }"# - .unindent(), - ), - ); - - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_16::restore_code_actions_on_format, - )], - &r#"{ - "formatter": [], - "code_actions_on_format": { - "bar": true, - "baz": false - } - }"# - .unindent(), - None, - ); - } - - #[test] - fn test_make_file_finder_include_ignored_an_enum() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum, - )], - &r#"{ }"#.unindent(), - None, - ); - - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum, - )], - &r#"{ - "file_finder": { - "include_ignored": true - } - }"# - .unindent(), - Some( - &r#"{ - "file_finder": { - "include_ignored": "all" - } - }"# - .unindent(), - ), - ); - - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum, - )], - &r#"{ - "file_finder": { - "include_ignored": false - } - }"# - .unindent(), - Some( - &r#"{ - "file_finder": { - "include_ignored": "indexed" - } - }"# - .unindent(), - ), - ); - - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum, - )], - &r#"{ - "file_finder": { - "include_ignored": null - } - }"# - .unindent(), - Some( - &r#"{ - "file_finder": { - "include_ignored": "smart" - } - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_remove_context_server_source() { - assert_migrate_settings( - &r#" - { - "context_servers": { - "extension_server": { - "source": "extension", - "settings": { - "foo": "bar" - } - }, - "custom_server": { - "source": "custom", - "command": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - }, - } - } - "# - .unindent(), - Some( - &r#" - { - "context_servers": { - "extension_server": { - "settings": { - "foo": "bar" - } - }, - "custom_server": { - "command": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - }, - } - } - "# - .unindent(), - ), - ); - } - - #[test] - fn test_project_panel_open_file_on_paste_migration() { - assert_migrate_settings( - &r#" - { - "project_panel": { - "open_file_on_paste": true - } - } - "# - .unindent(), - Some( - &r#" - { - "project_panel": { - "auto_open": { "on_paste": true } - } - } - "# - .unindent(), - ), - ); - - assert_migrate_settings( - &r#" - { - "project_panel": { - "open_file_on_paste": false - } - } - "# - .unindent(), - Some( - &r#" - { - "project_panel": { - "auto_open": { "on_paste": false } - } - } - "# - .unindent(), - ), - ); - } - - #[test] - fn test_enable_preview_from_code_navigation_migration() { - assert_migrate_settings( - &r#" - { - "other_setting_1": 1, - "preview_tabs": { - "other_setting_2": 2, - "enable_preview_from_code_navigation": false - } - } - "# - .unindent(), - Some( - &r#" - { - "other_setting_1": 1, - "preview_tabs": { - "other_setting_2": 2, - "enable_keep_preview_on_code_navigation": false - } - } - "# - .unindent(), - ), - ); - - assert_migrate_settings( - &r#" - { - "other_setting_1": 1, - "preview_tabs": { - "other_setting_2": 2, - "enable_preview_from_code_navigation": true - } - } - "# - .unindent(), - Some( - &r#" - { - "other_setting_1": 1, - "preview_tabs": { - "other_setting_2": 2, - "enable_keep_preview_on_code_navigation": true - } - } - "# - .unindent(), - ), - ); - } -} diff --git a/crates/migrator/src/patterns.rs b/crates/migrator/src/patterns.rs deleted file mode 100644 index 4132c93d93..0000000000 --- a/crates/migrator/src/patterns.rs +++ /dev/null @@ -1,14 +0,0 @@ -mod keymap; -mod settings; - -pub(crate) use keymap::{ - KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN, KEYMAP_ACTION_ARRAY_PATTERN, - KEYMAP_ACTION_STRING_PATTERN, KEYMAP_CONTEXT_PATTERN, -}; - -pub(crate) use settings::{ - SETTINGS_ASSISTANT_PATTERN, SETTINGS_ASSISTANT_TOOLS_PATTERN, - SETTINGS_DUPLICATED_AGENT_PATTERN, SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN, - SETTINGS_LANGUAGES_PATTERN, SETTINGS_NESTED_KEY_VALUE_PATTERN, SETTINGS_ROOT_KEY_VALUE_PATTERN, - migrate_language_setting, -}; diff --git a/crates/migrator/src/patterns/keymap.rs b/crates/migrator/src/patterns/keymap.rs deleted file mode 100644 index 439c10fd4a..0000000000 --- a/crates/migrator/src/patterns/keymap.rs +++ /dev/null @@ -1,77 +0,0 @@ -pub const KEYMAP_ACTION_ARRAY_PATTERN: &str = r#"(document - (array - (object - (pair - key: (string (string_content) @name) - value: ( - (object - (pair - key: (string) - value: ((array - . (string (string_content) @action_name) - . (string (string_content) @argument) - .)) @array - ) - ) - ) - ) - ) - ) - (#eq? @name "bindings") -)"#; - -pub const KEYMAP_ACTION_STRING_PATTERN: &str = r#"(document - (array - (object - (pair - key: (string (string_content) @name) - value: ( - (object - (pair - key: (string) - value: (string (string_content) @action_name) - ) - ) - ) - ) - ) - ) - (#eq? @name "bindings") -)"#; - -pub const KEYMAP_CONTEXT_PATTERN: &str = r#"(document - (array - (object - (pair - key: (string (string_content) @name) - value: (string (string_content) @context_predicate) - ) - ) - ) - (#eq? @name "context") -)"#; - -pub const KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN: &str = r#"(document - (array - (object - (pair - key: (string (string_content) @name) - value: ( - (object - (pair - key: (string) - value: ((array - . (string (string_content) @action_name) - . (object - (pair - key: (string (string_content) @argument_key) - value: (_) @argument_value)) - . ) @array - )) - ) - ) - ) - ) - ) - (#eq? @name "bindings") -)"#; diff --git a/crates/migrator/src/patterns/settings.rs b/crates/migrator/src/patterns/settings.rs deleted file mode 100644 index a068cce23b..0000000000 --- a/crates/migrator/src/patterns/settings.rs +++ /dev/null @@ -1,131 +0,0 @@ -pub const SETTINGS_ROOT_KEY_VALUE_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @name) - value: (_) @value - ) - ) -)"#; - -pub const SETTINGS_NESTED_KEY_VALUE_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @parent_key) - value: (object - (pair - key: (string (string_content) @setting_name) - value: (_) @setting_value - ) - ) - ) - ) -)"#; - -pub const SETTINGS_LANGUAGES_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @languages) - value: (object - (pair - key: (string) - value: (object - (pair - key: (string (string_content) @setting_name) - value: (_) @value - ) - ) - )) - ) - ) - (#eq? @languages "languages") -)"#; - -pub const SETTINGS_ASSISTANT_TOOLS_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @assistant) - value: (object - (pair - key: (string (string_content) @profiles) - value: (object - (pair - key: (_) - value: (object - (pair - key: (string (string_content) @tools_key) - value: (object - (pair - key: (string (string_content) @tool_name) - value: (_) @tool_value - ) - ) - ) - ) - ) - ) - ) - ) - ) - ) - (#eq? @assistant "assistant") - (#eq? @profiles "profiles") - (#eq? @tools_key "tools") -)"#; - -pub const SETTINGS_ASSISTANT_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @key) - ) - ) - (#eq? @key "assistant") -)"#; - -pub const SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @edit_predictions) - value: (object - (pair key: (string (string_content) @enabled_in_assistant)) - ) - ) - ) - (#eq? @edit_predictions "edit_predictions") - (#eq? @enabled_in_assistant "enabled_in_assistant") -)"#; - -pub const SETTINGS_DUPLICATED_AGENT_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @agent1) - value: (_) - ) @pair1 - (pair - key: (string (string_content) @agent2) - value: (_) - ) - ) - (#eq? @agent1 "agent") - (#eq? @agent2 "agent") -)"#; - -/// Migrate language settings, -/// calls `migrate_fn` with the top level object as well as all language settings under the "languages" key -/// Fails early if `migrate_fn` returns an error at any point -pub fn migrate_language_setting( - value: &mut serde_json::Value, - migrate_fn: fn(&mut serde_json::Value, path: &[&str]) -> anyhow::Result<()>, -) -> anyhow::Result<()> { - migrate_fn(value, &[])?; - let languages = value - .as_object_mut() - .and_then(|obj| obj.get_mut("languages")) - .and_then(|languages| languages.as_object_mut()); - if let Some(languages) = languages { - for (language_name, language) in languages.iter_mut() { - let path = vec!["languages", language_name]; - migrate_fn(language, &path)?; - } - } - Ok(()) -} diff --git a/crates/miniprofiler_ui/Cargo.toml b/crates/miniprofiler_ui/Cargo.toml deleted file mode 100644 index bb508a188e..0000000000 --- a/crates/miniprofiler_ui/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "miniprofiler_ui" -version = "0.1.0" -publish.workspace = true -edition.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/miniprofiler_ui.rs" - -[dependencies] -gpui.workspace = true -zed_actions.workspace = true -workspace.workspace = true -util.workspace = true -serde_json.workspace = true -smol.workspace = true - -[dev-dependencies] -gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/miniprofiler_ui/LICENSE-GPL b/crates/miniprofiler_ui/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/miniprofiler_ui/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/miniprofiler_ui/src/miniprofiler_ui.rs b/crates/miniprofiler_ui/src/miniprofiler_ui.rs deleted file mode 100644 index ea59b43cc1..0000000000 --- a/crates/miniprofiler_ui/src/miniprofiler_ui.rs +++ /dev/null @@ -1,410 +0,0 @@ -use std::{ - ops::Range, - path::PathBuf, - rc::Rc, - time::{Duration, Instant}, -}; - -use gpui::{ - App, AppContext, ClipboardItem, Context, Div, Entity, Hsla, InteractiveElement, - ParentElement as _, Render, SerializedTaskTiming, SharedString, StatefulInteractiveElement, - Styled, Task, TaskTiming, TitlebarOptions, UniformListScrollHandle, WindowBounds, WindowHandle, - WindowOptions, div, prelude::FluentBuilder, px, relative, size, uniform_list, -}; -use util::ResultExt; -use workspace::{ - Workspace, - ui::{ - ActiveTheme, Button, ButtonCommon, ButtonStyle, Checkbox, Clickable, Divider, - ScrollableHandle as _, ToggleState, Tooltip, WithScrollbar, h_flex, v_flex, - }, -}; -use zed_actions::OpenPerformanceProfiler; - -pub fn init(startup_time: Instant, cx: &mut App) { - cx.observe_new(move |workspace: &mut workspace::Workspace, _, _| { - workspace.register_action(move |workspace, _: &OpenPerformanceProfiler, window, cx| { - let window_handle = window - .window_handle() - .downcast::() - .expect("Workspaces are root Windows"); - open_performance_profiler(startup_time, workspace, window_handle, cx); - }); - }) - .detach(); -} - -fn open_performance_profiler( - startup_time: Instant, - _workspace: &mut workspace::Workspace, - workspace_handle: WindowHandle, - cx: &mut App, -) { - let existing_window = cx - .windows() - .into_iter() - .find_map(|window| window.downcast::()); - - if let Some(existing_window) = existing_window { - existing_window - .update(cx, |profiler_window, window, _cx| { - profiler_window.workspace = Some(workspace_handle); - window.activate_window(); - }) - .log_err(); - return; - } - - let default_bounds = size(px(1280.), px(720.)); // 16:9 - - cx.open_window( - WindowOptions { - titlebar: Some(TitlebarOptions { - title: Some("Profiler Window".into()), - appears_transparent: false, - traffic_light_position: None, - }), - focus: true, - show: true, - is_movable: true, - kind: gpui::WindowKind::Normal, - window_background: cx.theme().window_background_appearance(), - window_decorations: None, - window_min_size: Some(default_bounds), - window_bounds: Some(WindowBounds::centered(default_bounds, cx)), - ..Default::default() - }, - |_window, cx| ProfilerWindow::new(startup_time, Some(workspace_handle), cx), - ) - .log_err(); -} - -enum DataMode { - Realtime(Option>), - Snapshot(Vec), -} - -struct TimingBar { - location: &'static core::panic::Location<'static>, - start: Instant, - end: Instant, - color: Hsla, -} - -pub struct ProfilerWindow { - startup_time: Instant, - data: DataMode, - include_self_timings: ToggleState, - autoscroll: bool, - scroll_handle: UniformListScrollHandle, - workspace: Option>, - _refresh: Option>, -} - -impl ProfilerWindow { - pub fn new( - startup_time: Instant, - workspace_handle: Option>, - cx: &mut App, - ) -> Entity { - let entity = cx.new(|cx| ProfilerWindow { - startup_time, - data: DataMode::Realtime(None), - include_self_timings: ToggleState::Unselected, - autoscroll: true, - scroll_handle: UniformListScrollHandle::default(), - workspace: workspace_handle, - _refresh: Some(Self::begin_listen(cx)), - }); - - entity - } - - fn begin_listen(cx: &mut Context) -> Task<()> { - cx.spawn(async move |this, cx| { - loop { - let data = cx - .foreground_executor() - .dispatcher - .get_current_thread_timings(); - - this.update(cx, |this: &mut ProfilerWindow, cx| { - this.data = DataMode::Realtime(Some(data)); - cx.notify(); - }) - .ok(); - - // yield to the executor - cx.background_executor() - .timer(Duration::from_micros(1)) - .await; - } - }) - } - - fn get_timings(&self) -> Option<&Vec> { - match &self.data { - DataMode::Realtime(data) => data.as_ref(), - DataMode::Snapshot(data) => Some(data), - } - } - - fn render_timing(value_range: Range, item: TimingBar, cx: &App) -> Div { - let time_ms = item.end.duration_since(item.start).as_secs_f32() * 1000f32; - - let remap = value_range - .end - .duration_since(value_range.start) - .as_secs_f32() - * 1000f32; - - let start = (item.start.duration_since(value_range.start).as_secs_f32() * 1000f32) / remap; - let end = (item.end.duration_since(value_range.start).as_secs_f32() * 1000f32) / remap; - - let bar_width = end - start.abs(); - - let location = item - .location - .file() - .rsplit_once("/") - .unwrap_or(("", item.location.file())) - .1; - let location = location.rsplit_once("\\").unwrap_or(("", location)).1; - - let label = SharedString::from(format!( - "{}:{}:{}", - location, - item.location.line(), - item.location.column() - )); - - h_flex() - .gap_2() - .w_full() - .h(px(32.0)) - .child( - div() - .id(label.clone()) - .w(px(200.0)) - .flex_shrink_0() - .overflow_hidden() - .child(div().text_ellipsis().child(label.clone())) - .tooltip(Tooltip::text(label.clone())) - .on_click(move |_, _, cx| { - cx.write_to_clipboard(ClipboardItem::new_string(label.to_string())) - }), - ) - .child( - div() - .flex_1() - .h(px(24.0)) - .bg(cx.theme().colors().background) - .rounded_md() - .p(px(2.0)) - .relative() - .child( - div() - .absolute() - .h_full() - .rounded_sm() - .bg(item.color) - .left(relative(start.max(0f32))) - .w(relative(bar_width)), - ), - ) - .child( - div() - .min_w(px(70.)) - .flex_shrink_0() - .text_right() - .child(format!("{:.1} ms", time_ms)), - ) - } -} - -impl Render for ProfilerWindow { - fn render( - &mut self, - window: &mut gpui::Window, - cx: &mut gpui::Context, - ) -> impl gpui::IntoElement { - let scroll_offset = self.scroll_handle.offset(); - let max_offset = self.scroll_handle.max_offset(); - self.autoscroll = -scroll_offset.y >= (max_offset.height - px(24.)); - if self.autoscroll { - self.scroll_handle.scroll_to_bottom(); - } - - v_flex() - .id("profiler") - .w_full() - .h_full() - .bg(cx.theme().colors().surface_background) - .text_color(cx.theme().colors().text) - .child( - h_flex() - .py_2() - .px_4() - .w_full() - .justify_between() - .child( - h_flex() - .gap_2() - .child( - Button::new( - "switch-mode", - match self.data { - DataMode::Snapshot { .. } => "Resume", - DataMode::Realtime(_) => "Pause", - }, - ) - .style(ButtonStyle::Filled) - .on_click(cx.listener( - |this, _, _window, cx| { - match &this.data { - DataMode::Realtime(Some(data)) => { - this._refresh = None; - this.data = DataMode::Snapshot(data.clone()); - } - DataMode::Snapshot { .. } => { - this._refresh = Some(Self::begin_listen(cx)); - this.data = DataMode::Realtime(None); - } - _ => {} - }; - cx.notify(); - }, - )), - ) - .child( - Button::new("export-data", "Save") - .style(ButtonStyle::Filled) - .on_click(cx.listener(|this, _, _window, cx| { - let Some(workspace) = this.workspace else { - return; - }; - - let Some(data) = this.get_timings() else { - return; - }; - let timings = - SerializedTaskTiming::convert(this.startup_time, &data); - - let active_path = workspace - .read_with(cx, |workspace, cx| { - workspace.most_recent_active_path(cx) - }) - .log_err() - .flatten() - .and_then(|p| p.parent().map(|p| p.to_owned())) - .unwrap_or_else(|| PathBuf::default()); - - let path = cx.prompt_for_new_path( - &active_path, - Some("performance_profile.miniprof"), - ); - - cx.background_spawn(async move { - let path = path.await; - let path = - path.log_err().and_then(|p| p.log_err()).flatten(); - - let Some(path) = path else { - return; - }; - - let Some(timings) = - serde_json::to_string(&timings).log_err() - else { - return; - }; - - smol::fs::write(path, &timings).await.log_err(); - }) - .detach(); - })), - ), - ) - .child( - Checkbox::new("include-self", self.include_self_timings) - .label("Include profiler timings") - .on_click(cx.listener(|this, checked, _window, cx| { - this.include_self_timings = *checked; - cx.notify(); - })), - ), - ) - .when_some(self.get_timings(), |div, e| { - if e.len() == 0 { - return div; - } - - let min = e[0].start; - let max = e[e.len() - 1].end.unwrap_or_else(|| Instant::now()); - let timings = Rc::new( - e.into_iter() - .filter(|timing| { - timing - .end - .unwrap_or_else(|| Instant::now()) - .duration_since(timing.start) - .as_millis() - >= 1 - }) - .filter(|timing| { - if self.include_self_timings.selected() { - true - } else { - !timing.location.file().ends_with("miniprofiler_ui.rs") - } - }) - .cloned() - .collect::>(), - ); - - div.child(Divider::horizontal()).child( - v_flex() - .id("timings.bars") - .w_full() - .h_full() - .gap_2() - .child( - uniform_list("list", timings.len(), { - let timings = timings.clone(); - move |visible_range, _, cx| { - let mut items = vec![]; - for i in visible_range { - let timing = &timings[i]; - let value_range = - max.checked_sub(Duration::from_secs(10)).unwrap_or(min) - ..max; - items.push(Self::render_timing( - value_range, - TimingBar { - location: timing.location, - start: timing.start, - end: timing.end.unwrap_or_else(|| Instant::now()), - color: cx - .theme() - .accents() - .color_for_index(i as u32), - }, - cx, - )); - } - items - } - }) - .p_4() - .on_scroll_wheel(cx.listener(|this, _, _, cx| { - this.autoscroll = false; - cx.notify(); - })) - .track_scroll(&self.scroll_handle) - .size_full(), - ) - .vertical_scrollbar_for(&self.scroll_handle, window, cx), - ) - }) - } -} diff --git a/crates/mistral/Cargo.toml b/crates/mistral/Cargo.toml deleted file mode 100644 index c4d475f014..0000000000 --- a/crates/mistral/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "mistral" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/mistral.rs" - -[features] -default = [] -schemars = ["dep:schemars"] - -[dependencies] -anyhow.workspace = true -futures.workspace = true -http_client.workspace = true -schemars = { workspace = true, optional = true } -serde.workspace = true -serde_json.workspace = true -strum.workspace = true diff --git a/crates/mistral/LICENSE-GPL b/crates/mistral/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/mistral/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/mistral/src/mistral.rs b/crates/mistral/src/mistral.rs deleted file mode 100644 index eca4743d04..0000000000 --- a/crates/mistral/src/mistral.rs +++ /dev/null @@ -1,482 +0,0 @@ -use anyhow::{Result, anyhow}; -use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream}; -use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::convert::TryFrom; -use strum::EnumIter; - -pub const MISTRAL_API_URL: &str = "https://api.mistral.ai/v1"; -pub const CODESTRAL_API_URL: &str = "https://codestral.mistral.ai"; - -#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum Role { - User, - Assistant, - System, - Tool, -} - -impl TryFrom for Role { - type Error = anyhow::Error; - - fn try_from(value: String) -> Result { - match value.as_str() { - "user" => Ok(Self::User), - "assistant" => Ok(Self::Assistant), - "system" => Ok(Self::System), - "tool" => Ok(Self::Tool), - _ => anyhow::bail!("invalid role '{value}'"), - } - } -} - -impl From for String { - fn from(val: Role) -> Self { - match val { - Role::User => "user".to_owned(), - Role::Assistant => "assistant".to_owned(), - Role::System => "system".to_owned(), - Role::Tool => "tool".to_owned(), - } - } -} - -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)] -pub enum Model { - #[serde(rename = "codestral-latest", alias = "codestral-latest")] - #[default] - CodestralLatest, - - #[serde(rename = "mistral-large-latest", alias = "mistral-large-latest")] - MistralLargeLatest, - #[serde(rename = "mistral-medium-latest", alias = "mistral-medium-latest")] - MistralMediumLatest, - #[serde(rename = "mistral-small-latest", alias = "mistral-small-latest")] - MistralSmallLatest, - - #[serde(rename = "magistral-medium-latest", alias = "magistral-medium-latest")] - MagistralMediumLatest, - #[serde(rename = "magistral-small-latest", alias = "magistral-small-latest")] - MagistralSmallLatest, - - #[serde(rename = "open-mistral-nemo", alias = "open-mistral-nemo")] - OpenMistralNemo, - #[serde(rename = "open-codestral-mamba", alias = "open-codestral-mamba")] - OpenCodestralMamba, - - #[serde(rename = "devstral-medium-latest", alias = "devstral-medium-latest")] - DevstralMediumLatest, - #[serde(rename = "devstral-small-latest", alias = "devstral-small-latest")] - DevstralSmallLatest, - - #[serde(rename = "pixtral-12b-latest", alias = "pixtral-12b-latest")] - Pixtral12BLatest, - #[serde(rename = "pixtral-large-latest", alias = "pixtral-large-latest")] - PixtralLargeLatest, - - #[serde(rename = "custom")] - Custom { - name: String, - /// The name displayed in the UI, such as in the assistant panel model dropdown menu. - display_name: Option, - max_tokens: u64, - max_output_tokens: Option, - max_completion_tokens: Option, - supports_tools: Option, - supports_images: Option, - supports_thinking: Option, - }, -} - -impl Model { - pub fn default_fast() -> Self { - Model::MistralSmallLatest - } - - pub fn from_id(id: &str) -> Result { - match id { - "codestral-latest" => Ok(Self::CodestralLatest), - "mistral-large-latest" => Ok(Self::MistralLargeLatest), - "mistral-medium-latest" => Ok(Self::MistralMediumLatest), - "mistral-small-latest" => Ok(Self::MistralSmallLatest), - "magistral-medium-latest" => Ok(Self::MagistralMediumLatest), - "magistral-small-latest" => Ok(Self::MagistralSmallLatest), - "open-mistral-nemo" => Ok(Self::OpenMistralNemo), - "open-codestral-mamba" => Ok(Self::OpenCodestralMamba), - "devstral-medium-latest" => Ok(Self::DevstralMediumLatest), - "devstral-small-latest" => Ok(Self::DevstralSmallLatest), - "pixtral-12b-latest" => Ok(Self::Pixtral12BLatest), - "pixtral-large-latest" => Ok(Self::PixtralLargeLatest), - invalid_id => anyhow::bail!("invalid model id '{invalid_id}'"), - } - } - - pub fn id(&self) -> &str { - match self { - Self::CodestralLatest => "codestral-latest", - Self::MistralLargeLatest => "mistral-large-latest", - Self::MistralMediumLatest => "mistral-medium-latest", - Self::MistralSmallLatest => "mistral-small-latest", - Self::MagistralMediumLatest => "magistral-medium-latest", - Self::MagistralSmallLatest => "magistral-small-latest", - Self::OpenMistralNemo => "open-mistral-nemo", - Self::OpenCodestralMamba => "open-codestral-mamba", - Self::DevstralMediumLatest => "devstral-medium-latest", - Self::DevstralSmallLatest => "devstral-small-latest", - Self::Pixtral12BLatest => "pixtral-12b-latest", - Self::PixtralLargeLatest => "pixtral-large-latest", - Self::Custom { name, .. } => name, - } - } - - pub fn display_name(&self) -> &str { - match self { - Self::CodestralLatest => "codestral-latest", - Self::MistralLargeLatest => "mistral-large-latest", - Self::MistralMediumLatest => "mistral-medium-latest", - Self::MistralSmallLatest => "mistral-small-latest", - Self::MagistralMediumLatest => "magistral-medium-latest", - Self::MagistralSmallLatest => "magistral-small-latest", - Self::OpenMistralNemo => "open-mistral-nemo", - Self::OpenCodestralMamba => "open-codestral-mamba", - Self::DevstralMediumLatest => "devstral-medium-latest", - Self::DevstralSmallLatest => "devstral-small-latest", - Self::Pixtral12BLatest => "pixtral-12b-latest", - Self::PixtralLargeLatest => "pixtral-large-latest", - Self::Custom { - name, display_name, .. - } => display_name.as_ref().unwrap_or(name), - } - } - - pub fn max_token_count(&self) -> u64 { - match self { - Self::CodestralLatest => 256000, - Self::MistralLargeLatest => 131000, - Self::MistralMediumLatest => 128000, - Self::MistralSmallLatest => 32000, - Self::MagistralMediumLatest => 40000, - Self::MagistralSmallLatest => 40000, - Self::OpenMistralNemo => 131000, - Self::OpenCodestralMamba => 256000, - Self::DevstralMediumLatest => 128000, - Self::DevstralSmallLatest => 262144, - Self::Pixtral12BLatest => 128000, - Self::PixtralLargeLatest => 128000, - Self::Custom { max_tokens, .. } => *max_tokens, - } - } - - pub fn max_output_tokens(&self) -> Option { - match self { - Self::Custom { - max_output_tokens, .. - } => *max_output_tokens, - _ => None, - } - } - - pub fn supports_tools(&self) -> bool { - match self { - Self::CodestralLatest - | Self::MistralLargeLatest - | Self::MistralMediumLatest - | Self::MistralSmallLatest - | Self::MagistralMediumLatest - | Self::MagistralSmallLatest - | Self::OpenMistralNemo - | Self::OpenCodestralMamba - | Self::DevstralMediumLatest - | Self::DevstralSmallLatest - | Self::Pixtral12BLatest - | Self::PixtralLargeLatest => true, - Self::Custom { supports_tools, .. } => supports_tools.unwrap_or(false), - } - } - - pub fn supports_images(&self) -> bool { - match self { - Self::Pixtral12BLatest - | Self::PixtralLargeLatest - | Self::MistralMediumLatest - | Self::MistralSmallLatest => true, - Self::CodestralLatest - | Self::MistralLargeLatest - | Self::MagistralMediumLatest - | Self::MagistralSmallLatest - | Self::OpenMistralNemo - | Self::OpenCodestralMamba - | Self::DevstralMediumLatest - | Self::DevstralSmallLatest => false, - Self::Custom { - supports_images, .. - } => 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)] -pub struct Request { - pub model: String, - pub messages: Vec, - pub stream: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_tokens: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub temperature: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub response_format: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_choice: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub parallel_tool_calls: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub tools: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum ResponseFormat { - Text, - #[serde(rename = "json_object")] - JsonObject, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum ToolDefinition { - Function { function: FunctionDefinition }, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FunctionDefinition { - pub name: String, - pub description: Option, - pub parameters: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ToolChoice { - Auto, - Required, - None, - Any, - #[serde(untagged)] - Function(ToolDefinition), -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(tag = "role", rename_all = "lowercase")] -pub enum RequestMessage { - Assistant { - #[serde(flatten)] - #[serde(default, skip_serializing_if = "Option::is_none")] - content: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - tool_calls: Vec, - }, - User { - #[serde(flatten)] - content: MessageContent, - }, - System { - #[serde(flatten)] - content: MessageContent, - }, - Tool { - content: String, - tool_call_id: String, - }, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] -#[serde(untagged)] -pub enum MessageContent { - #[serde(rename = "content")] - Plain { content: String }, - #[serde(rename = "content")] - Multipart { content: Vec }, -} - -impl MessageContent { - pub fn empty() -> Self { - Self::Plain { - content: String::new(), - } - } - - pub fn push_part(&mut self, part: MessagePart) { - match self { - Self::Plain { content } => match part { - MessagePart::Text { text } => { - content.push_str(&text); - } - part => { - let mut parts = if content.is_empty() { - Vec::new() - } else { - vec![MessagePart::Text { - text: content.clone(), - }] - }; - parts.push(part); - *self = Self::Multipart { content: parts }; - } - }, - Self::Multipart { content } => { - content.push(part); - } - } - } -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] -#[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)] -pub struct ToolCall { - pub id: String, - #[serde(flatten)] - pub content: ToolCallContent, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum ToolCallContent { - Function { function: FunctionContent }, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct FunctionContent { - pub name: String, - pub arguments: String, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Usage { - pub prompt_tokens: u64, - pub completion_tokens: u64, - pub total_tokens: u64, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct StreamResponse { - pub id: String, - pub object: String, - pub created: u64, - pub model: String, - pub choices: Vec, - pub usage: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct StreamChoice { - pub index: u32, - pub delta: StreamDelta, - pub finish_reason: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct StreamDelta { - pub role: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub content: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_calls: Option>, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] -#[serde(untagged)] -pub enum MessageContentDelta { - Text(String), - Parts(Vec), -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] -pub struct ToolCallChunk { - pub index: usize, - pub id: Option, - pub function: Option, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] -pub struct FunctionChunk { - pub name: Option, - pub arguments: Option, -} - -pub async fn stream_completion( - client: &dyn HttpClient, - api_url: &str, - api_key: &str, - request: Request, -) -> Result>> { - let uri = format!("{api_url}/chat/completions"); - let request_builder = HttpRequest::builder() - .method(Method::POST) - .uri(uri) - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key.trim())); - - let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?; - let mut response = client.send(request).await?; - - if response.status().is_success() { - let reader = BufReader::new(response.into_body()); - Ok(reader - .lines() - .filter_map(|line| async move { - match line { - Ok(line) => { - let line = line.strip_prefix("data: ")?; - if line == "[DONE]" { - None - } else { - match serde_json::from_str(line) { - Ok(response) => Some(Ok(response)), - Err(error) => Some(Err(anyhow!(error))), - } - } - } - Err(error) => Some(Err(anyhow!(error))), - } - }) - .boxed()) - } else { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - anyhow::bail!( - "Failed to connect to Mistral API: {} {}", - response.status(), - body, - ); - } -} diff --git a/crates/multi_buffer/Cargo.toml b/crates/multi_buffer/Cargo.toml deleted file mode 100644 index 524c916682..0000000000 --- a/crates/multi_buffer/Cargo.toml +++ /dev/null @@ -1,63 +0,0 @@ -[package] -name = "multi_buffer" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/multi_buffer.rs" -doctest = false - -[features] -test-support = [ - "buffer_diff/test-support", - "gpui/test-support", - "language/test-support", - "text/test-support", - "util/test-support", -] - -[dependencies] -anyhow.workspace = true -clock.workspace = true -collections.workspace = true -ctor.workspace = true -buffer_diff.workspace = true -gpui.workspace = true -itertools.workspace = true -language.workspace = true -log.workspace = true -parking_lot.workspace = true -rand.workspace = true -rope.workspace = true -smol.workspace = true -settings.workspace = true -serde.workspace = true -smallvec.workspace = true -sum_tree.workspace = true -text.workspace = true -theme.workspace = true -tree-sitter.workspace = true -ztracing.workspace = true -tracing.workspace = true -util.workspace = true - -[dev-dependencies] -buffer_diff = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -indoc.workspace = true -language = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true -project = { workspace = true, features = ["test-support"] } -rand.workspace = true -settings = { workspace = true, features = ["test-support"] } -text = { workspace = true, features = ["test-support"] } -util = { workspace = true, features = ["test-support"] } -zlog.workspace = true - -[package.metadata.cargo-machete] -ignored = ["tracing"] diff --git a/crates/multi_buffer/LICENSE-GPL b/crates/multi_buffer/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/multi_buffer/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/multi_buffer/src/anchor.rs b/crates/multi_buffer/src/anchor.rs deleted file mode 100644 index 51696ba09e..0000000000 --- a/crates/multi_buffer/src/anchor.rs +++ /dev/null @@ -1,257 +0,0 @@ -use crate::{MultiBufferDimension, MultiBufferOffset, MultiBufferOffsetUtf16}; - -use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToPoint}; -use language::Point; -use std::{ - cmp::Ordering, - ops::{AddAssign, Range, Sub}, -}; -use sum_tree::Bias; - -#[derive(Clone, Copy, Eq, PartialEq, Hash)] -pub struct Anchor { - pub excerpt_id: ExcerptId, - pub text_anchor: text::Anchor, - pub diff_base_anchor: Option, -} - -impl std::fmt::Debug for Anchor { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.is_min() { - return write!(f, "Anchor::min({:?})", self.text_anchor.buffer_id); - } - if self.is_max() { - return write!(f, "Anchor::max({:?})", self.text_anchor.buffer_id); - } - - f.debug_struct("Anchor") - .field("excerpt_id", &self.excerpt_id) - .field("text_anchor", &self.text_anchor) - .field("diff_base_anchor", &self.diff_base_anchor) - .finish() - } -} - -impl Anchor { - pub fn with_diff_base_anchor(self, diff_base_anchor: text::Anchor) -> Self { - Self { - diff_base_anchor: Some(diff_base_anchor), - ..self - } - } - - pub fn in_buffer(excerpt_id: ExcerptId, text_anchor: text::Anchor) -> Self { - Self { - excerpt_id, - text_anchor, - diff_base_anchor: None, - } - } - - pub fn range_in_buffer(excerpt_id: ExcerptId, range: Range) -> Range { - Self::in_buffer(excerpt_id, range.start)..Self::in_buffer(excerpt_id, range.end) - } - - pub fn min() -> Self { - Self { - excerpt_id: ExcerptId::min(), - text_anchor: text::Anchor::MIN, - diff_base_anchor: None, - } - } - - pub fn max() -> Self { - Self { - excerpt_id: ExcerptId::max(), - text_anchor: text::Anchor::MAX, - diff_base_anchor: None, - } - } - - pub fn is_min(&self) -> bool { - self.excerpt_id == ExcerptId::min() - && self.text_anchor.is_min() - && self.diff_base_anchor.is_none() - } - - pub fn is_max(&self) -> bool { - self.excerpt_id == ExcerptId::max() - && self.text_anchor.is_max() - && self.diff_base_anchor.is_none() - } - - pub fn cmp(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering { - if self == other { - return Ordering::Equal; - } - - let self_excerpt_id = snapshot.latest_excerpt_id(self.excerpt_id); - let other_excerpt_id = snapshot.latest_excerpt_id(other.excerpt_id); - - let excerpt_id_cmp = self_excerpt_id.cmp(&other_excerpt_id, snapshot); - if excerpt_id_cmp.is_ne() { - return excerpt_id_cmp; - } - if self_excerpt_id == ExcerptId::max() - && self.text_anchor.is_max() - && self.text_anchor.is_max() - && self.diff_base_anchor.is_none() - && other.diff_base_anchor.is_none() - { - return Ordering::Equal; - } - if let Some(excerpt) = snapshot.excerpt(self_excerpt_id) { - let text_cmp = self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer); - 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 - .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, - }; - } - } - Ordering::Equal - } - - pub fn bias(&self) -> Bias { - self.text_anchor.bias - } - - 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 { - excerpt_id: excerpt.id, - text_anchor: self.text_anchor.bias_left(&excerpt.buffer), - diff_base_anchor: self.diff_base_anchor.map(|a| { - if let Some(base_text) = snapshot - .diffs - .get(&excerpt.buffer_id) - .map(|diff| diff.base_text()) - && a.buffer_id == Some(base_text.remote_id()) - { - return a.bias_left(base_text); - } - a - }), - }; - } - *self - } - - pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor { - if self.text_anchor.bias != Bias::Right - && let Some(excerpt) = snapshot.excerpt(self.excerpt_id) - { - return Self { - excerpt_id: excerpt.id, - text_anchor: self.text_anchor.bias_right(&excerpt.buffer), - diff_base_anchor: self.diff_base_anchor.map(|a| { - if let Some(base_text) = snapshot - .diffs - .get(&excerpt.buffer_id) - .map(|diff| diff.base_text()) - && a.buffer_id == Some(base_text.remote_id()) - { - return a.bias_right(base_text); - } - a - }), - }; - } - *self - } - - pub fn summary(&self, snapshot: &MultiBufferSnapshot) -> D - where - D: MultiBufferDimension - + Ord - + Sub - + AddAssign, - D::TextDimension: Sub + Ord, - { - snapshot.summary_for_anchor(self) - } - - pub fn is_valid(&self, snapshot: &MultiBufferSnapshot) -> bool { - if self.is_min() || self.is_max() { - true - } else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { - (self.text_anchor == excerpt.range.context.start - || self.text_anchor == excerpt.range.context.end - || self.text_anchor.is_valid(&excerpt.buffer)) - && excerpt.contains(self) - } else { - false - } - } -} - -impl ToOffset for Anchor { - fn to_offset(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffset { - self.summary(snapshot) - } - fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffsetUtf16 { - self.summary(snapshot) - } -} - -impl ToPoint for Anchor { - fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point { - self.summary(snapshot) - } - fn to_point_utf16(&self, snapshot: &MultiBufferSnapshot) -> rope::PointUtf16 { - self.summary(snapshot) - } -} - -pub trait AnchorRangeExt { - fn cmp(&self, other: &Range, buffer: &MultiBufferSnapshot) -> Ordering; - fn includes(&self, other: &Range, buffer: &MultiBufferSnapshot) -> bool; - fn overlaps(&self, other: &Range, buffer: &MultiBufferSnapshot) -> bool; - fn to_offset(&self, content: &MultiBufferSnapshot) -> Range; - fn to_point(&self, content: &MultiBufferSnapshot) -> Range; -} - -impl AnchorRangeExt for Range { - fn cmp(&self, other: &Range, buffer: &MultiBufferSnapshot) -> Ordering { - match self.start.cmp(&other.start, buffer) { - Ordering::Equal => other.end.cmp(&self.end, buffer), - ord => ord, - } - } - - fn includes(&self, other: &Range, buffer: &MultiBufferSnapshot) -> bool { - self.start.cmp(&other.start, buffer).is_le() && other.end.cmp(&self.end, buffer).is_le() - } - - fn overlaps(&self, other: &Range, buffer: &MultiBufferSnapshot) -> bool { - self.end.cmp(&other.start, buffer).is_ge() && self.start.cmp(&other.end, buffer).is_le() - } - - fn to_offset(&self, content: &MultiBufferSnapshot) -> Range { - self.start.to_offset(content)..self.end.to_offset(content) - } - - fn to_point(&self, content: &MultiBufferSnapshot) -> Range { - self.start.to_point(content)..self.end.to_point(content) - } -} diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs deleted file mode 100644 index 5b343ecc57..0000000000 --- a/crates/multi_buffer/src/multi_buffer.rs +++ /dev/null @@ -1,8227 +0,0 @@ -mod anchor; -#[cfg(test)] -mod multi_buffer_tests; -mod path_key; -mod transaction; - -use self::transaction::History; - -pub use anchor::{Anchor, AnchorRangeExt}; - -use anyhow::{Result, anyhow}; -use buffer_diff::{ - BufferDiff, BufferDiffEvent, BufferDiffSnapshot, DiffHunkSecondaryStatus, DiffHunkStatus, - DiffHunkStatusKind, -}; -use clock::ReplicaId; -use collections::{BTreeMap, Bound, HashMap, HashSet}; -use gpui::{App, Context, Entity, EntityId, EventEmitter}; -use itertools::Itertools; -use language::{ - AutoindentMode, BracketMatch, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, - CharClassifier, CharKind, CharScopeContext, Chunk, CursorShape, DiagnosticEntryRef, DiskState, - File, IndentGuideSettings, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, - Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, TextObject, ToOffset as _, - ToPoint as _, TransactionId, TreeSitterOptions, Unclipped, - language_settings::{LanguageSettings, language_settings}, -}; - -#[cfg(any(test, feature = "test-support"))] -use gpui::AppContext as _; - -use rope::DimensionPair; -use smallvec::SmallVec; -use smol::future::yield_now; -use std::{ - any::type_name, - borrow::Cow, - cell::{Cell, Ref, RefCell}, - cmp, - collections::VecDeque, - fmt::{self, Debug}, - future::Future, - io, - iter::{self, FromIterator}, - mem, - ops::{self, AddAssign, ControlFlow, Range, RangeBounds, Sub, SubAssign}, - rc::Rc, - str, - sync::Arc, - time::Duration, -}; -use sum_tree::{Bias, Cursor, Dimension, Dimensions, SumTree, TreeMap}; -use text::{ - BufferId, Edit, LineIndent, TextSummary, - locator::Locator, - subscription::{Subscription, Topic}, -}; -use theme::SyntaxTheme; -use util::post_inc; -use ztracing::instrument; - -pub use self::path_key::PathKey; - -#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub struct ExcerptId(u32); - -#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub struct BaseTextRow(pub u32); - -/// One or more [`Buffers`](Buffer) being edited in a single view. -/// -/// See -pub struct MultiBuffer { - /// A snapshot of the [`Excerpt`]s in the MultiBuffer. - /// Use [`MultiBuffer::snapshot`] to get a up-to-date snapshot. - snapshot: RefCell, - /// Contains the state of the buffers being edited - buffers: HashMap, - /// Mapping from path keys to their excerpts. - excerpts_by_path: BTreeMap>, - /// Mapping from excerpt IDs to their path key. - paths_by_excerpt: HashMap, - /// Mapping from buffer IDs to their diff states - diffs: HashMap, - subscriptions: Topic, - /// If true, the multi-buffer only contains a single [`Buffer`] and a single [`Excerpt`] - singleton: bool, - /// The history of the multi-buffer. - history: History, - /// The explicit title of the multi-buffer. - /// If `None`, it will be derived from the underlying path or content. - title: Option, - /// The writing capability of the multi-buffer. - capability: Capability, - buffer_changed_since_sync: Rc>, - follower: Option>, - filter_mode: Option, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum MultiBufferFilterMode { - KeepInsertions, - KeepDeletions, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Event { - ExcerptsAdded { - buffer: Entity, - predecessor: ExcerptId, - excerpts: Vec<(ExcerptId, ExcerptRange)>, - }, - ExcerptsRemoved { - ids: Vec, - removed_buffer_ids: Vec, - }, - ExcerptsExpanded { - ids: Vec, - }, - ExcerptsEdited { - excerpt_ids: Vec, - buffer_ids: Vec, - }, - DiffHunksToggled, - Edited { - edited_buffer: Option>, - }, - TransactionUndone { - transaction_id: TransactionId, - }, - Reloaded, - LanguageChanged(BufferId, bool), - Reparsed(BufferId), - Saved, - FileHandleChanged, - DirtyChanged, - DiagnosticsUpdated, - BufferDiffChanged, -} - -/// A diff hunk, representing a range of consequent lines in a multibuffer. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MultiBufferDiffHunk { - /// The row range in the multibuffer where this diff hunk appears. - pub row_range: Range, - /// The buffer ID that this hunk belongs to. - pub buffer_id: BufferId, - /// The range of the underlying buffer that this hunk corresponds to. - pub buffer_range: Range, - /// The excerpt that contains the diff hunk. - pub excerpt_id: ExcerptId, - /// The range within the buffer's diff base that this hunk corresponds to. - pub diff_base_byte_range: Range, - /// Whether or not this hunk also appears in the 'secondary diff'. - pub secondary_status: DiffHunkSecondaryStatus, - /// The word diffs for this hunk. - pub word_diffs: Vec>, -} - -impl MultiBufferDiffHunk { - pub fn status(&self) -> DiffHunkStatus { - let kind = if self.buffer_range.start == self.buffer_range.end { - DiffHunkStatusKind::Deleted - } else if self.diff_base_byte_range.is_empty() { - DiffHunkStatusKind::Added - } else { - DiffHunkStatusKind::Modified - }; - DiffHunkStatus { - kind, - secondary: self.secondary_status, - } - } - - pub fn is_created_file(&self) -> bool { - self.diff_base_byte_range == (BufferOffset(0)..BufferOffset(0)) - && self.buffer_range.start.is_min() - && self.buffer_range.end.is_max() - } - - pub fn multi_buffer_range(&self) -> Range { - let start = Anchor::in_buffer(self.excerpt_id, self.buffer_range.start); - let end = Anchor::in_buffer(self.excerpt_id, self.buffer_range.end); - start..end - } -} - -pub type MultiBufferPoint = Point; -type ExcerptOffset = ExcerptDimension; -type ExcerptPoint = ExcerptDimension; - -#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq, Hash, serde::Deserialize)] -#[serde(transparent)] -pub struct MultiBufferRow(pub u32); - -impl MultiBufferRow { - pub const MIN: Self = Self(0); - pub const MAX: Self = Self(u32::MAX); -} - -impl ops::Add for MultiBufferRow { - type Output = Self; - - fn add(self, rhs: usize) -> Self::Output { - MultiBufferRow(self.0 + rhs as u32) - } -} - -pub trait MultiBufferDimension: 'static + Copy + Default + std::fmt::Debug { - type TextDimension: TextDimension; - fn from_summary(summary: &MBTextSummary) -> Self; - - fn add_text_dim(&mut self, summary: &Self::TextDimension); - - fn add_mb_text_summary(&mut self, summary: &MBTextSummary); -} - -// todo(lw): MultiBufferPoint -impl MultiBufferDimension for Point { - type TextDimension = Point; - fn from_summary(summary: &MBTextSummary) -> Self { - summary.lines - } - - fn add_text_dim(&mut self, other: &Self::TextDimension) { - *self += *other; - } - - fn add_mb_text_summary(&mut self, summary: &MBTextSummary) { - *self += summary.lines; - } -} - -// todo(lw): MultiBufferPointUtf16 -impl MultiBufferDimension for PointUtf16 { - type TextDimension = PointUtf16; - fn from_summary(summary: &MBTextSummary) -> Self { - summary.lines_utf16() - } - - fn add_text_dim(&mut self, other: &Self::TextDimension) { - *self += *other; - } - - fn add_mb_text_summary(&mut self, summary: &MBTextSummary) { - *self += summary.lines_utf16(); - } -} - -#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq, Hash, serde::Deserialize)] -pub struct MultiBufferOffset(pub usize); - -impl fmt::Display for MultiBufferOffset { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl rand::distr::uniform::SampleUniform for MultiBufferOffset { - type Sampler = MultiBufferOffsetUniformSampler; -} - -pub struct MultiBufferOffsetUniformSampler { - sampler: rand::distr::uniform::UniformUsize, -} - -impl rand::distr::uniform::UniformSampler for MultiBufferOffsetUniformSampler { - type X = MultiBufferOffset; - - fn new(low_b: B1, high_b: B2) -> Result - where - B1: rand::distr::uniform::SampleBorrow + Sized, - B2: rand::distr::uniform::SampleBorrow + Sized, - { - let low = *low_b.borrow(); - let high = *high_b.borrow(); - let sampler = rand::distr::uniform::UniformUsize::new(low.0, high.0); - sampler.map(|sampler| MultiBufferOffsetUniformSampler { sampler }) - } - - #[inline] // if the range is constant, this helps LLVM to do the - // calculations at compile-time. - fn new_inclusive(low_b: B1, high_b: B2) -> Result - where - B1: rand::distr::uniform::SampleBorrow + Sized, - B2: rand::distr::uniform::SampleBorrow + Sized, - { - let low = *low_b.borrow(); - let high = *high_b.borrow(); - let sampler = rand::distr::uniform::UniformUsize::new_inclusive(low.0, high.0); - sampler.map(|sampler| MultiBufferOffsetUniformSampler { sampler }) - } - - fn sample(&self, rng: &mut R) -> Self::X { - MultiBufferOffset(self.sampler.sample(rng)) - } -} -impl MultiBufferDimension for MultiBufferOffset { - type TextDimension = usize; - fn from_summary(summary: &MBTextSummary) -> Self { - summary.len - } - - fn add_text_dim(&mut self, other: &Self::TextDimension) { - self.0 += *other; - } - - fn add_mb_text_summary(&mut self, summary: &MBTextSummary) { - *self += summary.len; - } -} -impl MultiBufferDimension for MultiBufferOffsetUtf16 { - type TextDimension = OffsetUtf16; - fn from_summary(summary: &MBTextSummary) -> Self { - MultiBufferOffsetUtf16(summary.len_utf16) - } - - fn add_text_dim(&mut self, other: &Self::TextDimension) { - self.0 += *other; - } - - fn add_mb_text_summary(&mut self, summary: &MBTextSummary) { - self.0 += summary.len_utf16; - } -} - -#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq, Hash, serde::Deserialize)] -pub struct BufferOffset(pub usize); - -impl TextDimension for BufferOffset { - fn from_text_summary(summary: &TextSummary) -> Self { - BufferOffset(usize::from_text_summary(summary)) - } - fn from_chunk(chunk: rope::ChunkSlice) -> Self { - BufferOffset(usize::from_chunk(chunk)) - } - fn add_assign(&mut self, other: &Self) { - TextDimension::add_assign(&mut self.0, &other.0); - } -} -impl<'a> sum_tree::Dimension<'a, rope::ChunkSummary> for BufferOffset { - fn zero(cx: ()) -> Self { - BufferOffset(>::zero(cx)) - } - - fn add_summary(&mut self, summary: &'a rope::ChunkSummary, cx: ()) { - usize::add_summary(&mut self.0, summary, cx); - } -} - -impl Sub for BufferOffset { - type Output = usize; - - fn sub(self, other: BufferOffset) -> Self::Output { - self.0 - other.0 - } -} - -impl AddAssign> for BufferOffset { - fn add_assign(&mut self, other: DimensionPair) { - self.0 += other.key; - } -} - -impl language::ToPoint for BufferOffset { - fn to_point(&self, snapshot: &text::BufferSnapshot) -> Point { - self.0.to_point(snapshot) - } -} - -impl language::ToPointUtf16 for BufferOffset { - fn to_point_utf16(&self, snapshot: &text::BufferSnapshot) -> PointUtf16 { - self.0.to_point_utf16(snapshot) - } -} - -impl language::ToOffset for BufferOffset { - fn to_offset(&self, snapshot: &text::BufferSnapshot) -> usize { - self.0.to_offset(snapshot) - } -} - -impl language::ToOffsetUtf16 for BufferOffset { - fn to_offset_utf16(&self, snapshot: &text::BufferSnapshot) -> OffsetUtf16 { - self.0.to_offset_utf16(snapshot) - } -} - -#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] -pub struct MultiBufferOffsetUtf16(pub OffsetUtf16); - -impl ops::Add for MultiBufferOffsetUtf16 { - type Output = MultiBufferOffsetUtf16; - - fn add(self, rhs: usize) -> Self::Output { - MultiBufferOffsetUtf16(OffsetUtf16(self.0.0 + rhs)) - } -} - -impl AddAssign for MultiBufferOffsetUtf16 { - fn add_assign(&mut self, rhs: OffsetUtf16) { - self.0 += rhs; - } -} - -impl AddAssign for MultiBufferOffsetUtf16 { - fn add_assign(&mut self, rhs: usize) { - self.0.0 += rhs; - } -} - -impl Sub for MultiBufferOffsetUtf16 { - type Output = OffsetUtf16; - - fn sub(self, other: MultiBufferOffsetUtf16) -> Self::Output { - self.0 - other.0 - } -} - -#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] -pub struct BufferOffsetUtf16(pub OffsetUtf16); - -impl MultiBufferOffset { - const ZERO: Self = Self(0); - pub fn saturating_sub(self, other: MultiBufferOffset) -> usize { - self.0.saturating_sub(other.0) - } - pub fn saturating_sub_usize(self, other: usize) -> MultiBufferOffset { - MultiBufferOffset(self.0.saturating_sub(other)) - } -} - -impl ops::Sub for MultiBufferOffset { - type Output = usize; - - fn sub(self, other: MultiBufferOffset) -> Self::Output { - self.0 - other.0 - } -} - -impl ops::Sub for MultiBufferOffset { - type Output = Self; - - fn sub(self, other: usize) -> Self::Output { - MultiBufferOffset(self.0 - other) - } -} - -impl ops::SubAssign for MultiBufferOffset { - fn sub_assign(&mut self, other: usize) { - self.0 -= other; - } -} - -impl ops::Add for BufferOffset { - type Output = Self; - - fn add(self, rhs: usize) -> Self::Output { - BufferOffset(self.0 + rhs) - } -} - -impl ops::AddAssign for BufferOffset { - fn add_assign(&mut self, other: usize) { - self.0 += other; - } -} - -impl ops::Add for MultiBufferOffset { - type Output = Self; - - fn add(self, rhs: usize) -> Self::Output { - MultiBufferOffset(self.0 + rhs) - } -} - -impl ops::AddAssign for MultiBufferOffset { - fn add_assign(&mut self, other: usize) { - self.0 += other; - } -} - -impl ops::Add for MultiBufferOffset { - type Output = Self; - - fn add(self, rhs: isize) -> Self::Output { - MultiBufferOffset((self.0 as isize + rhs) as usize) - } -} - -impl ops::Add for MultiBufferOffset { - type Output = Self; - - fn add(self, rhs: MultiBufferOffset) -> Self::Output { - MultiBufferOffset(self.0 + rhs.0) - } -} - -impl ops::AddAssign for MultiBufferOffset { - fn add_assign(&mut self, other: MultiBufferOffset) { - self.0 += other.0; - } -} - -pub trait ToOffset: 'static + fmt::Debug { - fn to_offset(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffset; - fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffsetUtf16; -} - -pub trait ToPoint: 'static + fmt::Debug { - fn to_point(&self, snapshot: &MultiBufferSnapshot) -> Point; - fn to_point_utf16(&self, snapshot: &MultiBufferSnapshot) -> PointUtf16; -} - -struct BufferState { - buffer: Entity, - last_version: RefCell, - last_non_text_state_update_count: Cell, - excerpts: Vec, - _subscriptions: [gpui::Subscription; 2], -} - -struct DiffState { - diff: Entity, - _subscription: gpui::Subscription, -} - -impl DiffState { - fn new(diff: Entity, cx: &mut Context) -> Self { - DiffState { - _subscription: cx.subscribe(&diff, |this, diff, event, cx| match event { - BufferDiffEvent::DiffChanged { changed_range } => { - if let Some(changed_range) = changed_range.clone() { - this.buffer_diff_changed(diff, changed_range, cx) - } - cx.emit(Event::BufferDiffChanged); - } - BufferDiffEvent::LanguageChanged => this.buffer_diff_language_changed(diff, cx), - _ => {} - }), - diff, - } - } -} - -/// The contents of a [`MultiBuffer`] at a single point in time. -#[derive(Clone, Default)] -pub struct MultiBufferSnapshot { - excerpts: SumTree, - diffs: TreeMap, - diff_transforms: SumTree, - non_text_state_update_count: usize, - edit_count: usize, - is_dirty: bool, - has_deleted_file: bool, - has_conflict: bool, - /// immutable fields - singleton: bool, - excerpt_ids: SumTree, - replaced_excerpts: TreeMap, - trailing_excerpt_update_count: usize, - all_diff_hunks_expanded: bool, - show_headers: bool, -} - -#[derive(Debug, Clone)] -/// A piece of text in the multi-buffer -enum DiffTransform { - Unmodified { - summary: MBTextSummary, - }, - InsertedHunk { - summary: MBTextSummary, - hunk_info: DiffTransformHunkInfo, - }, - FilteredInsertedHunk { - summary: MBTextSummary, - hunk_info: DiffTransformHunkInfo, - }, - DeletedHunk { - summary: TextSummary, - buffer_id: BufferId, - hunk_info: DiffTransformHunkInfo, - has_trailing_newline: bool, - }, -} - -#[derive(Clone, Debug)] -struct DiffTransformHunkInfo { - excerpt_id: ExcerptId, - hunk_start_anchor: text::Anchor, - hunk_secondary_status: DiffHunkSecondaryStatus, - base_text_byte_range: Range, -} - -impl Eq for DiffTransformHunkInfo {} - -impl PartialEq for DiffTransformHunkInfo { - fn eq(&self, other: &DiffTransformHunkInfo) -> bool { - self.excerpt_id == other.excerpt_id && self.hunk_start_anchor == other.hunk_start_anchor - } -} - -impl std::hash::Hash for DiffTransformHunkInfo { - fn hash(&self, state: &mut H) { - self.excerpt_id.hash(state); - self.hunk_start_anchor.hash(state); - } -} - -#[derive(Clone)] -pub struct ExcerptInfo { - pub id: ExcerptId, - pub buffer: BufferSnapshot, - pub buffer_id: BufferId, - pub range: ExcerptRange, - pub end_row: MultiBufferRow, -} - -/// Used with [`MultiBuffer::push_buffer_content_transform`] -#[derive(Clone, Debug)] -struct CurrentInsertedHunk { - hunk_excerpt_start: ExcerptOffset, - insertion_end_offset: ExcerptOffset, - hunk_info: DiffTransformHunkInfo, - is_filtered: bool, -} - -impl std::fmt::Debug for ExcerptInfo { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct(type_name::()) - .field("id", &self.id) - .field("buffer_id", &self.buffer_id) - .field("path", &self.buffer.file().map(|f| f.path())) - .field("range", &self.range) - .finish() - } -} - -/// A boundary between `Excerpt`s in a [`MultiBuffer`] -#[derive(Debug)] -pub struct ExcerptBoundary { - pub prev: Option, - pub next: ExcerptInfo, - /// The row in the `MultiBuffer` where the boundary is located - pub row: MultiBufferRow, -} - -impl ExcerptBoundary { - pub fn starts_new_buffer(&self) -> bool { - match (self.prev.as_ref(), &self.next) { - (None, _) => true, - (Some(prev), next) => prev.buffer_id != next.buffer_id, - } - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct ExpandInfo { - pub direction: ExpandExcerptDirection, - pub excerpt_id: ExcerptId, -} - -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub struct RowInfo { - pub buffer_id: Option, - pub buffer_row: Option, - pub base_text_row: Option, - pub multibuffer_row: Option, - pub diff_status: Option, - pub expand_info: Option, - pub wrapped_buffer_row: Option, -} - -/// A slice into a [`Buffer`] that is being edited in a [`MultiBuffer`]. -#[derive(Clone)] -struct Excerpt { - /// The unique identifier for this excerpt - id: ExcerptId, - /// The location of the excerpt in the [`MultiBuffer`] - locator: Locator, - /// The buffer being excerpted - buffer_id: BufferId, - /// A snapshot of the buffer being excerpted - buffer: BufferSnapshot, - /// The range of the buffer to be shown in the excerpt - range: ExcerptRange, - /// The last row in the excerpted slice of the buffer - max_buffer_row: BufferRow, - /// A summary of the text in the excerpt - text_summary: TextSummary, - has_trailing_newline: bool, -} - -/// A public view into an `Excerpt` in a [`MultiBuffer`]. -/// -/// Contains methods for getting the [`Buffer`] of the excerpt, -/// as well as mapping offsets to/from buffer and multibuffer coordinates. -#[derive(Clone)] -pub struct MultiBufferExcerpt<'a> { - excerpt: &'a Excerpt, - diff_transforms: - sum_tree::Cursor<'a, 'static, DiffTransform, DiffTransforms>, - /// The offset in the multibuffer considering diff transforms. - offset: MultiBufferOffset, - /// The offset in the multibuffer without diff transforms. - excerpt_offset: ExcerptOffset, - buffer_offset: BufferOffset, -} - -#[derive(Clone, Debug)] -struct ExcerptIdMapping { - id: ExcerptId, - locator: Locator, -} - -/// A range of text from a single [`Buffer`], to be shown as an `Excerpt`. -/// These ranges are relative to the buffer itself -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub struct ExcerptRange { - /// The full range of text to be shown in the excerpt. - pub context: Range, - /// The primary range of text to be highlighted in the excerpt. - /// In a multi-buffer search, this would be the text that matched the search - pub primary: Range, -} - -impl ExcerptRange { - pub fn new(context: Range) -> Self { - Self { - context: context.clone(), - primary: context, - } - } -} - -#[derive(Clone, Debug, Default)] -pub struct ExcerptSummary { - excerpt_id: ExcerptId, - /// The location of the last [`Excerpt`] being summarized - excerpt_locator: Locator, - widest_line_number: u32, - text: MBTextSummary, -} - -#[derive(Debug, Clone)] -pub struct DiffTransformSummary { - input: MBTextSummary, - output: MBTextSummary, -} - -/// Summary of a string of text. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub struct MBTextSummary { - /// Length in bytes. - pub len: MultiBufferOffset, - /// Length in UTF-8. - pub chars: usize, - /// Length in UTF-16 code units - pub len_utf16: OffsetUtf16, - /// A point representing the number of lines and the length of the last line. - /// - /// In other words, it marks the point after the last byte in the text, (if - /// EOF was a character, this would be its position). - pub lines: Point, - /// How many `char`s are in the first line - pub first_line_chars: u32, - /// How many `char`s are in the last line - pub last_line_chars: u32, - /// How many UTF-16 code units are in the last line - pub last_line_len_utf16: u32, - /// The row idx of the longest row - pub longest_row: u32, - /// How many `char`s are in the longest row - pub longest_row_chars: u32, -} - -impl From for MBTextSummary { - fn from(summary: TextSummary) -> Self { - MBTextSummary { - len: MultiBufferOffset(summary.len), - chars: summary.chars, - len_utf16: summary.len_utf16, - lines: summary.lines, - first_line_chars: summary.first_line_chars, - last_line_chars: summary.last_line_chars, - last_line_len_utf16: summary.last_line_len_utf16, - longest_row: summary.longest_row, - longest_row_chars: summary.longest_row_chars, - } - } -} -impl From<&str> for MBTextSummary { - fn from(text: &str) -> Self { - MBTextSummary::from(TextSummary::from(text)) - } -} - -impl MultiBufferDimension for MBTextSummary { - type TextDimension = TextSummary; - - fn from_summary(summary: &MBTextSummary) -> Self { - *summary - } - - fn add_text_dim(&mut self, summary: &Self::TextDimension) { - *self += *summary; - } - - fn add_mb_text_summary(&mut self, summary: &MBTextSummary) { - *self += *summary; - } -} - -impl AddAssign for MBTextSummary { - fn add_assign(&mut self, other: MBTextSummary) { - let joined_chars = self.last_line_chars + other.first_line_chars; - if joined_chars > self.longest_row_chars { - self.longest_row = self.lines.row; - self.longest_row_chars = joined_chars; - } - if other.longest_row_chars > self.longest_row_chars { - self.longest_row = self.lines.row + other.longest_row; - self.longest_row_chars = other.longest_row_chars; - } - - if self.lines.row == 0 { - self.first_line_chars += other.first_line_chars; - } - - if other.lines.row == 0 { - self.last_line_chars += other.first_line_chars; - self.last_line_len_utf16 += other.last_line_len_utf16; - } else { - self.last_line_chars = other.last_line_chars; - self.last_line_len_utf16 = other.last_line_len_utf16; - } - - self.chars += other.chars; - self.len += other.len; - self.len_utf16 += other.len_utf16; - self.lines += other.lines; - } -} - -impl AddAssign for MBTextSummary { - fn add_assign(&mut self, other: TextSummary) { - *self += MBTextSummary::from(other); - } -} - -impl MBTextSummary { - pub fn lines_utf16(&self) -> PointUtf16 { - PointUtf16 { - row: self.lines.row, - column: self.last_line_len_utf16, - } - } -} - -impl MultiBufferDimension for DimensionPair -where - K: MultiBufferDimension, - V: MultiBufferDimension, -{ - type TextDimension = DimensionPair; - - fn from_summary(summary: &MBTextSummary) -> Self { - Self { - key: K::from_summary(summary), - value: Some(V::from_summary(summary)), - } - } - - fn add_text_dim(&mut self, summary: &Self::TextDimension) { - self.key.add_text_dim(&summary.key); - if let Some(value) = &mut self.value { - if let Some(other_value) = summary.value.as_ref() { - value.add_text_dim(other_value); - } - } - } - - fn add_mb_text_summary(&mut self, summary: &MBTextSummary) { - self.key.add_mb_text_summary(summary); - if let Some(value) = &mut self.value { - value.add_mb_text_summary(summary); - } - } -} - -#[derive(Clone)] -pub struct MultiBufferRows<'a> { - point: Point, - is_empty: bool, - is_singleton: bool, - cursor: MultiBufferCursor<'a, Point, Point>, -} - -pub struct MultiBufferChunks<'a> { - excerpts: Cursor<'a, 'static, Excerpt, ExcerptOffset>, - diff_transforms: - Cursor<'a, 'static, DiffTransform, Dimensions>, - diffs: &'a TreeMap, - diff_base_chunks: Option<(BufferId, BufferChunks<'a>)>, - buffer_chunk: Option>, - range: Range, - excerpt_offset_range: Range, - excerpt_chunks: Option>, - language_aware: bool, -} - -pub struct ReversedMultiBufferChunks<'a> { - cursor: MultiBufferCursor<'a, MultiBufferOffset, BufferOffset>, - current_chunks: Option>, - start: MultiBufferOffset, - offset: MultiBufferOffset, -} - -pub struct MultiBufferBytes<'a> { - range: Range, - cursor: MultiBufferCursor<'a, MultiBufferOffset, BufferOffset>, - excerpt_bytes: Option>, - has_trailing_newline: bool, - chunk: &'a [u8], -} - -pub struct ReversedMultiBufferBytes<'a> { - range: Range, - chunks: ReversedMultiBufferChunks<'a>, - chunk: &'a [u8], -} - -#[derive(Clone)] -struct DiffTransforms { - output_dimension: OutputDimension, - excerpt_dimension: ExcerptDimension, -} - -impl<'a, MBD: MultiBufferDimension> Dimension<'a, DiffTransformSummary> for DiffTransforms { - fn zero(cx: ::Context<'_>) -> Self { - Self { - output_dimension: OutputDimension::zero(cx), - excerpt_dimension: as Dimension<'a, DiffTransformSummary>>::zero( - cx, - ), - } - } - - fn add_summary( - &mut self, - summary: &'a DiffTransformSummary, - cx: ::Context<'_>, - ) { - self.output_dimension.add_summary(summary, cx); - self.excerpt_dimension.add_summary(summary, cx); - } -} - -#[derive(Clone)] -struct MultiBufferCursor<'a, MBD, BD> { - excerpts: Cursor<'a, 'static, Excerpt, ExcerptDimension>, - diff_transforms: Cursor<'a, 'static, DiffTransform, DiffTransforms>, - snapshot: &'a MultiBufferSnapshot, - cached_region: Option>, -} - -/// Matches transformations to an item -/// This is essentially a more detailed version of DiffTransform -#[derive(Clone)] -struct MultiBufferRegion<'a, MBD, BD> { - buffer: &'a BufferSnapshot, - is_main_buffer: bool, - diff_hunk_status: Option, - excerpt: &'a Excerpt, - buffer_range: Range, - diff_base_byte_range: Option>, - range: Range, - has_trailing_newline: bool, -} - -impl<'a, MBD, BD> MultiBufferRegion<'a, MBD, BD> -where - MBD: Ord, - BD: Ord, -{ - fn is_filtered(&self) -> bool { - self.range.is_empty() && self.buffer_range.is_empty() && self.diff_hunk_status == None - } -} - -struct ExcerptChunks<'a> { - excerpt_id: ExcerptId, - content_chunks: BufferChunks<'a>, - has_footer: bool, -} - -#[derive(Debug)] -struct BufferEdit { - range: Range, - new_text: Arc, - is_insertion: bool, - original_indent_column: Option, - excerpt_id: ExcerptId, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -enum DiffChangeKind { - BufferEdited, - DiffUpdated { base_changed: bool }, - ExpandOrCollapseHunks { expand: bool }, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ExpandExcerptDirection { - Up, - Down, - UpAndDown, -} - -impl ExpandExcerptDirection { - pub fn should_expand_up(&self) -> bool { - match self { - ExpandExcerptDirection::Up => true, - ExpandExcerptDirection::Down => false, - ExpandExcerptDirection::UpAndDown => true, - } - } - - pub fn should_expand_down(&self) -> bool { - match self { - ExpandExcerptDirection::Up => false, - ExpandExcerptDirection::Down => true, - ExpandExcerptDirection::UpAndDown => true, - } - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct IndentGuide { - pub buffer_id: BufferId, - pub start_row: MultiBufferRow, - pub end_row: MultiBufferRow, - pub depth: u32, - pub tab_size: u32, - pub settings: IndentGuideSettings, -} - -impl IndentGuide { - pub fn indent_level(&self) -> u32 { - self.depth * self.tab_size - } -} - -impl MultiBuffer { - pub fn new(capability: Capability) -> Self { - Self::new_( - capability, - MultiBufferSnapshot { - show_headers: true, - ..MultiBufferSnapshot::default() - }, - ) - } - - pub fn without_headers(capability: Capability) -> Self { - Self::new_(capability, Default::default()) - } - - pub fn singleton(buffer: Entity, cx: &mut Context) -> Self { - let mut this = Self::new_( - buffer.read(cx).capability(), - MultiBufferSnapshot { - singleton: true, - ..MultiBufferSnapshot::default() - }, - ); - this.singleton = true; - let buffer_id = buffer.read(cx).remote_id(); - this.push_excerpts( - buffer, - [ExcerptRange::new(text::Anchor::min_max_range_for_buffer( - buffer_id, - ))], - cx, - ); - this - } - - #[inline] - pub fn new_(capability: Capability, snapshot: MultiBufferSnapshot) -> Self { - Self { - snapshot: RefCell::new(snapshot), - buffers: Default::default(), - diffs: HashMap::default(), - subscriptions: Topic::default(), - singleton: false, - capability, - title: None, - excerpts_by_path: Default::default(), - paths_by_excerpt: Default::default(), - buffer_changed_since_sync: Default::default(), - history: History::default(), - follower: None, - filter_mode: None, - } - } - - pub fn clone(&self, new_cx: &mut Context) -> Self { - let mut buffers = HashMap::default(); - let buffer_changed_since_sync = Rc::new(Cell::new(false)); - for (buffer_id, buffer_state) in self.buffers.iter() { - buffer_state.buffer.update(new_cx, |buffer, _| { - buffer.record_changes(Rc::downgrade(&buffer_changed_since_sync)); - }); - buffers.insert( - *buffer_id, - BufferState { - buffer: buffer_state.buffer.clone(), - last_version: buffer_state.last_version.clone(), - last_non_text_state_update_count: buffer_state - .last_non_text_state_update_count - .clone(), - excerpts: buffer_state.excerpts.clone(), - _subscriptions: [ - new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()), - new_cx.subscribe(&buffer_state.buffer, Self::on_buffer_event), - ], - }, - ); - } - let mut diff_bases = HashMap::default(); - for (buffer_id, diff) in self.diffs.iter() { - diff_bases.insert(*buffer_id, DiffState::new(diff.diff.clone(), new_cx)); - } - Self { - snapshot: RefCell::new(self.snapshot.borrow().clone()), - buffers: buffers, - excerpts_by_path: self.excerpts_by_path.clone(), - paths_by_excerpt: self.paths_by_excerpt.clone(), - diffs: diff_bases, - subscriptions: Default::default(), - singleton: self.singleton, - capability: self.capability, - history: self.history.clone(), - title: self.title.clone(), - buffer_changed_since_sync, - follower: None, - filter_mode: None, - } - } - - pub fn get_or_create_follower(&mut self, cx: &mut Context) -> Entity { - use gpui::AppContext as _; - - if let Some(follower) = &self.follower { - return follower.clone(); - } - - let follower = cx.new(|cx| self.clone(cx)); - follower.update(cx, |follower, _cx| { - follower.capability = Capability::ReadOnly; - }); - self.follower = Some(follower.clone()); - follower - } - - pub fn set_filter_mode(&mut self, new_mode: Option) { - self.filter_mode = new_mode; - let excerpt_len = self - .snapshot - .get_mut() - .diff_transforms - .summary() - .excerpt_len(); - let edits = Self::sync_diff_transforms( - self.snapshot.get_mut(), - vec![Edit { - old: ExcerptDimension(MultiBufferOffset(0))..excerpt_len, - new: ExcerptDimension(MultiBufferOffset(0))..excerpt_len, - }], - // TODO(split-diff) is this right? - DiffChangeKind::BufferEdited, - new_mode, - ); - if !edits.is_empty() { - self.subscriptions.publish(edits); - } - } - - pub fn set_group_interval(&mut self, group_interval: Duration) { - self.history.set_group_interval(group_interval); - } - - pub fn with_title(mut self, title: String) -> Self { - self.title = Some(title); - self - } - - pub fn read_only(&self) -> bool { - self.capability == Capability::ReadOnly - } - - /// Returns an up-to-date snapshot of the MultiBuffer. - #[ztracing::instrument(skip_all)] - pub fn snapshot(&self, cx: &App) -> MultiBufferSnapshot { - self.sync(cx); - self.snapshot.borrow().clone() - } - - pub fn read(&self, cx: &App) -> Ref<'_, MultiBufferSnapshot> { - self.sync(cx); - self.snapshot.borrow() - } - - pub fn as_singleton(&self) -> Option> { - if self.singleton { - Some(self.buffers.values().next().unwrap().buffer.clone()) - } else { - None - } - } - - pub fn is_singleton(&self) -> bool { - self.singleton - } - - pub fn subscribe(&mut self) -> Subscription { - self.subscriptions.subscribe() - } - - pub fn is_dirty(&self, cx: &App) -> bool { - self.read(cx).is_dirty() - } - - pub fn has_deleted_file(&self, cx: &App) -> bool { - self.read(cx).has_deleted_file() - } - - pub fn has_conflict(&self, cx: &App) -> bool { - self.read(cx).has_conflict() - } - - // The `is_empty` signature doesn't match what clippy expects - #[allow(clippy::len_without_is_empty)] - pub fn len(&self, cx: &App) -> MultiBufferOffset { - self.read(cx).len() - } - - pub fn is_empty(&self) -> bool { - self.buffers.is_empty() - } - - pub fn edit( - &mut self, - edits: I, - autoindent_mode: Option, - cx: &mut Context, - ) where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - if self.read_only() || self.buffers.is_empty() { - return; - } - self.sync_mut(cx); - let edits = edits - .into_iter() - .map(|(range, new_text)| { - let mut range = range.start.to_offset(self.snapshot.get_mut()) - ..range.end.to_offset(self.snapshot.get_mut()); - if range.start > range.end { - mem::swap(&mut range.start, &mut range.end); - } - (range, new_text.into()) - }) - .collect::>(); - - return edit_internal(self, edits, autoindent_mode, cx); - - // Non-generic part of edit, hoisted out to avoid blowing up LLVM IR. - fn edit_internal( - this: &mut MultiBuffer, - edits: Vec<(Range, Arc)>, - mut autoindent_mode: Option, - cx: &mut Context, - ) { - let original_indent_columns = match &mut autoindent_mode { - Some(AutoindentMode::Block { - original_indent_columns, - }) => mem::take(original_indent_columns), - _ => Default::default(), - }; - - let (buffer_edits, edited_excerpt_ids) = MultiBuffer::convert_edits_to_buffer_edits( - edits, - this.snapshot.get_mut(), - &original_indent_columns, - ); - - let mut buffer_ids = Vec::with_capacity(buffer_edits.len()); - for (buffer_id, mut edits) in buffer_edits { - buffer_ids.push(buffer_id); - edits.sort_by_key(|edit| edit.range.start); - this.buffers[&buffer_id].buffer.update(cx, |buffer, cx| { - let mut edits = edits.into_iter().peekable(); - let mut insertions = Vec::new(); - let mut original_indent_columns = Vec::new(); - let mut deletions = Vec::new(); - let empty_str: Arc = Arc::default(); - while let Some(BufferEdit { - mut range, - mut new_text, - mut is_insertion, - original_indent_column, - excerpt_id, - }) = edits.next() - { - while let Some(BufferEdit { - range: next_range, - is_insertion: next_is_insertion, - new_text: next_new_text, - excerpt_id: next_excerpt_id, - .. - }) = edits.peek() - { - if range.end >= next_range.start { - range.end = cmp::max(next_range.end, range.end); - is_insertion |= *next_is_insertion; - if excerpt_id == *next_excerpt_id { - new_text = format!("{new_text}{next_new_text}").into(); - } - edits.next(); - } else { - break; - } - } - - if is_insertion { - original_indent_columns.push(original_indent_column); - insertions.push(( - buffer.anchor_before(range.start)..buffer.anchor_before(range.end), - new_text.clone(), - )); - } else if !range.is_empty() { - deletions.push(( - buffer.anchor_before(range.start)..buffer.anchor_before(range.end), - empty_str.clone(), - )); - } - } - - let deletion_autoindent_mode = - if let Some(AutoindentMode::Block { .. }) = autoindent_mode { - Some(AutoindentMode::Block { - original_indent_columns: Default::default(), - }) - } else { - autoindent_mode.clone() - }; - let insertion_autoindent_mode = - if let Some(AutoindentMode::Block { .. }) = autoindent_mode { - Some(AutoindentMode::Block { - original_indent_columns, - }) - } else { - autoindent_mode.clone() - }; - - buffer.edit(deletions, deletion_autoindent_mode, cx); - buffer.edit(insertions, insertion_autoindent_mode, cx); - }) - } - - cx.emit(Event::ExcerptsEdited { - excerpt_ids: edited_excerpt_ids, - buffer_ids, - }); - } - } - - fn convert_edits_to_buffer_edits( - edits: Vec<(Range, Arc)>, - snapshot: &MultiBufferSnapshot, - original_indent_columns: &[Option], - ) -> (HashMap>, Vec) { - let mut buffer_edits: HashMap> = Default::default(); - let mut edited_excerpt_ids = Vec::new(); - let mut cursor = snapshot.cursor::(); - for (ix, (range, new_text)) in edits.into_iter().enumerate() { - let original_indent_column = original_indent_columns.get(ix).copied().flatten(); - - cursor.seek(&range.start); - let mut start_region = cursor.region().expect("start offset out of bounds"); - if !start_region.is_main_buffer { - cursor.next(); - if let Some(region) = cursor.region() { - start_region = region; - } else { - continue; - } - } - - if range.end < start_region.range.start { - continue; - } - - if range.end > start_region.range.end { - cursor.seek_forward(&range.end); - } - let mut end_region = cursor.region().expect("end offset out of bounds"); - if !end_region.is_main_buffer { - cursor.prev(); - if let Some(region) = cursor.region() { - end_region = region; - } else { - continue; - } - } - - if range.start > end_region.range.end { - continue; - } - - let start_overshoot = range.start.saturating_sub(start_region.range.start); - let end_overshoot = range.end.saturating_sub(end_region.range.start); - let buffer_start = (start_region.buffer_range.start + start_overshoot) - .min(start_region.buffer_range.end); - let buffer_end = - (end_region.buffer_range.start + end_overshoot).min(end_region.buffer_range.end); - - if start_region.excerpt.id == end_region.excerpt.id { - if start_region.is_main_buffer { - edited_excerpt_ids.push(start_region.excerpt.id); - buffer_edits - .entry(start_region.buffer.remote_id()) - .or_default() - .push(BufferEdit { - range: buffer_start..buffer_end, - new_text, - is_insertion: true, - original_indent_column, - excerpt_id: start_region.excerpt.id, - }); - } - } else { - let start_excerpt_range = buffer_start..start_region.buffer_range.end; - let end_excerpt_range = end_region.buffer_range.start..buffer_end; - if start_region.is_main_buffer { - edited_excerpt_ids.push(start_region.excerpt.id); - buffer_edits - .entry(start_region.buffer.remote_id()) - .or_default() - .push(BufferEdit { - range: start_excerpt_range, - new_text: new_text.clone(), - is_insertion: true, - original_indent_column, - excerpt_id: start_region.excerpt.id, - }); - } - if end_region.is_main_buffer { - edited_excerpt_ids.push(end_region.excerpt.id); - buffer_edits - .entry(end_region.buffer.remote_id()) - .or_default() - .push(BufferEdit { - range: end_excerpt_range, - new_text: new_text.clone(), - is_insertion: false, - original_indent_column, - excerpt_id: end_region.excerpt.id, - }); - } - - cursor.seek(&range.start); - cursor.next_excerpt(); - while let Some(region) = cursor.region() { - if region.excerpt.id == end_region.excerpt.id { - break; - } - if region.is_main_buffer { - edited_excerpt_ids.push(region.excerpt.id); - buffer_edits - .entry(region.buffer.remote_id()) - .or_default() - .push(BufferEdit { - range: region.buffer_range, - new_text: new_text.clone(), - is_insertion: false, - original_indent_column, - excerpt_id: region.excerpt.id, - }); - } - cursor.next_excerpt(); - } - } - } - (buffer_edits, edited_excerpt_ids) - } - - pub fn autoindent_ranges(&mut self, ranges: I, cx: &mut Context) - where - I: IntoIterator>, - S: ToOffset, - { - if self.read_only() || self.buffers.is_empty() { - return; - } - self.sync_mut(cx); - let empty = Arc::::from(""); - let edits = ranges - .into_iter() - .map(|range| { - let mut range = range.start.to_offset(self.snapshot.get_mut()) - ..range.end.to_offset(&self.snapshot.get_mut()); - if range.start > range.end { - mem::swap(&mut range.start, &mut range.end); - } - (range, empty.clone()) - }) - .collect::>(); - - return autoindent_ranges_internal(self, edits, cx); - - fn autoindent_ranges_internal( - this: &mut MultiBuffer, - edits: Vec<(Range, Arc)>, - cx: &mut Context, - ) { - let (buffer_edits, edited_excerpt_ids) = - MultiBuffer::convert_edits_to_buffer_edits(edits, this.snapshot.get_mut(), &[]); - - let mut buffer_ids = Vec::new(); - for (buffer_id, mut edits) in buffer_edits { - buffer_ids.push(buffer_id); - edits.sort_unstable_by_key(|edit| edit.range.start); - - 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; - } - ranges.push(edit.range); - } - - this.buffers[&buffer_id].buffer.update(cx, |buffer, cx| { - buffer.autoindent_ranges(ranges, cx); - }) - } - - cx.emit(Event::ExcerptsEdited { - excerpt_ids: edited_excerpt_ids, - buffer_ids, - }); - } - } - - /// Inserts newlines at the given position to create an empty line, returning the start of the new line. - /// You can also request the insertion of empty lines above and below the line starting at the returned point. - /// Panics if the given position is invalid. - pub fn insert_empty_line( - &mut self, - position: impl ToPoint, - space_above: bool, - space_below: bool, - cx: &mut Context, - ) -> Point { - let multibuffer_point = position.to_point(&self.read(cx)); - let (buffer, buffer_point, _) = self.point_to_buffer_point(multibuffer_point, cx).unwrap(); - self.start_transaction(cx); - let empty_line_start = buffer.update(cx, |buffer, cx| { - buffer.insert_empty_line(buffer_point, space_above, space_below, cx) - }); - self.end_transaction(cx); - multibuffer_point + (empty_line_start - buffer_point) - } - - pub fn set_active_selections( - &self, - selections: &[Selection], - line_mode: bool, - cursor_shape: CursorShape, - cx: &mut Context, - ) { - let mut selections_by_buffer: HashMap>> = - Default::default(); - let snapshot = self.read(cx); - let mut cursor = snapshot.excerpts.cursor::>(()); - for selection in selections { - let start_locator = snapshot.excerpt_locator_for_id(selection.start.excerpt_id); - let end_locator = snapshot.excerpt_locator_for_id(selection.end.excerpt_id); - - cursor.seek(&Some(start_locator), Bias::Left); - while let Some(excerpt) = cursor.item() - && excerpt.locator <= *end_locator - { - let mut start = excerpt.range.context.start; - let mut end = excerpt.range.context.end; - if excerpt.id == selection.start.excerpt_id { - start = selection.start.text_anchor; - } - if excerpt.id == selection.end.excerpt_id { - end = selection.end.text_anchor; - } - selections_by_buffer - .entry(excerpt.buffer_id) - .or_default() - .push(Selection { - id: selection.id, - start, - end, - reversed: selection.reversed, - goal: selection.goal, - }); - - cursor.next(); - } - } - - for (buffer_id, buffer_state) in self.buffers.iter() { - if !selections_by_buffer.contains_key(buffer_id) { - buffer_state - .buffer - .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); - } - } - - for (buffer_id, mut selections) in selections_by_buffer { - self.buffers[&buffer_id].buffer.update(cx, |buffer, cx| { - selections.sort_unstable_by(|a, b| a.start.cmp(&b.start, buffer)); - let mut selections = selections.into_iter().peekable(); - let merged_selections = Arc::from_iter(iter::from_fn(|| { - let mut selection = selections.next()?; - while let Some(next_selection) = selections.peek() { - if selection.end.cmp(&next_selection.start, buffer).is_ge() { - let next_selection = selections.next().unwrap(); - if next_selection.end.cmp(&selection.end, buffer).is_ge() { - selection.end = next_selection.end; - } - } else { - break; - } - } - Some(selection) - })); - buffer.set_active_selections(merged_selections, line_mode, cursor_shape, cx); - }); - } - } - - pub fn remove_active_selections(&self, cx: &mut Context) { - for buffer in self.buffers.values() { - buffer - .buffer - .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); - } - } - - pub fn push_excerpts( - &mut self, - buffer: Entity, - ranges: impl IntoIterator>, - cx: &mut Context, - ) -> Vec - where - O: text::ToOffset + Clone, - { - self.insert_excerpts_after(ExcerptId::max(), buffer, ranges, cx) - } - - #[instrument(skip_all)] - fn merge_excerpt_ranges<'a>( - expanded_ranges: impl IntoIterator> + 'a, - ) -> (Vec>, Vec) { - let mut merged_ranges: Vec> = Vec::new(); - let mut counts: Vec = Vec::new(); - for range in expanded_ranges { - if let Some(last_range) = merged_ranges.last_mut() { - assert!( - last_range.context.start <= range.context.start, - "ranges must be sorted: {last_range:?} <= {range:?}" - ); - if last_range.context.end >= range.context.start - || last_range.context.end.row + 1 == range.context.start.row - { - last_range.context.end = range.context.end.max(last_range.context.end); - *counts.last_mut().unwrap() += 1; - continue; - } - } - merged_ranges.push(range.clone()); - counts.push(1); - } - (merged_ranges, counts) - } - - pub fn insert_excerpts_after( - &mut self, - prev_excerpt_id: ExcerptId, - buffer: Entity, - ranges: impl IntoIterator>, - cx: &mut Context, - ) -> Vec - where - O: text::ToOffset + Clone, - { - let mut ids = Vec::new(); - let mut next_excerpt_id = - if let Some(last_entry) = self.snapshot.borrow().excerpt_ids.last() { - last_entry.id.0 + 1 - } else { - 1 - }; - self.insert_excerpts_with_ids_after( - prev_excerpt_id, - buffer, - ranges.into_iter().map(|range| { - let id = ExcerptId(post_inc(&mut next_excerpt_id)); - ids.push(id); - (id, range) - }), - cx, - ); - ids - } - - pub fn insert_excerpts_with_ids_after( - &mut self, - prev_excerpt_id: ExcerptId, - buffer: Entity, - ranges: impl IntoIterator)>, - cx: &mut Context, - ) where - O: text::ToOffset + Clone, - { - // TODO(split-diff) see if it's worth time avoiding collecting here later - let collected_ranges: Vec<_> = ranges.into_iter().collect(); - - assert_eq!(self.history.transaction_depth(), 0); - let mut ranges = collected_ranges.iter().cloned().peekable(); - if ranges.peek().is_none() { - return Default::default(); - } - - self.sync_mut(cx); - - let buffer_snapshot = buffer.read(cx).snapshot(); - let buffer_id = buffer_snapshot.remote_id(); - - let buffer_state = self.buffers.entry(buffer_id).or_insert_with(|| { - self.buffer_changed_since_sync.replace(true); - buffer.update(cx, |buffer, _| { - buffer.record_changes(Rc::downgrade(&self.buffer_changed_since_sync)); - }); - BufferState { - last_version: RefCell::new(buffer_snapshot.version().clone()), - last_non_text_state_update_count: Cell::new( - buffer_snapshot.non_text_state_update_count(), - ), - excerpts: Default::default(), - _subscriptions: [ - cx.observe(&buffer, |_, _, cx| cx.notify()), - cx.subscribe(&buffer, Self::on_buffer_event), - ], - buffer: buffer.clone(), - } - }); - - let mut snapshot = self.snapshot.get_mut(); - - let mut prev_locator = snapshot.excerpt_locator_for_id(prev_excerpt_id).clone(); - let mut new_excerpt_ids = mem::take(&mut snapshot.excerpt_ids); - let mut cursor = snapshot.excerpts.cursor::>(()); - let mut new_excerpts = cursor.slice(&prev_locator, Bias::Right); - prev_locator = cursor.start().unwrap_or(Locator::min_ref()).clone(); - - let edit_start = ExcerptDimension(new_excerpts.summary().text.len); - new_excerpts.update_last( - |excerpt| { - excerpt.has_trailing_newline = true; - }, - (), - ); - - let next_locator = if let Some(excerpt) = cursor.item() { - excerpt.locator.clone() - } else { - Locator::max() - }; - - let mut excerpts = Vec::new(); - while let Some((id, range)) = ranges.next() { - let locator = Locator::between(&prev_locator, &next_locator); - if let Err(ix) = buffer_state.excerpts.binary_search(&locator) { - buffer_state.excerpts.insert(ix, locator.clone()); - } - let range = ExcerptRange { - context: buffer_snapshot.anchor_before(&range.context.start) - ..buffer_snapshot.anchor_after(&range.context.end), - primary: buffer_snapshot.anchor_before(&range.primary.start) - ..buffer_snapshot.anchor_after(&range.primary.end), - }; - excerpts.push((id, range.clone())); - let excerpt = Excerpt::new( - id, - locator.clone(), - buffer_id, - buffer_snapshot.clone(), - range, - ranges.peek().is_some() || cursor.item().is_some(), - ); - new_excerpts.push(excerpt, ()); - prev_locator = locator.clone(); - - if let Some(last_mapping_entry) = new_excerpt_ids.last() { - assert!(id > last_mapping_entry.id, "excerpt ids must be increasing"); - } - new_excerpt_ids.push(ExcerptIdMapping { id, locator }, ()); - } - - let edit_end = ExcerptDimension(new_excerpts.summary().text.len); - - let suffix = cursor.suffix(); - let changed_trailing_excerpt = suffix.is_empty(); - new_excerpts.append(suffix, ()); - drop(cursor); - snapshot.excerpts = new_excerpts; - snapshot.excerpt_ids = new_excerpt_ids; - if changed_trailing_excerpt { - snapshot.trailing_excerpt_update_count += 1; - } - - let edits = Self::sync_diff_transforms( - &mut snapshot, - vec![Edit { - old: edit_start..edit_start, - new: edit_start..edit_end, - }], - DiffChangeKind::BufferEdited, - self.filter_mode, - ); - if !edits.is_empty() { - self.subscriptions.publish(edits); - } - - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| { - follower.insert_excerpts_with_ids_after( - prev_excerpt_id, - buffer.clone(), - collected_ranges, - cx, - ); - }) - } - - cx.emit(Event::Edited { - edited_buffer: None, - }); - cx.emit(Event::ExcerptsAdded { - buffer, - predecessor: prev_excerpt_id, - excerpts, - }); - cx.notify(); - } - - pub fn clear(&mut self, cx: &mut Context) { - self.sync_mut(cx); - let ids = self.excerpt_ids(); - let removed_buffer_ids = self.buffers.drain().map(|(id, _)| id).collect(); - self.excerpts_by_path.clear(); - self.paths_by_excerpt.clear(); - let MultiBufferSnapshot { - excerpts, - diffs: _, - diff_transforms: _, - non_text_state_update_count: _, - edit_count: _, - is_dirty, - has_deleted_file, - has_conflict, - singleton: _, - excerpt_ids: _, - replaced_excerpts, - trailing_excerpt_update_count, - all_diff_hunks_expanded: _, - show_headers: _, - } = self.snapshot.get_mut(); - let start = ExcerptDimension(MultiBufferOffset::ZERO); - let prev_len = ExcerptDimension(excerpts.summary().text.len); - *excerpts = Default::default(); - *trailing_excerpt_update_count += 1; - *is_dirty = false; - *has_deleted_file = false; - *has_conflict = false; - replaced_excerpts.clear(); - - let edits = Self::sync_diff_transforms( - self.snapshot.get_mut(), - vec![Edit { - old: start..prev_len, - new: start..start, - }], - DiffChangeKind::BufferEdited, - self.filter_mode, - ); - if !edits.is_empty() { - self.subscriptions.publish(edits); - } - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| { - follower.clear(cx); - }) - } - cx.emit(Event::Edited { - edited_buffer: None, - }); - cx.emit(Event::ExcerptsRemoved { - ids, - removed_buffer_ids, - }); - cx.notify(); - } - - #[ztracing::instrument(skip_all)] - pub fn excerpts_for_buffer( - &self, - buffer_id: BufferId, - cx: &App, - ) -> Vec<(ExcerptId, ExcerptRange)> { - let mut excerpts = Vec::new(); - let snapshot = self.read(cx); - let mut cursor = snapshot.excerpts.cursor::>(()); - if let Some(locators) = self.buffers.get(&buffer_id).map(|state| &state.excerpts) { - for locator in locators { - cursor.seek_forward(&Some(locator), Bias::Left); - if let Some(excerpt) = cursor.item() - && excerpt.locator == *locator - { - excerpts.push((excerpt.id, excerpt.range.clone())); - } - } - } - - excerpts - } - - pub fn excerpt_ranges_for_buffer(&self, buffer_id: BufferId, cx: &App) -> Vec> { - let snapshot = self.read(cx); - let mut excerpts = snapshot - .excerpts - .cursor::, ExcerptPoint>>(()); - let mut diff_transforms = snapshot - .diff_transforms - .cursor::>>(()); - diff_transforms.next(); - let locators = self - .buffers - .get(&buffer_id) - .into_iter() - .flat_map(|state| &state.excerpts); - 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; - let excerpt_end = excerpt_start + excerpt.text_summary.lines; - - diff_transforms.seek_forward(&excerpt_start, Bias::Left); - let overshoot = excerpt_start - diff_transforms.start().0; - let start = diff_transforms.start().1 + overshoot; - - diff_transforms.seek_forward(&excerpt_end, Bias::Right); - let overshoot = excerpt_end - diff_transforms.start().0; - let end = diff_transforms.start().1 + overshoot; - - result.push(start.0..end.0) - } - } - result - } - - pub fn excerpt_buffer_ids(&self) -> Vec { - self.snapshot - .borrow() - .excerpts - .iter() - .map(|entry| entry.buffer_id) - .collect() - } - - pub fn excerpt_ids(&self) -> Vec { - self.snapshot - .borrow() - .excerpts - .iter() - .map(|entry| entry.id) - .collect() - } - - pub fn excerpt_containing( - &self, - position: impl ToOffset, - cx: &App, - ) -> Option<(ExcerptId, Entity, Range)> { - let snapshot = self.read(cx); - let offset = position.to_offset(&snapshot); - - let mut cursor = snapshot.cursor::(); - cursor.seek(&offset); - cursor - .excerpt() - .or_else(|| snapshot.excerpts.last()) - .map(|excerpt| { - ( - excerpt.id, - self.buffers.get(&excerpt.buffer_id).unwrap().buffer.clone(), - excerpt.range.context.clone(), - ) - }) - } - - pub fn buffer_for_anchor(&self, anchor: Anchor, cx: &App) -> Option> { - if let Some(buffer_id) = anchor.text_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, - point: T, - cx: &App, - ) -> Option<(Entity, BufferOffset)> { - let snapshot = self.read(cx); - let (buffer, offset) = snapshot.point_to_buffer_offset(point)?; - Some(( - self.buffers.get(&buffer.remote_id())?.buffer.clone(), - offset, - )) - } - - // If point is at the end of the buffer, the last excerpt is returned - pub fn point_to_buffer_point( - &self, - point: T, - cx: &App, - ) -> Option<(Entity, Point, ExcerptId)> { - let snapshot = self.read(cx); - let (buffer, point, is_main_buffer) = - snapshot.point_to_buffer_point(point.to_point(&snapshot))?; - Some(( - self.buffers.get(&buffer.remote_id())?.buffer.clone(), - point, - is_main_buffer, - )) - } - - pub fn buffer_point_to_anchor( - &self, - buffer: &Entity, - point: Point, - cx: &App, - ) -> Option { - let mut found = None; - let snapshot = buffer.read(cx).snapshot(); - for (excerpt_id, range) in self.excerpts_for_buffer(snapshot.remote_id(), cx) { - let start = range.context.start.to_point(&snapshot); - let end = range.context.end.to_point(&snapshot); - if start <= point && point < end { - found = Some((snapshot.clip_point(point, Bias::Left), excerpt_id)); - break; - } - if point < start { - found = Some((start, excerpt_id)); - } - if point > end { - found = Some((end, excerpt_id)); - } - } - - found.map(|(point, excerpt_id)| { - let text_anchor = snapshot.anchor_after(point); - Anchor::in_buffer(excerpt_id, text_anchor) - }) - } - - pub fn buffer_anchor_to_anchor( - &self, - buffer: &Entity, - anchor: text::Anchor, - cx: &App, - ) -> Option { - let snapshot = buffer.read(cx).snapshot(); - for (excerpt_id, range) in self.excerpts_for_buffer(snapshot.remote_id(), cx) { - if range.context.start.cmp(&anchor, &snapshot).is_le() - && range.context.end.cmp(&anchor, &snapshot).is_ge() - { - return Some(Anchor::in_buffer(excerpt_id, anchor)); - } - } - - None - } - - pub fn remove_excerpts( - &mut self, - excerpt_ids: impl IntoIterator, - cx: &mut Context, - ) { - self.sync_mut(cx); - let ids = excerpt_ids.into_iter().collect::>(); - if ids.is_empty() { - return; - } - self.buffer_changed_since_sync.replace(true); - - let mut snapshot = self.snapshot.get_mut(); - let mut new_excerpts = SumTree::default(); - let mut cursor = snapshot - .excerpts - .cursor::, ExcerptOffset>>(()); - let mut edits = Vec::new(); - let mut excerpt_ids = ids.iter().copied().peekable(); - let mut removed_buffer_ids = Vec::new(); - - while let Some(excerpt_id) = excerpt_ids.next() { - self.paths_by_excerpt.remove(&excerpt_id); - // Seek to the next excerpt to remove, preserving any preceding excerpts. - let locator = snapshot.excerpt_locator_for_id(excerpt_id); - new_excerpts.append(cursor.slice(&Some(locator), Bias::Left), ()); - - if let Some(mut excerpt) = cursor.item() { - if excerpt.id != excerpt_id { - continue; - } - let mut old_start = cursor.start().1; - - // Skip over the removed excerpt. - 'remove_excerpts: loop { - if let Some(buffer_state) = self.buffers.get_mut(&excerpt.buffer_id) { - buffer_state.excerpts.retain(|l| l != &excerpt.locator); - if buffer_state.excerpts.is_empty() { - log::debug!( - "removing buffer and diff for buffer {}", - excerpt.buffer_id - ); - self.buffers.remove(&excerpt.buffer_id); - removed_buffer_ids.push(excerpt.buffer_id); - } - } - cursor.next(); - - // 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; - } - } - - break; - } - - // When removing the last excerpt, remove the trailing newline from - // the previous excerpt. - if cursor.item().is_none() && old_start > MultiBufferOffset::ZERO { - old_start -= 1; - new_excerpts.update_last(|e| e.has_trailing_newline = false, ()); - } - - // Push an edit for the removal of this run of excerpts. - let old_end = cursor.start().1; - let new_start = ExcerptDimension(new_excerpts.summary().text.len); - edits.push(Edit { - old: old_start..old_end, - new: new_start..new_start, - }); - } - } - let suffix = cursor.suffix(); - let changed_trailing_excerpt = suffix.is_empty(); - new_excerpts.append(suffix, ()); - drop(cursor); - snapshot.excerpts = new_excerpts; - for buffer_id in &removed_buffer_ids { - self.diffs.remove(buffer_id); - snapshot.diffs.remove(buffer_id); - } - - if changed_trailing_excerpt { - snapshot.trailing_excerpt_update_count += 1; - } - - let edits = Self::sync_diff_transforms( - &mut snapshot, - edits, - DiffChangeKind::BufferEdited, - self.filter_mode, - ); - if !edits.is_empty() { - self.subscriptions.publish(edits); - } - - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| { - follower.remove_excerpts(ids.clone(), cx); - }) - } - - cx.emit(Event::Edited { - edited_buffer: None, - }); - cx.emit(Event::ExcerptsRemoved { - ids, - removed_buffer_ids, - }); - cx.notify(); - } - - pub fn wait_for_anchors<'a, Anchors: 'a + Iterator>( - &self, - anchors: Anchors, - cx: &mut Context, - ) -> impl 'static + Future> + use { - let mut error = None; - let mut futures = Vec::new(); - for anchor in anchors { - if let Some(buffer_id) = anchor.text_anchor.buffer_id { - if let Some(buffer) = self.buffers.get(&buffer_id) { - buffer.buffer.update(cx, |buffer, _| { - futures.push(buffer.wait_for_anchors([anchor.text_anchor])) - }); - } else { - error = Some(anyhow!( - "buffer {buffer_id} is not part of this multi-buffer" - )); - break; - } - } - } - async move { - if let Some(error) = error { - Err(error)?; - } - for future in futures { - future.await?; - } - Ok(()) - } - } - - pub fn text_anchor_for_position( - &self, - position: T, - cx: &App, - ) -> Option<(Entity, language::Anchor)> { - let snapshot = self.read(cx); - let anchor = snapshot.anchor_before(position); - let buffer = self - .buffers - .get(&anchor.text_anchor.buffer_id?)? - .buffer - .clone(); - Some((buffer, anchor.text_anchor)) - } - - fn on_buffer_event( - &mut self, - buffer: Entity, - event: &language::BufferEvent, - cx: &mut Context, - ) { - use language::BufferEvent; - let buffer_id = buffer.read(cx).remote_id(); - cx.emit(match event { - BufferEvent::Edited => Event::Edited { - edited_buffer: Some(buffer), - }, - BufferEvent::DirtyChanged => Event::DirtyChanged, - BufferEvent::Saved => Event::Saved, - BufferEvent::FileHandleChanged => Event::FileHandleChanged, - BufferEvent::Reloaded => Event::Reloaded, - BufferEvent::LanguageChanged(has_language) => { - Event::LanguageChanged(buffer_id, *has_language) - } - BufferEvent::Reparsed => Event::Reparsed(buffer_id), - BufferEvent::DiagnosticsUpdated => Event::DiagnosticsUpdated, - BufferEvent::CapabilityChanged => { - self.capability = buffer.read(cx).capability(); - return; - } - BufferEvent::Operation { .. } | BufferEvent::ReloadNeeded => return, - }); - } - - fn buffer_diff_language_changed(&mut self, diff: Entity, cx: &mut Context) { - let diff = diff.read(cx); - let buffer_id = diff.buffer_id; - let diff = diff.snapshot(cx); - self.snapshot.get_mut().diffs.insert(buffer_id, diff); - } - - fn buffer_diff_changed( - &mut self, - diff: Entity, - range: Range, - cx: &mut Context, - ) { - self.sync_mut(cx); - - let diff = diff.read(cx); - let buffer_id = diff.buffer_id; - let Some(buffer_state) = self.buffers.get(&buffer_id) else { - return; - }; - self.buffer_changed_since_sync.replace(true); - - let buffer = buffer_state.buffer.read(cx); - let diff_change_range = range.to_offset(buffer); - - let new_diff = diff.snapshot(cx); - let mut snapshot = self.snapshot.get_mut(); - let base_text_changed = snapshot - .diffs - .get(&buffer_id) - .is_none_or(|old_diff| !new_diff.base_texts_eq(old_diff)); - - snapshot.diffs.insert_or_replace(buffer_id, new_diff); - - let mut excerpt_edits = Vec::new(); - for locator in &buffer_state.excerpts { - let mut cursor = snapshot - .excerpts - .cursor::, 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; - } - let excerpt_start = cursor.start().1; - let excerpt_len = excerpt.text_summary.len; - let diff_change_start_in_excerpt = diff_change_range - .start - .saturating_sub(excerpt_buffer_range.start); - let diff_change_end_in_excerpt = 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 edits = Self::sync_diff_transforms( - &mut snapshot, - excerpt_edits, - DiffChangeKind::DiffUpdated { - base_changed: base_text_changed, - }, - self.filter_mode, - ); - if !edits.is_empty() { - self.subscriptions.publish(edits); - } - cx.emit(Event::Edited { - edited_buffer: None, - }); - } - - pub fn all_buffers(&self) -> HashSet> { - self.buffers - .values() - .map(|state| state.buffer.clone()) - .collect() - } - - pub fn all_buffer_ids(&self) -> Vec { - self.buffers.keys().copied().collect() - } - - pub fn buffer(&self, buffer_id: BufferId) -> Option> { - self.buffers - .get(&buffer_id) - .map(|state| state.buffer.clone()) - } - - pub fn language_at(&self, point: T, cx: &App) -> Option> { - self.point_to_buffer_offset(point, cx) - .and_then(|(buffer, offset)| buffer.read(cx).language_at(offset)) - } - - pub fn language_settings<'a>(&'a self, cx: &'a App) -> Cow<'a, LanguageSettings> { - let buffer_id = self - .snapshot - .borrow() - .excerpts - .first() - .map(|excerpt| excerpt.buffer.remote_id()); - buffer_id - .and_then(|buffer_id| self.buffer(buffer_id)) - .map(|buffer| { - let buffer = buffer.read(cx); - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) - }) - .unwrap_or_else(move || self.language_settings_at(MultiBufferOffset::default(), cx)) - } - - pub fn language_settings_at<'a, T: ToOffset>( - &'a self, - point: T, - cx: &'a App, - ) -> Cow<'a, LanguageSettings> { - let mut language = None; - let mut file = None; - if let Some((buffer, offset)) = self.point_to_buffer_offset(point, cx) { - let buffer = buffer.read(cx); - language = buffer.language_at(offset); - file = buffer.file(); - } - language_settings(language.map(|l| l.name()), file, cx) - } - - pub fn for_each_buffer(&self, mut f: impl FnMut(&Entity)) { - self.buffers.values().for_each(|state| f(&state.buffer)) - } - - pub fn explicit_title(&self) -> Option<&str> { - self.title.as_deref() - } - - pub fn title<'a>(&'a self, cx: &'a App) -> Cow<'a, str> { - if let Some(title) = self.title.as_ref() { - return title.into(); - } - - if let Some(buffer) = self.as_singleton() { - let buffer = buffer.read(cx); - - if let Some(file) = buffer.file() { - return file.file_name(cx).into(); - } - - if let Some(title) = self.buffer_content_title(buffer) { - return title; - } - }; - - "untitled".into() - } - - fn buffer_content_title(&self, buffer: &Buffer) -> Option> { - let mut is_leading_whitespace = true; - let mut count = 0; - let mut prev_was_space = false; - let mut title = String::new(); - - for ch in buffer.snapshot().chars() { - if is_leading_whitespace && ch.is_whitespace() { - continue; - } - - is_leading_whitespace = false; - - if ch == '\n' || count >= 40 { - break; - } - - if ch.is_whitespace() { - if !prev_was_space { - title.push(' '); - count += 1; - prev_was_space = true; - } - } else { - title.push(ch); - count += 1; - prev_was_space = false; - } - } - - let title = title.trim_end().to_string(); - - if title.is_empty() { - return None; - } - - Some(title.into()) - } - - pub fn set_title(&mut self, title: String, cx: &mut Context) { - self.title = Some(title); - cx.notify(); - } - - /// Preserve preview tabs containing this multibuffer until additional edits occur. - pub fn refresh_preview(&self, cx: &mut Context) { - for buffer_state in self.buffers.values() { - buffer_state - .buffer - .update(cx, |buffer, _cx| buffer.refresh_preview()); - } - } - - /// Whether we should preserve the preview status of a tab containing this multi-buffer. - pub fn preserve_preview(&self, cx: &App) -> bool { - self.buffers - .values() - .all(|state| state.buffer.read(cx).preserve_preview()) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn is_parsing(&self, cx: &App) -> bool { - self.as_singleton().unwrap().read(cx).is_parsing() - } - - pub fn add_diff(&mut self, diff: Entity, cx: &mut Context) { - let buffer_id = diff.read(cx).buffer_id; - self.buffer_diff_changed( - diff.clone(), - text::Anchor::min_max_range_for_buffer(buffer_id), - cx, - ); - self.diffs - .insert(buffer_id, DiffState::new(diff.clone(), cx)); - - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| { - follower.add_diff(diff, cx); - }) - } - } - - pub fn diff_for(&self, buffer_id: BufferId) -> Option> { - self.diffs.get(&buffer_id).map(|state| state.diff.clone()) - } - - pub fn expand_diff_hunks(&mut self, ranges: Vec>, cx: &mut Context) { - self.expand_or_collapse_diff_hunks(ranges, true, cx); - } - - pub fn collapse_diff_hunks(&mut self, ranges: Vec>, cx: &mut Context) { - self.expand_or_collapse_diff_hunks(ranges, false, cx); - } - - pub fn set_all_diff_hunks_expanded(&mut self, cx: &mut Context) { - self.snapshot.get_mut().all_diff_hunks_expanded = true; - self.expand_or_collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], true, cx); - } - - pub fn all_diff_hunks_expanded(&self) -> bool { - self.snapshot.borrow().all_diff_hunks_expanded - } - - pub fn set_all_diff_hunks_collapsed(&mut self, cx: &mut Context) { - self.snapshot.get_mut().all_diff_hunks_expanded = false; - self.expand_or_collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], false, cx); - } - - pub fn has_multiple_hunks(&self, cx: &App) -> bool { - self.read(cx) - .diff_hunks_in_range(Anchor::min()..Anchor::max()) - .nth(1) - .is_some() - } - - pub fn single_hunk_is_expanded(&self, range: Range, cx: &App) -> bool { - let snapshot = self.read(cx); - let mut cursor = snapshot.diff_transforms.cursor::(()); - let offset_range = range.to_offset(&snapshot); - cursor.seek(&offset_range.start, Bias::Left); - while let Some(item) = cursor.item() { - if *cursor.start() >= offset_range.end && *cursor.start() > offset_range.start { - break; - } - if item.hunk_info().is_some() { - return true; - } - cursor.next(); - } - false - } - - pub fn has_expanded_diff_hunks_in_ranges(&self, ranges: &[Range], cx: &App) -> bool { - let snapshot = self.read(cx); - let mut cursor = snapshot.diff_transforms.cursor::(()); - for range in ranges { - let range = range.to_point(&snapshot); - let start = snapshot.point_to_offset(Point::new(range.start.row, 0)); - let end = snapshot.point_to_offset(Point::new(range.end.row + 1, 0)); - let start = start.saturating_sub_usize(1); - let end = snapshot.len().min(end + 1usize); - cursor.seek(&start, Bias::Right); - while let Some(item) = cursor.item() { - if *cursor.start() >= end { - break; - } - if item.hunk_info().is_some() { - return true; - } - cursor.next(); - } - } - false - } - - pub fn expand_or_collapse_diff_hunks_inner( - &mut self, - ranges: impl IntoIterator, ExcerptId)>, - expand: bool, - cx: &mut Context, - ) { - if self.snapshot.borrow().all_diff_hunks_expanded && !expand { - return; - } - self.sync_mut(cx); - let mut snapshot = self.snapshot.get_mut(); - let mut excerpt_edits = Vec::new(); - let mut last_hunk_row = None; - for (range, end_excerpt_id) in ranges { - for diff_hunk in snapshot.diff_hunks_in_range(range) { - 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) { - continue; - } - let start = Anchor::in_buffer(diff_hunk.excerpt_id, diff_hunk.buffer_range.start); - let end = Anchor::in_buffer(diff_hunk.excerpt_id, diff_hunk.buffer_range.end); - let start = snapshot.excerpt_offset_for_anchor(&start); - let end = snapshot.excerpt_offset_for_anchor(&end); - last_hunk_row = Some(diff_hunk.row_range.start); - excerpt_edits.push(text::Edit { - old: start..end, - new: start..end, - }); - } - } - - let edits = Self::sync_diff_transforms( - &mut snapshot, - excerpt_edits, - DiffChangeKind::ExpandOrCollapseHunks { expand }, - self.filter_mode, - ); - if !edits.is_empty() { - self.subscriptions.publish(edits); - } - cx.emit(Event::DiffHunksToggled); - cx.emit(Event::Edited { - edited_buffer: None, - }); - } - - pub fn expand_or_collapse_diff_hunks( - &mut self, - ranges: Vec>, - expand: bool, - cx: &mut Context, - ) { - let snapshot = self.snapshot.borrow().clone(); - let ranges = ranges.iter().map(move |range| { - let end_excerpt_id = range.end.excerpt_id; - let range = range.to_point(&snapshot); - let mut peek_end = range.end; - if range.end.row < snapshot.max_row().0 { - peek_end = Point::new(range.end.row + 1, 0); - }; - (range.start..peek_end, end_excerpt_id) - }); - self.expand_or_collapse_diff_hunks_inner(ranges, expand, cx); - } - - pub fn resize_excerpt( - &mut self, - id: ExcerptId, - range: Range, - cx: &mut Context, - ) { - self.sync_mut(cx); - - let mut snapshot = self.snapshot.get_mut(); - let locator = snapshot.excerpt_locator_for_id(id); - let mut new_excerpts = SumTree::default(); - let mut cursor = snapshot - .excerpts - .cursor::, ExcerptOffset>>(()); - let mut edits = Vec::>::new(); - - let prefix = cursor.slice(&Some(locator), Bias::Left); - new_excerpts.append(prefix, ()); - - let mut excerpt = cursor.item().unwrap().clone(); - let old_text_len = excerpt.text_summary.len; - - excerpt.range.context.start = range.start; - excerpt.range.context.end = range.end; - excerpt.max_buffer_row = range.end.to_point(&excerpt.buffer).row; - - excerpt.text_summary = excerpt - .buffer - .text_summary_for_range(excerpt.range.context.clone()); - - let new_start_offset = ExcerptDimension(new_excerpts.summary().text.len); - let old_start_offset = cursor.start().1; - let new_text_len = excerpt.text_summary.len; - let edit = Edit { - old: old_start_offset..old_start_offset + old_text_len, - new: new_start_offset..new_start_offset + new_text_len, - }; - - if let Some(last_edit) = edits.last_mut() { - if last_edit.old.end == edit.old.start { - last_edit.old.end = edit.old.end; - last_edit.new.end = edit.new.end; - } else { - edits.push(edit); - } - } else { - edits.push(edit); - } - - new_excerpts.push(excerpt, ()); - - cursor.next(); - - new_excerpts.append(cursor.suffix(), ()); - - drop(cursor); - snapshot.excerpts = new_excerpts; - - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| follower.resize_excerpt(id, range, cx)); - } - - let edits = Self::sync_diff_transforms( - &mut snapshot, - edits, - DiffChangeKind::BufferEdited, - self.filter_mode, - ); - if !edits.is_empty() { - self.subscriptions.publish(edits); - } - cx.emit(Event::Edited { - edited_buffer: None, - }); - cx.emit(Event::ExcerptsExpanded { ids: vec![id] }); - cx.notify(); - } - - pub fn expand_excerpts( - &mut self, - ids: impl IntoIterator, - line_count: u32, - direction: ExpandExcerptDirection, - cx: &mut Context, - ) { - if line_count == 0 { - return; - } - self.sync_mut(cx); - if !self.excerpts_by_path.is_empty() { - self.expand_excerpts_with_paths(ids, line_count, direction, cx); - return; - } - let mut snapshot = self.snapshot.get_mut(); - - let ids = ids.into_iter().collect::>(); - let locators = snapshot.excerpt_locators_for_ids(ids.iter().copied()); - let mut new_excerpts = SumTree::default(); - let mut cursor = snapshot - .excerpts - .cursor::, ExcerptOffset>>(()); - let mut excerpt_edits = Vec::>::new(); - - for locator in &locators { - let prefix = cursor.slice(&Some(locator), Bias::Left); - new_excerpts.append(prefix, ()); - - let mut excerpt = cursor.item().unwrap().clone(); - let old_text_len = excerpt.text_summary.len; - - let up_line_count = if direction.should_expand_up() { - line_count - } else { - 0 - }; - - let start_row = excerpt - .range - .context - .start - .to_point(&excerpt.buffer) - .row - .saturating_sub(up_line_count); - let start_point = Point::new(start_row, 0); - excerpt.range.context.start = excerpt.buffer.anchor_before(start_point); - - let down_line_count = if direction.should_expand_down() { - line_count - } else { - 0 - }; - - let mut end_point = excerpt.buffer.clip_point( - excerpt.range.context.end.to_point(&excerpt.buffer) - + Point::new(down_line_count, 0), - Bias::Left, - ); - end_point.column = excerpt.buffer.line_len(end_point.row); - excerpt.range.context.end = excerpt.buffer.anchor_after(end_point); - excerpt.max_buffer_row = end_point.row; - - excerpt.text_summary = excerpt - .buffer - .text_summary_for_range(excerpt.range.context.clone()); - - let new_start_offset = ExcerptDimension(new_excerpts.summary().text.len); - let old_start_offset = cursor.start().1; - let new_text_len = excerpt.text_summary.len; - let edit = Edit { - old: old_start_offset..old_start_offset + old_text_len, - new: new_start_offset..new_start_offset + new_text_len, - }; - - if let Some(last_edit) = excerpt_edits.last_mut() { - if last_edit.old.end == edit.old.start { - last_edit.old.end = edit.old.end; - last_edit.new.end = edit.new.end; - } else { - excerpt_edits.push(edit); - } - } else { - excerpt_edits.push(edit); - } - - new_excerpts.push(excerpt, ()); - - cursor.next(); - } - - new_excerpts.append(cursor.suffix(), ()); - - drop(cursor); - snapshot.excerpts = new_excerpts.clone(); - - let edits = Self::sync_diff_transforms( - &mut snapshot, - excerpt_edits.clone(), - DiffChangeKind::BufferEdited, - self.filter_mode, - ); - if !edits.is_empty() { - self.subscriptions.publish(edits); - } - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| { - follower.expand_excerpts(ids.clone(), line_count, direction, cx); - }) - } - cx.emit(Event::Edited { - edited_buffer: None, - }); - cx.emit(Event::ExcerptsExpanded { ids }); - cx.notify(); - } - - #[ztracing::instrument(skip_all)] - fn sync(&self, cx: &App) { - let changed = self.buffer_changed_since_sync.replace(false); - if !changed { - return; - } - let edits = Self::sync_from_buffer_changes( - &mut self.snapshot.borrow_mut(), - &self.buffers, - &self.diffs, - self.filter_mode, - cx, - ); - if !edits.is_empty() { - self.subscriptions.publish(edits); - } - } - - fn sync_mut(&mut self, cx: &App) { - let changed = self.buffer_changed_since_sync.replace(false); - if !changed { - return; - } - let edits = Self::sync_from_buffer_changes( - self.snapshot.get_mut(), - &self.buffers, - &self.diffs, - self.filter_mode, - cx, - ); - - if !edits.is_empty() { - self.subscriptions.publish(edits); - } - } - - fn sync_from_buffer_changes( - snapshot: &mut MultiBufferSnapshot, - buffers: &HashMap, - diffs: &HashMap, - filter_mode: Option, - cx: &App, - ) -> Vec> { - let MultiBufferSnapshot { - excerpts, - diffs: buffer_diff, - diff_transforms: _, - non_text_state_update_count, - edit_count, - is_dirty, - has_deleted_file, - has_conflict, - singleton: _, - excerpt_ids: _, - replaced_excerpts: _, - trailing_excerpt_update_count: _, - all_diff_hunks_expanded: _, - show_headers: _, - } = snapshot; - *is_dirty = false; - *has_deleted_file = false; - *has_conflict = false; - - let mut excerpts_to_edit = Vec::new(); - let mut non_text_state_updated = false; - let mut edited = false; - for buffer_state in buffers.values() { - let buffer = buffer_state.buffer.read(cx); - let version = buffer.version(); - let non_text_state_update_count = buffer.non_text_state_update_count(); - - let buffer_edited = version.changed_since(&buffer_state.last_version.borrow()); - let buffer_non_text_state_updated = - non_text_state_update_count > buffer_state.last_non_text_state_update_count.get(); - if buffer_edited || buffer_non_text_state_updated { - *buffer_state.last_version.borrow_mut() = version; - buffer_state - .last_non_text_state_update_count - .set(non_text_state_update_count); - excerpts_to_edit.extend( - buffer_state - .excerpts - .iter() - .map(|locator| (locator, buffer_state.buffer.clone(), buffer_edited)), - ); - } - - edited |= buffer_edited; - non_text_state_updated |= buffer_non_text_state_updated; - *is_dirty |= buffer.is_dirty(); - *has_deleted_file |= buffer - .file() - .is_some_and(|file| file.disk_state() == DiskState::Deleted); - *has_conflict |= buffer.has_conflict(); - } - if edited { - *edit_count += 1; - } - if non_text_state_updated { - *non_text_state_update_count += 1; - } - - for (id, diff) in diffs.iter() { - if buffer_diff.get(id).is_none() { - buffer_diff.insert(*id, diff.diff.read(cx).snapshot(cx)); - } - } - - excerpts_to_edit.sort_unstable_by_key(|(locator, _, _)| *locator); - - let mut edits = Vec::new(); - let mut new_excerpts = SumTree::default(); - let mut cursor = excerpts.cursor::, ExcerptOffset>>(()); - - for (locator, buffer, buffer_edited) in excerpts_to_edit { - new_excerpts.append(cursor.slice(&Some(locator), Bias::Left), ()); - let old_excerpt = cursor.item().unwrap(); - let buffer = buffer.read(cx); - let buffer_id = buffer.remote_id(); - - let mut new_excerpt; - if buffer_edited { - edits.extend( - buffer - .edits_since_in_range::( - old_excerpt.buffer.version(), - old_excerpt.range.context.clone(), - ) - .map(|edit| { - let excerpt_old_start = cursor.start().1; - let excerpt_new_start = - ExcerptDimension(new_excerpts.summary().text.len); - let old_start = excerpt_old_start + edit.old.start; - let old_end = excerpt_old_start + edit.old.end; - let new_start = excerpt_new_start + edit.new.start; - let new_end = excerpt_new_start + edit.new.end; - Edit { - old: old_start..old_end, - new: new_start..new_end, - } - }), - ); - - new_excerpt = Excerpt::new( - old_excerpt.id, - locator.clone(), - buffer_id, - buffer.snapshot(), - old_excerpt.range.clone(), - old_excerpt.has_trailing_newline, - ); - } else { - new_excerpt = old_excerpt.clone(); - new_excerpt.buffer = buffer.snapshot(); - } - - new_excerpts.push(new_excerpt, ()); - cursor.next(); - } - new_excerpts.append(cursor.suffix(), ()); - - drop(cursor); - *excerpts = new_excerpts; - Self::sync_diff_transforms(snapshot, edits, DiffChangeKind::BufferEdited, filter_mode) - } - - fn sync_diff_transforms( - snapshot: &mut MultiBufferSnapshot, - excerpt_edits: Vec>, - change_kind: DiffChangeKind, - filter_mode: Option, - ) -> Vec> { - if excerpt_edits.is_empty() { - return vec![]; - } - - let mut excerpts = snapshot.excerpts.cursor::(()); - let mut old_diff_transforms = snapshot - .diff_transforms - .cursor::>(()); - let mut new_diff_transforms = SumTree::default(); - let mut old_expanded_hunks = HashSet::default(); - let mut output_edits = Vec::new(); - let mut output_delta = 0_isize; - let mut at_transform_boundary = true; - let mut end_of_current_insert = None; - - let mut excerpt_edits: VecDeque<_> = excerpt_edits.into_iter().collect(); - while let Some(edit) = excerpt_edits.pop_front() { - excerpts.seek_forward(&edit.new.start, Bias::Right); - if excerpts.item().is_none() && *excerpts.start() == edit.new.start { - excerpts.prev(); - } - - // Keep any transforms that are before the edit. - if at_transform_boundary { - 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(); - } - } - - // Compute the start of the edit in output coordinates. - let edit_start_overshoot = if let Some(DiffTransform::FilteredInsertedHunk { .. }) = - old_diff_transforms.item() - { - 0 - } else { - edit.old.start - old_diff_transforms.start().0 - }; - let edit_old_start = old_diff_transforms.start().1 + edit_start_overshoot; - let edit_new_start = - MultiBufferOffset((edit_old_start.0 as isize + output_delta) as usize); - - let changed_diff_hunks = Self::recompute_diff_transforms_for_edit( - &edit, - &mut excerpts, - &mut old_diff_transforms, - &mut new_diff_transforms, - &mut end_of_current_insert, - &mut old_expanded_hunks, - snapshot, - change_kind, - filter_mode, - ); - - // When the added range of a hunk is edited, the end anchor of the hunk may be moved later - // in response by hunks_intersecting_range to keep it at a row boundary. In KeepDeletions - // mode, we need to make sure that the whole added range is still filtered out in this situation. - // We do that by adding an additional edit that covers the rest of the hunk added range. - if let Some(current_inserted_hunk) = &end_of_current_insert - && current_inserted_hunk.is_filtered - // No additional edit needed if we've already covered the whole added range. - && current_inserted_hunk.insertion_end_offset > edit.new.end - // No additional edit needed if this edit just touched the start of the hunk - // (this also prevents pushing the deleted region for the hunk twice). - && edit.new.end > current_inserted_hunk.hunk_excerpt_start - // No additional edit needed if there is a subsequent edit that intersects - // the same hunk (the last such edit will take care of it). - && excerpt_edits.front().is_none_or(|next_edit| { - next_edit.new.start >= current_inserted_hunk.insertion_end_offset - }) - { - let overshoot = current_inserted_hunk.insertion_end_offset - edit.new.end; - let additional_edit = Edit { - old: edit.old.end..edit.old.end + overshoot, - new: edit.new.end..current_inserted_hunk.insertion_end_offset, - }; - excerpt_edits.push_front(additional_edit); - } - - // Compute the end of the edit in output coordinates. - let edit_old_end_overshoot = if let Some(DiffTransform::FilteredInsertedHunk { - .. - }) = old_diff_transforms.item() - { - ExcerptDimension(MultiBufferOffset(0)) - } else { - ExcerptDimension(MultiBufferOffset( - edit.old.end - old_diff_transforms.start().0, - )) - }; - let edit_new_end_overshoot = if let Some(current_inserted_hunk) = &end_of_current_insert - && current_inserted_hunk.is_filtered - { - let insertion_end_offset = current_inserted_hunk.insertion_end_offset; - let excerpt_len = new_diff_transforms.summary().excerpt_len(); - let base = insertion_end_offset.max(excerpt_len); - edit.new.end.saturating_sub(base) - } else { - edit.new.end - new_diff_transforms.summary().excerpt_len() - }; - let edit_old_end = old_diff_transforms.start().1 + edit_old_end_overshoot.0; - let edit_new_end = new_diff_transforms.summary().output.len + edit_new_end_overshoot; - let output_edit = Edit { - old: edit_old_start..edit_old_end, - new: edit_new_start..edit_new_end, - }; - - output_delta += (output_edit.new.end - output_edit.new.start) as isize; - output_delta -= (output_edit.old.end - output_edit.old.start) as isize; - if changed_diff_hunks || matches!(change_kind, DiffChangeKind::BufferEdited) { - output_edits.push(output_edit); - } - - // 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 - .front() - .is_none_or(|next_edit| next_edit.old.start >= old_diff_transforms.end().0) - { - let keep_next_old_transform = (old_diff_transforms.start().0 >= edit.old.end) - && match old_diff_transforms.item() { - Some( - DiffTransform::InsertedHunk { hunk_info, .. } - | DiffTransform::FilteredInsertedHunk { hunk_info, .. }, - ) => excerpts.item().is_some_and(|excerpt| { - hunk_info.hunk_start_anchor.is_valid(&excerpt.buffer) - }), - _ => true, - }; - - let mut excerpt_offset = edit.new.end; - if !keep_next_old_transform { - excerpt_offset += old_diff_transforms.end().0 - edit.old.end; - old_diff_transforms.next(); - } - - old_expanded_hunks.clear(); - Self::push_buffer_content_transform( - snapshot, - &mut new_diff_transforms, - excerpt_offset, - end_of_current_insert.as_ref(), - ); - at_transform_boundary = true; - } - } - - // Keep any transforms that are after the last edit. - Self::append_diff_transforms(&mut new_diff_transforms, old_diff_transforms.suffix()); - - // Ensure there's always at least one buffer content transform. - if new_diff_transforms.is_empty() { - new_diff_transforms.push( - DiffTransform::Unmodified { - summary: Default::default(), - }, - (), - ); - } - - drop(old_diff_transforms); - drop(excerpts); - snapshot.diff_transforms = new_diff_transforms; - snapshot.edit_count += 1; - - #[cfg(any(test, feature = "test-support"))] - snapshot.check_invariants(); - output_edits - } - - fn recompute_diff_transforms_for_edit( - edit: &Edit, - excerpts: &mut Cursor, - old_diff_transforms: &mut Cursor< - DiffTransform, - Dimensions, - >, - new_diff_transforms: &mut SumTree, - end_of_current_insert: &mut Option, - old_expanded_hunks: &mut HashSet, - snapshot: &MultiBufferSnapshot, - change_kind: DiffChangeKind, - filter_mode: Option, - ) -> bool { - log::trace!( - "recomputing diff transform for edit {:?} => {:?}", - edit.old.start..edit.old.end, - edit.new.start..edit.new.end - ); - - // Record which hunks were previously expanded. - while let Some(item) = old_diff_transforms.item() { - if let Some(hunk_info) = item.hunk_info() { - log::trace!( - "previously expanded hunk at {:?}", - old_diff_transforms.start() - ); - old_expanded_hunks.insert(hunk_info); - } - if old_diff_transforms.end().0 > edit.old.end { - break; - } - old_diff_transforms.next(); - } - - // Avoid querying diff hunks if there's no possibility of hunks being expanded. - let all_diff_hunks_expanded = snapshot.all_diff_hunks_expanded; - if old_expanded_hunks.is_empty() - && change_kind == DiffChangeKind::BufferEdited - && !all_diff_hunks_expanded - { - return false; - } - - // Visit each excerpt that intersects the edit. - let mut did_expand_hunks = false; - while let Some(excerpt) = excerpts.item() { - // Recompute the expanded hunks in the portion of the excerpt that - // intersects the edit. - if let Some(diff) = snapshot.diffs.get(&excerpt.buffer_id) { - let buffer = &excerpt.buffer; - let excerpt_start = *excerpts.start(); - let excerpt_end = excerpt_start + excerpt.text_summary.len; - let excerpt_buffer_start = excerpt.range.context.start.to_offset(buffer); - let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len; - let edit_buffer_start = - excerpt_buffer_start + edit.new.start.saturating_sub(excerpt_start); - let edit_buffer_end = - excerpt_buffer_start + edit.new.end.saturating_sub(excerpt_start); - let edit_buffer_end = edit_buffer_end.min(excerpt_buffer_end); - let edit_anchor_range = - buffer.anchor_before(edit_buffer_start)..buffer.anchor_after(edit_buffer_end); - - for hunk in diff.hunks_intersecting_range(edit_anchor_range, buffer) { - if hunk.is_created_file() && !all_diff_hunks_expanded { - continue; - } - - let hunk_buffer_range = hunk.buffer_range.to_offset(buffer); - if hunk_buffer_range.start < excerpt_buffer_start { - log::trace!("skipping hunk that starts before excerpt"); - continue; - } - - let hunk_info = DiffTransformHunkInfo { - excerpt_id: excerpt.id, - hunk_start_anchor: hunk.buffer_range.start, - hunk_secondary_status: hunk.secondary_status, - base_text_byte_range: hunk.diff_base_byte_range.clone(), - }; - - let hunk_excerpt_start = excerpt_start - + hunk_buffer_range.start.saturating_sub(excerpt_buffer_start); - let hunk_excerpt_end = excerpt_end - .min(excerpt_start + (hunk_buffer_range.end - excerpt_buffer_start)); - - Self::push_buffer_content_transform( - snapshot, - new_diff_transforms, - hunk_excerpt_start, - end_of_current_insert.as_ref(), - ); - - // For every existing hunk, determine if it was previously expanded - // and if it should currently be expanded. - let was_previously_expanded = old_expanded_hunks.contains(&hunk_info); - let should_expand_hunk = match &change_kind { - DiffChangeKind::DiffUpdated { base_changed: true } => { - was_previously_expanded || all_diff_hunks_expanded - } - DiffChangeKind::ExpandOrCollapseHunks { expand } => { - let intersects = hunk_buffer_range.is_empty() - || hunk_buffer_range.end > edit_buffer_start; - if *expand { - intersects || was_previously_expanded || all_diff_hunks_expanded - } else { - !intersects && (was_previously_expanded || all_diff_hunks_expanded) - } - } - _ => was_previously_expanded || all_diff_hunks_expanded, - }; - - if should_expand_hunk { - did_expand_hunks = true; - log::trace!( - "expanding hunk {:?}, excerpt:{:?}", - hunk_excerpt_start..hunk_excerpt_end, - excerpt.id - ); - - if !hunk.diff_base_byte_range.is_empty() - && hunk_buffer_range.start >= edit_buffer_start - && hunk_buffer_range.start <= excerpt_buffer_end - && filter_mode != Some(MultiBufferFilterMode::KeepInsertions) - { - let base_text = diff.base_text(); - let mut text_cursor = - base_text.as_rope().cursor(hunk.diff_base_byte_range.start); - let mut base_text_summary = - text_cursor.summary::(hunk.diff_base_byte_range.end); - - let mut has_trailing_newline = false; - if base_text_summary.last_line_chars > 0 { - base_text_summary += TextSummary::newline(); - has_trailing_newline = true; - } - - new_diff_transforms.push( - DiffTransform::DeletedHunk { - summary: base_text_summary, - buffer_id: excerpt.buffer_id, - hunk_info: hunk_info.clone(), - has_trailing_newline, - }, - (), - ); - } - - if !hunk_buffer_range.is_empty() { - let is_filtered = - filter_mode == Some(MultiBufferFilterMode::KeepDeletions); - let insertion_end_offset = hunk_excerpt_end.min(excerpt_end); - *end_of_current_insert = Some(CurrentInsertedHunk { - hunk_excerpt_start, - insertion_end_offset, - hunk_info, - is_filtered, - }); - } - } - } - } - - if excerpts.end() <= edit.new.end { - excerpts.next(); - } else { - break; - } - } - - did_expand_hunks || !old_expanded_hunks.is_empty() - } - - fn append_diff_transforms( - new_transforms: &mut SumTree, - subtree: SumTree, - ) { - if let Some(transform) = subtree.first() - && Self::extend_last_buffer_content_transform(new_transforms, transform) - { - let mut cursor = subtree.cursor::<()>(()); - cursor.next(); - cursor.next(); - new_transforms.append(cursor.suffix(), ()); - return; - } - new_transforms.append(subtree, ()); - } - - fn push_diff_transform(new_transforms: &mut SumTree, transform: DiffTransform) { - if Self::extend_last_buffer_content_transform(new_transforms, &transform) { - return; - } - new_transforms.push(transform, ()); - } - - fn push_buffer_content_transform( - old_snapshot: &MultiBufferSnapshot, - new_transforms: &mut SumTree, - end_offset: ExcerptOffset, - current_inserted_hunk: Option<&CurrentInsertedHunk>, - ) { - if let Some(current_inserted_hunk) = current_inserted_hunk { - let start_offset = new_transforms.summary().excerpt_len(); - let end_offset = current_inserted_hunk.insertion_end_offset.min(end_offset); - if end_offset > start_offset { - let summary_to_add = old_snapshot - .text_summary_for_excerpt_offset_range::( - start_offset..end_offset, - ); - - let transform = if current_inserted_hunk.is_filtered { - DiffTransform::FilteredInsertedHunk { - summary: summary_to_add, - hunk_info: current_inserted_hunk.hunk_info.clone(), - } - } else { - DiffTransform::InsertedHunk { - summary: summary_to_add, - hunk_info: current_inserted_hunk.hunk_info.clone(), - } - }; - if !Self::extend_last_buffer_content_transform(new_transforms, &transform) { - new_transforms.push(transform, ()) - } - } - } - - let start_offset = new_transforms.summary().excerpt_len(); - if end_offset > start_offset { - let summary_to_add = old_snapshot - .text_summary_for_excerpt_offset_range::(start_offset..end_offset); - - let transform = DiffTransform::Unmodified { - summary: summary_to_add, - }; - if !Self::extend_last_buffer_content_transform(new_transforms, &transform) { - new_transforms.push(transform, ()) - } - } - } - - fn extend_last_buffer_content_transform( - new_transforms: &mut SumTree, - transform: &DiffTransform, - ) -> bool { - let mut did_extend = false; - new_transforms.update_last( - |last_transform| { - did_extend = last_transform.merge_with(&transform); - }, - (), - ); - did_extend - } -} - -impl DiffTransform { - /// Ergonomic wrapper for [`DiffTransform::merged_with`] that applies the - /// merging in-place. Returns `true` if merging was possible. - #[must_use = "check whether merging actually succeeded"] - fn merge_with(&mut self, other: &Self) -> bool { - match self.to_owned().merged_with(other) { - Some(merged) => { - *self = merged; - true - } - None => false, - } - } - - /// Attempt to merge `self` with `other`, and return the merged transform. - /// - /// This will succeed if all of the following are true: - /// - both transforms are the same variant - /// - neither transform is [`DiffTransform::DeletedHunk`] - /// - if both transform are either [`DiffTransform::InsertedHunk`] or - /// [`DiffTransform::FilteredInsertedHunk`], then their - /// `hunk_info.hunk_start_anchor`s match - #[must_use = "check whether merging actually succeeded"] - #[rustfmt::skip] - fn merged_with(self, other: &Self) -> Option { - match (self, other) { - ( - DiffTransform::Unmodified { mut summary }, - DiffTransform::Unmodified { summary: other_summary }, - ) => { - summary += *other_summary; - Some(DiffTransform::Unmodified { summary }) - } - ( - DiffTransform::FilteredInsertedHunk { mut summary, hunk_info }, - DiffTransform::FilteredInsertedHunk { - hunk_info: other_hunk_info, - summary: other_summary, - }, - ) => { - if hunk_info.hunk_start_anchor == other_hunk_info.hunk_start_anchor { - summary += *other_summary; - Some(DiffTransform::FilteredInsertedHunk { summary, hunk_info }) - } else { - None - } - } - ( - DiffTransform::InsertedHunk { mut summary, hunk_info }, - DiffTransform::InsertedHunk { - hunk_info: other_hunk_info, - summary: other_summary, - }, - ) => { - if hunk_info.hunk_start_anchor == other_hunk_info.hunk_start_anchor { - summary += *other_summary; - Some(DiffTransform::InsertedHunk { summary, hunk_info }) - } else { - None - } - } - _ => return None, - } - } -} - -fn build_excerpt_ranges( - ranges: impl IntoIterator>, - context_line_count: u32, - buffer_snapshot: &BufferSnapshot, -) -> Vec> { - ranges - .into_iter() - .map(|range| { - let start_row = range.start.row.saturating_sub(context_line_count); - let start = Point::new(start_row, 0); - let end_row = (range.end.row + context_line_count).min(buffer_snapshot.max_point().row); - let end = Point::new(end_row, buffer_snapshot.line_len(end_row)); - ExcerptRange { - context: start..end, - primary: range, - } - }) - .collect() -} - -#[cfg(any(test, feature = "test-support"))] -impl MultiBuffer { - pub fn build_simple(text: &str, cx: &mut gpui::App) -> Entity { - let buffer = cx.new(|cx| Buffer::local(text, cx)); - cx.new(|cx| Self::singleton(buffer, cx)) - } - - pub fn build_multi( - excerpts: [(&str, Vec>); COUNT], - cx: &mut gpui::App, - ) -> Entity { - 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); - multi.update(cx, |multi, cx| { - multi.push_excerpts(buffer, excerpt_ranges, cx) - }); - } - - multi - } - - pub fn build_from_buffer(buffer: Entity, cx: &mut gpui::App) -> Entity { - cx.new(|cx| Self::singleton(buffer, cx)) - } - - pub fn build_random(rng: &mut impl rand::Rng, cx: &mut gpui::App) -> Entity { - cx.new(|cx| { - let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); - let mutation_count = rng.random_range(1..=5); - multibuffer.randomly_edit_excerpts(rng, mutation_count, cx); - multibuffer - }) - } - - pub fn randomly_edit( - &mut self, - rng: &mut impl rand::Rng, - edit_count: usize, - cx: &mut Context, - ) { - use util::RandomCharIter; - - let snapshot = self.read(cx); - 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()) { - break; - } - - let new_start = last_end.map_or(MultiBufferOffset::ZERO, |last_end| last_end + 1usize); - let end = - snapshot.clip_offset(rng.random_range(new_start..=snapshot.len()), Bias::Right); - let start = snapshot.clip_offset(rng.random_range(new_start..=end), Bias::Right); - last_end = Some(end); - - let mut range = start..end; - if rng.random_bool(0.2) { - mem::swap(&mut range.start, &mut range.end); - } - - let new_text_len = rng.random_range(0..10); - let new_text: String = RandomCharIter::new(&mut *rng).take(new_text_len).collect(); - - edits.push((range, new_text.into())); - } - log::info!("mutating multi-buffer with {:?}", edits); - drop(snapshot); - - self.edit(edits, None, cx); - } - - pub fn randomly_edit_excerpts( - &mut self, - rng: &mut impl rand::Rng, - mutation_count: usize, - cx: &mut Context, - ) { - use rand::prelude::*; - use std::env; - use util::RandomCharIter; - - let max_excerpts = env::var("MAX_EXCERPTS") - .map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable")) - .unwrap_or(5); - - let mut buffers = Vec::new(); - for _ in 0..mutation_count { - if rng.random_bool(0.05) { - log::info!("Clearing multi-buffer"); - self.clear(cx); - continue; - } else if rng.random_bool(0.1) && !self.excerpt_ids().is_empty() { - let ids = self.excerpt_ids(); - let mut excerpts = HashSet::default(); - for _ in 0..rng.random_range(0..ids.len()) { - excerpts.extend(ids.choose(rng).copied()); - } - - let line_count = rng.random_range(0..5); - - log::info!("Expanding excerpts {excerpts:?} by {line_count} lines"); - - self.expand_excerpts( - excerpts.iter().cloned(), - line_count, - ExpandExcerptDirection::UpAndDown, - cx, - ); - continue; - } - - let excerpt_ids = self.excerpt_ids(); - if excerpt_ids.is_empty() || (rng.random() && excerpt_ids.len() < max_excerpts) { - let buffer_handle = if rng.random() || self.buffers.is_empty() { - let text = RandomCharIter::new(&mut *rng).take(10).collect::(); - buffers.push(cx.new(|cx| Buffer::local(text, cx))); - let buffer = buffers.last().unwrap().read(cx); - log::info!( - "Creating new buffer {} with text: {:?}", - buffer.remote_id(), - buffer.text() - ); - buffers.last().unwrap().clone() - } else { - self.buffers.values().choose(rng).unwrap().buffer.clone() - }; - - let buffer = buffer_handle.read(cx); - let buffer_text = buffer.text(); - let ranges = (0..rng.random_range(0..5)) - .map(|_| { - let end_ix = - buffer.clip_offset(rng.random_range(0..=buffer.len()), Bias::Right); - let start_ix = buffer.clip_offset(rng.random_range(0..=end_ix), Bias::Left); - ExcerptRange::new(start_ix..end_ix) - }) - .collect::>(); - log::info!( - "Inserting excerpts from buffer {} and ranges {:?}: {:?}", - buffer_handle.read(cx).remote_id(), - ranges.iter().map(|r| &r.context).collect::>(), - ranges - .iter() - .map(|r| &buffer_text[r.context.clone()]) - .collect::>() - ); - - let excerpt_id = self.push_excerpts(buffer_handle.clone(), ranges, cx); - log::info!("Inserted with ids: {:?}", excerpt_id); - } else { - let remove_count = rng.random_range(1..=excerpt_ids.len()); - let mut excerpts_to_remove = excerpt_ids - .choose_multiple(rng, remove_count) - .cloned() - .collect::>(); - let snapshot = self.snapshot.borrow(); - excerpts_to_remove.sort_unstable_by(|a, b| a.cmp(b, &snapshot)); - drop(snapshot); - log::info!("Removing excerpts {:?}", excerpts_to_remove); - self.remove_excerpts(excerpts_to_remove, cx); - } - } - } - - pub fn randomly_mutate( - &mut self, - rng: &mut impl rand::Rng, - mutation_count: usize, - cx: &mut Context, - ) { - use rand::prelude::*; - - if rng.random_bool(0.7) || self.singleton { - let buffer = self - .buffers - .values() - .choose(rng) - .map(|state| state.buffer.clone()); - - if let Some(buffer) = buffer { - buffer.update(cx, |buffer, cx| { - if rng.random() { - buffer.randomly_edit(rng, mutation_count, cx); - } else { - buffer.randomly_undo_redo(rng, cx); - } - }); - } else { - self.randomly_edit(rng, mutation_count, cx); - } - } else { - self.randomly_edit_excerpts(rng, mutation_count, cx); - } - - self.check_invariants(cx); - } - - fn check_invariants(&self, cx: &App) { - self.read(cx).check_invariants(); - } -} - -impl EventEmitter for MultiBuffer {} - -impl MultiBufferSnapshot { - pub fn text(&self) -> String { - self.chunks(MultiBufferOffset::ZERO..self.len(), false) - .map(|chunk| chunk.text) - .collect() - } - - pub fn reversed_chars_at(&self, position: T) -> impl Iterator + '_ { - self.reversed_chunks_in_range(MultiBufferOffset::ZERO..position.to_offset(self)) - .flat_map(|c| c.chars().rev()) - } - - fn reversed_chunks_in_range( - &self, - range: Range, - ) -> ReversedMultiBufferChunks<'_> { - let mut cursor = self.cursor::(); - cursor.seek(&range.end); - let current_chunks = cursor.region().as_ref().map(|region| { - let start_overshoot = range.start.saturating_sub(region.range.start); - let end_overshoot = range.end - region.range.start; - let end = (region.buffer_range.start + end_overshoot).min(region.buffer_range.end); - let start = region.buffer_range.start + start_overshoot; - region.buffer.reversed_chunks_in_range(start..end) - }); - ReversedMultiBufferChunks { - cursor, - current_chunks, - start: range.start, - offset: range.end, - } - } - - pub fn chars_at(&self, position: T) -> impl Iterator + '_ { - let offset = position.to_offset(self); - self.text_for_range(offset..self.len()) - .flat_map(|chunk| chunk.chars()) - } - - pub fn text_for_range(&self, range: Range) -> impl Iterator + '_ { - self.chunks(range, false).map(|chunk| chunk.text) - } - - pub fn is_line_blank(&self, row: MultiBufferRow) -> bool { - self.text_for_range(Point::new(row.0, 0)..Point::new(row.0, self.line_len(row))) - .all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none()) - } - - pub fn contains_str_at(&self, position: T, needle: &str) -> bool - where - T: ToOffset, - { - let position = position.to_offset(self); - position == self.clip_offset(position, Bias::Left) - && self - .bytes_in_range(position..self.len()) - .flatten() - .copied() - .take(needle.len()) - .eq(needle.bytes()) - } - - pub fn diff_hunks(&self) -> impl Iterator + '_ { - self.diff_hunks_in_range(Anchor::min()..Anchor::max()) - } - - pub fn diff_hunks_in_range( - &self, - range: Range, - ) -> impl Iterator + '_ { - let query_range = range.start.to_point(self)..range.end.to_point(self); - self.lift_buffer_metadata(query_range.clone(), move |buffer, buffer_range| { - let diff = self.diffs.get(&buffer.remote_id())?; - let buffer_start = buffer.anchor_before(buffer_range.start); - let buffer_end = buffer.anchor_after(buffer_range.end); - Some( - diff.hunks_intersecting_range(buffer_start..buffer_end, buffer) - .filter_map(|hunk| { - if hunk.is_created_file() && !self.all_diff_hunks_expanded { - return None; - } - Some((hunk.range.clone(), hunk)) - }), - ) - }) - .filter_map(move |(range, hunk, excerpt)| { - if range.start != range.end && range.end == query_range.start && !hunk.range.is_empty() - { - return None; - } - let end_row = if range.end.column == 0 { - range.end.row - } else { - range.end.row + 1 - }; - - let word_diffs = (!hunk.base_word_diffs.is_empty() - || !hunk.buffer_word_diffs.is_empty()) - .then(|| { - let hunk_start_offset = - Anchor::in_buffer(excerpt.id, hunk.buffer_range.start).to_offset(self); - - hunk.base_word_diffs - .iter() - .map(|diff| hunk_start_offset + diff.start..hunk_start_offset + diff.end) - .chain( - hunk.buffer_word_diffs - .into_iter() - .map(|diff| Anchor::range_in_buffer(excerpt.id, diff).to_offset(self)), - ) - .collect() - }) - .unwrap_or_default(); - - Some(MultiBufferDiffHunk { - row_range: MultiBufferRow(range.start.row)..MultiBufferRow(end_row), - buffer_id: excerpt.buffer_id, - excerpt_id: excerpt.id, - buffer_range: hunk.buffer_range.clone(), - word_diffs, - diff_base_byte_range: BufferOffset(hunk.diff_base_byte_range.start) - ..BufferOffset(hunk.diff_base_byte_range.end), - secondary_status: hunk.secondary_status, - }) - }) - } - - fn excerpts_for_range( - &self, - range: Range, - ) -> impl Iterator + '_ { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut cursor = self.cursor::(); - cursor.seek(&range.start); - std::iter::from_fn(move || { - let region = cursor.region()?; - if region.range.start > range.end - || region.range.start == range.end && region.range.start > range.start - { - return None; - } - cursor.next_excerpt(); - Some(region.excerpt) - }) - } - - pub fn excerpt_ids_for_range( - &self, - range: Range, - ) -> impl Iterator + '_ { - self.excerpts_for_range(range).map(|excerpt| excerpt.id) - } - - pub fn buffer_ids_for_range( - &self, - range: Range, - ) -> impl Iterator + '_ { - self.excerpts_for_range(range) - .map(|excerpt| excerpt.buffer_id) - } - - pub fn ranges_to_buffer_ranges( - &self, - ranges: impl Iterator>, - ) -> impl Iterator, ExcerptId)> { - ranges.flat_map(|range| self.range_to_buffer_ranges(range).into_iter()) - } - - pub fn range_to_buffer_ranges( - &self, - range: Range, - ) -> Vec<(&BufferSnapshot, Range, ExcerptId)> { - let start = range.start.to_offset(self); - let end = range.end.to_offset(self); - - let mut cursor = self.cursor::(); - cursor.seek(&start); - - let mut result: Vec<(&BufferSnapshot, Range, ExcerptId)> = Vec::new(); - while let Some(region) = cursor.region() { - if region.range.start > end { - break; - } - if region.is_main_buffer { - let start_overshoot = start.saturating_sub(region.range.start); - let end_overshoot = end.saturating_sub(region.range.start); - let start = region - .buffer_range - .end - .min(region.buffer_range.start + start_overshoot); - let end = region - .buffer_range - .end - .min(region.buffer_range.start + end_overshoot); - if let Some(prev) = result.last_mut().filter(|(_, prev_range, excerpt_id)| { - *excerpt_id == region.excerpt.id && prev_range.end == start - }) { - prev.1.end = end; - } else { - result.push((region.buffer, start..end, region.excerpt.id)); - } - } - cursor.next(); - } - result - } - - pub fn range_to_buffer_ranges_with_deleted_hunks( - &self, - range: Range, - ) -> impl Iterator< - Item = ( - &BufferSnapshot, - Range, - ExcerptId, - Option, - ), - > + '_ { - let start = range.start.to_offset(self); - let end = range.end.to_offset(self); - - let mut cursor = self.cursor::(); - cursor.seek(&start); - - std::iter::from_fn(move || { - let region = cursor.region()?; - if region.range.start > end { - return None; - } - let start_overshoot = start.saturating_sub(region.range.start); - let end_overshoot = end.saturating_sub(region.range.start); - let start = region - .buffer_range - .end - .min(region.buffer_range.start + start_overshoot); - let end = region - .buffer_range - .end - .min(region.buffer_range.start + end_overshoot); - - let region_excerpt_id = region.excerpt.id; - let deleted_hunk_anchor = if region.is_main_buffer { - None - } else { - Some(self.anchor_before(region.range.start)) - }; - let result = ( - region.buffer, - start..end, - region_excerpt_id, - deleted_hunk_anchor, - ); - cursor.next(); - Some(result) - }) - } - - /// Retrieves buffer metadata for the given range, and converts it into multi-buffer - /// coordinates. - /// - /// The given callback will be called for every excerpt intersecting the given range. It will - /// be passed the excerpt's buffer and the buffer range that the input range intersects. - /// The callback should return an iterator of metadata items from that buffer, each paired - /// with a buffer range. - /// - /// The returned iterator yields each of these metadata items, paired with its range in - /// multi-buffer coordinates. - fn lift_buffer_metadata<'a, MBD, M, I>( - &'a self, - query_range: Range, - get_buffer_metadata: impl 'a + Fn(&'a BufferSnapshot, Range) -> Option, - ) -> impl Iterator, M, &'a Excerpt)> + 'a - where - I: Iterator, M)> + 'a, - MBD: MultiBufferDimension - + Ord - + Sub - + ops::Add - + ops::AddAssign, - MBD::TextDimension: Sub - + ops::Add - + AddAssign - + Ord, - { - let mut current_excerpt_metadata: Option<(ExcerptId, I)> = None; - let mut cursor = self.cursor::(); - - // Find the excerpt and buffer offset where the given range ends. - cursor.seek(&query_range.end); - let mut range_end = None; - while let Some(region) = cursor.region() { - if region.is_main_buffer { - let mut buffer_end = region.buffer_range.start; - let overshoot = if query_range.end > region.range.start { - query_range.end - region.range.start - } else { - ::default() - }; - buffer_end = buffer_end + overshoot; - range_end = Some((region.excerpt.id, buffer_end)); - break; - } - cursor.next(); - } - - cursor.seek(&query_range.start); - - if let Some(region) = cursor.region().filter(|region| !region.is_main_buffer) - && region.range.start > MBD::default() - { - cursor.prev() - } - - iter::from_fn(move || { - loop { - let excerpt = cursor.excerpt()?; - - // If we have already retrieved metadata for this excerpt, continue to use it. - let metadata_iter = if let Some((_, metadata)) = current_excerpt_metadata - .as_mut() - .filter(|(excerpt_id, _)| *excerpt_id == excerpt.id) - { - Some(metadata) - } - // Otherwise, compute the intersection of the input range with the excerpt's range, - // and retrieve the metadata for the resulting range. - else { - let region = cursor.region()?; - let mut buffer_start; - if region.is_main_buffer { - buffer_start = region.buffer_range.start; - if query_range.start > region.range.start { - let overshoot = query_range.start - region.range.start; - buffer_start = buffer_start + overshoot; - } - buffer_start = buffer_start.min(region.buffer_range.end); - } else { - 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); - } - - get_buffer_metadata(&excerpt.buffer, buffer_start..buffer_end).map(|iterator| { - &mut current_excerpt_metadata.insert((excerpt.id, iterator)).1 - }) - }; - - // Visit each metadata item. - if let Some((metadata_buffer_range, metadata)) = - metadata_iter.and_then(Iterator::next) - { - // Find the multibuffer regions that contain the start and end of - // the metadata item's range. - if metadata_buffer_range.start > ::default() { - while let Some(region) = cursor.region() { - if region.is_main_buffer - && (region.buffer_range.end >= metadata_buffer_range.start - || cursor.is_at_end_of_excerpt()) - { - break; - } - cursor.next(); - } - } - let start_region = cursor.region()?; - while let Some(region) = cursor.region() { - if region.is_main_buffer - && (region.buffer_range.end > metadata_buffer_range.end - || cursor.is_at_end_of_excerpt()) - { - break; - } - cursor.next(); - } - let end_region = cursor.region(); - - // Convert the metadata item's range into multibuffer coordinates. - let mut start_position = start_region.range.start; - let region_buffer_start = start_region.buffer_range.start; - if start_region.is_main_buffer - && metadata_buffer_range.start > region_buffer_start - { - start_position = - start_position + (metadata_buffer_range.start - region_buffer_start); - start_position = start_position.min(start_region.range.end); - } - - let mut end_position = self.max_position(); - if let Some(end_region) = &end_region { - end_position = end_region.range.start; - debug_assert!(end_region.is_main_buffer); - let region_buffer_start = end_region.buffer_range.start; - if metadata_buffer_range.end > region_buffer_start { - end_position = - end_position + (metadata_buffer_range.end - region_buffer_start); - } - end_position = end_position.min(end_region.range.end); - } - - if start_position <= query_range.end && end_position >= query_range.start { - return Some((start_position..end_position, metadata, excerpt)); - } - } - // 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; - } - cursor.next_excerpt(); - } - } - }) - } - - pub fn diff_hunk_before(&self, position: T) -> Option { - let offset = position.to_offset(self); - - let mut cursor = self - .cursor::, DimensionPair>( - ); - cursor.seek(&DimensionPair { - key: offset, - value: None, - }); - cursor.seek_to_start_of_current_excerpt(); - let excerpt = cursor.excerpt()?; - - let excerpt_end = excerpt.range.context.end.to_offset(&excerpt.buffer); - let current_position = self - .anchor_before(offset) - .text_anchor - .to_offset(&excerpt.buffer); - let excerpt_end = excerpt - .buffer - .anchor_before(excerpt_end.min(current_position)); - - if let Some(diff) = self.diffs.get(&excerpt.buffer_id) { - for hunk in diff.hunks_intersecting_range_rev( - excerpt.range.context.start..excerpt_end, - &excerpt.buffer, - ) { - let hunk_end = hunk.buffer_range.end.to_offset(&excerpt.buffer); - if hunk_end >= current_position { - continue; - } - let start = Anchor::in_buffer(excerpt.id, hunk.buffer_range.start).to_point(self); - return Some(MultiBufferRow(start.row)); - } - } - - loop { - cursor.prev_excerpt(); - let excerpt = cursor.excerpt()?; - - let Some(diff) = self.diffs.get(&excerpt.buffer_id) else { - continue; - }; - let mut hunks = - diff.hunks_intersecting_range_rev(excerpt.range.context.clone(), &excerpt.buffer); - let Some(hunk) = hunks.next() else { - continue; - }; - let start = Anchor::in_buffer(excerpt.id, hunk.buffer_range.start).to_point(self); - return Some(MultiBufferRow(start.row)); - } - } - - pub fn has_diff_hunks(&self) -> bool { - self.diffs.values().any(|diff| !diff.is_empty()) - } - - pub fn is_inside_word( - &self, - position: T, - scope_context: Option, - ) -> bool { - let position = position.to_offset(self); - let classifier = self - .char_classifier_at(position) - .scope_context(scope_context); - let next_char_kind = self.chars_at(position).next().map(|c| classifier.kind(c)); - let prev_char_kind = self - .reversed_chars_at(position) - .next() - .map(|c| classifier.kind(c)); - prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word)) - } - - pub fn surrounding_word( - &self, - start: T, - scope_context: Option, - ) -> (Range, Option) { - let mut start = start.to_offset(self); - let mut end = start; - let mut next_chars = self.chars_at(start).peekable(); - let mut prev_chars = self.reversed_chars_at(start).peekable(); - - let classifier = self.char_classifier_at(start).scope_context(scope_context); - - let word_kind = cmp::max( - prev_chars.peek().copied().map(|c| classifier.kind(c)), - next_chars.peek().copied().map(|c| classifier.kind(c)), - ); - - for ch in prev_chars { - if Some(classifier.kind(ch)) == word_kind && ch != '\n' { - start -= ch.len_utf8(); - } else { - break; - } - } - - for ch in next_chars { - if Some(classifier.kind(ch)) == word_kind && ch != '\n' { - end += ch.len_utf8(); - } else { - break; - } - } - - (start..end, word_kind) - } - - pub fn char_kind_before( - &self, - start: T, - scope_context: Option, - ) -> Option { - let start = start.to_offset(self); - let classifier = self.char_classifier_at(start).scope_context(scope_context); - self.reversed_chars_at(start) - .next() - .map(|ch| classifier.kind(ch)) - } - - pub fn is_singleton(&self) -> bool { - self.singleton - } - - pub fn as_singleton(&self) -> Option<(&ExcerptId, BufferId, &BufferSnapshot)> { - if self.singleton { - self.excerpts - .iter() - .next() - .map(|e| (&e.id, e.buffer_id, &e.buffer)) - } else { - None - } - } - - pub fn len(&self) -> MultiBufferOffset { - self.diff_transforms.summary().output.len - } - - pub fn max_position(&self) -> MBD { - MBD::from_summary(&self.text_summary()) - } - - pub fn is_empty(&self) -> bool { - self.diff_transforms.summary().output.len == MultiBufferOffset(0) - } - - pub fn widest_line_number(&self) -> u32 { - // widest_line_number is 0-based, so 1 is added to get the displayed line number. - self.excerpts.summary().widest_line_number + 1 - } - - pub fn bytes_in_range(&self, range: Range) -> MultiBufferBytes<'_> { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut excerpts = self.cursor::(); - excerpts.seek(&range.start); - - let mut chunk; - let mut has_trailing_newline; - let excerpt_bytes; - if let Some(region) = excerpts.region() { - let mut bytes = region.buffer.bytes_in_range( - region.buffer_range.start + (range.start - region.range.start) - ..(region.buffer_range.start + (range.end - region.range.start)) - .min(region.buffer_range.end), - ); - chunk = bytes.next().unwrap_or(&[][..]); - excerpt_bytes = Some(bytes); - has_trailing_newline = region.has_trailing_newline && range.end >= region.range.end; - if chunk.is_empty() && has_trailing_newline { - chunk = b"\n"; - has_trailing_newline = false; - } - } else { - chunk = &[][..]; - excerpt_bytes = None; - has_trailing_newline = false; - }; - - MultiBufferBytes { - range, - cursor: excerpts, - excerpt_bytes, - has_trailing_newline, - chunk, - } - } - - pub fn reversed_bytes_in_range( - &self, - range: Range, - ) -> ReversedMultiBufferBytes<'_> { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut chunks = self.reversed_chunks_in_range(range.clone()); - let chunk = chunks.next().map_or(&[][..], |c| c.as_bytes()); - ReversedMultiBufferBytes { - range, - chunks, - chunk, - } - } - - pub fn row_infos(&self, start_row: MultiBufferRow) -> MultiBufferRows<'_> { - let mut cursor = self.cursor::(); - cursor.seek(&Point::new(start_row.0, 0)); - let mut result = MultiBufferRows { - point: Point::new(0, 0), - is_empty: self.excerpts.is_empty(), - is_singleton: self.is_singleton(), - cursor, - }; - result.seek(start_row); - result - } - - pub fn chunks( - &self, - range: Range, - language_aware: bool, - ) -> MultiBufferChunks<'_> { - let mut chunks = MultiBufferChunks { - excerpt_offset_range: ExcerptDimension(MultiBufferOffset::ZERO) - ..ExcerptDimension(MultiBufferOffset::ZERO), - range: MultiBufferOffset::ZERO..MultiBufferOffset::ZERO, - excerpts: self.excerpts.cursor(()), - diff_transforms: self.diff_transforms.cursor(()), - diffs: &self.diffs, - diff_base_chunks: None, - excerpt_chunks: None, - buffer_chunk: None, - language_aware, - }; - let range = range.start.to_offset(self)..range.end.to_offset(self); - chunks.seek(range); - chunks - } - - pub fn clip_offset(&self, offset: MultiBufferOffset, bias: Bias) -> MultiBufferOffset { - self.clip_dimension(offset, bias, text::BufferSnapshot::clip_offset) - } - - pub fn clip_point(&self, point: Point, bias: Bias) -> Point { - self.clip_dimension(point, bias, text::BufferSnapshot::clip_point) - } - - pub fn clip_offset_utf16( - &self, - offset: MultiBufferOffsetUtf16, - bias: Bias, - ) -> MultiBufferOffsetUtf16 { - self.clip_dimension(offset, bias, text::BufferSnapshot::clip_offset_utf16) - } - - pub fn clip_point_utf16(&self, point: Unclipped, bias: Bias) -> PointUtf16 { - self.clip_dimension(point.0, bias, |buffer, point, bias| { - buffer.clip_point_utf16(Unclipped(point), bias) - }) - } - - pub fn offset_to_point(&self, offset: MultiBufferOffset) -> Point { - self.convert_dimension(offset, text::BufferSnapshot::offset_to_point) - } - - pub fn offset_to_point_utf16(&self, offset: MultiBufferOffset) -> PointUtf16 { - self.convert_dimension(offset, text::BufferSnapshot::offset_to_point_utf16) - } - - pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 { - self.convert_dimension(point, text::BufferSnapshot::point_to_point_utf16) - } - - pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point { - self.convert_dimension(point, text::BufferSnapshot::point_utf16_to_point) - } - - #[instrument(skip_all)] - pub fn point_to_offset(&self, point: Point) -> MultiBufferOffset { - self.convert_dimension(point, text::BufferSnapshot::point_to_offset) - } - - pub fn point_to_offset_utf16(&self, point: Point) -> MultiBufferOffsetUtf16 { - self.convert_dimension(point, text::BufferSnapshot::point_to_offset_utf16) - } - - pub fn offset_utf16_to_offset(&self, offset: MultiBufferOffsetUtf16) -> MultiBufferOffset { - self.convert_dimension(offset, text::BufferSnapshot::offset_utf16_to_offset) - } - - pub fn offset_to_offset_utf16(&self, offset: MultiBufferOffset) -> MultiBufferOffsetUtf16 { - self.convert_dimension(offset, text::BufferSnapshot::offset_to_offset_utf16) - } - - pub fn point_utf16_to_offset(&self, point: PointUtf16) -> MultiBufferOffset { - self.convert_dimension(point, text::BufferSnapshot::point_utf16_to_offset) - } - - pub fn point_utf16_to_offset_utf16(&self, point: PointUtf16) -> MultiBufferOffsetUtf16 { - self.convert_dimension(point, text::BufferSnapshot::point_utf16_to_offset_utf16) - } - - fn clip_dimension( - &self, - position: MBD, - bias: Bias, - clip_buffer_position: fn(&text::BufferSnapshot, BD, Bias) -> BD, - ) -> MBD - where - MBD: MultiBufferDimension + Ord + Sub + ops::AddAssign<::Output>, - BD: TextDimension + Sub::Output> + AddAssign<::Output>, - { - let mut cursor = self.cursor::(); - cursor.seek(&position); - if let Some(region) = cursor.region() { - if position >= region.range.end { - return region.range.end; - } - let overshoot = position - region.range.start; - let mut buffer_position = region.buffer_range.start; - buffer_position += overshoot; - let clipped_buffer_position = - clip_buffer_position(region.buffer, buffer_position, bias); - let mut position = region.range.start; - position += clipped_buffer_position - region.buffer_range.start; - position - } else { - self.max_position() - } - } - - #[instrument(skip_all)] - fn convert_dimension( - &self, - key: MBR1, - convert_buffer_dimension: fn(&text::BufferSnapshot, BR1) -> BR2, - ) -> MBR2 - where - MBR1: MultiBufferDimension + Ord + Sub + ops::AddAssign<::Output>, - BR1: TextDimension + Sub::Output> + AddAssign<::Output>, - MBR2: MultiBufferDimension + Ord + Sub + ops::AddAssign<::Output>, - BR2: TextDimension + Sub::Output> + AddAssign<::Output>, - { - let mut cursor = self.cursor::, DimensionPair>(); - cursor.seek(&DimensionPair { key, value: None }); - if let Some(region) = cursor.region() { - if key >= region.range.end.key { - return region.range.end.value.unwrap(); - } - let start_key = region.range.start.key; - let start_value = region.range.start.value.unwrap(); - let buffer_start_key = region.buffer_range.start.key; - let buffer_start_value = region.buffer_range.start.value.unwrap(); - let mut buffer_key = buffer_start_key; - buffer_key += key - start_key; - let buffer_value = convert_buffer_dimension(region.buffer, buffer_key); - let mut result = start_value; - result += buffer_value - buffer_start_value; - result - } else { - self.max_position() - } - } - - pub fn point_to_buffer_offset( - &self, - point: T, - ) -> Option<(&BufferSnapshot, BufferOffset)> { - let offset = point.to_offset(self); - let mut cursor = self.cursor::(); - cursor.seek(&offset); - let region = cursor.region()?; - let overshoot = offset - region.range.start; - let buffer_offset = region.buffer_range.start + overshoot; - if buffer_offset == BufferOffset(region.buffer.len() + 1) - && region.has_trailing_newline - && !region.is_main_buffer - { - let main_buffer_position = cursor.main_buffer_position()?; - let buffer_snapshot = &cursor.excerpt()?.buffer; - return Some((buffer_snapshot, main_buffer_position)); - } else if buffer_offset > BufferOffset(region.buffer.len()) { - return None; - } - Some((region.buffer, buffer_offset)) - } - - pub fn point_to_buffer_point( - &self, - point: Point, - ) -> Option<(&BufferSnapshot, Point, ExcerptId)> { - let mut cursor = self.cursor::(); - cursor.seek(&point); - let region = cursor.region()?; - let overshoot = point - region.range.start; - let buffer_point = region.buffer_range.start + overshoot; - let excerpt = cursor.excerpt()?; - if buffer_point == region.buffer.max_point() + Point::new(1, 0) - && region.has_trailing_newline - && !region.is_main_buffer - { - return Some((&excerpt.buffer, cursor.main_buffer_position()?, excerpt.id)); - } else if buffer_point > region.buffer.max_point() { - return None; - } - Some((region.buffer, buffer_point, excerpt.id)) - } - - pub fn suggested_indents( - &self, - rows: impl IntoIterator, - cx: &App, - ) -> BTreeMap { - let mut result = BTreeMap::new(); - self.suggested_indents_callback( - rows, - |row, indent| { - result.insert(row, indent); - ControlFlow::Continue(()) - }, - cx, - ); - result - } - - // move this to be a generator once those are a thing - pub fn suggested_indents_callback( - &self, - rows: impl IntoIterator, - mut cb: impl FnMut(MultiBufferRow, IndentSize) -> ControlFlow<()>, - cx: &App, - ) { - let mut rows_for_excerpt = Vec::new(); - let mut cursor = self.cursor::(); - let mut rows = rows.into_iter().peekable(); - let mut prev_row = u32::MAX; - let mut prev_language_indent_size = IndentSize::default(); - - while let Some(row) = rows.next() { - cursor.seek(&Point::new(row, 0)); - let Some(region) = cursor.region() else { - continue; - }; - - // Retrieve the language and indent size once for each disjoint region being indented. - let single_indent_size = if row.saturating_sub(1) == prev_row { - prev_language_indent_size - } else { - region - .buffer - .language_indent_size_at(Point::new(row, 0), cx) - }; - prev_language_indent_size = single_indent_size; - prev_row = row; - - let start_buffer_row = region.buffer_range.start.row; - let start_multibuffer_row = region.range.start.row; - let end_multibuffer_row = region.range.end.row; - - rows_for_excerpt.push(row); - while let Some(next_row) = rows.peek().copied() { - if end_multibuffer_row > next_row { - rows_for_excerpt.push(next_row); - rows.next(); - } else { - break; - } - } - - let buffer_rows = rows_for_excerpt - .drain(..) - .map(|row| start_buffer_row + row - start_multibuffer_row); - let buffer_indents = region - .buffer - .suggested_indents(buffer_rows, single_indent_size); - for (row, indent) in buffer_indents { - if cb( - MultiBufferRow(start_multibuffer_row + row - start_buffer_row), - indent, - ) - .is_break() - { - return; - } - } - } - } - - pub fn indent_size_for_line(&self, row: MultiBufferRow) -> IndentSize { - if let Some((buffer, range)) = self.buffer_line_for_row(row) { - let mut size = buffer.indent_size_for_line(range.start.row); - size.len = size - .len - .min(range.end.column) - .saturating_sub(range.start.column); - size - } else { - IndentSize::spaces(0) - } - } - - pub fn line_indent_for_row(&self, row: MultiBufferRow) -> LineIndent { - if let Some((buffer, range)) = self.buffer_line_for_row(row) { - LineIndent::from_iter(buffer.text_for_range(range).flat_map(|s| s.chars())) - } else { - LineIndent::spaces(0) - } - } - - 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; - } - } - } - - indent - } - - pub fn is_line_whitespace_upto(&self, position: T) -> bool - where - T: ToOffset, - { - for char in self.reversed_chars_at(position) { - if !char.is_whitespace() { - return false; - } - if char == '\n' { - return true; - } - } - true - } - - pub fn prev_non_blank_row(&self, mut row: MultiBufferRow) -> Option { - while row.0 > 0 { - row.0 -= 1; - if !self.is_line_blank(row) { - return Some(row); - } - } - None - } - - pub fn line_len(&self, row: MultiBufferRow) -> u32 { - if let Some((_, range)) = self.buffer_line_for_row(row) { - range.end.column - range.start.column - } else { - 0 - } - } - - pub fn buffer_line_for_row( - &self, - row: MultiBufferRow, - ) -> Option<(&BufferSnapshot, Range)> { - let mut cursor = self.cursor::(); - let point = Point::new(row.0, 0); - cursor.seek(&point); - let region = cursor.region()?; - let overshoot = point.min(region.range.end) - region.range.start; - let buffer_point = region.buffer_range.start + overshoot; - if buffer_point.row > region.buffer_range.end.row { - return None; - } - let line_start = Point::new(buffer_point.row, 0).max(region.buffer_range.start); - let line_end = Point::new(buffer_point.row, region.buffer.line_len(buffer_point.row)) - .min(region.buffer_range.end); - Some((region.buffer, line_start..line_end)) - } - - pub fn max_point(&self) -> Point { - self.text_summary().lines - } - - pub fn max_row(&self) -> MultiBufferRow { - MultiBufferRow(self.text_summary().lines.row) - } - - pub fn text_summary(&self) -> MBTextSummary { - self.diff_transforms.summary().output - } - - pub fn text_summary_for_range(&self, range: Range) -> MBD - where - MBD: MultiBufferDimension + AddAssign, - O: ToOffset, - { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut cursor = self - .diff_transforms - .cursor::>(()); - cursor.seek(&range.start, Bias::Right); - - let Some(first_transform) = cursor.item() else { - return MBD::from_summary(&MBTextSummary::default()); - }; - - let diff_transform_start = cursor.start().0; - let diff_transform_end = cursor.end().0; - let diff_start = range.start; - let start_overshoot = diff_start - diff_transform_start; - let end_overshoot = std::cmp::min(range.end, diff_transform_end) - diff_transform_start; - - let mut result = match first_transform { - DiffTransform::Unmodified { .. } | DiffTransform::InsertedHunk { .. } => { - let excerpt_start = cursor.start().1 + start_overshoot; - let excerpt_end = cursor.start().1 + end_overshoot; - self.text_summary_for_excerpt_offset_range(excerpt_start..excerpt_end) - } - DiffTransform::FilteredInsertedHunk { .. } => MBD::default(), - DiffTransform::DeletedHunk { - buffer_id, - has_trailing_newline, - hunk_info, - .. - } => { - let buffer_start = hunk_info.base_text_byte_range.start + start_overshoot; - let mut buffer_end = hunk_info.base_text_byte_range.start + end_overshoot; - let Some(base_text) = self.diffs.get(buffer_id).map(|diff| diff.base_text()) else { - panic!("{:?} is in non-existent deleted hunk", range.start) - }; - - let include_trailing_newline = - *has_trailing_newline && range.end >= diff_transform_end; - if include_trailing_newline { - buffer_end -= 1; - } - - let mut summary = base_text - .text_summary_for_range::(buffer_start..buffer_end); - - if include_trailing_newline { - summary.add_assign(&::from_text_summary( - &TextSummary::newline(), - )) - } - - let mut result = MBD::default(); - result.add_text_dim(&summary); - result - } - }; - if range.end < diff_transform_end { - return result; - } - - cursor.next(); - result.add_mb_text_summary( - &cursor - .summary::<_, OutputDimension<_>>(&range.end, Bias::Right) - .0, - ); - - let Some(last_transform) = cursor.item() else { - return result; - }; - - let overshoot = range.end - cursor.start().0; - let suffix = match last_transform { - DiffTransform::Unmodified { .. } | DiffTransform::InsertedHunk { .. } => { - let end = cursor.start().1 + overshoot; - self.text_summary_for_excerpt_offset_range::(cursor.start().1..end) - } - DiffTransform::FilteredInsertedHunk { .. } => MBD::default(), - DiffTransform::DeletedHunk { - buffer_id, - has_trailing_newline, - hunk_info, - .. - } => { - let buffer_end = hunk_info.base_text_byte_range.start + overshoot; - let Some(base_text) = self.diffs.get(buffer_id).map(|diff| diff.base_text()) else { - panic!("{:?} is in non-existent deleted hunk", range.end) - }; - - let mut suffix = base_text.text_summary_for_range::( - hunk_info.base_text_byte_range.start..buffer_end, - ); - if *has_trailing_newline && buffer_end == hunk_info.base_text_byte_range.end + 1 { - suffix.add_assign(&::from_text_summary( - &TextSummary::from("\n"), - )) - } - - let mut result = MBD::default(); - result.add_text_dim(&suffix); - result - } - }; - - result += suffix; - result - } - - fn text_summary_for_excerpt_offset_range(&self, mut range: Range) -> MBD - where - MBD: MultiBufferDimension + AddAssign, - { - let mut summary = MBD::default(); - let mut cursor = self.excerpts.cursor::(()); - cursor.seek(&range.start, Bias::Right); - if let Some(excerpt) = cursor.item() { - let mut end_before_newline = cursor.end(); - if excerpt.has_trailing_newline { - end_before_newline -= 1; - } - - let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let start_in_excerpt = excerpt_start + (range.start - *cursor.start()); - let end_in_excerpt = - excerpt_start + (cmp::min(end_before_newline, range.end) - *cursor.start()); - summary.add_text_dim( - &excerpt - .buffer - .text_summary_for_range::( - start_in_excerpt..end_in_excerpt, - ), - ); - - if range.end > end_before_newline { - summary.add_mb_text_summary(&MBTextSummary::from(TextSummary::newline())); - } - - cursor.next(); - } - - if range.end > *cursor.start() { - summary += cursor - .summary::<_, ExcerptDimension>(&range.end, Bias::Right) - .0; - if let Some(excerpt) = cursor.item() { - range.end = cmp::max(*cursor.start(), range.end); - - let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let end_in_excerpt = excerpt_start + (range.end - *cursor.start()); - summary.add_text_dim( - &excerpt - .buffer - .text_summary_for_range::( - excerpt_start..end_in_excerpt, - ), - ); - } - } - - summary - } - - pub fn summary_for_anchor(&self, anchor: &Anchor) -> MBD - where - MBD: MultiBufferDimension - + Ord - + Sub - + AddAssign, - MBD::TextDimension: Sub + Ord, - { - self.summaries_for_anchors([anchor])[0] - } - - fn resolve_summary_for_anchor( - &self, - anchor: &Anchor, - excerpt_position: ExcerptDimension, - diff_transforms: &mut Cursor< - DiffTransform, - Dimensions, OutputDimension>, - >, - ) -> MBD - where - MBD: MultiBufferDimension + Ord + Sub + AddAssign<::Output>, - { - loop { - let transform_end_position = diff_transforms.end().0; - let at_transform_end = - transform_end_position == excerpt_position && diff_transforms.item().is_some(); - if at_transform_end && anchor.text_anchor.bias == Bias::Right { - diff_transforms.next(); - continue; - } - - let mut position = diff_transforms.start().1; - match diff_transforms.item() { - Some(DiffTransform::DeletedHunk { - buffer_id, - hunk_info, - .. - }) => { - if let Some(diff_base_anchor) = &anchor.diff_base_anchor - && let Some(base_text) = - self.diffs.get(buffer_id).map(|diff| diff.base_text()) - && base_text.can_resolve(diff_base_anchor) - { - let base_text_offset = diff_base_anchor.to_offset(base_text); - if base_text_offset >= hunk_info.base_text_byte_range.start - && base_text_offset <= hunk_info.base_text_byte_range.end - { - let position_in_hunk = base_text - .text_summary_for_range::( - hunk_info.base_text_byte_range.start..base_text_offset, - ); - position.0.add_text_dim(&position_in_hunk); - } else if at_transform_end { - diff_transforms.next(); - continue; - } - } - } - _ => { - if at_transform_end && anchor.diff_base_anchor.is_some() { - diff_transforms.next(); - continue; - } - - if !matches!( - diff_transforms.item(), - Some(DiffTransform::FilteredInsertedHunk { .. }) - ) { - let overshoot = excerpt_position - diff_transforms.start().0; - position += overshoot; - } - } - } - - return position.0; - } - } - - fn excerpt_offset_for_anchor(&self, anchor: &Anchor) -> ExcerptOffset { - let mut cursor = self - .excerpts - .cursor::, ExcerptOffset>>(()); - let locator = self.excerpt_locator_for_id(anchor.excerpt_id); - - cursor.seek(&Some(locator), Bias::Left); - if cursor.item().is_none() && anchor.excerpt_id == ExcerptId::max() { - cursor.prev(); - } - - let mut position = cursor.start().1; - if let Some(excerpt) = cursor.item() - && (excerpt.id == anchor.excerpt_id || anchor.excerpt_id == ExcerptId::max()) - { - 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 += buffer_position - excerpt_buffer_start; - } - } - position - } - - pub fn latest_excerpt_id(&self, mut excerpt_id: ExcerptId) -> ExcerptId { - while let Some(replacement) = self.replaced_excerpts.get(&excerpt_id) { - excerpt_id = *replacement; - } - excerpt_id - } - - pub fn summaries_for_anchors<'a, MBD, I>(&'a self, anchors: I) -> Vec - where - MBD: MultiBufferDimension - + Ord - + Sub - + AddAssign, - MBD::TextDimension: Sub + Ord, - I: 'a + IntoIterator, - { - let mut anchors = anchors.into_iter().peekable(); - let mut cursor = self.excerpts.cursor::(()); - let mut diff_transforms_cursor = self - .diff_transforms - .cursor::, OutputDimension>>(()); - diff_transforms_cursor.next(); - - let mut summaries = Vec::new(); - while let Some(anchor) = anchors.peek() { - let excerpt_id = self.latest_excerpt_id(anchor.excerpt_id); - - let excerpt_anchors = anchors.peeking_take_while(|anchor| { - self.latest_excerpt_id(anchor.excerpt_id) == excerpt_id - }); - - let locator = self.excerpt_locator_for_id(excerpt_id); - cursor.seek_forward(locator, Bias::Left); - if cursor.item().is_none() && excerpt_id == ExcerptId::max() { - cursor.prev(); - } - - let excerpt_start_position = ExcerptDimension(MBD::from_summary(&cursor.start().text)); - if let Some(excerpt) = cursor.item() { - if excerpt.id != excerpt_id && excerpt_id != ExcerptId::max() { - let position = self.resolve_summary_for_anchor( - &Anchor::min(), - excerpt_start_position, - &mut diff_transforms_cursor, - ); - summaries.extend(excerpt_anchors.map(|_| position)); - continue; - } - let excerpt_buffer_start = excerpt - .range - .context - .start - .summary::(&excerpt.buffer); - let excerpt_buffer_end = excerpt - .range - .context - .end - .summary::(&excerpt.buffer); - for (buffer_summary, anchor) in excerpt - .buffer - .summaries_for_anchors_with_payload::( - excerpt_anchors.map(|a| (&a.text_anchor, a)), - ) - { - let summary = cmp::min(excerpt_buffer_end, buffer_summary); - let mut position = excerpt_start_position; - if summary > excerpt_buffer_start { - position += summary - excerpt_buffer_start; - } - - if diff_transforms_cursor.start().0 < position { - diff_transforms_cursor.seek_forward(&position, Bias::Left); - } - - summaries.push(self.resolve_summary_for_anchor( - anchor, - position, - &mut diff_transforms_cursor, - )); - } - } else { - diff_transforms_cursor.seek_forward(&excerpt_start_position, Bias::Left); - let position = self.resolve_summary_for_anchor( - &Anchor::max(), - excerpt_start_position, - &mut diff_transforms_cursor, - ); - summaries.extend(excerpt_anchors.map(|_| position)); - } - } - - summaries - } - - pub fn dimensions_from_points<'a, MBD>( - &'a self, - points: impl 'a + IntoIterator, - ) -> impl 'a + Iterator - where - MBD: MultiBufferDimension + Sub + AddAssign<::Output>, - { - let mut cursor = self.cursor::, Point>(); - cursor.seek(&DimensionPair { - key: Point::default(), - value: None, - }); - let mut points = points.into_iter(); - iter::from_fn(move || { - let point = points.next()?; - - cursor.seek_forward(&DimensionPair { - key: point, - value: None, - }); - - if let Some(region) = cursor.region() { - let overshoot = point - region.range.start.key; - let buffer_point = region.buffer_range.start + overshoot; - let mut position = region.range.start.value.unwrap(); - position.add_text_dim( - ®ion - .buffer - .text_summary_for_range(region.buffer_range.start..buffer_point), - ); - if point == region.range.end.key && region.has_trailing_newline { - position.add_mb_text_summary(&MBTextSummary::from(TextSummary::newline())); - } - Some(position) - } else { - Some(MBD::from_summary(&self.text_summary())) - } - }) - } - - pub fn refresh_anchors<'a, I>(&'a self, anchors: I) -> Vec<(usize, Anchor, bool)> - where - I: 'a + IntoIterator, - { - let mut anchors = anchors.into_iter().enumerate().peekable(); - let mut cursor = self.excerpts.cursor::>(()); - cursor.next(); - - let mut result = Vec::new(); - - while let Some((_, anchor)) = anchors.peek() { - let old_excerpt_id = anchor.excerpt_id; - - // Find the location where this anchor's excerpt should be. - let old_locator = self.excerpt_locator_for_id(old_excerpt_id); - cursor.seek_forward(&Some(old_locator), Bias::Left); - - let next_excerpt = cursor.item(); - let prev_excerpt = cursor.prev_item(); - - // Process all of the anchors for this excerpt. - while let Some((anchor_ix, &anchor)) = - anchors.next_if(|(_, anchor)| anchor.excerpt_id == old_excerpt_id) - { - let mut anchor = anchor; - - // 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)) - || old_excerpt_id == ExcerptId::max() - || old_excerpt_id == ExcerptId::min(); - - // If the old excerpt no longer exists at this location, then attempt to - // find an equivalent position for this anchor in an adjacent excerpt. - if !kept_position { - for excerpt in [next_excerpt, prev_excerpt].iter().filter_map(|e| *e) { - if excerpt.contains(&anchor) { - anchor.excerpt_id = excerpt.id; - kept_position = true; - break; - } - } - } - - // If there's no adjacent excerpt that contains the anchor's position, - // then report that the anchor has lost its position. - if !kept_position { - anchor = if let Some(excerpt) = next_excerpt { - let mut text_anchor = excerpt - .range - .context - .start - .bias(anchor.text_anchor.bias, &excerpt.buffer); - if text_anchor - .cmp(&excerpt.range.context.end, &excerpt.buffer) - .is_gt() - { - text_anchor = excerpt.range.context.end; - } - Anchor::in_buffer(excerpt.id, text_anchor) - } else if let Some(excerpt) = prev_excerpt { - let mut text_anchor = excerpt - .range - .context - .end - .bias(anchor.text_anchor.bias, &excerpt.buffer); - if text_anchor - .cmp(&excerpt.range.context.start, &excerpt.buffer) - .is_lt() - { - text_anchor = excerpt.range.context.start; - } - Anchor::in_buffer(excerpt.id, text_anchor) - } else if anchor.text_anchor.bias == Bias::Left { - Anchor::min() - } else { - Anchor::max() - }; - } - - result.push((anchor_ix, anchor, kept_position)); - } - } - result.sort_unstable_by(|a, b| a.1.cmp(&b.1, self)); - result - } - - pub fn anchor_before(&self, position: T) -> Anchor { - self.anchor_at(position, Bias::Left) - } - - pub fn anchor_after(&self, position: T) -> Anchor { - self.anchor_at(position, Bias::Right) - } - - pub fn anchor_at(&self, position: T, mut bias: Bias) -> Anchor { - let offset = position.to_offset(self); - - // 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::>(()); - 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(); - } - let offset_in_transform = offset - diff_transforms.start().0; - let mut excerpt_offset = diff_transforms.start().1; - let mut diff_base_anchor = None; - if let Some(DiffTransform::DeletedHunk { - buffer_id, - has_trailing_newline, - hunk_info, - .. - }) = diff_transforms.item() - { - let base_text_byte_range = &hunk_info.base_text_byte_range; - let diff = self.diffs.get(buffer_id).expect("missing diff"); - if offset_in_transform > base_text_byte_range.len() { - debug_assert!(*has_trailing_newline); - bias = Bias::Right; - } else { - diff_base_anchor = Some( - diff.base_text() - .anchor_at(base_text_byte_range.start + offset_in_transform, bias), - ); - bias = Bias::Left; - } - } else { - excerpt_offset += MultiBufferOffset(offset_in_transform); - }; - - let mut excerpts = self - .excerpts - .cursor::>>(()); - excerpts.seek(&excerpt_offset, Bias::Right); - if excerpts.item().is_none() && excerpt_offset == excerpts.start().0 && bias == Bias::Left { - excerpts.prev(); - } - if let Some(excerpt) = excerpts.item() { - let mut overshoot = excerpt_offset.saturating_sub(excerpts.start().0); - if excerpt.has_trailing_newline && excerpt_offset == excerpts.end().0 { - overshoot -= 1; - bias = Bias::Right; - } - - let buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let text_anchor = - excerpt.clip_anchor(excerpt.buffer.anchor_at(buffer_start + overshoot, bias)); - let anchor = Anchor::in_buffer(excerpt.id, text_anchor); - match diff_base_anchor { - Some(diff_base_anchor) => anchor.with_diff_base_anchor(diff_base_anchor), - None => anchor, - } - } else if excerpt_offset == ExcerptDimension(MultiBufferOffset::ZERO) && bias == Bias::Left - { - Anchor::min() - } else { - Anchor::max() - } - } - - /// Wraps the [`text::Anchor`] in a [`multi_buffer::Anchor`] if this multi-buffer is a singleton. - pub fn as_singleton_anchor(&self, text_anchor: text::Anchor) -> Option { - let (excerpt, buffer, _) = self.as_singleton()?; - if text_anchor.buffer_id.is_none_or(|id| id == buffer) { - Some(Anchor::in_buffer(*excerpt, text_anchor)) - } else { - None - } - } - - /// Returns an anchor for the given excerpt and text anchor, - /// Returns [`None`] if the excerpt_id is no longer valid or the text anchor range is out of excerpt's bounds. - pub fn anchor_range_in_excerpt( - &self, - excerpt_id: ExcerptId, - text_anchor: Range, - ) -> Option> { - let excerpt = self.excerpt(self.latest_excerpt_id(excerpt_id))?; - - Some( - Self::anchor_in_excerpt_(excerpt, text_anchor.start)? - ..Self::anchor_in_excerpt_(excerpt, text_anchor.end)?, - ) - } - - /// Returns an anchor for the given excerpt and text anchor, - /// Returns [`None`] if the excerpt_id is no longer valid or the text anchor range is out of excerpt's bounds. - pub fn anchor_in_excerpt( - &self, - excerpt_id: ExcerptId, - text_anchor: text::Anchor, - ) -> Option { - let excerpt = self.excerpt(self.latest_excerpt_id(excerpt_id))?; - Self::anchor_in_excerpt_(excerpt, text_anchor) - } - - /// Same as [`MultiBuffer::anchor_in_excerpt`], but more efficient than calling it multiple times. - pub fn anchors_in_excerpt( - &self, - excerpt_id: ExcerptId, - text_anchors: impl IntoIterator, - ) -> Option>> { - let excerpt = self.excerpt(self.latest_excerpt_id(excerpt_id))?; - Some( - text_anchors - .into_iter() - .map(|text_anchor| Self::anchor_in_excerpt_(excerpt, text_anchor)), - ) - } - - fn anchor_in_excerpt_(excerpt: &Excerpt, text_anchor: text::Anchor) -> Option { - match text_anchor.buffer_id { - Some(buffer_id) if buffer_id == excerpt.buffer_id => (), - Some(_) => return None, - None if text_anchor.is_max() || text_anchor.is_min() => { - return Some(Anchor::in_buffer(excerpt.id, text_anchor)); - } - None => return None, - } - - let context = &excerpt.range.context; - if context.start.cmp(&text_anchor, &excerpt.buffer).is_gt() - || context.end.cmp(&text_anchor, &excerpt.buffer).is_lt() - { - return None; - } - - Some(Anchor::in_buffer(excerpt.id, text_anchor)) - } - - pub fn context_range_for_excerpt(&self, excerpt_id: ExcerptId) -> Option> { - Some(self.excerpt(excerpt_id)?.range.context.clone()) - } - - pub fn can_resolve(&self, anchor: &Anchor) -> bool { - if anchor.is_min() || anchor.is_max() { - // todo(lw): should be `!self.is_empty()` - true - } else if let Some(excerpt) = self.excerpt(anchor.excerpt_id) { - excerpt.buffer.can_resolve(&anchor.text_anchor) - } else { - false - } - } - - pub fn excerpts( - &self, - ) -> impl Iterator)> { - self.excerpts - .iter() - .map(|excerpt| (excerpt.id, &excerpt.buffer, excerpt.range.clone())) - } - - fn cursor<'a, MBD, BD>(&'a self) -> MultiBufferCursor<'a, MBD, BD> - where - MBD: MultiBufferDimension + Ord + Sub + ops::AddAssign<::Output>, - BD: TextDimension + AddAssign<::Output>, - { - let excerpts = self.excerpts.cursor(()); - let diff_transforms = self.diff_transforms.cursor(()); - MultiBufferCursor { - excerpts, - diff_transforms, - snapshot: &self, - cached_region: None, - } - } - - pub fn excerpt_before(&self, excerpt_id: ExcerptId) -> Option> { - let start_locator = self.excerpt_locator_for_id(excerpt_id); - let mut excerpts = self - .excerpts - .cursor::, ExcerptOffset>>(()); - excerpts.seek(&Some(start_locator), Bias::Left); - excerpts.prev(); - - let mut diff_transforms = self - .diff_transforms - .cursor::>(()); - diff_transforms.seek(&excerpts.start().1, Bias::Left); - if diff_transforms.end().excerpt_dimension < excerpts.start().1 { - diff_transforms.next(); - } - - let excerpt = excerpts.item()?; - Some(MultiBufferExcerpt { - excerpt, - offset: diff_transforms.start().output_dimension.0, - buffer_offset: BufferOffset(excerpt.range.context.start.to_offset(&excerpt.buffer)), - excerpt_offset: excerpts.start().1, - diff_transforms, - }) - } - - pub fn excerpt_boundaries_in_range( - &self, - range: R, - ) -> impl Iterator + '_ - where - R: RangeBounds, - T: ToOffset, - { - let start_offset; - let start = match range.start_bound() { - Bound::Included(start) => { - start_offset = start.to_offset(self); - Bound::Included(start_offset) - } - Bound::Excluded(_) => { - panic!("not supported") - } - Bound::Unbounded => { - start_offset = MultiBufferOffset::ZERO; - Bound::Unbounded - } - }; - let end = match range.end_bound() { - Bound::Included(end) => Bound::Included(end.to_offset(self)), - Bound::Excluded(end) => Bound::Excluded(end.to_offset(self)), - Bound::Unbounded => Bound::Unbounded, - }; - let bounds = (start, end); - let mut cursor = self.cursor::, BufferOffset>(); - cursor.seek(&DimensionPair { - key: start_offset, - value: None, - }); - - if cursor - .region() - .is_some_and(|region| bounds.contains(®ion.range.start.key)) - { - cursor.prev_excerpt(); - } else { - cursor.seek_to_start_of_current_excerpt(); - } - let mut prev_region = cursor.region(); - - cursor.next_excerpt(); - - iter::from_fn(move || { - loop { - if self.singleton { - return None; - } - - let next_region = cursor.region()?; - cursor.next_excerpt(); - if !bounds.contains(&next_region.range.start.key) { - prev_region = Some(next_region); - continue; - } - - let next_region_start = next_region.range.start.value.unwrap(); - let next_region_end = if let Some(region) = cursor.region() { - region.range.start.value.unwrap() - } else { - self.max_point() - }; - - let prev = prev_region.as_ref().map(|region| ExcerptInfo { - id: region.excerpt.id, - buffer: region.excerpt.buffer.clone(), - buffer_id: region.excerpt.buffer_id, - range: region.excerpt.range.clone(), - end_row: MultiBufferRow(next_region_start.row), - }); - - let next = ExcerptInfo { - id: next_region.excerpt.id, - buffer: next_region.excerpt.buffer.clone(), - buffer_id: next_region.excerpt.buffer_id, - range: next_region.excerpt.range.clone(), - end_row: if next_region.excerpt.has_trailing_newline { - MultiBufferRow(next_region_end.row - 1) - } else { - MultiBufferRow(next_region_end.row) - }, - }; - - let row = MultiBufferRow(next_region_start.row); - - prev_region = Some(next_region); - - return Some(ExcerptBoundary { row, prev, next }); - } - }) - } - - pub fn edit_count(&self) -> usize { - self.edit_count - } - - pub fn non_text_state_update_count(&self) -> usize { - self.non_text_state_update_count - } - - /// Returns the smallest enclosing bracket ranges containing the given range or - /// None if no brackets contain range or the range is not contained in a single - /// excerpt - /// - /// Can optionally pass a range_filter to filter the ranges of brackets to consider - #[ztracing::instrument(skip_all)] - pub fn innermost_enclosing_bracket_ranges( - &self, - range: Range, - range_filter: Option< - &dyn Fn(&BufferSnapshot, Range, Range) -> bool, - >, - ) -> Option<(Range, Range)> { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut excerpt = self.excerpt_containing(range.clone())?; - let buffer = excerpt.buffer(); - let excerpt_buffer_range = excerpt.buffer_range(); - - // Filter to ranges contained in the excerpt - let range_filter = |open: Range, close: Range| -> bool { - excerpt_buffer_range.contains(&BufferOffset(open.start)) - && excerpt_buffer_range.contains(&BufferOffset(close.end)) - && range_filter.is_none_or(|filter| { - filter( - buffer, - BufferOffset(open.start)..BufferOffset(close.end), - BufferOffset(close.start)..BufferOffset(close.end), - ) - }) - }; - - let (open, close) = excerpt.buffer().innermost_enclosing_bracket_ranges( - excerpt.map_range_to_buffer(range), - Some(&range_filter), - )?; - - Some(( - excerpt.map_range_from_buffer(BufferOffset(open.start)..BufferOffset(open.end)), - excerpt.map_range_from_buffer(BufferOffset(close.start)..BufferOffset(close.end)), - )) - } - - /// Returns enclosing bracket ranges containing the given range or returns None if the range is - /// not contained in a single excerpt - pub fn enclosing_bracket_ranges( - &self, - range: Range, - ) -> Option, Range)> + '_> - { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut excerpt = self.excerpt_containing(range.clone())?; - - Some( - excerpt - .buffer() - .enclosing_bracket_ranges(excerpt.map_range_to_buffer(range)) - .filter_map(move |pair| { - let open_range = - BufferOffset(pair.open_range.start)..BufferOffset(pair.open_range.end); - let close_range = - BufferOffset(pair.close_range.start)..BufferOffset(pair.close_range.end); - if excerpt.contains_buffer_range(open_range.start..close_range.end) { - Some(( - excerpt.map_range_from_buffer(open_range), - excerpt.map_range_from_buffer(close_range), - )) - } else { - None - } - }), - ) - } - - /// Returns enclosing bracket ranges containing the given range or returns None if the range is - /// not contained in a single excerpt - pub fn text_object_ranges( - &self, - range: Range, - options: TreeSitterOptions, - ) -> impl Iterator, TextObject)> + '_ { - let range = range.start.to_offset(self)..range.end.to_offset(self); - self.excerpt_containing(range.clone()) - .map(|mut excerpt| { - excerpt - .buffer() - .text_object_ranges(excerpt.map_range_to_buffer(range), options) - .filter_map(move |(range, text_object)| { - let range = BufferOffset(range.start)..BufferOffset(range.end); - if excerpt.contains_buffer_range(range.clone()) { - Some((excerpt.map_range_from_buffer(range), text_object)) - } else { - None - } - }) - }) - .into_iter() - .flatten() - } - - /// Returns bracket range pairs overlapping the given `range` or returns None if the `range` is - /// not contained in a single excerpt - pub fn bracket_ranges( - &self, - range: Range, - ) -> Option, Range)> + '_> - { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut excerpt = self.excerpt_containing(range.clone())?; - Some( - excerpt - .buffer() - .bracket_ranges(excerpt.map_range_to_buffer(range)) - .filter_map(move |pair| { - let open_range = - BufferOffset(pair.open_range.start)..BufferOffset(pair.open_range.end); - let close_range = - BufferOffset(pair.close_range.start)..BufferOffset(pair.close_range.end); - excerpt - .contains_buffer_range(open_range.start..close_range.end) - .then(|| BracketMatch { - open_range: excerpt.map_range_from_buffer(open_range), - close_range: excerpt.map_range_from_buffer(close_range), - color_index: pair.color_index, - newline_only: pair.newline_only, - syntax_layer_depth: pair.syntax_layer_depth, - }) - }) - .map(BracketMatch::bracket_ranges), - ) - } - - pub fn redacted_ranges<'a, T: ToOffset>( - &'a self, - range: Range, - redaction_enabled: impl Fn(Option<&Arc>) -> bool + 'a, - ) -> impl Iterator> + 'a { - let range = range.start.to_offset(self)..range.end.to_offset(self); - self.lift_buffer_metadata(range, move |buffer, range| { - if redaction_enabled(buffer.file()) { - Some(buffer.redacted_ranges(range).map(|range| (range, ()))) - } else { - None - } - }) - .map(|(range, _, _)| range) - } - - pub fn runnable_ranges( - &self, - range: Range, - ) -> impl Iterator, language::RunnableRange)> + '_ { - let range = range.start.to_offset(self)..range.end.to_offset(self); - self.lift_buffer_metadata(range, move |buffer, range| { - Some( - buffer - .runnable_ranges(range.clone()) - .filter(move |runnable| { - runnable.run_range.start >= range.start - && runnable.run_range.end < range.end - }) - .map(|runnable| (runnable.run_range.clone(), runnable)), - ) - }) - .map(|(run_range, runnable, _)| (run_range, runnable)) - } - - pub fn line_indents( - &self, - start_row: MultiBufferRow, - buffer_filter: impl Fn(&BufferSnapshot) -> bool, - ) -> impl Iterator { - let max_point = self.max_point(); - let mut cursor = self.cursor::(); - cursor.seek(&Point::new(start_row.0, 0)); - iter::from_fn(move || { - let mut region = cursor.region()?; - while !buffer_filter(®ion.excerpt.buffer) { - cursor.next(); - region = cursor.region()?; - } - let overshoot = start_row.0.saturating_sub(region.range.start.row); - let buffer_start_row = - (region.buffer_range.start.row + overshoot).min(region.buffer_range.end.row); - - let buffer_end_row = if region.is_main_buffer - && (region.has_trailing_newline || region.range.end == max_point) - { - region.buffer_range.end.row - } else { - region.buffer_range.end.row.saturating_sub(1) - }; - - let line_indents = region - .buffer - .line_indents_in_row_range(buffer_start_row..buffer_end_row); - cursor.next(); - 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() - } - - pub fn reversed_line_indents( - &self, - end_row: MultiBufferRow, - buffer_filter: impl Fn(&BufferSnapshot) -> bool, - ) -> impl Iterator { - let max_point = self.max_point(); - let mut cursor = self.cursor::(); - cursor.seek(&Point::new(end_row.0, 0)); - iter::from_fn(move || { - let mut region = cursor.region()?; - while !buffer_filter(®ion.excerpt.buffer) { - cursor.prev(); - region = cursor.region()?; - } - - let buffer_start_row = region.buffer_range.start.row; - let buffer_end_row = if region.is_main_buffer - && (region.has_trailing_newline || region.range.end == max_point) - { - region.buffer_range.end.row + 1 - } else { - region.buffer_range.end.row - }; - - let overshoot = end_row.0 - region.range.start.row; - let buffer_end_row = - (region.buffer_range.start.row + overshoot + 1).min(buffer_end_row); - - let line_indents = region - .buffer - .reversed_line_indents_in_row_range(buffer_start_row..buffer_end_row); - cursor.prev(); - 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() - } - - pub async fn enclosing_indent( - &self, - mut target_row: MultiBufferRow, - ) -> Option<(Range, LineIndent)> { - let max_row = MultiBufferRow(self.max_point().row); - if target_row >= max_row { - return None; - } - - let mut target_indent = self.line_indent_for_row(target_row); - - // If the current row is at the start of an indented block, we want to return this - // block as the enclosing indent. - if !target_indent.is_line_empty() && target_row < max_row { - let next_line_indent = self.line_indent_for_row(MultiBufferRow(target_row.0 + 1)); - if !next_line_indent.is_line_empty() - && target_indent.raw_len() < next_line_indent.raw_len() - { - target_indent = next_line_indent; - target_row.0 += 1; - } - } - - const SEARCH_ROW_LIMIT: u32 = 25000; - const SEARCH_WHITESPACE_ROW_LIMIT: u32 = 2500; - const YIELD_INTERVAL: u32 = 100; - - let mut accessed_row_counter = 0; - - // If there is a blank line at the current row, search for the next non indented lines - if target_indent.is_line_empty() { - let start = MultiBufferRow(target_row.0.saturating_sub(SEARCH_WHITESPACE_ROW_LIMIT)); - let end = - MultiBufferRow((max_row.0 + 1).min(target_row.0 + SEARCH_WHITESPACE_ROW_LIMIT)); - - let mut non_empty_line_above = None; - for (row, indent, _) in self.reversed_line_indents(target_row, |_| true) { - if row < start { - break; - } - accessed_row_counter += 1; - if accessed_row_counter == YIELD_INTERVAL { - accessed_row_counter = 0; - yield_now().await; - } - if !indent.is_line_empty() { - non_empty_line_above = Some((row, indent)); - break; - } - } - - let mut non_empty_line_below = None; - for (row, indent, _) in self.line_indents(target_row, |_| true) { - if row > end { - break; - } - accessed_row_counter += 1; - if accessed_row_counter == YIELD_INTERVAL { - accessed_row_counter = 0; - yield_now().await; - } - if !indent.is_line_empty() { - non_empty_line_below = Some((row, indent)); - break; - } - } - - let (row, indent) = match (non_empty_line_above, non_empty_line_below) { - (Some((above_row, above_indent)), Some((below_row, below_indent))) => { - if above_indent.raw_len() >= below_indent.raw_len() { - (above_row, above_indent) - } else { - (below_row, below_indent) - } - } - (Some(above), None) => above, - (None, Some(below)) => below, - _ => return None, - }; - - target_indent = indent; - target_row = row; - } - - let start = MultiBufferRow(target_row.0.saturating_sub(SEARCH_ROW_LIMIT)); - let end = MultiBufferRow((max_row.0 + 1).min(target_row.0 + SEARCH_ROW_LIMIT)); - - let mut start_indent = None; - for (row, indent, _) in self.reversed_line_indents(target_row, |_| true) { - if row < start { - break; - } - accessed_row_counter += 1; - if accessed_row_counter == YIELD_INTERVAL { - accessed_row_counter = 0; - yield_now().await; - } - if !indent.is_line_empty() && indent.raw_len() < target_indent.raw_len() { - start_indent = Some((row, indent)); - break; - } - } - let (start_row, start_indent_size) = start_indent?; - - let mut end_indent = (end, None); - for (row, indent, _) in self.line_indents(target_row, |_| true) { - if row > end { - break; - } - accessed_row_counter += 1; - if accessed_row_counter == YIELD_INTERVAL { - accessed_row_counter = 0; - yield_now().await; - } - if !indent.is_line_empty() && indent.raw_len() < target_indent.raw_len() { - end_indent = (MultiBufferRow(row.0.saturating_sub(1)), Some(indent)); - break; - } - } - let (end_row, end_indent_size) = end_indent; - - let indent = if let Some(end_indent_size) = end_indent_size { - if start_indent_size.raw_len() > end_indent_size.raw_len() { - start_indent_size - } else { - end_indent_size - } - } else { - start_indent_size - }; - - Some((start_row..end_row, indent)) - } - - pub fn indent_guides_in_range( - &self, - range: Range, - ignore_disabled_for_language: bool, - cx: &App, - ) -> impl Iterator { - let range = range.start.to_point(self)..range.end.to_point(self); - let start_row = MultiBufferRow(range.start.row); - let end_row = MultiBufferRow(range.end.row); - - let mut row_indents = self.line_indents(start_row, |buffer| { - let settings = - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx); - settings.indent_guides.enabled || ignore_disabled_for_language - }); - - let mut result = Vec::new(); - let mut indent_stack = SmallVec::<[IndentGuide; 8]>::new(); - - let mut prev_settings = None; - while let Some((first_row, mut line_indent, buffer)) = row_indents.next() { - if first_row > end_row { - break; - } - 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(); - } - let settings = &prev_settings - .get_or_insert_with(|| { - ( - buffer.remote_id(), - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx), - ) - }) - .1; - let tab_size = settings.tab_size.get(); - - // When encountering empty, continue until found useful line indent - // then add to the indent stack with the depth found - let mut found_indent = false; - let mut last_row = first_row; - if line_indent.is_line_blank() { - while !found_indent { - let Some((target_row, new_line_indent, _)) = row_indents.next() else { - break; - }; - const TRAILING_ROW_SEARCH_LIMIT: u32 = 25; - if target_row > MultiBufferRow(end_row.0 + TRAILING_ROW_SEARCH_LIMIT) { - break; - } - - if new_line_indent.is_line_blank() { - continue; - } - last_row = target_row.min(end_row); - line_indent = new_line_indent; - found_indent = true; - break; - } - } else { - found_indent = true - } - - let depth = if found_indent { - line_indent.len(tab_size) / tab_size - } else { - 0 - }; - - match depth.cmp(¤t_depth) { - cmp::Ordering::Less => { - for _ in 0..(current_depth - depth) { - let mut indent = indent_stack.pop().unwrap(); - if last_row != first_row { - // In this case, we landed on an empty row, had to seek forward, - // and discovered that the indent we where on is ending. - // This means that the last display row must - // be on line that ends this indent range, so we - // should display the range up to the first non-empty line - indent.end_row = MultiBufferRow(first_row.0.saturating_sub(1)); - } - - result.push(indent) - } - } - cmp::Ordering::Greater => { - for next_depth in current_depth..depth { - indent_stack.push(IndentGuide { - buffer_id: buffer.remote_id(), - start_row: first_row, - end_row: last_row, - depth: next_depth, - tab_size, - settings: settings.indent_guides.clone(), - }); - } - } - _ => {} - } - - for indent in indent_stack.iter_mut() { - indent.end_row = last_row; - } - } - - result.extend(indent_stack); - result.into_iter() - } - - pub fn trailing_excerpt_update_count(&self) -> usize { - self.trailing_excerpt_update_count - } - - pub fn file_at(&self, point: T) -> Option<&Arc> { - self.point_to_buffer_offset(point) - .and_then(|(buffer, _)| buffer.file()) - } - - pub fn language_at(&self, offset: T) -> Option<&Arc> { - self.point_to_buffer_offset(offset) - .and_then(|(buffer, offset)| buffer.language_at(offset)) - } - - fn language_settings<'a>(&'a self, cx: &'a App) -> Cow<'a, LanguageSettings> { - self.excerpts - .first() - .map(|excerpt| &excerpt.buffer) - .map(|buffer| { - language_settings( - buffer.language().map(|language| language.name()), - buffer.file(), - cx, - ) - }) - .unwrap_or_else(move || self.language_settings_at(MultiBufferOffset::ZERO, cx)) - } - - pub fn language_settings_at<'a, T: ToOffset>( - &'a self, - point: T, - cx: &'a App, - ) -> Cow<'a, LanguageSettings> { - let mut language = None; - let mut file = None; - if let Some((buffer, offset)) = self.point_to_buffer_offset(point) { - language = buffer.language_at(offset); - file = buffer.file(); - } - language_settings(language.map(|l| l.name()), file, cx) - } - - pub fn language_scope_at(&self, point: T) -> Option { - self.point_to_buffer_offset(point) - .and_then(|(buffer, offset)| buffer.language_scope_at(offset)) - } - - pub fn char_classifier_at(&self, point: T) -> CharClassifier { - self.point_to_buffer_offset(point) - .map(|(buffer, offset)| buffer.char_classifier_at(offset)) - .unwrap_or_default() - } - - pub fn language_indent_size_at( - &self, - position: T, - cx: &App, - ) -> Option { - let (buffer_snapshot, offset) = self.point_to_buffer_offset(position)?; - Some(buffer_snapshot.language_indent_size_at(offset, cx)) - } - - pub fn is_dirty(&self) -> bool { - self.is_dirty - } - - pub fn has_deleted_file(&self) -> bool { - self.has_deleted_file - } - - pub fn has_conflict(&self) -> bool { - self.has_conflict - } - - pub fn has_diagnostics(&self) -> bool { - self.excerpts - .iter() - .any(|excerpt| excerpt.buffer.has_diagnostics()) - } - - pub fn diagnostic_group( - &self, - buffer_id: BufferId, - group_id: usize, - ) -> impl Iterator> + '_ { - self.lift_buffer_metadata::( - Point::zero()..self.max_point(), - move |buffer, range| { - if buffer.remote_id() != buffer_id { - return None; - }; - Some( - buffer - .diagnostics_in_range(range, false) - .filter(move |diagnostic| diagnostic.diagnostic.group_id == group_id) - .map(move |DiagnosticEntryRef { diagnostic, range }| (range, diagnostic)), - ) - }, - ) - .map(|(range, diagnostic, _)| DiagnosticEntryRef { diagnostic, range }) - } - - pub fn diagnostics_in_range<'a, MBD>( - &'a self, - range: Range, - ) -> impl Iterator> + 'a - where - MBD::TextDimension: 'a - + text::ToOffset - + text::FromAnchor - + Sub - + fmt::Debug - + ops::Add - + ops::AddAssign - + Ord, - MBD: MultiBufferDimension - + Ord - + Sub - + ops::Add - + ops::AddAssign - + 'a, - { - self.lift_buffer_metadata::(range, move |buffer, buffer_range| { - Some( - buffer - .diagnostics_in_range(buffer_range.start..buffer_range.end, false) - .map(|entry| (entry.range, entry.diagnostic)), - ) - }) - .map(|(range, diagnostic, _)| DiagnosticEntryRef { diagnostic, range }) - } - - pub fn diagnostics_with_buffer_ids_in_range<'a, MBD>( - &'a self, - range: Range, - ) -> impl Iterator)> + 'a - where - MBD: MultiBufferDimension - + Ord - + Sub - + ops::Add - + ops::AddAssign, - MBD::TextDimension: Sub - + ops::Add - + text::ToOffset - + text::FromAnchor - + AddAssign - + Ord, - { - self.lift_buffer_metadata::(range, move |buffer, buffer_range| { - Some( - buffer - .diagnostics_in_range(buffer_range.start..buffer_range.end, false) - .map(|entry| (entry.range, entry.diagnostic)), - ) - }) - .map(|(range, diagnostic, b)| (b.buffer_id, DiagnosticEntryRef { diagnostic, range })) - } - - pub fn syntax_ancestor( - &self, - range: Range, - ) -> Option<(tree_sitter::Node<'_>, Range)> { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut excerpt = self.excerpt_containing(range.clone())?; - let node = excerpt - .buffer() - .syntax_ancestor(excerpt.map_range_to_buffer(range))?; - let node_range = node.byte_range(); - let node_range = BufferOffset(node_range.start)..BufferOffset(node_range.end); - if !excerpt.contains_buffer_range(node_range.clone()) { - return None; - }; - Some((node, excerpt.map_range_from_buffer(node_range))) - } - - pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option> { - let (excerpt_id, _, buffer) = self.as_singleton()?; - let outline = buffer.outline(theme); - Some(Outline::new( - outline - .items - .into_iter() - .flat_map(|item| { - Some(OutlineItem { - depth: item.depth, - range: self.anchor_range_in_excerpt(*excerpt_id, item.range)?, - source_range_for_text: self - .anchor_range_in_excerpt(*excerpt_id, item.source_range_for_text)?, - text: item.text, - highlight_ranges: item.highlight_ranges, - name_ranges: item.name_ranges, - body_range: item.body_range.and_then(|body_range| { - self.anchor_range_in_excerpt(*excerpt_id, body_range) - }), - annotation_range: item.annotation_range.and_then(|annotation_range| { - self.anchor_range_in_excerpt(*excerpt_id, annotation_range) - }), - }) - }) - .collect(), - )) - } - - pub fn symbols_containing( - &self, - offset: T, - theme: Option<&SyntaxTheme>, - ) -> Option<(BufferId, Vec>)> { - let anchor = self.anchor_before(offset); - let excerpt @ &Excerpt { - id: excerpt_id, - buffer_id, - ref buffer, - .. - } = self.excerpt(anchor.excerpt_id)?; - if cfg!(debug_assertions) { - match anchor.text_anchor.buffer_id { - // we clearly are hitting this according to sentry, but in what situations can this occur? - Some(anchor_buffer_id) => { - assert_eq!( - anchor_buffer_id, buffer_id, - "anchor {anchor:?} does not match with resolved excerpt {excerpt:?}" - ) - } - None => assert!(anchor.is_max()), - } - }; - Some(( - buffer_id, - buffer - .symbols_containing(anchor.text_anchor, theme) - .into_iter() - .flat_map(|item| { - Some(OutlineItem { - depth: item.depth, - source_range_for_text: Anchor::range_in_buffer( - excerpt_id, - item.source_range_for_text, - ), - range: Anchor::range_in_buffer(excerpt_id, item.range), - text: item.text, - highlight_ranges: item.highlight_ranges, - name_ranges: item.name_ranges, - body_range: item - .body_range - .map(|body_range| Anchor::range_in_buffer(excerpt_id, body_range)), - annotation_range: item - .annotation_range - .map(|body_range| Anchor::range_in_buffer(excerpt_id, body_range)), - }) - }) - .collect(), - )) - } - - fn excerpt_locator_for_id(&self, id: ExcerptId) -> &Locator { - if id == ExcerptId::min() { - Locator::min_ref() - } else if id == ExcerptId::max() { - Locator::max_ref() - } else { - let (_, _, item) = self.excerpt_ids.find::((), &id, Bias::Left); - if let Some(entry) = item - && entry.id == id - { - return &entry.locator; - } - panic!("invalid excerpt id {id:?}") - } - } - - /// Returns the locators referenced by the given excerpt IDs, sorted by locator. - fn excerpt_locators_for_ids( - &self, - ids: impl IntoIterator, - ) -> SmallVec<[Locator; 1]> { - let mut sorted_ids = ids.into_iter().collect::>(); - sorted_ids.sort_unstable(); - sorted_ids.dedup(); - let mut locators = SmallVec::new(); - - while sorted_ids.last() == Some(&ExcerptId::max()) { - sorted_ids.pop(); - locators.push(Locator::max()); - } - - let mut sorted_ids = sorted_ids.into_iter().peekable(); - locators.extend( - sorted_ids - .peeking_take_while(|excerpt| *excerpt == ExcerptId::min()) - .map(|_| Locator::min()), - ); - - let mut cursor = self.excerpt_ids.cursor::(()); - for id in sorted_ids { - if cursor.seek_forward(&id, Bias::Left) { - locators.push(cursor.item().unwrap().locator.clone()); - } else { - panic!("invalid excerpt id {:?}", id); - } - } - - locators.sort_unstable(); - locators - } - - pub fn buffer_id_for_excerpt(&self, excerpt_id: ExcerptId) -> Option { - Some(self.excerpt(excerpt_id)?.buffer_id) - } - - pub fn buffer_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<&BufferSnapshot> { - Some(&self.excerpt(excerpt_id)?.buffer) - } - - pub fn range_for_excerpt(&self, excerpt_id: ExcerptId) -> Option> { - let mut cursor = self - .excerpts - .cursor::, ExcerptPoint>>(()); - let locator = self.excerpt_locator_for_id(excerpt_id); - let mut sought_exact = cursor.seek(&Some(locator), Bias::Left); - if cursor.item().is_none() && excerpt_id == ExcerptId::max() { - sought_exact = true; - cursor.prev(); - } else if excerpt_id == ExcerptId::min() { - sought_exact = true; - } - if sought_exact { - let start = cursor.start().1; - let end = cursor.end().1; - let mut diff_transforms = self - .diff_transforms - .cursor::>>(()); - diff_transforms.seek(&start, Bias::Left); - let overshoot = start - diff_transforms.start().0; - let start = diff_transforms.start().1 + overshoot; - diff_transforms.seek(&end, Bias::Right); - let overshoot = end - diff_transforms.start().0; - let end = diff_transforms.start().1 + overshoot; - Some(start.0..end.0) - } else { - None - } - } - - /// Returns the excerpt for the given id. The returned excerpt is guaranteed - /// to have the latest excerpt id for the one passed in and will also remap - /// `ExcerptId::max()` to the corresponding excertp ID. - /// - /// Callers of this function should generally use the resulting excerpt's `id` field - /// afterwards. - fn excerpt(&self, excerpt_id: ExcerptId) -> Option<&Excerpt> { - let excerpt_id = self.latest_excerpt_id(excerpt_id); - 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); - } else if cursor.item().is_none() && excerpt_id == ExcerptId::max() { - cursor.prev(); - return cursor.item(); - } - None - } - - /// Returns the excerpt containing range and its offset start within the multibuffer or none if `range` spans multiple excerpts - pub fn excerpt_containing( - &self, - range: Range, - ) -> Option> { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut cursor = self.cursor::(); - cursor.seek(&range.start); - - let start_excerpt = cursor.excerpt()?; - if range.end != range.start { - cursor.seek_forward(&range.end); - if cursor.excerpt()?.id != start_excerpt.id { - return None; - } - } - - cursor.seek_to_start_of_current_excerpt(); - let region = cursor.region()?; - let offset = region.range.start; - let buffer_offset = start_excerpt.buffer_start_offset(); - let excerpt_offset = *cursor.excerpts.start(); - Some(MultiBufferExcerpt { - diff_transforms: cursor.diff_transforms, - excerpt: start_excerpt, - offset, - buffer_offset, - excerpt_offset, - }) - } - - pub fn buffer_id_for_anchor(&self, anchor: Anchor) -> Option { - if let Some(id) = anchor.text_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, - include_local: bool, - ) -> impl 'a + Iterator)> { - let mut cursor = self.excerpts.cursor::(()); - let start_locator = self.excerpt_locator_for_id(range.start.excerpt_id); - let end_locator = self.excerpt_locator_for_id(range.end.excerpt_id); - cursor.seek(start_locator, Bias::Left); - cursor - .take_while(move |excerpt| excerpt.locator <= *end_locator) - .flat_map(move |excerpt| { - let mut query_range = excerpt.range.context.start..excerpt.range.context.end; - if excerpt.id == range.start.excerpt_id { - query_range.start = range.start.text_anchor; - } - if excerpt.id == range.end.excerpt_id { - query_range.end = range.end.text_anchor; - } - - excerpt - .buffer - .selections_in_range(query_range, include_local) - .flat_map(move |(replica_id, line_mode, cursor_shape, selections)| { - selections.map(move |selection| { - let mut start = Anchor::in_buffer(excerpt.id, selection.start); - let mut end = Anchor::in_buffer(excerpt.id, selection.end); - if range.start.cmp(&start, self).is_gt() { - start = range.start; - } - if range.end.cmp(&end, self).is_lt() { - end = range.end; - } - - ( - replica_id, - line_mode, - cursor_shape, - Selection { - id: selection.id, - start, - end, - reversed: selection.reversed, - goal: selection.goal, - }, - ) - }) - }) - }) - } - - pub fn show_headers(&self) -> bool { - self.show_headers - } - - pub fn diff_for_buffer_id(&self, buffer_id: BufferId) -> Option<&BufferDiffSnapshot> { - self.diffs.get(&buffer_id) - } - - /// Visually annotates a position or range with the `Debug` representation of a value. The - /// callsite of this function is used as a key - previous annotations will be removed. - #[cfg(debug_assertions)] - #[track_caller] - pub fn debug(&self, ranges: &R, value: V) - where - R: debug::ToMultiBufferDebugRanges, - V: std::fmt::Debug, - { - self.debug_with_key(std::panic::Location::caller(), ranges, value); - } - - /// Visually annotates a position or range with the `Debug` representation of a value. Previous - /// debug annotations with the same key will be removed. The key is also used to determine the - /// annotation's color. - #[cfg(debug_assertions)] - #[track_caller] - pub fn debug_with_key(&self, key: &K, ranges: &R, value: V) - where - K: std::hash::Hash + 'static, - R: debug::ToMultiBufferDebugRanges, - V: std::fmt::Debug, - { - let text_ranges = ranges - .to_multi_buffer_debug_ranges(self) - .into_iter() - .flat_map(|range| { - self.range_to_buffer_ranges(range).into_iter().map( - |(buffer, range, _excerpt_id)| { - buffer.anchor_after(range.start)..buffer.anchor_before(range.end) - }, - ) - }) - .collect(); - text::debug::GlobalDebugRanges::with_locked(|debug_ranges| { - debug_ranges.insert(key, text_ranges, format!("{value:?}").into()) - }); - } -} - -#[cfg(any(test, feature = "test-support"))] -impl MultiBufferSnapshot { - pub fn random_byte_range( - &self, - start_offset: MultiBufferOffset, - rng: &mut impl rand::Rng, - ) -> Range { - let end = self.clip_offset(rng.random_range(start_offset..=self.len()), Bias::Right); - let start = self.clip_offset(rng.random_range(start_offset..=end), Bias::Right); - start..end - } - - #[cfg(any(test, feature = "test-support"))] - fn check_invariants(&self) { - let excerpts = self.excerpts.items(()); - let excerpt_ids = self.excerpt_ids.items(()); - - assert!( - self.excerpts.is_empty() || !self.diff_transforms.is_empty(), - "must be at least one diff transform if excerpts exist" - ); - - for (ix, excerpt) in excerpts.iter().enumerate() { - if ix == 0 { - if excerpt.locator <= Locator::min() { - panic!("invalid first excerpt locator {:?}", excerpt.locator); - } - } else if excerpt.locator <= excerpts[ix - 1].locator { - panic!("excerpts are out-of-order: {:?}", excerpts); - } - } - - for (ix, entry) in excerpt_ids.iter().enumerate() { - if ix == 0 { - 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 { - panic!("excerpt ids are out-of-order: {:?}", excerpt_ids); - } - } - - if self.diff_transforms.summary().input != self.excerpts.summary().text { - panic!( - "incorrect input summary. expected {:#?}, got {:#?}. transforms: {:#?}", - self.excerpts.summary().text, - self.diff_transforms.summary().input, - self.diff_transforms.items(()), - ); - } - - for (left, right) in self.diff_transforms.iter().tuple_windows() { - use sum_tree::Item; - - if left.is_buffer_content() - && left.summary(()).input.len == MultiBufferOffset(0) - && !self.is_empty() - { - panic!("empty buffer content transform in non-empty snapshot"); - } - assert!( - left.clone().merged_with(right).is_none(), - "two consecutive diff transforms could have been merged, but weren't" - ); - } - } -} - -impl<'a, MBD, BD> MultiBufferCursor<'a, MBD, BD> -where - MBD: MultiBufferDimension + Ord + Sub + ops::AddAssign<::Output>, - BD: TextDimension + AddAssign<::Output>, -{ - #[instrument(skip_all)] - fn seek(&mut self, position: &MBD) { - let position = OutputDimension(*position); - self.cached_region.take(); - self.diff_transforms.seek(&position, Bias::Right); - if self.diff_transforms.item().is_none() - && self.diff_transforms.start().output_dimension == position - { - self.diff_transforms.prev(); - } - - let mut excerpt_position = self.diff_transforms.start().excerpt_dimension; - if let Some(item) = self.diff_transforms.item() - && item.is_buffer_content() - { - let overshoot = position - self.diff_transforms.start().output_dimension; - excerpt_position += overshoot; - } - - self.excerpts.seek(&excerpt_position, Bias::Right); - if self.excerpts.item().is_none() && excerpt_position == *self.excerpts.start() { - self.excerpts.prev(); - } - } - - fn seek_forward(&mut self, position: &MBD) { - let position = OutputDimension(*position); - self.cached_region.take(); - self.diff_transforms.seek_forward(&position, Bias::Right); - if self.diff_transforms.item().is_none() - && self.diff_transforms.start().output_dimension == position - { - self.diff_transforms.prev(); - } - - let overshoot = position - self.diff_transforms.start().output_dimension; - let mut excerpt_position = self.diff_transforms.start().excerpt_dimension; - if let Some(item) = self.diff_transforms.item() - && item.is_buffer_content() - { - excerpt_position += overshoot; - } - - self.excerpts.seek_forward(&excerpt_position, Bias::Right); - if self.excerpts.item().is_none() && excerpt_position == *self.excerpts.start() { - self.excerpts.prev(); - } - } - - fn next_excerpt(&mut self) { - self.excerpts.next(); - self.seek_to_start_of_current_excerpt(); - } - - fn prev_excerpt(&mut self) { - self.excerpts.prev(); - self.seek_to_start_of_current_excerpt(); - } - - fn seek_to_start_of_current_excerpt(&mut self) { - self.cached_region.take(); - self.diff_transforms.seek(self.excerpts.start(), Bias::Left); - if self.diff_transforms.end().excerpt_dimension == *self.excerpts.start() - && self.diff_transforms.start().excerpt_dimension < *self.excerpts.start() - && self.diff_transforms.next_item().is_some() - { - self.diff_transforms.next(); - } - } - - fn next(&mut self) { - self.cached_region.take(); - match self - .diff_transforms - .end() - .excerpt_dimension - .cmp(&self.excerpts.end()) - { - cmp::Ordering::Less => { - self.diff_transforms.next(); - } - cmp::Ordering::Greater => { - self.excerpts.next(); - } - cmp::Ordering::Equal => { - self.diff_transforms.next(); - if self.diff_transforms.end().excerpt_dimension > self.excerpts.end() - || self.diff_transforms.item().is_none() - { - self.excerpts.next(); - } else if let Some(DiffTransform::DeletedHunk { hunk_info, .. }) = - self.diff_transforms.item() - && self - .excerpts - .item() - .is_some_and(|excerpt| excerpt.id != hunk_info.excerpt_id) - { - self.excerpts.next(); - } - } - } - } - - fn prev(&mut self) { - self.cached_region.take(); - match self - .diff_transforms - .start() - .excerpt_dimension - .cmp(self.excerpts.start()) - { - cmp::Ordering::Less => self.excerpts.prev(), - cmp::Ordering::Greater => self.diff_transforms.prev(), - cmp::Ordering::Equal => { - self.diff_transforms.prev(); - if self.diff_transforms.start().excerpt_dimension < *self.excerpts.start() - || self.diff_transforms.item().is_none() - { - self.excerpts.prev(); - } - } - } - } - - fn region(&mut self) -> Option> { - if self.cached_region.is_none() { - self.cached_region = self.build_region(); - } - self.cached_region.clone() - } - - fn is_at_start_of_excerpt(&mut self) -> bool { - if self.diff_transforms.start().excerpt_dimension > *self.excerpts.start() { - return false; - } else if self.diff_transforms.start().excerpt_dimension < *self.excerpts.start() { - return true; - } - - self.diff_transforms.prev(); - let prev_transform = self.diff_transforms.item(); - self.diff_transforms.next(); - - prev_transform.is_none_or(|prev_transform| prev_transform.is_buffer_content()) - } - - fn is_at_end_of_excerpt(&mut self) -> bool { - if self.diff_transforms.end().excerpt_dimension < self.excerpts.end() { - return false; - } else if self.diff_transforms.end().excerpt_dimension > self.excerpts.end() - || self.diff_transforms.item().is_none() - { - return true; - } - - let next_transform = self.diff_transforms.next_item(); - next_transform.is_none_or(|next_transform| match next_transform { - DiffTransform::Unmodified { .. } - | DiffTransform::InsertedHunk { .. } - | DiffTransform::FilteredInsertedHunk { .. } => true, - DiffTransform::DeletedHunk { hunk_info, .. } => self - .excerpts - .item() - .is_some_and(|excerpt| excerpt.id != hunk_info.excerpt_id), - }) - } - - fn main_buffer_position(&self) -> Option { - let excerpt = self.excerpts.item()?; - let buffer = &excerpt.buffer; - let buffer_context_start = excerpt.range.context.start.summary::(buffer); - let mut buffer_start = buffer_context_start; - let overshoot = self.diff_transforms.end().excerpt_dimension - *self.excerpts.start(); - buffer_start += overshoot; - Some(buffer_start) - } - - fn build_region(&self) -> Option> { - let excerpt = self.excerpts.item()?; - match self.diff_transforms.item()? { - DiffTransform::DeletedHunk { - buffer_id, - has_trailing_newline, - hunk_info, - .. - } => { - let diff = self.snapshot.diffs.get(buffer_id)?; - let buffer = diff.base_text(); - let mut rope_cursor = buffer.as_rope().cursor(0); - let buffer_start = rope_cursor.summary::(hunk_info.base_text_byte_range.start); - let buffer_range_len = - rope_cursor.summary::(hunk_info.base_text_byte_range.end); - let mut buffer_end = buffer_start; - TextDimension::add_assign(&mut buffer_end, &buffer_range_len); - let start = self.diff_transforms.start().output_dimension.0; - let end = self.diff_transforms.end().output_dimension.0; - - Some(MultiBufferRegion { - buffer, - excerpt, - has_trailing_newline: *has_trailing_newline, - is_main_buffer: false, - diff_hunk_status: Some(DiffHunkStatus::deleted( - hunk_info.hunk_secondary_status, - )), - buffer_range: buffer_start..buffer_end, - range: start..end, - diff_base_byte_range: Some(hunk_info.base_text_byte_range.clone()), - }) - } - transform @ (DiffTransform::Unmodified { .. } - | DiffTransform::InsertedHunk { .. } - | DiffTransform::FilteredInsertedHunk { .. }) => { - let mut diff_hunk_status = transform - .hunk_info() - .map(|hunk_info| DiffHunkStatus::added(hunk_info.hunk_secondary_status)); - - let diff_base_byte_range = transform - .hunk_info() - .map(|hunk_info| hunk_info.base_text_byte_range); - - let buffer = &excerpt.buffer; - let buffer_context_start = excerpt.range.context.start.summary::(buffer); - - let mut start = self.diff_transforms.start().output_dimension.0; - let mut buffer_start = buffer_context_start; - if self.diff_transforms.start().excerpt_dimension < *self.excerpts.start() { - let overshoot = - *self.excerpts.start() - self.diff_transforms.start().excerpt_dimension; - start += overshoot; - } else { - let overshoot = - self.diff_transforms.start().excerpt_dimension - *self.excerpts.start(); - buffer_start += overshoot; - } - - let mut end; - let mut buffer_end; - let has_trailing_newline; - if self.diff_transforms.end().excerpt_dimension < self.excerpts.end() { - let overshoot = - self.diff_transforms.end().excerpt_dimension - *self.excerpts.start(); - end = self.diff_transforms.end().output_dimension.0; - buffer_end = buffer_context_start; - buffer_end += overshoot; - has_trailing_newline = false; - } else { - let overshoot = - self.excerpts.end() - self.diff_transforms.start().excerpt_dimension; - end = self.diff_transforms.start().output_dimension.0; - end += overshoot; - buffer_end = excerpt.range.context.end.summary::(buffer); - has_trailing_newline = excerpt.has_trailing_newline; - }; - - if matches!(transform, DiffTransform::FilteredInsertedHunk { .. }) { - buffer_end = buffer_start; - end = start; - diff_hunk_status = None; - } - - Some(MultiBufferRegion { - buffer, - excerpt, - has_trailing_newline, - is_main_buffer: true, - diff_hunk_status, - diff_base_byte_range, - buffer_range: buffer_start..buffer_end, - range: start..end, - }) - } - } - } - - fn excerpt(&self) -> Option<&'a Excerpt> { - self.excerpts.item() - } -} - -impl Excerpt { - fn new( - id: ExcerptId, - locator: Locator, - buffer_id: BufferId, - buffer: BufferSnapshot, - range: ExcerptRange, - has_trailing_newline: bool, - ) -> Self { - Excerpt { - id, - locator, - max_buffer_row: range.context.end.to_point(&buffer).row, - text_summary: buffer - .text_summary_for_range::(range.context.to_offset(&buffer)), - buffer_id, - buffer, - range, - has_trailing_newline, - } - } - - fn chunks_in_range(&self, range: Range, language_aware: bool) -> ExcerptChunks<'_> { - let content_start = self.range.context.start.to_offset(&self.buffer); - let chunks_start = content_start + range.start; - let chunks_end = content_start + cmp::min(range.end, self.text_summary.len); - - let has_footer = self.has_trailing_newline - && range.start <= self.text_summary.len - && range.end > self.text_summary.len; - - let content_chunks = self.buffer.chunks(chunks_start..chunks_end, language_aware); - - ExcerptChunks { - excerpt_id: self.id, - content_chunks, - has_footer, - } - } - - fn seek_chunks(&self, excerpt_chunks: &mut ExcerptChunks, range: Range) { - let content_start = self.range.context.start.to_offset(&self.buffer); - let chunks_start = content_start + range.start; - let chunks_end = content_start + cmp::min(range.end, self.text_summary.len); - excerpt_chunks.content_chunks.seek(chunks_start..chunks_end); - excerpt_chunks.has_footer = self.has_trailing_newline - && range.start <= self.text_summary.len - && range.end > self.text_summary.len; - } - - fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor { - if text_anchor - .cmp(&self.range.context.start, &self.buffer) - .is_lt() - { - self.range.context.start - } else if text_anchor - .cmp(&self.range.context.end, &self.buffer) - .is_gt() - { - self.range.context.end - } else { - text_anchor - } - } - - fn contains(&self, anchor: &Anchor) -> bool { - (anchor.text_anchor.buffer_id == None - || anchor.text_anchor.buffer_id == Some(self.buffer_id)) - && self - .range - .context - .start - .cmp(&anchor.text_anchor, &self.buffer) - .is_le() - && self - .range - .context - .end - .cmp(&anchor.text_anchor, &self.buffer) - .is_ge() - } - - /// The [`Excerpt`]'s start offset in its [`Buffer`] - fn buffer_start_offset(&self) -> BufferOffset { - BufferOffset(self.range.context.start.to_offset(&self.buffer)) - } - - /// The [`Excerpt`]'s end offset in its [`Buffer`] - fn buffer_end_offset(&self) -> BufferOffset { - self.buffer_start_offset() + self.text_summary.len - } -} - -impl<'a> MultiBufferExcerpt<'a> { - pub fn id(&self) -> ExcerptId { - self.excerpt.id - } - - pub fn buffer_id(&self) -> BufferId { - self.excerpt.buffer_id - } - - pub fn start_anchor(&self) -> Anchor { - Anchor::in_buffer(self.excerpt.id, self.excerpt.range.context.start) - } - - pub fn end_anchor(&self) -> Anchor { - Anchor::in_buffer(self.excerpt.id, self.excerpt.range.context.end) - } - - pub fn buffer(&self) -> &'a BufferSnapshot { - &self.excerpt.buffer - } - - pub fn buffer_range(&self) -> Range { - self.buffer_offset - ..BufferOffset( - self.excerpt - .range - .context - .end - .to_offset(&self.excerpt.buffer.text), - ) - } - - pub fn start_offset(&self) -> MultiBufferOffset { - self.offset - } - - /// Maps an offset within the [`MultiBuffer`] to an offset within the [`Buffer`] - pub fn map_offset_to_buffer(&mut self, offset: MultiBufferOffset) -> BufferOffset { - self.map_range_to_buffer(offset..offset).start - } - - /// Maps a range within the [`MultiBuffer`] to a range within the [`Buffer`] - pub fn map_range_to_buffer(&mut self, range: Range) -> Range { - self.diff_transforms - .seek(&OutputDimension(range.start), Bias::Right); - let start = self.map_offset_to_buffer_internal(range.start); - let end = if range.end > range.start { - self.diff_transforms - .seek_forward(&OutputDimension(range.end), Bias::Right); - self.map_offset_to_buffer_internal(range.end) - } else { - start - }; - start..end - } - - fn map_offset_to_buffer_internal(&self, offset: MultiBufferOffset) -> BufferOffset { - let mut excerpt_offset = self.diff_transforms.start().excerpt_dimension; - if self - .diff_transforms - .item() - .is_some_and(|t| t.is_buffer_content()) - { - excerpt_offset += offset - self.diff_transforms.start().output_dimension.0; - }; - let offset_in_excerpt = excerpt_offset.saturating_sub(self.excerpt_offset); - self.buffer_offset + offset_in_excerpt - } - - /// Map an offset within the [`Buffer`] to an offset within the [`MultiBuffer`] - pub fn map_offset_from_buffer(&mut self, buffer_offset: BufferOffset) -> MultiBufferOffset { - self.map_range_from_buffer(buffer_offset..buffer_offset) - .start - } - - /// Map a range within the [`Buffer`] to a range within the [`MultiBuffer`] - pub fn map_range_from_buffer( - &mut self, - buffer_range: Range, - ) -> Range { - if buffer_range.start < self.buffer_offset { - log::warn!( - "Attempting to map a range from a buffer offset that starts before the current buffer offset" - ); - return self.offset..self.offset; - } - let overshoot = buffer_range.start - self.buffer_offset; - let excerpt_offset = self.excerpt_offset + overshoot; - let excerpt_seek_dim = excerpt_offset; - self.diff_transforms.seek(&excerpt_seek_dim, Bias::Right); - if self.diff_transforms.start().excerpt_dimension > excerpt_offset { - log::warn!( - "Attempting to map a range from a buffer offset that starts before the current buffer offset" - ); - return self.offset..self.offset; - } - let overshoot = excerpt_offset - self.diff_transforms.start().excerpt_dimension; - let start = self.diff_transforms.start().output_dimension.0 + overshoot; - - let end = if buffer_range.start < buffer_range.end { - let overshoot = buffer_range.end - self.buffer_offset; - let excerpt_offset = self.excerpt_offset + overshoot; - let excerpt_seek_dim = excerpt_offset; - self.diff_transforms - .seek_forward(&excerpt_seek_dim, Bias::Right); - let overshoot = excerpt_offset - self.diff_transforms.start().excerpt_dimension; - // todo(lw): Clamp end to the excerpt boundaries - self.diff_transforms.start().output_dimension.0 + overshoot - } else { - start - }; - - start..end - } - - /// Returns true if the entirety of the given range is in the buffer's excerpt - pub fn contains_buffer_range(&self, range: Range) -> bool { - range.start >= self.excerpt.buffer_start_offset() - && range.end <= self.excerpt.buffer_end_offset() - } - - pub fn max_buffer_row(&self) -> u32 { - self.excerpt.max_buffer_row - } -} - -impl ExcerptId { - pub fn min() -> Self { - Self(0) - } - - pub fn max() -> Self { - Self(u32::MAX) - } - - pub fn to_proto(self) -> u64 { - self.0 as _ - } - - pub fn from_proto(proto: u64) -> Self { - Self(proto as _) - } - - pub fn cmp(&self, other: &Self, snapshot: &MultiBufferSnapshot) -> cmp::Ordering { - let a = snapshot.excerpt_locator_for_id(*self); - let b = snapshot.excerpt_locator_for_id(*other); - a.cmp(b).then_with(|| self.0.cmp(&other.0)) - } -} - -impl From for usize { - fn from(val: ExcerptId) -> Self { - val.0 as usize - } -} - -impl fmt::Debug for Excerpt { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Excerpt") - .field("id", &self.id) - .field("locator", &self.locator) - .field("buffer_id", &self.buffer_id) - .field("range", &self.range) - .field("text_summary", &self.text_summary) - .field("has_trailing_newline", &self.has_trailing_newline) - .finish() - } -} - -impl sum_tree::Item for Excerpt { - type Summary = ExcerptSummary; - - fn summary(&self, _cx: ()) -> Self::Summary { - let mut text = self.text_summary; - if self.has_trailing_newline { - text += TextSummary::from("\n"); - } - ExcerptSummary { - excerpt_id: self.id, - excerpt_locator: self.locator.clone(), - widest_line_number: self.max_buffer_row, - text: text.into(), - } - } -} - -impl sum_tree::Item for ExcerptIdMapping { - type Summary = ExcerptId; - - fn summary(&self, _cx: ()) -> Self::Summary { - self.id - } -} - -impl sum_tree::KeyedItem for ExcerptIdMapping { - type Key = ExcerptId; - - fn key(&self) -> Self::Key { - self.id - } -} - -impl DiffTransform { - fn hunk_info(&self) -> Option { - match self { - DiffTransform::DeletedHunk { hunk_info, .. } - | DiffTransform::InsertedHunk { hunk_info, .. } - | DiffTransform::FilteredInsertedHunk { hunk_info, .. } => Some(hunk_info.clone()), - DiffTransform::Unmodified { .. } => None, - } - } - - fn is_buffer_content(&self) -> bool { - match self { - Self::Unmodified { .. } - | Self::InsertedHunk { .. } - | Self::FilteredInsertedHunk { .. } => true, - Self::DeletedHunk { .. } => false, - } - } -} - -impl sum_tree::Item for DiffTransform { - type Summary = DiffTransformSummary; - - fn summary(&self, _: ::Context<'_>) -> Self::Summary { - match self { - DiffTransform::InsertedHunk { summary, .. } - | DiffTransform::Unmodified { summary, .. } => DiffTransformSummary { - input: *summary, - output: *summary, - }, - &DiffTransform::DeletedHunk { summary, .. } => DiffTransformSummary { - input: MBTextSummary::default(), - output: summary.into(), - }, - DiffTransform::FilteredInsertedHunk { summary, .. } => DiffTransformSummary { - input: *summary, - output: MBTextSummary::default(), - }, - } - } -} - -impl DiffTransformSummary { - fn excerpt_len(&self) -> ExcerptOffset { - ExcerptDimension(self.input.len) - } -} - -impl sum_tree::ContextLessSummary for DiffTransformSummary { - fn zero() -> Self { - DiffTransformSummary { - input: MBTextSummary::default(), - output: MBTextSummary::default(), - } - } - - fn add_summary(&mut self, other: &Self) { - self.input += other.input; - self.output += other.output; - } -} - -impl sum_tree::ContextLessSummary for ExcerptId { - fn zero() -> Self { - Self(0) - } - - fn add_summary(&mut self, summary: &Self) { - *self = cmp::max(*self, *summary); - } -} - -impl sum_tree::ContextLessSummary for ExcerptSummary { - fn zero() -> Self { - Self::default() - } - - fn add_summary(&mut self, summary: &Self) { - debug_assert!(summary.excerpt_locator > self.excerpt_locator); - self.excerpt_locator = summary.excerpt_locator.clone(); - self.text += summary.text; - self.widest_line_number = cmp::max(self.widest_line_number, summary.widest_line_number); - } -} - -impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, Option<&'a Locator>> for Locator { - fn cmp(&self, cursor_location: &Option<&'a Locator>, _: ()) -> cmp::Ordering { - Ord::cmp(&Some(self), cursor_location) - } -} - -impl sum_tree::SeekTarget<'_, ExcerptSummary, ExcerptSummary> for Locator { - fn cmp(&self, cursor_location: &ExcerptSummary, _: ()) -> cmp::Ordering { - Ord::cmp(self, &cursor_location.excerpt_locator) - } -} - -impl<'a, MBD> sum_tree::Dimension<'a, ExcerptSummary> for ExcerptDimension -where - MBD: MultiBufferDimension + Default, -{ - fn zero(_: ()) -> Self { - ExcerptDimension(MBD::default()) - } - - fn add_summary(&mut self, summary: &'a ExcerptSummary, _: ()) { - MultiBufferDimension::add_mb_text_summary(&mut self.0, &summary.text) - } -} - -impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option<&'a Locator> { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a ExcerptSummary, _: ()) { - *self = Some(&summary.excerpt_locator); - } -} - -impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a ExcerptSummary, _: ()) { - *self = Some(summary.excerpt_id); - } -} - -#[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Debug)] -struct OutputDimension(T); - -impl PartialEq for OutputDimension { - fn eq(&self, other: &T) -> bool { - self.0 == *other - } -} - -impl PartialOrd for OutputDimension { - fn partial_cmp(&self, other: &T) -> Option { - self.0.partial_cmp(other) - } -} - -impl ops::Sub> for OutputDimension -where - T: ops::Sub, -{ - type Output = R; - - fn sub(self, other: OutputDimension) -> Self::Output { - self.0 - other.0 - } -} - -impl ops::Add for OutputDimension -where - T: ops::Add, -{ - type Output = OutputDimension; - - fn add(self, other: U) -> Self::Output { - OutputDimension(self.0 + other) - } -} - -impl AddAssign for OutputDimension -where - T: AddAssign, -{ - fn add_assign(&mut self, other: U) { - self.0 += other; - } -} - -impl SubAssign for OutputDimension -where - T: SubAssign, -{ - fn sub_assign(&mut self, other: U) { - self.0 -= other; - } -} - -#[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Debug)] -struct ExcerptDimension(T); - -impl PartialEq for ExcerptDimension { - fn eq(&self, other: &T) -> bool { - self.0 == *other - } -} - -impl PartialOrd for ExcerptDimension { - fn partial_cmp(&self, other: &T) -> Option { - self.0.partial_cmp(other) - } -} - -impl ExcerptOffset { - fn saturating_sub(self, other: ExcerptOffset) -> usize { - self.0.saturating_sub(other.0) - } -} - -impl ops::Sub> for ExcerptDimension -where - T: ops::Sub, -{ - type Output = R; - - fn sub(self, other: ExcerptDimension) -> Self::Output { - self.0 - other.0 - } -} - -impl ops::Add for ExcerptDimension -where - T: ops::Add, -{ - type Output = ExcerptDimension; - - fn add(self, other: U) -> Self::Output { - ExcerptDimension(self.0 + other) - } -} - -impl AddAssign for ExcerptDimension -where - T: AddAssign, -{ - fn add_assign(&mut self, other: U) { - self.0 += other; - } -} - -impl SubAssign for ExcerptDimension -where - T: SubAssign, -{ - fn sub_assign(&mut self, other: U) { - self.0 -= other; - } -} - -impl<'a> sum_tree::Dimension<'a, DiffTransformSummary> for MultiBufferOffset { - fn zero(_: ()) -> Self { - MultiBufferOffset::ZERO - } - - fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: ()) { - *self += summary.output.len; - } -} - -impl sum_tree::SeekTarget<'_, DiffTransformSummary, DiffTransformSummary> - for ExcerptDimension -where - MBD: MultiBufferDimension + Ord, -{ - fn cmp(&self, cursor_location: &DiffTransformSummary, _: ()) -> cmp::Ordering { - Ord::cmp(&self.0, &MBD::from_summary(&cursor_location.input)) - } -} - -impl<'a, MBD> sum_tree::SeekTarget<'a, DiffTransformSummary, DiffTransforms> - for ExcerptDimension -where - MBD: MultiBufferDimension + Ord, -{ - fn cmp(&self, cursor_location: &DiffTransforms, _: ()) -> cmp::Ordering { - Ord::cmp(&self.0, &cursor_location.excerpt_dimension.0) - } -} - -impl<'a, MBD: MultiBufferDimension> sum_tree::Dimension<'a, DiffTransformSummary> - for ExcerptDimension -{ - fn zero(_: ()) -> Self { - ExcerptDimension(MBD::default()) - } - - fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: ()) { - self.0.add_mb_text_summary(&summary.input) - } -} - -impl<'a, MBD> sum_tree::SeekTarget<'a, DiffTransformSummary, DiffTransforms> - for OutputDimension -where - MBD: MultiBufferDimension + Ord, -{ - fn cmp(&self, cursor_location: &DiffTransforms, _: ()) -> cmp::Ordering { - Ord::cmp(&self.0, &cursor_location.output_dimension.0) - } -} - -impl<'a, MBD: MultiBufferDimension> sum_tree::Dimension<'a, DiffTransformSummary> - for OutputDimension -{ - fn zero(_: ()) -> Self { - OutputDimension(MBD::default()) - } - - fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: ()) { - self.0.add_mb_text_summary(&summary.output) - } -} - -impl MultiBufferRows<'_> { - pub fn seek(&mut self, MultiBufferRow(row): MultiBufferRow) { - self.point = Point::new(row, 0); - self.cursor.seek(&self.point); - } -} - -impl Iterator for MultiBufferRows<'_> { - type Item = RowInfo; - - fn next(&mut self) -> Option { - if self.is_empty && self.point.row == 0 { - self.point += Point::new(1, 0); - return Some(RowInfo { - buffer_id: None, - buffer_row: Some(0), - base_text_row: Some(BaseTextRow(0)), - multibuffer_row: Some(MultiBufferRow(0)), - diff_status: None, - expand_info: None, - wrapped_buffer_row: None, - }); - } - - let mut region = self.cursor.region()?; - while self.point >= region.range.end { - 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; - // TODO(split-diff) perf - let base_text_row = self - .cursor - .snapshot - .diffs - .get(&last_excerpt.buffer_id) - .map(|diff| diff.row_to_base_text_row(last_row, &last_excerpt.buffer)) - .map(BaseTextRow); - - 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()) - && !(region.is_filtered() - && region - .diff_base_byte_range - .is_some_and(|range| !range.is_empty())); - 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), - base_text_row, - multibuffer_row: Some(multibuffer_row), - diff_status: None, - wrapped_buffer_row: None, - expand_info, - }); - } else { - return None; - }; - } - - let overshoot = self.point - region.range.start; - let buffer_point = region.buffer_range.start + overshoot; - let diff_status = region - .diff_hunk_status - .filter(|_| self.point < region.range.end); - let base_text_row = match diff_status { - // TODO(split-diff) perf - None => self - .cursor - .snapshot - .diffs - .get(®ion.excerpt.buffer_id) - .map(|diff| diff.row_to_base_text_row(buffer_point.row, ®ion.buffer)) - .map(BaseTextRow), - Some(DiffHunkStatus { - kind: DiffHunkStatusKind::Added, - .. - }) => None, - Some(DiffHunkStatus { - kind: DiffHunkStatusKind::Deleted, - .. - }) => Some(BaseTextRow(buffer_point.row)), - Some(DiffHunkStatus { - kind: DiffHunkStatusKind::Modified, - .. - }) => unreachable!(), - }; - let expand_info = if self.is_singleton { - None - } else { - let needs_expand_up = self.point.row == region.range.start.row - && self.cursor.is_at_start_of_excerpt() - && buffer_point.row > 0; - let needs_expand_down = (region.excerpt.has_trailing_newline - && self.point.row + 1 == region.range.end.row - || !region.excerpt.has_trailing_newline && self.point.row == region.range.end.row) - && self.cursor.is_at_end_of_excerpt() - && buffer_point.row < region.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: region.excerpt.id, - }) - }; - - let result = Some(RowInfo { - buffer_id: Some(region.buffer.remote_id()), - buffer_row: Some(buffer_point.row), - base_text_row, - multibuffer_row: Some(MultiBufferRow(self.point.row)), - diff_status, - expand_info, - wrapped_buffer_row: None, - }); - self.point += Point::new(1, 0); - result - } -} - -impl<'a> MultiBufferChunks<'a> { - pub fn offset(&self) -> MultiBufferOffset { - self.range.start - } - - pub fn seek(&mut self, range: Range) { - self.diff_transforms.seek(&range.end, Bias::Right); - let mut excerpt_end = self.diff_transforms.start().1; - if self - .diff_transforms - .item() - .is_some_and(|t| t.is_buffer_content()) - { - let overshoot = range.end - self.diff_transforms.start().0; - excerpt_end += overshoot; - } - - self.diff_transforms.seek(&range.start, Bias::Right); - let mut excerpt_start = self.diff_transforms.start().1; - if self - .diff_transforms - .item() - .is_some_and(|t| t.is_buffer_content()) - { - let overshoot = range.start - self.diff_transforms.start().0; - excerpt_start += overshoot; - } - - self.seek_to_excerpt_offset_range(excerpt_start..excerpt_end); - self.buffer_chunk.take(); - self.range = range; - } - - fn seek_to_excerpt_offset_range(&mut self, new_range: Range) { - self.excerpt_offset_range = new_range.clone(); - self.excerpts.seek(&new_range.start, Bias::Right); - if let Some(excerpt) = self.excerpts.item() { - let excerpt_start = *self.excerpts.start(); - if let Some(excerpt_chunks) = self - .excerpt_chunks - .as_mut() - .filter(|chunks| excerpt.id == chunks.excerpt_id) - { - excerpt.seek_chunks( - excerpt_chunks, - (self.excerpt_offset_range.start - excerpt_start) - ..(self.excerpt_offset_range.end - excerpt_start), - ); - } else { - self.excerpt_chunks = Some(excerpt.chunks_in_range( - (self.excerpt_offset_range.start - excerpt_start) - ..(self.excerpt_offset_range.end - excerpt_start), - self.language_aware, - )); - } - } else { - self.excerpt_chunks = None; - } - } - - fn next_excerpt_chunk(&mut self) -> Option> { - loop { - if self.excerpt_offset_range.is_empty() { - return None; - } else if let Some(chunk) = self.excerpt_chunks.as_mut()?.next() { - self.excerpt_offset_range.start += chunk.text.len(); - return Some(chunk); - } else { - self.excerpts.next(); - let excerpt = self.excerpts.item()?; - self.excerpt_chunks = Some(excerpt.chunks_in_range( - 0..(self.excerpt_offset_range.end - *self.excerpts.start()), - self.language_aware, - )); - } - } - } -} - -impl<'a> Iterator for ReversedMultiBufferChunks<'a> { - type Item = &'a str; - - fn next(&mut self) -> Option { - let mut region = self.cursor.region()?; - if self.offset == region.range.start { - self.cursor.prev(); - while let Some(region) = self.cursor.region() - && region.buffer_range.is_empty() - && !region.has_trailing_newline - { - self.cursor.prev(); - } - region = self.cursor.region()?; - let start_overshoot = self.start.saturating_sub(region.range.start); - self.current_chunks = Some(region.buffer.reversed_chunks_in_range( - region.buffer_range.start + start_overshoot..region.buffer_range.end, - )); - } - - if self.offset == region.range.end && region.has_trailing_newline { - self.offset -= 1; - Some("\n") - } else { - let chunk = self.current_chunks.as_mut().unwrap().next()?; - self.offset -= chunk.len(); - Some(chunk) - } - } -} - -impl<'a> Iterator for MultiBufferChunks<'a> { - type Item = Chunk<'a>; - - fn next(&mut self) -> Option> { - if self.range.start >= self.range.end { - return None; - } - if self.range.start == self.diff_transforms.end().0 { - self.diff_transforms.next(); - } - while let Some(DiffTransform::FilteredInsertedHunk { .. }) = self.diff_transforms.item() { - self.diff_transforms.next(); - let mut range = self.excerpt_offset_range.clone(); - range.start = self.diff_transforms.start().1; - self.seek_to_excerpt_offset_range(range); - self.buffer_chunk.take(); - } - - let diff_transform_start = self.diff_transforms.start().0; - let diff_transform_end = self.diff_transforms.end().0; - debug_assert!( - self.range.start < diff_transform_end, - "{:?} < {:?} of ({1:?}..{2:?})", - self.range.start, - diff_transform_end, - diff_transform_start - ); - - let diff_transform = self.diff_transforms.item()?; - match diff_transform { - DiffTransform::Unmodified { .. } - | DiffTransform::InsertedHunk { .. } - | DiffTransform::FilteredInsertedHunk { .. } => { - let chunk = if let Some(chunk) = &mut self.buffer_chunk { - chunk - } else { - let chunk = self.next_excerpt_chunk().unwrap(); - self.buffer_chunk.insert(chunk) - }; - - let chunk_end = self.range.start + chunk.text.len(); - let diff_transform_end = diff_transform_end.min(self.range.end); - - if diff_transform_end < chunk_end { - let split_idx = diff_transform_end - self.range.start; - let (before, after) = chunk.text.split_at(split_idx); - self.range.start = diff_transform_end; - let mask = 1u128.unbounded_shl(split_idx as u32).wrapping_sub(1); - let chars = chunk.chars & mask; - let tabs = chunk.tabs & mask; - - chunk.text = after; - chunk.chars = chunk.chars >> split_idx; - chunk.tabs = chunk.tabs >> split_idx; - - Some(Chunk { - text: before, - chars, - tabs, - ..chunk.clone() - }) - } else { - self.range.start = chunk_end; - self.buffer_chunk.take() - } - } - DiffTransform::DeletedHunk { - buffer_id, - hunk_info, - has_trailing_newline, - .. - } => { - let base_text_start = hunk_info.base_text_byte_range.start - + (self.range.start - diff_transform_start); - let base_text_end = - hunk_info.base_text_byte_range.start + (self.range.end - diff_transform_start); - let base_text_end = base_text_end.min(hunk_info.base_text_byte_range.end); - - let mut chunks = if let Some((_, mut chunks)) = self - .diff_base_chunks - .take() - .filter(|(id, _)| id == buffer_id) - { - if chunks.range().start != base_text_start || chunks.range().end < base_text_end - { - chunks.seek(base_text_start..base_text_end); - } - chunks - } else { - let base_buffer = &self.diffs.get(buffer_id)?.base_text(); - base_buffer.chunks(base_text_start..base_text_end, self.language_aware) - }; - - let chunk = if let Some(chunk) = chunks.next() { - self.range.start += chunk.text.len(); - self.diff_base_chunks = Some((*buffer_id, chunks)); - chunk - } else { - debug_assert!(has_trailing_newline); - self.range.start += "\n".len(); - Chunk { - text: "\n", - chars: 1u128, - ..Default::default() - } - }; - Some(chunk) - } - } - } -} - -impl MultiBufferBytes<'_> { - fn consume(&mut self, len: usize) { - self.range.start += len; - self.chunk = &self.chunk[len..]; - - if !self.range.is_empty() && self.chunk.is_empty() { - if let Some(chunk) = self.excerpt_bytes.as_mut().and_then(|bytes| bytes.next()) { - self.chunk = chunk; - } else if self.has_trailing_newline { - self.has_trailing_newline = false; - self.chunk = b"\n"; - } else { - self.cursor.next(); - while let Some(region) = self.cursor.region() - && region.buffer_range.is_empty() - && !region.has_trailing_newline - { - self.cursor.next(); - } - if let Some(region) = self.cursor.region() { - let mut excerpt_bytes = region.buffer.bytes_in_range( - region.buffer_range.start - ..(region.buffer_range.start + (self.range.end - region.range.start)) - .min(region.buffer_range.end), - ); - self.chunk = excerpt_bytes.next().unwrap_or(&[]); - self.excerpt_bytes = Some(excerpt_bytes); - self.has_trailing_newline = - region.has_trailing_newline && self.range.end >= region.range.end; - if self.chunk.is_empty() && self.has_trailing_newline { - self.has_trailing_newline = false; - self.chunk = b"\n"; - } - } - } - } - } -} - -impl<'a> Iterator for MultiBufferBytes<'a> { - type Item = &'a [u8]; - - fn next(&mut self) -> Option { - let chunk = self.chunk; - if chunk.is_empty() { - None - } else { - self.consume(chunk.len()); - Some(chunk) - } - } -} - -impl io::Read for MultiBufferBytes<'_> { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - let len = cmp::min(buf.len(), self.chunk.len()); - buf[..len].copy_from_slice(&self.chunk[..len]); - if len > 0 { - self.consume(len); - } - Ok(len) - } -} - -impl io::Read for ReversedMultiBufferBytes<'_> { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - let len = cmp::min(buf.len(), self.chunk.len()); - buf[..len].copy_from_slice(&self.chunk[..len]); - buf[..len].reverse(); - 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(); - } - } - Ok(len) - } -} - -impl<'a> Iterator for ExcerptChunks<'a> { - type Item = Chunk<'a>; - - fn next(&mut self) -> Option { - if let Some(chunk) = self.content_chunks.next() { - return Some(chunk); - } - - if self.has_footer { - let text = "\n"; - let chars = 0b1; - self.has_footer = false; - return Some(Chunk { - text, - chars, - ..Default::default() - }); - } - - None - } -} - -impl ToOffset for Point { - fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffset { - snapshot.point_to_offset(*self) - } - fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffsetUtf16 { - snapshot.point_to_offset_utf16(*self) - } -} - -impl ToOffset for MultiBufferOffset { - #[track_caller] - fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffset { - assert!( - *self <= snapshot.len(), - "offset {} is greater than the snapshot.len() {}", - self.0, - snapshot.len().0, - ); - *self - } - fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffsetUtf16 { - snapshot.offset_to_offset_utf16(*self) - } -} - -impl ToOffset for MultiBufferOffsetUtf16 { - fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffset { - snapshot.offset_utf16_to_offset(*self) - } - - fn to_offset_utf16(&self, _snapshot: &MultiBufferSnapshot) -> MultiBufferOffsetUtf16 { - *self - } -} - -impl ToOffset for PointUtf16 { - fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffset { - snapshot.point_utf16_to_offset(*self) - } - fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffsetUtf16 { - snapshot.point_utf16_to_offset_utf16(*self) - } -} - -impl ToPoint for MultiBufferOffset { - fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point { - snapshot.offset_to_point(*self) - } - fn to_point_utf16<'a>(&self, snapshot: &MultiBufferSnapshot) -> PointUtf16 { - snapshot.offset_to_point_utf16(*self) - } -} - -impl ToPoint for Point { - fn to_point<'a>(&self, _: &MultiBufferSnapshot) -> Point { - *self - } - fn to_point_utf16<'a>(&self, snapshot: &MultiBufferSnapshot) -> PointUtf16 { - snapshot.point_to_point_utf16(*self) - } -} - -impl ToPoint for PointUtf16 { - fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point { - snapshot.point_utf16_to_point(*self) - } - fn to_point_utf16<'a>(&self, _: &MultiBufferSnapshot) -> PointUtf16 { - *self - } -} - -impl From for EntityId { - fn from(id: ExcerptId) -> Self { - EntityId::from(id.0 as u64) - } -} - -#[cfg(debug_assertions)] -pub mod debug { - use super::*; - - pub trait ToMultiBufferDebugRanges { - fn to_multi_buffer_debug_ranges( - &self, - snapshot: &MultiBufferSnapshot, - ) -> Vec>; - } - - impl ToMultiBufferDebugRanges for T { - fn to_multi_buffer_debug_ranges( - &self, - snapshot: &MultiBufferSnapshot, - ) -> Vec> { - [self.to_offset(snapshot)].to_multi_buffer_debug_ranges(snapshot) - } - } - - impl ToMultiBufferDebugRanges for Range { - fn to_multi_buffer_debug_ranges( - &self, - snapshot: &MultiBufferSnapshot, - ) -> Vec> { - [self.start.to_offset(snapshot)..self.end.to_offset(snapshot)] - .to_multi_buffer_debug_ranges(snapshot) - } - } - - impl ToMultiBufferDebugRanges for Vec { - fn to_multi_buffer_debug_ranges( - &self, - snapshot: &MultiBufferSnapshot, - ) -> Vec> { - self.as_slice().to_multi_buffer_debug_ranges(snapshot) - } - } - - impl ToMultiBufferDebugRanges for Vec> { - fn to_multi_buffer_debug_ranges( - &self, - snapshot: &MultiBufferSnapshot, - ) -> Vec> { - self.as_slice().to_multi_buffer_debug_ranges(snapshot) - } - } - - impl ToMultiBufferDebugRanges for [T] { - fn to_multi_buffer_debug_ranges( - &self, - snapshot: &MultiBufferSnapshot, - ) -> Vec> { - self.iter() - .map(|item| { - let offset = item.to_offset(snapshot); - offset..offset - }) - .collect() - } - } - - impl ToMultiBufferDebugRanges for [Range] { - fn to_multi_buffer_debug_ranges( - &self, - snapshot: &MultiBufferSnapshot, - ) -> Vec> { - self.iter() - .map(|range| range.start.to_offset(snapshot)..range.end.to_offset(snapshot)) - .collect() - } - } -} diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs deleted file mode 100644 index fc2edcac15..0000000000 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ /dev/null @@ -1,4652 +0,0 @@ -use super::*; -use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; -use gpui::{App, TestAppContext}; -use indoc::indoc; -use language::{Buffer, Rope}; -use parking_lot::RwLock; -use rand::prelude::*; -use settings::SettingsStore; -use std::env; -use std::time::{Duration, Instant}; -use util::RandomCharIter; -use util::rel_path::rel_path; -use util::test::sample_text; - -#[ctor::ctor] -fn init_logger() { - zlog::init_test(); -} - -#[gpui::test] -fn test_empty_singleton(cx: &mut App) { - let buffer = cx.new(|cx| Buffer::local("", cx)); - let buffer_id = buffer.read(cx).remote_id(); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), ""); - assert_eq!( - snapshot.row_infos(MultiBufferRow(0)).collect::>(), - [RowInfo { - buffer_id: Some(buffer_id), - buffer_row: Some(0), - base_text_row: None, - multibuffer_row: Some(MultiBufferRow(0)), - diff_status: None, - expand_info: None, - wrapped_buffer_row: None, - }] - ); -} - -#[gpui::test] -fn test_singleton(cx: &mut App) { - let buffer = cx.new(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), buffer.read(cx).text()); - - assert_eq!( - snapshot - .row_infos(MultiBufferRow(0)) - .map(|info| info.buffer_row) - .collect::>(), - (0..buffer.read(cx).row_count()) - .map(Some) - .collect::>() - ); - assert_consistent_line_numbers(&snapshot); - - buffer.update(cx, |buffer, cx| buffer.edit([(1..3, "XXX\n")], None, cx)); - let snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!(snapshot.text(), buffer.read(cx).text()); - assert_eq!( - snapshot - .row_infos(MultiBufferRow(0)) - .map(|info| info.buffer_row) - .collect::>(), - (0..buffer.read(cx).row_count()) - .map(Some) - .collect::>() - ); - assert_consistent_line_numbers(&snapshot); -} - -#[gpui::test] -fn test_remote(cx: &mut App) { - let host_buffer = cx.new(|cx| Buffer::local("a", cx)); - let guest_buffer = cx.new(|cx| { - let state = host_buffer.read(cx).to_proto(cx); - let ops = cx - .background_executor() - .block(host_buffer.read(cx).serialize_ops(None, cx)); - let mut buffer = - Buffer::from_proto(ReplicaId::REMOTE_SERVER, Capability::ReadWrite, state, None) - .unwrap(); - buffer.apply_ops( - ops.into_iter() - .map(|op| language::proto::deserialize_operation(op).unwrap()), - cx, - ); - buffer - }); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(guest_buffer.clone(), cx)); - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), "a"); - - guest_buffer.update(cx, |buffer, cx| buffer.edit([(1..1, "b")], None, cx)); - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), "ab"); - - guest_buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "c")], None, cx)); - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), "abc"); -} - -#[gpui::test] -fn test_excerpt_boundaries_and_clipping(cx: &mut App) { - let buffer_1 = cx.new(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.new(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - - let events = Arc::new(RwLock::new(Vec::::new())); - multibuffer.update(cx, |_, cx| { - let events = events.clone(); - cx.subscribe(&multibuffer, move |_, _, event, _| { - if let Event::Edited { .. } = event { - events.write().push(event.clone()) - } - }) - .detach(); - }); - - let subscription = multibuffer.update(cx, |multibuffer, cx| { - let subscription = multibuffer.subscribe(); - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange::new(Point::new(1, 2)..Point::new(2, 5))], - cx, - ); - assert_eq!( - subscription.consume().into_inner(), - [Edit { - old: MultiBufferOffset(0)..MultiBufferOffset(0), - new: MultiBufferOffset(0)..MultiBufferOffset(10) - }] - ); - - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange::new(Point::new(3, 3)..Point::new(4, 4))], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange::new(Point::new(3, 1)..Point::new(3, 3))], - cx, - ); - assert_eq!( - subscription.consume().into_inner(), - [Edit { - old: MultiBufferOffset(10)..MultiBufferOffset(10), - new: MultiBufferOffset(10)..MultiBufferOffset(22) - }] - ); - - subscription - }); - - // Adding excerpts emits an edited event. - assert_eq!( - events.read().as_slice(), - &[ - Event::Edited { - edited_buffer: None, - }, - Event::Edited { - edited_buffer: None, - }, - Event::Edited { - edited_buffer: None, - } - ] - ); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!( - snapshot.text(), - indoc!( - " - bbbb - ccccc - ddd - eeee - jj" - ), - ); - assert_eq!( - snapshot - .row_infos(MultiBufferRow(0)) - .map(|info| info.buffer_row) - .collect::>(), - [Some(1), Some(2), Some(3), Some(4), Some(3)] - ); - assert_eq!( - snapshot - .row_infos(MultiBufferRow(2)) - .map(|info| info.buffer_row) - .collect::>(), - [Some(3), Some(4), Some(3)] - ); - assert_eq!( - snapshot - .row_infos(MultiBufferRow(4)) - .map(|info| info.buffer_row) - .collect::>(), - [Some(3)] - ); - assert!( - snapshot - .row_infos(MultiBufferRow(5)) - .map(|info| info.buffer_row) - .collect::>() - .is_empty() - ); - - assert_eq!( - boundaries_in_range(Point::new(0, 0)..Point::new(4, 2), &snapshot), - &[ - (MultiBufferRow(0), "bbbb\nccccc".to_string(), true), - (MultiBufferRow(2), "ddd\neeee".to_string(), false), - (MultiBufferRow(4), "jj".to_string(), true), - ] - ); - assert_eq!( - boundaries_in_range(Point::new(0, 0)..Point::new(2, 0), &snapshot), - &[(MultiBufferRow(0), "bbbb\nccccc".to_string(), true)] - ); - assert_eq!( - boundaries_in_range(Point::new(1, 0)..Point::new(1, 5), &snapshot), - &[] - ); - assert_eq!( - boundaries_in_range(Point::new(1, 0)..Point::new(2, 0), &snapshot), - &[] - ); - assert_eq!( - boundaries_in_range(Point::new(1, 0)..Point::new(4, 0), &snapshot), - &[(MultiBufferRow(2), "ddd\neeee".to_string(), false)] - ); - assert_eq!( - boundaries_in_range(Point::new(1, 0)..Point::new(4, 0), &snapshot), - &[(MultiBufferRow(2), "ddd\neeee".to_string(), false)] - ); - assert_eq!( - boundaries_in_range(Point::new(2, 0)..Point::new(3, 0), &snapshot), - &[(MultiBufferRow(2), "ddd\neeee".to_string(), false)] - ); - assert_eq!( - boundaries_in_range(Point::new(4, 0)..Point::new(4, 2), &snapshot), - &[(MultiBufferRow(4), "jj".to_string(), true)] - ); - assert_eq!( - boundaries_in_range(Point::new(4, 2)..Point::new(4, 2), &snapshot), - &[] - ); - - buffer_1.update(cx, |buffer, cx| { - let text = "\n"; - buffer.edit( - [ - (Point::new(0, 0)..Point::new(0, 0), text), - (Point::new(2, 1)..Point::new(2, 3), text), - ], - None, - cx, - ); - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!( - snapshot.text(), - concat!( - "bbbb\n", // Preserve newlines - "c\n", // - "cc\n", // - "ddd\n", // - "eeee\n", // - "jj" // - ) - ); - - assert_eq!( - subscription.consume().into_inner(), - [Edit { - old: MultiBufferOffset(6)..MultiBufferOffset(8), - new: MultiBufferOffset(6)..MultiBufferOffset(7) - }] - ); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!( - snapshot.clip_point(Point::new(0, 5), Bias::Left), - Point::new(0, 4) - ); - assert_eq!( - snapshot.clip_point(Point::new(0, 5), Bias::Right), - Point::new(0, 4) - ); - assert_eq!( - snapshot.clip_point(Point::new(5, 1), Bias::Right), - Point::new(5, 1) - ); - assert_eq!( - snapshot.clip_point(Point::new(5, 2), Bias::Right), - Point::new(5, 2) - ); - assert_eq!( - snapshot.clip_point(Point::new(5, 3), Bias::Right), - Point::new(5, 2) - ); - - let snapshot = multibuffer.update(cx, |multibuffer, cx| { - let (buffer_2_excerpt_id, _) = - multibuffer.excerpts_for_buffer(buffer_2.read(cx).remote_id(), cx)[0].clone(); - multibuffer.remove_excerpts([buffer_2_excerpt_id], cx); - multibuffer.snapshot(cx) - }); - - assert_eq!( - snapshot.text(), - concat!( - "bbbb\n", // Preserve newlines - "c\n", // - "cc\n", // - "ddd\n", // - "eeee", // - ) - ); - - fn boundaries_in_range( - range: Range, - snapshot: &MultiBufferSnapshot, - ) -> Vec<(MultiBufferRow, String, bool)> { - snapshot - .excerpt_boundaries_in_range(range) - .map(|boundary| { - let starts_new_buffer = boundary.starts_new_buffer(); - ( - boundary.row, - boundary - .next - .buffer - .text_for_range(boundary.next.range.context) - .collect::(), - starts_new_buffer, - ) - }) - .collect::>() - } -} - -#[gpui::test] -async fn test_diff_boundary_anchors(cx: &mut TestAppContext) { - let base_text = "one\ntwo\nthree\n"; - let text = "one\nthree\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 multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - multibuffer.update(cx, |multibuffer, cx| multibuffer.add_diff(diff, cx)); - - let (before, after) = multibuffer.update(cx, |multibuffer, cx| { - let before = multibuffer.snapshot(cx).anchor_before(Point::new(1, 0)); - let after = multibuffer.snapshot(cx).anchor_after(Point::new(1, 0)); - multibuffer.set_all_diff_hunks_expanded(cx); - (before, after) - }); - cx.run_until_parked(); - - let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - let actual_text = snapshot.text(); - let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::>(); - let actual_diff = format_diff(&actual_text, &actual_row_infos, &Default::default(), None); - pretty_assertions::assert_eq!( - actual_diff, - indoc! { - " one - - two - three - " - }, - ); - - multibuffer.update(cx, |multibuffer, cx| { - let snapshot = multibuffer.snapshot(cx); - assert_eq!(before.to_point(&snapshot), Point::new(1, 0)); - assert_eq!(after.to_point(&snapshot), Point::new(2, 0)); - assert_eq!( - vec![Point::new(1, 0), Point::new(2, 0),], - snapshot.summaries_for_anchors::(&[before, after]), - ) - }) -} - -#[gpui::test] -async fn test_diff_hunks_in_range(cx: &mut TestAppContext) { - let base_text = "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\n"; - let text = "one\nfour\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 multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { - (multibuffer.snapshot(cx), multibuffer.subscribe()) - }); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.add_diff(diff, cx); - multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc! { - " one - - two - - three - four - - five - - six - seven - - eight - " - }, - ); - - assert_eq!( - snapshot - .diff_hunks_in_range(Point::new(1, 0)..Point::MAX) - .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0) - .collect::>(), - vec![1..3, 4..6, 7..8] - ); - - assert_eq!(snapshot.diff_hunk_before(Point::new(1, 1)), None,); - assert_eq!( - snapshot.diff_hunk_before(Point::new(7, 0)), - Some(MultiBufferRow(4)) - ); - assert_eq!( - snapshot.diff_hunk_before(Point::new(4, 0)), - Some(MultiBufferRow(1)) - ); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc! { - " - one - four - seven - " - }, - ); - - assert_eq!( - snapshot.diff_hunk_before(Point::new(2, 0)), - Some(MultiBufferRow(1)), - ); - assert_eq!( - snapshot.diff_hunk_before(Point::new(4, 0)), - Some(MultiBufferRow(2)) - ); -} - -#[gpui::test] -async 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 multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - - let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { - multibuffer.add_diff(diff.clone(), cx); - (multibuffer.snapshot(cx), multibuffer.subscribe()) - }); - - cx.executor().run_until_parked(); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_all_diff_hunks_expanded(cx); - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc! { - " - one - two - + THREE - four - five - - six - seven - " - }, - ); - - // Insert a newline within an insertion hunk - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.edit([(Point::new(2, 0)..Point::new(2, 0), "__\n__")], None, cx); - }); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc! { - " - one - two - + __ - + __THREE - four - five - - six - seven - " - }, - ); - - // Delete the newline before a deleted hunk. - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.edit([(Point::new(5, 4)..Point::new(6, 0), "")], None, cx); - }); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc! { - " - one - two - + __ - + __THREE - four - fiveseven - " - }, - ); - - multibuffer.update(cx, |multibuffer, cx| multibuffer.undo(cx)); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc! { - " - one - two - + __ - + __THREE - four - five - - six - seven - " - }, - ); - - // Cannot (yet) insert at the beginning of a deleted hunk. - // (because it would put the newline in the wrong place) - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.edit([(Point::new(6, 0)..Point::new(6, 0), "\n")], None, cx); - }); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc! { - " - one - two - + __ - + __THREE - four - five - - six - seven - " - }, - ); - - // Replace a range that ends in a deleted hunk. - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.edit([(Point::new(5, 2)..Point::new(6, 2), "fty-")], None, cx); - }); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc! { - " - one - two - + __ - + __THREE - four - fifty-seven - " - }, - ); -} - -#[gpui::test] -fn test_excerpt_events(cx: &mut App) { - let buffer_1 = cx.new(|cx| Buffer::local(sample_text(10, 3, 'a'), cx)); - let buffer_2 = cx.new(|cx| Buffer::local(sample_text(10, 3, 'm'), cx)); - - let leader_multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - let follower_multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - let follower_edit_event_count = Arc::new(RwLock::new(0)); - - follower_multibuffer.update(cx, |_, cx| { - let follower_edit_event_count = follower_edit_event_count.clone(); - cx.subscribe( - &leader_multibuffer, - move |follower, _, event, cx| match event.clone() { - Event::ExcerptsAdded { - buffer, - predecessor, - excerpts, - } => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx), - Event::ExcerptsRemoved { ids, .. } => follower.remove_excerpts(ids, cx), - Event::Edited { .. } => { - *follower_edit_event_count.write() += 1; - } - _ => {} - }, - ) - .detach(); - }); - - leader_multibuffer.update(cx, |leader, cx| { - leader.push_excerpts( - buffer_1.clone(), - [ExcerptRange::new(0..8), ExcerptRange::new(12..16)], - cx, - ); - leader.insert_excerpts_after( - leader.excerpt_ids()[0], - buffer_2.clone(), - [ExcerptRange::new(0..5), ExcerptRange::new(10..15)], - cx, - ) - }); - assert_eq!( - leader_multibuffer.read(cx).snapshot(cx).text(), - follower_multibuffer.read(cx).snapshot(cx).text(), - ); - assert_eq!(*follower_edit_event_count.read(), 2); - - leader_multibuffer.update(cx, |leader, cx| { - let excerpt_ids = leader.excerpt_ids(); - leader.remove_excerpts([excerpt_ids[1], excerpt_ids[3]], cx); - }); - assert_eq!( - leader_multibuffer.read(cx).snapshot(cx).text(), - follower_multibuffer.read(cx).snapshot(cx).text(), - ); - assert_eq!(*follower_edit_event_count.read(), 3); - - // Removing an empty set of excerpts is a noop. - leader_multibuffer.update(cx, |leader, cx| { - leader.remove_excerpts([], cx); - }); - assert_eq!( - leader_multibuffer.read(cx).snapshot(cx).text(), - follower_multibuffer.read(cx).snapshot(cx).text(), - ); - assert_eq!(*follower_edit_event_count.read(), 3); - - // Adding an empty set of excerpts is a noop. - leader_multibuffer.update(cx, |leader, cx| { - leader.push_excerpts::(buffer_2.clone(), [], cx); - }); - assert_eq!( - leader_multibuffer.read(cx).snapshot(cx).text(), - follower_multibuffer.read(cx).snapshot(cx).text(), - ); - assert_eq!(*follower_edit_event_count.read(), 3); - - leader_multibuffer.update(cx, |leader, cx| { - leader.clear(cx); - }); - assert_eq!( - leader_multibuffer.read(cx).snapshot(cx).text(), - follower_multibuffer.read(cx).snapshot(cx).text(), - ); - assert_eq!(*follower_edit_event_count.read(), 4); -} - -#[gpui::test] -fn test_expand_excerpts(cx: &mut App) { - let buffer = cx.new(|cx| Buffer::local(sample_text(20, 3, 'a'), cx)); - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - PathKey::for_buffer(&buffer, cx), - buffer, - vec![ - // Note that in this test, this first excerpt - // does not contain a new line - Point::new(3, 2)..Point::new(3, 3), - Point::new(7, 1)..Point::new(7, 3), - Point::new(15, 0)..Point::new(15, 0), - ], - 1, - cx, - ) - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!( - snapshot.text(), - concat!( - "ccc\n", // - "ddd\n", // - "eee", // - "\n", // End of excerpt - "ggg\n", // - "hhh\n", // - "iii", // - "\n", // End of excerpt - "ooo\n", // - "ppp\n", // - "qqq", // End of excerpt - ) - ); - drop(snapshot); - - multibuffer.update(cx, |multibuffer, cx| { - let line_zero = multibuffer.snapshot(cx).anchor_before(Point::new(0, 0)); - multibuffer.expand_excerpts( - multibuffer.excerpt_ids(), - 1, - ExpandExcerptDirection::UpAndDown, - cx, - ); - let snapshot = multibuffer.snapshot(cx); - let line_two = snapshot.anchor_before(Point::new(2, 0)); - assert_eq!(line_two.cmp(&line_zero, &snapshot), cmp::Ordering::Greater); - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!( - snapshot.text(), - concat!( - "bbb\n", // - "ccc\n", // - "ddd\n", // - "eee\n", // - "fff\n", // - "ggg\n", // - "hhh\n", // - "iii\n", // - "jjj\n", // End of excerpt - "nnn\n", // - "ooo\n", // - "ppp\n", // - "qqq\n", // - "rrr", // End of excerpt - ) - ); -} - -#[gpui::test(iterations = 100)] -async fn test_set_anchored_excerpts_for_path(cx: &mut TestAppContext) { - let buffer_1 = cx.new(|cx| Buffer::local(sample_text(20, 3, 'a'), cx)); - let buffer_2 = cx.new(|cx| Buffer::local(sample_text(15, 4, 'a'), cx)); - let snapshot_1 = buffer_1.update(cx, |buffer, _| buffer.snapshot()); - let snapshot_2 = buffer_2.update(cx, |buffer, _| buffer.snapshot()); - let ranges_1 = vec![ - snapshot_1.anchor_before(Point::new(3, 2))..snapshot_1.anchor_before(Point::new(4, 2)), - snapshot_1.anchor_before(Point::new(7, 1))..snapshot_1.anchor_before(Point::new(7, 3)), - snapshot_1.anchor_before(Point::new(15, 0))..snapshot_1.anchor_before(Point::new(15, 0)), - ]; - let ranges_2 = vec![ - snapshot_2.anchor_before(Point::new(2, 1))..snapshot_2.anchor_before(Point::new(3, 1)), - snapshot_2.anchor_before(Point::new(10, 0))..snapshot_2.anchor_before(Point::new(10, 2)), - ]; - - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - let anchor_ranges_1 = multibuffer - .update(cx, |multibuffer, cx| { - multibuffer.set_anchored_excerpts_for_path( - PathKey::for_buffer(&buffer_1, cx), - buffer_1.clone(), - ranges_1, - 2, - cx, - ) - }) - .await; - let snapshot_1 = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - assert_eq!( - anchor_ranges_1 - .iter() - .map(|range| range.to_point(&snapshot_1)) - .collect::>(), - vec![ - Point::new(2, 2)..Point::new(3, 2), - Point::new(6, 1)..Point::new(6, 3), - Point::new(11, 0)..Point::new(11, 0), - ] - ); - let anchor_ranges_2 = multibuffer - .update(cx, |multibuffer, cx| { - multibuffer.set_anchored_excerpts_for_path( - PathKey::for_buffer(&buffer_2, cx), - buffer_2.clone(), - ranges_2, - 2, - cx, - ) - }) - .await; - let snapshot_2 = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - assert_eq!( - anchor_ranges_2 - .iter() - .map(|range| range.to_point(&snapshot_2)) - .collect::>(), - vec![ - Point::new(16, 1)..Point::new(17, 1), - Point::new(22, 0)..Point::new(22, 2) - ] - ); - - let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - assert_eq!( - snapshot.text(), - concat!( - "bbb\n", // buffer_1 - "ccc\n", // - "ddd\n", // <-- excerpt 1 - "eee\n", // <-- excerpt 1 - "fff\n", // - "ggg\n", // - "hhh\n", // <-- excerpt 2 - "iii\n", // - "jjj\n", // - // - "nnn\n", // - "ooo\n", // - "ppp\n", // <-- excerpt 3 - "qqq\n", // - "rrr\n", // - // - "aaaa\n", // buffer 2 - "bbbb\n", // - "cccc\n", // <-- excerpt 4 - "dddd\n", // <-- excerpt 4 - "eeee\n", // - "ffff\n", // - // - "iiii\n", // - "jjjj\n", // - "kkkk\n", // <-- excerpt 5 - "llll\n", // - "mmmm", // - ) - ); -} - -#[gpui::test] -fn test_empty_multibuffer(cx: &mut App) { - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), ""); - assert_eq!( - snapshot - .row_infos(MultiBufferRow(0)) - .map(|info| info.buffer_row) - .collect::>(), - &[Some(0)] - ); - assert!( - snapshot - .row_infos(MultiBufferRow(1)) - .map(|info| info.buffer_row) - .collect::>() - .is_empty(), - ); -} - -#[gpui::test] -async fn test_empty_diff_excerpt(cx: &mut TestAppContext) { - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - let buffer = cx.new(|cx| Buffer::local("", cx)); - let base_text = "a\nb\nc"; - - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.push_excerpts(buffer.clone(), [ExcerptRange::new(0..0)], cx); - multibuffer.set_all_diff_hunks_expanded(cx); - multibuffer.add_diff(diff.clone(), cx); - }); - cx.run_until_parked(); - - let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - assert_eq!(snapshot.text(), "a\nb\nc\n"); - - let hunk = snapshot - .diff_hunks_in_range(Point::new(1, 1)..Point::new(1, 1)) - .next() - .unwrap(); - - assert_eq!(hunk.diff_base_byte_range.start, BufferOffset(0)); - - let buf2 = cx.new(|cx| Buffer::local("X", cx)); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.push_excerpts(buf2, [ExcerptRange::new(0..1)], cx); - }); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "a\nb\nc")], None, cx); - diff.update(cx, |diff, cx| { - diff.recalculate_diff_sync(buffer.snapshot().text, cx); - }); - assert_eq!(buffer.text(), "a\nb\nc") - }); - cx.run_until_parked(); - - let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - assert_eq!(snapshot.text(), "a\nb\nc\nX"); - - buffer.update(cx, |buffer, cx| { - buffer.undo(cx); - diff.update(cx, |diff, cx| { - diff.recalculate_diff_sync(buffer.snapshot().text, cx); - }); - assert_eq!(buffer.text(), "") - }); - cx.run_until_parked(); - - let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - assert_eq!(snapshot.text(), "a\nb\nc\n\nX"); -} - -#[gpui::test] -fn test_singleton_multibuffer_anchors(cx: &mut App) { - let buffer = cx.new(|cx| Buffer::local("abcd", cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - let old_snapshot = multibuffer.read(cx).snapshot(cx); - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "X")], None, cx); - buffer.edit([(5..5, "Y")], None, cx); - }); - let new_snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!(old_snapshot.text(), "abcd"); - assert_eq!(new_snapshot.text(), "XabcdY"); - - assert_eq!( - old_snapshot - .anchor_before(MultiBufferOffset(0)) - .to_offset(&new_snapshot), - MultiBufferOffset(0) - ); - assert_eq!( - old_snapshot - .anchor_after(MultiBufferOffset(0)) - .to_offset(&new_snapshot), - MultiBufferOffset(1) - ); - assert_eq!( - old_snapshot - .anchor_before(MultiBufferOffset(4)) - .to_offset(&new_snapshot), - MultiBufferOffset(5) - ); - assert_eq!( - old_snapshot - .anchor_after(MultiBufferOffset(4)) - .to_offset(&new_snapshot), - MultiBufferOffset(6) - ); -} - -#[gpui::test] -fn test_multibuffer_anchors(cx: &mut App) { - let buffer_1 = cx.new(|cx| Buffer::local("abcd", cx)); - let buffer_2 = cx.new(|cx| Buffer::local("efghi", cx)); - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); - multibuffer.push_excerpts(buffer_1.clone(), [ExcerptRange::new(0..4)], cx); - multibuffer.push_excerpts(buffer_2.clone(), [ExcerptRange::new(0..5)], cx); - multibuffer - }); - let old_snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!( - old_snapshot - .anchor_before(MultiBufferOffset(0)) - .to_offset(&old_snapshot), - MultiBufferOffset(0) - ); - assert_eq!( - old_snapshot - .anchor_after(MultiBufferOffset(0)) - .to_offset(&old_snapshot), - MultiBufferOffset(0) - ); - assert_eq!(Anchor::min().to_offset(&old_snapshot), MultiBufferOffset(0)); - assert_eq!(Anchor::min().to_offset(&old_snapshot), MultiBufferOffset(0)); - assert_eq!( - Anchor::max().to_offset(&old_snapshot), - MultiBufferOffset(10) - ); - assert_eq!( - Anchor::max().to_offset(&old_snapshot), - MultiBufferOffset(10) - ); - - buffer_1.update(cx, |buffer, cx| { - buffer.edit([(0..0, "W")], None, cx); - buffer.edit([(5..5, "X")], None, cx); - }); - buffer_2.update(cx, |buffer, cx| { - buffer.edit([(0..0, "Y")], None, cx); - buffer.edit([(6..6, "Z")], None, cx); - }); - let new_snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!(old_snapshot.text(), "abcd\nefghi"); - assert_eq!(new_snapshot.text(), "WabcdX\nYefghiZ"); - - assert_eq!( - old_snapshot - .anchor_before(MultiBufferOffset(0)) - .to_offset(&new_snapshot), - MultiBufferOffset(0) - ); - assert_eq!( - old_snapshot - .anchor_after(MultiBufferOffset(0)) - .to_offset(&new_snapshot), - MultiBufferOffset(1) - ); - assert_eq!( - old_snapshot - .anchor_before(MultiBufferOffset(1)) - .to_offset(&new_snapshot), - MultiBufferOffset(2) - ); - assert_eq!( - old_snapshot - .anchor_after(MultiBufferOffset(1)) - .to_offset(&new_snapshot), - MultiBufferOffset(2) - ); - assert_eq!( - old_snapshot - .anchor_before(MultiBufferOffset(2)) - .to_offset(&new_snapshot), - MultiBufferOffset(3) - ); - assert_eq!( - old_snapshot - .anchor_after(MultiBufferOffset(2)) - .to_offset(&new_snapshot), - MultiBufferOffset(3) - ); - assert_eq!( - old_snapshot - .anchor_before(MultiBufferOffset(5)) - .to_offset(&new_snapshot), - MultiBufferOffset(7) - ); - assert_eq!( - old_snapshot - .anchor_after(MultiBufferOffset(5)) - .to_offset(&new_snapshot), - MultiBufferOffset(8) - ); - assert_eq!( - old_snapshot - .anchor_before(MultiBufferOffset(10)) - .to_offset(&new_snapshot), - MultiBufferOffset(13) - ); - assert_eq!( - old_snapshot - .anchor_after(MultiBufferOffset(10)) - .to_offset(&new_snapshot), - MultiBufferOffset(14) - ); -} - -#[gpui::test] -fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut App) { - let buffer_1 = cx.new(|cx| Buffer::local("abcd", cx)); - let buffer_2 = cx.new(|cx| Buffer::local("ABCDEFGHIJKLMNOP", cx)); - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - - // Create an insertion id in buffer 1 that doesn't exist in buffer 2. - // Add an excerpt from buffer 1 that spans this new insertion. - buffer_1.update(cx, |buffer, cx| buffer.edit([(4..4, "123")], None, cx)); - let excerpt_id_1 = multibuffer.update(cx, |multibuffer, cx| { - multibuffer - .push_excerpts(buffer_1.clone(), [ExcerptRange::new(0..7)], cx) - .pop() - .unwrap() - }); - - let snapshot_1 = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot_1.text(), "abcd123"); - - // Replace the buffer 1 excerpt with new excerpts from buffer 2. - let (excerpt_id_2, excerpt_id_3) = multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts([excerpt_id_1], cx); - let mut ids = multibuffer - .push_excerpts( - buffer_2.clone(), - [ - ExcerptRange::new(0..4), - ExcerptRange::new(6..10), - ExcerptRange::new(12..16), - ], - cx, - ) - .into_iter(); - (ids.next().unwrap(), ids.next().unwrap()) - }); - let snapshot_2 = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot_2.text(), "ABCD\nGHIJ\nMNOP"); - - // The old excerpt id doesn't get reused. - assert_ne!(excerpt_id_2, excerpt_id_1); - - // Resolve some anchors from the previous snapshot in the new snapshot. - // The current excerpts are from a different buffer, so we don't attempt to - // resolve the old text anchor in the new buffer. - assert_eq!( - snapshot_2.summary_for_anchor::( - &snapshot_1.anchor_before(MultiBufferOffset(2)) - ), - MultiBufferOffset(0) - ); - assert_eq!( - snapshot_2.summaries_for_anchors::(&[ - snapshot_1.anchor_before(MultiBufferOffset(2)), - snapshot_1.anchor_after(MultiBufferOffset(3)) - ]), - vec![MultiBufferOffset(0), MultiBufferOffset(0)] - ); - - // Refresh anchors from the old snapshot. The return value indicates that both - // anchors lost their original excerpt. - let refresh = snapshot_2.refresh_anchors(&[ - snapshot_1.anchor_before(MultiBufferOffset(2)), - snapshot_1.anchor_after(MultiBufferOffset(3)), - ]); - assert_eq!( - refresh, - &[ - (0, snapshot_2.anchor_before(MultiBufferOffset(0)), false), - (1, snapshot_2.anchor_after(MultiBufferOffset(0)), false), - ] - ); - - // Replace the middle excerpt with a smaller excerpt in buffer 2, - // that intersects the old excerpt. - let excerpt_id_5 = multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts([excerpt_id_3], cx); - multibuffer - .insert_excerpts_after( - excerpt_id_2, - buffer_2.clone(), - [ExcerptRange::new(5..8)], - cx, - ) - .pop() - .unwrap() - }); - - let snapshot_3 = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot_3.text(), "ABCD\nFGH\nMNOP"); - assert_ne!(excerpt_id_5, excerpt_id_3); - - // Resolve some anchors from the previous snapshot in the new snapshot. - // The third anchor can't be resolved, since its excerpt has been removed, - // so it resolves to the same position as its predecessor. - let anchors = [ - snapshot_2.anchor_before(MultiBufferOffset(0)), - snapshot_2.anchor_after(MultiBufferOffset(2)), - snapshot_2.anchor_after(MultiBufferOffset(6)), - snapshot_2.anchor_after(MultiBufferOffset(14)), - ]; - assert_eq!( - snapshot_3.summaries_for_anchors::(&anchors), - &[ - MultiBufferOffset(0), - MultiBufferOffset(2), - MultiBufferOffset(9), - MultiBufferOffset(13) - ] - ); - - let new_anchors = snapshot_3.refresh_anchors(&anchors); - assert_eq!( - new_anchors.iter().map(|a| (a.0, a.2)).collect::>(), - &[(0, true), (1, true), (2, true), (3, true)] - ); - assert_eq!( - snapshot_3.summaries_for_anchors::(new_anchors.iter().map(|a| &a.1)), - &[ - MultiBufferOffset(0), - MultiBufferOffset(2), - MultiBufferOffset(7), - MultiBufferOffset(13) - ] - ); -} - -#[gpui::test] -async fn test_basic_diff_hunks(cx: &mut TestAppContext) { - let text = indoc!( - " - ZERO - one - TWO - three - six - " - ); - let base_text = indoc!( - " - one - two - three - four - five - six - " - ); - - let buffer = cx.new(|cx| Buffer::local(text, cx)); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); - cx.run_until_parked(); - - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx); - multibuffer.add_diff(diff.clone(), cx); - multibuffer - }); - - let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { - (multibuffer.snapshot(cx), multibuffer.subscribe()) - }); - assert_eq!( - snapshot.text(), - indoc!( - " - ZERO - one - TWO - three - six - " - ), - ); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - + ZERO - one - - two - + TWO - three - - four - - five - six - " - ), - ); - - assert_eq!( - snapshot - .row_infos(MultiBufferRow(0)) - .map(|info| (info.buffer_row, info.diff_status)) - .collect::>(), - vec![ - (Some(0), Some(DiffHunkStatus::added_none())), - (Some(1), None), - (Some(1), Some(DiffHunkStatus::deleted_none())), - (Some(2), Some(DiffHunkStatus::added_none())), - (Some(3), None), - (Some(3), Some(DiffHunkStatus::deleted_none())), - (Some(4), Some(DiffHunkStatus::deleted_none())), - (Some(4), None), - (Some(5), None) - ] - ); - - assert_chunks_in_ranges(&snapshot); - assert_consistent_line_numbers(&snapshot); - assert_position_translation(&snapshot); - assert_line_indents(&snapshot); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], cx) - }); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - ZERO - one - TWO - three - six - " - ), - ); - - assert_chunks_in_ranges(&snapshot); - assert_consistent_line_numbers(&snapshot); - assert_position_translation(&snapshot); - assert_line_indents(&snapshot); - - // Expand the first diff hunk - multibuffer.update(cx, |multibuffer, cx| { - let position = multibuffer.read(cx).anchor_before(Point::new(2, 2)); - multibuffer.expand_diff_hunks(vec![position..position], cx) - }); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - ZERO - one - - two - + TWO - three - six - " - ), - ); - - // Expand the second diff hunk - multibuffer.update(cx, |multibuffer, cx| { - let start = multibuffer.read(cx).anchor_before(Point::new(4, 0)); - let end = multibuffer.read(cx).anchor_before(Point::new(5, 0)); - multibuffer.expand_diff_hunks(vec![start..end], cx) - }); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - ZERO - one - - two - + TWO - three - - four - - five - six - " - ), - ); - - assert_chunks_in_ranges(&snapshot); - assert_consistent_line_numbers(&snapshot); - assert_position_translation(&snapshot); - assert_line_indents(&snapshot); - - // Edit the buffer before the first hunk - buffer.update(cx, |buffer, cx| { - buffer.edit_via_marked_text( - indoc!( - " - ZERO - one« hundred - thousand» - TWO - three - six - " - ), - None, - cx, - ); - }); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - ZERO - one hundred - thousand - - two - + TWO - three - - four - - five - six - " - ), - ); - - assert_chunks_in_ranges(&snapshot); - assert_consistent_line_numbers(&snapshot); - assert_position_translation(&snapshot); - assert_line_indents(&snapshot); - - // Recalculate the diff, changing the first diff hunk. - diff.update(cx, |diff, cx| { - diff.recalculate_diff_sync(buffer.read(cx).text_snapshot(), cx); - }); - cx.run_until_parked(); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - ZERO - one hundred - thousand - TWO - three - - four - - five - six - " - ), - ); - - assert_eq!( - snapshot - .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.len()) - .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0) - .collect::>(), - &[0..4, 5..7] - ); -} - -#[gpui::test] -async fn test_repeatedly_expand_a_diff_hunk(cx: &mut TestAppContext) { - let text = indoc!( - " - one - TWO - THREE - four - FIVE - six - " - ); - let base_text = indoc!( - " - one - four - five - six - " - ); - - let buffer = cx.new(|cx| Buffer::local(text, cx)); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); - cx.run_until_parked(); - - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx); - multibuffer.add_diff(diff.clone(), cx); - multibuffer - }); - - let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { - (multibuffer.snapshot(cx), multibuffer.subscribe()) - }); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - one - + TWO - + THREE - four - - five - + FIVE - six - " - ), - ); - - // Regression test: expanding diff hunks that are already expanded should not change anything. - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.expand_diff_hunks( - vec![ - snapshot.anchor_before(Point::new(2, 0))..snapshot.anchor_before(Point::new(2, 0)), - ], - cx, - ); - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - one - + TWO - + THREE - four - - five - + FIVE - six - " - ), - ); - - // Now collapse all diff hunks - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - one - TWO - THREE - four - FIVE - six - " - ), - ); - - // Expand the hunks again, but this time provide two ranges that are both within the same hunk - // Target the first hunk which is between "one" and "four" - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.expand_diff_hunks( - vec![ - snapshot.anchor_before(Point::new(4, 0))..snapshot.anchor_before(Point::new(4, 0)), - snapshot.anchor_before(Point::new(4, 2))..snapshot.anchor_before(Point::new(4, 2)), - ], - cx, - ); - }); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - one - TWO - THREE - four - - five - + FIVE - six - " - ), - ); -} - -#[gpui::test] -fn test_set_excerpts_for_buffer_ordering(cx: &mut TestAppContext) { - let buf1 = cx.new(|cx| { - Buffer::local( - indoc! { - "zero - one - two - two.five - three - four - five - six - seven - eight - nine - ten - eleven - ", - }, - cx, - ) - }); - let path1: PathKey = PathKey::with_sort_prefix(0, rel_path("root").into_arc()); - - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - path1.clone(), - buf1.clone(), - vec![ - Point::row_range(1..2), - Point::row_range(6..7), - Point::row_range(11..12), - ], - 1, - cx, - ); - }); - - assert_excerpts_match( - &multibuffer, - cx, - indoc! { - "----- - zero - one - two - two.five - ----- - four - five - six - seven - ----- - nine - ten - eleven - " - }, - ); - - buf1.update(cx, |buffer, cx| buffer.edit([(0..5, "")], None, cx)); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - path1.clone(), - buf1.clone(), - vec![ - Point::row_range(0..3), - Point::row_range(5..7), - Point::row_range(10..11), - ], - 1, - cx, - ); - }); - - assert_excerpts_match( - &multibuffer, - cx, - indoc! { - "----- - one - two - two.five - three - four - five - six - seven - eight - nine - ten - eleven - " - }, - ); -} - -#[gpui::test] -fn test_set_excerpts_for_buffer(cx: &mut TestAppContext) { - let buf1 = cx.new(|cx| { - Buffer::local( - indoc! { - "zero - one - two - three - four - five - six - seven - ", - }, - cx, - ) - }); - let path1: PathKey = PathKey::with_sort_prefix(0, rel_path("root").into_arc()); - let buf2 = cx.new(|cx| { - Buffer::local( - indoc! { - "000 - 111 - 222 - 333 - 444 - 555 - 666 - 777 - 888 - 999 - " - }, - cx, - ) - }); - let path2 = PathKey::with_sort_prefix(1, rel_path("root").into_arc()); - - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - path1.clone(), - buf1.clone(), - vec![Point::row_range(0..1)], - 2, - cx, - ); - }); - - assert_excerpts_match( - &multibuffer, - cx, - indoc! { - "----- - zero - one - two - three - " - }, - ); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path(path1.clone(), buf1.clone(), vec![], 2, cx); - }); - - assert_excerpts_match(&multibuffer, cx, ""); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - path1.clone(), - buf1.clone(), - vec![Point::row_range(0..1), Point::row_range(7..8)], - 2, - cx, - ); - }); - - assert_excerpts_match( - &multibuffer, - cx, - indoc! {"----- - zero - one - two - three - ----- - five - six - seven - "}, - ); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - path1.clone(), - buf1.clone(), - vec![Point::row_range(0..1), Point::row_range(5..6)], - 2, - cx, - ); - }); - - assert_excerpts_match( - &multibuffer, - cx, - indoc! {"----- - zero - one - two - three - four - five - six - seven - "}, - ); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - path2.clone(), - buf2.clone(), - vec![Point::row_range(2..3)], - 2, - cx, - ); - }); - - assert_excerpts_match( - &multibuffer, - cx, - indoc! {"----- - zero - one - two - three - four - five - six - seven - ----- - 000 - 111 - 222 - 333 - 444 - 555 - "}, - ); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path(path1.clone(), buf1.clone(), vec![], 2, cx); - }); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - path1.clone(), - buf1.clone(), - vec![Point::row_range(3..4)], - 2, - cx, - ); - }); - - assert_excerpts_match( - &multibuffer, - cx, - indoc! {"----- - one - two - three - four - five - six - ----- - 000 - 111 - 222 - 333 - 444 - 555 - "}, - ); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - path1.clone(), - buf1.clone(), - vec![Point::row_range(3..4)], - 2, - cx, - ); - }); -} - -#[gpui::test] -fn test_set_excerpts_for_buffer_rename(cx: &mut TestAppContext) { - let buf1 = cx.new(|cx| { - Buffer::local( - indoc! { - "zero - one - two - three - four - five - six - seven - ", - }, - cx, - ) - }); - let path: PathKey = PathKey::with_sort_prefix(0, rel_path("root").into_arc()); - let buf2 = cx.new(|cx| { - Buffer::local( - indoc! { - "000 - 111 - 222 - 333 - " - }, - cx, - ) - }); - - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - path.clone(), - buf1.clone(), - vec![Point::row_range(1..1), Point::row_range(4..5)], - 1, - cx, - ); - }); - - assert_excerpts_match( - &multibuffer, - cx, - indoc! { - "----- - zero - one - two - three - four - five - six - " - }, - ); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - path.clone(), - buf2.clone(), - vec![Point::row_range(0..1)], - 2, - cx, - ); - }); - - assert_excerpts_match( - &multibuffer, - cx, - indoc! {"----- - 000 - 111 - 222 - 333 - "}, - ); -} - -#[gpui::test] -async fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) { - let base_text_1 = indoc!( - " - one - two - three - four - five - six - " - ); - let text_1 = indoc!( - " - ZERO - one - TWO - three - six - " - ); - let base_text_2 = indoc!( - " - seven - eight - nine - ten - eleven - twelve - " - ); - let text_2 = indoc!( - " - eight - nine - eleven - THIRTEEN - FOURTEEN - " - ); - - let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx)); - let buffer_2 = cx.new(|cx| Buffer::local(text_2, cx)); - let diff_1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text_1, &buffer_1, cx)); - let diff_2 = cx.new(|cx| BufferDiff::new_with_base_text(base_text_2, &buffer_2, cx)); - cx.run_until_parked(); - - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)], - cx, - ); - multibuffer.add_diff(diff_1.clone(), cx); - multibuffer.add_diff(diff_2.clone(), cx); - multibuffer - }); - - let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { - (multibuffer.snapshot(cx), multibuffer.subscribe()) - }); - assert_eq!( - snapshot.text(), - indoc!( - " - ZERO - one - TWO - three - six - - eight - nine - eleven - THIRTEEN - FOURTEEN - " - ), - ); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - + ZERO - one - - two - + TWO - three - - four - - five - six - - - seven - eight - nine - - ten - eleven - - twelve - + THIRTEEN - + FOURTEEN - " - ), - ); - - let id_1 = buffer_1.read_with(cx, |buffer, _| buffer.remote_id()); - let id_2 = buffer_2.read_with(cx, |buffer, _| buffer.remote_id()); - let base_id_1 = diff_1.read_with(cx, |diff, _| diff.base_text().remote_id()); - let base_id_2 = diff_2.read_with(cx, |diff, _| diff.base_text().remote_id()); - - let buffer_lines = (0..=snapshot.max_row().0) - .map(|row| { - let (buffer, range) = snapshot.buffer_line_for_row(MultiBufferRow(row))?; - Some(( - buffer.remote_id(), - buffer.text_for_range(range).collect::(), - )) - }) - .collect::>(); - pretty_assertions::assert_eq!( - buffer_lines, - [ - Some((id_1, "ZERO".into())), - Some((id_1, "one".into())), - Some((base_id_1, "two".into())), - Some((id_1, "TWO".into())), - Some((id_1, " three".into())), - Some((base_id_1, "four".into())), - Some((base_id_1, "five".into())), - Some((id_1, "six".into())), - Some((id_1, "".into())), - Some((base_id_2, "seven".into())), - Some((id_2, " eight".into())), - Some((id_2, "nine".into())), - Some((base_id_2, "ten".into())), - Some((id_2, "eleven".into())), - Some((base_id_2, "twelve".into())), - Some((id_2, "THIRTEEN".into())), - Some((id_2, "FOURTEEN".into())), - Some((id_2, "".into())), - ] - ); - - let buffer_ids_by_range = [ - (Point::new(0, 0)..Point::new(0, 0), &[id_1] as &[_]), - (Point::new(0, 0)..Point::new(2, 0), &[id_1]), - (Point::new(2, 0)..Point::new(2, 0), &[id_1]), - (Point::new(3, 0)..Point::new(3, 0), &[id_1]), - (Point::new(8, 0)..Point::new(9, 0), &[id_1]), - (Point::new(8, 0)..Point::new(10, 0), &[id_1, id_2]), - (Point::new(9, 0)..Point::new(9, 0), &[id_2]), - ]; - for (range, buffer_ids) in buffer_ids_by_range { - assert_eq!( - snapshot - .buffer_ids_for_range(range.clone()) - .collect::>(), - buffer_ids, - "buffer_ids_for_range({range:?}" - ); - } - - assert_position_translation(&snapshot); - assert_line_indents(&snapshot); - - assert_eq!( - snapshot - .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.len()) - .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0) - .collect::>(), - &[0..1, 2..4, 5..7, 9..10, 12..13, 14..17] - ); - - buffer_2.update(cx, |buffer, cx| { - buffer.edit_via_marked_text( - indoc!( - " - eight - «»eleven - THIRTEEN - FOURTEEN - " - ), - None, - cx, - ); - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - + ZERO - one - - two - + TWO - three - - four - - five - six - - - seven - eight - eleven - - twelve - + THIRTEEN - + FOURTEEN - " - ), - ); - - assert_line_indents(&snapshot); -} - -/// A naive implementation of a multi-buffer that does not maintain -/// any derived state, used for comparison in a randomized test. -#[derive(Default)] -struct ReferenceMultibuffer { - excerpts: Vec, - diffs: HashMap>, -} - -#[derive(Debug)] -struct ReferenceExcerpt { - id: ExcerptId, - buffer: Entity, - range: Range, - expanded_diff_hunks: Vec, -} - -#[derive(Debug)] -struct ReferenceRegion { - buffer_id: Option, - range: Range, - buffer_range: Option>, - status: Option, - excerpt_id: Option, -} - -impl ReferenceMultibuffer { - fn expand_excerpts(&mut self, excerpts: &HashSet, line_count: u32, cx: &App) { - if line_count == 0 { - return; - } - - for id in excerpts { - let excerpt = self.excerpts.iter_mut().find(|e| e.id == *id).unwrap(); - let snapshot = excerpt.buffer.read(cx).snapshot(); - let mut point_range = excerpt.range.to_point(&snapshot); - point_range.start = Point::new(point_range.start.row.saturating_sub(line_count), 0); - point_range.end = - snapshot.clip_point(Point::new(point_range.end.row + line_count, 0), Bias::Left); - point_range.end.column = snapshot.line_len(point_range.end.row); - excerpt.range = - snapshot.anchor_before(point_range.start)..snapshot.anchor_after(point_range.end); - } - } - - fn remove_excerpt(&mut self, id: ExcerptId, cx: &App) { - let ix = self - .excerpts - .iter() - .position(|excerpt| excerpt.id == id) - .unwrap(); - let excerpt = self.excerpts.remove(ix); - let buffer = excerpt.buffer.read(cx); - let id = buffer.remote_id(); - log::info!( - "Removing excerpt {}: {:?}", - ix, - buffer - .text_for_range(excerpt.range.to_offset(buffer)) - .collect::(), - ); - if !self - .excerpts - .iter() - .any(|excerpt| excerpt.buffer.read(cx).remote_id() == id) - { - self.diffs.remove(&id); - } - } - - fn insert_excerpt_after( - &mut self, - prev_id: ExcerptId, - new_excerpt_id: ExcerptId, - (buffer_handle, anchor_range): (Entity, Range), - ) { - let excerpt_ix = if prev_id == ExcerptId::max() { - self.excerpts.len() - } else { - self.excerpts - .iter() - .position(|excerpt| excerpt.id == prev_id) - .unwrap() - + 1 - }; - self.excerpts.insert( - excerpt_ix, - ReferenceExcerpt { - id: new_excerpt_id, - buffer: buffer_handle, - range: anchor_range, - expanded_diff_hunks: Vec::new(), - }, - ); - } - - fn expand_diff_hunks(&mut self, excerpt_id: ExcerptId, range: Range, cx: &App) { - let excerpt = self - .excerpts - .iter_mut() - .find(|e| e.id == excerpt_id) - .unwrap(); - let buffer = excerpt.buffer.read(cx).snapshot(); - let buffer_id = buffer.remote_id(); - let Some(diff) = self.diffs.get(&buffer_id) else { - return; - }; - let excerpt_range = excerpt.range.to_offset(&buffer); - for hunk in diff.read(cx).hunks_intersecting_range(range, &buffer, cx) { - let hunk_range = hunk.buffer_range.to_offset(&buffer); - if hunk_range.start < excerpt_range.start || hunk_range.start > excerpt_range.end { - continue; - } - if let Err(ix) = excerpt - .expanded_diff_hunks - .binary_search_by(|anchor| anchor.cmp(&hunk.buffer_range.start, &buffer)) - { - log::info!( - "expanding diff hunk {:?}. excerpt:{:?}, excerpt range:{:?}", - hunk_range, - excerpt_id, - excerpt_range - ); - excerpt - .expanded_diff_hunks - .insert(ix, hunk.buffer_range.start); - } else { - log::trace!("hunk {hunk_range:?} already expanded in excerpt {excerpt_id:?}"); - } - } - } - - fn expected_content( - &self, - filter_mode: Option, - all_diff_hunks_expanded: bool, - cx: &App, - ) -> (String, Vec, HashSet) { - let mut text = String::new(); - let mut regions = Vec::::new(); - let mut filtered_regions = Vec::::new(); - let mut excerpt_boundary_rows = HashSet::default(); - for excerpt in &self.excerpts { - excerpt_boundary_rows.insert(MultiBufferRow(text.matches('\n').count() as u32)); - let buffer = excerpt.buffer.read(cx); - let buffer_range = excerpt.range.to_offset(buffer); - let diff = self.diffs.get(&buffer.remote_id()).unwrap().read(cx); - let base_buffer = diff.base_text(); - - let mut offset = buffer_range.start; - let hunks = diff - .hunks_intersecting_range(excerpt.range.clone(), buffer, cx) - .peekable(); - - for hunk in hunks { - // Ignore hunks that are outside the excerpt range. - let mut hunk_range = hunk.buffer_range.to_offset(buffer); - - hunk_range.end = hunk_range.end.min(buffer_range.end); - if hunk_range.start > buffer_range.end || hunk_range.start < buffer_range.start { - log::trace!("skipping hunk outside excerpt range"); - continue; - } - - if !all_diff_hunks_expanded - && !excerpt.expanded_diff_hunks.iter().any(|expanded_anchor| { - 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) { - log::trace!("skipping hunk with deleted start: {:?}", hunk.range); - continue; - } - - if hunk_range.start >= offset { - // Add the buffer text before the hunk - let len = text.len(); - text.extend(buffer.text_for_range(offset..hunk_range.start)); - if text.len() > len { - regions.push(ReferenceRegion { - buffer_id: Some(buffer.remote_id()), - range: len..text.len(), - buffer_range: Some((offset..hunk_range.start).to_point(&buffer)), - status: None, - excerpt_id: Some(excerpt.id), - }); - } - - // Add the deleted text for the hunk. - if !hunk.diff_base_byte_range.is_empty() - && filter_mode != Some(MultiBufferFilterMode::KeepInsertions) - { - let mut base_text = base_buffer - .text_for_range(hunk.diff_base_byte_range.clone()) - .collect::(); - if !base_text.ends_with('\n') { - base_text.push('\n'); - } - let len = text.len(); - text.push_str(&base_text); - regions.push(ReferenceRegion { - buffer_id: Some(base_buffer.remote_id()), - range: len..text.len(), - buffer_range: Some(hunk.diff_base_byte_range.to_point(&base_buffer)), - status: Some(DiffHunkStatus::deleted(hunk.secondary_status)), - excerpt_id: Some(excerpt.id), - }); - } - - offset = hunk_range.start; - } - - // Add the inserted text for the hunk. - if hunk_range.end > offset { - let is_filtered = filter_mode == Some(MultiBufferFilterMode::KeepDeletions); - let range = if is_filtered { - text.len()..text.len() - } else { - let len = text.len(); - text.extend(buffer.text_for_range(offset..hunk_range.end)); - len..text.len() - }; - let region = ReferenceRegion { - buffer_id: Some(buffer.remote_id()), - range, - buffer_range: Some((offset..hunk_range.end).to_point(&buffer)), - status: Some(DiffHunkStatus::added(hunk.secondary_status)), - excerpt_id: Some(excerpt.id), - }; - offset = hunk_range.end; - if is_filtered { - filtered_regions.push(region); - } else { - regions.push(region); - } - } - } - - // Add the buffer text for the rest of the excerpt. - let len = text.len(); - text.extend(buffer.text_for_range(offset..buffer_range.end)); - text.push('\n'); - regions.push(ReferenceRegion { - buffer_id: Some(buffer.remote_id()), - range: len..text.len(), - buffer_range: Some((offset..buffer_range.end).to_point(&buffer)), - status: None, - excerpt_id: Some(excerpt.id), - }); - } - - // Remove final trailing newline. - if self.excerpts.is_empty() { - regions.push(ReferenceRegion { - buffer_id: None, - range: 0..1, - buffer_range: Some(Point::new(0, 0)..Point::new(0, 1)), - status: None, - excerpt_id: None, - }); - } else { - text.pop(); - } - - // Retrieve the row info using the region that contains - // the start of each multi-buffer line. - let mut ix = 0; - let row_infos = text - .split('\n') - .map(|line| { - let row_info = regions - .iter() - .position(|region| region.range.contains(&ix)) - .map_or(RowInfo::default(), |region_ix| { - let region = ®ions[region_ix]; - let buffer_row = region.buffer_range.as_ref().map(|buffer_range| { - buffer_range.start.row - + text[region.range.start..ix].matches('\n').count() as u32 - }); - let main_buffer = self - .excerpts - .iter() - .find(|e| e.id == region.excerpt_id.unwrap()) - .map(|e| e.buffer.clone()); - let base_text_row = match region.status { - None => Some( - main_buffer - .as_ref() - .map(|main_buffer| { - let diff = self - .diffs - .get(&main_buffer.read(cx).remote_id()) - .unwrap(); - let buffer_row = buffer_row.unwrap(); - BaseTextRow( - diff.read(cx).snapshot(cx).row_to_base_text_row( - buffer_row, - &main_buffer.read(cx).snapshot(), - ), - ) - }) - .unwrap_or_default(), - ), - Some(DiffHunkStatus { - kind: DiffHunkStatusKind::Added, - .. - }) => None, - Some(DiffHunkStatus { - kind: DiffHunkStatusKind::Deleted, - .. - }) => Some(BaseTextRow(buffer_row.unwrap())), - Some(DiffHunkStatus { - kind: DiffHunkStatusKind::Modified, - .. - }) => unreachable!(), - }; - let is_excerpt_start = region_ix == 0 - || ®ions[region_ix - 1].excerpt_id != ®ion.excerpt_id - || regions[region_ix - 1].range.is_empty(); - let mut is_excerpt_end = region_ix == regions.len() - 1 - || ®ions[region_ix + 1].excerpt_id != ®ion.excerpt_id; - let is_start = !text[region.range.start..ix].contains('\n'); - let mut is_end = if region.range.end > text.len() { - !text[ix..].contains('\n') - } else { - text[ix..region.range.end.min(text.len())] - .matches('\n') - .count() - == 1 - }; - if region_ix < regions.len() - 1 - && !text[ix..].contains("\n") - && region.status == Some(DiffHunkStatus::added_none()) - && regions[region_ix + 1].excerpt_id == region.excerpt_id - && regions[region_ix + 1].range.start == text.len() - { - is_end = true; - is_excerpt_end = true; - } - let multibuffer_row = - MultiBufferRow(text[..ix].matches('\n').count() as u32); - let mut expand_direction = None; - if let Some(buffer) = &main_buffer { - let buffer_row = buffer_row.unwrap(); - let needs_expand_up = is_excerpt_start && is_start && buffer_row > 0; - let needs_expand_down = is_excerpt_end - && is_end - && buffer.read(cx).max_point().row > buffer_row; - expand_direction = 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 - }; - } - RowInfo { - buffer_id: region.buffer_id, - diff_status: region.status, - buffer_row, - base_text_row, - wrapped_buffer_row: None, - - multibuffer_row: Some(multibuffer_row), - expand_info: expand_direction.zip(region.excerpt_id).map( - |(direction, excerpt_id)| ExpandInfo { - direction, - excerpt_id, - }, - ), - } - }); - ix += line.len() + 1; - row_info - }) - .collect(); - - (text, row_infos, excerpt_boundary_rows) - } - - fn diffs_updated(&mut self, cx: &App) { - for excerpt in &mut self.excerpts { - let buffer = excerpt.buffer.read(cx).snapshot(); - let excerpt_range = excerpt.range.to_offset(&buffer); - let buffer_id = buffer.remote_id(); - let diff = self.diffs.get(&buffer_id).unwrap().read(cx); - let mut hunks = diff.hunks_in_row_range(0..u32::MAX, &buffer, cx).peekable(); - excerpt.expanded_diff_hunks.retain(|hunk_anchor| { - if !hunk_anchor.is_valid(&buffer) { - return false; - } - while let Some(hunk) = hunks.peek() { - match hunk.buffer_range.start.cmp(hunk_anchor, &buffer) { - cmp::Ordering::Less => { - hunks.next(); - } - cmp::Ordering::Equal => { - let hunk_range = hunk.buffer_range.to_offset(&buffer); - return hunk_range.end >= excerpt_range.start - && hunk_range.start <= excerpt_range.end; - } - cmp::Ordering::Greater => break, - } - } - false - }); - } - } - - fn add_diff(&mut self, diff: Entity, cx: &mut App) { - let buffer_id = diff.read(cx).buffer_id; - self.diffs.insert(buffer_id, diff); - } -} - -#[gpui::test(iterations = 100)] -async fn test_random_set_ranges(cx: &mut TestAppContext, mut rng: StdRng) { - let base_text = "a\n".repeat(100); - let buf = cx.update(|cx| cx.new(|cx| Buffer::local(base_text, cx))); - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - fn row_ranges(ranges: &Vec>) -> Vec> { - ranges - .iter() - .map(|range| range.start.row..range.end.row) - .collect() - } - - for _ in 0..operations { - let snapshot = buf.update(cx, |buf, _| buf.snapshot()); - let num_ranges = rng.random_range(0..=10); - let max_row = snapshot.max_point().row; - let mut ranges = (0..num_ranges) - .map(|_| { - let start = rng.random_range(0..max_row); - let end = rng.random_range(start + 1..max_row + 1); - Point::row_range(start..end) - }) - .collect::>(); - ranges.sort_by_key(|range| range.start); - log::info!("Setting ranges: {:?}", row_ranges(&ranges)); - let (created, _) = multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - PathKey::for_buffer(&buf, cx), - buf.clone(), - ranges.clone(), - 2, - cx, - ) - }); - - assert_eq!(created.len(), ranges.len()); - - let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - let mut last_end = None; - 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); - seen_ranges.push(start..end); - - if let Some(last_end) = last_end.take() { - assert!( - start > last_end, - "multibuffer has out-of-order ranges: {:?}; {:?} <= {:?}", - row_ranges(&seen_ranges), - start, - last_end - ) - } - - ranges.retain(|range| range.start < start || range.end > end); - - last_end = Some(end) - } - - assert!( - ranges.is_empty(), - "multibuffer {:?} did not include all ranges: {:?}", - row_ranges(&seen_ranges), - row_ranges(&ranges) - ); - } -} - -// TODO(split-diff) bump up iterations -// #[gpui::test(iterations = 100)] -#[gpui::test] -async fn test_random_filtered_multibuffer(cx: &mut TestAppContext, rng: StdRng) { - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); - multibuffer.set_all_diff_hunks_expanded(cx); - multibuffer.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions)); - multibuffer - }); - let follower = multibuffer.update(cx, |multibuffer, cx| multibuffer.get_or_create_follower(cx)); - follower.update(cx, |follower, _| { - assert!(follower.all_diff_hunks_expanded()); - follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions)); - }); - test_random_multibuffer_impl(multibuffer, cx, rng).await; -} - -#[gpui::test(iterations = 100)] -async fn test_random_multibuffer(cx: &mut TestAppContext, rng: StdRng) { - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - test_random_multibuffer_impl(multibuffer, cx, rng).await; -} - -async fn test_random_multibuffer_impl( - multibuffer: Entity, - cx: &mut TestAppContext, - mut rng: StdRng, -) { - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - multibuffer.read_with(cx, |multibuffer, _| assert!(multibuffer.is_empty())); - let all_diff_hunks_expanded = - multibuffer.read_with(cx, |multibuffer, _| multibuffer.all_diff_hunks_expanded()); - let mut buffers: Vec> = Vec::new(); - let mut base_texts: HashMap = HashMap::default(); - let mut reference = ReferenceMultibuffer::default(); - let mut anchors = Vec::new(); - let mut old_versions = Vec::new(); - let mut old_follower_versions = Vec::new(); - let mut needs_diff_calculation = false; - - for _ in 0..operations { - match rng.random_range(0..100) { - 0..=14 if !buffers.is_empty() => { - let buffer = buffers.choose(&mut rng).unwrap(); - buffer.update(cx, |buf, cx| { - let edit_count = rng.random_range(1..5); - buf.randomly_edit(&mut rng, edit_count, cx); - log::info!("buffer text:\n{}", buf.text()); - needs_diff_calculation = true; - }); - cx.update(|cx| reference.diffs_updated(cx)); - } - 15..=19 if !reference.excerpts.is_empty() => { - multibuffer.update(cx, |multibuffer, cx| { - let ids = multibuffer.excerpt_ids(); - let mut excerpts = HashSet::default(); - for _ in 0..rng.random_range(0..ids.len()) { - excerpts.extend(ids.choose(&mut rng).copied()); - } - - let line_count = rng.random_range(0..5); - - let excerpt_ixs = excerpts - .iter() - .map(|id| reference.excerpts.iter().position(|e| e.id == *id).unwrap()) - .collect::>(); - log::info!("Expanding excerpts {excerpt_ixs:?} by {line_count} lines"); - multibuffer.expand_excerpts( - excerpts.iter().cloned(), - line_count, - ExpandExcerptDirection::UpAndDown, - cx, - ); - - reference.expand_excerpts(&excerpts, line_count, cx); - }); - } - 20..=29 if !reference.excerpts.is_empty() => { - let mut ids_to_remove = vec![]; - for _ in 0..rng.random_range(1..=3) { - let Some(excerpt) = reference.excerpts.choose(&mut rng) else { - break; - }; - let id = excerpt.id; - cx.update(|cx| reference.remove_excerpt(id, cx)); - ids_to_remove.push(id); - } - let snapshot = - multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - ids_to_remove.sort_unstable_by(|a, b| a.cmp(b, &snapshot)); - drop(snapshot); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts(ids_to_remove, cx) - }); - } - 30..=39 if !reference.excerpts.is_empty() => { - let multibuffer = - multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - let offset = multibuffer.clip_offset( - MultiBufferOffset(rng.random_range(0..=multibuffer.len().0)), - Bias::Left, - ); - let bias = if rng.random() { - Bias::Left - } else { - Bias::Right - }; - log::info!("Creating anchor at {} with bias {:?}", offset.0, bias); - anchors.push(multibuffer.anchor_at(offset, bias)); - anchors.sort_by(|a, b| a.cmp(b, &multibuffer)); - } - 40..=44 if !anchors.is_empty() => { - let multibuffer = - multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - let prev_len = anchors.len(); - anchors = multibuffer - .refresh_anchors(&anchors) - .into_iter() - .map(|a| a.1) - .collect(); - - // Ensure the newly-refreshed anchors point to a valid excerpt and don't - // overshoot its boundaries. - assert_eq!(anchors.len(), prev_len); - for anchor in &anchors { - if anchor.excerpt_id == ExcerptId::min() - || anchor.excerpt_id == ExcerptId::max() - { - continue; - } - - let excerpt = multibuffer.excerpt(anchor.excerpt_id).unwrap(); - assert_eq!(excerpt.id, anchor.excerpt_id); - assert!(excerpt.contains(anchor)); - } - } - 45..=55 if !reference.excerpts.is_empty() && !all_diff_hunks_expanded => { - multibuffer.update(cx, |multibuffer, cx| { - let snapshot = multibuffer.snapshot(cx); - let excerpt_ix = rng.random_range(0..reference.excerpts.len()); - let excerpt = &reference.excerpts[excerpt_ix]; - let start = excerpt.range.start; - let end = excerpt.range.end; - let range = snapshot.anchor_in_excerpt(excerpt.id, start).unwrap() - ..snapshot.anchor_in_excerpt(excerpt.id, end).unwrap(); - - log::info!( - "expanding diff hunks in range {:?} (excerpt id {:?}, index {excerpt_ix:?}, buffer id {:?})", - range.to_offset(&snapshot), - excerpt.id, - excerpt.buffer.read(cx).remote_id(), - ); - reference.expand_diff_hunks(excerpt.id, start..end, cx); - multibuffer.expand_diff_hunks(vec![range], cx); - }); - } - 56..=85 if needs_diff_calculation => { - multibuffer.update(cx, |multibuffer, cx| { - for buffer in multibuffer.all_buffers() { - let snapshot = buffer.read(cx).snapshot(); - multibuffer.diff_for(snapshot.remote_id()).unwrap().update( - cx, - |diff, cx| { - log::info!( - "recalculating diff for buffer {:?}", - snapshot.remote_id(), - ); - diff.recalculate_diff_sync(snapshot.text, cx); - }, - ); - } - reference.diffs_updated(cx); - needs_diff_calculation = false; - }); - } - _ => { - let buffer_handle = if buffers.is_empty() || rng.random_bool(0.4) { - let mut base_text = util::RandomCharIter::new(&mut rng) - .take(256) - .collect::(); - - let buffer = cx.new(|cx| Buffer::local(base_text.clone(), cx)); - text::LineEnding::normalize(&mut base_text); - base_texts.insert( - buffer.read_with(cx, |buffer, _| buffer.remote_id()), - base_text, - ); - buffers.push(buffer); - buffers.last().unwrap() - } else { - buffers.choose(&mut rng).unwrap() - }; - - let prev_excerpt_ix = rng.random_range(0..=reference.excerpts.len()); - let prev_excerpt_id = reference - .excerpts - .get(prev_excerpt_ix) - .map_or(ExcerptId::max(), |e| e.id); - let excerpt_ix = (prev_excerpt_ix + 1).min(reference.excerpts.len()); - - let (range, anchor_range) = buffer_handle.read_with(cx, |buffer, _| { - let end_row = rng.random_range(0..=buffer.max_point().row); - let start_row = rng.random_range(0..=end_row); - let end_ix = buffer.point_to_offset(Point::new(end_row, 0)); - let start_ix = buffer.point_to_offset(Point::new(start_row, 0)); - let anchor_range = buffer.anchor_before(start_ix)..buffer.anchor_after(end_ix); - - log::info!( - "Inserting excerpt at {} of {} for buffer {}: {:?}[{:?}] = {:?}", - excerpt_ix, - reference.excerpts.len(), - buffer.remote_id(), - buffer.text(), - start_ix..end_ix, - &buffer.text()[start_ix..end_ix] - ); - - (start_ix..end_ix, anchor_range) - }); - - let excerpt_id = multibuffer.update(cx, |multibuffer, cx| { - multibuffer - .insert_excerpts_after( - prev_excerpt_id, - buffer_handle.clone(), - [ExcerptRange::new(range.clone())], - cx, - ) - .pop() - .unwrap() - }); - - reference.insert_excerpt_after( - prev_excerpt_id, - excerpt_id, - (buffer_handle.clone(), anchor_range), - ); - - multibuffer.update(cx, |multibuffer, cx| { - 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)); - reference.add_diff(diff.clone(), cx); - multibuffer.add_diff(diff, cx) - } - }); - } - } - - if rng.random_bool(0.3) { - multibuffer.update(cx, |multibuffer, cx| { - old_versions.push((multibuffer.snapshot(cx), multibuffer.subscribe())); - - if let Some(follower) = &multibuffer.follower { - follower.update(cx, |follower, cx| { - old_follower_versions.push((follower.snapshot(cx), follower.subscribe())); - }) - } - }) - } - - multibuffer.read_with(cx, |multibuffer, cx| { - check_multibuffer(multibuffer, &reference, &anchors, cx, &mut rng); - - if let Some(follower) = &multibuffer.follower { - check_multibuffer(follower.read(cx), &reference, &anchors, cx, &mut rng); - } - }); - } - - let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - for (old_snapshot, subscription) in old_versions { - check_multibuffer_edits(&snapshot, &old_snapshot, subscription); - } - if let Some(follower) = multibuffer.read_with(cx, |multibuffer, _| multibuffer.follower.clone()) - { - let snapshot = follower.read_with(cx, |follower, cx| follower.snapshot(cx)); - for (old_snapshot, subscription) in old_follower_versions { - check_multibuffer_edits(&snapshot, &old_snapshot, subscription); - } - } -} - -fn check_multibuffer( - multibuffer: &MultiBuffer, - reference: &ReferenceMultibuffer, - anchors: &[Anchor], - cx: &App, - rng: &mut StdRng, -) { - let snapshot = multibuffer.snapshot(cx); - let filter_mode = multibuffer.filter_mode; - assert!(filter_mode.is_some() == snapshot.all_diff_hunks_expanded); - let actual_text = snapshot.text(); - let actual_boundary_rows = snapshot - .excerpt_boundaries_in_range(MultiBufferOffset(0)..) - .map(|b| b.row) - .collect::>(); - let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::>(); - - let (expected_text, expected_row_infos, expected_boundary_rows) = - reference.expected_content(filter_mode, snapshot.all_diff_hunks_expanded, cx); - - let (unfiltered_text, unfiltered_row_infos, unfiltered_boundary_rows) = - reference.expected_content(None, snapshot.all_diff_hunks_expanded, cx); - - let has_diff = actual_row_infos - .iter() - .any(|info| info.diff_status.is_some()) - || unfiltered_row_infos - .iter() - .any(|info| info.diff_status.is_some()); - let actual_diff = format_diff( - &actual_text, - &actual_row_infos, - &actual_boundary_rows, - Some(has_diff), - ); - let expected_diff = format_diff( - &expected_text, - &expected_row_infos, - &expected_boundary_rows, - Some(has_diff), - ); - - log::info!("Multibuffer content:\n{}", actual_diff); - if filter_mode.is_some() { - log::info!( - "Unfiltered multibuffer content:\n{}", - format_diff( - &unfiltered_text, - &unfiltered_row_infos, - &unfiltered_boundary_rows, - None, - ), - ); - } - - assert_eq!( - actual_row_infos.len(), - actual_text.split('\n').count(), - "line count: {}", - actual_text.split('\n').count() - ); - pretty_assertions::assert_eq!(actual_diff, expected_diff); - pretty_assertions::assert_eq!(actual_text, expected_text); - pretty_assertions::assert_eq!(actual_row_infos, expected_row_infos); - - for _ in 0..5 { - let start_row = rng.random_range(0..=expected_row_infos.len()); - assert_eq!( - snapshot - .row_infos(MultiBufferRow(start_row as u32)) - .collect::>(), - &expected_row_infos[start_row..], - "buffer_rows({})", - start_row - ); - } - - assert_eq!( - snapshot.widest_line_number(), - expected_row_infos - .into_iter() - .filter_map(|info| { - if info.diff_status.is_some_and(|status| status.is_deleted()) { - None - } else { - info.buffer_row - } - }) - .max() - .unwrap() - + 1 - ); - let reference_ranges = reference - .excerpts - .iter() - .map(|excerpt| { - ( - excerpt.id, - excerpt.range.to_offset(&excerpt.buffer.read(cx).snapshot()), - ) - }) - .collect::>(); - for i in 0..snapshot.len().0 { - let excerpt = snapshot - .excerpt_containing(MultiBufferOffset(i)..MultiBufferOffset(i)) - .unwrap(); - assert_eq!( - excerpt.buffer_range().start.0..excerpt.buffer_range().end.0, - reference_ranges[&excerpt.id()] - ); - } - - assert_consistent_line_numbers(&snapshot); - assert_position_translation(&snapshot); - - for (row, line) in expected_text.split('\n').enumerate() { - assert_eq!( - snapshot.line_len(MultiBufferRow(row as u32)), - line.len() as u32, - "line_len({}).", - row - ); - } - - let text_rope = Rope::from(expected_text.as_str()); - for _ in 0..10 { - let end_ix = text_rope.clip_offset(rng.random_range(0..=text_rope.len()), Bias::Right); - let start_ix = text_rope.clip_offset(rng.random_range(0..=end_ix), Bias::Left); - - let text_for_range = snapshot - .text_for_range(MultiBufferOffset(start_ix)..MultiBufferOffset(end_ix)) - .collect::(); - assert_eq!( - text_for_range, - &expected_text[start_ix..end_ix], - "incorrect text for range {:?}", - start_ix..end_ix - ); - - let expected_summary = - MBTextSummary::from(TextSummary::from(&expected_text[start_ix..end_ix])); - assert_eq!( - snapshot.text_summary_for_range::( - MultiBufferOffset(start_ix)..MultiBufferOffset(end_ix) - ), - expected_summary, - "incorrect summary for range {:?}", - start_ix..end_ix - ); - } - - // Anchor resolution - let summaries = snapshot.summaries_for_anchors::(anchors); - assert_eq!(anchors.len(), summaries.len()); - for (anchor, resolved_offset) in anchors.iter().zip(summaries) { - assert!(resolved_offset <= snapshot.len()); - assert_eq!( - snapshot.summary_for_anchor::(anchor), - resolved_offset, - "anchor: {:?}", - anchor - ); - } - - for _ in 0..10 { - let end_ix = text_rope.clip_offset(rng.random_range(0..=text_rope.len()), Bias::Right); - assert_eq!( - snapshot - .reversed_chars_at(MultiBufferOffset(end_ix)) - .collect::(), - expected_text[..end_ix].chars().rev().collect::(), - ); - } - - for _ in 0..10 { - let end_ix = rng.random_range(0..=text_rope.len()); - let end_ix = text_rope.floor_char_boundary(end_ix); - let start_ix = rng.random_range(0..=end_ix); - let start_ix = text_rope.floor_char_boundary(start_ix); - assert_eq!( - snapshot - .bytes_in_range(MultiBufferOffset(start_ix)..MultiBufferOffset(end_ix)) - .flatten() - .copied() - .collect::>(), - expected_text.as_bytes()[start_ix..end_ix].to_vec(), - "bytes_in_range({:?})", - start_ix..end_ix, - ); - } -} - -fn check_multibuffer_edits( - snapshot: &MultiBufferSnapshot, - old_snapshot: &MultiBufferSnapshot, - subscription: Subscription, -) { - let edits = subscription.consume().into_inner(); - - log::info!( - "applying subscription edits to old text: {:?}: {:#?}", - old_snapshot.text(), - edits, - ); - - let mut text = old_snapshot.text(); - for edit in edits { - let new_text: String = snapshot - .text_for_range(edit.new.start..edit.new.end) - .collect(); - text.replace_range( - (edit.new.start.0..edit.new.start.0 + (edit.old.end.0 - edit.old.start.0)).clone(), - &new_text, - ); - pretty_assertions::assert_eq!( - &text[0..edit.new.end.0], - snapshot - .text_for_range(MultiBufferOffset(0)..edit.new.end) - .collect::() - ); - } - pretty_assertions::assert_eq!(text, snapshot.text()); -} - -#[gpui::test] -fn test_history(cx: &mut App) { - let test_settings = SettingsStore::test(cx); - cx.set_global(test_settings); - - let group_interval: Duration = Duration::from_millis(1); - let buffer_1 = cx.new(|cx| { - let mut buf = Buffer::local("1234", cx); - buf.set_group_interval(group_interval); - buf - }); - let buffer_2 = cx.new(|cx| { - let mut buf = Buffer::local("5678", cx); - buf.set_group_interval(group_interval); - buf - }); - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - multibuffer.update(cx, |this, _| { - this.set_group_interval(group_interval); - }); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange::new(0..buffer_1.read(cx).len())], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange::new(0..buffer_2.read(cx).len())], - cx, - ); - }); - - let mut now = Instant::now(); - - multibuffer.update(cx, |multibuffer, cx| { - let transaction_1 = multibuffer.start_transaction_at(now, cx).unwrap(); - multibuffer.edit( - [ - (Point::new(0, 0)..Point::new(0, 0), "A"), - (Point::new(1, 0)..Point::new(1, 0), "A"), - ], - None, - cx, - ); - multibuffer.edit( - [ - (Point::new(0, 1)..Point::new(0, 1), "B"), - (Point::new(1, 1)..Point::new(1, 1), "B"), - ], - None, - cx, - ); - multibuffer.end_transaction_at(now, cx); - assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); - - // Verify edited ranges for transaction 1 - assert_eq!( - multibuffer.edited_ranges_for_transaction(transaction_1, cx), - &[ - Point::new(0, 0)..Point::new(0, 2), - Point::new(1, 0)..Point::new(1, 2) - ] - ); - - // Edit buffer 1 through the multibuffer - now += 2 * group_interval; - multibuffer.start_transaction_at(now, cx); - multibuffer.edit( - [(MultiBufferOffset(2)..MultiBufferOffset(2), "C")], - None, - cx, - ); - multibuffer.end_transaction_at(now, cx); - assert_eq!(multibuffer.read(cx).text(), "ABC1234\nAB5678"); - - // Edit buffer 1 independently - buffer_1.update(cx, |buffer_1, cx| { - buffer_1.start_transaction_at(now); - buffer_1.edit([(3..3, "D")], None, cx); - buffer_1.end_transaction_at(now, cx); - - now += 2 * group_interval; - buffer_1.start_transaction_at(now); - buffer_1.edit([(4..4, "E")], None, cx); - buffer_1.end_transaction_at(now, cx); - }); - assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678"); - - // An undo in the multibuffer undoes the multibuffer transaction - // and also any individual buffer edits that have occurred since - // that transaction. - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); - - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); - - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); - - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678"); - - // Undo buffer 2 independently. - buffer_2.update(cx, |buffer_2, cx| buffer_2.undo(cx)); - assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\n5678"); - - // An undo in the multibuffer undoes the components of the - // the last multibuffer transaction that are not already undone. - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "AB1234\n5678"); - - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); - - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); - - buffer_1.update(cx, |buffer_1, cx| buffer_1.redo(cx)); - assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678"); - - // Redo stack gets cleared after an edit. - now += 2 * group_interval; - multibuffer.start_transaction_at(now, cx); - multibuffer.edit( - [(MultiBufferOffset(0)..MultiBufferOffset(0), "X")], - None, - cx, - ); - multibuffer.end_transaction_at(now, cx); - assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678"); - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); - - // Transactions can be grouped manually. - multibuffer.redo(cx); - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); - multibuffer.group_until_transaction(transaction_1, cx); - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); - }); -} - -#[gpui::test] -async fn test_enclosing_indent(cx: &mut TestAppContext) { - async fn enclosing_indent( - text: &str, - buffer_row: u32, - cx: &mut TestAppContext, - ) -> Option<(Range, LineIndent)> { - let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx)); - let snapshot = cx.read(|cx| buffer.read(cx).snapshot(cx)); - let (range, indent) = snapshot - .enclosing_indent(MultiBufferRow(buffer_row)) - .await?; - Some((range.start.0..range.end.0, indent)) - } - - assert_eq!( - enclosing_indent( - indoc!( - " - fn b() { - if c { - let d = 2; - } - } - " - ), - 1, - cx, - ) - .await, - Some(( - 1..2, - LineIndent { - tabs: 0, - spaces: 4, - line_blank: false, - } - )) - ); - - assert_eq!( - enclosing_indent( - indoc!( - " - fn b() { - if c { - let d = 2; - } - } - " - ), - 2, - cx, - ) - .await, - Some(( - 1..2, - LineIndent { - tabs: 0, - spaces: 4, - line_blank: false, - } - )) - ); - - assert_eq!( - enclosing_indent( - indoc!( - " - fn b() { - if c { - let d = 2; - - let e = 5; - } - } - " - ), - 3, - cx, - ) - .await, - Some(( - 1..4, - LineIndent { - tabs: 0, - spaces: 4, - line_blank: false, - } - )) - ); -} - -#[gpui::test] -async fn test_summaries_for_anchors(cx: &mut TestAppContext) { - let base_text_1 = indoc!( - " - bar - " - ); - let text_1 = indoc!( - " - BAR - " - ); - let base_text_2 = indoc!( - " - foo - " - ); - let text_2 = indoc!( - " - FOO - " - ); - - let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx)); - let buffer_2 = cx.new(|cx| Buffer::local(text_2, cx)); - let diff_1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text_1, &buffer_1, cx)); - let diff_2 = cx.new(|cx| BufferDiff::new_with_base_text(base_text_2, &buffer_2, cx)); - cx.run_until_parked(); - - let mut ids = vec![]; - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); - multibuffer.set_all_diff_hunks_expanded(cx); - ids.extend(multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)], - cx, - )); - ids.extend(multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)], - cx, - )); - multibuffer.add_diff(diff_1.clone(), cx); - multibuffer.add_diff(diff_2.clone(), cx); - multibuffer - }); - - let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { - (multibuffer.snapshot(cx), multibuffer.subscribe()) - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - - bar - + BAR - - - foo - + FOO - " - ), - ); - - let anchor_1 = Anchor::in_buffer(ids[0], text::Anchor::MIN); - let point_1 = snapshot.summaries_for_anchors::([&anchor_1])[0]; - assert_eq!(point_1, Point::new(0, 0)); - - let anchor_2 = Anchor::in_buffer(ids[1], text::Anchor::MIN); - let point_2 = snapshot.summaries_for_anchors::([&anchor_2])[0]; - assert_eq!(point_2, Point::new(3, 0)); -} - -#[gpui::test] -async fn test_trailing_deletion_without_newline(cx: &mut TestAppContext) { - let base_text_1 = "one\ntwo".to_owned(); - let text_1 = "one\n".to_owned(); - - let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx)); - let diff_1 = cx.new(|cx| BufferDiff::new_with_base_text(&base_text_1, &buffer_1, cx)); - cx.run_until_parked(); - - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::singleton(buffer_1.clone(), cx); - multibuffer.add_diff(diff_1.clone(), cx); - multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); - multibuffer - }); - - let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { - (multibuffer.snapshot(cx), multibuffer.subscribe()) - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - one - - two - " - ), - ); - - assert_eq!(snapshot.max_point(), Point::new(2, 0)); - assert_eq!(snapshot.len().0, 8); - - assert_eq!( - snapshot - .dimensions_from_points::([Point::new(2, 0)]) - .collect::>(), - vec![Point::new(2, 0)] - ); - - let (_, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap(); - assert_eq!(translated_offset.0, "one\n".len()); - let (_, translated_point, _) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap(); - assert_eq!(translated_point, Point::new(1, 0)); - - // The same, for an excerpt that's not at the end of the multibuffer. - - let text_2 = "foo\n".to_owned(); - let buffer_2 = cx.new(|cx| Buffer::local(&text_2, cx)); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))], - cx, - ); - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - one - - two - - foo - " - ), - ); - - assert_eq!( - snapshot - .dimensions_from_points::([Point::new(2, 0)]) - .collect::>(), - vec![Point::new(2, 0)] - ); - - let buffer_1_id = buffer_1.read_with(cx, |buffer_1, _| buffer_1.remote_id()); - let (buffer, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap(); - assert_eq!(buffer.remote_id(), buffer_1_id); - assert_eq!(translated_offset.0, "one\n".len()); - let (buffer, translated_point, _) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap(); - assert_eq!(buffer.remote_id(), buffer_1_id); - assert_eq!(translated_point, Point::new(1, 0)); -} - -fn format_diff( - text: &str, - row_infos: &Vec, - boundary_rows: &HashSet, - has_diff: Option, -) -> String { - let has_diff = - has_diff.unwrap_or_else(|| row_infos.iter().any(|info| info.diff_status.is_some())); - text.split('\n') - .enumerate() - .zip(row_infos) - .map(|((ix, line), info)| { - let marker = match info.diff_status.map(|status| status.kind) { - Some(DiffHunkStatusKind::Added) => "+ ", - Some(DiffHunkStatusKind::Deleted) => "- ", - Some(DiffHunkStatusKind::Modified) => unreachable!(), - None => { - if has_diff && !line.is_empty() { - " " - } else { - "" - } - } - }; - let boundary_row = if boundary_rows.contains(&MultiBufferRow(ix as u32)) { - if has_diff { - " ----------\n" - } else { - "---------\n" - } - } else { - "" - }; - let expand = info - .expand_info - .map(|expand_info| match expand_info.direction { - ExpandExcerptDirection::Up => " [↑]", - ExpandExcerptDirection::Down => " [↓]", - ExpandExcerptDirection::UpAndDown => " [↕]", - }) - .unwrap_or_default(); - - format!("{boundary_row}{marker}{line}{expand}") - // let mbr = info - // .multibuffer_row - // .map(|row| format!("{:0>3}", row.0)) - // .unwrap_or_else(|| "???".to_string()); - // let byte_range = format!("{byte_range_start:0>3}..{byte_range_end:0>3}"); - // format!("{boundary_row}Row: {mbr}, Bytes: {byte_range} | {marker}{line}{expand}") - }) - .collect::>() - .join("\n") -} - -// fn format_transforms(snapshot: &MultiBufferSnapshot) -> String { -// snapshot -// .diff_transforms -// .iter() -// .map(|transform| { -// let (kind, summary) = match transform { -// DiffTransform::DeletedHunk { summary, .. } => (" Deleted", (*summary).into()), -// DiffTransform::FilteredInsertedHunk { summary, .. } => (" Filtered", *summary), -// DiffTransform::InsertedHunk { summary, .. } => (" Inserted", *summary), -// DiffTransform::Unmodified { summary, .. } => ("Unmodified", *summary), -// }; -// format!("{kind}(len: {}, lines: {:?})", summary.len, summary.lines) -// }) -// .join("\n") -// } - -// fn format_excerpts(snapshot: &MultiBufferSnapshot) -> String { -// snapshot -// .excerpts -// .iter() -// .map(|excerpt| { -// format!( -// "Excerpt(buffer_range = {:?}, lines = {:?}, has_trailing_newline = {:?})", -// excerpt.range.context.to_point(&excerpt.buffer), -// excerpt.text_summary.lines, -// excerpt.has_trailing_newline -// ) -// }) -// .join("\n") -// } - -#[gpui::test] -async fn test_basic_filtering(cx: &mut TestAppContext) { - let text = indoc!( - " - ZERO - one - TWO - three - six - " - ); - let base_text = indoc!( - " - one - two - three - four - five - six - " - ); - - let buffer = cx.new(|cx| Buffer::local(text, cx)); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); - cx.run_until_parked(); - - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx); - multibuffer.add_diff(diff.clone(), cx); - multibuffer.set_all_diff_hunks_expanded(cx); - multibuffer.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions)); - multibuffer - }); - - let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { - (multibuffer.snapshot(cx), multibuffer.subscribe()) - }); - - assert_eq!(snapshot.text(), base_text); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - one - - two - three - - four - - five - six - " - ), - ); - - buffer.update(cx, |buffer, cx| { - buffer.edit_via_marked_text( - indoc!( - " - ZERO - one - «»W«O - T»hree - six - " - ), - None, - cx, - ); - }); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc! { - " - one - - two - - four - - five - six - " - }, - ); -} - -#[gpui::test] -async fn test_base_text_line_numbers(cx: &mut TestAppContext) { - let base_text = indoc! {" - one - two - three - four - five - six - "}; - let buffer_text = indoc! {" - two - THREE - five - six - SEVEN - "}; - let multibuffer = cx.update(|cx| MultiBuffer::build_simple(buffer_text, cx)); - multibuffer.update(cx, |multibuffer, cx| { - let buffer = multibuffer.all_buffers().into_iter().next().unwrap(); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); - multibuffer.set_all_diff_hunks_expanded(cx); - multibuffer.add_diff(diff, cx); - }); - let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { - (multibuffer.snapshot(cx), multibuffer.subscribe()) - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc! {" - - one - two - - three - - four - + THREE - five - six - + SEVEN - "}, - ); - let base_text_rows = snapshot - .row_infos(MultiBufferRow(0)) - .map(|row_info| row_info.base_text_row) - .collect::>(); - pretty_assertions::assert_eq!( - base_text_rows, - vec![ - Some(BaseTextRow(0)), - Some(BaseTextRow(1)), - Some(BaseTextRow(2)), - Some(BaseTextRow(3)), - None, - Some(BaseTextRow(4)), - Some(BaseTextRow(5)), - None, - Some(BaseTextRow(6)), - ] - ) -} - -#[track_caller] -fn assert_excerpts_match( - multibuffer: &Entity, - cx: &mut TestAppContext, - expected: &str, -) { - let mut output = String::new(); - multibuffer.read_with(cx, |multibuffer, cx| { - for (_, buffer, range) in multibuffer.snapshot(cx).excerpts() { - output.push_str("-----\n"); - output.extend(buffer.text_for_range(range.context)); - if !output.ends_with('\n') { - output.push('\n'); - } - } - }); - assert_eq!(output, expected); -} - -#[track_caller] -fn assert_new_snapshot( - multibuffer: &Entity, - snapshot: &mut MultiBufferSnapshot, - subscription: &mut Subscription, - cx: &mut TestAppContext, - expected_diff: &str, -) { - let new_snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - let actual_text = new_snapshot.text(); - let line_infos = new_snapshot - .row_infos(MultiBufferRow(0)) - .collect::>(); - let actual_diff = format_diff(&actual_text, &line_infos, &Default::default(), None); - pretty_assertions::assert_eq!(actual_diff, expected_diff); - check_edits( - snapshot, - &new_snapshot, - &subscription.consume().into_inner(), - ); - *snapshot = new_snapshot; -} - -#[track_caller] -fn check_edits( - old_snapshot: &MultiBufferSnapshot, - new_snapshot: &MultiBufferSnapshot, - edits: &[Edit], -) { - let mut text = old_snapshot.text(); - let new_text = new_snapshot.text(); - for edit in edits.iter().rev() { - if !text.is_char_boundary(edit.old.start.0) - || !text.is_char_boundary(edit.old.end.0) - || !new_text.is_char_boundary(edit.new.start.0) - || !new_text.is_char_boundary(edit.new.end.0) - { - panic!( - "invalid edits: {:?}\nold text: {:?}\nnew text: {:?}", - edits, text, new_text - ); - } - - text.replace_range( - edit.old.start.0..edit.old.end.0, - &new_text[edit.new.start.0..edit.new.end.0], - ); - } - - pretty_assertions::assert_eq!(text, new_text, "invalid edits: {:?}", edits); -} - -#[track_caller] -fn assert_chunks_in_ranges(snapshot: &MultiBufferSnapshot) { - let full_text = snapshot.text(); - for ix in 0..full_text.len() { - let mut chunks = snapshot.chunks(MultiBufferOffset(0)..snapshot.len(), false); - chunks.seek(MultiBufferOffset(ix)..snapshot.len()); - let tail = chunks.map(|chunk| chunk.text).collect::(); - assert_eq!(tail, &full_text[ix..], "seek to range: {:?}", ix..); - } -} - -#[track_caller] -fn assert_consistent_line_numbers(snapshot: &MultiBufferSnapshot) { - let all_line_numbers = snapshot.row_infos(MultiBufferRow(0)).collect::>(); - for start_row in 1..all_line_numbers.len() { - let line_numbers = snapshot - .row_infos(MultiBufferRow(start_row as u32)) - .collect::>(); - assert_eq!( - line_numbers, - all_line_numbers[start_row..], - "start_row: {start_row}" - ); - } -} - -#[track_caller] -fn assert_position_translation(snapshot: &MultiBufferSnapshot) { - let text = Rope::from(snapshot.text()); - - let mut left_anchors = Vec::new(); - let mut right_anchors = Vec::new(); - let mut offsets = Vec::new(); - let mut points = Vec::new(); - for offset in 0..=text.len() + 1 { - let offset = MultiBufferOffset(offset); - let clipped_left = snapshot.clip_offset(offset, Bias::Left); - let clipped_right = snapshot.clip_offset(offset, Bias::Right); - assert_eq!( - clipped_left.0, - text.clip_offset(offset.0, Bias::Left), - "clip_offset({offset:?}, Left)" - ); - assert_eq!( - clipped_right.0, - text.clip_offset(offset.0, Bias::Right), - "clip_offset({offset:?}, Right)" - ); - assert_eq!( - snapshot.offset_to_point(clipped_left), - text.offset_to_point(clipped_left.0), - "offset_to_point({})", - clipped_left.0 - ); - assert_eq!( - snapshot.offset_to_point(clipped_right), - text.offset_to_point(clipped_right.0), - "offset_to_point({})", - clipped_right.0 - ); - let anchor_after = snapshot.anchor_after(clipped_left); - assert_eq!( - anchor_after.to_offset(snapshot), - clipped_left, - "anchor_after({}).to_offset {anchor_after:?}", - clipped_left.0 - ); - let anchor_before = snapshot.anchor_before(clipped_left); - assert_eq!( - anchor_before.to_offset(snapshot), - clipped_left, - "anchor_before({}).to_offset", - clipped_left.0 - ); - left_anchors.push(anchor_before); - right_anchors.push(anchor_after); - offsets.push(clipped_left); - points.push(text.offset_to_point(clipped_left.0)); - } - - for row in 0..text.max_point().row { - for column in 0..text.line_len(row) + 1 { - let point = Point { row, column }; - let clipped_left = snapshot.clip_point(point, Bias::Left); - let clipped_right = snapshot.clip_point(point, Bias::Right); - assert_eq!( - clipped_left, - text.clip_point(point, Bias::Left), - "clip_point({point:?}, Left)" - ); - assert_eq!( - clipped_right, - text.clip_point(point, Bias::Right), - "clip_point({point:?}, Right)" - ); - assert_eq!( - snapshot.point_to_offset(clipped_left).0, - text.point_to_offset(clipped_left), - "point_to_offset({clipped_left:?})" - ); - assert_eq!( - snapshot.point_to_offset(clipped_right).0, - text.point_to_offset(clipped_right), - "point_to_offset({clipped_right:?})" - ); - } - } - - assert_eq!( - snapshot.summaries_for_anchors::(&left_anchors), - offsets, - "left_anchors <-> offsets" - ); - assert_eq!( - snapshot.summaries_for_anchors::(&left_anchors), - points, - "left_anchors <-> points" - ); - assert_eq!( - snapshot.summaries_for_anchors::(&right_anchors), - offsets, - "right_anchors <-> offsets" - ); - assert_eq!( - snapshot.summaries_for_anchors::(&right_anchors), - points, - "right_anchors <-> points" - ); - - 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 == MultiBufferOffset(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 let Some((buffer, offset)) = snapshot.point_to_buffer_offset(snapshot.max_point()) { - assert!(offset.0 <= buffer.len()); - } - if let Some((buffer, point, _)) = snapshot.point_to_buffer_point(snapshot.max_point()) { - assert!(point <= buffer.max_point()); - } -} - -fn assert_line_indents(snapshot: &MultiBufferSnapshot) { - let max_row = snapshot.max_point().row; - let buffer_id = snapshot.excerpts().next().unwrap().1.remote_id(); - let text = text::Buffer::new(ReplicaId::LOCAL, buffer_id, snapshot.text()); - let mut line_indents = text - .line_indents_in_row_range(0..max_row + 1) - .collect::>(); - for start_row in 0..snapshot.max_point().row { - pretty_assertions::assert_eq!( - snapshot - .line_indents(MultiBufferRow(start_row), |_| true) - .map(|(row, indent, _)| (row.0, indent)) - .collect::>(), - &line_indents[(start_row as usize)..], - "line_indents({start_row})" - ); - } - - line_indents.reverse(); - pretty_assertions::assert_eq!( - snapshot - .reversed_line_indents(MultiBufferRow(max_row), |_| true) - .map(|(row, indent, _)| (row.0, indent)) - .collect::>(), - &line_indents[..], - "reversed_line_indents({max_row})" - ); -} - -#[gpui::test] -fn test_new_empty_buffer_uses_untitled_title(cx: &mut App) { - let buffer = cx.new(|cx| Buffer::local("", cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - - assert_eq!(multibuffer.read(cx).title(cx), "untitled"); -} - -#[gpui::test] -fn test_new_empty_buffer_uses_untitled_title_when_only_contains_whitespace(cx: &mut App) { - let buffer = cx.new(|cx| Buffer::local("\n ", cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - - assert_eq!(multibuffer.read(cx).title(cx), "untitled"); -} - -#[gpui::test] -fn test_new_empty_buffer_takes_first_line_for_title(cx: &mut App) { - let buffer = cx.new(|cx| Buffer::local("Hello World\nSecond line", cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - - assert_eq!(multibuffer.read(cx).title(cx), "Hello World"); -} - -#[gpui::test] -fn test_new_empty_buffer_takes_trimmed_first_line_for_title(cx: &mut App) { - let buffer = cx.new(|cx| Buffer::local("\nHello, World ", cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - - assert_eq!(multibuffer.read(cx).title(cx), "Hello, World"); -} - -#[gpui::test] -fn test_new_empty_buffer_uses_truncated_first_line_for_title(cx: &mut App) { - let title = "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee"; - let title_after = "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd"; - let buffer = cx.new(|cx| Buffer::local(title, cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - - assert_eq!(multibuffer.read(cx).title(cx), title_after); -} - -#[gpui::test] -fn test_new_empty_buffer_uses_truncated_first_line_for_title_after_merging_adjacent_spaces( - cx: &mut App, -) { - let title = "aaaaaaaaaabbbbbbbbbb ccccccccccddddddddddeeeeeeeeee"; - let title_after = "aaaaaaaaaabbbbbbbbbb ccccccccccddddddddd"; - let buffer = cx.new(|cx| Buffer::local(title, cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - - assert_eq!(multibuffer.read(cx).title(cx), title_after); -} - -#[gpui::test] -fn test_new_empty_buffers_title_can_be_set(cx: &mut App) { - let buffer = cx.new(|cx| Buffer::local("Hello World", cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - assert_eq!(multibuffer.read(cx).title(cx), "Hello World"); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_title("Hey".into(), cx) - }); - assert_eq!(multibuffer.read(cx).title(cx), "Hey"); -} - -#[gpui::test(iterations = 100)] -fn test_random_chunk_bitmaps(cx: &mut App, mut rng: StdRng) { - let multibuffer = if rng.random() { - let len = rng.random_range(0..10000); - let text = RandomCharIter::new(&mut rng).take(len).collect::(); - let buffer = cx.new(|cx| Buffer::local(text, cx)); - cx.new(|cx| MultiBuffer::singleton(buffer, cx)) - } else { - MultiBuffer::build_random(&mut rng, cx) - }; - - let snapshot = multibuffer.read(cx).snapshot(cx); - - let chunks = snapshot.chunks(MultiBufferOffset(0)..snapshot.len(), false); - - for chunk in chunks { - let chunk_text = chunk.text; - let chars_bitmap = chunk.chars; - let tabs_bitmap = chunk.tabs; - - if chunk_text.is_empty() { - assert_eq!( - chars_bitmap, 0, - "Empty chunk should have empty chars bitmap" - ); - assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap"); - continue; - } - - assert!( - chunk_text.len() <= 128, - "Chunk text length {} exceeds 128 bytes", - chunk_text.len() - ); - - // Verify chars bitmap - let char_indices = chunk_text - .char_indices() - .map(|(i, _)| i) - .collect::>(); - - for byte_idx in 0..chunk_text.len() { - let should_have_bit = char_indices.contains(&byte_idx); - let has_bit = chars_bitmap & (1 << byte_idx) != 0; - - if has_bit != should_have_bit { - eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes()); - eprintln!("Char indices: {:?}", char_indices); - eprintln!("Chars bitmap: {:#b}", chars_bitmap); - } - - assert_eq!( - has_bit, should_have_bit, - "Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}", - byte_idx, chunk_text, should_have_bit, has_bit - ); - } - - for (byte_idx, byte) in chunk_text.bytes().enumerate() { - let is_tab = byte == b'\t'; - let has_bit = tabs_bitmap & (1 << byte_idx) != 0; - - if has_bit != is_tab { - eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes()); - eprintln!("Tabs bitmap: {:#b}", tabs_bitmap); - assert_eq!( - has_bit, is_tab, - "Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}", - byte_idx, chunk_text, byte as char, is_tab, has_bit - ); - } - } - } -} - -#[gpui::test(iterations = 10)] -fn test_random_chunk_bitmaps_with_diffs(cx: &mut App, mut rng: StdRng) { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - use buffer_diff::BufferDiff; - use util::RandomCharIter; - - let multibuffer = if rng.random() { - let len = rng.random_range(100..10000); - let text = RandomCharIter::new(&mut rng).take(len).collect::(); - let buffer = cx.new(|cx| Buffer::local(text, cx)); - cx.new(|cx| MultiBuffer::singleton(buffer, cx)) - } else { - MultiBuffer::build_random(&mut rng, cx) - }; - - let _diff_count = rng.random_range(1..5); - let mut diffs = Vec::new(); - - multibuffer.update(cx, |multibuffer, cx| { - for buffer_id in multibuffer.excerpt_buffer_ids() { - if rng.random_bool(0.7) { - if let Some(buffer_handle) = multibuffer.buffer(buffer_id) { - let buffer_text = buffer_handle.read(cx).text(); - let mut base_text = String::new(); - - for line in buffer_text.lines() { - if rng.random_bool(0.3) { - continue; - } else if rng.random_bool(0.3) { - let line_len = rng.random_range(0..50); - let modified_line = RandomCharIter::new(&mut rng) - .take(line_len) - .collect::(); - base_text.push_str(&modified_line); - base_text.push('\n'); - } else { - base_text.push_str(line); - base_text.push('\n'); - } - } - - if rng.random_bool(0.5) { - let extra_lines = rng.random_range(1..5); - for _ in 0..extra_lines { - let line_len = rng.random_range(0..50); - let extra_line = RandomCharIter::new(&mut rng) - .take(line_len) - .collect::(); - base_text.push_str(&extra_line); - base_text.push('\n'); - } - } - - let diff = - cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer_handle, cx)); - diffs.push(diff.clone()); - multibuffer.add_diff(diff, cx); - } - } - } - }); - - multibuffer.update(cx, |multibuffer, cx| { - if rng.random_bool(0.5) { - multibuffer.set_all_diff_hunks_expanded(cx); - } else { - let snapshot = multibuffer.snapshot(cx); - let text = snapshot.text(); - - let mut ranges = Vec::new(); - for _ in 0..rng.random_range(1..5) { - if snapshot.len().0 == 0 { - break; - } - - let diff_size = rng.random_range(5..1000); - let mut start = rng.random_range(0..snapshot.len().0); - - while !text.is_char_boundary(start) { - start = start.saturating_sub(1); - } - - let mut end = rng.random_range(start..snapshot.len().0.min(start + diff_size)); - - while !text.is_char_boundary(end) { - end = end.saturating_add(1); - } - let start_anchor = snapshot.anchor_after(MultiBufferOffset(start)); - let end_anchor = snapshot.anchor_before(MultiBufferOffset(end)); - ranges.push(start_anchor..end_anchor); - } - multibuffer.expand_diff_hunks(ranges, cx); - } - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - let chunks = snapshot.chunks(MultiBufferOffset(0)..snapshot.len(), false); - - for chunk in chunks { - let chunk_text = chunk.text; - let chars_bitmap = chunk.chars; - let tabs_bitmap = chunk.tabs; - - if chunk_text.is_empty() { - assert_eq!( - chars_bitmap, 0, - "Empty chunk should have empty chars bitmap" - ); - assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap"); - continue; - } - - assert!( - chunk_text.len() <= 128, - "Chunk text length {} exceeds 128 bytes", - chunk_text.len() - ); - - let char_indices = chunk_text - .char_indices() - .map(|(i, _)| i) - .collect::>(); - - for byte_idx in 0..chunk_text.len() { - let should_have_bit = char_indices.contains(&byte_idx); - let has_bit = chars_bitmap & (1 << byte_idx) != 0; - - if has_bit != should_have_bit { - eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes()); - eprintln!("Char indices: {:?}", char_indices); - eprintln!("Chars bitmap: {:#b}", chars_bitmap); - } - - assert_eq!( - has_bit, should_have_bit, - "Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}", - byte_idx, chunk_text, should_have_bit, has_bit - ); - } - - for (byte_idx, byte) in chunk_text.bytes().enumerate() { - let is_tab = byte == b'\t'; - let has_bit = tabs_bitmap & (1 << byte_idx) != 0; - - if has_bit != is_tab { - eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes()); - eprintln!("Tabs bitmap: {:#b}", tabs_bitmap); - assert_eq!( - has_bit, is_tab, - "Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}", - byte_idx, chunk_text, byte as char, is_tab, has_bit - ); - } - } - } -} - -fn collect_word_diffs( - base_text: &str, - modified_text: &str, - cx: &mut TestAppContext, -) -> Vec { - let buffer = cx.new(|cx| Buffer::local(modified_text, cx)); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); - cx.run_until_parked(); - - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx); - multibuffer.add_diff(diff.clone(), cx); - multibuffer - }); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); - }); - - let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - let text = snapshot.text(); - - snapshot - .diff_hunks() - .flat_map(|hunk| hunk.word_diffs) - .map(|range| text[range.start.0..range.end.0].to_string()) - .collect() -} - -#[gpui::test] -async fn test_word_diff_simple_replacement(cx: &mut TestAppContext) { - let settings_store = cx.update(|cx| SettingsStore::test(cx)); - cx.set_global(settings_store); - - let base_text = "hello world foo bar\n"; - let modified_text = "hello WORLD foo BAR\n"; - - let word_diffs = collect_word_diffs(base_text, modified_text, cx); - - assert_eq!(word_diffs, vec!["world", "bar", "WORLD", "BAR"]); -} - -#[gpui::test] -async fn test_word_diff_consecutive_modified_lines(cx: &mut TestAppContext) { - let settings_store = cx.update(|cx| SettingsStore::test(cx)); - cx.set_global(settings_store); - - let base_text = "aaa bbb\nccc ddd\n"; - let modified_text = "aaa BBB\nccc DDD\n"; - - let word_diffs = collect_word_diffs(base_text, modified_text, cx); - - assert_eq!( - word_diffs, - vec!["bbb", "ddd", "BBB", "DDD"], - "consecutive modified lines should produce word diffs when line counts match" - ); -} - -#[gpui::test] -async fn test_word_diff_modified_lines_with_deletion_between(cx: &mut TestAppContext) { - let settings_store = cx.update(|cx| SettingsStore::test(cx)); - cx.set_global(settings_store); - - let base_text = "aaa bbb\ndeleted line\nccc ddd\n"; - let modified_text = "aaa BBB\nccc DDD\n"; - - let word_diffs = collect_word_diffs(base_text, modified_text, cx); - - assert_eq!( - word_diffs, - Vec::::new(), - "modified lines with a deleted line between should not produce word diffs" - ); -} - -#[gpui::test] -async fn test_word_diff_disabled(cx: &mut TestAppContext) { - let settings_store = cx.update(|cx| { - let mut settings_store = SettingsStore::test(cx); - settings_store.update_user_settings(cx, |settings| { - settings.project.all_languages.defaults.word_diff_enabled = Some(false); - }); - settings_store - }); - cx.set_global(settings_store); - - let base_text = "hello world\n"; - let modified_text = "hello WORLD\n"; - - let word_diffs = collect_word_diffs(base_text, modified_text, cx); - - assert_eq!( - word_diffs, - Vec::::new(), - "word diffs should be empty when disabled" - ); -} - -/// Tests `excerpt_containing` and `excerpts_for_range` (functions mapping multi-buffer text-coordinates to excerpts) -#[gpui::test] -fn test_excerpts_containment_functions(cx: &mut App) { - // Multibuffer content for these tests: - // 0123 - // 0: aa0 - // 1: aa1 - // ----- - // 2: bb0 - // 3: bb1 - // -----MultiBufferOffset(0).. - // 4: cc0 - - let buffer_1 = cx.new(|cx| Buffer::local("aa0\naa1", cx)); - let buffer_2 = cx.new(|cx| Buffer::local("bb0\nbb1", cx)); - let buffer_3 = cx.new(|cx| Buffer::local("cc0", cx)); - - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - - let (excerpt_1_id, excerpt_2_id, excerpt_3_id) = multibuffer.update(cx, |multibuffer, cx| { - let excerpt_1_id = multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 3))], - cx, - )[0]; - - let excerpt_2_id = multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 3))], - cx, - )[0]; - - let excerpt_3_id = multibuffer.push_excerpts( - buffer_3.clone(), - [ExcerptRange::new(Point::new(0, 0)..Point::new(0, 3))], - cx, - )[0]; - - (excerpt_1_id, excerpt_2_id, excerpt_3_id) - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!(snapshot.text(), "aa0\naa1\nbb0\nbb1\ncc0"); - - //// Test `excerpts_for_range` - - let p00 = snapshot.point_to_offset(Point::new(0, 0)); - let p10 = snapshot.point_to_offset(Point::new(1, 0)); - let p20 = snapshot.point_to_offset(Point::new(2, 0)); - let p23 = snapshot.point_to_offset(Point::new(2, 3)); - let p13 = snapshot.point_to_offset(Point::new(1, 3)); - let p40 = snapshot.point_to_offset(Point::new(4, 0)); - let p43 = snapshot.point_to_offset(Point::new(4, 3)); - - let excerpts: Vec<_> = snapshot.excerpts_for_range(p00..p00).collect(); - assert_eq!(excerpts.len(), 1); - assert_eq!(excerpts[0].id, excerpt_1_id); - - // Cursor at very end of excerpt 3 - let excerpts: Vec<_> = snapshot.excerpts_for_range(p43..p43).collect(); - assert_eq!(excerpts.len(), 1); - assert_eq!(excerpts[0].id, excerpt_3_id); - - let excerpts: Vec<_> = snapshot.excerpts_for_range(p00..p23).collect(); - assert_eq!(excerpts.len(), 2); - assert_eq!(excerpts[0].id, excerpt_1_id); - assert_eq!(excerpts[1].id, excerpt_2_id); - - // This range represent an selection with end-point just inside excerpt_2 - // Today we only expand the first excerpt, but another interpretation that - // we could consider is expanding both here - let excerpts: Vec<_> = snapshot.excerpts_for_range(p10..p20).collect(); - assert_eq!(excerpts.len(), 1); - assert_eq!(excerpts[0].id, excerpt_1_id); - - //// Test that `excerpts_for_range` and `excerpt_containing` agree for all single offsets (cursor positions) - for offset in 0..=snapshot.len().0 { - let offset = MultiBufferOffset(offset); - let excerpts_for_range: Vec<_> = snapshot.excerpts_for_range(offset..offset).collect(); - assert_eq!( - excerpts_for_range.len(), - 1, - "Expected exactly one excerpt for offset {offset}", - ); - - let excerpt_containing = snapshot.excerpt_containing(offset..offset); - assert!( - excerpt_containing.is_some(), - "Expected excerpt_containing to find excerpt for offset {offset}", - ); - - assert_eq!( - excerpts_for_range[0].id, - excerpt_containing.unwrap().id(), - "excerpts_for_range and excerpt_containing should agree for offset {offset}", - ); - } - - //// Test `excerpt_containing` behavior with ranges: - - // Ranges intersecting a single-excerpt - let containing = snapshot.excerpt_containing(p00..p13); - assert!(containing.is_some()); - assert_eq!(containing.unwrap().id(), excerpt_1_id); - - // Ranges intersecting multiple excerpts (should return None) - let containing = snapshot.excerpt_containing(p20..p40); - assert!( - containing.is_none(), - "excerpt_containing should return None for ranges spanning multiple excerpts" - ); -} diff --git a/crates/multi_buffer/src/path_key.rs b/crates/multi_buffer/src/path_key.rs deleted file mode 100644 index 119194d088..0000000000 --- a/crates/multi_buffer/src/path_key.rs +++ /dev/null @@ -1,437 +0,0 @@ -use std::{mem, ops::Range, sync::Arc}; - -use collections::HashSet; -use gpui::{App, AppContext, Context, Entity}; -use itertools::Itertools; -use language::{Buffer, BufferSnapshot}; -use rope::Point; -use text::{Bias, BufferId, OffsetRangeExt, locator::Locator}; -use util::{post_inc, rel_path::RelPath}; -use ztracing::instrument; - -use crate::{ - Anchor, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, build_excerpt_ranges, -}; - -#[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Hash, Debug)] -pub struct PathKey { - // Used by the derived PartialOrd & Ord - pub sort_prefix: Option, - pub path: Arc, -} - -impl PathKey { - pub fn with_sort_prefix(sort_prefix: u64, path: Arc) -> Self { - Self { - sort_prefix: Some(sort_prefix), - path, - } - } - - pub fn for_buffer(buffer: &Entity, cx: &App) -> Self { - if let Some(file) = buffer.read(cx).file() { - Self::with_sort_prefix(file.worktree_id(cx).to_proto(), file.path().clone()) - } else { - Self { - sort_prefix: None, - path: RelPath::unix(&buffer.entity_id().to_string()) - .unwrap() - .into_arc(), - } - } - } -} - -impl MultiBuffer { - pub fn paths(&self) -> impl Iterator + '_ { - self.excerpts_by_path.keys().cloned() - } - - pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context) { - if let Some(to_remove) = self.excerpts_by_path.remove(&path) { - self.remove_excerpts(to_remove, cx) - } - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| { - follower.remove_excerpts_for_path(path, cx); - }); - } - } - - pub fn location_for_path(&self, path: &PathKey, cx: &App) -> Option { - let excerpt_id = self.excerpts_by_path.get(path)?.first()?; - let snapshot = self.read(cx); - let excerpt = snapshot.excerpt(*excerpt_id)?; - Some(Anchor::in_buffer(excerpt.id, excerpt.range.context.start)) - } - - pub fn excerpt_paths(&self) -> impl Iterator { - self.excerpts_by_path.keys() - } - - /// Sets excerpts, returns `true` if at least one new excerpt was added. - #[instrument(skip_all)] - pub fn set_excerpts_for_path( - &mut self, - path: PathKey, - buffer: Entity, - ranges: impl IntoIterator>, - context_line_count: u32, - cx: &mut Context, - ) -> (Vec>, bool) { - let buffer_snapshot = buffer.read(cx).snapshot(); - let excerpt_ranges = build_excerpt_ranges(ranges, context_line_count, &buffer_snapshot); - - let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges); - self.set_merged_excerpt_ranges_for_path( - path, - buffer, - excerpt_ranges, - &buffer_snapshot, - new, - counts, - cx, - ) - } - - pub fn set_excerpt_ranges_for_path( - &mut self, - path: PathKey, - buffer: Entity, - buffer_snapshot: &BufferSnapshot, - excerpt_ranges: Vec>, - cx: &mut Context, - ) -> (Vec>, bool) { - let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges); - self.set_merged_excerpt_ranges_for_path( - path, - buffer, - excerpt_ranges, - buffer_snapshot, - new, - counts, - cx, - ) - } - - pub fn set_anchored_excerpts_for_path( - &self, - path_key: PathKey, - buffer: Entity, - ranges: Vec>, - context_line_count: u32, - cx: &Context, - ) -> impl Future>> + use<> { - let buffer_snapshot = buffer.read(cx).snapshot(); - let multi_buffer = cx.weak_entity(); - let mut app = cx.to_async(); - async move { - let snapshot = buffer_snapshot.clone(); - let (excerpt_ranges, new, counts) = app - .background_spawn(async move { - let ranges = ranges.into_iter().map(|range| range.to_point(&snapshot)); - let excerpt_ranges = - build_excerpt_ranges(ranges, context_line_count, &snapshot); - let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges); - (excerpt_ranges, new, counts) - }) - .await; - - multi_buffer - .update(&mut app, move |multi_buffer, cx| { - let (ranges, _) = multi_buffer.set_merged_excerpt_ranges_for_path( - path_key, - buffer, - excerpt_ranges, - &buffer_snapshot, - new, - counts, - cx, - ); - ranges - }) - .ok() - .unwrap_or_default() - } - } - - pub fn remove_excerpts_for_buffer(&mut self, buffer: BufferId, cx: &mut Context) { - self.remove_excerpts( - self.excerpts_for_buffer(buffer, cx) - .into_iter() - .map(|(excerpt, _)| excerpt), - cx, - ); - } - - pub(super) fn expand_excerpts_with_paths( - &mut self, - ids: impl IntoIterator, - line_count: u32, - direction: ExpandExcerptDirection, - cx: &mut Context, - ) { - let grouped = ids - .into_iter() - .chunk_by(|id| self.paths_by_excerpt.get(id).cloned()) - .into_iter() - .filter_map(|(k, v)| Some((k?, v.into_iter().collect::>()))) - .collect::>(); - let snapshot = self.snapshot(cx); - - for (path, ids) in grouped.into_iter() { - let Some(excerpt_ids) = self.excerpts_by_path.get(&path) else { - continue; - }; - - let ids_to_expand = HashSet::from_iter(ids); - let mut excerpt_id_ = None; - let expanded_ranges = excerpt_ids.iter().filter_map(|excerpt_id| { - let excerpt = snapshot.excerpt(*excerpt_id)?; - let excerpt_id = excerpt.id; - if excerpt_id_.is_none() { - excerpt_id_ = Some(excerpt_id); - } - - let mut context = excerpt.range.context.to_point(&excerpt.buffer); - if ids_to_expand.contains(&excerpt_id) { - match direction { - ExpandExcerptDirection::Up => { - context.start.row = context.start.row.saturating_sub(line_count); - context.start.column = 0; - } - ExpandExcerptDirection::Down => { - context.end.row = - (context.end.row + line_count).min(excerpt.buffer.max_point().row); - context.end.column = excerpt.buffer.line_len(context.end.row); - } - ExpandExcerptDirection::UpAndDown => { - context.start.row = context.start.row.saturating_sub(line_count); - context.start.column = 0; - context.end.row = - (context.end.row + line_count).min(excerpt.buffer.max_point().row); - context.end.column = excerpt.buffer.line_len(context.end.row); - } - } - } - - Some(ExcerptRange { - context, - primary: excerpt.range.primary.to_point(&excerpt.buffer), - }) - }); - 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; - } - merged_ranges.push(range) - } - let Some(excerpt_id) = excerpt_id_ else { - continue; - }; - let Some(buffer_id) = &snapshot.buffer_id_for_excerpt(excerpt_id) else { - continue; - }; - - let Some(buffer) = self.buffers.get(buffer_id).map(|b| b.buffer.clone()) else { - continue; - }; - - let buffer_snapshot = buffer.read(cx).snapshot(); - self.update_path_excerpts(path.clone(), buffer, &buffer_snapshot, merged_ranges, cx); - } - } - - /// Sets excerpts, returns `true` if at least one new excerpt was added. - fn set_merged_excerpt_ranges_for_path( - &mut self, - path: PathKey, - buffer: Entity, - ranges: Vec>, - buffer_snapshot: &BufferSnapshot, - new: Vec>, - counts: Vec, - cx: &mut Context, - ) -> (Vec>, bool) { - let (excerpt_ids, added_a_new_excerpt) = - self.update_path_excerpts(path, buffer, buffer_snapshot, new, cx); - - let mut result = Vec::new(); - let mut ranges = ranges.into_iter(); - for (excerpt_id, range_count) in excerpt_ids.into_iter().zip(counts.into_iter()) { - for range in ranges.by_ref().take(range_count) { - let range = Anchor::range_in_buffer( - excerpt_id, - buffer_snapshot.anchor_before(&range.primary.start) - ..buffer_snapshot.anchor_after(&range.primary.end), - ); - result.push(range) - } - } - (result, added_a_new_excerpt) - } - - fn update_path_excerpts( - &mut self, - path: PathKey, - buffer: Entity, - buffer_snapshot: &BufferSnapshot, - new: Vec>, - cx: &mut Context, - ) -> (Vec, bool) { - let mut insert_after = self - .excerpts_by_path - .range(..path.clone()) - .next_back() - .and_then(|(_, value)| value.last().copied()) - .unwrap_or(ExcerptId::min()); - - let existing = self - .excerpts_by_path - .get(&path) - .cloned() - .unwrap_or_default(); - let mut new_iter = new.into_iter().peekable(); - let mut existing_iter = existing.into_iter().peekable(); - - let mut excerpt_ids = Vec::new(); - let mut to_remove = Vec::new(); - let mut to_insert: Vec<(ExcerptId, ExcerptRange)> = Vec::new(); - let mut added_a_new_excerpt = false; - let snapshot = self.snapshot(cx); - - let mut next_excerpt_id = - // todo(lw): is this right? What if we remove the last excerpt, then we might reallocate with a wrong mapping? - if let Some(last_entry) = self.snapshot.borrow().excerpt_ids.last() { - last_entry.id.0 + 1 - } else { - 1 - }; - - let mut next_excerpt_id = move || ExcerptId(post_inc(&mut next_excerpt_id)); - - let mut excerpts_cursor = snapshot.excerpts.cursor::>(()); - excerpts_cursor.next(); - - loop { - let existing = if let Some(&existing_id) = existing_iter.peek() { - let locator = snapshot.excerpt_locator_for_id(existing_id); - excerpts_cursor.seek_forward(&Some(locator), Bias::Left); - if let Some(excerpt) = excerpts_cursor.item() { - if excerpt.buffer_id != buffer_snapshot.remote_id() { - to_remove.push(existing_id); - existing_iter.next(); - continue; - } - Some((existing_id, excerpt.range.context.to_point(buffer_snapshot))) - } else { - None - } - } else { - None - }; - - let new = new_iter.peek(); - 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((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 - .get_mut() - .replaced_excerpts - .insert(*existing_id, *last_id); - existing_iter.next(); - continue; - } - } - - match (new, existing) { - (None, None) => break, - (None, Some((existing_id, _))) => { - existing_iter.next(); - to_remove.push(existing_id); - continue; - } - (Some(_), None) => { - added_a_new_excerpt = true; - let new_id = next_excerpt_id(); - excerpt_ids.push(new_id); - to_insert.push((new_id, new_iter.next().unwrap())); - continue; - } - (Some(new), Some((_, existing_range))) => { - if existing_range.end < new.context.start { - let existing_id = existing_iter.next().unwrap(); - to_remove.push(existing_id); - continue; - } else if existing_range.start > new.context.end { - let new_id = next_excerpt_id(); - excerpt_ids.push(new_id); - to_insert.push((new_id, new_iter.next().unwrap())); - continue; - } - - if existing_range.start == new.context.start - && existing_range.end == new.context.end - { - self.insert_excerpts_with_ids_after( - insert_after, - buffer.clone(), - mem::take(&mut to_insert), - cx, - ); - insert_after = existing_iter.next().unwrap(); - excerpt_ids.push(insert_after); - new_iter.next(); - } else { - let existing_id = existing_iter.next().unwrap(); - let new_id = next_excerpt_id(); - self.snapshot - .get_mut() - .replaced_excerpts - .insert(existing_id, new_id); - to_remove.push(existing_id); - let mut range = new_iter.next().unwrap(); - range.context.start = range.context.start.min(existing_range.start); - range.context.end = range.context.end.max(existing_range.end); - excerpt_ids.push(new_id); - to_insert.push((new_id, range)); - } - } - }; - } - - self.insert_excerpts_with_ids_after(insert_after, buffer, to_insert, cx); - // todo(lw): There is a logic bug somewhere that causes the to_remove vector to be not ordered correctly - to_remove.sort_by_cached_key(|&id| snapshot.excerpt_locator_for_id(id)); - self.remove_excerpts(to_remove, cx); - - if excerpt_ids.is_empty() { - self.excerpts_by_path.remove(&path); - } else { - for excerpt_id in &excerpt_ids { - self.paths_by_excerpt.insert(*excerpt_id, path.clone()); - } - let snapshot = &*self.snapshot.get_mut(); - let mut excerpt_ids: Vec<_> = excerpt_ids.iter().dedup().cloned().collect(); - excerpt_ids.sort_by_cached_key(|&id| snapshot.excerpt_locator_for_id(id)); - self.excerpts_by_path.insert(path, excerpt_ids); - } - - (excerpt_ids, added_a_new_excerpt) - } -} diff --git a/crates/multi_buffer/src/transaction.rs b/crates/multi_buffer/src/transaction.rs deleted file mode 100644 index a65e394c8f..0000000000 --- a/crates/multi_buffer/src/transaction.rs +++ /dev/null @@ -1,538 +0,0 @@ -use gpui::{App, Context, Entity}; -use language::{self, Buffer, TransactionId}; -use std::{ - collections::HashMap, - ops::{AddAssign, Range, Sub}, - time::{Duration, Instant}, -}; -use sum_tree::Bias; -use text::BufferId; - -use crate::{BufferState, MultiBufferDimension}; - -use super::{Event, ExcerptSummary, MultiBuffer}; - -#[derive(Clone)] -pub(super) struct History { - next_transaction_id: TransactionId, - undo_stack: Vec, - redo_stack: Vec, - transaction_depth: usize, - group_interval: Duration, -} - -impl Default for History { - fn default() -> Self { - History { - next_transaction_id: clock::Lamport::MIN, - undo_stack: Vec::new(), - redo_stack: Vec::new(), - transaction_depth: 0, - group_interval: Duration::from_millis(300), - } - } -} - -#[derive(Clone)] -struct Transaction { - id: TransactionId, - buffer_transactions: HashMap, - first_edit_at: Instant, - last_edit_at: Instant, - suppress_grouping: bool, -} - -impl History { - fn start_transaction(&mut self, now: Instant) -> Option { - self.transaction_depth += 1; - if self.transaction_depth == 1 { - let id = self.next_transaction_id.tick(); - self.undo_stack.push(Transaction { - id, - buffer_transactions: Default::default(), - first_edit_at: now, - last_edit_at: now, - suppress_grouping: false, - }); - Some(id) - } else { - None - } - } - - fn end_transaction( - &mut self, - now: Instant, - buffer_transactions: HashMap, - ) -> bool { - assert_ne!(self.transaction_depth, 0); - self.transaction_depth -= 1; - if self.transaction_depth == 0 { - if buffer_transactions.is_empty() { - self.undo_stack.pop(); - false - } else { - self.redo_stack.clear(); - let transaction = self.undo_stack.last_mut().unwrap(); - transaction.last_edit_at = now; - for (buffer_id, transaction_id) in buffer_transactions { - transaction - .buffer_transactions - .entry(buffer_id) - .or_insert(transaction_id); - } - true - } - } else { - false - } - } - - fn push_transaction<'a, T>( - &mut self, - buffer_transactions: T, - now: Instant, - cx: &Context, - ) where - T: IntoIterator, &'a language::Transaction)>, - { - assert_eq!(self.transaction_depth, 0); - let transaction = Transaction { - id: self.next_transaction_id.tick(), - buffer_transactions: buffer_transactions - .into_iter() - .map(|(buffer, transaction)| (buffer.read(cx).remote_id(), transaction.id)) - .collect(), - first_edit_at: now, - last_edit_at: now, - suppress_grouping: false, - }; - if !transaction.buffer_transactions.is_empty() { - self.undo_stack.push(transaction); - self.redo_stack.clear(); - } - } - - fn finalize_last_transaction(&mut self) { - if let Some(transaction) = self.undo_stack.last_mut() { - transaction.suppress_grouping = true; - } - } - - fn forget(&mut self, transaction_id: TransactionId) -> Option { - if let Some(ix) = self - .undo_stack - .iter() - .rposition(|transaction| transaction.id == transaction_id) - { - Some(self.undo_stack.remove(ix)) - } else if let Some(ix) = self - .redo_stack - .iter() - .rposition(|transaction| transaction.id == transaction_id) - { - Some(self.redo_stack.remove(ix)) - } else { - None - } - } - - fn transaction(&self, transaction_id: TransactionId) -> Option<&Transaction> { - self.undo_stack - .iter() - .find(|transaction| transaction.id == transaction_id) - .or_else(|| { - self.redo_stack - .iter() - .find(|transaction| transaction.id == transaction_id) - }) - } - - fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> { - self.undo_stack - .iter_mut() - .find(|transaction| transaction.id == transaction_id) - .or_else(|| { - self.redo_stack - .iter_mut() - .find(|transaction| transaction.id == transaction_id) - }) - } - - fn pop_undo(&mut self) -> Option<&mut Transaction> { - assert_eq!(self.transaction_depth, 0); - if let Some(transaction) = self.undo_stack.pop() { - self.redo_stack.push(transaction); - self.redo_stack.last_mut() - } else { - None - } - } - - fn pop_redo(&mut self) -> Option<&mut Transaction> { - assert_eq!(self.transaction_depth, 0); - if let Some(transaction) = self.redo_stack.pop() { - self.undo_stack.push(transaction); - self.undo_stack.last_mut() - } else { - None - } - } - - fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> { - let ix = self - .undo_stack - .iter() - .rposition(|transaction| transaction.id == transaction_id)?; - let transaction = self.undo_stack.remove(ix); - self.redo_stack.push(transaction); - self.redo_stack.last() - } - - fn group(&mut self) -> Option { - let mut count = 0; - let mut transactions = self.undo_stack.iter(); - if let Some(mut transaction) = transactions.next_back() { - while let Some(prev_transaction) = transactions.next_back() { - if !prev_transaction.suppress_grouping - && transaction.first_edit_at - prev_transaction.last_edit_at - <= self.group_interval - { - transaction = prev_transaction; - count += 1; - } else { - break; - } - } - } - self.group_trailing(count) - } - - fn group_until(&mut self, transaction_id: TransactionId) { - let mut count = 0; - for transaction in self.undo_stack.iter().rev() { - if transaction.id == transaction_id { - self.group_trailing(count); - break; - } else if transaction.suppress_grouping { - break; - } else { - count += 1; - } - } - } - - fn group_trailing(&mut self, n: usize) -> Option { - let new_len = self.undo_stack.len() - n; - let (transactions_to_keep, transactions_to_merge) = self.undo_stack.split_at_mut(new_len); - if let Some(last_transaction) = transactions_to_keep.last_mut() { - if let Some(transaction) = transactions_to_merge.last() { - last_transaction.last_edit_at = transaction.last_edit_at; - } - for to_merge in transactions_to_merge { - for (buffer_id, transaction_id) in &to_merge.buffer_transactions { - last_transaction - .buffer_transactions - .entry(*buffer_id) - .or_insert(*transaction_id); - } - } - } - - self.undo_stack.truncate(new_len); - self.undo_stack.last().map(|t| t.id) - } - - pub(super) fn transaction_depth(&self) -> usize { - self.transaction_depth - } - - pub fn set_group_interval(&mut self, group_interval: Duration) { - self.group_interval = group_interval; - } -} - -impl MultiBuffer { - pub fn start_transaction(&mut self, cx: &mut Context) -> Option { - self.start_transaction_at(Instant::now(), cx) - } - - pub fn start_transaction_at( - &mut self, - now: Instant, - cx: &mut Context, - ) -> Option { - if let Some(buffer) = self.as_singleton() { - return buffer.update(cx, |buffer, _| buffer.start_transaction_at(now)); - } - - for BufferState { buffer, .. } in self.buffers.values() { - buffer.update(cx, |buffer, _| buffer.start_transaction_at(now)); - } - self.history.start_transaction(now) - } - - pub fn last_transaction_id(&self, cx: &App) -> Option { - if let Some(buffer) = self.as_singleton() { - buffer - .read(cx) - .peek_undo_stack() - .map(|history_entry| history_entry.transaction_id()) - } else { - let last_transaction = self.history.undo_stack.last()?; - Some(last_transaction.id) - } - } - - pub fn end_transaction(&mut self, cx: &mut Context) -> Option { - self.end_transaction_at(Instant::now(), cx) - } - - pub fn end_transaction_at( - &mut self, - now: Instant, - cx: &mut Context, - ) -> Option { - if let Some(buffer) = self.as_singleton() { - return buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)); - } - - let mut buffer_transactions = HashMap::default(); - for BufferState { buffer, .. } in self.buffers.values() { - if let Some(transaction_id) = - buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) - { - buffer_transactions.insert(buffer.read(cx).remote_id(), transaction_id); - } - } - - if self.history.end_transaction(now, buffer_transactions) { - let transaction_id = self.history.group().unwrap(); - Some(transaction_id) - } else { - None - } - } - - pub fn edited_ranges_for_transaction( - &self, - transaction_id: TransactionId, - cx: &App, - ) -> Vec> - where - D: MultiBufferDimension - + Ord - + Sub - + AddAssign, - D::TextDimension: PartialOrd + Sub, - { - let Some(transaction) = self.history.transaction(transaction_id) else { - return Vec::new(); - }; - - let mut ranges = Vec::new(); - let snapshot = self.read(cx); - let mut cursor = snapshot.excerpts.cursor::(()); - - for (buffer_id, buffer_transaction) in &transaction.buffer_transactions { - let Some(buffer_state) = self.buffers.get(buffer_id) else { - continue; - }; - - let buffer = buffer_state.buffer.read(cx); - 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_summary(&cursor.start().text); - - let mut start = excerpt_start; - start += range.start - excerpt_buffer_start; - let mut end = excerpt_start; - end += range.end - excerpt_buffer_start; - - ranges.push(start..end); - break; - } - } - } - } - } - - ranges.sort_by_key(|range| range.start); - ranges - } - - pub fn merge_transactions( - &mut self, - transaction: TransactionId, - destination: TransactionId, - cx: &mut Context, - ) { - if let Some(buffer) = self.as_singleton() { - 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.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); - } - } - } - } - - pub fn finalize_last_transaction(&mut self, cx: &mut Context) { - self.history.finalize_last_transaction(); - for BufferState { buffer, .. } in self.buffers.values() { - buffer.update(cx, |buffer, _| { - buffer.finalize_last_transaction(); - }); - } - } - - pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T, cx: &Context) - where - T: IntoIterator, &'a language::Transaction)>, - { - self.history - .push_transaction(buffer_transactions, Instant::now(), cx); - self.history.finalize_last_transaction(); - } - - pub fn group_until_transaction( - &mut self, - transaction_id: TransactionId, - cx: &mut Context, - ) { - if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, _| { - buffer.group_until_transaction(transaction_id) - }); - } else { - self.history.group_until(transaction_id); - } - } - pub fn undo(&mut self, cx: &mut Context) -> Option { - let mut transaction_id = None; - if let Some(buffer) = self.as_singleton() { - transaction_id = buffer.update(cx, |buffer, cx| buffer.undo(cx)); - } else { - while let Some(transaction) = self.history.pop_undo() { - let mut undone = false; - for (buffer_id, buffer_transaction_id) in &mut transaction.buffer_transactions { - if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { - undone |= buffer.update(cx, |buffer, cx| { - let undo_to = *buffer_transaction_id; - if let Some(entry) = buffer.peek_undo_stack() { - *buffer_transaction_id = entry.transaction_id(); - } - buffer.undo_to_transaction(undo_to, cx) - }); - } - } - - if undone { - transaction_id = Some(transaction.id); - break; - } - } - } - - if let Some(transaction_id) = transaction_id { - cx.emit(Event::TransactionUndone { transaction_id }); - } - - transaction_id - } - - pub fn redo(&mut self, cx: &mut Context) -> Option { - if let Some(buffer) = self.as_singleton() { - return buffer.update(cx, |buffer, cx| buffer.redo(cx)); - } - - while let Some(transaction) = self.history.pop_redo() { - let mut redone = false; - for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions.iter_mut() { - if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { - redone |= buffer.update(cx, |buffer, cx| { - let redo_to = *buffer_transaction_id; - if let Some(entry) = buffer.peek_redo_stack() { - *buffer_transaction_id = entry.transaction_id(); - } - buffer.redo_to_transaction(redo_to, cx) - }); - } - } - - if redone { - return Some(transaction.id); - } - } - - None - } - - pub fn undo_transaction(&mut self, transaction_id: TransactionId, cx: &mut Context) { - if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx)); - } else if let Some(transaction) = self.history.remove_from_undo(transaction_id) { - for (buffer_id, transaction_id) in &transaction.buffer_transactions { - if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { - buffer.update(cx, |buffer, cx| { - buffer.undo_transaction(*transaction_id, cx) - }); - } - } - } - } - - pub fn forget_transaction(&mut self, transaction_id: TransactionId, cx: &mut Context) { - if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, _| { - buffer.forget_transaction(transaction_id); - }); - } else if let Some(transaction) = self.history.forget(transaction_id) { - for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions { - if let Some(state) = self.buffers.get_mut(&buffer_id) { - state.buffer.update(cx, |buffer, _| { - buffer.forget_transaction(buffer_transaction_id); - }); - } - } - } - } -} diff --git a/crates/nc/Cargo.toml b/crates/nc/Cargo.toml deleted file mode 100644 index 534ec2271c..0000000000 --- a/crates/nc/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "nc" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/nc.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -futures.workspace = true -net.workspace = true -smol.workspace = true diff --git a/crates/nc/LICENSE-GPL b/crates/nc/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/nc/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/nc/src/nc.rs b/crates/nc/src/nc.rs deleted file mode 100644 index fccb4d726c..0000000000 --- a/crates/nc/src/nc.rs +++ /dev/null @@ -1,56 +0,0 @@ -use anyhow::Result; - -#[cfg(windows)] -pub fn main(_socket: &str) -> Result<()> { - // It looks like we can't get an async stdio stream on Windows from smol. - // - // We decided to merge this with a panic on Windows since this is only used - // by the experimental Claude Code Agent Server. - // - // We're tracking this internally, and we will address it before shipping the integration. - panic!("--nc isn't yet supported on Windows"); -} - -/// The main function for when Zed is running in netcat mode -#[cfg(not(windows))] -pub fn main(socket: &str) -> Result<()> { - use futures::{AsyncReadExt as _, AsyncWriteExt as _, FutureExt as _, io::BufReader, select}; - use net::async_net::UnixStream; - use smol::{Async, io::AsyncBufReadExt}; - - smol::block_on(async { - let socket_stream = UnixStream::connect(socket).await?; - let (socket_read, mut socket_write) = socket_stream.split(); - let mut socket_reader = BufReader::new(socket_read); - - let mut stdout = Async::new(std::io::stdout())?; - let stdin = Async::new(std::io::stdin())?; - let mut stdin_reader = BufReader::new(stdin); - - let mut socket_line = Vec::new(); - let mut stdin_line = Vec::new(); - - loop { - select! { - bytes_read = socket_reader.read_until(b'\n', &mut socket_line).fuse() => { - if bytes_read? == 0 { - break - } - stdout.write_all(&socket_line).await?; - stdout.flush().await?; - socket_line.clear(); - } - bytes_read = stdin_reader.read_until(b'\n', &mut stdin_line).fuse() => { - if bytes_read? == 0 { - break - } - socket_write.write_all(&stdin_line).await?; - socket_write.flush().await?; - stdin_line.clear(); - } - } - } - - anyhow::Ok(()) - }) -} diff --git a/crates/net/Cargo.toml b/crates/net/Cargo.toml deleted file mode 100644 index 8ce273e30c..0000000000 --- a/crates/net/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "net" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/net.rs" -doctest = false - -[dependencies] -smol.workspace = true - -[target.'cfg(target_os = "windows")'.dependencies] -anyhow.workspace = true -async-io = "2.4" -windows.workspace = true - -[dev-dependencies] -tempfile.workspace = true diff --git a/crates/net/LICENSE-GPL b/crates/net/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/net/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/net/src/async_net.rs b/crates/net/src/async_net.rs deleted file mode 100644 index 6a47902bd8..0000000000 --- a/crates/net/src/async_net.rs +++ /dev/null @@ -1,69 +0,0 @@ -#[cfg(not(target_os = "windows"))] -pub use smol::net::unix::{UnixListener, UnixStream}; - -#[cfg(target_os = "windows")] -pub use windows::{UnixListener, UnixStream}; - -#[cfg(target_os = "windows")] -pub mod windows { - use std::{ - io::Result, - path::Path, - pin::Pin, - task::{Context, Poll}, - }; - - use smol::{ - Async, - io::{AsyncRead, AsyncWrite}, - }; - - pub struct UnixListener(Async); - - impl UnixListener { - pub fn bind>(path: P) -> Result { - Ok(UnixListener(Async::new(crate::UnixListener::bind(path)?)?)) - } - - pub async fn accept(&self) -> Result<(UnixStream, ())> { - let (sock, _) = self.0.read_with(|listener| listener.accept()).await?; - Ok((UnixStream(Async::new(sock)?), ())) - } - } - - pub struct UnixStream(Async); - - impl UnixStream { - pub async fn connect>(path: P) -> Result { - Ok(UnixStream(Async::new(crate::UnixStream::connect(path)?)?)) - } - } - - impl AsyncRead for UnixStream { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut [u8], - ) -> Poll> { - Pin::new(&mut self.0).poll_read(cx, buf) - } - } - - impl AsyncWrite for UnixStream { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - Pin::new(&mut self.0).poll_write(cx, buf) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(&mut self.0).poll_flush(cx) - } - - fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(&mut self.0).poll_close(cx) - } - } -} diff --git a/crates/net/src/listener.rs b/crates/net/src/listener.rs deleted file mode 100644 index 4774bb850b..0000000000 --- a/crates/net/src/listener.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::{ - io::Result, - os::windows::io::{AsSocket, BorrowedSocket}, - path::Path, -}; - -use windows::Win32::Networking::WinSock::{SOCKADDR_UN, SOMAXCONN, bind, listen}; - -use crate::{ - socket::UnixSocket, - stream::UnixStream, - util::{init, map_ret, sockaddr_un}, -}; - -pub struct UnixListener(UnixSocket); - -impl UnixListener { - pub fn bind>(path: P) -> Result { - init(); - let socket = UnixSocket::new()?; - let (addr, len) = sockaddr_un(path)?; - unsafe { - map_ret(bind( - socket.as_raw(), - &addr as *const _ as *const _, - len as i32, - ))?; - map_ret(listen(socket.as_raw(), SOMAXCONN as _))?; - } - Ok(Self(socket)) - } - - pub fn accept(&self) -> Result<(UnixStream, ())> { - let mut storage = SOCKADDR_UN::default(); - let mut len = std::mem::size_of_val(&storage) as i32; - let raw = self.0.accept(&mut storage as *mut _ as *mut _, &mut len)?; - Ok((UnixStream::new(raw), ())) - } -} - -impl AsSocket for UnixListener { - fn as_socket(&self) -> BorrowedSocket<'_> { - unsafe { BorrowedSocket::borrow_raw(self.0.as_raw().0 as _) } - } -} diff --git a/crates/net/src/net.rs b/crates/net/src/net.rs deleted file mode 100644 index 4fa76ffcb8..0000000000 --- a/crates/net/src/net.rs +++ /dev/null @@ -1,107 +0,0 @@ -pub mod async_net; -#[cfg(target_os = "windows")] -pub mod listener; -#[cfg(target_os = "windows")] -pub mod socket; -#[cfg(target_os = "windows")] -pub mod stream; -#[cfg(target_os = "windows")] -mod util; - -#[cfg(target_os = "windows")] -pub use listener::*; -#[cfg(target_os = "windows")] -pub use socket::*; -#[cfg(not(target_os = "windows"))] -pub use std::os::unix::net::{UnixListener, UnixStream}; -#[cfg(target_os = "windows")] -pub use stream::*; - -#[cfg(test)] -mod tests { - use std::io::{Read, Write}; - - use smol::io::{AsyncReadExt, AsyncWriteExt}; - - const SERVER_MESSAGE: &str = "Connection closed"; - const CLIENT_MESSAGE: &str = "Hello, server!"; - const BUFFER_SIZE: usize = 32; - - #[test] - fn test_windows_listener() -> std::io::Result<()> { - use crate::{UnixListener, UnixStream}; - - let temp = tempfile::tempdir()?; - let socket = temp.path().join("socket.sock"); - let listener = UnixListener::bind(&socket)?; - - // Server - let server = std::thread::spawn(move || { - let (mut stream, _) = listener.accept().unwrap(); - - // Read data from the client - let mut buffer = [0; BUFFER_SIZE]; - let bytes_read = stream.read(&mut buffer).unwrap(); - let string = String::from_utf8_lossy(&buffer[..bytes_read]); - assert_eq!(string, CLIENT_MESSAGE); - - // Send a message back to the client - stream.write_all(SERVER_MESSAGE.as_bytes()).unwrap(); - }); - - // Client - let mut client = UnixStream::connect(&socket)?; - - // Send data to the server - client.write_all(CLIENT_MESSAGE.as_bytes())?; - let mut buffer = [0; BUFFER_SIZE]; - - // Read the response from the server - let bytes_read = client.read(&mut buffer)?; - let string = String::from_utf8_lossy(&buffer[..bytes_read]); - assert_eq!(string, SERVER_MESSAGE); - client.flush()?; - - server.join().unwrap(); - Ok(()) - } - - #[test] - fn test_unix_listener() -> std::io::Result<()> { - use crate::async_net::{UnixListener, UnixStream}; - - smol::block_on(async { - let temp = tempfile::tempdir()?; - let socket = temp.path().join("socket.sock"); - let listener = UnixListener::bind(&socket)?; - - // Server - let server = smol::spawn(async move { - let (mut stream, _) = listener.accept().await.unwrap(); - - // Read data from the client - let mut buffer = [0; BUFFER_SIZE]; - let bytes_read = stream.read(&mut buffer).await.unwrap(); - let string = String::from_utf8_lossy(&buffer[..bytes_read]); - assert_eq!(string, CLIENT_MESSAGE); - - // Send a message back to the client - stream.write_all(SERVER_MESSAGE.as_bytes()).await.unwrap(); - }); - - // Client - let mut client = UnixStream::connect(&socket).await?; - client.write_all(CLIENT_MESSAGE.as_bytes()).await?; - - // Read the response from the server - let mut buffer = [0; BUFFER_SIZE]; - let bytes_read = client.read(&mut buffer).await?; - let string = String::from_utf8_lossy(&buffer[..bytes_read]); - assert_eq!(string, "Connection closed"); - client.flush().await?; - - server.await; - Ok(()) - }) - } -} diff --git a/crates/net/src/socket.rs b/crates/net/src/socket.rs deleted file mode 100644 index 6a1fa3d4c4..0000000000 --- a/crates/net/src/socket.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::io::{Error, ErrorKind, Result}; - -use windows::Win32::{ - Foundation::{HANDLE, HANDLE_FLAG_INHERIT, HANDLE_FLAGS, SetHandleInformation}, - Networking::WinSock::{ - AF_UNIX, SEND_RECV_FLAGS, SOCK_STREAM, SOCKADDR, SOCKET, WSA_FLAG_OVERLAPPED, - WSAEWOULDBLOCK, WSASocketW, accept, closesocket, recv, send, - }, -}; - -use crate::util::map_ret; - -pub struct UnixSocket(SOCKET); - -impl UnixSocket { - pub fn new() -> Result { - unsafe { - let raw = WSASocketW(AF_UNIX as _, SOCK_STREAM.0, 0, None, 0, WSA_FLAG_OVERLAPPED)?; - SetHandleInformation( - HANDLE(raw.0 as _), - HANDLE_FLAG_INHERIT.0, - HANDLE_FLAGS::default(), - )?; - Ok(Self(raw)) - } - } - - pub(crate) fn as_raw(&self) -> SOCKET { - self.0 - } - - pub fn accept(&self, storage: *mut SOCKADDR, len: &mut i32) -> Result { - match unsafe { accept(self.0, Some(storage), Some(len)) } { - Ok(sock) => Ok(Self(sock)), - Err(err) => { - let wsa_err = unsafe { windows::Win32::Networking::WinSock::WSAGetLastError().0 }; - if wsa_err == WSAEWOULDBLOCK.0 { - Err(Error::new(ErrorKind::WouldBlock, "accept would block")) - } else { - Err(err.into()) - } - } - } - } - - pub(crate) fn recv(&self, buf: &mut [u8]) -> Result { - map_ret(unsafe { recv(self.0, buf, SEND_RECV_FLAGS::default()) }) - } - - pub(crate) fn send(&self, buf: &[u8]) -> Result { - map_ret(unsafe { send(self.0, buf, SEND_RECV_FLAGS::default()) }) - } -} - -impl Drop for UnixSocket { - fn drop(&mut self) { - unsafe { closesocket(self.0) }; - } -} diff --git a/crates/net/src/stream.rs b/crates/net/src/stream.rs deleted file mode 100644 index d8b6852fcf..0000000000 --- a/crates/net/src/stream.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::{ - io::{Read, Result, Write}, - os::windows::io::{AsSocket, BorrowedSocket}, - path::Path, -}; - -use async_io::IoSafe; -use windows::Win32::Networking::WinSock::connect; - -use crate::{ - socket::UnixSocket, - util::{init, map_ret, sockaddr_un}, -}; - -pub struct UnixStream(UnixSocket); - -unsafe impl IoSafe for UnixStream {} - -impl UnixStream { - pub fn new(socket: UnixSocket) -> Self { - Self(socket) - } - - pub fn connect>(path: P) -> Result { - init(); - unsafe { - let inner = UnixSocket::new()?; - let (addr, len) = sockaddr_un(path)?; - - map_ret(connect( - inner.as_raw(), - &addr as *const _ as *const _, - len as i32, - ))?; - Ok(Self(inner)) - } - } -} - -impl Read for UnixStream { - fn read(&mut self, buf: &mut [u8]) -> Result { - self.0.recv(buf) - } -} - -impl Write for UnixStream { - fn write(&mut self, buf: &[u8]) -> Result { - self.0.send(buf) - } - - fn flush(&mut self) -> Result<()> { - Ok(()) - } -} - -impl AsSocket for UnixStream { - fn as_socket(&self) -> BorrowedSocket<'_> { - unsafe { BorrowedSocket::borrow_raw(self.0.as_raw().0 as _) } - } -} diff --git a/crates/net/src/util.rs b/crates/net/src/util.rs deleted file mode 100644 index f454c099c7..0000000000 --- a/crates/net/src/util.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::{ - io::{Error, ErrorKind, Result}, - path::Path, - sync::Once, -}; - -use windows::Win32::Networking::WinSock::{ - ADDRESS_FAMILY, AF_UNIX, SOCKADDR_UN, SOCKET_ERROR, WSAGetLastError, WSAStartup, -}; - -pub(crate) fn init() { - static ONCE: Once = Once::new(); - - ONCE.call_once(|| unsafe { - let mut wsa_data = std::mem::zeroed(); - let result = WSAStartup(0x202, &mut wsa_data); - if result != 0 { - panic!("WSAStartup failed: {}", result); - } - }); -} - -// https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/ -pub(crate) fn sockaddr_un>(path: P) -> Result<(SOCKADDR_UN, usize)> { - let mut addr = SOCKADDR_UN::default(); - addr.sun_family = ADDRESS_FAMILY(AF_UNIX); - - let bytes = path - .as_ref() - .to_str() - .map(|s| s.as_bytes()) - .ok_or(ErrorKind::InvalidInput)?; - - if bytes.contains(&0) { - return Err(Error::new( - ErrorKind::InvalidInput, - "paths may not contain interior null bytes", - )); - } - if bytes.len() >= addr.sun_path.len() { - return Err(Error::new( - ErrorKind::InvalidInput, - "path must be shorter than SUN_LEN", - )); - } - - unsafe { - std::ptr::copy_nonoverlapping( - bytes.as_ptr(), - addr.sun_path.as_mut_ptr().cast(), - bytes.len(), - ); - } - - let mut len = sun_path_offset(&addr) + bytes.len(); - match bytes.first() { - Some(&0) | None => {} - Some(_) => len += 1, - } - Ok((addr, len)) -} - -pub(crate) fn map_ret(ret: i32) -> Result { - if ret == SOCKET_ERROR { - Err(Error::from_raw_os_error(unsafe { WSAGetLastError().0 })) - } else { - Ok(ret as usize) - } -} - -fn sun_path_offset(addr: &SOCKADDR_UN) -> usize { - // Work with an actual instance of the type since using a null pointer is UB - let base = addr as *const _ as usize; - let path = &addr.sun_path as *const _ as usize; - path - base -} diff --git a/crates/node_runtime/Cargo.toml b/crates/node_runtime/Cargo.toml deleted file mode 100644 index dfa40ad666..0000000000 --- a/crates/node_runtime/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "node_runtime" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/node_runtime.rs" -doctest = false - -[features] -test-support = [] - -[dependencies] -anyhow.workspace = true -async-compression.workspace = true -async-tar.workspace = true -async-trait.workspace = true -futures.workspace = true -http_client.workspace = true -log.workspace = true -paths.workspace = true -semver.workspace = true -serde.workspace = true -serde_json.workspace = true -smol.workspace = true -util.workspace = true -watch.workspace = true -which.workspace = true - -[target.'cfg(windows)'.dependencies] -async-std = { version = "1.12.0", features = ["unstable"] } diff --git a/crates/node_runtime/LICENSE-GPL b/crates/node_runtime/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/node_runtime/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs deleted file mode 100644 index 1eb6714500..0000000000 --- a/crates/node_runtime/src/node_runtime.rs +++ /dev/null @@ -1,880 +0,0 @@ -use anyhow::{Context as _, Result, anyhow, bail}; -use async_compression::futures::bufread::GzipDecoder; -use async_tar::Archive; -use futures::{AsyncReadExt, FutureExt as _, channel::oneshot, future::Shared}; -use http_client::{Host, HttpClient, Url}; -use log::Level; -use semver::Version; -use serde::Deserialize; -use smol::io::BufReader; -use smol::{fs, lock::Mutex}; -use std::fmt::Display; -use std::{ - env::{self, consts}, - ffi::OsString, - io, - net::{IpAddr, Ipv4Addr}, - path::{Path, PathBuf}, - process::Output, - sync::Arc, -}; -use util::ResultExt; -use util::archive::extract_zip; - -const NODE_CA_CERTS_ENV_VAR: &str = "NODE_EXTRA_CA_CERTS"; - -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct NodeBinaryOptions { - pub allow_path_lookup: bool, - pub allow_binary_download: bool, - 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>); - -struct NodeRuntimeState { - http: Arc, - instance: Option>, - last_options: Option, - options: watch::Receiver>, - shell_env_loaded: Shared>, -} - -impl NodeRuntime { - pub fn new( - http: Arc, - shell_env_loaded: Option>, - options: watch::Receiver>, - ) -> Self { - NodeRuntime(Arc::new(Mutex::new(NodeRuntimeState { - http, - instance: None, - last_options: None, - options, - shell_env_loaded: shell_env_loaded.unwrap_or(oneshot::channel().1).shared(), - }))) - } - - pub fn unavailable() -> Self { - NodeRuntime(Arc::new(Mutex::new(NodeRuntimeState { - http: Arc::new(http_client::BlockedHttpClient), - instance: None, - last_options: None, - options: watch::channel(Some(NodeBinaryOptions::default())).1, - shell_env_loaded: oneshot::channel().1.shared(), - }))) - } - - async fn instance(&self) -> Box { - 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.changed().await { - Ok(()) => {} - // failure case not cached - Err(err) => { - return Box::new(UnavailableNodeRuntime { - error_message: err.to_string().into(), - }); - } - } - }; - - if state.last_options.as_ref() != Some(&options) { - state.instance.take(); - } - if let Some(instance) = state.instance.as_ref() { - return instance.boxed_clone(); - } - - if let Some((node, npm)) = options.use_paths.as_ref() { - let instance = match SystemNodeRuntime::new(node.clone(), npm.clone()).await { - Ok(instance) => { - log::info!("using Node.js from `node.path` in settings: {:?}", instance); - Box::new(instance) - } - Err(err) => { - // failure case not cached, since it's cheap to check again - return Box::new(UnavailableNodeRuntime { - error_message: format!( - "failure checking Node.js from `node.path` in settings ({}): {:?}", - node.display(), - err - ) - .into(), - }); - } - }; - state.instance = Some(instance.boxed_clone()); - state.last_options = Some(options); - return instance; - } - - let system_node_error = if options.allow_path_lookup { - state.shell_env_loaded.clone().await.ok(); - match SystemNodeRuntime::detect().await { - Ok(instance) => { - log::info!("using Node.js found on PATH: {:?}", instance); - state.instance = Some(instance.boxed_clone()); - state.last_options = Some(options); - return Box::new(instance); - } - Err(err) => Some(err), - } - } else { - None - }; - - let instance = if options.allow_binary_download { - let (log_level, why_using_managed) = match system_node_error { - Some(err @ DetectError::Other(_)) => (Level::Warn, err.to_string()), - Some(err @ DetectError::NotInPath(_)) => (Level::Info, err.to_string()), - None => ( - Level::Info, - "`node.ignore_system_version` is `true` in settings".to_string(), - ), - }; - match ManagedNodeRuntime::install_if_needed(&state.http).await { - Ok(instance) => { - log::log!( - log_level, - "using Zed managed Node.js at {} since {}", - instance.installation_path.display(), - why_using_managed - ); - Box::new(instance) as Box - } - Err(err) => { - // failure case is cached, since downloading + installing may be expensive. The - // downside of this is that it may fail due to an intermittent network issue. - // - // TODO: Have `install_if_needed` indicate which failure cases are retryable - // and/or have shared tracking of when internet is available. - Box::new(UnavailableNodeRuntime { - error_message: format!( - "failure while downloading and/or installing Zed managed Node.js, \ - restart Zed to retry: {}", - err - ) - .into(), - }) as Box - } - } - } else if let Some(system_node_error) = system_node_error { - // failure case not cached, since it's cheap to check again - // - // TODO: When support is added for setting `options.allow_binary_download`, update this - // error message. - return Box::new(UnavailableNodeRuntime { - error_message: format!( - "failure while checking system Node.js from PATH: {}", - system_node_error - ) - .into(), - }); - } else { - // failure case is cached because it will always happen with these options - // - // TODO: When support is added for setting `options.allow_binary_download`, update this - // error message. - Box::new(UnavailableNodeRuntime { - error_message: "`node` settings do not allow any way to use Node.js" - .to_string() - .into(), - }) - }; - - state.instance = Some(instance.boxed_clone()); - state.last_options = Some(options); - instance - } - - pub async fn binary_path(&self) -> Result { - self.instance().await.binary_path() - } - - pub async fn run_npm_subcommand( - &self, - directory: Option<&Path>, - subcommand: &str, - args: &[&str], - ) -> Result { - let http = self.0.lock().await.http.clone(); - self.instance() - .await - .run_npm_subcommand(directory, http.proxy(), subcommand, args) - .await - } - - pub async fn npm_package_installed_version( - &self, - local_package_directory: &Path, - name: &str, - ) -> Result> { - self.instance() - .await - .npm_package_installed_version(local_package_directory, name) - .await - } - - pub async fn npm_package_latest_version(&self, name: &str) -> Result { - let http = self.0.lock().await.http.clone(); - let output = self - .instance() - .await - .run_npm_subcommand( - None, - http.proxy(), - "info", - &[ - name, - "--json", - "--fetch-retry-mintimeout", - "2000", - "--fetch-retry-maxtimeout", - "5000", - "--fetch-timeout", - "5000", - ], - ) - .await?; - - let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; - info.dist_tags - .latest - .or_else(|| info.versions.pop()) - .with_context(|| format!("no version found for npm package {name}")) - } - - pub async fn npm_install_packages( - &self, - directory: &Path, - packages: &[(&str, &str)], - ) -> Result<()> { - if packages.is_empty() { - return Ok(()); - } - - let packages: Vec<_> = packages - .iter() - .map(|(name, version)| format!("{name}@{version}")) - .collect(); - - let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect(); - arguments.extend_from_slice(&[ - "--save-exact", - "--fetch-retry-mintimeout", - "2000", - "--fetch-retry-maxtimeout", - "5000", - "--fetch-timeout", - "5000", - ]); - - // This is also wrong because the directory is wrong. - self.run_npm_subcommand(Some(directory), "install", &arguments) - .await?; - Ok(()) - } - - pub async fn should_install_npm_package( - &self, - package_name: &str, - local_executable_path: &Path, - local_package_directory: &Path, - version_strategy: VersionStrategy<'_>, - ) -> 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, - // we attempt to install the package. - if fs::metadata(local_executable_path).await.is_err() { - return true; - } - - let Some(installed_version) = self - .npm_package_installed_version(local_package_directory, package_name) - .await - .log_err() - .flatten() - else { - return true; - }; - - let Some(installed_version) = Version::parse(&installed_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 - } - } - } -} - -enum ArchiveType { - TarGz, - Zip, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct NpmInfo { - #[serde(default)] - dist_tags: NpmInfoDistTags, - versions: Vec, -} - -#[derive(Debug, Deserialize, Default)] -pub struct NpmInfoDistTags { - latest: Option, -} - -#[async_trait::async_trait] -trait NodeRuntimeTrait: Send + Sync { - fn boxed_clone(&self) -> Box; - fn binary_path(&self) -> Result; - - async fn run_npm_subcommand( - &self, - directory: Option<&Path>, - proxy: Option<&Url>, - subcommand: &str, - args: &[&str], - ) -> Result; - - async fn npm_package_installed_version( - &self, - local_package_directory: &Path, - name: &str, - ) -> Result>; -} - -#[derive(Clone)] -struct ManagedNodeRuntime { - installation_path: PathBuf, -} - -impl ManagedNodeRuntime { - const VERSION: &str = "v24.11.0"; - - #[cfg(not(windows))] - const NODE_PATH: &str = "bin/node"; - #[cfg(windows)] - const NODE_PATH: &str = "node.exe"; - - #[cfg(not(windows))] - const NPM_PATH: &str = "bin/npm"; - #[cfg(windows)] - const NPM_PATH: &str = "node_modules/npm/bin/npm-cli.js"; - - async fn install_if_needed(http: &Arc) -> Result { - log::info!("Node runtime install_if_needed"); - - let os = match consts::OS { - "macos" => "darwin", - "linux" => "linux", - "windows" => "win", - other => bail!("Running on unsupported os: {other}"), - }; - - let arch = match consts::ARCH { - "x86_64" => "x64", - "aarch64" => "arm64", - other => bail!("Running on unsupported architecture: {other}"), - }; - - let version = Self::VERSION; - let folder_name = format!("node-{version}-{os}-{arch}"); - let node_containing_dir = paths::data_dir().join("node"); - let node_dir = node_containing_dir.join(folder_name); - let node_binary = node_dir.join(Self::NODE_PATH); - let npm_file = node_dir.join(Self::NPM_PATH); - let node_ca_certs = env::var(NODE_CA_CERTS_ENV_VAR).unwrap_or_else(|_| String::new()); - - let valid = if fs::metadata(&node_binary).await.is_ok() { - let result = util::command::new_smol_command(&node_binary) - .env(NODE_CA_CERTS_ENV_VAR, node_ca_certs) - .arg(npm_file) - .arg("--version") - .args(["--cache".into(), node_dir.join("cache")]) - .args(["--userconfig".into(), node_dir.join("blank_user_npmrc")]) - .args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")]) - .output() - .await; - match result { - Ok(output) => { - if output.status.success() { - true - } else { - log::warn!( - "Zed managed Node.js binary at {} failed check with output: {:?}", - node_binary.display(), - output - ); - false - } - } - Err(err) => { - log::warn!( - "Zed managed Node.js binary at {} failed check, so re-downloading it. \ - Error: {}", - node_binary.display(), - err - ); - false - } - } - } else { - false - }; - - if !valid { - _ = fs::remove_dir_all(&node_containing_dir).await; - fs::create_dir(&node_containing_dir) - .await - .context("error creating node containing dir")?; - - let archive_type = match consts::OS { - "macos" | "linux" => ArchiveType::TarGz, - "windows" => ArchiveType::Zip, - other => bail!("Running on unsupported os: {other}"), - }; - - let version = Self::VERSION; - let file_name = format!( - "node-{version}-{os}-{arch}.{extension}", - extension = match archive_type { - ArchiveType::TarGz => "tar.gz", - ArchiveType::Zip => "zip", - } - ); - - let url = format!("https://nodejs.org/dist/{version}/{file_name}"); - log::info!("Downloading Node.js binary from {url}"); - let mut response = http - .get(&url, Default::default(), true) - .await - .context("error downloading Node binary tarball")?; - log::info!("Download of Node.js complete, extracting..."); - - let body = response.body_mut(); - match archive_type { - ArchiveType::TarGz => { - let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); - let archive = Archive::new(decompressed_bytes); - archive.unpack(&node_containing_dir).await?; - } - ArchiveType::Zip => extract_zip(&node_containing_dir, body).await?, - } - log::info!("Extracted Node.js to {}", node_containing_dir.display()) - } - - // Note: Not in the `if !valid {}` so we can populate these for existing installations - _ = fs::create_dir(node_dir.join("cache")).await; - _ = fs::write(node_dir.join("blank_user_npmrc"), []).await; - _ = fs::write(node_dir.join("blank_global_npmrc"), []).await; - - anyhow::Ok(ManagedNodeRuntime { - installation_path: node_dir, - }) - } -} - -fn path_with_node_binary_prepended(node_binary: &Path) -> Option { - let existing_path = env::var_os("PATH"); - let node_bin_dir = node_binary.parent().map(|dir| dir.as_os_str()); - match (existing_path, node_bin_dir) { - (Some(existing_path), Some(node_bin_dir)) => { - if let Ok(joined) = env::join_paths( - [PathBuf::from(node_bin_dir)] - .into_iter() - .chain(env::split_paths(&existing_path)), - ) { - Some(joined) - } else { - Some(existing_path) - } - } - (Some(existing_path), None) => Some(existing_path), - (None, Some(node_bin_dir)) => Some(node_bin_dir.to_owned()), - _ => None, - } -} - -#[async_trait::async_trait] -impl NodeRuntimeTrait for ManagedNodeRuntime { - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn binary_path(&self) -> Result { - Ok(self.installation_path.join(Self::NODE_PATH)) - } - - async fn run_npm_subcommand( - &self, - directory: Option<&Path>, - proxy: Option<&Url>, - subcommand: &str, - args: &[&str], - ) -> Result { - let attempt = || async move { - let node_binary = self.installation_path.join(Self::NODE_PATH); - let npm_file = self.installation_path.join(Self::NPM_PATH); - let env_path = path_with_node_binary_prepended(&node_binary).unwrap_or_default(); - - anyhow::ensure!( - smol::fs::metadata(&node_binary).await.is_ok(), - "missing node binary file" - ); - anyhow::ensure!( - smol::fs::metadata(&npm_file).await.is_ok(), - "missing npm file" - ); - - let node_ca_certs = env::var(NODE_CA_CERTS_ENV_VAR).unwrap_or_else(|_| String::new()); - - let mut command = util::command::new_smol_command(node_binary); - command.env("PATH", env_path); - command.env(NODE_CA_CERTS_ENV_VAR, node_ca_certs); - command.arg(npm_file).arg(subcommand); - command.arg(format!( - "--cache={}", - self.installation_path.join("cache").display() - )); - command.args([ - "--userconfig".into(), - self.installation_path.join("blank_user_npmrc"), - ]); - command.args([ - "--globalconfig".into(), - self.installation_path.join("blank_global_npmrc"), - ]); - command.args(args); - configure_npm_command(&mut command, directory, proxy); - command.output().await.map_err(|e| anyhow!("{e}")) - }; - - let mut output = attempt().await; - if output.is_err() { - output = attempt().await; - anyhow::ensure!( - output.is_ok(), - "failed to launch npm subcommand {subcommand} subcommand\nerr: {:?}", - output.err() - ); - } - - if let Ok(output) = &output { - anyhow::ensure!( - output.status.success(), - "failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - } - - output.map_err(|e| anyhow!("{e}")) - } - async fn npm_package_installed_version( - &self, - local_package_directory: &Path, - name: &str, - ) -> Result> { - read_package_installed_version(local_package_directory.join("node_modules"), name).await - } -} - -#[derive(Debug, Clone)] -pub struct SystemNodeRuntime { - node: PathBuf, - npm: PathBuf, - global_node_modules: PathBuf, - scratch_dir: PathBuf, -} - -impl SystemNodeRuntime { - const MIN_VERSION: semver::Version = Version::new(22, 0, 0); - async fn new(node: PathBuf, npm: PathBuf) -> Result { - let output = util::command::new_smol_command(&node) - .arg("--version") - .output() - .await - .with_context(|| format!("running node from {:?}", node))?; - if !output.status.success() { - anyhow::bail!( - "failed to run node --version. stdout: {}, stderr: {}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ); - } - let version_str = String::from_utf8_lossy(&output.stdout); - let version = semver::Version::parse(version_str.trim().trim_start_matches('v'))?; - if version < Self::MIN_VERSION { - anyhow::bail!( - "node at {} is too old. want: {}, got: {}", - node.to_string_lossy(), - Self::MIN_VERSION, - version - ) - } - - let scratch_dir = paths::data_dir().join("node"); - fs::create_dir(&scratch_dir).await.ok(); - fs::create_dir(scratch_dir.join("cache")).await.ok(); - - let mut this = Self { - node, - npm, - global_node_modules: PathBuf::default(), - scratch_dir, - }; - let output = this.run_npm_subcommand(None, None, "root", &["-g"]).await?; - this.global_node_modules = - PathBuf::from(String::from_utf8_lossy(&output.stdout).to_string()); - - Ok(this) - } - - async fn detect() -> std::result::Result { - let node = which::which("node").map_err(DetectError::NotInPath)?; - let npm = which::which("npm").map_err(DetectError::NotInPath)?; - Self::new(node, npm).await.map_err(DetectError::Other) - } -} - -enum DetectError { - NotInPath(which::Error), - Other(anyhow::Error), -} - -impl Display for DetectError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DetectError::NotInPath(err) => { - write!(f, "system Node.js wasn't found on PATH: {}", err) - } - DetectError::Other(err) => { - write!(f, "checking system Node.js failed with error: {}", err) - } - } - } -} - -#[async_trait::async_trait] -impl NodeRuntimeTrait for SystemNodeRuntime { - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn binary_path(&self) -> Result { - Ok(self.node.clone()) - } - - async fn run_npm_subcommand( - &self, - directory: Option<&Path>, - proxy: Option<&Url>, - subcommand: &str, - args: &[&str], - ) -> anyhow::Result { - let node_ca_certs = env::var(NODE_CA_CERTS_ENV_VAR).unwrap_or_else(|_| String::new()); - let mut command = util::command::new_smol_command(self.npm.clone()); - let path = path_with_node_binary_prepended(&self.node).unwrap_or_default(); - command - .env("PATH", path) - .env(NODE_CA_CERTS_ENV_VAR, node_ca_certs) - .arg(subcommand) - .arg(format!( - "--cache={}", - self.scratch_dir.join("cache").display() - )) - .args(args); - configure_npm_command(&mut command, directory, proxy); - let output = command.output().await?; - anyhow::ensure!( - output.status.success(), - "failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - Ok(output) - } - - async fn npm_package_installed_version( - &self, - local_package_directory: &Path, - name: &str, - ) -> Result> { - read_package_installed_version(local_package_directory.join("node_modules"), name).await - // todo: allow returning a globally installed version (requires callers not to hard-code the path) - } -} - -pub async fn read_package_installed_version( - node_module_directory: PathBuf, - name: &str, -) -> Result> { - let package_json_path = node_module_directory.join(name).join("package.json"); - - let mut file = match fs::File::open(package_json_path).await { - Ok(file) => file, - Err(err) => { - if err.kind() == io::ErrorKind::NotFound { - return Ok(None); - } - - Err(err)? - } - }; - - #[derive(Deserialize)] - struct PackageJson { - version: String, - } - - let mut contents = String::new(); - file.read_to_string(&mut contents).await?; - let package_json: PackageJson = serde_json::from_str(&contents)?; - Ok(Some(package_json.version)) -} - -#[derive(Clone)] -pub struct UnavailableNodeRuntime { - error_message: Arc, -} - -#[async_trait::async_trait] -impl NodeRuntimeTrait for UnavailableNodeRuntime { - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - fn binary_path(&self) -> Result { - bail!("{}", self.error_message) - } - - async fn run_npm_subcommand( - &self, - _: Option<&Path>, - _: Option<&Url>, - _: &str, - _: &[&str], - ) -> anyhow::Result { - bail!("{}", self.error_message) - } - - async fn npm_package_installed_version( - &self, - _local_package_directory: &Path, - _: &str, - ) -> Result> { - bail!("{}", self.error_message) - } -} - -fn configure_npm_command( - command: &mut smol::process::Command, - directory: Option<&Path>, - proxy: Option<&Url>, -) { - if let Some(directory) = directory { - command.current_dir(directory); - command.args(["--prefix".into(), directory.to_path_buf()]); - } - - if let Some(mut proxy) = proxy.cloned() { - // Map proxy settings from `http://localhost:10809` to `http://127.0.0.1:10809` - // NodeRuntime without environment information can not parse `localhost` - // correctly. - // TODO: map to `[::1]` if we are using ipv6 - if matches!(proxy.host(), Some(Host::Domain(domain)) if domain.eq_ignore_ascii_case("localhost")) - { - // When localhost is a valid Host, so is `127.0.0.1` - let _ = proxy.set_ip_host(IpAddr::V4(Ipv4Addr::LOCALHOST)); - } - - command.args(["--proxy", proxy.as_str()]); - } - - #[cfg(windows)] - { - // SYSTEMROOT is a critical environment variables for Windows. - if let Some(val) = env::var("SYSTEMROOT") - .context("Missing environment variable: SYSTEMROOT!") - .log_err() - { - command.env("SYSTEMROOT", val); - } - // Without ComSpec, the post-install will always fail. - if let Some(val) = env::var("ComSpec") - .context("Missing environment variable: ComSpec!") - .log_err() - { - command.env("ComSpec", val); - } - } -} - -#[cfg(test)] -mod tests { - use http_client::Url; - - use super::configure_npm_command; - - // Map localhost to 127.0.0.1 - // NodeRuntime without environment information can not parse `localhost` correctly. - #[test] - fn test_configure_npm_command_map_localhost_proxy() { - const CASES: [(&str, &str); 4] = [ - // Map localhost to 127.0.0.1 - ("http://localhost:9090/", "http://127.0.0.1:9090/"), - ("https://google.com/", "https://google.com/"), - ( - "http://username:password@proxy.thing.com:8080/", - "http://username:password@proxy.thing.com:8080/", - ), - // Test when localhost is contained within a different part of the URL - ( - "http://username:localhost@localhost:8080/", - "http://username:localhost@127.0.0.1:8080/", - ), - ]; - - for (proxy, mapped_proxy) in CASES { - let mut dummy = smol::process::Command::new(""); - let proxy = Url::parse(proxy).unwrap(); - configure_npm_command(&mut dummy, None, Some(&proxy)); - let proxy = dummy - .get_args() - .skip_while(|&arg| arg != "--proxy") - .skip(1) - .next(); - let proxy = proxy.expect("Proxy was not passed to Command correctly"); - assert_eq!( - proxy, mapped_proxy, - "Incorrectly mapped localhost to 127.0.0.1" - ); - } - } -} diff --git a/crates/notifications/Cargo.toml b/crates/notifications/Cargo.toml deleted file mode 100644 index 8304c788fd..0000000000 --- a/crates/notifications/Cargo.toml +++ /dev/null @@ -1,44 +0,0 @@ -[package] -name = "notifications" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/notifications.rs" -doctest = false - -[features] -test-support = [ - "channel/test-support", - "collections/test-support", - "gpui/test-support", - "rpc/test-support", -] - -[dependencies] -anyhow.workspace = true -channel.workspace = true -client.workspace = true -component.workspace = true -db.workspace = true -gpui.workspace = true -rpc.workspace = true -sum_tree.workspace = true -time.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -client = { workspace = true, features = ["test-support"] } -collections = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -rpc = { workspace = true, features = ["test-support"] } -settings = { workspace = true, features = ["test-support"] } -util = { workspace = true, features = ["test-support"] } diff --git a/crates/notifications/LICENSE-GPL b/crates/notifications/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/notifications/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs deleted file mode 100644 index 7cae74a729..0000000000 --- a/crates/notifications/src/notification_store.rs +++ /dev/null @@ -1,432 +0,0 @@ -use anyhow::{Context as _, Result}; -use channel::ChannelStore; -use client::{ChannelId, Client, UserStore}; -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 time::OffsetDateTime; -use util::ResultExt; - -pub fn init(client: Arc, user_store: Entity, cx: &mut App) { - let notification_store = cx.new(|cx| NotificationStore::new(client, user_store, cx)); - cx.set_global(GlobalNotificationStore(notification_store)); -} - -struct GlobalNotificationStore(Entity); - -impl Global for GlobalNotificationStore {} - -pub struct NotificationStore { - client: Arc, - user_store: Entity, - channel_store: Entity, - notifications: SumTree, - loaded_all_notifications: bool, - _watch_connection_status: Task>, - _subscriptions: Vec, -} - -#[derive(Clone, PartialEq, Eq, Debug)] -pub enum NotificationEvent { - NotificationsUpdated { - old_range: Range, - new_count: usize, - }, - NewNotification { - entry: NotificationEntry, - }, - NotificationRemoved { - entry: NotificationEntry, - }, - NotificationRead { - entry: NotificationEntry, - }, -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct NotificationEntry { - pub id: u64, - pub notification: Notification, - pub timestamp: OffsetDateTime, - pub is_read: bool, - pub response: Option, -} - -#[derive(Clone, Debug, Default)] -pub struct NotificationSummary { - max_id: u64, - count: usize, - unread_count: usize, -} - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] -struct Count(usize); - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] -struct NotificationId(u64); - -impl NotificationStore { - pub fn global(cx: &App) -> Entity { - cx.global::().0.clone() - } - - pub fn new(client: Arc, user_store: Entity, cx: &mut Context) -> Self { - let mut connection_status = client.status(); - let watch_connection_status = cx.spawn(async move |this, cx| { - while let Some(status) = connection_status.next().await { - let this = this.upgrade()?; - match status { - client::Status::Connected { .. } => { - if let Some(task) = this - .update(cx, |this, cx| this.handle_connect(cx)) - .log_err()? - { - task.await.log_err()?; - } - } - _ => this - .update(cx, |this, cx| this.handle_disconnect(cx)) - .log_err()?, - } - } - Some(()) - }); - - Self { - channel_store: ChannelStore::global(cx), - notifications: Default::default(), - loaded_all_notifications: false, - _watch_connection_status: watch_connection_status, - _subscriptions: vec![ - client.add_message_handler(cx.weak_entity(), Self::handle_new_notification), - client.add_message_handler(cx.weak_entity(), Self::handle_delete_notification), - ], - user_store, - client, - } - } - - pub fn notification_count(&self) -> usize { - self.notifications.summary().count - } - - pub fn unread_notification_count(&self) -> usize { - self.notifications.summary().unread_count - } - - // Get the nth newest notification. - pub fn notification_at(&self, ix: usize) -> Option<&NotificationEntry> { - let count = self.notifications.summary().count; - if ix >= count { - return None; - } - let ix = count - 1 - ix; - let (.., item) = self - .notifications - .find::((), &Count(ix), Bias::Right); - item - } - pub fn notification_for_id(&self, id: u64) -> Option<&NotificationEntry> { - let (.., item) = - self.notifications - .find::((), &NotificationId(id), Bias::Left); - if let Some(item) = item - && item.id == id - { - return Some(item); - } - None - } - - pub fn load_more_notifications( - &self, - clear_old: bool, - cx: &mut Context, - ) -> Option>> { - if self.loaded_all_notifications && !clear_old { - return None; - } - - let before_id = if clear_old { - None - } else { - self.notifications.first().map(|entry| entry.id) - }; - let request = self.client.request(proto::GetNotifications { before_id }); - Some(cx.spawn(async move |this, cx| { - let this = this - .upgrade() - .context("Notification store was dropped while loading notifications")?; - - let response = request.await?; - this.update(cx, |this, _| this.loaded_all_notifications = response.done)?; - Self::add_notifications( - this, - response.notifications, - AddNotificationsOptions { - is_new: false, - clear_old, - includes_first: response.done, - }, - cx, - ) - .await?; - Ok(()) - })) - } - - fn handle_connect(&mut self, cx: &mut Context) -> Option>> { - self.notifications = Default::default(); - cx.notify(); - self.load_more_notifications(true, cx) - } - - fn handle_disconnect(&mut self, cx: &mut Context) { - cx.notify() - } - - async fn handle_new_notification( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - Self::add_notifications( - this, - envelope.payload.notification.into_iter().collect(), - AddNotificationsOptions { - is_new: true, - clear_old: false, - includes_first: false, - }, - &mut cx, - ) - .await - } - - async fn handle_delete_notification( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - this.splice_notifications([(envelope.payload.notification_id, None)], false, cx); - Ok(()) - })? - } - - async fn add_notifications( - this: Entity, - notifications: Vec, - options: AddNotificationsOptions, - cx: &mut AsyncApp, - ) -> Result<()> { - let mut user_ids = Vec::new(); - - let notifications = notifications - .into_iter() - .filter_map(|message| { - Some(NotificationEntry { - id: message.id, - is_read: message.is_read, - timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64) - .ok()?, - notification: Notification::from_proto(&message)?, - response: message.response, - }) - }) - .collect::>(); - if notifications.is_empty() { - return Ok(()); - } - - for entry in ¬ifications { - match entry.notification { - Notification::ChannelInvitation { inviter_id, .. } => { - user_ids.push(inviter_id); - } - Notification::ContactRequest { - sender_id: requester_id, - } => { - user_ids.push(requester_id); - } - Notification::ContactRequestAccepted { - responder_id: contact_id, - } => { - user_ids.push(contact_id); - } - } - } - - let user_store = this.read_with(cx, |this, _| this.user_store.clone())?; - - user_store - .update(cx, |store, cx| store.get_users(user_ids, cx))? - .await?; - this.update(cx, |this, cx| { - if options.clear_old { - cx.emit(NotificationEvent::NotificationsUpdated { - old_range: 0..this.notifications.summary().count, - new_count: 0, - }); - this.notifications = SumTree::default(); - this.loaded_all_notifications = false; - } - - if options.includes_first { - this.loaded_all_notifications = true; - } - - this.splice_notifications( - notifications - .into_iter() - .map(|notification| (notification.id, Some(notification))), - options.is_new, - cx, - ); - }) - .log_err(); - - Ok(()) - } - - fn splice_notifications( - &mut self, - notifications: impl IntoIterator)>, - is_new: bool, - cx: &mut Context, - ) { - let mut cursor = self - .notifications - .cursor::>(()); - let mut new_notifications = SumTree::default(); - let mut old_range = 0..0; - - for (i, (id, new_notification)) in notifications.into_iter().enumerate() { - new_notifications.append(cursor.slice(&NotificationId(id), Bias::Left), ()); - - if i == 0 { - old_range.start = cursor.start().1.0; - } - - let old_notification = cursor.item(); - if let Some(old_notification) = old_notification { - if old_notification.id == id { - cursor.next(); - - if let Some(new_notification) = &new_notification { - if new_notification.is_read { - cx.emit(NotificationEvent::NotificationRead { - entry: new_notification.clone(), - }); - } - } else { - cx.emit(NotificationEvent::NotificationRemoved { - entry: old_notification.clone(), - }); - } - } - } else if let Some(new_notification) = &new_notification - && is_new - { - cx.emit(NotificationEvent::NewNotification { - entry: new_notification.clone(), - }); - } - - if let Some(notification) = new_notification { - new_notifications.push(notification, ()); - } - } - - old_range.end = cursor.start().1.0; - let new_count = new_notifications.summary().count - old_range.start; - new_notifications.append(cursor.suffix(), ()); - drop(cursor); - - self.notifications = new_notifications; - cx.emit(NotificationEvent::NotificationsUpdated { - old_range, - new_count, - }); - } - - pub fn respond_to_notification( - &mut self, - notification: Notification, - response: bool, - cx: &mut Context, - ) { - match notification { - Notification::ContactRequest { sender_id } => { - self.user_store - .update(cx, |store, cx| { - store.respond_to_contact_request(sender_id, response, cx) - }) - .detach(); - } - Notification::ChannelInvitation { channel_id, .. } => { - self.channel_store - .update(cx, |store, cx| { - store.respond_to_channel_invite(ChannelId(channel_id), response, cx) - }) - .detach(); - } - _ => {} - } - } -} - -impl EventEmitter for NotificationStore {} - -impl sum_tree::Item for NotificationEntry { - type Summary = NotificationSummary; - - fn summary(&self, _cx: ()) -> Self::Summary { - NotificationSummary { - max_id: self.id, - count: 1, - unread_count: if self.is_read { 0 } else { 1 }, - } - } -} - -impl sum_tree::ContextLessSummary for NotificationSummary { - fn zero() -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &Self) { - self.max_id = self.max_id.max(summary.max_id); - self.count += summary.count; - self.unread_count += summary.unread_count; - } -} - -impl sum_tree::Dimension<'_, NotificationSummary> for NotificationId { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &NotificationSummary, _: ()) { - debug_assert!(summary.max_id > self.0); - self.0 = summary.max_id; - } -} - -impl sum_tree::Dimension<'_, NotificationSummary> for Count { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &NotificationSummary, _: ()) { - self.0 += summary.count; - } -} - -struct AddNotificationsOptions { - is_new: bool, - clear_old: bool, - includes_first: bool, -} diff --git a/crates/notifications/src/notifications.rs b/crates/notifications/src/notifications.rs deleted file mode 100644 index ee952555eb..0000000000 --- a/crates/notifications/src/notifications.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod notification_store; - -pub use notification_store::*; -pub mod status_toast; diff --git a/crates/notifications/src/status_toast.rs b/crates/notifications/src/status_toast.rs deleted file mode 100644 index 7affa93f5a..0000000000 --- a/crates/notifications/src/status_toast.rs +++ /dev/null @@ -1,254 +0,0 @@ -use std::rc::Rc; - -use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement}; -use ui::{Tooltip, prelude::*}; -use workspace::{ToastAction, ToastView}; -use zed_actions::toast; - -#[derive(Clone, Copy)] -pub struct ToastIcon { - icon: IconName, - color: Color, -} - -impl ToastIcon { - pub fn new(icon: IconName) -> Self { - Self { - icon, - color: Color::default(), - } - } - - pub fn color(mut self, color: Color) -> Self { - self.color = color; - self - } -} - -impl From for ToastIcon { - fn from(icon: IconName) -> Self { - Self { - icon, - color: Color::default(), - } - } -} - -#[derive(RegisterComponent)] -pub struct StatusToast { - icon: Option, - text: SharedString, - action: Option, - show_dismiss: bool, - this_handle: Entity, - focus_handle: FocusHandle, -} - -impl StatusToast { - pub fn new( - text: impl Into, - cx: &mut App, - f: impl FnOnce(Self, &mut Context) -> Self, - ) -> Entity { - cx.new(|cx| { - let focus_handle = cx.focus_handle(); - - f( - Self { - text: text.into(), - icon: None, - action: None, - show_dismiss: false, - this_handle: cx.entity(), - focus_handle, - }, - cx, - ) - }) - } - - pub fn icon(mut self, icon: ToastIcon) -> Self { - self.icon = Some(icon); - self - } - - pub fn action( - mut self, - label: impl Into, - f: impl Fn(&mut Window, &mut App) + 'static, - ) -> Self { - let this_handle = self.this_handle.clone(); - self.action = Some(ToastAction::new( - label.into(), - Some(Rc::new(move |window, cx| { - this_handle.update(cx, |_, cx| { - cx.emit(DismissEvent); - }); - f(window, cx); - })), - )); - self - } - - pub fn dismiss_button(mut self, show: bool) -> Self { - self.show_dismiss = show; - self - } -} - -impl Render for StatusToast { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let has_action_or_dismiss = self.action.is_some() || self.show_dismiss; - - h_flex() - .id("status-toast") - .elevation_3(cx) - .gap_2() - .py_1p5() - .pl_2p5() - .map(|this| { - if has_action_or_dismiss { - this.pr_1p5() - } else { - this.pr_2p5() - } - }) - .flex_none() - .bg(cx.theme().colors().surface_background) - .shadow_lg() - .when_some(self.icon.as_ref(), |this, icon| { - this.child(Icon::new(icon.icon).color(icon.color)) - }) - .child(Label::new(self.text.clone()).color(Color::Default)) - .when_some(self.action.as_ref(), |this, action| { - this.child( - Button::new(action.id.clone(), action.label.clone()) - .tooltip(Tooltip::for_action_title( - action.label.clone(), - &toast::RunAction, - )) - .color(Color::Muted) - .when_some(action.on_click.clone(), |el, handler| { - el.on_click(move |_click_event, window, cx| handler(window, cx)) - }), - ) - }) - .when(self.show_dismiss, |this| { - let handle = self.this_handle.clone(); - this.child( - IconButton::new("dismiss", IconName::Close) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .tooltip(Tooltip::text("Dismiss")) - .on_click(move |_click_event, _window, cx| { - handle.update(cx, |_, cx| { - cx.emit(DismissEvent); - }); - }), - ) - }) - } -} - -impl ToastView for StatusToast { - fn action(&self) -> Option { - self.action.clone() - } -} - -impl Focusable for StatusToast { - fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl EventEmitter for StatusToast {} - -impl Component for StatusToast { - fn scope() -> ComponentScope { - ComponentScope::Notification - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - let text_example = StatusToast::new("Operation completed", cx, |this, _| this); - - let action_example = StatusToast::new("Update ready to install", cx, |this, _cx| { - this.action("Restart", |_, _| {}) - }); - - let dismiss_button_example = - StatusToast::new("Dismiss Button", cx, |this, _| this.dismiss_button(true)); - - let icon_example = StatusToast::new( - "Nathan Sobo accepted your contact request", - cx, - |this, _| this.icon(ToastIcon::new(IconName::Check).color(Color::Muted)), - ); - - let success_example = StatusToast::new("Pushed 4 changes to `zed/main`", cx, |this, _| { - this.icon(ToastIcon::new(IconName::Check).color(Color::Success)) - }); - - let error_example = StatusToast::new( - "git push: Couldn't find remote origin `iamnbutler/zed`", - cx, - |this, _cx| { - this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error)) - .action("More Info", |_, _| {}) - }, - ); - - let warning_example = StatusToast::new("You have outdated settings", cx, |this, _cx| { - this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning)) - .action("More Info", |_, _| {}) - }); - - let pr_example = - StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| { - this.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)) - .action("Open Pull Request", |_, cx| { - cx.open_url("https://github.com/") - }) - }); - - Some( - v_flex() - .gap_6() - .p_4() - .children(vec![ - example_group_with_title( - "Basic Toast", - vec![ - single_example("Text", div().child(text_example).into_any_element()), - single_example( - "Action", - div().child(action_example).into_any_element(), - ), - single_example("Icon", div().child(icon_example).into_any_element()), - single_example( - "Dismiss Button", - div().child(dismiss_button_example).into_any_element(), - ), - ], - ), - example_group_with_title( - "Examples", - vec![ - single_example( - "Success", - div().child(success_example).into_any_element(), - ), - single_example("Error", div().child(error_example).into_any_element()), - single_example( - "Warning", - div().child(warning_example).into_any_element(), - ), - single_example("Create PR", div().child(pr_example).into_any_element()), - ], - ) - .vertical(), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ollama/Cargo.toml b/crates/ollama/Cargo.toml deleted file mode 100644 index fed74993fa..0000000000 --- a/crates/ollama/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "ollama" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/ollama.rs" - -[features] -default = [] -schemars = ["dep:schemars"] - -[dependencies] -anyhow.workspace = true -futures.workspace = true -http_client.workspace = true -schemars = { workspace = true, optional = true } -serde.workspace = true -serde_json.workspace = true -settings.workspace = true diff --git a/crates/ollama/LICENSE-GPL b/crates/ollama/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/ollama/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/ollama/src/ollama.rs b/crates/ollama/src/ollama.rs deleted file mode 100644 index f6614379fa..0000000000 --- a/crates/ollama/src/ollama.rs +++ /dev/null @@ -1,660 +0,0 @@ -use anyhow::{Context as _, Result}; -use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream}; -use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Request as HttpRequest}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -pub use settings::KeepAlive; - -pub const OLLAMA_API_URL: &str = "http://localhost:11434"; - -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] -pub struct Model { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub keep_alive: Option, - pub supports_tools: Option, - pub supports_vision: Option, - pub supports_thinking: Option, -} - -fn get_max_tokens(name: &str) -> u64 { - /// Default context length for unknown models. - const DEFAULT_TOKENS: u64 = 4096; - /// Magic number. Lets many Ollama models work with ~16GB of ram. - /// Models that support context beyond 16k such as codestral (32k) or devstral (128k) will be clamped down to 16k - const MAXIMUM_TOKENS: u64 = 16384; - - match name.split(':').next().unwrap() { - "granite-code" | "phi" | "tinyllama" => 2048, - "llama2" | "stablelm2" | "vicuna" | "yi" => 4096, - "aya" | "codegemma" | "gemma" | "gemma2" | "llama3" | "starcoder" => 8192, - "codellama" | "starcoder2" => 16384, - "codestral" | "dolphin-mixtral" | "llava" | "magistral" | "mistral" | "mixstral" - | "qwen2" | "qwen2.5-coder" => 32768, - "cogito" | "command-r" | "deepseek-coder-v2" | "deepseek-r1" | "deepseek-v3" - | "devstral" | "gemma3" | "gpt-oss" | "granite3.3" | "llama3.1" | "llama3.2" - | "llama3.3" | "mistral-nemo" | "phi3" | "phi3.5" | "phi4" | "qwen3" | "yi-coder" => 128000, - "qwen3-coder" => 256000, - _ => DEFAULT_TOKENS, - } - .clamp(1, MAXIMUM_TOKENS) -} - -impl Model { - pub fn new( - name: &str, - display_name: Option<&str>, - max_tokens: Option, - supports_tools: Option, - supports_vision: Option, - supports_thinking: Option, - ) -> Self { - Self { - name: name.to_owned(), - display_name: display_name - .map(ToString::to_string) - .or_else(|| name.strip_suffix(":latest").map(ToString::to_string)), - max_tokens: max_tokens.unwrap_or_else(|| get_max_tokens(name)), - keep_alive: Some(KeepAlive::indefinite()), - supports_tools, - supports_vision, - supports_thinking, - } - } - - pub fn id(&self) -> &str { - &self.name - } - - pub fn display_name(&self) -> &str { - self.display_name.as_ref().unwrap_or(&self.name) - } - - pub fn max_token_count(&self) -> u64 { - self.max_tokens - } -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(tag = "role", rename_all = "lowercase")] -pub enum ChatMessage { - Assistant { - content: String, - tool_calls: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - images: Option>, - thinking: Option, - }, - User { - content: String, - #[serde(skip_serializing_if = "Option::is_none")] - images: Option>, - }, - System { - content: String, - }, - Tool { - tool_name: String, - content: String, - }, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct OllamaToolCall { - // TODO: Remove `Option` after most users have updated to Ollama v0.12.10, - // which was released on the 4th of November 2025 - pub id: Option, - pub function: OllamaFunctionCall, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct OllamaFunctionCall { - pub name: String, - pub arguments: Value, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct OllamaFunctionTool { - pub name: String, - pub description: Option, - pub parameters: Option, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum OllamaTool { - Function { function: OllamaFunctionTool }, -} - -#[derive(Serialize, Debug)] -pub struct ChatRequest { - pub model: String, - pub messages: Vec, - pub stream: bool, - pub keep_alive: KeepAlive, - pub options: Option, - pub tools: Vec, - pub think: Option, -} - -// https://github.com/ollama/ollama/blob/main/docs/modelfile.md#valid-parameters-and-values -#[derive(Serialize, Default, Debug)] -pub struct ChatOptions { - pub num_ctx: Option, - pub num_predict: Option, - pub stop: Option>, - pub temperature: Option, - pub top_p: Option, -} - -#[derive(Deserialize, Debug)] -pub struct ChatResponseDelta { - pub model: String, - pub created_at: String, - pub message: ChatMessage, - pub done_reason: Option, - pub done: bool, - pub prompt_eval_count: Option, - pub eval_count: Option, -} - -#[derive(Serialize, Deserialize)] -pub struct LocalModelsResponse { - pub models: Vec, -} - -#[derive(Serialize, Deserialize)] -pub struct LocalModelListing { - pub name: String, - pub modified_at: String, - pub size: u64, - pub digest: String, - pub details: ModelDetails, -} - -#[derive(Serialize, Deserialize)] -pub struct LocalModel { - pub modelfile: String, - pub parameters: String, - pub template: String, - pub details: ModelDetails, -} - -#[derive(Serialize, Deserialize)] -pub struct ModelDetails { - pub format: String, - pub family: String, - pub families: Option>, - pub parameter_size: String, - pub quantization_level: String, -} - -#[derive(Debug)] -pub struct ModelShow { - pub capabilities: Vec, - pub context_length: Option, - pub architecture: Option, -} - -impl<'de> Deserialize<'de> for ModelShow { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - use serde::de::{self, MapAccess, Visitor}; - use std::fmt; - - struct ModelShowVisitor; - - impl<'de> Visitor<'de> for ModelShowVisitor { - type Value = ModelShow; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a ModelShow object") - } - - fn visit_map

Some text link more text

(self, mut map: A) -> Result - where - A: MapAccess<'de>, - { - let mut capabilities: Vec = Vec::new(); - let mut architecture: Option = None; - let mut context_length: Option = None; - - while let Some(key) = map.next_key::()? { - match key.as_str() { - "capabilities" => { - capabilities = map.next_value()?; - } - "model_info" => { - let model_info: Value = map.next_value()?; - if let Value::Object(obj) = model_info { - architecture = obj - .get("general.architecture") - .and_then(|v| v.as_str()) - .map(String::from); - - if let Some(arch) = &architecture { - context_length = obj - .get(&format!("{}.context_length", arch)) - .and_then(|v| v.as_u64()); - } - } - } - _ => { - let _: de::IgnoredAny = map.next_value()?; - } - } - } - - Ok(ModelShow { - capabilities, - context_length, - architecture, - }) - } - } - - deserializer.deserialize_map(ModelShowVisitor) - } -} - -impl ModelShow { - pub fn supports_tools(&self) -> bool { - // .contains expects &String, which would require an additional allocation - self.capabilities.iter().any(|v| v == "tools") - } - - pub fn supports_vision(&self) -> bool { - self.capabilities.iter().any(|v| v == "vision") - } - - pub fn supports_thinking(&self) -> bool { - self.capabilities.iter().any(|v| v == "thinking") - } -} - -pub async fn stream_chat_completion( - client: &dyn HttpClient, - api_url: &str, - api_key: Option<&str>, - request: ChatRequest, -) -> Result>> { - let uri = format!("{api_url}/api/chat"); - let request = HttpRequest::builder() - .method(Method::POST) - .uri(uri) - .header("Content-Type", "application/json") - .when_some(api_key, |builder, api_key| { - builder.header("Authorization", format!("Bearer {api_key}")) - }) - .body(AsyncBody::from(serde_json::to_string(&request)?))?; - - let mut response = client.send(request).await?; - if response.status().is_success() { - let reader = BufReader::new(response.into_body()); - - Ok(reader - .lines() - .map(|line| match line { - Ok(line) => serde_json::from_str(&line).context("Unable to parse chat response"), - Err(e) => Err(e.into()), - }) - .boxed()) - } else { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - anyhow::bail!( - "Failed to connect to Ollama API: {} {}", - response.status(), - body, - ); - } -} - -pub async fn get_models( - client: &dyn HttpClient, - api_url: &str, - api_key: Option<&str>, -) -> Result> { - let uri = format!("{api_url}/api/tags"); - let request = HttpRequest::builder() - .method(Method::GET) - .uri(uri) - .header("Accept", "application/json") - .when_some(api_key, |builder, api_key| { - builder.header("Authorization", format!("Bearer {api_key}")) - }) - .body(AsyncBody::default())?; - - let mut response = client.send(request).await?; - - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - anyhow::ensure!( - response.status().is_success(), - "Failed to connect to Ollama API: {} {}", - response.status(), - body, - ); - let response: LocalModelsResponse = - serde_json::from_str(&body).context("Unable to parse Ollama tag listing")?; - Ok(response.models) -} - -/// Fetch details of a model, used to determine model capabilities -pub async fn show_model( - client: &dyn HttpClient, - api_url: &str, - api_key: Option<&str>, - model: &str, -) -> Result { - let uri = format!("{api_url}/api/show"); - let request = HttpRequest::builder() - .method(Method::POST) - .uri(uri) - .header("Content-Type", "application/json") - .when_some(api_key, |builder, api_key| { - builder.header("Authorization", format!("Bearer {api_key}")) - }) - .body(AsyncBody::from( - serde_json::json!({ "model": model }).to_string(), - ))?; - - let mut response = client.send(request).await?; - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - anyhow::ensure!( - response.status().is_success(), - "Failed to connect to Ollama API: {} {}", - response.status(), - body, - ); - let details: ModelShow = serde_json::from_str(body.as_str())?; - Ok(details) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_completion() { - let response = serde_json::json!({ - "model": "llama3.2", - "created_at": "2023-12-12T14:13:43.416799Z", - "message": { - "role": "assistant", - "content": "Hello! How are you today?" - }, - "done": true, - "total_duration": 5191566416u64, - "load_duration": 2154458, - "prompt_eval_count": 26, - "prompt_eval_duration": 383809000, - "eval_count": 298, - "eval_duration": 4799921000u64 - }); - let _: ChatResponseDelta = serde_json::from_value(response).unwrap(); - } - - #[test] - fn parse_streaming_completion() { - let partial = serde_json::json!({ - "model": "llama3.2", - "created_at": "2023-08-04T08:52:19.385406455-07:00", - "message": { - "role": "assistant", - "content": "The", - "images": null - }, - "done": false - }); - - let _: ChatResponseDelta = serde_json::from_value(partial).unwrap(); - - let last = serde_json::json!({ - "model": "llama3.2", - "created_at": "2023-08-04T19:22:45.499127Z", - "message": { - "role": "assistant", - "content": "" - }, - "done": true, - "total_duration": 4883583458u64, - "load_duration": 1334875, - "prompt_eval_count": 26, - "prompt_eval_duration": 342546000, - "eval_count": 282, - "eval_duration": 4535599000u64 - }); - - let _: ChatResponseDelta = serde_json::from_value(last).unwrap(); - } - - #[test] - fn parse_tool_call() { - let response = serde_json::json!({ - "model": "llama3.2:3b", - "created_at": "2025-04-28T20:02:02.140489Z", - "message": { - "role": "assistant", - "content": "", - "tool_calls": [ - { - "id": "call_llama3.2:3b_145155", - "function": { - "name": "weather", - "arguments": { - "city": "london", - } - } - } - ] - }, - "done_reason": "stop", - "done": true, - "total_duration": 2758629166u64, - "load_duration": 1770059875, - "prompt_eval_count": 147, - "prompt_eval_duration": 684637583, - "eval_count": 16, - "eval_duration": 302561917, - }); - - let result: ChatResponseDelta = serde_json::from_value(response).unwrap(); - match result.message { - ChatMessage::Assistant { - content, - tool_calls, - images: _, - thinking, - } => { - assert!(content.is_empty()); - assert!(tool_calls.is_some_and(|v| !v.is_empty())); - assert!(thinking.is_none()); - } - _ => panic!("Deserialized wrong role"), - } - } - - // Backwards compatibility with Ollama versions prior to v0.12.10 November 2025 - // This test is a copy of `parse_tool_call()` with the `id` field omitted. - #[test] - fn parse_tool_call_pre_0_12_10() { - let response = serde_json::json!({ - "model": "llama3.2:3b", - "created_at": "2025-04-28T20:02:02.140489Z", - "message": { - "role": "assistant", - "content": "", - "tool_calls": [ - { - "function": { - "name": "weather", - "arguments": { - "city": "london", - } - } - } - ] - }, - "done_reason": "stop", - "done": true, - "total_duration": 2758629166u64, - "load_duration": 1770059875, - "prompt_eval_count": 147, - "prompt_eval_duration": 684637583, - "eval_count": 16, - "eval_duration": 302561917, - }); - - let result: ChatResponseDelta = serde_json::from_value(response).unwrap(); - match result.message { - ChatMessage::Assistant { - content, - tool_calls: Some(tool_calls), - images: _, - thinking, - } => { - assert!(content.is_empty()); - assert!(thinking.is_none()); - - // When the `Option` around `id` is removed, this test should complain - // and be subsequently deleted in favor of `parse_tool_call()` - assert!(tool_calls.first().is_some_and(|call| call.id.is_none())) - } - _ => panic!("Deserialized wrong role"), - } - } - - #[test] - fn parse_show_model() { - let response = serde_json::json!({ - "license": "LLAMA 3.2 COMMUNITY LICENSE AGREEMENT...", - "details": { - "parent_model": "", - "format": "gguf", - "family": "llama", - "families": ["llama"], - "parameter_size": "3.2B", - "quantization_level": "Q4_K_M" - }, - "model_info": { - "general.architecture": "llama", - "general.basename": "Llama-3.2", - "general.file_type": 15, - "general.finetune": "Instruct", - "general.languages": ["en", "de", "fr", "it", "pt", "hi", "es", "th"], - "general.parameter_count": 3212749888u64, - "general.quantization_version": 2, - "general.size_label": "3B", - "general.tags": ["facebook", "meta", "pytorch", "llama", "llama-3", "text-generation"], - "general.type": "model", - "llama.attention.head_count": 24, - "llama.attention.head_count_kv": 8, - "llama.attention.key_length": 128, - "llama.attention.layer_norm_rms_epsilon": 0.00001, - "llama.attention.value_length": 128, - "llama.block_count": 28, - "llama.context_length": 131072, - "llama.embedding_length": 3072, - "llama.feed_forward_length": 8192, - "llama.rope.dimension_count": 128, - "llama.rope.freq_base": 500000, - "llama.vocab_size": 128256, - "tokenizer.ggml.bos_token_id": 128000, - "tokenizer.ggml.eos_token_id": 128009, - "tokenizer.ggml.merges": null, - "tokenizer.ggml.model": "gpt2", - "tokenizer.ggml.pre": "llama-bpe", - "tokenizer.ggml.token_type": null, - "tokenizer.ggml.tokens": null - }, - "tensors": [ - { "name": "rope_freqs.weight", "type": "F32", "shape": [64] }, - { "name": "token_embd.weight", "type": "Q4_K_S", "shape": [3072, 128256] } - ], - "capabilities": ["completion", "tools"], - "modified_at": "2025-04-29T21:24:41.445877632+03:00" - }); - - let result: ModelShow = serde_json::from_value(response).unwrap(); - assert!(result.supports_tools()); - assert!(result.capabilities.contains(&"tools".to_string())); - assert!(result.capabilities.contains(&"completion".to_string())); - - assert_eq!(result.architecture, Some("llama".to_string())); - assert_eq!(result.context_length, Some(131072)); - } - - #[test] - fn serialize_chat_request_with_images() { - let base64_image = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; - - let request = ChatRequest { - model: "llava".to_string(), - messages: vec![ChatMessage::User { - content: "What do you see in this image?".to_string(), - images: Some(vec![base64_image.to_string()]), - }], - stream: false, - keep_alive: KeepAlive::default(), - options: None, - think: None, - tools: vec![], - }; - - let serialized = serde_json::to_string(&request).unwrap(); - assert!(serialized.contains("images")); - assert!(serialized.contains(base64_image)); - } - - #[test] - fn serialize_chat_request_without_images() { - let request = ChatRequest { - model: "llama3.2".to_string(), - messages: vec![ChatMessage::User { - content: "Hello, world!".to_string(), - images: None, - }], - stream: false, - keep_alive: KeepAlive::default(), - options: None, - think: None, - tools: vec![], - }; - - let serialized = serde_json::to_string(&request).unwrap(); - assert!(!serialized.contains("images")); - } - - #[test] - fn test_json_format_with_images() { - let base64_image = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; - - let request = ChatRequest { - model: "llava".to_string(), - messages: vec![ChatMessage::User { - content: "What do you see?".to_string(), - images: Some(vec![base64_image.to_string()]), - }], - stream: false, - keep_alive: KeepAlive::default(), - options: None, - think: None, - tools: vec![], - }; - - let serialized = serde_json::to_string(&request).unwrap(); - - let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap(); - let message_images = parsed["messages"][0]["images"].as_array().unwrap(); - assert_eq!(message_images.len(), 1); - assert_eq!(message_images[0].as_str().unwrap(), base64_image); - } -} diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml deleted file mode 100644 index 2ff3467c48..0000000000 --- a/crates/onboarding/Cargo.toml +++ /dev/null @@ -1,44 +0,0 @@ -[package] -name = "onboarding" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/onboarding.rs" - -[features] -default = [] - -[dependencies] -anyhow.workspace = true -client.workspace = true -component.workspace = true -db.workspace = true -documented.workspace = true -fs.workspace = true -fuzzy.workspace = true -git.workspace = true -gpui.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 -vim_mode_setting.workspace = true -workspace.workspace = true -zed_actions.workspace = true -zlog.workspace = true - -[dev-dependencies] -db = {workspace = true, features = ["test-support"]} diff --git a/crates/onboarding/LICENSE-GPL b/crates/onboarding/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/onboarding/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/onboarding/src/base_keymap_picker.rs b/crates/onboarding/src/base_keymap_picker.rs deleted file mode 100644 index 63a2894a93..0000000000 --- a/crates/onboarding/src/base_keymap_picker.rs +++ /dev/null @@ -1,229 +0,0 @@ -use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; -use gpui::{ - App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, Task, WeakEntity, Window, - actions, -}; -use picker::{Picker, PickerDelegate}; -use project::Fs; -use settings::{BaseKeymap, Settings, update_settings_file}; -use std::sync::Arc; -use ui::{ListItem, ListItemSpacing, prelude::*}; -use util::ResultExt; -use workspace::{ModalView, Workspace, ui::HighlightedLabel}; - -actions!( - zed, - [ - /// Toggles the base keymap selector modal. - ToggleBaseKeymapSelector - ] -); - -pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, _window, _cx| { - workspace.register_action(toggle); - }) - .detach(); -} - -pub fn toggle( - workspace: &mut Workspace, - _: &ToggleBaseKeymapSelector, - window: &mut Window, - cx: &mut Context, -) { - let fs = workspace.app_state().fs.clone(); - workspace.toggle_modal(window, cx, |window, cx| { - BaseKeymapSelector::new( - BaseKeymapSelectorDelegate::new(cx.entity().downgrade(), fs, cx), - window, - cx, - ) - }); -} - -pub struct BaseKeymapSelector { - picker: Entity>, -} - -impl Focusable for BaseKeymapSelector { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl EventEmitter for BaseKeymapSelector {} -impl ModalView for BaseKeymapSelector {} - -impl BaseKeymapSelector { - pub fn new( - delegate: BaseKeymapSelectorDelegate, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); - Self { picker } - } -} - -impl Render for BaseKeymapSelector { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - v_flex().w(rems(34.)).child(self.picker.clone()) - } -} - -pub struct BaseKeymapSelectorDelegate { - selector: WeakEntity, - matches: Vec, - selected_index: usize, - fs: Arc, -} - -impl BaseKeymapSelectorDelegate { - fn new( - selector: WeakEntity, - fs: Arc, - cx: &mut Context, - ) -> Self { - let base = BaseKeymap::get(None, cx); - let selected_index = BaseKeymap::OPTIONS - .iter() - .position(|(_, value)| value == base) - .unwrap_or(0); - Self { - selector, - matches: Vec::new(), - selected_index, - fs, - } - } -} - -impl PickerDelegate for BaseKeymapSelectorDelegate { - type ListItem = ui::ListItem; - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Select a base keymap...".into() - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _window: &mut Window, - _: &mut Context>, - ) { - self.selected_index = ix; - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - let background = cx.background_executor().clone(); - let candidates = BaseKeymap::names() - .enumerate() - .map(|(id, name)| StringMatchCandidate::new(id, name)) - .collect::>(); - - cx.spawn_in(window, async move |this, cx| { - let matches = if query.is_empty() { - candidates - .into_iter() - .enumerate() - .map(|(index, candidate)| StringMatch { - candidate_id: index, - string: candidate.string, - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - match_strings( - &candidates, - &query, - false, - true, - 100, - &Default::default(), - background, - ) - .await - }; - - this.update(cx, |this, _| { - this.delegate.matches = matches; - this.delegate.selected_index = this - .delegate - .selected_index - .min(this.delegate.matches.len().saturating_sub(1)); - }) - .log_err(); - }) - } - - fn confirm( - &mut self, - _: bool, - _: &mut Window, - cx: &mut Context>, - ) { - if let Some(selection) = self.matches.get(self.selected_index) { - let base_keymap = BaseKeymap::from_names(&selection.string); - - telemetry::event!( - "Settings Changed", - setting = "keymap", - value = base_keymap.to_string() - ); - - update_settings_file(self.fs.clone(), cx, move |setting, _| { - setting.base_keymap = Some(base_keymap.into()) - }); - } - - self.selector - .update(cx, |_, cx| { - cx.emit(DismissEvent); - }) - .ok(); - } - - fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { - self.selector - .update(cx, |_, cx| { - cx.emit(DismissEvent); - }) - .log_err(); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _window: &mut Window, - _cx: &mut Context>, - ) -> Option { - let keymap_match = &self.matches.get(ix)?; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child(HighlightedLabel::new( - keymap_match.string.clone(), - keymap_match.positions.clone(), - )), - ) - } -} diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs deleted file mode 100644 index ab5d578f7d..0000000000 --- a/crates/onboarding/src/basics_page.rs +++ /dev/null @@ -1,486 +0,0 @@ -use std::sync::Arc; - -use client::TelemetrySettings; -use fs::Fs; -use gpui::{Action, App, IntoElement}; -use settings::{BaseKeymap, Settings, update_settings_file}; -use theme::{ - Appearance, SystemAppearance, ThemeAppearanceMode, ThemeName, ThemeRegistry, ThemeSelection, - ThemeSettings, -}; -use ui::{ - Divider, ParentElement as _, StatefulInteractiveElement, SwitchField, TintColor, - ToggleButtonGroup, ToggleButtonGroupSize, ToggleButtonSimple, ToggleButtonWithIcon, prelude::*, - rems_from_px, -}; -use vim_mode_setting::VimModeSetting; - -use crate::{ - ImportCursorSettings, ImportVsCodeSettings, SettingsImportState, - theme_preview::{ThemePreviewStyle, 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 { - let theme_selection = ThemeSettings::get_global(cx).theme.clone(); - let system_appearance = theme::SystemAppearance::global(cx); - - let theme_mode = theme_selection - .mode() - .unwrap_or_else(|| match *system_appearance { - Appearance::Light => ThemeAppearanceMode::Light, - Appearance::Dark => ThemeAppearanceMode::Dark, - }); - - return v_flex() - .gap_2() - .child( - h_flex().justify_between().child(Label::new("Theme")).child( - ToggleButtonGroup::single_row( - "theme-selector-onboarding-dark-light", - [ - ThemeAppearanceMode::Light, - ThemeAppearanceMode::Dark, - ThemeAppearanceMode::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); - - telemetry::event!( - "Welcome Theme mode Changed", - from = theme_mode, - to = mode - ); - }, - ) - }), - ) - .size(ToggleButtonGroupSize::Medium) - .tab_index(tab_index) - .selected_index(theme_mode as usize) - .style(ui::ToggleButtonGroupStyle::Outlined) - .width(rems_from_px(3. * 64.)), - ), - ) - .child( - h_flex() - .gap_4() - .justify_between() - .children(render_theme_previews(tab_index, &theme_selection, cx)), - ); - - fn render_theme_previews( - tab_index: &mut isize, - theme_selection: &ThemeSelection, - cx: &mut App, - ) -> [impl IntoElement; 3] { - let system_appearance = SystemAppearance::global(cx); - let theme_registry = ThemeRegistry::global(cx); - - let theme_seed = 0xBEEF as f32; - let theme_mode = theme_selection - .mode() - .unwrap_or_else(|| match *system_appearance { - Appearance::Light => ThemeAppearanceMode::Light, - Appearance::Dark => ThemeAppearanceMode::Dark, - }); - let appearance = match theme_mode { - ThemeAppearanceMode::Light => Appearance::Light, - ThemeAppearanceMode::Dark => Appearance::Dark, - ThemeAppearanceMode::System => *system_appearance, - }; - let current_theme_name: SharedString = theme_selection.name(appearance).0.into(); - - 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(); - let current_theme_name = current_theme_name.clone(); - - move |_, _, cx| { - write_theme_change(theme_name.clone(), theme_mode, cx); - telemetry::event!( - "Welcome Theme Changed", - from = current_theme_name, - to = theme_name - ); - } - }) - .map(|this| { - if theme_mode == ThemeAppearanceMode::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: ThemeAppearanceMode, cx: &mut App) { - let fs = ::global(cx); - update_settings_file(fs, cx, move |settings, _cx| { - theme::set_mode(settings, mode); - }); - } - - fn write_theme_change( - theme: impl Into>, - theme_mode: ThemeAppearanceMode, - cx: &mut App, - ) { - let fs = ::global(cx); - let theme = theme.into(); - update_settings_file(fs, cx, move |settings, cx| { - if theme_mode == ThemeAppearanceMode::System { - let (light_theme, dark_theme) = - get_theme_family_themes(&theme).unwrap_or((theme.as_ref(), theme.as_ref())); - - settings.theme.theme = Some(settings::ThemeSelection::Dynamic { - mode: ThemeAppearanceMode::System, - light: ThemeName(light_theme.into()), - dark: ThemeName(dark_theme.into()), - }); - } else { - let appearance = *SystemAppearance::global(cx); - theme::set_theme(settings, theme, appearance, appearance); - } - }); - } -} - -fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement { - let fs = ::global(cx); - - v_flex() - .gap_4() - .child( - SwitchField::new( - "onboarding-telemetry-metrics", - None::<&str>, - Some("Help improve Zed by sending anonymous usage data".into()), - if TelemetrySettings::get_global(cx).metrics { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - { - let fs = fs.clone(); - 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.telemetry.get_or_insert_default().metrics = Some(enabled); - }); - - // This telemetry event shouldn't fire when it's off. If it does we'll be alerted - // and can fix it in a timely manner to respect a user's choice. - telemetry::event!( - "Welcome Page Telemetry Metrics Toggled", - options = if enabled { "on" } else { "off" } - ); - } - }, - ) - .tab_index({ - *tab_index += 1; - *tab_index - }), - ) - .child( - SwitchField::new( - "onboarding-telemetry-crash-reports", - None::<&str>, - Some( - "Help fix Zed by sending crash reports so we can fix critical issues fast" - .into(), - ), - if TelemetrySettings::get_global(cx).diagnostics { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - { - let fs = fs.clone(); - 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.telemetry.get_or_insert_default().diagnostics = Some(enabled); - }); - - // This telemetry event shouldn't fire when it's off. If it does we'll be alerted - // and can fix it in a timely manner to respect a user's choice. - telemetry::event!( - "Welcome Page Telemetry Diagnostics Toggled", - options = if enabled { "on" } else { "off" } - ); - } - }, - ) - .tab_index({ - *tab_index += 1; - *tab_index - }), - ) -} - -fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement { - let base_keymap = match BaseKeymap::get_global(cx) { - BaseKeymap::VSCode => Some(0), - BaseKeymap::JetBrains => Some(1), - BaseKeymap::SublimeText => Some(2), - BaseKeymap::Atom => Some(3), - BaseKeymap::Emacs => Some(4), - BaseKeymap::Cursor => Some(5), - 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.base_keymap = Some(keymap_base.into()); - }); - - telemetry::event!("Welcome Keymap Changed", keymap = 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", - Some("Vim Mode"), - Some("Coming from Neovim? Use our first-class implementation of Vim Mode".into()), - toggle_state, - { - let fs = ::global(cx); - move |&selection, _, cx| { - let vim_mode = match selection { - ToggleState::Selected => true, - ToggleState::Unselected => false, - ToggleState::Indeterminate => { - return; - } - }; - update_settings_file(fs.clone(), cx, move |setting, _| { - setting.vim_mode = Some(vim_mode); - }); - - telemetry::event!( - "Welcome Vim Mode Toggled", - options = if vim_mode { "on" } else { "off" }, - ); - } - }, - ) - .tab_index({ - *tab_index += 1; - *tab_index - 1 - }) -} - -fn render_setting_import_button( - tab_index: isize, - label: SharedString, - action: &dyn Action, - imported: bool, -) -> impl IntoElement + 'static { - let action = action.boxed_clone(); - - Button::new(label.clone(), label.clone()) - .style(ButtonStyle::OutlinedGhost) - .size(ButtonSize::Medium) - .label_size(LabelSize::Small) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .toggle_state(imported) - .tab_index(tab_index) - .when(imported, |this| { - this.icon(IconName::Check) - .icon_size(IconSize::Small) - .color(Color::Success) - }) - .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: &mut App) -> impl IntoElement { - let import_state = SettingsImportState::global(cx); - let imports: [(SharedString, &dyn Action, bool); 2] = [ - ( - "VS Code".into(), - &ImportVsCodeSettings { skip_prompt: false }, - import_state.vscode, - ), - ( - "Cursor".into(), - &ImportCursorSettings { skip_prompt: false }, - import_state.cursor, - ), - ]; - - let [vscode, cursor] = imports.map(|(label, action, imported)| { - *tab_index += 1; - render_setting_import_button(*tab_index - 1, label, action, imported) - }); - - h_flex() - .gap_2() - .flex_wrap() - .justify_between() - .child( - v_flex() - .gap_0p5() - .max_w_5_6() - .child(Label::new("Import Settings")) - .child( - Label::new("Automatically pull your settings from other editors") - .color(Color::Muted), - ), - ) - .child(h_flex().gap_1().child(vscode).child(cursor)) -} - -pub(crate) fn render_basics_page(cx: &mut App) -> impl IntoElement { - let mut tab_index = 0; - v_flex() - .id("basics-page") - .gap_6() - .child(render_theme_section(&mut tab_index, cx)) - .child(render_base_keymap_section(&mut tab_index, cx)) - .child(render_import_settings_section(&mut tab_index, cx)) - .child(render_vim_mode_switch(&mut tab_index, cx)) - .child(Divider::horizontal().color(ui::DividerColor::BorderVariant)) - .child(render_telemetry_section(&mut tab_index, cx)) -} diff --git a/crates/onboarding/src/multibuffer_hint.rs b/crates/onboarding/src/multibuffer_hint.rs deleted file mode 100644 index 9d290306d8..0000000000 --- a/crates/onboarding/src/multibuffer_hint.rs +++ /dev/null @@ -1,184 +0,0 @@ -use std::collections::HashSet; -use std::sync::OnceLock; -use std::sync::atomic::{AtomicUsize, Ordering}; - -use db::kvp::KEY_VALUE_STORE; -use gpui::{App, EntityId, EventEmitter, Subscription}; -use ui::{IconButtonShape, Tooltip, prelude::*}; -use workspace::item::{ItemBufferKind, ItemEvent, ItemHandle}; -use workspace::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; - -pub struct MultibufferHint { - shown_on: HashSet, - active_item: Option>, - subscription: Option, -} - -const NUMBER_OF_HINTS: usize = 10; - -const SHOWN_COUNT_KEY: &str = "MULTIBUFFER_HINT_SHOWN_COUNT"; - -impl Default for MultibufferHint { - fn default() -> Self { - Self::new() - } -} - -impl MultibufferHint { - pub fn new() -> Self { - Self { - shown_on: Default::default(), - active_item: None, - subscription: None, - } - } -} - -impl MultibufferHint { - fn counter() -> &'static AtomicUsize { - static SHOWN_COUNT: OnceLock = OnceLock::new(); - SHOWN_COUNT.get_or_init(|| { - let value: usize = KEY_VALUE_STORE - .read_kvp(SHOWN_COUNT_KEY) - .ok() - .flatten() - .and_then(|v| v.parse().ok()) - .unwrap_or(0); - - AtomicUsize::new(value) - }) - } - - fn shown_count() -> usize { - Self::counter().load(Ordering::Relaxed) - } - - fn increment_count(cx: &mut App) { - Self::set_count(Self::shown_count() + 1, cx) - } - - pub(crate) fn set_count(count: usize, cx: &mut App) { - Self::counter().store(count, Ordering::Relaxed); - - db::write_and_log(cx, move || { - KEY_VALUE_STORE.write_kvp(SHOWN_COUNT_KEY.to_string(), format!("{}", count)) - }); - } - - fn dismiss(&mut self, cx: &mut App) { - Self::set_count(NUMBER_OF_HINTS, cx) - } - - /// Determines the toolbar location for this [`MultibufferHint`]. - fn determine_toolbar_location(&mut self, cx: &mut Context) -> ToolbarItemLocation { - if Self::shown_count() >= NUMBER_OF_HINTS { - return ToolbarItemLocation::Hidden; - } - - let Some(active_pane_item) = self.active_item.as_ref() else { - return ToolbarItemLocation::Hidden; - }; - - if active_pane_item.buffer_kind(cx) == ItemBufferKind::Singleton - || active_pane_item.breadcrumbs(cx.theme(), cx).is_none() - || !active_pane_item.can_save(cx) - { - return ToolbarItemLocation::Hidden; - } - - if self.shown_on.insert(active_pane_item.item_id()) { - Self::increment_count(cx); - } - - ToolbarItemLocation::Secondary - } -} - -impl EventEmitter for MultibufferHint {} - -impl ToolbarItemView for MultibufferHint { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - window: &mut Window, - cx: &mut Context, - ) -> ToolbarItemLocation { - cx.notify(); - self.active_item = active_pane_item.map(|item| item.boxed_clone()); - - let Some(active_pane_item) = active_pane_item else { - return ToolbarItemLocation::Hidden; - }; - - let this = cx.entity().downgrade(); - self.subscription = Some(active_pane_item.subscribe_to_item_events( - window, - cx, - Box::new(move |event, _, cx| { - if let ItemEvent::UpdateBreadcrumbs = event { - this.update(cx, |this, cx| { - cx.notify(); - let location = this.determine_toolbar_location(cx); - cx.emit(ToolbarItemEvent::ChangeLocation(location)) - }) - .ok(); - } - }), - )); - - self.determine_toolbar_location(cx) - } -} - -impl Render for MultibufferHint { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - h_flex() - .px_2() - .py_0p5() - .justify_between() - .bg(cx.theme().status().info_background.opacity(0.5)) - .border_1() - .border_color(cx.theme().colors().border_variant) - .rounded_sm() - .overflow_hidden() - .child( - h_flex() - .gap_0p5() - .child( - h_flex() - .gap_2() - .child( - Icon::new(IconName::Info) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .child(Label::new( - "Edit and save files directly in the results multibuffer!", - )), - ) - .child( - Button::new("open_docs", "Learn More") - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .icon_position(IconPosition::End) - .on_click(move |_event, _, cx| { - cx.open_url("https://zed.dev/docs/multibuffers") - }), - ), - ) - .child( - IconButton::new("dismiss", IconName::Close) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _event, _, cx| { - this.dismiss(cx); - cx.emit(ToolbarItemEvent::ChangeLocation( - ToolbarItemLocation::Hidden, - )) - })) - .tooltip(Tooltip::text("Dismiss Hint")), - ) - .into_any_element() - } -} diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs deleted file mode 100644 index 94581e1423..0000000000 --- a/crates/onboarding/src/onboarding.rs +++ /dev/null @@ -1,676 +0,0 @@ -pub use crate::welcome::ShowWelcome; -use crate::{multibuffer_hint::MultibufferHint, welcome::WelcomePage}; -use client::{Client, UserStore, zed_urls}; -use db::kvp::KEY_VALUE_STORE; -use fs::Fs; -use gpui::{ - Action, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, EventEmitter, - FocusHandle, Focusable, Global, IntoElement, KeyContext, Render, ScrollHandle, 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::{ - Divider, KeyBinding, ParentElement as _, StatefulInteractiveElement, Vector, VectorName, - WithScrollbar as _, 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, -}; - -mod base_keymap_picker; -mod basics_page; -pub mod multibuffer_hint; -mod theme_preview; -mod welcome; - -/// Imports settings from Visual Studio Code. -#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct ImportVsCodeSettings { - #[serde(default)] - pub skip_prompt: bool, -} - -/// Imports settings from Cursor editor. -#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct ImportCursorSettings { - #[serde(default)] - pub skip_prompt: bool, -} - -pub const FIRST_OPEN: &str = "first_open"; -pub const DOCS_URL: &str = "https://zed.dev/docs/"; - -actions!( - zed, - [ - /// Opens the onboarding view. - OpenOnboarding - ] -); - -actions!( - onboarding, - [ - /// 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 - .with_local_workspace(window, cx, |workspace, window, cx| { - let existing = workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()); - - if let Some(existing) = existing { - workspace.activate_item(&existing, true, true, window, cx); - } else { - let settings_page = Onboarding::new(workspace, cx); - workspace.add_item_to_active_pane( - Box::new(settings_page), - None, - true, - window, - cx, - ) - } - }) - .detach(); - }); - }); - - cx.on_action(|_: &ShowWelcome, cx| { - with_active_or_new_workspace(cx, |workspace, window, cx| { - workspace - .with_local_workspace(window, cx, |workspace, window, cx| { - let existing = workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()); - - if let Some(existing) = existing { - workspace.activate_item(&existing, true, true, window, cx); - } else { - let settings_page = WelcomePage::new(window, cx); - workspace.add_item_to_active_pane( - Box::new(settings_page), - None, - true, - window, - cx, - ) - } - }) - .detach(); - }); - }); - - cx.observe_new(|workspace: &mut Workspace, _window, _cx| { - workspace.register_action(|_workspace, action: &ImportVsCodeSettings, window, cx| { - 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, - cx, - ) - .await - }) - .detach(); - }); - - workspace.register_action(|_workspace, action: &ImportCursorSettings, window, cx| { - 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, - cx, - ) - .await - }) - .detach(); - }); - }) - .detach(); - - base_keymap_picker::init(cx); - - register_serializable_item::(cx); - register_serializable_item::(cx); -} - -pub fn show_onboarding_view(app_state: Arc, cx: &mut App) -> Task> { - telemetry::event!("Onboarding Page Opened"); - open_new( - Default::default(), - app_state, - cx, - |workspace, window, cx| { - { - workspace.toggle_dock(DockPosition::Left, window, cx); - let onboarding_page = Onboarding::new(workspace, cx); - workspace.add_item_to_center(Box::new(onboarding_page.clone()), window, cx); - - window.focus(&onboarding_page.focus_handle(cx)); - - cx.notify(); - }; - db::write_and_log(cx, || { - KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string()) - }); - }, - ) -} - -struct Onboarding { - workspace: WeakEntity, - focus_handle: FocusHandle, - user_store: Entity, - scroll_handle: ScrollHandle, - _settings_subscription: Subscription, -} - -impl Onboarding { - fn new(workspace: &Workspace, cx: &mut App) -> Entity { - let font_family_cache = theme::FontFamilyCache::global(cx); - - cx.new(|cx| { - cx.spawn(async move |this, cx| { - font_family_cache.prefetch(cx).await; - this.update(cx, |_, cx| { - cx.notify(); - }) - }) - .detach(); - - Self { - workspace: workspace.weak_handle(), - focus_handle: cx.focus_handle(), - scroll_handle: ScrollHandle::new(), - user_store: workspace.user_store().clone(), - _settings_subscription: cx - .observe_global::(move |_, cx| cx.notify()), - } - }) - } - - fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) { - telemetry::event!("Finish Setup"); - 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, cx: &mut Context) -> AnyElement { - crate::basics_page::render_basics_page(cx).into_any_element() - } -} - -impl Render for Onboarding { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .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) - .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(|_, _: &menu::SelectNext, window, cx| { - window.focus_next(); - cx.notify(); - })) - .on_action(cx.listener(|_, _: &menu::SelectPrevious, window, cx| { - window.focus_prev(); - cx.notify(); - })) - .child( - div() - .max_w(Rems(48.0)) - .size_full() - .mx_auto() - .child( - v_flex() - .id("page-content") - .m_auto() - .p_12() - .size_full() - .max_w_full() - .min_w_0() - .gap_6() - .overflow_y_scroll() - .child( - h_flex() - .w_full() - .gap_4() - .justify_between() - .child( - h_flex() - .gap_4() - .child(Vector::square(VectorName::ZedLogo, rems(2.5))) - .child( - v_flex() - .child( - Headline::new("Welcome to Zed") - .size(HeadlineSize::Small), - ) - .child( - Label::new("The editor for what's next") - .color(Color::Muted) - .size(LabelSize::Small) - .italic(), - ), - ), - ) - .child({ - Button::new("finish_setup", "Finish Setup") - .style(ButtonStyle::Filled) - .size(ButtonSize::Medium) - .width(Rems(12.0)) - .key_binding( - KeyBinding::for_action_in( - &Finish, - &self.focus_handle, - cx, - ) - .size(rems_from_px(12.)), - ) - .on_click(|_, window, cx| { - window.dispatch_action(Finish.boxed_clone(), cx); - }) - }), - ) - .child(Divider::horizontal().color(ui::DividerColor::BorderVariant)) - .child(self.render_page(cx)) - .track_scroll(&self.scroll_handle), - ) - .vertical_scrollbar_for(&self.scroll_handle, window, cx), - ) - } -} - -impl EventEmitter for Onboarding {} - -impl Focusable for Onboarding { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl Item for Onboarding { - type Event = ItemEvent; - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "Onboarding".into() - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - Some("Onboarding Page Opened") - } - - fn show_toolbar(&self) -> bool { - false - } - - fn can_split(&self) -> bool { - true - } - - fn clone_on_split( - &self, - _workspace_id: Option, - _: &mut Window, - cx: &mut Context, - ) -> Task>> { - Task::ready(Some(cx.new(|cx| Onboarding { - workspace: self.workspace.clone(), - user_store: self.user_store.clone(), - scroll_handle: ScrollHandle::new(), - focus_handle: cx.focus_handle(), - _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), - }))) - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { - f(*event) - } -} - -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, - cx: &mut AsyncWindowContext, -) { - use util::truncate_and_remove_front; - - let vscode_settings = - match settings::VsCodeSettings::load_user_settings(source, fs.clone()).await { - Ok(vscode_settings) => vscode_settings, - Err(err) => { - zlog::error!("{err:?}"); - let _ = cx.prompt( - gpui::PromptLevel::Info, - &format!("Could not find or load a {source} settings file"), - None, - &["Ok"], - ); - return; - } - }; - - if !skip_prompt { - let prompt = cx.prompt( - gpui::PromptLevel::Warning, - &format!( - "Importing {} settings may overwrite your existing settings. \ - Will import settings from {}", - vscode_settings.source, - truncate_and_remove_front(&vscode_settings.path.to_string_lossy(), 128), - ), - None, - &["Ok", "Cancel"], - ); - let result = cx.spawn(async move |_| prompt.await.ok()).await; - if result != Some(0) { - return; - } - }; - - let Ok(result_channel) = cx.update(|_, cx| { - let source = vscode_settings.source; - let path = vscode_settings.path.clone(); - let result_channel = 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(_) = - persistence::ONBOARDING_PAGES.get_onboarding_page(item_id, workspace_id)? - { - workspace.update(cx, |workspace, cx| Onboarding::new(workspace, cx)) - } 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()?; - - Some(cx.background_spawn(async move { - persistence::ONBOARDING_PAGES - .save_onboarding_page(item_id, workspace_id) - .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; - ), - sql!( - CREATE TABLE onboarding_pages_2 ( - workspace_id INTEGER, - item_id INTEGER UNIQUE, - - PRIMARY KEY(workspace_id, item_id), - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ) STRICT; - INSERT INTO onboarding_pages_2 SELECT workspace_id, item_id FROM onboarding_pages; - DROP TABLE onboarding_pages; - ALTER TABLE onboarding_pages_2 RENAME TO onboarding_pages; - ), - ]; - } - - db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]); - - impl OnboardingPagesDb { - query! { - pub async fn save_onboarding_page( - item_id: workspace::ItemId, - workspace_id: workspace::WorkspaceId - ) -> Result<()> { - INSERT OR REPLACE INTO onboarding_pages(item_id, workspace_id) - VALUES (?, ?) - } - } - - query! { - pub fn get_onboarding_page( - item_id: workspace::ItemId, - workspace_id: workspace::WorkspaceId - ) -> Result> { - SELECT item_id - FROM onboarding_pages - WHERE item_id = ? AND workspace_id = ? - } - } - } -} diff --git a/crates/onboarding/src/theme_preview.rs b/crates/onboarding/src/theme_preview.rs deleted file mode 100644 index 8bd65d8a27..0000000000 --- a/crates/onboarding/src/theme_preview.rs +++ /dev/null @@ -1,381 +0,0 @@ -#![allow(unused, dead_code)] -use gpui::{Hsla, Length}; -use std::{ - cell::LazyCell, - sync::{Arc, LazyLock, OnceLock}, -}; -use theme::{Theme, ThemeColors, 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 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() - } -} - -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, - ) - .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) - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - let theme_registry = ThemeRegistry::global(cx); - - let one_dark = theme_registry.get("One Dark"); - let one_light = theme_registry.get("One Light"); - let gruvbox_dark = theme_registry.get("Gruvbox Dark"); - let gruvbox_light = theme_registry.get("Gruvbox Light"); - - let themes_to_preview = vec![ - one_dark.clone().ok(), - one_light.ok(), - gruvbox_dark.ok(), - gruvbox_light.ok(), - ] - .into_iter() - .flatten() - .collect::>(); - - Some( - v_flex() - .gap_6() - .p_4() - .children({ - if let Some(one_dark) = one_dark.ok() { - vec![example_group(vec![single_example( - "Default", - div() - .w(px(240.)) - .h(px(180.)) - .child(ThemePreviewTile::new(one_dark, 0.42)) - .into_any_element(), - )])] - } else { - vec![] - } - }) - .child( - example_group(vec![single_example( - "Default Themes", - h_flex() - .gap_4() - .children( - themes_to_preview - .into_iter() - .map(|theme| { - div() - .w(px(200.)) - .h(px(140.)) - .child(ThemePreviewTile::new(theme, 0.42)) - }) - .collect::>(), - ) - .into_any_element(), - )]) - .grow(), - ) - .into_any_element(), - ) - } -} diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs deleted file mode 100644 index b2711cd52d..0000000000 --- a/crates/onboarding/src/welcome.rs +++ /dev/null @@ -1,443 +0,0 @@ -use gpui::{ - Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, - ParentElement, Render, Styled, Task, Window, actions, -}; -use menu::{SelectNext, SelectPrevious}; -use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*}; -use workspace::{ - NewFile, Open, - item::{Item, ItemEvent}, - with_active_or_new_workspace, -}; -use zed_actions::{Extensions, OpenSettings, agent, command_palette}; - -use crate::{Onboarding, OpenOnboarding}; - -actions!( - zed, - [ - /// Show the Zed welcome screen - ShowWelcome - ] -); - -const CONTENT: (Section<4>, Section<3>) = ( - Section { - title: "Get Started", - entries: [ - SectionEntry { - icon: IconName::Plus, - title: "New File", - action: &NewFile, - }, - SectionEntry { - icon: IconName::FolderOpen, - title: "Open Project", - action: &Open, - }, - SectionEntry { - icon: IconName::CloudDownload, - title: "Clone Repository", - action: &git::Clone, - }, - SectionEntry { - icon: IconName::ListCollapse, - title: "Open Command Palette", - action: &command_palette::Toggle, - }, - ], - }, - Section { - title: "Configure", - entries: [ - SectionEntry { - icon: IconName::Settings, - title: "Open Settings", - action: &OpenSettings, - }, - SectionEntry { - icon: IconName::ZedAssistant, - title: "View AI Settings", - action: &agent::OpenSettings, - }, - SectionEntry { - icon: IconName::Blocks, - title: "Explore Extensions", - action: &Extensions { - category_filter: None, - id: None, - }, - }, - ], - }, -); - -struct Section { - title: &'static str, - entries: [SectionEntry; COLS], -} - -impl Section { - fn render(self, index_offset: usize, focus: &FocusHandle, cx: &mut App) -> impl IntoElement { - v_flex() - .min_w_full() - .child( - h_flex() - .px_1() - .mb_2() - .gap_2() - .child( - Label::new(self.title.to_ascii_uppercase()) - .buffer_font(cx) - .color(Color::Muted) - .size(LabelSize::XSmall), - ) - .child(Divider::horizontal().color(DividerColor::BorderVariant)), - ) - .children( - self.entries - .iter() - .enumerate() - .map(|(index, entry)| entry.render(index_offset + index, focus, cx)), - ) - } -} - -struct SectionEntry { - icon: IconName, - title: &'static str, - action: &'static dyn Action, -} - -impl SectionEntry { - fn render(&self, button_index: usize, focus: &FocusHandle, 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() - .justify_between() - .child( - h_flex() - .gap_2() - .child( - Icon::new(self.icon) - .color(Color::Muted) - .size(IconSize::XSmall), - ) - .child(Label::new(self.title)), - ) - .child( - KeyBinding::for_action_in(self.action, focus, cx).size(rems_from_px(12.)), - ), - ) - .on_click(|_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx)) - } -} - -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, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let (first_section, second_section) = CONTENT; - let first_section_entries = first_section.entries.len(); - let last_index = first_section_entries + second_section.entries.len(); - - h_flex() - .size_full() - .justify_center() - .overflow_hidden() - .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() - .py_40() - .size_full() - .relative() - .max_w(px(1100.)) - .child( - div() - .size_full() - .max_w_128() - .mx_auto() - .child( - h_flex() - .w_full() - .justify_center() - .gap_4() - .child(Vector::square(VectorName::ZedLogo, rems(2.))) - .child( - div().child(Headline::new("Welcome to Zed")).child( - Label::new("The editor for what's next") - .size(LabelSize::Small) - .color(Color::Muted) - .italic(), - ), - ), - ) - .child( - v_flex() - .mt_10() - .gap_6() - .child(first_section.render( - Default::default(), - &self.focus_handle, - cx, - )) - .child(second_section.render( - first_section_entries, - &self.focus_handle, - cx, - )) - .child( - h_flex() - .w_full() - .pt_4() - .justify_center() - // We call this a hack - .rounded_b_xs() - .border_t_1() - .border_color(cx.theme().colors().border.opacity(0.6)) - .border_dashed() - .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, - ); - }); - }); - }), - ), - ), - ), - ), - ) - } -} - -impl WelcomePage { - pub fn new(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| { - let focus_handle = cx.focus_handle(); - cx.on_focus(&focus_handle, window, |_, _, cx| cx.notify()) - .detach(); - - WelcomePage { focus_handle } - }) - } -} - -impl EventEmitter for WelcomePage {} - -impl Focusable for WelcomePage { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl Item for WelcomePage { - type Event = ItemEvent; - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "Welcome".into() - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - Some("New Welcome Page Opened") - } - - fn show_toolbar(&self) -> bool { - false - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { - 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 deleted file mode 100644 index 037ca14437..0000000000 --- a/crates/open_ai/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "open_ai" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/open_ai.rs" - -[features] -default = [] -schemars = ["dep:schemars"] - -[dependencies] -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 -settings.workspace = true -strum.workspace = true -thiserror.workspace = true diff --git a/crates/open_ai/LICENSE-GPL b/crates/open_ai/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/open_ai/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs deleted file mode 100644 index d8b4722543..0000000000 --- a/crates/open_ai/src/open_ai.rs +++ /dev/null @@ -1,626 +0,0 @@ -use anyhow::{Context as _, Result, anyhow}; -use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream}; -use http_client::{ - AsyncBody, HttpClient, Method, Request as HttpRequest, StatusCode, - http::{HeaderMap, HeaderValue}, -}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -pub use settings::OpenAiReasoningEffort as ReasoningEffort; -use std::{convert::TryFrom, future::Future}; -use strum::EnumIter; -use thiserror::Error; - -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()) -} - -#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum Role { - User, - Assistant, - System, - Tool, -} - -impl TryFrom for Role { - type Error = anyhow::Error; - - fn try_from(value: String) -> Result { - match value.as_str() { - "user" => Ok(Self::User), - "assistant" => Ok(Self::Assistant), - "system" => Ok(Self::System), - "tool" => Ok(Self::Tool), - _ => anyhow::bail!("invalid role '{value}'"), - } - } -} - -impl From for String { - fn from(val: Role) -> Self { - match val { - Role::User => "user".to_owned(), - Role::Assistant => "assistant".to_owned(), - Role::System => "system".to_owned(), - Role::Tool => "tool".to_owned(), - } - } -} - -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)] -pub enum Model { - #[serde(rename = "gpt-3.5-turbo")] - ThreePointFiveTurbo, - #[serde(rename = "gpt-4")] - Four, - #[serde(rename = "gpt-4-turbo")] - FourTurbo, - #[serde(rename = "gpt-4o")] - #[default] - FourOmni, - #[serde(rename = "gpt-4o-mini")] - FourOmniMini, - #[serde(rename = "gpt-4.1")] - FourPointOne, - #[serde(rename = "gpt-4.1-mini")] - FourPointOneMini, - #[serde(rename = "gpt-4.1-nano")] - FourPointOneNano, - #[serde(rename = "o1")] - O1, - #[serde(rename = "o3-mini")] - O3Mini, - #[serde(rename = "o3")] - 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 = "gpt-5.1")] - FivePointOne, - #[serde(rename = "gpt-5.2")] - FivePointTwo, - #[serde(rename = "custom")] - Custom { - name: String, - /// The name displayed in the UI, such as in the assistant panel model dropdown menu. - display_name: Option, - 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 - } - - pub fn from_id(id: &str) -> Result { - match id { - "gpt-3.5-turbo" => Ok(Self::ThreePointFiveTurbo), - "gpt-4" => Ok(Self::Four), - "gpt-4-turbo-preview" => Ok(Self::FourTurbo), - "gpt-4o" => Ok(Self::FourOmni), - "gpt-4o-mini" => Ok(Self::FourOmniMini), - "gpt-4.1" => Ok(Self::FourPointOne), - "gpt-4.1-mini" => Ok(Self::FourPointOneMini), - "gpt-4.1-nano" => Ok(Self::FourPointOneNano), - "o1" => Ok(Self::O1), - "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), - "gpt-5.1" => Ok(Self::FivePointOne), - "gpt-5.2" => Ok(Self::FivePointTwo), - invalid_id => anyhow::bail!("invalid model id '{invalid_id}'"), - } - } - - pub fn id(&self) -> &str { - match self { - Self::ThreePointFiveTurbo => "gpt-3.5-turbo", - Self::Four => "gpt-4", - Self::FourTurbo => "gpt-4-turbo", - Self::FourOmni => "gpt-4o", - Self::FourOmniMini => "gpt-4o-mini", - Self::FourPointOne => "gpt-4.1", - Self::FourPointOneMini => "gpt-4.1-mini", - Self::FourPointOneNano => "gpt-4.1-nano", - Self::O1 => "o1", - 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::FivePointOne => "gpt-5.1", - Self::FivePointTwo => "gpt-5.2", - Self::Custom { name, .. } => name, - } - } - - pub fn display_name(&self) -> &str { - match self { - Self::ThreePointFiveTurbo => "gpt-3.5-turbo", - Self::Four => "gpt-4", - Self::FourTurbo => "gpt-4-turbo", - Self::FourOmni => "gpt-4o", - Self::FourOmniMini => "gpt-4o-mini", - Self::FourPointOne => "gpt-4.1", - Self::FourPointOneMini => "gpt-4.1-mini", - Self::FourPointOneNano => "gpt-4.1-nano", - Self::O1 => "o1", - 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::FivePointOne => "gpt-5.1", - Self::FivePointTwo => "gpt-5.2", - Self::Custom { - name, display_name, .. - } => display_name.as_ref().unwrap_or(name), - } - } - - pub fn max_token_count(&self) -> u64 { - match self { - Self::ThreePointFiveTurbo => 16_385, - Self::Four => 8_192, - Self::FourTurbo => 128_000, - Self::FourOmni => 128_000, - Self::FourOmniMini => 128_000, - Self::FourPointOne => 1_047_576, - Self::FourPointOneMini => 1_047_576, - Self::FourPointOneNano => 1_047_576, - Self::O1 => 200_000, - 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::FivePointOne => 400_000, - Self::FivePointTwo => 400_000, - Self::Custom { max_tokens, .. } => *max_tokens, - } - } - - pub fn max_output_tokens(&self) -> Option { - match self { - Self::Custom { - max_output_tokens, .. - } => *max_output_tokens, - Self::ThreePointFiveTurbo => Some(4_096), - Self::Four => Some(8_192), - Self::FourTurbo => Some(4_096), - Self::FourOmni => Some(16_384), - Self::FourOmniMini => Some(16_384), - Self::FourPointOne => Some(32_768), - Self::FourPointOneMini => Some(32_768), - Self::FourPointOneNano => Some(32_768), - Self::O1 => Some(100_000), - 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), - Self::FivePointOne => Some(128_000), - Self::FivePointTwo => Some(128_000), - } - } - - pub fn reasoning_effort(&self) -> Option { - match self { - Self::Custom { - reasoning_effort, .. - } => reasoning_effort.to_owned(), - _ => None, - } - } - - /// Returns whether the given model supports the `parallel_tool_calls` parameter. - /// - /// If the model does not support the parameter, do not pass it up, or the API will return an error. - pub fn supports_parallel_tool_calls(&self) -> bool { - match self { - Self::ThreePointFiveTurbo - | Self::Four - | Self::FourTurbo - | Self::FourOmni - | Self::FourOmniMini - | Self::FourPointOne - | Self::FourPointOneMini - | Self::FourPointOneNano - | Self::Five - | Self::FiveMini - | Self::FivePointOne - | Self::FivePointTwo - | Self::FiveNano => 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)] -pub struct Request { - pub model: String, - pub messages: Vec, - pub stream: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_completion_tokens: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub stop: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub temperature: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_choice: Option, - /// Whether to enable parallel function calling during tool use. - #[serde(default, skip_serializing_if = "Option::is_none")] - 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)] -#[serde(rename_all = "lowercase")] -pub enum ToolChoice { - Auto, - Required, - None, - #[serde(untagged)] - Other(ToolDefinition), -} - -#[derive(Clone, Deserialize, Serialize, Debug)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum ToolDefinition { - #[allow(dead_code)] - Function { function: FunctionDefinition }, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct FunctionDefinition { - pub name: String, - pub description: Option, - pub parameters: Option, -} - -#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(tag = "role", rename_all = "lowercase")] -pub enum RequestMessage { - Assistant { - content: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - tool_calls: Vec, - }, - User { - content: MessageContent, - }, - System { - content: MessageContent, - }, - Tool { - content: MessageContent, - tool_call_id: String, - }, -} - -#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] -#[serde(untagged)] -pub enum MessageContent { - Plain(String), - Multipart(Vec), -} - -impl MessageContent { - pub fn empty() -> Self { - MessageContent::Multipart(vec![]) - } - - pub fn push_part(&mut self, part: MessagePart) { - match self { - MessageContent::Plain(text) => { - *self = - MessageContent::Multipart(vec![MessagePart::Text { text: text.clone() }, part]); - } - MessageContent::Multipart(parts) if parts.is_empty() => match part { - MessagePart::Text { text } => *self = MessageContent::Plain(text), - MessagePart::Image { .. } => *self = MessageContent::Multipart(vec![part]), - }, - MessageContent::Multipart(parts) => parts.push(part), - } - } -} - -impl From> for MessageContent { - fn from(mut parts: Vec) -> Self { - if let [MessagePart::Text { text }] = parts.as_mut_slice() { - MessageContent::Plain(std::mem::take(text)) - } else { - MessageContent::Multipart(parts) - } - } -} - -#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] -#[serde(tag = "type")] -pub enum MessagePart { - #[serde(rename = "text")] - Text { text: String }, - #[serde(rename = "image_url")] - Image { image_url: ImageUrl }, -} - -#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] -pub struct ImageUrl { - pub url: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub detail: Option, -} - -#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ToolCall { - pub id: String, - #[serde(flatten)] - pub content: ToolCallContent, -} - -#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum ToolCallContent { - Function { function: FunctionContent }, -} - -#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct FunctionContent { - pub name: String, - pub arguments: String, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct Response { - pub id: String, - pub object: String, - pub created: u64, - pub model: String, - pub choices: Vec, - pub usage: Usage, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct Choice { - pub index: u32, - pub message: RequestMessage, - pub finish_reason: Option, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ResponseMessageDelta { - pub role: Option, - pub content: Option, - #[serde(default, skip_serializing_if = "is_none_or_empty")] - pub tool_calls: Option>, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ToolCallChunk { - pub index: usize, - pub id: Option, - - // There is also an optional `type` field that would determine if a - // function is there. Sometimes this streams in with the `function` before - // it streams in the `type` - pub function: Option, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct FunctionChunk { - pub name: Option, - pub arguments: Option, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct Usage { - pub prompt_tokens: u64, - pub completion_tokens: u64, - pub total_tokens: u64, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct ChoiceDelta { - pub index: u32, - pub delta: Option, - pub finish_reason: Option, -} - -#[derive(Error, Debug)] -pub enum RequestError { - #[error("HTTP response error from {provider}'s API: status {status_code} - {body:?}")] - HttpResponseError { - provider: String, - status_code: StatusCode, - body: String, - headers: HeaderMap, - }, - #[error(transparent)] - Other(#[from] anyhow::Error), -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct ResponseStreamError { - message: String, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(untagged)] -pub enum ResponseStreamResult { - Ok(ResponseStreamEvent), - Err { error: ResponseStreamError }, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct ResponseStreamEvent { - pub choices: Vec, - pub usage: Option, -} - -pub async fn stream_completion( - client: &dyn HttpClient, - provider_name: &str, - api_url: &str, - api_key: &str, - request: Request, -) -> Result>, RequestError> { - let uri = format!("{api_url}/chat/completions"); - let request_builder = HttpRequest::builder() - .method(Method::POST) - .uri(uri) - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key.trim())); - - let request = request_builder - .body(AsyncBody::from( - serde_json::to_string(&request).map_err(|e| RequestError::Other(e.into()))?, - )) - .map_err(|e| RequestError::Other(e.into()))?; - - let mut response = client.send(request).await?; - if response.status().is_success() { - let reader = BufReader::new(response.into_body()); - Ok(reader - .lines() - .filter_map(|line| async move { - match line { - Ok(line) => { - let line = line.strip_prefix("data: ").or_else(|| line.strip_prefix("data:"))?; - if line == "[DONE]" { - None - } else { - 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))), - } - }) - .boxed()) - } else { - let mut body = String::new(); - response - .body_mut() - .read_to_string(&mut body) - .await - .map_err(|e| RequestError::Other(e.into()))?; - - Err(RequestError::HttpResponseError { - provider: provider_name.to_owned(), - status_code: response.status(), - body, - headers: response.headers().clone(), - }) - } -} - -#[derive(Copy, Clone, Serialize, Deserialize)] -pub enum OpenAiEmbeddingModel { - #[serde(rename = "text-embedding-3-small")] - TextEmbedding3Small, - #[serde(rename = "text-embedding-3-large")] - TextEmbedding3Large, -} - -#[derive(Serialize)] -struct OpenAiEmbeddingRequest<'a> { - model: OpenAiEmbeddingModel, - input: Vec<&'a str>, -} - -#[derive(Deserialize)] -pub struct OpenAiEmbeddingResponse { - pub data: Vec, -} - -#[derive(Deserialize)] -pub struct OpenAiEmbedding { - pub embedding: Vec, -} - -pub fn embed<'a>( - client: &dyn HttpClient, - api_url: &str, - api_key: &str, - model: OpenAiEmbeddingModel, - texts: impl IntoIterator, -) -> impl 'static + Future> { - let uri = format!("{api_url}/embeddings"); - - let request = OpenAiEmbeddingRequest { - model, - input: texts.into_iter().collect(), - }; - let body = AsyncBody::from(serde_json::to_string(&request).unwrap()); - let request = HttpRequest::builder() - .method(Method::POST) - .uri(uri) - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key.trim())) - .body(body) - .map(|request| client.send(request)); - - async move { - let mut response = request?.await?; - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - anyhow::ensure!( - response.status().is_success(), - "error during embedding, status: {:?}, body: {:?}", - response.status(), - body - ); - let response: OpenAiEmbeddingResponse = - serde_json::from_str(&body).context("failed to parse OpenAI embedding response")?; - Ok(response) - } -} diff --git a/crates/open_router/Cargo.toml b/crates/open_router/Cargo.toml deleted file mode 100644 index cccb92c33b..0000000000 --- a/crates/open_router/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "open_router" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/open_router.rs" - -[features] -default = [] -schemars = ["dep:schemars"] - -[dependencies] -anyhow.workspace = true -futures.workspace = true -http_client.workspace = true -schemars = { workspace = true, optional = true } -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -strum.workspace = true -thiserror.workspace = true diff --git a/crates/open_router/LICENSE-GPL b/crates/open_router/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/open_router/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/open_router/src/open_router.rs b/crates/open_router/src/open_router.rs deleted file mode 100644 index 57ff9558c2..0000000000 --- a/crates/open_router/src/open_router.rs +++ /dev/null @@ -1,750 +0,0 @@ -use anyhow::{Result, anyhow}; -use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream}; -use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, http}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -pub use settings::DataCollection; -pub use settings::ModelMode; -pub use settings::OpenRouterAvailableModel as AvailableModel; -pub use settings::OpenRouterProvider as Provider; -use std::{convert::TryFrom, io, time::Duration}; -use strum::EnumString; -use thiserror::Error; - -pub const OPEN_ROUTER_API_URL: &str = "https://openrouter.ai/api/v1"; - -fn extract_retry_after(headers: &http::HeaderMap) -> Option { - if let Some(reset) = headers.get("X-RateLimit-Reset") { - if let Ok(s) = reset.to_str() { - if let Ok(epoch_ms) = s.parse::() { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as u64; - if epoch_ms > now { - return Some(std::time::Duration::from_millis(epoch_ms - now)); - } - } - } - } - None -} - -fn is_none_or_empty, U>(opt: &Option) -> bool { - opt.as_ref().is_none_or(|v| v.as_ref().is_empty()) -} - -#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum Role { - User, - Assistant, - System, - Tool, -} - -impl TryFrom for Role { - type Error = anyhow::Error; - - fn try_from(value: String) -> Result { - match value.as_str() { - "user" => Ok(Self::User), - "assistant" => Ok(Self::Assistant), - "system" => Ok(Self::System), - "tool" => Ok(Self::Tool), - _ => Err(anyhow!("invalid role '{value}'")), - } - } -} - -impl From for String { - fn from(val: Role) -> Self { - match val { - Role::User => "user".to_owned(), - Role::Assistant => "assistant".to_owned(), - Role::System => "system".to_owned(), - Role::Tool => "tool".to_owned(), - } - } -} - -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] -pub struct Model { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub supports_tools: Option, - pub supports_images: Option, - #[serde(default)] - pub mode: ModelMode, - pub provider: Option, -} - -impl Model { - pub fn default_fast() -> Self { - Self::new( - "openrouter/auto", - Some("Auto Router"), - Some(2000000), - Some(true), - Some(false), - Some(ModelMode::Default), - None, - ) - } - - pub fn default() -> Self { - Self::default_fast() - } - - pub fn new( - name: &str, - display_name: Option<&str>, - max_tokens: Option, - supports_tools: Option, - supports_images: Option, - mode: Option, - provider: Option, - ) -> Self { - Self { - name: name.to_owned(), - display_name: display_name.map(|s| s.to_owned()), - max_tokens: max_tokens.unwrap_or(2000000), - supports_tools, - supports_images, - mode: mode.unwrap_or(ModelMode::Default), - provider, - } - } - - pub fn id(&self) -> &str { - &self.name - } - - pub fn display_name(&self) -> &str { - self.display_name.as_ref().unwrap_or(&self.name) - } - - pub fn max_token_count(&self) -> u64 { - self.max_tokens - } - - pub fn max_output_tokens(&self) -> Option { - None - } - - pub fn supports_tool_calls(&self) -> bool { - self.supports_tools.unwrap_or(false) - } - - pub fn supports_parallel_tool_calls(&self) -> bool { - false - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Request { - pub model: String, - pub messages: Vec, - pub stream: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_tokens: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub stop: Vec, - pub temperature: f32, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_choice: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - 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 reasoning: Option, - pub usage: RequestUsage, - pub provider: Option, -} - -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct RequestUsage { - pub include: bool, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ToolChoice { - Auto, - Required, - None, - #[serde(untagged)] - Other(ToolDefinition), -} - -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Deserialize, Serialize, Debug)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum ToolDefinition { - #[allow(dead_code)] - Function { function: FunctionDefinition }, -} - -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct FunctionDefinition { - pub name: String, - pub description: Option, - pub parameters: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Reasoning { - #[serde(skip_serializing_if = "Option::is_none")] - pub effort: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub max_tokens: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub exclude: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub enabled: Option, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(tag = "role", rename_all = "lowercase")] -pub enum RequestMessage { - Assistant { - content: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - tool_calls: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - reasoning_details: Option, - }, - User { - content: MessageContent, - }, - System { - content: MessageContent, - }, - Tool { - content: MessageContent, - tool_call_id: String, - }, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(untagged)] -pub enum MessageContent { - Plain(String), - Multipart(Vec), -} - -impl MessageContent { - pub fn empty() -> Self { - Self::Plain(String::new()) - } - - pub fn push_part(&mut self, part: MessagePart) { - match self { - Self::Plain(text) if text.is_empty() => { - *self = Self::Multipart(vec![part]); - } - Self::Plain(text) => { - let text_part = MessagePart::Text { - text: std::mem::take(text), - }; - *self = Self::Multipart(vec![text_part, part]); - } - Self::Multipart(parts) => parts.push(part), - } - } -} - -impl From> for MessageContent { - fn from(parts: Vec) -> Self { - if parts.len() == 1 - && let MessagePart::Text { text } = &parts[0] - { - return Self::Plain(text.clone()); - } - Self::Multipart(parts) - } -} - -impl From for MessageContent { - fn from(text: String) -> Self { - Self::Plain(text) - } -} - -impl From<&str> for MessageContent { - fn from(text: &str) -> Self { - Self::Plain(text.to_string()) - } -} - -impl MessageContent { - pub fn as_text(&self) -> Option<&str> { - match self { - Self::Plain(text) => Some(text), - Self::Multipart(parts) if parts.len() == 1 => { - if let MessagePart::Text { text } = &parts[0] { - Some(text) - } else { - None - } - } - _ => None, - } - } - - pub fn to_text(&self) -> String { - match self { - Self::Plain(text) => text.clone(), - Self::Multipart(parts) => parts - .iter() - .filter_map(|part| { - if let MessagePart::Text { text } = part { - Some(text.as_str()) - } else { - None - } - }) - .collect::>() - .join(""), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum MessagePart { - Text { - text: String, - }, - #[serde(rename = "image_url")] - Image { - image_url: String, - }, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ToolCall { - pub id: String, - #[serde(flatten)] - pub content: ToolCallContent, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum ToolCallContent { - Function { function: FunctionContent }, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct FunctionContent { - pub name: String, - pub arguments: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub thought_signature: Option, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ResponseMessageDelta { - pub role: Option, - pub content: Option, - pub reasoning: Option, - #[serde(default, skip_serializing_if = "is_none_or_empty")] - pub tool_calls: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub reasoning_details: Option, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ToolCallChunk { - pub index: usize, - pub id: Option, - pub function: Option, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct FunctionChunk { - pub name: Option, - pub arguments: Option, - #[serde(default)] - pub thought_signature: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Usage { - pub prompt_tokens: u64, - pub completion_tokens: u64, - pub total_tokens: u64, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct ChoiceDelta { - pub index: u32, - pub delta: ResponseMessageDelta, - pub finish_reason: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct ResponseStreamEvent { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub id: Option, - pub created: u32, - pub model: String, - pub choices: Vec, - pub usage: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Response { - pub id: String, - pub object: String, - pub created: u64, - pub model: String, - pub choices: Vec, - pub usage: Usage, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Choice { - pub index: u32, - pub message: RequestMessage, - pub finish_reason: Option, -} - -#[derive(Default, Debug, Clone, PartialEq, Deserialize)] -pub struct ListModelsResponse { - pub data: Vec, -} - -#[derive(Default, Debug, Clone, PartialEq, Deserialize)] -pub struct ModelEntry { - pub id: String, - pub name: String, - pub created: usize, - pub description: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub context_length: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub supported_parameters: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub architecture: Option, -} - -#[derive(Default, Debug, Clone, PartialEq, Deserialize)] -pub struct ModelArchitecture { - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub input_modalities: Vec, -} - -pub async fn stream_completion( - client: &dyn HttpClient, - api_url: &str, - api_key: &str, - request: Request, -) -> Result>, OpenRouterError> { - let uri = format!("{api_url}/chat/completions"); - let request_builder = HttpRequest::builder() - .method(Method::POST) - .uri(uri) - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key)) - .header("HTTP-Referer", "https://zed.dev") - .header("X-Title", "Zed Editor"); - - let request = request_builder - .body(AsyncBody::from( - serde_json::to_string(&request).map_err(OpenRouterError::SerializeRequest)?, - )) - .map_err(OpenRouterError::BuildRequestBody)?; - let mut response = client - .send(request) - .await - .map_err(OpenRouterError::HttpSend)?; - - if response.status().is_success() { - let reader = BufReader::new(response.into_body()); - Ok(reader - .lines() - .filter_map(|line| async move { - match line { - Ok(line) => { - if line.starts_with(':') { - return None; - } - - let line = line.strip_prefix("data: ")?; - if line == "[DONE]" { - None - } else { - match serde_json::from_str::(line) { - Ok(response) => Some(Ok(response)), - Err(error) => { - if line.trim().is_empty() { - None - } else { - Some(Err(OpenRouterError::DeserializeResponse(error))) - } - } - } - } - } - Err(error) => Some(Err(OpenRouterError::ReadResponse(error))), - } - }) - .boxed()) - } else { - let code = ApiErrorCode::from_status(response.status().as_u16()); - - let mut body = String::new(); - response - .body_mut() - .read_to_string(&mut body) - .await - .map_err(OpenRouterError::ReadResponse)?; - - let error_response = match serde_json::from_str::(&body) { - Ok(OpenRouterErrorResponse { error }) => error, - Err(_) => OpenRouterErrorBody { - code: response.status().as_u16(), - message: body, - metadata: None, - }, - }; - - match code { - ApiErrorCode::RateLimitError => { - let retry_after = extract_retry_after(response.headers()); - Err(OpenRouterError::RateLimit { - retry_after: retry_after.unwrap_or_else(|| std::time::Duration::from_secs(60)), - }) - } - ApiErrorCode::OverloadedError => { - let retry_after = extract_retry_after(response.headers()); - Err(OpenRouterError::ServerOverloaded { retry_after }) - } - _ => Err(OpenRouterError::ApiError(ApiError { - code: code, - message: error_response.message, - })), - } - } -} - -pub async fn list_models( - client: &dyn HttpClient, - api_url: &str, - api_key: &str, -) -> Result, OpenRouterError> { - let uri = format!("{api_url}/models/user"); - let request_builder = HttpRequest::builder() - .method(Method::GET) - .uri(uri) - .header("Accept", "application/json") - .header("Authorization", format!("Bearer {}", api_key)) - .header("HTTP-Referer", "https://zed.dev") - .header("X-Title", "Zed Editor"); - - let request = request_builder - .body(AsyncBody::default()) - .map_err(OpenRouterError::BuildRequestBody)?; - let mut response = client - .send(request) - .await - .map_err(OpenRouterError::HttpSend)?; - - let mut body = String::new(); - response - .body_mut() - .read_to_string(&mut body) - .await - .map_err(OpenRouterError::ReadResponse)?; - - if response.status().is_success() { - let response: ListModelsResponse = - serde_json::from_str(&body).map_err(OpenRouterError::DeserializeResponse)?; - - let models = response - .data - .into_iter() - .map(|entry| Model { - name: entry.id, - // OpenRouter returns display names in the format "provider_name: model_name". - // When displayed in the UI, these names can get truncated from the right. - // Since users typically already know the provider, we extract just the model name - // portion (after the colon) to create a more concise and user-friendly label - // for the model dropdown in the agent panel. - display_name: Some( - entry - .name - .split(':') - .next_back() - .unwrap_or(&entry.name) - .trim() - .to_string(), - ), - max_tokens: entry.context_length.unwrap_or(2000000), - supports_tools: Some(entry.supported_parameters.contains(&"tools".to_string())), - supports_images: Some( - entry - .architecture - .as_ref() - .map(|arch| arch.input_modalities.contains(&"image".to_string())) - .unwrap_or(false), - ), - mode: if entry - .supported_parameters - .contains(&"reasoning".to_string()) - { - ModelMode::Thinking { - budget_tokens: Some(4_096), - } - } else { - ModelMode::Default - }, - provider: None, - }) - .collect(); - - Ok(models) - } else { - let code = ApiErrorCode::from_status(response.status().as_u16()); - - let mut body = String::new(); - response - .body_mut() - .read_to_string(&mut body) - .await - .map_err(OpenRouterError::ReadResponse)?; - - let error_response = match serde_json::from_str::(&body) { - Ok(OpenRouterErrorResponse { error }) => error, - Err(_) => OpenRouterErrorBody { - code: response.status().as_u16(), - message: body, - metadata: None, - }, - }; - - match code { - ApiErrorCode::RateLimitError => { - let retry_after = extract_retry_after(response.headers()); - Err(OpenRouterError::RateLimit { - retry_after: retry_after.unwrap_or_else(|| std::time::Duration::from_secs(60)), - }) - } - ApiErrorCode::OverloadedError => { - let retry_after = extract_retry_after(response.headers()); - Err(OpenRouterError::ServerOverloaded { retry_after }) - } - _ => Err(OpenRouterError::ApiError(ApiError { - code: code, - message: error_response.message, - })), - } - } -} - -#[derive(Debug)] -pub enum OpenRouterError { - /// Failed to serialize the HTTP request body to JSON - SerializeRequest(serde_json::Error), - - /// Failed to construct the HTTP request body - BuildRequestBody(http::Error), - - /// Failed to send the HTTP request - HttpSend(anyhow::Error), - - /// Failed to deserialize the response from JSON - DeserializeResponse(serde_json::Error), - - /// Failed to read from response stream - ReadResponse(io::Error), - - /// Rate limit exceeded - RateLimit { retry_after: Duration }, - - /// Server overloaded - ServerOverloaded { retry_after: Option }, - - /// API returned an error response - ApiError(ApiError), -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct OpenRouterErrorBody { - pub code: u16, - pub message: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub metadata: Option>, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct OpenRouterErrorResponse { - pub error: OpenRouterErrorBody, -} - -#[derive(Debug, Serialize, Deserialize, Error)] -#[error("OpenRouter API Error: {code}: {message}")] -pub struct ApiError { - pub code: ApiErrorCode, - pub message: String, -} - -/// An OpenROuter API error code. -/// -#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumString, Serialize, Deserialize)] -#[strum(serialize_all = "snake_case")] -pub enum ApiErrorCode { - /// 400: Bad Request (invalid or missing params, CORS) - InvalidRequestError, - /// 401: Invalid credentials (OAuth session expired, disabled/invalid API key) - AuthenticationError, - /// 402: Your account or API key has insufficient credits. Add more credits and retry the request. - PaymentRequiredError, - /// 403: Your chosen model requires moderation and your input was flagged - PermissionError, - /// 408: Your request timed out - RequestTimedOut, - /// 429: You are being rate limited - RateLimitError, - /// 502: Your chosen model is down or we received an invalid response from it - ApiError, - /// 503: There is no available model provider that meets your routing requirements - OverloadedError, -} - -impl std::fmt::Display for ApiErrorCode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = match self { - ApiErrorCode::InvalidRequestError => "invalid_request_error", - ApiErrorCode::AuthenticationError => "authentication_error", - ApiErrorCode::PaymentRequiredError => "payment_required_error", - ApiErrorCode::PermissionError => "permission_error", - ApiErrorCode::RequestTimedOut => "request_timed_out", - ApiErrorCode::RateLimitError => "rate_limit_error", - ApiErrorCode::ApiError => "api_error", - ApiErrorCode::OverloadedError => "overloaded_error", - }; - write!(f, "{s}") - } -} - -impl ApiErrorCode { - pub fn from_status(status: u16) -> Self { - match status { - 400 => ApiErrorCode::InvalidRequestError, - 401 => ApiErrorCode::AuthenticationError, - 402 => ApiErrorCode::PaymentRequiredError, - 403 => ApiErrorCode::PermissionError, - 408 => ApiErrorCode::RequestTimedOut, - 429 => ApiErrorCode::RateLimitError, - 502 => ApiErrorCode::ApiError, - 503 => ApiErrorCode::OverloadedError, - _ => ApiErrorCode::ApiError, - } - } -} diff --git a/crates/outline/Cargo.toml b/crates/outline/Cargo.toml deleted file mode 100644 index 5069fa2373..0000000000 --- a/crates/outline/Cargo.toml +++ /dev/null @@ -1,41 +0,0 @@ -[package] -name = "outline" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/outline.rs" -doctest = false - -[dependencies] -editor.workspace = true -fuzzy.workspace = true -gpui.workspace = true -language.workspace = true -ordered-float.workspace = true -picker.workspace = true -settings.workspace = true -smol.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -editor = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -indoc.workspace = true -language = { workspace = true, features = ["test-support"] } -menu.workspace = true -project = { workspace = true, features = ["test-support"] } -rope.workspace = true -serde_json.workspace = true -tree-sitter-rust.workspace = true -tree-sitter-typescript.workspace = true -workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/outline/LICENSE-GPL b/crates/outline/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/outline/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs deleted file mode 100644 index 1f5cf1edab..0000000000 --- a/crates/outline/src/outline.rs +++ /dev/null @@ -1,610 +0,0 @@ -use std::ops::Range; -use std::{ - cmp::{self, Reverse}, - sync::Arc, -}; - -use editor::scroll::ScrollOffset; -use editor::{Anchor, AnchorRangeExt, Editor, scroll::Autoscroll}; -use editor::{MultiBufferOffset, RowHighlightOptions, SelectionEffects}; -use fuzzy::StringMatch; -use gpui::{ - App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle, - ParentElement, Point, Render, Styled, StyledText, Task, TextStyle, WeakEntity, Window, div, - rems, -}; -use language::{Outline, OutlineItem}; -use ordered_float::OrderedFloat; -use picker::{Picker, PickerDelegate}; -use settings::Settings; -use theme::{ActiveTheme, ThemeSettings}; -use ui::{ListItem, ListItemSpacing, prelude::*}; -use util::ResultExt; -use workspace::{DismissDecision, ModalView, Workspace}; - -pub fn init(cx: &mut App) { - cx.observe_new(OutlineView::register).detach(); - zed_actions::outline::TOGGLE_OUTLINE - .set(|view, window, cx| { - let Ok(editor) = view.downcast::() else { - return; - }; - - toggle(editor, &Default::default(), window, cx); - }) - .ok(); -} - -pub fn toggle( - editor: Entity, - _: &zed_actions::outline::ToggleOutline, - window: &mut Window, - cx: &mut App, -) { - let outline = editor - .read(cx) - .buffer() - .read(cx) - .snapshot(cx) - .outline(Some(cx.theme().syntax())); - - let workspace = window.root::().flatten(); - if let Some((workspace, outline)) = workspace.zip(outline) { - workspace.update(cx, |workspace, cx| { - workspace.toggle_modal(window, cx, |window, cx| { - OutlineView::new(outline, editor, window, cx) - }); - }) - } -} - -pub struct OutlineView { - picker: Entity>, -} - -impl Focusable for OutlineView { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl EventEmitter for OutlineView {} -impl ModalView for OutlineView { - fn on_before_dismiss( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> DismissDecision { - self.picker.update(cx, |picker, cx| { - picker.delegate.restore_active_editor(window, cx) - }); - DismissDecision::Dismiss(true) - } -} - -impl Render for OutlineView { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .w(rems(34.)) - .on_action(cx.listener( - |_this: &mut OutlineView, - _: &zed_actions::outline::ToggleOutline, - _window: &mut Window, - cx: &mut Context| { - // When outline::Toggle is triggered while the outline is open, dismiss it - cx.emit(DismissEvent); - }, - )) - .child(self.picker.clone()) - } -} - -impl OutlineView { - fn register(editor: &mut Editor, _: Option<&mut Window>, cx: &mut Context) { - if editor.mode().is_full() { - let handle = cx.entity().downgrade(); - editor - .register_action(move |action, window, cx| { - if let Some(editor) = handle.upgrade() { - toggle(editor, action, window, cx); - } - }) - .detach(); - } - } - - fn new( - outline: Outline, - editor: Entity, - window: &mut Window, - cx: &mut Context, - ) -> OutlineView { - let delegate = OutlineViewDelegate::new(cx.entity().downgrade(), outline, editor, cx); - let picker = cx.new(|cx| { - Picker::uniform_list(delegate, window, cx).max_height(Some(vh(0.75, window))) - }); - OutlineView { picker } - } -} - -struct OutlineViewDelegate { - outline_view: WeakEntity, - active_editor: Entity, - outline: Outline, - selected_match_index: usize, - prev_scroll_position: Option>, - matches: Vec, - last_query: String, -} - -enum OutlineRowHighlights {} - -impl OutlineViewDelegate { - fn new( - outline_view: WeakEntity, - outline: Outline, - editor: Entity, - - cx: &mut Context, - ) -> Self { - Self { - outline_view, - last_query: Default::default(), - matches: Default::default(), - selected_match_index: 0, - prev_scroll_position: Some(editor.update(cx, |editor, cx| editor.scroll_position(cx))), - active_editor: editor, - outline, - } - } - - fn restore_active_editor(&mut self, window: &mut Window, cx: &mut App) { - self.active_editor.update(cx, |editor, cx| { - editor.clear_row_highlights::(); - if let Some(scroll_position) = self.prev_scroll_position { - editor.set_scroll_position(scroll_position, window, cx); - } - }) - } - - fn set_selected_index( - &mut self, - ix: usize, - navigate: bool, - - cx: &mut Context>, - ) { - self.selected_match_index = ix; - - if navigate && !self.matches.is_empty() { - let selected_match = &self.matches[self.selected_match_index]; - let outline_item = &self.outline.items[selected_match.candidate_id]; - - self.active_editor.update(cx, |active_editor, cx| { - active_editor.clear_row_highlights::(); - active_editor.highlight_rows::( - outline_item.range.start..outline_item.range.end, - cx.theme().colors().editor_highlighted_line_background, - RowHighlightOptions { - autoscroll: true, - ..Default::default() - }, - cx, - ); - active_editor.request_autoscroll(Autoscroll::center(), cx); - }); - } - } -} - -impl PickerDelegate for OutlineViewDelegate { - type ListItem = ListItem; - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Search buffer symbols...".into() - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_match_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _: &mut Window, - cx: &mut Context>, - ) { - self.set_selected_index(ix, true, cx); - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - let selected_index; - if query.is_empty() { - self.restore_active_editor(window, cx); - self.matches = self - .outline - .items - .iter() - .enumerate() - .map(|(index, _)| StringMatch { - candidate_id: index, - score: Default::default(), - positions: Default::default(), - string: Default::default(), - }) - .collect(); - - let (buffer, cursor_offset) = self.active_editor.update(cx, |editor, cx| { - let buffer = editor.buffer().read(cx).snapshot(cx); - let cursor_offset = editor - .selections - .newest::(&editor.display_snapshot(cx)) - .head(); - (buffer, cursor_offset) - }); - selected_index = self - .outline - .items - .iter() - .enumerate() - .map(|(ix, item)| { - let range = item.range.to_offset(&buffer); - let distance_to_closest_endpoint = cmp::min( - (range.start.0 as isize - cursor_offset.0 as isize).abs(), - (range.end.0 as isize - cursor_offset.0 as isize).abs(), - ); - let depth = if range.contains(&cursor_offset) { - Some(item.depth) - } else { - None - }; - (ix, depth, distance_to_closest_endpoint) - }) - .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance))) - .map(|(ix, _, _)| ix) - .unwrap_or(0); - } else { - self.matches = smol::block_on( - self.outline - .search(&query, cx.background_executor().clone()), - ); - selected_index = self - .matches - .iter() - .enumerate() - .max_by_key(|(_, m)| OrderedFloat(m.score)) - .map(|(ix, _)| ix) - .unwrap_or(0); - } - self.last_query = query; - self.set_selected_index(selected_index, !self.last_query.is_empty(), cx); - Task::ready(()) - } - - fn confirm( - &mut self, - _: bool, - window: &mut Window, - cx: &mut Context>, - ) { - self.prev_scroll_position.take(); - self.set_selected_index(self.selected_match_index, true, cx); - - self.active_editor.update(cx, |active_editor, cx| { - let highlight = active_editor - .highlighted_rows::() - .next(); - if let Some((rows, _)) = highlight { - active_editor.change_selections( - SelectionEffects::scroll(Autoscroll::center()), - window, - cx, - |s| s.select_ranges([rows.start..rows.start]), - ); - active_editor.clear_row_highlights::(); - window.focus(&active_editor.focus_handle(cx)); - } - }); - - self.dismissed(window, cx); - } - - fn dismissed(&mut self, window: &mut Window, cx: &mut Context>) { - self.outline_view - .update(cx, |_, cx| cx.emit(DismissEvent)) - .log_err(); - self.restore_active_editor(window, cx); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - let mat = self.matches.get(ix)?; - let outline_item = self.outline.items.get(mat.candidate_id)?; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child( - div() - .text_ui(cx) - .pl(rems(outline_item.depth as f32)) - .child(render_item(outline_item, mat.ranges(), cx)), - ), - ) - } -} - -pub fn render_item( - outline_item: &OutlineItem, - match_ranges: impl IntoIterator>, - cx: &App, -) -> StyledText { - let highlight_style = HighlightStyle { - background_color: Some(cx.theme().colors().text_accent.alpha(0.3)), - ..Default::default() - }; - let custom_highlights = match_ranges - .into_iter() - .map(|range| (range, highlight_style)); - - let settings = ThemeSettings::get_global(cx); - - // TODO: We probably shouldn't need to build a whole new text style here - // but I'm not sure how to get the current one and modify it. - // Before this change TextStyle::default() was used here, which was giving us the wrong font and text color. - let text_style = TextStyle { - color: cx.theme().colors().text, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: settings.buffer_font_size(cx).into(), - font_weight: settings.buffer_font.weight, - line_height: relative(1.), - ..Default::default() - }; - let highlights = gpui::combine_highlights( - custom_highlights, - outline_item.highlight_ranges.iter().cloned(), - ); - - StyledText::new(outline_item.text.clone()).with_default_highlights(&text_style, highlights) -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::{TestAppContext, VisualTestContext}; - use indoc::indoc; - use project::{FakeFs, Project}; - use serde_json::json; - use util::{path, rel_path::rel_path}; - use workspace::{AppState, Workspace}; - - #[gpui::test] - async fn test_outline_view_row_highlights(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({ - "a.rs": indoc!{" - // display line 0 - struct SingleLine; // display line 1 - // display line 2 - struct MultiLine { // display line 3 - field_1: i32, // display line 4 - field_2: i32, // display line 5 - } // display line 6 - "} - }), - ) - .await; - - let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - project.read_with(cx, |project, _| { - project.languages().add(language::rust_lang()) - }); - - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let worktree_id = workspace.update(cx, |workspace, cx| { - workspace.project().update(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }) - }); - let _buffer = project - .update(cx, |project, cx| { - project.open_local_buffer(path!("/dir/a.rs"), cx) - }) - .await - .unwrap(); - let editor = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - let ensure_outline_view_contents = - |outline_view: &Entity>, cx: &mut VisualTestContext| { - assert_eq!(query(outline_view, cx), ""); - assert_eq!( - outline_names(outline_view, cx), - vec![ - "struct SingleLine", - "struct MultiLine", - "field_1", - "field_2" - ], - ); - }; - - let outline_view = open_outline_view(&workspace, cx); - ensure_outline_view_contents(&outline_view, cx); - assert_eq!( - highlighted_display_rows(&editor, cx), - Vec::::new(), - "Initially opened outline view should have no highlights" - ); - assert_single_caret_at_row(&editor, 0, cx); - - cx.dispatch_action(menu::Confirm); - // Ensures that outline still goes to entry even if no queries have been made - assert_single_caret_at_row(&editor, 1, cx); - - let outline_view = open_outline_view(&workspace, cx); - - cx.dispatch_action(menu::SelectNext); - ensure_outline_view_contents(&outline_view, cx); - assert_eq!( - highlighted_display_rows(&editor, cx), - vec![3, 4, 5, 6], - "Second struct's rows should be highlighted" - ); - assert_single_caret_at_row(&editor, 1, cx); - - cx.dispatch_action(menu::SelectPrevious); - ensure_outline_view_contents(&outline_view, cx); - assert_eq!( - highlighted_display_rows(&editor, cx), - vec![1], - "First struct's row should be highlighted" - ); - assert_single_caret_at_row(&editor, 1, cx); - - cx.dispatch_action(menu::Cancel); - ensure_outline_view_contents(&outline_view, cx); - assert_eq!( - highlighted_display_rows(&editor, cx), - Vec::::new(), - "No rows should be highlighted after outline view is cancelled and closed" - ); - assert_single_caret_at_row(&editor, 1, cx); - - let outline_view = open_outline_view(&workspace, cx); - ensure_outline_view_contents(&outline_view, cx); - assert_eq!( - highlighted_display_rows(&editor, cx), - Vec::::new(), - "Reopened outline view should have no highlights" - ); - assert_single_caret_at_row(&editor, 1, cx); - - let expected_first_highlighted_row = 3; - cx.dispatch_action(menu::SelectNext); - ensure_outline_view_contents(&outline_view, cx); - assert_eq!( - highlighted_display_rows(&editor, cx), - vec![expected_first_highlighted_row, 4, 5, 6] - ); - assert_single_caret_at_row(&editor, 1, cx); - cx.dispatch_action(menu::Confirm); - ensure_outline_view_contents(&outline_view, cx); - assert_eq!( - highlighted_display_rows(&editor, cx), - Vec::::new(), - "No rows should be highlighted after outline view is confirmed and closed" - ); - // On confirm, should place the caret on the first row of the highlighted rows range. - assert_single_caret_at_row(&editor, expected_first_highlighted_row, cx); - } - - fn open_outline_view( - workspace: &Entity, - cx: &mut VisualTestContext, - ) -> Entity> { - cx.dispatch_action(zed_actions::outline::ToggleOutline); - workspace.update(cx, |workspace, cx| { - workspace - .active_modal::(cx) - .unwrap() - .read(cx) - .picker - .clone() - }) - } - - fn query( - outline_view: &Entity>, - cx: &mut VisualTestContext, - ) -> String { - outline_view.update(cx, |outline_view, cx| outline_view.query(cx)) - } - - fn outline_names( - outline_view: &Entity>, - cx: &mut VisualTestContext, - ) -> Vec { - outline_view.read_with(cx, |outline_view, _| { - let items = &outline_view.delegate.outline.items; - outline_view - .delegate - .matches - .iter() - .map(|hit| items[hit.candidate_id].text.clone()) - .collect::>() - }) - } - - fn highlighted_display_rows(editor: &Entity, cx: &mut VisualTestContext) -> Vec { - editor.update_in(cx, |editor, window, cx| { - editor - .highlighted_display_rows(window, cx) - .into_keys() - .map(|r| r.0) - .collect() - }) - } - - fn init_test(cx: &mut TestAppContext) -> Arc { - cx.update(|cx| { - let state = AppState::test(cx); - crate::init(cx); - editor::init(cx); - state - }) - } - - #[track_caller] - fn assert_single_caret_at_row( - editor: &Entity, - buffer_row: u32, - cx: &mut VisualTestContext, - ) { - let selections = editor.update(cx, |editor, cx| { - editor - .selections - .all::(&editor.display_snapshot(cx)) - .into_iter() - .map(|s| s.start..s.end) - .collect::>() - }); - assert!( - selections.len() == 1, - "Expected one caret selection but got: {selections:?}" - ); - let selection = &selections[0]; - assert!( - selection.start == selection.end, - "Expected a single caret selection, but got: {selection:?}" - ); - assert_eq!(selection.start.row, buffer_row); - } -} diff --git a/crates/outline_panel/Cargo.toml b/crates/outline_panel/Cargo.toml deleted file mode 100644 index 72e2d1eb63..0000000000 --- a/crates/outline_panel/Cargo.toml +++ /dev/null @@ -1,47 +0,0 @@ -[package] -name = "outline_panel" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/outline_panel.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -collections.workspace = true -db.workspace = true -editor.workspace = true -file_icons.workspace = true -fuzzy.workspace = true -gpui.workspace = true -itertools.workspace = true -language.workspace = true -log.workspace = true -menu.workspace = true -outline.workspace = true -project.workspace = true -search.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smallvec.workspace = true -smol.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -worktree.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -search = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true - -[package.metadata.cargo-machete] -ignored = ["log"] diff --git a/crates/outline_panel/LICENSE-GPL b/crates/outline_panel/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/outline_panel/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs deleted file mode 100644 index a787ad5b03..0000000000 --- a/crates/outline_panel/src/outline_panel.rs +++ /dev/null @@ -1,7760 +0,0 @@ -mod outline_panel_settings; - -use anyhow::Context as _; -use collections::{BTreeSet, HashMap, HashSet, hash_map}; -use db::kvp::KEY_VALUE_STORE; -use editor::{ - AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, ExcerptId, ExcerptRange, - MultiBufferSnapshot, RangeToAnchorExt, SelectionEffects, - display_map::ToDisplayPoint, - items::{entry_git_aware_label_color, entry_label_color}, - scroll::{Autoscroll, ScrollAnchor}, -}; -use file_icons::FileIcons; -use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; -use gpui::{ - Action, AnyElement, App, AppContext as _, AsyncWindowContext, Bounds, ClipboardItem, Context, - DismissEvent, Div, ElementId, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle, - InteractiveElement, IntoElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, - MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, ScrollStrategy, - SharedString, Stateful, StatefulInteractiveElement as _, Styled, Subscription, Task, - UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, div, point, px, size, - uniform_list, -}; -use itertools::Itertools; -use language::{Anchor, BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem}; -use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrevious}; -use std::{ - cmp, - collections::BTreeMap, - hash::Hash, - ops::Range, - path::{Path, PathBuf}, - sync::{ - Arc, OnceLock, - atomic::{self, AtomicBool}, - }, - time::Duration, - u32, -}; - -use outline_panel_settings::{DockSide, OutlinePanelSettings, ShowIndentGuides}; -use project::{File, Fs, GitEntry, GitTraversal, Project, ProjectItem}; -use search::{BufferSearchBar, ProjectSearchView}; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore}; -use smol::channel; -use theme::{SyntaxTheme, ThemeSettings}; -use ui::{ - ContextMenu, FluentBuilder, HighlightedLabel, IconButton, IconButtonShape, IndentGuideColors, - IndentGuideLayout, ListItem, ScrollAxes, Scrollbars, Tab, Tooltip, WithScrollbar, prelude::*, -}; -use util::{RangeExt, ResultExt, TryFutureExt, debug_panic, rel_path::RelPath}; -use workspace::{ - OpenInTerminal, WeakItemHandle, Workspace, - dock::{DockPosition, Panel, PanelEvent}, - item::ItemHandle, - searchable::{SearchEvent, SearchableItem}, -}; -use worktree::{Entry, ProjectEntryId, WorktreeId}; - -actions!( - outline_panel, - [ - /// Collapses all entries in the outline tree. - CollapseAllEntries, - /// Collapses the currently selected entry. - CollapseSelectedEntry, - /// Expands all entries in the outline tree. - ExpandAllEntries, - /// Expands the currently selected entry. - ExpandSelectedEntry, - /// Folds the selected directory. - FoldDirectory, - /// Opens the selected entry in the editor. - OpenSelectedEntry, - /// Reveals the selected item in the system file manager. - RevealInFileManager, - /// Selects the parent of the current entry. - SelectParent, - /// Toggles the pin status of the active editor. - ToggleActiveEditorPin, - /// Unfolds the selected directory. - UnfoldDirectory, - /// Toggles focus on the outline panel. - ToggleFocus, - ] -); - -const OUTLINE_PANEL_KEY: &str = "OutlinePanel"; -const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); - -type Outline = OutlineItem; -type HighlightStyleData = Arc, HighlightStyle)>>>; - -pub struct OutlinePanel { - fs: Arc, - width: Option, - project: Entity, - workspace: WeakEntity, - active: bool, - pinned: bool, - scroll_handle: UniformListScrollHandle, - context_menu: Option<(Entity, Point, Subscription)>, - focus_handle: FocusHandle, - pending_serialization: Task>, - fs_entries_depth: HashMap<(WorktreeId, ProjectEntryId), usize>, - fs_entries: Vec, - fs_children_count: HashMap, FsChildren>>, - collapsed_entries: HashSet, - unfolded_dirs: HashMap>, - selected_entry: SelectedEntry, - active_item: Option, - _subscriptions: Vec, - new_entries_for_fs_update: HashSet, - fs_entries_update_task: Task<()>, - cached_entries_update_task: Task<()>, - reveal_selection_task: Task>, - outline_fetch_tasks: HashMap<(BufferId, ExcerptId), Task<()>>, - excerpts: HashMap>, - cached_entries: Vec, - filter_editor: Entity, - mode: ItemsDisplayMode, - max_width_item_index: Option, - preserve_selection_on_buffer_fold_toggles: HashSet, - pending_default_expansion_depth: Option, - outline_children_cache: HashMap, usize), bool>>, -} - -#[derive(Debug)] -enum ItemsDisplayMode { - Search(SearchState), - Outline, -} - -#[derive(Debug)] -struct SearchState { - kind: SearchKind, - query: String, - matches: Vec<(Range, Arc>)>, - highlight_search_match_tx: channel::Sender, - _search_match_highlighter: Task<()>, - _search_match_notify: Task<()>, -} - -struct HighlightArguments { - multi_buffer_snapshot: MultiBufferSnapshot, - match_range: Range, - search_data: Arc>, -} - -impl SearchState { - fn new( - kind: SearchKind, - query: String, - previous_matches: HashMap, Arc>>, - new_matches: Vec>, - theme: Arc, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let (highlight_search_match_tx, highlight_search_match_rx) = channel::unbounded(); - let (notify_tx, notify_rx) = channel::unbounded::<()>(); - Self { - kind, - query, - matches: new_matches - .into_iter() - .map(|range| { - let search_data = previous_matches - .get(&range) - .map(Arc::clone) - .unwrap_or_default(); - (range, search_data) - }) - .collect(), - highlight_search_match_tx, - _search_match_highlighter: cx.background_spawn(async move { - while let Ok(highlight_arguments) = highlight_search_match_rx.recv().await { - let needs_init = highlight_arguments.search_data.get().is_none(); - let search_data = highlight_arguments.search_data.get_or_init(|| { - SearchData::new( - &highlight_arguments.match_range, - &highlight_arguments.multi_buffer_snapshot, - ) - }); - if needs_init { - notify_tx.try_send(()).ok(); - } - - let highlight_data = &search_data.highlights_data; - if highlight_data.get().is_some() { - continue; - } - let mut left_whitespaces_count = 0; - let mut non_whitespace_symbol_occurred = false; - let context_offset_range = search_data - .context_range - .to_offset(&highlight_arguments.multi_buffer_snapshot); - let mut offset = context_offset_range.start; - let mut context_text = String::new(); - let mut highlight_ranges = Vec::new(); - for mut chunk in highlight_arguments - .multi_buffer_snapshot - .chunks(context_offset_range.start..context_offset_range.end, true) - { - if !non_whitespace_symbol_occurred { - for c in chunk.text.chars() { - if c.is_whitespace() { - left_whitespaces_count += c.len_utf8(); - } else { - non_whitespace_symbol_occurred = true; - break; - } - } - } - - if chunk.text.len() > context_offset_range.end - offset { - chunk.text = &chunk.text[0..(context_offset_range.end - offset)]; - offset = context_offset_range.end; - } else { - offset += chunk.text.len(); - } - let style = chunk - .syntax_highlight_id - .and_then(|highlight| highlight.style(&theme)); - if let Some(style) = style { - let start = context_text.len(); - let end = start + chunk.text.len(); - highlight_ranges.push((start..end, style)); - } - context_text.push_str(chunk.text); - if offset >= context_offset_range.end { - break; - } - } - - highlight_ranges.iter_mut().for_each(|(range, _)| { - range.start = range.start.saturating_sub(left_whitespaces_count); - range.end = range.end.saturating_sub(left_whitespaces_count); - }); - if highlight_data.set(highlight_ranges).ok().is_some() { - notify_tx.try_send(()).ok(); - } - - let trimmed_text = context_text[left_whitespaces_count..].to_owned(); - debug_assert_eq!( - trimmed_text, search_data.context_text, - "Highlighted text that does not match the buffer text" - ); - } - }), - _search_match_notify: cx.spawn_in(window, async move |outline_panel, cx| { - loop { - match notify_rx.recv().await { - Ok(()) => {} - Err(_) => break, - }; - while let Ok(()) = notify_rx.try_recv() { - // - } - let update_result = outline_panel.update(cx, |_, cx| { - cx.notify(); - }); - if update_result.is_err() { - break; - } - } - }), - } - } -} - -#[derive(Debug)] -enum SelectedEntry { - Invalidated(Option), - Valid(PanelEntry, usize), - None, -} - -impl SelectedEntry { - fn invalidate(&mut self) { - match std::mem::replace(self, SelectedEntry::None) { - Self::Valid(entry, _) => *self = Self::Invalidated(Some(entry)), - Self::None => *self = Self::Invalidated(None), - other => *self = other, - } - } - - fn is_invalidated(&self) -> bool { - matches!(self, Self::Invalidated(_)) - } -} - -#[derive(Debug, Clone, Copy, Default)] -struct FsChildren { - files: usize, - dirs: usize, -} - -impl FsChildren { - fn may_be_fold_part(&self) -> bool { - self.dirs == 0 || (self.dirs == 1 && self.files == 0) - } -} - -#[derive(Clone, Debug)] -struct CachedEntry { - depth: usize, - string_match: Option, - entry: PanelEntry, -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -enum CollapsedEntry { - Dir(WorktreeId, ProjectEntryId), - File(WorktreeId, BufferId), - ExternalFile(BufferId), - Excerpt(BufferId, ExcerptId), - Outline(BufferId, ExcerptId, Range), -} - -#[derive(Debug)] -struct Excerpt { - range: ExcerptRange, - outlines: ExcerptOutlines, -} - -impl Excerpt { - fn invalidate_outlines(&mut self) { - if let ExcerptOutlines::Outlines(valid_outlines) = &mut self.outlines { - self.outlines = ExcerptOutlines::Invalidated(std::mem::take(valid_outlines)); - } - } - - fn iter_outlines(&self) -> impl Iterator { - match &self.outlines { - ExcerptOutlines::Outlines(outlines) => outlines.iter(), - ExcerptOutlines::Invalidated(outlines) => outlines.iter(), - ExcerptOutlines::NotFetched => [].iter(), - } - } - - fn should_fetch_outlines(&self) -> bool { - match &self.outlines { - ExcerptOutlines::Outlines(_) => false, - ExcerptOutlines::Invalidated(_) => true, - ExcerptOutlines::NotFetched => true, - } - } -} - -#[derive(Debug)] -enum ExcerptOutlines { - Outlines(Vec), - Invalidated(Vec), - NotFetched, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -struct FoldedDirsEntry { - worktree_id: WorktreeId, - entries: Vec, -} - -// TODO: collapse the inner enums into panel entry -#[derive(Clone, Debug)] -enum PanelEntry { - Fs(FsEntry), - FoldedDirs(FoldedDirsEntry), - Outline(OutlineEntry), - Search(SearchEntry), -} - -#[derive(Clone, Debug)] -struct SearchEntry { - match_range: Range, - kind: SearchKind, - render_data: Arc>, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -enum SearchKind { - Project, - Buffer, -} - -#[derive(Clone, Debug)] -struct SearchData { - context_range: Range, - context_text: String, - truncated_left: bool, - truncated_right: bool, - search_match_indices: Vec>, - highlights_data: HighlightStyleData, -} - -impl PartialEq for PanelEntry { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::Fs(a), Self::Fs(b)) => a == b, - ( - Self::FoldedDirs(FoldedDirsEntry { - worktree_id: worktree_id_a, - entries: entries_a, - }), - Self::FoldedDirs(FoldedDirsEntry { - worktree_id: worktree_id_b, - entries: entries_b, - }), - ) => worktree_id_a == worktree_id_b && entries_a == entries_b, - (Self::Outline(a), Self::Outline(b)) => a == b, - ( - Self::Search(SearchEntry { - match_range: match_range_a, - kind: kind_a, - .. - }), - Self::Search(SearchEntry { - match_range: match_range_b, - kind: kind_b, - .. - }), - ) => match_range_a == match_range_b && kind_a == kind_b, - _ => false, - } - } -} - -impl Eq for PanelEntry {} - -const SEARCH_MATCH_CONTEXT_SIZE: u32 = 40; -const TRUNCATED_CONTEXT_MARK: &str = "…"; - -impl SearchData { - fn new( - match_range: &Range, - multi_buffer_snapshot: &MultiBufferSnapshot, - ) -> Self { - let match_point_range = match_range.to_point(multi_buffer_snapshot); - let context_left_border = multi_buffer_snapshot.clip_point( - language::Point::new( - match_point_range.start.row, - match_point_range - .start - .column - .saturating_sub(SEARCH_MATCH_CONTEXT_SIZE), - ), - Bias::Left, - ); - let context_right_border = multi_buffer_snapshot.clip_point( - language::Point::new( - match_point_range.end.row, - match_point_range.end.column + SEARCH_MATCH_CONTEXT_SIZE, - ), - Bias::Right, - ); - - let context_anchor_range = - (context_left_border..context_right_border).to_anchors(multi_buffer_snapshot); - let context_offset_range = context_anchor_range.to_offset(multi_buffer_snapshot); - let match_offset_range = match_range.to_offset(multi_buffer_snapshot); - - let mut search_match_indices = vec![ - match_offset_range.start - context_offset_range.start - ..match_offset_range.end - context_offset_range.start, - ]; - - let entire_context_text = multi_buffer_snapshot - .text_for_range(context_offset_range.clone()) - .collect::(); - let left_whitespaces_offset = entire_context_text - .chars() - .take_while(|c| c.is_whitespace()) - .map(|c| c.len_utf8()) - .sum::(); - - let mut extended_context_left_border = context_left_border; - extended_context_left_border.column = extended_context_left_border.column.saturating_sub(1); - let extended_context_left_border = - multi_buffer_snapshot.clip_point(extended_context_left_border, Bias::Left); - let mut extended_context_right_border = context_right_border; - extended_context_right_border.column += 1; - let extended_context_right_border = - multi_buffer_snapshot.clip_point(extended_context_right_border, Bias::Right); - - let truncated_left = left_whitespaces_offset == 0 - && extended_context_left_border < context_left_border - && multi_buffer_snapshot - .chars_at(extended_context_left_border) - .last() - .is_some_and(|c| !c.is_whitespace()); - let truncated_right = entire_context_text - .chars() - .last() - .is_none_or(|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()); - search_match_indices.iter_mut().for_each(|range| { - range.start = range.start.saturating_sub(left_whitespaces_offset); - range.end = range.end.saturating_sub(left_whitespaces_offset); - }); - - let trimmed_row_offset_range = - context_offset_range.start + left_whitespaces_offset..context_offset_range.end; - let trimmed_text = entire_context_text[left_whitespaces_offset..].to_owned(); - Self { - highlights_data: Arc::default(), - search_match_indices, - context_range: trimmed_row_offset_range.to_anchors(multi_buffer_snapshot), - context_text: trimmed_text, - truncated_left, - truncated_right, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -struct OutlineEntryExcerpt { - id: ExcerptId, - buffer_id: BufferId, - range: ExcerptRange, -} - -#[derive(Clone, Debug, Eq)] -struct OutlineEntryOutline { - buffer_id: BufferId, - excerpt_id: ExcerptId, - outline: Outline, -} - -impl PartialEq for OutlineEntryOutline { - fn eq(&self, other: &Self) -> bool { - self.buffer_id == other.buffer_id - && self.excerpt_id == other.excerpt_id - && self.outline.depth == other.outline.depth - && self.outline.range == other.outline.range - && self.outline.text == other.outline.text - } -} - -impl Hash for OutlineEntryOutline { - fn hash(&self, state: &mut H) { - ( - self.buffer_id, - self.excerpt_id, - self.outline.depth, - &self.outline.range, - &self.outline.text, - ) - .hash(state); - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -enum OutlineEntry { - Excerpt(OutlineEntryExcerpt), - Outline(OutlineEntryOutline), -} - -impl OutlineEntry { - fn ids(&self) -> (BufferId, ExcerptId) { - match self { - OutlineEntry::Excerpt(excerpt) => (excerpt.buffer_id, excerpt.id), - OutlineEntry::Outline(outline) => (outline.buffer_id, outline.excerpt_id), - } - } -} - -#[derive(Debug, Clone, Eq)] -struct FsEntryFile { - worktree_id: WorktreeId, - entry: GitEntry, - buffer_id: BufferId, - excerpts: Vec, -} - -impl PartialEq for FsEntryFile { - fn eq(&self, other: &Self) -> bool { - self.worktree_id == other.worktree_id - && self.entry.id == other.entry.id - && self.buffer_id == other.buffer_id - } -} - -impl Hash for FsEntryFile { - fn hash(&self, state: &mut H) { - (self.buffer_id, self.entry.id, self.worktree_id).hash(state); - } -} - -#[derive(Debug, Clone, Eq)] -struct FsEntryDirectory { - worktree_id: WorktreeId, - entry: GitEntry, -} - -impl PartialEq for FsEntryDirectory { - fn eq(&self, other: &Self) -> bool { - self.worktree_id == other.worktree_id && self.entry.id == other.entry.id - } -} - -impl Hash for FsEntryDirectory { - fn hash(&self, state: &mut H) { - (self.worktree_id, self.entry.id).hash(state); - } -} - -#[derive(Debug, Clone, Eq)] -struct FsEntryExternalFile { - buffer_id: BufferId, - excerpts: Vec, -} - -impl PartialEq for FsEntryExternalFile { - fn eq(&self, other: &Self) -> bool { - self.buffer_id == other.buffer_id - } -} - -impl Hash for FsEntryExternalFile { - fn hash(&self, state: &mut H) { - self.buffer_id.hash(state); - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum FsEntry { - ExternalFile(FsEntryExternalFile), - Directory(FsEntryDirectory), - File(FsEntryFile), -} - -struct ActiveItem { - item_handle: Box, - active_editor: WeakEntity, - _buffer_search_subscription: Subscription, - _editor_subscription: Subscription, -} - -#[derive(Debug)] -pub enum Event { - Focus, -} - -#[derive(Serialize, Deserialize)] -struct SerializedOutlinePanel { - width: Option, - active: Option, -} - -pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, _, _| { - workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { - workspace.toggle_panel_focus::(window, cx); - }); - }) - .detach(); -} - -impl OutlinePanel { - pub async fn load( - workspace: WeakEntity, - mut cx: AsyncWindowContext, - ) -> anyhow::Result> { - let serialized_panel = match workspace - .read_with(&cx, |workspace, _| { - OutlinePanel::serialization_key(workspace) - }) - .ok() - .flatten() - { - Some(serialization_key) => cx - .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) }) - .await - .context("loading outline panel") - .log_err() - .flatten() - .map(|panel| serde_json::from_str::(&panel)) - .transpose() - .log_err() - .flatten(), - None => None, - }; - - workspace.update_in(&mut cx, |workspace, window, cx| { - let panel = Self::new(workspace, window, cx); - if let Some(serialized_panel) = serialized_panel { - panel.update(cx, |panel, cx| { - panel.width = serialized_panel.width.map(|px| px.round()); - panel.active = serialized_panel.active.unwrap_or(false); - cx.notify(); - }); - } - panel - }) - } - - fn new( - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let project = workspace.project().clone(); - let workspace_handle = cx.entity().downgrade(); - - cx.new(|cx| { - let filter_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Search buffer symbols…", window, cx); - editor - }); - let filter_update_subscription = cx.subscribe_in( - &filter_editor, - window, - |outline_panel: &mut Self, _, event, window, cx| { - if let editor::EditorEvent::BufferEdited = event { - outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); - } - }, - ); - - let focus_handle = cx.focus_handle(); - let focus_subscription = cx.on_focus(&focus_handle, window, Self::focus_in); - let workspace_subscription = cx.subscribe_in( - &workspace - .weak_handle() - .upgrade() - .expect("have a &mut Workspace"), - window, - move |outline_panel, workspace, event, window, cx| { - if let workspace::Event::ActiveItemChanged = event { - if let Some((new_active_item, new_active_editor)) = - workspace_active_editor(workspace.read(cx), cx) - { - if outline_panel.should_replace_active_item(new_active_item.as_ref()) { - outline_panel.replace_active_editor( - new_active_item, - new_active_editor, - window, - cx, - ); - } - } else { - outline_panel.clear_previous(window, cx); - cx.notify(); - } - } - }, - ); - - let icons_subscription = cx.observe_global::(|_, cx| { - cx.notify(); - }); - - let mut outline_panel_settings = *OutlinePanelSettings::get_global(cx); - let mut current_theme = ThemeSettings::get_global(cx).clone(); - let settings_subscription = - cx.observe_global_in::(window, move |outline_panel, window, cx| { - let new_settings = OutlinePanelSettings::get_global(cx); - let new_theme = ThemeSettings::get_global(cx); - if ¤t_theme != new_theme { - outline_panel_settings = *new_settings; - current_theme = new_theme.clone(); - for excerpts in outline_panel.excerpts.values_mut() { - for excerpt in excerpts.values_mut() { - excerpt.invalidate_outlines(); - } - } - let update_cached_items = outline_panel.update_non_fs_items(window, cx); - if update_cached_items { - outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); - } - } else if &outline_panel_settings != new_settings { - let old_expansion_depth = outline_panel_settings.expand_outlines_with_depth; - outline_panel_settings = *new_settings; - - if old_expansion_depth != new_settings.expand_outlines_with_depth { - let old_collapsed_entries = outline_panel.collapsed_entries.clone(); - outline_panel - .collapsed_entries - .retain(|entry| !matches!(entry, CollapsedEntry::Outline(..))); - - let new_depth = new_settings.expand_outlines_with_depth; - - for (buffer_id, excerpts) in &outline_panel.excerpts { - for (excerpt_id, excerpt) in excerpts { - if let ExcerptOutlines::Outlines(outlines) = &excerpt.outlines { - for outline in outlines { - if outline_panel - .outline_children_cache - .get(buffer_id) - .and_then(|children_map| { - let key = - (outline.range.clone(), outline.depth); - children_map.get(&key) - }) - .copied() - .unwrap_or(false) - && (new_depth == 0 || outline.depth >= new_depth) - { - outline_panel.collapsed_entries.insert( - CollapsedEntry::Outline( - *buffer_id, - *excerpt_id, - outline.range.clone(), - ), - ); - } - } - } - } - } - - if old_collapsed_entries != outline_panel.collapsed_entries { - outline_panel.update_cached_entries( - Some(UPDATE_DEBOUNCE), - window, - cx, - ); - } - } else { - cx.notify(); - } - } - }); - - let scroll_handle = UniformListScrollHandle::new(); - - let mut outline_panel = Self { - mode: ItemsDisplayMode::Outline, - active: false, - pinned: false, - workspace: workspace_handle, - project, - fs: workspace.app_state().fs.clone(), - max_width_item_index: None, - scroll_handle, - focus_handle, - filter_editor, - fs_entries: Vec::new(), - fs_entries_depth: HashMap::default(), - fs_children_count: HashMap::default(), - collapsed_entries: HashSet::default(), - unfolded_dirs: HashMap::default(), - selected_entry: SelectedEntry::None, - context_menu: None, - width: None, - active_item: None, - pending_serialization: Task::ready(None), - new_entries_for_fs_update: HashSet::default(), - preserve_selection_on_buffer_fold_toggles: HashSet::default(), - pending_default_expansion_depth: None, - fs_entries_update_task: Task::ready(()), - cached_entries_update_task: Task::ready(()), - reveal_selection_task: Task::ready(Ok(())), - outline_fetch_tasks: HashMap::default(), - excerpts: HashMap::default(), - cached_entries: Vec::new(), - _subscriptions: vec![ - settings_subscription, - icons_subscription, - focus_subscription, - workspace_subscription, - filter_update_subscription, - ], - outline_children_cache: HashMap::default(), - }; - if let Some((item, editor)) = workspace_active_editor(workspace, cx) { - outline_panel.replace_active_editor(item, editor, window, cx); - } - outline_panel - }) - } - - fn serialization_key(workspace: &Workspace) -> Option { - workspace - .database_id() - .map(|id| i64::from(id).to_string()) - .or(workspace.session_id()) - .map(|id| format!("{}-{:?}", OUTLINE_PANEL_KEY, id)) - } - - fn serialize(&mut self, cx: &mut Context) { - let Some(serialization_key) = self - .workspace - .read_with(cx, |workspace, _| { - OutlinePanel::serialization_key(workspace) - }) - .ok() - .flatten() - else { - return; - }; - let width = self.width; - let active = Some(self.active); - self.pending_serialization = cx.background_spawn( - async move { - KEY_VALUE_STORE - .write_kvp( - serialization_key, - serde_json::to_string(&SerializedOutlinePanel { width, active })?, - ) - .await?; - anyhow::Ok(()) - } - .log_err(), - ); - } - - fn dispatch_context(&self, window: &mut Window, cx: &mut Context) -> KeyContext { - let mut dispatch_context = KeyContext::new_with_defaults(); - dispatch_context.add("OutlinePanel"); - dispatch_context.add("menu"); - let identifier = if self.filter_editor.focus_handle(cx).is_focused(window) { - "editing" - } else { - "not_editing" - }; - dispatch_context.add(identifier); - dispatch_context - } - - fn unfold_directory( - &mut self, - _: &UnfoldDirectory, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(PanelEntry::FoldedDirs(FoldedDirsEntry { - worktree_id, - entries, - .. - })) = self.selected_entry().cloned() - { - self.unfolded_dirs - .entry(worktree_id) - .or_default() - .extend(entries.iter().map(|entry| entry.id)); - self.update_cached_entries(None, window, cx); - } - } - - fn fold_directory(&mut self, _: &FoldDirectory, window: &mut Window, cx: &mut Context) { - let (worktree_id, entry) = match self.selected_entry().cloned() { - Some(PanelEntry::Fs(FsEntry::Directory(directory))) => { - (directory.worktree_id, Some(directory.entry)) - } - Some(PanelEntry::FoldedDirs(folded_dirs)) => { - (folded_dirs.worktree_id, folded_dirs.entries.last().cloned()) - } - _ => return, - }; - let Some(entry) = entry else { - return; - }; - let unfolded_dirs = self.unfolded_dirs.get_mut(&worktree_id); - let worktree = self - .project - .read(cx) - .worktree_for_id(worktree_id, cx) - .map(|w| w.read(cx).snapshot()); - let Some((_, unfolded_dirs)) = worktree.zip(unfolded_dirs) else { - return; - }; - - unfolded_dirs.remove(&entry.id); - self.update_cached_entries(None, window, cx); - } - - fn open_selected_entry( - &mut self, - _: &OpenSelectedEntry, - window: &mut Window, - cx: &mut Context, - ) { - if self.filter_editor.focus_handle(cx).is_focused(window) { - cx.propagate() - } else if let Some(selected_entry) = self.selected_entry().cloned() { - self.scroll_editor_to_entry(&selected_entry, true, true, window, cx); - } - } - - fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context) { - if self.filter_editor.focus_handle(cx).is_focused(window) { - self.focus_handle.focus(window); - } else { - self.filter_editor.focus_handle(cx).focus(window); - } - - if self.context_menu.is_some() { - self.context_menu.take(); - cx.notify(); - } - } - - fn open_excerpts( - &mut self, - action: &editor::actions::OpenExcerpts, - window: &mut Window, - cx: &mut Context, - ) { - if self.filter_editor.focus_handle(cx).is_focused(window) { - cx.propagate() - } else if let Some((active_editor, selected_entry)) = - self.active_editor().zip(self.selected_entry().cloned()) - { - self.scroll_editor_to_entry(&selected_entry, true, true, window, cx); - active_editor.update(cx, |editor, cx| editor.open_excerpts(action, window, cx)); - } - } - - fn open_excerpts_split( - &mut self, - action: &editor::actions::OpenExcerptsSplit, - window: &mut Window, - cx: &mut Context, - ) { - if self.filter_editor.focus_handle(cx).is_focused(window) { - cx.propagate() - } else if let Some((active_editor, selected_entry)) = - self.active_editor().zip(self.selected_entry().cloned()) - { - self.scroll_editor_to_entry(&selected_entry, true, true, window, cx); - active_editor.update(cx, |editor, cx| { - editor.open_excerpts_in_split(action, window, cx) - }); - } - } - - fn scroll_editor_to_entry( - &mut self, - entry: &PanelEntry, - prefer_selection_change: bool, - prefer_focus_change: bool, - window: &mut Window, - cx: &mut Context, - ) { - let Some(active_editor) = self.active_editor() else { - return; - }; - let active_multi_buffer = active_editor.read(cx).buffer().clone(); - let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx); - let mut change_selection = prefer_selection_change; - let mut change_focus = prefer_focus_change; - let mut scroll_to_buffer = None; - let scroll_target = match entry { - PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => { - change_focus = false; - None - } - PanelEntry::Fs(FsEntry::ExternalFile(file)) => { - change_selection = false; - scroll_to_buffer = Some(file.buffer_id); - multi_buffer_snapshot.excerpts().find_map( - |(excerpt_id, buffer_snapshot, excerpt_range)| { - if buffer_snapshot.remote_id() == file.buffer_id { - multi_buffer_snapshot - .anchor_in_excerpt(excerpt_id, excerpt_range.context.start) - } else { - None - } - }, - ) - } - - PanelEntry::Fs(FsEntry::File(file)) => { - change_selection = false; - scroll_to_buffer = Some(file.buffer_id); - self.project - .update(cx, |project, cx| { - project - .path_for_entry(file.entry.id, cx) - .and_then(|path| project.get_open_buffer(&path, cx)) - }) - .map(|buffer| { - active_multi_buffer - .read(cx) - .excerpts_for_buffer(buffer.read(cx).remote_id(), cx) - }) - .and_then(|excerpts| { - let (excerpt_id, excerpt_range) = excerpts.first()?; - multi_buffer_snapshot - .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start) - }) - } - PanelEntry::Outline(OutlineEntry::Outline(outline)) => multi_buffer_snapshot - .anchor_in_excerpt(outline.excerpt_id, outline.outline.range.start) - .or_else(|| { - multi_buffer_snapshot - .anchor_in_excerpt(outline.excerpt_id, outline.outline.range.end) - }), - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { - change_selection = false; - change_focus = false; - multi_buffer_snapshot.anchor_in_excerpt(excerpt.id, excerpt.range.context.start) - } - PanelEntry::Search(search_entry) => Some(search_entry.match_range.start), - }; - - if let Some(anchor) = scroll_target { - let activate = self - .workspace - .update(cx, |workspace, cx| match self.active_item() { - Some(active_item) => workspace.activate_item( - active_item.as_ref(), - true, - change_focus, - window, - cx, - ), - None => workspace.activate_item(&active_editor, true, change_focus, window, cx), - }); - - if activate.is_ok() { - self.select_entry(entry.clone(), true, window, cx); - if change_selection { - active_editor.update(cx, |editor, cx| { - editor.change_selections( - SelectionEffects::scroll(Autoscroll::center()), - window, - cx, - |s| s.select_ranges(Some(anchor..anchor)), - ); - }); - } 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 f64); - } - - active_editor.update(cx, |editor, cx| { - editor.set_scroll_anchor(ScrollAnchor { offset, anchor }, window, cx); - }); - } - - if change_focus { - active_editor.focus_handle(cx).focus(window); - } else { - self.focus_handle.focus(window); - } - } - } - } - - fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context) { - if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| { - self.cached_entries - .iter() - .map(|cached_entry| &cached_entry.entry) - .skip_while(|entry| entry != &selected_entry) - .nth(1) - .cloned() - }) { - self.select_entry(entry_to_select, true, window, cx); - } else { - self.select_first(&SelectFirst {}, window, cx) - } - if let Some(selected_entry) = self.selected_entry().cloned() { - self.scroll_editor_to_entry(&selected_entry, true, false, window, cx); - } - } - - fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context) { - if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| { - self.cached_entries - .iter() - .rev() - .map(|cached_entry| &cached_entry.entry) - .skip_while(|entry| entry != &selected_entry) - .nth(1) - .cloned() - }) { - self.select_entry(entry_to_select, true, window, cx); - } else { - self.select_last(&SelectLast, window, cx) - } - if let Some(selected_entry) = self.selected_entry().cloned() { - self.scroll_editor_to_entry(&selected_entry, true, false, window, cx); - } - } - - fn select_parent(&mut self, _: &SelectParent, window: &mut Window, cx: &mut Context) { - if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| { - let mut previous_entries = self - .cached_entries - .iter() - .rev() - .map(|cached_entry| &cached_entry.entry) - .skip_while(|entry| entry != &selected_entry) - .skip(1); - match &selected_entry { - PanelEntry::Fs(fs_entry) => match fs_entry { - FsEntry::ExternalFile(..) => None, - FsEntry::File(FsEntryFile { - worktree_id, entry, .. - }) - | FsEntry::Directory(FsEntryDirectory { - worktree_id, entry, .. - }) => entry.path.parent().and_then(|parent_path| { - previous_entries.find(|entry| match entry { - PanelEntry::Fs(FsEntry::Directory(directory)) => { - directory.worktree_id == *worktree_id - && directory.entry.path.as_ref() == parent_path - } - PanelEntry::FoldedDirs(FoldedDirsEntry { - worktree_id: dirs_worktree_id, - entries: dirs, - .. - }) => { - dirs_worktree_id == worktree_id - && dirs - .last() - .is_some_and(|dir| dir.path.as_ref() == parent_path) - } - _ => false, - }) - }), - }, - PanelEntry::FoldedDirs(folded_dirs) => folded_dirs - .entries - .first() - .and_then(|entry| entry.path.parent()) - .and_then(|parent_path| { - previous_entries.find(|entry| { - if let PanelEntry::Fs(FsEntry::Directory(directory)) = entry { - directory.worktree_id == folded_dirs.worktree_id - && directory.entry.path.as_ref() == parent_path - } else { - false - } - }) - }), - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { - previous_entries.find(|entry| match entry { - PanelEntry::Fs(FsEntry::File(file)) => { - file.buffer_id == excerpt.buffer_id - && file.excerpts.contains(&excerpt.id) - } - PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => { - external_file.buffer_id == excerpt.buffer_id - && external_file.excerpts.contains(&excerpt.id) - } - _ => false, - }) - } - PanelEntry::Outline(OutlineEntry::Outline(outline)) => { - previous_entries.find(|entry| { - if let PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) = entry { - outline.buffer_id == excerpt.buffer_id - && outline.excerpt_id == excerpt.id - } else { - false - } - }) - } - PanelEntry::Search(_) => { - previous_entries.find(|entry| !matches!(entry, PanelEntry::Search(_))) - } - } - }) { - self.select_entry(entry_to_select.clone(), true, window, cx); - } else { - self.select_first(&SelectFirst {}, window, cx); - } - } - - fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context) { - if let Some(first_entry) = self.cached_entries.first() { - self.select_entry(first_entry.entry.clone(), true, window, cx); - } - } - - fn select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context) { - if let Some(new_selection) = self - .cached_entries - .iter() - .rev() - .map(|cached_entry| &cached_entry.entry) - .next() - { - self.select_entry(new_selection.clone(), true, window, cx); - } - } - - fn autoscroll(&mut self, cx: &mut Context) { - if let Some(selected_entry) = self.selected_entry() { - let index = self - .cached_entries - .iter() - .position(|cached_entry| &cached_entry.entry == selected_entry); - if let Some(index) = index { - self.scroll_handle - .scroll_to_item(index, ScrollStrategy::Center); - cx.notify(); - } - } - } - - fn focus_in(&mut self, window: &mut Window, cx: &mut Context) { - if !self.focus_handle.contains_focused(window, cx) { - cx.emit(Event::Focus); - } - } - - fn deploy_context_menu( - &mut self, - position: Point, - entry: PanelEntry, - window: &mut Window, - cx: &mut Context, - ) { - self.select_entry(entry.clone(), true, window, cx); - let is_root = match &entry { - PanelEntry::Fs(FsEntry::File(FsEntryFile { - worktree_id, entry, .. - })) - | PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { - worktree_id, entry, .. - })) => self - .project - .read(cx) - .worktree_for_id(*worktree_id, cx) - .map(|worktree| { - worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id) - }) - .unwrap_or(false), - PanelEntry::FoldedDirs(FoldedDirsEntry { - worktree_id, - entries, - .. - }) => entries - .first() - .and_then(|entry| { - self.project - .read(cx) - .worktree_for_id(*worktree_id, cx) - .map(|worktree| { - worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id) - }) - }) - .unwrap_or(false), - PanelEntry::Fs(FsEntry::ExternalFile(..)) => false, - PanelEntry::Outline(..) => { - cx.notify(); - return; - } - PanelEntry::Search(_) => { - cx.notify(); - return; - } - }; - let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs; - let is_foldable = auto_fold_dirs && !is_root && self.is_foldable(&entry); - let is_unfoldable = auto_fold_dirs && !is_root && self.is_unfoldable(&entry); - - let context_menu = ContextMenu::build(window, cx, |menu, _, _| { - menu.context(self.focus_handle.clone()) - .when(cfg!(target_os = "macos"), |menu| { - menu.action("Reveal in Finder", Box::new(RevealInFileManager)) - }) - .when(cfg!(not(target_os = "macos")), |menu| { - menu.action("Reveal in File Manager", Box::new(RevealInFileManager)) - }) - .action("Open in Terminal", Box::new(OpenInTerminal)) - .when(is_unfoldable, |menu| { - menu.action("Unfold Directory", Box::new(UnfoldDirectory)) - }) - .when(is_foldable, |menu| { - menu.action("Fold Directory", Box::new(FoldDirectory)) - }) - .separator() - .action("Copy Path", Box::new(zed_actions::workspace::CopyPath)) - .action( - "Copy Relative Path", - Box::new(zed_actions::workspace::CopyRelativePath), - ) - }); - window.focus(&context_menu.focus_handle(cx)); - let subscription = cx.subscribe(&context_menu, |outline_panel, _, _: &DismissEvent, cx| { - outline_panel.context_menu.take(); - cx.notify(); - }); - self.context_menu = Some((context_menu, position, subscription)); - cx.notify(); - } - - fn is_unfoldable(&self, entry: &PanelEntry) -> bool { - matches!(entry, PanelEntry::FoldedDirs(..)) - } - - fn is_foldable(&self, entry: &PanelEntry) -> bool { - let (directory_worktree, directory_entry) = match entry { - PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { - worktree_id, - entry: directory_entry, - .. - })) => (*worktree_id, Some(directory_entry)), - _ => return false, - }; - let Some(directory_entry) = directory_entry else { - return false; - }; - - if self - .unfolded_dirs - .get(&directory_worktree) - .is_none_or(|unfolded_dirs| !unfolded_dirs.contains(&directory_entry.id)) - { - return false; - } - - let children = self - .fs_children_count - .get(&directory_worktree) - .and_then(|entries| entries.get(&directory_entry.path)) - .copied() - .unwrap_or_default(); - - children.may_be_fold_part() && children.dirs > 0 - } - - fn expand_selected_entry( - &mut self, - _: &ExpandSelectedEntry, - window: &mut Window, - cx: &mut Context, - ) { - let Some(active_editor) = self.active_editor() else { - return; - }; - let Some(selected_entry) = self.selected_entry().cloned() else { - return; - }; - let mut buffers_to_unfold = HashSet::default(); - let entry_to_expand = match &selected_entry { - PanelEntry::FoldedDirs(FoldedDirsEntry { - entries: dir_entries, - worktree_id, - .. - }) => dir_entries.last().map(|entry| { - buffers_to_unfold.extend(self.buffers_inside_directory(*worktree_id, entry)); - CollapsedEntry::Dir(*worktree_id, entry.id) - }), - PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { - worktree_id, entry, .. - })) => { - buffers_to_unfold.extend(self.buffers_inside_directory(*worktree_id, entry)); - Some(CollapsedEntry::Dir(*worktree_id, entry.id)) - } - PanelEntry::Fs(FsEntry::File(FsEntryFile { - worktree_id, - buffer_id, - .. - })) => { - buffers_to_unfold.insert(*buffer_id); - Some(CollapsedEntry::File(*worktree_id, *buffer_id)) - } - PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => { - buffers_to_unfold.insert(external_file.buffer_id); - Some(CollapsedEntry::ExternalFile(external_file.buffer_id)) - } - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { - Some(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)) - } - PanelEntry::Outline(OutlineEntry::Outline(outline)) => Some(CollapsedEntry::Outline( - outline.buffer_id, - outline.excerpt_id, - outline.outline.range.clone(), - )), - PanelEntry::Search(_) => return, - }; - let Some(collapsed_entry) = entry_to_expand else { - return; - }; - let expanded = self.collapsed_entries.remove(&collapsed_entry); - if expanded { - if let CollapsedEntry::Dir(worktree_id, dir_entry_id) = collapsed_entry { - let task = self.project.update(cx, |project, cx| { - project.expand_entry(worktree_id, dir_entry_id, cx) - }); - if let Some(task) = task { - task.detach_and_log_err(cx); - } - }; - - active_editor.update(cx, |editor, cx| { - buffers_to_unfold.retain(|buffer_id| editor.is_buffer_folded(*buffer_id, cx)); - }); - self.select_entry(selected_entry, true, window, cx); - if buffers_to_unfold.is_empty() { - self.update_cached_entries(None, window, cx); - } else { - self.toggle_buffers_fold(buffers_to_unfold, false, window, cx) - .detach(); - } - } else { - self.select_next(&SelectNext, window, cx) - } - } - - fn collapse_selected_entry( - &mut self, - _: &CollapseSelectedEntry, - window: &mut Window, - cx: &mut Context, - ) { - let Some(active_editor) = self.active_editor() else { - return; - }; - let Some(selected_entry) = self.selected_entry().cloned() else { - return; - }; - - let mut buffers_to_fold = HashSet::default(); - let collapsed = match &selected_entry { - PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { - worktree_id, entry, .. - })) => { - if self - .collapsed_entries - .insert(CollapsedEntry::Dir(*worktree_id, entry.id)) - { - buffers_to_fold.extend(self.buffers_inside_directory(*worktree_id, entry)); - true - } else { - false - } - } - PanelEntry::Fs(FsEntry::File(FsEntryFile { - worktree_id, - buffer_id, - .. - })) => { - if self - .collapsed_entries - .insert(CollapsedEntry::File(*worktree_id, *buffer_id)) - { - buffers_to_fold.insert(*buffer_id); - true - } else { - false - } - } - PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => { - if self - .collapsed_entries - .insert(CollapsedEntry::ExternalFile(external_file.buffer_id)) - { - buffers_to_fold.insert(external_file.buffer_id); - true - } else { - false - } - } - PanelEntry::FoldedDirs(folded_dirs) => { - let mut folded = false; - if let Some(dir_entry) = folded_dirs.entries.last() - && 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 - } - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self - .collapsed_entries - .insert(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)), - PanelEntry::Outline(OutlineEntry::Outline(outline)) => { - self.collapsed_entries.insert(CollapsedEntry::Outline( - outline.buffer_id, - outline.excerpt_id, - outline.outline.range.clone(), - )) - } - PanelEntry::Search(_) => false, - }; - - if collapsed { - active_editor.update(cx, |editor, cx| { - buffers_to_fold.retain(|buffer_id| !editor.is_buffer_folded(*buffer_id, cx)); - }); - self.select_entry(selected_entry, true, window, cx); - if buffers_to_fold.is_empty() { - self.update_cached_entries(None, window, cx); - } else { - self.toggle_buffers_fold(buffers_to_fold, true, window, cx) - .detach(); - } - } else { - self.select_parent(&SelectParent, window, cx); - } - } - - pub fn expand_all_entries( - &mut self, - _: &ExpandAllEntries, - window: &mut Window, - cx: &mut Context, - ) { - let Some(active_editor) = self.active_editor() else { - return; - }; - - let mut to_uncollapse: HashSet = HashSet::default(); - let mut buffers_to_unfold: HashSet = HashSet::default(); - - for fs_entry in &self.fs_entries { - match fs_entry { - FsEntry::File(FsEntryFile { - worktree_id, - buffer_id, - .. - }) => { - to_uncollapse.insert(CollapsedEntry::File(*worktree_id, *buffer_id)); - buffers_to_unfold.insert(*buffer_id); - } - FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => { - to_uncollapse.insert(CollapsedEntry::ExternalFile(*buffer_id)); - buffers_to_unfold.insert(*buffer_id); - } - FsEntry::Directory(FsEntryDirectory { - worktree_id, entry, .. - }) => { - to_uncollapse.insert(CollapsedEntry::Dir(*worktree_id, entry.id)); - } - } - } - - for (&buffer_id, excerpts) in &self.excerpts { - for (&excerpt_id, excerpt) in excerpts { - match &excerpt.outlines { - ExcerptOutlines::Outlines(outlines) => { - for outline in outlines { - to_uncollapse.insert(CollapsedEntry::Outline( - buffer_id, - excerpt_id, - outline.range.clone(), - )); - } - } - ExcerptOutlines::Invalidated(outlines) => { - for outline in outlines { - to_uncollapse.insert(CollapsedEntry::Outline( - buffer_id, - excerpt_id, - outline.range.clone(), - )); - } - } - ExcerptOutlines::NotFetched => {} - } - to_uncollapse.insert(CollapsedEntry::Excerpt(buffer_id, excerpt_id)); - } - } - - for cached in &self.cached_entries { - if let PanelEntry::FoldedDirs(FoldedDirsEntry { - worktree_id, - entries, - .. - }) = &cached.entry - { - if let Some(last) = entries.last() { - to_uncollapse.insert(CollapsedEntry::Dir(*worktree_id, last.id)); - } - } - } - - self.collapsed_entries - .retain(|entry| !to_uncollapse.contains(entry)); - - active_editor.update(cx, |editor, cx| { - buffers_to_unfold.retain(|buffer_id| editor.is_buffer_folded(*buffer_id, cx)); - }); - - if buffers_to_unfold.is_empty() { - self.update_cached_entries(None, window, cx); - } else { - self.toggle_buffers_fold(buffers_to_unfold, false, window, cx) - .detach(); - } - } - - pub fn collapse_all_entries( - &mut self, - _: &CollapseAllEntries, - window: &mut Window, - cx: &mut Context, - ) { - let Some(active_editor) = self.active_editor() else { - return; - }; - let mut buffers_to_fold = HashSet::default(); - self.collapsed_entries - .extend(self.cached_entries.iter().filter_map( - |cached_entry| match &cached_entry.entry { - PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { - worktree_id, - entry, - .. - })) => Some(CollapsedEntry::Dir(*worktree_id, entry.id)), - PanelEntry::Fs(FsEntry::File(FsEntryFile { - worktree_id, - buffer_id, - .. - })) => { - buffers_to_fold.insert(*buffer_id); - Some(CollapsedEntry::File(*worktree_id, *buffer_id)) - } - PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => { - buffers_to_fold.insert(external_file.buffer_id); - Some(CollapsedEntry::ExternalFile(external_file.buffer_id)) - } - PanelEntry::FoldedDirs(FoldedDirsEntry { - worktree_id, - entries, - .. - }) => Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id)), - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { - Some(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)) - } - PanelEntry::Outline(OutlineEntry::Outline(outline)) => { - Some(CollapsedEntry::Outline( - outline.buffer_id, - outline.excerpt_id, - outline.outline.range.clone(), - )) - } - PanelEntry::Search(_) => None, - }, - )); - - active_editor.update(cx, |editor, cx| { - buffers_to_fold.retain(|buffer_id| !editor.is_buffer_folded(*buffer_id, cx)); - }); - if buffers_to_fold.is_empty() { - self.update_cached_entries(None, window, cx); - } else { - self.toggle_buffers_fold(buffers_to_fold, true, window, cx) - .detach(); - } - } - - fn toggle_expanded(&mut self, entry: &PanelEntry, window: &mut Window, cx: &mut Context) { - let Some(active_editor) = self.active_editor() else { - return; - }; - let mut fold = false; - let mut buffers_to_toggle = HashSet::default(); - match entry { - PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { - worktree_id, - entry: dir_entry, - .. - })) => { - let entry_id = dir_entry.id; - let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id); - buffers_to_toggle.extend(self.buffers_inside_directory(*worktree_id, dir_entry)); - if self.collapsed_entries.remove(&collapsed_entry) { - self.project - .update(cx, |project, cx| { - project.expand_entry(*worktree_id, entry_id, cx) - }) - .unwrap_or_else(|| Task::ready(Ok(()))) - .detach_and_log_err(cx); - } else { - self.collapsed_entries.insert(collapsed_entry); - fold = true; - } - } - PanelEntry::Fs(FsEntry::File(FsEntryFile { - worktree_id, - buffer_id, - .. - })) => { - let collapsed_entry = CollapsedEntry::File(*worktree_id, *buffer_id); - buffers_to_toggle.insert(*buffer_id); - if !self.collapsed_entries.remove(&collapsed_entry) { - self.collapsed_entries.insert(collapsed_entry); - fold = true; - } - } - PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => { - let collapsed_entry = CollapsedEntry::ExternalFile(external_file.buffer_id); - buffers_to_toggle.insert(external_file.buffer_id); - if !self.collapsed_entries.remove(&collapsed_entry) { - self.collapsed_entries.insert(collapsed_entry); - fold = true; - } - } - PanelEntry::FoldedDirs(FoldedDirsEntry { - worktree_id, - entries: dir_entries, - .. - }) => { - if let Some(dir_entry) = dir_entries.first() { - let entry_id = dir_entry.id; - let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id); - buffers_to_toggle - .extend(self.buffers_inside_directory(*worktree_id, dir_entry)); - if self.collapsed_entries.remove(&collapsed_entry) { - self.project - .update(cx, |project, cx| { - project.expand_entry(*worktree_id, entry_id, cx) - }) - .unwrap_or_else(|| Task::ready(Ok(()))) - .detach_and_log_err(cx); - } else { - self.collapsed_entries.insert(collapsed_entry); - fold = true; - } - } - } - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { - let collapsed_entry = CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id); - if !self.collapsed_entries.remove(&collapsed_entry) { - self.collapsed_entries.insert(collapsed_entry); - } - } - PanelEntry::Outline(OutlineEntry::Outline(outline)) => { - let collapsed_entry = CollapsedEntry::Outline( - outline.buffer_id, - outline.excerpt_id, - outline.outline.range.clone(), - ); - if !self.collapsed_entries.remove(&collapsed_entry) { - self.collapsed_entries.insert(collapsed_entry); - } - } - _ => {} - } - - active_editor.update(cx, |editor, cx| { - buffers_to_toggle.retain(|buffer_id| { - let folded = editor.is_buffer_folded(*buffer_id, cx); - if fold { !folded } else { folded } - }); - }); - - self.select_entry(entry.clone(), true, window, cx); - if buffers_to_toggle.is_empty() { - self.update_cached_entries(None, window, cx); - } else { - self.toggle_buffers_fold(buffers_to_toggle, fold, window, cx) - .detach(); - } - } - - fn toggle_buffers_fold( - &self, - buffers: HashSet, - fold: bool, - window: &mut Window, - cx: &mut Context, - ) -> Task<()> { - let Some(active_editor) = self.active_editor() else { - return Task::ready(()); - }; - cx.spawn_in(window, async move |outline_panel, cx| { - outline_panel - .update_in(cx, |outline_panel, window, cx| { - active_editor.update(cx, |editor, cx| { - for buffer_id in buffers { - outline_panel - .preserve_selection_on_buffer_fold_toggles - .insert(buffer_id); - if fold { - editor.fold_buffer(buffer_id, cx); - } else { - editor.unfold_buffer(buffer_id, cx); - } - } - }); - if let Some(selection) = outline_panel.selected_entry().cloned() { - outline_panel.scroll_editor_to_entry(&selection, false, false, window, cx); - } - }) - .ok(); - }) - } - - fn copy_path( - &mut self, - _: &zed_actions::workspace::CopyPath, - _: &mut Window, - cx: &mut Context, - ) { - if let Some(clipboard_text) = self - .selected_entry() - .and_then(|entry| self.abs_path(entry, cx)) - .map(|p| p.to_string_lossy().into_owned()) - { - cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text)); - } - } - - fn copy_relative_path( - &mut self, - _: &zed_actions::workspace::CopyRelativePath, - _: &mut Window, - cx: &mut Context, - ) { - let path_style = self.project.read(cx).path_style(cx); - if let Some(clipboard_text) = self - .selected_entry() - .and_then(|entry| match entry { - PanelEntry::Fs(entry) => self.relative_path(entry, cx), - PanelEntry::FoldedDirs(folded_dirs) => { - folded_dirs.entries.last().map(|entry| entry.path.clone()) - } - PanelEntry::Search(_) | PanelEntry::Outline(..) => None, - }) - .map(|p| p.display(path_style).to_string()) - { - cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text)); - } - } - - fn reveal_in_finder( - &mut self, - _: &RevealInFileManager, - _: &mut Window, - cx: &mut Context, - ) { - if let Some(abs_path) = self - .selected_entry() - .and_then(|entry| self.abs_path(entry, cx)) - { - cx.reveal_path(&abs_path); - } - } - - fn open_in_terminal( - &mut self, - _: &OpenInTerminal, - window: &mut Window, - cx: &mut Context, - ) { - let selected_entry = self.selected_entry(); - let abs_path = selected_entry.and_then(|entry| self.abs_path(entry, cx)); - let working_directory = if let ( - Some(abs_path), - Some(PanelEntry::Fs(FsEntry::File(..) | FsEntry::ExternalFile(..))), - ) = (&abs_path, selected_entry) - { - abs_path.parent().map(|p| p.to_owned()) - } else { - abs_path - }; - - if let Some(working_directory) = working_directory { - window.dispatch_action( - workspace::OpenTerminal { working_directory }.boxed_clone(), - cx, - ) - } - } - - fn reveal_entry_for_selection( - &mut self, - editor: Entity, - window: &mut Window, - cx: &mut Context, - ) { - if !self.active - || !OutlinePanelSettings::get_global(cx).auto_reveal_entries - || self.focus_handle.contains_focused(window, cx) - { - return; - } - let project = self.project.clone(); - self.reveal_selection_task = cx.spawn_in(window, async move |outline_panel, cx| { - cx.background_executor().timer(UPDATE_DEBOUNCE).await; - let entry_with_selection = - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.location_for_editor_selection(&editor, window, cx) - })?; - let Some(entry_with_selection) = entry_with_selection else { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.selected_entry = SelectedEntry::None; - cx.notify(); - })?; - return Ok(()); - }; - let related_buffer_entry = match &entry_with_selection { - PanelEntry::Fs(FsEntry::File(FsEntryFile { - worktree_id, - buffer_id, - .. - })) => project.update(cx, |project, cx| { - let entry_id = project - .buffer_for_id(*buffer_id, cx) - .and_then(|buffer| buffer.read(cx).entry_id(cx)); - project - .worktree_for_id(*worktree_id, cx) - .zip(entry_id) - .and_then(|(worktree, entry_id)| { - let entry = worktree.read(cx).entry_for_id(entry_id)?.clone(); - Some((worktree, entry)) - }) - })?, - PanelEntry::Outline(outline_entry) => { - let (buffer_id, excerpt_id) = outline_entry.ids(); - outline_panel.update(cx, |outline_panel, cx| { - outline_panel - .collapsed_entries - .remove(&CollapsedEntry::ExternalFile(buffer_id)); - outline_panel - .collapsed_entries - .remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id)); - let project = outline_panel.project.read(cx); - let entry_id = project - .buffer_for_id(buffer_id, cx) - .and_then(|buffer| buffer.read(cx).entry_id(cx)); - - entry_id.and_then(|entry_id| { - project - .worktree_for_entry(entry_id, cx) - .and_then(|worktree| { - let worktree_id = worktree.read(cx).id(); - outline_panel - .collapsed_entries - .remove(&CollapsedEntry::File(worktree_id, buffer_id)); - let entry = worktree.read(cx).entry_for_id(entry_id)?.clone(); - Some((worktree, entry)) - }) - }) - })? - } - PanelEntry::Fs(FsEntry::ExternalFile(..)) => None, - PanelEntry::Search(SearchEntry { match_range, .. }) => match_range - .start - .text_anchor - .buffer_id - .or(match_range.end.text_anchor.buffer_id) - .map(|buffer_id| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel - .collapsed_entries - .remove(&CollapsedEntry::ExternalFile(buffer_id)); - let project = project.read(cx); - let entry_id = project - .buffer_for_id(buffer_id, cx) - .and_then(|buffer| buffer.read(cx).entry_id(cx)); - - entry_id.and_then(|entry_id| { - project - .worktree_for_entry(entry_id, cx) - .and_then(|worktree| { - let worktree_id = worktree.read(cx).id(); - outline_panel - .collapsed_entries - .remove(&CollapsedEntry::File(worktree_id, buffer_id)); - let entry = - worktree.read(cx).entry_for_id(entry_id)?.clone(); - Some((worktree, entry)) - }) - }) - }) - }) - .transpose()? - .flatten(), - _ => return anyhow::Ok(()), - }; - if let Some((worktree, buffer_entry)) = related_buffer_entry { - outline_panel.update(cx, |outline_panel, cx| { - let worktree_id = worktree.read(cx).id(); - let mut dirs_to_expand = Vec::new(); - { - let mut traversal = worktree.read(cx).traverse_from_path( - true, - true, - true, - buffer_entry.path.as_ref(), - ); - let mut current_entry = buffer_entry; - loop { - if current_entry.is_dir() - && outline_panel - .collapsed_entries - .remove(&CollapsedEntry::Dir(worktree_id, current_entry.id)) - { - dirs_to_expand.push(current_entry.id); - } - - if traversal.back_to_parent() - && let Some(parent_entry) = traversal.entry() - { - current_entry = parent_entry.clone(); - continue; - } - break; - } - } - for dir_to_expand in dirs_to_expand { - project - .update(cx, |project, cx| { - project.expand_entry(worktree_id, dir_to_expand, cx) - }) - .unwrap_or_else(|| Task::ready(Ok(()))) - .detach_and_log_err(cx) - } - })? - } - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.select_entry(entry_with_selection, false, window, cx); - outline_panel.update_cached_entries(None, window, cx); - })?; - - anyhow::Ok(()) - }); - } - - fn render_excerpt( - &self, - excerpt: &OutlineEntryExcerpt, - depth: usize, - window: &mut Window, - cx: &mut Context, - ) -> Option> { - let item_id = ElementId::from(excerpt.id.to_proto() as usize); - let is_active = match self.selected_entry() { - Some(PanelEntry::Outline(OutlineEntry::Excerpt(selected_excerpt))) => { - selected_excerpt.buffer_id == excerpt.buffer_id && selected_excerpt.id == excerpt.id - } - _ => false, - }; - let has_outlines = self - .excerpts - .get(&excerpt.buffer_id) - .and_then(|excerpts| match &excerpts.get(&excerpt.id)?.outlines { - ExcerptOutlines::Outlines(outlines) => Some(outlines), - ExcerptOutlines::Invalidated(outlines) => Some(outlines), - ExcerptOutlines::NotFetched => None, - }) - .is_some_and(|outlines| !outlines.is_empty()); - let is_expanded = !self - .collapsed_entries - .contains(&CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)); - let color = entry_label_color(is_active); - let icon = if has_outlines { - FileIcons::get_chevron_icon(is_expanded, cx) - .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element()) - } else { - None - } - .unwrap_or_else(empty_icon); - - let label = self.excerpt_label(excerpt.buffer_id, &excerpt.range, cx)?; - let label_element = Label::new(label) - .single_line() - .color(color) - .into_any_element(); - - Some(self.entry_element( - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt.clone())), - item_id, - depth, - icon, - is_active, - label_element, - window, - cx, - )) - } - - fn excerpt_label( - &self, - buffer_id: BufferId, - range: &ExcerptRange, - cx: &App, - ) -> Option { - let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx)?; - let excerpt_range = range.context.to_point(&buffer_snapshot); - Some(format!( - "Lines {}- {}", - excerpt_range.start.row + 1, - excerpt_range.end.row + 1, - )) - } - - fn render_outline( - &self, - outline: &OutlineEntryOutline, - depth: usize, - string_match: Option<&StringMatch>, - window: &mut Window, - cx: &mut Context, - ) -> Stateful
{ - let item_id = ElementId::from(SharedString::from(format!( - "{:?}|{:?}{:?}|{:?}", - outline.buffer_id, outline.excerpt_id, outline.outline.range, &outline.outline.text, - ))); - - let label_element = outline::render_item( - &outline.outline, - string_match - .map(|string_match| string_match.ranges().collect::>()) - .unwrap_or_default(), - cx, - ) - .into_any_element(); - - let is_active = match self.selected_entry() { - Some(PanelEntry::Outline(OutlineEntry::Outline(selected))) => { - outline == selected && outline.outline == selected.outline - } - _ => false, - }; - - let has_children = self - .outline_children_cache - .get(&outline.buffer_id) - .and_then(|children_map| { - let key = (outline.outline.range.clone(), outline.outline.depth); - children_map.get(&key) - }) - .copied() - .unwrap_or(false); - let is_expanded = !self.collapsed_entries.contains(&CollapsedEntry::Outline( - outline.buffer_id, - outline.excerpt_id, - outline.outline.range.clone(), - )); - - let icon = if has_children { - FileIcons::get_chevron_icon(is_expanded, cx) - .map(|icon_path| { - Icon::from_path(icon_path) - .color(entry_label_color(is_active)) - .into_any_element() - }) - .unwrap_or_else(empty_icon) - } else { - empty_icon() - }; - - self.entry_element( - PanelEntry::Outline(OutlineEntry::Outline(outline.clone())), - item_id, - depth, - icon, - is_active, - label_element, - window, - cx, - ) - } - - fn render_entry( - &self, - rendered_entry: &FsEntry, - depth: usize, - string_match: Option<&StringMatch>, - window: &mut Window, - cx: &mut Context, - ) -> Stateful
{ - let settings = OutlinePanelSettings::get_global(cx); - let is_active = match self.selected_entry() { - Some(PanelEntry::Fs(selected_entry)) => selected_entry == rendered_entry, - _ => false, - }; - let (item_id, label_element, icon) = match rendered_entry { - FsEntry::File(FsEntryFile { - worktree_id, entry, .. - }) => { - let name = self.entry_name(worktree_id, entry, cx); - let color = - entry_git_aware_label_color(entry.git_summary, entry.is_ignored, is_active); - let icon = if settings.file_icons { - FileIcons::get_icon(entry.path.as_std_path(), cx) - .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element()) - } else { - None - }; - ( - ElementId::from(entry.id.to_proto() as usize), - HighlightedLabel::new( - name, - string_match - .map(|string_match| string_match.positions.clone()) - .unwrap_or_default(), - ) - .color(color) - .into_any_element(), - icon.unwrap_or_else(empty_icon), - ) - } - FsEntry::Directory(directory) => { - let name = self.entry_name(&directory.worktree_id, &directory.entry, cx); - - let is_expanded = !self.collapsed_entries.contains(&CollapsedEntry::Dir( - directory.worktree_id, - directory.entry.id, - )); - let color = entry_git_aware_label_color( - directory.entry.git_summary, - directory.entry.is_ignored, - is_active, - ); - let icon = if settings.folder_icons { - FileIcons::get_folder_icon(is_expanded, directory.entry.path.as_std_path(), cx) - } else { - FileIcons::get_chevron_icon(is_expanded, cx) - } - .map(Icon::from_path) - .map(|icon| icon.color(color).into_any_element()); - ( - ElementId::from(directory.entry.id.to_proto() as usize), - HighlightedLabel::new( - name, - string_match - .map(|string_match| string_match.positions.clone()) - .unwrap_or_default(), - ) - .color(color) - .into_any_element(), - icon.unwrap_or_else(empty_icon), - ) - } - FsEntry::ExternalFile(external_file) => { - let color = entry_label_color(is_active); - let (icon, name) = match self.buffer_snapshot_for_id(external_file.buffer_id, cx) { - Some(buffer_snapshot) => match buffer_snapshot.file() { - Some(file) => { - let path = file.path(); - let icon = if settings.file_icons { - FileIcons::get_icon(path.as_std_path(), cx) - } else { - None - } - .map(Icon::from_path) - .map(|icon| icon.color(color).into_any_element()); - (icon, file_name(path.as_std_path())) - } - None => (None, "Untitled".to_string()), - }, - None => (None, "Unknown buffer".to_string()), - }; - ( - ElementId::from(external_file.buffer_id.to_proto() as usize), - HighlightedLabel::new( - name, - string_match - .map(|string_match| string_match.positions.clone()) - .unwrap_or_default(), - ) - .color(color) - .into_any_element(), - icon.unwrap_or_else(empty_icon), - ) - } - }; - - self.entry_element( - PanelEntry::Fs(rendered_entry.clone()), - item_id, - depth, - icon, - is_active, - label_element, - window, - cx, - ) - } - - fn render_folded_dirs( - &self, - folded_dir: &FoldedDirsEntry, - depth: usize, - string_match: Option<&StringMatch>, - window: &mut Window, - cx: &mut Context, - ) -> Stateful
{ - let settings = OutlinePanelSettings::get_global(cx); - let is_active = match self.selected_entry() { - Some(PanelEntry::FoldedDirs(selected_dirs)) => { - selected_dirs.worktree_id == folded_dir.worktree_id - && selected_dirs.entries == folded_dir.entries - } - _ => false, - }; - let (item_id, label_element, icon) = { - let name = self.dir_names_string(&folded_dir.entries, folded_dir.worktree_id, cx); - - let is_expanded = folded_dir.entries.iter().all(|dir| { - !self - .collapsed_entries - .contains(&CollapsedEntry::Dir(folded_dir.worktree_id, dir.id)) - }); - let is_ignored = folded_dir.entries.iter().any(|entry| entry.is_ignored); - let git_status = folded_dir - .entries - .first() - .map(|entry| entry.git_summary) - .unwrap_or_default(); - let color = entry_git_aware_label_color(git_status, is_ignored, is_active); - let icon = if settings.folder_icons { - FileIcons::get_folder_icon(is_expanded, &Path::new(&name), cx) - } else { - FileIcons::get_chevron_icon(is_expanded, cx) - } - .map(Icon::from_path) - .map(|icon| icon.color(color).into_any_element()); - ( - ElementId::from( - folded_dir - .entries - .last() - .map(|entry| entry.id.to_proto()) - .unwrap_or_else(|| folded_dir.worktree_id.to_proto()) - as usize, - ), - HighlightedLabel::new( - name, - string_match - .map(|string_match| string_match.positions.clone()) - .unwrap_or_default(), - ) - .color(color) - .into_any_element(), - icon.unwrap_or_else(empty_icon), - ) - }; - - self.entry_element( - PanelEntry::FoldedDirs(folded_dir.clone()), - item_id, - depth, - icon, - is_active, - label_element, - window, - cx, - ) - } - - fn render_search_match( - &mut self, - multi_buffer_snapshot: Option<&MultiBufferSnapshot>, - match_range: &Range, - render_data: &Arc>, - kind: SearchKind, - depth: usize, - string_match: Option<&StringMatch>, - window: &mut Window, - cx: &mut Context, - ) -> Option> { - 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(); - } - return None; - } - }; - let search_matches = string_match - .iter() - .flat_map(|string_match| string_match.ranges()) - .collect::>(); - let match_ranges = if search_matches.is_empty() { - &search_data.search_match_indices - } else { - &search_matches - }; - let label_element = outline::render_item( - &OutlineItem { - depth, - annotation_range: None, - range: search_data.context_range.clone(), - text: search_data.context_text.clone(), - source_range_for_text: search_data.context_range.clone(), - highlight_ranges: search_data - .highlights_data - .get() - .cloned() - .unwrap_or_default(), - name_ranges: search_data.search_match_indices.clone(), - body_range: Some(search_data.context_range.clone()), - }, - match_ranges.iter().cloned(), - cx, - ); - let truncated_contents_label = || Label::new(TRUNCATED_CONTEXT_MARK); - let entire_label = h_flex() - .justify_center() - .p_0() - .when(search_data.truncated_left, |parent| { - parent.child(truncated_contents_label()) - }) - .child(label_element) - .when(search_data.truncated_right, |parent| { - parent.child(truncated_contents_label()) - }) - .into_any_element(); - - let is_active = match self.selected_entry() { - Some(PanelEntry::Search(SearchEntry { - match_range: selected_match_range, - .. - })) => match_range == selected_match_range, - _ => false, - }; - Some(self.entry_element( - PanelEntry::Search(SearchEntry { - kind, - match_range: match_range.clone(), - render_data: render_data.clone(), - }), - ElementId::from(SharedString::from(format!("search-{match_range:?}"))), - depth, - empty_icon(), - is_active, - entire_label, - window, - cx, - )) - } - - fn entry_element( - &self, - rendered_entry: PanelEntry, - item_id: ElementId, - depth: usize, - icon_element: AnyElement, - is_active: bool, - label_element: gpui::AnyElement, - window: &mut Window, - cx: &mut Context, - ) -> Stateful
{ - let settings = OutlinePanelSettings::get_global(cx); - div() - .text_ui(cx) - .id(item_id.clone()) - .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() { - return; - } - - let change_focus = event.click_count() > 1; - outline_panel.toggle_expanded(&clicked_entry, window, cx); - - outline_panel.scroll_editor_to_entry( - &clicked_entry, - true, - change_focus, - window, - cx, - ); - }) - }) - .cursor_pointer() - .child( - ListItem::new(item_id) - .indent_level(depth) - .indent_step_size(px(settings.indent_size)) - .toggle_state(is_active) - .child( - h_flex() - .child(h_flex().w(px(16.)).justify_center().child(icon_element)) - .child(h_flex().h_6().child(label_element).ml_1()), - ) - .on_secondary_mouse_down(cx.listener( - move |outline_panel, event: &MouseDownEvent, window, cx| { - // Stop propagation to prevent the catch-all context menu for the project - // panel from being deployed. - cx.stop_propagation(); - outline_panel.deploy_context_menu( - event.position, - rendered_entry.clone(), - window, - cx, - ) - }, - )), - ) - .border_1() - .border_r_2() - .rounded_none() - .hover(|style| { - if is_active { - style - } else { - let hover_color = cx.theme().colors().ghost_element_hover; - style.bg(hover_color).border_color(hover_color) - } - }) - .when( - is_active && self.focus_handle.contains_focused(window, cx), - |div| div.border_color(Color::Selected.color(cx)), - ) - } - - fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &App) -> String { - match self.project.read(cx).worktree_for_id(*worktree_id, cx) { - Some(worktree) => { - let worktree = worktree.read(cx); - match worktree.snapshot().root_entry() { - Some(root_entry) => { - if root_entry.id == entry.id { - file_name(worktree.abs_path().as_ref()) - } else { - let path = worktree.absolutize(entry.path.as_ref()); - file_name(&path) - } - } - None => { - let path = worktree.absolutize(entry.path.as_ref()); - file_name(&path) - } - } - } - None => file_name(entry.path.as_std_path()), - } - } - - fn update_fs_entries( - &mut self, - active_editor: Entity, - debounce: Option, - window: &mut Window, - cx: &mut Context, - ) { - if !self.active { - return; - } - - let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs; - let active_multi_buffer = active_editor.read(cx).buffer().clone(); - let new_entries = self.new_entries_for_fs_update.clone(); - let repo_snapshots = self.project.update(cx, |project, cx| { - project.git_store().read(cx).repo_snapshots(cx) - }); - self.fs_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| { - if let Some(debounce) = debounce { - cx.background_executor().timer(debounce).await; - } - - let mut new_collapsed_entries = HashSet::default(); - let mut new_unfolded_dirs = HashMap::default(); - let mut root_entries = HashSet::default(); - let mut new_excerpts = HashMap::>::default(); - let Ok(buffer_excerpts) = outline_panel.update(cx, |outline_panel, cx| { - let git_store = outline_panel.project.read(cx).git_store().clone(); - 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( - HashMap::default(), - |mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| { - let buffer_id = buffer_snapshot.remote_id(); - let file = File::from_dyn(buffer_snapshot.file()); - let entry_id = file.and_then(|file| file.project_entry_id()); - let worktree = file.map(|file| file.worktree.read(cx).snapshot()); - let is_new = new_entries.contains(&excerpt_id) - || !outline_panel.excerpts.contains_key(&buffer_id); - let is_folded = active_editor.read(cx).is_buffer_folded(buffer_id, cx); - let status = git_store - .read(cx) - .repository_and_path_for_buffer_id(buffer_id, cx) - .and_then(|(repo, path)| { - Some(repo.read(cx).status_for_path(&path)?.status) - }); - buffer_excerpts - .entry(buffer_id) - .or_insert_with(|| { - (is_new, is_folded, Vec::new(), entry_id, worktree, status) - }) - .2 - .push(excerpt_id); - - let outlines = match outline_panel - .excerpts - .get(&buffer_id) - .and_then(|excerpts| excerpts.get(&excerpt_id)) - { - Some(old_excerpt) => match &old_excerpt.outlines { - ExcerptOutlines::Outlines(outlines) => { - ExcerptOutlines::Outlines(outlines.clone()) - } - ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched, - ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched, - }, - None => ExcerptOutlines::NotFetched, - }; - new_excerpts.entry(buffer_id).or_default().insert( - excerpt_id, - Excerpt { - range: excerpt_range, - outlines, - }, - ); - buffer_excerpts - }, - ) - }) else { - return; - }; - - let Some(( - new_collapsed_entries, - new_unfolded_dirs, - new_fs_entries, - new_depth_map, - new_children_count, - )) = cx - .background_spawn(async move { - let mut processed_external_buffers = HashSet::default(); - let mut new_worktree_entries = - BTreeMap::>::default(); - let mut worktree_excerpts = HashMap::< - WorktreeId, - HashMap)>, - >::default(); - let mut external_excerpts = HashMap::default(); - - for (buffer_id, (is_new, is_folded, excerpts, entry_id, worktree, status)) in - buffer_excerpts - { - if is_folded { - match &worktree { - Some(worktree) => { - new_collapsed_entries - .insert(CollapsedEntry::File(worktree.id(), buffer_id)); - } - None => { - new_collapsed_entries - .insert(CollapsedEntry::ExternalFile(buffer_id)); - } - } - } else if is_new { - match &worktree { - Some(worktree) => { - new_collapsed_entries - .remove(&CollapsedEntry::File(worktree.id(), buffer_id)); - } - None => { - new_collapsed_entries - .remove(&CollapsedEntry::ExternalFile(buffer_id)); - } - } - } - - if let Some(worktree) = worktree { - let worktree_id = worktree.id(); - let unfolded_dirs = new_unfolded_dirs.entry(worktree_id).or_default(); - - match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() { - Some(entry) => { - let entry = GitEntry { - git_summary: status - .map(|status| status.summary()) - .unwrap_or_default(), - entry, - }; - let mut traversal = GitTraversal::new( - &repo_snapshots, - worktree.traverse_from_path( - true, - true, - true, - entry.path.as_ref(), - ), - ); - - let mut entries_to_add = HashMap::default(); - worktree_excerpts - .entry(worktree_id) - .or_default() - .insert(entry.id, (buffer_id, excerpts)); - let mut current_entry = entry; - loop { - if current_entry.is_dir() { - let is_root = - worktree.root_entry().map(|entry| entry.id) - == Some(current_entry.id); - if is_root { - root_entries.insert(current_entry.id); - if auto_fold_dirs { - unfolded_dirs.insert(current_entry.id); - } - } - if is_new { - new_collapsed_entries.remove(&CollapsedEntry::Dir( - worktree_id, - current_entry.id, - )); - } - } - - 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; - } - break; - } - new_worktree_entries - .entry(worktree_id) - .or_insert_with(HashMap::default) - .extend(entries_to_add); - } - None => { - if processed_external_buffers.insert(buffer_id) { - external_excerpts - .entry(buffer_id) - .or_insert_with(Vec::new) - .extend(excerpts); - } - } - } - } else if processed_external_buffers.insert(buffer_id) { - external_excerpts - .entry(buffer_id) - .or_insert_with(Vec::new) - .extend(excerpts); - } - } - - let mut new_children_count = - HashMap::, FsChildren>>::default(); - - let worktree_entries = new_worktree_entries - .into_iter() - .map(|(worktree_id, entries)| { - let mut entries = entries.into_values().collect::>(); - entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref())); - (worktree_id, entries) - }) - .flat_map(|(worktree_id, entries)| { - { - 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 entry.is_dir() { - Some(FsEntry::Directory(FsEntryDirectory { - worktree_id, - entry, - })) - } else { - let (buffer_id, excerpts) = worktree_excerpts - .get_mut(&worktree_id) - .and_then(|worktree_excerpts| { - worktree_excerpts.remove(&entry.id) - })?; - Some(FsEntry::File(FsEntryFile { - worktree_id, - buffer_id, - entry, - excerpts, - })) - } - }) - .collect::>() - } - }) - .collect::>(); - - let mut visited_dirs = Vec::new(); - let mut new_depth_map = HashMap::default(); - let new_visible_entries = external_excerpts - .into_iter() - .sorted_by_key(|(id, _)| *id) - .map(|(buffer_id, excerpts)| { - FsEntry::ExternalFile(FsEntryExternalFile { - buffer_id, - excerpts, - }) - }) - .chain(worktree_entries) - .filter(|visible_item| { - match visible_item { - FsEntry::Directory(directory) => { - let parent_id = back_to_common_visited_parent( - &mut visited_dirs, - &directory.worktree_id, - &directory.entry, - ); - - let mut depth = 0; - if !root_entries.contains(&directory.entry.id) { - if auto_fold_dirs { - let children = new_children_count - .get(&directory.worktree_id) - .and_then(|children_count| { - children_count.get(&directory.entry.path) - }) - .copied() - .unwrap_or_default(); - - if !children.may_be_fold_part() - || (children.dirs == 0 - && visited_dirs - .last() - .map(|(parent_dir_id, _)| { - new_unfolded_dirs - .get(&directory.worktree_id) - .is_none_or(|unfolded_dirs| { - unfolded_dirs - .contains(parent_dir_id) - }) - }) - .unwrap_or(true)) - { - new_unfolded_dirs - .entry(directory.worktree_id) - .or_default() - .insert(directory.entry.id); - } - } - - depth = parent_id - .and_then(|(worktree_id, id)| { - new_depth_map.get(&(worktree_id, id)).copied() - }) - .unwrap_or(0) - + 1; - }; - visited_dirs - .push((directory.entry.id, directory.entry.path.clone())); - new_depth_map - .insert((directory.worktree_id, directory.entry.id), depth); - } - FsEntry::File(FsEntryFile { - worktree_id, - entry: file_entry, - .. - }) => { - let parent_id = back_to_common_visited_parent( - &mut visited_dirs, - worktree_id, - file_entry, - ); - let depth = if root_entries.contains(&file_entry.id) { - 0 - } else { - parent_id - .and_then(|(worktree_id, id)| { - new_depth_map.get(&(worktree_id, id)).copied() - }) - .unwrap_or(0) - + 1 - }; - new_depth_map.insert((*worktree_id, file_entry.id), depth); - } - FsEntry::ExternalFile(..) => { - visited_dirs.clear(); - } - } - - true - }) - .collect::>(); - - anyhow::Ok(( - new_collapsed_entries, - new_unfolded_dirs, - new_visible_entries, - new_depth_map, - new_children_count, - )) - }) - .await - .log_err() - else { - return; - }; - - outline_panel - .update_in(cx, |outline_panel, window, cx| { - outline_panel.new_entries_for_fs_update.clear(); - outline_panel.excerpts = new_excerpts; - outline_panel.collapsed_entries = new_collapsed_entries; - outline_panel.unfolded_dirs = new_unfolded_dirs; - outline_panel.fs_entries = new_fs_entries; - outline_panel.fs_entries_depth = new_depth_map; - outline_panel.fs_children_count = new_children_count; - outline_panel.update_non_fs_items(window, cx); - - // Only update cached entries if we don't have outlines to fetch - // If we do have outlines to fetch, let fetch_outdated_outlines handle the update - if outline_panel.excerpt_fetch_ranges(cx).is_empty() { - outline_panel.update_cached_entries(debounce, window, cx); - } - - cx.notify(); - }) - .ok(); - }); - } - - fn replace_active_editor( - &mut self, - new_active_item: Box, - new_active_editor: Entity, - window: &mut Window, - cx: &mut Context, - ) { - self.clear_previous(window, cx); - - let default_expansion_depth = - OutlinePanelSettings::get_global(cx).expand_outlines_with_depth; - // We'll apply the expansion depth after outlines are loaded - self.pending_default_expansion_depth = Some(default_expansion_depth); - - let buffer_search_subscription = cx.subscribe_in( - &new_active_editor, - window, - |outline_panel: &mut Self, - _, - e: &SearchEvent, - window: &mut Window, - cx: &mut Context| { - if matches!(e, SearchEvent::MatchesInvalidated) { - let update_cached_items = outline_panel.update_search_matches(window, cx); - if update_cached_items { - outline_panel.selected_entry.invalidate(); - outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); - } - }; - outline_panel.autoscroll(cx); - }, - ); - self.active_item = Some(ActiveItem { - _buffer_search_subscription: buffer_search_subscription, - _editor_subscription: subscribe_for_editor_events(&new_active_editor, window, cx), - item_handle: new_active_item.downgrade_item(), - active_editor: new_active_editor.downgrade(), - }); - self.new_entries_for_fs_update - .extend(new_active_editor.read(cx).buffer().read(cx).excerpt_ids()); - self.selected_entry.invalidate(); - self.update_fs_entries(new_active_editor, None, window, cx); - } - - fn clear_previous(&mut self, window: &mut Window, cx: &mut App) { - self.fs_entries_update_task = Task::ready(()); - self.outline_fetch_tasks.clear(); - self.cached_entries_update_task = Task::ready(()); - self.reveal_selection_task = Task::ready(Ok(())); - self.filter_editor - .update(cx, |editor, cx| editor.clear(window, cx)); - self.collapsed_entries.clear(); - self.unfolded_dirs.clear(); - self.active_item = None; - self.fs_entries.clear(); - self.fs_entries_depth.clear(); - self.fs_children_count.clear(); - self.excerpts.clear(); - self.cached_entries = Vec::new(); - self.selected_entry = SelectedEntry::None; - self.pinned = false; - self.mode = ItemsDisplayMode::Outline; - self.pending_default_expansion_depth = None; - } - - fn location_for_editor_selection( - &self, - editor: &Entity, - window: &mut Window, - cx: &mut Context, - ) -> Option { - let selection = editor.update(cx, |editor, cx| { - editor - .selections - .newest::(&editor.display_snapshot(cx)) - .head() - }); - let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx)); - let multi_buffer = editor.read(cx).buffer(); - let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx); - let (excerpt_id, buffer, _) = editor - .read(cx) - .buffer() - .read(cx) - .excerpt_containing(selection, cx)?; - let buffer_id = buffer.read(cx).remote_id(); - - if editor.read(cx).is_buffer_folded(buffer_id, cx) { - return self - .fs_entries - .iter() - .find(|fs_entry| match fs_entry { - FsEntry::Directory(..) => false, - FsEntry::File(FsEntryFile { - buffer_id: other_buffer_id, - .. - }) - | FsEntry::ExternalFile(FsEntryExternalFile { - buffer_id: other_buffer_id, - .. - }) => buffer_id == *other_buffer_id, - }) - .cloned() - .map(PanelEntry::Fs); - } - - let selection_display_point = selection.to_display_point(&editor_snapshot); - - match &self.mode { - ItemsDisplayMode::Search(search_state) => search_state - .matches - .iter() - .rev() - .min_by_key(|&(match_range, _)| { - let match_display_range = - match_range.clone().to_display_points(&editor_snapshot); - let start_distance = if selection_display_point < match_display_range.start { - match_display_range.start - selection_display_point - } else { - selection_display_point - match_display_range.start - }; - let end_distance = if selection_display_point < match_display_range.end { - match_display_range.end - selection_display_point - } else { - selection_display_point - match_display_range.end - }; - start_distance + end_distance - }) - .and_then(|(closest_range, _)| { - self.cached_entries.iter().find_map(|cached_entry| { - if let PanelEntry::Search(SearchEntry { match_range, .. }) = - &cached_entry.entry - { - if match_range == closest_range { - Some(cached_entry.entry.clone()) - } else { - None - } - } else { - None - } - }) - }), - ItemsDisplayMode::Outline => self.outline_location( - buffer_id, - excerpt_id, - multi_buffer_snapshot, - editor_snapshot, - selection_display_point, - ), - } - } - - fn outline_location( - &self, - buffer_id: BufferId, - excerpt_id: ExcerptId, - multi_buffer_snapshot: editor::MultiBufferSnapshot, - editor_snapshot: editor::EditorSnapshot, - selection_display_point: DisplayPoint, - ) -> Option { - let excerpt_outlines = self - .excerpts - .get(&buffer_id) - .and_then(|excerpts| excerpts.get(&excerpt_id)) - .into_iter() - .flat_map(|excerpt| excerpt.iter_outlines()) - .flat_map(|outline| { - let range = multi_buffer_snapshot - .anchor_range_in_excerpt(excerpt_id, outline.range.clone())?; - Some(( - range.start.to_display_point(&editor_snapshot) - ..range.end.to_display_point(&editor_snapshot), - outline, - )) - }) - .collect::>(); - - let mut matching_outline_indices = Vec::new(); - let mut children = HashMap::default(); - let mut parents_stack = Vec::<(&Range, &&Outline, usize)>::new(); - - for (i, (outline_range, outline)) in excerpt_outlines.iter().enumerate() { - if outline_range - .to_inclusive() - .contains(&selection_display_point) - { - matching_outline_indices.push(i); - } else if (outline_range.start.row()..outline_range.end.row()) - .to_inclusive() - .contains(&selection_display_point.row()) - { - matching_outline_indices.push(i); - } - - while let Some((parent_range, parent_outline, _)) = parents_stack.last() { - if parent_outline.depth >= outline.depth - || !parent_range.contains(&outline_range.start) - { - parents_stack.pop(); - } else { - break; - } - } - if let Some((_, _, parent_index)) = parents_stack.last_mut() { - children - .entry(*parent_index) - .or_insert_with(Vec::new) - .push(i); - } - parents_stack.push((outline_range, outline, i)); - } - - let outline_item = matching_outline_indices - .into_iter() - .flat_map(|i| Some((i, excerpt_outlines.get(i)?))) - .filter(|(i, _)| { - children - .get(i) - .map(|children| { - children.iter().all(|child_index| { - excerpt_outlines - .get(*child_index) - .map(|(child_range, _)| child_range.start > selection_display_point) - .unwrap_or(false) - }) - }) - .unwrap_or(true) - }) - .min_by_key(|(_, (outline_range, outline))| { - let distance_from_start = if outline_range.start > selection_display_point { - outline_range.start - selection_display_point - } else { - selection_display_point - outline_range.start - }; - let distance_from_end = if outline_range.end > selection_display_point { - outline_range.end - selection_display_point - } else { - selection_display_point - outline_range.end - }; - - ( - cmp::Reverse(outline.depth), - distance_from_start + distance_from_end, - ) - }) - .map(|(_, (_, outline))| *outline) - .cloned(); - - let closest_container = match outline_item { - Some(outline) => PanelEntry::Outline(OutlineEntry::Outline(OutlineEntryOutline { - buffer_id, - excerpt_id, - outline, - })), - None => { - self.cached_entries.iter().rev().find_map(|cached_entry| { - match &cached_entry.entry { - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { - if excerpt.buffer_id == buffer_id && excerpt.id == excerpt_id { - Some(cached_entry.entry.clone()) - } else { - None - } - } - PanelEntry::Fs( - FsEntry::ExternalFile(FsEntryExternalFile { - buffer_id: file_buffer_id, - excerpts: file_excerpts, - }) - | FsEntry::File(FsEntryFile { - buffer_id: file_buffer_id, - excerpts: file_excerpts, - .. - }), - ) => { - if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) { - Some(cached_entry.entry.clone()) - } else { - None - } - } - _ => None, - } - })? - } - }; - Some(closest_container) - } - - fn fetch_outdated_outlines(&mut self, window: &mut Window, cx: &mut Context) { - let excerpt_fetch_ranges = self.excerpt_fetch_ranges(cx); - if excerpt_fetch_ranges.is_empty() { - return; - } - - let syntax_theme = cx.theme().syntax().clone(); - let first_update = Arc::new(AtomicBool::new(true)); - for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges { - for (excerpt_id, excerpt_range) in excerpt_ranges { - let syntax_theme = syntax_theme.clone(); - let buffer_snapshot = buffer_snapshot.clone(); - let first_update = first_update.clone(); - self.outline_fetch_tasks.insert( - (buffer_id, excerpt_id), - cx.spawn_in(window, async move |outline_panel, cx| { - let buffer_language = buffer_snapshot.language().cloned(); - let fetched_outlines = cx - .background_spawn(async move { - let mut outlines = buffer_snapshot.outline_items_containing( - excerpt_range.context, - false, - Some(&syntax_theme), - ); - outlines.retain(|outline| { - buffer_language.is_none() - || buffer_language.as_ref() - == buffer_snapshot.language_at(outline.range.start) - }); - - let outlines_with_children = outlines - .windows(2) - .filter_map(|window| { - let current = &window[0]; - let next = &window[1]; - if next.depth > current.depth { - Some((current.range.clone(), current.depth)) - } else { - None - } - }) - .collect::>(); - - (outlines, outlines_with_children) - }) - .await; - - let (fetched_outlines, outlines_with_children) = fetched_outlines; - - outline_panel - .update_in(cx, |outline_panel, window, cx| { - let pending_default_depth = - outline_panel.pending_default_expansion_depth.take(); - - let debounce = - if first_update.fetch_and(false, atomic::Ordering::AcqRel) { - None - } else { - Some(UPDATE_DEBOUNCE) - }; - - if let Some(excerpt) = outline_panel - .excerpts - .entry(buffer_id) - .or_default() - .get_mut(&excerpt_id) - { - excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines); - - if let Some(default_depth) = pending_default_depth - && 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(), - ), - ); - }); - } - - // Even if no outlines to check, we still need to update cached entries - // to show the outline entries that were just fetched - outline_panel.update_cached_entries(debounce, window, cx); - } - }) - .ok(); - }), - ); - } - } - } - - 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()) - } - - fn invalidate_outlines(&mut self, ids: &[ExcerptId]) { - self.outline_fetch_tasks.clear(); - let mut ids = ids.iter().collect::>(); - for excerpts in self.excerpts.values_mut() { - ids.retain(|id| { - if let Some(excerpt) = excerpts.get_mut(id) { - excerpt.invalidate_outlines(); - false - } else { - true - } - }); - if ids.is_empty() { - break; - } - } - } - - fn excerpt_fetch_ranges( - &self, - cx: &App, - ) -> HashMap< - BufferId, - ( - BufferSnapshot, - HashMap>, - ), - > { - self.fs_entries - .iter() - .fold(HashMap::default(), |mut excerpts_to_fetch, fs_entry| { - match fs_entry { - FsEntry::File(FsEntryFile { - buffer_id, - excerpts: file_excerpts, - .. - }) - | FsEntry::ExternalFile(FsEntryExternalFile { - buffer_id, - excerpts: file_excerpts, - }) => { - let excerpts = self.excerpts.get(buffer_id); - for &file_excerpt in file_excerpts { - if let Some(excerpt) = excerpts - .and_then(|excerpts| excerpts.get(&file_excerpt)) - .filter(|excerpt| excerpt.should_fetch_outlines()) - { - match excerpts_to_fetch.entry(*buffer_id) { - hash_map::Entry::Occupied(mut o) => { - o.get_mut().1.insert(file_excerpt, excerpt.range.clone()); - } - hash_map::Entry::Vacant(v) => { - if let Some(buffer_snapshot) = - self.buffer_snapshot_for_id(*buffer_id, cx) - { - v.insert((buffer_snapshot, HashMap::default())) - .1 - .insert(file_excerpt, excerpt.range.clone()); - } - } - } - } - } - } - FsEntry::Directory(..) => {} - } - excerpts_to_fetch - }) - } - - fn buffer_snapshot_for_id(&self, buffer_id: BufferId, cx: &App) -> Option { - let editor = self.active_editor()?; - Some( - editor - .read(cx) - .buffer() - .read(cx) - .buffer(buffer_id)? - .read(cx) - .snapshot(), - ) - } - - fn abs_path(&self, entry: &PanelEntry, cx: &App) -> Option { - match entry { - PanelEntry::Fs( - FsEntry::File(FsEntryFile { buffer_id, .. }) - | FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }), - ) => self - .buffer_snapshot_for_id(*buffer_id, cx) - .and_then(|buffer_snapshot| { - let file = File::from_dyn(buffer_snapshot.file())?; - Some(file.worktree.read(cx).absolutize(&file.path)) - }), - PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { - worktree_id, entry, .. - })) => Some( - self.project - .read(cx) - .worktree_for_id(*worktree_id, cx)? - .read(cx) - .absolutize(&entry.path), - ), - PanelEntry::FoldedDirs(FoldedDirsEntry { - worktree_id, - entries: dirs, - .. - }) => dirs.last().and_then(|entry| { - self.project - .read(cx) - .worktree_for_id(*worktree_id, cx) - .map(|worktree| worktree.read(cx).absolutize(&entry.path)) - }), - PanelEntry::Search(_) | PanelEntry::Outline(..) => None, - } - } - - fn relative_path(&self, entry: &FsEntry, cx: &App) -> Option> { - match entry { - FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => { - let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?; - Some(buffer_snapshot.file()?.path().clone()) - } - FsEntry::Directory(FsEntryDirectory { entry, .. }) => Some(entry.path.clone()), - FsEntry::File(FsEntryFile { entry, .. }) => Some(entry.path.clone()), - } - } - - fn update_cached_entries( - &mut self, - debounce: Option, - window: &mut Window, - cx: &mut Context, - ) { - if !self.active { - return; - } - - let is_singleton = self.is_singleton_active(cx); - let query = self.query(cx); - self.cached_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| { - if let Some(debounce) = debounce { - cx.background_executor().timer(debounce).await; - } - let Some(new_cached_entries) = outline_panel - .update_in(cx, |outline_panel, window, cx| { - outline_panel.generate_cached_entries(is_singleton, query, window, cx) - }) - .ok() - else { - return; - }; - let (new_cached_entries, max_width_item_index) = new_cached_entries.await; - outline_panel - .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) = - outline_panel.active_editor().and_then(|active_editor| { - outline_panel.location_for_editor_selection( - &active_editor, - window, - cx, - ) - }) - { - outline_panel.select_entry(new_selected_entry, false, window, cx); - } - - outline_panel.autoscroll(cx); - cx.notify(); - }) - .ok(); - }); - } - - fn generate_cached_entries( - &self, - is_singleton: bool, - query: Option, - window: &mut Window, - cx: &mut Context, - ) -> Task<(Vec, Option)> { - let project = self.project.clone(); - let Some(active_editor) = self.active_editor() else { - return Task::ready((Vec::new(), None)); - }; - cx.spawn_in(window, async move |outline_panel, cx| { - let mut generation_state = GenerationState::default(); - - let Ok(()) = outline_panel.update(cx, |outline_panel, cx| { - let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs; - let mut folded_dirs_entry = None::<(usize, FoldedDirsEntry)>; - let track_matches = query.is_some(); - - #[derive(Debug)] - struct ParentStats { - path: Arc, - folded: bool, - expanded: bool, - depth: usize, - } - let mut parent_dirs = Vec::::new(); - for entry in outline_panel.fs_entries.clone() { - let is_expanded = outline_panel.is_expanded(&entry); - let (depth, should_add) = match &entry { - FsEntry::Directory(directory_entry) => { - let mut should_add = true; - let is_root = project - .read(cx) - .worktree_for_id(directory_entry.worktree_id, cx) - .is_some_and(|worktree| { - worktree.read(cx).root_entry() == Some(&directory_entry.entry) - }); - let folded = auto_fold_dirs - && !is_root - && outline_panel - .unfolded_dirs - .get(&directory_entry.worktree_id) - .is_none_or(|unfolded_dirs| { - !unfolded_dirs.contains(&directory_entry.entry.id) - }); - let fs_depth = outline_panel - .fs_entries_depth - .get(&(directory_entry.worktree_id, directory_entry.entry.id)) - .copied() - .unwrap_or(0); - while let Some(parent) = parent_dirs.last() { - if !is_root && directory_entry.entry.path.starts_with(&parent.path) - { - break; - } - parent_dirs.pop(); - } - let auto_fold = match parent_dirs.last() { - Some(parent) => { - parent.folded - && Some(parent.path.as_ref()) - == directory_entry.entry.path.parent() - && outline_panel - .fs_children_count - .get(&directory_entry.worktree_id) - .and_then(|entries| { - entries.get(&directory_entry.entry.path) - }) - .copied() - .unwrap_or_default() - .may_be_fold_part() - } - None => false, - }; - let folded = folded || auto_fold; - let (depth, parent_expanded, parent_folded) = match parent_dirs.last() { - Some(parent) => { - let parent_folded = parent.folded; - let parent_expanded = parent.expanded; - let new_depth = if parent_folded { - parent.depth - } else { - parent.depth + 1 - }; - parent_dirs.push(ParentStats { - path: directory_entry.entry.path.clone(), - folded, - expanded: parent_expanded && is_expanded, - depth: new_depth, - }); - (new_depth, parent_expanded, parent_folded) - } - None => { - parent_dirs.push(ParentStats { - path: directory_entry.entry.path.clone(), - folded, - expanded: is_expanded, - depth: fs_depth, - }); - (fs_depth, true, false) - } - }; - - if let Some((folded_depth, mut folded_dirs)) = folded_dirs_entry.take() - { - if folded - && directory_entry.worktree_id == folded_dirs.worktree_id - && directory_entry.entry.path.parent() - == folded_dirs - .entries - .last() - .map(|entry| entry.path.as_ref()) - { - folded_dirs.entries.push(directory_entry.entry.clone()); - folded_dirs_entry = Some((folded_depth, folded_dirs)) - } else { - if !is_singleton { - let start_of_collapsed_dir_sequence = !parent_expanded - && parent_dirs - .iter() - .rev() - .nth(folded_dirs.entries.len() + 1) - .is_none_or(|parent| parent.expanded); - if start_of_collapsed_dir_sequence - || parent_expanded - || query.is_some() - { - if parent_folded { - folded_dirs - .entries - .push(directory_entry.entry.clone()); - should_add = false; - } - let new_folded_dirs = - PanelEntry::FoldedDirs(folded_dirs.clone()); - outline_panel.push_entry( - &mut generation_state, - track_matches, - new_folded_dirs, - folded_depth, - cx, - ); - } - } - - folded_dirs_entry = if parent_folded { - None - } else { - Some(( - depth, - FoldedDirsEntry { - worktree_id: directory_entry.worktree_id, - entries: vec![directory_entry.entry.clone()], - }, - )) - }; - } - } else if folded { - folded_dirs_entry = Some(( - depth, - FoldedDirsEntry { - worktree_id: directory_entry.worktree_id, - entries: vec![directory_entry.entry.clone()], - }, - )); - } - - let should_add = - should_add && parent_expanded && folded_dirs_entry.is_none(); - (depth, should_add) - } - FsEntry::ExternalFile(..) => { - if let Some((folded_depth, folded_dir)) = folded_dirs_entry.take() { - let parent_expanded = parent_dirs - .iter() - .rev() - .find(|parent| { - folded_dir - .entries - .iter() - .all(|entry| entry.path != parent.path) - }) - .is_none_or(|parent| parent.expanded); - if !is_singleton && (parent_expanded || query.is_some()) { - outline_panel.push_entry( - &mut generation_state, - track_matches, - PanelEntry::FoldedDirs(folded_dir), - folded_depth, - cx, - ); - } - } - parent_dirs.clear(); - (0, true) - } - FsEntry::File(file) => { - if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() { - let parent_expanded = parent_dirs - .iter() - .rev() - .find(|parent| { - folded_dirs - .entries - .iter() - .all(|entry| entry.path != parent.path) - }) - .is_none_or(|parent| parent.expanded); - if !is_singleton && (parent_expanded || query.is_some()) { - outline_panel.push_entry( - &mut generation_state, - track_matches, - PanelEntry::FoldedDirs(folded_dirs), - folded_depth, - cx, - ); - } - } - - let fs_depth = outline_panel - .fs_entries_depth - .get(&(file.worktree_id, file.entry.id)) - .copied() - .unwrap_or(0); - while let Some(parent) = parent_dirs.last() { - if file.entry.path.starts_with(&parent.path) { - break; - } - parent_dirs.pop(); - } - match parent_dirs.last() { - Some(parent) => { - let new_depth = parent.depth + 1; - (new_depth, parent.expanded) - } - None => (fs_depth, true), - } - } - }; - - if !is_singleton - && (should_add || (query.is_some() && folded_dirs_entry.is_none())) - { - outline_panel.push_entry( - &mut generation_state, - track_matches, - PanelEntry::Fs(entry.clone()), - depth, - cx, - ); - } - - match outline_panel.mode { - ItemsDisplayMode::Search(_) => { - if is_singleton || query.is_some() || (should_add && is_expanded) { - outline_panel.add_search_entries( - &mut generation_state, - &active_editor, - entry.clone(), - depth, - query.clone(), - is_singleton, - cx, - ); - } - } - ItemsDisplayMode::Outline => { - let excerpts_to_consider = - if is_singleton || query.is_some() || (should_add && is_expanded) { - match &entry { - FsEntry::File(FsEntryFile { - buffer_id, - excerpts, - .. - }) - | FsEntry::ExternalFile(FsEntryExternalFile { - buffer_id, - excerpts, - .. - }) => Some((*buffer_id, excerpts)), - _ => None, - } - } 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 is_singleton - && matches!(entry, FsEntry::File(..) | FsEntry::ExternalFile(..)) - && !generation_state.entries.iter().any(|item| { - matches!(item.entry, PanelEntry::Outline(..) | PanelEntry::Search(_)) - }) - { - outline_panel.push_entry( - &mut generation_state, - track_matches, - PanelEntry::Fs(entry.clone()), - 0, - cx, - ); - } - } - - if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() { - let parent_expanded = parent_dirs - .iter() - .rev() - .find(|parent| { - folded_dirs - .entries - .iter() - .all(|entry| entry.path != parent.path) - }) - .is_none_or(|parent| parent.expanded); - if parent_expanded || query.is_some() { - outline_panel.push_entry( - &mut generation_state, - track_matches, - PanelEntry::FoldedDirs(folded_dirs), - folded_depth, - cx, - ); - } - } - }) else { - return (Vec::new(), None); - }; - - let Some(query) = query else { - return ( - generation_state.entries, - generation_state - .max_width_estimate_and_index - .map(|(_, index)| index), - ); - }; - - let mut matched_ids = match_strings( - &generation_state.match_candidates, - &query, - true, - true, - usize::MAX, - &AtomicBool::default(), - cx.background_executor().clone(), - ) - .await - .into_iter() - .map(|string_match| (string_match.candidate_id, string_match)) - .collect::>(); - - let mut id = 0; - generation_state.entries.retain_mut(|cached_entry| { - let retain = match matched_ids.remove(&id) { - Some(string_match) => { - cached_entry.string_match = Some(string_match); - true - } - None => false, - }; - id += 1; - retain - }); - - ( - generation_state.entries, - generation_state - .max_width_estimate_and_index - .map(|(_, index)| index), - ) - }) - } - - fn push_entry( - &self, - state: &mut GenerationState, - track_matches: bool, - entry: PanelEntry, - depth: usize, - cx: &mut App, - ) { - let entry = if let PanelEntry::FoldedDirs(folded_dirs_entry) = &entry { - match folded_dirs_entry.entries.len() { - 0 => { - debug_panic!("Empty folded dirs receiver"); - return; - } - 1 => PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { - worktree_id: folded_dirs_entry.worktree_id, - entry: folded_dirs_entry.entries[0].clone(), - })), - _ => entry, - } - } else { - entry - }; - - if track_matches { - let id = state.entries.len(); - match &entry { - PanelEntry::Fs(fs_entry) => { - if let Some(file_name) = self - .relative_path(fs_entry, cx) - .and_then(|path| Some(path.file_name()?.to_string())) - { - state - .match_candidates - .push(StringMatchCandidate::new(id, &file_name)); - } - } - PanelEntry::FoldedDirs(folded_dir_entry) => { - let dir_names = self.dir_names_string( - &folded_dir_entry.entries, - folded_dir_entry.worktree_id, - cx, - ); - { - state - .match_candidates - .push(StringMatchCandidate::new(id, &dir_names)); - } - } - PanelEntry::Outline(OutlineEntry::Outline(outline_entry)) => state - .match_candidates - .push(StringMatchCandidate::new(id, &outline_entry.outline.text)), - PanelEntry::Outline(OutlineEntry::Excerpt(_)) => {} - PanelEntry::Search(new_search_entry) => { - if let Some(search_data) = new_search_entry.render_data.get() { - state - .match_candidates - .push(StringMatchCandidate::new(id, &search_data.context_text)); - } - } - } - } - - let width_estimate = self.width_estimate(depth, &entry, cx); - if Some(width_estimate) - > state - .max_width_estimate_and_index - .map(|(estimate, _)| estimate) - { - state.max_width_estimate_and_index = Some((width_estimate, state.entries.len())); - } - state.entries.push(CachedEntry { - depth, - entry, - string_match: None, - }); - } - - fn dir_names_string(&self, entries: &[GitEntry], worktree_id: WorktreeId, cx: &App) -> String { - let dir_names_segment = entries - .iter() - .map(|entry| self.entry_name(&worktree_id, entry, cx)) - .collect::(); - dir_names_segment.to_string_lossy().into_owned() - } - - fn query(&self, cx: &App) -> Option { - let query = self.filter_editor.read(cx).text(cx); - if query.trim().is_empty() { - None - } else { - Some(query) - } - } - - fn is_expanded(&self, entry: &FsEntry) -> bool { - let entry_to_check = match entry { - FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => { - CollapsedEntry::ExternalFile(*buffer_id) - } - FsEntry::File(FsEntryFile { - worktree_id, - buffer_id, - .. - }) => CollapsedEntry::File(*worktree_id, *buffer_id), - FsEntry::Directory(FsEntryDirectory { - worktree_id, entry, .. - }) => CollapsedEntry::Dir(*worktree_id, entry.id), - }; - !self.collapsed_entries.contains(&entry_to_check) - } - - fn update_non_fs_items(&mut self, window: &mut Window, cx: &mut Context) -> bool { - if !self.active { - return false; - } - - let mut update_cached_items = false; - update_cached_items |= self.update_search_matches(window, cx); - self.fetch_outdated_outlines(window, cx); - if update_cached_items { - self.selected_entry.invalidate(); - } - update_cached_items - } - - fn update_search_matches( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> bool { - if !self.active { - return false; - } - - let project_search = self - .active_item() - .and_then(|item| item.downcast::()); - let project_search_matches = project_search - .as_ref() - .map(|project_search| project_search.read(cx).get_matches(cx)) - .unwrap_or_default(); - - let buffer_search = self - .active_item() - .as_deref() - .and_then(|active_item| { - self.workspace - .upgrade() - .and_then(|workspace| workspace.read(cx).pane_for(active_item)) - }) - .and_then(|pane| { - pane.read(cx) - .toolbar() - .read(cx) - .item_of_type::() - }); - let buffer_search_matches = self - .active_editor() - .map(|active_editor| { - active_editor.update(cx, |editor, cx| editor.get_matches(window, cx)) - }) - .unwrap_or_default(); - - let mut update_cached_entries = false; - if buffer_search_matches.is_empty() && project_search_matches.is_empty() { - if matches!(self.mode, ItemsDisplayMode::Search(_)) { - self.mode = ItemsDisplayMode::Outline; - update_cached_entries = true; - } - } else { - let (kind, new_search_matches, new_search_query) = if buffer_search_matches.is_empty() { - ( - SearchKind::Project, - project_search_matches, - project_search - .map(|project_search| project_search.read(cx).search_query_text(cx)) - .unwrap_or_default(), - ) - } else { - ( - SearchKind::Buffer, - buffer_search_matches, - buffer_search - .map(|buffer_search| buffer_search.read(cx).query(cx)) - .unwrap_or_default(), - ) - }; - - let mut previous_matches = HashMap::default(); - update_cached_entries = match &mut self.mode { - ItemsDisplayMode::Search(current_search_state) => { - let update = current_search_state.query != new_search_query - || current_search_state.kind != kind - || current_search_state.matches.is_empty() - || current_search_state.matches.iter().enumerate().any( - |(i, (match_range, _))| new_search_matches.get(i) != Some(match_range), - ); - if current_search_state.kind == kind { - previous_matches.extend(current_search_state.matches.drain(..)); - } - update - } - ItemsDisplayMode::Outline => true, - }; - self.mode = ItemsDisplayMode::Search(SearchState::new( - kind, - new_search_query, - previous_matches, - new_search_matches, - cx.theme().syntax().clone(), - window, - cx, - )); - } - update_cached_entries - } - - fn add_excerpt_entries( - &mut self, - state: &mut GenerationState, - buffer_id: BufferId, - entries_to_add: &[ExcerptId], - parent_depth: usize, - track_matches: bool, - is_singleton: bool, - query: Option<&str>, - cx: &mut Context, - ) { - if let Some(excerpts) = self.excerpts.get(&buffer_id) { - let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx); - - for &excerpt_id in entries_to_add { - let Some(excerpt) = excerpts.get(&excerpt_id) else { - continue; - }; - let excerpt_depth = parent_depth + 1; - self.push_entry( - state, - track_matches, - PanelEntry::Outline(OutlineEntry::Excerpt(OutlineEntryExcerpt { - buffer_id, - id: excerpt_id, - range: excerpt.range.clone(), - })), - excerpt_depth, - cx, - ); - - let mut outline_base_depth = excerpt_depth + 1; - if is_singleton { - outline_base_depth = 0; - state.clear(); - } else if query.is_none() - && self - .collapsed_entries - .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id)) - { - continue; - } - - let mut last_depth_at_level: Vec>> = vec![None; 10]; - - let all_outlines: Vec<_> = excerpt.iter_outlines().collect(); - - let mut outline_has_children = HashMap::default(); - let mut visible_outlines = Vec::new(); - let mut collapsed_state: Option<(usize, Range)> = None; - - for (i, &outline) in all_outlines.iter().enumerate() { - let has_children = all_outlines - .get(i + 1) - .map(|next| next.depth > outline.depth) - .unwrap_or(false); - - outline_has_children - .insert((outline.range.clone(), outline.depth), has_children); - - let mut should_include = true; - - if let Some((collapsed_depth, collapsed_range)) = &collapsed_state { - if outline.depth <= *collapsed_depth { - collapsed_state = None; - } else if let Some(buffer_snapshot) = buffer_snapshot.as_ref() { - let outline_start = outline.range.start; - if outline_start - .cmp(&collapsed_range.start, buffer_snapshot) - .is_ge() - && outline_start - .cmp(&collapsed_range.end, buffer_snapshot) - .is_lt() - { - should_include = false; // Skip - inside collapsed range - } else { - collapsed_state = None; - } - } - } - - // Check if this outline itself is collapsed - if should_include - && self.collapsed_entries.contains(&CollapsedEntry::Outline( - buffer_id, - excerpt_id, - outline.range.clone(), - )) - { - collapsed_state = Some((outline.depth, outline.range.clone())); - } - - if should_include { - visible_outlines.push(outline); - } - } - - self.outline_children_cache - .entry(buffer_id) - .or_default() - .extend(outline_has_children); - - for outline in visible_outlines { - let outline_entry = OutlineEntryOutline { - buffer_id, - excerpt_id, - outline: outline.clone(), - }; - - if outline.depth < last_depth_at_level.len() { - last_depth_at_level[outline.depth] = Some(outline.range.clone()); - // Clear deeper levels when we go back to a shallower depth - for d in (outline.depth + 1)..last_depth_at_level.len() { - last_depth_at_level[d] = None; - } - } - - self.push_entry( - state, - track_matches, - PanelEntry::Outline(OutlineEntry::Outline(outline_entry)), - outline_base_depth + outline.depth, - cx, - ); - } - } - } - } - - fn add_search_entries( - &mut self, - state: &mut GenerationState, - active_editor: &Entity, - parent_entry: FsEntry, - parent_depth: usize, - filter_query: Option, - is_singleton: bool, - cx: &mut Context, - ) { - let ItemsDisplayMode::Search(search_state) = &mut self.mode else { - return; - }; - - let kind = search_state.kind; - let related_excerpts = match &parent_entry { - FsEntry::Directory(_) => return, - FsEntry::ExternalFile(external) => &external.excerpts, - FsEntry::File(file) => &file.excerpts, - } - .iter() - .copied() - .collect::>(); - - let depth = if is_singleton { 0 } else { parent_depth + 1 }; - let new_search_matches = search_state - .matches - .iter() - .filter(|(match_range, _)| { - related_excerpts.contains(&match_range.start.excerpt_id) - || related_excerpts.contains(&match_range.end.excerpt_id) - }) - .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) = snapshot.buffer_id_for_anchor(match_range.end) - && editor.is_buffer_folded(buffer_id, cx) - { - return false; - } - true - }); - - let new_search_entries = new_search_matches - .map(|(match_range, search_data)| SearchEntry { - match_range: match_range.clone(), - kind, - render_data: Arc::clone(search_data), - }) - .collect::>(); - for new_search_entry in new_search_entries { - self.push_entry( - state, - filter_query.is_some(), - PanelEntry::Search(new_search_entry), - depth, - cx, - ); - } - } - - fn active_editor(&self) -> Option> { - self.active_item.as_ref()?.active_editor.upgrade() - } - - fn active_item(&self) -> Option> { - self.active_item.as_ref()?.item_handle.upgrade() - } - - fn should_replace_active_item(&self, new_active_item: &dyn ItemHandle) -> bool { - self.active_item().is_none_or(|active_item| { - !self.pinned && active_item.item_id() != new_active_item.item_id() - }) - } - - pub fn toggle_active_editor_pin( - &mut self, - _: &ToggleActiveEditorPin, - window: &mut Window, - cx: &mut Context, - ) { - self.pinned = !self.pinned; - if !self.pinned - && 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); - } - - cx.notify(); - } - - fn selected_entry(&self) -> Option<&PanelEntry> { - match &self.selected_entry { - SelectedEntry::Invalidated(entry) => entry.as_ref(), - SelectedEntry::Valid(entry, _) => Some(entry), - SelectedEntry::None => None, - } - } - - fn select_entry( - &mut self, - entry: PanelEntry, - focus: bool, - window: &mut Window, - cx: &mut Context, - ) { - if focus { - self.focus_handle.focus(window); - } - let ix = self - .cached_entries - .iter() - .enumerate() - .find(|(_, cached_entry)| &cached_entry.entry == &entry) - .map(|(i, _)| i) - .unwrap_or_default(); - - self.selected_entry = SelectedEntry::Valid(entry, ix); - - self.autoscroll(cx); - cx.notify(); - } - - fn width_estimate(&self, depth: usize, entry: &PanelEntry, cx: &App) -> u64 { - let item_text_chars = match entry { - PanelEntry::Fs(FsEntry::ExternalFile(external)) => self - .buffer_snapshot_for_id(external.buffer_id, cx) - .and_then(|snapshot| Some(snapshot.file()?.path().file_name()?.len())) - .unwrap_or_default(), - PanelEntry::Fs(FsEntry::Directory(directory)) => directory - .entry - .path - .file_name() - .map(|name| name.len()) - .unwrap_or_default(), - PanelEntry::Fs(FsEntry::File(file)) => file - .entry - .path - .file_name() - .map(|name| name.len()) - .unwrap_or_default(), - PanelEntry::FoldedDirs(folded_dirs) => { - folded_dirs - .entries - .iter() - .map(|dir| { - dir.path - .file_name() - .map(|name| name.len()) - .unwrap_or_default() - }) - .sum::() - + folded_dirs.entries.len().saturating_sub(1) * "/".len() - } - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self - .excerpt_label(excerpt.buffer_id, &excerpt.range, cx) - .map(|label| label.len()) - .unwrap_or_default(), - PanelEntry::Outline(OutlineEntry::Outline(entry)) => entry.outline.text.len(), - PanelEntry::Search(search) => search - .render_data - .get() - .map(|data| data.context_text.len()) - .unwrap_or_default(), - }; - - (item_text_chars + depth) as u64 - } - - fn render_main_contents( - &mut self, - query: Option, - show_indent_guides: bool, - indent_size: f32, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let contents = if self.cached_entries.is_empty() { - let header = if query.is_some() { - "No matches for query" - } else { - "No outlines available" - }; - - v_flex() - .id("empty-outline-state") - .gap_0p5() - .flex_1() - .justify_center() - .size_full() - .child(h_flex().justify_center().child(Label::new(header))) - .when_some(query, |panel, query| { - panel.child( - h_flex() - .px_0p5() - .justify_center() - .bg(cx.theme().colors().element_selected.opacity(0.2)) - .child(Label::new(query)), - ) - }) - .child(h_flex().justify_center().child({ - let keystroke = match self.position(window, cx) { - DockPosition::Left => window.keystroke_text_for(&workspace::ToggleLeftDock), - DockPosition::Bottom => { - window.keystroke_text_for(&workspace::ToggleBottomDock) - } - DockPosition::Right => { - window.keystroke_text_for(&workspace::ToggleRightDock) - } - }; - Label::new(format!("Toggle Panel With {keystroke}")).color(Color::Muted) - })) - } else { - let list_contents = { - let items_len = self.cached_entries.len(); - let multi_buffer_snapshot = self - .active_editor() - .map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx)); - uniform_list( - "entries", - items_len, - cx.processor(move |outline_panel, range: Range, window, cx| { - let entries = outline_panel.cached_entries.get(range); - entries - .map(|entries| entries.to_vec()) - .unwrap_or_default() - .into_iter() - .filter_map(|cached_entry| match cached_entry.entry { - PanelEntry::Fs(entry) => Some(outline_panel.render_entry( - &entry, - cached_entry.depth, - cached_entry.string_match.as_ref(), - window, - cx, - )), - PanelEntry::FoldedDirs(folded_dirs_entry) => { - Some(outline_panel.render_folded_dirs( - &folded_dirs_entry, - cached_entry.depth, - cached_entry.string_match.as_ref(), - window, - cx, - )) - } - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { - outline_panel.render_excerpt( - &excerpt, - cached_entry.depth, - window, - cx, - ) - } - PanelEntry::Outline(OutlineEntry::Outline(entry)) => { - Some(outline_panel.render_outline( - &entry, - cached_entry.depth, - cached_entry.string_match.as_ref(), - window, - cx, - )) - } - PanelEntry::Search(SearchEntry { - match_range, - render_data, - kind, - .. - }) => outline_panel.render_search_match( - multi_buffer_snapshot.as_ref(), - &match_range, - &render_data, - kind, - cached_entry.depth, - cached_entry.string_match.as_ref(), - window, - cx, - ), - }) - .collect() - }), - ) - .with_sizing_behavior(ListSizingBehavior::Infer) - .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained) - .with_width_from_item(self.max_width_item_index) - .track_scroll(&self.scroll_handle) - .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.); - - 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() - }), - ) - }) - }; - - v_flex() - .flex_shrink() - .size_full() - .child(list_contents.size_full().flex_shrink()) - .custom_scrollbars( - Scrollbars::for_settings::() - .tracked_scroll_handle(&self.scroll_handle.clone()) - .with_track_along( - ScrollAxes::Horizontal, - cx.theme().colors().panel_background, - ) - .tracked_entity(cx.entity_id()), - window, - cx, - ) - } - .children(self.context_menu.as_ref().map(|(menu, position, _)| { - deferred( - anchored() - .position(*position) - .anchor(gpui::Corner::TopLeft) - .child(menu.clone()), - ) - .with_priority(1) - })); - - v_flex().w_full().flex_1().overflow_hidden().child(contents) - } - - fn render_filter_footer(&mut self, pinned: bool, cx: &mut Context) -> Div { - let (icon, icon_tooltip) = if pinned { - (IconName::Unpin, "Unpin Outline") - } else { - (IconName::Pin, "Pin Active Outline") - }; - - h_flex() - .p_2() - .h(Tab::container_height(cx)) - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border) - .child( - h_flex() - .w_full() - .gap_1p5() - .child( - Icon::new(IconName::MagnifyingGlass) - .size(IconSize::Small) - .color(Color::Muted), - ) - .child(self.filter_editor.clone()), - ) - .child( - IconButton::new("pin_button", icon) - .tooltip(Tooltip::text(icon_tooltip)) - .shape(IconButtonShape::Square) - .on_click(cx.listener(|outline_panel, _, window, cx| { - outline_panel.toggle_active_editor_pin(&ToggleActiveEditorPin, window, cx); - })), - ) - } - - fn buffers_inside_directory( - &self, - dir_worktree: WorktreeId, - dir_entry: &GitEntry, - ) -> HashSet { - if !dir_entry.is_dir() { - debug_panic!("buffers_inside_directory called on a non-directory entry {dir_entry:?}"); - return HashSet::default(); - } - - self.fs_entries - .iter() - .skip_while(|fs_entry| match fs_entry { - FsEntry::Directory(directory) => { - directory.worktree_id != dir_worktree || &directory.entry != dir_entry - } - _ => true, - }) - .skip(1) - .take_while(|fs_entry| match fs_entry { - FsEntry::ExternalFile(..) => false, - FsEntry::Directory(directory) => { - directory.worktree_id == dir_worktree - && directory.entry.path.starts_with(&dir_entry.path) - } - FsEntry::File(file) => { - file.worktree_id == dir_worktree && file.entry.path.starts_with(&dir_entry.path) - } - }) - .filter_map(|fs_entry| match fs_entry { - FsEntry::File(file) => Some(file.buffer_id), - _ => None, - }) - .collect() - } -} - -fn workspace_active_editor( - workspace: &Workspace, - cx: &App, -) -> Option<(Box, Entity)> { - let active_item = workspace.active_item(cx)?; - let active_editor = active_item - .act_as::(cx) - .filter(|editor| editor.read(cx).mode().is_full())?; - Some((active_item, active_editor)) -} - -fn back_to_common_visited_parent( - visited_dirs: &mut Vec<(ProjectEntryId, Arc)>, - worktree_id: &WorktreeId, - new_entry: &Entry, -) -> Option<(WorktreeId, ProjectEntryId)> { - while let Some((visited_dir_id, visited_path)) = visited_dirs.last() { - match new_entry.path.parent() { - Some(parent_path) => { - if parent_path == visited_path.as_ref() { - return Some((*worktree_id, *visited_dir_id)); - } - } - None => { - break; - } - } - visited_dirs.pop(); - } - None -} - -fn file_name(path: &Path) -> String { - let mut current_path = path; - loop { - if let Some(file_name) = current_path.file_name() { - return file_name.to_string_lossy().into_owned(); - } - match current_path.parent() { - Some(parent) => current_path = parent, - None => return path.to_string_lossy().into_owned(), - } - } -} - -impl Panel for OutlinePanel { - fn persistent_name() -> &'static str { - "Outline Panel" - } - - fn panel_key() -> &'static str { - OUTLINE_PANEL_KEY - } - - fn position(&self, _: &Window, cx: &App) -> DockPosition { - match OutlinePanelSettings::get_global(cx).dock { - DockSide::Left => DockPosition::Left, - DockSide::Right => DockPosition::Right, - } - } - - fn position_is_valid(&self, position: DockPosition) -> bool { - matches!(position, DockPosition::Left | DockPosition::Right) - } - - fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { - settings::update_settings_file(self.fs.clone(), cx, move |settings, _| { - let dock = match position { - DockPosition::Left | DockPosition::Bottom => DockSide::Left, - DockPosition::Right => DockSide::Right, - }; - settings.outline_panel.get_or_insert_default().dock = Some(dock); - }); - } - - fn size(&self, _: &Window, cx: &App) -> Pixels { - self.width - .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width) - } - - fn set_size(&mut self, size: Option, window: &mut Window, cx: &mut Context) { - self.width = size; - cx.notify(); - cx.defer_in(window, |this, _, cx| { - this.serialize(cx); - }); - } - - fn icon(&self, _: &Window, cx: &App) -> Option { - OutlinePanelSettings::get_global(cx) - .button - .then_some(IconName::ListTree) - } - - fn icon_tooltip(&self, _window: &Window, _: &App) -> Option<&'static str> { - Some("Outline Panel") - } - - fn toggle_action(&self) -> Box { - Box::new(ToggleFocus) - } - - fn starts_open(&self, _window: &Window, _: &App) -> bool { - self.active - } - - fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context) { - cx.spawn_in(window, async move |outline_panel, cx| { - outline_panel - .update_in(cx, |outline_panel, window, cx| { - let old_active = outline_panel.active; - outline_panel.active = active; - if old_active != active { - if active - && 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) - } - return; - } - - if !outline_panel.pinned { - outline_panel.clear_previous(window, cx); - } - } - outline_panel.serialize(cx); - }) - .ok(); - }) - .detach() - } - - fn activation_priority(&self) -> u32 { - 5 - } -} - -impl Focusable for OutlinePanel { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.filter_editor.focus_handle(cx) - } -} - -impl EventEmitter for OutlinePanel {} - -impl EventEmitter for OutlinePanel {} - -impl Render for OutlinePanel { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let (is_local, is_via_ssh) = self.project.read_with(cx, |project, _| { - (project.is_local(), project.is_via_remote_server()) - }); - let query = self.query(cx); - let pinned = self.pinned; - let settings = OutlinePanelSettings::get_global(cx); - let indent_size = settings.indent_size; - let show_indent_guides = settings.indent_guides.show == ShowIndentGuides::Always; - - let search_query = match &self.mode { - ItemsDisplayMode::Search(search_query) => Some(search_query), - _ => None, - }; - - let search_query_text = search_query.map(|sq| sq.query.to_string()); - - v_flex() - .id("outline-panel") - .size_full() - .overflow_hidden() - .relative() - .key_context(self.dispatch_context(window, cx)) - .on_action(cx.listener(Self::open_selected_entry)) - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(Self::select_next)) - .on_action(cx.listener(Self::select_previous)) - .on_action(cx.listener(Self::select_first)) - .on_action(cx.listener(Self::select_last)) - .on_action(cx.listener(Self::select_parent)) - .on_action(cx.listener(Self::expand_selected_entry)) - .on_action(cx.listener(Self::collapse_selected_entry)) - .on_action(cx.listener(Self::expand_all_entries)) - .on_action(cx.listener(Self::collapse_all_entries)) - .on_action(cx.listener(Self::copy_path)) - .on_action(cx.listener(Self::copy_relative_path)) - .on_action(cx.listener(Self::toggle_active_editor_pin)) - .on_action(cx.listener(Self::unfold_directory)) - .on_action(cx.listener(Self::fold_directory)) - .on_action(cx.listener(Self::open_excerpts)) - .on_action(cx.listener(Self::open_excerpts_split)) - .when(is_local, |el| { - el.on_action(cx.listener(Self::reveal_in_finder)) - }) - .when(is_local || is_via_ssh, |el| { - el.on_action(cx.listener(Self::open_in_terminal)) - }) - .on_mouse_down( - MouseButton::Right, - cx.listener(move |outline_panel, event: &MouseDownEvent, window, cx| { - if let Some(entry) = outline_panel.selected_entry().cloned() { - outline_panel.deploy_context_menu(event.position, entry, window, cx) - } else if let Some(entry) = outline_panel.fs_entries.first().cloned() { - outline_panel.deploy_context_menu( - event.position, - PanelEntry::Fs(entry), - window, - cx, - ) - } - }), - ) - .track_focus(&self.focus_handle) - .child(self.render_filter_footer(pinned, cx)) - .when_some(search_query_text, |outline_panel, query_text| { - outline_panel.child( - h_flex() - .py_1p5() - .px_2() - .h(Tab::container_height(cx)) - .gap_0p5() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .child(Label::new("Searching:").color(Color::Muted)) - .child(Label::new(query_text)), - ) - }) - .child(self.render_main_contents(query, show_indent_guides, indent_size, window, cx)) - } -} - -fn find_active_indent_guide_ix( - outline_panel: &OutlinePanel, - candidates: &[IndentGuideLayout], -) -> Option { - let SelectedEntry::Valid(_, target_ix) = &outline_panel.selected_entry else { - return None; - }; - let target_depth = outline_panel - .cached_entries - .get(*target_ix) - .map(|cached_entry| cached_entry.depth)?; - - let (target_ix, target_depth) = if let Some(target_depth) = outline_panel - .cached_entries - .get(target_ix + 1) - .filter(|cached_entry| cached_entry.depth > target_depth) - .map(|entry| entry.depth) - { - (target_ix + 1, target_depth.saturating_sub(1)) - } else { - (*target_ix, target_depth.saturating_sub(1)) - }; - - candidates - .iter() - .enumerate() - .find(|(_, guide)| { - guide.offset.y <= target_ix - && target_ix < guide.offset.y + guide.length - && guide.offset.x == target_depth - }) - .map(|(ix, _)| ix) -} - -fn subscribe_for_editor_events( - editor: &Entity, - window: &mut Window, - cx: &mut Context, -) -> Subscription { - let debounce = Some(UPDATE_DEBOUNCE); - cx.subscribe_in( - editor, - window, - move |outline_panel, editor, e: &EditorEvent, window, cx| { - if !outline_panel.active { - return; - } - match e { - EditorEvent::SelectionsChanged { local: true } => { - outline_panel.reveal_entry_for_selection(editor.clone(), window, cx); - cx.notify(); - } - EditorEvent::ExcerptsAdded { excerpts, .. } => { - outline_panel - .new_entries_for_fs_update - .extend(excerpts.iter().map(|&(excerpt_id, _)| excerpt_id)); - outline_panel.update_fs_entries(editor.clone(), debounce, window, cx); - } - EditorEvent::ExcerptsRemoved { ids, .. } => { - let mut ids = ids.iter().collect::>(); - for excerpts in outline_panel.excerpts.values_mut() { - excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id)); - if ids.is_empty() { - break; - } - } - outline_panel.update_fs_entries(editor.clone(), debounce, window, cx); - } - EditorEvent::ExcerptsExpanded { ids } => { - outline_panel.invalidate_outlines(ids); - let update_cached_items = outline_panel.update_non_fs_items(window, cx); - if update_cached_items { - outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); - } - } - EditorEvent::ExcerptsEdited { ids } => { - outline_panel.invalidate_outlines(ids); - let update_cached_items = outline_panel.update_non_fs_items(window, cx); - if update_cached_items { - outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); - } - } - EditorEvent::BufferFoldToggled { ids, .. } => { - outline_panel.invalidate_outlines(ids); - let mut latest_unfolded_buffer_id = None; - let mut latest_folded_buffer_id = None; - let mut ignore_selections_change = false; - outline_panel.new_entries_for_fs_update.extend( - ids.iter() - .filter(|id| { - outline_panel - .excerpts - .iter() - .find_map(|(buffer_id, excerpts)| { - if excerpts.contains_key(id) { - ignore_selections_change |= outline_panel - .preserve_selection_on_buffer_fold_toggles - .remove(buffer_id); - Some(buffer_id) - } else { - None - } - }) - .map(|buffer_id| { - if editor.read(cx).is_buffer_folded(*buffer_id, cx) { - latest_folded_buffer_id = Some(*buffer_id); - false - } else { - latest_unfolded_buffer_id = Some(*buffer_id); - true - } - }) - .unwrap_or(true) - }) - .copied(), - ); - if !ignore_selections_change - && 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( - |fs_entry| match fs_entry { - FsEntry::ExternalFile(external) => { - if external.buffer_id == toggled_buffer_id { - Some(fs_entry.clone()) - } else { - None - } - } - FsEntry::File(FsEntryFile { buffer_id, .. }) => { - if *buffer_id == toggled_buffer_id { - Some(fs_entry.clone()) - } else { - None - } - } - FsEntry::Directory(..) => None, - }, - ) - }) - .map(PanelEntry::Fs) - { - 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() { - excerpt.invalidate_outlines(); - } - } - let update_cached_items = outline_panel.update_non_fs_items(window, cx); - if update_cached_items { - outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); - } - } - EditorEvent::TitleChanged => { - outline_panel.update_fs_entries(editor.clone(), debounce, window, cx); - } - _ => {} - } - }, - ) -} - -fn empty_icon() -> AnyElement { - h_flex() - .size(IconSize::default().rems()) - .invisible() - .flex_none() - .into_any_element() -} - -#[derive(Debug, Default)] -struct GenerationState { - entries: Vec, - match_candidates: Vec, - max_width_estimate_and_index: Option<(u64, usize)>, -} - -impl GenerationState { - fn clear(&mut self) { - self.entries.clear(); - self.match_candidates.clear(); - self.max_width_estimate_and_index = None; - } -} - -#[cfg(test)] -mod tests { - use db::indoc; - use gpui::{TestAppContext, VisualTestContext, WindowHandle}; - use language::rust_lang; - use pretty_assertions::assert_eq; - use project::FakeFs; - use search::{ - buffer_search, - project_search::{self, perform_project_search}, - }; - use serde_json::json; - use util::path; - use workspace::{OpenOptions, OpenVisible, ToolbarItemView}; - - use super::*; - - const SELECTED_MARKER: &str = " <==== selected"; - - #[gpui::test(iterations = 10)] - async fn test_project_search_results_toggling(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - let root = path!("/rust-analyzer"); - populate_with_test_ra_project(&fs, root).await; - let project = Project::test(fs.clone(), [Path::new(root)], cx).await; - project.read_with(cx, |project, _| project.languages().add(rust_lang())); - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let outline_panel = outline_panel(&workspace, cx); - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.set_active(true, window, cx) - }); - - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::default(), - window, - cx, - ) - }) - .unwrap(); - let search_view = workspace - .update(cx, |workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()) - .expect("Project search view expected to appear after new search event trigger") - }) - .unwrap(); - - let query = "param_names_for_lifetime_elision_hints"; - perform_project_search(&search_view, query, cx); - search_view.update(cx, |search_view, cx| { - search_view - .results_editor() - .update(cx, |results_editor, cx| { - assert_eq!( - results_editor.display_text(cx).match_indices(query).count(), - 9 - ); - }); - }); - - let all_matches = r#"rust-analyzer/ - crates/ - ide/src/ - inlay_hints/ - fn_lifetime_fn.rs - search: match config.«param_names_for_lifetime_elision_hints» { - search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» { - search: Some(it) if config.«param_names_for_lifetime_elision_hints» => { - search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG }, - inlay_hints.rs - search: pub «param_names_for_lifetime_elision_hints»: bool, - search: «param_names_for_lifetime_elision_hints»: self - static_index.rs - search: «param_names_for_lifetime_elision_hints»: false, - rust-analyzer/src/ - cli/ - analysis_stats.rs - search: «param_names_for_lifetime_elision_hints»: true, - config.rs - search: «param_names_for_lifetime_elision_hints»: self"# - .to_string(); - - let select_first_in_all_matches = |line_to_select: &str| { - assert!( - all_matches.contains(line_to_select), - "`{line_to_select}` was not found in all matches `{all_matches}`" - ); - all_matches.replacen( - line_to_select, - &format!("{line_to_select}{SELECTED_MARKER}"), - 1, - ) - }; - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - select_first_in_all_matches( - "search: match config.«param_names_for_lifetime_elision_hints» {" - ) - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.select_parent(&SelectParent, window, cx); - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - select_first_in_all_matches("fn_lifetime_fn.rs") - ); - }); - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"rust-analyzer/ - crates/ - ide/src/ - inlay_hints/ - fn_lifetime_fn.rs{SELECTED_MARKER} - inlay_hints.rs - search: pub «param_names_for_lifetime_elision_hints»: bool, - search: «param_names_for_lifetime_elision_hints»: self - static_index.rs - search: «param_names_for_lifetime_elision_hints»: false, - rust-analyzer/src/ - cli/ - analysis_stats.rs - search: «param_names_for_lifetime_elision_hints»: true, - config.rs - search: «param_names_for_lifetime_elision_hints»: self"#, - ) - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.expand_all_entries(&ExpandAllEntries, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.select_parent(&SelectParent, window, cx); - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - select_first_in_all_matches("inlay_hints/") - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.select_parent(&SelectParent, window, cx); - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - select_first_in_all_matches("ide/src/") - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"rust-analyzer/ - crates/ - ide/src/{SELECTED_MARKER} - rust-analyzer/src/ - cli/ - analysis_stats.rs - search: «param_names_for_lifetime_elision_hints»: true, - config.rs - search: «param_names_for_lifetime_elision_hints»: self"#, - ) - ); - }); - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - select_first_in_all_matches("ide/src/") - ); - }); - } - - #[gpui::test(iterations = 10)] - async fn test_item_filtering(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - let root = path!("/rust-analyzer"); - populate_with_test_ra_project(&fs, root).await; - let project = Project::test(fs.clone(), [Path::new(root)], cx).await; - project.read_with(cx, |project, _| project.languages().add(rust_lang())); - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let outline_panel = outline_panel(&workspace, cx); - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.set_active(true, window, cx) - }); - - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::default(), - window, - cx, - ) - }) - .unwrap(); - let search_view = workspace - .update(cx, |workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()) - .expect("Project search view expected to appear after new search event trigger") - }) - .unwrap(); - - let query = "param_names_for_lifetime_elision_hints"; - perform_project_search(&search_view, query, cx); - search_view.update(cx, |search_view, cx| { - search_view - .results_editor() - .update(cx, |results_editor, cx| { - assert_eq!( - results_editor.display_text(cx).match_indices(query).count(), - 9 - ); - }); - }); - let all_matches = r#"rust-analyzer/ - crates/ - ide/src/ - inlay_hints/ - fn_lifetime_fn.rs - search: match config.«param_names_for_lifetime_elision_hints» { - search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» { - search: Some(it) if config.«param_names_for_lifetime_elision_hints» => { - search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG }, - inlay_hints.rs - search: pub «param_names_for_lifetime_elision_hints»: bool, - search: «param_names_for_lifetime_elision_hints»: self - static_index.rs - search: «param_names_for_lifetime_elision_hints»: false, - rust-analyzer/src/ - cli/ - analysis_stats.rs - search: «param_names_for_lifetime_elision_hints»: true, - config.rs - search: «param_names_for_lifetime_elision_hints»: self"# - .to_string(); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - None, - cx, - ), - all_matches, - ); - }); - - let filter_text = "a"; - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.filter_editor.update(cx, |filter_editor, cx| { - filter_editor.set_text(filter_text, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - None, - cx, - ), - all_matches - .lines() - .skip(1) // `/rust-analyzer/` is a root entry with path `` and it will be filtered out - .filter(|item| item.contains(filter_text)) - .collect::>() - .join("\n"), - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.filter_editor.update(cx, |filter_editor, cx| { - filter_editor.set_text("", window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - None, - cx, - ), - all_matches, - ); - }); - } - - #[gpui::test(iterations = 10)] - async fn test_item_opening(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - let root = path!("/rust-analyzer"); - populate_with_test_ra_project(&fs, root).await; - let project = Project::test(fs.clone(), [Path::new(root)], cx).await; - project.read_with(cx, |project, _| project.languages().add(rust_lang())); - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let outline_panel = outline_panel(&workspace, cx); - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.set_active(true, window, cx) - }); - - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::default(), - window, - cx, - ) - }) - .unwrap(); - let search_view = workspace - .update(cx, |workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()) - .expect("Project search view expected to appear after new search event trigger") - }) - .unwrap(); - - let query = "param_names_for_lifetime_elision_hints"; - perform_project_search(&search_view, query, cx); - search_view.update(cx, |search_view, cx| { - search_view - .results_editor() - .update(cx, |results_editor, cx| { - assert_eq!( - results_editor.display_text(cx).match_indices(query).count(), - 9 - ); - }); - }); - let all_matches = r#"rust-analyzer/ - crates/ - ide/src/ - inlay_hints/ - fn_lifetime_fn.rs - search: match config.«param_names_for_lifetime_elision_hints» { - search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» { - search: Some(it) if config.«param_names_for_lifetime_elision_hints» => { - search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG }, - inlay_hints.rs - search: pub «param_names_for_lifetime_elision_hints»: bool, - search: «param_names_for_lifetime_elision_hints»: self - static_index.rs - search: «param_names_for_lifetime_elision_hints»: false, - rust-analyzer/src/ - cli/ - analysis_stats.rs - search: «param_names_for_lifetime_elision_hints»: true, - config.rs - search: «param_names_for_lifetime_elision_hints»: self"# - .to_string(); - let select_first_in_all_matches = |line_to_select: &str| { - assert!( - all_matches.contains(line_to_select), - "`{line_to_select}` was not found in all matches `{all_matches}`" - ); - all_matches.replacen( - line_to_select, - &format!("{line_to_select}{SELECTED_MARKER}"), - 1, - ) - }; - let clear_outline_metadata = |input: &str| { - input - .replace("search: ", "") - .replace("«", "") - .replace("»", "") - }; - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - let active_editor = outline_panel.read_with(cx, |outline_panel, _| { - outline_panel - .active_editor() - .expect("should have an active editor open") - }); - let initial_outline_selection = - "search: match config.«param_names_for_lifetime_elision_hints» {"; - outline_panel.update_in(cx, |outline_panel, window, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - select_first_in_all_matches(initial_outline_selection) - ); - assert_eq!( - selected_row_text(&active_editor, cx), - clear_outline_metadata(initial_outline_selection), - "Should place the initial editor selection on the corresponding search result" - ); - - outline_panel.select_next(&SelectNext, window, cx); - outline_panel.select_next(&SelectNext, window, cx); - }); - - let navigated_outline_selection = - "search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {"; - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - select_first_in_all_matches(navigated_outline_selection) - ); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - outline_panel.update(cx, |_, cx| { - assert_eq!( - selected_row_text(&active_editor, cx), - clear_outline_metadata(navigated_outline_selection), - "Should still have the initial caret position after SelectNext calls" - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx); - }); - outline_panel.update(cx, |_outline_panel, cx| { - assert_eq!( - selected_row_text(&active_editor, cx), - clear_outline_metadata(navigated_outline_selection), - "After opening, should move the caret to the opened outline entry's position" - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.select_next(&SelectNext, window, cx); - }); - let next_navigated_outline_selection = "search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },"; - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - select_first_in_all_matches(next_navigated_outline_selection) - ); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - outline_panel.update(cx, |_outline_panel, cx| { - assert_eq!( - selected_row_text(&active_editor, cx), - clear_outline_metadata(next_navigated_outline_selection), - "Should again preserve the selection after another SelectNext call" - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.open_excerpts(&editor::actions::OpenExcerpts, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - let new_active_editor = outline_panel.read_with(cx, |outline_panel, _| { - outline_panel - .active_editor() - .expect("should have an active editor open") - }); - outline_panel.update(cx, |outline_panel, cx| { - assert_ne!( - active_editor, new_active_editor, - "After opening an excerpt, new editor should be open" - ); - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - "outline: pub(super) fn hints -outline: fn hints_lifetimes_named <==== selected" - ); - assert_eq!( - selected_row_text(&new_active_editor, cx), - clear_outline_metadata(next_navigated_outline_selection), - "When opening the excerpt, should navigate to the place corresponding the outline entry" - ); - }); - } - - #[gpui::test] - async fn test_multiple_worktrees(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/root"), - json!({ - "one": { - "a.txt": "aaa aaa" - }, - "two": { - "b.txt": "a aaa" - } - - }), - ) - .await; - let project = Project::test(fs.clone(), [Path::new(path!("/root/one"))], cx).await; - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let outline_panel = outline_panel(&workspace, cx); - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.set_active(true, window, cx) - }); - - let items = workspace - .update(cx, |workspace, window, cx| { - workspace.open_paths( - vec![PathBuf::from(path!("/root/two"))], - OpenOptions { - visible: Some(OpenVisible::OnlyDirectories), - ..Default::default() - }, - None, - window, - cx, - ) - }) - .unwrap() - .await; - assert_eq!(items.len(), 1, "Were opening another worktree directory"); - assert!( - items[0].is_none(), - "Directory should be opened successfully" - ); - - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::default(), - window, - cx, - ) - }) - .unwrap(); - let search_view = workspace - .update(cx, |workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()) - .expect("Project search view expected to appear after new search event trigger") - }) - .unwrap(); - - let query = "aaa"; - perform_project_search(&search_view, query, cx); - search_view.update(cx, |search_view, cx| { - search_view - .results_editor() - .update(cx, |results_editor, cx| { - assert_eq!( - results_editor.display_text(cx).match_indices(query).count(), - 3 - ); - }); - }); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"one/ - a.txt - search: «aaa» aaa <==== selected - search: aaa «aaa» -two/ - b.txt - search: a «aaa»"#, - ), - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.select_previous(&SelectPrevious, window, cx); - outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"one/ - a.txt <==== selected -two/ - b.txt - search: a «aaa»"#, - ), - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.select_next(&SelectNext, window, cx); - outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"one/ - a.txt -two/ <==== selected"#, - ), - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"one/ - a.txt -two/ <==== selected - b.txt - search: a «aaa»"#, - ) - ); - }); - } - - #[gpui::test] - async fn test_navigating_in_singleton(cx: &mut TestAppContext) { - init_test(cx); - - let root = path!("/root"); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - root, - json!({ - "src": { - "lib.rs": indoc!(" -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -struct OutlineEntryExcerpt { - id: ExcerptId, - buffer_id: BufferId, - range: ExcerptRange, -}"), - } - }), - ) - .await; - let project = Project::test(fs.clone(), [Path::new(root)], cx).await; - project.read_with(cx, |project, _| project.languages().add(rust_lang())); - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let outline_panel = outline_panel(&workspace, cx); - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.set_active(true, window, cx) - }); - }); - - let _editor = workspace - .update(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from(path!("/root/src/lib.rs")), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .unwrap() - .await - .expect("Failed to open Rust source file") - .downcast::() - .expect("Should open an editor for Rust source file"); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt - outline: id - outline: buffer_id - outline: range" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_next(&SelectNext, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt <==== selected - outline: id - outline: buffer_id - outline: range" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_next(&SelectNext, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt - outline: id <==== selected - outline: buffer_id - outline: range" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_next(&SelectNext, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt - outline: id - outline: buffer_id <==== selected - outline: range" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_next(&SelectNext, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt - outline: id - outline: buffer_id - outline: range <==== selected" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_next(&SelectNext, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt <==== selected - outline: id - outline: buffer_id - outline: range" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_previous(&SelectPrevious, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt - outline: id - outline: buffer_id - outline: range <==== selected" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_previous(&SelectPrevious, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt - outline: id - outline: buffer_id <==== selected - outline: range" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_previous(&SelectPrevious, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt - outline: id <==== selected - outline: buffer_id - outline: range" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_previous(&SelectPrevious, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt <==== selected - outline: id - outline: buffer_id - outline: range" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_previous(&SelectPrevious, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt - outline: id - outline: buffer_id - outline: range <==== selected" - ) - ); - }); - } - - #[gpui::test(iterations = 10)] - async fn test_frontend_repo_structure(cx: &mut TestAppContext) { - init_test(cx); - - let root = path!("/frontend-project"); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - root, - json!({ - "public": { - "lottie": { - "syntax-tree.json": r#"{ "something": "static" }"# - } - }, - "src": { - "app": { - "(site)": { - "(about)": { - "jobs": { - "[slug]": { - "page.tsx": r#"static"# - } - } - }, - "(blog)": { - "post": { - "[slug]": { - "page.tsx": r#"static"# - } - } - }, - } - }, - "components": { - "ErrorBoundary.tsx": r#"static"#, - } - } - - }), - ) - .await; - let project = Project::test(fs.clone(), [Path::new(root)], cx).await; - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let outline_panel = outline_panel(&workspace, cx); - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.set_active(true, window, cx) - }); - - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::default(), - window, - cx, - ) - }) - .unwrap(); - let search_view = workspace - .update(cx, |workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()) - .expect("Project search view expected to appear after new search event trigger") - }) - .unwrap(); - - let query = "static"; - perform_project_search(&search_view, query, cx); - search_view.update(cx, |search_view, cx| { - search_view - .results_editor() - .update(cx, |results_editor, cx| { - assert_eq!( - results_editor.display_text(cx).match_indices(query).count(), - 4 - ); - }); - }); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"frontend-project/ - public/lottie/ - syntax-tree.json - search: {{ "something": "«static»" }} <==== selected - src/ - app/(site)/ - (about)/jobs/[slug]/ - page.tsx - search: «static» - (blog)/post/[slug]/ - page.tsx - search: «static» - components/ - ErrorBoundary.tsx - search: «static»"# - ) - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - // Move to 5th element in the list, 3 items down. - for _ in 0..2 { - outline_panel.select_next(&SelectNext, window, cx); - } - outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"frontend-project/ - public/lottie/ - syntax-tree.json - search: {{ "something": "«static»" }} - src/ - app/(site)/ <==== selected - components/ - ErrorBoundary.tsx - search: «static»"# - ) - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - // Move to the next visible non-FS entry - for _ in 0..3 { - outline_panel.select_next(&SelectNext, window, cx); - } - }); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"frontend-project/ - public/lottie/ - syntax-tree.json - search: {{ "something": "«static»" }} - src/ - app/(site)/ - components/ - ErrorBoundary.tsx - search: «static» <==== selected"# - ) - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel - .active_editor() - .expect("Should have an active editor") - .update(cx, |editor, cx| { - editor.toggle_fold(&editor::actions::ToggleFold, window, cx) - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"frontend-project/ - public/lottie/ - syntax-tree.json - search: {{ "something": "«static»" }} - src/ - app/(site)/ - components/ - ErrorBoundary.tsx <==== selected"# - ) - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel - .active_editor() - .expect("Should have an active editor") - .update(cx, |editor, cx| { - editor.toggle_fold(&editor::actions::ToggleFold, window, cx) - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"frontend-project/ - public/lottie/ - syntax-tree.json - search: {{ "something": "«static»" }} - src/ - app/(site)/ - components/ - ErrorBoundary.tsx <==== selected - search: «static»"# - ) - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.collapse_all_entries(&CollapseAllEntries, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!(r#"frontend-project/"#) - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.expand_all_entries(&ExpandAllEntries, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"frontend-project/ - public/lottie/ - syntax-tree.json - search: {{ "something": "«static»" }} - src/ - app/(site)/ - (about)/jobs/[slug]/ - page.tsx - search: «static» - (blog)/post/[slug]/ - page.tsx - search: «static» - components/ - ErrorBoundary.tsx <==== selected - search: «static»"# - ) - ); - }); - } - - async fn add_outline_panel( - project: &Entity, - cx: &mut TestAppContext, - ) -> WindowHandle { - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let outline_panel = window - .update(cx, |_, window, cx| { - cx.spawn_in(window, async |this, cx| { - OutlinePanel::load(this, cx.clone()).await - }) - }) - .unwrap() - .await - .expect("Failed to load outline panel"); - - window - .update(cx, |workspace, window, cx| { - workspace.add_panel(outline_panel, window, cx); - }) - .unwrap(); - window - } - - fn outline_panel( - workspace: &WindowHandle, - cx: &mut TestAppContext, - ) -> Entity { - workspace - .update(cx, |workspace, _, cx| { - workspace - .panel::(cx) - .expect("no outline panel") - }) - .unwrap() - } - - fn display_entries( - project: &Entity, - multi_buffer_snapshot: &MultiBufferSnapshot, - cached_entries: &[CachedEntry], - selected_entry: Option<&PanelEntry>, - cx: &mut App, - ) -> String { - let project = project.read(cx); - let mut display_string = String::new(); - for entry in cached_entries { - if !display_string.is_empty() { - display_string += "\n"; - } - for _ in 0..entry.depth { - display_string += " "; - } - display_string += &match &entry.entry { - PanelEntry::Fs(entry) => match entry { - FsEntry::ExternalFile(_) => { - panic!("Did not cover external files with tests") - } - FsEntry::Directory(directory) => { - let path = if let Some(worktree) = project - .worktree_for_id(directory.worktree_id, cx) - .filter(|worktree| { - worktree.read(cx).root_entry() == Some(&directory.entry.entry) - }) { - worktree - .read(cx) - .root_name() - .join(&directory.entry.path) - .as_unix_str() - .to_string() - } else { - directory - .entry - .path - .file_name() - .unwrap_or_default() - .to_string() - }; - format!("{path}/") - } - FsEntry::File(file) => file - .entry - .path - .file_name() - .map(|name| name.to_string()) - .unwrap_or_default(), - }, - PanelEntry::FoldedDirs(folded_dirs) => folded_dirs - .entries - .iter() - .filter_map(|dir| dir.path.file_name()) - .map(|name| name.to_string() + "/") - .collect(), - PanelEntry::Outline(outline_entry) => match outline_entry { - OutlineEntry::Excerpt(_) => continue, - OutlineEntry::Outline(outline_entry) => { - format!("outline: {}", outline_entry.outline.text) - } - }, - PanelEntry::Search(search_entry) => { - let search_data = search_entry.render_data.get_or_init(|| { - SearchData::new(&search_entry.match_range, multi_buffer_snapshot) - }); - let mut search_result = String::new(); - let mut last_end = 0; - for range in &search_data.search_match_indices { - search_result.push_str(&search_data.context_text[last_end..range.start]); - search_result.push('«'); - search_result.push_str(&search_data.context_text[range.start..range.end]); - search_result.push('»'); - last_end = range.end; - } - search_result.push_str(&search_data.context_text[last_end..]); - - format!("search: {search_result}") - } - }; - - if Some(&entry.entry) == selected_entry { - display_string += SELECTED_MARKER; - } - } - display_string - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings = SettingsStore::test(cx); - cx.set_global(settings); - - theme::init(theme::LoadThemes::JustBase, cx); - - editor::init(cx); - project_search::init(cx); - buffer_search::init(cx); - super::init(cx); - }); - } - - // Based on https://github.com/rust-lang/rust-analyzer/ - async fn populate_with_test_ra_project(fs: &FakeFs, root: &str) { - fs.insert_tree( - root, - json!({ - "crates": { - "ide": { - "src": { - "inlay_hints": { - "fn_lifetime_fn.rs": r##" - pub(super) fn hints( - acc: &mut Vec, - config: &InlayHintsConfig, - func: ast::Fn, - ) -> Option<()> { - // ... snip - - let mut used_names: FxHashMap = - match config.param_names_for_lifetime_elision_hints { - true => generic_param_list - .iter() - .flat_map(|gpl| gpl.lifetime_params()) - .filter_map(|param| param.lifetime()) - .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0))) - .collect(), - false => Default::default(), - }; - { - let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided); - if self_param.is_some() && potential_lt_refs.next().is_some() { - allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints { - // self can't be used as a lifetime, so no need to check for collisions - "'self".into() - } else { - gen_idx_name() - }); - } - potential_lt_refs.for_each(|(name, ..)| { - let name = match name { - Some(it) if config.param_names_for_lifetime_elision_hints => { - if let Some(c) = used_names.get_mut(it.text().as_str()) { - *c += 1; - SmolStr::from(format!("'{text}{c}", text = it.text().as_str())) - } else { - used_names.insert(it.text().as_str().into(), 0); - SmolStr::from_iter(["\'", it.text().as_str()]) - } - } - _ => gen_idx_name(), - }; - allocated_lifetimes.push(name); - }); - } - - // ... snip - } - - // ... snip - - #[test] - fn hints_lifetimes_named() { - check_with_config( - InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }, - r#" - fn nested_in<'named>(named: & &X< &()>) {} - // ^'named1, 'named2, 'named3, $ - //^'named1 ^'named2 ^'named3 - "#, - ); - } - - // ... snip - "##, - }, - "inlay_hints.rs": r#" - #[derive(Clone, Debug, PartialEq, Eq)] - pub struct InlayHintsConfig { - // ... snip - pub param_names_for_lifetime_elision_hints: bool, - pub max_length: Option, - // ... snip - } - - impl Config { - pub fn inlay_hints(&self) -> InlayHintsConfig { - InlayHintsConfig { - // ... snip - param_names_for_lifetime_elision_hints: self - .inlayHints_lifetimeElisionHints_useParameterNames() - .to_owned(), - max_length: self.inlayHints_maxLength().to_owned(), - // ... snip - } - } - } - "#, - "static_index.rs": r#" -// ... snip - fn add_file(&mut self, file_id: FileId) { - let current_crate = crates_for(self.db, file_id).pop().map(Into::into); - let folds = self.analysis.folding_ranges(file_id).unwrap(); - let inlay_hints = self - .analysis - .inlay_hints( - &InlayHintsConfig { - // ... snip - closure_style: hir::ClosureStyle::ImplFn, - param_names_for_lifetime_elision_hints: false, - binding_mode_hints: false, - max_length: Some(25), - closure_capture_hints: false, - // ... snip - }, - file_id, - None, - ) - .unwrap(); - // ... snip - } -// ... snip - "# - } - }, - "rust-analyzer": { - "src": { - "cli": { - "analysis_stats.rs": r#" - // ... snip - for &file_id in &file_ids { - _ = analysis.inlay_hints( - &InlayHintsConfig { - // ... snip - implicit_drop_hints: true, - lifetime_elision_hints: ide::LifetimeElisionHints::Always, - param_names_for_lifetime_elision_hints: true, - hide_named_constructor_hints: false, - hide_closure_initialization_hints: false, - closure_style: hir::ClosureStyle::ImplFn, - max_length: Some(25), - closing_brace_hints_min_lines: Some(20), - fields_to_resolve: InlayFieldsToResolve::empty(), - range_exclusive_hints: true, - }, - file_id.into(), - None, - ); - } - // ... snip - "#, - }, - "config.rs": r#" - config_data! { - /// Configs that only make sense when they are set by a client. As such they can only be defined - /// by setting them using client's settings (e.g `settings.json` on VS Code). - client: struct ClientDefaultConfigData <- ClientConfigInput -> { - // ... snip - /// Maximum length for inlay hints. Set to null to have an unlimited length. - inlayHints_maxLength: Option = Some(25), - // ... snip - /// Whether to prefer using parameter names as the name for elided lifetime hints if possible. - inlayHints_lifetimeElisionHints_useParameterNames: bool = false, - // ... snip - } - } - - impl Config { - // ... snip - pub fn inlay_hints(&self) -> InlayHintsConfig { - InlayHintsConfig { - // ... snip - param_names_for_lifetime_elision_hints: self - .inlayHints_lifetimeElisionHints_useParameterNames() - .to_owned(), - max_length: self.inlayHints_maxLength().to_owned(), - // ... snip - } - } - // ... snip - } - "# - } - } - } - }), - ) - .await; - } - - fn snapshot(outline_panel: &OutlinePanel, cx: &App) -> MultiBufferSnapshot { - outline_panel - .active_editor() - .unwrap() - .read(cx) - .buffer() - .read(cx) - .snapshot(cx) - } - - fn selected_row_text(editor: &Entity, cx: &mut App) -> String { - editor.update(cx, |editor, cx| { - let selections = editor.selections.all::(&editor.display_snapshot(cx)); - assert_eq!(selections.len(), 1, "Active editor should have exactly one selection after any outline panel interactions"); - let selection = selections.first().unwrap(); - let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx); - let line_start = language::Point::new(selection.start.row, 0); - let line_end = multi_buffer_snapshot.clip_point(language::Point::new(selection.end.row, u32::MAX), language::Bias::Right); - multi_buffer_snapshot.text_for_range(line_start..line_end).collect::().trim().to_owned() - }) - } - - #[gpui::test] - async fn test_outline_keyboard_expand_collapse(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/test", - json!({ - "src": { - "lib.rs": indoc!(" - mod outer { - pub struct OuterStruct { - field: String, - } - impl OuterStruct { - pub fn new() -> Self { - Self { field: String::new() } - } - pub fn method(&self) { - println!(\"{}\", self.field); - } - } - mod inner { - pub fn inner_function() { - let x = 42; - println!(\"{}\", x); - } - pub struct InnerStruct { - value: i32, - } - } - } - fn main() { - let s = outer::OuterStruct::new(); - s.method(); - } - "), - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - project.read_with(cx, |project, _| project.languages().add(rust_lang())); - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let outline_panel = outline_panel(&workspace, cx); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.set_active(true, window, cx) - }); - - workspace - .update(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from("/test/src/lib.rs"), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .unwrap() - .await - .unwrap(); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500)); - cx.run_until_parked(); - - // Force another update cycle to ensure outlines are fetched - outline_panel.update_in(cx, |panel, window, cx| { - panel.update_non_fs_items(window, cx); - panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: mod outer <==== selected - outline: pub struct OuterStruct - outline: field - outline: impl OuterStruct - outline: pub fn new - outline: pub fn method - outline: mod inner - outline: pub fn inner_function - outline: pub struct InnerStruct - outline: value -outline: fn main" - ) - ); - }); - - let parent_outline = outline_panel - .read_with(cx, |panel, _cx| { - panel - .cached_entries - .iter() - .find_map(|entry| match &entry.entry { - PanelEntry::Outline(OutlineEntry::Outline(outline)) - if panel - .outline_children_cache - .get(&outline.buffer_id) - .and_then(|children_map| { - let key = - (outline.outline.range.clone(), outline.outline.depth); - children_map.get(&key) - }) - .copied() - .unwrap_or(false) => - { - Some(entry.entry.clone()) - } - _ => None, - }) - }) - .expect("Should find an outline with children"); - - outline_panel.update_in(cx, |panel, window, cx| { - panel.select_entry(parent_outline.clone(), true, window, cx); - panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: mod outer <==== selected -outline: fn main" - ) - ); - }); - - outline_panel.update_in(cx, |panel, window, cx| { - panel.expand_selected_entry(&ExpandSelectedEntry, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: mod outer <==== selected - outline: pub struct OuterStruct - outline: field - outline: impl OuterStruct - outline: pub fn new - outline: pub fn method - outline: mod inner - outline: pub fn inner_function - outline: pub struct InnerStruct - outline: value -outline: fn main" - ) - ); - }); - - outline_panel.update_in(cx, |panel, window, cx| { - panel.collapsed_entries.clear(); - panel.update_cached_entries(None, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - outline_panel.update_in(cx, |panel, window, cx| { - let outlines_with_children: Vec<_> = panel - .cached_entries - .iter() - .filter_map(|entry| match &entry.entry { - PanelEntry::Outline(OutlineEntry::Outline(outline)) - if panel - .outline_children_cache - .get(&outline.buffer_id) - .and_then(|children_map| { - let key = (outline.outline.range.clone(), outline.outline.depth); - children_map.get(&key) - }) - .copied() - .unwrap_or(false) => - { - Some(entry.entry.clone()) - } - _ => None, - }) - .collect(); - - for outline in outlines_with_children { - panel.select_entry(outline, false, window, cx); - panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx); - } - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: mod outer -outline: fn main" - ) - ); - }); - - let collapsed_entries_count = - outline_panel.read_with(cx, |panel, _| panel.collapsed_entries.len()); - assert!( - collapsed_entries_count > 0, - "Should have collapsed entries tracked" - ); - } - - #[gpui::test] - async fn test_outline_click_toggle_behavior(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/test", - json!({ - "src": { - "main.rs": indoc!(" - struct Config { - name: String, - value: i32, - } - impl Config { - fn new(name: String) -> Self { - Self { name, value: 0 } - } - fn get_value(&self) -> i32 { - self.value - } - } - enum Status { - Active, - Inactive, - } - fn process_config(config: Config) -> Status { - if config.get_value() > 0 { - Status::Active - } else { - Status::Inactive - } - } - fn main() { - let config = Config::new(\"test\".to_string()); - let status = process_config(config); - } - "), - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - project.read_with(cx, |project, _| project.languages().add(rust_lang())); - - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let outline_panel = outline_panel(&workspace, cx); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.set_active(true, window, cx) - }); - - let _editor = workspace - .update(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from("/test/src/main.rs"), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .unwrap() - .await - .unwrap(); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, _cx| { - outline_panel.selected_entry = SelectedEntry::None; - }); - - // Check initial state - all entries should be expanded by default - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct Config - outline: name - outline: value -outline: impl Config - outline: fn new - outline: fn get_value -outline: enum Status - outline: Active - outline: Inactive -outline: fn process_config -outline: fn main" - ) - ); - }); - - outline_panel.update(cx, |outline_panel, _cx| { - outline_panel.selected_entry = SelectedEntry::None; - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_first(&SelectFirst, window, cx); - }); - }); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct Config <==== selected - outline: name - outline: value -outline: impl Config - outline: fn new - outline: fn get_value -outline: enum Status - outline: Active - outline: Inactive -outline: fn process_config -outline: fn main" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx); - }); - }); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct Config <==== selected -outline: impl Config - outline: fn new - outline: fn get_value -outline: enum Status - outline: Active - outline: Inactive -outline: fn process_config -outline: fn main" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx); - }); - }); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct Config <==== selected - outline: name - outline: value -outline: impl Config - outline: fn new - outline: fn get_value -outline: enum Status - outline: Active - outline: Inactive -outline: fn process_config -outline: fn main" - ) - ); - }); - } - - #[gpui::test] - async fn test_outline_expand_collapse_all(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/test", - json!({ - "src": { - "lib.rs": indoc!(" - mod outer { - pub struct OuterStruct { - field: String, - } - impl OuterStruct { - pub fn new() -> Self { - Self { field: String::new() } - } - pub fn method(&self) { - println!(\"{}\", self.field); - } - } - mod inner { - pub fn inner_function() { - let x = 42; - println!(\"{}\", x); - } - pub struct InnerStruct { - value: i32, - } - } - } - fn main() { - let s = outer::OuterStruct::new(); - s.method(); - } - "), - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - project.read_with(cx, |project, _| project.languages().add(rust_lang())); - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let outline_panel = outline_panel(&workspace, cx); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.set_active(true, window, cx) - }); - - workspace - .update(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from("/test/src/lib.rs"), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .unwrap() - .await - .unwrap(); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500)); - cx.run_until_parked(); - - // Force another update cycle to ensure outlines are fetched - outline_panel.update_in(cx, |panel, window, cx| { - panel.update_non_fs_items(window, cx); - panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: mod outer <==== selected - outline: pub struct OuterStruct - outline: field - outline: impl OuterStruct - outline: pub fn new - outline: pub fn method - outline: mod inner - outline: pub fn inner_function - outline: pub struct InnerStruct - outline: value -outline: fn main" - ) - ); - }); - - let _parent_outline = outline_panel - .read_with(cx, |panel, _cx| { - panel - .cached_entries - .iter() - .find_map(|entry| match &entry.entry { - PanelEntry::Outline(OutlineEntry::Outline(outline)) - if panel - .outline_children_cache - .get(&outline.buffer_id) - .and_then(|children_map| { - let key = - (outline.outline.range.clone(), outline.outline.depth); - children_map.get(&key) - }) - .copied() - .unwrap_or(false) => - { - Some(entry.entry.clone()) - } - _ => None, - }) - }) - .expect("Should find an outline with children"); - - // Collapse all entries - outline_panel.update_in(cx, |panel, window, cx| { - panel.collapse_all_entries(&CollapseAllEntries, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - let expected_collapsed_output = indoc!( - " - outline: mod outer <==== selected - outline: fn main" - ); - - outline_panel.update(cx, |panel, cx| { - assert_eq! { - display_entries( - &project, - &snapshot(panel, cx), - &panel.cached_entries, - panel.selected_entry(), - cx, - ), - expected_collapsed_output - }; - }); - - // Expand all entries - outline_panel.update_in(cx, |panel, window, cx| { - panel.expand_all_entries(&ExpandAllEntries, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - let expected_expanded_output = indoc!( - " - outline: mod outer <==== selected - outline: pub struct OuterStruct - outline: field - outline: impl OuterStruct - outline: pub fn new - outline: pub fn method - outline: mod inner - outline: pub fn inner_function - outline: pub struct InnerStruct - outline: value - outline: fn main" - ); - - outline_panel.update(cx, |panel, cx| { - assert_eq! { - display_entries( - &project, - &snapshot(panel, cx), - &panel.cached_entries, - panel.selected_entry(), - cx, - ), - expected_expanded_output - }; - }); - } - - #[gpui::test] - async fn test_buffer_search(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/test", - json!({ - "foo.txt": r#"<_constitution> - - - - - -## 📊 Output - -| Field | Meaning | -"# - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - let editor = workspace - .update(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from("/test/foo.txt"), - OpenOptions { - visible: Some(OpenVisible::All), - ..OpenOptions::default() - }, - window, - cx, - ) - }) - .unwrap() - .await - .unwrap() - .downcast::() - .unwrap(); - - let search_bar = workspace - .update(cx, |_, window, cx| { - cx.new(|cx| { - let mut search_bar = BufferSearchBar::new(None, window, cx); - search_bar.set_active_pane_item(Some(&editor), window, cx); - search_bar.show(window, cx); - search_bar - }) - }) - .unwrap(); - - let outline_panel = outline_panel(&workspace, cx); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.set_active(true, window, cx) - }); - - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search(" ", None, true, window, cx) - }) - .await - .unwrap(); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - "search: | Field« » | Meaning | <==== selected -search: | Field « » | Meaning | -search: | Field « » | Meaning | -search: | Field « » | Meaning | -search: | Field « »| Meaning | -search: | Field | Meaning« » | -search: | Field | Meaning « » | -search: | Field | Meaning « » | -search: | Field | Meaning « » | -search: | Field | Meaning « » | -search: | Field | Meaning « » | -search: | Field | Meaning « » | -search: | Field | Meaning « »|" - ); - }); - } -} diff --git a/crates/outline_panel/src/outline_panel_settings.rs b/crates/outline_panel/src/outline_panel_settings.rs deleted file mode 100644 index b2b1a6fe68..0000000000 --- a/crates/outline_panel/src/outline_panel_settings.rs +++ /dev/null @@ -1,66 +0,0 @@ -use editor::EditorSettings; -use gpui::{App, Pixels}; -use settings::RegisterSetting; -pub use settings::{DockSide, Settings, ShowIndentGuides}; -use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar}; - -#[derive(Debug, Clone, Copy, PartialEq, RegisterSetting)] -pub struct OutlinePanelSettings { - pub button: bool, - pub default_width: Pixels, - pub dock: DockSide, - pub file_icons: bool, - pub folder_icons: bool, - pub git_status: bool, - pub indent_size: f32, - pub indent_guides: IndentGuidesSettings, - pub auto_reveal_entries: bool, - pub auto_fold_dirs: bool, - pub scrollbar: ScrollbarSettings, - pub expand_outlines_with_depth: usize, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct ScrollbarSettings { - /// When to show the scrollbar in the project panel. - /// - /// Default: inherits editor scrollbar settings - pub show: Option, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct IndentGuidesSettings { - pub show: ShowIndentGuides, -} - -impl ScrollbarVisibility for OutlinePanelSettings { - fn visibility(&self, cx: &App) -> ShowScrollbar { - self.scrollbar - .show - .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show) - } -} - -impl Settings for OutlinePanelSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let panel = content.outline_panel.as_ref().unwrap(); - Self { - button: panel.button.unwrap(), - default_width: panel.default_width.map(gpui::px).unwrap(), - dock: panel.dock.unwrap(), - file_icons: panel.file_icons.unwrap(), - folder_icons: panel.folder_icons.unwrap(), - git_status: panel.git_status.unwrap(), - indent_size: panel.indent_size.unwrap(), - indent_guides: IndentGuidesSettings { - show: panel.indent_guides.unwrap().show.unwrap(), - }, - auto_reveal_entries: panel.auto_reveal_entries.unwrap(), - auto_fold_dirs: panel.auto_fold_dirs.unwrap(), - scrollbar: ScrollbarSettings { - show: panel.scrollbar.unwrap().show.map(Into::into), - }, - expand_outlines_with_depth: panel.expand_outlines_with_depth.unwrap(), - } - } -} diff --git a/crates/panel/Cargo.toml b/crates/panel/Cargo.toml deleted file mode 100644 index 3c51e6d6dc..0000000000 --- a/crates/panel/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "panel" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/panel.rs" - -[dependencies] -editor.workspace = true -gpui.workspace = true -settings.workspace = true -theme.workspace = true -ui.workspace = true -workspace.workspace = true diff --git a/crates/panel/LICENSE-GPL b/crates/panel/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/panel/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/panel/src/panel.rs b/crates/panel/src/panel.rs deleted file mode 100644 index 1930f654e9..0000000000 --- a/crates/panel/src/panel.rs +++ /dev/null @@ -1,136 +0,0 @@ -//! # panel -use editor::{Editor, EditorElement, EditorStyle}; -use gpui::{Entity, TextStyle, actions}; -use settings::Settings; -use theme::ThemeSettings; -use ui::{Tab, prelude::*}; - -actions!( - panel, - [ - /// Navigates to the next tab in the panel. - NextPanelTab, - /// Navigates to the previous tab in the panel. - PreviousPanelTab - ] -); - -pub trait PanelHeader: workspace::Panel { - fn header_height(&self, cx: &mut App) -> Pixels { - Tab::container_height(cx) - } - - fn panel_header_container(&self, _window: &mut Window, cx: &mut App) -> Div { - h_flex() - .h(self.header_height(cx)) - .w_full() - .px_1() - .flex_none() - } -} - -/// Implement this trait to enable a panel to have tabs. -pub trait PanelTabs: PanelHeader { - /// Returns the index of the currently selected tab. - fn selected_tab(&self, cx: &mut App) -> usize; - /// Selects the tab at the given index. - fn select_tab(&self, cx: &mut App, index: usize); - /// Moves to the next tab. - fn next_tab(&self, _: NextPanelTab, cx: &mut App) -> Self; - /// Moves to the previous tab. - fn previous_tab(&self, _: PreviousPanelTab, cx: &mut App) -> Self; -} - -#[derive(IntoElement)] -pub struct PanelTab {} - -impl RenderOnce for PanelTab { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - div() - } -} - -pub fn panel_button(label: impl Into) -> ui::Button { - let label = label.into(); - let id = ElementId::Name(label.to_lowercase().replace(' ', "_").into()); - ui::Button::new(id, label) - .label_size(ui::LabelSize::Small) - .icon_size(ui::IconSize::Small) - // TODO: Change this once we use on_surface_bg in button_like - .layer(ui::ElevationIndex::ModalSurface) - .size(ui::ButtonSize::Compact) -} - -pub fn panel_filled_button(label: impl Into) -> ui::Button { - panel_button(label).style(ui::ButtonStyle::Filled) -} - -pub fn panel_icon_button(id: impl Into, icon: IconName) -> ui::IconButton { - let id = ElementId::Name(id.into()); - - IconButton::new(id, icon) - // TODO: Change this once we use on_surface_bg in button_like - .layer(ui::ElevationIndex::ModalSurface) -} - -pub fn panel_filled_icon_button(id: impl Into, icon: IconName) -> ui::IconButton { - panel_icon_button(id, icon).style(ui::ButtonStyle::Filled) -} - -pub fn panel_editor_container(_window: &mut Window, cx: &mut App) -> Div { - v_flex() - .size_full() - .gap(px(8.)) - .p_2() - .bg(cx.theme().colors().editor_background) -} - -pub fn panel_editor_style(monospace: bool, window: &Window, cx: &App) -> EditorStyle { - let settings = ThemeSettings::get_global(cx); - - let font_size = TextSize::Small.rems(cx).to_pixels(window.rem_size()); - - let (font_family, font_fallbacks, font_features, font_weight, line_height) = if monospace { - ( - settings.buffer_font.family.clone(), - settings.buffer_font.fallbacks.clone(), - settings.buffer_font.features.clone(), - settings.buffer_font.weight, - font_size * settings.buffer_line_height.value(), - ) - } else { - ( - settings.ui_font.family.clone(), - settings.ui_font.fallbacks.clone(), - settings.ui_font.features.clone(), - settings.ui_font.weight, - window.line_height(), - ) - }; - - EditorStyle { - background: cx.theme().colors().editor_background, - local_player: cx.theme().players().local(), - text: TextStyle { - color: cx.theme().colors().text, - font_family, - font_fallbacks, - font_features, - font_size: TextSize::Small.rems(cx).into(), - font_weight, - line_height: line_height.into(), - ..Default::default() - }, - syntax: cx.theme().syntax().clone(), - ..Default::default() - } -} - -pub fn panel_editor_element( - editor: &Entity, - monospace: bool, - window: &mut Window, - cx: &mut App, -) -> EditorElement { - EditorElement::new(editor, panel_editor_style(monospace, window, cx)) -} diff --git a/crates/paths/Cargo.toml b/crates/paths/Cargo.toml deleted file mode 100644 index 24da7d46e9..0000000000 --- a/crates/paths/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "paths" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[features] -test-support = [] - -[lib] -path = "src/paths.rs" - -[dependencies] -dirs.workspace = true -ignore.workspace = true -util.workspace = true diff --git a/crates/paths/LICENSE-GPL b/crates/paths/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/paths/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs deleted file mode 100644 index a6aa8354b4..0000000000 --- a/crates/paths/src/paths.rs +++ /dev/null @@ -1,566 +0,0 @@ -//! Paths to locations used by Zed. - -use std::env; -use std::path::{Path, PathBuf}; -use std::sync::{LazyLock, OnceLock}; - -pub use util::paths::home_dir; -use util::rel_path::RelPath; - -/// A default editorconfig file name to use when resolving project settings. -pub const EDITORCONFIG_NAME: &str = ".editorconfig"; - -/// A custom data directory override, set only by `set_custom_data_dir`. -/// This is used to override the default data directory location. -/// The directory will be created if it doesn't exist when set. -static CUSTOM_DATA_DIR: OnceLock = OnceLock::new(); - -/// The resolved data directory, combining custom override or platform defaults. -/// This is set once and cached for subsequent calls. -/// On macOS, this is `~/Library/Application Support/Zed`. -/// On Linux/FreeBSD, this is `$XDG_DATA_HOME/zed`. -/// On Windows, this is `%LOCALAPPDATA%\Zed`. -static CURRENT_DATA_DIR: OnceLock = OnceLock::new(); - -/// The resolved config directory, combining custom override or platform defaults. -/// This is set once and cached for subsequent calls. -/// On macOS, this is `~/.config/zed`. -/// On Linux/FreeBSD, this is `$XDG_CONFIG_HOME/zed`. -/// On Windows, this is `%APPDATA%\Zed`. -static CONFIG_DIR: OnceLock = OnceLock::new(); - -/// Returns the relative path to the zed_server directory on the ssh host. -pub fn remote_server_dir_relative() -> &'static RelPath { - static CACHED: LazyLock<&'static RelPath> = - LazyLock::new(|| RelPath::unix(".zed_server").unwrap()); - *CACHED -} - -/// Returns the relative path to the zed_wsl_server directory on the wsl host. -pub fn remote_wsl_server_dir_relative() -> &'static RelPath { - static CACHED: LazyLock<&'static RelPath> = - LazyLock::new(|| RelPath::unix(".zed_wsl_server").unwrap()); - *CACHED -} - -/// Sets a custom directory for all user data, overriding the default data directory. -/// This function must be called before any other path operations that depend on the data directory. -/// The directory's path will be canonicalized to an absolute path by a blocking FS operation. -/// The directory will be created if it doesn't exist. -/// -/// # 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. -/// -/// # Returns -/// -/// A reference to the static `PathBuf` containing the custom data directory path. -/// -/// # Panics -/// -/// Panics if: -/// * Called after the data directory has been initialized (e.g., via `data_dir` or `config_dir`) -/// * The directory's path cannot be canonicalized to an absolute path -/// * The directory cannot be created -pub fn set_custom_data_dir(dir: &str) -> &'static PathBuf { - if CURRENT_DATA_DIR.get().is_some() || CONFIG_DIR.get().is_some() { - panic!("set_custom_data_dir called after data_dir or config_dir was initialized"); - } - CUSTOM_DATA_DIR.get_or_init(|| { - let mut path = PathBuf::from(dir); - if path.is_relative() && path.exists() { - let abs_path = path - .canonicalize() - .expect("failed to canonicalize custom data directory's path to an absolute path"); - path = util::paths::SanitizedPath::new(&abs_path).into() - } - std::fs::create_dir_all(&path).expect("failed to create custom data directory"); - path - }) -} - -/// Returns the path to the configuration directory used by Zed. -pub fn config_dir() -> &'static PathBuf { - CONFIG_DIR.get_or_init(|| { - if let Some(custom_dir) = CUSTOM_DATA_DIR.get() { - custom_dir.join("config") - } else if cfg!(target_os = "windows") { - dirs::config_dir() - .expect("failed to determine RoamingAppData directory") - .join("Zed") - } else if cfg!(any(target_os = "linux", target_os = "freebsd")) { - if let Ok(flatpak_xdg_config) = std::env::var("FLATPAK_XDG_CONFIG_HOME") { - flatpak_xdg_config.into() - } else { - dirs::config_dir().expect("failed to determine XDG_CONFIG_HOME directory") - } - .join("zed") - } else { - home_dir().join(".config").join("zed") - } - }) -} - -/// Returns the path to the data directory used by Zed. -pub fn data_dir() -> &'static PathBuf { - CURRENT_DATA_DIR.get_or_init(|| { - if let Some(custom_dir) = CUSTOM_DATA_DIR.get() { - custom_dir.clone() - } else if cfg!(target_os = "macos") { - home_dir().join("Library/Application Support/Zed") - } else if cfg!(any(target_os = "linux", target_os = "freebsd")) { - if let Ok(flatpak_xdg_data) = std::env::var("FLATPAK_XDG_DATA_HOME") { - flatpak_xdg_data.into() - } else { - dirs::data_local_dir().expect("failed to determine XDG_DATA_HOME directory") - } - .join("zed") - } else if cfg!(target_os = "windows") { - dirs::data_local_dir() - .expect("failed to determine LocalAppData directory") - .join("Zed") - } else { - config_dir().clone() // Fallback - } - }) -} - -/// Returns the path to the temp directory used by Zed. -pub fn temp_dir() -> &'static PathBuf { - static TEMP_DIR: OnceLock = OnceLock::new(); - TEMP_DIR.get_or_init(|| { - if cfg!(target_os = "macos") { - return dirs::cache_dir() - .expect("failed to determine cachesDirectory directory") - .join("Zed"); - } - - if cfg!(target_os = "windows") { - return dirs::cache_dir() - .expect("failed to determine LocalAppData directory") - .join("Zed"); - } - - if cfg!(any(target_os = "linux", target_os = "freebsd")) { - return if let Ok(flatpak_xdg_cache) = std::env::var("FLATPAK_XDG_CACHE_HOME") { - flatpak_xdg_cache.into() - } else { - dirs::cache_dir().expect("failed to determine XDG_CACHE_HOME directory") - } - .join("zed"); - } - - home_dir().join(".cache").join("zed") - }) -} - -/// Returns the path to the hang traces directory. -pub fn hang_traces_dir() -> &'static PathBuf { - static LOGS_DIR: OnceLock = OnceLock::new(); - LOGS_DIR.get_or_init(|| data_dir().join("hang_traces")) -} - -/// Returns the path to the logs directory. -pub fn logs_dir() -> &'static PathBuf { - static LOGS_DIR: OnceLock = OnceLock::new(); - LOGS_DIR.get_or_init(|| { - if cfg!(target_os = "macos") { - home_dir().join("Library/Logs/Zed") - } else { - data_dir().join("logs") - } - }) -} - -/// Returns the path to the Zed server directory on this SSH host. -pub fn remote_server_state_dir() -> &'static PathBuf { - static REMOTE_SERVER_STATE: OnceLock = OnceLock::new(); - REMOTE_SERVER_STATE.get_or_init(|| data_dir().join("server_state")) -} - -/// Returns the path to the `Zed.log` file. -pub fn log_file() -> &'static PathBuf { - static LOG_FILE: OnceLock = OnceLock::new(); - LOG_FILE.get_or_init(|| logs_dir().join("Zed.log")) -} - -/// Returns the path to the `Zed.log.old` file. -pub fn old_log_file() -> &'static PathBuf { - static OLD_LOG_FILE: OnceLock = OnceLock::new(); - OLD_LOG_FILE.get_or_init(|| logs_dir().join("Zed.log.old")) -} - -/// Returns the path to the database directory. -pub fn database_dir() -> &'static PathBuf { - static DATABASE_DIR: OnceLock = OnceLock::new(); - DATABASE_DIR.get_or_init(|| data_dir().join("db")) -} - -/// Returns the path to the crashes directory, if it exists for the current platform. -pub fn crashes_dir() -> &'static Option { - static CRASHES_DIR: OnceLock> = OnceLock::new(); - CRASHES_DIR.get_or_init(|| { - cfg!(target_os = "macos").then_some(home_dir().join("Library/Logs/DiagnosticReports")) - }) -} - -/// Returns the path to the retired crashes directory, if it exists for the current platform. -pub fn crashes_retired_dir() -> &'static Option { - static CRASHES_RETIRED_DIR: OnceLock> = OnceLock::new(); - CRASHES_RETIRED_DIR.get_or_init(|| crashes_dir().as_ref().map(|dir| dir.join("Retired"))) -} - -/// Returns the path to the `settings.json` file. -pub fn settings_file() -> &'static PathBuf { - static SETTINGS_FILE: OnceLock = OnceLock::new(); - SETTINGS_FILE.get_or_init(|| config_dir().join("settings.json")) -} - -/// Returns the path to the global settings file. -pub fn global_settings_file() -> &'static PathBuf { - static GLOBAL_SETTINGS_FILE: OnceLock = OnceLock::new(); - GLOBAL_SETTINGS_FILE.get_or_init(|| config_dir().join("global_settings.json")) -} - -/// Returns the path to the `settings_backup.json` file. -pub fn settings_backup_file() -> &'static PathBuf { - static SETTINGS_FILE: OnceLock = OnceLock::new(); - SETTINGS_FILE.get_or_init(|| config_dir().join("settings_backup.json")) -} - -/// Returns the path to the `keymap.json` file. -pub fn keymap_file() -> &'static PathBuf { - static KEYMAP_FILE: OnceLock = OnceLock::new(); - KEYMAP_FILE.get_or_init(|| config_dir().join("keymap.json")) -} - -/// Returns the path to the `keymap_backup.json` file. -pub fn keymap_backup_file() -> &'static PathBuf { - static KEYMAP_FILE: OnceLock = OnceLock::new(); - KEYMAP_FILE.get_or_init(|| config_dir().join("keymap_backup.json")) -} - -/// Returns the path to the `tasks.json` file. -pub fn tasks_file() -> &'static PathBuf { - static TASKS_FILE: OnceLock = OnceLock::new(); - TASKS_FILE.get_or_init(|| config_dir().join("tasks.json")) -} - -/// Returns the path to the `debug.json` file. -pub fn debug_scenarios_file() -> &'static PathBuf { - static DEBUG_SCENARIOS_FILE: OnceLock = OnceLock::new(); - DEBUG_SCENARIOS_FILE.get_or_init(|| config_dir().join("debug.json")) -} - -/// Returns the path to the extensions directory. -/// -/// This is where installed extensions are stored. -pub fn extensions_dir() -> &'static PathBuf { - static EXTENSIONS_DIR: OnceLock = OnceLock::new(); - EXTENSIONS_DIR.get_or_init(|| data_dir().join("extensions")) -} - -/// Returns the path to the extensions directory. -/// -/// This is where installed extensions are stored on a remote. -pub fn remote_extensions_dir() -> &'static PathBuf { - static EXTENSIONS_DIR: OnceLock = OnceLock::new(); - EXTENSIONS_DIR.get_or_init(|| data_dir().join("remote_extensions")) -} - -/// Returns the path to the extensions directory. -/// -/// This is where installed extensions are stored on a remote. -pub fn remote_extensions_uploads_dir() -> &'static PathBuf { - static UPLOAD_DIR: OnceLock = OnceLock::new(); - UPLOAD_DIR.get_or_init(|| remote_extensions_dir().join("uploads")) -} - -/// Returns the path to the themes directory. -/// -/// This is where themes that are not provided by extensions are stored. -pub fn themes_dir() -> &'static PathBuf { - static THEMES_DIR: OnceLock = OnceLock::new(); - THEMES_DIR.get_or_init(|| config_dir().join("themes")) -} - -/// Returns the path to the snippets directory. -pub fn snippets_dir() -> &'static PathBuf { - static SNIPPETS_DIR: OnceLock = OnceLock::new(); - SNIPPETS_DIR.get_or_init(|| config_dir().join("snippets")) -} - -/// Returns the path to the contexts directory. -/// -/// This is where the saved contexts from the Assistant are stored. -pub fn text_threads_dir() -> &'static PathBuf { - static CONTEXTS_DIR: OnceLock = OnceLock::new(); - CONTEXTS_DIR.get_or_init(|| { - if cfg!(target_os = "macos") { - config_dir().join("conversations") - } else { - data_dir().join("conversations") - } - }) -} - -/// Returns the path to the contexts directory. -/// -/// This is where the prompts for use with the Assistant are stored. -pub fn prompts_dir() -> &'static PathBuf { - static PROMPTS_DIR: OnceLock = OnceLock::new(); - PROMPTS_DIR.get_or_init(|| { - if cfg!(target_os = "macos") { - config_dir().join("prompts") - } else { - data_dir().join("prompts") - } - }) -} - -/// Returns the path to the prompt templates directory. -/// -/// This is where the prompt templates for core features can be overridden with templates. -/// -/// # Arguments -/// -/// * `dev_mode` - If true, assumes the current working directory is the Zed repository. -pub fn prompt_overrides_dir(repo_path: Option<&Path>) -> PathBuf { - if let Some(path) = repo_path { - let dev_path = path.join("assets").join("prompts"); - if dev_path.exists() { - return dev_path; - } - } - - static PROMPT_TEMPLATES_DIR: OnceLock = OnceLock::new(); - PROMPT_TEMPLATES_DIR - .get_or_init(|| { - if cfg!(target_os = "macos") { - config_dir().join("prompt_overrides") - } else { - data_dir().join("prompt_overrides") - } - }) - .clone() -} - -/// Returns the path to the semantic search's embeddings directory. -/// -/// This is where the embeddings used to power semantic search are stored. -pub fn embeddings_dir() -> &'static PathBuf { - static EMBEDDINGS_DIR: OnceLock = OnceLock::new(); - EMBEDDINGS_DIR.get_or_init(|| { - if cfg!(target_os = "macos") { - config_dir().join("embeddings") - } else { - data_dir().join("embeddings") - } - }) -} - -/// Returns the path to the languages directory. -/// -/// This is where language servers are downloaded to for languages built-in to Zed. -pub fn languages_dir() -> &'static PathBuf { - static LANGUAGES_DIR: OnceLock = OnceLock::new(); - LANGUAGES_DIR.get_or_init(|| data_dir().join("languages")) -} - -/// Returns the path to the debug adapters directory -/// -/// This is where debug adapters are downloaded to for DAPs that are built-in to Zed. -pub fn debug_adapters_dir() -> &'static PathBuf { - static DEBUG_ADAPTERS_DIR: OnceLock = OnceLock::new(); - DEBUG_ADAPTERS_DIR.get_or_init(|| data_dir().join("debug_adapters")) -} - -/// Returns the path to the external agents directory -/// -/// This is where agent servers are downloaded to -pub fn external_agents_dir() -> &'static PathBuf { - static EXTERNAL_AGENTS_DIR: OnceLock = OnceLock::new(); - EXTERNAL_AGENTS_DIR.get_or_init(|| data_dir().join("external_agents")) -} - -/// Returns the path to the Copilot directory. -pub fn copilot_dir() -> &'static PathBuf { - static COPILOT_DIR: OnceLock = OnceLock::new(); - COPILOT_DIR.get_or_init(|| data_dir().join("copilot")) -} - -/// Returns the path to the Supermaven directory. -pub fn supermaven_dir() -> &'static PathBuf { - static SUPERMAVEN_DIR: OnceLock = OnceLock::new(); - SUPERMAVEN_DIR.get_or_init(|| data_dir().join("supermaven")) -} - -/// Returns the path to the default Prettier directory. -pub fn default_prettier_dir() -> &'static PathBuf { - static DEFAULT_PRETTIER_DIR: OnceLock = OnceLock::new(); - DEFAULT_PRETTIER_DIR.get_or_init(|| data_dir().join("prettier")) -} - -/// Returns the path to the remote server binaries directory. -pub fn remote_servers_dir() -> &'static PathBuf { - static REMOTE_SERVERS_DIR: OnceLock = OnceLock::new(); - REMOTE_SERVERS_DIR.get_or_init(|| data_dir().join("remote_servers")) -} - -/// Returns the path to the directory where the devcontainer CLI is installed. -pub fn devcontainer_dir() -> &'static PathBuf { - static DEVCONTAINER_DIR: OnceLock = OnceLock::new(); - DEVCONTAINER_DIR.get_or_init(|| data_dir().join("devcontainer")) -} - -/// Returns the relative path to a `.zed` folder within a project. -pub fn local_settings_folder_name() -> &'static str { - ".zed" -} - -/// Returns the relative path to a `.vscode` folder within a project. -pub fn local_vscode_folder_name() -> &'static str { - ".vscode" -} - -/// Returns the relative path to a `settings.json` file within a project. -pub fn local_settings_file_relative_path() -> &'static RelPath { - static CACHED: LazyLock<&'static RelPath> = - LazyLock::new(|| RelPath::unix(".zed/settings.json").unwrap()); - *CACHED -} - -/// Returns the relative path to a `tasks.json` file within a project. -pub fn local_tasks_file_relative_path() -> &'static RelPath { - static CACHED: LazyLock<&'static RelPath> = - LazyLock::new(|| RelPath::unix(".zed/tasks.json").unwrap()); - *CACHED -} - -/// Returns the relative path to a `.vscode/tasks.json` file within a project. -pub fn local_vscode_tasks_file_relative_path() -> &'static RelPath { - static CACHED: LazyLock<&'static RelPath> = - LazyLock::new(|| RelPath::unix(".vscode/tasks.json").unwrap()); - *CACHED -} - -pub fn debug_task_file_name() -> &'static str { - "debug.json" -} - -pub fn task_file_name() -> &'static str { - "tasks.json" -} - -/// Returns the relative path to a `debug.json` file within a project. -/// .zed/debug.json -pub fn local_debug_file_relative_path() -> &'static RelPath { - static CACHED: LazyLock<&'static RelPath> = - LazyLock::new(|| RelPath::unix(".zed/debug.json").unwrap()); - *CACHED -} - -/// Returns the relative path to a `.vscode/launch.json` file within a project. -pub fn local_vscode_launch_file_relative_path() -> &'static RelPath { - static CACHED: LazyLock<&'static RelPath> = - LazyLock::new(|| RelPath::unix(".vscode/launch.json").unwrap()); - *CACHED -} - -pub fn user_ssh_config_file() -> PathBuf { - home_dir().join(".ssh/config") -} - -pub fn global_ssh_config_file() -> Option<&'static Path> { - if cfg!(windows) { - None - } else { - Some(Path::new("/etc/ssh/ssh_config")) - } -} - -/// Returns candidate paths for the vscode user settings file -pub fn vscode_settings_file_paths() -> Vec { - let mut paths = vscode_user_data_paths(); - for path in paths.iter_mut() { - path.push("User/settings.json"); - } - paths -} - -/// Returns candidate paths for the cursor user settings file -pub fn cursor_settings_file_paths() -> Vec { - let mut paths = cursor_user_data_paths(); - for path in paths.iter_mut() { - path.push("User/settings.json"); - } - paths -} - -fn vscode_user_data_paths() -> Vec { - // https://github.com/microsoft/vscode/blob/23e7148cdb6d8a27f0109ff77e5b1e019f8da051/src/vs/platform/environment/node/userDataPath.ts#L45 - const VSCODE_PRODUCT_NAMES: &[&str] = &[ - "Code", - "Code - OSS", - "VSCodium", - "Code Dev", - "Code - OSS Dev", - "code-oss-dev", - ]; - let mut paths = Vec::new(); - if let Ok(portable_path) = env::var("VSCODE_PORTABLE") { - paths.push(Path::new(&portable_path).join("user-data")); - } - if let Ok(vscode_appdata) = env::var("VSCODE_APPDATA") { - for product_name in VSCODE_PRODUCT_NAMES { - paths.push(Path::new(&vscode_appdata).join(product_name)); - } - } - for product_name in VSCODE_PRODUCT_NAMES { - add_vscode_user_data_paths(&mut paths, product_name); - } - paths -} - -fn cursor_user_data_paths() -> Vec { - let mut paths = Vec::new(); - add_vscode_user_data_paths(&mut paths, "Cursor"); - paths -} - -fn add_vscode_user_data_paths(paths: &mut Vec, product_name: &str) { - if cfg!(target_os = "macos") { - paths.push( - home_dir() - .join("Library/Application Support") - .join(product_name), - ); - } else if cfg!(target_os = "windows") { - if let Some(data_local_dir) = dirs::data_local_dir() { - paths.push(data_local_dir.join(product_name)); - } - if let Some(data_dir) = dirs::data_dir() { - paths.push(data_dir.join(product_name)); - } - } else { - paths.push( - dirs::config_dir() - .unwrap_or(home_dir().join(".config")) - .join(product_name), - ); - } -} - -#[cfg(any(test, feature = "test-support"))] -pub fn global_gitignore_path() -> Option { - Some(home_dir().join(".config").join("git").join("ignore")) -} - -#[cfg(not(any(test, feature = "test-support")))] -pub fn global_gitignore_path() -> Option { - static GLOBAL_GITIGNORE_PATH: OnceLock> = OnceLock::new(); - GLOBAL_GITIGNORE_PATH - .get_or_init(::ignore::gitignore::gitconfig_excludes_path) - .clone() -} diff --git a/crates/picker/Cargo.toml b/crates/picker/Cargo.toml deleted file mode 100644 index 1344d177f4..0000000000 --- a/crates/picker/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "picker" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/picker.rs" -doctest = false - -[features] -test-support = [] - -[dependencies] -anyhow.workspace = true -editor.workspace = true -gpui.workspace = true -menu.workspace = true -schemars.workspace = true -serde.workspace = true -theme.workspace = true -ui.workspace = true -workspace.workspace = true - -[dev-dependencies] -ctor.workspace = true -editor = { workspace = true, features = ["test-support"] } -env_logger.workspace = true -gpui = { workspace = true, features = ["test-support"] } -serde_json.workspace = true diff --git a/crates/picker/LICENSE-GPL b/crates/picker/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/picker/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/picker/src/head.rs b/crates/picker/src/head.rs deleted file mode 100644 index 700896e341..0000000000 --- a/crates/picker/src/head.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::sync::Arc; - -use editor::{Editor, EditorEvent}; -use gpui::{App, Entity, FocusHandle, Focusable, prelude::*}; -use ui::prelude::*; - -/// The head of a [`Picker`](crate::Picker). -pub(crate) enum Head { - /// Picker has an editor that allows the user to filter the list. - Editor(Entity), - - /// Picker has no head, it's just a list of items. - Empty(Entity), -} - -impl Head { - pub fn editor( - placeholder_text: Arc, - edit_handler: impl FnMut(&mut V, &Entity, &EditorEvent, &mut Window, &mut Context) - + 'static, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text(placeholder_text.as_ref(), window, cx); - editor - }); - cx.subscribe_in(&editor, window, edit_handler).detach(); - Self::Editor(editor) - } - - pub fn empty( - blur_handler: impl FnMut(&mut V, &mut Window, &mut Context) + 'static, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let head = cx.new(EmptyHead::new); - cx.on_blur(&head.focus_handle(cx), window, blur_handler) - .detach(); - Self::Empty(head) - } -} - -/// An invisible element that can hold focus. -pub(crate) struct EmptyHead { - focus_handle: FocusHandle, -} - -impl EmptyHead { - fn new(cx: &mut Context) -> Self { - Self { - focus_handle: cx.focus_handle(), - } - } -} - -impl Render for EmptyHead { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - div().track_focus(&self.focus_handle(cx)) - } -} - -impl Focusable for EmptyHead { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} diff --git a/crates/picker/src/highlighted_match_with_paths.rs b/crates/picker/src/highlighted_match_with_paths.rs deleted file mode 100644 index 7427104762..0000000000 --- a/crates/picker/src/highlighted_match_with_paths.rs +++ /dev/null @@ -1,116 +0,0 @@ -use ui::{HighlightedLabel, prelude::*}; - -#[derive(Clone)] -pub struct HighlightedMatchWithPaths { - pub prefix: Option, - pub match_label: HighlightedMatch, - pub paths: Vec, -} - -#[derive(Debug, Clone, IntoElement)] -pub struct HighlightedMatch { - pub text: String, - pub highlight_positions: Vec, - pub color: Color, -} - -impl HighlightedMatch { - pub fn join(components: impl Iterator, separator: &str) -> Self { - // Track a running byte offset and insert separators between parts. - let mut first = true; - let mut byte_offset = 0; - let mut text = String::new(); - let mut highlight_positions = Vec::new(); - for component in components { - if !first { - text.push_str(separator); - byte_offset += separator.len(); - } - first = false; - - highlight_positions.extend( - component - .highlight_positions - .iter() - .map(|position| position + byte_offset), - ); - text.push_str(&component.text); - byte_offset += component.text.len(); - } - - Self { - text, - highlight_positions, - color: Color::Default, - } - } - - pub fn color(self, color: Color) -> Self { - Self { color, ..self } - } -} -impl RenderOnce for HighlightedMatch { - fn render(self, _window: &mut Window, _: &mut App) -> impl IntoElement { - HighlightedLabel::new(self.text, self.highlight_positions).color(self.color) - } -} - -impl HighlightedMatchWithPaths { - pub fn render_paths_children(&mut self, element: Div) -> Div { - element.children(self.paths.clone().into_iter().map(|path| { - HighlightedLabel::new(path.text, path.highlight_positions) - .size(LabelSize::Small) - .color(Color::Muted) - })) - } -} - -impl RenderOnce for HighlightedMatchWithPaths { - fn render(mut self, _window: &mut Window, _: &mut App) -> impl IntoElement { - v_flex() - .child( - h_flex().gap_1().child(self.match_label.clone()).when_some( - self.prefix.as_ref(), - |this, prefix| { - this.child(Label::new(format!("({})", prefix)).color(Color::Muted)) - }, - ), - ) - .when(!self.paths.is_empty(), |this| { - self.render_paths_children(this) - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn join_offsets_positions_by_bytes_not_chars() { - // "αβγ" is 3 Unicode scalar values, 6 bytes in UTF-8. - let left_text = "αβγ".to_string(); - let right_text = "label".to_string(); - let left = HighlightedMatch { - text: left_text, - highlight_positions: vec![], - color: Color::Default, - }; - let right = HighlightedMatch { - text: right_text, - highlight_positions: vec![0, 1], - color: Color::Default, - }; - let joined = HighlightedMatch::join([left, right].into_iter(), ""); - - assert!( - joined - .highlight_positions - .iter() - .all(|&p| joined.text.is_char_boundary(p)), - "join produced non-boundary positions {:?} for text {:?}", - joined.highlight_positions, - joined.text - ); - } -} diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs deleted file mode 100644 index 3d6ae27dfa..0000000000 --- a/crates/picker/src/picker.rs +++ /dev/null @@ -1,976 +0,0 @@ -mod head; -pub mod highlighted_match_with_paths; -pub mod popover_menu; - -use anyhow::Result; -use editor::{ - Editor, SelectionEffects, - actions::{MoveDown, MoveUp}, - scroll::Autoscroll, -}; -use gpui::{ - Action, AnyElement, App, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle, - Focusable, Length, ListSizingBehavior, ListState, MouseButton, MouseUpEvent, Render, - ScrollStrategy, Task, UniformListScrollHandle, Window, actions, div, list, prelude::*, - uniform_list, -}; -use head::Head; -use schemars::JsonSchema; -use serde::Deserialize; -use std::{ops::Range, sync::Arc, time::Duration}; -use theme::ThemeSettings; -use ui::{ - Color, Divider, DocumentationAside, DocumentationEdge, DocumentationSide, Label, ListItem, - ListItemSpacing, ScrollAxes, Scrollbars, WithScrollbar, prelude::*, utils::WithRemSize, v_flex, -}; -use workspace::{ModalView, item::Settings}; - -enum ElementContainer { - List(ListState), - UniformList(UniformListScrollHandle), -} - -pub enum Direction { - Up, - Down, -} - -actions!( - picker, - [ - /// Confirms the selected completion in the picker. - ConfirmCompletion - ] -); - -/// ConfirmInput is an alternative editor action which - instead of selecting active picker entry - treats pickers editor input literally, -/// performing some kind of action on it. -#[derive(Clone, PartialEq, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = picker)] -#[serde(deny_unknown_fields)] -pub struct ConfirmInput { - pub secondary: bool, -} - -struct PendingUpdateMatches { - delegate_update_matches: Option>, - _task: Task>, -} - -pub struct Picker { - pub delegate: D, - element_container: ElementContainer, - head: Head, - pending_update_matches: Option, - confirm_on_update: Option, - width: Option, - widest_item: Option, - max_height: Option, - /// An external control to display a scrollbar in the `Picker`. - show_scrollbar: bool, - /// Whether the `Picker` is rendered as a self-contained modal. - /// - /// Set this to `false` when rendering the `Picker` as part of a larger modal. - is_modal: bool, -} - -#[derive(Debug, Default, Clone, Copy, PartialEq)] -pub enum PickerEditorPosition { - #[default] - /// Render the editor at the start of the picker. Usually the top - Start, - /// Render the editor at the end of the picker. Usually the bottom - End, -} - -pub trait PickerDelegate: Sized + 'static { - type ListItem: IntoElement; - - fn match_count(&self) -> usize; - fn selected_index(&self) -> usize; - fn separators_after_indices(&self) -> Vec { - Vec::new() - } - fn set_selected_index( - &mut self, - ix: usize, - window: &mut Window, - cx: &mut Context>, - ); - - /// Called before the picker handles `SelectPrevious` or `SelectNext`. Return `Some(query)` to - /// set a new query and prevent the default selection behavior. - fn select_history( - &mut self, - _direction: Direction, - _query: &str, - _window: &mut Window, - _cx: &mut App, - ) -> Option { - None - } - fn can_select( - &mut self, - _ix: usize, - _window: &mut Window, - _cx: &mut Context>, - ) -> bool { - true - } - - // Allows binding some optional effect to when the selection changes. - fn selected_index_changed( - &self, - _ix: usize, - _window: &mut Window, - _cx: &mut Context>, - ) -> Option> { - None - } - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc; - fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option { - Some("No matches".into()) - } - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()>; - - // Delegates that support this method (e.g. the CommandPalette) can chose to block on any background - // work for up to `duration` to try and get a result synchronously. - // This avoids a flash of an empty command-palette on cmd-shift-p, and lets workspace::SendKeystrokes - // mostly work when dismissing a palette. - fn finalize_update_matches( - &mut self, - _query: String, - _duration: Duration, - _window: &mut Window, - _cx: &mut Context>, - ) -> bool { - false - } - - /// Override if you want to have update the query instead of confirming. - fn confirm_update_query( - &mut self, - _window: &mut Window, - _cx: &mut Context>, - ) -> Option { - None - } - fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>); - /// Instead of interacting with currently selected entry, treats editor input literally, - /// performing some kind of action on it. - fn confirm_input( - &mut self, - _secondary: bool, - _window: &mut Window, - _: &mut Context>, - ) { - } - fn dismissed(&mut self, window: &mut Window, cx: &mut Context>); - fn should_dismiss(&self) -> bool { - true - } - fn confirm_completion( - &mut self, - _query: String, - _window: &mut Window, - _: &mut Context>, - ) -> Option { - None - } - - fn editor_position(&self) -> PickerEditorPosition { - PickerEditorPosition::default() - } - - fn render_editor( - &self, - editor: &Entity, - _window: &mut Window, - _cx: &mut Context>, - ) -> Div { - v_flex() - .when( - self.editor_position() == PickerEditorPosition::End, - |this| this.child(Divider::horizontal()), - ) - .child( - h_flex() - .overflow_hidden() - .flex_none() - .h_9() - .px_2p5() - .child(editor.clone()), - ) - .when( - self.editor_position() == PickerEditorPosition::Start, - |this| this.child(Divider::horizontal()), - ) - } - - fn render_match( - &self, - ix: usize, - selected: bool, - window: &mut Window, - cx: &mut Context>, - ) -> Option; - - fn render_header( - &self, - _window: &mut Window, - _: &mut Context>, - ) -> Option { - None - } - - fn render_footer( - &self, - _window: &mut Window, - _: &mut Context>, - ) -> Option { - None - } - - fn documentation_aside( - &self, - _window: &mut Window, - _cx: &mut Context>, - ) -> Option { - None - } -} - -impl Focusable for Picker { - fn focus_handle(&self, cx: &App) -> FocusHandle { - match &self.head { - Head::Editor(editor) => editor.focus_handle(cx), - Head::Empty(head) => head.focus_handle(cx), - } - } -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -enum ContainerKind { - List, - UniformList, -} - -impl Picker { - /// A picker, which displays its matches using `gpui::uniform_list`, all matches should have the same height. - /// The picker allows the user to perform search items by text. - /// If `PickerDelegate::render_match` can return items with different heights, use `Picker::list`. - pub fn uniform_list(delegate: D, window: &mut Window, cx: &mut Context) -> Self { - let head = Head::editor( - delegate.placeholder_text(window, cx), - Self::on_input_editor_event, - window, - cx, - ); - - Self::new(delegate, ContainerKind::UniformList, head, window, cx) - } - - /// A picker, which displays its matches using `gpui::uniform_list`, all matches should have the same height. - /// If `PickerDelegate::render_match` can return items with different heights, use `Picker::list`. - pub fn nonsearchable_uniform_list( - delegate: D, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let head = Head::empty(Self::on_empty_head_blur, window, cx); - - Self::new(delegate, ContainerKind::UniformList, head, window, cx) - } - - /// A picker, which displays its matches using `gpui::list`, matches can have different heights. - /// The picker allows the user to perform search items by text. - /// If `PickerDelegate::render_match` only returns items with the same height, use `Picker::uniform_list` as its implementation is optimized for that. - pub fn nonsearchable_list(delegate: D, window: &mut Window, cx: &mut Context) -> Self { - let head = Head::empty(Self::on_empty_head_blur, window, cx); - - Self::new(delegate, ContainerKind::List, head, window, cx) - } - - /// A picker, which displays its matches using `gpui::list`, matches can have different heights. - /// The picker allows the user to perform search items by text. - /// If `PickerDelegate::render_match` only returns items with the same height, use `Picker::uniform_list` as its implementation is optimized for that. - pub fn list(delegate: D, window: &mut Window, cx: &mut Context) -> Self { - let head = Head::editor( - delegate.placeholder_text(window, cx), - Self::on_input_editor_event, - window, - cx, - ); - - Self::new(delegate, ContainerKind::List, head, window, cx) - } - - fn new( - delegate: D, - container: ContainerKind, - head: Head, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let element_container = Self::create_element_container(container); - let mut this = Self { - delegate, - head, - element_container, - pending_update_matches: None, - confirm_on_update: None, - width: None, - widest_item: None, - max_height: Some(rems(24.).into()), - show_scrollbar: false, - is_modal: true, - }; - this.update_matches("".to_string(), window, cx); - // give the delegate 4ms to render the first set of suggestions. - this.delegate - .finalize_update_matches("".to_string(), Duration::from_millis(4), window, cx); - this - } - - fn create_element_container(container: ContainerKind) -> ElementContainer { - match container { - ContainerKind::UniformList => { - ElementContainer::UniformList(UniformListScrollHandle::new()) - } - ContainerKind::List => { - ElementContainer::List(ListState::new(0, gpui::ListAlignment::Top, px(1000.))) - } - } - } - - pub fn width(mut self, width: impl Into) -> Self { - self.width = Some(width.into()); - self - } - - pub fn widest_item(mut self, ix: Option) -> Self { - self.widest_item = ix; - self - } - - pub fn max_height(mut self, max_height: Option) -> Self { - self.max_height = max_height; - self - } - - pub fn show_scrollbar(mut self, show_scrollbar: bool) -> Self { - self.show_scrollbar = show_scrollbar; - self - } - - pub fn modal(mut self, modal: bool) -> Self { - self.is_modal = modal; - self - } - - pub fn list_measure_all(mut self) -> Self { - match self.element_container { - ElementContainer::List(state) => { - self.element_container = ElementContainer::List(state.measure_all()); - } - _ => {} - } - self - } - - pub fn focus(&self, window: &mut Window, cx: &mut App) { - self.focus_handle(cx).focus(window); - } - - /// Handles the selecting an index, and passing the change to the delegate. - /// If `fallback_direction` is set to `None`, the index will not be selected - /// if the element at that index cannot be selected. - /// If `fallback_direction` is set to - /// `Some(..)`, the next selectable element will be selected in the - /// specified direction (Down or Up), cycling through all elements until - /// finding one that can be selected or returning if there are no selectable elements. - /// If `scroll_to_index` is true, the new selected index will be scrolled into - /// view. - /// - /// If some effect is bound to `selected_index_changed`, it will be executed. - pub fn set_selected_index( - &mut self, - mut ix: usize, - fallback_direction: Option, - scroll_to_index: bool, - window: &mut Window, - cx: &mut Context, - ) { - let match_count = self.delegate.match_count(); - if match_count == 0 { - return; - } - - if let Some(bias) = fallback_direction { - let mut curr_ix = ix; - while !self.delegate.can_select(curr_ix, window, cx) { - curr_ix = match bias { - Direction::Down => { - if curr_ix == match_count - 1 { - 0 - } else { - curr_ix + 1 - } - } - Direction::Up => { - if curr_ix == 0 { - match_count - 1 - } else { - curr_ix - 1 - } - } - }; - // There is no item that can be selected - if ix == curr_ix { - return; - } - } - ix = curr_ix; - } else if !self.delegate.can_select(ix, window, cx) { - return; - } - - let previous_index = self.delegate.selected_index(); - self.delegate.set_selected_index(ix, window, cx); - let current_index = self.delegate.selected_index(); - - if previous_index != current_index { - if let Some(action) = self.delegate.selected_index_changed(ix, window, cx) { - action(window, cx); - } - if scroll_to_index { - self.scroll_to_item_index(ix); - } - } - } - - pub fn select_next( - &mut self, - _: &menu::SelectNext, - window: &mut Window, - cx: &mut Context, - ) { - let query = self.query(cx); - if let Some(query) = self - .delegate - .select_history(Direction::Down, &query, window, cx) - { - self.set_query(query, window, cx); - return; - } - let count = self.delegate.match_count(); - if count > 0 { - let index = self.delegate.selected_index(); - let ix = if index == count - 1 { 0 } else { index + 1 }; - self.set_selected_index(ix, Some(Direction::Down), true, window, cx); - cx.notify(); - } - } - - pub fn editor_move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context) { - self.select_previous(&Default::default(), window, cx); - } - - fn select_previous( - &mut self, - _: &menu::SelectPrevious, - window: &mut Window, - cx: &mut Context, - ) { - let query = self.query(cx); - if let Some(query) = self - .delegate - .select_history(Direction::Up, &query, window, cx) - { - self.set_query(query, window, cx); - return; - } - let count = self.delegate.match_count(); - if count > 0 { - let index = self.delegate.selected_index(); - let ix = if index == 0 { count - 1 } else { index - 1 }; - self.set_selected_index(ix, Some(Direction::Up), true, window, cx); - cx.notify(); - } - } - - pub fn editor_move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context) { - self.select_next(&Default::default(), window, cx); - } - - pub fn select_first( - &mut self, - _: &menu::SelectFirst, - window: &mut Window, - cx: &mut Context, - ) { - let count = self.delegate.match_count(); - if count > 0 { - self.set_selected_index(0, Some(Direction::Down), true, window, cx); - cx.notify(); - } - } - - fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context) { - let count = self.delegate.match_count(); - if count > 0 { - self.set_selected_index(count - 1, Some(Direction::Up), true, window, cx); - cx.notify(); - } - } - - pub fn cycle_selection(&mut self, window: &mut Window, cx: &mut Context) { - let count = self.delegate.match_count(); - let index = self.delegate.selected_index(); - let new_index = if index + 1 == count { 0 } else { index + 1 }; - self.set_selected_index(new_index, Some(Direction::Down), true, window, cx); - cx.notify(); - } - - pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context) { - if self.delegate.should_dismiss() { - self.delegate.dismissed(window, cx); - cx.emit(DismissEvent); - } - } - - fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { - if self.pending_update_matches.is_some() - && !self.delegate.finalize_update_matches( - self.query(cx), - Duration::from_millis(16), - window, - cx, - ) - { - self.confirm_on_update = Some(false) - } else { - self.pending_update_matches.take(); - self.do_confirm(false, window, cx); - } - } - - fn secondary_confirm( - &mut self, - _: &menu::SecondaryConfirm, - window: &mut Window, - cx: &mut Context, - ) { - if self.pending_update_matches.is_some() - && !self.delegate.finalize_update_matches( - self.query(cx), - Duration::from_millis(16), - window, - cx, - ) - { - self.confirm_on_update = Some(true) - } else { - self.do_confirm(true, window, cx); - } - } - - fn confirm_input(&mut self, input: &ConfirmInput, window: &mut Window, cx: &mut Context) { - self.delegate.confirm_input(input.secondary, window, cx); - } - - fn confirm_completion( - &mut self, - _: &ConfirmCompletion, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(new_query) = self.delegate.confirm_completion(self.query(cx), window, cx) { - self.set_query(new_query, window, cx); - } else { - cx.propagate() - } - } - - fn handle_click( - &mut self, - ix: usize, - secondary: bool, - window: &mut Window, - cx: &mut Context, - ) { - cx.stop_propagation(); - window.prevent_default(); - self.set_selected_index(ix, None, false, window, cx); - self.do_confirm(secondary, window, cx) - } - - fn do_confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context) { - if let Some(update_query) = self.delegate.confirm_update_query(window, cx) { - self.set_query(update_query, window, cx); - self.set_selected_index(0, Some(Direction::Down), false, window, cx); - } else { - self.delegate.confirm(secondary, window, cx) - } - } - - fn on_input_editor_event( - &mut self, - _: &Entity, - event: &editor::EditorEvent, - window: &mut Window, - cx: &mut Context, - ) { - let Head::Editor(editor) = &self.head else { - panic!("unexpected call"); - }; - match event { - editor::EditorEvent::BufferEdited => { - let query = editor.read(cx).text(cx); - self.update_matches(query, window, cx); - } - editor::EditorEvent::Blurred => { - if self.is_modal && window.is_window_active() { - self.cancel(&menu::Cancel, window, cx); - } - } - _ => {} - } - } - - fn on_empty_head_blur(&mut self, window: &mut Window, cx: &mut Context) { - let Head::Empty(_) = &self.head else { - panic!("unexpected call"); - }; - if window.is_window_active() { - self.cancel(&menu::Cancel, window, cx); - } - } - - pub fn refresh_placeholder(&mut self, window: &mut Window, cx: &mut App) { - match &self.head { - Head::Editor(editor) => { - let placeholder = self.delegate.placeholder_text(window, cx); - editor.update(cx, |editor, cx| { - editor.set_placeholder_text(placeholder.as_ref(), window, cx); - cx.notify(); - }); - } - Head::Empty(_) => {} - } - } - - pub fn refresh(&mut self, window: &mut Window, cx: &mut Context) { - let query = self.query(cx); - self.update_matches(query, window, cx); - } - - pub fn update_matches(&mut self, query: String, window: &mut Window, cx: &mut Context) { - let delegate_pending_update_matches = self.delegate.update_matches(query, window, cx); - - self.matches_updated(window, cx); - // This struct ensures that we can synchronously drop the task returned by the - // delegate's `update_matches` method and the task that the picker is spawning. - // If we simply capture the delegate's task into the picker's task, when the picker's - // task gets synchronously dropped, the delegate's task would keep running until - // the picker's task has a chance of being scheduled, because dropping a task happens - // asynchronously. - self.pending_update_matches = Some(PendingUpdateMatches { - delegate_update_matches: Some(delegate_pending_update_matches), - _task: cx.spawn_in(window, async move |this, cx| { - let delegate_pending_update_matches = this.update(cx, |this, _| { - this.pending_update_matches - .as_mut() - .unwrap() - .delegate_update_matches - .take() - .unwrap() - })?; - delegate_pending_update_matches.await; - this.update_in(cx, |this, window, cx| { - this.matches_updated(window, cx); - }) - }), - }); - } - - fn matches_updated(&mut self, window: &mut Window, cx: &mut Context) { - if let ElementContainer::List(state) = &mut self.element_container { - state.reset(self.delegate.match_count()); - } - - let index = self.delegate.selected_index(); - self.scroll_to_item_index(index); - self.pending_update_matches = None; - if let Some(secondary) = self.confirm_on_update.take() { - self.do_confirm(secondary, window, cx); - } - cx.notify(); - } - - pub fn query(&self, cx: &App) -> String { - match &self.head { - Head::Editor(editor) => editor.read(cx).text(cx), - Head::Empty(_) => "".to_string(), - } - } - - pub fn set_query(&self, query: impl Into>, window: &mut Window, cx: &mut App) { - if let Head::Editor(editor) = &self.head { - editor.update(cx, |editor, cx| { - editor.set_text(query, window, cx); - let editor_offset = editor.buffer().read(cx).len(cx); - editor.change_selections( - SelectionEffects::scroll(Autoscroll::Next), - window, - cx, - |s| s.select_ranges(Some(editor_offset..editor_offset)), - ); - }); - } - } - - fn scroll_to_item_index(&mut self, ix: usize) { - match &mut self.element_container { - ElementContainer::List(state) => state.scroll_to_reveal_item(ix), - ElementContainer::UniformList(scroll_handle) => { - scroll_handle.scroll_to_item(ix, ScrollStrategy::Nearest) - } - } - } - - fn render_element( - &self, - window: &mut Window, - cx: &mut Context, - ix: usize, - ) -> impl IntoElement + use { - div() - .id(("item", ix)) - .cursor_pointer() - .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| { - this.handle_click(ix, event.modifiers().secondary(), window, cx) - })) - // As of this writing, GPUI intercepts `ctrl-[mouse-event]`s on macOS - // and produces right mouse button events. This matches platforms norms - // but means that UIs which depend on holding ctrl down (such as the tab - // switcher) can't be clicked on. Hence, this handler. - .on_mouse_up( - MouseButton::Right, - cx.listener(move |this, event: &MouseUpEvent, window, cx| { - // We specifically want to use the platform key here, as - // ctrl will already be held down for the tab switcher. - this.handle_click(ix, event.modifiers.platform, window, cx) - }), - ) - .children(self.delegate.render_match( - ix, - ix == self.delegate.selected_index(), - window, - cx, - )) - .when( - self.delegate.separators_after_indices().contains(&ix), - |picker| { - picker - .border_color(cx.theme().colors().border_variant) - .border_b_1() - .py(px(-1.0)) - }, - ) - } - - fn render_element_container(&self, cx: &mut Context) -> impl IntoElement { - let sizing_behavior = if self.max_height.is_some() { - ListSizingBehavior::Infer - } else { - ListSizingBehavior::Auto - }; - - match &self.element_container { - ElementContainer::UniformList(scroll_handle) => uniform_list( - "candidates", - self.delegate.match_count(), - cx.processor(move |picker, visible_range: Range, window, cx| { - visible_range - .map(|ix| picker.render_element(window, cx, ix)) - .collect() - }), - ) - .with_sizing_behavior(sizing_behavior) - .when_some(self.widest_item, |el, widest_item| { - el.with_width_from_item(Some(widest_item)) - }) - .flex_grow() - .py_1() - .track_scroll(&scroll_handle) - .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(), - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn logical_scroll_top_index(&self) -> usize { - match &self.element_container { - ElementContainer::List(state) => state.logical_scroll_top().item_ix, - ElementContainer::UniformList(scroll_handle) => { - scroll_handle.logical_scroll_top_index() - } - } - } -} - -impl EventEmitter for Picker {} -impl ModalView for Picker {} - -impl Render for Picker { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx); - let window_size = window.viewport_size(); - let rem_size = window.rem_size(); - let is_wide_window = window_size.width / rem_size > rems_from_px(800.).0; - - let aside = self.delegate.documentation_aside(window, cx); - - let editor_position = self.delegate.editor_position(); - let menu = v_flex() - .key_context("Picker") - .size_full() - .when_some(self.width, |el, width| el.w(width)) - .overflow_hidden() - // This is a bit of a hack to remove the modal styling when we're rendering the `Picker` - // as a part of a modal rather than the entire modal. - // - // We should revisit how the `Picker` is styled to make it more composable. - .when(self.is_modal, |this| this.elevation_3(cx)) - .on_action(cx.listener(Self::select_next)) - .on_action(cx.listener(Self::select_previous)) - .on_action(cx.listener(Self::editor_move_down)) - .on_action(cx.listener(Self::editor_move_up)) - .on_action(cx.listener(Self::select_first)) - .on_action(cx.listener(Self::select_last)) - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(Self::confirm)) - .on_action(cx.listener(Self::secondary_confirm)) - .on_action(cx.listener(Self::confirm_completion)) - .on_action(cx.listener(Self::confirm_input)) - .children(match &self.head { - Head::Editor(editor) => { - if editor_position == PickerEditorPosition::Start { - Some(self.delegate.render_editor(&editor.clone(), window, cx)) - } else { - None - } - } - Head::Empty(empty_head) => Some(div().child(empty_head.clone())), - }) - .when(self.delegate.match_count() > 0, |el| { - el.child( - v_flex() - .id("element-container") - .relative() - .flex_grow() - .when_some(self.max_height, |div, max_h| div.max_h(max_h)) - .overflow_hidden() - .children(self.delegate.render_header(window, cx)) - .child(self.render_element_container(cx)) - .when(self.show_scrollbar, |this| { - let base_scrollbar_config = - Scrollbars::new(ScrollAxes::Vertical).width_sm(); - - this.map(|this| match &self.element_container { - ElementContainer::List(state) => this.custom_scrollbars( - base_scrollbar_config.tracked_scroll_handle(state), - window, - cx, - ), - ElementContainer::UniformList(state) => this.custom_scrollbars( - base_scrollbar_config.tracked_scroll_handle(state), - window, - cx, - ), - }) - }), - ) - }) - .when(self.delegate.match_count() == 0, |el| { - el.when_some(self.delegate.no_matches_text(window, cx), |el, text| { - el.child( - v_flex().flex_grow().py_2().child( - ListItem::new("empty_state") - .inset(true) - .spacing(ListItemSpacing::Sparse) - .disabled(true) - .child(Label::new(text).color(Color::Muted)), - ), - ) - }) - }) - .children(self.delegate.render_footer(window, cx)) - .children(match &self.head { - Head::Editor(editor) => { - if editor_position == PickerEditorPosition::End { - Some(self.delegate.render_editor(&editor.clone(), window, cx)) - } else { - None - } - } - Head::Empty(empty_head) => Some(div().child(empty_head.clone())), - }); - - let Some(aside) = aside else { - return menu; - }; - - let render_aside = |aside: DocumentationAside, cx: &mut Context| { - WithRemSize::new(ui_font_size) - .occlude() - .elevation_2(cx) - .w_full() - .p_2() - .overflow_hidden() - .when(is_wide_window, |this| this.max_w_96()) - .when(!is_wide_window, |this| this.max_w_48()) - .child((aside.render)(cx)) - }; - - if is_wide_window { - div().relative().child(menu).child( - h_flex() - .absolute() - .when(aside.side == DocumentationSide::Left, |this| { - this.right_full().mr_1() - }) - .when(aside.side == DocumentationSide::Right, |this| { - this.left_full().ml_1() - }) - .when(aside.edge == DocumentationEdge::Top, |this| this.top_0()) - .when(aside.edge == DocumentationEdge::Bottom, |this| { - this.bottom_0() - }) - .child(render_aside(aside, cx)), - ) - } else { - v_flex() - .w_full() - .gap_1() - .justify_end() - .child(render_aside(aside, cx)) - .child(menu) - } - } -} diff --git a/crates/picker/src/popover_menu.rs b/crates/picker/src/popover_menu.rs deleted file mode 100644 index 42eedb2492..0000000000 --- a/crates/picker/src/popover_menu.rs +++ /dev/null @@ -1,101 +0,0 @@ -use gpui::{ - AnyView, Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Point, - Subscription, -}; -use ui::{ - FluentBuilder as _, IntoElement, PopoverMenu, PopoverMenuHandle, PopoverTrigger, prelude::*, -}; - -use crate::{Picker, PickerDelegate}; - -pub struct PickerPopoverMenu -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, - P: PickerDelegate, -{ - picker: Entity>, - trigger: T, - tooltip: TT, - handle: Option>>, - anchor: Corner, - offset: Option>, - _subscriptions: Vec, -} - -impl PickerPopoverMenu -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, - P: PickerDelegate, -{ - pub fn new( - picker: Entity>, - trigger: T, - tooltip: TT, - anchor: Corner, - cx: &mut App, - ) -> Self { - Self { - _subscriptions: vec![cx.subscribe(&picker, |picker, &DismissEvent, cx| { - picker.update(cx, |_, cx| cx.emit(DismissEvent)); - })], - picker, - trigger, - tooltip, - handle: None, - offset: Some(Point { - x: px(0.0), - y: px(-2.0), - }), - anchor, - } - } - - pub fn with_handle(mut self, handle: PopoverMenuHandle>) -> Self { - self.handle = Some(handle); - self - } - - pub fn offset(mut self, offset: Point) -> Self { - self.offset = Some(offset); - self - } -} - -impl EventEmitter for PickerPopoverMenu -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, - P: PickerDelegate, -{ -} - -impl Focusable for PickerPopoverMenu -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, - P: PickerDelegate, -{ - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl RenderOnce for PickerPopoverMenu -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, - P: PickerDelegate, -{ - 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.offset, |menu, offset| menu.offset(offset)) - } -} diff --git a/crates/prettier/Cargo.toml b/crates/prettier/Cargo.toml deleted file mode 100644 index 9da1e4c8d6..0000000000 --- a/crates/prettier/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "prettier" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/prettier.rs" -doctest = false - -[features] -test-support = [] - -[dependencies] -anyhow.workspace = true -collections.workspace = true -fs.workspace = true -gpui.workspace = true -language.workspace = true -log.workspace = true -lsp.workspace = true -node_runtime.workspace = true -parking_lot.workspace = true -paths.workspace = true -serde.workspace = true -serde_json.workspace = true -util.workspace = true - -[dev-dependencies] -fs = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } diff --git a/crates/prettier/LICENSE-GPL b/crates/prettier/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/prettier/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs deleted file mode 100644 index bc4ce609a1..0000000000 --- a/crates/prettier/src/prettier.rs +++ /dev/null @@ -1,1191 +0,0 @@ -use anyhow::Context as _; -use collections::{HashMap, HashSet}; -use fs::Fs; -use gpui::{AsyncApp, Entity}; -use language::language_settings::PrettierSettings; -use language::{Buffer, Diff, Language, language_settings::language_settings}; -use lsp::{LanguageServer, LanguageServerId}; -use node_runtime::NodeRuntime; -use paths::default_prettier_dir; -use serde::{Deserialize, Serialize}; -use std::{ - ops::ControlFlow, - path::{Path, PathBuf}, - sync::Arc, -}; -use util::{ - paths::{PathMatcher, PathStyle}, - rel_path::RelPath, -}; - -#[derive(Debug, Clone)] -pub enum Prettier { - Real(RealPrettier), - #[cfg(any(test, feature = "test-support"))] - Test(TestPrettier), -} - -#[derive(Debug, Clone)] -pub struct RealPrettier { - default: bool, - prettier_dir: PathBuf, - server: Arc, -} - -#[cfg(any(test, feature = "test-support"))] -#[derive(Debug, Clone)] -pub struct TestPrettier { - prettier_dir: PathBuf, - default: bool, -} - -pub const FAIL_THRESHOLD: usize = 4; -pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js"; -pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js"); -const PRETTIER_PACKAGE_NAME: &str = "prettier"; -const TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME: &str = "prettier-plugin-tailwindcss"; - -#[cfg(any(test, feature = "test-support"))] -pub const FORMAT_SUFFIX: &str = "\nformatted by test prettier"; - -impl Prettier { - pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[ - ".prettierrc", - ".prettierrc.json", - ".prettierrc.json5", - ".prettierrc.yaml", - ".prettierrc.yml", - ".prettierrc.toml", - ".prettierrc.js", - ".prettierrc.cjs", - ".prettierrc.mjs", - ".prettierrc.ts", - ".prettierrc.cts", - ".prettierrc.mts", - "package.json", - "prettier.config.js", - "prettier.config.cjs", - "prettier.config.mjs", - "prettier.config.ts", - "prettier.config.cts", - "prettier.config.mts", - ".editorconfig", - ".prettierignore", - ]; - - pub async fn locate_prettier_installation( - fs: &dyn Fs, - installed_prettiers: &HashSet, - locate_from: &Path, - ) -> anyhow::Result>> { - let mut path_to_check = locate_from - .components() - .take_while(|component| component.as_os_str().to_string_lossy() != "node_modules") - .collect::(); - if path_to_check != locate_from { - log::debug!( - "Skipping prettier location for path {path_to_check:?} that is inside node_modules" - ); - return Ok(ControlFlow::Break(())); - } - let path_to_check_metadata = fs - .metadata(&path_to_check) - .await - .with_context(|| format!("failed to get metadata for initial path {path_to_check:?}"))? - .with_context(|| format!("empty metadata for initial path {path_to_check:?}"))?; - if !path_to_check_metadata.is_dir { - path_to_check.pop(); - } - - let mut closest_package_json_path = None; - loop { - if installed_prettiers.contains(&path_to_check) { - log::debug!("Found prettier path {path_to_check:?} in installed prettiers"); - return Ok(ControlFlow::Continue(Some(path_to_check))); - } else if let Some(package_json_contents) = - read_package_json(fs, &path_to_check).await? - { - if has_prettier_in_node_modules(fs, &path_to_check).await? { - log::debug!("Found prettier path {path_to_check:?} in the node_modules"); - return Ok(ControlFlow::Continue(Some(path_to_check))); - } else { - match &closest_package_json_path { - None => closest_package_json_path = Some(path_to_check.clone()), - Some(closest_package_json_path) => { - match package_json_contents.get("workspaces") { - Some(serde_json::Value::Array(workspaces)) => { - let subproject_path = closest_package_json_path.strip_prefix(&path_to_check).expect("traversing path parents, should be able to strip prefix"); - if workspaces.iter().filter_map(|value| { - if let serde_json::Value::String(s) = value { - Some(s.clone()) - } else { - log::warn!("Skipping non-string 'workspaces' value: {value:?}"); - None - } - }).any(|workspace_definition| { - workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition], PathStyle::local()).ok().is_some_and( - |path_matcher| RelPath::new(subproject_path, PathStyle::local()).is_ok_and(|path| path_matcher.is_match(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:?}" - ); - return Ok(ControlFlow::Continue(Some(path_to_check))); - } else { - log::warn!( - "Skipping path {path_to_check:?} workspace root with \ - workspaces {workspaces:?} that have no prettier installed" - ); - } - } - Some(unknown) => log::error!( - "Failed to parse workspaces for {path_to_check:?} from package.json, \ - got {unknown:?}. Skipping." - ), - None => log::warn!( - "Skipping path {path_to_check:?} that has no prettier \ - dependency and no workspaces section in its package.json" - ), - } - } - } - } - } - - if !path_to_check.pop() { - log::debug!("Found no prettier in ancestors of {locate_from:?}"); - return Ok(ControlFlow::Continue(None)); - } - } - } - - pub async fn locate_prettier_ignore( - fs: &dyn Fs, - prettier_ignores: &HashSet, - locate_from: &Path, - ) -> anyhow::Result>> { - let mut path_to_check = locate_from - .components() - .take_while(|component| component.as_os_str().to_string_lossy() != "node_modules") - .collect::(); - if path_to_check != locate_from { - log::debug!( - "Skipping prettier ignore location for path {path_to_check:?} that is inside node_modules" - ); - return Ok(ControlFlow::Break(())); - } - - let path_to_check_metadata = fs - .metadata(&path_to_check) - .await - .with_context(|| format!("failed to get metadata for initial path {path_to_check:?}"))? - .with_context(|| format!("empty metadata for initial path {path_to_check:?}"))?; - if !path_to_check_metadata.is_dir { - path_to_check.pop(); - } - - let mut closest_package_json_path = None; - loop { - if prettier_ignores.contains(&path_to_check) { - log::debug!("Found prettier ignore at {path_to_check:?}"); - return Ok(ControlFlow::Continue(Some(path_to_check))); - } else if let Some(package_json_contents) = - read_package_json(fs, &path_to_check).await? - { - let ignore_path = path_to_check.join(".prettierignore"); - if let Some(metadata) = fs - .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))); - } - match &closest_package_json_path { - None => closest_package_json_path = Some(path_to_check.clone()), - Some(closest_package_json_path) => { - if let Some(serde_json::Value::Array(workspaces)) = - package_json_contents.get("workspaces") - { - let subproject_path = closest_package_json_path - .strip_prefix(&path_to_check) - .expect("traversing path parents, should be able to strip prefix"); - - if workspaces - .iter() - .filter_map(|value| { - if let serde_json::Value::String(s) = value { - Some(s.clone()) - } else { - log::warn!( - "Skipping non-string 'workspaces' value: {value:?}" - ); - None - } - }) - .any(|workspace_definition| { - workspace_definition == subproject_path.to_string_lossy() - || PathMatcher::new( - &[workspace_definition], - PathStyle::local(), - ) - .ok() - .is_some_and( - |path_matcher| { - RelPath::new(subproject_path, PathStyle::local()) - .is_ok_and(|rel_path| { - path_matcher.is_match(rel_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 !path_to_check.pop() { - log::debug!("Found no prettier ignore in ancestors of {locate_from:?}"); - return Ok(ControlFlow::Continue(None)); - } - } - } - - #[cfg(any(test, feature = "test-support"))] - pub async fn start( - _: LanguageServerId, - prettier_dir: PathBuf, - _: NodeRuntime, - _: AsyncApp, - ) -> anyhow::Result { - Ok(Self::Test(TestPrettier { - default: prettier_dir == default_prettier_dir().as_path(), - prettier_dir, - })) - } - - #[cfg(not(any(test, feature = "test-support")))] - pub async fn start( - server_id: LanguageServerId, - prettier_dir: PathBuf, - node: NodeRuntime, - mut cx: AsyncApp, - ) -> anyhow::Result { - use lsp::{LanguageServerBinary, LanguageServerName}; - - let executor = cx.background_executor().clone(); - anyhow::ensure!( - prettier_dir.is_dir(), - "Prettier dir {prettier_dir:?} is not a directory" - ); - let prettier_server = default_prettier_dir().join(PRETTIER_SERVER_FILE); - anyhow::ensure!( - prettier_server.is_file(), - "no prettier server package found at {prettier_server:?}" - ); - - let node_path = executor - .spawn(async move { node.binary_path().await }) - .await?; - let server_name = LanguageServerName("prettier".into()); - let server_binary = LanguageServerBinary { - path: node_path, - arguments: vec![prettier_server.into(), prettier_dir.as_path().into()], - env: None, - }; - let server = LanguageServer::new( - Arc::new(parking_lot::Mutex::new(None)), - server_id, - server_name, - server_binary, - &prettier_dir, - None, - Default::default(), - &mut cx, - ) - .context("prettier server creation")?; - - let server = cx - .update(|cx| { - let params = server.default_initialize_params(false, cx); - let configuration = lsp::DidChangeConfigurationParams { - settings: Default::default(), - }; - executor.spawn(server.initialize(params, configuration.into(), cx)) - })? - .await - .context("prettier server initialization")?; - Ok(Self::Real(RealPrettier { - server, - default: prettier_dir == default_prettier_dir().as_path(), - prettier_dir, - })) - } - - pub async fn format( - &self, - buffer: &Entity, - buffer_path: Option, - ignore_dir: Option, - cx: &mut AsyncApp, - ) -> anyhow::Result { - match self { - Self::Real(local) => { - let params = buffer - .update(cx, |buffer, cx| { - let buffer_language = buffer.language().map(|language| language.as_ref()); - let language_settings = language_settings(buffer_language.map(|l| l.name()), buffer.file(), cx); - let prettier_settings = &language_settings.prettier; - anyhow::ensure!( - prettier_settings.allowed, - "Cannot format: prettier is not allowed for language {buffer_language:?}" - ); - let prettier_node_modules = self.prettier_dir().join("node_modules"); - anyhow::ensure!( - prettier_node_modules.is_dir(), - "Prettier node_modules dir does not exist: {prettier_node_modules:?}" - ); - let plugin_name_into_path = |plugin_name: &str| { - let prettier_plugin_dir = prettier_node_modules.join(plugin_name); - [ - prettier_plugin_dir.join("dist").join("index.mjs"), - prettier_plugin_dir.join("dist").join("index.js"), - prettier_plugin_dir.join("dist").join("plugin.js"), - prettier_plugin_dir.join("src").join("plugin.js"), - prettier_plugin_dir.join("lib").join("index.js"), - prettier_plugin_dir.join("index.mjs"), - prettier_plugin_dir.join("index.js"), - prettier_plugin_dir.join("plugin.js"), - // this one is for @prettier/plugin-php - prettier_plugin_dir.join("standalone.js"), - // this one is for prettier-plugin-latex - prettier_plugin_dir.join("dist").join("prettier-plugin-latex.js"), - prettier_plugin_dir, - ] - .into_iter() - .find(|possible_plugin_path| possible_plugin_path.is_file()) - }; - - // Tailwind plugin requires being added last - // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins - let mut add_tailwind_back = false; - - let mut located_plugins = prettier_settings.plugins.iter() - .filter(|plugin_name| { - if plugin_name.as_str() == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME { - add_tailwind_back = true; - false - } else { - true - } - }) - .map(|plugin_name| { - let plugin_path = plugin_name_into_path(plugin_name); - (plugin_name.clone(), plugin_path) - }) - .collect::>(); - if add_tailwind_back { - located_plugins.push(( - TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME.to_owned(), - plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME), - )); - } - - let prettier_options = if self.is_default() { - let mut options = prettier_settings.options.clone(); - if !options.contains_key("tabWidth") { - options.insert( - "tabWidth".to_string(), - serde_json::Value::Number(serde_json::Number::from( - language_settings.tab_size.get(), - )), - ); - } - if !options.contains_key("printWidth") { - options.insert( - "printWidth".to_string(), - serde_json::Value::Number(serde_json::Number::from( - language_settings.preferred_line_length, - )), - ); - } - if !options.contains_key("useTabs") { - options.insert( - "useTabs".to_string(), - serde_json::Value::Bool(language_settings.hard_tabs), - ); - } - Some(options) - } else { - None - }; - - let plugins = located_plugins - .into_iter() - .filter_map(|(plugin_name, located_plugin_path)| { - match located_plugin_path { - Some(path) => Some(path), - None => { - log::error!("Have not found plugin path for {plugin_name:?} inside {prettier_node_modules:?}"); - None - } - } - }) - .collect(); - - let parser = prettier_parser_name(buffer_path.as_deref(), buffer_language, prettier_settings).context("getting prettier parser")?; - - let ignore_path = ignore_dir.and_then(|dir| { - let ignore_file = dir.join(".prettierignore"); - ignore_file.is_file().then_some(ignore_file) - }); - - log::debug!( - "Formatting file {:?} with prettier, plugins :{:?}, options: {:?}, ignore_path: {:?}", - buffer.file().map(|f| f.full_path(cx)), - plugins, - prettier_options, - ignore_path, - ); - - anyhow::Ok(FormatParams { - text: buffer.text(), - options: FormatOptions { - path: buffer_path, - parser, - plugins, - prettier_options, - ignore_path, - }, - }) - })? - .context("building prettier request")?; - - let response = local - .server - .request::(params) - .await - .into_response()?; - let diff_task = buffer.update(cx, |buffer, cx| buffer.diff(response.text, cx))?; - Ok(diff_task.await) - } - #[cfg(any(test, feature = "test-support"))] - Self::Test(_) => Ok(buffer - .update(cx, |buffer, cx| { - match buffer - .language() - .map(|language| language.lsp_id()) - .as_deref() - { - Some("rust") => anyhow::bail!("prettier does not support Rust"), - Some(_other) => { - let mut formatted_text = buffer.text() + FORMAT_SUFFIX; - - let buffer_language = - buffer.language().map(|language| language.as_ref()); - let language_settings = language_settings( - buffer_language.map(|l| l.name()), - buffer.file(), - cx, - ); - let prettier_settings = &language_settings.prettier; - let parser = prettier_parser_name( - buffer_path.as_deref(), - buffer_language, - prettier_settings, - )?; - - if let Some(parser) = parser { - formatted_text = format!("{formatted_text}\n{parser}"); - } - - Ok(buffer.diff(formatted_text, cx)) - } - None => panic!("Should not format buffer without a language with prettier"), - } - })?? - .await), - } - } - - pub async fn clear_cache(&self) -> anyhow::Result<()> { - match self { - Self::Real(local) => local - .server - .request::(()) - .await - .into_response() - .context("prettier clear cache"), - #[cfg(any(test, feature = "test-support"))] - Self::Test(_) => Ok(()), - } - } - - pub fn server(&self) -> Option<&Arc> { - match self { - Self::Real(local) => Some(&local.server), - #[cfg(any(test, feature = "test-support"))] - Self::Test(_) => None, - } - } - - pub fn is_default(&self) -> bool { - match self { - Self::Real(local) => local.default, - #[cfg(any(test, feature = "test-support"))] - Self::Test(test_prettier) => test_prettier.default, - } - } - - pub fn prettier_dir(&self) -> &Path { - match self { - Self::Real(local) => &local.prettier_dir, - #[cfg(any(test, feature = "test-support"))] - Self::Test(test_prettier) => &test_prettier.prettier_dir, - } - } -} - -fn prettier_parser_name( - buffer_path: Option<&Path>, - buffer_language: Option<&Language>, - prettier_settings: &PrettierSettings, -) -> anyhow::Result> { - let parser = if buffer_path.is_none() { - let parser = prettier_settings - .parser - .as_deref() - .or_else(|| buffer_language.and_then(|language| language.prettier_parser_name())); - if parser.is_none() { - log::error!( - "Formatting unsaved file with prettier failed. No prettier parser configured for language {buffer_language:?}" - ); - anyhow::bail!("Cannot determine prettier parser for unsaved file"); - } - parser - } else if let (Some(buffer_language), Some(buffer_path)) = (buffer_language, buffer_path) - && buffer_path.extension().is_some_and(|extension| { - !buffer_language - .config() - .matcher - .path_suffixes - .contains(&extension.to_string_lossy().into_owned()) - }) - { - buffer_language.prettier_parser_name() - } else { - prettier_settings.parser.as_deref() - }; - - Ok(parser.map(ToOwned::to_owned)) -} - -async fn has_prettier_in_node_modules(fs: &dyn Fs, path: &Path) -> anyhow::Result { - let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME); - if let Some(node_modules_location_metadata) = fs - .metadata(&possible_node_modules_location) - .await - .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))? - { - return Ok(node_modules_location_metadata.is_dir); - } - Ok(false) -} - -async fn read_package_json( - fs: &dyn Fs, - path: &Path, -) -> anyhow::Result>> { - let possible_package_json = path.join("package.json"); - if let Some(package_json_metadata) = fs - .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) - .map(Some) - .with_context(|| format!("parsing {possible_package_json:?} file contents")); - } - Ok(None) -} - -enum Format {} - -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct FormatParams { - text: String, - options: FormatOptions, -} - -#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct FormatOptions { - plugins: Vec, - parser: Option, - #[serde(rename = "filepath")] - path: Option, - prettier_options: Option>, - ignore_path: Option, -} - -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct FormatResult { - text: String, -} - -impl lsp::request::Request for Format { - type Params = FormatParams; - type Result = FormatResult; - const METHOD: &'static str = "prettier/format"; -} - -enum ClearCache {} - -impl lsp::request::Request for ClearCache { - type Params = (); - type Result = (); - const METHOD: &'static str = "prettier/clear_cache"; -} - -#[cfg(test)] -mod tests { - use fs::FakeFs; - use serde_json::json; - - use super::*; - - #[gpui::test] - async fn test_prettier_lookup_finds_nothing(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - ".config": { - "zed": { - "settings.json": r#"{ "formatter": "auto" }"#, - }, - }, - "work": { - "project": { - "src": { - "index.js": "// index.js file contents", - }, - "node_modules": { - "expect": { - "build": { - "print.js": "// print.js file contents", - }, - "package.json": r#"{ - "devDependencies": { - "prettier": "2.5.1" - } - }"#, - }, - "prettier": { - "index.js": "// Dummy prettier package file", - }, - }, - "package.json": r#"{}"# - }, - } - }), - ) - .await; - - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/.config/zed/settings.json"), - ) - .await - .unwrap(), - ControlFlow::Continue(None), - "Should find no prettier for path hierarchy without it" - ); - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/work/project/src/index.js") - ) - .await - .unwrap(), - ControlFlow::Continue(Some(PathBuf::from("/root/work/project"))), - "Should successfully find a prettier for path hierarchy that has node_modules with prettier, but no package.json mentions of it" - ); - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/work/project/node_modules/expect/build/print.js") - ) - .await - .unwrap(), - ControlFlow::Break(()), - "Should not format files inside node_modules/" - ); - } - - #[gpui::test] - async fn test_prettier_lookup_in_simple_npm_projects(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "web_blog": { - "node_modules": { - "prettier": { - "index.js": "// Dummy prettier package file", - }, - "expect": { - "build": { - "print.js": "// print.js file contents", - }, - "package.json": r#"{ - "devDependencies": { - "prettier": "2.5.1" - } - }"#, - }, - }, - "pages": { - "[slug].tsx": "// [slug].tsx file contents", - }, - "package.json": r#"{ - "devDependencies": { - "prettier": "2.3.0" - }, - "prettier": { - "semi": false, - "printWidth": 80, - "htmlWhitespaceSensitivity": "strict", - "tabWidth": 4 - } - }"# - } - }), - ) - .await; - - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/web_blog/pages/[slug].tsx") - ) - .await - .unwrap(), - ControlFlow::Continue(Some(PathBuf::from("/root/web_blog"))), - "Should find a preinstalled prettier in the project root" - ); - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/web_blog/node_modules/expect/build/print.js") - ) - .await - .unwrap(), - ControlFlow::Break(()), - "Should not allow formatting node_modules/ contents" - ); - } - - #[gpui::test] - async fn test_prettier_lookup_for_not_installed(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "work": { - "web_blog": { - "node_modules": { - "expect": { - "build": { - "print.js": "// print.js file contents", - }, - "package.json": r#"{ - "devDependencies": { - "prettier": "2.5.1" - } - }"#, - }, - }, - "pages": { - "[slug].tsx": "// [slug].tsx file contents", - }, - "package.json": r#"{ - "devDependencies": { - "prettier": "2.3.0" - }, - "prettier": { - "semi": false, - "printWidth": 80, - "htmlWhitespaceSensitivity": "strict", - "tabWidth": 4 - } - }"# - } - } - }), - ) - .await; - - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/work/web_blog/pages/[slug].tsx") - ) - .await - .unwrap(), - ControlFlow::Continue(None), - "Should find no prettier when node_modules don't have it" - ); - - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::from_iter( - [PathBuf::from("/root"), PathBuf::from("/root/work")].into_iter() - ), - Path::new("/root/work/web_blog/pages/[slug].tsx") - ) - .await - .unwrap(), - ControlFlow::Continue(Some(PathBuf::from("/root/work"))), - "Should return closest cached value found without path checks" - ); - - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/work/web_blog/node_modules/expect/build/print.js") - ) - .await - .unwrap(), - ControlFlow::Break(()), - "Should not allow formatting files inside node_modules/" - ); - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::from_iter( - [PathBuf::from("/root"), PathBuf::from("/root/work")].into_iter() - ), - Path::new("/root/work/web_blog/node_modules/expect/build/print.js") - ) - .await - .unwrap(), - ControlFlow::Break(()), - "Should ignore cache lookup for files inside node_modules/" - ); - } - - #[gpui::test] - async fn test_prettier_lookup_in_npm_workspaces(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "work": { - "full-stack-foundations": { - "exercises": { - "03.loading": { - "01.problem.loader": { - "app": { - "routes": { - "users+": { - "$username_+": { - "notes.tsx": "// notes.tsx file contents", - }, - }, - }, - }, - "node_modules": { - "test.js": "// test.js contents", - }, - "package.json": r#"{ - "devDependencies": { - "prettier": "^3.0.3" - } - }"# - }, - }, - }, - "package.json": r#"{ - "workspaces": ["exercises/*/*", "examples/*"] - }"#, - "node_modules": { - "prettier": { - "index.js": "// Dummy prettier package file", - }, - }, - }, - } - }), - ) - .await; - - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/app/routes/users+/$username_+/notes.tsx"), - ).await.unwrap(), - ControlFlow::Continue(Some(PathBuf::from("/root/work/full-stack-foundations"))), - "Should ascend to the multi-workspace root and find the prettier there", - ); - - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/work/full-stack-foundations/node_modules/prettier/index.js") - ) - .await - .unwrap(), - ControlFlow::Break(()), - "Should not allow formatting files inside root node_modules/" - ); - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/node_modules/test.js") - ) - .await - .unwrap(), - ControlFlow::Break(()), - "Should not allow formatting files inside submodule's node_modules/" - ); - } - - #[gpui::test] - async fn test_prettier_lookup_in_npm_workspaces_for_not_installed( - cx: &mut gpui::TestAppContext, - ) { - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "work": { - "full-stack-foundations": { - "exercises": { - "03.loading": { - "01.problem.loader": { - "app": { - "routes": { - "users+": { - "$username_+": { - "notes.tsx": "// notes.tsx file contents", - }, - }, - }, - }, - "node_modules": {}, - "package.json": r#"{ - "devDependencies": { - "prettier": "^3.0.3" - } - }"# - }, - }, - }, - "package.json": r#"{ - "workspaces": ["exercises/*/*", "examples/*"] - }"#, - }, - } - }), - ) - .await; - - match Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/app/routes/users+/$username_+/notes.tsx") - ) - .await { - Ok(path) => panic!("Expected to fail for prettier in package.json but not in node_modules found, but got path {path:?}"), - Err(e) => { - let message = e.to_string().replace("\\\\", "/"); - assert!(message.contains("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader"), "Error message should mention which project had prettier defined"); - assert!(message.contains("/root/work/full-stack-foundations"), "Error message should mention potential candidates without prettier node_modules contents"); - }, - }; - } - - #[gpui::test] - async fn test_prettier_ignore_with_editor_prettier(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "project": { - "src": { - "index.js": "// index.js file contents", - "ignored.js": "// this file should be ignored", - }, - ".prettierignore": "ignored.js", - "package.json": r#"{ - "name": "test-project" - }"# - } - }), - ) - .await; - - assert_eq!( - Prettier::locate_prettier_ignore( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/project/src/index.js"), - ) - .await - .unwrap(), - ControlFlow::Continue(Some(PathBuf::from("/root/project"))), - "Should find prettierignore in project root" - ); - } - - #[gpui::test] - async fn test_prettier_ignore_in_monorepo_with_only_child_ignore( - cx: &mut gpui::TestAppContext, - ) { - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "monorepo": { - "node_modules": { - "prettier": { - "index.js": "// Dummy prettier package file", - } - }, - "packages": { - "web": { - "src": { - "index.js": "// index.js contents", - "ignored.js": "// this should be ignored", - }, - ".prettierignore": "ignored.js", - "package.json": r#"{ - "name": "web-package" - }"# - } - }, - "package.json": r#"{ - "workspaces": ["packages/*"], - "devDependencies": { - "prettier": "^2.0.0" - } - }"# - } - }), - ) - .await; - - assert_eq!( - Prettier::locate_prettier_ignore( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/monorepo/packages/web/src/index.js"), - ) - .await - .unwrap(), - ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))), - "Should find prettierignore in child package" - ); - } - - #[gpui::test] - async fn test_prettier_ignore_in_monorepo_with_root_and_child_ignores( - cx: &mut gpui::TestAppContext, - ) { - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "monorepo": { - "node_modules": { - "prettier": { - "index.js": "// Dummy prettier package file", - } - }, - ".prettierignore": "main.js", - "packages": { - "web": { - "src": { - "main.js": "// this should not be ignored", - "ignored.js": "// this should be ignored", - }, - ".prettierignore": "ignored.js", - "package.json": r#"{ - "name": "web-package" - }"# - } - }, - "package.json": r#"{ - "workspaces": ["packages/*"], - "devDependencies": { - "prettier": "^2.0.0" - } - }"# - } - }), - ) - .await; - - assert_eq!( - Prettier::locate_prettier_ignore( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/monorepo/packages/web/src/main.js"), - ) - .await - .unwrap(), - ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))), - "Should find child package prettierignore first" - ); - - assert_eq!( - Prettier::locate_prettier_ignore( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/monorepo/packages/web/src/ignored.js"), - ) - .await - .unwrap(), - ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))), - "Should find child package prettierignore first" - ); - } -} diff --git a/crates/prettier/src/prettier_server.js b/crates/prettier/src/prettier_server.js deleted file mode 100644 index b3d8a660a4..0000000000 --- a/crates/prettier/src/prettier_server.js +++ /dev/null @@ -1,266 +0,0 @@ -const { Buffer } = require("buffer"); -const fs = require("fs"); -const path = require("path"); -const { once } = require("events"); - -const prettierContainerPath = process.argv[2]; -if (prettierContainerPath == null || prettierContainerPath.length == 0) { - process.stderr.write( - `Prettier path argument was not specified or empty.\nUsage: ${process.argv[0]} ${process.argv[1]} prettier/path\n`, - ); - process.exit(1); -} -fs.stat(prettierContainerPath, (err, stats) => { - if (err) { - process.stderr.write(`Path '${prettierContainerPath}' does not exist\n`); - process.exit(1); - } - - if (!stats.isDirectory()) { - process.stderr.write(`Path '${prettierContainerPath}' exists but is not a directory\n`); - process.exit(1); - } -}); -const prettierPath = path.join(prettierContainerPath, "node_modules/prettier"); - -class Prettier { - constructor(path, prettier, config) { - this.path = path; - this.prettier = prettier; - this.config = config; - } -} - -(async () => { - let prettier; - let config; - try { - prettier = await loadPrettier(prettierPath); - config = (await prettier.resolveConfig(prettierPath)) || {}; - } catch (e) { - process.stderr.write(`Failed to load prettier: ${e}\n`); - process.exit(1); - } - process.stderr.write(`Prettier at path '${prettierPath}' loaded successfully, config: ${JSON.stringify(config)}\n`); - process.stdin.resume(); - handleBuffer(new Prettier(prettierPath, prettier, config)); -})(); - -async function handleBuffer(prettier) { - for await (const messageText of readStdin()) { - let message; - try { - message = JSON.parse(messageText); - } catch (e) { - sendResponse(makeError(`Parse error in request message: ${e}\nMessage: ${messageText}`)); - continue; - } - // allow concurrent request handling by not `await`ing the message handling promise (async function) - handleMessage(message, prettier).catch((e) => { - if ((message.params || {}).text !== undefined) { - message.params.text = "..snip.."; - } - sendResponse({ - id: message.id, - ...makeError(`${e}\nWhile handling prettier request: ${JSON.stringify(message)}`), - }); - }); - } -} - -const headerSeparator = "\r\n"; -const contentLengthHeaderName = "Content-Length"; - -async function* readStdin() { - let buffer = Buffer.alloc(0); - let streamEnded = false; - process.stdin.on("end", () => { - streamEnded = true; - }); - process.stdin.on("data", (data) => { - buffer = Buffer.concat([buffer, data]); - }); - - async function handleStreamEnded(errorMessage) { - sendResponse(makeError(errorMessage)); - buffer = Buffer.alloc(0); - messageLength = null; - await once(process.stdin, "readable"); - streamEnded = false; - } - - try { - let headersLength = null; - let messageLength = null; - main_loop: while (true) { - if (messageLength === null) { - while (buffer.indexOf(`${headerSeparator}${headerSeparator}`) === -1) { - if (streamEnded) { - await handleStreamEnded("Unexpected end of stream: headers not found"); - continue main_loop; - } else if (buffer.length > contentLengthHeaderName.length * 10) { - await handleStreamEnded( - `Unexpected stream of bytes: no headers end found after ${buffer.length} bytes of input`, - ); - continue main_loop; - } - await once(process.stdin, "readable"); - } - const headers = buffer.subarray(0, buffer.indexOf(`${headerSeparator}${headerSeparator}`)).toString("ascii"); - const contentLengthHeader = headers - .split(headerSeparator) - .map((header) => header.split(":")) - .filter((header) => header[2] === undefined) - .filter((header) => (header[1] || "").length > 0) - .find((header) => (header[0] || "").trim() === contentLengthHeaderName); - const contentLength = (contentLengthHeader || [])[1]; - if (contentLength === undefined) { - await handleStreamEnded(`Missing or incorrect ${contentLengthHeaderName} header: ${headers}`); - continue main_loop; - } - headersLength = headers.length + headerSeparator.length * 2; - messageLength = parseInt(contentLength, 10); - } - - while (buffer.length < headersLength + messageLength) { - if (streamEnded) { - await handleStreamEnded( - `Unexpected end of stream: buffer length ${buffer.length} does not match expected header length ${headersLength} + body length ${messageLength}`, - ); - continue main_loop; - } - await once(process.stdin, "readable"); - } - - const messageEnd = headersLength + messageLength; - const message = buffer.subarray(headersLength, messageEnd); - buffer = buffer.subarray(messageEnd); - headersLength = null; - messageLength = null; - yield message.toString("utf8"); - } - } catch (e) { - sendResponse(makeError(`Error reading stdin: ${e}`)); - } finally { - process.stdin.off("data", () => {}); - } -} - -async function handleMessage(message, prettier) { - const { method, id, params } = message; - if (method === undefined) { - throw new Error(`Message method is undefined: ${JSON.stringify(message)}`); - } else if (method == "initialized") { - return; - } else if (method === "shutdown") { - sendResponse({ result: {} }); - } else if (method == "exit") { - process.exit(0); - } - - if (id === undefined) { - throw new Error(`Message id is undefined: ${JSON.stringify(message)}`); - } - - if (method === "prettier/format") { - if (params === undefined || params.text === undefined) { - throw new Error(`Message params.text is undefined: ${JSON.stringify(message)}`); - } - if (params.options === undefined) { - throw new Error(`Message params.options is undefined: ${JSON.stringify(message)}`); - } - - let resolvedConfig = {}; - if (params.options.filepath) { - resolvedConfig = (await prettier.prettier.resolveConfig(params.options.filepath)) || {}; - - if (params.options.ignorePath) { - const fileInfo = await prettier.prettier.getFileInfo(params.options.filepath, { - ignorePath: params.options.ignorePath, - }); - if (fileInfo.ignored) { - process.stderr.write( - `Ignoring file '${params.options.filepath}' based on rules in '${params.options.ignorePath}'\n`, - ); - sendResponse({ id, result: { text: params.text } }); - return; - } - } - } - - // Marking the params.options.filepath as undefined makes - // prettier.format() work even if no filepath is set. - if (params.options.filepath === null) { - params.options.filepath = undefined; - } - - const plugins = - Array.isArray(resolvedConfig?.plugins) && resolvedConfig.plugins.length > 0 - ? resolvedConfig.plugins - : params.options.plugins; - - const options = { - ...(params.options.prettierOptions || prettier.config), - ...resolvedConfig, - plugins, - parser: params.options.parser, - filepath: params.options.filepath, - }; - process.stderr.write( - `Resolved config: ${JSON.stringify(resolvedConfig)}, will format file '${ - params.options.filepath || "" - }' with options: ${JSON.stringify(options)}\n`, - ); - const formattedText = await prettier.prettier.format(params.text, options); - sendResponse({ id, result: { text: formattedText } }); - } else if (method === "prettier/clear_cache") { - prettier.prettier.clearConfigCache(); - prettier.config = (await prettier.prettier.resolveConfig(prettier.path)) || {}; - sendResponse({ id, result: null }); - } else if (method === "initialize") { - sendResponse({ - id, - result: { - capabilities: {}, - }, - }); - } else { - throw new Error(`Unknown method: ${method}`); - } -} - -function makeError(message) { - return { - error: { - code: -32600, // invalid request code - message, - }, - }; -} - -function sendResponse(response) { - const responsePayloadString = JSON.stringify({ - jsonrpc: "2.0", - ...response, - }); - const headers = `${contentLengthHeaderName}: ${Buffer.byteLength( - responsePayloadString, - )}${headerSeparator}${headerSeparator}`; - process.stdout.write(headers + responsePayloadString); -} - -function loadPrettier(prettierPath) { - return new Promise((resolve, reject) => { - fs.access(prettierPath, fs.constants.F_OK, (err) => { - if (err) { - reject(`Path '${prettierPath}' does not exist.Error: ${err}`); - } else { - try { - resolve(require(prettierPath)); - } catch (err) { - reject(`Error requiring prettier module from path '${prettierPath}'.Error: ${err}`); - } - } - }); - }); -} diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml deleted file mode 100644 index 9e2789fc10..0000000000 --- a/crates/project/Cargo.toml +++ /dev/null @@ -1,120 +0,0 @@ -[package] -name = "project" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/project.rs" -doctest = false - -[features] -test-support = [ - "buffer_diff/test-support", - "client/test-support", - "language/test-support", - "settings/test-support", - "snippet_provider/test-support", - "text/test-support", - "prettier/test-support", - "worktree/test-support", - "gpui/test-support", - "dap/test-support", - "dap_adapters/test-support", -] - -[dependencies] -aho-corasick.workspace = true -anyhow.workspace = true -askpass.workspace = true -async-trait.workspace = true -base64.workspace = true -buffer_diff.workspace = true -circular-buffer.workspace = true -client.workspace = true -clock.workspace = true -collections.workspace = true -context_server.workspace = true -dap.workspace = true -extension.workspace = true -fancy-regex.workspace = true -fs.workspace = true -futures.workspace = true -fuzzy.workspace = true -git.workspace = true -git_hosting_providers.workspace = true -globset.workspace = true -gpui.workspace = true -http_client.workspace = true -image.workspace = true -itertools.workspace = true -indexmap.workspace = true -language.workspace = true -log.workspace = true -lsp.workspace = true -markdown.workspace = true -node_runtime.workspace = true -parking_lot.workspace = true -paths.workspace = true -postage.workspace = true -prettier.workspace = true -rand.workspace = true -regex.workspace = true -remote.workspace = true -rpc.workspace = true -schemars.workspace = true -semver.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -sha2.workspace = true -shellexpand.workspace = true -smallvec.workspace = true -smol.workspace = true -snippet.workspace = true -snippet_provider.workspace = true -sum_tree.workspace = true -task.workspace = true -tempfile.workspace = true -terminal.workspace = true -text.workspace = true -toml.workspace = true -url.workspace = true -util.workspace = true -watch.workspace = true -wax.workspace = true -which.workspace = true -worktree.workspace = true -zeroize.workspace = true -zlog.workspace = true -ztracing.workspace = true -tracing.workspace = true - -[dev-dependencies] -client = { workspace = true, features = ["test-support"] } -collections = { workspace = true, features = ["test-support"] } -context_server = { workspace = true, features = ["test-support"] } -buffer_diff = { workspace = true, features = ["test-support"] } -dap = { workspace = true, features = ["test-support"] } -dap_adapters = { workspace = true, features = ["test-support"] } -fs = { workspace = true, features = ["test-support"] } -git2.workspace = true -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -lsp = { workspace = true, features = ["test-support"] } -prettier = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true -release_channel.workspace = true -rpc = { workspace = true, features = ["test-support"] } -settings = { workspace = true, features = ["test-support"] } -snippet_provider = { workspace = true, features = ["test-support"] } -unindent.workspace = true -util = { workspace = true, features = ["test-support"] } -worktree = { workspace = true, features = ["test-support"] } - -[package.metadata.cargo-machete] -ignored = ["tracing"] diff --git a/crates/project/LICENSE-GPL b/crates/project/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/project/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/project/src/agent_server_store.rs b/crates/project/src/agent_server_store.rs deleted file mode 100644 index a2cc57beae..0000000000 --- a/crates/project/src/agent_server_store.rs +++ /dev/null @@ -1,2346 +0,0 @@ -use std::{ - any::Any, - borrow::Borrow, - path::{Path, PathBuf}, - str::FromStr as _, - sync::Arc, - time::Duration, -}; - -use anyhow::{Context as _, Result, bail}; -use collections::HashMap; -use fs::{Fs, RemoveOptions, RenameOptions}; -use futures::StreamExt as _; -use gpui::{ - AppContext as _, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task, -}; -use http_client::{HttpClient, github::AssetKind}; -use node_runtime::NodeRuntime; -use remote::RemoteClient; -use rpc::{ - AnyProtoClient, TypedEnvelope, - proto::{self, ExternalExtensionAgent}, -}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{RegisterSetting, SettingsStore}; -use task::{Shell, SpawnInTerminal}; -use util::{ResultExt as _, debug_panic}; - -use crate::ProjectEnvironment; - -#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)] -pub struct AgentServerCommand { - #[serde(rename = "command")] - pub path: PathBuf, - #[serde(default)] - pub args: Vec, - pub env: Option>, -} - -impl std::fmt::Debug for AgentServerCommand { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let filtered_env = self.env.as_ref().map(|env| { - env.iter() - .map(|(k, v)| { - ( - k, - if util::redact::should_redact(k) { - "[REDACTED]" - } else { - v - }, - ) - }) - .collect::>() - }); - - f.debug_struct("AgentServerCommand") - .field("path", &self.path) - .field("args", &self.args) - .field("env", &filtered_env) - .finish() - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct ExternalAgentServerName(pub SharedString); - -impl std::fmt::Display for ExternalAgentServerName { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From<&'static str> for ExternalAgentServerName { - fn from(value: &'static str) -> Self { - ExternalAgentServerName(value.into()) - } -} - -impl From for SharedString { - fn from(value: ExternalAgentServerName) -> Self { - value.0 - } -} - -impl Borrow for ExternalAgentServerName { - fn borrow(&self) -> &str { - &self.0 - } -} - -pub trait ExternalAgentServer { - fn get_command( - &mut self, - root_dir: Option<&str>, - extra_env: HashMap, - status_tx: Option>, - new_version_available_tx: Option>>, - cx: &mut AsyncApp, - ) -> Task)>>; - - fn as_any_mut(&mut self) -> &mut dyn Any; -} - -impl dyn ExternalAgentServer { - fn downcast_mut(&mut self) -> Option<&mut T> { - self.as_any_mut().downcast_mut() - } -} - -enum AgentServerStoreState { - Local { - node_runtime: NodeRuntime, - fs: Arc, - project_environment: Entity, - downstream_client: Option<(u64, AnyProtoClient)>, - settings: Option, - http_client: Arc, - extension_agents: Vec<( - Arc, - String, - HashMap, - HashMap, - Option, - )>, - _subscriptions: [Subscription; 1], - }, - Remote { - project_id: u64, - upstream_client: Entity, - }, - Collab, -} - -pub struct AgentServerStore { - state: AgentServerStoreState, - external_agents: HashMap>, - agent_icons: HashMap, - agent_display_names: HashMap, -} - -pub struct AgentServersUpdated; - -impl EventEmitter for AgentServerStore {} - -#[cfg(test)] -mod ext_agent_tests { - use super::*; - use std::{collections::HashSet, fmt::Write as _}; - - // Helper to build a store in Collab mode so we can mutate internal maps without - // needing to spin up a full project environment. - fn collab_store() -> AgentServerStore { - AgentServerStore { - state: AgentServerStoreState::Collab, - external_agents: HashMap::default(), - agent_icons: HashMap::default(), - agent_display_names: HashMap::default(), - } - } - - // A simple fake that implements ExternalAgentServer without needing async plumbing. - struct NoopExternalAgent; - - impl ExternalAgentServer for NoopExternalAgent { - fn get_command( - &mut self, - _root_dir: Option<&str>, - _extra_env: HashMap, - _status_tx: Option>, - _new_version_available_tx: Option>>, - _cx: &mut AsyncApp, - ) -> Task)>> { - Task::ready(Ok(( - AgentServerCommand { - path: PathBuf::from("noop"), - args: Vec::new(), - env: None, - }, - "".to_string(), - None, - ))) - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - } - - #[test] - fn external_agent_server_name_display() { - let name = ExternalAgentServerName(SharedString::from("Ext: Tool")); - let mut s = String::new(); - write!(&mut s, "{name}").unwrap(); - assert_eq!(s, "Ext: Tool"); - } - - #[test] - fn sync_extension_agents_removes_previous_extension_entries() { - let mut store = collab_store(); - - // Seed with a couple of agents that will be replaced by extensions - store.external_agents.insert( - ExternalAgentServerName(SharedString::from("foo-agent")), - Box::new(NoopExternalAgent) as Box, - ); - store.external_agents.insert( - ExternalAgentServerName(SharedString::from("bar-agent")), - Box::new(NoopExternalAgent) as Box, - ); - store.external_agents.insert( - ExternalAgentServerName(SharedString::from("custom")), - Box::new(NoopExternalAgent) as Box, - ); - - // Simulate the removal phase: if we're syncing extensions that provide - // "foo-agent" and "bar-agent", those should be removed first - let extension_agent_names: HashSet = - ["foo-agent".to_string(), "bar-agent".to_string()] - .into_iter() - .collect(); - - let keys_to_remove: Vec<_> = store - .external_agents - .keys() - .filter(|name| extension_agent_names.contains(name.0.as_ref())) - .cloned() - .collect(); - - for key in keys_to_remove { - store.external_agents.remove(&key); - } - - // Only the custom entry should remain. - let remaining: Vec<_> = store - .external_agents - .keys() - .map(|k| k.0.to_string()) - .collect(); - assert_eq!(remaining, vec!["custom".to_string()]); - } -} - -impl AgentServerStore { - /// Synchronizes extension-provided agent servers with the store. - pub fn sync_extension_agents<'a, I>( - &mut self, - manifests: I, - extensions_dir: PathBuf, - cx: &mut Context, - ) where - I: IntoIterator, - { - // Collect manifests first so we can iterate twice - let manifests: Vec<_> = manifests.into_iter().collect(); - - // Remove all extension-provided agents - // (They will be re-added below if they're in the currently installed extensions) - self.external_agents.retain(|name, agent| { - if agent.downcast_mut::().is_some() { - self.agent_icons.remove(name); - self.agent_display_names.remove(name); - false - } else { - // Keep the hardcoded external agents that don't come from extensions - // (In the future we may move these over to being extensions too.) - true - } - }); - - // Insert agent servers from extension manifests - match &mut self.state { - AgentServerStoreState::Local { - extension_agents, .. - } => { - extension_agents.clear(); - for (ext_id, manifest) in manifests { - for (agent_name, agent_entry) in &manifest.agent_servers { - // Store absolute icon path if provided, resolving symlinks for dev extensions - // Store display name from manifest - self.agent_display_names.insert( - ExternalAgentServerName(agent_name.clone().into()), - SharedString::from(agent_entry.name.clone()), - ); - - let icon_path = if let Some(icon) = &agent_entry.icon { - let icon_path = extensions_dir.join(ext_id).join(icon); - // Canonicalize to resolve symlinks (dev extensions are symlinked) - let absolute_icon_path = icon_path - .canonicalize() - .unwrap_or(icon_path) - .to_string_lossy() - .to_string(); - self.agent_icons.insert( - ExternalAgentServerName(agent_name.clone().into()), - SharedString::from(absolute_icon_path.clone()), - ); - Some(absolute_icon_path) - } else { - None - }; - - extension_agents.push(( - agent_name.clone(), - ext_id.to_owned(), - agent_entry.targets.clone(), - agent_entry.env.clone(), - icon_path, - )); - } - } - self.reregister_agents(cx); - } - AgentServerStoreState::Remote { - project_id, - upstream_client, - } => { - let mut agents = vec![]; - for (ext_id, manifest) in manifests { - for (agent_name, agent_entry) in &manifest.agent_servers { - // Store display name from manifest - self.agent_display_names.insert( - ExternalAgentServerName(agent_name.clone().into()), - SharedString::from(agent_entry.name.clone()), - ); - - // Store absolute icon path if provided, resolving symlinks for dev extensions - let icon = if let Some(icon) = &agent_entry.icon { - let icon_path = extensions_dir.join(ext_id).join(icon); - // Canonicalize to resolve symlinks (dev extensions are symlinked) - let absolute_icon_path = icon_path - .canonicalize() - .unwrap_or(icon_path) - .to_string_lossy() - .to_string(); - - // Store icon locally for remote client - self.agent_icons.insert( - ExternalAgentServerName(agent_name.clone().into()), - SharedString::from(absolute_icon_path.clone()), - ); - - Some(absolute_icon_path) - } else { - None - }; - - agents.push(ExternalExtensionAgent { - name: agent_name.to_string(), - icon_path: icon, - extension_id: ext_id.to_string(), - targets: agent_entry - .targets - .iter() - .map(|(k, v)| (k.clone(), v.to_proto())) - .collect(), - env: agent_entry - .env - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(), - }); - } - } - upstream_client - .read(cx) - .proto_client() - .send(proto::ExternalExtensionAgentsUpdated { - project_id: *project_id, - agents, - }) - .log_err(); - } - AgentServerStoreState::Collab => { - // Do nothing - } - } - - cx.emit(AgentServersUpdated); - } - - pub fn agent_icon(&self, name: &ExternalAgentServerName) -> Option { - self.agent_icons.get(name).cloned() - } - - pub fn agent_display_name(&self, name: &ExternalAgentServerName) -> Option { - self.agent_display_names.get(name).cloned() - } - - pub fn init_remote(session: &AnyProtoClient) { - session.add_entity_message_handler(Self::handle_external_agents_updated); - session.add_entity_message_handler(Self::handle_loading_status_updated); - session.add_entity_message_handler(Self::handle_new_version_available); - } - - pub fn init_headless(session: &AnyProtoClient) { - session.add_entity_message_handler(Self::handle_external_extension_agents_updated); - session.add_entity_request_handler(Self::handle_get_agent_server_command); - } - - fn agent_servers_settings_changed(&mut self, cx: &mut Context) { - let AgentServerStoreState::Local { - settings: old_settings, - .. - } = &mut self.state - else { - debug_panic!( - "should not be subscribed to agent server settings changes in non-local project" - ); - return; - }; - - let new_settings = cx - .global::() - .get::(None) - .clone(); - if Some(&new_settings) == old_settings.as_ref() { - return; - } - - self.reregister_agents(cx); - } - - fn reregister_agents(&mut self, cx: &mut Context) { - let AgentServerStoreState::Local { - node_runtime, - fs, - project_environment, - downstream_client, - settings: old_settings, - http_client, - extension_agents, - .. - } = &mut self.state - else { - debug_panic!("Non-local projects should never attempt to reregister. This is a bug!"); - - return; - }; - - let new_settings = cx - .global::() - .get::(None) - .clone(); - - self.external_agents.clear(); - self.external_agents.insert( - GEMINI_NAME.into(), - Box::new(LocalGemini { - fs: fs.clone(), - node_runtime: node_runtime.clone(), - project_environment: project_environment.clone(), - custom_command: new_settings - .gemini - .clone() - .and_then(|settings| settings.custom_command()), - ignore_system_version: new_settings - .gemini - .as_ref() - .and_then(|settings| settings.ignore_system_version) - .unwrap_or(false), - }), - ); - self.external_agents.insert( - CODEX_NAME.into(), - Box::new(LocalCodex { - fs: fs.clone(), - project_environment: project_environment.clone(), - custom_command: new_settings - .codex - .clone() - .and_then(|settings| settings.custom_command()), - http_client: http_client.clone(), - no_browser: downstream_client - .as_ref() - .is_some_and(|(_, client)| !client.has_wsl_interop()), - }), - ); - self.external_agents.insert( - CLAUDE_CODE_NAME.into(), - Box::new(LocalClaudeCode { - fs: fs.clone(), - node_runtime: node_runtime.clone(), - project_environment: project_environment.clone(), - custom_command: new_settings - .claude - .clone() - .and_then(|settings| settings.custom_command()), - }), - ); - self.external_agents - .extend( - new_settings - .custom - .iter() - .filter_map(|(name, settings)| match settings { - CustomAgentServerSettings::Custom { command, .. } => Some(( - ExternalAgentServerName(name.clone()), - Box::new(LocalCustomAgent { - command: command.clone(), - project_environment: project_environment.clone(), - }) as Box, - )), - CustomAgentServerSettings::Extension { .. } => None, - }), - ); - self.external_agents.extend(extension_agents.iter().map( - |(agent_name, ext_id, targets, env, icon_path)| { - let name = ExternalAgentServerName(agent_name.clone().into()); - - // Restore icon if present - if let Some(icon) = icon_path { - self.agent_icons - .insert(name.clone(), SharedString::from(icon.clone())); - } - - ( - name, - Box::new(LocalExtensionArchiveAgent { - fs: fs.clone(), - http_client: http_client.clone(), - node_runtime: node_runtime.clone(), - project_environment: project_environment.clone(), - extension_id: Arc::from(&**ext_id), - targets: targets.clone(), - env: env.clone(), - agent_id: agent_name.clone(), - }) as Box, - ) - }, - )); - - *old_settings = Some(new_settings.clone()); - - if let Some((project_id, downstream_client)) = downstream_client { - downstream_client - .send(proto::ExternalAgentsUpdated { - project_id: *project_id, - names: self - .external_agents - .keys() - .map(|name| name.to_string()) - .collect(), - }) - .log_err(); - } - cx.emit(AgentServersUpdated); - } - - pub fn node_runtime(&self) -> Option { - match &self.state { - AgentServerStoreState::Local { node_runtime, .. } => Some(node_runtime.clone()), - _ => None, - } - } - - pub fn local( - node_runtime: NodeRuntime, - fs: Arc, - project_environment: Entity, - http_client: Arc, - cx: &mut Context, - ) -> Self { - let subscription = cx.observe_global::(|this, cx| { - this.agent_servers_settings_changed(cx); - }); - let mut this = Self { - state: AgentServerStoreState::Local { - node_runtime, - fs, - project_environment, - http_client, - downstream_client: None, - settings: None, - extension_agents: vec![], - _subscriptions: [subscription], - }, - external_agents: Default::default(), - agent_icons: Default::default(), - agent_display_names: Default::default(), - }; - if let Some(_events) = extension::ExtensionEvents::try_global(cx) {} - this.agent_servers_settings_changed(cx); - this - } - - pub(crate) fn remote(project_id: u64, upstream_client: Entity) -> Self { - // Set up the builtin agents here so they're immediately available in - // remote projects--we know that the HeadlessProject on the other end - // will have them. - let external_agents: [(ExternalAgentServerName, Box); 3] = [ - ( - CLAUDE_CODE_NAME.into(), - Box::new(RemoteExternalAgentServer { - project_id, - upstream_client: upstream_client.clone(), - name: CLAUDE_CODE_NAME.into(), - status_tx: None, - new_version_available_tx: None, - }) as Box, - ), - ( - CODEX_NAME.into(), - Box::new(RemoteExternalAgentServer { - project_id, - upstream_client: upstream_client.clone(), - name: CODEX_NAME.into(), - status_tx: None, - new_version_available_tx: None, - }) as Box, - ), - ( - GEMINI_NAME.into(), - Box::new(RemoteExternalAgentServer { - project_id, - upstream_client: upstream_client.clone(), - name: GEMINI_NAME.into(), - status_tx: None, - new_version_available_tx: None, - }) as Box, - ), - ]; - - Self { - state: AgentServerStoreState::Remote { - project_id, - upstream_client, - }, - external_agents: external_agents.into_iter().collect(), - agent_icons: HashMap::default(), - agent_display_names: HashMap::default(), - } - } - - pub(crate) fn collab(_cx: &mut Context) -> Self { - Self { - state: AgentServerStoreState::Collab, - external_agents: Default::default(), - agent_icons: Default::default(), - agent_display_names: Default::default(), - } - } - - pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context) { - match &mut self.state { - AgentServerStoreState::Local { - downstream_client, .. - } => { - *downstream_client = Some((project_id, client.clone())); - // Send the current list of external agents downstream, but only after a delay, - // to avoid having the message arrive before the downstream project's agent server store - // sets up its handlers. - cx.spawn(async move |this, cx| { - cx.background_executor().timer(Duration::from_secs(1)).await; - let names = this.update(cx, |this, _| { - this.external_agents - .keys() - .map(|name| name.to_string()) - .collect() - })?; - client - .send(proto::ExternalAgentsUpdated { project_id, names }) - .log_err(); - anyhow::Ok(()) - }) - .detach(); - } - AgentServerStoreState::Remote { .. } => { - debug_panic!( - "external agents over collab not implemented, remote project should not be shared" - ); - } - AgentServerStoreState::Collab => { - debug_panic!("external agents over collab not implemented, should not be shared"); - } - } - } - - pub fn get_external_agent( - &mut self, - name: &ExternalAgentServerName, - ) -> Option<&mut (dyn ExternalAgentServer + 'static)> { - self.external_agents - .get_mut(name) - .map(|agent| agent.as_mut()) - } - - pub fn external_agents(&self) -> impl Iterator { - self.external_agents.keys() - } - - async fn handle_get_agent_server_command( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let (command, root_dir, login_command) = this - .update(&mut cx, |this, cx| { - let AgentServerStoreState::Local { - downstream_client, .. - } = &this.state - else { - debug_panic!("should not receive GetAgentServerCommand in a non-local project"); - bail!("unexpected GetAgentServerCommand request in a non-local project"); - }; - let agent = this - .external_agents - .get_mut(&*envelope.payload.name) - .with_context(|| format!("agent `{}` not found", envelope.payload.name))?; - let (status_tx, new_version_available_tx) = downstream_client - .clone() - .map(|(project_id, downstream_client)| { - let (status_tx, mut status_rx) = watch::channel(SharedString::from("")); - let (new_version_available_tx, mut new_version_available_rx) = - watch::channel(None); - cx.spawn({ - let downstream_client = downstream_client.clone(); - let name = envelope.payload.name.clone(); - async move |_, _| { - while let Some(status) = status_rx.recv().await.ok() { - downstream_client.send( - proto::ExternalAgentLoadingStatusUpdated { - project_id, - name: name.clone(), - status: status.to_string(), - }, - )?; - } - anyhow::Ok(()) - } - }) - .detach_and_log_err(cx); - cx.spawn({ - let name = envelope.payload.name.clone(); - async move |_, _| { - if let Some(version) = - new_version_available_rx.recv().await.ok().flatten() - { - downstream_client.send( - proto::NewExternalAgentVersionAvailable { - project_id, - name: name.clone(), - version, - }, - )?; - } - anyhow::Ok(()) - } - }) - .detach_and_log_err(cx); - (status_tx, new_version_available_tx) - }) - .unzip(); - anyhow::Ok(agent.get_command( - envelope.payload.root_dir.as_deref(), - HashMap::default(), - status_tx, - new_version_available_tx, - &mut cx.to_async(), - )) - })?? - .await?; - Ok(proto::AgentServerCommand { - path: command.path.to_string_lossy().into_owned(), - args: command.args, - env: command - .env - .map(|env| env.into_iter().collect()) - .unwrap_or_default(), - root_dir: root_dir, - login: login_command.map(|cmd| cmd.to_proto()), - }) - } - - async fn handle_external_agents_updated( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - let AgentServerStoreState::Remote { - project_id, - upstream_client, - } = &this.state - else { - debug_panic!( - "handle_external_agents_updated should not be called for a non-remote project" - ); - bail!("unexpected ExternalAgentsUpdated message") - }; - - let mut status_txs = this - .external_agents - .iter_mut() - .filter_map(|(name, agent)| { - Some(( - name.clone(), - agent - .downcast_mut::()? - .status_tx - .take(), - )) - }) - .collect::>(); - let mut new_version_available_txs = this - .external_agents - .iter_mut() - .filter_map(|(name, agent)| { - Some(( - name.clone(), - agent - .downcast_mut::()? - .new_version_available_tx - .take(), - )) - }) - .collect::>(); - - this.external_agents = envelope - .payload - .names - .into_iter() - .map(|name| { - let agent = RemoteExternalAgentServer { - project_id: *project_id, - upstream_client: upstream_client.clone(), - name: ExternalAgentServerName(name.clone().into()), - status_tx: status_txs.remove(&*name).flatten(), - new_version_available_tx: new_version_available_txs - .remove(&*name) - .flatten(), - }; - ( - ExternalAgentServerName(name.into()), - Box::new(agent) as Box, - ) - }) - .collect(); - cx.emit(AgentServersUpdated); - Ok(()) - })? - } - - async fn handle_external_extension_agents_updated( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - let AgentServerStoreState::Local { - extension_agents, .. - } = &mut this.state - else { - panic!( - "handle_external_extension_agents_updated \ - should not be called for a non-remote project" - ); - }; - - for ExternalExtensionAgent { - name, - icon_path, - extension_id, - targets, - env, - } in envelope.payload.agents - { - let icon_path_string = icon_path.clone(); - if let Some(icon_path) = icon_path { - this.agent_icons.insert( - ExternalAgentServerName(name.clone().into()), - icon_path.into(), - ); - } - extension_agents.push(( - Arc::from(&*name), - extension_id, - targets - .into_iter() - .map(|(k, v)| (k, extension::TargetConfig::from_proto(v))) - .collect(), - env.into_iter().collect(), - icon_path_string, - )); - } - - this.reregister_agents(cx); - cx.emit(AgentServersUpdated); - Ok(()) - })? - } - - async fn handle_loading_status_updated( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |this, _| { - if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name) - && let Some(agent) = agent.downcast_mut::() - && let Some(status_tx) = &mut agent.status_tx - { - status_tx.send(envelope.payload.status.into()).ok(); - } - }) - } - - async fn handle_new_version_available( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |this, _| { - if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name) - && let Some(agent) = agent.downcast_mut::() - && let Some(new_version_available_tx) = &mut agent.new_version_available_tx - { - new_version_available_tx - .send(Some(envelope.payload.version)) - .ok(); - } - }) - } - - pub fn get_extension_id_for_agent( - &mut self, - name: &ExternalAgentServerName, - ) -> Option> { - self.external_agents.get_mut(name).and_then(|agent| { - agent - .as_any_mut() - .downcast_ref::() - .map(|ext_agent| ext_agent.extension_id.clone()) - }) - } -} - -fn get_or_npm_install_builtin_agent( - binary_name: SharedString, - package_name: SharedString, - entrypoint_path: PathBuf, - minimum_version: Option, - status_tx: Option>, - new_version_available: Option>>, - fs: Arc, - node_runtime: NodeRuntime, - cx: &mut AsyncApp, -) -> Task> { - cx.spawn(async move |cx| { - let node_path = node_runtime.binary_path().await?; - let dir = paths::external_agents_dir().join(binary_name.as_str()); - fs.create_dir(&dir).await?; - - let mut stream = fs.read_dir(&dir).await?; - let mut versions = Vec::new(); - let mut to_delete = Vec::new(); - while let Some(entry) = stream.next().await { - let Ok(entry) = entry else { continue }; - let Some(file_name) = entry.file_name() else { - continue; - }; - - if let Some(name) = file_name.to_str() - && let Some(version) = semver::Version::from_str(name).ok() - && fs - .is_file(&dir.join(file_name).join(&entrypoint_path)) - .await - { - versions.push((version, file_name.to_owned())); - } else { - to_delete.push(file_name.to_owned()) - } - } - - versions.sort(); - let newest_version = if let Some((version, file_name)) = versions.last().cloned() - && minimum_version.is_none_or(|minimum_version| version >= minimum_version) - { - versions.pop(); - Some(file_name) - } else { - None - }; - log::debug!("existing version of {package_name}: {newest_version:?}"); - to_delete.extend(versions.into_iter().map(|(_, file_name)| file_name)); - - cx.background_spawn({ - let fs = fs.clone(); - let dir = dir.clone(); - async move { - for file_name in to_delete { - fs.remove_dir( - &dir.join(file_name), - RemoveOptions { - recursive: true, - ignore_if_not_exists: false, - }, - ) - .await - .ok(); - } - } - }) - .detach(); - - let version = if let Some(file_name) = newest_version { - cx.background_spawn({ - let file_name = file_name.clone(); - let dir = dir.clone(); - let fs = fs.clone(); - async move { - let latest_version = node_runtime - .npm_package_latest_version(&package_name) - .await - .ok(); - if let Some(latest_version) = latest_version - && &latest_version != &file_name.to_string_lossy() - { - let download_result = download_latest_version( - fs, - dir.clone(), - node_runtime, - package_name.clone(), - ) - .await - .log_err(); - if let Some(mut new_version_available) = new_version_available - && download_result.is_some() - { - new_version_available.send(Some(latest_version)).ok(); - } - } - } - }) - .detach(); - file_name - } else { - if let Some(mut status_tx) = status_tx { - status_tx.send("Installing…".into()).ok(); - } - let dir = dir.clone(); - cx.background_spawn(download_latest_version( - fs.clone(), - dir.clone(), - node_runtime, - package_name.clone(), - )) - .await? - .into() - }; - - let agent_server_path = dir.join(version).join(entrypoint_path); - let agent_server_path_exists = fs.is_file(&agent_server_path).await; - anyhow::ensure!( - agent_server_path_exists, - "Missing entrypoint path {} after installation", - agent_server_path.to_string_lossy() - ); - - anyhow::Ok(AgentServerCommand { - path: node_path, - args: vec![agent_server_path.to_string_lossy().into_owned()], - env: None, - }) - }) -} - -fn find_bin_in_path( - bin_name: SharedString, - root_dir: PathBuf, - env: HashMap, - cx: &mut AsyncApp, -) -> Task> { - cx.background_executor().spawn(async move { - let which_result = if cfg!(windows) { - which::which(bin_name.as_str()) - } else { - let shell_path = env.get("PATH").cloned(); - which::which_in(bin_name.as_str(), shell_path.as_ref(), &root_dir) - }; - - if let Err(which::Error::CannotFindBinaryPath) = which_result { - return None; - } - - which_result.log_err() - }) -} - -async fn download_latest_version( - fs: Arc, - dir: PathBuf, - node_runtime: NodeRuntime, - package_name: SharedString, -) -> Result { - log::debug!("downloading latest version of {package_name}"); - - let tmp_dir = tempfile::tempdir_in(&dir)?; - - node_runtime - .npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")]) - .await?; - - let version = node_runtime - .npm_package_installed_version(tmp_dir.path(), &package_name) - .await? - .context("expected package to be installed")?; - - fs.rename( - &tmp_dir.keep(), - &dir.join(&version), - RenameOptions { - ignore_if_exists: true, - overwrite: true, - create_parents: false, - }, - ) - .await?; - - anyhow::Ok(version) -} - -struct RemoteExternalAgentServer { - project_id: u64, - upstream_client: Entity, - name: ExternalAgentServerName, - status_tx: Option>, - new_version_available_tx: Option>>, -} - -impl ExternalAgentServer for RemoteExternalAgentServer { - fn get_command( - &mut self, - root_dir: Option<&str>, - extra_env: HashMap, - status_tx: Option>, - new_version_available_tx: Option>>, - cx: &mut AsyncApp, - ) -> Task)>> { - let project_id = self.project_id; - let name = self.name.to_string(); - let upstream_client = self.upstream_client.downgrade(); - let root_dir = root_dir.map(|root_dir| root_dir.to_owned()); - self.status_tx = status_tx; - self.new_version_available_tx = new_version_available_tx; - cx.spawn(async move |cx| { - let mut response = upstream_client - .update(cx, |upstream_client, _| { - upstream_client - .proto_client() - .request(proto::GetAgentServerCommand { - project_id, - name, - root_dir: root_dir.clone(), - }) - })? - .await?; - let root_dir = response.root_dir; - response.env.extend(extra_env); - let command = upstream_client.update(cx, |client, _| { - client.build_command( - Some(response.path), - &response.args, - &response.env.into_iter().collect(), - Some(root_dir.clone()), - None, - ) - })??; - Ok(( - AgentServerCommand { - path: command.program.into(), - args: command.args, - env: Some(command.env), - }, - root_dir, - response.login.map(SpawnInTerminal::from_proto), - )) - }) - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -struct LocalGemini { - fs: Arc, - node_runtime: NodeRuntime, - project_environment: Entity, - custom_command: Option, - ignore_system_version: bool, -} - -impl ExternalAgentServer for LocalGemini { - fn get_command( - &mut self, - root_dir: Option<&str>, - extra_env: HashMap, - status_tx: Option>, - new_version_available_tx: Option>>, - cx: &mut AsyncApp, - ) -> Task)>> { - let fs = self.fs.clone(); - let node_runtime = self.node_runtime.clone(); - let project_environment = self.project_environment.downgrade(); - let custom_command = self.custom_command.clone(); - let ignore_system_version = self.ignore_system_version; - let root_dir: Arc = root_dir - .map(|root_dir| Path::new(root_dir)) - .unwrap_or(paths::home_dir()) - .into(); - - cx.spawn(async move |cx| { - let mut env = project_environment - .update(cx, |project_environment, cx| { - project_environment.local_directory_environment( - &Shell::System, - root_dir.clone(), - cx, - ) - })? - .await - .unwrap_or_default(); - - let mut command = if let Some(mut custom_command) = custom_command { - env.extend(custom_command.env.unwrap_or_default()); - custom_command.env = Some(env); - custom_command - } else if !ignore_system_version - && let Some(bin) = - find_bin_in_path("gemini".into(), root_dir.to_path_buf(), env.clone(), cx).await - { - AgentServerCommand { - path: bin, - args: Vec::new(), - env: Some(env), - } - } else { - let mut command = get_or_npm_install_builtin_agent( - GEMINI_NAME.into(), - "@google/gemini-cli".into(), - "node_modules/@google/gemini-cli/dist/index.js".into(), - if cfg!(windows) { - // v0.8.x on Windows has a bug that causes the initialize request to hang forever - Some("0.9.0".parse().unwrap()) - } else { - Some("0.2.1".parse().unwrap()) - }, - status_tx, - new_version_available_tx, - fs, - node_runtime, - cx, - ) - .await?; - command.env = Some(env); - command - }; - - // Gemini CLI doesn't seem to have a dedicated invocation for logging in--we just run it normally without any arguments. - let login = task::SpawnInTerminal { - command: Some(command.path.to_string_lossy().into_owned()), - args: command.args.clone(), - env: command.env.clone().unwrap_or_default(), - label: "gemini /auth".into(), - ..Default::default() - }; - - command.env.get_or_insert_default().extend(extra_env); - command.args.push("--experimental-acp".into()); - Ok(( - command, - root_dir.to_string_lossy().into_owned(), - Some(login), - )) - }) - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -struct LocalClaudeCode { - fs: Arc, - node_runtime: NodeRuntime, - project_environment: Entity, - custom_command: Option, -} - -impl ExternalAgentServer for LocalClaudeCode { - fn get_command( - &mut self, - root_dir: Option<&str>, - extra_env: HashMap, - status_tx: Option>, - new_version_available_tx: Option>>, - cx: &mut AsyncApp, - ) -> Task)>> { - let fs = self.fs.clone(); - let node_runtime = self.node_runtime.clone(); - let project_environment = self.project_environment.downgrade(); - let custom_command = self.custom_command.clone(); - let root_dir: Arc = root_dir - .map(|root_dir| Path::new(root_dir)) - .unwrap_or(paths::home_dir()) - .into(); - - cx.spawn(async move |cx| { - let mut env = project_environment - .update(cx, |project_environment, cx| { - project_environment.local_directory_environment( - &Shell::System, - root_dir.clone(), - cx, - ) - })? - .await - .unwrap_or_default(); - env.insert("ANTHROPIC_API_KEY".into(), "".into()); - - let (mut command, login_command) = if let Some(mut custom_command) = custom_command { - env.extend(custom_command.env.unwrap_or_default()); - custom_command.env = Some(env); - (custom_command, None) - } else { - let mut command = get_or_npm_install_builtin_agent( - "claude-code-acp".into(), - "@zed-industries/claude-code-acp".into(), - "node_modules/@zed-industries/claude-code-acp/dist/index.js".into(), - Some("0.5.2".parse().unwrap()), - status_tx, - new_version_available_tx, - fs, - node_runtime, - cx, - ) - .await?; - command.env = Some(env); - let login = command - .args - .first() - .and_then(|path| { - path.strip_suffix("/@zed-industries/claude-code-acp/dist/index.js") - }) - .map(|path_prefix| task::SpawnInTerminal { - command: Some(command.path.to_string_lossy().into_owned()), - args: vec![ - Path::new(path_prefix) - .join("@anthropic-ai/claude-agent-sdk/cli.js") - .to_string_lossy() - .to_string(), - "/login".into(), - ], - env: command.env.clone().unwrap_or_default(), - label: "claude /login".into(), - ..Default::default() - }); - (command, login) - }; - - command.env.get_or_insert_default().extend(extra_env); - Ok(( - command, - root_dir.to_string_lossy().into_owned(), - login_command, - )) - }) - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -struct LocalCodex { - fs: Arc, - project_environment: Entity, - http_client: Arc, - custom_command: Option, - no_browser: bool, -} - -impl ExternalAgentServer for LocalCodex { - fn get_command( - &mut self, - root_dir: Option<&str>, - extra_env: HashMap, - mut status_tx: Option>, - _new_version_available_tx: Option>>, - cx: &mut AsyncApp, - ) -> Task)>> { - let fs = self.fs.clone(); - let project_environment = self.project_environment.downgrade(); - let http = self.http_client.clone(); - let custom_command = self.custom_command.clone(); - let root_dir: Arc = root_dir - .map(|root_dir| Path::new(root_dir)) - .unwrap_or(paths::home_dir()) - .into(); - let no_browser = self.no_browser; - - cx.spawn(async move |cx| { - let mut env = project_environment - .update(cx, |project_environment, cx| { - project_environment.local_directory_environment( - &Shell::System, - root_dir.clone(), - cx, - ) - })? - .await - .unwrap_or_default(); - if no_browser { - env.insert("NO_BROWSER".to_owned(), "1".to_owned()); - } - - let mut command = if let Some(mut custom_command) = custom_command { - env.extend(custom_command.env.unwrap_or_default()); - custom_command.env = Some(env); - custom_command - } else { - let dir = paths::external_agents_dir().join(CODEX_NAME); - fs.create_dir(&dir).await?; - - let bin_name = if cfg!(windows) { - "codex-acp.exe" - } else { - "codex-acp" - }; - - let find_latest_local_version = async || -> Option { - let mut local_versions: Vec<(semver::Version, String)> = Vec::new(); - let mut stream = fs.read_dir(&dir).await.ok()?; - while let Some(entry) = stream.next().await { - let Ok(entry) = entry else { continue }; - let Some(file_name) = entry.file_name() else { - continue; - }; - let version_path = dir.join(&file_name); - if fs.is_file(&version_path.join(bin_name)).await { - let version_str = file_name.to_string_lossy(); - if let Ok(version) = - semver::Version::from_str(version_str.trim_start_matches('v')) - { - local_versions.push((version, version_str.into_owned())); - } - } - } - local_versions.sort_by(|(a, _), (b, _)| a.cmp(b)); - local_versions.last().map(|(_, v)| dir.join(v)) - }; - - let fallback_to_latest_local_version = - async |err: anyhow::Error| -> Result { - if let Some(local) = find_latest_local_version().await { - log::info!( - "Falling back to locally installed Codex version: {}", - local.display() - ); - Ok(local) - } else { - Err(err) - } - }; - - let version_dir = match ::http_client::github::latest_github_release( - CODEX_ACP_REPO, - true, - false, - http.clone(), - ) - .await - { - Ok(release) => { - let version_dir = dir.join(&release.tag_name); - if !fs.is_dir(&version_dir).await { - if let Some(ref mut status_tx) = status_tx { - status_tx.send("Installing…".into()).ok(); - } - - let tag = release.tag_name.clone(); - let version_number = tag.trim_start_matches('v'); - let asset_name = asset_name(version_number) - .context("codex acp is not supported for this architecture")?; - let asset = release - .assets - .into_iter() - .find(|asset| asset.name == asset_name) - .with_context(|| { - format!("no asset found matching `{asset_name:?}`") - })?; - // Strip "sha256:" prefix from digest if present (GitHub API format) - let digest = asset - .digest - .as_deref() - .and_then(|d| d.strip_prefix("sha256:").or(Some(d))); - match ::http_client::github_download::download_server_binary( - &*http, - &asset.browser_download_url, - digest, - &version_dir, - if cfg!(target_os = "windows") && cfg!(target_arch = "x86_64") { - AssetKind::Zip - } else { - AssetKind::TarGz - }, - ) - .await - { - Ok(()) => { - // remove older versions - util::fs::remove_matching(&dir, |entry| entry != version_dir) - .await; - version_dir - } - Err(err) => { - log::error!( - "Failed to download Codex release {}: {err:#}", - release.tag_name - ); - fallback_to_latest_local_version(err).await? - } - } - } else { - version_dir - } - } - Err(err) => { - log::error!("Failed to fetch Codex latest release: {err:#}"); - fallback_to_latest_local_version(err).await? - } - }; - - let bin_path = version_dir.join(bin_name); - anyhow::ensure!( - fs.is_file(&bin_path).await, - "Missing Codex binary at {} after installation", - bin_path.to_string_lossy() - ); - - let mut cmd = AgentServerCommand { - path: bin_path, - args: Vec::new(), - env: None, - }; - cmd.env = Some(env); - cmd - }; - - command.env.get_or_insert_default().extend(extra_env); - Ok((command, root_dir.to_string_lossy().into_owned(), None)) - }) - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -pub const CODEX_ACP_REPO: &str = "zed-industries/codex-acp"; - -fn get_platform_info() -> Option<(&'static str, &'static str, &'static str)> { - let arch = if cfg!(target_arch = "x86_64") { - "x86_64" - } else if cfg!(target_arch = "aarch64") { - "aarch64" - } else { - return None; - }; - - let platform = if cfg!(target_os = "macos") { - "apple-darwin" - } else if cfg!(target_os = "windows") { - "pc-windows-msvc" - } else if cfg!(target_os = "linux") { - "unknown-linux-gnu" - } else { - return None; - }; - - // Windows uses .zip in release assets - let ext = if cfg!(target_os = "windows") { - "zip" - } else { - "tar.gz" - }; - - Some((arch, platform, ext)) -} - -fn asset_name(version: &str) -> Option { - let (arch, platform, ext) = get_platform_info()?; - Some(format!("codex-acp-{version}-{arch}-{platform}.{ext}")) -} - -struct LocalExtensionArchiveAgent { - fs: Arc, - http_client: Arc, - node_runtime: NodeRuntime, - project_environment: Entity, - extension_id: Arc, - agent_id: Arc, - targets: HashMap, - env: HashMap, -} - -struct LocalCustomAgent { - project_environment: Entity, - command: AgentServerCommand, -} - -impl ExternalAgentServer for LocalExtensionArchiveAgent { - fn get_command( - &mut self, - root_dir: Option<&str>, - extra_env: HashMap, - _status_tx: Option>, - _new_version_available_tx: Option>>, - cx: &mut AsyncApp, - ) -> Task)>> { - let fs = self.fs.clone(); - let http_client = self.http_client.clone(); - let node_runtime = self.node_runtime.clone(); - let project_environment = self.project_environment.downgrade(); - let extension_id = self.extension_id.clone(); - let agent_id = self.agent_id.clone(); - let targets = self.targets.clone(); - let base_env = self.env.clone(); - - let root_dir: Arc = root_dir - .map(|root_dir| Path::new(root_dir)) - .unwrap_or(paths::home_dir()) - .into(); - - cx.spawn(async move |cx| { - // Get project environment - let mut env = project_environment - .update(cx, |project_environment, cx| { - project_environment.local_directory_environment( - &Shell::System, - root_dir.clone(), - cx, - ) - })? - .await - .unwrap_or_default(); - - // Merge manifest env and extra env - env.extend(base_env); - env.extend(extra_env); - - let cache_key = format!("{}/{}", extension_id, agent_id); - let dir = paths::external_agents_dir().join(&cache_key); - fs.create_dir(&dir).await?; - - // Determine platform key - let os = if cfg!(target_os = "macos") { - "darwin" - } else if cfg!(target_os = "linux") { - "linux" - } else if cfg!(target_os = "windows") { - "windows" - } else { - anyhow::bail!("unsupported OS"); - }; - - let arch = if cfg!(target_arch = "aarch64") { - "aarch64" - } else if cfg!(target_arch = "x86_64") { - "x86_64" - } else { - anyhow::bail!("unsupported architecture"); - }; - - let platform_key = format!("{}-{}", os, arch); - let target_config = targets.get(&platform_key).with_context(|| { - format!( - "no target specified for platform '{}'. Available platforms: {}", - platform_key, - targets - .keys() - .map(|k| k.as_str()) - .collect::>() - .join(", ") - ) - })?; - - let archive_url = &target_config.archive; - - // Use URL as version identifier for caching - // Hash the URL to get a stable directory name - use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; - let mut hasher = DefaultHasher::new(); - archive_url.hash(&mut hasher); - let url_hash = hasher.finish(); - let version_dir = dir.join(format!("v_{:x}", url_hash)); - - if !fs.is_dir(&version_dir).await { - // Determine SHA256 for verification - let sha256 = if let Some(provided_sha) = &target_config.sha256 { - // Use provided SHA256 - Some(provided_sha.clone()) - } else if archive_url.starts_with("https://github.com/") { - // Try to fetch SHA256 from GitHub API - // Parse URL to extract repo and tag/file info - // Format: https://github.com/owner/repo/releases/download/tag/file.zip - if let Some(caps) = archive_url.strip_prefix("https://github.com/") { - let parts: Vec<&str> = caps.split('/').collect(); - if parts.len() >= 6 && parts[2] == "releases" && parts[3] == "download" { - let repo = format!("{}/{}", parts[0], parts[1]); - let tag = parts[4]; - let filename = parts[5..].join("/"); - - // Try to get release info from GitHub - if let Ok(release) = ::http_client::github::get_release_by_tag_name( - &repo, - tag, - http_client.clone(), - ) - .await - { - // Find matching asset - if let Some(asset) = - release.assets.iter().find(|a| a.name == filename) - { - // Strip "sha256:" prefix if present - asset.digest.as_ref().and_then(|d| { - d.strip_prefix("sha256:") - .map(|s| s.to_string()) - .or_else(|| Some(d.clone())) - }) - } else { - None - } - } else { - None - } - } else { - None - } - } else { - None - } - } else { - None - }; - - // Determine archive type from URL - let asset_kind = if archive_url.ends_with(".zip") { - AssetKind::Zip - } else if archive_url.ends_with(".tar.gz") || archive_url.ends_with(".tgz") { - AssetKind::TarGz - } else { - anyhow::bail!("unsupported archive type in URL: {}", archive_url); - }; - - // Download and extract - ::http_client::github_download::download_server_binary( - &*http_client, - archive_url, - sha256.as_deref(), - &version_dir, - asset_kind, - ) - .await?; - } - - // Validate and resolve cmd path - let cmd = &target_config.cmd; - - let cmd_path = if cmd == "node" { - // Use Zed's managed Node.js runtime - node_runtime.binary_path().await? - } else { - if cmd.contains("..") { - anyhow::bail!("command path cannot contain '..': {}", cmd); - } - - if cmd.starts_with("./") || cmd.starts_with(".\\") { - // Relative to extraction directory - let cmd_path = version_dir.join(&cmd[2..]); - anyhow::ensure!( - fs.is_file(&cmd_path).await, - "Missing command {} after extraction", - cmd_path.to_string_lossy() - ); - cmd_path - } else { - // On PATH - anyhow::bail!("command must be relative (start with './'): {}", cmd); - } - }; - - let command = AgentServerCommand { - path: cmd_path, - args: target_config.args.clone(), - env: Some(env), - }; - - Ok((command, version_dir.to_string_lossy().into_owned(), None)) - }) - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -impl ExternalAgentServer for LocalCustomAgent { - fn get_command( - &mut self, - root_dir: Option<&str>, - extra_env: HashMap, - _status_tx: Option>, - _new_version_available_tx: Option>>, - cx: &mut AsyncApp, - ) -> Task)>> { - let mut command = self.command.clone(); - let root_dir: Arc = root_dir - .map(|root_dir| Path::new(root_dir)) - .unwrap_or(paths::home_dir()) - .into(); - let project_environment = self.project_environment.downgrade(); - cx.spawn(async move |cx| { - let mut env = project_environment - .update(cx, |project_environment, cx| { - project_environment.local_directory_environment( - &Shell::System, - root_dir.clone(), - cx, - ) - })? - .await - .unwrap_or_default(); - env.extend(command.env.unwrap_or_default()); - env.extend(extra_env); - command.env = Some(env); - Ok((command, root_dir.to_string_lossy().into_owned(), None)) - }) - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -pub const GEMINI_NAME: &'static str = "gemini"; -pub const CLAUDE_CODE_NAME: &'static str = "claude"; -pub const CODEX_NAME: &'static str = "codex"; - -#[derive(Default, Clone, JsonSchema, Debug, PartialEq, RegisterSetting)] -pub struct AllAgentServersSettings { - pub gemini: Option, - pub claude: Option, - pub codex: Option, - pub custom: HashMap, -} -#[derive(Default, Clone, JsonSchema, Debug, PartialEq)] -pub struct BuiltinAgentServerSettings { - pub path: Option, - pub args: Option>, - pub env: Option>, - pub ignore_system_version: Option, - pub default_mode: Option, - pub default_model: Option, -} - -impl BuiltinAgentServerSettings { - pub(crate) fn custom_command(self) -> Option { - self.path.map(|path| AgentServerCommand { - path, - args: self.args.unwrap_or_default(), - env: self.env, - }) - } -} - -impl From for BuiltinAgentServerSettings { - fn from(value: settings::BuiltinAgentServerSettings) -> Self { - BuiltinAgentServerSettings { - path: value - .path - .map(|p| PathBuf::from(shellexpand::tilde(&p.to_string_lossy()).as_ref())), - args: value.args, - env: value.env, - ignore_system_version: value.ignore_system_version, - default_mode: value.default_mode, - default_model: value.default_model, - } - } -} - -impl From for BuiltinAgentServerSettings { - fn from(value: AgentServerCommand) -> Self { - BuiltinAgentServerSettings { - path: Some(value.path), - args: Some(value.args), - env: value.env, - ..Default::default() - } - } -} - -#[derive(Clone, JsonSchema, Debug, PartialEq)] -pub enum CustomAgentServerSettings { - Custom { - command: AgentServerCommand, - /// The default mode to use for this agent. - /// - /// Note: Not only all agents support modes. - /// - /// Default: None - default_mode: Option, - /// The default model to use for this agent. - /// - /// This should be the model ID as reported by the agent. - /// - /// Default: None - default_model: Option, - }, - Extension { - /// The default mode to use for this agent. - /// - /// Note: Not only all agents support modes. - /// - /// Default: None - default_mode: Option, - /// The default model to use for this agent. - /// - /// This should be the model ID as reported by the agent. - /// - /// Default: None - default_model: Option, - }, -} - -impl CustomAgentServerSettings { - pub fn command(&self) -> Option<&AgentServerCommand> { - match self { - CustomAgentServerSettings::Custom { command, .. } => Some(command), - CustomAgentServerSettings::Extension { .. } => None, - } - } - - pub fn default_mode(&self) -> Option<&str> { - match self { - CustomAgentServerSettings::Custom { default_mode, .. } - | CustomAgentServerSettings::Extension { default_mode, .. } => default_mode.as_deref(), - } - } - - pub fn default_model(&self) -> Option<&str> { - match self { - CustomAgentServerSettings::Custom { default_model, .. } - | CustomAgentServerSettings::Extension { default_model, .. } => { - default_model.as_deref() - } - } - } -} - -impl From for CustomAgentServerSettings { - fn from(value: settings::CustomAgentServerSettings) -> Self { - match value { - settings::CustomAgentServerSettings::Custom { - path, - args, - env, - default_mode, - default_model, - } => CustomAgentServerSettings::Custom { - command: AgentServerCommand { - path: PathBuf::from(shellexpand::tilde(&path.to_string_lossy()).as_ref()), - args, - env, - }, - default_mode, - default_model, - }, - settings::CustomAgentServerSettings::Extension { - default_mode, - default_model, - } => CustomAgentServerSettings::Extension { - default_mode, - default_model, - }, - } - } -} - -impl settings::Settings for AllAgentServersSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let agent_settings = content.agent_servers.clone().unwrap(); - Self { - gemini: agent_settings.gemini.map(Into::into), - claude: agent_settings.claude.map(Into::into), - codex: agent_settings.codex.map(Into::into), - custom: agent_settings - .custom - .into_iter() - .map(|(k, v)| (k, v.into())) - .collect(), - } - } -} - -#[cfg(test)] -mod extension_agent_tests { - use crate::worktree_store::WorktreeStore; - - use super::*; - use gpui::TestAppContext; - use std::sync::Arc; - - #[test] - fn extension_agent_constructs_proper_display_names() { - // Verify the display name format for extension-provided agents - let name1 = ExternalAgentServerName(SharedString::from("Extension: Agent")); - assert!(name1.0.contains(": ")); - - let name2 = ExternalAgentServerName(SharedString::from("MyExt: MyAgent")); - assert_eq!(name2.0, "MyExt: MyAgent"); - - // Non-extension agents shouldn't have the separator - let custom = ExternalAgentServerName(SharedString::from("custom")); - assert!(!custom.0.contains(": ")); - } - - struct NoopExternalAgent; - - impl ExternalAgentServer for NoopExternalAgent { - fn get_command( - &mut self, - _root_dir: Option<&str>, - _extra_env: HashMap, - _status_tx: Option>, - _new_version_available_tx: Option>>, - _cx: &mut AsyncApp, - ) -> Task)>> { - Task::ready(Ok(( - AgentServerCommand { - path: PathBuf::from("noop"), - args: Vec::new(), - env: None, - }, - "".to_string(), - None, - ))) - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - } - - #[test] - fn sync_removes_only_extension_provided_agents() { - let mut store = AgentServerStore { - state: AgentServerStoreState::Collab, - external_agents: HashMap::default(), - agent_icons: HashMap::default(), - agent_display_names: HashMap::default(), - }; - - // Seed with extension agents (contain ": ") and custom agents (don't contain ": ") - store.external_agents.insert( - ExternalAgentServerName(SharedString::from("Ext1: Agent1")), - Box::new(NoopExternalAgent) as Box, - ); - store.external_agents.insert( - ExternalAgentServerName(SharedString::from("Ext2: Agent2")), - Box::new(NoopExternalAgent) as Box, - ); - store.external_agents.insert( - ExternalAgentServerName(SharedString::from("custom-agent")), - Box::new(NoopExternalAgent) as Box, - ); - - // Simulate removal phase - let keys_to_remove: Vec<_> = store - .external_agents - .keys() - .filter(|name| name.0.contains(": ")) - .cloned() - .collect(); - - for key in keys_to_remove { - store.external_agents.remove(&key); - } - - // Only custom-agent should remain - assert_eq!(store.external_agents.len(), 1); - assert!( - store - .external_agents - .contains_key(&ExternalAgentServerName(SharedString::from("custom-agent"))) - ); - } - - #[test] - fn archive_launcher_constructs_with_all_fields() { - use extension::AgentServerManifestEntry; - - let mut env = HashMap::default(); - env.insert("GITHUB_TOKEN".into(), "secret".into()); - - let mut targets = HashMap::default(); - targets.insert( - "darwin-aarch64".to_string(), - extension::TargetConfig { - archive: - "https://github.com/owner/repo/releases/download/v1.0.0/agent-darwin-arm64.zip" - .into(), - cmd: "./agent".into(), - args: vec![], - sha256: None, - env: Default::default(), - }, - ); - - let _entry = AgentServerManifestEntry { - name: "GitHub Agent".into(), - targets, - env, - icon: None, - }; - - // Verify display name construction - let expected_name = ExternalAgentServerName(SharedString::from("GitHub Agent")); - assert_eq!(expected_name.0, "GitHub Agent"); - } - - #[gpui::test] - async fn archive_agent_uses_extension_and_agent_id_for_cache_key(cx: &mut TestAppContext) { - let fs = fs::FakeFs::new(cx.background_executor.clone()); - let http_client = http_client::FakeHttpClient::with_404_response(); - let worktree_store = cx.new(|_| WorktreeStore::local(false, fs.clone())); - let project_environment = cx.new(|cx| { - crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx) - }); - - let agent = LocalExtensionArchiveAgent { - fs, - http_client, - node_runtime: node_runtime::NodeRuntime::unavailable(), - project_environment, - extension_id: Arc::from("my-extension"), - agent_id: Arc::from("my-agent"), - targets: { - let mut map = HashMap::default(); - map.insert( - "darwin-aarch64".to_string(), - extension::TargetConfig { - archive: "https://example.com/my-agent-darwin-arm64.zip".into(), - cmd: "./my-agent".into(), - args: vec!["--serve".into()], - sha256: None, - env: Default::default(), - }, - ); - map - }, - env: { - let mut map = HashMap::default(); - map.insert("PORT".into(), "8080".into()); - map - }, - }; - - // Verify agent is properly constructed - assert_eq!(agent.extension_id.as_ref(), "my-extension"); - assert_eq!(agent.agent_id.as_ref(), "my-agent"); - assert_eq!(agent.env.get("PORT"), Some(&"8080".to_string())); - assert!(agent.targets.contains_key("darwin-aarch64")); - } - - #[test] - fn sync_extension_agents_registers_archive_launcher() { - use extension::AgentServerManifestEntry; - - let expected_name = ExternalAgentServerName(SharedString::from("Release Agent")); - assert_eq!(expected_name.0, "Release Agent"); - - // Verify the manifest entry structure for archive-based installation - let mut env = HashMap::default(); - env.insert("API_KEY".into(), "secret".into()); - - let mut targets = HashMap::default(); - targets.insert( - "linux-x86_64".to_string(), - extension::TargetConfig { - archive: "https://github.com/org/project/releases/download/v2.1.0/release-agent-linux-x64.tar.gz".into(), - cmd: "./release-agent".into(), - args: vec!["serve".into()], - sha256: None, - env: Default::default(), - }, - ); - - let manifest_entry = AgentServerManifestEntry { - name: "Release Agent".into(), - targets: targets.clone(), - env, - icon: None, - }; - - // Verify target config is present - assert!(manifest_entry.targets.contains_key("linux-x86_64")); - let target = manifest_entry.targets.get("linux-x86_64").unwrap(); - assert_eq!(target.cmd, "./release-agent"); - } - - #[gpui::test] - async fn test_node_command_uses_managed_runtime(cx: &mut TestAppContext) { - let fs = fs::FakeFs::new(cx.background_executor.clone()); - let http_client = http_client::FakeHttpClient::with_404_response(); - let node_runtime = NodeRuntime::unavailable(); - let worktree_store = cx.new(|_| WorktreeStore::local(false, fs.clone())); - let project_environment = cx.new(|cx| { - crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx) - }); - - let agent = LocalExtensionArchiveAgent { - fs: fs.clone(), - http_client, - node_runtime, - project_environment, - extension_id: Arc::from("node-extension"), - agent_id: Arc::from("node-agent"), - targets: { - let mut map = HashMap::default(); - map.insert( - "darwin-aarch64".to_string(), - extension::TargetConfig { - archive: "https://example.com/node-agent.zip".into(), - cmd: "node".into(), - args: vec!["index.js".into()], - sha256: None, - env: Default::default(), - }, - ); - map - }, - env: HashMap::default(), - }; - - // Verify that when cmd is "node", it attempts to use the node runtime - assert_eq!(agent.extension_id.as_ref(), "node-extension"); - assert_eq!(agent.agent_id.as_ref(), "node-agent"); - - let target = agent.targets.get("darwin-aarch64").unwrap(); - assert_eq!(target.cmd, "node"); - assert_eq!(target.args, vec!["index.js"]); - } - - #[gpui::test] - async fn test_commands_run_in_extraction_directory(cx: &mut TestAppContext) { - let fs = fs::FakeFs::new(cx.background_executor.clone()); - let http_client = http_client::FakeHttpClient::with_404_response(); - let node_runtime = NodeRuntime::unavailable(); - let worktree_store = cx.new(|_| WorktreeStore::local(false, fs.clone())); - let project_environment = cx.new(|cx| { - crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx) - }); - - let agent = LocalExtensionArchiveAgent { - fs: fs.clone(), - http_client, - node_runtime, - project_environment, - extension_id: Arc::from("test-ext"), - agent_id: Arc::from("test-agent"), - targets: { - let mut map = HashMap::default(); - map.insert( - "darwin-aarch64".to_string(), - extension::TargetConfig { - archive: "https://example.com/test.zip".into(), - cmd: "node".into(), - args: vec![ - "server.js".into(), - "--config".into(), - "./config.json".into(), - ], - sha256: None, - env: Default::default(), - }, - ); - map - }, - env: HashMap::default(), - }; - - // Verify the agent is configured with relative paths in args - let target = agent.targets.get("darwin-aarch64").unwrap(); - assert_eq!(target.args[0], "server.js"); - assert_eq!(target.args[2], "./config.json"); - // These relative paths will resolve relative to the extraction directory - // when the command is executed - } - - #[test] - fn test_tilde_expansion_in_settings() { - let settings = settings::BuiltinAgentServerSettings { - path: Some(PathBuf::from("~/bin/agent")), - args: Some(vec!["--flag".into()]), - env: None, - ignore_system_version: None, - default_mode: None, - default_model: None, - }; - - let BuiltinAgentServerSettings { path, .. } = settings.into(); - - let path = path.unwrap(); - assert!( - !path.to_string_lossy().starts_with("~"), - "Tilde should be expanded for builtin agent path" - ); - - let settings = settings::CustomAgentServerSettings::Custom { - path: PathBuf::from("~/custom/agent"), - args: vec!["serve".into()], - env: None, - default_mode: None, - default_model: None, - }; - - let converted: CustomAgentServerSettings = settings.into(); - let CustomAgentServerSettings::Custom { - command: AgentServerCommand { path, .. }, - .. - } = converted - else { - panic!("Expected Custom variant"); - }; - - assert!( - !path.to_string_lossy().starts_with("~"), - "Tilde should be expanded for custom agent path" - ); - } -} diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs deleted file mode 100644 index aea2482c83..0000000000 --- a/crates/project/src/buffer_store.rs +++ /dev/null @@ -1,1686 +0,0 @@ -use crate::{ - ProjectPath, - lsp_store::OpenLspBufferHandle, - worktree_store::{WorktreeStore, WorktreeStoreEvent}, -}; -use anyhow::{Context as _, Result, anyhow}; -use client::Client; -use collections::{HashMap, HashSet, hash_map}; -use futures::{Future, FutureExt as _, channel::oneshot, future::Shared}; -use gpui::{ - App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, -}; -use language::{ - Buffer, BufferEvent, Capability, DiskState, File as _, Language, Operation, - proto::{ - deserialize_line_ending, deserialize_version, serialize_line_ending, serialize_version, - split_operations, - }, -}; -use rpc::{ - AnyProtoClient, ErrorCode, ErrorExt as _, TypedEnvelope, - proto::{self}, -}; - -use std::{io, sync::Arc, time::Instant}; -use text::{BufferId, ReplicaId}; -use util::{ResultExt as _, TryFutureExt, debug_panic, maybe, rel_path::RelPath}; -use worktree::{File, PathChange, ProjectEntryId, Worktree, WorktreeId}; - -/// A set of open buffers. -pub struct BufferStore { - state: BufferStoreState, - #[allow(clippy::type_complexity)] - loading_buffers: HashMap, Arc>>>>, - worktree_store: Entity, - opened_buffers: HashMap, - path_to_buffer_id: HashMap, - downstream_client: Option<(AnyProtoClient, u64)>, - shared_buffers: HashMap>, - non_searchable_buffers: HashSet, -} - -#[derive(Hash, Eq, PartialEq, Clone)] -struct SharedBuffer { - buffer: Entity, - lsp_handle: Option, -} - -enum BufferStoreState { - Local(LocalBufferStore), - Remote(RemoteBufferStore), -} - -struct RemoteBufferStore { - shared_with_me: HashSet>, - upstream_client: AnyProtoClient, - project_id: u64, - loading_remote_buffers_by_id: HashMap>, - remote_buffer_listeners: - HashMap>>>>, - worktree_store: Entity, -} - -struct LocalBufferStore { - local_buffer_ids_by_entry_id: HashMap, - worktree_store: Entity, - _subscription: Subscription, -} - -enum OpenBuffer { - Complete { buffer: WeakEntity }, - Operations(Vec), -} - -pub enum BufferStoreEvent { - BufferAdded(Entity), - BufferOpened { - buffer: Entity, - project_path: ProjectPath, - }, - SharedBufferClosed(proto::PeerId, BufferId), - BufferDropped(BufferId), - BufferChangedFilePath { - buffer: Entity, - old_file: Option>, - }, -} - -#[derive(Default, Debug, Clone)] -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 { - pub fn wait_for_remote_buffer( - &mut self, - id: BufferId, - cx: &mut Context, - ) -> Task>> { - let (tx, rx) = oneshot::channel(); - self.remote_buffer_listeners.entry(id).or_default().push(tx); - - cx.spawn(async move |this, cx| { - if let Some(buffer) = this - .read_with(cx, |buffer_store, _| buffer_store.get(id)) - .ok() - .flatten() - { - return Ok(buffer); - } - - cx.background_spawn(async move { rx.await? }).await - }) - } - - fn save_remote_buffer( - &self, - buffer_handle: Entity, - new_path: Option, - cx: &Context, - ) -> Task> { - let buffer = buffer_handle.read(cx); - let buffer_id = buffer.remote_id().into(); - let version = buffer.version(); - let rpc = self.upstream_client.clone(); - let project_id = self.project_id; - cx.spawn(async move |_, cx| { - let response = rpc - .request(proto::SaveBuffer { - project_id, - buffer_id, - new_path, - version: serialize_version(&version), - }) - .await?; - let version = deserialize_version(&response.version); - let mtime = response.mtime.map(|mtime| mtime.into()); - - buffer_handle.update(cx, |buffer, cx| { - buffer.did_save(version.clone(), mtime, cx); - })?; - - Ok(()) - }) - } - - pub fn handle_create_buffer_for_peer( - &mut self, - envelope: TypedEnvelope, - replica_id: ReplicaId, - capability: Capability, - cx: &mut Context, - ) -> Result>> { - match envelope.payload.variant.context("missing variant")? { - proto::create_buffer_for_peer::Variant::State(mut state) => { - let buffer_id = BufferId::new(state.id)?; - - let buffer_result = maybe!({ - let mut buffer_file = None; - if let Some(file) = state.file.take() { - let worktree_id = worktree::WorktreeId::from_proto(file.worktree_id); - let worktree = self - .worktree_store - .read(cx) - .worktree_for_id(worktree_id, cx) - .with_context(|| { - format!("no worktree found for id {}", file.worktree_id) - })?; - buffer_file = Some(Arc::new(File::from_proto(file, worktree, cx)?) - as Arc); - } - Buffer::from_proto(replica_id, capability, state, buffer_file) - }); - - match buffer_result { - Ok(buffer) => { - let buffer = cx.new(|_| buffer); - self.loading_remote_buffers_by_id.insert(buffer_id, buffer); - } - Err(error) => { - if let Some(listeners) = self.remote_buffer_listeners.remove(&buffer_id) { - for listener in listeners { - listener.send(Err(anyhow!(error.cloned()))).ok(); - } - } - } - } - } - proto::create_buffer_for_peer::Variant::Chunk(chunk) => { - let buffer_id = BufferId::new(chunk.buffer_id)?; - let buffer = self - .loading_remote_buffers_by_id - .get(&buffer_id) - .cloned() - .with_context(|| { - format!( - "received chunk for buffer {} without initial state", - chunk.buffer_id - ) - })?; - - let result = maybe!({ - let operations = chunk - .operations - .into_iter() - .map(language::proto::deserialize_operation) - .collect::>>()?; - buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx)); - anyhow::Ok(()) - }); - - if let Err(error) = result { - self.loading_remote_buffers_by_id.remove(&buffer_id); - if let Some(listeners) = self.remote_buffer_listeners.remove(&buffer_id) { - for listener in listeners { - listener.send(Err(error.cloned())).ok(); - } - } - } else if chunk.is_last { - self.loading_remote_buffers_by_id.remove(&buffer_id); - if self.upstream_client.is_via_collab() { - // retain buffers sent by peers to avoid races. - self.shared_with_me.insert(buffer.clone()); - } - - if let Some(senders) = self.remote_buffer_listeners.remove(&buffer_id) { - for sender in senders { - sender.send(Ok(buffer.clone())).ok(); - } - } - return Ok(Some(buffer)); - } - } - } - Ok(None) - } - - pub fn incomplete_buffer_ids(&self) -> Vec { - self.loading_remote_buffers_by_id - .keys() - .copied() - .collect::>() - } - - pub fn deserialize_project_transaction( - &self, - message: proto::ProjectTransaction, - push_to_history: bool, - cx: &mut Context, - ) -> Task> { - cx.spawn(async move |this, cx| { - let mut project_transaction = ProjectTransaction::default(); - for (buffer_id, transaction) in message.buffer_ids.into_iter().zip(message.transactions) - { - let buffer_id = BufferId::new(buffer_id)?; - let buffer = this - .update(cx, |this, cx| this.wait_for_remote_buffer(buffer_id, cx))? - .await?; - let transaction = language::proto::deserialize_transaction(transaction)?; - project_transaction.0.insert(buffer, transaction); - } - - for (buffer, transaction) in &project_transaction.0 { - buffer - .update(cx, |buffer, _| { - buffer.wait_for_edits(transaction.edit_ids.iter().copied()) - })? - .await?; - - if push_to_history { - buffer.update(cx, |buffer, _| { - buffer.push_transaction(transaction.clone(), Instant::now()); - buffer.finalize_last_transaction(); - })?; - } - } - - Ok(project_transaction) - }) - } - - fn open_buffer( - &self, - path: Arc, - worktree: Entity, - cx: &mut Context, - ) -> Task>> { - let worktree_id = worktree.read(cx).id().to_proto(); - let project_id = self.project_id; - let client = self.upstream_client.clone(); - cx.spawn(async move |this, cx| { - let response = client - .request(proto::OpenBufferByPath { - project_id, - worktree_id, - path: path.to_proto(), - }) - .await?; - let buffer_id = BufferId::new(response.buffer_id)?; - - let buffer = this - .update(cx, { - |this, cx| this.wait_for_remote_buffer(buffer_id, cx) - })? - .await?; - - Ok(buffer) - }) - } - - fn create_buffer( - &self, - project_searchable: bool, - cx: &mut Context, - ) -> Task>> { - let create = self.upstream_client.request(proto::OpenNewBuffer { - project_id: self.project_id, - }); - cx.spawn(async move |this, cx| { - let response = create.await?; - let buffer_id = BufferId::new(response.buffer_id)?; - - this.update(cx, |this, cx| { - if !project_searchable { - this.non_searchable_buffers.insert(buffer_id); - } - this.wait_for_remote_buffer(buffer_id, cx) - })? - .await - }) - } - - fn reload_buffers( - &self, - buffers: HashSet>, - push_to_history: bool, - cx: &mut Context, - ) -> Task> { - let request = self.upstream_client.request(proto::ReloadBuffers { - project_id: self.project_id, - buffer_ids: buffers - .iter() - .map(|buffer| buffer.read(cx).remote_id().to_proto()) - .collect(), - }); - - cx.spawn(async move |this, cx| { - let response = request.await?.transaction.context("missing transaction")?; - this.update(cx, |this, cx| { - this.deserialize_project_transaction(response, push_to_history, cx) - })? - .await - }) - } -} - -impl LocalBufferStore { - fn save_local_buffer( - &self, - buffer_handle: Entity, - worktree: Entity, - path: Arc, - mut has_changed_file: bool, - cx: &mut Context, - ) -> Task> { - let buffer = buffer_handle.read(cx); - - let text = buffer.as_rope().clone(); - let line_ending = buffer.line_ending(); - let version = buffer.version(); - let buffer_id = buffer.remote_id(); - let file = buffer.file().cloned(); - if file - .as_ref() - .is_some_and(|file| file.disk_state() == DiskState::New) - { - has_changed_file = true; - } - - let save = worktree.update(cx, |worktree, cx| { - worktree.write_file(path, text, line_ending, cx) - }); - - cx.spawn(async move |this, cx| { - let new_file = save.await?; - let mtime = new_file.disk_state().mtime(); - this.update(cx, |this, cx| { - if let Some((downstream_client, project_id)) = this.downstream_client.clone() { - if has_changed_file { - downstream_client - .send(proto::UpdateBufferFile { - project_id, - buffer_id: buffer_id.to_proto(), - file: Some(language::File::to_proto(&*new_file, cx)), - }) - .log_err(); - } - downstream_client - .send(proto::BufferSaved { - project_id, - buffer_id: buffer_id.to_proto(), - version: serialize_version(&version), - mtime: mtime.map(|time| time.into()), - }) - .log_err(); - } - })?; - buffer_handle.update(cx, |buffer, cx| { - if has_changed_file { - buffer.file_updated(new_file, cx); - } - buffer.did_save(version.clone(), mtime, cx); - }) - }) - } - - 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 - { - Self::local_worktree_entries_changed(this, &worktree, changes, cx); - } - }) - .detach(); - } - - fn local_worktree_entries_changed( - this: &mut BufferStore, - worktree_handle: &Entity, - changes: &[(Arc, ProjectEntryId, PathChange)], - cx: &mut Context, - ) { - let snapshot = worktree_handle.read(cx).snapshot(); - for (path, entry_id, _) in changes { - Self::local_worktree_entry_changed( - this, - *entry_id, - path, - worktree_handle, - &snapshot, - cx, - ); - } - } - - fn local_worktree_entry_changed( - this: &mut BufferStore, - entry_id: ProjectEntryId, - path: &Arc, - worktree: &Entity, - snapshot: &worktree::Snapshot, - cx: &mut Context, - ) -> Option<()> { - let project_path = ProjectPath { - worktree_id: snapshot.id(), - path: path.clone(), - }; - - let buffer_id = this - .as_local_mut() - .and_then(|local| local.local_buffer_ids_by_entry_id.get(&entry_id)) - .copied() - .or_else(|| this.path_to_buffer_id.get(&project_path).copied())?; - - let buffer = if let Some(buffer) = this.get(buffer_id) { - Some(buffer) - } else { - this.opened_buffers.remove(&buffer_id); - this.non_searchable_buffers.remove(&buffer_id); - None - }; - - let buffer = if let Some(buffer) = buffer { - buffer - } else { - this.path_to_buffer_id.remove(&project_path); - let this = this.as_local_mut()?; - this.local_buffer_ids_by_entry_id.remove(&entry_id); - return None; - }; - - let events = buffer.update(cx, |buffer, cx| { - let file = buffer.file()?; - let old_file = File::from_dyn(Some(file))?; - if old_file.worktree != *worktree { - return None; - } - - let snapshot_entry = old_file - .entry_id - .and_then(|entry_id| snapshot.entry_for_id(entry_id)) - .or_else(|| snapshot.entry_for_path(old_file.path.as_ref())); - - let new_file = if let Some(entry) = snapshot_entry { - File { - disk_state: match entry.mtime { - Some(mtime) => DiskState::Present { mtime }, - None => old_file.disk_state, - }, - is_local: true, - entry_id: Some(entry.id), - path: entry.path.clone(), - worktree: worktree.clone(), - is_private: entry.is_private, - } - } else { - File { - disk_state: DiskState::Deleted, - is_local: true, - entry_id: old_file.entry_id, - path: old_file.path.clone(), - worktree: worktree.clone(), - is_private: old_file.is_private, - } - }; - - if new_file == *old_file { - return None; - } - - let mut events = Vec::new(); - if new_file.path != old_file.path { - this.path_to_buffer_id.remove(&ProjectPath { - path: old_file.path.clone(), - worktree_id: old_file.worktree_id(cx), - }); - this.path_to_buffer_id.insert( - ProjectPath { - worktree_id: new_file.worktree_id(cx), - path: new_file.path.clone(), - }, - buffer_id, - ); - events.push(BufferStoreEvent::BufferChangedFilePath { - buffer: cx.entity(), - old_file: buffer.file().cloned(), - }); - } - let local = this.as_local_mut()?; - if new_file.entry_id != old_file.entry_id { - if let Some(entry_id) = old_file.entry_id { - local.local_buffer_ids_by_entry_id.remove(&entry_id); - } - if let Some(entry_id) = new_file.entry_id { - local - .local_buffer_ids_by_entry_id - .insert(entry_id, buffer_id); - } - } - - if let Some((client, project_id)) = &this.downstream_client { - client - .send(proto::UpdateBufferFile { - project_id: *project_id, - buffer_id: buffer_id.to_proto(), - file: Some(new_file.to_proto(cx)), - }) - .ok(); - } - - buffer.file_updated(Arc::new(new_file), cx); - Some(events) - })?; - - for event in events { - cx.emit(event); - } - - None - } - - fn save_buffer( - &self, - buffer: Entity, - cx: &mut Context, - ) -> Task> { - let Some(file) = File::from_dyn(buffer.read(cx).file()) else { - return Task::ready(Err(anyhow!("buffer doesn't have a file"))); - }; - let worktree = file.worktree.clone(); - self.save_local_buffer(buffer, worktree, file.path.clone(), false, cx) - } - - fn save_buffer_as( - &self, - buffer: Entity, - path: ProjectPath, - cx: &mut Context, - ) -> Task> { - let Some(worktree) = self - .worktree_store - .read(cx) - .worktree_for_id(path.worktree_id, cx) - else { - return Task::ready(Err(anyhow!("no such worktree"))); - }; - self.save_local_buffer(buffer, worktree, path.path, true, cx) - } - - fn open_buffer( - &self, - path: Arc, - worktree: Entity, - cx: &mut Context, - ) -> Task>> { - let load_file = worktree.update(cx, |worktree, cx| worktree.load_file(path.as_ref(), cx)); - cx.spawn(async move |this, cx| { - let path = path.clone(); - let buffer = match load_file.await { - Ok(loaded) => { - let reservation = cx.reserve_entity::()?; - let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64()); - let text_buffer = cx - .background_spawn(async move { - text::Buffer::new(ReplicaId::LOCAL, buffer_id, loaded.text) - }) - .await; - cx.insert_entity(reservation, |_| { - Buffer::build(text_buffer, Some(loaded.file), Capability::ReadWrite) - })? - } - Err(error) if is_not_found_error(&error) => cx.new(|cx| { - let buffer_id = BufferId::from(cx.entity_id().as_non_zero_u64()); - let text_buffer = text::Buffer::new(ReplicaId::LOCAL, buffer_id, ""); - Buffer::build( - text_buffer, - Some(Arc::new(File { - worktree, - path, - disk_state: DiskState::New, - entry_id: None, - is_local: true, - is_private: false, - })), - Capability::ReadWrite, - ) - })?, - Err(e) => return Err(e), - }; - this.update(cx, |this, cx| { - this.add_buffer(buffer.clone(), cx)?; - let buffer_id = buffer.read(cx).remote_id(); - if let Some(file) = File::from_dyn(buffer.read(cx).file()) { - this.path_to_buffer_id.insert( - ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }, - buffer_id, - ); - let this = this.as_local_mut().unwrap(); - if let Some(entry_id) = file.entry_id { - this.local_buffer_ids_by_entry_id - .insert(entry_id, buffer_id); - } - } - - anyhow::Ok(()) - })??; - - Ok(buffer) - }) - } - - fn create_buffer( - &self, - project_searchable: bool, - cx: &mut Context, - ) -> Task>> { - cx.spawn(async move |buffer_store, cx| { - let buffer = - cx.new(|cx| Buffer::local("", cx).with_language(language::PLAIN_TEXT.clone(), cx))?; - buffer_store.update(cx, |buffer_store, cx| { - buffer_store.add_buffer(buffer.clone(), cx).log_err(); - if !project_searchable { - buffer_store - .non_searchable_buffers - .insert(buffer.read(cx).remote_id()); - } - })?; - Ok(buffer) - }) - } - - fn reload_buffers( - &self, - buffers: HashSet>, - push_to_history: bool, - cx: &mut Context, - ) -> Task> { - cx.spawn(async move |_, cx| { - let mut project_transaction = ProjectTransaction::default(); - for buffer in buffers { - let transaction = buffer.update(cx, |buffer, cx| buffer.reload(cx))?.await?; - buffer.update(cx, |buffer, cx| { - if let Some(transaction) = transaction { - if !push_to_history { - buffer.forget_transaction(transaction.id); - } - project_transaction.0.insert(cx.entity(), transaction); - } - })?; - } - - Ok(project_transaction) - }) - } -} - -impl BufferStore { - pub fn init(client: &AnyProtoClient) { - client.add_entity_message_handler(Self::handle_buffer_reloaded); - client.add_entity_message_handler(Self::handle_buffer_saved); - client.add_entity_message_handler(Self::handle_update_buffer_file); - client.add_entity_request_handler(Self::handle_save_buffer); - client.add_entity_request_handler(Self::handle_reload_buffers); - } - - /// Creates a buffer store, optionally retaining its buffers. - pub fn local(worktree_store: Entity, cx: &mut Context) -> Self { - Self { - state: BufferStoreState::Local(LocalBufferStore { - local_buffer_ids_by_entry_id: Default::default(), - worktree_store: worktree_store.clone(), - _subscription: cx.subscribe(&worktree_store, |this, _, event, cx| { - if let WorktreeStoreEvent::WorktreeAdded(worktree) = event { - let this = this.as_local_mut().unwrap(); - this.subscribe_to_worktree(worktree, cx); - } - }), - }), - downstream_client: None, - opened_buffers: Default::default(), - path_to_buffer_id: Default::default(), - shared_buffers: Default::default(), - loading_buffers: Default::default(), - non_searchable_buffers: Default::default(), - worktree_store, - } - } - - pub fn remote( - worktree_store: Entity, - upstream_client: AnyProtoClient, - remote_id: u64, - _cx: &mut Context, - ) -> Self { - Self { - state: BufferStoreState::Remote(RemoteBufferStore { - shared_with_me: Default::default(), - loading_remote_buffers_by_id: Default::default(), - remote_buffer_listeners: Default::default(), - project_id: remote_id, - upstream_client, - worktree_store: worktree_store.clone(), - }), - downstream_client: None, - opened_buffers: Default::default(), - path_to_buffer_id: Default::default(), - loading_buffers: Default::default(), - shared_buffers: Default::default(), - non_searchable_buffers: Default::default(), - worktree_store, - } - } - - fn as_local_mut(&mut self) -> Option<&mut LocalBufferStore> { - match &mut self.state { - BufferStoreState::Local(state) => Some(state), - _ => None, - } - } - - fn as_remote_mut(&mut self) -> Option<&mut RemoteBufferStore> { - match &mut self.state { - BufferStoreState::Remote(state) => Some(state), - _ => None, - } - } - - fn as_remote(&self) -> Option<&RemoteBufferStore> { - match &self.state { - BufferStoreState::Remote(state) => Some(state), - _ => None, - } - } - - pub fn open_buffer( - &mut self, - project_path: ProjectPath, - cx: &mut Context, - ) -> Task>> { - if let Some(buffer) = self.get_by_path(&project_path) { - cx.emit(BufferStoreEvent::BufferOpened { - buffer: buffer.clone(), - project_path, - }); - - return Task::ready(Ok(buffer)); - } - - let task = match self.loading_buffers.entry(project_path.clone()) { - hash_map::Entry::Occupied(e) => e.get().clone(), - hash_map::Entry::Vacant(entry) => { - let path = project_path.path.clone(); - let Some(worktree) = self - .worktree_store - .read(cx) - .worktree_for_id(project_path.worktree_id, cx) - else { - return Task::ready(Err(anyhow!("no such worktree"))); - }; - let load_buffer = match &self.state { - BufferStoreState::Local(this) => this.open_buffer(path, worktree, cx), - BufferStoreState::Remote(this) => this.open_buffer(path, worktree, cx), - }; - - entry - .insert( - // todo(lw): hot foreground spawn - cx.spawn(async move |this, cx| { - let load_result = load_buffer.await; - this.update(cx, |this, cx| { - // Record the fact that the buffer is no longer loading. - this.loading_buffers.remove(&project_path); - - let buffer = load_result.map_err(Arc::new)?; - cx.emit(BufferStoreEvent::BufferOpened { - buffer: buffer.clone(), - project_path, - }); - - Ok(buffer) - })? - }) - .shared(), - ) - .clone() - } - }; - - cx.background_spawn(async move { - task.await.map_err(|e| { - if e.error_code() != ErrorCode::Internal { - anyhow!(e.error_code()) - } else { - anyhow!("{e}") - } - }) - }) - } - - pub fn create_buffer( - &mut self, - project_searchable: bool, - cx: &mut Context, - ) -> Task>> { - match &self.state { - BufferStoreState::Local(this) => this.create_buffer(project_searchable, cx), - BufferStoreState::Remote(this) => this.create_buffer(project_searchable, cx), - } - } - - pub fn save_buffer( - &mut self, - buffer: Entity, - cx: &mut Context, - ) -> Task> { - match &mut self.state { - BufferStoreState::Local(this) => this.save_buffer(buffer, cx), - BufferStoreState::Remote(this) => this.save_remote_buffer(buffer, None, cx), - } - } - - pub fn save_buffer_as( - &mut self, - buffer: Entity, - path: ProjectPath, - cx: &mut Context, - ) -> Task> { - let old_file = buffer.read(cx).file().cloned(); - let task = match &self.state { - BufferStoreState::Local(this) => this.save_buffer_as(buffer.clone(), path, cx), - BufferStoreState::Remote(this) => { - this.save_remote_buffer(buffer.clone(), Some(path.to_proto()), cx) - } - }; - cx.spawn(async move |this, cx| { - task.await?; - this.update(cx, |this, cx| { - old_file.clone().and_then(|file| { - this.path_to_buffer_id.remove(&ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path().clone(), - }) - }); - - cx.emit(BufferStoreEvent::BufferChangedFilePath { buffer, old_file }); - }) - }) - } - - fn add_buffer(&mut self, buffer_entity: Entity, cx: &mut Context) -> Result<()> { - let buffer = buffer_entity.read(cx); - let remote_id = buffer.remote_id(); - let path = File::from_dyn(buffer.file()).map(|file| ProjectPath { - path: file.path.clone(), - worktree_id: file.worktree_id(cx), - }); - let is_remote = buffer.replica_id().is_remote(); - let open_buffer = OpenBuffer::Complete { - buffer: buffer_entity.downgrade(), - }; - - let handle = cx.entity().downgrade(); - buffer_entity.update(cx, move |_, cx| { - cx.on_release(move |buffer, cx| { - handle - .update(cx, |_, cx| { - cx.emit(BufferStoreEvent::BufferDropped(buffer.remote_id())) - }) - .ok(); - }) - .detach() - }); - let _expect_path_to_exist; - match self.opened_buffers.entry(remote_id) { - hash_map::Entry::Vacant(entry) => { - entry.insert(open_buffer); - _expect_path_to_exist = false; - } - hash_map::Entry::Occupied(mut entry) => { - if let OpenBuffer::Operations(operations) = entry.get_mut() { - buffer_entity.update(cx, |b, cx| b.apply_ops(operations.drain(..), cx)); - } else if entry.get().upgrade().is_some() { - if is_remote { - return Ok(()); - } else { - debug_panic!("buffer {remote_id} was already registered"); - anyhow::bail!("buffer {remote_id} was already registered"); - } - } - entry.insert(open_buffer); - _expect_path_to_exist = true; - } - } - - if let Some(path) = path { - self.path_to_buffer_id.insert(path, remote_id); - } - - cx.subscribe(&buffer_entity, Self::on_buffer_event).detach(); - cx.emit(BufferStoreEvent::BufferAdded(buffer_entity)); - Ok(()) - } - - pub fn buffers(&self) -> impl '_ + Iterator> { - self.opened_buffers - .values() - .filter_map(|buffer| buffer.upgrade()) - } - - pub(crate) fn is_searchable(&self, id: &BufferId) -> bool { - !self.non_searchable_buffers.contains(&id) - } - - pub fn loading_buffers( - &self, - ) -> impl Iterator>>)> { - self.loading_buffers.iter().map(|(path, task)| { - let task = task.clone(); - (path, async move { - task.await.map_err(|e| { - if e.error_code() != ErrorCode::Internal { - anyhow!(e.error_code()) - } else { - anyhow!("{e}") - } - }) - }) - }) - } - - pub fn buffer_id_for_project_path(&self, project_path: &ProjectPath) -> Option<&BufferId> { - self.path_to_buffer_id.get(project_path) - } - - pub fn get_by_path(&self, path: &ProjectPath) -> Option> { - self.path_to_buffer_id - .get(path) - .and_then(|buffer_id| self.get(*buffer_id)) - } - - pub fn get(&self, buffer_id: BufferId) -> Option> { - self.opened_buffers.get(&buffer_id)?.upgrade() - } - - pub fn get_existing(&self, buffer_id: BufferId) -> Result> { - self.get(buffer_id) - .with_context(|| format!("unknown buffer id {buffer_id}")) - } - - pub fn get_possibly_incomplete(&self, buffer_id: BufferId) -> Option> { - self.get(buffer_id).or_else(|| { - self.as_remote() - .and_then(|remote| remote.loading_remote_buffers_by_id.get(&buffer_id).cloned()) - }) - } - - pub fn buffer_version_info(&self, cx: &App) -> (Vec, Vec) { - let buffers = self - .buffers() - .map(|buffer| { - let buffer = buffer.read(cx); - proto::BufferVersion { - id: buffer.remote_id().into(), - version: language::proto::serialize_version(&buffer.version), - } - }) - .collect(); - let incomplete_buffer_ids = self - .as_remote() - .map(|remote| remote.incomplete_buffer_ids()) - .unwrap_or_default(); - (buffers, incomplete_buffer_ids) - } - - pub fn disconnected_from_host(&mut self, cx: &mut App) { - for open_buffer in self.opened_buffers.values_mut() { - if let Some(buffer) = open_buffer.upgrade() { - buffer.update(cx, |buffer, _| buffer.give_up_waiting()); - } - } - - for buffer in self.buffers() { - buffer.update(cx, |buffer, cx| { - buffer.set_capability(Capability::ReadOnly, cx) - }); - } - - if let Some(remote) = self.as_remote_mut() { - // Wake up all futures currently waiting on a buffer to get opened, - // to give them a chance to fail now that we've disconnected. - remote.remote_buffer_listeners.clear() - } - } - - pub fn shared(&mut self, remote_id: u64, downstream_client: AnyProtoClient, _cx: &mut App) { - self.downstream_client = Some((downstream_client, remote_id)); - } - - pub fn unshared(&mut self, _cx: &mut Context) { - self.downstream_client.take(); - self.forget_shared_buffers(); - } - - pub fn discard_incomplete(&mut self) { - self.opened_buffers - .retain(|_, buffer| !matches!(buffer, OpenBuffer::Operations(_))); - } - - fn buffer_changed_file(&mut self, buffer: Entity, cx: &mut App) -> Option<()> { - let file = File::from_dyn(buffer.read(cx).file())?; - - let remote_id = buffer.read(cx).remote_id(); - if let Some(entry_id) = file.entry_id { - if let Some(local) = self.as_local_mut() { - match local.local_buffer_ids_by_entry_id.get(&entry_id) { - Some(_) => { - return None; - } - None => { - local - .local_buffer_ids_by_entry_id - .insert(entry_id, remote_id); - } - } - } - self.path_to_buffer_id.insert( - ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }, - remote_id, - ); - }; - - Some(()) - } - - fn on_buffer_event( - &mut self, - buffer: Entity, - event: &BufferEvent, - cx: &mut Context, - ) { - match event { - BufferEvent::FileHandleChanged => { - self.buffer_changed_file(buffer, cx); - } - BufferEvent::Reloaded => { - let Some((downstream_client, project_id)) = self.downstream_client.as_ref() else { - return; - }; - let buffer = buffer.read(cx); - downstream_client - .send(proto::BufferReloaded { - project_id: *project_id, - buffer_id: buffer.remote_id().to_proto(), - version: serialize_version(&buffer.version()), - mtime: buffer.saved_mtime().map(|t| t.into()), - line_ending: serialize_line_ending(buffer.line_ending()) as i32, - }) - .log_err(); - } - BufferEvent::LanguageChanged(_) => {} - _ => {} - } - } - - pub async fn handle_update_buffer( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let payload = envelope.payload; - let buffer_id = BufferId::new(payload.buffer_id)?; - let ops = payload - .operations - .into_iter() - .map(language::proto::deserialize_operation) - .collect::, _>>()?; - this.update(&mut cx, |this, cx| { - match this.opened_buffers.entry(buffer_id) { - hash_map::Entry::Occupied(mut e) => match e.get_mut() { - OpenBuffer::Operations(operations) => operations.extend_from_slice(&ops), - OpenBuffer::Complete { buffer, .. } => { - if let Some(buffer) = buffer.upgrade() { - buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx)); - } - } - }, - hash_map::Entry::Vacant(e) => { - e.insert(OpenBuffer::Operations(ops)); - } - } - Ok(proto::Ack {}) - })? - } - - pub fn register_shared_lsp_handle( - &mut self, - peer_id: proto::PeerId, - 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; - } - debug_panic!("tried to register shared lsp handle, but buffer was not shared") - } - - pub fn handle_synchronize_buffers( - &mut self, - envelope: TypedEnvelope, - cx: &mut Context, - client: Arc, - ) -> Result { - let project_id = envelope.payload.project_id; - let mut response = proto::SynchronizeBuffersResponse { - buffers: Default::default(), - }; - let Some(guest_id) = envelope.original_sender_id else { - anyhow::bail!("missing original_sender_id on SynchronizeBuffers request"); - }; - - self.shared_buffers.entry(guest_id).or_default().clear(); - for buffer in envelope.payload.buffers { - let buffer_id = BufferId::new(buffer.id)?; - let remote_version = language::proto::deserialize_version(&buffer.version); - if let Some(buffer) = self.get(buffer_id) { - self.shared_buffers - .entry(guest_id) - .or_default() - .entry(buffer_id) - .or_insert_with(|| SharedBuffer { - buffer: buffer.clone(), - lsp_handle: None, - }); - - let buffer = buffer.read(cx); - response.buffers.push(proto::BufferVersion { - id: buffer_id.into(), - version: language::proto::serialize_version(&buffer.version), - }); - - let operations = buffer.serialize_ops(Some(remote_version), cx); - let client = client.clone(); - if let Some(file) = buffer.file() { - client - .send(proto::UpdateBufferFile { - project_id, - buffer_id: buffer_id.into(), - file: Some(file.to_proto(cx)), - }) - .log_err(); - } - - // TODO(max): do something - // client - // .send(proto::UpdateStagedText { - // project_id, - // buffer_id: buffer_id.into(), - // diff_base: buffer.diff_base().map(ToString::to_string), - // }) - // .log_err(); - - client - .send(proto::BufferReloaded { - project_id, - buffer_id: buffer_id.into(), - version: language::proto::serialize_version(buffer.saved_version()), - mtime: buffer.saved_mtime().map(|time| time.into()), - line_ending: language::proto::serialize_line_ending(buffer.line_ending()) - as i32, - }) - .log_err(); - - cx.background_spawn( - async move { - let operations = operations.await; - for chunk in split_operations(operations) { - client - .request(proto::UpdateBuffer { - project_id, - buffer_id: buffer_id.into(), - operations: chunk, - }) - .await?; - } - anyhow::Ok(()) - } - .log_err(), - ) - .detach(); - } - } - Ok(response) - } - - pub fn handle_create_buffer_for_peer( - &mut self, - envelope: TypedEnvelope, - replica_id: ReplicaId, - capability: Capability, - cx: &mut Context, - ) -> Result<()> { - let remote = self - .as_remote_mut() - .context("buffer store is not a remote")?; - - if let Some(buffer) = - remote.handle_create_buffer_for_peer(envelope, replica_id, capability, cx)? - { - self.add_buffer(buffer, cx)?; - } - - Ok(()) - } - - pub async fn handle_update_buffer_file( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let buffer_id = envelope.payload.buffer_id; - let buffer_id = BufferId::new(buffer_id)?; - - this.update(&mut cx, |this, cx| { - let payload = envelope.payload.clone(); - if let Some(buffer) = this.get_possibly_incomplete(buffer_id) { - let file = payload.file.context("invalid file")?; - let worktree = this - .worktree_store - .read(cx) - .worktree_for_id(WorktreeId::from_proto(file.worktree_id), cx) - .context("no such worktree")?; - let file = File::from_proto(file, worktree, cx)?; - let old_file = buffer.update(cx, |buffer, cx| { - let old_file = buffer.file().cloned(); - 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) { - Some(old_file) - } else { - None - } - }); - if let Some(old_file) = old_file { - cx.emit(BufferStoreEvent::BufferChangedFilePath { buffer, old_file }); - } - } - if let Some((downstream_client, project_id)) = this.downstream_client.as_ref() { - downstream_client - .send(proto::UpdateBufferFile { - project_id: *project_id, - buffer_id: buffer_id.into(), - file: envelope.payload.file, - }) - .log_err(); - } - Ok(()) - })? - } - - pub async fn handle_save_buffer( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let (buffer, project_id) = this.read_with(&cx, |this, _| { - anyhow::Ok(( - this.get_existing(buffer_id)?, - this.downstream_client - .as_ref() - .map(|(_, project_id)| *project_id) - .context("project is not shared")?, - )) - })??; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&envelope.payload.version)) - })? - .await?; - let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id())?; - - if let Some(new_path) = envelope.payload.new_path - && let Some(new_path) = ProjectPath::from_proto(new_path) - { - this.update(&mut cx, |this, cx| { - this.save_buffer_as(buffer.clone(), new_path, cx) - })? - .await?; - } else { - this.update(&mut cx, |this, cx| this.save_buffer(buffer.clone(), cx))? - .await?; - } - - buffer.read_with(&cx, |buffer, _| proto::BufferSaved { - project_id, - buffer_id: buffer_id.into(), - version: serialize_version(buffer.saved_version()), - mtime: buffer.saved_mtime().map(|time| time.into()), - }) - } - - pub async fn handle_close_buffer( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - 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); - } - return; - } - debug_panic!( - "peer_id {} closed buffer_id {} which was either not open or already closed", - peer_id, - buffer_id - ) - }) - } - - pub async fn handle_buffer_saved( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let version = deserialize_version(&envelope.payload.version); - let mtime = envelope.payload.mtime.clone().map(|time| time.into()); - this.update(&mut cx, move |this, cx| { - if let Some(buffer) = this.get_possibly_incomplete(buffer_id) { - buffer.update(cx, |buffer, cx| { - buffer.did_save(version, mtime, cx); - }); - } - - if let Some((downstream_client, project_id)) = this.downstream_client.as_ref() { - downstream_client - .send(proto::BufferSaved { - project_id: *project_id, - buffer_id: buffer_id.into(), - mtime: envelope.payload.mtime, - version: envelope.payload.version, - }) - .log_err(); - } - }) - } - - pub async fn handle_buffer_reloaded( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let version = deserialize_version(&envelope.payload.version); - let mtime = envelope.payload.mtime.clone().map(|time| time.into()); - let line_ending = deserialize_line_ending( - proto::LineEnding::from_i32(envelope.payload.line_ending) - .context("missing line ending")?, - ); - this.update(&mut cx, |this, cx| { - if let Some(buffer) = this.get_possibly_incomplete(buffer_id) { - buffer.update(cx, |buffer, cx| { - buffer.did_reload(version, line_ending, mtime, cx); - }); - } - - if let Some((downstream_client, project_id)) = this.downstream_client.as_ref() { - downstream_client - .send(proto::BufferReloaded { - project_id: *project_id, - buffer_id: buffer_id.into(), - mtime: envelope.payload.mtime, - version: envelope.payload.version, - line_ending: envelope.payload.line_ending, - }) - .log_err(); - } - }) - } - - pub fn reload_buffers( - &self, - buffers: HashSet>, - push_to_history: bool, - cx: &mut Context, - ) -> Task> { - if buffers.is_empty() { - return Task::ready(Ok(ProjectTransaction::default())); - } - match &self.state { - BufferStoreState::Local(this) => this.reload_buffers(buffers, push_to_history, cx), - BufferStoreState::Remote(this) => this.reload_buffers(buffers, push_to_history, cx), - } - } - - async fn handle_reload_buffers( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let sender_id = envelope.original_sender_id().unwrap_or_default(); - let reload = this.update(&mut cx, |this, cx| { - let mut buffers = HashSet::default(); - for buffer_id in &envelope.payload.buffer_ids { - let buffer_id = BufferId::new(*buffer_id)?; - buffers.insert(this.get_existing(buffer_id)?); - } - anyhow::Ok(this.reload_buffers(buffers, false, cx)) - })??; - - let project_transaction = reload.await?; - let project_transaction = this.update(&mut cx, |this, cx| { - this.serialize_project_transaction_for_peer(project_transaction, sender_id, cx) - })?; - Ok(proto::ReloadBuffersResponse { - transaction: Some(project_transaction), - }) - } - - pub fn create_buffer_for_peer( - &mut self, - buffer: &Entity, - peer_id: proto::PeerId, - cx: &mut Context, - ) -> Task> { - let buffer_id = buffer.read(cx).remote_id(); - let shared_buffers = self.shared_buffers.entry(peer_id).or_default(); - if shared_buffers.contains_key(&buffer_id) { - return Task::ready(Ok(())); - } - shared_buffers.insert( - buffer_id, - SharedBuffer { - buffer: buffer.clone(), - lsp_handle: None, - }, - ); - - let Some((client, project_id)) = self.downstream_client.clone() else { - return Task::ready(Ok(())); - }; - - cx.spawn(async move |this, cx| { - let Some(buffer) = this.read_with(cx, |this, _| this.get(buffer_id))? else { - return anyhow::Ok(()); - }; - - let operations = buffer.update(cx, |b, cx| b.serialize_ops(None, cx))?; - let operations = operations.await; - let state = buffer.update(cx, |buffer, cx| buffer.to_proto(cx))?; - - let initial_state = proto::CreateBufferForPeer { - project_id, - peer_id: Some(peer_id), - variant: Some(proto::create_buffer_for_peer::Variant::State(state)), - }; - - if client.send(initial_state).log_err().is_some() { - let client = client.clone(); - cx.background_spawn(async move { - let mut chunks = split_operations(operations).peekable(); - while let Some(chunk) = chunks.next() { - let is_last = chunks.peek().is_none(); - client.send(proto::CreateBufferForPeer { - project_id, - peer_id: Some(peer_id), - variant: Some(proto::create_buffer_for_peer::Variant::Chunk( - proto::BufferChunk { - buffer_id: buffer_id.into(), - operations: chunk, - is_last, - }, - )), - })?; - } - anyhow::Ok(()) - }) - .await - .log_err(); - } - Ok(()) - }) - } - - pub fn forget_shared_buffers(&mut self) { - self.shared_buffers.clear(); - } - - pub fn forget_shared_buffers_for(&mut self, peer_id: &proto::PeerId) { - self.shared_buffers.remove(peer_id); - } - - pub fn update_peer_id(&mut self, old_peer_id: &proto::PeerId, new_peer_id: proto::PeerId) { - if let Some(buffers) = self.shared_buffers.remove(old_peer_id) { - self.shared_buffers.insert(new_peer_id, buffers); - } - } - - pub fn has_shared_buffers(&self) -> bool { - !self.shared_buffers.is_empty() - } - - pub fn create_local_buffer( - &mut self, - text: &str, - language: Option>, - project_searchable: bool, - cx: &mut Context, - ) -> Entity { - let buffer = cx.new(|cx| { - Buffer::local(text, cx) - .with_language(language.unwrap_or_else(|| language::PLAIN_TEXT.clone()), cx) - }); - - self.add_buffer(buffer.clone(), cx).log_err(); - let buffer_id = buffer.read(cx).remote_id(); - if !project_searchable { - self.non_searchable_buffers.insert(buffer_id); - } - - if let Some(file) = File::from_dyn(buffer.read(cx).file()) { - self.path_to_buffer_id.insert( - ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }, - buffer_id, - ); - let this = self - .as_local_mut() - .expect("local-only method called in a non-local context"); - if let Some(entry_id) = file.entry_id { - this.local_buffer_ids_by_entry_id - .insert(entry_id, buffer_id); - } - } - buffer - } - - pub fn deserialize_project_transaction( - &mut self, - message: proto::ProjectTransaction, - push_to_history: bool, - cx: &mut Context, - ) -> Task> { - if let Some(this) = self.as_remote_mut() { - this.deserialize_project_transaction(message, push_to_history, cx) - } else { - debug_panic!("not a remote buffer store"); - Task::ready(Err(anyhow!("not a remote buffer store"))) - } - } - - pub fn wait_for_remote_buffer( - &mut self, - id: BufferId, - cx: &mut Context, - ) -> Task>> { - if let Some(this) = self.as_remote_mut() { - this.wait_for_remote_buffer(id, cx) - } else { - debug_panic!("not a remote buffer store"); - Task::ready(Err(anyhow!("not a remote buffer store"))) - } - } - - pub fn serialize_project_transaction_for_peer( - &mut self, - project_transaction: ProjectTransaction, - peer_id: proto::PeerId, - cx: &mut Context, - ) -> proto::ProjectTransaction { - let mut serialized_transaction = proto::ProjectTransaction { - buffer_ids: Default::default(), - transactions: Default::default(), - }; - for (buffer, transaction) in project_transaction.0 { - self.create_buffer_for_peer(&buffer, peer_id, cx) - .detach_and_log_err(cx); - serialized_transaction - .buffer_ids - .push(buffer.read(cx).remote_id().into()); - serialized_transaction - .transactions - .push(language::proto::serialize_transaction(&transaction)); - } - serialized_transaction - } -} - -impl OpenBuffer { - fn upgrade(&self) -> Option> { - match self { - OpenBuffer::Complete { buffer, .. } => buffer.upgrade(), - OpenBuffer::Operations(_) => None, - } - } -} - -fn is_not_found_error(error: &anyhow::Error) -> bool { - error - .root_cause() - .downcast_ref::() - .is_some_and(|err| err.kind() == io::ErrorKind::NotFound) -} diff --git a/crates/project/src/color_extractor.rs b/crates/project/src/color_extractor.rs deleted file mode 100644 index 6e9907e30b..0000000000 --- a/crates/project/src/color_extractor.rs +++ /dev/null @@ -1,295 +0,0 @@ -use std::sync::LazyLock; - -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*\)"#; - -static RELAXED_HEX_REGEX: LazyLock = LazyLock::new(|| { - RegexBuilder::new(HEX) - .case_insensitive(false) - .build() - .expect("Failed to create RELAXED_HEX_REGEX") -}); - -static STRICT_HEX_REGEX: LazyLock = LazyLock::new(|| { - RegexBuilder::new(&format!("^{HEX}$")) - .case_insensitive(true) - .build() - .expect("Failed to create STRICT_HEX_REGEX") -}); - -static RELAXED_RGB_OR_HSL_REGEX: LazyLock = LazyLock::new(|| { - RegexBuilder::new(RGB_OR_HSL) - .case_insensitive(false) - .build() - .expect("Failed to create RELAXED_RGB_OR_HSL_REGEX") -}); - -static STRICT_RGB_OR_HSL_REGEX: LazyLock = LazyLock::new(|| { - RegexBuilder::new(&format!("^{RGB_OR_HSL}$")) - .case_insensitive(true) - .build() - .expect("Failed to create STRICT_RGB_OR_HSL_REGEX") -}); - -/// Extracts a color from an LSP [`CompletionItem`]. -/// -/// Adapted from https://github.com/microsoft/vscode/blob/a6870fcb6d79093738c17e8319b760cf1c41764a/src/vs/editor/contrib/suggest/browser/suggestWidgetRenderer.ts#L34-L61 -pub fn extract_color(item: &CompletionItem) -> Option { - // Try to extract from entire `label` field. - parse(&item.label, ParseMode::Strict) - // Try to extract from entire `detail` field. - .or_else(|| { - item.detail - .as_ref() - .and_then(|detail| parse(detail, ParseMode::Strict)) - }) - // Try to extract from beginning or end of `documentation` field. - .or_else(|| match item.documentation { - Some(Documentation::String(ref str)) => parse(str, ParseMode::Relaxed), - Some(Documentation::MarkupContent(ref markup)) => { - parse(&markup.value, ParseMode::Relaxed) - } - None => None, - }) -} - -enum ParseMode { - Strict, - Relaxed, -} - -fn parse(str: &str, mode: ParseMode) -> Option { - let (hex, rgb) = match mode { - ParseMode::Strict => (&STRICT_HEX_REGEX, &STRICT_RGB_OR_HSL_REGEX), - ParseMode::Relaxed => (&RELAXED_HEX_REGEX, &RELAXED_RGB_OR_HSL_REGEX), - }; - - if let Some(captures) = hex.captures(str) { - let rmatch = captures.get(0)?; - - // Color must be anchored to start or end of string. - if rmatch.start() > 0 && rmatch.end() != str.len() { - return None; - } - - let hex = captures.get(1)?.as_str(); - - return from_hex(hex); - } - - if let Some(captures) = rgb.captures(str) { - let rmatch = captures.get(0)?; - - // Color must be anchored to start or end of string. - if rmatch.start() > 0 && rmatch.end() != str.len() { - return None; - } - - let typ = captures.get(1)?.as_str(); - let r_or_h = captures.get(2)?.as_str(); - let g_or_s = captures.get(3)?.as_str(); - let b_or_l = captures.get(4)?.as_str(); - let a = captures.get(5).map(|a| a.as_str()); - - return match (typ, a) { - ("rgb", None) | ("rgba", Some(_)) => from_rgb(r_or_h, g_or_s, b_or_l, a), - ("hsl", None) | ("hsla", Some(_)) => from_hsl(r_or_h, g_or_s, b_or_l, a), - _ => None, - }; - } - - None -} - -fn parse_component(value: &str, max: f32) -> Option { - if let Some(field) = value.strip_suffix("%") { - field.parse::().map(|value| value / 100.).ok() - } else { - value.parse::().map(|value| value / max).ok() - } -} - -fn from_hex(hex: &str) -> Option { - Rgba::try_from(hex).map(Hsla::from).ok() -} - -fn from_rgb(r: &str, g: &str, b: &str, a: Option<&str>) -> Option { - let r = parse_component(r, 255.)?; - let g = parse_component(g, 255.)?; - let b = parse_component(b, 255.)?; - let a = a.and_then(|a| parse_component(a, 1.0)).unwrap_or(1.0); - - Some(Rgba { r, g, b, a }.into()) -} - -fn from_hsl(h: &str, s: &str, l: &str, a: Option<&str>) -> Option { - let h = parse_component(h, 360.)?; - let s = parse_component(s, 100.)?; - let l = parse_component(l, 100.)?; - let a = a.and_then(|a| parse_component(a, 1.0)).unwrap_or(1.0); - - Some(Hsla { h, s, l, a }) -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::rgba; - use lsp::{CompletionItem, CompletionItemKind}; - - pub const COLOR_TABLE: &[(&str, Option)] = &[ - // -- Invalid -- - // Invalid hex - ("f0f", None), - ("#fof", None), - // Extra field - ("rgb(255, 0, 0, 0.0)", None), - ("hsl(120, 0, 0, 0.0)", None), - // Missing field - ("rgba(255, 0, 0)", None), - ("hsla(120, 0, 0)", None), - // No decimal after zero - ("rgba(255, 0, 0, 0)", None), - ("hsla(120, 0, 0, 0)", None), - // Decimal after one - ("rgba(255, 0, 0, 1.0)", None), - ("hsla(120, 0, 0, 1.0)", None), - // HEX (sRGB) - ("#f0f", Some(0xFF00FFFF)), - ("#ff0000", Some(0xFF0000FF)), - // RGB / RGBA (sRGB) - ("rgb(255, 0, 0)", Some(0xFF0000FF)), - ("rgba(255, 0, 0, 0.4)", Some(0xFF000066)), - ("rgba(255, 0, 0, 1)", Some(0xFF0000FF)), - ("rgb(20%, 0%, 0%)", Some(0x330000FF)), - ("rgba(20%, 0%, 0%, 1)", Some(0x330000FF)), - ("rgb(0%, 20%, 0%)", Some(0x003300FF)), - ("rgba(0%, 20%, 0%, 1)", Some(0x003300FF)), - ("rgb(0%, 0%, 20%)", Some(0x000033FF)), - ("rgba(0%, 0%, 20%, 1)", Some(0x000033FF)), - // HSL / HSLA (sRGB) - ("hsl(0, 100%, 50%)", Some(0xFF0000FF)), - ("hsl(120, 100%, 50%)", Some(0x00FF00FF)), - ("hsla(0, 100%, 50%, 0.0)", Some(0xFF000000)), - ("hsla(0, 100%, 50%, 0.4)", Some(0xFF000066)), - ("hsla(0, 100%, 50%, 1)", Some(0xFF0000FF)), - ("hsla(120, 100%, 50%, 0.0)", Some(0x00FF0000)), - ("hsla(120, 100%, 50%, 0.4)", Some(0x00FF0066)), - ("hsla(120, 100%, 50%, 1)", Some(0x00FF00FF)), - ]; - - #[test] - fn can_extract_from_label() { - for (color_str, color_val) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: color_str.to_string(), - detail: None, - documentation: None, - ..Default::default() - }); - - assert_eq!(color, color_val.map(|v| Hsla::from(rgba(v)))); - } - } - - #[test] - fn only_whole_label_matches_are_allowed() { - for (color_str, _) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: format!("{} foo", color_str).to_string(), - detail: None, - documentation: None, - ..Default::default() - }); - - assert_eq!(color, None); - } - } - - #[test] - fn can_extract_from_detail() { - for (color_str, color_val) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: "".to_string(), - detail: Some(color_str.to_string()), - documentation: None, - ..Default::default() - }); - - assert_eq!(color, color_val.map(|v| Hsla::from(rgba(v)))); - } - } - - #[test] - fn only_whole_detail_matches_are_allowed() { - for (color_str, _) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: "".to_string(), - detail: Some(format!("{} foo", color_str).to_string()), - documentation: None, - ..Default::default() - }); - - assert_eq!(color, None); - } - } - - #[test] - fn can_extract_from_documentation_start() { - for (color_str, color_val) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: "".to_string(), - detail: None, - documentation: Some(Documentation::String( - format!("{} foo", color_str).to_string(), - )), - ..Default::default() - }); - - assert_eq!(color, color_val.map(|v| Hsla::from(rgba(v)))); - } - } - - #[test] - fn can_extract_from_documentation_end() { - for (color_str, color_val) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: "".to_string(), - detail: None, - documentation: Some(Documentation::String( - format!("foo {}", color_str).to_string(), - )), - ..Default::default() - }); - - assert_eq!(color, color_val.map(|v| Hsla::from(rgba(v)))); - } - } - - #[test] - fn cannot_extract_from_documentation_middle() { - for (color_str, _) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: "".to_string(), - detail: None, - documentation: Some(Documentation::String( - format!("foo {} foo", color_str).to_string(), - )), - ..Default::default() - }); - - assert_eq!(color, None); - } - } -} diff --git a/crates/project/src/connection_manager.rs b/crates/project/src/connection_manager.rs deleted file mode 100644 index 253d5d32a1..0000000000 --- a/crates/project/src/connection_manager.rs +++ /dev/null @@ -1,223 +0,0 @@ -use super::Project; -use anyhow::Result; -use client::Client; -use collections::{HashMap, HashSet}; -use futures::{FutureExt, StreamExt}; -use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Global, Task, WeakEntity}; -use postage::stream::Stream; -use rpc::proto; -use std::{sync::Arc, time::Duration}; -use util::ResultExt; - -impl Global for GlobalManager {} -struct GlobalManager(Entity); - -pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); - -pub struct Manager { - client: Arc, - maintain_connection: Option>>, - projects: HashSet>, -} - -pub fn init(client: Arc, cx: &mut App) { - let manager = cx.new(|_| Manager { - client, - maintain_connection: None, - projects: HashSet::default(), - }); - cx.set_global(GlobalManager(manager)); -} - -impl Manager { - pub fn global(cx: &App) -> Entity { - cx.global::().0.clone() - } - - pub fn maintain_project_connection( - &mut self, - project: &Entity, - cx: &mut Context, - ) { - let manager = cx.weak_entity(); - project.update(cx, |_, cx| { - let manager = manager.clone(); - cx.on_release(move |project, cx| { - manager - .update(cx, |manager, cx| { - manager.projects.retain(|p| { - if let Some(p) = p.upgrade() { - p.read(cx).remote_id() != project.remote_id() - } else { - false - } - }); - if manager.projects.is_empty() { - manager.maintain_connection.take(); - } - }) - .ok(); - }) - .detach(); - }); - - self.projects.insert(project.downgrade()); - if self.maintain_connection.is_none() { - self.maintain_connection = Some(cx.spawn({ - let client = self.client.clone(); - async move |_, cx| { - Self::maintain_connection(manager, client.clone(), cx) - .await - .log_err() - } - })); - } - } - - fn reconnected(&mut self, cx: &mut Context) -> Task> { - let mut projects = HashMap::default(); - - let request = self.client.request_envelope(proto::RejoinRemoteProjects { - rejoined_projects: self - .projects - .iter() - .filter_map(|project| { - if let Some(handle) = project.upgrade() { - let project = handle.read(cx); - let project_id = project.remote_id()?; - projects.insert(project_id, handle.clone()); - let mut worktrees = Vec::new(); - let mut repositories = Vec::new(); - for (id, repository) in project.repositories(cx) { - repositories.push(proto::RejoinRepository { - id: id.to_proto(), - scan_id: repository.read(cx).scan_id, - }); - } - for worktree in project.worktrees(cx) { - let worktree = worktree.read(cx); - worktrees.push(proto::RejoinWorktree { - id: worktree.id().to_proto(), - scan_id: worktree.completed_scan_id() as u64, - }); - } - Some(proto::RejoinProject { - id: project_id, - worktrees, - repositories, - }) - } else { - None - } - }) - .collect(), - }); - - cx.spawn(async move |this, cx| { - let response = request.await?; - let message_id = response.message_id; - - this.update(cx, |_, cx| { - for rejoined_project in response.payload.rejoined_projects { - if let Some(project) = projects.get(&rejoined_project.id) { - project.update(cx, |project, cx| { - project.rejoined(rejoined_project, message_id, cx).log_err(); - }); - } - } - }) - }) - } - - fn connection_lost(&mut self, cx: &mut Context) { - for project in self.projects.drain() { - if let Some(project) = project.upgrade() { - project.update(cx, |project, cx| { - project.disconnected_from_host(cx); - project.close(cx); - }); - } - } - self.maintain_connection.take(); - } - - async fn maintain_connection( - this: WeakEntity, - client: Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - let mut client_status = client.status(); - loop { - let _ = client_status.try_recv(); - - let is_connected = client_status.borrow().is_connected(); - // Even if we're initially connected, any future change of the status means we momentarily disconnected. - if !is_connected || client_status.next().await.is_some() { - log::info!("detected client disconnection"); - - // Wait for client to re-establish a connection to the server. - { - let mut reconnection_timeout = - cx.background_executor().timer(RECONNECT_TIMEOUT).fuse(); - let client_reconnection = async { - let mut remaining_attempts = 3; - while remaining_attempts > 0 { - if client_status.borrow().is_connected() { - log::info!("client reconnected, attempting to rejoin projects"); - - let Some(this) = this.upgrade() else { break }; - match this.update(cx, |this, cx| this.reconnected(cx)) { - Ok(task) => { - if task.await.log_err().is_some() { - return true; - } else { - remaining_attempts -= 1; - } - } - Err(_app_dropped) => return false, - } - } else if client_status.borrow().is_signed_out() { - return false; - } - - log::info!( - "waiting for client status change, remaining attempts {}", - remaining_attempts - ); - client_status.next().await; - } - false - } - .fuse(); - futures::pin_mut!(client_reconnection); - - futures::select_biased! { - reconnected = client_reconnection => { - if reconnected { - log::info!("successfully reconnected"); - // If we successfully joined the room, go back around the loop - // waiting for future connection status changes. - continue; - } - } - _ = reconnection_timeout => { - log::info!("rejoin project reconnection timeout expired"); - } - } - } - - break; - } - } - - // The client failed to re-establish a connection to the server - // or an error occurred while trying to re-join the room. Either way - // we leave the room and return an error. - if let Some(this) = this.upgrade() { - log::info!("reconnection failed, disconnecting projects"); - this.update(cx, |this, cx| this.connection_lost(cx))?; - } - - Ok(()) - } -} diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs deleted file mode 100644 index 7ba46a4687..0000000000 --- a/crates/project/src/context_server_store.rs +++ /dev/null @@ -1,1437 +0,0 @@ -pub mod extension; -pub mod registry; - -use std::sync::Arc; - -use anyhow::{Context as _, Result}; -use collections::{HashMap, HashSet}; -use context_server::{ContextServer, ContextServerCommand, ContextServerId}; -use futures::{FutureExt as _, future::join_all}; -use gpui::{App, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, actions}; -use registry::ContextServerDescriptorRegistry; -use settings::{Settings as _, SettingsStore}; -use util::{ResultExt as _, rel_path::RelPath}; - -use crate::{ - Project, - project_settings::{ContextServerSettings, ProjectSettings}, - worktree_store::WorktreeStore, -}; - -pub fn init(cx: &mut App) { - extension::init(cx); -} - -actions!( - context_server, - [ - /// Restarts the context server. - Restart - ] -); - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum ContextServerStatus { - Starting, - Running, - Stopped, - Error(Arc), -} - -impl ContextServerStatus { - fn from_state(state: &ContextServerState) -> Self { - match state { - ContextServerState::Starting { .. } => ContextServerStatus::Starting, - ContextServerState::Running { .. } => ContextServerStatus::Running, - ContextServerState::Stopped { .. } => ContextServerStatus::Stopped, - ContextServerState::Error { error, .. } => ContextServerStatus::Error(error.clone()), - } - } -} - -enum ContextServerState { - Starting { - server: Arc, - configuration: Arc, - _task: Task<()>, - }, - Running { - server: Arc, - configuration: Arc, - }, - Stopped { - server: Arc, - configuration: Arc, - }, - Error { - server: Arc, - configuration: Arc, - error: Arc, - }, -} - -impl ContextServerState { - pub fn server(&self) -> Arc { - match self { - ContextServerState::Starting { server, .. } => server.clone(), - ContextServerState::Running { server, .. } => server.clone(), - ContextServerState::Stopped { server, .. } => server.clone(), - ContextServerState::Error { server, .. } => server.clone(), - } - } - - pub fn configuration(&self) -> Arc { - match self { - ContextServerState::Starting { configuration, .. } => configuration.clone(), - ContextServerState::Running { configuration, .. } => configuration.clone(), - ContextServerState::Stopped { configuration, .. } => configuration.clone(), - ContextServerState::Error { configuration, .. } => configuration.clone(), - } - } -} - -#[derive(Debug, PartialEq, Eq)] -pub enum ContextServerConfiguration { - Custom { - command: ContextServerCommand, - }, - Extension { - command: ContextServerCommand, - settings: serde_json::Value, - }, - Http { - url: url::Url, - headers: HashMap, - }, -} - -impl ContextServerConfiguration { - pub fn command(&self) -> Option<&ContextServerCommand> { - match self { - ContextServerConfiguration::Custom { command } => Some(command), - ContextServerConfiguration::Extension { command, .. } => Some(command), - ContextServerConfiguration::Http { .. } => None, - } - } - - pub async fn from_settings( - settings: ContextServerSettings, - id: ContextServerId, - registry: Entity, - worktree_store: Entity, - cx: &AsyncApp, - ) -> Option { - match settings { - ContextServerSettings::Stdio { - enabled: _, - command, - } => Some(ContextServerConfiguration::Custom { command }), - ContextServerSettings::Extension { - enabled: _, - settings, - } => { - let descriptor = cx - .update(|cx| registry.read(cx).context_server_descriptor(&id.0)) - .ok() - .flatten()?; - - match descriptor.command(worktree_store, cx).await { - Ok(command) => { - Some(ContextServerConfiguration::Extension { command, settings }) - } - Err(e) => { - log::error!( - "Failed to create context server configuration from settings: {e:#}" - ); - None - } - } - } - ContextServerSettings::Http { - enabled: _, - url, - headers: auth, - } => { - let url = url::Url::parse(&url).log_err()?; - Some(ContextServerConfiguration::Http { url, headers: auth }) - } - } - } -} - -pub type ContextServerFactory = - Box) -> Arc>; - -pub struct ContextServerStore { - context_server_settings: HashMap, ContextServerSettings>, - servers: HashMap, - worktree_store: Entity, - project: WeakEntity, - registry: Entity, - update_servers_task: Option>>, - context_server_factory: Option, - needs_server_update: bool, - _subscriptions: Vec, -} - -pub enum Event { - ServerStatusChanged { - server_id: ContextServerId, - status: ContextServerStatus, - }, -} - -impl EventEmitter for ContextServerStore {} - -impl ContextServerStore { - pub fn new( - worktree_store: Entity, - weak_project: WeakEntity, - cx: &mut Context, - ) -> Self { - Self::new_internal( - true, - None, - ContextServerDescriptorRegistry::default_global(cx), - worktree_store, - weak_project, - cx, - ) - } - - /// Returns all configured context server ids, excluding the ones that are disabled - pub fn configured_server_ids(&self) -> Vec { - self.context_server_settings - .iter() - .filter(|(_, settings)| settings.enabled()) - .map(|(id, _)| ContextServerId(id.clone())) - .collect() - } - - #[cfg(any(test, feature = "test-support"))] - pub fn test( - registry: Entity, - worktree_store: Entity, - weak_project: WeakEntity, - cx: &mut Context, - ) -> Self { - Self::new_internal(false, None, registry, worktree_store, weak_project, cx) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn test_maintain_server_loop( - context_server_factory: Option, - registry: Entity, - worktree_store: Entity, - weak_project: WeakEntity, - cx: &mut Context, - ) -> Self { - Self::new_internal( - true, - context_server_factory, - registry, - worktree_store, - weak_project, - cx, - ) - } - - fn new_internal( - maintain_server_loop: bool, - context_server_factory: Option, - registry: Entity, - worktree_store: Entity, - weak_project: WeakEntity, - cx: &mut Context, - ) -> Self { - let subscriptions = if maintain_server_loop { - vec![ - cx.observe(®istry, |this, _registry, cx| { - this.available_context_servers_changed(cx); - }), - cx.observe_global::(|this, cx| { - let settings = Self::resolve_context_server_settings(&this.worktree_store, cx); - if &this.context_server_settings == settings { - return; - } - this.context_server_settings = settings.clone(); - this.available_context_servers_changed(cx); - }), - ] - } else { - Vec::new() - }; - - let mut this = Self { - _subscriptions: subscriptions, - context_server_settings: Self::resolve_context_server_settings(&worktree_store, cx) - .clone(), - worktree_store, - project: weak_project, - registry, - needs_server_update: false, - servers: HashMap::default(), - update_servers_task: None, - context_server_factory, - }; - if maintain_server_loop { - this.available_context_servers_changed(cx); - } - this - } - - pub fn get_server(&self, id: &ContextServerId) -> Option> { - self.servers.get(id).map(|state| state.server()) - } - - pub fn get_running_server(&self, id: &ContextServerId) -> Option> { - if let Some(ContextServerState::Running { server, .. }) = self.servers.get(id) { - Some(server.clone()) - } else { - None - } - } - - pub fn status_for_server(&self, id: &ContextServerId) -> Option { - self.servers.get(id).map(ContextServerStatus::from_state) - } - - pub fn configuration_for_server( - &self, - id: &ContextServerId, - ) -> Option> { - self.servers.get(id).map(|state| state.configuration()) - } - - pub fn server_ids(&self, cx: &App) -> HashSet { - self.servers - .keys() - .cloned() - .chain( - self.registry - .read(cx) - .context_server_descriptors() - .into_iter() - .map(|(id, _)| ContextServerId(id)), - ) - .collect() - } - - pub fn running_servers(&self) -> Vec> { - self.servers - .values() - .filter_map(|state| { - if let ContextServerState::Running { server, .. } = state { - Some(server.clone()) - } else { - None - } - }) - .collect() - } - - pub fn start_server(&mut self, server: Arc, cx: &mut Context) { - cx.spawn(async move |this, cx| { - let this = this.upgrade().context("Context server store dropped")?; - let settings = this - .update(cx, |this, _| { - this.context_server_settings.get(&server.id().0).cloned() - }) - .ok() - .flatten() - .context("Failed to get context server settings")?; - - if !settings.enabled() { - return Ok(()); - } - - let (registry, worktree_store) = this.update(cx, |this, _| { - (this.registry.clone(), this.worktree_store.clone()) - })?; - let configuration = ContextServerConfiguration::from_settings( - settings, - server.id(), - registry, - worktree_store, - cx, - ) - .await - .context("Failed to create context server configuration")?; - - this.update(cx, |this, cx| { - this.run_server(server, Arc::new(configuration), cx) - }) - }) - .detach_and_log_err(cx); - } - - pub fn stop_server(&mut self, id: &ContextServerId, cx: &mut Context) -> Result<()> { - if matches!( - self.servers.get(id), - Some(ContextServerState::Stopped { .. }) - ) { - return Ok(()); - } - - let state = self - .servers - .remove(id) - .context("Context server not found")?; - - let server = state.server(); - let configuration = state.configuration(); - let mut result = Ok(()); - if let ContextServerState::Running { server, .. } = &state { - result = server.stop(); - } - drop(state); - - self.update_server_state( - id.clone(), - ContextServerState::Stopped { - configuration, - server, - }, - cx, - ); - - result - } - - fn run_server( - &mut self, - server: Arc, - configuration: Arc, - cx: &mut Context, - ) { - let id = server.id(); - if matches!( - self.servers.get(&id), - Some(ContextServerState::Starting { .. } | ContextServerState::Running { .. }) - ) { - self.stop_server(&id, cx).log_err(); - } - let task = cx.spawn({ - let id = server.id(); - let server = server.clone(); - let configuration = configuration.clone(); - - async move |this, cx| { - match server.clone().start(cx).await { - Ok(_) => { - debug_assert!(server.client().is_some()); - - this.update(cx, |this, cx| { - this.update_server_state( - id.clone(), - ContextServerState::Running { - server, - configuration, - }, - cx, - ) - }) - .log_err() - } - Err(err) => { - log::error!("{} context server failed to start: {}", id, err); - this.update(cx, |this, cx| { - this.update_server_state( - id.clone(), - ContextServerState::Error { - configuration, - server, - error: err.to_string().into(), - }, - cx, - ) - }) - .log_err() - } - }; - } - }); - - self.update_server_state( - id.clone(), - ContextServerState::Starting { - configuration, - _task: task, - server, - }, - cx, - ); - } - - fn remove_server(&mut self, id: &ContextServerId, cx: &mut Context) -> Result<()> { - let state = self - .servers - .remove(id) - .context("Context server not found")?; - drop(state); - cx.emit(Event::ServerStatusChanged { - server_id: id.clone(), - status: ContextServerStatus::Stopped, - }); - Ok(()) - } - - fn create_context_server( - &self, - id: ContextServerId, - configuration: Arc, - cx: &mut Context, - ) -> Result> { - if let Some(factory) = self.context_server_factory.as_ref() { - return Ok(factory(id, configuration)); - } - - match configuration.as_ref() { - ContextServerConfiguration::Http { url, headers } => Ok(Arc::new(ContextServer::http( - id, - url, - headers.clone(), - cx.http_client(), - cx.background_executor().clone(), - )?)), - _ => { - let root_path = self - .project - .read_with(cx, |project, cx| project.active_project_directory(cx)) - .ok() - .flatten() - .or_else(|| { - self.worktree_store.read_with(cx, |store, cx| { - store.visible_worktrees(cx).fold(None, |acc, item| { - if acc.is_none() { - item.read(cx).root_dir() - } else { - acc - } - }) - }) - }); - Ok(Arc::new(ContextServer::stdio( - id, - configuration.command().unwrap().clone(), - root_path, - ))) - } - } - } - - fn resolve_context_server_settings<'a>( - worktree_store: &'a Entity, - cx: &'a App, - ) -> &'a HashMap, ContextServerSettings> { - let location = worktree_store - .read(cx) - .visible_worktrees(cx) - .next() - .map(|worktree| settings::SettingsLocation { - worktree_id: worktree.read(cx).id(), - path: RelPath::empty(), - }); - &ProjectSettings::get(location, cx).context_servers - } - - fn update_server_state( - &mut self, - id: ContextServerId, - state: ContextServerState, - cx: &mut Context, - ) { - let status = ContextServerStatus::from_state(&state); - self.servers.insert(id.clone(), state); - cx.emit(Event::ServerStatusChanged { - server_id: id, - status, - }); - } - - fn available_context_servers_changed(&mut self, cx: &mut Context) { - if self.update_servers_task.is_some() { - self.needs_server_update = true; - } else { - self.needs_server_update = false; - self.update_servers_task = Some(cx.spawn(async move |this, cx| { - if let Err(err) = Self::maintain_servers(this.clone(), cx).await { - log::error!("Error maintaining context servers: {}", err); - } - - this.update(cx, |this, cx| { - this.update_servers_task.take(); - if this.needs_server_update { - this.available_context_servers_changed(cx); - } - })?; - - Ok(()) - })); - } - } - - async fn maintain_servers(this: WeakEntity, cx: &mut AsyncApp) -> Result<()> { - let (mut configured_servers, registry, worktree_store) = this.update(cx, |this, _| { - ( - this.context_server_settings.clone(), - this.registry.clone(), - this.worktree_store.clone(), - ) - })?; - - for (id, _) in - registry.read_with(cx, |registry, _| registry.context_server_descriptors())? - { - configured_servers - .entry(id) - .or_insert(ContextServerSettings::default_extension()); - } - - let (enabled_servers, disabled_servers): (HashMap<_, _>, HashMap<_, _>) = - configured_servers - .into_iter() - .partition(|(_, settings)| settings.enabled()); - - let configured_servers = join_all(enabled_servers.into_iter().map(|(id, settings)| { - let id = ContextServerId(id); - ContextServerConfiguration::from_settings( - settings, - id.clone(), - registry.clone(), - worktree_store.clone(), - cx, - ) - .map(|config| (id, config)) - })) - .await - .into_iter() - .filter_map(|(id, config)| config.map(|config| (id, config))) - .collect::>(); - - let mut servers_to_start = Vec::new(); - let mut servers_to_remove = HashSet::default(); - let mut servers_to_stop = HashSet::default(); - - this.update(cx, |this, cx| { - 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 disabled_servers.contains_key(&server_id.0) { - servers_to_stop.insert(server_id.clone()); - } else { - servers_to_remove.insert(server_id.clone()); - } - } - } - - for (id, config) in configured_servers { - let state = this.servers.get(&id); - let is_stopped = matches!(state, Some(ContextServerState::Stopped { .. })); - let existing_config = state.as_ref().map(|state| state.configuration()); - if existing_config.as_deref() != Some(&config) || is_stopped { - let config = Arc::new(config); - let server = this.create_context_server(id.clone(), config.clone(), cx)?; - servers_to_start.push((server, config)); - if this.servers.contains_key(&id) { - servers_to_stop.insert(id); - } - } - } - - anyhow::Ok(()) - })??; - - this.update(cx, |this, cx| { - for id in servers_to_stop { - this.stop_server(&id, cx)?; - } - for id in servers_to_remove { - this.remove_server(&id, cx)?; - } - for (server, config) in servers_to_start { - this.run_server(server, config, cx); - } - anyhow::Ok(()) - })? - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - FakeFs, Project, context_server_store::registry::ContextServerDescriptor, - project_settings::ProjectSettings, - }; - use context_server::test::create_fake_transport; - use gpui::{AppContext, TestAppContext, UpdateGlobal as _}; - use http_client::{FakeHttpClient, Response}; - use serde_json::json; - use std::{cell::RefCell, path::PathBuf, rc::Rc}; - use util::path; - - #[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"; - - let (_fs, project) = setup_context_server_test( - cx, - json!({"code.rs": ""}), - vec![ - (SERVER_1_ID.into(), dummy_server_settings()), - (SERVER_2_ID.into(), dummy_server_settings()), - ], - ) - .await; - - let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); - let store = cx.new(|cx| { - ContextServerStore::test( - registry.clone(), - project.read(cx).worktree_store(), - project.downgrade(), - cx, - ) - }); - - let server_1_id = ContextServerId(SERVER_1_ID.into()); - let server_2_id = ContextServerId(SERVER_2_ID.into()); - - let server_1 = Arc::new(ContextServer::new( - server_1_id.clone(), - Arc::new(create_fake_transport(SERVER_1_ID, cx.executor())), - )); - let server_2 = Arc::new(ContextServer::new( - server_2_id.clone(), - Arc::new(create_fake_transport(SERVER_2_ID, cx.executor())), - )); - - store.update(cx, |store, cx| store.start_server(server_1, cx)); - - cx.run_until_parked(); - - cx.update(|cx| { - assert_eq!( - store.read(cx).status_for_server(&server_1_id), - Some(ContextServerStatus::Running) - ); - assert_eq!(store.read(cx).status_for_server(&server_2_id), None); - }); - - store.update(cx, |store, cx| store.start_server(server_2.clone(), cx)); - - cx.run_until_parked(); - - cx.update(|cx| { - assert_eq!( - store.read(cx).status_for_server(&server_1_id), - Some(ContextServerStatus::Running) - ); - assert_eq!( - store.read(cx).status_for_server(&server_2_id), - Some(ContextServerStatus::Running) - ); - }); - - store - .update(cx, |store, cx| store.stop_server(&server_2_id, cx)) - .unwrap(); - - cx.update(|cx| { - assert_eq!( - store.read(cx).status_for_server(&server_1_id), - Some(ContextServerStatus::Running) - ); - assert_eq!( - store.read(cx).status_for_server(&server_2_id), - Some(ContextServerStatus::Stopped) - ); - }); - } - - #[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"; - - let (_fs, project) = setup_context_server_test( - cx, - json!({"code.rs": ""}), - vec![ - (SERVER_1_ID.into(), dummy_server_settings()), - (SERVER_2_ID.into(), dummy_server_settings()), - ], - ) - .await; - - let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); - let store = cx.new(|cx| { - ContextServerStore::test( - registry.clone(), - project.read(cx).worktree_store(), - project.downgrade(), - cx, - ) - }); - - let server_1_id = ContextServerId(SERVER_1_ID.into()); - let server_2_id = ContextServerId(SERVER_2_ID.into()); - - let server_1 = Arc::new(ContextServer::new( - server_1_id.clone(), - Arc::new(create_fake_transport(SERVER_1_ID, cx.executor())), - )); - let server_2 = Arc::new(ContextServer::new( - server_2_id.clone(), - Arc::new(create_fake_transport(SERVER_2_ID, cx.executor())), - )); - - let _server_events = assert_server_events( - &store, - vec![ - (server_1_id.clone(), ContextServerStatus::Starting), - (server_1_id, ContextServerStatus::Running), - (server_2_id.clone(), ContextServerStatus::Starting), - (server_2_id.clone(), ContextServerStatus::Running), - (server_2_id.clone(), ContextServerStatus::Stopped), - ], - cx, - ); - - store.update(cx, |store, cx| store.start_server(server_1, cx)); - - cx.run_until_parked(); - - store.update(cx, |store, cx| store.start_server(server_2.clone(), cx)); - - cx.run_until_parked(); - - store - .update(cx, |store, cx| store.stop_server(&server_2_id, cx)) - .unwrap(); - } - - #[gpui::test(iterations = 25)] - async fn test_context_server_concurrent_starts(cx: &mut TestAppContext) { - const SERVER_1_ID: &str = "mcp-1"; - - let (_fs, project) = setup_context_server_test( - cx, - json!({"code.rs": ""}), - vec![(SERVER_1_ID.into(), dummy_server_settings())], - ) - .await; - - let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); - let store = cx.new(|cx| { - ContextServerStore::test( - registry.clone(), - project.read(cx).worktree_store(), - project.downgrade(), - cx, - ) - }); - - let server_id = ContextServerId(SERVER_1_ID.into()); - - let server_with_same_id_1 = Arc::new(ContextServer::new( - server_id.clone(), - Arc::new(create_fake_transport(SERVER_1_ID, cx.executor())), - )); - let server_with_same_id_2 = Arc::new(ContextServer::new( - server_id.clone(), - Arc::new(create_fake_transport(SERVER_1_ID, cx.executor())), - )); - - // If we start another server with the same id, we should report that we stopped the previous one - let _server_events = assert_server_events( - &store, - vec![ - (server_id.clone(), ContextServerStatus::Starting), - (server_id.clone(), ContextServerStatus::Stopped), - (server_id.clone(), ContextServerStatus::Starting), - (server_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - - store.update(cx, |store, cx| { - store.start_server(server_with_same_id_1.clone(), cx) - }); - store.update(cx, |store, cx| { - store.start_server(server_with_same_id_2.clone(), cx) - }); - - cx.run_until_parked(); - - cx.update(|cx| { - assert_eq!( - store.read(cx).status_for_server(&server_id), - Some(ContextServerStatus::Running) - ); - }); - } - - #[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"; - - let server_1_id = ContextServerId(SERVER_1_ID.into()); - let server_2_id = ContextServerId(SERVER_2_ID.into()); - - let fake_descriptor_1 = Arc::new(FakeContextServerDescriptor::new(SERVER_1_ID)); - - let (_fs, project) = setup_context_server_test( - cx, - json!({"code.rs": ""}), - vec![( - SERVER_1_ID.into(), - ContextServerSettings::Extension { - enabled: true, - settings: json!({ - "somevalue": true - }), - }, - )], - ) - .await; - - let executor = cx.executor(); - let registry = cx.new(|cx| { - let mut registry = ContextServerDescriptorRegistry::new(); - registry.register_context_server_descriptor(SERVER_1_ID.into(), fake_descriptor_1, cx); - registry - }); - let store = cx.new(|cx| { - ContextServerStore::test_maintain_server_loop( - Some(Box::new(move |id, _| { - Arc::new(ContextServer::new( - id.clone(), - Arc::new(create_fake_transport(id.0.to_string(), executor.clone())), - )) - })), - registry.clone(), - project.read(cx).worktree_store(), - project.downgrade(), - cx, - ) - }); - - // Ensure that mcp-1 starts up - { - let _server_events = assert_server_events( - &store, - vec![ - (server_1_id.clone(), ContextServerStatus::Starting), - (server_1_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - cx.run_until_parked(); - } - - // Ensure that mcp-1 is restarted when the configuration was changed - { - let _server_events = assert_server_events( - &store, - vec![ - (server_1_id.clone(), ContextServerStatus::Stopped), - (server_1_id.clone(), ContextServerStatus::Starting), - (server_1_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - set_context_server_configuration( - vec![( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Extension { - enabled: true, - settings: json!({ - "somevalue": false - }), - }, - )], - cx, - ); - - cx.run_until_parked(); - } - - // Ensure that mcp-1 is not restarted when the configuration was not changed - { - let _server_events = assert_server_events(&store, vec![], cx); - set_context_server_configuration( - vec![( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Extension { - enabled: true, - settings: json!({ - "somevalue": false - }), - }, - )], - cx, - ); - - cx.run_until_parked(); - } - - // Ensure that mcp-2 is started once it is added to the settings - { - let _server_events = assert_server_events( - &store, - vec![ - (server_2_id.clone(), ContextServerStatus::Starting), - (server_2_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - set_context_server_configuration( - vec![ - ( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Extension { - enabled: true, - settings: json!({ - "somevalue": false - }), - }, - ), - ( - server_2_id.0.clone(), - settings::ContextServerSettingsContent::Stdio { - enabled: true, - command: ContextServerCommand { - path: "somebinary".into(), - args: vec!["arg".to_string()], - env: None, - timeout: None, - }, - }, - ), - ], - cx, - ); - - cx.run_until_parked(); - } - - // Ensure that mcp-2 is restarted once the args have changed - { - let _server_events = assert_server_events( - &store, - vec![ - (server_2_id.clone(), ContextServerStatus::Stopped), - (server_2_id.clone(), ContextServerStatus::Starting), - (server_2_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - set_context_server_configuration( - vec![ - ( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Extension { - enabled: true, - settings: json!({ - "somevalue": false - }), - }, - ), - ( - server_2_id.0.clone(), - settings::ContextServerSettingsContent::Stdio { - enabled: true, - command: ContextServerCommand { - path: "somebinary".into(), - args: vec!["anotherArg".to_string()], - env: None, - timeout: None, - }, - }, - ), - ], - cx, - ); - - cx.run_until_parked(); - } - - // Ensure that mcp-2 is removed once it is removed from the settings - { - let _server_events = assert_server_events( - &store, - vec![(server_2_id.clone(), ContextServerStatus::Stopped)], - cx, - ); - set_context_server_configuration( - vec![( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Extension { - enabled: true, - settings: json!({ - "somevalue": false - }), - }, - )], - cx, - ); - - cx.run_until_parked(); - - cx.update(|cx| { - assert_eq!(store.read(cx).status_for_server(&server_2_id), None); - }); - } - - // Ensure that nothing happens if the settings do not change - { - let _server_events = assert_server_events(&store, vec![], cx); - set_context_server_configuration( - vec![( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Extension { - enabled: true, - settings: json!({ - "somevalue": false - }), - }, - )], - cx, - ); - - cx.run_until_parked(); - - cx.update(|cx| { - assert_eq!( - store.read(cx).status_for_server(&server_1_id), - Some(ContextServerStatus::Running) - ); - assert_eq!(store.read(cx).status_for_server(&server_2_id), None); - }); - } - } - - #[gpui::test] - async fn test_context_server_enabled_disabled(cx: &mut TestAppContext) { - const SERVER_1_ID: &str = "mcp-1"; - - let server_1_id = ContextServerId(SERVER_1_ID.into()); - - let (_fs, project) = setup_context_server_test( - cx, - json!({"code.rs": ""}), - vec![( - SERVER_1_ID.into(), - ContextServerSettings::Stdio { - enabled: true, - command: ContextServerCommand { - path: "somebinary".into(), - args: vec!["arg".to_string()], - env: None, - timeout: None, - }, - }, - )], - ) - .await; - - let executor = cx.executor(); - let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); - let store = cx.new(|cx| { - ContextServerStore::test_maintain_server_loop( - Some(Box::new(move |id, _| { - Arc::new(ContextServer::new( - id.clone(), - Arc::new(create_fake_transport(id.0.to_string(), executor.clone())), - )) - })), - registry.clone(), - project.read(cx).worktree_store(), - project.downgrade(), - cx, - ) - }); - - // Ensure that mcp-1 starts up - { - let _server_events = assert_server_events( - &store, - vec![ - (server_1_id.clone(), ContextServerStatus::Starting), - (server_1_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - cx.run_until_parked(); - } - - // Ensure that mcp-1 is stopped once it is disabled. - { - let _server_events = assert_server_events( - &store, - vec![(server_1_id.clone(), ContextServerStatus::Stopped)], - cx, - ); - set_context_server_configuration( - vec![( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Stdio { - enabled: false, - command: ContextServerCommand { - path: "somebinary".into(), - args: vec!["arg".to_string()], - env: None, - timeout: None, - }, - }, - )], - cx, - ); - - cx.run_until_parked(); - } - - // Ensure that mcp-1 is started once it is enabled again. - { - let _server_events = assert_server_events( - &store, - vec![ - (server_1_id.clone(), ContextServerStatus::Starting), - (server_1_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - set_context_server_configuration( - vec![( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Stdio { - enabled: true, - command: ContextServerCommand { - path: "somebinary".into(), - args: vec!["arg".to_string()], - timeout: None, - env: None, - }, - }, - )], - cx, - ); - - cx.run_until_parked(); - } - } - - fn set_context_server_configuration( - context_servers: Vec<(Arc, settings::ContextServerSettingsContent)>, - cx: &mut TestAppContext, - ) { - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |content| { - content.project.context_servers.clear(); - for (id, config) in context_servers { - content.project.context_servers.insert(id, config); - } - }); - }) - }); - } - - #[gpui::test] - async fn test_remote_context_server(cx: &mut TestAppContext) { - const SERVER_ID: &str = "remote-server"; - let server_id = ContextServerId(SERVER_ID.into()); - let server_url = "http://example.com/api"; - - let (_fs, project) = setup_context_server_test( - cx, - json!({ "code.rs": "" }), - vec![( - SERVER_ID.into(), - ContextServerSettings::Http { - enabled: true, - url: server_url.to_string(), - headers: Default::default(), - }, - )], - ) - .await; - - let client = FakeHttpClient::create(|_| async move { - use http_client::AsyncBody; - - let response = Response::builder() - .status(200) - .header("Content-Type", "application/json") - .body(AsyncBody::from( - serde_json::to_string(&json!({ - "jsonrpc": "2.0", - "id": 0, - "result": { - "protocolVersion": "2024-11-05", - "capabilities": {}, - "serverInfo": { - "name": "test-server", - "version": "1.0.0" - } - } - })) - .unwrap(), - )) - .unwrap(); - Ok(response) - }); - cx.update(|cx| cx.set_http_client(client)); - let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); - let store = cx.new(|cx| { - ContextServerStore::test_maintain_server_loop( - None, - registry.clone(), - project.read(cx).worktree_store(), - project.downgrade(), - cx, - ) - }); - - let _server_events = assert_server_events( - &store, - vec![ - (server_id.clone(), ContextServerStatus::Starting), - (server_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - cx.run_until_parked(); - } - - struct ServerEvents { - received_event_count: Rc>, - expected_event_count: usize, - _subscription: Subscription, - } - - impl Drop for ServerEvents { - fn drop(&mut self) { - let actual_event_count = *self.received_event_count.borrow(); - assert_eq!( - actual_event_count, self.expected_event_count, - " - Expected to receive {} context server store events, but received {} events", - self.expected_event_count, actual_event_count - ); - } - } - - fn dummy_server_settings() -> ContextServerSettings { - ContextServerSettings::Stdio { - enabled: true, - command: ContextServerCommand { - path: "somebinary".into(), - args: vec!["arg".to_string()], - env: None, - timeout: None, - }, - } - } - - fn assert_server_events( - store: &Entity, - expected_events: Vec<(ContextServerId, ContextServerStatus)>, - cx: &mut TestAppContext, - ) -> ServerEvents { - cx.update(|cx| { - let mut ix = 0; - let received_event_count = Rc::new(RefCell::new(0)); - let expected_event_count = expected_events.len(); - let subscription = cx.subscribe(store, { - let received_event_count = received_event_count.clone(); - move |_, event, _| match event { - Event::ServerStatusChanged { - server_id: actual_server_id, - status: actual_status, - } => { - let (expected_server_id, expected_status) = &expected_events[ix]; - - assert_eq!( - actual_server_id, expected_server_id, - "Expected different server id at index {}", - ix - ); - assert_eq!( - actual_status, expected_status, - "Expected different status at index {}", - ix - ); - ix += 1; - *received_event_count.borrow_mut() += 1; - } - } - }); - ServerEvents { - expected_event_count, - received_event_count, - _subscription: subscription, - } - }) - } - - async fn setup_context_server_test( - cx: &mut TestAppContext, - files: serde_json::Value, - context_server_configurations: Vec<(Arc, ContextServerSettings)>, - ) -> (Arc, Entity) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - let mut settings = ProjectSettings::get_global(cx).clone(); - for (id, config) in context_server_configurations { - settings.context_servers.insert(id, config); - } - ProjectSettings::override_global(settings, cx); - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/test"), files).await; - let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; - - (fs, project) - } - - struct FakeContextServerDescriptor { - path: PathBuf, - } - - impl FakeContextServerDescriptor { - fn new(path: impl Into) -> Self { - Self { path: path.into() } - } - } - - impl ContextServerDescriptor for FakeContextServerDescriptor { - fn command( - &self, - _worktree_store: Entity, - _cx: &AsyncApp, - ) -> Task> { - Task::ready(Ok(ContextServerCommand { - path: self.path.clone(), - args: vec!["arg1".to_string(), "arg2".to_string()], - env: None, - timeout: None, - })) - } - - fn configuration( - &self, - _worktree_store: Entity, - _cx: &AsyncApp, - ) -> Task>> { - Task::ready(Ok(None)) - } - } -} diff --git a/crates/project/src/context_server_store/extension.rs b/crates/project/src/context_server_store/extension.rs deleted file mode 100644 index ca5cacf3b5..0000000000 --- a/crates/project/src/context_server_store/extension.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::sync::Arc; - -use anyhow::Result; -use context_server::ContextServerCommand; -use extension::{ - ContextServerConfiguration, Extension, ExtensionContextServerProxy, ExtensionHostProxy, - ProjectDelegate, -}; -use gpui::{App, AsyncApp, Entity, Task}; - -use crate::worktree_store::WorktreeStore; - -use super::registry::{self, ContextServerDescriptorRegistry}; - -pub fn init(cx: &mut App) { - let proxy = ExtensionHostProxy::default_global(cx); - proxy.register_context_server_proxy(ContextServerDescriptorRegistryProxy { - context_server_factory_registry: ContextServerDescriptorRegistry::default_global(cx), - }); -} - -struct ExtensionProject { - worktree_ids: Vec, -} - -impl ProjectDelegate for ExtensionProject { - fn worktree_ids(&self) -> Vec { - self.worktree_ids.clone() - } -} - -struct ContextServerDescriptor { - id: Arc, - extension: Arc, -} - -fn extension_project( - worktree_store: Entity, - cx: &mut AsyncApp, -) -> Result> { - worktree_store.update(cx, |worktree_store, cx| { - Arc::new(ExtensionProject { - worktree_ids: worktree_store - .visible_worktrees(cx) - .map(|worktree| worktree.read(cx).id().to_proto()) - .collect(), - }) - }) -} - -impl registry::ContextServerDescriptor for ContextServerDescriptor { - fn command( - &self, - worktree_store: Entity, - cx: &AsyncApp, - ) -> Task> { - let id = self.id.clone(); - let extension = self.extension.clone(); - cx.spawn(async move |cx| { - let extension_project = extension_project(worktree_store, cx)?; - let mut command = extension - .context_server_command(id.clone(), extension_project.clone()) - .await?; - command.command = extension.path_from_extension(&command.command); - - log::debug!("loaded command for context server {id}: {command:?}"); - - Ok(ContextServerCommand { - path: command.command, - args: command.args, - env: Some(command.env.into_iter().collect()), - timeout: None, - }) - }) - } - - fn configuration( - &self, - worktree_store: Entity, - cx: &AsyncApp, - ) -> Task>> { - let id = self.id.clone(); - let extension = self.extension.clone(); - cx.spawn(async move |cx| { - let extension_project = extension_project(worktree_store, cx)?; - let configuration = extension - .context_server_configuration(id.clone(), extension_project) - .await?; - - log::debug!("loaded configuration for context server {id}: {configuration:?}"); - - Ok(configuration) - }) - } -} - -struct ContextServerDescriptorRegistryProxy { - context_server_factory_registry: Entity, -} - -impl ExtensionContextServerProxy for ContextServerDescriptorRegistryProxy { - fn register_context_server(&self, extension: Arc, id: Arc, cx: &mut App) { - self.context_server_factory_registry - .update(cx, |registry, cx| { - registry.register_context_server_descriptor( - id.clone(), - Arc::new(ContextServerDescriptor { id, extension }) - as Arc, - cx, - ) - }); - } - - fn unregister_context_server(&self, server_id: Arc, cx: &mut App) { - self.context_server_factory_registry - .update(cx, |registry, cx| { - registry.unregister_context_server_descriptor_by_id(&server_id, cx) - }); - } -} diff --git a/crates/project/src/context_server_store/registry.rs b/crates/project/src/context_server_store/registry.rs deleted file mode 100644 index b705fcadee..0000000000 --- a/crates/project/src/context_server_store/registry.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::sync::Arc; - -use anyhow::Result; -use collections::HashMap; -use context_server::ContextServerCommand; -use extension::ContextServerConfiguration; -use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Global, Task}; - -use crate::worktree_store::WorktreeStore; - -pub trait ContextServerDescriptor { - fn command( - &self, - worktree_store: Entity, - cx: &AsyncApp, - ) -> Task>; - fn configuration( - &self, - worktree_store: Entity, - cx: &AsyncApp, - ) -> Task>>; -} - -struct GlobalContextServerDescriptorRegistry(Entity); - -impl Global for GlobalContextServerDescriptorRegistry {} - -#[derive(Default)] -pub struct ContextServerDescriptorRegistry { - context_servers: HashMap, Arc>, -} - -impl ContextServerDescriptorRegistry { - /// Returns the global [`ContextServerDescriptorRegistry`]. - /// - /// Inserts a default [`ContextServerDescriptorRegistry`] if one does not yet exist. - pub fn default_global(cx: &mut App) -> Entity { - if !cx.has_global::() { - let registry = cx.new(|_| Self::new()); - cx.set_global(GlobalContextServerDescriptorRegistry(registry)); - } - cx.global::() - .0 - .clone() - } - - pub fn new() -> Self { - Self { - context_servers: HashMap::default(), - } - } - - pub fn context_server_descriptors(&self) -> Vec<(Arc, Arc)> { - self.context_servers - .iter() - .map(|(id, factory)| (id.clone(), factory.clone())) - .collect() - } - - pub fn context_server_descriptor(&self, id: &str) -> Option> { - self.context_servers.get(id).cloned() - } - - /// Registers the provided [`ContextServerDescriptor`]. - pub fn register_context_server_descriptor( - &mut self, - id: Arc, - descriptor: Arc, - cx: &mut Context, - ) { - self.context_servers.insert(id, descriptor); - cx.notify(); - } - - /// Unregisters the [`ContextServerDescriptor`] for the server with the given ID. - pub fn unregister_context_server_descriptor_by_id( - &mut self, - server_id: &str, - cx: &mut Context, - ) { - self.context_servers.remove(server_id); - cx.notify(); - } -} diff --git a/crates/project/src/debounced_delay.rs b/crates/project/src/debounced_delay.rs deleted file mode 100644 index 0ea045be5f..0000000000 --- a/crates/project/src/debounced_delay.rs +++ /dev/null @@ -1,54 +0,0 @@ -use futures::{FutureExt, channel::oneshot}; -use gpui::{Context, Task}; -use std::{marker::PhantomData, time::Duration}; - -pub struct DebouncedDelay { - task: Option>, - cancel_channel: Option>, - _phantom_data: PhantomData, -} - -impl Default for DebouncedDelay { - fn default() -> Self { - Self::new() - } -} - -impl DebouncedDelay { - pub fn new() -> Self { - Self { - task: None, - cancel_channel: None, - _phantom_data: PhantomData, - } - } - - pub fn fire_new(&mut self, delay: Duration, cx: &mut Context, func: F) - where - F: 'static + Send + FnOnce(&mut E, &mut Context) -> Task<()>, - { - if let Some(channel) = self.cancel_channel.take() { - _ = channel.send(()); - } - - let (sender, mut receiver) = oneshot::channel::<()>(); - self.cancel_channel = Some(sender); - - let previous_task = self.task.take(); - self.task = Some(cx.spawn(async move |entity, cx| { - let mut timer = cx.background_executor().timer(delay).fuse(); - if let Some(previous_task) = previous_task { - previous_task.await; - } - - futures::select_biased! { - _ = receiver => return, - _ = timer => {} - } - - if let Ok(task) = entity.update(cx, |project, cx| (func)(project, cx)) { - task.await; - } - })); - } -} diff --git a/crates/project/src/debugger.rs b/crates/project/src/debugger.rs deleted file mode 100644 index 0bf6a0d61b..0000000000 --- a/crates/project/src/debugger.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Zed's debugger data layer is implemented in terms of 3 concepts: -//! - DAP store - that knows about all of the available debug sessions. -//! - Debug sessions - that bear responsibility of communicating with debug adapters and managing the state of each individual session. -//! For the most part it is agnostic over the communication layer (it'll use RPC for peers and actual DAP requests for the host). -//! - Breakpoint store - that knows about all breakpoints set for a project. -//! -//! 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. -//! - 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. -//! - 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; -pub mod dap_command; -pub mod dap_store; -pub mod locators; -mod memory; -pub mod session; - -#[cfg(any(feature = "test-support", test))] -pub mod test; -pub use memory::MemoryCell; diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs deleted file mode 100644 index 42663ab985..0000000000 --- a/crates/project/src/debugger/breakpoint_store.rs +++ /dev/null @@ -1,1034 +0,0 @@ -//! Module for managing breakpoints in a project. -//! -//! Breakpoints are separate from a session because they're not associated with any particular debug session. They can also be set up without a session running. -use anyhow::{Context as _, Result}; -pub use breakpoints_in_file::{BreakpointSessionState, BreakpointWithPosition}; -use breakpoints_in_file::{BreakpointsInFile, StatefulBreakpoint}; -use collections::{BTreeMap, HashMap}; -use dap::{StackFrameId, client::SessionId}; -use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, Subscription, Task}; -use itertools::Itertools; -use language::{Buffer, BufferSnapshot, proto::serialize_anchor as serialize_text_anchor}; -use rpc::{ - AnyProtoClient, TypedEnvelope, - proto::{self}, -}; -use std::{hash::Hash, ops::Range, path::Path, sync::Arc, u32}; -use text::{Point, PointUtf16}; -use util::maybe; - -use crate::{Project, ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore}; - -use super::session::ThreadId; - -mod breakpoints_in_file { - use collections::HashMap; - use language::{BufferEvent, DiskState}; - - use super::*; - - #[derive(Clone, Debug, PartialEq, Eq)] - pub struct BreakpointWithPosition { - pub position: text::Anchor, - pub bp: Breakpoint, - } - - /// A breakpoint with per-session data about it's state (as seen by the Debug Adapter). - #[derive(Clone, Debug)] - pub struct StatefulBreakpoint { - pub bp: BreakpointWithPosition, - pub session_state: HashMap, - } - - impl StatefulBreakpoint { - pub(super) fn new(bp: BreakpointWithPosition) -> Self { - Self { - bp, - session_state: Default::default(), - } - } - pub(super) fn position(&self) -> &text::Anchor { - &self.bp.position - } - } - - #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] - pub struct BreakpointSessionState { - /// Session-specific identifier for the breakpoint, as assigned by Debug Adapter. - pub id: u64, - pub verified: bool, - } - #[derive(Clone)] - pub(super) struct BreakpointsInFile { - pub(super) buffer: Entity, - // TODO: This is.. less than ideal, as it's O(n) and does not return entries in order. We'll have to change TreeMap to support passing in the context for comparisons - pub(super) breakpoints: Vec, - _subscription: Arc, - } - - impl BreakpointsInFile { - pub(super) fn new(buffer: Entity, cx: &mut Context) -> Self { - let subscription = Arc::from(cx.subscribe( - &buffer, - |breakpoint_store, buffer, event, cx| match event { - BufferEvent::Saved => { - if let Some(abs_path) = BreakpointStore::abs_path_from_buffer(&buffer, cx) { - cx.emit(BreakpointStoreEvent::BreakpointsUpdated( - abs_path, - BreakpointUpdatedReason::FileSaved, - )); - } - } - BufferEvent::FileHandleChanged => { - let entity_id = buffer.entity_id(); - - if buffer.read(cx).file().is_none_or(|f| f.disk_state() == DiskState::Deleted) { - breakpoint_store.breakpoints.retain(|_, breakpoints_in_file| { - breakpoints_in_file.buffer.entity_id() != entity_id - }); - - cx.notify(); - return; - } - - if let Some(abs_path) = BreakpointStore::abs_path_from_buffer(&buffer, cx) { - if breakpoint_store.breakpoints.contains_key(&abs_path) { - return; - } - - if let Some(old_path) = breakpoint_store - .breakpoints - .iter() - .find(|(_, in_file)| in_file.buffer.entity_id() == entity_id) - .map(|values| values.0) - .cloned() - { - let Some(breakpoints_in_file) = - breakpoint_store.breakpoints.remove(&old_path) else { - log::error!("Couldn't get breakpoints in file from old path during buffer rename handling"); - return; - }; - - breakpoint_store.breakpoints.insert(abs_path, breakpoints_in_file); - cx.notify(); - } - } - } - _ => {} - }, - )); - - BreakpointsInFile { - buffer, - breakpoints: Vec::new(), - _subscription: subscription, - } - } - } -} - -#[derive(Clone)] -struct RemoteBreakpointStore { - upstream_client: AnyProtoClient, - _upstream_project_id: u64, -} - -#[derive(Clone)] -struct LocalBreakpointStore { - worktree_store: Entity, - buffer_store: Entity, -} - -#[derive(Clone)] -enum BreakpointStoreMode { - Local(LocalBreakpointStore), - Remote(RemoteBreakpointStore), -} - -#[derive(Clone, PartialEq)] -pub struct ActiveStackFrame { - pub session_id: SessionId, - pub thread_id: ThreadId, - pub stack_frame_id: StackFrameId, - pub path: Arc, - pub position: text::Anchor, -} - -pub struct BreakpointStore { - breakpoints: BTreeMap, BreakpointsInFile>, - downstream_client: Option<(AnyProtoClient, u64)>, - active_stack_frame: Option, - // E.g ssh - mode: BreakpointStoreMode, -} - -impl BreakpointStore { - pub fn init(client: &AnyProtoClient) { - client.add_entity_request_handler(Self::handle_toggle_breakpoint); - client.add_entity_message_handler(Self::handle_breakpoints_for_file); - } - pub fn local(worktree_store: Entity, buffer_store: Entity) -> Self { - BreakpointStore { - breakpoints: BTreeMap::new(), - mode: BreakpointStoreMode::Local(LocalBreakpointStore { - worktree_store, - buffer_store, - }), - downstream_client: None, - active_stack_frame: Default::default(), - } - } - - pub(crate) fn remote(upstream_project_id: u64, upstream_client: AnyProtoClient) -> Self { - BreakpointStore { - breakpoints: BTreeMap::new(), - mode: BreakpointStoreMode::Remote(RemoteBreakpointStore { - upstream_client, - _upstream_project_id: upstream_project_id, - }), - downstream_client: None, - active_stack_frame: Default::default(), - } - } - - pub(crate) fn shared(&mut self, project_id: u64, downstream_client: AnyProtoClient) { - self.downstream_client = Some((downstream_client, project_id)); - } - - pub(crate) fn unshared(&mut self, cx: &mut Context) { - self.downstream_client.take(); - - cx.notify(); - } - - async fn handle_breakpoints_for_file( - this: Entity, - message: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let breakpoints = cx.update(|cx| this.read(cx).breakpoint_store())?; - if message.payload.breakpoints.is_empty() { - return Ok(()); - } - - let buffer = this - .update(&mut cx, |this, cx| { - let path = - this.project_path_for_absolute_path(message.payload.path.as_ref(), cx)?; - Some(this.open_buffer(path, cx)) - }) - .ok() - .flatten() - .context("Invalid project path")? - .await?; - - breakpoints.update(&mut cx, move |this, cx| { - let bps = this - .breakpoints - .entry(Arc::::from(message.payload.path.as_ref())) - .or_insert_with(|| BreakpointsInFile::new(buffer, cx)); - - bps.breakpoints = message - .payload - .breakpoints - .into_iter() - .filter_map(|breakpoint| { - let position = - language::proto::deserialize_anchor(breakpoint.position.clone()?)?; - let session_state = breakpoint - .session_state - .iter() - .map(|(session_id, state)| { - let state = BreakpointSessionState { - id: state.id, - verified: state.verified, - }; - (SessionId::from_proto(*session_id), state) - }) - .collect(); - let breakpoint = Breakpoint::from_proto(breakpoint)?; - let bp = BreakpointWithPosition { - position, - bp: breakpoint, - }; - - Some(StatefulBreakpoint { bp, session_state }) - }) - .collect(); - - cx.notify(); - })?; - - Ok(()) - } - - async fn handle_toggle_breakpoint( - this: Entity, - message: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let breakpoints = this.read_with(&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) - })? - .context("Could not resolve provided abs path")?; - let buffer = this - .update(&mut cx, |this, cx| { - this.buffer_store().read(cx).get_by_path(&path) - })? - .context("Could not find buffer for a given path")?; - let breakpoint = message - .payload - .breakpoint - .context("Breakpoint not present in RPC payload")?; - let position = language::proto::deserialize_anchor( - breakpoint - .position - .clone() - .context("Anchor not present in RPC payload")?, - ) - .context("Anchor deserialization failed")?; - let breakpoint = - Breakpoint::from_proto(breakpoint).context("Could not deserialize breakpoint")?; - - breakpoints.update(&mut cx, |this, cx| { - this.toggle_breakpoint( - buffer, - BreakpointWithPosition { - position, - bp: breakpoint, - }, - BreakpointEditAction::Toggle, - cx, - ); - })?; - Ok(proto::Ack {}) - } - - pub(crate) fn broadcast(&self) { - if let Some((client, project_id)) = &self.downstream_client { - for (path, breakpoint_set) in &self.breakpoints { - let _ = client.send(proto::BreakpointsForFile { - project_id: *project_id, - path: path.to_str().map(ToOwned::to_owned).unwrap(), - breakpoints: breakpoint_set - .breakpoints - .iter() - .filter_map(|breakpoint| { - breakpoint.bp.bp.to_proto( - path, - breakpoint.position(), - &breakpoint.session_state, - ) - }) - .collect(), - }); - } - } - } - - pub(crate) fn update_session_breakpoint( - &mut self, - session_id: SessionId, - _: dap::BreakpointEventReason, - breakpoint: dap::Breakpoint, - ) { - maybe!({ - let event_id = breakpoint.id?; - - let state = self - .breakpoints - .values_mut() - .find_map(|breakpoints_in_file| { - breakpoints_in_file - .breakpoints - .iter_mut() - .find_map(|state| { - let state = state.session_state.get_mut(&session_id)?; - - if state.id == event_id { - Some(state) - } else { - None - } - }) - })?; - - state.verified = breakpoint.verified; - Some(()) - }); - } - - pub(super) fn mark_breakpoints_verified( - &mut self, - session_id: SessionId, - abs_path: &Path, - - it: impl Iterator, - ) { - maybe!({ - let breakpoints = self.breakpoints.get_mut(abs_path)?; - for (breakpoint, state) in it { - if let Some(to_update) = breakpoints - .breakpoints - .iter_mut() - .find(|bp| *bp.position() == breakpoint.position) - { - to_update - .session_state - .entry(session_id) - .insert_entry(state); - } - } - Some(()) - }); - } - - pub fn abs_path_from_buffer(buffer: &Entity, cx: &App) -> Option> { - worktree::File::from_dyn(buffer.read(cx).file()) - .map(|file| file.worktree.read(cx).absolutize(&file.path)) - .map(Arc::::from) - } - - pub fn toggle_breakpoint( - &mut self, - buffer: Entity, - mut breakpoint: BreakpointWithPosition, - edit_action: BreakpointEditAction, - cx: &mut Context, - ) { - let Some(abs_path) = Self::abs_path_from_buffer(&buffer, cx) else { - return; - }; - - let breakpoint_set = self - .breakpoints - .entry(abs_path.clone()) - .or_insert_with(|| BreakpointsInFile::new(buffer, cx)); - - match edit_action { - BreakpointEditAction::Toggle => { - let len_before = breakpoint_set.breakpoints.len(); - breakpoint_set - .breakpoints - .retain(|value| breakpoint != value.bp); - if len_before == breakpoint_set.breakpoints.len() { - // We did not remove any breakpoint, hence let's toggle one. - breakpoint_set - .breakpoints - .push(StatefulBreakpoint::new(breakpoint.clone())); - } - } - BreakpointEditAction::InvertState => { - if let Some(bp) = breakpoint_set - .breakpoints - .iter_mut() - .find(|value| breakpoint == value.bp) - { - let bp = &mut bp.bp.bp; - if bp.is_enabled() { - bp.state = BreakpointState::Disabled; - } else { - bp.state = BreakpointState::Enabled; - } - } else { - breakpoint.bp.state = BreakpointState::Disabled; - breakpoint_set - .breakpoints - .push(StatefulBreakpoint::new(breakpoint.clone())); - } - } - BreakpointEditAction::EditLogMessage(log_message) => { - if !log_message.is_empty() { - let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|bp| { - if breakpoint.position == *bp.position() { - Some(&mut bp.bp.bp) - } else { - None - } - }); - - if let Some(found_bp) = found_bp { - found_bp.message = Some(log_message); - } else { - breakpoint.bp.message = Some(log_message); - // We did not remove any breakpoint, hence let's toggle one. - breakpoint_set - .breakpoints - .push(StatefulBreakpoint::new(breakpoint.clone())); - } - } else if breakpoint.bp.message.is_some() { - if let Some(position) = breakpoint_set - .breakpoints - .iter() - .find_position(|other| breakpoint == other.bp) - .map(|res| res.0) - { - breakpoint_set.breakpoints.remove(position); - } else { - log::error!("Failed to find position of breakpoint to delete") - } - } - } - BreakpointEditAction::EditHitCondition(hit_condition) => { - if !hit_condition.is_empty() { - let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|other| { - if breakpoint.position == *other.position() { - Some(&mut other.bp.bp) - } else { - None - } - }); - - if let Some(found_bp) = found_bp { - found_bp.hit_condition = Some(hit_condition); - } else { - breakpoint.bp.hit_condition = Some(hit_condition); - // We did not remove any breakpoint, hence let's toggle one. - breakpoint_set - .breakpoints - .push(StatefulBreakpoint::new(breakpoint.clone())) - } - } else if breakpoint.bp.hit_condition.is_some() { - if let Some(position) = breakpoint_set - .breakpoints - .iter() - .find_position(|bp| breakpoint == bp.bp) - .map(|res| res.0) - { - breakpoint_set.breakpoints.remove(position); - } else { - log::error!("Failed to find position of breakpoint to delete") - } - } - } - BreakpointEditAction::EditCondition(condition) => { - if !condition.is_empty() { - let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|other| { - if breakpoint.position == *other.position() { - Some(&mut other.bp.bp) - } else { - None - } - }); - - if let Some(found_bp) = found_bp { - found_bp.condition = Some(condition); - } else { - breakpoint.bp.condition = Some(condition); - // We did not remove any breakpoint, hence let's toggle one. - breakpoint_set - .breakpoints - .push(StatefulBreakpoint::new(breakpoint.clone())); - } - } else if breakpoint.bp.condition.is_some() { - if let Some(position) = breakpoint_set - .breakpoints - .iter() - .find_position(|bp| breakpoint == bp.bp) - .map(|res| res.0) - { - breakpoint_set.breakpoints.remove(position); - } else { - log::error!("Failed to find position of breakpoint to delete") - } - } - } - } - - if breakpoint_set.breakpoints.is_empty() { - self.breakpoints.remove(&abs_path); - } - if let BreakpointStoreMode::Remote(remote) = &self.mode { - if let Some(breakpoint) = - breakpoint - .bp - .to_proto(&abs_path, &breakpoint.position, &HashMap::default()) - { - cx.background_spawn(remote.upstream_client.request(proto::ToggleBreakpoint { - project_id: remote._upstream_project_id, - path: abs_path.to_str().map(ToOwned::to_owned).unwrap(), - breakpoint: Some(breakpoint), - })) - .detach(); - } - } else if let Some((client, project_id)) = &self.downstream_client { - let breakpoints = self - .breakpoints - .get(&abs_path) - .map(|breakpoint_set| { - breakpoint_set - .breakpoints - .iter() - .filter_map(|bp| { - bp.bp - .bp - .to_proto(&abs_path, bp.position(), &bp.session_state) - }) - .collect() - }) - .unwrap_or_default(); - - let _ = client.send(proto::BreakpointsForFile { - project_id: *project_id, - path: abs_path.to_str().map(ToOwned::to_owned).unwrap(), - breakpoints, - }); - } - - cx.emit(BreakpointStoreEvent::BreakpointsUpdated( - abs_path, - BreakpointUpdatedReason::Toggled, - )); - cx.notify(); - } - - pub fn on_file_rename( - &mut self, - old_path: Arc, - new_path: Arc, - cx: &mut Context, - ) { - if let Some(breakpoints) = self.breakpoints.remove(&old_path) { - self.breakpoints.insert(new_path, breakpoints); - - cx.notify(); - } - } - - pub fn clear_breakpoints(&mut self, cx: &mut Context) { - let breakpoint_paths = self.breakpoints.keys().cloned().collect(); - self.breakpoints.clear(); - cx.emit(BreakpointStoreEvent::BreakpointsCleared(breakpoint_paths)); - } - - pub fn breakpoints<'a>( - &'a self, - buffer: &'a Entity, - range: Option>, - buffer_snapshot: &'a BufferSnapshot, - cx: &App, - ) -> impl Iterator)> + 'a - { - let abs_path = Self::abs_path_from_buffer(buffer, cx); - let active_session_id = self - .active_stack_frame - .as_ref() - .map(|frame| frame.session_id); - abs_path - .and_then(|path| self.breakpoints.get(&path)) - .into_iter() - .flat_map(move |file_breakpoints| { - 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; - } - let session_state = active_session_id - .and_then(|id| bp.session_state.get(&id)) - .copied(); - Some((&bp.bp, session_state)) - } - }) - }) - } - - pub fn active_position(&self) -> Option<&ActiveStackFrame> { - self.active_stack_frame.as_ref() - } - - pub fn remove_active_position( - &mut self, - session_id: Option, - cx: &mut Context, - ) { - if let Some(session_id) = session_id { - self.active_stack_frame - .take_if(|active_stack_frame| active_stack_frame.session_id == session_id); - } else { - self.active_stack_frame.take(); - } - - cx.emit(BreakpointStoreEvent::ClearDebugLines); - cx.notify(); - } - - pub fn set_active_position(&mut self, position: ActiveStackFrame, cx: &mut Context) { - if self - .active_stack_frame - .as_ref() - .is_some_and(|active_position| active_position == &position) - { - cx.emit(BreakpointStoreEvent::SetDebugLine); - return; - } - - if self.active_stack_frame.is_some() { - cx.emit(BreakpointStoreEvent::ClearDebugLines); - } - - self.active_stack_frame = Some(position); - - cx.emit(BreakpointStoreEvent::SetDebugLine); - cx.notify(); - } - - pub fn breakpoint_at_row( - &self, - path: &Path, - row: u32, - cx: &App, - ) -> Option<(Entity, BreakpointWithPosition)> { - self.breakpoints.get(path).and_then(|breakpoints| { - let snapshot = breakpoints.buffer.read(cx).text_snapshot(); - - breakpoints - .breakpoints - .iter() - .find(|bp| bp.position().summary::(&snapshot).row == row) - .map(|breakpoint| (breakpoints.buffer.clone(), breakpoint.bp.clone())) - }) - } - - pub fn breakpoints_from_path(&self, path: &Arc) -> Vec { - self.breakpoints - .get(path) - .map(|bp| bp.breakpoints.iter().map(|bp| bp.bp.clone()).collect()) - .unwrap_or_default() - } - - pub fn source_breakpoints_from_path( - &self, - path: &Arc, - cx: &App, - ) -> Vec { - self.breakpoints - .get(path) - .map(|bp| { - let snapshot = bp.buffer.read(cx).snapshot(); - bp.breakpoints - .iter() - .map(|bp| { - let position = snapshot.summary_for_anchor::(bp.position()).row; - let bp = &bp.bp; - SourceBreakpoint { - row: position, - path: path.clone(), - state: bp.bp.state, - message: bp.bp.message.clone(), - condition: bp.bp.condition.clone(), - hit_condition: bp.bp.hit_condition.clone(), - } - }) - .collect() - }) - .unwrap_or_default() - } - - pub fn all_breakpoints(&self) -> BTreeMap, Vec> { - self.breakpoints - .iter() - .map(|(path, bp)| { - ( - path.clone(), - bp.breakpoints.iter().map(|bp| bp.bp.clone()).collect(), - ) - }) - .collect() - } - pub fn all_source_breakpoints(&self, cx: &App) -> BTreeMap, Vec> { - self.breakpoints - .iter() - .map(|(path, bp)| { - let snapshot = bp.buffer.read(cx).snapshot(); - ( - path.clone(), - bp.breakpoints - .iter() - .map(|breakpoint| { - let position = snapshot - .summary_for_anchor::(breakpoint.position()) - .row; - let breakpoint = &breakpoint.bp; - SourceBreakpoint { - row: position, - path: path.clone(), - message: breakpoint.bp.message.clone(), - state: breakpoint.bp.state, - hit_condition: breakpoint.bp.hit_condition.clone(), - condition: breakpoint.bp.condition.clone(), - } - }) - .collect(), - ) - }) - .collect() - } - - pub fn with_serialized_breakpoints( - &self, - breakpoints: BTreeMap, Vec>, - cx: &mut Context, - ) -> Task> { - if let BreakpointStoreMode::Local(mode) = &self.mode { - let mode = mode.clone(); - cx.spawn(async move |this, cx| { - let mut new_breakpoints = BTreeMap::default(); - for (path, bps) in breakpoints { - if bps.is_empty() { - continue; - } - let (worktree, relative_path) = mode - .worktree_store - .update(cx, |this, cx| { - this.find_or_create_worktree(&path, false, cx) - })? - .await?; - let buffer = mode - .buffer_store - .update(cx, |this, cx| { - let path = ProjectPath { - worktree_id: worktree.read(cx).id(), - path: relative_path, - }; - this.open_buffer(path, cx) - })? - .await; - let Ok(buffer) = buffer else { - log::error!("Todo: Serialized breakpoints which do not have buffer (yet)"); - continue; - }; - let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?; - - let mut breakpoints_for_file = - this.update(cx, |_, cx| BreakpointsInFile::new(buffer, cx))?; - - for bp in bps { - let max_point = snapshot.max_point_utf16(); - let point = PointUtf16::new(bp.row, 0); - if point > max_point { - log::error!("skipping a deserialized breakpoint that's out of range"); - continue; - } - let position = snapshot.anchor_after(point); - breakpoints_for_file - .breakpoints - .push(StatefulBreakpoint::new(BreakpointWithPosition { - position, - bp: Breakpoint { - message: bp.message, - state: bp.state, - condition: bp.condition, - hit_condition: bp.hit_condition, - }, - })) - } - new_breakpoints.insert(path, breakpoints_for_file); - } - this.update(cx, |this, cx| { - for (path, count) in new_breakpoints.iter().map(|(path, bp_in_file)| { - (path.to_string_lossy(), bp_in_file.breakpoints.len()) - }) { - let breakpoint_str = if count > 1 { - "breakpoints" - } else { - "breakpoint" - }; - log::debug!("Deserialized {count} {breakpoint_str} at path: {path}"); - } - - this.breakpoints = new_breakpoints; - - cx.notify(); - })?; - - Ok(()) - }) - } else { - Task::ready(Ok(())) - } - } - - #[cfg(any(test, feature = "test-support"))] - pub(crate) fn breakpoint_paths(&self) -> Vec> { - self.breakpoints.keys().cloned().collect() - } -} - -#[derive(Clone, Copy)] -pub enum BreakpointUpdatedReason { - Toggled, - FileSaved, -} - -pub enum BreakpointStoreEvent { - SetDebugLine, - ClearDebugLines, - BreakpointsUpdated(Arc, BreakpointUpdatedReason), - BreakpointsCleared(Vec>), -} - -impl EventEmitter for BreakpointStore {} - -type BreakpointMessage = Arc; - -#[derive(Clone, Debug)] -pub enum BreakpointEditAction { - Toggle, - InvertState, - EditLogMessage(BreakpointMessage), - EditCondition(BreakpointMessage), - EditHitCondition(BreakpointMessage), -} - -#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] -pub enum BreakpointState { - Enabled, - Disabled, -} - -impl BreakpointState { - #[inline] - pub fn is_enabled(&self) -> bool { - matches!(self, BreakpointState::Enabled) - } - - #[inline] - pub fn is_disabled(&self) -> bool { - matches!(self, BreakpointState::Disabled) - } - - #[inline] - pub fn to_int(self) -> i32 { - match self { - BreakpointState::Enabled => 0, - BreakpointState::Disabled => 1, - } - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct Breakpoint { - pub message: Option, - /// How many times do we hit the breakpoint until we actually stop at it e.g. (2 = 2 times of the breakpoint action) - pub hit_condition: Option>, - pub condition: Option, - pub state: BreakpointState, -} - -impl Breakpoint { - pub fn new_standard() -> Self { - Self { - state: BreakpointState::Enabled, - hit_condition: None, - condition: None, - message: None, - } - } - - pub fn new_condition(hit_condition: &str) -> Self { - Self { - state: BreakpointState::Enabled, - condition: None, - hit_condition: Some(hit_condition.into()), - message: None, - } - } - - pub fn new_log(log_message: &str) -> Self { - Self { - state: BreakpointState::Enabled, - hit_condition: None, - condition: None, - message: Some(log_message.into()), - } - } - - fn to_proto( - &self, - _path: &Path, - position: &text::Anchor, - session_states: &HashMap, - ) -> Option { - Some(client::proto::Breakpoint { - position: Some(serialize_text_anchor(position)), - state: match self.state { - BreakpointState::Enabled => proto::BreakpointState::Enabled.into(), - BreakpointState::Disabled => proto::BreakpointState::Disabled.into(), - }, - message: self.message.as_ref().map(|s| String::from(s.as_ref())), - condition: self.condition.as_ref().map(|s| String::from(s.as_ref())), - hit_condition: self - .hit_condition - .as_ref() - .map(|s| String::from(s.as_ref())), - session_state: session_states - .iter() - .map(|(session_id, state)| { - ( - session_id.to_proto(), - proto::BreakpointSessionState { - id: state.id, - verified: state.verified, - }, - ) - }) - .collect(), - }) - } - - fn from_proto(breakpoint: client::proto::Breakpoint) -> Option { - Some(Self { - state: match proto::BreakpointState::from_i32(breakpoint.state) { - Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled, - None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled, - }, - message: breakpoint.message.map(Into::into), - condition: breakpoint.condition.map(Into::into), - hit_condition: breakpoint.hit_condition.map(Into::into), - }) - } - - #[inline] - pub fn is_enabled(&self) -> bool { - self.state.is_enabled() - } - - #[inline] - pub fn is_disabled(&self) -> bool { - self.state.is_disabled() - } -} - -/// Breakpoint for location within source code. -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct SourceBreakpoint { - pub row: u32, - pub path: Arc, - pub message: Option>, - pub condition: Option>, - pub hit_condition: Option>, - pub state: BreakpointState, -} - -impl From for dap::SourceBreakpoint { - fn from(bp: SourceBreakpoint) -> Self { - Self { - line: bp.row as u64 + 1, - column: None, - condition: bp - .condition - .map(|condition| String::from(condition.as_ref())), - hit_condition: bp - .hit_condition - .map(|hit_condition| String::from(hit_condition.as_ref())), - log_message: bp.message.map(|message| String::from(message.as_ref())), - mode: None, - } - } -} diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs deleted file mode 100644 index 772ff2dcfe..0000000000 --- a/crates/project/src/debugger/dap_command.rs +++ /dev/null @@ -1,1975 +0,0 @@ -use std::sync::Arc; - -use anyhow::{Context as _, Ok, Result}; -use base64::Engine; -use dap::{ - Capabilities, ContinueArguments, ExceptionFilterOptions, InitializeRequestArguments, - InitializeRequestArgumentsPathFormat, NextArguments, SetVariableResponse, SourceBreakpoint, - StepInArguments, StepOutArguments, SteppingGranularity, ValueFormat, Variable, - VariablesArgumentsFilter, - client::SessionId, - proto_conversions::ProtoConversion, - requests::{Continue, Next}, -}; - -use rpc::proto; -use serde_json::Value; -use util::ResultExt; - -pub trait LocalDapCommand: 'static + Send + Sync + std::fmt::Debug { - type Response: 'static + Send + std::fmt::Debug; - type DapRequest: 'static + Send + dap::requests::Request; - /// Is this request idempotent? Is it safe to cache the response for as long as the execution environment is unchanged? - const CACHEABLE: bool = false; - - fn is_supported(_capabilities: &Capabilities) -> bool { - true - } - - fn to_dap(&self) -> ::Arguments; - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result; -} - -pub trait DapCommand: LocalDapCommand { - type ProtoRequest: 'static + Send; - type ProtoResponse: 'static + Send; - - #[allow(dead_code)] - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId; - - #[allow(dead_code)] - fn from_proto(request: &Self::ProtoRequest) -> Self; - - #[allow(unused)] - fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest; - - #[allow(dead_code)] - fn response_to_proto( - debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse; - - #[allow(unused)] - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result; -} - -impl LocalDapCommand for Arc { - type Response = T::Response; - type DapRequest = T::DapRequest; - - fn is_supported(capabilities: &Capabilities) -> bool { - T::is_supported(capabilities) - } - - fn to_dap(&self) -> ::Arguments { - T::to_dap(self) - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - T::response_from_dap(self, message) - } -} - -impl DapCommand for Arc { - type ProtoRequest = T::ProtoRequest; - type ProtoResponse = T::ProtoResponse; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - T::client_id_from_proto(request) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Arc::new(T::from_proto(request)) - } - - fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { - T::to_proto(self, debug_client_id, upstream_project_id) - } - - fn response_to_proto( - debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - T::response_to_proto(debug_client_id, message) - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - T::response_from_proto(self, message) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub struct StepCommand { - pub thread_id: i64, - pub granularity: Option, - pub single_thread: Option, -} - -impl StepCommand { - fn from_proto(message: proto::DapNextRequest) -> Self { - const LINE: i32 = proto::SteppingGranularity::Line as i32; - const INSTRUCTION: i32 = proto::SteppingGranularity::Instruction as i32; - - let granularity = message.granularity.map(|granularity| match granularity { - LINE => SteppingGranularity::Line, - INSTRUCTION => SteppingGranularity::Instruction, - _ => SteppingGranularity::Statement, - }); - - Self { - thread_id: message.thread_id, - granularity, - single_thread: message.single_thread, - } - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct NextCommand { - pub inner: StepCommand, -} - -impl LocalDapCommand for NextCommand { - type Response = ::Response; - type DapRequest = Next; - - fn to_dap(&self) -> ::Arguments { - NextArguments { - thread_id: self.inner.thread_id, - single_thread: self.inner.single_thread, - granularity: self.inner.granularity, - } - } - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -impl DapCommand for NextCommand { - type ProtoRequest = proto::DapNextRequest; - type ProtoResponse = proto::Ack; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - inner: StepCommand::from_proto(request.clone()), - } - } - - fn response_to_proto( - _debug_client_id: SessionId, - _message: Self::Response, - ) -> Self::ProtoResponse { - proto::Ack {} - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapNextRequest { - proto::DapNextRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - thread_id: self.inner.thread_id, - single_thread: self.inner.single_thread, - granularity: self.inner.granularity.map(|gran| gran.to_proto() as i32), - } - } - - fn response_from_proto(&self, _message: Self::ProtoResponse) -> Result { - Ok(()) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct StepInCommand { - pub inner: StepCommand, -} - -impl LocalDapCommand for StepInCommand { - type Response = ::Response; - type DapRequest = dap::requests::StepIn; - - fn to_dap(&self) -> ::Arguments { - StepInArguments { - thread_id: self.inner.thread_id, - single_thread: self.inner.single_thread, - target_id: None, - granularity: self.inner.granularity, - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -impl DapCommand for StepInCommand { - type ProtoRequest = proto::DapStepInRequest; - type ProtoResponse = proto::Ack; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - inner: StepCommand::from_proto(proto::DapNextRequest { - project_id: request.project_id, - client_id: request.client_id, - thread_id: request.thread_id, - single_thread: request.single_thread, - granularity: request.granularity, - }), - } - } - - fn response_to_proto( - _debug_client_id: SessionId, - _message: Self::Response, - ) -> Self::ProtoResponse { - proto::Ack {} - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapStepInRequest { - proto::DapStepInRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - thread_id: self.inner.thread_id, - single_thread: self.inner.single_thread, - granularity: self.inner.granularity.map(|gran| gran.to_proto() as i32), - target_id: None, - } - } - - fn response_from_proto(&self, _message: Self::ProtoResponse) -> Result { - Ok(()) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct StepOutCommand { - pub inner: StepCommand, -} - -impl LocalDapCommand for StepOutCommand { - type Response = ::Response; - type DapRequest = dap::requests::StepOut; - - fn to_dap(&self) -> ::Arguments { - StepOutArguments { - thread_id: self.inner.thread_id, - single_thread: self.inner.single_thread, - granularity: self.inner.granularity, - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -impl DapCommand for StepOutCommand { - type ProtoRequest = proto::DapStepOutRequest; - type ProtoResponse = proto::Ack; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - inner: StepCommand::from_proto(proto::DapNextRequest { - project_id: request.project_id, - client_id: request.client_id, - thread_id: request.thread_id, - single_thread: request.single_thread, - granularity: request.granularity, - }), - } - } - - fn response_to_proto( - _debug_client_id: SessionId, - _message: Self::Response, - ) -> Self::ProtoResponse { - proto::Ack {} - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapStepOutRequest { - proto::DapStepOutRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - thread_id: self.inner.thread_id, - single_thread: self.inner.single_thread, - granularity: self.inner.granularity.map(|gran| gran.to_proto() as i32), - } - } - - fn response_from_proto(&self, _message: Self::ProtoResponse) -> Result { - Ok(()) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct StepBackCommand { - pub inner: StepCommand, -} -impl LocalDapCommand for StepBackCommand { - type Response = ::Response; - type DapRequest = dap::requests::StepBack; - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities.supports_step_back.unwrap_or_default() - } - - fn to_dap(&self) -> ::Arguments { - dap::StepBackArguments { - thread_id: self.inner.thread_id, - single_thread: self.inner.single_thread, - granularity: self.inner.granularity, - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -impl DapCommand for StepBackCommand { - type ProtoRequest = proto::DapStepBackRequest; - type ProtoResponse = proto::Ack; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - inner: StepCommand::from_proto(proto::DapNextRequest { - project_id: request.project_id, - client_id: request.client_id, - thread_id: request.thread_id, - single_thread: request.single_thread, - granularity: request.granularity, - }), - } - } - - fn response_to_proto( - _debug_client_id: SessionId, - _message: Self::Response, - ) -> Self::ProtoResponse { - proto::Ack {} - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapStepBackRequest { - proto::DapStepBackRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - thread_id: self.inner.thread_id, - single_thread: self.inner.single_thread, - granularity: self.inner.granularity.map(|gran| gran.to_proto() as i32), - } - } - - fn response_from_proto(&self, _message: Self::ProtoResponse) -> Result { - Ok(()) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct ContinueCommand { - pub args: ContinueArguments, -} - -impl LocalDapCommand for ContinueCommand { - type Response = ::Response; - type DapRequest = Continue; - - fn to_dap(&self) -> ::Arguments { - self.args.clone() - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} - -impl DapCommand for ContinueCommand { - type ProtoRequest = proto::DapContinueRequest; - type ProtoResponse = proto::DapContinueResponse; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapContinueRequest { - proto::DapContinueRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - thread_id: self.args.thread_id, - single_thread: self.args.single_thread, - } - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - args: ContinueArguments { - thread_id: request.thread_id, - single_thread: request.single_thread, - }, - } - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - Ok(Self::Response { - all_threads_continued: message.all_threads_continued, - }) - } - - fn response_to_proto( - debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - proto::DapContinueResponse { - client_id: debug_client_id.to_proto(), - all_threads_continued: message.all_threads_continued, - } - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct PauseCommand { - pub thread_id: i64, -} - -impl LocalDapCommand for PauseCommand { - type Response = ::Response; - type DapRequest = dap::requests::Pause; - fn to_dap(&self) -> ::Arguments { - dap::PauseArguments { - thread_id: self.thread_id, - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -impl DapCommand for PauseCommand { - type ProtoRequest = proto::DapPauseRequest; - type ProtoResponse = proto::Ack; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - thread_id: request.thread_id, - } - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapPauseRequest { - proto::DapPauseRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - thread_id: self.thread_id, - } - } - - fn response_to_proto( - _debug_client_id: SessionId, - _message: Self::Response, - ) -> Self::ProtoResponse { - proto::Ack {} - } - - fn response_from_proto(&self, _message: Self::ProtoResponse) -> Result { - Ok(()) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct DisconnectCommand { - pub restart: Option, - pub terminate_debuggee: Option, - pub suspend_debuggee: Option, -} - -impl LocalDapCommand for DisconnectCommand { - type Response = ::Response; - type DapRequest = dap::requests::Disconnect; - - fn to_dap(&self) -> ::Arguments { - dap::DisconnectArguments { - restart: self.restart, - terminate_debuggee: self.terminate_debuggee, - suspend_debuggee: self.suspend_debuggee, - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -impl DapCommand for DisconnectCommand { - type ProtoRequest = proto::DapDisconnectRequest; - type ProtoResponse = proto::Ack; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - restart: request.restart, - terminate_debuggee: request.terminate_debuggee, - suspend_debuggee: request.suspend_debuggee, - } - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapDisconnectRequest { - proto::DapDisconnectRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - restart: self.restart, - terminate_debuggee: self.terminate_debuggee, - suspend_debuggee: self.suspend_debuggee, - } - } - - fn response_to_proto( - _debug_client_id: SessionId, - _message: Self::Response, - ) -> Self::ProtoResponse { - proto::Ack {} - } - - fn response_from_proto(&self, _message: Self::ProtoResponse) -> Result { - Ok(()) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct TerminateThreadsCommand { - pub thread_ids: Option>, -} - -impl LocalDapCommand for TerminateThreadsCommand { - type Response = ::Response; - type DapRequest = dap::requests::TerminateThreads; - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities - .supports_terminate_threads_request - .unwrap_or_default() - } - - fn to_dap(&self) -> ::Arguments { - dap::TerminateThreadsArguments { - thread_ids: self.thread_ids.clone(), - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -impl DapCommand for TerminateThreadsCommand { - type ProtoRequest = proto::DapTerminateThreadsRequest; - type ProtoResponse = proto::Ack; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - let thread_ids = if request.thread_ids.is_empty() { - None - } else { - Some(request.thread_ids.clone()) - }; - - Self { thread_ids } - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapTerminateThreadsRequest { - proto::DapTerminateThreadsRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - thread_ids: self.thread_ids.clone().unwrap_or_default(), - } - } - - fn response_to_proto( - _debug_client_id: SessionId, - _message: Self::Response, - ) -> Self::ProtoResponse { - proto::Ack {} - } - - fn response_from_proto(&self, _message: Self::ProtoResponse) -> Result { - Ok(()) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct TerminateCommand { - pub restart: Option, -} - -impl LocalDapCommand for TerminateCommand { - type Response = ::Response; - type DapRequest = dap::requests::Terminate; - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities.supports_terminate_request.unwrap_or_default() - } - fn to_dap(&self) -> ::Arguments { - dap::TerminateArguments { - restart: self.restart, - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -impl DapCommand for TerminateCommand { - type ProtoRequest = proto::DapTerminateRequest; - type ProtoResponse = proto::Ack; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - restart: request.restart, - } - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapTerminateRequest { - proto::DapTerminateRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - restart: self.restart, - } - } - - fn response_to_proto( - _debug_client_id: SessionId, - _message: Self::Response, - ) -> Self::ProtoResponse { - proto::Ack {} - } - - fn response_from_proto(&self, _message: Self::ProtoResponse) -> Result { - Ok(()) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct RestartCommand { - pub raw: serde_json::Value, -} - -impl LocalDapCommand for RestartCommand { - type Response = ::Response; - type DapRequest = dap::requests::Restart; - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities.supports_restart_request.unwrap_or_default() - } - - fn to_dap(&self) -> ::Arguments { - dap::RestartArguments { - raw: self.raw.clone(), - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -impl DapCommand for RestartCommand { - type ProtoRequest = proto::DapRestartRequest; - type ProtoResponse = proto::Ack; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - raw: serde_json::from_slice(&request.raw_args) - .log_err() - .unwrap_or(serde_json::Value::Null), - } - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapRestartRequest { - let raw_args = serde_json::to_vec(&self.raw).log_err().unwrap_or_default(); - - proto::DapRestartRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - raw_args, - } - } - - fn response_to_proto( - _debug_client_id: SessionId, - _message: Self::Response, - ) -> Self::ProtoResponse { - proto::Ack {} - } - - fn response_from_proto(&self, _message: Self::ProtoResponse) -> Result { - Ok(()) - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct VariablesCommand { - pub variables_reference: u64, - pub filter: Option, - pub start: Option, - pub count: Option, - pub format: Option, -} - -impl LocalDapCommand for VariablesCommand { - type Response = Vec; - type DapRequest = dap::requests::Variables; - const CACHEABLE: bool = true; - - fn to_dap(&self) -> ::Arguments { - dap::VariablesArguments { - variables_reference: self.variables_reference, - filter: self.filter, - start: self.start, - count: self.count, - format: self.format.clone(), - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message.variables) - } -} - -impl DapCommand for VariablesCommand { - type ProtoRequest = proto::VariablesRequest; - type ProtoResponse = proto::DapVariables; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { - proto::VariablesRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - variables_reference: self.variables_reference, - filter: None, - start: self.start, - count: self.count, - format: None, - } - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - variables_reference: request.variables_reference, - filter: None, - start: request.start, - count: request.count, - format: None, - } - } - - fn response_to_proto( - debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - proto::DapVariables { - client_id: debug_client_id.to_proto(), - variables: message.to_proto(), - } - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - Ok(Vec::from_proto(message.variables)) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct SetVariableValueCommand { - pub name: String, - pub value: String, - pub variables_reference: u64, -} -impl LocalDapCommand for SetVariableValueCommand { - type Response = SetVariableResponse; - type DapRequest = dap::requests::SetVariable; - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities.supports_set_variable.unwrap_or_default() - } - fn to_dap(&self) -> ::Arguments { - dap::SetVariableArguments { - format: None, - name: self.name.clone(), - value: self.value.clone(), - variables_reference: self.variables_reference, - } - } - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} - -impl DapCommand for SetVariableValueCommand { - type ProtoRequest = proto::DapSetVariableValueRequest; - type ProtoResponse = proto::DapSetVariableValueResponse; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { - proto::DapSetVariableValueRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - variables_reference: self.variables_reference, - value: self.value.clone(), - name: self.name.clone(), - } - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - variables_reference: request.variables_reference, - name: request.name.clone(), - value: request.value.clone(), - } - } - - fn response_to_proto( - debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - proto::DapSetVariableValueResponse { - client_id: debug_client_id.to_proto(), - value: message.value, - variable_type: message.type_, - named_variables: message.named_variables, - variables_reference: message.variables_reference, - indexed_variables: message.indexed_variables, - memory_reference: message.memory_reference, - } - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - Ok(SetVariableResponse { - value: message.value, - type_: message.variable_type, - variables_reference: message.variables_reference, - named_variables: message.named_variables, - indexed_variables: message.indexed_variables, - memory_reference: message.memory_reference, - value_location_reference: None, // TODO - }) - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub(crate) struct RestartStackFrameCommand { - pub stack_frame_id: u64, -} - -impl LocalDapCommand for RestartStackFrameCommand { - type Response = ::Response; - type DapRequest = dap::requests::RestartFrame; - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities.supports_restart_frame.unwrap_or_default() - } - - fn to_dap(&self) -> ::Arguments { - dap::RestartFrameArguments { - frame_id: self.stack_frame_id, - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -impl DapCommand for RestartStackFrameCommand { - type ProtoRequest = proto::DapRestartStackFrameRequest; - type ProtoResponse = proto::Ack; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - stack_frame_id: request.stack_frame_id, - } - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapRestartStackFrameRequest { - proto::DapRestartStackFrameRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - stack_frame_id: self.stack_frame_id, - } - } - - fn response_to_proto( - _debug_client_id: SessionId, - _message: Self::Response, - ) -> Self::ProtoResponse { - proto::Ack {} - } - - fn response_from_proto(&self, _message: Self::ProtoResponse) -> Result { - Ok(()) - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub(crate) struct ModulesCommand; - -impl LocalDapCommand for ModulesCommand { - type Response = Vec; - type DapRequest = dap::requests::Modules; - const CACHEABLE: bool = true; - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities.supports_modules_request.unwrap_or_default() - } - - fn to_dap(&self) -> ::Arguments { - dap::ModulesArguments { - start_module: None, - module_count: None, - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message.modules) - } -} - -impl DapCommand for ModulesCommand { - type ProtoRequest = proto::DapModulesRequest; - type ProtoResponse = proto::DapModulesResponse; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(_request: &Self::ProtoRequest) -> Self { - Self {} - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapModulesRequest { - proto::DapModulesRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - } - } - - fn response_to_proto( - debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - proto::DapModulesResponse { - modules: message - .into_iter() - .map(|module| module.to_proto()) - .collect(), - client_id: debug_client_id.to_proto(), - } - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - Ok(message - .modules - .into_iter() - .filter_map(|module| dap::Module::from_proto(module).ok()) - .collect()) - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub(crate) struct LoadedSourcesCommand; - -impl LocalDapCommand for LoadedSourcesCommand { - type Response = Vec; - type DapRequest = dap::requests::LoadedSources; - const CACHEABLE: bool = true; - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities - .supports_loaded_sources_request - .unwrap_or_default() - } - fn to_dap(&self) -> ::Arguments { - dap::LoadedSourcesArguments {} - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message.sources) - } -} - -impl DapCommand for LoadedSourcesCommand { - type ProtoRequest = proto::DapLoadedSourcesRequest; - type ProtoResponse = proto::DapLoadedSourcesResponse; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(_request: &Self::ProtoRequest) -> Self { - Self {} - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapLoadedSourcesRequest { - proto::DapLoadedSourcesRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - } - } - - fn response_to_proto( - debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - proto::DapLoadedSourcesResponse { - sources: message - .into_iter() - .map(|source| source.to_proto()) - .collect(), - client_id: debug_client_id.to_proto(), - } - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - Ok(message - .sources - .into_iter() - .map(dap::Source::from_proto) - .collect()) - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub(crate) struct StackTraceCommand { - pub thread_id: i64, - pub start_frame: Option, - pub levels: Option, -} - -impl LocalDapCommand for StackTraceCommand { - type Response = Vec; - type DapRequest = dap::requests::StackTrace; - const CACHEABLE: bool = true; - - fn to_dap(&self) -> ::Arguments { - dap::StackTraceArguments { - thread_id: self.thread_id, - start_frame: self.start_frame, - levels: self.levels, - format: None, - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message.stack_frames) - } -} - -impl DapCommand for StackTraceCommand { - type ProtoRequest = proto::DapStackTraceRequest; - type ProtoResponse = proto::DapStackTraceResponse; - - fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { - proto::DapStackTraceRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - thread_id: self.thread_id, - start_frame: self.start_frame, - stack_trace_levels: self.levels, - } - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - thread_id: request.thread_id, - start_frame: request.start_frame, - levels: request.stack_trace_levels, - } - } - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - Ok(message - .frames - .into_iter() - .map(dap::StackFrame::from_proto) - .collect()) - } - - fn response_to_proto( - _debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - proto::DapStackTraceResponse { - frames: message.to_proto(), - } - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub(crate) struct ScopesCommand { - pub stack_frame_id: u64, -} - -impl LocalDapCommand for ScopesCommand { - type Response = Vec; - type DapRequest = dap::requests::Scopes; - const CACHEABLE: bool = true; - - fn to_dap(&self) -> ::Arguments { - dap::ScopesArguments { - frame_id: self.stack_frame_id, - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message.scopes) - } -} - -impl DapCommand for ScopesCommand { - type ProtoRequest = proto::DapScopesRequest; - type ProtoResponse = proto::DapScopesResponse; - - fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { - proto::DapScopesRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - stack_frame_id: self.stack_frame_id, - } - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - stack_frame_id: request.stack_frame_id, - } - } - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - Ok(Vec::from_proto(message.scopes)) - } - - fn response_to_proto( - _debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - proto::DapScopesResponse { - scopes: message.to_proto(), - } - } -} - -impl LocalDapCommand for super::session::CompletionsQuery { - type Response = dap::CompletionsResponse; - type DapRequest = dap::requests::Completions; - const CACHEABLE: bool = true; - - fn to_dap(&self) -> ::Arguments { - dap::CompletionsArguments { - text: self.query.clone(), - frame_id: self.frame_id, - column: self.column, - line: None, - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities - .supports_completions_request - .unwrap_or_default() - } -} - -impl DapCommand for super::session::CompletionsQuery { - type ProtoRequest = proto::DapCompletionRequest; - type ProtoResponse = proto::DapCompletionResponse; - - fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { - proto::DapCompletionRequest { - client_id: debug_client_id.to_proto(), - project_id: upstream_project_id, - frame_id: self.frame_id, - query: self.query.clone(), - column: self.column, - line: self.line, - } - } - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - query: request.query.clone(), - frame_id: request.frame_id, - column: request.column, - line: request.line, - } - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - Ok(dap::CompletionsResponse { - targets: Vec::from_proto(message.completions), - }) - } - - fn response_to_proto( - _debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - proto::DapCompletionResponse { - client_id: _debug_client_id.to_proto(), - completions: message.targets.to_proto(), - } - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub(crate) struct EvaluateCommand { - pub expression: String, - pub frame_id: Option, - pub context: Option, - pub source: Option, -} - -impl LocalDapCommand for EvaluateCommand { - type Response = dap::EvaluateResponse; - type DapRequest = dap::requests::Evaluate; - fn to_dap(&self) -> ::Arguments { - dap::EvaluateArguments { - expression: self.expression.clone(), - frame_id: self.frame_id, - context: self.context.clone(), - source: self.source.clone(), - line: None, - column: None, - format: None, - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} -impl DapCommand for EvaluateCommand { - type ProtoRequest = proto::DapEvaluateRequest; - type ProtoResponse = proto::DapEvaluateResponse; - - fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { - proto::DapEvaluateRequest { - client_id: debug_client_id.to_proto(), - project_id: upstream_project_id, - expression: self.expression.clone(), - frame_id: self.frame_id, - context: self - .context - .clone() - .map(|context| context.to_proto().into()), - } - } - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - expression: request.expression.clone(), - frame_id: request.frame_id, - context: Some(dap::EvaluateArgumentsContext::from_proto(request.context())), - source: None, - } - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - Ok(dap::EvaluateResponse { - result: message.result.clone(), - type_: message.evaluate_type.clone(), - presentation_hint: None, - variables_reference: message.variable_reference, - named_variables: message.named_variables, - indexed_variables: message.indexed_variables, - memory_reference: message.memory_reference, - value_location_reference: None, //TODO - }) - } - - fn response_to_proto( - _debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - proto::DapEvaluateResponse { - result: message.result, - evaluate_type: message.type_, - variable_reference: message.variables_reference, - named_variables: message.named_variables, - indexed_variables: message.indexed_variables, - memory_reference: message.memory_reference, - } - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub(crate) struct ThreadsCommand; - -impl LocalDapCommand for ThreadsCommand { - type Response = Vec; - type DapRequest = dap::requests::Threads; - const CACHEABLE: bool = true; - - fn to_dap(&self) -> ::Arguments { - dap::ThreadsArgument {} - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message.threads) - } -} - -impl DapCommand for ThreadsCommand { - type ProtoRequest = proto::DapThreadsRequest; - type ProtoResponse = proto::DapThreadsResponse; - - fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { - proto::DapThreadsRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - } - } - - fn from_proto(_request: &Self::ProtoRequest) -> Self { - Self {} - } - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - Ok(Vec::from_proto(message.threads)) - } - - fn response_to_proto( - _debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - proto::DapThreadsResponse { - threads: message.to_proto(), - } - } -} - -#[derive(Clone, Debug, Hash, PartialEq)] -pub(super) struct Initialize { - pub(super) adapter_id: String, -} - -fn dap_client_capabilities(adapter_id: String) -> InitializeRequestArguments { - InitializeRequestArguments { - client_id: Some("zed".to_owned()), - client_name: Some("Zed".to_owned()), - adapter_id, - locale: Some("en-US".to_owned()), - path_format: Some(InitializeRequestArgumentsPathFormat::Path), - supports_variable_type: Some(true), - supports_variable_paging: Some(false), - supports_run_in_terminal_request: Some(true), - supports_memory_references: Some(true), - supports_progress_reporting: Some(false), - supports_invalidated_event: Some(false), - lines_start_at1: Some(true), - columns_start_at1: Some(true), - supports_memory_event: Some(false), - supports_args_can_be_interpreted_by_shell: Some(false), - supports_start_debugging_request: Some(true), - supports_ansistyling: Some(true), - } -} - -impl LocalDapCommand for Initialize { - type Response = Capabilities; - type DapRequest = dap::requests::Initialize; - - fn to_dap(&self) -> ::Arguments { - dap_client_capabilities(self.adapter_id.clone()) - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} - -#[derive(Clone, Debug, Hash, PartialEq)] -pub(super) struct ConfigurationDone {} - -impl LocalDapCommand for ConfigurationDone { - type Response = (); - type DapRequest = dap::requests::ConfigurationDone; - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities - .supports_configuration_done_request - .unwrap_or_default() - } - - fn to_dap(&self) -> ::Arguments { - dap::ConfigurationDoneArguments {} - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} - -#[derive(Clone, Debug, Hash, PartialEq)] -pub(super) struct Launch { - pub(super) raw: Value, -} - -impl LocalDapCommand for Launch { - type Response = (); - type DapRequest = dap::requests::Launch; - - fn to_dap(&self) -> ::Arguments { - dap::LaunchRequestArguments { - raw: self.raw.clone(), - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} - -#[derive(Clone, Debug, Hash, PartialEq)] -pub(super) struct Attach { - pub(super) raw: Value, -} - -impl LocalDapCommand for Attach { - type Response = (); - type DapRequest = dap::requests::Attach; - - fn to_dap(&self) -> ::Arguments { - dap::AttachRequestArguments { - raw: self.raw.clone(), - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} - -#[derive(Clone, Debug, Hash, PartialEq)] -pub(super) struct SetBreakpoints { - pub(super) source: dap::Source, - pub(super) breakpoints: Vec, - pub(super) source_modified: Option, -} - -impl LocalDapCommand for SetBreakpoints { - type Response = Vec; - type DapRequest = dap::requests::SetBreakpoints; - - fn to_dap(&self) -> ::Arguments { - dap::SetBreakpointsArguments { - lines: None, - source_modified: self.source_modified, - source: self.source.clone(), - breakpoints: Some(self.breakpoints.clone()), - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message.breakpoints) - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub enum DataBreakpointContext { - Variable { - variables_reference: u64, - name: String, - bytes: Option, - }, - Expression { - expression: String, - frame_id: Option, - }, - Address { - address: String, - bytes: Option, - }, -} - -impl DataBreakpointContext { - pub fn human_readable_label(&self) -> String { - match self { - DataBreakpointContext::Variable { name, .. } => format!("Variable: {}", name), - DataBreakpointContext::Expression { expression, .. } => { - format!("Expression: {}", expression) - } - DataBreakpointContext::Address { address, bytes } => { - let mut label = format!("Address: {}", address); - if let Some(bytes) = bytes { - label.push_str(&format!( - " ({} byte{})", - bytes, - if *bytes == 1 { "" } else { "s" } - )); - } - label - } - } - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub(crate) struct DataBreakpointInfoCommand { - pub context: Arc, - pub mode: Option, -} - -impl LocalDapCommand for DataBreakpointInfoCommand { - type Response = dap::DataBreakpointInfoResponse; - type DapRequest = dap::requests::DataBreakpointInfo; - const CACHEABLE: bool = true; - - // todo(debugger): We should expand this trait in the future to take a &self - // Depending on this command is_supported could be differentb - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities.supports_data_breakpoints.unwrap_or(false) - } - - fn to_dap(&self) -> ::Arguments { - let (variables_reference, name, frame_id, as_address, bytes) = match &*self.context { - DataBreakpointContext::Variable { - variables_reference, - name, - bytes, - } => ( - Some(*variables_reference), - name.clone(), - None, - Some(false), - *bytes, - ), - DataBreakpointContext::Expression { - expression, - frame_id, - } => (None, expression.clone(), *frame_id, Some(false), None), - DataBreakpointContext::Address { address, bytes } => { - (None, address.clone(), None, Some(true), *bytes) - } - }; - - dap::DataBreakpointInfoArguments { - variables_reference, - name, - frame_id, - bytes, - as_address, - mode: self.mode.clone(), - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub(crate) struct SetDataBreakpointsCommand { - pub breakpoints: Vec, -} - -impl LocalDapCommand for SetDataBreakpointsCommand { - type Response = Vec; - type DapRequest = dap::requests::SetDataBreakpoints; - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities.supports_data_breakpoints.unwrap_or(false) - } - - fn to_dap(&self) -> ::Arguments { - dap::SetDataBreakpointsArguments { - breakpoints: self.breakpoints.clone(), - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message.breakpoints) - } -} - -#[derive(Clone, Debug, Hash, PartialEq)] -pub(super) enum SetExceptionBreakpoints { - Plain { - filters: Vec, - }, - WithOptions { - filters: Vec, - }, -} - -impl LocalDapCommand for SetExceptionBreakpoints { - type Response = Vec; - type DapRequest = dap::requests::SetExceptionBreakpoints; - - fn to_dap(&self) -> ::Arguments { - match self { - SetExceptionBreakpoints::Plain { filters } => dap::SetExceptionBreakpointsArguments { - filters: filters.clone(), - exception_options: None, - filter_options: None, - }, - SetExceptionBreakpoints::WithOptions { filters } => { - dap::SetExceptionBreakpointsArguments { - filters: vec![], - filter_options: Some(filters.clone()), - exception_options: None, - } - } - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message.breakpoints.unwrap_or_default()) - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub(super) struct LocationsCommand { - pub(super) reference: u64, -} - -impl LocalDapCommand for LocationsCommand { - type Response = dap::LocationsResponse; - type DapRequest = dap::requests::Locations; - const CACHEABLE: bool = true; - - fn to_dap(&self) -> ::Arguments { - dap::LocationsArguments { - location_reference: self.reference, - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} - -impl DapCommand for LocationsCommand { - type ProtoRequest = proto::DapLocationsRequest; - type ProtoResponse = proto::DapLocationsResponse; - - fn client_id_from_proto(message: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(message.session_id) - } - - fn from_proto(message: &Self::ProtoRequest) -> Self { - Self { - reference: message.location_reference, - } - } - - fn to_proto(&self, session_id: SessionId, project_id: u64) -> Self::ProtoRequest { - proto::DapLocationsRequest { - project_id, - session_id: session_id.to_proto(), - location_reference: self.reference, - } - } - - fn response_to_proto(_: SessionId, response: Self::Response) -> Self::ProtoResponse { - proto::DapLocationsResponse { - source: Some(response.source.to_proto()), - line: response.line, - column: response.column, - end_line: response.end_line, - end_column: response.end_column, - } - } - - fn response_from_proto(&self, response: Self::ProtoResponse) -> Result { - Ok(dap::LocationsResponse { - source: response - .source - .map(::from_proto) - .context("Missing `source` field in Locations proto")?, - line: response.line, - column: response.column, - end_line: response.end_line, - end_column: response.end_column, - }) - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub(crate) struct ReadMemory { - pub(crate) memory_reference: String, - pub(crate) offset: Option, - pub(crate) count: u64, -} - -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct ReadMemoryResponse { - pub(super) address: Arc, - pub(super) unreadable_bytes: Option, - pub(super) content: Arc<[u8]>, -} - -impl LocalDapCommand for ReadMemory { - type Response = ReadMemoryResponse; - type DapRequest = dap::requests::ReadMemory; - const CACHEABLE: bool = true; - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities - .supports_read_memory_request - .unwrap_or_default() - } - fn to_dap(&self) -> ::Arguments { - dap::ReadMemoryArguments { - memory_reference: self.memory_reference.clone(), - offset: self.offset, - count: self.count, - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - let data = if let Some(data) = message.data { - base64::engine::general_purpose::STANDARD - .decode(data) - .log_err() - .context("parsing base64 data from DAP's ReadMemory response")? - } else { - vec![] - }; - - Ok(ReadMemoryResponse { - address: message.address.into(), - content: data.into(), - unreadable_bytes: message.unreadable_bytes, - }) - } -} - -impl LocalDapCommand for dap::WriteMemoryArguments { - type Response = dap::WriteMemoryResponse; - type DapRequest = dap::requests::WriteMemory; - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities - .supports_write_memory_request - .unwrap_or_default() - } - fn to_dap(&self) -> ::Arguments { - self.clone() - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs deleted file mode 100644 index 4a588e7c43..0000000000 --- a/crates/project/src/debugger/dap_store.rs +++ /dev/null @@ -1,1035 +0,0 @@ -use super::{ - breakpoint_store::BreakpointStore, - dap_command::EvaluateCommand, - locators, - session::{self, Session, SessionStateEvent}, -}; -use crate::{ - InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState, - debugger::session::SessionQuirks, - project_settings::{DapBinary, ProjectSettings}, - worktree_store::WorktreeStore, -}; -use anyhow::{Context as _, Result, anyhow}; -use async_trait::async_trait; -use collections::HashMap; -use dap::{ - Capabilities, DapRegistry, DebugRequest, EvaluateArgumentsContext, StackFrameId, - adapters::{ - DapDelegate, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments, - }, - client::SessionId, - inline_value::VariableLookupKind, - messages::Message, -}; -use fs::{Fs, RemoveOptions}; -use futures::{ - StreamExt, TryStreamExt as _, - channel::mpsc::{self, UnboundedSender}, - future::{Shared, join_all}, -}; -use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task}; -use http_client::HttpClient; -use language::{Buffer, LanguageToolchainStore}; -use node_runtime::NodeRuntime; -use settings::InlayHintKind; - -use remote::RemoteClient; -use rpc::{ - AnyProtoClient, TypedEnvelope, - proto::{self}, -}; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsLocation, WorktreeId}; -use std::{ - borrow::Borrow, - collections::BTreeMap, - ffi::OsStr, - net::Ipv4Addr, - path::{Path, PathBuf}, - sync::{Arc, Once}, -}; -use task::{DebugScenario, SpawnInTerminal, TaskContext, TaskTemplate}; -use util::{ResultExt as _, rel_path::RelPath}; -use worktree::Worktree; - -#[derive(Debug)] -pub enum DapStoreEvent { - DebugClientStarted(SessionId), - DebugSessionInitialized(SessionId), - DebugClientShutdown(SessionId), - DebugClientEvent { - session_id: SessionId, - message: Message, - }, - Notification(String), - RemoteHasInitialized, -} - -enum DapStoreMode { - Local(LocalDapStore), - Remote(RemoteDapStore), - Collab, -} - -pub struct LocalDapStore { - fs: Arc, - node_runtime: NodeRuntime, - http_client: Arc, - environment: Entity, - toolchain_store: Arc, - is_headless: bool, -} - -pub struct RemoteDapStore { - remote_client: Entity, - upstream_client: AnyProtoClient, - upstream_project_id: u64, - node_runtime: NodeRuntime, - http_client: Arc, -} - -pub struct DapStore { - mode: DapStoreMode, - downstream_client: Option<(AnyProtoClient, u64)>, - breakpoint_store: Entity, - worktree_store: Entity, - sessions: BTreeMap>, - next_session_id: u32, - adapter_options: BTreeMap>, -} - -impl EventEmitter for DapStore {} - -#[derive(Clone, Serialize, Deserialize)] -pub struct PersistedExceptionBreakpoint { - pub enabled: bool, -} - -/// Represents best-effort serialization of adapter state during last session (e.g. watches) -#[derive(Clone, Default, Serialize, Deserialize)] -pub struct PersistedAdapterOptions { - /// Which exception breakpoints were enabled during the last session with this adapter? - pub exception_breakpoints: BTreeMap, -} - -impl DapStore { - pub fn init(client: &AnyProtoClient, cx: &mut App) { - static ADD_LOCATORS: Once = Once::new(); - ADD_LOCATORS.call_once(|| { - let registry = DapRegistry::global(cx); - registry.add_locator(Arc::new(locators::cargo::CargoLocator {})); - registry.add_locator(Arc::new(locators::go::GoLocator {})); - registry.add_locator(Arc::new(locators::node::NodeLocator)); - registry.add_locator(Arc::new(locators::python::PythonLocator)); - }); - client.add_entity_request_handler(Self::handle_run_debug_locator); - client.add_entity_request_handler(Self::handle_get_debug_adapter_binary); - client.add_entity_message_handler(Self::handle_log_to_debug_console); - } - - #[expect(clippy::too_many_arguments)] - pub fn new_local( - http_client: Arc, - node_runtime: NodeRuntime, - fs: Arc, - environment: Entity, - toolchain_store: Arc, - worktree_store: Entity, - breakpoint_store: Entity, - is_headless: bool, - cx: &mut Context, - ) -> Self { - let mode = DapStoreMode::Local(LocalDapStore { - fs: fs.clone(), - environment, - http_client, - node_runtime, - toolchain_store, - is_headless, - }); - - Self::new(mode, breakpoint_store, worktree_store, fs, cx) - } - - pub fn new_remote( - project_id: u64, - remote_client: Entity, - breakpoint_store: Entity, - worktree_store: Entity, - node_runtime: NodeRuntime, - http_client: Arc, - fs: Arc, - cx: &mut Context, - ) -> Self { - let mode = DapStoreMode::Remote(RemoteDapStore { - upstream_client: remote_client.read(cx).proto_client(), - remote_client, - upstream_project_id: project_id, - node_runtime, - http_client, - }); - - Self::new(mode, breakpoint_store, worktree_store, fs, cx) - } - - pub fn new_collab( - _project_id: u64, - _upstream_client: AnyProtoClient, - breakpoint_store: Entity, - worktree_store: Entity, - fs: Arc, - cx: &mut Context, - ) -> Self { - Self::new( - DapStoreMode::Collab, - breakpoint_store, - worktree_store, - fs, - cx, - ) - } - - fn new( - mode: DapStoreMode, - breakpoint_store: Entity, - worktree_store: Entity, - fs: Arc, - cx: &mut Context, - ) -> Self { - cx.background_spawn(async move { - let dir = paths::debug_adapters_dir().join("js-debug-companion"); - - let mut children = fs.read_dir(&dir).await?.try_collect::>().await?; - children.sort_by_key(|child| semver::Version::parse(child.file_name()?.to_str()?).ok()); - - if let Some(child) = children.last() - && let Some(name) = child.file_name() - && let Some(name) = name.to_str() - && semver::Version::parse(name).is_ok() - { - children.pop(); - } - - for child in children { - fs.remove_dir( - &child, - RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await - .ok(); - } - - anyhow::Ok(()) - }) - .detach(); - - Self { - mode, - next_session_id: 0, - downstream_client: None, - breakpoint_store, - worktree_store, - sessions: Default::default(), - adapter_options: Default::default(), - } - } - - pub fn get_debug_adapter_binary( - &mut self, - definition: DebugTaskDefinition, - session_id: SessionId, - worktree: &Entity, - console: UnboundedSender, - cx: &mut Context, - ) -> Task> { - match &self.mode { - DapStoreMode::Local(_) => { - let Some(adapter) = DapRegistry::global(cx).adapter(&definition.adapter) else { - return Task::ready(Err(anyhow!("Failed to find a debug adapter"))); - }; - - let settings_location = SettingsLocation { - worktree_id: worktree.read(cx).id(), - path: RelPath::empty(), - }; - let dap_settings = ProjectSettings::get(Some(settings_location), cx) - .dap - .get(&adapter.name()); - let user_installed_path = dap_settings.and_then(|s| match &s.binary { - DapBinary::Default => None, - DapBinary::Custom(binary) => { - let path = PathBuf::from(binary); - Some(worktree.read(cx).resolve_executable_path(path)) - } - }); - let user_args = dap_settings.map(|s| s.args.clone()); - let user_env = dap_settings.map(|s| s.env.clone()); - - let delegate = self.delegate(worktree, console, cx); - - let worktree = worktree.clone(); - cx.spawn(async move |this, cx| { - let mut binary = adapter - .get_binary( - &delegate, - &definition, - user_installed_path, - user_args, - user_env, - cx, - ) - .await?; - - let env = this - .update(cx, |this, cx| { - this.as_local() - .unwrap() - .environment - .update(cx, |environment, cx| { - environment.worktree_environment(worktree, cx) - }) - })? - .await; - - if let Some(mut env) = env { - env.extend(std::mem::take(&mut binary.envs)); - binary.envs = env; - } - - Ok(binary) - }) - } - DapStoreMode::Remote(remote) => { - let request = remote - .upstream_client - .request(proto::GetDebugAdapterBinary { - session_id: session_id.to_proto(), - project_id: remote.upstream_project_id, - worktree_id: worktree.read(cx).id().to_proto(), - definition: Some(definition.to_proto()), - }); - let remote = remote.remote_client.clone(); - - cx.spawn(async move |_, cx| { - let response = request.await?; - let binary = DebugAdapterBinary::from_proto(response)?; - - let port_forwarding; - let connection; - if let Some(c) = binary.connection { - let host = Ipv4Addr::LOCALHOST; - let port; - if remote.read_with(cx, |remote, _cx| remote.shares_network_interface())? { - port = c.port; - port_forwarding = None; - } else { - port = dap::transport::TcpTransport::unused_port(host).await?; - port_forwarding = Some((port, c.host.to_string(), c.port)); - } - connection = Some(TcpArguments { - port, - host, - timeout: c.timeout, - }) - } else { - port_forwarding = None; - connection = None; - } - - let command = remote.read_with(cx, |remote, _cx| { - remote.build_command( - binary.command, - &binary.arguments, - &binary.envs, - binary.cwd.map(|path| path.display().to_string()), - port_forwarding, - ) - })??; - - Ok(DebugAdapterBinary { - command: Some(command.program), - arguments: command.args, - envs: command.env, - cwd: None, - connection, - request_args: binary.request_args, - }) - }) - } - DapStoreMode::Collab => { - Task::ready(Err(anyhow!("Debugging is not yet supported via collab"))) - } - } - } - - pub fn debug_scenario_for_build_task( - &self, - build: TaskTemplate, - adapter: DebugAdapterName, - label: SharedString, - cx: &mut App, - ) -> Task> { - let locators = DapRegistry::global(cx).locators(); - - cx.background_spawn(async move { - for locator in locators.values() { - if let Some(scenario) = locator.create_scenario(&build, &label, &adapter).await { - return Some(scenario); - } - } - None - }) - } - - pub fn run_debug_locator( - &mut self, - locator_name: &str, - build_command: SpawnInTerminal, - cx: &mut Context, - ) -> Task> { - match &self.mode { - DapStoreMode::Local(_) => { - // Pre-resolve args with existing environment. - let locators = DapRegistry::global(cx).locators(); - let locator = locators.get(locator_name); - - if let Some(locator) = locator.cloned() { - cx.background_spawn(async move { - let result = locator - .run(build_command.clone()) - .await - .log_with_level(log::Level::Error); - if let Some(result) = result { - return Ok(result); - } - - anyhow::bail!( - "None of the locators for task `{}` completed successfully", - build_command.label - ) - }) - } else { - Task::ready(Err(anyhow!( - "Couldn't find any locator for task `{}`. Specify the `attach` or `launch` arguments in your debug scenario definition", - build_command.label - ))) - } - } - DapStoreMode::Remote(remote) => { - let request = remote.upstream_client.request(proto::RunDebugLocators { - project_id: remote.upstream_project_id, - build_command: Some(build_command.to_proto()), - locator: locator_name.to_owned(), - }); - cx.background_spawn(async move { - let response = request.await?; - DebugRequest::from_proto(response) - }) - } - DapStoreMode::Collab => { - Task::ready(Err(anyhow!("Debugging is not yet supported via collab"))) - } - } - } - - fn as_local(&self) -> Option<&LocalDapStore> { - match &self.mode { - DapStoreMode::Local(local_dap_store) => Some(local_dap_store), - _ => None, - } - } - - pub fn new_session( - &mut self, - label: Option, - adapter: DebugAdapterName, - task_context: TaskContext, - parent_session: Option>, - quirks: SessionQuirks, - cx: &mut Context, - ) -> Entity { - let session_id = SessionId(util::post_inc(&mut self.next_session_id)); - - if let Some(session) = &parent_session { - session.update(cx, |session, _| { - session.add_child_session_id(session_id); - }); - } - - let (remote_client, node_runtime, http_client) = match &self.mode { - DapStoreMode::Local(_) => (None, None, None), - DapStoreMode::Remote(remote_dap_store) => ( - Some(remote_dap_store.remote_client.clone()), - Some(remote_dap_store.node_runtime.clone()), - Some(remote_dap_store.http_client.clone()), - ), - DapStoreMode::Collab => (None, None, None), - }; - let session = Session::new( - self.breakpoint_store.clone(), - session_id, - parent_session, - label, - adapter, - task_context, - quirks, - remote_client, - node_runtime, - http_client, - cx, - ); - - self.sessions.insert(session_id, session.clone()); - cx.notify(); - - cx.subscribe(&session, { - move |this: &mut DapStore, _, event: &SessionStateEvent, cx| match event { - SessionStateEvent::Shutdown => { - this.shutdown_session(session_id, cx).detach_and_log_err(cx); - } - SessionStateEvent::Restart | SessionStateEvent::SpawnChildSession { .. } => {} - SessionStateEvent::Running => { - cx.emit(DapStoreEvent::DebugClientStarted(session_id)); - } - } - }) - .detach(); - - session - } - - pub fn boot_session( - &self, - session: Entity, - definition: DebugTaskDefinition, - worktree: Entity, - cx: &mut Context, - ) -> Task> { - let dap_store = cx.weak_entity(); - let console = session.update(cx, |session, cx| session.console_output(cx)); - let session_id = session.read(cx).session_id(); - - cx.spawn({ - let session = session.clone(); - async move |this, cx| { - let binary = this - .update(cx, |this, cx| { - this.get_debug_adapter_binary( - definition.clone(), - session_id, - &worktree, - console, - cx, - ) - })? - .await?; - session - .update(cx, |session, cx| { - session.boot(binary, worktree, dap_store, cx) - })? - .await - } - }) - } - - pub fn session_by_id( - &self, - session_id: impl Borrow, - ) -> Option> { - let session_id = session_id.borrow(); - - self.sessions.get(session_id).cloned() - } - pub fn sessions(&self) -> impl Iterator> { - self.sessions.values() - } - - pub fn capabilities_by_id( - &self, - session_id: impl Borrow, - cx: &App, - ) -> Option { - let session_id = session_id.borrow(); - self.sessions - .get(session_id) - .map(|client| client.read(cx).capabilities.clone()) - } - - pub fn breakpoint_store(&self) -> &Entity { - &self.breakpoint_store - } - - pub fn worktree_store(&self) -> &Entity { - &self.worktree_store - } - - #[allow(dead_code)] - async fn handle_ignore_breakpoint_state( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let session_id = SessionId::from_proto(envelope.payload.session_id); - - this.update(&mut cx, |this, cx| { - if let Some(session) = this.session_by_id(&session_id) { - session.update(cx, |session, cx| { - session.set_ignore_breakpoints(envelope.payload.ignore, cx) - }) - } else { - Task::ready(HashMap::default()) - } - })? - .await; - - Ok(()) - } - - fn delegate( - &self, - worktree: &Entity, - console: UnboundedSender, - cx: &mut App, - ) -> Arc { - let Some(local_store) = self.as_local() else { - unimplemented!("Starting session on remote side"); - }; - - Arc::new(DapAdapterDelegate::new( - local_store.fs.clone(), - worktree.read(cx).snapshot(), - console, - local_store.node_runtime.clone(), - local_store.http_client.clone(), - local_store.toolchain_store.clone(), - local_store - .environment - .update(cx, |env, cx| env.worktree_environment(worktree.clone(), cx)), - local_store.is_headless, - )) - } - - pub fn resolve_inline_value_locations( - &self, - session: Entity, - stack_frame_id: StackFrameId, - buffer_handle: Entity, - inline_value_locations: Vec, - cx: &mut Context, - ) -> Task>> { - let snapshot = buffer_handle.read(cx).snapshot(); - let local_variables = - session - .read(cx) - .variables_by_stack_frame_id(stack_frame_id, false, true); - let global_variables = - session - .read(cx) - .variables_by_stack_frame_id(stack_frame_id, true, false); - - fn format_value(mut value: String) -> String { - const LIMIT: usize = 100; - - if let Some(index) = value.find("\n") { - value.truncate(index); - value.push_str("…"); - } - - if value.len() > LIMIT { - let mut index = LIMIT; - // If index isn't a char boundary truncate will cause a panic - while !value.is_char_boundary(index) { - index -= 1; - } - value.truncate(index); - value.push_str("…"); - } - - format!(": {}", value) - } - - cx.spawn(async move |_, cx| { - let mut inlay_hints = Vec::with_capacity(inline_value_locations.len()); - for inline_value_location in inline_value_locations.iter() { - let point = snapshot.point_to_point_utf16(language::Point::new( - inline_value_location.row as u32, - inline_value_location.column as u32, - )); - let position = snapshot.anchor_after(point); - - match inline_value_location.lookup { - VariableLookupKind::Variable => { - let variable_search = - if inline_value_location.scope - == dap::inline_value::VariableScope::Local - { - local_variables.iter().chain(global_variables.iter()).find( - |variable| variable.name == inline_value_location.variable_name, - ) - } else { - global_variables.iter().find(|variable| { - variable.name == inline_value_location.variable_name - }) - }; - - let Some(variable) = variable_search else { - continue; - }; - - inlay_hints.push(InlayHint { - position, - label: InlayHintLabel::String(format_value(variable.value.clone())), - kind: Some(InlayHintKind::Type), - padding_left: false, - padding_right: false, - tooltip: None, - resolve_state: ResolveState::Resolved, - }); - } - VariableLookupKind::Expression => { - let Ok(eval_task) = session.read_with(cx, |session, _| { - session.state.request_dap(EvaluateCommand { - expression: inline_value_location.variable_name.clone(), - frame_id: Some(stack_frame_id), - source: None, - context: Some(EvaluateArgumentsContext::Variables), - }) - }) else { - continue; - }; - - if let Some(response) = eval_task.await.log_err() { - inlay_hints.push(InlayHint { - position, - label: InlayHintLabel::String(format_value(response.result)), - kind: Some(InlayHintKind::Type), - padding_left: false, - padding_right: false, - tooltip: None, - resolve_state: ResolveState::Resolved, - }); - }; - } - }; - } - - Ok(inlay_hints) - }) - } - - pub fn shutdown_sessions(&mut self, cx: &mut Context) -> Task<()> { - let mut tasks = vec![]; - for session_id in self.sessions.keys().cloned().collect::>() { - tasks.push(self.shutdown_session(session_id, cx)); - } - - cx.background_executor().spawn(async move { - futures::future::join_all(tasks).await; - }) - } - - pub fn shutdown_session( - &mut self, - session_id: SessionId, - cx: &mut Context, - ) -> Task> { - let Some(session) = self.sessions.remove(&session_id) else { - return Task::ready(Err(anyhow!("Could not find session: {:?}", session_id))); - }; - - let shutdown_children = session - .read(cx) - .child_session_ids() - .iter() - .map(|session_id| self.shutdown_session(*session_id, cx)) - .collect::>(); - - let shutdown_parent_task = if let Some(parent_session) = session - .read(cx) - .parent_id(cx) - .and_then(|session_id| self.session_by_id(session_id)) - { - 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() { - Some(parent_session.session_id()) - } else { - None - } - }); - - shutdown_id.map(|session_id| self.shutdown_session(session_id, cx)) - } else { - None - }; - - let shutdown_task = session.update(cx, |this, cx| this.shutdown(cx)); - - cx.emit(DapStoreEvent::DebugClientShutdown(session_id)); - - cx.background_spawn(async move { - if !shutdown_children.is_empty() { - let _ = join_all(shutdown_children).await; - } - - shutdown_task.await; - - if let Some(parent_task) = shutdown_parent_task { - parent_task.await?; - } - - Ok(()) - }) - } - - pub fn shared( - &mut self, - project_id: u64, - downstream_client: AnyProtoClient, - _: &mut Context, - ) { - self.downstream_client = Some((downstream_client, project_id)); - } - - pub fn unshared(&mut self, cx: &mut Context) { - self.downstream_client.take(); - - cx.notify(); - } - - async fn handle_run_debug_locator( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let task = envelope - .payload - .build_command - .context("missing definition")?; - let build_task = SpawnInTerminal::from_proto(task); - let locator = envelope.payload.locator; - let request = this - .update(&mut cx, |this, cx| { - this.run_debug_locator(&locator, build_task, cx) - })? - .await?; - - Ok(request.to_proto()) - } - - async fn handle_get_debug_adapter_binary( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let definition = DebugTaskDefinition::from_proto( - envelope.payload.definition.context("missing definition")?, - )?; - let (tx, mut rx) = mpsc::unbounded(); - let session_id = envelope.payload.session_id; - cx.spawn({ - let this = this.clone(); - async move |cx| { - while let Some(message) = rx.next().await { - this.read_with(cx, |this, _| { - if let Some((downstream, project_id)) = this.downstream_client.clone() { - downstream - .send(proto::LogToDebugConsole { - project_id, - session_id, - message, - }) - .ok(); - } - }) - .ok(); - } - } - }) - .detach(); - - let worktree = this - .update(&mut cx, |this, cx| { - this.worktree_store - .read(cx) - .worktree_for_id(WorktreeId::from_proto(envelope.payload.worktree_id), cx) - })? - .context("Failed to find worktree with a given ID")?; - let binary = this - .update(&mut cx, |this, cx| { - this.get_debug_adapter_binary( - definition, - SessionId::from_proto(session_id), - &worktree, - tx, - cx, - ) - })? - .await?; - Ok(binary.to_proto()) - } - - async fn handle_log_to_debug_console( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let session_id = SessionId::from_proto(envelope.payload.session_id); - this.update(&mut cx, |this, cx| { - let Some(session) = this.sessions.get(&session_id) else { - return; - }; - session.update(cx, |session, cx| { - session - .console_output(cx) - .unbounded_send(envelope.payload.message) - .ok(); - }) - }) - } - - pub fn sync_adapter_options( - &mut self, - session: &Entity, - cx: &App, - ) -> Arc { - let session = session.read(cx); - let adapter = session.adapter(); - let exceptions = session.exception_breakpoints(); - let exception_breakpoints = exceptions - .map(|(exception, enabled)| { - ( - exception.filter.clone(), - PersistedExceptionBreakpoint { enabled: *enabled }, - ) - }) - .collect(); - let options = Arc::new(PersistedAdapterOptions { - exception_breakpoints, - }); - self.adapter_options.insert(adapter, options.clone()); - options - } - - pub fn set_adapter_options( - &mut self, - adapter: DebugAdapterName, - options: PersistedAdapterOptions, - ) { - self.adapter_options.insert(adapter, Arc::new(options)); - } - - pub fn adapter_options(&self, name: &str) -> Option> { - self.adapter_options.get(name).cloned() - } - - pub fn all_adapter_options(&self) -> &BTreeMap> { - &self.adapter_options - } -} - -#[derive(Clone)] -pub struct DapAdapterDelegate { - fs: Arc, - console: mpsc::UnboundedSender, - worktree: worktree::Snapshot, - node_runtime: NodeRuntime, - http_client: Arc, - toolchain_store: Arc, - load_shell_env_task: Shared>>>, - is_headless: bool, -} - -impl DapAdapterDelegate { - pub fn new( - fs: Arc, - worktree: worktree::Snapshot, - status: mpsc::UnboundedSender, - node_runtime: NodeRuntime, - http_client: Arc, - toolchain_store: Arc, - load_shell_env_task: Shared>>>, - is_headless: bool, - ) -> Self { - Self { - fs, - console: status, - worktree, - http_client, - node_runtime, - toolchain_store, - load_shell_env_task, - is_headless, - } - } -} - -#[async_trait] -impl dap::adapters::DapDelegate for DapAdapterDelegate { - fn worktree_id(&self) -> WorktreeId { - self.worktree.id() - } - - fn worktree_root_path(&self) -> &Path { - self.worktree.abs_path() - } - fn http_client(&self) -> Arc { - self.http_client.clone() - } - - fn node_runtime(&self) -> NodeRuntime { - self.node_runtime.clone() - } - - fn fs(&self) -> Arc { - self.fs.clone() - } - - fn output_to_console(&self, msg: String) { - self.console.unbounded_send(msg).ok(); - } - - #[cfg(not(target_os = "windows"))] - async fn which(&self, command: &OsStr) -> Option { - let worktree_abs_path = self.worktree.abs_path(); - let shell_path = self.shell_env().await.get("PATH").cloned(); - which::which_in(command, shell_path.as_ref(), worktree_abs_path).ok() - } - - #[cfg(target_os = "windows")] - async fn which(&self, command: &OsStr) -> Option { - // On Windows, `PATH` is handled differently from Unix. Windows generally expects users to modify the `PATH` themselves, - // and every program loads it directly from the system at startup. - // There's also no concept of a default shell on Windows, and you can't really retrieve one, so trying to get shell environment variables - // from a specific directory doesn’t make sense on Windows. - which::which(command).ok() - } - - async fn shell_env(&self) -> HashMap { - let task = self.load_shell_env_task.clone(); - task.await.unwrap_or_default() - } - - fn toolchain_store(&self) -> Arc { - self.toolchain_store.clone() - } - - async fn read_text_file(&self, path: &RelPath) -> Result { - let entry = self - .worktree - .entry_for_path(path) - .with_context(|| format!("no worktree entry for path {path:?}"))?; - let abs_path = self.worktree.absolutize(&entry.path); - - self.fs.load(&abs_path).await - } - - fn is_headless(&self) -> bool { - self.is_headless - } -} diff --git a/crates/project/src/debugger/locators.rs b/crates/project/src/debugger/locators.rs deleted file mode 100644 index 2faa4c4ca9..0000000000 --- a/crates/project/src/debugger/locators.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub(crate) mod cargo; -pub(crate) mod go; -pub(crate) mod node; -pub(crate) mod python; diff --git a/crates/project/src/debugger/locators/cargo.rs b/crates/project/src/debugger/locators/cargo.rs deleted file mode 100644 index 2f7d8cdc5f..0000000000 --- a/crates/project/src/debugger/locators/cargo.rs +++ /dev/null @@ -1,219 +0,0 @@ -use anyhow::{Context as _, Result}; -use async_trait::async_trait; -use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName}; -use gpui::SharedString; -use serde_json::{Value, json}; -use smol::{Timer, io::AsyncReadExt, process::Stdio}; -use std::time::Duration; -use task::{BuildTaskDefinition, DebugScenario, ShellBuilder, SpawnInTerminal, TaskTemplate}; -use util::command::new_smol_command; - -pub(crate) struct CargoLocator; - -async fn find_best_executable(executables: &[String], test_name: &str) -> Option { - if executables.len() == 1 { - return executables.first().cloned(); - } - for executable in executables { - let Some(mut child) = new_smol_command(&executable) - .arg("--list") - .stdout(Stdio::piped()) - .spawn() - .ok() - else { - continue; - }; - let mut test_lines = String::default(); - let exec_result = smol::future::race( - async { - if let Some(mut stdout) = child.stdout.take() { - stdout.read_to_string(&mut test_lines).await?; - } - Ok(()) - }, - async { - Timer::after(Duration::from_secs(3)).await; - anyhow::bail!("Timed out waiting for executable stdout") - }, - ); - - if let Err(err) = exec_result.await { - log::warn!("Failed to list tests for {executable}: {err}"); - } else { - for line in test_lines.lines() { - if line.contains(&test_name) { - return Some(executable.clone()); - } - } - } - let _ = child.kill(); - } - None -} -#[async_trait] -impl DapLocator for CargoLocator { - fn name(&self) -> SharedString { - SharedString::new_static("rust-cargo-locator") - } - async fn create_scenario( - &self, - build_config: &TaskTemplate, - resolved_label: &str, - adapter: &DebugAdapterName, - ) -> Option { - if build_config.command != "cargo" { - return None; - } - let mut task_template = build_config.clone(); - let cargo_action = task_template.args.first_mut()?; - if cargo_action == "check" || cargo_action == "clean" { - return None; - } - - match cargo_action.as_ref() { - "run" | "r" => { - *cargo_action = "build".to_owned(); - } - "test" | "t" | "bench" => { - let delimiter = task_template - .args - .iter() - .position(|arg| arg == "--") - .unwrap_or(task_template.args.len()); - if !task_template.args[..delimiter] - .iter() - .any(|arg| arg == "--no-run") - { - task_template.args.insert(delimiter, "--no-run".to_owned()); - } - } - _ => {} - } - - let config = if adapter.as_ref() == "CodeLLDB" { - json!({ - "sourceLanguages": ["rust"] - }) - } else { - Value::Null - }; - Some(DebugScenario { - adapter: adapter.0.clone(), - label: resolved_label.to_string().into(), - build: Some(BuildTaskDefinition::Template { - task_template, - locator_name: Some(self.name()), - }), - config, - tcp_connection: None, - }) - } - - async fn run(&self, build_config: SpawnInTerminal) -> Result { - let cwd = build_config - .cwd - .clone() - .context("Couldn't get cwd from debug config which is needed for locators")?; - let builder = ShellBuilder::new(&build_config.shell, cfg!(windows)).non_interactive(); - let mut child = builder - .build_command( - Some("cargo".into()), - &build_config - .args - .iter() - .cloned() - .take_while(|arg| arg != "--") - .chain(Some("--message-format=json".to_owned())) - .collect::>(), - ) - .envs(build_config.env.iter().map(|(k, v)| (k.clone(), v.clone()))) - .current_dir(cwd) - .stdout(Stdio::piped()) - .spawn()?; - - let mut output = String::new(); - if let Some(mut stdout) = child.stdout.take() { - stdout.read_to_string(&mut output).await?; - } - - let status = child.status().await?; - anyhow::ensure!(status.success(), "Cargo command failed"); - - let is_test = build_config - .args - .first() - .is_some_and(|arg| arg == "test" || arg == "t"); - - let is_ignored = build_config.args.contains(&"--include-ignored".to_owned()); - - let executables = output - .lines() - .filter(|line| !line.trim().is_empty()) - .filter_map(|line| serde_json::from_str(line).ok()) - .filter(|json: &Value| { - let is_test_binary = json - .get("profile") - .and_then(|profile| profile.get("test")) - .and_then(Value::as_bool) - .unwrap_or(false); - - if is_test { - is_test_binary - } else { - !is_test_binary - } - }) - .filter_map(|json: Value| { - json.get("executable") - .and_then(Value::as_str) - .map(String::from) - }) - .collect::>(); - anyhow::ensure!( - !executables.is_empty(), - "Couldn't get executable in cargo locator" - ); - - let mut test_name = None; - if is_test { - test_name = build_config - .args - .iter() - .rev() - .take_while(|name| "--" != name.as_str()) - .find(|name| !name.starts_with("-")) - .cloned(); - } - let executable = { - if let Some(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 - } else { - None - } - }; - - let Some(executable) = executable.or_else(|| executables.first().cloned()) else { - anyhow::bail!("Couldn't get executable in cargo locator"); - }; - - let mut args: Vec<_> = test_name.into_iter().collect(); - if is_test { - args.push("--nocapture".to_owned()); - if is_ignored { - args.push("--include-ignored".to_owned()); - args.push("--exact".to_owned()); - } - } - - Ok(DebugRequest::Launch(task::LaunchRequest { - program: executable, - cwd: build_config.cwd, - args, - env: build_config.env.into_iter().collect(), - })) - } -} diff --git a/crates/project/src/debugger/locators/go.rs b/crates/project/src/debugger/locators/go.rs deleted file mode 100644 index eec06084ec..0000000000 --- a/crates/project/src/debugger/locators/go.rs +++ /dev/null @@ -1,441 +0,0 @@ -use anyhow::Result; -use async_trait::async_trait; -use collections::HashMap; -use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName}; -use gpui::SharedString; -use serde::{Deserialize, Serialize}; -use task::{DebugScenario, SpawnInTerminal, TaskTemplate}; - -pub(crate) struct GoLocator; - -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -struct DelveLaunchRequest { - request: String, - mode: String, - program: String, - #[serde(skip_serializing_if = "Option::is_none")] - cwd: Option, - args: Vec, - build_flags: Vec, - env: HashMap, -} - -fn is_debug_flag(arg: &str) -> Option { - let mut part = if let Some(suffix) = arg.strip_prefix("test.") { - suffix - } else { - arg - }; - let mut might_have_arg = true; - if let Some(idx) = part.find('=') { - might_have_arg = false; - part = &part[..idx]; - } - match part { - "benchmem" | "failfast" | "fullpath" | "fuzzworker" | "json" | "short" | "v" - | "paniconexit0" => Some(false), - "bench" - | "benchtime" - | "blockprofile" - | "blockprofilerate" - | "count" - | "coverprofile" - | "cpu" - | "cpuprofile" - | "fuzz" - | "fuzzcachedir" - | "fuzzminimizetime" - | "fuzztime" - | "gocoverdir" - | "list" - | "memprofile" - | "memprofilerate" - | "mutexprofile" - | "mutexprofilefraction" - | "outputdir" - | "parallel" - | "run" - | "shuffle" - | "skip" - | "testlogfile" - | "timeout" - | "trace" => Some(might_have_arg), - _ if arg.starts_with("test.") => Some(false), - _ => None, - } -} - -fn is_build_flag(mut arg: &str) -> Option { - let mut might_have_arg = true; - if let Some(idx) = arg.find('=') { - might_have_arg = false; - arg = &arg[..idx]; - } - match arg { - "a" | "n" | "race" | "msan" | "asan" | "cover" | "work" | "x" | "v" | "buildvcs" - | "json" | "linkshared" | "modcacherw" | "trimpath" => Some(false), - - "p" | "covermode" | "coverpkg" | "asmflags" | "buildmode" | "compiler" | "gccgoflags" - | "gcflags" | "installsuffix" | "ldflags" | "mod" | "modfile" | "overlay" | "pgo" - | "pkgdir" | "tags" | "toolexec" => Some(might_have_arg), - _ => None, - } -} - -#[async_trait] -impl DapLocator for GoLocator { - fn name(&self) -> SharedString { - SharedString::new_static("go-debug-locator") - } - - async fn create_scenario( - &self, - build_config: &TaskTemplate, - resolved_label: &str, - adapter: &DebugAdapterName, - ) -> Option { - if build_config.command != "go" { - return None; - } - let go_action = build_config.args.first()?; - - match go_action.as_str() { - "test" => { - let mut program = ".".to_string(); - let mut args = Vec::default(); - let mut build_flags = Vec::default(); - - let mut all_args_are_test = false; - let mut next_arg_is_test = false; - let mut next_arg_is_build = false; - let mut seen_pkg = false; - let mut seen_v = false; - - for arg in build_config.args.iter().skip(1) { - if all_args_are_test || next_arg_is_test { - // HACK: tasks assume that they are run in a shell context, - // so the -run regex has escaped specials. Delve correctly - // handles escaping, so we undo that here. - if let Some((left, right)) = arg.split_once("/") - && left.starts_with("\\^") - && left.ends_with("\\$") - && right.starts_with("\\^") - && right.ends_with("\\$") - { - let mut left = left[1..left.len() - 2].to_string(); - left.push('$'); - - let mut right = right[1..right.len() - 2].to_string(); - right.push('$'); - - args.push(format!("{left}/{right}")); - } else if arg.starts_with("\\^") && arg.ends_with("\\$") { - let mut arg = arg[1..arg.len() - 2].to_string(); - arg.push('$'); - args.push(arg); - } else { - args.push(arg.clone()); - } - next_arg_is_test = false; - } else if next_arg_is_build { - build_flags.push(arg.clone()); - next_arg_is_build = false; - } else if arg.starts_with('-') { - let flag = arg.trim_start_matches('-'); - if flag == "args" { - all_args_are_test = true; - } else if let Some(has_arg) = is_debug_flag(flag) { - if flag == "v" || flag == "test.v" { - seen_v = true; - } - if flag.starts_with("test.") { - args.push(arg.clone()); - } else { - args.push(format!("-test.{flag}")) - } - next_arg_is_test = has_arg; - } else if let Some(has_arg) = is_build_flag(flag) { - build_flags.push(arg.clone()); - next_arg_is_build = has_arg; - } - } else if !seen_pkg { - program = arg.clone(); - seen_pkg = true; - } else { - args.push(arg.clone()); - } - } - if !seen_v { - args.push("-test.v".to_string()); - } - - let config: serde_json::Value = serde_json::to_value(DelveLaunchRequest { - request: "launch".to_string(), - mode: "test".to_string(), - program, - args, - build_flags, - cwd: build_config.cwd.clone(), - env: build_config.env.clone(), - }) - .unwrap(); - - Some(DebugScenario { - label: resolved_label.to_string().into(), - adapter: adapter.0.clone(), - build: None, - config, - tcp_connection: None, - }) - } - "run" => { - let mut next_arg_is_build = false; - let mut seen_pkg = false; - - let mut program = ".".to_string(); - let mut args = Vec::default(); - let mut build_flags = Vec::default(); - - for arg in build_config.args.iter().skip(1) { - if seen_pkg { - args.push(arg.clone()) - } else if next_arg_is_build { - build_flags.push(arg.clone()); - next_arg_is_build = false; - } else if arg.starts_with("-") { - if let Some(has_arg) = is_build_flag(arg.trim_start_matches("-")) { - next_arg_is_build = has_arg; - } - build_flags.push(arg.clone()) - } else { - program = arg.to_string(); - seen_pkg = true; - } - } - - let config: serde_json::Value = serde_json::to_value(DelveLaunchRequest { - cwd: build_config.cwd.clone(), - env: build_config.env.clone(), - request: "launch".to_string(), - mode: "debug".to_string(), - program, - args, - build_flags, - }) - .unwrap(); - - Some(DebugScenario { - label: resolved_label.to_string().into(), - adapter: adapter.0.clone(), - build: None, - config, - tcp_connection: None, - }) - } - _ => None, - } - } - - async fn run(&self, _build_config: SpawnInTerminal) -> Result { - unreachable!() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::TestAppContext; - use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskTemplate}; - - #[gpui::test] - async fn test_create_scenario_for_go_build(_: &mut TestAppContext) { - let locator = GoLocator; - let task = TaskTemplate { - label: "go build".into(), - command: "go".into(), - args: vec!["build".into(), ".".into()], - env: Default::default(), - cwd: Some("${ZED_WORKTREE_ROOT}".into()), - use_new_terminal: false, - allow_concurrent_runs: false, - reveal: RevealStrategy::Always, - reveal_target: RevealTarget::Dock, - hide: HideStrategy::Never, - shell: Shell::System, - tags: vec![], - show_summary: true, - show_command: true, - }; - - let scenario = locator - .create_scenario(&task, "test label", &DebugAdapterName("Delve".into())) - .await; - - assert!(scenario.is_none()); - } - - #[gpui::test] - async fn test_skip_non_go_commands_with_non_delve_adapter(_: &mut TestAppContext) { - let locator = GoLocator; - let task = TaskTemplate { - label: "cargo build".into(), - command: "cargo".into(), - args: vec!["build".into()], - env: Default::default(), - cwd: Some("${ZED_WORKTREE_ROOT}".into()), - use_new_terminal: false, - allow_concurrent_runs: false, - reveal: RevealStrategy::Always, - reveal_target: RevealTarget::Dock, - hide: HideStrategy::Never, - shell: Shell::System, - tags: vec![], - show_summary: true, - show_command: true, - }; - - let scenario = locator - .create_scenario( - &task, - "test label", - &DebugAdapterName("SomeOtherAdapter".into()), - ) - .await; - assert!(scenario.is_none()); - - let scenario = locator - .create_scenario(&task, "test label", &DebugAdapterName("Delve".into())) - .await; - assert!(scenario.is_none()); - } - #[gpui::test] - async fn test_go_locator_run(_: &mut TestAppContext) { - let locator = GoLocator; - let delve = DebugAdapterName("Delve".into()); - - let task = TaskTemplate { - label: "go run with flags".into(), - command: "go".into(), - args: vec![ - "run".to_string(), - "-race".to_string(), - "-ldflags".to_string(), - "-X main.version=1.0".to_string(), - "./cmd/myapp".to_string(), - "--config".to_string(), - "production.yaml".to_string(), - "--verbose".to_string(), - ], - env: { - let mut env = HashMap::default(); - env.insert("GO_ENV".to_string(), "production".to_string()); - env - }, - cwd: Some("/project/root".into()), - ..Default::default() - }; - - let scenario = locator - .create_scenario(&task, "test run label", &delve) - .await - .unwrap(); - - let config: DelveLaunchRequest = serde_json::from_value(scenario.config).unwrap(); - - assert_eq!( - config, - DelveLaunchRequest { - request: "launch".to_string(), - mode: "debug".to_string(), - program: "./cmd/myapp".to_string(), - build_flags: vec![ - "-race".to_string(), - "-ldflags".to_string(), - "-X main.version=1.0".to_string() - ], - args: vec![ - "--config".to_string(), - "production.yaml".to_string(), - "--verbose".to_string(), - ], - env: { - let mut env = HashMap::default(); - env.insert("GO_ENV".to_string(), "production".to_string()); - env - }, - cwd: Some("/project/root".to_string()), - } - ); - } - - #[gpui::test] - async fn test_go_locator_test(_: &mut TestAppContext) { - let locator = GoLocator; - let delve = DebugAdapterName("Delve".into()); - - // Test with tags and run flag - let task_with_tags = TaskTemplate { - label: "test".into(), - command: "go".into(), - args: vec![ - "test".to_string(), - "-tags".to_string(), - "integration,unit".to_string(), - "-run".to_string(), - "Foo".to_string(), - ".".to_string(), - ], - ..Default::default() - }; - let result = locator - .create_scenario(&task_with_tags, "", &delve) - .await - .unwrap(); - - let config: DelveLaunchRequest = serde_json::from_value(result.config).unwrap(); - - assert_eq!( - config, - DelveLaunchRequest { - request: "launch".to_string(), - mode: "test".to_string(), - program: ".".to_string(), - build_flags: vec!["-tags".to_string(), "integration,unit".to_string(),], - args: vec![ - "-test.run".to_string(), - "Foo".to_string(), - "-test.v".to_string() - ], - env: HashMap::default(), - cwd: None, - } - ); - } - - #[gpui::test] - async fn test_skip_unsupported_go_commands(_: &mut TestAppContext) { - let locator = GoLocator; - let task = TaskTemplate { - label: "go clean".into(), - command: "go".into(), - args: vec!["clean".into()], - env: Default::default(), - cwd: Some("${ZED_WORKTREE_ROOT}".into()), - use_new_terminal: false, - allow_concurrent_runs: false, - reveal: RevealStrategy::Always, - reveal_target: RevealTarget::Dock, - hide: HideStrategy::Never, - shell: Shell::System, - tags: vec![], - show_summary: true, - show_command: true, - }; - - let scenario = locator - .create_scenario(&task, "test label", &DebugAdapterName("Delve".into())) - .await; - assert!(scenario.is_none()); - } -} diff --git a/crates/project/src/debugger/locators/node.rs b/crates/project/src/debugger/locators/node.rs deleted file mode 100644 index a535c7165a..0000000000 --- a/crates/project/src/debugger/locators/node.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::borrow::Cow; - -use anyhow::{Result, bail}; -use async_trait::async_trait; -use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName}; -use gpui::SharedString; - -use task::{DebugScenario, SpawnInTerminal, TaskTemplate, VariableName}; - -pub(crate) struct NodeLocator; - -const TYPESCRIPT_RUNNER_VARIABLE: VariableName = - VariableName::Custom(Cow::Borrowed("TYPESCRIPT_RUNNER")); - -#[async_trait] -impl DapLocator for NodeLocator { - fn name(&self) -> SharedString { - SharedString::new_static("Node") - } - - /// Determines whether this locator can generate debug target for given task. - async fn create_scenario( - &self, - build_config: &TaskTemplate, - resolved_label: &str, - adapter: &DebugAdapterName, - ) -> Option { - if adapter.0.as_ref() != "JavaScript" { - return None; - } - if build_config.command != TYPESCRIPT_RUNNER_VARIABLE.template_value() - && build_config.command != "npm" - && build_config.command != "pnpm" - && build_config.command != "yarn" - { - return None; - } - - let config = serde_json::json!({ - "request": "launch", - "type": "pwa-node", - "args": build_config.args.clone(), - "cwd": build_config.cwd.clone(), - "runtimeExecutable": build_config.command.clone(), - "env": build_config.env.clone(), - "runtimeArgs": ["--inspect-brk"], - "console": "integratedTerminal", - }); - - Some(DebugScenario { - adapter: adapter.0.clone(), - label: resolved_label.to_string().into(), - build: None, - config, - tcp_connection: None, - }) - } - - async fn run(&self, _: SpawnInTerminal) -> Result { - bail!("JavaScript locator should not require DapLocator::run to be ran"); - } -} diff --git a/crates/project/src/debugger/locators/python.rs b/crates/project/src/debugger/locators/python.rs deleted file mode 100644 index c3754548d0..0000000000 --- a/crates/project/src/debugger/locators/python.rs +++ /dev/null @@ -1,146 +0,0 @@ -use std::path::Path; - -use anyhow::{Result, bail}; -use async_trait::async_trait; -use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName}; -use gpui::SharedString; - -use task::{DebugScenario, SpawnInTerminal, TaskTemplate, VariableName}; - -pub(crate) struct PythonLocator; - -#[async_trait] -impl DapLocator for PythonLocator { - fn name(&self) -> SharedString { - SharedString::new_static("Python") - } - - /// Determines whether this locator can generate debug target for given task. - async fn create_scenario( - &self, - build_config: &TaskTemplate, - resolved_label: &str, - adapter: &DebugAdapterName, - ) -> Option { - if adapter.0.as_ref() != "Debugpy" { - return None; - } - 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"))); - if !valid_program || build_config.args.iter().any(|arg| arg == "-c") { - // We cannot debug selections. - return None; - } - let command = build_config.command.clone(); - let module_specifier_position = build_config - .args - .iter() - .position(|arg| arg == "-m") - .map(|position| position + 1); - // Skip the -m and module name, get all that's after. - let mut rest_of_the_args = module_specifier_position - .and_then(|position| build_config.args.get(position..)) - .into_iter() - .flatten() - .fuse(); - let mod_name = rest_of_the_args.next(); - let args = rest_of_the_args.collect::>(); - - let program_position = mod_name - .is_none() - .then(|| { - let zed_file = VariableName::File.template_value_with_whitespace(); - build_config.args.iter().position(|arg| *arg == zed_file) - }) - .flatten(); - let args = if let Some(position) = program_position { - args.into_iter().skip(position).collect::>() - } else { - args - }; - if program_position.is_none() && mod_name.is_none() { - return None; - } - let mut config = serde_json::json!({ - "request": "launch", - "python": command, - "args": args, - "cwd": build_config.cwd.clone() - }); - if let Some(config_obj) = config.as_object_mut() { - if let Some(module) = mod_name { - config_obj.insert("module".to_string(), module.clone().into()); - } - if let Some(program) = program_position { - config_obj.insert( - "program".to_string(), - build_config.args[program].clone().into(), - ); - } - } - - Some(DebugScenario { - adapter: adapter.0.clone(), - label: resolved_label.to_string().into(), - build: None, - config, - tcp_connection: None, - }) - } - - async fn run(&self, _: SpawnInTerminal) -> Result { - bail!("Python locator should not require DapLocator::run to be ran"); - } -} - -#[cfg(test)] -mod test { - use serde_json::json; - - use super::*; - - #[gpui::test] - async fn test_python_locator() { - let adapter = DebugAdapterName("Debugpy".into()); - let build_task = TaskTemplate { - label: "run module '$ZED_FILE'".into(), - command: "$ZED_CUSTOM_PYTHON_ACTIVE_ZED_TOOLCHAIN".into(), - args: vec!["-m".into(), "$ZED_CUSTOM_PYTHON_MODULE_NAME".into()], - env: Default::default(), - cwd: Some("$ZED_WORKTREE_ROOT".into()), - use_new_terminal: false, - allow_concurrent_runs: false, - reveal: task::RevealStrategy::Always, - reveal_target: task::RevealTarget::Dock, - hide: task::HideStrategy::Never, - tags: vec!["python-module-main-method".into()], - shell: task::Shell::System, - show_summary: false, - show_command: false, - }; - - let expected_scenario = DebugScenario { - adapter: "Debugpy".into(), - label: "run module 'main.py'".into(), - build: None, - config: json!({ - "request": "launch", - "python": "$ZED_CUSTOM_PYTHON_ACTIVE_ZED_TOOLCHAIN", - "args": [], - "cwd": "$ZED_WORKTREE_ROOT", - "module": "$ZED_CUSTOM_PYTHON_MODULE_NAME", - }), - tcp_connection: None, - }; - - assert_eq!( - PythonLocator - .create_scenario(&build_task, "run module 'main.py'", &adapter) - .await - .expect("Failed to create a scenario"), - expected_scenario - ); - } -} diff --git a/crates/project/src/debugger/memory.rs b/crates/project/src/debugger/memory.rs deleted file mode 100644 index 42ad64e688..0000000000 --- a/crates/project/src/debugger/memory.rs +++ /dev/null @@ -1,384 +0,0 @@ -//! This module defines the format in which memory of debuggee is represented. -//! -//! 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. -//! Note that per DAP we don't know what the address space layout is, so we can't optimize off of it. -//! Note that while we optimize for a paged layout, we also want to be able to represent memory that is not paged. -//! This use case is relevant to embedded folks. Furthermore, we cater to default 4k page size. -//! It is picked arbitrarily as a ubiquous default - other than that, the underlying format of Zed's memory storage should not be relevant -//! to the users of this module. - -use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc}; - -use gpui::BackgroundExecutor; -use smallvec::SmallVec; - -const PAGE_SIZE: u64 = 4096; - -/// Represents the contents of a single page. We special-case unmapped pages to be allocation-free, -/// since they're going to make up the majority of the memory in a program space (even though the user might not even get to see them - ever). -#[derive(Clone, Debug)] -pub(super) enum PageContents { - /// Whole page is unreadable. - Unmapped, - Mapped(Arc), -} - -impl PageContents { - #[cfg(test)] - fn mapped(contents: Vec) -> Self { - PageContents::Mapped(Arc::new(MappedPageContents( - vec![PageChunk::Mapped(contents.into())].into(), - ))) - } -} - -#[derive(Clone, Debug)] -enum PageChunk { - Mapped(Arc<[u8]>), - Unmapped(u64), -} - -impl PageChunk { - fn len(&self) -> u64 { - match self { - PageChunk::Mapped(contents) => contents.len() as u64, - PageChunk::Unmapped(size) => *size, - } - } -} - -impl MappedPageContents { - fn len(&self) -> u64 { - self.0.iter().map(|chunk| chunk.len()).sum() - } -} -/// We hope for the whole page to be mapped in a single chunk, but we do leave the possibility open -/// of having interleaved read permissions in a single page; debuggee's execution environment might either -/// have a different page size OR it might not have paged memory layout altogether -/// (which might be relevant to embedded systems). -/// -/// As stated previously, the concept of a page in this module has to do more -/// with optimizing fetching of the memory and not with the underlying bits and pieces -/// of the memory of a debuggee. - -#[derive(Default, Debug)] -pub(super) struct MappedPageContents( - /// Most of the time there should be only one chunk (either mapped or unmapped), - /// but we do leave the possibility open of having multiple regions of memory in a single page. - SmallVec<[PageChunk; 1]>, -); - -type MemoryAddress = u64; -#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq)] -#[repr(transparent)] -pub(super) struct PageAddress(u64); - -impl PageAddress { - pub(super) fn iter_range( - range: RangeInclusive, - ) -> impl Iterator { - let mut current = range.start().0; - let end = range.end().0; - - std::iter::from_fn(move || { - if current > end { - None - } else { - let addr = PageAddress(current); - current += PAGE_SIZE; - Some(addr) - } - }) - } -} - -pub(super) struct Memory { - pages: BTreeMap, -} - -/// Represents a single memory cell (or None if a given cell is unmapped/unknown). -#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Ord, Eq)] -#[repr(transparent)] -pub struct MemoryCell(pub Option); - -impl Memory { - pub(super) fn new() -> Self { - Self { - pages: Default::default(), - } - } - - pub(super) fn memory_range_to_page_range( - range: RangeInclusive, - ) -> RangeInclusive { - let start_page = (range.start() / PAGE_SIZE) * PAGE_SIZE; - let end_page = (range.end() / PAGE_SIZE) * PAGE_SIZE; - PageAddress(start_page)..=PageAddress(end_page) - } - - pub(super) fn build_page(&self, page_address: PageAddress) -> Option { - if self.pages.contains_key(&page_address) { - // We already know the state of this page. - None - } else { - Some(MemoryPageBuilder::new(page_address)) - } - } - - pub(super) fn insert_page(&mut self, address: PageAddress, page: PageContents) { - self.pages.insert(address, page); - } - - pub(super) fn memory_range(&self, range: RangeInclusive) -> MemoryIterator { - let pages = Self::memory_range_to_page_range(range.clone()); - let pages = self - .pages - .range(pages) - .map(|(address, page)| (*address, page.clone())) - .collect::>(); - MemoryIterator::new(range, pages.into_iter()) - } - - pub(crate) fn clear(&mut self, background_executor: &BackgroundExecutor) { - let memory = std::mem::take(&mut self.pages); - background_executor - .spawn(async move { - drop(memory); - }) - .detach(); - } -} - -/// Builder for memory pages. -/// -/// Memory reads in DAP are sequential (or at least we make them so). -/// ReadMemory response includes `unreadableBytes` property indicating the number of bytes -/// that could not be read after the last successfully read byte. -/// -/// We use it as follows: -/// - We start off with a "large" 1-page ReadMemory request. -/// - 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, - base_address: PageAddress, - left_to_read: u64, -} - -/// Represents a chunk of memory of which we don't know if it's mapped or unmapped; thus we need -/// to issue a request to figure out it's state. -pub(super) struct UnknownMemory { - pub(super) address: MemoryAddress, - pub(super) size: u64, -} - -impl MemoryPageBuilder { - fn new(base_address: PageAddress) -> Self { - Self { - chunks: Default::default(), - base_address, - left_to_read: PAGE_SIZE, - } - } - - pub(super) fn build(self) -> (PageAddress, PageContents) { - debug_assert_eq!(self.left_to_read, 0); - debug_assert_eq!( - self.chunks.len(), - PAGE_SIZE, - "Expected `build` to be called on a fully-fetched page" - ); - let contents = if let Some(first) = self.chunks.0.first() - && self.chunks.len() == 1 - && matches!(first, PageChunk::Unmapped(PAGE_SIZE)) - { - PageContents::Unmapped - } else { - PageContents::Mapped(Arc::new(MappedPageContents(self.chunks.0))) - }; - (self.base_address, contents) - } - /// Drives the fetching of memory, in an iterator-esque style. - pub(super) fn next_request(&self) -> Option { - if self.left_to_read == 0 { - None - } else { - let offset_in_current_page = PAGE_SIZE - self.left_to_read; - Some(UnknownMemory { - address: self.base_address.0 + offset_in_current_page, - size: self.left_to_read, - }) - } - } - pub(super) fn unknown(&mut self, bytes: u64) { - if bytes == 0 { - return; - } - self.left_to_read -= bytes; - self.chunks.0.push(PageChunk::Unmapped(bytes)); - } - pub(super) fn known(&mut self, data: Arc<[u8]>) { - if data.is_empty() { - return; - } - self.left_to_read -= data.len() as u64; - self.chunks.0.push(PageChunk::Mapped(data)); - } -} - -fn page_contents_into_iter(data: Arc) -> Box> { - let mut data_range = 0..data.0.len(); - let iter = std::iter::from_fn(move || { - let data = &data; - let data_ref = data.clone(); - data_range.next().map(move |index| { - let contents = &data_ref.0[index]; - match contents { - PageChunk::Mapped(items) => { - let chunk_range = 0..items.len(); - let items = items.clone(); - Box::new( - chunk_range - .into_iter() - .map(move |ix| MemoryCell(Some(items[ix]))), - ) as Box> - } - PageChunk::Unmapped(len) => { - Box::new(std::iter::repeat_n(MemoryCell(None), *len as usize)) - } - } - }) - }) - .flatten(); - - Box::new(iter) -} -/// Defines an iteration over a range of memory. Some of this memory might be unmapped or straight up missing. -/// Thus, this iterator alternates between synthesizing values and yielding known memory. -pub struct MemoryIterator { - start: MemoryAddress, - end: MemoryAddress, - current_known_page: Option<(PageAddress, Box>)>, - pages: std::vec::IntoIter<(PageAddress, PageContents)>, -} - -impl MemoryIterator { - fn new( - range: RangeInclusive, - pages: std::vec::IntoIter<(PageAddress, PageContents)>, - ) -> Self { - Self { - start: *range.start(), - end: *range.end(), - current_known_page: None, - pages, - } - } - fn fetch_next_page(&mut self) -> bool { - if let Some((mut address, chunk)) = self.pages.next() { - let mut contents = match chunk { - PageContents::Unmapped => None, - PageContents::Mapped(mapped_page_contents) => { - Some(page_contents_into_iter(mapped_page_contents)) - } - }; - - if address.0 < self.start { - // Skip ahead till our iterator is at the start of the range - - //address: 20, start: 25 - // - let to_skip = self.start - address.0; - address.0 += to_skip; - if let Some(contents) = &mut contents { - contents.nth(to_skip as usize - 1); - } - } - self.current_known_page = contents.map(|contents| (address, contents)); - true - } else { - false - } - } -} -impl Iterator for MemoryIterator { - type Item = MemoryCell; - - fn next(&mut self) -> Option { - if self.start > self.end { - 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 !self.fetch_next_page() { - self.start += 1; - Some(MemoryCell(None)) - } else { - self.next() - } - } -} - -#[cfg(test)] -mod tests { - use crate::debugger::{ - MemoryCell, - memory::{MemoryIterator, PageAddress, PageContents}, - }; - - #[test] - fn iterate_over_unmapped_memory() { - let empty_iterator = MemoryIterator::new(0..=127, Default::default()); - let actual = empty_iterator.collect::>(); - let expected = vec![MemoryCell(None); 128]; - assert_eq!(actual.len(), expected.len()); - assert_eq!(actual, expected); - } - - #[test] - fn iterate_over_partially_mapped_memory() { - let it = MemoryIterator::new( - 0..=127, - vec![(PageAddress(5), PageContents::mapped(vec![1]))].into_iter(), - ); - let actual = it.collect::>(); - let expected = std::iter::repeat_n(MemoryCell(None), 5) - .chain(std::iter::once(MemoryCell(Some(1)))) - .chain(std::iter::repeat_n(MemoryCell(None), 122)) - .collect::>(); - assert_eq!(actual.len(), expected.len()); - assert_eq!(actual, expected); - } - - #[test] - fn reads_from_the_middle_of_a_page() { - let partial_iter = MemoryIterator::new( - 20..=30, - vec![(PageAddress(0), PageContents::mapped((0..255).collect()))].into_iter(), - ); - let actual = partial_iter.collect::>(); - let expected = (20..=30) - .map(|val| MemoryCell(Some(val))) - .collect::>(); - assert_eq!(actual.len(), expected.len()); - assert_eq!(actual, expected); - } -} diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs deleted file mode 100644 index 82a139ea24..0000000000 --- a/crates/project/src/debugger/session.rs +++ /dev/null @@ -1,3177 +0,0 @@ -use super::breakpoint_store::{ - BreakpointStore, BreakpointStoreEvent, BreakpointUpdatedReason, SourceBreakpoint, -}; -use super::dap_command::{ - self, Attach, ConfigurationDone, ContinueCommand, DataBreakpointInfoCommand, DisconnectCommand, - EvaluateCommand, Initialize, Launch, LoadedSourcesCommand, LocalDapCommand, LocationsCommand, - ModulesCommand, NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand, - ScopesCommand, SetDataBreakpointsCommand, SetExceptionBreakpoints, SetVariableValueCommand, - StackTraceCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand, - TerminateCommand, TerminateThreadsCommand, ThreadsCommand, VariablesCommand, -}; -use super::dap_store::DapStore; -use crate::debugger::breakpoint_store::BreakpointSessionState; -use crate::debugger::dap_command::{DataBreakpointContext, ReadMemory}; -use crate::debugger::memory::{self, Memory, MemoryIterator, MemoryPageBuilder, PageAddress}; -use anyhow::{Context as _, Result, anyhow, bail}; -use base64::Engine; -use collections::{HashMap, HashSet, IndexMap}; -use dap::adapters::{DebugAdapterBinary, DebugAdapterName}; -use dap::messages::Response; -use dap::requests::{Request, RunInTerminal, StartDebugging}; -use dap::transport::TcpTransport; -use dap::{ - Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, StackFrameId, - SteppingGranularity, StoppedEvent, VariableReference, - client::{DebugAdapterClient, SessionId}, - messages::{Events, Message}, -}; -use dap::{ - ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEvent, OutputEventCategory, - RunInTerminalRequestArguments, StackFramePresentationHint, StartDebuggingRequestArguments, - StartDebuggingRequestArgumentsRequest, VariablePresentationHint, WriteMemoryArguments, -}; -use futures::channel::mpsc::UnboundedSender; -use futures::channel::{mpsc, oneshot}; -use futures::io::BufReader; -use futures::{AsyncBufReadExt as _, SinkExt, StreamExt, TryStreamExt}; -use futures::{FutureExt, future::Shared}; -use gpui::{ - App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, SharedString, - Task, WeakEntity, -}; -use http_client::HttpClient; -use node_runtime::NodeRuntime; -use remote::RemoteClient; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use smol::net::{TcpListener, TcpStream}; -use std::any::TypeId; -use std::collections::{BTreeMap, VecDeque}; -use std::net::Ipv4Addr; -use std::ops::RangeInclusive; -use std::path::PathBuf; -use std::process::Stdio; -use std::time::Duration; -use std::u64; -use std::{ - any::Any, - collections::hash_map::Entry, - hash::{Hash, Hasher}, - path::Path, - sync::Arc, -}; -use task::TaskContext; -use text::{PointUtf16, ToPointUtf16}; -use url::Url; -use util::command::new_smol_command; -use util::{ResultExt, debug_panic, maybe}; -use worktree::Worktree; - -const MAX_TRACKED_OUTPUT_EVENTS: usize = 5000; -const DEBUG_HISTORY_LIMIT: usize = 10; - -#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd, Ord, Eq)] -#[repr(transparent)] -pub struct ThreadId(pub i64); - -impl From for ThreadId { - fn from(id: i64) -> Self { - Self(id) - } -} - -#[derive(Clone, Debug)] -pub struct StackFrame { - pub dap: dap::StackFrame, - pub scopes: Vec, -} - -impl From for StackFrame { - fn from(stack_frame: dap::StackFrame) -> Self { - Self { - scopes: vec![], - dap: stack_frame, - } - } -} - -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub enum ThreadStatus { - #[default] - Running, - Stopped, - Stepping, - Exited, - Ended, -} - -impl ThreadStatus { - pub fn label(&self) -> &'static str { - match self { - ThreadStatus::Running => "Running", - ThreadStatus::Stopped => "Stopped", - ThreadStatus::Stepping => "Stepping", - ThreadStatus::Exited => "Exited", - ThreadStatus::Ended => "Ended", - } - } -} - -#[derive(Debug, Clone)] -pub struct Thread { - dap: dap::Thread, - stack_frames: Vec, - stack_frames_error: Option, - _has_stopped: bool, -} - -impl From for Thread { - fn from(dap: dap::Thread) -> Self { - Self { - dap, - stack_frames: Default::default(), - stack_frames_error: None, - _has_stopped: false, - } - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Watcher { - pub expression: SharedString, - pub value: SharedString, - pub variables_reference: u64, - pub presentation_hint: Option, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct DataBreakpointState { - pub dap: dap::DataBreakpoint, - pub is_enabled: bool, - pub context: Arc, -} - -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>>), - Running(RunningMode), -} - -#[derive(Clone)] -pub struct RunningMode { - client: Arc, - binary: DebugAdapterBinary, - tmp_breakpoint: Option, - worktree: WeakEntity, - executor: BackgroundExecutor, - is_started: bool, - has_ever_stopped: bool, - messages_tx: UnboundedSender, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] -pub struct SessionQuirks { - pub compact: bool, - pub prefer_thread_name: bool, -} - -fn client_source(abs_path: &Path) -> dap::Source { - dap::Source { - name: abs_path - .file_name() - .map(|filename| filename.to_string_lossy().into_owned()), - path: Some(abs_path.to_string_lossy().into_owned()), - source_reference: None, - presentation_hint: None, - origin: None, - sources: None, - adapter_data: None, - checksums: None, - } -} - -impl RunningMode { - async fn new( - session_id: SessionId, - parent_session: Option>, - worktree: WeakEntity, - binary: DebugAdapterBinary, - messages_tx: futures::channel::mpsc::UnboundedSender, - cx: &mut AsyncApp, - ) -> Result { - let message_handler = Box::new({ - let messages_tx = messages_tx.clone(); - move |message| { - messages_tx.unbounded_send(message).ok(); - } - }); - - let client = if let Some(client) = parent_session - .and_then(|session| cx.update(|cx| session.read(cx).adapter_client()).ok()) - .flatten() - { - client - .create_child_connection(session_id, binary.clone(), message_handler, cx) - .await? - } else { - DebugAdapterClient::start(session_id, binary.clone(), message_handler, cx).await? - }; - - Ok(Self { - client: Arc::new(client), - worktree, - tmp_breakpoint: None, - binary, - executor: cx.background_executor().clone(), - is_started: false, - has_ever_stopped: false, - messages_tx, - }) - } - - pub(crate) fn worktree(&self) -> &WeakEntity { - &self.worktree - } - - fn unset_breakpoints_from_paths(&self, paths: &Vec>, cx: &mut App) -> Task<()> { - let tasks: Vec<_> = paths - .iter() - .map(|path| { - self.request(dap_command::SetBreakpoints { - source: client_source(path), - source_modified: None, - breakpoints: vec![], - }) - }) - .collect(); - - cx.background_spawn(async move { - futures::future::join_all(tasks) - .await - .iter() - .for_each(|res| match res { - Ok(_) => {} - Err(err) => { - log::warn!("Set breakpoints request failed: {}", err); - } - }); - }) - } - - fn send_breakpoints_from_path( - &self, - abs_path: Arc, - reason: BreakpointUpdatedReason, - breakpoint_store: &Entity, - cx: &mut App, - ) -> Task<()> { - let breakpoints = - breakpoint_store - .read(cx) - .source_breakpoints_from_path(&abs_path, cx) - .into_iter() - .filter(|bp| bp.state.is_enabled()) - .chain(self.tmp_breakpoint.iter().filter_map(|breakpoint| { - breakpoint.path.eq(&abs_path).then(|| breakpoint.clone()) - })) - .map(Into::into) - .collect(); - - let raw_breakpoints = breakpoint_store - .read(cx) - .breakpoints_from_path(&abs_path) - .into_iter() - .filter(|bp| bp.bp.state.is_enabled()) - .collect::>(); - - let task = self.request(dap_command::SetBreakpoints { - source: client_source(&abs_path), - source_modified: Some(matches!(reason, BreakpointUpdatedReason::FileSaved)), - breakpoints, - }); - let session_id = self.client.id(); - let breakpoint_store = breakpoint_store.downgrade(); - cx.spawn(async move |cx| match cx.background_spawn(task).await { - Ok(breakpoints) => { - let breakpoints = - breakpoints - .into_iter() - .zip(raw_breakpoints) - .filter_map(|(dap_bp, zed_bp)| { - Some(( - zed_bp, - BreakpointSessionState { - id: dap_bp.id?, - verified: dap_bp.verified, - }, - )) - }); - breakpoint_store - .update(cx, |this, _| { - this.mark_breakpoints_verified(session_id, &abs_path, breakpoints); - }) - .ok(); - } - Err(err) => log::warn!("Set breakpoints request failed for path: {}", err), - }) - } - - fn send_exception_breakpoints( - &self, - filters: Vec, - supports_filter_options: bool, - ) -> Task>> { - let arg = if supports_filter_options { - SetExceptionBreakpoints::WithOptions { - filters: filters - .into_iter() - .map(|filter| ExceptionFilterOptions { - filter_id: filter.filter, - condition: None, - mode: None, - }) - .collect(), - } - } else { - SetExceptionBreakpoints::Plain { - filters: filters.into_iter().map(|filter| filter.filter).collect(), - } - }; - self.request(arg) - } - - fn send_source_breakpoints( - &self, - ignore_breakpoints: bool, - breakpoint_store: &Entity, - cx: &App, - ) -> Task, anyhow::Error>> { - let mut breakpoint_tasks = Vec::new(); - let breakpoints = breakpoint_store.read(cx).all_source_breakpoints(cx); - let mut raw_breakpoints = breakpoint_store.read_with(cx, |this, _| this.all_breakpoints()); - debug_assert_eq!(raw_breakpoints.len(), breakpoints.len()); - let session_id = self.client.id(); - for (path, breakpoints) in breakpoints { - let breakpoints = if ignore_breakpoints { - vec![] - } else { - breakpoints - .into_iter() - .filter(|bp| bp.state.is_enabled()) - .map(Into::into) - .collect() - }; - - let raw_breakpoints = raw_breakpoints - .remove(&path) - .unwrap_or_default() - .into_iter() - .filter(|bp| bp.bp.state.is_enabled()); - let error_path = path.clone(); - let send_request = self - .request(dap_command::SetBreakpoints { - source: client_source(&path), - source_modified: Some(false), - breakpoints, - }) - .map(|result| result.map_err(move |e| (error_path, e))); - - let task = cx.spawn({ - let breakpoint_store = breakpoint_store.downgrade(); - async move |cx| { - let breakpoints = cx.background_spawn(send_request).await?; - - let breakpoints = breakpoints.into_iter().zip(raw_breakpoints).filter_map( - |(dap_bp, zed_bp)| { - Some(( - zed_bp, - BreakpointSessionState { - id: dap_bp.id?, - verified: dap_bp.verified, - }, - )) - }, - ); - breakpoint_store - .update(cx, |this, _| { - this.mark_breakpoints_verified(session_id, &path, breakpoints); - }) - .ok(); - - Ok(()) - } - }); - breakpoint_tasks.push(task); - } - - cx.background_spawn(async move { - futures::future::join_all(breakpoint_tasks) - .await - .into_iter() - .filter_map(Result::err) - .collect::>() - }) - } - - fn initialize_sequence( - &self, - capabilities: &Capabilities, - initialized_rx: oneshot::Receiver<()>, - dap_store: WeakEntity, - cx: &mut Context, - ) -> Task> { - let raw = self.binary.request_args.clone(); - - // Of relevance: https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522 - let launch = match raw.request { - dap::StartDebuggingRequestArgumentsRequest::Launch => self.request(Launch { - raw: raw.configuration, - }), - dap::StartDebuggingRequestArgumentsRequest::Attach => self.request(Attach { - raw: raw.configuration, - }), - }; - - let configuration_done_supported = ConfigurationDone::is_supported(capabilities); - // From spec (on initialization sequence): - // client sends a setExceptionBreakpoints request if one or more exceptionBreakpointFilters have been defined (or if supportsConfigurationDoneRequest is not true) - // - // Thus we should send setExceptionBreakpoints even if `exceptionFilters` variable is empty (as long as there were some options in the first place). - let should_send_exception_breakpoints = capabilities - .exception_breakpoint_filters - .as_ref() - .is_some_and(|filters| !filters.is_empty()) - || !configuration_done_supported; - let supports_exception_filters = capabilities - .supports_exception_filter_options - .unwrap_or_default(); - let this = self.clone(); - let worktree = self.worktree().clone(); - let mut filters = capabilities - .exception_breakpoint_filters - .clone() - .unwrap_or_default(); - let configuration_sequence = cx.spawn({ - async move |session, cx| { - let adapter_name = session.read_with(cx, |this, _| this.adapter())?; - let (breakpoint_store, adapter_defaults) = - dap_store.read_with(cx, |dap_store, _| { - ( - dap_store.breakpoint_store().clone(), - dap_store.adapter_options(&adapter_name), - ) - })?; - initialized_rx.await?; - let errors_by_path = cx - .update(|cx| this.send_source_breakpoints(false, &breakpoint_store, cx))? - .await; - - dap_store.update(cx, |_, cx| { - let Some(worktree) = worktree.upgrade() else { - return; - }; - - for (path, error) in &errors_by_path { - log::error!("failed to set breakpoints for {path:?}: {error}"); - } - - if let Some(failed_path) = errors_by_path.keys().next() { - let failed_path = failed_path - .strip_prefix(worktree.read(cx).abs_path()) - .unwrap_or(failed_path) - .display(); - let message = format!( - "Failed to set breakpoints for {failed_path}{}", - match errors_by_path.len() { - 0 => unreachable!(), - 1 => "".into(), - 2 => " and 1 other path".into(), - n => format!(" and {} other paths", n - 1), - } - ); - cx.emit(super::dap_store::DapStoreEvent::Notification(message)); - } - })?; - - if should_send_exception_breakpoints { - _ = session.update(cx, |this, _| { - filters.retain(|filter| { - let is_enabled = if let Some(defaults) = adapter_defaults.as_ref() { - defaults - .exception_breakpoints - .get(&filter.filter) - .map(|options| options.enabled) - .unwrap_or_else(|| filter.default.unwrap_or_default()) - } else { - filter.default.unwrap_or_default() - }; - this.exception_breakpoints - .entry(filter.filter.clone()) - .or_insert_with(|| (filter.clone(), is_enabled)); - is_enabled - }); - }); - - this.send_exception_breakpoints(filters, supports_exception_filters) - .await - .ok(); - } - - if configuration_done_supported { - this.request(ConfigurationDone {}) - } else { - Task::ready(Ok(())) - } - .await - } - }); - - let task = cx.background_spawn(futures::future::try_join(launch, configuration_sequence)); - - cx.spawn(async move |this, cx| { - let result = task.await; - - this.update(cx, |this, cx| { - if let Some(this) = this.as_running_mut() { - this.is_started = true; - cx.notify(); - } - }) - .ok(); - - result?; - anyhow::Ok(()) - }) - } - - fn reconnect_for_ssh(&self, cx: &mut AsyncApp) -> Option>> { - let client = self.client.clone(); - let messages_tx = self.messages_tx.clone(); - let message_handler = Box::new(move |message| { - messages_tx.unbounded_send(message).ok(); - }); - if client.should_reconnect_for_ssh() { - Some(cx.spawn(async move |cx| { - client.connect(message_handler, cx).await?; - anyhow::Ok(()) - })) - } else { - None - } - } - - fn request(&self, request: R) -> Task> - where - ::Response: 'static, - ::Arguments: 'static + Send, - { - let request = Arc::new(request); - - let request_clone = request.clone(); - let connection = self.client.clone(); - self.executor.spawn(async move { - let args = request_clone.to_dap(); - let response = connection.request::(args).await?; - request.response_from_dap(response) - }) - } -} - -impl SessionState { - pub(super) fn request_dap(&self, request: R) -> Task> - where - ::Response: 'static, - ::Arguments: 'static + Send, - { - match self { - SessionState::Running(debug_adapter_client) => debug_adapter_client.request(request), - SessionState::Booting(_) => Task::ready(Err(anyhow!( - "no adapter running to send request: {request:?}" - ))), - } - } - - /// Did this debug session stop at least once? - pub(crate) fn has_ever_stopped(&self) -> bool { - match self { - SessionState::Booting(_) => false, - SessionState::Running(running_mode) => running_mode.has_ever_stopped, - } - } - - fn stopped(&mut self) { - if let SessionState::Running(running) = self { - running.has_ever_stopped = true; - } - } -} - -#[derive(Default)] -struct ThreadStates { - global_state: Option, - known_thread_states: IndexMap, -} - -impl ThreadStates { - fn stop_all_threads(&mut self) { - self.global_state = Some(ThreadStatus::Stopped); - self.known_thread_states.clear(); - } - - fn exit_all_threads(&mut self) { - self.global_state = Some(ThreadStatus::Exited); - self.known_thread_states.clear(); - } - - fn continue_all_threads(&mut self) { - self.global_state = Some(ThreadStatus::Running); - self.known_thread_states.clear(); - } - - fn stop_thread(&mut self, thread_id: ThreadId) { - self.known_thread_states - .insert(thread_id, ThreadStatus::Stopped); - } - - fn continue_thread(&mut self, thread_id: ThreadId) { - self.known_thread_states - .insert(thread_id, ThreadStatus::Running); - } - - fn process_step(&mut self, thread_id: ThreadId) { - self.known_thread_states - .insert(thread_id, ThreadStatus::Stepping); - } - - fn thread_status(&self, thread_id: ThreadId) -> ThreadStatus { - self.thread_state(thread_id) - .unwrap_or(ThreadStatus::Running) - } - - fn thread_state(&self, thread_id: ThreadId) -> Option { - self.known_thread_states - .get(&thread_id) - .copied() - .or(self.global_state) - } - - fn exit_thread(&mut self, thread_id: ThreadId) { - self.known_thread_states - .insert(thread_id, ThreadStatus::Exited); - } - - fn any_stopped_thread(&self) -> bool { - self.global_state - .is_some_and(|state| state == ThreadStatus::Stopped) - || self - .known_thread_states - .values() - .any(|status| *status == ThreadStatus::Stopped) - } -} - -// TODO(debugger): Wrap dap types with reference counting so the UI doesn't have to clone them on refresh -#[derive(Default)] -pub struct SessionSnapshot { - threads: IndexMap, - thread_states: ThreadStates, - variables: HashMap>, - stack_frames: IndexMap, - locations: HashMap, - modules: Vec, - loaded_sources: Vec, -} - -type IsEnabled = bool; - -#[derive(Copy, Clone, Default, Debug, PartialEq, PartialOrd, Eq, Ord)] -pub struct OutputToken(pub usize); -/// Represents a current state of a single debug adapter and provides ways to mutate it. -pub struct Session { - pub state: SessionState, - active_snapshot: SessionSnapshot, - snapshots: VecDeque, - selected_snapshot_index: Option, - id: SessionId, - label: Option, - adapter: DebugAdapterName, - pub(super) capabilities: Capabilities, - child_session_ids: HashSet, - parent_session: Option>, - output_token: OutputToken, - output: Box>, - watchers: HashMap, - is_session_terminated: bool, - requests: HashMap>>>>, - pub(crate) breakpoint_store: Entity, - ignore_breakpoints: bool, - exception_breakpoints: BTreeMap, - data_breakpoints: BTreeMap, - background_tasks: Vec>, - restart_task: Option>, - task_context: TaskContext, - memory: memory::Memory, - quirks: SessionQuirks, - remote_client: Option>, - node_runtime: Option, - http_client: Option>, - companion_port: Option, -} - -trait CacheableCommand: Any + Send + Sync { - fn dyn_eq(&self, rhs: &dyn CacheableCommand) -> bool; - fn dyn_hash(&self, hasher: &mut dyn Hasher); - fn as_any_arc(self: Arc) -> Arc; -} - -impl CacheableCommand for T -where - T: LocalDapCommand + PartialEq + Eq + Hash, -{ - fn dyn_eq(&self, rhs: &dyn CacheableCommand) -> bool { - (rhs as &dyn Any).downcast_ref::() == Some(self) - } - - fn dyn_hash(&self, mut hasher: &mut dyn Hasher) { - T::hash(self, &mut hasher); - } - - fn as_any_arc(self: Arc) -> Arc { - self - } -} - -pub(crate) struct RequestSlot(Arc); - -impl From for RequestSlot { - fn from(request: T) -> Self { - Self(Arc::new(request)) - } -} - -impl PartialEq for RequestSlot { - fn eq(&self, other: &Self) -> bool { - self.0.dyn_eq(other.0.as_ref()) - } -} - -impl Eq for RequestSlot {} - -impl Hash for RequestSlot { - fn hash(&self, state: &mut H) { - self.0.dyn_hash(state); - (&*self.0 as &dyn Any).type_id().hash(state) - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub struct CompletionsQuery { - pub query: String, - pub column: u64, - pub line: Option, - pub frame_id: Option, -} - -impl CompletionsQuery { - pub fn new( - buffer: &language::Buffer, - cursor_position: language::Anchor, - frame_id: Option, - ) -> Self { - let PointUtf16 { row, column } = cursor_position.to_point_utf16(&buffer.snapshot()); - Self { - query: buffer.text(), - column: column as u64, - frame_id, - line: Some(row as u64), - } - } -} - -#[derive(Debug)] -pub enum SessionEvent { - Modules, - LoadedSources, - Stopped(Option), - StackTrace, - Variables, - Watchers, - Threads, - InvalidateInlineValue, - CapabilitiesLoaded, - RunInTerminal { - request: RunInTerminalRequestArguments, - sender: mpsc::Sender>, - }, - DataBreakpointInfo, - ConsoleOutput, - HistoricSnapshotSelected, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum SessionStateEvent { - Running, - Shutdown, - Restart, - SpawnChildSession { - request: StartDebuggingRequestArguments, - }, -} - -impl EventEmitter for Session {} -impl EventEmitter for Session {} - -// local session will send breakpoint updates to DAP for all new breakpoints -// remote side will only send breakpoint updates when it is a breakpoint created by that peer -// BreakpointStore notifies session on breakpoint changes -impl Session { - pub(crate) fn new( - breakpoint_store: Entity, - session_id: SessionId, - parent_session: Option>, - label: Option, - adapter: DebugAdapterName, - task_context: TaskContext, - quirks: SessionQuirks, - remote_client: Option>, - node_runtime: Option, - http_client: Option>, - cx: &mut App, - ) -> Entity { - cx.new::(|cx| { - cx.subscribe(&breakpoint_store, |this, store, event, cx| match event { - BreakpointStoreEvent::BreakpointsUpdated(path, reason) => { - if let Some(local) = (!this.ignore_breakpoints) - .then(|| this.as_running_mut()) - .flatten() - { - local - .send_breakpoints_from_path(path.clone(), *reason, &store, cx) - .detach(); - }; - } - BreakpointStoreEvent::BreakpointsCleared(paths) => { - if let Some(local) = (!this.ignore_breakpoints) - .then(|| this.as_running_mut()) - .flatten() - { - local.unset_breakpoints_from_paths(paths, cx).detach(); - } - } - BreakpointStoreEvent::SetDebugLine | BreakpointStoreEvent::ClearDebugLines => {} - }) - .detach(); - - Self { - state: SessionState::Booting(None), - snapshots: VecDeque::with_capacity(DEBUG_HISTORY_LIMIT), - selected_snapshot_index: None, - active_snapshot: Default::default(), - id: session_id, - child_session_ids: HashSet::default(), - parent_session, - capabilities: Capabilities::default(), - watchers: HashMap::default(), - output_token: OutputToken(0), - output: circular_buffer::CircularBuffer::boxed(), - requests: HashMap::default(), - background_tasks: Vec::default(), - restart_task: None, - is_session_terminated: false, - ignore_breakpoints: false, - breakpoint_store, - data_breakpoints: Default::default(), - exception_breakpoints: Default::default(), - label, - adapter, - task_context, - memory: memory::Memory::new(), - quirks, - remote_client, - node_runtime, - http_client, - companion_port: None, - } - }) - } - - pub fn task_context(&self) -> &TaskContext { - &self.task_context - } - - pub fn worktree(&self) -> Option> { - match &self.state { - SessionState::Booting(_) => None, - SessionState::Running(local_mode) => local_mode.worktree.upgrade(), - } - } - - pub fn boot( - &mut self, - binary: DebugAdapterBinary, - worktree: Entity, - dap_store: WeakEntity, - cx: &mut Context, - ) -> Task> { - let (message_tx, mut message_rx) = futures::channel::mpsc::unbounded(); - let (initialized_tx, initialized_rx) = futures::channel::oneshot::channel(); - - let background_tasks = vec![cx.spawn(async move |this: WeakEntity, cx| { - let mut initialized_tx = Some(initialized_tx); - while let Some(message) = message_rx.next().await { - if let Message::Event(event) = message { - if let Events::Initialized(_) = *event { - if let Some(tx) = initialized_tx.take() { - tx.send(()).ok(); - } - } else { - let Ok(_) = this.update(cx, |session, cx| { - session.handle_dap_event(event, cx); - }) else { - break; - }; - } - } else if let Message::Request(request) = message { - let Ok(_) = this.update(cx, |this, cx| { - if request.command == StartDebugging::COMMAND { - this.handle_start_debugging_request(request, cx) - .detach_and_log_err(cx); - } else if request.command == RunInTerminal::COMMAND { - this.handle_run_in_terminal_request(request, cx) - .detach_and_log_err(cx); - } - }) else { - break; - }; - } - } - })]; - self.background_tasks = background_tasks; - let id = self.id; - let parent_session = self.parent_session.clone(); - - cx.spawn(async move |this, cx| { - let mode = RunningMode::new( - id, - parent_session, - worktree.downgrade(), - binary.clone(), - message_tx, - cx, - ) - .await?; - this.update(cx, |this, cx| { - match &mut this.state { - SessionState::Booting(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"); - } - }; - this.state = SessionState::Running(mode); - cx.emit(SessionStateEvent::Running); - })?; - - this.update(cx, |session, cx| session.request_initialize(cx))? - .await?; - - let result = this - .update(cx, |session, cx| { - session.initialize_sequence(initialized_rx, dap_store.clone(), cx) - })? - .await; - - if result.is_err() { - let mut console = this.update(cx, |session, cx| session.console_output(cx))?; - - console - .send(format!( - "Tried to launch debugger with: {}", - serde_json::to_string_pretty(&binary.request_args.configuration) - .unwrap_or_default(), - )) - .await - .ok(); - } - - result - }) - } - - pub fn session_id(&self) -> SessionId { - self.id - } - - pub fn child_session_ids(&self) -> HashSet { - self.child_session_ids.clone() - } - - pub fn add_child_session_id(&mut self, session_id: SessionId) { - self.child_session_ids.insert(session_id); - } - - pub fn remove_child_session_id(&mut self, session_id: SessionId) { - self.child_session_ids.remove(&session_id); - } - - pub fn parent_id(&self, cx: &App) -> Option { - self.parent_session - .as_ref() - .map(|session| session.read(cx).id) - } - - pub fn parent_session(&self) -> Option<&Entity> { - self.parent_session.as_ref() - } - - pub fn on_app_quit(&mut self, cx: &mut Context) -> Task<()> { - let Some(client) = self.adapter_client() else { - return Task::ready(()); - }; - - let supports_terminate = self - .capabilities - .support_terminate_debuggee - .unwrap_or(false); - - cx.background_spawn(async move { - if supports_terminate { - client - .request::(dap::TerminateArguments { - restart: Some(false), - }) - .await - .ok(); - } else { - client - .request::(dap::DisconnectArguments { - restart: Some(false), - terminate_debuggee: Some(true), - suspend_debuggee: Some(false), - }) - .await - .ok(); - } - }) - } - - pub fn capabilities(&self) -> &Capabilities { - &self.capabilities - } - - pub fn binary(&self) -> Option<&DebugAdapterBinary> { - match &self.state { - SessionState::Booting(_) => None, - SessionState::Running(running_mode) => Some(&running_mode.binary), - } - } - - pub fn adapter(&self) -> DebugAdapterName { - self.adapter.clone() - } - - pub fn label(&self) -> Option { - self.label.clone() - } - - pub fn is_terminated(&self) -> bool { - self.is_session_terminated - } - - pub fn console_output(&mut self, cx: &mut Context) -> mpsc::UnboundedSender { - let (tx, mut rx) = mpsc::unbounded(); - - cx.spawn(async move |this, cx| { - while let Some(output) = rx.next().await { - this.update(cx, |this, _| { - let event = dap::OutputEvent { - category: None, - output, - group: None, - variables_reference: None, - source: None, - line: None, - column: None, - data: None, - location_reference: None, - }; - this.push_output(event); - })?; - } - anyhow::Ok(()) - }) - .detach(); - - tx - } - - pub fn is_started(&self) -> bool { - match &self.state { - SessionState::Booting(_) => false, - SessionState::Running(running) => running.is_started, - } - } - - pub fn is_building(&self) -> bool { - matches!(self.state, SessionState::Booting(_)) - } - - pub fn as_running_mut(&mut self) -> Option<&mut RunningMode> { - match &mut self.state { - SessionState::Running(local_mode) => Some(local_mode), - SessionState::Booting(_) => None, - } - } - - pub fn as_running(&self) -> Option<&RunningMode> { - match &self.state { - SessionState::Running(local_mode) => Some(local_mode), - SessionState::Booting(_) => None, - } - } - - fn handle_start_debugging_request( - &mut self, - request: dap::messages::Request, - cx: &mut Context, - ) -> Task> { - let request_seq = request.seq; - - let launch_request: Option> = request - .arguments - .as_ref() - .map(|value| serde_json::from_value(value.clone())); - - let mut success = true; - if let Some(Ok(request)) = launch_request { - cx.emit(SessionStateEvent::SpawnChildSession { request }); - } else { - log::error!( - "Failed to parse launch request arguments: {:?}", - request.arguments - ); - success = false; - } - - cx.spawn(async move |this, cx| { - this.update(cx, |this, cx| { - this.respond_to_client( - request_seq, - success, - StartDebugging::COMMAND.to_string(), - None, - cx, - ) - })? - .await - }) - } - - fn handle_run_in_terminal_request( - &mut self, - request: dap::messages::Request, - cx: &mut Context, - ) -> Task> { - let request_args = match serde_json::from_value::( - request.arguments.unwrap_or_default(), - ) { - Ok(args) => args, - Err(error) => { - return cx.spawn(async move |session, cx| { - let error = serde_json::to_value(dap::ErrorResponse { - error: Some(dap::Message { - id: request.seq, - format: error.to_string(), - variables: None, - send_telemetry: None, - show_user: None, - url: None, - url_label: None, - }), - }) - .ok(); - - session - .update(cx, |this, cx| { - this.respond_to_client( - request.seq, - false, - StartDebugging::COMMAND.to_string(), - error, - cx, - ) - })? - .await?; - - Err(anyhow!("Failed to parse RunInTerminalRequestArguments")) - }); - } - }; - - let seq = request.seq; - - let (tx, mut rx) = mpsc::channel::>(1); - cx.emit(SessionEvent::RunInTerminal { - request: request_args, - sender: tx, - }); - cx.notify(); - - cx.spawn(async move |session, cx| { - let result = util::maybe!(async move { - rx.next().await.ok_or_else(|| { - anyhow!("failed to receive response from spawn terminal".to_string()) - })? - }) - .await; - let (success, body) = match result { - Ok(pid) => ( - true, - serde_json::to_value(dap::RunInTerminalResponse { - process_id: None, - shell_process_id: Some(pid as u64), - }) - .ok(), - ), - Err(error) => ( - false, - serde_json::to_value(dap::ErrorResponse { - error: Some(dap::Message { - id: seq, - format: error.to_string(), - variables: None, - send_telemetry: None, - show_user: None, - url: None, - url_label: None, - }), - }) - .ok(), - ), - }; - - session - .update(cx, |session, cx| { - session.respond_to_client( - seq, - success, - RunInTerminal::COMMAND.to_string(), - body, - cx, - ) - })? - .await - }) - } - - pub(super) fn request_initialize(&mut self, cx: &mut Context) -> Task> { - let adapter_id = self.adapter().to_string(); - let request = Initialize { adapter_id }; - - let SessionState::Running(running) = &self.state else { - return Task::ready(Err(anyhow!( - "Cannot send initialize request, task still building" - ))); - }; - let mut response = running.request(request.clone()); - - cx.spawn(async move |this, cx| { - loop { - let capabilities = response.await; - match capabilities { - Err(e) => { - let Ok(Some(reconnect)) = this.update(cx, |this, cx| { - this.as_running() - .and_then(|running| running.reconnect_for_ssh(&mut cx.to_async())) - }) else { - return Err(e); - }; - log::info!("Failed to connect to debug adapter: {}, retrying...", e); - reconnect.await?; - - let Ok(Some(r)) = this.update(cx, |this, _| { - this.as_running() - .map(|running| running.request(request.clone())) - }) else { - return Err(e); - }; - response = r - } - Ok(capabilities) => { - this.update(cx, |session, cx| { - session.capabilities = capabilities; - - cx.emit(SessionEvent::CapabilitiesLoaded); - })?; - return Ok(()); - } - } - } - }) - } - - pub(super) fn initialize_sequence( - &mut self, - initialize_rx: oneshot::Receiver<()>, - dap_store: WeakEntity, - cx: &mut Context, - ) -> Task> { - match &self.state { - SessionState::Running(local_mode) => { - local_mode.initialize_sequence(&self.capabilities, initialize_rx, dap_store, cx) - } - SessionState::Booting(_) => { - Task::ready(Err(anyhow!("cannot initialize, still building"))) - } - } - } - - pub fn run_to_position( - &mut self, - breakpoint: SourceBreakpoint, - active_thread_id: ThreadId, - cx: &mut Context, - ) { - match &mut self.state { - SessionState::Running(local_mode) => { - if !matches!( - self.active_snapshot - .thread_states - .thread_state(active_thread_id), - Some(ThreadStatus::Stopped) - ) { - return; - }; - let path = breakpoint.path.clone(); - local_mode.tmp_breakpoint = Some(breakpoint); - let task = local_mode.send_breakpoints_from_path( - path, - BreakpointUpdatedReason::Toggled, - &self.breakpoint_store, - cx, - ); - - cx.spawn(async move |this, cx| { - task.await; - this.update(cx, |this, cx| { - this.continue_thread(active_thread_id, cx); - }) - }) - .detach(); - } - SessionState::Booting(_) => {} - } - } - - pub fn has_new_output(&self, last_update: OutputToken) -> bool { - self.output_token.0.checked_sub(last_update.0).unwrap_or(0) != 0 - } - - pub fn output( - &self, - since: OutputToken, - ) -> (impl Iterator, OutputToken) { - if self.output_token.0 == 0 { - return (self.output.range(0..0), OutputToken(0)); - }; - - let events_since = self.output_token.0.checked_sub(since.0).unwrap_or(0); - - let clamped_events_since = events_since.clamp(0, self.output.len()); - ( - self.output - .range(self.output.len() - clamped_events_since..), - self.output_token, - ) - } - - pub fn respond_to_client( - &self, - request_seq: u64, - success: bool, - command: String, - body: Option, - cx: &mut Context, - ) -> Task> { - let Some(local_session) = self.as_running() else { - unreachable!("Cannot respond to remote client"); - }; - let client = local_session.client.clone(); - - cx.background_spawn(async move { - client - .send_message(Message::Response(Response { - body, - success, - command, - seq: request_seq + 1, - request_seq, - message: None, - })) - .await - }) - } - - fn session_state(&self) -> &SessionSnapshot { - self.selected_snapshot_index - .and_then(|ix| self.snapshots.get(ix)) - .unwrap_or_else(|| &self.active_snapshot) - } - - fn push_to_history(&mut self) { - if !self.has_ever_stopped() { - return; - } - - while self.snapshots.len() >= DEBUG_HISTORY_LIMIT { - self.snapshots.pop_front(); - } - - self.snapshots - .push_back(std::mem::take(&mut self.active_snapshot)); - } - - pub fn historic_snapshots(&self) -> &VecDeque { - &self.snapshots - } - - pub fn select_historic_snapshot(&mut self, ix: Option, cx: &mut Context) { - if self.selected_snapshot_index == ix { - return; - } - - if self - .selected_snapshot_index - .is_some_and(|ix| self.snapshots.len() <= ix) - { - debug_panic!("Attempted to select a debug session with an out of bounds index"); - return; - } - - self.selected_snapshot_index = ix; - cx.emit(SessionEvent::HistoricSnapshotSelected); - cx.notify(); - } - - pub fn active_snapshot_index(&self) -> Option { - self.selected_snapshot_index - } - - fn handle_stopped_event(&mut self, event: StoppedEvent, cx: &mut Context) { - self.push_to_history(); - - self.state.stopped(); - // todo(debugger): Find a clean way to get around the clone - 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; - Some((local, path)) - }) { - local - .send_breakpoints_from_path( - path, - BreakpointUpdatedReason::Toggled, - &breakpoint_store, - cx, - ) - .detach(); - }; - - if event.all_threads_stopped.unwrap_or_default() || event.thread_id.is_none() { - self.active_snapshot.thread_states.stop_all_threads(); - self.invalidate_command_type::(); - } - - // Event if we stopped all threads we still need to insert the thread_id - // to our own data - if let Some(thread_id) = event.thread_id { - self.active_snapshot - .thread_states - .stop_thread(ThreadId(thread_id)); - - self.invalidate_state( - &StackTraceCommand { - thread_id, - start_frame: None, - levels: None, - } - .into(), - ); - } - - self.invalidate_generic(); - self.active_snapshot.threads.clear(); - self.active_snapshot.variables.clear(); - cx.emit(SessionEvent::Stopped( - event - .thread_id - .map(Into::into) - .filter(|_| !event.preserve_focus_hint.unwrap_or(false)), - )); - cx.emit(SessionEvent::InvalidateInlineValue); - cx.notify(); - } - - pub(crate) fn handle_dap_event(&mut self, event: Box, cx: &mut Context) { - match *event { - Events::Initialized(_) => { - debug_assert!( - false, - "Initialized event should have been handled in LocalMode" - ); - } - Events::Stopped(event) => self.handle_stopped_event(event, cx), - Events::Continued(event) => { - if event.all_threads_continued.unwrap_or_default() { - self.active_snapshot.thread_states.continue_all_threads(); - self.breakpoint_store.update(cx, |store, cx| { - store.remove_active_position(Some(self.session_id()), cx) - }); - } else { - self.active_snapshot - .thread_states - .continue_thread(ThreadId(event.thread_id)); - } - // todo(debugger): We should be able to get away with only invalidating generic if all threads were continued - self.invalidate_generic(); - } - Events::Exited(_event) => { - self.clear_active_debug_line(cx); - } - Events::Terminated(_) => { - self.shutdown(cx).detach(); - } - Events::Thread(event) => { - let thread_id = ThreadId(event.thread_id); - - match event.reason { - dap::ThreadEventReason::Started => { - self.active_snapshot - .thread_states - .continue_thread(thread_id); - } - dap::ThreadEventReason::Exited => { - self.active_snapshot.thread_states.exit_thread(thread_id); - } - reason => { - log::error!("Unhandled thread event reason {:?}", reason); - } - } - self.invalidate_state(&ThreadsCommand.into()); - cx.notify(); - } - Events::Output(event) => { - if event - .category - .as_ref() - .is_some_and(|category| *category == OutputEventCategory::Telemetry) - { - return; - } - - self.push_output(event); - cx.notify(); - } - Events::Breakpoint(event) => self.breakpoint_store.update(cx, |store, _| { - store.update_session_breakpoint(self.session_id(), event.reason, event.breakpoint); - }), - Events::Module(event) => { - match event.reason { - dap::ModuleEventReason::New => { - self.active_snapshot.modules.push(event.module); - } - dap::ModuleEventReason::Changed => { - if let Some(module) = self - .active_snapshot - .modules - .iter_mut() - .find(|other| event.module.id == other.id) - { - *module = event.module; - } - } - dap::ModuleEventReason::Removed => { - self.active_snapshot - .modules - .retain(|other| event.module.id != other.id); - } - } - - // todo(debugger): We should only send the invalidate command to downstream clients. - // self.invalidate_state(&ModulesCommand.into()); - } - Events::LoadedSource(_) => { - self.invalidate_state(&LoadedSourcesCommand.into()); - } - Events::Capabilities(event) => { - self.capabilities = self.capabilities.merge(event.capabilities); - - // The adapter might've enabled new exception breakpoints (or disabled existing ones). - let recent_filters = self - .capabilities - .exception_breakpoint_filters - .iter() - .flatten() - .map(|filter| (filter.filter.clone(), filter.clone())) - .collect::>(); - for filter in recent_filters.values() { - let default = filter.default.unwrap_or_default(); - self.exception_breakpoints - .entry(filter.filter.clone()) - .or_insert_with(|| (filter.clone(), default)); - } - self.exception_breakpoints - .retain(|k, _| recent_filters.contains_key(k)); - if self.is_started() { - self.send_exception_breakpoints(cx); - } - - // Remove the ones that no longer exist. - cx.notify(); - } - Events::Memory(_) => {} - Events::Process(_) => {} - Events::ProgressEnd(_) => {} - Events::ProgressStart(_) => {} - Events::ProgressUpdate(_) => {} - Events::Invalidated(_) => {} - Events::Other(event) => { - if event.event == "launchBrowserInCompanion" { - let Some(request) = serde_json::from_value(event.body).ok() else { - log::error!("failed to deserialize launchBrowserInCompanion event"); - return; - }; - self.launch_browser_for_remote_server(request, cx); - } else if event.event == "killCompanionBrowser" { - let Some(request) = serde_json::from_value(event.body).ok() else { - log::error!("failed to deserialize killCompanionBrowser event"); - return; - }; - self.kill_browser(request, cx); - } - } - } - } - - /// Ensure that there's a request in flight for the given command, and if not, send it. Use this to run requests that are idempotent. - fn fetch( - &mut self, - request: T, - process_result: impl FnOnce(&mut Self, Result, &mut Context) + 'static, - cx: &mut Context, - ) { - const { - assert!( - T::CACHEABLE, - "Only requests marked as cacheable should invoke `fetch`" - ); - } - - if (!self.active_snapshot.thread_states.any_stopped_thread() - && request.type_id() != TypeId::of::()) - || self.selected_snapshot_index.is_some() - || self.is_session_terminated - { - return; - } - - let request_map = self - .requests - .entry(std::any::TypeId::of::()) - .or_default(); - - if let Entry::Vacant(vacant) = request_map.entry(request.into()) { - let command = vacant.key().0.clone().as_any_arc().downcast::().unwrap(); - - let task = Self::request_inner::>( - &self.capabilities, - &self.state, - command, - |this, result, cx| { - process_result(this, result, cx); - None - }, - cx, - ); - let task = cx - .background_executor() - .spawn(async move { - let _ = task.await?; - Some(()) - }) - .shared(); - - vacant.insert(task); - cx.notify(); - } - } - - fn request_inner( - capabilities: &Capabilities, - mode: &SessionState, - request: T, - process_result: impl FnOnce( - &mut Self, - Result, - &mut Context, - ) -> Option - + 'static, - cx: &mut Context, - ) -> Task> { - if !T::is_supported(capabilities) { - log::warn!( - "Attempted to send a DAP request that isn't supported: {:?}", - request - ); - let error = Err(anyhow::Error::msg( - "Couldn't complete request because it's not supported", - )); - return cx.spawn(async move |this, cx| { - this.update(cx, |this, cx| process_result(this, error, cx)) - .ok() - .flatten() - }); - } - - let request = mode.request_dap(request); - cx.spawn(async move |this, cx| { - let result = request.await; - this.update(cx, |this, cx| process_result(this, result, cx)) - .ok() - .flatten() - }) - } - - fn request( - &self, - request: T, - process_result: impl FnOnce( - &mut Self, - Result, - &mut Context, - ) -> Option - + 'static, - cx: &mut Context, - ) -> Task> { - Self::request_inner(&self.capabilities, &self.state, request, process_result, cx) - } - - fn invalidate_command_type(&mut self) { - self.requests.remove(&std::any::TypeId::of::()); - } - - fn invalidate_generic(&mut self) { - self.invalidate_command_type::(); - self.invalidate_command_type::(); - self.invalidate_command_type::(); - self.invalidate_command_type::(); - self.invalidate_command_type::(); - let executor = self.as_running().map(|running| running.executor.clone()); - if let Some(executor) = executor { - self.memory.clear(&executor); - } - } - - fn invalidate_state(&mut self, key: &RequestSlot) { - self.requests - .entry((&*key.0 as &dyn Any).type_id()) - .and_modify(|request_map| { - request_map.remove(key); - }); - } - - fn push_output(&mut self, event: OutputEvent) { - self.output.push_back(event); - self.output_token.0 += 1; - } - - pub fn any_stopped_thread(&self) -> bool { - self.active_snapshot.thread_states.any_stopped_thread() - } - - pub fn thread_status(&self, thread_id: ThreadId) -> ThreadStatus { - self.active_snapshot.thread_states.thread_status(thread_id) - } - - pub fn threads(&mut self, cx: &mut Context) -> Vec<(dap::Thread, ThreadStatus)> { - self.fetch( - dap_command::ThreadsCommand, - |this, result, cx| { - let Some(result) = result.log_err() else { - return; - }; - - this.active_snapshot.threads = result - .into_iter() - .map(|thread| (ThreadId(thread.id), Thread::from(thread))) - .collect(); - - this.invalidate_command_type::(); - cx.emit(SessionEvent::Threads); - cx.notify(); - }, - cx, - ); - - let state = self.session_state(); - state - .threads - .values() - .map(|thread| { - ( - thread.dap.clone(), - state.thread_states.thread_status(ThreadId(thread.dap.id)), - ) - }) - .collect() - } - - pub fn modules(&mut self, cx: &mut Context) -> &[Module] { - self.fetch( - dap_command::ModulesCommand, - |this, result, cx| { - let Some(result) = result.log_err() else { - return; - }; - - this.active_snapshot.modules = result; - cx.emit(SessionEvent::Modules); - cx.notify(); - }, - cx, - ); - - &self.session_state().modules - } - - // CodeLLDB returns the size of a pointed-to-memory, which we can use to make the experience of go-to-memory better. - pub fn data_access_size( - &mut self, - frame_id: Option, - evaluate_name: &str, - cx: &mut Context, - ) -> Task> { - let request = self.request( - EvaluateCommand { - expression: format!("?${{sizeof({evaluate_name})}}"), - frame_id, - - context: Some(EvaluateArgumentsContext::Repl), - source: None, - }, - |_, response, _| response.ok(), - cx, - ); - cx.background_spawn(async move { - let result = request.await?; - result.result.parse().ok() - }) - } - - pub fn memory_reference_of_expr( - &mut self, - frame_id: Option, - expression: String, - cx: &mut Context, - ) -> Task)>> { - let request = self.request( - EvaluateCommand { - expression, - frame_id, - - context: Some(EvaluateArgumentsContext::Repl), - source: None, - }, - |_, response, _| response.ok(), - cx, - ); - cx.background_spawn(async move { - let result = request.await?; - result - .memory_reference - .map(|reference| (reference, result.type_)) - }) - } - - pub fn write_memory(&mut self, address: u64, data: &[u8], cx: &mut Context) { - let data = base64::engine::general_purpose::STANDARD.encode(data); - self.request( - WriteMemoryArguments { - memory_reference: address.to_string(), - data, - allow_partial: None, - offset: None, - }, - |this, response, cx| { - this.memory.clear(cx.background_executor()); - this.invalidate_command_type::(); - this.invalidate_command_type::(); - cx.emit(SessionEvent::Variables); - response.ok() - }, - cx, - ) - .detach(); - } - pub fn read_memory( - &mut self, - range: RangeInclusive, - cx: &mut Context, - ) -> MemoryIterator { - // This function is a bit more involved when it comes to fetching data. - // Since we attempt to read memory in pages, we need to account for some parts - // of memory being unreadable. Therefore, we start off by fetching a page per request. - // In case that fails, we try to re-fetch smaller regions until we have the full range. - let page_range = Memory::memory_range_to_page_range(range.clone()); - for page_address in PageAddress::iter_range(page_range) { - self.read_single_page_memory(page_address, cx); - } - self.memory.memory_range(range) - } - - fn read_single_page_memory(&mut self, page_start: PageAddress, cx: &mut Context) { - _ = maybe!({ - let builder = self.memory.build_page(page_start)?; - - self.memory_read_fetch_page_recursive(builder, cx); - Some(()) - }); - } - fn memory_read_fetch_page_recursive( - &mut self, - mut builder: MemoryPageBuilder, - cx: &mut Context, - ) { - let Some(next_request) = builder.next_request() else { - // We're done fetching. Let's grab the page and insert it into our memory store. - let (address, contents) = builder.build(); - self.memory.insert_page(address, contents); - - return; - }; - let size = next_request.size; - self.fetch( - ReadMemory { - memory_reference: format!("0x{:X}", next_request.address), - offset: Some(0), - count: next_request.size, - }, - move |this, memory, cx| { - if let Ok(memory) = memory { - builder.known(memory.content); - if let Some(unknown) = memory.unreadable_bytes { - builder.unknown(unknown); - } - // This is the recursive bit: if we're not yet done with - // the whole page, we'll kick off a new request with smaller range. - // Note that this function is recursive only conceptually; - // since it kicks off a new request with callback, we don't need to worry about stack overflow. - this.memory_read_fetch_page_recursive(builder, cx); - } else { - builder.unknown(size); - } - }, - cx, - ); - } - - pub fn ignore_breakpoints(&self) -> bool { - self.ignore_breakpoints - } - - pub fn toggle_ignore_breakpoints( - &mut self, - cx: &mut App, - ) -> Task, anyhow::Error>> { - self.set_ignore_breakpoints(!self.ignore_breakpoints, cx) - } - - pub(crate) fn set_ignore_breakpoints( - &mut self, - ignore: bool, - cx: &mut App, - ) -> Task, anyhow::Error>> { - if self.ignore_breakpoints == ignore { - return Task::ready(HashMap::default()); - } - - self.ignore_breakpoints = ignore; - - if let Some(local) = self.as_running() { - local.send_source_breakpoints(ignore, &self.breakpoint_store, cx) - } else { - // todo(debugger): We need to propagate this change to downstream sessions and send a message to upstream sessions - unimplemented!() - } - } - - pub fn data_breakpoints(&self) -> impl Iterator { - self.data_breakpoints.values() - } - - pub fn exception_breakpoints( - &self, - ) -> impl Iterator { - self.exception_breakpoints.values() - } - - pub fn toggle_exception_breakpoint(&mut self, id: &str, cx: &App) { - if let Some((_, is_enabled)) = self.exception_breakpoints.get_mut(id) { - *is_enabled = !*is_enabled; - self.send_exception_breakpoints(cx); - } - } - - fn send_exception_breakpoints(&mut self, cx: &App) { - if let Some(local) = self.as_running() { - let exception_filters = self - .exception_breakpoints - .values() - .filter_map(|(filter, is_enabled)| is_enabled.then(|| filter.clone())) - .collect(); - - let supports_exception_filters = self - .capabilities - .supports_exception_filter_options - .unwrap_or_default(); - local - .send_exception_breakpoints(exception_filters, supports_exception_filters) - .detach_and_log_err(cx); - } else { - debug_assert!(false, "Not implemented"); - } - } - - pub fn toggle_data_breakpoint(&mut self, id: &str, cx: &mut Context<'_, Session>) { - if let Some(state) = self.data_breakpoints.get_mut(id) { - state.is_enabled = !state.is_enabled; - self.send_exception_breakpoints(cx); - } - } - - fn send_data_breakpoints(&mut self, cx: &mut Context) { - if let Some(mode) = self.as_running() { - let breakpoints = self - .data_breakpoints - .values() - .filter_map(|state| state.is_enabled.then(|| state.dap.clone())) - .collect(); - let command = SetDataBreakpointsCommand { breakpoints }; - mode.request(command).detach_and_log_err(cx); - } - } - - pub fn create_data_breakpoint( - &mut self, - context: Arc, - data_id: String, - dap: dap::DataBreakpoint, - cx: &mut Context, - ) { - if self.data_breakpoints.remove(&data_id).is_none() { - self.data_breakpoints.insert( - data_id, - DataBreakpointState { - dap, - is_enabled: true, - context, - }, - ); - } - self.send_data_breakpoints(cx); - } - - pub fn breakpoints_enabled(&self) -> bool { - self.ignore_breakpoints - } - - pub fn loaded_sources(&mut self, cx: &mut Context) -> &[Source] { - self.fetch( - dap_command::LoadedSourcesCommand, - |this, result, cx| { - let Some(result) = result.log_err() else { - return; - }; - this.active_snapshot.loaded_sources = result; - cx.emit(SessionEvent::LoadedSources); - cx.notify(); - }, - cx, - ); - &self.session_state().loaded_sources - } - - fn fallback_to_manual_restart( - &mut self, - res: Result<()>, - cx: &mut Context, - ) -> Option<()> { - if res.log_err().is_none() { - cx.emit(SessionStateEvent::Restart); - return None; - } - Some(()) - } - - fn empty_response(&mut self, res: Result<()>, _cx: &mut Context) -> Option<()> { - res.log_err()?; - Some(()) - } - - fn on_step_response( - thread_id: ThreadId, - ) -> impl FnOnce(&mut Self, Result, &mut Context) -> Option + 'static - { - move |this, response, cx| match response.log_err() { - Some(response) => { - this.breakpoint_store.update(cx, |store, cx| { - store.remove_active_position(Some(this.session_id()), cx) - }); - Some(response) - } - None => { - this.active_snapshot.thread_states.stop_thread(thread_id); - cx.notify(); - None - } - } - } - - fn clear_active_debug_line_response( - &mut self, - response: Result<()>, - cx: &mut Context, - ) -> Option<()> { - response.log_err()?; - self.clear_active_debug_line(cx); - Some(()) - } - - fn clear_active_debug_line(&mut self, cx: &mut Context) { - self.breakpoint_store.update(cx, |store, cx| { - store.remove_active_position(Some(self.id), cx) - }); - } - - pub fn pause_thread(&mut self, thread_id: ThreadId, cx: &mut Context) { - self.request( - PauseCommand { - thread_id: thread_id.0, - }, - Self::empty_response, - cx, - ) - .detach(); - } - - pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut Context) { - self.request( - RestartStackFrameCommand { stack_frame_id }, - Self::empty_response, - cx, - ) - .detach(); - } - - pub fn restart(&mut self, args: Option, cx: &mut Context) { - if self.restart_task.is_some() || self.as_running().is_none() { - return; - } - - let supports_dap_restart = - self.capabilities.supports_restart_request.unwrap_or(false) && !self.is_terminated(); - - self.restart_task = Some(cx.spawn(async move |this, cx| { - let _ = this.update(cx, |session, cx| { - if supports_dap_restart { - session - .request( - RestartCommand { - raw: args.unwrap_or(Value::Null), - }, - Self::fallback_to_manual_restart, - cx, - ) - .detach(); - } else { - cx.emit(SessionStateEvent::Restart); - } - }); - })); - } - - pub fn shutdown(&mut self, cx: &mut Context) -> Task<()> { - if self.is_session_terminated { - return Task::ready(()); - } - - self.is_session_terminated = true; - self.active_snapshot.thread_states.exit_all_threads(); - cx.notify(); - - let task = match &mut self.state { - SessionState::Running(_) => { - if self - .capabilities - .supports_terminate_request - .unwrap_or_default() - { - self.request( - TerminateCommand { - restart: Some(false), - }, - Self::clear_active_debug_line_response, - cx, - ) - } else { - self.request( - DisconnectCommand { - restart: Some(false), - terminate_debuggee: Some(true), - suspend_debuggee: Some(false), - }, - Self::clear_active_debug_line_response, - cx, - ) - } - } - SessionState::Booting(build_task) => { - build_task.take(); - Task::ready(Some(())) - } - }; - - cx.emit(SessionStateEvent::Shutdown); - - cx.spawn(async move |this, cx| { - task.await; - let _ = this.update(cx, |this, _| { - if let Some(adapter_client) = this.adapter_client() { - adapter_client.kill(); - } - }); - }) - } - - pub fn completions( - &mut self, - query: CompletionsQuery, - cx: &mut Context, - ) -> Task>> { - let task = self.request(query, |_, result, _| result.log_err(), cx); - - cx.background_executor().spawn(async move { - anyhow::Ok( - task.await - .map(|response| response.targets) - .context("failed to fetch completions")?, - ) - }) - } - - pub fn continue_thread(&mut self, thread_id: ThreadId, cx: &mut Context) { - self.select_historic_snapshot(None, cx); - - let supports_single_thread_execution_requests = - self.capabilities.supports_single_thread_execution_requests; - self.active_snapshot - .thread_states - .continue_thread(thread_id); - self.request( - ContinueCommand { - args: ContinueArguments { - thread_id: thread_id.0, - single_thread: supports_single_thread_execution_requests, - }, - }, - Self::on_step_response::(thread_id), - cx, - ) - .detach(); - } - - pub fn adapter_client(&self) -> Option> { - match self.state { - SessionState::Running(ref local) => Some(local.client.clone()), - SessionState::Booting(_) => None, - } - } - - pub fn has_ever_stopped(&self) -> bool { - self.state.has_ever_stopped() - } - - pub fn step_over( - &mut self, - thread_id: ThreadId, - granularity: SteppingGranularity, - cx: &mut Context, - ) { - self.select_historic_snapshot(None, cx); - - let supports_single_thread_execution_requests = - self.capabilities.supports_single_thread_execution_requests; - let supports_stepping_granularity = self - .capabilities - .supports_stepping_granularity - .unwrap_or_default(); - - let command = NextCommand { - inner: StepCommand { - thread_id: thread_id.0, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests, - }, - }; - - self.active_snapshot.thread_states.process_step(thread_id); - self.request( - command, - Self::on_step_response::(thread_id), - cx, - ) - .detach(); - } - - pub fn step_in( - &mut self, - thread_id: ThreadId, - granularity: SteppingGranularity, - cx: &mut Context, - ) { - self.select_historic_snapshot(None, cx); - - let supports_single_thread_execution_requests = - self.capabilities.supports_single_thread_execution_requests; - let supports_stepping_granularity = self - .capabilities - .supports_stepping_granularity - .unwrap_or_default(); - - let command = StepInCommand { - inner: StepCommand { - thread_id: thread_id.0, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests, - }, - }; - - self.active_snapshot.thread_states.process_step(thread_id); - self.request( - command, - Self::on_step_response::(thread_id), - cx, - ) - .detach(); - } - - pub fn step_out( - &mut self, - thread_id: ThreadId, - granularity: SteppingGranularity, - cx: &mut Context, - ) { - self.select_historic_snapshot(None, cx); - - let supports_single_thread_execution_requests = - self.capabilities.supports_single_thread_execution_requests; - let supports_stepping_granularity = self - .capabilities - .supports_stepping_granularity - .unwrap_or_default(); - - let command = StepOutCommand { - inner: StepCommand { - thread_id: thread_id.0, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests, - }, - }; - - self.active_snapshot.thread_states.process_step(thread_id); - self.request( - command, - Self::on_step_response::(thread_id), - cx, - ) - .detach(); - } - - pub fn step_back( - &mut self, - thread_id: ThreadId, - granularity: SteppingGranularity, - cx: &mut Context, - ) { - self.select_historic_snapshot(None, cx); - - let supports_single_thread_execution_requests = - self.capabilities.supports_single_thread_execution_requests; - let supports_stepping_granularity = self - .capabilities - .supports_stepping_granularity - .unwrap_or_default(); - - let command = StepBackCommand { - inner: StepCommand { - thread_id: thread_id.0, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests, - }, - }; - - self.active_snapshot.thread_states.process_step(thread_id); - - self.request( - command, - Self::on_step_response::(thread_id), - cx, - ) - .detach(); - } - - pub fn stack_frames( - &mut self, - thread_id: ThreadId, - cx: &mut Context, - ) -> Result> { - if self.active_snapshot.thread_states.thread_status(thread_id) == ThreadStatus::Stopped - && self.requests.contains_key(&ThreadsCommand.type_id()) - && self.active_snapshot.threads.contains_key(&thread_id) - // ^ todo(debugger): We need a better way to check that we're not querying stale data - // We could still be using an old thread id and have sent a new thread's request - // This isn't the biggest concern right now because it hasn't caused any issues outside of tests - // But it very well could cause a minor bug in the future that is hard to track down - { - self.fetch( - super::dap_command::StackTraceCommand { - thread_id: thread_id.0, - start_frame: None, - levels: None, - }, - move |this, stack_frames, cx| { - let entry = - this.active_snapshot - .threads - .entry(thread_id) - .and_modify(|thread| match &stack_frames { - Ok(stack_frames) => { - thread.stack_frames = stack_frames - .iter() - .cloned() - .map(StackFrame::from) - .collect(); - thread.stack_frames_error = None; - } - Err(error) => { - thread.stack_frames.clear(); - thread.stack_frames_error = Some(error.to_string().into()); - } - }); - debug_assert!( - matches!(entry, indexmap::map::Entry::Occupied(_)), - "Sent request for thread_id that doesn't exist" - ); - if let Ok(stack_frames) = stack_frames { - this.active_snapshot.stack_frames.extend( - stack_frames - .into_iter() - .filter(|frame| { - // Workaround for JavaScript debug adapter sending out "fake" stack frames for delineating await points. This is fine, - // except that they always use an id of 0 for it, which collides with other (valid) stack frames. - !(frame.id == 0 - && frame.line == 0 - && frame.column == 0 - && frame.presentation_hint - == Some(StackFramePresentationHint::Label)) - }) - .map(|frame| (frame.id, StackFrame::from(frame))), - ); - } - - this.invalidate_command_type::(); - this.invalidate_command_type::(); - - cx.emit(SessionEvent::StackTrace); - }, - cx, - ); - } - - match self.session_state().threads.get(&thread_id) { - Some(thread) => { - if let Some(error) = &thread.stack_frames_error { - Err(anyhow!(error.to_string())) - } else { - Ok(thread.stack_frames.clone()) - } - } - None => Ok(Vec::new()), - } - } - - pub fn scopes(&mut self, stack_frame_id: u64, cx: &mut Context) -> &[dap::Scope] { - if self.requests.contains_key(&TypeId::of::()) - && self - .requests - .contains_key(&TypeId::of::()) - { - self.fetch( - ScopesCommand { stack_frame_id }, - move |this, scopes, cx| { - let Some(scopes) = scopes.log_err() else { - return - }; - - for scope in scopes.iter() { - this.variables(scope.variables_reference, cx); - } - - let entry = this - .active_snapshot - .stack_frames - .entry(stack_frame_id) - .and_modify(|stack_frame| { - stack_frame.scopes = scopes; - }); - - cx.emit(SessionEvent::Variables); - - debug_assert!( - matches!(entry, indexmap::map::Entry::Occupied(_)), - "Sent scopes request for stack_frame_id that doesn't exist or hasn't been fetched" - ); - }, - cx, - ); - } - - self.session_state() - .stack_frames - .get(&stack_frame_id) - .map(|frame| frame.scopes.as_slice()) - .unwrap_or_default() - } - - pub fn variables_by_stack_frame_id( - &self, - stack_frame_id: StackFrameId, - globals: bool, - locals: bool, - ) -> Vec { - let state = self.session_state(); - let Some(stack_frame) = state.stack_frames.get(&stack_frame_id) else { - return Vec::new(); - }; - - stack_frame - .scopes - .iter() - .filter(|scope| { - (scope.name.to_lowercase().contains("local") && locals) - || (scope.name.to_lowercase().contains("global") && globals) - }) - .filter_map(|scope| state.variables.get(&scope.variables_reference)) - .flatten() - .cloned() - .collect() - } - - pub fn watchers(&self) -> &HashMap { - &self.watchers - } - - pub fn add_watcher( - &mut self, - expression: SharedString, - frame_id: u64, - cx: &mut Context, - ) -> Task> { - let request = self.state.request_dap(EvaluateCommand { - expression: expression.to_string(), - context: Some(EvaluateArgumentsContext::Watch), - frame_id: Some(frame_id), - source: None, - }); - - cx.spawn(async move |this, cx| { - let response = request.await?; - - this.update(cx, |session, cx| { - session.watchers.insert( - expression.clone(), - Watcher { - expression, - value: response.result.into(), - variables_reference: response.variables_reference, - presentation_hint: response.presentation_hint, - }, - ); - cx.emit(SessionEvent::Watchers); - }) - }) - } - - pub fn refresh_watchers(&mut self, frame_id: u64, cx: &mut Context) { - let watches = self.watchers.clone(); - for (_, watch) in watches.into_iter() { - self.add_watcher(watch.expression.clone(), frame_id, cx) - .detach(); - } - } - - pub fn remove_watcher(&mut self, expression: SharedString) { - self.watchers.remove(&expression); - } - - pub fn variables( - &mut self, - variables_reference: VariableReference, - cx: &mut Context, - ) -> Vec { - let command = VariablesCommand { - variables_reference, - filter: None, - start: None, - count: None, - format: None, - }; - - self.fetch( - command, - move |this, variables, cx| { - let Some(variables) = variables.log_err() else { - return; - }; - - this.active_snapshot - .variables - .insert(variables_reference, variables); - - cx.emit(SessionEvent::Variables); - cx.emit(SessionEvent::InvalidateInlineValue); - }, - cx, - ); - - self.session_state() - .variables - .get(&variables_reference) - .cloned() - .unwrap_or_default() - } - - pub fn data_breakpoint_info( - &mut self, - context: Arc, - mode: Option, - cx: &mut Context, - ) -> Task> { - let command = DataBreakpointInfoCommand { context, mode }; - - self.request(command, |_, response, _| response.ok(), cx) - } - - pub fn set_variable_value( - &mut self, - stack_frame_id: u64, - variables_reference: u64, - name: String, - value: String, - cx: &mut Context, - ) { - if self.capabilities.supports_set_variable.unwrap_or_default() { - self.request( - SetVariableValueCommand { - name, - value, - variables_reference, - }, - move |this, response, cx| { - let response = response.log_err()?; - this.invalidate_command_type::(); - this.invalidate_command_type::(); - this.memory.clear(cx.background_executor()); - this.refresh_watchers(stack_frame_id, cx); - cx.emit(SessionEvent::Variables); - Some(response) - }, - cx, - ) - .detach(); - } - } - - pub fn evaluate( - &mut self, - expression: String, - context: Option, - frame_id: Option, - source: Option, - cx: &mut Context, - ) -> Task<()> { - let event = dap::OutputEvent { - category: None, - output: format!("> {expression}"), - group: None, - variables_reference: None, - source: None, - line: None, - column: None, - data: None, - location_reference: None, - }; - self.push_output(event); - let request = self.state.request_dap(EvaluateCommand { - expression, - context, - frame_id, - source, - }); - cx.spawn(async move |this, cx| { - let response = request.await; - this.update(cx, |this, cx| { - this.memory.clear(cx.background_executor()); - this.invalidate_command_type::(); - this.invalidate_command_type::(); - cx.emit(SessionEvent::Variables); - match response { - Ok(response) => { - let event = dap::OutputEvent { - category: None, - output: format!("< {}", &response.result), - group: None, - variables_reference: Some(response.variables_reference), - source: None, - line: None, - column: None, - data: None, - location_reference: None, - }; - this.push_output(event); - } - Err(e) => { - let event = dap::OutputEvent { - category: None, - output: format!("{}", e), - group: None, - variables_reference: None, - source: None, - line: None, - column: None, - data: None, - location_reference: None, - }; - this.push_output(event); - } - }; - cx.notify(); - }) - .ok(); - }) - } - - pub fn location( - &mut self, - reference: u64, - cx: &mut Context, - ) -> Option { - self.fetch( - LocationsCommand { reference }, - move |this, response, _| { - let Some(response) = response.log_err() else { - return; - }; - this.active_snapshot.locations.insert(reference, response); - }, - cx, - ); - self.session_state().locations.get(&reference).cloned() - } - - pub fn is_attached(&self) -> bool { - let SessionState::Running(local_mode) = &self.state else { - return false; - }; - local_mode.binary.request_args.request == StartDebuggingRequestArgumentsRequest::Attach - } - - pub fn disconnect_client(&mut self, cx: &mut Context) { - let command = DisconnectCommand { - restart: Some(false), - terminate_debuggee: Some(false), - suspend_debuggee: Some(false), - }; - - self.request(command, Self::empty_response, cx).detach() - } - - pub fn terminate_threads(&mut self, thread_ids: Option>, cx: &mut Context) { - if self - .capabilities - .supports_terminate_threads_request - .unwrap_or_default() - { - self.request( - TerminateThreadsCommand { - thread_ids: thread_ids.map(|ids| ids.into_iter().map(|id| id.0).collect()), - }, - Self::clear_active_debug_line_response, - cx, - ) - .detach(); - } else { - self.shutdown(cx).detach(); - } - } - - pub fn thread_state(&self, thread_id: ThreadId) -> Option { - self.session_state().thread_states.thread_state(thread_id) - } - - pub fn quirks(&self) -> SessionQuirks { - self.quirks - } - - fn launch_browser_for_remote_server( - &mut self, - mut request: LaunchBrowserInCompanionParams, - cx: &mut Context, - ) { - let Some(remote_client) = self.remote_client.clone() else { - log::error!("can't launch browser in companion for non-remote project"); - return; - }; - let Some(http_client) = self.http_client.clone() else { - return; - }; - let Some(node_runtime) = self.node_runtime.clone() else { - return; - }; - - let mut console_output = self.console_output(cx); - let task = cx.spawn(async move |this, cx| { - let forward_ports_process = if remote_client - .read_with(cx, |client, _| client.shares_network_interface())? - { - request.other.insert( - "proxyUri".into(), - format!("127.0.0.1:{}", request.server_port).into(), - ); - None - } else { - let port = TcpTransport::unused_port(Ipv4Addr::LOCALHOST) - .await - .context("getting port for DAP")?; - request - .other - .insert("proxyUri".into(), format!("127.0.0.1:{port}").into()); - let mut port_forwards = vec![(port, "localhost".to_owned(), request.server_port)]; - - if let Some(value) = request.params.get("url") - && let Some(url) = value.as_str() - && let Some(url) = Url::parse(url).ok() - && let Some(frontend_port) = url.port() - { - port_forwards.push((frontend_port, "localhost".to_owned(), frontend_port)); - } - - let child = remote_client.update(cx, |client, _| { - let command = client.build_forward_ports_command(port_forwards)?; - let child = new_smol_command(command.program) - .args(command.args) - .envs(command.env) - .spawn() - .context("spawning port forwarding process")?; - anyhow::Ok(child) - })??; - Some(child) - }; - - let mut companion_process = None; - let companion_port = - if let Some(companion_port) = this.read_with(cx, |this, _| this.companion_port)? { - companion_port - } else { - let task = cx.spawn(async move |cx| spawn_companion(node_runtime, cx).await); - match task.await { - Ok((port, child)) => { - companion_process = Some(child); - port - } - Err(e) => { - console_output - .send(format!("Failed to launch browser companion process: {e}")) - .await - .ok(); - return Err(e); - } - } - }; - - let mut background_tasks = Vec::new(); - if let Some(mut forward_ports_process) = forward_ports_process { - background_tasks.push(cx.spawn(async move |_| { - forward_ports_process.status().await.log_err(); - })); - }; - if let Some(mut companion_process) = companion_process { - if let Some(stderr) = companion_process.stderr.take() { - let mut console_output = console_output.clone(); - background_tasks.push(cx.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 - { - console_output - .send(format!("companion stderr: {line}")) - .await - .ok(); - line.clear(); - } - })); - } - background_tasks.push(cx.spawn({ - let mut console_output = console_output.clone(); - async move |_| match companion_process.status().await { - Ok(status) => { - if status.success() { - console_output - .send("Companion process exited normally".into()) - .await - .ok(); - } else { - console_output - .send(format!( - "Companion process exited abnormally with {status:?}" - )) - .await - .ok(); - } - } - Err(e) => { - console_output - .send(format!("Failed to join companion process: {e}")) - .await - .ok(); - } - } - })); - } - - // TODO pass wslInfo as needed - - let companion_address = format!("127.0.0.1:{companion_port}"); - let mut companion_started = false; - for _ in 0..10 { - if TcpStream::connect(&companion_address).await.is_ok() { - companion_started = true; - break; - } - cx.background_executor() - .timer(Duration::from_millis(100)) - .await; - } - if !companion_started { - console_output - .send("Browser companion failed to start".into()) - .await - .ok(); - bail!("Browser companion failed to start"); - } - - let response = http_client - .post_json( - &format!("http://{companion_address}/launch-and-attach"), - serde_json::to_string(&request) - .context("serializing request")? - .into(), - ) - .await; - match response { - Ok(response) => { - if !response.status().is_success() { - console_output - .send("Launch request to companion failed".into()) - .await - .ok(); - return Err(anyhow!("launch request failed")); - } - } - Err(e) => { - console_output - .send("Failed to read response from companion".into()) - .await - .ok(); - return Err(e); - } - } - - this.update(cx, |this, _| { - this.background_tasks.extend(background_tasks); - this.companion_port = Some(companion_port); - })?; - - anyhow::Ok(()) - }); - self.background_tasks.push(cx.spawn(async move |_, _| { - task.await.log_err(); - })); - } - - fn kill_browser(&self, request: KillCompanionBrowserParams, cx: &mut App) { - let Some(companion_port) = self.companion_port else { - log::error!("received killCompanionBrowser but js-debug-companion is not running"); - return; - }; - let Some(http_client) = self.http_client.clone() else { - return; - }; - - cx.spawn(async move |_| { - http_client - .post_json( - &format!("http://127.0.0.1:{companion_port}/kill"), - serde_json::to_string(&request) - .context("serializing request")? - .into(), - ) - .await?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx) - } -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct LaunchBrowserInCompanionParams { - server_port: u16, - params: HashMap, - #[serde(flatten)] - other: HashMap, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct KillCompanionBrowserParams { - launch_id: u64, -} - -async fn spawn_companion( - node_runtime: NodeRuntime, - cx: &mut AsyncApp, -) -> Result<(u16, smol::process::Child)> { - let binary_path = node_runtime - .binary_path() - .await - .context("getting node path")?; - let path = cx - .spawn(async move |cx| get_or_install_companion(node_runtime, cx).await) - .await?; - log::info!("will launch js-debug-companion version {path:?}"); - - let port = { - let listener = TcpListener::bind("127.0.0.1:0") - .await - .context("getting port for companion")?; - listener.local_addr()?.port() - }; - - let dir = paths::data_dir() - .join("js_debug_companion_state") - .to_string_lossy() - .to_string(); - - let child = new_smol_command(binary_path) - .arg(path) - .args([ - format!("--listen=127.0.0.1:{port}"), - format!("--state={dir}"), - ]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .context("spawning companion child process")?; - - Ok((port, child)) -} - -async fn get_or_install_companion(node: NodeRuntime, cx: &mut AsyncApp) -> Result { - const PACKAGE_NAME: &str = "@zed-industries/js-debug-companion-cli"; - - async fn install_latest_version(dir: PathBuf, node: NodeRuntime) -> Result { - let temp_dir = tempfile::tempdir().context("creating temporary directory")?; - node.npm_install_packages(temp_dir.path(), &[(PACKAGE_NAME, "latest")]) - .await - .context("installing latest companion package")?; - let version = node - .npm_package_installed_version(temp_dir.path(), PACKAGE_NAME) - .await - .context("getting installed companion version")? - .context("companion was not installed")?; - smol::fs::rename(temp_dir.path(), dir.join(&version)) - .await - .context("moving companion package into place")?; - Ok(dir.join(version)) - } - - let dir = paths::debug_adapters_dir().join("js-debug-companion"); - let (latest_installed_version, latest_version) = cx - .background_spawn({ - let dir = dir.clone(); - let node = node.clone(); - async move { - smol::fs::create_dir_all(&dir) - .await - .context("creating companion installation directory")?; - - let mut children = smol::fs::read_dir(&dir) - .await - .context("reading companion installation directory")? - .try_collect::>() - .await - .context("reading companion installation directory entries")?; - children - .sort_by_key(|child| semver::Version::parse(child.file_name().to_str()?).ok()); - - let latest_installed_version = children.last().and_then(|child| { - let version = child.file_name().into_string().ok()?; - Some((child.path(), version)) - }); - let latest_version = node - .npm_package_latest_version(PACKAGE_NAME) - .await - .log_err(); - anyhow::Ok((latest_installed_version, latest_version)) - } - }) - .await?; - - let path = if let Some((installed_path, installed_version)) = latest_installed_version { - if let Some(latest_version) = latest_version - && latest_version != installed_version - { - cx.background_spawn(install_latest_version(dir.clone(), node.clone())) - .detach(); - } - Ok(installed_path) - } else { - cx.background_spawn(install_latest_version(dir.clone(), node.clone())) - .await - }; - - Ok(path? - .join("node_modules") - .join(PACKAGE_NAME) - .join("out") - .join("cli.js")) -} diff --git a/crates/project/src/debugger/test.rs b/crates/project/src/debugger/test.rs deleted file mode 100644 index 53b88323e6..0000000000 --- a/crates/project/src/debugger/test.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::{path::Path, sync::Arc}; - -use dap::client::DebugAdapterClient; -use gpui::{App, Subscription}; - -use super::session::{Session, SessionStateEvent}; - -pub fn intercept_debug_sessions) + 'static>( - cx: &mut gpui::TestAppContext, - configure: T, -) -> Subscription { - cx.update(|cx| { - let configure = Arc::new(configure); - cx.observe_new::(move |_, _, cx| { - let configure = configure.clone(); - cx.subscribe_self(move |session, event, cx| { - let configure = configure.clone(); - if matches!(event, SessionStateEvent::Running) { - let client = session.adapter_client().unwrap(); - register_default_handlers(session, &client, cx); - configure(&client); - } - }) - .detach(); - }) - }) -} - -fn register_default_handlers(session: &Session, client: &Arc, cx: &mut App) { - client.on_request::(move |_, _| Ok(Default::default())); - let paths = session.breakpoint_store.read(cx).breakpoint_paths(); - - client.on_request::(move |_, args| { - let p = Arc::from(Path::new(&args.source.path.unwrap())); - if !paths.contains(&p) { - panic!("Sent breakpoints for path without any") - } - - Ok(dap::SetBreakpointsResponse { - breakpoints: Vec::default(), - }) - }); - - client.on_request::(move |_, _| Ok(())); - - client.on_request::(move |_, _| { - Ok(dap::SetExceptionBreakpointsResponse { breakpoints: None }) - }); - - client.on_request::(move |_, _| Ok(())); - - client.on_request::(move |_, _| { - Ok(dap::ThreadsResponse { threads: vec![] }) - }); -} diff --git a/crates/project/src/environment.rs b/crates/project/src/environment.rs deleted file mode 100644 index c4e807621e..0000000000 --- a/crates/project/src/environment.rs +++ /dev/null @@ -1,421 +0,0 @@ -use anyhow::{Context as _, bail}; -use futures::{FutureExt, StreamExt as _, channel::mpsc, future::Shared}; -use language::Buffer; -use remote::RemoteClient; -use rpc::proto::{self, REMOTE_SERVER_PROJECT_ID}; -use std::{collections::VecDeque, path::Path, sync::Arc}; -use task::{Shell, shell_to_proto}; -use terminal::terminal_settings::TerminalSettings; -use util::{ResultExt, command::new_smol_command, rel_path::RelPath}; -use worktree::Worktree; - -use collections::HashMap; -use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task, WeakEntity}; -use settings::Settings as _; - -use crate::{ - project_settings::{DirenvSettings, ProjectSettings}, - worktree_store::WorktreeStore, -}; - -pub struct ProjectEnvironment { - cli_environment: Option>, - local_environments: HashMap<(Shell, Arc), Shared>>>>, - remote_environments: HashMap<(Shell, Arc), Shared>>>>, - environment_error_messages: VecDeque, - environment_error_messages_tx: mpsc::UnboundedSender, - worktree_store: WeakEntity, - remote_client: Option>, - is_remote_project: bool, - _tasks: Vec>, -} - -pub enum ProjectEnvironmentEvent { - ErrorsUpdated, -} - -impl EventEmitter for ProjectEnvironment {} - -impl ProjectEnvironment { - pub fn new( - cli_environment: Option>, - worktree_store: WeakEntity, - remote_client: Option>, - is_remote_project: bool, - cx: &mut Context, - ) -> Self { - let (tx, mut rx) = mpsc::unbounded(); - let task = cx.spawn(async move |this, cx| { - while let Some(message) = rx.next().await { - this.update(cx, |this, cx| { - this.environment_error_messages.push_back(message); - cx.emit(ProjectEnvironmentEvent::ErrorsUpdated); - }) - .ok(); - } - }); - Self { - cli_environment, - local_environments: Default::default(), - remote_environments: Default::default(), - environment_error_messages: Default::default(), - environment_error_messages_tx: tx, - worktree_store, - remote_client, - is_remote_project, - _tasks: vec![task], - } - } - - /// Returns the inherited CLI environment, if this project was opened from the Zed CLI. - pub(crate) fn get_cli_environment(&self) -> Option> { - if cfg!(any(test, feature = "test-support")) { - return Some(HashMap::default()); - } - if let Some(mut env) = self.cli_environment.clone() { - set_origin_marker(&mut env, EnvironmentOrigin::Cli); - Some(env) - } else { - None - } - } - - pub fn buffer_environment( - &mut self, - buffer: &Entity, - worktree_store: &Entity, - cx: &mut Context, - ) -> Shared>>> { - if let Some(cli_environment) = self.get_cli_environment() { - log::debug!("using project environment variables from CLI"); - return Task::ready(Some(cli_environment)).shared(); - } - - let Some(worktree) = buffer - .read(cx) - .file() - .map(|f| f.worktree_id(cx)) - .and_then(|worktree_id| worktree_store.read(cx).worktree_for_id(worktree_id, cx)) - else { - return Task::ready(None).shared(); - }; - self.worktree_environment(worktree, cx) - } - - pub fn worktree_environment( - &mut self, - worktree: Entity, - cx: &mut App, - ) -> Shared>>> { - if let Some(cli_environment) = self.get_cli_environment() { - log::debug!("using project environment variables from CLI"); - return Task::ready(Some(cli_environment)).shared(); - } - - let worktree = worktree.read(cx); - let mut abs_path = worktree.abs_path(); - if worktree.is_single_file() { - let Some(parent) = abs_path.parent() else { - return Task::ready(None).shared(); - }; - abs_path = parent.into(); - } - - let remote_client = self.remote_client.as_ref().and_then(|it| it.upgrade()); - match remote_client { - Some(remote_client) => remote_client.clone().read(cx).shell().map(|shell| { - self.remote_directory_environment( - &Shell::Program(shell), - abs_path, - remote_client, - cx, - ) - }), - None if self.is_remote_project => { - Some(self.local_directory_environment(&Shell::System, abs_path, cx)) - } - None => Some({ - let shell = TerminalSettings::get( - Some(settings::SettingsLocation { - worktree_id: worktree.id(), - path: RelPath::empty(), - }), - cx, - ) - .shell - .clone(); - - self.local_directory_environment(&shell, abs_path, cx) - }), - } - .unwrap_or_else(|| Task::ready(None).shared()) - } - - pub fn directory_environment( - &mut self, - abs_path: Arc, - cx: &mut App, - ) -> Shared>>> { - let remote_client = self.remote_client.as_ref().and_then(|it| it.upgrade()); - match remote_client { - Some(remote_client) => remote_client.clone().read(cx).shell().map(|shell| { - self.remote_directory_environment( - &Shell::Program(shell), - abs_path, - remote_client, - cx, - ) - }), - None if self.is_remote_project => { - Some(self.local_directory_environment(&Shell::System, abs_path, cx)) - } - None => self - .worktree_store - .read_with(cx, |worktree_store, cx| { - worktree_store.find_worktree(&abs_path, cx) - }) - .ok() - .map(|worktree| { - let shell = terminal::terminal_settings::TerminalSettings::get( - worktree - .as_ref() - .map(|(worktree, path)| settings::SettingsLocation { - worktree_id: worktree.read(cx).id(), - path: &path, - }), - cx, - ) - .shell - .clone(); - - self.local_directory_environment(&shell, abs_path, cx) - }), - } - .unwrap_or_else(|| Task::ready(None).shared()) - } - - /// Returns the project environment, if possible. - /// If the project was opened from the CLI, then the inherited CLI environment is returned. - /// If it wasn't opened from the CLI, and an absolute path is given, then a shell is spawned in - /// that directory, to get environment variables as if the user has `cd`'d there. - pub fn local_directory_environment( - &mut self, - shell: &Shell, - abs_path: Arc, - cx: &mut App, - ) -> Shared>>> { - if let Some(cli_environment) = self.get_cli_environment() { - log::debug!("using project environment variables from CLI"); - return Task::ready(Some(cli_environment)).shared(); - } - - self.local_environments - .entry((shell.clone(), abs_path.clone())) - .or_insert_with(|| { - let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone(); - let shell = shell.clone(); - let tx = self.environment_error_messages_tx.clone(); - cx.spawn(async move |cx| { - let mut shell_env = cx - .background_spawn(load_directory_shell_environment( - shell, - abs_path.clone(), - load_direnv, - tx, - )) - .await - .log_err(); - - if let Some(shell_env) = shell_env.as_mut() { - let path = shell_env - .get("PATH") - .map(|path| path.as_str()) - .unwrap_or_default(); - log::debug!( - "using project environment variables shell launched in {:?}. PATH={:?}", - abs_path, - path - ); - - set_origin_marker(shell_env, EnvironmentOrigin::WorktreeShell); - } - - shell_env - }) - .shared() - }) - .clone() - } - - pub fn remote_directory_environment( - &mut self, - shell: &Shell, - abs_path: Arc, - remote_client: Entity, - cx: &mut App, - ) -> Shared>>> { - if cfg!(any(test, feature = "test-support")) { - return Task::ready(Some(HashMap::default())).shared(); - } - - self.remote_environments - .entry((shell.clone(), abs_path.clone())) - .or_insert_with(|| { - let response = - remote_client - .read(cx) - .proto_client() - .request(proto::GetDirectoryEnvironment { - project_id: REMOTE_SERVER_PROJECT_ID, - shell: Some(shell_to_proto(shell.clone())), - directory: abs_path.to_string_lossy().to_string(), - }); - cx.background_spawn(async move { - let environment = response.await.log_err()?; - Some(environment.environment.into_iter().collect()) - }) - .shared() - }) - .clone() - } - - pub fn peek_environment_error(&self) -> Option<&String> { - self.environment_error_messages.front() - } - - pub fn pop_environment_error(&mut self) -> Option { - self.environment_error_messages.pop_front() - } -} - -fn set_origin_marker(env: &mut HashMap, origin: EnvironmentOrigin) { - env.insert(ZED_ENVIRONMENT_ORIGIN_MARKER.to_string(), origin.into()); -} - -const ZED_ENVIRONMENT_ORIGIN_MARKER: &str = "ZED_ENVIRONMENT"; - -enum EnvironmentOrigin { - Cli, - WorktreeShell, -} - -impl From for String { - fn from(val: EnvironmentOrigin) -> Self { - match val { - EnvironmentOrigin::Cli => "cli".into(), - EnvironmentOrigin::WorktreeShell => "worktree-shell".into(), - } - } -} - -async fn load_directory_shell_environment( - shell: Shell, - abs_path: Arc, - load_direnv: DirenvSettings, - tx: mpsc::UnboundedSender, -) -> anyhow::Result> { - if let DirenvSettings::Disabled = load_direnv { - return Ok(HashMap::default()); - } - - let meta = smol::fs::metadata(&abs_path).await.with_context(|| { - tx.unbounded_send(format!("Failed to open {}", abs_path.display())) - .ok(); - format!("stat {abs_path:?}") - })?; - - let dir = if meta.is_dir() { - abs_path.clone() - } else { - abs_path - .parent() - .with_context(|| { - tx.unbounded_send(format!("Failed to open {}", abs_path.display())) - .ok(); - format!("getting parent of {abs_path:?}") - })? - .into() - }; - - let (shell, args) = shell.program_and_args(); - let mut envs = util::shell_env::capture(shell.clone(), args, abs_path) - .await - .with_context(|| { - tx.unbounded_send("Failed to load environment variables".into()) - .ok(); - format!("capturing shell environment with {shell:?}") - })?; - - if cfg!(target_os = "windows") - && let Some(path) = envs.remove("Path") - { - // windows env vars are case-insensitive, so normalize the path var - // so we can just assume `PATH` in other places - envs.insert("PATH".into(), path); - } - // If the user selects `Direct` for direnv, it would set an environment - // variable that later uses to know that it should not run the hook. - // We would include in `.envs` call so it is okay to run the hook - // even if direnv direct mode is enabled. - let direnv_environment = match load_direnv { - DirenvSettings::ShellHook => None, - DirenvSettings::Disabled => bail!("direnv integration is disabled"), - // Note: direnv is not available on Windows, so we skip direnv processing - // and just return the shell environment - DirenvSettings::Direct if cfg!(target_os = "windows") => None, - DirenvSettings::Direct => load_direnv_environment(&envs, &dir) - .await - .with_context(|| { - tx.unbounded_send("Failed to load direnv environment".into()) - .ok(); - "load direnv environment" - }) - .log_err(), - }; - if let Some(direnv_environment) = direnv_environment { - for (key, value) in direnv_environment { - if let Some(value) = value { - envs.insert(key, value); - } else { - envs.remove(&key); - } - } - } - - Ok(envs) -} - -async fn load_direnv_environment( - env: &HashMap, - dir: &Path, -) -> anyhow::Result>> { - let Some(direnv_path) = which::which("direnv").ok() else { - return Ok(HashMap::default()); - }; - - let args = &["export", "json"]; - let direnv_output = new_smol_command(&direnv_path) - .args(args) - .envs(env) - .env("TERM", "dumb") - .current_dir(dir) - .output() - .await - .context("running direnv")?; - - if !direnv_output.status.success() { - bail!( - "Loading direnv environment failed ({}), stderr: {}", - direnv_output.status, - String::from_utf8_lossy(&direnv_output.stderr) - ); - } - - let output = String::from_utf8_lossy(&direnv_output.stdout); - if output.is_empty() { - // direnv outputs nothing when it has no changes to apply to environment variables - return Ok(HashMap::default()); - } - - serde_json::from_str(&output).context("parsing direnv json") -} diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs deleted file mode 100644 index c73ab914b7..0000000000 --- a/crates/project/src/git_store.rs +++ /dev/null @@ -1,6297 +0,0 @@ -pub mod branch_diff; -mod conflict_set; -pub mod git_traversal; -pub mod pending_op; - -use crate::{ - ProjectEnvironment, ProjectItem, ProjectPath, - buffer_store::{BufferStore, BufferStoreEvent}, - worktree_store::{WorktreeStore, WorktreeStoreEvent}, -}; -use anyhow::{Context as _, Result, anyhow, bail}; -use askpass::{AskPassDelegate, EncryptedPassword, IKnowWhatIAmDoingAndIHaveReadTheDocs}; -use buffer_diff::{BufferDiff, BufferDiffEvent}; -use client::ProjectId; -use collections::HashMap; -pub use conflict_set::{ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate}; -use fs::Fs; -use futures::{ - FutureExt, StreamExt, - channel::{ - mpsc, - oneshot::{self, Canceled}, - }, - future::{self, Shared}, - stream::FuturesOrdered, -}; -use git::{ - BuildPermalinkParams, GitHostingProviderRegistry, Oid, RunHook, - blame::Blame, - parse_git_remote_url, - repository::{ - Branch, CommitDetails, CommitDiff, CommitFile, CommitOptions, DiffType, FetchOptions, - GitRepository, GitRepositoryCheckpoint, PushOptions, Remote, RemoteCommandOutput, RepoPath, - ResetMode, UpstreamTrackingStatus, Worktree as GitWorktree, - }, - stash::{GitStash, StashEntry}, - status::{ - DiffTreeType, FileStatus, GitSummary, StatusCode, TrackedStatus, TreeDiff, TreeDiffStatus, - UnmergedStatus, UnmergedStatusCode, - }, -}; -use gpui::{ - App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task, - WeakEntity, -}; -use language::{ - Buffer, BufferEvent, Language, LanguageRegistry, - proto::{deserialize_version, serialize_version}, -}; -use parking_lot::Mutex; -use pending_op::{PendingOp, PendingOpId, PendingOps, PendingOpsSummary}; -use postage::stream::Stream as _; -use rpc::{ - AnyProtoClient, TypedEnvelope, - proto::{self, git_reset, split_repository_update}, -}; -use serde::Deserialize; -use settings::WorktreeId; -use smol::future::yield_now; -use std::{ - cmp::Ordering, - collections::{BTreeSet, HashSet, VecDeque}, - future::Future, - mem, - ops::Range, - path::{Path, PathBuf}, - str::FromStr, - sync::{ - Arc, - atomic::{self, AtomicU64}, - }, - time::Instant, -}; -use sum_tree::{Edit, SumTree, TreeSet}; -use task::Shell; -use text::{Bias, BufferId}; -use util::{ - ResultExt, debug_panic, - paths::{PathStyle, SanitizedPath}, - post_inc, - rel_path::RelPath, -}; -use worktree::{ - File, PathChange, PathKey, PathProgress, PathSummary, PathTarget, ProjectEntryId, - UpdatedGitRepositoriesSet, UpdatedGitRepository, Worktree, -}; -use zeroize::Zeroize; - -pub struct GitStore { - state: GitStoreState, - buffer_store: Entity, - worktree_store: Entity, - repositories: HashMap>, - worktree_ids: HashMap>, - active_repo_id: Option, - #[allow(clippy::type_complexity)] - loading_diffs: - HashMap<(BufferId, DiffKind), Shared, Arc>>>>, - diffs: HashMap>, - shared_diffs: HashMap>, - _subscriptions: Vec, -} - -#[derive(Default)] -struct SharedDiffs { - unstaged: Option>, - uncommitted: Option>, -} - -struct BufferGitState { - unstaged_diff: Option>, - uncommitted_diff: Option>, - conflict_set: Option>, - recalculate_diff_task: Option>>, - reparse_conflict_markers_task: Option>>, - language: Option>, - language_registry: Option>, - conflict_updated_futures: Vec>, - recalculating_tx: postage::watch::Sender, - - /// These operation counts are used to ensure that head and index text - /// values read from the git repository are up-to-date with any hunk staging - /// operations that have been performed on the BufferDiff. - /// - /// The operation count is incremented immediately when the user initiates a - /// hunk stage/unstage operation. Then, upon finishing writing the new index - /// text do disk, the `operation count as of write` is updated to reflect - /// the operation count that prompted the write. - hunk_staging_operation_count: usize, - hunk_staging_operation_count_as_of_write: usize, - - head_text: Option>, - index_text: Option>, - head_changed: bool, - index_changed: bool, - language_changed: bool, -} - -#[derive(Clone, Debug)] -enum DiffBasesChange { - SetIndex(Option), - SetHead(Option), - SetEach { - index: Option, - head: Option, - }, - SetBoth(Option), -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -enum DiffKind { - Unstaged, - Uncommitted, -} - -enum GitStoreState { - Local { - next_repository_id: Arc, - downstream: Option, - project_environment: Entity, - fs: Arc, - }, - Remote { - upstream_client: AnyProtoClient, - upstream_project_id: u64, - downstream: Option<(AnyProtoClient, ProjectId)>, - }, -} - -enum DownstreamUpdate { - UpdateRepository(RepositorySnapshot), - RemoveRepository(RepositoryId), -} - -struct LocalDownstreamState { - client: AnyProtoClient, - project_id: ProjectId, - updates_tx: mpsc::UnboundedSender, - _task: Task>, -} - -#[derive(Clone, Debug)] -pub struct GitStoreCheckpoint { - checkpoints_by_work_dir_abs_path: HashMap, GitRepositoryCheckpoint>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct StatusEntry { - pub repo_path: RepoPath, - pub status: FileStatus, -} - -impl StatusEntry { - fn to_proto(&self) -> proto::StatusEntry { - let simple_status = match self.status { - FileStatus::Ignored | FileStatus::Untracked => proto::GitStatus::Added as i32, - FileStatus::Unmerged { .. } => proto::GitStatus::Conflict as i32, - FileStatus::Tracked(TrackedStatus { - index_status, - worktree_status, - }) => tracked_status_to_proto(if worktree_status != StatusCode::Unmodified { - worktree_status - } else { - index_status - }), - }; - - proto::StatusEntry { - repo_path: self.repo_path.to_proto(), - simple_status, - status: Some(status_to_proto(self.status)), - } - } -} - -impl TryFrom for StatusEntry { - type Error = anyhow::Error; - - fn try_from(value: proto::StatusEntry) -> Result { - let repo_path = RepoPath::from_proto(&value.repo_path).context("invalid repo path")?; - let status = status_from_proto(value.simple_status, value.status)?; - Ok(Self { repo_path, status }) - } -} - -impl sum_tree::Item for StatusEntry { - type Summary = PathSummary; - - fn summary(&self, _: ::Context<'_>) -> Self::Summary { - PathSummary { - max_path: self.repo_path.as_ref().clone(), - item_summary: self.status.summary(), - } - } -} - -impl sum_tree::KeyedItem for StatusEntry { - type Key = PathKey; - - fn key(&self) -> Self::Key { - PathKey(self.repo_path.as_ref().clone()) - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct RepositoryId(pub u64); - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct MergeDetails { - pub conflicted_paths: TreeSet, - pub message: Option, - pub heads: Vec>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RepositorySnapshot { - pub id: RepositoryId, - pub statuses_by_path: SumTree, - pub work_directory_abs_path: Arc, - pub path_style: PathStyle, - pub branch: Option, - pub head_commit: Option, - pub scan_id: u64, - pub merge: MergeDetails, - pub remote_origin_url: Option, - pub remote_upstream_url: Option, - pub stash_entries: GitStash, -} - -type JobId = u64; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct JobInfo { - pub start: Instant, - pub message: SharedString, -} - -pub struct Repository { - this: WeakEntity, - snapshot: RepositorySnapshot, - commit_message_buffer: Option>, - git_store: WeakEntity, - // For a local repository, holds paths that have had worktree events since the last status scan completed, - // and that should be examined during the next status scan. - paths_needing_status_update: BTreeSet, - job_sender: mpsc::UnboundedSender, - active_jobs: HashMap, - pending_ops: SumTree, - job_id: JobId, - askpass_delegates: Arc>>, - latest_askpass_id: u64, - repository_state: Shared>>, -} - -impl std::ops::Deref for Repository { - type Target = RepositorySnapshot; - - fn deref(&self) -> &Self::Target { - &self.snapshot - } -} - -#[derive(Clone)] -pub struct LocalRepositoryState { - pub fs: Arc, - pub backend: Arc, - pub environment: Arc>, -} - -impl LocalRepositoryState { - async fn new( - work_directory_abs_path: Arc, - dot_git_abs_path: Arc, - project_environment: WeakEntity, - fs: Arc, - cx: &mut AsyncApp, - ) -> anyhow::Result { - let environment = project_environment - .update(cx, |project_environment, cx| { - project_environment.local_directory_environment(&Shell::System, work_directory_abs_path.clone(), cx) - })? - .await - .unwrap_or_else(|| { - log::error!("failed to get working directory environment for repository {work_directory_abs_path:?}"); - HashMap::default() - }); - let search_paths = environment.get("PATH").map(|val| val.to_owned()); - let backend = cx - .background_spawn({ - let fs = fs.clone(); - async move { - let system_git_binary_path = search_paths - .and_then(|search_paths| { - which::which_in("git", Some(search_paths), &work_directory_abs_path) - .ok() - }) - .or_else(|| which::which("git").ok()); - fs.open_repo(&dot_git_abs_path, system_git_binary_path.as_deref()) - .with_context(|| format!("opening repository at {dot_git_abs_path:?}")) - } - }) - .await?; - Ok(LocalRepositoryState { - backend, - environment: Arc::new(environment), - fs, - }) - } -} - -#[derive(Clone)] -pub struct RemoteRepositoryState { - pub project_id: ProjectId, - pub client: AnyProtoClient, -} - -#[derive(Clone)] -pub enum RepositoryState { - Local(LocalRepositoryState), - Remote(RemoteRepositoryState), -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum RepositoryEvent { - StatusesChanged, - MergeHeadsChanged, - BranchChanged, - StashEntriesChanged, - PendingOpsChanged { pending_ops: SumTree }, -} - -#[derive(Clone, Debug)] -pub struct JobsUpdated; - -#[derive(Debug)] -pub enum GitStoreEvent { - ActiveRepositoryChanged(Option), - RepositoryUpdated(RepositoryId, RepositoryEvent, bool), - RepositoryAdded, - RepositoryRemoved(RepositoryId), - IndexWriteError(anyhow::Error), - JobsUpdated, - ConflictsUpdated, -} - -impl EventEmitter for Repository {} -impl EventEmitter for Repository {} -impl EventEmitter for GitStore {} - -pub struct GitJob { - job: Box Task<()>>, - key: Option, -} - -#[derive(PartialEq, Eq)] -enum GitJobKey { - WriteIndex(Vec), - ReloadBufferDiffBases, - RefreshStatuses, - ReloadGitState, -} - -impl GitStore { - pub fn local( - worktree_store: &Entity, - buffer_store: Entity, - environment: Entity, - fs: Arc, - cx: &mut Context, - ) -> Self { - Self::new( - worktree_store.clone(), - buffer_store, - GitStoreState::Local { - next_repository_id: Arc::new(AtomicU64::new(1)), - downstream: None, - project_environment: environment, - fs, - }, - cx, - ) - } - - pub fn remote( - worktree_store: &Entity, - buffer_store: Entity, - upstream_client: AnyProtoClient, - project_id: u64, - cx: &mut Context, - ) -> Self { - Self::new( - worktree_store.clone(), - buffer_store, - GitStoreState::Remote { - upstream_client, - upstream_project_id: project_id, - downstream: None, - }, - cx, - ) - } - - fn new( - worktree_store: Entity, - buffer_store: Entity, - state: GitStoreState, - cx: &mut Context, - ) -> Self { - let _subscriptions = vec![ - cx.subscribe(&worktree_store, Self::on_worktree_store_event), - cx.subscribe(&buffer_store, Self::on_buffer_store_event), - ]; - - GitStore { - state, - buffer_store, - worktree_store, - repositories: HashMap::default(), - worktree_ids: HashMap::default(), - active_repo_id: None, - _subscriptions, - loading_diffs: HashMap::default(), - shared_diffs: HashMap::default(), - diffs: HashMap::default(), - } - } - - 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_rename_branch); - client.add_entity_request_handler(Self::handle_create_remote); - client.add_entity_request_handler(Self::handle_remove_remote); - client.add_entity_request_handler(Self::handle_delete_branch); - client.add_entity_request_handler(Self::handle_git_init); - client.add_entity_request_handler(Self::handle_push); - client.add_entity_request_handler(Self::handle_pull); - client.add_entity_request_handler(Self::handle_fetch); - client.add_entity_request_handler(Self::handle_stage); - client.add_entity_request_handler(Self::handle_unstage); - client.add_entity_request_handler(Self::handle_stash); - client.add_entity_request_handler(Self::handle_stash_pop); - client.add_entity_request_handler(Self::handle_stash_apply); - client.add_entity_request_handler(Self::handle_stash_drop); - client.add_entity_request_handler(Self::handle_commit); - client.add_entity_request_handler(Self::handle_run_hook); - client.add_entity_request_handler(Self::handle_reset); - client.add_entity_request_handler(Self::handle_show); - client.add_entity_request_handler(Self::handle_load_commit_diff); - client.add_entity_request_handler(Self::handle_file_history); - client.add_entity_request_handler(Self::handle_checkout_files); - client.add_entity_request_handler(Self::handle_open_commit_message_buffer); - client.add_entity_request_handler(Self::handle_set_index_text); - client.add_entity_request_handler(Self::handle_askpass); - client.add_entity_request_handler(Self::handle_check_for_pushed_commits); - client.add_entity_request_handler(Self::handle_git_diff); - client.add_entity_request_handler(Self::handle_tree_diff); - client.add_entity_request_handler(Self::handle_get_blob_content); - client.add_entity_request_handler(Self::handle_open_unstaged_diff); - client.add_entity_request_handler(Self::handle_open_uncommitted_diff); - client.add_entity_message_handler(Self::handle_update_diff_bases); - client.add_entity_request_handler(Self::handle_get_permalink_to_line); - 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); - client.add_entity_request_handler(Self::handle_get_worktrees); - client.add_entity_request_handler(Self::handle_create_worktree); - } - - pub fn is_local(&self) -> bool { - matches!(self.state, GitStoreState::Local { .. }) - } - pub fn set_active_repo_for_path(&mut self, project_path: &ProjectPath, cx: &mut Context) { - if let Some((repo, _)) = self.repository_and_path_for_project_path(project_path, cx) { - let id = repo.read(cx).id; - if self.active_repo_id != Some(id) { - self.active_repo_id = Some(id); - cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id))); - } - } - } - - pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context) { - match &mut self.state { - GitStoreState::Remote { - downstream: downstream_client, - .. - } => { - for repo in self.repositories.values() { - let update = repo.read(cx).snapshot.initial_update(project_id); - for update in split_repository_update(update) { - client.send(update).log_err(); - } - } - *downstream_client = Some((client, ProjectId(project_id))); - } - GitStoreState::Local { - downstream: downstream_client, - .. - } => { - let mut snapshots = HashMap::default(); - let (updates_tx, mut updates_rx) = mpsc::unbounded(); - for repo in self.repositories.values() { - updates_tx - .unbounded_send(DownstreamUpdate::UpdateRepository( - repo.read(cx).snapshot.clone(), - )) - .ok(); - } - *downstream_client = Some(LocalDownstreamState { - client: client.clone(), - project_id: ProjectId(project_id), - updates_tx, - _task: cx.spawn(async move |this, cx| { - cx.background_spawn(async move { - while let Some(update) = updates_rx.next().await { - match update { - DownstreamUpdate::UpdateRepository(snapshot) => { - if let Some(old_snapshot) = snapshots.get_mut(&snapshot.id) - { - let update = - snapshot.build_update(old_snapshot, project_id); - *old_snapshot = snapshot; - for update in split_repository_update(update) { - client.send(update)?; - } - } else { - let update = snapshot.initial_update(project_id); - for update in split_repository_update(update) { - client.send(update)?; - } - snapshots.insert(snapshot.id, snapshot); - } - } - DownstreamUpdate::RemoveRepository(id) => { - client.send(proto::RemoveRepository { - project_id, - id: id.to_proto(), - })?; - } - } - } - anyhow::Ok(()) - }) - .await - .ok(); - this.update(cx, |this, _| { - if let GitStoreState::Local { - downstream: downstream_client, - .. - } = &mut this.state - { - downstream_client.take(); - } else { - unreachable!("unshared called on remote store"); - } - }) - }), - }); - } - } - } - - pub fn unshared(&mut self, _cx: &mut Context) { - match &mut self.state { - GitStoreState::Local { - downstream: downstream_client, - .. - } => { - downstream_client.take(); - } - GitStoreState::Remote { - downstream: downstream_client, - .. - } => { - downstream_client.take(); - } - } - self.shared_diffs.clear(); - } - - pub(crate) fn forget_shared_diffs_for(&mut self, peer_id: &proto::PeerId) { - self.shared_diffs.remove(peer_id); - } - - pub fn active_repository(&self) -> Option> { - self.active_repo_id - .as_ref() - .map(|id| self.repositories[id].clone()) - } - - pub fn open_unstaged_diff( - &mut self, - buffer: Entity, - 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 - .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) - }); - } - return Task::ready(Ok(unstaged_diff)); - } - - let Some((repo, repo_path)) = - self.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx) - else { - return Task::ready(Err(anyhow!("failed to find git repository for buffer"))); - }; - - let task = self - .loading_diffs - .entry((buffer_id, DiffKind::Unstaged)) - .or_insert_with(|| { - let staged_text = repo.update(cx, |repo, cx| { - repo.load_staged_text(buffer_id, repo_path, cx) - }); - cx.spawn(async move |this, cx| { - Self::open_diff_internal( - this, - DiffKind::Unstaged, - staged_text.await.map(DiffBasesChange::SetIndex), - buffer, - cx, - ) - .await - .map_err(Arc::new) - }) - .shared() - }) - .clone(); - - cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) }) - } - - pub fn open_diff_since( - &mut self, - oid: Option, - buffer: Entity, - repo: Entity, - languages: Arc, - cx: &mut Context, - ) -> Task>> { - cx.spawn(async move |this, cx| { - let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?; - let content = match oid { - None => None, - Some(oid) => Some( - repo.update(cx, |repo, cx| repo.load_blob_content(oid, cx))? - .await?, - ), - }; - let buffer_diff = cx.new(|cx| BufferDiff::new(&buffer_snapshot, cx))?; - - buffer_diff - .update(cx, |buffer_diff, cx| { - buffer_diff.set_base_text( - content.map(Arc::new), - buffer_snapshot.language().cloned(), - Some(languages.clone()), - buffer_snapshot.text, - cx, - ) - })? - .await?; - let unstaged_diff = this - .update(cx, |this, cx| this.open_unstaged_diff(buffer.clone(), cx))? - .await?; - buffer_diff.update(cx, |buffer_diff, _| { - buffer_diff.set_secondary_diff(unstaged_diff); - })?; - - this.update(cx, |_, cx| { - cx.subscribe(&buffer_diff, Self::on_buffer_diff_event) - .detach(); - })?; - - Ok(buffer_diff) - }) - } - - pub fn open_uncommitted_diff( - &mut self, - buffer: Entity, - cx: &mut Context, - ) -> 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 - .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) - }); - } - return Task::ready(Ok(uncommitted_diff)); - } - - let Some((repo, repo_path)) = - self.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx) - else { - return Task::ready(Err(anyhow!("failed to find git repository for buffer"))); - }; - - let task = self - .loading_diffs - .entry((buffer_id, DiffKind::Uncommitted)) - .or_insert_with(|| { - let changes = repo.update(cx, |repo, cx| { - repo.load_committed_text(buffer_id, repo_path, cx) - }); - - // todo(lw): hot foreground spawn - cx.spawn(async move |this, cx| { - Self::open_diff_internal(this, DiffKind::Uncommitted, changes.await, buffer, cx) - .await - .map_err(Arc::new) - }) - .shared() - }) - .clone(); - - cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) }) - } - - async fn open_diff_internal( - this: WeakEntity, - kind: DiffKind, - texts: Result, - buffer_entity: Entity, - cx: &mut AsyncApp, - ) -> Result> { - let diff_bases_change = match texts { - Err(e) => { - this.update(cx, |this, cx| { - let buffer = buffer_entity.read(cx); - let buffer_id = buffer.remote_id(); - this.loading_diffs.remove(&(buffer_id, kind)); - })?; - return Err(e); - } - Ok(change) => change, - }; - - this.update(cx, |this, cx| { - let buffer = buffer_entity.read(cx); - let buffer_id = buffer.remote_id(); - let language = buffer.language().cloned(); - let language_registry = buffer.language_registry(); - let text_snapshot = buffer.text_snapshot(); - this.loading_diffs.remove(&(buffer_id, kind)); - - let git_store = cx.weak_entity(); - let diff_state = this - .diffs - .entry(buffer_id) - .or_insert_with(|| cx.new(|_| BufferGitState::new(git_store))); - - let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx)); - - cx.subscribe(&diff, Self::on_buffer_diff_event).detach(); - diff_state.update(cx, |diff_state, cx| { - diff_state.language = language; - diff_state.language_registry = language_registry; - - match kind { - DiffKind::Unstaged => diff_state.unstaged_diff = Some(diff.downgrade()), - DiffKind::Uncommitted => { - let unstaged_diff = if let Some(diff) = diff_state.unstaged_diff() { - diff - } else { - let unstaged_diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx)); - diff_state.unstaged_diff = Some(unstaged_diff.downgrade()); - unstaged_diff - }; - - diff.update(cx, |diff, _| diff.set_secondary_diff(unstaged_diff)); - diff_state.uncommitted_diff = Some(diff.downgrade()) - } - } - - diff_state.diff_bases_changed(text_snapshot, Some(diff_bases_change), cx); - let rx = diff_state.wait_for_recalculation(); - - anyhow::Ok(async move { - if let Some(rx) = rx { - rx.await; - } - Ok(diff) - }) - }) - })?? - .await - } - - pub fn get_unstaged_diff(&self, buffer_id: BufferId, cx: &App) -> Option> { - let diff_state = self.diffs.get(&buffer_id)?; - diff_state.read(cx).unstaged_diff.as_ref()?.upgrade() - } - - pub fn get_uncommitted_diff( - &self, - buffer_id: BufferId, - cx: &App, - ) -> Option> { - let diff_state = self.diffs.get(&buffer_id)?; - diff_state.read(cx).uncommitted_diff.as_ref()?.upgrade() - } - - pub fn open_conflict_set( - &mut self, - buffer: Entity, - cx: &mut Context, - ) -> Entity { - 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 - .read(cx) - .conflict_set - .as_ref() - .and_then(|weak| weak.upgrade()) - { - let conflict_set = conflict_set; - let buffer_snapshot = buffer.read(cx).text_snapshot(); - - git_state.update(cx, |state, cx| { - let _ = state.reparse_conflict_markers(buffer_snapshot, cx); - }); - - 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)); - let git_store = cx.weak_entity(); - let buffer_git_state = self - .diffs - .entry(buffer_id) - .or_insert_with(|| cx.new(|_| BufferGitState::new(git_store))); - let conflict_set = cx.new(|cx| ConflictSet::new(buffer_id, is_unmerged, cx)); - - self._subscriptions - .push(cx.subscribe(&conflict_set, |_, _, _, cx| { - cx.emit(GitStoreEvent::ConflictsUpdated); - })); - - buffer_git_state.update(cx, |state, cx| { - state.conflict_set = Some(conflict_set.downgrade()); - let buffer_snapshot = buffer.read(cx).text_snapshot(); - let _ = state.reparse_conflict_markers(buffer_snapshot, cx); - }); - - conflict_set - } - - pub fn project_path_git_status( - &self, - project_path: &ProjectPath, - cx: &App, - ) -> Option { - let (repo, repo_path) = self.repository_and_path_for_project_path(project_path, cx)?; - Some(repo.read(cx).status_for_path(&repo_path)?.status) - } - - pub fn checkpoint(&self, cx: &mut App) -> Task> { - let mut work_directory_abs_paths = Vec::new(); - let mut checkpoints = Vec::new(); - for repository in self.repositories.values() { - repository.update(cx, |repository, _| { - work_directory_abs_paths.push(repository.snapshot.work_directory_abs_path.clone()); - checkpoints.push(repository.checkpoint().map(|checkpoint| checkpoint?)); - }); - } - - cx.background_executor().spawn(async move { - let checkpoints = future::try_join_all(checkpoints).await?; - Ok(GitStoreCheckpoint { - checkpoints_by_work_dir_abs_path: work_directory_abs_paths - .into_iter() - .zip(checkpoints) - .collect(), - }) - }) - } - - pub fn restore_checkpoint( - &self, - checkpoint: GitStoreCheckpoint, - cx: &mut App, - ) -> Task> { - let repositories_by_work_dir_abs_path = self - .repositories - .values() - .map(|repo| (repo.read(cx).snapshot.work_directory_abs_path.clone(), repo)) - .collect::>(); - - let mut tasks = Vec::new(); - for (work_dir_abs_path, checkpoint) in checkpoint.checkpoints_by_work_dir_abs_path { - if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path) { - let restore = repository.update(cx, |repository, _| { - repository.restore_checkpoint(checkpoint) - }); - tasks.push(async move { restore.await? }); - } - } - cx.background_spawn(async move { - future::try_join_all(tasks).await?; - Ok(()) - }) - } - - /// Compares two checkpoints, returning true if they are equal. - pub fn compare_checkpoints( - &self, - left: GitStoreCheckpoint, - mut right: GitStoreCheckpoint, - cx: &mut App, - ) -> Task> { - let repositories_by_work_dir_abs_path = self - .repositories - .values() - .map(|repo| (repo.read(cx).snapshot.work_directory_abs_path.clone(), repo)) - .collect::>(); - - let mut tasks = Vec::new(); - for (work_dir_abs_path, left_checkpoint) in left.checkpoints_by_work_dir_abs_path { - if let Some(right_checkpoint) = right - .checkpoints_by_work_dir_abs_path - .remove(&work_dir_abs_path) - { - if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path) - { - let compare = repository.update(cx, |repository, _| { - repository.compare_checkpoints(left_checkpoint, right_checkpoint) - }); - - tasks.push(async move { compare.await? }); - } - } else { - return Task::ready(Ok(false)); - } - } - cx.background_spawn(async move { - Ok(future::try_join_all(tasks) - .await? - .into_iter() - .all(|result| result)) - }) - } - - /// Blames a buffer. - pub fn blame_buffer( - &self, - buffer: &Entity, - version: Option, - cx: &mut Context, - ) -> Task>> { - let buffer = buffer.read(cx); - let Some((repo, repo_path)) = - self.repository_and_path_for_buffer_id(buffer.remote_id(), cx) - else { - return Task::ready(Err(anyhow!("failed to find a git repository for buffer"))); - }; - let content = match &version { - Some(version) => buffer.rope_for_version(version), - None => buffer.as_rope().clone(), - }; - let line_ending = buffer.line_ending(); - let version = version.unwrap_or(buffer.version()); - let buffer_id = buffer.remote_id(); - - let repo = repo.downgrade(); - cx.spawn(async move |_, cx| { - let repository_state = repo - .update(cx, |repo, _| repo.repository_state.clone())? - .await - .map_err(|err| anyhow::anyhow!(err))?; - match repository_state { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => backend - .blame(repo_path.clone(), content, line_ending) - .await - .with_context(|| format!("Failed to blame {:?}", repo_path.as_ref())) - .map(Some), - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let response = client - .request(proto::BlameBuffer { - project_id: project_id.to_proto(), - buffer_id: buffer_id.into(), - version: serialize_version(&version), - }) - .await?; - Ok(deserialize_blame_buffer_response(response)) - } - } - }) - } - - pub fn file_history( - &self, - repo: &Entity, - path: RepoPath, - cx: &mut App, - ) -> Task> { - let rx = repo.update(cx, |repo, _| repo.file_history(path)); - - cx.spawn(|_: &mut AsyncApp| async move { rx.await? }) - } - - pub fn file_history_paginated( - &self, - repo: &Entity, - path: RepoPath, - skip: usize, - limit: Option, - cx: &mut App, - ) -> Task> { - let rx = repo.update(cx, |repo, _| repo.file_history_paginated(path, skip, limit)); - - cx.spawn(|_: &mut AsyncApp| async move { rx.await? }) - } - - pub fn get_permalink_to_line( - &self, - buffer: &Entity, - selection: Range, - cx: &mut App, - ) -> Task> { - let Some(file) = File::from_dyn(buffer.read(cx).file()) else { - return Task::ready(Err(anyhow!("buffer has no file"))); - }; - - let Some((repo, repo_path)) = self.repository_and_path_for_project_path( - &(file.worktree.read(cx).id(), file.path.clone()).into(), - cx, - ) else { - // If we're not in a Git repo, check whether this is a Rust source - // file in the Cargo registry (presumably opened with go-to-definition - // from a normal Rust file). If so, we can put together a permalink - // using crate metadata. - if buffer - .read(cx) - .language() - .is_none_or(|lang| lang.name() != "Rust".into()) - { - return Task::ready(Err(anyhow!("no permalink available"))); - } - let file_path = file.worktree.read(cx).absolutize(&file.path); - return cx.spawn(async move |cx| { - let provider_registry = cx.update(GitHostingProviderRegistry::default_global)?; - get_permalink_in_rust_registry_src(provider_registry, file_path, selection) - .context("no permalink available") - }); - }; - - let buffer_id = buffer.read(cx).remote_id(); - let branch = repo.read(cx).branch.clone(); - let remote = branch - .as_ref() - .and_then(|b| b.upstream.as_ref()) - .and_then(|b| b.remote_name()) - .unwrap_or("origin") - .to_string(); - - let rx = repo.update(cx, |repo, _| { - repo.send_job(None, move |state, cx| async move { - match state { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - let origin_url = backend - .remote_url(&remote) - .await - .with_context(|| format!("remote \"{remote}\" not found"))?; - - let sha = backend.head_sha().await.context("reading HEAD SHA")?; - - let provider_registry = - cx.update(GitHostingProviderRegistry::default_global)?; - - let (provider, remote) = - parse_git_remote_url(provider_registry, &origin_url) - .context("parsing Git remote URL")?; - - Ok(provider.build_permalink( - remote, - BuildPermalinkParams::new(&sha, &repo_path, Some(selection)), - )) - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let response = client - .request(proto::GetPermalinkToLine { - project_id: project_id.to_proto(), - buffer_id: buffer_id.into(), - selection: Some(proto::Range { - start: selection.start as u64, - end: selection.end as u64, - }), - }) - .await?; - - url::Url::parse(&response.permalink).context("failed to parse permalink") - } - } - }) - }); - cx.spawn(|_: &mut AsyncApp| async move { rx.await? }) - } - - fn downstream_client(&self) -> Option<(AnyProtoClient, ProjectId)> { - match &self.state { - GitStoreState::Local { - downstream: downstream_client, - .. - } => downstream_client - .as_ref() - .map(|state| (state.client.clone(), state.project_id)), - GitStoreState::Remote { - downstream: downstream_client, - .. - } => downstream_client.clone(), - } - } - - fn upstream_client(&self) -> Option { - match &self.state { - GitStoreState::Local { .. } => None, - GitStoreState::Remote { - upstream_client, .. - } => Some(upstream_client.clone()), - } - } - - fn on_worktree_store_event( - &mut self, - worktree_store: Entity, - event: &WorktreeStoreEvent, - cx: &mut Context, - ) { - let GitStoreState::Local { - project_environment, - downstream, - next_repository_id, - fs, - } = &self.state - else { - return; - }; - - match event { - WorktreeStoreEvent::WorktreeUpdatedEntries(worktree_id, updated_entries) => { - if let Some(worktree) = self - .worktree_store - .read(cx) - .worktree_for_id(*worktree_id, cx) - { - let paths_by_git_repo = - self.process_updated_entries(&worktree, updated_entries, cx); - let downstream = downstream - .as_ref() - .map(|downstream| downstream.updates_tx.clone()); - cx.spawn(async move |_, cx| { - let paths_by_git_repo = paths_by_git_repo.await; - for (repo, paths) in paths_by_git_repo { - repo.update(cx, |repo, cx| { - repo.paths_changed(paths, downstream.clone(), cx); - }) - .ok(); - } - }) - .detach(); - } - } - WorktreeStoreEvent::WorktreeUpdatedGitRepositories(worktree_id, changed_repos) => { - let Some(worktree) = worktree_store.read(cx).worktree_for_id(*worktree_id, cx) - else { - return; - }; - if !worktree.read(cx).is_visible() { - log::debug!( - "not adding repositories for local worktree {:?} because it's not visible", - worktree.read(cx).abs_path() - ); - return; - } - self.update_repositories_from_worktree( - *worktree_id, - project_environment.clone(), - next_repository_id.clone(), - downstream - .as_ref() - .map(|downstream| downstream.updates_tx.clone()), - changed_repos.clone(), - fs.clone(), - cx, - ); - self.local_worktree_git_repos_changed(worktree, changed_repos, cx); - } - WorktreeStoreEvent::WorktreeRemoved(_entity_id, worktree_id) => { - let repos_without_worktree: Vec = self - .worktree_ids - .iter_mut() - .filter_map(|(repo_id, worktree_ids)| { - worktree_ids.remove(worktree_id); - if worktree_ids.is_empty() { - Some(*repo_id) - } else { - None - } - }) - .collect(); - let is_active_repo_removed = repos_without_worktree - .iter() - .any(|repo_id| self.active_repo_id == Some(*repo_id)); - - for repo_id in repos_without_worktree { - self.repositories.remove(&repo_id); - self.worktree_ids.remove(&repo_id); - if let Some(updates_tx) = - downstream.as_ref().map(|downstream| &downstream.updates_tx) - { - updates_tx - .unbounded_send(DownstreamUpdate::RemoveRepository(repo_id)) - .ok(); - } - } - - if is_active_repo_removed { - if let Some((&repo_id, _)) = self.repositories.iter().next() { - self.active_repo_id = Some(repo_id); - cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(repo_id))); - } else { - self.active_repo_id = None; - cx.emit(GitStoreEvent::ActiveRepositoryChanged(None)); - } - } - } - _ => {} - } - } - fn on_repository_event( - &mut self, - repo: Entity, - event: &RepositoryEvent, - cx: &mut Context, - ) { - let id = repo.read(cx).id; - let repo_snapshot = repo.read(cx).snapshot.clone(); - 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); - } - } - } - anyhow::Ok(()) - }) - .ok(); - } - } - cx.emit(GitStoreEvent::RepositoryUpdated( - id, - event.clone(), - self.active_repo_id == Some(id), - )) - } - - fn on_jobs_updated(&mut self, _: Entity, _: &JobsUpdated, cx: &mut Context) { - cx.emit(GitStoreEvent::JobsUpdated) - } - - /// Update our list of repositories and schedule git scans in response to a notification from a worktree, - fn update_repositories_from_worktree( - &mut self, - worktree_id: WorktreeId, - project_environment: Entity, - next_repository_id: Arc, - updates_tx: Option>, - updated_git_repositories: UpdatedGitRepositoriesSet, - fs: Arc, - cx: &mut Context, - ) { - let mut removed_ids = Vec::new(); - for update in updated_git_repositories.iter() { - if let Some((id, existing)) = self.repositories.iter().find(|(_, repo)| { - let existing_work_directory_abs_path = - repo.read(cx).work_directory_abs_path.clone(); - Some(&existing_work_directory_abs_path) - == update.old_work_directory_abs_path.as_ref() - || Some(&existing_work_directory_abs_path) - == update.new_work_directory_abs_path.as_ref() - }) { - let repo_id = *id; - if let Some(new_work_directory_abs_path) = - update.new_work_directory_abs_path.clone() - { - self.worktree_ids - .entry(repo_id) - .or_insert_with(HashSet::new) - .insert(worktree_id); - existing.update(cx, |existing, cx| { - existing.snapshot.work_directory_abs_path = new_work_directory_abs_path; - existing.schedule_scan(updates_tx.clone(), cx); - }); - } else { - if let Some(worktree_ids) = self.worktree_ids.get_mut(&repo_id) { - worktree_ids.remove(&worktree_id); - if worktree_ids.is_empty() { - removed_ids.push(repo_id); - } - } - } - } else if let UpdatedGitRepository { - new_work_directory_abs_path: Some(work_directory_abs_path), - dot_git_abs_path: Some(dot_git_abs_path), - repository_dir_abs_path: Some(_repository_dir_abs_path), - common_dir_abs_path: Some(_common_dir_abs_path), - .. - } = update - { - let id = RepositoryId(next_repository_id.fetch_add(1, atomic::Ordering::Release)); - let git_store = cx.weak_entity(); - let repo = cx.new(|cx| { - let mut repo = Repository::local( - id, - work_directory_abs_path.clone(), - dot_git_abs_path.clone(), - project_environment.downgrade(), - fs.clone(), - git_store, - cx, - ); - if let Some(updates_tx) = updates_tx.as_ref() { - // trigger an empty `UpdateRepository` to ensure remote active_repo_id is set correctly - updates_tx - .unbounded_send(DownstreamUpdate::UpdateRepository(repo.snapshot())) - .ok(); - } - repo.schedule_scan(updates_tx.clone(), cx); - repo - }); - self._subscriptions - .push(cx.subscribe(&repo, Self::on_repository_event)); - self._subscriptions - .push(cx.subscribe(&repo, Self::on_jobs_updated)); - self.repositories.insert(id, repo); - self.worktree_ids.insert(id, HashSet::from([worktree_id])); - cx.emit(GitStoreEvent::RepositoryAdded); - self.active_repo_id.get_or_insert_with(|| { - cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id))); - id - }); - } - } - - for id in removed_ids { - if self.active_repo_id == Some(id) { - self.active_repo_id = None; - cx.emit(GitStoreEvent::ActiveRepositoryChanged(None)); - } - self.repositories.remove(&id); - if let Some(updates_tx) = updates_tx.as_ref() { - updates_tx - .unbounded_send(DownstreamUpdate::RemoveRepository(id)) - .ok(); - } - } - } - - fn on_buffer_store_event( - &mut self, - _: Entity, - event: &BufferStoreEvent, - cx: &mut Context, - ) { - match event { - BufferStoreEvent::BufferAdded(buffer) => { - 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) { - diff_state.update(cx, |diff_state, cx| { - diff_state.buffer_language_changed(buffer, cx); - }); - } - } - }) - .detach(); - } - BufferStoreEvent::SharedBufferClosed(peer_id, buffer_id) => { - if let Some(diffs) = self.shared_diffs.get_mut(peer_id) { - diffs.remove(buffer_id); - } - } - BufferStoreEvent::BufferDropped(buffer_id) => { - self.diffs.remove(buffer_id); - for diffs in self.shared_diffs.values_mut() { - diffs.remove(buffer_id); - } - } - BufferStoreEvent::BufferChangedFilePath { buffer, .. } => { - // Whenever a buffer's file path changes, it's possible that the - // new path is actually a path that is being tracked by a git - // repository. In that case, we'll want to update the buffer's - // `BufferDiffState`, in case it already has one. - let buffer_id = buffer.read(cx).remote_id(); - let diff_state = self.diffs.get(&buffer_id); - let repo = self.repository_and_path_for_buffer_id(buffer_id, cx); - - if let Some(diff_state) = diff_state - && let Some((repo, repo_path)) = repo - { - let buffer = buffer.clone(); - let diff_state = diff_state.clone(); - - cx.spawn(async move |_git_store, cx| { - async { - let diff_bases_change = repo - .update(cx, |repo, cx| { - repo.load_committed_text(buffer_id, repo_path, cx) - })? - .await?; - - diff_state.update(cx, |diff_state, cx| { - let buffer_snapshot = buffer.read(cx).text_snapshot(); - diff_state.diff_bases_changed( - buffer_snapshot, - Some(diff_bases_change), - cx, - ); - }) - } - .await - .log_err(); - }) - .detach(); - } - } - _ => {} - } - } - - pub fn recalculate_buffer_diffs( - &mut self, - buffers: Vec>, - cx: &mut Context, - ) -> impl Future + use<> { - let mut futures = Vec::new(); - for buffer in buffers { - if let Some(diff_state) = self.diffs.get_mut(&buffer.read(cx).remote_id()) { - let buffer = buffer.read(cx).text_snapshot(); - diff_state.update(cx, |diff_state, cx| { - diff_state.recalculate_diffs(buffer.clone(), cx); - futures.extend(diff_state.wait_for_recalculation().map(FutureExt::boxed)); - }); - futures.push(diff_state.update(cx, |diff_state, cx| { - diff_state - .reparse_conflict_markers(buffer, cx) - .map(|_| {}) - .boxed() - })); - } - } - async move { - futures::future::join_all(futures).await; - } - } - - fn on_buffer_diff_event( - &mut self, - diff: Entity, - event: &BufferDiffEvent, - cx: &mut Context, - ) { - if let BufferDiffEvent::HunksStagedOrUnstaged(new_index_text) = event { - let buffer_id = diff.read(cx).buffer_id; - if let Some(diff_state) = self.diffs.get(&buffer_id) { - let hunk_staging_operation_count = diff_state.update(cx, |diff_state, _| { - diff_state.hunk_staging_operation_count += 1; - diff_state.hunk_staging_operation_count - }); - if let Some((repo, path)) = self.repository_and_path_for_buffer_id(buffer_id, cx) { - let recv = repo.update(cx, |repo, cx| { - log::debug!("hunks changed for {}", path.as_unix_str()); - repo.spawn_set_index_text_job( - path, - new_index_text.as_ref().map(|rope| rope.to_string()), - Some(hunk_staging_operation_count), - cx, - ) - }); - let diff = diff.downgrade(); - cx.spawn(async move |this, cx| { - if let Ok(Err(error)) = cx.background_spawn(recv).await { - diff.update(cx, |diff, cx| { - diff.clear_pending_hunks(cx); - }) - .ok(); - this.update(cx, |_, cx| cx.emit(GitStoreEvent::IndexWriteError(error))) - .ok(); - } - }) - .detach(); - } - } - } - } - - fn local_worktree_git_repos_changed( - &mut self, - worktree: Entity, - changed_repos: &UpdatedGitRepositoriesSet, - cx: &mut Context, - ) { - log::debug!("local worktree repos changed"); - debug_assert!(worktree.read(cx).is_local()); - - for repository in self.repositories.values() { - 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) - }) { - repository.reload_buffer_diff_bases(cx); - } - }); - } - } - - pub fn repositories(&self) -> &HashMap> { - &self.repositories - } - - pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option { - let (repo, path) = self.repository_and_path_for_buffer_id(buffer_id, cx)?; - let status = repo.read(cx).snapshot.status_for_path(&path)?; - Some(status.status) - } - - pub fn repository_and_path_for_buffer_id( - &self, - buffer_id: BufferId, - cx: &App, - ) -> Option<(Entity, RepoPath)> { - let buffer = self.buffer_store.read(cx).get(buffer_id)?; - let project_path = buffer.read(cx).project_path(cx)?; - self.repository_and_path_for_project_path(&project_path, cx) - } - - pub fn repository_and_path_for_project_path( - &self, - path: &ProjectPath, - cx: &App, - ) -> Option<(Entity, RepoPath)> { - let abs_path = self.worktree_store.read(cx).absolutize(path, cx)?; - self.repositories - .values() - .filter_map(|repo| { - let repo_path = repo.read(cx).abs_path_to_repo_path(&abs_path)?; - Some((repo.clone(), repo_path)) - }) - .max_by_key(|(repo, _)| repo.read(cx).work_directory_abs_path.clone()) - } - - pub fn git_init( - &self, - path: Arc, - fallback_branch_name: String, - cx: &App, - ) -> Task> { - match &self.state { - GitStoreState::Local { fs, .. } => { - let fs = fs.clone(); - cx.background_executor() - .spawn(async move { fs.git_init(&path, fallback_branch_name).await }) - } - GitStoreState::Remote { - upstream_client, - upstream_project_id: project_id, - .. - } => { - let client = upstream_client.clone(); - let project_id = *project_id; - cx.background_executor().spawn(async move { - client - .request(proto::GitInit { - project_id: project_id, - abs_path: path.to_string_lossy().into_owned(), - fallback_branch_name, - }) - .await?; - Ok(()) - }) - } - } - } - - 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::Remote { - upstream_client, - upstream_project_id, - .. - } => { - if upstream_client.is_via_collab() { - return Task::ready(Err(anyhow!( - "Git Clone isn't supported for project guests" - ))); - } - let request = upstream_client.request(proto::GitClone { - project_id: *upstream_project_id, - abs_path: path.to_string_lossy().into_owned(), - remote_repo: repo, - }); - - cx.background_spawn(async move { - let result = request.await?; - - match result.success { - true => Ok(()), - false => Err(anyhow!("Git Clone failed")), - } - }) - } - } - } - - async fn handle_update_repository( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - let path_style = this.worktree_store.read(cx).path_style(); - let mut update = envelope.payload; - - let id = RepositoryId::from_proto(update.id); - let client = this.upstream_client().context("no upstream client")?; - - let mut repo_subscription = None; - let repo = this.repositories.entry(id).or_insert_with(|| { - let git_store = cx.weak_entity(); - let repo = cx.new(|cx| { - Repository::remote( - id, - Path::new(&update.abs_path).into(), - path_style, - ProjectId(update.project_id), - client, - git_store, - cx, - ) - }); - repo_subscription = Some(cx.subscribe(&repo, Self::on_repository_event)); - cx.emit(GitStoreEvent::RepositoryAdded); - repo - }); - this._subscriptions.extend(repo_subscription); - - repo.update(cx, { - let update = update.clone(); - |repo, cx| repo.apply_remote_update(update, cx) - })?; - - this.active_repo_id.get_or_insert_with(|| { - cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id))); - id - }); - - if let Some((client, project_id)) = this.downstream_client() { - update.project_id = project_id.to_proto(); - client.send(update).log_err(); - } - Ok(()) - })? - } - - async fn handle_remove_repository( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - let mut update = envelope.payload; - let id = RepositoryId::from_proto(update.id); - this.repositories.remove(&id); - if let Some((client, project_id)) = this.downstream_client() { - update.project_id = project_id.to_proto(); - client.send(update).log_err(); - } - if this.active_repo_id == Some(id) { - this.active_repo_id = None; - cx.emit(GitStoreEvent::ActiveRepositoryChanged(None)); - } - cx.emit(GitStoreEvent::RepositoryRemoved(id)); - }) - } - - async fn handle_git_init( - this: Entity, - envelope: TypedEnvelope, - cx: AsyncApp, - ) -> Result { - let path: Arc = PathBuf::from(envelope.payload.abs_path).into(); - let name = envelope.payload.fallback_branch_name; - cx.update(|cx| this.read(cx).git_init(path, name, cx))? - .await?; - - 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, - 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 fetch_options = FetchOptions::from_proto(envelope.payload.remote); - let askpass_id = envelope.payload.askpass_id; - - let askpass = make_remote_delegate( - this, - envelope.payload.project_id, - repository_id, - askpass_id, - &mut cx, - ); - - let remote_output = repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.fetch(fetch_options, askpass, cx) - })? - .await??; - - Ok(proto::RemoteMessageResponse { - stdout: remote_output.stdout, - stderr: remote_output.stderr, - }) - } - - async fn handle_push( - 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 askpass_id = envelope.payload.askpass_id; - let askpass = make_remote_delegate( - this, - envelope.payload.project_id, - repository_id, - askpass_id, - &mut cx, - ); - - let options = envelope - .payload - .options - .as_ref() - .map(|_| match envelope.payload.options() { - proto::push::PushOptions::SetUpstream => git::repository::PushOptions::SetUpstream, - proto::push::PushOptions::Force => git::repository::PushOptions::Force, - }); - - let branch_name = envelope.payload.branch_name.into(); - let remote_name = envelope.payload.remote_name.into(); - - let remote_output = repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.push(branch_name, remote_name, options, askpass, cx) - })? - .await??; - Ok(proto::RemoteMessageResponse { - stdout: remote_output.stdout, - stderr: remote_output.stderr, - }) - } - - async fn handle_pull( - 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 askpass_id = envelope.payload.askpass_id; - let askpass = make_remote_delegate( - this, - envelope.payload.project_id, - repository_id, - askpass_id, - &mut cx, - ); - - let branch_name = envelope.payload.branch_name.map(|name| name.into()); - let remote_name = envelope.payload.remote_name.into(); - let rebase = envelope.payload.rebase; - - let remote_message = repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.pull(branch_name, remote_name, rebase, askpass, cx) - })? - .await??; - - Ok(proto::RemoteMessageResponse { - stdout: remote_message.stdout, - stderr: remote_message.stderr, - }) - } - - async fn handle_stage( - 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 entries = envelope - .payload - .paths - .into_iter() - .map(|path| RepoPath::new(&path)) - .collect::>>()?; - - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.stage_entries(entries, cx) - })? - .await?; - Ok(proto::Ack {}) - } - - async fn handle_unstage( - 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 entries = envelope - .payload - .paths - .into_iter() - .map(|path| RepoPath::new(&path)) - .collect::>>()?; - - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.unstage_entries(entries, cx) - })? - .await?; - - Ok(proto::Ack {}) - } - - async fn handle_stash( - 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 entries = envelope - .payload - .paths - .into_iter() - .map(|path| RepoPath::new(&path)) - .collect::>>()?; - - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.stash_entries(entries, cx) - })? - .await?; - - Ok(proto::Ack {}) - } - - async fn handle_stash_pop( - 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 stash_index = envelope.payload.stash_index.map(|i| i as usize); - - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.stash_pop(stash_index, cx) - })? - .await?; - - Ok(proto::Ack {}) - } - - async fn handle_stash_apply( - 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 stash_index = envelope.payload.stash_index.map(|i| i as usize); - - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.stash_apply(stash_index, cx) - })? - .await?; - - Ok(proto::Ack {}) - } - - async fn handle_stash_drop( - 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 stash_index = envelope.payload.stash_index.map(|i| i as usize); - - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.stash_drop(stash_index, cx) - })? - .await??; - - Ok(proto::Ack {}) - } - - async fn handle_set_index_text( - 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 repo_path = RepoPath::from_proto(&envelope.payload.path)?; - - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.spawn_set_index_text_job( - repo_path, - envelope.payload.text, - None, - cx, - ) - })? - .await??; - Ok(proto::Ack {}) - } - - async fn handle_run_hook( - 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 hook = RunHook::from_proto(envelope.payload.hook).context("invalid hook")?; - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.run_hook(hook, cx) - })? - .await??; - Ok(proto::Ack {}) - } - - async fn handle_commit( - 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 askpass_id = envelope.payload.askpass_id; - - let askpass = make_remote_delegate( - this, - envelope.payload.project_id, - repository_id, - askpass_id, - &mut cx, - ); - - let message = SharedString::from(envelope.payload.message); - let name = envelope.payload.name.map(SharedString::from); - let email = envelope.payload.email.map(SharedString::from); - let options = envelope.payload.options.unwrap_or_default(); - - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.commit( - message, - name.zip(email), - CommitOptions { - amend: options.amend, - signoff: options.signoff, - }, - askpass, - cx, - ) - })? - .await??; - Ok(proto::Ack {}) - } - - async fn handle_get_remotes( - 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_name = envelope.payload.branch_name; - let is_push = envelope.payload.is_push; - - let remotes = repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.get_remotes(branch_name, is_push) - })? - .await??; - - Ok(proto::GetRemotesResponse { - remotes: remotes - .into_iter() - .map(|remotes| proto::get_remotes_response::Remote { - name: remotes.name.to_string(), - }) - .collect::>(), - }) - } - - async fn handle_get_worktrees( - 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 worktrees = repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.worktrees() - })? - .await??; - - Ok(proto::GitWorktreesResponse { - worktrees: worktrees - .into_iter() - .map(|worktree| worktree_to_proto(&worktree)) - .collect::>(), - }) - } - - async fn handle_create_worktree( - 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 directory = PathBuf::from(envelope.payload.directory); - let name = envelope.payload.name; - let commit = envelope.payload.commit; - - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.create_worktree(name, directory, commit) - })? - .await??; - - Ok(proto::Ack {}) - } - - async fn handle_get_branches( - 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 branches = repository_handle - .update(&mut cx, |repository_handle, _| repository_handle.branches())? - .await??; - - Ok(proto::GitBranchesResponse { - branches: branches - .into_iter() - .map(|branch| branch_to_proto(&branch)) - .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, - 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_name = envelope.payload.branch_name; - - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.create_branch(branch_name, None) - })? - .await??; - - Ok(proto::Ack {}) - } - - async fn handle_change_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_name = envelope.payload.branch_name; - - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.change_branch(branch_name) - })? - .await??; - - Ok(proto::Ack {}) - } - - async fn handle_rename_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 = envelope.payload.branch; - let new_name = envelope.payload.new_name; - - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.rename_branch(branch, new_name) - })? - .await??; - - Ok(proto::Ack {}) - } - - async fn handle_create_remote( - 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 remote_name = envelope.payload.remote_name; - let remote_url = envelope.payload.remote_url; - - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.create_remote(remote_name, remote_url) - })? - .await??; - - Ok(proto::Ack {}) - } - - async fn handle_delete_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_name = envelope.payload.branch_name; - - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.delete_branch(branch_name) - })? - .await??; - - Ok(proto::Ack {}) - } - - async fn handle_remove_remote( - 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 remote_name = envelope.payload.remote_name; - - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.remove_remote(remote_name) - })? - .await??; - - Ok(proto::Ack {}) - } - - async fn handle_show( - 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 commit = repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.show(envelope.payload.commit) - })? - .await??; - Ok(proto::GitCommitDetails { - sha: commit.sha.into(), - message: commit.message.into(), - commit_timestamp: commit.commit_timestamp, - author_email: commit.author_email.into(), - author_name: commit.author_name.into(), - }) - } - - async fn handle_load_commit_diff( - 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 commit_diff = repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.load_commit_diff(envelope.payload.commit) - })? - .await??; - Ok(proto::LoadCommitDiffResponse { - files: commit_diff - .files - .into_iter() - .map(|file| proto::CommitFile { - path: file.path.to_proto(), - old_text: file.old_text, - new_text: file.new_text, - }) - .collect(), - }) - } - - async fn handle_file_history( - 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 path = RepoPath::from_proto(&envelope.payload.path)?; - let skip = envelope.payload.skip as usize; - let limit = envelope.payload.limit.map(|l| l as usize); - - let file_history = repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.file_history_paginated(path, skip, limit) - })? - .await??; - - Ok(proto::GitFileHistoryResponse { - entries: file_history - .entries - .into_iter() - .map(|entry| proto::FileHistoryEntry { - sha: entry.sha.to_string(), - subject: entry.subject.to_string(), - message: entry.message.to_string(), - commit_timestamp: entry.commit_timestamp, - author_name: entry.author_name.to_string(), - author_email: entry.author_email.to_string(), - }) - .collect(), - path: file_history.path.to_proto(), - }) - } - - async fn handle_reset( - 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 mode = match envelope.payload.mode() { - git_reset::ResetMode::Soft => ResetMode::Soft, - git_reset::ResetMode::Mixed => ResetMode::Mixed, - }; - - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.reset(envelope.payload.commit, mode, cx) - })? - .await??; - Ok(proto::Ack {}) - } - - async fn handle_checkout_files( - 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 paths = envelope - .payload - .paths - .iter() - .map(|s| RepoPath::from_proto(s)) - .collect::>>()?; - - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.checkout_files(&envelope.payload.commit, paths, cx) - })? - .await?; - Ok(proto::Ack {}) - } - - async fn handle_open_commit_message_buffer( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository = Self::repository_for_request(&this, repository_id, &mut cx)?; - let buffer = repository - .update(&mut cx, |repository, cx| { - repository.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx) - })? - .await?; - - let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id())?; - this.update(&mut cx, |this, cx| { - this.buffer_store.update(cx, |buffer_store, cx| { - buffer_store - .create_buffer_for_peer( - &buffer, - envelope.original_sender_id.unwrap_or(envelope.sender_id), - cx, - ) - .detach_and_log_err(cx); - }) - })?; - - Ok(proto::OpenBufferResponse { - buffer_id: buffer_id.to_proto(), - }) - } - - async fn handle_askpass( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository = Self::repository_for_request(&this, repository_id, &mut cx)?; - - let delegates = cx.update(|cx| repository.read(cx).askpass_delegates.clone())?; - let Some(mut askpass) = delegates.lock().remove(&envelope.payload.askpass_id) else { - debug_panic!("no askpass found"); - anyhow::bail!("no askpass found"); - }; - - let response = askpass - .ask_password(envelope.payload.prompt) - .await - .ok_or_else(|| anyhow::anyhow!("askpass cancelled"))?; - - delegates - .lock() - .insert(envelope.payload.askpass_id, askpass); - - // In fact, we don't quite know what we're doing here, as we're sending askpass password unencrypted, but.. - Ok(proto::AskPassResponse { - response: response.decrypt(IKnowWhatIAmDoingAndIHaveReadTheDocs)?, - }) - } - - async fn handle_check_for_pushed_commits( - 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 branches = repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.check_for_pushed_commits() - })? - .await??; - Ok(proto::CheckForPushedCommitsResponse { - pushed_to: branches - .into_iter() - .map(|commit| commit.to_string()) - .collect(), - }) - } - - async fn handle_git_diff( - 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 diff_type = match envelope.payload.diff_type() { - proto::git_diff::DiffType::HeadToIndex => DiffType::HeadToIndex, - proto::git_diff::DiffType::HeadToWorktree => DiffType::HeadToWorktree, - }; - - let mut diff = repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.diff(diff_type, cx) - })? - .await??; - const ONE_MB: usize = 1_000_000; - if diff.len() > ONE_MB { - diff = diff.chars().take(ONE_MB).collect() - } - - Ok(proto::GitDiffResponse { diff }) - } - - async fn handle_tree_diff( - this: Entity, - request: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId(request.payload.repository_id); - let diff_type = if request.payload.is_merge { - DiffTreeType::MergeBase { - base: request.payload.base.into(), - head: request.payload.head.into(), - } - } else { - DiffTreeType::Since { - base: request.payload.base.into(), - head: request.payload.head.into(), - } - }; - - let diff = this - .update(&mut cx, |this, cx| { - let repository = this.repositories().get(&repository_id)?; - Some(repository.update(cx, |repo, cx| repo.diff_tree(diff_type, cx))) - })? - .context("missing repository")? - .await??; - - Ok(proto::GetTreeDiffResponse { - entries: diff - .entries - .into_iter() - .map(|(path, status)| proto::TreeDiffStatus { - path: path.as_ref().to_proto(), - status: match status { - TreeDiffStatus::Added {} => proto::tree_diff_status::Status::Added.into(), - TreeDiffStatus::Modified { .. } => { - proto::tree_diff_status::Status::Modified.into() - } - TreeDiffStatus::Deleted { .. } => { - proto::tree_diff_status::Status::Deleted.into() - } - }, - oid: match status { - TreeDiffStatus::Deleted { old } | TreeDiffStatus::Modified { old } => { - Some(old.to_string()) - } - TreeDiffStatus::Added => None, - }, - }) - .collect(), - }) - } - - async fn handle_get_blob_content( - this: Entity, - request: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let oid = git::Oid::from_str(&request.payload.oid)?; - let repository_id = RepositoryId(request.payload.repository_id); - let content = this - .update(&mut cx, |this, cx| { - let repository = this.repositories().get(&repository_id)?; - Some(repository.update(cx, |repo, cx| repo.load_blob_content(oid, cx))) - })? - .context("missing repository")? - .await?; - Ok(proto::GetBlobContentResponse { content }) - } - - async fn handle_open_unstaged_diff( - this: Entity, - request: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let buffer_id = BufferId::new(request.payload.buffer_id)?; - let diff = this - .update(&mut cx, |this, cx| { - let buffer = this.buffer_store.read(cx).get(buffer_id)?; - Some(this.open_unstaged_diff(buffer, cx)) - })? - .context("missing buffer")? - .await?; - this.update(&mut cx, |this, _| { - let shared_diffs = this - .shared_diffs - .entry(request.original_sender_id.unwrap_or(request.sender_id)) - .or_default(); - shared_diffs.entry(buffer_id).or_default().unstaged = Some(diff.clone()); - })?; - let staged_text = diff.read_with(&cx, |diff, _| diff.base_text_string())?; - Ok(proto::OpenUnstagedDiffResponse { staged_text }) - } - - async fn handle_open_uncommitted_diff( - this: Entity, - request: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let buffer_id = BufferId::new(request.payload.buffer_id)?; - let diff = this - .update(&mut cx, |this, cx| { - let buffer = this.buffer_store.read(cx).get(buffer_id)?; - Some(this.open_uncommitted_diff(buffer, cx)) - })? - .context("missing buffer")? - .await?; - this.update(&mut cx, |this, _| { - let shared_diffs = this - .shared_diffs - .entry(request.original_sender_id.unwrap_or(request.sender_id)) - .or_default(); - shared_diffs.entry(buffer_id).or_default().uncommitted = Some(diff.clone()); - })?; - diff.read_with(&cx, |diff, cx| { - use proto::open_uncommitted_diff_response::Mode; - - let unstaged_diff = diff.secondary_diff(); - let index_snapshot = unstaged_diff.and_then(|diff| { - let diff = diff.read(cx); - diff.base_text_exists().then(|| diff.base_text()) - }); - - let mode; - let staged_text; - let committed_text; - if diff.base_text_exists() { - let committed_snapshot = diff.base_text(); - committed_text = Some(committed_snapshot.text()); - if let Some(index_text) = index_snapshot { - if index_text.remote_id() == committed_snapshot.remote_id() { - mode = Mode::IndexMatchesHead; - staged_text = None; - } else { - mode = Mode::IndexAndHead; - staged_text = Some(index_text.text()); - } - } else { - mode = Mode::IndexAndHead; - staged_text = None; - } - } else { - mode = Mode::IndexAndHead; - committed_text = None; - staged_text = index_snapshot.as_ref().map(|buffer| buffer.text()); - } - - proto::OpenUncommittedDiffResponse { - committed_text, - staged_text, - mode: mode.into(), - } - }) - } - - async fn handle_update_diff_bases( - this: Entity, - request: TypedEnvelope, - mut cx: AsyncApp, - ) -> 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); - }) - } - }) - } - - async fn handle_blame_buffer( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let version = deserialize_version(&envelope.payload.version); - let buffer = this.read_with(&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 blame = this - .update(&mut cx, |this, cx| { - this.blame_buffer(&buffer, Some(version), cx) - })? - .await?; - Ok(serialize_blame_buffer_response(blame)) - } - - async fn handle_get_permalink_to_line( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - // let version = deserialize_version(&envelope.payload.version); - let selection = { - let proto_selection = envelope - .payload - .selection - .context("no selection to get permalink for defined")?; - proto_selection.start as u32..proto_selection.end as u32 - }; - let buffer = this.read_with(&cx, |this, cx| { - this.buffer_store.read(cx).get_existing(buffer_id) - })??; - let permalink = this - .update(&mut cx, |this, cx| { - this.get_permalink_to_line(&buffer, selection, cx) - })? - .await?; - Ok(proto::GetPermalinkToLineResponse { - permalink: permalink.to_string(), - }) - } - - fn repository_for_request( - this: &Entity, - id: RepositoryId, - cx: &mut AsyncApp, - ) -> Result> { - this.read_with(cx, |this, _| { - this.repositories - .get(&id) - .context("missing repository handle") - .cloned() - })? - } - - pub fn repo_snapshots(&self, cx: &App) -> HashMap { - self.repositories - .iter() - .map(|(id, repo)| (*id, repo.read(cx).snapshot.clone())) - .collect() - } - - fn process_updated_entries( - &self, - worktree: &Entity, - updated_entries: &[(Arc, ProjectEntryId, PathChange)], - cx: &mut App, - ) -> Task, Vec>> { - let path_style = worktree.read(cx).path_style(); - let mut repo_paths = self - .repositories - .values() - .map(|repo| (repo.read(cx).work_directory_abs_path.clone(), repo.clone())) - .collect::>(); - let mut entries: Vec<_> = updated_entries - .iter() - .map(|(path, _, _)| path.clone()) - .collect(); - entries.sort(); - let worktree = worktree.read(cx); - - let entries = entries - .into_iter() - .map(|path| worktree.absolutize(&path)) - .collect::>(); - - let executor = cx.background_executor().clone(); - cx.background_executor().spawn(async move { - repo_paths.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0)); - let mut paths_by_git_repo = HashMap::<_, Vec<_>>::default(); - let mut tasks = FuturesOrdered::new(); - for (repo_path, repo) in repo_paths.into_iter().rev() { - let entries = entries.clone(); - let task = executor.spawn(async move { - // Find all repository paths that belong to this repo - let mut ix = entries.partition_point(|path| path < &*repo_path); - if ix == entries.len() { - return None; - }; - - let mut paths = Vec::new(); - // 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, path_style, - ) - { - paths.push((repo_path, ix)); - ix += 1; - } - if paths.is_empty() { - None - } else { - Some((repo, paths)) - } - }); - tasks.push_back(task); - } - - // Now, let's filter out the "duplicate" entries that were processed by multiple distinct repos. - let mut path_was_used = vec![false; entries.len()]; - let tasks = tasks.collect::>().await; - // Process tasks from the back: iterating backwards allows us to see more-specific paths first. - // We always want to assign a path to it's innermost repository. - for t in tasks { - let Some((repo, paths)) = t else { - continue; - }; - let entry = paths_by_git_repo.entry(repo).or_default(); - for (repo_path, ix) in paths { - if path_was_used[ix] { - continue; - } - path_was_used[ix] = true; - entry.push(repo_path); - } - } - - paths_by_git_repo - }) - } -} - -impl BufferGitState { - fn new(_git_store: WeakEntity) -> Self { - Self { - unstaged_diff: Default::default(), - uncommitted_diff: Default::default(), - recalculate_diff_task: Default::default(), - language: Default::default(), - language_registry: Default::default(), - recalculating_tx: postage::watch::channel_with(false).0, - hunk_staging_operation_count: 0, - hunk_staging_operation_count_as_of_write: 0, - head_text: Default::default(), - index_text: Default::default(), - head_changed: Default::default(), - index_changed: Default::default(), - language_changed: Default::default(), - conflict_updated_futures: Default::default(), - conflict_set: Default::default(), - reparse_conflict_markers_task: Default::default(), - } - } - - fn buffer_language_changed(&mut self, buffer: Entity, cx: &mut Context) { - self.language = buffer.read(cx).language().cloned(); - self.language_changed = true; - let _ = self.recalculate_diffs(buffer.read(cx).text_snapshot(), cx); - } - - fn reparse_conflict_markers( - &mut self, - buffer: text::BufferSnapshot, - cx: &mut Context, - ) -> oneshot::Receiver<()> { - let (tx, rx) = oneshot::channel(); - - let Some(conflict_set) = self - .conflict_set - .as_ref() - .and_then(|conflict_set| conflict_set.upgrade()) - else { - return rx; - }; - - let old_snapshot = conflict_set.read_with(cx, |conflict_set, _| { - if conflict_set.has_conflict { - Some(conflict_set.snapshot()) - } else { - None - } - }); - - if let Some(old_snapshot) = old_snapshot { - self.conflict_updated_futures.push(tx); - self.reparse_conflict_markers_task = Some(cx.spawn(async move |this, cx| { - let (snapshot, changed_range) = cx - .background_spawn(async move { - let new_snapshot = ConflictSet::parse(&buffer); - let changed_range = old_snapshot.compare(&new_snapshot, &buffer); - (new_snapshot, changed_range) - }) - .await; - this.update(cx, |this, cx| { - if let Some(conflict_set) = &this.conflict_set { - conflict_set - .update(cx, |conflict_set, cx| { - conflict_set.set_snapshot(snapshot, changed_range, cx); - }) - .ok(); - } - let futures = std::mem::take(&mut this.conflict_updated_futures); - for tx in futures { - tx.send(()).ok(); - } - }) - })) - } - - rx - } - - fn unstaged_diff(&self) -> Option> { - self.unstaged_diff.as_ref().and_then(|set| set.upgrade()) - } - - fn uncommitted_diff(&self) -> Option> { - self.uncommitted_diff.as_ref().and_then(|set| set.upgrade()) - } - - fn handle_base_texts_updated( - &mut self, - buffer: text::BufferSnapshot, - message: proto::UpdateDiffBases, - cx: &mut Context, - ) { - use proto::update_diff_bases::Mode; - - let Some(mode) = Mode::from_i32(message.mode) else { - return; - }; - - let diff_bases_change = match mode { - Mode::HeadOnly => DiffBasesChange::SetHead(message.committed_text), - Mode::IndexOnly => DiffBasesChange::SetIndex(message.staged_text), - Mode::IndexMatchesHead => DiffBasesChange::SetBoth(message.committed_text), - Mode::IndexAndHead => DiffBasesChange::SetEach { - index: message.staged_text, - head: message.committed_text, - }, - }; - - self.diff_bases_changed(buffer, Some(diff_bases_change), cx); - } - - pub fn wait_for_recalculation(&mut self) -> Option + use<>> { - if *self.recalculating_tx.borrow() { - let mut rx = self.recalculating_tx.subscribe(); - Some(async move { - loop { - let is_recalculating = rx.recv().await; - if is_recalculating != Some(true) { - break; - } - } - }) - } else { - None - } - } - - fn diff_bases_changed( - &mut self, - buffer: text::BufferSnapshot, - diff_bases_change: Option, - cx: &mut Context, - ) { - match diff_bases_change { - Some(DiffBasesChange::SetIndex(index)) => { - self.index_text = index.map(|mut index| { - text::LineEnding::normalize(&mut index); - Arc::new(index) - }); - self.index_changed = true; - } - Some(DiffBasesChange::SetHead(head)) => { - self.head_text = head.map(|mut head| { - text::LineEnding::normalize(&mut head); - Arc::new(head) - }); - self.head_changed = true; - } - Some(DiffBasesChange::SetBoth(text)) => { - let text = text.map(|mut text| { - text::LineEnding::normalize(&mut text); - Arc::new(text) - }); - self.head_text = text.clone(); - self.index_text = text; - self.head_changed = true; - self.index_changed = true; - } - Some(DiffBasesChange::SetEach { index, head }) => { - self.index_text = index.map(|mut index| { - text::LineEnding::normalize(&mut index); - Arc::new(index) - }); - self.index_changed = true; - self.head_text = head.map(|mut head| { - text::LineEnding::normalize(&mut head); - Arc::new(head) - }); - self.head_changed = true; - } - None => {} - } - - self.recalculate_diffs(buffer, cx) - } - - fn recalculate_diffs(&mut self, buffer: text::BufferSnapshot, cx: &mut Context) { - *self.recalculating_tx.borrow_mut() = true; - - let language = self.language.clone(); - let language_registry = self.language_registry.clone(); - let unstaged_diff = self.unstaged_diff(); - let uncommitted_diff = self.uncommitted_diff(); - let head = self.head_text.clone(); - let index = self.index_text.clone(); - let index_changed = self.index_changed; - let head_changed = self.head_changed; - let language_changed = self.language_changed; - let prev_hunk_staging_operation_count = self.hunk_staging_operation_count_as_of_write; - let index_matches_head = match (self.index_text.as_ref(), self.head_text.as_ref()) { - (Some(index), Some(head)) => Arc::ptr_eq(index, head), - (None, None) => true, - _ => false, - }; - self.recalculate_diff_task = Some(cx.spawn(async move |this, cx| { - log::debug!( - "start recalculating diffs for buffer {}", - buffer.remote_id() - ); - - let mut new_unstaged_diff = None; - if let Some(unstaged_diff) = &unstaged_diff { - new_unstaged_diff = Some( - BufferDiff::update_diff( - unstaged_diff.clone(), - buffer.clone(), - index, - index_changed, - language_changed, - language.clone(), - language_registry.clone(), - cx, - ) - .await?, - ); - } - - // Dropping BufferDiff can be expensive, so yield back to the event loop - // for a bit - yield_now().await; - - let mut new_uncommitted_diff = None; - if let Some(uncommitted_diff) = &uncommitted_diff { - new_uncommitted_diff = if index_matches_head { - new_unstaged_diff.clone() - } else { - Some( - BufferDiff::update_diff( - uncommitted_diff.clone(), - buffer.clone(), - head, - head_changed, - language_changed, - language.clone(), - language_registry.clone(), - cx, - ) - .await?, - ) - } - } - - // Dropping BufferDiff can be expensive, so yield back to the event loop - // for a bit - yield_now().await; - - let cancel = this.update(cx, |this, _| { - // This checks whether all pending stage/unstage operations - // have quiesced (i.e. both the corresponding write and the - // read of that write have completed). If not, then we cancel - // this recalculation attempt to avoid invalidating pending - // state too quickly; another recalculation will come along - // later and clear the pending state once the state of the index has settled. - if this.hunk_staging_operation_count > prev_hunk_staging_operation_count { - *this.recalculating_tx.borrow_mut() = false; - true - } else { - false - } - })?; - if cancel { - log::debug!( - concat!( - "aborting recalculating diffs for buffer {}", - "due to subsequent hunk operations", - ), - buffer.remote_id() - ); - return Ok(()); - } - - let unstaged_changed_range = if let Some((unstaged_diff, new_unstaged_diff)) = - unstaged_diff.as_ref().zip(new_unstaged_diff.clone()) - { - unstaged_diff.update(cx, |diff, cx| { - if language_changed { - diff.language_changed(cx); - } - diff.set_snapshot(new_unstaged_diff, &buffer, cx) - })? - } else { - None - }; - - yield_now().await; - - if let Some((uncommitted_diff, new_uncommitted_diff)) = - uncommitted_diff.as_ref().zip(new_uncommitted_diff.clone()) - { - uncommitted_diff.update(cx, |diff, cx| { - if language_changed { - diff.language_changed(cx); - } - diff.set_snapshot_with_secondary( - new_uncommitted_diff, - &buffer, - unstaged_changed_range, - true, - cx, - ); - })?; - } - - log::debug!( - "finished recalculating diffs for buffer {}", - buffer.remote_id() - ); - - if let Some(this) = this.upgrade() { - this.update(cx, |this, _| { - this.index_changed = false; - this.head_changed = false; - this.language_changed = false; - *this.recalculating_tx.borrow_mut() = false; - })?; - } - - Ok(()) - })); - } -} - -fn make_remote_delegate( - this: Entity, - project_id: u64, - repository_id: RepositoryId, - askpass_id: u64, - cx: &mut AsyncApp, -) -> AskPassDelegate { - AskPassDelegate::new(cx, move |prompt, tx, cx| { - this.update(cx, |this, cx| { - let Some((client, _)) = this.downstream_client() else { - return; - }; - let response = client.request(proto::AskPassRequest { - project_id, - repository_id: repository_id.to_proto(), - askpass_id, - prompt, - }); - cx.spawn(async move |_, _| { - let mut response = response.await?.response; - tx.send(EncryptedPassword::try_from(response.as_ref())?) - .ok(); - response.zeroize(); - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - }) - .log_err(); - }) -} - -impl RepositoryId { - pub fn to_proto(self) -> u64 { - self.0 - } - - pub fn from_proto(id: u64) -> Self { - RepositoryId(id) - } -} - -impl RepositorySnapshot { - fn empty(id: RepositoryId, work_directory_abs_path: Arc, path_style: PathStyle) -> Self { - Self { - id, - statuses_by_path: Default::default(), - work_directory_abs_path, - branch: None, - head_commit: None, - scan_id: 0, - merge: Default::default(), - remote_origin_url: None, - remote_upstream_url: None, - stash_entries: Default::default(), - path_style, - } - } - - fn initial_update(&self, project_id: u64) -> proto::UpdateRepository { - proto::UpdateRepository { - branch_summary: self.branch.as_ref().map(branch_to_proto), - head_commit_details: self.head_commit.as_ref().map(commit_details_to_proto), - updated_statuses: self - .statuses_by_path - .iter() - .map(|entry| entry.to_proto()) - .collect(), - removed_statuses: Default::default(), - current_merge_conflicts: self - .merge - .conflicted_paths - .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_string_lossy().into_owned(), - entry_ids: vec![self.id.to_proto()], - scan_id: self.scan_id, - is_last_update: true, - stash_entries: self - .stash_entries - .entries - .iter() - .map(stash_to_proto) - .collect(), - remote_upstream_url: self.remote_upstream_url.clone(), - remote_origin_url: self.remote_origin_url.clone(), - } - } - - fn build_update(&self, old: &Self, project_id: u64) -> proto::UpdateRepository { - let mut updated_statuses: Vec = Vec::new(); - let mut removed_statuses: Vec = Vec::new(); - - let mut new_statuses = self.statuses_by_path.iter().peekable(); - let mut old_statuses = old.statuses_by_path.iter().peekable(); - - let mut current_new_entry = new_statuses.next(); - let mut current_old_entry = old_statuses.next(); - loop { - match (current_new_entry, current_old_entry) { - (Some(new_entry), Some(old_entry)) => { - match new_entry.repo_path.cmp(&old_entry.repo_path) { - Ordering::Less => { - updated_statuses.push(new_entry.to_proto()); - current_new_entry = new_statuses.next(); - } - Ordering::Equal => { - if new_entry.status != old_entry.status { - updated_statuses.push(new_entry.to_proto()); - } - current_old_entry = old_statuses.next(); - current_new_entry = new_statuses.next(); - } - Ordering::Greater => { - removed_statuses.push(old_entry.repo_path.to_proto()); - current_old_entry = old_statuses.next(); - } - } - } - (None, Some(old_entry)) => { - removed_statuses.push(old_entry.repo_path.to_proto()); - current_old_entry = old_statuses.next(); - } - (Some(new_entry), None) => { - updated_statuses.push(new_entry.to_proto()); - current_new_entry = new_statuses.next(); - } - (None, None) => break, - } - } - - proto::UpdateRepository { - branch_summary: self.branch.as_ref().map(branch_to_proto), - head_commit_details: self.head_commit.as_ref().map(commit_details_to_proto), - updated_statuses, - removed_statuses, - current_merge_conflicts: self - .merge - .conflicted_paths - .iter() - .map(|path| 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_string_lossy().into_owned(), - entry_ids: vec![], - scan_id: self.scan_id, - is_last_update: true, - stash_entries: self - .stash_entries - .entries - .iter() - .map(stash_to_proto) - .collect(), - remote_upstream_url: self.remote_upstream_url.clone(), - remote_origin_url: self.remote_origin_url.clone(), - } - } - - pub fn status(&self) -> impl Iterator + '_ { - self.statuses_by_path.iter().cloned() - } - - pub fn status_summary(&self) -> GitSummary { - self.statuses_by_path.summary().item_summary - } - - pub fn status_for_path(&self, path: &RepoPath) -> Option { - self.statuses_by_path - .get(&PathKey(path.as_ref().clone()), ()) - .cloned() - } - - pub fn abs_path_to_repo_path(&self, abs_path: &Path) -> Option { - Self::abs_path_to_repo_path_inner(&self.work_directory_abs_path, abs_path, self.path_style) - } - - fn repo_path_to_abs_path(&self, repo_path: &RepoPath) -> PathBuf { - self.path_style - .join(&self.work_directory_abs_path, repo_path.as_std_path()) - .unwrap() - .into() - } - - #[inline] - fn abs_path_to_repo_path_inner( - work_directory_abs_path: &Path, - abs_path: &Path, - path_style: PathStyle, - ) -> Option { - let rel_path = path_style.strip_prefix(abs_path, work_directory_abs_path)?; - Some(RepoPath::from_rel_path(&rel_path)) - } - - pub fn had_conflict_on_last_merge_head_change(&self, repo_path: &RepoPath) -> bool { - 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); - let has_conflict_currently = self - .status_for_path(repo_path) - .is_some_and(|entry| entry.status.is_conflicted()); - had_conflict_on_last_merge_head_change || has_conflict_currently - } - - /// This is the name that will be displayed in the repository selector for this repository. - pub fn display_name(&self) -> SharedString { - self.work_directory_abs_path - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string() - .into() - } -} - -pub fn stash_to_proto(entry: &StashEntry) -> proto::StashEntry { - proto::StashEntry { - oid: entry.oid.as_bytes().to_vec(), - message: entry.message.clone(), - branch: entry.branch.clone(), - index: entry.index as u64, - timestamp: entry.timestamp, - } -} - -pub fn proto_to_stash(entry: &proto::StashEntry) -> Result { - Ok(StashEntry { - oid: Oid::from_bytes(&entry.oid)?, - message: entry.message.clone(), - index: entry.index as usize, - branch: entry.branch.clone(), - timestamp: entry.timestamp, - }) -} - -impl MergeDetails { - async fn load( - backend: &Arc, - status: &SumTree, - prev_snapshot: &RepositorySnapshot, - ) -> Result<(MergeDetails, bool)> { - log::debug!("load merge details"); - let message = backend.merge_message().await; - let heads = backend - .revparse_batch(vec![ - "MERGE_HEAD".into(), - "CHERRY_PICK_HEAD".into(), - "REBASE_HEAD".into(), - "REVERT_HEAD".into(), - "APPLY_HEAD".into(), - ]) - .await - .log_err() - .unwrap_or_default() - .into_iter() - .map(|opt| opt.map(SharedString::from)) - .collect::>(); - let merge_heads_changed = heads != prev_snapshot.merge.heads; - let conflicted_paths = if merge_heads_changed { - let current_conflicted_paths = TreeSet::from_ordered_entries( - status - .iter() - .filter(|entry| entry.status.is_conflicted()) - .map(|entry| entry.repo_path.clone()), - ); - - // It can happen that we run a scan while a lengthy merge is in progress - // that will eventually result in conflicts, but before those conflicts - // are reported by `git status`. Since for the moment we only care about - // the merge heads state for the purposes of tracking conflicts, don't update - // this state until we see some conflicts. - if heads.iter().any(Option::is_some) - && !prev_snapshot.merge.heads.iter().any(Option::is_some) - && current_conflicted_paths.is_empty() - { - log::debug!("not updating merge heads because no conflicts found"); - return Ok(( - MergeDetails { - message: message.map(SharedString::from), - ..prev_snapshot.merge.clone() - }, - false, - )); - } - - current_conflicted_paths - } else { - prev_snapshot.merge.conflicted_paths.clone() - }; - let details = MergeDetails { - conflicted_paths, - message: message.map(SharedString::from), - heads, - }; - Ok((details, merge_heads_changed)) - } -} - -impl Repository { - pub fn snapshot(&self) -> RepositorySnapshot { - self.snapshot.clone() - } - - pub fn pending_ops(&self) -> impl Iterator + '_ { - self.pending_ops.iter().cloned() - } - - pub fn pending_ops_summary(&self) -> PathSummary { - self.pending_ops.summary().clone() - } - - pub fn pending_ops_for_path(&self, path: &RepoPath) -> Option { - self.pending_ops - .get(&PathKey(path.as_ref().clone()), ()) - .cloned() - } - - fn local( - id: RepositoryId, - work_directory_abs_path: Arc, - dot_git_abs_path: Arc, - project_environment: WeakEntity, - fs: Arc, - git_store: WeakEntity, - cx: &mut Context, - ) -> Self { - let snapshot = - RepositorySnapshot::empty(id, work_directory_abs_path.clone(), PathStyle::local()); - let state = cx - .spawn(async move |_, cx| { - LocalRepositoryState::new( - work_directory_abs_path, - dot_git_abs_path, - project_environment, - fs, - cx, - ) - .await - .map_err(|err| err.to_string()) - }) - .shared(); - let job_sender = Repository::spawn_local_git_worker(state.clone(), cx); - let state = cx - .spawn(async move |_, _| { - let state = state.await?; - Ok(RepositoryState::Local(state)) - }) - .shared(); - - Repository { - this: cx.weak_entity(), - git_store, - snapshot, - pending_ops: Default::default(), - repository_state: state, - commit_message_buffer: None, - askpass_delegates: Default::default(), - paths_needing_status_update: Default::default(), - latest_askpass_id: 0, - job_sender, - job_id: 0, - active_jobs: Default::default(), - } - } - - fn remote( - id: RepositoryId, - work_directory_abs_path: Arc, - path_style: PathStyle, - project_id: ProjectId, - client: AnyProtoClient, - git_store: WeakEntity, - cx: &mut Context, - ) -> Self { - let snapshot = RepositorySnapshot::empty(id, work_directory_abs_path, path_style); - let repository_state = RemoteRepositoryState { project_id, client }; - let job_sender = Self::spawn_remote_git_worker(repository_state.clone(), cx); - let repository_state = Task::ready(Ok(RepositoryState::Remote(repository_state))).shared(); - Self { - this: cx.weak_entity(), - snapshot, - commit_message_buffer: None, - git_store, - pending_ops: Default::default(), - paths_needing_status_update: Default::default(), - job_sender, - repository_state, - askpass_delegates: Default::default(), - latest_askpass_id: 0, - active_jobs: Default::default(), - job_id: 0, - } - } - - pub fn git_store(&self) -> Option> { - self.git_store.upgrade() - } - - fn reload_buffer_diff_bases(&mut self, cx: &mut Context) { - let this = cx.weak_entity(); - let git_store = self.git_store.clone(); - let _ = self.send_keyed_job( - Some(GitJobKey::ReloadBufferDiffBases), - None, - |state, mut cx| async move { - let RepositoryState::Local(LocalRepositoryState { backend, .. }) = state else { - log::error!("tried to recompute diffs for a non-local repository"); - return Ok(()); - }; - - let Some(this) = this.upgrade() else { - return Ok(()); - }; - - let repo_diff_state_updates = this.update(&mut cx, |this, cx| { - git_store.update(cx, |git_store, cx| { - git_store - .diffs - .iter() - .filter_map(|(buffer_id, diff_state)| { - let buffer_store = git_store.buffer_store.read(cx); - let buffer = buffer_store.get(*buffer_id)?; - let file = File::from_dyn(buffer.read(cx).file())?; - let abs_path = file.worktree.read(cx).absolutize(&file.path); - let repo_path = this.abs_path_to_repo_path(&abs_path)?; - log::debug!( - "start reload diff bases for repo path {}", - repo_path.as_unix_str() - ); - diff_state.update(cx, |diff_state, _| { - let has_unstaged_diff = diff_state - .unstaged_diff - .as_ref() - .is_some_and(|diff| diff.is_upgradable()); - let has_uncommitted_diff = diff_state - .uncommitted_diff - .as_ref() - .is_some_and(|set| set.is_upgradable()); - - Some(( - buffer, - repo_path, - has_unstaged_diff.then(|| diff_state.index_text.clone()), - has_uncommitted_diff.then(|| diff_state.head_text.clone()), - )) - }) - }) - .collect::>() - }) - })??; - - let buffer_diff_base_changes = cx - .background_spawn(async move { - let mut changes = Vec::new(); - for (buffer, repo_path, current_index_text, current_head_text) in - &repo_diff_state_updates - { - let index_text = if current_index_text.is_some() { - backend.load_index_text(repo_path.clone()).await - } else { - None - }; - let head_text = if current_head_text.is_some() { - backend.load_committed_text(repo_path.clone()).await - } else { - None - }; - - let change = - match (current_index_text.as_ref(), current_head_text.as_ref()) { - (Some(current_index), Some(current_head)) => { - let index_changed = - index_text.as_ref() != current_index.as_deref(); - let head_changed = - head_text.as_ref() != current_head.as_deref(); - if index_changed && head_changed { - if index_text == head_text { - Some(DiffBasesChange::SetBoth(head_text)) - } else { - Some(DiffBasesChange::SetEach { - index: index_text, - head: head_text, - }) - } - } else if index_changed { - Some(DiffBasesChange::SetIndex(index_text)) - } else if head_changed { - Some(DiffBasesChange::SetHead(head_text)) - } else { - None - } - } - (Some(current_index), None) => { - let index_changed = - index_text.as_ref() != current_index.as_deref(); - index_changed - .then_some(DiffBasesChange::SetIndex(index_text)) - } - (None, Some(current_head)) => { - let head_changed = - head_text.as_ref() != current_head.as_deref(); - head_changed.then_some(DiffBasesChange::SetHead(head_text)) - } - (None, None) => None, - }; - - changes.push((buffer.clone(), change)) - } - changes - }) - .await; - - git_store.update(&mut cx, |git_store, cx| { - for (buffer, diff_bases_change) in buffer_diff_base_changes { - let buffer_snapshot = buffer.read(cx).text_snapshot(); - let buffer_id = buffer_snapshot.remote_id(); - let Some(diff_state) = git_store.diffs.get(&buffer_id) else { - continue; - }; - - let downstream_client = git_store.downstream_client(); - diff_state.update(cx, |diff_state, cx| { - use proto::update_diff_bases::Mode; - - if let Some((diff_bases_change, (client, project_id))) = - diff_bases_change.clone().zip(downstream_client) - { - let (staged_text, committed_text, mode) = match diff_bases_change { - DiffBasesChange::SetIndex(index) => { - (index, None, Mode::IndexOnly) - } - DiffBasesChange::SetHead(head) => (None, head, Mode::HeadOnly), - DiffBasesChange::SetEach { index, head } => { - (index, head, Mode::IndexAndHead) - } - DiffBasesChange::SetBoth(text) => { - (None, text, Mode::IndexMatchesHead) - } - }; - client - .send(proto::UpdateDiffBases { - project_id: project_id.to_proto(), - buffer_id: buffer_id.to_proto(), - staged_text, - committed_text, - mode: mode as i32, - }) - .log_err(); - } - - diff_state.diff_bases_changed(buffer_snapshot, diff_bases_change, cx); - }); - } - }) - }, - ); - } - - pub fn send_job( - &mut self, - status: Option, - job: F, - ) -> oneshot::Receiver - where - F: FnOnce(RepositoryState, AsyncApp) -> Fut + 'static, - Fut: Future + 'static, - R: Send + 'static, - { - self.send_keyed_job(None, status, job) - } - - fn send_keyed_job( - &mut self, - key: Option, - status: Option, - job: F, - ) -> oneshot::Receiver - where - F: FnOnce(RepositoryState, AsyncApp) -> Fut + 'static, - Fut: Future + 'static, - R: Send + 'static, - { - let (result_tx, result_rx) = futures::channel::oneshot::channel(); - let job_id = post_inc(&mut self.job_id); - let this = self.this.clone(); - self.job_sender - .unbounded_send(GitJob { - key, - job: Box::new(move |state, cx: &mut AsyncApp| { - let job = job(state, cx.clone()); - cx.spawn(async move |cx| { - if let Some(s) = status.clone() { - this.update(cx, |this, cx| { - this.active_jobs.insert( - job_id, - JobInfo { - start: Instant::now(), - message: s.clone(), - }, - ); - - cx.notify(); - }) - .ok(); - } - let result = job.await; - - this.update(cx, |this, cx| { - this.active_jobs.remove(&job_id); - cx.notify(); - }) - .ok(); - - result_tx.send(result).ok(); - }) - }), - }) - .ok(); - result_rx - } - - pub fn set_as_active_repository(&self, cx: &mut Context) { - let Some(git_store) = self.git_store.upgrade() else { - return; - }; - let entity = cx.entity(); - git_store.update(cx, |git_store, cx| { - let Some((&id, _)) = git_store - .repositories - .iter() - .find(|(_, handle)| *handle == &entity) - else { - return; - }; - git_store.active_repo_id = Some(id); - cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id))); - }); - } - - pub fn cached_status(&self) -> impl '_ + Iterator { - self.snapshot.status() - } - - pub fn cached_stash(&self) -> GitStash { - self.snapshot.stash_entries.clone() - } - - pub fn repo_path_to_project_path(&self, path: &RepoPath, cx: &App) -> Option { - let git_store = self.git_store.upgrade()?; - let worktree_store = git_store.read(cx).worktree_store.read(cx); - let abs_path = self.snapshot.repo_path_to_abs_path(path); - let abs_path = SanitizedPath::new(&abs_path); - let (worktree, relative_path) = worktree_store.find_worktree(abs_path, cx)?; - Some(ProjectPath { - worktree_id: worktree.read(cx).id(), - path: relative_path, - }) - } - - pub fn project_path_to_repo_path(&self, path: &ProjectPath, cx: &App) -> Option { - let git_store = self.git_store.upgrade()?; - let worktree_store = git_store.read(cx).worktree_store.read(cx); - let abs_path = worktree_store.absolutize(path, cx)?; - self.snapshot.abs_path_to_repo_path(&abs_path) - } - - pub fn contains_sub_repo(&self, other: &Entity, cx: &App) -> bool { - other - .read(cx) - .snapshot - .work_directory_abs_path - .starts_with(&self.snapshot.work_directory_abs_path) - } - - pub fn open_commit_buffer( - &mut self, - languages: Option>, - buffer_store: Entity, - cx: &mut Context, - ) -> Task>> { - let id = self.id; - if let Some(buffer) = self.commit_message_buffer.clone() { - return Task::ready(Ok(buffer)); - } - let this = cx.weak_entity(); - - let rx = self.send_job(None, move |state, mut cx| async move { - let Some(this) = this.upgrade() else { - bail!("git store was dropped"); - }; - match state { - RepositoryState::Local(..) => { - this.update(&mut cx, |_, cx| { - Self::open_local_commit_buffer(languages, buffer_store, cx) - })? - .await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let request = client.request(proto::OpenCommitMessageBuffer { - project_id: project_id.0, - repository_id: id.to_proto(), - }); - let response = request.await.context("requesting to open commit buffer")?; - let buffer_id = BufferId::new(response.buffer_id)?; - let buffer = buffer_store - .update(&mut cx, |buffer_store, cx| { - buffer_store.wait_for_remote_buffer(buffer_id, cx) - })? - .await?; - if let Some(language_registry) = languages { - let git_commit_language = - language_registry.language_for_name("Git Commit").await?; - buffer.update(&mut cx, |buffer, cx| { - buffer.set_language(Some(git_commit_language), cx); - })?; - } - this.update(&mut cx, |this, _| { - this.commit_message_buffer = Some(buffer.clone()); - })?; - Ok(buffer) - } - } - }); - - cx.spawn(|_, _: &mut AsyncApp| async move { rx.await? }) - } - - fn open_local_commit_buffer( - language_registry: Option>, - buffer_store: Entity, - cx: &mut Context, - ) -> Task>> { - cx.spawn(async move |repository, cx| { - let buffer = buffer_store - .update(cx, |buffer_store, cx| buffer_store.create_buffer(false, cx))? - .await?; - - if let Some(language_registry) = language_registry { - let git_commit_language = language_registry.language_for_name("Git Commit").await?; - buffer.update(cx, |buffer, cx| { - buffer.set_language(Some(git_commit_language), cx); - })?; - } - - repository.update(cx, |repository, _| { - repository.commit_message_buffer = Some(buffer.clone()); - })?; - Ok(buffer) - }) - } - - pub fn checkout_files( - &mut self, - commit: &str, - paths: Vec, - cx: &mut Context, - ) -> Task> { - let commit = commit.to_string(); - let id = self.id; - - self.spawn_job_with_tracking( - paths.clone(), - pending_op::GitStatus::Reverted, - cx, - async move |this, cx| { - this.update(cx, |this, _cx| { - this.send_job( - Some(format!("git checkout {}", commit).into()), - move |git_repo, _| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => { - backend - .checkout_files(commit, paths, environment.clone()) - .await - } - RepositoryState::Remote(RemoteRepositoryState { - project_id, - client, - }) => { - client - .request(proto::GitCheckoutFiles { - project_id: project_id.0, - repository_id: id.to_proto(), - commit, - paths: paths - .into_iter() - .map(|p| p.to_proto()) - .collect(), - }) - .await?; - - Ok(()) - } - } - }, - ) - })? - .await? - }, - ) - } - - pub fn reset( - &mut self, - commit: String, - reset_mode: ResetMode, - _cx: &mut App, - ) -> oneshot::Receiver> { - let id = self.id; - - self.send_job(None, move |git_repo, _| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => backend.reset(commit, reset_mode, environment).await, - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::GitReset { - project_id: project_id.0, - repository_id: id.to_proto(), - commit, - mode: match reset_mode { - ResetMode::Soft => git_reset::ResetMode::Soft.into(), - ResetMode::Mixed => git_reset::ResetMode::Mixed.into(), - }, - }) - .await?; - - Ok(()) - } - } - }) - } - - pub fn show(&mut self, commit: String) -> oneshot::Receiver> { - let id = self.id; - self.send_job(None, move |git_repo, _cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.show(commit).await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let resp = client - .request(proto::GitShow { - project_id: project_id.0, - repository_id: id.to_proto(), - commit, - }) - .await?; - - Ok(CommitDetails { - sha: resp.sha.into(), - message: resp.message.into(), - commit_timestamp: resp.commit_timestamp, - author_email: resp.author_email.into(), - author_name: resp.author_name.into(), - }) - } - } - }) - } - - pub fn load_commit_diff(&mut self, commit: String) -> oneshot::Receiver> { - let id = self.id; - self.send_job(None, move |git_repo, cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.load_commit(commit, cx).await - } - RepositoryState::Remote(RemoteRepositoryState { - client, project_id, .. - }) => { - let response = client - .request(proto::LoadCommitDiff { - project_id: project_id.0, - repository_id: id.to_proto(), - commit, - }) - .await?; - Ok(CommitDiff { - files: response - .files - .into_iter() - .map(|file| { - Ok(CommitFile { - path: RepoPath::from_proto(&file.path)?, - old_text: file.old_text, - new_text: file.new_text, - }) - }) - .collect::>>()?, - }) - } - } - }) - } - - pub fn file_history( - &mut self, - path: RepoPath, - ) -> oneshot::Receiver> { - self.file_history_paginated(path, 0, None) - } - - pub fn file_history_paginated( - &mut self, - path: RepoPath, - skip: usize, - limit: Option, - ) -> oneshot::Receiver> { - let id = self.id; - self.send_job(None, move |git_repo, _cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.file_history_paginated(path, skip, limit).await - } - RepositoryState::Remote(RemoteRepositoryState { client, project_id }) => { - let response = client - .request(proto::GitFileHistory { - project_id: project_id.0, - repository_id: id.to_proto(), - path: path.to_proto(), - skip: skip as u64, - limit: limit.map(|l| l as u64), - }) - .await?; - Ok(git::repository::FileHistory { - entries: response - .entries - .into_iter() - .map(|entry| git::repository::FileHistoryEntry { - sha: entry.sha.into(), - subject: entry.subject.into(), - message: entry.message.into(), - commit_timestamp: entry.commit_timestamp, - author_name: entry.author_name.into(), - author_email: entry.author_email.into(), - }) - .collect(), - path: RepoPath::from_proto(&response.path)?, - }) - } - } - }) - } - - fn buffer_store(&self, cx: &App) -> Option> { - Some(self.git_store.upgrade()?.read(cx).buffer_store.clone()) - } - - fn save_buffers<'a>( - &self, - entries: impl IntoIterator, - cx: &mut Context, - ) -> Vec>> { - let mut save_futures = Vec::new(); - if let Some(buffer_store) = self.buffer_store(cx) { - buffer_store.update(cx, |buffer_store, cx| { - for path in entries { - 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 - .read(cx) - .file() - .is_some_and(|file| file.disk_state().exists()) - && buffer.read(cx).has_unsaved_edits() - { - save_futures.push(buffer_store.save_buffer(buffer, cx)); - } - } - }) - } - save_futures - } - - pub fn stage_entries( - &mut self, - entries: Vec, - cx: &mut Context, - ) -> Task> { - if entries.is_empty() { - return Task::ready(Ok(())); - } - let id = self.id; - let save_tasks = self.save_buffers(&entries, cx); - let paths = entries - .iter() - .map(|p| p.as_unix_str()) - .collect::>() - .join(" "); - let status = format!("git add {paths}"); - let job_key = GitJobKey::WriteIndex(entries.clone()); - - self.spawn_job_with_tracking( - entries.clone(), - pending_op::GitStatus::Staged, - cx, - async move |this, cx| { - for save_task in save_tasks { - save_task.await?; - } - - this.update(cx, |this, _| { - this.send_keyed_job( - Some(job_key), - Some(status.into()), - move |git_repo, _cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => backend.stage_paths(entries, environment.clone()).await, - RepositoryState::Remote(RemoteRepositoryState { - project_id, - client, - }) => { - client - .request(proto::Stage { - project_id: project_id.0, - repository_id: id.to_proto(), - paths: entries - .into_iter() - .map(|repo_path| repo_path.to_proto()) - .collect(), - }) - .await - .context("sending stage request")?; - - Ok(()) - } - } - }, - ) - })? - .await? - }, - ) - } - - pub fn unstage_entries( - &mut self, - entries: Vec, - cx: &mut Context, - ) -> Task> { - if entries.is_empty() { - return Task::ready(Ok(())); - } - let id = self.id; - let save_tasks = self.save_buffers(&entries, cx); - let paths = entries - .iter() - .map(|p| p.as_unix_str()) - .collect::>() - .join(" "); - let status = format!("git reset {paths}"); - let job_key = GitJobKey::WriteIndex(entries.clone()); - - self.spawn_job_with_tracking( - entries.clone(), - pending_op::GitStatus::Unstaged, - cx, - async move |this, cx| { - for save_task in save_tasks { - save_task.await?; - } - - this.update(cx, |this, _| { - this.send_keyed_job( - Some(job_key), - Some(status.into()), - move |git_repo, _cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => backend.unstage_paths(entries, environment).await, - RepositoryState::Remote(RemoteRepositoryState { - project_id, - client, - }) => { - client - .request(proto::Unstage { - project_id: project_id.0, - repository_id: id.to_proto(), - paths: entries - .into_iter() - .map(|repo_path| repo_path.to_proto()) - .collect(), - }) - .await - .context("sending unstage request")?; - - Ok(()) - } - } - }, - ) - })? - .await? - }, - ) - } - - pub fn stage_all(&mut self, cx: &mut Context) -> Task> { - let to_stage = self - .cached_status() - .filter_map(|entry| { - if let Some(ops) = self.pending_ops_for_path(&entry.repo_path) { - if ops.staging() || ops.staged() { - None - } else { - Some(entry.repo_path) - } - } else if entry.status.staging().is_fully_staged() { - None - } else { - Some(entry.repo_path) - } - }) - .collect(); - self.stage_entries(to_stage, cx) - } - - pub fn unstage_all(&mut self, cx: &mut Context) -> Task> { - let to_unstage = self - .cached_status() - .filter_map(|entry| { - if let Some(ops) = self.pending_ops_for_path(&entry.repo_path) { - if !ops.staging() && !ops.staged() { - None - } else { - Some(entry.repo_path) - } - } else if entry.status.staging().is_fully_unstaged() { - None - } else { - Some(entry.repo_path) - } - }) - .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(); - - self.stash_entries(to_stash, cx) - } - - pub fn stash_entries( - &mut self, - entries: Vec, - cx: &mut Context, - ) -> Task> { - let id = self.id; - - cx.spawn(async move |this, cx| { - this.update(cx, |this, _| { - this.send_job(None, move |git_repo, _cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => backend.stash_paths(entries, environment).await, - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::Stash { - project_id: project_id.0, - repository_id: id.to_proto(), - paths: entries - .into_iter() - .map(|repo_path| repo_path.to_proto()) - .collect(), - }) - .await - .context("sending stash request")?; - Ok(()) - } - } - }) - })? - .await??; - Ok(()) - }) - } - - pub fn stash_pop( - &mut self, - index: Option, - cx: &mut Context, - ) -> Task> { - let id = self.id; - cx.spawn(async move |this, cx| { - this.update(cx, |this, _| { - this.send_job(None, move |git_repo, _cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => backend.stash_pop(index, environment).await, - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::StashPop { - project_id: project_id.0, - repository_id: id.to_proto(), - stash_index: index.map(|i| i as u64), - }) - .await - .context("sending stash pop request")?; - Ok(()) - } - } - }) - })? - .await??; - Ok(()) - }) - } - - pub fn stash_apply( - &mut self, - index: Option, - cx: &mut Context, - ) -> Task> { - let id = self.id; - cx.spawn(async move |this, cx| { - this.update(cx, |this, _| { - this.send_job(None, move |git_repo, _cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => backend.stash_apply(index, environment).await, - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::StashApply { - project_id: project_id.0, - repository_id: id.to_proto(), - stash_index: index.map(|i| i as u64), - }) - .await - .context("sending stash apply request")?; - Ok(()) - } - } - }) - })? - .await??; - Ok(()) - }) - } - - pub fn stash_drop( - &mut self, - index: Option, - cx: &mut Context, - ) -> oneshot::Receiver> { - let id = self.id; - let updates_tx = self - .git_store() - .and_then(|git_store| match &git_store.read(cx).state { - GitStoreState::Local { downstream, .. } => downstream - .as_ref() - .map(|downstream| downstream.updates_tx.clone()), - _ => None, - }); - let this = cx.weak_entity(); - self.send_job(None, move |git_repo, mut cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => { - // TODO would be nice to not have to do this manually - let result = backend.stash_drop(index, environment).await; - if result.is_ok() - && let Ok(stash_entries) = backend.stash_entries().await - { - let snapshot = this.update(&mut cx, |this, cx| { - this.snapshot.stash_entries = stash_entries; - cx.emit(RepositoryEvent::StashEntriesChanged); - this.snapshot.clone() - })?; - if let Some(updates_tx) = updates_tx { - updates_tx - .unbounded_send(DownstreamUpdate::UpdateRepository(snapshot)) - .ok(); - } - } - - result - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::StashDrop { - project_id: project_id.0, - repository_id: id.to_proto(), - stash_index: index.map(|i| i as u64), - }) - .await - .context("sending stash pop request")?; - Ok(()) - } - } - }) - } - - pub fn run_hook(&mut self, hook: RunHook, _cx: &mut App) -> oneshot::Receiver> { - let id = self.id; - self.send_job( - Some(format!("git hook {}", hook.as_str()).into()), - move |git_repo, _cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => backend.run_hook(hook, environment.clone()).await, - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::RunGitHook { - project_id: project_id.0, - repository_id: id.to_proto(), - hook: hook.to_proto(), - }) - .await?; - - Ok(()) - } - } - }, - ) - } - - pub fn commit( - &mut self, - message: SharedString, - name_and_email: Option<(SharedString, SharedString)>, - options: CommitOptions, - askpass: AskPassDelegate, - cx: &mut App, - ) -> oneshot::Receiver> { - let id = self.id; - let askpass_delegates = self.askpass_delegates.clone(); - let askpass_id = util::post_inc(&mut self.latest_askpass_id); - - let rx = self.run_hook(RunHook::PreCommit, cx); - - self.send_job(Some("git commit".into()), move |git_repo, _cx| async move { - rx.await??; - - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => { - backend - .commit(message, name_and_email, options, askpass, environment) - .await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - askpass_delegates.lock().insert(askpass_id, askpass); - let _defer = util::defer(|| { - let askpass_delegate = askpass_delegates.lock().remove(&askpass_id); - debug_assert!(askpass_delegate.is_some()); - }); - let (name, email) = name_and_email.unzip(); - client - .request(proto::Commit { - project_id: project_id.0, - repository_id: id.to_proto(), - message: String::from(message), - name: name.map(String::from), - email: email.map(String::from), - options: Some(proto::commit::CommitOptions { - amend: options.amend, - signoff: options.signoff, - }), - askpass_id, - }) - .await - .context("sending commit request")?; - - Ok(()) - } - } - }) - } - - pub fn fetch( - &mut self, - fetch_options: FetchOptions, - askpass: AskPassDelegate, - _cx: &mut App, - ) -> oneshot::Receiver> { - let askpass_delegates = self.askpass_delegates.clone(); - let askpass_id = util::post_inc(&mut self.latest_askpass_id); - let id = self.id; - - self.send_job(Some("git fetch".into()), move |git_repo, cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => backend.fetch(fetch_options, askpass, environment, cx).await, - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - askpass_delegates.lock().insert(askpass_id, askpass); - let _defer = util::defer(|| { - let askpass_delegate = askpass_delegates.lock().remove(&askpass_id); - debug_assert!(askpass_delegate.is_some()); - }); - - let response = client - .request(proto::Fetch { - project_id: project_id.0, - repository_id: id.to_proto(), - askpass_id, - remote: fetch_options.to_proto(), - }) - .await - .context("sending fetch request")?; - - Ok(RemoteCommandOutput { - stdout: response.stdout, - stderr: response.stderr, - }) - } - } - }) - } - - pub fn push( - &mut self, - branch: SharedString, - remote: SharedString, - options: Option, - askpass: AskPassDelegate, - cx: &mut Context, - ) -> oneshot::Receiver> { - let askpass_delegates = self.askpass_delegates.clone(); - let askpass_id = util::post_inc(&mut self.latest_askpass_id); - let id = self.id; - - let args = options - .map(|option| match option { - PushOptions::SetUpstream => " --set-upstream", - PushOptions::Force => " --force-with-lease", - }) - .unwrap_or(""); - - let updates_tx = self - .git_store() - .and_then(|git_store| match &git_store.read(cx).state { - GitStoreState::Local { downstream, .. } => downstream - .as_ref() - .map(|downstream| downstream.updates_tx.clone()), - _ => None, - }); - - let this = cx.weak_entity(); - self.send_job( - Some(format!("git push {} {} {}", args, remote, branch).into()), - move |git_repo, mut cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => { - let result = backend - .push( - branch.to_string(), - remote.to_string(), - options, - askpass, - environment.clone(), - cx.clone(), - ) - .await; - // TODO would be nice to not have to do this manually - if result.is_ok() { - let branches = backend.branches().await?; - let branch = branches.into_iter().find(|branch| branch.is_head); - log::info!("head branch after scan is {branch:?}"); - let snapshot = this.update(&mut cx, |this, cx| { - this.snapshot.branch = branch; - cx.emit(RepositoryEvent::BranchChanged); - this.snapshot.clone() - })?; - if let Some(updates_tx) = updates_tx { - updates_tx - .unbounded_send(DownstreamUpdate::UpdateRepository(snapshot)) - .ok(); - } - } - result - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - askpass_delegates.lock().insert(askpass_id, askpass); - let _defer = util::defer(|| { - let askpass_delegate = askpass_delegates.lock().remove(&askpass_id); - debug_assert!(askpass_delegate.is_some()); - }); - let response = client - .request(proto::Push { - project_id: project_id.0, - repository_id: id.to_proto(), - askpass_id, - branch_name: branch.to_string(), - remote_name: remote.to_string(), - options: options.map(|options| match options { - PushOptions::Force => proto::push::PushOptions::Force, - PushOptions::SetUpstream => { - proto::push::PushOptions::SetUpstream - } - } - as i32), - }) - .await - .context("sending push request")?; - - Ok(RemoteCommandOutput { - stdout: response.stdout, - stderr: response.stderr, - }) - } - } - }, - ) - } - - pub fn pull( - &mut self, - branch: Option, - remote: SharedString, - rebase: bool, - askpass: AskPassDelegate, - _cx: &mut App, - ) -> oneshot::Receiver> { - let askpass_delegates = self.askpass_delegates.clone(); - let askpass_id = util::post_inc(&mut self.latest_askpass_id); - let id = self.id; - - let mut status = "git pull".to_string(); - if rebase { - status.push_str(" --rebase"); - } - status.push_str(&format!(" {}", remote)); - if let Some(b) = &branch { - status.push_str(&format!(" {}", b)); - } - - self.send_job(Some(status.into()), move |git_repo, cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => { - backend - .pull( - branch.as_ref().map(|b| b.to_string()), - remote.to_string(), - rebase, - askpass, - environment.clone(), - cx, - ) - .await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - askpass_delegates.lock().insert(askpass_id, askpass); - let _defer = util::defer(|| { - let askpass_delegate = askpass_delegates.lock().remove(&askpass_id); - debug_assert!(askpass_delegate.is_some()); - }); - let response = client - .request(proto::Pull { - project_id: project_id.0, - repository_id: id.to_proto(), - askpass_id, - rebase, - branch_name: branch.as_ref().map(|b| b.to_string()), - remote_name: remote.to_string(), - }) - .await - .context("sending pull request")?; - - Ok(RemoteCommandOutput { - stdout: response.stdout, - stderr: response.stderr, - }) - } - } - }) - } - - fn spawn_set_index_text_job( - &mut self, - path: RepoPath, - content: Option, - hunk_staging_operation_count: Option, - cx: &mut Context, - ) -> oneshot::Receiver> { - let id = self.id; - let this = cx.weak_entity(); - let git_store = self.git_store.clone(); - let abs_path = self.snapshot.repo_path_to_abs_path(&path); - self.send_keyed_job( - Some(GitJobKey::WriteIndex(vec![path.clone()])), - None, - move |git_repo, mut cx| async move { - log::debug!( - "start updating index text for buffer {}", - path.as_unix_str() - ); - - match git_repo { - RepositoryState::Local(LocalRepositoryState { - fs, - backend, - environment, - .. - }) => { - let executable = match fs.metadata(&abs_path).await { - Ok(Some(meta)) => meta.is_executable, - Ok(None) => false, - Err(_err) => false, - }; - backend - .set_index_text(path.clone(), content, environment.clone(), executable) - .await?; - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::SetIndexText { - project_id: project_id.0, - repository_id: id.to_proto(), - path: path.to_proto(), - text: content, - }) - .await?; - } - } - log::debug!( - "finish updating index text for buffer {}", - path.as_unix_str() - ); - - if let Some(hunk_staging_operation_count) = hunk_staging_operation_count { - let project_path = this - .read_with(&cx, |this, cx| this.repo_path_to_project_path(&path, cx)) - .ok() - .flatten(); - git_store.update(&mut cx, |git_store, cx| { - let buffer_id = git_store - .buffer_store - .read(cx) - .get_by_path(&project_path?)? - .read(cx) - .remote_id(); - let diff_state = git_store.diffs.get(&buffer_id)?; - diff_state.update(cx, |diff_state, _| { - diff_state.hunk_staging_operation_count_as_of_write = - hunk_staging_operation_count; - }); - Some(()) - })?; - } - Ok(()) - }, - ) - } - - pub fn create_remote( - &mut self, - remote_name: String, - remote_url: String, - ) -> oneshot::Receiver> { - let id = self.id; - self.send_job( - Some(format!("git remote add {remote_name} {remote_url}").into()), - move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.create_remote(remote_name, remote_url).await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::GitCreateRemote { - project_id: project_id.0, - repository_id: id.to_proto(), - remote_name, - remote_url, - }) - .await?; - - Ok(()) - } - } - }, - ) - } - - pub fn remove_remote(&mut self, remote_name: String) -> oneshot::Receiver> { - let id = self.id; - self.send_job( - Some(format!("git remove remote {remote_name}").into()), - move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.remove_remote(remote_name).await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::GitRemoveRemote { - project_id: project_id.0, - repository_id: id.to_proto(), - remote_name, - }) - .await?; - - Ok(()) - } - } - }, - ) - } - - pub fn get_remotes( - &mut self, - branch_name: Option, - is_push: bool, - ) -> oneshot::Receiver>> { - let id = self.id; - self.send_job(None, move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - let remote = if let Some(branch_name) = branch_name { - if is_push { - backend.get_push_remote(branch_name).await? - } else { - backend.get_branch_remote(branch_name).await? - } - } else { - None - }; - - match remote { - Some(remote) => Ok(vec![remote]), - None => backend.get_all_remotes().await, - } - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let response = client - .request(proto::GetRemotes { - project_id: project_id.0, - repository_id: id.to_proto(), - branch_name, - is_push, - }) - .await?; - - let remotes = response - .remotes - .into_iter() - .map(|remotes| Remote { - name: remotes.name.into(), - }) - .collect(); - - Ok(remotes) - } - } - }) - } - - pub fn branches(&mut self) -> oneshot::Receiver>> { - let id = self.id; - self.send_job(None, move |repo, _| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.branches().await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let response = client - .request(proto::GitGetBranches { - project_id: project_id.0, - repository_id: id.to_proto(), - }) - .await?; - - let branches = response - .branches - .into_iter() - .map(|branch| proto_to_branch(&branch)) - .collect(); - - Ok(branches) - } - } - }) - } - - pub fn worktrees(&mut self) -> oneshot::Receiver>> { - let id = self.id; - self.send_job(None, move |repo, _| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.worktrees().await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let response = client - .request(proto::GitGetWorktrees { - project_id: project_id.0, - repository_id: id.to_proto(), - }) - .await?; - - let worktrees = response - .worktrees - .into_iter() - .map(|worktree| proto_to_worktree(&worktree)) - .collect(); - - Ok(worktrees) - } - } - }) - } - - pub fn create_worktree( - &mut self, - name: String, - path: PathBuf, - commit: Option, - ) -> oneshot::Receiver> { - let id = self.id; - self.send_job( - Some("git worktree add".into()), - move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.create_worktree(name, path, commit).await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::GitCreateWorktree { - project_id: project_id.0, - repository_id: id.to_proto(), - name, - directory: path.to_string_lossy().to_string(), - commit, - }) - .await?; - - Ok(()) - } - } - }, - ) - } - - pub fn default_branch(&mut self) -> oneshot::Receiver>> { - let id = self.id; - self.send_job(None, move |repo, _| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.default_branch().await - } - RepositoryState::Remote(RemoteRepositoryState { 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_tree( - &mut self, - diff_type: DiffTreeType, - _cx: &App, - ) -> oneshot::Receiver> { - let repository_id = self.snapshot.id; - self.send_job(None, move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.diff_tree(diff_type).await - } - RepositoryState::Remote(RemoteRepositoryState { client, project_id }) => { - let response = client - .request(proto::GetTreeDiff { - project_id: project_id.0, - repository_id: repository_id.0, - is_merge: matches!(diff_type, DiffTreeType::MergeBase { .. }), - base: diff_type.base().to_string(), - head: diff_type.head().to_string(), - }) - .await?; - - let entries = response - .entries - .into_iter() - .filter_map(|entry| { - let status = match entry.status() { - proto::tree_diff_status::Status::Added => TreeDiffStatus::Added, - proto::tree_diff_status::Status::Modified => { - TreeDiffStatus::Modified { - old: git::Oid::from_str( - &entry.oid.context("missing oid").log_err()?, - ) - .log_err()?, - } - } - proto::tree_diff_status::Status::Deleted => { - TreeDiffStatus::Deleted { - old: git::Oid::from_str( - &entry.oid.context("missing oid").log_err()?, - ) - .log_err()?, - } - } - }; - Some(( - RepoPath::from_rel_path( - &RelPath::from_proto(&entry.path).log_err()?, - ), - status, - )) - }) - .collect(); - - Ok(TreeDiff { entries }) - } - } - }) - } - - 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 { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.diff(diff_type).await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let response = client - .request(proto::GitDiff { - project_id: project_id.0, - repository_id: id.to_proto(), - diff_type: match diff_type { - DiffType::HeadToIndex => { - proto::git_diff::DiffType::HeadToIndex.into() - } - DiffType::HeadToWorktree => { - proto::git_diff::DiffType::HeadToWorktree.into() - } - }, - }) - .await?; - - Ok(response.diff) - } - } - }) - } - - pub fn create_branch( - &mut self, - branch_name: String, - base_branch: Option, - ) -> oneshot::Receiver> { - let id = self.id; - let status_msg = if let Some(ref base) = base_branch { - format!("git switch -c {branch_name} {base}").into() - } else { - format!("git switch -c {branch_name}").into() - }; - self.send_job(Some(status_msg), move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.create_branch(branch_name, base_branch).await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::GitCreateBranch { - project_id: project_id.0, - repository_id: id.to_proto(), - branch_name, - }) - .await?; - - Ok(()) - } - } - }) - } - - pub fn change_branch(&mut self, branch_name: String) -> oneshot::Receiver> { - let id = self.id; - self.send_job( - Some(format!("git switch {branch_name}").into()), - move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.change_branch(branch_name).await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::GitChangeBranch { - project_id: project_id.0, - repository_id: id.to_proto(), - branch_name, - }) - .await?; - - Ok(()) - } - } - }, - ) - } - - pub fn delete_branch(&mut self, branch_name: String) -> oneshot::Receiver> { - let id = self.id; - self.send_job( - Some(format!("git branch -d {branch_name}").into()), - move |repo, _cx| async move { - match repo { - RepositoryState::Local(state) => state.backend.delete_branch(branch_name).await, - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::GitDeleteBranch { - project_id: project_id.0, - repository_id: id.to_proto(), - branch_name, - }) - .await?; - - Ok(()) - } - } - }, - ) - } - - pub fn rename_branch( - &mut self, - branch: String, - new_name: String, - ) -> oneshot::Receiver> { - let id = self.id; - self.send_job( - Some(format!("git branch -m {branch} {new_name}").into()), - move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.rename_branch(branch, new_name).await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::GitRenameBranch { - project_id: project_id.0, - repository_id: id.to_proto(), - branch, - new_name, - }) - .await?; - - Ok(()) - } - } - }, - ) - } - - pub fn check_for_pushed_commits(&mut self) -> oneshot::Receiver>> { - let id = self.id; - self.send_job(None, move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.check_for_pushed_commit().await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let response = client - .request(proto::CheckForPushedCommits { - project_id: project_id.0, - repository_id: id.to_proto(), - }) - .await?; - - let branches = response.pushed_to.into_iter().map(Into::into).collect(); - - Ok(branches) - } - } - }) - } - - pub fn checkpoint(&mut self) -> oneshot::Receiver> { - self.send_job(None, |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.checkpoint().await - } - RepositoryState::Remote(..) => anyhow::bail!("not implemented yet"), - } - }) - } - - pub fn restore_checkpoint( - &mut self, - checkpoint: GitRepositoryCheckpoint, - ) -> oneshot::Receiver> { - self.send_job(None, move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.restore_checkpoint(checkpoint).await - } - RepositoryState::Remote { .. } => anyhow::bail!("not implemented yet"), - } - }) - } - - pub(crate) fn apply_remote_update( - &mut self, - update: proto::UpdateRepository, - cx: &mut Context, - ) -> Result<()> { - let conflicted_paths = TreeSet::from_ordered_entries( - update - .current_merge_conflicts - .into_iter() - .filter_map(|path| RepoPath::from_proto(&path).log_err()), - ); - let new_branch = update.branch_summary.as_ref().map(proto_to_branch); - let new_head_commit = update - .head_commit_details - .as_ref() - .map(proto_to_commit_details); - if self.snapshot.branch != new_branch || self.snapshot.head_commit != new_head_commit { - cx.emit(RepositoryEvent::BranchChanged) - } - self.snapshot.branch = new_branch; - self.snapshot.head_commit = new_head_commit; - - self.snapshot.merge.conflicted_paths = conflicted_paths; - self.snapshot.merge.message = update.merge_message.map(SharedString::from); - let new_stash_entries = GitStash { - entries: update - .stash_entries - .iter() - .filter_map(|entry| proto_to_stash(entry).ok()) - .collect(), - }; - if self.snapshot.stash_entries != new_stash_entries { - cx.emit(RepositoryEvent::StashEntriesChanged) - } - self.snapshot.stash_entries = new_stash_entries; - self.snapshot.remote_upstream_url = update.remote_upstream_url; - self.snapshot.remote_origin_url = update.remote_origin_url; - - let edits = update - .removed_statuses - .into_iter() - .filter_map(|path| { - Some(sum_tree::Edit::Remove(PathKey( - RelPath::from_proto(&path).log_err()?, - ))) - }) - .chain( - update - .updated_statuses - .into_iter() - .filter_map(|updated_status| { - Some(sum_tree::Edit::Insert(updated_status.try_into().log_err()?)) - }), - ) - .collect::>(); - if !edits.is_empty() { - cx.emit(RepositoryEvent::StatusesChanged); - } - self.snapshot.statuses_by_path.edit(edits, ()); - if update.is_last_update { - self.snapshot.scan_id = update.scan_id; - } - self.clear_pending_ops(cx); - Ok(()) - } - - pub fn compare_checkpoints( - &mut self, - left: GitRepositoryCheckpoint, - right: GitRepositoryCheckpoint, - ) -> oneshot::Receiver> { - self.send_job(None, move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.compare_checkpoints(left, right).await - } - RepositoryState::Remote { .. } => anyhow::bail!("not implemented yet"), - } - }) - } - - pub fn diff_checkpoints( - &mut self, - base_checkpoint: GitRepositoryCheckpoint, - target_checkpoint: GitRepositoryCheckpoint, - ) -> oneshot::Receiver> { - self.send_job(None, move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend - .diff_checkpoints(base_checkpoint, target_checkpoint) - .await - } - RepositoryState::Remote { .. } => anyhow::bail!("not implemented yet"), - } - }) - } - - fn clear_pending_ops(&mut self, cx: &mut Context) { - let updated = SumTree::from_iter( - self.pending_ops.iter().filter_map(|ops| { - let inner_ops: Vec = - ops.ops.iter().filter(|op| op.running()).cloned().collect(); - if inner_ops.is_empty() { - None - } else { - Some(PendingOps { - repo_path: ops.repo_path.clone(), - ops: inner_ops, - }) - } - }), - (), - ); - - if updated != self.pending_ops { - cx.emit(RepositoryEvent::PendingOpsChanged { - pending_ops: self.pending_ops.clone(), - }) - } - - self.pending_ops = updated; - } - - fn schedule_scan( - &mut self, - updates_tx: Option>, - cx: &mut Context, - ) { - let this = cx.weak_entity(); - let _ = self.send_keyed_job( - Some(GitJobKey::ReloadGitState), - None, - |state, mut cx| async move { - log::debug!("run scheduled git status scan"); - - let Some(this) = this.upgrade() else { - return Ok(()); - }; - let RepositoryState::Local(LocalRepositoryState { backend, .. }) = state else { - bail!("not a local repository") - }; - let (snapshot, events) = this - .update(&mut cx, |this, _| { - this.paths_needing_status_update.clear(); - compute_snapshot( - this.id, - this.work_directory_abs_path.clone(), - this.snapshot.clone(), - backend.clone(), - ) - })? - .await?; - this.update(&mut cx, |this, cx| { - this.snapshot = snapshot.clone(); - this.clear_pending_ops(cx); - for event in events { - cx.emit(event); - } - })?; - if let Some(updates_tx) = updates_tx { - updates_tx - .unbounded_send(DownstreamUpdate::UpdateRepository(snapshot)) - .ok(); - } - Ok(()) - }, - ); - } - - fn spawn_local_git_worker( - state: Shared>>, - cx: &mut Context, - ) -> mpsc::UnboundedSender { - let (job_tx, mut job_rx) = mpsc::unbounded::(); - - cx.spawn(async move |_, cx| { - let state = state.await.map_err(|err| anyhow::anyhow!(err))?; - if let Some(git_hosting_provider_registry) = - cx.update(|cx| GitHostingProviderRegistry::try_global(cx))? - { - git_hosting_providers::register_additional_providers( - git_hosting_provider_registry, - state.backend.clone(), - ) - .await; - } - let state = RepositoryState::Local(state); - let mut jobs = VecDeque::new(); - loop { - while let Ok(Some(next_job)) = job_rx.try_next() { - jobs.push_back(next_job); - } - - if let Some(job) = jobs.pop_front() { - if let Some(current_key) = &job.key - && 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); - } else { - break; - } - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - - job_tx - } - - fn spawn_remote_git_worker( - state: RemoteRepositoryState, - cx: &mut Context, - ) -> mpsc::UnboundedSender { - let (job_tx, mut job_rx) = mpsc::unbounded::(); - - cx.spawn(async move |_, cx| { - let state = RepositoryState::Remote(state); - let mut jobs = VecDeque::new(); - loop { - while let Ok(Some(next_job)) = job_rx.try_next() { - jobs.push_back(next_job); - } - - if let Some(job) = jobs.pop_front() { - if let Some(current_key) = &job.key - && 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); - } else { - break; - } - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - - job_tx - } - - fn load_staged_text( - &mut self, - buffer_id: BufferId, - repo_path: RepoPath, - cx: &App, - ) -> Task>> { - let rx = self.send_job(None, move |state, _| async move { - match state { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - anyhow::Ok(backend.load_index_text(repo_path).await) - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let response = client - .request(proto::OpenUnstagedDiff { - project_id: project_id.to_proto(), - buffer_id: buffer_id.to_proto(), - }) - .await?; - Ok(response.staged_text) - } - } - }); - cx.spawn(|_: &mut AsyncApp| async move { rx.await? }) - } - - fn load_committed_text( - &mut self, - buffer_id: BufferId, - repo_path: RepoPath, - cx: &App, - ) -> Task> { - let rx = self.send_job(None, move |state, _| async move { - match state { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - let committed_text = backend.load_committed_text(repo_path.clone()).await; - let staged_text = backend.load_index_text(repo_path).await; - let diff_bases_change = if committed_text == staged_text { - DiffBasesChange::SetBoth(committed_text) - } else { - DiffBasesChange::SetEach { - index: staged_text, - head: committed_text, - } - }; - anyhow::Ok(diff_bases_change) - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - use proto::open_uncommitted_diff_response::Mode; - - let response = client - .request(proto::OpenUncommittedDiff { - project_id: project_id.to_proto(), - buffer_id: buffer_id.to_proto(), - }) - .await?; - let mode = Mode::from_i32(response.mode).context("Invalid mode")?; - let bases = match mode { - Mode::IndexMatchesHead => DiffBasesChange::SetBoth(response.committed_text), - Mode::IndexAndHead => DiffBasesChange::SetEach { - head: response.committed_text, - index: response.staged_text, - }, - }; - Ok(bases) - } - } - }); - - cx.spawn(|_: &mut AsyncApp| async move { rx.await? }) - } - fn load_blob_content(&mut self, oid: Oid, cx: &App) -> Task> { - let repository_id = self.snapshot.id; - let rx = self.send_job(None, move |state, _| async move { - match state { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.load_blob_content(oid).await - } - RepositoryState::Remote(RemoteRepositoryState { client, project_id }) => { - let response = client - .request(proto::GetBlobContent { - project_id: project_id.to_proto(), - repository_id: repository_id.0, - oid: oid.to_string(), - }) - .await?; - Ok(response.content) - } - } - }); - cx.spawn(|_: &mut AsyncApp| async move { rx.await? }) - } - - fn paths_changed( - &mut self, - paths: Vec, - updates_tx: Option>, - cx: &mut Context, - ) { - self.paths_needing_status_update.extend(paths); - - let this = cx.weak_entity(); - let _ = self.send_keyed_job( - Some(GitJobKey::RefreshStatuses), - None, - |state, mut cx| async move { - let (prev_snapshot, mut changed_paths) = this.update(&mut cx, |this, _| { - ( - this.snapshot.clone(), - mem::take(&mut this.paths_needing_status_update), - ) - })?; - let RepositoryState::Local(LocalRepositoryState { backend, .. }) = state else { - bail!("not a local repository") - }; - - let paths = changed_paths.iter().cloned().collect::>(); - if paths.is_empty() { - return Ok(()); - } - let statuses = backend.status(&paths).await?; - let stash_entries = backend.stash_entries().await?; - - let changed_path_statuses = cx - .background_spawn(async move { - let mut changed_path_statuses = Vec::new(); - let prev_statuses = prev_snapshot.statuses_by_path.clone(); - let mut cursor = prev_statuses.cursor::(()); - - 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; - } - - changed_path_statuses.push(Edit::Insert(StatusEntry { - repo_path: repo_path.clone(), - status: *status, - })); - } - let mut cursor = prev_statuses.cursor::(()); - for path in changed_paths.into_iter() { - if cursor.seek_forward(&PathTarget::Path(&path), Bias::Left) { - changed_path_statuses - .push(Edit::Remove(PathKey(path.as_ref().clone()))); - } - } - changed_path_statuses - }) - .await; - - this.update(&mut cx, |this, cx| { - if this.snapshot.stash_entries != stash_entries { - cx.emit(RepositoryEvent::StashEntriesChanged); - this.snapshot.stash_entries = stash_entries; - } - - if !changed_path_statuses.is_empty() { - cx.emit(RepositoryEvent::StatusesChanged); - this.snapshot - .statuses_by_path - .edit(changed_path_statuses, ()); - this.snapshot.scan_id += 1; - } - - if let Some(updates_tx) = updates_tx { - updates_tx - .unbounded_send(DownstreamUpdate::UpdateRepository( - this.snapshot.clone(), - )) - .ok(); - } - }) - }, - ); - } - - /// currently running git command and when it started - pub fn current_job(&self) -> Option { - self.active_jobs.values().next().cloned() - } - - pub fn barrier(&mut self) -> oneshot::Receiver<()> { - self.send_job(None, |_, _| async {}) - } - - fn spawn_job_with_tracking( - &mut self, - paths: Vec, - git_status: pending_op::GitStatus, - cx: &mut Context, - f: AsyncFn, - ) -> Task> - where - AsyncFn: AsyncFnOnce(WeakEntity, &mut AsyncApp) -> Result<()> + 'static, - { - let ids = self.new_pending_ops_for_paths(paths, git_status); - - cx.spawn(async move |this, cx| { - let (job_status, result) = match f(this.clone(), cx).await { - Ok(()) => (pending_op::JobStatus::Finished, Ok(())), - Err(err) if err.is::() => (pending_op::JobStatus::Skipped, Ok(())), - Err(err) => (pending_op::JobStatus::Error, Err(err)), - }; - - this.update(cx, |this, _| { - let mut edits = Vec::with_capacity(ids.len()); - for (id, entry) in ids { - if let Some(mut ops) = this - .pending_ops - .get(&PathKey(entry.as_ref().clone()), ()) - .cloned() - { - if let Some(op) = ops.op_by_id_mut(id) { - op.job_status = job_status; - } - edits.push(sum_tree::Edit::Insert(ops)); - } - } - this.pending_ops.edit(edits, ()); - })?; - - result - }) - } - - fn new_pending_ops_for_paths( - &mut self, - paths: Vec, - git_status: pending_op::GitStatus, - ) -> Vec<(PendingOpId, RepoPath)> { - let mut edits = Vec::with_capacity(paths.len()); - let mut ids = Vec::with_capacity(paths.len()); - for path in paths { - let mut ops = self - .pending_ops - .get(&PathKey(path.as_ref().clone()), ()) - .cloned() - .unwrap_or_else(|| PendingOps::new(&path)); - let id = ops.max_id() + 1; - ops.ops.push(PendingOp { - id, - git_status, - job_status: pending_op::JobStatus::Running, - }); - edits.push(sum_tree::Edit::Insert(ops)); - ids.push((id, path)); - } - self.pending_ops.edit(edits, ()); - ids - } -} - -fn get_permalink_in_rust_registry_src( - provider_registry: Arc, - path: PathBuf, - selection: Range, -) -> Result { - #[derive(Deserialize)] - struct CargoVcsGit { - sha1: String, - } - - #[derive(Deserialize)] - struct CargoVcsInfo { - git: CargoVcsGit, - path_in_vcs: String, - } - - #[derive(Deserialize)] - struct CargoPackage { - repository: String, - } - - #[derive(Deserialize)] - struct CargoToml { - package: CargoPackage, - } - - let Some((dir, cargo_vcs_info_json)) = path.ancestors().skip(1).find_map(|dir| { - let json = std::fs::read_to_string(dir.join(".cargo_vcs_info.json")).ok()?; - Some((dir, json)) - }) else { - bail!("No .cargo_vcs_info.json found in parent directories") - }; - let cargo_vcs_info = serde_json::from_str::(&cargo_vcs_info_json)?; - let cargo_toml = std::fs::read_to_string(dir.join("Cargo.toml"))?; - let manifest = toml::from_str::(&cargo_toml)?; - let (provider, remote) = parse_git_remote_url(provider_registry, &manifest.package.repository) - .context("parsing package.repository field of manifest")?; - let path = PathBuf::from(cargo_vcs_info.path_in_vcs).join(path.strip_prefix(dir).unwrap()); - let permalink = provider.build_permalink( - remote, - BuildPermalinkParams::new( - &cargo_vcs_info.git.sha1, - &RepoPath::from_rel_path( - &RelPath::new(&path, PathStyle::local()).context("invalid path")?, - ), - Some(selection), - ), - ); - Ok(permalink) -} - -fn serialize_blame_buffer_response(blame: Option) -> proto::BlameBufferResponse { - let Some(blame) = blame else { - return proto::BlameBufferResponse { - blame_response: None, - }; - }; - - let entries = blame - .entries - .into_iter() - .map(|entry| proto::BlameEntry { - sha: entry.sha.as_bytes().into(), - start_line: entry.range.start, - end_line: entry.range.end, - original_line_number: entry.original_line_number, - author: entry.author, - author_mail: entry.author_mail, - author_time: entry.author_time, - author_tz: entry.author_tz, - committer: entry.committer_name, - committer_mail: entry.committer_email, - committer_time: entry.committer_time, - committer_tz: entry.committer_tz, - summary: entry.summary, - previous: entry.previous, - filename: entry.filename, - }) - .collect::>(); - - let messages = blame - .messages - .into_iter() - .map(|(oid, message)| proto::CommitMessage { - oid: oid.as_bytes().into(), - message, - }) - .collect::>(); - - proto::BlameBufferResponse { - blame_response: Some(proto::blame_buffer_response::BlameResponse { entries, messages }), - } -} - -fn deserialize_blame_buffer_response( - response: proto::BlameBufferResponse, -) -> Option { - let response = response.blame_response?; - let entries = response - .entries - .into_iter() - .filter_map(|entry| { - Some(git::blame::BlameEntry { - sha: git::Oid::from_bytes(&entry.sha).ok()?, - range: entry.start_line..entry.end_line, - original_line_number: entry.original_line_number, - committer_name: entry.committer, - committer_time: entry.committer_time, - committer_tz: entry.committer_tz, - committer_email: entry.committer_mail, - author: entry.author, - author_mail: entry.author_mail, - author_time: entry.author_time, - author_tz: entry.author_tz, - summary: entry.summary, - previous: entry.previous, - filename: entry.filename, - }) - }) - .collect::>(); - - let messages = response - .messages - .into_iter() - .filter_map(|message| Some((git::Oid::from_bytes(&message.oid).ok()?, message.message))) - .collect::>(); - - Some(Blame { entries, messages }) -} - -fn branch_to_proto(branch: &git::repository::Branch) -> proto::Branch { - proto::Branch { - is_head: branch.is_head, - ref_name: branch.ref_name.to_string(), - unix_timestamp: branch - .most_recent_commit - .as_ref() - .map(|commit| commit.commit_timestamp as u64), - upstream: branch.upstream.as_ref().map(|upstream| proto::GitUpstream { - ref_name: upstream.ref_name.to_string(), - tracking: upstream - .tracking - .status() - .map(|upstream| proto::UpstreamTracking { - ahead: upstream.ahead as u64, - behind: upstream.behind as u64, - }), - }), - most_recent_commit: branch - .most_recent_commit - .as_ref() - .map(|commit| proto::CommitSummary { - sha: commit.sha.to_string(), - subject: commit.subject.to_string(), - commit_timestamp: commit.commit_timestamp, - author_name: commit.author_name.to_string(), - }), - } -} - -fn worktree_to_proto(worktree: &git::repository::Worktree) -> proto::Worktree { - proto::Worktree { - path: worktree.path.to_string_lossy().to_string(), - ref_name: worktree.ref_name.to_string(), - sha: worktree.sha.to_string(), - } -} - -fn proto_to_worktree(proto: &proto::Worktree) -> git::repository::Worktree { - git::repository::Worktree { - path: PathBuf::from(proto.path.clone()), - ref_name: proto.ref_name.clone().into(), - sha: proto.sha.clone().into(), - } -} - -fn proto_to_branch(proto: &proto::Branch) -> git::repository::Branch { - git::repository::Branch { - is_head: proto.is_head, - ref_name: proto.ref_name.clone().into(), - upstream: proto - .upstream - .as_ref() - .map(|upstream| git::repository::Upstream { - ref_name: upstream.ref_name.to_string().into(), - tracking: upstream - .tracking - .as_ref() - .map(|tracking| { - git::repository::UpstreamTracking::Tracked(UpstreamTrackingStatus { - ahead: tracking.ahead as u32, - behind: tracking.behind as u32, - }) - }) - .unwrap_or(git::repository::UpstreamTracking::Gone), - }), - most_recent_commit: proto.most_recent_commit.as_ref().map(|commit| { - git::repository::CommitSummary { - sha: commit.sha.to_string().into(), - subject: commit.subject.to_string().into(), - commit_timestamp: commit.commit_timestamp, - author_name: commit.author_name.to_string().into(), - has_parent: true, - } - }), - } -} - -fn commit_details_to_proto(commit: &CommitDetails) -> proto::GitCommitDetails { - proto::GitCommitDetails { - sha: commit.sha.to_string(), - message: commit.message.to_string(), - commit_timestamp: commit.commit_timestamp, - author_email: commit.author_email.to_string(), - author_name: commit.author_name.to_string(), - } -} - -fn proto_to_commit_details(proto: &proto::GitCommitDetails) -> CommitDetails { - CommitDetails { - sha: proto.sha.clone().into(), - message: proto.message.clone().into(), - commit_timestamp: proto.commit_timestamp, - author_email: proto.author_email.clone().into(), - author_name: proto.author_name.clone().into(), - } -} - -async fn compute_snapshot( - id: RepositoryId, - work_directory_abs_path: Arc, - prev_snapshot: RepositorySnapshot, - backend: Arc, -) -> Result<(RepositorySnapshot, Vec)> { - let mut events = Vec::new(); - let branches = backend.branches().await?; - let branch = branches.into_iter().find(|branch| branch.is_head); - let statuses = backend - .status(&[RepoPath::from_rel_path( - &RelPath::new(".".as_ref(), PathStyle::local()).unwrap(), - )]) - .await?; - let stash_entries = backend.stash_entries().await?; - let statuses_by_path = SumTree::from_iter( - statuses - .entries - .iter() - .map(|(repo_path, status)| StatusEntry { - repo_path: repo_path.clone(), - status: *status, - }), - (), - ); - let (merge_details, merge_heads_changed) = - MergeDetails::load(&backend, &statuses_by_path, &prev_snapshot).await?; - log::debug!("new merge details (changed={merge_heads_changed:?}): {merge_details:?}"); - - if merge_heads_changed { - events.push(RepositoryEvent::MergeHeadsChanged); - } - - if statuses_by_path != prev_snapshot.statuses_by_path { - events.push(RepositoryEvent::StatusesChanged) - } - - // Useful when branch is None in detached head state - let head_commit = match backend.head_sha().await { - Some(head_sha) => backend.show(head_sha).await.log_err(), - None => None, - }; - - if branch != prev_snapshot.branch || head_commit != prev_snapshot.head_commit { - events.push(RepositoryEvent::BranchChanged); - } - - let remote_origin_url = backend.remote_url("origin").await; - let remote_upstream_url = backend.remote_url("upstream").await; - - let snapshot = RepositorySnapshot { - id, - statuses_by_path, - work_directory_abs_path, - path_style: prev_snapshot.path_style, - scan_id: prev_snapshot.scan_id + 1, - branch, - head_commit, - merge: merge_details, - remote_origin_url, - remote_upstream_url, - stash_entries, - }; - - Ok((snapshot, events)) -} - -fn status_from_proto( - simple_status: i32, - status: Option, -) -> anyhow::Result { - use proto::git_file_status::Variant; - - let Some(variant) = status.and_then(|status| status.variant) else { - let code = proto::GitStatus::from_i32(simple_status) - .with_context(|| format!("Invalid git status code: {simple_status}"))?; - let result = match code { - proto::GitStatus::Added => TrackedStatus { - worktree_status: StatusCode::Added, - index_status: StatusCode::Unmodified, - } - .into(), - proto::GitStatus::Modified => TrackedStatus { - worktree_status: StatusCode::Modified, - index_status: StatusCode::Unmodified, - } - .into(), - proto::GitStatus::Conflict => UnmergedStatus { - first_head: UnmergedStatusCode::Updated, - second_head: UnmergedStatusCode::Updated, - } - .into(), - proto::GitStatus::Deleted => TrackedStatus { - worktree_status: StatusCode::Deleted, - index_status: StatusCode::Unmodified, - } - .into(), - _ => anyhow::bail!("Invalid code for simple status: {simple_status}"), - }; - return Ok(result); - }; - - let result = match variant { - Variant::Untracked(_) => FileStatus::Untracked, - Variant::Ignored(_) => FileStatus::Ignored, - Variant::Unmerged(unmerged) => { - let [first_head, second_head] = - [unmerged.first_head, unmerged.second_head].map(|head| { - let code = proto::GitStatus::from_i32(head) - .with_context(|| format!("Invalid git status code: {head}"))?; - let result = match code { - proto::GitStatus::Added => UnmergedStatusCode::Added, - proto::GitStatus::Updated => UnmergedStatusCode::Updated, - proto::GitStatus::Deleted => UnmergedStatusCode::Deleted, - _ => anyhow::bail!("Invalid code for unmerged status: {code:?}"), - }; - Ok(result) - }); - let [first_head, second_head] = [first_head?, second_head?]; - UnmergedStatus { - first_head, - second_head, - } - .into() - } - Variant::Tracked(tracked) => { - let [index_status, worktree_status] = [tracked.index_status, tracked.worktree_status] - .map(|status| { - let code = proto::GitStatus::from_i32(status) - .with_context(|| format!("Invalid git status code: {status}"))?; - let result = match code { - proto::GitStatus::Modified => StatusCode::Modified, - proto::GitStatus::TypeChanged => StatusCode::TypeChanged, - proto::GitStatus::Added => StatusCode::Added, - proto::GitStatus::Deleted => StatusCode::Deleted, - proto::GitStatus::Renamed => StatusCode::Renamed, - proto::GitStatus::Copied => StatusCode::Copied, - proto::GitStatus::Unmodified => StatusCode::Unmodified, - _ => anyhow::bail!("Invalid code for tracked status: {code:?}"), - }; - Ok(result) - }); - let [index_status, worktree_status] = [index_status?, worktree_status?]; - TrackedStatus { - index_status, - worktree_status, - } - .into() - } - }; - Ok(result) -} - -fn status_to_proto(status: FileStatus) -> proto::GitFileStatus { - use proto::git_file_status::{Tracked, Unmerged, Variant}; - - let variant = match status { - FileStatus::Untracked => Variant::Untracked(Default::default()), - FileStatus::Ignored => Variant::Ignored(Default::default()), - FileStatus::Unmerged(UnmergedStatus { - first_head, - second_head, - }) => Variant::Unmerged(Unmerged { - first_head: unmerged_status_to_proto(first_head), - second_head: unmerged_status_to_proto(second_head), - }), - FileStatus::Tracked(TrackedStatus { - index_status, - worktree_status, - }) => Variant::Tracked(Tracked { - index_status: tracked_status_to_proto(index_status), - worktree_status: tracked_status_to_proto(worktree_status), - }), - }; - proto::GitFileStatus { - variant: Some(variant), - } -} - -fn unmerged_status_to_proto(code: UnmergedStatusCode) -> i32 { - match code { - UnmergedStatusCode::Added => proto::GitStatus::Added as _, - UnmergedStatusCode::Deleted => proto::GitStatus::Deleted as _, - UnmergedStatusCode::Updated => proto::GitStatus::Updated as _, - } -} - -fn tracked_status_to_proto(code: StatusCode) -> i32 { - match code { - StatusCode::Added => proto::GitStatus::Added as _, - StatusCode::Deleted => proto::GitStatus::Deleted as _, - StatusCode::Modified => proto::GitStatus::Modified as _, - StatusCode::Renamed => proto::GitStatus::Renamed as _, - StatusCode::TypeChanged => proto::GitStatus::TypeChanged as _, - StatusCode::Copied => proto::GitStatus::Copied as _, - StatusCode::Unmodified => proto::GitStatus::Unmodified as _, - } -} diff --git a/crates/project/src/git_store/branch_diff.rs b/crates/project/src/git_store/branch_diff.rs deleted file mode 100644 index dd0026961e..0000000000 --- a/crates/project/src/git_store/branch_diff.rs +++ /dev/null @@ -1,386 +0,0 @@ -use anyhow::Result; -use buffer_diff::BufferDiff; -use collections::HashSet; -use futures::StreamExt; -use git::{ - repository::RepoPath, - status::{DiffTreeType, FileStatus, StatusCode, TrackedStatus, TreeDiff, TreeDiffStatus}, -}; -use gpui::{ - App, AsyncWindowContext, Context, Entity, EventEmitter, SharedString, Subscription, Task, - WeakEntity, Window, -}; - -use language::Buffer; -use text::BufferId; -use util::ResultExt; -use ztracing::instrument; - -use crate::{ - Project, - git_store::{GitStoreEvent, Repository, RepositoryEvent}, -}; - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum DiffBase { - Head, - Merge { base_ref: SharedString }, -} - -impl DiffBase { - pub fn is_merge_base(&self) -> bool { - matches!(self, DiffBase::Merge { .. }) - } -} - -pub struct BranchDiff { - diff_base: DiffBase, - repo: Option>, - project: Entity, - base_commit: Option, - head_commit: Option, - tree_diff: Option, - _subscription: Subscription, - update_needed: postage::watch::Sender<()>, - _task: Task<()>, -} - -pub enum BranchDiffEvent { - FileListChanged, -} - -impl EventEmitter for BranchDiff {} - -impl BranchDiff { - pub fn new( - source: DiffBase, - project: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let git_store = project.read(cx).git_store().clone(); - let git_store_subscription = cx.subscribe_in( - &git_store, - window, - move |this, _git_store, event, _window, cx| match event { - GitStoreEvent::ActiveRepositoryChanged(_) - | GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::StatusesChanged, true) - | GitStoreEvent::ConflictsUpdated => { - cx.emit(BranchDiffEvent::FileListChanged); - *this.update_needed.borrow_mut() = (); - } - _ => {} - }, - ); - - let (send, recv) = postage::watch::channel::<()>(); - let worker = window.spawn(cx, { - let this = cx.weak_entity(); - async |cx| Self::handle_status_updates(this, recv, cx).await - }); - let repo = git_store.read(cx).active_repository(); - - Self { - diff_base: source, - repo, - project, - tree_diff: None, - base_commit: None, - head_commit: None, - _subscription: git_store_subscription, - _task: worker, - update_needed: send, - } - } - - pub fn diff_base(&self) -> &DiffBase { - &self.diff_base - } - - pub async fn handle_status_updates( - this: WeakEntity, - mut recv: postage::watch::Receiver<()>, - cx: &mut AsyncWindowContext, - ) { - Self::reload_tree_diff(this.clone(), cx).await.log_err(); - while recv.next().await.is_some() { - let Ok(needs_update) = this.update(cx, |this, cx| { - let mut needs_update = false; - let active_repo = this - .project - .read(cx) - .git_store() - .read(cx) - .active_repository(); - if active_repo != this.repo { - needs_update = true; - this.repo = active_repo; - } else if let Some(repo) = this.repo.as_ref() { - repo.update(cx, |repo, _| { - if let Some(branch) = &repo.branch - && let DiffBase::Merge { base_ref } = &this.diff_base - && let Some(commit) = branch.most_recent_commit.as_ref() - && &branch.ref_name == base_ref - && this.base_commit.as_ref() != Some(&commit.sha) - { - this.base_commit = Some(commit.sha.clone()); - needs_update = true; - } - - if repo.head_commit.as_ref().map(|c| &c.sha) != this.head_commit.as_ref() { - this.head_commit = repo.head_commit.as_ref().map(|c| c.sha.clone()); - needs_update = true; - } - }) - } - needs_update - }) else { - return; - }; - - if needs_update { - Self::reload_tree_diff(this.clone(), cx).await.log_err(); - } - } - } - - pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option { - let (repo, path) = self - .project - .read(cx) - .git_store() - .read(cx) - .repository_and_path_for_buffer_id(buffer_id, cx)?; - if self.repo() == Some(&repo) { - return self.merge_statuses( - repo.read(cx) - .status_for_path(&path) - .map(|status| status.status), - self.tree_diff - .as_ref() - .and_then(|diff| diff.entries.get(&path)), - ); - } - None - } - - pub fn merge_statuses( - &self, - diff_from_head: Option, - diff_from_merge_base: Option<&TreeDiffStatus>, - ) -> Option { - match (diff_from_head, diff_from_merge_base) { - (None, None) => None, - (Some(diff_from_head), None) => Some(diff_from_head), - (Some(diff_from_head @ FileStatus::Unmerged(_)), _) => Some(diff_from_head), - - // file does not exist in HEAD - // but *does* exist in work-tree - // and *does* exist in merge-base - ( - Some(FileStatus::Untracked) - | Some(FileStatus::Tracked(TrackedStatus { - index_status: StatusCode::Added, - worktree_status: _, - })), - Some(_), - ) => Some(FileStatus::Tracked(TrackedStatus { - index_status: StatusCode::Modified, - worktree_status: StatusCode::Modified, - })), - - // file exists in HEAD - // but *does not* exist in work-tree - (Some(diff_from_head), Some(diff_from_merge_base)) if diff_from_head.is_deleted() => { - match diff_from_merge_base { - TreeDiffStatus::Added => None, // unchanged, didn't exist in merge base or worktree - _ => Some(diff_from_head), - } - } - - // file exists in HEAD - // and *does* exist in work-tree - (Some(FileStatus::Tracked(_)), Some(tree_status)) => { - Some(FileStatus::Tracked(TrackedStatus { - index_status: match tree_status { - TreeDiffStatus::Added { .. } => StatusCode::Added, - _ => StatusCode::Modified, - }, - worktree_status: match tree_status { - TreeDiffStatus::Added => StatusCode::Added, - _ => StatusCode::Modified, - }, - })) - } - - (_, Some(diff_from_merge_base)) => { - Some(diff_status_to_file_status(diff_from_merge_base)) - } - } - } - - pub async fn reload_tree_diff( - this: WeakEntity, - cx: &mut AsyncWindowContext, - ) -> Result<()> { - let task = this.update(cx, |this, cx| { - let DiffBase::Merge { base_ref } = this.diff_base.clone() else { - return None; - }; - let Some(repo) = this.repo.as_ref() else { - this.tree_diff.take(); - return None; - }; - repo.update(cx, |repo, cx| { - Some(repo.diff_tree( - DiffTreeType::MergeBase { - base: base_ref, - head: "HEAD".into(), - }, - cx, - )) - }) - })?; - let Some(task) = task else { return Ok(()) }; - - let diff = task.await??; - this.update(cx, |this, cx| { - this.tree_diff = Some(diff); - cx.emit(BranchDiffEvent::FileListChanged); - cx.notify(); - }) - } - - pub fn repo(&self) -> Option<&Entity> { - self.repo.as_ref() - } - - #[instrument(skip_all)] - pub fn load_buffers(&mut self, cx: &mut Context) -> Vec { - let mut output = Vec::default(); - let Some(repo) = self.repo.clone() else { - return output; - }; - - self.project.update(cx, |_project, cx| { - let mut seen = HashSet::default(); - - for item in repo.read(cx).cached_status() { - seen.insert(item.repo_path.clone()); - let branch_diff = self - .tree_diff - .as_ref() - .and_then(|t| t.entries.get(&item.repo_path)) - .cloned(); - let Some(status) = self.merge_statuses(Some(item.status), branch_diff.as_ref()) - else { - continue; - }; - if !status.has_changes() { - continue; - } - - let Some(project_path) = - repo.read(cx).repo_path_to_project_path(&item.repo_path, cx) - else { - continue; - }; - let task = Self::load_buffer(branch_diff, project_path, repo.clone(), cx); - - output.push(DiffBuffer { - repo_path: item.repo_path.clone(), - load: task, - file_status: item.status, - }); - } - let Some(tree_diff) = self.tree_diff.as_ref() else { - return; - }; - - for (path, branch_diff) in tree_diff.entries.iter() { - if seen.contains(&path) { - continue; - } - - let Some(project_path) = repo.read(cx).repo_path_to_project_path(&path, cx) else { - continue; - }; - let task = - Self::load_buffer(Some(branch_diff.clone()), project_path, repo.clone(), cx); - - let file_status = diff_status_to_file_status(branch_diff); - - output.push(DiffBuffer { - repo_path: path.clone(), - load: task, - file_status, - }); - } - }); - output - } - - #[instrument(skip_all)] - fn load_buffer( - branch_diff: Option, - project_path: crate::ProjectPath, - repo: Entity, - cx: &Context<'_, Project>, - ) -> Task, Entity)>> { - let task = cx.spawn(async move |project, cx| { - let buffer = project - .update(cx, |project, cx| project.open_buffer(project_path, cx))? - .await?; - - let languages = project.update(cx, |project, _cx| project.languages().clone())?; - - let changes = if let Some(entry) = branch_diff { - let oid = match entry { - git::status::TreeDiffStatus::Added { .. } => None, - git::status::TreeDiffStatus::Modified { old, .. } - | git::status::TreeDiffStatus::Deleted { old } => Some(old), - }; - project - .update(cx, |project, cx| { - project.git_store().update(cx, |git_store, cx| { - git_store.open_diff_since(oid, buffer.clone(), repo, languages, cx) - }) - })? - .await? - } else { - project - .update(cx, |project, cx| { - project.open_uncommitted_diff(buffer.clone(), cx) - })? - .await? - }; - Ok((buffer, changes)) - }); - task - } -} - -fn diff_status_to_file_status(branch_diff: &git::status::TreeDiffStatus) -> FileStatus { - let file_status = match branch_diff { - git::status::TreeDiffStatus::Added { .. } => FileStatus::Tracked(TrackedStatus { - index_status: StatusCode::Added, - worktree_status: StatusCode::Added, - }), - git::status::TreeDiffStatus::Modified { .. } => FileStatus::Tracked(TrackedStatus { - index_status: StatusCode::Modified, - worktree_status: StatusCode::Modified, - }), - git::status::TreeDiffStatus::Deleted { .. } => FileStatus::Tracked(TrackedStatus { - index_status: StatusCode::Deleted, - worktree_status: StatusCode::Deleted, - }), - }; - file_status -} - -#[derive(Debug)] -pub struct DiffBuffer { - pub repo_path: RepoPath, - pub file_status: FileStatus, - pub load: Task, Entity)>>, -} diff --git a/crates/project/src/git_store/conflict_set.rs b/crates/project/src/git_store/conflict_set.rs deleted file mode 100644 index 064b6998cd..0000000000 --- a/crates/project/src/git_store/conflict_set.rs +++ /dev/null @@ -1,742 +0,0 @@ -use gpui::{App, Context, Entity, EventEmitter, SharedString}; -use std::{cmp::Ordering, ops::Range, sync::Arc}; -use text::{Anchor, BufferId, OffsetRangeExt as _}; - -pub struct ConflictSet { - pub has_conflict: bool, - pub snapshot: ConflictSetSnapshot, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ConflictSetUpdate { - pub buffer_range: Option>, - pub old_range: Range, - pub new_range: Range, -} - -#[derive(Debug, Clone)] -pub struct ConflictSetSnapshot { - pub buffer_id: BufferId, - pub conflicts: Arc<[ConflictRegion]>, -} - -impl ConflictSetSnapshot { - pub fn conflicts_in_range( - &self, - range: Range, - buffer: &text::BufferSnapshot, - ) -> &[ConflictRegion] { - let start_ix = self - .conflicts - .binary_search_by(|conflict| { - conflict - .range - .end - .cmp(&range.start, buffer) - .then(Ordering::Greater) - }) - .unwrap_err(); - let end_ix = start_ix - + self.conflicts[start_ix..] - .binary_search_by(|conflict| { - conflict - .range - .start - .cmp(&range.end, buffer) - .then(Ordering::Less) - }) - .unwrap_err(); - &self.conflicts[start_ix..end_ix] - } - - pub fn compare(&self, other: &Self, buffer: &text::BufferSnapshot) -> ConflictSetUpdate { - let common_prefix_len = self - .conflicts - .iter() - .zip(other.conflicts.iter()) - .take_while(|(old, new)| old == new) - .count(); - let common_suffix_len = self.conflicts[common_prefix_len..] - .iter() - .rev() - .zip(other.conflicts[common_prefix_len..].iter().rev()) - .take_while(|(old, new)| old == new) - .count(); - let old_conflicts = - &self.conflicts[common_prefix_len..(self.conflicts.len() - common_suffix_len)]; - let new_conflicts = - &other.conflicts[common_prefix_len..(other.conflicts.len() - common_suffix_len)]; - let old_range = common_prefix_len..(common_prefix_len + old_conflicts.len()); - let new_range = common_prefix_len..(common_prefix_len + new_conflicts.len()); - let start = match (old_conflicts.first(), new_conflicts.first()) { - (None, None) => None, - (None, Some(conflict)) => Some(conflict.range.start), - (Some(conflict), None) => Some(conflict.range.start), - (Some(first), Some(second)) => { - Some(*first.range.start.min(&second.range.start, buffer)) - } - }; - let end = match (old_conflicts.last(), new_conflicts.last()) { - (None, None) => None, - (None, Some(conflict)) => Some(conflict.range.end), - (Some(first), None) => Some(first.range.end), - (Some(first), Some(second)) => Some(*first.range.end.max(&second.range.end, buffer)), - }; - ConflictSetUpdate { - buffer_range: start.zip(end).map(|(start, end)| start..end), - old_range, - new_range, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ConflictRegion { - pub ours_branch_name: SharedString, - pub theirs_branch_name: SharedString, - pub range: Range, - pub ours: Range, - pub theirs: Range, - pub base: Option>, -} - -impl ConflictRegion { - pub fn resolve( - &self, - buffer: Entity, - ranges: &[Range], - cx: &mut App, - ) { - let buffer_snapshot = buffer.read(cx).snapshot(); - let mut deletions = Vec::new(); - let empty = ""; - let outer_range = self.range.to_offset(&buffer_snapshot); - let mut offset = outer_range.start; - for kept_range in ranges { - let kept_range = kept_range.to_offset(&buffer_snapshot); - if kept_range.start > offset { - deletions.push((offset..kept_range.start, empty)); - } - offset = kept_range.end; - } - if outer_range.end > offset { - deletions.push((offset..outer_range.end, empty)); - } - - buffer.update(cx, |buffer, cx| { - buffer.edit(deletions, None, cx); - }); - } -} - -impl ConflictSet { - pub fn new(buffer_id: BufferId, has_conflict: bool, _: &mut Context) -> Self { - Self { - has_conflict, - snapshot: ConflictSetSnapshot { - buffer_id, - conflicts: Default::default(), - }, - } - } - - pub fn set_has_conflict(&mut self, has_conflict: bool, cx: &mut Context) -> bool { - if has_conflict != self.has_conflict { - self.has_conflict = has_conflict; - if !self.has_conflict { - cx.emit(ConflictSetUpdate { - buffer_range: None, - old_range: 0..self.snapshot.conflicts.len(), - new_range: 0..0, - }); - self.snapshot.conflicts = Default::default(); - } - true - } else { - false - } - } - - pub fn snapshot(&self) -> ConflictSetSnapshot { - self.snapshot.clone() - } - - pub fn set_snapshot( - &mut self, - snapshot: ConflictSetSnapshot, - update: ConflictSetUpdate, - cx: &mut Context, - ) { - self.snapshot = snapshot; - cx.emit(update); - } - - pub fn parse(buffer: &text::BufferSnapshot) -> ConflictSetSnapshot { - let mut conflicts = Vec::new(); - - let mut line_pos = 0; - let buffer_len = buffer.len(); - let mut lines = buffer.text_for_range(0..buffer_len).lines(); - - let mut conflict_start: Option = None; - let mut ours_start: Option = None; - let mut ours_end: Option = None; - let mut ours_branch_name: Option = None; - let mut base_start: Option = None; - let mut base_end: Option = None; - let mut theirs_start: Option = None; - let mut theirs_branch_name: Option = None; - - while let Some(line) = lines.next() { - let line_end = line_pos + line.len(); - - if let Some(branch_name) = line.strip_prefix("<<<<<<< ") { - // If we see a new conflict marker while already parsing one, - // abandon the previous one and start a new one - conflict_start = Some(line_pos); - ours_start = Some(line_end + 1); - - let branch_name = branch_name.trim(); - if !branch_name.is_empty() { - ours_branch_name = Some(SharedString::new(branch_name)); - } - } else if line.starts_with("||||||| ") - && conflict_start.is_some() - && ours_start.is_some() - { - ours_end = Some(line_pos); - base_start = Some(line_end + 1); - } else if line.starts_with("=======") - && conflict_start.is_some() - && ours_start.is_some() - { - // Set ours_end if not already set (would be set if we have base markers) - if ours_end.is_none() { - ours_end = Some(line_pos); - } else if base_start.is_some() { - base_end = Some(line_pos); - } - theirs_start = Some(line_end + 1); - } else if let Some(branch_name) = line.strip_prefix(">>>>>>> ") - && conflict_start.is_some() - && ours_start.is_some() - && ours_end.is_some() - && theirs_start.is_some() - { - let branch_name = branch_name.trim(); - if !branch_name.is_empty() { - theirs_branch_name = Some(SharedString::new(branch_name)); - } - - let theirs_end = line_pos; - let conflict_end = (line_end + 1).min(buffer_len); - - let range = buffer.anchor_after(conflict_start.unwrap()) - ..buffer.anchor_before(conflict_end); - let ours = buffer.anchor_after(ours_start.unwrap()) - ..buffer.anchor_before(ours_end.unwrap()); - let theirs = - buffer.anchor_after(theirs_start.unwrap())..buffer.anchor_before(theirs_end); - - let base = base_start - .zip(base_end) - .map(|(start, end)| buffer.anchor_after(start)..buffer.anchor_before(end)); - - conflicts.push(ConflictRegion { - ours_branch_name: ours_branch_name - .take() - .unwrap_or_else(|| SharedString::new_static("HEAD")), - theirs_branch_name: theirs_branch_name - .take() - .unwrap_or_else(|| SharedString::new_static("Origin")), - range, - ours, - theirs, - base, - }); - - conflict_start = None; - ours_start = None; - ours_end = None; - base_start = None; - base_end = None; - theirs_start = None; - } - - line_pos = line_end + 1; - } - - ConflictSetSnapshot { - conflicts: conflicts.into(), - buffer_id: buffer.remote_id(), - } - } -} - -impl EventEmitter for ConflictSet {} - -#[cfg(test)] -mod tests { - use std::sync::mpsc; - - use crate::Project; - - use super::*; - use fs::FakeFs; - use git::{ - repository::{RepoPath, repo_path}, - status::{UnmergedStatus, UnmergedStatusCode}, - }; - use gpui::{BackgroundExecutor, TestAppContext}; - use serde_json::json; - use text::{Buffer, BufferId, Point, ReplicaId, ToOffset as _}; - use unindent::Unindent as _; - use util::{path, rel_path::rel_path}; - - #[test] - fn test_parse_conflicts_in_buffer() { - // Create a buffer with conflict markers - let test_content = r#" - This is some text before the conflict. - <<<<<<< HEAD - This is our version - ======= - This is their version - >>>>>>> branch-name - - Another conflict: - <<<<<<< HEAD - Our second change - ||||||| merged common ancestors - Original content - ======= - Their second change - >>>>>>> branch-name - "# - .unindent(); - - let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content); - let snapshot = buffer.snapshot(); - - let conflict_snapshot = ConflictSet::parse(&snapshot); - assert_eq!(conflict_snapshot.conflicts.len(), 2); - - let first = &conflict_snapshot.conflicts[0]; - assert!(first.base.is_none()); - assert_eq!(first.ours_branch_name.as_ref(), "HEAD"); - assert_eq!(first.theirs_branch_name.as_ref(), "branch-name"); - let our_text = snapshot - .text_for_range(first.ours.clone()) - .collect::(); - let their_text = snapshot - .text_for_range(first.theirs.clone()) - .collect::(); - assert_eq!(our_text, "This is our version\n"); - assert_eq!(their_text, "This is their version\n"); - - let second = &conflict_snapshot.conflicts[1]; - assert!(second.base.is_some()); - assert_eq!(second.ours_branch_name.as_ref(), "HEAD"); - assert_eq!(second.theirs_branch_name.as_ref(), "branch-name"); - let our_text = snapshot - .text_for_range(second.ours.clone()) - .collect::(); - let their_text = snapshot - .text_for_range(second.theirs.clone()) - .collect::(); - let base_text = snapshot - .text_for_range(second.base.as_ref().unwrap().clone()) - .collect::(); - assert_eq!(our_text, "Our second change\n"); - assert_eq!(their_text, "Their second change\n"); - assert_eq!(base_text, "Original content\n"); - - // Test conflicts_in_range - let range = snapshot.anchor_before(0)..snapshot.anchor_before(snapshot.len()); - let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot); - assert_eq!(conflicts_in_range.len(), 2); - - // Test with a range that includes only the first conflict - let first_conflict_end = conflict_snapshot.conflicts[0].range.end; - let range = snapshot.anchor_before(0)..first_conflict_end; - let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot); - assert_eq!(conflicts_in_range.len(), 1); - - // Test with a range that includes only the second conflict - let second_conflict_start = conflict_snapshot.conflicts[1].range.start; - let range = second_conflict_start..snapshot.anchor_before(snapshot.len()); - let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot); - assert_eq!(conflicts_in_range.len(), 1); - - // Test with a range that doesn't include any conflicts - let range = buffer.anchor_after(first_conflict_end.to_next_offset(&buffer)) - ..buffer.anchor_before(second_conflict_start.to_previous_offset(&buffer)); - let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot); - assert_eq!(conflicts_in_range.len(), 0); - } - - #[test] - fn test_nested_conflict_markers() { - // Create a buffer with nested conflict markers - let test_content = r#" - This is some text before the conflict. - <<<<<<< HEAD - This is our version - <<<<<<< HEAD - This is a nested conflict marker - ======= - This is their version in a nested conflict - >>>>>>> branch-nested - ======= - This is their version - >>>>>>> branch-name - "# - .unindent(); - - let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content); - let snapshot = buffer.snapshot(); - - let conflict_snapshot = ConflictSet::parse(&snapshot); - - assert_eq!(conflict_snapshot.conflicts.len(), 1); - - // The conflict should have our version, their version, but no base - let conflict = &conflict_snapshot.conflicts[0]; - assert!(conflict.base.is_none()); - assert_eq!(conflict.ours_branch_name.as_ref(), "HEAD"); - assert_eq!(conflict.theirs_branch_name.as_ref(), "branch-nested"); - - // Check that the nested conflict was detected correctly - let our_text = snapshot - .text_for_range(conflict.ours.clone()) - .collect::(); - assert_eq!(our_text, "This is a nested conflict marker\n"); - let their_text = snapshot - .text_for_range(conflict.theirs.clone()) - .collect::(); - assert_eq!(their_text, "This is their version in a nested conflict\n"); - } - - #[test] - fn test_conflict_markers_at_eof() { - let test_content = r#" - <<<<<<< ours - ======= - This is their version - >>>>>>> "# - .unindent(); - let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content); - let snapshot = buffer.snapshot(); - - let conflict_snapshot = ConflictSet::parse(&snapshot); - assert_eq!(conflict_snapshot.conflicts.len(), 1); - assert_eq!( - conflict_snapshot.conflicts[0].ours_branch_name.as_ref(), - "ours" - ); - assert_eq!( - conflict_snapshot.conflicts[0].theirs_branch_name.as_ref(), - "Origin" // default branch name if there is none - ); - } - - #[test] - fn test_conflicts_in_range() { - // Create a buffer with conflict markers - let test_content = r#" - one - <<<<<<< HEAD1 - two - ======= - three - >>>>>>> branch1 - four - five - <<<<<<< HEAD2 - six - ======= - seven - >>>>>>> branch2 - eight - nine - <<<<<<< HEAD3 - ten - ======= - eleven - >>>>>>> branch3 - twelve - <<<<<<< HEAD4 - thirteen - ======= - fourteen - >>>>>>> branch4 - fifteen - "# - .unindent(); - - let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content.clone()); - let snapshot = buffer.snapshot(); - - let conflict_snapshot = ConflictSet::parse(&snapshot); - assert_eq!(conflict_snapshot.conflicts.len(), 4); - assert_eq!( - conflict_snapshot.conflicts[0].ours_branch_name.as_ref(), - "HEAD1" - ); - assert_eq!( - conflict_snapshot.conflicts[0].theirs_branch_name.as_ref(), - "branch1" - ); - assert_eq!( - conflict_snapshot.conflicts[1].ours_branch_name.as_ref(), - "HEAD2" - ); - assert_eq!( - conflict_snapshot.conflicts[1].theirs_branch_name.as_ref(), - "branch2" - ); - assert_eq!( - conflict_snapshot.conflicts[2].ours_branch_name.as_ref(), - "HEAD3" - ); - assert_eq!( - conflict_snapshot.conflicts[2].theirs_branch_name.as_ref(), - "branch3" - ); - assert_eq!( - conflict_snapshot.conflicts[3].ours_branch_name.as_ref(), - "HEAD4" - ); - assert_eq!( - conflict_snapshot.conflicts[3].theirs_branch_name.as_ref(), - "branch4" - ); - - let range = test_content.find("seven").unwrap()..test_content.find("eleven").unwrap(); - let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end); - assert_eq!( - conflict_snapshot.conflicts_in_range(range, &snapshot), - &conflict_snapshot.conflicts[1..=2] - ); - - let range = test_content.find("one").unwrap()..test_content.find("<<<<<<< HEAD2").unwrap(); - let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end); - assert_eq!( - conflict_snapshot.conflicts_in_range(range, &snapshot), - &conflict_snapshot.conflicts[0..=1] - ); - - let range = - test_content.find("eight").unwrap() - 1..test_content.find(">>>>>>> branch3").unwrap(); - let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end); - assert_eq!( - conflict_snapshot.conflicts_in_range(range, &snapshot), - &conflict_snapshot.conflicts[1..=2] - ); - - let range = test_content.find("thirteen").unwrap() - 1..test_content.len(); - let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end); - assert_eq!( - conflict_snapshot.conflicts_in_range(range, &snapshot), - &conflict_snapshot.conflicts[3..=3] - ); - } - - #[gpui::test] - async fn test_conflict_updates(executor: BackgroundExecutor, cx: &mut TestAppContext) { - zlog::init_test(); - cx.update(|cx| { - settings::init(cx); - }); - let initial_text = " - one - two - three - four - five - " - .unindent(); - let fs = FakeFs::new(executor); - fs.insert_tree( - path!("/project"), - json!({ - ".git": {}, - "a.txt": initial_text, - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let (git_store, buffer) = project.update(cx, |project, cx| { - ( - project.git_store().clone(), - project.open_local_buffer(path!("/project/a.txt"), cx), - ) - }); - let buffer = buffer.await.unwrap(); - let conflict_set = git_store.update(cx, |git_store, cx| { - git_store.open_conflict_set(buffer.clone(), cx) - }); - let (events_tx, events_rx) = mpsc::channel::(); - let _conflict_set_subscription = cx.update(|cx| { - cx.subscribe(&conflict_set, move |_, event, _| { - events_tx.send(event.clone()).ok(); - }) - }); - let conflicts_snapshot = - conflict_set.read_with(cx, |conflict_set, _| conflict_set.snapshot()); - assert!(conflicts_snapshot.conflicts.is_empty()); - - buffer.update(cx, |buffer, cx| { - buffer.edit( - [ - (4..4, "<<<<<<< HEAD\n"), - (14..14, "=======\nTWO\n>>>>>>> branch\n"), - ], - None, - cx, - ); - }); - - cx.run_until_parked(); - events_rx.try_recv().expect_err( - "no conflicts should be registered as long as the file's status is unchanged", - ); - - fs.with_git_state(path!("/project/.git").as_ref(), true, |state| { - state.unmerged_paths.insert( - repo_path("a.txt"), - UnmergedStatus { - first_head: UnmergedStatusCode::Updated, - second_head: UnmergedStatusCode::Updated, - }, - ); - // Cause the repository to emit MergeHeadsChanged. - state.refs.insert("MERGE_HEAD".into(), "123".into()) - }) - .unwrap(); - - cx.run_until_parked(); - let update = events_rx - .try_recv() - .expect("status change should trigger conflict parsing"); - assert_eq!(update.old_range, 0..0); - assert_eq!(update.new_range, 0..1); - - let conflict = conflict_set.read_with(cx, |conflict_set, _| { - conflict_set.snapshot().conflicts[0].clone() - }); - cx.update(|cx| { - conflict.resolve(buffer.clone(), std::slice::from_ref(&conflict.theirs), cx); - }); - - cx.run_until_parked(); - let update = events_rx - .try_recv() - .expect("conflicts should be removed after resolution"); - assert_eq!(update.old_range, 0..1); - assert_eq!(update.new_range, 0..0); - } - - #[gpui::test] - async fn test_conflict_updates_without_merge_head( - executor: BackgroundExecutor, - cx: &mut TestAppContext, - ) { - zlog::init_test(); - cx.update(|cx| { - settings::init(cx); - }); - - let initial_text = " - zero - <<<<<<< HEAD - one - ======= - two - >>>>>>> Stashed Changes - three - " - .unindent(); - - let fs = FakeFs::new(executor); - fs.insert_tree( - path!("/project"), - json!({ - ".git": {}, - "a.txt": initial_text, - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let (git_store, buffer) = project.update(cx, |project, cx| { - ( - project.git_store().clone(), - project.open_local_buffer(path!("/project/a.txt"), cx), - ) - }); - - cx.run_until_parked(); - fs.with_git_state(path!("/project/.git").as_ref(), true, |state| { - state.unmerged_paths.insert( - RepoPath::from_rel_path(rel_path("a.txt")), - UnmergedStatus { - first_head: UnmergedStatusCode::Updated, - second_head: UnmergedStatusCode::Updated, - }, - ) - }) - .unwrap(); - - let buffer = buffer.await.unwrap(); - - // Open the conflict set for a file that currently has conflicts. - let conflict_set = git_store.update(cx, |git_store, cx| { - git_store.open_conflict_set(buffer.clone(), cx) - }); - - cx.run_until_parked(); - conflict_set.update(cx, |conflict_set, cx| { - let conflict_range = conflict_set.snapshot().conflicts[0] - .range - .to_point(buffer.read(cx)); - assert_eq!(conflict_range, Point::new(1, 0)..Point::new(6, 0)); - }); - - // Simulate the conflict being removed by e.g. staging the file. - fs.with_git_state(path!("/project/.git").as_ref(), true, |state| { - state.unmerged_paths.remove(&repo_path("a.txt")) - }) - .unwrap(); - - cx.run_until_parked(); - conflict_set.update(cx, |conflict_set, _| { - assert!(!conflict_set.has_conflict); - assert_eq!(conflict_set.snapshot.conflicts.len(), 0); - }); - - // Simulate the conflict being re-added. - fs.with_git_state(path!("/project/.git").as_ref(), true, |state| { - state.unmerged_paths.insert( - repo_path("a.txt"), - UnmergedStatus { - first_head: UnmergedStatusCode::Updated, - second_head: UnmergedStatusCode::Updated, - }, - ) - }) - .unwrap(); - - cx.run_until_parked(); - conflict_set.update(cx, |conflict_set, cx| { - let conflict_range = conflict_set.snapshot().conflicts[0] - .range - .to_point(buffer.read(cx)); - assert_eq!(conflict_range, Point::new(1, 0)..Point::new(6, 0)); - }); - } -} diff --git a/crates/project/src/git_store/git_traversal.rs b/crates/project/src/git_store/git_traversal.rs deleted file mode 100644 index 39857951ad..0000000000 --- a/crates/project/src/git_store/git_traversal.rs +++ /dev/null @@ -1,793 +0,0 @@ -use collections::HashMap; -use git::{repository::RepoPath, status::GitSummary}; -use std::{collections::BTreeMap, ops::Deref, path::Path}; -use sum_tree::Cursor; -use text::Bias; -use util::rel_path::RelPath; -use worktree::{Entry, PathProgress, PathTarget, Traversal}; - -use super::{RepositoryId, RepositorySnapshot, StatusEntry}; - -/// Walks the worktree entries and their associated git statuses. -pub struct GitTraversal<'a> { - traversal: Traversal<'a>, - current_entry_summary: Option, - repo_root_to_snapshot: BTreeMap<&'a Path, &'a RepositorySnapshot>, - repo_location: Option<( - RepositoryId, - Cursor<'a, 'static, StatusEntry, PathProgress<'a>>, - )>, -} - -impl<'a> GitTraversal<'a> { - pub fn new( - repo_snapshots: &'a HashMap, - traversal: Traversal<'a>, - ) -> GitTraversal<'a> { - let repo_root_to_snapshot = repo_snapshots - .values() - .map(|snapshot| (&*snapshot.work_directory_abs_path, snapshot)) - .collect(); - let mut this = GitTraversal { - traversal, - current_entry_summary: None, - repo_location: None, - repo_root_to_snapshot, - }; - this.synchronize_statuses(true); - this - } - - fn repo_root_for_path(&self, path: &Path) -> Option<(&'a RepositorySnapshot, RepoPath)> { - // We might need to perform a range search multiple times, as there may be a nested repository inbetween - // the target and our path. E.g: - // /our_root_repo/ - // .git/ - // other_repo/ - // .git/ - // our_query.txt - let query = path.ancestors(); - for query in query { - let (_, snapshot) = self - .repo_root_to_snapshot - .range(Path::new("")..=query) - .last()?; - - let stripped = snapshot - .abs_path_to_repo_path(path) - .map(|repo_path| (*snapshot, repo_path)); - if stripped.is_some() { - return stripped; - } - } - - None - } - - fn synchronize_statuses(&mut self, reset: bool) { - self.current_entry_summary = None; - - let Some(entry) = self.entry() else { - return; - }; - - let abs_path = self.traversal.snapshot().absolutize(&entry.path); - - let Some((repo, repo_path)) = self.repo_root_for_path(&abs_path) else { - self.repo_location = None; - return; - }; - - // Update our state if we changed repositories. - if reset - || self - .repo_location - .as_ref() - .map(|(prev_repo_id, _)| *prev_repo_id) - != Some(repo.id) - { - self.repo_location = Some((repo.id, repo.statuses_by_path.cursor::(()))); - } - - let Some((_, statuses)) = &mut self.repo_location else { - return; - }; - - if entry.is_dir() { - let mut statuses = statuses.clone(); - statuses.seek_forward(&PathTarget::Path(&repo_path), Bias::Left); - let summary = statuses.summary(&PathTarget::Successor(&repo_path), Bias::Left); - - self.current_entry_summary = Some(summary); - } else if entry.is_file() { - // For a file entry, park the cursor on the corresponding status - if statuses.seek_forward(&PathTarget::Path(&repo_path), Bias::Left) { - // TODO: Investigate statuses.item() being None here. - self.current_entry_summary = statuses.item().map(|item| item.status.into()); - } else { - self.current_entry_summary = Some(GitSummary::UNCHANGED); - } - } - } - - pub fn advance(&mut self) -> bool { - let found = self.traversal.advance_by(1); - self.synchronize_statuses(false); - found - } - - pub fn advance_to_sibling(&mut self) -> bool { - let found = self.traversal.advance_to_sibling(); - self.synchronize_statuses(false); - found - } - - pub fn back_to_parent(&mut self) -> bool { - let found = self.traversal.back_to_parent(); - self.synchronize_statuses(true); - found - } - - pub fn start_offset(&self) -> usize { - self.traversal.start_offset() - } - - pub fn end_offset(&self) -> usize { - self.traversal.end_offset() - } - - pub fn entry(&self) -> Option> { - let entry = self.traversal.entry()?; - let git_summary = self.current_entry_summary.unwrap_or(GitSummary::UNCHANGED); - Some(GitEntryRef { entry, git_summary }) - } -} - -impl<'a> Iterator for GitTraversal<'a> { - type Item = GitEntryRef<'a>; - - fn next(&mut self) -> Option { - if let Some(item) = self.entry() { - self.advance(); - Some(item) - } else { - None - } - } -} - -pub struct ChildEntriesGitIter<'a> { - parent_path: &'a RelPath, - traversal: GitTraversal<'a>, -} - -impl<'a> ChildEntriesGitIter<'a> { - pub fn new( - repo_snapshots: &'a HashMap, - worktree_snapshot: &'a worktree::Snapshot, - parent_path: &'a RelPath, - ) -> Self { - let mut traversal = GitTraversal::new( - repo_snapshots, - worktree_snapshot.traverse_from_path(true, true, true, parent_path), - ); - traversal.advance(); - ChildEntriesGitIter { - parent_path, - traversal, - } - } -} - -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); - } - None - } -} - -#[derive(Debug, Clone, Copy)] -pub struct GitEntryRef<'a> { - pub entry: &'a Entry, - pub git_summary: GitSummary, -} - -impl GitEntryRef<'_> { - pub fn to_owned(self) -> GitEntry { - GitEntry { - entry: self.entry.clone(), - git_summary: self.git_summary, - } - } -} - -impl Deref for GitEntryRef<'_> { - type Target = Entry; - - fn deref(&self) -> &Self::Target { - self.entry - } -} - -impl AsRef for GitEntryRef<'_> { - fn as_ref(&self) -> &Entry { - self.entry - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct GitEntry { - pub entry: Entry, - pub git_summary: GitSummary, -} - -impl GitEntry { - pub fn to_ref(&self) -> GitEntryRef<'_> { - GitEntryRef { - entry: &self.entry, - git_summary: self.git_summary, - } - } -} - -impl Deref for GitEntry { - type Target = Entry; - - fn deref(&self) -> &Self::Target { - &self.entry - } -} - -impl AsRef for GitEntry { - fn as_ref(&self) -> &Entry { - &self.entry - } -} - -#[cfg(test)] -mod tests { - use std::time::Duration; - - use crate::Project; - - use super::*; - use fs::FakeFs; - use git::status::{FileStatus, StatusCode, TrackedSummary, UnmergedStatus, UnmergedStatusCode}; - use gpui::TestAppContext; - use serde_json::json; - use settings::SettingsStore; - use util::{path, rel_path::rel_path}; - - const CONFLICT: FileStatus = FileStatus::Unmerged(UnmergedStatus { - first_head: UnmergedStatusCode::Updated, - second_head: UnmergedStatusCode::Updated, - }); - const ADDED: GitSummary = GitSummary { - index: TrackedSummary::ADDED, - count: 1, - ..GitSummary::UNCHANGED - }; - const MODIFIED: GitSummary = GitSummary { - index: TrackedSummary::MODIFIED, - count: 1, - ..GitSummary::UNCHANGED - }; - - #[gpui::test] - async fn test_git_traversal_with_one_repo(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/root"), - json!({ - "x": { - ".git": {}, - "x1.txt": "foo", - "x2.txt": "bar", - "y": { - ".git": {}, - "y1.txt": "baz", - "y2.txt": "qux" - }, - "z.txt": "sneaky..." - }, - "z": { - ".git": {}, - "z1.txt": "quux", - "z2.txt": "quuux" - } - }), - ) - .await; - - fs.set_status_for_repo( - Path::new(path!("/root/x/.git")), - &[ - ("x2.txt", StatusCode::Modified.index()), - ("z.txt", StatusCode::Added.index()), - ], - ); - fs.set_status_for_repo(Path::new(path!("/root/x/y/.git")), &[("y1.txt", CONFLICT)]); - fs.set_status_for_repo( - Path::new(path!("/root/z/.git")), - &[("z2.txt", StatusCode::Added.index())], - ); - - let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - cx.executor().run_until_parked(); - - let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| { - ( - project.git_store().read(cx).repo_snapshots(cx), - project.worktrees(cx).next().unwrap().read(cx).snapshot(), - ) - }); - - let traversal = GitTraversal::new( - &repo_snapshots, - worktree_snapshot.traverse_from_path(true, false, true, RelPath::unix("x").unwrap()), - ); - let entries = traversal - .map(|entry| (entry.path.clone(), entry.git_summary)) - .collect::>(); - pretty_assertions::assert_eq!( - entries, - [ - (rel_path("x/x1.txt").into(), GitSummary::UNCHANGED), - (rel_path("x/x2.txt").into(), MODIFIED), - (rel_path("x/y/y1.txt").into(), GitSummary::CONFLICT), - (rel_path("x/y/y2.txt").into(), GitSummary::UNCHANGED), - (rel_path("x/z.txt").into(), ADDED), - (rel_path("z/z1.txt").into(), GitSummary::UNCHANGED), - (rel_path("z/z2.txt").into(), ADDED), - ] - ) - } - - #[gpui::test] - async fn test_git_traversal_with_nested_repos(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/root"), - json!({ - "x": { - ".git": {}, - "x1.txt": "foo", - "x2.txt": "bar", - "y": { - ".git": {}, - "y1.txt": "baz", - "y2.txt": "qux" - }, - "z.txt": "sneaky..." - }, - "z": { - ".git": {}, - "z1.txt": "quux", - "z2.txt": "quuux" - } - }), - ) - .await; - - fs.set_status_for_repo( - Path::new(path!("/root/x/.git")), - &[ - ("x2.txt", StatusCode::Modified.index()), - ("z.txt", StatusCode::Added.index()), - ], - ); - fs.set_status_for_repo(Path::new(path!("/root/x/y/.git")), &[("y1.txt", CONFLICT)]); - - fs.set_status_for_repo( - Path::new(path!("/root/z/.git")), - &[("z2.txt", StatusCode::Added.index())], - ); - - let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - cx.executor().run_until_parked(); - - let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| { - ( - project.git_store().read(cx).repo_snapshots(cx), - project.worktrees(cx).next().unwrap().read(cx).snapshot(), - ) - }); - - // Sanity check the propagation for x/y and z - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("x/y", GitSummary::CONFLICT), - ("x/y/y1.txt", GitSummary::CONFLICT), - ("x/y/y2.txt", GitSummary::UNCHANGED), - ], - ); - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("z", ADDED), - ("z/z1.txt", GitSummary::UNCHANGED), - ("z/z2.txt", ADDED), - ], - ); - - // Test one of the fundamental cases of propagation blocking, the transition from one git repository to another - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("x", MODIFIED + ADDED), - ("x/y", GitSummary::CONFLICT), - ("x/y/y1.txt", GitSummary::CONFLICT), - ], - ); - - // Sanity check everything around it - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("x", MODIFIED + ADDED), - ("x/x1.txt", GitSummary::UNCHANGED), - ("x/x2.txt", MODIFIED), - ("x/y", GitSummary::CONFLICT), - ("x/y/y1.txt", GitSummary::CONFLICT), - ("x/y/y2.txt", GitSummary::UNCHANGED), - ("x/z.txt", ADDED), - ], - ); - - // Test the other fundamental case, transitioning from git repository to non-git repository - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("", GitSummary::UNCHANGED), - ("x", MODIFIED + ADDED), - ("x/x1.txt", GitSummary::UNCHANGED), - ], - ); - - // And all together now - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("", GitSummary::UNCHANGED), - ("x", MODIFIED + ADDED), - ("x/x1.txt", GitSummary::UNCHANGED), - ("x/x2.txt", MODIFIED), - ("x/y", GitSummary::CONFLICT), - ("x/y/y1.txt", GitSummary::CONFLICT), - ("x/y/y2.txt", GitSummary::UNCHANGED), - ("x/z.txt", ADDED), - ("z", ADDED), - ("z/z1.txt", GitSummary::UNCHANGED), - ("z/z2.txt", ADDED), - ], - ); - } - - #[gpui::test] - async fn test_git_traversal_simple(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/root"), - json!({ - ".git": {}, - "a": { - "b": { - "c1.txt": "", - "c2.txt": "", - }, - "d": { - "e1.txt": "", - "e2.txt": "", - "e3.txt": "", - } - }, - "f": { - "no-status.txt": "" - }, - "g": { - "h1.txt": "", - "h2.txt": "" - }, - }), - ) - .await; - - fs.set_status_for_repo( - Path::new(path!("/root/.git")), - &[ - ("a/b/c1.txt", StatusCode::Added.index()), - ("a/d/e2.txt", StatusCode::Modified.index()), - ("g/h2.txt", CONFLICT), - ], - ); - - let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - cx.executor().run_until_parked(); - - let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| { - ( - project.git_store().read(cx).repo_snapshots(cx), - project.worktrees(cx).next().unwrap().read(cx).snapshot(), - ) - }); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("", GitSummary::CONFLICT + MODIFIED + ADDED), - ("g", GitSummary::CONFLICT), - ("g/h2.txt", GitSummary::CONFLICT), - ], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("", GitSummary::CONFLICT + ADDED + MODIFIED), - ("a", ADDED + MODIFIED), - ("a/b", ADDED), - ("a/b/c1.txt", ADDED), - ("a/b/c2.txt", GitSummary::UNCHANGED), - ("a/d", MODIFIED), - ("a/d/e2.txt", MODIFIED), - ("f", GitSummary::UNCHANGED), - ("f/no-status.txt", GitSummary::UNCHANGED), - ("g", GitSummary::CONFLICT), - ("g/h2.txt", GitSummary::CONFLICT), - ], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("a/b", ADDED), - ("a/b/c1.txt", ADDED), - ("a/b/c2.txt", GitSummary::UNCHANGED), - ("a/d", MODIFIED), - ("a/d/e1.txt", GitSummary::UNCHANGED), - ("a/d/e2.txt", MODIFIED), - ("f", GitSummary::UNCHANGED), - ("f/no-status.txt", GitSummary::UNCHANGED), - ("g", GitSummary::CONFLICT), - ], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("a/b/c1.txt", ADDED), - ("a/b/c2.txt", GitSummary::UNCHANGED), - ("a/d/e1.txt", GitSummary::UNCHANGED), - ("a/d/e2.txt", MODIFIED), - ("f/no-status.txt", GitSummary::UNCHANGED), - ], - ); - } - - #[gpui::test] - async fn test_git_traversal_with_repos_under_project(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/root"), - json!({ - "x": { - ".git": {}, - "x1.txt": "foo", - "x2.txt": "bar" - }, - "y": { - ".git": {}, - "y1.txt": "baz", - "y2.txt": "qux" - }, - "z": { - ".git": {}, - "z1.txt": "quux", - "z2.txt": "quuux" - } - }), - ) - .await; - - fs.set_status_for_repo( - Path::new(path!("/root/x/.git")), - &[("x1.txt", StatusCode::Added.index())], - ); - fs.set_status_for_repo( - Path::new(path!("/root/y/.git")), - &[ - ("y1.txt", CONFLICT), - ("y2.txt", StatusCode::Modified.index()), - ], - ); - fs.set_status_for_repo( - Path::new(path!("/root/z/.git")), - &[("z2.txt", StatusCode::Modified.index())], - ); - - let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - cx.executor().run_until_parked(); - - let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| { - ( - project.git_store().read(cx).repo_snapshots(cx), - project.worktrees(cx).next().unwrap().read(cx).snapshot(), - ) - }); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[("x", ADDED), ("x/x1.txt", ADDED)], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("y", GitSummary::CONFLICT + MODIFIED), - ("y/y1.txt", GitSummary::CONFLICT), - ("y/y2.txt", MODIFIED), - ], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[("z", MODIFIED), ("z/z2.txt", MODIFIED)], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[("x", ADDED), ("x/x1.txt", ADDED)], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("x", ADDED), - ("x/x1.txt", ADDED), - ("x/x2.txt", GitSummary::UNCHANGED), - ("y", GitSummary::CONFLICT + MODIFIED), - ("y/y1.txt", GitSummary::CONFLICT), - ("y/y2.txt", MODIFIED), - ("z", MODIFIED), - ("z/z1.txt", GitSummary::UNCHANGED), - ("z/z2.txt", MODIFIED), - ], - ); - } - - fn init_test(cx: &mut gpui::TestAppContext) { - zlog::init_test(); - - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - }); - } - - #[gpui::test] - async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) { - init_test(cx); - - // Create a worktree with a git directory. - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/root"), - json!({ - ".git": {}, - "a.txt": "", - "b": { - "c.txt": "", - }, - }), - ) - .await; - fs.set_head_and_index_for_repo( - path!("/root/.git").as_ref(), - &[("a.txt", "".into()), ("b/c.txt", "".into())], - ); - cx.run_until_parked(); - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - cx.executor().run_until_parked(); - - let (old_entry_ids, old_mtimes) = project.read_with(cx, |project, cx| { - let tree = project.worktrees(cx).next().unwrap().read(cx); - ( - tree.entries(true, 0).map(|e| e.id).collect::>(), - tree.entries(true, 0).map(|e| e.mtime).collect::>(), - ) - }); - - // Regression test: after the directory is scanned, touch the git repo's - // working directory, bumping its mtime. That directory keeps its project - // entry id after the directories are re-scanned. - fs.touch_path(path!("/root")).await; - cx.executor().run_until_parked(); - - let (new_entry_ids, new_mtimes) = project.read_with(cx, |project, cx| { - let tree = project.worktrees(cx).next().unwrap().read(cx); - ( - tree.entries(true, 0).map(|e| e.id).collect::>(), - tree.entries(true, 0).map(|e| e.mtime).collect::>(), - ) - }); - assert_eq!(new_entry_ids, old_entry_ids); - assert_ne!(new_mtimes, old_mtimes); - - // Regression test: changes to the git repository should still be - // detected. - fs.set_head_for_repo( - path!("/root/.git").as_ref(), - &[("a.txt", "".into()), ("b/c.txt", "something-else".into())], - "deadbeef", - ); - cx.executor().run_until_parked(); - cx.executor().advance_clock(Duration::from_secs(1)); - - let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| { - ( - project.git_store().read(cx).repo_snapshots(cx), - project.worktrees(cx).next().unwrap().read(cx).snapshot(), - ) - }); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("", MODIFIED), - ("a.txt", GitSummary::UNCHANGED), - ("b/c.txt", MODIFIED), - ], - ); - } - - #[track_caller] - fn check_git_statuses( - repo_snapshots: &HashMap, - worktree_snapshot: &worktree::Snapshot, - expected_statuses: &[(&str, GitSummary)], - ) { - let mut traversal = GitTraversal::new( - repo_snapshots, - worktree_snapshot.traverse_from_path(true, true, false, RelPath::empty()), - ); - let found_statuses = expected_statuses - .iter() - .map(|&(path, _)| { - let git_entry = traversal - .find(|git_entry| git_entry.path.as_ref() == rel_path(path)) - .unwrap_or_else(|| panic!("Traversal has no entry for {path:?}")); - (path, git_entry.git_summary) - }) - .collect::>(); - pretty_assertions::assert_eq!(found_statuses, expected_statuses); - } -} diff --git a/crates/project/src/git_store/pending_op.rs b/crates/project/src/git_store/pending_op.rs deleted file mode 100644 index 1991eed407..0000000000 --- a/crates/project/src/git_store/pending_op.rs +++ /dev/null @@ -1,147 +0,0 @@ -use git::repository::RepoPath; -use std::ops::Add; -use sum_tree::{ContextLessSummary, Item, KeyedItem}; -use worktree::{PathKey, PathSummary}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum GitStatus { - Staged, - Unstaged, - Reverted, - Unchanged, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum JobStatus { - Running, - Finished, - Skipped, - Error, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PendingOps { - pub repo_path: RepoPath, - pub ops: Vec, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct PendingOp { - pub id: PendingOpId, - pub git_status: GitStatus, - pub job_status: JobStatus, -} - -#[derive(Clone, Debug)] -pub struct PendingOpsSummary { - pub staged_count: usize, - pub staging_count: usize, -} - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct PendingOpId(pub u16); - -impl Item for PendingOps { - type Summary = PathSummary; - - fn summary(&self, _cx: ()) -> Self::Summary { - PathSummary { - max_path: self.repo_path.as_ref().clone(), - item_summary: PendingOpsSummary { - staged_count: self.staged() as usize, - staging_count: self.staging() as usize, - }, - } - } -} - -impl ContextLessSummary for PendingOpsSummary { - fn zero() -> Self { - Self { - staged_count: 0, - staging_count: 0, - } - } - - fn add_summary(&mut self, summary: &Self) { - self.staged_count += summary.staged_count; - self.staging_count += summary.staging_count; - } -} - -impl KeyedItem for PendingOps { - type Key = PathKey; - - fn key(&self) -> Self::Key { - PathKey(self.repo_path.as_ref().clone()) - } -} - -impl Add for PendingOpId { - type Output = PendingOpId; - - fn add(self, rhs: u16) -> Self::Output { - Self(self.0 + rhs) - } -} - -impl From for PendingOpId { - fn from(id: u16) -> Self { - Self(id) - } -} - -impl PendingOps { - pub fn new(path: &RepoPath) -> Self { - Self { - repo_path: path.clone(), - ops: Vec::new(), - } - } - - pub fn max_id(&self) -> PendingOpId { - self.ops.last().map(|op| op.id).unwrap_or_default() - } - - pub fn op_by_id(&self, id: PendingOpId) -> Option<&PendingOp> { - self.ops.iter().find(|op| op.id == id) - } - - pub fn op_by_id_mut(&mut self, id: PendingOpId) -> Option<&mut PendingOp> { - self.ops.iter_mut().find(|op| op.id == id) - } - - /// File is staged if the last job is finished and has status Staged. - pub fn staged(&self) -> bool { - if let Some(last) = self.ops.last() { - if last.git_status == GitStatus::Staged && last.job_status == JobStatus::Finished { - return true; - } - } - false - } - - /// File is staged if the last job is not finished and has status Staged. - pub fn staging(&self) -> bool { - if let Some(last) = self.ops.last() { - if last.git_status == GitStatus::Staged && last.job_status != JobStatus::Finished { - return true; - } - } - false - } -} - -impl PendingOp { - pub fn running(&self) -> bool { - self.job_status == JobStatus::Running - } - - pub fn finished(&self) -> bool { - matches!(self.job_status, JobStatus::Finished | JobStatus::Skipped) - } - - pub fn error(&self) -> bool { - self.job_status == JobStatus::Error - } -} diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs deleted file mode 100644 index 71bee30b99..0000000000 --- a/crates/project/src/image_store.rs +++ /dev/null @@ -1,987 +0,0 @@ -use crate::{ - Project, ProjectEntryId, ProjectItem, ProjectPath, - worktree_store::{WorktreeStore, WorktreeStoreEvent}, -}; -use anyhow::{Context as _, Result}; -use collections::{HashMap, HashSet, hash_map}; -use futures::{StreamExt, channel::oneshot}; -use gpui::{ - App, AsyncApp, Context, Entity, EventEmitter, Img, Subscription, Task, WeakEntity, prelude::*, -}; -pub use image::ImageFormat; -use image::{ExtendedColorType, GenericImageView, ImageReader}; -use language::{DiskState, File}; -use rpc::{AnyProtoClient, ErrorExt as _, TypedEnvelope, proto}; -use std::num::NonZeroU64; -use std::path::PathBuf; -use std::sync::Arc; -use util::{ResultExt, rel_path::RelPath}; -use worktree::{LoadedBinaryFile, PathChange, Worktree, WorktreeId}; - -#[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd, Ord, Eq)] -pub struct ImageId(NonZeroU64); - -impl ImageId { - pub fn to_proto(&self) -> u64 { - self.0.get() - } -} - -impl std::fmt::Display for ImageId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for ImageId { - fn from(id: NonZeroU64) -> Self { - ImageId(id) - } -} - -#[derive(Debug)] -pub enum ImageItemEvent { - ReloadNeeded, - Reloaded, - FileHandleChanged, - MetadataUpdated, -} - -impl EventEmitter for ImageItem {} - -pub enum ImageStoreEvent { - ImageAdded(Entity), -} - -impl EventEmitter for ImageStore {} - -#[derive(Debug, Clone, Copy)] -pub struct ImageMetadata { - pub width: u32, - pub height: u32, - pub file_size: u64, - pub colors: Option, - pub format: ImageFormat, -} - -#[derive(Debug, Clone, Copy)] -pub struct ImageColorInfo { - pub channels: u8, - pub bits_per_channel: u8, -} - -impl ImageColorInfo { - pub fn from_color_type(color_type: impl Into) -> Option { - let (channels, bits_per_channel) = match color_type.into() { - ExtendedColorType::L8 => (1, 8), - ExtendedColorType::L16 => (1, 16), - ExtendedColorType::La8 => (2, 8), - ExtendedColorType::La16 => (2, 16), - ExtendedColorType::Rgb8 => (3, 8), - ExtendedColorType::Rgb16 => (3, 16), - ExtendedColorType::Rgba8 => (4, 8), - ExtendedColorType::Rgba16 => (4, 16), - ExtendedColorType::A8 => (1, 8), - ExtendedColorType::Bgr8 => (3, 8), - ExtendedColorType::Bgra8 => (4, 8), - ExtendedColorType::Cmyk8 => (4, 8), - _ => return None, - }; - - Some(Self { - channels, - bits_per_channel, - }) - } - - pub const fn bits_per_pixel(&self) -> u8 { - self.channels * self.bits_per_channel - } -} - -pub struct ImageItem { - pub id: ImageId, - pub file: Arc, - pub image: Arc, - reload_task: Option>, - pub image_metadata: Option, -} - -impl ImageItem { - fn compute_metadata_from_bytes(image_bytes: &[u8]) -> Result { - let image_format = image::guess_format(image_bytes)?; - - let mut image_reader = ImageReader::new(std::io::Cursor::new(image_bytes)); - image_reader.set_format(image_format); - let image = image_reader.decode()?; - - let (width, height) = image.dimensions(); - - Ok(ImageMetadata { - width, - height, - file_size: image_bytes.len() as u64, - format: image_format, - colors: ImageColorInfo::from_color_type(image.color()), - }) - } - - pub async fn load_image_metadata( - image: Entity, - project: Entity, - cx: &mut AsyncApp, - ) -> Result { - let (fs, image_path) = cx.update(|cx| { - let fs = project.read(cx).fs().clone(); - let image_path = image - .read(cx) - .abs_path(cx) - .context("absolutizing image file path")?; - anyhow::Ok((fs, image_path)) - })??; - - let image_bytes = fs.load_bytes(&image_path).await?; - Self::compute_metadata_from_bytes(&image_bytes) - } - - pub fn project_path(&self, cx: &App) -> ProjectPath { - ProjectPath { - worktree_id: self.file.worktree_id(cx), - path: self.file.path().clone(), - } - } - - pub fn abs_path(&self, cx: &App) -> Option { - Some(self.file.as_local()?.abs_path(cx)) - } - - fn file_updated(&mut self, new_file: Arc, cx: &mut Context) { - let mut file_changed = false; - - let old_file = &self.file; - if new_file.path() != old_file.path() { - file_changed = true; - } - - let old_state = old_file.disk_state(); - let new_state = new_file.disk_state(); - if old_state != new_state { - file_changed = true; - if matches!(new_state, DiskState::Present { .. }) { - cx.emit(ImageItemEvent::ReloadNeeded) - } - } - - self.file = new_file; - if file_changed { - cx.emit(ImageItemEvent::FileHandleChanged); - cx.notify(); - } - } - - fn reload(&mut self, cx: &mut Context) -> Option> { - let local_file = self.file.as_local()?; - let (tx, rx) = futures::channel::oneshot::channel(); - - let content = local_file.load_bytes(cx); - self.reload_task = Some(cx.spawn(async move |this, cx| { - if let Some(image) = content - .await - .context("Failed to load image content") - .and_then(create_gpui_image) - .log_err() - { - this.update(cx, |this, cx| { - this.image = image; - cx.emit(ImageItemEvent::Reloaded); - }) - .log_err(); - } - _ = tx.send(()); - })); - Some(rx) - } -} - -pub fn is_image_file(project: &Entity, path: &ProjectPath, cx: &App) -> bool { - let ext = util::maybe!({ - let worktree_abs_path = project - .read(cx) - .worktree_for_id(path.worktree_id, cx)? - .read(cx) - .abs_path(); - path.path - .extension() - .or_else(|| worktree_abs_path.extension()?.to_str()) - .map(str::to_lowercase) - }); - - match ext { - Some(ext) => Img::extensions().contains(&ext.as_str()) && !ext.contains("svg"), - None => false, - } -} - -impl ProjectItem for ImageItem { - fn try_open( - project: &Entity, - path: &ProjectPath, - cx: &mut App, - ) -> Option>>> { - if is_image_file(project, path, cx) { - Some(cx.spawn({ - let path = path.clone(); - let project = project.clone(); - async move |cx| { - project - .update(cx, |project, cx| project.open_image(path, cx))? - .await - } - })) - } else { - None - } - } - - fn entry_id(&self, _: &App) -> Option { - self.file.entry_id - } - - fn project_path(&self, cx: &App) -> Option { - Some(self.project_path(cx)) - } - - fn is_dirty(&self) -> bool { - false - } -} - -trait ImageStoreImpl { - fn open_image( - &self, - path: Arc, - worktree: Entity, - cx: &mut Context, - ) -> Task>>; - - fn reload_images( - &self, - images: HashSet>, - cx: &mut Context, - ) -> Task>; - - fn as_local(&self) -> Option>; - fn as_remote(&self) -> Option>; -} - -struct RemoteImageStore { - upstream_client: AnyProtoClient, - project_id: u64, - loading_remote_images_by_id: HashMap, - remote_image_listeners: - HashMap>>>>, - loaded_images: HashMap>, -} - -struct LoadingRemoteImage { - state: proto::ImageState, - chunks: Vec>, - received_size: u64, -} - -struct LocalImageStore { - local_image_ids_by_path: HashMap, - local_image_ids_by_entry_id: HashMap, - image_store: WeakEntity, - _subscription: Subscription, -} - -pub struct ImageStore { - state: Box, - opened_images: HashMap>, - worktree_store: Entity, - #[allow(clippy::type_complexity)] - loading_images_by_path: HashMap< - ProjectPath, - postage::watch::Receiver, Arc>>>, - >, -} - -impl ImageStore { - pub fn local(worktree_store: Entity, cx: &mut Context) -> Self { - let this = cx.weak_entity(); - Self { - state: Box::new(cx.new(|cx| { - let subscription = cx.subscribe( - &worktree_store, - |this: &mut LocalImageStore, _, event, cx| { - if let WorktreeStoreEvent::WorktreeAdded(worktree) = event { - this.subscribe_to_worktree(worktree, cx); - } - }, - ); - - LocalImageStore { - local_image_ids_by_path: Default::default(), - local_image_ids_by_entry_id: Default::default(), - image_store: this, - _subscription: subscription, - } - })), - opened_images: Default::default(), - loading_images_by_path: Default::default(), - worktree_store, - } - } - - pub fn remote( - worktree_store: Entity, - upstream_client: AnyProtoClient, - project_id: u64, - cx: &mut Context, - ) -> Self { - Self { - state: Box::new(cx.new(|_| RemoteImageStore { - upstream_client, - project_id, - loading_remote_images_by_id: Default::default(), - remote_image_listeners: Default::default(), - loaded_images: Default::default(), - })), - opened_images: Default::default(), - loading_images_by_path: Default::default(), - worktree_store, - } - } - - pub fn images(&self) -> impl '_ + Iterator> { - self.opened_images - .values() - .filter_map(|image| image.upgrade()) - } - - pub fn get(&self, image_id: ImageId) -> Option> { - self.opened_images - .get(&image_id) - .and_then(|image| image.upgrade()) - } - - pub fn get_by_path(&self, path: &ProjectPath, cx: &App) -> Option> { - self.images() - .find(|image| &image.read(cx).project_path(cx) == path) - } - - pub fn open_image( - &mut self, - project_path: ProjectPath, - cx: &mut Context, - ) -> Task>> { - let existing_image = self.get_by_path(&project_path, cx); - if let Some(existing_image) = existing_image { - return Task::ready(Ok(existing_image)); - } - - let Some(worktree) = self - .worktree_store - .read(cx) - .worktree_for_id(project_path.worktree_id, cx) - else { - return Task::ready(Err(anyhow::anyhow!("no such worktree"))); - }; - - let loading_watch = match self.loading_images_by_path.entry(project_path.clone()) { - // If the given path is already being loaded, then wait for that existing - // task to complete and return the same image. - hash_map::Entry::Occupied(e) => e.get().clone(), - - // Otherwise, record the fact that this path is now being loaded. - hash_map::Entry::Vacant(entry) => { - let (mut tx, rx) = postage::watch::channel(); - entry.insert(rx.clone()); - - let load_image = self - .state - .open_image(project_path.path.clone(), worktree, cx); - - cx.spawn(async move |this, cx| { - let load_result = load_image.await; - *tx.borrow_mut() = Some(this.update(cx, |this, _cx| { - // Record the fact that the image is no longer loading. - this.loading_images_by_path.remove(&project_path); - let image = load_result.map_err(Arc::new)?; - Ok(image) - })?); - anyhow::Ok(()) - }) - .detach(); - rx - } - }; - - cx.background_spawn(async move { - Self::wait_for_loading_image(loading_watch) - .await - .map_err(|e| e.cloned()) - }) - } - - pub async fn wait_for_loading_image( - mut receiver: postage::watch::Receiver< - Option, Arc>>, - >, - ) -> Result, Arc> { - loop { - if let Some(result) = receiver.borrow().as_ref() { - match result { - Ok(image) => return Ok(image.to_owned()), - Err(e) => return Err(e.to_owned()), - } - } - receiver.next().await; - } - } - - pub fn reload_images( - &self, - images: HashSet>, - cx: &mut Context, - ) -> Task> { - if images.is_empty() { - return Task::ready(Ok(())); - } - - self.state.reload_images(images, cx) - } - - fn add_image(&mut self, image: Entity, cx: &mut Context) -> Result<()> { - let image_id = image.read(cx).id; - self.opened_images.insert(image_id, image.downgrade()); - cx.subscribe(&image, Self::on_image_event).detach(); - cx.emit(ImageStoreEvent::ImageAdded(image)); - Ok(()) - } - - fn on_image_event( - &mut self, - image: Entity, - 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); - }) - } - } - - pub fn handle_create_image_for_peer( - &mut self, - envelope: TypedEnvelope, - cx: &mut Context, - ) -> Result<()> { - if let Some(remote) = self.state.as_remote() { - let worktree_store = self.worktree_store.clone(); - let image = remote.update(cx, |remote, cx| { - remote.handle_create_image_for_peer(envelope, &worktree_store, cx) - })?; - if let Some(image) = image { - remote.update(cx, |this, cx| { - let image = image.clone(); - let image_id = image.read(cx).id; - this.loaded_images.insert(image_id, image) - }); - - self.add_image(image, cx)?; - } - } - - Ok(()) - } -} - -impl RemoteImageStore { - pub fn wait_for_remote_image( - &mut self, - id: ImageId, - cx: &mut Context, - ) -> Task>> { - if let Some(image) = self.loaded_images.remove(&id) { - return Task::ready(Ok(image)); - } - - let (tx, rx) = oneshot::channel(); - self.remote_image_listeners.entry(id).or_default().push(tx); - - cx.spawn(async move |_this, cx| { - let result = cx.background_spawn(async move { rx.await? }).await; - result - }) - } - - pub fn handle_create_image_for_peer( - &mut self, - envelope: TypedEnvelope, - worktree_store: &Entity, - cx: &mut Context, - ) -> Result>> { - use proto::create_image_for_peer::Variant; - match envelope.payload.variant { - Some(Variant::State(state)) => { - let image_id = - ImageId::from(NonZeroU64::new(state.id).context("invalid image id")?); - - self.loading_remote_images_by_id.insert( - image_id, - LoadingRemoteImage { - state, - chunks: Vec::new(), - received_size: 0, - }, - ); - Ok(None) - } - Some(Variant::Chunk(chunk)) => { - let image_id = - ImageId::from(NonZeroU64::new(chunk.image_id).context("invalid image id")?); - - let loading = self - .loading_remote_images_by_id - .get_mut(&image_id) - .context("received chunk for unknown image")?; - - loading.received_size += chunk.data.len() as u64; - loading.chunks.push(chunk.data); - - if loading.received_size == loading.state.content_size { - let loading = self.loading_remote_images_by_id.remove(&image_id).unwrap(); - - let mut content = Vec::with_capacity(loading.received_size as usize); - for chunk_data in loading.chunks { - content.extend_from_slice(&chunk_data); - } - - let image_metadata = ImageItem::compute_metadata_from_bytes(&content).log_err(); - let image = create_gpui_image(content)?; - - let proto_file = loading.state.file.context("missing file in image state")?; - let worktree_id = WorktreeId::from_proto(proto_file.worktree_id); - let worktree = worktree_store - .read(cx) - .worktree_for_id(worktree_id, cx) - .context("worktree not found")?; - - let file = Arc::new( - worktree::File::from_proto(proto_file, worktree, cx) - .context("invalid file in image state")?, - ); - - let entity = cx.new(|_cx| ImageItem { - id: image_id, - file, - image, - image_metadata, - reload_task: None, - }); - - if let Some(listeners) = self.remote_image_listeners.remove(&image_id) { - for listener in listeners { - listener.send(Ok(entity.clone())).ok(); - } - } - - Ok(Some(entity)) - } else { - Ok(None) - } - } - None => { - log::warn!("Received CreateImageForPeer with no variant"); - Ok(None) - } - } - } - - // TODO: subscribe to worktree and update image contents or at least mark as dirty on file changes -} - -impl ImageStoreImpl for Entity { - fn open_image( - &self, - path: Arc, - worktree: Entity, - cx: &mut Context, - ) -> Task>> { - let this = self.clone(); - - let load_file = worktree.update(cx, |worktree, cx| { - worktree.load_binary_file(path.as_ref(), cx) - }); - cx.spawn(async move |image_store, cx| { - let LoadedBinaryFile { file, content } = load_file.await?; - let image = create_gpui_image(content)?; - - let entity = cx.new(|cx| ImageItem { - id: cx.entity_id().as_non_zero_u64().into(), - file: file.clone(), - image, - image_metadata: None, - reload_task: None, - })?; - - let image_id = cx.read_entity(&entity, |model, _| model.id)?; - - this.update(cx, |this, cx| { - image_store.update(cx, |image_store, cx| { - image_store.add_image(entity.clone(), cx) - })??; - this.local_image_ids_by_path.insert( - ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }, - image_id, - ); - - if let Some(entry_id) = file.entry_id { - this.local_image_ids_by_entry_id.insert(entry_id, image_id); - } - - anyhow::Ok(()) - })??; - - Ok(entity) - }) - } - - fn reload_images( - &self, - images: HashSet>, - cx: &mut Context, - ) -> Task> { - cx.spawn(async move |_, cx| { - for image in images { - if let Some(rec) = image.update(cx, |image, cx| image.reload(cx))? { - rec.await? - } - } - Ok(()) - }) - } - - fn as_local(&self) -> Option> { - Some(self.clone()) - } - - fn as_remote(&self) -> Option> { - None - } -} - -impl ImageStoreImpl for Entity { - fn open_image( - &self, - path: Arc, - worktree: Entity, - cx: &mut Context, - ) -> Task>> { - let worktree_id = worktree.read(cx).id().to_proto(); - let (project_id, client) = { - let store = self.read(cx); - (store.project_id, store.upstream_client.clone()) - }; - let remote_store = self.clone(); - - cx.spawn(async move |_image_store, cx| { - let response = client - .request(rpc::proto::OpenImageByPath { - project_id, - worktree_id, - path: path.to_proto(), - }) - .await?; - - let image_id = ImageId::from( - NonZeroU64::new(response.image_id).context("invalid image_id in response")?, - ); - - remote_store - .update(cx, |remote_store, cx| { - remote_store.wait_for_remote_image(image_id, cx) - })? - .await - }) - } - - fn reload_images( - &self, - _images: HashSet>, - _cx: &mut Context, - ) -> Task> { - Task::ready(Err(anyhow::anyhow!( - "Reloading images from remote is not supported" - ))) - } - - fn as_local(&self) -> Option> { - None - } - - fn as_remote(&self) -> Option> { - Some(self.clone()) - } -} - -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); - } - }) - .detach(); - } - - fn local_worktree_entries_changed( - &mut self, - worktree_handle: &Entity, - changes: &[(Arc, ProjectEntryId, PathChange)], - cx: &mut Context, - ) { - let snapshot = worktree_handle.read(cx).snapshot(); - for (path, entry_id, _) in changes { - self.local_worktree_entry_changed(*entry_id, path, worktree_handle, &snapshot, cx); - } - } - - fn local_worktree_entry_changed( - &mut self, - entry_id: ProjectEntryId, - path: &Arc, - worktree: &Entity, - snapshot: &worktree::Snapshot, - cx: &mut Context, - ) -> Option<()> { - let project_path = ProjectPath { - worktree_id: snapshot.id(), - path: path.clone(), - }; - let image_id = match self.local_image_ids_by_entry_id.get(&entry_id) { - Some(&image_id) => image_id, - None => self.local_image_ids_by_path.get(&project_path).copied()?, - }; - - let image = self - .image_store - .update(cx, |image_store, _| { - if let Some(image) = image_store.get(image_id) { - Some(image) - } else { - image_store.opened_images.remove(&image_id); - None - } - }) - .ok() - .flatten(); - let image = if let Some(image) = image { - image - } else { - self.local_image_ids_by_path.remove(&project_path); - self.local_image_ids_by_entry_id.remove(&entry_id); - return None; - }; - - image.update(cx, |image, cx| { - let old_file = &image.file; - if old_file.worktree != *worktree { - return; - } - - let snapshot_entry = old_file - .entry_id - .and_then(|entry_id| snapshot.entry_for_id(entry_id)) - .or_else(|| snapshot.entry_for_path(old_file.path.as_ref())); - - let new_file = if let Some(entry) = snapshot_entry { - worktree::File { - disk_state: match entry.mtime { - Some(mtime) => DiskState::Present { mtime }, - None => old_file.disk_state, - }, - is_local: true, - entry_id: Some(entry.id), - path: entry.path.clone(), - worktree: worktree.clone(), - is_private: entry.is_private, - } - } else { - worktree::File { - disk_state: DiskState::Deleted, - is_local: true, - entry_id: old_file.entry_id, - path: old_file.path.clone(), - worktree: worktree.clone(), - is_private: old_file.is_private, - } - }; - - if new_file == **old_file { - return; - } - - if new_file.path != old_file.path { - self.local_image_ids_by_path.remove(&ProjectPath { - path: old_file.path.clone(), - worktree_id: old_file.worktree_id(cx), - }); - self.local_image_ids_by_path.insert( - ProjectPath { - worktree_id: new_file.worktree_id(cx), - path: new_file.path.clone(), - }, - image_id, - ); - } - - if new_file.entry_id != old_file.entry_id { - if let Some(entry_id) = old_file.entry_id { - self.local_image_ids_by_entry_id.remove(&entry_id); - } - if let Some(entry_id) = new_file.entry_id { - self.local_image_ids_by_entry_id.insert(entry_id, image_id); - } - } - - image.file_updated(Arc::new(new_file), cx); - }); - None - } - - fn image_changed_file(&mut self, image: Entity, cx: &mut App) -> Option<()> { - let image = image.read(cx); - let file = &image.file; - - let image_id = image.id; - if let Some(entry_id) = file.entry_id { - match self.local_image_ids_by_entry_id.get(&entry_id) { - Some(_) => { - return None; - } - None => { - self.local_image_ids_by_entry_id.insert(entry_id, image_id); - } - } - }; - self.local_image_ids_by_path.insert( - ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }, - image_id, - ); - - Some(()) - } -} - -fn create_gpui_image(content: Vec) -> anyhow::Result> { - let format = image::guess_format(&content)?; - - Ok(Arc::new(gpui::Image::from_bytes( - match format { - image::ImageFormat::Png => gpui::ImageFormat::Png, - image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg, - image::ImageFormat::WebP => gpui::ImageFormat::Webp, - image::ImageFormat::Gif => gpui::ImageFormat::Gif, - image::ImageFormat::Bmp => gpui::ImageFormat::Bmp, - image::ImageFormat::Tiff => gpui::ImageFormat::Tiff, - image::ImageFormat::Ico => gpui::ImageFormat::Ico, - format => anyhow::bail!("Image format {format:?} not supported"), - }, - content, - ))) -} - -#[cfg(test)] -mod tests { - use super::*; - use fs::FakeFs; - use gpui::TestAppContext; - use serde_json::json; - use settings::SettingsStore; - use util::rel_path::rel_path; - - pub fn init_test(cx: &mut TestAppContext) { - zlog::init_test(); - - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - }); - } - - #[gpui::test] - async fn test_image_not_loaded_twice(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - fs.insert_tree("/root", json!({})).await; - // Create a png file that consists of a single white pixel - fs.insert_file( - "/root/image_1.png", - vec![ - 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, - 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, - 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, - 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, - 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, - ], - ) - .await; - - let project = Project::test(fs, ["/root".as_ref()], cx).await; - - let worktree_id = - cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id()); - - let project_path = ProjectPath { - worktree_id, - path: rel_path("image_1.png").into(), - }; - - let (task1, task2) = project.update(cx, |project, cx| { - ( - project.open_image(project_path.clone(), cx), - project.open_image(project_path.clone(), cx), - ) - }); - - let image1 = task1.await.unwrap(); - let image2 = task2.await.unwrap(); - - assert_eq!(image1, image2); - } - - #[gpui::test] - fn test_compute_metadata_from_bytes() { - // Single white pixel PNG - let png_bytes = vec![ - 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, - 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, - 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, - 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, - 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, - ]; - - let metadata = ImageItem::compute_metadata_from_bytes(&png_bytes).unwrap(); - - assert_eq!(metadata.width, 1); - assert_eq!(metadata.height, 1); - assert_eq!(metadata.file_size, png_bytes.len() as u64); - assert_eq!(metadata.format, image::ImageFormat::Png); - assert!(metadata.colors.is_some()); - } -} diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs deleted file mode 100644 index 05ee70bf66..0000000000 --- a/crates/project/src/lsp_command.rs +++ /dev/null @@ -1,4643 +0,0 @@ -mod signature_help; - -use crate::{ - CodeAction, CompletionSource, CoreCompletion, CoreCompletionResponse, DocumentColor, - DocumentHighlight, DocumentSymbol, Hover, HoverBlock, HoverBlockKind, InlayHint, - InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, - LocationLink, LspAction, LspPullDiagnostics, MarkupContent, PrepareRenameResponse, - ProjectTransaction, PulledDiagnostics, ResolveState, - lsp_store::{LocalLspStore, LspStore}, -}; -use anyhow::{Context as _, Result}; -use async_trait::async_trait; -use client::proto::{self, PeerId}; -use clock::Global; -use collections::{HashMap, HashSet}; -use futures::future; -use gpui::{App, AsyncApp, Entity, SharedString, Task}; -use language::{ - Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, CharScopeContext, - OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped, - language_settings::{InlayHintKind, LanguageSettings, language_settings}, - point_from_lsp, point_to_lsp, - proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, - range_from_lsp, range_to_lsp, -}; -use lsp::{ - AdapterServerCapabilities, CodeActionKind, CodeActionOptions, CodeDescription, - CompletionContext, CompletionListItemDefaultsEditRange, CompletionTriggerKind, - DocumentHighlightKind, LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities, - OneOf, RenameOptions, ServerCapabilities, -}; -use serde_json::Value; -use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature}; -use std::{ - cmp::Reverse, collections::hash_map, mem, ops::Range, path::Path, str::FromStr, sync::Arc, -}; -use text::{BufferId, LineEnding}; -use util::{ResultExt as _, debug_panic}; - -pub use signature_help::SignatureHelp; - -pub fn lsp_formatting_options(settings: &LanguageSettings) -> lsp::FormattingOptions { - lsp::FormattingOptions { - tab_size: settings.tab_size.into(), - insert_spaces: !settings.hard_tabs, - trim_trailing_whitespace: Some(settings.remove_trailing_whitespace_on_save), - trim_final_newlines: Some(settings.ensure_final_newline_on_save), - insert_final_newline: Some(settings.ensure_final_newline_on_save), - ..lsp::FormattingOptions::default() - } -} - -pub fn file_path_to_lsp_url(path: &Path) -> Result { - match lsp::Uri::from_file_path(path) { - Ok(url) => Ok(url), - Err(()) => anyhow::bail!("Invalid file path provided to LSP request: {path:?}"), - } -} - -pub(crate) fn make_text_document_identifier(path: &Path) -> Result { - Ok(lsp::TextDocumentIdentifier { - uri: file_path_to_lsp_url(path)?, - }) -} - -pub(crate) fn make_lsp_text_document_position( - path: &Path, - position: PointUtf16, -) -> Result { - Ok(lsp::TextDocumentPositionParams { - text_document: make_text_document_identifier(path)?, - position: point_to_lsp(position), - }) -} - -#[async_trait(?Send)] -pub trait LspCommand: 'static + Sized + Send + std::fmt::Debug { - type Response: 'static + Default + Send + std::fmt::Debug; - type LspRequest: 'static + Send + lsp::request::Request; - type ProtoRequest: 'static + Send + proto::RequestMessage; - - fn display_name(&self) -> &str; - - fn status(&self) -> Option { - None - } - - fn to_lsp_params_or_response( - &self, - path: &Path, - buffer: &Buffer, - language_server: &Arc, - cx: &App, - ) -> Result< - LspParamsOrResponse<::Params, Self::Response>, - > { - if self.check_capabilities(language_server.adapter_server_capabilities()) { - Ok(LspParamsOrResponse::Params(self.to_lsp( - path, - buffer, - language_server, - cx, - )?)) - } else { - Ok(LspParamsOrResponse::Response(Default::default())) - } - } - - /// When false, `to_lsp_params_or_response` default implementation will return the default response. - fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool; - - fn to_lsp( - &self, - path: &Path, - buffer: &Buffer, - language_server: &Arc, - cx: &App, - ) -> Result<::Params>; - - async fn response_from_lsp( - self, - message: ::Result, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - cx: AsyncApp, - ) -> Result; - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest; - - async fn from_proto( - message: Self::ProtoRequest, - lsp_store: Entity, - buffer: Entity, - cx: AsyncApp, - ) -> Result; - - fn response_to_proto( - response: Self::Response, - lsp_store: &mut LspStore, - peer_id: PeerId, - buffer_version: &clock::Global, - cx: &mut App, - ) -> ::Response; - - async fn response_from_proto( - self, - message: ::Response, - lsp_store: Entity, - buffer: Entity, - cx: AsyncApp, - ) -> Result; - - fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result; -} - -pub enum LspParamsOrResponse { - Params(P), - Response(R), -} - -#[derive(Debug)] -pub(crate) struct PrepareRename { - pub position: PointUtf16, -} - -#[derive(Debug)] -pub(crate) struct PerformRename { - pub position: PointUtf16, - pub new_name: String, - pub push_to_history: bool, -} - -#[derive(Debug, Clone, Copy)] -pub struct GetDefinitions { - pub position: PointUtf16, -} - -#[derive(Debug, Clone, Copy)] -pub(crate) struct GetDeclarations { - pub position: PointUtf16, -} - -#[derive(Debug, Clone, Copy)] -pub(crate) struct GetTypeDefinitions { - pub position: PointUtf16, -} - -#[derive(Debug, Clone, Copy)] -pub(crate) struct GetImplementations { - pub position: PointUtf16, -} - -#[derive(Debug, Clone, Copy)] -pub(crate) struct GetReferences { - pub position: PointUtf16, -} - -#[derive(Debug)] -pub(crate) struct GetDocumentHighlights { - pub position: PointUtf16, -} - -#[derive(Debug, Copy, Clone)] -pub(crate) struct GetDocumentSymbols; - -#[derive(Clone, Debug)] -pub(crate) struct GetSignatureHelp { - pub position: PointUtf16, -} - -#[derive(Clone, Debug)] -pub(crate) struct GetHover { - pub position: PointUtf16, -} - -#[derive(Debug)] -pub(crate) struct GetCompletions { - pub position: PointUtf16, - pub context: CompletionContext, - pub server_id: Option, -} - -#[derive(Clone, Debug)] -pub(crate) struct GetCodeActions { - pub range: Range, - pub kinds: Option>, -} - -#[derive(Debug)] -pub(crate) struct OnTypeFormatting { - pub position: PointUtf16, - pub trigger: String, - pub options: lsp::FormattingOptions, - pub push_to_history: bool, -} - -#[derive(Clone, Debug)] -pub(crate) struct InlayHints { - pub range: Range, -} - -#[derive(Debug, Copy, Clone)] -pub(crate) struct GetCodeLens; - -#[derive(Debug, Copy, Clone)] -pub(crate) struct GetDocumentColor; - -impl GetCodeLens { - pub(crate) fn can_resolve_lens(capabilities: &ServerCapabilities) -> bool { - capabilities - .code_lens_provider - .as_ref() - .and_then(|code_lens_options| code_lens_options.resolve_provider) - .unwrap_or(false) - } -} - -#[derive(Debug)] -pub(crate) struct LinkedEditingRange { - pub position: Anchor, -} - -#[derive(Clone, Debug)] -pub(crate) struct GetDocumentDiagnostics { - /// We cannot blindly rely on server's capabilities.diagnostic_provider, as they're a singular field, whereas - /// a server can register multiple diagnostic providers post-mortem. - pub registration_id: Option, - pub identifier: Option, - pub previous_result_id: Option, -} - -#[async_trait(?Send)] -impl LspCommand for PrepareRename { - type Response = PrepareRenameResponse; - type LspRequest = lsp::request::PrepareRenameRequest; - type ProtoRequest = proto::PrepareRename; - - fn display_name(&self) -> &str { - "Prepare rename" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - capabilities - .server_capabilities - .rename_provider - .is_some_and(|capability| match capability { - OneOf::Left(enabled) => enabled, - OneOf::Right(options) => options.prepare_provider.unwrap_or(false), - }) - } - - fn to_lsp_params_or_response( - &self, - path: &Path, - buffer: &Buffer, - language_server: &Arc, - cx: &App, - ) -> Result> { - let rename_provider = language_server - .adapter_server_capabilities() - .server_capabilities - .rename_provider; - match rename_provider { - Some(lsp::OneOf::Right(RenameOptions { - prepare_provider: Some(true), - .. - })) => Ok(LspParamsOrResponse::Params(self.to_lsp( - path, - buffer, - language_server, - cx, - )?)), - Some(lsp::OneOf::Right(_)) => Ok(LspParamsOrResponse::Response( - PrepareRenameResponse::OnlyUnpreparedRenameSupported, - )), - Some(lsp::OneOf::Left(true)) => Ok(LspParamsOrResponse::Response( - PrepareRenameResponse::OnlyUnpreparedRenameSupported, - )), - _ => anyhow::bail!("Rename not supported"), - } - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - make_lsp_text_document_position(path, self.position) - } - - async fn response_from_lsp( - self, - message: Option, - _: Entity, - buffer: Entity, - _: LanguageServerId, - cx: AsyncApp, - ) -> Result { - buffer.read_with(&cx, |buffer, _| match message { - Some(lsp::PrepareRenameResponse::Range(range)) - | Some(lsp::PrepareRenameResponse::RangeWithPlaceholder { range, .. }) => { - let Range { start, end } = range_from_lsp(range); - if buffer.clip_point_utf16(start, Bias::Left) == start.0 - && buffer.clip_point_utf16(end, Bias::Left) == end.0 - { - Ok(PrepareRenameResponse::Success( - buffer.anchor_after(start)..buffer.anchor_before(end), - )) - } else { - Ok(PrepareRenameResponse::InvalidPosition) - } - } - Some(lsp::PrepareRenameResponse::DefaultBehavior { .. }) => { - let snapshot = buffer.snapshot(); - let (range, _) = snapshot.surrounding_word(self.position, None); - let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end); - Ok(PrepareRenameResponse::Success(range)) - } - None => Ok(PrepareRenameResponse::InvalidPosition), - })? - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::PrepareRename { - proto::PrepareRename { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::PrepareRename, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - }) - } - - fn response_to_proto( - response: PrepareRenameResponse, - _: &mut LspStore, - _: PeerId, - buffer_version: &clock::Global, - _: &mut App, - ) -> proto::PrepareRenameResponse { - match response { - PrepareRenameResponse::Success(range) => proto::PrepareRenameResponse { - can_rename: true, - only_unprepared_rename_supported: false, - start: Some(language::proto::serialize_anchor(&range.start)), - end: Some(language::proto::serialize_anchor(&range.end)), - version: serialize_version(buffer_version), - }, - PrepareRenameResponse::OnlyUnpreparedRenameSupported => proto::PrepareRenameResponse { - can_rename: false, - only_unprepared_rename_supported: true, - start: None, - end: None, - version: vec![], - }, - PrepareRenameResponse::InvalidPosition => proto::PrepareRenameResponse { - can_rename: false, - only_unprepared_rename_supported: false, - start: None, - end: None, - version: vec![], - }, - } - } - - async fn response_from_proto( - self, - message: proto::PrepareRenameResponse, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - if message.can_rename { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - if let (Some(start), Some(end)) = ( - message.start.and_then(deserialize_anchor), - message.end.and_then(deserialize_anchor), - ) { - Ok(PrepareRenameResponse::Success(start..end)) - } else { - anyhow::bail!( - "Missing start or end position in remote project PrepareRenameResponse" - ); - } - } else if message.only_unprepared_rename_supported { - Ok(PrepareRenameResponse::OnlyUnpreparedRenameSupported) - } else { - Ok(PrepareRenameResponse::InvalidPosition) - } - } - - fn buffer_id_from_proto(message: &proto::PrepareRename) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for PerformRename { - type Response = ProjectTransaction; - type LspRequest = lsp::request::Rename; - type ProtoRequest = proto::PerformRename; - - fn display_name(&self) -> &str { - "Rename" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - capabilities - .server_capabilities - .rename_provider - .is_some_and(|capability| match capability { - OneOf::Left(enabled) => enabled, - OneOf::Right(_options) => true, - }) - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::RenameParams { - text_document_position: make_lsp_text_document_position(path, self.position)?, - new_name: self.new_name.clone(), - work_done_progress_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: Option, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - mut cx: AsyncApp, - ) -> Result { - if let Some(edit) = message { - let (_, 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_server, - &mut cx, - ) - .await - } else { - Ok(ProjectTransaction::default()) - } - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::PerformRename { - proto::PerformRename { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - new_name: self.new_name.clone(), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::PerformRename, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - new_name: message.new_name, - push_to_history: false, - }) - } - - fn response_to_proto( - response: ProjectTransaction, - lsp_store: &mut LspStore, - peer_id: PeerId, - _: &clock::Global, - cx: &mut App, - ) -> proto::PerformRenameResponse { - let transaction = lsp_store.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.serialize_project_transaction_for_peer(response, peer_id, cx) - }); - proto::PerformRenameResponse { - transaction: Some(transaction), - } - } - - async fn response_from_proto( - self, - message: proto::PerformRenameResponse, - lsp_store: Entity, - _: Entity, - mut cx: AsyncApp, - ) -> Result { - let message = message.transaction.context("missing transaction")?; - lsp_store - .update(&mut cx, |lsp_store, cx| { - lsp_store.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.deserialize_project_transaction(message, self.push_to_history, cx) - }) - })? - .await - } - - fn buffer_id_from_proto(message: &proto::PerformRename) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetDefinitions { - type Response = Vec; - type LspRequest = lsp::request::GotoDefinition; - type ProtoRequest = proto::GetDefinition; - - fn display_name(&self) -> &str { - "Get definition" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - capabilities - .server_capabilities - .definition_provider - .is_some_and(|capability| match capability { - OneOf::Left(supported) => supported, - OneOf::Right(_options) => true, - }) - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::GotoDefinitionParams { - text_document_position_params: make_lsp_text_document_position(path, self.position)?, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: Option, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - cx: AsyncApp, - ) -> Result> { - location_links_from_lsp(message, lsp_store, buffer, server_id, cx).await - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDefinition { - proto::GetDefinition { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::GetDefinition, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - }) - } - - fn response_to_proto( - response: Vec, - lsp_store: &mut LspStore, - peer_id: PeerId, - _: &clock::Global, - cx: &mut App, - ) -> proto::GetDefinitionResponse { - let links = location_links_to_proto(response, lsp_store, peer_id, cx); - proto::GetDefinitionResponse { links } - } - - async fn response_from_proto( - self, - message: proto::GetDefinitionResponse, - lsp_store: Entity, - _: Entity, - cx: AsyncApp, - ) -> Result> { - location_links_from_proto(message.links, lsp_store, cx).await - } - - fn buffer_id_from_proto(message: &proto::GetDefinition) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetDeclarations { - type Response = Vec; - type LspRequest = lsp::request::GotoDeclaration; - type ProtoRequest = proto::GetDeclaration; - - fn display_name(&self) -> &str { - "Get declaration" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - capabilities - .server_capabilities - .declaration_provider - .is_some_and(|capability| match capability { - lsp::DeclarationCapability::Simple(supported) => supported, - lsp::DeclarationCapability::RegistrationOptions(..) => true, - lsp::DeclarationCapability::Options(..) => true, - }) - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::GotoDeclarationParams { - text_document_position_params: make_lsp_text_document_position(path, self.position)?, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: Option, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - cx: AsyncApp, - ) -> Result> { - location_links_from_lsp(message, lsp_store, buffer, server_id, cx).await - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDeclaration { - proto::GetDeclaration { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::GetDeclaration, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - }) - } - - fn response_to_proto( - response: Vec, - lsp_store: &mut LspStore, - peer_id: PeerId, - _: &clock::Global, - cx: &mut App, - ) -> proto::GetDeclarationResponse { - let links = location_links_to_proto(response, lsp_store, peer_id, cx); - proto::GetDeclarationResponse { links } - } - - async fn response_from_proto( - self, - message: proto::GetDeclarationResponse, - lsp_store: Entity, - _: Entity, - cx: AsyncApp, - ) -> Result> { - location_links_from_proto(message.links, lsp_store, cx).await - } - - fn buffer_id_from_proto(message: &proto::GetDeclaration) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetImplementations { - type Response = Vec; - type LspRequest = lsp::request::GotoImplementation; - type ProtoRequest = proto::GetImplementation; - - fn display_name(&self) -> &str { - "Get implementation" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - capabilities - .server_capabilities - .implementation_provider - .is_some_and(|capability| match capability { - lsp::ImplementationProviderCapability::Simple(enabled) => enabled, - lsp::ImplementationProviderCapability::Options(_options) => true, - }) - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::GotoImplementationParams { - text_document_position_params: make_lsp_text_document_position(path, self.position)?, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: Option, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - cx: AsyncApp, - ) -> Result> { - location_links_from_lsp(message, lsp_store, buffer, server_id, cx).await - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetImplementation { - proto::GetImplementation { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::GetImplementation, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - }) - } - - fn response_to_proto( - response: Vec, - lsp_store: &mut LspStore, - peer_id: PeerId, - _: &clock::Global, - cx: &mut App, - ) -> proto::GetImplementationResponse { - let links = location_links_to_proto(response, lsp_store, peer_id, cx); - proto::GetImplementationResponse { links } - } - - async fn response_from_proto( - self, - message: proto::GetImplementationResponse, - project: Entity, - _: Entity, - cx: AsyncApp, - ) -> Result> { - location_links_from_proto(message.links, project, cx).await - } - - fn buffer_id_from_proto(message: &proto::GetImplementation) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetTypeDefinitions { - type Response = Vec; - type LspRequest = lsp::request::GotoTypeDefinition; - type ProtoRequest = proto::GetTypeDefinition; - - fn display_name(&self) -> &str { - "Get type definition" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - !matches!( - &capabilities.server_capabilities.type_definition_provider, - None | Some(lsp::TypeDefinitionProviderCapability::Simple(false)) - ) - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::GotoTypeDefinitionParams { - text_document_position_params: make_lsp_text_document_position(path, self.position)?, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: Option, - project: Entity, - buffer: Entity, - server_id: LanguageServerId, - cx: AsyncApp, - ) -> Result> { - location_links_from_lsp(message, project, buffer, server_id, cx).await - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetTypeDefinition { - proto::GetTypeDefinition { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::GetTypeDefinition, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - }) - } - - fn response_to_proto( - response: Vec, - lsp_store: &mut LspStore, - peer_id: PeerId, - _: &clock::Global, - cx: &mut App, - ) -> proto::GetTypeDefinitionResponse { - let links = location_links_to_proto(response, lsp_store, peer_id, cx); - proto::GetTypeDefinitionResponse { links } - } - - async fn response_from_proto( - self, - message: proto::GetTypeDefinitionResponse, - project: Entity, - _: Entity, - cx: AsyncApp, - ) -> Result> { - location_links_from_proto(message.links, project, cx).await - } - - fn buffer_id_from_proto(message: &proto::GetTypeDefinition) -> Result { - BufferId::new(message.buffer_id) - } -} - -fn language_server_for_buffer( - lsp_store: &Entity, - buffer: &Entity, - server_id: LanguageServerId, - cx: &mut AsyncApp, -) -> Result<(Arc, Arc)> { - lsp_store - .update(cx, |lsp_store, cx| { - buffer.update(cx, |buffer, cx| { - lsp_store - .language_server_for_local_buffer(buffer, server_id, cx) - .map(|(adapter, server)| (adapter.clone(), server.clone())) - }) - })? - .context("no language server found for buffer") -} - -pub async fn location_links_from_proto( - proto_links: Vec, - lsp_store: Entity, - mut cx: AsyncApp, -) -> Result> { - let mut links = Vec::new(); - - for link in proto_links { - links.push(location_link_from_proto(link, lsp_store.clone(), &mut cx).await?) - } - - Ok(links) -} - -pub fn location_link_from_proto( - link: proto::LocationLink, - lsp_store: Entity, - cx: &mut AsyncApp, -) -> Task> { - cx.spawn(async move |cx| { - let origin = match link.origin { - Some(origin) => { - let buffer_id = BufferId::new(origin.buffer_id)?; - let buffer = lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.wait_for_remote_buffer(buffer_id, cx) - })? - .await?; - let start = origin - .start - .and_then(deserialize_anchor) - .context("missing origin start")?; - let end = origin - .end - .and_then(deserialize_anchor) - .context("missing origin end")?; - buffer - .update(cx, |buffer, _| buffer.wait_for_anchors([start, end]))? - .await?; - Some(Location { - buffer, - range: start..end, - }) - } - None => None, - }; - - let target = link.target.context("missing target")?; - let buffer_id = BufferId::new(target.buffer_id)?; - let buffer = lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.wait_for_remote_buffer(buffer_id, cx) - })? - .await?; - let start = target - .start - .and_then(deserialize_anchor) - .context("missing target start")?; - let end = target - .end - .and_then(deserialize_anchor) - .context("missing target end")?; - buffer - .update(cx, |buffer, _| buffer.wait_for_anchors([start, end]))? - .await?; - let target = Location { - buffer, - range: start..end, - }; - Ok(LocationLink { origin, target }) - }) -} - -pub async fn location_links_from_lsp( - message: Option, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - mut cx: AsyncApp, -) -> Result> { - let message = match message { - Some(message) => message, - None => return Ok(Vec::new()), - }; - - let mut unresolved_links = Vec::new(); - match message { - lsp::GotoDefinitionResponse::Scalar(loc) => { - unresolved_links.push((None, loc.uri, loc.range)); - } - - lsp::GotoDefinitionResponse::Array(locs) => { - unresolved_links.extend(locs.into_iter().map(|l| (None, l.uri, l.range))); - } - - lsp::GotoDefinitionResponse::Link(links) => { - unresolved_links.extend(links.into_iter().map(|l| { - ( - l.origin_selection_range, - l.target_uri, - l.target_selection_range, - ) - })); - } - } - - let (_, 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) - })? - .await?; - - cx.update(|cx| { - let origin_location = origin_range.map(|origin_range| { - let origin_buffer = buffer.read(cx); - let origin_start = - origin_buffer.clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left); - let origin_end = - origin_buffer.clip_point_utf16(point_from_lsp(origin_range.end), Bias::Left); - Location { - buffer: buffer.clone(), - range: origin_buffer.anchor_after(origin_start) - ..origin_buffer.anchor_before(origin_end), - } - }); - - let target_buffer = target_buffer_handle.read(cx); - let target_start = - target_buffer.clip_point_utf16(point_from_lsp(target_range.start), Bias::Left); - let target_end = - target_buffer.clip_point_utf16(point_from_lsp(target_range.end), Bias::Left); - let target_location = Location { - buffer: target_buffer_handle, - range: target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end), - }; - - definitions.push(LocationLink { - origin: origin_location, - target: target_location, - }) - })?; - } - Ok(definitions) -} - -pub async fn location_link_from_lsp( - link: lsp::LocationLink, - lsp_store: &Entity, - buffer: &Entity, - server_id: LanguageServerId, - cx: &mut AsyncApp, -) -> Result { - let (_, language_server) = language_server_for_buffer(lsp_store, buffer, server_id, cx)?; - - let (origin_range, target_uri, target_range) = ( - link.origin_selection_range, - link.target_uri, - link.target_selection_range, - ); - - 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) - })? - .await?; - - cx.update(|cx| { - let origin_location = origin_range.map(|origin_range| { - let origin_buffer = buffer.read(cx); - let origin_start = - origin_buffer.clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left); - let origin_end = - origin_buffer.clip_point_utf16(point_from_lsp(origin_range.end), Bias::Left); - Location { - buffer: buffer.clone(), - range: origin_buffer.anchor_after(origin_start) - ..origin_buffer.anchor_before(origin_end), - } - }); - - let target_buffer = target_buffer_handle.read(cx); - let target_start = - target_buffer.clip_point_utf16(point_from_lsp(target_range.start), Bias::Left); - let target_end = - target_buffer.clip_point_utf16(point_from_lsp(target_range.end), Bias::Left); - let target_location = Location { - buffer: target_buffer_handle, - range: target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end), - }; - - LocationLink { - origin: origin_location, - target: target_location, - } - }) -} - -pub fn location_links_to_proto( - links: Vec, - lsp_store: &mut LspStore, - peer_id: PeerId, - cx: &mut App, -) -> Vec { - links - .into_iter() - .map(|definition| location_link_to_proto(definition, lsp_store, peer_id, cx)) - .collect() -} - -pub fn location_link_to_proto( - location: LocationLink, - lsp_store: &mut LspStore, - peer_id: PeerId, - cx: &mut App, -) -> proto::LocationLink { - let origin = location.origin.map(|origin| { - lsp_store - .buffer_store() - .update(cx, |buffer_store, cx| { - buffer_store.create_buffer_for_peer(&origin.buffer, peer_id, cx) - }) - .detach_and_log_err(cx); - - let buffer_id = origin.buffer.read(cx).remote_id().into(); - proto::Location { - start: Some(serialize_anchor(&origin.range.start)), - end: Some(serialize_anchor(&origin.range.end)), - buffer_id, - } - }); - - lsp_store - .buffer_store() - .update(cx, |buffer_store, cx| { - buffer_store.create_buffer_for_peer(&location.target.buffer, peer_id, cx) - }) - .detach_and_log_err(cx); - - let buffer_id = location.target.buffer.read(cx).remote_id().into(); - let target = proto::Location { - start: Some(serialize_anchor(&location.target.range.start)), - end: Some(serialize_anchor(&location.target.range.end)), - buffer_id, - }; - - proto::LocationLink { - origin, - target: Some(target), - } -} - -#[async_trait(?Send)] -impl LspCommand for GetReferences { - type Response = Vec; - type LspRequest = lsp::request::References; - type ProtoRequest = proto::GetReferences; - - fn display_name(&self) -> &str { - "Find all references" - } - - fn status(&self) -> Option { - Some("Finding references...".to_owned()) - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - match &capabilities.server_capabilities.references_provider { - Some(OneOf::Left(has_support)) => *has_support, - Some(OneOf::Right(_)) => true, - None => false, - } - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::ReferenceParams { - text_document_position: make_lsp_text_document_position(path, self.position)?, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - context: lsp::ReferenceContext { - include_declaration: true, - }, - }) - } - - async fn response_from_lsp( - self, - locations: Option>, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - mut cx: AsyncApp, - ) -> Result> { - let mut references = Vec::new(); - let (_, language_server) = - language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; - - if let Some(locations) = locations { - for lsp_location in locations { - let target_buffer_handle = lsp_store - .update(&mut cx, |lsp_store, cx| { - lsp_store.open_local_buffer_via_lsp( - lsp_location.uri, - language_server.server_id(), - cx, - ) - })? - .await?; - - target_buffer_handle - .clone() - .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); - references.push(Location { - buffer: target_buffer_handle, - range: target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end), - }); - })?; - } - } - - Ok(references) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetReferences { - proto::GetReferences { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::GetReferences, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - }) - } - - fn response_to_proto( - response: Vec, - lsp_store: &mut LspStore, - peer_id: PeerId, - _: &clock::Global, - cx: &mut App, - ) -> proto::GetReferencesResponse { - let locations = response - .into_iter() - .map(|definition| { - lsp_store - .buffer_store() - .update(cx, |buffer_store, cx| { - buffer_store.create_buffer_for_peer(&definition.buffer, peer_id, cx) - }) - .detach_and_log_err(cx); - let buffer_id = definition.buffer.read(cx).remote_id(); - proto::Location { - start: Some(serialize_anchor(&definition.range.start)), - end: Some(serialize_anchor(&definition.range.end)), - buffer_id: buffer_id.into(), - } - }) - .collect(); - proto::GetReferencesResponse { locations } - } - - async fn response_from_proto( - self, - message: proto::GetReferencesResponse, - project: Entity, - _: Entity, - mut cx: AsyncApp, - ) -> Result> { - let mut locations = Vec::new(); - for location in message.locations { - let buffer_id = BufferId::new(location.buffer_id)?; - let target_buffer = project - .update(&mut cx, |this, cx| { - this.wait_for_remote_buffer(buffer_id, cx) - })? - .await?; - let start = location - .start - .and_then(deserialize_anchor) - .context("missing target start")?; - let end = location - .end - .and_then(deserialize_anchor) - .context("missing target end")?; - target_buffer - .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))? - .await?; - locations.push(Location { - buffer: target_buffer, - range: start..end, - }) - } - Ok(locations) - } - - fn buffer_id_from_proto(message: &proto::GetReferences) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetDocumentHighlights { - type Response = Vec; - type LspRequest = lsp::request::DocumentHighlightRequest; - type ProtoRequest = proto::GetDocumentHighlights; - - fn display_name(&self) -> &str { - "Get document highlights" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - capabilities - .server_capabilities - .document_highlight_provider - .is_some_and(|capability| match capability { - OneOf::Left(supported) => supported, - OneOf::Right(_options) => true, - }) - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::DocumentHighlightParams { - text_document_position_params: make_lsp_text_document_position(path, self.position)?, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - lsp_highlights: Option>, - _: Entity, - buffer: Entity, - _: LanguageServerId, - cx: AsyncApp, - ) -> Result> { - buffer.read_with(&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 - .into_iter() - .map(|lsp_highlight| { - let start = buffer - .clip_point_utf16(point_from_lsp(lsp_highlight.range.start), Bias::Left); - let end = buffer - .clip_point_utf16(point_from_lsp(lsp_highlight.range.end), Bias::Left); - DocumentHighlight { - range: buffer.anchor_after(start)..buffer.anchor_before(end), - kind: lsp_highlight - .kind - .unwrap_or(lsp::DocumentHighlightKind::READ), - } - }) - .collect() - }) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDocumentHighlights { - proto::GetDocumentHighlights { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::GetDocumentHighlights, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - }) - } - - fn response_to_proto( - response: Vec, - _: &mut LspStore, - _: PeerId, - _: &clock::Global, - _: &mut App, - ) -> proto::GetDocumentHighlightsResponse { - let highlights = response - .into_iter() - .map(|highlight| proto::DocumentHighlight { - start: Some(serialize_anchor(&highlight.range.start)), - end: Some(serialize_anchor(&highlight.range.end)), - kind: match highlight.kind { - DocumentHighlightKind::TEXT => proto::document_highlight::Kind::Text.into(), - DocumentHighlightKind::WRITE => proto::document_highlight::Kind::Write.into(), - DocumentHighlightKind::READ => proto::document_highlight::Kind::Read.into(), - _ => proto::document_highlight::Kind::Text.into(), - }, - }) - .collect(); - proto::GetDocumentHighlightsResponse { highlights } - } - - async fn response_from_proto( - self, - message: proto::GetDocumentHighlightsResponse, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result> { - let mut highlights = Vec::new(); - for highlight in message.highlights { - let start = highlight - .start - .and_then(deserialize_anchor) - .context("missing target start")?; - let end = highlight - .end - .and_then(deserialize_anchor) - .context("missing target end")?; - buffer - .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))? - .await?; - let kind = match proto::document_highlight::Kind::from_i32(highlight.kind) { - Some(proto::document_highlight::Kind::Text) => DocumentHighlightKind::TEXT, - Some(proto::document_highlight::Kind::Read) => DocumentHighlightKind::READ, - Some(proto::document_highlight::Kind::Write) => DocumentHighlightKind::WRITE, - None => DocumentHighlightKind::TEXT, - }; - highlights.push(DocumentHighlight { - range: start..end, - kind, - }); - } - Ok(highlights) - } - - fn buffer_id_from_proto(message: &proto::GetDocumentHighlights) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetDocumentSymbols { - type Response = Vec; - type LspRequest = lsp::request::DocumentSymbolRequest; - type ProtoRequest = proto::GetDocumentSymbols; - - fn display_name(&self) -> &str { - "Get document symbols" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - capabilities - .server_capabilities - .document_symbol_provider - .is_some_and(|capability| match capability { - OneOf::Left(supported) => supported, - OneOf::Right(_options) => true, - }) - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::DocumentSymbolParams { - text_document: make_text_document_identifier(path)?, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - lsp_symbols: Option, - _: Entity, - _: Entity, - _: LanguageServerId, - _: AsyncApp, - ) -> Result> { - let Some(lsp_symbols) = lsp_symbols else { - return Ok(Vec::new()); - }; - - let symbols: Vec<_> = match lsp_symbols { - lsp::DocumentSymbolResponse::Flat(symbol_information) => symbol_information - .into_iter() - .map(|lsp_symbol| DocumentSymbol { - name: lsp_symbol.name, - kind: lsp_symbol.kind, - range: range_from_lsp(lsp_symbol.location.range), - selection_range: range_from_lsp(lsp_symbol.location.range), - children: Vec::new(), - }) - .collect(), - lsp::DocumentSymbolResponse::Nested(nested_responses) => { - fn convert_symbol(lsp_symbol: lsp::DocumentSymbol) -> DocumentSymbol { - DocumentSymbol { - name: lsp_symbol.name, - kind: lsp_symbol.kind, - range: range_from_lsp(lsp_symbol.range), - selection_range: range_from_lsp(lsp_symbol.selection_range), - children: lsp_symbol - .children - .map(|children| { - children.into_iter().map(convert_symbol).collect::>() - }) - .unwrap_or_default(), - } - } - nested_responses.into_iter().map(convert_symbol).collect() - } - }; - Ok(symbols) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDocumentSymbols { - proto::GetDocumentSymbols { - project_id, - buffer_id: buffer.remote_id().into(), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::GetDocumentSymbols, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - Ok(Self) - } - - fn response_to_proto( - response: Vec, - _: &mut LspStore, - _: PeerId, - _: &clock::Global, - _: &mut App, - ) -> proto::GetDocumentSymbolsResponse { - let symbols = response - .into_iter() - .map(|symbol| { - fn convert_symbol_to_proto(symbol: DocumentSymbol) -> proto::DocumentSymbol { - proto::DocumentSymbol { - name: symbol.name.clone(), - kind: unsafe { mem::transmute::(symbol.kind) }, - start: Some(proto::PointUtf16 { - row: symbol.range.start.0.row, - column: symbol.range.start.0.column, - }), - end: Some(proto::PointUtf16 { - row: symbol.range.end.0.row, - column: symbol.range.end.0.column, - }), - selection_start: Some(proto::PointUtf16 { - row: symbol.selection_range.start.0.row, - column: symbol.selection_range.start.0.column, - }), - selection_end: Some(proto::PointUtf16 { - row: symbol.selection_range.end.0.row, - column: symbol.selection_range.end.0.column, - }), - children: symbol - .children - .into_iter() - .map(convert_symbol_to_proto) - .collect(), - } - } - convert_symbol_to_proto(symbol) - }) - .collect::>(); - - proto::GetDocumentSymbolsResponse { symbols } - } - - async fn response_from_proto( - self, - message: proto::GetDocumentSymbolsResponse, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> Result> { - let mut symbols = Vec::with_capacity(message.symbols.len()); - for serialized_symbol in message.symbols { - fn deserialize_symbol_with_children( - serialized_symbol: proto::DocumentSymbol, - ) -> Result { - let kind = - unsafe { mem::transmute::(serialized_symbol.kind) }; - - let start = serialized_symbol.start.context("invalid start")?; - let end = serialized_symbol.end.context("invalid end")?; - - let selection_start = serialized_symbol - .selection_start - .context("invalid selection start")?; - let selection_end = serialized_symbol - .selection_end - .context("invalid selection end")?; - - Ok(DocumentSymbol { - name: serialized_symbol.name, - kind, - range: Unclipped(PointUtf16::new(start.row, start.column)) - ..Unclipped(PointUtf16::new(end.row, end.column)), - selection_range: Unclipped(PointUtf16::new( - selection_start.row, - selection_start.column, - )) - ..Unclipped(PointUtf16::new(selection_end.row, selection_end.column)), - children: serialized_symbol - .children - .into_iter() - .filter_map(|symbol| deserialize_symbol_with_children(symbol).ok()) - .collect::>(), - }) - } - - symbols.push(deserialize_symbol_with_children(serialized_symbol)?); - } - - Ok(symbols) - } - - fn buffer_id_from_proto(message: &proto::GetDocumentSymbols) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetSignatureHelp { - type Response = Option; - type LspRequest = lsp::SignatureHelpRequest; - type ProtoRequest = proto::GetSignatureHelp; - - fn display_name(&self) -> &str { - "Get signature help" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - capabilities - .server_capabilities - .signature_help_provider - .is_some() - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _cx: &App, - ) -> Result { - Ok(lsp::SignatureHelpParams { - text_document_position_params: make_lsp_text_document_position(path, self.position)?, - context: None, - work_done_progress_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: Option, - lsp_store: Entity, - _: Entity, - id: LanguageServerId, - cx: AsyncApp, - ) -> Result { - let Some(message) = message else { - return Ok(None); - }; - cx.update(|cx| { - SignatureHelp::new( - message, - Some(lsp_store.read(cx).languages.clone()), - Some(id), - cx, - ) - }) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest { - let offset = buffer.point_utf16_to_offset(self.position); - proto::GetSignatureHelp { - project_id, - buffer_id: buffer.remote_id().to_proto(), - position: Some(serialize_anchor(&buffer.anchor_after(offset))), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - payload: Self::ProtoRequest, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&payload.version)) - })? - .await - .with_context(|| format!("waiting for version for buffer {}", buffer.entity_id()))?; - let buffer_snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot())?; - Ok(Self { - position: payload - .position - .and_then(deserialize_anchor) - .context("invalid position")? - .to_point_utf16(&buffer_snapshot), - }) - } - - fn response_to_proto( - response: Self::Response, - _: &mut LspStore, - _: PeerId, - _: &Global, - _: &mut App, - ) -> proto::GetSignatureHelpResponse { - proto::GetSignatureHelpResponse { - signature_help: response - .map(|signature_help| lsp_to_proto_signature(signature_help.original_data)), - } - } - - async fn response_from_proto( - self, - response: proto::GetSignatureHelpResponse, - lsp_store: Entity, - _: Entity, - cx: AsyncApp, - ) -> Result { - cx.update(|cx| { - response - .signature_help - .map(proto_to_lsp_signature) - .and_then(|signature| { - SignatureHelp::new( - signature, - Some(lsp_store.read(cx).languages.clone()), - None, - cx, - ) - }) - }) - } - - fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetHover { - type Response = Option; - type LspRequest = lsp::request::HoverRequest; - type ProtoRequest = proto::GetHover; - - fn display_name(&self) -> &str { - "Get hover" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - match capabilities.server_capabilities.hover_provider { - Some(lsp::HoverProviderCapability::Simple(enabled)) => enabled, - Some(lsp::HoverProviderCapability::Options(_)) => true, - None => false, - } - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::HoverParams { - text_document_position_params: make_lsp_text_document_position(path, self.position)?, - work_done_progress_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: Option, - _: Entity, - buffer: Entity, - _: LanguageServerId, - cx: AsyncApp, - ) -> Result { - let Some(hover) = message else { - return Ok(None); - }; - - let (language, range) = buffer.read_with(&cx, |buffer, _| { - ( - buffer.language().cloned(), - hover.range.map(|range| { - let token_start = - buffer.clip_point_utf16(point_from_lsp(range.start), Bias::Left); - let token_end = buffer.clip_point_utf16(point_from_lsp(range.end), Bias::Left); - buffer.anchor_after(token_start)..buffer.anchor_before(token_end) - }), - ) - })?; - - fn hover_blocks_from_marked_string(marked_string: lsp::MarkedString) -> Option { - let block = match marked_string { - lsp::MarkedString::String(content) => HoverBlock { - text: content, - kind: HoverBlockKind::Markdown, - }, - lsp::MarkedString::LanguageString(lsp::LanguageString { language, value }) => { - HoverBlock { - text: value, - kind: HoverBlockKind::Code { language }, - } - } - }; - if block.text.is_empty() { - None - } else { - Some(block) - } - } - - let contents = match hover.contents { - lsp::HoverContents::Scalar(marked_string) => { - hover_blocks_from_marked_string(marked_string) - .into_iter() - .collect() - } - lsp::HoverContents::Array(marked_strings) => marked_strings - .into_iter() - .filter_map(hover_blocks_from_marked_string) - .collect(), - lsp::HoverContents::Markup(markup_content) => vec![HoverBlock { - text: markup_content.value, - kind: if markup_content.kind == lsp::MarkupKind::Markdown { - HoverBlockKind::Markdown - } else { - HoverBlockKind::PlainText - }, - }], - }; - - Ok(Some(Hover { - contents, - range, - language, - })) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest { - proto::GetHover { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - version: serialize_version(&buffer.version), - } - } - - async fn from_proto( - message: Self::ProtoRequest, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - }) - } - - fn response_to_proto( - response: Self::Response, - _: &mut LspStore, - _: PeerId, - _: &clock::Global, - _: &mut App, - ) -> proto::GetHoverResponse { - if let Some(response) = response { - let (start, end) = if let Some(range) = response.range { - ( - Some(language::proto::serialize_anchor(&range.start)), - Some(language::proto::serialize_anchor(&range.end)), - ) - } else { - (None, None) - }; - - let contents = response - .contents - .into_iter() - .map(|block| proto::HoverBlock { - text: block.text, - is_markdown: block.kind == HoverBlockKind::Markdown, - language: if let HoverBlockKind::Code { language } = block.kind { - Some(language) - } else { - None - }, - }) - .collect(); - - proto::GetHoverResponse { - start, - end, - contents, - } - } else { - proto::GetHoverResponse { - start: None, - end: None, - contents: Vec::new(), - } - } - } - - async fn response_from_proto( - self, - message: proto::GetHoverResponse, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let contents: Vec<_> = message - .contents - .into_iter() - .map(|block| HoverBlock { - text: block.text, - kind: if let Some(language) = block.language { - HoverBlockKind::Code { language } - } else if block.is_markdown { - HoverBlockKind::Markdown - } else { - HoverBlockKind::PlainText - }, - }) - .collect(); - if contents.is_empty() { - return Ok(None); - } - - let language = buffer.read_with(&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)) - } else { - None - }; - if let Some(range) = range.as_ref() { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_anchors([range.start, range.end]) - })? - .await?; - } - - Ok(Some(Hover { - contents, - range, - language, - })) - } - - fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result { - BufferId::new(message.buffer_id) - } -} - -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; - type LspRequest = lsp::request::Completion; - type ProtoRequest = proto::GetCompletions; - - fn display_name(&self) -> &str { - "Get completion" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - capabilities - .server_capabilities - .completion_provider - .is_some() - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::CompletionParams { - text_document_position: make_lsp_text_document_position(path, self.position)?, - context: Some(self.context.clone()), - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - completions: Option, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - mut cx: AsyncApp, - ) -> Result { - let mut response_list = None; - let (mut completions, mut is_incomplete) = if let Some(completions) = completions { - match completions { - lsp::CompletionResponse::Array(completions) => (completions, false), - lsp::CompletionResponse::List(mut list) => { - let is_incomplete = list.is_incomplete; - let items = std::mem::take(&mut list.items); - response_list = Some(list); - (items, is_incomplete) - } - } - } else { - (Vec::new(), false) - }; - - let unfiltered_completions_count = completions.len(); - - let language_server_adapter = lsp_store - .read_with(&cx, |lsp_store, _| { - lsp_store.language_server_adapter_for_id(server_id) - })? - .with_context(|| format!("no language server with id {server_id}"))?; - - let lsp_defaults = response_list - .as_ref() - .and_then(|list| list.item_defaults.clone()) - .map(Arc::new); - - let mut completion_edits = Vec::new(); - buffer.update(&mut cx, |buffer, _cx| { - let snapshot = buffer.snapshot(); - let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left); - - let mut range_for_token = None; - completions.retain(|lsp_completion| { - let lsp_edit = lsp_completion.text_edit.clone().or_else(|| { - let default_text_edit = lsp_defaults.as_deref()?.edit_range.as_ref()?; - let new_text = lsp_completion - .text_edit_text - .as_ref() - .unwrap_or(&lsp_completion.label) - .clone(); - match default_text_edit { - CompletionListItemDefaultsEditRange::Range(range) => { - Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: *range, - new_text, - })) - } - CompletionListItemDefaultsEditRange::InsertAndReplace { - insert, - replace, - } => Some(lsp::CompletionTextEdit::InsertAndReplace( - lsp::InsertReplaceEdit { - new_text, - insert: *insert, - replace: *replace, - }, - )), - } - }); - - let edit = match lsp_edit { - // If the language server provides a range to overwrite, then - // check that the range is valid. - Some(completion_text_edit) => { - match parse_completion_text_edit(&completion_text_edit, &snapshot) { - Some(edit) => edit, - None => return false, - } - } - // If the language server does not provide a range, then infer - // the range based on the syntax tree. - None => { - if self.position != clipped_position { - log::info!("completion out of expected range "); - return false; - } - - let default_edit_range = lsp_defaults.as_ref().and_then(|lsp_defaults| { - lsp_defaults - .edit_range - .as_ref() - .and_then(|range| match range { - CompletionListItemDefaultsEditRange::Range(r) => Some(r), - _ => None, - }) - }); - - let range = if let Some(range) = default_edit_range { - let range = range_from_lsp(*range); - let start = snapshot.clip_point_utf16(range.start, Bias::Left); - let end = snapshot.clip_point_utf16(range.end, Bias::Left); - if start != range.start.0 || end != range.end.0 { - log::info!("completion out of expected range"); - return false; - } - - snapshot.anchor_before(start)..snapshot.anchor_after(end) - } else { - range_for_token - .get_or_insert_with(|| { - let offset = self.position.to_offset(&snapshot); - let (range, kind) = snapshot.surrounding_word( - offset, - Some(CharScopeContext::Completion), - ); - let range = if kind == Some(CharKind::Word) { - range - } else { - offset..offset - }; - - snapshot.anchor_before(range.start) - ..snapshot.anchor_after(range.end) - }) - .clone() - }; - - // We already know text_edit is None here - let text = lsp_completion - .insert_text - .as_ref() - .unwrap_or(&lsp_completion.label) - .clone(); - - ParsedCompletionEdit { - replace_range: range, - insert_range: None, - new_text: text, - } - } - }; - - completion_edits.push(edit); - true - }); - })?; - - // If completions were filtered out due to errors that may be transient, mark the result - // incomplete so that it is re-queried. - if unfiltered_completions_count != completions.len() { - is_incomplete = true; - } - - language_server_adapter - .process_completions(&mut completions) - .await; - - let completions = completions - .into_iter() - .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 - .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); - } - CoreCompletion { - replace_range: edit.replace_range, - new_text: edit.new_text, - source: CompletionSource::Lsp { - insert_range: edit.insert_range, - server_id, - lsp_completion: Box::new(lsp_completion), - lsp_defaults: lsp_defaults.clone(), - resolved: false, - }, - } - }) - .collect(); - - Ok(CoreCompletionResponse { - completions, - is_incomplete, - }) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCompletions { - let anchor = buffer.anchor_after(self.position); - proto::GetCompletions { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor(&anchor)), - version: serialize_version(&buffer.version()), - server_id: self.server_id.map(|id| id.to_proto()), - } - } - - async fn from_proto( - message: proto::GetCompletions, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let version = deserialize_version(&message.version); - buffer - .update(&mut cx, |buffer, _| buffer.wait_for_version(version))? - .await?; - let position = message - .position - .and_then(language::proto::deserialize_anchor) - .map(|p| { - buffer.read_with(&cx, |buffer, _| { - buffer.clip_point_utf16(Unclipped(p.to_point_utf16(buffer)), Bias::Left) - }) - }) - .context("invalid position")??; - Ok(Self { - position, - context: CompletionContext { - trigger_kind: CompletionTriggerKind::INVOKED, - trigger_character: None, - }, - server_id: message - .server_id - .map(|id| lsp::LanguageServerId::from_proto(id)), - }) - } - - fn response_to_proto( - response: CoreCompletionResponse, - _: &mut LspStore, - _: PeerId, - buffer_version: &clock::Global, - _: &mut App, - ) -> proto::GetCompletionsResponse { - proto::GetCompletionsResponse { - completions: response - .completions - .iter() - .map(LspStore::serialize_completion) - .collect(), - version: serialize_version(buffer_version), - can_reuse: !response.is_incomplete, - } - } - - async fn response_from_proto( - self, - message: proto::GetCompletionsResponse, - _project: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - - let completions = message - .completions - .into_iter() - .map(LspStore::deserialize_completion) - .collect::>>()?; - - Ok(CoreCompletionResponse { - completions, - is_incomplete: !message.can_reuse, - }) - } - - fn buffer_id_from_proto(message: &proto::GetCompletions) -> Result { - BufferId::new(message.buffer_id) - } -} - -pub struct ParsedCompletionEdit { - pub replace_range: Range, - pub insert_range: Option>, - pub new_text: String, -} - -pub(crate) fn parse_completion_text_edit( - edit: &lsp::CompletionTextEdit, - snapshot: &BufferSnapshot, -) -> Option { - let (replace_range, insert_range, new_text) = match edit { - lsp::CompletionTextEdit::Edit(edit) => (edit.range, None, &edit.new_text), - lsp::CompletionTextEdit::InsertAndReplace(edit) => { - (edit.replace, Some(edit.insert), &edit.new_text) - } - }; - - let replace_range = { - let range = range_from_lsp(replace_range); - let start = snapshot.clip_point_utf16(range.start, Bias::Left); - let end = snapshot.clip_point_utf16(range.end, Bias::Left); - if start != range.start.0 || end != range.end.0 { - log::info!( - "completion out of expected range, start: {start:?}, end: {end:?}, range: {range:?}" - ); - return None; - } - snapshot.anchor_before(start)..snapshot.anchor_after(end) - }; - - let insert_range = match insert_range { - None => None, - Some(insert_range) => { - let range = range_from_lsp(insert_range); - let start = snapshot.clip_point_utf16(range.start, Bias::Left); - let end = snapshot.clip_point_utf16(range.end, Bias::Left); - if start != range.start.0 || end != range.end.0 { - log::info!("completion (insert) out of expected range"); - return None; - } - Some(snapshot.anchor_before(start)..snapshot.anchor_after(end)) - } - }; - - Some(ParsedCompletionEdit { - insert_range, - replace_range, - new_text: new_text.clone(), - }) -} - -#[async_trait(?Send)] -impl LspCommand for GetCodeActions { - type Response = Vec; - type LspRequest = lsp::request::CodeActionRequest; - type ProtoRequest = proto::GetCodeActions; - - fn display_name(&self) -> &str { - "Get code actions" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - match &capabilities.server_capabilities.code_action_provider { - None => false, - Some(lsp::CodeActionProviderCapability::Simple(false)) => false, - _ => { - // If we do know that we want specific code actions AND we know that - // the server only supports specific code actions, then we want to filter - // down to the ones that are supported. - if let Some((requested, supported)) = self - .kinds - .as_ref() - .zip(Self::supported_code_action_kinds(capabilities)) - { - let server_supported = supported.into_iter().collect::>(); - requested.iter().any(|kind| server_supported.contains(kind)) - } else { - true - } - } - } - } - - fn to_lsp( - &self, - path: &Path, - buffer: &Buffer, - language_server: &Arc, - _: &App, - ) -> Result { - let mut relevant_diagnostics = Vec::new(); - for entry in buffer - .snapshot() - .diagnostics_in_range::<_, language::PointUtf16>(self.range.clone(), false) - { - relevant_diagnostics.push(entry.to_lsp_diagnostic_stub()?); - } - - let supported = - Self::supported_code_action_kinds(language_server.adapter_server_capabilities()); - - let only = if let Some(requested) = &self.kinds { - if let Some(supported_kinds) = supported { - let server_supported = supported_kinds.into_iter().collect::>(); - - let filtered = requested - .iter() - .filter(|kind| server_supported.contains(kind)) - .cloned() - .collect(); - Some(filtered) - } else { - Some(requested.clone()) - } - } else { - supported - }; - - Ok(lsp::CodeActionParams { - text_document: make_text_document_identifier(path)?, - range: range_to_lsp(self.range.to_point_utf16(buffer))?, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - context: lsp::CodeActionContext { - diagnostics: relevant_diagnostics, - only, - ..lsp::CodeActionContext::default() - }, - }) - } - - async fn response_from_lsp( - self, - actions: Option, - lsp_store: Entity, - _: Entity, - server_id: LanguageServerId, - cx: AsyncApp, - ) -> Result> { - let requested_kinds_set = self - .kinds - .map(|kinds| kinds.into_iter().collect::>()); - - let language_server = cx.update(|cx| { - lsp_store - .read(cx) - .language_server_for_id(server_id) - .with_context(|| { - format!("Missing the language server that just returned a response {server_id}") - }) - })??; - - let server_capabilities = language_server.capabilities(); - let available_commands = server_capabilities - .execute_command_provider - .as_ref() - .map(|options| options.commands.as_slice()) - .unwrap_or_default(); - Ok(actions - .unwrap_or_default() - .into_iter() - .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; - } - (LspAction::Action(Box::new(lsp_action)), false) - } - lsp::CodeActionOrCommand::Command(command) => { - if available_commands.contains(&command.command) { - (LspAction::Command(command), true) - } else { - return None; - } - } - }; - - if let Some((requested_kinds, kind)) = - requested_kinds_set.as_ref().zip(lsp_action.action_kind()) - && !requested_kinds.contains(&kind) - { - return None; - } - - Some(CodeAction { - server_id, - range: self.range.clone(), - lsp_action, - resolved, - }) - }) - .collect()) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCodeActions { - proto::GetCodeActions { - project_id, - buffer_id: buffer.remote_id().into(), - start: Some(language::proto::serialize_anchor(&self.range.start)), - end: Some(language::proto::serialize_anchor(&self.range.end)), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::GetCodeActions, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let start = message - .start - .and_then(language::proto::deserialize_anchor) - .context("invalid start")?; - let end = message - .end - .and_then(language::proto::deserialize_anchor) - .context("invalid end")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - - Ok(Self { - range: start..end, - kinds: None, - }) - } - - fn response_to_proto( - code_actions: Vec, - _: &mut LspStore, - _: PeerId, - buffer_version: &clock::Global, - _: &mut App, - ) -> proto::GetCodeActionsResponse { - proto::GetCodeActionsResponse { - actions: code_actions - .iter() - .map(LspStore::serialize_code_action) - .collect(), - version: serialize_version(buffer_version), - } - } - - async fn response_from_proto( - self, - message: proto::GetCodeActionsResponse, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result> { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - message - .actions - .into_iter() - .map(LspStore::deserialize_code_action) - .collect() - } - - fn buffer_id_from_proto(message: &proto::GetCodeActions) -> Result { - BufferId::new(message.buffer_id) - } -} - -impl GetCodeActions { - fn supported_code_action_kinds( - capabilities: AdapterServerCapabilities, - ) -> Option> { - match capabilities.server_capabilities.code_action_provider { - Some(lsp::CodeActionProviderCapability::Options(CodeActionOptions { - code_action_kinds: Some(supported_action_kinds), - .. - })) => Some(supported_action_kinds), - _ => capabilities.code_action_kinds, - } - } - - pub fn can_resolve_actions(capabilities: &ServerCapabilities) -> bool { - capabilities - .code_action_provider - .as_ref() - .and_then(|options| match options { - lsp::CodeActionProviderCapability::Simple(_is_supported) => None, - lsp::CodeActionProviderCapability::Options(options) => options.resolve_provider, - }) - .unwrap_or(false) - } -} - -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; - type LspRequest = lsp::request::OnTypeFormatting; - type ProtoRequest = proto::OnTypeFormatting; - - fn display_name(&self) -> &str { - "Formatting on typing" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - Self::supports_on_type_formatting(&self.trigger, &capabilities.server_capabilities) - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::DocumentOnTypeFormattingParams { - text_document_position: make_lsp_text_document_position(path, self.position)?, - ch: self.trigger.clone(), - options: self.options.clone(), - }) - } - - async fn response_from_lsp( - self, - message: Option>, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - mut cx: AsyncApp, - ) -> Result> { - if let Some(edits) = message { - let (lsp_adapter, lsp_server) = - language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; - LocalLspStore::deserialize_text_edits( - lsp_store, - buffer, - edits, - self.push_to_history, - lsp_adapter, - lsp_server, - &mut cx, - ) - .await - } else { - Ok(None) - } - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::OnTypeFormatting { - proto::OnTypeFormatting { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - trigger: self.trigger.clone(), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::OnTypeFormatting, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - - let options = buffer.update(&mut cx, |buffer, cx| { - lsp_formatting_options( - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx).as_ref(), - ) - })?; - - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - trigger: message.trigger.clone(), - options, - push_to_history: false, - }) - } - - fn response_to_proto( - response: Option, - _: &mut LspStore, - _: PeerId, - _: &clock::Global, - _: &mut App, - ) -> proto::OnTypeFormattingResponse { - proto::OnTypeFormattingResponse { - transaction: response - .map(|transaction| language::proto::serialize_transaction(&transaction)), - } - } - - async fn response_from_proto( - self, - message: proto::OnTypeFormattingResponse, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> Result> { - let Some(transaction) = message.transaction else { - return Ok(None); - }; - Ok(Some(language::proto::deserialize_transaction(transaction)?)) - } - - fn buffer_id_from_proto(message: &proto::OnTypeFormatting) -> Result { - BufferId::new(message.buffer_id) - } -} - -impl InlayHints { - pub async fn lsp_to_project_hint( - lsp_hint: lsp::InlayHint, - buffer_handle: &Entity, - server_id: LanguageServerId, - resolve_state: ResolveState, - force_no_type_left_padding: bool, - cx: &mut AsyncApp, - ) -> anyhow::Result { - let kind = lsp_hint.kind.and_then(|kind| match kind { - lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), - lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), - _ => None, - }); - - let position = buffer_handle.read_with(cx, |buffer, _| { - let position = buffer.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); - if kind == Some(InlayHintKind::Parameter) { - buffer.anchor_before(position) - } else { - buffer.anchor_after(position) - } - })?; - let label = Self::lsp_inlay_label_to_project(lsp_hint.label, server_id) - .await - .context("lsp to project inlay hint conversion")?; - let padding_left = if force_no_type_left_padding && kind == Some(InlayHintKind::Type) { - false - } else { - lsp_hint.padding_left.unwrap_or(false) - }; - - Ok(InlayHint { - position, - padding_left, - padding_right: lsp_hint.padding_right.unwrap_or(false), - label, - kind, - tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip { - lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s), - lsp::InlayHintTooltip::MarkupContent(markup_content) => { - InlayHintTooltip::MarkupContent(MarkupContent { - kind: match markup_content.kind { - lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, - lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, - }, - value: markup_content.value, - }) - } - }), - resolve_state, - }) - } - - async fn lsp_inlay_label_to_project( - lsp_label: lsp::InlayHintLabel, - server_id: LanguageServerId, - ) -> anyhow::Result { - let label = match lsp_label { - lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), - lsp::InlayHintLabel::LabelParts(lsp_parts) => { - let mut parts = Vec::with_capacity(lsp_parts.len()); - for lsp_part in lsp_parts { - parts.push(InlayHintLabelPart { - value: lsp_part.value, - tooltip: lsp_part.tooltip.map(|tooltip| match tooltip { - lsp::InlayHintLabelPartTooltip::String(s) => { - InlayHintLabelPartTooltip::String(s) - } - lsp::InlayHintLabelPartTooltip::MarkupContent(markup_content) => { - InlayHintLabelPartTooltip::MarkupContent(MarkupContent { - kind: match markup_content.kind { - lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, - lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, - }, - value: markup_content.value, - }) - } - }), - location: Some(server_id).zip(lsp_part.location), - }); - } - InlayHintLabel::LabelParts(parts) - } - }; - - Ok(label) - } - - pub fn project_to_proto_hint(response_hint: InlayHint) -> proto::InlayHint { - let (state, lsp_resolve_state) = match response_hint.resolve_state { - ResolveState::Resolved => (0, None), - ResolveState::CanResolve(server_id, resolve_data) => ( - 1, - Some(proto::resolve_state::LspResolveState { - server_id: server_id.0 as u64, - value: resolve_data.map(|json_data| { - serde_json::to_string(&json_data) - .expect("failed to serialize resolve json data") - }), - }), - ), - ResolveState::Resolving => (2, None), - }; - let resolve_state = Some(proto::ResolveState { - state, - lsp_resolve_state, - }); - proto::InlayHint { - position: Some(language::proto::serialize_anchor(&response_hint.position)), - padding_left: response_hint.padding_left, - padding_right: response_hint.padding_right, - label: Some(proto::InlayHintLabel { - label: Some(match response_hint.label { - InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), - InlayHintLabel::LabelParts(label_parts) => { - proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { - parts: label_parts.into_iter().map(|label_part| { - let location_url = label_part.location.as_ref().map(|(_, location)| location.uri.to_string()); - let location_range_start = label_part.location.as_ref().map(|(_, location)| point_from_lsp(location.range.start).0).map(|point| proto::PointUtf16 { row: point.row, column: point.column }); - let location_range_end = label_part.location.as_ref().map(|(_, location)| point_from_lsp(location.range.end).0).map(|point| proto::PointUtf16 { row: point.row, column: point.column }); - proto::InlayHintLabelPart { - value: label_part.value, - tooltip: label_part.tooltip.map(|tooltip| { - let proto_tooltip = match tooltip { - InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), - InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { - is_markdown: markup_content.kind == HoverBlockKind::Markdown, - value: markup_content.value, - }), - }; - proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} - }), - location_url, - location_range_start, - location_range_end, - language_server_id: label_part.location.as_ref().map(|(server_id, _)| server_id.0 as u64), - }}).collect() - }) - } - }), - }), - kind: response_hint.kind.map(|kind| kind.name().to_string()), - tooltip: response_hint.tooltip.map(|response_tooltip| { - let proto_tooltip = match response_tooltip { - InlayHintTooltip::String(s) => proto::inlay_hint_tooltip::Content::Value(s), - InlayHintTooltip::MarkupContent(markup_content) => { - proto::inlay_hint_tooltip::Content::MarkupContent(proto::MarkupContent { - is_markdown: markup_content.kind == HoverBlockKind::Markdown, - value: markup_content.value, - }) - } - }; - proto::InlayHintTooltip { - content: Some(proto_tooltip), - } - }), - resolve_state, - } - } - - pub fn proto_to_project_hint(message_hint: proto::InlayHint) -> anyhow::Result { - let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| { - panic!("incorrect proto inlay hint message: no resolve state in hint {message_hint:?}",) - }); - let resolve_state_data = resolve_state - .lsp_resolve_state.as_ref() - .map(|lsp_resolve_state| { - let value = lsp_resolve_state.value.as_deref().map(|value| { - serde_json::from_str::>(value) - .with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}")) - }).transpose()?.flatten(); - anyhow::Ok((LanguageServerId(lsp_resolve_state.server_id as usize), value)) - }) - .transpose()?; - let resolve_state = match resolve_state.state { - 0 => ResolveState::Resolved, - 1 => { - let (server_id, lsp_resolve_state) = resolve_state_data.with_context(|| { - format!( - "No lsp resolve data for the hint that can be resolved: {message_hint:?}" - ) - })?; - ResolveState::CanResolve(server_id, lsp_resolve_state) - } - 2 => ResolveState::Resolving, - invalid => { - anyhow::bail!("Unexpected resolve state {invalid} for hint {message_hint:?}") - } - }; - Ok(InlayHint { - position: message_hint - .position - .and_then(language::proto::deserialize_anchor) - .context("invalid position")?, - label: match message_hint - .label - .and_then(|label| label.label) - .context("missing label")? - { - proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s), - proto::inlay_hint_label::Label::LabelParts(parts) => { - let mut label_parts = Vec::new(); - for part in parts.parts { - label_parts.push(InlayHintLabelPart { - value: part.value, - tooltip: part.tooltip.map(|tooltip| match tooltip.content { - Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => { - InlayHintLabelPartTooltip::String(s) - } - Some( - proto::inlay_hint_label_part_tooltip::Content::MarkupContent( - markup_content, - ), - ) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent { - kind: if markup_content.is_markdown { - HoverBlockKind::Markdown - } else { - HoverBlockKind::PlainText - }, - value: markup_content.value, - }), - None => InlayHintLabelPartTooltip::String(String::new()), - }), - location: { - match part - .location_url - .zip( - part.location_range_start.and_then(|start| { - Some(start..part.location_range_end?) - }), - ) - .zip(part.language_server_id) - { - Some(((uri, range), server_id)) => Some(( - LanguageServerId(server_id as usize), - lsp::Location { - uri: lsp::Uri::from_str(&uri) - .context("invalid uri in hint part {part:?}")?, - range: lsp::Range::new( - point_to_lsp(PointUtf16::new( - range.start.row, - range.start.column, - )), - point_to_lsp(PointUtf16::new( - range.end.row, - range.end.column, - )), - ), - }, - )), - None => None, - } - }, - }); - } - - InlayHintLabel::LabelParts(label_parts) - } - }, - padding_left: message_hint.padding_left, - padding_right: message_hint.padding_right, - kind: message_hint - .kind - .as_deref() - .and_then(InlayHintKind::from_name), - tooltip: message_hint.tooltip.and_then(|tooltip| { - Some(match tooltip.content? { - proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s), - proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => { - InlayHintTooltip::MarkupContent(MarkupContent { - kind: if markup_content.is_markdown { - HoverBlockKind::Markdown - } else { - HoverBlockKind::PlainText - }, - value: markup_content.value, - }) - } - }) - }), - resolve_state, - }) - } - - pub fn project_to_lsp_hint(hint: InlayHint, snapshot: &BufferSnapshot) -> lsp::InlayHint { - lsp::InlayHint { - position: point_to_lsp(hint.position.to_point_utf16(snapshot)), - kind: hint.kind.map(|kind| match kind { - InlayHintKind::Type => lsp::InlayHintKind::TYPE, - InlayHintKind::Parameter => lsp::InlayHintKind::PARAMETER, - }), - text_edits: None, - tooltip: hint.tooltip.and_then(|tooltip| { - Some(match tooltip { - InlayHintTooltip::String(s) => lsp::InlayHintTooltip::String(s), - InlayHintTooltip::MarkupContent(markup_content) => { - lsp::InlayHintTooltip::MarkupContent(lsp::MarkupContent { - kind: match markup_content.kind { - HoverBlockKind::PlainText => lsp::MarkupKind::PlainText, - HoverBlockKind::Markdown => lsp::MarkupKind::Markdown, - HoverBlockKind::Code { .. } => return None, - }, - value: markup_content.value, - }) - } - }) - }), - label: match hint.label { - InlayHintLabel::String(s) => lsp::InlayHintLabel::String(s), - InlayHintLabel::LabelParts(label_parts) => lsp::InlayHintLabel::LabelParts( - label_parts - .into_iter() - .map(|part| lsp::InlayHintLabelPart { - value: part.value, - tooltip: part.tooltip.and_then(|tooltip| { - Some(match tooltip { - InlayHintLabelPartTooltip::String(s) => { - lsp::InlayHintLabelPartTooltip::String(s) - } - InlayHintLabelPartTooltip::MarkupContent(markup_content) => { - lsp::InlayHintLabelPartTooltip::MarkupContent( - lsp::MarkupContent { - kind: match markup_content.kind { - HoverBlockKind::PlainText => { - lsp::MarkupKind::PlainText - } - HoverBlockKind::Markdown => { - lsp::MarkupKind::Markdown - } - HoverBlockKind::Code { .. } => return None, - }, - value: markup_content.value, - }, - ) - } - }) - }), - location: part.location.map(|(_, location)| location), - command: None, - }) - .collect(), - ), - }, - padding_left: Some(hint.padding_left), - padding_right: Some(hint.padding_right), - data: match hint.resolve_state { - ResolveState::CanResolve(_, data) => data, - ResolveState::Resolving | ResolveState::Resolved => None, - }, - } - } - - pub fn can_resolve_inlays(capabilities: &ServerCapabilities) -> bool { - capabilities - .inlay_hint_provider - .as_ref() - .and_then(|options| match options { - OneOf::Left(_is_supported) => None, - OneOf::Right(capabilities) => match capabilities { - lsp::InlayHintServerCapabilities::Options(o) => o.resolve_provider, - lsp::InlayHintServerCapabilities::RegistrationOptions(o) => { - o.inlay_hint_options.resolve_provider - } - }, - }) - .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)] -impl LspCommand for InlayHints { - type Response = Vec; - type LspRequest = lsp::InlayHintRequest; - type ProtoRequest = proto::InlayHints; - - fn display_name(&self) -> &str { - "Inlay hints" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - Self::check_capabilities(&capabilities.server_capabilities) - } - - fn to_lsp( - &self, - path: &Path, - buffer: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::InlayHintParams { - text_document: lsp::TextDocumentIdentifier { - uri: file_path_to_lsp_url(path)?, - }, - range: range_to_lsp(self.range.to_point_utf16(buffer))?, - work_done_progress_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: Option>, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - mut cx: AsyncApp, - ) -> anyhow::Result> { - let (lsp_adapter, lsp_server) = - language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; - // `typescript-language-server` adds padding to the left for type hints, turning - // `const foo: boolean` into `const foo : boolean` which looks odd. - // `rust-analyzer` does not have the padding for this case, and we have to accommodate both. - // - // We could trim the whole string, but being pessimistic on par with the situation above, - // there might be a hint with multiple whitespaces at the end(s) which we need to display properly. - // Hence let's use a heuristic first to handle the most awkward case and look for more. - let force_no_type_left_padding = - lsp_adapter.name.0.as_ref() == "typescript-language-server"; - - let hints = message.unwrap_or_default().into_iter().map(|lsp_hint| { - let resolve_state = if InlayHints::can_resolve_inlays(&lsp_server.capabilities()) { - ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()) - } else { - ResolveState::Resolved - }; - - let buffer = buffer.clone(); - cx.spawn(async move |cx| { - InlayHints::lsp_to_project_hint( - lsp_hint, - &buffer, - server_id, - resolve_state, - force_no_type_left_padding, - cx, - ) - .await - }) - }); - future::join_all(hints) - .await - .into_iter() - .collect::>() - .context("lsp to project inlay hints conversion") - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::InlayHints { - proto::InlayHints { - project_id, - buffer_id: buffer.remote_id().into(), - start: Some(language::proto::serialize_anchor(&self.range.start)), - end: Some(language::proto::serialize_anchor(&self.range.end)), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::InlayHints, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let start = message - .start - .and_then(language::proto::deserialize_anchor) - .context("invalid start")?; - let end = message - .end - .and_then(language::proto::deserialize_anchor) - .context("invalid end")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - - Ok(Self { range: start..end }) - } - - fn response_to_proto( - response: Vec, - _: &mut LspStore, - _: PeerId, - buffer_version: &clock::Global, - _: &mut App, - ) -> proto::InlayHintsResponse { - proto::InlayHintsResponse { - hints: response - .into_iter() - .map(InlayHints::project_to_proto_hint) - .collect(), - version: serialize_version(buffer_version), - } - } - - async fn response_from_proto( - self, - message: proto::InlayHintsResponse, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> anyhow::Result> { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - - let mut hints = Vec::new(); - for message_hint in message.hints { - hints.push(InlayHints::proto_to_project_hint(message_hint)?); - } - - Ok(hints) - } - - fn buffer_id_from_proto(message: &proto::InlayHints) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetCodeLens { - type Response = Vec; - type LspRequest = lsp::CodeLensRequest; - type ProtoRequest = proto::GetCodeLens; - - fn display_name(&self) -> &str { - "Code Lens" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - capabilities - .server_capabilities - .code_lens_provider - .is_some() - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::CodeLensParams { - text_document: lsp::TextDocumentIdentifier { - uri: file_path_to_lsp_url(path)?, - }, - work_done_progress_params: lsp::WorkDoneProgressParams::default(), - partial_result_params: lsp::PartialResultParams::default(), - }) - } - - async fn response_from_lsp( - self, - message: Option>, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - cx: AsyncApp, - ) -> anyhow::Result> { - let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot())?; - let language_server = cx.update(|cx| { - lsp_store - .read(cx) - .language_server_for_id(server_id) - .with_context(|| { - format!("Missing the language server that just returned a response {server_id}") - }) - })??; - let server_capabilities = language_server.capabilities(); - let available_commands = server_capabilities - .execute_command_provider - .as_ref() - .map(|options| options.commands.as_slice()) - .unwrap_or_default(); - Ok(message - .unwrap_or_default() - .into_iter() - .filter(|code_lens| { - code_lens - .command - .as_ref() - .is_none_or(|command| available_commands.contains(&command.command)) - }) - .map(|code_lens| { - let code_lens_range = range_from_lsp(code_lens.range); - let start = snapshot.clip_point_utf16(code_lens_range.start, Bias::Left); - let end = snapshot.clip_point_utf16(code_lens_range.end, Bias::Right); - let range = snapshot.anchor_before(start)..snapshot.anchor_after(end); - CodeAction { - server_id, - range, - lsp_action: LspAction::CodeLens(code_lens), - resolved: false, - } - }) - .collect()) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCodeLens { - proto::GetCodeLens { - project_id, - buffer_id: buffer.remote_id().into(), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::GetCodeLens, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - Ok(Self) - } - - fn response_to_proto( - response: Vec, - _: &mut LspStore, - _: PeerId, - buffer_version: &clock::Global, - _: &mut App, - ) -> proto::GetCodeLensResponse { - proto::GetCodeLensResponse { - lens_actions: response - .iter() - .map(LspStore::serialize_code_action) - .collect(), - version: serialize_version(buffer_version), - } - } - - async fn response_from_proto( - self, - message: proto::GetCodeLensResponse, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> anyhow::Result> { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - message - .lens_actions - .into_iter() - .map(LspStore::deserialize_code_action) - .collect::>>() - .context("deserializing proto code lens response") - } - - fn buffer_id_from_proto(message: &proto::GetCodeLens) -> Result { - BufferId::new(message.buffer_id) - } -} - -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>; - type LspRequest = lsp::request::LinkedEditingRange; - type ProtoRequest = proto::LinkedEditingRange; - - fn display_name(&self) -> &str { - "Linked editing range" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - Self::check_server_capabilities(capabilities.server_capabilities) - } - - fn to_lsp( - &self, - path: &Path, - buffer: &Buffer, - _server: &Arc, - _: &App, - ) -> Result { - let position = self.position.to_point_utf16(&buffer.snapshot()); - Ok(lsp::LinkedEditingRangeParams { - text_document_position_params: make_lsp_text_document_position(path, position)?, - work_done_progress_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: Option, - _: Entity, - buffer: Entity, - _server_id: LanguageServerId, - cx: AsyncApp, - ) -> Result>> { - if let Some(lsp::LinkedEditingRanges { mut ranges, .. }) = message { - ranges.sort_by_key(|range| range.start); - - buffer.read_with(&cx, |buffer, _| { - ranges - .into_iter() - .map(|range| { - let start = - buffer.clip_point_utf16(point_from_lsp(range.start), Bias::Left); - let end = buffer.clip_point_utf16(point_from_lsp(range.end), Bias::Left); - buffer.anchor_before(start)..buffer.anchor_after(end) - }) - .collect() - }) - } else { - Ok(vec![]) - } - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LinkedEditingRange { - proto::LinkedEditingRange { - project_id, - buffer_id: buffer.remote_id().to_proto(), - position: Some(serialize_anchor(&self.position)), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::LinkedEditingRange, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message.position.context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - let position = deserialize_anchor(position).context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| buffer.wait_for_anchors([position]))? - .await?; - Ok(Self { position }) - } - - fn response_to_proto( - response: Vec>, - _: &mut LspStore, - _: PeerId, - buffer_version: &clock::Global, - _: &mut App, - ) -> proto::LinkedEditingRangeResponse { - proto::LinkedEditingRangeResponse { - items: response - .into_iter() - .map(|range| proto::AnchorRange { - start: Some(serialize_anchor(&range.start)), - end: Some(serialize_anchor(&range.end)), - }) - .collect(), - version: serialize_version(buffer_version), - } - } - - async fn response_from_proto( - self, - message: proto::LinkedEditingRangeResponse, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result>> { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - let items: Vec> = message - .items - .into_iter() - .filter_map(|range| { - let start = deserialize_anchor(range.start?)?; - let end = deserialize_anchor(range.end?)?; - Some(start..end) - }) - .collect(); - for range in &items { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_anchors([range.start, range.end]) - })? - .await?; - } - Ok(items) - } - - fn buffer_id_from_proto(message: &proto::LinkedEditingRange) -> Result { - BufferId::new(message.buffer_id) - } -} - -impl GetDocumentDiagnostics { - pub fn diagnostics_from_proto( - response: proto::GetDocumentDiagnosticsResponse, - ) -> Vec { - response - .pulled_diagnostics - .into_iter() - .filter_map(|diagnostics| { - Some(LspPullDiagnostics::Response { - registration_id: diagnostics.registration_id.map(SharedString::from), - server_id: LanguageServerId::from_proto(diagnostics.server_id), - uri: lsp::Uri::from_str(diagnostics.uri.as_str()).log_err()?, - diagnostics: if diagnostics.changed { - PulledDiagnostics::Unchanged { - result_id: SharedString::new(diagnostics.result_id?), - } - } else { - PulledDiagnostics::Changed { - result_id: diagnostics.result_id.map(SharedString::new), - diagnostics: diagnostics - .diagnostics - .into_iter() - .filter_map(|diagnostic| { - GetDocumentDiagnostics::deserialize_lsp_diagnostic(diagnostic) - .context("deserializing diagnostics") - .log_err() - }) - .collect(), - } - }, - }) - }) - .collect() - } - - fn deserialize_lsp_diagnostic(diagnostic: proto::LspDiagnostic) -> Result { - let start = diagnostic.start.context("invalid start range")?; - let end = diagnostic.end.context("invalid end range")?; - - let range = Range:: { - start: PointUtf16 { - row: start.row, - column: start.column, - }, - end: PointUtf16 { - row: end.row, - column: end.column, - }, - }; - - let data = diagnostic.data.and_then(|data| Value::from_str(&data).ok()); - let code = diagnostic.code.map(lsp::NumberOrString::String); - - let related_information = diagnostic - .related_information - .into_iter() - .map(|info| { - let start = info.location_range_start.unwrap(); - let end = info.location_range_end.unwrap(); - - lsp::DiagnosticRelatedInformation { - location: lsp::Location { - range: lsp::Range { - start: point_to_lsp(PointUtf16::new(start.row, start.column)), - end: point_to_lsp(PointUtf16::new(end.row, end.column)), - }, - uri: lsp::Uri::from_str(&info.location_url.unwrap()).unwrap(), - }, - message: info.message, - } - }) - .collect::>(); - - let tags = diagnostic - .tags - .into_iter() - .filter_map(|tag| match proto::LspDiagnosticTag::from_i32(tag) { - Some(proto::LspDiagnosticTag::Unnecessary) => Some(lsp::DiagnosticTag::UNNECESSARY), - Some(proto::LspDiagnosticTag::Deprecated) => Some(lsp::DiagnosticTag::DEPRECATED), - _ => None, - }) - .collect::>(); - - Ok(lsp::Diagnostic { - range: language::range_to_lsp(range)?, - severity: match proto::lsp_diagnostic::Severity::from_i32(diagnostic.severity).unwrap() - { - proto::lsp_diagnostic::Severity::Error => Some(lsp::DiagnosticSeverity::ERROR), - proto::lsp_diagnostic::Severity::Warning => Some(lsp::DiagnosticSeverity::WARNING), - proto::lsp_diagnostic::Severity::Information => { - Some(lsp::DiagnosticSeverity::INFORMATION) - } - proto::lsp_diagnostic::Severity::Hint => Some(lsp::DiagnosticSeverity::HINT), - _ => None, - }, - code, - code_description: diagnostic - .code_description - .map(|code_description| CodeDescription { - href: Some(lsp::Uri::from_str(&code_description).unwrap()), - }), - related_information: Some(related_information), - tags: Some(tags), - source: diagnostic.source.clone(), - message: diagnostic.message, - data, - }) - } - - fn serialize_lsp_diagnostic(diagnostic: lsp::Diagnostic) -> Result { - let range = language::range_from_lsp(diagnostic.range); - let related_information = diagnostic - .related_information - .unwrap_or_default() - .into_iter() - .map(|related_information| { - let location_range_start = - point_from_lsp(related_information.location.range.start).0; - let location_range_end = point_from_lsp(related_information.location.range.end).0; - - Ok(proto::LspDiagnosticRelatedInformation { - location_url: Some(related_information.location.uri.to_string()), - location_range_start: Some(proto::PointUtf16 { - row: location_range_start.row, - column: location_range_start.column, - }), - location_range_end: Some(proto::PointUtf16 { - row: location_range_end.row, - column: location_range_end.column, - }), - message: related_information.message, - }) - }) - .collect::>>()?; - - let tags = diagnostic - .tags - .unwrap_or_default() - .into_iter() - .map(|tag| match tag { - lsp::DiagnosticTag::UNNECESSARY => proto::LspDiagnosticTag::Unnecessary, - lsp::DiagnosticTag::DEPRECATED => proto::LspDiagnosticTag::Deprecated, - _ => proto::LspDiagnosticTag::None, - } as i32) - .collect(); - - Ok(proto::LspDiagnostic { - start: Some(proto::PointUtf16 { - row: range.start.0.row, - column: range.start.0.column, - }), - end: Some(proto::PointUtf16 { - row: range.end.0.row, - column: range.end.0.column, - }), - severity: match diagnostic.severity { - Some(lsp::DiagnosticSeverity::ERROR) => proto::lsp_diagnostic::Severity::Error, - Some(lsp::DiagnosticSeverity::WARNING) => proto::lsp_diagnostic::Severity::Warning, - Some(lsp::DiagnosticSeverity::INFORMATION) => { - proto::lsp_diagnostic::Severity::Information - } - Some(lsp::DiagnosticSeverity::HINT) => proto::lsp_diagnostic::Severity::Hint, - _ => proto::lsp_diagnostic::Severity::None, - } as i32, - code: diagnostic.code.as_ref().map(|code| match code { - lsp::NumberOrString::Number(code) => code.to_string(), - lsp::NumberOrString::String(code) => code.clone(), - }), - source: diagnostic.source.clone(), - related_information, - tags, - code_description: diagnostic - .code_description - .and_then(|desc| desc.href.map(|url| url.to_string())), - message: diagnostic.message, - data: diagnostic.data.as_ref().map(|data| data.to_string()), - }) - } - - pub fn deserialize_workspace_diagnostics_report( - report: lsp::WorkspaceDiagnosticReportResult, - server_id: LanguageServerId, - registration_id: Option, - ) -> Vec { - let mut pulled_diagnostics = HashMap::default(); - match report { - lsp::WorkspaceDiagnosticReportResult::Report(workspace_diagnostic_report) => { - for report in workspace_diagnostic_report.items { - match report { - lsp::WorkspaceDocumentDiagnosticReport::Full(report) => { - process_full_workspace_diagnostics_report( - &mut pulled_diagnostics, - server_id, - report, - registration_id.clone(), - ) - } - lsp::WorkspaceDocumentDiagnosticReport::Unchanged(report) => { - process_unchanged_workspace_diagnostics_report( - &mut pulled_diagnostics, - server_id, - report, - registration_id.clone(), - ) - } - } - } - } - lsp::WorkspaceDiagnosticReportResult::Partial( - workspace_diagnostic_report_partial_result, - ) => { - for report in workspace_diagnostic_report_partial_result.items { - match report { - lsp::WorkspaceDocumentDiagnosticReport::Full(report) => { - process_full_workspace_diagnostics_report( - &mut pulled_diagnostics, - server_id, - report, - registration_id.clone(), - ) - } - lsp::WorkspaceDocumentDiagnosticReport::Unchanged(report) => { - process_unchanged_workspace_diagnostics_report( - &mut pulled_diagnostics, - server_id, - report, - registration_id.clone(), - ) - } - } - } - } - } - pulled_diagnostics.into_values().collect() - } -} - -#[derive(Debug)] -pub struct WorkspaceLspPullDiagnostics { - pub version: Option, - pub diagnostics: LspPullDiagnostics, -} - -fn process_full_workspace_diagnostics_report( - diagnostics: &mut HashMap, - server_id: LanguageServerId, - report: lsp::WorkspaceFullDocumentDiagnosticReport, - registration_id: Option, -) { - let mut new_diagnostics = HashMap::default(); - process_full_diagnostics_report( - &mut new_diagnostics, - server_id, - report.uri, - report.full_document_diagnostic_report, - registration_id, - ); - diagnostics.extend(new_diagnostics.into_iter().map(|(uri, diagnostics)| { - ( - uri, - WorkspaceLspPullDiagnostics { - version: report.version.map(|v| v as i32), - diagnostics, - }, - ) - })); -} - -fn process_unchanged_workspace_diagnostics_report( - diagnostics: &mut HashMap, - server_id: LanguageServerId, - report: lsp::WorkspaceUnchangedDocumentDiagnosticReport, - registration_id: Option, -) { - let mut new_diagnostics = HashMap::default(); - process_unchanged_diagnostics_report( - &mut new_diagnostics, - server_id, - report.uri, - report.unchanged_document_diagnostic_report, - registration_id, - ); - diagnostics.extend(new_diagnostics.into_iter().map(|(uri, diagnostics)| { - ( - uri, - WorkspaceLspPullDiagnostics { - version: report.version.map(|v| v as i32), - diagnostics, - }, - ) - })); -} - -#[async_trait(?Send)] -impl LspCommand for GetDocumentDiagnostics { - type Response = Vec; - type LspRequest = lsp::request::DocumentDiagnosticRequest; - type ProtoRequest = proto::GetDocumentDiagnostics; - - fn display_name(&self) -> &str { - "Get diagnostics" - } - - fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool { - true - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::DocumentDiagnosticParams { - text_document: lsp::TextDocumentIdentifier { - uri: file_path_to_lsp_url(path)?, - }, - identifier: self.identifier.clone(), - previous_result_id: self.previous_result_id.clone().map(|id| id.to_string()), - partial_result_params: Default::default(), - work_done_progress_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: lsp::DocumentDiagnosticReportResult, - _: Entity, - buffer: Entity, - server_id: LanguageServerId, - cx: AsyncApp, - ) -> Result { - let url = buffer.read_with(&cx, |buffer, cx| { - buffer - .file() - .and_then(|file| file.as_local()) - .map(|file| { - let abs_path = file.abs_path(cx); - file_path_to_lsp_url(&abs_path) - }) - .transpose()? - .with_context(|| format!("missing url on buffer {}", buffer.remote_id())) - })??; - - let mut pulled_diagnostics = HashMap::default(); - match message { - lsp::DocumentDiagnosticReportResult::Report(report) => match report { - lsp::DocumentDiagnosticReport::Full(report) => { - if let Some(related_documents) = report.related_documents { - process_related_documents( - &mut pulled_diagnostics, - server_id, - related_documents, - self.registration_id.clone(), - ); - } - process_full_diagnostics_report( - &mut pulled_diagnostics, - server_id, - url, - report.full_document_diagnostic_report, - self.registration_id, - ); - } - lsp::DocumentDiagnosticReport::Unchanged(report) => { - if let Some(related_documents) = report.related_documents { - process_related_documents( - &mut pulled_diagnostics, - server_id, - related_documents, - self.registration_id.clone(), - ); - } - process_unchanged_diagnostics_report( - &mut pulled_diagnostics, - server_id, - url, - report.unchanged_document_diagnostic_report, - self.registration_id, - ); - } - }, - lsp::DocumentDiagnosticReportResult::Partial(report) => { - if let Some(related_documents) = report.related_documents { - process_related_documents( - &mut pulled_diagnostics, - server_id, - related_documents, - self.registration_id, - ); - } - } - } - - Ok(pulled_diagnostics.into_values().collect()) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDocumentDiagnostics { - proto::GetDocumentDiagnostics { - project_id, - buffer_id: buffer.remote_id().into(), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - _: proto::GetDocumentDiagnostics, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> Result { - anyhow::bail!( - "proto::GetDocumentDiagnostics is not expected to be converted from proto directly, as it needs `previous_result_id` fetched first" - ) - } - - fn response_to_proto( - response: Self::Response, - _: &mut LspStore, - _: PeerId, - _: &clock::Global, - _: &mut App, - ) -> proto::GetDocumentDiagnosticsResponse { - let pulled_diagnostics = response - .into_iter() - .filter_map(|diagnostics| match diagnostics { - LspPullDiagnostics::Default => None, - LspPullDiagnostics::Response { - server_id, - uri, - diagnostics, - registration_id, - } => { - let mut changed = false; - let (diagnostics, result_id) = match diagnostics { - PulledDiagnostics::Unchanged { result_id } => (Vec::new(), Some(result_id)), - PulledDiagnostics::Changed { - result_id, - diagnostics, - } => { - changed = true; - (diagnostics, result_id) - } - }; - Some(proto::PulledDiagnostics { - changed, - result_id: result_id.map(|id| id.to_string()), - uri: uri.to_string(), - server_id: server_id.to_proto(), - diagnostics: diagnostics - .into_iter() - .filter_map(|diagnostic| { - GetDocumentDiagnostics::serialize_lsp_diagnostic(diagnostic) - .context("serializing diagnostics") - .log_err() - }) - .collect(), - registration_id: registration_id.as_ref().map(ToString::to_string), - }) - } - }) - .collect(); - - proto::GetDocumentDiagnosticsResponse { pulled_diagnostics } - } - - async fn response_from_proto( - self, - response: proto::GetDocumentDiagnosticsResponse, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> Result { - Ok(Self::diagnostics_from_proto(response)) - } - - fn buffer_id_from_proto(message: &proto::GetDocumentDiagnostics) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetDocumentColor { - type Response = Vec; - type LspRequest = lsp::request::DocumentColor; - type ProtoRequest = proto::GetDocumentColor; - - fn display_name(&self) -> &str { - "Document color" - } - - fn check_capabilities(&self, server_capabilities: AdapterServerCapabilities) -> bool { - server_capabilities - .server_capabilities - .color_provider - .as_ref() - .is_some_and(|capability| match capability { - lsp::ColorProviderCapability::Simple(supported) => *supported, - lsp::ColorProviderCapability::ColorProvider(..) => true, - lsp::ColorProviderCapability::Options(..) => true, - }) - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::DocumentColorParams { - text_document: make_text_document_identifier(path)?, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: Vec, - _: Entity, - _: Entity, - _: LanguageServerId, - _: AsyncApp, - ) -> Result { - Ok(message - .into_iter() - .map(|color| DocumentColor { - lsp_range: color.range, - color: color.color, - resolved: false, - color_presentations: Vec::new(), - }) - .collect()) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest { - proto::GetDocumentColor { - project_id, - buffer_id: buffer.remote_id().to_proto(), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - _: Self::ProtoRequest, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> Result { - Ok(Self {}) - } - - fn response_to_proto( - response: Self::Response, - _: &mut LspStore, - _: PeerId, - buffer_version: &clock::Global, - _: &mut App, - ) -> proto::GetDocumentColorResponse { - proto::GetDocumentColorResponse { - colors: response - .into_iter() - .map(|color| { - let start = point_from_lsp(color.lsp_range.start).0; - let end = point_from_lsp(color.lsp_range.end).0; - proto::ColorInformation { - red: color.color.red, - green: color.color.green, - blue: color.color.blue, - alpha: color.color.alpha, - lsp_range_start: Some(proto::PointUtf16 { - row: start.row, - column: start.column, - }), - lsp_range_end: Some(proto::PointUtf16 { - row: end.row, - column: end.column, - }), - } - }) - .collect(), - version: serialize_version(buffer_version), - } - } - - async fn response_from_proto( - self, - message: proto::GetDocumentColorResponse, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> Result { - Ok(message - .colors - .into_iter() - .filter_map(|color| { - let start = color.lsp_range_start?; - let start = PointUtf16::new(start.row, start.column); - let end = color.lsp_range_end?; - let end = PointUtf16::new(end.row, end.column); - Some(DocumentColor { - resolved: false, - color_presentations: Vec::new(), - lsp_range: lsp::Range { - start: point_to_lsp(start), - end: point_to_lsp(end), - }, - color: lsp::Color { - red: color.red, - green: color.green, - blue: color.blue, - alpha: color.alpha, - }, - }) - }) - .collect()) - } - - fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result { - BufferId::new(message.buffer_id) - } -} - -fn process_related_documents( - diagnostics: &mut HashMap, - server_id: LanguageServerId, - documents: impl IntoIterator, - registration_id: Option, -) { - for (url, report_kind) in documents { - match report_kind { - lsp::DocumentDiagnosticReportKind::Full(report) => process_full_diagnostics_report( - diagnostics, - server_id, - url, - report, - registration_id.clone(), - ), - lsp::DocumentDiagnosticReportKind::Unchanged(report) => { - process_unchanged_diagnostics_report( - diagnostics, - server_id, - url, - report, - registration_id.clone(), - ) - } - } - } -} - -fn process_unchanged_diagnostics_report( - diagnostics: &mut HashMap, - server_id: LanguageServerId, - uri: lsp::Uri, - report: lsp::UnchangedDocumentDiagnosticReport, - registration_id: Option, -) { - let result_id = SharedString::new(report.result_id); - match diagnostics.entry(uri.clone()) { - hash_map::Entry::Occupied(mut o) => match o.get_mut() { - LspPullDiagnostics::Default => { - o.insert(LspPullDiagnostics::Response { - server_id, - uri, - diagnostics: PulledDiagnostics::Unchanged { result_id }, - registration_id, - }); - } - LspPullDiagnostics::Response { - server_id: existing_server_id, - uri: existing_uri, - diagnostics: existing_diagnostics, - .. - } => { - if server_id != *existing_server_id || &uri != existing_uri { - debug_panic!( - "Unexpected state: file {uri} has two different sets of diagnostics reported" - ); - } - match existing_diagnostics { - PulledDiagnostics::Unchanged { .. } => { - *existing_diagnostics = PulledDiagnostics::Unchanged { result_id }; - } - PulledDiagnostics::Changed { .. } => {} - } - } - }, - hash_map::Entry::Vacant(v) => { - v.insert(LspPullDiagnostics::Response { - server_id, - uri, - diagnostics: PulledDiagnostics::Unchanged { result_id }, - registration_id, - }); - } - } -} - -fn process_full_diagnostics_report( - diagnostics: &mut HashMap, - server_id: LanguageServerId, - uri: lsp::Uri, - report: lsp::FullDocumentDiagnosticReport, - registration_id: Option, -) { - let result_id = report.result_id.map(SharedString::new); - match diagnostics.entry(uri.clone()) { - hash_map::Entry::Occupied(mut o) => match o.get_mut() { - LspPullDiagnostics::Default => { - o.insert(LspPullDiagnostics::Response { - server_id, - uri, - diagnostics: PulledDiagnostics::Changed { - result_id, - diagnostics: report.items, - }, - registration_id, - }); - } - LspPullDiagnostics::Response { - server_id: existing_server_id, - uri: existing_uri, - diagnostics: existing_diagnostics, - .. - } => { - if server_id != *existing_server_id || &uri != existing_uri { - debug_panic!( - "Unexpected state: file {uri} has two different sets of diagnostics reported" - ); - } - match existing_diagnostics { - PulledDiagnostics::Unchanged { .. } => { - *existing_diagnostics = PulledDiagnostics::Changed { - result_id, - diagnostics: report.items, - }; - } - PulledDiagnostics::Changed { - result_id: existing_result_id, - diagnostics: existing_diagnostics, - } => { - if result_id.is_some() { - *existing_result_id = result_id; - } - existing_diagnostics.extend(report.items); - } - } - } - }, - hash_map::Entry::Vacant(v) => { - v.insert(LspPullDiagnostics::Response { - server_id, - uri, - diagnostics: PulledDiagnostics::Changed { - result_id, - diagnostics: report.items, - }, - registration_id, - }); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use lsp::{DiagnosticSeverity, DiagnosticTag}; - use serde_json::json; - - #[test] - fn test_serialize_lsp_diagnostic() { - let lsp_diagnostic = lsp::Diagnostic { - range: lsp::Range { - start: lsp::Position::new(0, 1), - end: lsp::Position::new(2, 3), - }, - severity: Some(DiagnosticSeverity::ERROR), - code: Some(lsp::NumberOrString::String("E001".to_string())), - source: Some("test-source".to_string()), - message: "Test error message".to_string(), - related_information: None, - tags: Some(vec![DiagnosticTag::DEPRECATED]), - code_description: None, - data: Some(json!({"detail": "test detail"})), - }; - - let proto_diagnostic = GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic) - .expect("Failed to serialize diagnostic"); - - let start = proto_diagnostic.start.unwrap(); - let end = proto_diagnostic.end.unwrap(); - assert_eq!(start.row, 0); - assert_eq!(start.column, 1); - assert_eq!(end.row, 2); - assert_eq!(end.column, 3); - assert_eq!( - proto_diagnostic.severity, - proto::lsp_diagnostic::Severity::Error as i32 - ); - assert_eq!(proto_diagnostic.code, Some("E001".to_string())); - assert_eq!(proto_diagnostic.source, Some("test-source".to_string())); - assert_eq!(proto_diagnostic.message, "Test error message"); - } - - #[test] - fn test_deserialize_lsp_diagnostic() { - let proto_diagnostic = proto::LspDiagnostic { - start: Some(proto::PointUtf16 { row: 0, column: 1 }), - end: Some(proto::PointUtf16 { row: 2, column: 3 }), - severity: proto::lsp_diagnostic::Severity::Warning as i32, - code: Some("ERR".to_string()), - source: Some("Prism".to_string()), - message: "assigned but unused variable - a".to_string(), - related_information: vec![], - tags: vec![], - code_description: None, - data: None, - }; - - let lsp_diagnostic = GetDocumentDiagnostics::deserialize_lsp_diagnostic(proto_diagnostic) - .expect("Failed to deserialize diagnostic"); - - assert_eq!(lsp_diagnostic.range.start.line, 0); - assert_eq!(lsp_diagnostic.range.start.character, 1); - assert_eq!(lsp_diagnostic.range.end.line, 2); - assert_eq!(lsp_diagnostic.range.end.character, 3); - assert_eq!(lsp_diagnostic.severity, Some(DiagnosticSeverity::WARNING)); - assert_eq!( - lsp_diagnostic.code, - Some(lsp::NumberOrString::String("ERR".to_string())) - ); - assert_eq!(lsp_diagnostic.source, Some("Prism".to_string())); - assert_eq!(lsp_diagnostic.message, "assigned but unused variable - a"); - } - - #[test] - fn test_related_information() { - let related_info = lsp::DiagnosticRelatedInformation { - location: lsp::Location { - uri: lsp::Uri::from_str("file:///test.rs").unwrap(), - range: lsp::Range { - start: lsp::Position::new(1, 1), - end: lsp::Position::new(1, 5), - }, - }, - message: "Related info message".to_string(), - }; - - let lsp_diagnostic = lsp::Diagnostic { - range: lsp::Range { - start: lsp::Position::new(0, 0), - end: lsp::Position::new(0, 1), - }, - severity: Some(DiagnosticSeverity::INFORMATION), - code: None, - source: Some("Prism".to_string()), - message: "assigned but unused variable - a".to_string(), - related_information: Some(vec![related_info]), - tags: None, - code_description: None, - data: None, - }; - - let proto_diagnostic = GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic) - .expect("Failed to serialize diagnostic"); - - assert_eq!(proto_diagnostic.related_information.len(), 1); - let related = &proto_diagnostic.related_information[0]; - assert_eq!(related.location_url, Some("file:///test.rs".to_string())); - assert_eq!(related.message, "Related info message"); - } - - #[test] - fn test_invalid_ranges() { - let proto_diagnostic = proto::LspDiagnostic { - start: None, - end: Some(proto::PointUtf16 { row: 2, column: 3 }), - severity: proto::lsp_diagnostic::Severity::Error as i32, - code: None, - source: None, - message: "Test message".to_string(), - related_information: vec![], - tags: vec![], - code_description: None, - data: None, - }; - - let result = GetDocumentDiagnostics::deserialize_lsp_diagnostic(proto_diagnostic); - assert!(result.is_err()); - } -} diff --git a/crates/project/src/lsp_command/signature_help.rs b/crates/project/src/lsp_command/signature_help.rs deleted file mode 100644 index 6a49931183..0000000000 --- a/crates/project/src/lsp_command/signature_help.rs +++ /dev/null @@ -1,793 +0,0 @@ -use std::{ops::Range, sync::Arc}; - -use gpui::{App, AppContext, Entity, FontWeight, HighlightStyle, SharedString}; -use language::LanguageRegistry; -use lsp::LanguageServerId; -use markdown::Markdown; -use rpc::proto::{self, documentation}; -use util::maybe; - -#[derive(Debug)] -pub struct SignatureHelp { - pub active_signature: usize, - pub signatures: Vec, - pub(super) original_data: lsp::SignatureHelp, -} - -#[derive(Debug, Clone)] -pub struct SignatureHelpData { - pub label: SharedString, - pub documentation: Option>, - pub highlights: Vec<(Range, HighlightStyle)>, - pub active_parameter: Option, - pub parameters: Vec, -} - -#[derive(Debug, Clone)] -pub struct ParameterInfo { - pub label_range: Option>, - pub documentation: Option>, -} - -impl SignatureHelp { - pub fn new( - help: lsp::SignatureHelp, - language_registry: Option>, - lang_server_id: Option, - cx: &mut App, - ) -> Option { - if help.signatures.is_empty() { - return None; - } - let active_signature = help.active_signature.unwrap_or(0) as usize; - let mut signatures = Vec::::with_capacity(help.signatures.capacity()); - for signature in &help.signatures { - let label = SharedString::from(signature.label.clone()); - let active_parameter = signature - .active_parameter - .unwrap_or_else(|| help.active_parameter.unwrap_or(0)) - as usize; - let mut highlights = Vec::new(); - let mut parameter_infos = Vec::new(); - - if let Some(parameters) = &signature.parameters { - for (index, parameter) in parameters.iter().enumerate() { - let label_range = match ¶meter.label { - &lsp::ParameterLabel::LabelOffsets([offset1, offset2]) => { - maybe!({ - let offset1 = offset1 as usize; - let offset2 = offset2 as usize; - if offset1 < offset2 { - let mut indices = label.char_indices().scan( - 0, - |utf16_offset_acc, (offset, c)| { - let utf16_offset = *utf16_offset_acc; - *utf16_offset_acc += c.len_utf16(); - Some((utf16_offset, offset)) - }, - ); - let (_, offset1) = indices - .find(|(utf16_offset, _)| *utf16_offset == offset1)?; - let (_, offset2) = indices - .find(|(utf16_offset, _)| *utf16_offset == offset2)?; - Some(offset1..offset2) - } else { - log::warn!( - "language server {lang_server_id:?} produced invalid parameter label range: {offset1:?}..{offset2:?}", - ); - None - } - }) - } - lsp::ParameterLabel::Simple(parameter_label) => { - if let Some(start) = signature.label.find(parameter_label) { - Some(start..start + parameter_label.len()) - } else { - None - } - } - }; - - if let Some(label_range) = &label_range - && index == active_parameter - { - highlights.push(( - label_range.clone(), - HighlightStyle { - font_weight: Some(FontWeight::EXTRA_BOLD), - ..HighlightStyle::default() - }, - )); - } - - let documentation = parameter - .documentation - .as_ref() - .map(|doc| documentation_to_markdown(doc, language_registry.clone(), cx)); - - parameter_infos.push(ParameterInfo { - label_range, - documentation, - }); - } - } - - let documentation = signature - .documentation - .as_ref() - .map(|doc| documentation_to_markdown(doc, language_registry.clone(), cx)); - - signatures.push(SignatureHelpData { - label, - documentation, - highlights, - active_parameter: Some(active_parameter), - parameters: parameter_infos, - }); - } - Some(Self { - signatures, - active_signature, - original_data: help, - }) - } -} - -fn documentation_to_markdown( - documentation: &lsp::Documentation, - language_registry: Option>, - cx: &mut App, -) -> Entity { - match documentation { - lsp::Documentation::String(string) => { - cx.new(|cx| Markdown::new_text(SharedString::from(string), cx)) - } - lsp::Documentation::MarkupContent(markup) => match markup.kind { - lsp::MarkupKind::PlainText => { - cx.new(|cx| Markdown::new_text(SharedString::from(&markup.value), cx)) - } - lsp::MarkupKind::Markdown => cx.new(|cx| { - Markdown::new( - SharedString::from(&markup.value), - language_registry, - None, - cx, - ) - }), - }, - } -} - -pub fn lsp_to_proto_signature(lsp_help: lsp::SignatureHelp) -> proto::SignatureHelp { - proto::SignatureHelp { - signatures: lsp_help - .signatures - .into_iter() - .map(|signature| proto::SignatureInformation { - label: signature.label, - documentation: signature.documentation.map(lsp_to_proto_documentation), - parameters: signature - .parameters - .unwrap_or_default() - .into_iter() - .map(|parameter_info| proto::ParameterInformation { - label: Some(match parameter_info.label { - lsp::ParameterLabel::Simple(label) => { - proto::parameter_information::Label::Simple(label) - } - lsp::ParameterLabel::LabelOffsets(offsets) => { - proto::parameter_information::Label::LabelOffsets( - proto::LabelOffsets { - start: offsets[0], - end: offsets[1], - }, - ) - } - }), - documentation: parameter_info.documentation.map(lsp_to_proto_documentation), - }) - .collect(), - active_parameter: signature.active_parameter, - }) - .collect(), - active_signature: lsp_help.active_signature, - active_parameter: lsp_help.active_parameter, - } -} - -fn lsp_to_proto_documentation(documentation: lsp::Documentation) -> proto::Documentation { - proto::Documentation { - content: Some(match documentation { - lsp::Documentation::String(string) => proto::documentation::Content::Value(string), - lsp::Documentation::MarkupContent(content) => { - proto::documentation::Content::MarkupContent(proto::MarkupContent { - is_markdown: matches!(content.kind, lsp::MarkupKind::Markdown), - value: content.value, - }) - } - }), - } -} - -pub fn proto_to_lsp_signature(proto_help: proto::SignatureHelp) -> lsp::SignatureHelp { - lsp::SignatureHelp { - signatures: proto_help - .signatures - .into_iter() - .map(|signature| lsp::SignatureInformation { - label: signature.label, - documentation: signature.documentation.and_then(proto_to_lsp_documentation), - parameters: Some( - signature - .parameters - .into_iter() - .filter_map(|parameter_info| { - Some(lsp::ParameterInformation { - label: match parameter_info.label? { - proto::parameter_information::Label::Simple(string) => { - lsp::ParameterLabel::Simple(string) - } - proto::parameter_information::Label::LabelOffsets(offsets) => { - lsp::ParameterLabel::LabelOffsets([ - offsets.start, - offsets.end, - ]) - } - }, - documentation: parameter_info - .documentation - .and_then(proto_to_lsp_documentation), - }) - }) - .collect(), - ), - active_parameter: signature.active_parameter, - }) - .collect(), - active_signature: proto_help.active_signature, - active_parameter: proto_help.active_parameter, - } -} - -fn proto_to_lsp_documentation(documentation: proto::Documentation) -> Option { - { - Some(match documentation.content? { - documentation::Content::Value(string) => lsp::Documentation::String(string), - documentation::Content::MarkupContent(markup) => { - lsp::Documentation::MarkupContent(if markup.is_markdown { - lsp::MarkupContent { - kind: lsp::MarkupKind::Markdown, - value: markup.value, - } - } else { - lsp::MarkupContent { - kind: lsp::MarkupKind::PlainText, - value: markup.value, - } - }) - } - }) - } -} - -#[cfg(test)] -mod tests { - use gpui::{FontWeight, HighlightStyle, SharedString, TestAppContext}; - use lsp::{Documentation, MarkupContent, MarkupKind}; - - use crate::lsp_command::signature_help::SignatureHelp; - - fn current_parameter() -> HighlightStyle { - HighlightStyle { - font_weight: Some(FontWeight::EXTRA_BOLD), - ..Default::default() - } - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_1(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![lsp::SignatureInformation { - label: "fn test(foo: u8, bar: &str)".to_string(), - documentation: Some(Documentation::String( - "This is a test documentation".to_string(), - )), - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }], - active_signature: Some(0), - active_parameter: Some(0), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test(foo: u8, bar: &str)"), - vec![(8..15, current_parameter())] - ) - ); - assert_eq!( - signature - .documentation - .unwrap() - .update(cx, |documentation, _| documentation.source().to_owned()), - "This is a test documentation", - ) - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_2(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![lsp::SignatureInformation { - label: "fn test(foo: u8, bar: &str)".to_string(), - documentation: Some(Documentation::MarkupContent(MarkupContent { - kind: MarkupKind::Markdown, - value: "This is a test documentation".to_string(), - })), - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }], - active_signature: Some(0), - active_parameter: Some(1), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test(foo: u8, bar: &str)"), - vec![(17..26, current_parameter())] - ) - ); - assert_eq!( - signature - .documentation - .unwrap() - .update(cx, |documentation, _| documentation.source().to_owned()), - "This is a test documentation", - ) - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_3(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![ - lsp::SignatureInformation { - label: "fn test1(foo: u8, bar: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - lsp::SignatureInformation { - label: "fn test2(hoge: String, fuga: bool)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("hoge: String".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("fuga: bool".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - ], - active_signature: Some(0), - active_parameter: Some(0), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test1(foo: u8, bar: &str)"), - vec![(9..16, current_parameter())] - ) - ); - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_4(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![ - lsp::SignatureInformation { - label: "fn test1(foo: u8, bar: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - lsp::SignatureInformation { - label: "fn test2(hoge: String, fuga: bool)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("hoge: String".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("fuga: bool".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - ], - active_signature: Some(1), - active_parameter: Some(0), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test2(hoge: String, fuga: bool)"), - vec![(9..21, current_parameter())] - ) - ); - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_5(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![ - lsp::SignatureInformation { - label: "fn test1(foo: u8, bar: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - lsp::SignatureInformation { - label: "fn test2(hoge: String, fuga: bool)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("hoge: String".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("fuga: bool".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - ], - active_signature: Some(1), - active_parameter: Some(1), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test2(hoge: String, fuga: bool)"), - vec![(23..33, current_parameter())] - ) - ); - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_6(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![ - lsp::SignatureInformation { - label: "fn test1(foo: u8, bar: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - lsp::SignatureInformation { - label: "fn test2(hoge: String, fuga: bool)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("hoge: String".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("fuga: bool".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - ], - active_signature: Some(1), - active_parameter: None, - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test2(hoge: String, fuga: bool)"), - vec![(9..21, current_parameter())] - ) - ); - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_7(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![ - lsp::SignatureInformation { - label: "fn test1(foo: u8, bar: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - lsp::SignatureInformation { - label: "fn test2(hoge: String, fuga: bool)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("hoge: String".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("fuga: bool".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - lsp::SignatureInformation { - label: "fn test3(one: usize, two: u32)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("one: usize".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("two: u32".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - ], - active_signature: Some(2), - active_parameter: Some(1), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test3(one: usize, two: u32)"), - vec![(21..29, current_parameter())] - ) - ); - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_8(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![], - active_signature: None, - active_parameter: None, - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_none()); - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_9(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![lsp::SignatureInformation { - label: "fn test(foo: u8, bar: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::LabelOffsets([8, 15]), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::LabelOffsets([17, 26]), - documentation: None, - }, - ]), - active_parameter: None, - }], - active_signature: Some(0), - active_parameter: Some(0), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test(foo: u8, bar: &str)"), - vec![(8..15, current_parameter())] - ) - ); - } - - #[gpui::test] - fn test_parameter_documentation(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![lsp::SignatureInformation { - label: "fn test(foo: u8, bar: &str)".to_string(), - documentation: Some(Documentation::String( - "This is a test documentation".to_string(), - )), - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: Some(Documentation::String("The foo parameter".to_string())), - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: Some(Documentation::String("The bar parameter".to_string())), - }, - ]), - active_parameter: None, - }], - active_signature: Some(0), - active_parameter: Some(0), - }; - let maybe_signature_help = - cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_signature_help.is_some()); - - let signature_help = maybe_signature_help.unwrap(); - let signature = &signature_help.signatures[signature_help.active_signature]; - - // Check that parameter documentation is extracted - assert_eq!(signature.parameters.len(), 2); - assert_eq!( - signature.parameters[0] - .documentation - .as_ref() - .unwrap() - .update(cx, |documentation, _| documentation.source().to_owned()), - "The foo parameter", - ); - assert_eq!( - signature.parameters[1] - .documentation - .as_ref() - .unwrap() - .update(cx, |documentation, _| documentation.source().to_owned()), - "The bar parameter", - ); - - // Check that the active parameter is correct - assert_eq!(signature.active_parameter, Some(0)); - } - - #[gpui::test] - fn test_create_signature_help_implements_utf16_spec(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![lsp::SignatureInformation { - label: "fn test(🦀: u8, 🦀: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::LabelOffsets([8, 10]), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::LabelOffsets([16, 18]), - documentation: None, - }, - ]), - active_parameter: None, - }], - active_signature: Some(0), - active_parameter: Some(0), - }; - let signature_help = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(signature_help.is_some()); - - let markdown = signature_help.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test(🦀: u8, 🦀: &str)"), - vec![(8..12, current_parameter())] - ) - ); - } -} diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs deleted file mode 100644 index a8c639fe59..0000000000 --- a/crates/project/src/lsp_store.rs +++ /dev/null @@ -1,14274 +0,0 @@ -//! 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 log_store; -pub mod lsp_ext_command; -pub mod rust_analyzer_ext; -pub mod vue_language_server_ext; - -mod inlay_hint_cache; - -use self::inlay_hint_cache::BufferInlayHints; -use crate::{ - CodeAction, ColorPresentation, Completion, CompletionDisplayOptions, CompletionResponse, - CompletionSource, CoreCompletion, DocumentColor, Hover, InlayHint, InlayId, LocationLink, - LspAction, LspPullDiagnostics, ManifestProvidersStore, Project, ProjectItem, ProjectPath, - ProjectTransaction, PulledDiagnostics, ResolveState, Symbol, - buffer_store::{BufferStore, BufferStoreEvent}, - environment::ProjectEnvironment, - lsp_command::{self, *}, - lsp_store::{ - self, - log_store::{GlobalLogStore, LanguageServerKind}, - }, - manifest_tree::{ - LanguageServerTree, LanguageServerTreeNode, LaunchDisposition, ManifestQueryDelegate, - ManifestTree, - }, - prettier_store::{self, PrettierStore, PrettierStoreEvent}, - project_settings::{LspSettings, ProjectSettings}, - toolchain_store::{LocalToolchainStore, ToolchainStoreEvent}, - worktree_store::{WorktreeStore, WorktreeStoreEvent}, - yarn::YarnPathStore, -}; -use anyhow::{Context as _, Result, anyhow}; -use async_trait::async_trait; -use client::{TypedEnvelope, proto}; -use clock::Global; -use collections::{BTreeMap, BTreeSet, HashMap, HashSet, btree_map}; -use futures::{ - AsyncWriteExt, Future, FutureExt, StreamExt, - future::{Either, Shared, join_all, pending, select}, - select, select_biased, - stream::FuturesUnordered, -}; -use globset::{Glob, GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder}; -use gpui::{ - App, AppContext, AsyncApp, Context, Entity, EventEmitter, PromptLevel, SharedString, Task, - WeakEntity, -}; -use http_client::HttpClient; -use itertools::Itertools as _; -use language::{ - Bias, BinaryStatus, Buffer, BufferRow, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic, - DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, Diff, File as _, Language, LanguageName, - LanguageRegistry, LocalFile, LspAdapter, LspAdapterDelegate, LspInstaller, ManifestDelegate, - ManifestName, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Toolchain, - Transaction, Unclipped, - language_settings::{FormatOnSave, Formatter, LanguageSettings, language_settings}, - point_to_lsp, - proto::{ - deserialize_anchor, deserialize_lsp_edit, deserialize_version, serialize_anchor, - serialize_lsp_edit, serialize_version, - }, - range_from_lsp, range_to_lsp, - row_chunk::RowChunk, -}; -use lsp::{ - AdapterServerCapabilities, CodeActionKind, CompletionContext, CompletionOptions, - DiagnosticServerCapabilities, 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, Uri, - WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder, notification::DidRenameFiles, -}; -use node_runtime::read_package_installed_version; -use parking_lot::Mutex; -use postage::{mpsc, sink::Sink, stream::Stream, watch}; -use rand::prelude::*; -use rpc::{ - AnyProtoClient, ErrorCode, ErrorExt as _, - proto::{LspRequestId, LspRequestMessage as _}, -}; -use serde::Serialize; -use serde_json::Value; -use settings::{Settings, SettingsLocation, SettingsStore}; -use sha2::{Digest, Sha256}; -use smol::channel::Sender; -use snippet::Snippet; -use std::{ - any::TypeId, - borrow::Cow, - cell::RefCell, - cmp::{Ordering, Reverse}, - convert::TryInto, - ffi::OsStr, - future::ready, - iter, mem, - ops::{ControlFlow, Range}, - path::{self, Path, PathBuf}, - pin::pin, - rc::Rc, - sync::{ - Arc, - atomic::{self, AtomicUsize}, - }, - time::{Duration, Instant}, - vec, -}; -use sum_tree::Dimensions; -use text::{Anchor, BufferId, LineEnding, OffsetRangeExt, ToPoint as _}; - -use util::{ - ConnectionResult, ResultExt as _, debug_panic, defer, maybe, merge_json_value_into, - paths::{PathStyle, SanitizedPath}, - post_inc, - rel_path::RelPath, -}; - -pub use fs::*; -pub use language::Location; -pub use lsp_store::inlay_hint_cache::{CacheInlayHints, InvalidationStrategy}; -#[cfg(any(test, feature = "test-support"))] -pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX; -pub use worktree::{ - Entry, EntryKind, FS_WATCH_LATENCY, File, LocalWorktree, PathChange, ProjectEntryId, - UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings, -}; - -const SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); -pub const SERVER_PROGRESS_THROTTLE_TIMEOUT: Duration = Duration::from_millis(100); -const WORKSPACE_DIAGNOSTICS_TOKEN_START: &str = "id:"; -const SERVER_DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(10); - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] -pub enum ProgressToken { - Number(i32), - String(SharedString), -} - -impl std::fmt::Display for ProgressToken { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Number(number) => write!(f, "{number}"), - Self::String(string) => write!(f, "{string}"), - } - } -} - -impl ProgressToken { - fn from_lsp(value: lsp::NumberOrString) -> Self { - match value { - lsp::NumberOrString::Number(number) => Self::Number(number), - lsp::NumberOrString::String(string) => Self::String(SharedString::new(string)), - } - } - - fn to_lsp(&self) -> lsp::NumberOrString { - match self { - Self::Number(number) => lsp::NumberOrString::Number(*number), - Self::String(string) => lsp::NumberOrString::String(string.to_string()), - } - } - - fn from_proto(value: proto::ProgressToken) -> Option { - Some(match value.value? { - proto::progress_token::Value::Number(number) => Self::Number(number), - proto::progress_token::Value::String(string) => Self::String(SharedString::new(string)), - }) - } - - fn to_proto(&self) -> proto::ProgressToken { - proto::ProgressToken { - value: Some(match self { - Self::Number(number) => proto::progress_token::Value::Number(*number), - Self::String(string) => proto::progress_token::Value::String(string.to_string()), - }), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FormatTrigger { - Save, - Manual, -} - -pub enum LspFormatTarget { - Buffers, - Ranges(BTreeMap>>), -} - -#[derive(Clone, PartialEq, Eq, Hash)] -pub struct OpenLspBufferHandle(Entity); - -struct OpenLspBuffer(Entity); - -impl FormatTrigger { - fn from_proto(value: i32) -> FormatTrigger { - match value { - 0 => FormatTrigger::Save, - 1 => FormatTrigger::Manual, - _ => FormatTrigger::Save, - } - } -} - -#[derive(Clone)] -struct UnifiedLanguageServer { - id: LanguageServerId, - project_roots: HashSet>, -} - -#[derive(Clone, Debug, 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 registration_id: Option, - pub server_id: LanguageServerId, - pub disk_based_sources: Cow<'a, [String]>, -} - -pub struct DocumentDiagnostics { - diagnostics: Vec>>, - document_abs_path: PathBuf, - version: Option, -} - -#[derive(Default, Debug)] -struct DynamicRegistrations { - did_change_watched_files: HashMap>, - diagnostics: HashMap, DiagnosticServerCapabilities>, -} - -pub struct LocalLspStore { - weak: WeakEntity, - worktree_store: Entity, - toolchain_store: Entity, - http_client: Arc, - environment: Entity, - fs: Arc, - languages: Arc, - language_server_ids: HashMap, - 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_dynamic_registrations: HashMap, - supplementary_language_servers: - HashMap)>, - prettier_store: Entity, - next_diagnostic_group_id: usize, - diagnostics: HashMap< - WorktreeId, - HashMap< - Arc, - Vec<( - LanguageServerId, - Vec>>, - )>, - >, - >, - buffer_snapshots: HashMap>>, // buffer_id -> server_id -> vec of snapshots - _subscription: gpui::Subscription, - lsp_tree: LanguageServerTree, - registered_buffers: HashMap, - buffers_opened_in_servers: HashMap>, - buffer_pull_diagnostics_result_ids: HashMap< - LanguageServerId, - HashMap, HashMap>>, - >, - workspace_pull_diagnostics_result_ids: HashMap< - LanguageServerId, - HashMap, HashMap>>, - >, -} - -impl LocalLspStore { - /// Returns the running language server for the given ID. Note if the language server is starting, it will not be returned. - pub fn running_language_server_for_id( - &self, - id: LanguageServerId, - ) -> Option<&Arc> { - let language_server_state = self.language_servers.get(&id)?; - - match language_server_state { - LanguageServerState::Running { server, .. } => Some(server), - LanguageServerState::Starting { .. } => None, - } - } - - 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 root_path = worktree.abs_path(); - let toolchain = key.toolchain.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!( - "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 pending_workspace_folders: Arc>> = Default::default(); - - let pending_server = cx.spawn({ - let adapter = adapter.clone(); - let server_name = adapter.name.clone(); - let stderr_capture = stderr_capture.clone(); - #[cfg(any(test, feature = "test-support"))] - let lsp_store = self.weak.clone(); - let pending_workspace_folders = pending_workspace_folders.clone(); - async move |cx| { - let binary = binary.await?; - #[cfg(any(test, feature = "test-support"))] - if let Some(server) = lsp_store - .update(&mut cx.clone(), |this, cx| { - this.languages.create_fake_language_server( - server_id, - &server_name, - binary.clone(), - &mut cx.to_async(), - ) - }) - .ok() - .flatten() - { - return Ok(server); - } - - let code_action_kinds = adapter.code_action_kinds(); - lsp::LanguageServer::new( - stderr_capture, - server_id, - server_name, - binary, - &root_path, - code_action_kinds, - Some(pending_workspace_folders), - cx, - ) - } - }); - - let startup = { - let server_name = adapter.name.0.clone(); - let delegate = delegate as Arc; - let key = key.clone(); - let adapter = adapter.clone(); - let lsp_store = self.weak.clone(); - let pending_workspace_folders = pending_workspace_folders.clone(); - - let pull_diagnostics = ProjectSettings::get_global(cx) - .diagnostics - .lsp_pull_diagnostics - .enabled; - cx.spawn(async move |cx| { - let result = async { - let language_server = pending_server.await?; - - let workspace_config = Self::workspace_configuration_for_adapter( - adapter.adapter.clone(), - &delegate, - toolchain, - None, - cx, - ) - .await?; - - let mut initialization_options = Self::initialization_options_for_adapter( - adapter.adapter.clone(), - &delegate, - ) - .await?; - - match (&mut initialization_options, override_options) { - (Some(initialization_options), Some(override_options)) => { - merge_json_value_into(override_options, initialization_options); - } - (None, override_options) => initialization_options = override_options, - _ => {} - } - - let initialization_params = cx.update(|cx| { - let mut params = - language_server.default_initialize_params(pull_diagnostics, cx); - params.initialization_options = initialization_options; - adapter.adapter.prepare_initialize_params(params, cx) - })??; - - Self::setup_lsp_messages( - lsp_store.clone(), - &language_server, - delegate.clone(), - adapter.clone(), - ); - - let did_change_configuration_params = lsp::DidChangeConfigurationParams { - settings: workspace_config, - }; - let language_server = cx - .update(|cx| { - language_server.initialize( - initialization_params, - Arc::new(did_change_configuration_params.clone()), - cx, - ) - })? - .await - .inspect_err(|_| { - if let Some(lsp_store) = lsp_store.upgrade() { - lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.cleanup_lsp_data(server_id); - cx.emit(LspStoreEvent::LanguageServerRemoved(server_id)) - }) - .ok(); - } - })?; - - language_server.notify::( - did_change_configuration_params, - )?; - - anyhow::Ok(language_server) - } - .await; - - match result { - Ok(server) => { - lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.insert_newly_running_language_server( - adapter, - server.clone(), - server_id, - key, - pending_workspace_folders, - cx, - ); - }) - .ok(); - stderr_capture.lock().take(); - Some(server) - } - - Err(err) => { - let log = stderr_capture.lock().take().unwrap_or_default(); - delegate.update_status( - adapter.name(), - BinaryStatus::Failed { - error: if log.is_empty() { - format!("{err:#}") - } else { - 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}"); - } - None - } - } - }) - }; - let state = LanguageServerState::Starting { - startup, - pending_workspace_folders, - }; - - self.languages - .update_lsp_binary_status(adapter.name(), BinaryStatus::Starting); - - self.language_servers.insert(server_id, state); - self.language_server_ids - .entry(key) - .or_insert(UnifiedLanguageServer { - id: server_id, - project_roots: Default::default(), - }); - 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 - && let Some(path) = settings.path.as_ref().map(PathBuf::from) - { - let settings = settings.clone(); - - return cx.background_spawn(async move { - let mut env = delegate.shell_env().await; - env.extend(settings.env.unwrap_or_default()); - - Ok(LanguageServerBinary { - path: delegate.resolve_executable_path(path), - env: Some(env), - arguments: settings - .arguments - .unwrap_or_default() - .iter() - .map(Into::into) - .collect(), - }) - }); - } - 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, - pre_release: settings - .fetch - .as_ref() - .and_then(|f| f.pre_release) - .unwrap_or(false), - }; - - cx.spawn(async move |cx| { - let (existing_binary, maybe_download_binary) = adapter - .clone() - .get_language_server_command(delegate.clone(), toolchain, lsp_binary_options, cx) - .await - .await; - - delegate.update_status(adapter.name.clone(), BinaryStatus::None); - - let mut binary = match (existing_binary, maybe_download_binary) { - (binary, None) => binary?, - (Err(_), Some(downloader)) => downloader.await?, - (Ok(existing_binary), Some(downloader)) => { - let mut download_timeout = cx - .background_executor() - .timer(SERVER_DOWNLOAD_TIMEOUT) - .fuse(); - let mut downloader = downloader.fuse(); - futures::select! { - _ = download_timeout => { - // Return existing binary and kick the existing work to the background. - cx.spawn(async move |_| downloader.await).detach(); - Ok(existing_binary) - }, - downloaded_or_existing_binary = downloader => { - // If download fails, this results in the existing binary. - downloaded_or_existing_binary - } - }? - } - }; - let mut shell_env = delegate.shell_env().await; - - 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(env) = &settings.env { - shell_env.extend(env.iter().map(|(k, v)| (k.clone(), v.clone()))); - } - } - - binary.env = Some(shell_env); - Ok(binary) - }) - } - - fn setup_lsp_messages( - lsp_store: WeakEntity, - language_server: &LanguageServer, - delegate: Arc, - adapter: Arc, - ) { - let name = language_server.name(); - let server_id = language_server.server_id(); - language_server - .on_notification::({ - let adapter = adapter.clone(); - let this = lsp_store.clone(); - move |mut params, cx| { - let adapter = adapter.clone(); - if let Some(this) = this.upgrade() { - this.update(cx, |this, cx| { - { - let buffer = params - .uri - .to_file_path() - .map(|file_path| this.get_buffer(&file_path, cx)) - .ok() - .flatten(); - adapter.process_diagnostics(&mut params, server_id, buffer); - } - - this.merge_lsp_diagnostics( - DiagnosticSourceKind::Pushed, - vec![DocumentDiagnosticsUpdate { - server_id, - diagnostics: params, - result_id: None, - disk_based_sources: Cow::Borrowed( - &adapter.disk_based_diagnostic_sources, - ), - registration_id: None, - }], - |_, diagnostic, cx| match diagnostic.source_kind { - DiagnosticSourceKind::Other | DiagnosticSourceKind::Pushed => { - adapter.retain_old_diagnostic(diagnostic, cx) - } - DiagnosticSourceKind::Pulled => true, - }, - cx, - ) - .log_err(); - }) - .ok(); - } - } - }) - .detach(); - language_server - .on_request::({ - let adapter = adapter.adapter.clone(); - let delegate = delegate.clone(); - let this = lsp_store.clone(); - move |params, cx| { - let adapter = adapter.clone(); - let delegate = delegate.clone(); - let this = this.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 mut scope_uri_to_workspace_config = BTreeMap::new(); - for item in ¶ms.items { - let scope_uri = item.scope_uri.clone(); - let std::collections::btree_map::Entry::Vacant(new_scope_uri) = - scope_uri_to_workspace_config.entry(scope_uri.clone()) - else { - // We've already queried workspace configuration of this URI. - continue; - }; - let workspace_config = Self::workspace_configuration_for_adapter( - adapter.clone(), - &delegate, - toolchain_for_id.clone(), - scope_uri, - &mut cx, - ) - .await?; - new_scope_uri.insert(workspace_config); - } - - Ok(params - .items - .into_iter() - .filter_map(|item| { - let workspace_config = - scope_uri_to_workspace_config.get(&item.scope_uri)?; - if let Some(section) = &item.section { - Some( - workspace_config - .get(section) - .cloned() - .unwrap_or(serde_json::Value::Null), - ) - } else { - Some(workspace_config.clone()) - } - }) - .collect()) - } - } - }) - .detach(); - - language_server - .on_request::({ - let this = lsp_store.clone(); - move |_, cx| { - let this = this.clone(); - let cx = cx.clone(); - async move { - let Some(server) = - this.read_with(&cx, |this, _| this.language_server_for_id(server_id))? - else { - return Ok(None); - }; - let root = server.workspace_folders(); - Ok(Some( - root.into_iter() - .map(|uri| WorkspaceFolder { - uri, - name: Default::default(), - }) - .collect(), - )) - } - } - }) - .detach(); - // Even though we don't have handling for these requests, respond to them to - // avoid stalling any language server like `gopls` which waits for a response - // to these requests when initializing. - language_server - .on_request::({ - let this = lsp_store.clone(); - move |params, cx| { - let this = this.clone(); - let mut cx = cx.clone(); - async move { - this.update(&mut cx, |this, _| { - if let Some(status) = this.language_server_statuses.get_mut(&server_id) - { - status - .progress_tokens - .insert(ProgressToken::from_lsp(params.token)); - } - })?; - - Ok(()) - } - } - }) - .detach(); - - language_server - .on_request::({ - let lsp_store = lsp_store.clone(); - move |params, cx| { - let lsp_store = lsp_store.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:#}" - ); - } - }; - } - }) - .ok(); - Ok(()) - } - } - }) - .detach(); - - language_server - .on_request::({ - let lsp_store = lsp_store.clone(); - move |params, cx| { - let lsp_store = lsp_store.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:#}" - ); - } - } - } - }) - .ok(); - Ok(()) - } - } - }) - .detach(); - - language_server - .on_request::({ - let this = lsp_store.clone(); - move |params, cx| { - let mut cx = cx.clone(); - let this = this.clone(); - async move { - LocalLspStore::on_lsp_workspace_edit( - this.clone(), - params, - server_id, - &mut cx, - ) - .await - } - } - }) - .detach(); - - language_server - .on_request::({ - let lsp_store = lsp_store.clone(); - let request_id = Arc::new(AtomicUsize::new(0)); - move |(), cx| { - let lsp_store = lsp_store.clone(); - let request_id = request_id.clone(); - let mut cx = cx.clone(); - async move { - lsp_store - .update(&mut cx, |lsp_store, cx| { - let request_id = - Some(request_id.fetch_add(1, atomic::Ordering::AcqRel)); - cx.emit(LspStoreEvent::RefreshInlayHints { - server_id, - request_id, - }); - lsp_store - .downstream_client - .as_ref() - .map(|(client, project_id)| { - client.send(proto::RefreshInlayHints { - project_id: *project_id, - server_id: server_id.to_proto(), - request_id: request_id.map(|id| id as u64), - }) - }) - })? - .transpose()?; - Ok(()) - } - } - }) - .detach(); - - language_server - .on_request::({ - let this = lsp_store.clone(); - move |(), cx| { - let this = this.clone(); - let mut cx = cx.clone(); - async move { - this.update(&mut cx, |this, cx| { - cx.emit(LspStoreEvent::RefreshCodeLens); - this.downstream_client.as_ref().map(|(client, project_id)| { - client.send(proto::RefreshCodeLens { - project_id: *project_id, - }) - }) - })? - .transpose()?; - Ok(()) - } - } - }) - .detach(); - - language_server - .on_request::({ - let this = lsp_store.clone(); - move |(), cx| { - let this = this.clone(); - let mut cx = cx.clone(); - async move { - this.update(&mut cx, |lsp_store, _| { - lsp_store.pull_workspace_diagnostics(server_id); - lsp_store - .downstream_client - .as_ref() - .map(|(client, project_id)| { - client.send(proto::PullWorkspaceDiagnostics { - project_id: *project_id, - server_id: server_id.to_proto(), - }) - }) - })? - .transpose()?; - Ok(()) - } - } - }) - .detach(); - - language_server - .on_request::({ - let this = lsp_store.clone(); - let name = name.to_string(); - move |params, cx| { - let this = this.clone(); - let name = name.to_string(); - let mut cx = cx.clone(); - async move { - let actions = params.actions.unwrap_or_default(); - let (tx, rx) = smol::channel::bounded(1); - let request = LanguageServerPromptRequest { - level: match params.typ { - lsp::MessageType::ERROR => PromptLevel::Critical, - lsp::MessageType::WARNING => PromptLevel::Warning, - _ => PromptLevel::Info, - }, - message: params.message, - actions, - response_channel: tx, - lsp_name: name.clone(), - }; - - let did_update = this - .update(&mut cx, |_, cx| { - cx.emit(LspStoreEvent::LanguageServerPrompt(request)); - }) - .is_ok(); - if did_update { - let response = rx.recv().await.ok(); - Ok(response) - } else { - Ok(None) - } - } - } - }) - .detach(); - language_server - .on_notification::({ - let this = lsp_store.clone(); - let name = name.to_string(); - move |params, cx| { - let this = this.clone(); - let name = name.to_string(); - let mut cx = cx.clone(); - - let (tx, _) = smol::channel::bounded(1); - let request = LanguageServerPromptRequest { - level: match params.typ { - lsp::MessageType::ERROR => PromptLevel::Critical, - lsp::MessageType::WARNING => PromptLevel::Warning, - _ => PromptLevel::Info, - }, - message: params.message, - actions: vec![], - response_channel: tx, - lsp_name: name, - }; - - let _ = this.update(&mut cx, |_, cx| { - cx.emit(LspStoreEvent::LanguageServerPrompt(request)); - }); - } - }) - .detach(); - - let disk_based_diagnostics_progress_token = - adapter.disk_based_diagnostics_progress_token.clone(); - - language_server - .on_notification::({ - let this = lsp_store.clone(); - move |params, cx| { - if let Some(this) = this.upgrade() { - this.update(cx, |this, cx| { - this.on_lsp_progress( - params, - server_id, - disk_based_diagnostics_progress_token.clone(), - cx, - ); - }) - .ok(); - } - } - }) - .detach(); - - language_server - .on_notification::({ - let this = lsp_store.clone(); - move |params, cx| { - if let Some(this) = this.upgrade() { - this.update(cx, |_, cx| { - cx.emit(LspStoreEvent::LanguageServerLog( - server_id, - LanguageServerLogType::Log(params.typ), - params.message, - )); - }) - .ok(); - } - } - }) - .detach(); - - language_server - .on_notification::({ - let this = lsp_store.clone(); - move |params, cx| { - let mut cx = cx.clone(); - if let Some(this) = this.upgrade() { - this.update(&mut cx, |_, cx| { - cx.emit(LspStoreEvent::LanguageServerLog( - server_id, - LanguageServerLogType::Trace { - verbose_info: params.verbose, - }, - params.message, - )); - }) - .ok(); - } - } - }) - .detach(); - - vue_language_server_ext::register_requests(lsp_store.clone(), language_server); - json_language_server_ext::register_requests(lsp_store.clone(), language_server); - rust_analyzer_ext::register_notifications(lsp_store.clone(), language_server); - clangd_ext::register_notifications(lsp_store, language_server, adapter); - } - - fn shutdown_language_servers_on_quit( - &mut self, - _: &mut Context, - ) -> impl Future + use<> { - let shutdown_futures = self - .language_servers - .drain() - .map(|(_, server_state)| Self::shutdown_server(server_state)) - .collect::>(); - - async move { - join_all(shutdown_futures).await; - } - } - - async fn shutdown_server(server_state: LanguageServerState) -> anyhow::Result<()> { - match server_state { - LanguageServerState::Running { server, .. } => { - if let Some(shutdown) = server.shutdown() { - shutdown.await; - } - } - LanguageServerState::Starting { startup, .. } => { - if let Some(server) = startup.await - && let Some(shutdown) = server.shutdown() - { - shutdown.await; - } - } - } - Ok(()) - } - - fn language_servers_for_worktree( - &self, - worktree_id: WorktreeId, - ) -> 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 - } - }) - } - - fn language_server_ids_for_project_path( - &self, - project_path: ProjectPath, - language: &Language, - cx: &mut App, - ) -> Vec { - let Some(worktree) = self - .worktree_store - .read(cx) - .worktree_for_id(project_path.worktree_id, cx) - else { - return Vec::new(); - }; - let delegate: Arc = - Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); - - self.lsp_tree - .get( - project_path, - language.name(), - language.manifest(), - &delegate, - cx, - ) - .collect::>() - } - - fn language_server_ids_for_buffer( - &self, - buffer: &Buffer, - cx: &mut App, - ) -> Vec { - if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) { - let worktree_id = file.worktree_id(cx); - - let path: Arc = file - .path() - .parent() - .map(Arc::from) - .unwrap_or_else(|| file.path().clone()); - let worktree_path = ProjectPath { worktree_id, path }; - self.language_server_ids_for_project_path(worktree_path, language, cx) - } else { - Vec::new() - } - } - - fn language_servers_for_buffer<'a>( - &'a self, - buffer: &'a Buffer, - cx: &'a mut App, - ) -> impl Iterator, &'a Arc)> { - self.language_server_ids_for_buffer(buffer, cx) - .into_iter() - .filter_map(|server_id| match self.language_servers.get(&server_id)? { - LanguageServerState::Running { - adapter, server, .. - } => Some((adapter, server)), - _ => None, - }) - } - - async fn execute_code_action_kind_locally( - lsp_store: WeakEntity, - mut buffers: Vec>, - kind: CodeActionKind, - push_to_history: bool, - cx: &mut AsyncApp, - ) -> anyhow::Result { - // Do not allow multiple concurrent code actions requests for the - // same buffer. - lsp_store.update(cx, |this, cx| { - let this = this.as_local_mut().unwrap(); - buffers.retain(|buffer| { - this.buffers_being_formatted - .insert(buffer.read(cx).remote_id()) - }); - })?; - let _cleanup = defer({ - let this = lsp_store.clone(); - let mut cx = cx.clone(); - let buffers = &buffers; - move || { - this.update(&mut cx, |this, cx| { - let this = this.as_local_mut().unwrap(); - for buffer in buffers { - this.buffers_being_formatted - .remove(&buffer.read(cx).remote_id()); - } - }) - .ok(); - } - }); - let mut project_transaction = ProjectTransaction::default(); - - for buffer in &buffers { - let adapters_and_servers = lsp_store.update(cx, |lsp_store, cx| { - buffer.update(cx, |buffer, cx| { - lsp_store - .as_local() - .unwrap() - .language_servers_for_buffer(buffer, cx) - .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())) - .collect::>() - }) - })?; - for (_, language_server) in adapters_and_servers.iter() { - let actions = Self::get_server_code_actions_from_action_kinds( - &lsp_store, - language_server.server_id(), - vec![kind.clone()], - buffer, - cx, - ) - .await?; - Self::execute_code_actions_on_server( - &lsp_store, - language_server, - actions, - push_to_history, - &mut project_transaction, - cx, - ) - .await?; - } - } - Ok(project_transaction) - } - - async fn format_locally( - lsp_store: WeakEntity, - mut buffers: Vec, - push_to_history: bool, - trigger: FormatTrigger, - logger: zlog::Logger, - cx: &mut AsyncApp, - ) -> anyhow::Result { - // Do not allow multiple concurrent formatting requests for the - // same buffer. - lsp_store.update(cx, |this, cx| { - let this = this.as_local_mut().unwrap(); - buffers.retain(|buffer| { - this.buffers_being_formatted - .insert(buffer.handle.read(cx).remote_id()) - }); - })?; - - let _cleanup = defer({ - let this = lsp_store.clone(); - let mut cx = cx.clone(); - let buffers = &buffers; - move || { - this.update(&mut cx, |this, cx| { - let this = this.as_local_mut().unwrap(); - for buffer in buffers { - this.buffers_being_formatted - .remove(&buffer.handle.read(cx).remote_id()); - } - }) - .ok(); - } - }); - - let mut project_transaction = ProjectTransaction::default(); - - for buffer in &buffers { - zlog::debug!( - logger => - "formatting buffer '{:?}'", - buffer.abs_path.as_ref().unwrap_or(&PathBuf::from("unknown")).display() - ); - // Create an empty transaction to hold all of the formatting edits. - let formatting_transaction_id = buffer.handle.update(cx, |buffer, cx| { - // ensure no transactions created while formatting are - // grouped with the previous transaction in the history - // based on the transaction group interval - buffer.finalize_last_transaction(); - buffer - .start_transaction() - .context("transaction already open")?; - buffer.end_transaction(cx); - let transaction_id = buffer.push_empty_transaction(cx.background_executor().now()); - buffer.finalize_last_transaction(); - anyhow::Ok(transaction_id) - })??; - - let result = Self::format_buffer_locally( - lsp_store.clone(), - buffer, - formatting_transaction_id, - trigger, - logger, - cx, - ) - .await; - - buffer.handle.update(cx, |buffer, cx| { - let Some(formatting_transaction) = - buffer.get_transaction(formatting_transaction_id).cloned() - else { - zlog::warn!(logger => "no formatting transaction"); - return; - }; - if formatting_transaction.edit_ids.is_empty() { - zlog::debug!(logger => "no changes made while formatting"); - buffer.forget_transaction(formatting_transaction_id); - return; - } - if !push_to_history { - zlog::trace!(logger => "forgetting format transaction"); - buffer.forget_transaction(formatting_transaction.id); - } - project_transaction - .0 - .insert(cx.entity(), formatting_transaction); - })?; - - result?; - } - - Ok(project_transaction) - } - - async fn format_buffer_locally( - lsp_store: WeakEntity, - buffer: &FormattableBuffer, - formatting_transaction_id: clock::Lamport, - trigger: FormatTrigger, - logger: zlog::Logger, - cx: &mut AsyncApp, - ) -> Result<()> { - let (adapters_and_servers, settings) = lsp_store.update(cx, |lsp_store, cx| { - buffer.handle.update(cx, |buffer, cx| { - let adapters_and_servers = lsp_store - .as_local() - .unwrap() - .language_servers_for_buffer(buffer, cx) - .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())) - .collect::>(); - let settings = - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) - .into_owned(); - (adapters_and_servers, settings) - }) - })?; - - /// Apply edits to the buffer that will become part of the formatting transaction. - /// Fails if the buffer has been edited since the start of that transaction. - fn extend_formatting_transaction( - buffer: &FormattableBuffer, - formatting_transaction_id: text::TransactionId, - cx: &mut AsyncApp, - operation: impl FnOnce(&mut Buffer, &mut Context), - ) -> anyhow::Result<()> { - buffer.handle.update(cx, |buffer, cx| { - let last_transaction_id = buffer.peek_undo_stack().map(|t| t.transaction_id()); - if last_transaction_id != Some(formatting_transaction_id) { - anyhow::bail!("Buffer edited while formatting. Aborting") - } - buffer.start_transaction(); - operation(buffer, cx); - if let Some(transaction_id) = buffer.end_transaction(cx) { - buffer.merge_transactions(transaction_id, formatting_transaction_id); - } - Ok(()) - })? - } - - // handle whitespace formatting - if settings.remove_trailing_whitespace_on_save { - zlog::trace!(logger => "removing trailing whitespace"); - let diff = buffer - .handle - .read_with(cx, |buffer, cx| buffer.remove_trailing_whitespace(cx))? - .await; - extend_formatting_transaction(buffer, formatting_transaction_id, cx, |buffer, cx| { - buffer.apply_diff(diff, cx); - })?; - } - - if settings.ensure_final_newline_on_save { - zlog::trace!(logger => "ensuring final newline"); - extend_formatting_transaction(buffer, formatting_transaction_id, cx, |buffer, cx| { - buffer.ensure_final_newline(cx); - })?; - } - - // Formatter for `code_actions_on_format` that runs before - // the rest of the formatters - let mut code_actions_on_format_formatters = None; - let should_run_code_actions_on_format = !matches!( - (trigger, &settings.format_on_save), - (FormatTrigger::Save, &FormatOnSave::Off) - ); - if should_run_code_actions_on_format { - let have_code_actions_to_run_on_format = settings - .code_actions_on_format - .values() - .any(|enabled| *enabled); - if have_code_actions_to_run_on_format { - zlog::trace!(logger => "going to run code actions on format"); - code_actions_on_format_formatters = Some( - settings - .code_actions_on_format - .iter() - .filter_map(|(action, enabled)| enabled.then_some(action)) - .cloned() - .map(Formatter::CodeAction) - .collect::>(), - ); - } - } - - let formatters = match (trigger, &settings.format_on_save) { - (FormatTrigger::Save, FormatOnSave::Off) => &[], - (FormatTrigger::Manual, _) | (FormatTrigger::Save, FormatOnSave::On) => { - settings.formatter.as_ref() - } - }; - - let formatters = code_actions_on_format_formatters - .iter() - .flatten() - .chain(formatters); - - for formatter in formatters { - let formatter = if formatter == &Formatter::Auto { - if settings.prettier.allowed { - zlog::trace!(logger => "Formatter set to auto: defaulting to prettier"); - &Formatter::Prettier - } else { - zlog::trace!(logger => "Formatter set to auto: defaulting to primary language server"); - &Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current) - } - } else { - formatter - }; - match formatter { - Formatter::Auto => unreachable!("Auto resolved above"), - Formatter::Prettier => { - let logger = zlog::scoped!(logger => "prettier"); - zlog::trace!(logger => "formatting"); - let _timer = zlog::time!(logger => "Formatting buffer via prettier"); - - let prettier = lsp_store.read_with(cx, |lsp_store, _cx| { - lsp_store.prettier_store().unwrap().downgrade() - })?; - let diff = prettier_store::format_with_prettier(&prettier, &buffer.handle, cx) - .await - .transpose()?; - let Some(diff) = diff else { - zlog::trace!(logger => "No changes"); - continue; - }; - - extend_formatting_transaction( - buffer, - formatting_transaction_id, - cx, - |buffer, cx| { - buffer.apply_diff(diff, cx); - }, - )?; - } - Formatter::External { command, arguments } => { - let logger = zlog::scoped!(logger => "command"); - zlog::trace!(logger => "formatting"); - let _timer = zlog::time!(logger => "Formatting buffer via external command"); - - let diff = Self::format_via_external_command( - buffer, - command.as_ref(), - arguments.as_deref(), - cx, - ) - .await - .with_context(|| { - format!("Failed to format buffer via external command: {}", command) - })?; - let Some(diff) = diff else { - zlog::trace!(logger => "No changes"); - continue; - }; - - extend_formatting_transaction( - buffer, - formatting_transaction_id, - cx, - |buffer, cx| { - buffer.apply_diff(diff, cx); - }, - )?; - } - Formatter::LanguageServer(specifier) => { - let logger = zlog::scoped!(logger => "language-server"); - zlog::trace!(logger => "formatting"); - let _timer = zlog::time!(logger => "Formatting buffer using language server"); - - let Some(buffer_path_abs) = buffer.abs_path.as_ref() else { - zlog::warn!(logger => "Cannot format buffer that is not backed by a file on disk using language servers. Skipping"); - continue; - }; - - let language_server = match specifier { - settings::LanguageServerFormatterSpecifier::Specific { name } => { - adapters_and_servers.iter().find_map(|(adapter, server)| { - if adapter.name.0.as_ref() == name { - Some(server.clone()) - } else { - None - } - }) - } - settings::LanguageServerFormatterSpecifier::Current => { - adapters_and_servers.first().map(|e| e.1.clone()) - } - }; - - let Some(language_server) = language_server else { - log::debug!( - "No language server found to format buffer '{:?}'. Skipping", - buffer_path_abs.as_path().to_string_lossy() - ); - continue; - }; - - zlog::trace!( - logger => - "Formatting buffer '{:?}' using language server '{:?}'", - buffer_path_abs.as_path().to_string_lossy(), - language_server.name() - ); - - let edits = if let Some(ranges) = buffer.ranges.as_ref() { - zlog::trace!(logger => "formatting ranges"); - Self::format_ranges_via_lsp( - &lsp_store, - &buffer.handle, - ranges, - buffer_path_abs, - &language_server, - &settings, - cx, - ) - .await - .context("Failed to format ranges via language server")? - } else { - zlog::trace!(logger => "formatting full"); - Self::format_via_lsp( - &lsp_store, - &buffer.handle, - buffer_path_abs, - &language_server, - &settings, - cx, - ) - .await - .context("failed to format via language server")? - }; - - if edits.is_empty() { - zlog::trace!(logger => "No changes"); - continue; - } - extend_formatting_transaction( - buffer, - formatting_transaction_id, - cx, - |buffer, cx| { - buffer.edit(edits, None, cx); - }, - )?; - } - Formatter::CodeAction(code_action_name) => { - let logger = zlog::scoped!(logger => "code-actions"); - zlog::trace!(logger => "formatting"); - let _timer = zlog::time!(logger => "Formatting buffer using code actions"); - - let Some(buffer_path_abs) = buffer.abs_path.as_ref() else { - zlog::warn!(logger => "Cannot format buffer that is not backed by a file on disk using code actions. Skipping"); - continue; - }; - - let code_action_kind: CodeActionKind = code_action_name.clone().into(); - zlog::trace!(logger => "Attempting to resolve code actions {:?}", &code_action_kind); - - let mut actions_and_servers = Vec::new(); - - for (index, (_, language_server)) in adapters_and_servers.iter().enumerate() { - let actions_result = Self::get_server_code_actions_from_action_kinds( - &lsp_store, - language_server.server_id(), - vec![code_action_kind.clone()], - &buffer.handle, - cx, - ) - .await - .with_context(|| { - format!( - "Failed to resolve code action {:?} with language server {}", - code_action_kind, - language_server.name() - ) - }); - let Ok(actions) = actions_result else { - // note: it may be better to set result to the error and break formatters here - // but for now we try to execute the actions that we can resolve and skip the rest - zlog::error!( - logger => - "Failed to resolve code action {:?} with language server {}", - code_action_kind, - language_server.name() - ); - continue; - }; - for action in actions { - actions_and_servers.push((action, index)); - } - } - - if actions_and_servers.is_empty() { - zlog::warn!(logger => "No code actions were resolved, continuing"); - continue; - } - - 'actions: for (mut action, server_index) in actions_and_servers { - let server = &adapters_and_servers[server_index].1; - - let describe_code_action = |action: &CodeAction| { - format!( - "code action '{}' with title \"{}\" on server {}", - action - .lsp_action - .action_kind() - .unwrap_or("unknown".into()) - .as_str(), - action.lsp_action.title(), - server.name(), - ) - }; - - zlog::trace!(logger => "Executing {}", describe_code_action(&action)); - - if let Err(err) = Self::try_resolve_code_action(server, &mut action).await { - zlog::error!( - logger => - "Failed to resolve {}. Error: {}", - describe_code_action(&action), - err - ); - continue; - } - - if let Some(edit) = action.lsp_action.edit().cloned() { - // NOTE: code below duplicated from `Self::deserialize_workspace_edit` - // but filters out and logs warnings for code actions that require unreasonably - // difficult handling on our part, such as: - // - applying edits that call commands - // which can result in arbitrary workspace edits being sent from the server that - // have no way of being tied back to the command that initiated them (i.e. we - // can't know which edits are part of the format request, or if the server is done sending - // actions in response to the command) - // - actions that create/delete/modify/rename files other than the one we are formatting - // as we then would need to handle such changes correctly in the local history as well - // as the remote history through the ProjectTransaction - // - actions with snippet edits, as these simply don't make sense in the context of a format request - // Supporting these actions is not impossible, but not supported as of yet. - if edit.changes.is_none() && edit.document_changes.is_none() { - zlog::trace!( - logger => - "No changes for code action. Skipping {}", - describe_code_action(&action), - ); - continue; - } - - let mut operations = Vec::new(); - if let Some(document_changes) = edit.document_changes { - match document_changes { - lsp::DocumentChanges::Edits(edits) => operations.extend( - edits.into_iter().map(lsp::DocumentChangeOperation::Edit), - ), - lsp::DocumentChanges::Operations(ops) => operations = ops, - } - } else if let Some(changes) = edit.changes { - operations.extend(changes.into_iter().map(|(uri, edits)| { - lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit { - text_document: - lsp::OptionalVersionedTextDocumentIdentifier { - uri, - version: None, - }, - edits: edits.into_iter().map(Edit::Plain).collect(), - }) - })); - } - - let mut edits = Vec::with_capacity(operations.len()); - - if operations.is_empty() { - zlog::trace!( - logger => - "No changes for code action. Skipping {}", - describe_code_action(&action), - ); - continue; - } - for operation in operations { - let op = match operation { - lsp::DocumentChangeOperation::Edit(op) => op, - lsp::DocumentChangeOperation::Op(_) => { - zlog::warn!( - logger => - "Code actions which create, delete, or rename files are not supported on format. Skipping {}", - describe_code_action(&action), - ); - continue 'actions; - } - }; - let Ok(file_path) = op.text_document.uri.to_file_path() else { - zlog::warn!( - logger => - "Failed to convert URI '{:?}' to file path. Skipping {}", - &op.text_document.uri, - describe_code_action(&action), - ); - continue 'actions; - }; - if &file_path != buffer_path_abs { - zlog::warn!( - logger => - "File path '{:?}' does not match buffer path '{:?}'. Skipping {}", - file_path, - buffer_path_abs, - describe_code_action(&action), - ); - continue 'actions; - } - - let mut lsp_edits = Vec::new(); - for edit in op.edits { - match edit { - Edit::Plain(edit) => { - if !lsp_edits.contains(&edit) { - lsp_edits.push(edit); - } - } - Edit::Annotated(edit) => { - if !lsp_edits.contains(&edit.text_edit) { - lsp_edits.push(edit.text_edit); - } - } - Edit::Snippet(_) => { - zlog::warn!( - logger => - "Code actions which produce snippet edits are not supported during formatting. Skipping {}", - describe_code_action(&action), - ); - continue 'actions; - } - } - } - let edits_result = lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.as_local_mut().unwrap().edits_from_lsp( - &buffer.handle, - lsp_edits, - server.server_id(), - op.text_document.version, - cx, - ) - })? - .await; - let Ok(resolved_edits) = edits_result else { - zlog::warn!( - logger => - "Failed to resolve edits from LSP for buffer {:?} while handling {}", - buffer_path_abs.as_path(), - describe_code_action(&action), - ); - continue 'actions; - }; - edits.extend(resolved_edits); - } - - if edits.is_empty() { - zlog::warn!(logger => "No edits resolved from LSP"); - continue; - } - - extend_formatting_transaction( - buffer, - formatting_transaction_id, - cx, - |buffer, cx| { - zlog::info!( - "Applying edits {edits:?}. Content: {:?}", - buffer.text() - ); - buffer.edit(edits, None, cx); - zlog::info!("Applied edits. New Content: {:?}", buffer.text()); - }, - )?; - } - - if let Some(command) = action.lsp_action.command() { - zlog::warn!( - logger => - "Executing code action command '{}'. This may cause formatting to abort unnecessarily as well as splitting formatting into two entries in the undo history", - &command.command, - ); - - // bail early if command is invalid - let server_capabilities = server.capabilities(); - let available_commands = server_capabilities - .execute_command_provider - .as_ref() - .map(|options| options.commands.as_slice()) - .unwrap_or_default(); - if !available_commands.contains(&command.command) { - zlog::warn!( - logger => - "Cannot execute a command {} not listed in the language server capabilities of server {}", - command.command, - server.name(), - ); - continue; - } - - // noop so we just ensure buffer hasn't been edited since resolving code actions - extend_formatting_transaction( - buffer, - formatting_transaction_id, - cx, - |_, _| {}, - )?; - zlog::info!(logger => "Executing command {}", &command.command); - - lsp_store.update(cx, |this, _| { - this.as_local_mut() - .unwrap() - .last_workspace_edits_by_language_server - .remove(&server.server_id()); - })?; - - let execute_command_result = server - .request::( - lsp::ExecuteCommandParams { - command: command.command.clone(), - arguments: command.arguments.clone().unwrap_or_default(), - ..Default::default() - }, - ) - .await - .into_response(); - - if execute_command_result.is_err() { - zlog::error!( - logger => - "Failed to execute command '{}' as part of {}", - &command.command, - describe_code_action(&action), - ); - continue 'actions; - } - - let mut project_transaction_command = - lsp_store.update(cx, |this, _| { - this.as_local_mut() - .unwrap() - .last_workspace_edits_by_language_server - .remove(&server.server_id()) - .unwrap_or_default() - })?; - - if let Some(transaction) = - project_transaction_command.0.remove(&buffer.handle) - { - zlog::trace!( - logger => - "Successfully captured {} edits that resulted from command {}", - transaction.edit_ids.len(), - &command.command, - ); - let transaction_id_project_transaction = transaction.id; - buffer.handle.update(cx, |buffer, _| { - // it may have been removed from history if push_to_history was - // false in deserialize_workspace_edit. If so push it so we - // can merge it with the format transaction - // and pop the combined transaction off the history stack - // later if push_to_history is false - if buffer.get_transaction(transaction.id).is_none() { - buffer.push_transaction(transaction, Instant::now()); - } - buffer.merge_transactions( - transaction_id_project_transaction, - formatting_transaction_id, - ); - })?; - } - - if !project_transaction_command.0.is_empty() { - let mut extra_buffers = String::new(); - for buffer in project_transaction_command.0.keys() { - buffer - .read_with(cx, |b, cx| { - if let Some(path) = b.project_path(cx) { - if !extra_buffers.is_empty() { - extra_buffers.push_str(", "); - } - extra_buffers.push_str(path.path.as_unix_str()); - } - }) - .ok(); - } - zlog::warn!( - logger => - "Unexpected edits to buffers other than the buffer actively being formatted due to command {}. Impacted buffers: [{}].", - &command.command, - extra_buffers, - ); - // NOTE: if this case is hit, the proper thing to do is to for each buffer, merge the extra transaction - // into the existing transaction in project_transaction if there is one, and if there isn't one in project_transaction, - // add it so it's included, and merge it into the format transaction when its created later - } - } - } - } - } - } - - Ok(()) - } - - pub async fn format_ranges_via_lsp( - this: &WeakEntity, - buffer_handle: &Entity, - ranges: &[Range], - abs_path: &Path, - language_server: &Arc, - settings: &LanguageSettings, - cx: &mut AsyncApp, - ) -> 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)) { - anyhow::bail!( - "{} language server does not support range formatting", - language_server.name() - ); - } - - let uri = file_path_to_lsp_url(abs_path)?; - let text_document = lsp::TextDocumentIdentifier::new(uri); - - let lsp_edits = { - let mut lsp_ranges = Vec::new(); - this.update(cx, |_this, cx| { - // TODO(#22930): In the case of formatting multibuffer selections, this buffer may - // not have been sent to the language server. This seems like a fairly systemic - // issue, though, the resolution probably is not specific to formatting. - // - // TODO: Instead of using current snapshot, should use the latest snapshot sent to - // LSP. - let snapshot = buffer_handle.read(cx).snapshot(); - for range in ranges { - lsp_ranges.push(range_to_lsp(range.to_point_utf16(&snapshot))?); - } - anyhow::Ok(()) - })??; - - let mut edits = None; - for range in lsp_ranges { - if let Some(mut edit) = language_server - .request::(lsp::DocumentRangeFormattingParams { - text_document: text_document.clone(), - range, - options: lsp_command::lsp_formatting_options(settings), - work_done_progress_params: Default::default(), - }) - .await - .into_response()? - { - edits.get_or_insert_with(Vec::new).append(&mut edit); - } - } - edits - }; - - if let Some(lsp_edits) = lsp_edits { - this.update(cx, |this, cx| { - this.as_local_mut().unwrap().edits_from_lsp( - buffer_handle, - lsp_edits, - language_server.server_id(), - None, - cx, - ) - })? - .await - } else { - Ok(Vec::with_capacity(0)) - } - } - - async fn format_via_lsp( - this: &WeakEntity, - buffer: &Entity, - abs_path: &Path, - language_server: &Arc, - settings: &LanguageSettings, - cx: &mut AsyncApp, - ) -> Result, Arc)>> { - let logger = zlog::scoped!("lsp_format"); - zlog::debug!(logger => "Formatting via LSP"); - - let uri = file_path_to_lsp_url(abs_path)?; - let text_document = lsp::TextDocumentIdentifier::new(uri); - let capabilities = &language_server.capabilities(); - - let formatting_provider = capabilities.document_formatting_provider.as_ref(); - let range_formatting_provider = capabilities.document_range_formatting_provider.as_ref(); - - let lsp_edits = if matches!(formatting_provider, Some(p) if *p != OneOf::Left(false)) { - let _timer = zlog::time!(logger => "format-full"); - language_server - .request::(lsp::DocumentFormattingParams { - text_document, - options: lsp_command::lsp_formatting_options(settings), - work_done_progress_params: Default::default(), - }) - .await - .into_response()? - } else if matches!(range_formatting_provider, Some(p) if *p != OneOf::Left(false)) { - let _timer = zlog::time!(logger => "format-range"); - let buffer_start = lsp::Position::new(0, 0); - let buffer_end = buffer.read_with(cx, |b, _| point_to_lsp(b.max_point_utf16()))?; - language_server - .request::(lsp::DocumentRangeFormattingParams { - text_document: text_document.clone(), - range: lsp::Range::new(buffer_start, buffer_end), - options: lsp_command::lsp_formatting_options(settings), - work_done_progress_params: Default::default(), - }) - .await - .into_response()? - } else { - None - }; - - if let Some(lsp_edits) = lsp_edits { - this.update(cx, |this, cx| { - this.as_local_mut().unwrap().edits_from_lsp( - buffer, - lsp_edits, - language_server.server_id(), - None, - cx, - ) - })? - .await - } else { - Ok(Vec::with_capacity(0)) - } - } - - async fn format_via_external_command( - buffer: &FormattableBuffer, - command: &str, - arguments: Option<&[String]>, - cx: &mut AsyncApp, - ) -> Result> { - let working_dir_path = buffer.handle.update(cx, |buffer, cx| { - let file = File::from_dyn(buffer.file())?; - let worktree = file.worktree.read(cx); - let mut worktree_path = worktree.abs_path().to_path_buf(); - if worktree.root_entry()?.is_file() { - worktree_path.pop(); - } - Some(worktree_path) - })?; - - let mut child = util::command::new_smol_command(command); - - if let Some(buffer_env) = buffer.env.as_ref() { - child.envs(buffer_env); - } - - if let Some(working_dir_path) = working_dir_path { - child.current_dir(working_dir_path); - } - - if let Some(arguments) = arguments { - child.args(arguments.iter().map(|arg| { - if let Some(buffer_abs_path) = buffer.abs_path.as_ref() { - arg.replace("{buffer_path}", &buffer_abs_path.to_string_lossy()) - } else { - arg.replace("{buffer_path}", "Untitled") - } - })); - } - - let mut child = child - .stdin(smol::process::Stdio::piped()) - .stdout(smol::process::Stdio::piped()) - .stderr(smol::process::Stdio::piped()) - .spawn()?; - - let stdin = child.stdin.as_mut().context("failed to acquire stdin")?; - let text = buffer - .handle - .read_with(cx, |buffer, _| buffer.as_rope().clone())?; - for chunk in text.chunks() { - stdin.write_all(chunk.as_bytes()).await?; - } - stdin.flush().await?; - - let output = child.output().await?; - anyhow::ensure!( - output.status.success(), - "command failed with exit code {:?}:\nstdout: {}\nstderr: {}", - output.status.code(), - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ); - - let stdout = String::from_utf8(output.stdout)?; - Ok(Some( - buffer - .handle - .update(cx, |buffer, cx| buffer.diff(stdout, cx))? - .await, - )) - } - - async fn try_resolve_code_action( - lang_server: &LanguageServer, - action: &mut CodeAction, - ) -> anyhow::Result<()> { - match &mut action.lsp_action { - LspAction::Action(lsp_action) => { - if !action.resolved - && GetCodeActions::can_resolve_actions(&lang_server.capabilities()) - && lsp_action.data.is_some() - && (lsp_action.command.is_none() || lsp_action.edit.is_none()) - { - *lsp_action = Box::new( - lang_server - .request::(*lsp_action.clone()) - .await - .into_response()?, - ); - } - } - LspAction::CodeLens(lens) => { - if !action.resolved && GetCodeLens::can_resolve_lens(&lang_server.capabilities()) { - *lens = lang_server - .request::(lens.clone()) - .await - .into_response()?; - } - } - LspAction::Command(_) => {} - } - - action.resolved = true; - anyhow::Ok(()) - } - - fn initialize_buffer(&mut self, buffer_handle: &Entity, cx: &mut Context) { - 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(); - - if let Some(diagnostics) = self.diagnostics.get(&worktree_id) { - for (server_id, diagnostics) in - diagnostics.get(file.path()).cloned().unwrap_or_default() - { - self.update_buffer_diagnostics( - buffer_handle, - server_id, - None, - None, - None, - Vec::new(), - diagnostics, - cx, - ) - .log_err(); - } - } - 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 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 - .as_ref() - .map(|characters| characters.iter().cloned().collect()) - }) - .unwrap_or_default(), - cx, - ); - }); - } - } - - pub(crate) fn reset_buffer(&mut self, buffer: &Entity, old_file: &File, cx: &mut App) { - buffer.update(cx, |buffer, cx| { - let Some(language) = buffer.language() else { - return; - }; - let path = ProjectPath { - worktree_id: old_file.worktree_id(cx), - path: old_file.path.clone(), - }; - for server_id in self.language_server_ids_for_project_path(path, language, cx) { - buffer.update_diagnostics(server_id, DiagnosticSet::new([], buffer), cx); - buffer.set_completion_triggers(server_id, Default::default(), cx); - } - }); - } - - fn update_buffer_diagnostics( - &mut self, - buffer: &Entity, - server_id: LanguageServerId, - registration_id: Option>, - result_id: Option, - version: Option, - new_diagnostics: Vec>>, - reused_diagnostics: Vec>>, - cx: &mut Context, - ) -> Result<()> { - fn compare_diagnostics(a: &Diagnostic, b: &Diagnostic) -> Ordering { - Ordering::Equal - .then_with(|| b.is_primary.cmp(&a.is_primary)) - .then_with(|| a.is_disk_based.cmp(&b.is_disk_based)) - .then_with(|| a.severity.cmp(&b.severity)) - .then_with(|| a.message.cmp(&b.message)) - } - - let mut diagnostics = Vec::with_capacity(new_diagnostics.len() + reused_diagnostics.len()); - diagnostics.extend(new_diagnostics.into_iter().map(|d| (true, d))); - diagnostics.extend(reused_diagnostics.into_iter().map(|d| (false, d))); - - diagnostics.sort_unstable_by(|(_, a), (_, b)| { - Ordering::Equal - .then_with(|| a.range.start.cmp(&b.range.start)) - .then_with(|| b.range.end.cmp(&a.range.end)) - .then_with(|| compare_diagnostics(&a.diagnostic, &b.diagnostic)) - }); - - let snapshot = self.buffer_snapshot_for_lsp_version(buffer, server_id, version, cx)?; - - let edits_since_save = std::cell::LazyCell::new(|| { - let saved_version = buffer.read(cx).saved_version(); - Patch::new(snapshot.edits_since::(saved_version).collect()) - }); - - let mut sanitized_diagnostics = Vec::with_capacity(diagnostics.len()); - - for (new_diagnostic, entry) in diagnostics { - let start; - let end; - if new_diagnostic && entry.diagnostic.is_disk_based { - // Some diagnostics are based on files on disk instead of buffers' - // current contents. Adjust these diagnostics' ranges to reflect - // any unsaved edits. - // Do not alter the reused ones though, as their coordinates were stored as anchors - // and were properly adjusted on reuse. - start = Unclipped((*edits_since_save).old_to_new(entry.range.start.0)); - end = Unclipped((*edits_since_save).old_to_new(entry.range.end.0)); - } else { - start = entry.range.start; - end = entry.range.end; - } - - let mut range = snapshot.clip_point_utf16(start, Bias::Left) - ..snapshot.clip_point_utf16(end, Bias::Right); - - // Expand empty ranges by one codepoint - if range.start == range.end { - // This will be go to the next boundary when being clipped - range.end.column += 1; - range.end = snapshot.clip_point_utf16(Unclipped(range.end), Bias::Right); - if range.start == range.end && range.end.column > 0 { - range.start.column -= 1; - range.start = snapshot.clip_point_utf16(Unclipped(range.start), Bias::Left); - } - } - - sanitized_diagnostics.push(DiagnosticEntry { - range, - diagnostic: entry.diagnostic, - }); - } - drop(edits_since_save); - - let set = DiagnosticSet::new(sanitized_diagnostics, &snapshot); - buffer.update(cx, |buffer, cx| { - if let Some(registration_id) = registration_id { - if let Some(abs_path) = File::from_dyn(buffer.file()).map(|f| f.abs_path(cx)) { - self.buffer_pull_diagnostics_result_ids - .entry(server_id) - .or_default() - .entry(registration_id) - .or_default() - .insert(abs_path, result_id); - } - } - - buffer.update_diagnostics(server_id, set, cx) - }); - - 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, - only_register_servers: HashSet, - cx: &mut Context, - ) { - let buffer = buffer_handle.read(cx); - let buffer_id = buffer.remote_id(); - - let Some(file) = File::from_dyn(buffer.file()) else { - return; - }; - if !file.is_local() { - return; - } - - let abs_path = file.abs_path(cx); - let Some(uri) = file_path_to_lsp_url(&abs_path).log_err() else { - return; - }; - let initial_snapshot = buffer.text_snapshot(); - let worktree_id = file.worktree_id(cx); - - let Some(language) = buffer.language().cloned() else { - return; - }; - let path: Arc = file - .path() - .parent() - .map(Arc::from) - .unwrap_or_else(|| file.path().clone()); - let Some(worktree) = self - .worktree_store - .read(cx) - .worktree_for_id(worktree_id, cx) - else { - return; - }; - 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))) - .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 servers = self - .lsp_tree - .walk( - ProjectPath { worktree_id, path }, - language.name(), - language.manifest(), - &delegate, - cx, - ) - .collect::>(); - (false, lsp_delegate, servers) - }); - let servers_and_adapters = servers - .into_iter() - .filter_map(|server_node| { - if reused && server_node.server_id().is_none() { - 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(name) = server_node.name() - && !only_register_servers.contains(&LanguageServerSelector::Name(name)) - { - return None; - } - } - - let server_id = server_node.server_id_or_init(|disposition| { - let path = &disposition.path; - - { - let uri = Uri::from_file_path(worktree.read(cx).absolutize(&path.path)); - - 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); - }; - server_id - } - })?; - let server_state = self.language_servers.get(&server_id)?; - if let LanguageServerState::Running { - server, adapter, .. - } = server_state - { - Some((server.clone(), adapter.clone())) - } else { - None - } - }) - .collect::>(); - for (server, adapter) in servers_and_adapters { - 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 - .as_ref() - .map(|characters| characters.iter().cloned().collect()) - }) - .unwrap_or_default(), - cx, - ); - }); - - let snapshot = LspBufferSnapshot { - version: 0, - 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()), - 0, - initial_snapshot.text(), - ); - - vec![snapshot] - }); - - self.buffers_opened_in_servers - .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().into_owned(), - buffer_id: buffer_id.to_proto(), - }, - ), - }); - } - } - } - - fn reuse_existing_language_server<'lang_name>( - &self, - server_tree: &LanguageServerTree, - worktree: &Entity, - language_name: &'lang_name LanguageName, - cx: &mut App, - ) -> Option<( - Arc, - impl FnOnce(&mut LanguageServerTree) -> Vec + use<'lang_name>, - )> { - if worktree.read(cx).is_visible() { - return None; - } - - let worktree_store = self.worktree_store.read(cx); - let servers = server_tree - .instances - .iter() - .filter(|(worktree_id, _)| { - worktree_store - .worktree_for_id(**worktree_id, cx) - .is_some_and(|worktree| worktree.read(cx).is_visible()) - }) - .flat_map(|(worktree_id, servers)| { - servers - .roots - .iter() - .flat_map(|(_, language_servers)| language_servers) - .map(move |(_, (server_node, server_languages))| { - (worktree_id, server_node, server_languages) - }) - .filter(|(_, _, server_languages)| server_languages.contains(language_name)) - .map(|(worktree_id, server_node, _)| { - ( - *worktree_id, - LanguageServerTreeNode::from(Arc::downgrade(server_node)), - ) - }) - }) - .fold(HashMap::default(), |mut acc, (worktree_id, server_node)| { - acc.entry(worktree_id) - .or_insert_with(Vec::new) - .push(server_node); - acc - }) - .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 - }; - - let delegate = LocalLspAdapterDelegate::from_local_lsp(self, worktree, cx); - Some((delegate, apply)) - } - - pub(crate) fn unregister_old_buffer_from_language_servers( - &mut self, - buffer: &Entity, - old_file: &File, - cx: &mut App, - ) { - let old_path = match old_file.as_local() { - Some(local) => local.abs_path(cx), - None => return, - }; - - let Ok(file_url) = lsp::Uri::from_file_path(old_path.as_path()) else { - debug_panic!("{old_path:?} is not parseable as an URI"); - return; - }; - self.unregister_buffer_from_language_servers(buffer, &file_url, cx); - } - - pub(crate) fn unregister_buffer_from_language_servers( - &mut self, - buffer: &Entity, - file_url: &lsp::Uri, - cx: &mut App, - ) { - buffer.update(cx, |buffer, cx| { - let mut snapshots = self.buffer_snapshots.remove(&buffer.remote_id()); - - for (_, language_server) in self.language_servers_for_buffer(buffer, cx) { - if snapshots - .as_mut() - .is_some_and(|map| map.remove(&language_server.server_id()).is_some()) - { - language_server.unregister_buffer(file_url.clone()); - } - } - }); - } - - fn buffer_snapshot_for_lsp_version( - &mut self, - buffer: &Entity, - server_id: LanguageServerId, - version: Option, - cx: &App, - ) -> Result { - const OLD_VERSIONS_TO_RETAIN: i32 = 10; - - if let Some(version) = version { - let buffer_id = buffer.read(cx).remote_id(); - let snapshots = if let Some(snapshots) = self - .buffer_snapshots - .get_mut(&buffer_id) - .and_then(|m| m.get_mut(&server_id)) - { - snapshots - } else if version == 0 { - // Some language servers report version 0 even if the buffer hasn't been opened yet. - // We detect this case and treat it as if the version was `None`. - return Ok(buffer.read(cx).text_snapshot()); - } else { - anyhow::bail!("no snapshots found for buffer {buffer_id} and server {server_id}"); - }; - - let found_snapshot = snapshots - .binary_search_by_key(&version, |e| e.version) - .map(|ix| snapshots[ix].snapshot.clone()) - .map_err(|_| { - anyhow!("snapshot not found for buffer {buffer_id} server {server_id} at version {version}") - })?; - - snapshots.retain(|snapshot| snapshot.version + OLD_VERSIONS_TO_RETAIN >= version); - Ok(found_snapshot) - } else { - Ok((buffer.read(cx)).text_snapshot()) - } - } - - async fn get_server_code_actions_from_action_kinds( - lsp_store: &WeakEntity, - language_server_id: LanguageServerId, - code_action_kinds: Vec, - buffer: &Entity, - cx: &mut AsyncApp, - ) -> Result> { - let actions = lsp_store - .update(cx, move |this, cx| { - let request = GetCodeActions { - range: text::Anchor::min_max_range_for_buffer(buffer.read(cx).remote_id()), - kinds: Some(code_action_kinds), - }; - let server = LanguageServerToQuery::Other(language_server_id); - this.request_lsp(buffer.clone(), server, request, cx) - })? - .await?; - Ok(actions) - } - - pub async fn execute_code_actions_on_server( - lsp_store: &WeakEntity, - language_server: &Arc, - - actions: Vec, - push_to_history: bool, - project_transaction: &mut ProjectTransaction, - cx: &mut AsyncApp, - ) -> anyhow::Result<()> { - for mut action in actions { - Self::try_resolve_code_action(language_server, &mut action) - .await - .context("resolving a formatting code action")?; - - if let Some(edit) = action.lsp_action.edit() { - if edit.changes.is_none() && edit.document_changes.is_none() { - continue; - } - - let new = Self::deserialize_workspace_edit( - lsp_store.upgrade().context("project dropped")?, - edit.clone(), - push_to_history, - language_server.clone(), - cx, - ) - .await?; - project_transaction.0.extend(new.0); - } - - if let Some(command) = action.lsp_action.command() { - let server_capabilities = language_server.capabilities(); - let available_commands = server_capabilities - .execute_command_provider - .as_ref() - .map(|options| options.commands.as_slice()) - .unwrap_or_default(); - if available_commands.contains(&command.command) { - lsp_store.update(cx, |lsp_store, _| { - if let LspStoreMode::Local(mode) = &mut lsp_store.mode { - mode.last_workspace_edits_by_language_server - .remove(&language_server.server_id()); - } - })?; - - language_server - .request::(lsp::ExecuteCommandParams { - command: command.command.clone(), - arguments: command.arguments.clone().unwrap_or_default(), - ..Default::default() - }) - .await - .into_response() - .context("execute command")?; - - lsp_store.update(cx, |this, _| { - if let LspStoreMode::Local(mode) = &mut this.mode { - project_transaction.0.extend( - mode.last_workspace_edits_by_language_server - .remove(&language_server.server_id()) - .unwrap_or_default() - .0, - ) - } - })?; - } else { - log::warn!( - "Cannot execute a command {} not listed in the language server capabilities", - command.command - ) - } - } - } - Ok(()) - } - - pub async fn deserialize_text_edits( - this: Entity, - buffer_to_edit: Entity, - edits: Vec, - push_to_history: bool, - _: Arc, - language_server: Arc, - cx: &mut AsyncApp, - ) -> Result> { - let edits = this - .update(cx, |this, cx| { - this.as_local_mut().unwrap().edits_from_lsp( - &buffer_to_edit, - edits, - language_server.server_id(), - None, - cx, - ) - })? - .await?; - - let transaction = buffer_to_edit.update(cx, |buffer, cx| { - buffer.finalize_last_transaction(); - buffer.start_transaction(); - for (range, text) in edits { - buffer.edit([(range, text)], None, cx); - } - - if buffer.end_transaction(cx).is_some() { - let transaction = buffer.finalize_last_transaction().unwrap().clone(); - if !push_to_history { - buffer.forget_transaction(transaction.id); - } - Some(transaction) - } else { - None - } - })?; - - Ok(transaction) - } - - #[allow(clippy::type_complexity)] - pub(crate) fn edits_from_lsp( - &mut self, - buffer: &Entity, - lsp_edits: impl 'static + Send + IntoIterator, - server_id: LanguageServerId, - version: Option, - cx: &mut Context, - ) -> Task, Arc)>>> { - let snapshot = self.buffer_snapshot_for_lsp_version(buffer, server_id, version, cx); - cx.background_spawn(async move { - let snapshot = snapshot?; - let mut lsp_edits = lsp_edits - .into_iter() - .map(|edit| (range_from_lsp(edit.range), edit.new_text)) - .collect::>(); - - lsp_edits.sort_by_key(|(range, _)| (range.start, range.end)); - - let mut lsp_edits = lsp_edits.into_iter().peekable(); - let mut edits = Vec::new(); - while let Some((range, mut new_text)) = lsp_edits.next() { - // Clip invalid ranges provided by the language server. - let mut range = snapshot.clip_point_utf16(range.start, Bias::Left) - ..snapshot.clip_point_utf16(range.end, Bias::Left); - - // Combine any LSP edits that are adjacent. - // - // Also, combine LSP edits that are separated from each other by only - // a newline. This is important because for some code actions, - // Rust-analyzer rewrites the entire buffer via a series of edits that - // are separated by unchanged newline characters. - // - // In order for the diffing logic below to work properly, any edits that - // cancel each other out must be combined into one. - while let Some((next_range, next_text)) = lsp_edits.peek() { - if next_range.start.0 > range.end { - if next_range.start.0.row > range.end.row + 1 - || next_range.start.0.column > 0 - || snapshot.clip_point_utf16( - Unclipped(PointUtf16::new(range.end.row, u32::MAX)), - Bias::Left, - ) > range.end - { - break; - } - new_text.push('\n'); - } - range.end = snapshot.clip_point_utf16(next_range.end, Bias::Left); - new_text.push_str(next_text); - lsp_edits.next(); - } - - // For multiline edits, perform a diff of the old and new text so that - // we can identify the changes more precisely, preserving the locations - // of any anchors positioned in the unchanged regions. - if range.end.row > range.start.row { - let offset = range.start.to_offset(&snapshot); - let old_text = snapshot.text_for_range(range).collect::(); - let range_edits = language::text_diff(old_text.as_str(), &new_text); - edits.extend(range_edits.into_iter().map(|(range, replacement)| { - ( - snapshot.anchor_after(offset + range.start) - ..snapshot.anchor_before(offset + range.end), - replacement, - ) - })); - } else if range.end == range.start { - let anchor = snapshot.anchor_after(range.start); - edits.push((anchor..anchor, new_text.into())); - } else { - let edit_start = snapshot.anchor_after(range.start); - let edit_end = snapshot.anchor_before(range.end); - edits.push((edit_start..edit_end, new_text.into())); - } - } - - Ok(edits) - }) - } - - pub(crate) async fn deserialize_workspace_edit( - this: Entity, - edit: lsp::WorkspaceEdit, - push_to_history: bool, - language_server: Arc, - cx: &mut AsyncApp, - ) -> Result { - let fs = this.read_with(cx, |this, _| this.as_local().unwrap().fs.clone())?; - - let mut operations = Vec::new(); - if let Some(document_changes) = edit.document_changes { - match document_changes { - lsp::DocumentChanges::Edits(edits) => { - operations.extend(edits.into_iter().map(lsp::DocumentChangeOperation::Edit)) - } - lsp::DocumentChanges::Operations(ops) => operations = ops, - } - } else if let Some(changes) = edit.changes { - operations.extend(changes.into_iter().map(|(uri, edits)| { - lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit { - text_document: lsp::OptionalVersionedTextDocumentIdentifier { - uri, - version: None, - }, - edits: edits.into_iter().map(Edit::Plain).collect(), - }) - })); - } - - let mut project_transaction = ProjectTransaction::default(); - for operation in operations { - match operation { - lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Create(op)) => { - let abs_path = op - .uri - .to_file_path() - .map_err(|()| anyhow!("can't convert URI to path"))?; - - if let Some(parent_path) = abs_path.parent() { - fs.create_dir(parent_path).await?; - } - if abs_path.ends_with("/") { - fs.create_dir(&abs_path).await?; - } else { - fs.create_file( - &abs_path, - op.options - .map(|options| fs::CreateOptions { - overwrite: options.overwrite.unwrap_or(false), - ignore_if_exists: options.ignore_if_exists.unwrap_or(false), - }) - .unwrap_or_default(), - ) - .await?; - } - } - - lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Rename(op)) => { - let source_abs_path = op - .old_uri - .to_file_path() - .map_err(|()| anyhow!("can't convert URI to path"))?; - let target_abs_path = op - .new_uri - .to_file_path() - .map_err(|()| anyhow!("can't convert URI to path"))?; - - let options = fs::RenameOptions { - overwrite: op - .options - .as_ref() - .and_then(|options| options.overwrite) - .unwrap_or(false), - ignore_if_exists: op - .options - .as_ref() - .and_then(|options| options.ignore_if_exists) - .unwrap_or(false), - create_parents: true, - }; - - fs.rename(&source_abs_path, &target_abs_path, options) - .await?; - } - - lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Delete(op)) => { - let abs_path = op - .uri - .to_file_path() - .map_err(|()| anyhow!("can't convert URI to path"))?; - let options = op - .options - .map(|options| fs::RemoveOptions { - recursive: options.recursive.unwrap_or(false), - ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false), - }) - .unwrap_or_default(); - if abs_path.ends_with("/") { - fs.remove_dir(&abs_path, options).await?; - } else { - fs.remove_file(&abs_path, options).await?; - } - } - - lsp::DocumentChangeOperation::Edit(op) => { - let buffer_to_edit = this - .update(cx, |this, cx| { - this.open_local_buffer_via_lsp( - op.text_document.uri.clone(), - language_server.server_id(), - cx, - ) - })? - .await?; - - let edits = this - .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| { - this.worktree_store - .read(cx) - .entry_for_path(&project_path, cx) - .is_some_and(|entry| Some(entry.id) == active_entry) - }); - let local = this.as_local_mut().unwrap(); - - let (mut edits, mut snippet_edits) = (vec![], vec![]); - for edit in op.edits { - match edit { - Edit::Plain(edit) => { - if !edits.contains(&edit) { - edits.push(edit) - } - } - Edit::Annotated(edit) => { - if !edits.contains(&edit.text_edit) { - edits.push(edit.text_edit) - } - } - Edit::Snippet(edit) => { - let Ok(snippet) = Snippet::parse(&edit.snippet.value) - else { - continue; - }; - - if is_active_entry { - snippet_edits.push((edit.range, snippet)); - } else { - // Since this buffer is not focused, apply a normal edit. - let new_edit = TextEdit { - range: edit.range, - new_text: snippet.text, - }; - if !edits.contains(&new_edit) { - edits.push(new_edit); - } - } - } - } - } - if !snippet_edits.is_empty() { - let buffer_id = buffer_to_edit.read(cx).remote_id(); - let version = if let Some(buffer_version) = op.text_document.version - { - local - .buffer_snapshot_for_lsp_version( - &buffer_to_edit, - language_server.server_id(), - Some(buffer_version), - cx, - ) - .ok() - .map(|snapshot| snapshot.version) - } else { - Some(buffer_to_edit.read(cx).saved_version().clone()) - }; - - let most_recent_edit = - version.and_then(|version| version.most_recent()); - // Check if the edit that triggered that edit has been made by this participant. - - if let Some(most_recent_edit) = most_recent_edit { - cx.emit(LspStoreEvent::SnippetEdit { - buffer_id, - edits: snippet_edits, - most_recent_edit, - }); - } - } - - local.edits_from_lsp( - &buffer_to_edit, - edits, - language_server.server_id(), - op.text_document.version, - cx, - ) - })? - .await?; - - let transaction = buffer_to_edit.update(cx, |buffer, cx| { - buffer.finalize_last_transaction(); - buffer.start_transaction(); - for (range, text) in edits { - buffer.edit([(range, text)], None, cx); - } - - 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) - } - }) - })?; - if let Some(transaction) = transaction { - project_transaction.0.insert(buffer_to_edit, transaction); - } - } - } - } - - Ok(project_transaction) - } - - async fn on_lsp_workspace_edit( - this: WeakEntity, - params: lsp::ApplyWorkspaceEditParams, - server_id: LanguageServerId, - cx: &mut AsyncApp, - ) -> Result { - let this = this.upgrade().context("project project closed")?; - let language_server = this - .read_with(cx, |this, _| this.language_server_for_id(server_id))? - .context("language server not found")?; - let transaction = Self::deserialize_workspace_edit( - this.clone(), - params.edit, - true, - language_server.clone(), - cx, - ) - .await - .log_err(); - this.update(cx, |this, _| { - if let Some(transaction) = transaction { - this.as_local_mut() - .unwrap() - .last_workspace_edits_by_language_server - .insert(server_id, transaction); - } - })?; - Ok(lsp::ApplyWorkspaceEditResponse { - applied: true, - failed_change: None, - failure_reason: None, - }) - } - - fn remove_worktree( - &mut self, - id_to_remove: WorktreeId, - cx: &mut Context, - ) -> Vec { - self.diagnostics.remove(&id_to_remove); - self.prettier_store.update(cx, |prettier_store, cx| { - prettier_store.remove_worktree(id_to_remove, cx); - }); - - let mut servers_to_remove = BTreeSet::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); - } else { - servers_to_preserve.insert(state.id); - } - } - 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 { - self.language_server_watched_paths - .remove(server_id_to_remove); - self.language_server_paths_watched_for_rename - .remove(server_id_to_remove); - self.last_workspace_edits_by_language_server - .remove(server_id_to_remove); - self.language_servers.remove(server_id_to_remove); - self.buffer_pull_diagnostics_result_ids - .remove(server_id_to_remove); - self.workspace_pull_diagnostics_result_ids - .remove(server_id_to_remove); - for buffer_servers in self.buffers_opened_in_servers.values_mut() { - buffer_servers.remove(server_id_to_remove); - } - cx.emit(LspStoreEvent::LanguageServerRemoved(*server_id_to_remove)); - } - servers_to_remove.into_iter().collect() - } - - fn rebuild_watched_paths_inner<'a>( - &'a self, - language_server_id: LanguageServerId, - watchers: impl Iterator, - cx: &mut Context, - ) -> LanguageServerWatchedPathsBuilder { - let worktrees = self - .worktree_store - .read(cx) - .worktrees() - .filter_map(|worktree| { - self.language_servers_for_worktree(worktree.read(cx).id()) - .find(|server| server.server_id() == language_server_id) - .map(|_| worktree) - }) - .collect::>(); - - let mut worktree_globs = HashMap::default(); - let mut abs_globs = HashMap::default(); - log::trace!( - "Processing new watcher paths for language server with id {}", - language_server_id - ); - - for watcher in watchers { - if let Some((worktree, literal_prefix, pattern)) = - Self::worktree_and_path_for_file_watcher(&worktrees, watcher, cx) - { - worktree.update(cx, |worktree, _| { - if let Some((tree, glob)) = - worktree.as_local_mut().zip(Glob::new(&pattern).log_err()) - { - tree.add_path_prefix_to_scan(literal_prefix); - worktree_globs - .entry(tree.id()) - .or_insert_with(GlobSetBuilder::new) - .add(glob); - } - }); - } else { - let (path, pattern) = match &watcher.glob_pattern { - lsp::GlobPattern::String(s) => { - let watcher_path = SanitizedPath::new(s); - let path = glob_literal_prefix(watcher_path.as_path()); - let pattern = watcher_path - .as_path() - .strip_prefix(&path) - .map(|p| p.to_string_lossy().into_owned()) - .unwrap_or_else(|e| { - debug_panic!( - "Failed to strip prefix for string pattern: {}, with prefix: {}, with error: {}", - s, - path.display(), - e - ); - watcher_path.as_path().to_string_lossy().into_owned() - }); - (path, pattern) - } - lsp::GlobPattern::Relative(rp) => { - let Ok(mut base_uri) = match &rp.base_uri { - lsp::OneOf::Left(workspace_folder) => &workspace_folder.uri, - lsp::OneOf::Right(base_uri) => base_uri, - } - .to_file_path() else { - continue; - }; - - let path = glob_literal_prefix(Path::new(&rp.pattern)); - let pattern = Path::new(&rp.pattern) - .strip_prefix(&path) - .map(|p| p.to_string_lossy().into_owned()) - .unwrap_or_else(|e| { - debug_panic!( - "Failed to strip prefix for relative pattern: {}, with prefix: {}, with error: {}", - rp.pattern, - path.display(), - e - ); - rp.pattern.clone() - }); - base_uri.push(path); - (base_uri, pattern) - } - }; - - if let Some(glob) = Glob::new(&pattern).log_err() { - if !path - .components() - .any(|c| matches!(c, path::Component::Normal(_))) - { - // For an unrooted glob like `**/Cargo.toml`, watch it within each worktree, - // rather than adding a new watcher for `/`. - for worktree in &worktrees { - worktree_globs - .entry(worktree.read(cx).id()) - .or_insert_with(GlobSetBuilder::new) - .add(glob.clone()); - } - } else { - abs_globs - .entry(path.into()) - .or_insert_with(GlobSetBuilder::new) - .add(glob); - } - } - } - } - - let mut watch_builder = LanguageServerWatchedPathsBuilder::default(); - for (worktree_id, builder) in worktree_globs { - if let Ok(globset) = builder.build() { - watch_builder.watch_worktree(worktree_id, globset); - } - } - for (abs_path, builder) in abs_globs { - if let Ok(globset) = builder.build() { - watch_builder.watch_abs_path(abs_path, globset); - } - } - watch_builder - } - - fn worktree_and_path_for_file_watcher( - worktrees: &[Entity], - watcher: &FileSystemWatcher, - cx: &App, - ) -> Option<(Entity, Arc, String)> { - worktrees.iter().find_map(|worktree| { - let tree = worktree.read(cx); - let worktree_root_path = tree.abs_path(); - let path_style = tree.path_style(); - match &watcher.glob_pattern { - lsp::GlobPattern::String(s) => { - let watcher_path = SanitizedPath::new(s); - let relative = watcher_path - .as_path() - .strip_prefix(&worktree_root_path) - .ok()?; - let literal_prefix = glob_literal_prefix(relative); - Some(( - worktree.clone(), - RelPath::new(&literal_prefix, path_style).ok()?.into_arc(), - relative.to_string_lossy().into_owned(), - )) - } - lsp::GlobPattern::Relative(rp) => { - let base_uri = match &rp.base_uri { - lsp::OneOf::Left(workspace_folder) => &workspace_folder.uri, - lsp::OneOf::Right(base_uri) => base_uri, - } - .to_file_path() - .ok()?; - let relative = base_uri.strip_prefix(&worktree_root_path).ok()?; - let mut literal_prefix = relative.to_owned(); - literal_prefix.push(glob_literal_prefix(Path::new(&rp.pattern))); - Some(( - worktree.clone(), - RelPath::new(&literal_prefix, path_style).ok()?.into_arc(), - rp.pattern.clone(), - )) - } - } - }) - } - - fn rebuild_watched_paths( - &mut self, - language_server_id: LanguageServerId, - cx: &mut Context, - ) { - let Some(registrations) = self - .language_server_dynamic_registrations - .get(&language_server_id) - else { - return; - }; - - let watch_builder = self.rebuild_watched_paths_inner( - language_server_id, - registrations.did_change_watched_files.values().flatten(), - cx, - ); - let watcher = watch_builder.build(self.fs.clone(), language_server_id, cx); - self.language_server_watched_paths - .insert(language_server_id, watcher); - - cx.notify(); - } - - fn on_lsp_did_change_watched_files( - &mut self, - language_server_id: LanguageServerId, - registration_id: &str, - params: DidChangeWatchedFilesRegistrationOptions, - cx: &mut Context, - ) { - let registrations = self - .language_server_dynamic_registrations - .entry(language_server_id) - .or_default(); - - registrations - .did_change_watched_files - .insert(registration_id.to_string(), params.watchers); - - self.rebuild_watched_paths(language_server_id, cx); - } - - fn on_lsp_unregister_did_change_watched_files( - &mut self, - language_server_id: LanguageServerId, - registration_id: &str, - cx: &mut Context, - ) { - let registrations = self - .language_server_dynamic_registrations - .entry(language_server_id) - .or_default(); - - if registrations - .did_change_watched_files - .remove(registration_id) - .is_some() - { - log::info!( - "language server {}: unregistered workspace/DidChangeWatchedFiles capability with id {}", - language_server_id, - registration_id - ); - } else { - log::warn!( - "language server {}: failed to unregister workspace/DidChangeWatchedFiles capability with id {}. not registered.", - language_server_id, - registration_id - ); - } - - self.rebuild_watched_paths(language_server_id, cx); - } - - async fn initialization_options_for_adapter( - adapter: Arc, - delegate: &Arc, - ) -> Result> { - let Some(mut initialization_config) = - adapter.clone().initialization_options(delegate).await? - else { - return Ok(None); - }; - - for other_adapter in delegate.registered_lsp_adapters() { - if other_adapter.name() == adapter.name() { - continue; - } - if let Ok(Some(target_config)) = other_adapter - .clone() - .additional_initialization_options(adapter.name(), delegate) - .await - { - merge_json_value_into(target_config.clone(), &mut initialization_config); - } - } - - Ok(Some(initialization_config)) - } - - async fn workspace_configuration_for_adapter( - adapter: Arc, - delegate: &Arc, - toolchain: Option, - requested_uri: Option, - cx: &mut AsyncApp, - ) -> Result { - let mut workspace_config = adapter - .clone() - .workspace_configuration(delegate, toolchain, requested_uri, cx) - .await?; - - for other_adapter in delegate.registered_lsp_adapters() { - if other_adapter.name() == adapter.name() { - continue; - } - if let Ok(Some(target_config)) = other_adapter - .clone() - .additional_workspace_configuration(adapter.name(), delegate, cx) - .await - { - merge_json_value_into(target_config.clone(), &mut workspace_config); - } - } - - 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), - binary: Some(proto::LanguageServerBinaryInfo { - path: server.binary().path.to_string_lossy().into_owned(), - arguments: server - .binary() - .arguments - .iter() - .map(|arg| arg.to_string_lossy().into_owned()) - .collect(), - }), - configuration: serde_json::to_string(server.configuration()).ok(), - workspace_folders: server - .workspace_folders() - .iter() - .map(|uri| uri.to_string()) - .collect(), - }, - ), - }); - } -} - -#[derive(Debug)] -pub struct FormattableBuffer { - handle: Entity, - abs_path: Option, - env: Option>, - ranges: Option>>, -} - -pub struct RemoteLspStore { - upstream_client: Option, - upstream_project_id: u64, -} - -pub(crate) enum LspStoreMode { - Local(LocalLspStore), // ssh host and collab host - Remote(RemoteLspStore), // collab guest -} - -impl LspStoreMode { - fn is_local(&self) -> bool { - matches!(self, LspStoreMode::Local(_)) - } -} - -pub struct LspStore { - mode: LspStoreMode, - last_formatting_failure: Option, - downstream_client: Option<(AnyProtoClient, u64)>, - nonce: u128, - buffer_store: Entity, - worktree_store: Entity, - pub languages: Arc, - pub language_server_statuses: BTreeMap, - active_entry: Option, - _maintain_workspace_config: (Task>, watch::Sender<()>), - _maintain_buffer_languages: Task<()>, - diagnostic_summaries: - HashMap, HashMap>>, - pub lsp_server_capabilities: HashMap, - lsp_data: HashMap, - next_hint_id: Arc, -} - -#[derive(Debug)] -pub struct BufferLspData { - buffer_version: Global, - document_colors: Option, - code_lens: Option, - inlay_hints: BufferInlayHints, - lsp_requests: HashMap>>, - chunk_lsp_requests: HashMap>, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -struct LspKey { - request_type: TypeId, - server_queried: Option, -} - -impl BufferLspData { - fn new(buffer: &Entity, cx: &mut App) -> Self { - Self { - buffer_version: buffer.read(cx).version(), - document_colors: None, - code_lens: None, - inlay_hints: BufferInlayHints::new(buffer, cx), - lsp_requests: HashMap::default(), - chunk_lsp_requests: HashMap::default(), - } - } - - fn remove_server_data(&mut self, for_server: LanguageServerId) { - if let Some(document_colors) = &mut self.document_colors { - document_colors.colors.remove(&for_server); - document_colors.cache_version += 1; - } - - if let Some(code_lens) = &mut self.code_lens { - code_lens.lens.remove(&for_server); - } - - self.inlay_hints.remove_server_data(for_server); - } - - #[cfg(any(test, feature = "test-support"))] - pub fn inlay_hints(&self) -> &BufferInlayHints { - &self.inlay_hints - } -} - -#[derive(Debug, Default, Clone)] -pub struct DocumentColors { - pub colors: HashSet, - pub cache_version: Option, -} - -type DocumentColorTask = Shared>>>; -type CodeLensTask = Shared>, Arc>>>; - -#[derive(Debug, Default)] -struct DocumentColorData { - colors: HashMap>, - cache_version: usize, - colors_update: Option<(Global, DocumentColorTask)>, -} - -#[derive(Debug, Default)] -struct CodeLensData { - lens: HashMap>, - update: Option<(Global, CodeLensTask)>, -} - -#[derive(Debug)] -pub enum LspStoreEvent { - LanguageServerAdded(LanguageServerId, LanguageServerName, Option), - LanguageServerRemoved(LanguageServerId), - LanguageServerUpdate { - language_server_id: LanguageServerId, - name: Option, - message: proto::update_language_server::Variant, - }, - LanguageServerLog(LanguageServerId, LanguageServerLogType, String), - LanguageServerPrompt(LanguageServerPromptRequest), - LanguageDetected { - buffer: Entity, - new_language: Option>, - }, - Notification(String), - RefreshInlayHints { - server_id: LanguageServerId, - request_id: Option, - }, - RefreshCodeLens, - DiagnosticsUpdated { - server_id: LanguageServerId, - paths: Vec, - }, - DiskBasedDiagnosticsStarted { - language_server_id: LanguageServerId, - }, - DiskBasedDiagnosticsFinished { - language_server_id: LanguageServerId, - }, - SnippetEdit { - buffer_id: BufferId, - edits: Vec<(lsp::Range, Snippet)>, - most_recent_edit: clock::Lamport, - }, -} - -#[derive(Clone, Debug, Serialize)] -pub struct LanguageServerStatus { - pub name: LanguageServerName, - pub pending_work: BTreeMap, - pub has_pending_diagnostic_updates: bool, - pub progress_tokens: HashSet, - pub worktree: Option, - pub binary: Option, - pub configuration: Option, - pub workspace_folders: BTreeSet, -} - -#[derive(Clone, Debug)] -struct CoreSymbol { - pub language_server_name: LanguageServerName, - pub source_worktree_id: WorktreeId, - pub source_language_server_id: LanguageServerId, - pub path: SymbolLocation, - pub name: String, - pub kind: lsp::SymbolKind, - pub range: Range>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum SymbolLocation { - InProject(ProjectPath), - OutsideProject { - abs_path: Arc, - signature: [u8; 32], - }, -} - -impl SymbolLocation { - fn file_name(&self) -> Option<&str> { - match self { - Self::InProject(path) => path.path.file_name(), - Self::OutsideProject { abs_path, .. } => abs_path.file_name()?.to_str(), - } - } -} - -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_restart_language_servers); - client.add_entity_request_handler(Self::handle_stop_language_servers); - client.add_entity_request_handler(Self::handle_cancel_language_server_work); - client.add_entity_message_handler(Self::handle_start_language_server); - client.add_entity_message_handler(Self::handle_update_language_server); - client.add_entity_message_handler(Self::handle_language_server_log); - client.add_entity_message_handler(Self::handle_update_diagnostic_summary); - client.add_entity_request_handler(Self::handle_format_buffers); - client.add_entity_request_handler(Self::handle_apply_code_action_kind); - client.add_entity_request_handler(Self::handle_resolve_completion_documentation); - client.add_entity_request_handler(Self::handle_apply_code_action); - client.add_entity_request_handler(Self::handle_get_project_symbols); - client.add_entity_request_handler(Self::handle_resolve_inlay_hint); - client.add_entity_request_handler(Self::handle_get_color_presentation); - client.add_entity_request_handler(Self::handle_open_buffer_for_symbol); - client.add_entity_request_handler(Self::handle_refresh_inlay_hints); - client.add_entity_request_handler(Self::handle_refresh_code_lens); - client.add_entity_request_handler(Self::handle_on_type_formatting); - 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_pull_workspace_diagnostics); - client.add_entity_request_handler(Self::handle_lsp_get_completions); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_lsp_command::); - - client.add_entity_request_handler(Self::handle_lsp_ext_cancel_flycheck); - client.add_entity_request_handler(Self::handle_lsp_ext_run_flycheck); - client.add_entity_request_handler(Self::handle_lsp_ext_clear_flycheck); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler( - Self::handle_lsp_command::, - ); - client.add_entity_request_handler( - Self::handle_lsp_command::, - ); - client.add_entity_request_handler( - Self::handle_lsp_command::, - ); - } - - pub fn as_remote(&self) -> Option<&RemoteLspStore> { - match &self.mode { - LspStoreMode::Remote(remote_lsp_store) => Some(remote_lsp_store), - _ => None, - } - } - - pub fn as_local(&self) -> Option<&LocalLspStore> { - match &self.mode { - LspStoreMode::Local(local_lsp_store) => Some(local_lsp_store), - _ => None, - } - } - - pub fn as_local_mut(&mut self) -> Option<&mut LocalLspStore> { - match &mut self.mode { - LspStoreMode::Local(local_lsp_store) => Some(local_lsp_store), - _ => None, - } - } - - pub fn upstream_client(&self) -> Option<(AnyProtoClient, u64)> { - match &self.mode { - LspStoreMode::Remote(RemoteLspStore { - upstream_client: Some(upstream_client), - upstream_project_id, - .. - }) => Some((upstream_client.clone(), *upstream_project_id)), - - LspStoreMode::Remote(RemoteLspStore { - upstream_client: None, - .. - }) => None, - LspStoreMode::Local(_) => None, - } - } - - pub fn new_local( - buffer_store: Entity, - worktree_store: Entity, - prettier_store: Entity, - toolchain_store: Entity, - environment: Entity, - manifest_tree: Entity, - languages: Arc, - http_client: Arc, - fs: Arc, - cx: &mut Context, - ) -> Self { - let yarn = YarnPathStore::new(fs.clone(), cx); - cx.subscribe(&buffer_store, Self::on_buffer_store_event) - .detach(); - cx.subscribe(&worktree_store, Self::on_worktree_store_event) - .detach(); - cx.subscribe(&prettier_store, Self::on_prettier_store_event) - .detach(); - cx.subscribe(&toolchain_store, Self::on_toolchain_store_event) - .detach(); - cx.observe_global::(Self::on_settings_changed) - .detach(); - subscribe_to_binary_statuses(&languages, cx).detach(); - - let _maintain_workspace_config = { - let (sender, receiver) = watch::channel(); - (Self::maintain_workspace_config(receiver, cx), sender) - }; - - Self { - mode: LspStoreMode::Local(LocalLspStore { - weak: cx.weak_entity(), - worktree_store: worktree_store.clone(), - - supplementary_language_servers: Default::default(), - languages: languages.clone(), - language_server_ids: Default::default(), - language_servers: Default::default(), - last_workspace_edits_by_language_server: Default::default(), - language_server_watched_paths: Default::default(), - language_server_paths_watched_for_rename: Default::default(), - language_server_dynamic_registrations: Default::default(), - buffers_being_formatted: Default::default(), - buffer_snapshots: Default::default(), - prettier_store, - environment, - http_client, - fs, - yarn, - next_diagnostic_group_id: Default::default(), - diagnostics: Default::default(), - _subscription: cx.on_app_quit(|this, cx| { - this.as_local_mut() - .unwrap() - .shutdown_language_servers_on_quit(cx) - }), - lsp_tree: LanguageServerTree::new( - manifest_tree, - languages.clone(), - toolchain_store.clone(), - ), - toolchain_store, - registered_buffers: HashMap::default(), - buffers_opened_in_servers: HashMap::default(), - buffer_pull_diagnostics_result_ids: HashMap::default(), - workspace_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, - languages: languages.clone(), - language_server_statuses: Default::default(), - nonce: StdRng::from_os_rng().random(), - diagnostic_summaries: HashMap::default(), - lsp_server_capabilities: HashMap::default(), - lsp_data: HashMap::default(), - next_hint_id: Arc::default(), - active_entry: None, - _maintain_workspace_config, - _maintain_buffer_languages: Self::maintain_buffer_languages(languages, cx), - } - } - - fn send_lsp_proto_request( - &self, - buffer: Entity, - client: AnyProtoClient, - upstream_project_id: u64, - 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?; - let this = this.upgrade().context("project dropped")?; - request - .response_from_proto(response, this, buffer, cx.clone()) - .await - }) - } - - pub(super) fn new_remote( - buffer_store: Entity, - worktree_store: Entity, - languages: Arc, - upstream_client: AnyProtoClient, - project_id: u64, - cx: &mut Context, - ) -> Self { - cx.subscribe(&buffer_store, Self::on_buffer_store_event) - .detach(); - cx.subscribe(&worktree_store, Self::on_worktree_store_event) - .detach(); - subscribe_to_binary_statuses(&languages, cx).detach(); - let _maintain_workspace_config = { - let (sender, receiver) = watch::channel(); - (Self::maintain_workspace_config(receiver, cx), sender) - }; - Self { - mode: LspStoreMode::Remote(RemoteLspStore { - upstream_client: Some(upstream_client), - upstream_project_id: project_id, - }), - downstream_client: None, - last_formatting_failure: None, - buffer_store, - worktree_store, - languages: languages.clone(), - language_server_statuses: Default::default(), - nonce: StdRng::from_os_rng().random(), - diagnostic_summaries: HashMap::default(), - lsp_server_capabilities: HashMap::default(), - next_hint_id: Arc::default(), - lsp_data: HashMap::default(), - active_entry: None, - - _maintain_workspace_config, - _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), - } - } - - fn on_buffer_store_event( - &mut self, - _: Entity, - event: &BufferStoreEvent, - cx: &mut Context, - ) { - match event { - BufferStoreEvent::BufferAdded(buffer) => { - self.on_buffer_added(buffer, cx).log_err(); - } - 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 local.registered_buffers.contains_key(&buffer_id) { - local.unregister_old_buffer_from_language_servers(buffer, old_file, cx); - } - } - - self.detect_language_for_buffer(buffer, cx); - if let Some(local) = self.as_local_mut() { - local.initialize_buffer(buffer, cx); - if local.registered_buffers.contains_key(&buffer_id) { - local.register_buffer_with_language_servers(buffer, HashSet::default(), cx); - } - } - } - _ => {} - } - } - - fn on_worktree_store_event( - &mut self, - _: Entity, - event: &WorktreeStoreEvent, - cx: &mut Context, - ) { - match event { - WorktreeStoreEvent::WorktreeAdded(worktree) => { - if !worktree.read(cx).is_local() { - return; - } - cx.subscribe(worktree, |this, worktree, event, cx| match event { - worktree::Event::UpdatedEntries(changes) => { - this.update_local_worktree_language_servers(&worktree, changes, cx); - } - worktree::Event::UpdatedGitRepositories(_) - | worktree::Event::DeletedEntry(_) => {} - }) - .detach() - } - WorktreeStoreEvent::WorktreeRemoved(_, id) => self.remove_worktree(*id, cx), - WorktreeStoreEvent::WorktreeUpdateSent(worktree) => { - worktree.update(cx, |worktree, _cx| self.send_diagnostic_summaries(worktree)); - } - WorktreeStoreEvent::WorktreeReleased(..) - | WorktreeStoreEvent::WorktreeOrderChanged - | WorktreeStoreEvent::WorktreeUpdatedEntries(..) - | WorktreeStoreEvent::WorktreeUpdatedGitRepositories(..) - | WorktreeStoreEvent::WorktreeDeletedEntry(..) => {} - } - } - - fn on_prettier_store_event( - &mut self, - _: Entity, - event: &PrettierStoreEvent, - cx: &mut Context, - ) { - match event { - PrettierStoreEvent::LanguageServerRemoved(prettier_server_id) => { - self.unregister_supplementary_language_server(*prettier_server_id, cx); - } - PrettierStoreEvent::LanguageServerAdded { - new_server_id, - name, - prettier_server, - } => { - self.register_supplementary_language_server( - *new_server_id, - name.clone(), - prettier_server.clone(), - cx, - ); - } - } - } - - fn on_toolchain_store_event( - &mut self, - _: Entity, - event: &ToolchainStoreEvent, - _: &mut Context, - ) { - if let ToolchainStoreEvent::ToolchainActivated = event { - self.request_workspace_config_refresh() - } - } - - fn request_workspace_config_refresh(&mut self) { - *self._maintain_workspace_config.1.borrow_mut() = (); - } - - pub fn prettier_store(&self) -> Option> { - self.as_local().map(|local| local.prettier_store.clone()) - } - - fn on_buffer_event( - &mut self, - buffer: Entity, - event: &language::BufferEvent, - cx: &mut Context, - ) { - match event { - language::BufferEvent::Edited => { - self.on_buffer_edited(buffer, cx); - } - - language::BufferEvent::Saved => { - self.on_buffer_saved(buffer, cx); - } - - _ => {} - } - } - - fn on_buffer_added(&mut self, buffer: &Entity, cx: &mut Context) -> Result<()> { - buffer - .read(cx) - .set_language_registry(self.languages.clone()); - - cx.subscribe(buffer, |this, buffer, event, cx| { - this.on_buffer_event(buffer, event, cx); - }) - .detach(); - - self.detect_language_for_buffer(buffer, cx); - if let Some(local) = self.as_local_mut() { - local.initialize_buffer(buffer, cx); - } - - Ok(()) - } - - pub(crate) fn register_buffer_with_language_servers( - &mut self, - buffer: &Entity, - only_register_servers: HashSet, - ignore_refcounts: bool, - cx: &mut Context, - ) -> OpenLspBufferHandle { - let buffer_id = buffer.read(cx).remote_id(); - let handle = OpenLspBufferHandle(cx.new(|_| OpenLspBuffer(buffer.clone()))); - if let Some(local) = self.as_local_mut() { - let refcount = local.registered_buffers.entry(buffer_id).or_insert(0); - if !ignore_refcounts { - *refcount += 1; - } - - // We run early exits on non-existing buffers AFTER we mark the buffer as registered in order to handle buffer saving. - // When a new unnamed buffer is created and saved, we will start loading it's language. Once the language is loaded, we go over all "language-less" buffers and try to fit that new language - // with them. However, we do that only for the buffers that we think are open in at least one editor; thus, we need to keep tab of unnamed buffers as well, even though they're not actually registered with any language - // servers in practice (we don't support non-file URI schemes in our LSP impl). - let Some(file) = File::from_dyn(buffer.read(cx).file()) else { - return handle; - }; - if !file.is_local() { - return handle; - } - - if ignore_refcounts || *refcount == 1 { - local.register_buffer_with_language_servers(buffer, only_register_servers, cx); - } - if !ignore_refcounts { - cx.observe_release(&handle.0, move |lsp_store, buffer, cx| { - let refcount = { - let local = lsp_store.as_local_mut().unwrap(); - let Some(refcount) = local.registered_buffers.get_mut(&buffer_id) else { - debug_panic!("bad refcounting"); - return; - }; - - *refcount -= 1; - *refcount - }; - if refcount == 0 { - lsp_store.lsp_data.remove(&buffer_id); - let local = lsp_store.as_local_mut().unwrap(); - local.registered_buffers.remove(&buffer_id); - - local.buffers_opened_in_servers.remove(&buffer_id); - if let Some(file) = File::from_dyn(buffer.0.read(cx).file()).cloned() { - local.unregister_old_buffer_from_language_servers(&buffer.0, &file, cx); - - let buffer_abs_path = file.abs_path(cx); - for (_, buffer_pull_diagnostics_result_ids) in - &mut local.buffer_pull_diagnostics_result_ids - { - buffer_pull_diagnostics_result_ids.retain( - |_, buffer_result_ids| { - buffer_result_ids.remove(&buffer_abs_path); - !buffer_result_ids.is_empty() - }, - ); - } - - let diagnostic_updates = local - .language_servers - .keys() - .cloned() - .map(|server_id| DocumentDiagnosticsUpdate { - diagnostics: DocumentDiagnostics { - document_abs_path: buffer_abs_path.clone(), - version: None, - diagnostics: Vec::new(), - }, - result_id: None, - registration_id: None, - server_id: server_id, - disk_based_sources: Cow::Borrowed(&[]), - }) - .collect::>(); - - lsp_store - .merge_diagnostic_entries( - diagnostic_updates, - |_, diagnostic, _| { - diagnostic.source_kind != DiagnosticSourceKind::Pulled - }, - cx, - ) - .context("Clearing diagnostics for the closed buffer") - .log_err(); - } - } - }) - .detach(); - } - } else if let Some((upstream_client, upstream_project_id)) = self.upstream_client() { - let buffer_id = buffer.read(cx).remote_id().to_proto(); - cx.background_spawn(async move { - upstream_client - .request(proto::RegisterBufferWithLanguageServers { - project_id: upstream_project_id, - buffer_id, - only_servers: only_register_servers - .into_iter() - .map(|selector| { - let selector = match selector { - LanguageServerSelector::Id(language_server_id) => { - proto::language_server_selector::Selector::ServerId( - language_server_id.to_proto(), - ) - } - LanguageServerSelector::Name(language_server_name) => { - proto::language_server_selector::Selector::Name( - language_server_name.to_string(), - ) - } - }; - proto::LanguageServerSelector { - selector: Some(selector), - } - }) - .collect(), - }) - .await - }) - .detach(); - } else { - // Our remote connection got closed - } - handle - } - - fn maintain_buffer_languages( - languages: Arc, - cx: &mut Context, - ) -> Task<()> { - let mut subscription = languages.subscribe(); - let mut prev_reload_count = languages.reload_count(); - cx.spawn(async move |this, cx| { - while let Some(()) = subscription.next().await { - if let Some(this) = this.upgrade() { - // If the language registry has been reloaded, then remove and - // re-assign the languages on all open buffers. - let reload_count = languages.reload_count(); - if reload_count > prev_reload_count { - prev_reload_count = reload_count; - this.update(cx, |this, cx| { - this.buffer_store.clone().update(cx, |buffer_store, cx| { - for buffer in buffer_store.buffers() { - if let Some(f) = File::from_dyn(buffer.read(cx).file()).cloned() - { - buffer.update(cx, |buffer, cx| { - buffer.set_language_async(None, cx) - }); - if let Some(local) = this.as_local_mut() { - local.reset_buffer(&buffer, &f, cx); - - 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, - ); - } - } - } - } - }); - }) - .ok(); - } - - this.update(cx, |this, cx| { - let mut plain_text_buffers = Vec::new(); - let mut buffers_with_unknown_injections = Vec::new(); - for handle in this.buffer_store.read(cx).buffers() { - let buffer = handle.read(cx); - if buffer.language().is_none() - || buffer.language() == Some(&*language::PLAIN_TEXT) - { - plain_text_buffers.push(handle); - } else if buffer.contains_unknown_injections() { - buffers_with_unknown_injections.push(handle); - } - } - - // Deprioritize the invisible worktrees so main worktrees' language servers can be started first, - // and reused later in the invisible worktrees. - plain_text_buffers.sort_by_key(|buffer| { - Reverse( - File::from_dyn(buffer.read(cx).file()) - .map(|file| file.worktree.read(cx).is_visible()), - ) - }); - - for buffer in plain_text_buffers { - this.detect_language_for_buffer(&buffer, cx); - if let Some(local) = this.as_local_mut() { - local.initialize_buffer(&buffer, cx); - if local - .registered_buffers - .contains_key(&buffer.read(cx).remote_id()) - { - local.register_buffer_with_language_servers( - &buffer, - HashSet::default(), - cx, - ); - } - } - } - - for buffer in buffers_with_unknown_injections { - buffer.update(cx, |buffer, cx| buffer.reparse(cx, false)); - } - }) - .ok(); - } - } - }) - } - - fn detect_language_for_buffer( - &mut self, - buffer_handle: &Entity, - cx: &mut Context, - ) -> Option { - // If the buffer has a language, set it and start the language server if we haven't already. - let buffer = buffer_handle.read(cx); - let file = buffer.file()?; - - let content = buffer.as_rope(); - let available_language = self.languages.language_for_file(file, Some(content), cx); - if let Some(available_language) = &available_language { - if let Some(Ok(Ok(new_language))) = self - .languages - .load_language(available_language) - .now_or_never() - { - self.set_language_for_buffer(buffer_handle, new_language, cx); - } - } else { - cx.emit(LspStoreEvent::LanguageDetected { - buffer: buffer_handle.clone(), - new_language: None, - }); - } - - available_language - } - - pub(crate) fn set_language_for_buffer( - &mut self, - buffer_entity: &Entity, - new_language: Arc, - cx: &mut Context, - ) { - 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); - } - buffer_entity.update(cx, |buffer, cx| { - if buffer - .language() - .is_none_or(|old_language| !Arc::ptr_eq(old_language, &new_language)) - { - buffer.set_language_async(Some(new_language.clone()), cx); - } - }); - - let settings = - language_settings(Some(new_language.name()), buffer_file.as_ref(), cx).into_owned(); - let buffer_file = File::from_dyn(buffer_file.as_ref()); - - 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); - } - 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, - ) - }) - } - } - - cx.emit(LspStoreEvent::LanguageDetected { - buffer: buffer_entity.clone(), - new_language: Some(new_language), - }) - } - - pub fn buffer_store(&self) -> Entity { - self.buffer_store.clone() - } - - pub fn set_active_entry(&mut self, active_entry: Option) { - self.active_entry = active_entry; - } - - 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.as_ref())) - }); - 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(); - } - } - } - - fn is_capable_for_proto_request( - &self, - buffer: &Entity, - request: &R, - cx: &App, - ) -> 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: &App, - ) -> bool - where - F: FnMut(&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) - } - - fn all_capable_for_proto_request( - &self, - buffer: &Entity, - mut check: F, - cx: &App, - ) -> Vec - where - F: FnMut(&lsp::LanguageServerName, &lsp::ServerCapabilities) -> bool, - { - let Some(language) = buffer.read(cx).language().cloned() else { - return Vec::default(); - }; - 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, &server_status.name)) - }) - .filter_map(|(server_id, server_name)| { - self.lsp_server_capabilities - .get(server_id) - .map(|c| (server_id, server_name, c)) - }) - .filter(|(_, server_name, capabilities)| check(server_name, capabilities)) - .map(|(server_id, _, _)| *server_id) - .collect() - } - - pub fn request_lsp( - &mut self, - buffer: 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, - upstream_client, - upstream_project_id, - request, - cx, - ); - } - - let Some(language_server) = buffer.update(cx, |buffer, cx| match server { - LanguageServerToQuery::FirstCapable => self.as_local().and_then(|local| { - local - .language_servers_for_buffer(buffer, cx) - .find(|(_, server)| { - request.check_capabilities(server.adapter_server_capabilities()) - }) - .map(|(_, server)| server.clone()) - }), - LanguageServerToQuery::Other(id) => self - .language_server_for_local_buffer(buffer, id, cx) - .and_then(|(_, server)| { - request - .check_capabilities(server.adapter_server_capabilities()) - .then(|| Arc::clone(server)) - }), - }) else { - return Task::ready(Ok(Default::default())); - }; - - let file = File::from_dyn(buffer.read(cx).file()).and_then(File::as_local); - - let Some(file) = file else { - return Task::ready(Ok(Default::default())); - }; - - let lsp_params = match request.to_lsp_params_or_response( - &file.abs_path(cx), - buffer.read(cx), - &language_server, - cx, - ) { - Ok(LspParamsOrResponse::Params(lsp_params)) => lsp_params, - Ok(LspParamsOrResponse::Response(response)) => return Task::ready(Ok(response)), - Err(err) => { - let message = format!( - "{} via {} failed: {}", - request.display_name(), - language_server.name(), - err - ); - // rust-analyzer likes to error with this when its still loading up - if !message.ends_with("content modified") { - log::warn!("{message}"); - } - return Task::ready(Err(anyhow!(message))); - } - }; - - let status = request.status(); - if !request.check_capabilities(language_server.adapter_server_capabilities()) { - return Task::ready(Ok(Default::default())); - } - cx.spawn(async move |this, cx| { - let lsp_request = language_server.request::(lsp_params); - - let id = lsp_request.id(); - let _cleanup = if status.is_some() { - cx.update(|cx| { - this.update(cx, |this, cx| { - this.on_lsp_work_start( - language_server.server_id(), - ProgressToken::Number(id), - LanguageServerProgress { - is_disk_based_diagnostics_progress: false, - is_cancellable: false, - title: None, - message: status.clone(), - percentage: None, - last_update_at: cx.background_executor().now(), - }, - cx, - ); - }) - }) - .log_err(); - - Some(defer(|| { - cx.update(|cx| { - this.update(cx, |this, cx| { - this.on_lsp_work_end( - language_server.server_id(), - ProgressToken::Number(id), - cx, - ); - }) - }) - .log_err(); - })) - } else { - None - }; - - let result = lsp_request.await.into_response(); - - let response = result.map_err(|err| { - let message = format!( - "{} via {} failed: {}", - request.display_name(), - language_server.name(), - err - ); - // rust-analyzer likes to error with this when its still loading up - if !message.ends_with("content modified") { - log::warn!("{message}"); - } - anyhow::anyhow!(message) - })?; - - request - .response_from_lsp( - response, - this.upgrade().context("no app context")?, - buffer, - language_server.server_id(), - cx.clone(), - ) - .await - }) - } - - fn on_settings_changed(&mut self, cx: &mut Context) { - let mut language_formatters_to_check = Vec::new(); - for buffer in self.buffer_store.read(cx).buffers() { - let buffer = buffer.read(cx); - let buffer_file = File::from_dyn(buffer.file()); - let buffer_language = buffer.language(); - let settings = language_settings(buffer_language.map(|l| l.name()), buffer.file(), cx); - if buffer_language.is_some() { - language_formatters_to_check.push(( - buffer_file.map(|f| f.worktree_id(cx)), - settings.into_owned(), - )); - } - } - - self.request_workspace_config_refresh(); - - if let Some(prettier_store) = self.as_local().map(|s| s.prettier_store.clone()) { - prettier_store.update(cx, |prettier_store, cx| { - prettier_store.on_settings_changed(language_formatters_to_check, cx) - }) - } - - cx.notify(); - } - - 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, - )) - } - }; - - 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 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((_, 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 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 = Uri::from_file_path(worktree.read(cx).absolutize(&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( - &worktree, - lsp_delegate.clone(), - disposition, - &language.name(), - cx, - ); - if let Some(state) = local.language_servers.get(&server_id) - && 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() - .into_owned(), - buffer_id: buffer_id.to_proto(), - }, - ), - }); - } - } - } else { - continue; - } - } - 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(); - } - } - - pub fn apply_code_action( - &self, - buffer_handle: Entity, - mut action: CodeAction, - push_to_history: bool, - cx: &mut Context, - ) -> Task> { - if let Some((upstream_client, project_id)) = self.upstream_client() { - let request = proto::ApplyCodeAction { - project_id, - buffer_id: buffer_handle.read(cx).remote_id().into(), - action: Some(Self::serialize_code_action(&action)), - }; - let buffer_store = self.buffer_store(); - cx.spawn(async move |_, cx| { - let response = upstream_client - .request(request) - .await? - .transaction - .context("missing transaction")?; - - buffer_store - .update(cx, |buffer_store, cx| { - buffer_store.deserialize_project_transaction(response, push_to_history, cx) - })? - .await - }) - } else if self.mode.is_local() { - let Some((_, 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 { - return Task::ready(Ok(ProjectTransaction::default())); - }; - cx.spawn(async move |this, cx| { - 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()) { - return LocalLspStore::deserialize_workspace_edit( - this.upgrade().context("no app present")?, - edit.clone(), - push_to_history, - - lang_server.clone(), - cx, - ) - .await; - } - - if let Some(command) = action.lsp_action.command() { - let server_capabilities = lang_server.capabilities(); - let available_commands = server_capabilities - .execute_command_provider - .as_ref() - .map(|options| options.commands.as_slice()) - .unwrap_or_default(); - if available_commands.contains(&command.command) { - this.update(cx, |this, _| { - this.as_local_mut() - .unwrap() - .last_workspace_edits_by_language_server - .remove(&lang_server.server_id()); - })?; - - let _result = lang_server - .request::(lsp::ExecuteCommandParams { - command: command.command.clone(), - arguments: command.arguments.clone().unwrap_or_default(), - ..lsp::ExecuteCommandParams::default() - }) - .await.into_response() - .context("execute command")?; - - return this.update(cx, |this, _| { - this.as_local_mut() - .unwrap() - .last_workspace_edits_by_language_server - .remove(&lang_server.server_id()) - .unwrap_or_default() - }); - } else { - log::warn!("Cannot execute a command {} not listed in the language server capabilities", command.command); - } - } - - Ok(ProjectTransaction::default()) - }) - } else { - Task::ready(Err(anyhow!("no upstream client and not local"))) - } - } - - pub fn apply_code_action_kind( - &mut self, - buffers: HashSet>, - kind: CodeActionKind, - push_to_history: bool, - cx: &mut Context, - ) -> Task> { - if self.as_local().is_some() { - cx.spawn(async move |lsp_store, cx| { - let buffers = buffers.into_iter().collect::>(); - let result = LocalLspStore::execute_code_action_kind_locally( - lsp_store.clone(), - buffers, - kind, - push_to_history, - cx, - ) - .await; - lsp_store.update(cx, |lsp_store, _| { - lsp_store.update_last_formatting_failure(&result); - })?; - result - }) - } else if let Some((client, project_id)) = self.upstream_client() { - let buffer_store = self.buffer_store(); - cx.spawn(async move |lsp_store, cx| { - let result = client - .request(proto::ApplyCodeActionKind { - project_id, - kind: kind.as_str().to_owned(), - buffer_ids: buffers - .iter() - .map(|buffer| { - buffer.read_with(cx, |buffer, _| buffer.remote_id().into()) - }) - .collect::>()?, - }) - .await - .and_then(|result| result.transaction.context("missing transaction")); - lsp_store.update(cx, |lsp_store, _| { - lsp_store.update_last_formatting_failure(&result); - })?; - - let transaction_response = result?; - buffer_store - .update(cx, |buffer_store, cx| { - buffer_store.deserialize_project_transaction( - transaction_response, - push_to_history, - cx, - ) - })? - .await - }) - } else { - Task::ready(Ok(ProjectTransaction::default())) - } - } - - pub fn resolved_hint( - &mut self, - buffer_id: BufferId, - id: InlayId, - cx: &mut Context, - ) -> Option { - let buffer = self.buffer_store.read(cx).get(buffer_id)?; - - let lsp_data = self.lsp_data.get_mut(&buffer_id)?; - let buffer_lsp_hints = &mut lsp_data.inlay_hints; - let hint = buffer_lsp_hints.hint_for_id(id)?.clone(); - let (server_id, resolve_data) = match &hint.resolve_state { - ResolveState::Resolved => return Some(ResolvedHint::Resolved(hint)), - ResolveState::Resolving => { - return Some(ResolvedHint::Resolving( - buffer_lsp_hints.hint_resolves.get(&id)?.clone(), - )); - } - ResolveState::CanResolve(server_id, resolve_data) => (*server_id, resolve_data.clone()), - }; - - let resolve_task = self.resolve_inlay_hint(hint, buffer, server_id, cx); - let buffer_lsp_hints = &mut self.lsp_data.get_mut(&buffer_id)?.inlay_hints; - let previous_task = buffer_lsp_hints.hint_resolves.insert( - id, - cx.spawn(async move |lsp_store, cx| { - let resolved_hint = resolve_task.await; - lsp_store - .update(cx, |lsp_store, _| { - if let Some(old_inlay_hint) = lsp_store - .lsp_data - .get_mut(&buffer_id) - .and_then(|buffer_lsp_data| buffer_lsp_data.inlay_hints.hint_for_id(id)) - { - match resolved_hint { - Ok(resolved_hint) => { - *old_inlay_hint = resolved_hint; - } - Err(e) => { - old_inlay_hint.resolve_state = - ResolveState::CanResolve(server_id, resolve_data); - log::error!("Inlay hint resolve failed: {e:#}"); - } - } - } - }) - .ok(); - }) - .shared(), - ); - debug_assert!( - previous_task.is_none(), - "Did not change hint's resolve state after spawning its resolve" - ); - buffer_lsp_hints.hint_for_id(id)?.resolve_state = ResolveState::Resolving; - None - } - - fn resolve_inlay_hint( - &self, - mut hint: InlayHint, - buffer: 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(), - language_server_id: server_id.0 as u64, - hint: Some(InlayHints::project_to_proto_hint(hint.clone())), - }; - cx.background_spawn(async move { - let response = upstream_client - .request(request) - .await - .context("inlay hints proto request")?; - match response.hint { - Some(resolved_hint) => InlayHints::proto_to_project_hint(resolved_hint) - .context("inlay hints proto resolve response conversion"), - None => Ok(hint), - } - }) - } else { - let Some(lang_server) = buffer.update(cx, |buffer, cx| { - self.language_server_for_local_buffer(buffer, server_id, cx) - .map(|(_, server)| server.clone()) - }) else { - return Task::ready(Ok(hint)); - }; - if !InlayHints::can_resolve_inlays(&lang_server.capabilities()) { - return Task::ready(Ok(hint)); - } - let buffer_snapshot = buffer.read(cx).snapshot(); - cx.spawn(async move |_, cx| { - let resolve_task = lang_server.request::( - InlayHints::project_to_lsp_hint(hint, &buffer_snapshot), - ); - let resolved_hint = resolve_task - .await - .into_response() - .context("inlay hint resolve LSP request")?; - let resolved_hint = InlayHints::lsp_to_project_hint( - resolved_hint, - &buffer, - server_id, - ResolveState::Resolved, - false, - cx, - ) - .await?; - Ok(resolved_hint) - }) - } - } - - pub fn resolve_color_presentation( - &mut self, - mut color: DocumentColor, - buffer: Entity, - server_id: LanguageServerId, - cx: &mut Context, - ) -> Task> { - if color.resolved { - return Task::ready(Ok(color)); - } - - if let Some((upstream_client, project_id)) = self.upstream_client() { - let start = color.lsp_range.start; - let end = color.lsp_range.end; - let request = proto::GetColorPresentation { - project_id, - server_id: server_id.to_proto(), - buffer_id: buffer.read(cx).remote_id().into(), - color: Some(proto::ColorInformation { - red: color.color.red, - green: color.color.green, - blue: color.color.blue, - alpha: color.color.alpha, - lsp_range_start: Some(proto::PointUtf16 { - row: start.line, - column: start.character, - }), - lsp_range_end: Some(proto::PointUtf16 { - row: end.line, - column: end.character, - }), - }), - }; - cx.background_spawn(async move { - let response = upstream_client - .request(request) - .await - .context("color presentation proto request")?; - color.resolved = true; - color.color_presentations = response - .presentations - .into_iter() - .map(|presentation| ColorPresentation { - label: SharedString::from(presentation.label), - text_edit: presentation.text_edit.and_then(deserialize_lsp_edit), - additional_text_edits: presentation - .additional_text_edits - .into_iter() - .filter_map(deserialize_lsp_edit) - .collect(), - }) - .collect(); - Ok(color) - }) - } else { - let path = match buffer - .update(cx, |buffer, cx| { - Some(File::from_dyn(buffer.file())?.abs_path(cx)) - }) - .context("buffer with the missing path") - { - Ok(path) => path, - Err(e) => return Task::ready(Err(e)), - }; - let Some(lang_server) = buffer.update(cx, |buffer, cx| { - self.language_server_for_local_buffer(buffer, server_id, cx) - .map(|(_, server)| server.clone()) - }) else { - return Task::ready(Ok(color)); - }; - cx.background_spawn(async move { - let resolve_task = lang_server.request::( - lsp::ColorPresentationParams { - text_document: make_text_document_identifier(&path)?, - color: color.color, - range: color.lsp_range, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }, - ); - color.color_presentations = resolve_task - .await - .into_response() - .context("color presentation resolve LSP request")? - .into_iter() - .map(|presentation| ColorPresentation { - label: SharedString::from(presentation.label), - text_edit: presentation.text_edit, - additional_text_edits: presentation - .additional_text_edits - .unwrap_or_default(), - }) - .collect(); - color.resolved = true; - Ok(color) - }) - } - } - - pub(crate) fn linked_edits( - &mut self, - buffer: &Entity, - position: Anchor, - cx: &mut Context, - ) -> Task>>> { - let snapshot = buffer.read(cx).snapshot(); - let scope = snapshot.language_scope_at(position); - let Some(server_id) = self - .as_local() - .and_then(|local| { - buffer.update(cx, |buffer, cx| { - local - .language_servers_for_buffer(buffer, cx) - .filter(|(_, server)| { - LinkedEditingRange::check_server_capabilities(server.capabilities()) - }) - .filter(|(adapter, _)| { - scope - .as_ref() - .map(|scope| scope.language_allowed(&adapter.name)) - .unwrap_or(true) - }) - .map(|(_, server)| LanguageServerToQuery::Other(server.server_id())) - .next() - }) - }) - .or_else(|| { - self.upstream_client() - .is_some() - .then_some(LanguageServerToQuery::FirstCapable) - }) - .filter(|_| { - maybe!({ - let language = buffer.read(cx).language_at(position)?; - Some( - language_settings(Some(language.name()), buffer.read(cx).file(), cx) - .linked_edits, - ) - }) == Some(true) - }) - else { - return Task::ready(Ok(Vec::new())); - }; - - self.request_lsp( - buffer.clone(), - server_id, - LinkedEditingRange { position }, - cx, - ) - } - - fn apply_on_type_formatting( - &mut self, - buffer: Entity, - position: Anchor, - trigger: String, - 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(), - position: Some(serialize_anchor(&position)), - trigger, - version: serialize_version(&buffer.read(cx).version()), - }; - cx.background_spawn(async move { - client - .request(request) - .await? - .transaction - .map(language::proto::deserialize_transaction) - .transpose() - }) - } else if let Some(local) = self.as_local_mut() { - let buffer_id = buffer.read(cx).remote_id(); - local.buffers_being_formatted.insert(buffer_id); - cx.spawn(async move |this, cx| { - let _cleanup = defer({ - let this = this.clone(); - let mut cx = cx.clone(); - move || { - this.update(&mut cx, |this, _| { - if let Some(local) = this.as_local_mut() { - local.buffers_being_formatted.remove(&buffer_id); - } - }) - .ok(); - } - }); - - buffer - .update(cx, |buffer, _| { - buffer.wait_for_edits(Some(position.timestamp)) - })? - .await?; - this.update(cx, |this, cx| { - let position = position.to_point_utf16(buffer.read(cx)); - this.on_type_format(buffer, position, trigger, false, cx) - })? - .await - }) - } else { - Task::ready(Err(anyhow!("No upstream client or local language server"))) - } - } - - pub fn on_type_format( - &mut self, - buffer: Entity, - position: T, - trigger: String, - push_to_history: bool, - cx: &mut Context, - ) -> Task>> { - let position = position.to_point_utf16(buffer.read(cx)); - self.on_type_format_impl(buffer, position, trigger, push_to_history, cx) - } - - fn on_type_format_impl( - &mut self, - buffer: Entity, - position: PointUtf16, - trigger: String, - push_to_history: bool, - cx: &mut Context, - ) -> Task>> { - let options = buffer.update(cx, |buffer, cx| { - lsp_command::lsp_formatting_options( - language_settings( - buffer.language_at(position).map(|l| l.name()), - buffer.file(), - cx, - ) - .as_ref(), - ) - }); - - cx.spawn(async move |this, cx| { - if let Some(waiter) = - buffer.update(cx, |buffer, _| buffer.wait_for_autoindent_applied())? - { - waiter.await?; - } - cx.update(|cx| { - this.update(cx, |this, cx| { - this.request_lsp( - buffer.clone(), - LanguageServerToQuery::FirstCapable, - OnTypeFormatting { - position, - trigger, - options, - push_to_history, - }, - cx, - ) - }) - })?? - .await - }) - } - - pub fn definitions( - &mut self, - buffer: &Entity, - position: PointUtf16, - cx: &mut Context, - ) -> 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( - project_id, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.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); - }; - 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, - lsp_store.clone(), - buffer.clone(), - cx.clone(), - ) - })) - .await; - - Ok(Some( - actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect(), - )) - }) - } else { - let definitions_task = self.request_multiple_lsp_locally( - buffer, - Some(position), - GetDefinitions { position }, - cx, - ); - cx.background_spawn(async move { - Ok(Some( - definitions_task - .await - .into_iter() - .flat_map(|(_, definitions)| definitions) - .dedup() - .collect(), - )) - }) - } - } - - pub fn declarations( - &mut self, - buffer: &Entity, - position: PointUtf16, - cx: &mut Context, - ) -> 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( - project_id, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.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); - }; - 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, - lsp_store.clone(), - buffer.clone(), - cx.clone(), - ) - })) - .await; - - Ok(Some( - actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect(), - )) - }) - } else { - let declarations_task = self.request_multiple_lsp_locally( - buffer, - Some(position), - GetDeclarations { position }, - cx, - ); - cx.background_spawn(async move { - Ok(Some( - declarations_task - .await - .into_iter() - .flat_map(|(_, declarations)| declarations) - .dedup() - .collect(), - )) - }) - } - } - - pub fn type_definitions( - &mut self, - buffer: &Entity, - position: PointUtf16, - cx: &mut Context, - ) -> 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( - project_id, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.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); - }; - 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, - lsp_store.clone(), - buffer.clone(), - cx.clone(), - ) - })) - .await; - - Ok(Some( - actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect(), - )) - }) - } else { - let type_definitions_task = self.request_multiple_lsp_locally( - buffer, - 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(), - )) - }) - } - } - - pub fn implementations( - &mut self, - buffer: &Entity, - position: PointUtf16, - cx: &mut Context, - ) -> 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( - project_id, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.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); - }; - 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, - lsp_store.clone(), - buffer.clone(), - cx.clone(), - ) - })) - .await; - - Ok(Some( - actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect(), - )) - }) - } else { - let implementations_task = self.request_multiple_lsp_locally( - buffer, - Some(position), - GetImplementations { position }, - cx, - ); - cx.background_spawn(async move { - Ok(Some( - implementations_task - .await - .into_iter() - .flat_map(|(_, implementations)| implementations) - .dedup() - .collect(), - )) - }) - } - } - - pub fn references( - &mut self, - buffer: &Entity, - position: PointUtf16, - cx: &mut Context, - ) -> 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( - project_id, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.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); - }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - - let locations = join_all(responses.payload.into_iter().map(|lsp_response| { - GetReferences { position }.response_from_proto( - lsp_response.response, - lsp_store.clone(), - buffer.clone(), - cx.clone(), - ) - })) - .await - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect(); - Ok(Some(locations)) - }) - } else { - let references_task = self.request_multiple_lsp_locally( - buffer, - Some(position), - GetReferences { position }, - cx, - ); - cx.background_spawn(async move { - Ok(Some( - references_task - .await - .into_iter() - .flat_map(|(_, references)| references) - .dedup() - .collect(), - )) - }) - } - } - - pub fn code_actions( - &mut self, - buffer: &Entity, - range: Range, - kinds: Option>, - cx: &mut Context, - ) -> 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( - project_id, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.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); - }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - let actions = join_all(responses.payload.into_iter().map(|response| { - GetCodeActions { - range: range.clone(), - kinds: kinds.clone(), - } - .response_from_proto( - response.response, - lsp_store.clone(), - buffer.clone(), - cx.clone(), - ) - })) - .await; - - Ok(Some( - actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .collect(), - )) - }) - } else { - let all_actions_task = self.request_multiple_lsp_locally( - buffer, - Some(range.start), - GetCodeActions { range, kinds }, - cx, - ); - cx.background_spawn(async move { - Ok(Some( - all_actions_task - .await - .into_iter() - .flat_map(|(_, actions)| actions) - .collect(), - )) - }) - } - } - - pub fn code_lens_actions( - &mut self, - buffer: &Entity, - cx: &mut Context, - ) -> CodeLensTask { - let version_queried_for = buffer.read(cx).version(); - let buffer_id = buffer.read(cx).remote_id(); - let existing_servers = self.as_local().map(|local| { - local - .buffers_opened_in_servers - .get(&buffer_id) - .cloned() - .unwrap_or_default() - }); - - if let Some(lsp_data) = self.current_lsp_data(buffer_id) { - if let Some(cached_lens) = &lsp_data.code_lens { - if !version_queried_for.changed_since(&lsp_data.buffer_version) { - let has_different_servers = existing_servers.is_some_and(|existing_servers| { - existing_servers != cached_lens.lens.keys().copied().collect() - }); - if !has_different_servers { - return Task::ready(Ok(Some( - cached_lens.lens.values().flatten().cloned().collect(), - ))) - .shared(); - } - } else if let Some((updating_for, running_update)) = cached_lens.update.as_ref() { - if !version_queried_for.changed_since(updating_for) { - return running_update.clone(); - } - } - } - } - - let lens_lsp_data = self - .latest_lsp_data(buffer, cx) - .code_lens - .get_or_insert_default(); - let buffer = buffer.clone(); - let query_version_queried_for = version_queried_for.clone(); - let new_task = cx - .spawn(async move |lsp_store, cx| { - cx.background_executor() - .timer(Duration::from_millis(30)) - .await; - let fetched_lens = lsp_store - .update(cx, |lsp_store, cx| lsp_store.fetch_code_lens(&buffer, cx)) - .map_err(Arc::new)? - .await - .context("fetching code lens") - .map_err(Arc::new); - let fetched_lens = match fetched_lens { - Ok(fetched_lens) => fetched_lens, - Err(e) => { - lsp_store - .update(cx, |lsp_store, _| { - if let Some(lens_lsp_data) = lsp_store - .lsp_data - .get_mut(&buffer_id) - .and_then(|lsp_data| lsp_data.code_lens.as_mut()) - { - lens_lsp_data.update = None; - } - }) - .ok(); - return Err(e); - } - }; - - lsp_store - .update(cx, |lsp_store, _| { - let lsp_data = lsp_store.current_lsp_data(buffer_id)?; - let code_lens = lsp_data.code_lens.as_mut()?; - if let Some(fetched_lens) = fetched_lens { - if lsp_data.buffer_version == query_version_queried_for { - code_lens.lens.extend(fetched_lens); - } else if !lsp_data - .buffer_version - .changed_since(&query_version_queried_for) - { - lsp_data.buffer_version = query_version_queried_for; - code_lens.lens = fetched_lens; - } - } - code_lens.update = None; - Some(code_lens.lens.values().flatten().cloned().collect()) - }) - .map_err(Arc::new) - }) - .shared(); - lens_lsp_data.update = Some((version_queried_for, new_task.clone())); - new_task - } - - fn fetch_code_lens( - &mut self, - buffer: &Entity, - cx: &mut Context, - ) -> 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( - project_id, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.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); - }; - 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, - ) - } - })) - .await; - - let mut has_errors = false; - let code_lens_actions = code_lens_actions - .into_iter() - .filter_map(|(server_id, code_lens)| match code_lens { - Ok(code_lens) => Some((server_id, code_lens)), - Err(e) => { - has_errors = true; - log::error!("{e:#}"); - None - } - }) - .collect::>(); - anyhow::ensure!( - !has_errors || !code_lens_actions.is_empty(), - "Failed to fetch code lens" - ); - Ok(Some(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())) - }) - } - } - - #[inline(never)] - pub fn completions( - &self, - buffer: &Entity, - position: PointUtf16, - context: CompletionContext, - cx: &mut Context, - ) -> Task>> { - let language_registry = self.languages.clone(); - - if let Some((upstream_client, project_id)) = self.upstream_client() { - let snapshot = buffer.read(cx).snapshot(); - let offset = position.to_offset(&snapshot); - let scope = snapshot.language_scope_at(offset); - let capable_lsps = self.all_capable_for_proto_request( - buffer, - |server_name, capabilities| { - capabilities.completion_provider.is_some() - && scope - .as_ref() - .map(|scope| scope.language_allowed(server_name)) - .unwrap_or(true) - }, - cx, - ); - if capable_lsps.is_empty() { - return Task::ready(Ok(Vec::new())); - } - - let language = buffer.read(cx).language().cloned(); - - // In the future, we should provide project guests with the names of LSP adapters, - // so that they can use the correct LSP adapter when computing labels. For now, - // guests just use the first LSP adapter associated with the buffer's language. - let lsp_adapter = language.as_ref().and_then(|language| { - language_registry - .lsp_adapters(&language.name()) - .first() - .cloned() - }); - - let buffer = buffer.clone(); - - cx.spawn(async move |this, cx| { - let requests = join_all( - capable_lsps - .into_iter() - .map(|id| { - let request = GetCompletions { - position, - context: context.clone(), - server_id: Some(id), - }; - let buffer = buffer.clone(); - let language = language.clone(); - let lsp_adapter = lsp_adapter.clone(); - let upstream_client = upstream_client.clone(); - let response = this - .update(cx, |this, cx| { - this.send_lsp_proto_request( - buffer, - upstream_client, - project_id, - request, - cx, - ) - }) - .log_err(); - async move { - let response = response?.await.log_err()?; - - let completions = populate_labels_for_completions( - response.completions, - language, - lsp_adapter, - ) - .await; - - Some(CompletionResponse { - completions, - display_options: CompletionDisplayOptions::default(), - is_incomplete: response.is_incomplete, - }) - } - }) - .collect::>(), - ); - Ok(requests.await.into_iter().flatten().collect::>()) - }) - } else if let Some(local) = self.as_local() { - let snapshot = buffer.read(cx).snapshot(); - let offset = position.to_offset(&snapshot); - let scope = snapshot.language_scope_at(offset); - let language = snapshot.language().cloned(); - let completion_settings = language_settings( - language.as_ref().map(|language| language.name()), - buffer.read(cx).file(), - cx, - ) - .completions - .clone(); - if !completion_settings.lsp { - return Task::ready(Ok(Vec::new())); - } - - let server_ids: Vec<_> = buffer.update(cx, |buffer, cx| { - local - .language_servers_for_buffer(buffer, cx) - .filter(|(_, server)| server.capabilities().completion_provider.is_some()) - .filter(|(adapter, _)| { - scope - .as_ref() - .map(|scope| scope.language_allowed(&adapter.name)) - .unwrap_or(true) - }) - .map(|(_, server)| server.server_id()) - .collect() - }); - - let buffer = buffer.clone(); - let lsp_timeout = completion_settings.lsp_fetch_timeout_ms; - let lsp_timeout = if lsp_timeout > 0 { - Some(Duration::from_millis(lsp_timeout)) - } else { - None - }; - cx.spawn(async move |this, cx| { - let mut tasks = Vec::with_capacity(server_ids.len()); - this.update(cx, |lsp_store, cx| { - for server_id in server_ids { - let lsp_adapter = lsp_store.language_server_adapter_for_id(server_id); - let lsp_timeout = lsp_timeout - .map(|lsp_timeout| cx.background_executor().timer(lsp_timeout)); - let mut timeout = cx.background_spawn(async move { - match lsp_timeout { - Some(lsp_timeout) => { - lsp_timeout.await; - true - }, - None => false, - } - }).fuse(); - let mut lsp_request = lsp_store.request_lsp( - buffer.clone(), - LanguageServerToQuery::Other(server_id), - GetCompletions { - position, - context: context.clone(), - server_id: Some(server_id), - }, - cx, - ).fuse(); - let new_task = cx.background_spawn(async move { - select_biased! { - response = lsp_request => anyhow::Ok(Some(response?)), - timeout_happened = timeout => { - if timeout_happened { - log::warn!("Fetching completions from server {server_id} timed out, timeout ms: {}", completion_settings.lsp_fetch_timeout_ms); - Ok(None) - } else { - let completions = lsp_request.await?; - Ok(Some(completions)) - } - }, - } - }); - tasks.push((lsp_adapter, new_task)); - } - })?; - - let futures = tasks.into_iter().map(async |(lsp_adapter, task)| { - let completion_response = task.await.ok()??; - let completions = populate_labels_for_completions( - completion_response.completions, - language.clone(), - lsp_adapter, - ) - .await; - Some(CompletionResponse { - completions, - display_options: CompletionDisplayOptions::default(), - is_incomplete: completion_response.is_incomplete, - }) - }); - - let responses: Vec> = join_all(futures).await; - - Ok(responses.into_iter().flatten().collect()) - }) - } else { - Task::ready(Err(anyhow!("No upstream client or local language server"))) - } - } - - pub fn resolve_completions( - &self, - buffer: Entity, - completion_indices: Vec, - completions: Rc>>, - 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| { - let mut did_resolve = false; - if let Some((client, project_id)) = client { - for completion_index in completion_indices { - let server_id = { - let completion = &completions.borrow()[completion_index]; - completion.source.server_id() - }; - if let Some(server_id) = server_id { - if Self::resolve_completion_remote( - project_id, - server_id, - buffer_id, - completions.clone(), - completion_index, - client.clone(), - ) - .await - .log_err() - .is_some() - { - did_resolve = true; - } - } else { - resolve_word_completion( - &buffer_snapshot, - &mut completions.borrow_mut()[completion_index], - ); - } - } - } else { - for completion_index in completion_indices { - let server_id = { - let completion = &completions.borrow()[completion_index]; - completion.source.server_id() - }; - if let Some(server_id) = server_id { - let server_and_adapter = lsp_store - .read_with(cx, |lsp_store, _| { - let server = lsp_store.language_server_for_id(server_id)?; - let adapter = - lsp_store.language_server_adapter_for_id(server.server_id())?; - Some((server, adapter)) - }) - .ok() - .flatten(); - let Some((server, adapter)) = server_and_adapter else { - continue; - }; - - let resolved = Self::resolve_completion_local( - server, - completions.clone(), - completion_index, - ) - .await - .log_err() - .is_some(); - if resolved { - Self::regenerate_completion_labels( - adapter, - &buffer_snapshot, - completions.clone(), - completion_index, - ) - .await - .log_err(); - did_resolve = true; - } - } else { - resolve_word_completion( - &buffer_snapshot, - &mut completions.borrow_mut()[completion_index], - ); - } - } - } - - Ok(did_resolve) - }) - } - - async fn resolve_completion_local( - server: Arc, - completions: Rc>>, - completion_index: usize, - ) -> Result<()> { - let server_id = server.server_id(); - if !GetCompletions::can_resolve_completions(&server.capabilities()) { - return Ok(()); - } - - let request = { - let completion = &completions.borrow()[completion_index]; - match &completion.source { - CompletionSource::Lsp { - lsp_completion, - resolved, - server_id: completion_server_id, - .. - } => { - if *resolved { - return Ok(()); - } - anyhow::ensure!( - server_id == *completion_server_id, - "server_id mismatch, querying completion resolve for {server_id} but completion server id is {completion_server_id}" - ); - server.request::(*lsp_completion.clone()) - } - CompletionSource::BufferWord { .. } - | CompletionSource::Dap { .. } - | CompletionSource::Custom => { - return Ok(()); - } - } - }; - let resolved_completion = request - .await - .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 - - let mut completions = completions.borrow_mut(); - let completion = &mut completions[completion_index]; - if let CompletionSource::Lsp { - lsp_completion, - resolved, - server_id: completion_server_id, - .. - } = &mut completion.source - { - if *resolved { - return Ok(()); - } - anyhow::ensure!( - server_id == *completion_server_id, - "server_id mismatch, applying completion resolve for {server_id} but completion server id is {completion_server_id}" - ); - *lsp_completion = Box::new(resolved_completion); - *resolved = true; - } - Ok(()) - } - - async fn regenerate_completion_labels( - adapter: Arc, - snapshot: &BufferSnapshot, - completions: Rc>>, - completion_index: usize, - ) -> Result<()> { - let completion_item = completions.borrow()[completion_index] - .source - .lsp_completion(true) - .map(Cow::into_owned); - if let Some(lsp_documentation) = completion_item - .as_ref() - .and_then(|completion_item| completion_item.documentation.clone()) - { - let mut completions = completions.borrow_mut(); - let completion = &mut completions[completion_index]; - completion.documentation = Some(lsp_documentation.into()); - } else { - let mut completions = completions.borrow_mut(); - let completion = &mut completions[completion_index]; - completion.documentation = Some(CompletionDocumentation::Undocumented); - } - - let mut new_label = match completion_item { - Some(completion_item) => { - // NB: Zed does not have `details` inside the completion resolve capabilities, but certain language servers violate the spec and do not return `details` immediately, e.g. https://github.com/yioneko/vtsls/issues/213 - // So we have to update the label here anyway... - let language = snapshot.language(); - match language { - Some(language) => { - adapter - .labels_for_completions( - std::slice::from_ref(&completion_item), - language, - ) - .await? - } - None => Vec::new(), - } - .pop() - .flatten() - .unwrap_or_else(|| { - CodeLabel::fallback_for_completion( - &completion_item, - language.map(|language| language.as_ref()), - ) - }) - } - None => CodeLabel::plain( - completions.borrow()[completion_index].new_text.clone(), - None, - ), - }; - ensure_uniform_list_compatible_label(&mut new_label); - - let mut completions = completions.borrow_mut(); - let completion = &mut completions[completion_index]; - if completion.label.filter_text() == new_label.filter_text() { - completion.label = new_label; - } else { - log::error!( - "Resolved completion changed display label from {} to {}. \ - Refusing to apply this because it changes the fuzzy match text from {} to {}", - completion.label.text(), - new_label.text(), - completion.label.filter_text(), - new_label.filter_text() - ); - } - - Ok(()) - } - - async fn resolve_completion_remote( - project_id: u64, - server_id: LanguageServerId, - buffer_id: BufferId, - completions: Rc>>, - completion_index: usize, - client: AnyProtoClient, - ) -> Result<()> { - let lsp_completion = { - let completion = &completions.borrow()[completion_index]; - match &completion.source { - CompletionSource::Lsp { - lsp_completion, - resolved, - server_id: completion_server_id, - .. - } => { - anyhow::ensure!( - server_id == *completion_server_id, - "remote server_id mismatch, querying completion resolve for {server_id} but completion server id is {completion_server_id}" - ); - if *resolved { - return Ok(()); - } - serde_json::to_string(lsp_completion).unwrap().into_bytes() - } - CompletionSource::Custom - | CompletionSource::Dap { .. } - | CompletionSource::BufferWord { .. } => { - return Ok(()); - } - } - }; - let request = proto::ResolveCompletionDocumentation { - project_id, - language_server_id: server_id.0 as u64, - lsp_completion, - buffer_id: buffer_id.into(), - }; - - let response = client - .request(request) - .await - .context("completion documentation resolve proto request")?; - let resolved_lsp_completion = serde_json::from_slice(&response.lsp_completion)?; - - let documentation = if response.documentation.is_empty() { - CompletionDocumentation::Undocumented - } else if response.documentation_is_markdown { - CompletionDocumentation::MultiLineMarkdown(response.documentation.into()) - } else if response.documentation.lines().count() <= 1 { - CompletionDocumentation::SingleLine(response.documentation.into()) - } else { - CompletionDocumentation::MultiLinePlainText(response.documentation.into()) - }; - - let mut completions = completions.borrow_mut(); - let completion = &mut completions[completion_index]; - completion.documentation = Some(documentation); - if let CompletionSource::Lsp { - insert_range, - lsp_completion, - resolved, - server_id: completion_server_id, - lsp_defaults: _, - } = &mut completion.source - { - let completion_insert_range = response - .old_insert_start - .and_then(deserialize_anchor) - .zip(response.old_insert_end.and_then(deserialize_anchor)); - *insert_range = completion_insert_range.map(|(start, end)| start..end); - - if *resolved { - return Ok(()); - } - anyhow::ensure!( - server_id == *completion_server_id, - "remote server_id mismatch, applying completion resolve for {server_id} but completion server id is {completion_server_id}" - ); - *lsp_completion = Box::new(resolved_lsp_completion); - *resolved = true; - } - - let replace_range = response - .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; - } - - Ok(()) - } - - pub fn apply_additional_edits_for_completion( - &self, - buffer_handle: Entity, - completions: Rc>>, - completion_index: usize, - push_to_history: bool, - cx: &mut Context, - ) -> Task>> { - if let Some((client, project_id)) = self.upstream_client() { - let buffer = buffer_handle.read(cx); - let buffer_id = buffer.remote_id(); - cx.spawn(async move |_, cx| { - let request = { - let completion = completions.borrow()[completion_index].clone(); - proto::ApplyCompletionAdditionalEdits { - project_id, - buffer_id: buffer_id.into(), - completion: Some(Self::serialize_completion(&CoreCompletion { - replace_range: completion.replace_range, - new_text: completion.new_text, - source: completion.source, - })), - } - }; - - if let Some(transaction) = client.request(request).await?.transaction { - let transaction = language::proto::deserialize_transaction(transaction)?; - buffer_handle - .update(cx, |buffer, _| { - buffer.wait_for_edits(transaction.edit_ids.iter().copied()) - })? - .await?; - if push_to_history { - buffer_handle.update(cx, |buffer, _| { - buffer.push_transaction(transaction.clone(), Instant::now()); - buffer.finalize_last_transaction(); - })?; - } - Ok(Some(transaction)) - } else { - Ok(None) - } - }) - } else { - let Some(server) = buffer_handle.update(cx, |buffer, cx| { - let completion = &completions.borrow()[completion_index]; - let server_id = completion.source.server_id()?; - Some( - self.language_server_for_local_buffer(buffer, server_id, cx)? - .1 - .clone(), - ) - }) else { - return Task::ready(Ok(None)); - }; - - cx.spawn(async move |this, cx| { - Self::resolve_completion_local( - server.clone(), - completions.clone(), - completion_index, - ) - .await - .context("resolving completion")?; - let completion = completions.borrow()[completion_index].clone(); - let additional_text_edits = completion - .source - .lsp_completion(true) - .as_ref() - .and_then(|lsp_completion| lsp_completion.additional_text_edits.clone()); - if let Some(edits) = additional_text_edits { - let edits = this - .update(cx, |this, cx| { - this.as_local_mut().unwrap().edits_from_lsp( - &buffer_handle, - edits, - server.server_id(), - None, - cx, - ) - })? - .await?; - - buffer_handle.update(cx, |buffer, cx| { - buffer.finalize_last_transaction(); - buffer.start_transaction(); - - for (range, text) in edits { - let primary = &completion.replace_range; - - // Special case: if both ranges start at the very beginning of the file (line 0, column 0), - // and the primary completion is just an insertion (empty range), then this is likely - // an auto-import scenario and should not be considered overlapping - // https://github.com/zed-industries/zed/issues/26136 - let is_file_start_auto_import = { - let snapshot = buffer.snapshot(); - let primary_start_point = primary.start.to_point(&snapshot); - let range_start_point = range.start.to_point(&snapshot); - - let result = primary_start_point.row == 0 - && primary_start_point.column == 0 - && range_start_point.row == 0 - && range_start_point.column == 0; - - result - }; - - let has_overlap = if is_file_start_auto_import { - false - } else { - let start_within = primary.start.cmp(&range.start, buffer).is_le() - && primary.end.cmp(&range.start, buffer).is_ge(); - let end_within = range.start.cmp(&primary.end, buffer).is_le() - && range.end.cmp(&primary.end, buffer).is_ge(); - let result = start_within || end_within; - result - }; - - //Skip additional edits which overlap with the primary completion edit - //https://github.com/zed-industries/zed/pull/1871 - if !has_overlap { - buffer.edit([(range, text)], None, cx); - } - } - - let transaction = if buffer.end_transaction(cx).is_some() { - let transaction = buffer.finalize_last_transaction().unwrap().clone(); - if !push_to_history { - buffer.forget_transaction(transaction.id); - } - Some(transaction) - } else { - None - }; - Ok(transaction) - })? - } else { - Ok(None) - } - }) - } - } - - pub fn pull_diagnostics( - &mut self, - buffer: Entity, - cx: &mut Context, - ) -> Task>>> { - let buffer_id = buffer.read(cx).remote_id(); - - if let Some((client, upstream_project_id)) = self.upstream_client() { - let mut suitable_capabilities = None; - // Are we capable for proto request? - let any_server_has_diagnostics_provider = self.check_if_capable_for_proto_request( - &buffer, - |capabilities| { - if let Some(caps) = &capabilities.diagnostic_provider { - suitable_capabilities = Some(caps.clone()); - true - } else { - false - } - }, - cx, - ); - // We don't really care which caps are passed into the request, as they're ignored by RPC anyways. - let Some(dynamic_caps) = suitable_capabilities else { - return Task::ready(Ok(None)); - }; - assert!(any_server_has_diagnostics_provider); - - let identifier = buffer_diagnostic_identifier(&dynamic_caps); - let request = GetDocumentDiagnostics { - previous_result_id: None, - identifier, - registration_id: None, - }; - let request_task = client.request_lsp( - upstream_project_id, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(upstream_project_id, buffer.read(cx)), - ); - 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) - }) - } else { - let servers = buffer.update(cx, |buffer, cx| { - self.running_language_servers_for_local_buffer(buffer, cx) - .map(|(_, server)| server.clone()) - .collect::>() - }); - - let pull_diagnostics = servers - .into_iter() - .flat_map(|server| { - let result = maybe!({ - let local = self.as_local()?; - let server_id = server.server_id(); - let providers_with_identifiers = local - .language_server_dynamic_registrations - .get(&server_id) - .into_iter() - .flat_map(|registrations| registrations.diagnostics.clone()) - .collect::>(); - Some( - providers_with_identifiers - .into_iter() - .map(|(registration_id, dynamic_caps)| { - let identifier = buffer_diagnostic_identifier(&dynamic_caps); - let registration_id = registration_id.map(SharedString::from); - let result_id = self.result_id_for_buffer_pull( - server_id, - buffer_id, - ®istration_id, - cx, - ); - self.request_lsp( - buffer.clone(), - LanguageServerToQuery::Other(server_id), - GetDocumentDiagnostics { - previous_result_id: result_id, - registration_id, - identifier, - }, - cx, - ) - }) - .collect::>(), - ) - }); - - result.unwrap_or_default() - }) - .collect::>(); - - cx.background_spawn(async move { - let mut responses = Vec::new(); - for diagnostics in join_all(pull_diagnostics).await { - responses.extend(diagnostics?); - } - Ok(Some(responses)) - }) - } - } - - pub fn applicable_inlay_chunks( - &mut self, - buffer: &Entity, - ranges: &[Range], - cx: &mut Context, - ) -> Vec> { - self.latest_lsp_data(buffer, cx) - .inlay_hints - .applicable_chunks(ranges) - .map(|chunk| chunk.row_range()) - .collect() - } - - pub fn invalidate_inlay_hints<'a>( - &'a mut self, - for_buffers: impl IntoIterator + 'a, - ) { - for buffer_id in for_buffers { - if let Some(lsp_data) = self.lsp_data.get_mut(buffer_id) { - lsp_data.inlay_hints.clear(); - } - } - } - - pub fn inlay_hints( - &mut self, - invalidate: InvalidationStrategy, - buffer: Entity, - ranges: Vec>, - known_chunks: Option<(clock::Global, HashSet>)>, - cx: &mut Context, - ) -> HashMap, Task>> { - let next_hint_id = self.next_hint_id.clone(); - let lsp_data = self.latest_lsp_data(&buffer, cx); - let query_version = lsp_data.buffer_version.clone(); - let mut lsp_refresh_requested = false; - let for_server = if let InvalidationStrategy::RefreshRequested { - server_id, - request_id, - } = invalidate - { - let invalidated = lsp_data - .inlay_hints - .invalidate_for_server_refresh(server_id, request_id); - lsp_refresh_requested = invalidated; - Some(server_id) - } else { - None - }; - let existing_inlay_hints = &mut lsp_data.inlay_hints; - let known_chunks = known_chunks - .filter(|(known_version, _)| !lsp_data.buffer_version.changed_since(known_version)) - .map(|(_, known_chunks)| known_chunks) - .unwrap_or_default(); - - let mut hint_fetch_tasks = Vec::new(); - let mut cached_inlay_hints = None; - let mut ranges_to_query = None; - let applicable_chunks = existing_inlay_hints - .applicable_chunks(ranges.as_slice()) - .filter(|chunk| !known_chunks.contains(&chunk.row_range())) - .collect::>(); - if applicable_chunks.is_empty() { - return HashMap::default(); - } - - for row_chunk in applicable_chunks { - match ( - existing_inlay_hints - .cached_hints(&row_chunk) - .filter(|_| !lsp_refresh_requested) - .cloned(), - existing_inlay_hints - .fetched_hints(&row_chunk) - .as_ref() - .filter(|_| !lsp_refresh_requested) - .cloned(), - ) { - (None, None) => { - let Some(chunk_range) = existing_inlay_hints.chunk_range(row_chunk) else { - continue; - }; - ranges_to_query - .get_or_insert_with(Vec::new) - .push((row_chunk, chunk_range)); - } - (None, Some(fetched_hints)) => hint_fetch_tasks.push((row_chunk, fetched_hints)), - (Some(cached_hints), None) => { - for (server_id, cached_hints) in cached_hints { - if for_server.is_none_or(|for_server| for_server == server_id) { - cached_inlay_hints - .get_or_insert_with(HashMap::default) - .entry(row_chunk.row_range()) - .or_insert_with(HashMap::default) - .entry(server_id) - .or_insert_with(Vec::new) - .extend(cached_hints); - } - } - } - (Some(cached_hints), Some(fetched_hints)) => { - hint_fetch_tasks.push((row_chunk, fetched_hints)); - for (server_id, cached_hints) in cached_hints { - if for_server.is_none_or(|for_server| for_server == server_id) { - cached_inlay_hints - .get_or_insert_with(HashMap::default) - .entry(row_chunk.row_range()) - .or_insert_with(HashMap::default) - .entry(server_id) - .or_insert_with(Vec::new) - .extend(cached_hints); - } - } - } - } - } - - if hint_fetch_tasks.is_empty() - && ranges_to_query - .as_ref() - .is_none_or(|ranges| ranges.is_empty()) - && let Some(cached_inlay_hints) = cached_inlay_hints - { - cached_inlay_hints - .into_iter() - .map(|(row_chunk, hints)| (row_chunk, Task::ready(Ok(hints)))) - .collect() - } else { - for (chunk, range_to_query) in ranges_to_query.into_iter().flatten() { - let next_hint_id = next_hint_id.clone(); - let buffer = buffer.clone(); - let query_version = query_version.clone(); - let new_inlay_hints = cx - .spawn(async move |lsp_store, cx| { - let new_fetch_task = lsp_store.update(cx, |lsp_store, cx| { - lsp_store.fetch_inlay_hints(for_server, &buffer, range_to_query, cx) - })?; - new_fetch_task - .await - .and_then(|new_hints_by_server| { - lsp_store.update(cx, |lsp_store, cx| { - let lsp_data = lsp_store.latest_lsp_data(&buffer, cx); - let update_cache = lsp_data.buffer_version == query_version; - if new_hints_by_server.is_empty() { - if update_cache { - lsp_data.inlay_hints.invalidate_for_chunk(chunk); - } - HashMap::default() - } else { - new_hints_by_server - .into_iter() - .map(|(server_id, new_hints)| { - let new_hints = new_hints - .into_iter() - .map(|new_hint| { - ( - InlayId::Hint(next_hint_id.fetch_add( - 1, - atomic::Ordering::AcqRel, - )), - new_hint, - ) - }) - .collect::>(); - if update_cache { - lsp_data.inlay_hints.insert_new_hints( - chunk, - server_id, - new_hints.clone(), - ); - } - (server_id, new_hints) - }) - .collect() - } - }) - }) - .map_err(Arc::new) - }) - .shared(); - - let fetch_task = lsp_data.inlay_hints.fetched_hints(&chunk); - *fetch_task = Some(new_inlay_hints.clone()); - hint_fetch_tasks.push((chunk, new_inlay_hints)); - } - - cached_inlay_hints - .unwrap_or_default() - .into_iter() - .map(|(row_chunk, hints)| (row_chunk, Task::ready(Ok(hints)))) - .chain(hint_fetch_tasks.into_iter().map(|(chunk, hints_fetch)| { - ( - chunk.row_range(), - cx.spawn(async move |_, _| { - hints_fetch.await.map_err(|e| { - if e.error_code() != ErrorCode::Internal { - anyhow!(e.error_code()) - } else { - anyhow!("{e:#}") - } - }) - }), - ) - })) - .collect() - } - } - - fn fetch_inlay_hints( - &mut self, - for_server: Option, - buffer: &Entity, - range: Range, - cx: &mut Context, - ) -> Task>>> { - let request = InlayHints { - range: range.clone(), - }; - if let Some((upstream_client, project_id)) = self.upstream_client() { - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(HashMap::default())); - } - let request_task = upstream_client.request_lsp( - project_id, - for_server.map(|id| id.to_proto()), - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.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(HashMap::default()); - }; - let Some(responses) = request_task.await? else { - return Ok(HashMap::default()); - }; - - let inlay_hints = join_all(responses.payload.into_iter().map(|response| { - let lsp_store = lsp_store.clone(); - let buffer = buffer.clone(); - let cx = cx.clone(); - let request = request.clone(); - async move { - ( - LanguageServerId::from_proto(response.server_id), - request - .response_from_proto(response.response, lsp_store, buffer, cx) - .await, - ) - } - })) - .await; - - let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?; - let mut has_errors = false; - let inlay_hints = inlay_hints - .into_iter() - .filter_map(|(server_id, inlay_hints)| match inlay_hints { - Ok(inlay_hints) => Some((server_id, inlay_hints)), - Err(e) => { - has_errors = true; - log::error!("{e:#}"); - None - } - }) - .map(|(server_id, mut new_hints)| { - new_hints.retain(|hint| { - hint.position.is_valid(&buffer_snapshot) - && range.start.is_valid(&buffer_snapshot) - && range.end.is_valid(&buffer_snapshot) - && hint.position.cmp(&range.start, &buffer_snapshot).is_ge() - && hint.position.cmp(&range.end, &buffer_snapshot).is_lt() - }); - (server_id, new_hints) - }) - .collect::>(); - anyhow::ensure!( - !has_errors || !inlay_hints.is_empty(), - "Failed to fetch inlay hints" - ); - Ok(inlay_hints) - }) - } else { - let inlay_hints_task = match for_server { - Some(server_id) => { - let server_task = self.request_lsp( - buffer.clone(), - LanguageServerToQuery::Other(server_id), - request, - cx, - ); - cx.background_spawn(async move { - let mut responses = Vec::new(); - match server_task.await { - Ok(response) => responses.push((server_id, response)), - // rust-analyzer likes to error with this when its still loading up - Err(e) if format!("{e:#}").ends_with("content modified") => (), - Err(e) => log::error!( - "Error handling response for inlay hints request: {e:#}" - ), - } - responses - }) - } - None => self.request_multiple_lsp_locally(buffer, None::, request, cx), - }; - let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); - cx.background_spawn(async move { - Ok(inlay_hints_task - .await - .into_iter() - .map(|(server_id, mut new_hints)| { - new_hints.retain(|hint| { - hint.position.is_valid(&buffer_snapshot) - && range.start.is_valid(&buffer_snapshot) - && range.end.is_valid(&buffer_snapshot) - && hint.position.cmp(&range.start, &buffer_snapshot).is_ge() - && hint.position.cmp(&range.end, &buffer_snapshot).is_lt() - }); - (server_id, new_hints) - }) - .collect()) - }) - } - } - - pub fn pull_diagnostics_for_buffer( - &mut self, - buffer: Entity, - cx: &mut Context, - ) -> Task> { - 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(()); - }; - lsp_store.update(cx, |lsp_store, cx| { - if lsp_store.as_local().is_none() { - return; - } - - let mut unchanged_buffers = HashMap::default(); - let server_diagnostics_updates = diagnostics - .into_iter() - .filter_map(|diagnostics_set| match diagnostics_set { - LspPullDiagnostics::Response { - server_id, - uri, - diagnostics, - registration_id, - } => Some((server_id, uri, diagnostics, registration_id)), - LspPullDiagnostics::Default => None, - }) - .fold( - HashMap::default(), - |mut acc, (server_id, uri, diagnostics, new_registration_id)| { - let (result_id, diagnostics) = match diagnostics { - PulledDiagnostics::Unchanged { result_id } => { - unchanged_buffers - .entry(new_registration_id.clone()) - .or_insert_with(HashSet::default) - .insert(uri.clone()); - (Some(result_id), Vec::new()) - } - PulledDiagnostics::Changed { - result_id, - diagnostics, - } => (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(HashMap::default) - .entry(new_registration_id.clone()) - .or_insert_with(Vec::new) - .push(DocumentDiagnosticsUpdate { - server_id, - diagnostics: lsp::PublishDiagnosticsParams { - uri, - diagnostics, - version: None, - }, - result_id, - disk_based_sources, - registration_id: new_registration_id, - }); - acc - }, - ); - - for diagnostic_updates in server_diagnostics_updates.into_values() { - for (registration_id, diagnostic_updates) in diagnostic_updates { - lsp_store - .merge_lsp_diagnostics( - DiagnosticSourceKind::Pulled, - diagnostic_updates, - |document_uri, old_diagnostic, _| match old_diagnostic.source_kind { - DiagnosticSourceKind::Pulled => { - old_diagnostic.registration_id != registration_id - || unchanged_buffers - .get(&old_diagnostic.registration_id) - .is_some_and(|unchanged_buffers| { - unchanged_buffers.contains(&document_uri) - }) - } - DiagnosticSourceKind::Other | DiagnosticSourceKind::Pushed => { - true - } - }, - cx, - ) - .log_err(); - } - } - }) - }) - } - - pub fn document_colors( - &mut self, - known_cache_version: Option, - buffer: Entity, - cx: &mut Context, - ) -> Option { - let version_queried_for = buffer.read(cx).version(); - let buffer_id = buffer.read(cx).remote_id(); - - let current_language_servers = self.as_local().map(|local| { - local - .buffers_opened_in_servers - .get(&buffer_id) - .cloned() - .unwrap_or_default() - }); - - if let Some(lsp_data) = self.current_lsp_data(buffer_id) { - if let Some(cached_colors) = &lsp_data.document_colors { - if !version_queried_for.changed_since(&lsp_data.buffer_version) { - let has_different_servers = - current_language_servers.is_some_and(|current_language_servers| { - current_language_servers - != cached_colors.colors.keys().copied().collect() - }); - if !has_different_servers { - let cache_version = cached_colors.cache_version; - if Some(cache_version) == known_cache_version { - return None; - } else { - return Some( - Task::ready(Ok(DocumentColors { - colors: cached_colors - .colors - .values() - .flatten() - .cloned() - .collect(), - cache_version: Some(cache_version), - })) - .shared(), - ); - } - } - } - } - } - - let color_lsp_data = self - .latest_lsp_data(&buffer, cx) - .document_colors - .get_or_insert_default(); - if let Some((updating_for, running_update)) = &color_lsp_data.colors_update - && !version_queried_for.changed_since(updating_for) - { - return Some(running_update.clone()); - } - let buffer_version_queried_for = version_queried_for.clone(); - let new_task = cx - .spawn(async move |lsp_store, cx| { - cx.background_executor() - .timer(Duration::from_millis(30)) - .await; - let fetched_colors = lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.fetch_document_colors_for_buffer(&buffer, cx) - })? - .await - .context("fetching document colors") - .map_err(Arc::new); - let fetched_colors = match fetched_colors { - Ok(fetched_colors) => { - if Some(true) - == buffer - .update(cx, |buffer, _| { - buffer.version() != buffer_version_queried_for - }) - .ok() - { - return Ok(DocumentColors::default()); - } - fetched_colors - } - Err(e) => { - lsp_store - .update(cx, |lsp_store, _| { - if let Some(lsp_data) = lsp_store.lsp_data.get_mut(&buffer_id) { - if let Some(document_colors) = &mut lsp_data.document_colors { - document_colors.colors_update = None; - } - } - }) - .ok(); - return Err(e); - } - }; - - lsp_store - .update(cx, |lsp_store, cx| { - let lsp_data = lsp_store.latest_lsp_data(&buffer, cx); - let lsp_colors = lsp_data.document_colors.get_or_insert_default(); - - if let Some(fetched_colors) = fetched_colors { - if lsp_data.buffer_version == buffer_version_queried_for { - lsp_colors.colors.extend(fetched_colors); - lsp_colors.cache_version += 1; - } else if !lsp_data - .buffer_version - .changed_since(&buffer_version_queried_for) - { - lsp_data.buffer_version = buffer_version_queried_for; - lsp_colors.colors = fetched_colors; - lsp_colors.cache_version += 1; - } - } - lsp_colors.colors_update = None; - let colors = lsp_colors - .colors - .values() - .flatten() - .cloned() - .collect::>(); - DocumentColors { - colors, - cache_version: Some(lsp_colors.cache_version), - } - }) - .map_err(Arc::new) - }) - .shared(); - color_lsp_data.colors_update = Some((version_queried_for, new_task.clone())); - Some(new_task) - } - - fn fetch_document_colors_for_buffer( - &mut self, - buffer: &Entity, - cx: &mut Context, - ) -> 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( - project_id, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); - cx.spawn(async move |lsp_store, cx| { - let Some(lsp_store) = lsp_store.upgrade() else { - return Ok(None); - }; - let colors = join_all( - request_task - .await - .log_err() - .flatten() - .map(|response| response.payload) - .unwrap_or_default() - .into_iter() - .map(|color_response| { - let response = request.response_from_proto( - color_response.response, - lsp_store.clone(), - buffer.clone(), - cx.clone(), - ); - async move { - ( - LanguageServerId::from_proto(color_response.server_id), - response.await.log_err().unwrap_or_default(), - ) - } - }), - ) - .await - .into_iter() - .fold(HashMap::default(), |mut acc, (server_id, colors)| { - acc.entry(server_id) - .or_insert_with(HashSet::default) - .extend(colors); - acc - }); - Ok(Some(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(), - )) - }) - } - } - - pub fn signature_help( - &mut self, - buffer: &Entity, - position: T, - cx: &mut Context, - ) -> 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, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(upstream_project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); - cx.spawn(async move |weak_lsp_store, cx| { - let lsp_store = weak_lsp_store.upgrade()?; - let signatures = join_all( - request_task - .await - .log_err() - .flatten() - .map(|response| response.payload) - .unwrap_or_default() - .into_iter() - .map(|response| { - let response = GetSignatureHelp { position }.response_from_proto( - response.response, - lsp_store.clone(), - buffer.clone(), - cx.clone(), - ); - async move { response.await.log_err().flatten() } - }), - ) - .await - .into_iter() - .flatten() - .collect(); - Some(signatures) - }) - } else { - let all_actions_task = self.request_multiple_lsp_locally( - buffer, - Some(position), - GetSignatureHelp { position }, - cx, - ); - cx.background_spawn(async move { - Some( - all_actions_task - .await - .into_iter() - .flat_map(|(_, actions)| actions) - .collect::>(), - ) - }) - } - } - - pub fn hover( - &mut self, - buffer: &Entity, - position: PointUtf16, - cx: &mut Context, - ) -> 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, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(upstream_project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); - cx.spawn(async move |weak_lsp_store, cx| { - let lsp_store = weak_lsp_store.upgrade()?; - let hovers = join_all( - request_task - .await - .log_err() - .flatten() - .map(|response| response.payload) - .unwrap_or_default() - .into_iter() - .map(|response| { - let response = GetHover { position }.response_from_proto( - response.response, - lsp_store.clone(), - buffer.clone(), - cx.clone(), - ); - async move { - response - .await - .log_err() - .flatten() - .and_then(remove_empty_hover_blocks) - } - }), - ) - .await - .into_iter() - .flatten() - .collect(); - Some(hovers) - }) - } else { - let all_actions_task = self.request_multiple_lsp_locally( - buffer, - Some(position), - GetHover { position }, - cx, - ); - cx.background_spawn(async move { - Some( - all_actions_task - .await - .into_iter() - .filter_map(|(_, hover)| remove_empty_hover_blocks(hover?)) - .collect::>(), - ) - }) - } - } - - pub fn symbols(&self, query: &str, cx: &mut Context) -> Task>> { - let language_registry = self.languages.clone(); - - if let Some((upstream_client, project_id)) = self.upstream_client().as_ref() { - let request = upstream_client.request(proto::GetProjectSymbols { - project_id: *project_id, - query: query.to_string(), - }); - cx.foreground_executor().spawn(async move { - let response = request.await?; - let mut symbols = Vec::new(); - let core_symbols = response - .symbols - .into_iter() - .filter_map(|symbol| Self::deserialize_symbol(symbol).log_err()) - .collect::>(); - populate_labels_for_symbols(core_symbols, &language_registry, None, &mut symbols) - .await; - Ok(symbols) - }) - } else if let Some(local) = self.as_local() { - struct WorkspaceSymbolsResult { - server_id: LanguageServerId, - lsp_adapter: Arc, - worktree: WeakEntity, - lsp_symbols: Vec<(String, SymbolKind, lsp::Location)>, - } - - let mut requests = Vec::new(); - let mut requested_servers = BTreeSet::new(); - for (seed, state) in local.language_server_ids.iter() { - let Some(worktree_handle) = self - .worktree_store - .read(cx) - .worktree_for_id(seed.worktree_id, cx) - else { - continue; - }; - let worktree = worktree_handle.read(cx); - if !worktree.is_visible() { - continue; - } - - if !requested_servers.insert(state.id) { - continue; - } - - 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, - }; - if !supports_workspace_symbol_request { - continue; - } - let worktree_handle = worktree_handle.clone(); - let server_id = server.server_id(); - requests.push( - server - .request::( - lsp::WorkspaceSymbolParams { - query: query.to_string(), - ..Default::default() - }, - ) - .map(move |response| { - let lsp_symbols = response.into_response() - .context("workspace symbols request") - .log_err() - .flatten() - .map(|symbol_response| match symbol_response { - lsp::WorkspaceSymbolResponse::Flat(flat_responses) => { - flat_responses.into_iter().map(|lsp_symbol| { - (lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location) - }).collect::>() - } - lsp::WorkspaceSymbolResponse::Nested(nested_responses) => { - nested_responses.into_iter().filter_map(|lsp_symbol| { - let location = match lsp_symbol.location { - OneOf::Left(location) => location, - OneOf::Right(_) => { - log::error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport"); - return None - } - }; - Some((lsp_symbol.name, lsp_symbol.kind, location)) - }).collect::>() - } - }).unwrap_or_default(); - - WorkspaceSymbolsResult { - server_id, - lsp_adapter, - worktree: worktree_handle.downgrade(), - lsp_symbols, - } - }), - ); - } - - cx.spawn(async move |this, cx| { - let responses = futures::future::join_all(requests).await; - let this = match this.upgrade() { - Some(this) => this, - None => return Ok(Vec::new()), - }; - - let mut symbols = Vec::new(); - for result in responses { - let core_symbols = this.update(cx, |this, cx| { - result - .lsp_symbols - .into_iter() - .filter_map(|(symbol_name, symbol_kind, symbol_location)| { - let abs_path = symbol_location.uri.to_file_path().ok()?; - let source_worktree = result.worktree.upgrade()?; - let source_worktree_id = source_worktree.read(cx).id(); - - let path = if let Some((tree, rel_path)) = - this.worktree_store.read(cx).find_worktree(&abs_path, cx) - { - let worktree_id = tree.read(cx).id(); - SymbolLocation::InProject(ProjectPath { - worktree_id, - path: rel_path, - }) - } else { - SymbolLocation::OutsideProject { - signature: this.symbol_signature(&abs_path), - abs_path: abs_path.into(), - } - }; - - Some(CoreSymbol { - source_language_server_id: result.server_id, - language_server_name: result.lsp_adapter.name.clone(), - source_worktree_id, - path, - kind: symbol_kind, - name: symbol_name, - range: range_from_lsp(symbol_location.range), - }) - }) - .collect() - })?; - - populate_labels_for_symbols( - core_symbols, - &language_registry, - Some(result.lsp_adapter), - &mut symbols, - ) - .await; - } - - Ok(symbols) - }) - } else { - Task::ready(Err(anyhow!("No upstream client or local language server"))) - } - } - - pub fn diagnostic_summary(&self, include_ignored: bool, cx: &App) -> DiagnosticSummary { - let mut summary = DiagnosticSummary::default(); - for (_, _, path_summary) in self.diagnostic_summaries(include_ignored, cx) { - summary.error_count += path_summary.error_count; - summary.warning_count += path_summary.warning_count; - } - summary - } - - /// Returns the diagnostic summary for a specific project path. - pub fn diagnostic_summary_for_path( - &self, - project_path: &ProjectPath, - _: &App, - ) -> DiagnosticSummary { - if let Some(summaries) = self - .diagnostic_summaries - .get(&project_path.worktree_id) - .and_then(|map| map.get(&project_path.path)) - { - let (error_count, warning_count) = summaries.iter().fold( - (0, 0), - |(error_count, warning_count), (_language_server_id, summary)| { - ( - error_count + summary.error_count, - warning_count + summary.warning_count, - ) - }, - ); - - DiagnosticSummary { - error_count, - warning_count, - } - } else { - DiagnosticSummary::default() - } - } - - pub fn diagnostic_summaries<'a>( - &'a self, - include_ignored: bool, - cx: &'a App, - ) -> impl Iterator + 'a { - self.worktree_store - .read(cx) - .visible_worktrees(cx) - .filter_map(|worktree| { - let worktree = worktree.read(cx); - Some((worktree, self.diagnostic_summaries.get(&worktree.id())?)) - }) - .flat_map(move |(worktree, summaries)| { - let worktree_id = worktree.id(); - summaries - .iter() - .filter(move |(path, _)| { - include_ignored - || worktree - .entry_for_path(path.as_ref()) - .is_some_and(|entry| !entry.is_ignored) - }) - .flat_map(move |(path, summaries)| { - summaries.iter().map(move |(server_id, summary)| { - ( - ProjectPath { - worktree_id, - path: path.clone(), - }, - *server_id, - *summary, - ) - }) - }) - }) - } - - pub fn on_buffer_edited( - &mut self, - buffer: Entity, - cx: &mut Context, - ) -> Option<()> { - let language_servers: Vec<_> = buffer.update(cx, |buffer, cx| { - Some( - self.as_local()? - .language_servers_for_buffer(buffer, cx) - .map(|i| i.1.clone()) - .collect(), - ) - })?; - - let buffer = buffer.read(cx); - let file = File::from_dyn(buffer.file())?; - let abs_path = file.as_local()?.abs_path(cx); - let uri = lsp::Uri::from_file_path(&abs_path) - .ok() - .with_context(|| format!("Failed to convert path to URI: {}", abs_path.display())) - .log_err()?; - let next_snapshot = buffer.text_snapshot(); - for language_server in language_servers { - let language_server = language_server.clone(); - - let buffer_snapshots = self - .as_local_mut()? - .buffer_snapshots - .get_mut(&buffer.remote_id()) - .and_then(|m| m.get_mut(&language_server.server_id()))?; - let previous_snapshot = buffer_snapshots.last()?; - - let build_incremental_change = || { - buffer - .edits_since::>( - 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); - let new_text = next_snapshot - .text_for_range(edit.new.start.1..edit.new.end.1) - .collect(); - lsp::TextDocumentContentChangeEvent { - range: Some(lsp::Range::new( - point_to_lsp(edit_start), - point_to_lsp(edit_end), - )), - range_length: None, - text: new_text, - } - }) - .collect() - }; - - let document_sync_kind = language_server - .capabilities() - .text_document_sync - .as_ref() - .and_then(|sync| match sync { - lsp::TextDocumentSyncCapability::Kind(kind) => Some(*kind), - lsp::TextDocumentSyncCapability::Options(options) => options.change, - }); - - let content_changes: Vec<_> = match document_sync_kind { - Some(lsp::TextDocumentSyncKind::FULL) => { - vec![lsp::TextDocumentContentChangeEvent { - range: None, - range_length: None, - text: next_snapshot.text(), - }] - } - Some(lsp::TextDocumentSyncKind::INCREMENTAL) => build_incremental_change(), - _ => { - #[cfg(any(test, feature = "test-support"))] - { - build_incremental_change() - } - - #[cfg(not(any(test, feature = "test-support")))] - { - continue; - } - } - }; - - let next_version = previous_snapshot.version + 1; - buffer_snapshots.push(LspBufferSnapshot { - version: next_version, - snapshot: next_snapshot.clone(), - }); - - language_server - .notify::( - lsp::DidChangeTextDocumentParams { - text_document: lsp::VersionedTextDocumentIdentifier::new( - uri.clone(), - next_version, - ), - content_changes, - }, - ) - .ok(); - self.pull_workspace_diagnostics(language_server.server_id()); - } - - None - } - - pub fn on_buffer_saved( - &mut self, - buffer: Entity, - cx: &mut Context, - ) -> Option<()> { - let file = File::from_dyn(buffer.read(cx).file())?; - let worktree_id = file.worktree_id(cx); - let abs_path = file.as_local()?.abs_path(cx); - let text_document = lsp::TextDocumentIdentifier { - uri: file_path_to_lsp_url(&abs_path).log_err()?, - }; - let local = self.as_local()?; - - for server in local.language_servers_for_worktree(worktree_id) { - if let Some(include_text) = include_text(server.as_ref()) { - let text = if include_text { - Some(buffer.read(cx).text()) - } else { - None - }; - server - .notify::( - lsp::DidSaveTextDocumentParams { - text_document: text_document.clone(), - text, - }, - ) - .ok(); - } - } - - let language_servers = buffer.update(cx, |buffer, cx| { - local.language_server_ids_for_buffer(buffer, cx) - }); - for language_server_id in language_servers { - self.simulate_disk_based_diagnostics_events_if_needed(language_server_id, cx); - } - - None - } - - async fn refresh_workspace_configurations(lsp_store: &WeakEntity, cx: &mut AsyncApp) { - maybe!(async move { - let mut refreshed_servers = HashSet::default(); - let servers = lsp_store - .update(cx, |lsp_store, cx| { - let local = lsp_store.as_local()?; - - let servers = local - .language_server_ids - .iter() - .filter_map(|(seed, state)| { - 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; - - let states = local.language_servers.get(&server_id)?; - - match states { - LanguageServerState::Starting { .. } => None, - LanguageServerState::Running { - adapter, server, .. - } => { - 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(), - &delegate, - toolchain, - None, - cx, - ) - .await - .ok()?; - server - .notify::( - lsp::DidChangeConfigurationParams { settings }, - ) - .ok()?; - Some(()) - })) - } - } - }) - .collect::>(); - - Some(servers) - }) - .ok() - .flatten()?; - - log::debug!("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 maintain_workspace_config( - external_refresh_requests: watch::Receiver<()>, - cx: &mut Context, - ) -> Task> { - let (mut settings_changed_tx, mut settings_changed_rx) = watch::channel(); - let _ = postage::stream::Stream::try_recv(&mut settings_changed_rx); - - let settings_observation = cx.observe_global::(move |_, _| { - *settings_changed_tx.borrow_mut() = (); - }); - - 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, cx).await; - } - - drop(settings_observation); - anyhow::Ok(()) - }) - } - - pub fn running_language_servers_for_local_buffer<'a>( - &'a self, - buffer: &Buffer, - cx: &mut App, - ) -> impl Iterator, &'a Arc)> { - let local = self.as_local(); - let language_server_ids = local - .map(|local| local.language_server_ids_for_buffer(buffer, cx)) - .unwrap_or_default(); - - language_server_ids - .into_iter() - .filter_map( - move |server_id| match local?.language_servers.get(&server_id)? { - LanguageServerState::Running { - adapter, server, .. - } => Some((adapter, server)), - _ => None, - }, - ) - } - - pub fn language_servers_for_local_buffer( - &self, - buffer: &Buffer, - cx: &mut App, - ) -> Vec { - let local = self.as_local(); - local - .map(|local| local.language_server_ids_for_buffer(buffer, cx)) - .unwrap_or_default() - } - - pub fn language_server_for_local_buffer<'a>( - &'a self, - buffer: &'a Buffer, - server_id: LanguageServerId, - cx: &'a mut App, - ) -> Option<(&'a Arc, &'a Arc)> { - self.as_local()? - .language_servers_for_buffer(buffer, cx) - .find(|(_, s)| s.server_id() == server_id) - } - - fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut Context) { - self.diagnostic_summaries.remove(&id_to_remove); - if let Some(local) = self.as_local_mut() { - let to_remove = local.remove_worktree(id_to_remove, cx); - for server in to_remove { - self.language_server_statuses.remove(&server); - } - } - } - - pub fn shared( - &mut self, - project_id: u64, - downstream_client: AnyProtoClient, - _: &mut Context, - ) { - 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: status.worktree.map(|id| id.to_proto()), - }), - capabilities: serde_json::to_string(&server.capabilities()) - .expect("serializing server LSP capabilities"), - }) - .log_err(); - } - } - } - - pub fn disconnected_from_host(&mut self) { - self.downstream_client.take(); - } - - pub fn disconnected_from_ssh_remote(&mut self) { - if let LspStoreMode::Remote(RemoteLspStore { - upstream_client, .. - }) = &mut self.mode - { - upstream_client.take(); - } - } - - pub(crate) fn set_language_server_statuses_from_proto( - &mut self, - project: WeakEntity, - language_servers: Vec, - server_capabilities: Vec, - cx: &mut Context, - ) { - let lsp_logs = cx - .try_global::() - .map(|lsp_store| lsp_store.0.clone()); - - 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); - } - - let name = LanguageServerName::from_proto(server.name); - let worktree = server.worktree_id.map(WorktreeId::from_proto); - - if let Some(lsp_logs) = &lsp_logs { - lsp_logs.update(cx, |lsp_logs, cx| { - lsp_logs.add_language_server( - // Only remote clients get their language servers set from proto - LanguageServerKind::Remote { - project: project.clone(), - }, - server_id, - Some(name.clone()), - worktree, - None, - cx, - ); - }); - } - - ( - server_id, - LanguageServerStatus { - name, - pending_work: Default::default(), - has_pending_diagnostic_updates: false, - progress_tokens: Default::default(), - worktree, - binary: None, - configuration: None, - workspace_folders: BTreeSet::new(), - }, - ) - }) - .collect(); - } - - #[cfg(test)] - pub fn update_diagnostic_entries( - &mut self, - server_id: LanguageServerId, - abs_path: PathBuf, - result_id: Option, - version: Option, - diagnostics: Vec>>, - 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(&[]), - registration_id: None, - }], - |_, _, _| false, - cx, - )?; - Ok(()) - } - - pub fn merge_diagnostic_entries<'a>( - &mut self, - diagnostic_updates: Vec>, - merge: impl Fn(&lsp::Uri, &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 worktree_id = worktree.read(cx).id(); - let project_path = ProjectPath { - worktree_id, - path: relative_path, - }; - - let document_uri = lsp::Uri::from_file_path(abs_path) - .map_err(|()| anyhow!("Failed to convert buffer path {abs_path:?} to lsp Uri"))?; - 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(&document_uri, &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, - Some(update.registration_id), - update.result_id, - update.diagnostics.version, - update.diagnostics.diagnostics.clone(), - reused_diagnostics.clone(), - cx, - )?; - - update.diagnostics.diagnostics.extend(reused_diagnostics); - } else if let Some(local) = self.as_local() { - let reused_diagnostics = local - .diagnostics - .get(&worktree_id) - .and_then(|diagnostics_for_tree| diagnostics_for_tree.get(&project_path.path)) - .and_then(|diagnostics_by_server_id| { - diagnostics_by_server_id - .binary_search_by_key(&server_id, |e| e.0) - .ok() - .map(|ix| &diagnostics_by_server_id[ix].1) - }) - .into_iter() - .flatten() - .filter(|v| merge(&document_uri, &v.diagnostic, cx)); - - update - .diagnostics - .diagnostics - .extend(reused_diagnostics.cloned()); - } - - let updated = worktree.update(cx, |worktree, cx| { - self.update_worktree_diagnostics( - worktree.id(), - server_id, - project_path.path.clone(), - update.diagnostics.diagnostics, - 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(()) => {} - } - } - - 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 }); - } - Ok(()) - } - - fn update_worktree_diagnostics( - &mut self, - worktree_id: WorktreeId, - server_id: LanguageServerId, - path_in_worktree: Arc, - diagnostics: Vec>>, - _: &mut Context, - ) -> Result>> { - let local = match &mut self.mode { - LspStoreMode::Local(local_lsp_store) => local_lsp_store, - _ => anyhow::bail!("update_worktree_diagnostics called on remote"), - }; - - 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 old_summary = summaries_by_server_id - .remove(&server_id) - .unwrap_or_default(); - - let new_summary = DiagnosticSummary::new(&diagnostics); - if diagnostics.is_empty() { - if let Some(diagnostics_by_server_id) = diagnostics_for_tree.get_mut(&path_in_worktree) - { - 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); - } - } - } else { - summaries_by_server_id.insert(server_id, new_summary); - let diagnostics_by_server_id = diagnostics_for_tree - .entry(path_in_worktree.clone()) - .or_default(); - match diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) { - Ok(ix) => { - diagnostics_by_server_id[ix] = (server_id, diagnostics); - } - Err(ix) => { - diagnostics_by_server_id.insert(ix, (server_id, diagnostics)); - } - } - } - - 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)) - } - } else { - Ok(ControlFlow::Break(())) - } - } - - pub fn open_buffer_for_symbol( - &mut self, - symbol: &Symbol, - cx: &mut Context, - ) -> Task>> { - if let Some((client, project_id)) = self.upstream_client() { - let request = client.request(proto::OpenBufferForSymbol { - project_id, - symbol: Some(Self::serialize_symbol(symbol)), - }); - cx.spawn(async move |this, cx| { - let response = request.await?; - let buffer_id = BufferId::new(response.buffer_id)?; - this.update(cx, |this, cx| this.wait_for_remote_buffer(buffer_id, cx))? - .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 { - return Task::ready(Err(anyhow!( - "language server for worktree and language not found" - ))); - }; - - let symbol_abs_path = match &symbol.path { - SymbolLocation::InProject(project_path) => self - .worktree_store - .read(cx) - .absolutize(&project_path, cx) - .context("no such worktree"), - SymbolLocation::OutsideProject { - abs_path, - signature: _, - } => Ok(abs_path.to_path_buf()), - }; - let symbol_abs_path = match symbol_abs_path { - Ok(abs_path) => abs_path, - Err(err) => return Task::ready(Err(err)), - }; - let symbol_uri = if let Ok(uri) = lsp::Uri::from_file_path(symbol_abs_path) { - uri - } else { - return Task::ready(Err(anyhow!("invalid symbol path"))); - }; - - self.open_local_buffer_via_lsp(symbol_uri, symbol.source_language_server_id, cx) - } else { - Task::ready(Err(anyhow!("no upstream client or local store"))) - } - } - - pub(crate) fn open_local_buffer_via_lsp( - &mut self, - abs_path: lsp::Uri, - language_server_id: LanguageServerId, - cx: &mut Context, - ) -> Task>> { - cx.spawn(async move |lsp_store, cx| { - // Escape percent-encoded string. - let current_scheme = abs_path.scheme().to_owned(); - // Uri is immutable, so we can't modify the scheme - - let abs_path = abs_path - .to_file_path() - .map_err(|()| anyhow!("can't convert URI to path"))?; - let p = abs_path.clone(); - let yarn_worktree = lsp_store - .update(cx, move |lsp_store, cx| match lsp_store.as_local() { - Some(local_lsp_store) => local_lsp_store.yarn.update(cx, |_, cx| { - cx.spawn(async move |this, cx| { - let t = this - .update(cx, |this, cx| this.process_path(&p, ¤t_scheme, cx)) - .ok()?; - t.await - }) - }), - None => Task::ready(None), - })? - .await; - let (worktree_root_target, known_relative_path) = - if let Some((zip_root, relative_path)) = yarn_worktree { - (zip_root, Some(relative_path)) - } else { - (Arc::::from(abs_path.as_path()), None) - }; - let (worktree, relative_path) = if let Some(result) = - lsp_store.update(cx, |lsp_store, cx| { - lsp_store.worktree_store.update(cx, |worktree_store, cx| { - worktree_store.find_worktree(&worktree_root_target, cx) - }) - })? { - let relative_path = known_relative_path.unwrap_or_else(|| result.1.clone()); - (result.0, relative_path) - } else { - let worktree = lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.worktree_store.update(cx, |worktree_store, cx| { - worktree_store.create_worktree(&worktree_root_target, false, cx) - }) - })? - .await?; - 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, - ) - } - }) - .ok(); - } - let worktree_root = worktree.read_with(cx, |worktree, _| worktree.abs_path())?; - let relative_path = if let Some(known_path) = known_relative_path { - known_path - } else { - RelPath::new(abs_path.strip_prefix(worktree_root)?, PathStyle::local())? - .into_arc() - }; - (worktree, relative_path) - }; - let project_path = ProjectPath { - worktree_id: worktree.read_with(cx, |worktree, _| worktree.id())?, - path: relative_path, - }; - lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(project_path, cx) - }) - })? - .await - }) - } - - fn request_multiple_lsp_locally( - &mut self, - buffer: &Entity, - position: Option

, - request: R, - cx: &mut Context, - ) -> Task> - where - P: ToOffset, - R: LspCommand + Clone, - ::Result: Send, - ::Params: Send, - { - let Some(local) = self.as_local() else { - return Task::ready(Vec::new()); - }; - - let snapshot = buffer.read(cx).snapshot(); - let scope = position.and_then(|position| snapshot.language_scope_at(position)); - - let server_ids = buffer.update(cx, |buffer, cx| { - local - .language_servers_for_buffer(buffer, cx) - .filter(|(adapter, _)| { - scope - .as_ref() - .map(|scope| scope.language_allowed(&adapter.name)) - .unwrap_or(true) - }) - .map(|(_, server)| server.server_id()) - .filter(|server_id| { - self.as_local().is_none_or(|local| { - local - .buffers_opened_in_servers - .get(&snapshot.remote_id()) - .is_some_and(|servers| servers.contains(server_id)) - }) - }) - .collect::>() - }); - - let mut response_results = server_ids - .into_iter() - .map(|server_id| { - let task = self.request_lsp( - buffer.clone(), - LanguageServerToQuery::Other(server_id), - request.clone(), - cx, - ); - async move { (server_id, task.await) } - }) - .collect::>(); - - cx.background_spawn(async move { - let mut responses = Vec::with_capacity(response_results.len()); - while let Some((server_id, response_result)) = response_results.next().await { - match response_result { - Ok(response) => responses.push((server_id, response)), - // rust-analyzer likes to error with this when its still loading up - Err(e) if format!("{e:#}").ends_with("content modified") => (), - Err(e) => log::error!("Error handling response for request {request:?}: {e:#}"), - } - } - responses - }) - } - - async fn handle_lsp_get_completions( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let sender_id = envelope.original_sender_id().unwrap_or_default(); - - let buffer_id = GetCompletions::buffer_id_from_proto(&envelope.payload)?; - let buffer_handle = this.update(&mut cx, |this, cx| { - this.buffer_store.read(cx).get_existing(buffer_id) - })??; - let request = GetCompletions::from_proto( - envelope.payload, - this.clone(), - buffer_handle.clone(), - cx.clone(), - ) - .await?; - - let server_to_query = match request.server_id { - Some(server_id) => LanguageServerToQuery::Other(server_id), - None => LanguageServerToQuery::FirstCapable, - }; - - let response = this - .update(&mut cx, |this, cx| { - this.request_lsp(buffer_handle.clone(), server_to_query, request, cx) - })? - .await?; - this.update(&mut cx, |this, cx| { - Ok(GetCompletions::response_to_proto( - response, - this, - sender_id, - &buffer_handle.read(cx).version(), - cx, - )) - })? - } - - async fn handle_lsp_command( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<::Response> - where - ::Params: Send, - ::Result: Send, - { - let sender_id = envelope.original_sender_id().unwrap_or_default(); - let buffer_id = T::buffer_id_from_proto(&envelope.payload)?; - let buffer_handle = this.update(&mut cx, |this, cx| { - this.buffer_store.read(cx).get_existing(buffer_id) - })??; - let request = T::from_proto( - envelope.payload, - this.clone(), - buffer_handle.clone(), - cx.clone(), - ) - .await?; - let response = this - .update(&mut cx, |this, cx| { - this.request_lsp( - buffer_handle.clone(), - LanguageServerToQuery::FirstCapable, - request, - cx, - ) - })? - .await?; - this.update(&mut cx, |this, cx| { - Ok(T::response_to_proto( - response, - this, - sender_id, - &buffer_handle.read(cx).version(), - cx, - )) - })? - } - - 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); - let server_id = lsp_query.server_id.map(LanguageServerId::from_proto); - 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, - server_id, - sender_id, - lsp_request_id, - get_references, - position, - &mut cx, - ) - .await?; - } - Request::GetDocumentColor(get_document_color) => { - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - get_document_color, - None, - &mut cx, - ) - .await?; - } - Request::GetHover(get_hover) => { - let position = get_hover.position.clone().and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - get_hover, - position, - &mut cx, - ) - .await?; - } - Request::GetCodeActions(get_code_actions) => { - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - get_code_actions, - None, - &mut cx, - ) - .await?; - } - Request::GetSignatureHelp(get_signature_help) => { - let position = get_signature_help - .position - .clone() - .and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - get_signature_help, - position, - &mut cx, - ) - .await?; - } - Request::GetCodeLens(get_code_lens) => { - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - get_code_lens, - None, - &mut cx, - ) - .await?; - } - Request::GetDefinition(get_definition) => { - let position = get_definition.position.clone().and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - get_definition, - position, - &mut cx, - ) - .await?; - } - Request::GetDeclaration(get_declaration) => { - let position = get_declaration - .position - .clone() - .and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - get_declaration, - position, - &mut cx, - ) - .await?; - } - Request::GetTypeDefinition(get_type_definition) => { - let position = get_type_definition - .position - .clone() - .and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - get_type_definition, - position, - &mut cx, - ) - .await?; - } - Request::GetImplementation(get_implementation) => { - let position = get_implementation - .position - .clone() - .and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - get_implementation, - position, - &mut cx, - ) - .await?; - } - 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 lsp_data = lsp_store.latest_lsp_data(&buffer, cx); - let key = LspKey { - request_type: TypeId::of::(), - server_queried: server_id, - }; - if ::ProtoRequest::stop_previous_requests( - ) { - if let Some(lsp_requests) = lsp_data.lsp_requests.get_mut(&key) { - lsp_requests.clear(); - }; - } - - let existing_queries = lsp_data.lsp_requests.entry(key).or_default(); - existing_queries.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:#}"), - }; - } - }), - ); - })?; - } - Request::InlayHints(inlay_hints) => { - let query_start = inlay_hints - .start - .clone() - .and_then(deserialize_anchor) - .context("invalid inlay hints range start")?; - let query_end = inlay_hints - .end - .clone() - .and_then(deserialize_anchor) - .context("invalid inlay hints range end")?; - Self::deduplicate_range_based_lsp_requests::( - &lsp_store, - server_id, - lsp_request_id, - &inlay_hints, - query_start..query_end, - &mut cx, - ) - .await - .context("preparing inlay hints request")?; - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - inlay_hints, - None, - &mut cx, - ) - .await - .context("querying for inlay hints")? - } - } - 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(()) - } - - async fn handle_apply_code_action( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let sender_id = envelope.original_sender_id().unwrap_or_default(); - let action = - Self::deserialize_code_action(envelope.payload.action.context("invalid action")?)?; - let apply_code_action = this.update(&mut cx, |this, cx| { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let buffer = this.buffer_store.read(cx).get_existing(buffer_id)?; - anyhow::Ok(this.apply_code_action(buffer, action, false, cx)) - })??; - - let project_transaction = apply_code_action.await?; - let project_transaction = this.update(&mut cx, |this, cx| { - this.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.serialize_project_transaction_for_peer( - project_transaction, - sender_id, - cx, - ) - }) - })?; - Ok(proto::ApplyCodeActionResponse { - transaction: Some(project_transaction), - }) - } - - async fn handle_register_buffer_with_language_servers( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let peer_id = envelope.original_sender_id.unwrap_or(envelope.sender_id); - this.update(&mut cx, |this, cx| { - if let Some((upstream_client, upstream_project_id)) = this.upstream_client() { - return upstream_client.send(proto::RegisterBufferWithLanguageServers { - project_id: upstream_project_id, - buffer_id: buffer_id.to_proto(), - only_servers: envelope.payload.only_servers, - }); - } - - let Some(buffer) = this.buffer_store().read(cx).get(buffer_id) else { - anyhow::bail!("buffer is not open"); - }; - - let handle = this.register_buffer_with_language_servers( - &buffer, - envelope - .payload - .only_servers - .into_iter() - .filter_map(|selector| { - Some(match selector.selector? { - proto::language_server_selector::Selector::ServerId(server_id) => { - LanguageServerSelector::Id(LanguageServerId::from_proto(server_id)) - } - proto::language_server_selector::Selector::Name(name) => { - LanguageServerSelector::Name(LanguageServerName( - SharedString::from(name), - )) - } - }) - }) - .collect(), - false, - cx, - ); - this.buffer_store().update(cx, |buffer_store, _| { - buffer_store.register_shared_lsp_handle(peer_id, buffer_id, handle); - }); - - Ok(()) - })??; - Ok(proto::Ack {}) - } - - async fn handle_rename_project_entry( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id); - let new_worktree_id = WorktreeId::from_proto(envelope.payload.new_worktree_id); - let new_path = - RelPath::from_proto(&envelope.payload.new_path).context("invalid relative path")?; - - let (worktree_store, old_worktree, new_worktree, old_entry) = this - .update(&mut cx, |this, cx| { - let (worktree, entry) = this - .worktree_store - .read(cx) - .worktree_and_entry_for_id(entry_id, cx)?; - let new_worktree = this - .worktree_store - .read(cx) - .worktree_for_id(new_worktree_id, cx)?; - Some(( - this.worktree_store.clone(), - worktree, - new_worktree, - entry.clone(), - )) - })? - .context("worktree not found")?; - let (old_abs_path, old_worktree_id) = old_worktree.read_with(&cx, |worktree, _| { - (worktree.absolutize(&old_entry.path), worktree.id()) - })?; - let new_abs_path = - new_worktree.read_with(&cx, |worktree, _| worktree.absolutize(&new_path))?; - - let _transaction = Self::will_rename_entry( - this.downgrade(), - old_worktree_id, - &old_abs_path, - &new_abs_path, - old_entry.is_dir(), - cx.clone(), - ) - .await; - let response = WorktreeStore::handle_rename_project_entry( - worktree_store, - envelope.payload, - cx.clone(), - ) - .await; - this.read_with(&cx, |this, _| { - this.did_rename_entry( - old_worktree_id, - &old_abs_path, - &new_abs_path, - old_entry.is_dir(), - ); - }) - .ok(); - response - } - - async fn handle_update_diagnostic_summary( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |lsp_store, 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) - { - let project_path = ProjectPath { - worktree_id, - path: RelPath::from_proto(&message_summary.path).context("invalid path")?, - }; - let path = project_path.path.clone(); - let server_id = LanguageServerId(message_summary.language_server_id as usize); - let summary = DiagnosticSummary { - error_count: message_summary.error_count as usize, - warning_count: message_summary.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) - { - summaries.remove(&server_id); - if summaries.is_empty() { - worktree_summaries.remove(&path); - } - } - } else { - lsp_store - .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(), - }) - } - } - } - 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 }); - } - Ok(()) - })? - } - - async fn handle_start_language_server( - lsp_store: 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| { - 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( - server_id, - LanguageServerStatus { - name: server_name.clone(), - pending_work: Default::default(), - has_pending_diagnostic_updates: false, - progress_tokens: Default::default(), - worktree: server.worktree_id.map(WorktreeId::from_proto), - binary: None, - configuration: None, - workspace_folders: BTreeSet::new(), - }, - ); - cx.emit(LspStoreEvent::LanguageServerAdded( - server_id, - server_name, - server.worktree_id.map(WorktreeId::from_proto), - )); - cx.notify(); - })?; - Ok(()) - } - - async fn handle_update_language_server( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - lsp_store.update(&mut cx, |lsp_store, cx| { - let language_server_id = LanguageServerId(envelope.payload.language_server_id as usize); - - match envelope.payload.variant.context("invalid variant")? { - proto::update_language_server::Variant::WorkStart(payload) => { - lsp_store.on_lsp_work_start( - language_server_id, - ProgressToken::from_proto(payload.token.context("missing progress token")?) - .context("invalid progress token value")?, - LanguageServerProgress { - title: payload.title, - is_disk_based_diagnostics_progress: false, - is_cancellable: payload.is_cancellable.unwrap_or(false), - message: payload.message, - percentage: payload.percentage.map(|p| p as usize), - last_update_at: cx.background_executor().now(), - }, - cx, - ); - } - proto::update_language_server::Variant::WorkProgress(payload) => { - lsp_store.on_lsp_work_progress( - language_server_id, - ProgressToken::from_proto(payload.token.context("missing progress token")?) - .context("invalid progress token value")?, - LanguageServerProgress { - title: None, - is_disk_based_diagnostics_progress: false, - is_cancellable: payload.is_cancellable.unwrap_or(false), - message: payload.message, - percentage: payload.percentage.map(|p| p as usize), - last_update_at: cx.background_executor().now(), - }, - cx, - ); - } - - proto::update_language_server::Variant::WorkEnd(payload) => { - lsp_store.on_lsp_work_end( - language_server_id, - ProgressToken::from_proto(payload.token.context("missing progress token")?) - .context("invalid progress token value")?, - cx, - ); - } - - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(_) => { - lsp_store.disk_based_diagnostics_started(language_server_id, cx); - } - - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(_) => { - lsp_store.disk_based_diagnostics_finished(language_server_id, cx) - } - - non_lsp @ proto::update_language_server::Variant::StatusUpdate(_) - | non_lsp @ proto::update_language_server::Variant::RegisteredForBuffer(_) - | non_lsp @ proto::update_language_server::Variant::MetadataUpdated(_) => { - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id, - name: envelope - .payload - .server_name - .map(SharedString::new) - .map(LanguageServerName), - message: non_lsp, - }); - } - } - - Ok(()) - })? - } - - async fn handle_language_server_log( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let language_server_id = LanguageServerId(envelope.payload.language_server_id as usize); - let log_type = envelope - .payload - .log_type - .map(LanguageServerLogType::from_proto) - .context("invalid language server log type")?; - - let message = envelope.payload.message; - - this.update(&mut cx, |_, cx| { - cx.emit(LspStoreEvent::LanguageServerLog( - language_server_id, - log_type, - message, - )); - }) - } - - async fn handle_lsp_ext_cancel_flycheck( - lsp_store: Entity, - envelope: TypedEnvelope, - cx: AsyncApp, - ) -> Result { - let server_id = LanguageServerId(envelope.payload.language_server_id as usize); - let task = lsp_store.read_with(&cx, |lsp_store, _| { - if let Some(server) = lsp_store.language_server_for_id(server_id) { - Some(server.notify::(())) - } else { - None - } - })?; - if let Some(task) = task { - task.context("handling lsp ext cancel flycheck")?; - } - - Ok(proto::Ack {}) - } - - async fn handle_lsp_ext_run_flycheck( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let server_id = LanguageServerId(envelope.payload.language_server_id as usize); - 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)) - }) - .transpose()? - } else { - None - }; - server.notify::( - lsp_store::lsp_ext_command::RunFlycheckParams { text_document }, - )?; - } - anyhow::Ok(()) - })??; - - Ok(proto::Ack {}) - } - - async fn handle_lsp_ext_clear_flycheck( - lsp_store: Entity, - envelope: TypedEnvelope, - cx: AsyncApp, - ) -> Result { - let server_id = LanguageServerId(envelope.payload.language_server_id as usize); - lsp_store - .read_with(&cx, |lsp_store, _| { - if let Some(server) = lsp_store.language_server_for_id(server_id) { - Some(server.notify::(())) - } else { - None - } - }) - .context("handling lsp ext clear flycheck")?; - - Ok(proto::Ack {}) - } - - pub fn disk_based_diagnostics_started( - &mut self, - language_server_id: LanguageServerId, - cx: &mut Context, - ) { - if let Some(language_server_status) = - self.language_server_statuses.get_mut(&language_server_id) - { - language_server_status.has_pending_diagnostic_updates = true; - } - - cx.emit(LspStoreEvent::DiskBasedDiagnosticsStarted { language_server_id }); - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id, - name: self - .language_server_adapter_for_id(language_server_id) - .map(|adapter| adapter.name()), - message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( - Default::default(), - ), - }) - } - - pub fn disk_based_diagnostics_finished( - &mut self, - language_server_id: LanguageServerId, - cx: &mut Context, - ) { - if let Some(language_server_status) = - self.language_server_statuses.get_mut(&language_server_id) - { - language_server_status.has_pending_diagnostic_updates = false; - } - - cx.emit(LspStoreEvent::DiskBasedDiagnosticsFinished { language_server_id }); - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id, - name: self - .language_server_adapter_for_id(language_server_id) - .map(|adapter| adapter.name()), - message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( - Default::default(), - ), - }) - } - - // After saving a buffer using a language server that doesn't provide a disk-based progress token, - // kick off a timer that will reset every time the buffer is saved. If the timer eventually fires, - // simulate disk-based diagnostics being finished so that other pieces of UI (e.g., project - // diagnostics view, diagnostic status bar) can update. We don't emit an event right away because - // the language server might take some time to publish diagnostics. - fn simulate_disk_based_diagnostics_events_if_needed( - &mut self, - language_server_id: LanguageServerId, - cx: &mut Context, - ) { - const DISK_BASED_DIAGNOSTICS_DEBOUNCE: Duration = Duration::from_secs(1); - - let Some(LanguageServerState::Running { - simulate_disk_based_diagnostics_completion, - adapter, - .. - }) = self - .as_local_mut() - .and_then(|local_store| local_store.language_servers.get_mut(&language_server_id)) - else { - return; - }; - - if adapter.disk_based_diagnostics_progress_token.is_some() { - return; - } - - let prev_task = - simulate_disk_based_diagnostics_completion.replace(cx.spawn(async move |this, cx| { - cx.background_executor() - .timer(DISK_BASED_DIAGNOSTICS_DEBOUNCE) - .await; - - this.update(cx, |this, cx| { - this.disk_based_diagnostics_finished(language_server_id, cx); - - if let Some(LanguageServerState::Running { - simulate_disk_based_diagnostics_completion, - .. - }) = this.as_local_mut().and_then(|local_store| { - local_store.language_servers.get_mut(&language_server_id) - }) { - *simulate_disk_based_diagnostics_completion = None; - } - }) - .ok(); - })); - - if prev_task.is_none() { - self.disk_based_diagnostics_started(language_server_id, cx); - } - } - - pub fn language_server_statuses( - &self, - ) -> impl DoubleEndedIterator { - self.language_server_statuses - .iter() - .map(|(key, value)| (*key, value)) - } - - pub(super) fn did_rename_entry( - &self, - worktree_id: WorktreeId, - old_path: &Path, - new_path: &Path, - is_dir: bool, - ) { - maybe!({ - let local_store = self.as_local()?; - - let old_uri = lsp::Uri::from_file_path(old_path) - .ok() - .map(|uri| uri.to_string())?; - let new_uri = lsp::Uri::from_file_path(new_path) - .ok() - .map(|uri| uri.to_string())?; - - for language_server in local_store.language_servers_for_worktree(worktree_id) { - let Some(filter) = local_store - .language_server_paths_watched_for_rename - .get(&language_server.server_id()) - else { - continue; - }; - - if filter.should_send_did_rename(&old_uri, is_dir) { - language_server - .notify::(RenameFilesParams { - files: vec![FileRename { - old_uri: old_uri.clone(), - new_uri: new_uri.clone(), - }], - }) - .ok(); - } - } - Some(()) - }); - } - - pub(super) fn will_rename_entry( - this: WeakEntity, - worktree_id: WorktreeId, - old_path: &Path, - new_path: &Path, - is_dir: bool, - cx: AsyncApp, - ) -> Task { - let old_uri = lsp::Uri::from_file_path(old_path) - .ok() - .map(|uri| uri.to_string()); - let new_uri = lsp::Uri::from_file_path(new_path) - .ok() - .map(|uri| uri.to_string()); - cx.spawn(async move |cx| { - let mut tasks = vec![]; - this.update(cx, |this, cx| { - let local_store = this.as_local()?; - let old_uri = old_uri?; - let new_uri = new_uri?; - for language_server in local_store.language_servers_for_worktree(worktree_id) { - let Some(filter) = local_store - .language_server_paths_watched_for_rename - .get(&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(); - let new_uri = new_uri.clone(); - let language_server = language_server.clone(); - async move |this, cx| { - let edit = language_server - .request::(RenameFilesParams { - files: vec![FileRename { old_uri, new_uri }], - }) - .await - .into_response() - .context("will rename files") - .log_err() - .flatten()?; - - let transaction = LocalLspStore::deserialize_workspace_edit( - this.upgrade()?, - edit, - false, - language_server.clone(), - cx, - ) - .await - .ok()?; - Some(transaction) - } - }); - tasks.push(apply_edit); - } - } - Some(()) - }) - .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); - } - } - } - merged_transaction - }) - } - - fn lsp_notify_abs_paths_changed( - &mut self, - server_id: LanguageServerId, - changes: Vec, - ) { - maybe!({ - let server = self.language_server_for_id(server_id)?; - let changes = changes - .into_iter() - .filter_map(|event| { - let typ = match event.kind? { - PathEventKind::Created => lsp::FileChangeType::CREATED, - PathEventKind::Removed => lsp::FileChangeType::DELETED, - PathEventKind::Changed => lsp::FileChangeType::CHANGED, - }; - Some(lsp::FileEvent { - uri: file_path_to_lsp_url(&event.path).log_err()?, - typ, - }) - }) - .collect::>(); - if !changes.is_empty() { - server - .notify::( - lsp::DidChangeWatchedFilesParams { changes }, - ) - .ok(); - } - Some(()) - }); - } - - pub fn language_server_for_id(&self, id: LanguageServerId) -> Option> { - self.as_local()?.language_server_for_id(id) - } - - fn on_lsp_progress( - &mut self, - progress_params: lsp::ProgressParams, - language_server_id: LanguageServerId, - disk_based_diagnostics_progress_token: Option, - cx: &mut Context, - ) { - match progress_params.value { - lsp::ProgressParamsValue::WorkDone(progress) => { - self.handle_work_done_progress( - progress, - language_server_id, - disk_based_diagnostics_progress_token, - ProgressToken::from_lsp(progress_params.token), - cx, - ); - } - lsp::ProgressParamsValue::WorkspaceDiagnostic(report) => { - let registration_id = match progress_params.token { - lsp::NumberOrString::Number(_) => None, - lsp::NumberOrString::String(token) => token - .split_once(WORKSPACE_DIAGNOSTICS_TOKEN_START) - .map(|(_, id)| id.to_owned()), - }; - if let Some(LanguageServerState::Running { - workspace_diagnostics_refresh_tasks, - .. - }) = self - .as_local_mut() - .and_then(|local| local.language_servers.get_mut(&language_server_id)) - && let Some(workspace_diagnostics) = - workspace_diagnostics_refresh_tasks.get_mut(®istration_id) - { - workspace_diagnostics.progress_tx.try_send(()).ok(); - self.apply_workspace_diagnostic_report( - language_server_id, - report, - registration_id.map(SharedString::from), - cx, - ) - } - } - } - } - - fn handle_work_done_progress( - &mut self, - progress: lsp::WorkDoneProgress, - language_server_id: LanguageServerId, - disk_based_diagnostics_progress_token: Option, - token: ProgressToken, - cx: &mut Context, - ) { - let language_server_status = - if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { - status - } else { - return; - }; - - if !language_server_status.progress_tokens.contains(&token) { - return; - } - - let is_disk_based_diagnostics_progress = - if let (Some(disk_based_token), ProgressToken::String(token)) = - (&disk_based_diagnostics_progress_token, &token) - { - token.starts_with(disk_based_token) - } else { - false - }; - - match progress { - lsp::WorkDoneProgress::Begin(report) => { - if is_disk_based_diagnostics_progress { - self.disk_based_diagnostics_started(language_server_id, cx); - } - self.on_lsp_work_start( - language_server_id, - token.clone(), - LanguageServerProgress { - title: Some(report.title), - is_disk_based_diagnostics_progress, - is_cancellable: report.cancellable.unwrap_or(false), - message: report.message.clone(), - percentage: report.percentage.map(|p| p as usize), - last_update_at: cx.background_executor().now(), - }, - cx, - ); - } - lsp::WorkDoneProgress::Report(report) => self.on_lsp_work_progress( - language_server_id, - token, - LanguageServerProgress { - title: None, - is_disk_based_diagnostics_progress, - is_cancellable: report.cancellable.unwrap_or(false), - message: report.message, - percentage: report.percentage.map(|p| p as usize), - last_update_at: cx.background_executor().now(), - }, - cx, - ), - lsp::WorkDoneProgress::End(_) => { - language_server_status.progress_tokens.remove(&token); - self.on_lsp_work_end(language_server_id, token.clone(), cx); - if is_disk_based_diagnostics_progress { - self.disk_based_diagnostics_finished(language_server_id, cx); - } - } - } - } - - fn on_lsp_work_start( - &mut self, - language_server_id: LanguageServerId, - token: ProgressToken, - progress: LanguageServerProgress, - cx: &mut Context, - ) { - if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { - status.pending_work.insert(token.clone(), progress.clone()); - cx.notify(); - } - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id, - name: self - .language_server_adapter_for_id(language_server_id) - .map(|adapter| adapter.name()), - message: proto::update_language_server::Variant::WorkStart(proto::LspWorkStart { - token: Some(token.to_proto()), - title: progress.title, - message: progress.message, - percentage: progress.percentage.map(|p| p as u32), - is_cancellable: Some(progress.is_cancellable), - }), - }) - } - - fn on_lsp_work_progress( - &mut self, - language_server_id: LanguageServerId, - token: ProgressToken, - progress: LanguageServerProgress, - cx: &mut Context, - ) { - let mut did_update = false; - if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { - match status.pending_work.entry(token.clone()) { - btree_map::Entry::Vacant(entry) => { - entry.insert(progress.clone()); - did_update = true; - } - btree_map::Entry::Occupied(mut entry) => { - let entry = entry.get_mut(); - if (progress.last_update_at - entry.last_update_at) - >= SERVER_PROGRESS_THROTTLE_TIMEOUT - { - entry.last_update_at = progress.last_update_at; - if progress.message.is_some() { - entry.message = progress.message.clone(); - } - if progress.percentage.is_some() { - entry.percentage = progress.percentage; - } - if progress.is_cancellable != entry.is_cancellable { - entry.is_cancellable = progress.is_cancellable; - } - did_update = true; - } - } - } - } - - if did_update { - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id, - name: self - .language_server_adapter_for_id(language_server_id) - .map(|adapter| adapter.name()), - message: proto::update_language_server::Variant::WorkProgress( - proto::LspWorkProgress { - token: Some(token.to_proto()), - message: progress.message, - percentage: progress.percentage.map(|p| p as u32), - is_cancellable: Some(progress.is_cancellable), - }, - ), - }) - } - } - - fn on_lsp_work_end( - &mut self, - language_server_id: LanguageServerId, - token: ProgressToken, - 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 { - server_id: language_server_id, - request_id: None, - }); - } - cx.notify(); - } - - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id, - name: self - .language_server_adapter_for_id(language_server_id) - .map(|adapter| adapter.name()), - message: proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd { - token: Some(token.to_proto()), - }), - }) - } - - pub async fn handle_resolve_completion_documentation( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let lsp_completion = serde_json::from_slice(&envelope.payload.lsp_completion)?; - - let completion = this - .read_with(&cx, |this, cx| { - let id = LanguageServerId(envelope.payload.language_server_id as usize); - let server = this - .language_server_for_id(id) - .with_context(|| format!("No language server {id}"))?; - - anyhow::Ok(cx.background_spawn(async move { - let can_resolve = server - .capabilities() - .completion_provider - .as_ref() - .and_then(|options| options.resolve_provider) - .unwrap_or(false); - if can_resolve { - server - .request::(lsp_completion) - .await - .into_response() - .context("resolve completion item") - } else { - anyhow::Ok(lsp_completion) - } - })) - })?? - .await?; - - let mut documentation_is_markdown = false; - let lsp_completion = serde_json::to_string(&completion)?.into_bytes(); - let documentation = match completion.documentation { - Some(lsp::Documentation::String(text)) => text, - - Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value })) => { - documentation_is_markdown = kind == lsp::MarkupKind::Markdown; - value - } - - _ => String::new(), - }; - - // If we have a new buffer_id, that means we're talking to a new client - // and want to check for new text_edits in the completion too. - let mut old_replace_start = None; - let mut old_replace_end = None; - let mut old_insert_start = None; - let mut old_insert_end = None; - let mut new_text = String::default(); - if let Ok(buffer_id) = BufferId::new(envelope.payload.buffer_id) { - let buffer_snapshot = this.update(&mut cx, |this, cx| { - let buffer = this.buffer_store.read(cx).get_existing(buffer_id)?; - anyhow::Ok(buffer.read(cx).snapshot()) - })??; - - if let Some(text_edit) = completion.text_edit.as_ref() { - let edit = parse_completion_text_edit(text_edit, &buffer_snapshot); - - if let Some(mut edit) = edit { - LineEnding::normalize(&mut edit.new_text); - - new_text = edit.new_text; - old_replace_start = Some(serialize_anchor(&edit.replace_range.start)); - old_replace_end = Some(serialize_anchor(&edit.replace_range.end)); - if let Some(insert_range) = edit.insert_range { - old_insert_start = Some(serialize_anchor(&insert_range.start)); - old_insert_end = Some(serialize_anchor(&insert_range.end)); - } - } - } - } - - Ok(proto::ResolveCompletionDocumentationResponse { - documentation, - documentation_is_markdown, - old_replace_start, - old_replace_end, - new_text, - lsp_completion, - old_insert_start, - old_insert_end, - }) - } - - async fn handle_on_type_formatting( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let on_type_formatting = this.update(&mut cx, |this, cx| { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let buffer = this.buffer_store.read(cx).get_existing(buffer_id)?; - let position = envelope - .payload - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - anyhow::Ok(this.apply_on_type_formatting( - buffer, - position, - envelope.payload.trigger.clone(), - cx, - )) - })??; - - let transaction = on_type_formatting - .await? - .as_ref() - .map(language::proto::serialize_transaction); - Ok(proto::OnTypeFormattingResponse { transaction }) - } - - async fn handle_refresh_inlay_hints( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - lsp_store.update(&mut cx, |_, cx| { - cx.emit(LspStoreEvent::RefreshInlayHints { - server_id: LanguageServerId::from_proto(envelope.payload.server_id), - request_id: envelope.payload.request_id.map(|id| id as usize), - }); - })?; - Ok(proto::Ack {}) - } - - async fn handle_pull_workspace_diagnostics( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let server_id = LanguageServerId::from_proto(envelope.payload.server_id); - lsp_store.update(&mut cx, |lsp_store, _| { - lsp_store.pull_workspace_diagnostics(server_id); - })?; - Ok(proto::Ack {}) - } - - async fn handle_get_color_presentation( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let buffer = lsp_store.update(&mut cx, |lsp_store, cx| { - lsp_store.buffer_store.read(cx).get_existing(buffer_id) - })??; - - let color = envelope - .payload - .color - .context("invalid color resolve request")?; - let start = color - .lsp_range_start - .context("invalid color resolve request")?; - let end = color - .lsp_range_end - .context("invalid color resolve request")?; - - let color = DocumentColor { - lsp_range: lsp::Range { - start: point_to_lsp(PointUtf16::new(start.row, start.column)), - end: point_to_lsp(PointUtf16::new(end.row, end.column)), - }, - color: lsp::Color { - red: color.red, - green: color.green, - blue: color.blue, - alpha: color.alpha, - }, - resolved: false, - color_presentations: Vec::new(), - }; - let resolved_color = lsp_store - .update(&mut cx, |lsp_store, cx| { - lsp_store.resolve_color_presentation( - color, - buffer.clone(), - LanguageServerId(envelope.payload.server_id as usize), - cx, - ) - })? - .await - .context("resolving color presentation")?; - - Ok(proto::GetColorPresentationResponse { - presentations: resolved_color - .color_presentations - .into_iter() - .map(|presentation| proto::ColorPresentation { - label: presentation.label.to_string(), - text_edit: presentation.text_edit.map(serialize_lsp_edit), - additional_text_edits: presentation - .additional_text_edits - .into_iter() - .map(serialize_lsp_edit) - .collect(), - }) - .collect(), - }) - } - - async fn handle_resolve_inlay_hint( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let proto_hint = envelope - .payload - .hint - .expect("incorrect protobuf resolve inlay hint message: missing the inlay hint"); - let hint = InlayHints::proto_to_project_hint(proto_hint) - .context("resolved proto inlay hint conversion")?; - let buffer = lsp_store.update(&mut cx, |lsp_store, cx| { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - lsp_store.buffer_store.read(cx).get_existing(buffer_id) - })??; - let response_hint = lsp_store - .update(&mut cx, |lsp_store, cx| { - lsp_store.resolve_inlay_hint( - hint, - buffer, - LanguageServerId(envelope.payload.language_server_id as usize), - cx, - ) - })? - .await - .context("inlay hints fetch")?; - Ok(proto::ResolveInlayHintResponse { - hint: Some(InlayHints::project_to_proto_hint(response_hint)), - }) - } - - async fn handle_refresh_code_lens( - this: Entity, - _: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - this.update(&mut cx, |_, cx| { - cx.emit(LspStoreEvent::RefreshCodeLens); - })?; - Ok(proto::Ack {}) - } - - async fn handle_open_buffer_for_symbol( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let peer_id = envelope.original_sender_id().unwrap_or_default(); - let symbol = envelope.payload.symbol.context("invalid symbol")?; - let symbol = Self::deserialize_symbol(symbol)?; - this.read_with(&cx, |this, _| { - if let SymbolLocation::OutsideProject { - abs_path, - signature, - } = &symbol.path - { - let new_signature = this.symbol_signature(&abs_path); - anyhow::ensure!(&new_signature == signature, "invalid symbol signature"); - } - Ok(()) - })??; - let buffer = this - .update(&mut cx, |this, cx| { - this.open_buffer_for_symbol( - &Symbol { - language_server_name: symbol.language_server_name, - source_worktree_id: symbol.source_worktree_id, - source_language_server_id: symbol.source_language_server_id, - path: symbol.path, - name: symbol.name, - kind: symbol.kind, - range: symbol.range, - label: CodeLabel::default(), - }, - cx, - ) - })? - .await?; - - this.update(&mut cx, |this, cx| { - let is_private = buffer - .read(cx) - .file() - .map(|f| f.is_private()) - .unwrap_or_default(); - if is_private { - Err(anyhow!(rpc::ErrorCode::UnsharedItem)) - } else { - this.buffer_store - .update(cx, |buffer_store, cx| { - buffer_store.create_buffer_for_peer(&buffer, peer_id, cx) - }) - .detach_and_log_err(cx); - let buffer_id = buffer.read(cx).remote_id().to_proto(); - Ok(proto::OpenBufferForSymbolResponse { buffer_id }) - } - })? - } - - fn symbol_signature(&self, abs_path: &Path) -> [u8; 32] { - let mut hasher = Sha256::new(); - hasher.update(abs_path.to_string_lossy().as_bytes()); - hasher.update(self.nonce.to_be_bytes()); - hasher.finalize().as_slice().try_into().unwrap() - } - - pub async fn handle_get_project_symbols( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let symbols = this - .update(&mut cx, |this, cx| { - this.symbols(&envelope.payload.query, cx) - })? - .await?; - - Ok(proto::GetProjectSymbolsResponse { - symbols: symbols.iter().map(Self::serialize_symbol).collect(), - }) - } - - pub async fn handle_restart_language_servers( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - this.update(&mut cx, |lsp_store, cx| { - let buffers = - lsp_store.buffer_ids_to_buffers(envelope.payload.buffer_ids.into_iter(), cx); - lsp_store.restart_language_servers_for_buffers( - buffers, - envelope - .payload - .only_servers - .into_iter() - .filter_map(|selector| { - Some(match selector.selector? { - proto::language_server_selector::Selector::ServerId(server_id) => { - LanguageServerSelector::Id(LanguageServerId::from_proto(server_id)) - } - proto::language_server_selector::Selector::Name(name) => { - LanguageServerSelector::Name(LanguageServerName( - SharedString::from(name), - )) - } - }) - }) - .collect(), - cx, - ); - })?; - - Ok(proto::Ack {}) - } - - pub async fn handle_stop_language_servers( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - lsp_store.update(&mut cx, |lsp_store, cx| { - if envelope.payload.all - && envelope.payload.also_servers.is_empty() - && envelope.payload.buffer_ids.is_empty() - { - lsp_store.stop_all_language_servers(cx); - } else { - let buffers = - lsp_store.buffer_ids_to_buffers(envelope.payload.buffer_ids.into_iter(), cx); - lsp_store - .stop_language_servers_for_buffers( - buffers, - envelope - .payload - .also_servers - .into_iter() - .filter_map(|selector| { - Some(match selector.selector? { - proto::language_server_selector::Selector::ServerId( - server_id, - ) => LanguageServerSelector::Id(LanguageServerId::from_proto( - server_id, - )), - proto::language_server_selector::Selector::Name(name) => { - LanguageServerSelector::Name(LanguageServerName( - SharedString::from(name), - )) - } - }) - }) - .collect(), - cx, - ) - .detach_and_log_err(cx); - } - })?; - - Ok(proto::Ack {}) - } - - pub async fn handle_cancel_language_server_work( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - lsp_store.update(&mut cx, |lsp_store, cx| { - if let Some(work) = envelope.payload.work { - match work { - proto::cancel_language_server_work::Work::Buffers(buffers) => { - let buffers = - lsp_store.buffer_ids_to_buffers(buffers.buffer_ids.into_iter(), cx); - lsp_store.cancel_language_server_work_for_buffers(buffers, cx); - } - proto::cancel_language_server_work::Work::LanguageServerWork(work) => { - let server_id = LanguageServerId::from_proto(work.language_server_id); - let token = work - .token - .map(|token| { - ProgressToken::from_proto(token) - .context("invalid work progress token") - }) - .transpose()?; - lsp_store.cancel_language_server_work(server_id, token, cx); - } - } - } - anyhow::Ok(()) - })??; - - Ok(proto::Ack {}) - } - - fn buffer_ids_to_buffers( - &mut self, - buffer_ids: impl Iterator, - cx: &mut Context, - ) -> Vec> { - buffer_ids - .into_iter() - .flat_map(|buffer_id| { - self.buffer_store - .read(cx) - .get(BufferId::new(buffer_id).log_err()?) - }) - .collect::>() - } - - async fn handle_apply_additional_edits_for_completion( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let (buffer, completion) = this.update(&mut cx, |this, cx| { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let buffer = this.buffer_store.read(cx).get_existing(buffer_id)?; - let completion = Self::deserialize_completion( - envelope.payload.completion.context("invalid completion")?, - )?; - anyhow::Ok((buffer, completion)) - })??; - - let apply_additional_edits = this.update(&mut cx, |this, cx| { - this.apply_additional_edits_for_completion( - buffer, - Rc::new(RefCell::new(Box::new([Completion { - replace_range: completion.replace_range, - new_text: completion.new_text, - source: completion.source, - documentation: None, - label: CodeLabel::default(), - match_start: None, - snippet_deduplication_key: None, - insert_text_mode: None, - icon_path: None, - confirm: None, - }]))), - 0, - false, - cx, - ) - })?; - - Ok(proto::ApplyCompletionAdditionalEditsResponse { - transaction: apply_additional_edits - .await? - .as_ref() - .map(language::proto::serialize_transaction), - }) - } - - pub fn last_formatting_failure(&self) -> Option<&str> { - self.last_formatting_failure.as_deref() - } - - pub fn reset_last_formatting_failure(&mut self) { - self.last_formatting_failure = None; - } - - pub fn environment_for_buffer( - &self, - buffer: &Entity, - cx: &mut Context, - ) -> Shared>>> { - if let Some(environment) = &self.as_local().map(|local| local.environment.clone()) { - environment.update(cx, |env, cx| { - env.buffer_environment(buffer, &self.worktree_store, cx) - }) - } else { - Task::ready(None).shared() - } - } - - pub fn format( - &mut self, - buffers: HashSet>, - target: LspFormatTarget, - push_to_history: bool, - trigger: FormatTrigger, - cx: &mut Context, - ) -> Task> { - let logger = zlog::scoped!("format"); - if self.as_local().is_some() { - zlog::trace!(logger => "Formatting locally"); - let logger = zlog::scoped!(logger => "local"); - let buffers = buffers - .into_iter() - .map(|buffer_handle| { - let buffer = buffer_handle.read(cx); - let buffer_abs_path = File::from_dyn(buffer.file()) - .and_then(|file| file.as_local().map(|f| f.abs_path(cx))); - - (buffer_handle, buffer_abs_path, buffer.remote_id()) - }) - .collect::>(); - - cx.spawn(async move |lsp_store, cx| { - let mut formattable_buffers = Vec::with_capacity(buffers.len()); - - for (handle, abs_path, id) in buffers { - let env = lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.environment_for_buffer(&handle, cx) - })? - .await; - - let ranges = match &target { - LspFormatTarget::Buffers => None, - LspFormatTarget::Ranges(ranges) => { - Some(ranges.get(&id).context("No format ranges provided for buffer")?.clone()) - } - }; - - formattable_buffers.push(FormattableBuffer { - handle, - abs_path, - env, - ranges, - }); - } - zlog::trace!(logger => "Formatting {:?} buffers", formattable_buffers.len()); - - let format_timer = zlog::time!(logger => "Formatting buffers"); - let result = LocalLspStore::format_locally( - lsp_store.clone(), - formattable_buffers, - push_to_history, - trigger, - logger, - cx, - ) - .await; - format_timer.end(); - - zlog::trace!(logger => "Formatting completed with result {:?}", result.as_ref().map(|_| "")); - - lsp_store.update(cx, |lsp_store, _| { - lsp_store.update_last_formatting_failure(&result); - })?; - - result - }) - } else if let Some((client, project_id)) = self.upstream_client() { - zlog::trace!(logger => "Formatting remotely"); - let logger = zlog::scoped!(logger => "remote"); - // Don't support formatting ranges via remote - match target { - LspFormatTarget::Buffers => {} - LspFormatTarget::Ranges(_) => { - zlog::trace!(logger => "Ignoring unsupported remote range formatting request"); - return Task::ready(Ok(ProjectTransaction::default())); - } - } - - let buffer_store = self.buffer_store(); - cx.spawn(async move |lsp_store, cx| { - zlog::trace!(logger => "Sending remote format request"); - let request_timer = zlog::time!(logger => "remote format request"); - let result = client - .request(proto::FormatBuffers { - project_id, - trigger: trigger as i32, - buffer_ids: buffers - .iter() - .map(|buffer| buffer.read_with(cx, |buffer, _| buffer.remote_id().into())) - .collect::>()?, - }) - .await - .and_then(|result| result.transaction.context("missing transaction")); - request_timer.end(); - - zlog::trace!(logger => "Remote format request resolved to {:?}", result.as_ref().map(|_| "")); - - lsp_store.update(cx, |lsp_store, _| { - lsp_store.update_last_formatting_failure(&result); - })?; - - let transaction_response = result?; - let _timer = zlog::time!(logger => "deserializing project transaction"); - buffer_store - .update(cx, |buffer_store, cx| { - buffer_store.deserialize_project_transaction( - transaction_response, - push_to_history, - cx, - ) - })? - .await - }) - } else { - zlog::trace!(logger => "Not formatting"); - Task::ready(Ok(ProjectTransaction::default())) - } - } - - async fn handle_format_buffers( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let sender_id = envelope.original_sender_id().unwrap_or_default(); - let format = this.update(&mut cx, |this, cx| { - let mut buffers = HashSet::default(); - for buffer_id in &envelope.payload.buffer_ids { - let buffer_id = BufferId::new(*buffer_id)?; - buffers.insert(this.buffer_store.read(cx).get_existing(buffer_id)?); - } - let trigger = FormatTrigger::from_proto(envelope.payload.trigger); - anyhow::Ok(this.format(buffers, LspFormatTarget::Buffers, false, trigger, cx)) - })??; - - let project_transaction = format.await?; - let project_transaction = this.update(&mut cx, |this, cx| { - this.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.serialize_project_transaction_for_peer( - project_transaction, - sender_id, - cx, - ) - }) - })?; - Ok(proto::FormatBuffersResponse { - transaction: Some(project_transaction), - }) - } - - async fn handle_apply_code_action_kind( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let sender_id = envelope.original_sender_id().unwrap_or_default(); - let format = this.update(&mut cx, |this, cx| { - let mut buffers = HashSet::default(); - for buffer_id in &envelope.payload.buffer_ids { - let buffer_id = BufferId::new(*buffer_id)?; - buffers.insert(this.buffer_store.read(cx).get_existing(buffer_id)?); - } - let kind = match envelope.payload.kind.as_str() { - "" => CodeActionKind::EMPTY, - "quickfix" => CodeActionKind::QUICKFIX, - "refactor" => CodeActionKind::REFACTOR, - "refactor.extract" => CodeActionKind::REFACTOR_EXTRACT, - "refactor.inline" => CodeActionKind::REFACTOR_INLINE, - "refactor.rewrite" => CodeActionKind::REFACTOR_REWRITE, - "source" => CodeActionKind::SOURCE, - "source.organizeImports" => CodeActionKind::SOURCE_ORGANIZE_IMPORTS, - "source.fixAll" => CodeActionKind::SOURCE_FIX_ALL, - _ => anyhow::bail!( - "Invalid code action kind {}", - envelope.payload.kind.as_str() - ), - }; - anyhow::Ok(this.apply_code_action_kind(buffers, kind, false, cx)) - })??; - - let project_transaction = format.await?; - let project_transaction = this.update(&mut cx, |this, cx| { - this.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.serialize_project_transaction_for_peer( - project_transaction, - sender_id, - cx, - ) - }) - })?; - Ok(proto::ApplyCodeActionKindResponse { - transaction: Some(project_transaction), - }) - } - - async fn shutdown_language_server( - server_state: Option, - name: LanguageServerName, - cx: &mut AsyncApp, - ) { - let server = match server_state { - Some(LanguageServerState::Starting { startup, .. }) => { - let mut timer = cx - .background_executor() - .timer(SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT) - .fuse(); - - select! { - server = startup.fuse() => server, - () = timer => { - log::info!("timeout waiting for language server {name} to finish launching before stopping"); - None - }, - } - } - - Some(LanguageServerState::Running { server, .. }) => Some(server), - - None => None, - }; - - if let Some(server) = server - && let Some(shutdown) = server.shutdown() - { - shutdown.await; - } - } - - // Returns a list of all of the worktrees which no longer have a language server and the root path - // for the stopped server - fn stop_local_language_server( - &mut self, - server_id: LanguageServerId, - cx: &mut Context, - ) -> Task<()> { - let local = match &mut self.mode { - LspStoreMode::Local(local) => local, - _ => { - return Task::ready(()); - } - }; - - // Remove this server ID from all entries in the given worktree. - local - .language_server_ids - .retain(|_, state| state.id != server_id); - self.buffer_store.update(cx, |buffer_store, cx| { - for buffer in buffer_store.buffers() { - buffer.update(cx, |buffer, cx| { - buffer.update_diagnostics(server_id, DiagnosticSet::new([], buffer), cx); - buffer.set_completion_triggers(server_id, Default::default(), cx); - }); - } - }); - - for (worktree_id, summaries) in self.diagnostic_summaries.iter_mut() { - summaries.retain(|path, summaries_by_server_id| { - if summaries_by_server_id.remove(&server_id).is_some() { - if let Some((client, project_id)) = self.downstream_client.clone() { - client - .send(proto::UpdateDiagnosticSummary { - project_id, - worktree_id: worktree_id.to_proto(), - summary: Some(proto::DiagnosticSummary { - path: path.as_ref().to_proto(), - language_server_id: server_id.0 as u64, - error_count: 0, - warning_count: 0, - }), - more_summaries: Vec::new(), - }) - .log_err(); - } - !summaries_by_server_id.is_empty() - } else { - true - } - }); - } - - let local = self.as_local_mut().unwrap(); - for diagnostics in local.diagnostics.values_mut() { - diagnostics.retain(|_, diagnostics_by_server_id| { - if let Ok(ix) = diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) { - diagnostics_by_server_id.remove(ix); - !diagnostics_by_server_id.is_empty() - } else { - true - } - }); - } - local.language_server_watched_paths.remove(&server_id); - - let server_state = local.language_servers.remove(&server_id); - self.cleanup_lsp_data(server_id); - let name = self - .language_server_statuses - .remove(&server_id) - .map(|status| status.name) - .or_else(|| { - if let Some(LanguageServerState::Running { adapter, .. }) = server_state.as_ref() { - Some(adapter.name()) - } else { - None - } - }); - - if let Some(name) = name { - log::info!("stopping language server {name}"); - self.languages - .update_lsp_binary_status(name.clone(), BinaryStatus::Stopping); - cx.notify(); - - return cx.spawn(async move |lsp_store, cx| { - Self::shutdown_language_server(server_state, name.clone(), cx).await; - lsp_store - .update(cx, |lsp_store, cx| { - lsp_store - .languages - .update_lsp_binary_status(name, BinaryStatus::Stopped); - cx.emit(LspStoreEvent::LanguageServerRemoved(server_id)); - cx.notify(); - }) - .ok(); - }); - } - - if server_state.is_some() { - cx.emit(LspStoreEvent::LanguageServerRemoved(server_id)); - } - Task::ready(()) - } - - pub fn stop_all_language_servers(&mut self, cx: &mut Context) { - if let Some((client, project_id)) = self.upstream_client() { - let request = client.request(proto::StopLanguageServers { - project_id, - buffer_ids: Vec::new(), - also_servers: Vec::new(), - all: true, - }); - cx.background_spawn(request).detach_and_log_err(cx); - } else { - let Some(local) = self.as_local_mut() else { - return; - }; - let language_servers_to_stop = local - .language_server_ids - .values() - .map(|state| state.id) - .collect(); - local.lsp_tree.remove_nodes(&language_servers_to_stop); - let tasks = language_servers_to_stop - .into_iter() - .map(|server| self.stop_local_language_server(server, cx)) - .collect::>(); - cx.background_spawn(async move { - futures::future::join_all(tasks).await; - }) - .detach(); - } - } - - pub fn restart_language_servers_for_buffers( - &mut self, - buffers: Vec>, - only_restart_servers: HashSet, - cx: &mut Context, - ) { - if let Some((client, project_id)) = self.upstream_client() { - let request = client.request(proto::RestartLanguageServers { - project_id, - buffer_ids: buffers - .into_iter() - .map(|b| b.read(cx).remote_id().to_proto()) - .collect(), - only_servers: only_restart_servers - .into_iter() - .map(|selector| { - let selector = match selector { - LanguageServerSelector::Id(language_server_id) => { - proto::language_server_selector::Selector::ServerId( - language_server_id.to_proto(), - ) - } - LanguageServerSelector::Name(language_server_name) => { - proto::language_server_selector::Selector::Name( - language_server_name.to_string(), - ) - } - }; - proto::LanguageServerSelector { - selector: Some(selector), - } - }) - .collect(), - all: false, - }); - cx.background_spawn(request).detach_and_log_err(cx); - } else { - let stop_task = if only_restart_servers.is_empty() { - self.stop_local_language_servers_for_buffers(&buffers, HashSet::default(), cx) - } else { - self.stop_local_language_servers_for_buffers(&[], only_restart_servers.clone(), cx) - }; - cx.spawn(async move |lsp_store, cx| { - stop_task.await; - lsp_store - .update(cx, |lsp_store, cx| { - for buffer in buffers { - lsp_store.register_buffer_with_language_servers( - &buffer, - only_restart_servers.clone(), - true, - cx, - ); - } - }) - .ok() - }) - .detach(); - } - } - - pub fn stop_language_servers_for_buffers( - &mut self, - buffers: Vec>, - also_stop_servers: HashSet, - cx: &mut Context, - ) -> Task> { - if let Some((client, project_id)) = self.upstream_client() { - let request = client.request(proto::StopLanguageServers { - project_id, - buffer_ids: buffers - .into_iter() - .map(|b| b.read(cx).remote_id().to_proto()) - .collect(), - also_servers: also_stop_servers - .into_iter() - .map(|selector| { - let selector = match selector { - LanguageServerSelector::Id(language_server_id) => { - proto::language_server_selector::Selector::ServerId( - language_server_id.to_proto(), - ) - } - LanguageServerSelector::Name(language_server_name) => { - proto::language_server_selector::Selector::Name( - language_server_name.to_string(), - ) - } - }; - proto::LanguageServerSelector { - selector: Some(selector), - } - }) - .collect(), - all: false, - }); - cx.background_spawn(async move { - let _ = request.await?; - Ok(()) - }) - } else { - let task = - self.stop_local_language_servers_for_buffers(&buffers, also_stop_servers, cx); - cx.background_spawn(async move { - task.await; - Ok(()) - }) - } - } - - fn stop_local_language_servers_for_buffers( - &mut self, - buffers: &[Entity], - also_stop_servers: HashSet, - cx: &mut Context, - ) -> Task<()> { - let Some(local) = self.as_local_mut() else { - return Task::ready(()); - }; - let mut language_server_names_to_stop = BTreeSet::default(); - let mut language_servers_to_stop = also_stop_servers - .into_iter() - .flat_map(|selector| match selector { - LanguageServerSelector::Id(id) => Some(id), - LanguageServerSelector::Name(name) => { - language_server_names_to_stop.insert(name); - None - } - }) - .collect::>(); - - let mut covered_worktrees = HashSet::default(); - 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() - }); - } - }); - } - 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)), - ); - } - - local.lsp_tree.remove_nodes(&language_servers_to_stop); - let tasks = language_servers_to_stop - .into_iter() - .map(|server| self.stop_local_language_server(server, cx)) - .collect::>(); - - cx.background_spawn(futures::future::join_all(tasks).map(|_| ())) - } - - fn get_buffer<'a>(&self, abs_path: &Path, cx: &'a App) -> Option<&'a Buffer> { - let (worktree, relative_path) = - self.worktree_store.read(cx).find_worktree(&abs_path, cx)?; - - let project_path = ProjectPath { - worktree_id: worktree.read(cx).id(), - path: relative_path, - }; - - Some( - self.buffer_store() - .read(cx) - .get_by_path(&project_path)? - .read(cx), - ) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn update_diagnostics( - &mut self, - server_id: LanguageServerId, - diagnostics: lsp::PublishDiagnosticsParams, - result_id: Option, - source_kind: DiagnosticSourceKind, - disk_based_sources: &[String], - cx: &mut Context, - ) -> Result<()> { - self.merge_lsp_diagnostics( - source_kind, - vec![DocumentDiagnosticsUpdate { - diagnostics, - result_id, - server_id, - disk_based_sources: Cow::Borrowed(disk_based_sources), - registration_id: None, - }], - |_, _, _| false, - cx, - ) - } - - pub fn merge_lsp_diagnostics( - &mut self, - source_kind: DiagnosticSourceKind, - lsp_diagnostics: Vec>, - merge: impl Fn(&lsp::Uri, &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, - update.registration_id.clone(), - ), - result_id: update.result_id, - server_id: update.server_id, - disk_based_sources: update.disk_based_sources, - registration_id: update.registration_id, - }) - }) - .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], - registration_id: Option, - ) -> DocumentDiagnostics { - 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); - - // Ensure that primary diagnostics are always the most severe - lsp_diagnostics - .diagnostics - .sort_by_key(|item| item.severity); - - for diagnostic in &lsp_diagnostics.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| { - infos.iter().any(|info| { - primary_diagnostic_group_ids.contains_key(&( - source, - diagnostic.code.clone(), - range_from_lsp(info.location.range), - )) - }) - }); - - let is_unnecessary = diagnostic - .tags - .as_ref() - .is_some_and(|tags| tags.contains(&DiagnosticTag::UNNECESSARY)); - - let underline = self - .language_server_adapter_for_id(server_id) - .is_none_or(|adapter| adapter.underline_diagnostic(diagnostic)); - - if is_supporting { - supporting_diagnostics.insert( - (source, diagnostic.code.clone(), range), - (diagnostic.severity, is_unnecessary), - ); - } 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)); - - sources_by_group_id.insert(group_id, source); - primary_diagnostic_group_ids - .insert((source, diagnostic.code.clone(), range.clone()), group_id); - - diagnostics.push(DiagnosticEntry { - range, - diagnostic: Diagnostic { - source: diagnostic.source.clone(), - source_kind, - code: diagnostic.code.clone(), - code_description: diagnostic - .code_description - .as_ref() - .and_then(|d| d.href.clone()), - severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR), - markdown: adapter.as_ref().and_then(|adapter| { - adapter.diagnostic_message_to_markdown(&diagnostic.message) - }), - message: diagnostic.message.trim().to_string(), - group_id, - is_primary: true, - is_disk_based, - is_unnecessary, - underline, - data: diagnostic.data.clone(), - registration_id: registration_id.clone(), - }, - }); - if let Some(infos) = &diagnostic.related_information { - for info in infos { - if info.location.uri == lsp_diagnostics.uri && !info.message.is_empty() { - let range = range_from_lsp(info.location.range); - diagnostics.push(DiagnosticEntry { - range, - diagnostic: Diagnostic { - source: diagnostic.source.clone(), - source_kind, - code: diagnostic.code.clone(), - code_description: diagnostic - .code_description - .as_ref() - .and_then(|d| d.href.clone()), - severity: DiagnosticSeverity::INFORMATION, - markdown: adapter.as_ref().and_then(|adapter| { - adapter.diagnostic_message_to_markdown(&info.message) - }), - message: info.message.trim().to_string(), - group_id, - is_primary: false, - is_disk_based, - is_unnecessary: false, - underline, - data: diagnostic.data.clone(), - registration_id: registration_id.clone(), - }, - }); - } - } - } - } - } - - for entry in &mut diagnostics { - let diagnostic = &mut entry.diagnostic; - if !diagnostic.is_primary { - let source = *sources_by_group_id.get(&diagnostic.group_id).unwrap(); - if let Some(&(severity, is_unnecessary)) = supporting_diagnostics.get(&( - source, - diagnostic.code.clone(), - entry.range.clone(), - )) { - if let Some(severity) = severity { - diagnostic.severity = severity; - } - diagnostic.is_unnecessary = is_unnecessary; - } - } - } - - DocumentDiagnostics { - diagnostics, - document_abs_path, - version: lsp_diagnostics.version, - } - } - - fn insert_newly_running_language_server( - &mut self, - adapter: Arc, - language_server: Arc, - server_id: LanguageServerId, - key: LanguageServerSeed, - workspace_folders: Arc>>, - cx: &mut Context, - ) { - let Some(local) = self.as_local_mut() else { - return; - }; - // If the language server for this key doesn't match the server id, don't store the - // server. Which will cause it to be dropped, killing the process - if local - .language_server_ids - .get(&key) - .map(|state| state.id != server_id) - .unwrap_or(false) - { - return; - } - - // Update language_servers collection with Running variant of LanguageServerState - // indicating that the server is up and running and ready - let workspace_folders = workspace_folders.lock().clone(); - language_server.set_workspace_folders(workspace_folders); - - let workspace_diagnostics_refresh_tasks = language_server - .capabilities() - .diagnostic_provider - .and_then(|provider| { - local - .language_server_dynamic_registrations - .entry(server_id) - .or_default() - .diagnostics - .entry(None) - .or_insert(provider.clone()); - let workspace_refresher = - lsp_workspace_diagnostics_refresh(None, provider, language_server.clone(), cx)?; - - Some((None, workspace_refresher)) - }) - .into_iter() - .collect(); - local.language_servers.insert( - server_id, - LanguageServerState::Running { - workspace_diagnostics_refresh_tasks, - adapter: adapter.clone(), - server: language_server.clone(), - simulate_disk_based_diagnostics_completion: None, - }, - ); - local - .languages - .update_lsp_binary_status(adapter.name(), BinaryStatus::None); - if let Some(file_ops_caps) = language_server - .capabilities() - .workspace - .as_ref() - .and_then(|ws| ws.file_operations.as_ref()) - { - let did_rename_caps = file_ops_caps.did_rename.as_ref(); - let will_rename_caps = file_ops_caps.will_rename.as_ref(); - if did_rename_caps.or(will_rename_caps).is_some() { - let watcher = RenamePathsWatchedForServer::default() - .with_did_rename_patterns(did_rename_caps) - .with_will_rename_patterns(will_rename_caps); - local - .language_server_paths_watched_for_rename - .insert(server_id, watcher); - } - } - - self.language_server_statuses.insert( - server_id, - LanguageServerStatus { - name: language_server.name(), - pending_work: Default::default(), - has_pending_diagnostic_updates: false, - progress_tokens: Default::default(), - worktree: Some(key.worktree_id), - binary: Some(language_server.binary().clone()), - configuration: Some(language_server.configuration().clone()), - workspace_folders: language_server.workspace_folders(), - }, - ); - - cx.emit(LspStoreEvent::LanguageServerAdded( - server_id, - language_server.name(), - Some(key.worktree_id), - )); - - 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(), - name: language_server.name().to_string(), - worktree_id: Some(key.worktree_id.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]; - 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 - .values() - .any(|(node, _)| node.id() == Some(server_id)) - { - worktrees_using_server.push(*worktree_id); - } - } - } - } - } - - let mut buffer_paths_registered = Vec::new(); - self.buffer_store.clone().update(cx, |buffer_store, cx| { - let mut lsp_adapters = HashMap::default(); - for buffer_handle in buffer_store.buffers() { - let buffer = buffer_handle.read(cx); - let file = match File::from_dyn(buffer.file()) { - Some(file) => file, - None => continue, - }; - let language = match buffer.language() { - Some(language) => language, - None => continue, - }; - - if !worktrees_using_server.contains(&file.worktree.read(cx).id()) - || !lsp_adapters - .entry(language.name()) - .or_insert_with(|| self.languages.lsp_adapters(&language.name())) - .iter() - .any(|a| a.name == key.name) - { - continue; - } - // didOpen - let file = match file.as_local() { - Some(file) => file, - None => continue, - }; - - let local = self.as_local_mut().unwrap(); - - let buffer_id = buffer.remote_id(); - if local.registered_buffers.contains_key(&buffer_id) { - let versions = local - .buffer_snapshots - .entry(buffer_id) - .or_default() - .entry(server_id) - .and_modify(|_| { - assert!( - false, - "There should not be an existing snapshot for a newly inserted buffer" - ) - }) - .or_insert_with(|| { - vec![LspBufferSnapshot { - version: 0, - snapshot: buffer.text_snapshot(), - }] - }); - - let snapshot = versions.last().unwrap(); - let version = snapshot.version; - let initial_snapshot = &snapshot.snapshot; - let uri = lsp::Uri::from_file_path(file.abs_path(cx)).unwrap(); - language_server.register_buffer( - uri, - adapter.language_id(&language.name()), - version, - initial_snapshot.text(), - ); - buffer_paths_registered.push((buffer_id, file.abs_path(cx))); - local - .buffers_opened_in_servers - .entry(buffer_id) - .or_default() - .insert(server_id); - } - buffer_handle.update(cx, |buffer, cx| { - buffer.set_completion_triggers( - server_id, - language_server - .capabilities() - .completion_provider - .as_ref() - .and_then(|provider| { - provider - .trigger_characters - .as_ref() - .map(|characters| characters.iter().cloned().collect()) - }) - .unwrap_or_default(), - cx, - ) - }); - } - }); - - for (buffer_id, 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().into_owned(), - buffer_id: buffer_id.to_proto(), - }, - ), - }); - } - - cx.notify(); - } - - pub fn language_servers_running_disk_based_diagnostics( - &self, - ) -> impl Iterator + '_ { - self.language_server_statuses - .iter() - .filter_map(|(id, status)| { - if status.has_pending_diagnostic_updates { - Some(*id) - } else { - None - } - }) - } - - pub(crate) fn cancel_language_server_work_for_buffers( - &mut self, - buffers: impl IntoIterator>, - cx: &mut Context, - ) { - if let Some((client, project_id)) = self.upstream_client() { - let request = client.request(proto::CancelLanguageServerWork { - project_id, - work: Some(proto::cancel_language_server_work::Work::Buffers( - proto::cancel_language_server_work::Buffers { - buffer_ids: buffers - .into_iter() - .map(|b| b.read(cx).remote_id().to_proto()) - .collect(), - }, - )), - }); - cx.background_spawn(request).detach_and_log_err(cx); - } else if let Some(local) = self.as_local() { - let servers = buffers - .into_iter() - .flat_map(|buffer| { - buffer.update(cx, |buffer, cx| { - local.language_server_ids_for_buffer(buffer, cx).into_iter() - }) - }) - .collect::>(); - for server_id in servers { - self.cancel_language_server_work(server_id, None, cx); - } - } - } - - pub(crate) fn cancel_language_server_work( - &mut self, - server_id: LanguageServerId, - token_to_cancel: Option, - cx: &mut Context, - ) { - if let Some(local) = self.as_local() { - let status = self.language_server_statuses.get(&server_id); - let server = local.language_servers.get(&server_id); - 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 progress.is_cancellable { - server - .notify::( - WorkDoneProgressCancelParams { - token: token.to_lsp(), - }, - ) - .ok(); - } - } - } - } else if let Some((client, project_id)) = self.upstream_client() { - let request = client.request(proto::CancelLanguageServerWork { - project_id, - work: Some( - proto::cancel_language_server_work::Work::LanguageServerWork( - proto::cancel_language_server_work::LanguageServerWork { - language_server_id: server_id.to_proto(), - token: token_to_cancel.map(|token| token.to_proto()), - }, - ), - ), - }); - cx.background_spawn(request).detach_and_log_err(cx); - } - } - - fn register_supplementary_language_server( - &mut self, - id: LanguageServerId, - name: LanguageServerName, - server: Arc, - cx: &mut Context, - ) { - if let Some(local) = self.as_local_mut() { - local - .supplementary_language_servers - .insert(id, (name.clone(), server)); - cx.emit(LspStoreEvent::LanguageServerAdded(id, name, None)); - } - } - - fn unregister_supplementary_language_server( - &mut self, - id: LanguageServerId, - cx: &mut Context, - ) { - if let Some(local) = self.as_local_mut() { - local.supplementary_language_servers.remove(&id); - cx.emit(LspStoreEvent::LanguageServerRemoved(id)); - } - } - - pub(crate) fn supplementary_language_servers( - &self, - ) -> impl '_ + Iterator { - self.as_local().into_iter().flat_map(|local| { - local - .supplementary_language_servers - .iter() - .map(|(id, (name, _))| (*id, name.clone())) - }) - } - - pub fn language_server_adapter_for_id( - &self, - id: LanguageServerId, - ) -> Option> { - self.as_local() - .and_then(|local| local.language_servers.get(&id)) - .and_then(|language_server_state| match language_server_state { - LanguageServerState::Running { adapter, .. } => Some(adapter.clone()), - _ => None, - }) - } - - pub(super) fn update_local_worktree_language_servers( - &mut self, - worktree_handle: &Entity, - changes: &[(Arc, ProjectEntryId, PathChange)], - cx: &mut Context, - ) { - if changes.is_empty() { - return; - } - - 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) - }); - - 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)) - .collect::>(); - language_server_ids.sort(); - language_server_ids.dedup(); - - // let abs_path = worktree_handle.read(cx).abs_path(); - for server_id in &language_server_ids { - if let Some(LanguageServerState::Running { server, .. }) = - local.language_servers.get(server_id) - && 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.as_std_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, - }; - let uri = lsp::Uri::from_file_path( - worktree_handle.read(cx).absolutize(&path), - ) - .ok()?; - Some(lsp::FileEvent { uri, typ }) - }) - .collect(), - }; - if !params.changes.is_empty() { - server - .notify::(params) - .ok(); - } - } - } - for (path, _, _) in changes { - if let Some(file_name) = path.file_name() - && local.watched_manifest_filenames.contains(file_name) - { - self.request_workspace_config_refresh(); - break; - } - } - } - - pub fn wait_for_remote_buffer( - &mut self, - id: BufferId, - cx: &mut Context, - ) -> Task>> { - self.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.wait_for_remote_buffer(id, cx) - }) - } - - fn serialize_symbol(symbol: &Symbol) -> proto::Symbol { - let mut result = proto::Symbol { - language_server_name: symbol.language_server_name.0.to_string(), - source_worktree_id: symbol.source_worktree_id.to_proto(), - language_server_id: symbol.source_language_server_id.to_proto(), - name: symbol.name.clone(), - kind: unsafe { mem::transmute::(symbol.kind) }, - start: Some(proto::PointUtf16 { - row: symbol.range.start.0.row, - column: symbol.range.start.0.column, - }), - end: Some(proto::PointUtf16 { - row: symbol.range.end.0.row, - column: symbol.range.end.0.column, - }), - worktree_id: Default::default(), - path: Default::default(), - signature: Default::default(), - }; - match &symbol.path { - SymbolLocation::InProject(path) => { - result.worktree_id = path.worktree_id.to_proto(); - result.path = path.path.to_proto(); - } - SymbolLocation::OutsideProject { - abs_path, - signature, - } => { - result.path = abs_path.to_string_lossy().into_owned(); - result.signature = signature.to_vec(); - } - } - result - } - - fn deserialize_symbol(serialized_symbol: proto::Symbol) -> Result { - let source_worktree_id = WorktreeId::from_proto(serialized_symbol.source_worktree_id); - let worktree_id = WorktreeId::from_proto(serialized_symbol.worktree_id); - let kind = unsafe { mem::transmute::(serialized_symbol.kind) }; - - let path = if serialized_symbol.signature.is_empty() { - SymbolLocation::InProject(ProjectPath { - worktree_id, - path: RelPath::from_proto(&serialized_symbol.path) - .context("invalid symbol path")?, - }) - } else { - SymbolLocation::OutsideProject { - abs_path: Path::new(&serialized_symbol.path).into(), - signature: serialized_symbol - .signature - .try_into() - .map_err(|_| anyhow!("invalid signature"))?, - } - }; - - let start = serialized_symbol.start.context("invalid start")?; - let end = serialized_symbol.end.context("invalid end")?; - Ok(CoreSymbol { - language_server_name: LanguageServerName(serialized_symbol.language_server_name.into()), - source_worktree_id, - source_language_server_id: LanguageServerId::from_proto( - serialized_symbol.language_server_id, - ), - path, - name: serialized_symbol.name, - range: Unclipped(PointUtf16::new(start.row, start.column)) - ..Unclipped(PointUtf16::new(end.row, end.column)), - kind, - }) - } - - pub(crate) fn serialize_completion(completion: &CoreCompletion) -> proto::Completion { - let mut serialized_completion = proto::Completion { - old_replace_start: Some(serialize_anchor(&completion.replace_range.start)), - old_replace_end: Some(serialize_anchor(&completion.replace_range.end)), - new_text: completion.new_text.clone(), - ..proto::Completion::default() - }; - match &completion.source { - CompletionSource::Lsp { - insert_range, - server_id, - lsp_completion, - lsp_defaults, - resolved, - } => { - let (old_insert_start, old_insert_end) = insert_range - .as_ref() - .map(|range| (serialize_anchor(&range.start), serialize_anchor(&range.end))) - .unzip(); - - serialized_completion.old_insert_start = old_insert_start; - serialized_completion.old_insert_end = old_insert_end; - serialized_completion.source = proto::completion::Source::Lsp as i32; - serialized_completion.server_id = server_id.0 as u64; - serialized_completion.lsp_completion = serde_json::to_vec(lsp_completion).unwrap(); - serialized_completion.lsp_defaults = lsp_defaults - .as_deref() - .map(|lsp_defaults| serde_json::to_vec(lsp_defaults).unwrap()); - serialized_completion.resolved = *resolved; - } - CompletionSource::BufferWord { - word_range, - resolved, - } => { - serialized_completion.source = proto::completion::Source::BufferWord as i32; - serialized_completion.buffer_word_start = Some(serialize_anchor(&word_range.start)); - serialized_completion.buffer_word_end = Some(serialize_anchor(&word_range.end)); - serialized_completion.resolved = *resolved; - } - CompletionSource::Custom => { - serialized_completion.source = proto::completion::Source::Custom as i32; - serialized_completion.resolved = true; - } - CompletionSource::Dap { sort_text } => { - serialized_completion.source = proto::completion::Source::Dap as i32; - serialized_completion.sort_text = Some(sort_text.clone()); - } - } - - serialized_completion - } - - pub(crate) fn deserialize_completion(completion: proto::Completion) -> Result { - let old_replace_start = completion - .old_replace_start - .and_then(deserialize_anchor) - .context("invalid old start")?; - let old_replace_end = completion - .old_replace_end - .and_then(deserialize_anchor) - .context("invalid old end")?; - let insert_range = { - match completion.old_insert_start.zip(completion.old_insert_end) { - Some((start, end)) => { - let start = deserialize_anchor(start).context("invalid insert old start")?; - let end = deserialize_anchor(end).context("invalid insert old end")?; - Some(start..end) - } - None => None, - } - }; - Ok(CoreCompletion { - replace_range: old_replace_start..old_replace_end, - new_text: completion.new_text, - source: match proto::completion::Source::from_i32(completion.source) { - Some(proto::completion::Source::Custom) => CompletionSource::Custom, - Some(proto::completion::Source::Lsp) => CompletionSource::Lsp { - insert_range, - server_id: LanguageServerId::from_proto(completion.server_id), - lsp_completion: serde_json::from_slice(&completion.lsp_completion)?, - lsp_defaults: completion - .lsp_defaults - .as_deref() - .map(serde_json::from_slice) - .transpose()?, - resolved: completion.resolved, - }, - Some(proto::completion::Source::BufferWord) => { - let word_range = completion - .buffer_word_start - .and_then(deserialize_anchor) - .context("invalid buffer word start")? - ..completion - .buffer_word_end - .and_then(deserialize_anchor) - .context("invalid buffer word end")?; - CompletionSource::BufferWord { - word_range, - resolved: completion.resolved, - } - } - Some(proto::completion::Source::Dap) => CompletionSource::Dap { - sort_text: completion - .sort_text - .context("expected sort text to exist")?, - }, - _ => anyhow::bail!("Unexpected completion source {}", completion.source), - }, - }) - } - - pub(crate) fn serialize_code_action(action: &CodeAction) -> proto::CodeAction { - let (kind, lsp_action) = match &action.lsp_action { - LspAction::Action(code_action) => ( - proto::code_action::Kind::Action as i32, - serde_json::to_vec(code_action).unwrap(), - ), - LspAction::Command(command) => ( - proto::code_action::Kind::Command as i32, - serde_json::to_vec(command).unwrap(), - ), - LspAction::CodeLens(code_lens) => ( - proto::code_action::Kind::CodeLens as i32, - serde_json::to_vec(code_lens).unwrap(), - ), - }; - - proto::CodeAction { - server_id: action.server_id.0 as u64, - start: Some(serialize_anchor(&action.range.start)), - end: Some(serialize_anchor(&action.range.end)), - lsp_action, - kind, - resolved: action.resolved, - } - } - - pub(crate) fn deserialize_code_action(action: proto::CodeAction) -> Result { - let start = action - .start - .and_then(deserialize_anchor) - .context("invalid start")?; - let end = action - .end - .and_then(deserialize_anchor) - .context("invalid end")?; - let lsp_action = match proto::code_action::Kind::from_i32(action.kind) { - Some(proto::code_action::Kind::Action) => { - LspAction::Action(serde_json::from_slice(&action.lsp_action)?) - } - Some(proto::code_action::Kind::Command) => { - LspAction::Command(serde_json::from_slice(&action.lsp_action)?) - } - Some(proto::code_action::Kind::CodeLens) => { - LspAction::CodeLens(serde_json::from_slice(&action.lsp_action)?) - } - None => anyhow::bail!("Unknown action kind {}", action.kind), - }; - Ok(CodeAction { - server_id: LanguageServerId(action.server_id as usize), - range: start..end, - resolved: action.resolved, - lsp_action, - }) - } - - fn update_last_formatting_failure(&mut self, formatting_result: &anyhow::Result) { - match &formatting_result { - Ok(_) => self.last_formatting_failure = None, - Err(error) => { - let error_string = format!("{error:#}"); - log::error!("Formatting failed: {error_string}"); - self.last_formatting_failure - .replace(error_string.lines().join(" ")); - } - } - } - - fn cleanup_lsp_data(&mut self, for_server: LanguageServerId) { - self.lsp_server_capabilities.remove(&for_server); - for lsp_data in self.lsp_data.values_mut() { - lsp_data.remove_server_data(for_server); - } - if let Some(local) = self.as_local_mut() { - local.buffer_pull_diagnostics_result_ids.remove(&for_server); - local - .workspace_pull_diagnostics_result_ids - .remove(&for_server); - for buffer_servers in local.buffers_opened_in_servers.values_mut() { - buffer_servers.remove(&for_server); - } - } - } - - pub fn result_id_for_buffer_pull( - &self, - server_id: LanguageServerId, - buffer_id: BufferId, - registration_id: &Option, - cx: &App, - ) -> Option { - let abs_path = self - .buffer_store - .read(cx) - .get(buffer_id) - .and_then(|b| File::from_dyn(b.read(cx).file())) - .map(|f| f.abs_path(cx))?; - self.as_local()? - .buffer_pull_diagnostics_result_ids - .get(&server_id)? - .get(registration_id)? - .get(&abs_path)? - .clone() - } - - /// Gets all result_ids for a workspace diagnostics pull request. - /// First, it tries to find buffer's result_id retrieved via the diagnostics pull; if it fails, it falls back to the workspace disagnostics pull result_id. - /// The latter is supposed to be of lower priority as we keep on pulling diagnostics for open buffers eagerly. - pub fn result_ids_for_workspace_refresh( - &self, - server_id: LanguageServerId, - registration_id: &Option, - ) -> HashMap { - let Some(local) = self.as_local() else { - return HashMap::default(); - }; - local - .workspace_pull_diagnostics_result_ids - .get(&server_id) - .into_iter() - .filter_map(|diagnostics| diagnostics.get(registration_id)) - .flatten() - .filter_map(|(abs_path, result_id)| { - let result_id = local - .buffer_pull_diagnostics_result_ids - .get(&server_id) - .and_then(|buffer_ids_result_ids| { - buffer_ids_result_ids.get(registration_id)?.get(abs_path) - }) - .cloned() - .flatten() - .or_else(|| result_id.clone())?; - Some((abs_path.clone(), result_id)) - }) - .collect() - } - - pub fn pull_workspace_diagnostics(&mut self, server_id: LanguageServerId) { - if let Some(LanguageServerState::Running { - workspace_diagnostics_refresh_tasks, - .. - }) = self - .as_local_mut() - .and_then(|local| local.language_servers.get_mut(&server_id)) - { - for diagnostics in workspace_diagnostics_refresh_tasks.values_mut() { - diagnostics.refresh_tx.try_send(()).ok(); - } - } - } - - pub fn pull_workspace_diagnostics_for_buffer(&mut self, buffer_id: BufferId, cx: &mut App) { - let Some(buffer) = self.buffer_store().read(cx).get_existing(buffer_id).ok() else { - return; - }; - let Some(local) = self.as_local_mut() else { - return; - }; - - for server_id in buffer.update(cx, |buffer, cx| { - local.language_server_ids_for_buffer(buffer, cx) - }) { - if let Some(LanguageServerState::Running { - workspace_diagnostics_refresh_tasks, - .. - }) = local.language_servers.get_mut(&server_id) - { - for diagnostics in workspace_diagnostics_refresh_tasks.values_mut() { - diagnostics.refresh_tx.try_send(()).ok(); - } - } - } - } - - fn apply_workspace_diagnostic_report( - &mut self, - server_id: LanguageServerId, - report: lsp::WorkspaceDiagnosticReportResult, - registration_id: Option, - cx: &mut Context, - ) { - let workspace_diagnostics = - GetDocumentDiagnostics::deserialize_workspace_diagnostics_report( - report, - server_id, - registration_id, - ); - let mut unchanged_buffers = HashMap::default(); - let workspace_diagnostics_updates = workspace_diagnostics - .into_iter() - .filter_map( - |workspace_diagnostics| match workspace_diagnostics.diagnostics { - LspPullDiagnostics::Response { - server_id, - uri, - diagnostics, - registration_id, - } => Some(( - server_id, - uri, - diagnostics, - workspace_diagnostics.version, - registration_id, - )), - LspPullDiagnostics::Default => None, - }, - ) - .fold( - HashMap::default(), - |mut acc, (server_id, uri, diagnostics, version, new_registration_id)| { - let (result_id, diagnostics) = match diagnostics { - PulledDiagnostics::Unchanged { result_id } => { - unchanged_buffers - .entry(new_registration_id.clone()) - .or_insert_with(HashSet::default) - .insert(uri.clone()); - (Some(result_id), Vec::new()) - } - PulledDiagnostics::Changed { - result_id, - diagnostics, - } => (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(), - ); - - let Some(abs_path) = uri.to_file_path().ok() else { - return acc; - }; - let Some((worktree, relative_path)) = - self.worktree_store.read(cx).find_worktree(abs_path.clone(), cx) - else { - log::warn!("skipping workspace diagnostics update, no worktree found for path {abs_path:?}"); - return acc; - }; - let worktree_id = worktree.read(cx).id(); - let project_path = ProjectPath { - worktree_id, - path: relative_path, - }; - if let Some(local_lsp_store) = self.as_local_mut() { - local_lsp_store.workspace_pull_diagnostics_result_ids.entry(server_id) - .or_default().entry(new_registration_id.clone()).or_default().insert(abs_path, result_id.clone()); - } - // The LSP spec recommends that "diagnostics from a document pull should win over diagnostics from a workspace pull." - // Since we actively pull diagnostics for documents with open buffers, we ignore contents of workspace pulls for these documents. - if self.buffer_store.read(cx).get_by_path(&project_path).is_none() { - acc.entry(server_id) - .or_insert_with(HashMap::default) - .entry(new_registration_id.clone()) - .or_insert_with(Vec::new) - .push(DocumentDiagnosticsUpdate { - server_id, - diagnostics: lsp::PublishDiagnosticsParams { - uri, - diagnostics, - version, - }, - result_id, - disk_based_sources, - registration_id: new_registration_id, - }); - } - acc - }, - ); - - for diagnostic_updates in workspace_diagnostics_updates.into_values() { - for (registration_id, diagnostic_updates) in diagnostic_updates { - self.merge_lsp_diagnostics( - DiagnosticSourceKind::Pulled, - diagnostic_updates, - |document_uri, old_diagnostic, _| match old_diagnostic.source_kind { - DiagnosticSourceKind::Pulled => { - old_diagnostic.registration_id != registration_id - || unchanged_buffers - .get(&old_diagnostic.registration_id) - .is_some_and(|unchanged_buffers| { - unchanged_buffers.contains(&document_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/didChangeWorkspaceFolders" => { - // In this case register options is an empty object, we can ignore it - let caps = lsp::WorkspaceFoldersServerCapabilities { - supported: Some(true), - change_notifications: Some(OneOf::Right(reg.id)), - }; - server.update_capabilities(|capabilities| { - capabilities - .workspace - .get_or_insert_default() - .workspace_folders = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); - } - "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" => { - let options = parse_register_capabilities(reg)?; - let provider = match options { - OneOf::Left(value) => lsp::CodeActionProviderCapability::Simple(value), - OneOf::Right(caps) => caps, - }; - server.update_capabilities(|capabilities| { - capabilities.code_action_provider = Some(provider); - }); - 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.clone()); - }); - - if let Some(local) = self.as_local() { - let mut buffers_with_language_server = Vec::new(); - for handle in self.buffer_store.read(cx).buffers() { - let buffer_id = handle.read(cx).remote_id(); - if local - .buffers_opened_in_servers - .get(&buffer_id) - .filter(|s| s.contains(&server_id)) - .is_some() - { - buffers_with_language_server.push(handle); - } - } - let triggers = caps - .trigger_characters - .unwrap_or_default() - .into_iter() - .collect::>(); - for handle in buffers_with_language_server { - let triggers = triggers.clone(); - let _ = handle.update(cx, move |buffer, cx| { - buffer.set_completion_triggers(server_id, triggers, cx); - }); - } - } - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/hover" => { - let options = parse_register_capabilities(reg)?; - let provider = match options { - OneOf::Left(value) => lsp::HoverProviderCapability::Simple(value), - OneOf::Right(caps) => caps, - }; - server.update_capabilities(|capabilities| { - capabilities.hover_provider = Some(provider); - }); - 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), - } - }) - .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); - } - } - "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()? - { - let local = self - .as_local_mut() - .context("Expected LSP Store to be local")?; - let state = local - .language_servers - .get_mut(&server_id) - .context("Could not obtain Language Servers state")?; - local - .language_server_dynamic_registrations - .entry(server_id) - .or_default() - .diagnostics - .insert(Some(reg.id.clone()), caps.clone()); - - let supports_workspace_diagnostics = - |capabilities: &DiagnosticServerCapabilities| match capabilities { - DiagnosticServerCapabilities::Options(diagnostic_options) => { - diagnostic_options.workspace_diagnostics - } - DiagnosticServerCapabilities::RegistrationOptions( - diagnostic_registration_options, - ) => { - diagnostic_registration_options - .diagnostic_options - .workspace_diagnostics - } - }; - - if supports_workspace_diagnostics(&caps) { - if let LanguageServerState::Running { - workspace_diagnostics_refresh_tasks, - .. - } = state - && let Some(task) = lsp_workspace_diagnostics_refresh( - Some(reg.id.clone()), - caps.clone(), - server.clone(), - cx, - ) - { - workspace_diagnostics_refresh_tasks.insert(Some(reg.id), task); - } - } - - server.update_capabilities(|capabilities| { - capabilities.diagnostic_provider = Some(caps); - }); - - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/documentColor" => { - let options = parse_register_capabilities(reg)?; - let provider = match options { - OneOf::Left(value) => lsp::ColorProviderCapability::Simple(value), - OneOf::Right(caps) => caps, - }; - server.update_capabilities(|capabilities| { - capabilities.color_provider = Some(provider); - }); - 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/didChangeWorkspaceFolders" => { - server.update_capabilities(|capabilities| { - capabilities - .workspace - .get_or_insert_with(|| lsp::WorkspaceServerCapabilities { - workspace_folders: None, - file_operations: None, - }) - .workspace_folders = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "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" => { - let local = self - .as_local_mut() - .context("Expected LSP Store to be local")?; - - let state = local - .language_servers - .get_mut(&server_id) - .context("Could not obtain Language Servers state")?; - let registrations = local - .language_server_dynamic_registrations - .get_mut(&server_id) - .with_context(|| { - format!("Expected dynamic registration to exist for server {server_id}") - })?; - registrations.diagnostics - .remove(&Some(unreg.id.clone())) - .with_context(|| format!( - "Attempted to unregister non-existent diagnostic registration with ID {}", - unreg.id) - )?; - let removed_last_diagnostic_provider = registrations.diagnostics.is_empty(); - - if let LanguageServerState::Running { - workspace_diagnostics_refresh_tasks, - .. - } = state - { - workspace_diagnostics_refresh_tasks.remove(&Some(unreg.id.clone())); - } - - if removed_last_diagnostic_provider { - server.update_capabilities(|capabilities| { - debug_assert!(capabilities.diagnostic_provider.is_some()); - 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 deduplicate_range_based_lsp_requests( - lsp_store: &Entity, - server_id: Option, - lsp_request_id: LspRequestId, - proto_request: &T::ProtoRequest, - range: Range, - cx: &mut AsyncApp, - ) -> Result<()> - where - T: LspCommand, - T::ProtoRequest: proto::LspRequestMessage, - { - let buffer_id = BufferId::new(proto_request.buffer_id())?; - let version = deserialize_version(proto_request.buffer_version()); - let buffer = lsp_store.update(cx, |this, cx| { - this.buffer_store.read(cx).get_existing(buffer_id) - })??; - buffer - .update(cx, |buffer, _| buffer.wait_for_version(version))? - .await?; - lsp_store.update(cx, |lsp_store, cx| { - let lsp_data = lsp_store.latest_lsp_data(&buffer, cx); - let chunks_queried_for = lsp_data - .inlay_hints - .applicable_chunks(&[range]) - .collect::>(); - match chunks_queried_for.as_slice() { - &[chunk] => { - let key = LspKey { - request_type: TypeId::of::(), - server_queried: server_id, - }; - let previous_request = lsp_data - .chunk_lsp_requests - .entry(key) - .or_default() - .insert(chunk, lsp_request_id); - if let Some((previous_request, running_requests)) = - previous_request.zip(lsp_data.lsp_requests.get_mut(&key)) - { - running_requests.remove(&previous_request); - } - } - _ambiguous_chunks => { - // Have not found a unique chunk for the query range — be lenient and let the query to be spawned, - // there, a buffer version-based check will be performed and outdated requests discarded. - } - } - anyhow::Ok(()) - })??; - - Ok(()) - } - - async fn query_lsp_locally( - lsp_store: Entity, - for_server_id: Option, - sender_id: proto::PeerId, - lsp_request_id: LspRequestId, - proto_request: T::ProtoRequest, - position: Option, - cx: &mut 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(cx, |this, cx| { - this.buffer_store.read(cx).get_existing(buffer_id) - })??; - buffer - .update(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?; - let key = LspKey { - request_type: TypeId::of::(), - server_queried: for_server_id, - }; - lsp_store.update(cx, |lsp_store, cx| { - let request_task = match for_server_id { - Some(server_id) => { - let server_task = lsp_store.request_lsp( - buffer.clone(), - LanguageServerToQuery::Other(server_id), - request.clone(), - cx, - ); - cx.background_spawn(async move { - let mut responses = Vec::new(); - match server_task.await { - Ok(response) => responses.push((server_id, response)), - // rust-analyzer likes to error with this when its still loading up - Err(e) if format!("{e:#}").ends_with("content modified") => (), - Err(e) => log::error!( - "Error handling response for request {request:?}: {e:#}" - ), - } - responses - }) - } - None => lsp_store.request_multiple_lsp_locally(&buffer, position, request, cx), - }; - let lsp_data = lsp_store.latest_lsp_data(&buffer, cx); - if T::ProtoRequest::stop_previous_requests() { - if let Some(lsp_requests) = lsp_data.lsp_requests.get_mut(&key) { - lsp_requests.clear(); - } - } - lsp_data.lsp_requests.entry(key).or_default().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 { - Some( - self.lsp_data - .get_mut(&buffer_id)? - .code_lens - .take()? - .update - .take()? - .1, - ) - } - - pub fn downstream_client(&self) -> Option<(AnyProtoClient, u64)> { - self.downstream_client.clone() - } - - pub fn worktree_store(&self) -> Entity { - self.worktree_store.clone() - } - - /// Gets what's stored in the LSP data for the given buffer. - pub fn current_lsp_data(&mut self, buffer_id: BufferId) -> Option<&mut BufferLspData> { - self.lsp_data.get_mut(&buffer_id) - } - - /// Gets the most recent LSP data for the given buffer: if the data is absent or out of date, - /// new [`BufferLspData`] will be created to replace the previous state. - pub fn latest_lsp_data(&mut self, buffer: &Entity, cx: &mut App) -> &mut BufferLspData { - let (buffer_id, buffer_version) = - buffer.read_with(cx, |buffer, _| (buffer.remote_id(), buffer.version())); - let lsp_data = self - .lsp_data - .entry(buffer_id) - .or_insert_with(|| BufferLspData::new(buffer, cx)); - if buffer_version.changed_since(&lsp_data.buffer_version) { - *lsp_data = BufferLspData::new(buffer, cx); - } - lsp_data - } -} - -// 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( - languages: &Arc, - cx: &mut Context<'_, LspStore>, -) -> Task<()> { - let mut server_statuses = languages.language_server_binary_statuses(); - cx.spawn(async move |lsp_store, cx| { - while let Some((server_name, binary_status)) = server_statuses.next().await { - if lsp_store - .update(cx, |_, cx| { - let mut message = None; - let binary_status = match binary_status { - BinaryStatus::None => proto::ServerBinaryStatus::None, - BinaryStatus::CheckingForUpdate => { - proto::ServerBinaryStatus::CheckingForUpdate - } - BinaryStatus::Downloading => proto::ServerBinaryStatus::Downloading, - BinaryStatus::Starting => proto::ServerBinaryStatus::Starting, - BinaryStatus::Stopping => proto::ServerBinaryStatus::Stopping, - BinaryStatus::Stopped => proto::ServerBinaryStatus::Stopped, - BinaryStatus::Failed { error } => { - message = Some(error); - proto::ServerBinaryStatus::Failed - } - }; - cx.emit(LspStoreEvent::LanguageServerUpdate { - // Binary updates are about the binary that might not have any language server id at that point. - // Reuse `LanguageServerUpdate` for them and provide a fake id that won't be used on the receiver side. - language_server_id: LanguageServerId(0), - name: Some(server_name), - message: proto::update_language_server::Variant::StatusUpdate( - proto::StatusUpdate { - message, - status: Some(proto::status_update::Status::Binary( - binary_status as i32, - )), - }, - ), - }); - }) - .is_err() - { - break; - } - } - }) -} - -fn lsp_workspace_diagnostics_refresh( - registration_id: Option, - options: DiagnosticServerCapabilities, - server: Arc, - cx: &mut Context<'_, LspStore>, -) -> Option { - let identifier = workspace_diagnostic_identifier(&options)?; - let registration_id_shared = registration_id.as_ref().map(SharedString::from); - - let (progress_tx, mut progress_rx) = mpsc::channel(1); - let (mut refresh_tx, mut refresh_rx) = mpsc::channel(1); - refresh_tx.try_send(()).ok(); - - let workspace_query_language_server = cx.spawn(async move |lsp_store, cx| { - let mut attempts = 0; - let max_attempts = 50; - let mut requests = 0; - - loop { - let Some(()) = refresh_rx.recv().await else { - return; - }; - - 'request: loop { - requests += 1; - if attempts > max_attempts { - log::error!( - "Failed to pull workspace diagnostics {max_attempts} times, aborting" - ); - return; - } - let backoff_millis = (50 * (1 << attempts)).clamp(30, 1000); - cx.background_executor() - .timer(Duration::from_millis(backoff_millis)) - .await; - attempts += 1; - - let Ok(previous_result_ids) = lsp_store.update(cx, |lsp_store, _| { - lsp_store - .result_ids_for_workspace_refresh(server.server_id(), ®istration_id_shared) - .into_iter() - .filter_map(|(abs_path, result_id)| { - let uri = file_path_to_lsp_url(&abs_path).ok()?; - Some(lsp::PreviousResultId { - uri, - value: result_id.to_string(), - }) - }) - .collect() - }) else { - return; - }; - - let token = if let Some(registration_id) = ®istration_id { - format!( - "workspace/diagnostic/{}/{requests}/{WORKSPACE_DIAGNOSTICS_TOKEN_START}{registration_id}", - server.server_id(), - ) - } else { - format!("workspace/diagnostic/{}/{requests}", server.server_id()) - }; - - progress_rx.try_recv().ok(); - let timer = - LanguageServer::default_request_timer(cx.background_executor().clone()).fuse(); - let progress = pin!(progress_rx.recv().fuse()); - let response_result = server - .request_with_timer::( - lsp::WorkspaceDiagnosticParams { - previous_result_ids, - identifier: identifier.clone(), - work_done_progress_params: Default::default(), - partial_result_params: lsp::PartialResultParams { - partial_result_token: Some(lsp::ProgressToken::String(token)), - }, - }, - select(timer, progress).then(|either| match either { - Either::Left((message, ..)) => ready(message).left_future(), - Either::Right(..) => pending::().right_future(), - }), - ) - .await; - - // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic_refresh - // > If a server closes a workspace diagnostic pull request the client should re-trigger the request. - match response_result { - ConnectionResult::Timeout => { - log::error!("Timeout during workspace diagnostics pull"); - continue 'request; - } - ConnectionResult::ConnectionReset => { - log::error!("Server closed a workspace diagnostics pull request"); - continue 'request; - } - ConnectionResult::Result(Err(e)) => { - log::error!("Error during workspace diagnostics pull: {e:#}"); - break 'request; - } - ConnectionResult::Result(Ok(pulled_diagnostics)) => { - attempts = 0; - if lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.apply_workspace_diagnostic_report( - server.server_id(), - pulled_diagnostics, - registration_id_shared.clone(), - cx, - ) - }) - .is_err() - { - return; - } - break 'request; - } - } - } - } - }); - - Some(WorkspaceRefreshTask { - refresh_tx, - progress_tx, - task: workspace_query_language_server, - }) -} - -fn buffer_diagnostic_identifier(options: &DiagnosticServerCapabilities) -> Option { - match &options { - lsp::DiagnosticServerCapabilities::Options(diagnostic_options) => { - diagnostic_options.identifier.clone() - } - lsp::DiagnosticServerCapabilities::RegistrationOptions(registration_options) => { - let diagnostic_options = ®istration_options.diagnostic_options; - diagnostic_options.identifier.clone() - } - } -} - -fn workspace_diagnostic_identifier( - options: &DiagnosticServerCapabilities, -) -> Option> { - match &options { - lsp::DiagnosticServerCapabilities::Options(diagnostic_options) => { - if !diagnostic_options.workspace_diagnostics { - return None; - } - Some(diagnostic_options.identifier.clone()) - } - lsp::DiagnosticServerCapabilities::RegistrationOptions(registration_options) => { - let diagnostic_options = ®istration_options.diagnostic_options; - if !diagnostic_options.workspace_diagnostics { - return None; - } - Some(diagnostic_options.identifier.clone()) - } - } -} - -fn resolve_word_completion(snapshot: &BufferSnapshot, completion: &mut Completion) { - let CompletionSource::BufferWord { - word_range, - resolved, - } = &mut completion.source - else { - return; - }; - if *resolved { - return; - } - - if completion.new_text - != snapshot - .text_for_range(word_range.clone()) - .collect::() - { - return; - } - - let mut offset = 0; - for chunk in snapshot.chunks(word_range.clone(), true) { - let end_offset = offset + chunk.text.len(); - if let Some(highlight_id) = chunk.syntax_highlight_id { - completion - .label - .runs - .push((offset..end_offset, highlight_id)); - } - offset = end_offset; - } - *resolved = true; -} - -impl EventEmitter for LspStore {} - -fn remove_empty_hover_blocks(mut hover: Hover) -> Option { - hover - .contents - .retain(|hover_block| !hover_block.text.trim().is_empty()); - if hover.contents.is_empty() { - None - } else { - Some(hover) - } -} - -async fn populate_labels_for_completions( - new_completions: Vec, - language: Option>, - lsp_adapter: Option>, -) -> Vec { - let lsp_completions = new_completions - .iter() - .filter_map(|new_completion| { - new_completion - .source - .lsp_completion(true) - .map(|lsp_completion| lsp_completion.into_owned()) - }) - .collect::>(); - - let mut labels = if let Some((language, lsp_adapter)) = language.as_ref().zip(lsp_adapter) { - lsp_adapter - .labels_for_completions(&lsp_completions, language) - .await - .log_err() - .unwrap_or_default() - } else { - Vec::new() - } - .into_iter() - .fuse(); - - let mut completions = Vec::new(); - 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 mut label = labels.next().flatten().unwrap_or_else(|| { - CodeLabel::fallback_for_completion(&lsp_completion, language.as_deref()) - }); - ensure_uniform_list_compatible_label(&mut label); - completions.push(Completion { - label, - documentation, - replace_range: completion.replace_range, - new_text: completion.new_text, - insert_text_mode: lsp_completion.insert_text_mode, - source: completion.source, - icon_path: None, - confirm: None, - match_start: None, - snippet_deduplication_key: None, - }); - } - None => { - let mut label = CodeLabel::plain(completion.new_text.clone(), None); - ensure_uniform_list_compatible_label(&mut label); - completions.push(Completion { - label, - documentation: None, - replace_range: completion.replace_range, - new_text: completion.new_text, - source: completion.source, - insert_text_mode: None, - icon_path: None, - confirm: None, - match_start: None, - snippet_deduplication_key: None, - }); - } - } - } - completions -} - -#[derive(Debug)] -pub enum LanguageServerToQuery { - /// Query language servers in order of users preference, up until one capable of handling the request is found. - FirstCapable, - /// Query a specific language server. - Other(LanguageServerId), -} - -#[derive(Default)] -struct RenamePathsWatchedForServer { - did_rename: Vec, - will_rename: Vec, -} - -impl RenamePathsWatchedForServer { - fn with_did_rename_patterns( - mut self, - did_rename: Option<&FileOperationRegistrationOptions>, - ) -> Self { - if let Some(did_rename) = did_rename { - self.did_rename = did_rename - .filters - .iter() - .filter_map(|filter| filter.try_into().log_err()) - .collect(); - } - self - } - fn with_will_rename_patterns( - mut self, - will_rename: Option<&FileOperationRegistrationOptions>, - ) -> Self { - if let Some(will_rename) = will_rename { - self.will_rename = will_rename - .filters - .iter() - .filter_map(|filter| filter.try_into().log_err()) - .collect(); - } - self - } - - fn should_send_did_rename(&self, path: &str, is_dir: bool) -> bool { - self.did_rename.iter().any(|pred| pred.eval(path, is_dir)) - } - fn should_send_will_rename(&self, path: &str, is_dir: bool) -> bool { - self.will_rename.iter().any(|pred| pred.eval(path, is_dir)) - } -} - -impl TryFrom<&FileOperationFilter> for RenameActionPredicate { - type Error = globset::Error; - fn try_from(ops: &FileOperationFilter) -> Result { - Ok(Self { - kind: ops.pattern.matches.clone(), - glob: GlobBuilder::new(&ops.pattern.glob) - .case_insensitive( - ops.pattern - .options - .as_ref() - .is_some_and(|ops| ops.ignore_case.unwrap_or(false)), - ) - .build()? - .compile_matcher(), - }) - } -} -struct RenameActionPredicate { - glob: GlobMatcher, - kind: Option, -} - -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| { - let expected_kind = if is_dir { - FileOperationPatternKind::Folder - } else { - FileOperationPatternKind::File - }; - kind == &expected_kind - }) && self.glob.is_match(path) - } -} - -#[derive(Default)] -struct LanguageServerWatchedPaths { - worktree_paths: HashMap, - abs_paths: HashMap, (GlobSet, Task<()>)>, -} - -#[derive(Default)] -struct LanguageServerWatchedPathsBuilder { - worktree_paths: HashMap, - abs_paths: HashMap, GlobSet>, -} - -impl LanguageServerWatchedPathsBuilder { - fn watch_worktree(&mut self, worktree_id: WorktreeId, glob_set: GlobSet) { - self.worktree_paths.insert(worktree_id, glob_set); - } - fn watch_abs_path(&mut self, path: Arc, glob_set: GlobSet) { - self.abs_paths.insert(path, glob_set); - } - fn build( - self, - fs: Arc, - language_server_id: LanguageServerId, - cx: &mut Context, - ) -> LanguageServerWatchedPaths { - let lsp_store = cx.weak_entity(); - - const LSP_ABS_PATH_OBSERVE: Duration = Duration::from_millis(100); - let abs_paths = self - .abs_paths - .into_iter() - .map(|(abs_path, globset)| { - let task = cx.spawn({ - let abs_path = abs_path.clone(); - let fs = fs.clone(); - - let lsp_store = lsp_store.clone(); - async move |_, cx| { - maybe!(async move { - let mut push_updates = fs.watch(&abs_path, LSP_ABS_PATH_OBSERVE).await; - while let Some(update) = push_updates.0.next().await { - let action = lsp_store - .update(cx, |this, _| { - let Some(local) = this.as_local() else { - return ControlFlow::Break(()); - }; - let Some(watcher) = local - .language_server_watched_paths - .get(&language_server_id) - else { - return ControlFlow::Break(()); - }; - let (globs, _) = watcher.abs_paths.get(&abs_path).expect( - "Watched abs path is not registered with a watcher", - ); - let matching_entries = update - .into_iter() - .filter(|event| globs.is_match(&event.path)) - .collect::>(); - this.lsp_notify_abs_paths_changed( - language_server_id, - matching_entries, - ); - ControlFlow::Continue(()) - }) - .ok()?; - - if action.is_break() { - break; - } - } - Some(()) - }) - .await; - } - }); - (abs_path, (globset, task)) - }) - .collect(); - LanguageServerWatchedPaths { - worktree_paths: self.worktree_paths, - abs_paths, - } - } -} - -struct LspBufferSnapshot { - version: i32, - snapshot: TextBufferSnapshot, -} - -/// A prompt requested by LSP server. -#[derive(Clone, Debug)] -pub struct LanguageServerPromptRequest { - pub level: PromptLevel, - pub message: String, - pub actions: Vec, - pub lsp_name: String, - pub(crate) response_channel: Sender, -} - -impl LanguageServerPromptRequest { - pub async fn respond(self, index: usize) -> Option<()> { - if let Some(response) = self.actions.into_iter().nth(index) { - self.response_channel.send(response).await.ok() - } else { - None - } - } -} -impl PartialEq for LanguageServerPromptRequest { - fn eq(&self, other: &Self) -> bool { - self.message == other.message && self.actions == other.actions - } -} - -#[derive(Clone, Debug, PartialEq)] -pub enum LanguageServerLogType { - Log(MessageType), - Trace { verbose_info: Option }, - Rpc { received: bool }, -} - -impl LanguageServerLogType { - pub fn to_proto(&self) -> proto::language_server_log::LogType { - match self { - Self::Log(log_type) => { - use proto::log_message::LogLevel; - let level = match *log_type { - MessageType::ERROR => LogLevel::Error, - MessageType::WARNING => LogLevel::Warning, - MessageType::INFO => LogLevel::Info, - MessageType::LOG => LogLevel::Log, - other => { - log::warn!("Unknown lsp log message type: {other:?}"); - LogLevel::Log - } - }; - proto::language_server_log::LogType::Log(proto::LogMessage { - level: level as i32, - }) - } - Self::Trace { verbose_info } => { - proto::language_server_log::LogType::Trace(proto::TraceMessage { - verbose_info: verbose_info.to_owned(), - }) - } - Self::Rpc { received } => { - let kind = if *received { - proto::rpc_message::Kind::Received - } else { - proto::rpc_message::Kind::Sent - }; - let kind = kind as i32; - proto::language_server_log::LogType::Rpc(proto::RpcMessage { kind }) - } - } - } - - pub fn from_proto(log_type: proto::language_server_log::LogType) -> Self { - use proto::log_message::LogLevel; - use proto::rpc_message; - match log_type { - proto::language_server_log::LogType::Log(message_type) => Self::Log( - match LogLevel::from_i32(message_type.level).unwrap_or(LogLevel::Log) { - LogLevel::Error => MessageType::ERROR, - LogLevel::Warning => MessageType::WARNING, - LogLevel::Info => MessageType::INFO, - LogLevel::Log => MessageType::LOG, - }, - ), - proto::language_server_log::LogType::Trace(trace_message) => Self::Trace { - verbose_info: trace_message.verbose_info, - }, - proto::language_server_log::LogType::Rpc(message) => Self::Rpc { - received: match rpc_message::Kind::from_i32(message.kind) - .unwrap_or(rpc_message::Kind::Received) - { - rpc_message::Kind::Received => true, - rpc_message::Kind::Sent => false, - }, - }, - } - } -} - -pub struct WorkspaceRefreshTask { - refresh_tx: mpsc::Sender<()>, - progress_tx: mpsc::Sender<()>, - #[allow(dead_code)] - task: Task<()>, -} - -pub enum LanguageServerState { - Starting { - startup: Task>>, - /// List of language servers that will be added to the workspace once it's initialization completes. - pending_workspace_folders: Arc>>, - }, - - Running { - adapter: Arc, - server: Arc, - simulate_disk_based_diagnostics_completion: Option>, - workspace_diagnostics_refresh_tasks: HashMap, WorkspaceRefreshTask>, - }, -} - -impl LanguageServerState { - fn add_workspace_folder(&self, uri: Uri) { - match self { - LanguageServerState::Starting { - pending_workspace_folders, - .. - } => { - pending_workspace_folders.lock().insert(uri); - } - LanguageServerState::Running { server, .. } => { - server.add_workspace_folder(uri); - } - } - } - fn _remove_workspace_folder(&self, uri: Uri) { - match self { - LanguageServerState::Starting { - pending_workspace_folders, - .. - } => { - pending_workspace_folders.lock().remove(&uri); - } - LanguageServerState::Running { server, .. } => server.remove_workspace_folder(uri), - } - } -} - -impl std::fmt::Debug for LanguageServerState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - LanguageServerState::Starting { .. } => { - f.debug_struct("LanguageServerState::Starting").finish() - } - LanguageServerState::Running { .. } => { - f.debug_struct("LanguageServerState::Running").finish() - } - } - } -} - -#[derive(Clone, Debug, Serialize)] -pub struct LanguageServerProgress { - pub is_disk_based_diagnostics_progress: bool, - pub is_cancellable: bool, - pub title: Option, - pub message: Option, - pub percentage: Option, - #[serde(skip_serializing)] - pub last_update_at: Instant, -} - -#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize)] -pub struct DiagnosticSummary { - pub error_count: usize, - pub warning_count: usize, -} - -impl DiagnosticSummary { - pub fn new<'a, T: 'a>(diagnostics: impl IntoIterator>) -> Self { - let mut this = Self { - error_count: 0, - warning_count: 0, - }; - - for entry in diagnostics { - if entry.diagnostic.is_primary { - match entry.diagnostic.severity { - DiagnosticSeverity::ERROR => this.error_count += 1, - DiagnosticSeverity::WARNING => this.warning_count += 1, - _ => {} - } - } - } - - this - } - - pub fn is_empty(&self) -> bool { - self.error_count == 0 && self.warning_count == 0 - } - - pub fn to_proto( - self, - language_server_id: LanguageServerId, - path: &RelPath, - ) -> proto::DiagnosticSummary { - proto::DiagnosticSummary { - path: path.to_proto(), - language_server_id: language_server_id.0 as u64, - error_count: self.error_count as u32, - warning_count: self.warning_count as u32, - } - } -} - -#[derive(Clone, Debug)] -pub enum CompletionDocumentation { - /// There is no documentation for this completion. - Undocumented, - /// A single line of documentation. - SingleLine(SharedString), - /// Multiple lines of plain text documentation. - MultiLinePlainText(SharedString), - /// Markdown documentation. - MultiLineMarkdown(SharedString), - /// Both single line and multiple lines of plain text documentation. - SingleLineAndMultiLinePlainText { - single_line: SharedString, - plain_text: Option, - }, -} - -impl CompletionDocumentation { - #[cfg(any(test, feature = "test-support"))] - pub fn text(&self) -> SharedString { - match self { - CompletionDocumentation::Undocumented => "".into(), - CompletionDocumentation::SingleLine(s) => s.clone(), - CompletionDocumentation::MultiLinePlainText(s) => s.clone(), - CompletionDocumentation::MultiLineMarkdown(s) => s.clone(), - CompletionDocumentation::SingleLineAndMultiLinePlainText { single_line, .. } => { - single_line.clone() - } - } - } -} - -impl From for CompletionDocumentation { - fn from(docs: lsp::Documentation) -> Self { - match docs { - lsp::Documentation::String(text) => { - if text.lines().count() <= 1 { - CompletionDocumentation::SingleLine(text.into()) - } else { - CompletionDocumentation::MultiLinePlainText(text.into()) - } - } - - lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind { - lsp::MarkupKind::PlainText => { - if value.lines().count() <= 1 { - CompletionDocumentation::SingleLine(value.into()) - } else { - CompletionDocumentation::MultiLinePlainText(value.into()) - } - } - - lsp::MarkupKind::Markdown => { - CompletionDocumentation::MultiLineMarkdown(value.into()) - } - }, - } - } -} - -pub enum ResolvedHint { - Resolved(InlayHint), - Resolving(Shared>), -} - -fn glob_literal_prefix(glob: &Path) -> PathBuf { - glob.components() - .take_while(|component| match component { - path::Component::Normal(part) => !part.to_string_lossy().contains(['*', '?', '{', '}']), - _ => true, - }) - .collect() -} - -pub struct SshLspAdapter { - name: LanguageServerName, - binary: LanguageServerBinary, - initialization_options: Option, - code_action_kinds: Option>, -} - -impl SshLspAdapter { - pub fn new( - name: LanguageServerName, - binary: LanguageServerBinary, - initialization_options: Option, - code_action_kinds: Option, - ) -> Self { - Self { - name, - binary, - initialization_options, - code_action_kinds: code_action_kinds - .as_ref() - .and_then(|c| serde_json::from_str(c).ok()), - } - } -} - -impl LspInstaller for SshLspAdapter { - type BinaryVersion = (); - async fn check_if_user_installed( - &self, - _: &dyn LspAdapterDelegate, - _: Option, - _: &AsyncApp, - ) -> Option { - Some(self.binary.clone()) - } - - async fn cached_server_binary( - &self, - _: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Option { - None - } - - async fn fetch_latest_server_version( - &self, - _: &dyn LspAdapterDelegate, - _: bool, - _: &mut AsyncApp, - ) -> Result<()> { - anyhow::bail!("SshLspAdapter does not support fetch_latest_server_version") - } - - async fn fetch_server_binary( - &self, - _: (), - _: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Result { - anyhow::bail!("SshLspAdapter does not support fetch_server_binary") - } -} - -#[async_trait(?Send)] -impl LspAdapter for SshLspAdapter { - fn name(&self) -> LanguageServerName { - self.name.clone() - } - - async fn initialization_options( - self: Arc, - _: &Arc, - ) -> Result> { - let Some(options) = &self.initialization_options else { - return Ok(None); - }; - let result = serde_json::from_str(options)?; - Ok(result) - } - - fn code_action_kinds(&self) -> Option> { - self.code_action_kinds.clone() - } -} - -pub fn language_server_settings<'a>( - delegate: &'a dyn LspAdapterDelegate, - language: &LanguageServerName, - cx: &'a App, -) -> Option<&'a LspSettings> { - language_server_settings_for( - SettingsLocation { - worktree_id: delegate.worktree_id(), - path: RelPath::empty(), - }, - language, - cx, - ) -} - -pub fn language_server_settings_for<'a>( - location: SettingsLocation<'a>, - language: &LanguageServerName, - cx: &'a App, -) -> Option<&'a LspSettings> { - ProjectSettings::get(Some(location), cx).lsp.get(language) -} - -pub struct LocalLspAdapterDelegate { - lsp_store: WeakEntity, - worktree: worktree::Snapshot, - fs: Arc, - http_client: Arc, - language_registry: Arc, - load_shell_env_task: Shared>>>, -} - -impl LocalLspAdapterDelegate { - pub fn new( - language_registry: Arc, - environment: &Entity, - lsp_store: WeakEntity, - worktree: &Entity, - http_client: Arc, - fs: Arc, - cx: &mut App, - ) -> Arc { - let load_shell_env_task = - environment.update(cx, |env, cx| env.worktree_environment(worktree.clone(), cx)); - - Arc::new(Self { - lsp_store, - worktree: worktree.read(cx).snapshot(), - fs, - http_client, - language_registry, - load_shell_env_task, - }) - } - - fn from_local_lsp( - local: &LocalLspStore, - worktree: &Entity, - cx: &mut App, - ) -> Arc { - Self::new( - local.languages.clone(), - &local.environment, - local.weak.clone(), - worktree, - local.http_client.clone(), - local.fs.clone(), - cx, - ) - } -} - -#[async_trait] -impl LspAdapterDelegate for LocalLspAdapterDelegate { - fn show_notification(&self, message: &str, cx: &mut App) { - self.lsp_store - .update(cx, |_, cx| { - cx.emit(LspStoreEvent::Notification(message.to_owned())) - }) - .ok(); - } - - fn http_client(&self) -> Arc { - self.http_client.clone() - } - - fn worktree_id(&self) -> WorktreeId { - self.worktree.id() - } - - fn worktree_root_path(&self) -> &Path { - self.worktree.abs_path().as_ref() - } - - fn resolve_executable_path(&self, path: PathBuf) -> PathBuf { - self.worktree.resolve_executable_path(path) - } - - async fn shell_env(&self) -> HashMap { - let task = self.load_shell_env_task.clone(); - task.await.unwrap_or_default() - } - - async fn npm_package_installed_version( - &self, - package_name: &str, - ) -> Result> { - let local_package_directory = self.worktree_root_path(); - let node_modules_directory = local_package_directory.join("node_modules"); - - if let Some(version) = - read_package_installed_version(node_modules_directory.clone(), package_name).await? - { - return Ok(Some((node_modules_directory, version))); - } - let Some(npm) = self.which("npm".as_ref()).await else { - log::warn!( - "Failed to find npm executable for {:?}", - local_package_directory - ); - return Ok(None); - }; - - let env = self.shell_env().await; - let output = util::command::new_smol_command(&npm) - .args(["root", "-g"]) - .envs(env) - .current_dir(local_package_directory) - .output() - .await?; - let global_node_modules = - PathBuf::from(String::from_utf8_lossy(&output.stdout).to_string()); - - if let Some(version) = - read_package_installed_version(global_node_modules.clone(), package_name).await? - { - return Ok(Some((global_node_modules, version))); - } - return Ok(None); - } - - async fn which(&self, command: &OsStr) -> Option { - let mut worktree_abs_path = self.worktree_root_path().to_path_buf(); - if self.fs.is_file(&worktree_abs_path).await { - worktree_abs_path.pop(); - } - - let env = self.shell_env().await; - - let shell_path = env.get("PATH").cloned(); - - which::which_in(command, shell_path.as_ref(), worktree_abs_path).ok() - } - - async fn try_exec(&self, command: LanguageServerBinary) -> Result<()> { - let mut working_dir = self.worktree_root_path().to_path_buf(); - if self.fs.is_file(&working_dir).await { - working_dir.pop(); - } - let output = util::command::new_smol_command(&command.path) - .args(command.arguments) - .envs(command.env.clone().unwrap_or_default()) - .current_dir(working_dir) - .output() - .await?; - - anyhow::ensure!( - output.status.success(), - "{}, stdout: {:?}, stderr: {:?}", - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - Ok(()) - } - - fn update_status(&self, server_name: LanguageServerName, status: language::BinaryStatus) { - self.language_registry - .update_lsp_binary_status(server_name, status); - } - - fn registered_lsp_adapters(&self) -> Vec> { - self.language_registry - .all_lsp_adapters() - .into_iter() - .map(|adapter| adapter.adapter.clone() as Arc) - .collect() - } - - async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option> { - let dir = self.language_registry.language_server_download_dir(name)?; - - if !dir.exists() { - smol::fs::create_dir_all(&dir) - .await - .context("failed to create container directory") - .log_err()?; - } - - Some(dir) - } - - async fn read_text_file(&self, path: &RelPath) -> Result { - let entry = self - .worktree - .entry_for_path(path) - .with_context(|| format!("no worktree entry for path {path:?}"))?; - let abs_path = self.worktree.absolutize(&entry.path); - self.fs.load(&abs_path).await - } -} - -async fn populate_labels_for_symbols( - symbols: Vec, - language_registry: &Arc, - lsp_adapter: Option>, - output: &mut Vec, -) { - #[allow(clippy::mutable_key_type)] - let mut symbols_by_language = HashMap::>, Vec>::default(); - - let mut unknown_paths = BTreeSet::>::new(); - for symbol in symbols { - let Some(file_name) = symbol.path.file_name() else { - continue; - }; - let language = language_registry - .load_language_for_file_path(Path::new(file_name)) - .await - .ok() - .or_else(|| { - unknown_paths.insert(file_name.into()); - None - }); - symbols_by_language - .entry(language) - .or_default() - .push(symbol); - } - - for unknown_path in unknown_paths { - log::info!("no language found for symbol in file {unknown_path:?}"); - } - - let mut label_params = Vec::new(); - for (language, mut symbols) in symbols_by_language { - label_params.clear(); - label_params.extend( - symbols - .iter_mut() - .map(|symbol| (mem::take(&mut symbol.name), symbol.kind)), - ); - - let mut labels = Vec::new(); - if let Some(language) = language { - let lsp_adapter = lsp_adapter.clone().or_else(|| { - language_registry - .lsp_adapters(&language.name()) - .first() - .cloned() - }); - if let Some(lsp_adapter) = lsp_adapter { - labels = lsp_adapter - .labels_for_symbols(&label_params, &language) - .await - .log_err() - .unwrap_or_default(); - } - } - - for ((symbol, (name, _)), label) in symbols - .into_iter() - .zip(label_params.drain(..)) - .zip(labels.into_iter().chain(iter::repeat(None))) - { - output.push(Symbol { - language_server_name: symbol.language_server_name, - source_worktree_id: symbol.source_worktree_id, - source_language_server_id: symbol.source_language_server_id, - path: symbol.path, - label: label.unwrap_or_else(|| CodeLabel::plain(name.clone(), None)), - name, - kind: symbol.kind, - range: symbol.range, - }); - } - } -} - -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::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, - } -} - -/// Completion items are displayed in a `UniformList`. -/// Usually, those items are single-line strings, but in LSP responses, -/// completion items `label`, `detail` and `label_details.description` may contain newlines or long spaces. -/// Many language plugins construct these items by joining these parts together, and we may use `CodeLabel::fallback_for_completion` that uses `label` at least. -/// All that may lead to a newline being inserted into resulting `CodeLabel.text`, which will force `UniformList` to bloat each entry to occupy more space, -/// breaking the completions menu presentation. -/// -/// Sanitize the text to ensure there are no newlines, or, if there are some, remove them and also remove long space sequences if there were newlines. -fn ensure_uniform_list_compatible_label(label: &mut CodeLabel) { - let mut new_text = String::with_capacity(label.text.len()); - 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 newlines_removed = false; - - for (idx, c) in chars { - offset_map[idx] = new_idx; - - match c { - '\n' if last_char_was_space => { - newlines_removed = true; - } - '\t' | ' ' if last_char_was_space => {} - '\n' if !last_char_was_space => { - new_text.push(' '); - new_idx += 1; - last_char_was_space = true; - newlines_removed = true; - } - ' ' | '\t' => { - new_text.push(' '); - new_idx += 1; - last_char_was_space = true; - } - _ => { - new_text.push(c); - new_idx += c.len_utf8(); - last_char_was_space = false; - } - } - } - offset_map[label.text.len()] = new_idx; - - // Only modify the label if newlines were removed. - if !newlines_removed { - return; - } - - let last_index = new_idx; - let mut run_ranges_errors = Vec::new(); - label.runs.retain_mut(|(range, _)| { - match offset_map.get(range.start) { - Some(&start) => range.start = start, - None => { - run_ranges_errors.push(range.clone()); - return false; - } - } - - match offset_map.get(range.end) { - Some(&end) => range.end = end, - None => { - run_ranges_errors.push(range.clone()); - range.end = last_index; - } - } - true - }); - if !run_ranges_errors.is_empty() { - log::error!( - "Completion label has errors in its run ranges: {run_ranges_errors:?}, label text: {}", - label.text - ); - } - - let mut wrong_filter_range = None; - if label.filter_range == (0..label.text.len()) { - label.filter_range = 0..new_text.len(); - } else { - let mut original_filter_range = Some(label.filter_range.clone()); - match offset_map.get(label.filter_range.start) { - Some(&start) => label.filter_range.start = start, - None => { - wrong_filter_range = original_filter_range.take(); - label.filter_range.start = last_index; - } - } - - match offset_map.get(label.filter_range.end) { - Some(&end) => label.filter_range.end = end, - None => { - wrong_filter_range = original_filter_range.take(); - label.filter_range.end = last_index; - } - } - } - if let Some(wrong_filter_range) = wrong_filter_range { - log::error!( - "Completion label has an invalid filter range: {wrong_filter_range:?}, label text: {}", - label.text - ); - } - - label.text = new_text; -} - -#[cfg(test)] -mod tests { - use language::HighlightId; - - use super::*; - - #[test] - fn test_glob_literal_prefix() { - assert_eq!(glob_literal_prefix(Path::new("**/*.js")), Path::new("")); - assert_eq!( - glob_literal_prefix(Path::new("node_modules/**/*.js")), - Path::new("node_modules") - ); - assert_eq!( - glob_literal_prefix(Path::new("foo/{bar,baz}.js")), - Path::new("foo") - ); - assert_eq!( - glob_literal_prefix(Path::new("foo/bar/baz.js")), - Path::new("foo/bar/baz.js") - ); - - #[cfg(target_os = "windows")] - { - assert_eq!(glob_literal_prefix(Path::new("**\\*.js")), Path::new("")); - assert_eq!( - glob_literal_prefix(Path::new("node_modules\\**/*.js")), - Path::new("node_modules") - ); - assert_eq!( - glob_literal_prefix(Path::new("foo/{bar,baz}.js")), - Path::new("foo") - ); - assert_eq!( - glob_literal_prefix(Path::new("foo\\bar\\baz.js")), - Path::new("foo/bar/baz.js") - ); - } - } - - #[test] - fn test_multi_len_chars_normalization() { - let mut label = CodeLabel::new( - "myElˇ (parameter) myElˇ: {\n foo: string;\n}".to_string(), - 0..6, - vec![(0..6, HighlightId(1))], - ); - ensure_uniform_list_compatible_label(&mut label); - assert_eq!( - label, - CodeLabel::new( - "myElˇ (parameter) myElˇ: { foo: string; }".to_string(), - 0..6, - vec![(0..6, HighlightId(1))], - ) - ); - } -} diff --git a/crates/project/src/lsp_store/clangd_ext.rs b/crates/project/src/lsp_store/clangd_ext.rs deleted file mode 100644 index 466d0c6e2a..0000000000 --- a/crates/project/src/lsp_store/clangd_ext.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::{borrow::Cow, sync::Arc}; - -use ::serde::{Deserialize, Serialize}; -use gpui::WeakEntity; -use language::{CachedLspAdapter, Diagnostic, DiagnosticSourceKind}; -use lsp::{LanguageServer, LanguageServerName}; -use util::ResultExt as _; - -use crate::{LspStore, lsp_store::DocumentDiagnosticsUpdate}; - -pub const CLANGD_SERVER_NAME: LanguageServerName = LanguageServerName::new_static("clangd"); -const INACTIVE_REGION_MESSAGE: &str = "inactive region"; -const INACTIVE_DIAGNOSTIC_SEVERITY: lsp::DiagnosticSeverity = lsp::DiagnosticSeverity::INFORMATION; - -#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct InactiveRegionsParams { - pub text_document: lsp::OptionalVersionedTextDocumentIdentifier, - pub regions: Vec, -} - -/// InactiveRegions is a clangd extension that marks regions of inactive code. -pub struct InactiveRegions; - -impl lsp::notification::Notification for InactiveRegions { - type Params = InactiveRegionsParams; - const METHOD: &'static str = "textDocument/inactiveRegions"; -} - -pub fn is_inactive_region(diag: &Diagnostic) -> bool { - diag.is_unnecessary - && diag.severity == INACTIVE_DIAGNOSTIC_SEVERITY - && diag.message == INACTIVE_REGION_MESSAGE - && diag - .source - .as_ref() - .is_some_and(|v| v == &CLANGD_SERVER_NAME.0) -} - -pub fn is_lsp_inactive_region(diag: &lsp::Diagnostic) -> bool { - diag.severity == Some(INACTIVE_DIAGNOSTIC_SEVERITY) - && diag.message == INACTIVE_REGION_MESSAGE - && diag - .source - .as_ref() - .is_some_and(|v| v == &CLANGD_SERVER_NAME.0) -} - -pub fn register_notifications( - lsp_store: WeakEntity, - language_server: &LanguageServer, - adapter: Arc, -) { - if language_server.name() != CLANGD_SERVER_NAME { - return; - } - let server_id = language_server.server_id(); - - language_server - .on_notification::({ - let adapter = adapter; - let this = lsp_store; - - move |params: InactiveRegionsParams, cx| { - let adapter = adapter.clone(); - this.update(cx, |this, cx| { - let diagnostics = params - .regions - .into_iter() - .map(|range| lsp::Diagnostic { - range, - severity: Some(INACTIVE_DIAGNOSTIC_SEVERITY), - source: Some(CLANGD_SERVER_NAME.to_string()), - message: INACTIVE_REGION_MESSAGE.to_string(), - tags: Some(vec![lsp::DiagnosticTag::UNNECESSARY]), - ..lsp::Diagnostic::default() - }) - .collect(); - let mapped_diagnostics = lsp::PublishDiagnosticsParams { - uri: params.text_document.uri, - version: params.text_document.version, - diagnostics, - }; - this.merge_lsp_diagnostics( - DiagnosticSourceKind::Pushed, - vec![DocumentDiagnosticsUpdate { - server_id, - diagnostics: mapped_diagnostics, - result_id: None, - disk_based_sources: Cow::Borrowed( - &adapter.disk_based_diagnostic_sources, - ), - registration_id: None, - }], - |_, diag, _| !is_inactive_region(diag), - cx, - ) - .log_err(); - }) - .ok(); - } - }) - .detach(); -} diff --git a/crates/project/src/lsp_store/inlay_hint_cache.rs b/crates/project/src/lsp_store/inlay_hint_cache.rs deleted file mode 100644 index 804552b52c..0000000000 --- a/crates/project/src/lsp_store/inlay_hint_cache.rs +++ /dev/null @@ -1,233 +0,0 @@ -use std::{collections::hash_map, ops::Range, sync::Arc}; - -use collections::HashMap; -use futures::future::Shared; -use gpui::{App, Entity, Task}; -use language::{ - Buffer, - row_chunk::{RowChunk, RowChunks}, -}; -use lsp::LanguageServerId; -use text::Anchor; - -use crate::{InlayHint, InlayId}; - -pub type CacheInlayHints = HashMap>; -pub type CacheInlayHintsTask = Shared>>>; - -/// A logic to apply when querying for new inlay hints and deciding what to do with the old entries in the cache in case of conflicts. -#[derive(Debug, Clone, Copy)] -pub enum InvalidationStrategy { - /// Language servers reset hints via request. - /// Demands to re-query all inlay hints needed and invalidate all cached entries, but does not require instant update with invalidation. - /// - /// Despite nothing forbids language server from sending this request on every edit, it is expected to be sent only when certain internal server state update, invisible for the editor otherwise. - RefreshRequested { - server_id: LanguageServerId, - request_id: Option, - }, - /// Multibuffer excerpt(s) and/or singleton buffer(s) were edited at least on one place. - /// Neither editor nor LSP is able to tell which open file hints' are not affected, so all of them have to be invalidated, re-queried and do that fast enough to avoid being slow, but also debounce to avoid loading hints on every fast keystroke sequence. - BufferEdited, - /// A new file got opened/new excerpt was added to a multibuffer/a [multi]buffer was scrolled to a new position. - /// No invalidation should be done at all, all new hints are added to the cache. - /// - /// A special case is the editor toggles and settings change: - /// in addition to LSP capabilities, Zed allows omitting certain hint kinds (defined by the corresponding LSP part: type/parameter/other) and toggling hints. - /// This does not lead to cache invalidation, but would require cache usage for determining which hints are not displayed and issuing an update to inlays on the screen. - None, -} - -impl InvalidationStrategy { - pub fn should_invalidate(&self) -> bool { - matches!( - self, - InvalidationStrategy::RefreshRequested { .. } | InvalidationStrategy::BufferEdited - ) - } -} - -pub struct BufferInlayHints { - chunks: RowChunks, - hints_by_chunks: Vec>, - fetches_by_chunks: Vec>, - hints_by_id: HashMap, - latest_invalidation_requests: HashMap>, - pub(super) hint_resolves: HashMap>>, -} - -#[derive(Debug, Clone, Copy)] -struct HintForId { - chunk_id: usize, - server_id: LanguageServerId, - position: usize, -} - -impl std::fmt::Debug for BufferInlayHints { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("BufferInlayHints") - .field("buffer_chunks", &self.chunks) - .field("hints_by_chunks", &self.hints_by_chunks) - .field("fetches_by_chunks", &self.fetches_by_chunks) - .field("hints_by_id", &self.hints_by_id) - .finish_non_exhaustive() - } -} - -const MAX_ROWS_IN_A_CHUNK: u32 = 50; - -impl BufferInlayHints { - pub fn new(buffer: &Entity, cx: &mut App) -> Self { - let chunks = RowChunks::new(buffer.read(cx).text_snapshot(), MAX_ROWS_IN_A_CHUNK); - - Self { - hints_by_chunks: vec![None; chunks.len()], - fetches_by_chunks: vec![None; chunks.len()], - latest_invalidation_requests: HashMap::default(), - hints_by_id: HashMap::default(), - hint_resolves: HashMap::default(), - chunks, - } - } - - pub fn applicable_chunks( - &self, - ranges: &[Range], - ) -> impl Iterator { - self.chunks.applicable_chunks(ranges) - } - - pub fn cached_hints(&mut self, chunk: &RowChunk) -> Option<&CacheInlayHints> { - self.hints_by_chunks[chunk.id].as_ref() - } - - pub fn fetched_hints(&mut self, chunk: &RowChunk) -> &mut Option { - &mut self.fetches_by_chunks[chunk.id] - } - - #[cfg(any(test, feature = "test-support"))] - pub fn all_cached_hints(&self) -> Vec { - self.hints_by_chunks - .iter() - .filter_map(|hints| hints.as_ref()) - .flat_map(|hints| hints.values().cloned()) - .flatten() - .map(|(_, hint)| hint) - .collect() - } - - #[cfg(any(test, feature = "test-support"))] - pub fn all_fetched_hints(&self) -> Vec { - self.fetches_by_chunks - .iter() - .filter_map(|fetches| fetches.clone()) - .collect() - } - - pub fn remove_server_data(&mut self, for_server: LanguageServerId) { - for (chunk_index, hints) in self.hints_by_chunks.iter_mut().enumerate() { - if let Some(hints) = hints { - if hints.remove(&for_server).is_some() { - self.fetches_by_chunks[chunk_index] = None; - } - } - } - } - - pub fn clear(&mut self) { - self.hints_by_chunks = vec![None; self.chunks.len()]; - self.fetches_by_chunks = vec![None; self.chunks.len()]; - self.hints_by_id.clear(); - self.hint_resolves.clear(); - self.latest_invalidation_requests.clear(); - } - - pub fn insert_new_hints( - &mut self, - chunk: RowChunk, - server_id: LanguageServerId, - new_hints: Vec<(InlayId, InlayHint)>, - ) { - let existing_hints = self.hints_by_chunks[chunk.id] - .get_or_insert_default() - .entry(server_id) - .or_insert_with(Vec::new); - let existing_count = existing_hints.len(); - existing_hints.extend(new_hints.into_iter().enumerate().filter_map( - |(i, (id, new_hint))| { - let new_hint_for_id = HintForId { - chunk_id: chunk.id, - server_id, - position: existing_count + i, - }; - if let hash_map::Entry::Vacant(vacant_entry) = self.hints_by_id.entry(id) { - vacant_entry.insert(new_hint_for_id); - Some((id, new_hint)) - } else { - None - } - }, - )); - *self.fetched_hints(&chunk) = None; - } - - pub fn hint_for_id(&mut self, id: InlayId) -> Option<&mut InlayHint> { - let hint_for_id = self.hints_by_id.get(&id)?; - let (hint_id, hint) = self - .hints_by_chunks - .get_mut(hint_for_id.chunk_id)? - .as_mut()? - .get_mut(&hint_for_id.server_id)? - .get_mut(hint_for_id.position)?; - debug_assert_eq!(*hint_id, id, "Invalid pointer {hint_for_id:?}"); - Some(hint) - } - - pub(crate) fn invalidate_for_server_refresh( - &mut self, - for_server: LanguageServerId, - request_id: Option, - ) -> bool { - match self.latest_invalidation_requests.entry(for_server) { - hash_map::Entry::Occupied(mut o) => { - if request_id > *o.get() { - o.insert(request_id); - } else { - return false; - } - } - hash_map::Entry::Vacant(v) => { - v.insert(request_id); - } - } - - for (chunk_id, chunk_data) in self.hints_by_chunks.iter_mut().enumerate() { - if let Some(removed_hints) = chunk_data - .as_mut() - .and_then(|chunk_data| chunk_data.remove(&for_server)) - { - for (id, _) in removed_hints { - self.hints_by_id.remove(&id); - self.hint_resolves.remove(&id); - } - self.fetches_by_chunks[chunk_id] = None; - } - } - - true - } - - pub(crate) fn invalidate_for_chunk(&mut self, chunk: RowChunk) { - self.fetches_by_chunks[chunk.id] = None; - if let Some(hints_by_server) = self.hints_by_chunks[chunk.id].take() { - for (hint_id, _) in hints_by_server.into_values().flatten() { - self.hints_by_id.remove(&hint_id); - self.hint_resolves.remove(&hint_id); - } - } - } - - pub fn chunk_range(&self, chunk: RowChunk) -> Option> { - self.chunks.chunk_range(chunk) - } -} diff --git a/crates/project/src/lsp_store/json_language_server_ext.rs b/crates/project/src/lsp_store/json_language_server_ext.rs deleted file mode 100644 index 78df713273..0000000000 --- a/crates/project/src/lsp_store/json_language_server_ext.rs +++ /dev/null @@ -1,98 +0,0 @@ -use anyhow::{Context, Result}; -use gpui::{App, AsyncApp, Entity, Global, WeakEntity}; -use lsp::LanguageServer; - -use crate::LspStore; - -const LOGGER: zlog::Logger = zlog::scoped!("json-schema"); - -/// https://github.com/Microsoft/vscode/blob/main/extensions/json-language-features/server/README.md#schema-content-request -/// -/// Represents a "JSON language server-specific, non-standardized, extension to the LSP" with which the vscode-json-language-server -/// can request the contents of a schema that is associated with a uri scheme it does not support. -/// In our case, we provide the uris for actions on server startup under the `zed://schemas/action/{normalize_action_name}` scheme. -/// We can then respond to this request with the schema content on demand, thereby greatly reducing the total size of the JSON we send to the server on startup -struct SchemaContentRequest {} - -impl lsp::request::Request for SchemaContentRequest { - type Params = Vec; - - type Result = String; - - const METHOD: &'static str = "vscode/content"; -} - -type SchemaRequestHandler = fn(Entity, String, &mut AsyncApp) -> Result; -pub struct SchemaHandlingImpl(SchemaRequestHandler); - -impl Global for SchemaHandlingImpl {} - -pub fn register_schema_handler(handler: SchemaRequestHandler, cx: &mut App) { - debug_assert!( - !cx.has_global::(), - "SchemaHandlingImpl already registered" - ); - cx.set_global(SchemaHandlingImpl(handler)); -} - -struct SchemaContentsChanged {} - -impl lsp::notification::Notification for SchemaContentsChanged { - const METHOD: &'static str = "json/schemaContent"; - type Params = String; -} - -pub fn notify_schema_changed(lsp_store: Entity, uri: String, cx: &App) { - zlog::trace!(LOGGER => "Notifying schema changed for URI: {:?}", uri); - let servers = lsp_store.read_with(cx, |lsp_store, _| { - let mut servers = Vec::new(); - let Some(local) = lsp_store.as_local() else { - return servers; - }; - - for states in local.language_servers.values() { - let json_server = match states { - super::LanguageServerState::Running { - adapter, server, .. - } if adapter.adapter.is_primary_zed_json_schema_adapter() => server.clone(), - _ => continue, - }; - - servers.push(json_server); - } - servers - }); - for server in servers { - zlog::trace!(LOGGER => "Notifying server {:?} of schema change for URI: {:?}", server.server_id(), &uri); - // TODO: handle errors - server.notify::(uri.clone()).ok(); - } -} - -pub fn register_requests(lsp_store: WeakEntity, language_server: &LanguageServer) { - language_server - .on_request::(move |params, cx| { - let handler = cx.try_read_global::(|handler, _| { - handler.0 - }); - let mut cx = cx.clone(); - let uri = params.clone().pop(); - let lsp_store = lsp_store.clone(); - let resolution = async move { - let lsp_store = lsp_store.upgrade().context("LSP store has been dropped")?; - let uri = uri.context("No URI")?; - let handle_schema_request = handler.context("No schema handler registered")?; - handle_schema_request(lsp_store, uri, &mut cx) - }; - async move { - zlog::trace!(LOGGER => "Handling schema request for {:?}", ¶ms); - let result = resolution.await; - match &result { - Ok(content) => {zlog::trace!(LOGGER => "Schema request resolved with {}B schema", content.len());}, - Err(err) => {zlog::warn!(LOGGER => "Schema request failed: {}", err);}, - } - result - } - }) - .detach(); -} diff --git a/crates/project/src/lsp_store/log_store.rs b/crates/project/src/lsp_store/log_store.rs deleted file mode 100644 index 92f8fecadd..0000000000 --- a/crates/project/src/lsp_store/log_store.rs +++ /dev/null @@ -1,716 +0,0 @@ -use std::{collections::VecDeque, sync::Arc}; - -use collections::HashMap; -use futures::{StreamExt, channel::mpsc}; -use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, Subscription, WeakEntity}; -use lsp::{ - IoKind, LanguageServer, LanguageServerId, LanguageServerName, LanguageServerSelector, - MessageType, TraceValue, -}; -use rpc::proto; -use settings::WorktreeId; - -use crate::{LanguageServerLogType, LspStore, Project, ProjectItem as _}; - -const SEND_LINE: &str = "\n// Send:"; -const RECEIVE_LINE: &str = "\n// Receive:"; -const MAX_STORED_LOG_ENTRIES: usize = 2000; - -pub fn init(on_headless_host: bool, cx: &mut App) -> Entity { - let log_store = cx.new(|cx| LogStore::new(on_headless_host, cx)); - cx.set_global(GlobalLogStore(log_store.clone())); - log_store -} - -pub struct GlobalLogStore(pub Entity); - -impl Global for GlobalLogStore {} - -#[derive(Debug)] -pub enum Event { - NewServerLogEntry { - id: LanguageServerId, - kind: LanguageServerLogType, - text: String, - }, -} - -impl EventEmitter for LogStore {} - -pub struct LogStore { - on_headless_host: bool, - projects: HashMap, ProjectState>, - pub copilot_log_subscription: Option, - pub language_servers: HashMap, - io_tx: mpsc::UnboundedSender<(LanguageServerId, IoKind, String)>, -} - -struct ProjectState { - _subscriptions: [Subscription; 2], -} - -pub trait Message: AsRef { - type Level: Copy + std::fmt::Debug; - fn should_include(&self, _: Self::Level) -> bool { - true - } -} - -#[derive(Debug)] -pub struct LogMessage { - message: String, - typ: MessageType, -} - -impl AsRef for LogMessage { - fn as_ref(&self) -> &str { - &self.message - } -} - -impl Message for LogMessage { - type Level = MessageType; - - fn should_include(&self, level: Self::Level) -> bool { - match (self.typ, level) { - (MessageType::ERROR, _) => true, - (_, MessageType::ERROR) => false, - (MessageType::WARNING, _) => true, - (_, MessageType::WARNING) => false, - (MessageType::INFO, _) => true, - (_, MessageType::INFO) => false, - _ => true, - } - } -} - -#[derive(Debug)] -pub struct TraceMessage { - message: String, - is_verbose: bool, -} - -impl AsRef for TraceMessage { - fn as_ref(&self) -> &str { - &self.message - } -} - -impl Message for TraceMessage { - type Level = TraceValue; - - fn should_include(&self, level: Self::Level) -> bool { - match level { - TraceValue::Off => false, - TraceValue::Messages => !self.is_verbose, - TraceValue::Verbose => true, - } - } -} - -#[derive(Debug)] -pub struct RpcMessage { - message: String, -} - -impl AsRef for RpcMessage { - fn as_ref(&self) -> &str { - &self.message - } -} - -impl Message for RpcMessage { - type Level = (); -} - -pub struct LanguageServerState { - pub name: Option, - pub worktree_id: Option, - pub kind: LanguageServerKind, - log_messages: VecDeque, - trace_messages: VecDeque, - pub rpc_state: Option, - pub trace_level: TraceValue, - pub log_level: MessageType, - io_logs_subscription: Option, - pub toggled_log_kind: Option, -} - -impl std::fmt::Debug for LanguageServerState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("LanguageServerState") - .field("name", &self.name) - .field("worktree_id", &self.worktree_id) - .field("kind", &self.kind) - .field("log_messages", &self.log_messages) - .field("trace_messages", &self.trace_messages) - .field("rpc_state", &self.rpc_state) - .field("trace_level", &self.trace_level) - .field("log_level", &self.log_level) - .field("toggled_log_kind", &self.toggled_log_kind) - .finish_non_exhaustive() - } -} - -#[derive(PartialEq, Clone)] -pub enum LanguageServerKind { - Local { project: WeakEntity }, - Remote { project: WeakEntity }, - LocalSsh { lsp_store: WeakEntity }, - Global, -} - -impl std::fmt::Debug for LanguageServerKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - LanguageServerKind::Local { .. } => write!(f, "LanguageServerKind::Local"), - LanguageServerKind::Remote { .. } => write!(f, "LanguageServerKind::Remote"), - LanguageServerKind::LocalSsh { .. } => write!(f, "LanguageServerKind::LocalSsh"), - LanguageServerKind::Global => write!(f, "LanguageServerKind::Global"), - } - } -} - -impl LanguageServerKind { - pub fn project(&self) -> Option<&WeakEntity> { - match self { - Self::Local { project } => Some(project), - Self::Remote { project } => Some(project), - Self::LocalSsh { .. } => None, - Self::Global { .. } => None, - } - } -} - -#[derive(Debug)] -pub struct LanguageServerRpcState { - pub rpc_messages: VecDeque, - last_message_kind: Option, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum MessageKind { - Send, - Receive, -} - -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub enum LogKind { - Rpc, - Trace, - #[default] - Logs, - ServerInfo, -} - -impl LogKind { - pub fn from_server_log_type(log_type: &LanguageServerLogType) -> Self { - match log_type { - LanguageServerLogType::Log(_) => Self::Logs, - LanguageServerLogType::Trace { .. } => Self::Trace, - LanguageServerLogType::Rpc { .. } => Self::Rpc, - } - } -} - -impl LogStore { - pub fn new(on_headless_host: bool, cx: &mut Context) -> Self { - let (io_tx, mut io_rx) = mpsc::unbounded(); - - let log_store = Self { - projects: HashMap::default(), - language_servers: HashMap::default(), - copilot_log_subscription: None, - on_headless_host, - io_tx, - }; - cx.spawn(async move |log_store, cx| { - while let Some((server_id, io_kind, message)) = io_rx.next().await { - if let Some(log_store) = log_store.upgrade() { - log_store.update(cx, |log_store, cx| { - log_store.on_io(server_id, io_kind, &message, cx); - })?; - } - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - - log_store - } - - pub fn add_project(&mut self, project: &Entity, cx: &mut Context) { - let weak_project = project.downgrade(); - self.projects.insert( - project.downgrade(), - ProjectState { - _subscriptions: [ - cx.observe_release(project, move |this, _, _| { - this.projects.remove(&weak_project); - this.language_servers - .retain(|_, state| state.kind.project() != Some(&weak_project)); - }), - cx.subscribe(project, move |log_store, project, event, cx| { - let server_kind = if project.read(cx).is_local() { - LanguageServerKind::Local { - project: project.downgrade(), - } - } else { - LanguageServerKind::Remote { - project: project.downgrade(), - } - }; - match event { - crate::Event::LanguageServerAdded(id, name, worktree_id) => { - log_store.add_language_server( - server_kind, - *id, - Some(name.clone()), - *worktree_id, - project - .read(cx) - .lsp_store() - .read(cx) - .language_server_for_id(*id), - cx, - ); - } - crate::Event::LanguageServerBufferRegistered { - server_id, - buffer_id, - name, - .. - } => { - let worktree_id = project - .read(cx) - .buffer_for_id(*buffer_id, cx) - .and_then(|buffer| { - Some(buffer.read(cx).project_path(cx)?.worktree_id) - }); - let name = name.clone().or_else(|| { - project - .read(cx) - .lsp_store() - .read(cx) - .language_server_statuses - .get(server_id) - .map(|status| status.name.clone()) - }); - log_store.add_language_server( - server_kind, - *server_id, - name, - worktree_id, - None, - cx, - ); - } - crate::Event::LanguageServerRemoved(id) => { - log_store.remove_language_server(*id, cx); - } - crate::Event::LanguageServerLog(id, typ, message) => { - log_store.add_language_server( - server_kind, - *id, - None, - None, - None, - cx, - ); - match typ { - crate::LanguageServerLogType::Log(typ) => { - log_store.add_language_server_log(*id, *typ, message, cx); - } - crate::LanguageServerLogType::Trace { verbose_info } => { - log_store.add_language_server_trace( - *id, - message, - verbose_info.clone(), - cx, - ); - } - crate::LanguageServerLogType::Rpc { received } => { - let kind = if *received { - MessageKind::Receive - } else { - MessageKind::Send - }; - log_store.add_language_server_rpc(*id, kind, message, cx); - } - } - } - crate::Event::ToggleLspLogs { - server_id, - enabled, - toggled_log_kind, - } => { - log_store.toggle_lsp_logs(*server_id, *enabled, *toggled_log_kind); - } - _ => {} - } - }), - ], - }, - ); - } - - pub fn get_language_server_state( - &mut self, - id: LanguageServerId, - ) -> Option<&mut LanguageServerState> { - self.language_servers.get_mut(&id) - } - - pub fn add_language_server( - &mut self, - kind: LanguageServerKind, - server_id: LanguageServerId, - name: Option, - worktree_id: Option, - server: Option>, - cx: &mut Context, - ) -> Option<&mut LanguageServerState> { - let server_state = self.language_servers.entry(server_id).or_insert_with(|| { - cx.notify(); - LanguageServerState { - name: None, - worktree_id: None, - kind, - rpc_state: None, - log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), - trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), - trace_level: TraceValue::Off, - log_level: MessageType::LOG, - io_logs_subscription: None, - toggled_log_kind: None, - } - }); - - if let Some(name) = name { - server_state.name = Some(name); - } - if let Some(worktree_id) = worktree_id { - server_state.worktree_id = Some(worktree_id); - } - - if let Some(server) = server.filter(|_| server_state.io_logs_subscription.is_none()) { - let io_tx = self.io_tx.clone(); - let server_id = server.server_id(); - server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| { - io_tx - .unbounded_send((server_id, io_kind, message.to_string())) - .ok(); - })); - } - - Some(server_state) - } - - pub fn add_language_server_log( - &mut self, - id: LanguageServerId, - typ: MessageType, - message: &str, - cx: &mut Context, - ) -> Option<()> { - let store_logs = !self.on_headless_host; - let language_server_state = self.get_language_server_state(id)?; - - let log_lines = &mut language_server_state.log_messages; - let message = message.trim_end().to_string(); - if !store_logs { - // Send all messages regardless of the visibility in case of not storing, to notify the receiver anyway - self.emit_event( - Event::NewServerLogEntry { - id, - kind: LanguageServerLogType::Log(typ), - text: message, - }, - cx, - ); - } else if let Some(new_message) = Self::push_new_message( - log_lines, - LogMessage { message, typ }, - language_server_state.log_level, - ) { - self.emit_event( - Event::NewServerLogEntry { - id, - kind: LanguageServerLogType::Log(typ), - text: new_message, - }, - cx, - ); - } - Some(()) - } - - fn add_language_server_trace( - &mut self, - id: LanguageServerId, - message: &str, - verbose_info: Option, - cx: &mut Context, - ) -> Option<()> { - let store_logs = !self.on_headless_host; - let language_server_state = self.get_language_server_state(id)?; - - let log_lines = &mut language_server_state.trace_messages; - if !store_logs { - // Send all messages regardless of the visibility in case of not storing, to notify the receiver anyway - self.emit_event( - Event::NewServerLogEntry { - id, - kind: LanguageServerLogType::Trace { verbose_info }, - text: message.trim().to_string(), - }, - cx, - ); - } else if let Some(new_message) = Self::push_new_message( - log_lines, - TraceMessage { - message: message.trim().to_string(), - is_verbose: false, - }, - TraceValue::Messages, - ) { - if let Some(verbose_message) = verbose_info.as_ref() { - Self::push_new_message( - log_lines, - TraceMessage { - message: verbose_message.clone(), - is_verbose: true, - }, - TraceValue::Verbose, - ); - } - self.emit_event( - Event::NewServerLogEntry { - id, - kind: LanguageServerLogType::Trace { verbose_info }, - text: new_message, - }, - cx, - ); - } - Some(()) - } - - fn push_new_message( - log_lines: &mut VecDeque, - message: T, - current_severity: ::Level, - ) -> Option { - while log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { - log_lines.pop_front(); - } - let visible = message.should_include(current_severity); - - let visible_message = visible.then(|| message.as_ref().to_string()); - log_lines.push_back(message); - visible_message - } - - fn add_language_server_rpc( - &mut self, - language_server_id: LanguageServerId, - kind: MessageKind, - message: &str, - cx: &mut Context<'_, Self>, - ) { - let store_logs = !self.on_headless_host; - let Some(state) = self - .get_language_server_state(language_server_id) - .and_then(|state| state.rpc_state.as_mut()) - else { - return; - }; - - let received = kind == MessageKind::Receive; - let rpc_log_lines = &mut state.rpc_messages; - if state.last_message_kind != Some(kind) { - while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { - rpc_log_lines.pop_front(); - } - let line_before_message = match kind { - MessageKind::Send => SEND_LINE, - MessageKind::Receive => RECEIVE_LINE, - }; - if store_logs { - rpc_log_lines.push_back(RpcMessage { - message: line_before_message.to_string(), - }); - } - // Do not send a synthetic message over the wire, it will be derived from the actual RPC message - cx.emit(Event::NewServerLogEntry { - id: language_server_id, - kind: LanguageServerLogType::Rpc { received }, - text: line_before_message.to_string(), - }); - } - - while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { - rpc_log_lines.pop_front(); - } - - if store_logs { - rpc_log_lines.push_back(RpcMessage { - message: message.trim().to_owned(), - }); - } - - self.emit_event( - Event::NewServerLogEntry { - id: language_server_id, - kind: LanguageServerLogType::Rpc { received }, - text: message.to_owned(), - }, - cx, - ); - } - - pub fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context) { - self.language_servers.remove(&id); - cx.notify(); - } - - pub fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque> { - Some(&self.language_servers.get(&server_id)?.log_messages) - } - - pub fn server_trace(&self, server_id: LanguageServerId) -> Option<&VecDeque> { - Some(&self.language_servers.get(&server_id)?.trace_messages) - } - - pub fn server_ids_for_project<'a>( - &'a self, - lookup_project: &'a WeakEntity, - ) -> impl Iterator + 'a { - self.language_servers - .iter() - .filter_map(move |(id, state)| match &state.kind { - LanguageServerKind::Local { project } | LanguageServerKind::Remote { project } => { - if project == lookup_project { - Some(*id) - } else { - None - } - } - LanguageServerKind::Global | LanguageServerKind::LocalSsh { .. } => Some(*id), - }) - } - - pub fn enable_rpc_trace_for_language_server( - &mut self, - server_id: LanguageServerId, - ) -> Option<&mut LanguageServerRpcState> { - let rpc_state = self - .language_servers - .get_mut(&server_id)? - .rpc_state - .get_or_insert_with(|| LanguageServerRpcState { - rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), - last_message_kind: None, - }); - Some(rpc_state) - } - - pub fn disable_rpc_trace_for_language_server( - &mut self, - server_id: LanguageServerId, - ) -> Option<()> { - self.language_servers.get_mut(&server_id)?.rpc_state.take(); - Some(()) - } - - pub fn has_server_logs(&self, server: &LanguageServerSelector) -> bool { - match server { - LanguageServerSelector::Id(id) => self.language_servers.contains_key(id), - LanguageServerSelector::Name(name) => self - .language_servers - .iter() - .any(|(_, state)| state.name.as_ref() == Some(name)), - } - } - - fn on_io( - &mut self, - language_server_id: LanguageServerId, - io_kind: IoKind, - message: &str, - cx: &mut Context, - ) -> Option<()> { - let is_received = match io_kind { - IoKind::StdOut => true, - IoKind::StdIn => false, - IoKind::StdErr => { - self.add_language_server_log(language_server_id, MessageType::LOG, message, cx); - return Some(()); - } - }; - - let kind = if is_received { - MessageKind::Receive - } else { - MessageKind::Send - }; - - self.add_language_server_rpc(language_server_id, kind, message, cx); - cx.notify(); - Some(()) - } - - fn emit_event(&mut self, e: Event, cx: &mut Context) { - match &e { - Event::NewServerLogEntry { id, kind, text } => { - if let Some(state) = self.get_language_server_state(*id) { - let downstream_client = match &state.kind { - LanguageServerKind::Remote { project } - | LanguageServerKind::Local { project } => project - .upgrade() - .map(|project| project.read(cx).lsp_store()), - LanguageServerKind::LocalSsh { lsp_store } => lsp_store.upgrade(), - LanguageServerKind::Global => None, - } - .and_then(|lsp_store| lsp_store.read(cx).downstream_client()); - if let Some((client, project_id)) = downstream_client { - if Some(LogKind::from_server_log_type(kind)) == state.toggled_log_kind { - client - .send(proto::LanguageServerLog { - project_id, - language_server_id: id.to_proto(), - message: text.clone(), - log_type: Some(kind.to_proto()), - }) - .ok(); - } - } - } - } - } - - cx.emit(e); - } - - pub fn toggle_lsp_logs( - &mut self, - server_id: LanguageServerId, - enabled: bool, - toggled_log_kind: LogKind, - ) { - if let Some(server_state) = self.get_language_server_state(server_id) { - if enabled { - server_state.toggled_log_kind = Some(toggled_log_kind); - } else { - server_state.toggled_log_kind = None; - } - } - if LogKind::Rpc == toggled_log_kind { - if enabled { - self.enable_rpc_trace_for_language_server(server_id); - } else { - self.disable_rpc_trace_for_language_server(server_id); - } - } - } -} diff --git a/crates/project/src/lsp_store/lsp_ext_command.rs b/crates/project/src/lsp_store/lsp_ext_command.rs deleted file mode 100644 index 5066143244..0000000000 --- a/crates/project/src/lsp_store/lsp_ext_command.rs +++ /dev/null @@ -1,806 +0,0 @@ -use crate::{ - LocationLink, - lsp_command::{ - LspCommand, file_path_to_lsp_url, location_link_from_lsp, location_link_from_proto, - location_link_to_proto, location_links_from_lsp, location_links_from_proto, - location_links_to_proto, - }, - lsp_store::LspStore, - make_lsp_text_document_position, make_text_document_identifier, -}; -use anyhow::{Context as _, Result}; -use async_trait::async_trait; -use collections::HashMap; -use gpui::{App, AsyncApp, Entity}; -use language::{ - Buffer, point_to_lsp, - proto::{deserialize_anchor, serialize_anchor}, -}; -use lsp::{AdapterServerCapabilities, LanguageServer, LanguageServerId}; -use rpc::proto::{self, PeerId}; -use serde::{Deserialize, Serialize}; -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; -use task::TaskTemplate; -use text::{BufferId, PointUtf16, ToPointUtf16}; - -pub enum LspExtExpandMacro {} - -impl lsp::request::Request for LspExtExpandMacro { - type Params = ExpandMacroParams; - type Result = Option; - const METHOD: &'static str = "rust-analyzer/expandMacro"; -} - -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ExpandMacroParams { - pub text_document: lsp::TextDocumentIdentifier, - pub position: lsp::Position, -} - -#[derive(Default, Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ExpandedMacro { - pub name: String, - pub expansion: String, -} - -impl ExpandedMacro { - pub fn is_empty(&self) -> bool { - self.name.is_empty() && self.expansion.is_empty() - } -} -#[derive(Debug)] -pub struct ExpandMacro { - pub position: PointUtf16, -} - -#[async_trait(?Send)] -impl LspCommand for ExpandMacro { - type Response = ExpandedMacro; - type LspRequest = LspExtExpandMacro; - type ProtoRequest = proto::LspExtExpandMacro; - - fn display_name(&self) -> &str { - "Expand macro" - } - - fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool { - true - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(ExpandMacroParams { - text_document: make_text_document_identifier(path)?, - position: point_to_lsp(self.position), - }) - } - - async fn response_from_lsp( - self, - message: Option, - _: Entity, - _: Entity, - _: LanguageServerId, - _: AsyncApp, - ) -> anyhow::Result { - Ok(message - .map(|message| ExpandedMacro { - name: message.name, - expansion: message.expansion, - }) - .unwrap_or_default()) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtExpandMacro { - proto::LspExtExpandMacro { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - } - } - - async fn from_proto( - message: Self::ProtoRequest, - _: Entity, - buffer: Entity, - 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))?, - }) - } - - fn response_to_proto( - response: ExpandedMacro, - _: &mut LspStore, - _: PeerId, - _: &clock::Global, - _: &mut App, - ) -> proto::LspExtExpandMacroResponse { - proto::LspExtExpandMacroResponse { - name: response.name, - expansion: response.expansion, - } - } - - async fn response_from_proto( - self, - message: proto::LspExtExpandMacroResponse, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> anyhow::Result { - Ok(ExpandedMacro { - name: message.name, - expansion: message.expansion, - }) - } - - fn buffer_id_from_proto(message: &proto::LspExtExpandMacro) -> Result { - BufferId::new(message.buffer_id) - } -} - -pub enum LspOpenDocs {} - -impl lsp::request::Request for LspOpenDocs { - type Params = OpenDocsParams; - type Result = Option; - const METHOD: &'static str = "experimental/externalDocs"; -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct OpenDocsParams { - pub text_document: lsp::TextDocumentIdentifier, - pub position: lsp::Position, -} - -#[derive(Serialize, Deserialize, Debug, Default)] -#[serde(rename_all = "camelCase")] -pub struct DocsUrls { - pub web: Option, - pub local: Option, -} - -impl DocsUrls { - pub fn is_empty(&self) -> bool { - self.web.is_none() && self.local.is_none() - } -} - -#[derive(Debug)] -pub struct OpenDocs { - pub position: PointUtf16, -} - -#[async_trait(?Send)] -impl LspCommand for OpenDocs { - type Response = DocsUrls; - type LspRequest = LspOpenDocs; - type ProtoRequest = proto::LspExtOpenDocs; - - fn display_name(&self) -> &str { - "Open docs" - } - - fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool { - true - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(OpenDocsParams { - text_document: lsp::TextDocumentIdentifier { - uri: lsp::Uri::from_file_path(path).unwrap(), - }, - position: point_to_lsp(self.position), - }) - } - - async fn response_from_lsp( - self, - message: Option, - _: Entity, - _: Entity, - _: LanguageServerId, - _: AsyncApp, - ) -> anyhow::Result { - Ok(message - .map(|message| DocsUrls { - web: message.web, - local: message.local, - }) - .unwrap_or_default()) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtOpenDocs { - proto::LspExtOpenDocs { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - } - } - - async fn from_proto( - message: Self::ProtoRequest, - _: Entity, - buffer: Entity, - 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))?, - }) - } - - fn response_to_proto( - response: DocsUrls, - _: &mut LspStore, - _: PeerId, - _: &clock::Global, - _: &mut App, - ) -> proto::LspExtOpenDocsResponse { - proto::LspExtOpenDocsResponse { - web: response.web, - local: response.local, - } - } - - async fn response_from_proto( - self, - message: proto::LspExtOpenDocsResponse, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> anyhow::Result { - Ok(DocsUrls { - web: message.web, - local: message.local, - }) - } - - fn buffer_id_from_proto(message: &proto::LspExtOpenDocs) -> Result { - BufferId::new(message.buffer_id) - } -} - -pub enum LspSwitchSourceHeader {} - -impl lsp::request::Request for LspSwitchSourceHeader { - type Params = SwitchSourceHeaderParams; - type Result = Option; - const METHOD: &'static str = "textDocument/switchSourceHeader"; -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct SwitchSourceHeaderParams(lsp::TextDocumentIdentifier); - -#[derive(Serialize, Deserialize, Debug, Default)] -#[serde(rename_all = "camelCase")] -pub struct SwitchSourceHeaderResult(pub String); - -#[derive(Default, Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct SwitchSourceHeader; - -#[derive(Debug)] -pub struct GoToParentModule { - pub position: PointUtf16, -} - -pub struct LspGoToParentModule {} - -impl lsp::request::Request for LspGoToParentModule { - type Params = lsp::TextDocumentPositionParams; - type Result = Option>; - const METHOD: &'static str = "experimental/parentModule"; -} - -#[async_trait(?Send)] -impl LspCommand for SwitchSourceHeader { - type Response = SwitchSourceHeaderResult; - type LspRequest = LspSwitchSourceHeader; - type ProtoRequest = proto::LspExtSwitchSourceHeader; - - fn display_name(&self) -> &str { - "Switch source header" - } - - fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool { - true - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(SwitchSourceHeaderParams(make_text_document_identifier( - path, - )?)) - } - - async fn response_from_lsp( - self, - message: Option, - _: Entity, - _: Entity, - _: LanguageServerId, - _: AsyncApp, - ) -> anyhow::Result { - Ok(message - .map(|message| SwitchSourceHeaderResult(message.0)) - .unwrap_or_default()) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtSwitchSourceHeader { - proto::LspExtSwitchSourceHeader { - project_id, - buffer_id: buffer.remote_id().into(), - } - } - - async fn from_proto( - _: Self::ProtoRequest, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> anyhow::Result { - Ok(Self {}) - } - - fn response_to_proto( - response: SwitchSourceHeaderResult, - _: &mut LspStore, - _: PeerId, - _: &clock::Global, - _: &mut App, - ) -> proto::LspExtSwitchSourceHeaderResponse { - proto::LspExtSwitchSourceHeaderResponse { - target_file: response.0, - } - } - - async fn response_from_proto( - self, - message: proto::LspExtSwitchSourceHeaderResponse, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> anyhow::Result { - Ok(SwitchSourceHeaderResult(message.target_file)) - } - - fn buffer_id_from_proto(message: &proto::LspExtSwitchSourceHeader) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GoToParentModule { - type Response = Vec; - type LspRequest = LspGoToParentModule; - type ProtoRequest = proto::LspExtGoToParentModule; - - fn display_name(&self) -> &str { - "Go to parent module" - } - - fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool { - true - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - make_lsp_text_document_position(path, self.position) - } - - async fn response_from_lsp( - self, - links: Option>, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - cx: AsyncApp, - ) -> anyhow::Result> { - location_links_from_lsp( - links.map(lsp::GotoDefinitionResponse::Link), - lsp_store, - buffer, - server_id, - cx, - ) - .await - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtGoToParentModule { - proto::LspExtGoToParentModule { - project_id, - buffer_id: buffer.remote_id().to_proto(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - } - } - - async fn from_proto( - request: Self::ProtoRequest, - _: Entity, - buffer: Entity, - 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))?, - }) - } - - fn response_to_proto( - links: Vec, - lsp_store: &mut LspStore, - peer_id: PeerId, - _: &clock::Global, - cx: &mut App, - ) -> proto::LspExtGoToParentModuleResponse { - proto::LspExtGoToParentModuleResponse { - links: location_links_to_proto(links, lsp_store, peer_id, cx), - } - } - - async fn response_from_proto( - self, - message: proto::LspExtGoToParentModuleResponse, - lsp_store: Entity, - _: Entity, - cx: AsyncApp, - ) -> anyhow::Result> { - location_links_from_proto(message.links, lsp_store, cx).await - } - - fn buffer_id_from_proto(message: &proto::LspExtGoToParentModule) -> Result { - BufferId::new(message.buffer_id) - } -} - -// https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#runnables -// Taken from https://github.com/rust-lang/rust-analyzer/blob/a73a37a757a58b43a796d3eb86a1f7dfd0036659/crates/rust-analyzer/src/lsp/ext.rs#L425-L489 -pub enum Runnables {} - -impl lsp::request::Request for Runnables { - type Params = RunnablesParams; - type Result = Vec; - const METHOD: &'static str = "experimental/runnables"; -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct RunnablesParams { - pub text_document: lsp::TextDocumentIdentifier, - #[serde(default)] - pub position: Option, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Runnable { - pub label: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub location: Option, - pub kind: RunnableKind, - pub args: RunnableArgs, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -#[serde(untagged)] -pub enum RunnableArgs { - Cargo(CargoRunnableArgs), - Shell(ShellRunnableArgs), -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "lowercase")] -pub enum RunnableKind { - Cargo, - Shell, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct CargoRunnableArgs { - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub environment: HashMap, - pub cwd: PathBuf, - /// Command to be executed instead of cargo - #[serde(default)] - pub override_cargo: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub workspace_root: Option, - // command, --package and --lib stuff - #[serde(default)] - pub cargo_args: Vec, - // stuff after -- - #[serde(default)] - pub executable_args: Vec, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct ShellRunnableArgs { - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub environment: HashMap, - pub cwd: PathBuf, - pub program: String, - #[serde(default)] - pub args: Vec, -} - -#[derive(Debug)] -pub struct GetLspRunnables { - pub buffer_id: BufferId, - pub position: Option, -} - -#[derive(Debug, Default)] -pub struct LspRunnables { - pub runnables: Vec<(Option, TaskTemplate)>, -} - -#[async_trait(?Send)] -impl LspCommand for GetLspRunnables { - type Response = LspRunnables; - type LspRequest = Runnables; - type ProtoRequest = proto::LspExtRunnables; - - fn display_name(&self) -> &str { - "LSP Runnables" - } - - fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool { - true - } - - fn to_lsp( - &self, - path: &Path, - buffer: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - let url = file_path_to_lsp_url(path)?; - Ok(RunnablesParams { - text_document: lsp::TextDocumentIdentifier::new(url), - position: self - .position - .map(|anchor| point_to_lsp(anchor.to_point_utf16(&buffer.snapshot()))), - }) - } - - async fn response_from_lsp( - self, - lsp_runnables: Vec, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - mut cx: AsyncApp, - ) -> Result { - let mut runnables = Vec::with_capacity(lsp_runnables.len()); - - for runnable in lsp_runnables { - let location = match runnable.location { - Some(location) => Some( - location_link_from_lsp(location, &lsp_store, &buffer, server_id, &mut cx) - .await?, - ), - None => None, - }; - let mut task_template = TaskTemplate::default(); - task_template.label = runnable.label; - match runnable.args { - RunnableArgs::Cargo(cargo) => { - match cargo.override_cargo { - Some(override_cargo) => { - let mut override_parts = - override_cargo.split(" ").map(|s| s.to_string()); - task_template.command = override_parts - .next() - .unwrap_or_else(|| override_cargo.clone()); - task_template.args.extend(override_parts); - } - None => task_template.command = "cargo".to_string(), - }; - task_template.env = cargo.environment; - task_template.cwd = Some( - cargo - .workspace_root - .unwrap_or(cargo.cwd) - .to_string_lossy() - .to_string(), - ); - task_template.args.extend(cargo.cargo_args); - if !cargo.executable_args.is_empty() { - let shell_kind = task_template.shell.shell_kind(cfg!(windows)); - task_template.args.push("--".to_string()); - task_template.args.extend( - cargo - .executable_args - .into_iter() - // rust-analyzer's doctest data may be smth. like - // ``` - // command: "cargo", - // args: [ - // "test", - // "--doc", - // "--package", - // "cargo-output-parser", - // "--", - // "X::new", - // "--show-output", - // ], - // ``` - // and `X::new` will cause troubles if not escaped properly, as later - // the task runs as `$SHELL -i -c "cargo test ..."`. - // - // We cannot escape all shell arguments unconditionally, as we use this for ssh commands, which may involve paths starting with `~`. - // That bit is not auto-expanded when using single quotes. - // Escape extra cargo args unconditionally as those are unlikely to contain `~`. - .flat_map(|extra_arg| { - shell_kind.try_quote(&extra_arg).map(|s| s.to_string()) - }), - ); - } - } - RunnableArgs::Shell(shell) => { - task_template.command = shell.program; - task_template.args = shell.args; - task_template.env = shell.environment; - task_template.cwd = Some(shell.cwd.to_string_lossy().into_owned()); - } - } - - runnables.push((location, task_template)); - } - - Ok(LspRunnables { runnables }) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtRunnables { - proto::LspExtRunnables { - project_id, - buffer_id: buffer.remote_id().to_proto(), - position: self.position.as_ref().map(serialize_anchor), - } - } - - async fn from_proto( - message: proto::LspExtRunnables, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> Result { - let buffer_id = Self::buffer_id_from_proto(&message)?; - let position = message.position.and_then(deserialize_anchor); - Ok(Self { - buffer_id, - position, - }) - } - - fn response_to_proto( - response: LspRunnables, - lsp_store: &mut LspStore, - peer_id: PeerId, - _: &clock::Global, - cx: &mut App, - ) -> proto::LspExtRunnablesResponse { - proto::LspExtRunnablesResponse { - runnables: response - .runnables - .into_iter() - .map(|(location, task_template)| proto::LspRunnable { - location: location - .map(|location| location_link_to_proto(location, lsp_store, peer_id, cx)), - task_template: serde_json::to_vec(&task_template).unwrap(), - }) - .collect(), - } - } - - async fn response_from_proto( - self, - message: proto::LspExtRunnablesResponse, - lsp_store: Entity, - _: Entity, - mut cx: AsyncApp, - ) -> Result { - let mut runnables = LspRunnables { - runnables: Vec::new(), - }; - - for lsp_runnable in message.runnables { - let location = match lsp_runnable.location { - Some(location) => { - Some(location_link_from_proto(location, lsp_store.clone(), &mut cx).await?) - } - None => None, - }; - let task_template = serde_json::from_slice(&lsp_runnable.task_template) - .context("deserializing task template from proto")?; - runnables.runnables.push((location, task_template)); - } - - Ok(runnables) - } - - fn buffer_id_from_proto(message: &proto::LspExtRunnables) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[derive(Debug)] -pub struct LspExtCancelFlycheck {} - -#[derive(Debug)] -pub struct LspExtRunFlycheck {} - -#[derive(Debug)] -pub struct LspExtClearFlycheck {} - -impl lsp::notification::Notification for LspExtCancelFlycheck { - type Params = (); - const METHOD: &'static str = "rust-analyzer/cancelFlycheck"; -} - -impl lsp::notification::Notification for LspExtRunFlycheck { - type Params = RunFlycheckParams; - const METHOD: &'static str = "rust-analyzer/runFlycheck"; -} - -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct RunFlycheckParams { - pub text_document: Option, -} - -impl lsp::notification::Notification for LspExtClearFlycheck { - type Params = (); - const METHOD: &'static str = "rust-analyzer/clearFlycheck"; -} diff --git a/crates/project/src/lsp_store/rust_analyzer_ext.rs b/crates/project/src/lsp_store/rust_analyzer_ext.rs deleted file mode 100644 index 4d5f134e5f..0000000000 --- a/crates/project/src/lsp_store/rust_analyzer_ext.rs +++ /dev/null @@ -1,275 +0,0 @@ -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 rpc::proto; - -use crate::{LspStore, LspStoreEvent, Project, ProjectPath, lsp_store}; - -pub const RUST_ANALYZER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer"); -pub const CARGO_DIAGNOSTICS_SOURCE_NAME: &str = "rustc"; - -/// Experimental: Informs the end user about the state of the server -/// -/// [Rust Analyzer Specification](https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#server-status) -#[derive(Debug)] -enum ServerStatus {} - -#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -struct ServerStatusParams { - pub health: ServerHealth, - pub message: Option, -} - -impl lsp::notification::Notification for ServerStatus { - type Params = ServerStatusParams; - const METHOD: &'static str = "experimental/serverStatus"; -} - -pub fn register_notifications(lsp_store: WeakEntity, language_server: &LanguageServer) { - let name = language_server.name(); - let server_id = language_server.server_id(); - - language_server - .on_notification::({ - move |params, cx| { - let message = params.message; - let log_message = message.as_ref().map(|message| { - format!("Language server {name} (id {server_id}) status update: {message}") - }); - let status = match ¶ms.health { - ServerHealth::Ok => { - if let Some(log_message) = log_message { - log::info!("{log_message}"); - } - proto::ServerHealth::Ok - } - ServerHealth::Warning => { - if let Some(log_message) = log_message { - log::warn!("{log_message}"); - } - proto::ServerHealth::Warning - } - ServerHealth::Error => { - if let Some(log_message) = log_message { - log::error!("{log_message}"); - } - proto::ServerHealth::Error - } - }; - - lsp_store - .update(cx, |_, cx| { - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id: server_id, - name: Some(name.clone()), - message: proto::update_language_server::Variant::StatusUpdate( - proto::StatusUpdate { - message, - status: Some(proto::status_update::Status::Health( - status as i32, - )), - }, - ), - }); - }) - .ok(); - } - }) - .detach(); -} - -pub fn cancel_flycheck( - project: Entity, - buffer_path: Option, - 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) - }) - }) - }); - - 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) - else { - return Ok(()); - }; - - if let Some((client, project_id)) = upstream_client { - let request = proto::LspExtCancelFlycheck { - project_id, - language_server_id: rust_analyzer_server.to_proto(), - }; - client - .request(request) - .await - .context("lsp ext cancel flycheck proto request")?; - } else { - lsp_store - .read_with(cx, |lsp_store, _| { - if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) { - server.notify::(()) - } else { - Ok(()) - } - }) - .context("lsp ext cancel flycheck")??; - }; - anyhow::Ok(()) - }) -} - -pub fn run_flycheck( - project: Entity, - buffer_path: Option, - 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) - }) - }) - }); - - 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) - else { - return Ok(()); - }; - - 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, - language_server_id: rust_analyzer_server.to_proto(), - current_file_only: false, - }; - client - .request(request) - .await - .context("lsp ext run flycheck proto request")?; - } else { - lsp_store - .read_with(cx, |lsp_store, _| { - if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) { - server.notify::( - lsp_store::lsp_ext_command::RunFlycheckParams { - text_document: None, - }, - ) - } else { - Ok(()) - } - }) - .context("lsp ext run flycheck")??; - }; - anyhow::Ok(()) - }) -} - -pub fn clear_flycheck( - project: Entity, - buffer_path: Option, - 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) - }) - }) - }); - - 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) - else { - return Ok(()); - }; - - if let Some((client, project_id)) = upstream_client { - let request = proto::LspExtClearFlycheck { - project_id, - language_server_id: rust_analyzer_server.to_proto(), - }; - client - .request(request) - .await - .context("lsp ext clear flycheck proto request")?; - } else { - lsp_store - .read_with(cx, |lsp_store, _| { - if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) { - server.notify::(()) - } else { - Ok(()) - } - }) - .context("lsp ext 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/lsp_store/vue_language_server_ext.rs b/crates/project/src/lsp_store/vue_language_server_ext.rs deleted file mode 100644 index 2824974540..0000000000 --- a/crates/project/src/lsp_store/vue_language_server_ext.rs +++ /dev/null @@ -1,124 +0,0 @@ -use anyhow::Context as _; -use gpui::{AppContext, WeakEntity}; -use lsp::{LanguageServer, LanguageServerName}; -use serde_json::Value; - -use crate::LspStore; - -struct VueServerRequest; -struct TypescriptServerResponse; - -impl lsp::notification::Notification for VueServerRequest { - type Params = Vec<(u64, String, serde_json::Value)>; - - const METHOD: &'static str = "tsserver/request"; -} - -impl lsp::notification::Notification for TypescriptServerResponse { - type Params = Vec<(u64, serde_json::Value)>; - - const METHOD: &'static str = "tsserver/response"; -} - -const VUE_SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vue-language-server"); -const VTSLS: LanguageServerName = LanguageServerName::new_static("vtsls"); -const TS_LS: LanguageServerName = LanguageServerName::new_static("typescript-language-server"); - -pub fn register_requests(lsp_store: WeakEntity, language_server: &LanguageServer) { - let language_server_name = language_server.name(); - if language_server_name == VUE_SERVER_NAME { - let vue_server_id = language_server.server_id(); - language_server - .on_notification::({ - move |params, cx| { - let lsp_store = lsp_store.clone(); - let Ok(Some(vue_server)) = lsp_store.read_with(cx, |this, _| { - this.language_server_for_id(vue_server_id) - }) else { - return; - }; - - let requests = params; - let target_server = match lsp_store.read_with(cx, |this, _| { - let language_server_id = this - .as_local() - .and_then(|local| { - local.language_server_ids.iter().find_map(|(seed, v)| { - [VTSLS, TS_LS].contains(&seed.name).then_some(v.id) - }) - }) - .context("Could not find language server")?; - - this.language_server_for_id(language_server_id) - .context("language server not found") - }) { - Ok(Ok(server)) => server, - other => { - log::warn!( - "vue-language-server forwarding skipped: {other:?}. \ - Returning null tsserver responses" - ); - if !requests.is_empty() { - let null_responses = requests - .into_iter() - .map(|(id, _, _)| (id, Value::Null)) - .collect::>(); - let _ = vue_server - .notify::(null_responses); - } - return; - } - }; - - let cx = cx.clone(); - for (request_id, command, payload) in requests.into_iter() { - let target_server = target_server.clone(); - let vue_server = vue_server.clone(); - cx.background_spawn(async move { - let response = target_server - .request::( - lsp::ExecuteCommandParams { - command: "typescript.tsserverRequest".to_owned(), - arguments: vec![Value::String(command), payload], - ..Default::default() - }, - ) - .await; - - let response_body = match response { - util::ConnectionResult::Result(Ok(result)) => match result { - Some(Value::Object(mut map)) => map - .remove("body") - .unwrap_or(Value::Object(map)), - Some(other) => other, - None => Value::Null, - }, - util::ConnectionResult::Result(Err(error)) => { - log::warn!( - "typescript.tsserverRequest failed: {error:?} for request {request_id}" - ); - Value::Null - } - other => { - log::warn!( - "typescript.tsserverRequest did not return a response: {other:?} for request {request_id}" - ); - Value::Null - } - }; - - if let Err(err) = vue_server - .notify::(vec![(request_id, response_body)]) - { - log::warn!( - "Failed to notify vue-language-server of tsserver response: {err:?}" - ); - } - }) - .detach(); - } - } - }) - .detach(); - } -} diff --git a/crates/project/src/manifest_tree.rs b/crates/project/src/manifest_tree.rs deleted file mode 100644 index ffa4872ca7..0000000000 --- a/crates/project/src/manifest_tree.rs +++ /dev/null @@ -1,224 +0,0 @@ -//! This module defines a Manifest Tree. -//! -//! A Manifest Tree is responsible for determining where the manifests for subprojects are located in a project. -//! This then is used to provide those locations to language servers & determine locations eligible for toolchain selection. - -mod manifest_store; -mod path_trie; -mod server_tree; - -use std::{borrow::Borrow, collections::hash_map::Entry, ops::ControlFlow, sync::Arc}; - -use collections::HashMap; -use gpui::{App, AppContext as _, Context, Entity, Subscription}; -use language::{ManifestDelegate, ManifestName, ManifestQuery}; -pub use manifest_store::ManifestProvidersStore; -use path_trie::{LabelPresence, RootPathTrie, TriePath}; -use settings::{SettingsStore, WorktreeId}; -use util::rel_path::RelPath; -use worktree::{Event as WorktreeEvent, Snapshot, Worktree}; - -use crate::{ - ProjectPath, - worktree_store::{WorktreeStore, WorktreeStoreEvent}, -}; - -pub(crate) use server_tree::{LanguageServerTree, LanguageServerTreeNode, LaunchDisposition}; - -struct WorktreeRoots { - roots: RootPathTrie, - worktree_store: Entity, - _worktree_subscription: Subscription, -} - -impl WorktreeRoots { - fn new( - worktree_store: Entity, - worktree: Entity, - cx: &mut App, - ) -> Entity { - cx.new(|cx| Self { - roots: RootPathTrie::new(), - worktree_store, - _worktree_subscription: cx.subscribe(&worktree, |this: &mut Self, _, event, cx| { - 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); - } - } - } - WorktreeEvent::UpdatedGitRepositories(_) => {} - WorktreeEvent::DeletedEntry(entry_id) => { - let Some(entry) = this.worktree_store.read(cx).entry_for_id(*entry_id, cx) - else { - return; - }; - let path = TriePath::from(entry.path.as_ref()); - this.roots.remove(&path); - } - } - }), - }) - } -} - -pub struct ManifestTree { - root_points: HashMap>, - worktree_store: Entity, - _subscriptions: [Subscription; 2], -} - -impl ManifestTree { - pub fn new(worktree_store: Entity, cx: &mut App) -> Entity { - cx.new(|cx| Self { - root_points: Default::default(), - _subscriptions: [ - cx.subscribe(&worktree_store, Self::on_worktree_store_event), - cx.observe_global::(|this, cx| { - for roots in this.root_points.values_mut() { - roots.update(cx, |worktree_roots, _| { - worktree_roots.roots = RootPathTrie::new(); - }) - } - }), - ], - worktree_store, - }) - } - - pub(crate) fn root_for_path( - &mut self, - ProjectPath { worktree_id, path }: &ProjectPath, - manifest_name: &ManifestName, - 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) { - 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) - else { - return Default::default(); - }; - let roots = WorktreeRoots::new(self.worktree_store.clone(), worktree, cx); - vacant_entry.insert(roots).clone() - } - }; - - 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 { - 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; - } - - } - ControlFlow::Continue(()) - }); - }); - - if current_presence == LabelPresence::KnownAbsent { - // Some part of the path is unexplored. - let depth = marked_path - .as_ref() - .map(|root_path| { - path.strip_prefix(&root_path.path) - .unwrap() - .components() - .count() - }) - .unwrap_or_else(|| path.components().count() + 1); - - if depth > 0 - && let Some(provider) = - ManifestProvidersStore::global(cx).get(manifest_name.borrow()) - { - let root = provider.search(ManifestQuery { - path: path.clone(), - depth, - delegate: delegate.clone(), - }); - match root { - Some(known_root) => worktree_roots.update(cx, |this, _| { - 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, - path: known_root, - }); - }), - None => worktree_roots.update(cx, |this, _| { - this.roots - .insert(&key, manifest_name.clone(), LabelPresence::KnownAbsent); - }), - } - } - } - 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: RelPath::empty().into(), - }) - } - - fn on_worktree_store_event( - &mut self, - _: Entity, - evt: &WorktreeStoreEvent, - _: &mut Context, - ) { - if let WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) = evt { - self.root_points.remove(worktree_id); - } - } -} - -pub(crate) struct ManifestQueryDelegate { - worktree: Snapshot, -} - -impl ManifestQueryDelegate { - pub fn new(worktree: Snapshot) -> Self { - Self { worktree } - } -} - -impl ManifestDelegate for ManifestQueryDelegate { - fn exists(&self, path: &RelPath, 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()) - }) - } - - fn worktree_id(&self) -> WorktreeId { - self.worktree.id() - } -} diff --git a/crates/project/src/manifest_tree/manifest_store.rs b/crates/project/src/manifest_tree/manifest_store.rs deleted file mode 100644 index cf9f81aee4..0000000000 --- a/crates/project/src/manifest_tree/manifest_store.rs +++ /dev/null @@ -1,51 +0,0 @@ -use collections::{HashMap, HashSet}; -use gpui::{App, Global, SharedString}; -use parking_lot::RwLock; -use std::{ops::Deref, sync::Arc}; - -use language::{ManifestName, ManifestProvider}; - -#[derive(Default)] -struct ManifestProvidersState { - providers: HashMap>, -} - -#[derive(Clone, Default)] -pub struct ManifestProvidersStore(Arc>); - -#[derive(Default)] -struct GlobalManifestProvider(ManifestProvidersStore); - -impl Deref for GlobalManifestProvider { - type Target = ManifestProvidersStore; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Global for GlobalManifestProvider {} - -impl ManifestProvidersStore { - /// Returns the global [`ManifestStore`]. - /// - /// Inserts a default [`ManifestStore`] if one does not yet exist. - pub fn global(cx: &mut App) -> Self { - cx.default_global::().0.clone() - } - - pub fn register(&self, provider: Arc) { - self.0.write().providers.insert(provider.name(), provider); - } - - pub fn unregister(&self, name: &SharedString) { - self.0.write().providers.remove(name); - } - - 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 deleted file mode 100644 index 9710bb46d0..0000000000 --- a/crates/project/src/manifest_tree/path_trie.rs +++ /dev/null @@ -1,265 +0,0 @@ -use std::{ - collections::{BTreeMap, btree_map::Entry}, - ops::ControlFlow, - sync::Arc, -}; - -use util::rel_path::RelPath; - -/// [RootPathTrie] is a workhorse of [super::ManifestTree]. It is responsible for determining the closest known entry for a given path. -/// It also determines how much of a given path is unexplored, thus letting callers fill in that gap if needed. -/// Conceptually, it allows one to annotate Worktree entries with arbitrary extra metadata and run closest-ancestor searches. -/// -/// A path is unexplored when the closest ancestor of a path is not the path itself; that means that we have not yet ran the scan on that path. -/// For example, if there's a project root at path `python/project` and we query for a path `python/project/subdir/another_subdir/file.py`, there is -/// a known root at `python/project` and the unexplored part is `subdir/another_subdir` - we need to run a scan on these 2 directories. -pub(super) struct RootPathTrie

{ - const GROUP_NAME: &str = "project_entry"; - - let kind = details.kind; - let is_sticky = details.sticky.is_some(); - let sticky_index = details.sticky.as_ref().map(|this| this.sticky_index); - let settings = ProjectPanelSettings::get_global(cx); - let show_editor = details.is_editing && !details.is_processing; - - let selection = SelectedEntry { - worktree_id: details.worktree_id, - entry_id, - }; - - let is_marked = self.marked_entries.contains(&selection); - let is_active = self - .state - .selection - .is_some_and(|selection| selection.entry_id == entry_id); - - let file_name = details.filename.clone(); - - let mut icon = details.icon.clone(); - if settings.file_icons && show_editor && details.kind.is_file() { - let filename = self.filename_editor.read(cx).text(cx); - if filename.len() > 2 { - icon = FileIcons::get_icon(Path::new(&filename), cx); - } - } - - let filename_text_color = details.filename_text_color; - let diagnostic_severity = details.diagnostic_severity; - let item_colors = get_item_color(is_sticky, cx); - - let canonical_path = details - .canonical_path - .as_ref() - .map(|f| f.to_string_lossy().into_owned()); - let path_style = self.project.read(cx).path_style(cx); - let path = details.path.clone(); - let path_for_external_paths = path.clone(); - let path_for_dragged_selection = path.clone(); - - let depth = details.depth; - let worktree_id = details.worktree_id; - let dragged_selection = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: selection.worktree_id, - entry_id: self.resolve_entry(selection.entry_id), - }, - marked_selections: Arc::from(self.marked_entries.clone()), - }; - - let bg_color = if is_marked { - item_colors.marked - } else { - item_colors.default - }; - - let bg_hover_color = if is_marked { - item_colors.marked - } else { - item_colors.hover - }; - - let validation_color_and_message = if show_editor { - match self - .state - .edit_state - .as_ref() - .map_or(ValidationState::None, |e| e.validation_state.clone()) - { - ValidationState::Error(msg) => Some((Color::Error.color(cx), msg)), - ValidationState::Warning(msg) => Some((Color::Warning.color(cx), msg)), - ValidationState::None => None, - } - } else { - None - }; - - let border_color = - if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) { - match validation_color_and_message { - Some((color, _)) => color, - None => item_colors.focused, - } - } else { - bg_color - }; - - let border_hover_color = - if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) { - match validation_color_and_message { - Some((color, _)) => color, - None => item_colors.focused, - } - } else { - bg_hover_color - }; - - let folded_directory_drag_target = self.folded_directory_drag_target; - let is_highlighted = { - if let Some(highlight_entry_id) = - self.drag_target_entry - .as_ref() - .and_then(|drag_target| match drag_target { - DragTarget::Entry { - highlight_entry_id, .. - } => Some(*highlight_entry_id), - DragTarget::Background => self.state.last_worktree_root_id, - }) - { - // Highlight if same entry or it's children - if entry_id == highlight_entry_id { - true - } else { - maybe!({ - let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?; - let highlight_entry = worktree.read(cx).entry_for_id(highlight_entry_id)?; - Some(path.starts_with(&highlight_entry.path)) - }) - .unwrap_or(false) - } - } else { - false - } - }; - - let id: ElementId = if is_sticky { - SharedString::from(format!("project_panel_sticky_item_{}", entry_id.to_usize())).into() - } else { - (entry_id.to_proto() as usize).into() - }; - - div() - .id(id.clone()) - .relative() - .group(GROUP_NAME) - .cursor_pointer() - .rounded_none() - .bg(bg_color) - .border_1() - .border_r_2() - .border_color(border_color) - .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color)) - .when(is_sticky, |this| { - this.block_mouse_except_scroll() - }) - .when(!is_sticky, |this| { - this - .when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over)) - .when(settings.drag_and_drop, |this| this - .on_drag_move::(cx.listener( - move |this, event: &DragMoveEvent, _, cx| { - let is_current_target = this.drag_target_entry.as_ref() - .and_then(|entry| match entry { - DragTarget::Entry { entry_id: target_id, .. } => Some(*target_id), - DragTarget::Background { .. } => None, - }) == Some(entry_id); - - if !event.bounds.contains(&event.event.position) { - // Entry responsible for setting drag target is also responsible to - // clear it up after drag is out of bounds - if is_current_target { - this.drag_target_entry = None; - } - return; - } - - if is_current_target { - return; - } - - this.marked_entries.clear(); - - let Some((entry_id, highlight_entry_id)) = maybe!({ - let target_worktree = this.project.read(cx).worktree_for_id(selection.worktree_id, cx)?.read(cx); - let target_entry = target_worktree.entry_for_path(&path_for_external_paths)?; - let highlight_entry_id = this.highlight_entry_for_external_drag(target_entry, target_worktree)?; - Some((target_entry.id, highlight_entry_id)) - }) else { - return; - }; - - this.drag_target_entry = Some(DragTarget::Entry { - entry_id, - highlight_entry_id, - }); - - }, - )) - .on_drop(cx.listener( - move |this, external_paths: &ExternalPaths, window, cx| { - this.drag_target_entry = None; - this.hover_scroll_task.take(); - this.drop_external_files(external_paths.paths(), entry_id, window, cx); - cx.stop_propagation(); - }, - )) - .on_drag_move::(cx.listener( - move |this, event: &DragMoveEvent, window, cx| { - let is_current_target = this.drag_target_entry.as_ref() - .and_then(|entry| match entry { - DragTarget::Entry { entry_id: target_id, .. } => Some(*target_id), - DragTarget::Background { .. } => None, - }) == Some(entry_id); - - if !event.bounds.contains(&event.event.position) { - // Entry responsible for setting drag target is also responsible to - // clear it up after drag is out of bounds - if is_current_target { - this.drag_target_entry = None; - } - return; - } - - if is_current_target { - return; - } - - let drag_state = event.drag(cx); - - if drag_state.items().count() == 1 { - this.marked_entries.clear(); - this.marked_entries.push(drag_state.active_selection); - } - - let Some((entry_id, highlight_entry_id)) = maybe!({ - let target_worktree = this.project.read(cx).worktree_for_id(selection.worktree_id, cx)?.read(cx); - let target_entry = target_worktree.entry_for_path(&path_for_dragged_selection)?; - let highlight_entry_id = this.highlight_entry_for_selection_drag(target_entry, target_worktree, drag_state, cx)?; - Some((target_entry.id, highlight_entry_id)) - }) else { - return; - }; - - this.drag_target_entry = Some(DragTarget::Entry { - entry_id, - highlight_entry_id, - }); - - this.hover_expand_task.take(); - - if !kind.is_dir() - || this - .state - .expanded_dir_ids - .get(&details.worktree_id) - .is_some_and(|ids| ids.binary_search(&entry_id).is_ok()) - { - return; - } - - let bounds = event.bounds; - this.hover_expand_task = - Some(cx.spawn_in(window, async move |this, cx| { - cx.background_executor() - .timer(Duration::from_millis(500)) - .await; - this.update_in(cx, |this, window, cx| { - this.hover_expand_task.take(); - if this.drag_target_entry.as_ref().and_then(|entry| match entry { - DragTarget::Entry { entry_id: target_id, .. } => Some(*target_id), - DragTarget::Background { .. } => None, - }) == Some(entry_id) - && bounds.contains(&window.mouse_position()) - { - this.expand_entry(worktree_id, entry_id, cx); - this.update_visible_entries( - Some((worktree_id, entry_id)), - false, - false, - window, - cx, - ); - cx.notify(); - } - }) - .ok(); - })); - }, - )) - .on_drag( - dragged_selection, - { - let active_component = self.state.ancestors.get(&entry_id).and_then(|ancestors| ancestors.active_component(&details.filename)); - move |selection, click_offset, _window, cx| { - let filename = active_component.as_ref().unwrap_or_else(|| &details.filename); - cx.new(|_| DraggedProjectEntryView { - icon: details.icon.clone(), - filename: filename.clone(), - click_offset, - selection: selection.active_selection, - selections: selection.marked_selections.clone(), - }) - } - } - ) - .on_drop( - cx.listener(move |this, selections: &DraggedSelection, window, cx| { - this.drag_target_entry = None; - this.hover_scroll_task.take(); - this.hover_expand_task.take(); - if folded_directory_drag_target.is_some() { - return; - } - this.drag_onto(selections, entry_id, kind.is_file(), window, cx); - }), - )) - }) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |this, _, _, cx| { - this.mouse_down = true; - cx.propagate(); - }), - ) - .on_click( - cx.listener(move |project_panel, event: &gpui::ClickEvent, window, cx| { - if event.is_right_click() || event.first_focus() - || show_editor - { - return; - } - if event.standard_click() { - project_panel.mouse_down = false; - } - cx.stop_propagation(); - - if let Some(selection) = project_panel.state.selection.filter(|_| event.modifiers().shift) { - let current_selection = project_panel.index_for_selection(selection); - let clicked_entry = SelectedEntry { - entry_id, - worktree_id, - }; - let target_selection = project_panel.index_for_selection(clicked_entry); - if let Some(((_, _, source_index), (_, _, target_index))) = - current_selection.zip(target_selection) - { - let range_start = source_index.min(target_index); - let range_end = source_index.max(target_index) + 1; - let mut new_selections = Vec::new(); - project_panel.for_each_visible_entry( - range_start..range_end, - window, - cx, - |entry_id, details, _, _| { - new_selections.push(SelectedEntry { - entry_id, - worktree_id: details.worktree_id, - }); - }, - ); - - for selection in &new_selections { - if !project_panel.marked_entries.contains(selection) { - project_panel.marked_entries.push(*selection); - } - } - - project_panel.state.selection = Some(clicked_entry); - if !project_panel.marked_entries.contains(&clicked_entry) { - project_panel.marked_entries.push(clicked_entry); - } - } - } else if event.modifiers().secondary() { - if event.click_count() > 1 { - project_panel.split_entry(entry_id, false, None, cx); - } else { - project_panel.state.selection = Some(selection); - if let Some(position) = project_panel.marked_entries.iter().position(|e| *e == selection) { - project_panel.marked_entries.remove(position); - } else { - project_panel.marked_entries.push(selection); - } - } - } else if kind.is_dir() { - project_panel.marked_entries.clear(); - if is_sticky - && let Some((_, _, index)) = project_panel.index_for_entry(entry_id, worktree_id) { - project_panel.scroll_handle.scroll_to_item_strict_with_offset(index, ScrollStrategy::Top, sticky_index.unwrap_or(0)); - cx.notify(); - // move down by 1px so that clicked item - // don't count as sticky anymore - cx.on_next_frame(window, |_, window, cx| { - cx.on_next_frame(window, |this, _, cx| { - let mut offset = this.scroll_handle.offset(); - offset.y += px(1.); - this.scroll_handle.set_offset(offset); - cx.notify(); - }); - }); - return; - } - if event.modifiers().alt { - project_panel.toggle_expand_all(entry_id, window, cx); - } else { - project_panel.toggle_expanded(entry_id, window, cx); - } - } else { - let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enable_preview_from_project_panel; - let click_count = event.click_count(); - let focus_opened_item = click_count > 1; - let allow_preview = preview_tabs_enabled && click_count == 1; - project_panel.open_entry(entry_id, focus_opened_item, allow_preview, cx); - } - }), - ) - .child( - ListItem::new(id) - .indent_level(depth) - .indent_step_size(px(settings.indent_size)) - .spacing(match settings.entry_spacing { - ProjectPanelEntrySpacing::Comfortable => ListItemSpacing::Dense, - ProjectPanelEntrySpacing::Standard => { - ListItemSpacing::ExtraDense - } - }) - .selectable(false) - .when_some(canonical_path, |this, path| { - this.end_slot::( - div() - .id("symlink_icon") - .pr_3() - .tooltip(move |_window, cx| { - Tooltip::with_meta( - path.to_string(), - None, - "Symbolic Link", - cx, - ) - }) - .child( - Icon::new(IconName::ArrowUpRight) - .size(IconSize::Indicator) - .color(filename_text_color), - ) - .into_any_element(), - ) - }) - .child(if let Some(icon) = &icon { - if let Some((_, decoration_color)) = - entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity) - { - let is_warning = diagnostic_severity - .map(|severity| matches!(severity, DiagnosticSeverity::WARNING)) - .unwrap_or(false); - div().child( - DecoratedIcon::new( - Icon::from_path(icon.clone()).color(Color::Muted), - Some( - IconDecoration::new( - if kind.is_file() { - if is_warning { - IconDecorationKind::Triangle - } else { - IconDecorationKind::X - } - } else { - IconDecorationKind::Dot - }, - bg_color, - cx, - ) - .group_name(Some(GROUP_NAME.into())) - .knockout_hover_color(bg_hover_color) - .color(decoration_color.color(cx)) - .position(Point { - x: px(-2.), - y: px(-2.), - }), - ), - ) - .into_any_element(), - ) - } else { - h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted)) - } - } else if let Some((icon_name, color)) = - entry_diagnostic_aware_icon_name_and_color(diagnostic_severity) - { - h_flex() - .size(IconSize::default().rems()) - .child(Icon::new(icon_name).color(color).size(IconSize::Small)) - } else { - h_flex() - .size(IconSize::default().rems()) - .invisible() - .flex_none() - }) - .child( - if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) { - h_flex().h_6().w_full().child(editor.clone()) - } else { - h_flex().h_6().map(|mut this| { - if let Some(folded_ancestors) = self.state.ancestors.get(&entry_id) { - let components = Path::new(&file_name) - .components() - .map(|comp| comp.as_os_str().to_string_lossy().into_owned()) - .collect::>(); - let active_index = folded_ancestors.active_index(); - let components_len = components.len(); - let delimiter = SharedString::new(path_style.primary_separator()); - for (index, component) in components.iter().enumerate() { - if index != 0 { - let delimiter_target_index = index - 1; - let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - delimiter_target_index).cloned(); - this = this.child( - div() - .when(!is_sticky, |div| { - div - .when(settings.drag_and_drop, |div| div - .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| { - this.hover_scroll_task.take(); - this.drag_target_entry = None; - this.folded_directory_drag_target = None; - if let Some(target_entry_id) = target_entry_id { - this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx); - } - })) - .on_drag_move(cx.listener( - move |this, event: &DragMoveEvent, _, _| { - if event.bounds.contains(&event.event.position) { - this.folded_directory_drag_target = Some( - FoldedDirectoryDragTarget { - entry_id, - index: delimiter_target_index, - is_delimiter_target: true, - } - ); - } else { - let is_current_target = this.folded_directory_drag_target - .is_some_and(|target| - target.entry_id == entry_id && - target.index == delimiter_target_index && - target.is_delimiter_target - ); - if is_current_target { - this.folded_directory_drag_target = None; - } - } - - }, - ))) - }) - .child( - Label::new(delimiter.clone()) - .single_line() - .color(filename_text_color) - ) - ); - } - let id = SharedString::from(format!( - "project_panel_path_component_{}_{index}", - entry_id.to_usize() - )); - let label = div() - .id(id) - .when(!is_sticky,| div| { - div - .when(index != components_len - 1, |div|{ - let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned(); - div - .when(settings.drag_and_drop, |div| div - .on_drag_move(cx.listener( - move |this, event: &DragMoveEvent, _, _| { - if event.bounds.contains(&event.event.position) { - this.folded_directory_drag_target = Some( - FoldedDirectoryDragTarget { - entry_id, - index, - is_delimiter_target: false, - } - ); - } else { - let is_current_target = this.folded_directory_drag_target - .as_ref() - .is_some_and(|target| - target.entry_id == entry_id && - target.index == index && - !target.is_delimiter_target - ); - if is_current_target { - this.folded_directory_drag_target = None; - } - } - }, - )) - .on_drop(cx.listener(move |this, selections: &DraggedSelection, window,cx| { - this.hover_scroll_task.take(); - this.drag_target_entry = None; - this.folded_directory_drag_target = None; - if let Some(target_entry_id) = target_entry_id { - this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx); - } - })) - .when(folded_directory_drag_target.is_some_and(|target| - target.entry_id == entry_id && - target.index == index - ), |this| { - this.bg(item_colors.drag_over) - })) - }) - }) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |this, _, _, cx| { - if index != active_index - && let Some(folds) = - this.state.ancestors.get_mut(&entry_id) - { - folds.current_ancestor_depth = - components_len - 1 - index; - cx.notify(); - } - }), - ) - .child( - Label::new(component) - .single_line() - .color(filename_text_color) - .when( - index == active_index - && (is_active || is_marked), - |this| this.underline(), - ), - ); - - this = this.child(label); - } - - this - } else { - this.child( - Label::new(file_name) - .single_line() - .color(filename_text_color), - ) - } - }) - }, - ) - .on_secondary_mouse_down(cx.listener( - move |this, event: &MouseDownEvent, window, cx| { - // Stop propagation to prevent the catch-all context menu for the project - // panel from being deployed. - cx.stop_propagation(); - // Some context menu actions apply to all marked entries. If the user - // right-clicks on an entry that is not marked, they may not realize the - // action applies to multiple entries. To avoid inadvertent changes, all - // entries are unmarked. - if !this.marked_entries.contains(&selection) { - this.marked_entries.clear(); - } - this.deploy_context_menu(event.position, entry_id, window, cx); - }, - )) - .overflow_x(), - ) - .when_some( - validation_color_and_message, - |this, (color, message)| { - this - .relative() - .child( - deferred( - div() - .occlude() - .absolute() - .top_full() - .left(px(-1.)) // Used px over rem so that it doesn't change with font size - .right(px(-0.5)) - .py_1() - .px_2() - .border_1() - .border_color(color) - .bg(cx.theme().colors().background) - .child( - Label::new(message) - .color(Color::from(color)) - .size(LabelSize::Small) - ) - ) - ) - } - ) - } - - fn details_for_entry( - &self, - entry: &Entry, - worktree_id: WorktreeId, - root_name: &RelPath, - entries_paths: &HashSet>, - git_status: GitSummary, - sticky: Option, - _window: &mut Window, - cx: &mut Context, - ) -> EntryDetails { - let (show_file_icons, show_folder_icons) = { - let settings = ProjectPanelSettings::get_global(cx); - (settings.file_icons, settings.folder_icons) - }; - - let expanded_entry_ids = self - .state - .expanded_dir_ids - .get(&worktree_id) - .map(Vec::as_slice) - .unwrap_or(&[]); - let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok(); - - let icon = match entry.kind { - EntryKind::File => { - if show_file_icons { - FileIcons::get_icon(entry.path.as_std_path(), cx) - } else { - None - } - } - _ => { - if show_folder_icons { - FileIcons::get_folder_icon(is_expanded, entry.path.as_std_path(), cx) - } else { - FileIcons::get_chevron_icon(is_expanded, cx) - } - } - }; - - let path_style = self.project.read(cx).path_style(cx); - let (depth, difference) = - ProjectPanel::calculate_depth_and_difference(entry, entries_paths); - - let filename = if difference > 1 { - entry - .path - .last_n_components(difference) - .map_or(String::new(), |suffix| { - suffix.display(path_style).to_string() - }) - } else { - entry - .path - .file_name() - .map(|name| name.to_string()) - .unwrap_or_else(|| root_name.as_unix_str().to_string()) - }; - - let selection = SelectedEntry { - worktree_id, - entry_id: entry.id, - }; - let is_marked = self.marked_entries.contains(&selection); - let is_selected = self.state.selection == Some(selection); - - let diagnostic_severity = self - .diagnostics - .get(&(worktree_id, entry.path.clone())) - .cloned(); - - let filename_text_color = - entry_git_aware_label_color(git_status, entry.is_ignored, is_marked); - - let is_cut = self - .clipboard - .as_ref() - .is_some_and(|e| e.is_cut() && e.items().contains(&selection)); - - EntryDetails { - filename, - icon, - path: entry.path.clone(), - depth, - kind: entry.kind, - is_ignored: entry.is_ignored, - is_expanded, - is_selected, - is_marked, - is_editing: false, - is_processing: false, - is_cut, - sticky, - filename_text_color, - diagnostic_severity, - git_status, - is_private: entry.is_private, - worktree_id, - canonical_path: entry.canonical_path.clone(), - } - } - - fn dispatch_context(&self, window: &Window, cx: &Context) -> KeyContext { - let mut dispatch_context = KeyContext::new_with_defaults(); - dispatch_context.add("ProjectPanel"); - dispatch_context.add("menu"); - - let identifier = if self.filename_editor.focus_handle(cx).is_focused(window) { - "editing" - } else { - "not_editing" - }; - - dispatch_context.add(identifier); - dispatch_context - } - - fn reveal_entry( - &mut self, - project: Entity, - entry_id: ProjectEntryId, - skip_ignored: bool, - window: &mut Window, - cx: &mut Context, - ) -> Result<()> { - let worktree = project - .read(cx) - .worktree_for_entry(entry_id, cx) - .context("can't reveal a non-existent entry in the project panel")?; - let worktree = worktree.read(cx); - if skip_ignored - && worktree - .entry_for_id(entry_id) - .is_none_or(|entry| entry.is_ignored && !entry.is_always_included) - { - anyhow::bail!("can't reveal an ignored entry in the project panel"); - } - let is_active_item_file_diff_view = self - .workspace - .upgrade() - .and_then(|ws| ws.read(cx).active_item(cx)) - .map(|item| item.act_as_type(TypeId::of::(), cx).is_some()) - .unwrap_or(false); - if is_active_item_file_diff_view { - return Ok(()); - } - - let worktree_id = worktree.id(); - self.expand_entry(worktree_id, entry_id, cx); - self.update_visible_entries(Some((worktree_id, entry_id)), false, true, window, cx); - self.marked_entries.clear(); - self.marked_entries.push(SelectedEntry { - worktree_id, - entry_id, - }); - cx.notify(); - Ok(()) - } - - fn find_active_indent_guide( - &self, - indent_guides: &[IndentGuideLayout], - cx: &App, - ) -> Option { - let (worktree, entry) = self.selected_entry(cx)?; - - // Find the parent entry of the indent guide, this will either be the - // expanded folder we have selected, or the parent of the currently - // selected file/collapsed directory - let mut entry = entry; - loop { - let is_expanded_dir = entry.is_dir() - && self - .state - .expanded_dir_ids - .get(&worktree.id()) - .map(|ids| ids.binary_search(&entry.id).is_ok()) - .unwrap_or(false); - if is_expanded_dir { - break; - } - entry = worktree.entry_for_path(&entry.path.parent()?)?; - } - - let (active_indent_range, depth) = { - let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?; - let child_paths = &self.state.visible_entries[worktree_ix].entries; - let mut child_count = 0; - let depth = entry.path.ancestors().count(); - while let Some(entry) = child_paths.get(child_offset + child_count + 1) { - if entry.path.ancestors().count() <= depth { - break; - } - child_count += 1; - } - - let start = ix + 1; - let end = start + child_count; - - let visible_worktree = &self.state.visible_entries[worktree_ix]; - let visible_worktree_entries = visible_worktree.index.get_or_init(|| { - visible_worktree - .entries - .iter() - .map(|e| e.path.clone()) - .collect() - }); - - // Calculate the actual depth of the entry, taking into account that directories can be auto-folded. - let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries); - (start..end, depth) - }; - - let candidates = indent_guides - .iter() - .enumerate() - .filter(|(_, indent_guide)| indent_guide.offset.x == depth); - - for (i, indent) in candidates { - // Find matches that are either an exact match, partially on screen, or inside the enclosing indent - if active_indent_range.start <= indent.offset.y + indent.length - && indent.offset.y <= active_indent_range.end - { - return Some(i); - } - } - None - } - - fn render_sticky_entries( - &self, - child: StickyProjectPanelCandidate, - window: &mut Window, - cx: &mut Context, - ) -> SmallVec<[AnyElement; 8]> { - let project = self.project.read(cx); - - let Some((worktree_id, entry_ref)) = self.entry_at_index(child.index) else { - return SmallVec::new(); - }; - - let Some(visible) = self - .state - .visible_entries - .iter() - .find(|worktree| worktree.worktree_id == worktree_id) - else { - return SmallVec::new(); - }; - - let Some(worktree) = project.worktree_for_id(worktree_id, cx) else { - return SmallVec::new(); - }; - let worktree = worktree.read(cx).snapshot(); - - let paths = visible - .index - .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect()); - - let mut sticky_parents = Vec::new(); - let mut current_path = entry_ref.path.clone(); - - 'outer: loop { - if let Some(parent_path) = current_path.parent() { - for ancestor_path in parent_path.ancestors() { - if paths.contains(ancestor_path) - && let Some(parent_entry) = worktree.entry_for_path(ancestor_path) - { - sticky_parents.push(parent_entry.clone()); - current_path = parent_entry.path.clone(); - continue 'outer; - } - } - } - break 'outer; - } - - if sticky_parents.is_empty() { - return SmallVec::new(); - } - - sticky_parents.reverse(); - - let panel_settings = ProjectPanelSettings::get_global(cx); - let git_status_enabled = panel_settings.git_status; - let root_name = worktree.root_name(); - - let git_summaries_by_id = if git_status_enabled { - visible - .entries - .iter() - .map(|e| (e.id, e.git_summary)) - .collect::>() - } else { - Default::default() - }; - - // already checked if non empty above - let last_item_index = sticky_parents.len() - 1; - sticky_parents - .iter() - .enumerate() - .map(|(index, entry)| { - let git_status = git_summaries_by_id - .get(&entry.id) - .copied() - .unwrap_or_default(); - let sticky_details = Some(StickyDetails { - sticky_index: index, - }); - let details = self.details_for_entry( - entry, - worktree_id, - root_name, - paths, - git_status, - sticky_details, - window, - cx, - ); - self.render_entry(entry.id, details, window, cx) - .when(index == last_item_index, |this| { - let shadow_color_top = hsla(0.0, 0.0, 0.0, 0.1); - let shadow_color_bottom = hsla(0.0, 0.0, 0.0, 0.); - let sticky_shadow = div() - .absolute() - .left_0() - .bottom_neg_1p5() - .h_1p5() - .w_full() - .bg(linear_gradient( - 0., - linear_color_stop(shadow_color_top, 1.), - linear_color_stop(shadow_color_bottom, 0.), - )); - this.child(sticky_shadow) - }) - .into_any() - }) - .collect() - } -} - -#[derive(Clone)] -struct StickyProjectPanelCandidate { - index: usize, - depth: usize, -} - -impl StickyCandidate for StickyProjectPanelCandidate { - fn depth(&self) -> usize { - self.depth - } -} - -fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize { - const ICON_SIZE_FACTOR: usize = 2; - let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars; - if is_symlink { - item_width += ICON_SIZE_FACTOR; - } - item_width -} - -impl Render for ProjectPanel { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let has_worktree = !self.state.visible_entries.is_empty(); - let project = self.project.read(cx); - let panel_settings = ProjectPanelSettings::get_global(cx); - let indent_size = panel_settings.indent_size; - let show_indent_guides = panel_settings.indent_guides.show == ShowIndentGuides::Always; - let show_sticky_entries = { - if panel_settings.sticky_scroll { - let is_scrollable = self.scroll_handle.is_scrollable(); - let is_scrolled = self.scroll_handle.offset().y < px(0.); - is_scrollable && is_scrolled - } else { - false - } - }; - - let is_local = project.is_local(); - - if has_worktree { - let item_count = self - .state - .visible_entries - .iter() - .map(|worktree| worktree.entries.len()) - .sum(); - - fn handle_drag_move( - this: &mut ProjectPanel, - e: &DragMoveEvent, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(previous_position) = this.previous_drag_position { - // Refresh cursor only when an actual drag happens, - // because modifiers are not updated when the cursor is not moved. - if e.event.position != previous_position { - this.refresh_drag_cursor_style(&e.event.modifiers, window, cx); - } - } - this.previous_drag_position = Some(e.event.position); - - if !e.bounds.contains(&e.event.position) { - this.drag_target_entry = None; - return; - } - this.hover_scroll_task.take(); - let panel_height = e.bounds.size.height; - if panel_height <= px(0.) { - return; - } - - let event_offset = e.event.position.y - e.bounds.origin.y; - // How far along in the project panel is our cursor? (0. is the top of a list, 1. is the bottom) - let hovered_region_offset = event_offset / panel_height; - - // We want the scrolling to be a bit faster when the cursor is closer to the edge of a list. - // These pixels offsets were picked arbitrarily. - let vertical_scroll_offset = if hovered_region_offset <= 0.05 { - 8. - } else if hovered_region_offset <= 0.15 { - 5. - } else if hovered_region_offset >= 0.95 { - -8. - } else if hovered_region_offset >= 0.85 { - -5. - } else { - return; - }; - let adjustment = point(px(0.), px(vertical_scroll_offset)); - this.hover_scroll_task = Some(cx.spawn_in(window, async move |this, cx| { - loop { - let should_stop_scrolling = this - .update(cx, |this, cx| { - this.hover_scroll_task.as_ref()?; - let handle = this.scroll_handle.0.borrow_mut(); - let offset = handle.base_handle.offset(); - - handle.base_handle.set_offset(offset + adjustment); - cx.notify(); - Some(()) - }) - .ok() - .flatten() - .is_some(); - if should_stop_scrolling { - return; - } - cx.background_executor() - .timer(Duration::from_millis(16)) - .await; - } - })); - } - h_flex() - .id("project-panel") - .group("project-panel") - .when(panel_settings.drag_and_drop, |this| { - this.on_drag_move(cx.listener(handle_drag_move::)) - .on_drag_move(cx.listener(handle_drag_move::)) - }) - .size_full() - .relative() - .on_modifiers_changed(cx.listener( - |this, event: &ModifiersChangedEvent, window, cx| { - this.refresh_drag_cursor_style(&event.modifiers, window, cx); - }, - )) - .key_context(self.dispatch_context(window, cx)) - .on_action(cx.listener(Self::scroll_up)) - .on_action(cx.listener(Self::scroll_down)) - .on_action(cx.listener(Self::scroll_cursor_center)) - .on_action(cx.listener(Self::scroll_cursor_top)) - .on_action(cx.listener(Self::scroll_cursor_bottom)) - .on_action(cx.listener(Self::select_next)) - .on_action(cx.listener(Self::select_previous)) - .on_action(cx.listener(Self::select_first)) - .on_action(cx.listener(Self::select_last)) - .on_action(cx.listener(Self::select_parent)) - .on_action(cx.listener(Self::select_next_git_entry)) - .on_action(cx.listener(Self::select_prev_git_entry)) - .on_action(cx.listener(Self::select_next_diagnostic)) - .on_action(cx.listener(Self::select_prev_diagnostic)) - .on_action(cx.listener(Self::select_next_directory)) - .on_action(cx.listener(Self::select_prev_directory)) - .on_action(cx.listener(Self::expand_selected_entry)) - .on_action(cx.listener(Self::collapse_selected_entry)) - .on_action(cx.listener(Self::collapse_all_entries)) - .on_action(cx.listener(Self::open)) - .on_action(cx.listener(Self::open_permanent)) - .on_action(cx.listener(Self::open_split_vertical)) - .on_action(cx.listener(Self::open_split_horizontal)) - .on_action(cx.listener(Self::confirm)) - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(Self::copy_path)) - .on_action(cx.listener(Self::copy_relative_path)) - .on_action(cx.listener(Self::new_search_in_directory)) - .on_action(cx.listener(Self::unfold_directory)) - .on_action(cx.listener(Self::fold_directory)) - .on_action(cx.listener(Self::remove_from_project)) - .on_action(cx.listener(Self::compare_marked_files)) - .when(!project.is_read_only(cx), |el| { - el.on_action(cx.listener(Self::new_file)) - .on_action(cx.listener(Self::new_directory)) - .on_action(cx.listener(Self::rename)) - .on_action(cx.listener(Self::delete)) - .on_action(cx.listener(Self::cut)) - .on_action(cx.listener(Self::copy)) - .on_action(cx.listener(Self::paste)) - .on_action(cx.listener(Self::duplicate)) - .when(!project.is_remote(), |el| { - el.on_action(cx.listener(Self::trash)) - }) - }) - .when(project.is_local(), |el| { - el.on_action(cx.listener(Self::reveal_in_finder)) - .on_action(cx.listener(Self::open_system)) - .on_action(cx.listener(Self::open_in_terminal)) - }) - .when(project.is_via_remote_server(), |el| { - el.on_action(cx.listener(Self::open_in_terminal)) - }) - .track_focus(&self.focus_handle(cx)) - .child( - v_flex() - .child( - uniform_list("entries", item_count, { - cx.processor(|this, range: Range, window, cx| { - this.rendered_entries_len = range.end - range.start; - let mut items = Vec::with_capacity(this.rendered_entries_len); - this.for_each_visible_entry( - range, - window, - cx, - |id, details, window, cx| { - items.push(this.render_entry(id, details, window, cx)); - }, - ); - items - }) - }) - .when(show_indent_guides, |list| { - list.with_decoration( - ui::indent_guides( - px(indent_size), - IndentGuideColors::panel(cx), - ) - .with_compute_indents_fn( - cx.entity(), - |this, range, window, cx| { - let mut items = - SmallVec::with_capacity(range.end - range.start); - this.iter_visible_entries( - range, - window, - cx, - |entry, _, entries, _, _| { - let (depth, _) = - Self::calculate_depth_and_difference( - entry, entries, - ); - items.push(depth); - }, - ); - items - }, - ) - .on_click(cx.listener( - |this, - active_indent_guide: &IndentGuideLayout, - window, - cx| { - if window.modifiers().secondary() { - let ix = active_indent_guide.offset.y; - let Some((target_entry, worktree)) = maybe!({ - let (worktree_id, entry) = - this.entry_at_index(ix)?; - let worktree = this - .project - .read(cx) - .worktree_for_id(worktree_id, cx)?; - let target_entry = worktree - .read(cx) - .entry_for_path(&entry.path.parent()?)?; - Some((target_entry, worktree)) - }) else { - return; - }; - - this.collapse_entry( - target_entry.clone(), - worktree, - window, - cx, - ); - } - }, - )) - .with_render_fn( - cx.entity(), - move |this, params, _, cx| { - const LEFT_OFFSET: Pixels = px(14.); - const PADDING_Y: Pixels = px(4.); - const HITBOX_OVERDRAW: Pixels = px(3.); - - let active_indent_guide_index = this - .find_active_indent_guide( - ¶ms.indent_guides, - cx, - ); - - let indent_size = params.indent_size; - let item_height = params.item_height; - - params - .indent_guides - .into_iter() - .enumerate() - .map(|(idx, layout)| { - let offset = if layout.continues_offscreen { - px(0.) - } else { - PADDING_Y - }; - let bounds = Bounds::new( - point( - layout.offset.x * indent_size - + LEFT_OFFSET, - layout.offset.y * item_height + offset, - ), - size( - px(1.), - layout.length * item_height - - offset * 2., - ), - ); - ui::RenderedIndentGuide { - bounds, - layout, - is_active: Some(idx) - == active_indent_guide_index, - hitbox: Some(Bounds::new( - point( - bounds.origin.x - HITBOX_OVERDRAW, - bounds.origin.y, - ), - size( - bounds.size.width - + HITBOX_OVERDRAW * 2., - bounds.size.height, - ), - )), - } - }) - .collect() - }, - ), - ) - }) - .when(show_sticky_entries, |list| { - let sticky_items = ui::sticky_items( - cx.entity(), - |this, range, window, cx| { - let mut items = - SmallVec::with_capacity(range.end - range.start); - this.iter_visible_entries( - range, - window, - cx, - |entry, index, entries, _, _| { - let (depth, _) = - Self::calculate_depth_and_difference( - entry, entries, - ); - let candidate = - StickyProjectPanelCandidate { index, depth }; - items.push(candidate); - }, - ); - items - }, - |this, marker_entry, window, cx| { - let sticky_entries = - this.render_sticky_entries(marker_entry, window, cx); - this.sticky_items_count = sticky_entries.len(); - sticky_entries - }, - ); - list.with_decoration(if show_indent_guides { - sticky_items.with_decoration( - ui::indent_guides( - px(indent_size), - IndentGuideColors::panel(cx), - ) - .with_render_fn( - cx.entity(), - move |_, params, _, _| { - const LEFT_OFFSET: Pixels = px(14.); - - let indent_size = params.indent_size; - let item_height = params.item_height; - - params - .indent_guides - .into_iter() - .map(|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: false, - hitbox: None, - } - }) - .collect() - }, - ), - ) - } else { - sticky_items - }) - }) - .with_sizing_behavior(ListSizingBehavior::Infer) - .with_horizontal_sizing_behavior( - ListHorizontalSizingBehavior::Unconstrained, - ) - .with_width_from_item(self.state.max_width_item_index) - .track_scroll(&self.scroll_handle), - ) - .child( - div() - .id("project-panel-blank-area") - .block_mouse_except_scroll() - .flex_grow() - .when( - self.drag_target_entry.as_ref().is_some_and( - |entry| match entry { - DragTarget::Background => true, - DragTarget::Entry { - highlight_entry_id, .. - } => self.state.last_worktree_root_id.is_some_and( - |root_id| *highlight_entry_id == root_id, - ), - }, - ), - |div| div.bg(cx.theme().colors().drop_target_background), - ) - .on_drag_move::(cx.listener( - move |this, event: &DragMoveEvent, _, _| { - let Some(_last_root_id) = this.state.last_worktree_root_id - else { - return; - }; - if event.bounds.contains(&event.event.position) { - this.drag_target_entry = Some(DragTarget::Background); - } else { - if this.drag_target_entry.as_ref().is_some_and(|e| { - matches!(e, DragTarget::Background) - }) { - this.drag_target_entry = None; - } - } - }, - )) - .on_drag_move::(cx.listener( - move |this, event: &DragMoveEvent, _, cx| { - let Some(last_root_id) = this.state.last_worktree_root_id - else { - return; - }; - if event.bounds.contains(&event.event.position) { - let drag_state = event.drag(cx); - if this.should_highlight_background_for_selection_drag( - &drag_state, - last_root_id, - cx, - ) { - this.drag_target_entry = - Some(DragTarget::Background); - } - } else { - if this.drag_target_entry.as_ref().is_some_and(|e| { - matches!(e, DragTarget::Background) - }) { - this.drag_target_entry = None; - } - } - }, - )) - .on_drop(cx.listener( - move |this, external_paths: &ExternalPaths, window, cx| { - this.drag_target_entry = None; - this.hover_scroll_task.take(); - if let Some(entry_id) = this.state.last_worktree_root_id { - this.drop_external_files( - external_paths.paths(), - entry_id, - window, - cx, - ); - } - cx.stop_propagation(); - }, - )) - .on_drop(cx.listener( - move |this, selections: &DraggedSelection, window, cx| { - this.drag_target_entry = None; - this.hover_scroll_task.take(); - if let Some(entry_id) = this.state.last_worktree_root_id { - this.drag_onto(selections, entry_id, false, window, cx); - } - cx.stop_propagation(); - }, - )) - .on_click(cx.listener(|this, event, window, cx| { - if matches!(event, gpui::ClickEvent::Keyboard(_)) { - return; - } - cx.stop_propagation(); - this.state.selection = None; - this.marked_entries.clear(); - this.focus_handle(cx).focus(window); - })) - .on_mouse_down( - MouseButton::Right, - cx.listener(move |this, event: &MouseDownEvent, window, cx| { - // When deploying the context menu anywhere below the last project entry, - // act as if the user clicked the root of the last worktree. - if let Some(entry_id) = this.state.last_worktree_root_id { - this.deploy_context_menu( - event.position, - entry_id, - window, - cx, - ); - } - }), - ) - .when(!project.is_read_only(cx), |el| { - el.on_click(cx.listener( - |this, event: &gpui::ClickEvent, window, cx| { - if event.click_count() > 1 - && let Some(entry_id) = - this.state.last_worktree_root_id - { - let project = this.project.read(cx); - - let worktree_id = if let Some(worktree) = - project.worktree_for_entry(entry_id, cx) - { - worktree.read(cx).id() - } else { - return; - }; - - this.state.selection = Some(SelectedEntry { - worktree_id, - entry_id, - }); - - this.new_file(&NewFile, window, cx); - } - }, - )) - }), - ) - .size_full(), - ) - .custom_scrollbars( - Scrollbars::for_settings::() - .tracked_scroll_handle(&self.scroll_handle) - .with_track_along( - ScrollAxes::Horizontal, - cx.theme().colors().panel_background, - ) - .notify_content(), - window, - cx, - ) - .children(self.context_menu.as_ref().map(|(menu, position, _)| { - deferred( - anchored() - .position(*position) - .anchor(gpui::Corner::TopLeft) - .child(menu.clone()), - ) - .with_priority(3) - })) - } else { - let focus_handle = self.focus_handle(cx); - - v_flex() - .id("empty-project_panel") - .p_4() - .size_full() - .items_center() - .justify_center() - .gap_1() - .track_focus(&self.focus_handle(cx)) - .child( - Button::new("open_project", "Open Project") - .full_width() - .key_binding(KeyBinding::for_action_in( - &workspace::Open, - &focus_handle, - cx, - )) - .on_click(cx.listener(|this, _, window, cx| { - this.workspace - .update(cx, |_, cx| { - window.dispatch_action(workspace::Open.boxed_clone(), cx); - }) - .log_err(); - })), - ) - .child( - h_flex() - .w_1_2() - .gap_2() - .child(Divider::horizontal()) - .child(Label::new("or").size(LabelSize::XSmall).color(Color::Muted)) - .child(Divider::horizontal()), - ) - .child( - Button::new("clone_repo", "Clone Repository") - .full_width() - .on_click(cx.listener(|this, _, window, cx| { - this.workspace - .update(cx, |_, cx| { - window.dispatch_action(git::Clone.boxed_clone(), cx); - }) - .log_err(); - })), - ) - .when(is_local, |div| { - div.when(panel_settings.drag_and_drop, |div| { - div.drag_over::(|style, _, _, cx| { - style.bg(cx.theme().colors().drop_target_background) - }) - .on_drop(cx.listener( - move |this, external_paths: &ExternalPaths, window, cx| { - this.drag_target_entry = None; - this.hover_scroll_task.take(); - if let Some(task) = this - .workspace - .update(cx, |workspace, cx| { - workspace.open_workspace_for_paths( - true, - external_paths.paths().to_owned(), - window, - cx, - ) - }) - .log_err() - { - task.detach_and_log_err(cx); - } - cx.stop_propagation(); - }, - )) - }) - }) - } - } -} - -impl Render for DraggedProjectEntryView { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let ui_font = ThemeSettings::get_global(cx).ui_font.clone(); - h_flex() - .font(ui_font) - .pl(self.click_offset.x + px(12.)) - .pt(self.click_offset.y + px(12.)) - .child( - div() - .flex() - .gap_1() - .items_center() - .py_1() - .px_2() - .rounded_lg() - .bg(cx.theme().colors().background) - .map(|this| { - if self.selections.len() > 1 && self.selections.contains(&self.selection) { - this.child(Label::new(format!("{} entries", self.selections.len()))) - } else { - this.child(if let Some(icon) = &self.icon { - div().child(Icon::from_path(icon.clone())) - } else { - div() - }) - .child(Label::new(self.filename.clone())) - } - }), - ) - } -} - -impl EventEmitter for ProjectPanel {} - -impl EventEmitter for ProjectPanel {} - -impl Panel for ProjectPanel { - fn position(&self, _: &Window, cx: &App) -> DockPosition { - match ProjectPanelSettings::get_global(cx).dock { - DockSide::Left => DockPosition::Left, - DockSide::Right => DockPosition::Right, - } - } - - fn position_is_valid(&self, position: DockPosition) -> bool { - matches!(position, DockPosition::Left | DockPosition::Right) - } - - fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { - settings::update_settings_file(self.fs.clone(), cx, move |settings, _| { - let dock = match position { - DockPosition::Left | DockPosition::Bottom => DockSide::Left, - DockPosition::Right => DockSide::Right, - }; - settings.project_panel.get_or_insert_default().dock = Some(dock); - }); - } - - fn size(&self, _: &Window, cx: &App) -> Pixels { - self.width - .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width) - } - - fn set_size(&mut self, size: Option, window: &mut Window, cx: &mut Context) { - self.width = size; - cx.notify(); - cx.defer_in(window, |this, _, cx| { - this.serialize(cx); - }); - } - - fn icon(&self, _: &Window, cx: &App) -> Option { - ProjectPanelSettings::get_global(cx) - .button - .then_some(IconName::FileTree) - } - - fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> { - Some("Project Panel") - } - - fn toggle_action(&self) -> Box { - Box::new(ToggleFocus) - } - - fn persistent_name() -> &'static str { - "Project Panel" - } - - fn panel_key() -> &'static str { - PROJECT_PANEL_KEY - } - - fn starts_open(&self, _: &Window, cx: &App) -> bool { - if !ProjectPanelSettings::get_global(cx).starts_open { - return false; - } - - let project = &self.project.read(cx); - project.visible_worktrees(cx).any(|tree| { - tree.read(cx) - .root_entry() - .is_some_and(|entry| entry.is_dir()) - }) - } - - fn activation_priority(&self) -> u32 { - 0 - } -} - -impl Focusable for ProjectPanel { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl ClipboardEntry { - fn is_cut(&self) -> bool { - matches!(self, Self::Cut { .. }) - } - - fn items(&self) -> &BTreeSet { - match self { - ClipboardEntry::Copied(entries) | ClipboardEntry::Cut(entries) => entries, - } - } - - fn into_copy_entry(self) -> Self { - match self { - ClipboardEntry::Copied(_) => self, - ClipboardEntry::Cut(entries) => ClipboardEntry::Copied(entries), - } - } -} - -#[inline] -fn cmp_directories_first(a: &Entry, b: &Entry) -> cmp::Ordering { - util::paths::compare_rel_paths((&a.path, a.is_file()), (&b.path, b.is_file())) -} - -#[inline] -fn cmp_mixed(a: &Entry, b: &Entry) -> cmp::Ordering { - util::paths::compare_rel_paths_mixed((&a.path, a.is_file()), (&b.path, b.is_file())) -} - -#[inline] -fn cmp_files_first(a: &Entry, b: &Entry) -> cmp::Ordering { - util::paths::compare_rel_paths_files_first((&a.path, a.is_file()), (&b.path, b.is_file())) -} - -#[inline] -fn cmp_with_mode(a: &Entry, b: &Entry, mode: &settings::ProjectPanelSortMode) -> cmp::Ordering { - match mode { - settings::ProjectPanelSortMode::DirectoriesFirst => cmp_directories_first(a, b), - settings::ProjectPanelSortMode::Mixed => cmp_mixed(a, b), - settings::ProjectPanelSortMode::FilesFirst => cmp_files_first(a, b), - } -} - -pub fn sort_worktree_entries_with_mode( - entries: &mut [impl AsRef], - mode: settings::ProjectPanelSortMode, -) { - entries.sort_by(|lhs, rhs| cmp_with_mode(lhs.as_ref(), rhs.as_ref(), &mode)); -} - -pub fn par_sort_worktree_entries_with_mode( - entries: &mut Vec, - mode: settings::ProjectPanelSortMode, -) { - entries.par_sort_by(|lhs, rhs| cmp_with_mode(lhs, rhs, &mode)); -} - -#[cfg(test)] -mod project_panel_tests; diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs deleted file mode 100644 index b031627034..0000000000 --- a/crates/project_panel/src/project_panel_settings.rs +++ /dev/null @@ -1,124 +0,0 @@ -use editor::EditorSettings; -use gpui::Pixels; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{ - DockSide, ProjectPanelEntrySpacing, ProjectPanelSortMode, RegisterSetting, Settings, - ShowDiagnostics, ShowIndentGuides, -}; -use ui::{ - px, - scrollbars::{ScrollbarVisibility, ShowScrollbar}, -}; - -#[derive(Deserialize, Debug, Clone, Copy, PartialEq, RegisterSetting)] -pub struct ProjectPanelSettings { - pub button: bool, - pub hide_gitignore: bool, - pub default_width: Pixels, - pub dock: DockSide, - pub entry_spacing: ProjectPanelEntrySpacing, - pub file_icons: bool, - pub folder_icons: bool, - pub git_status: bool, - pub indent_size: f32, - pub indent_guides: IndentGuidesSettings, - pub sticky_scroll: bool, - pub auto_reveal_entries: bool, - pub auto_fold_dirs: bool, - pub starts_open: bool, - pub scrollbar: ScrollbarSettings, - pub show_diagnostics: ShowDiagnostics, - pub hide_root: bool, - pub hide_hidden: bool, - pub drag_and_drop: bool, - pub auto_open: AutoOpenSettings, - pub sort_mode: ProjectPanelSortMode, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct IndentGuidesSettings { - pub show: ShowIndentGuides, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct ScrollbarSettings { - /// When to show the scrollbar in the project panel. - /// - /// Default: inherits editor scrollbar settings - pub show: Option, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct AutoOpenSettings { - pub on_create: bool, - pub on_paste: bool, - pub on_drop: bool, -} - -impl AutoOpenSettings { - #[inline] - pub fn should_open_on_create(self) -> bool { - self.on_create - } - - #[inline] - pub fn should_open_on_paste(self) -> bool { - self.on_paste - } - - #[inline] - pub fn should_open_on_drop(self) -> bool { - self.on_drop - } -} - -impl ScrollbarVisibility for ProjectPanelSettings { - fn visibility(&self, cx: &ui::App) -> ShowScrollbar { - self.scrollbar - .show - .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show) - } -} - -impl Settings for ProjectPanelSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let project_panel = content.project_panel.clone().unwrap(); - Self { - button: project_panel.button.unwrap(), - hide_gitignore: project_panel.hide_gitignore.unwrap(), - default_width: px(project_panel.default_width.unwrap()), - dock: project_panel.dock.unwrap(), - entry_spacing: project_panel.entry_spacing.unwrap(), - file_icons: project_panel.file_icons.unwrap(), - folder_icons: project_panel.folder_icons.unwrap(), - git_status: project_panel.git_status.unwrap(), - indent_size: project_panel.indent_size.unwrap(), - indent_guides: IndentGuidesSettings { - show: project_panel.indent_guides.unwrap().show.unwrap(), - }, - sticky_scroll: project_panel.sticky_scroll.unwrap(), - auto_reveal_entries: project_panel.auto_reveal_entries.unwrap(), - auto_fold_dirs: project_panel.auto_fold_dirs.unwrap(), - starts_open: project_panel.starts_open.unwrap(), - scrollbar: ScrollbarSettings { - show: project_panel.scrollbar.unwrap().show.map(Into::into), - }, - show_diagnostics: project_panel.show_diagnostics.unwrap(), - hide_root: project_panel.hide_root.unwrap(), - hide_hidden: project_panel.hide_hidden.unwrap(), - drag_and_drop: project_panel.drag_and_drop.unwrap(), - auto_open: { - let auto_open = project_panel.auto_open.unwrap(); - AutoOpenSettings { - on_create: auto_open.on_create.unwrap(), - on_paste: auto_open.on_paste.unwrap(), - on_drop: auto_open.on_drop.unwrap(), - } - }, - sort_mode: project_panel - .sort_mode - .unwrap_or(ProjectPanelSortMode::DirectoriesFirst), - } - } -} diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs deleted file mode 100644 index 3f54e01927..0000000000 --- a/crates/project_panel/src/project_panel_tests.rs +++ /dev/null @@ -1,8195 +0,0 @@ -use super::*; -use collections::HashSet; -use editor::MultiBufferOffset; -use gpui::{Empty, Entity, TestAppContext, VisualTestContext, WindowHandle}; -use pretty_assertions::assert_eq; -use project::FakeFs; -use serde_json::json; -use settings::{ProjectPanelAutoOpenSettings, SettingsStore}; -use std::path::{Path, PathBuf}; -use util::{path, paths::PathStyle, rel_path::rel_path}; -use workspace::{ - AppState, ItemHandle, Pane, - item::{Item, ProjectItem}, - register_project_item, -}; - -#[gpui::test] -async fn test_visible_list(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - ".dockerignore": "", - ".git": { - "HEAD": "", - }, - "a": { - "0": { "q": "", "r": "", "s": "" }, - "1": { "t": "", "u": "" }, - "2": { "v": "", "w": "", "x": "", "y": "" }, - }, - "b": { - "3": { "Q": "" }, - "4": { "R": "", "S": "", "T": "", "U": "" }, - }, - "C": { - "5": {}, - "6": { "V": "", "W": "" }, - "7": { "X": "" }, - "8": { "Y": {}, "Z": "" } - } - }), - ) - .await; - fs.insert_tree( - "/root2", - json!({ - "d": { - "9": "" - }, - "e": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - toggle_expand_dir(&panel, "root1/b", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root1", - " > .git", - " > a", - " v b <== selected", - " > 3", - " > 4", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - assert_eq!( - visible_entries_as_strings(&panel, 6..9, cx), - &[ - // - " > C", - " .dockerignore", - "v root2", - ] - ); -} - -#[gpui::test] -async fn test_opening_file(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/src"), - json!({ - "test": { - "first.rs": "// First Rust file", - "second.rs": "// Second Rust file", - "third.rs": "// Third Rust file", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "src/test", cx); - select_path(&panel, "src/test/first.rs", cx); - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.rs <== selected <== marked", - " second.rs", - " third.rs" - ] - ); - ensure_single_file_is_opened(&workspace, "test/first.rs", cx); - - select_path(&panel, "src/test/second.rs", cx); - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.rs", - " second.rs <== selected <== marked", - " third.rs" - ] - ); - ensure_single_file_is_opened(&workspace, "test/second.rs", cx); -} - -#[gpui::test] -async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) { - init_test(cx); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["**/.git".to_string(), "**/4/**".to_string()]); - }); - }); - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/root1", - json!({ - ".dockerignore": "", - ".git": { - "HEAD": "", - }, - "a": { - "0": { "q": "", "r": "", "s": "" }, - "1": { "t": "", "u": "" }, - "2": { "v": "", "w": "", "x": "", "y": "" }, - }, - "b": { - "3": { "Q": "" }, - "4": { "R": "", "S": "", "T": "", "U": "" }, - }, - "C": { - "5": {}, - "6": { "V": "", "W": "" }, - "7": { "X": "" }, - "8": { "Y": {}, "Z": "" } - } - }), - ) - .await; - fs.insert_tree( - "/root2", - json!({ - "d": { - "4": "" - }, - "e": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root1", - " > a", - " > b", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - toggle_expand_dir(&panel, "root1/b", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root1", - " > a", - " v b <== selected", - " > 3", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - toggle_expand_dir(&panel, "root2/d", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root1", - " > a", - " v b", - " > 3", - " > C", - " .dockerignore", - "v root2", - " v d <== selected", - " > e", - ] - ); - - toggle_expand_dir(&panel, "root2/e", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root1", - " > a", - " v b", - " > 3", - " > C", - " .dockerignore", - "v root2", - " v d", - " v e <== selected", - ] - ); -} - -#[gpui::test] -async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root1"), - json!({ - "dir_1": { - "nested_dir_1": { - "nested_dir_2": { - "nested_dir_3": { - "file_a.java": "// File contents", - "file_b.java": "// File contents", - "file_c.java": "// File contents", - "nested_dir_4": { - "nested_dir_5": { - "file_d.java": "// File contents", - } - } - } - } - } - } - }), - ) - .await; - fs.insert_tree( - path!("/root2"), - json!({ - "dir_2": { - "file_1.java": "// File contents", - } - }), - ) - .await; - - // Test 1: Multiple worktrees with auto_fold_dirs = true - let project = Project::test( - fs.clone(), - [path!("/root1").as_ref(), path!("/root2").as_ref()], - cx, - ) - .await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - auto_fold_dirs: true, - sort_mode: settings::ProjectPanelSortMode::DirectoriesFirst, - ..settings - }, - cx, - ); - }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > dir_1/nested_dir_1/nested_dir_2/nested_dir_3", - "v root2", - " > dir_2", - ] - ); - - toggle_expand_dir( - &panel, - "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3", - cx, - ); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " v dir_1/nested_dir_1/nested_dir_2/nested_dir_3 <== selected", - " > nested_dir_4/nested_dir_5", - " file_a.java", - " file_b.java", - " file_c.java", - "v root2", - " > dir_2", - ] - ); - - toggle_expand_dir( - &panel, - "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5", - cx, - ); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " v dir_1/nested_dir_1/nested_dir_2/nested_dir_3", - " v nested_dir_4/nested_dir_5 <== selected", - " file_d.java", - " file_a.java", - " file_b.java", - " file_c.java", - "v root2", - " > dir_2", - ] - ); - toggle_expand_dir(&panel, "root2/dir_2", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " v dir_1/nested_dir_1/nested_dir_2/nested_dir_3", - " v nested_dir_4/nested_dir_5", - " file_d.java", - " file_a.java", - " file_b.java", - " file_c.java", - "v root2", - " v dir_2 <== selected", - " file_1.java", - ] - ); - - // Test 2: Single worktree with auto_fold_dirs = true and hide_root = true - { - let project = Project::test(fs.clone(), [path!("/root1").as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - auto_fold_dirs: true, - hide_root: true, - ..settings - }, - cx, - ); - }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["> dir_1/nested_dir_1/nested_dir_2/nested_dir_3"], - "Single worktree with hide_root=true should hide root and show auto-folded paths" - ); - - toggle_expand_dir( - &panel, - "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3", - cx, - ); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v dir_1/nested_dir_1/nested_dir_2/nested_dir_3 <== selected", - " > nested_dir_4/nested_dir_5", - " file_a.java", - " file_b.java", - " file_c.java", - ], - "Expanded auto-folded path with hidden root should show contents without root prefix" - ); - - toggle_expand_dir( - &panel, - "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5", - cx, - ); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v dir_1/nested_dir_1/nested_dir_2/nested_dir_3", - " v nested_dir_4/nested_dir_5 <== selected", - " file_d.java", - " file_a.java", - " file_b.java", - " file_c.java", - ], - "Nested expansion with hidden root should maintain proper indentation" - ); - } -} - -#[gpui::test(iterations = 30)] -async fn test_editing_files(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - ".dockerignore": "", - ".git": { - "HEAD": "", - }, - "a": { - "0": { "q": "", "r": "", "s": "" }, - "1": { "t": "", "u": "" }, - "2": { "v": "", "w": "", "x": "", "y": "" }, - }, - "b": { - "3": { "Q": "" }, - "4": { "R": "", "S": "", "T": "", "U": "" }, - }, - "C": { - "5": {}, - "6": { "V": "", "W": "" }, - "7": { "X": "" }, - "8": { "Y": {}, "Z": "" } - } - }), - ) - .await; - fs.insert_tree( - "/root2", - json!({ - "d": { - "9": "" - }, - "e": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - select_path(&panel, "root1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1 <== selected", - " > .git", - " > a", - " > b", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - // Add a file with the root folder selected. The filename editor is placed - // before the first file in the root folder. - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " > C", - " [EDITOR: ''] <== selected", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text("the-new-filename", window, cx) - }); - panel.confirm_edit(true, window, cx).unwrap() - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " > C", - " [PROCESSING: 'the-new-filename'] <== selected", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - confirm.await.unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " > C", - " .dockerignore", - " the-new-filename <== selected <== marked", - "v root2", - " > d", - " > e", - ] - ); - - select_path(&panel, "root1/b", cx); - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3", - " > 4", - " [EDITOR: ''] <== selected", - " > C", - " .dockerignore", - " the-new-filename", - ] - ); - - panel - .update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text("another-filename.txt", window, cx) - }); - panel.confirm_edit(true, window, cx).unwrap() - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3", - " > 4", - " another-filename.txt <== selected <== marked", - " > C", - " .dockerignore", - " the-new-filename", - ] - ); - - select_path(&panel, "root1/b/another-filename.txt", cx); - panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3", - " > 4", - " [EDITOR: 'another-filename.txt'] <== selected <== marked", - " > C", - " .dockerignore", - " the-new-filename", - ] - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - let file_name_selections = editor - .selections - .all::(&editor.display_snapshot(cx)); - assert_eq!( - file_name_selections.len(), - 1, - "File editing should have a single selection, but got: {file_name_selections:?}" - ); - let file_name_selection = &file_name_selections[0]; - assert_eq!( - file_name_selection.start, - MultiBufferOffset(0), - "Should select the file name from the start" - ); - assert_eq!( - file_name_selection.end, - MultiBufferOffset("another-filename".len()), - "Should not select file extension" - ); - - editor.set_text("a-different-filename.tar.gz", window, cx) - }); - panel.confirm_edit(true, window, cx).unwrap() - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3", - " > 4", - " [PROCESSING: 'a-different-filename.tar.gz'] <== selected <== marked", - " > C", - " .dockerignore", - " the-new-filename", - ] - ); - - confirm.await.unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3", - " > 4", - " a-different-filename.tar.gz <== selected", - " > C", - " .dockerignore", - " the-new-filename", - ] - ); - - panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3", - " > 4", - " [EDITOR: 'a-different-filename.tar.gz'] <== selected", - " > C", - " .dockerignore", - " the-new-filename", - ] - ); - - panel.update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - let file_name_selections = editor.selections.all::(&editor.display_snapshot(cx)); - assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); - let file_name_selection = &file_name_selections[0]; - assert_eq!(file_name_selection.start, MultiBufferOffset(0), "Should select the file name from the start"); - assert_eq!(file_name_selection.end, MultiBufferOffset("a-different-filename.tar".len()), "Should not select file extension, but still may select anything up to the last dot.."); - - }); - panel.cancel(&menu::Cancel, window, cx) - }); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - panel.new_directory(&NewDirectory, window, cx) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > [EDITOR: ''] <== selected", - " > 3", - " > 4", - " a-different-filename.tar.gz", - " > C", - " .dockerignore", - ] - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("new-dir", window, cx)); - panel.confirm_edit(true, window, cx).unwrap() - }); - panel.update_in(cx, |panel, window, cx| { - panel.select_next(&Default::default(), window, cx) - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > [PROCESSING: 'new-dir']", - " > 3 <== selected", - " > 4", - " a-different-filename.tar.gz", - " > C", - " .dockerignore", - ] - ); - - confirm.await.unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3 <== selected", - " > 4", - " > new-dir", - " a-different-filename.tar.gz", - " > C", - " .dockerignore", - ] - ); - - panel.update_in(cx, |panel, window, cx| { - panel.rename(&Default::default(), window, cx) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > [EDITOR: '3'] <== selected", - " > 4", - " > new-dir", - " a-different-filename.tar.gz", - " > C", - " .dockerignore", - ] - ); - - // Dismiss the rename editor when it loses focus. - workspace.update(cx, |_, window, _| window.blur()).unwrap(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3 <== selected", - " > 4", - " > new-dir", - " a-different-filename.tar.gz", - " > C", - " .dockerignore", - ] - ); - - // Test empty filename and filename with only whitespace - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " v 3", - " [EDITOR: ''] <== selected", - " Q", - " > 4", - " > new-dir", - " a-different-filename.tar.gz", - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text("", window, cx); - }); - assert!(panel.confirm_edit(true, window, cx).is_none()); - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text(" ", window, cx); - }); - assert!(panel.confirm_edit(true, window, cx).is_none()); - panel.cancel(&menu::Cancel, window, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " v 3 <== selected", - " Q", - " > 4", - " > new-dir", - " a-different-filename.tar.gz", - " > C", - ] - ); -} - -#[gpui::test(iterations = 10)] -async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - ".dockerignore": "", - ".git": { - "HEAD": "", - }, - "a": { - "0": { "q": "", "r": "", "s": "" }, - "1": { "t": "", "u": "" }, - "2": { "v": "", "w": "", "x": "", "y": "" }, - }, - "b": { - "3": { "Q": "" }, - "4": { "R": "", "S": "", "T": "", "U": "" }, - }, - "C": { - "5": {}, - "6": { "V": "", "W": "" }, - "7": { "X": "" }, - "8": { "Y": {}, "Z": "" } - } - }), - ) - .await; - fs.insert_tree( - "/root2", - json!({ - "d": { - "9": "" - }, - "e": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - select_path(&panel, "root1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1 <== selected", - " > .git", - " > a", - " > b", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - // Add a file with the root folder selected. The filename editor is placed - // before the first file in the root folder. - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " > C", - " [EDITOR: ''] <== selected", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text("/bdir1/dir2/the-new-filename", window, cx) - }); - panel.confirm_edit(true, window, cx).unwrap() - }); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " > C", - " [PROCESSING: 'bdir1/dir2/the-new-filename'] <== selected", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - confirm.await.unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..13, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " v bdir1", - " v dir2", - " the-new-filename <== selected <== marked", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); -} - -#[gpui::test] -async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root1"), - json!({ - ".dockerignore": "", - ".git": { - "HEAD": "", - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root1").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - select_path(&panel, "root1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v root1 <== selected", " > .git", " .dockerignore",] - ); - - // Add a file with the root folder selected. The filename editor is placed - // before the first file in the root folder. - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " [EDITOR: ''] <== selected", - " .dockerignore", - ] - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - // If we want to create a subdirectory, there should be no prefix slash. - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("new_dir/", window, cx)); - panel.confirm_edit(true, window, cx).unwrap() - }); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " [PROCESSING: 'new_dir'] <== selected", - " .dockerignore", - ] - ); - - confirm.await.unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " v new_dir <== selected", - " .dockerignore", - ] - ); - - // Test filename with whitespace - select_path(&panel, "root1", cx); - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - let confirm = panel.update_in(cx, |panel, window, cx| { - // If we want to create a subdirectory, there should be no prefix slash. - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("new dir 2/", window, cx)); - panel.confirm_edit(true, window, cx).unwrap() - }); - confirm.await.unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " v new dir 2 <== selected", - " v new_dir", - " .dockerignore", - ] - ); - - // Test filename ends with "\" - #[cfg(target_os = "windows")] - { - select_path(&panel, "root1", cx); - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - let confirm = panel.update_in(cx, |panel, window, cx| { - // If we want to create a subdirectory, there should be no prefix slash. - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("new_dir_3\\", window, cx)); - panel.confirm_edit(true, window, cx).unwrap() - }); - confirm.await.unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " v new dir 2", - " v new_dir", - " v new_dir_3 <== selected", - " .dockerignore", - ] - ); - } -} - -#[gpui::test] -async fn test_copy_paste(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "one.two.txt": "", - "one.txt": "" - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update_in(cx, |panel, window, cx| { - panel.select_next(&Default::default(), window, cx); - panel.select_next(&Default::default(), window, cx); - }); - - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root1", - " one.txt <== selected", - " one.two.txt", - ] - ); - - // Regression test - file name is created correctly when - // the copied file's name contains multiple dots. - panel.update_in(cx, |panel, window, cx| { - panel.copy(&Default::default(), window, cx); - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root1", - " one.txt", - " [EDITOR: 'one copy.txt'] <== selected <== marked", - " one.two.txt", - ] - ); - - panel.update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - let file_name_selections = editor - .selections - .all::(&editor.display_snapshot(cx)); - assert_eq!( - file_name_selections.len(), - 1, - "File editing should have a single selection, but got: {file_name_selections:?}" - ); - let file_name_selection = &file_name_selections[0]; - assert_eq!( - file_name_selection.start, - MultiBufferOffset("one".len()), - "Should select the file name disambiguation after the original file name" - ); - assert_eq!( - file_name_selection.end, - MultiBufferOffset("one copy".len()), - "Should select the file name disambiguation until the extension" - ); - }); - assert!(panel.confirm_edit(true, window, cx).is_none()); - }); - - panel.update_in(cx, |panel, window, cx| { - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root1", - " one.txt", - " one copy.txt", - " [EDITOR: 'one copy 1.txt'] <== selected <== marked", - " one.two.txt", - ] - ); - - panel.update_in(cx, |panel, window, cx| { - assert!(panel.confirm_edit(true, window, cx).is_none()) - }); -} - -#[gpui::test] -async fn test_cut_paste(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "one.txt": "", - "two.txt": "", - "a": {}, - "b": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - select_path_with_mark(&panel, "root/one.txt", cx); - select_path_with_mark(&panel, "root/two.txt", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root", - " > a", - " > b", - " one.txt <== marked", - " two.txt <== selected <== marked", - ] - ); - - panel.update_in(cx, |panel, window, cx| { - panel.cut(&Default::default(), window, cx); - }); - - select_path(&panel, "root/a", cx); - - panel.update_in(cx, |panel, window, cx| { - panel.paste(&Default::default(), window, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root", - " v a", - " one.txt <== marked", - " two.txt <== selected <== marked", - " > b", - ], - "Cut entries should be moved on first paste." - ); - - panel.update_in(cx, |panel, window, cx| { - panel.cancel(&menu::Cancel {}, window, cx) - }); - cx.executor().run_until_parked(); - - select_path(&panel, "root/b", cx); - - panel.update_in(cx, |panel, window, cx| { - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root", - " v a", - " one.txt", - " two.txt", - " v b", - " one.txt", - " two.txt <== selected", - ], - "Cut entries should only be copied for the second paste!" - ); -} - -#[gpui::test] -async fn test_cut_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "one.txt": "", - "two.txt": "", - "three.txt": "", - "a": { - "0": { "q": "", "r": "", "s": "" }, - "1": { "t": "", "u": "" }, - "2": { "v": "", "w": "", "x": "", "y": "" }, - }, - }), - ) - .await; - - fs.insert_tree( - "/root2", - json!({ - "one.txt": "", - "two.txt": "", - "four.txt": "", - "b": { - "3": { "Q": "" }, - "4": { "R": "", "S": "", "T": "", "U": "" }, - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - select_path(&panel, "root1/three.txt", cx); - panel.update_in(cx, |panel, window, cx| { - panel.cut(&Default::default(), window, cx); - }); - - select_path(&panel, "root2/one.txt", cx); - panel.update_in(cx, |panel, window, cx| { - panel.select_next(&Default::default(), window, cx); - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root1", - " > a", - " one.txt", - " two.txt", - "v root2", - " > b", - " four.txt", - " one.txt", - " three.txt <== selected <== marked", - " two.txt", - ] - ); - - select_path(&panel, "root1/a", cx); - panel.update_in(cx, |panel, window, cx| { - panel.cut(&Default::default(), window, cx); - }); - select_path(&panel, "root2/two.txt", cx); - panel.update_in(cx, |panel, window, cx| { - panel.select_next(&Default::default(), window, cx); - panel.paste(&Default::default(), window, cx); - }); - - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root1", - " one.txt", - " two.txt", - "v root2", - " > a <== selected", - " > b", - " four.txt", - " one.txt", - " three.txt <== marked", - " two.txt", - ] - ); -} - -#[gpui::test] -async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "one.txt": "", - "two.txt": "", - "three.txt": "", - "a": { - "0": { "q": "", "r": "", "s": "" }, - "1": { "t": "", "u": "" }, - "2": { "v": "", "w": "", "x": "", "y": "" }, - }, - }), - ) - .await; - - fs.insert_tree( - "/root2", - json!({ - "one.txt": "", - "two.txt": "", - "four.txt": "", - "b": { - "3": { "Q": "" }, - "4": { "R": "", "S": "", "T": "", "U": "" }, - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - select_path(&panel, "root1/three.txt", cx); - panel.update_in(cx, |panel, window, cx| { - panel.copy(&Default::default(), window, cx); - }); - - select_path(&panel, "root2/one.txt", cx); - panel.update_in(cx, |panel, window, cx| { - panel.select_next(&Default::default(), window, cx); - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root1", - " > a", - " one.txt", - " three.txt", - " two.txt", - "v root2", - " > b", - " four.txt", - " one.txt", - " three.txt <== selected <== marked", - " two.txt", - ] - ); - - select_path(&panel, "root1/three.txt", cx); - panel.update_in(cx, |panel, window, cx| { - panel.copy(&Default::default(), window, cx); - }); - select_path(&panel, "root2/two.txt", cx); - panel.update_in(cx, |panel, window, cx| { - panel.select_next(&Default::default(), window, cx); - panel.paste(&Default::default(), window, cx); - }); - - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root1", - " > a", - " one.txt", - " three.txt", - " two.txt", - "v root2", - " > b", - " four.txt", - " one.txt", - " three.txt", - " [EDITOR: 'three copy.txt'] <== selected <== marked", - " two.txt", - ] - ); - - panel.update_in(cx, |panel, window, cx| { - panel.cancel(&menu::Cancel {}, window, cx) - }); - cx.executor().run_until_parked(); - - select_path(&panel, "root1/a", cx); - panel.update_in(cx, |panel, window, cx| { - panel.copy(&Default::default(), window, cx); - }); - select_path(&panel, "root2/two.txt", cx); - panel.update_in(cx, |panel, window, cx| { - panel.select_next(&Default::default(), window, cx); - panel.paste(&Default::default(), window, cx); - }); - - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root1", - " > a", - " one.txt", - " three.txt", - " two.txt", - "v root2", - " > a <== selected", - " > b", - " four.txt", - " one.txt", - " three.txt", - " three copy.txt", - " two.txt", - ] - ); -} - -#[gpui::test] -async fn test_copy_paste_directory(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "a": { - "one.txt": "", - "two.txt": "", - "inner_dir": { - "three.txt": "", - "four.txt": "", - } - }, - "b": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - select_path(&panel, "root/a", cx); - panel.update_in(cx, |panel, window, cx| { - panel.copy(&Default::default(), window, cx); - panel.select_next(&Default::default(), window, cx); - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - - let pasted_dir = find_project_entry(&panel, "root/b/a", cx); - assert_ne!(pasted_dir, None, "Pasted directory should have an entry"); - - let pasted_dir_file = find_project_entry(&panel, "root/b/a/one.txt", cx); - assert_ne!( - pasted_dir_file, None, - "Pasted directory file should have an entry" - ); - - let pasted_dir_inner_dir = find_project_entry(&panel, "root/b/a/inner_dir", cx); - assert_ne!( - pasted_dir_inner_dir, None, - "Directories inside pasted directory should have an entry" - ); - - toggle_expand_dir(&panel, "root/b/a", cx); - toggle_expand_dir(&panel, "root/b/a/inner_dir", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root", - " > a", - " v b", - " v a", - " v inner_dir <== selected", - " four.txt", - " three.txt", - " one.txt", - " two.txt", - ] - ); - - select_path(&panel, "root", cx); - panel.update_in(cx, |panel, window, cx| { - panel.paste(&Default::default(), window, cx) - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root", - " > a", - " > [EDITOR: 'a copy'] <== selected", - " v b", - " v a", - " v inner_dir", - " four.txt", - " three.txt", - " one.txt", - " two.txt" - ] - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("c", window, cx)); - panel.confirm_edit(true, window, cx).unwrap() - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root", - " > a", - " > [PROCESSING: 'c'] <== selected", - " v b", - " v a", - " v inner_dir", - " four.txt", - " three.txt", - " one.txt", - " two.txt" - ] - ); - - confirm.await.unwrap(); - - panel.update_in(cx, |panel, window, cx| { - panel.paste(&Default::default(), window, cx) - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root", - " > a", - " v b", - " v a", - " v inner_dir", - " four.txt", - " three.txt", - " one.txt", - " two.txt", - " v c", - " > a <== selected", - " > inner_dir", - " one.txt", - " two.txt", - ] - ); -} - -#[gpui::test] -async fn test_copy_paste_directory_with_sibling_file(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/test", - json!({ - "dir1": { - "a.txt": "", - "b.txt": "", - }, - "dir2": {}, - "c.txt": "", - "d.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "test/dir1", cx); - - cx.simulate_modifiers_change(gpui::Modifiers { - control: true, - ..Default::default() - }); - - select_path_with_mark(&panel, "test/dir1", cx); - select_path_with_mark(&panel, "test/c.txt", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v test", - " v dir1 <== marked", - " a.txt", - " b.txt", - " > dir2", - " c.txt <== selected <== marked", - " d.txt", - ], - "Initial state before copying dir1 and c.txt" - ); - - panel.update_in(cx, |panel, window, cx| { - panel.copy(&Default::default(), window, cx); - }); - select_path(&panel, "test/dir2", cx); - panel.update_in(cx, |panel, window, cx| { - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - - toggle_expand_dir(&panel, "test/dir2/dir1", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v test", - " v dir1 <== marked", - " a.txt", - " b.txt", - " v dir2", - " v dir1 <== selected", - " a.txt", - " b.txt", - " c.txt", - " c.txt <== marked", - " d.txt", - ], - "Should copy dir1 as well as c.txt into dir2" - ); - - // Disambiguating multiple files should not open the rename editor. - select_path(&panel, "test/dir2", cx); - panel.update_in(cx, |panel, window, cx| { - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v test", - " v dir1 <== marked", - " a.txt", - " b.txt", - " v dir2", - " v dir1", - " a.txt", - " b.txt", - " > dir1 copy <== selected", - " c.txt", - " c copy.txt", - " c.txt <== marked", - " d.txt", - ], - "Should copy dir1 as well as c.txt into dir2 and disambiguate them without opening the rename editor" - ); -} - -#[gpui::test] -async fn test_copy_paste_nested_and_root_entries(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/test", - json!({ - "dir1": { - "a.txt": "", - "b.txt": "", - }, - "dir2": {}, - "c.txt": "", - "d.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "test/dir1", cx); - - cx.simulate_modifiers_change(gpui::Modifiers { - control: true, - ..Default::default() - }); - - select_path_with_mark(&panel, "test/dir1/a.txt", cx); - select_path_with_mark(&panel, "test/dir1", cx); - select_path_with_mark(&panel, "test/c.txt", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v test", - " v dir1 <== marked", - " a.txt <== marked", - " b.txt", - " > dir2", - " c.txt <== selected <== marked", - " d.txt", - ], - "Initial state before copying a.txt, dir1 and c.txt" - ); - - panel.update_in(cx, |panel, window, cx| { - panel.copy(&Default::default(), window, cx); - }); - select_path(&panel, "test/dir2", cx); - panel.update_in(cx, |panel, window, cx| { - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - - toggle_expand_dir(&panel, "test/dir2/dir1", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v test", - " v dir1 <== marked", - " a.txt <== marked", - " b.txt", - " v dir2", - " v dir1 <== selected", - " a.txt", - " b.txt", - " c.txt", - " c.txt <== marked", - " d.txt", - ], - "Should copy dir1 and c.txt into dir2. a.txt is already present in copied dir1." - ); -} - -#[gpui::test] -async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/src"), - json!({ - "test": { - "first.rs": "// First Rust file", - "second.rs": "// Second Rust file", - "third.rs": "// Third Rust file", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "src/test", cx); - select_path(&panel, "src/test/first.rs", cx); - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.rs <== selected <== marked", - " second.rs", - " third.rs" - ] - ); - ensure_single_file_is_opened(&workspace, "test/first.rs", cx); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " second.rs <== selected", - " third.rs" - ], - "Project panel should have no deleted file, no other file is selected in it" - ); - ensure_no_open_items_and_panes(&workspace, cx); - - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " second.rs <== selected <== marked", - " third.rs" - ] - ); - ensure_single_file_is_opened(&workspace, "test/second.rs", cx); - - workspace - .update(cx, |workspace, window, cx| { - let active_items = workspace - .panes() - .iter() - .filter_map(|pane| pane.read(cx).active_item()) - .collect::>(); - assert_eq!(active_items.len(), 1); - let open_editor = active_items - .into_iter() - .next() - .unwrap() - .downcast::() - .expect("Open item should be an editor"); - open_editor.update(cx, |editor, cx| { - editor.set_text("Another text!", window, cx) - }); - }) - .unwrap(); - submit_deletion_skipping_prompt(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v src", " v test", " third.rs <== selected"], - "Project panel should have no deleted file, with one last file remaining" - ); - ensure_no_open_items_and_panes(&workspace, cx); -} - -#[gpui::test] -async fn test_auto_open_new_file_when_enabled(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - set_auto_open_settings( - cx, - ProjectPanelAutoOpenSettings { - on_create: Some(true), - ..Default::default() - }, - ); - - 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 workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - panel - .update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text("auto-open.rs", window, cx); - }); - panel.confirm_edit(true, window, cx).unwrap() - }) - .await - .unwrap(); - cx.run_until_parked(); - - ensure_single_file_is_opened(&workspace, "auto-open.rs", cx); -} - -#[gpui::test] -async fn test_auto_open_new_file_when_disabled(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - set_auto_open_settings( - cx, - ProjectPanelAutoOpenSettings { - on_create: Some(false), - ..Default::default() - }, - ); - - 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 workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - panel - .update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text("manual-open.rs", window, cx); - }); - panel.confirm_edit(true, window, cx).unwrap() - }) - .await - .unwrap(); - cx.run_until_parked(); - - ensure_no_open_items_and_panes(&workspace, cx); -} - -#[gpui::test] -async fn test_auto_open_on_paste_when_enabled(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - set_auto_open_settings( - cx, - ProjectPanelAutoOpenSettings { - on_paste: Some(true), - ..Default::default() - }, - ); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "src": { - "original.rs": "" - }, - "target": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/src", cx); - toggle_expand_dir(&panel, "root/target", cx); - - select_path(&panel, "root/src/original.rs", cx); - panel.update_in(cx, |panel, window, cx| { - panel.copy(&Default::default(), window, cx); - }); - - select_path(&panel, "root/target", cx); - panel.update_in(cx, |panel, window, cx| { - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - - ensure_single_file_is_opened(&workspace, "target/original.rs", cx); -} - -#[gpui::test] -async fn test_auto_open_on_paste_when_disabled(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - set_auto_open_settings( - cx, - ProjectPanelAutoOpenSettings { - on_paste: Some(false), - ..Default::default() - }, - ); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "src": { - "original.rs": "" - }, - "target": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/src", cx); - toggle_expand_dir(&panel, "root/target", cx); - - select_path(&panel, "root/src/original.rs", cx); - panel.update_in(cx, |panel, window, cx| { - panel.copy(&Default::default(), window, cx); - }); - - select_path(&panel, "root/target", cx); - panel.update_in(cx, |panel, window, cx| { - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - - ensure_no_open_items_and_panes(&workspace, cx); - assert!( - find_project_entry(&panel, "root/target/original.rs", cx).is_some(), - "Pasted entry should exist even when auto-open is disabled" - ); -} - -#[gpui::test] -async fn test_auto_open_on_drop_when_enabled(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - set_auto_open_settings( - cx, - ProjectPanelAutoOpenSettings { - on_drop: Some(true), - ..Default::default() - }, - ); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/root"), json!({})).await; - - let temp_dir = tempfile::tempdir().unwrap(); - let external_path = temp_dir.path().join("dropped.rs"); - std::fs::write(&external_path, "// dropped").unwrap(); - fs.insert_tree_from_real_fs(temp_dir.path(), temp_dir.path()) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - let root_entry = find_project_entry(&panel, "root", cx).unwrap(); - panel.update_in(cx, |panel, window, cx| { - panel.drop_external_files(std::slice::from_ref(&external_path), root_entry, window, cx); - }); - cx.executor().run_until_parked(); - - ensure_single_file_is_opened(&workspace, "dropped.rs", cx); -} - -#[gpui::test] -async fn test_auto_open_on_drop_when_disabled(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - set_auto_open_settings( - cx, - ProjectPanelAutoOpenSettings { - on_drop: Some(false), - ..Default::default() - }, - ); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/root"), json!({})).await; - - let temp_dir = tempfile::tempdir().unwrap(); - let external_path = temp_dir.path().join("manual.rs"); - std::fs::write(&external_path, "// dropped").unwrap(); - fs.insert_tree_from_real_fs(temp_dir.path(), temp_dir.path()) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - let root_entry = find_project_entry(&panel, "root", cx).unwrap(); - panel.update_in(cx, |panel, window, cx| { - panel.drop_external_files(std::slice::from_ref(&external_path), root_entry, window, cx); - }); - cx.executor().run_until_parked(); - - ensure_no_open_items_and_panes(&workspace, cx); - assert!( - find_project_entry(&panel, "root/manual.rs", cx).is_some(), - "Dropped entry should exist even when auto-open is disabled" - ); -} - -#[gpui::test] -async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/src", - json!({ - "test": { - "first.rs": "// First Rust file", - "second.rs": "// Second Rust file", - "third.rs": "// Third Rust file", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - select_path(&panel, "src", cx); - panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src <== selected", - " > test" - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel.new_directory(&NewDirectory, window, cx) - }); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src", - " > [EDITOR: ''] <== selected", - " > test" - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("test", window, cx)); - assert!( - panel.confirm_edit(true, window, cx).is_none(), - "Should not allow to confirm on conflicting new directory name" - ); - }); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!( - panel.state.edit_state.is_some(), - "Edit state should not be None after conflicting new directory name" - ); - panel.cancel(&menu::Cancel, window, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src <== selected", - " > test" - ], - "File list should be unchanged after failed folder create confirmation" - ); - - select_path(&panel, "src/test", cx); - panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src", - " > test <== selected" - ] - ); - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " [EDITOR: ''] <== selected", - " first.rs", - " second.rs", - " third.rs" - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("first.rs", window, cx)); - assert!( - panel.confirm_edit(true, window, cx).is_none(), - "Should not allow to confirm on conflicting new file name" - ); - }); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!( - panel.state.edit_state.is_some(), - "Edit state should not be None after conflicting new file name" - ); - panel.cancel(&menu::Cancel, window, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test <== selected", - " first.rs", - " second.rs", - " third.rs" - ], - "File list should be unchanged after failed file create confirmation" - ); - - select_path(&panel, "src/test/first.rs", cx); - panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.rs <== selected", - " second.rs", - " third.rs" - ], - ); - panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " [EDITOR: 'first.rs'] <== selected", - " second.rs", - " third.rs" - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("second.rs", window, cx)); - assert!( - panel.confirm_edit(true, window, cx).is_none(), - "Should not allow to confirm on conflicting file rename" - ) - }); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!( - panel.state.edit_state.is_some(), - "Edit state should not be None after conflicting file rename" - ); - panel.cancel(&menu::Cancel, window, cx); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.rs <== selected", - " second.rs", - " third.rs" - ], - "File list should be unchanged after failed rename confirmation" - ); -} - -// NOTE: This test is skipped on Windows, because on Windows, -// when it triggers the lsp store it converts `/src/test/first copy.txt` into an uri -// but it fails with message `"/src\\test\\first copy.txt" is not parseable as an URI` -#[gpui::test] -#[cfg_attr(target_os = "windows", ignore)] -async fn test_create_duplicate_items_and_check_history(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/src", - json!({ - "test": { - "first.txt": "// First Txt file", - "second.txt": "// Second Txt file", - "third.txt": "// Third Txt file", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - select_path(&panel, "src", cx); - panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src <== selected", - " > test" - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel.new_directory(&NewDirectory, window, cx) - }); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src", - " > [EDITOR: ''] <== selected", - " > test" - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("test", window, cx)); - assert!( - panel.confirm_edit(true, window, cx).is_none(), - "Should not allow to confirm on conflicting new directory name" - ); - }); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!( - panel.state.edit_state.is_some(), - "Edit state should not be None after conflicting new directory name" - ); - panel.cancel(&menu::Cancel, window, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src <== selected", - " > test" - ], - "File list should be unchanged after failed folder create confirmation" - ); - - select_path(&panel, "src/test", cx); - panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src", - " > test <== selected" - ] - ); - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " [EDITOR: ''] <== selected", - " first.txt", - " second.txt", - " third.txt" - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("first.txt", window, cx)); - assert!( - panel.confirm_edit(true, window, cx).is_none(), - "Should not allow to confirm on conflicting new file name" - ); - }); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!( - panel.state.edit_state.is_some(), - "Edit state should not be None after conflicting new file name" - ); - panel.cancel(&menu::Cancel, window, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test <== selected", - " first.txt", - " second.txt", - " third.txt" - ], - "File list should be unchanged after failed file create confirmation" - ); - - select_path(&panel, "src/test/first.txt", cx); - panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.txt <== selected", - " second.txt", - " third.txt" - ], - ); - panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " [EDITOR: 'first.txt'] <== selected", - " second.txt", - " third.txt" - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("second.txt", window, cx)); - assert!( - panel.confirm_edit(true, window, cx).is_none(), - "Should not allow to confirm on conflicting file rename" - ) - }); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!( - panel.state.edit_state.is_some(), - "Edit state should not be None after conflicting file rename" - ); - panel.cancel(&menu::Cancel, window, cx); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.txt <== selected", - " second.txt", - " third.txt" - ], - "File list should be unchanged after failed rename confirmation" - ); - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - // Try to duplicate and check history - panel.update_in(cx, |panel, window, cx| { - panel.duplicate(&Duplicate, window, cx) - }); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.txt", - " [EDITOR: 'first copy.txt'] <== selected <== marked", - " second.txt", - " third.txt" - ], - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("fourth.txt", window, cx)); - panel.confirm_edit(true, window, cx).unwrap() - }); - confirm.await.unwrap(); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.txt", - " fourth.txt <== selected", - " second.txt", - " third.txt" - ], - "File list should be different after rename confirmation" - ); - - panel.update_in(cx, |panel, window, cx| { - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.executor().run_until_parked(); - - select_path(&panel, "src/test/first.txt", cx); - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - - workspace - .read_with(cx, |this, cx| { - assert!( - this.recent_navigation_history_iter(cx) - .any(|(project_path, abs_path)| { - project_path.path == Arc::from(rel_path("test/fourth.txt")) - && abs_path == Some(PathBuf::from(path!("/src/test/fourth.txt"))) - }) - ); - }) - .unwrap(); -} - -// NOTE: This test is skipped on Windows, because on Windows, -// when it triggers the lsp store it converts `/src/test/first.txt` into an uri -// but it fails with message `"/src\\test\\first.txt" is not parseable as an URI` -#[gpui::test] -#[cfg_attr(target_os = "windows", ignore)] -async fn test_rename_item_and_check_history(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/src", - json!({ - "test": { - "first.txt": "// First Txt file", - "second.txt": "// Second Txt file", - "third.txt": "// Third Txt file", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - select_path(&panel, "src", cx); - panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src <== selected", - " > test" - ] - ); - - select_path(&panel, "src/test", cx); - panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src", - " > test <== selected" - ] - ); - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - - select_path(&panel, "src/test/first.txt", cx); - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - - panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " [EDITOR: 'first.txt'] <== selected <== marked", - " second.txt", - " third.txt" - ], - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("fourth.txt", window, cx)); - panel.confirm_edit(true, window, cx).unwrap() - }); - confirm.await.unwrap(); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " fourth.txt <== selected", - " second.txt", - " third.txt" - ], - "File list should be different after rename confirmation" - ); - - panel.update_in(cx, |panel, window, cx| { - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.executor().run_until_parked(); - - select_path(&panel, "src/test/second.txt", cx); - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - - workspace - .read_with(cx, |this, cx| { - assert!( - this.recent_navigation_history_iter(cx) - .any(|(project_path, abs_path)| { - project_path.path == Arc::from(rel_path("test/fourth.txt")) - && abs_path == Some(PathBuf::from(path!("/src/test/fourth.txt"))) - }) - ); - }) - .unwrap(); -} - -#[gpui::test] -async fn test_select_git_entry(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "tree1": { - ".git": {}, - "dir1": { - "modified1.txt": "1", - "unmodified1.txt": "1", - "modified2.txt": "1", - }, - "dir2": { - "modified3.txt": "1", - "unmodified2.txt": "1", - }, - "modified4.txt": "1", - "unmodified3.txt": "1", - }, - "tree2": { - ".git": {}, - "dir3": { - "modified5.txt": "1", - "unmodified4.txt": "1", - }, - "modified6.txt": "1", - "unmodified5.txt": "1", - } - }), - ) - .await; - - // Mark files as git modified - fs.set_head_and_index_for_repo( - path!("/root/tree1/.git").as_ref(), - &[ - ("dir1/modified1.txt", "modified".into()), - ("dir1/modified2.txt", "modified".into()), - ("modified4.txt", "modified".into()), - ("dir2/modified3.txt", "modified".into()), - ], - ); - fs.set_head_and_index_for_repo( - path!("/root/tree2/.git").as_ref(), - &[ - ("dir3/modified5.txt", "modified".into()), - ("modified6.txt", "modified".into()), - ], - ); - - let project = Project::test( - fs.clone(), - [path!("/root/tree1").as_ref(), path!("/root/tree2").as_ref()], - cx, - ) - .await; - - let (scan1_complete, scan2_complete) = project.update(cx, |project, cx| { - let mut worktrees = project.worktrees(cx); - let worktree1 = worktrees.next().unwrap(); - let worktree2 = worktrees.next().unwrap(); - ( - worktree1.read(cx).as_local().unwrap().scan_complete(), - worktree2.read(cx).as_local().unwrap().scan_complete(), - ) - }); - scan1_complete.await; - scan2_complete.await; - cx.run_until_parked(); - - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Check initial state - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v tree1", - " > .git", - " > dir1", - " > dir2", - " modified4.txt", - " unmodified3.txt", - "v tree2", - " > .git", - " > dir3", - " modified6.txt", - " unmodified5.txt" - ], - ); - - // Test selecting next modified entry - panel.update_in(cx, |panel, window, cx| { - panel.select_next_git_entry(&SelectNextGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..6, cx), - &[ - "v tree1", - " > .git", - " v dir1", - " modified1.txt <== selected", - " modified2.txt", - " unmodified1.txt", - ], - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_next_git_entry(&SelectNextGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..6, cx), - &[ - "v tree1", - " > .git", - " v dir1", - " modified1.txt", - " modified2.txt <== selected", - " unmodified1.txt", - ], - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_next_git_entry(&SelectNextGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 6..9, cx), - &[ - " v dir2", - " modified3.txt <== selected", - " unmodified2.txt", - ], - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_next_git_entry(&SelectNextGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 9..11, cx), - &[" modified4.txt <== selected", " unmodified3.txt",], - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_next_git_entry(&SelectNextGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 13..16, cx), - &[ - " v dir3", - " modified5.txt <== selected", - " unmodified4.txt", - ], - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_next_git_entry(&SelectNextGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 16..18, cx), - &[" modified6.txt <== selected", " unmodified5.txt",], - ); - - // Wraps around to first modified file - panel.update_in(cx, |panel, window, cx| { - panel.select_next_git_entry(&SelectNextGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..18, cx), - &[ - "v tree1", - " > .git", - " v dir1", - " modified1.txt <== selected", - " modified2.txt", - " unmodified1.txt", - " v dir2", - " modified3.txt", - " unmodified2.txt", - " modified4.txt", - " unmodified3.txt", - "v tree2", - " > .git", - " v dir3", - " modified5.txt", - " unmodified4.txt", - " modified6.txt", - " unmodified5.txt", - ], - ); - - // Wraps around again to last modified file - panel.update_in(cx, |panel, window, cx| { - panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 16..18, cx), - &[" modified6.txt <== selected", " unmodified5.txt",], - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 13..16, cx), - &[ - " v dir3", - " modified5.txt <== selected", - " unmodified4.txt", - ], - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 9..11, cx), - &[" modified4.txt <== selected", " unmodified3.txt",], - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 6..9, cx), - &[ - " v dir2", - " modified3.txt <== selected", - " unmodified2.txt", - ], - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..6, cx), - &[ - "v tree1", - " > .git", - " v dir1", - " modified1.txt", - " modified2.txt <== selected", - " unmodified1.txt", - ], - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..6, cx), - &[ - "v tree1", - " > .git", - " v dir1", - " modified1.txt <== selected", - " modified2.txt", - " unmodified1.txt", - ], - ); -} - -#[gpui::test] -async fn test_select_directory(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project_root", - json!({ - "dir_1": { - "nested_dir": { - "file_a.py": "# File contents", - } - }, - "file_1.py": "# File contents", - "dir_2": { - - }, - "dir_3": { - - }, - "file_2.py": "# File contents", - "dir_4": { - - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - select_path(&panel, "project_root/dir_1", cx); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " > dir_1 <== selected", - " > dir_2", - " > dir_3", - " > dir_4", - " file_1.py", - " file_2.py", - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel.select_prev_directory(&SelectPrevDirectory, window, cx) - }); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root <== selected", - " > dir_1", - " > dir_2", - " > dir_3", - " > dir_4", - " file_1.py", - " file_2.py", - ] - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_prev_directory(&SelectPrevDirectory, window, cx) - }); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " > dir_1", - " > dir_2", - " > dir_3", - " > dir_4 <== selected", - " file_1.py", - " file_2.py", - ] - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_next_directory(&SelectNextDirectory, window, cx) - }); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root <== selected", - " > dir_1", - " > dir_2", - " > dir_3", - " > dir_4", - " file_1.py", - " file_2.py", - ] - ); -} - -#[gpui::test] -async fn test_select_first_last(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project_root", - json!({ - "dir_1": { - "nested_dir": { - "file_a.py": "# File contents", - } - }, - "file_1.py": "# File contents", - "file_2.py": "# File contents", - "zdir_2": { - "nested_dir2": { - "file_b.py": "# File contents", - } - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " > dir_1", - " > zdir_2", - " file_1.py", - " file_2.py", - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel.select_first(&SelectFirst, window, cx) - }); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root <== selected", - " > dir_1", - " > zdir_2", - " file_1.py", - " file_2.py", - ] - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_last(&SelectLast, window, cx) - }); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " > dir_1", - " > zdir_2", - " file_1.py", - " file_2.py <== selected", - ] - ); - - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "> dir_1", - "> zdir_2", - " file_1.py", - " file_2.py", - ], - "With hide_root=true, root should be hidden" - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_first(&SelectFirst, window, cx) - }); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "> dir_1 <== selected", - "> zdir_2", - " file_1.py", - " file_2.py", - ], - "With hide_root=true, first entry should be dir_1, not the hidden root" - ); -} - -#[gpui::test] -async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project_root", - json!({ - "dir_1": { - "nested_dir": { - "file_a.py": "# File contents", - } - }, - "file_1.py": "# File contents", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - select_path(&panel, "project_root/dir_1", cx); - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - select_path(&panel, "project_root/dir_1/nested_dir", cx); - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1", - " > nested_dir <== selected", - " file_1.py", - ] - ); -} - -#[gpui::test] -async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project_root", - json!({ - "dir_1": { - "nested_dir": { - "file_a.py": "# File contents", - "file_b.py": "# File contents", - "file_c.py": "# File contents", - }, - "file_1.py": "# File contents", - "file_2.py": "# File contents", - "file_3.py": "# File contents", - }, - "dir_2": { - "file_1.py": "# File contents", - "file_2.py": "# File contents", - "file_3.py": "# File contents", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update_in(cx, |panel, window, cx| { - panel.collapse_all_entries(&CollapseAllEntries, window, cx) - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v project_root", " > dir_1", " > dir_2",] - ); - - // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries - toggle_expand_dir(&panel, "project_root/dir_1", cx); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1 <== selected", - " > nested_dir", - " file_1.py", - " file_2.py", - " file_3.py", - " > dir_2", - ] - ); -} - -#[gpui::test] -async fn test_collapse_all_entries_multiple_worktrees(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - let worktree_content = json!({ - "dir_1": { - "file_1.py": "# File contents", - }, - "dir_2": { - "file_1.py": "# File contents", - } - }); - - fs.insert_tree("/project_root_1", worktree_content.clone()) - .await; - fs.insert_tree("/project_root_2", worktree_content).await; - - let project = Project::test( - fs.clone(), - ["/project_root_1".as_ref(), "/project_root_2".as_ref()], - cx, - ) - .await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update_in(cx, |panel, window, cx| { - panel.collapse_all_entries(&CollapseAllEntries, window, cx) - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["> project_root_1", "> project_root_2",] - ); -} - -#[gpui::test] -async fn test_collapse_all_entries_with_collapsed_root(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project_root", - json!({ - "dir_1": { - "nested_dir": { - "file_a.py": "# File contents", - "file_b.py": "# File contents", - "file_c.py": "# File contents", - }, - "file_1.py": "# File contents", - "file_2.py": "# File contents", - "file_3.py": "# File contents", - }, - "dir_2": { - "file_1.py": "# File contents", - "file_2.py": "# File contents", - "file_3.py": "# File contents", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Open project_root/dir_1 to ensure that a nested directory is expanded - toggle_expand_dir(&panel, "project_root/dir_1", cx); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1 <== selected", - " > nested_dir", - " file_1.py", - " file_2.py", - " file_3.py", - " > dir_2", - ] - ); - - // Close root directory - toggle_expand_dir(&panel, "project_root", cx); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["> project_root <== selected"] - ); - - // Run collapse_all_entries and make sure root is not expanded - panel.update_in(cx, |panel, window, cx| { - panel.collapse_all_entries(&CollapseAllEntries, window, cx) - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["> project_root <== selected"] - ); -} - -#[gpui::test] -async fn test_new_file_move(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.as_fake().insert_tree(path!("/root"), json!({})).await; - let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Make a new buffer with no backing file - workspace - .update(cx, |workspace, window, cx| { - Editor::new_file(workspace, &Default::default(), window, cx) - }) - .unwrap(); - - cx.executor().run_until_parked(); - - // "Save as" the buffer, creating a new backing file for it - let save_task = workspace - .update(cx, |workspace, window, cx| { - workspace.save_active_item(workspace::SaveIntent::Save, window, cx) - }) - .unwrap(); - - cx.executor().run_until_parked(); - cx.simulate_new_path_selection(|_| Some(PathBuf::from(path!("/root/new")))); - save_task.await.unwrap(); - - // Rename the file - select_path(&panel, "root/new", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v root", " new <== selected <== marked"] - ); - panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); - panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("newer", window, cx)); - }); - panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx)); - - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v root", " newer <== selected"] - ); - - workspace - .update(cx, |workspace, window, cx| { - workspace.save_active_item(workspace::SaveIntent::Save, window, cx) - }) - .unwrap() - .await - .unwrap(); - - cx.executor().run_until_parked(); - // assert that saving the file doesn't restore "new" - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v root", " newer <== selected"] - ); -} - -// NOTE: This test is skipped on Windows, because on Windows, unlike on Unix, -// you can't rename a directory which some program has already open. This is a -// limitation of the Windows. Since Zed will have the root open, it will hold an open handle -// to it, and thus renaming it will fail on Windows. -// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder -// See: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_rename_information -#[gpui::test] -#[cfg_attr(target_os = "windows", ignore)] -async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "dir1": { - "file1.txt": "content 1", - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root1/dir1", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &["v root1", " v dir1 <== selected", " file1.txt",], - "Initial state with worktrees" - ); - - select_path(&panel, "root1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &["v root1 <== selected", " v dir1", " file1.txt",], - ); - - // Rename root1 to new_root1 - panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v [EDITOR: 'root1'] <== selected", - " v dir1", - " file1.txt", - ], - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("new_root1", window, cx)); - panel.confirm_edit(true, window, cx).unwrap() - }); - confirm.await.unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v new_root1 <== selected", - " v dir1", - " file1.txt", - ], - "Should update worktree name" - ); - - // Ensure internal paths have been updated - select_path(&panel, "new_root1/dir1/file1.txt", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v new_root1", - " v dir1", - " file1.txt <== selected", - ], - "Files in renamed worktree are selectable" - ); -} - -#[gpui::test] -async fn test_rename_with_hide_root(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "dir1": { "file1.txt": "content" }, - "file2.txt": "content", - }), - ) - .await; - fs.insert_tree("/root2", json!({ "file3.txt": "content" })) - .await; - - // Test 1: Single worktree, hide_root=true - rename should be blocked - { - let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update(cx, |panel, cx| { - let project = panel.project.read(cx); - let worktree = project.visible_worktrees(cx).next().unwrap(); - let root_entry = worktree.read(cx).root_entry().unwrap(); - panel.state.selection = Some(SelectedEntry { - worktree_id: worktree.read(cx).id(), - entry_id: root_entry.id, - }); - }); - - panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); - - assert!( - panel.read_with(cx, |panel, _| panel.state.edit_state.is_none()), - "Rename should be blocked when hide_root=true with single worktree" - ); - } - - // Test 2: Multiple worktrees, hide_root=true - rename should work - { - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - select_path(&panel, "root1", cx); - panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); - - #[cfg(target_os = "windows")] - assert!( - panel.read_with(cx, |panel, _| panel.state.edit_state.is_none()), - "Rename should be blocked on Windows even with multiple worktrees" - ); - - #[cfg(not(target_os = "windows"))] - { - assert!( - panel.read_with(cx, |panel, _| panel.state.edit_state.is_some()), - "Rename should work with multiple worktrees on non-Windows when hide_root=true" - ); - panel.update_in(cx, |panel, window, cx| { - panel.cancel(&menu::Cancel, window, cx) - }); - } - } -} - -#[gpui::test] -async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project_root", - json!({ - "dir_1": { - "nested_dir": { - "file_a.py": "# File contents", - } - }, - "file_1.py": "# File contents", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let worktree_id = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id()); - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - this.select_next(&Default::default(), window, cx); - this.expand_selected_entry(&Default::default(), window, cx); - }) - }); - cx.run_until_parked(); - - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - this.expand_selected_entry(&Default::default(), window, cx); - }) - }); - cx.run_until_parked(); - - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - this.select_next(&Default::default(), window, cx); - this.expand_selected_entry(&Default::default(), window, cx); - }) - }); - cx.run_until_parked(); - - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - this.select_next(&Default::default(), window, cx); - }) - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1", - " v nested_dir", - " file_a.py <== selected", - " file_1.py", - ] - ); - let modifiers_with_shift = gpui::Modifiers { - shift: true, - ..Default::default() - }; - cx.run_until_parked(); - cx.simulate_modifiers_change(modifiers_with_shift); - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - this.select_next(&Default::default(), window, cx); - }) - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1", - " v nested_dir", - " file_a.py", - " file_1.py <== selected <== marked", - ] - ); - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - this.select_previous(&Default::default(), window, cx); - }) - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1", - " v nested_dir", - " file_a.py <== selected <== marked", - " file_1.py <== marked", - ] - ); - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - let drag = DraggedSelection { - active_selection: this.state.selection.unwrap(), - marked_selections: this.marked_entries.clone().into(), - }; - let target_entry = this - .project - .read(cx) - .entry_for_path(&(worktree_id, rel_path("")).into(), cx) - .unwrap(); - this.drag_onto(&drag, target_entry.id, false, window, cx); - }); - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1", - " v nested_dir", - " file_1.py <== marked", - " file_a.py <== selected <== marked", - ] - ); - // ESC clears out all marks - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - this.cancel(&menu::Cancel, window, cx); - }) - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1", - " v nested_dir", - " file_1.py", - " file_a.py <== selected", - ] - ); - // ESC clears out all marks - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - this.select_previous(&SelectPrevious, window, cx); - this.select_next(&SelectNext, window, cx); - }) - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1", - " v nested_dir", - " file_1.py <== marked", - " file_a.py <== selected <== marked", - ] - ); - cx.simulate_modifiers_change(Default::default()); - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - this.cut(&Cut, window, cx); - this.select_previous(&SelectPrevious, window, cx); - this.select_previous(&SelectPrevious, window, cx); - - this.paste(&Paste, window, cx); - this.update_visible_entries(None, false, false, window, cx); - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1", - " v nested_dir", - " file_1.py <== marked", - " file_a.py <== selected <== marked", - ] - ); - cx.simulate_modifiers_change(modifiers_with_shift); - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - this.expand_selected_entry(&Default::default(), window, cx); - this.select_next(&SelectNext, window, cx); - this.select_next(&SelectNext, window, cx); - }) - }); - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1", - " v nested_dir <== selected", - ] - ); -} - -#[gpui::test] -async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "a": { - "b": { - "c": { - "d": {} - } - } - }, - "target_destination": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - auto_fold_dirs: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Case 1: Move last dir 'd' - should move only 'd', leaving 'a/b/c' - select_path(&panel, "root/a/b/c/d", cx); - panel.update_in(cx, |panel, window, cx| { - let drag = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: panel.state.selection.as_ref().unwrap().worktree_id, - entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id), - }, - marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]), - }; - let target_entry = panel - .project - .read(cx) - .visible_worktrees(cx) - .next() - .unwrap() - .read(cx) - .entry_for_path(rel_path("target_destination")) - .unwrap(); - panel.drag_onto(&drag, target_entry.id, false, window, cx); - }); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root", - " > a/b/c", - " > target_destination/d <== selected" - ], - "Moving last empty directory 'd' should leave 'a/b/c' and move only 'd'" - ); - - // Reset - select_path(&panel, "root/target_destination/d", cx); - panel.update_in(cx, |panel, window, cx| { - let drag = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: panel.state.selection.as_ref().unwrap().worktree_id, - entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id), - }, - marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]), - }; - let target_entry = panel - .project - .read(cx) - .visible_worktrees(cx) - .next() - .unwrap() - .read(cx) - .entry_for_path(rel_path("a/b/c")) - .unwrap(); - panel.drag_onto(&drag, target_entry.id, false, window, cx); - }); - cx.executor().run_until_parked(); - - // Case 2: Move middle dir 'b' - should move 'b/c/d', leaving only 'a' - select_path(&panel, "root/a/b", cx); - panel.update_in(cx, |panel, window, cx| { - let drag = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: panel.state.selection.as_ref().unwrap().worktree_id, - entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id), - }, - marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]), - }; - let target_entry = panel - .project - .read(cx) - .visible_worktrees(cx) - .next() - .unwrap() - .read(cx) - .entry_for_path(rel_path("target_destination")) - .unwrap(); - panel.drag_onto(&drag, target_entry.id, false, window, cx); - }); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v root", " v a", " > target_destination/b/c/d"], - "Moving middle directory 'b' should leave only 'a' and move 'b/c/d'" - ); - - // Reset - select_path(&panel, "root/target_destination/b", cx); - panel.update_in(cx, |panel, window, cx| { - let drag = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: panel.state.selection.as_ref().unwrap().worktree_id, - entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id), - }, - marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]), - }; - let target_entry = panel - .project - .read(cx) - .visible_worktrees(cx) - .next() - .unwrap() - .read(cx) - .entry_for_path(rel_path("a")) - .unwrap(); - panel.drag_onto(&drag, target_entry.id, false, window, cx); - }); - cx.executor().run_until_parked(); - - // Case 3: Move first dir 'a' - should move whole 'a/b/c/d' - select_path(&panel, "root/a", cx); - panel.update_in(cx, |panel, window, cx| { - let drag = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: panel.state.selection.as_ref().unwrap().worktree_id, - entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id), - }, - marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]), - }; - let target_entry = panel - .project - .read(cx) - .visible_worktrees(cx) - .next() - .unwrap() - .read(cx) - .entry_for_path(rel_path("target_destination")) - .unwrap(); - panel.drag_onto(&drag, target_entry.id, false, window, cx); - }); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v root", " > target_destination/a/b/c/d"], - "Moving first directory 'a' should move whole 'a/b/c/d' chain" - ); -} - -#[gpui::test] -async fn test_drag_entries_between_different_worktrees(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root_a", - json!({ - "src": { - "lib.rs": "", - "main.rs": "" - }, - "docs": { - "guide.md": "" - }, - "multi": { - "alpha.txt": "", - "beta.txt": "" - } - }), - ) - .await; - fs.insert_tree( - "/root_b", - json!({ - "dst": { - "existing.md": "" - }, - "target.txt": "" - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root_a".as_ref(), "/root_b".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Case 1: move a file onto a directory in another worktree. - select_path(&panel, "root_a/src/main.rs", cx); - drag_selection_to(&panel, "root_b/dst", false, cx); - assert!( - find_project_entry(&panel, "root_b/dst/main.rs", cx).is_some(), - "Dragged file should appear under destination worktree" - ); - assert_eq!( - find_project_entry(&panel, "root_a/src/main.rs", cx), - None, - "Dragged file should be removed from the source worktree" - ); - - // Case 2: drop a file onto another worktree file so it lands in the parent directory. - select_path(&panel, "root_a/docs/guide.md", cx); - drag_selection_to(&panel, "root_b/dst/existing.md", true, cx); - assert!( - find_project_entry(&panel, "root_b/dst/guide.md", cx).is_some(), - "Dropping onto a file should place the entry beside the target file" - ); - assert_eq!( - find_project_entry(&panel, "root_a/docs/guide.md", cx), - None, - "Source file should be removed after the move" - ); - - // Case 3: move an entire directory. - select_path(&panel, "root_a/src", cx); - drag_selection_to(&panel, "root_b/dst", false, cx); - assert!( - find_project_entry(&panel, "root_b/dst/src/lib.rs", cx).is_some(), - "Dragging a directory should move its nested contents" - ); - assert_eq!( - find_project_entry(&panel, "root_a/src", cx), - None, - "Directory should no longer exist in the source worktree" - ); - - // Case 4: multi-selection drag between worktrees. - panel.update(cx, |panel, _| panel.marked_entries.clear()); - select_path_with_mark(&panel, "root_a/multi/alpha.txt", cx); - select_path_with_mark(&panel, "root_a/multi/beta.txt", cx); - drag_selection_to(&panel, "root_b/dst", false, cx); - assert!( - find_project_entry(&panel, "root_b/dst/alpha.txt", cx).is_some() - && find_project_entry(&panel, "root_b/dst/beta.txt", cx).is_some(), - "All marked entries should move to the destination worktree" - ); - assert_eq!( - find_project_entry(&panel, "root_a/multi/alpha.txt", cx), - None, - "Marked entries should be removed from the origin worktree" - ); - assert_eq!( - find_project_entry(&panel, "root_a/multi/beta.txt", cx), - None, - "Marked entries should be removed from the origin worktree" - ); -} - -#[gpui::test] -async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(Vec::new()); - settings - .project_panel - .get_or_insert_default() - .auto_reveal_entries = Some(false); - }); - }) - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/project_root", - json!({ - ".git": {}, - ".gitignore": "**/gitignored_dir", - "dir_1": { - "file_1.py": "# File 1_1 contents", - "file_2.py": "# File 1_2 contents", - "file_3.py": "# File 1_3 contents", - "gitignored_dir": { - "file_a.py": "# File contents", - "file_b.py": "# File contents", - "file_c.py": "# File contents", - }, - }, - "dir_2": { - "file_1.py": "# File 2_1 contents", - "file_2.py": "# File 2_2 contents", - "file_3.py": "# File 2_3 contents", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " > dir_1", - " > dir_2", - " .gitignore", - ] - ); - - let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx) - .expect("dir 1 file is not ignored and should have an entry"); - let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx) - .expect("dir 2 file is not ignored and should have an entry"); - let gitignored_dir_file = - find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx); - assert_eq!( - gitignored_dir_file, None, - "File in the gitignored dir should not have an entry before its dir is toggled" - ); - - toggle_expand_dir(&panel, "project_root/dir_1", cx); - toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " v gitignored_dir <== selected", - " file_a.py", - " file_b.py", - " file_c.py", - " file_1.py", - " file_2.py", - " file_3.py", - " > dir_2", - " .gitignore", - ], - "Should show gitignored dir file list in the project panel" - ); - let gitignored_dir_file = - find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx) - .expect("after gitignored dir got opened, a file entry should be present"); - - toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx); - toggle_expand_dir(&panel, "project_root/dir_1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " > dir_1 <== selected", - " > dir_2", - " .gitignore", - ], - "Should hide all dir contents again and prepare for the auto reveal test" - ); - - for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] { - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::ActiveEntryChanged(Some(file_entry))) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " > dir_1 <== selected", - " > dir_2", - " .gitignore", - ], - "When no auto reveal is enabled, the selected entry should not be revealed in the project panel" - ); - } - - cx.update(|_, cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings - .project_panel - .get_or_insert_default() - .auto_reveal_entries = Some(true) - }); - }) - }); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file))) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " > gitignored_dir", - " file_1.py <== selected <== marked", - " file_2.py", - " file_3.py", - " > dir_2", - " .gitignore", - ], - "When auto reveal is enabled, not ignored dir_1 entry should be revealed" - ); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file))) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " > gitignored_dir", - " file_1.py", - " file_2.py", - " file_3.py", - " v dir_2", - " file_1.py <== selected <== marked", - " file_2.py", - " file_3.py", - " .gitignore", - ], - "When auto reveal is enabled, not ignored dir_2 entry should be revealed" - ); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::ActiveEntryChanged(Some( - gitignored_dir_file, - ))) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " > gitignored_dir", - " file_1.py", - " file_2.py", - " file_3.py", - " v dir_2", - " file_1.py <== selected <== marked", - " file_2.py", - " file_3.py", - " .gitignore", - ], - "When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel" - ); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file)) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " v gitignored_dir", - " file_a.py <== selected <== marked", - " file_b.py", - " file_c.py", - " file_1.py", - " file_2.py", - " file_3.py", - " v dir_2", - " file_1.py", - " file_2.py", - " file_3.py", - " .gitignore", - ], - "When a gitignored entry is explicitly revealed, it should be shown in the project tree" - ); -} - -#[gpui::test] -async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(Vec::new()); - settings.project.worktree.file_scan_inclusions = - Some(vec!["always_included_but_ignored_dir/*".to_string()]); - settings - .project_panel - .get_or_insert_default() - .auto_reveal_entries = Some(false) - }); - }) - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/project_root", - json!({ - ".git": {}, - ".gitignore": "**/gitignored_dir\n/always_included_but_ignored_dir", - "dir_1": { - "file_1.py": "# File 1_1 contents", - "file_2.py": "# File 1_2 contents", - "file_3.py": "# File 1_3 contents", - "gitignored_dir": { - "file_a.py": "# File contents", - "file_b.py": "# File contents", - "file_c.py": "# File contents", - }, - }, - "dir_2": { - "file_1.py": "# File 2_1 contents", - "file_2.py": "# File 2_2 contents", - "file_3.py": "# File 2_3 contents", - }, - "always_included_but_ignored_dir": { - "file_a.py": "# File contents", - "file_b.py": "# File contents", - "file_c.py": "# File contents", - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " > always_included_but_ignored_dir", - " > dir_1", - " > dir_2", - " .gitignore", - ] - ); - - let gitignored_dir_file = - find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx); - let always_included_but_ignored_dir_file = find_project_entry( - &panel, - "project_root/always_included_but_ignored_dir/file_a.py", - cx, - ) - .expect("file that is .gitignored but set to always be included should have an entry"); - assert_eq!( - gitignored_dir_file, None, - "File in the gitignored dir should not have an entry unless its directory is toggled" - ); - - toggle_expand_dir(&panel, "project_root/dir_1", cx); - cx.run_until_parked(); - cx.update(|_, cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings - .project_panel - .get_or_insert_default() - .auto_reveal_entries = Some(true) - }); - }) - }); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::ActiveEntryChanged(Some( - always_included_but_ignored_dir_file, - ))) - }) - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v always_included_but_ignored_dir", - " file_a.py <== selected <== marked", - " file_b.py", - " file_c.py", - " v dir_1", - " > gitignored_dir", - " file_1.py", - " file_2.py", - " file_3.py", - " > dir_2", - " .gitignore", - ], - "When auto reveal is enabled, a gitignored but always included selected entry should be revealed in the project panel" - ); -} - -#[gpui::test] -async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(Vec::new()); - settings - .project_panel - .get_or_insert_default() - .auto_reveal_entries = Some(false) - }); - }) - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/project_root", - json!({ - ".git": {}, - ".gitignore": "**/gitignored_dir", - "dir_1": { - "file_1.py": "# File 1_1 contents", - "file_2.py": "# File 1_2 contents", - "file_3.py": "# File 1_3 contents", - "gitignored_dir": { - "file_a.py": "# File contents", - "file_b.py": "# File contents", - "file_c.py": "# File contents", - }, - }, - "dir_2": { - "file_1.py": "# File 2_1 contents", - "file_2.py": "# File 2_2 contents", - "file_3.py": "# File 2_3 contents", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " > dir_1", - " > dir_2", - " .gitignore", - ] - ); - - let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx) - .expect("dir 1 file is not ignored and should have an entry"); - let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx) - .expect("dir 2 file is not ignored and should have an entry"); - let gitignored_dir_file = - find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx); - assert_eq!( - gitignored_dir_file, None, - "File in the gitignored dir should not have an entry before its dir is toggled" - ); - - toggle_expand_dir(&panel, "project_root/dir_1", cx); - toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " v gitignored_dir <== selected", - " file_a.py", - " file_b.py", - " file_c.py", - " file_1.py", - " file_2.py", - " file_3.py", - " > dir_2", - " .gitignore", - ], - "Should show gitignored dir file list in the project panel" - ); - let gitignored_dir_file = - find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx) - .expect("after gitignored dir got opened, a file entry should be present"); - - toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx); - toggle_expand_dir(&panel, "project_root/dir_1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " > dir_1 <== selected", - " > dir_2", - " .gitignore", - ], - "Should hide all dir contents again and prepare for the explicit reveal test" - ); - - for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] { - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::ActiveEntryChanged(Some(file_entry))) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " > dir_1 <== selected", - " > dir_2", - " .gitignore", - ], - "When no auto reveal is enabled, the selected entry should not be revealed in the project panel" - ); - } - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(dir_1_file)) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " > gitignored_dir", - " file_1.py <== selected <== marked", - " file_2.py", - " file_3.py", - " > dir_2", - " .gitignore", - ], - "With no auto reveal, explicit reveal should show the dir_1 entry in the project panel" - ); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(dir_2_file)) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " > gitignored_dir", - " file_1.py", - " file_2.py", - " file_3.py", - " v dir_2", - " file_1.py <== selected <== marked", - " file_2.py", - " file_3.py", - " .gitignore", - ], - "With no auto reveal, explicit reveal should show the dir_2 entry in the project panel" - ); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file)) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " v gitignored_dir", - " file_a.py <== selected <== marked", - " file_b.py", - " file_c.py", - " file_1.py", - " file_2.py", - " file_3.py", - " v dir_2", - " file_1.py", - " file_2.py", - " file_3.py", - " .gitignore", - ], - "With no auto reveal, explicit reveal should show the gitignored entry in the project panel" - ); -} - -#[gpui::test] -async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) { - init_test(cx); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]); - }); - }); - }); - - cx.update(|cx| { - register_project_item::(cx); - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - ".dockerignore": "", - ".git": { - "HEAD": "", - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - select_path(&panel, "root1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v root1 <== selected", " .dockerignore",] - ); - workspace - .update(cx, |workspace, _, cx| { - assert!( - workspace.active_item(cx).is_none(), - "Should have no active items in the beginning" - ); - }) - .unwrap(); - - let excluded_file_path = ".git/COMMIT_EDITMSG"; - let excluded_dir_path = "excluded_dir"; - - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - panel - .update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text(excluded_file_path, window, cx) - }); - panel.confirm_edit(true, window, cx).unwrap() - }) - .await - .unwrap(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..13, cx), - &["v root1", " .dockerignore"], - "Excluded dir should not be shown after opening a file in it" - ); - panel.update_in(cx, |panel, window, cx| { - assert!( - !panel.filename_editor.read(cx).is_focused(window), - "Should have closed the file name editor" - ); - }); - workspace - .update(cx, |workspace, _, cx| { - let active_entry_path = workspace - .active_item(cx) - .expect("should have opened and activated the excluded item") - .act_as::(cx) - .expect("should have opened the corresponding project item for the excluded item") - .read(cx) - .path - .clone(); - assert_eq!( - active_entry_path.path.as_ref(), - rel_path(excluded_file_path), - "Should open the excluded file" - ); - - assert!( - workspace.notification_ids().is_empty(), - "Should have no notifications after opening an excluded file" - ); - }) - .unwrap(); - assert!( - fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await, - "Should have created the excluded file" - ); - - select_path(&panel, "root1", cx); - panel.update_in(cx, |panel, window, cx| { - panel.new_directory(&NewDirectory, window, cx) - }); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - panel - .update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text(excluded_file_path, window, cx) - }); - panel.confirm_edit(true, window, cx).unwrap() - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..13, cx), - &["v root1", " .dockerignore"], - "Should not change the project panel after trying to create an excluded directorya directory with the same name as the excluded file" - ); - panel.update_in(cx, |panel, window, cx| { - assert!( - !panel.filename_editor.read(cx).is_focused(window), - "Should have closed the file name editor" - ); - }); - workspace - .update(cx, |workspace, _, cx| { - let notifications = workspace.notification_ids(); - assert_eq!( - notifications.len(), - 1, - "Should receive one notification with the error message" - ); - workspace.dismiss_notification(notifications.first().unwrap(), cx); - assert!(workspace.notification_ids().is_empty()); - }) - .unwrap(); - - select_path(&panel, "root1", cx); - panel.update_in(cx, |panel, window, cx| { - panel.new_directory(&NewDirectory, window, cx) - }); - cx.run_until_parked(); - - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - - panel - .update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text(excluded_dir_path, window, cx) - }); - panel.confirm_edit(true, window, cx).unwrap() - }) - .await - .unwrap(); - - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..13, cx), - &["v root1", " .dockerignore"], - "Should not change the project panel after trying to create an excluded directory" - ); - panel.update_in(cx, |panel, window, cx| { - assert!( - !panel.filename_editor.read(cx).is_focused(window), - "Should have closed the file name editor" - ); - }); - workspace - .update(cx, |workspace, _, cx| { - let notifications = workspace.notification_ids(); - assert_eq!( - notifications.len(), - 1, - "Should receive one notification explaining that no directory is actually shown" - ); - workspace.dismiss_notification(notifications.first().unwrap(), cx); - assert!(workspace.notification_ids().is_empty()); - }) - .unwrap(); - assert!( - fs.is_dir(Path::new("/root1/excluded_dir")).await, - "Should have created the excluded directory" - ); -} - -#[gpui::test] -async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/src", - json!({ - "test": { - "first.rs": "// First Rust file", - "second.rs": "// Second Rust file", - "third.rs": "// Third Rust file", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - select_path(&panel, "src", cx); - panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src <== selected", - " > test" - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel.new_directory(&NewDirectory, window, cx) - }); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src", - " > [EDITOR: ''] <== selected", - " > test" - ] - ); - - panel.update_in(cx, |panel, window, cx| { - panel.cancel(&menu::Cancel, window, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src <== selected", - " > test" - ] - ); -} - -#[gpui::test] -async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "dir1": { - "subdir1": {}, - "file1.txt": "", - "file2.txt": "", - }, - "dir2": { - "subdir2": {}, - "file3.txt": "", - "file4.txt": "", - }, - "file5.txt": "", - "file6.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/dir1", cx); - toggle_expand_dir(&panel, "root/dir2", cx); - - // Test Case 1: Delete middle file in directory - select_path(&panel, "root/dir1/file1.txt", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir1", - " > subdir1", - " file1.txt <== selected", - " file2.txt", - " v dir2", - " > subdir2", - " file3.txt", - " file4.txt", - " file5.txt", - " file6.txt", - ], - "Initial state before deleting middle file" - ); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir1", - " > subdir1", - " file2.txt <== selected", - " v dir2", - " > subdir2", - " file3.txt", - " file4.txt", - " file5.txt", - " file6.txt", - ], - "Should select next file after deleting middle file" - ); - - // Test Case 2: Delete last file in directory - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir1", - " > subdir1 <== selected", - " v dir2", - " > subdir2", - " file3.txt", - " file4.txt", - " file5.txt", - " file6.txt", - ], - "Should select next directory when last file is deleted" - ); - - // Test Case 3: Delete root level file - select_path(&panel, "root/file6.txt", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir1", - " > subdir1", - " v dir2", - " > subdir2", - " file3.txt", - " file4.txt", - " file5.txt", - " file6.txt <== selected", - ], - "Initial state before deleting root level file" - ); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir1", - " > subdir1", - " v dir2", - " > subdir2", - " file3.txt", - " file4.txt", - " file5.txt <== selected", - ], - "Should select prev entry at root level" - ); -} - -#[gpui::test] -async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "aa": "// Testing 1", - "bb": "// Testing 2", - "cc": "// Testing 3", - "dd": "// Testing 4", - "ee": "// Testing 5", - "ff": "// Testing 6", - "gg": "// Testing 7", - "hh": "// Testing 8", - "ii": "// Testing 8", - ".gitignore": "bb\ndd\nee\nff\nii\n'", - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - // Test 1: Auto selection with one gitignored file next to the deleted file - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_gitignore: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - select_path(&panel, "root/aa", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root", - " .gitignore", - " aa <== selected", - " cc", - " gg", - " hh" - ], - "Initial state should hide files on .gitignore" - ); - - submit_deletion(&panel, cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root", - " .gitignore", - " cc <== selected", - " gg", - " hh" - ], - "Should select next entry not on .gitignore" - ); - - // Test 2: Auto selection with many gitignored files next to the deleted file - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root", - " .gitignore", - " gg <== selected", - " hh" - ], - "Should select next entry not on .gitignore" - ); - - // Test 3: Auto selection of entry before deleted file - select_path(&panel, "root/hh", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root", - " .gitignore", - " gg", - " hh <== selected" - ], - "Should select next entry not on .gitignore" - ); - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v root", " .gitignore", " gg <== selected"], - "Should select next entry not on .gitignore" - ); -} - -#[gpui::test] -async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "dir1": { - "file1": "// Testing", - "file2": "// Testing", - "file3": "// Testing" - }, - "aa": "// Testing", - ".gitignore": "file1\nfile3\n", - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_gitignore: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Test 1: Visible items should exclude files on gitignore - toggle_expand_dir(&panel, "root/dir1", cx); - select_path(&panel, "root/dir1/file2", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root", - " v dir1", - " file2 <== selected", - " .gitignore", - " aa" - ], - "Initial state should hide files on .gitignore" - ); - submit_deletion(&panel, cx); - - // Test 2: Auto selection should go to the parent - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root", - " v dir1 <== selected", - " .gitignore", - " aa" - ], - "Initial state should hide files on .gitignore" - ); -} - -#[gpui::test] -async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "dir1": { - "subdir1": { - "a.txt": "", - "b.txt": "" - }, - "file1.txt": "", - }, - "dir2": { - "subdir2": { - "c.txt": "", - "d.txt": "" - }, - "file2.txt": "", - }, - "file3.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/dir1", cx); - toggle_expand_dir(&panel, "root/dir1/subdir1", cx); - toggle_expand_dir(&panel, "root/dir2", cx); - toggle_expand_dir(&panel, "root/dir2/subdir2", cx); - - // Test Case 1: Select and delete nested directory with parent - cx.simulate_modifiers_change(gpui::Modifiers { - control: true, - ..Default::default() - }); - select_path_with_mark(&panel, "root/dir1/subdir1", cx); - select_path_with_mark(&panel, "root/dir1", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir1 <== selected <== marked", - " v subdir1 <== marked", - " a.txt", - " b.txt", - " file1.txt", - " v dir2", - " v subdir2", - " c.txt", - " d.txt", - " file2.txt", - " file3.txt", - ], - "Initial state before deleting nested directory with parent" - ); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir2 <== selected", - " v subdir2", - " c.txt", - " d.txt", - " file2.txt", - " file3.txt", - ], - "Should select next directory after deleting directory with parent" - ); - - // Test Case 2: Select mixed files and directories across levels - select_path_with_mark(&panel, "root/dir2/subdir2/c.txt", cx); - select_path_with_mark(&panel, "root/dir2/file2.txt", cx); - select_path_with_mark(&panel, "root/file3.txt", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir2", - " v subdir2", - " c.txt <== marked", - " d.txt", - " file2.txt <== marked", - " file3.txt <== selected <== marked", - ], - "Initial state before deleting" - ); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir2 <== selected", - " v subdir2", - " d.txt", - ], - "Should select sibling directory" - ); -} - -#[gpui::test] -async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "dir1": { - "subdir1": { - "a.txt": "", - "b.txt": "" - }, - "file1.txt": "", - }, - "dir2": { - "subdir2": { - "c.txt": "", - "d.txt": "" - }, - "file2.txt": "", - }, - "file3.txt": "", - "file4.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/dir1", cx); - toggle_expand_dir(&panel, "root/dir1/subdir1", cx); - toggle_expand_dir(&panel, "root/dir2", cx); - toggle_expand_dir(&panel, "root/dir2/subdir2", cx); - - // Test Case 1: Select all root files and directories - cx.simulate_modifiers_change(gpui::Modifiers { - control: true, - ..Default::default() - }); - select_path_with_mark(&panel, "root/dir1", cx); - select_path_with_mark(&panel, "root/dir2", cx); - select_path_with_mark(&panel, "root/file3.txt", cx); - select_path_with_mark(&panel, "root/file4.txt", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1 <== marked", - " v subdir1", - " a.txt", - " b.txt", - " file1.txt", - " v dir2 <== marked", - " v subdir2", - " c.txt", - " d.txt", - " file2.txt", - " file3.txt <== marked", - " file4.txt <== selected <== marked", - ], - "State before deleting all contents" - ); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &["v root <== selected"], - "Only empty root directory should remain after deleting all contents" - ); -} - -#[gpui::test] -async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "dir1": { - "subdir1": { - "file_a.txt": "content a", - "file_b.txt": "content b", - }, - "subdir2": { - "file_c.txt": "content c", - }, - "file1.txt": "content 1", - }, - "dir2": { - "file2.txt": "content 2", - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/dir1", cx); - toggle_expand_dir(&panel, "root/dir1/subdir1", cx); - toggle_expand_dir(&panel, "root/dir2", cx); - cx.simulate_modifiers_change(gpui::Modifiers { - control: true, - ..Default::default() - }); - - // Test Case 1: Select parent directory, subdirectory, and a file inside the subdirectory - select_path_with_mark(&panel, "root/dir1", cx); - select_path_with_mark(&panel, "root/dir1/subdir1", cx); - select_path_with_mark(&panel, "root/dir1/subdir1/file_a.txt", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1 <== marked", - " v subdir1 <== marked", - " file_a.txt <== selected <== marked", - " file_b.txt", - " > subdir2", - " file1.txt", - " v dir2", - " file2.txt", - ], - "State with parent dir, subdir, and file selected" - ); - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &["v root", " v dir2 <== selected", " file2.txt",], - "Only dir2 should remain after deletion" - ); -} - -#[gpui::test] -async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - // First worktree - fs.insert_tree( - "/root1", - json!({ - "dir1": { - "file1.txt": "content 1", - "file2.txt": "content 2", - }, - "dir2": { - "file3.txt": "content 3", - }, - }), - ) - .await; - - // Second worktree - fs.insert_tree( - "/root2", - json!({ - "dir3": { - "file4.txt": "content 4", - "file5.txt": "content 5", - }, - "file6.txt": "content 6", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Expand all directories for testing - toggle_expand_dir(&panel, "root1/dir1", cx); - toggle_expand_dir(&panel, "root1/dir2", cx); - toggle_expand_dir(&panel, "root2/dir3", cx); - - // Test Case 1: Delete files across different worktrees - cx.simulate_modifiers_change(gpui::Modifiers { - control: true, - ..Default::default() - }); - select_path_with_mark(&panel, "root1/dir1/file1.txt", cx); - select_path_with_mark(&panel, "root2/dir3/file4.txt", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root1", - " v dir1", - " file1.txt <== marked", - " file2.txt", - " v dir2", - " file3.txt", - "v root2", - " v dir3", - " file4.txt <== selected <== marked", - " file5.txt", - " file6.txt", - ], - "Initial state with files selected from different worktrees" - ); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root1", - " v dir1", - " file2.txt", - " v dir2", - " file3.txt", - "v root2", - " v dir3", - " file5.txt <== selected", - " file6.txt", - ], - "Should select next file in the last worktree after deletion" - ); - - // Test Case 2: Delete directories from different worktrees - select_path_with_mark(&panel, "root1/dir1", cx); - select_path_with_mark(&panel, "root2/dir3", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root1", - " v dir1 <== marked", - " file2.txt", - " v dir2", - " file3.txt", - "v root2", - " v dir3 <== selected <== marked", - " file5.txt", - " file6.txt", - ], - "State with directories marked from different worktrees" - ); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root1", - " v dir2", - " file3.txt", - "v root2", - " file6.txt <== selected", - ], - "Should select remaining file in last worktree after directory deletion" - ); - - // Test Case 4: Delete all remaining files except roots - select_path_with_mark(&panel, "root1/dir2/file3.txt", cx); - select_path_with_mark(&panel, "root2/file6.txt", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root1", - " v dir2", - " file3.txt <== marked", - "v root2", - " file6.txt <== selected <== marked", - ], - "State with all remaining files marked" - ); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &["v root1", " v dir2", "v root2 <== selected"], - "Second parent root should be selected after deleting" - ); -} - -#[gpui::test] -async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "dir1": { - "file1.txt": "", - "file2.txt": "", - "file3.txt": "", - }, - "dir2": { - "file4.txt": "", - "file5.txt": "", - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/dir1", cx); - toggle_expand_dir(&panel, "root/dir2", cx); - - cx.simulate_modifiers_change(gpui::Modifiers { - control: true, - ..Default::default() - }); - - select_path_with_mark(&panel, "root/dir1/file2.txt", cx); - select_path(&panel, "root/dir1/file1.txt", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir1", - " file1.txt <== selected", - " file2.txt <== marked", - " file3.txt", - " v dir2", - " file4.txt", - " file5.txt", - ], - "Initial state with one marked entry and different selection" - ); - - // Delete should operate on the selected entry (file1.txt) - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir1", - " file2.txt <== selected <== marked", - " file3.txt", - " v dir2", - " file4.txt", - " file5.txt", - ], - "Should delete selected file, not marked file" - ); - - select_path_with_mark(&panel, "root/dir1/file3.txt", cx); - select_path_with_mark(&panel, "root/dir2/file4.txt", cx); - select_path(&panel, "root/dir2/file5.txt", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir1", - " file2.txt <== marked", - " file3.txt <== marked", - " v dir2", - " file4.txt <== marked", - " file5.txt <== selected", - ], - "Initial state with multiple marked entries and different selection" - ); - - // Delete should operate on all marked entries, ignoring the selection - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir1", - " v dir2", - " file5.txt <== selected", - ], - "Should delete all marked files, leaving only the selected file" - ); -} - -#[gpui::test] -async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root_b", - json!({ - "dir1": { - "file1.txt": "content 1", - "file2.txt": "content 2", - }, - }), - ) - .await; - - fs.insert_tree( - "/root_c", - json!({ - "dir2": {}, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root_b/dir1", cx); - toggle_expand_dir(&panel, "root_c/dir2", cx); - - cx.simulate_modifiers_change(gpui::Modifiers { - control: true, - ..Default::default() - }); - select_path_with_mark(&panel, "root_b/dir1/file1.txt", cx); - select_path_with_mark(&panel, "root_b/dir1/file2.txt", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root_b", - " v dir1", - " file1.txt <== marked", - " file2.txt <== selected <== marked", - "v root_c", - " v dir2", - ], - "Initial state with files marked in root_b" - ); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root_b", - " v dir1 <== selected", - "v root_c", - " v dir2", - ], - "After deletion in root_b as it's last deletion, selection should be in root_b" - ); - - select_path_with_mark(&panel, "root_c/dir2", cx); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &["v root_b", " v dir1", "v root_c <== selected",], - "After deleting from root_c, it should remain in root_c" - ); -} - -fn toggle_expand_dir(panel: &Entity, path: &str, cx: &mut VisualTestContext) { - let path = rel_path(path); - panel.update_in(cx, |panel, window, cx| { - for worktree in panel.project.read(cx).worktrees(cx).collect::>() { - let worktree = worktree.read(cx); - if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { - let entry_id = worktree.entry_for_path(relative_path).unwrap().id; - panel.toggle_expanded(entry_id, window, cx); - return; - } - } - panic!("no worktree for path {:?}", path); - }); - cx.run_until_parked(); -} - -#[gpui::test] -async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - ".gitignore": "**/ignored_dir\n**/ignored_nested", - "dir1": { - "empty1": { - "empty2": { - "empty3": { - "file.txt": "" - } - } - }, - "subdir1": { - "file1.txt": "", - "file2.txt": "", - "ignored_nested": { - "ignored_file.txt": "" - } - }, - "ignored_dir": { - "subdir": { - "deep_file.txt": "" - } - } - } - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - // Test 1: When auto-fold is enabled - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - auto_fold_dirs: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &["v root", " > dir1", " .gitignore",], - "Initial state should show collapsed root structure" - ); - - toggle_expand_dir(&panel, "root/dir1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1 <== selected", - " > empty1/empty2/empty3", - " > ignored_dir", - " > subdir1", - " .gitignore", - ], - "Should show first level with auto-folded dirs and ignored dir visible" - ); - - let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap(); - panel.update_in(cx, |panel, window, cx| { - let project = panel.project.read(cx); - let worktree = project.worktrees(cx).next().unwrap().read(cx); - panel.expand_all_for_entry(worktree.id(), entry_id, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1 <== selected", - " v empty1", - " v empty2", - " v empty3", - " file.txt", - " > ignored_dir", - " v subdir1", - " > ignored_nested", - " file1.txt", - " file2.txt", - " .gitignore", - ], - "After expand_all with auto-fold: should not expand ignored_dir, should expand folded dirs, and should not expand ignored_nested" - ); - - // Test 2: When auto-fold is disabled - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - auto_fold_dirs: false, - ..settings - }, - cx, - ); - }); - - panel.update_in(cx, |panel, window, cx| { - panel.collapse_all_entries(&CollapseAllEntries, window, cx); - }); - - toggle_expand_dir(&panel, "root/dir1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1 <== selected", - " > empty1", - " > ignored_dir", - " > subdir1", - " .gitignore", - ], - "With auto-fold disabled: should show all directories separately" - ); - - let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap(); - panel.update_in(cx, |panel, window, cx| { - let project = panel.project.read(cx); - let worktree = project.worktrees(cx).next().unwrap().read(cx); - panel.expand_all_for_entry(worktree.id(), entry_id, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1 <== selected", - " v empty1", - " v empty2", - " v empty3", - " file.txt", - " > ignored_dir", - " v subdir1", - " > ignored_nested", - " file1.txt", - " file2.txt", - " .gitignore", - ], - "After expand_all without auto-fold: should expand all dirs normally, \ - expand ignored_dir itself but not its subdirs, and not expand ignored_nested" - ); - - // Test 3: When explicitly called on ignored directory - let ignored_dir_entry = find_project_entry(&panel, "root/dir1/ignored_dir", cx).unwrap(); - panel.update_in(cx, |panel, window, cx| { - let project = panel.project.read(cx); - let worktree = project.worktrees(cx).next().unwrap().read(cx); - panel.expand_all_for_entry(worktree.id(), ignored_dir_entry, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1 <== selected", - " v empty1", - " v empty2", - " v empty3", - " file.txt", - " v ignored_dir", - " v subdir", - " deep_file.txt", - " v subdir1", - " > ignored_nested", - " file1.txt", - " file2.txt", - " .gitignore", - ], - "After expand_all on ignored_dir: should expand all contents of the ignored directory" - ); -} - -#[gpui::test] -async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "dir1": { - "subdir1": { - "nested1": { - "file1.txt": "", - "file2.txt": "" - }, - }, - "subdir2": { - "file4.txt": "" - } - }, - "dir2": { - "single_file": { - "file5.txt": "" - } - } - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - // Test 1: Basic collapsing - { - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/dir1", cx); - toggle_expand_dir(&panel, "root/dir1/subdir1", cx); - toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx); - toggle_expand_dir(&panel, "root/dir1/subdir2", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1", - " v subdir1", - " v nested1", - " file1.txt", - " file2.txt", - " v subdir2 <== selected", - " file4.txt", - " > dir2", - ], - "Initial state with everything expanded" - ); - - let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap(); - panel.update_in(cx, |panel, window, cx| { - let project = panel.project.read(cx); - let worktree = project.worktrees(cx).next().unwrap().read(cx); - panel.collapse_all_for_entry(worktree.id(), entry_id, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &["v root", " > dir1", " > dir2",], - "All subdirs under dir1 should be collapsed" - ); - } - - // Test 2: With auto-fold enabled - { - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - auto_fold_dirs: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/dir1", cx); - toggle_expand_dir(&panel, "root/dir1/subdir1", cx); - toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1", - " v subdir1/nested1 <== selected", - " file1.txt", - " file2.txt", - " > subdir2", - " > dir2/single_file", - ], - "Initial state with some dirs expanded" - ); - - let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap(); - panel.update(cx, |panel, cx| { - let project = panel.project.read(cx); - let worktree = project.worktrees(cx).next().unwrap().read(cx); - panel.collapse_all_for_entry(worktree.id(), entry_id, cx); - }); - - toggle_expand_dir(&panel, "root/dir1", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1 <== selected", - " > subdir1/nested1", - " > subdir2", - " > dir2/single_file", - ], - "Subdirs should be collapsed and folded with auto-fold enabled" - ); - } - - // Test 3: With auto-fold disabled - { - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - auto_fold_dirs: false, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/dir1", cx); - toggle_expand_dir(&panel, "root/dir1/subdir1", cx); - toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1", - " v subdir1", - " v nested1 <== selected", - " file1.txt", - " file2.txt", - " > subdir2", - " > dir2", - ], - "Initial state with some dirs expanded and auto-fold disabled" - ); - - let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap(); - panel.update(cx, |panel, cx| { - let project = panel.project.read(cx); - let worktree = project.worktrees(cx).next().unwrap().read(cx); - panel.collapse_all_for_entry(worktree.id(), entry_id, cx); - }); - - toggle_expand_dir(&panel, "root/dir1", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1 <== selected", - " > subdir1", - " > subdir2", - " > dir2", - ], - "Subdirs should be collapsed but not folded with auto-fold disabled" - ); - } -} - -#[gpui::test] -async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "dir1": { - "file1.txt": "", - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " > dir1", - ], - "Initial state with nothing selected" - ); - - panel.update_in(cx, |panel, window, cx| { - panel.new_file(&NewFile, window, cx); - }); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - panel - .update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text("hello_from_no_selections", window, cx) - }); - panel.confirm_edit(true, window, cx).unwrap() - }) - .await - .unwrap(); - cx.run_until_parked(); - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " > dir1", - " hello_from_no_selections <== selected <== marked", - ], - "A new file is created under the root directory" - ); -} - -#[gpui::test] -async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "existing_dir": { - "existing_file.txt": "", - }, - "existing_file.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "> existing_dir", - " existing_file.txt", - ], - "Initial state with hide_root=true, root should be hidden and nothing selected" - ); - - panel.update(cx, |panel, _| { - assert!( - panel.state.selection.is_none(), - "Should have no selection initially" - ); - }); - - // Test 1: Create new file when no entry is selected - panel.update_in(cx, |panel, window, cx| { - panel.new_file(&NewFile, window, cx); - }); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - cx.run_until_parked(); - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "> existing_dir", - " [EDITOR: ''] <== selected", - " existing_file.txt", - ], - "Editor should appear at root level when hide_root=true and no selection" - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text("new_file_at_root.txt", window, cx) - }); - panel.confirm_edit(true, window, cx).unwrap() - }); - confirm.await.unwrap(); - cx.run_until_parked(); - - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "> existing_dir", - " existing_file.txt", - " new_file_at_root.txt <== selected <== marked", - ], - "New file should be created at root level and visible without root prefix" - ); - - assert!( - fs.is_file(Path::new("/root/new_file_at_root.txt")).await, - "File should be created in the actual root directory" - ); - - // Test 2: Create new directory when no entry is selected - panel.update(cx, |panel, _| { - panel.state.selection = None; - }); - - panel.update_in(cx, |panel, window, cx| { - panel.new_directory(&NewDirectory, window, cx); - }); - cx.run_until_parked(); - - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "> [EDITOR: ''] <== selected", - "> existing_dir", - " existing_file.txt", - " new_file_at_root.txt", - ], - "Directory editor should appear at root level when hide_root=true and no selection" - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text("new_dir_at_root", window, cx) - }); - panel.confirm_edit(true, window, cx).unwrap() - }); - confirm.await.unwrap(); - cx.run_until_parked(); - - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "> existing_dir", - "v new_dir_at_root <== selected", - " existing_file.txt", - " new_file_at_root.txt", - ], - "New directory should be created at root level and visible without root prefix" - ); - - assert!( - fs.is_dir(Path::new("/root/new_dir_at_root")).await, - "Directory should be created in the actual root directory" - ); -} - -#[cfg(windows)] -#[gpui::test] -async fn test_create_entry_with_trailing_dot_windows(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "dir1": { - "file1.txt": "", - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " > dir1", - ], - "Initial state with nothing selected" - ); - - panel.update_in(cx, |panel, window, cx| { - panel.new_file(&NewFile, window, cx); - }); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - panel - .update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("foo.", window, cx)); - panel.confirm_edit(true, window, cx).unwrap() - }) - .await - .unwrap(); - cx.run_until_parked(); - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " > dir1", - " foo <== selected <== marked", - ], - "A new file is created under the root directory without the trailing dot" - ); -} - -#[gpui::test] -async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "dir1": { - "file1.txt": "", - "dir2": { - "file2.txt": "" - } - }, - "file3.txt": "" - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update(cx, |panel, cx| { - let project = panel.project.read(cx); - let worktree = project.visible_worktrees(cx).next().unwrap(); - let worktree = worktree.read(cx); - - // Test 1: Target is a directory, should highlight the directory itself - let dir_entry = worktree.entry_for_path(rel_path("dir1")).unwrap(); - let result = panel.highlight_entry_for_external_drag(dir_entry, worktree); - assert_eq!( - result, - Some(dir_entry.id), - "Should highlight directory itself" - ); - - // Test 2: Target is nested file, should highlight immediate parent - let nested_file = worktree - .entry_for_path(rel_path("dir1/dir2/file2.txt")) - .unwrap(); - let nested_parent = worktree.entry_for_path(rel_path("dir1/dir2")).unwrap(); - let result = panel.highlight_entry_for_external_drag(nested_file, worktree); - assert_eq!( - result, - Some(nested_parent.id), - "Should highlight immediate parent" - ); - - // Test 3: Target is root level file, should highlight root - let root_file = worktree.entry_for_path(rel_path("file3.txt")).unwrap(); - let result = panel.highlight_entry_for_external_drag(root_file, worktree); - assert_eq!( - result, - Some(worktree.root_entry().unwrap().id), - "Root level file should return None" - ); - - // Test 4: Target is root itself, should highlight root - let root_entry = worktree.root_entry().unwrap(); - let result = panel.highlight_entry_for_external_drag(root_entry, worktree); - assert_eq!( - result, - Some(root_entry.id), - "Root level file should return None" - ); - }); -} - -#[gpui::test] -async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "parent_dir": { - "child_file.txt": "", - "sibling_file.txt": "", - "child_dir": { - "nested_file.txt": "" - } - }, - "other_dir": { - "other_file.txt": "" - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update(cx, |panel, cx| { - let project = panel.project.read(cx); - let worktree = project.visible_worktrees(cx).next().unwrap(); - let worktree_id = worktree.read(cx).id(); - let worktree = worktree.read(cx); - - let parent_dir = worktree.entry_for_path(rel_path("parent_dir")).unwrap(); - let child_file = worktree - .entry_for_path(rel_path("parent_dir/child_file.txt")) - .unwrap(); - let sibling_file = worktree - .entry_for_path(rel_path("parent_dir/sibling_file.txt")) - .unwrap(); - let child_dir = worktree - .entry_for_path(rel_path("parent_dir/child_dir")) - .unwrap(); - let other_dir = worktree.entry_for_path(rel_path("other_dir")).unwrap(); - let other_file = worktree - .entry_for_path(rel_path("other_dir/other_file.txt")) - .unwrap(); - - // Test 1: Single item drag, don't highlight parent directory - let dragged_selection = DraggedSelection { - active_selection: SelectedEntry { - worktree_id, - entry_id: child_file.id, - }, - marked_selections: Arc::new([SelectedEntry { - worktree_id, - entry_id: child_file.id, - }]), - }; - let result = - panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx); - assert_eq!(result, None, "Should not highlight parent of dragged item"); - - // Test 2: Single item drag, don't highlight sibling files - let result = panel.highlight_entry_for_selection_drag( - sibling_file, - worktree, - &dragged_selection, - cx, - ); - assert_eq!(result, None, "Should not highlight sibling files"); - - // Test 3: Single item drag, highlight unrelated directory - let result = - panel.highlight_entry_for_selection_drag(other_dir, worktree, &dragged_selection, cx); - assert_eq!( - result, - Some(other_dir.id), - "Should highlight unrelated directory" - ); - - // Test 4: Single item drag, highlight sibling directory - let result = - panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx); - assert_eq!( - result, - Some(child_dir.id), - "Should highlight sibling directory" - ); - - // Test 5: Multiple items drag, highlight parent directory - let dragged_selection = DraggedSelection { - active_selection: SelectedEntry { - worktree_id, - entry_id: child_file.id, - }, - marked_selections: Arc::new([ - SelectedEntry { - worktree_id, - entry_id: child_file.id, - }, - SelectedEntry { - worktree_id, - entry_id: sibling_file.id, - }, - ]), - }; - let result = - panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx); - assert_eq!( - result, - Some(parent_dir.id), - "Should highlight parent with multiple items" - ); - - // Test 6: Target is file in different directory, highlight parent - let result = - panel.highlight_entry_for_selection_drag(other_file, worktree, &dragged_selection, cx); - assert_eq!( - result, - Some(other_dir.id), - "Should highlight parent of target file" - ); - - // Test 7: Target is directory, always highlight - let result = - panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx); - assert_eq!( - result, - Some(child_dir.id), - "Should always highlight directories" - ); - }); -} - -#[gpui::test] -async fn test_highlight_entry_for_selection_drag_cross_worktree(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "src": { - "main.rs": "", - "lib.rs": "" - } - }), - ) - .await; - fs.insert_tree( - "/root2", - json!({ - "src": { - "main.rs": "", - "test.rs": "" - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update(cx, |panel, cx| { - let project = panel.project.read(cx); - let worktrees: Vec<_> = project.visible_worktrees(cx).collect(); - - let worktree_a = &worktrees[0]; - let main_rs_from_a = worktree_a - .read(cx) - .entry_for_path(rel_path("src/main.rs")) - .unwrap(); - - let worktree_b = &worktrees[1]; - let src_dir_from_b = worktree_b.read(cx).entry_for_path(rel_path("src")).unwrap(); - let main_rs_from_b = worktree_b - .read(cx) - .entry_for_path(rel_path("src/main.rs")) - .unwrap(); - - // Test dragging file from worktree A onto parent of file with same relative path in worktree B - let dragged_selection = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: worktree_a.read(cx).id(), - entry_id: main_rs_from_a.id, - }, - marked_selections: Arc::new([SelectedEntry { - worktree_id: worktree_a.read(cx).id(), - entry_id: main_rs_from_a.id, - }]), - }; - - let result = panel.highlight_entry_for_selection_drag( - src_dir_from_b, - worktree_b.read(cx), - &dragged_selection, - cx, - ); - assert_eq!( - result, - Some(src_dir_from_b.id), - "Should highlight target directory from different worktree even with same relative path" - ); - - // Test dragging file from worktree A onto file with same relative path in worktree B - let result = panel.highlight_entry_for_selection_drag( - main_rs_from_b, - worktree_b.read(cx), - &dragged_selection, - cx, - ); - assert_eq!( - result, - Some(src_dir_from_b.id), - "Should highlight parent of target file from different worktree" - ); - }); -} - -#[gpui::test] -async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "parent_dir": { - "child_file.txt": "", - "nested_dir": { - "nested_file.txt": "" - } - }, - "root_file.txt": "" - }), - ) - .await; - - fs.insert_tree( - "/root2", - json!({ - "other_dir": { - "other_file.txt": "" - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update(cx, |panel, cx| { - let project = panel.project.read(cx); - let worktrees: Vec<_> = project.visible_worktrees(cx).collect(); - let worktree1 = worktrees[0].read(cx); - let worktree2 = worktrees[1].read(cx); - let worktree1_id = worktree1.id(); - let _worktree2_id = worktree2.id(); - - let root1_entry = worktree1.root_entry().unwrap(); - let root2_entry = worktree2.root_entry().unwrap(); - let _parent_dir = worktree1.entry_for_path(rel_path("parent_dir")).unwrap(); - let child_file = worktree1 - .entry_for_path(rel_path("parent_dir/child_file.txt")) - .unwrap(); - let nested_file = worktree1 - .entry_for_path(rel_path("parent_dir/nested_dir/nested_file.txt")) - .unwrap(); - let root_file = worktree1.entry_for_path(rel_path("root_file.txt")).unwrap(); - - // Test 1: Multiple entries - should always highlight background - let multiple_dragged_selection = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: worktree1_id, - entry_id: child_file.id, - }, - marked_selections: Arc::new([ - SelectedEntry { - worktree_id: worktree1_id, - entry_id: child_file.id, - }, - SelectedEntry { - worktree_id: worktree1_id, - entry_id: nested_file.id, - }, - ]), - }; - - let result = panel.should_highlight_background_for_selection_drag( - &multiple_dragged_selection, - root1_entry.id, - cx, - ); - assert!(result, "Should highlight background for multiple entries"); - - // Test 2: Single entry with non-empty parent path - should highlight background - let nested_dragged_selection = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: worktree1_id, - entry_id: nested_file.id, - }, - marked_selections: Arc::new([SelectedEntry { - worktree_id: worktree1_id, - entry_id: nested_file.id, - }]), - }; - - let result = panel.should_highlight_background_for_selection_drag( - &nested_dragged_selection, - root1_entry.id, - cx, - ); - assert!(result, "Should highlight background for nested file"); - - // Test 3: Single entry at root level, same worktree - should NOT highlight background - let root_file_dragged_selection = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: worktree1_id, - entry_id: root_file.id, - }, - marked_selections: Arc::new([SelectedEntry { - worktree_id: worktree1_id, - entry_id: root_file.id, - }]), - }; - - let result = panel.should_highlight_background_for_selection_drag( - &root_file_dragged_selection, - root1_entry.id, - cx, - ); - assert!( - !result, - "Should NOT highlight background for root file in same worktree" - ); - - // Test 4: Single entry at root level, different worktree - should highlight background - let result = panel.should_highlight_background_for_selection_drag( - &root_file_dragged_selection, - root2_entry.id, - cx, - ); - assert!( - result, - "Should highlight background for root file from different worktree" - ); - - // Test 5: Single entry in subdirectory - should highlight background - let child_file_dragged_selection = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: worktree1_id, - entry_id: child_file.id, - }, - marked_selections: Arc::new([SelectedEntry { - worktree_id: worktree1_id, - entry_id: child_file.id, - }]), - }; - - let result = panel.should_highlight_background_for_selection_drag( - &child_file_dragged_selection, - root1_entry.id, - cx, - ); - assert!( - result, - "Should highlight background for file with non-empty parent path" - ); - }); -} - -#[gpui::test] -async fn test_hide_root(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "dir1": { - "file1.txt": "content", - "file2.txt": "content", - }, - "dir2": { - "file3.txt": "content", - }, - "file4.txt": "content", - }), - ) - .await; - - fs.insert_tree( - "/root2", - json!({ - "dir3": { - "file5.txt": "content", - }, - "file6.txt": "content", - }), - ) - .await; - - // Test 1: Single worktree with hide_root = false - { - let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: false, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > dir1", - " > dir2", - " file4.txt", - ], - "With hide_root=false and single worktree, root should be visible" - ); - } - - // Test 2: Single worktree with hide_root = true - { - let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - // Set hide_root to true - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["> dir1", "> dir2", " file4.txt",], - "With hide_root=true and single worktree, root should be hidden" - ); - - // Test expanding directories still works without root - toggle_expand_dir(&panel, "root1/dir1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v dir1 <== selected", - " file1.txt", - " file2.txt", - "> dir2", - " file4.txt", - ], - "Should be able to expand directories even when root is hidden" - ); - } - - // Test 3: Multiple worktrees with hide_root = true - { - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - // Set hide_root to true - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > dir1", - " > dir2", - " file4.txt", - "v root2", - " > dir3", - " file6.txt", - ], - "With hide_root=true and multiple worktrees, roots should still be visible" - ); - } - - // Test 4: Multiple worktrees with hide_root = false - { - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: false, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > dir1", - " > dir2", - " file4.txt", - "v root2", - " > dir3", - " file6.txt", - ], - "With hide_root=false and multiple worktrees, roots should be visible" - ); - } -} - -#[gpui::test] -async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "file1.txt": "content of file1", - "file2.txt": "content of file2", - "dir1": { - "file3.txt": "content of file3" - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - let file1_path = "root/file1.txt"; - let file2_path = "root/file2.txt"; - select_path_with_mark(&panel, file1_path, cx); - select_path_with_mark(&panel, file2_path, cx); - - panel.update_in(cx, |panel, window, cx| { - panel.compare_marked_files(&CompareMarkedFiles, window, cx); - }); - cx.executor().run_until_parked(); - - workspace - .update(cx, |workspace, _, cx| { - let active_items = workspace - .panes() - .iter() - .filter_map(|pane| pane.read(cx).active_item()) - .collect::>(); - assert_eq!(active_items.len(), 1); - let diff_view = active_items - .into_iter() - .next() - .unwrap() - .downcast::() - .expect("Open item should be an FileDiffView"); - assert_eq!(diff_view.tab_content_text(0, cx), "file1.txt ↔ file2.txt"); - assert_eq!( - diff_view.tab_tooltip_text(cx).unwrap(), - format!( - "{} ↔ {}", - rel_path(file1_path).display(PathStyle::local()), - rel_path(file2_path).display(PathStyle::local()) - ) - ); - }) - .unwrap(); - - let file1_entry_id = find_project_entry(&panel, file1_path, cx).unwrap(); - let file2_entry_id = find_project_entry(&panel, file2_path, cx).unwrap(); - let worktree_id = panel.update(cx, |panel, cx| { - panel - .project - .read(cx) - .worktrees(cx) - .next() - .unwrap() - .read(cx) - .id() - }); - - let expected_entries = [ - SelectedEntry { - worktree_id, - entry_id: file1_entry_id, - }, - SelectedEntry { - worktree_id, - entry_id: file2_entry_id, - }, - ]; - panel.update(cx, |panel, _cx| { - assert_eq!( - &panel.marked_entries, &expected_entries, - "Should keep marked entries after comparison" - ); - }); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(file2_entry_id)) - }) - }); - - panel.update(cx, |panel, _cx| { - assert_eq!( - &panel.marked_entries, &expected_entries, - "Marked entries should persist after focusing back on the project panel" - ); - }); -} - -#[gpui::test] -async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "file1.txt": "content of file1", - "file2.txt": "content of file2", - "dir1": {}, - "dir2": { - "file3.txt": "content of file3" - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Test 1: When only one file is selected, there should be no compare option - select_path(&panel, "root/file1.txt", cx); - - let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx)); - assert_eq!( - selected_files, None, - "Should not have compare option when only one file is selected" - ); - - // Test 2: When multiple files are selected, there should be a compare option - select_path_with_mark(&panel, "root/file1.txt", cx); - select_path_with_mark(&panel, "root/file2.txt", cx); - - let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx)); - assert!( - selected_files.is_some(), - "Should have files selected for comparison" - ); - if let Some((file1, file2)) = selected_files { - assert!( - file1.to_string_lossy().ends_with("file1.txt") - && file2.to_string_lossy().ends_with("file2.txt"), - "Should have file1.txt and file2.txt as the selected files when multi-selecting" - ); - } - - // Test 3: Selecting a directory shouldn't count as a comparable file - select_path_with_mark(&panel, "root/dir1", cx); - - let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx)); - assert!( - selected_files.is_some(), - "Directory selection should not affect comparable files" - ); - if let Some((file1, file2)) = selected_files { - assert!( - file1.to_string_lossy().ends_with("file1.txt") - && file2.to_string_lossy().ends_with("file2.txt"), - "Selecting a directory should not affect the number of comparable files" - ); - } - - // Test 4: Selecting one more file - select_path_with_mark(&panel, "root/dir2/file3.txt", cx); - - let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx)); - assert!( - selected_files.is_some(), - "Directory selection should not affect comparable files" - ); - if let Some((file1, file2)) = selected_files { - assert!( - file1.to_string_lossy().ends_with("file2.txt") - && file2.to_string_lossy().ends_with("file3.txt"), - "Selecting a directory should not affect the number of comparable files" - ); - } -} - -#[gpui::test] -async fn test_hide_hidden_entries(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - ".hidden-file.txt": "hidden file content", - "visible-file.txt": "visible file content", - ".hidden-parent-dir": { - "nested-dir": { - "file.txt": "file content", - } - }, - "visible-dir": { - "file-in-visible.txt": "file content", - "nested": { - ".hidden-nested-dir": { - ".double-hidden-dir": { - "deep-file-1.txt": "deep content 1", - "deep-file-2.txt": "deep content 2" - }, - "hidden-nested-file-1.txt": "hidden nested 1", - "hidden-nested-file-2.txt": "hidden nested 2" - }, - "visible-nested-file.txt": "visible nested content" - } - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_hidden: false, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/.hidden-parent-dir", cx); - toggle_expand_dir(&panel, "root/.hidden-parent-dir/nested-dir", cx); - toggle_expand_dir(&panel, "root/visible-dir", cx); - toggle_expand_dir(&panel, "root/visible-dir/nested", cx); - toggle_expand_dir(&panel, "root/visible-dir/nested/.hidden-nested-dir", cx); - toggle_expand_dir( - &panel, - "root/visible-dir/nested/.hidden-nested-dir/.double-hidden-dir", - cx, - ); - - let expanded = [ - "v root", - " v .hidden-parent-dir", - " v nested-dir", - " file.txt", - " v visible-dir", - " v nested", - " v .hidden-nested-dir", - " v .double-hidden-dir <== selected", - " deep-file-1.txt", - " deep-file-2.txt", - " hidden-nested-file-1.txt", - " hidden-nested-file-2.txt", - " visible-nested-file.txt", - " file-in-visible.txt", - " .hidden-file.txt", - " visible-file.txt", - ]; - - assert_eq!( - visible_entries_as_strings(&panel, 0..30, cx), - &expanded, - "With hide_hidden=false, contents of hidden nested directory should be visible" - ); - - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_hidden: true, - ..settings - }, - cx, - ); - }); - - panel.update_in(cx, |panel, window, cx| { - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..30, cx), - &[ - "v root", - " v visible-dir", - " v nested", - " visible-nested-file.txt", - " file-in-visible.txt", - " visible-file.txt", - ], - "With hide_hidden=false, contents of hidden nested directory should be visible" - ); - - panel.update_in(cx, |panel, window, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_hidden: false, - ..settings - }, - cx, - ); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..30, cx), - &expanded, - "With hide_hidden=false, deeply nested hidden directories and their contents should be visible" - ); -} - -fn select_path(panel: &Entity, path: &str, cx: &mut VisualTestContext) { - let path = rel_path(path); - panel.update_in(cx, |panel, window, cx| { - for worktree in panel.project.read(cx).worktrees(cx).collect::>() { - let worktree = worktree.read(cx); - if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { - let entry_id = worktree.entry_for_path(relative_path).unwrap().id; - panel.update_visible_entries( - Some((worktree.id(), entry_id)), - false, - false, - window, - cx, - ); - return; - } - } - panic!("no worktree for path {:?}", path); - }); - cx.run_until_parked(); -} - -fn select_path_with_mark(panel: &Entity, path: &str, cx: &mut VisualTestContext) { - let path = rel_path(path); - panel.update(cx, |panel, cx| { - for worktree in panel.project.read(cx).worktrees(cx).collect::>() { - let worktree = worktree.read(cx); - if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { - let entry_id = worktree.entry_for_path(relative_path).unwrap().id; - let entry = crate::SelectedEntry { - worktree_id: worktree.id(), - entry_id, - }; - if !panel.marked_entries.contains(&entry) { - panel.marked_entries.push(entry); - } - panel.state.selection = Some(entry); - return; - } - } - panic!("no worktree for path {:?}", path); - }); -} - -fn drag_selection_to( - panel: &Entity, - target_path: &str, - is_file: bool, - cx: &mut VisualTestContext, -) { - let target_entry = find_project_entry(panel, target_path, cx) - .unwrap_or_else(|| panic!("no entry for target path {target_path:?}")); - - panel.update_in(cx, |panel, window, cx| { - let selection = panel - .state - .selection - .expect("a selection is required before dragging"); - let drag = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: selection.worktree_id, - entry_id: panel.resolve_entry(selection.entry_id), - }, - marked_selections: Arc::from(panel.marked_entries.clone()), - }; - panel.drag_onto(&drag, target_entry, is_file, window, cx); - }); - cx.executor().run_until_parked(); -} - -fn find_project_entry( - panel: &Entity, - path: &str, - cx: &mut VisualTestContext, -) -> Option { - let path = rel_path(path); - panel.update(cx, |panel, cx| { - for worktree in panel.project.read(cx).worktrees(cx).collect::>() { - let worktree = worktree.read(cx); - if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { - return worktree.entry_for_path(relative_path).map(|entry| entry.id); - } - } - panic!("no worktree for path {path:?}"); - }) -} - -fn visible_entries_as_strings( - panel: &Entity, - range: Range, - cx: &mut VisualTestContext, -) -> Vec { - let mut result = Vec::new(); - let mut project_entries = HashSet::default(); - let mut has_editor = false; - - panel.update_in(cx, |panel, window, cx| { - panel.for_each_visible_entry(range, window, cx, |project_entry, details, _, _| { - if details.is_editing { - assert!(!has_editor, "duplicate editor entry"); - has_editor = true; - } else { - assert!( - project_entries.insert(project_entry), - "duplicate project entry {:?} {:?}", - project_entry, - details - ); - } - - let indent = " ".repeat(details.depth); - let icon = if details.kind.is_dir() { - if details.is_expanded { "v " } else { "> " } - } else { - " " - }; - #[cfg(windows)] - let filename = details.filename.replace("\\", "/"); - #[cfg(not(windows))] - let filename = details.filename; - let name = if details.is_editing { - format!("[EDITOR: '{}']", filename) - } else if details.is_processing { - format!("[PROCESSING: '{}']", filename) - } else { - filename - }; - let selected = if details.is_selected { - " <== selected" - } else { - "" - }; - let marked = if details.is_marked { - " <== marked" - } else { - "" - }; - - result.push(format!("{indent}{icon}{name}{selected}{marked}")); - }); - }); - - result -} - -/// Test that missing sort_mode field defaults to DirectoriesFirst -#[gpui::test] -async fn test_sort_mode_default_fallback(cx: &mut gpui::TestAppContext) { - init_test(cx); - - // Verify that when sort_mode is not specified, it defaults to DirectoriesFirst - let default_settings = cx.read(|cx| *ProjectPanelSettings::get_global(cx)); - assert_eq!( - default_settings.sort_mode, - settings::ProjectPanelSortMode::DirectoriesFirst, - "sort_mode should default to DirectoriesFirst" - ); -} - -/// Test sort modes: DirectoriesFirst (default) vs Mixed -#[gpui::test] -async fn test_sort_mode_directories_first(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "zebra.txt": "", - "Apple": {}, - "banana.rs": "", - "Carrot": {}, - "aardvark.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Default sort mode should be DirectoriesFirst - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root", - " > Apple", - " > Carrot", - " aardvark.txt", - " banana.rs", - " zebra.txt", - ] - ); -} - -#[gpui::test] -async fn test_sort_mode_mixed(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "Zebra.txt": "", - "apple": {}, - "Banana.rs": "", - "carrot": {}, - "Aardvark.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - // Switch to Mixed mode - cx.update(|_, cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project_panel.get_or_insert_default().sort_mode = - Some(settings::ProjectPanelSortMode::Mixed); - }); - }); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Mixed mode: case-insensitive sorting - // Aardvark < apple < Banana < carrot < Zebra (all case-insensitive) - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root", - " Aardvark.txt", - " > apple", - " Banana.rs", - " > carrot", - " Zebra.txt", - ] - ); -} - -#[gpui::test] -async fn test_sort_mode_files_first(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "Zebra.txt": "", - "apple": {}, - "Banana.rs": "", - "carrot": {}, - "Aardvark.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - // Switch to FilesFirst mode - cx.update(|_, cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project_panel.get_or_insert_default().sort_mode = - Some(settings::ProjectPanelSortMode::FilesFirst); - }); - }); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // FilesFirst mode: files first, then directories (both case-insensitive) - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root", - " Aardvark.txt", - " Banana.rs", - " Zebra.txt", - " > apple", - " > carrot", - ] - ); -} - -#[gpui::test] -async fn test_sort_mode_toggle(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "file2.txt": "", - "dir1": {}, - "file1.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Initially DirectoriesFirst - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &["v root", " > dir1", " file1.txt", " file2.txt",] - ); - - // Toggle to Mixed - cx.update(|_, cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project_panel.get_or_insert_default().sort_mode = - Some(settings::ProjectPanelSortMode::Mixed); - }); - }); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &["v root", " > dir1", " file1.txt", " file2.txt",] - ); - - // Toggle back to DirectoriesFirst - cx.update(|_, cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project_panel.get_or_insert_default().sort_mode = - Some(settings::ProjectPanelSortMode::DirectoriesFirst); - }); - }); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &["v root", " > dir1", " file1.txt", " file2.txt",] - ); -} - -fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - theme::init(theme::LoadThemes::JustBase, cx); - crate::init(cx); - - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings - .project_panel - .get_or_insert_default() - .auto_fold_dirs = Some(false); - settings.project.worktree.file_scan_exclusions = Some(Vec::new()); - }); - }); - }); -} - -fn init_test_with_editor(cx: &mut TestAppContext) { - cx.update(|cx| { - let app_state = AppState::test(cx); - theme::init(theme::LoadThemes::JustBase, cx); - editor::init(cx); - crate::init(cx); - workspace::init(app_state, cx); - - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings - .project_panel - .get_or_insert_default() - .auto_fold_dirs = Some(false); - settings.project.worktree.file_scan_exclusions = Some(Vec::new()) - }); - }); - }); -} - -fn set_auto_open_settings( - cx: &mut TestAppContext, - auto_open_settings: ProjectPanelAutoOpenSettings, -) { - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project_panel.get_or_insert_default().auto_open = Some(auto_open_settings); - }); - }) - }); -} - -fn ensure_single_file_is_opened( - window: &WindowHandle, - expected_path: &str, - cx: &mut TestAppContext, -) { - window - .update(cx, |workspace, _, cx| { - let worktrees = workspace.worktrees(cx).collect::>(); - assert_eq!(worktrees.len(), 1); - let worktree_id = worktrees[0].read(cx).id(); - - let open_project_paths = workspace - .panes() - .iter() - .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) - .collect::>(); - assert_eq!( - open_project_paths, - vec![ProjectPath { - worktree_id, - path: Arc::from(rel_path(expected_path)) - }], - "Should have opened file, selected in project panel" - ); - }) - .unwrap(); -} - -fn submit_deletion(panel: &Entity, cx: &mut VisualTestContext) { - assert!( - !cx.has_pending_prompt(), - "Should have no prompts before the deletion" - ); - panel.update_in(cx, |panel, window, cx| { - panel.delete(&Delete { skip_prompt: false }, window, cx) - }); - assert!( - cx.has_pending_prompt(), - "Should have a prompt after the deletion" - ); - cx.simulate_prompt_answer("Delete"); - assert!( - !cx.has_pending_prompt(), - "Should have no prompts after prompt was replied to" - ); - cx.executor().run_until_parked(); -} - -fn submit_deletion_skipping_prompt(panel: &Entity, cx: &mut VisualTestContext) { - assert!( - !cx.has_pending_prompt(), - "Should have no prompts before the deletion" - ); - panel.update_in(cx, |panel, window, cx| { - panel.delete(&Delete { skip_prompt: true }, window, cx) - }); - assert!(!cx.has_pending_prompt(), "Should have received no prompts"); - cx.executor().run_until_parked(); -} - -fn ensure_no_open_items_and_panes(workspace: &WindowHandle, cx: &mut VisualTestContext) { - assert!( - !cx.has_pending_prompt(), - "Should have no prompts after deletion operation closes the file" - ); - workspace - .read_with(cx, |workspace, cx| { - let open_project_paths = workspace - .panes() - .iter() - .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) - .collect::>(); - assert!( - open_project_paths.is_empty(), - "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}" - ); - }) - .unwrap(); -} - -struct TestProjectItemView { - focus_handle: FocusHandle, - path: ProjectPath, -} - -struct TestProjectItem { - path: ProjectPath, -} - -impl project::ProjectItem for TestProjectItem { - fn try_open( - _project: &Entity, - path: &ProjectPath, - cx: &mut App, - ) -> Option>>> { - let path = path.clone(); - Some(cx.spawn(async move |cx| cx.new(|_| Self { path }))) - } - - fn entry_id(&self, _: &App) -> Option { - None - } - - fn project_path(&self, _: &App) -> Option { - Some(self.path.clone()) - } - - fn is_dirty(&self) -> bool { - false - } -} - -impl ProjectItem for TestProjectItemView { - type Item = TestProjectItem; - - fn for_project_item( - _: Entity, - _: Option<&Pane>, - project_item: Entity, - _: &mut Window, - cx: &mut Context, - ) -> Self - where - Self: Sized, - { - Self { - path: project_item.update(cx, |project_item, _| project_item.path.clone()), - focus_handle: cx.focus_handle(), - } - } -} - -impl Item for TestProjectItemView { - type Event = (); - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "Test".into() - } -} - -impl EventEmitter<()> for TestProjectItemView {} - -impl Focusable for TestProjectItemView { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for TestProjectItemView { - fn render(&mut self, _window: &mut Window, _: &mut Context) -> impl IntoElement { - Empty - } -} diff --git a/crates/project_panel/src/utils.rs b/crates/project_panel/src/utils.rs deleted file mode 100644 index 486def9b84..0000000000 --- a/crates/project_panel/src/utils.rs +++ /dev/null @@ -1,42 +0,0 @@ -pub(crate) struct ReversibleIterable { - pub(crate) it: It, - pub(crate) reverse: bool, -} - -impl ReversibleIterable { - pub(crate) fn new(it: T, reverse: bool) -> Self { - Self { it, reverse } - } -} - -impl ReversibleIterable -where - It: Iterator, -{ - pub(crate) fn find_single_ended(mut self, pred: F) -> Option - where - F: FnMut(&Item) -> bool, - { - if self.reverse { - self.it.filter(pred).last() - } else { - self.it.find(pred) - } - } -} - -impl ReversibleIterable -where - It: DoubleEndedIterator, -{ - pub(crate) fn find(mut self, mut pred: F) -> Option - where - F: FnMut(&Item) -> bool, - { - if self.reverse { - self.it.rfind(|x| pred(x)) - } else { - self.it.find(|x| pred(x)) - } - } -} diff --git a/crates/project_symbols/Cargo.toml b/crates/project_symbols/Cargo.toml deleted file mode 100644 index 83e3cb587d..0000000000 --- a/crates/project_symbols/Cargo.toml +++ /dev/null @@ -1,40 +0,0 @@ -[package] -name = "project_symbols" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/project_symbols.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -editor.workspace = true -fuzzy.workspace = true -gpui.workspace = true -ordered-float.workspace = true -picker.workspace = true -project.workspace = true -serde_json.workspace = true -settings.workspace = true -theme.workspace = true -util.workspace = true -workspace.workspace = true - -[dev-dependencies] -editor = { workspace = true, features = ["test-support"] } -futures.workspace = true -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -lsp = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } -release_channel.workspace = true -semver.workspace = true -settings = { workspace = true, features = ["test-support"] } -theme = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/project_symbols/LICENSE-GPL b/crates/project_symbols/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/project_symbols/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs deleted file mode 100644 index d96de4b876..0000000000 --- a/crates/project_symbols/src/project_symbols.rs +++ /dev/null @@ -1,463 +0,0 @@ -use editor::{Bias, Editor, SelectionEffects, scroll::Autoscroll, styled_runs_for_code_label}; -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{ - App, Context, DismissEvent, Entity, HighlightStyle, ParentElement, StyledText, Task, TextStyle, - WeakEntity, Window, relative, rems, -}; -use ordered_float::OrderedFloat; -use picker::{Picker, PickerDelegate}; -use project::{Project, Symbol, lsp_store::SymbolLocation}; -use settings::Settings; -use std::{cmp::Reverse, sync::Arc}; -use theme::{ActiveTheme, ThemeSettings}; -use util::ResultExt; -use workspace::{ - Workspace, - ui::{LabelLike, ListItem, ListItemSpacing, prelude::*}, -}; - -pub fn init(cx: &mut App) { - cx.observe_new( - |workspace: &mut Workspace, _window, _: &mut Context| { - workspace.register_action( - |workspace, _: &workspace::ToggleProjectSymbols, window, cx| { - let project = workspace.project().clone(); - let handle = cx.entity().downgrade(); - workspace.toggle_modal(window, cx, move |window, cx| { - let delegate = ProjectSymbolsDelegate::new(handle, project); - Picker::uniform_list(delegate, window, cx).width(rems(34.)) - }) - }, - ); - }, - ) - .detach(); -} - -pub type ProjectSymbols = Entity>; - -pub struct ProjectSymbolsDelegate { - workspace: WeakEntity, - project: Entity, - selected_match_index: usize, - symbols: Vec, - visible_match_candidates: Vec, - external_match_candidates: Vec, - show_worktree_root_name: bool, - matches: Vec, -} - -impl ProjectSymbolsDelegate { - fn new(workspace: WeakEntity, project: Entity) -> Self { - Self { - workspace, - project, - selected_match_index: 0, - symbols: Default::default(), - visible_match_candidates: Default::default(), - external_match_candidates: Default::default(), - matches: Default::default(), - show_worktree_root_name: false, - } - } - - fn filter(&mut self, query: &str, window: &mut Window, cx: &mut Context>) { - const MAX_MATCHES: usize = 100; - let mut visible_matches = cx.background_executor().block(fuzzy::match_strings( - &self.visible_match_candidates, - query, - false, - true, - MAX_MATCHES, - &Default::default(), - cx.background_executor().clone(), - )); - let mut external_matches = cx.background_executor().block(fuzzy::match_strings( - &self.external_match_candidates, - query, - false, - true, - MAX_MATCHES - visible_matches.len().min(MAX_MATCHES), - &Default::default(), - cx.background_executor().clone(), - )); - let sort_key_for_match = |mat: &StringMatch| { - let symbol = &self.symbols[mat.candidate_id]; - (Reverse(OrderedFloat(mat.score)), symbol.label.filter_text()) - }; - - visible_matches.sort_unstable_by_key(sort_key_for_match); - external_matches.sort_unstable_by_key(sort_key_for_match); - let mut matches = visible_matches; - matches.append(&mut external_matches); - - for mat in &mut matches { - let symbol = &self.symbols[mat.candidate_id]; - let filter_start = symbol.label.filter_range.start; - for position in &mut mat.positions { - *position += filter_start; - } - } - - self.matches = matches; - self.set_selected_index(0, window, cx); - } -} - -impl PickerDelegate for ProjectSymbolsDelegate { - type ListItem = ListItem; - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Search project symbols...".into() - } - - fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) { - if let Some(symbol) = self - .matches - .get(self.selected_match_index) - .map(|mat| self.symbols[mat.candidate_id].clone()) - { - let buffer = self.project.update(cx, |project, cx| { - project.open_buffer_for_symbol(&symbol, cx) - }); - let symbol = symbol.clone(); - let workspace = self.workspace.clone(); - cx.spawn_in(window, async move |_, cx| { - let buffer = buffer.await?; - workspace.update_in(cx, |workspace, window, cx| { - let position = buffer - .read(cx) - .clip_point_utf16(symbol.range.start, Bias::Left); - let pane = if secondary { - workspace.adjacent_pane(window, cx) - } else { - workspace.active_pane().clone() - }; - - let editor = workspace.open_project_item::( - pane, buffer, true, true, true, true, window, cx, - ); - - editor.update(cx, |editor, cx| { - editor.change_selections( - SelectionEffects::scroll(Autoscroll::center()), - window, - cx, - |s| s.select_ranges([position..position]), - ); - }); - })?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - cx.emit(DismissEvent); - } - } - - fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context>) {} - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_match_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _window: &mut Window, - _cx: &mut Context>, - ) { - self.selected_match_index = ix; - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - self.filter(&query, window, cx); - self.show_worktree_root_name = self.project.read(cx).visible_worktrees(cx).count() > 1; - let symbols = self - .project - .update(cx, |project, cx| project.symbols(&query, cx)); - cx.spawn_in(window, async move |this, cx| { - let symbols = symbols.await.log_err(); - if let Some(symbols) = symbols { - this.update_in(cx, |this, window, cx| { - let delegate = &mut this.delegate; - let project = delegate.project.read(cx); - let (visible_match_candidates, external_match_candidates) = symbols - .iter() - .enumerate() - .map(|(id, symbol)| { - StringMatchCandidate::new(id, symbol.label.filter_text()) - }) - .partition(|candidate| { - if let SymbolLocation::InProject(path) = &symbols[candidate.id].path { - project - .entry_for_path(path, cx) - .is_some_and(|e| !e.is_ignored) - } else { - false - } - }); - - delegate.visible_match_candidates = visible_match_candidates; - delegate.external_match_candidates = external_match_candidates; - delegate.symbols = symbols; - delegate.filter(&query, window, cx); - }) - .log_err(); - } - }) - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _window: &mut Window, - cx: &mut Context>, - ) -> Option { - let path_style = self.project.read(cx).path_style(cx); - let string_match = &self.matches.get(ix)?; - let symbol = &self.symbols.get(string_match.candidate_id)?; - let theme = cx.theme(); - let local_player = theme.players().local(); - let syntax_runs = styled_runs_for_code_label(&symbol.label, theme.syntax(), &local_player); - - let path = match &symbol.path { - SymbolLocation::InProject(project_path) => { - let project = self.project.read(cx); - let mut path = project_path.path.clone(); - if self.show_worktree_root_name - && let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx) - { - path = worktree.read(cx).root_name().join(&path); - } - path.display(path_style).into_owned().into() - } - SymbolLocation::OutsideProject { - abs_path, - signature: _, - } => abs_path.to_string_lossy(), - }; - let label = symbol.label.text.clone(); - let path = path.to_string(); - - let settings = ThemeSettings::get_global(cx); - - let text_style = TextStyle { - color: cx.theme().colors().text, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: settings.buffer_font_size(cx).into(), - font_weight: settings.buffer_font.weight, - line_height: relative(1.), - ..Default::default() - }; - - let highlight_style = HighlightStyle { - background_color: Some(cx.theme().colors().text_accent.alpha(0.3)), - ..Default::default() - }; - let custom_highlights = string_match - .positions - .iter() - .map(|pos| (*pos..pos + 1, highlight_style)); - - let highlights = gpui::combine_highlights(custom_highlights, syntax_runs); - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child( - v_flex() - .child(LabelLike::new().child( - StyledText::new(label).with_default_highlights(&text_style, highlights), - )) - .child(Label::new(path).size(LabelSize::Small).color(Color::Muted)), - ), - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use futures::StreamExt; - use gpui::{TestAppContext, VisualContext}; - use language::{FakeLspAdapter, Language, LanguageConfig, LanguageMatcher}; - use lsp::OneOf; - use project::FakeFs; - use serde_json::json; - use settings::SettingsStore; - use std::{path::Path, sync::Arc}; - use util::path; - - #[gpui::test] - async fn test_project_symbols(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({ "test.rs": "" })) - .await; - - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - - let language_registry = project.read_with(cx, |project, _| project.languages().clone()); - language_registry.add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - None, - ))); - let mut fake_servers = language_registry.register_fake_lsp( - "Rust", - FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - workspace_symbol_provider: Some(OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - }, - ); - - let _buffer = project - .update(cx, |project, cx| { - project.open_local_buffer_with_lsp(path!("/dir/test.rs"), cx) - }) - .await - .unwrap(); - - // Set up fake language server to return fuzzy matches against - // a fixed set of symbol names. - let fake_symbols = [ - symbol("one", path!("/external")), - symbol("ton", path!("/dir/test.rs")), - symbol("uno", path!("/dir/test.rs")), - ]; - let fake_server = fake_servers.next().await.unwrap(); - fake_server.set_request_handler::( - move |params: lsp::WorkspaceSymbolParams, cx| { - let executor = cx.background_executor().clone(); - let fake_symbols = fake_symbols.clone(); - async move { - let candidates = fake_symbols - .iter() - .enumerate() - .map(|(id, symbol)| StringMatchCandidate::new(id, &symbol.name)) - .collect::>(); - let matches = if params.query.is_empty() { - Vec::new() - } else { - fuzzy::match_strings( - &candidates, - ¶ms.query, - true, - true, - 100, - &Default::default(), - executor.clone(), - ) - .await - }; - - Ok(Some(lsp::WorkspaceSymbolResponse::Flat( - matches - .into_iter() - .map(|mat| fake_symbols[mat.candidate_id].clone()) - .collect(), - ))) - } - }, - ); - - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - // Create the project symbols view. - let symbols = cx.new_window_entity(|window, cx| { - Picker::uniform_list( - ProjectSymbolsDelegate::new(workspace.downgrade(), project.clone()), - window, - cx, - ) - }); - - // Spawn multiples updates before the first update completes, - // such that in the end, there are no matches. Testing for regression: - // https://github.com/zed-industries/zed/issues/861 - symbols.update_in(cx, |p, window, cx| { - p.update_matches("o".to_string(), window, cx); - p.update_matches("on".to_string(), window, cx); - p.update_matches("onex".to_string(), window, cx); - }); - - cx.run_until_parked(); - symbols.read_with(cx, |symbols, _| { - assert_eq!(symbols.delegate.matches.len(), 0); - }); - - // Spawn more updates such that in the end, there are matches. - symbols.update_in(cx, |p, window, cx| { - p.update_matches("one".to_string(), window, cx); - p.update_matches("on".to_string(), window, cx); - }); - - cx.run_until_parked(); - symbols.read_with(cx, |symbols, _| { - let delegate = &symbols.delegate; - assert_eq!(delegate.matches.len(), 2); - assert_eq!(delegate.matches[0].string, "ton"); - assert_eq!(delegate.matches[1].string, "one"); - }); - - // Spawn more updates such that in the end, there are again no matches. - symbols.update_in(cx, |p, window, cx| { - p.update_matches("o".to_string(), window, cx); - p.update_matches("".to_string(), window, cx); - }); - - cx.run_until_parked(); - symbols.read_with(cx, |symbols, _| { - assert_eq!(symbols.delegate.matches.len(), 0); - }); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let store = SettingsStore::test(cx); - cx.set_global(store); - theme::init(theme::LoadThemes::JustBase, cx); - release_channel::init(semver::Version::new(0, 0, 0), cx); - editor::init(cx); - }); - } - - fn symbol(name: &str, path: impl AsRef) -> lsp::SymbolInformation { - #[allow(deprecated)] - lsp::SymbolInformation { - name: name.to_string(), - kind: lsp::SymbolKind::FUNCTION, - tags: None, - deprecated: None, - container_name: None, - location: lsp::Location::new( - lsp::Uri::from_file_path(path.as_ref()).unwrap(), - lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), - ), - } - } -} diff --git a/crates/prompt_store/Cargo.toml b/crates/prompt_store/Cargo.toml deleted file mode 100644 index 13bacbfad3..0000000000 --- a/crates/prompt_store/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "prompt_store" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/prompt_store.rs" - -[dependencies] -anyhow.workspace = true -assets.workspace = true -chrono.workspace = true -collections.workspace = true -fs.workspace = true -futures.workspace = true -fuzzy.workspace = true -gpui.workspace = true -handlebars.workspace = true -heed.workspace = true -language.workspace = true -log.workspace = true -parking_lot.workspace = true -paths.workspace = true -rope.workspace = true -serde.workspace = true -text.workspace = true -util.workspace = true -uuid.workspace = true diff --git a/crates/prompt_store/LICENSE-GPL b/crates/prompt_store/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/prompt_store/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/prompt_store/src/prompt_store.rs b/crates/prompt_store/src/prompt_store.rs deleted file mode 100644 index fb087ce34d..0000000000 --- a/crates/prompt_store/src/prompt_store.rs +++ /dev/null @@ -1,471 +0,0 @@ -mod prompts; - -use anyhow::{Context as _, Result, anyhow}; -use chrono::{DateTime, Utc}; -use collections::HashMap; -use futures::FutureExt as _; -use futures::future::Shared; -use fuzzy::StringMatchCandidate; -use gpui::{ - App, AppContext, Context, Entity, EventEmitter, Global, ReadGlobal, SharedString, Task, -}; -use heed::{ - Database, RoTxn, - types::{SerdeBincode, SerdeJson, Str}, -}; -use parking_lot::RwLock; -pub use prompts::*; -use rope::Rope; -use serde::{Deserialize, Serialize}; -use std::{ - cmp::Reverse, - future::Future, - path::PathBuf, - sync::{Arc, atomic::AtomicBool}, -}; -use text::LineEnding; -use util::ResultExt; -use uuid::Uuid; - -/// Init starts loading the PromptStore in the background and assigns -/// a shared future to a global. -pub fn init(cx: &mut App) { - let db_path = paths::prompts_dir().join("prompts-library-db.0.mdb"); - let prompt_store_task = PromptStore::new(db_path, cx); - let prompt_store_entity_task = cx - .spawn(async move |cx| { - prompt_store_task - .await - .and_then(|prompt_store| cx.new(|_cx| prompt_store)) - .map_err(Arc::new) - }) - .shared(); - cx.set_global(GlobalPromptStore(prompt_store_entity_task)) -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct PromptMetadata { - pub id: PromptId, - pub title: Option, - pub default: bool, - pub saved_at: DateTime, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(tag = "kind")] -pub enum PromptId { - User { uuid: UserPromptId }, - EditWorkflow, -} - -impl PromptId { - pub fn new() -> PromptId { - UserPromptId::new().into() - } - - pub fn is_built_in(&self) -> bool { - !matches!(self, PromptId::User { .. }) - } -} - -impl From for PromptId { - fn from(uuid: UserPromptId) -> Self { - PromptId::User { uuid } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(transparent)] -pub struct UserPromptId(pub Uuid); - -impl UserPromptId { - pub fn new() -> UserPromptId { - UserPromptId(Uuid::new_v4()) - } -} - -impl From for UserPromptId { - fn from(uuid: Uuid) -> Self { - UserPromptId(uuid) - } -} - -impl std::fmt::Display for PromptId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PromptId::User { uuid } => write!(f, "{}", uuid.0), - PromptId::EditWorkflow => write!(f, "Edit workflow"), - } - } -} - -pub struct PromptStore { - env: heed::Env, - metadata_cache: RwLock, - metadata: Database, SerdeJson>, - bodies: Database, Str>, -} - -pub struct PromptsUpdatedEvent; - -impl EventEmitter for PromptStore {} - -#[derive(Default)] -struct MetadataCache { - metadata: Vec, - metadata_by_id: HashMap, -} - -impl MetadataCache { - fn from_db( - db: Database, SerdeJson>, - txn: &RoTxn, - ) -> Result { - let mut cache = MetadataCache::default(); - for result in db.iter(txn)? { - let (prompt_id, metadata) = result?; - cache.metadata.push(metadata.clone()); - cache.metadata_by_id.insert(prompt_id, metadata); - } - cache.sort(); - Ok(cache) - } - - fn insert(&mut self, metadata: PromptMetadata) { - self.metadata_by_id.insert(metadata.id, metadata.clone()); - if let Some(old_metadata) = self.metadata.iter_mut().find(|m| m.id == metadata.id) { - *old_metadata = metadata; - } else { - self.metadata.push(metadata); - } - self.sort(); - } - - fn remove(&mut self, id: PromptId) { - self.metadata.retain(|metadata| metadata.id != id); - self.metadata_by_id.remove(&id); - } - - fn sort(&mut self) { - self.metadata.sort_unstable_by(|a, b| { - a.title - .cmp(&b.title) - .then_with(|| b.saved_at.cmp(&a.saved_at)) - }); - } -} - -impl PromptStore { - pub fn global(cx: &App) -> impl Future>> + use<> { - let store = GlobalPromptStore::global(cx).0.clone(); - async move { store.await.map_err(|err| anyhow!(err)) } - } - - pub fn new(db_path: PathBuf, cx: &App) -> Task> { - cx.background_spawn(async move { - std::fs::create_dir_all(&db_path)?; - - let db_env = unsafe { - heed::EnvOpenOptions::new() - .map_size(1024 * 1024 * 1024) // 1GB - .max_dbs(4) // Metadata and bodies (possibly v1 of both as well) - .open(db_path)? - }; - - let mut txn = db_env.write_txn()?; - let metadata = db_env.create_database(&mut txn, Some("metadata.v2"))?; - let bodies = db_env.create_database(&mut txn, Some("bodies.v2"))?; - - // Remove edit workflow prompt, as we decided to opt into it using - // a slash command instead. - metadata.delete(&mut txn, &PromptId::EditWorkflow).ok(); - bodies.delete(&mut txn, &PromptId::EditWorkflow).ok(); - - txn.commit()?; - - Self::upgrade_dbs(&db_env, metadata, bodies).log_err(); - - let txn = db_env.read_txn()?; - let metadata_cache = MetadataCache::from_db(metadata, &txn)?; - txn.commit()?; - - Ok(PromptStore { - env: db_env, - metadata_cache: RwLock::new(metadata_cache), - metadata, - bodies, - }) - }) - } - - fn upgrade_dbs( - env: &heed::Env, - metadata_db: heed::Database, SerdeJson>, - bodies_db: heed::Database, Str>, - ) -> Result<()> { - #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)] - pub struct PromptIdV1(Uuid); - - #[derive(Clone, Debug, Serialize, Deserialize)] - pub struct PromptMetadataV1 { - pub id: PromptIdV1, - pub title: Option, - pub default: bool, - pub saved_at: DateTime, - } - - let mut txn = env.write_txn()?; - let Some(bodies_v1_db) = env - .open_database::, SerdeBincode>( - &txn, - Some("bodies"), - )? - else { - return Ok(()); - }; - let mut bodies_v1 = bodies_v1_db - .iter(&txn)? - .collect::>>()?; - - let Some(metadata_v1_db) = env - .open_database::, SerdeBincode>( - &txn, - Some("metadata"), - )? - else { - return Ok(()); - }; - let metadata_v1 = metadata_v1_db - .iter(&txn)? - .collect::>>()?; - - for (prompt_id_v1, metadata_v1) in metadata_v1 { - let prompt_id_v2 = UserPromptId(prompt_id_v1.0).into(); - let Some(body_v1) = bodies_v1.remove(&prompt_id_v1) else { - continue; - }; - - if metadata_db - .get(&txn, &prompt_id_v2)? - .is_none_or(|metadata_v2| metadata_v1.saved_at > metadata_v2.saved_at) - { - metadata_db.put( - &mut txn, - &prompt_id_v2, - &PromptMetadata { - id: prompt_id_v2, - title: metadata_v1.title.clone(), - default: metadata_v1.default, - saved_at: metadata_v1.saved_at, - }, - )?; - bodies_db.put(&mut txn, &prompt_id_v2, &body_v1)?; - } - } - - txn.commit()?; - - Ok(()) - } - - pub fn load(&self, id: PromptId, cx: &App) -> Task> { - let env = self.env.clone(); - let bodies = self.bodies; - cx.background_spawn(async move { - let txn = env.read_txn()?; - let mut prompt = bodies.get(&txn, &id)?.context("prompt not found")?.into(); - LineEnding::normalize(&mut prompt); - Ok(prompt) - }) - } - - pub fn all_prompt_metadata(&self) -> Vec { - self.metadata_cache.read().metadata.clone() - } - - pub fn default_prompt_metadata(&self) -> Vec { - return self - .metadata_cache - .read() - .metadata - .iter() - .filter(|metadata| metadata.default) - .cloned() - .collect::>(); - } - - pub fn delete(&self, id: PromptId, cx: &Context) -> Task> { - self.metadata_cache.write().remove(id); - - let db_connection = self.env.clone(); - let bodies = self.bodies; - let metadata = self.metadata; - - let task = cx.background_spawn(async move { - let mut txn = db_connection.write_txn()?; - - metadata.delete(&mut txn, &id)?; - bodies.delete(&mut txn, &id)?; - - txn.commit()?; - anyhow::Ok(()) - }); - - cx.spawn(async move |this, cx| { - task.await?; - this.update(cx, |_, cx| cx.emit(PromptsUpdatedEvent)).ok(); - anyhow::Ok(()) - }) - } - - /// Returns the number of prompts in the store. - pub fn prompt_count(&self) -> usize { - self.metadata_cache.read().metadata.len() - } - - pub fn metadata(&self, id: PromptId) -> Option { - self.metadata_cache.read().metadata_by_id.get(&id).cloned() - } - - pub fn first(&self) -> Option { - self.metadata_cache.read().metadata.first().cloned() - } - - pub fn id_for_title(&self, title: &str) -> Option { - let metadata_cache = self.metadata_cache.read(); - let metadata = metadata_cache - .metadata - .iter() - .find(|metadata| metadata.title.as_ref().map(|title| &***title) == Some(title))?; - Some(metadata.id) - } - - pub fn search( - &self, - query: String, - cancellation_flag: Arc, - cx: &App, - ) -> Task> { - let cached_metadata = self.metadata_cache.read().metadata.clone(); - let executor = cx.background_executor().clone(); - cx.background_spawn(async move { - let mut matches = if query.is_empty() { - cached_metadata - } else { - let candidates = cached_metadata - .iter() - .enumerate() - .filter_map(|(ix, metadata)| { - Some(StringMatchCandidate::new(ix, metadata.title.as_ref()?)) - }) - .collect::>(); - let matches = fuzzy::match_strings( - &candidates, - &query, - false, - true, - 100, - &cancellation_flag, - executor, - ) - .await; - matches - .into_iter() - .map(|mat| cached_metadata[mat.candidate_id].clone()) - .collect() - }; - matches.sort_by_key(|metadata| Reverse(metadata.default)); - matches - }) - } - - pub fn save( - &self, - id: PromptId, - title: Option, - default: bool, - body: Rope, - cx: &Context, - ) -> Task> { - if id.is_built_in() { - return Task::ready(Err(anyhow!("built-in prompts cannot be saved"))); - } - - let prompt_metadata = PromptMetadata { - id, - title, - default, - saved_at: Utc::now(), - }; - self.metadata_cache.write().insert(prompt_metadata.clone()); - - let db_connection = self.env.clone(); - let bodies = self.bodies; - let metadata = self.metadata; - - let task = cx.background_spawn(async move { - let mut txn = db_connection.write_txn()?; - - metadata.put(&mut txn, &id, &prompt_metadata)?; - bodies.put(&mut txn, &id, &body.to_string())?; - - txn.commit()?; - - anyhow::Ok(()) - }); - - cx.spawn(async move |this, cx| { - task.await?; - this.update(cx, |_, cx| cx.emit(PromptsUpdatedEvent)).ok(); - anyhow::Ok(()) - }) - } - - pub fn save_metadata( - &self, - id: PromptId, - mut title: Option, - default: bool, - cx: &Context, - ) -> Task> { - let mut cache = self.metadata_cache.write(); - - if id.is_built_in() { - title = cache - .metadata_by_id - .get(&id) - .and_then(|metadata| metadata.title.clone()); - } - - let prompt_metadata = PromptMetadata { - id, - title, - default, - saved_at: Utc::now(), - }; - - cache.insert(prompt_metadata.clone()); - - let db_connection = self.env.clone(); - let metadata = self.metadata; - - let task = cx.background_spawn(async move { - let mut txn = db_connection.write_txn()?; - metadata.put(&mut txn, &id, &prompt_metadata)?; - txn.commit()?; - - anyhow::Ok(()) - }); - - cx.spawn(async move |this, cx| { - task.await?; - this.update(cx, |_, cx| cx.emit(PromptsUpdatedEvent)).ok(); - anyhow::Ok(()) - }) - } -} - -/// Wraps a shared future to a prompt store so it can be assigned as a context global. -pub struct GlobalPromptStore(Shared, Arc>>>); - -impl Global for GlobalPromptStore {} diff --git a/crates/prompt_store/src/prompts.rs b/crates/prompt_store/src/prompts.rs deleted file mode 100644 index d6a172218a..0000000000 --- a/crates/prompt_store/src/prompts.rs +++ /dev/null @@ -1,476 +0,0 @@ -use anyhow::Result; -use assets::Assets; -use fs::Fs; -use futures::StreamExt; -use gpui::{App, AppContext as _, AssetSource}; -use handlebars::{Handlebars, RenderError}; -use language::{BufferSnapshot, LanguageName, Point}; -use parking_lot::Mutex; -use serde::Serialize; -use std::{ - ops::Range, - path::{Path, PathBuf}, - sync::Arc, - time::Duration, -}; -use text::LineEnding; -use util::{ - ResultExt, get_default_system_shell_preferring_bash, rel_path::RelPath, shell::ShellKind, -}; - -use crate::UserPromptId; - -#[derive(Default, Debug, Clone, Serialize)] -pub struct ProjectContext { - pub worktrees: Vec, - /// Whether any worktree has a rules_file. Provided as a field because handlebars can't do this. - pub has_rules: bool, - pub user_rules: Vec, - /// `!user_rules.is_empty()` - provided as a field because handlebars can't do this. - pub has_user_rules: bool, - pub os: String, - pub arch: String, - pub shell: String, -} - -impl ProjectContext { - pub fn new(worktrees: Vec, default_user_rules: Vec) -> Self { - let has_rules = worktrees - .iter() - .any(|worktree| worktree.rules_file.is_some()); - Self { - worktrees, - has_rules, - has_user_rules: !default_user_rules.is_empty(), - user_rules: default_user_rules, - os: std::env::consts::OS.to_string(), - arch: std::env::consts::ARCH.to_string(), - shell: ShellKind::new(&get_default_system_shell_preferring_bash(), cfg!(windows)) - .to_string(), - } - } -} - -#[derive(Debug, Clone, Serialize)] -pub struct UserRulesContext { - pub uuid: UserPromptId, - pub title: Option, - pub contents: String, -} - -#[derive(Debug, Clone, Eq, PartialEq, Serialize)] -pub struct WorktreeContext { - pub root_name: String, - pub abs_path: Arc, - pub rules_file: Option, -} - -#[derive(Debug, Clone, Eq, PartialEq, Serialize)] -pub struct RulesFileContext { - pub path_in_worktree: Arc, - pub text: String, - // This used for opening rules files. TODO: Since it isn't related to prompt templating, this - // should be moved elsewhere. - #[serde(skip)] - pub project_entry_id: usize, -} - -#[derive(Serialize)] -pub struct ContentPromptDiagnosticContext { - pub line_number: usize, - pub error_message: String, - pub code_content: String, -} - -#[derive(Serialize)] -pub struct ContentPromptContext { - pub content_type: String, - pub language_name: Option, - pub is_insert: bool, - pub is_truncated: bool, - pub document_content: String, - pub user_prompt: String, - pub rewrite_section: Option, - pub diagnostic_errors: Vec, -} - -#[derive(Serialize)] -pub struct ContentPromptContextV2 { - pub content_type: String, - pub language_name: Option, - pub is_truncated: bool, - pub document_content: String, - pub rewrite_section: Option, - pub diagnostic_errors: Vec, -} - -#[derive(Serialize)] -pub struct TerminalAssistantPromptContext { - pub os: String, - pub arch: String, - pub shell: Option, - pub working_directory: Option, - pub latest_output: Vec, - pub user_prompt: String, -} - -pub struct PromptLoadingParams<'a> { - pub fs: Arc, - pub repo_path: Option, - pub cx: &'a gpui::App, -} - -pub struct PromptBuilder { - handlebars: Arc>>, -} - -impl PromptBuilder { - pub fn load(fs: Arc, stdout_is_a_pty: bool, cx: &mut App) -> Arc { - Self::new(Some(PromptLoadingParams { - fs: fs.clone(), - repo_path: stdout_is_a_pty - .then(|| std::env::current_dir().log_err()) - .flatten(), - cx, - })) - .log_err() - .map(Arc::new) - .unwrap_or_else(|| Arc::new(Self::new(None).unwrap())) - } - - pub fn new(loading_params: Option) -> Result { - let mut handlebars = Handlebars::new(); - Self::register_built_in_templates(&mut handlebars)?; - - let handlebars = Arc::new(Mutex::new(handlebars)); - - if let Some(params) = loading_params { - Self::watch_fs_for_template_overrides(params, handlebars.clone()); - } - - Ok(Self { handlebars }) - } - - /// Watches the filesystem for changes to prompt template overrides. - /// - /// This function sets up a file watcher on the prompt templates directory. It performs - /// an initial scan of the directory and registers any existing template overrides. - /// Then it continuously monitors for changes, reloading templates as they are - /// modified or added. - /// - /// If the templates directory doesn't exist initially, it waits for it to be created. - /// If the directory is removed, it restores the built-in templates and waits for the - /// directory to be recreated. - /// - /// # Arguments - /// - /// * `params` - A `PromptLoadingParams` struct containing the filesystem, repository path, - /// and application context. - /// * `handlebars` - An `Arc>` for registering and updating templates. - fn watch_fs_for_template_overrides( - params: PromptLoadingParams, - handlebars: Arc>>, - ) { - let templates_dir = paths::prompt_overrides_dir(params.repo_path.as_deref()); - params.cx.background_spawn(async move { - let Some(parent_dir) = templates_dir.parent() else { - return; - }; - - let mut found_dir_once = false; - loop { - // Check if the templates directory exists and handle its status - // If it exists, log its presence and check if it's a symlink - // If it doesn't exist: - // - Log that we're using built-in prompts - // - Check if it's a broken symlink and log if so - // - Set up a watcher to detect when it's created - // After the first check, set the `found_dir_once` flag - // This allows us to avoid logging when looping back around after deleting the prompt overrides directory. - let dir_status = params.fs.is_dir(&templates_dir).await; - let symlink_status = params.fs.read_link(&templates_dir).await.ok(); - if dir_status { - let mut log_message = format!("Prompt template overrides directory found at {}", templates_dir.display()); - if let Some(target) = symlink_status { - log_message.push_str(" -> "); - log_message.push_str(&target.display().to_string()); - } - log::trace!("{}.", log_message); - } else { - if !found_dir_once { - log::trace!("No prompt template overrides directory found at {}. Using built-in prompts.", templates_dir.display()); - if let Some(target) = symlink_status { - log::trace!("Symlink found pointing to {}, but target is invalid.", target.display()); - } - } - - if params.fs.is_dir(parent_dir).await { - let (mut changes, _watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await; - while let Some(changed_paths) = changes.next().await { - if changed_paths.iter().any(|p| &p.path == &templates_dir) { - let mut log_message = format!("Prompt template overrides directory detected at {}", templates_dir.display()); - if let Ok(target) = params.fs.read_link(&templates_dir).await { - log_message.push_str(" -> "); - log_message.push_str(&target.display().to_string()); - } - log::trace!("{}.", log_message); - break; - } - } - } else { - return; - } - } - - found_dir_once = true; - - // Initial scan of the prompt overrides directory - if let Ok(mut entries) = params.fs.read_dir(&templates_dir).await { - while let Some(Ok(file_path)) = entries.next().await { - if file_path.to_string_lossy().ends_with(".hbs") - && let Ok(content) = params.fs.load(&file_path).await { - let file_name = file_path.file_stem().unwrap().to_string_lossy(); - log::debug!("Registering prompt template override: {}", file_name); - handlebars.lock().register_template_string(&file_name, content).log_err(); - } - } - } - - // Watch both the parent directory and the template overrides directory: - // - Monitor the parent directory to detect if the template overrides directory is deleted. - // - Monitor the template overrides directory to re-register templates when they change. - // Combine both watch streams into a single stream. - let (parent_changes, parent_watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await; - let (changes, watcher) = params.fs.watch(&templates_dir, Duration::from_secs(1)).await; - let mut combined_changes = futures::stream::select(changes, parent_changes); - - while let Some(changed_paths) = combined_changes.next().await { - if changed_paths.iter().any(|p| &p.path == &templates_dir) - && !params.fs.is_dir(&templates_dir).await { - log::info!("Prompt template overrides directory removed. Restoring built-in prompt templates."); - Self::register_built_in_templates(&mut handlebars.lock()).log_err(); - break; - } - for event in changed_paths { - if event.path.starts_with(&templates_dir) && event.path.extension().is_some_and(|ext| ext == "hbs") { - log::info!("Reloading prompt template override: {}", event.path.display()); - if let Some(content) = params.fs.load(&event.path).await.log_err() { - let file_name = event.path.file_stem().unwrap().to_string_lossy(); - handlebars.lock().register_template_string(&file_name, content).log_err(); - } - } - } - } - - drop(watcher); - drop(parent_watcher); - } - }) - .detach(); - } - - fn register_built_in_templates(handlebars: &mut Handlebars) -> Result<()> { - for path in Assets.list("prompts")? { - if let Some(id) = path - .split('/') - .next_back() - .and_then(|s| s.strip_suffix(".hbs")) - && let Some(prompt) = Assets.load(path.as_ref()).log_err().flatten() - { - log::debug!("Registering built-in prompt template: {}", id); - let prompt = String::from_utf8_lossy(prompt.as_ref()); - handlebars.register_template_string(id, LineEnding::normalize_cow(prompt))? - } - } - - Ok(()) - } - - pub fn generate_inline_transformation_prompt_v2( - &self, - language_name: Option<&LanguageName>, - buffer: BufferSnapshot, - range: Range, - ) -> Result { - let content_type = match language_name.as_ref().map(|l| l.as_ref()) { - None | Some("Markdown" | "Plain Text") => "text", - Some(_) => "code", - }; - - const MAX_CTX: usize = 50000; - let is_insert = range.is_empty(); - let mut is_truncated = false; - - let before_range = 0..range.start; - let truncated_before = if before_range.len() > MAX_CTX { - is_truncated = true; - let start = buffer.clip_offset(range.start - MAX_CTX, text::Bias::Right); - start..range.start - } else { - before_range - }; - - let after_range = range.end..buffer.len(); - let truncated_after = if after_range.len() > MAX_CTX { - is_truncated = true; - let end = buffer.clip_offset(range.end + MAX_CTX, text::Bias::Left); - range.end..end - } else { - after_range - }; - - let mut document_content = String::new(); - for chunk in buffer.text_for_range(truncated_before) { - document_content.push_str(chunk); - } - if is_insert { - document_content.push_str(""); - } else { - document_content.push_str("\n"); - for chunk in buffer.text_for_range(range.clone()) { - document_content.push_str(chunk); - } - document_content.push_str("\n"); - } - for chunk in buffer.text_for_range(truncated_after) { - document_content.push_str(chunk); - } - - let rewrite_section = if !is_insert { - let mut section = String::new(); - for chunk in buffer.text_for_range(range.clone()) { - section.push_str(chunk); - } - Some(section) - } else { - None - }; - let diagnostics = buffer.diagnostics_in_range::<_, Point>(range, false); - let diagnostic_errors: Vec = diagnostics - .map(|entry| { - let start = entry.range.start; - ContentPromptDiagnosticContext { - line_number: (start.row + 1) as usize, - error_message: entry.diagnostic.message.clone(), - code_content: buffer.text_for_range(entry.range).collect(), - } - }) - .collect(); - - let context = ContentPromptContextV2 { - content_type: content_type.to_string(), - language_name: language_name.map(|s| s.to_string()), - is_truncated, - document_content, - rewrite_section, - diagnostic_errors, - }; - self.handlebars.lock().render("content_prompt_v2", &context) - } - - pub fn generate_inline_transformation_prompt( - &self, - user_prompt: String, - language_name: Option<&LanguageName>, - buffer: BufferSnapshot, - range: Range, - ) -> Result { - let content_type = match language_name.as_ref().map(|l| l.as_ref()) { - None | Some("Markdown" | "Plain Text") => "text", - Some(_) => "code", - }; - - const MAX_CTX: usize = 50000; - let is_insert = range.is_empty(); - let mut is_truncated = false; - - let before_range = 0..range.start; - let truncated_before = if before_range.len() > MAX_CTX { - is_truncated = true; - let start = buffer.clip_offset(range.start - MAX_CTX, text::Bias::Right); - start..range.start - } else { - before_range - }; - - let after_range = range.end..buffer.len(); - let truncated_after = if after_range.len() > MAX_CTX { - is_truncated = true; - let end = buffer.clip_offset(range.end + MAX_CTX, text::Bias::Left); - range.end..end - } else { - after_range - }; - - let mut document_content = String::new(); - for chunk in buffer.text_for_range(truncated_before) { - document_content.push_str(chunk); - } - if is_insert { - document_content.push_str(""); - } else { - document_content.push_str("\n"); - for chunk in buffer.text_for_range(range.clone()) { - document_content.push_str(chunk); - } - document_content.push_str("\n"); - } - for chunk in buffer.text_for_range(truncated_after) { - document_content.push_str(chunk); - } - - let rewrite_section = if !is_insert { - let mut section = String::new(); - for chunk in buffer.text_for_range(range.clone()) { - section.push_str(chunk); - } - Some(section) - } else { - None - }; - let diagnostics = buffer.diagnostics_in_range::<_, Point>(range, false); - let diagnostic_errors: Vec = diagnostics - .map(|entry| { - let start = entry.range.start; - ContentPromptDiagnosticContext { - line_number: (start.row + 1) as usize, - error_message: entry.diagnostic.message.clone(), - code_content: buffer.text_for_range(entry.range).collect(), - } - }) - .collect(); - - let context = ContentPromptContext { - content_type: content_type.to_string(), - language_name: language_name.map(|s| s.to_string()), - is_insert, - is_truncated, - document_content, - user_prompt, - rewrite_section, - diagnostic_errors, - }; - self.handlebars.lock().render("content_prompt", &context) - } - - pub fn generate_terminal_assistant_prompt( - &self, - user_prompt: &str, - shell: Option<&str>, - working_directory: Option<&str>, - latest_output: &[String], - ) -> Result { - let context = TerminalAssistantPromptContext { - os: std::env::consts::OS.to_string(), - arch: std::env::consts::ARCH.to_string(), - shell: shell.map(|s| s.to_string()), - working_directory: working_directory.map(|s| s.to_string()), - latest_output: latest_output.to_vec(), - user_prompt: user_prompt.to_string(), - }; - - self.handlebars - .lock() - .render("terminal_assistant_prompt", &context) - } -} diff --git a/crates/proto/Cargo.toml b/crates/proto/Cargo.toml deleted file mode 100644 index 5b5b8b985c..0000000000 --- a/crates/proto/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -description = "Shared protocol for communication between the Zed app and the zed.dev server" -edition.workspace = true -name = "proto" -version = "0.1.0" -publish.workspace = true -license = "GPL-3.0-or-later" - -[features] -test-support = ["collections/test-support"] - -[lints] -workspace = true - -[lib] -path = "src/proto.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -prost.workspace = true -serde.workspace = true - -[build-dependencies] -prost-build.workspace = true - -[dev-dependencies] -collections = { workspace = true, features = ["test-support"] } -typed-path = "0.11" diff --git a/crates/proto/LICENSE-GPL b/crates/proto/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/proto/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/proto/build.rs b/crates/proto/build.rs deleted file mode 100644 index 184d0e53d5..0000000000 --- a/crates/proto/build.rs +++ /dev/null @@ -1,10 +0,0 @@ -fn main() { - println!("cargo:rerun-if-changed=proto"); - let mut build = prost_build::Config::new(); - build - .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") - .type_attribute("ProjectPath", "#[derive(Hash, Eq)]") - .type_attribute("Anchor", "#[derive(Hash, Eq)]") - .compile_protos(&["proto/zed.proto"], &["proto"]) - .unwrap(); -} diff --git a/crates/proto/proto/ai.proto b/crates/proto/proto/ai.proto deleted file mode 100644 index 2216446a82..0000000000 --- a/crates/proto/proto/ai.proto +++ /dev/null @@ -1,220 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -import "buffer.proto"; -import "task.proto"; - -message Context { - repeated ContextOperation operations = 1; -} - -message ContextMetadata { - string context_id = 1; - optional string summary = 2; -} - -message ContextMessageStatus { - oneof variant { - Done done = 1; - Pending pending = 2; - Error error = 3; - Canceled canceled = 4; - } - - message Done {} - - message Pending {} - - message Error { - string message = 1; - } - - message Canceled {} -} - -message ContextMessage { - LamportTimestamp id = 1; - Anchor start = 2; - LanguageModelRole role = 3; - ContextMessageStatus status = 4; -} - -message SlashCommandOutputSection { - AnchorRange range = 1; - string icon_name = 2; - string label = 3; - optional string metadata = 4; -} - -message ThoughtProcessOutputSection { - AnchorRange range = 1; -} - -message ContextOperation { - oneof variant { - InsertMessage insert_message = 1; - UpdateMessage update_message = 2; - UpdateSummary update_summary = 3; - BufferOperation buffer_operation = 5; - SlashCommandStarted slash_command_started = 6; - SlashCommandOutputSectionAdded slash_command_output_section_added = 7; - SlashCommandCompleted slash_command_completed = 8; - ThoughtProcessOutputSectionAdded thought_process_output_section_added = 9; - } - - reserved 4; - - message InsertMessage { - ContextMessage message = 1; - repeated VectorClockEntry version = 2; - } - - message UpdateMessage { - LamportTimestamp message_id = 1; - LanguageModelRole role = 2; - ContextMessageStatus status = 3; - LamportTimestamp timestamp = 4; - repeated VectorClockEntry version = 5; - } - - message UpdateSummary { - string summary = 1; - bool done = 2; - LamportTimestamp timestamp = 3; - repeated VectorClockEntry version = 4; - } - - message SlashCommandStarted { - LamportTimestamp id = 1; - AnchorRange output_range = 2; - string name = 3; - repeated VectorClockEntry version = 4; - } - - message SlashCommandOutputSectionAdded { - LamportTimestamp timestamp = 1; - SlashCommandOutputSection section = 2; - repeated VectorClockEntry version = 3; - } - - message SlashCommandCompleted { - LamportTimestamp id = 1; - LamportTimestamp timestamp = 3; - optional string error_message = 4; - repeated VectorClockEntry version = 5; - } - - message ThoughtProcessOutputSectionAdded { - LamportTimestamp timestamp = 1; - ThoughtProcessOutputSection section = 2; - repeated VectorClockEntry version = 3; - } - - message BufferOperation { - Operation operation = 1; - } -} - -message AdvertiseContexts { - uint64 project_id = 1; - repeated ContextMetadata contexts = 2; -} - -message OpenContext { - uint64 project_id = 1; - string context_id = 2; -} - -message OpenContextResponse { - Context context = 1; -} - -message CreateContext { - uint64 project_id = 1; -} - -message CreateContextResponse { - string context_id = 1; - Context context = 2; -} - -message UpdateContext { - uint64 project_id = 1; - string context_id = 2; - ContextOperation operation = 3; -} - -message ContextVersion { - string context_id = 1; - repeated VectorClockEntry context_version = 2; - repeated VectorClockEntry buffer_version = 3; -} - -message SynchronizeContexts { - uint64 project_id = 1; - repeated ContextVersion contexts = 2; -} - -message SynchronizeContextsResponse { - repeated ContextVersion contexts = 1; -} - -enum LanguageModelRole { - LanguageModelUser = 0; - LanguageModelAssistant = 1; - LanguageModelSystem = 2; - reserved 3; -} - -message GetAgentServerCommand { - uint64 project_id = 1; - string name = 2; - optional string root_dir = 3; -} - -message AgentServerCommand { - string path = 1; - repeated string args = 2; - map env = 3; - string root_dir = 4; - - optional SpawnInTerminal login = 5; -} - -message ExternalAgentsUpdated { - uint64 project_id = 1; - repeated string names = 2; -} - -message ExternalExtensionAgentTarget { - string archive = 1; - string cmd = 2; - repeated string args = 3; - optional string sha256 = 4; - map env = 5; -} - -message ExternalExtensionAgent { - string name = 1; - optional string icon_path = 2; - string extension_id = 3; - map targets = 4; - map env = 5; -} - -message ExternalExtensionAgentsUpdated { - uint64 project_id = 1; - repeated ExternalExtensionAgent agents = 2; -} - -message ExternalAgentLoadingStatusUpdated { - uint64 project_id = 1; - string name = 2; - string status = 3; -} - -message NewExternalAgentVersionAvailable { - uint64 project_id = 1; - string name = 2; - string version = 3; -} diff --git a/crates/proto/proto/app.proto b/crates/proto/proto/app.proto deleted file mode 100644 index 889086e200..0000000000 --- a/crates/proto/proto/app.proto +++ /dev/null @@ -1,76 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -message UpdateInviteInfo { - string url = 1; - uint32 count = 2; -} - -message ShutdownRemoteServer {} - -message Toast { - uint64 project_id = 1; - string notification_id = 2; - string message = 3; -} - -message HideToast { - uint64 project_id = 1; - string notification_id = 2; -} - -message OpenServerSettings { - uint64 project_id = 1; -} - -message GetCrashFiles { -} - -message GetCrashFilesResponse { - repeated CrashReport crashes = 1; - reserved 2; // old panics -} - -message CrashReport { - reserved 1, 2; - string metadata = 3; - bytes minidump_contents = 4; -} - -message Extension { - string id = 1; - string version = 2; - bool dev = 3; -} - -message SyncExtensions { - repeated Extension extensions = 1; -} - -message SyncExtensionsResponse { - string tmp_dir = 1; - repeated Extension missing_extensions = 2; -} - -message InstallExtension { - Extension extension = 1; - string tmp_dir = 2; -} - -message AskPassRequest { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - uint64 askpass_id = 4; - string prompt = 5; -} - -message AskPassResponse { - string response = 1; -} - -message GetSupermavenApiKey {} - -message GetSupermavenApiKeyResponse { - string api_key = 1; -} diff --git a/crates/proto/proto/buf.yaml b/crates/proto/proto/buf.yaml deleted file mode 100644 index 93e819b2f7..0000000000 --- a/crates/proto/proto/buf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -version: v1 -breaking: - use: - - WIRE diff --git a/crates/proto/proto/buffer.proto b/crates/proto/proto/buffer.proto deleted file mode 100644 index 486716b36a..0000000000 --- a/crates/proto/proto/buffer.proto +++ /dev/null @@ -1,315 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -import "core.proto"; -import "worktree.proto"; - -message OpenNewBuffer { - uint64 project_id = 1; -} - -message OpenBufferResponse { - uint64 buffer_id = 1; -} - -message CreateBufferForPeer { - uint64 project_id = 1; - PeerId peer_id = 2; - oneof variant { - BufferState state = 3; - BufferChunk chunk = 4; - } -} - -message UpdateBuffer { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated Operation operations = 3; -} - -message OpenBufferByPath { - uint64 project_id = 1; - uint64 worktree_id = 2; - string path = 3; -} - -message OpenBufferById { - uint64 project_id = 1; - uint64 id = 2; -} - -message UpdateBufferFile { - uint64 project_id = 1; - uint64 buffer_id = 2; - File file = 3; -} - -message SaveBuffer { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated VectorClockEntry version = 3; - optional ProjectPath new_path = 4; -} - -message CloseBuffer { - uint64 project_id = 1; - uint64 buffer_id = 2; -} - -message BufferSaved { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated VectorClockEntry version = 3; - Timestamp mtime = 4; - reserved 5; -} - -message BufferReloaded { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated VectorClockEntry version = 3; - Timestamp mtime = 4; - reserved 5; - LineEnding line_ending = 6; -} - -message ReloadBuffers { - uint64 project_id = 1; - repeated uint64 buffer_ids = 2; -} - -message ReloadBuffersResponse { - ProjectTransaction transaction = 1; -} - -message SynchronizeBuffers { - uint64 project_id = 1; - repeated BufferVersion buffers = 2; -} - -message SynchronizeBuffersResponse { - repeated BufferVersion buffers = 1; -} - -message BufferVersion { - uint64 id = 1; - repeated VectorClockEntry version = 2; -} - -message BufferState { - uint64 id = 1; - optional File file = 2; - string base_text = 3; - LineEnding line_ending = 5; - repeated VectorClockEntry saved_version = 6; - Timestamp saved_mtime = 8; - - reserved 7; - reserved 4; -} - -message BufferChunk { - uint64 buffer_id = 1; - repeated Operation operations = 2; - bool is_last = 3; -} - -enum LineEnding { - Unix = 0; - Windows = 1; -} - -message VectorClockEntry { - uint32 replica_id = 1; - uint32 timestamp = 2; -} - -message UndoMapEntry { - uint32 replica_id = 1; - uint32 local_timestamp = 2; - repeated UndoCount counts = 3; -} - -message UndoCount { - uint32 replica_id = 1; - uint32 lamport_timestamp = 2; - uint32 count = 3; -} - -message Operation { - oneof variant { - Edit edit = 1; - Undo undo = 2; - UpdateSelections update_selections = 3; - UpdateDiagnostics update_diagnostics = 4; - UpdateCompletionTriggers update_completion_triggers = 5; - UpdateLineEnding update_line_ending = 6; - } - - message Edit { - uint32 replica_id = 1; - uint32 lamport_timestamp = 2; - repeated VectorClockEntry version = 3; - repeated Range ranges = 4; - repeated string new_text = 5; - } - - message Undo { - uint32 replica_id = 1; - uint32 lamport_timestamp = 2; - repeated VectorClockEntry version = 3; - repeated UndoCount counts = 4; - } - - message UpdateSelections { - uint32 replica_id = 1; - uint32 lamport_timestamp = 2; - repeated Selection selections = 3; - bool line_mode = 4; - CursorShape cursor_shape = 5; - } - - message UpdateCompletionTriggers { - uint32 replica_id = 1; - uint32 lamport_timestamp = 2; - repeated string triggers = 3; - uint64 language_server_id = 4; - } - - message UpdateLineEnding { - uint32 replica_id = 1; - uint32 lamport_timestamp = 2; - LineEnding line_ending = 3; - } -} - -message ProjectTransaction { - repeated uint64 buffer_ids = 1; - repeated Transaction transactions = 2; -} - -message Transaction { - LamportTimestamp id = 1; - repeated LamportTimestamp edit_ids = 2; - repeated VectorClockEntry start = 3; -} - -message LamportTimestamp { - uint32 replica_id = 1; - uint32 value = 2; -} - -message Range { - uint64 start = 1; - uint64 end = 2; -} - -message Selection { - uint64 id = 1; - EditorAnchor start = 2; - EditorAnchor end = 3; - bool reversed = 4; -} - -message EditorAnchor { - uint64 excerpt_id = 1; - Anchor anchor = 2; -} - -enum CursorShape { - CursorBar = 0; - CursorBlock = 1; - CursorUnderscore = 2; - CursorHollow = 3; -} - -message UpdateDiagnostics { - uint32 replica_id = 1; - uint32 lamport_timestamp = 2; - uint64 server_id = 3; - repeated Diagnostic diagnostics = 4; -} - -message Anchor { - uint32 replica_id = 1; - uint32 timestamp = 2; - uint64 offset = 3; - Bias bias = 4; - optional uint64 buffer_id = 5; -} - -message AnchorRange { - Anchor start = 1; - Anchor end = 2; -} - -message Location { - uint64 buffer_id = 1; - Anchor start = 2; - Anchor end = 3; -} - -enum Bias { - Left = 0; - Right = 1; -} - -message Diagnostic { - Anchor start = 1; - Anchor end = 2; - optional string source = 3; - optional string registration_id = 17; - - enum SourceKind { - Pulled = 0; - Pushed = 1; - Other = 2; - } - - SourceKind source_kind = 16; - Severity severity = 4; - string message = 5; - optional string code = 6; - uint64 group_id = 7; - bool is_primary = 8; - - reserved 9; - - bool is_disk_based = 10; - bool is_unnecessary = 11; - bool underline = 15; - - enum Severity { - None = 0; - Error = 1; - Warning = 2; - Information = 3; - Hint = 4; - } - optional string data = 12; - optional string code_description = 13; - optional string markdown = 14; -} - -message SearchQuery { - string query = 2; - bool regex = 3; - bool whole_word = 4; - bool case_sensitive = 5; - repeated string files_to_include = 10; - repeated string files_to_exclude = 11; - bool match_full_paths = 9; - bool include_ignored = 8; - string files_to_include_legacy = 6; - string files_to_exclude_legacy = 7; -} - -message FindSearchCandidates { - uint64 project_id = 1; - SearchQuery query = 2; - uint64 limit = 3; -} - -message FindSearchCandidatesResponse { - repeated uint64 buffer_ids = 1; -} diff --git a/crates/proto/proto/call.proto b/crates/proto/proto/call.proto deleted file mode 100644 index 9e801515af..0000000000 --- a/crates/proto/proto/call.proto +++ /dev/null @@ -1,427 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -import "core.proto"; -import "worktree.proto"; -import "buffer.proto"; -import "lsp.proto"; -import "channel.proto"; -import "git.proto"; - -message CreateRoom {} - -message CreateRoomResponse { - Room room = 1; - optional LiveKitConnectionInfo live_kit_connection_info = 2; -} - -message JoinRoom { - uint64 id = 1; -} - -message JoinRoomResponse { - Room room = 1; - optional uint64 channel_id = 2; - optional LiveKitConnectionInfo live_kit_connection_info = 3; -} - -message RejoinRoom { - uint64 id = 1; - repeated UpdateProject reshared_projects = 2; - repeated RejoinProject rejoined_projects = 3; -} - -message RejoinRemoteProjects { - repeated RejoinProject rejoined_projects = 1; -} - -message RejoinRemoteProjectsResponse { - repeated RejoinedProject rejoined_projects = 1; -} - -message RejoinProject { - uint64 id = 1; - repeated RejoinWorktree worktrees = 2; - repeated RejoinRepository repositories = 3; -} - -message RejoinWorktree { - uint64 id = 1; - uint64 scan_id = 2; -} - -message RejoinRepository { - uint64 id = 1; - uint64 scan_id = 2; -} - -message RejoinRoomResponse { - Room room = 1; - repeated ResharedProject reshared_projects = 2; - repeated RejoinedProject rejoined_projects = 3; -} - -message ResharedProject { - uint64 id = 1; - repeated Collaborator collaborators = 2; -} - -message RejoinedProject { - uint64 id = 1; - repeated WorktreeMetadata worktrees = 2; - repeated Collaborator collaborators = 3; - repeated LanguageServer language_servers = 4; - repeated string language_server_capabilities = 5; -} - -message LeaveRoom {} - -message Room { - uint64 id = 1; - repeated Participant participants = 2; - repeated PendingParticipant pending_participants = 3; - repeated Follower followers = 4; - string livekit_room = 5; -} - -message Participant { - uint64 user_id = 1; - PeerId peer_id = 2; - repeated ParticipantProject projects = 3; - ParticipantLocation location = 4; - uint32 participant_index = 5; - ChannelRole role = 6; - reserved 7; -} - -message PendingParticipant { - uint64 user_id = 1; - uint64 calling_user_id = 2; - optional uint64 initial_project_id = 3; -} - -message ParticipantProject { - uint64 id = 1; - repeated string worktree_root_names = 2; -} - -message Follower { - PeerId leader_id = 1; - PeerId follower_id = 2; - uint64 project_id = 3; -} - -message ParticipantLocation { - oneof variant { - SharedProject shared_project = 1; - UnsharedProject unshared_project = 2; - External external = 3; - } - - message SharedProject { - uint64 id = 1; - } - - message UnsharedProject {} - - message External {} -} - -message Call { - uint64 room_id = 1; - uint64 called_user_id = 2; - optional uint64 initial_project_id = 3; -} - -message IncomingCall { - uint64 room_id = 1; - uint64 calling_user_id = 2; - repeated uint64 participant_user_ids = 3; - optional ParticipantProject initial_project = 4; -} - -message CallCanceled { - uint64 room_id = 1; -} - -message CancelCall { - uint64 room_id = 1; - uint64 called_user_id = 2; -} - -message DeclineCall { - uint64 room_id = 1; -} - -message UpdateParticipantLocation { - uint64 room_id = 1; - ParticipantLocation location = 2; -} - -message RoomUpdated { - Room room = 1; -} - -message LiveKitConnectionInfo { - string server_url = 1; - string token = 2; - bool can_publish = 3; -} - -message ShareProject { - uint64 room_id = 1; - repeated WorktreeMetadata worktrees = 2; - reserved 3; - bool is_ssh_project = 4; - optional bool windows_paths = 5; -} - -message ShareProjectResponse { - uint64 project_id = 1; -} - -message UnshareProject { - uint64 project_id = 1; -} - -message UpdateProject { - uint64 project_id = 1; - repeated WorktreeMetadata worktrees = 2; -} - -message JoinProject { - uint64 project_id = 1; - optional string committer_email = 2; - optional string committer_name = 3; -} - -message JoinProjectResponse { - uint64 project_id = 5; - uint32 replica_id = 1; - repeated WorktreeMetadata worktrees = 2; - repeated Collaborator collaborators = 3; - repeated LanguageServer language_servers = 4; - repeated string language_server_capabilities = 8; - ChannelRole role = 6; - bool windows_paths = 9; - reserved 7; -} - -message LeaveProject { - uint64 project_id = 1; -} - -message UpdateWorktree { - uint64 project_id = 1; - uint64 worktree_id = 2; - string root_name = 3; - repeated Entry updated_entries = 4; - repeated uint64 removed_entries = 5; - repeated RepositoryEntry updated_repositories = 6; // deprecated - repeated uint64 removed_repositories = 7; // deprecated - uint64 scan_id = 8; - bool is_last_update = 9; - string abs_path = 10; -} - -// deprecated -message RepositoryEntry { - uint64 repository_id = 1; - reserved 2; - repeated StatusEntry updated_statuses = 3; - repeated string removed_statuses = 4; - repeated string current_merge_conflicts = 5; - optional Branch branch_summary = 6; -} - -message AddProjectCollaborator { - uint64 project_id = 1; - Collaborator collaborator = 2; -} - -message UpdateProjectCollaborator { - uint64 project_id = 1; - PeerId old_peer_id = 2; - PeerId new_peer_id = 3; -} - -message RemoveProjectCollaborator { - uint64 project_id = 1; - PeerId peer_id = 2; -} - -message GetUsers { - repeated uint64 user_ids = 1; -} - -message FuzzySearchUsers { - string query = 1; -} - -message UsersResponse { - repeated User users = 1; -} - -message RequestContact { - uint64 responder_id = 1; -} - -message RemoveContact { - uint64 user_id = 1; -} - -message RespondToContactRequest { - uint64 requester_id = 1; - ContactRequestResponse response = 2; -} - -enum ContactRequestResponse { - Accept = 0; - Decline = 1; - Block = 2; - Dismiss = 3; -} - -message UpdateContacts { - repeated Contact contacts = 1; - repeated uint64 remove_contacts = 2; - repeated IncomingContactRequest incoming_requests = 3; - repeated uint64 remove_incoming_requests = 4; - repeated uint64 outgoing_requests = 5; - repeated uint64 remove_outgoing_requests = 6; -} - -message ShowContacts {} - -message IncomingContactRequest { - uint64 requester_id = 1; -} - -message Follow { - uint64 room_id = 1; - optional uint64 project_id = 2; - PeerId leader_id = 3; -} - -message FollowResponse { - View active_view = 3; - // TODO: Remove after version 0.145.x stabilizes. - optional ViewId active_view_id = 1; - repeated View views = 2; -} - -message UpdateFollowers { - uint64 room_id = 1; - optional uint64 project_id = 2; - reserved 3; - oneof variant { - View create_view = 5; - // TODO: Remove after version 0.145.x stabilizes. - UpdateActiveView update_active_view = 4; - UpdateView update_view = 6; - } -} - -message Unfollow { - uint64 room_id = 1; - optional uint64 project_id = 2; - PeerId leader_id = 3; -} - -message ViewId { - PeerId creator = 1; - uint64 id = 2; -} - -message UpdateActiveView { - optional ViewId id = 1; - optional PeerId leader_id = 2; - View view = 3; -} - -enum PanelId { - AssistantPanel = 0; - DebugPanel = 1; -} - -message UpdateView { - ViewId id = 1; - optional PeerId leader_id = 2; - - oneof variant { - Editor editor = 3; - } - - message Editor { - repeated ExcerptInsertion inserted_excerpts = 1; - repeated uint64 deleted_excerpts = 2; - repeated Selection selections = 3; - optional Selection pending_selection = 4; - EditorAnchor scroll_top_anchor = 5; - reserved 6; - reserved 7; - double scroll_x = 8; - double scroll_y = 9; - } -} - -message View { - ViewId id = 1; - optional PeerId leader_id = 2; - optional PanelId panel_id = 6; - - oneof variant { - Editor editor = 3; - ChannelView channel_view = 4; - ContextEditor context_editor = 5; - } - - message Editor { - bool singleton = 1; - optional string title = 2; - repeated Excerpt excerpts = 3; - repeated Selection selections = 4; - optional Selection pending_selection = 5; - EditorAnchor scroll_top_anchor = 6; - reserved 7; - reserved 8; - double scroll_x = 9; - double scroll_y = 10; - } - - message ChannelView { - uint64 channel_id = 1; - Editor editor = 2; - } - - message ContextEditor { - string context_id = 1; - Editor editor = 2; - } -} - -message ExcerptInsertion { - Excerpt excerpt = 1; - optional uint64 previous_excerpt_id = 2; -} - -message Excerpt { - uint64 id = 1; - uint64 buffer_id = 2; - Anchor context_start = 3; - Anchor context_end = 4; - Anchor primary_start = 5; - Anchor primary_end = 6; -} - -message Contact { - uint64 user_id = 1; - bool online = 2; - bool busy = 3; -} - -message SetRoomParticipantRole { - uint64 room_id = 1; - uint64 user_id = 2; - ChannelRole role = 3; -} diff --git a/crates/proto/proto/channel.proto b/crates/proto/proto/channel.proto deleted file mode 100644 index cada21cd5b..0000000000 --- a/crates/proto/proto/channel.proto +++ /dev/null @@ -1,294 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -import "core.proto"; -import "buffer.proto"; - -message Channel { - uint64 id = 1; - string name = 2; - ChannelVisibility visibility = 3; - int32 channel_order = 4; - repeated uint64 parent_path = 5; -} - -enum ChannelVisibility { - Public = 0; - Members = 1; -} - -message UpdateChannels { - repeated Channel channels = 1; - repeated uint64 delete_channels = 4; - repeated Channel channel_invitations = 5; - repeated uint64 remove_channel_invitations = 6; - repeated ChannelParticipants channel_participants = 7; - repeated ChannelBufferVersion latest_channel_buffer_versions = 9; - - reserved 8; - reserved 10 to 15; -} - -message UpdateUserChannels { - repeated ChannelBufferVersion observed_channel_buffer_version = 2; - repeated ChannelMembership channel_memberships = 3; - - reserved 1; -} - -message ChannelMembership { - uint64 channel_id = 1; - ChannelRole role = 2; -} - -message ChannelMessageId { - uint64 channel_id = 1; - uint64 message_id = 2; -} - -message ChannelPermission { - uint64 channel_id = 1; - ChannelRole role = 3; -} - -message ChannelParticipants { - uint64 channel_id = 1; - repeated uint64 participant_user_ids = 2; -} - -message JoinChannel { - uint64 channel_id = 1; -} - -message DeleteChannel { - uint64 channel_id = 1; -} - -message GetChannelMembers { - uint64 channel_id = 1; - string query = 2; - uint64 limit = 3; -} - -message GetChannelMembersResponse { - repeated ChannelMember members = 1; - repeated User users = 2; -} - -message ChannelMember { - uint64 user_id = 1; - Kind kind = 3; - ChannelRole role = 4; - - enum Kind { - Member = 0; - Invitee = 1; - } -} - -message SubscribeToChannels {} - -message CreateChannel { - string name = 1; - optional uint64 parent_id = 2; -} - -message CreateChannelResponse { - Channel channel = 1; - optional uint64 parent_id = 2; -} - -message InviteChannelMember { - uint64 channel_id = 1; - uint64 user_id = 2; - ChannelRole role = 4; -} - -message RemoveChannelMember { - uint64 channel_id = 1; - uint64 user_id = 2; -} - -enum ChannelRole { - Admin = 0; - Member = 1; - Guest = 2; - Banned = 3; - Talker = 4; -} - -message SetChannelMemberRole { - uint64 channel_id = 1; - uint64 user_id = 2; - ChannelRole role = 3; -} - -message SetChannelVisibility { - uint64 channel_id = 1; - ChannelVisibility visibility = 2; -} - -message RenameChannel { - uint64 channel_id = 1; - string name = 2; -} - -message RenameChannelResponse { - Channel channel = 1; -} - -message JoinChannelChat { - uint64 channel_id = 1; -} - -message JoinChannelChatResponse { - repeated ChannelMessage messages = 1; - bool done = 2; -} - -message LeaveChannelChat { - uint64 channel_id = 1; -} - -message SendChannelMessage { - uint64 channel_id = 1; - string body = 2; - Nonce nonce = 3; - repeated ChatMention mentions = 4; - optional uint64 reply_to_message_id = 5; -} - -message RemoveChannelMessage { - uint64 channel_id = 1; - uint64 message_id = 2; -} - -message UpdateChannelMessage { - uint64 channel_id = 1; - uint64 message_id = 2; - Nonce nonce = 4; - string body = 5; - repeated ChatMention mentions = 6; -} - -message AckChannelMessage { - uint64 channel_id = 1; - uint64 message_id = 2; -} - -message SendChannelMessageResponse { - ChannelMessage message = 1; -} - -message ChannelMessageSent { - uint64 channel_id = 1; - ChannelMessage message = 2; -} - -message ChannelMessageUpdate { - uint64 channel_id = 1; - ChannelMessage message = 2; -} - -message GetChannelMessages { - uint64 channel_id = 1; - uint64 before_message_id = 2; -} - -message GetChannelMessagesResponse { - repeated ChannelMessage messages = 1; - bool done = 2; -} - -message GetChannelMessagesById { - repeated uint64 message_ids = 1; -} - -message MoveChannel { - uint64 channel_id = 1; - uint64 to = 2; -} - -message ReorderChannel { - uint64 channel_id = 1; - enum Direction { - Up = 0; - Down = 1; - } - Direction direction = 2; -} - -message JoinChannelBuffer { - uint64 channel_id = 1; -} - -message ChannelBufferVersion { - uint64 channel_id = 1; - repeated VectorClockEntry version = 2; - uint64 epoch = 3; -} - -message UpdateChannelBufferCollaborators { - uint64 channel_id = 1; - repeated Collaborator collaborators = 2; -} - -message UpdateChannelBuffer { - uint64 channel_id = 1; - repeated Operation operations = 2; -} - -message ChannelMessage { - uint64 id = 1; - string body = 2; - uint64 timestamp = 3; - uint64 sender_id = 4; - Nonce nonce = 5; - repeated ChatMention mentions = 6; - optional uint64 reply_to_message_id = 7; - optional uint64 edited_at = 8; -} - -message ChatMention { - Range range = 1; - uint64 user_id = 2; -} - -message RejoinChannelBuffers { - repeated ChannelBufferVersion buffers = 1; -} - -message RejoinChannelBuffersResponse { - repeated RejoinedChannelBuffer buffers = 1; -} - -message AckBufferOperation { - uint64 buffer_id = 1; - uint64 epoch = 2; - repeated VectorClockEntry version = 3; -} - -message JoinChannelBufferResponse { - uint64 buffer_id = 1; - uint32 replica_id = 2; - string base_text = 3; - repeated Operation operations = 4; - repeated Collaborator collaborators = 5; - uint64 epoch = 6; -} - -message RejoinedChannelBuffer { - uint64 channel_id = 1; - repeated VectorClockEntry version = 2; - repeated Operation operations = 3; - repeated Collaborator collaborators = 4; -} - -message LeaveChannelBuffer { - uint64 channel_id = 1; -} - -message RespondToChannelInvite { - uint64 channel_id = 1; - bool accept = 2; -} diff --git a/crates/proto/proto/core.proto b/crates/proto/proto/core.proto deleted file mode 100644 index 121ea74912..0000000000 --- a/crates/proto/proto/core.proto +++ /dev/null @@ -1,29 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -message PeerId { - uint32 owner_id = 1; - uint32 id = 2; -} - -message User { - reserved 4; - uint64 id = 1; - string github_login = 2; - string avatar_url = 3; - optional string name = 5; -} - -message Nonce { - uint64 upper_half = 1; - uint64 lower_half = 2; -} - -message Collaborator { - PeerId peer_id = 1; - uint32 replica_id = 2; - uint64 user_id = 3; - bool is_host = 4; - optional string committer_name = 5; - optional string committer_email = 6; -} diff --git a/crates/proto/proto/debugger.proto b/crates/proto/proto/debugger.proto deleted file mode 100644 index dcfb91c77d..0000000000 --- a/crates/proto/proto/debugger.proto +++ /dev/null @@ -1,555 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -import "core.proto"; -import "buffer.proto"; -import "task.proto"; - -enum BreakpointState { - Enabled = 0; - Disabled = 1; -} - -message Breakpoint { - Anchor position = 1; - BreakpointState state = 2; - reserved 3; - optional string message = 4; - optional string condition = 5; - optional string hit_condition = 6; - map session_state = 7; -} - -message BreakpointSessionState { - uint64 id = 1; - bool verified = 2; -} - -message BreakpointsForFile { - uint64 project_id = 1; - string path = 2; - repeated Breakpoint breakpoints = 3; -} - -message ToggleBreakpoint { - uint64 project_id = 1; - string path = 2; - Breakpoint breakpoint = 3; -} - -enum DapThreadStatus { - Running = 0; - Stopped = 1; - Exited = 2; - Ended = 3; -} - -enum VariablesArgumentsFilter { - Indexed = 0; - Named = 1; -} - -message ValueFormat { - optional bool hex = 1; -} - -message VariablesRequest { - uint64 project_id = 1; - uint64 client_id = 2; - uint64 variables_reference = 3; - optional VariablesArgumentsFilter filter = 4; - optional uint64 start = 5; - optional uint64 count = 6; - optional ValueFormat format = 7; -} - -enum SteppingGranularity { - Statement = 0; - Line = 1; - Instruction = 2; -} - -message DapLocationsRequest { - uint64 project_id = 1; - uint64 session_id = 2; - uint64 location_reference = 3; -} - -message DapLocationsResponse { - DapSource source = 1; - uint64 line = 2; - optional uint64 column = 3; - optional uint64 end_line = 4; - optional uint64 end_column = 5; -} - -enum DapEvaluateContext { - Repl = 0; - Watch = 1; - Hover = 2; - Clipboard = 3; - EvaluateVariables = 4; - EvaluateUnknown = 5; -} - -message DapEvaluateRequest { - uint64 project_id = 1; - uint64 client_id = 2; - string expression = 3; - optional uint64 frame_id = 4; - optional DapEvaluateContext context = 5; -} - -message DapEvaluateResponse { - string result = 1; - optional string evaluate_type = 2; - uint64 variable_reference = 3; - optional uint64 named_variables = 4; - optional uint64 indexed_variables = 5; - optional string memory_reference = 6; -} - - -message DapCompletionRequest { - uint64 project_id = 1; - uint64 client_id = 2; - string query = 3; - optional uint64 frame_id = 4; - optional uint64 line = 5; - uint64 column = 6; -} - -enum DapCompletionItemType { - Method = 0; - Function = 1; - Constructor = 2; - Field = 3; - Variable = 4; - Class = 5; - Interface = 6; - Module = 7; - Property = 8; - Unit = 9; - Value = 10; - Enum = 11; - Keyword = 12; - Snippet = 13; - Text = 14; - Color = 15; - CompletionItemFile = 16; - Reference = 17; - Customcolor = 19; -} - -message DapCompletionItem { - string label = 1; - optional string text = 2; - optional string sort_text = 3; - optional string detail = 4; - optional DapCompletionItemType typ = 5; - optional uint64 start = 6; - optional uint64 length = 7; - optional uint64 selection_start = 8; - optional uint64 selection_length = 9; -} - -message DapCompletionResponse { - uint64 client_id = 1; - repeated DapCompletionItem completions = 2; -} - -message DapScopesRequest { - uint64 project_id = 1; - uint64 client_id = 2; - uint64 stack_frame_id = 3; -} - -message DapScopesResponse { - repeated DapScope scopes = 1; -} - -message DapSetVariableValueRequest { - uint64 project_id = 1; - uint64 client_id = 2; - string name = 3; - string value = 4; - uint64 variables_reference = 5; -} - -message DapSetVariableValueResponse { - uint64 client_id = 1; - string value = 2; - optional string variable_type = 3; - optional uint64 variables_reference = 4; - optional uint64 named_variables = 5; - optional uint64 indexed_variables = 6; - optional string memory_reference = 7; -} - -message DapPauseRequest { - uint64 project_id = 1; - uint64 client_id = 2; - int64 thread_id = 3; -} - -message DapDisconnectRequest { - uint64 project_id = 1; - uint64 client_id = 2; - optional bool restart = 3; - optional bool terminate_debuggee = 4; - optional bool suspend_debuggee = 5; -} - -message DapTerminateThreadsRequest { - uint64 project_id = 1; - uint64 client_id = 2; - repeated int64 thread_ids = 3; -} - -message DapThreadsRequest { - uint64 project_id = 1; - uint64 client_id = 2; -} - -message DapThreadsResponse { - repeated DapThread threads = 1; -} - -message DapTerminateRequest { - uint64 project_id = 1; - uint64 client_id = 2; - optional bool restart = 3; -} - -message DapRestartRequest { - uint64 project_id = 1; - uint64 client_id = 2; - bytes raw_args = 3; -} - -message DapRestartStackFrameRequest { - uint64 project_id = 1; - uint64 client_id = 2; - uint64 stack_frame_id = 3; -} - -message ToggleIgnoreBreakpoints { - uint64 project_id = 1; - uint32 session_id = 2; -} - -message IgnoreBreakpointState { - uint64 project_id = 1; - uint64 session_id = 2; - bool ignore = 3; -} - -message DapNextRequest { - uint64 project_id = 1; - uint64 client_id = 2; - int64 thread_id = 3; - optional bool single_thread = 4; - optional SteppingGranularity granularity = 5; -} - -message DapStepInRequest { - uint64 project_id = 1; - uint64 client_id = 2; - int64 thread_id = 3; - optional uint64 target_id = 4; - optional bool single_thread = 5; - optional SteppingGranularity granularity = 6; -} - -message DapStepOutRequest { - uint64 project_id = 1; - uint64 client_id = 2; - int64 thread_id = 3; - optional bool single_thread = 4; - optional SteppingGranularity granularity = 5; -} - -message DapStepBackRequest { - uint64 project_id = 1; - uint64 client_id = 2; - int64 thread_id = 3; - optional bool single_thread = 4; - optional SteppingGranularity granularity = 5; -} - -message DapContinueRequest { - uint64 project_id = 1; - uint64 client_id = 2; - int64 thread_id = 3; - optional bool single_thread = 4; -} - -message DapContinueResponse { - uint64 client_id = 1; - optional bool all_threads_continued = 2; -} - -message DapModulesRequest { - uint64 project_id = 1; - uint64 client_id = 2; -} - -message DapModulesResponse { - uint64 client_id = 1; - repeated DapModule modules = 2; -} - -message DapLoadedSourcesRequest { - uint64 project_id = 1; - uint64 client_id = 2; -} - -message DapLoadedSourcesResponse { - uint64 client_id = 1; - repeated DapSource sources = 2; -} - -message DapStackTraceRequest { - uint64 project_id = 1; - uint64 client_id = 2; - int64 thread_id = 3; - optional uint64 start_frame = 4; - optional uint64 stack_trace_levels = 5; -} - -message DapStackTraceResponse { - repeated DapStackFrame frames = 1; -} - -message DapStackFrame { - uint64 id = 1; - string name = 2; - optional DapSource source = 3; - uint64 line = 4; - uint64 column = 5; - optional uint64 end_line = 6; - optional uint64 end_column = 7; - optional bool can_restart = 8; - optional string instruction_pointer_reference = 9; - optional DapModuleId module_id = 10; - optional DapStackPresentationHint presentation_hint = 11; -} - -message DebuggerLoadedSourceList { - uint64 client_id = 1; - repeated DapSource sources = 2; -} - -message DapVariables { - uint64 client_id = 1; - repeated DapVariable variables = 2; -} - -// Remote Debugging: Dap Types -message DapVariable { - string name = 1; - string value = 2; - optional string type = 3; - // optional DapVariablePresentationHint presentation_hint = 4; - optional string evaluate_name = 5; - uint64 variables_reference = 6; - optional uint64 named_variables = 7; - optional uint64 indexed_variables = 8; - optional string memory_reference = 9; -} - -message DapThread { - int64 id = 1; - string name = 2; -} - -message DapScope { - string name = 1; - optional DapScopePresentationHint presentation_hint = 2; - uint64 variables_reference = 3; - optional uint64 named_variables = 4; - optional uint64 indexed_variables = 5; - bool expensive = 6; - optional DapSource source = 7; - optional uint64 line = 8; - optional uint64 column = 9; - optional uint64 end_line = 10; - optional uint64 end_column = 11; -} - -message DapSource { - optional string name = 1; - optional string path = 2; - optional uint64 source_reference = 3; - optional DapSourcePresentationHint presentation_hint = 4; - optional string origin = 5; - repeated DapSource sources = 6; - optional bytes adapter_data = 7; - repeated DapChecksum checksums = 8; -} - -enum DapOutputCategory { - ConsoleOutput = 0; - Important = 1; - Stdout = 2; - Stderr = 3; - Unknown = 4; -} - -enum DapOutputEventGroup { - Start = 0; - StartCollapsed = 1; - End = 2; -} - -message DapOutputEvent { - string output = 1; - optional DapOutputCategory category = 2; - optional uint64 variables_reference = 3; - optional DapOutputEventGroup group = 4; - optional DapSource source = 5; - optional uint32 line = 6; - optional uint32 column = 7; -} - -enum DapChecksumAlgorithm { - CHECKSUM_ALGORITHM_UNSPECIFIED = 0; - MD5 = 1; - SHA1 = 2; - SHA256 = 3; - TIMESTAMP = 4; -} - -message DapChecksum { - DapChecksumAlgorithm algorithm = 1; - string checksum = 2; -} - -enum DapScopePresentationHint { - Arguments = 0; - Locals = 1; - Registers = 2; - ReturnValue = 3; - ScopeUnknown = 4; -} - -enum DapSourcePresentationHint { - SourceNormal = 0; - Emphasize = 1; - Deemphasize = 2; - SourceUnknown = 3; -} - -enum DapStackPresentationHint { - StackNormal = 0; - Label = 1; - Subtle = 2; - StackUnknown = 3; -} -message DapModule { - DapModuleId id = 1; - string name = 2; - optional string path = 3; - optional bool is_optimized = 4; - optional bool is_user_code = 5; - optional string version = 6; - optional string symbol_status = 7; - optional string symbol_file_path = 8; - optional string date_time_stamp = 9; - optional string address_range = 10; -} - -message DebugTaskDefinition { - string adapter = 1; - string label = 2; - string config = 3; - optional TcpHost tcp_connection = 4; -} - -message TcpHost { - optional uint32 port = 1; - optional string host = 2; - optional uint64 timeout = 3; -} - -message DebugLaunchRequest { - string program = 1; - optional string cwd = 2; - repeated string args = 3; - map env = 4; -} - -message DebugAttachRequest { - uint32 process_id = 1; -} - -message DapModuleId { - oneof id { - uint32 number = 1; - string string = 2; - } -} - -message GetDebugAdapterBinary { - uint64 project_id = 1; - uint64 session_id = 3; - DebugTaskDefinition definition = 2; - uint64 worktree_id = 4; -} - -message DebugAdapterBinary { - optional string command = 1; - repeated string arguments = 2; - map envs = 3; - optional string cwd = 4; - optional TcpHost connection = 5; - string configuration = 7; - LaunchType launch_type = 8; - enum LaunchType { - Attach = 0; - Launch = 1; - } -} - -message RunDebugLocators { - uint64 project_id = 1; - SpawnInTerminal build_command = 2; - string locator = 3; -} - -message DebugRequest { - oneof request { - DebugLaunchRequest debug_launch_request = 1; - DebugAttachRequest debug_attach_request = 2; - } -} - -message DebugScenario { - string label = 1; - string adapter = 2; - reserved 3; - DebugRequest request = 4; - optional TcpHost connection = 5; - optional bool stop_on_entry = 6; - optional string configuration = 7; -} - -message LogToDebugConsole { - uint64 project_id = 1; - uint64 session_id = 2; - string message = 3; -} - -message GetProcesses { - uint64 project_id = 1; -} - -message GetProcessesResponse { - repeated ProcessInfo processes = 1; -} - -message ProcessInfo { - uint32 pid = 1; - string name = 2; - repeated string command = 3; -} diff --git a/crates/proto/proto/git.proto b/crates/proto/proto/git.proto deleted file mode 100644 index d1e56f4f8c..0000000000 --- a/crates/proto/proto/git.proto +++ /dev/null @@ -1,589 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -import "worktree.proto"; -import "buffer.proto"; - -message GitBranchesResponse { - repeated Branch branches = 1; -} - -message UpdateDiffBases { - uint64 project_id = 1; - uint64 buffer_id = 2; - - enum Mode { - // No collaborator is using the unstaged diff. - HEAD_ONLY = 0; - // No collaborator is using the diff from HEAD. - INDEX_ONLY = 1; - // Both the unstaged and uncommitted diffs are demanded, - // and the contents of the index and HEAD are the same for this path. - INDEX_MATCHES_HEAD = 2; - // Both the unstaged and uncommitted diffs are demanded, - // and the contents of the index and HEAD differ for this path, - // where None means the path doesn't exist in that state of the repo. - INDEX_AND_HEAD = 3; - } - - optional string staged_text = 3; - optional string committed_text = 4; - Mode mode = 5; -} - -message OpenUnstagedDiff { - uint64 project_id = 1; - uint64 buffer_id = 2; -} - -message OpenUnstagedDiffResponse { - optional string staged_text = 1; -} - -message OpenUncommittedDiff { - uint64 project_id = 1; - uint64 buffer_id = 2; -} - -message OpenUncommittedDiffResponse { - enum Mode { - INDEX_MATCHES_HEAD = 0; - INDEX_AND_HEAD = 1; - } - optional string staged_text = 1; - optional string committed_text = 2; - Mode mode = 3; -} - -message SetIndexText { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - string path = 4; - optional string text = 5; -} - -message GetPermalinkToLine { - uint64 project_id = 1; - uint64 buffer_id = 2; - Range selection = 3; -} - -message GetPermalinkToLineResponse { - string permalink = 1; -} - -message Branch { - bool is_head = 1; - string ref_name = 2; - optional uint64 unix_timestamp = 3; - optional GitUpstream upstream = 4; - optional CommitSummary most_recent_commit = 5; -} - -message GitUpstream { - string ref_name = 1; - optional UpstreamTracking tracking = 2; -} - -message UpstreamTracking { - uint64 ahead = 1; - uint64 behind = 2; -} - -message CommitSummary { - string sha = 1; - string subject = 2; - int64 commit_timestamp = 3; - string author_name = 4; -} - -message GitBranches { - uint64 project_id = 1; - ProjectPath repository = 2; -} - - -message UpdateGitBranch { - uint64 project_id = 1; - string branch_name = 2; - ProjectPath repository = 3; -} - -message UpdateRepository { - uint64 project_id = 1; - uint64 id = 2; - string abs_path = 3; - repeated uint64 entry_ids = 4; - optional Branch branch_summary = 5; - repeated StatusEntry updated_statuses = 6; - repeated string removed_statuses = 7; - repeated string current_merge_conflicts = 8; - uint64 scan_id = 9; - bool is_last_update = 10; - optional GitCommitDetails head_commit_details = 11; - optional string merge_message = 12; - repeated StashEntry stash_entries = 13; - optional string remote_upstream_url = 14; - optional string remote_origin_url = 15; -} - -message RemoveRepository { - uint64 project_id = 1; - uint64 id = 2; -} - -enum GitStatus { - Added = 0; - Modified = 1; - Conflict = 2; - Deleted = 3; - Updated = 4; - TypeChanged = 5; - Renamed = 6; - Copied = 7; - Unmodified = 8; -} - -message GitFileStatus { - oneof variant { - Untracked untracked = 1; - Ignored ignored = 2; - Unmerged unmerged = 3; - Tracked tracked = 4; - } - - message Untracked {} - message Ignored {} - message Unmerged { - GitStatus first_head = 1; - GitStatus second_head = 2; - } - message Tracked { - GitStatus index_status = 1; - GitStatus worktree_status = 2; - } -} - -message GitGetBranches { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; -} - -message GitCreateBranch { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - string branch_name = 4; -} - -message GitChangeBranch { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - string branch_name = 4; -} - -message GitRenameBranch { - uint64 project_id = 1; - uint64 repository_id = 2; - string branch = 3; - string new_name = 4; -} - -message GitCreateRemote { - uint64 project_id = 1; - uint64 repository_id = 2; - string remote_name = 3; - string remote_url = 4; -} - -message GitRemoveRemote { - uint64 project_id = 1; - uint64 repository_id = 2; - string remote_name = 3; -} - -message GitDeleteBranch { - uint64 project_id = 1; - uint64 repository_id = 2; - string branch_name = 3; -} - -message GitDiff { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - DiffType diff_type = 4; - - enum DiffType { - HEAD_TO_WORKTREE = 0; - HEAD_TO_INDEX = 1; - } -} - -message GitDiffResponse { - string diff = 1; -} - -message GitInit { - uint64 project_id = 1; - string abs_path = 2; - string fallback_branch_name = 3; -} - -message GitClone { - uint64 project_id = 1; - string abs_path = 2; - string remote_repo = 3; -} - -message GitCloneResponse { - bool success = 1; -} - -message CheckForPushedCommits { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; -} - -message CheckForPushedCommitsResponse { - repeated string pushed_to = 1; -} - -message GitShow { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - string commit = 4; -} - -message GitCommitDetails { - string sha = 1; - string message = 2; - int64 commit_timestamp = 3; - string author_email = 4; - string author_name = 5; -} - -message LoadCommitDiff { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - string commit = 4; -} - -message LoadCommitDiffResponse { - repeated CommitFile files = 1; -} - -message CommitFile { - string path = 1; - optional string old_text = 2; - optional string new_text = 3; -} - -message GitReset { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - string commit = 4; - ResetMode mode = 5; - enum ResetMode { - SOFT = 0; - MIXED = 1; - } -} - -message GitCheckoutFiles { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - string commit = 4; - repeated string paths = 5; -} - -message GitFileHistory { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - string path = 4; - uint64 skip = 5; - optional uint64 limit = 6; -} - -message GitFileHistoryResponse { - repeated FileHistoryEntry entries = 1; - string path = 2; -} - -message FileHistoryEntry { - string sha = 1; - string subject = 2; - string message = 3; - int64 commit_timestamp = 4; - string author_name = 5; - string author_email = 6; -} - -// Move to `git.proto` once collab's min version is >=0.171.0. -message StatusEntry { - string repo_path = 1; - // Can be removed once collab's min version is >=0.171.0. - GitStatus simple_status = 2; - GitFileStatus status = 3; -} - -message StashEntry { - bytes oid = 1; - string message = 2; - optional string branch = 3; - uint64 index = 4; - int64 timestamp = 5; -} - -message Stage { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - repeated string paths = 4; -} - -message Unstage { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - repeated string paths = 4; -} - -message Stash { - uint64 project_id = 1; - uint64 repository_id = 2; - repeated string paths = 3; -} - -message StashPop { - uint64 project_id = 1; - uint64 repository_id = 2; - optional uint64 stash_index = 3; -} - -message StashApply { - uint64 project_id = 1; - uint64 repository_id = 2; - optional uint64 stash_index = 3; -} - -message StashDrop { - uint64 project_id = 1; - uint64 repository_id = 2; - optional uint64 stash_index = 3; -} - -message Commit { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - optional string name = 4; - optional string email = 5; - string message = 6; - optional CommitOptions options = 7; - reserved 8; - uint64 askpass_id = 9; - - message CommitOptions { - bool amend = 1; - bool signoff = 2; - } -} - -message OpenCommitMessageBuffer { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; -} - -message Push { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - string remote_name = 4; - string branch_name = 5; - optional PushOptions options = 6; - uint64 askpass_id = 7; - - enum PushOptions { - SET_UPSTREAM = 0; - FORCE = 1; - } -} - -message Fetch { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - uint64 askpass_id = 4; - optional string remote = 5; -} - -message GetRemotes { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - optional string branch_name = 4; - bool is_push = 5; -} - -message GetRemotesResponse { - repeated Remote remotes = 1; - - message Remote { - string name = 1; - } -} - -message Pull { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - string remote_name = 4; - optional string branch_name = 5; - uint64 askpass_id = 6; - bool rebase = 7; -} - -message RemoteMessageResponse { - string stdout = 1; - string stderr = 2; -} - -message BlameBuffer { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated VectorClockEntry version = 3; -} - -message BlameEntry { - bytes sha = 1; - - uint32 start_line = 2; - uint32 end_line = 3; - uint32 original_line_number = 4; - - optional string author = 5; - optional string author_mail = 6; - optional int64 author_time = 7; - optional string author_tz = 8; - - optional string committer = 9; - optional string committer_mail = 10; - optional int64 committer_time = 11; - optional string committer_tz = 12; - - optional string summary = 13; - optional string previous = 14; - - string filename = 15; -} - -message CommitMessage { - bytes oid = 1; - string message = 2; -} - -message CommitPermalink { - bytes oid = 1; - string permalink = 2; -} - -message BlameBufferResponse { - message BlameResponse { - repeated BlameEntry entries = 1; - repeated CommitMessage messages = 2; - reserved 3; - reserved 4; - } - - optional BlameResponse blame_response = 5; - - reserved 1 to 4; -} - -message GetDefaultBranch { - uint64 project_id = 1; - uint64 repository_id = 2; -} - -message GetDefaultBranchResponse { - optional string branch = 1; -} - -message GetTreeDiff { - uint64 project_id = 1; - uint64 repository_id = 2; - bool is_merge = 3; - string base = 4; - string head = 5; -} - -message GetTreeDiffResponse { - repeated TreeDiffStatus entries = 1; -} - -message TreeDiffStatus { - enum Status { - ADDED = 0; - MODIFIED = 1; - DELETED = 2; - } - - Status status = 1; - string path = 2; - optional string oid = 3; -} - -message GetBlobContent { - uint64 project_id = 1; - uint64 repository_id = 2; - string oid =3; -} - -message GetBlobContentResponse { - string content = 1; -} - -message GitGetWorktrees { - uint64 project_id = 1; - uint64 repository_id = 2; -} - -message GitWorktreesResponse { - repeated Worktree worktrees = 1; -} - -message Worktree { - string path = 1; - string ref_name = 2; - string sha = 3; -} - -message GitCreateWorktree { - uint64 project_id = 1; - uint64 repository_id = 2; - string name = 3; - string directory = 4; - optional string commit = 5; -} - -message RunGitHook { - enum GitHook { - PRE_COMMIT = 0; - reserved 1; - } - - uint64 project_id = 1; - uint64 repository_id = 2; - GitHook hook = 3; -} diff --git a/crates/proto/proto/image.proto b/crates/proto/proto/image.proto deleted file mode 100644 index e3232e6847..0000000000 --- a/crates/proto/proto/image.proto +++ /dev/null @@ -1,36 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -import "core.proto"; -import "worktree.proto"; - -message OpenImageByPath { - uint64 project_id = 1; - uint64 worktree_id = 2; - string path = 3; -} - -message OpenImageResponse { - uint64 image_id = 1; -} - -message CreateImageForPeer { - uint64 project_id = 1; - PeerId peer_id = 2; - oneof variant { - ImageState state = 3; - ImageChunk chunk = 4; - } -} - -message ImageState { - uint64 id = 1; - optional File file = 2; - uint64 content_size = 3; - string format = 4; // e.g., "png", "jpeg", "webp", etc. -} - -message ImageChunk { - uint64 image_id = 1; - bytes data = 2; -} diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto deleted file mode 100644 index 7717cacdef..0000000000 --- a/crates/proto/proto/lsp.proto +++ /dev/null @@ -1,971 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -import "core.proto"; -import "worktree.proto"; -import "buffer.proto"; - -message GetDefinition { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 4; -} - -message GetDefinitionResponse { - repeated LocationLink links = 1; -} - -message GetDeclaration { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 4; -} - -message GetDeclarationResponse { - repeated LocationLink links = 1; -} - -message GetTypeDefinition { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 4; - } - -message GetTypeDefinitionResponse { - repeated LocationLink links = 1; -} -message GetImplementation { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 4; - } - -message GetImplementationResponse { - repeated LocationLink links = 1; -} - -message GetReferences { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 4; - } - -message GetReferencesResponse { - repeated Location locations = 1; -} - -message GetDocumentHighlights { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 4; - } - -message GetDocumentHighlightsResponse { - repeated DocumentHighlight highlights = 1; -} - -message LocationLink { - optional Location origin = 1; - Location target = 2; -} - -message DocumentHighlight { - Kind kind = 1; - Anchor start = 2; - Anchor end = 3; - - enum Kind { - Text = 0; - Read = 1; - Write = 2; - } -} - -message GetProjectSymbols { - uint64 project_id = 1; - string query = 2; -} - -message GetProjectSymbolsResponse { - repeated Symbol symbols = 4; -} - -message Symbol { - uint64 source_worktree_id = 1; - uint64 worktree_id = 2; - string language_server_name = 3; - string name = 4; - int32 kind = 5; - string path = 6; - // Cannot use generate anchors for unopened files, - // so we are forced to use point coords instead - PointUtf16 start = 7; - PointUtf16 end = 8; - bytes signature = 9; - uint64 language_server_id = 10; -} - -message GetDocumentSymbols { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated VectorClockEntry version = 3; -} - -message GetDocumentSymbolsResponse { - repeated DocumentSymbol symbols = 1; -} - -message DocumentSymbol { - string name = 1; - int32 kind = 2; - // Cannot use generate anchors for unopened files, - // so we are forced to use point coords instead - PointUtf16 start = 3; - PointUtf16 end = 4; - PointUtf16 selection_start = 5; - PointUtf16 selection_end = 6; - repeated DocumentSymbol children = 7; -} - -message InlayHints { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor start = 3; - Anchor end = 4; - repeated VectorClockEntry version = 5; -} - -message InlayHintsResponse { - repeated InlayHint hints = 1; - repeated VectorClockEntry version = 2; -} - -message PointUtf16 { - uint32 row = 1; - uint32 column = 2; -} - -message LspExtExpandMacro { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; -} - -message LspExtExpandMacroResponse { - string name = 1; - string expansion = 2; -} - -message LspExtOpenDocs { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; -} - -message LspExtOpenDocsResponse { - optional string web = 1; - optional string local = 2; -} - -message LspExtSwitchSourceHeader { - uint64 project_id = 1; - uint64 buffer_id = 2; -} - -message LspExtSwitchSourceHeaderResponse { - string target_file = 1; -} - -message LspExtGoToParentModule { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; -} - -message LspExtGoToParentModuleResponse { - repeated LocationLink links = 1; -} - -message GetCompletionsResponse { - repeated Completion completions = 1; - repeated VectorClockEntry version = 2; - // `!is_complete`, inverted for a default of `is_complete = true` - bool can_reuse = 3; -} - -message ApplyCompletionAdditionalEdits { - uint64 project_id = 1; - uint64 buffer_id = 2; - Completion completion = 3; -} - -message ApplyCompletionAdditionalEditsResponse { - Transaction transaction = 1; -} - -message Completion { - Anchor old_replace_start = 1; - Anchor old_replace_end = 2; - string new_text = 3; - uint64 server_id = 4; - bytes lsp_completion = 5; - bool resolved = 6; - Source source = 7; - optional bytes lsp_defaults = 8; - optional Anchor buffer_word_start = 9; - optional Anchor buffer_word_end = 10; - Anchor old_insert_start = 11; - Anchor old_insert_end = 12; - optional string sort_text = 13; - - enum Source { - Lsp = 0; - Custom = 1; - BufferWord = 2; - Dap = 3; - } -} - -message GetCodeActions { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor start = 3; - Anchor end = 4; - repeated VectorClockEntry version = 5; -} - -message GetCodeActionsResponse { - repeated CodeAction actions = 1; - repeated VectorClockEntry version = 2; -} - -message GetSignatureHelp { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 4; -} - -message GetSignatureHelpResponse { - optional SignatureHelp signature_help = 1; -} - -message SignatureHelp { - repeated SignatureInformation signatures = 1; - optional uint32 active_signature = 2; - optional uint32 active_parameter = 3; -} - -message SignatureInformation { - string label = 1; - optional Documentation documentation = 2; - repeated ParameterInformation parameters = 3; - optional uint32 active_parameter = 4; -} - -message Documentation { - oneof content { - string value = 1; - MarkupContent markup_content = 2; - } -} - -enum MarkupKind { - PlainText = 0; - Markdown = 1; -} - -message ParameterInformation { - oneof label { - string simple = 1; - LabelOffsets label_offsets = 2; - } - optional Documentation documentation = 3; -} - -message LabelOffsets { - uint32 start = 1; - uint32 end = 2; -} - -message GetHover { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 5; -} - -message GetHoverResponse { - optional Anchor start = 1; - optional Anchor end = 2; - repeated HoverBlock contents = 3; -} - -message HoverBlock { - string text = 1; - optional string language = 2; - bool is_markdown = 3; -} - -message ApplyCodeAction { - uint64 project_id = 1; - uint64 buffer_id = 2; - CodeAction action = 3; -} - -message ApplyCodeActionResponse { - ProjectTransaction transaction = 1; -} - -message PrepareRename { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 4; -} - -message PrepareRenameResponse { - bool can_rename = 1; - Anchor start = 2; - Anchor end = 3; - repeated VectorClockEntry version = 4; - bool only_unprepared_rename_supported = 5; -} - -message PerformRename { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - string new_name = 4; - repeated VectorClockEntry version = 5; -} - -message OnTypeFormatting { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - string trigger = 4; - repeated VectorClockEntry version = 5; -} - -message OnTypeFormattingResponse { - Transaction transaction = 1; -} - - -message LinkedEditingRange { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 4; -} - -message LinkedEditingRangeResponse { - repeated AnchorRange items = 1; - repeated VectorClockEntry version = 4; -} - -message InlayHint { - Anchor position = 1; - InlayHintLabel label = 2; - optional string kind = 3; - bool padding_left = 4; - bool padding_right = 5; - InlayHintTooltip tooltip = 6; - ResolveState resolve_state = 7; -} - -message InlayHintLabel { - oneof label { - string value = 1; - InlayHintLabelParts label_parts = 2; - } -} - -message InlayHintLabelParts { - repeated InlayHintLabelPart parts = 1; -} - -message InlayHintLabelPart { - string value = 1; - InlayHintLabelPartTooltip tooltip = 2; - optional string location_url = 3; - PointUtf16 location_range_start = 4; - PointUtf16 location_range_end = 5; - optional uint64 language_server_id = 6; -} - -message InlayHintTooltip { - oneof content { - string value = 1; - MarkupContent markup_content = 2; - } -} - -message InlayHintLabelPartTooltip { - oneof content { - string value = 1; - MarkupContent markup_content = 2; - } -} - -message ResolveState { - State state = 1; - LspResolveState lsp_resolve_state = 2; - - enum State { - Resolved = 0; - CanResolve = 1; - Resolving = 2; - } - - message LspResolveState { - optional string value = 1; - uint64 server_id = 2; - } -} - -// This type is used to resolve more than just -// the documentation, but for backwards-compatibility -// reasons we can't rename the type. -message ResolveCompletionDocumentation { - uint64 project_id = 1; - uint64 language_server_id = 2; - bytes lsp_completion = 3; - uint64 buffer_id = 4; -} - -message ResolveCompletionDocumentationResponse { - string documentation = 1; - bool documentation_is_markdown = 2; - Anchor old_replace_start = 3; - Anchor old_replace_end = 4; - string new_text = 5; - bytes lsp_completion = 6; - Anchor old_insert_start = 7; - Anchor old_insert_end = 8; -} - -message ResolveInlayHint { - uint64 project_id = 1; - uint64 buffer_id = 2; - uint64 language_server_id = 3; - InlayHint hint = 4; -} - -message ResolveInlayHintResponse { - InlayHint hint = 1; -} - -message RefreshInlayHints { - uint64 project_id = 1; - uint64 server_id = 2; - optional uint64 request_id = 3; -} - -message CodeLens { - bytes lsp_lens = 1; -} - -message GetCodeLens { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated VectorClockEntry version = 3; -} - -message GetCodeLensResponse { - repeated CodeAction lens_actions = 1; - repeated VectorClockEntry version = 2; -} - -message RefreshCodeLens { - uint64 project_id = 1; -} - -message MarkupContent { - bool is_markdown = 1; - string value = 2; -} - -message PerformRenameResponse { - ProjectTransaction transaction = 2; -} - -message CodeAction { - uint64 server_id = 1; - Anchor start = 2; - Anchor end = 3; - bytes lsp_action = 4; - Kind kind = 5; - bool resolved = 6; - enum Kind { - Action = 0; - Command = 1; - CodeLens = 2; - } -} - -message LanguageServer { - uint64 id = 1; - string name = 2; - optional uint64 worktree_id = 3; -} - -message StartLanguageServer { - uint64 project_id = 1; - LanguageServer server = 2; - string capabilities = 3; -} - -message UpdateDiagnosticSummary { - uint64 project_id = 1; - uint64 worktree_id = 2; - DiagnosticSummary summary = 3; - repeated DiagnosticSummary more_summaries = 4; -} - -message DiagnosticSummary { - string path = 1; - uint64 language_server_id = 2; - uint32 error_count = 3; - uint32 warning_count = 4; -} - -message UpdateLanguageServer { - uint64 project_id = 1; - uint64 language_server_id = 2; - optional string server_name = 8; - oneof variant { - LspWorkStart work_start = 3; - LspWorkProgress work_progress = 4; - LspWorkEnd work_end = 5; - LspDiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 6; - LspDiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 7; - StatusUpdate status_update = 9; - RegisteredForBuffer registered_for_buffer = 10; - ServerMetadataUpdated metadata_updated = 11; - } -} - -message ProgressToken { - oneof value { - int32 number = 1; - string string = 2; - } -} - -message LspWorkStart { - reserved 1; - optional string title = 4; - optional string message = 2; - optional uint32 percentage = 3; - optional bool is_cancellable = 5; - ProgressToken token = 6; -} - -message LspWorkProgress { - reserved 1; - optional string message = 2; - optional uint32 percentage = 3; - optional bool is_cancellable = 4; - ProgressToken token = 5; -} - -message LspWorkEnd { - reserved 1; - ProgressToken token = 2; -} - -message LspDiskBasedDiagnosticsUpdating {} - -message LspDiskBasedDiagnosticsUpdated {} - -message StatusUpdate { - optional string message = 1; - oneof status { - ServerBinaryStatus binary = 2; - ServerHealth health = 3; - } -} - -enum ServerHealth { - OK = 0; - WARNING = 1; - ERROR = 2; -} - -enum ServerBinaryStatus { - NONE = 0; - CHECKING_FOR_UPDATE = 1; - DOWNLOADING = 2; - STARTING = 3; - STOPPING = 4; - STOPPED = 5; - FAILED = 6; -} - -message RegisteredForBuffer { - string buffer_abs_path = 1; - uint64 buffer_id = 2; -} - -message LanguageServerBinaryInfo { - string path = 1; - repeated string arguments = 2; -} - -message ServerMetadataUpdated { - optional string capabilities = 1; - optional LanguageServerBinaryInfo binary = 2; - optional string configuration = 3; - repeated string workspace_folders = 4; -} - -message LanguageServerLog { - uint64 project_id = 1; - uint64 language_server_id = 2; - string message = 3; - oneof log_type { - LogMessage log = 4; - TraceMessage trace = 5; - RpcMessage rpc = 6; - } -} - -message LogMessage { - LogLevel level = 1; - - enum LogLevel { - LOG = 0; - INFO = 1; - WARNING = 2; - ERROR = 3; - } -} - -message TraceMessage { - optional string verbose_info = 1; -} - -message RpcMessage { - Kind kind = 1; - - enum Kind { - RECEIVED = 0; - SENT = 1; - } -} - -message LspLogTrace { - optional string message = 1; -} - -message ApplyCodeActionKind { - uint64 project_id = 1; - string kind = 2; - repeated uint64 buffer_ids = 3; -} - -message ApplyCodeActionKindResponse { - ProjectTransaction transaction = 1; -} - -message RegisterBufferWithLanguageServers { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated LanguageServerSelector only_servers = 3; -} - -enum FormatTrigger { - Save = 0; - Manual = 1; -} - -message OpenBufferForSymbol { - uint64 project_id = 1; - Symbol symbol = 2; -} - -message OpenBufferForSymbolResponse { - uint64 buffer_id = 1; -} - -message FormatBuffers { - uint64 project_id = 1; - FormatTrigger trigger = 2; - repeated uint64 buffer_ids = 3; -} - -message FormatBuffersResponse { - ProjectTransaction transaction = 1; -} - -message GetCompletions { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 4; - optional uint64 server_id = 5; -} - -message CancelLanguageServerWork { - uint64 project_id = 1; - - oneof work { - Buffers buffers = 2; - LanguageServerWork language_server_work = 3; - } - - message Buffers { - repeated uint64 buffer_ids = 2; - } - - message LanguageServerWork { - uint64 language_server_id = 1; - reserved 2; - optional ProgressToken token = 3; - } -} - -message LanguageServerPromptRequest { - uint64 project_id = 1; - - oneof level { - Info info = 2; - Warning warning = 3; - Critical critical = 4; - } - - message Info {} - message Warning {} - message Critical {} - - string message = 5; - repeated string actions = 6; - string lsp_name = 7; -} - -message LanguageServerPromptResponse { - optional uint64 action_response = 1; -} - -message GetDocumentColor { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated VectorClockEntry version = 3; - -} - -message GetDocumentColorResponse { - repeated ColorInformation colors = 1; - repeated VectorClockEntry version = 2; - -} - -message ColorInformation { - PointUtf16 lsp_range_start = 1; - PointUtf16 lsp_range_end = 2; - float red = 3; - float green = 4; - float blue = 5; - float alpha = 6; -} - -message GetColorPresentation { - uint64 project_id = 1; - uint64 buffer_id = 2; - ColorInformation color = 3; - uint64 server_id = 4; -} - -message GetColorPresentationResponse { - repeated ColorPresentation presentations = 1; -} - -message ColorPresentation { - string label = 1; - optional TextEdit text_edit = 2; - repeated TextEdit additional_text_edits = 3; -} - -message TextEdit { - string new_text = 1; - PointUtf16 lsp_range_start = 2; - PointUtf16 lsp_range_end = 3; -} - -message LspQuery { - uint64 project_id = 1; - uint64 lsp_request_id = 2; - optional uint64 server_id = 15; - oneof request { - GetReferences get_references = 3; - GetDocumentColor get_document_color = 4; - GetHover get_hover = 5; - GetCodeActions get_code_actions = 6; - GetSignatureHelp get_signature_help = 7; - GetCodeLens get_code_lens = 8; - GetDocumentDiagnostics get_document_diagnostics = 9; - GetDefinition get_definition = 10; - GetDeclaration get_declaration = 11; - GetTypeDefinition get_type_definition = 12; - GetImplementation get_implementation = 13; - InlayHints inlay_hints = 14; - } -} - -message LspQueryResponse { - uint64 project_id = 1; - uint64 lsp_request_id = 2; - repeated LspResponse responses = 3; -} - -message LspResponse { - oneof response { - GetHoverResponse get_hover_response = 1; - GetCodeActionsResponse get_code_actions_response = 2; - GetSignatureHelpResponse get_signature_help_response = 3; - GetCodeLensResponse get_code_lens_response = 4; - GetDocumentDiagnosticsResponse get_document_diagnostics_response = 5; - GetDocumentColorResponse get_document_color_response = 6; - GetDefinitionResponse get_definition_response = 8; - GetDeclarationResponse get_declaration_response = 9; - GetTypeDefinitionResponse get_type_definition_response = 10; - GetImplementationResponse get_implementation_response = 11; - GetReferencesResponse get_references_response = 12; - InlayHintsResponse inlay_hints_response = 13; - } - uint64 server_id = 7; -} - -message AllLanguageServers {} - -message LanguageServerSelector { - oneof selector { - uint64 server_id = 1; - string name = 2; - } -} - -message RestartLanguageServers { - uint64 project_id = 1; - repeated uint64 buffer_ids = 2; - repeated LanguageServerSelector only_servers = 3; - bool all = 4; -} - -message StopLanguageServers { - uint64 project_id = 1; - repeated uint64 buffer_ids = 2; - repeated LanguageServerSelector also_servers = 3; - bool all = 4; -} - -message LspExtRunnables { - uint64 project_id = 1; - uint64 buffer_id = 2; - optional Anchor position = 3; -} - -message LspExtRunnablesResponse { - repeated LspRunnable runnables = 1; -} - -message LspRunnable { - bytes task_template = 1; - optional LocationLink location = 2; -} - -message LspExtCancelFlycheck { - uint64 project_id = 1; - uint64 language_server_id = 2; -} - -message LspExtRunFlycheck { - uint64 project_id = 1; - optional uint64 buffer_id = 2; - uint64 language_server_id = 3; - bool current_file_only = 4; -} - -message LspExtClearFlycheck { - uint64 project_id = 1; - uint64 language_server_id = 2; -} - -message LspDiagnosticRelatedInformation { - optional string location_url = 1; - PointUtf16 location_range_start = 2; - PointUtf16 location_range_end = 3; - string message = 4; -} - -enum LspDiagnosticTag { - None = 0; - Unnecessary = 1; - Deprecated = 2; -} - -message LspDiagnostic { - PointUtf16 start = 1; - PointUtf16 end = 2; - Severity severity = 3; - optional string code = 4; - optional string code_description = 5; - optional string source = 6; - string message = 7; - repeated LspDiagnosticRelatedInformation related_information = 8; - repeated LspDiagnosticTag tags = 9; - optional string data = 10; - - enum Severity { - None = 0; - Error = 1; - Warning = 2; - Information = 3; - Hint = 4; - } -} - -message GetDocumentDiagnostics { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated VectorClockEntry version = 3; -} - -message GetDocumentDiagnosticsResponse { - repeated PulledDiagnostics pulled_diagnostics = 1; -} - -message PulledDiagnostics { - uint64 server_id = 1; - string uri = 2; - optional string result_id = 3; - bool changed = 4; - repeated LspDiagnostic diagnostics = 5; - optional string registration_id = 6; -} - -message PullWorkspaceDiagnostics { - uint64 project_id = 1; - uint64 server_id = 2; -} - -message ToggleLspLogs { - uint64 project_id = 1; - LogType log_type = 2; - uint64 server_id = 3; - bool enabled = 4; - - enum LogType { - LOG = 0; - TRACE = 1; - RPC = 2; - } -} diff --git a/crates/proto/proto/notification.proto b/crates/proto/proto/notification.proto deleted file mode 100644 index ebd3d7fe44..0000000000 --- a/crates/proto/proto/notification.proto +++ /dev/null @@ -1,37 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -message GetNotifications { - optional uint64 before_id = 1; -} - -message AddNotification { - Notification notification = 1; -} - -message GetNotificationsResponse { - repeated Notification notifications = 1; - bool done = 2; -} - -message DeleteNotification { - uint64 notification_id = 1; -} - -message UpdateNotification { - Notification notification = 1; -} - -message MarkNotificationRead { - uint64 notification_id = 1; -} - -message Notification { - uint64 id = 1; - uint64 timestamp = 2; - string kind = 3; - optional uint64 entity_id = 4; - string content = 5; - bool is_read = 6; - optional bool response = 7; -} diff --git a/crates/proto/proto/task.proto b/crates/proto/proto/task.proto deleted file mode 100644 index 1844087d62..0000000000 --- a/crates/proto/proto/task.proto +++ /dev/null @@ -1,60 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -import "buffer.proto"; - -message TaskContextForLocation { - uint64 project_id = 1; - Location location = 2; - map task_variables = 3; -} - -message TaskContext { - optional string cwd = 1; - map task_variables = 2; - map project_env = 3; -} - -message Shell { - message WithArguments { - string program = 1; - repeated string args = 2; - } - - oneof shell_type { - System system = 1; - string program = 2; - WithArguments with_arguments = 3; - } -} - -message System {} - -enum RevealStrategy { - RevealAlways = 0; - RevealNever = 1; -} - -enum HideStrategy { - HideAlways = 0; - HideNever = 1; - HideOnSuccess = 2; -} - -message SpawnInTerminal { - string label = 1; - optional string command = 2; - repeated string args = 3; - map env = 4; - optional string cwd = 5; -} - -message GetDirectoryEnvironment { - uint64 project_id = 1; - Shell shell = 2; - string directory = 3; -} - -message DirectoryEnvironment { - map environment = 1; -} diff --git a/crates/proto/proto/toolchain.proto b/crates/proto/proto/toolchain.proto deleted file mode 100644 index b190322ca0..0000000000 --- a/crates/proto/proto/toolchain.proto +++ /dev/null @@ -1,59 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -message ListToolchains { - uint64 project_id = 1; - uint64 worktree_id = 2; - string language_name = 3; - optional string path = 4; -} - -message Toolchain { - string name = 1; - string path = 2; - string raw_json = 3; -} - -message ToolchainGroup { - uint64 start_index = 1; - string name = 2; -} - -message ListToolchainsResponse { - repeated Toolchain toolchains = 1; - bool has_values = 2; - repeated ToolchainGroup groups = 3; - optional string relative_worktree_path = 4; -} - -message ActivateToolchain { - uint64 project_id = 1; - uint64 worktree_id = 2; - Toolchain toolchain = 3; - string language_name = 4; - optional string path = 5; -} - -message ActiveToolchain { - uint64 project_id = 1; - uint64 worktree_id = 2; - string language_name = 3; - optional string path = 4; -} - -message ActiveToolchainResponse { - optional Toolchain toolchain = 1; -} - -message ResolveToolchain { - uint64 project_id = 1; - string abs_path = 2; - string language_name = 3; -} - -message ResolveToolchainResponse { - oneof response { - Toolchain toolchain = 1; - string error = 2; - } -} diff --git a/crates/proto/proto/worktree.proto b/crates/proto/proto/worktree.proto deleted file mode 100644 index 9ab9e95438..0000000000 --- a/crates/proto/proto/worktree.proto +++ /dev/null @@ -1,160 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -message Timestamp { - uint64 seconds = 1; - uint32 nanos = 2; -} - -message File { - uint64 worktree_id = 1; - optional uint64 entry_id = 2; - string path = 3; - Timestamp mtime = 4; - bool is_deleted = 5; -} - -message Entry { - uint64 id = 1; - bool is_dir = 2; - string path = 3; - uint64 inode = 4; - Timestamp mtime = 5; - bool is_ignored = 7; - bool is_external = 8; - reserved 6; - reserved 9; - bool is_fifo = 10; - optional uint64 size = 11; - optional string canonical_path = 12; - bool is_hidden = 13; -} - -message AddWorktree { - string path = 1; - uint64 project_id = 2; - bool visible = 3; -} - -message AddWorktreeResponse { - uint64 worktree_id = 1; - string canonicalized_path = 2; -} - -message RemoveWorktree { - uint64 worktree_id = 1; -} - -message GetPathMetadata { - uint64 project_id = 1; - string path = 2; -} - -message GetPathMetadataResponse { - bool exists = 1; - string path = 2; - bool is_dir = 3; -} - -message WorktreeMetadata { - uint64 id = 1; - string root_name = 2; - bool visible = 3; - string abs_path = 4; -} - -message ProjectPath { - uint64 worktree_id = 1; - string path = 2; -} - -message ListRemoteDirectoryConfig { - bool is_dir = 1; -} - -message ListRemoteDirectory { - uint64 dev_server_id = 1; - string path = 2; - ListRemoteDirectoryConfig config = 3; -} - -message EntryInfo { - bool is_dir = 1; -} - -message ListRemoteDirectoryResponse { - repeated string entries = 1; - repeated EntryInfo entry_info = 2; -} - -message CreateProjectEntry { - uint64 project_id = 1; - uint64 worktree_id = 2; - string path = 3; - bool is_directory = 4; - optional bytes content = 5; -} - -message RenameProjectEntry { - uint64 project_id = 1; - uint64 entry_id = 2; - string new_path = 3; - uint64 new_worktree_id = 4; -} - -message CopyProjectEntry { - uint64 project_id = 1; - uint64 entry_id = 2; - string new_path = 3; - uint64 new_worktree_id = 5; - reserved 4; -} - -message DeleteProjectEntry { - uint64 project_id = 1; - uint64 entry_id = 2; - bool use_trash = 3; -} - -message ExpandProjectEntry { - uint64 project_id = 1; - uint64 entry_id = 2; -} - -message ExpandProjectEntryResponse { - uint64 worktree_scan_id = 1; -} - -message ExpandAllForProjectEntry { - uint64 project_id = 1; - uint64 entry_id = 2; -} - -message ExpandAllForProjectEntryResponse { - uint64 worktree_scan_id = 1; -} - -message ProjectEntryResponse { - optional Entry entry = 1; - uint64 worktree_scan_id = 2; -} - -message UpdateWorktreeSettings { - uint64 project_id = 1; - uint64 worktree_id = 2; - string path = 3; - optional string content = 4; - optional LocalSettingsKind kind = 5; -} - -enum LocalSettingsKind { - Settings = 0; - Tasks = 1; - Editorconfig = 2; - Debug = 3; -} - -message UpdateUserSettings { - uint64 project_id = 1; - string contents = 2; -} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto deleted file mode 100644 index 8e26a26a43..0000000000 --- a/crates/proto/proto/zed.proto +++ /dev/null @@ -1,527 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -import "ai.proto"; -import "app.proto"; -import "buffer.proto"; -import "call.proto"; -import "channel.proto"; -import "core.proto"; -import "debugger.proto"; -import "git.proto"; -import "image.proto"; -import "lsp.proto"; -import "notification.proto"; -import "task.proto"; -import "toolchain.proto"; -import "worktree.proto"; - -// Looking for a number? Search "// current max" - -message Envelope { - uint32 id = 1; - optional uint32 responding_to = 2; - optional PeerId original_sender_id = 3; - optional uint32 ack_id = 266; - - oneof payload { - Hello hello = 4; - Ack ack = 5; - Error error = 6; - Ping ping = 7; - Test test = 8; - EndStream end_stream = 165; - - CreateRoom create_room = 9; - CreateRoomResponse create_room_response = 10; - JoinRoom join_room = 11; - JoinRoomResponse join_room_response = 12; - RejoinRoom rejoin_room = 13; - RejoinRoomResponse rejoin_room_response = 14; - LeaveRoom leave_room = 15; - Call call = 16; - IncomingCall incoming_call = 17; - CallCanceled call_canceled = 18; - CancelCall cancel_call = 19; - DeclineCall decline_call = 20; - UpdateParticipantLocation update_participant_location = 21; - RoomUpdated room_updated = 22; - - ShareProject share_project = 23; - ShareProjectResponse share_project_response = 24; - UnshareProject unshare_project = 25; - JoinProject join_project = 26; - JoinProjectResponse join_project_response = 27; - LeaveProject leave_project = 28; - AddProjectCollaborator add_project_collaborator = 29; - UpdateProjectCollaborator update_project_collaborator = 30; - RemoveProjectCollaborator remove_project_collaborator = 31; - - GetDefinition get_definition = 32; - GetDefinitionResponse get_definition_response = 33; - GetDeclaration get_declaration = 237; - GetDeclarationResponse get_declaration_response = 238; - GetTypeDefinition get_type_definition = 34; - GetTypeDefinitionResponse get_type_definition_response = 35; - - GetReferences get_references = 36; - GetReferencesResponse get_references_response = 37; - GetDocumentHighlights get_document_highlights = 38; - GetDocumentHighlightsResponse get_document_highlights_response = 39; - GetProjectSymbols get_project_symbols = 40; - GetProjectSymbolsResponse get_project_symbols_response = 41; - OpenBufferForSymbol open_buffer_for_symbol = 42; - OpenBufferForSymbolResponse open_buffer_for_symbol_response = 43; - - UpdateProject update_project = 44; - UpdateWorktree update_worktree = 45; - - CreateProjectEntry create_project_entry = 46; - RenameProjectEntry rename_project_entry = 47; - CopyProjectEntry copy_project_entry = 48; - DeleteProjectEntry delete_project_entry = 49; - ProjectEntryResponse project_entry_response = 50; - ExpandProjectEntry expand_project_entry = 51; - ExpandProjectEntryResponse expand_project_entry_response = 52; - ExpandAllForProjectEntry expand_all_for_project_entry = 291; - ExpandAllForProjectEntryResponse expand_all_for_project_entry_response = 292; - UpdateDiagnosticSummary update_diagnostic_summary = 53; - StartLanguageServer start_language_server = 54; - UpdateLanguageServer update_language_server = 55; - - OpenBufferById open_buffer_by_id = 56; - OpenBufferByPath open_buffer_by_path = 57; - OpenBufferResponse open_buffer_response = 58; - CreateBufferForPeer create_buffer_for_peer = 59; - UpdateBuffer update_buffer = 60; - UpdateBufferFile update_buffer_file = 61; - SaveBuffer save_buffer = 62; - BufferSaved buffer_saved = 63; - BufferReloaded buffer_reloaded = 64; - ReloadBuffers reload_buffers = 65; - ReloadBuffersResponse reload_buffers_response = 66; - SynchronizeBuffers synchronize_buffers = 67; - SynchronizeBuffersResponse synchronize_buffers_response = 68; - FormatBuffers format_buffers = 69; - FormatBuffersResponse format_buffers_response = 70; - GetCompletions get_completions = 71; - GetCompletionsResponse get_completions_response = 72; - ResolveCompletionDocumentation resolve_completion_documentation = 73; - ResolveCompletionDocumentationResponse resolve_completion_documentation_response = 74; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 75; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 76; - GetCodeActions get_code_actions = 77; - GetCodeActionsResponse get_code_actions_response = 78; - GetHover get_hover = 79; - GetHoverResponse get_hover_response = 80; - ApplyCodeAction apply_code_action = 81; - ApplyCodeActionResponse apply_code_action_response = 82; - PrepareRename prepare_rename = 83; - PrepareRenameResponse prepare_rename_response = 84; - PerformRename perform_rename = 85; - PerformRenameResponse perform_rename_response = 86; - - UpdateContacts update_contacts = 89; - UpdateInviteInfo update_invite_info = 90; - ShowContacts show_contacts = 91; - - GetUsers get_users = 92; - FuzzySearchUsers fuzzy_search_users = 93; - UsersResponse users_response = 94; - RequestContact request_contact = 95; - RespondToContactRequest respond_to_contact_request = 96; - RemoveContact remove_contact = 97; - - Follow follow = 98; - FollowResponse follow_response = 99; - UpdateFollowers update_followers = 100; - Unfollow unfollow = 101; - UpdateDiffBases update_diff_bases = 104; - - OnTypeFormatting on_type_formatting = 105; - OnTypeFormattingResponse on_type_formatting_response = 106; - - UpdateWorktreeSettings update_worktree_settings = 107; - - InlayHints inlay_hints = 108; - InlayHintsResponse inlay_hints_response = 109; - ResolveInlayHint resolve_inlay_hint = 110; - ResolveInlayHintResponse resolve_inlay_hint_response = 111; - RefreshInlayHints refresh_inlay_hints = 112; - - CreateChannel create_channel = 113; - CreateChannelResponse create_channel_response = 114; - InviteChannelMember invite_channel_member = 115; - RemoveChannelMember remove_channel_member = 116; - RespondToChannelInvite respond_to_channel_invite = 117; - UpdateChannels update_channels = 118; - JoinChannel join_channel = 119; - DeleteChannel delete_channel = 120; - GetChannelMembers get_channel_members = 121; - GetChannelMembersResponse get_channel_members_response = 122; - SetChannelMemberRole set_channel_member_role = 123; - RenameChannel rename_channel = 124; - RenameChannelResponse rename_channel_response = 125; - SubscribeToChannels subscribe_to_channels = 207; - - JoinChannelBuffer join_channel_buffer = 126; - JoinChannelBufferResponse join_channel_buffer_response = 127; - UpdateChannelBuffer update_channel_buffer = 128; - LeaveChannelBuffer leave_channel_buffer = 129; - UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 130; - RejoinChannelBuffers rejoin_channel_buffers = 131; - RejoinChannelBuffersResponse rejoin_channel_buffers_response = 132; - AckBufferOperation ack_buffer_operation = 133; - - JoinChannelChat join_channel_chat = 134; - JoinChannelChatResponse join_channel_chat_response = 135; - LeaveChannelChat leave_channel_chat = 136; - SendChannelMessage send_channel_message = 137; - SendChannelMessageResponse send_channel_message_response = 138; - ChannelMessageSent channel_message_sent = 139; - GetChannelMessages get_channel_messages = 140; - GetChannelMessagesResponse get_channel_messages_response = 141; - RemoveChannelMessage remove_channel_message = 142; - AckChannelMessage ack_channel_message = 143; - GetChannelMessagesById get_channel_messages_by_id = 144; - - MoveChannel move_channel = 147; - ReorderChannel reorder_channel = 349; - SetChannelVisibility set_channel_visibility = 148; - - AddNotification add_notification = 149; - GetNotifications get_notifications = 150; - GetNotificationsResponse get_notifications_response = 151; - DeleteNotification delete_notification = 152; - MarkNotificationRead mark_notification_read = 153; - LspExtExpandMacro lsp_ext_expand_macro = 154; - LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155; - SetRoomParticipantRole set_room_participant_role = 156; - - UpdateUserChannels update_user_channels = 157; - - GetImplementation get_implementation = 162; - GetImplementationResponse get_implementation_response = 163; - - UpdateChannelMessage update_channel_message = 170; - ChannelMessageUpdate channel_message_update = 171; - - BlameBuffer blame_buffer = 172; - BlameBufferResponse blame_buffer_response = 173; - - UpdateNotification update_notification = 174; - - RestartLanguageServers restart_language_servers = 208; - - RejoinRemoteProjects rejoin_remote_projects = 186; - RejoinRemoteProjectsResponse rejoin_remote_projects_response = 187; - - OpenNewBuffer open_new_buffer = 196; - - GetSupermavenApiKey get_supermaven_api_key = 198; - GetSupermavenApiKeyResponse get_supermaven_api_key_response = 199; - - TaskContextForLocation task_context_for_location = 203; - TaskContext task_context = 204; - - LinkedEditingRange linked_editing_range = 209; - LinkedEditingRangeResponse linked_editing_range_response = 210; - - AdvertiseContexts advertise_contexts = 211; - OpenContext open_context = 212; - OpenContextResponse open_context_response = 213; - CreateContext create_context = 232; - CreateContextResponse create_context_response = 233; - UpdateContext update_context = 214; - SynchronizeContexts synchronize_contexts = 215; - SynchronizeContextsResponse synchronize_contexts_response = 216; - - GetSignatureHelp get_signature_help = 217; - GetSignatureHelpResponse get_signature_help_response = 218; - - ListRemoteDirectory list_remote_directory = 219; - ListRemoteDirectoryResponse list_remote_directory_response = 220; - AddWorktree add_worktree = 222; - AddWorktreeResponse add_worktree_response = 223; - - LspExtSwitchSourceHeader lsp_ext_switch_source_header = 241; - LspExtSwitchSourceHeaderResponse lsp_ext_switch_source_header_response = 242; - - FindSearchCandidates find_search_candidates = 243; - FindSearchCandidatesResponse find_search_candidates_response = 244; - - CloseBuffer close_buffer = 245; - - ShutdownRemoteServer shutdown_remote_server = 257; - - RemoveWorktree remove_worktree = 258; - - LanguageServerLog language_server_log = 260; - - Toast toast = 261; - HideToast hide_toast = 262; - - OpenServerSettings open_server_settings = 263; - - GetPermalinkToLine get_permalink_to_line = 264; - GetPermalinkToLineResponse get_permalink_to_line_response = 265; - - FlushBufferedMessages flush_buffered_messages = 267; - - LanguageServerPromptRequest language_server_prompt_request = 268; - LanguageServerPromptResponse language_server_prompt_response = 269; - - GitBranchesResponse git_branches_response = 271; - - UpdateGitBranch update_git_branch = 272; - - ListToolchains list_toolchains = 273; - ListToolchainsResponse list_toolchains_response = 274; - ActivateToolchain activate_toolchain = 275; - ActiveToolchain active_toolchain = 276; - ActiveToolchainResponse active_toolchain_response = 277; - - GetPathMetadata get_path_metadata = 278; - GetPathMetadataResponse get_path_metadata_response = 279; - - CancelLanguageServerWork cancel_language_server_work = 282; - - LspExtOpenDocs lsp_ext_open_docs = 283; - LspExtOpenDocsResponse lsp_ext_open_docs_response = 284; - - SyncExtensions sync_extensions = 285; - SyncExtensionsResponse sync_extensions_response = 286; - InstallExtension install_extension = 287; - - OpenUnstagedDiff open_unstaged_diff = 288; - OpenUnstagedDiffResponse open_unstaged_diff_response = 289; - - RegisterBufferWithLanguageServers register_buffer_with_language_servers = 290; - - Stage stage = 293; - Unstage unstage = 294; - Commit commit = 295; - OpenCommitMessageBuffer open_commit_message_buffer = 296; - - OpenUncommittedDiff open_uncommitted_diff = 297; - OpenUncommittedDiffResponse open_uncommitted_diff_response = 298; - - SetIndexText set_index_text = 299; - - GitShow git_show = 300; - GitReset git_reset = 301; - GitCommitDetails git_commit_details = 302; - GitCheckoutFiles git_checkout_files = 303; - - Push push = 304; - Fetch fetch = 305; - GetRemotes get_remotes = 306; - GetRemotesResponse get_remotes_response = 307; - Pull pull = 308; - - ApplyCodeActionKind apply_code_action_kind = 309; - ApplyCodeActionKindResponse apply_code_action_kind_response = 310; - - RemoteMessageResponse remote_message_response = 311; - - GitGetBranches git_get_branches = 312; - GitCreateBranch git_create_branch = 313; - GitChangeBranch git_change_branch = 314; - - CheckForPushedCommits check_for_pushed_commits = 315; - CheckForPushedCommitsResponse check_for_pushed_commits_response = 316; - - AskPassRequest ask_pass_request = 317; - AskPassResponse ask_pass_response = 318; - - GitDiff git_diff = 319; - GitDiffResponse git_diff_response = 320; - GitInit git_init = 321; - - CodeLens code_lens = 322; - GetCodeLens get_code_lens = 323; - GetCodeLensResponse get_code_lens_response = 324; - RefreshCodeLens refresh_code_lens = 325; - - ToggleBreakpoint toggle_breakpoint = 326; - BreakpointsForFile breakpoints_for_file = 327; - - UpdateRepository update_repository = 328; - RemoveRepository remove_repository = 329; - - GetDocumentSymbols get_document_symbols = 330; - GetDocumentSymbolsResponse get_document_symbols_response = 331; - - LoadCommitDiff load_commit_diff = 334; - LoadCommitDiffResponse load_commit_diff_response = 335; - - StopLanguageServers stop_language_servers = 336; - - LspExtRunnables lsp_ext_runnables = 337; - LspExtRunnablesResponse lsp_ext_runnables_response = 338; - - GetDebugAdapterBinary get_debug_adapter_binary = 339; - DebugAdapterBinary debug_adapter_binary = 340; - RunDebugLocators run_debug_locators = 341; - DebugRequest debug_request = 342; - - LspExtGoToParentModule lsp_ext_go_to_parent_module = 343; - LspExtGoToParentModuleResponse lsp_ext_go_to_parent_module_response = 344; - LspExtCancelFlycheck lsp_ext_cancel_flycheck = 345; - LspExtRunFlycheck lsp_ext_run_flycheck = 346; - LspExtClearFlycheck lsp_ext_clear_flycheck = 347; - - LogToDebugConsole log_to_debug_console = 348; - - GetDocumentDiagnostics get_document_diagnostics = 350; - GetDocumentDiagnosticsResponse get_document_diagnostics_response = 351; - PullWorkspaceDiagnostics pull_workspace_diagnostics = 352; - - GetDocumentColor get_document_color = 353; - GetDocumentColorResponse get_document_color_response = 354; - GetColorPresentation get_color_presentation = 355; - GetColorPresentationResponse get_color_presentation_response = 356; - - Stash stash = 357; - StashPop stash_pop = 358; - - GetDefaultBranch get_default_branch = 359; - GetDefaultBranchResponse get_default_branch_response = 360; - - GetCrashFiles get_crash_files = 361; - GetCrashFilesResponse get_crash_files_response = 362; - - GitClone git_clone = 363; - GitCloneResponse git_clone_response = 364; - - LspQuery lsp_query = 365; - LspQueryResponse lsp_query_response = 366; - ToggleLspLogs toggle_lsp_logs = 367; - - UpdateUserSettings update_user_settings = 368; - - GetProcesses get_processes = 369; - GetProcessesResponse get_processes_response = 370; - - ResolveToolchain resolve_toolchain = 371; - ResolveToolchainResponse resolve_toolchain_response = 372; - - GetAgentServerCommand get_agent_server_command = 373; - AgentServerCommand agent_server_command = 374; - - ExternalAgentsUpdated external_agents_updated = 375; - ExternalAgentLoadingStatusUpdated external_agent_loading_status_updated = 376; - NewExternalAgentVersionAvailable new_external_agent_version_available = 377; - - StashDrop stash_drop = 378; - StashApply stash_apply = 379; - - GitRenameBranch git_rename_branch = 380; - - RemoteStarted remote_started = 381; - - GetDirectoryEnvironment get_directory_environment = 382; - DirectoryEnvironment directory_environment = 383; - - GetTreeDiff get_tree_diff = 384; - GetTreeDiffResponse get_tree_diff_response = 385; - - GetBlobContent get_blob_content = 386; - GetBlobContentResponse get_blob_content_response = 387; - - GitWorktreesResponse git_worktrees_response = 388; - GitGetWorktrees git_get_worktrees = 389; - GitCreateWorktree git_create_worktree = 390; - - OpenImageByPath open_image_by_path = 391; - OpenImageResponse open_image_response = 392; - CreateImageForPeer create_image_for_peer = 393; - - - GitFileHistory git_file_history = 397; - GitFileHistoryResponse git_file_history_response = 398; - - RunGitHook run_git_hook = 399; - - GitDeleteBranch git_delete_branch = 400; - - ExternalExtensionAgentsUpdated external_extension_agents_updated = 401; - - GitCreateRemote git_create_remote = 402; - GitRemoveRemote git_remove_remote = 403;// current max - } - - reserved 87 to 88, 396; - reserved 102 to 103; - reserved 158 to 161; - reserved 164; - reserved 166 to 169; - reserved 175 to 185; - reserved 188; - reserved 189 to 192; - reserved 193 to 195; - reserved 197; - reserved 200 to 202; - reserved 205 to 206; - reserved 221; - reserved 224 to 229; - reserved 230 to 231; - reserved 234 to 236; - reserved 239 to 240; - reserved 246; - reserved 247 to 254; - reserved 255 to 256; - reserved 259; - reserved 270; - reserved 280 to 281; - reserved 332 to 333; - reserved 394 to 395; -} - -message Hello { - PeerId peer_id = 1; -} - -message Ping {} - -message Ack {} - -message Error { - string message = 1; - ErrorCode code = 2; - repeated string tags = 3; -} - -enum ErrorCode { - Internal = 0; - NoSuchChannel = 1; - Disconnected = 2; - SignedOut = 3; - UpgradeRequired = 4; - Forbidden = 5; - NeedsCla = 7; - NotARootChannel = 8; - BadPublicNesting = 9; - CircularNesting = 10; - WrongMoveTarget = 11; - UnsharedItem = 12; - NoSuchProject = 13; - DevServerProjectPathDoesNotExist = 16; - RemoteUpgradeRequired = 17; - RateLimitExceeded = 18; - CommitFailed = 19; - reserved 6; - reserved 14 to 15; -} - -message EndStream {} - -message Test { - uint64 id = 1; -} - -message FlushBufferedMessages {} - -message FlushBufferedMessagesResponse {} - -message RemoteStarted {} diff --git a/crates/proto/src/error.rs b/crates/proto/src/error.rs deleted file mode 100644 index d83b0fc499..0000000000 --- a/crates/proto/src/error.rs +++ /dev/null @@ -1,250 +0,0 @@ -/// Some helpers for structured error handling. -/// -/// The helpers defined here allow you to pass type-safe error codes from -/// the collab server to the client; and provide a mechanism for additional -/// structured data alongside the message. -/// -/// When returning an error, it can be as simple as: -/// -/// `return Err(Error::Forbidden.into())` -/// -/// If you'd like to log more context, you can set a message. These messages -/// show up in our logs, but are not shown visibly to users. -/// -/// `return Err(Error::Forbidden.message("not an admin").into())` -/// -/// If you'd like to provide enough context that the UI can render a good error -/// message (or would be helpful to see in a structured format in the logs), you -/// can use .with_tag(): -/// -/// `return Err(Error::WrongReleaseChannel.with_tag("required", "stable").into())` -/// -/// When handling an error you can use .error_code() to match which error it was -/// and .error_tag() to read any tags. -/// -/// ```ignore -/// use proto::{ErrorCode, ErrorExt}; -/// -/// match err.error_code() { -/// ErrorCode::Forbidden => alert("I'm sorry I can't do that."), -/// ErrorCode::WrongReleaseChannel => -/// alert(format!("You need to be on the {} release channel.", err.error_tag("required").unwrap())), -/// ErrorCode::Internal => alert("Sorry, something went wrong"), -/// } -/// ``` -/// -pub use crate::ErrorCode; - -/// ErrorCodeExt provides some helpers for structured error handling. -/// -/// The primary implementation is on the proto::ErrorCode to easily convert -/// that into an anyhow::Error, which we use pervasively. -/// -/// The RpcError struct provides support for further metadata if needed. -pub trait ErrorCodeExt { - /// Return an anyhow::Error containing this. - /// (useful in places where .into() doesn't have enough type information) - fn anyhow(self) -> anyhow::Error; - - /// Add a message to the error (by default the error code is used) - fn message(self, msg: String) -> RpcError; - - /// Add a tag to the error. Tags are key value pairs that can be used - /// to send semi-structured data along with the error. - fn with_tag(self, k: &str, v: &str) -> RpcError; -} - -impl ErrorCodeExt for ErrorCode { - fn anyhow(self) -> anyhow::Error { - self.into() - } - - fn message(self, msg: String) -> RpcError { - let err: RpcError = self.into(); - err.message(msg) - } - - fn with_tag(self, k: &str, v: &str) -> RpcError { - let err: RpcError = self.into(); - err.with_tag(k, v) - } -} - -/// ErrorExt provides helpers for structured error handling. -/// -/// The primary implementation is on the anyhow::Error, which is -/// what we use throughout our codebase. Though under the hood this -pub trait ErrorExt { - /// error_code() returns the ErrorCode (or ErrorCode::Internal if there is none) - fn error_code(&self) -> ErrorCode; - /// error_tag() returns the value of the tag with the given key, if any. - fn error_tag(&self, k: &str) -> Option<&str>; - /// to_proto() converts the error into a crate::Error - fn to_proto(&self) -> crate::Error; - /// Clones the error and turns into an [anyhow::Error]. - fn cloned(&self) -> anyhow::Error; -} - -impl ErrorExt for anyhow::Error { - fn error_code(&self) -> ErrorCode { - if let Some(rpc_error) = self.downcast_ref::() { - rpc_error.code - } else { - ErrorCode::Internal - } - } - - fn error_tag(&self, k: &str) -> Option<&str> { - if let Some(rpc_error) = self.downcast_ref::() { - rpc_error.error_tag(k) - } else { - None - } - } - - fn to_proto(&self) -> crate::Error { - if let Some(rpc_error) = self.downcast_ref::() { - rpc_error.to_proto() - } else { - ErrorCode::Internal - .message( - format!("{self:#}") - .lines() - .fold(String::new(), |mut message, line| { - if !message.is_empty() { - message.push(' '); - } - message.push_str(line); - message - }), - ) - .to_proto() - } - } - - fn cloned(&self) -> anyhow::Error { - if let Some(rpc_error) = self.downcast_ref::() { - rpc_error.cloned() - } else { - anyhow::anyhow!("{self:#}") - } - } -} - -impl From for anyhow::Error { - fn from(value: ErrorCode) -> Self { - RpcError { - request: None, - code: value, - msg: format!("{:?}", value), - tags: Default::default(), - } - .into() - } -} - -#[derive(Clone, Debug)] -pub struct RpcError { - request: Option, - msg: String, - code: ErrorCode, - tags: Vec, -} - -/// RpcError is a structured error type that is returned by the collab server. -/// In addition to a message, it lets you set a specific ErrorCode, and attach -/// small amounts of metadata to help the client handle the error appropriately. -/// -/// This struct is not typically used directly, as we pass anyhow::Error around -/// in the app; however it is useful for chaining .message() and .with_tag() on -/// ErrorCode. -impl RpcError { - /// from_proto converts a crate::Error into an anyhow::Error containing - /// an RpcError. - pub fn from_proto(error: &crate::Error, request: &str) -> anyhow::Error { - RpcError { - request: Some(request.to_string()), - code: error.code(), - msg: error.message.clone(), - tags: error.tags.clone(), - } - .into() - } -} - -impl ErrorCodeExt for RpcError { - fn message(mut self, msg: String) -> RpcError { - self.msg = msg; - self - } - - fn with_tag(mut self, k: &str, v: &str) -> RpcError { - self.tags.push(format!("{}={}", k, v)); - self - } - - fn anyhow(self) -> anyhow::Error { - self.into() - } -} - -impl ErrorExt for RpcError { - fn error_tag(&self, k: &str) -> Option<&str> { - for tag in &self.tags { - let mut parts = tag.split('='); - if let Some(key) = parts.next() - && key == k - { - return parts.next(); - } - } - None - } - - fn error_code(&self) -> ErrorCode { - self.code - } - - fn to_proto(&self) -> crate::Error { - crate::Error { - code: self.code as i32, - message: self.msg.clone(), - tags: self.tags.clone(), - } - } - - fn cloned(&self) -> anyhow::Error { - self.clone().into() - } -} - -impl std::error::Error for RpcError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - None - } -} - -impl std::fmt::Display for RpcError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - if let Some(request) = &self.request { - write!(f, "RPC request {} failed: {}", request, self.msg)? - } else { - write!(f, "{}", self.msg)? - } - for tag in &self.tags { - write!(f, " {}", tag)? - } - Ok(()) - } -} - -impl From for RpcError { - fn from(code: ErrorCode) -> Self { - RpcError { - request: None, - code, - msg: format!("{:?}", code), - tags: Default::default(), - } - } -} diff --git a/crates/proto/src/macros.rs b/crates/proto/src/macros.rs deleted file mode 100644 index 59e984d7db..0000000000 --- a/crates/proto/src/macros.rs +++ /dev/null @@ -1,100 +0,0 @@ -#[macro_export] -macro_rules! messages { - ($(($name:ident, $priority:ident)),* $(,)?) => { - pub fn build_typed_envelope(sender_id: PeerId, received_at: std::time::Instant, envelope: Envelope) -> Option> { - match envelope.payload { - $(Some(envelope::Payload::$name(payload)) => { - Some(Box::new(TypedEnvelope { - sender_id, - original_sender_id: envelope.original_sender_id, - message_id: envelope.id, - payload, - received_at, - })) - }, )* - _ => None - } - } - - $( - impl EnvelopedMessage for $name { - const NAME: &'static str = std::stringify!($name); - const PRIORITY: MessagePriority = MessagePriority::$priority; - - fn into_envelope( - self, - id: u32, - responding_to: Option, - original_sender_id: Option, - ) -> Envelope { - Envelope { - id, - responding_to, - original_sender_id, - payload: Some(envelope::Payload::$name(self)), - ack_id: None, - } - } - - fn from_envelope(envelope: Envelope) -> Option { - if let Some(envelope::Payload::$name(msg)) = envelope.payload { - Some(msg) - } else { - None - } - } - } - )* - }; -} - -#[macro_export] -macro_rules! request_messages { - ($(($request_name:ident, $response_name:ident)),* $(,)?) => { - $(impl RequestMessage for $request_name { - type Response = $response_name; - })* - }; -} - -#[macro_export] -macro_rules! entity_messages { - ({$id_field:ident, $entity_type:ty}, $($name:ident),* $(,)?) => { - $(impl EntityMessage for $name { - type Entity = $entity_type; - - fn remote_entity_id(&self) -> u64 { - self.$id_field - } - })* - }; -} - -#[macro_export] -macro_rules! lsp_messages { - ($(($request_name:ident, $response_name:ident, $stop_previous_requests:expr)),* $(,)?) => { - $(impl LspRequestMessage for $request_name { - type Response = $response_name; - - fn to_proto_query(self) -> $crate::lsp_query::Request { - $crate::lsp_query::Request::$request_name(self) - } - - fn response_to_proto_query(response: Self::Response) -> $crate::lsp_response::Response { - $crate::lsp_response::Response::$response_name(response) - } - - fn buffer_id(&self) -> u64 { - self.buffer_id - } - - fn buffer_version(&self) -> &[$crate::VectorClockEntry] { - &self.version - } - - fn stop_previous_requests() -> bool { - $stop_previous_requests - } - })* - }; -} diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs deleted file mode 100644 index 455f947046..0000000000 --- a/crates/proto/src/proto.rs +++ /dev/null @@ -1,923 +0,0 @@ -#![allow(non_snake_case)] - -pub mod error; -mod macros; -mod typed_envelope; - -pub use error::*; -pub use prost::{DecodeError, Message}; -use std::{ - cmp, - fmt::Debug, - iter, mem, - time::{Duration, SystemTime, UNIX_EPOCH}, -}; -pub use typed_envelope::*; - -include!(concat!(env!("OUT_DIR"), "/zed.messages.rs")); - -pub const REMOTE_SERVER_PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 }; -pub const REMOTE_SERVER_PROJECT_ID: u64 = 0; - -messages!( - (Ack, Foreground), - (AckBufferOperation, Background), - (AckChannelMessage, Background), - (ActivateToolchain, Foreground), - (ActiveToolchain, Foreground), - (ActiveToolchainResponse, Foreground), - (ResolveToolchain, Background), - (ResolveToolchainResponse, Background), - (AddNotification, Foreground), - (AddProjectCollaborator, Foreground), - (AddWorktree, Foreground), - (AddWorktreeResponse, Foreground), - (AdvertiseContexts, Foreground), - (ApplyCodeAction, Background), - (ApplyCodeActionResponse, Background), - (ApplyCompletionAdditionalEdits, Background), - (ApplyCompletionAdditionalEditsResponse, Background), - (BlameBuffer, Foreground), - (BlameBufferResponse, Foreground), - (BufferReloaded, Foreground), - (BufferSaved, Foreground), - (Call, Foreground), - (CallCanceled, Foreground), - (CancelCall, Foreground), - (CancelLanguageServerWork, Foreground), - (ChannelMessageSent, Foreground), - (ChannelMessageUpdate, Foreground), - (CloseBuffer, Foreground), - (Commit, Background), - (RunGitHook, Background), - (CopyProjectEntry, Foreground), - (CreateBufferForPeer, Foreground), - (CreateImageForPeer, Foreground), - (CreateChannel, Foreground), - (CreateChannelResponse, Foreground), - (CreateContext, Foreground), - (CreateContextResponse, Foreground), - (CreateProjectEntry, Foreground), - (CreateRoom, Foreground), - (CreateRoomResponse, Foreground), - (DeclineCall, Foreground), - (DeleteChannel, Foreground), - (DeleteNotification, Foreground), - (DeleteProjectEntry, Foreground), - (EndStream, Foreground), - (Error, Foreground), - (ExpandProjectEntry, Foreground), - (ExpandProjectEntryResponse, Foreground), - (FindSearchCandidatesResponse, Background), - (FindSearchCandidates, Background), - (FlushBufferedMessages, Foreground), - (ExpandAllForProjectEntry, Foreground), - (ExpandAllForProjectEntryResponse, Foreground), - (Follow, Foreground), - (FollowResponse, Foreground), - (ApplyCodeActionKind, Foreground), - (ApplyCodeActionKindResponse, Foreground), - (FormatBuffers, Foreground), - (FormatBuffersResponse, Foreground), - (FuzzySearchUsers, Foreground), - (GetChannelMembers, Foreground), - (GetChannelMembersResponse, Foreground), - (GetChannelMessages, Background), - (GetChannelMessagesById, Background), - (GetChannelMessagesResponse, Background), - (GetCodeActions, Background), - (GetCodeActionsResponse, Background), - (GetCompletions, Background), - (GetCompletionsResponse, Background), - (GetDeclaration, Background), - (GetDeclarationResponse, Background), - (GetDefinition, Background), - (GetDefinitionResponse, Background), - (GetDocumentHighlights, Background), - (GetDocumentHighlightsResponse, Background), - (GetDocumentSymbols, Background), - (GetDocumentSymbolsResponse, Background), - (GetHover, Background), - (GetHoverResponse, Background), - (GetNotifications, Foreground), - (GetNotificationsResponse, Foreground), - (GetCrashFiles, Background), - (GetCrashFilesResponse, Background), - (GetPathMetadata, Background), - (GetPathMetadataResponse, Background), - (GetPermalinkToLine, Foreground), - (GetProcesses, Background), - (GetProcessesResponse, Background), - (GetPermalinkToLineResponse, Foreground), - (GetProjectSymbols, Background), - (GetProjectSymbolsResponse, Background), - (GetReferences, Background), - (GetReferencesResponse, Background), - (GetSignatureHelp, Background), - (GetSignatureHelpResponse, Background), - (GetSupermavenApiKey, Background), - (GetSupermavenApiKeyResponse, Background), - (GetTypeDefinition, Background), - (GetTypeDefinitionResponse, Background), - (GetImplementation, Background), - (GetImplementationResponse, Background), - (OpenUnstagedDiff, Foreground), - (OpenUnstagedDiffResponse, Foreground), - (OpenUncommittedDiff, Foreground), - (OpenUncommittedDiffResponse, Foreground), - (GetUsers, Foreground), - (GitGetBranches, Background), - (GitBranchesResponse, Background), - (Hello, Foreground), - (HideToast, Background), - (IncomingCall, Foreground), - (InlayHints, Background), - (InlayHintsResponse, Background), - (InstallExtension, Background), - (InviteChannelMember, Foreground), - (JoinChannel, Foreground), - (JoinChannelBuffer, Foreground), - (JoinChannelBufferResponse, Foreground), - (JoinChannelChat, Foreground), - (JoinChannelChatResponse, Foreground), - (JoinProject, Foreground), - (JoinProjectResponse, Foreground), - (JoinRoom, Foreground), - (JoinRoomResponse, Foreground), - (LanguageServerLog, Foreground), - (LanguageServerPromptRequest, Foreground), - (LanguageServerPromptResponse, Foreground), - (LeaveChannelBuffer, Background), - (LeaveChannelChat, Foreground), - (LeaveProject, Foreground), - (LeaveRoom, Foreground), - (LinkedEditingRange, Background), - (LinkedEditingRangeResponse, Background), - (ListRemoteDirectory, Background), - (ListRemoteDirectoryResponse, Background), - (ListToolchains, Foreground), - (ListToolchainsResponse, Foreground), - (LoadCommitDiff, Foreground), - (LoadCommitDiffResponse, Foreground), - (LspExtExpandMacro, Background), - (LspExtExpandMacroResponse, Background), - (LspExtOpenDocs, Background), - (LspExtOpenDocsResponse, Background), - (LspExtRunnables, Background), - (LspExtRunnablesResponse, Background), - (LspExtSwitchSourceHeader, Background), - (LspExtSwitchSourceHeaderResponse, Background), - (LspExtGoToParentModule, Background), - (LspExtGoToParentModuleResponse, Background), - (LspExtCancelFlycheck, Background), - (LspExtRunFlycheck, Background), - (LspExtClearFlycheck, Background), - (MarkNotificationRead, Foreground), - (MoveChannel, Foreground), - (ReorderChannel, Foreground), - (LspQuery, Background), - (LspQueryResponse, Background), - (OnTypeFormatting, Background), - (OnTypeFormattingResponse, Background), - (OpenBufferById, Background), - (OpenBufferByPath, Background), - (OpenImageByPath, Background), - (OpenBufferForSymbol, Background), - (OpenBufferForSymbolResponse, Background), - (OpenBufferResponse, Background), - (OpenImageResponse, Background), - (OpenCommitMessageBuffer, Background), - (OpenContext, Foreground), - (OpenContextResponse, Foreground), - (OpenNewBuffer, Foreground), - (OpenServerSettings, Foreground), - (PerformRename, Background), - (PerformRenameResponse, Background), - (Ping, Foreground), - (PrepareRename, Background), - (PrepareRenameResponse, Background), - (ProjectEntryResponse, Foreground), - (RefreshInlayHints, Foreground), - (RegisterBufferWithLanguageServers, Background), - (RejoinChannelBuffers, Foreground), - (RejoinChannelBuffersResponse, Foreground), - (RejoinRemoteProjects, Foreground), - (RejoinRemoteProjectsResponse, Foreground), - (RejoinRoom, Foreground), - (RejoinRoomResponse, Foreground), - (ReloadBuffers, Foreground), - (ReloadBuffersResponse, Foreground), - (RemoveChannelMember, Foreground), - (RemoveChannelMessage, Foreground), - (RemoveContact, Foreground), - (RemoveProjectCollaborator, Foreground), - (RemoveWorktree, Foreground), - (RenameChannel, Foreground), - (RenameChannelResponse, Foreground), - (RenameProjectEntry, Foreground), - (RequestContact, Foreground), - (ResolveCompletionDocumentation, Background), - (ResolveCompletionDocumentationResponse, Background), - (ResolveInlayHint, Background), - (ResolveInlayHintResponse, Background), - (GetDocumentColor, Background), - (GetDocumentColorResponse, Background), - (GetColorPresentation, Background), - (GetColorPresentationResponse, Background), - (RefreshCodeLens, Background), - (GetCodeLens, Background), - (GetCodeLensResponse, Background), - (RespondToChannelInvite, Foreground), - (RespondToContactRequest, Foreground), - (RestartLanguageServers, Foreground), - (StopLanguageServers, Background), - (RoomUpdated, Foreground), - (SaveBuffer, Foreground), - (SendChannelMessage, Background), - (SendChannelMessageResponse, Background), - (SetChannelMemberRole, Foreground), - (SetChannelVisibility, Foreground), - (SetRoomParticipantRole, Foreground), - (ShareProject, Foreground), - (ShareProjectResponse, Foreground), - (ShowContacts, Foreground), - (ShutdownRemoteServer, Foreground), - (Stage, Background), - (StartLanguageServer, Foreground), - (SubscribeToChannels, Foreground), - (SyncExtensions, Background), - (SyncExtensionsResponse, Background), - (BreakpointsForFile, Background), - (ToggleBreakpoint, Foreground), - (SynchronizeBuffers, Foreground), - (SynchronizeBuffersResponse, Foreground), - (SynchronizeContexts, Foreground), - (SynchronizeContextsResponse, Foreground), - (TaskContext, Background), - (TaskContextForLocation, Background), - (Test, Foreground), - (Toast, Background), - (Unfollow, Foreground), - (UnshareProject, Foreground), - (Unstage, Background), - (Stash, Background), - (StashPop, Background), - (StashApply, Background), - (StashDrop, Background), - (UpdateBuffer, Foreground), - (UpdateBufferFile, Foreground), - (UpdateChannelBuffer, Foreground), - (UpdateChannelBufferCollaborators, Foreground), - (UpdateChannelMessage, Foreground), - (UpdateChannels, Foreground), - (UpdateContacts, Foreground), - (UpdateContext, Foreground), - (UpdateDiagnosticSummary, Foreground), - (UpdateDiffBases, Foreground), - (UpdateFollowers, Foreground), - (UpdateGitBranch, Background), - (UpdateInviteInfo, Foreground), - (UpdateLanguageServer, Foreground), - (UpdateNotification, Foreground), - (UpdateParticipantLocation, Foreground), - (UpdateProject, Foreground), - (UpdateProjectCollaborator, Foreground), - (UpdateUserChannels, Foreground), - (UpdateWorktree, Foreground), - (UpdateWorktreeSettings, Foreground), - (UpdateUserSettings, Background), - (UpdateRepository, Foreground), - (RemoveRepository, Foreground), - (UsersResponse, Foreground), - (GitReset, Background), - (GitDeleteBranch, Background), - (GitCheckoutFiles, Background), - (GitShow, Background), - (GitCommitDetails, Background), - (GitFileHistory, Background), - (GitFileHistoryResponse, Background), - (SetIndexText, Background), - (Push, Background), - (Fetch, Background), - (GetRemotes, Background), - (GetRemotesResponse, Background), - (Pull, Background), - (RemoteMessageResponse, Background), - (AskPassRequest, Background), - (AskPassResponse, Background), - (GitCreateRemote, Background), - (GitRemoveRemote, Background), - (GitCreateBranch, Background), - (GitChangeBranch, Background), - (GitRenameBranch, Background), - (CheckForPushedCommits, Background), - (CheckForPushedCommitsResponse, Background), - (GitDiff, Background), - (GitDiffResponse, Background), - (GitInit, Background), - (GetDebugAdapterBinary, Background), - (DebugAdapterBinary, Background), - (RunDebugLocators, Background), - (DebugRequest, Background), - (LogToDebugConsole, Background), - (GetDocumentDiagnostics, Background), - (GetDocumentDiagnosticsResponse, Background), - (PullWorkspaceDiagnostics, Background), - (GetDefaultBranch, Background), - (GetDefaultBranchResponse, Background), - (GetTreeDiff, Background), - (GetTreeDiffResponse, Background), - (GetBlobContent, Background), - (GetBlobContentResponse, Background), - (GitClone, Background), - (GitCloneResponse, Background), - (ToggleLspLogs, Background), - (GetDirectoryEnvironment, Background), - (DirectoryEnvironment, Background), - (GetAgentServerCommand, Background), - (AgentServerCommand, Background), - (ExternalAgentsUpdated, Background), - (ExternalExtensionAgentsUpdated, Background), - (ExternalAgentLoadingStatusUpdated, Background), - (NewExternalAgentVersionAvailable, Background), - (RemoteStarted, Background), - (GitGetWorktrees, Background), - (GitWorktreesResponse, Background), - (GitCreateWorktree, Background) -); - -request_messages!( - (ApplyCodeAction, ApplyCodeActionResponse), - ( - ApplyCompletionAdditionalEdits, - ApplyCompletionAdditionalEditsResponse - ), - (Call, Ack), - (CancelCall, Ack), - (Commit, Ack), - (RunGitHook, Ack), - (CopyProjectEntry, ProjectEntryResponse), - (CreateChannel, CreateChannelResponse), - (CreateProjectEntry, ProjectEntryResponse), - (CreateRoom, CreateRoomResponse), - (DeclineCall, Ack), - (DeleteChannel, Ack), - (DeleteProjectEntry, ProjectEntryResponse), - (ExpandProjectEntry, ExpandProjectEntryResponse), - (ExpandAllForProjectEntry, ExpandAllForProjectEntryResponse), - (Follow, FollowResponse), - (ApplyCodeActionKind, ApplyCodeActionKindResponse), - (FormatBuffers, FormatBuffersResponse), - (FuzzySearchUsers, UsersResponse), - (GetChannelMembers, GetChannelMembersResponse), - (GetChannelMessages, GetChannelMessagesResponse), - (GetChannelMessagesById, GetChannelMessagesResponse), - (GetCodeActions, GetCodeActionsResponse), - (GetCompletions, GetCompletionsResponse), - (GetDefinition, GetDefinitionResponse), - (GetDeclaration, GetDeclarationResponse), - (GetImplementation, GetImplementationResponse), - (GetDocumentHighlights, GetDocumentHighlightsResponse), - (GetDocumentSymbols, GetDocumentSymbolsResponse), - (GetHover, GetHoverResponse), - (GetNotifications, GetNotificationsResponse), - (GetProjectSymbols, GetProjectSymbolsResponse), - (GetReferences, GetReferencesResponse), - (GetSignatureHelp, GetSignatureHelpResponse), - (OpenUnstagedDiff, OpenUnstagedDiffResponse), - (OpenUncommittedDiff, OpenUncommittedDiffResponse), - (GetSupermavenApiKey, GetSupermavenApiKeyResponse), - (GetTypeDefinition, GetTypeDefinitionResponse), - (LinkedEditingRange, LinkedEditingRangeResponse), - (ListRemoteDirectory, ListRemoteDirectoryResponse), - (GetUsers, UsersResponse), - (IncomingCall, Ack), - (InlayHints, InlayHintsResponse), - (GetCodeLens, GetCodeLensResponse), - (InviteChannelMember, Ack), - (JoinChannel, JoinRoomResponse), - (JoinChannelBuffer, JoinChannelBufferResponse), - (JoinChannelChat, JoinChannelChatResponse), - (JoinProject, JoinProjectResponse), - (JoinRoom, JoinRoomResponse), - (LeaveChannelBuffer, Ack), - (LeaveRoom, Ack), - (LoadCommitDiff, LoadCommitDiffResponse), - (MarkNotificationRead, Ack), - (MoveChannel, Ack), - (OnTypeFormatting, OnTypeFormattingResponse), - (OpenBufferById, OpenBufferResponse), - (OpenBufferByPath, OpenBufferResponse), - (OpenImageByPath, OpenImageResponse), - (OpenBufferForSymbol, OpenBufferForSymbolResponse), - (OpenCommitMessageBuffer, OpenBufferResponse), - (OpenNewBuffer, OpenBufferResponse), - (PerformRename, PerformRenameResponse), - (Ping, Ack), - (PrepareRename, PrepareRenameResponse), - (RefreshInlayHints, Ack), - (RefreshCodeLens, Ack), - (RejoinChannelBuffers, RejoinChannelBuffersResponse), - (RejoinRoom, RejoinRoomResponse), - (ReloadBuffers, ReloadBuffersResponse), - (RemoveChannelMember, Ack), - (RemoveChannelMessage, Ack), - (UpdateChannelMessage, Ack), - (RemoveContact, Ack), - (RenameChannel, RenameChannelResponse), - (RenameProjectEntry, ProjectEntryResponse), - (ReorderChannel, Ack), - (RequestContact, Ack), - ( - ResolveCompletionDocumentation, - ResolveCompletionDocumentationResponse - ), - (ResolveInlayHint, ResolveInlayHintResponse), - (GetDocumentColor, GetDocumentColorResponse), - (GetColorPresentation, GetColorPresentationResponse), - (RespondToChannelInvite, Ack), - (RespondToContactRequest, Ack), - (SaveBuffer, BufferSaved), - (Stage, Ack), - (FindSearchCandidates, FindSearchCandidatesResponse), - (SendChannelMessage, SendChannelMessageResponse), - (SetChannelMemberRole, Ack), - (SetChannelVisibility, Ack), - (ShareProject, ShareProjectResponse), - (SynchronizeBuffers, SynchronizeBuffersResponse), - (TaskContextForLocation, TaskContext), - (Test, Test), - (Unstage, Ack), - (Stash, Ack), - (StashPop, Ack), - (StashApply, Ack), - (StashDrop, Ack), - (UpdateBuffer, Ack), - (UpdateParticipantLocation, Ack), - (UpdateProject, Ack), - (UpdateWorktree, Ack), - (UpdateRepository, Ack), - (RemoveRepository, Ack), - (LspExtExpandMacro, LspExtExpandMacroResponse), - (LspExtOpenDocs, LspExtOpenDocsResponse), - (LspExtRunnables, LspExtRunnablesResponse), - (SetRoomParticipantRole, Ack), - (BlameBuffer, BlameBufferResponse), - (RejoinRemoteProjects, RejoinRemoteProjectsResponse), - (LspQuery, Ack), - (LspQueryResponse, Ack), - (RestartLanguageServers, Ack), - (StopLanguageServers, Ack), - (OpenContext, OpenContextResponse), - (CreateContext, CreateContextResponse), - (SynchronizeContexts, SynchronizeContextsResponse), - (LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse), - (LspExtGoToParentModule, LspExtGoToParentModuleResponse), - (LspExtCancelFlycheck, Ack), - (LspExtRunFlycheck, Ack), - (LspExtClearFlycheck, Ack), - (AddWorktree, AddWorktreeResponse), - (ShutdownRemoteServer, Ack), - (RemoveWorktree, Ack), - (OpenServerSettings, OpenBufferResponse), - (GetPermalinkToLine, GetPermalinkToLineResponse), - (FlushBufferedMessages, Ack), - (LanguageServerPromptRequest, LanguageServerPromptResponse), - (GitGetBranches, GitBranchesResponse), - (UpdateGitBranch, Ack), - (ListToolchains, ListToolchainsResponse), - (ActivateToolchain, Ack), - (ActiveToolchain, ActiveToolchainResponse), - (ResolveToolchain, ResolveToolchainResponse), - (GetPathMetadata, GetPathMetadataResponse), - (GetCrashFiles, GetCrashFilesResponse), - (CancelLanguageServerWork, Ack), - (SyncExtensions, SyncExtensionsResponse), - (InstallExtension, Ack), - (RegisterBufferWithLanguageServers, Ack), - (GitShow, GitCommitDetails), - (GitFileHistory, GitFileHistoryResponse), - (GitReset, Ack), - (GitDeleteBranch, Ack), - (GitCheckoutFiles, Ack), - (SetIndexText, Ack), - (Push, RemoteMessageResponse), - (Fetch, RemoteMessageResponse), - (GetRemotes, GetRemotesResponse), - (Pull, RemoteMessageResponse), - (AskPassRequest, AskPassResponse), - (GitCreateRemote, Ack), - (GitRemoveRemote, Ack), - (GitCreateBranch, Ack), - (GitChangeBranch, Ack), - (GitRenameBranch, Ack), - (CheckForPushedCommits, CheckForPushedCommitsResponse), - (GitDiff, GitDiffResponse), - (GitInit, Ack), - (ToggleBreakpoint, Ack), - (GetDebugAdapterBinary, DebugAdapterBinary), - (RunDebugLocators, DebugRequest), - (GetDocumentDiagnostics, GetDocumentDiagnosticsResponse), - (PullWorkspaceDiagnostics, Ack), - (GetDefaultBranch, GetDefaultBranchResponse), - (GetBlobContent, GetBlobContentResponse), - (GetTreeDiff, GetTreeDiffResponse), - (GitClone, GitCloneResponse), - (ToggleLspLogs, Ack), - (GetDirectoryEnvironment, DirectoryEnvironment), - (GetProcesses, GetProcessesResponse), - (GetAgentServerCommand, AgentServerCommand), - (RemoteStarted, Ack), - (GitGetWorktrees, GitWorktreesResponse), - (GitCreateWorktree, Ack) -); - -lsp_messages!( - (GetReferences, GetReferencesResponse, true), - (GetDocumentColor, GetDocumentColorResponse, true), - (GetHover, GetHoverResponse, true), - (GetCodeActions, GetCodeActionsResponse, true), - (GetSignatureHelp, GetSignatureHelpResponse, true), - (GetCodeLens, GetCodeLensResponse, true), - (GetDocumentDiagnostics, GetDocumentDiagnosticsResponse, true), - (GetDefinition, GetDefinitionResponse, true), - (GetDeclaration, GetDeclarationResponse, true), - (GetTypeDefinition, GetTypeDefinitionResponse, true), - (GetImplementation, GetImplementationResponse, true), - (InlayHints, InlayHintsResponse, false), -); - -entity_messages!( - {project_id, ShareProject}, - AddProjectCollaborator, - AddWorktree, - ApplyCodeAction, - ApplyCompletionAdditionalEdits, - BlameBuffer, - BufferReloaded, - BufferSaved, - CloseBuffer, - Commit, - RunGitHook, - GetColorPresentation, - CopyProjectEntry, - CreateBufferForPeer, - CreateImageForPeer, - CreateProjectEntry, - GetDocumentColor, - DeleteProjectEntry, - ExpandProjectEntry, - ExpandAllForProjectEntry, - FindSearchCandidates, - ApplyCodeActionKind, - FormatBuffers, - GetCodeActions, - GetCodeLens, - GetCompletions, - GetDefinition, - GetDeclaration, - GetImplementation, - GetDocumentHighlights, - GetDocumentSymbols, - GetHover, - GetProjectSymbols, - GetReferences, - GetSignatureHelp, - OpenUnstagedDiff, - OpenUncommittedDiff, - GetTypeDefinition, - InlayHints, - JoinProject, - LeaveProject, - LinkedEditingRange, - LoadCommitDiff, - LspQuery, - LspQueryResponse, - RestartLanguageServers, - StopLanguageServers, - OnTypeFormatting, - OpenNewBuffer, - OpenBufferById, - OpenBufferByPath, - OpenImageByPath, - OpenBufferForSymbol, - OpenCommitMessageBuffer, - PerformRename, - PrepareRename, - RefreshInlayHints, - RefreshCodeLens, - ReloadBuffers, - RemoveProjectCollaborator, - RenameProjectEntry, - ResolveCompletionDocumentation, - ResolveInlayHint, - SaveBuffer, - Stage, - StartLanguageServer, - SynchronizeBuffers, - TaskContextForLocation, - UnshareProject, - Unstage, - Stash, - StashPop, - StashApply, - StashDrop, - UpdateBuffer, - UpdateBufferFile, - UpdateDiagnosticSummary, - UpdateDiffBases, - UpdateLanguageServer, - UpdateProject, - UpdateProjectCollaborator, - UpdateWorktree, - UpdateRepository, - RemoveRepository, - UpdateWorktreeSettings, - UpdateUserSettings, - LspExtExpandMacro, - LspExtOpenDocs, - LspExtRunnables, - AdvertiseContexts, - OpenContext, - CreateContext, - UpdateContext, - SynchronizeContexts, - LspExtSwitchSourceHeader, - LspExtGoToParentModule, - LspExtCancelFlycheck, - LspExtRunFlycheck, - LspExtClearFlycheck, - LanguageServerLog, - Toast, - HideToast, - OpenServerSettings, - GetPermalinkToLine, - LanguageServerPromptRequest, - GitGetBranches, - UpdateGitBranch, - ListToolchains, - ActivateToolchain, - ActiveToolchain, - ResolveToolchain, - GetPathMetadata, - GetProcesses, - CancelLanguageServerWork, - RegisterBufferWithLanguageServers, - GitShow, - GitFileHistory, - GitReset, - GitDeleteBranch, - GitCheckoutFiles, - SetIndexText, - ToggleLspLogs, - GetDirectoryEnvironment, - - Push, - Fetch, - GetRemotes, - Pull, - AskPassRequest, - GitChangeBranch, - GitRenameBranch, - GitCreateBranch, - GitCreateRemote, - GitRemoveRemote, - CheckForPushedCommits, - GitDiff, - GitInit, - BreakpointsForFile, - ToggleBreakpoint, - RunDebugLocators, - GetDebugAdapterBinary, - LogToDebugConsole, - GetDocumentDiagnostics, - PullWorkspaceDiagnostics, - GetDefaultBranch, - GetTreeDiff, - GetBlobContent, - GitClone, - GetAgentServerCommand, - ExternalAgentsUpdated, - ExternalExtensionAgentsUpdated, - ExternalAgentLoadingStatusUpdated, - NewExternalAgentVersionAvailable, - GitGetWorktrees, - GitCreateWorktree -); - -entity_messages!( - {channel_id, Channel}, - ChannelMessageSent, - ChannelMessageUpdate, - RemoveChannelMessage, - UpdateChannelMessage, - UpdateChannelBuffer, - UpdateChannelBufferCollaborators, -); - -impl From for SystemTime { - fn from(val: Timestamp) -> Self { - UNIX_EPOCH - .checked_add(Duration::new(val.seconds, val.nanos)) - .unwrap() - } -} - -impl From for Timestamp { - fn from(time: SystemTime) -> Self { - let duration = time.duration_since(UNIX_EPOCH).unwrap_or_default(); - Self { - seconds: duration.as_secs(), - nanos: duration.subsec_nanos(), - } - } -} - -impl From for Nonce { - fn from(nonce: u128) -> Self { - let upper_half = (nonce >> 64) as u64; - let lower_half = nonce as u64; - Self { - upper_half, - lower_half, - } - } -} - -impl From for u128 { - fn from(nonce: Nonce) -> Self { - let upper_half = (nonce.upper_half as u128) << 64; - let lower_half = nonce.lower_half as u128; - upper_half | lower_half - } -} - -#[cfg(any(test, feature = "test-support"))] -pub const MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE: usize = 2; -#[cfg(not(any(test, feature = "test-support")))] -pub const MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE: usize = 256; - -pub fn split_worktree_update(mut message: UpdateWorktree) -> impl Iterator { - let mut done = false; - - iter::from_fn(move || { - if done { - return None; - } - - let updated_entries_chunk_size = cmp::min( - message.updated_entries.len(), - MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE, - ); - let updated_entries: Vec<_> = message - .updated_entries - .drain(..updated_entries_chunk_size) - .collect(); - - let removed_entries_chunk_size = cmp::min( - message.removed_entries.len(), - MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE, - ); - let removed_entries = message - .removed_entries - .drain(..removed_entries_chunk_size) - .collect(); - - let mut updated_repositories = Vec::new(); - let mut limit = MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE; - while let Some(repo) = message.updated_repositories.first_mut() { - let updated_statuses_limit = cmp::min(repo.updated_statuses.len(), limit); - let removed_statuses_limit = cmp::min(repo.removed_statuses.len(), limit); - - updated_repositories.push(RepositoryEntry { - repository_id: repo.repository_id, - branch_summary: repo.branch_summary.clone(), - updated_statuses: repo - .updated_statuses - .drain(..updated_statuses_limit) - .collect(), - removed_statuses: repo - .removed_statuses - .drain(..removed_statuses_limit) - .collect(), - current_merge_conflicts: repo.current_merge_conflicts.clone(), - }); - if repo.removed_statuses.is_empty() && repo.updated_statuses.is_empty() { - message.updated_repositories.remove(0); - } - limit = limit.saturating_sub(removed_statuses_limit + updated_statuses_limit); - if limit == 0 { - break; - } - } - - done = message.updated_entries.is_empty() - && message.removed_entries.is_empty() - && message.updated_repositories.is_empty(); - - let removed_repositories = if done { - mem::take(&mut message.removed_repositories) - } else { - Default::default() - }; - - Some(UpdateWorktree { - project_id: message.project_id, - worktree_id: message.worktree_id, - root_name: message.root_name.clone(), - abs_path: message.abs_path.clone(), - updated_entries, - removed_entries, - scan_id: message.scan_id, - is_last_update: done && message.is_last_update, - updated_repositories, - removed_repositories, - }) - }) -} - -pub fn split_repository_update( - mut update: UpdateRepository, -) -> impl Iterator { - let mut updated_statuses_iter = mem::take(&mut update.updated_statuses).into_iter().fuse(); - let mut removed_statuses_iter = mem::take(&mut update.removed_statuses).into_iter().fuse(); - std::iter::from_fn({ - let update = update.clone(); - move || { - let updated_statuses = updated_statuses_iter - .by_ref() - .take(MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE) - .collect::>(); - let removed_statuses = removed_statuses_iter - .by_ref() - .take(MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE) - .collect::>(); - if updated_statuses.is_empty() && removed_statuses.is_empty() { - return None; - } - Some(UpdateRepository { - updated_statuses, - removed_statuses, - is_last_update: false, - ..update.clone() - }) - } - }) - .chain([UpdateRepository { - updated_statuses: Vec::new(), - removed_statuses: Vec::new(), - is_last_update: true, - ..update - }]) -} - -impl LspQuery { - pub fn query_name_and_write_permissions(&self) -> (&str, bool) { - match self.request { - Some(lsp_query::Request::GetHover(_)) => ("GetHover", false), - Some(lsp_query::Request::GetCodeActions(_)) => ("GetCodeActions", true), - Some(lsp_query::Request::GetSignatureHelp(_)) => ("GetSignatureHelp", false), - Some(lsp_query::Request::GetCodeLens(_)) => ("GetCodeLens", true), - Some(lsp_query::Request::GetDocumentDiagnostics(_)) => { - ("GetDocumentDiagnostics", false) - } - Some(lsp_query::Request::GetDefinition(_)) => ("GetDefinition", false), - Some(lsp_query::Request::GetDeclaration(_)) => ("GetDeclaration", false), - Some(lsp_query::Request::GetTypeDefinition(_)) => ("GetTypeDefinition", false), - Some(lsp_query::Request::GetImplementation(_)) => ("GetImplementation", false), - Some(lsp_query::Request::GetReferences(_)) => ("GetReferences", false), - Some(lsp_query::Request::GetDocumentColor(_)) => ("GetDocumentColor", false), - Some(lsp_query::Request::InlayHints(_)) => ("InlayHints", false), - None => ("", true), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_converting_peer_id_from_and_to_u64() { - let peer_id = PeerId { - owner_id: 10, - id: 3, - }; - assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id); - let peer_id = PeerId { - owner_id: u32::MAX, - id: 3, - }; - assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id); - let peer_id = PeerId { - owner_id: 10, - id: u32::MAX, - }; - assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id); - let peer_id = PeerId { - owner_id: u32::MAX, - id: u32::MAX, - }; - assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id); - } -} diff --git a/crates/proto/src/typed_envelope.rs b/crates/proto/src/typed_envelope.rs deleted file mode 100644 index a7a8a1c7a2..0000000000 --- a/crates/proto/src/typed_envelope.rs +++ /dev/null @@ -1,210 +0,0 @@ -use crate::{Envelope, PeerId}; -use anyhow::{Context as _, Result}; -use serde::Serialize; -use std::{ - any::{Any, TypeId}, - cmp, - fmt::{self, Debug}, -}; -use std::{marker::PhantomData, time::Instant}; - -pub trait EnvelopedMessage: Clone + Debug + Serialize + Sized + Send + Sync + 'static { - const NAME: &'static str; - const PRIORITY: MessagePriority; - fn into_envelope( - self, - id: u32, - responding_to: Option, - original_sender_id: Option, - ) -> Envelope; - fn from_envelope(envelope: Envelope) -> Option; -} - -pub trait EntityMessage: EnvelopedMessage { - type Entity; - fn remote_entity_id(&self) -> u64; -} - -pub trait RequestMessage: EnvelopedMessage { - type Response: EnvelopedMessage; -} - -/// A trait to bind LSP request and responses for the proto layer. -/// Should be used for every LSP request that has to traverse through the proto layer. -/// -/// `lsp_messages` macro in the same crate provides a convenient way to implement this. -pub trait LspRequestMessage: EnvelopedMessage { - type Response: EnvelopedMessage; - - fn to_proto_query(self) -> crate::lsp_query::Request; - - fn response_to_proto_query(response: Self::Response) -> crate::lsp_response::Response; - - fn buffer_id(&self) -> u64; - - fn buffer_version(&self) -> &[crate::VectorClockEntry]; - - /// Whether to deduplicate the requests, or keep the previous ones running when another - /// request of the same kind is processed. - fn stop_previous_requests() -> bool; -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct LspRequestId(pub u64); - -/// A response from a single language server. -/// There could be multiple responses for a single LSP request, -/// from different servers. -pub struct ProtoLspResponse { - pub server_id: u64, - pub response: R, -} - -impl ProtoLspResponse> { - pub fn into_response(self) -> Result> { - let envelope = self - .response - .into_any() - .downcast::>() - .map_err(|_| { - anyhow::anyhow!( - "cannot downcast LspResponse to {} for message {}", - T::Response::NAME, - T::NAME, - ) - })?; - - Ok(ProtoLspResponse { - server_id: self.server_id, - response: envelope.payload, - }) - } -} - -pub trait AnyTypedEnvelope: Any + Send + Sync { - fn payload_type_id(&self) -> TypeId; - fn payload_type_name(&self) -> &'static str; - fn into_any(self: Box) -> Box; - fn is_background(&self) -> bool; - fn original_sender_id(&self) -> Option; - fn sender_id(&self) -> PeerId; - fn message_id(&self) -> u32; -} - -pub enum MessagePriority { - Foreground, - Background, -} - -impl AnyTypedEnvelope for TypedEnvelope { - fn payload_type_id(&self) -> TypeId { - TypeId::of::() - } - - fn payload_type_name(&self) -> &'static str { - T::NAME - } - - fn into_any(self: Box) -> Box { - self - } - - fn is_background(&self) -> bool { - matches!(T::PRIORITY, MessagePriority::Background) - } - - fn original_sender_id(&self) -> Option { - self.original_sender_id - } - - fn sender_id(&self) -> PeerId { - self.sender_id - } - - fn message_id(&self) -> u32 { - self.message_id - } -} - -impl PeerId { - pub fn from_u64(peer_id: u64) -> Self { - let owner_id = (peer_id >> 32) as u32; - let id = peer_id as u32; - Self { owner_id, id } - } - - pub fn as_u64(self) -> u64 { - ((self.owner_id as u64) << 32) | (self.id as u64) - } -} - -impl Copy for PeerId {} - -impl Eq for PeerId {} - -impl Ord for PeerId { - fn cmp(&self, other: &Self) -> cmp::Ordering { - self.owner_id - .cmp(&other.owner_id) - .then_with(|| self.id.cmp(&other.id)) - } -} - -impl PartialOrd for PeerId { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl std::hash::Hash for PeerId { - fn hash(&self, state: &mut H) { - self.owner_id.hash(state); - self.id.hash(state); - } -} - -impl fmt::Display for PeerId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}/{}", self.owner_id, self.id) - } -} - -pub struct Receipt { - pub sender_id: PeerId, - pub message_id: u32, - payload_type: PhantomData, -} - -impl Clone for Receipt { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for Receipt {} - -#[derive(Clone, Debug)] -pub struct TypedEnvelope { - pub sender_id: PeerId, - pub original_sender_id: Option, - pub message_id: u32, - pub payload: T, - pub received_at: Instant, -} - -impl TypedEnvelope { - pub fn original_sender_id(&self) -> Result { - self.original_sender_id - .context("missing original_sender_id") - } -} - -impl TypedEnvelope { - pub fn receipt(&self) -> Receipt { - Receipt { - sender_id: self.sender_id, - message_id: self.message_id, - payload_type: PhantomData, - } - } -} diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml deleted file mode 100644 index feaf511b81..0000000000 --- a/crates/recent_projects/Cargo.toml +++ /dev/null @@ -1,62 +0,0 @@ -[package] -name = "recent_projects" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/recent_projects.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -askpass.workspace = true -auto_update.workspace = true -db.workspace = true -editor.workspace = true -extension_host.workspace = true -file_finder.workspace = true -futures.workspace = true -fuzzy.workspace = true -gpui.workspace = true -language.workspace = true -log.workspace = true -markdown.workspace = true -menu.workspace = true -node_runtime.workspace = true -ordered-float.workspace = true -paths.workspace = true -picker.workspace = true -project.workspace = true -release_channel.workspace = true -remote.workspace = true -semver.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smol.workspace = true -task.workspace = true -telemetry.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -worktree.workspace = true -zed_actions.workspace = true -indoc.workspace = true - -[target.'cfg(target_os = "windows")'.dependencies] -windows-registry = "0.6.0" - -[dev-dependencies] -dap.workspace = true -editor = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } -serde_json.workspace = true -settings = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/recent_projects/LICENSE-GPL b/crates/recent_projects/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/recent_projects/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/recent_projects/src/dev_container.rs b/crates/recent_projects/src/dev_container.rs deleted file mode 100644 index 0e6b8b381d..0000000000 --- a/crates/recent_projects/src/dev_container.rs +++ /dev/null @@ -1,295 +0,0 @@ -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use gpui::AsyncWindowContext; -use node_runtime::NodeRuntime; -use serde::Deserialize; -use settings::DevContainerConnection; -use smol::fs; -use workspace::Workspace; - -use crate::remote_connections::Connection; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct DevContainerUp { - _outcome: String, - container_id: String, - _remote_user: String, - remote_workspace_folder: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct DevContainerConfiguration { - name: Option, -} - -#[derive(Debug, Deserialize)] -struct DevContainerConfigurationOutput { - configuration: DevContainerConfiguration, -} - -#[cfg(not(target_os = "windows"))] -fn dev_container_cli() -> String { - "devcontainer".to_string() -} - -#[cfg(target_os = "windows")] -fn dev_container_cli() -> String { - "devcontainer.cmd".to_string() -} - -async fn check_for_docker() -> Result<(), DevContainerError> { - let mut command = util::command::new_smol_command("docker"); - command.arg("--version"); - - match command.output().await { - Ok(_) => Ok(()), - Err(e) => { - log::error!("Unable to find docker in $PATH: {:?}", e); - Err(DevContainerError::DockerNotAvailable) - } - } -} - -async fn ensure_devcontainer_cli(node_runtime: NodeRuntime) -> Result { - let mut command = util::command::new_smol_command(&dev_container_cli()); - command.arg("--version"); - - if let Err(e) = command.output().await { - log::error!( - "Unable to find devcontainer CLI in $PATH. Checking for a zed installed version. Error: {:?}", - e - ); - - let datadir_cli_path = paths::devcontainer_dir() - .join("node_modules") - .join(".bin") - .join(&dev_container_cli()); - - let mut command = - util::command::new_smol_command(&datadir_cli_path.as_os_str().display().to_string()); - command.arg("--version"); - - if let Err(e) = command.output().await { - log::error!( - "Unable to find devcontainer CLI in Data dir. Will try to install. Error: {:?}", - e - ); - } else { - log::info!("Found devcontainer CLI in Data dir"); - return Ok(datadir_cli_path.clone()); - } - - if let Err(e) = fs::create_dir_all(paths::devcontainer_dir()).await { - log::error!("Unable to create devcontainer directory. Error: {:?}", e); - return Err(DevContainerError::DevContainerCliNotAvailable); - } - - if let Err(e) = node_runtime - .npm_install_packages( - &paths::devcontainer_dir(), - &[("@devcontainers/cli", "latest")], - ) - .await - { - log::error!( - "Unable to install devcontainer CLI to data directory. Error: {:?}", - e - ); - return Err(DevContainerError::DevContainerCliNotAvailable); - }; - - let mut command = util::command::new_smol_command(&datadir_cli_path.display().to_string()); - command.arg("--version"); - if let Err(e) = command.output().await { - log::error!( - "Unable to find devcontainer cli after NPM install. Error: {:?}", - e - ); - Err(DevContainerError::DevContainerCliNotAvailable) - } else { - Ok(datadir_cli_path) - } - } else { - log::info!("Found devcontainer cli on $PATH, using it"); - Ok(PathBuf::from(&dev_container_cli())) - } -} - -async fn devcontainer_up( - path_to_cli: &PathBuf, - path: Arc, -) -> Result { - let mut command = util::command::new_smol_command(path_to_cli.display().to_string()); - command.arg("up"); - command.arg("--workspace-folder"); - command.arg(path.display().to_string()); - - match command.output().await { - Ok(output) => { - if output.status.success() { - let raw = String::from_utf8_lossy(&output.stdout); - serde_json::from_str::(&raw).map_err(|e| { - log::error!( - "Unable to parse response from 'devcontainer up' command, error: {:?}", - e - ); - DevContainerError::DevContainerParseFailed - }) - } else { - log::error!( - "Non-success status running devcontainer up for workspace: out: {:?}, err: {:?}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - Err(DevContainerError::DevContainerUpFailed) - } - } - Err(e) => { - log::error!("Error running devcontainer up: {:?}", e); - Err(DevContainerError::DevContainerUpFailed) - } - } -} - -async fn devcontainer_read_configuration( - path_to_cli: &PathBuf, - path: Arc, -) -> Result { - let mut command = util::command::new_smol_command(path_to_cli.display().to_string()); - command.arg("read-configuration"); - command.arg("--workspace-folder"); - command.arg(path.display().to_string()); - match command.output().await { - Ok(output) => { - if output.status.success() { - let raw = String::from_utf8_lossy(&output.stdout); - serde_json::from_str::(&raw).map_err(|e| { - log::error!( - "Unable to parse response from 'devcontainer read-configuration' command, error: {:?}", - e - ); - DevContainerError::DevContainerParseFailed - }) - } else { - log::error!( - "Non-success status running devcontainer read-configuration for workspace: out: {:?}, err: {:?}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - Err(DevContainerError::DevContainerUpFailed) - } - } - Err(e) => { - log::error!("Error running devcontainer read-configuration: {:?}", e); - Err(DevContainerError::DevContainerUpFailed) - } - } -} - -// Name the project with two fallbacks -async fn get_project_name( - path_to_cli: &PathBuf, - path: Arc, - remote_workspace_folder: String, - container_id: String, -) -> Result { - if let Ok(dev_container_configuration) = - devcontainer_read_configuration(path_to_cli, path).await - && let Some(name) = dev_container_configuration.configuration.name - { - // Ideally, name the project after the name defined in devcontainer.json - Ok(name) - } else { - // Otherwise, name the project after the remote workspace folder name - Ok(Path::new(&remote_workspace_folder) - .file_name() - .and_then(|name| name.to_str()) - .map(|string| string.into()) - // Finally, name the project after the container ID as a last resort - .unwrap_or_else(|| container_id.clone())) - } -} - -fn project_directory(cx: &mut AsyncWindowContext) -> Option> { - let Some(workspace) = cx.window_handle().downcast::() else { - return None; - }; - - match workspace.update(cx, |workspace, _, cx| { - workspace.project().read(cx).active_project_directory(cx) - }) { - Ok(dir) => dir, - Err(e) => { - log::error!("Error getting project directory from workspace: {:?}", e); - None - } - } -} - -pub(crate) async fn start_dev_container( - cx: &mut AsyncWindowContext, - node_runtime: NodeRuntime, -) -> Result<(Connection, String), DevContainerError> { - check_for_docker().await?; - - let path_to_devcontainer_cli = ensure_devcontainer_cli(node_runtime).await?; - - let Some(directory) = project_directory(cx) else { - return Err(DevContainerError::DevContainerNotFound); - }; - - if let Ok(DevContainerUp { - container_id, - remote_workspace_folder, - .. - }) = devcontainer_up(&path_to_devcontainer_cli, directory.clone()).await - { - let project_name = get_project_name( - &path_to_devcontainer_cli, - directory, - remote_workspace_folder.clone(), - container_id.clone(), - ) - .await?; - - let connection = Connection::DevContainer(DevContainerConnection { - name: project_name.into(), - container_id: container_id.into(), - }); - - Ok((connection, remote_workspace_folder)) - } else { - Err(DevContainerError::DevContainerUpFailed) - } -} - -#[derive(Debug)] -pub(crate) enum DevContainerError { - DockerNotAvailable, - DevContainerCliNotAvailable, - DevContainerUpFailed, - DevContainerNotFound, - DevContainerParseFailed, -} - -#[cfg(test)] -mod test { - - use crate::dev_container::DevContainerUp; - - #[test] - fn should_parse_from_devcontainer_json() { - let json = r#"{"outcome":"success","containerId":"826abcac45afd412abff083ab30793daff2f3c8ce2c831df728baf39933cb37a","remoteUser":"vscode","remoteWorkspaceFolder":"/workspaces/zed"}"#; - let up: DevContainerUp = serde_json::from_str(json).unwrap(); - assert_eq!(up._outcome, "success"); - assert_eq!( - up.container_id, - "826abcac45afd412abff083ab30793daff2f3c8ce2c831df728baf39933cb37a" - ); - assert_eq!(up._remote_user, "vscode"); - assert_eq!(up.remote_workspace_folder, "/workspaces/zed"); - } -} diff --git a/crates/recent_projects/src/dev_container_suggest.rs b/crates/recent_projects/src/dev_container_suggest.rs deleted file mode 100644 index 1e50080ea1..0000000000 --- a/crates/recent_projects/src/dev_container_suggest.rs +++ /dev/null @@ -1,106 +0,0 @@ -use db::kvp::KEY_VALUE_STORE; -use gpui::{SharedString, Window}; -use project::{Project, WorktreeId}; -use std::sync::LazyLock; -use ui::prelude::*; -use util::rel_path::RelPath; -use workspace::Workspace; -use workspace::notifications::NotificationId; -use workspace::notifications::simple_message_notification::MessageNotification; -use worktree::UpdatedEntriesSet; - -const DEV_CONTAINER_SUGGEST_KEY: &str = "dev_container_suggest_dismissed"; - -fn devcontainer_path() -> &'static RelPath { - static PATH: LazyLock<&'static RelPath> = - LazyLock::new(|| RelPath::unix(".devcontainer").expect("valid path")); - *PATH -} - -fn project_devcontainer_key(project_path: &str) -> String { - format!("{}_{}", DEV_CONTAINER_SUGGEST_KEY, project_path) -} - -pub fn suggest_on_worktree_updated( - worktree_id: WorktreeId, - updated_entries: &UpdatedEntriesSet, - project: &gpui::Entity, - window: &mut Window, - cx: &mut Context, -) { - let devcontainer_updated = updated_entries - .iter() - .any(|(path, _, _)| path.as_ref() == devcontainer_path()); - - if !devcontainer_updated { - return; - } - - let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) else { - return; - }; - - let worktree = worktree.read(cx); - - if !worktree.is_local() { - return; - } - - let has_devcontainer = worktree - .entry_for_path(devcontainer_path()) - .is_some_and(|entry| entry.is_dir()); - - if !has_devcontainer { - return; - } - - let abs_path = worktree.abs_path(); - let project_path = abs_path.to_string_lossy().to_string(); - let key_for_dismiss = project_devcontainer_key(&project_path); - - let already_dismissed = KEY_VALUE_STORE - .read_kvp(&key_for_dismiss) - .ok() - .flatten() - .is_some(); - - if already_dismissed { - return; - } - - cx.on_next_frame(window, move |workspace, _window, cx| { - struct DevContainerSuggestionNotification; - - let notification_id = NotificationId::composite::( - SharedString::from(project_path.clone()), - ); - - workspace.show_notification(notification_id, cx, |cx| { - cx.new(move |cx| { - MessageNotification::new( - "This project contains a Dev Container configuration file. Would you like to re-open it in a container?", - cx, - ) - .primary_message("Yes, Open in Container") - .primary_icon(IconName::Check) - .primary_icon_color(Color::Success) - .primary_on_click({ - move |window, cx| { - window.dispatch_action(Box::new(zed_actions::OpenDevContainer), cx); - } - }) - .secondary_message("Don't Show Again") - .secondary_icon(IconName::Close) - .secondary_icon_color(Color::Error) - .secondary_on_click({ - move |_window, cx| { - let key = key_for_dismiss.clone(); - db::write_and_log(cx, move || { - KEY_VALUE_STORE.write_kvp(key, "dismissed".to_string()) - }); - } - }) - }) - }); - }); -} diff --git a/crates/recent_projects/src/disconnected_overlay.rs b/crates/recent_projects/src/disconnected_overlay.rs deleted file mode 100644 index c97f7062a8..0000000000 --- a/crates/recent_projects/src/disconnected_overlay.rs +++ /dev/null @@ -1,205 +0,0 @@ -use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, Focusable, Render, WeakEntity}; -use project::project_settings::ProjectSettings; -use remote::RemoteConnectionOptions; -use settings::Settings; -use ui::{ - Button, ButtonCommon, ButtonStyle, Clickable, Context, ElevationIndex, FluentBuilder, Headline, - HeadlineSize, IconName, IconPosition, InteractiveElement, IntoElement, Label, Modal, - ModalFooter, ModalHeader, ParentElement, Section, Styled, StyledExt, Window, div, h_flex, rems, -}; -use workspace::{ModalView, OpenOptions, Workspace, notifications::DetachAndPromptErr}; - -use crate::open_remote_project; - -enum Host { - CollabGuestProject, - RemoteServerProject(RemoteConnectionOptions), -} - -pub struct DisconnectedOverlay { - workspace: WeakEntity, - host: Host, - focus_handle: FocusHandle, - finished: bool, -} - -impl EventEmitter for DisconnectedOverlay {} -impl Focusable for DisconnectedOverlay { - fn focus_handle(&self, _cx: &gpui::App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} -impl ModalView for DisconnectedOverlay { - fn on_before_dismiss( - &mut self, - _window: &mut Window, - _: &mut Context, - ) -> workspace::DismissDecision { - workspace::DismissDecision::Dismiss(self.finished) - } - fn fade_out_background(&self) -> bool { - true - } -} - -impl DisconnectedOverlay { - pub fn register( - workspace: &mut Workspace, - window: Option<&mut Window>, - cx: &mut Context, - ) { - let Some(window) = window else { - return; - }; - cx.subscribe_in( - workspace.project(), - window, - |workspace, project, event, window, cx| { - if !matches!( - event, - project::Event::DisconnectedFromHost - | project::Event::DisconnectedFromSshRemote - ) { - return; - } - let handle = cx.entity().downgrade(); - - let remote_connection_options = project.read(cx).remote_connection_options(cx); - let host = if let Some(ssh_connection_options) = remote_connection_options { - Host::RemoteServerProject(ssh_connection_options) - } else { - Host::CollabGuestProject - }; - - workspace.toggle_modal(window, cx, |_, cx| DisconnectedOverlay { - finished: false, - workspace: handle, - host, - focus_handle: cx.focus_handle(), - }); - }, - ) - .detach(); - } - - fn handle_reconnect(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { - self.finished = true; - cx.emit(DismissEvent); - - if let Host::RemoteServerProject(ssh_connection_options) = &self.host { - self.reconnect_to_remote_project(ssh_connection_options.clone(), window, cx); - } - } - - fn reconnect_to_remote_project( - &self, - connection_options: RemoteConnectionOptions, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = self.workspace.upgrade() else { - return; - }; - - let Some(window_handle) = window.window_handle().downcast::() else { - return; - }; - - let app_state = workspace.read(cx).app_state().clone(); - let paths = workspace - .read(cx) - .root_paths(cx) - .iter() - .map(|path| path.to_path_buf()) - .collect(); - - cx.spawn_in(window, async move |_, cx| { - open_remote_project( - connection_options, - paths, - app_state, - OpenOptions { - replace_window: Some(window_handle), - ..Default::default() - }, - cx, - ) - .await?; - Ok(()) - }) - .detach_and_prompt_err("Failed to reconnect", window, cx, |_, _, _| None); - } - - fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { - self.finished = true; - cx.emit(DismissEvent) - } -} - -impl Render for DisconnectedOverlay { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let can_reconnect = matches!(self.host, Host::RemoteServerProject(_)); - - let message = match &self.host { - Host::CollabGuestProject => { - "Your connection to the remote project has been lost.".to_string() - } - Host::RemoteServerProject(options) => { - let autosave = if ProjectSettings::get_global(cx) - .session - .restore_unsaved_buffers - { - "\nUnsaved changes are stored locally." - } else { - "" - }; - format!( - "Your connection to {} has been lost.{}", - options.display_name(), - autosave - ) - } - }; - - div() - .track_focus(&self.focus_handle(cx)) - .elevation_3(cx) - .on_action(cx.listener(Self::cancel)) - .occlude() - .w(rems(24.)) - .max_h(rems(40.)) - .child( - Modal::new("disconnected", None) - .header( - ModalHeader::new() - .show_dismiss_button(true) - .child(Headline::new("Disconnected").size(HeadlineSize::Small)), - ) - .section(Section::new().child(Label::new(message))) - .footer( - ModalFooter::new().end_slot( - h_flex() - .gap_2() - .child( - Button::new("close-window", "Close Window") - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ModalSurface) - .on_click(cx.listener(move |_, _, window, _| { - window.remove_window(); - })), - ) - .when(can_reconnect, |el| { - el.child( - Button::new("reconnect", "Reconnect") - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ModalSurface) - .icon(IconName::ArrowCircle) - .icon_position(IconPosition::Start) - .on_click(cx.listener(Self::handle_reconnect)), - ) - }), - ), - ), - ) - } -} diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs deleted file mode 100644 index 435933a880..0000000000 --- a/crates/recent_projects/src/recent_projects.rs +++ /dev/null @@ -1,1043 +0,0 @@ -mod dev_container; -mod dev_container_suggest; -pub mod disconnected_overlay; -mod remote_connections; -mod remote_servers; -mod ssh_config; - -use std::path::PathBuf; - -#[cfg(target_os = "windows")] -mod wsl_picker; - -use remote::RemoteConnectionOptions; -pub use remote_connections::{RemoteConnectionModal, connect, open_remote_project}; - -use disconnected_overlay::DisconnectedOverlay; -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{ - Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, - Subscription, Task, WeakEntity, Window, -}; -use ordered_float::OrderedFloat; -use picker::{ - Picker, PickerDelegate, - highlighted_match_with_paths::{HighlightedMatch, HighlightedMatchWithPaths}, -}; -pub use remote_connections::SshSettings; -pub use remote_servers::RemoteServerProjects; -use settings::Settings; -use std::{path::Path, sync::Arc}; -use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, tooltip_container}; -use util::{ResultExt, paths::PathExt}; -use workspace::{ - CloseIntent, HistoryManager, ModalView, OpenOptions, PathList, SerializedWorkspaceLocation, - WORKSPACE_DB, Workspace, WorkspaceId, notifications::DetachAndPromptErr, - with_active_or_new_workspace, -}; -use zed_actions::{OpenDevContainer, OpenRecent, OpenRemote}; - -pub fn init(cx: &mut App) { - #[cfg(target_os = "windows")] - cx.on_action(|open_wsl: &zed_actions::wsl_actions::OpenFolderInWsl, cx| { - let create_new_window = open_wsl.create_new_window; - with_active_or_new_workspace(cx, move |workspace, window, cx| { - use gpui::PathPromptOptions; - use project::DirectoryLister; - - let paths = workspace.prompt_for_open_path( - PathPromptOptions { - files: true, - directories: true, - multiple: false, - prompt: None, - }, - DirectoryLister::Local( - workspace.project().clone(), - workspace.app_state().fs.clone(), - ), - window, - cx, - ); - - cx.spawn_in(window, async move |workspace, cx| { - use util::paths::SanitizedPath; - - let Some(paths) = paths.await.log_err().flatten() else { - return; - }; - - let paths = paths - .into_iter() - .filter_map(|path| SanitizedPath::new(&path).local_to_wsl()) - .collect::>(); - - if paths.is_empty() { - let message = indoc::indoc! { r#" - Invalid path specified when trying to open a folder inside WSL. - - Please note that Zed currently does not support opening network share folders inside wsl. - "#}; - - let _ = cx.prompt(gpui::PromptLevel::Critical, "Invalid path", Some(&message), &["Ok"]).await; - return; - } - - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_modal(window, cx, |window, cx| { - crate::wsl_picker::WslOpenModal::new(paths, create_new_window, window, cx) - }); - }).log_err(); - }) - .detach(); - }); - }); - - #[cfg(target_os = "windows")] - cx.on_action(|open_wsl: &zed_actions::wsl_actions::OpenWsl, cx| { - let create_new_window = open_wsl.create_new_window; - with_active_or_new_workspace(cx, move |workspace, window, cx| { - let handle = cx.entity().downgrade(); - let fs = workspace.project().read(cx).fs().clone(); - workspace.toggle_modal(window, cx, |window, cx| { - RemoteServerProjects::wsl(create_new_window, fs, window, handle, cx) - }); - }); - }); - - #[cfg(target_os = "windows")] - cx.on_action(|open_wsl: &remote::OpenWslPath, cx| { - let open_wsl = open_wsl.clone(); - with_active_or_new_workspace(cx, move |workspace, window, cx| { - let fs = workspace.project().read(cx).fs().clone(); - add_wsl_distro(fs, &open_wsl.distro, cx); - let open_options = OpenOptions { - replace_window: window.window_handle().downcast::(), - ..Default::default() - }; - - let app_state = workspace.app_state().clone(); - - cx.spawn_in(window, async move |_, cx| { - open_remote_project( - RemoteConnectionOptions::Wsl(open_wsl.distro.clone()), - open_wsl.paths, - app_state, - open_options, - cx, - ) - .await - }) - .detach(); - }); - }); - - cx.on_action(|open_recent: &OpenRecent, cx| { - let create_new_window = open_recent.create_new_window; - with_active_or_new_workspace(cx, move |workspace, window, cx| { - let Some(recent_projects) = workspace.active_modal::(cx) else { - let focus_handle = workspace.focus_handle(cx); - RecentProjects::open(workspace, create_new_window, window, focus_handle, cx); - return; - }; - - recent_projects.update(cx, |recent_projects, cx| { - recent_projects - .picker - .update(cx, |picker, cx| picker.cycle_selection(window, cx)) - }); - }); - }); - cx.on_action(|open_remote: &OpenRemote, cx| { - let from_existing_connection = open_remote.from_existing_connection; - let create_new_window = open_remote.create_new_window; - with_active_or_new_workspace(cx, move |workspace, window, cx| { - if from_existing_connection { - cx.propagate(); - return; - } - let handle = cx.entity().downgrade(); - let fs = workspace.project().read(cx).fs().clone(); - workspace.toggle_modal(window, cx, |window, cx| { - RemoteServerProjects::new(create_new_window, fs, window, handle, cx) - }) - }); - }); - - cx.observe_new(DisconnectedOverlay::register).detach(); - - cx.on_action(|_: &OpenDevContainer, cx| { - with_active_or_new_workspace(cx, move |workspace, window, cx| { - let app_state = workspace.app_state().clone(); - let replace_window = window.window_handle().downcast::(); - - cx.spawn_in(window, async move |_, mut cx| { - let (connection, starting_dir) = match dev_container::start_dev_container( - &mut cx, - app_state.node_runtime.clone(), - ) - .await - { - Ok((c, s)) => (c, s), - Err(e) => { - log::error!("Failed to start Dev Container: {:?}", e); - cx.prompt( - gpui::PromptLevel::Critical, - "Failed to start Dev Container", - Some(&format!("{:?}", e)), - &["Ok"], - ) - .await - .ok(); - return; - } - }; - - let result = open_remote_project( - connection.into(), - vec![starting_dir].into_iter().map(PathBuf::from).collect(), - app_state, - OpenOptions { - replace_window, - ..OpenOptions::default() - }, - &mut cx, - ) - .await; - - if let Err(e) = result { - log::error!("Failed to connect: {e:#}"); - cx.prompt( - gpui::PromptLevel::Critical, - "Failed to connect", - Some(&e.to_string()), - &["Ok"], - ) - .await - .ok(); - } - }) - .detach(); - - let fs = workspace.project().read(cx).fs().clone(); - let handle = cx.entity().downgrade(); - workspace.toggle_modal(window, cx, |window, cx| { - RemoteServerProjects::new_dev_container(fs, window, handle, cx) - }); - }); - }); - - // Subscribe to worktree additions to suggest opening the project in a dev container - cx.observe_new( - |workspace: &mut Workspace, window: Option<&mut Window>, cx: &mut Context| { - let Some(window) = window else { - return; - }; - cx.subscribe_in( - workspace.project(), - window, - move |_, project, event, window, cx| { - if let project::Event::WorktreeUpdatedEntries(worktree_id, updated_entries) = - event - { - dev_container_suggest::suggest_on_worktree_updated( - *worktree_id, - updated_entries, - project, - window, - cx, - ); - } - }, - ) - .detach(); - }, - ) - .detach(); -} - -#[cfg(target_os = "windows")] -pub fn add_wsl_distro( - fs: Arc, - connection_options: &remote::WslConnectionOptions, - cx: &App, -) { - use gpui::ReadGlobal; - use settings::SettingsStore; - - let distro_name = SharedString::from(&connection_options.distro_name); - let user = connection_options.user.clone(); - SettingsStore::global(cx).update_settings_file(fs, move |setting, _| { - let connections = setting - .remote - .wsl_connections - .get_or_insert(Default::default()); - - if !connections - .iter() - .any(|conn| conn.distro_name == distro_name && conn.user == user) - { - use std::collections::BTreeSet; - - connections.push(settings::WslConnection { - distro_name, - user, - projects: BTreeSet::new(), - }) - } - }); -} - -pub struct RecentProjects { - pub picker: Entity>, - rem_width: f32, - _subscription: Subscription, -} - -impl ModalView for RecentProjects {} - -impl RecentProjects { - fn new( - delegate: RecentProjectsDelegate, - rem_width: f32, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let picker = cx.new(|cx| { - // We want to use a list when we render paths, because the items can have different heights (multiple paths). - if delegate.render_paths { - Picker::list(delegate, window, cx) - } else { - Picker::uniform_list(delegate, window, cx) - } - }); - let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent)); - // We do not want to block the UI on a potentially lengthy call to DB, so we're gonna swap - // out workspace locations once the future runs to completion. - cx.spawn_in(window, async move |this, cx| { - let workspaces = WORKSPACE_DB - .recent_workspaces_on_disk() - .await - .log_err() - .unwrap_or_default(); - this.update_in(cx, move |this, window, cx| { - this.picker.update(cx, move |picker, cx| { - picker.delegate.set_workspaces(workspaces); - picker.update_matches(picker.query(cx), window, cx) - }) - }) - .ok() - }) - .detach(); - Self { - picker, - rem_width, - _subscription, - } - } - - pub fn open( - workspace: &mut Workspace, - create_new_window: bool, - window: &mut Window, - focus_handle: FocusHandle, - cx: &mut Context, - ) { - let weak = cx.entity().downgrade(); - workspace.toggle_modal(window, cx, |window, cx| { - let delegate = RecentProjectsDelegate::new(weak, create_new_window, true, focus_handle); - - Self::new(delegate, 34., window, cx) - }) - } -} - -impl EventEmitter for RecentProjects {} - -impl Focusable for RecentProjects { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl Render for RecentProjects { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .key_context("RecentProjects") - .w(rems(self.rem_width)) - .child(self.picker.clone()) - .on_mouse_down_out(cx.listener(|this, _, window, cx| { - this.picker.update(cx, |this, cx| { - this.cancel(&Default::default(), window, cx); - }) - })) - } -} - -pub struct RecentProjectsDelegate { - workspace: WeakEntity, - workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>, - selected_match_index: usize, - matches: Vec, - render_paths: bool, - create_new_window: bool, - // Flag to reset index when there is a new query vs not reset index when user delete an item - reset_selected_match_index: bool, - has_any_non_local_projects: bool, - focus_handle: FocusHandle, -} - -impl RecentProjectsDelegate { - fn new( - workspace: WeakEntity, - create_new_window: bool, - render_paths: bool, - focus_handle: FocusHandle, - ) -> Self { - Self { - workspace, - workspaces: Vec::new(), - selected_match_index: 0, - matches: Default::default(), - create_new_window, - render_paths, - reset_selected_match_index: true, - has_any_non_local_projects: false, - focus_handle, - } - } - - pub fn set_workspaces( - &mut self, - workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>, - ) { - self.workspaces = workspaces; - self.has_any_non_local_projects = !self - .workspaces - .iter() - .all(|(_, location, _)| matches!(location, SerializedWorkspaceLocation::Local)); - } -} -impl EventEmitter for RecentProjectsDelegate {} -impl PickerDelegate for RecentProjectsDelegate { - type ListItem = ListItem; - - fn placeholder_text(&self, window: &mut Window, _: &mut App) -> Arc { - let (create_window, reuse_window) = if self.create_new_window { - ( - window.keystroke_text_for(&menu::Confirm), - window.keystroke_text_for(&menu::SecondaryConfirm), - ) - } else { - ( - window.keystroke_text_for(&menu::SecondaryConfirm), - window.keystroke_text_for(&menu::Confirm), - ) - }; - Arc::from(format!( - "{reuse_window} reuses this window, {create_window} opens a new one", - )) - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_match_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _window: &mut Window, - _cx: &mut Context>, - ) { - self.selected_match_index = ix; - } - - fn update_matches( - &mut self, - query: String, - _: &mut Window, - cx: &mut Context>, - ) -> gpui::Task<()> { - let query = query.trim_start(); - let smart_case = query.chars().any(|c| c.is_uppercase()); - let candidates = self - .workspaces - .iter() - .enumerate() - .filter(|(_, (id, _, _))| !self.is_current_workspace(*id, cx)) - .map(|(id, (_, _, paths))| { - let combined_string = paths - .ordered_paths() - .map(|path| path.compact().to_string_lossy().into_owned()) - .collect::>() - .join(""); - StringMatchCandidate::new(id, &combined_string) - }) - .collect::>(); - self.matches = smol::block_on(fuzzy::match_strings( - candidates.as_slice(), - query, - smart_case, - true, - 100, - &Default::default(), - cx.background_executor().clone(), - )); - self.matches.sort_unstable_by(|a, b| { - b.score - .partial_cmp(&a.score) // Descending score - .unwrap_or(std::cmp::Ordering::Equal) - .then_with(|| a.candidate_id.cmp(&b.candidate_id)) // Ascending candidate_id for ties - }); - - if self.reset_selected_match_index { - self.selected_match_index = self - .matches - .iter() - .enumerate() - .rev() - .max_by_key(|(_, m)| OrderedFloat(m.score)) - .map(|(ix, _)| ix) - .unwrap_or(0); - } - self.reset_selected_match_index = true; - Task::ready(()) - } - - fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) { - if let Some((selected_match, workspace)) = self - .matches - .get(self.selected_index()) - .zip(self.workspace.upgrade()) - { - let (candidate_workspace_id, candidate_workspace_location, candidate_workspace_paths) = - &self.workspaces[selected_match.candidate_id]; - let replace_current_window = if self.create_new_window { - secondary - } else { - !secondary - }; - workspace.update(cx, |workspace, cx| { - if workspace.database_id() == Some(*candidate_workspace_id) { - return; - } - match candidate_workspace_location.clone() { - SerializedWorkspaceLocation::Local => { - let paths = candidate_workspace_paths.paths().to_vec(); - if replace_current_window { - cx.spawn_in(window, async move |workspace, cx| { - let continue_replacing = workspace - .update_in(cx, |workspace, window, cx| { - workspace.prepare_to_close( - CloseIntent::ReplaceWindow, - window, - cx, - ) - })? - .await?; - if continue_replacing { - workspace - .update_in(cx, |workspace, window, cx| { - workspace - .open_workspace_for_paths(true, paths, window, cx) - })? - .await - } else { - Ok(()) - } - }) - } else { - workspace.open_workspace_for_paths(false, paths, window, cx) - } - } - SerializedWorkspaceLocation::Remote(mut connection) => { - let app_state = workspace.app_state().clone(); - - let replace_window = if replace_current_window { - window.window_handle().downcast::() - } else { - None - }; - - let open_options = OpenOptions { - replace_window, - ..Default::default() - }; - - if let RemoteConnectionOptions::Ssh(connection) = &mut connection { - SshSettings::get_global(cx) - .fill_connection_options_from_settings(connection); - }; - - let paths = candidate_workspace_paths.paths().to_vec(); - - cx.spawn_in(window, async move |_, cx| { - open_remote_project( - connection.clone(), - paths, - app_state, - open_options, - cx, - ) - .await - }) - } - } - .detach_and_prompt_err( - "Failed to open project", - window, - cx, - |_, _, _| None, - ); - }); - cx.emit(DismissEvent); - } - } - - fn dismissed(&mut self, _window: &mut Window, _: &mut Context>) {} - - fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option { - let text = if self.workspaces.is_empty() { - "Recently opened projects will show up here".into() - } else { - "No matches".into() - }; - Some(text) - } - - fn render_match( - &self, - ix: usize, - selected: bool, - window: &mut Window, - cx: &mut Context>, - ) -> Option { - let hit = self.matches.get(ix)?; - - let (_, location, paths) = self.workspaces.get(hit.candidate_id)?; - - let mut path_start_offset = 0; - - let (match_labels, paths): (Vec<_>, Vec<_>) = paths - .ordered_paths() - .map(|p| p.compact()) - .map(|path| { - let highlighted_text = - highlights_for_path(path.as_ref(), &hit.positions, path_start_offset); - path_start_offset += highlighted_text.1.text.len(); - highlighted_text - }) - .unzip(); - - let prefix = match &location { - SerializedWorkspaceLocation::Remote(options) => { - Some(SharedString::from(options.display_name())) - } - _ => None, - }; - - let highlighted_match = HighlightedMatchWithPaths { - prefix, - match_label: HighlightedMatch::join(match_labels.into_iter().flatten(), ", "), - paths, - }; - - let focus_handle = self.focus_handle.clone(); - - let secondary_actions = h_flex() - .gap_px() - .child( - IconButton::new("open_new_window", IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) - .tooltip({ - move |_, cx| { - Tooltip::for_action_in( - "Open Project in New Window", - &menu::SecondaryConfirm, - &focus_handle, - cx, - ) - } - }) - .on_click(cx.listener(move |this, _event, window, cx| { - cx.stop_propagation(); - window.prevent_default(); - this.delegate.set_selected_index(ix, window, cx); - this.delegate.confirm(true, window, cx); - })), - ) - .child( - IconButton::new("delete", IconName::Close) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Delete from Recent Projects")) - .on_click(cx.listener(move |this, _event, window, cx| { - cx.stop_propagation(); - window.prevent_default(); - - this.delegate.delete_recent_project(ix, window, cx) - })), - ) - .into_any_element(); - - Some( - ListItem::new(ix) - .toggle_state(selected) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .child( - h_flex() - .id("projecy_info_container") - .gap_3() - .flex_grow() - .when(self.has_any_non_local_projects, |this| { - this.child(match location { - SerializedWorkspaceLocation::Local => Icon::new(IconName::Screen) - .color(Color::Muted) - .into_any_element(), - SerializedWorkspaceLocation::Remote(options) => { - Icon::new(match options { - RemoteConnectionOptions::Ssh { .. } => IconName::Server, - RemoteConnectionOptions::Wsl { .. } => IconName::Linux, - RemoteConnectionOptions::Docker(_) => IconName::Box, - }) - .color(Color::Muted) - .into_any_element() - } - }) - }) - .child({ - let mut highlighted = highlighted_match.clone(); - if !self.render_paths { - highlighted.paths.clear(); - } - highlighted.render(window, cx) - }) - .tooltip(move |_, cx| { - let tooltip_highlighted_location = highlighted_match.clone(); - cx.new(|_| MatchTooltip { - highlighted_location: tooltip_highlighted_location, - }) - .into() - }), - ) - .map(|el| { - if self.selected_index() == ix { - el.end_slot(secondary_actions) - } else { - el.end_hover_slot(secondary_actions) - } - }), - ) - } - - fn render_footer(&self, _: &mut Window, cx: &mut Context>) -> Option { - Some( - h_flex() - .w_full() - .p_2() - .gap_2() - .justify_end() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child( - Button::new("remote", "Open Remote Folder") - .key_binding(KeyBinding::for_action( - &OpenRemote { - from_existing_connection: false, - create_new_window: false, - }, - cx, - )) - .on_click(|_, window, cx| { - window.dispatch_action( - OpenRemote { - from_existing_connection: false, - create_new_window: false, - } - .boxed_clone(), - cx, - ) - }), - ) - .child( - Button::new("local", "Open Local Folder") - .key_binding(KeyBinding::for_action(&workspace::Open, cx)) - .on_click(|_, window, cx| { - window.dispatch_action(workspace::Open.boxed_clone(), cx) - }), - ) - .into_any(), - ) - } -} - -// Compute the highlighted text for the name and path -fn highlights_for_path( - path: &Path, - match_positions: &Vec, - path_start_offset: usize, -) -> (Option, HighlightedMatch) { - let path_string = path.to_string_lossy(); - let path_text = path_string.to_string(); - let path_byte_len = path_text.len(); - // Get the subset of match highlight positions that line up with the given path. - // Also adjusts them to start at the path start - let path_positions = match_positions - .iter() - .copied() - .skip_while(|position| *position < path_start_offset) - .take_while(|position| *position < path_start_offset + path_byte_len) - .map(|position| position - path_start_offset) - .collect::>(); - - // Again subset the highlight positions to just those that line up with the file_name - // again adjusted to the start of the file_name - let file_name_text_and_positions = path.file_name().map(|file_name| { - let file_name_text = file_name.to_string_lossy().into_owned(); - let file_name_start_byte = path_byte_len - file_name_text.len(); - let highlight_positions = path_positions - .iter() - .copied() - .skip_while(|position| *position < file_name_start_byte) - .take_while(|position| *position < file_name_start_byte + file_name_text.len()) - .map(|position| position - file_name_start_byte) - .collect::>(); - HighlightedMatch { - text: file_name_text, - highlight_positions, - color: Color::Default, - } - }); - - ( - file_name_text_and_positions, - HighlightedMatch { - text: path_text, - highlight_positions: path_positions, - color: Color::Default, - }, - ) -} -impl RecentProjectsDelegate { - fn delete_recent_project( - &self, - ix: usize, - window: &mut Window, - cx: &mut Context>, - ) { - if let Some(selected_match) = self.matches.get(ix) { - let (workspace_id, _, _) = self.workspaces[selected_match.candidate_id]; - cx.spawn_in(window, async move |this, cx| { - let _ = WORKSPACE_DB.delete_workspace_by_id(workspace_id).await; - let workspaces = WORKSPACE_DB - .recent_workspaces_on_disk() - .await - .unwrap_or_default(); - this.update_in(cx, move |picker, window, cx| { - picker.delegate.set_workspaces(workspaces); - picker - .delegate - .set_selected_index(ix.saturating_sub(1), window, cx); - picker.delegate.reset_selected_match_index = false; - picker.update_matches(picker.query(cx), window, cx); - // After deleting a project, we want to update the history manager to reflect the change. - // But we do not emit a update event when user opens a project, because it's handled in `workspace::load_workspace`. - if let Some(history_manager) = HistoryManager::global(cx) { - history_manager - .update(cx, |this, cx| this.delete_history(workspace_id, cx)); - } - }) - }) - .detach(); - } - } - - fn is_current_workspace( - &self, - workspace_id: WorkspaceId, - cx: &mut Context>, - ) -> bool { - if let Some(workspace) = self.workspace.upgrade() { - let workspace = workspace.read(cx); - if Some(workspace_id) == workspace.database_id() { - return true; - } - } - - false - } -} -struct MatchTooltip { - highlighted_location: HighlightedMatchWithPaths, -} - -impl Render for MatchTooltip { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - tooltip_container(cx, |div, _| { - self.highlighted_location.render_paths_children(div) - }) - } -} - -#[cfg(test)] -mod tests { - use std::path::PathBuf; - - use editor::Editor; - use gpui::{TestAppContext, UpdateGlobal, WindowHandle}; - - use serde_json::json; - use settings::SettingsStore; - use util::path; - use workspace::{AppState, open_paths}; - - use super::*; - - #[gpui::test] - async fn test_prompts_on_dirty_before_submit(cx: &mut TestAppContext) { - let app_state = init_test(cx); - - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings - .session - .get_or_insert_default() - .restore_unsaved_buffers = Some(false) - }); - }); - }); - - app_state - .fs - .as_fake() - .insert_tree( - path!("/dir"), - json!({ - "main.ts": "a" - }), - ) - .await; - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/dir/main.ts"))], - app_state, - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 1); - - let workspace = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); - workspace - .update(cx, |workspace, _, _| assert!(!workspace.is_edited())) - .unwrap(); - - let editor = workspace - .read_with(cx, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }) - .unwrap(); - workspace - .update(cx, |_, window, cx| { - editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx)); - }) - .unwrap(); - workspace - .update(cx, |workspace, _, _| assert!(workspace.is_edited(), "After inserting more text into the editor without saving, we should have a dirty project")) - .unwrap(); - - let recent_projects_picker = open_recent_projects(&workspace, cx); - workspace - .update(cx, |_, _, cx| { - recent_projects_picker.update(cx, |picker, cx| { - assert_eq!(picker.query(cx), ""); - let delegate = &mut picker.delegate; - delegate.matches = vec![StringMatch { - candidate_id: 0, - score: 1.0, - positions: Vec::new(), - string: "fake candidate".to_string(), - }]; - delegate.set_workspaces(vec![( - WorkspaceId::default(), - SerializedWorkspaceLocation::Local, - PathList::new(&[path!("/test/path")]), - )]); - }); - }) - .unwrap(); - - assert!( - !cx.has_pending_prompt(), - "Should have no pending prompt on dirty project before opening the new recent project" - ); - cx.dispatch_action(*workspace, menu::Confirm); - workspace - .update(cx, |workspace, _, cx| { - assert!( - workspace.active_modal::(cx).is_none(), - "Should remove the modal after selecting new recent project" - ) - }) - .unwrap(); - assert!( - cx.has_pending_prompt(), - "Dirty workspace should prompt before opening the new recent project" - ); - cx.simulate_prompt_answer("Cancel"); - assert!( - !cx.has_pending_prompt(), - "Should have no pending prompt after cancelling" - ); - workspace - .update(cx, |workspace, _, _| { - assert!( - workspace.is_edited(), - "Should be in the same dirty project after cancelling" - ) - }) - .unwrap(); - } - - fn open_recent_projects( - workspace: &WindowHandle, - cx: &mut TestAppContext, - ) -> Entity> { - cx.dispatch_action( - (*workspace).into(), - OpenRecent { - create_new_window: false, - }, - ); - workspace - .update(cx, |workspace, _, cx| { - workspace - .active_modal::(cx) - .unwrap() - .read(cx) - .picker - .clone() - }) - .unwrap() - } - - fn init_test(cx: &mut TestAppContext) -> Arc { - cx.update(|cx| { - let state = AppState::test(cx); - crate::init(cx); - editor::init(cx); - state - }) - } -} diff --git a/crates/recent_projects/src/remote_connections.rs b/crates/recent_projects/src/remote_connections.rs deleted file mode 100644 index c0a655d19e..0000000000 --- a/crates/recent_projects/src/remote_connections.rs +++ /dev/null @@ -1,880 +0,0 @@ -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; - -use anyhow::{Context as _, Result}; -use askpass::EncryptedPassword; -use auto_update::AutoUpdater; -use editor::Editor; -use extension_host::ExtensionStore; -use futures::channel::oneshot; -use gpui::{ - AnyWindowHandle, App, AsyncApp, DismissEvent, Entity, EventEmitter, Focusable, FontFeatures, - ParentElement as _, PromptLevel, Render, SharedString, Task, TextStyleRefinement, WeakEntity, -}; - -use language::{CursorShape, Point}; -use markdown::{Markdown, MarkdownElement, MarkdownStyle}; -use release_channel::ReleaseChannel; -use remote::{ - ConnectionIdentifier, DockerConnectionOptions, RemoteClient, RemoteConnection, - RemoteConnectionOptions, RemotePlatform, SshConnectionOptions, -}; -use semver::Version; -pub use settings::SshConnection; -use settings::{DevContainerConnection, ExtendingVec, RegisterSetting, Settings, WslConnection}; -use theme::ThemeSettings; -use ui::{ - ActiveTheme, Color, CommonAnimationExt, Context, InteractiveElement, IntoElement, KeyBinding, - LabelCommon, ListItem, Styled, Window, prelude::*, -}; -use util::paths::PathWithPosition; -use workspace::{AppState, ModalView, Workspace}; - -#[derive(RegisterSetting)] -pub struct SshSettings { - pub ssh_connections: ExtendingVec, - pub wsl_connections: ExtendingVec, - /// Whether to read ~/.ssh/config for ssh connection sources. - pub read_ssh_config: bool, -} - -impl SshSettings { - pub fn ssh_connections(&self) -> impl Iterator + use<> { - self.ssh_connections.clone().0.into_iter() - } - - pub fn wsl_connections(&self) -> impl Iterator + use<> { - self.wsl_connections.clone().0.into_iter() - } - - pub fn fill_connection_options_from_settings(&self, options: &mut SshConnectionOptions) { - for conn in self.ssh_connections() { - if conn.host == options.host - && conn.username == options.username - && conn.port == options.port - { - options.nickname = conn.nickname; - options.upload_binary_over_ssh = conn.upload_binary_over_ssh.unwrap_or_default(); - options.args = Some(conn.args); - options.port_forwards = conn.port_forwards; - break; - } - } - } - - pub fn connection_options_for( - &self, - host: String, - port: Option, - username: Option, - ) -> SshConnectionOptions { - let mut options = SshConnectionOptions { - host, - port, - username, - ..Default::default() - }; - self.fill_connection_options_from_settings(&mut options); - options - } -} - -#[derive(Clone, PartialEq)] -pub enum Connection { - Ssh(SshConnection), - Wsl(WslConnection), - DevContainer(DevContainerConnection), -} - -impl From for RemoteConnectionOptions { - fn from(val: Connection) -> Self { - match val { - Connection::Ssh(conn) => RemoteConnectionOptions::Ssh(conn.into()), - Connection::Wsl(conn) => RemoteConnectionOptions::Wsl(conn.into()), - Connection::DevContainer(conn) => { - RemoteConnectionOptions::Docker(DockerConnectionOptions { - name: conn.name.to_string(), - container_id: conn.container_id.to_string(), - upload_binary_over_docker_exec: false, - }) - } - } - } -} - -impl From for Connection { - fn from(val: SshConnection) -> Self { - Connection::Ssh(val) - } -} - -impl From for Connection { - fn from(val: WslConnection) -> Self { - Connection::Wsl(val) - } -} - -impl Settings for SshSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let remote = &content.remote; - Self { - ssh_connections: remote.ssh_connections.clone().unwrap_or_default().into(), - wsl_connections: remote.wsl_connections.clone().unwrap_or_default().into(), - read_ssh_config: remote.read_ssh_config.unwrap(), - } - } -} - -pub struct RemoteConnectionPrompt { - connection_string: SharedString, - nickname: Option, - is_wsl: bool, - is_devcontainer: bool, - status_message: Option, - prompt: Option<(Entity, oneshot::Sender)>, - cancellation: Option>, - editor: Entity, -} - -impl Drop for RemoteConnectionPrompt { - fn drop(&mut self) { - if let Some(cancel) = self.cancellation.take() { - cancel.send(()).ok(); - } - } -} - -pub struct RemoteConnectionModal { - pub prompt: Entity, - paths: Vec, - finished: bool, -} - -impl RemoteConnectionPrompt { - pub(crate) fn new( - connection_string: String, - nickname: Option, - is_wsl: bool, - is_devcontainer: bool, - window: &mut Window, - cx: &mut Context, - ) -> Self { - Self { - connection_string: connection_string.into(), - nickname: nickname.map(|nickname| nickname.into()), - is_wsl, - is_devcontainer, - editor: cx.new(|cx| Editor::single_line(window, cx)), - status_message: None, - cancellation: None, - prompt: None, - } - } - - pub fn set_cancellation_tx(&mut self, tx: oneshot::Sender<()>) { - self.cancellation = Some(tx); - } - - fn set_prompt( - &mut self, - prompt: String, - tx: oneshot::Sender, - window: &mut Window, - cx: &mut Context, - ) { - let theme = ThemeSettings::get_global(cx); - - let refinement = TextStyleRefinement { - font_family: Some(theme.buffer_font.family.clone()), - font_features: Some(FontFeatures::disable_ligatures()), - font_size: Some(theme.buffer_font_size(cx).into()), - color: Some(cx.theme().colors().editor_foreground), - background_color: Some(gpui::transparent_black()), - ..Default::default() - }; - - self.editor.update(cx, |editor, cx| { - if prompt.contains("yes/no") { - editor.set_masked(false, cx); - } else { - editor.set_masked(true, cx); - } - editor.set_text_style_refinement(refinement); - editor.set_cursor_shape(CursorShape::Block, cx); - }); - - let markdown = cx.new(|cx| Markdown::new_text(prompt.into(), cx)); - self.prompt = Some((markdown, tx)); - self.status_message.take(); - window.focus(&self.editor.focus_handle(cx)); - cx.notify(); - } - - pub fn set_status(&mut self, status: Option, cx: &mut Context) { - self.status_message = status.map(|s| s.into()); - cx.notify(); - } - - pub fn confirm(&mut self, window: &mut Window, cx: &mut Context) { - if let Some((_, tx)) = self.prompt.take() { - self.status_message = Some("Connecting".into()); - - self.editor.update(cx, |editor, cx| { - let pw = editor.text(cx); - if let Ok(secure) = EncryptedPassword::try_from(pw.as_ref()) { - tx.send(secure).ok(); - } - editor.clear(window, cx); - }); - } - } -} - -impl Render for RemoteConnectionPrompt { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let theme = ThemeSettings::get_global(cx); - - let mut text_style = window.text_style(); - let refinement = TextStyleRefinement { - font_family: Some(theme.buffer_font.family.clone()), - font_features: Some(FontFeatures::disable_ligatures()), - font_size: Some(theme.buffer_font_size(cx).into()), - color: Some(cx.theme().colors().editor_foreground), - background_color: Some(gpui::transparent_black()), - ..Default::default() - }; - - text_style.refine(&refinement); - let markdown_style = MarkdownStyle { - base_text_style: text_style, - selection_background_color: cx.theme().colors().element_selection_background, - ..Default::default() - }; - - v_flex() - .key_context("PasswordPrompt") - .p_2() - .size_full() - .text_buffer(cx) - .when_some(self.status_message.clone(), |el, status_message| { - el.child( - h_flex() - .gap_2() - .child( - Icon::new(IconName::ArrowCircle) - .color(Color::Muted) - .with_rotate_animation(2), - ) - .child( - div() - .text_ellipsis() - .overflow_x_hidden() - .child(format!("{}…", status_message)), - ), - ) - }) - .when_some(self.prompt.as_ref(), |el, prompt| { - el.child( - div() - .size_full() - .overflow_hidden() - .child(MarkdownElement::new(prompt.0.clone(), markdown_style)) - .child(self.editor.clone()), - ) - .when(window.capslock().on, |el| { - el.child(Label::new("⚠️ ⇪ is on")) - }) - }) - } -} - -impl RemoteConnectionModal { - pub fn new( - connection_options: &RemoteConnectionOptions, - paths: Vec, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let (connection_string, nickname, is_wsl, is_devcontainer) = match connection_options { - RemoteConnectionOptions::Ssh(options) => ( - options.connection_string(), - options.nickname.clone(), - false, - false, - ), - RemoteConnectionOptions::Wsl(options) => { - (options.distro_name.clone(), None, true, false) - } - RemoteConnectionOptions::Docker(options) => (options.name.clone(), None, false, true), - }; - Self { - prompt: cx.new(|cx| { - RemoteConnectionPrompt::new( - connection_string, - nickname, - is_wsl, - is_devcontainer, - window, - cx, - ) - }), - finished: false, - paths, - } - } - - fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { - self.prompt - .update(cx, |prompt, cx| prompt.confirm(window, cx)) - } - - pub fn finished(&mut self, cx: &mut Context) { - self.finished = true; - cx.emit(DismissEvent); - } - - fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { - if let Some(tx) = self - .prompt - .update(cx, |prompt, _cx| prompt.cancellation.take()) - { - tx.send(()).ok(); - } - self.finished(cx); - } -} - -pub(crate) struct SshConnectionHeader { - pub(crate) connection_string: SharedString, - pub(crate) paths: Vec, - pub(crate) nickname: Option, - pub(crate) is_wsl: bool, - pub(crate) is_devcontainer: bool, -} - -impl RenderOnce for SshConnectionHeader { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let theme = cx.theme(); - - let mut header_color = theme.colors().text; - header_color.fade_out(0.96); - - let (main_label, meta_label) = if let Some(nickname) = self.nickname { - (nickname, Some(format!("({})", self.connection_string))) - } else { - (self.connection_string, None) - }; - - let icon = if self.is_wsl { - IconName::Linux - } else if self.is_devcontainer { - IconName::Box - } else { - IconName::Server - }; - - h_flex() - .px(DynamicSpacing::Base12.rems(cx)) - .pt(DynamicSpacing::Base08.rems(cx)) - .pb(DynamicSpacing::Base04.rems(cx)) - .rounded_t_sm() - .w_full() - .gap_1p5() - .child(Icon::new(icon).size(IconSize::Small)) - .child( - h_flex() - .gap_1() - .overflow_x_hidden() - .child( - div() - .max_w_96() - .overflow_x_hidden() - .text_ellipsis() - .child(Headline::new(main_label).size(HeadlineSize::XSmall)), - ) - .children( - meta_label.map(|label| { - Label::new(label).color(Color::Muted).size(LabelSize::Small) - }), - ) - .child(div().overflow_x_hidden().text_ellipsis().children( - self.paths.into_iter().map(|path| { - Label::new(path.to_string_lossy().into_owned()) - .size(LabelSize::Small) - .color(Color::Muted) - }), - )), - ) - } -} - -impl Render for RemoteConnectionModal { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { - let nickname = self.prompt.read(cx).nickname.clone(); - let connection_string = self.prompt.read(cx).connection_string.clone(); - let is_wsl = self.prompt.read(cx).is_wsl; - let is_devcontainer = self.prompt.read(cx).is_devcontainer; - - let theme = cx.theme().clone(); - let body_color = theme.colors().editor_background; - - v_flex() - .elevation_3(cx) - .w(rems(34.)) - .border_1() - .border_color(theme.colors().border) - .key_context("SshConnectionModal") - .track_focus(&self.focus_handle(cx)) - .on_action(cx.listener(Self::dismiss)) - .on_action(cx.listener(Self::confirm)) - .child( - SshConnectionHeader { - paths: self.paths.clone(), - connection_string, - nickname, - is_wsl, - is_devcontainer, - } - .render(window, cx), - ) - .child( - div() - .w_full() - .bg(body_color) - .border_y_1() - .border_color(theme.colors().border_variant) - .child(self.prompt.clone()), - ) - .child( - div().w_full().py_1().child( - ListItem::new("li-devcontainer-go-back") - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Close).color(Color::Muted)) - .child(Label::new("Cancel")) - .end_slot( - KeyBinding::for_action_in(&menu::Cancel, &self.focus_handle(cx), cx) - .size(rems_from_px(12.)), - ) - .on_click(cx.listener(|this, _, window, cx| { - this.dismiss(&menu::Cancel, window, cx); - })), - ), - ) - } -} - -impl Focusable for RemoteConnectionModal { - fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle { - self.prompt.read(cx).editor.focus_handle(cx) - } -} - -impl EventEmitter for RemoteConnectionModal {} - -impl ModalView for RemoteConnectionModal { - fn on_before_dismiss( - &mut self, - _window: &mut Window, - _: &mut Context, - ) -> workspace::DismissDecision { - workspace::DismissDecision::Dismiss(self.finished) - } - - fn fade_out_background(&self) -> bool { - true - } -} - -#[derive(Clone)] -pub struct RemoteClientDelegate { - window: AnyWindowHandle, - ui: WeakEntity, - known_password: Option, -} - -impl remote::RemoteClientDelegate for RemoteClientDelegate { - fn ask_password( - &self, - prompt: String, - tx: oneshot::Sender, - cx: &mut AsyncApp, - ) { - let mut known_password = self.known_password.clone(); - if let Some(password) = known_password.take() { - tx.send(password).ok(); - } else { - self.window - .update(cx, |_, window, cx| { - self.ui.update(cx, |modal, cx| { - modal.set_prompt(prompt, tx, window, cx); - }) - }) - .ok(); - } - } - - fn set_status(&self, status: Option<&str>, cx: &mut AsyncApp) { - self.update_status(status, cx) - } - - fn download_server_binary_locally( - &self, - platform: RemotePlatform, - release_channel: ReleaseChannel, - version: Option, - cx: &mut AsyncApp, - ) -> Task> { - let this = self.clone(); - cx.spawn(async move |cx| { - AutoUpdater::download_remote_server_release( - release_channel, - version.clone(), - platform.os, - platform.arch, - move |status, cx| this.set_status(Some(status), cx), - cx, - ) - .await - .with_context(|| { - format!( - "Downloading remote server binary (version: {}, os: {}, arch: {})", - version - .as_ref() - .map(|v| format!("{}", v)) - .unwrap_or("unknown".to_string()), - platform.os, - platform.arch, - ) - }) - }) - } - - fn get_download_url( - &self, - platform: RemotePlatform, - release_channel: ReleaseChannel, - version: Option, - cx: &mut AsyncApp, - ) -> Task>> { - cx.spawn(async move |cx| { - AutoUpdater::get_remote_server_release_url( - release_channel, - version, - platform.os, - platform.arch, - cx, - ) - .await - }) - } -} - -impl RemoteClientDelegate { - fn update_status(&self, status: Option<&str>, cx: &mut AsyncApp) { - self.window - .update(cx, |_, _, cx| { - self.ui.update(cx, |modal, cx| { - modal.set_status(status.map(|s| s.to_string()), cx); - }) - }) - .ok(); - } -} - -pub fn connect( - unique_identifier: ConnectionIdentifier, - connection_options: RemoteConnectionOptions, - ui: Entity, - window: &mut Window, - cx: &mut App, -) -> Task>>> { - let window = window.window_handle(); - let known_password = match &connection_options { - RemoteConnectionOptions::Ssh(ssh_connection_options) => ssh_connection_options - .password - .as_deref() - .and_then(|pw| pw.try_into().ok()), - _ => None, - }; - let (tx, rx) = oneshot::channel(); - ui.update(cx, |ui, _cx| ui.set_cancellation_tx(tx)); - - let delegate = Arc::new(RemoteClientDelegate { - window, - ui: ui.downgrade(), - known_password, - }); - - cx.spawn(async move |cx| { - let connection = remote::connect(connection_options, delegate.clone(), cx).await?; - cx.update(|cx| remote::RemoteClient::new(unique_identifier, connection, rx, delegate, cx))? - .await - }) -} - -pub async fn open_remote_project( - connection_options: RemoteConnectionOptions, - paths: Vec, - app_state: Arc, - open_options: workspace::OpenOptions, - cx: &mut AsyncApp, -) -> Result<()> { - let created_new_window = open_options.replace_window.is_none(); - let window = if let Some(window) = open_options.replace_window { - window - } else { - let workspace_position = cx - .update(|cx| { - // todo: These paths are wrong they may have column and line information - workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx) - })? - .await - .context("fetching ssh workspace position from db")?; - - let mut options = - cx.update(|cx| (app_state.build_window_options)(workspace_position.display, cx))?; - options.window_bounds = workspace_position.window_bounds; - - cx.open_window(options, |window, cx| { - let project = project::Project::local( - app_state.client.clone(), - app_state.node_runtime.clone(), - app_state.user_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - None, - cx, - ); - cx.new(|cx| { - let mut workspace = Workspace::new(None, project, app_state.clone(), window, cx); - workspace.centered_layout = workspace_position.centered_layout; - workspace - }) - })? - }; - - loop { - let (cancel_tx, cancel_rx) = oneshot::channel(); - let delegate = window.update(cx, { - let paths = paths.clone(); - let connection_options = connection_options.clone(); - move |workspace, window, cx| { - window.activate_window(); - workspace.toggle_modal(window, cx, |window, cx| { - RemoteConnectionModal::new(&connection_options, paths, window, cx) - }); - - let ui = workspace - .active_modal::(cx)? - .read(cx) - .prompt - .clone(); - - ui.update(cx, |ui, _cx| { - ui.set_cancellation_tx(cancel_tx); - }); - - Some(Arc::new(RemoteClientDelegate { - window: window.window_handle(), - ui: ui.downgrade(), - known_password: if let RemoteConnectionOptions::Ssh(options) = - &connection_options - { - options - .password - .as_deref() - .and_then(|pw| EncryptedPassword::try_from(pw).ok()) - } else { - None - }, - })) - } - })?; - - let Some(delegate) = delegate else { break }; - - let remote_connection = - match remote::connect(connection_options.clone(), delegate.clone(), cx).await { - Ok(connection) => connection, - Err(e) => { - window - .update(cx, |workspace, _, cx| { - if let Some(ui) = workspace.active_modal::(cx) { - ui.update(cx, |modal, cx| modal.finished(cx)) - } - }) - .ok(); - log::error!("Failed to open project: {e:#}"); - let response = window - .update(cx, |_, window, cx| { - window.prompt( - PromptLevel::Critical, - match connection_options { - RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH", - RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL", - RemoteConnectionOptions::Docker(_) => { - "Failed to connect to Dev Container" - } - }, - Some(&format!("{e:#}")), - &["Retry", "Cancel"], - cx, - ) - })? - .await; - - if response == Ok(0) { - continue; - } - - if created_new_window { - window - .update(cx, |_, window, _| window.remove_window()) - .ok(); - } - break; - } - }; - - let (paths, paths_with_positions) = - determine_paths_with_positions(&remote_connection, paths.clone()).await; - - let opened_items = cx - .update(|cx| { - workspace::open_remote_project_with_new_connection( - window, - remote_connection, - cancel_rx, - delegate.clone(), - app_state.clone(), - paths.clone(), - cx, - ) - })? - .await; - - window - .update(cx, |workspace, _, cx| { - if let Some(ui) = workspace.active_modal::(cx) { - ui.update(cx, |modal, cx| modal.finished(cx)) - } - }) - .ok(); - - match opened_items { - Err(e) => { - log::error!("Failed to open project: {e:#}"); - let response = window - .update(cx, |_, window, cx| { - window.prompt( - PromptLevel::Critical, - match connection_options { - RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH", - RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL", - RemoteConnectionOptions::Docker(_) => { - "Failed to connect to Dev Container" - } - }, - Some(&format!("{e:#}")), - &["Retry", "Cancel"], - cx, - ) - })? - .await; - if response == Ok(0) { - continue; - } - - if created_new_window { - window - .update(cx, |_, window, _| window.remove_window()) - .ok(); - } - } - - Ok(items) => { - for (item, path) in items.into_iter().zip(paths_with_positions) { - let Some(item) = item else { - continue; - }; - let Some(row) = path.row else { - continue; - }; - if let Some(active_editor) = item.downcast::() { - window - .update(cx, |_, window, cx| { - active_editor.update(cx, |editor, cx| { - let row = row.saturating_sub(1); - let col = path.column.unwrap_or(0).saturating_sub(1); - editor.go_to_singleton_buffer_point( - Point::new(row, col), - window, - cx, - ); - }); - }) - .ok(); - } - } - } - } - - window - .update(cx, |workspace, _, cx| { - if let Some(client) = workspace.project().read(cx).remote_client() { - ExtensionStore::global(cx) - .update(cx, |store, cx| store.register_remote_client(client, cx)); - } - }) - .ok(); - - break; - } - - // Already showed the error to the user - Ok(()) -} - -pub(crate) async fn determine_paths_with_positions( - remote_connection: &Arc, - mut paths: Vec, -) -> (Vec, Vec) { - let mut paths_with_positions = Vec::::new(); - for path in &mut paths { - if let Some(path_str) = path.to_str() { - let path_with_position = PathWithPosition::parse_str(&path_str); - if path_with_position.row.is_some() { - if !path_exists(&remote_connection, &path).await { - *path = path_with_position.path.clone(); - paths_with_positions.push(path_with_position); - continue; - } - } - } - paths_with_positions.push(PathWithPosition::from_path(path.clone())) - } - (paths, paths_with_positions) -} - -async fn path_exists(connection: &Arc, path: &Path) -> bool { - let Ok(command) = connection.build_command( - Some("test".to_string()), - &["-e".to_owned(), path.to_string_lossy().to_string()], - &Default::default(), - None, - None, - ) else { - return false; - }; - let Ok(mut child) = util::command::new_smol_command(command.program) - .args(command.args) - .envs(command.env) - .spawn() - else { - return false; - }; - child.status().await.is_ok_and(|status| status.success()) -} diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs deleted file mode 100644 index 32a4ef1a81..0000000000 --- a/crates/recent_projects/src/remote_servers.rs +++ /dev/null @@ -1,2787 +0,0 @@ -use crate::{ - dev_container::start_dev_container, - remote_connections::{ - Connection, RemoteConnectionModal, RemoteConnectionPrompt, SshConnection, - SshConnectionHeader, SshSettings, connect, determine_paths_with_positions, - open_remote_project, - }, - ssh_config::parse_ssh_config_hosts, -}; -use editor::Editor; -use file_finder::OpenPathDelegate; -use futures::{FutureExt, channel::oneshot, future::Shared, select}; -use gpui::{ - AnyElement, App, ClickEvent, ClipboardItem, Context, DismissEvent, Entity, EventEmitter, - FocusHandle, Focusable, PromptLevel, ScrollHandle, Subscription, Task, WeakEntity, Window, - canvas, -}; -use language::Point; -use log::info; -use paths::{global_ssh_config_file, user_ssh_config_file}; -use picker::Picker; -use project::{Fs, Project}; -use remote::{ - RemoteClient, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions, - remote_client::ConnectionIdentifier, -}; -use settings::{ - RemoteProject, RemoteSettingsContent, Settings as _, SettingsStore, update_settings_file, - watch_config_file, -}; -use smol::stream::StreamExt as _; -use std::{ - borrow::Cow, - collections::BTreeSet, - path::PathBuf, - rc::Rc, - sync::{ - Arc, - atomic::{self, AtomicUsize}, - }, -}; -use ui::{ - CommonAnimationExt, IconButtonShape, KeyBinding, List, ListItem, ListSeparator, Modal, - ModalHeader, Navigable, NavigableEntry, Section, Tooltip, WithScrollbar, prelude::*, -}; -use util::{ - ResultExt, - paths::{PathStyle, RemotePathBuf}, - rel_path::RelPath, -}; -use workspace::{ - ModalView, OpenOptions, Toast, Workspace, - notifications::{DetachAndPromptErr, NotificationId}, - open_remote_project_with_existing_connection, -}; - -pub struct RemoteServerProjects { - mode: Mode, - focus_handle: FocusHandle, - workspace: WeakEntity, - retained_connections: Vec>, - ssh_config_updates: Task<()>, - ssh_config_servers: BTreeSet, - create_new_window: bool, - _subscription: Subscription, -} - -struct CreateRemoteServer { - address_editor: Entity, - address_error: Option, - ssh_prompt: Option>, - _creating: Option>>, -} - -impl CreateRemoteServer { - fn new(window: &mut Window, cx: &mut App) -> Self { - let address_editor = cx.new(|cx| Editor::single_line(window, cx)); - address_editor.update(cx, |this, cx| { - this.focus_handle(cx).focus(window); - }); - Self { - address_editor, - address_error: None, - ssh_prompt: None, - _creating: None, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -enum DevContainerCreationProgress { - Initial, - Creating, - Error(String), -} - -#[derive(Clone)] -struct CreateRemoteDevContainer { - // 3 Navigable Options - // - Create from devcontainer.json - // - Edit devcontainer.json - // - Go back - entries: [NavigableEntry; 3], - progress: DevContainerCreationProgress, -} - -impl CreateRemoteDevContainer { - fn new(window: &mut Window, cx: &mut Context) -> Self { - let entries = std::array::from_fn(|_| NavigableEntry::focusable(cx)); - entries[0].focus_handle.focus(window); - Self { - entries, - progress: DevContainerCreationProgress::Initial, - } - } - - fn progress(&mut self, progress: DevContainerCreationProgress) -> Self { - self.progress = progress; - self.clone() - } -} - -#[cfg(target_os = "windows")] -struct AddWslDistro { - picker: Entity>, - connection_prompt: Option>, - _creating: Option>, -} - -#[cfg(target_os = "windows")] -impl AddWslDistro { - fn new(window: &mut Window, cx: &mut Context) -> Self { - use crate::wsl_picker::{WslDistroSelected, WslPickerDelegate, WslPickerDismissed}; - - let delegate = WslPickerDelegate::new(); - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)); - - cx.subscribe_in( - &picker, - window, - |this, _, _: &WslDistroSelected, window, cx| { - this.confirm(&menu::Confirm, window, cx); - }, - ) - .detach(); - - cx.subscribe_in( - &picker, - window, - |this, _, _: &WslPickerDismissed, window, cx| { - this.cancel(&menu::Cancel, window, cx); - }, - ) - .detach(); - - AddWslDistro { - picker, - connection_prompt: None, - _creating: None, - } - } -} - -enum ProjectPickerData { - Ssh { - connection_string: SharedString, - nickname: Option, - }, - Wsl { - distro_name: SharedString, - }, -} - -struct ProjectPicker { - data: ProjectPickerData, - picker: Entity>, - _path_task: Shared>>, -} - -struct EditNicknameState { - index: SshServerIndex, - editor: Entity, -} - -impl EditNicknameState { - fn new(index: SshServerIndex, window: &mut Window, cx: &mut App) -> Self { - let this = Self { - index, - editor: cx.new(|cx| Editor::single_line(window, cx)), - }; - let starting_text = SshSettings::get_global(cx) - .ssh_connections() - .nth(index.0) - .and_then(|state| state.nickname) - .filter(|text| !text.is_empty()); - this.editor.update(cx, |this, cx| { - this.set_placeholder_text("Add a nickname for this server", window, cx); - if let Some(starting_text) = starting_text { - this.set_text(starting_text, window, cx); - } - }); - this.editor.focus_handle(cx).focus(window); - this - } -} - -impl Focusable for ProjectPicker { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl ProjectPicker { - fn new( - create_new_window: bool, - index: ServerIndex, - connection: RemoteConnectionOptions, - project: Entity, - home_dir: RemotePathBuf, - path_style: PathStyle, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let (tx, rx) = oneshot::channel(); - let lister = project::DirectoryLister::Project(project.clone()); - let delegate = file_finder::OpenPathDelegate::new(tx, lister, false, path_style); - - let picker = cx.new(|cx| { - let picker = Picker::uniform_list(delegate, window, cx) - .width(rems(34.)) - .modal(false); - picker.set_query(home_dir.to_string(), window, cx); - picker - }); - - let data = match &connection { - RemoteConnectionOptions::Ssh(connection) => ProjectPickerData::Ssh { - connection_string: connection.connection_string().into(), - nickname: connection.nickname.clone().map(|nick| nick.into()), - }, - RemoteConnectionOptions::Wsl(connection) => ProjectPickerData::Wsl { - distro_name: connection.distro_name.clone().into(), - }, - RemoteConnectionOptions::Docker(_) => ProjectPickerData::Ssh { - // Not implemented as a project picker at this time - connection_string: "".into(), - nickname: None, - }, - }; - let _path_task = cx - .spawn_in(window, { - let workspace = workspace; - async move |this, cx| { - let Ok(Some(paths)) = rx.await else { - workspace - .update_in(cx, |workspace, window, cx| { - let fs = workspace.project().read(cx).fs().clone(); - let weak = cx.entity().downgrade(); - workspace.toggle_modal(window, cx, |window, cx| { - RemoteServerProjects::new( - create_new_window, - fs, - window, - weak, - cx, - ) - }); - }) - .log_err()?; - return None; - }; - - let app_state = workspace - .read_with(cx, |workspace, _| workspace.app_state().clone()) - .ok()?; - - let remote_connection = project - .read_with(cx, |project, cx| { - project.remote_client()?.read(cx).connection() - }) - .ok()??; - - let (paths, paths_with_positions) = - determine_paths_with_positions(&remote_connection, paths).await; - - cx.update(|_, cx| { - let fs = app_state.fs.clone(); - update_settings_file(fs, cx, { - let paths = paths - .iter() - .map(|path| path.to_string_lossy().into_owned()) - .collect(); - move |settings, _| match index { - ServerIndex::Ssh(index) => { - if let Some(server) = settings - .remote - .ssh_connections - .as_mut() - .and_then(|connections| connections.get_mut(index.0)) - { - server.projects.insert(RemoteProject { paths }); - }; - } - ServerIndex::Wsl(index) => { - if let Some(server) = settings - .remote - .wsl_connections - .as_mut() - .and_then(|connections| connections.get_mut(index.0)) - { - server.projects.insert(RemoteProject { paths }); - }; - } - } - }); - }) - .log_err(); - - let options = cx - .update(|_, cx| (app_state.build_window_options)(None, cx)) - .log_err()?; - let window = cx - .open_window(options, |window, cx| { - cx.new(|cx| { - telemetry::event!("SSH Project Created"); - Workspace::new(None, project.clone(), app_state.clone(), window, cx) - }) - }) - .log_err()?; - - let items = open_remote_project_with_existing_connection( - connection, project, paths, app_state, window, cx, - ) - .await - .log_err(); - - if let Some(items) = items { - for (item, path) in items.into_iter().zip(paths_with_positions) { - let Some(item) = item else { - continue; - }; - let Some(row) = path.row else { - continue; - }; - if let Some(active_editor) = item.downcast::() { - window - .update(cx, |_, window, cx| { - active_editor.update(cx, |editor, cx| { - let row = row.saturating_sub(1); - let col = path.column.unwrap_or(0).saturating_sub(1); - editor.go_to_singleton_buffer_point( - Point::new(row, col), - window, - cx, - ); - }); - }) - .ok(); - } - } - } - - this.update(cx, |_, cx| { - cx.emit(DismissEvent); - }) - .ok(); - Some(()) - } - }) - .shared(); - cx.new(|_| Self { - _path_task, - picker, - data, - }) - } -} - -impl gpui::Render for ProjectPicker { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .child(match &self.data { - ProjectPickerData::Ssh { - connection_string, - nickname, - } => SshConnectionHeader { - connection_string: connection_string.clone(), - paths: Default::default(), - nickname: nickname.clone(), - is_wsl: false, - is_devcontainer: false, - } - .render(window, cx), - ProjectPickerData::Wsl { distro_name } => SshConnectionHeader { - connection_string: distro_name.clone(), - paths: Default::default(), - nickname: None, - is_wsl: true, - is_devcontainer: false, - } - .render(window, cx), - }) - .child( - div() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child(self.picker.clone()), - ) - } -} - -#[repr(transparent)] -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -struct SshServerIndex(usize); -impl std::fmt::Display for SshServerIndex { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -#[repr(transparent)] -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -struct WslServerIndex(usize); -impl std::fmt::Display for WslServerIndex { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -enum ServerIndex { - Ssh(SshServerIndex), - Wsl(WslServerIndex), -} -impl From for ServerIndex { - fn from(index: SshServerIndex) -> Self { - Self::Ssh(index) - } -} -impl From for ServerIndex { - fn from(index: WslServerIndex) -> Self { - Self::Wsl(index) - } -} - -#[derive(Clone)] -enum RemoteEntry { - Project { - open_folder: NavigableEntry, - projects: Vec<(NavigableEntry, RemoteProject)>, - configure: NavigableEntry, - connection: Connection, - index: ServerIndex, - }, - SshConfig { - open_folder: NavigableEntry, - host: SharedString, - }, -} - -impl RemoteEntry { - fn is_from_zed(&self) -> bool { - matches!(self, Self::Project { .. }) - } - - fn connection(&self) -> Cow<'_, Connection> { - match self { - Self::Project { connection, .. } => Cow::Borrowed(connection), - Self::SshConfig { host, .. } => Cow::Owned( - SshConnection { - host: host.clone(), - ..SshConnection::default() - } - .into(), - ), - } - } -} - -#[derive(Clone)] -struct DefaultState { - scroll_handle: ScrollHandle, - add_new_server: NavigableEntry, - add_new_devcontainer: NavigableEntry, - add_new_wsl: NavigableEntry, - servers: Vec, -} - -impl DefaultState { - fn new(ssh_config_servers: &BTreeSet, cx: &mut App) -> Self { - let handle = ScrollHandle::new(); - let add_new_server = NavigableEntry::new(&handle, cx); - let add_new_devcontainer = NavigableEntry::new(&handle, cx); - let add_new_wsl = NavigableEntry::new(&handle, cx); - - let ssh_settings = SshSettings::get_global(cx); - let read_ssh_config = ssh_settings.read_ssh_config; - - let ssh_servers = ssh_settings - .ssh_connections() - .enumerate() - .map(|(index, connection)| { - let open_folder = NavigableEntry::new(&handle, cx); - let configure = NavigableEntry::new(&handle, cx); - let projects = connection - .projects - .iter() - .map(|project| (NavigableEntry::new(&handle, cx), project.clone())) - .collect(); - RemoteEntry::Project { - open_folder, - configure, - projects, - index: ServerIndex::Ssh(SshServerIndex(index)), - connection: connection.into(), - } - }); - - let wsl_servers = ssh_settings - .wsl_connections() - .enumerate() - .map(|(index, connection)| { - let open_folder = NavigableEntry::new(&handle, cx); - let configure = NavigableEntry::new(&handle, cx); - let projects = connection - .projects - .iter() - .map(|project| (NavigableEntry::new(&handle, cx), project.clone())) - .collect(); - RemoteEntry::Project { - open_folder, - configure, - projects, - index: ServerIndex::Wsl(WslServerIndex(index)), - connection: connection.into(), - } - }); - - let mut servers = ssh_servers.chain(wsl_servers).collect::>(); - - if read_ssh_config { - let mut extra_servers_from_config = ssh_config_servers.clone(); - for server in &servers { - if let RemoteEntry::Project { - connection: Connection::Ssh(ssh_options), - .. - } = server - { - extra_servers_from_config.remove(&SharedString::new(ssh_options.host.clone())); - } - } - servers.extend(extra_servers_from_config.into_iter().map(|host| { - RemoteEntry::SshConfig { - open_folder: NavigableEntry::new(&handle, cx), - host, - } - })); - } - - Self { - scroll_handle: handle, - add_new_server, - add_new_devcontainer, - add_new_wsl, - servers, - } - } -} - -#[derive(Clone)] -enum ViewServerOptionsState { - Ssh { - connection: SshConnectionOptions, - server_index: SshServerIndex, - entries: [NavigableEntry; 4], - }, - Wsl { - connection: WslConnectionOptions, - server_index: WslServerIndex, - entries: [NavigableEntry; 2], - }, -} - -impl ViewServerOptionsState { - fn entries(&self) -> &[NavigableEntry] { - match self { - Self::Ssh { entries, .. } => entries, - Self::Wsl { entries, .. } => entries, - } - } -} - -enum Mode { - Default(DefaultState), - ViewServerOptions(ViewServerOptionsState), - EditNickname(EditNicknameState), - ProjectPicker(Entity), - CreateRemoteServer(CreateRemoteServer), - CreateRemoteDevContainer(CreateRemoteDevContainer), - #[cfg(target_os = "windows")] - AddWslDistro(AddWslDistro), -} - -impl Mode { - fn default_mode(ssh_config_servers: &BTreeSet, cx: &mut App) -> Self { - Self::Default(DefaultState::new(ssh_config_servers, cx)) - } -} - -impl RemoteServerProjects { - #[cfg(target_os = "windows")] - pub fn wsl( - create_new_window: bool, - fs: Arc, - window: &mut Window, - workspace: WeakEntity, - cx: &mut Context, - ) -> Self { - Self::new_inner( - Mode::AddWslDistro(AddWslDistro::new(window, cx)), - create_new_window, - fs, - window, - workspace, - cx, - ) - } - - pub fn new( - create_new_window: bool, - fs: Arc, - window: &mut Window, - workspace: WeakEntity, - cx: &mut Context, - ) -> Self { - Self::new_inner( - Mode::default_mode(&BTreeSet::new(), cx), - create_new_window, - fs, - window, - workspace, - cx, - ) - } - - /// Creates a new RemoteServerProjects modal that opens directly in dev container creation mode. - /// Used when suggesting dev container connection from toast notification. - pub fn new_dev_container( - fs: Arc, - window: &mut Window, - workspace: WeakEntity, - cx: &mut Context, - ) -> Self { - Self::new_inner( - Mode::CreateRemoteDevContainer( - CreateRemoteDevContainer::new(window, cx) - .progress(DevContainerCreationProgress::Creating), - ), - false, - fs, - window, - workspace, - cx, - ) - } - - fn new_inner( - mode: Mode, - create_new_window: bool, - fs: Arc, - window: &mut Window, - workspace: WeakEntity, - cx: &mut Context, - ) -> Self { - let focus_handle = cx.focus_handle(); - let mut read_ssh_config = SshSettings::get_global(cx).read_ssh_config; - let ssh_config_updates = if read_ssh_config { - spawn_ssh_config_watch(fs.clone(), cx) - } else { - Task::ready(()) - }; - - let mut base_style = window.text_style(); - base_style.refine(&gpui::TextStyleRefinement { - color: Some(cx.theme().colors().editor_foreground), - ..Default::default() - }); - - let _subscription = - cx.observe_global_in::(window, move |recent_projects, _, cx| { - let new_read_ssh_config = SshSettings::get_global(cx).read_ssh_config; - if read_ssh_config != new_read_ssh_config { - read_ssh_config = new_read_ssh_config; - if read_ssh_config { - recent_projects.ssh_config_updates = spawn_ssh_config_watch(fs.clone(), cx); - } else { - recent_projects.ssh_config_servers.clear(); - recent_projects.ssh_config_updates = Task::ready(()); - } - } - }); - - Self { - mode, - focus_handle, - workspace, - retained_connections: Vec::new(), - ssh_config_updates, - ssh_config_servers: BTreeSet::new(), - create_new_window, - _subscription, - } - } - - fn project_picker( - create_new_window: bool, - index: ServerIndex, - connection_options: remote::RemoteConnectionOptions, - project: Entity, - home_dir: RemotePathBuf, - path_style: PathStyle, - window: &mut Window, - cx: &mut Context, - workspace: WeakEntity, - ) -> Self { - let fs = project.read(cx).fs().clone(); - let mut this = Self::new(create_new_window, fs, window, workspace.clone(), cx); - this.mode = Mode::ProjectPicker(ProjectPicker::new( - create_new_window, - index, - connection_options, - project, - home_dir, - path_style, - workspace, - window, - cx, - )); - cx.notify(); - - this - } - - fn create_ssh_server( - &mut self, - editor: Entity, - window: &mut Window, - cx: &mut Context, - ) { - let input = get_text(&editor, cx); - if input.is_empty() { - return; - } - - let connection_options = match SshConnectionOptions::parse_command_line(&input) { - Ok(c) => c, - Err(e) => { - self.mode = Mode::CreateRemoteServer(CreateRemoteServer { - address_editor: editor, - address_error: Some(format!("could not parse: {:?}", e).into()), - ssh_prompt: None, - _creating: None, - }); - return; - } - }; - let ssh_prompt = cx.new(|cx| { - RemoteConnectionPrompt::new( - connection_options.connection_string(), - connection_options.nickname.clone(), - false, - false, - window, - cx, - ) - }); - - let connection = connect( - ConnectionIdentifier::setup(), - RemoteConnectionOptions::Ssh(connection_options.clone()), - ssh_prompt.clone(), - window, - cx, - ) - .prompt_err("Failed to connect", window, cx, |_, _, _| None); - - let address_editor = editor.clone(); - let creating = cx.spawn_in(window, async move |this, cx| { - match connection.await { - Some(Some(client)) => this - .update_in(cx, |this, window, cx| { - info!("ssh server created"); - telemetry::event!("SSH Server Created"); - this.retained_connections.push(client); - this.add_ssh_server(connection_options, cx); - this.mode = Mode::default_mode(&this.ssh_config_servers, cx); - this.focus_handle(cx).focus(window); - cx.notify() - }) - .log_err(), - _ => this - .update(cx, |this, cx| { - address_editor.update(cx, |this, _| { - this.set_read_only(false); - }); - this.mode = Mode::CreateRemoteServer(CreateRemoteServer { - address_editor, - address_error: None, - ssh_prompt: None, - _creating: None, - }); - cx.notify() - }) - .log_err(), - }; - None - }); - - editor.update(cx, |this, _| { - this.set_read_only(true); - }); - self.mode = Mode::CreateRemoteServer(CreateRemoteServer { - address_editor: editor, - address_error: None, - ssh_prompt: Some(ssh_prompt), - _creating: Some(creating), - }); - } - - #[cfg(target_os = "windows")] - fn connect_wsl_distro( - &mut self, - picker: Entity>, - distro: String, - window: &mut Window, - cx: &mut Context, - ) { - let connection_options = WslConnectionOptions { - distro_name: distro, - user: None, - }; - - let prompt = cx.new(|cx| { - RemoteConnectionPrompt::new( - connection_options.distro_name.clone(), - None, - true, - false, - window, - cx, - ) - }); - let connection = connect( - ConnectionIdentifier::setup(), - connection_options.clone().into(), - prompt.clone(), - window, - cx, - ) - .prompt_err("Failed to connect", window, cx, |_, _, _| None); - - let wsl_picker = picker.clone(); - let creating = cx.spawn_in(window, async move |this, cx| { - match connection.await { - Some(Some(client)) => this.update_in(cx, |this, window, cx| { - telemetry::event!("WSL Distro Added"); - this.retained_connections.push(client); - let Some(fs) = this - .workspace - .read_with(cx, |workspace, cx| { - workspace.project().read(cx).fs().clone() - }) - .log_err() - else { - return; - }; - - crate::add_wsl_distro(fs, &connection_options, cx); - this.mode = Mode::default_mode(&BTreeSet::new(), cx); - this.focus_handle(cx).focus(window); - cx.notify(); - }), - _ => this.update(cx, |this, cx| { - this.mode = Mode::AddWslDistro(AddWslDistro { - picker: wsl_picker, - connection_prompt: None, - _creating: None, - }); - cx.notify(); - }), - } - .log_err(); - }); - - self.mode = Mode::AddWslDistro(AddWslDistro { - picker, - connection_prompt: Some(prompt), - _creating: Some(creating), - }); - } - - fn view_server_options( - &mut self, - (server_index, connection): (ServerIndex, RemoteConnectionOptions), - window: &mut Window, - cx: &mut Context, - ) { - self.mode = Mode::ViewServerOptions(match (server_index, connection) { - (ServerIndex::Ssh(server_index), RemoteConnectionOptions::Ssh(connection)) => { - ViewServerOptionsState::Ssh { - connection, - server_index, - entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)), - } - } - (ServerIndex::Wsl(server_index), RemoteConnectionOptions::Wsl(connection)) => { - ViewServerOptionsState::Wsl { - connection, - server_index, - entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)), - } - } - _ => { - log::error!("server index and connection options mismatch"); - self.mode = Mode::default_mode(&BTreeSet::default(), cx); - return; - } - }); - self.focus_handle(cx).focus(window); - cx.notify(); - } - - fn view_in_progress_dev_container(&mut self, window: &mut Window, cx: &mut Context) { - self.mode = Mode::CreateRemoteDevContainer( - CreateRemoteDevContainer::new(window, cx) - .progress(DevContainerCreationProgress::Creating), - ); - self.focus_handle(cx).focus(window); - cx.notify(); - } - - fn create_remote_project( - &mut self, - index: ServerIndex, - connection_options: RemoteConnectionOptions, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = self.workspace.upgrade() else { - return; - }; - - let create_new_window = self.create_new_window; - workspace.update(cx, |_, cx| { - cx.defer_in(window, move |workspace, window, cx| { - let app_state = workspace.app_state().clone(); - workspace.toggle_modal(window, cx, |window, cx| { - RemoteConnectionModal::new(&connection_options, Vec::new(), window, cx) - }); - let prompt = workspace - .active_modal::(cx) - .unwrap() - .read(cx) - .prompt - .clone(); - - let connect = connect( - ConnectionIdentifier::setup(), - connection_options.clone(), - prompt, - window, - cx, - ) - .prompt_err("Failed to connect", window, cx, |_, _, _| None); - - cx.spawn_in(window, async move |workspace, cx| { - let session = connect.await; - - workspace.update(cx, |workspace, cx| { - if let Some(prompt) = workspace.active_modal::(cx) { - prompt.update(cx, |prompt, cx| prompt.finished(cx)) - } - })?; - - let Some(Some(session)) = session else { - return workspace.update_in(cx, |workspace, window, cx| { - let weak = cx.entity().downgrade(); - let fs = workspace.project().read(cx).fs().clone(); - workspace.toggle_modal(window, cx, |window, cx| { - RemoteServerProjects::new(create_new_window, fs, window, weak, cx) - }); - }); - }; - - let (path_style, project) = cx.update(|_, cx| { - ( - session.read(cx).path_style(), - project::Project::remote( - session, - app_state.client.clone(), - app_state.node_runtime.clone(), - app_state.user_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - cx, - ), - ) - })?; - - let home_dir = project - .read_with(cx, |project, cx| project.resolve_abs_path("~", cx))? - .await - .and_then(|path| path.into_abs_path()) - .map(|path| RemotePathBuf::new(path, path_style)) - .unwrap_or_else(|| match path_style { - PathStyle::Posix => RemotePathBuf::from_str("/", PathStyle::Posix), - PathStyle::Windows => { - RemotePathBuf::from_str("C:\\", PathStyle::Windows) - } - }); - - workspace - .update_in(cx, |workspace, window, cx| { - let weak = cx.entity().downgrade(); - workspace.toggle_modal(window, cx, |window, cx| { - RemoteServerProjects::project_picker( - create_new_window, - index, - connection_options, - project, - home_dir, - path_style, - window, - cx, - weak, - ) - }); - }) - .ok(); - Ok(()) - }) - .detach(); - }) - }) - } - - fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { - match &self.mode { - Mode::Default(_) | Mode::ViewServerOptions(_) => {} - Mode::ProjectPicker(_) => {} - Mode::CreateRemoteServer(state) => { - if let Some(prompt) = state.ssh_prompt.as_ref() { - prompt.update(cx, |prompt, cx| { - prompt.confirm(window, cx); - }); - return; - } - - self.create_ssh_server(state.address_editor.clone(), window, cx); - } - Mode::CreateRemoteDevContainer(_) => {} - Mode::EditNickname(state) => { - let text = Some(state.editor.read(cx).text(cx)).filter(|text| !text.is_empty()); - let index = state.index; - self.update_settings_file(cx, move |setting, _| { - if let Some(connections) = setting.ssh_connections.as_mut() - && let Some(connection) = connections.get_mut(index.0) - { - connection.nickname = text; - } - }); - self.mode = Mode::default_mode(&self.ssh_config_servers, cx); - self.focus_handle.focus(window); - } - #[cfg(target_os = "windows")] - Mode::AddWslDistro(state) => { - let delegate = &state.picker.read(cx).delegate; - let distro = delegate.selected_distro().unwrap(); - self.connect_wsl_distro(state.picker.clone(), distro, window, cx); - } - } - } - - fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context) { - match &self.mode { - Mode::Default(_) => cx.emit(DismissEvent), - Mode::CreateRemoteServer(state) if state.ssh_prompt.is_some() => { - let new_state = CreateRemoteServer::new(window, cx); - let old_prompt = state.address_editor.read(cx).text(cx); - new_state.address_editor.update(cx, |this, cx| { - this.set_text(old_prompt, window, cx); - }); - - self.mode = Mode::CreateRemoteServer(new_state); - cx.notify(); - } - _ => { - self.mode = Mode::default_mode(&self.ssh_config_servers, cx); - self.focus_handle(cx).focus(window); - cx.notify(); - } - } - } - - fn render_remote_connection( - &mut self, - ix: usize, - remote_server: RemoteEntry, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let connection = remote_server.connection().into_owned(); - - let (main_label, aux_label, is_wsl) = match &connection { - Connection::Ssh(connection) => { - if let Some(nickname) = connection.nickname.clone() { - let aux_label = SharedString::from(format!("({})", connection.host)); - (nickname.into(), Some(aux_label), false) - } else { - (connection.host.clone(), None, false) - } - } - Connection::Wsl(wsl_connection_options) => { - (wsl_connection_options.distro_name.clone(), None, true) - } - Connection::DevContainer(dev_container_options) => { - (dev_container_options.name.clone(), None, false) - } - }; - v_flex() - .w_full() - .child(ListSeparator) - .child( - h_flex() - .group("ssh-server") - .w_full() - .pt_0p5() - .px_3() - .gap_1() - .overflow_hidden() - .child( - h_flex() - .gap_1() - .max_w_96() - .overflow_hidden() - .text_ellipsis() - .when(is_wsl, |this| { - this.child( - Label::new("WSL:") - .size(LabelSize::Small) - .color(Color::Muted), - ) - }) - .child( - Label::new(main_label) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - .children( - aux_label.map(|label| { - Label::new(label).size(LabelSize::Small).color(Color::Muted) - }), - ), - ) - .child(match &remote_server { - RemoteEntry::Project { - open_folder, - projects, - configure, - connection, - index, - } => { - let index = *index; - List::new() - .empty_message("No projects.") - .children(projects.iter().enumerate().map(|(pix, p)| { - v_flex().gap_0p5().child(self.render_remote_project( - index, - remote_server.clone(), - pix, - p, - window, - cx, - )) - })) - .child( - h_flex() - .id(("new-remote-project-container", ix)) - .track_focus(&open_folder.focus_handle) - .anchor_scroll(open_folder.scroll_anchor.clone()) - .on_action(cx.listener({ - let connection = connection.clone(); - move |this, _: &menu::Confirm, window, cx| { - this.create_remote_project( - index, - connection.clone().into(), - window, - cx, - ); - } - })) - .child( - ListItem::new(("new-remote-project", ix)) - .toggle_state( - open_folder.focus_handle.contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) - .child(Label::new("Open Folder")) - .on_click(cx.listener({ - let connection = connection.clone(); - move |this, _, window, cx| { - this.create_remote_project( - index, - connection.clone().into(), - window, - cx, - ); - } - })), - ), - ) - .child( - h_flex() - .id(("server-options-container", ix)) - .track_focus(&configure.focus_handle) - .anchor_scroll(configure.scroll_anchor.clone()) - .on_action(cx.listener({ - let connection = connection.clone(); - move |this, _: &menu::Confirm, window, cx| { - this.view_server_options( - (index, connection.clone().into()), - window, - cx, - ); - } - })) - .child( - ListItem::new(("server-options", ix)) - .toggle_state( - configure.focus_handle.contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot( - Icon::new(IconName::Settings).color(Color::Muted), - ) - .child(Label::new("View Server Options")) - .on_click(cx.listener({ - let ssh_connection = connection.clone(); - move |this, _, window, cx| { - this.view_server_options( - (index, ssh_connection.clone().into()), - window, - cx, - ); - } - })), - ), - ) - } - RemoteEntry::SshConfig { open_folder, host } => List::new().child( - h_flex() - .id(("new-remote-project-container", ix)) - .track_focus(&open_folder.focus_handle) - .anchor_scroll(open_folder.scroll_anchor.clone()) - .on_action(cx.listener({ - let connection = connection.clone(); - let host = host.clone(); - move |this, _: &menu::Confirm, window, cx| { - let new_ix = this.create_host_from_ssh_config(&host, cx); - this.create_remote_project( - new_ix.into(), - connection.clone().into(), - window, - cx, - ); - } - })) - .child( - ListItem::new(("new-remote-project", ix)) - .toggle_state(open_folder.focus_handle.contains_focused(window, cx)) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) - .child(Label::new("Open Folder")) - .on_click(cx.listener({ - let host = host.clone(); - move |this, _, window, cx| { - let new_ix = this.create_host_from_ssh_config(&host, cx); - this.create_remote_project( - new_ix.into(), - connection.clone().into(), - window, - cx, - ); - } - })), - ), - ), - }) - } - - fn render_remote_project( - &mut self, - server_ix: ServerIndex, - server: RemoteEntry, - ix: usize, - (navigation, project): &(NavigableEntry, RemoteProject), - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let create_new_window = self.create_new_window; - let is_from_zed = server.is_from_zed(); - let element_id_base = SharedString::from(format!( - "remote-project-{}", - match server_ix { - ServerIndex::Ssh(index) => format!("ssh-{index}"), - ServerIndex::Wsl(index) => format!("wsl-{index}"), - } - )); - let container_element_id_base = - SharedString::from(format!("remote-project-container-{element_id_base}")); - - let callback = Rc::new({ - let project = project.clone(); - move |remote_server_projects: &mut Self, - secondary_confirm: bool, - window: &mut Window, - cx: &mut Context| { - let Some(app_state) = remote_server_projects - .workspace - .read_with(cx, |workspace, _| workspace.app_state().clone()) - .log_err() - else { - return; - }; - let project = project.clone(); - let server = server.connection().into_owned(); - cx.emit(DismissEvent); - - let replace_window = match (create_new_window, secondary_confirm) { - (true, false) | (false, true) => None, - (true, true) | (false, false) => window.window_handle().downcast::(), - }; - - cx.spawn_in(window, async move |_, cx| { - let result = open_remote_project( - server.into(), - project.paths.into_iter().map(PathBuf::from).collect(), - app_state, - OpenOptions { - replace_window, - ..OpenOptions::default() - }, - cx, - ) - .await; - if let Err(e) = result { - log::error!("Failed to connect: {e:#}"); - cx.prompt( - gpui::PromptLevel::Critical, - "Failed to connect", - Some(&e.to_string()), - &["Ok"], - ) - .await - .ok(); - } - }) - .detach(); - } - }); - - div() - .id((container_element_id_base, ix)) - .track_focus(&navigation.focus_handle) - .anchor_scroll(navigation.scroll_anchor.clone()) - .on_action(cx.listener({ - let callback = callback.clone(); - move |this, _: &menu::Confirm, window, cx| { - callback(this, false, window, cx); - } - })) - .on_action(cx.listener({ - let callback = callback.clone(); - move |this, _: &menu::SecondaryConfirm, window, cx| { - callback(this, true, window, cx); - } - })) - .child( - ListItem::new((element_id_base, ix)) - .toggle_state(navigation.focus_handle.contains_focused(window, cx)) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot( - Icon::new(IconName::Folder) - .color(Color::Muted) - .size(IconSize::Small), - ) - .child(Label::new(project.paths.join(", "))) - .on_click(cx.listener(move |this, e: &ClickEvent, window, cx| { - let secondary_confirm = e.modifiers().platform; - callback(this, secondary_confirm, window, cx) - })) - .when(is_from_zed, |server_list_item| { - server_list_item.end_hover_slot::(Some( - div() - .mr_2() - .child({ - let project = project.clone(); - // Right-margin to offset it from the Scrollbar - IconButton::new("remove-remote-project", IconName::Trash) - .icon_size(IconSize::Small) - .shape(IconButtonShape::Square) - .size(ButtonSize::Large) - .tooltip(Tooltip::text("Delete Remote Project")) - .on_click(cx.listener(move |this, _, _, cx| { - this.delete_remote_project(server_ix, &project, cx) - })) - }) - .into_any_element(), - )) - }), - ) - } - - fn update_settings_file( - &mut self, - cx: &mut Context, - f: impl FnOnce(&mut RemoteSettingsContent, &App) + Send + Sync + 'static, - ) { - let Some(fs) = self - .workspace - .read_with(cx, |workspace, _| workspace.app_state().fs.clone()) - .log_err() - else { - return; - }; - update_settings_file(fs, cx, move |setting, cx| f(&mut setting.remote, cx)); - } - - fn delete_ssh_server(&mut self, server: SshServerIndex, cx: &mut Context) { - self.update_settings_file(cx, move |setting, _| { - if let Some(connections) = setting.ssh_connections.as_mut() { - connections.remove(server.0); - } - }); - } - - fn delete_remote_project( - &mut self, - server: ServerIndex, - project: &RemoteProject, - cx: &mut Context, - ) { - match server { - ServerIndex::Ssh(server) => { - self.delete_ssh_project(server, project, cx); - } - ServerIndex::Wsl(server) => { - self.delete_wsl_project(server, project, cx); - } - } - } - - fn delete_ssh_project( - &mut self, - server: SshServerIndex, - project: &RemoteProject, - cx: &mut Context, - ) { - let project = project.clone(); - self.update_settings_file(cx, move |setting, _| { - if let Some(server) = setting - .ssh_connections - .as_mut() - .and_then(|connections| connections.get_mut(server.0)) - { - server.projects.remove(&project); - } - }); - } - - fn delete_wsl_project( - &mut self, - server: WslServerIndex, - project: &RemoteProject, - cx: &mut Context, - ) { - let project = project.clone(); - self.update_settings_file(cx, move |setting, _| { - if let Some(server) = setting - .wsl_connections - .as_mut() - .and_then(|connections| connections.get_mut(server.0)) - { - server.projects.remove(&project); - } - }); - } - - fn delete_wsl_distro(&mut self, server: WslServerIndex, cx: &mut Context) { - self.update_settings_file(cx, move |setting, _| { - if let Some(connections) = setting.wsl_connections.as_mut() { - connections.remove(server.0); - } - }); - } - - fn add_ssh_server( - &mut self, - connection_options: remote::SshConnectionOptions, - cx: &mut Context, - ) { - self.update_settings_file(cx, move |setting, _| { - setting - .ssh_connections - .get_or_insert(Default::default()) - .push(SshConnection { - host: SharedString::from(connection_options.host), - username: connection_options.username, - port: connection_options.port, - projects: BTreeSet::new(), - nickname: None, - args: connection_options.args.unwrap_or_default(), - upload_binary_over_ssh: None, - port_forwards: connection_options.port_forwards, - }) - }); - } - - fn edit_in_dev_container_json(&mut self, window: &mut Window, cx: &mut Context) { - let Some(workspace) = self.workspace.upgrade() else { - cx.emit(DismissEvent); - cx.notify(); - return; - }; - - workspace.update(cx, |workspace, cx| { - let project = workspace.project().clone(); - - let worktree = project - .read(cx) - .visible_worktrees(cx) - .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree)); - - if let Some(worktree) = worktree { - let tree_id = worktree.read(cx).id(); - let devcontainer_path = RelPath::unix(".devcontainer/devcontainer.json").unwrap(); - cx.spawn_in(window, async move |workspace, cx| { - workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_path( - (tree_id, devcontainer_path), - None, - true, - window, - cx, - ) - })? - .await - }) - .detach(); - } else { - return; - } - }); - cx.emit(DismissEvent); - cx.notify(); - } - - fn open_dev_container(&self, window: &mut Window, cx: &mut Context) { - let Some(app_state) = self - .workspace - .read_with(cx, |workspace, _| workspace.app_state().clone()) - .log_err() - else { - return; - }; - - let replace_window = window.window_handle().downcast::(); - - cx.spawn_in(window, async move |entity, cx| { - let (connection, starting_dir) = - match start_dev_container(cx, app_state.node_runtime.clone()).await { - Ok((c, s)) => (c, s), - Err(e) => { - log::error!("Failed to start dev container: {:?}", e); - entity - .update_in(cx, |remote_server_projects, window, cx| { - remote_server_projects.mode = Mode::CreateRemoteDevContainer( - CreateRemoteDevContainer::new(window, cx).progress( - DevContainerCreationProgress::Error(format!("{:?}", e)), - ), - ); - }) - .log_err(); - return; - } - }; - entity - .update(cx, |_, cx| { - cx.emit(DismissEvent); - }) - .log_err(); - - let result = open_remote_project( - connection.into(), - vec![starting_dir].into_iter().map(PathBuf::from).collect(), - app_state, - OpenOptions { - replace_window, - ..OpenOptions::default() - }, - cx, - ) - .await; - if let Err(e) = result { - log::error!("Failed to connect: {e:#}"); - cx.prompt( - gpui::PromptLevel::Critical, - "Failed to connect", - Some(&e.to_string()), - &["Ok"], - ) - .await - .ok(); - } - }) - .detach(); - } - - fn render_create_dev_container( - &self, - state: &CreateRemoteDevContainer, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - match &state.progress { - DevContainerCreationProgress::Error(message) => { - self.focus_handle(cx).focus(window); - return div() - .track_focus(&self.focus_handle(cx)) - .size_full() - .child( - v_flex() - .py_1() - .child( - ListItem::new("Error") - .inset(true) - .selectable(false) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::XCircle).color(Color::Error)) - .child(Label::new("Error Creating Dev Container:")) - .child(Label::new(message).buffer_font(cx)), - ) - .child(ListSeparator) - .child( - div() - .id("devcontainer-go-back") - .track_focus(&state.entries[0].focus_handle) - .on_action(cx.listener( - |this, _: &menu::Confirm, window, cx| { - this.mode = - Mode::default_mode(&this.ssh_config_servers, cx); - cx.focus_self(window); - cx.notify(); - }, - )) - .child( - ListItem::new("li-devcontainer-go-back") - .toggle_state( - state.entries[0] - .focus_handle - .contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot( - Icon::new(IconName::ArrowLeft).color(Color::Muted), - ) - .child(Label::new("Go Back")) - .end_slot( - KeyBinding::for_action_in( - &menu::Cancel, - &self.focus_handle, - cx, - ) - .size(rems_from_px(12.)), - ) - .on_click(cx.listener(|this, _, window, cx| { - let state = - CreateRemoteDevContainer::new(window, cx); - this.mode = Mode::CreateRemoteDevContainer(state); - - cx.notify(); - })), - ), - ), - ) - .into_any_element(); - } - _ => {} - }; - - let mut view = Navigable::new( - div() - .track_focus(&self.focus_handle(cx)) - .size_full() - .child( - v_flex() - .pb_1() - .child( - ModalHeader::new() - .child(Headline::new("Dev Containers").size(HeadlineSize::XSmall)), - ) - .child(ListSeparator) - .child( - div() - .id("confirm-create-from-devcontainer-json") - .track_focus(&state.entries[0].focus_handle) - .on_action(cx.listener({ - move |this, _: &menu::Confirm, window, cx| { - this.open_dev_container(window, cx); - this.view_in_progress_dev_container(window, cx); - } - })) - .map(|this| { - if state.progress == DevContainerCreationProgress::Creating { - this.child( - ListItem::new("creating") - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .disabled(true) - .start_slot( - Icon::new(IconName::ArrowCircle) - .color(Color::Muted) - .with_rotate_animation(2), - ) - .child( - h_flex() - .opacity(0.6) - .gap_1() - .child(Label::new("Creating From")) - .child( - Label::new("devcontainer.json") - .buffer_font(cx), - ) - .child(LoadingLabel::new("")), - ), - ) - } else { - this.child( - ListItem::new( - "li-confirm-create-from-devcontainer-json", - ) - .toggle_state( - state.entries[0] - .focus_handle - .contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot( - Icon::new(IconName::Plus).color(Color::Muted), - ) - .child( - h_flex() - .gap_1() - .child(Label::new("Open or Create New From")) - .child( - Label::new("devcontainer.json") - .buffer_font(cx), - ), - ) - .on_click( - cx.listener({ - move |this, _, window, cx| { - this.open_dev_container(window, cx); - this.view_in_progress_dev_container( - window, cx, - ); - cx.notify(); - } - }), - ), - ) - } - }), - ) - .child( - div() - .id("edit-devcontainer-json") - .track_focus(&state.entries[1].focus_handle) - .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| { - this.edit_in_dev_container_json(window, cx); - })) - .child( - ListItem::new("li-edit-devcontainer-json") - .toggle_state( - state.entries[1] - .focus_handle - .contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Pencil).color(Color::Muted)) - .child( - h_flex().gap_1().child(Label::new("Edit")).child( - Label::new("devcontainer.json").buffer_font(cx), - ), - ) - .on_click(cx.listener(move |this, _, window, cx| { - this.edit_in_dev_container_json(window, cx); - })), - ), - ) - .child(ListSeparator) - .child( - div() - .id("devcontainer-go-back") - .track_focus(&state.entries[2].focus_handle) - .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| { - this.mode = Mode::default_mode(&this.ssh_config_servers, cx); - cx.focus_self(window); - cx.notify(); - })) - .child( - ListItem::new("li-devcontainer-go-back") - .toggle_state( - state.entries[2] - .focus_handle - .contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot( - Icon::new(IconName::ArrowLeft).color(Color::Muted), - ) - .child(Label::new("Go Back")) - .end_slot( - KeyBinding::for_action_in( - &menu::Cancel, - &self.focus_handle, - cx, - ) - .size(rems_from_px(12.)), - ) - .on_click(cx.listener(|this, _, window, cx| { - this.mode = - Mode::default_mode(&this.ssh_config_servers, cx); - cx.focus_self(window); - cx.notify() - })), - ), - ), - ) - .into_any_element(), - ); - - view = view.entry(state.entries[0].clone()); - view = view.entry(state.entries[1].clone()); - view = view.entry(state.entries[2].clone()); - - view.render(window, cx).into_any_element() - } - - fn render_create_remote_server( - &self, - state: &CreateRemoteServer, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let ssh_prompt = state.ssh_prompt.clone(); - - state.address_editor.update(cx, |editor, cx| { - if editor.text(cx).is_empty() { - editor.set_placeholder_text("ssh user@example -p 2222", window, cx); - } - }); - - let theme = cx.theme(); - - v_flex() - .track_focus(&self.focus_handle(cx)) - .id("create-remote-server") - .overflow_hidden() - .size_full() - .flex_1() - .child( - div() - .p_2() - .border_b_1() - .border_color(theme.colors().border_variant) - .child(state.address_editor.clone()), - ) - .child( - h_flex() - .bg(theme.colors().editor_background) - .rounded_b_sm() - .w_full() - .map(|this| { - if let Some(ssh_prompt) = ssh_prompt { - this.child(h_flex().w_full().child(ssh_prompt)) - } else if let Some(address_error) = &state.address_error { - this.child( - h_flex().p_2().w_full().gap_2().child( - Label::new(address_error.clone()) - .size(LabelSize::Small) - .color(Color::Error), - ), - ) - } else { - this.child( - h_flex() - .p_2() - .w_full() - .gap_1() - .child( - Label::new( - "Enter the command you use to SSH into this server.", - ) - .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://zed.dev/docs/remote-development", - ); - }), - ), - ) - } - }), - ) - } - - #[cfg(target_os = "windows")] - fn render_add_wsl_distro( - &self, - state: &AddWslDistro, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let connection_prompt = state.connection_prompt.clone(); - - state.picker.update(cx, |picker, cx| { - picker.focus_handle(cx).focus(window); - }); - - v_flex() - .id("add-wsl-distro") - .overflow_hidden() - .size_full() - .flex_1() - .map(|this| { - if let Some(connection_prompt) = connection_prompt { - this.child(connection_prompt) - } else { - this.child(state.picker.clone()) - } - }) - } - - fn render_view_options( - &mut self, - options: ViewServerOptionsState, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let last_entry = options.entries().last().unwrap(); - - let mut view = Navigable::new( - div() - .track_focus(&self.focus_handle(cx)) - .size_full() - .child(match &options { - ViewServerOptionsState::Ssh { connection, .. } => SshConnectionHeader { - connection_string: connection.host.clone().into(), - paths: Default::default(), - nickname: connection.nickname.clone().map(|s| s.into()), - is_wsl: false, - is_devcontainer: false, - } - .render(window, cx) - .into_any_element(), - ViewServerOptionsState::Wsl { connection, .. } => SshConnectionHeader { - connection_string: connection.distro_name.clone().into(), - paths: Default::default(), - nickname: None, - is_wsl: true, - is_devcontainer: false, - } - .render(window, cx) - .into_any_element(), - }) - .child( - v_flex() - .pb_1() - .child(ListSeparator) - .map(|this| match &options { - ViewServerOptionsState::Ssh { - connection, - entries, - server_index, - } => this.child(self.render_edit_ssh( - connection, - *server_index, - entries, - window, - cx, - )), - ViewServerOptionsState::Wsl { - connection, - entries, - server_index, - } => this.child(self.render_edit_wsl( - connection, - *server_index, - entries, - window, - cx, - )), - }) - .child(ListSeparator) - .child({ - div() - .id("ssh-options-copy-server-address") - .track_focus(&last_entry.focus_handle) - .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| { - this.mode = Mode::default_mode(&this.ssh_config_servers, cx); - cx.focus_self(window); - cx.notify(); - })) - .child( - ListItem::new("go-back") - .toggle_state( - last_entry.focus_handle.contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot( - Icon::new(IconName::ArrowLeft).color(Color::Muted), - ) - .child(Label::new("Go Back")) - .on_click(cx.listener(|this, _, window, cx| { - this.mode = - Mode::default_mode(&this.ssh_config_servers, cx); - cx.focus_self(window); - cx.notify() - })), - ) - }), - ) - .into_any_element(), - ); - - for entry in options.entries() { - view = view.entry(entry.clone()); - } - - view.render(window, cx).into_any_element() - } - - fn render_edit_wsl( - &self, - connection: &WslConnectionOptions, - index: WslServerIndex, - entries: &[NavigableEntry], - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let distro_name = SharedString::new(connection.distro_name.clone()); - - v_flex().child({ - fn remove_wsl_distro( - remote_servers: Entity, - index: WslServerIndex, - distro_name: SharedString, - window: &mut Window, - cx: &mut App, - ) { - let prompt_message = format!("Remove WSL distro `{}`?", distro_name); - - let confirmation = window.prompt( - PromptLevel::Warning, - &prompt_message, - None, - &["Yes, remove it", "No, keep it"], - cx, - ); - - cx.spawn(async move |cx| { - if confirmation.await.ok() == Some(0) { - remote_servers - .update(cx, |this, cx| { - this.delete_wsl_distro(index, cx); - }) - .ok(); - remote_servers - .update(cx, |this, cx| { - this.mode = Mode::default_mode(&this.ssh_config_servers, cx); - cx.notify(); - }) - .ok(); - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - div() - .id("wsl-options-remove-distro") - .track_focus(&entries[0].focus_handle) - .on_action(cx.listener({ - let distro_name = distro_name.clone(); - move |_, _: &menu::Confirm, window, cx| { - remove_wsl_distro(cx.entity(), index, distro_name.clone(), window, cx); - cx.focus_self(window); - } - })) - .child( - ListItem::new("remove-distro") - .toggle_state(entries[0].focus_handle.contains_focused(window, cx)) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Trash).color(Color::Error)) - .child(Label::new("Remove Distro").color(Color::Error)) - .on_click(cx.listener(move |_, _, window, cx| { - remove_wsl_distro(cx.entity(), index, distro_name.clone(), window, cx); - cx.focus_self(window); - })), - ) - }) - } - - fn render_edit_ssh( - &self, - connection: &SshConnectionOptions, - index: SshServerIndex, - entries: &[NavigableEntry], - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let connection_string = SharedString::new(connection.host.clone()); - - v_flex() - .child({ - let label = if connection.nickname.is_some() { - "Edit Nickname" - } else { - "Add Nickname to Server" - }; - div() - .id("ssh-options-add-nickname") - .track_focus(&entries[0].focus_handle) - .on_action(cx.listener(move |this, _: &menu::Confirm, window, cx| { - this.mode = Mode::EditNickname(EditNicknameState::new(index, window, cx)); - cx.notify(); - })) - .child( - ListItem::new("add-nickname") - .toggle_state(entries[0].focus_handle.contains_focused(window, cx)) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Pencil).color(Color::Muted)) - .child(Label::new(label)) - .on_click(cx.listener(move |this, _, window, cx| { - this.mode = - Mode::EditNickname(EditNicknameState::new(index, window, cx)); - cx.notify(); - })), - ) - }) - .child({ - let workspace = self.workspace.clone(); - fn callback( - workspace: WeakEntity, - connection_string: SharedString, - cx: &mut App, - ) { - cx.write_to_clipboard(ClipboardItem::new_string(connection_string.to_string())); - workspace - .update(cx, |this, cx| { - struct SshServerAddressCopiedToClipboard; - let notification = format!( - "Copied server address ({}) to clipboard", - connection_string - ); - - this.show_toast( - Toast::new( - NotificationId::composite::( - connection_string.clone(), - ), - notification, - ) - .autohide(), - cx, - ); - }) - .ok(); - } - div() - .id("ssh-options-copy-server-address") - .track_focus(&entries[1].focus_handle) - .on_action({ - let connection_string = connection_string.clone(); - let workspace = self.workspace.clone(); - move |_: &menu::Confirm, _, cx| { - callback(workspace.clone(), connection_string.clone(), cx); - } - }) - .child( - ListItem::new("copy-server-address") - .toggle_state(entries[1].focus_handle.contains_focused(window, cx)) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Copy).color(Color::Muted)) - .child(Label::new("Copy Server Address")) - .end_hover_slot( - Label::new(connection_string.clone()).color(Color::Muted), - ) - .on_click({ - let connection_string = connection_string.clone(); - move |_, _, cx| { - callback(workspace.clone(), connection_string.clone(), cx); - } - }), - ) - }) - .child({ - fn remove_ssh_server( - remote_servers: Entity, - index: SshServerIndex, - connection_string: SharedString, - window: &mut Window, - cx: &mut App, - ) { - let prompt_message = format!("Remove server `{}`?", connection_string); - - let confirmation = window.prompt( - PromptLevel::Warning, - &prompt_message, - None, - &["Yes, remove it", "No, keep it"], - cx, - ); - - cx.spawn(async move |cx| { - if confirmation.await.ok() == Some(0) { - remote_servers - .update(cx, |this, cx| { - this.delete_ssh_server(index, cx); - }) - .ok(); - remote_servers - .update(cx, |this, cx| { - this.mode = Mode::default_mode(&this.ssh_config_servers, cx); - cx.notify(); - }) - .ok(); - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - div() - .id("ssh-options-copy-server-address") - .track_focus(&entries[2].focus_handle) - .on_action(cx.listener({ - let connection_string = connection_string.clone(); - move |_, _: &menu::Confirm, window, cx| { - remove_ssh_server( - cx.entity(), - index, - connection_string.clone(), - window, - cx, - ); - cx.focus_self(window); - } - })) - .child( - ListItem::new("remove-server") - .toggle_state(entries[2].focus_handle.contains_focused(window, cx)) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Trash).color(Color::Error)) - .child(Label::new("Remove Server").color(Color::Error)) - .on_click(cx.listener(move |_, _, window, cx| { - remove_ssh_server( - cx.entity(), - index, - connection_string.clone(), - window, - cx, - ); - cx.focus_self(window); - })), - ) - }) - } - - fn render_edit_nickname( - &self, - state: &EditNicknameState, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let Some(connection) = SshSettings::get_global(cx) - .ssh_connections() - .nth(state.index.0) - else { - return v_flex() - .id("ssh-edit-nickname") - .track_focus(&self.focus_handle(cx)); - }; - - let connection_string = connection.host.clone(); - let nickname = connection.nickname.map(|s| s.into()); - - v_flex() - .id("ssh-edit-nickname") - .track_focus(&self.focus_handle(cx)) - .child( - SshConnectionHeader { - connection_string, - paths: Default::default(), - nickname, - is_wsl: false, - is_devcontainer: false, - } - .render(window, cx), - ) - .child( - h_flex() - .p_2() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child(state.editor.clone()), - ) - } - - fn render_default( - &mut self, - mut state: DefaultState, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let ssh_settings = SshSettings::get_global(cx); - let mut should_rebuild = false; - - let ssh_connections_changed = ssh_settings.ssh_connections.0.iter().ne(state - .servers - .iter() - .filter_map(|server| match server { - RemoteEntry::Project { - connection: Connection::Ssh(connection), - .. - } => Some(connection), - _ => None, - })); - - let wsl_connections_changed = ssh_settings.wsl_connections.0.iter().ne(state - .servers - .iter() - .filter_map(|server| match server { - RemoteEntry::Project { - connection: Connection::Wsl(connection), - .. - } => Some(connection), - _ => None, - })); - - if ssh_connections_changed || wsl_connections_changed { - should_rebuild = true; - }; - - if !should_rebuild && ssh_settings.read_ssh_config { - let current_ssh_hosts: BTreeSet = state - .servers - .iter() - .filter_map(|server| match server { - RemoteEntry::SshConfig { host, .. } => Some(host.clone()), - _ => None, - }) - .collect(); - let mut expected_ssh_hosts = self.ssh_config_servers.clone(); - for server in &state.servers { - if let RemoteEntry::Project { - connection: Connection::Ssh(connection), - .. - } = server - { - expected_ssh_hosts.remove(&connection.host); - } - } - should_rebuild = current_ssh_hosts != expected_ssh_hosts; - } - - if should_rebuild { - self.mode = Mode::default_mode(&self.ssh_config_servers, cx); - if let Mode::Default(new_state) = &self.mode { - state = new_state.clone(); - } - } - - let connect_button = div() - .id("ssh-connect-new-server-container") - .track_focus(&state.add_new_server.focus_handle) - .anchor_scroll(state.add_new_server.scroll_anchor.clone()) - .child( - ListItem::new("register-remote-server-button") - .toggle_state( - state - .add_new_server - .focus_handle - .contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) - .child(Label::new("Connect SSH Server")) - .on_click(cx.listener(|this, _, window, cx| { - let state = CreateRemoteServer::new(window, cx); - this.mode = Mode::CreateRemoteServer(state); - - cx.notify(); - })), - ) - .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| { - let state = CreateRemoteServer::new(window, cx); - this.mode = Mode::CreateRemoteServer(state); - - cx.notify(); - })); - - let connect_dev_container_button = div() - .id("connect-new-dev-container") - .track_focus(&state.add_new_devcontainer.focus_handle) - .anchor_scroll(state.add_new_devcontainer.scroll_anchor.clone()) - .child( - ListItem::new("register-dev-container-button") - .toggle_state( - state - .add_new_devcontainer - .focus_handle - .contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) - .child(Label::new("Connect Dev Container")) - .on_click(cx.listener(|this, _, window, cx| { - let state = CreateRemoteDevContainer::new(window, cx); - this.mode = Mode::CreateRemoteDevContainer(state); - - cx.notify(); - })), - ) - .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| { - let state = CreateRemoteDevContainer::new(window, cx); - this.mode = Mode::CreateRemoteDevContainer(state); - - cx.notify(); - })); - - #[cfg(target_os = "windows")] - let wsl_connect_button = div() - .id("wsl-connect-new-server") - .track_focus(&state.add_new_wsl.focus_handle) - .anchor_scroll(state.add_new_wsl.scroll_anchor.clone()) - .child( - ListItem::new("wsl-add-new-server") - .toggle_state(state.add_new_wsl.focus_handle.contains_focused(window, cx)) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) - .child(Label::new("Add WSL Distro")) - .on_click(cx.listener(|this, _, window, cx| { - let state = AddWslDistro::new(window, cx); - this.mode = Mode::AddWslDistro(state); - - cx.notify(); - })), - ) - .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| { - let state = AddWslDistro::new(window, cx); - this.mode = Mode::AddWslDistro(state); - - cx.notify(); - })); - - let has_open_project = self - .workspace - .upgrade() - .map(|workspace| { - workspace - .read(cx) - .project() - .read(cx) - .visible_worktrees(cx) - .next() - .is_some() - }) - .unwrap_or(false); - - let modal_section = v_flex() - .track_focus(&self.focus_handle(cx)) - .id("ssh-server-list") - .overflow_y_scroll() - .track_scroll(&state.scroll_handle) - .size_full() - .child(connect_button) - .when(has_open_project, |this| { - this.child(connect_dev_container_button) - }); - - #[cfg(target_os = "windows")] - let modal_section = modal_section.child(wsl_connect_button); - #[cfg(not(target_os = "windows"))] - let modal_section = modal_section; - - let mut modal_section = Navigable::new( - modal_section - .child( - List::new() - .empty_message( - h_flex() - .size_full() - .p_2() - .justify_center() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child( - Label::new("No remote servers registered yet.") - .color(Color::Muted), - ) - .into_any_element(), - ) - .children(state.servers.iter().enumerate().map(|(ix, connection)| { - self.render_remote_connection(ix, connection.clone(), window, cx) - .into_any_element() - })), - ) - .into_any_element(), - ) - .entry(state.add_new_server.clone()); - - if has_open_project { - modal_section = modal_section.entry(state.add_new_devcontainer.clone()); - } - - if cfg!(target_os = "windows") { - modal_section = modal_section.entry(state.add_new_wsl.clone()); - } - - for server in &state.servers { - match server { - RemoteEntry::Project { - open_folder, - projects, - configure, - .. - } => { - for (navigation_state, _) in projects { - modal_section = modal_section.entry(navigation_state.clone()); - } - modal_section = modal_section - .entry(open_folder.clone()) - .entry(configure.clone()); - } - RemoteEntry::SshConfig { open_folder, .. } => { - modal_section = modal_section.entry(open_folder.clone()); - } - } - } - let mut modal_section = modal_section.render(window, cx).into_any_element(); - - let (create_window, reuse_window) = if self.create_new_window { - ( - window.keystroke_text_for(&menu::Confirm), - window.keystroke_text_for(&menu::SecondaryConfirm), - ) - } else { - ( - window.keystroke_text_for(&menu::SecondaryConfirm), - window.keystroke_text_for(&menu::Confirm), - ) - }; - let placeholder_text = Arc::from(format!( - "{reuse_window} reuses this window, {create_window} opens a new one", - )); - - Modal::new("remote-projects", None) - .header( - ModalHeader::new() - .child(Headline::new("Remote Projects").size(HeadlineSize::XSmall)) - .child( - Label::new(placeholder_text) - .color(Color::Muted) - .size(LabelSize::XSmall), - ), - ) - .section( - Section::new().padded(false).child( - v_flex() - .min_h(rems(20.)) - .size_full() - .relative() - .child(ListSeparator) - .child( - canvas( - |bounds, window, cx| { - modal_section.prepaint_as_root( - bounds.origin, - bounds.size.into(), - window, - cx, - ); - modal_section - }, - |_, mut modal_section, window, cx| { - modal_section.paint(window, cx); - }, - ) - .size_full(), - ) - .vertical_scrollbar_for(&state.scroll_handle, window, cx), - ), - ) - .into_any_element() - } - - fn create_host_from_ssh_config( - &mut self, - ssh_config_host: &SharedString, - cx: &mut Context<'_, Self>, - ) -> SshServerIndex { - let new_ix = Arc::new(AtomicUsize::new(0)); - - let update_new_ix = new_ix.clone(); - self.update_settings_file(cx, move |settings, _| { - update_new_ix.store( - settings - .ssh_connections - .as_ref() - .map_or(0, |connections| connections.len()), - atomic::Ordering::Release, - ); - }); - - self.add_ssh_server( - SshConnectionOptions { - host: ssh_config_host.to_string(), - ..SshConnectionOptions::default() - }, - cx, - ); - self.mode = Mode::default_mode(&self.ssh_config_servers, cx); - SshServerIndex(new_ix.load(atomic::Ordering::Acquire)) - } -} - -fn spawn_ssh_config_watch(fs: Arc, cx: &Context) -> Task<()> { - let mut user_ssh_config_watcher = - watch_config_file(cx.background_executor(), fs.clone(), user_ssh_config_file()); - let mut global_ssh_config_watcher = global_ssh_config_file() - .map(|it| watch_config_file(cx.background_executor(), fs, it.to_owned())) - .unwrap_or_else(|| futures::channel::mpsc::unbounded().1); - - cx.spawn(async move |remote_server_projects, cx| { - let mut global_hosts = BTreeSet::default(); - let mut user_hosts = BTreeSet::default(); - let mut running_receivers = 2; - - loop { - select! { - new_global_file_contents = global_ssh_config_watcher.next().fuse() => { - match new_global_file_contents { - Some(new_global_file_contents) => { - global_hosts = parse_ssh_config_hosts(&new_global_file_contents); - if remote_server_projects.update(cx, |remote_server_projects, cx| { - remote_server_projects.ssh_config_servers = global_hosts.iter().chain(user_hosts.iter()).map(SharedString::from).collect(); - cx.notify(); - }).is_err() { - return; - } - }, - None => { - running_receivers -= 1; - if running_receivers == 0 { - return; - } - } - } - }, - new_user_file_contents = user_ssh_config_watcher.next().fuse() => { - match new_user_file_contents { - Some(new_user_file_contents) => { - user_hosts = parse_ssh_config_hosts(&new_user_file_contents); - if remote_server_projects.update(cx, |remote_server_projects, cx| { - remote_server_projects.ssh_config_servers = global_hosts.iter().chain(user_hosts.iter()).map(SharedString::from).collect(); - cx.notify(); - }).is_err() { - return; - } - }, - None => { - running_receivers -= 1; - if running_receivers == 0 { - return; - } - } - } - }, - } - } - }) -} - -fn get_text(element: &Entity, cx: &mut App) -> String { - element.read(cx).text(cx).trim().to_string() -} - -impl ModalView for RemoteServerProjects {} - -impl Focusable for RemoteServerProjects { - fn focus_handle(&self, cx: &App) -> FocusHandle { - match &self.mode { - Mode::ProjectPicker(picker) => picker.focus_handle(cx), - _ => self.focus_handle.clone(), - } - } -} - -impl EventEmitter for RemoteServerProjects {} - -impl Render for RemoteServerProjects { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .elevation_3(cx) - .w(rems(34.)) - .key_context("RemoteServerModal") - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(Self::confirm)) - .capture_any_mouse_down(cx.listener(|this, _, window, cx| { - this.focus_handle(cx).focus(window); - })) - .on_mouse_down_out(cx.listener(|this, _, _, cx| { - if matches!(this.mode, Mode::Default(_)) { - cx.emit(DismissEvent) - } - })) - .child(match &self.mode { - Mode::Default(state) => self - .render_default(state.clone(), window, cx) - .into_any_element(), - Mode::ViewServerOptions(state) => self - .render_view_options(state.clone(), window, cx) - .into_any_element(), - Mode::ProjectPicker(element) => element.clone().into_any_element(), - Mode::CreateRemoteServer(state) => self - .render_create_remote_server(state, window, cx) - .into_any_element(), - Mode::CreateRemoteDevContainer(state) => self - .render_create_dev_container(state, window, cx) - .into_any_element(), - Mode::EditNickname(state) => self - .render_edit_nickname(state, window, cx) - .into_any_element(), - #[cfg(target_os = "windows")] - Mode::AddWslDistro(state) => self - .render_add_wsl_distro(state, window, cx) - .into_any_element(), - }) - } -} diff --git a/crates/recent_projects/src/ssh_config.rs b/crates/recent_projects/src/ssh_config.rs deleted file mode 100644 index f381818205..0000000000 --- a/crates/recent_projects/src/ssh_config.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::collections::BTreeSet; - -pub fn parse_ssh_config_hosts(config: &str) -> BTreeSet { - let mut hosts = BTreeSet::new(); - let mut needs_another_line = false; - for line in config.lines() { - let line = line.trim_start(); - if let Some(line) = line.strip_prefix("Host") { - match line.chars().next() { - Some('\\') => { - needs_another_line = true; - } - Some('\n' | '\r') => { - needs_another_line = false; - } - Some(c) if c.is_whitespace() => { - parse_hosts_from(line, &mut hosts); - } - Some(_) | None => { - needs_another_line = false; - } - }; - - if needs_another_line { - parse_hosts_from(line, &mut hosts); - needs_another_line = line.trim_end().ends_with('\\'); - } else { - needs_another_line = false; - } - } else if needs_another_line { - needs_another_line = line.trim_end().ends_with('\\'); - parse_hosts_from(line, &mut hosts); - } else { - needs_another_line = false; - } - } - - hosts -} - -fn parse_hosts_from(line: &str, hosts: &mut BTreeSet) { - hosts.extend( - line.split_whitespace() - .filter(|field| !field.starts_with("!")) - .filter(|field| !field.contains("*")) - .filter(|field| !field.is_empty()) - .map(|field| field.to_owned()), - ); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_thank_you_bjorn3() { - let hosts = " - Host * - AddKeysToAgent yes - UseKeychain yes - IdentityFile ~/.ssh/id_ed25519 - - Host whatever.* - User another - - Host !not_this - User not_me - - Host something - HostName whatever.tld - - Host linux bsd host3 - User bjorn - - Host rpi - user rpi - hostname rpi.local - - Host \ - somehost \ - anotherhost - Hostname 192.168.3.3"; - - let expected_hosts = BTreeSet::from_iter([ - "something".to_owned(), - "linux".to_owned(), - "host3".to_owned(), - "bsd".to_owned(), - "rpi".to_owned(), - "somehost".to_owned(), - "anotherhost".to_owned(), - ]); - - assert_eq!(expected_hosts, parse_ssh_config_hosts(hosts)); - } -} diff --git a/crates/recent_projects/src/wsl_picker.rs b/crates/recent_projects/src/wsl_picker.rs deleted file mode 100644 index e386b723fa..0000000000 --- a/crates/recent_projects/src/wsl_picker.rs +++ /dev/null @@ -1,295 +0,0 @@ -use std::{path::PathBuf, sync::Arc}; - -use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Subscription, Task}; -use picker::Picker; -use remote::{RemoteConnectionOptions, WslConnectionOptions}; -use ui::{ - App, Context, HighlightedLabel, Icon, IconName, InteractiveElement, ListItem, ParentElement, - Render, Styled, StyledExt, Toggleable, Window, div, h_flex, rems, v_flex, -}; -use util::ResultExt as _; -use workspace::{ModalView, Workspace}; - -use crate::open_remote_project; - -#[derive(Clone, Debug)] -pub struct WslDistroSelected { - pub secondary: bool, - pub distro: String, -} - -#[derive(Clone, Debug)] -pub struct WslPickerDismissed; - -pub(crate) struct WslPickerDelegate { - selected_index: usize, - distro_list: Option>, - matches: Vec, -} - -impl WslPickerDelegate { - pub fn new() -> Self { - WslPickerDelegate { - selected_index: 0, - distro_list: None, - matches: Vec::new(), - } - } - - pub fn selected_distro(&self) -> Option { - self.matches - .get(self.selected_index) - .map(|m| m.string.clone()) - } -} - -impl WslPickerDelegate { - fn fetch_distros() -> anyhow::Result> { - use anyhow::Context; - use windows_registry::CURRENT_USER; - - let lxss_key = CURRENT_USER - .open("Software\\Microsoft\\Windows\\CurrentVersion\\Lxss") - .context("failed to get lxss wsl key")?; - - let distros = lxss_key - .keys() - .context("failed to get wsl distros")? - .filter_map(|key| { - lxss_key - .open(&key) - .context("failed to open subkey for distro") - .log_err() - }) - .filter_map(|distro| distro.get_string("DistributionName").ok()) - .collect::>(); - - Ok(distros) - } -} - -impl EventEmitter for Picker {} - -impl EventEmitter for Picker {} - -impl picker::PickerDelegate for WslPickerDelegate { - type ListItem = ListItem; - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _window: &mut Window, - cx: &mut Context>, - ) { - self.selected_index = ix; - cx.notify(); - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - Arc::from("Enter WSL distro name") - } - - fn update_matches( - &mut self, - query: String, - _window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - use fuzzy::StringMatchCandidate; - - let needs_fetch = self.distro_list.is_none(); - if needs_fetch { - let distros = Self::fetch_distros().log_err(); - self.distro_list = distros; - } - - if let Some(distro_list) = &self.distro_list { - use ordered_float::OrderedFloat; - - let candidates = distro_list - .iter() - .enumerate() - .map(|(id, distro)| StringMatchCandidate::new(id, distro)) - .collect::>(); - - let query = query.trim_start(); - let smart_case = query.chars().any(|c| c.is_uppercase()); - self.matches = smol::block_on(fuzzy::match_strings( - candidates.as_slice(), - query, - smart_case, - true, - 100, - &Default::default(), - cx.background_executor().clone(), - )); - self.matches.sort_unstable_by_key(|m| m.candidate_id); - - self.selected_index = self - .matches - .iter() - .enumerate() - .rev() - .max_by_key(|(_, m)| OrderedFloat(m.score)) - .map(|(index, _)| index) - .unwrap_or(0); - } - - Task::ready(()) - } - - fn confirm(&mut self, secondary: bool, _window: &mut Window, cx: &mut Context>) { - if let Some(distro) = self.matches.get(self.selected_index) { - cx.emit(WslDistroSelected { - secondary, - distro: distro.string.clone(), - }); - } - } - - fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { - cx.emit(WslPickerDismissed); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - _: &mut Context>, - ) -> Option { - let matched = self.matches.get(ix)?; - Some( - ListItem::new(ix) - .toggle_state(selected) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .child( - h_flex() - .flex_grow() - .gap_3() - .child(Icon::new(IconName::Linux)) - .child(v_flex().child(HighlightedLabel::new( - matched.string.clone(), - matched.positions.clone(), - ))), - ), - ) - } -} - -pub(crate) struct WslOpenModal { - paths: Vec, - create_new_window: bool, - picker: Entity>, - _subscriptions: [Subscription; 2], -} - -impl WslOpenModal { - pub fn new( - paths: Vec, - create_new_window: bool, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let delegate = WslPickerDelegate::new(); - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)); - - let selected = cx.subscribe_in( - &picker, - window, - |this, _, event: &WslDistroSelected, window, cx| { - this.confirm(&event.distro, event.secondary, window, cx); - }, - ); - - let dismissed = cx.subscribe_in( - &picker, - window, - |this, _, _: &WslPickerDismissed, window, cx| { - this.cancel(&menu::Cancel, window, cx); - }, - ); - - WslOpenModal { - paths, - create_new_window, - picker, - _subscriptions: [selected, dismissed], - } - } - - fn confirm( - &mut self, - distro: &str, - secondary: bool, - window: &mut Window, - cx: &mut Context, - ) { - let app_state = workspace::AppState::global(cx); - let Some(app_state) = app_state.upgrade() else { - return; - }; - - let connection_options = RemoteConnectionOptions::Wsl(WslConnectionOptions { - distro_name: distro.to_string(), - user: None, - }); - - let replace_current_window = match self.create_new_window { - true => secondary, - false => !secondary, - }; - let replace_window = match replace_current_window { - true => window.window_handle().downcast::(), - false => None, - }; - - let paths = self.paths.clone(); - let open_options = workspace::OpenOptions { - replace_window, - ..Default::default() - }; - - cx.emit(DismissEvent); - cx.spawn_in(window, async move |_, cx| { - open_remote_project(connection_options, paths, app_state, open_options, cx).await - }) - .detach(); - } - - fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { - cx.emit(DismissEvent); - } -} - -impl ModalView for WslOpenModal {} - -impl Focusable for WslOpenModal { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl EventEmitter for WslOpenModal {} - -impl Render for WslOpenModal { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl ui::IntoElement { - div() - .on_mouse_down_out(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))) - .on_action(cx.listener(Self::cancel)) - .elevation_3(cx) - .w(rems(34.)) - .flex_1() - .overflow_hidden() - .child(self.picker.clone()) - } -} diff --git a/crates/release_channel/Cargo.toml b/crates/release_channel/Cargo.toml deleted file mode 100644 index 54a8afff8c..0000000000 --- a/crates/release_channel/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "release_channel" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[dependencies] -gpui.workspace = true -semver.workspace = true diff --git a/crates/release_channel/LICENSE-GPL b/crates/release_channel/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/release_channel/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/release_channel/src/lib.rs b/crates/release_channel/src/lib.rs deleted file mode 100644 index 65201ccc46..0000000000 --- a/crates/release_channel/src/lib.rs +++ /dev/null @@ -1,232 +0,0 @@ -//! Provides constructs for the Zed app version and release channel. - -#![deny(missing_docs)] - -use std::{env, str::FromStr, sync::LazyLock}; - -use gpui::{App, Global}; -use semver::Version; - -/// stable | dev | nightly | preview -pub static RELEASE_CHANNEL_NAME: LazyLock = LazyLock::new(|| { - if cfg!(debug_assertions) { - env::var("ZED_RELEASE_CHANNEL") - .unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string()) - } else { - include_str!("../../zed/RELEASE_CHANNEL").trim().to_string() - } -}); - -#[doc(hidden)] -pub static RELEASE_CHANNEL: LazyLock = - LazyLock::new(|| match ReleaseChannel::from_str(&RELEASE_CHANNEL_NAME) { - Ok(channel) => channel, - _ => panic!("invalid release channel {}", *RELEASE_CHANNEL_NAME), - }); - -/// The app identifier for the current release channel, Windows only. -#[cfg(target_os = "windows")] -pub fn app_identifier() -> &'static str { - match *RELEASE_CHANNEL { - ReleaseChannel::Dev => "Zed-Editor-Dev", - ReleaseChannel::Nightly => "Zed-Editor-Nightly", - ReleaseChannel::Preview => "Zed-Editor-Preview", - ReleaseChannel::Stable => "Zed-Editor-Stable", - } -} - -/// The Git commit SHA that Zed was built at. -#[derive(Clone, Eq, Debug, PartialEq)] -pub struct AppCommitSha(String); - -struct GlobalAppCommitSha(AppCommitSha); - -impl Global for GlobalAppCommitSha {} - -impl AppCommitSha { - /// Creates a new [`AppCommitSha`]. - pub fn new(sha: String) -> Self { - AppCommitSha(sha) - } - - /// Returns the global [`AppCommitSha`], if one is set. - pub fn try_global(cx: &App) -> Option { - cx.try_global::() - .map(|sha| sha.0.clone()) - } - - /// Sets the global [`AppCommitSha`]. - pub fn set_global(sha: AppCommitSha, cx: &mut App) { - cx.set_global(GlobalAppCommitSha(sha)) - } - - /// Returns the full commit SHA. - pub fn full(&self) -> String { - self.0.to_string() - } - - /// Returns the short (7 character) commit SHA. - pub fn short(&self) -> String { - self.0.chars().take(7).collect() - } -} - -struct GlobalAppVersion(Version); - -impl Global for GlobalAppVersion {} - -/// The version of Zed. -pub struct AppVersion; - -impl AppVersion { - /// Load the app version from env. - pub fn load( - pkg_version: &str, - build_id: Option<&str>, - commit_sha: Option, - ) -> Version { - let mut version: Version = if let Ok(from_env) = env::var("ZED_APP_VERSION") { - from_env.parse().expect("invalid ZED_APP_VERSION") - } else { - pkg_version.parse().expect("invalid version in Cargo.toml") - }; - let mut pre = String::from(RELEASE_CHANNEL.dev_name()); - - if let Some(build_id) = build_id { - pre.push('.'); - pre.push_str(&build_id); - } - - if let Some(sha) = commit_sha { - pre.push('.'); - pre.push_str(&sha.0); - } - if let Ok(build) = semver::BuildMetadata::new(&pre) { - version.build = build; - } - - version - } - - /// Returns the global version number. - pub fn global(cx: &App) -> Version { - if cx.has_global::() { - cx.global::().0.clone() - } else { - Version::new(0, 0, 0) - } - } -} - -/// A Zed release channel. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] -pub enum ReleaseChannel { - /// The development release channel. - /// - /// Used for local debug builds of Zed. - #[default] - Dev, - - /// The Nightly release channel. - Nightly, - - /// The Preview release channel. - Preview, - - /// The Stable release channel. - Stable, -} - -struct GlobalReleaseChannel(ReleaseChannel); - -impl Global for GlobalReleaseChannel {} - -/// Initializes the release channel. -pub fn init(app_version: Version, cx: &mut App) { - cx.set_global(GlobalAppVersion(app_version)); - cx.set_global(GlobalReleaseChannel(*RELEASE_CHANNEL)) -} - -/// Initializes the release channel for tests that rely on fake release channel. -pub fn init_test(app_version: Version, release_channel: ReleaseChannel, cx: &mut App) { - cx.set_global(GlobalAppVersion(app_version)); - cx.set_global(GlobalReleaseChannel(release_channel)) -} - -impl ReleaseChannel { - /// Returns the global [`ReleaseChannel`]. - pub fn global(cx: &App) -> Self { - cx.global::().0 - } - - /// Returns the global [`ReleaseChannel`], if one is set. - pub fn try_global(cx: &App) -> Option { - cx.try_global::() - .map(|channel| channel.0) - } - - /// Returns whether we want to poll for updates for this [`ReleaseChannel`] - pub fn poll_for_updates(&self) -> bool { - !matches!(self, ReleaseChannel::Dev) - } - - /// Returns the display name for this [`ReleaseChannel`]. - pub fn display_name(&self) -> &'static str { - match self { - ReleaseChannel::Dev => "Zed Dev", - ReleaseChannel::Nightly => "Zed Nightly", - ReleaseChannel::Preview => "Zed Preview", - ReleaseChannel::Stable => "Zed", - } - } - - /// Returns the programmatic name for this [`ReleaseChannel`]. - pub fn dev_name(&self) -> &'static str { - match self { - ReleaseChannel::Dev => "dev", - ReleaseChannel::Nightly => "nightly", - ReleaseChannel::Preview => "preview", - ReleaseChannel::Stable => "stable", - } - } - - /// Returns the application ID that's used by Wayland as application ID - /// and WM_CLASS on X11. - /// This also has to match the bundle identifier for Zed on macOS. - pub fn app_id(&self) -> &'static str { - match self { - ReleaseChannel::Dev => "dev.zed.Zed-Dev", - ReleaseChannel::Nightly => "dev.zed.Zed-Nightly", - ReleaseChannel::Preview => "dev.zed.Zed-Preview", - ReleaseChannel::Stable => "dev.zed.Zed", - } - } - - /// Returns the query parameter for this [`ReleaseChannel`]. - pub fn release_query_param(&self) -> Option<&'static str> { - match self { - Self::Dev => None, - Self::Nightly => Some("nightly=1"), - Self::Preview => Some("preview=1"), - Self::Stable => None, - } - } -} - -/// Error indicating that release channel string does not match any known release channel names. -#[derive(Copy, Clone, Debug, Hash, PartialEq)] -pub struct InvalidReleaseChannel; - -impl FromStr for ReleaseChannel { - type Err = InvalidReleaseChannel; - - fn from_str(channel: &str) -> Result { - Ok(match channel { - "dev" => ReleaseChannel::Dev, - "nightly" => ReleaseChannel::Nightly, - "preview" => ReleaseChannel::Preview, - "stable" => ReleaseChannel::Stable, - _ => return Err(InvalidReleaseChannel), - }) - } -} diff --git a/crates/remote/Cargo.toml b/crates/remote/Cargo.toml deleted file mode 100644 index ae32cd5cb1..0000000000 --- a/crates/remote/Cargo.toml +++ /dev/null @@ -1,48 +0,0 @@ -[package] -name = "remote" -description = "Client-side subsystem for remote editing" -edition.workspace = true -version = "0.1.0" -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/remote.rs" -doctest = false - -[features] -default = [] -test-support = ["fs/test-support"] - -[dependencies] -anyhow.workspace = true -askpass.workspace = true -async-trait.workspace = true -collections.workspace = true -fs.workspace = true -futures.workspace = true -gpui.workspace = true -log.workspace = true -parking_lot.workspace = true -paths.workspace = true -prost.workspace = true -release_channel.workspace = true -rpc = { workspace = true, features = ["gpui"] } -schemars.workspace = true -semver.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smol.workspace = true -tempfile.workspace = true -thiserror.workspace = true -urlencoding.workspace = true -util.workspace = true -which.workspace = true - -[dev-dependencies] -gpui = { workspace = true, features = ["test-support"] } -fs = { workspace = true, features = ["test-support"] } diff --git a/crates/remote/LICENSE-GPL b/crates/remote/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/remote/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/remote/src/json_log.rs b/crates/remote/src/json_log.rs deleted file mode 100644 index 96cebbb355..0000000000 --- a/crates/remote/src/json_log.rs +++ /dev/null @@ -1,59 +0,0 @@ -use log::{Level, Log, Record}; -use serde::{Deserialize, Serialize}; - -#[derive(Deserialize, Debug, Serialize)] -pub struct LogRecord<'a> { - pub level: usize, - pub module_path: Option<&'a str>, - pub file: Option<&'a str>, - pub line: Option, - pub message: String, -} - -impl<'a> LogRecord<'a> { - pub fn new(record: &'a Record<'a>) -> Self { - Self { - level: serialize_level(record.level()), - module_path: record.module_path(), - file: record.file(), - line: record.line(), - message: record.args().to_string(), - } - } - - pub fn log(&'a self, logger: &dyn Log) { - if let Some(level) = deserialize_level(self.level) { - logger.log( - &log::Record::builder() - .module_path(self.module_path) - .target("remote_server") - .args(format_args!("{}", self.message)) - .file(self.file) - .line(self.line) - .level(level) - .build(), - ) - } - } -} - -fn serialize_level(level: Level) -> usize { - match level { - Level::Error => 1, - Level::Warn => 2, - Level::Info => 3, - Level::Debug => 4, - Level::Trace => 5, - } -} - -fn deserialize_level(level: usize) -> Option { - match level { - 1 => Some(Level::Error), - 2 => Some(Level::Warn), - 3 => Some(Level::Info), - 4 => Some(Level::Debug), - 5 => Some(Level::Trace), - _ => None, - } -} diff --git a/crates/remote/src/protocol.rs b/crates/remote/src/protocol.rs deleted file mode 100644 index 867a31b164..0000000000 --- a/crates/remote/src/protocol.rs +++ /dev/null @@ -1,76 +0,0 @@ -use anyhow::Result; -use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use prost::Message as _; -use rpc::proto::Envelope; - -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] -pub struct MessageId(pub u32); - -pub type MessageLen = u32; -pub const MESSAGE_LEN_SIZE: usize = size_of::(); - -pub fn message_len_from_buffer(buffer: &[u8]) -> MessageLen { - MessageLen::from_le_bytes(buffer.try_into().unwrap()) -} - -pub async fn read_message_with_len( - stream: &mut S, - buffer: &mut Vec, - message_len: MessageLen, -) -> Result { - buffer.resize(message_len as usize, 0); - stream.read_exact(buffer).await?; - Ok(Envelope::decode(buffer.as_slice())?) -} - -pub async fn read_message( - stream: &mut S, - buffer: &mut Vec, -) -> Result { - buffer.resize(MESSAGE_LEN_SIZE, 0); - stream.read_exact(buffer).await?; - - let len = message_len_from_buffer(buffer); - - read_message_with_len(stream, buffer, len).await -} - -pub async fn write_message( - stream: &mut S, - buffer: &mut Vec, - message: Envelope, -) -> Result<()> { - let message_len = message.encoded_len() as u32; - stream - .write_all(message_len.to_le_bytes().as_slice()) - .await?; - buffer.clear(); - buffer.reserve(message_len as usize); - message.encode(buffer)?; - stream.write_all(buffer).await?; - Ok(()) -} - -pub async fn write_size_prefixed_buffer( - stream: &mut S, - buffer: &mut Vec, -) -> Result<()> { - let len = buffer.len() as u32; - stream.write_all(len.to_le_bytes().as_slice()).await?; - stream.write_all(buffer).await?; - Ok(()) -} - -pub async fn read_message_raw( - stream: &mut S, - buffer: &mut Vec, -) -> Result<()> { - buffer.resize(MESSAGE_LEN_SIZE, 0); - stream.read_exact(buffer).await?; - - let message_len = message_len_from_buffer(buffer); - buffer.resize(message_len as usize, 0); - stream.read_exact(buffer).await?; - - Ok(()) -} diff --git a/crates/remote/src/proxy.rs b/crates/remote/src/proxy.rs deleted file mode 100644 index d715d5ecf6..0000000000 --- a/crates/remote/src/proxy.rs +++ /dev/null @@ -1,25 +0,0 @@ -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum ProxyLaunchError { - #[error("Attempted reconnect, but server not running.")] - ServerNotRunning, -} - -impl ProxyLaunchError { - pub fn to_exit_code(&self) -> i32 { - match self { - // We're using 90 as the exit code, because 0-78 are often taken - // by shells and other conventions and >128 also has certain meanings - // in certain contexts. - Self::ServerNotRunning => 90, - } - } - - pub fn from_exit_code(exit_code: i32) -> Option { - match exit_code { - 90 => Some(Self::ServerNotRunning), - _ => None, - } - } -} diff --git a/crates/remote/src/remote.rs b/crates/remote/src/remote.rs deleted file mode 100644 index 51b71c988a..0000000000 --- a/crates/remote/src/remote.rs +++ /dev/null @@ -1,15 +0,0 @@ -pub mod json_log; -pub mod protocol; -pub mod proxy; -pub mod remote_client; -mod transport; - -#[cfg(target_os = "windows")] -pub use remote_client::OpenWslPath; -pub use remote_client::{ - ConnectionIdentifier, ConnectionState, RemoteClient, RemoteClientDelegate, RemoteClientEvent, - RemoteConnection, RemoteConnectionOptions, RemotePlatform, connect, -}; -pub use transport::docker::DockerConnectionOptions; -pub use transport::ssh::{SshConnectionOptions, SshPortForwardOption}; -pub use transport::wsl::WslConnectionOptions; diff --git a/crates/remote/src/remote_client.rs b/crates/remote/src/remote_client.rs deleted file mode 100644 index e8fa4fe4a3..0000000000 --- a/crates/remote/src/remote_client.rs +++ /dev/null @@ -1,1714 +0,0 @@ -use crate::{ - SshConnectionOptions, - protocol::MessageId, - proxy::ProxyLaunchError, - transport::{ - docker::{DockerConnectionOptions, DockerExecConnection}, - ssh::SshRemoteConnection, - wsl::{WslConnectionOptions, WslRemoteConnection}, - }, -}; -use anyhow::{Context as _, Result, anyhow}; -use askpass::EncryptedPassword; -use async_trait::async_trait; -use collections::HashMap; -use futures::{ - Future, FutureExt as _, StreamExt as _, - channel::{ - mpsc::{self, Sender, UnboundedReceiver, UnboundedSender}, - oneshot, - }, - future::{BoxFuture, Shared}, - select, select_biased, -}; -use gpui::{ - App, AppContext as _, AsyncApp, BackgroundExecutor, BorrowAppContext, Context, Entity, - EventEmitter, FutureExt, Global, Task, WeakEntity, -}; -use parking_lot::Mutex; - -use release_channel::ReleaseChannel; -use rpc::{ - AnyProtoClient, ErrorExt, ProtoClient, ProtoMessageHandlerSet, RpcError, - proto::{self, Envelope, EnvelopedMessage, PeerId, RequestMessage, build_typed_envelope}, -}; -use semver::Version; -use std::{ - collections::VecDeque, - fmt, - ops::ControlFlow, - path::PathBuf, - sync::{ - Arc, Weak, - atomic::{AtomicU32, AtomicU64, Ordering::SeqCst}, - }, - time::{Duration, Instant}, -}; -use util::{ - ResultExt, - paths::{PathStyle, RemotePathBuf}, -}; - -#[derive(Copy, Clone, Debug)] -pub struct RemotePlatform { - pub os: &'static str, - pub arch: &'static str, -} - -#[derive(Clone, Debug)] -pub struct CommandTemplate { - pub program: String, - pub args: Vec, - pub env: HashMap, -} - -pub trait RemoteClientDelegate: Send + Sync { - fn ask_password( - &self, - prompt: String, - tx: oneshot::Sender, - cx: &mut AsyncApp, - ); - fn get_download_url( - &self, - platform: RemotePlatform, - release_channel: ReleaseChannel, - version: Option, - cx: &mut AsyncApp, - ) -> Task>>; - fn download_server_binary_locally( - &self, - platform: RemotePlatform, - release_channel: ReleaseChannel, - version: Option, - cx: &mut AsyncApp, - ) -> Task>; - fn set_status(&self, status: Option<&str>, cx: &mut AsyncApp); -} - -const MAX_MISSED_HEARTBEATS: usize = 5; -const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); -const HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(5); -const INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_secs(60); - -const MAX_RECONNECT_ATTEMPTS: usize = 3; - -enum State { - Connecting, - Connected { - remote_connection: Arc, - delegate: Arc, - - multiplex_task: Task>, - heartbeat_task: Task>, - }, - HeartbeatMissed { - missed_heartbeats: usize, - - ssh_connection: Arc, - delegate: Arc, - - multiplex_task: Task>, - heartbeat_task: Task>, - }, - Reconnecting, - ReconnectFailed { - ssh_connection: Arc, - delegate: Arc, - - error: anyhow::Error, - attempts: usize, - }, - ReconnectExhausted, - ServerNotRunning, -} - -impl fmt::Display for State { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Connecting => write!(f, "connecting"), - Self::Connected { .. } => write!(f, "connected"), - Self::Reconnecting => write!(f, "reconnecting"), - Self::ReconnectFailed { .. } => write!(f, "reconnect failed"), - Self::ReconnectExhausted => write!(f, "reconnect exhausted"), - Self::HeartbeatMissed { .. } => write!(f, "heartbeat missed"), - Self::ServerNotRunning { .. } => write!(f, "server not running"), - } - } -} - -impl State { - fn remote_connection(&self) -> Option> { - match self { - Self::Connected { - remote_connection: ssh_connection, - .. - } => Some(ssh_connection.clone()), - Self::HeartbeatMissed { ssh_connection, .. } => Some(ssh_connection.clone()), - Self::ReconnectFailed { ssh_connection, .. } => Some(ssh_connection.clone()), - _ => None, - } - } - - fn can_reconnect(&self) -> bool { - match self { - Self::Connected { .. } - | Self::HeartbeatMissed { .. } - | Self::ReconnectFailed { .. } => true, - State::Connecting - | State::Reconnecting - | State::ReconnectExhausted - | State::ServerNotRunning => false, - } - } - - fn is_reconnect_failed(&self) -> bool { - matches!(self, Self::ReconnectFailed { .. }) - } - - fn is_reconnect_exhausted(&self) -> bool { - matches!(self, Self::ReconnectExhausted { .. }) - } - - fn is_server_not_running(&self) -> bool { - matches!(self, Self::ServerNotRunning) - } - - fn is_reconnecting(&self) -> bool { - matches!(self, Self::Reconnecting { .. }) - } - - fn heartbeat_recovered(self) -> Self { - match self { - Self::HeartbeatMissed { - ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - .. - } => Self::Connected { - remote_connection: ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - }, - _ => self, - } - } - - fn heartbeat_missed(self) -> Self { - match self { - Self::Connected { - remote_connection: ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - } => Self::HeartbeatMissed { - missed_heartbeats: 1, - ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - }, - Self::HeartbeatMissed { - missed_heartbeats, - ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - } => Self::HeartbeatMissed { - missed_heartbeats: missed_heartbeats + 1, - ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - }, - _ => self, - } - } -} - -/// The state of the ssh connection. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ConnectionState { - Connecting, - Connected, - HeartbeatMissed, - Reconnecting, - Disconnected, -} - -impl From<&State> for ConnectionState { - fn from(value: &State) -> Self { - match value { - State::Connecting => Self::Connecting, - State::Connected { .. } => Self::Connected, - State::Reconnecting | State::ReconnectFailed { .. } => Self::Reconnecting, - State::HeartbeatMissed { .. } => Self::HeartbeatMissed, - State::ReconnectExhausted => Self::Disconnected, - State::ServerNotRunning => Self::Disconnected, - } - } -} - -pub struct RemoteClient { - client: Arc, - unique_identifier: String, - connection_options: RemoteConnectionOptions, - path_style: PathStyle, - state: Option, -} - -#[derive(Debug)] -pub enum RemoteClientEvent { - Disconnected, -} - -impl EventEmitter for RemoteClient {} - -/// Identifies the socket on the remote server so that reconnects -/// can re-join the same project. -pub enum ConnectionIdentifier { - Setup(u64), - Workspace(i64), -} - -static NEXT_ID: AtomicU64 = AtomicU64::new(1); - -impl ConnectionIdentifier { - pub fn setup() -> Self { - Self::Setup(NEXT_ID.fetch_add(1, SeqCst)) - } - - // This string gets used in a socket name, and so must be relatively short. - // The total length of: - // /home/{username}/.local/share/zed/server_state/{name}/stdout.sock - // Must be less than about 100 characters - // https://unix.stackexchange.com/questions/367008/why-is-socket-path-length-limited-to-a-hundred-chars - // So our strings should be at most 20 characters or so. - fn to_string(&self, cx: &App) -> String { - let identifier_prefix = match ReleaseChannel::global(cx) { - ReleaseChannel::Stable => "".to_string(), - release_channel => format!("{}-", release_channel.dev_name()), - }; - match self { - Self::Setup(setup_id) => format!("{identifier_prefix}setup-{setup_id}"), - Self::Workspace(workspace_id) => { - format!("{identifier_prefix}workspace-{workspace_id}",) - } - } - } -} - -pub async fn connect( - connection_options: RemoteConnectionOptions, - delegate: Arc, - cx: &mut AsyncApp, -) -> Result> { - cx.update(|cx| { - cx.update_default_global(|pool: &mut ConnectionPool, cx| { - pool.connect(connection_options.clone(), delegate.clone(), cx) - }) - })? - .await - .map_err(|e| e.cloned()) -} - -impl RemoteClient { - pub fn new( - unique_identifier: ConnectionIdentifier, - remote_connection: Arc, - cancellation: oneshot::Receiver<()>, - delegate: Arc, - cx: &mut App, - ) -> Task>>> { - let unique_identifier = unique_identifier.to_string(cx); - cx.spawn(async move |cx| { - let success = Box::pin(async move { - let (outgoing_tx, outgoing_rx) = mpsc::unbounded::(); - let (incoming_tx, incoming_rx) = mpsc::unbounded::(); - let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1); - - let client = cx.update(|cx| { - ChannelClient::new( - incoming_rx, - outgoing_tx, - cx, - "client", - remote_connection.has_wsl_interop(), - ) - })?; - - let path_style = remote_connection.path_style(); - let this = cx.new(|_| Self { - client: client.clone(), - unique_identifier: unique_identifier.clone(), - connection_options: remote_connection.connection_options(), - path_style, - state: Some(State::Connecting), - })?; - - let io_task = remote_connection.start_proxy( - unique_identifier, - false, - incoming_tx, - outgoing_rx, - connection_activity_tx, - delegate.clone(), - cx, - ); - - let ready = client - .wait_for_remote_started() - .with_timeout(INITIAL_CONNECTION_TIMEOUT, cx.background_executor()) - .await; - match ready { - Ok(Some(_)) => {} - Ok(None) => { - let mut error = "remote client exited before becoming ready".to_owned(); - if let Some(status) = io_task.now_or_never() { - match status { - Ok(exit_code) => { - error.push_str(&format!(", exit_code={exit_code:?}")) - } - Err(e) => error.push_str(&format!(", error={e:?}")), - } - } - let error = anyhow::anyhow!("{error}"); - log::error!("failed to establish connection: {}", error); - return Err(error); - } - Err(_) => { - let mut error = - "remote client did not become ready within the timeout".to_owned(); - if let Some(status) = io_task.now_or_never() { - match status { - Ok(exit_code) => { - error.push_str(&format!(", exit_code={exit_code:?}")) - } - Err(e) => error.push_str(&format!(", error={e:?}")), - } - } - let error = anyhow::anyhow!("{error}"); - log::error!("failed to establish connection: {}", error); - return Err(error); - } - } - let multiplex_task = Self::monitor(this.downgrade(), io_task, cx); - if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await { - log::error!("failed to establish connection: {}", error); - return Err(error); - } - - let heartbeat_task = Self::heartbeat(this.downgrade(), connection_activity_rx, cx); - - this.update(cx, |this, _| { - this.state = Some(State::Connected { - remote_connection, - delegate, - multiplex_task, - heartbeat_task, - }); - })?; - - Ok(Some(this)) - }); - - select! { - _ = cancellation.fuse() => { - Ok(None) - } - result = success.fuse() => result - } - }) - } - - pub fn proto_client_from_channels( - incoming_rx: mpsc::UnboundedReceiver, - outgoing_tx: mpsc::UnboundedSender, - cx: &App, - name: &'static str, - has_wsl_interop: bool, - ) -> AnyProtoClient { - ChannelClient::new(incoming_rx, outgoing_tx, cx, name, has_wsl_interop).into() - } - - pub fn shutdown_processes( - &mut self, - shutdown_request: Option, - executor: BackgroundExecutor, - ) -> Option + use> { - let state = self.state.take()?; - log::info!("shutting down ssh processes"); - - let State::Connected { - multiplex_task, - heartbeat_task, - remote_connection: ssh_connection, - delegate, - } = state - else { - return None; - }; - - let client = self.client.clone(); - - Some(async move { - if let Some(shutdown_request) = shutdown_request { - client.send(shutdown_request).log_err(); - // We wait 50ms instead of waiting for a response, because - // waiting for a response would require us to wait on the main thread - // which we want to avoid in an `on_app_quit` callback. - executor.timer(Duration::from_millis(50)).await; - } - - // Drop `multiplex_task` because it owns our ssh_proxy_process, which is a - // child of master_process. - drop(multiplex_task); - // Now drop the rest of state, which kills master process. - drop(heartbeat_task); - drop(ssh_connection); - drop(delegate); - }) - } - - fn reconnect(&mut self, cx: &mut Context) -> Result<()> { - let can_reconnect = self - .state - .as_ref() - .map(|state| state.can_reconnect()) - .unwrap_or(false); - if !can_reconnect { - log::info!("aborting reconnect, because not in state that allows reconnecting"); - let error = if let Some(state) = self.state.as_ref() { - format!("invalid state, cannot reconnect while in state {state}") - } else { - "no state set".to_string() - }; - anyhow::bail!(error); - } - - let state = self.state.take().unwrap(); - let (attempts, remote_connection, delegate) = match state { - State::Connected { - remote_connection: ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - } - | State::HeartbeatMissed { - ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - .. - } => { - drop(multiplex_task); - drop(heartbeat_task); - (0, ssh_connection, delegate) - } - State::ReconnectFailed { - attempts, - ssh_connection, - delegate, - .. - } => (attempts, ssh_connection, delegate), - State::Connecting - | State::Reconnecting - | State::ReconnectExhausted - | State::ServerNotRunning => unreachable!(), - }; - - let attempts = attempts + 1; - if attempts > MAX_RECONNECT_ATTEMPTS { - log::error!( - "Failed to reconnect to after {} attempts, giving up", - MAX_RECONNECT_ATTEMPTS - ); - self.set_state(State::ReconnectExhausted, cx); - return Ok(()); - } - - self.set_state(State::Reconnecting, cx); - - log::info!("Trying to reconnect to ssh server... Attempt {}", attempts); - - let unique_identifier = self.unique_identifier.clone(); - let client = self.client.clone(); - let reconnect_task = cx.spawn(async move |this, cx| { - macro_rules! failed { - ($error:expr, $attempts:expr, $ssh_connection:expr, $delegate:expr) => { - delegate.set_status(Some(&format!("{error:#}", error = $error)), cx); - return State::ReconnectFailed { - error: anyhow!($error), - attempts: $attempts, - ssh_connection: $ssh_connection, - delegate: $delegate, - }; - }; - } - - if let Err(error) = remote_connection - .kill() - .await - .context("Failed to kill ssh process") - { - failed!(error, attempts, remote_connection, delegate); - }; - - let connection_options = remote_connection.connection_options(); - - let (outgoing_tx, outgoing_rx) = mpsc::unbounded::(); - let (incoming_tx, incoming_rx) = mpsc::unbounded::(); - let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1); - - let (ssh_connection, io_task) = match async { - let ssh_connection = cx - .update_global(|pool: &mut ConnectionPool, cx| { - pool.connect(connection_options, delegate.clone(), cx) - })? - .await - .map_err(|error| error.cloned())?; - - let io_task = ssh_connection.start_proxy( - unique_identifier, - true, - incoming_tx, - outgoing_rx, - connection_activity_tx, - delegate.clone(), - cx, - ); - anyhow::Ok((ssh_connection, io_task)) - } - .await - { - Ok((ssh_connection, io_task)) => (ssh_connection, io_task), - Err(error) => { - failed!(error, attempts, remote_connection, delegate); - } - }; - - let multiplex_task = Self::monitor(this.clone(), io_task, cx); - client.reconnect(incoming_rx, outgoing_tx, cx); - - if let Err(error) = client.resync(HEARTBEAT_TIMEOUT).await { - failed!(error, attempts, ssh_connection, delegate); - }; - - State::Connected { - remote_connection: ssh_connection, - delegate, - multiplex_task, - heartbeat_task: Self::heartbeat(this.clone(), connection_activity_rx, cx), - } - }); - - cx.spawn(async move |this, cx| { - let new_state = reconnect_task.await; - this.update(cx, |this, cx| { - this.try_set_state(cx, |old_state| { - if old_state.is_reconnecting() { - match &new_state { - State::Connecting - | State::Reconnecting - | State::HeartbeatMissed { .. } - | State::ServerNotRunning => {} - State::Connected { .. } => { - log::info!("Successfully reconnected"); - } - State::ReconnectFailed { - error, attempts, .. - } => { - log::error!( - "Reconnect attempt {} failed: {:?}. Starting new attempt...", - attempts, - error - ); - } - State::ReconnectExhausted => { - log::error!("Reconnect attempt failed and all attempts exhausted"); - } - } - Some(new_state) - } else { - None - } - }); - - if this.state_is(State::is_reconnect_failed) { - this.reconnect(cx) - } else if this.state_is(State::is_reconnect_exhausted) { - Ok(()) - } else { - log::debug!("State has transition from Reconnecting into new state while attempting reconnect."); - Ok(()) - } - }) - }) - .detach_and_log_err(cx); - - Ok(()) - } - - fn heartbeat( - this: WeakEntity, - mut connection_activity_rx: mpsc::Receiver<()>, - cx: &mut AsyncApp, - ) -> Task> { - let Ok(client) = this.read_with(cx, |this, _| this.client.clone()) else { - return Task::ready(Err(anyhow!("SshRemoteClient lost"))); - }; - - cx.spawn(async move |cx| { - let mut missed_heartbeats = 0; - - let keepalive_timer = cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse(); - futures::pin_mut!(keepalive_timer); - - loop { - select_biased! { - result = connection_activity_rx.next().fuse() => { - if result.is_none() { - log::warn!("ssh heartbeat: connection activity channel has been dropped. stopping."); - return Ok(()); - } - - if missed_heartbeats != 0 { - missed_heartbeats = 0; - let _ =this.update(cx, |this, cx| { - this.handle_heartbeat_result(missed_heartbeats, cx) - })?; - } - } - _ = keepalive_timer => { - log::debug!("Sending heartbeat to server..."); - - let result = select_biased! { - _ = connection_activity_rx.next().fuse() => { - Ok(()) - } - ping_result = client.ping(HEARTBEAT_TIMEOUT).fuse() => { - ping_result - } - }; - - if result.is_err() { - missed_heartbeats += 1; - log::warn!( - "No heartbeat from server after {:?}. Missed heartbeat {} out of {}.", - HEARTBEAT_TIMEOUT, - missed_heartbeats, - MAX_MISSED_HEARTBEATS - ); - } else if missed_heartbeats != 0 { - missed_heartbeats = 0; - } else { - continue; - } - - let result = this.update(cx, |this, cx| { - this.handle_heartbeat_result(missed_heartbeats, cx) - })?; - if result.is_break() { - return Ok(()); - } - } - } - - keepalive_timer.set(cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse()); - } - }) - } - - fn handle_heartbeat_result( - &mut self, - missed_heartbeats: usize, - cx: &mut Context, - ) -> ControlFlow<()> { - let state = self.state.take().unwrap(); - let next_state = if missed_heartbeats > 0 { - state.heartbeat_missed() - } else { - state.heartbeat_recovered() - }; - - self.set_state(next_state, cx); - - if missed_heartbeats >= MAX_MISSED_HEARTBEATS { - log::error!( - "Missed last {} heartbeats. Reconnecting...", - missed_heartbeats - ); - - self.reconnect(cx) - .context("failed to start reconnect process after missing heartbeats") - .log_err(); - ControlFlow::Break(()) - } else { - ControlFlow::Continue(()) - } - } - - fn monitor( - this: WeakEntity, - io_task: Task>, - cx: &AsyncApp, - ) -> Task> { - cx.spawn(async move |cx| { - let result = io_task.await; - - match result { - Ok(exit_code) => { - if let Some(error) = ProxyLaunchError::from_exit_code(exit_code) { - match error { - ProxyLaunchError::ServerNotRunning => { - log::error!("failed to reconnect because server is not running"); - this.update(cx, |this, cx| { - this.set_state(State::ServerNotRunning, cx); - })?; - } - } - } else if exit_code > 0 { - log::error!("proxy process terminated unexpectedly"); - this.update(cx, |this, cx| { - this.reconnect(cx).ok(); - })?; - } - } - Err(error) => { - log::warn!("ssh io task died with error: {:?}. reconnecting...", error); - this.update(cx, |this, cx| { - this.reconnect(cx).ok(); - })?; - } - } - - Ok(()) - }) - } - - fn state_is(&self, check: impl FnOnce(&State) -> bool) -> bool { - self.state.as_ref().is_some_and(check) - } - - fn try_set_state(&mut self, cx: &mut Context, map: impl FnOnce(&State) -> Option) { - let new_state = self.state.as_ref().and_then(map); - if let Some(new_state) = new_state { - self.state.replace(new_state); - cx.notify(); - } - } - - fn set_state(&mut self, state: State, cx: &mut Context) { - log::info!("setting state to '{}'", &state); - - let is_reconnect_exhausted = state.is_reconnect_exhausted(); - let is_server_not_running = state.is_server_not_running(); - self.state.replace(state); - - if is_reconnect_exhausted || is_server_not_running { - cx.emit(RemoteClientEvent::Disconnected); - } - cx.notify(); - } - - pub fn shell(&self) -> Option { - Some(self.remote_connection()?.shell()) - } - - pub fn default_system_shell(&self) -> Option { - Some(self.remote_connection()?.default_system_shell()) - } - - pub fn shares_network_interface(&self) -> bool { - self.remote_connection() - .map_or(false, |connection| connection.shares_network_interface()) - } - - pub fn build_command( - &self, - program: Option, - args: &[String], - env: &HashMap, - working_dir: Option, - port_forward: Option<(u16, String, u16)>, - ) -> Result { - let Some(connection) = self.remote_connection() else { - return Err(anyhow!("no ssh connection")); - }; - connection.build_command(program, args, env, working_dir, port_forward) - } - - pub fn build_forward_ports_command( - &self, - forwards: Vec<(u16, String, u16)>, - ) -> Result { - let Some(connection) = self.remote_connection() else { - return Err(anyhow!("no ssh connection")); - }; - connection.build_forward_ports_command(forwards) - } - - pub fn upload_directory( - &self, - src_path: PathBuf, - dest_path: RemotePathBuf, - cx: &App, - ) -> Task> { - let Some(connection) = self.remote_connection() else { - return Task::ready(Err(anyhow!("no ssh connection"))); - }; - connection.upload_directory(src_path, dest_path, cx) - } - - pub fn proto_client(&self) -> AnyProtoClient { - self.client.clone().into() - } - - pub fn connection_options(&self) -> RemoteConnectionOptions { - self.connection_options.clone() - } - - pub fn connection(&self) -> Option> { - if let State::Connected { - remote_connection, .. - } = self.state.as_ref()? - { - Some(remote_connection.clone()) - } else { - None - } - } - - pub fn connection_state(&self) -> ConnectionState { - self.state - .as_ref() - .map(ConnectionState::from) - .unwrap_or(ConnectionState::Disconnected) - } - - pub fn is_disconnected(&self) -> bool { - self.connection_state() == ConnectionState::Disconnected - } - - pub fn path_style(&self) -> PathStyle { - self.path_style - } - - #[cfg(any(test, feature = "test-support"))] - pub fn simulate_disconnect(&self, client_cx: &mut App) -> Task<()> { - let opts = self.connection_options(); - client_cx.spawn(async move |cx| { - let connection = cx - .update_global(|c: &mut ConnectionPool, _| { - if let Some(ConnectionPoolEntry::Connecting(c)) = c.connections.get(&opts) { - c.clone() - } else { - panic!("missing test connection") - } - }) - .unwrap() - .await - .unwrap(); - - connection.simulate_disconnect(cx); - }) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn fake_server( - client_cx: &mut gpui::TestAppContext, - server_cx: &mut gpui::TestAppContext, - ) -> (RemoteConnectionOptions, AnyProtoClient) { - let port = client_cx - .update(|cx| cx.default_global::().connections.len() as u16 + 1); - let opts = RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: "".to_string(), - port: Some(port), - ..Default::default() - }); - let (outgoing_tx, _) = mpsc::unbounded::(); - let (_, incoming_rx) = mpsc::unbounded::(); - let server_client = server_cx - .update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "fake-server", false)); - let connection: Arc = Arc::new(fake::FakeRemoteConnection { - connection_options: opts.clone(), - server_cx: fake::SendableCx::new(server_cx), - server_channel: server_client.clone(), - }); - - client_cx.update(|cx| { - cx.update_default_global(|c: &mut ConnectionPool, cx| { - c.connections.insert( - opts.clone(), - ConnectionPoolEntry::Connecting( - cx.background_spawn({ - let connection = connection.clone(); - async move { Ok(connection.clone()) } - }) - .shared(), - ), - ); - }) - }); - - (opts, server_client.into()) - } - - #[cfg(any(test, feature = "test-support"))] - pub async fn fake_client( - opts: RemoteConnectionOptions, - client_cx: &mut gpui::TestAppContext, - ) -> Entity { - let (_tx, rx) = oneshot::channel(); - let mut cx = client_cx.to_async(); - let connection = connect(opts, Arc::new(fake::Delegate), &mut cx) - .await - .unwrap(); - client_cx - .update(|cx| { - Self::new( - ConnectionIdentifier::setup(), - connection, - rx, - Arc::new(fake::Delegate), - cx, - ) - }) - .await - .unwrap() - .unwrap() - } - - fn remote_connection(&self) -> Option> { - self.state - .as_ref() - .and_then(|state| state.remote_connection()) - } -} - -enum ConnectionPoolEntry { - Connecting(Shared, Arc>>>), - Connected(Weak), -} - -#[derive(Default)] -struct ConnectionPool { - connections: HashMap, -} - -impl Global for ConnectionPool {} - -impl ConnectionPool { - pub fn connect( - &mut self, - opts: RemoteConnectionOptions, - delegate: Arc, - cx: &mut App, - ) -> Shared, Arc>>> { - let connection = self.connections.get(&opts); - match connection { - Some(ConnectionPoolEntry::Connecting(task)) => { - delegate.set_status( - Some("Waiting for existing connection attempt"), - &mut cx.to_async(), - ); - return task.clone(); - } - Some(ConnectionPoolEntry::Connected(ssh)) => { - if let Some(ssh) = ssh.upgrade() - && !ssh.has_been_killed() - { - return Task::ready(Ok(ssh)).shared(); - } - self.connections.remove(&opts); - } - None => {} - } - - let task = cx - .spawn({ - let opts = opts.clone(); - let delegate = delegate.clone(); - async move |cx| { - let connection = match opts.clone() { - RemoteConnectionOptions::Ssh(opts) => { - SshRemoteConnection::new(opts, delegate, cx) - .await - .map(|connection| Arc::new(connection) as Arc) - } - RemoteConnectionOptions::Wsl(opts) => { - WslRemoteConnection::new(opts, delegate, cx) - .await - .map(|connection| Arc::new(connection) as Arc) - } - RemoteConnectionOptions::Docker(opts) => { - DockerExecConnection::new(opts, delegate, cx) - .await - .map(|connection| Arc::new(connection) as Arc) - } - }; - - cx.update_global(|pool: &mut Self, _| { - debug_assert!(matches!( - pool.connections.get(&opts), - Some(ConnectionPoolEntry::Connecting(_)) - )); - match connection { - Ok(connection) => { - pool.connections.insert( - opts.clone(), - ConnectionPoolEntry::Connected(Arc::downgrade(&connection)), - ); - Ok(connection) - } - Err(error) => { - pool.connections.remove(&opts); - Err(Arc::new(error)) - } - } - })? - } - }) - .shared(); - - self.connections - .insert(opts.clone(), ConnectionPoolEntry::Connecting(task.clone())); - task - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum RemoteConnectionOptions { - Ssh(SshConnectionOptions), - Wsl(WslConnectionOptions), - Docker(DockerConnectionOptions), -} - -impl RemoteConnectionOptions { - pub fn display_name(&self) -> String { - match self { - RemoteConnectionOptions::Ssh(opts) => opts.host.clone(), - RemoteConnectionOptions::Wsl(opts) => opts.distro_name.clone(), - RemoteConnectionOptions::Docker(opts) => opts.name.clone(), - } - } -} - -impl From for RemoteConnectionOptions { - fn from(opts: SshConnectionOptions) -> Self { - RemoteConnectionOptions::Ssh(opts) - } -} - -impl From for RemoteConnectionOptions { - fn from(opts: WslConnectionOptions) -> Self { - RemoteConnectionOptions::Wsl(opts) - } -} - -#[cfg(target_os = "windows")] -/// Open a wsl path (\\wsl.localhost\\path) -#[derive(Debug, Clone, PartialEq, Eq, gpui::Action)] -#[action(namespace = workspace, no_json, no_register)] -pub struct OpenWslPath { - pub distro: WslConnectionOptions, - pub paths: Vec, -} - -#[async_trait(?Send)] -pub trait RemoteConnection: Send + Sync { - fn start_proxy( - &self, - unique_identifier: String, - reconnect: bool, - incoming_tx: UnboundedSender, - outgoing_rx: UnboundedReceiver, - connection_activity_tx: Sender<()>, - delegate: Arc, - cx: &mut AsyncApp, - ) -> Task>; - fn upload_directory( - &self, - src_path: PathBuf, - dest_path: RemotePathBuf, - cx: &App, - ) -> Task>; - async fn kill(&self) -> Result<()>; - fn has_been_killed(&self) -> bool; - fn shares_network_interface(&self) -> bool { - false - } - fn build_command( - &self, - program: Option, - args: &[String], - env: &HashMap, - working_dir: Option, - port_forward: Option<(u16, String, u16)>, - ) -> Result; - fn build_forward_ports_command( - &self, - forwards: Vec<(u16, String, u16)>, - ) -> Result; - fn connection_options(&self) -> RemoteConnectionOptions; - fn path_style(&self) -> PathStyle; - fn shell(&self) -> String; - fn default_system_shell(&self) -> String; - fn has_wsl_interop(&self) -> bool; - - #[cfg(any(test, feature = "test-support"))] - fn simulate_disconnect(&self, _: &AsyncApp) {} -} - -type ResponseChannels = Mutex)>>>; - -struct Signal { - tx: Mutex>>, - rx: Shared>>, -} - -impl Signal { - pub fn new(cx: &App) -> Self { - let (tx, rx) = oneshot::channel(); - - let task = cx - .background_executor() - .spawn(async move { rx.await.ok() }) - .shared(); - - Self { - tx: Mutex::new(Some(tx)), - rx: task, - } - } - - fn set(&self, value: T) { - if let Some(tx) = self.tx.lock().take() { - let _ = tx.send(value); - } - } - - fn wait(&self) -> Shared>> { - self.rx.clone() - } -} - -struct ChannelClient { - next_message_id: AtomicU32, - outgoing_tx: Mutex>, - buffer: Mutex>, - response_channels: ResponseChannels, - message_handlers: Mutex, - max_received: AtomicU32, - name: &'static str, - task: Mutex>>, - remote_started: Signal<()>, - has_wsl_interop: bool, -} - -impl ChannelClient { - fn new( - incoming_rx: mpsc::UnboundedReceiver, - outgoing_tx: mpsc::UnboundedSender, - cx: &App, - name: &'static str, - has_wsl_interop: bool, - ) -> Arc { - Arc::new_cyclic(|this| Self { - outgoing_tx: Mutex::new(outgoing_tx), - next_message_id: AtomicU32::new(0), - max_received: AtomicU32::new(0), - response_channels: ResponseChannels::default(), - message_handlers: Default::default(), - buffer: Mutex::new(VecDeque::new()), - name, - task: Mutex::new(Self::start_handling_messages( - this.clone(), - incoming_rx, - &cx.to_async(), - )), - remote_started: Signal::new(cx), - has_wsl_interop, - }) - } - - fn wait_for_remote_started(&self) -> Shared>> { - self.remote_started.wait() - } - - fn start_handling_messages( - this: Weak, - mut incoming_rx: mpsc::UnboundedReceiver, - cx: &AsyncApp, - ) -> Task> { - cx.spawn(async move |cx| { - if let Some(this) = this.upgrade() { - let envelope = proto::RemoteStarted {}.into_envelope(0, None, None); - this.outgoing_tx.lock().unbounded_send(envelope).ok(); - }; - - let peer_id = PeerId { owner_id: 0, id: 0 }; - while let Some(incoming) = incoming_rx.next().await { - let Some(this) = this.upgrade() else { - return anyhow::Ok(()); - }; - if let Some(ack_id) = incoming.ack_id { - let mut buffer = this.buffer.lock(); - while buffer.front().is_some_and(|msg| msg.id <= ack_id) { - buffer.pop_front(); - } - } - if let Some(proto::envelope::Payload::FlushBufferedMessages(_)) = &incoming.payload - { - log::debug!( - "{}:ssh message received. name:FlushBufferedMessages", - this.name - ); - { - let buffer = this.buffer.lock(); - for envelope in buffer.iter() { - this.outgoing_tx - .lock() - .unbounded_send(envelope.clone()) - .ok(); - } - } - let mut envelope = proto::Ack {}.into_envelope(0, Some(incoming.id), None); - envelope.id = this.next_message_id.fetch_add(1, SeqCst); - this.outgoing_tx.lock().unbounded_send(envelope).ok(); - continue; - } - - if let Some(proto::envelope::Payload::RemoteStarted(_)) = &incoming.payload { - this.remote_started.set(()); - let mut envelope = proto::Ack {}.into_envelope(0, Some(incoming.id), None); - envelope.id = this.next_message_id.fetch_add(1, SeqCst); - this.outgoing_tx.lock().unbounded_send(envelope).ok(); - continue; - } - - this.max_received.store(incoming.id, SeqCst); - - if let Some(request_id) = incoming.responding_to { - let request_id = MessageId(request_id); - let sender = this.response_channels.lock().remove(&request_id); - if let Some(sender) = sender { - let (tx, rx) = oneshot::channel(); - if incoming.payload.is_some() { - sender.send((incoming, tx)).ok(); - } - rx.await.ok(); - } - } else if let Some(envelope) = - build_typed_envelope(peer_id, Instant::now(), incoming) - { - let type_name = envelope.payload_type_name(); - let message_id = envelope.message_id(); - if let Some(future) = ProtoMessageHandlerSet::handle_message( - &this.message_handlers, - envelope, - this.clone().into(), - cx.clone(), - ) { - log::debug!("{}:ssh message received. name:{type_name}", this.name); - cx.foreground_executor() - .spawn(async move { - match future.await { - Ok(_) => { - log::debug!( - "{}:ssh message handled. name:{type_name}", - this.name - ); - } - Err(error) => { - log::error!( - "{}:error handling message. type:{}, error:{:#}", - this.name, - type_name, - format!("{error:#}").lines().fold( - String::new(), - |mut message, line| { - if !message.is_empty() { - message.push(' '); - } - message.push_str(line); - message - } - ) - ); - } - } - }) - .detach() - } else { - log::error!("{}:unhandled ssh message name:{type_name}", this.name); - if let Err(e) = AnyProtoClient::from(this.clone()).send_response( - message_id, - anyhow::anyhow!("no handler registered for {type_name}").to_proto(), - ) { - log::error!( - "{}:error sending error response for {type_name}:{e:#}", - this.name - ); - } - } - } - } - anyhow::Ok(()) - }) - } - - fn reconnect( - self: &Arc, - incoming_rx: UnboundedReceiver, - outgoing_tx: UnboundedSender, - cx: &AsyncApp, - ) { - *self.outgoing_tx.lock() = outgoing_tx; - *self.task.lock() = Self::start_handling_messages(Arc::downgrade(self), incoming_rx, cx); - } - - fn request( - &self, - payload: T, - ) -> impl 'static + Future> { - self.request_internal(payload, true) - } - - fn request_internal( - &self, - payload: T, - use_buffer: bool, - ) -> impl 'static + Future> { - log::debug!("ssh request start. name:{}", T::NAME); - let response = - self.request_dynamic(payload.into_envelope(0, None, None), T::NAME, use_buffer); - async move { - let response = response.await?; - log::debug!("ssh request finish. name:{}", T::NAME); - T::Response::from_envelope(response).context("received a response of the wrong type") - } - } - - async fn resync(&self, timeout: Duration) -> Result<()> { - smol::future::or( - async { - self.request_internal(proto::FlushBufferedMessages {}, false) - .await?; - - for envelope in self.buffer.lock().iter() { - self.outgoing_tx - .lock() - .unbounded_send(envelope.clone()) - .ok(); - } - Ok(()) - }, - async { - smol::Timer::after(timeout).await; - anyhow::bail!("Timed out resyncing remote client") - }, - ) - .await - } - - async fn ping(&self, timeout: Duration) -> Result<()> { - smol::future::or( - async { - self.request(proto::Ping {}).await?; - Ok(()) - }, - async { - smol::Timer::after(timeout).await; - anyhow::bail!("Timed out pinging remote client") - }, - ) - .await - } - - fn send(&self, payload: T) -> Result<()> { - log::debug!("ssh send name:{}", T::NAME); - self.send_dynamic(payload.into_envelope(0, None, None)) - } - - fn request_dynamic( - &self, - mut envelope: proto::Envelope, - type_name: &'static str, - use_buffer: bool, - ) -> impl 'static + Future> { - envelope.id = self.next_message_id.fetch_add(1, SeqCst); - let (tx, rx) = oneshot::channel(); - let mut response_channels_lock = self.response_channels.lock(); - response_channels_lock.insert(MessageId(envelope.id), tx); - drop(response_channels_lock); - - let result = if use_buffer { - self.send_buffered(envelope) - } else { - self.send_unbuffered(envelope) - }; - async move { - if let Err(error) = &result { - log::error!("failed to send message: {error}"); - anyhow::bail!("failed to send message: {error}"); - } - - let response = rx.await.context("connection lost")?.0; - if let Some(proto::envelope::Payload::Error(error)) = &response.payload { - return Err(RpcError::from_proto(error, type_name)); - } - Ok(response) - } - } - - pub fn send_dynamic(&self, mut envelope: proto::Envelope) -> Result<()> { - envelope.id = self.next_message_id.fetch_add(1, SeqCst); - self.send_buffered(envelope) - } - - fn send_buffered(&self, mut envelope: proto::Envelope) -> Result<()> { - envelope.ack_id = Some(self.max_received.load(SeqCst)); - self.buffer.lock().push_back(envelope.clone()); - // ignore errors on send (happen while we're reconnecting) - // assume that the global "disconnected" overlay is sufficient. - self.outgoing_tx.lock().unbounded_send(envelope).ok(); - Ok(()) - } - - fn send_unbuffered(&self, mut envelope: proto::Envelope) -> Result<()> { - envelope.ack_id = Some(self.max_received.load(SeqCst)); - self.outgoing_tx.lock().unbounded_send(envelope).ok(); - Ok(()) - } -} - -impl ProtoClient for ChannelClient { - fn request( - &self, - envelope: proto::Envelope, - request_type: &'static str, - ) -> BoxFuture<'static, Result> { - self.request_dynamic(envelope, request_type, true).boxed() - } - - fn send(&self, envelope: proto::Envelope, _message_type: &'static str) -> Result<()> { - self.send_dynamic(envelope) - } - - fn send_response(&self, envelope: Envelope, _message_type: &'static str) -> anyhow::Result<()> { - self.send_dynamic(envelope) - } - - fn message_handler_set(&self) -> &Mutex { - &self.message_handlers - } - - fn is_via_collab(&self) -> bool { - false - } - - fn has_wsl_interop(&self) -> bool { - self.has_wsl_interop - } -} - -#[cfg(any(test, feature = "test-support"))] -mod fake { - use super::{ChannelClient, RemoteClientDelegate, RemoteConnection, RemotePlatform}; - use crate::remote_client::{CommandTemplate, RemoteConnectionOptions}; - use anyhow::Result; - use askpass::EncryptedPassword; - use async_trait::async_trait; - use collections::HashMap; - use futures::{ - FutureExt, SinkExt, StreamExt, - channel::{ - mpsc::{self, Sender}, - oneshot, - }, - select_biased, - }; - use gpui::{App, AppContext as _, AsyncApp, Task, TestAppContext}; - use release_channel::ReleaseChannel; - use rpc::proto::Envelope; - use semver::Version; - use std::{path::PathBuf, sync::Arc}; - use util::paths::{PathStyle, RemotePathBuf}; - - pub(super) struct FakeRemoteConnection { - pub(super) connection_options: RemoteConnectionOptions, - pub(super) server_channel: Arc, - pub(super) server_cx: SendableCx, - } - - pub(super) struct SendableCx(AsyncApp); - impl SendableCx { - // SAFETY: When run in test mode, GPUI is always single threaded. - pub(super) fn new(cx: &TestAppContext) -> Self { - Self(cx.to_async()) - } - - // SAFETY: Enforce that we're on the main thread by requiring a valid AsyncApp - fn get(&self, _: &AsyncApp) -> AsyncApp { - self.0.clone() - } - } - - // SAFETY: There is no way to access a SendableCx from a different thread, see [`SendableCx::new`] and [`SendableCx::get`] - unsafe impl Send for SendableCx {} - unsafe impl Sync for SendableCx {} - - #[async_trait(?Send)] - impl RemoteConnection for FakeRemoteConnection { - async fn kill(&self) -> Result<()> { - Ok(()) - } - - fn has_been_killed(&self) -> bool { - false - } - - fn build_command( - &self, - program: Option, - args: &[String], - env: &HashMap, - _: Option, - _: Option<(u16, String, u16)>, - ) -> Result { - let ssh_program = program.unwrap_or_else(|| "sh".to_string()); - let mut ssh_args = Vec::new(); - ssh_args.push(ssh_program); - ssh_args.extend(args.iter().cloned()); - Ok(CommandTemplate { - program: "ssh".into(), - args: ssh_args, - env: env.clone(), - }) - } - - fn build_forward_ports_command( - &self, - forwards: Vec<(u16, String, u16)>, - ) -> anyhow::Result { - Ok(CommandTemplate { - program: "ssh".into(), - args: std::iter::once("-N".to_owned()) - .chain(forwards.into_iter().map(|(local_port, host, remote_port)| { - format!("{local_port}:{host}:{remote_port}") - })) - .collect(), - env: Default::default(), - }) - } - - fn upload_directory( - &self, - _src_path: PathBuf, - _dest_path: RemotePathBuf, - _cx: &App, - ) -> Task> { - unreachable!() - } - - fn connection_options(&self) -> RemoteConnectionOptions { - self.connection_options.clone() - } - - fn simulate_disconnect(&self, cx: &AsyncApp) { - let (outgoing_tx, _) = mpsc::unbounded::(); - let (_, incoming_rx) = mpsc::unbounded::(); - self.server_channel - .reconnect(incoming_rx, outgoing_tx, &self.server_cx.get(cx)); - } - - fn start_proxy( - &self, - _unique_identifier: String, - _reconnect: bool, - mut client_incoming_tx: mpsc::UnboundedSender, - mut client_outgoing_rx: mpsc::UnboundedReceiver, - mut connection_activity_tx: Sender<()>, - _delegate: Arc, - cx: &mut AsyncApp, - ) -> Task> { - let (mut server_incoming_tx, server_incoming_rx) = mpsc::unbounded::(); - let (server_outgoing_tx, mut server_outgoing_rx) = mpsc::unbounded::(); - - self.server_channel.reconnect( - server_incoming_rx, - server_outgoing_tx, - &self.server_cx.get(cx), - ); - - cx.background_spawn(async move { - loop { - select_biased! { - server_to_client = server_outgoing_rx.next().fuse() => { - let Some(server_to_client) = server_to_client else { - return Ok(1) - }; - connection_activity_tx.try_send(()).ok(); - client_incoming_tx.send(server_to_client).await.ok(); - } - client_to_server = client_outgoing_rx.next().fuse() => { - let Some(client_to_server) = client_to_server else { - return Ok(1) - }; - server_incoming_tx.send(client_to_server).await.ok(); - } - } - } - }) - } - - fn path_style(&self) -> PathStyle { - PathStyle::local() - } - - fn shell(&self) -> String { - "sh".to_owned() - } - - fn default_system_shell(&self) -> String { - "sh".to_owned() - } - - fn has_wsl_interop(&self) -> bool { - false - } - } - - pub(super) struct Delegate; - - impl RemoteClientDelegate for Delegate { - fn ask_password(&self, _: String, _: oneshot::Sender, _: &mut AsyncApp) { - unreachable!() - } - - fn download_server_binary_locally( - &self, - _: RemotePlatform, - _: ReleaseChannel, - _: Option, - _: &mut AsyncApp, - ) -> Task> { - unreachable!() - } - - fn get_download_url( - &self, - _platform: RemotePlatform, - _release_channel: ReleaseChannel, - _version: Option, - _cx: &mut AsyncApp, - ) -> Task>> { - unreachable!() - } - - fn set_status(&self, _: Option<&str>, _: &mut AsyncApp) {} - } -} diff --git a/crates/remote/src/transport.rs b/crates/remote/src/transport.rs deleted file mode 100644 index 4cafbf60ee..0000000000 --- a/crates/remote/src/transport.rs +++ /dev/null @@ -1,428 +0,0 @@ -use crate::{ - RemotePlatform, - json_log::LogRecord, - protocol::{MESSAGE_LEN_SIZE, message_len_from_buffer, read_message_with_len, write_message}, -}; -use anyhow::{Context as _, Result}; -use futures::{ - AsyncReadExt as _, FutureExt as _, StreamExt as _, - channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender}, -}; -use gpui::{AppContext as _, AsyncApp, Task}; -use rpc::proto::Envelope; -use smol::process::Child; - -pub mod docker; -pub mod ssh; -pub mod wsl; - -/// Parses the output of `uname -sm` to determine the remote platform. -/// Takes the last line to skip possible shell initialization output. -fn parse_platform(output: &str) -> Result { - let output = output.trim(); - let uname = output.rsplit_once('\n').map_or(output, |(_, last)| last); - let Some((os, arch)) = uname.split_once(" ") else { - anyhow::bail!("unknown uname: {uname:?}") - }; - - let os = match os { - "Darwin" => "macos", - "Linux" => "linux", - _ => anyhow::bail!( - "Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development" - ), - }; - - // exclude armv5,6,7 as they are 32-bit. - let arch = if arch.starts_with("armv8") - || arch.starts_with("armv9") - || arch.starts_with("arm64") - || arch.starts_with("aarch64") - { - "aarch64" - } else if arch.starts_with("x86") { - "x86_64" - } else { - anyhow::bail!( - "Prebuilt remote servers are not yet available for {arch:?}. See https://zed.dev/docs/remote-development" - ) - }; - - Ok(RemotePlatform { os, arch }) -} - -/// Parses the output of `echo $SHELL` to determine the remote shell. -/// Takes the last line to skip possible shell initialization output. -fn parse_shell(output: &str, fallback_shell: &str) -> String { - let output = output.trim(); - let shell = output.rsplit_once('\n').map_or(output, |(_, last)| last); - if shell.is_empty() { - log::error!("$SHELL is not set, falling back to {fallback_shell}"); - fallback_shell.to_owned() - } else { - shell.to_owned() - } -} - -fn handle_rpc_messages_over_child_process_stdio( - mut remote_proxy_process: Child, - incoming_tx: UnboundedSender, - mut outgoing_rx: UnboundedReceiver, - mut connection_activity_tx: Sender<()>, - cx: &AsyncApp, -) -> Task> { - let mut child_stderr = remote_proxy_process.stderr.take().unwrap(); - let mut child_stdout = remote_proxy_process.stdout.take().unwrap(); - let mut child_stdin = remote_proxy_process.stdin.take().unwrap(); - - let mut stdin_buffer = Vec::new(); - let mut stdout_buffer = Vec::new(); - let mut stderr_buffer = Vec::new(); - let mut stderr_offset = 0; - - let stdin_task = cx.background_spawn(async move { - while let Some(outgoing) = outgoing_rx.next().await { - write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?; - } - anyhow::Ok(()) - }); - - let stdout_task = cx.background_spawn({ - let mut connection_activity_tx = connection_activity_tx.clone(); - async move { - loop { - stdout_buffer.resize(MESSAGE_LEN_SIZE, 0); - let len = child_stdout.read(&mut stdout_buffer).await?; - - if len == 0 { - return anyhow::Ok(()); - } - - if len < MESSAGE_LEN_SIZE { - child_stdout.read_exact(&mut stdout_buffer[len..]).await?; - } - - let message_len = message_len_from_buffer(&stdout_buffer); - let envelope = - read_message_with_len(&mut child_stdout, &mut stdout_buffer, message_len) - .await?; - connection_activity_tx.try_send(()).ok(); - incoming_tx.unbounded_send(envelope).ok(); - } - } - }); - - let stderr_task: Task> = cx.background_spawn(async move { - loop { - stderr_buffer.resize(stderr_offset + 1024, 0); - - let len = child_stderr - .read(&mut stderr_buffer[stderr_offset..]) - .await?; - if len == 0 { - return anyhow::Ok(()); - } - - stderr_offset += len; - let mut start_ix = 0; - while let Some(ix) = stderr_buffer[start_ix..stderr_offset] - .iter() - .position(|b| b == &b'\n') - { - let line_ix = start_ix + ix; - let content = &stderr_buffer[start_ix..line_ix]; - start_ix = line_ix + 1; - if let Ok(record) = serde_json::from_slice::(content) { - record.log(log::logger()) - } else { - eprintln!("(remote) {}", String::from_utf8_lossy(content)); - } - } - stderr_buffer.drain(0..start_ix); - stderr_offset -= start_ix; - - connection_activity_tx.try_send(()).ok(); - } - }); - - cx.background_spawn(async move { - let result = futures::select! { - result = stdin_task.fuse() => { - result.context("stdin") - } - result = stdout_task.fuse() => { - result.context("stdout") - } - result = stderr_task.fuse() => { - result.context("stderr") - } - }; - let status = remote_proxy_process.status().await?.code().unwrap_or(1); - match result { - Ok(_) => Ok(status), - Err(error) => Err(error), - } - }) -} - -#[cfg(debug_assertions)] -async fn build_remote_server_from_source( - platform: &crate::RemotePlatform, - delegate: &dyn crate::RemoteClientDelegate, - cx: &mut AsyncApp, -) -> Result> { - use smol::process::{Command, Stdio}; - use std::env::VarError; - use std::path::Path; - use util::command::new_smol_command; - - // By default, we make building remote server from source opt-out and we do not force artifact compression - // for quicker builds. - let build_remote_server = - std::env::var("ZED_BUILD_REMOTE_SERVER").unwrap_or("nocompress".into()); - - if let "false" | "no" | "off" | "0" = &*build_remote_server { - return Ok(None); - } - - async fn run_cmd(command: &mut Command) -> Result<()> { - let output = command - .kill_on_drop(true) - .stderr(Stdio::inherit()) - .output() - .await?; - anyhow::ensure!( - output.status.success(), - "Failed to run command: {command:?}" - ); - Ok(()) - } - - let use_musl = !build_remote_server.contains("nomusl"); - let triple = format!( - "{}-{}", - platform.arch, - match platform.os { - "linux" => - if use_musl { - "unknown-linux-musl" - } else { - "unknown-linux-gnu" - }, - "macos" => "apple-darwin", - _ => anyhow::bail!("can't cross compile for: {:?}", platform), - } - ); - let mut rust_flags = match std::env::var("RUSTFLAGS") { - Ok(val) => val, - Err(VarError::NotPresent) => String::new(), - Err(e) => { - log::error!("Failed to get env var `RUSTFLAGS` value: {e}"); - String::new() - } - }; - if platform.os == "linux" && use_musl { - rust_flags.push_str(" -C target-feature=+crt-static"); - - if let Ok(path) = std::env::var("ZED_ZSTD_MUSL_LIB") { - rust_flags.push_str(&format!(" -C link-arg=-L{path}")); - } - } - if build_remote_server.contains("mold") { - rust_flags.push_str(" -C link-arg=-fuse-ld=mold"); - } - - if platform.arch == std::env::consts::ARCH && platform.os == std::env::consts::OS { - delegate.set_status(Some("Building remote server binary from source"), cx); - log::info!("building remote server binary from source"); - run_cmd( - new_smol_command("cargo") - .current_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/../..")) - .args([ - "build", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - "--target", - &triple, - ]) - .env("RUSTFLAGS", &rust_flags), - ) - .await?; - } else { - if which("zig", cx).await?.is_none() { - anyhow::bail!(if cfg!(not(windows)) { - "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup)" - } else { - "zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup)" - }); - } - - let rustup = which("rustup", cx) - .await? - .context("rustup not found on $PATH, install rustup (see https://rustup.rs/)")?; - delegate.set_status(Some("Adding rustup target for cross-compilation"), cx); - log::info!("adding rustup target"); - run_cmd( - new_smol_command(rustup) - .args(["target", "add"]) - .arg(&triple), - ) - .await?; - - if which("cargo-zigbuild", cx).await?.is_none() { - delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx); - log::info!("installing cargo-zigbuild"); - run_cmd(new_smol_command("cargo").args(["install", "--locked", "cargo-zigbuild"])) - .await?; - } - - delegate.set_status( - Some(&format!( - "Building remote binary from source for {triple} with Zig" - )), - cx, - ); - log::info!("building remote binary from source for {triple} with Zig"); - run_cmd( - new_smol_command("cargo") - .args([ - "zigbuild", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - "--target", - &triple, - ]) - .env("RUSTFLAGS", &rust_flags), - ) - .await?; - }; - let bin_path = Path::new("target") - .join("remote_server") - .join(&triple) - .join("debug") - .join("remote_server"); - - let path = if !build_remote_server.contains("nocompress") { - delegate.set_status(Some("Compressing binary"), cx); - - #[cfg(not(target_os = "windows"))] - { - run_cmd(new_smol_command("gzip").args(["-f", &bin_path.to_string_lossy()])).await?; - } - - #[cfg(target_os = "windows")] - { - // On Windows, we use 7z to compress the binary - - let seven_zip = which("7z.exe",cx) - .await? - .context("7z.exe not found on $PATH, install it (e.g. with `winget install -e --id 7zip.7zip`) or, if you don't want this behaviour, set $env:ZED_BUILD_REMOTE_SERVER=\"nocompress\"")?; - let gz_path = format!("target/remote_server/{}/debug/remote_server.gz", triple); - if smol::fs::metadata(&gz_path).await.is_ok() { - smol::fs::remove_file(&gz_path).await?; - } - run_cmd(new_smol_command(seven_zip).args([ - "a", - "-tgzip", - &gz_path, - &bin_path.to_string_lossy(), - ])) - .await?; - } - - let mut archive_path = bin_path; - archive_path.set_extension("gz"); - std::env::current_dir()?.join(archive_path) - } else { - bin_path - }; - - Ok(Some(path)) -} - -#[cfg(debug_assertions)] -async fn which( - binary_name: impl AsRef, - cx: &mut AsyncApp, -) -> Result> { - let binary_name = binary_name.as_ref().to_string(); - let binary_name_cloned = binary_name.clone(); - let res = cx - .background_spawn(async move { which::which(binary_name_cloned) }) - .await; - match res { - Ok(path) => Ok(Some(path)), - Err(which::Error::CannotFindBinaryPath) => Ok(None), - Err(err) => Err(anyhow::anyhow!( - "Failed to run 'which' to find the binary '{binary_name}': {err}" - )), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_platform() { - let result = parse_platform("Linux x86_64\n").unwrap(); - assert_eq!(result.os, "linux"); - assert_eq!(result.arch, "x86_64"); - - let result = parse_platform("Darwin arm64\n").unwrap(); - assert_eq!(result.os, "macos"); - assert_eq!(result.arch, "aarch64"); - - let result = parse_platform("Linux x86_64").unwrap(); - assert_eq!(result.os, "linux"); - assert_eq!(result.arch, "x86_64"); - - let result = parse_platform("some shell init output\nLinux aarch64\n").unwrap(); - assert_eq!(result.os, "linux"); - assert_eq!(result.arch, "aarch64"); - - let result = parse_platform("some shell init output\nLinux aarch64").unwrap(); - assert_eq!(result.os, "linux"); - assert_eq!(result.arch, "aarch64"); - - assert_eq!(parse_platform("Linux armv8l\n").unwrap().arch, "aarch64"); - assert_eq!(parse_platform("Linux aarch64\n").unwrap().arch, "aarch64"); - assert_eq!(parse_platform("Linux x86_64\n").unwrap().arch, "x86_64"); - - let result = parse_platform( - r#"Linux x86_64 - What you're referring to as Linux, is in fact, GNU/Linux...\n"#, - ) - .unwrap(); - assert_eq!(result.os, "linux"); - assert_eq!(result.arch, "x86_64"); - - assert!(parse_platform("Windows x86_64\n").is_err()); - assert!(parse_platform("Linux armv7l\n").is_err()); - } - - #[test] - fn test_parse_shell() { - assert_eq!(parse_shell("/bin/bash\n", "sh"), "/bin/bash"); - assert_eq!(parse_shell("/bin/zsh\n", "sh"), "/bin/zsh"); - - assert_eq!(parse_shell("/bin/bash", "sh"), "/bin/bash"); - assert_eq!( - parse_shell("some shell init output\n/bin/bash\n", "sh"), - "/bin/bash" - ); - assert_eq!( - parse_shell("some shell init output\n/bin/bash", "sh"), - "/bin/bash" - ); - assert_eq!(parse_shell("", "sh"), "sh"); - assert_eq!(parse_shell("\n", "sh"), "sh"); - } -} diff --git a/crates/remote/src/transport/docker.rs b/crates/remote/src/transport/docker.rs deleted file mode 100644 index 09f5935ec6..0000000000 --- a/crates/remote/src/transport/docker.rs +++ /dev/null @@ -1,757 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use anyhow::anyhow; -use async_trait::async_trait; -use collections::HashMap; -use parking_lot::Mutex; -use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; -use semver::Version as SemanticVersion; -use std::time::Instant; -use std::{ - path::{Path, PathBuf}, - process::Stdio, - sync::Arc, -}; -use util::ResultExt; -use util::shell::ShellKind; -use util::{ - paths::{PathStyle, RemotePathBuf}, - rel_path::RelPath, -}; - -use futures::channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender}; -use gpui::{App, AppContext, AsyncApp, Task}; -use rpc::proto::Envelope; - -use crate::{ - RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions, RemotePlatform, - remote_client::CommandTemplate, -}; - -#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] -pub struct DockerConnectionOptions { - pub name: String, - pub container_id: String, - pub upload_binary_over_docker_exec: bool, -} - -pub(crate) struct DockerExecConnection { - proxy_process: Mutex>, - remote_dir_for_server: String, - remote_binary_relpath: Option>, - connection_options: DockerConnectionOptions, - remote_platform: Option, - path_style: Option, - shell: Option, -} - -impl DockerExecConnection { - pub async fn new( - connection_options: DockerConnectionOptions, - delegate: Arc, - cx: &mut AsyncApp, - ) -> Result { - let mut this = Self { - proxy_process: Mutex::new(None), - remote_dir_for_server: "/".to_string(), - remote_binary_relpath: None, - connection_options, - remote_platform: None, - path_style: None, - shell: None, - }; - let (release_channel, version, commit) = cx.update(|cx| { - ( - ReleaseChannel::global(cx), - AppVersion::global(cx), - AppCommitSha::try_global(cx), - ) - })?; - let remote_platform = this.check_remote_platform().await?; - - this.path_style = match remote_platform.os { - "windows" => Some(PathStyle::Windows), - _ => Some(PathStyle::Posix), - }; - - this.remote_platform = Some(remote_platform); - - this.shell = Some(this.discover_shell().await); - - this.remote_dir_for_server = this.docker_user_home_dir().await?.trim().to_string(); - - this.remote_binary_relpath = Some( - this.ensure_server_binary( - &delegate, - release_channel, - version, - &this.remote_dir_for_server, - commit, - cx, - ) - .await?, - ); - - Ok(this) - } - - async fn discover_shell(&self) -> String { - let default_shell = "sh"; - match self - .run_docker_exec("sh", None, &Default::default(), &["-c", "echo $SHELL"]) - .await - { - Ok(shell) => match shell.trim() { - "" => { - log::error!("$SHELL is not set, falling back to {default_shell}"); - default_shell.to_owned() - } - shell => shell.to_owned(), - }, - Err(e) => { - log::error!("Failed to get shell: {e}"); - default_shell.to_owned() - } - } - } - - async fn check_remote_platform(&self) -> Result { - let uname = self - .run_docker_exec("uname", None, &Default::default(), &["-sm"]) - .await?; - let Some((os, arch)) = uname.split_once(" ") else { - anyhow::bail!("unknown uname: {uname:?}") - }; - - let os = match os.trim() { - "Darwin" => "macos", - "Linux" => "linux", - _ => anyhow::bail!( - "Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development" - ), - }; - // exclude armv5,6,7 as they are 32-bit. - let arch = if arch.starts_with("armv8") - || arch.starts_with("armv9") - || arch.starts_with("arm64") - || arch.starts_with("aarch64") - { - "aarch64" - } else if arch.starts_with("x86") { - "x86_64" - } else { - anyhow::bail!( - "Prebuilt remote servers are not yet available for {arch:?}. See https://zed.dev/docs/remote-development" - ) - }; - - Ok(RemotePlatform { os, arch }) - } - - async fn ensure_server_binary( - &self, - delegate: &Arc, - release_channel: ReleaseChannel, - version: SemanticVersion, - remote_dir_for_server: &str, - commit: Option, - cx: &mut AsyncApp, - ) -> Result> { - let remote_platform = if self.remote_platform.is_some() { - self.remote_platform.unwrap() - } else { - anyhow::bail!("No remote platform defined; cannot proceed.") - }; - - let version_str = match release_channel { - ReleaseChannel::Nightly => { - let commit = commit.map(|s| s.full()).unwrap_or_default(); - format!("{}-{}", version, commit) - } - ReleaseChannel::Dev => "build".to_string(), - _ => version.to_string(), - }; - let binary_name = format!( - "zed-remote-server-{}-{}", - release_channel.dev_name(), - version_str - ); - let dst_path = - paths::remote_server_dir_relative().join(RelPath::unix(&binary_name).unwrap()); - - #[cfg(debug_assertions)] - if let Some(remote_server_path) = - super::build_remote_server_from_source(&remote_platform, delegate.as_ref(), cx).await? - { - let tmp_path = paths::remote_server_dir_relative().join( - RelPath::unix(&format!( - "download-{}-{}", - std::process::id(), - remote_server_path.file_name().unwrap().to_string_lossy() - )) - .unwrap(), - ); - self.upload_local_server_binary( - &remote_server_path, - &tmp_path, - &remote_dir_for_server, - delegate, - cx, - ) - .await?; - self.extract_server_binary(&dst_path, &tmp_path, &remote_dir_for_server, delegate, cx) - .await?; - return Ok(dst_path); - } - - if self - .run_docker_exec( - &dst_path.display(self.path_style()), - Some(&remote_dir_for_server), - &Default::default(), - &["version"], - ) - .await - .is_ok() - { - return Ok(dst_path); - } - - let wanted_version = cx.update(|cx| match release_channel { - ReleaseChannel::Nightly => Ok(None), - ReleaseChannel::Dev => { - anyhow::bail!( - "ZED_BUILD_REMOTE_SERVER is not set and no remote server exists at ({:?})", - dst_path - ) - } - _ => Ok(Some(AppVersion::global(cx))), - })??; - - let tmp_path_gz = paths::remote_server_dir_relative().join( - RelPath::unix(&format!( - "{}-download-{}.gz", - binary_name, - std::process::id() - )) - .unwrap(), - ); - if !self.connection_options.upload_binary_over_docker_exec - && let Some(url) = delegate - .get_download_url(remote_platform, release_channel, wanted_version.clone(), cx) - .await? - { - match self - .download_binary_on_server(&url, &tmp_path_gz, &remote_dir_for_server, delegate, cx) - .await - { - Ok(_) => { - self.extract_server_binary( - &dst_path, - &tmp_path_gz, - &remote_dir_for_server, - delegate, - cx, - ) - .await - .context("extracting server binary")?; - return Ok(dst_path); - } - Err(e) => { - log::error!( - "Failed to download binary on server, attempting to download locally and then upload it the server: {e:#}", - ) - } - } - } - - let src_path = delegate - .download_server_binary_locally(remote_platform, release_channel, wanted_version, cx) - .await - .context("downloading server binary locally")?; - self.upload_local_server_binary( - &src_path, - &tmp_path_gz, - &remote_dir_for_server, - delegate, - cx, - ) - .await - .context("uploading server binary")?; - self.extract_server_binary( - &dst_path, - &tmp_path_gz, - &remote_dir_for_server, - delegate, - cx, - ) - .await - .context("extracting server binary")?; - Ok(dst_path) - } - - async fn docker_user_home_dir(&self) -> Result { - let inner_program = self.shell(); - self.run_docker_exec( - &inner_program, - None, - &Default::default(), - &["-c", "echo $HOME"], - ) - .await - } - - async fn extract_server_binary( - &self, - dst_path: &RelPath, - tmp_path: &RelPath, - remote_dir_for_server: &str, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - delegate.set_status(Some("Extracting remote development server"), cx); - let server_mode = 0o755; - - let shell_kind = ShellKind::Posix; - let orig_tmp_path = tmp_path.display(self.path_style()); - let server_mode = format!("{:o}", server_mode); - let server_mode = shell_kind - .try_quote(&server_mode) - .context("shell quoting")?; - let dst_path = dst_path.display(self.path_style()); - let dst_path = shell_kind.try_quote(&dst_path).context("shell quoting")?; - let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") { - let orig_tmp_path = shell_kind - .try_quote(&orig_tmp_path) - .context("shell quoting")?; - let tmp_path = shell_kind.try_quote(&tmp_path).context("shell quoting")?; - format!( - "gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}", - ) - } else { - let orig_tmp_path = shell_kind - .try_quote(&orig_tmp_path) - .context("shell quoting")?; - format!("chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}",) - }; - let args = shell_kind.args_for_shell(false, script.to_string()); - self.run_docker_exec( - "sh", - Some(&remote_dir_for_server), - &Default::default(), - &args, - ) - .await - .log_err(); - Ok(()) - } - - async fn upload_local_server_binary( - &self, - src_path: &Path, - tmp_path_gz: &RelPath, - remote_dir_for_server: &str, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - if let Some(parent) = tmp_path_gz.parent() { - self.run_docker_exec( - "mkdir", - Some(remote_dir_for_server), - &Default::default(), - &["-p", parent.display(self.path_style()).as_ref()], - ) - .await?; - } - - let src_stat = smol::fs::metadata(&src_path).await?; - let size = src_stat.len(); - - let t0 = Instant::now(); - delegate.set_status(Some("Uploading remote development server"), cx); - log::info!( - "uploading remote development server to {:?} ({}kb)", - tmp_path_gz, - size / 1024 - ); - self.upload_file(src_path, tmp_path_gz, remote_dir_for_server) - .await - .context("failed to upload server binary")?; - log::info!("uploaded remote development server in {:?}", t0.elapsed()); - Ok(()) - } - - async fn upload_file( - &self, - src_path: &Path, - dest_path: &RelPath, - remote_dir_for_server: &str, - ) -> Result<()> { - log::debug!("uploading file {:?} to {:?}", src_path, dest_path); - - let src_path_display = src_path.display().to_string(); - let dest_path_str = dest_path.display(self.path_style()); - - let mut command = util::command::new_smol_command("docker"); - command.arg("cp"); - command.arg("-a"); - command.arg(&src_path_display); - command.arg(format!( - "{}:{}/{}", - &self.connection_options.container_id, remote_dir_for_server, dest_path_str - )); - - let output = command.output().await?; - - if output.status.success() { - return Ok(()); - } - - let stderr = String::from_utf8_lossy(&output.stderr); - log::debug!( - "failed to upload file via docker cp {src_path_display} -> {dest_path_str}: {stderr}", - ); - anyhow::bail!( - "failed to upload file via docker cp {} -> {}: {}", - src_path_display, - dest_path_str, - stderr, - ); - } - - async fn run_docker_command( - &self, - subcommand: &str, - args: &[impl AsRef], - ) -> Result { - let mut command = util::command::new_smol_command("docker"); - command.arg(subcommand); - for arg in args { - command.arg(arg.as_ref()); - } - let output = command.output().await?; - anyhow::ensure!( - output.status.success(), - "failed to run command {command:?}: {}", - String::from_utf8_lossy(&output.stderr) - ); - Ok(String::from_utf8_lossy(&output.stdout).to_string()) - } - - async fn run_docker_exec( - &self, - inner_program: &str, - working_directory: Option<&str>, - env: &HashMap, - program_args: &[impl AsRef], - ) -> Result { - let mut args = match working_directory { - Some(dir) => vec!["-w".to_string(), dir.to_string()], - None => vec![], - }; - - for (k, v) in env.iter() { - args.push("-e".to_string()); - let env_declaration = format!("{}={}", k, v); - args.push(env_declaration); - } - - args.push(self.connection_options.container_id.clone()); - args.push(inner_program.to_string()); - - for arg in program_args { - args.push(arg.as_ref().to_owned()); - } - self.run_docker_command("exec", args.as_ref()).await - } - - async fn download_binary_on_server( - &self, - url: &str, - tmp_path_gz: &RelPath, - remote_dir_for_server: &str, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - if let Some(parent) = tmp_path_gz.parent() { - self.run_docker_exec( - "mkdir", - Some(remote_dir_for_server), - &Default::default(), - &["-p", parent.display(self.path_style()).as_ref()], - ) - .await?; - } - - delegate.set_status(Some("Downloading remote development server on host"), cx); - - match self - .run_docker_exec( - "curl", - Some(remote_dir_for_server), - &Default::default(), - &[ - "-f", - "-L", - url, - "-o", - &tmp_path_gz.display(self.path_style()), - ], - ) - .await - { - Ok(_) => {} - Err(e) => { - if self - .run_docker_exec("which", None, &Default::default(), &["curl"]) - .await - .is_ok() - { - return Err(e); - } - - log::info!("curl is not available, trying wget"); - match self - .run_docker_exec( - "wget", - Some(remote_dir_for_server), - &Default::default(), - &[url, "-O", &tmp_path_gz.display(self.path_style())], - ) - .await - { - Ok(_) => {} - Err(e) => { - if self - .run_docker_exec("which", None, &Default::default(), &["wget"]) - .await - .is_ok() - { - return Err(e); - } else { - anyhow::bail!("Neither curl nor wget is available"); - } - } - } - } - } - Ok(()) - } - - fn kill_inner(&self) -> Result<()> { - if let Some(pid) = self.proxy_process.lock().take() { - if let Ok(_) = util::command::new_smol_command("kill") - .arg(pid.to_string()) - .spawn() - { - Ok(()) - } else { - Err(anyhow::anyhow!("Failed to kill process")) - } - } else { - Ok(()) - } - } -} - -#[async_trait(?Send)] -impl RemoteConnection for DockerExecConnection { - fn has_wsl_interop(&self) -> bool { - false - } - fn start_proxy( - &self, - unique_identifier: String, - reconnect: bool, - incoming_tx: UnboundedSender, - outgoing_rx: UnboundedReceiver, - connection_activity_tx: Sender<()>, - delegate: Arc, - cx: &mut AsyncApp, - ) -> Task> { - // We'll try connecting anew every time we open a devcontainer, so proactively try to kill any old connections. - if !self.has_been_killed() { - if let Err(e) = self.kill_inner() { - return Task::ready(Err(e)); - }; - } - - delegate.set_status(Some("Starting proxy"), cx); - - let Some(remote_binary_relpath) = self.remote_binary_relpath.clone() else { - return Task::ready(Err(anyhow!("Remote binary path not set"))); - }; - - let mut docker_args = vec![ - "exec".to_string(), - "-w".to_string(), - self.remote_dir_for_server.clone(), - "-i".to_string(), - self.connection_options.container_id.to_string(), - ]; - for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] { - if let Some(value) = std::env::var(env_var).ok() { - docker_args.push("-e".to_string()); - docker_args.push(format!("{}='{}'", env_var, value)); - } - } - let val = remote_binary_relpath - .display(self.path_style()) - .into_owned(); - docker_args.push(val); - docker_args.push("proxy".to_string()); - docker_args.push("--identifier".to_string()); - docker_args.push(unique_identifier); - if reconnect { - docker_args.push("--reconnect".to_string()); - } - let mut command = util::command::new_smol_command("docker"); - command - .kill_on_drop(true) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .args(docker_args); - - let Ok(child) = command.spawn() else { - return Task::ready(Err(anyhow::anyhow!( - "Failed to start remote server process" - ))); - }; - - let mut proxy_process = self.proxy_process.lock(); - *proxy_process = Some(child.id()); - - super::handle_rpc_messages_over_child_process_stdio( - child, - incoming_tx, - outgoing_rx, - connection_activity_tx, - cx, - ) - } - - fn upload_directory( - &self, - src_path: PathBuf, - dest_path: RemotePathBuf, - cx: &App, - ) -> Task> { - let dest_path_str = dest_path.to_string(); - let src_path_display = src_path.display().to_string(); - - let mut command = util::command::new_smol_command("docker"); - command.arg("cp"); - command.arg("-a"); // Archive mode is required to assign the file ownership to the default docker exec user - command.arg(src_path_display); - command.arg(format!( - "{}:{}", - self.connection_options.container_id, dest_path_str - )); - - cx.background_spawn(async move { - let output = command.output().await?; - - if output.status.success() { - Ok(()) - } else { - Err(anyhow::anyhow!("Failed to upload directory")) - } - }) - } - - async fn kill(&self) -> Result<()> { - self.kill_inner() - } - - fn has_been_killed(&self) -> bool { - self.proxy_process.lock().is_none() - } - - fn build_command( - &self, - program: Option, - args: &[String], - env: &HashMap, - working_dir: Option, - _port_forward: Option<(u16, String, u16)>, - ) -> Result { - let mut parsed_working_dir = None; - - let path_style = self.path_style(); - - if let Some(working_dir) = working_dir { - let working_dir = RemotePathBuf::new(working_dir, path_style).to_string(); - - const TILDE_PREFIX: &'static str = "~/"; - if working_dir.starts_with(TILDE_PREFIX) { - let working_dir = working_dir.trim_start_matches("~").trim_start_matches("/"); - parsed_working_dir = Some(format!("$HOME/{working_dir}")); - } else { - parsed_working_dir = Some(working_dir); - } - } - - let mut inner_program = Vec::new(); - - if let Some(program) = program { - inner_program.push(program); - for arg in args { - inner_program.push(arg.clone()); - } - } else { - inner_program.push(self.shell()); - inner_program.push("-l".to_string()); - }; - - let mut docker_args = vec!["exec".to_string()]; - - if let Some(parsed_working_dir) = parsed_working_dir { - docker_args.push("-w".to_string()); - docker_args.push(parsed_working_dir); - } - - for (k, v) in env.iter() { - docker_args.push("-e".to_string()); - docker_args.push(format!("{}={}", k, v)); - } - - docker_args.push("-it".to_string()); - docker_args.push(self.connection_options.container_id.to_string()); - - docker_args.append(&mut inner_program); - - Ok(CommandTemplate { - program: "docker".to_string(), - args: docker_args, - // Docker-exec pipes in environment via the "-e" argument - env: Default::default(), - }) - } - - fn build_forward_ports_command( - &self, - _forwards: Vec<(u16, String, u16)>, - ) -> Result { - Err(anyhow::anyhow!("Not currently supported for docker_exec")) - } - - fn connection_options(&self) -> RemoteConnectionOptions { - RemoteConnectionOptions::Docker(self.connection_options.clone()) - } - - fn path_style(&self) -> PathStyle { - self.path_style.unwrap_or(PathStyle::Posix) - } - - fn shell(&self) -> String { - match &self.shell { - Some(shell) => shell.clone(), - None => self.default_system_shell(), - } - } - - fn default_system_shell(&self) -> String { - String::from("/bin/sh") - } -} diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs deleted file mode 100644 index 9412549f20..0000000000 --- a/crates/remote/src/transport/ssh.rs +++ /dev/null @@ -1,1501 +0,0 @@ -use crate::{ - RemoteClientDelegate, RemotePlatform, - remote_client::{CommandTemplate, RemoteConnection, RemoteConnectionOptions}, - transport::{parse_platform, parse_shell}, -}; -use anyhow::{Context as _, Result, anyhow}; -use async_trait::async_trait; -use collections::HashMap; -use futures::{ - AsyncReadExt as _, FutureExt as _, - channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender}, - select_biased, -}; -use gpui::{App, AppContext as _, AsyncApp, Task}; -use parking_lot::Mutex; -use paths::remote_server_dir_relative; -use release_channel::{AppVersion, ReleaseChannel}; -use rpc::proto::Envelope; -use semver::Version; -pub use settings::SshPortForwardOption; -use smol::{ - fs, - process::{self, Child, Stdio}, -}; -use std::{ - path::{Path, PathBuf}, - sync::Arc, - time::Instant, -}; -use tempfile::TempDir; -use util::{ - paths::{PathStyle, RemotePathBuf}, - rel_path::RelPath, - shell::{Shell, ShellKind}, - shell_builder::ShellBuilder, -}; - -pub(crate) struct SshRemoteConnection { - socket: SshSocket, - master_process: Mutex>, - remote_binary_path: Option>, - ssh_platform: RemotePlatform, - ssh_path_style: PathStyle, - ssh_shell: String, - ssh_shell_kind: ShellKind, - ssh_default_system_shell: String, - _temp_dir: TempDir, -} - -#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] -pub struct SshConnectionOptions { - pub host: String, - pub username: Option, - pub port: Option, - pub password: Option, - pub args: Option>, - pub port_forwards: Option>, - - pub nickname: Option, - pub upload_binary_over_ssh: bool, -} - -impl From for SshConnectionOptions { - fn from(val: settings::SshConnection) -> Self { - SshConnectionOptions { - host: val.host.into(), - username: val.username, - port: val.port, - password: None, - args: Some(val.args), - nickname: val.nickname, - upload_binary_over_ssh: val.upload_binary_over_ssh.unwrap_or_default(), - port_forwards: val.port_forwards, - } - } -} - -struct SshSocket { - connection_options: SshConnectionOptions, - #[cfg(not(target_os = "windows"))] - socket_path: std::path::PathBuf, - envs: HashMap, - #[cfg(target_os = "windows")] - _proxy: askpass::PasswordProxy, -} - -struct MasterProcess { - process: Child, -} - -#[cfg(not(target_os = "windows"))] -impl MasterProcess { - pub fn new( - askpass_script_path: &std::ffi::OsStr, - additional_args: Vec, - socket_path: &std::path::Path, - url: &str, - ) -> Result { - let args = [ - "-N", - "-o", - "ControlPersist=no", - "-o", - "ControlMaster=yes", - "-o", - ]; - - let mut master_process = util::command::new_smol_command("ssh"); - master_process - .kill_on_drop(true) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .env("SSH_ASKPASS_REQUIRE", "force") - .env("SSH_ASKPASS", askpass_script_path) - .args(additional_args) - .args(args); - - master_process.arg(format!("ControlPath={}", socket_path.display())); - - let process = master_process.arg(&url).spawn()?; - - Ok(MasterProcess { process }) - } - - pub async fn wait_connected(&mut self) -> Result<()> { - let Some(mut stdout) = self.process.stdout.take() else { - anyhow::bail!("ssh process stdout capture failed"); - }; - - let mut output = Vec::new(); - stdout.read_to_end(&mut output).await?; - Ok(()) - } -} - -#[cfg(target_os = "windows")] -impl MasterProcess { - const CONNECTION_ESTABLISHED_MAGIC: &str = "ZED_SSH_CONNECTION_ESTABLISHED"; - - pub fn new( - askpass_script_path: &std::ffi::OsStr, - additional_args: Vec, - url: &str, - ) -> Result { - // On Windows, `ControlMaster` and `ControlPath` are not supported: - // https://github.com/PowerShell/Win32-OpenSSH/issues/405 - // https://github.com/PowerShell/Win32-OpenSSH/wiki/Project-Scope - // - // Using an ugly workaround to detect connection establishment - // -N doesn't work with JumpHosts as windows openssh never closes stdin in that case - let args = [ - "-t", - &format!("echo '{}'; exec $0", Self::CONNECTION_ESTABLISHED_MAGIC), - ]; - - let mut master_process = util::command::new_smol_command("ssh"); - master_process - .kill_on_drop(true) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .env("SSH_ASKPASS_REQUIRE", "force") - .env("SSH_ASKPASS", askpass_script_path) - .args(additional_args) - .arg(url) - .args(args); - - let process = master_process.spawn()?; - - Ok(MasterProcess { process }) - } - - pub async fn wait_connected(&mut self) -> Result<()> { - use smol::io::AsyncBufReadExt; - - let Some(stdout) = self.process.stdout.take() else { - anyhow::bail!("ssh process stdout capture failed"); - }; - - let mut reader = smol::io::BufReader::new(stdout); - - let mut line = String::new(); - - loop { - let n = reader.read_line(&mut line).await?; - if n == 0 { - anyhow::bail!("ssh process exited before connection established"); - } - - if line.contains(Self::CONNECTION_ESTABLISHED_MAGIC) { - return Ok(()); - } - } - } -} - -impl AsRef for MasterProcess { - fn as_ref(&self) -> &Child { - &self.process - } -} - -impl AsMut for MasterProcess { - fn as_mut(&mut self) -> &mut Child { - &mut self.process - } -} - -#[async_trait(?Send)] -impl RemoteConnection for SshRemoteConnection { - async fn kill(&self) -> Result<()> { - let Some(mut process) = self.master_process.lock().take() else { - return Ok(()); - }; - process.as_mut().kill().ok(); - process.as_mut().status().await?; - Ok(()) - } - - fn has_been_killed(&self) -> bool { - self.master_process.lock().is_none() - } - - fn connection_options(&self) -> RemoteConnectionOptions { - RemoteConnectionOptions::Ssh(self.socket.connection_options.clone()) - } - - fn shell(&self) -> String { - self.ssh_shell.clone() - } - - fn default_system_shell(&self) -> String { - self.ssh_default_system_shell.clone() - } - - fn build_command( - &self, - input_program: Option, - input_args: &[String], - input_env: &HashMap, - working_dir: Option, - port_forward: Option<(u16, String, u16)>, - ) -> Result { - let Self { - ssh_path_style, - socket, - ssh_shell_kind, - ssh_shell, - .. - } = self; - let env = socket.envs.clone(); - build_command( - input_program, - input_args, - input_env, - working_dir, - port_forward, - env, - *ssh_path_style, - ssh_shell, - *ssh_shell_kind, - socket.ssh_args(), - ) - } - - fn build_forward_ports_command( - &self, - forwards: Vec<(u16, String, u16)>, - ) -> Result { - let Self { socket, .. } = self; - let mut args = socket.ssh_args(); - args.push("-N".into()); - for (local_port, host, remote_port) in forwards { - args.push("-L".into()); - args.push(format!("{local_port}:{host}:{remote_port}")); - } - Ok(CommandTemplate { - program: "ssh".into(), - args, - env: Default::default(), - }) - } - - fn upload_directory( - &self, - src_path: PathBuf, - dest_path: RemotePathBuf, - cx: &App, - ) -> Task> { - let dest_path_str = dest_path.to_string(); - let src_path_display = src_path.display().to_string(); - - let mut sftp_command = self.build_sftp_command(); - let mut scp_command = - self.build_scp_command(&src_path, &dest_path_str, Some(&["-C", "-r"])); - - cx.background_spawn(async move { - // We will try SFTP first, and if that fails, we will fall back to SCP. - // If SCP fails also, we give up and return an error. - // The reason we allow a fallback from SFTP to SCP is that if the user has to specify a password, - // depending on the implementation of SSH stack, SFTP may disable interactive password prompts in batch mode. - // This is for example the case on Windows as evidenced by this implementation snippet: - // https://github.com/PowerShell/openssh-portable/blob/b8c08ef9da9450a94a9c5ef717d96a7bd83f3332/sshconnect2.c#L417 - if Self::is_sftp_available().await { - log::debug!("using SFTP for directory upload"); - let mut child = sftp_command.spawn()?; - if let Some(mut stdin) = child.stdin.take() { - use futures::AsyncWriteExt; - let sftp_batch = format!("put -r \"{src_path_display}\" \"{dest_path_str}\"\n"); - stdin.write_all(sftp_batch.as_bytes()).await?; - stdin.flush().await?; - } - - let output = child.output().await?; - if output.status.success() { - return Ok(()); - } - - let stderr = String::from_utf8_lossy(&output.stderr); - log::debug!("failed to upload directory via SFTP {src_path_display} -> {dest_path_str}: {stderr}"); - } - - log::debug!("using SCP for directory upload"); - let output = scp_command.output().await?; - - if output.status.success() { - return Ok(()); - } - - let stderr = String::from_utf8_lossy(&output.stderr); - log::debug!("failed to upload directory via SCP {src_path_display} -> {dest_path_str}: {stderr}"); - - anyhow::bail!( - "failed to upload directory via SFTP/SCP {} -> {}: {}", - src_path_display, - dest_path_str, - stderr, - ); - }) - } - - fn start_proxy( - &self, - unique_identifier: String, - reconnect: bool, - incoming_tx: UnboundedSender, - outgoing_rx: UnboundedReceiver, - connection_activity_tx: Sender<()>, - delegate: Arc, - cx: &mut AsyncApp, - ) -> Task> { - delegate.set_status(Some("Starting proxy"), cx); - - let Some(remote_binary_path) = self.remote_binary_path.clone() else { - return Task::ready(Err(anyhow!("Remote binary path not set"))); - }; - - let mut proxy_args = vec![]; - for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] { - if let Some(value) = std::env::var(env_var).ok() { - proxy_args.push(format!("{}='{}'", env_var, value)); - } - } - proxy_args.push(remote_binary_path.display(self.path_style()).into_owned()); - proxy_args.push("proxy".to_owned()); - proxy_args.push("--identifier".to_owned()); - proxy_args.push(unique_identifier); - - if reconnect { - proxy_args.push("--reconnect".to_owned()); - } - - let ssh_proxy_process = match self - .socket - .ssh_command(self.ssh_shell_kind, "env", &proxy_args, false) - // IMPORTANT: we kill this process when we drop the task that uses it. - .kill_on_drop(true) - .spawn() - { - Ok(process) => process, - Err(error) => { - return Task::ready(Err(anyhow!("failed to spawn remote server: {}", error))); - } - }; - - super::handle_rpc_messages_over_child_process_stdio( - ssh_proxy_process, - incoming_tx, - outgoing_rx, - connection_activity_tx, - cx, - ) - } - - fn path_style(&self) -> PathStyle { - self.ssh_path_style - } - - fn has_wsl_interop(&self) -> bool { - false - } -} - -impl SshRemoteConnection { - pub(crate) async fn new( - connection_options: SshConnectionOptions, - delegate: Arc, - cx: &mut AsyncApp, - ) -> Result { - use askpass::AskPassResult; - - let url = connection_options.ssh_url(); - - let temp_dir = tempfile::Builder::new() - .prefix("zed-ssh-session") - .tempdir()?; - let askpass_delegate = askpass::AskPassDelegate::new(cx, { - let delegate = delegate.clone(); - move |prompt, tx, cx| delegate.ask_password(prompt, tx, cx) - }); - - let mut askpass = - askpass::AskPassSession::new(cx.background_executor(), askpass_delegate).await?; - - delegate.set_status(Some("Connecting"), cx); - - // Start the master SSH process, which does not do anything except for establish - // the connection and keep it open, allowing other ssh commands to reuse it - // via a control socket. - #[cfg(not(target_os = "windows"))] - let socket_path = temp_dir.path().join("ssh.sock"); - - #[cfg(target_os = "windows")] - let mut master_process = MasterProcess::new( - askpass.script_path().as_ref(), - connection_options.additional_args(), - &url, - )?; - #[cfg(not(target_os = "windows"))] - let mut master_process = MasterProcess::new( - askpass.script_path().as_ref(), - connection_options.additional_args(), - &socket_path, - &url, - )?; - - let result = select_biased! { - result = askpass.run().fuse() => { - match result { - AskPassResult::CancelledByUser => { - master_process.as_mut().kill().ok(); - anyhow::bail!("SSH connection canceled") - } - AskPassResult::Timedout => { - anyhow::bail!("connecting to host timed out") - } - } - } - _ = master_process.wait_connected().fuse() => { - anyhow::Ok(()) - } - }; - - if let Err(e) = result { - return Err(e.context("Failed to connect to host")); - } - - if master_process.as_mut().try_status()?.is_some() { - let mut output = Vec::new(); - output.clear(); - let mut stderr = master_process.as_mut().stderr.take().unwrap(); - stderr.read_to_end(&mut output).await?; - - let error_message = format!( - "failed to connect: {}", - String::from_utf8_lossy(&output).trim() - ); - anyhow::bail!(error_message); - } - - #[cfg(not(target_os = "windows"))] - let socket = SshSocket::new(connection_options, socket_path).await?; - #[cfg(target_os = "windows")] - let socket = SshSocket::new( - connection_options, - askpass - .get_password() - .or_else(|| askpass::EncryptedPassword::try_from("").ok()) - .context("Failed to fetch askpass password")?, - cx.background_executor().clone(), - ) - .await?; - drop(askpass); - - let ssh_shell = socket.shell().await; - log::info!("Remote shell discovered: {}", ssh_shell); - let ssh_platform = socket.platform(ShellKind::new(&ssh_shell, false)).await?; - log::info!("Remote platform discovered: {:?}", ssh_platform); - let ssh_path_style = match ssh_platform.os { - "windows" => PathStyle::Windows, - _ => PathStyle::Posix, - }; - let ssh_default_system_shell = String::from("/bin/sh"); - let ssh_shell_kind = ShellKind::new( - &ssh_shell, - match ssh_platform.os { - "windows" => true, - _ => false, - }, - ); - - let mut this = Self { - socket, - master_process: Mutex::new(Some(master_process)), - _temp_dir: temp_dir, - remote_binary_path: None, - ssh_path_style, - ssh_platform, - ssh_shell, - ssh_shell_kind, - ssh_default_system_shell, - }; - - let (release_channel, version) = - cx.update(|cx| (ReleaseChannel::global(cx), AppVersion::global(cx)))?; - this.remote_binary_path = Some( - this.ensure_server_binary(&delegate, release_channel, version, cx) - .await?, - ); - - Ok(this) - } - - async fn ensure_server_binary( - &self, - delegate: &Arc, - release_channel: ReleaseChannel, - version: Version, - cx: &mut AsyncApp, - ) -> Result> { - let version_str = match release_channel { - ReleaseChannel::Dev => "build".to_string(), - _ => version.to_string(), - }; - let binary_name = format!( - "zed-remote-server-{}-{}", - release_channel.dev_name(), - version_str - ); - let dst_path = - paths::remote_server_dir_relative().join(RelPath::unix(&binary_name).unwrap()); - - #[cfg(debug_assertions)] - if let Some(remote_server_path) = - super::build_remote_server_from_source(&self.ssh_platform, delegate.as_ref(), cx) - .await? - { - let tmp_path = paths::remote_server_dir_relative().join( - RelPath::unix(&format!( - "download-{}-{}", - std::process::id(), - remote_server_path.file_name().unwrap().to_string_lossy() - )) - .unwrap(), - ); - self.upload_local_server_binary(&remote_server_path, &tmp_path, delegate, cx) - .await?; - self.extract_server_binary(&dst_path, &tmp_path, delegate, cx) - .await?; - return Ok(dst_path); - } - - if self - .socket - .run_command( - self.ssh_shell_kind, - &dst_path.display(self.path_style()), - &["version"], - true, - ) - .await - .is_ok() - { - return Ok(dst_path); - } - - let wanted_version = cx.update(|cx| match release_channel { - ReleaseChannel::Nightly => Ok(None), - ReleaseChannel::Dev => { - anyhow::bail!( - "ZED_BUILD_REMOTE_SERVER is not set and no remote server exists at ({:?})", - dst_path - ) - } - _ => Ok(Some(AppVersion::global(cx))), - })??; - - let tmp_path_gz = remote_server_dir_relative().join( - RelPath::unix(&format!( - "{}-download-{}.gz", - binary_name, - std::process::id() - )) - .unwrap(), - ); - if !self.socket.connection_options.upload_binary_over_ssh - && let Some(url) = delegate - .get_download_url( - self.ssh_platform, - release_channel, - wanted_version.clone(), - cx, - ) - .await? - { - match self - .download_binary_on_server(&url, &tmp_path_gz, delegate, cx) - .await - { - Ok(_) => { - self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx) - .await - .context("extracting server binary")?; - return Ok(dst_path); - } - Err(e) => { - log::error!( - "Failed to download binary on server, attempting to download locally and then upload it the server: {e:#}", - ) - } - } - } - - let src_path = delegate - .download_server_binary_locally( - self.ssh_platform, - release_channel, - wanted_version.clone(), - cx, - ) - .await - .context("downloading server binary locally")?; - self.upload_local_server_binary(&src_path, &tmp_path_gz, delegate, cx) - .await - .context("uploading server binary")?; - self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx) - .await - .context("extracting server binary")?; - Ok(dst_path) - } - - async fn download_binary_on_server( - &self, - url: &str, - tmp_path_gz: &RelPath, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - if let Some(parent) = tmp_path_gz.parent() { - self.socket - .run_command( - self.ssh_shell_kind, - "mkdir", - &["-p", parent.display(self.path_style()).as_ref()], - true, - ) - .await?; - } - - delegate.set_status(Some("Downloading remote development server on host"), cx); - - const CONNECT_TIMEOUT_SECS: &str = "10"; - - match self - .socket - .run_command( - self.ssh_shell_kind, - "curl", - &[ - "-f", - "-L", - "--connect-timeout", - CONNECT_TIMEOUT_SECS, - url, - "-o", - &tmp_path_gz.display(self.path_style()), - ], - true, - ) - .await - { - Ok(_) => {} - Err(e) => { - if self - .socket - .run_command(self.ssh_shell_kind, "which", &["curl"], true) - .await - .is_ok() - { - return Err(e); - } - - log::info!("curl is not available, trying wget"); - match self - .socket - .run_command( - self.ssh_shell_kind, - "wget", - &[ - "--connect-timeout", - CONNECT_TIMEOUT_SECS, - "--tries", - "1", - url, - "-O", - &tmp_path_gz.display(self.path_style()), - ], - true, - ) - .await - { - Ok(_) => {} - Err(e) => { - if self - .socket - .run_command(self.ssh_shell_kind, "which", &["wget"], true) - .await - .is_ok() - { - return Err(e); - } else { - anyhow::bail!("Neither curl nor wget is available"); - } - } - } - } - } - - Ok(()) - } - - async fn upload_local_server_binary( - &self, - src_path: &Path, - tmp_path_gz: &RelPath, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - if let Some(parent) = tmp_path_gz.parent() { - self.socket - .run_command( - self.ssh_shell_kind, - "mkdir", - &["-p", parent.display(self.path_style()).as_ref()], - true, - ) - .await?; - } - - let src_stat = fs::metadata(&src_path).await?; - let size = src_stat.len(); - - let t0 = Instant::now(); - delegate.set_status(Some("Uploading remote development server"), cx); - log::info!( - "uploading remote development server to {:?} ({}kb)", - tmp_path_gz, - size / 1024 - ); - self.upload_file(src_path, tmp_path_gz) - .await - .context("failed to upload server binary")?; - log::info!("uploaded remote development server in {:?}", t0.elapsed()); - Ok(()) - } - - async fn extract_server_binary( - &self, - dst_path: &RelPath, - tmp_path: &RelPath, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - delegate.set_status(Some("Extracting remote development server"), cx); - let server_mode = 0o755; - - let shell_kind = ShellKind::Posix; - let orig_tmp_path = tmp_path.display(self.path_style()); - let server_mode = format!("{:o}", server_mode); - let server_mode = shell_kind - .try_quote(&server_mode) - .context("shell quoting")?; - let dst_path = dst_path.display(self.path_style()); - let dst_path = shell_kind.try_quote(&dst_path).context("shell quoting")?; - let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") { - let orig_tmp_path = shell_kind - .try_quote(&orig_tmp_path) - .context("shell quoting")?; - let tmp_path = shell_kind.try_quote(&tmp_path).context("shell quoting")?; - format!( - "gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}", - ) - } else { - let orig_tmp_path = shell_kind - .try_quote(&orig_tmp_path) - .context("shell quoting")?; - format!("chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}",) - }; - let args = shell_kind.args_for_shell(false, script.to_string()); - self.socket - .run_command(shell_kind, "sh", &args, true) - .await?; - Ok(()) - } - - fn build_scp_command( - &self, - src_path: &Path, - dest_path_str: &str, - args: Option<&[&str]>, - ) -> process::Command { - let mut command = util::command::new_smol_command("scp"); - self.socket.ssh_options(&mut command, false).args( - self.socket - .connection_options - .port - .map(|port| vec!["-P".to_string(), port.to_string()]) - .unwrap_or_default(), - ); - if let Some(args) = args { - command.args(args); - } - command.arg(src_path).arg(format!( - "{}:{}", - self.socket.connection_options.scp_url(), - dest_path_str - )); - command - } - - fn build_sftp_command(&self) -> process::Command { - let mut command = util::command::new_smol_command("sftp"); - self.socket.ssh_options(&mut command, false).args( - self.socket - .connection_options - .port - .map(|port| vec!["-P".to_string(), port.to_string()]) - .unwrap_or_default(), - ); - command.arg("-b").arg("-"); - command.arg(self.socket.connection_options.scp_url()); - command.stdin(Stdio::piped()); - command - } - - async fn upload_file(&self, src_path: &Path, dest_path: &RelPath) -> Result<()> { - log::debug!("uploading file {:?} to {:?}", src_path, dest_path); - - let src_path_display = src_path.display().to_string(); - let dest_path_str = dest_path.display(self.path_style()); - - // We will try SFTP first, and if that fails, we will fall back to SCP. - // If SCP fails also, we give up and return an error. - // The reason we allow a fallback from SFTP to SCP is that if the user has to specify a password, - // depending on the implementation of SSH stack, SFTP may disable interactive password prompts in batch mode. - // This is for example the case on Windows as evidenced by this implementation snippet: - // https://github.com/PowerShell/openssh-portable/blob/b8c08ef9da9450a94a9c5ef717d96a7bd83f3332/sshconnect2.c#L417 - if Self::is_sftp_available().await { - log::debug!("using SFTP for file upload"); - let mut command = self.build_sftp_command(); - let sftp_batch = format!("put {src_path_display} {dest_path_str}\n"); - - let mut child = command.spawn()?; - if let Some(mut stdin) = child.stdin.take() { - use futures::AsyncWriteExt; - stdin.write_all(sftp_batch.as_bytes()).await?; - stdin.flush().await?; - } - - let output = child.output().await?; - if output.status.success() { - return Ok(()); - } - - let stderr = String::from_utf8_lossy(&output.stderr); - log::debug!( - "failed to upload file via SFTP {src_path_display} -> {dest_path_str}: {stderr}" - ); - } - - log::debug!("using SCP for file upload"); - let mut command = self.build_scp_command(src_path, &dest_path_str, None); - let output = command.output().await?; - - if output.status.success() { - return Ok(()); - } - - let stderr = String::from_utf8_lossy(&output.stderr); - log::debug!( - "failed to upload file via SCP {src_path_display} -> {dest_path_str}: {stderr}", - ); - anyhow::bail!( - "failed to upload file via STFP/SCP {} -> {}: {}", - src_path_display, - dest_path_str, - stderr, - ); - } - - async fn is_sftp_available() -> bool { - which::which("sftp").is_ok() - } -} - -impl SshSocket { - #[cfg(not(target_os = "windows"))] - async fn new(options: SshConnectionOptions, socket_path: PathBuf) -> Result { - Ok(Self { - connection_options: options, - envs: HashMap::default(), - socket_path, - }) - } - - #[cfg(target_os = "windows")] - async fn new( - options: SshConnectionOptions, - password: askpass::EncryptedPassword, - executor: gpui::BackgroundExecutor, - ) -> Result { - let mut envs = HashMap::default(); - let get_password = - move |_| Task::ready(std::ops::ControlFlow::Continue(Ok(password.clone()))); - - let _proxy = askpass::PasswordProxy::new(get_password, executor).await?; - envs.insert("SSH_ASKPASS_REQUIRE".into(), "force".into()); - envs.insert( - "SSH_ASKPASS".into(), - _proxy.script_path().as_ref().display().to_string(), - ); - - Ok(Self { - connection_options: options, - envs, - _proxy, - }) - } - - // :WARNING: ssh unquotes arguments when executing on the remote :WARNING: - // e.g. $ ssh host sh -c 'ls -l' is equivalent to $ ssh host sh -c ls -l - // and passes -l as an argument to sh, not to ls. - // Furthermore, some setups (e.g. Coder) will change directory when SSH'ing - // into a machine. You must use `cd` to get back to $HOME. - // You need to do it like this: $ ssh host "cd; sh -c 'ls -l /tmp'" - fn ssh_command( - &self, - shell_kind: ShellKind, - program: &str, - args: &[impl AsRef], - allow_pseudo_tty: bool, - ) -> process::Command { - let mut command = util::command::new_smol_command("ssh"); - let program = shell_kind.prepend_command_prefix(program); - let mut to_run = shell_kind - .try_quote_prefix_aware(&program) - .expect("shell quoting") - .into_owned(); - for arg in args { - // We're trying to work with: sh, bash, zsh, fish, tcsh, ...? - debug_assert!( - !arg.as_ref().contains('\n'), - "multiline arguments do not work in all shells" - ); - to_run.push(' '); - to_run.push_str(&shell_kind.try_quote(arg.as_ref()).expect("shell quoting")); - } - let separator = shell_kind.sequential_commands_separator(); - let to_run = format!("cd{separator} {to_run}"); - self.ssh_options(&mut command, true) - .arg(self.connection_options.ssh_url()); - if !allow_pseudo_tty { - command.arg("-T"); - } - command.arg(to_run); - log::debug!("ssh {:?}", command); - command - } - - async fn run_command( - &self, - shell_kind: ShellKind, - program: &str, - args: &[impl AsRef], - allow_pseudo_tty: bool, - ) -> Result { - let mut command = self.ssh_command(shell_kind, program, args, allow_pseudo_tty); - let output = command.output().await?; - anyhow::ensure!( - output.status.success(), - "failed to run command {command:?}: {}", - String::from_utf8_lossy(&output.stderr) - ); - Ok(String::from_utf8_lossy(&output.stdout).to_string()) - } - - #[cfg(not(target_os = "windows"))] - fn ssh_options<'a>( - &self, - command: &'a mut process::Command, - include_port_forwards: bool, - ) -> &'a mut process::Command { - let args = if include_port_forwards { - self.connection_options.additional_args() - } else { - self.connection_options.additional_args_for_scp() - }; - - command - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .args(args) - .args(["-o", "ControlMaster=no", "-o"]) - .arg(format!("ControlPath={}", self.socket_path.display())) - } - - #[cfg(target_os = "windows")] - fn ssh_options<'a>( - &self, - command: &'a mut process::Command, - include_port_forwards: bool, - ) -> &'a mut process::Command { - let args = if include_port_forwards { - self.connection_options.additional_args() - } else { - self.connection_options.additional_args_for_scp() - }; - - command - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .args(args) - .envs(self.envs.clone()) - } - - // On Windows, we need to use `SSH_ASKPASS` to provide the password to ssh. - // On Linux, we use the `ControlPath` option to create a socket file that ssh can use to - #[cfg(not(target_os = "windows"))] - fn ssh_args(&self) -> Vec { - let mut arguments = self.connection_options.additional_args(); - arguments.extend(vec![ - "-o".to_string(), - "ControlMaster=no".to_string(), - "-o".to_string(), - format!("ControlPath={}", self.socket_path.display()), - self.connection_options.ssh_url(), - ]); - arguments - } - - #[cfg(target_os = "windows")] - fn ssh_args(&self) -> Vec { - let mut arguments = self.connection_options.additional_args(); - arguments.push(self.connection_options.ssh_url()); - arguments - } - - async fn platform(&self, shell: ShellKind) -> Result { - let output = self.run_command(shell, "uname", &["-sm"], false).await?; - parse_platform(&output) - } - - async fn shell(&self) -> String { - const DEFAULT_SHELL: &str = "sh"; - match self - .run_command(ShellKind::Posix, "sh", &["-c", "echo $SHELL"], false) - .await - { - Ok(output) => parse_shell(&output, DEFAULT_SHELL), - Err(e) => { - log::error!("Failed to detect remote shell: {e}"); - DEFAULT_SHELL.to_owned() - } - } - } -} - -fn parse_port_number(port_str: &str) -> Result { - port_str - .parse() - .with_context(|| format!("parsing port number: {port_str}")) -} - -fn parse_port_forward_spec(spec: &str) -> Result { - let parts: Vec<&str> = spec.split(':').collect(); - - match parts.len() { - 4 => { - let local_port = parse_port_number(parts[1])?; - let remote_port = parse_port_number(parts[3])?; - - Ok(SshPortForwardOption { - local_host: Some(parts[0].to_string()), - local_port, - remote_host: Some(parts[2].to_string()), - remote_port, - }) - } - 3 => { - let local_port = parse_port_number(parts[0])?; - let remote_port = parse_port_number(parts[2])?; - - Ok(SshPortForwardOption { - local_host: None, - local_port, - remote_host: Some(parts[1].to_string()), - remote_port, - }) - } - _ => anyhow::bail!("Invalid port forward format"), - } -} - -impl SshConnectionOptions { - pub fn parse_command_line(input: &str) -> Result { - let input = input.trim_start_matches("ssh "); - let mut hostname: Option = None; - let mut username: Option = None; - let mut port: Option = None; - let mut args = Vec::new(); - let mut port_forwards: Vec = Vec::new(); - - // disallowed: -E, -e, -F, -f, -G, -g, -M, -N, -n, -O, -q, -S, -s, -T, -t, -V, -v, -W - const ALLOWED_OPTS: &[&str] = &[ - "-4", "-6", "-A", "-a", "-C", "-K", "-k", "-X", "-x", "-Y", "-y", - ]; - const ALLOWED_ARGS: &[&str] = &[ - "-B", "-b", "-c", "-D", "-F", "-I", "-i", "-J", "-l", "-m", "-o", "-P", "-p", "-R", - "-w", - ]; - - let mut tokens = ShellKind::Posix - .split(input) - .context("invalid input")? - .into_iter(); - - 'outer: while let Some(arg) = tokens.next() { - if ALLOWED_OPTS.contains(&(&arg as &str)) { - args.push(arg.to_string()); - continue; - } - if arg == "-p" { - port = tokens.next().and_then(|arg| arg.parse().ok()); - continue; - } else if let Some(p) = arg.strip_prefix("-p") { - port = p.parse().ok(); - continue; - } - if arg == "-l" { - username = tokens.next(); - continue; - } else if let Some(l) = arg.strip_prefix("-l") { - username = Some(l.to_string()); - continue; - } - if arg == "-L" || arg.starts_with("-L") { - let forward_spec = if arg == "-L" { - tokens.next() - } else { - Some(arg.strip_prefix("-L").unwrap().to_string()) - }; - - if let Some(spec) = forward_spec { - port_forwards.push(parse_port_forward_spec(&spec)?); - } else { - anyhow::bail!("Missing port forward format"); - } - } - - for a in ALLOWED_ARGS { - if arg == *a { - args.push(arg); - if let Some(next) = tokens.next() { - args.push(next); - } - continue 'outer; - } else if arg.starts_with(a) { - args.push(arg); - continue 'outer; - } - } - if arg.starts_with("-") || hostname.is_some() { - anyhow::bail!("unsupported argument: {:?}", arg); - } - let mut input = &arg as &str; - // Destination might be: username1@username2@ip2@ip1 - if let Some((u, rest)) = input.rsplit_once('@') { - input = rest; - username = Some(u.to_string()); - } - if let Some((rest, p)) = input.split_once(':') { - input = rest; - port = p.parse().ok() - } - hostname = Some(input.to_string()) - } - - let Some(hostname) = hostname else { - anyhow::bail!("missing hostname"); - }; - - let port_forwards = match port_forwards.len() { - 0 => None, - _ => Some(port_forwards), - }; - - Ok(Self { - host: hostname, - username, - port, - port_forwards, - args: Some(args), - password: None, - nickname: None, - upload_binary_over_ssh: false, - }) - } - - pub fn ssh_url(&self) -> String { - let mut result = String::from("ssh://"); - if let Some(username) = &self.username { - // Username might be: username1@username2@ip2 - let username = urlencoding::encode(username); - result.push_str(&username); - result.push('@'); - } - result.push_str(&self.host); - if let Some(port) = self.port { - result.push(':'); - result.push_str(&port.to_string()); - } - result - } - - pub fn additional_args_for_scp(&self) -> Vec { - self.args.iter().flatten().cloned().collect::>() - } - - pub fn additional_args(&self) -> Vec { - let mut args = self.additional_args_for_scp(); - - if let Some(forwards) = &self.port_forwards { - args.extend(forwards.iter().map(|pf| { - let local_host = match &pf.local_host { - Some(host) => host, - None => "localhost", - }; - let remote_host = match &pf.remote_host { - Some(host) => host, - None => "localhost", - }; - - format!( - "-L{}:{}:{}:{}", - local_host, pf.local_port, remote_host, pf.remote_port - ) - })); - } - - args - } - - fn scp_url(&self) -> String { - if let Some(username) = &self.username { - format!("{}@{}", username, self.host) - } else { - self.host.clone() - } - } - - pub fn connection_string(&self) -> String { - let host = if let Some(username) = &self.username { - format!("{}@{}", username, self.host) - } else { - self.host.clone() - }; - if let Some(port) = &self.port { - format!("{}:{}", host, port) - } else { - host - } - } -} - -fn build_command( - input_program: Option, - input_args: &[String], - input_env: &HashMap, - working_dir: Option, - port_forward: Option<(u16, String, u16)>, - ssh_env: HashMap, - ssh_path_style: PathStyle, - ssh_shell: &str, - ssh_shell_kind: ShellKind, - ssh_args: Vec, -) -> Result { - use std::fmt::Write as _; - - let mut exec = String::new(); - if let Some(working_dir) = working_dir { - let working_dir = RemotePathBuf::new(working_dir, ssh_path_style).to_string(); - - // shlex will wrap the command in single quotes (''), disabling ~ expansion, - // replace with something that works - const TILDE_PREFIX: &'static str = "~/"; - if working_dir.starts_with(TILDE_PREFIX) { - let working_dir = working_dir.trim_start_matches("~").trim_start_matches("/"); - write!( - exec, - "cd \"$HOME/{working_dir}\" {} ", - ssh_shell_kind.sequential_and_commands_separator() - )?; - } else { - write!( - exec, - "cd \"{working_dir}\" {} ", - ssh_shell_kind.sequential_and_commands_separator() - )?; - } - } else { - write!( - exec, - "cd {} ", - ssh_shell_kind.sequential_and_commands_separator() - )?; - }; - write!(exec, "exec env ")?; - - for (k, v) in input_env.iter() { - write!( - exec, - "{}={} ", - k, - ssh_shell_kind.try_quote(v).context("shell quoting")? - )?; - } - - if let Some(input_program) = input_program { - write!( - exec, - "{}", - ssh_shell_kind - .try_quote_prefix_aware(&input_program) - .context("shell quoting")? - )?; - for arg in input_args { - let arg = ssh_shell_kind.try_quote(&arg).context("shell quoting")?; - write!(exec, " {}", &arg)?; - } - } else { - write!(exec, "{ssh_shell} -l")?; - }; - let (command, command_args) = ShellBuilder::new(&Shell::Program(ssh_shell.to_owned()), false) - .build(Some(exec.clone()), &[]); - - let mut args = Vec::new(); - args.extend(ssh_args); - - if let Some((local_port, host, remote_port)) = port_forward { - args.push("-L".into()); - args.push(format!("{local_port}:{host}:{remote_port}")); - } - - args.push("-t".into()); - args.push(command); - args.extend(command_args); - - Ok(CommandTemplate { - program: "ssh".into(), - args, - env: ssh_env, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_build_command() -> Result<()> { - let mut input_env = HashMap::default(); - input_env.insert("INPUT_VA".to_string(), "val".to_string()); - let mut env = HashMap::default(); - env.insert("SSH_VAR".to_string(), "ssh-val".to_string()); - - let command = build_command( - Some("remote_program".to_string()), - &["arg1".to_string(), "arg2".to_string()], - &input_env, - Some("~/work".to_string()), - None, - env.clone(), - PathStyle::Posix, - "/bin/fish", - ShellKind::Fish, - vec!["-p".to_string(), "2222".to_string()], - )?; - - assert_eq!(command.program, "ssh"); - assert_eq!( - command.args.iter().map(String::as_str).collect::>(), - [ - "-p", - "2222", - "-t", - "/bin/fish", - "-i", - "-c", - "cd \"$HOME/work\" && exec env INPUT_VA=val remote_program arg1 arg2" - ] - ); - assert_eq!(command.env, env); - - let mut input_env = HashMap::default(); - input_env.insert("INPUT_VA".to_string(), "val".to_string()); - let mut env = HashMap::default(); - env.insert("SSH_VAR".to_string(), "ssh-val".to_string()); - - let command = build_command( - None, - &["arg1".to_string(), "arg2".to_string()], - &input_env, - None, - Some((1, "foo".to_owned(), 2)), - env.clone(), - PathStyle::Posix, - "/bin/fish", - ShellKind::Fish, - vec!["-p".to_string(), "2222".to_string()], - )?; - - assert_eq!(command.program, "ssh"); - assert_eq!( - command.args.iter().map(String::as_str).collect::>(), - [ - "-p", - "2222", - "-L", - "1:foo:2", - "-t", - "/bin/fish", - "-i", - "-c", - "cd && exec env INPUT_VA=val /bin/fish -l" - ] - ); - assert_eq!(command.env, env); - - Ok(()) - } - - #[test] - fn scp_args_exclude_port_forward_flags() { - let options = SshConnectionOptions { - host: "example.com".into(), - args: Some(vec![ - "-p".to_string(), - "2222".to_string(), - "-o".to_string(), - "StrictHostKeyChecking=no".to_string(), - ]), - port_forwards: Some(vec![SshPortForwardOption { - local_host: Some("127.0.0.1".to_string()), - local_port: 8080, - remote_host: Some("127.0.0.1".to_string()), - remote_port: 80, - }]), - ..Default::default() - }; - - let ssh_args = options.additional_args(); - assert!( - ssh_args.iter().any(|arg| arg.starts_with("-L")), - "expected ssh args to include port-forward: {ssh_args:?}" - ); - - let scp_args = options.additional_args_for_scp(); - assert_eq!( - scp_args, - vec![ - "-p".to_string(), - "2222".to_string(), - "-o".to_string(), - "StrictHostKeyChecking=no".to_string(), - ] - ); - } -} diff --git a/crates/remote/src/transport/wsl.rs b/crates/remote/src/transport/wsl.rs deleted file mode 100644 index d27648e678..0000000000 --- a/crates/remote/src/transport/wsl.rs +++ /dev/null @@ -1,607 +0,0 @@ -use crate::{ - RemoteClientDelegate, RemotePlatform, - remote_client::{CommandTemplate, RemoteConnection, RemoteConnectionOptions}, - transport::{parse_platform, parse_shell}, -}; -use anyhow::{Context, Result, anyhow, bail}; -use async_trait::async_trait; -use collections::HashMap; -use futures::channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender}; -use gpui::{App, AppContext as _, AsyncApp, Task}; -use release_channel::{AppVersion, ReleaseChannel}; -use rpc::proto::Envelope; -use semver::Version; -use smol::{fs, process}; -use std::{ - ffi::OsStr, - fmt::Write as _, - path::{Path, PathBuf}, - process::Stdio, - sync::Arc, - time::Instant, -}; -use util::{ - paths::{PathStyle, RemotePathBuf}, - rel_path::RelPath, - shell::{Shell, ShellKind}, - shell_builder::ShellBuilder, -}; - -#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Deserialize, schemars::JsonSchema)] -pub struct WslConnectionOptions { - pub distro_name: String, - pub user: Option, -} - -impl From for WslConnectionOptions { - fn from(val: settings::WslConnection) -> Self { - WslConnectionOptions { - distro_name: val.distro_name.into(), - user: val.user, - } - } -} - -#[derive(Debug)] -pub(crate) struct WslRemoteConnection { - remote_binary_path: Option>, - platform: RemotePlatform, - shell: String, - shell_kind: ShellKind, - default_system_shell: String, - has_wsl_interop: bool, - connection_options: WslConnectionOptions, -} - -impl WslRemoteConnection { - pub(crate) async fn new( - connection_options: WslConnectionOptions, - delegate: Arc, - cx: &mut AsyncApp, - ) -> Result { - log::info!( - "Connecting to WSL distro {} with user {:?}", - connection_options.distro_name, - connection_options.user - ); - let (release_channel, version) = - cx.update(|cx| (ReleaseChannel::global(cx), AppVersion::global(cx)))?; - - let mut this = Self { - connection_options, - remote_binary_path: None, - platform: RemotePlatform { os: "", arch: "" }, - shell: String::new(), - shell_kind: ShellKind::Posix, - default_system_shell: String::from("/bin/sh"), - has_wsl_interop: false, - }; - delegate.set_status(Some("Detecting WSL environment"), cx); - this.shell = this - .detect_shell() - .await - .context("failed detecting shell")?; - log::info!("Remote shell discovered: {}", this.shell); - this.shell_kind = ShellKind::new(&this.shell, false); - this.has_wsl_interop = this.detect_has_wsl_interop().await.unwrap_or_default(); - log::info!( - "Remote has wsl interop {}", - if this.has_wsl_interop { - "enabled" - } else { - "disabled" - } - ); - this.platform = this - .detect_platform() - .await - .context("failed detecting platform")?; - log::info!("Remote platform discovered: {:?}", this.platform); - this.remote_binary_path = Some( - this.ensure_server_binary(&delegate, release_channel, version, cx) - .await - .context("failed ensuring server binary")?, - ); - log::debug!("Detected WSL environment: {this:#?}"); - - Ok(this) - } - - async fn detect_platform(&self) -> Result { - let program = self.shell_kind.prepend_command_prefix("uname"); - let output = self.run_wsl_command_with_output(&program, &["-sm"]).await?; - parse_platform(&output) - } - - async fn detect_shell(&self) -> Result { - const DEFAULT_SHELL: &str = "sh"; - match self - .run_wsl_command_with_output("sh", &["-c", "echo $SHELL"]) - .await - { - Ok(output) => Ok(parse_shell(&output, DEFAULT_SHELL)), - Err(e) => { - log::error!("Failed to detect remote shell: {e}"); - Ok(DEFAULT_SHELL.to_owned()) - } - } - } - - async fn detect_has_wsl_interop(&self) -> Result { - Ok(self - .run_wsl_command_with_output("cat", &["/proc/sys/fs/binfmt_misc/WSLInterop"]) - .await - .inspect_err(|err| log::error!("Failed to detect wsl interop: {err}"))? - .contains("enabled")) - } - - async fn windows_path_to_wsl_path(&self, source: &Path) -> Result { - windows_path_to_wsl_path_impl(&self.connection_options, source).await - } - - async fn run_wsl_command_with_output(&self, program: &str, args: &[&str]) -> Result { - run_wsl_command_with_output_impl(&self.connection_options, program, args).await - } - - async fn run_wsl_command(&self, program: &str, args: &[&str]) -> Result<()> { - run_wsl_command_impl(&self.connection_options, program, args, false) - .await - .map(|_| ()) - } - - async fn ensure_server_binary( - &self, - delegate: &Arc, - release_channel: ReleaseChannel, - version: Version, - cx: &mut AsyncApp, - ) -> Result> { - let version_str = match release_channel { - ReleaseChannel::Dev => "build".to_string(), - _ => version.to_string(), - }; - - let binary_name = format!( - "zed-remote-server-{}-{}", - release_channel.dev_name(), - version_str - ); - - let dst_path = - paths::remote_wsl_server_dir_relative().join(RelPath::unix(&binary_name).unwrap()); - - if let Some(parent) = dst_path.parent() { - let parent = parent.display(PathStyle::Posix); - let mkdir = self.shell_kind.prepend_command_prefix("mkdir"); - self.run_wsl_command(&mkdir, &["-p", &parent]) - .await - .map_err(|e| anyhow!("Failed to create directory: {}", e))?; - } - - #[cfg(debug_assertions)] - if let Some(remote_server_path) = - super::build_remote_server_from_source(&self.platform, delegate.as_ref(), cx).await? - { - let tmp_path = paths::remote_wsl_server_dir_relative().join( - &RelPath::unix(&format!( - "download-{}-{}", - std::process::id(), - remote_server_path.file_name().unwrap().to_string_lossy() - )) - .unwrap(), - ); - self.upload_file(&remote_server_path, &tmp_path, delegate, cx) - .await?; - self.extract_and_install(&tmp_path, &dst_path, delegate, cx) - .await?; - return Ok(dst_path); - } - - if self - .run_wsl_command(&dst_path.display(PathStyle::Posix), &["version"]) - .await - .is_ok() - { - return Ok(dst_path); - } - - let wanted_version = match release_channel { - ReleaseChannel::Nightly | ReleaseChannel::Dev => None, - _ => Some(cx.update(|cx| AppVersion::global(cx))?), - }; - - let src_path = delegate - .download_server_binary_locally(self.platform, release_channel, wanted_version, cx) - .await?; - - let tmp_path = format!( - "{}.{}.gz", - dst_path.display(PathStyle::Posix), - std::process::id() - ); - let tmp_path = RelPath::unix(&tmp_path).unwrap(); - - self.upload_file(&src_path, &tmp_path, delegate, cx).await?; - self.extract_and_install(&tmp_path, &dst_path, delegate, cx) - .await?; - - Ok(dst_path) - } - - async fn upload_file( - &self, - src_path: &Path, - dst_path: &RelPath, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - delegate.set_status(Some("Uploading remote server"), cx); - - if let Some(parent) = dst_path.parent() { - let parent = parent.display(PathStyle::Posix); - let mkdir = self.shell_kind.prepend_command_prefix("mkdir"); - self.run_wsl_command(&mkdir, &["-p", &parent]) - .await - .context("Failed to create directory when uploading file")?; - } - - let t0 = Instant::now(); - let src_stat = fs::metadata(&src_path) - .await - .with_context(|| format!("source path does not exist: {}", src_path.display()))?; - let size = src_stat.len(); - log::info!( - "uploading remote server to WSL {:?} ({}kb)", - dst_path, - size / 1024 - ); - - let src_path_in_wsl = self.windows_path_to_wsl_path(src_path).await?; - let cp = self.shell_kind.prepend_command_prefix("cp"); - self.run_wsl_command( - &cp, - &["-f", &src_path_in_wsl, &dst_path.display(PathStyle::Posix)], - ) - .await - .map_err(|e| { - anyhow!( - "Failed to copy file {}({}) to WSL {:?}: {}", - src_path.display(), - src_path_in_wsl, - dst_path, - e - ) - })?; - - log::info!("uploaded remote server in {:?}", t0.elapsed()); - Ok(()) - } - - async fn extract_and_install( - &self, - tmp_path: &RelPath, - dst_path: &RelPath, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - delegate.set_status(Some("Extracting remote server"), cx); - - let tmp_path_str = tmp_path.display(PathStyle::Posix); - let dst_path_str = dst_path.display(PathStyle::Posix); - - // Build extraction script with proper error handling - let script = if tmp_path_str.ends_with(".gz") { - let uncompressed = tmp_path_str.trim_end_matches(".gz"); - format!( - "set -e; gunzip -f '{}' && chmod 755 '{}' && mv -f '{}' '{}'", - tmp_path_str, uncompressed, uncompressed, dst_path_str - ) - } else { - format!( - "set -e; chmod 755 '{}' && mv -f '{}' '{}'", - tmp_path_str, tmp_path_str, dst_path_str - ) - }; - - self.run_wsl_command("sh", &["-c", &script]) - .await - .map_err(|e| anyhow!("Failed to extract server binary: {}", e))?; - Ok(()) - } -} - -#[async_trait(?Send)] -impl RemoteConnection for WslRemoteConnection { - fn start_proxy( - &self, - unique_identifier: String, - reconnect: bool, - incoming_tx: UnboundedSender, - outgoing_rx: UnboundedReceiver, - connection_activity_tx: Sender<()>, - delegate: Arc, - cx: &mut AsyncApp, - ) -> Task> { - delegate.set_status(Some("Starting proxy"), cx); - - let Some(remote_binary_path) = &self.remote_binary_path else { - return Task::ready(Err(anyhow!("Remote binary path not set"))); - }; - - let mut proxy_args = vec![]; - for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] { - if let Some(value) = std::env::var(env_var).ok() { - // We don't quote the value here as it seems excessive and may result in invalid envs for the - // proxy server. For example, `RUST_LOG='debug'` will result in a warning "invalid logging spec 'debug'', ignoring it" - // in the proxy server. Therefore, we pass the env vars as is. - proxy_args.push(format!("{}={}", env_var, value)); - } - } - - proxy_args.push(remote_binary_path.display(PathStyle::Posix).into_owned()); - proxy_args.push("proxy".to_owned()); - proxy_args.push("--identifier".to_owned()); - proxy_args.push(unique_identifier); - - if reconnect { - proxy_args.push("--reconnect".to_owned()); - } - - let proxy_process = - match wsl_command_impl(&self.connection_options, "env", &proxy_args, false) - .kill_on_drop(true) - .spawn() - { - Ok(process) => process, - Err(error) => { - return Task::ready(Err(anyhow!("failed to spawn remote server: {}", error))); - } - }; - - super::handle_rpc_messages_over_child_process_stdio( - proxy_process, - incoming_tx, - outgoing_rx, - connection_activity_tx, - cx, - ) - } - - fn upload_directory( - &self, - src_path: PathBuf, - dest_path: RemotePathBuf, - cx: &App, - ) -> Task> { - cx.background_spawn({ - let options = self.connection_options.clone(); - async move { - let wsl_src = windows_path_to_wsl_path_impl(&options, &src_path).await?; - - run_wsl_command_impl( - &options, - "cp", - &["-r", &wsl_src, &dest_path.to_string()], - true, - ) - .await - .map_err(|e| { - anyhow!( - "failed to upload directory {} -> {}: {}", - src_path.display(), - dest_path, - e - ) - })?; - - Ok(()) - } - }) - } - - async fn kill(&self) -> Result<()> { - Ok(()) - } - - fn has_been_killed(&self) -> bool { - false - } - - fn shares_network_interface(&self) -> bool { - true - } - - fn build_command( - &self, - program: Option, - args: &[String], - env: &HashMap, - working_dir: Option, - port_forward: Option<(u16, String, u16)>, - ) -> Result { - if port_forward.is_some() { - bail!("WSL shares the network interface with the host system"); - } - - let shell_kind = self.shell_kind; - let working_dir = working_dir - .map(|working_dir| RemotePathBuf::new(working_dir, PathStyle::Posix).to_string()) - .unwrap_or("~".to_string()); - - let mut exec = String::from("exec env "); - - for (k, v) in env.iter() { - write!( - exec, - "{}={} ", - k, - shell_kind.try_quote(v).context("shell quoting")? - )?; - } - - if let Some(program) = program { - write!( - exec, - "{}", - shell_kind - .try_quote_prefix_aware(&program) - .context("shell quoting")? - )?; - for arg in args { - let arg = shell_kind.try_quote(&arg).context("shell quoting")?; - write!(exec, " {}", &arg)?; - } - } else { - write!(&mut exec, "{} -l", self.shell)?; - } - let (command, args) = - ShellBuilder::new(&Shell::Program(self.shell.clone()), false).build(Some(exec), &[]); - - let mut wsl_args = if let Some(user) = &self.connection_options.user { - vec![ - "--distribution".to_string(), - self.connection_options.distro_name.clone(), - "--user".to_string(), - user.clone(), - "--cd".to_string(), - working_dir, - "--".to_string(), - command, - ] - } else { - vec![ - "--distribution".to_string(), - self.connection_options.distro_name.clone(), - "--cd".to_string(), - working_dir, - "--".to_string(), - command, - ] - }; - wsl_args.extend(args); - - Ok(CommandTemplate { - program: "wsl.exe".to_string(), - args: wsl_args, - env: HashMap::default(), - }) - } - - fn build_forward_ports_command( - &self, - _: Vec<(u16, String, u16)>, - ) -> anyhow::Result { - Err(anyhow!("WSL shares a network interface with the host")) - } - - fn connection_options(&self) -> RemoteConnectionOptions { - RemoteConnectionOptions::Wsl(self.connection_options.clone()) - } - - fn path_style(&self) -> PathStyle { - PathStyle::Posix - } - - fn shell(&self) -> String { - self.shell.clone() - } - - fn default_system_shell(&self) -> String { - self.default_system_shell.clone() - } - - fn has_wsl_interop(&self) -> bool { - self.has_wsl_interop - } -} - -/// `wslpath` is a executable available in WSL, it's a linux binary. -/// So it doesn't support Windows style paths. -async fn sanitize_path(path: &Path) -> Result { - let path = smol::fs::canonicalize(path) - .await - .with_context(|| format!("Failed to canonicalize path {}", path.display()))?; - let path_str = path.to_string_lossy(); - - let sanitized = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str); - Ok(sanitized.replace('\\', "/")) -} - -async fn run_wsl_command_with_output_impl( - options: &WslConnectionOptions, - program: &str, - args: &[&str], -) -> Result { - match run_wsl_command_impl(options, program, args, true).await { - Ok(res) => Ok(res), - Err(exec_err) => match run_wsl_command_impl(options, program, args, false).await { - Ok(res) => Ok(res), - Err(e) => Err(e.context(exec_err)), - }, - } -} - -async fn windows_path_to_wsl_path_impl( - options: &WslConnectionOptions, - source: &Path, -) -> Result { - let source = sanitize_path(source).await?; - run_wsl_command_with_output_impl(options, "wslpath", &["-u", &source]).await -} - -async fn run_wsl_command_impl( - options: &WslConnectionOptions, - program: &str, - args: &[&str], - exec: bool, -) -> Result { - let mut command = wsl_command_impl(options, program, args, exec); - let output = command - .output() - .await - .with_context(|| format!("Failed to run command '{:?}'", command))?; - - if !output.status.success() { - return Err(anyhow!( - "Command '{:?}' failed: {}", - command, - String::from_utf8_lossy(&output.stderr).trim() - )); - } - - Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned()) -} - -/// Creates a new `wsl.exe` command that runs the given program with the given arguments. -/// -/// If `exec` is true, the command will be executed in the WSL environment without spawning a new shell. -fn wsl_command_impl( - options: &WslConnectionOptions, - program: &str, - args: &[impl AsRef], - exec: bool, -) -> process::Command { - let mut command = util::command::new_smol_command("wsl.exe"); - - if let Some(user) = &options.user { - command.arg("--user").arg(user); - } - - command - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .arg("--distribution") - .arg(&options.distro_name) - .arg("--cd") - .arg("~"); - - if exec { - command.arg("--exec"); - } - - command.arg(program).args(args); - - log::debug!("wsl {:?}", command); - command -} diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml deleted file mode 100644 index 114dc777c1..0000000000 --- a/crates/remote_server/Cargo.toml +++ /dev/null @@ -1,109 +0,0 @@ -[package] -name = "remote_server" -description = "Daemon used for remote editing" -edition.workspace = true -version = "0.1.0" -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/remote_server.rs" -doctest = false - -[[bin]] -name = "remote_server" - -[features] -default = [] -debug-embed = ["dep:rust-embed"] -test-support = ["fs/test-support"] - -[dependencies] -anyhow.workspace = true -askpass.workspace = true -clap.workspace = true -client.workspace = true -dap_adapters.workspace = true -debug_adapter_extension.workspace = true -env_logger.workspace = true -extension.workspace = true -extension_host.workspace = true -fs.workspace = true -futures.workspace = true -git.workspace = true -git_hosting_providers.workspace = true -git2 = { workspace = true, features = ["vendored-libgit2"] } -gpui.workspace = true -gpui_tokio.workspace = true -http_client.workspace = true -image.workspace = true -json_schema_store.workspace = true -language.workspace = true -language_extension.workspace = true -languages.workspace = true -log.workspace = true -lsp.workspace = true -node_runtime.workspace = true -paths.workspace = true -project.workspace = true -proto.workspace = true -release_channel.workspace = true -remote.workspace = true -reqwest_client.workspace = true -rpc.workspace = true -rust-embed = { workspace = true, optional = true, features = ["debug-embed"] } -semver.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -shellexpand.workspace = true -smol.workspace = true -sysinfo.workspace = true -task.workspace = true -util.workspace = true -watch.workspace = true -worktree.workspace = true -thiserror.workspace = true -rayon.workspace = true - -[target.'cfg(not(windows))'.dependencies] -crashes.workspace = true -crash-handler.workspace = true -fork.workspace = true -libc.workspace = true -minidumper.workspace = true - -[dev-dependencies] -action_log.workspace = true -agent = { workspace = true, features = ["test-support"] } -client = { workspace = true, features = ["test-support"] } -clock = { workspace = true, features = ["test-support"] } -collections.workspace = true -dap = { workspace = true, features = ["test-support"] } -editor = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } -fs = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -http_client = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -node_runtime = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true -project = { workspace = true, features = ["test-support"] } -remote = { workspace = true, features = ["test-support"] } -theme = { workspace = true, features = ["test-support"] } -language_model = { workspace = true, features = ["test-support"] } -lsp = { workspace = true, features = ["test-support"] } -prompt_store.workspace = true -unindent.workspace = true -serde_json.workspace = true -zlog.workspace = true - -[build-dependencies] -cargo_toml.workspace = true -toml.workspace = true - -[package.metadata.cargo-machete] -ignored = ["git2", "rust-embed", "paths"] diff --git a/crates/remote_server/LICENSE-GPL b/crates/remote_server/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/remote_server/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/remote_server/build.rs b/crates/remote_server/build.rs deleted file mode 100644 index 3ad13d3d6e..0000000000 --- a/crates/remote_server/build.rs +++ /dev/null @@ -1,34 +0,0 @@ -#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")] -use std::process::Command; - -const ZED_MANIFEST: &str = include_str!("../zed/Cargo.toml"); - -fn main() { - let zed_cargo_toml: cargo_toml::Manifest = - toml::from_str(ZED_MANIFEST).expect("failed to parse zed Cargo.toml"); - println!( - "cargo:rustc-env=ZED_PKG_VERSION={}", - zed_cargo_toml.package.unwrap().version.unwrap() - ); - println!( - "cargo:rustc-env=TARGET={}", - std::env::var("TARGET").unwrap() - ); - - // Populate git sha environment variable if git is available - println!("cargo:rerun-if-changed=../../.git/logs/HEAD"); - if let Some(output) = Command::new("git") - .args(["rev-parse", "HEAD"]) - .output() - .ok() - .filter(|output| output.status.success()) - { - let git_sha = String::from_utf8_lossy(&output.stdout); - let git_sha = git_sha.trim(); - - println!("cargo:rustc-env=ZED_COMMIT_SHA={git_sha}"); - } - if let Some(build_identifier) = option_env!("GITHUB_RUN_NUMBER") { - println!("cargo:rustc-env=ZED_BUILD_ID={build_identifier}"); - } -} diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs deleted file mode 100644 index 361e74579c..0000000000 --- a/crates/remote_server/src/headless_project.rs +++ /dev/null @@ -1,893 +0,0 @@ -use anyhow::{Context as _, Result, anyhow}; -use language::File; -use lsp::LanguageServerId; - -use extension::ExtensionHostProxy; -use extension_host::headless_host::HeadlessExtensionStore; -use fs::Fs; -use gpui::{App, AppContext as _, AsyncApp, Context, Entity, PromptLevel}; -use http_client::HttpClient; -use language::{Buffer, BufferEvent, LanguageRegistry, proto::serialize_operation}; -use node_runtime::NodeRuntime; -use project::{ - LspStore, LspStoreEvent, ManifestTree, PrettierStore, ProjectEnvironment, ProjectPath, - ToolchainStore, WorktreeId, - agent_server_store::AgentServerStore, - buffer_store::{BufferStore, BufferStoreEvent}, - debugger::{breakpoint_store::BreakpointStore, dap_store::DapStore}, - git_store::GitStore, - image_store::ImageId, - lsp_store::log_store::{self, GlobalLogStore, LanguageServerKind, LogKind}, - project_settings::SettingsObserver, - search::SearchQuery, - task_store::TaskStore, - worktree_store::WorktreeStore, -}; -use rpc::{ - AnyProtoClient, TypedEnvelope, - proto::{self, REMOTE_SERVER_PEER_ID, REMOTE_SERVER_PROJECT_ID}, -}; - -use settings::initial_server_settings_content; -use smol::stream::StreamExt; -use std::{ - num::NonZeroU64, - path::{Path, PathBuf}, - sync::{ - Arc, - atomic::{AtomicU64, AtomicUsize, Ordering}, - }, -}; -use sysinfo::{ProcessRefreshKind, RefreshKind, System, UpdateKind}; -use util::{ResultExt, paths::PathStyle, rel_path::RelPath}; -use worktree::Worktree; - -pub struct HeadlessProject { - pub fs: Arc, - pub session: AnyProtoClient, - pub worktree_store: Entity, - pub buffer_store: Entity, - pub lsp_store: Entity, - pub task_store: Entity, - pub dap_store: Entity, - pub agent_server_store: Entity, - pub settings_observer: Entity, - pub next_entry_id: Arc, - pub languages: Arc, - pub extensions: Entity, - pub git_store: Entity, - pub environment: Entity, - // Used mostly to keep alive the toolchain store for RPC handlers. - // Local variant is used within LSP store, but that's a separate entity. - pub _toolchain_store: Entity, -} - -pub struct HeadlessAppState { - pub session: AnyProtoClient, - pub fs: Arc, - pub http_client: Arc, - pub node_runtime: NodeRuntime, - pub languages: Arc, - pub extension_host_proxy: Arc, -} - -impl HeadlessProject { - pub fn init(cx: &mut App) { - settings::init(cx); - log_store::init(true, cx); - } - - pub fn new( - HeadlessAppState { - session, - fs, - http_client, - node_runtime, - languages, - extension_host_proxy: proxy, - }: HeadlessAppState, - cx: &mut Context, - ) -> Self { - debug_adapter_extension::init(proxy.clone(), cx); - languages::init(languages.clone(), fs.clone(), node_runtime.clone(), cx); - - let worktree_store = cx.new(|cx| { - let mut store = WorktreeStore::local(true, fs.clone()); - store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); - store - }); - - let environment = - cx.new(|cx| ProjectEnvironment::new(None, worktree_store.downgrade(), None, true, cx)); - let manifest_tree = ManifestTree::new(worktree_store.clone(), cx); - let toolchain_store = cx.new(|cx| { - ToolchainStore::local( - languages.clone(), - worktree_store.clone(), - environment.clone(), - manifest_tree.clone(), - fs.clone(), - cx, - ) - }); - - let buffer_store = cx.new(|cx| { - let mut buffer_store = BufferStore::local(worktree_store.clone(), cx); - buffer_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); - buffer_store - }); - - let breakpoint_store = - cx.new(|_| BreakpointStore::local(worktree_store.clone(), buffer_store.clone())); - - let dap_store = cx.new(|cx| { - let mut dap_store = DapStore::new_local( - http_client.clone(), - node_runtime.clone(), - fs.clone(), - environment.clone(), - toolchain_store.read(cx).as_language_toolchain_store(), - worktree_store.clone(), - breakpoint_store.clone(), - true, - cx, - ); - dap_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); - dap_store - }); - - let git_store = cx.new(|cx| { - let mut store = GitStore::local( - &worktree_store, - buffer_store.clone(), - environment.clone(), - fs.clone(), - cx, - ); - store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); - store - }); - - let prettier_store = cx.new(|cx| { - PrettierStore::new( - node_runtime.clone(), - fs.clone(), - languages.clone(), - worktree_store.clone(), - cx, - ) - }); - - let task_store = cx.new(|cx| { - let mut task_store = TaskStore::local( - buffer_store.downgrade(), - worktree_store.clone(), - toolchain_store.read(cx).as_language_toolchain_store(), - environment.clone(), - cx, - ); - task_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); - task_store - }); - let settings_observer = cx.new(|cx| { - let mut observer = SettingsObserver::new_local( - fs.clone(), - worktree_store.clone(), - task_store.clone(), - cx, - ); - observer.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); - observer - }); - - let lsp_store = cx.new(|cx| { - let mut lsp_store = LspStore::new_local( - buffer_store.clone(), - worktree_store.clone(), - prettier_store.clone(), - toolchain_store - .read(cx) - .as_local_store() - .expect("Toolchain store to be local") - .clone(), - environment.clone(), - manifest_tree, - languages.clone(), - http_client.clone(), - fs.clone(), - cx, - ); - lsp_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); - lsp_store - }); - - let agent_server_store = cx.new(|cx| { - let mut agent_server_store = AgentServerStore::local( - node_runtime.clone(), - fs.clone(), - environment.clone(), - http_client.clone(), - cx, - ); - agent_server_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); - agent_server_store - }); - - cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); - language_extension::init( - language_extension::LspAccess::ViaLspStore(lsp_store.clone()), - proxy.clone(), - languages.clone(), - ); - - cx.subscribe(&buffer_store, |_this, _buffer_store, event, cx| { - if let BufferStoreEvent::BufferAdded(buffer) = event { - cx.subscribe(buffer, Self::on_buffer_event).detach(); - } - }) - .detach(); - - let extensions = HeadlessExtensionStore::new( - fs.clone(), - http_client.clone(), - paths::remote_extensions_dir().to_path_buf(), - proxy, - node_runtime, - cx, - ); - - // local_machine -> ssh handlers - session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &worktree_store); - session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &buffer_store); - session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &cx.entity()); - session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &lsp_store); - session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &task_store); - session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &toolchain_store); - session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &dap_store); - session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &settings_observer); - session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &git_store); - session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &agent_server_store); - - session.add_request_handler(cx.weak_entity(), Self::handle_list_remote_directory); - session.add_request_handler(cx.weak_entity(), Self::handle_get_path_metadata); - session.add_request_handler(cx.weak_entity(), Self::handle_shutdown_remote_server); - session.add_request_handler(cx.weak_entity(), Self::handle_ping); - session.add_request_handler(cx.weak_entity(), Self::handle_get_processes); - - session.add_entity_request_handler(Self::handle_add_worktree); - session.add_request_handler(cx.weak_entity(), Self::handle_remove_worktree); - - session.add_entity_request_handler(Self::handle_open_buffer_by_path); - session.add_entity_request_handler(Self::handle_open_new_buffer); - session.add_entity_request_handler(Self::handle_find_search_candidates); - session.add_entity_request_handler(Self::handle_open_server_settings); - session.add_entity_request_handler(Self::handle_get_directory_environment); - session.add_entity_message_handler(Self::handle_toggle_lsp_logs); - session.add_entity_request_handler(Self::handle_open_image_by_path); - - session.add_entity_request_handler(BufferStore::handle_update_buffer); - session.add_entity_message_handler(BufferStore::handle_close_buffer); - - session.add_request_handler( - extensions.downgrade(), - HeadlessExtensionStore::handle_sync_extensions, - ); - session.add_request_handler( - extensions.downgrade(), - HeadlessExtensionStore::handle_install_extension, - ); - - BufferStore::init(&session); - WorktreeStore::init(&session); - SettingsObserver::init(&session); - LspStore::init(&session); - TaskStore::init(Some(&session)); - ToolchainStore::init(&session); - DapStore::init(&session, cx); - // todo(debugger): Re init breakpoint store when we set it up for collab - // BreakpointStore::init(&client); - GitStore::init(&session); - AgentServerStore::init_headless(&session); - - HeadlessProject { - next_entry_id: Default::default(), - session, - settings_observer, - fs, - worktree_store, - buffer_store, - lsp_store, - task_store, - dap_store, - agent_server_store, - languages, - extensions, - git_store, - environment, - _toolchain_store: toolchain_store, - } - } - - fn on_buffer_event( - &mut self, - buffer: Entity, - event: &BufferEvent, - cx: &mut Context, - ) { - if let BufferEvent::Operation { - operation, - is_local: true, - } = event - { - cx.background_spawn(self.session.request(proto::UpdateBuffer { - project_id: REMOTE_SERVER_PROJECT_ID, - buffer_id: buffer.read(cx).remote_id().to_proto(), - operations: vec![serialize_operation(operation)], - })) - .detach() - } - } - - fn on_lsp_store_event( - &mut self, - lsp_store: Entity, - event: &LspStoreEvent, - cx: &mut Context, - ) { - match event { - LspStoreEvent::LanguageServerAdded(id, name, worktree_id) => { - let log_store = cx - .try_global::() - .map(|lsp_logs| lsp_logs.0.clone()); - if let Some(log_store) = log_store { - log_store.update(cx, |log_store, cx| { - log_store.add_language_server( - LanguageServerKind::LocalSsh { - lsp_store: self.lsp_store.downgrade(), - }, - *id, - Some(name.clone()), - *worktree_id, - lsp_store.read(cx).language_server_for_id(*id), - cx, - ); - }); - } - } - LspStoreEvent::LanguageServerRemoved(id) => { - let log_store = cx - .try_global::() - .map(|lsp_logs| lsp_logs.0.clone()); - if let Some(log_store) = log_store { - log_store.update(cx, |log_store, cx| { - log_store.remove_language_server(*id, cx); - }); - } - } - LspStoreEvent::LanguageServerUpdate { - language_server_id, - name, - message, - } => { - self.session - .send(proto::UpdateLanguageServer { - project_id: REMOTE_SERVER_PROJECT_ID, - server_name: name.as_ref().map(|name| name.to_string()), - language_server_id: language_server_id.to_proto(), - variant: Some(message.clone()), - }) - .log_err(); - } - LspStoreEvent::Notification(message) => { - self.session - .send(proto::Toast { - project_id: REMOTE_SERVER_PROJECT_ID, - notification_id: "lsp".to_string(), - message: message.clone(), - }) - .log_err(); - } - LspStoreEvent::LanguageServerPrompt(prompt) => { - let request = self.session.request(proto::LanguageServerPromptRequest { - project_id: REMOTE_SERVER_PROJECT_ID, - actions: prompt - .actions - .iter() - .map(|action| action.title.to_string()) - .collect(), - level: Some(prompt_to_proto(prompt)), - lsp_name: prompt.lsp_name.clone(), - message: prompt.message.clone(), - }); - let prompt = prompt.clone(); - cx.background_spawn(async move { - let response = request.await?; - if let Some(action_response) = response.action_response { - prompt.respond(action_response as usize).await; - } - anyhow::Ok(()) - }) - .detach(); - } - _ => {} - } - } - - pub async fn handle_add_worktree( - this: Entity, - message: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - use client::ErrorCodeExt; - let fs = this.read_with(&cx, |this, _| this.fs.clone())?; - let path = PathBuf::from(shellexpand::tilde(&message.payload.path).to_string()); - - let canonicalized = match fs.canonicalize(&path).await { - Ok(path) => path, - Err(e) => { - let mut parent = path - .parent() - .ok_or(e) - .with_context(|| format!("{path:?} does not exist"))?; - if parent == Path::new("") { - parent = util::paths::home_dir(); - } - let parent = fs.canonicalize(parent).await.map_err(|_| { - anyhow!( - proto::ErrorCode::DevServerProjectPathDoesNotExist - .with_tag("path", path.to_string_lossy().as_ref()) - ) - })?; - parent.join(path.file_name().unwrap()) - } - }; - - let worktree = this - .read_with(&cx.clone(), |this, _| { - Worktree::local( - Arc::from(canonicalized.as_path()), - message.payload.visible, - this.fs.clone(), - this.next_entry_id.clone(), - true, - &mut cx, - ) - })? - .await?; - - let response = this.read_with(&cx, |_, cx| { - let worktree = worktree.read(cx); - proto::AddWorktreeResponse { - worktree_id: worktree.id().to_proto(), - canonicalized_path: canonicalized.to_string_lossy().into_owned(), - } - })?; - - // We spawn this asynchronously, so that we can send the response back - // *before* `worktree_store.add()` can send out UpdateProject requests - // to the client about the new worktree. - // - // That lets the client manage the reference/handles of the newly-added - // worktree, before getting interrupted by an UpdateProject request. - // - // This fixes the problem of the client sending the AddWorktree request, - // headless project sending out a project update, client receiving it - // and immediately dropping the reference of the new client, causing it - // to be dropped on the headless project, and the client only then - // receiving a response to AddWorktree. - cx.spawn(async move |cx| { - this.update(cx, |this, cx| { - this.worktree_store.update(cx, |worktree_store, cx| { - worktree_store.add(&worktree, cx); - }); - }) - .log_err(); - }) - .detach(); - - Ok(response) - } - - pub async fn handle_remove_worktree( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - this.update(&mut cx, |this, cx| { - this.worktree_store.update(cx, |worktree_store, cx| { - worktree_store.remove_worktree(worktree_id, cx); - }); - })?; - Ok(proto::Ack {}) - } - - pub async fn handle_open_buffer_by_path( - this: Entity, - message: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let worktree_id = WorktreeId::from_proto(message.payload.worktree_id); - let path = RelPath::from_proto(&message.payload.path)?; - let (buffer_store, buffer) = this.update(&mut cx, |this, cx| { - let buffer_store = this.buffer_store.clone(); - let buffer = this.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.open_buffer(ProjectPath { worktree_id, path }, cx) - }); - anyhow::Ok((buffer_store, buffer)) - })??; - - let buffer = buffer.await?; - let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?; - buffer_store.update(&mut cx, |buffer_store, cx| { - buffer_store - .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx) - .detach_and_log_err(cx); - })?; - - Ok(proto::OpenBufferResponse { - buffer_id: buffer_id.to_proto(), - }) - } - - pub async fn handle_open_image_by_path( - this: Entity, - message: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - static NEXT_ID: AtomicU64 = AtomicU64::new(1); - let worktree_id = WorktreeId::from_proto(message.payload.worktree_id); - let path = RelPath::from_proto(&message.payload.path)?; - let project_id = message.payload.project_id; - use proto::create_image_for_peer::Variant; - - let (worktree_store, session) = this.read_with(&cx, |this, _| { - (this.worktree_store.clone(), this.session.clone()) - })?; - - let worktree = worktree_store - .read_with(&cx, |store, cx| store.worktree_for_id(worktree_id, cx))? - .context("worktree not found")?; - - let load_task = worktree.update(&mut cx, |worktree, cx| { - worktree.load_binary_file(path.as_ref(), cx) - })?; - - let loaded_file = load_task.await?; - let content = loaded_file.content; - let file = loaded_file.file; - - let proto_file = worktree.read_with(&cx, |_worktree, cx| file.to_proto(cx))?; - let image_id = - ImageId::from(NonZeroU64::new(NEXT_ID.fetch_add(1, Ordering::Relaxed)).unwrap()); - - let format = image::guess_format(&content) - .map(|f| format!("{:?}", f).to_lowercase()) - .unwrap_or_else(|_| "unknown".to_string()); - - let state = proto::ImageState { - id: image_id.to_proto(), - file: Some(proto_file), - content_size: content.len() as u64, - format, - }; - - session.send(proto::CreateImageForPeer { - project_id, - peer_id: Some(REMOTE_SERVER_PEER_ID), - variant: Some(Variant::State(state)), - })?; - - const CHUNK_SIZE: usize = 1024 * 1024; // 1MB chunks - for chunk in content.chunks(CHUNK_SIZE) { - session.send(proto::CreateImageForPeer { - project_id, - peer_id: Some(REMOTE_SERVER_PEER_ID), - variant: Some(Variant::Chunk(proto::ImageChunk { - image_id: image_id.to_proto(), - data: chunk.to_vec(), - })), - })?; - } - - Ok(proto::OpenImageResponse { - image_id: image_id.to_proto(), - }) - } - - pub async fn handle_open_new_buffer( - this: Entity, - _message: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let (buffer_store, buffer) = this.update(&mut cx, |this, cx| { - let buffer_store = this.buffer_store.clone(); - let buffer = this - .buffer_store - .update(cx, |buffer_store, cx| buffer_store.create_buffer(true, cx)); - anyhow::Ok((buffer_store, buffer)) - })??; - - let buffer = buffer.await?; - let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?; - buffer_store.update(&mut cx, |buffer_store, cx| { - buffer_store - .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx) - .detach_and_log_err(cx); - })?; - - Ok(proto::OpenBufferResponse { - buffer_id: buffer_id.to_proto(), - }) - } - - async fn handle_toggle_lsp_logs( - _: Entity, - envelope: TypedEnvelope, - cx: AsyncApp, - ) -> Result<()> { - let server_id = LanguageServerId::from_proto(envelope.payload.server_id); - cx.update(|cx| { - let log_store = cx - .try_global::() - .map(|global_log_store| global_log_store.0.clone()) - .context("lsp logs store is missing")?; - let toggled_log_kind = - match proto::toggle_lsp_logs::LogType::from_i32(envelope.payload.log_type) - .context("invalid log type")? - { - proto::toggle_lsp_logs::LogType::Log => LogKind::Logs, - proto::toggle_lsp_logs::LogType::Trace => LogKind::Trace, - proto::toggle_lsp_logs::LogType::Rpc => LogKind::Rpc, - }; - log_store.update(cx, |log_store, _| { - log_store.toggle_lsp_logs(server_id, envelope.payload.enabled, toggled_log_kind); - }); - anyhow::Ok(()) - })??; - - Ok(()) - } - - async fn handle_open_server_settings( - this: Entity, - _: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let settings_path = paths::settings_file(); - let (worktree, path) = this - .update(&mut cx, |this, cx| { - this.worktree_store.update(cx, |worktree_store, cx| { - worktree_store.find_or_create_worktree(settings_path, false, cx) - }) - })? - .await?; - - let (buffer, buffer_store) = this.update(&mut cx, |this, cx| { - let buffer = this.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.open_buffer( - ProjectPath { - worktree_id: worktree.read(cx).id(), - path: path, - }, - cx, - ) - }); - - (buffer, this.buffer_store.clone()) - })?; - - let buffer = buffer.await?; - - let buffer_id = cx.update(|cx| { - if buffer.read(cx).is_empty() { - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, initial_server_settings_content())], None, cx) - }); - } - - let buffer_id = buffer.read(cx).remote_id(); - - buffer_store.update(cx, |buffer_store, cx| { - buffer_store - .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx) - .detach_and_log_err(cx); - }); - - buffer_id - })?; - - Ok(proto::OpenBufferResponse { - buffer_id: buffer_id.to_proto(), - }) - } - - async fn handle_find_search_candidates( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let message = envelope.payload; - let query = SearchQuery::from_proto( - message.query.context("missing query field")?, - PathStyle::local(), - )?; - let results = this.update(&mut cx, |this, cx| { - project::Search::local( - this.fs.clone(), - this.buffer_store.clone(), - this.worktree_store.clone(), - message.limit as _, - cx, - ) - .into_handle(query, cx) - .matching_buffers(cx) - })?; - - let mut response = proto::FindSearchCandidatesResponse { - buffer_ids: Vec::new(), - }; - - let buffer_store = this.read_with(&cx, |this, _| this.buffer_store.clone())?; - - while let Ok(buffer) = results.recv().await { - let buffer_id = buffer.read_with(&cx, |this, _| this.remote_id())?; - response.buffer_ids.push(buffer_id.to_proto()); - buffer_store - .update(&mut cx, |buffer_store, cx| { - buffer_store.create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx) - })? - .await?; - } - - Ok(response) - } - - async fn handle_list_remote_directory( - this: Entity, - envelope: TypedEnvelope, - cx: AsyncApp, - ) -> Result { - let fs = cx.read_entity(&this, |this, _| this.fs.clone())?; - let expanded = PathBuf::from(shellexpand::tilde(&envelope.payload.path).to_string()); - let check_info = envelope - .payload - .config - .as_ref() - .is_some_and(|config| config.is_dir); - - let mut entries = Vec::new(); - let mut entry_info = Vec::new(); - let mut response = fs.read_dir(&expanded).await?; - while let Some(path) = response.next().await { - let path = path?; - if let Some(file_name) = path.file_name() { - entries.push(file_name.to_string_lossy().into_owned()); - if check_info { - let is_dir = fs.is_dir(&path).await; - entry_info.push(proto::EntryInfo { is_dir }); - } - } - } - Ok(proto::ListRemoteDirectoryResponse { - entries, - entry_info, - }) - } - - async fn handle_get_path_metadata( - this: Entity, - envelope: TypedEnvelope, - cx: AsyncApp, - ) -> Result { - let fs = cx.read_entity(&this, |this, _| this.fs.clone())?; - let expanded = PathBuf::from(shellexpand::tilde(&envelope.payload.path).to_string()); - - let metadata = fs.metadata(&expanded).await?; - let is_dir = metadata.map(|metadata| metadata.is_dir).unwrap_or(false); - - Ok(proto::GetPathMetadataResponse { - exists: metadata.is_some(), - is_dir, - path: expanded.to_string_lossy().into_owned(), - }) - } - - async fn handle_shutdown_remote_server( - _this: Entity, - _envelope: TypedEnvelope, - cx: AsyncApp, - ) -> Result { - cx.spawn(async move |cx| { - cx.update(|cx| { - // TODO: This is a hack, because in a headless project, shutdown isn't executed - // when calling quit, but it should be. - cx.shutdown(); - cx.quit(); - }) - }) - .detach(); - - Ok(proto::Ack {}) - } - - pub async fn handle_ping( - _this: Entity, - _envelope: TypedEnvelope, - _cx: AsyncApp, - ) -> Result { - log::debug!("Received ping from client"); - Ok(proto::Ack {}) - } - - async fn handle_get_processes( - _this: Entity, - _envelope: TypedEnvelope, - _cx: AsyncApp, - ) -> Result { - let mut processes = Vec::new(); - let refresh_kind = RefreshKind::nothing().with_processes( - ProcessRefreshKind::nothing() - .without_tasks() - .with_cmd(UpdateKind::Always), - ); - - for process in System::new_with_specifics(refresh_kind) - .processes() - .values() - { - let name = process.name().to_string_lossy().into_owned(); - let command = process - .cmd() - .iter() - .map(|s| s.to_string_lossy().into_owned()) - .collect::>(); - - processes.push(proto::ProcessInfo { - pid: process.pid().as_u32(), - name, - command, - }); - } - - processes.sort_by_key(|p| p.name.clone()); - - Ok(proto::GetProcessesResponse { processes }) - } - - async fn handle_get_directory_environment( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let shell = task::shell_from_proto(envelope.payload.shell.context("missing shell")?)?; - let directory = PathBuf::from(envelope.payload.directory); - let environment = this - .update(&mut cx, |this, cx| { - this.environment.update(cx, |environment, cx| { - environment.local_directory_environment(&shell, directory.into(), cx) - }) - })? - .await - .context("failed to get directory environment")? - .into_iter() - .collect(); - Ok(proto::DirectoryEnvironment { environment }) - } -} - -fn prompt_to_proto( - prompt: &project::LanguageServerPromptRequest, -) -> proto::language_server_prompt_request::Level { - match prompt.level { - PromptLevel::Info => proto::language_server_prompt_request::Level::Info( - proto::language_server_prompt_request::Info {}, - ), - PromptLevel::Warning => proto::language_server_prompt_request::Level::Warning( - proto::language_server_prompt_request::Warning {}, - ), - PromptLevel::Critical => proto::language_server_prompt_request::Level::Critical( - proto::language_server_prompt_request::Critical {}, - ), - } -} diff --git a/crates/remote_server/src/main.rs b/crates/remote_server/src/main.rs deleted file mode 100644 index 368c7cb639..0000000000 --- a/crates/remote_server/src/main.rs +++ /dev/null @@ -1,55 +0,0 @@ -#![cfg_attr(target_os = "windows", allow(unused, dead_code))] - -use clap::Parser; -use remote_server::Commands; -use std::path::PathBuf; - -#[derive(Parser)] -#[command(disable_version_flag = true)] -struct Cli { - #[command(subcommand)] - command: Option, - /// Used for SSH/Git password authentication, to remove the need for netcat as a dependency, - /// by having Zed act like netcat communicating over a Unix socket. - #[arg(long, hide = true)] - askpass: Option, - /// Used for recording minidumps on crashes by having the server run a separate - /// process communicating over a socket. - #[arg(long, hide = true)] - crash_handler: Option, - /// Used for loading the environment from the project. - #[arg(long, hide = true)] - printenv: bool, -} - -#[cfg(windows)] -fn main() { - unimplemented!() -} - -#[cfg(not(windows))] -fn main() -> anyhow::Result<()> { - let cli = Cli::parse(); - - if let Some(socket_path) = &cli.askpass { - askpass::main(socket_path); - return Ok(()); - } - - if let Some(socket) = &cli.crash_handler { - crashes::crash_server(socket.as_path()); - return Ok(()); - } - - if cli.printenv { - util::shell_env::print_env(); - return Ok(()); - } - - if let Some(command) = cli.command { - remote_server::run(command) - } else { - eprintln!("usage: remote "); - std::process::exit(1); - } -} diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs deleted file mode 100644 index a91d1d055d..0000000000 --- a/crates/remote_server/src/remote_editing_tests.rs +++ /dev/null @@ -1,1981 +0,0 @@ -/// todo(windows) -/// The tests in this file assume that server_cx is running on Windows too. -/// We neead to find a way to test Windows-Non-Windows interactions. -use crate::headless_project::HeadlessProject; -use agent::{AgentTool, ReadFileTool, ReadFileToolInput, Templates, Thread, ToolCallEventStream}; -use client::{Client, UserStore}; -use clock::FakeSystemClock; -use collections::{HashMap, HashSet}; -use language_model::{LanguageModelToolResultContent, fake_provider::FakeLanguageModel}; -use prompt_store::ProjectContext; - -use extension::ExtensionHostProxy; -use fs::{FakeFs, Fs}; -use gpui::{AppContext as _, Entity, SharedString, TestAppContext}; -use http_client::{BlockedHttpClient, FakeHttpClient}; -use language::{ - Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding, - language_settings::{AllLanguageSettings, language_settings}, -}; -use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind, LanguageServerName}; -use node_runtime::NodeRuntime; -use project::{ - ProgressToken, Project, - agent_server_store::AgentServerCommand, - search::{SearchQuery, SearchResult}, -}; -use remote::RemoteClient; -use serde_json::json; -use settings::{Settings, SettingsLocation, SettingsStore, initial_server_settings_content}; -use smol::stream::StreamExt; -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; -use unindent::Unindent as _; -use util::{path, rel_path::rel_path}; - -#[gpui::test] -async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - "project2": { - "README.md": "# project 2", - }, - }), - ) - .await; - fs.set_index_for_repo( - Path::new(path!("/code/project1/.git")), - &[("src/lib.rs", "fn one() -> usize { 0 }".into())], - ); - - let (project, _headless) = init_test(&fs, cx, server_cx).await; - let (worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap(); - - // The client sees the worktree's contents. - cx.executor().run_until_parked(); - let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); - worktree.update(cx, |worktree, _cx| { - assert_eq!( - worktree.paths().collect::>(), - vec![ - rel_path("README.md"), - rel_path("src"), - rel_path("src/lib.rs"), - ] - ); - }); - - // The user opens a buffer in the remote worktree. The buffer's - // contents are loaded from the remote filesystem. - let buffer = project - .update(cx, |project, cx| { - project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx) - }) - .await - .unwrap(); - let diff = project - .update(cx, |project, cx| { - project.open_unstaged_diff(buffer.clone(), cx) - }) - .await - .unwrap(); - - diff.update(cx, |diff, _| { - assert_eq!(diff.base_text_string().unwrap(), "fn one() -> usize { 0 }"); - }); - - buffer.update(cx, |buffer, cx| { - assert_eq!(buffer.text(), "fn one() -> usize { 1 }"); - let ix = buffer.text().find('1').unwrap(); - buffer.edit([(ix..ix + 1, "100")], None, cx); - }); - - // The user saves the buffer. The new contents are written to the - // remote filesystem. - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - assert_eq!( - fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(), - "fn one() -> usize { 100 }" - ); - - // A new file is created in the remote filesystem. The user - // sees the new file. - fs.save( - path!("/code/project1/src/main.rs").as_ref(), - &"fn main() {}".into(), - Default::default(), - ) - .await - .unwrap(); - cx.executor().run_until_parked(); - worktree.update(cx, |worktree, _cx| { - assert_eq!( - worktree.paths().collect::>(), - vec![ - rel_path("README.md"), - rel_path("src"), - rel_path("src/lib.rs"), - rel_path("src/main.rs"), - ] - ); - }); - - // A file that is currently open in a buffer is renamed. - fs.rename( - path!("/code/project1/src/lib.rs").as_ref(), - path!("/code/project1/src/lib2.rs").as_ref(), - Default::default(), - ) - .await - .unwrap(); - cx.executor().run_until_parked(); - buffer.update(cx, |buffer, _| { - assert_eq!(&**buffer.file().unwrap().path(), rel_path("src/lib2.rs")); - }); - - fs.set_index_for_repo( - Path::new(path!("/code/project1/.git")), - &[("src/lib2.rs", "fn one() -> usize { 100 }".into())], - ); - cx.executor().run_until_parked(); - diff.update(cx, |diff, _| { - assert_eq!( - diff.base_text_string().unwrap(), - "fn one() -> usize { 100 }" - ); - }); -} - -#[gpui::test] -async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - }), - ) - .await; - - let (project, headless) = init_test(&fs, cx, server_cx).await; - - project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap(); - - cx.run_until_parked(); - - async fn do_search(project: &Entity, mut cx: TestAppContext) -> Entity { - let receiver = project.update(&mut cx, |project, cx| { - project.search( - SearchQuery::text( - "project", - false, - true, - false, - Default::default(), - Default::default(), - false, - None, - ) - .unwrap(), - cx, - ) - }); - - let first_response = receiver.recv().await.unwrap(); - let SearchResult::Buffer { buffer, .. } = first_response else { - panic!("incorrect result"); - }; - buffer.update(&mut cx, |buffer, cx| { - assert_eq!( - buffer.file().unwrap().full_path(cx).to_string_lossy(), - path!("project1/README.md") - ) - }); - - assert!(receiver.recv().await.is_err()); - buffer - } - - let buffer = do_search(&project, cx.clone()).await; - - // test that the headless server is tracking which buffers we have open correctly. - cx.run_until_parked(); - headless.update(server_cx, |headless, cx| { - assert!(headless.buffer_store.read(cx).has_shared_buffers()) - }); - do_search(&project, cx.clone()).await; - - cx.update(|_| { - drop(buffer); - }); - cx.run_until_parked(); - headless.update(server_cx, |headless, cx| { - assert!(!headless.buffer_store.read(cx).has_shared_buffers()) - }); - - do_search(&project, cx.clone()).await; -} - -#[gpui::test] -async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - "/code", - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - }), - ) - .await; - - let (project, headless) = init_test(&fs, cx, server_cx).await; - - cx.update_global(|settings_store: &mut SettingsStore, cx| { - settings_store.set_user_settings( - r#"{"languages":{"Rust":{"language_servers":["from-local-settings"]}}}"#, - cx, - ) - }) - .unwrap(); - - cx.run_until_parked(); - - server_cx.read(|cx| { - assert_eq!( - AllLanguageSettings::get_global(cx) - .language(None, Some(&"Rust".into()), cx) - .language_servers, - ["from-local-settings"], - "User language settings should be synchronized with the server settings" - ) - }); - - server_cx - .update_global(|settings_store: &mut SettingsStore, cx| { - settings_store.set_server_settings( - r#"{"languages":{"Rust":{"language_servers":["from-server-settings"]}}}"#, - cx, - ) - }) - .unwrap(); - - cx.run_until_parked(); - - server_cx.read(|cx| { - assert_eq!( - AllLanguageSettings::get_global(cx) - .language(None, Some(&"Rust".into()), cx) - .language_servers, - ["from-server-settings".to_string()], - "Server language settings should take precedence over the user settings" - ) - }); - - fs.insert_tree( - "/code/project1/.zed", - json!({ - "settings.json": r#" - { - "languages": {"Rust":{"language_servers":["override-rust-analyzer"]}}, - "lsp": { - "override-rust-analyzer": { - "binary": { - "path": "~/.cargo/bin/rust-analyzer" - } - } - } - }"# - }), - ) - .await; - - let worktree_id = project - .update(cx, |project, cx| { - project.find_or_create_worktree("/code/project1", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |worktree, _| worktree.id()); - - let buffer = project - .update(cx, |project, cx| { - project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx) - }) - .await - .unwrap(); - cx.run_until_parked(); - - server_cx.read(|cx| { - let worktree_id = headless - .read(cx) - .worktree_store - .read(cx) - .worktrees() - .next() - .unwrap() - .read(cx) - .id(); - assert_eq!( - AllLanguageSettings::get( - Some(SettingsLocation { - worktree_id, - path: rel_path("src/lib.rs") - }), - cx - ) - .language(None, Some(&"Rust".into()), cx) - .language_servers, - ["override-rust-analyzer".to_string()] - ) - }); - - cx.read(|cx| { - let file = buffer.read(cx).file(); - assert_eq!( - language_settings(Some("Rust".into()), file, cx).language_servers, - ["override-rust-analyzer".to_string()] - ) - }); -} - -#[gpui::test] -async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - }), - ) - .await; - - let (project, headless) = init_test(&fs, cx, server_cx).await; - - fs.insert_tree( - path!("/code/project1/.zed"), - json!({ - "settings.json": r#" - { - "languages": {"Rust":{"language_servers":["rust-analyzer", "fake-analyzer"]}}, - "lsp": { - "rust-analyzer": { - "binary": { - "path": "~/.cargo/bin/rust-analyzer" - } - }, - "fake-analyzer": { - "binary": { - "path": "~/.cargo/bin/rust-analyzer" - } - } - } - }"# - }), - ) - .await; - - cx.update_entity(&project, |project, _| { - project.languages().register_test_language(LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".into()], - ..Default::default() - }, - ..Default::default() - }); - project.languages().register_fake_lsp_adapter( - "Rust", - FakeLspAdapter { - name: "rust-analyzer", - capabilities: lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions::default()), - rename_provider: Some(lsp::OneOf::Left(true)), - ..lsp::ServerCapabilities::default() - }, - ..FakeLspAdapter::default() - }, - ); - project.languages().register_fake_lsp_adapter( - "Rust", - FakeLspAdapter { - name: "fake-analyzer", - capabilities: lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions::default()), - rename_provider: Some(lsp::OneOf::Left(true)), - ..lsp::ServerCapabilities::default() - }, - ..FakeLspAdapter::default() - }, - ) - }); - - let mut fake_lsp = server_cx.update(|cx| { - headless.read(cx).languages.register_fake_lsp_server( - LanguageServerName("rust-analyzer".into()), - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions::default()), - rename_provider: Some(lsp::OneOf::Left(true)), - ..lsp::ServerCapabilities::default() - }, - None, - ) - }); - - let mut fake_second_lsp = server_cx.update(|cx| { - headless.read(cx).languages.register_fake_lsp_adapter( - "Rust", - FakeLspAdapter { - name: "fake-analyzer", - capabilities: lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions::default()), - rename_provider: Some(lsp::OneOf::Left(true)), - ..lsp::ServerCapabilities::default() - }, - ..FakeLspAdapter::default() - }, - ); - headless.read(cx).languages.register_fake_lsp_server( - LanguageServerName("fake-analyzer".into()), - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions::default()), - rename_provider: Some(lsp::OneOf::Left(true)), - ..lsp::ServerCapabilities::default() - }, - None, - ) - }); - - cx.run_until_parked(); - - let worktree_id = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |worktree, _| worktree.id()); - - // Wait for the settings to synchronize - cx.run_until_parked(); - - let (buffer, _handle) = project - .update(cx, |project, cx| { - project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx) - }) - .await - .unwrap(); - cx.run_until_parked(); - - let fake_lsp = fake_lsp.next().await.unwrap(); - let fake_second_lsp = fake_second_lsp.next().await.unwrap(); - - cx.read(|cx| { - let file = buffer.read(cx).file(); - assert_eq!( - language_settings(Some("Rust".into()), file, cx).language_servers, - ["rust-analyzer".to_string(), "fake-analyzer".to_string()] - ) - }); - - let buffer_id = cx.read(|cx| { - let buffer = buffer.read(cx); - assert_eq!(buffer.language().unwrap().name(), "Rust".into()); - buffer.remote_id() - }); - - server_cx.read(|cx| { - let buffer = headless - .read(cx) - .buffer_store - .read(cx) - .get(buffer_id) - .unwrap(); - - assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust".into()); - }); - - server_cx.read(|cx| { - let lsp_store = headless.read(cx).lsp_store.read(cx); - assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 2); - }); - - fake_lsp.set_request_handler::(|_, _| async move { - Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem { - label: "boop".to_string(), - ..Default::default() - }]))) - }); - - fake_second_lsp.set_request_handler::(|_, _| async move { - Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem { - label: "beep".to_string(), - ..Default::default() - }]))) - }); - - let result = project - .update(cx, |project, cx| { - project.completions( - &buffer, - 0, - CompletionContext { - trigger_kind: CompletionTriggerKind::INVOKED, - trigger_character: None, - }, - cx, - ) - }) - .await - .unwrap(); - - assert_eq!( - result - .into_iter() - .flat_map(|response| response.completions) - .map(|c| c.label.text) - .collect::>(), - vec!["boop".to_string(), "beep".to_string()] - ); - - fake_lsp.set_request_handler::(|_, _| async move { - Ok(Some(lsp::WorkspaceEdit { - changes: Some( - [( - lsp::Uri::from_file_path(path!("/code/project1/src/lib.rs")).unwrap(), - vec![lsp::TextEdit::new( - lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)), - "two".to_string(), - )], - )] - .into_iter() - .collect(), - ), - ..Default::default() - })) - }); - - project - .update(cx, |project, cx| { - project.perform_rename(buffer.clone(), 3, "two".to_string(), cx) - }) - .await - .unwrap(); - - cx.run_until_parked(); - buffer.update(cx, |buffer, _| { - assert_eq!(buffer.text(), "fn two() -> usize { 1 }") - }) -} - -#[gpui::test] -async fn test_remote_cancel_language_server_work( - cx: &mut TestAppContext, - server_cx: &mut TestAppContext, -) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - }), - ) - .await; - - let (project, headless) = init_test(&fs, cx, server_cx).await; - - fs.insert_tree( - path!("/code/project1/.zed"), - json!({ - "settings.json": r#" - { - "languages": {"Rust":{"language_servers":["rust-analyzer"]}}, - "lsp": { - "rust-analyzer": { - "binary": { - "path": "~/.cargo/bin/rust-analyzer" - } - } - } - }"# - }), - ) - .await; - - cx.update_entity(&project, |project, _| { - project.languages().register_test_language(LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".into()], - ..Default::default() - }, - ..Default::default() - }); - project.languages().register_fake_lsp_adapter( - "Rust", - FakeLspAdapter { - name: "rust-analyzer", - ..Default::default() - }, - ) - }); - - let mut fake_lsp = server_cx.update(|cx| { - headless.read(cx).languages.register_fake_lsp_server( - LanguageServerName("rust-analyzer".into()), - Default::default(), - None, - ) - }); - - cx.run_until_parked(); - - let worktree_id = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |worktree, _| worktree.id()); - - cx.run_until_parked(); - - let (buffer, _handle) = project - .update(cx, |project, cx| { - project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx) - }) - .await - .unwrap(); - - cx.run_until_parked(); - - let mut fake_lsp = fake_lsp.next().await.unwrap(); - - // Cancelling all language server work for a given buffer - { - // Two operations, one cancellable and one not. - fake_lsp - .start_progress_with( - "another-token", - lsp::WorkDoneProgressBegin { - cancellable: Some(false), - ..Default::default() - }, - ) - .await; - - let progress_token = "the-progress-token"; - fake_lsp - .start_progress_with( - progress_token, - lsp::WorkDoneProgressBegin { - cancellable: Some(true), - ..Default::default() - }, - ) - .await; - - cx.executor().run_until_parked(); - - project.update(cx, |project, cx| { - project.cancel_language_server_work_for_buffers([buffer.clone()], cx) - }); - - cx.executor().run_until_parked(); - - // Verify the cancellation was received on the server side - let cancel_notification = fake_lsp - .receive_notification::() - .await; - assert_eq!( - cancel_notification.token, - lsp::NumberOrString::String(progress_token.into()) - ); - } - - // Cancelling work by server_id and token - { - let server_id = fake_lsp.server.server_id(); - let progress_token = "the-progress-token"; - - fake_lsp - .start_progress_with( - progress_token, - lsp::WorkDoneProgressBegin { - cancellable: Some(true), - ..Default::default() - }, - ) - .await; - - cx.executor().run_until_parked(); - - project.update(cx, |project, cx| { - project.cancel_language_server_work( - server_id, - Some(ProgressToken::String(SharedString::from(progress_token))), - cx, - ) - }); - - cx.executor().run_until_parked(); - - // Verify the cancellation was received on the server side - let cancel_notification = fake_lsp - .receive_notification::() - .await; - assert_eq!( - cancel_notification.token, - lsp::NumberOrString::String(progress_token.to_owned()) - ); - } -} - -#[gpui::test] -async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - }), - ) - .await; - - let (project, _headless) = init_test(&fs, cx, server_cx).await; - let (worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap(); - - let worktree_id = cx.update(|cx| worktree.read(cx).id()); - - let buffer = project - .update(cx, |project, cx| { - project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx) - }) - .await - .unwrap(); - - fs.save( - &PathBuf::from(path!("/code/project1/src/lib.rs")), - &("bangles".to_string().into()), - LineEnding::Unix, - ) - .await - .unwrap(); - - cx.run_until_parked(); - - buffer.update(cx, |buffer, cx| { - assert_eq!(buffer.text(), "bangles"); - buffer.edit([(0..0, "a")], None, cx); - }); - - fs.save( - &PathBuf::from(path!("/code/project1/src/lib.rs")), - &("bloop".to_string().into()), - LineEnding::Unix, - ) - .await - .unwrap(); - - cx.run_until_parked(); - cx.update(|cx| { - assert!(buffer.read(cx).has_conflict()); - }); - - project - .update(cx, |project, cx| { - project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx) - }) - .await - .unwrap(); - cx.run_until_parked(); - - cx.update(|cx| { - assert!(!buffer.read(cx).has_conflict()); - }); -} - -#[gpui::test] -async fn test_remote_resolve_path_in_buffer( - cx: &mut TestAppContext, - server_cx: &mut TestAppContext, -) { - let fs = FakeFs::new(server_cx.executor()); - // Even though we are not testing anything from project1, it is necessary to test if project2 is picking up correct worktree - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - "project2": { - ".git": {}, - "README.md": "# project 2", - "src": { - "lib.rs": "fn two() -> usize { 2 }" - } - } - }), - ) - .await; - - let (project, _headless) = init_test(&fs, cx, server_cx).await; - - let _ = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap(); - - let (worktree2, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project2"), true, cx) - }) - .await - .unwrap(); - - let worktree2_id = cx.update(|cx| worktree2.read(cx).id()); - - cx.run_until_parked(); - - let buffer2 = project - .update(cx, |project, cx| { - project.open_buffer((worktree2_id, rel_path("src/lib.rs")), cx) - }) - .await - .unwrap(); - - let path = project - .update(cx, |project, cx| { - project.resolve_path_in_buffer(path!("/code/project2/README.md"), &buffer2, cx) - }) - .await - .unwrap(); - assert!(path.is_file()); - assert_eq!(path.abs_path().unwrap(), path!("/code/project2/README.md")); - - let path = project - .update(cx, |project, cx| { - project.resolve_path_in_buffer("../README.md", &buffer2, cx) - }) - .await - .unwrap(); - assert!(path.is_file()); - assert_eq!( - path.project_path().unwrap().clone(), - (worktree2_id, rel_path("README.md")).into() - ); - - let path = project - .update(cx, |project, cx| { - project.resolve_path_in_buffer("../src", &buffer2, cx) - }) - .await - .unwrap(); - assert_eq!( - path.project_path().unwrap().clone(), - (worktree2_id, rel_path("src")).into() - ); - assert!(path.is_dir()); -} - -#[gpui::test] -async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - }), - ) - .await; - - let (project, _headless) = init_test(&fs, cx, server_cx).await; - - let path = project - .update(cx, |project, cx| { - project.resolve_abs_path(path!("/code/project1/README.md"), cx) - }) - .await - .unwrap(); - - assert!(path.is_file()); - assert_eq!(path.abs_path().unwrap(), path!("/code/project1/README.md")); - - let path = project - .update(cx, |project, cx| { - project.resolve_abs_path(path!("/code/project1/src"), cx) - }) - .await - .unwrap(); - - assert!(path.is_dir()); - assert_eq!(path.abs_path().unwrap(), path!("/code/project1/src")); - - let path = project - .update(cx, |project, cx| { - project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx) - }) - .await; - assert!(path.is_none()); -} - -#[gpui::test(iterations = 10)] -async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - "/code", - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - }), - ) - .await; - - let (project, _headless) = init_test(&fs, cx, server_cx).await; - let (worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree("/code/project1", true, cx) - }) - .await - .unwrap(); - let worktree_id = worktree.read_with(cx, |tree, _| tree.id()); - - // Open a buffer on the client but cancel after a random amount of time. - let buffer = project.update(cx, |p, cx| { - p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx) - }); - cx.executor().simulate_random_delay().await; - drop(buffer); - - // Try opening the same buffer again as the client, and ensure we can - // still do it despite the cancellation above. - let buffer = project - .update(cx, |p, cx| { - p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx) - }) - .await - .unwrap(); - - buffer.read_with(cx, |buf, _| { - assert_eq!(buf.text(), "fn one() -> usize { 1 }") - }); -} - -#[gpui::test] -async fn test_adding_then_removing_then_adding_worktrees( - cx: &mut TestAppContext, - server_cx: &mut TestAppContext, -) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - "project2": { - "README.md": "# project 2", - }, - }), - ) - .await; - - let (project, _headless) = init_test(&fs, cx, server_cx).await; - let (_worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap(); - - let (worktree_2, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project2"), true, cx) - }) - .await - .unwrap(); - let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id()); - - project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx)); - - let (worktree_2, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project2"), true, cx) - }) - .await - .unwrap(); - - cx.run_until_parked(); - worktree_2.update(cx, |worktree, _cx| { - assert!(worktree.is_visible()); - let entries = worktree.entries(true, 0).collect::>(); - assert_eq!(entries.len(), 2); - assert_eq!(entries[1].path.as_unix_str(), "README.md") - }) -} - -#[gpui::test] -async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - }), - ) - .await; - - let (project, _headless) = init_test(&fs, cx, server_cx).await; - let buffer = project.update(cx, |project, cx| project.open_server_settings(cx)); - cx.executor().run_until_parked(); - - let buffer = buffer.await.unwrap(); - - cx.update(|cx| { - assert_eq!( - buffer.read(cx).text(), - initial_server_settings_content() - .to_string() - .replace("\r\n", "\n") - ) - }) -} - -#[gpui::test(iterations = 20)] -async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - }), - ) - .await; - - let (project, _headless) = init_test(&fs, cx, server_cx).await; - - let (worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap(); - - let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); - let buffer = project - .update(cx, |project, cx| { - project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx) - }) - .await - .unwrap(); - - buffer.update(cx, |buffer, cx| { - assert_eq!(buffer.text(), "fn one() -> usize { 1 }"); - let ix = buffer.text().find('1').unwrap(); - buffer.edit([(ix..ix + 1, "100")], None, cx); - }); - - let client = cx.read(|cx| project.read(cx).remote_client().unwrap()); - client - .update(cx, |client, cx| client.simulate_disconnect(cx)) - .detach(); - - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - - assert_eq!( - fs.load(path!("/code/project1/src/lib.rs").as_ref()) - .await - .unwrap(), - "fn one() -> usize { 100 }" - ); -} - -#[gpui::test] -async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - "/code", - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - }, - }), - ) - .await; - - let (project, _) = init_test(&fs, cx, server_cx).await; - - let (worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree("/code/project1", true, cx) - }) - .await - .unwrap(); - - cx.run_until_parked(); - - fs.rename( - &PathBuf::from("/code/project1"), - &PathBuf::from("/code/project2"), - Default::default(), - ) - .await - .unwrap(); - - cx.run_until_parked(); - worktree.update(cx, |worktree, _| { - assert_eq!(worktree.root_name(), "project2") - }) -} - -#[gpui::test] -async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - "/code", - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - }, - }), - ) - .await; - - let (project, _) = init_test(&fs, cx, server_cx).await; - let (worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree("/code/project1", true, cx) - }) - .await - .unwrap(); - - cx.run_until_parked(); - - let entry = project - .update(cx, |project, cx| { - let worktree = worktree.read(cx); - let entry = worktree.entry_for_path(rel_path("README.md")).unwrap(); - project.rename_entry(entry.id, (worktree.id(), rel_path("README.rst")).into(), cx) - }) - .await - .unwrap() - .into_included() - .unwrap(); - - cx.run_until_parked(); - - worktree.update(cx, |worktree, _| { - assert_eq!( - worktree.entry_for_path(rel_path("README.rst")).unwrap().id, - entry.id - ) - }); -} - -#[gpui::test] -async fn test_copy_file_into_remote_project( - cx: &mut TestAppContext, - server_cx: &mut TestAppContext, -) { - let remote_fs = FakeFs::new(server_cx.executor()); - remote_fs - .insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "main.rs": "" - } - }, - }), - ) - .await; - - let (project, _) = init_test(&remote_fs, cx, server_cx).await; - let (worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap(); - - cx.run_until_parked(); - - let local_fs = project - .read_with(cx, |project, _| project.fs().clone()) - .as_fake(); - local_fs - .insert_tree( - path!("/local-code"), - json!({ - "dir1": { - "file1": "file 1 content", - "dir2": { - "file2": "file 2 content", - "dir3": { - "file3": "" - }, - "dir4": {} - }, - "dir5": {} - }, - "file4": "file 4 content" - }), - ) - .await; - - worktree - .update(cx, |worktree, cx| { - worktree.copy_external_entries( - rel_path("src").into(), - vec![ - Path::new(path!("/local-code/dir1/file1")).into(), - Path::new(path!("/local-code/dir1/dir2")).into(), - ], - local_fs.clone(), - cx, - ) - }) - .await - .unwrap(); - - assert_eq!( - remote_fs.paths(true), - vec![ - PathBuf::from(path!("/")), - PathBuf::from(path!("/code")), - PathBuf::from(path!("/code/project1")), - PathBuf::from(path!("/code/project1/.git")), - PathBuf::from(path!("/code/project1/README.md")), - PathBuf::from(path!("/code/project1/src")), - PathBuf::from(path!("/code/project1/src/dir2")), - PathBuf::from(path!("/code/project1/src/file1")), - PathBuf::from(path!("/code/project1/src/main.rs")), - PathBuf::from(path!("/code/project1/src/dir2/dir3")), - PathBuf::from(path!("/code/project1/src/dir2/dir4")), - PathBuf::from(path!("/code/project1/src/dir2/file2")), - PathBuf::from(path!("/code/project1/src/dir2/dir3/file3")), - ] - ); - assert_eq!( - remote_fs - .load(path!("/code/project1/src/file1").as_ref()) - .await - .unwrap(), - "file 1 content" - ); - assert_eq!( - remote_fs - .load(path!("/code/project1/src/dir2/file2").as_ref()) - .await - .unwrap(), - "file 2 content" - ); - assert_eq!( - remote_fs - .load(path!("/code/project1/src/dir2/dir3/file3").as_ref()) - .await - .unwrap(), - "" - ); -} - -#[gpui::test] -async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let text_2 = " - fn one() -> usize { - 1 - } - " - .unindent(); - let text_1 = " - fn one() -> usize { - 0 - } - " - .unindent(); - - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - "/code", - json!({ - "project1": { - ".git": {}, - "src": { - "lib.rs": text_2 - }, - "README.md": "# project 1", - }, - }), - ) - .await; - fs.set_index_for_repo( - Path::new("/code/project1/.git"), - &[("src/lib.rs", text_1.clone())], - ); - fs.set_head_for_repo( - Path::new("/code/project1/.git"), - &[("src/lib.rs", text_1.clone())], - "deadbeef", - ); - - let (project, _headless) = init_test(&fs, cx, server_cx).await; - let (worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree("/code/project1", true, cx) - }) - .await - .unwrap(); - let worktree_id = cx.update(|cx| worktree.read(cx).id()); - cx.executor().run_until_parked(); - - let buffer = project - .update(cx, |project, cx| { - project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx) - }) - .await - .unwrap(); - let diff = project - .update(cx, |project, cx| { - project.open_uncommitted_diff(buffer.clone(), cx) - }) - .await - .unwrap(); - - diff.read_with(cx, |diff, cx| { - assert_eq!(diff.base_text_string().unwrap(), text_1); - assert_eq!( - diff.secondary_diff() - .unwrap() - .read(cx) - .base_text_string() - .unwrap(), - text_1 - ); - }); - - // stage the current buffer's contents - fs.set_index_for_repo( - Path::new("/code/project1/.git"), - &[("src/lib.rs", text_2.clone())], - ); - - cx.executor().run_until_parked(); - diff.read_with(cx, |diff, cx| { - assert_eq!(diff.base_text_string().unwrap(), text_1); - assert_eq!( - diff.secondary_diff() - .unwrap() - .read(cx) - .base_text_string() - .unwrap(), - text_2 - ); - }); - - // commit the current buffer's contents - fs.set_head_for_repo( - Path::new("/code/project1/.git"), - &[("src/lib.rs", text_2.clone())], - "deadbeef", - ); - - cx.executor().run_until_parked(); - diff.read_with(cx, |diff, cx| { - assert_eq!(diff.base_text_string().unwrap(), text_2); - assert_eq!( - diff.secondary_diff() - .unwrap() - .read(cx) - .base_text_string() - .unwrap(), - text_2 - ); - }); -} - -#[gpui::test] -async fn test_remote_git_diffs_when_recv_update_repository_delay( - cx: &mut TestAppContext, - server_cx: &mut TestAppContext, -) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - theme::init(theme::LoadThemes::JustBase, cx); - release_channel::init(semver::Version::new(0, 0, 0), cx); - editor::init(cx); - }); - - use editor::Editor; - use gpui::VisualContext; - let text_2 = " - fn one() -> usize { - 1 - } - " - .unindent(); - let text_1 = " - fn one() -> usize { - 0 - } - " - .unindent(); - - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - "src": { - "lib.rs": text_2 - }, - "README.md": "# project 1", - }, - }), - ) - .await; - - let (project, _headless) = init_test(&fs, cx, server_cx).await; - let (worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap(); - let worktree_id = cx.update(|cx| worktree.read(cx).id()); - let buffer = project - .update(cx, |project, cx| { - project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx) - }) - .await - .unwrap(); - let buffer_id = cx.update(|cx| buffer.read(cx).remote_id()); - - let cx = cx.add_empty_window(); - let editor = cx.new_window_entity(|window, cx| { - Editor::for_buffer(buffer, Some(project.clone()), window, cx) - }); - - // Remote server will send proto::UpdateRepository after the instance of Editor create. - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - }, - }), - ) - .await; - - fs.set_index_for_repo( - Path::new(path!("/code/project1/.git")), - &[("src/lib.rs", text_1.clone())], - ); - fs.set_head_for_repo( - Path::new(path!("/code/project1/.git")), - &[("src/lib.rs", text_1.clone())], - "sha", - ); - - cx.executor().run_until_parked(); - let diff = editor - .read_with(cx, |editor, cx| { - editor - .buffer() - .read_with(cx, |buffer, _| buffer.diff_for(buffer_id)) - }) - .unwrap(); - - diff.read_with(cx, |diff, cx| { - assert_eq!(diff.base_text_string().unwrap(), text_1); - assert_eq!( - diff.secondary_diff() - .unwrap() - .read(cx) - .base_text_string() - .unwrap(), - text_1 - ); - }); - - // stage the current buffer's contents - fs.set_index_for_repo( - Path::new(path!("/code/project1/.git")), - &[("src/lib.rs", text_2.clone())], - ); - - cx.executor().run_until_parked(); - diff.read_with(cx, |diff, cx| { - assert_eq!(diff.base_text_string().unwrap(), text_1); - assert_eq!( - diff.secondary_diff() - .unwrap() - .read(cx) - .base_text_string() - .unwrap(), - text_2 - ); - }); - - // commit the current buffer's contents - fs.set_head_for_repo( - Path::new(path!("/code/project1/.git")), - &[("src/lib.rs", text_2.clone())], - "sha", - ); - - cx.executor().run_until_parked(); - diff.read_with(cx, |diff, cx| { - assert_eq!(diff.base_text_string().unwrap(), text_2); - assert_eq!( - diff.secondary_diff() - .unwrap() - .read(cx) - .base_text_string() - .unwrap(), - text_2 - ); - }); -} - -#[gpui::test] -async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - }, - }), - ) - .await; - - let (project, headless_project) = init_test(&fs, cx, server_cx).await; - let branches = ["main", "dev", "feature-1"]; - let branches_set = branches - .iter() - .map(ToString::to_string) - .collect::>(); - fs.insert_branches(Path::new(path!("/code/project1/.git")), &branches); - - let (_worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap(); - // Give the worktree a bit of time to index the file system - cx.run_until_parked(); - - let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap()); - - let remote_branches = repository - .update(cx, |repository, _| repository.branches()) - .await - .unwrap() - .unwrap(); - - let new_branch = branches[2]; - - let remote_branches = remote_branches - .into_iter() - .map(|branch| branch.name().to_string()) - .collect::>(); - - assert_eq!(&remote_branches, &branches_set); - - cx.update(|cx| { - repository.update(cx, |repository, _cx| { - repository.change_branch(new_branch.to_string()) - }) - }) - .await - .unwrap() - .unwrap(); - - cx.run_until_parked(); - - let server_branch = server_cx.update(|cx| { - headless_project.update(cx, |headless_project, cx| { - headless_project.git_store.update(cx, |git_store, cx| { - git_store - .repositories() - .values() - .next() - .unwrap() - .read(cx) - .branch - .as_ref() - .unwrap() - .clone() - }) - }) - }); - - assert_eq!(server_branch.name(), branches[2]); - - // Also try creating a new branch - cx.update(|cx| { - repository.update(cx, |repo, _cx| { - repo.create_branch("totally-new-branch".to_string(), None) - }) - }) - .await - .unwrap() - .unwrap(); - - cx.update(|cx| { - repository.update(cx, |repo, _cx| { - repo.change_branch("totally-new-branch".to_string()) - }) - }) - .await - .unwrap() - .unwrap(); - - cx.run_until_parked(); - - let server_branch = server_cx.update(|cx| { - headless_project.update(cx, |headless_project, cx| { - headless_project.git_store.update(cx, |git_store, cx| { - git_store - .repositories() - .values() - .next() - .unwrap() - .read(cx) - .branch - .as_ref() - .unwrap() - .clone() - }) - }) - }); - - assert_eq!(server_branch.name(), "totally-new-branch"); -} - -#[gpui::test] -async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/project"), - json!({ - "a.txt": "A", - "b.txt": "B", - }), - ) - .await; - - let (project, _headless_project) = init_test(&fs, cx, server_cx).await; - project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/project"), true, cx) - }) - .await - .unwrap(); - - let action_log = cx.new(|_| action_log::ActionLog::new(project.clone())); - - // Create a minimal thread for the ReadFileTool - let context_server_registry = - cx.new(|cx| agent::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), - cx, - ) - }); - - let input = ReadFileToolInput { - path: "project/b.txt".into(), - start_line: None, - end_line: None, - }; - let read_tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log)); - let (event_stream, _) = ToolCallEventStream::test(); - - let exists_result = cx.update(|cx| read_tool.clone().run(input, event_stream.clone(), cx)); - let output = exists_result.await.unwrap(); - assert_eq!(output, LanguageModelToolResultContent::Text("B".into())); - - let input = ReadFileToolInput { - path: "project/c.txt".into(), - start_line: None, - end_line: None, - }; - let does_not_exist_result = cx.update(|cx| read_tool.run(input, event_stream, cx)); - does_not_exist_result.await.unwrap_err(); -} - -#[gpui::test] -async fn test_remote_external_agent_server( - cx: &mut TestAppContext, - server_cx: &mut TestAppContext, -) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree(path!("/project"), json!({})).await; - - let (project, _headless_project) = init_test(&fs, cx, server_cx).await; - project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/project"), true, cx) - }) - .await - .unwrap(); - let names = project.update(cx, |project, cx| { - project - .agent_server_store() - .read(cx) - .external_agents() - .map(|name| name.to_string()) - .collect::>() - }); - pretty_assertions::assert_eq!(names, ["codex", "gemini", "claude"]); - server_cx.update_global::(|settings_store, cx| { - settings_store - .set_server_settings( - &json!({ - "agent_servers": { - "foo": { - "type": "custom", - "command": "foo-cli", - "args": ["--flag"], - "env": { - "VAR": "val" - } - } - } - }) - .to_string(), - cx, - ) - .unwrap(); - }); - server_cx.run_until_parked(); - cx.run_until_parked(); - let names = project.update(cx, |project, cx| { - project - .agent_server_store() - .read(cx) - .external_agents() - .map(|name| name.to_string()) - .collect::>() - }); - pretty_assertions::assert_eq!(names, ["gemini", "codex", "claude", "foo"]); - let (command, root, login) = project - .update(cx, |project, cx| { - project.agent_server_store().update(cx, |store, cx| { - store - .get_external_agent(&"foo".into()) - .unwrap() - .get_command( - None, - HashMap::from_iter([("OTHER_VAR".into(), "other-val".into())]), - None, - None, - &mut cx.to_async(), - ) - }) - }) - .await - .unwrap(); - assert_eq!( - command, - AgentServerCommand { - path: "ssh".into(), - args: vec!["foo-cli".into(), "--flag".into()], - env: Some(HashMap::from_iter([ - ("VAR".into(), "val".into()), - ("OTHER_VAR".into(), "other-val".into()) - ])) - } - ); - assert_eq!(&PathBuf::from(root), paths::home_dir()); - assert!(login.is_none()); -} - -pub async fn init_test( - server_fs: &Arc, - cx: &mut TestAppContext, - server_cx: &mut TestAppContext, -) -> (Entity, Entity) { - let server_fs = server_fs.clone(); - cx.update(|cx| { - release_channel::init(semver::Version::new(0, 0, 0), cx); - }); - server_cx.update(|cx| { - release_channel::init(semver::Version::new(0, 0, 0), cx); - }); - init_logger(); - - let (opts, ssh_server_client) = RemoteClient::fake_server(cx, server_cx); - let http_client = Arc::new(BlockedHttpClient); - let node_runtime = NodeRuntime::unavailable(); - let languages = Arc::new(LanguageRegistry::new(cx.executor())); - let proxy = Arc::new(ExtensionHostProxy::new()); - server_cx.update(HeadlessProject::init); - let headless = server_cx.new(|cx| { - HeadlessProject::new( - crate::HeadlessAppState { - session: ssh_server_client, - fs: server_fs.clone(), - http_client, - node_runtime, - languages, - extension_host_proxy: proxy, - }, - cx, - ) - }); - - let ssh = RemoteClient::fake_client(opts, cx).await; - let project = build_project(ssh, cx); - project - .update(cx, { - let headless = headless.clone(); - |_, cx| cx.on_release(|_, _| drop(headless)) - }) - .detach(); - (project, headless) -} - -fn init_logger() { - zlog::init_test(); -} - -fn build_project(ssh: Entity, cx: &mut TestAppContext) -> Entity { - cx.update(|cx| { - if !cx.has_global::() { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - } - }); - - let client = cx.update(|cx| { - Client::new( - Arc::new(FakeSystemClock::new()), - FakeHttpClient::with_404_response(), - cx, - ) - }); - - let node = NodeRuntime::unavailable(); - let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - let languages = Arc::new(LanguageRegistry::test(cx.executor())); - let fs = FakeFs::new(cx.executor()); - - cx.update(|cx| { - Project::init(&client, cx); - }); - - cx.update(|cx| Project::remote(ssh, client, node, user_store, languages, fs, cx)) -} diff --git a/crates/remote_server/src/remote_server.rs b/crates/remote_server/src/remote_server.rs deleted file mode 100644 index 98e8744b11..0000000000 --- a/crates/remote_server/src/remote_server.rs +++ /dev/null @@ -1,87 +0,0 @@ -mod headless_project; - -#[cfg(not(windows))] -pub mod unix; - -#[cfg(test)] -mod remote_editing_tests; - -use clap::Subcommand; -use std::path::PathBuf; - -pub use headless_project::{HeadlessAppState, HeadlessProject}; - -#[derive(Subcommand)] -pub enum Commands { - Run { - #[arg(long)] - log_file: PathBuf, - #[arg(long)] - pid_file: PathBuf, - #[arg(long)] - stdin_socket: PathBuf, - #[arg(long)] - stdout_socket: PathBuf, - #[arg(long)] - stderr_socket: PathBuf, - }, - Proxy { - #[arg(long)] - reconnect: bool, - #[arg(long)] - identifier: String, - }, - Version, -} - -#[cfg(not(windows))] -pub fn run(command: Commands) -> anyhow::Result<()> { - use anyhow::Context; - use release_channel::{RELEASE_CHANNEL, ReleaseChannel}; - use unix::{ExecuteProxyError, execute_proxy, execute_run}; - - match command { - Commands::Run { - log_file, - pid_file, - stdin_socket, - stdout_socket, - stderr_socket, - } => execute_run( - log_file, - pid_file, - stdin_socket, - stdout_socket, - stderr_socket, - ), - Commands::Proxy { - identifier, - reconnect, - } => execute_proxy(identifier, reconnect) - .inspect_err(|err| { - if let ExecuteProxyError::ServerNotRunning(err) = err { - std::process::exit(err.to_exit_code()); - } - }) - .context("running proxy on the remote server"), - Commands::Version => { - let release_channel = *RELEASE_CHANNEL; - match release_channel { - ReleaseChannel::Stable | ReleaseChannel::Preview => { - println!("{}", env!("ZED_PKG_VERSION")) - } - ReleaseChannel::Nightly | ReleaseChannel::Dev => { - let commit_sha = - option_env!("ZED_COMMIT_SHA").unwrap_or(release_channel.dev_name()); - let build_id = option_env!("ZED_BUILD_ID"); - if let Some(build_id) = build_id { - println!("{}+{}", build_id, commit_sha) - } else { - println!("{commit_sha}"); - } - } - }; - Ok(()) - } - } -} diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs deleted file mode 100644 index 8adeaa5947..0000000000 --- a/crates/remote_server/src/unix.rs +++ /dev/null @@ -1,1045 +0,0 @@ -use crate::HeadlessProject; -use crate::headless_project::HeadlessAppState; -use anyhow::{Context as _, Result, anyhow}; -use client::ProxySettings; -use util::ResultExt; - -use extension::ExtensionHostProxy; -use fs::{Fs, RealFs}; -use futures::channel::{mpsc, oneshot}; -use futures::{AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, SinkExt, select, select_biased}; -use git::GitHostingProviderRegistry; -use gpui::{App, AppContext as _, Context, Entity, UpdateGlobal as _}; -use gpui_tokio::Tokio; -use http_client::{Url, read_proxy_from_env}; -use language::LanguageRegistry; -use node_runtime::{NodeBinaryOptions, NodeRuntime}; -use paths::logs_dir; -use project::project_settings::ProjectSettings; -use util::command::new_smol_command; - -use proto::CrashReport; -use release_channel::{AppCommitSha, AppVersion, RELEASE_CHANNEL, ReleaseChannel}; -use remote::RemoteClient; -use remote::{ - json_log::LogRecord, - protocol::{read_message, write_message}, - proxy::ProxyLaunchError, -}; -use reqwest_client::ReqwestClient; -use rpc::proto::{self, Envelope, REMOTE_SERVER_PROJECT_ID}; -use rpc::{AnyProtoClient, TypedEnvelope}; -use settings::{Settings, SettingsStore, watch_config_file}; -use smol::Async; -use smol::channel::{Receiver, Sender}; -use smol::io::AsyncReadExt; -use smol::{net::unix::UnixListener, stream::StreamExt as _}; -use std::{ - env, - ffi::OsStr, - fs::File, - io::Write, - mem, - ops::ControlFlow, - path::{Path, PathBuf}, - process::ExitStatus, - str::FromStr, - sync::{Arc, LazyLock}, -}; -use thiserror::Error; - -pub static VERSION: LazyLock = LazyLock::new(|| match *RELEASE_CHANNEL { - ReleaseChannel::Stable | ReleaseChannel::Preview => env!("ZED_PKG_VERSION").to_owned(), - ReleaseChannel::Nightly | ReleaseChannel::Dev => { - let commit_sha = option_env!("ZED_COMMIT_SHA").unwrap_or("missing-zed-commit-sha"); - let build_identifier = option_env!("ZED_BUILD_ID"); - if let Some(build_id) = build_identifier { - format!("{build_id}+{commit_sha}") - } else { - commit_sha.to_owned() - } - } -}); - -fn init_logging_proxy() { - env_logger::builder() - .format(|buf, record| { - let mut log_record = LogRecord::new(record); - log_record.message = format!("(remote proxy) {}", log_record.message); - serde_json::to_writer(&mut *buf, &log_record)?; - buf.write_all(b"\n")?; - Ok(()) - }) - .init(); -} - -fn init_logging_server(log_file_path: PathBuf) -> Result>> { - struct MultiWrite { - file: File, - channel: Sender>, - buffer: Vec, - } - - impl Write for MultiWrite { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - let written = self.file.write(buf)?; - self.buffer.extend_from_slice(&buf[..written]); - Ok(written) - } - - fn flush(&mut self) -> std::io::Result<()> { - self.channel - .send_blocking(self.buffer.clone()) - .map_err(std::io::Error::other)?; - self.buffer.clear(); - self.file.flush() - } - } - - let log_file = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open(&log_file_path) - .context("Failed to open log file in append mode")?; - - let (tx, rx) = smol::channel::unbounded(); - - let target = Box::new(MultiWrite { - file: log_file, - channel: tx, - buffer: Vec::new(), - }); - - env_logger::Builder::new() - .filter_level(log::LevelFilter::Info) - .parse_default_env() - .target(env_logger::Target::Pipe(target)) - .format(|buf, record| { - let mut log_record = LogRecord::new(record); - log_record.message = format!("(remote server) {}", log_record.message); - serde_json::to_writer(&mut *buf, &log_record)?; - buf.write_all(b"\n")?; - Ok(()) - }) - .init(); - - Ok(rx) -} - -fn handle_crash_files_requests(project: &Entity, client: &AnyProtoClient) { - client.add_request_handler( - project.downgrade(), - |_, _: TypedEnvelope, _cx| async move { - let mut legacy_panics = Vec::new(); - let mut crashes = Vec::new(); - let mut children = smol::fs::read_dir(paths::logs_dir()).await?; - while let Some(child) = children.next().await { - let child = child?; - let child_path = child.path(); - - let extension = child_path.extension(); - if extension == Some(OsStr::new("panic")) { - let filename = if let Some(filename) = child_path.file_name() { - filename.to_string_lossy() - } else { - continue; - }; - - if !filename.starts_with("zed") { - continue; - } - - let file_contents = smol::fs::read_to_string(&child_path) - .await - .context("error reading panic file")?; - - legacy_panics.push(file_contents); - smol::fs::remove_file(&child_path) - .await - .context("error removing panic") - .log_err(); - } else if extension == Some(OsStr::new("dmp")) { - let mut json_path = child_path.clone(); - json_path.set_extension("json"); - if let Ok(json_content) = smol::fs::read_to_string(&json_path).await { - crashes.push(CrashReport { - metadata: json_content, - minidump_contents: smol::fs::read(&child_path).await?, - }); - smol::fs::remove_file(&child_path).await.log_err(); - smol::fs::remove_file(&json_path).await.log_err(); - } else { - log::error!("Couldn't find json metadata for crash: {child_path:?}"); - } - } - } - - anyhow::Ok(proto::GetCrashFilesResponse { crashes }) - }, - ); -} - -struct ServerListeners { - stdin: UnixListener, - stdout: UnixListener, - stderr: UnixListener, -} - -impl ServerListeners { - pub fn new(stdin_path: PathBuf, stdout_path: PathBuf, stderr_path: PathBuf) -> Result { - Ok(Self { - stdin: UnixListener::bind(stdin_path).context("failed to bind stdin socket")?, - stdout: UnixListener::bind(stdout_path).context("failed to bind stdout socket")?, - stderr: UnixListener::bind(stderr_path).context("failed to bind stderr socket")?, - }) - } -} - -fn start_server( - listeners: ServerListeners, - log_rx: Receiver>, - cx: &mut App, - is_wsl_interop: bool, -) -> AnyProtoClient { - // This is the server idle timeout. If no connection comes in this timeout, the server will shut down. - const IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10 * 60); - - let (incoming_tx, incoming_rx) = mpsc::unbounded::(); - let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded::(); - let (app_quit_tx, mut app_quit_rx) = mpsc::unbounded::<()>(); - - cx.on_app_quit(move |_| { - let mut app_quit_tx = app_quit_tx.clone(); - async move { - log::info!("app quitting. sending signal to server main loop"); - app_quit_tx.send(()).await.ok(); - } - }) - .detach(); - - cx.spawn(async move |cx| { - let mut stdin_incoming = listeners.stdin.incoming(); - let mut stdout_incoming = listeners.stdout.incoming(); - let mut stderr_incoming = listeners.stderr.incoming(); - - loop { - let streams = futures::future::join3(stdin_incoming.next(), stdout_incoming.next(), stderr_incoming.next()); - - log::info!("accepting new connections"); - let result = select! { - streams = streams.fuse() => { - let (Some(Ok(stdin_stream)), Some(Ok(stdout_stream)), Some(Ok(stderr_stream))) = streams else { - break; - }; - anyhow::Ok((stdin_stream, stdout_stream, stderr_stream)) - } - _ = futures::FutureExt::fuse(smol::Timer::after(IDLE_TIMEOUT)) => { - log::warn!("timed out waiting for new connections after {:?}. exiting.", IDLE_TIMEOUT); - cx.update(|cx| { - // TODO: This is a hack, because in a headless project, shutdown isn't executed - // when calling quit, but it should be. - cx.shutdown(); - cx.quit(); - })?; - break; - } - _ = app_quit_rx.next().fuse() => { - break; - } - }; - - let Ok((mut stdin_stream, mut stdout_stream, mut stderr_stream)) = result else { - break; - }; - - let mut input_buffer = Vec::new(); - let mut output_buffer = Vec::new(); - - let (mut stdin_msg_tx, mut stdin_msg_rx) = mpsc::unbounded::(); - cx.background_spawn(async move { - while let Ok(msg) = read_message(&mut stdin_stream, &mut input_buffer).await { - if (stdin_msg_tx.send(msg).await).is_err() { - break; - } - } - }).detach(); - - loop { - - select_biased! { - _ = app_quit_rx.next().fuse() => { - return anyhow::Ok(()); - } - - stdin_message = stdin_msg_rx.next().fuse() => { - let Some(message) = stdin_message else { - log::warn!("error reading message on stdin. exiting."); - break; - }; - if let Err(error) = incoming_tx.unbounded_send(message) { - log::error!("failed to send message to application: {error:?}. exiting."); - return Err(anyhow!(error)); - } - } - - outgoing_message = outgoing_rx.next().fuse() => { - let Some(message) = outgoing_message else { - log::error!("stdout handler, no message"); - break; - }; - - if let Err(error) = - write_message(&mut stdout_stream, &mut output_buffer, message).await - { - log::error!("failed to write stdout message: {:?}", error); - break; - } - if let Err(error) = stdout_stream.flush().await { - log::error!("failed to flush stdout message: {:?}", error); - break; - } - } - - log_message = log_rx.recv().fuse() => { - if let Ok(log_message) = log_message { - if let Err(error) = stderr_stream.write_all(&log_message).await { - log::error!("failed to write log message to stderr: {:?}", error); - break; - } - if let Err(error) = stderr_stream.flush().await { - log::error!("failed to flush stderr stream: {:?}", error); - break; - } - } - } - } - } - } - anyhow::Ok(()) - }) - .detach(); - - RemoteClient::proto_client_from_channels(incoming_rx, outgoing_tx, cx, "server", is_wsl_interop) -} - -fn init_paths() -> anyhow::Result<()> { - for path in [ - paths::config_dir(), - paths::extensions_dir(), - paths::languages_dir(), - paths::logs_dir(), - paths::temp_dir(), - paths::hang_traces_dir(), - paths::remote_extensions_dir(), - paths::remote_extensions_uploads_dir(), - ] - .iter() - { - std::fs::create_dir_all(path).with_context(|| format!("creating directory {path:?}"))?; - } - Ok(()) -} - -pub fn execute_run( - log_file: PathBuf, - pid_file: PathBuf, - stdin_socket: PathBuf, - stdout_socket: PathBuf, - stderr_socket: PathBuf, -) -> Result<()> { - init_paths()?; - - match daemonize()? { - ControlFlow::Break(_) => return Ok(()), - ControlFlow::Continue(_) => {} - } - - let app = gpui::Application::headless(); - let id = std::process::id().to_string(); - app.background_executor() - .spawn(crashes::init(crashes::InitCrashHandler { - session_id: id, - zed_version: VERSION.to_owned(), - binary: "zed-remote-server".to_string(), - release_channel: release_channel::RELEASE_CHANNEL_NAME.clone(), - commit_sha: option_env!("ZED_COMMIT_SHA").unwrap_or("no_sha").to_owned(), - })) - .detach(); - let log_rx = init_logging_server(log_file)?; - log::info!( - "starting up. pid_file: {:?}, stdin_socket: {:?}, stdout_socket: {:?}, stderr_socket: {:?}", - pid_file, - stdin_socket, - stdout_socket, - stderr_socket - ); - - write_pid_file(&pid_file) - .with_context(|| format!("failed to write pid file: {:?}", &pid_file))?; - - let listeners = ServerListeners::new(stdin_socket, stdout_socket, stderr_socket)?; - - rayon::ThreadPoolBuilder::new() - .num_threads(std::thread::available_parallelism().map_or(1, |n| n.get().div_ceil(2))) - .stack_size(10 * 1024 * 1024) - .thread_name(|ix| format!("RayonWorker{}", ix)) - .build_global() - .unwrap(); - - let (shell_env_loaded_tx, shell_env_loaded_rx) = oneshot::channel(); - app.background_executor() - .spawn(async { - util::load_login_shell_environment().await.log_err(); - shell_env_loaded_tx.send(()).ok(); - }) - .detach(); - - let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new()); - app.run(move |cx| { - settings::init(cx); - let app_commit_sha = option_env!("ZED_COMMIT_SHA").map(|s| AppCommitSha::new(s.to_owned())); - let app_version = AppVersion::load( - env!("ZED_PKG_VERSION"), - option_env!("ZED_BUILD_ID"), - app_commit_sha, - ); - release_channel::init(app_version, cx); - gpui_tokio::init(cx); - - HeadlessProject::init(cx); - - let is_wsl_interop = if cfg!(target_os = "linux") { - // See: https://learn.microsoft.com/en-us/windows/wsl/filesystems#disable-interoperability - matches!(std::fs::read_to_string("/proc/sys/fs/binfmt_misc/WSLInterop"), Ok(s) if s.contains("enabled")) - } else { - false - }; - - log::info!("gpui app started, initializing server"); - let session = start_server(listeners, log_rx, cx, is_wsl_interop); - - GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx); - git_hosting_providers::init(cx); - dap_adapters::init(cx); - - extension::init(cx); - let extension_host_proxy = ExtensionHostProxy::global(cx); - - json_schema_store::init(cx); - - let project = cx.new(|cx| { - let fs = Arc::new(RealFs::new(None, cx.background_executor().clone())); - let node_settings_rx = initialize_settings(session.clone(), fs.clone(), cx); - - let proxy_url = read_proxy_settings(cx); - - let http_client = { - let _guard = Tokio::handle(cx).enter(); - Arc::new( - ReqwestClient::proxy_and_user_agent( - proxy_url, - &format!( - "Zed-Server/{} ({}; {})", - env!("CARGO_PKG_VERSION"), - std::env::consts::OS, - std::env::consts::ARCH - ), - ) - .expect("Could not start HTTP client"), - ) - }; - - let node_runtime = NodeRuntime::new( - http_client.clone(), - Some(shell_env_loaded_rx), - node_settings_rx, - ); - - let mut languages = LanguageRegistry::new(cx.background_executor().clone()); - languages.set_language_server_download_dir(paths::languages_dir().clone()); - let languages = Arc::new(languages); - - HeadlessProject::new( - HeadlessAppState { - session: session.clone(), - fs, - http_client, - node_runtime, - languages, - extension_host_proxy, - }, - cx, - ) - }); - - handle_crash_files_requests(&project, &session); - - cx.background_spawn(async move { cleanup_old_binaries() }) - .detach(); - - mem::forget(project); - }); - log::info!("gpui app is shut down. quitting."); - Ok(()) -} - -#[derive(Debug, Error)] -pub(crate) enum ServerPathError { - #[error("Failed to create server_dir `{path}`")] - CreateServerDir { - #[source] - source: std::io::Error, - path: PathBuf, - }, - #[error("Failed to create logs_dir `{path}`")] - CreateLogsDir { - #[source] - source: std::io::Error, - path: PathBuf, - }, -} - -#[derive(Clone, Debug)] -struct ServerPaths { - log_file: PathBuf, - pid_file: PathBuf, - stdin_socket: PathBuf, - stdout_socket: PathBuf, - stderr_socket: PathBuf, -} - -impl ServerPaths { - fn new(identifier: &str) -> Result { - let server_dir = paths::remote_server_state_dir().join(identifier); - std::fs::create_dir_all(&server_dir).map_err(|source| { - ServerPathError::CreateServerDir { - source, - path: server_dir.clone(), - } - })?; - let log_dir = logs_dir(); - std::fs::create_dir_all(log_dir).map_err(|source| ServerPathError::CreateLogsDir { - source: source, - path: log_dir.clone(), - })?; - - let pid_file = server_dir.join("server.pid"); - let stdin_socket = server_dir.join("stdin.sock"); - let stdout_socket = server_dir.join("stdout.sock"); - let stderr_socket = server_dir.join("stderr.sock"); - let log_file = logs_dir().join(format!("server-{}.log", identifier)); - - Ok(Self { - pid_file, - stdin_socket, - stdout_socket, - stderr_socket, - log_file, - }) - } -} - -#[derive(Debug, Error)] -pub(crate) enum ExecuteProxyError { - #[error("Failed to init server paths")] - ServerPath(#[from] ServerPathError), - - #[error(transparent)] - ServerNotRunning(#[from] ProxyLaunchError), - - #[error("Failed to check PidFile '{path}'")] - CheckPidFile { - #[source] - source: CheckPidError, - path: PathBuf, - }, - - #[error("Failed to kill existing server with pid '{pid}'")] - KillRunningServer { - #[source] - source: std::io::Error, - pid: u32, - }, - - #[error("failed to spawn server")] - SpawnServer(#[source] SpawnServerError), - - #[error("stdin_task failed")] - StdinTask(#[source] anyhow::Error), - #[error("stdout_task failed")] - StdoutTask(#[source] anyhow::Error), - #[error("stderr_task failed")] - StderrTask(#[source] anyhow::Error), -} - -pub(crate) fn execute_proxy( - identifier: String, - is_reconnecting: bool, -) -> Result<(), ExecuteProxyError> { - init_logging_proxy(); - - let server_paths = ServerPaths::new(&identifier)?; - - let id = std::process::id().to_string(); - smol::spawn(crashes::init(crashes::InitCrashHandler { - session_id: id, - zed_version: VERSION.to_owned(), - binary: "zed-remote-server".to_string(), - release_channel: release_channel::RELEASE_CHANNEL_NAME.clone(), - commit_sha: option_env!("ZED_COMMIT_SHA").unwrap_or("no_sha").to_owned(), - })) - .detach(); - - log::info!("starting proxy process. PID: {}", std::process::id()); - smol::block_on(async { - let server_pid = check_pid_file(&server_paths.pid_file) - .await - .map_err(|source| ExecuteProxyError::CheckPidFile { - source, - path: server_paths.pid_file.clone(), - })?; - let server_running = server_pid.is_some(); - if is_reconnecting { - if !server_running { - log::error!("attempted to reconnect, but no server running"); - return Err(ExecuteProxyError::ServerNotRunning( - ProxyLaunchError::ServerNotRunning, - )); - } - } else { - if let Some(pid) = server_pid { - log::info!( - "proxy found server already running with PID {}. Killing process and cleaning up files...", - pid - ); - kill_running_server(pid, &server_paths).await?; - } - - spawn_server(&server_paths) - .await - .map_err(ExecuteProxyError::SpawnServer)?; - }; - Ok(()) - })?; - - let stdin_task = smol::spawn(async move { - let stdin = Async::new(std::io::stdin())?; - let stream = smol::net::unix::UnixStream::connect(&server_paths.stdin_socket).await?; - handle_io(stdin, stream, "stdin").await - }); - - let stdout_task: smol::Task> = smol::spawn(async move { - let stdout = Async::new(std::io::stdout())?; - let stream = smol::net::unix::UnixStream::connect(&server_paths.stdout_socket).await?; - handle_io(stream, stdout, "stdout").await - }); - - let stderr_task: smol::Task> = smol::spawn(async move { - let mut stderr = Async::new(std::io::stderr())?; - let mut stream = smol::net::unix::UnixStream::connect(&server_paths.stderr_socket).await?; - let mut stderr_buffer = vec![0; 2048]; - loop { - match stream - .read(&mut stderr_buffer) - .await - .context("reading stderr")? - { - 0 => { - let error = - std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "stderr closed"); - Err(anyhow!(error))?; - } - n => { - stderr.write_all(&stderr_buffer[..n]).await?; - stderr.flush().await?; - } - } - } - }); - - if let Err(forwarding_result) = smol::block_on(async move { - futures::select! { - result = stdin_task.fuse() => result.map_err(ExecuteProxyError::StdinTask), - result = stdout_task.fuse() => result.map_err(ExecuteProxyError::StdoutTask), - result = stderr_task.fuse() => result.map_err(ExecuteProxyError::StderrTask), - } - }) { - log::error!( - "encountered error while forwarding messages: {:?}, terminating...", - forwarding_result - ); - return Err(forwarding_result); - } - - Ok(()) -} - -async fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<(), ExecuteProxyError> { - log::info!("killing existing server with PID {}", pid); - new_smol_command("kill") - .arg(pid.to_string()) - .output() - .await - .map_err(|source| ExecuteProxyError::KillRunningServer { source, pid })?; - - for file in [ - &paths.pid_file, - &paths.stdin_socket, - &paths.stdout_socket, - &paths.stderr_socket, - ] { - log::debug!("cleaning up file {:?} before starting new server", file); - std::fs::remove_file(file).ok(); - } - Ok(()) -} - -#[derive(Debug, Error)] -pub(crate) enum SpawnServerError { - #[error("failed to remove stdin socket")] - RemoveStdinSocket(#[source] std::io::Error), - - #[error("failed to remove stdout socket")] - RemoveStdoutSocket(#[source] std::io::Error), - - #[error("failed to remove stderr socket")] - RemoveStderrSocket(#[source] std::io::Error), - - #[error("failed to get current_exe")] - CurrentExe(#[source] std::io::Error), - - #[error("failed to launch server process")] - ProcessStatus(#[source] std::io::Error), - - #[error("failed to launch and detach server process: {status}\n{paths}")] - LaunchStatus { status: ExitStatus, paths: String }, -} - -async fn spawn_server(paths: &ServerPaths) -> Result<(), SpawnServerError> { - if paths.stdin_socket.exists() { - std::fs::remove_file(&paths.stdin_socket).map_err(SpawnServerError::RemoveStdinSocket)?; - } - if paths.stdout_socket.exists() { - std::fs::remove_file(&paths.stdout_socket).map_err(SpawnServerError::RemoveStdoutSocket)?; - } - if paths.stderr_socket.exists() { - std::fs::remove_file(&paths.stderr_socket).map_err(SpawnServerError::RemoveStderrSocket)?; - } - - let binary_name = std::env::current_exe().map_err(SpawnServerError::CurrentExe)?; - let mut server_process = new_smol_command(binary_name); - server_process - .arg("run") - .arg("--log-file") - .arg(&paths.log_file) - .arg("--pid-file") - .arg(&paths.pid_file) - .arg("--stdin-socket") - .arg(&paths.stdin_socket) - .arg("--stdout-socket") - .arg(&paths.stdout_socket) - .arg("--stderr-socket") - .arg(&paths.stderr_socket); - - let status = server_process - .status() - .await - .map_err(SpawnServerError::ProcessStatus)?; - - if !status.success() { - return Err(SpawnServerError::LaunchStatus { - status, - paths: format!( - "log file: {:?}, pid file: {:?}", - paths.log_file, paths.pid_file, - ), - }); - } - - let mut total_time_waited = std::time::Duration::from_secs(0); - let wait_duration = std::time::Duration::from_millis(20); - while !paths.stdout_socket.exists() - || !paths.stdin_socket.exists() - || !paths.stderr_socket.exists() - { - log::debug!("waiting for server to be ready to accept connections..."); - std::thread::sleep(wait_duration); - total_time_waited += wait_duration; - } - - log::info!( - "server ready to accept connections. total time waited: {:?}", - total_time_waited - ); - - Ok(()) -} - -#[derive(Debug, Error)] -#[error("Failed to remove PID file for missing process (pid `{pid}`")] -pub(crate) struct CheckPidError { - #[source] - source: std::io::Error, - pid: u32, -} - -async fn check_pid_file(path: &Path) -> Result, CheckPidError> { - let Some(pid) = std::fs::read_to_string(&path) - .ok() - .and_then(|contents| contents.parse::().ok()) - else { - return Ok(None); - }; - - log::debug!("Checking if process with PID {} exists...", pid); - match new_smol_command("kill") - .arg("-0") - .arg(pid.to_string()) - .output() - .await - { - Ok(output) if output.status.success() => { - log::debug!( - "Process with PID {} exists. NOT spawning new server, but attaching to existing one.", - pid - ); - Ok(Some(pid)) - } - _ => { - log::debug!( - "Found PID file, but process with that PID does not exist. Removing PID file." - ); - std::fs::remove_file(&path).map_err(|source| CheckPidError { source, pid })?; - Ok(None) - } - } -} - -fn write_pid_file(path: &Path) -> Result<()> { - if path.exists() { - std::fs::remove_file(path)?; - } - let pid = std::process::id().to_string(); - log::debug!("writing PID {} to file {:?}", pid, path); - std::fs::write(path, pid).context("Failed to write PID file") -} - -async fn handle_io(mut reader: R, mut writer: W, socket_name: &str) -> Result<()> -where - R: AsyncRead + Unpin, - W: AsyncWrite + Unpin, -{ - use remote::protocol::{read_message_raw, write_size_prefixed_buffer}; - - let mut buffer = Vec::new(); - loop { - read_message_raw(&mut reader, &mut buffer) - .await - .with_context(|| format!("failed to read message from {}", socket_name))?; - write_size_prefixed_buffer(&mut writer, &mut buffer) - .await - .with_context(|| format!("failed to write message to {}", socket_name))?; - writer.flush().await?; - buffer.clear(); - } -} - -fn initialize_settings( - session: AnyProtoClient, - fs: Arc, - cx: &mut App, -) -> watch::Receiver> { - let user_settings_file_rx = - watch_config_file(cx.background_executor(), fs, paths::settings_file().clone()); - - handle_settings_file_changes(user_settings_file_rx, cx, { - move |err, _cx| { - if let Some(e) = err { - log::info!("Server settings failed to change: {}", e); - - session - .send(proto::Toast { - project_id: REMOTE_SERVER_PROJECT_ID, - notification_id: "server-settings-failed".to_string(), - message: format!( - "Error in settings on remote host {:?}: {}", - paths::settings_file(), - e - ), - }) - .log_err(); - } else { - session - .send(proto::HideToast { - project_id: REMOTE_SERVER_PROJECT_ID, - notification_id: "server-settings-failed".to_string(), - }) - .log_err(); - } - } - }); - - let (mut tx, rx) = watch::channel(None); - let mut node_settings = None; - cx.observe_global::(move |cx| { - let new_node_settings = &ProjectSettings::get_global(cx).node; - if Some(new_node_settings) != node_settings.as_ref() { - log::info!("Got new node settings: {new_node_settings:?}"); - let options = NodeBinaryOptions { - allow_path_lookup: !new_node_settings.ignore_system_version, - // TODO: Implement this setting - allow_binary_download: true, - use_paths: new_node_settings.path.as_ref().map(|node_path| { - let node_path = PathBuf::from(shellexpand::tilde(node_path).as_ref()); - let npm_path = new_node_settings - .npm_path - .as_ref() - .map(|path| PathBuf::from(shellexpand::tilde(&path).as_ref())); - ( - node_path.clone(), - npm_path.unwrap_or_else(|| { - let base_path = PathBuf::new(); - node_path.parent().unwrap_or(&base_path).join("npm") - }), - ) - }), - }; - node_settings = Some(new_node_settings.clone()); - tx.send(Some(options)).ok(); - } - }) - .detach(); - - rx -} - -pub fn handle_settings_file_changes( - mut server_settings_file: mpsc::UnboundedReceiver, - cx: &mut App, - settings_changed: impl Fn(Option, &mut App) + 'static, -) { - let server_settings_content = cx - .background_executor() - .block(server_settings_file.next()) - .unwrap(); - SettingsStore::update_global(cx, |store, cx| { - store - .set_server_settings(&server_settings_content, cx) - .log_err(); - }); - cx.spawn(async move |cx| { - while let Some(server_settings_content) = server_settings_file.next().await { - let result = cx.update_global(|store: &mut SettingsStore, cx| { - let result = store.set_server_settings(&server_settings_content, cx); - if let Err(err) = &result { - log::error!("Failed to load server settings: {err}"); - } - settings_changed(result.err(), cx); - cx.refresh_windows(); - }); - if result.is_err() { - break; // App dropped - } - } - }) - .detach(); -} - -fn read_proxy_settings(cx: &mut Context) -> Option { - let proxy_str = ProxySettings::get_global(cx).proxy.to_owned(); - - proxy_str - .as_ref() - .and_then(|input: &String| { - input - .parse::() - .inspect_err(|e| log::error!("Error parsing proxy settings: {}", e)) - .ok() - }) - .or_else(read_proxy_from_env) -} - -fn daemonize() -> Result> { - match fork::fork().map_err(|e| anyhow!("failed to call fork with error code {e}"))? { - fork::Fork::Parent(_) => { - return Ok(ControlFlow::Break(())); - } - fork::Fork::Child => {} - } - - // Once we've detached from the parent, we want to close stdout/stderr/stdin - // so that the outer SSH process is not attached to us in any way anymore. - unsafe { redirect_standard_streams() }?; - - Ok(ControlFlow::Continue(())) -} - -unsafe fn redirect_standard_streams() -> Result<()> { - let devnull_fd = unsafe { libc::open(b"/dev/null\0" as *const [u8; 10] as _, libc::O_RDWR) }; - anyhow::ensure!(devnull_fd != -1, "failed to open /dev/null"); - - let process_stdio = |name, fd| { - let reopened_fd = unsafe { libc::dup2(devnull_fd, fd) }; - anyhow::ensure!( - reopened_fd != -1, - format!("failed to redirect {} to /dev/null", name) - ); - Ok(()) - }; - - process_stdio("stdin", libc::STDIN_FILENO)?; - process_stdio("stdout", libc::STDOUT_FILENO)?; - process_stdio("stderr", libc::STDERR_FILENO)?; - - anyhow::ensure!( - unsafe { libc::close(devnull_fd) != -1 }, - "failed to close /dev/null fd after redirecting" - ); - - Ok(()) -} - -fn cleanup_old_binaries() -> Result<()> { - let server_dir = paths::remote_server_dir_relative(); - let release_channel = release_channel::RELEASE_CHANNEL.dev_name(); - let prefix = format!("zed-remote-server-{}-", release_channel); - - for entry in std::fs::read_dir(server_dir.as_std_path())? { - let path = entry?.path(); - - if let Some(file_name) = path.file_name() - && let Some(version) = file_name.to_string_lossy().strip_prefix(&prefix) - && !is_new_version(version) - && !is_file_in_use(file_name) - { - log::info!("removing old remote server binary: {:?}", path); - std::fs::remove_file(&path)?; - } - } - - Ok(()) -} - -fn is_new_version(version: &str) -> bool { - semver::Version::from_str(version) - .ok() - .zip(semver::Version::from_str(env!("ZED_PKG_VERSION")).ok()) - .is_some_and(|(version, current_version)| version >= current_version) -} - -fn is_file_in_use(file_name: &OsStr) -> bool { - let info = sysinfo::System::new_with_specifics(sysinfo::RefreshKind::nothing().with_processes( - sysinfo::ProcessRefreshKind::nothing().with_exe(sysinfo::UpdateKind::Always), - )); - - for process in info.processes().values() { - if process - .exe() - .is_some_and(|exe| exe.file_name().is_some_and(|name| name == file_name)) - { - return true; - } - } - - false -} diff --git a/crates/repl/Cargo.toml b/crates/repl/Cargo.toml deleted file mode 100644 index 14040ba484..0000000000 --- a/crates/repl/Cargo.toml +++ /dev/null @@ -1,70 +0,0 @@ -[package] -name = "repl" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/repl.rs" -doctest = false - -[dependencies] -alacritty_terminal.workspace = true -anyhow.workspace = true -async-dispatcher.workspace = true -async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manual-roots", "tokio-runtime"] } -base64.workspace = true -client.workspace = true -collections.workspace = true -command_palette_hooks.workspace = true -editor.workspace = true -feature_flags.workspace = true -file_icons.workspace = true -futures.workspace = true -gpui.workspace = true -http_client.workspace = true -image.workspace = true -jupyter-websocket-client.workspace = true -jupyter-protocol.workspace = true -language.workspace = true -log.workspace = true -markdown_preview.workspace = true -menu.workspace = true -multi_buffer.workspace = true -nbformat.workspace = true -project.workspace = true -runtimelib.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smol.workspace = true -telemetry.workspace = true -terminal.workspace = true -terminal_view.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -uuid.workspace = true -workspace.workspace = true -picker.workspace = true - -[dev-dependencies] -editor = { workspace = true, features = ["test-support"] } -env_logger.workspace = true -gpui = { workspace = true, features = ["test-support"] } -http_client = { workspace = true, features = ["test-support"] } -indoc.workspace = true -language = { workspace = true, features = ["test-support"] } -languages = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } -settings = { workspace = true, features = ["test-support"] } -terminal_view = { workspace = true, features = ["test-support"] } -theme = { workspace = true, features = ["test-support"] } -tree-sitter-md.workspace = true -tree-sitter-typescript.workspace = true -tree-sitter-python.workspace = true -util = { workspace = true, features = ["test-support"] } diff --git a/crates/repl/LICENSE-GPL b/crates/repl/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/repl/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/repl/src/components.rs b/crates/repl/src/components.rs deleted file mode 100644 index 53236bc6a8..0000000000 --- a/crates/repl/src/components.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod kernel_list_item; -mod kernel_options; - -pub use kernel_list_item::*; -pub use kernel_options::*; diff --git a/crates/repl/src/components/kernel_list_item.rs b/crates/repl/src/components/kernel_list_item.rs deleted file mode 100644 index 467407fbd2..0000000000 --- a/crates/repl/src/components/kernel_list_item.rs +++ /dev/null @@ -1,60 +0,0 @@ -use gpui::AnyElement; -use ui::{Indicator, ListItem, prelude::*}; - -use crate::KernelSpecification; - -#[derive(IntoElement)] -pub struct KernelListItem { - kernel_specification: KernelSpecification, - status_color: Color, - buttons: Vec, - children: Vec, -} - -impl KernelListItem { - pub fn new(kernel_specification: KernelSpecification) -> Self { - Self { - kernel_specification, - status_color: Color::Disabled, - buttons: Vec::new(), - children: Vec::new(), - } - } - - pub fn status_color(mut self, color: Color) -> Self { - self.status_color = color; - self - } - - pub fn button(mut self, button: impl IntoElement) -> Self { - self.buttons.push(button.into_any_element()); - self - } - - pub fn buttons(mut self, buttons: impl IntoIterator) -> Self { - self.buttons - .extend(buttons.into_iter().map(|button| button.into_any_element())); - self - } -} - -impl ParentElement for KernelListItem { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements); - } -} - -impl RenderOnce for KernelListItem { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - ListItem::new(self.kernel_specification.name()) - .selectable(false) - .start_slot( - h_flex() - .size_3() - .justify_center() - .child(Indicator::dot().color(self.status_color)), - ) - .children(self.children) - .end_slot(h_flex().gap_2().children(self.buttons)) - } -} diff --git a/crates/repl/src/components/kernel_options.rs b/crates/repl/src/components/kernel_options.rs deleted file mode 100644 index bceefd08cc..0000000000 --- a/crates/repl/src/components/kernel_options.rs +++ /dev/null @@ -1,283 +0,0 @@ -use crate::KERNEL_DOCS_URL; -use crate::kernels::KernelSpecification; -use crate::repl_store::ReplStore; - -use gpui::AnyView; -use gpui::DismissEvent; - -use gpui::FontWeight; -use picker::Picker; -use picker::PickerDelegate; -use project::WorktreeId; - -use std::sync::Arc; -use ui::ListItemSpacing; - -use gpui::SharedString; -use gpui::Task; -use ui::{ListItem, PopoverMenu, PopoverMenuHandle, PopoverTrigger, prelude::*}; - -type OnSelect = Box; - -#[derive(IntoElement)] -pub struct KernelSelector -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, -{ - handle: Option>>, - on_select: OnSelect, - trigger: T, - tooltip: TT, - info_text: Option, - worktree_id: WorktreeId, -} - -pub struct KernelPickerDelegate { - all_kernels: Vec, - filtered_kernels: Vec, - selected_kernelspec: Option, - on_select: OnSelect, -} - -// Helper function to truncate long paths -fn truncate_path(path: &SharedString, max_length: usize) -> SharedString { - if path.len() <= max_length { - path.to_string().into() - } else { - let truncated = path.chars().rev().take(max_length - 3).collect::(); - format!("...{}", truncated.chars().rev().collect::()).into() - } -} - -impl KernelSelector -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, -{ - pub fn new(on_select: OnSelect, worktree_id: WorktreeId, trigger: T, tooltip: TT) -> Self { - KernelSelector { - on_select, - handle: None, - trigger, - tooltip, - info_text: None, - worktree_id, - } - } - - pub fn with_handle(mut self, handle: PopoverMenuHandle>) -> Self { - self.handle = Some(handle); - self - } - - pub fn with_info_text(mut self, text: impl Into) -> Self { - self.info_text = Some(text.into()); - self - } -} - -impl PickerDelegate for KernelPickerDelegate { - type ListItem = ListItem; - - fn match_count(&self) -> usize { - self.filtered_kernels.len() - } - - fn selected_index(&self) -> usize { - if let Some(kernelspec) = self.selected_kernelspec.as_ref() { - self.filtered_kernels - .iter() - .position(|k| k == kernelspec) - .unwrap_or(0) - } else { - 0 - } - } - - fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context>) { - self.selected_kernelspec = self.filtered_kernels.get(ix).cloned(); - cx.notify(); - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Select a kernel...".into() - } - - fn update_matches( - &mut self, - query: String, - _window: &mut Window, - _cx: &mut Context>, - ) -> Task<()> { - let all_kernels = self.all_kernels.clone(); - - if query.is_empty() { - self.filtered_kernels = all_kernels; - return Task::ready(()); - } - - self.filtered_kernels = if query.is_empty() { - all_kernels - } else { - all_kernels - .into_iter() - .filter(|kernel| kernel.name().to_lowercase().contains(&query.to_lowercase())) - .collect() - }; - - Task::ready(()) - } - - fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { - if let Some(kernelspec) = &self.selected_kernelspec { - (self.on_select)(kernelspec.clone(), window, cx); - cx.emit(DismissEvent); - } - } - - fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context>) {} - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - let kernelspec = self.filtered_kernels.get(ix)?; - let is_selected = self.selected_kernelspec.as_ref() == Some(kernelspec); - let icon = kernelspec.icon(cx); - - let (name, kernel_type, path_or_url) = match kernelspec { - KernelSpecification::Jupyter(_) => (kernelspec.name(), "Jupyter", None), - KernelSpecification::PythonEnv(_) => ( - kernelspec.name(), - "Python Env", - Some(truncate_path(&kernelspec.path(), 42)), - ), - KernelSpecification::Remote(_) => ( - kernelspec.name(), - "Remote", - Some(truncate_path(&kernelspec.path(), 42)), - ), - }; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child( - h_flex() - .w_full() - .gap_3() - .child(icon.color(Color::Default).size(IconSize::Medium)) - .child( - v_flex() - .flex_grow() - .gap_0p5() - .child( - h_flex() - .justify_between() - .child( - div().w_48().text_ellipsis().child( - Label::new(name) - .weight(FontWeight::MEDIUM) - .size(LabelSize::Default), - ), - ) - .when_some(path_or_url, |flex, path| { - flex.text_ellipsis().child( - Label::new(path) - .size(LabelSize::Small) - .color(Color::Muted), - ) - }), - ) - .child( - h_flex() - .gap_1() - .child( - Label::new(kernelspec.language()) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( - Label::new(kernel_type) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ), - ), - ) - .when(is_selected, |item| { - item.end_slot( - Icon::new(IconName::Check) - .color(Color::Accent) - .size(IconSize::Small), - ) - }), - ) - } - - 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() - .child( - Button::new("kernel-docs", "Kernel Docs") - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .icon_position(IconPosition::End) - .on_click(move |_, _, cx| cx.open_url(KERNEL_DOCS_URL)), - ) - .into_any(), - ) - } -} - -impl RenderOnce for KernelSelector -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, -{ - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let store = ReplStore::global(cx).read(cx); - - let all_kernels: Vec = store - .kernel_specifications_for_worktree(self.worktree_id) - .cloned() - .collect(); - - let selected_kernelspec = store.active_kernelspec(self.worktree_id, None, cx); - - let delegate = KernelPickerDelegate { - on_select: self.on_select, - all_kernels: all_kernels.clone(), - filtered_kernels: all_kernels, - selected_kernelspec, - }; - - let picker_view = cx.new(|cx| { - Picker::uniform_list(delegate, window, cx) - .width(rems(30.)) - .max_height(Some(rems(20.).into())) - }); - - PopoverMenu::new("kernel-switcher") - .menu(move |_window, _cx| Some(picker_view.clone())) - .trigger_with_tooltip(self.trigger, self.tooltip) - .attach(gpui::Corner::BottomLeft) - .when_some(self.handle, |menu, handle| menu.with_handle(handle)) - } -} diff --git a/crates/repl/src/jupyter_settings.rs b/crates/repl/src/jupyter_settings.rs deleted file mode 100644 index 0adf80dc66..0000000000 --- a/crates/repl/src/jupyter_settings.rs +++ /dev/null @@ -1,28 +0,0 @@ -use collections::HashMap; - -use editor::EditorSettings; -use gpui::App; -use settings::{RegisterSetting, Settings}; - -#[derive(Debug, Default, RegisterSetting)] -pub struct JupyterSettings { - pub kernel_selections: HashMap, -} - -impl JupyterSettings { - pub fn enabled(cx: &App) -> bool { - // In order to avoid a circular dependency between `editor` and `repl` crates, - // we put the `enable` flag on its settings. - // This allows the editor to set up context for key bindings/actions. - EditorSettings::jupyter_enabled(cx) - } -} - -impl Settings for JupyterSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let jupyter = content.editor.jupyter.clone().unwrap(); - Self { - kernel_selections: jupyter.kernel_selections.unwrap_or_default(), - } - } -} diff --git a/crates/repl/src/kernels/mod.rs b/crates/repl/src/kernels/mod.rs deleted file mode 100644 index ab8f27121e..0000000000 --- a/crates/repl/src/kernels/mod.rs +++ /dev/null @@ -1,259 +0,0 @@ -mod native_kernel; -use std::{fmt::Debug, future::Future, path::PathBuf}; - -use futures::{ - channel::mpsc::{self, Receiver}, - future::Shared, - stream, -}; -use gpui::{App, Entity, Task, Window}; -use language::LanguageName; -pub use native_kernel::*; - -mod remote_kernels; -use project::{Project, ProjectPath, Toolchains, WorktreeId}; -pub use remote_kernels::*; - -use anyhow::Result; -use jupyter_protocol::JupyterKernelspec; -use runtimelib::{ExecutionState, JupyterMessage, KernelInfoReply}; -use ui::{Icon, IconName, SharedString}; -use util::rel_path::RelPath; - -pub type JupyterMessageChannel = stream::SelectAll>; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum KernelSpecification { - Remote(RemoteKernelSpecification), - Jupyter(LocalKernelSpecification), - PythonEnv(LocalKernelSpecification), -} - -impl KernelSpecification { - pub fn name(&self) -> SharedString { - match self { - Self::Jupyter(spec) => spec.name.clone().into(), - Self::PythonEnv(spec) => spec.name.clone().into(), - Self::Remote(spec) => spec.name.clone().into(), - } - } - - pub fn type_name(&self) -> SharedString { - match self { - Self::Jupyter(_) => "Jupyter".into(), - Self::PythonEnv(_) => "Python Environment".into(), - Self::Remote(_) => "Remote".into(), - } - } - - pub fn path(&self) -> SharedString { - SharedString::from(match self { - Self::Jupyter(spec) => spec.path.to_string_lossy().into_owned(), - Self::PythonEnv(spec) => spec.path.to_string_lossy().into_owned(), - Self::Remote(spec) => spec.url.to_string(), - }) - } - - pub fn language(&self) -> SharedString { - SharedString::from(match self { - Self::Jupyter(spec) => spec.kernelspec.language.clone(), - Self::PythonEnv(spec) => spec.kernelspec.language.clone(), - Self::Remote(spec) => spec.kernelspec.language.clone(), - }) - } - - pub fn icon(&self, cx: &App) -> Icon { - let lang_name = match self { - Self::Jupyter(spec) => spec.kernelspec.language.clone(), - Self::PythonEnv(spec) => spec.kernelspec.language.clone(), - Self::Remote(spec) => spec.kernelspec.language.clone(), - }; - - file_icons::FileIcons::get(cx) - .get_icon_for_type(&lang_name.to_lowercase(), cx) - .map(Icon::from_path) - .unwrap_or(Icon::new(IconName::ReplNeutral)) - } -} - -pub fn python_env_kernel_specifications( - project: &Entity, - worktree_id: WorktreeId, - cx: &mut App, -) -> impl Future>> + use<> { - let python_language = LanguageName::new_static("Python"); - let toolchains = project.read(cx).available_toolchains( - ProjectPath { - worktree_id, - path: RelPath::empty().into(), - }, - python_language, - cx, - ); - let background_executor = cx.background_executor().clone(); - - async move { - let (toolchains, user_toolchains) = if let Some(Toolchains { - toolchains, - root_path: _, - user_toolchains, - }) = toolchains.await - { - (toolchains, user_toolchains) - } else { - return Ok(Vec::new()); - }; - - let kernelspecs = user_toolchains - .into_values() - .flatten() - .chain(toolchains.toolchains) - .map(|toolchain| { - background_executor.spawn(async move { - let python_path = toolchain.path.to_string(); - - // Check if ipykernel is installed - let ipykernel_check = util::command::new_smol_command(&python_path) - .args(&["-c", "import ipykernel"]) - .output() - .await; - - if ipykernel_check.is_ok() && ipykernel_check.unwrap().status.success() { - // Create a default kernelspec for this environment - let default_kernelspec = JupyterKernelspec { - argv: vec![ - python_path.clone(), - "-m".to_string(), - "ipykernel_launcher".to_string(), - "-f".to_string(), - "{connection_file}".to_string(), - ], - display_name: toolchain.name.to_string(), - language: "python".to_string(), - interrupt_mode: None, - metadata: None, - env: None, - }; - - Some(KernelSpecification::PythonEnv(LocalKernelSpecification { - name: toolchain.name.to_string(), - path: PathBuf::from(&python_path), - kernelspec: default_kernelspec, - })) - } else { - None - } - }) - }); - - let kernel_specs = futures::future::join_all(kernelspecs) - .await - .into_iter() - .flatten() - .collect(); - - anyhow::Ok(kernel_specs) - } -} - -pub trait RunningKernel: Send + Debug { - fn request_tx(&self) -> mpsc::Sender; - fn working_directory(&self) -> &PathBuf; - fn execution_state(&self) -> &ExecutionState; - fn set_execution_state(&mut self, state: ExecutionState); - fn kernel_info(&self) -> Option<&KernelInfoReply>; - fn set_kernel_info(&mut self, info: KernelInfoReply); - fn force_shutdown(&mut self, window: &mut Window, cx: &mut App) -> Task>; -} - -#[derive(Debug, Clone)] -pub enum KernelStatus { - Idle, - Busy, - Starting, - Error, - ShuttingDown, - Shutdown, - Restarting, -} - -impl KernelStatus { - pub fn is_connected(&self) -> bool { - matches!(self, KernelStatus::Idle | KernelStatus::Busy) - } -} - -impl ToString for KernelStatus { - fn to_string(&self) -> String { - match self { - KernelStatus::Idle => "Idle".to_string(), - KernelStatus::Busy => "Busy".to_string(), - KernelStatus::Starting => "Starting".to_string(), - KernelStatus::Error => "Error".to_string(), - KernelStatus::ShuttingDown => "Shutting Down".to_string(), - KernelStatus::Shutdown => "Shutdown".to_string(), - KernelStatus::Restarting => "Restarting".to_string(), - } - } -} - -#[derive(Debug)] -pub enum Kernel { - RunningKernel(Box), - StartingKernel(Shared>), - ErroredLaunch(String), - ShuttingDown, - Shutdown, - Restarting, -} - -impl From<&Kernel> for KernelStatus { - fn from(kernel: &Kernel) -> Self { - match kernel { - Kernel::RunningKernel(kernel) => match kernel.execution_state() { - ExecutionState::Idle => KernelStatus::Idle, - ExecutionState::Busy => KernelStatus::Busy, - ExecutionState::Unknown => KernelStatus::Error, - ExecutionState::Starting => KernelStatus::Starting, - ExecutionState::Restarting => KernelStatus::Restarting, - ExecutionState::Terminating => KernelStatus::ShuttingDown, - ExecutionState::AutoRestarting => KernelStatus::Restarting, - ExecutionState::Dead => KernelStatus::Error, - ExecutionState::Other(_) => KernelStatus::Error, - }, - Kernel::StartingKernel(_) => KernelStatus::Starting, - Kernel::ErroredLaunch(_) => KernelStatus::Error, - Kernel::ShuttingDown => KernelStatus::ShuttingDown, - Kernel::Shutdown => KernelStatus::Shutdown, - Kernel::Restarting => KernelStatus::Restarting, - } - } -} - -impl Kernel { - pub fn status(&self) -> KernelStatus { - self.into() - } - - pub fn set_execution_state(&mut self, status: &ExecutionState) { - if let Kernel::RunningKernel(running_kernel) = self { - running_kernel.set_execution_state(status.clone()); - } - } - - pub fn set_kernel_info(&mut self, kernel_info: &KernelInfoReply) { - if let Kernel::RunningKernel(running_kernel) = self { - running_kernel.set_kernel_info(kernel_info.clone()); - } - } - - pub fn is_shutting_down(&self) -> bool { - match self { - Kernel::Restarting | Kernel::ShuttingDown => true, - Kernel::RunningKernel(_) - | Kernel::StartingKernel(_) - | Kernel::ErroredLaunch(_) - | Kernel::Shutdown => false, - } - } -} diff --git a/crates/repl/src/kernels/native_kernel.rs b/crates/repl/src/kernels/native_kernel.rs deleted file mode 100644 index 8630768dec..0000000000 --- a/crates/repl/src/kernels/native_kernel.rs +++ /dev/null @@ -1,542 +0,0 @@ -use anyhow::{Context as _, Result}; -use futures::{ - AsyncBufReadExt as _, SinkExt as _, - channel::mpsc::{self}, - io::BufReader, - stream::{FuturesUnordered, SelectAll, StreamExt}, -}; -use gpui::{App, AppContext as _, Entity, EntityId, Task, Window}; -use jupyter_protocol::{ - ExecutionState, JupyterKernelspec, JupyterMessage, JupyterMessageContent, KernelInfoReply, - connection_info::{ConnectionInfo, Transport}, -}; -use project::Fs; -use runtimelib::dirs; -use smol::{net::TcpListener, process::Command}; -use std::{ - env, - fmt::Debug, - net::{IpAddr, Ipv4Addr, SocketAddr}, - path::PathBuf, - sync::Arc, -}; -use uuid::Uuid; - -use crate::Session; - -use super::RunningKernel; - -#[derive(Debug, Clone)] -pub struct LocalKernelSpecification { - pub name: String, - pub path: PathBuf, - pub kernelspec: JupyterKernelspec, -} - -impl PartialEq for LocalKernelSpecification { - fn eq(&self, other: &Self) -> bool { - self.name == other.name && self.path == other.path - } -} - -impl Eq for LocalKernelSpecification {} - -impl LocalKernelSpecification { - #[must_use] - fn command(&self, connection_path: &PathBuf) -> Result { - let argv = &self.kernelspec.argv; - - anyhow::ensure!(!argv.is_empty(), "Empty argv in kernelspec {}", self.name); - anyhow::ensure!(argv.len() >= 2, "Invalid argv in kernelspec {}", self.name); - anyhow::ensure!( - argv.iter().any(|arg| arg == "{connection_file}"), - "Missing 'connection_file' in argv in kernelspec {}", - self.name - ); - - let mut cmd = util::command::new_smol_command(&argv[0]); - - for arg in &argv[1..] { - if arg == "{connection_file}" { - cmd.arg(connection_path); - } else { - cmd.arg(arg); - } - } - - if let Some(env) = &self.kernelspec.env { - cmd.envs(env); - } - - Ok(cmd) - } -} - -// Find a set of open ports. This creates a listener with port set to 0. The listener will be closed at the end when it goes out of scope. -// There's a race condition between closing the ports and usage by a kernel, but it's inherent to the Jupyter protocol. -async fn peek_ports(ip: IpAddr) -> Result<[u16; 5]> { - let mut addr_zeroport: SocketAddr = SocketAddr::new(ip, 0); - addr_zeroport.set_port(0); - let mut ports: [u16; 5] = [0; 5]; - for i in 0..5 { - let listener = TcpListener::bind(addr_zeroport).await?; - let addr = listener.local_addr()?; - ports[i] = addr.port(); - } - Ok(ports) -} - -pub struct NativeRunningKernel { - pub process: smol::process::Child, - connection_path: PathBuf, - _process_status_task: Option>, - pub working_directory: PathBuf, - pub request_tx: mpsc::Sender, - pub execution_state: ExecutionState, - pub kernel_info: Option, -} - -impl Debug for NativeRunningKernel { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("RunningKernel") - .field("process", &self.process) - .finish() - } -} - -impl NativeRunningKernel { - pub fn new( - kernel_specification: LocalKernelSpecification, - entity_id: EntityId, - working_directory: PathBuf, - fs: Arc, - // todo: convert to weak view - session: Entity, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - window.spawn(cx, async move |cx| { - let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); - let ports = peek_ports(ip).await?; - - let connection_info = ConnectionInfo { - transport: Transport::TCP, - ip: ip.to_string(), - stdin_port: ports[0], - control_port: ports[1], - hb_port: ports[2], - shell_port: ports[3], - iopub_port: ports[4], - signature_scheme: "hmac-sha256".to_string(), - key: uuid::Uuid::new_v4().to_string(), - kernel_name: Some(format!("zed-{}", kernel_specification.name)), - }; - - let runtime_dir = dirs::runtime_dir(); - fs.create_dir(&runtime_dir) - .await - .with_context(|| format!("Failed to create jupyter runtime dir {runtime_dir:?}"))?; - let connection_path = runtime_dir.join(format!("kernel-zed-{entity_id}.json")); - let content = serde_json::to_string(&connection_info)?; - fs.atomic_write(connection_path.clone(), content).await?; - - let mut cmd = kernel_specification.command(&connection_path)?; - - let mut process = cmd - .current_dir(&working_directory) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .stdin(std::process::Stdio::piped()) - .kill_on_drop(true) - .spawn() - .context("failed to start the kernel process")?; - - let session_id = Uuid::new_v4().to_string(); - - let mut iopub_socket = - runtimelib::create_client_iopub_connection(&connection_info, "", &session_id) - .await?; - let mut shell_socket = - runtimelib::create_client_shell_connection(&connection_info, &session_id).await?; - let mut control_socket = - runtimelib::create_client_control_connection(&connection_info, &session_id).await?; - - let (request_tx, mut request_rx) = - futures::channel::mpsc::channel::(100); - - let (mut control_reply_tx, control_reply_rx) = futures::channel::mpsc::channel(100); - let (mut shell_reply_tx, shell_reply_rx) = futures::channel::mpsc::channel(100); - - let mut messages_rx = SelectAll::new(); - messages_rx.push(control_reply_rx); - messages_rx.push(shell_reply_rx); - - cx.spawn({ - let session = session.clone(); - - async move |cx| { - while let Some(message) = messages_rx.next().await { - session - .update_in(cx, |session, window, cx| { - session.route(&message, window, cx); - }) - .ok(); - } - } - }) - .detach(); - - // iopub task - let iopub_task = cx.spawn({ - let session = session.clone(); - - async move |cx| -> anyhow::Result<()> { - loop { - let message = iopub_socket.read().await?; - session - .update_in(cx, |session, window, cx| { - session.route(&message, window, cx); - }) - .ok(); - } - } - }); - - let (mut control_request_tx, mut control_request_rx) = - futures::channel::mpsc::channel(100); - let (mut shell_request_tx, mut shell_request_rx) = futures::channel::mpsc::channel(100); - - let routing_task = cx.background_spawn({ - async move { - while let Some(message) = request_rx.next().await { - match message.content { - JupyterMessageContent::DebugRequest(_) - | JupyterMessageContent::InterruptRequest(_) - | JupyterMessageContent::ShutdownRequest(_) => { - control_request_tx.send(message).await?; - } - _ => { - shell_request_tx.send(message).await?; - } - } - } - anyhow::Ok(()) - } - }); - - let shell_task = cx.background_spawn({ - async move { - while let Some(message) = shell_request_rx.next().await { - shell_socket.send(message).await.ok(); - let reply = shell_socket.read().await?; - shell_reply_tx.send(reply).await?; - } - anyhow::Ok(()) - } - }); - - let control_task = cx.background_spawn({ - async move { - while let Some(message) = control_request_rx.next().await { - control_socket.send(message).await.ok(); - let reply = control_socket.read().await?; - control_reply_tx.send(reply).await?; - } - anyhow::Ok(()) - } - }); - - let stderr = process.stderr.take(); - - cx.spawn(async move |_cx| { - if stderr.is_none() { - return; - } - let reader = BufReader::new(stderr.unwrap()); - let mut lines = reader.lines(); - while let Some(Ok(line)) = lines.next().await { - log::error!("kernel: {}", line); - } - }) - .detach(); - - let stdout = process.stdout.take(); - - cx.spawn(async move |_cx| { - if stdout.is_none() { - return; - } - let reader = BufReader::new(stdout.unwrap()); - let mut lines = reader.lines(); - while let Some(Ok(line)) = lines.next().await { - log::info!("kernel: {}", line); - } - }) - .detach(); - - cx.spawn({ - let session = session.clone(); - async move |cx| { - async fn with_name( - name: &'static str, - task: Task>, - ) -> (&'static str, Result<()>) { - (name, task.await) - } - - let mut tasks = FuturesUnordered::new(); - tasks.push(with_name("iopub task", iopub_task)); - tasks.push(with_name("shell task", shell_task)); - tasks.push(with_name("control task", control_task)); - tasks.push(with_name("routing task", routing_task)); - - while let Some((name, result)) = tasks.next().await { - if let Err(err) = result { - log::error!("kernel: handling failed for {name}: {err:?}"); - - session - .update(cx, |session, cx| { - session.kernel_errored( - format!("handling failed for {name}: {err}"), - cx, - ); - cx.notify(); - }) - .ok(); - } - } - } - }) - .detach(); - - let status = process.status(); - - let process_status_task = cx.spawn(async move |cx| { - let error_message = match status.await { - Ok(status) => { - if status.success() { - log::info!("kernel process exited successfully"); - return; - } - - format!("kernel process exited with status: {:?}", status) - } - Err(err) => { - format!("kernel process exited with error: {:?}", err) - } - }; - - log::error!("{}", error_message); - - session - .update(cx, |session, cx| { - session.kernel_errored(error_message, cx); - - cx.notify(); - }) - .ok(); - }); - - anyhow::Ok(Box::new(Self { - process, - request_tx, - working_directory, - _process_status_task: Some(process_status_task), - connection_path, - execution_state: ExecutionState::Idle, - kernel_info: None, - }) as Box) - }) - } -} - -impl RunningKernel for NativeRunningKernel { - fn request_tx(&self) -> mpsc::Sender { - self.request_tx.clone() - } - - fn working_directory(&self) -> &PathBuf { - &self.working_directory - } - - fn execution_state(&self) -> &ExecutionState { - &self.execution_state - } - - fn set_execution_state(&mut self, state: ExecutionState) { - self.execution_state = state; - } - - fn kernel_info(&self) -> Option<&KernelInfoReply> { - self.kernel_info.as_ref() - } - - fn set_kernel_info(&mut self, info: KernelInfoReply) { - self.kernel_info = Some(info); - } - - fn force_shutdown(&mut self, _window: &mut Window, _cx: &mut App) -> Task> { - self._process_status_task.take(); - self.request_tx.close_channel(); - Task::ready(self.process.kill().context("killing the kernel process")) - } -} - -impl Drop for NativeRunningKernel { - fn drop(&mut self) { - std::fs::remove_file(&self.connection_path).ok(); - self.request_tx.close_channel(); - self.process.kill().ok(); - } -} - -async fn read_kernelspec_at( - // Path should be a directory to a jupyter kernelspec, as in - // /usr/local/share/jupyter/kernels/python3 - kernel_dir: PathBuf, - fs: &dyn Fs, -) -> Result { - let path = kernel_dir; - let kernel_name = if let Some(kernel_name) = path.file_name() { - kernel_name.to_string_lossy().into_owned() - } else { - anyhow::bail!("Invalid kernelspec directory: {path:?}"); - }; - - if !fs.is_dir(path.as_path()).await { - anyhow::bail!("Not a directory: {path:?}"); - } - - let expected_kernel_json = path.join("kernel.json"); - let spec = fs.load(expected_kernel_json.as_path()).await?; - let spec = serde_json::from_str::(&spec)?; - - Ok(LocalKernelSpecification { - name: kernel_name, - path, - kernelspec: spec, - }) -} - -/// Read a directory of kernelspec directories -async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> Result> { - let mut kernelspec_dirs = fs.read_dir(&path).await?; - - let mut valid_kernelspecs = Vec::new(); - while let Some(path) = kernelspec_dirs.next().await { - match path { - Ok(path) => { - if fs.is_dir(path.as_path()).await - && let Ok(kernelspec) = read_kernelspec_at(path, fs).await - { - valid_kernelspecs.push(kernelspec); - } - } - Err(err) => log::warn!("Error reading kernelspec directory: {err:?}"), - } - } - - Ok(valid_kernelspecs) -} - -pub async fn local_kernel_specifications(fs: Arc) -> Result> { - let mut data_dirs = dirs::data_dirs(); - - // Pick up any kernels from conda or conda environment - if let Ok(conda_prefix) = env::var("CONDA_PREFIX") { - let conda_prefix = PathBuf::from(conda_prefix); - let conda_data_dir = conda_prefix.join("share").join("jupyter"); - data_dirs.push(conda_data_dir); - } - - // Search for kernels inside the base python environment - let command = util::command::new_smol_command("python") - .arg("-c") - .arg("import sys; print(sys.prefix)") - .output() - .await; - - if let Ok(command) = command - && command.status.success() - { - let python_prefix = String::from_utf8(command.stdout); - if let Ok(python_prefix) = python_prefix { - let python_prefix = PathBuf::from(python_prefix.trim()); - let python_data_dir = python_prefix.join("share").join("jupyter"); - data_dirs.push(python_data_dir); - } - } - - let kernel_dirs = data_dirs - .iter() - .map(|dir| dir.join("kernels")) - .map(|path| read_kernels_dir(path, fs.as_ref())) - .collect::>(); - - let kernel_dirs = futures::future::join_all(kernel_dirs).await; - let kernel_dirs = kernel_dirs - .into_iter() - .filter_map(Result::ok) - .flatten() - .collect::>(); - - Ok(kernel_dirs) -} - -#[cfg(test)] -mod test { - use super::*; - use std::path::PathBuf; - - use gpui::TestAppContext; - use project::FakeFs; - use serde_json::json; - - #[gpui::test] - async fn test_get_kernelspecs(cx: &mut TestAppContext) { - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/jupyter", - json!({ - ".zed": { - "settings.json": r#"{ "tab_size": 8 }"#, - "tasks.json": r#"[{ - "label": "cargo check", - "command": "cargo", - "args": ["check", "--all"] - },]"#, - }, - "kernels": { - "python": { - "kernel.json": r#"{ - "display_name": "Python 3", - "language": "python", - "argv": ["python3", "-m", "ipykernel_launcher", "-f", "{connection_file}"], - "env": {} - }"# - }, - "deno": { - "kernel.json": r#"{ - "display_name": "Deno", - "language": "typescript", - "argv": ["deno", "run", "--unstable", "--allow-net", "--allow-read", "https://deno.land/std/http/file_server.ts", "{connection_file}"], - "env": {} - }"# - } - }, - }), - ) - .await; - - let mut kernels = read_kernels_dir(PathBuf::from("/jupyter/kernels"), fs.as_ref()) - .await - .unwrap(); - - kernels.sort_by(|a, b| a.name.cmp(&b.name)); - - assert_eq!( - kernels.iter().map(|c| c.name.clone()).collect::>(), - vec!["deno", "python"] - ); - } -} diff --git a/crates/repl/src/kernels/remote_kernels.rs b/crates/repl/src/kernels/remote_kernels.rs deleted file mode 100644 index 6bc8b0d1b1..0000000000 --- a/crates/repl/src/kernels/remote_kernels.rs +++ /dev/null @@ -1,294 +0,0 @@ -use futures::{SinkExt as _, channel::mpsc}; -use gpui::{App, AppContext as _, Entity, Task, Window}; -use http_client::{AsyncBody, HttpClient, Request}; -use jupyter_protocol::{ExecutionState, JupyterKernelspec, JupyterMessage, KernelInfoReply}; - -use async_tungstenite::tokio::connect_async; -use async_tungstenite::tungstenite::{client::IntoClientRequest, http::HeaderValue}; - -use futures::StreamExt; -use smol::io::AsyncReadExt as _; - -use crate::Session; - -use super::RunningKernel; -use anyhow::Result; -use jupyter_websocket_client::{ - JupyterWebSocket, JupyterWebSocketReader, JupyterWebSocketWriter, KernelLaunchRequest, - KernelSpecsResponse, RemoteServer, -}; -use std::{fmt::Debug, sync::Arc}; - -#[derive(Debug, Clone)] -pub struct RemoteKernelSpecification { - pub name: String, - pub url: String, - pub token: String, - pub kernelspec: JupyterKernelspec, -} - -pub async fn launch_remote_kernel( - remote_server: &RemoteServer, - http_client: Arc, - kernel_name: &str, - _path: &str, -) -> Result { - // - let kernel_launch_request = KernelLaunchRequest { - name: kernel_name.to_string(), - // Note: since the path we have locally may not be the same as the one on the remote server, - // we don't send it. We'll have to evaluate this decision along the way. - path: None, - }; - - let kernel_launch_request = serde_json::to_string(&kernel_launch_request)?; - - let request = Request::builder() - .method("POST") - .uri(&remote_server.api_url("/kernels")) - .header("Authorization", format!("token {}", remote_server.token)) - .body(AsyncBody::from(kernel_launch_request))?; - - let response = http_client.send(request).await?; - - if !response.status().is_success() { - let mut body = String::new(); - response.into_body().read_to_string(&mut body).await?; - anyhow::bail!("Failed to launch kernel: {body}"); - } - - let mut body = String::new(); - response.into_body().read_to_string(&mut body).await?; - - let response: jupyter_websocket_client::Kernel = serde_json::from_str(&body)?; - - Ok(response.id) -} - -pub async fn list_remote_kernelspecs( - remote_server: RemoteServer, - http_client: Arc, -) -> Result> { - let url = remote_server.api_url("/kernelspecs"); - - let request = Request::builder() - .method("GET") - .uri(&url) - .header("Authorization", format!("token {}", remote_server.token)) - .body(AsyncBody::default())?; - - let response = http_client.send(request).await?; - - anyhow::ensure!( - response.status().is_success(), - "Failed to fetch kernel specs: {}", - response.status() - ); - let mut body = response.into_body(); - - let mut body_bytes = Vec::new(); - body.read_to_end(&mut body_bytes).await?; - - let kernel_specs: KernelSpecsResponse = serde_json::from_slice(&body_bytes)?; - - let remote_kernelspecs = kernel_specs - .kernelspecs - .into_iter() - .map(|(name, spec)| RemoteKernelSpecification { - name, - url: remote_server.base_url.clone(), - token: remote_server.token.clone(), - kernelspec: spec.spec, - }) - .collect::>(); - - anyhow::ensure!(!remote_kernelspecs.is_empty(), "No kernel specs found"); - Ok(remote_kernelspecs) -} - -impl PartialEq for RemoteKernelSpecification { - fn eq(&self, other: &Self) -> bool { - self.name == other.name && self.url == other.url - } -} - -impl Eq for RemoteKernelSpecification {} - -pub struct RemoteRunningKernel { - remote_server: RemoteServer, - _receiving_task: Task>, - _routing_task: Task>, - http_client: Arc, - pub working_directory: std::path::PathBuf, - pub request_tx: mpsc::Sender, - pub execution_state: ExecutionState, - pub kernel_info: Option, - pub kernel_id: String, -} - -impl RemoteRunningKernel { - pub fn new( - kernelspec: RemoteKernelSpecification, - working_directory: std::path::PathBuf, - session: Entity, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - let remote_server = RemoteServer { - base_url: kernelspec.url, - token: kernelspec.token, - }; - - let http_client = cx.http_client(); - - window.spawn(cx, async move |cx| { - let kernel_id = launch_remote_kernel( - &remote_server, - http_client.clone(), - &kernelspec.name, - working_directory.to_str().unwrap_or_default(), - ) - .await?; - - let ws_url = format!( - "{}/api/kernels/{}/channels?token={}", - remote_server.base_url.replace("http", "ws"), - kernel_id, - remote_server.token - ); - - let mut req: Request<()> = ws_url.into_client_request()?; - let headers = req.headers_mut(); - - headers.insert( - "User-Agent", - HeaderValue::from_str(&format!( - "Zed/{} ({}; {})", - "repl", - std::env::consts::OS, - std::env::consts::ARCH - ))?, - ); - - let response = connect_async(req).await; - - let (ws_stream, _response) = response?; - - let kernel_socket = JupyterWebSocket { inner: ws_stream }; - - let (mut w, mut r): (JupyterWebSocketWriter, JupyterWebSocketReader) = - kernel_socket.split(); - - let (request_tx, mut request_rx) = - futures::channel::mpsc::channel::(100); - - let routing_task = cx.background_spawn({ - async move { - while let Some(message) = request_rx.next().await { - w.send(message).await.ok(); - } - Ok(()) - } - }); - - let receiving_task = cx.spawn({ - let session = session.clone(); - - async move |cx| { - while let Some(message) = r.next().await { - match message { - Ok(message) => { - session - .update_in(cx, |session, window, cx| { - session.route(&message, window, cx); - }) - .ok(); - } - Err(e) => { - log::error!("Error receiving message: {:?}", e); - } - } - } - Ok(()) - } - }); - - anyhow::Ok(Box::new(Self { - _routing_task: routing_task, - _receiving_task: receiving_task, - remote_server, - working_directory, - request_tx, - // todo(kyle): pull this from the kernel API to start with - execution_state: ExecutionState::Idle, - kernel_info: None, - kernel_id, - http_client: http_client.clone(), - }) as Box) - }) - } -} - -impl Debug for RemoteRunningKernel { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("RemoteRunningKernel") - // custom debug that keeps tokens out of logs - .field("remote_server url", &self.remote_server.base_url) - .field("working_directory", &self.working_directory) - .field("request_tx", &self.request_tx) - .field("execution_state", &self.execution_state) - .field("kernel_info", &self.kernel_info) - .finish() - } -} - -impl RunningKernel for RemoteRunningKernel { - fn request_tx(&self) -> futures::channel::mpsc::Sender { - self.request_tx.clone() - } - - fn working_directory(&self) -> &std::path::PathBuf { - &self.working_directory - } - - fn execution_state(&self) -> &runtimelib::ExecutionState { - &self.execution_state - } - - fn set_execution_state(&mut self, state: runtimelib::ExecutionState) { - self.execution_state = state; - } - - fn kernel_info(&self) -> Option<&runtimelib::KernelInfoReply> { - self.kernel_info.as_ref() - } - - fn set_kernel_info(&mut self, info: runtimelib::KernelInfoReply) { - self.kernel_info = Some(info); - } - - fn force_shutdown(&mut self, window: &mut Window, cx: &mut App) -> Task> { - let url = self - .remote_server - .api_url(&format!("/kernels/{}", self.kernel_id)); - let token = self.remote_server.token.clone(); - let http_client = self.http_client.clone(); - - window.spawn(cx, async move |_| { - let request = Request::builder() - .method("DELETE") - .uri(&url) - .header("Authorization", format!("token {}", token)) - .body(AsyncBody::default())?; - - let response = http_client.send(request).await?; - - anyhow::ensure!( - response.status().is_success(), - "Failed to shutdown kernel: {}", - response.status() - ); - Ok(()) - }) - } -} diff --git a/crates/repl/src/notebook.rs b/crates/repl/src/notebook.rs deleted file mode 100644 index 9c6738f799..0000000000 --- a/crates/repl/src/notebook.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod cell; -mod notebook_ui; -pub use cell::*; -pub use notebook_ui::*; diff --git a/crates/repl/src/notebook/cell.rs b/crates/repl/src/notebook/cell.rs deleted file mode 100644 index 87b8e1d55a..0000000000 --- a/crates/repl/src/notebook/cell.rs +++ /dev/null @@ -1,746 +0,0 @@ -#![allow(unused, dead_code)] -use std::sync::Arc; - -use editor::{Editor, EditorMode, MultiBuffer}; -use futures::future::Shared; -use gpui::{ - App, Entity, Hsla, RetainAllImageCache, Task, TextStyleRefinement, image_cache, prelude::*, -}; -use language::{Buffer, Language, LanguageRegistry}; -use markdown_preview::{markdown_parser::parse_markdown, markdown_renderer::render_markdown_block}; -use nbformat::v4::{CellId, CellMetadata, CellType}; -use settings::Settings as _; -use theme::ThemeSettings; -use ui::{IconButtonShape, prelude::*}; -use util::ResultExt; - -use crate::{ - notebook::{CODE_BLOCK_INSET, GUTTER_WIDTH}, - outputs::{Output, plain::TerminalOutput, user_error::ErrorView}, -}; - -#[derive(Copy, Clone, PartialEq, PartialOrd)] -pub enum CellPosition { - First, - Middle, - Last, -} - -pub enum CellControlType { - RunCell, - RerunCell, - ClearCell, - CellOptions, - CollapseCell, - ExpandCell, -} - -impl CellControlType { - fn icon_name(&self) -> IconName { - match self { - CellControlType::RunCell => IconName::PlayFilled, - CellControlType::RerunCell => IconName::ArrowCircle, - CellControlType::ClearCell => IconName::ListX, - CellControlType::CellOptions => IconName::Ellipsis, - CellControlType::CollapseCell => IconName::ChevronDown, - CellControlType::ExpandCell => IconName::ChevronRight, - } - } -} - -pub struct CellControl { - button: IconButton, -} - -impl CellControl { - fn new(id: impl Into, control_type: CellControlType) -> Self { - let icon_name = control_type.icon_name(); - let id = id.into(); - let button = IconButton::new(id, icon_name) - .icon_size(IconSize::Small) - .shape(IconButtonShape::Square); - Self { button } - } -} - -impl Clickable for CellControl { - fn on_click( - self, - handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - let button = self.button.on_click(handler); - Self { button } - } - - fn cursor_style(self, _cursor_style: gpui::CursorStyle) -> Self { - self - } -} - -/// A notebook cell -#[derive(Clone)] -pub enum Cell { - Code(Entity), - Markdown(Entity), - Raw(Entity), -} - -fn convert_outputs( - outputs: &Vec, - window: &mut Window, - cx: &mut App, -) -> Vec { - outputs - .iter() - .map(|output| match output { - nbformat::v4::Output::Stream { text, .. } => Output::Stream { - content: cx.new(|cx| TerminalOutput::from(&text.0, window, cx)), - }, - nbformat::v4::Output::DisplayData(display_data) => { - Output::new(&display_data.data, None, window, cx) - } - nbformat::v4::Output::ExecuteResult(execute_result) => { - Output::new(&execute_result.data, None, window, cx) - } - nbformat::v4::Output::Error(error) => Output::ErrorOutput(ErrorView { - ename: error.ename.clone(), - evalue: error.evalue.clone(), - traceback: cx - .new(|cx| TerminalOutput::from(&error.traceback.join("\n"), window, cx)), - }), - }) - .collect() -} - -impl Cell { - pub fn load( - cell: &nbformat::v4::Cell, - languages: &Arc, - notebook_language: Shared>>>, - window: &mut Window, - cx: &mut App, - ) -> Self { - match cell { - nbformat::v4::Cell::Markdown { - id, - metadata, - source, - .. - } => { - let source = source.join(""); - - let entity = cx.new(|cx| { - let markdown_parsing_task = { - let languages = languages.clone(); - let source = source.clone(); - - cx.spawn_in(window, async move |this, cx| { - let parsed_markdown = cx - .background_spawn(async move { - parse_markdown(&source, None, Some(languages)).await - }) - .await; - - this.update(cx, |cell: &mut MarkdownCell, _| { - cell.parsed_markdown = Some(parsed_markdown); - }) - .log_err(); - }) - }; - - MarkdownCell { - markdown_parsing_task, - image_cache: RetainAllImageCache::new(cx), - languages: languages.clone(), - id: id.clone(), - metadata: metadata.clone(), - source: source.clone(), - parsed_markdown: None, - selected: false, - cell_position: None, - } - }); - - Cell::Markdown(entity) - } - nbformat::v4::Cell::Code { - id, - metadata, - execution_count, - source, - outputs, - } => Cell::Code(cx.new(|cx| { - let text = source.join(""); - - let buffer = cx.new(|cx| Buffer::local(text.clone(), cx)); - let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - - let editor_view = cx.new(|cx| { - let mut editor = Editor::new( - EditorMode::AutoHeight { - min_lines: 1, - max_lines: Some(1024), - }, - multi_buffer, - None, - window, - cx, - ); - - let theme = ThemeSettings::get_global(cx); - - let refinement = TextStyleRefinement { - font_family: Some(theme.buffer_font.family.clone()), - font_size: Some(theme.buffer_font_size(cx).into()), - color: Some(cx.theme().colors().editor_foreground), - background_color: Some(gpui::transparent_black()), - ..Default::default() - }; - - editor.set_text(text, window, cx); - editor.set_show_gutter(false, cx); - editor.set_text_style_refinement(refinement); - - // editor.set_read_only(true); - editor - }); - - let buffer = buffer.clone(); - let language_task = cx.spawn_in(window, async move |this, cx| { - let language = notebook_language.await; - - buffer.update(cx, |buffer, cx| { - buffer.set_language(language.clone(), cx); - }); - }); - - CodeCell { - id: id.clone(), - metadata: metadata.clone(), - execution_count: *execution_count, - source: source.join(""), - editor: editor_view, - outputs: convert_outputs(outputs, window, cx), - selected: false, - language_task, - cell_position: None, - } - })), - nbformat::v4::Cell::Raw { - id, - metadata, - source, - } => Cell::Raw(cx.new(|_| RawCell { - id: id.clone(), - metadata: metadata.clone(), - source: source.join(""), - selected: false, - cell_position: None, - })), - } - } -} - -pub trait RenderableCell: Render { - const CELL_TYPE: CellType; - - fn id(&self) -> &CellId; - fn cell_type(&self) -> CellType; - fn metadata(&self) -> &CellMetadata; - fn source(&self) -> &String; - fn selected(&self) -> bool; - fn set_selected(&mut self, selected: bool) -> &mut Self; - fn selected_bg_color(&self, window: &mut Window, cx: &mut Context) -> Hsla { - if self.selected() { - let mut color = cx.theme().colors().icon_accent; - color.fade_out(0.9); - color - } else { - // TODO: this is wrong - cx.theme().colors().tab_bar_background - } - } - fn control(&self, _window: &mut Window, _cx: &mut Context) -> Option { - None - } - - fn cell_position_spacer( - &self, - is_first: bool, - window: &mut Window, - cx: &mut Context, - ) -> Option { - let cell_position = self.cell_position(); - - if (cell_position == Some(&CellPosition::First) && is_first) - || (cell_position == Some(&CellPosition::Last) && !is_first) - { - Some(div().flex().w_full().h(DynamicSpacing::Base12.px(cx))) - } else { - None - } - } - - fn gutter(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let is_selected = self.selected(); - - div() - .relative() - .h_full() - .w(px(GUTTER_WIDTH)) - .child( - div() - .w(px(GUTTER_WIDTH)) - .flex() - .flex_none() - .justify_center() - .h_full() - .child( - div() - .flex_none() - .w(px(1.)) - .h_full() - .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent)) - .when(!is_selected, |this| this.bg(cx.theme().colors().border)), - ), - ) - .when_some(self.control(window, cx), |this, control| { - this.child( - div() - .absolute() - .top(px(CODE_BLOCK_INSET - 2.0)) - .left_0() - .flex() - .flex_none() - .w(px(GUTTER_WIDTH)) - .h(px(GUTTER_WIDTH + 12.0)) - .items_center() - .justify_center() - .bg(cx.theme().colors().tab_bar_background) - .child(control.button), - ) - }) - } - - fn cell_position(&self) -> Option<&CellPosition>; - fn set_cell_position(&mut self, position: CellPosition) -> &mut Self; -} - -pub trait RunnableCell: RenderableCell { - fn execution_count(&self) -> Option; - fn set_execution_count(&mut self, count: i32) -> &mut Self; - fn run(&mut self, window: &mut Window, cx: &mut Context) -> (); -} - -pub struct MarkdownCell { - id: CellId, - metadata: CellMetadata, - image_cache: Entity, - source: String, - parsed_markdown: Option, - markdown_parsing_task: Task<()>, - selected: bool, - cell_position: Option, - languages: Arc, -} - -impl RenderableCell for MarkdownCell { - const CELL_TYPE: CellType = CellType::Markdown; - - fn id(&self) -> &CellId { - &self.id - } - - fn cell_type(&self) -> CellType { - CellType::Markdown - } - - fn metadata(&self) -> &CellMetadata { - &self.metadata - } - - fn source(&self) -> &String { - &self.source - } - - fn selected(&self) -> bool { - self.selected - } - - fn set_selected(&mut self, selected: bool) -> &mut Self { - self.selected = selected; - self - } - - fn control(&self, _window: &mut Window, _: &mut Context) -> Option { - None - } - - fn cell_position(&self) -> Option<&CellPosition> { - self.cell_position.as_ref() - } - - fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self { - self.cell_position = Some(cell_position); - self - } -} - -impl Render for MarkdownCell { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let Some(parsed) = self.parsed_markdown.as_ref() else { - return div(); - }; - - let mut markdown_render_context = - markdown_preview::markdown_renderer::RenderContext::new(None, window, cx); - - v_flex() - .size_full() - // TODO: Move base cell render into trait impl so we don't have to repeat this - .children(self.cell_position_spacer(true, window, cx)) - .child( - h_flex() - .w_full() - .pr_6() - .rounded_xs() - .items_start() - .gap(DynamicSpacing::Base08.rems(cx)) - .bg(self.selected_bg_color(window, cx)) - .child(self.gutter(window, cx)) - .child( - v_flex() - .image_cache(self.image_cache.clone()) - .size_full() - .flex_1() - .p_3() - .font_ui(cx) - .text_size(TextSize::Default.rems(cx)) - .children(parsed.children.iter().map(|child| { - div().relative().child(div().relative().child( - render_markdown_block(child, &mut markdown_render_context), - )) - })), - ), - ) - // TODO: Move base cell render into trait impl so we don't have to repeat this - .children(self.cell_position_spacer(false, window, cx)) - } -} - -pub struct CodeCell { - id: CellId, - metadata: CellMetadata, - execution_count: Option, - source: String, - editor: Entity, - outputs: Vec, - selected: bool, - cell_position: Option, - language_task: Task<()>, -} - -impl CodeCell { - pub fn is_dirty(&self, cx: &App) -> bool { - self.editor.read(cx).buffer().read(cx).is_dirty(cx) - } - pub fn has_outputs(&self) -> bool { - !self.outputs.is_empty() - } - - pub fn clear_outputs(&mut self) { - self.outputs.clear(); - } - - fn output_control(&self) -> Option { - if self.has_outputs() { - Some(CellControlType::ClearCell) - } else { - None - } - } - - pub fn gutter_output(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let is_selected = self.selected(); - - div() - .relative() - .h_full() - .w(px(GUTTER_WIDTH)) - .child( - div() - .w(px(GUTTER_WIDTH)) - .flex() - .flex_none() - .justify_center() - .h_full() - .child( - div() - .flex_none() - .w(px(1.)) - .h_full() - .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent)) - .when(!is_selected, |this| this.bg(cx.theme().colors().border)), - ), - ) - .when(self.has_outputs(), |this| { - this.child( - div() - .absolute() - .top(px(CODE_BLOCK_INSET - 2.0)) - .left_0() - .flex() - .flex_none() - .w(px(GUTTER_WIDTH)) - .h(px(GUTTER_WIDTH + 12.0)) - .items_center() - .justify_center() - .bg(cx.theme().colors().tab_bar_background) - .child(IconButton::new("control", IconName::Ellipsis)), - ) - }) - } -} - -impl RenderableCell for CodeCell { - const CELL_TYPE: CellType = CellType::Code; - - fn id(&self) -> &CellId { - &self.id - } - - fn cell_type(&self) -> CellType { - CellType::Code - } - - fn metadata(&self) -> &CellMetadata { - &self.metadata - } - - fn source(&self) -> &String { - &self.source - } - - fn control(&self, window: &mut Window, cx: &mut Context) -> Option { - let cell_control = if self.has_outputs() { - CellControl::new("rerun-cell", CellControlType::RerunCell) - } else { - CellControl::new("run-cell", CellControlType::RunCell) - .on_click(cx.listener(move |this, _, window, cx| this.run(window, cx))) - }; - - Some(cell_control) - } - - fn selected(&self) -> bool { - self.selected - } - - fn set_selected(&mut self, selected: bool) -> &mut Self { - self.selected = selected; - self - } - - fn cell_position(&self) -> Option<&CellPosition> { - self.cell_position.as_ref() - } - - fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self { - self.cell_position = Some(cell_position); - self - } -} - -impl RunnableCell for CodeCell { - fn run(&mut self, window: &mut Window, cx: &mut Context) { - println!("Running code cell: {}", self.id); - } - - fn execution_count(&self) -> Option { - self.execution_count - .and_then(|count| if count > 0 { Some(count) } else { None }) - } - - fn set_execution_count(&mut self, count: i32) -> &mut Self { - self.execution_count = Some(count); - self - } -} - -impl Render for CodeCell { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .size_full() - // TODO: Move base cell render into trait impl so we don't have to repeat this - .children(self.cell_position_spacer(true, window, cx)) - // Editor portion - .child( - h_flex() - .w_full() - .pr_6() - .rounded_xs() - .items_start() - .gap(DynamicSpacing::Base08.rems(cx)) - .bg(self.selected_bg_color(window, cx)) - .child(self.gutter(window, cx)) - .child( - div().py_1p5().w_full().child( - div() - .flex() - .size_full() - .flex_1() - .py_3() - .px_5() - .rounded_lg() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().editor_background) - .child(div().w_full().child(self.editor.clone())), - ), - ), - ) - // Output portion - .child( - h_flex() - .w_full() - .pr_6() - .rounded_xs() - .items_start() - .gap(DynamicSpacing::Base08.rems(cx)) - .bg(self.selected_bg_color(window, cx)) - .child(self.gutter_output(window, cx)) - .child( - div().py_1p5().w_full().child( - div() - .flex() - .size_full() - .flex_1() - .py_3() - .px_5() - .rounded_lg() - .border_1() - // .border_color(cx.theme().colors().border) - // .bg(cx.theme().colors().editor_background) - .child(div().w_full().children(self.outputs.iter().map( - |output| { - let content = match output { - Output::Plain { content, .. } => { - Some(content.clone().into_any_element()) - } - Output::Markdown { content, .. } => { - Some(content.clone().into_any_element()) - } - Output::Stream { content, .. } => { - Some(content.clone().into_any_element()) - } - Output::Image { content, .. } => { - Some(content.clone().into_any_element()) - } - Output::Message(message) => Some( - div().child(message.clone()).into_any_element(), - ), - Output::Table { content, .. } => { - Some(content.clone().into_any_element()) - } - Output::ErrorOutput(error_view) => { - error_view.render(window, cx) - } - Output::ClearOutputWaitMarker => None, - }; - - div() - // .w_full() - // .mt_3() - // .p_3() - // .rounded_sm() - // .bg(cx.theme().colors().editor_background) - // .border(px(1.)) - // .border_color(cx.theme().colors().border) - // .shadow_xs() - .children(content) - }, - ))), - ), - ), - ) - // TODO: Move base cell render into trait impl so we don't have to repeat this - .children(self.cell_position_spacer(false, window, cx)) - } -} - -pub struct RawCell { - id: CellId, - metadata: CellMetadata, - source: String, - selected: bool, - cell_position: Option, -} - -impl RenderableCell for RawCell { - const CELL_TYPE: CellType = CellType::Raw; - - fn id(&self) -> &CellId { - &self.id - } - - fn cell_type(&self) -> CellType { - CellType::Raw - } - - fn metadata(&self) -> &CellMetadata { - &self.metadata - } - - fn source(&self) -> &String { - &self.source - } - - fn selected(&self) -> bool { - self.selected - } - - fn set_selected(&mut self, selected: bool) -> &mut Self { - self.selected = selected; - self - } - - fn cell_position(&self) -> Option<&CellPosition> { - self.cell_position.as_ref() - } - - fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self { - self.cell_position = Some(cell_position); - self - } -} - -impl Render for RawCell { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .size_full() - // TODO: Move base cell render into trait impl so we don't have to repeat this - .children(self.cell_position_spacer(true, window, cx)) - .child( - h_flex() - .w_full() - .pr_2() - .rounded_xs() - .items_start() - .gap(DynamicSpacing::Base08.rems(cx)) - .bg(self.selected_bg_color(window, cx)) - .child(self.gutter(window, cx)) - .child( - div() - .flex() - .size_full() - .flex_1() - .p_3() - .font_ui(cx) - .text_size(TextSize::Default.rems(cx)) - .child(self.source.clone()), - ), - ) - // TODO: Move base cell render into trait impl so we don't have to repeat this - .children(self.cell_position_spacer(false, window, cx)) - } -} diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs deleted file mode 100644 index 07c6e9c8aa..0000000000 --- a/crates/repl/src/notebook/notebook_ui.rs +++ /dev/null @@ -1,836 +0,0 @@ -#![allow(unused, dead_code)] -use std::future::Future; -use std::{path::PathBuf, sync::Arc}; - -use anyhow::{Context as _, Result}; -use client::proto::ViewId; -use collections::HashMap; -use feature_flags::{FeatureFlagAppExt as _, NotebookFeatureFlag}; -use futures::FutureExt; -use futures::future::Shared; -use gpui::{ - AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, ListScrollEvent, ListState, - Point, Task, actions, list, prelude::*, -}; -use language::{Language, LanguageRegistry}; -use project::{Project, ProjectEntryId, ProjectPath}; -use ui::{Tooltip, prelude::*}; -use workspace::item::{ItemEvent, SaveOptions, TabContentParams}; -use workspace::searchable::SearchableItemHandle; -use workspace::{Item, ItemHandle, Pane, ProjectItem, ToolbarItemLocation}; -use workspace::{ToolbarItemEvent, ToolbarItemView}; - -use super::{Cell, CellPosition, RenderableCell}; - -use nbformat::v4::CellId; -use nbformat::v4::Metadata as NotebookMetadata; - -actions!( - notebook, - [ - /// Opens a Jupyter notebook file. - OpenNotebook, - /// Runs all cells in the notebook. - RunAll, - /// Clears all cell outputs. - ClearOutputs, - /// Moves the current cell up. - MoveCellUp, - /// Moves the current cell down. - MoveCellDown, - /// Adds a new markdown cell. - AddMarkdownBlock, - /// Adds a new code cell. - AddCodeBlock, - ] -); - -pub(crate) const MAX_TEXT_BLOCK_WIDTH: f32 = 9999.0; -pub(crate) const SMALL_SPACING_SIZE: f32 = 8.0; -pub(crate) const MEDIUM_SPACING_SIZE: f32 = 12.0; -pub(crate) const LARGE_SPACING_SIZE: f32 = 16.0; -pub(crate) const GUTTER_WIDTH: f32 = 19.0; -pub(crate) const CODE_BLOCK_INSET: f32 = MEDIUM_SPACING_SIZE; -pub(crate) const CONTROL_SIZE: f32 = 20.0; - -pub fn init(cx: &mut App) { - if cx.has_flag::() || std::env::var("LOCAL_NOTEBOOK_DEV").is_ok() { - workspace::register_project_item::(cx); - } - - cx.observe_flag::({ - move |is_enabled, cx| { - if is_enabled { - workspace::register_project_item::(cx); - } else { - // todo: there is no way to unregister a project item, so if the feature flag - // gets turned off they need to restart Zed. - } - } - }) - .detach(); -} - -pub struct NotebookEditor { - languages: Arc, - project: Entity, - - focus_handle: FocusHandle, - notebook_item: Entity, - - remote_id: Option, - cell_list: ListState, - - selected_cell_index: usize, - cell_order: Vec, - cell_map: HashMap, -} - -impl NotebookEditor { - pub fn new( - project: Entity, - notebook_item: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let focus_handle = cx.focus_handle(); - - let languages = project.read(cx).languages().clone(); - let language_name = notebook_item.read(cx).language_name(); - - let notebook_language = notebook_item.read(cx).notebook_language(); - let notebook_language = cx - .spawn_in(window, async move |_, _| notebook_language.await) - .shared(); - - let mut cell_order = vec![]; // Vec - let mut cell_map = HashMap::default(); // HashMap - - for (index, cell) in notebook_item - .read(cx) - .notebook - .clone() - .cells - .iter() - .enumerate() - { - let cell_id = cell.id(); - cell_order.push(cell_id.clone()); - cell_map.insert( - cell_id.clone(), - Cell::load(cell, &languages, notebook_language.clone(), window, cx), - ); - } - - let notebook_handle = cx.entity().downgrade(); - let cell_count = cell_order.len(); - - let this = cx.entity(); - let cell_list = ListState::new(cell_count, gpui::ListAlignment::Top, px(1000.)); - - Self { - project, - languages: languages.clone(), - focus_handle, - notebook_item, - remote_id: None, - cell_list, - selected_cell_index: 0, - cell_order: cell_order.clone(), - cell_map: cell_map.clone(), - } - } - - fn has_outputs(&self, window: &mut Window, cx: &mut Context) -> bool { - self.cell_map.values().any(|cell| { - if let Cell::Code(code_cell) = cell { - code_cell.read(cx).has_outputs() - } else { - false - } - }) - } - - fn clear_outputs(&mut self, window: &mut Window, cx: &mut Context) { - for cell in self.cell_map.values() { - if let Cell::Code(code_cell) = cell { - code_cell.update(cx, |cell, _cx| { - cell.clear_outputs(); - }); - } - } - } - - fn run_cells(&mut self, window: &mut Window, cx: &mut Context) { - println!("Cells would all run here, if that was implemented!"); - } - - fn open_notebook(&mut self, _: &OpenNotebook, _window: &mut Window, _cx: &mut Context) { - println!("Open notebook triggered"); - } - - fn move_cell_up(&mut self, window: &mut Window, cx: &mut Context) { - println!("Move cell up triggered"); - } - - fn move_cell_down(&mut self, window: &mut Window, cx: &mut Context) { - println!("Move cell down triggered"); - } - - fn add_markdown_block(&mut self, window: &mut Window, cx: &mut Context) { - println!("Add markdown block triggered"); - } - - fn add_code_block(&mut self, window: &mut Window, cx: &mut Context) { - println!("Add code block triggered"); - } - - fn cell_count(&self) -> usize { - self.cell_map.len() - } - - fn selected_index(&self) -> usize { - self.selected_cell_index - } - - pub fn set_selected_index( - &mut self, - index: usize, - jump_to_index: bool, - window: &mut Window, - cx: &mut Context, - ) { - // let previous_index = self.selected_cell_index; - self.selected_cell_index = index; - let current_index = self.selected_cell_index; - - // in the future we may have some `on_cell_change` event that we want to fire here - - if jump_to_index { - self.jump_to_cell(current_index, window, cx); - } - } - - pub fn select_next( - &mut self, - _: &menu::SelectNext, - window: &mut Window, - cx: &mut Context, - ) { - let count = self.cell_count(); - if count > 0 { - let index = self.selected_index(); - let ix = if index == count - 1 { - count - 1 - } else { - index + 1 - }; - self.set_selected_index(ix, true, window, cx); - cx.notify(); - } - } - - pub fn select_previous( - &mut self, - _: &menu::SelectPrevious, - window: &mut Window, - cx: &mut Context, - ) { - let count = self.cell_count(); - if count > 0 { - let index = self.selected_index(); - let ix = if index == 0 { 0 } else { index - 1 }; - self.set_selected_index(ix, true, window, cx); - cx.notify(); - } - } - - pub fn select_first( - &mut self, - _: &menu::SelectFirst, - window: &mut Window, - cx: &mut Context, - ) { - let count = self.cell_count(); - if count > 0 { - self.set_selected_index(0, true, window, cx); - cx.notify(); - } - } - - pub fn select_last( - &mut self, - _: &menu::SelectLast, - window: &mut Window, - cx: &mut Context, - ) { - let count = self.cell_count(); - if count > 0 { - self.set_selected_index(count - 1, true, window, cx); - cx.notify(); - } - } - - fn jump_to_cell(&mut self, index: usize, _window: &mut Window, _cx: &mut Context) { - self.cell_list.scroll_to_reveal_item(index); - } - - fn button_group(window: &mut Window, cx: &mut Context) -> Div { - v_flex() - .gap(DynamicSpacing::Base04.rems(cx)) - .items_center() - .w(px(CONTROL_SIZE + 4.0)) - .overflow_hidden() - .rounded(px(5.)) - .bg(cx.theme().colors().title_bar_background) - .p_px() - .border_1() - .border_color(cx.theme().colors().border) - } - - fn render_notebook_control( - id: impl Into, - icon: IconName, - _window: &mut Window, - _cx: &mut Context, - ) -> IconButton { - let id: ElementId = ElementId::Name(id.into()); - IconButton::new(id, icon).width(px(CONTROL_SIZE)) - } - - fn render_notebook_controls( - &self, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let has_outputs = self.has_outputs(window, cx); - - v_flex() - .max_w(px(CONTROL_SIZE + 4.0)) - .items_center() - .gap(DynamicSpacing::Base16.rems(cx)) - .justify_between() - .flex_none() - .h_full() - .py(DynamicSpacing::Base12.px(cx)) - .child( - v_flex() - .gap(DynamicSpacing::Base08.rems(cx)) - .child( - Self::button_group(window, cx) - .child( - Self::render_notebook_control( - "run-all-cells", - IconName::PlayFilled, - window, - cx, - ) - .tooltip(move |window, cx| { - Tooltip::for_action("Execute all cells", &RunAll, cx) - }) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(RunAll), cx); - }), - ) - .child( - Self::render_notebook_control( - "clear-all-outputs", - IconName::ListX, - window, - cx, - ) - .disabled(!has_outputs) - .tooltip(move |window, cx| { - Tooltip::for_action("Clear all outputs", &ClearOutputs, cx) - }) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(ClearOutputs), cx); - }), - ), - ) - .child( - Self::button_group(window, cx) - .child( - Self::render_notebook_control( - "move-cell-up", - IconName::ArrowUp, - window, - cx, - ) - .tooltip(move |window, cx| { - Tooltip::for_action("Move cell up", &MoveCellUp, cx) - }) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(MoveCellUp), cx); - }), - ) - .child( - Self::render_notebook_control( - "move-cell-down", - IconName::ArrowDown, - window, - cx, - ) - .tooltip(move |window, cx| { - Tooltip::for_action("Move cell down", &MoveCellDown, cx) - }) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(MoveCellDown), cx); - }), - ), - ) - .child( - Self::button_group(window, cx) - .child( - Self::render_notebook_control( - "new-markdown-cell", - IconName::Plus, - window, - cx, - ) - .tooltip(move |window, cx| { - Tooltip::for_action("Add markdown block", &AddMarkdownBlock, cx) - }) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(AddMarkdownBlock), cx); - }), - ) - .child( - Self::render_notebook_control( - "new-code-cell", - IconName::Code, - window, - cx, - ) - .tooltip(move |window, cx| { - Tooltip::for_action("Add code block", &AddCodeBlock, cx) - }) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(AddCodeBlock), cx); - }), - ), - ), - ) - .child( - v_flex() - .gap(DynamicSpacing::Base08.rems(cx)) - .items_center() - .child(Self::render_notebook_control( - "more-menu", - IconName::Ellipsis, - window, - cx, - )) - .child( - Self::button_group(window, cx) - .child(IconButton::new("repl", IconName::ReplNeutral)), - ), - ) - } - - fn cell_position(&self, index: usize) -> CellPosition { - match index { - 0 => CellPosition::First, - index if index == self.cell_count() - 1 => CellPosition::Last, - _ => CellPosition::Middle, - } - } - - fn render_cell( - &self, - index: usize, - cell: &Cell, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let cell_position = self.cell_position(index); - - let is_selected = index == self.selected_cell_index; - - match cell { - Cell::Code(cell) => { - cell.update(cx, |cell, _cx| { - cell.set_selected(is_selected) - .set_cell_position(cell_position); - }); - cell.clone().into_any_element() - } - Cell::Markdown(cell) => { - cell.update(cx, |cell, _cx| { - cell.set_selected(is_selected) - .set_cell_position(cell_position); - }); - cell.clone().into_any_element() - } - Cell::Raw(cell) => { - cell.update(cx, |cell, _cx| { - cell.set_selected(is_selected) - .set_cell_position(cell_position); - }); - cell.clone().into_any_element() - } - } - } -} - -impl Render for NotebookEditor { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .key_context("notebook") - .track_focus(&self.focus_handle) - .on_action(cx.listener(|this, &OpenNotebook, window, cx| { - this.open_notebook(&OpenNotebook, window, cx) - })) - .on_action( - cx.listener(|this, &ClearOutputs, window, cx| this.clear_outputs(window, cx)), - ) - .on_action(cx.listener(|this, &RunAll, window, cx| this.run_cells(window, cx))) - .on_action(cx.listener(|this, &MoveCellUp, window, cx| this.move_cell_up(window, cx))) - .on_action( - cx.listener(|this, &MoveCellDown, window, cx| this.move_cell_down(window, cx)), - ) - .on_action(cx.listener(|this, &AddMarkdownBlock, window, cx| { - this.add_markdown_block(window, cx) - })) - .on_action( - cx.listener(|this, &AddCodeBlock, window, cx| this.add_code_block(window, cx)), - ) - .on_action(cx.listener(Self::select_next)) - .on_action(cx.listener(Self::select_previous)) - .on_action(cx.listener(Self::select_first)) - .on_action(cx.listener(Self::select_last)) - .flex() - .items_start() - .size_full() - .overflow_hidden() - .px(DynamicSpacing::Base12.px(cx)) - .gap(DynamicSpacing::Base12.px(cx)) - .bg(cx.theme().colors().tab_bar_background) - .child( - v_flex() - .id("notebook-cells") - .flex_1() - .size_full() - .overflow_y_scroll() - .child(list( - self.cell_list.clone(), - cx.processor(|this, ix, window, cx| { - this.cell_order - .get(ix) - .and_then(|cell_id| this.cell_map.get(cell_id)) - .map(|cell| { - this.render_cell(ix, cell, window, cx).into_any_element() - }) - .unwrap_or_else(|| div().into_any()) - }), - )) - .size_full(), - ) - .child(self.render_notebook_controls(window, cx)) - } -} - -impl Focusable for NotebookEditor { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -// Intended to be a NotebookBuffer -pub struct NotebookItem { - path: PathBuf, - project_path: ProjectPath, - languages: Arc, - // Raw notebook data - notebook: nbformat::v4::Notebook, - // Store our version of the notebook in memory (cell_order, cell_map) - id: ProjectEntryId, -} - -impl project::ProjectItem for NotebookItem { - fn try_open( - project: &Entity, - path: &ProjectPath, - cx: &mut App, - ) -> Option>>> { - let path = path.clone(); - let project = project.clone(); - let fs = project.read(cx).fs().clone(); - let languages = project.read(cx).languages().clone(); - - if path.path.extension().unwrap_or_default() == "ipynb" { - Some(cx.spawn(async move |cx| { - let abs_path = project - .read_with(cx, |project, cx| project.absolute_path(&path, cx))? - .with_context(|| format!("finding the absolute path of {path:?}"))?; - - // todo: watch for changes to the file - let file_content = fs.load(abs_path.as_path()).await?; - let notebook = nbformat::parse_notebook(&file_content); - - let notebook = match notebook { - Ok(nbformat::Notebook::V4(notebook)) => notebook, - // 4.1 - 4.4 are converted to 4.5 - Ok(nbformat::Notebook::Legacy(legacy_notebook)) => { - // TODO: Decide if we want to mutate the notebook by including Cell IDs - // and any other conversions - - nbformat::upgrade_legacy_notebook(legacy_notebook)? - } - // Bad notebooks and notebooks v4.0 and below are not supported - Err(e) => { - anyhow::bail!("Failed to parse notebook: {:?}", e); - } - }; - - let id = project - .update(cx, |project, cx| { - project.entry_for_path(&path, cx).map(|entry| entry.id) - })? - .context("Entry not found")?; - - cx.new(|_| NotebookItem { - path: abs_path, - project_path: path, - languages, - notebook, - id, - }) - })) - } else { - None - } - } - - fn entry_id(&self, _: &App) -> Option { - Some(self.id) - } - - fn project_path(&self, _: &App) -> Option { - Some(self.project_path.clone()) - } - - fn is_dirty(&self) -> bool { - false - } -} - -impl NotebookItem { - pub fn language_name(&self) -> Option { - self.notebook - .metadata - .language_info - .as_ref() - .map(|l| l.name.clone()) - .or(self - .notebook - .metadata - .kernelspec - .as_ref() - .and_then(|spec| spec.language.clone())) - } - - pub fn notebook_language(&self) -> impl Future>> + use<> { - let language_name = self.language_name(); - let languages = self.languages.clone(); - - async move { - if let Some(language_name) = language_name { - languages.language_for_name(&language_name).await.ok() - } else { - None - } - } - } -} - -impl EventEmitter<()> for NotebookEditor {} - -// pub struct NotebookControls { -// pane_focused: bool, -// active_item: Option>, -// // subscription: Option, -// } - -// impl NotebookControls { -// pub fn new() -> Self { -// Self { -// pane_focused: false, -// active_item: Default::default(), -// // subscription: Default::default(), -// } -// } -// } - -// impl EventEmitter for NotebookControls {} - -// impl Render for NotebookControls { -// fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { -// div().child("notebook controls") -// } -// } - -// impl ToolbarItemView for NotebookControls { -// fn set_active_pane_item( -// &mut self, -// active_pane_item: Option<&dyn workspace::ItemHandle>, -// window: &mut Window, cx: &mut Context, -// ) -> workspace::ToolbarItemLocation { -// cx.notify(); -// self.active_item = None; - -// let Some(item) = active_pane_item else { -// return ToolbarItemLocation::Hidden; -// }; - -// ToolbarItemLocation::PrimaryLeft -// } - -// fn pane_focus_update(&mut self, pane_focused: bool, _window: &mut Window, _cx: &mut Context) { -// self.pane_focused = pane_focused; -// } -// } - -impl Item for NotebookEditor { - type Event = (); - - fn can_split(&self) -> bool { - true - } - - fn clone_on_split( - &self, - _workspace_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> Task>> - where - Self: Sized, - { - Task::ready(Some(cx.new(|cx| { - Self::new(self.project.clone(), self.notebook_item.clone(), window, cx) - }))) - } - - fn buffer_kind(&self, _: &App) -> workspace::item::ItemBufferKind { - workspace::item::ItemBufferKind::Singleton - } - - fn for_each_project_item( - &self, - cx: &App, - f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem), - ) { - f(self.notebook_item.entity_id(), self.notebook_item.read(cx)) - } - - fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement { - Label::new(self.tab_content_text(params.detail.unwrap_or(0), cx)) - .single_line() - .color(params.text_color()) - .when(params.preview, |this| this.italic()) - .into_any_element() - } - - fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString { - let path = &self.notebook_item.read(cx).path; - let title = path - .file_name() - .unwrap_or_else(|| path.as_os_str()) - .to_string_lossy() - .to_string(); - title.into() - } - - fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { - Some(IconName::Book.into()) - } - - fn show_toolbar(&self) -> bool { - false - } - - // TODO - fn pixel_position_of_cursor(&self, _: &App) -> Option> { - None - } - - // TODO - fn as_searchable(&self, _: &Entity, _: &App) -> Option> { - None - } - - fn set_nav_history( - &mut self, - _: workspace::ItemNavHistory, - _window: &mut Window, - _: &mut Context, - ) { - // TODO - } - - // TODO - fn can_save(&self, _cx: &App) -> bool { - false - } - // TODO - fn save( - &mut self, - _options: SaveOptions, - _project: Entity, - _window: &mut Window, - _cx: &mut Context, - ) -> Task> { - unimplemented!("save() must be implemented if can_save() returns true") - } - - // TODO - fn save_as( - &mut self, - _project: Entity, - _path: ProjectPath, - _window: &mut Window, - _cx: &mut Context, - ) -> Task> { - unimplemented!("save_as() must be implemented if can_save() returns true") - } - // TODO - fn reload( - &mut self, - _project: Entity, - _window: &mut Window, - _cx: &mut Context, - ) -> Task> { - unimplemented!("reload() must be implemented if can_save() returns true") - } - - fn is_dirty(&self, cx: &App) -> bool { - self.cell_map.values().any(|cell| { - if let Cell::Code(code_cell) = cell { - code_cell.read(cx).is_dirty(cx) - } else { - false - } - }) - } -} - -// TODO: Implement this to allow us to persist to the database, etc: -// impl SerializableItem for NotebookEditor {} - -impl ProjectItem for NotebookEditor { - type Item = NotebookItem; - - fn for_project_item( - project: Entity, - _: Option<&Pane>, - item: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Self - where - Self: Sized, - { - Self::new(project, item, window, cx) - } -} diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs deleted file mode 100644 index b99562393a..0000000000 --- a/crates/repl/src/outputs.rs +++ /dev/null @@ -1,617 +0,0 @@ -//! # REPL Output Module -//! -//! This module provides the core functionality for handling and displaying -//! various types of output from Jupyter kernels. -//! -//! ## Key Components -//! -//! - `OutputContent`: An enum that encapsulates different types of output content. -//! - `ExecutionView`: Manages the display of outputs for a single execution. -//! - `ExecutionStatus`: Represents the current status of an execution. -//! -//! ## Output Types -//! -//! The module supports several output types, including: -//! - Plain text -//! - Markdown -//! - Images (PNG and JPEG) -//! - Tables -//! - Error messages -//! -//! ## Clipboard Support -//! -//! Most output types implement the `SupportsClipboard` trait, allowing -//! users to easily copy output content to the system clipboard. -//! -//! ## Rendering -//! -//! The module provides rendering capabilities for each output type, -//! ensuring proper display within the REPL interface. -//! -//! ## Jupyter Integration -//! -//! This module is designed to work with Jupyter message protocols, -//! interpreting and displaying various types of Jupyter output. - -use editor::{Editor, MultiBuffer}; -use gpui::{AnyElement, ClipboardItem, Entity, Render, WeakEntity}; -use language::Buffer; -use runtimelib::{ExecutionState, JupyterMessageContent, MimeBundle, MimeType}; -use ui::{ - ButtonStyle, CommonAnimationExt, Context, IconButton, IconName, IntoElement, Styled, Tooltip, - Window, div, h_flex, prelude::*, v_flex, -}; - -mod image; -use image::ImageView; - -mod markdown; -use markdown::MarkdownView; - -mod table; -use table::TableView; - -pub mod plain; -use plain::TerminalOutput; - -pub(crate) mod user_error; -use user_error::ErrorView; -use workspace::Workspace; - -/// When deciding what to render from a collection of mediatypes, we need to rank them in order of importance -fn rank_mime_type(mimetype: &MimeType) -> usize { - match mimetype { - MimeType::DataTable(_) => 6, - MimeType::Png(_) => 4, - MimeType::Jpeg(_) => 3, - MimeType::Markdown(_) => 2, - MimeType::Plain(_) => 1, - // All other media types are not supported in Zed at this time - _ => 0, - } -} - -pub(crate) trait OutputContent { - fn clipboard_content(&self, window: &Window, cx: &App) -> Option; - fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool { - false - } - fn has_buffer_content(&self, _window: &Window, _cx: &App) -> bool { - false - } - fn buffer_content(&mut self, _window: &mut Window, _cx: &mut App) -> Option> { - None - } -} - -impl OutputContent for Entity { - fn clipboard_content(&self, window: &Window, cx: &App) -> Option { - self.read(cx).clipboard_content(window, cx) - } - - fn has_clipboard_content(&self, window: &Window, cx: &App) -> bool { - self.read(cx).has_clipboard_content(window, cx) - } - - fn has_buffer_content(&self, window: &Window, cx: &App) -> bool { - self.read(cx).has_buffer_content(window, cx) - } - - fn buffer_content(&mut self, window: &mut Window, cx: &mut App) -> Option> { - self.update(cx, |item, cx| item.buffer_content(window, cx)) - } -} - -pub enum Output { - Plain { - content: Entity, - display_id: Option, - }, - Stream { - content: Entity, - }, - Image { - content: Entity, - display_id: Option, - }, - ErrorOutput(ErrorView), - Message(String), - Table { - content: Entity, - display_id: Option, - }, - Markdown { - content: Entity, - display_id: Option, - }, - ClearOutputWaitMarker, -} - -impl Output { - fn render_output_controls( - v: Entity, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Option { - if !v.has_clipboard_content(window, cx) && !v.has_buffer_content(window, cx) { - return None; - } - - Some( - h_flex() - .pl_1() - .when(v.has_clipboard_content(window, cx), |el| { - let v = v.clone(); - el.child( - IconButton::new(ElementId::Name("copy-output".into()), IconName::Copy) - .style(ButtonStyle::Transparent) - .tooltip(Tooltip::text("Copy Output")) - .on_click(move |_, window, cx| { - let clipboard_content = v.clipboard_content(window, cx); - - if let Some(clipboard_content) = clipboard_content.as_ref() { - cx.write_to_clipboard(clipboard_content.clone()); - } - }), - ) - }) - .when(v.has_buffer_content(window, cx), |el| { - let v = v.clone(); - el.child( - IconButton::new( - ElementId::Name("open-in-buffer".into()), - IconName::FileTextOutlined, - ) - .style(ButtonStyle::Transparent) - .tooltip(Tooltip::text("Open in Buffer")) - .on_click({ - let workspace = workspace.clone(); - move |_, window, cx| { - let buffer_content = - v.update(cx, |item, cx| item.buffer_content(window, cx)); - - if let Some(buffer_content) = buffer_content.as_ref() { - let buffer = buffer_content.clone(); - let editor = Box::new(cx.new(|cx| { - let multibuffer = cx.new(|cx| { - let mut multi_buffer = - MultiBuffer::singleton(buffer.clone(), cx); - - multi_buffer.set_title("REPL Output".to_string(), cx); - multi_buffer - }); - - Editor::for_multibuffer(multibuffer, None, window, cx) - })); - workspace - .update(cx, |workspace, cx| { - workspace.add_item_to_active_pane( - editor, None, true, window, cx, - ); - }) - .ok(); - } - } - }), - ) - }) - .into_any_element(), - ) - } - - pub fn render( - &self, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement + use<> { - let content = match self { - Self::Plain { content, .. } => Some(content.clone().into_any_element()), - Self::Markdown { content, .. } => Some(content.clone().into_any_element()), - Self::Stream { content, .. } => Some(content.clone().into_any_element()), - Self::Image { content, .. } => Some(content.clone().into_any_element()), - Self::Message(message) => Some(div().child(message.clone()).into_any_element()), - Self::Table { content, .. } => Some(content.clone().into_any_element()), - Self::ErrorOutput(error_view) => error_view.render(window, cx), - Self::ClearOutputWaitMarker => None, - }; - - h_flex() - .id("output-content") - .w_full() - .overflow_x_scroll() - .items_start() - .child(div().flex_1().children(content)) - .children(match self { - Self::Plain { content, .. } => { - Self::render_output_controls(content.clone(), workspace, window, cx) - } - Self::Markdown { content, .. } => { - Self::render_output_controls(content.clone(), workspace, window, cx) - } - Self::Stream { content, .. } => { - Self::render_output_controls(content.clone(), workspace, window, cx) - } - Self::Image { content, .. } => { - Self::render_output_controls(content.clone(), workspace, window, cx) - } - Self::ErrorOutput(err) => { - // Add buttons for the traceback section - Some( - h_flex() - .pl_1() - .child( - IconButton::new( - ElementId::Name("copy-full-error-traceback".into()), - IconName::Copy, - ) - .style(ButtonStyle::Transparent) - .tooltip(Tooltip::text("Copy Full Error")) - .on_click({ - let ename = err.ename.clone(); - let evalue = err.evalue.clone(); - let traceback = err.traceback.clone(); - move |_, _window, cx| { - let traceback_text = traceback.read(cx).full_text(); - let full_error = - format!("{}: {}\n{}", ename, evalue, traceback_text); - let clipboard_content = - ClipboardItem::new_string(full_error); - cx.write_to_clipboard(clipboard_content); - } - }), - ) - .child( - IconButton::new( - ElementId::Name("open-full-error-in-buffer-traceback".into()), - IconName::FileTextOutlined, - ) - .style(ButtonStyle::Transparent) - .tooltip(Tooltip::text("Open Full Error in Buffer")) - .on_click({ - let ename = err.ename.clone(); - let evalue = err.evalue.clone(); - let traceback = err.traceback.clone(); - move |_, window, cx| { - if let Some(workspace) = workspace.upgrade() { - let traceback_text = traceback.read(cx).full_text(); - let full_error = format!( - "{}: {}\n{}", - ename, evalue, traceback_text - ); - let buffer = cx.new(|cx| { - let mut buffer = Buffer::local(full_error, cx) - .with_language( - language::PLAIN_TEXT.clone(), - cx, - ); - buffer.set_capability( - language::Capability::ReadOnly, - cx, - ); - buffer - }); - let editor = Box::new(cx.new(|cx| { - let multibuffer = cx.new(|cx| { - let mut multi_buffer = - MultiBuffer::singleton(buffer.clone(), cx); - multi_buffer - .set_title("Full Error".to_string(), cx); - multi_buffer - }); - Editor::for_multibuffer( - multibuffer, - None, - window, - cx, - ) - })); - workspace.update(cx, |workspace, cx| { - workspace.add_item_to_active_pane( - editor, None, true, window, cx, - ); - }); - } - } - }), - ) - .into_any_element(), - ) - } - Self::Message(_) => None, - Self::Table { content, .. } => { - Self::render_output_controls(content.clone(), workspace, window, cx) - } - Self::ClearOutputWaitMarker => None, - }) - } - - pub fn display_id(&self) -> Option { - match self { - Output::Plain { display_id, .. } => display_id.clone(), - Output::Stream { .. } => None, - Output::Image { display_id, .. } => display_id.clone(), - Output::ErrorOutput(_) => None, - Output::Message(_) => None, - Output::Table { display_id, .. } => display_id.clone(), - Output::Markdown { display_id, .. } => display_id.clone(), - Output::ClearOutputWaitMarker => None, - } - } - - pub fn new( - data: &MimeBundle, - display_id: Option, - window: &mut Window, - cx: &mut App, - ) -> Self { - match data.richest(rank_mime_type) { - Some(MimeType::Plain(text)) => Output::Plain { - content: cx.new(|cx| TerminalOutput::from(text, window, cx)), - display_id, - }, - Some(MimeType::Markdown(text)) => { - let content = cx.new(|cx| MarkdownView::from(text.clone(), cx)); - Output::Markdown { - content, - display_id, - } - } - Some(MimeType::Png(data)) | Some(MimeType::Jpeg(data)) => match ImageView::from(data) { - Ok(view) => Output::Image { - content: cx.new(|_| view), - display_id, - }, - Err(error) => Output::Message(format!("Failed to load image: {}", error)), - }, - Some(MimeType::DataTable(data)) => Output::Table { - content: cx.new(|cx| TableView::new(data, window, cx)), - display_id, - }, - // Any other media types are not supported - _ => Output::Message("Unsupported media type".to_string()), - } - } -} - -#[derive(Default, Clone, Debug)] -pub enum ExecutionStatus { - #[default] - Unknown, - ConnectingToKernel, - Queued, - Executing, - Finished, - ShuttingDown, - Shutdown, - KernelErrored(String), - Restarting, -} - -/// An ExecutionView shows the outputs of an execution. -/// It can hold zero or more outputs, which the user -/// sees as "the output" for a single execution. -pub struct ExecutionView { - #[allow(unused)] - workspace: WeakEntity, - pub outputs: Vec, - pub status: ExecutionStatus, -} - -impl ExecutionView { - pub fn new( - status: ExecutionStatus, - workspace: WeakEntity, - _cx: &mut Context, - ) -> Self { - Self { - workspace, - outputs: Default::default(), - status, - } - } - - /// Accept a Jupyter message belonging to this execution - pub fn push_message( - &mut self, - message: &JupyterMessageContent, - window: &mut Window, - cx: &mut Context, - ) { - let output: Output = match message { - JupyterMessageContent::ExecuteResult(result) => Output::new( - &result.data, - result.transient.as_ref().and_then(|t| t.display_id.clone()), - window, - cx, - ), - JupyterMessageContent::DisplayData(result) => Output::new( - &result.data, - result.transient.as_ref().and_then(|t| t.display_id.clone()), - window, - cx, - ), - JupyterMessageContent::StreamContent(result) => { - // Previous stream data will combine together, handling colors, carriage returns, etc - if let Some(new_terminal) = self.apply_terminal_text(&result.text, window, cx) { - new_terminal - } else { - return; - } - } - JupyterMessageContent::ErrorOutput(result) => { - let terminal = - cx.new(|cx| TerminalOutput::from(&result.traceback.join("\n"), window, cx)); - - Output::ErrorOutput(ErrorView { - ename: result.ename.clone(), - evalue: result.evalue.clone(), - traceback: terminal, - }) - } - JupyterMessageContent::ExecuteReply(reply) => { - for payload in reply.payload.iter() { - if let runtimelib::Payload::Page { data, .. } = payload { - let output = Output::new(data, None, window, cx); - self.outputs.push(output); - } - } - cx.notify(); - return; - } - JupyterMessageContent::ClearOutput(options) => { - if !options.wait { - self.outputs.clear(); - cx.notify(); - return; - } - - // Create a marker to clear the output after we get in a new output - Output::ClearOutputWaitMarker - } - JupyterMessageContent::Status(status) => { - match status.execution_state { - ExecutionState::Busy => { - self.status = ExecutionStatus::Executing; - } - ExecutionState::Idle => self.status = ExecutionStatus::Finished, - ExecutionState::Unknown => self.status = ExecutionStatus::Unknown, - ExecutionState::Starting => self.status = ExecutionStatus::ConnectingToKernel, - ExecutionState::Restarting => self.status = ExecutionStatus::Restarting, - ExecutionState::Terminating => self.status = ExecutionStatus::ShuttingDown, - ExecutionState::AutoRestarting => self.status = ExecutionStatus::Restarting, - ExecutionState::Dead => self.status = ExecutionStatus::Shutdown, - ExecutionState::Other(_) => self.status = ExecutionStatus::Unknown, - } - cx.notify(); - return; - } - _msg => { - return; - } - }; - - // Check for a clear output marker as the previous output, so we can clear it out - if let Some(output) = self.outputs.last() - && let Output::ClearOutputWaitMarker = output - { - self.outputs.clear(); - } - - self.outputs.push(output); - - cx.notify(); - } - - pub fn update_display_data( - &mut self, - data: &MimeBundle, - display_id: &str, - window: &mut Window, - cx: &mut Context, - ) { - let mut any = false; - - self.outputs.iter_mut().for_each(|output| { - if let Some(other_display_id) = output.display_id().as_ref() - && other_display_id == display_id - { - *output = Output::new(data, Some(display_id.to_owned()), window, cx); - any = true; - } - }); - - if any { - cx.notify(); - } - } - - fn apply_terminal_text( - &mut self, - text: &str, - window: &mut Window, - cx: &mut Context, - ) -> Option { - if let Some(last_output) = self.outputs.last_mut() - && let Output::Stream { - content: last_stream, - } = last_output - { - // Don't need to add a new output, we already have a terminal output - // and can just update the most recent terminal output - last_stream.update(cx, |last_stream, cx| { - last_stream.append_text(text, cx); - cx.notify(); - }); - return None; - } - - Some(Output::Stream { - content: cx.new(|cx| TerminalOutput::from(text, window, cx)), - }) - } -} - -impl Render for ExecutionView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let status = match &self.status { - ExecutionStatus::ConnectingToKernel => Label::new("Connecting to kernel...") - .color(Color::Muted) - .into_any_element(), - ExecutionStatus::Executing => h_flex() - .gap_2() - .child( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .color(Color::Muted) - .with_rotate_animation(3), - ) - .child(Label::new("Executing...").color(Color::Muted)) - .into_any_element(), - ExecutionStatus::Finished => Icon::new(IconName::Check) - .size(IconSize::Small) - .into_any_element(), - ExecutionStatus::Unknown => Label::new("Unknown status") - .color(Color::Muted) - .into_any_element(), - ExecutionStatus::ShuttingDown => Label::new("Kernel shutting down...") - .color(Color::Muted) - .into_any_element(), - ExecutionStatus::Restarting => Label::new("Kernel restarting...") - .color(Color::Muted) - .into_any_element(), - ExecutionStatus::Shutdown => Label::new("Kernel shutdown") - .color(Color::Muted) - .into_any_element(), - ExecutionStatus::Queued => Label::new("Queued...") - .color(Color::Muted) - .into_any_element(), - ExecutionStatus::KernelErrored(error) => Label::new(format!("Kernel error: {}", error)) - .color(Color::Error) - .into_any_element(), - }; - - if self.outputs.is_empty() { - return v_flex() - .min_h(window.line_height()) - .justify_center() - .child(status) - .into_any_element(); - } - - div() - .w_full() - .children( - self.outputs - .iter() - .map(|output| output.render(self.workspace.clone(), window, cx)), - ) - .children(match self.status { - ExecutionStatus::Executing => vec![status], - ExecutionStatus::Queued => vec![status], - _ => vec![], - }) - .into_any_element() - } -} diff --git a/crates/repl/src/outputs/image.rs b/crates/repl/src/outputs/image.rs deleted file mode 100644 index fefdbec2fa..0000000000 --- a/crates/repl/src/outputs/image.rs +++ /dev/null @@ -1,98 +0,0 @@ -use anyhow::Result; -use base64::{ - Engine as _, alphabet, - engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig}, -}; -use gpui::{App, ClipboardItem, Image, ImageFormat, RenderImage, Window, img}; -use std::sync::Arc; -use ui::{IntoElement, Styled, div, prelude::*}; - -use crate::outputs::OutputContent; - -/// ImageView renders an image inline in an editor, adapting to the line height to fit the image. -pub struct ImageView { - clipboard_image: Arc, - height: u32, - width: u32, - image: Arc, -} - -pub const STANDARD_INDIFFERENT: GeneralPurpose = GeneralPurpose::new( - &alphabet::STANDARD, - GeneralPurposeConfig::new() - .with_encode_padding(false) - .with_decode_padding_mode(DecodePaddingMode::Indifferent), -); - -impl ImageView { - pub fn from(base64_encoded_data: &str) -> Result { - let filtered = - base64_encoded_data.replace(&[' ', '\n', '\t', '\r', '\x0b', '\x0c'][..], ""); - let bytes = STANDARD_INDIFFERENT.decode(filtered)?; - - let format = image::guess_format(&bytes)?; - - let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8(); - - // Convert from RGBA to BGRA. - for pixel in data.chunks_exact_mut(4) { - pixel.swap(0, 2); - } - - let height = data.height(); - let width = data.width(); - - let gpui_image_data = RenderImage::new(vec![image::Frame::new(data)]); - - let format = match format { - image::ImageFormat::Png => ImageFormat::Png, - image::ImageFormat::Jpeg => ImageFormat::Jpeg, - image::ImageFormat::Gif => ImageFormat::Gif, - image::ImageFormat::WebP => ImageFormat::Webp, - image::ImageFormat::Tiff => ImageFormat::Tiff, - image::ImageFormat::Bmp => ImageFormat::Bmp, - image::ImageFormat::Ico => ImageFormat::Ico, - format => { - anyhow::bail!("unsupported image format {format:?}"); - } - }; - - // Convert back to a GPUI image for use with the clipboard - let clipboard_image = Arc::new(Image::from_bytes(format, bytes)); - - Ok(ImageView { - clipboard_image, - height, - width, - image: Arc::new(gpui_image_data), - }) - } -} - -impl Render for ImageView { - fn render(&mut self, window: &mut Window, _: &mut Context) -> impl IntoElement { - let line_height = window.line_height(); - - let (height, width) = if self.height as f32 / f32::from(line_height) == u8::MAX as f32 { - let height = u8::MAX as f32 * line_height; - let width = self.width as f32 * height / self.height as f32; - (height, width) - } else { - (self.height.into(), self.width.into()) - }; - - let image = self.image.clone(); - - div().h(height).w(width).child(img(image)) - } -} - -impl OutputContent for ImageView { - fn clipboard_content(&self, _window: &Window, _cx: &App) -> Option { - Some(ClipboardItem::new_image(self.clipboard_image.as_ref())) - } - - fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool { - true - } -} diff --git a/crates/repl/src/outputs/markdown.rs b/crates/repl/src/outputs/markdown.rs deleted file mode 100644 index bd88f4e159..0000000000 --- a/crates/repl/src/outputs/markdown.rs +++ /dev/null @@ -1,93 +0,0 @@ -use anyhow::Result; -use gpui::{ - App, ClipboardItem, Context, Entity, RetainAllImageCache, Task, Window, div, prelude::*, -}; -use language::Buffer; -use markdown_preview::{ - markdown_elements::ParsedMarkdown, markdown_parser::parse_markdown, - markdown_renderer::render_markdown_block, -}; -use ui::v_flex; - -use crate::outputs::OutputContent; - -pub struct MarkdownView { - raw_text: String, - image_cache: Entity, - contents: Option, - parsing_markdown_task: Option>>, -} - -impl MarkdownView { - pub fn from(text: String, cx: &mut Context) -> Self { - let parsed = { - let text = text.clone(); - cx.background_spawn(async move { parse_markdown(&text.clone(), None, None).await }) - }; - let task = cx.spawn(async move |markdown_view, cx| { - let content = parsed.await; - - markdown_view.update(cx, |markdown, cx| { - markdown.parsing_markdown_task.take(); - markdown.contents = Some(content); - cx.notify(); - }) - }); - - Self { - raw_text: text, - image_cache: RetainAllImageCache::new(cx), - contents: None, - parsing_markdown_task: Some(task), - } - } -} - -impl OutputContent for MarkdownView { - fn clipboard_content(&self, _window: &Window, _cx: &App) -> Option { - Some(ClipboardItem::new_string(self.raw_text.clone())) - } - - fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool { - true - } - - fn has_buffer_content(&self, _window: &Window, _cx: &App) -> bool { - true - } - - fn buffer_content(&mut self, _: &mut Window, cx: &mut App) -> Option> { - let buffer = cx.new(|cx| { - // TODO: Bring in the language registry so we can set the language to markdown - let mut buffer = Buffer::local(self.raw_text.clone(), cx) - .with_language(language::PLAIN_TEXT.clone(), cx); - buffer.set_capability(language::Capability::ReadOnly, cx); - buffer - }); - Some(buffer) - } -} - -impl Render for MarkdownView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let Some(parsed) = self.contents.as_ref() else { - return div().into_any_element(); - }; - - let mut markdown_render_context = - markdown_preview::markdown_renderer::RenderContext::new(None, window, cx); - - v_flex() - .image_cache(self.image_cache.clone()) - .gap_3() - .py_4() - .children(parsed.children.iter().map(|child| { - div().relative().child( - div() - .relative() - .child(render_markdown_block(child, &mut markdown_render_context)), - ) - })) - .into_any_element() - } -} diff --git a/crates/repl/src/outputs/plain.rs b/crates/repl/src/outputs/plain.rs deleted file mode 100644 index 54e4983b9f..0000000000 --- a/crates/repl/src/outputs/plain.rs +++ /dev/null @@ -1,351 +0,0 @@ -//! # Plain Text Output -//! -//! This module provides functionality for rendering plain text output in a terminal-like format. -//! It uses the Alacritty terminal emulator backend to process and display text, supporting -//! ANSI escape sequences for formatting, colors, and other terminal features. -//! -//! The main component of this module is the `TerminalOutput` struct, which handles the parsing -//! and rendering of text input, simulating a basic terminal environment within REPL output. -//! -//! This module is used for displaying: -//! -//! - Standard output (stdout) -//! - Standard error (stderr) -//! - Plain text content -//! - Error tracebacks -//! - -use alacritty_terminal::{ - event::VoidListener, - grid::Dimensions as _, - index::{Column, Line, Point}, - term::Config, - vte::ansi::Processor, -}; -use gpui::{Bounds, ClipboardItem, Entity, FontStyle, TextStyle, WhiteSpace, canvas, size}; -use language::Buffer; -use settings::Settings as _; -use terminal::terminal_settings::TerminalSettings; -use terminal_view::terminal_element::TerminalElement; -use theme::ThemeSettings; -use ui::{IntoElement, prelude::*}; - -use crate::outputs::OutputContent; -use crate::repl_settings::ReplSettings; - -/// The `TerminalOutput` struct handles the parsing and rendering of text input, -/// simulating a basic terminal environment within REPL output. -/// -/// `TerminalOutput` is designed to handle various types of text-based output, including: -/// -/// * stdout (standard output) -/// * stderr (standard error) -/// * text/plain content -/// * error tracebacks -/// -/// It uses the Alacritty terminal emulator backend to process and render text, -/// supporting ANSI escape sequences for text formatting and colors. -/// -pub struct TerminalOutput { - full_buffer: Option>, - /// ANSI escape sequence processor for parsing input text. - parser: Processor, - /// Alacritty terminal instance that manages the terminal state and content. - handler: alacritty_terminal::Term, -} - -/// Returns the default text style for the terminal output. -pub fn text_style(window: &mut Window, cx: &mut App) -> TextStyle { - let settings = ThemeSettings::get_global(cx).clone(); - - let font_size = settings.buffer_font_size(cx).into(); - let font_family = settings.buffer_font.family; - let font_features = settings.buffer_font.features; - let font_weight = settings.buffer_font.weight; - let font_fallbacks = settings.buffer_font.fallbacks; - - let theme = cx.theme(); - - TextStyle { - font_family, - font_features, - font_weight, - font_fallbacks, - font_size, - font_style: FontStyle::Normal, - line_height: window.line_height().into(), - background_color: Some(theme.colors().terminal_ansi_background), - white_space: WhiteSpace::Normal, - // These are going to be overridden per-cell - color: theme.colors().terminal_foreground, - ..Default::default() - } -} - -/// Returns the default terminal size for the terminal output. -pub fn terminal_size(window: &mut Window, cx: &mut App) -> terminal::TerminalBounds { - let text_style = text_style(window, cx); - let text_system = window.text_system(); - - let line_height = window.line_height(); - - let font_pixels = text_style.font_size.to_pixels(window.rem_size()); - let font_id = text_system.resolve_font(&text_style.font()); - - let cell_width = text_system - .advance(font_id, font_pixels, 'w') - .unwrap() - .width; - - let num_lines = ReplSettings::get_global(cx).max_lines; - let columns = ReplSettings::get_global(cx).max_columns; - - // Reversed math from terminal::TerminalSize to get pixel width according to terminal width - let width = columns as f32 * cell_width; - let height = num_lines as f32 * window.line_height(); - - terminal::TerminalBounds { - cell_width, - line_height, - bounds: Bounds { - origin: gpui::Point::default(), - size: size(width, height), - }, - } -} - -impl TerminalOutput { - /// Creates a new `TerminalOutput` instance. - /// - /// This method initializes a new terminal emulator with default configuration - /// and sets up the necessary components for handling terminal events and rendering. - /// - pub fn new(window: &mut Window, cx: &mut App) -> Self { - let term = alacritty_terminal::Term::new( - Config::default(), - &terminal_size(window, cx), - VoidListener, - ); - - Self { - parser: Processor::new(), - handler: term, - full_buffer: None, - } - } - - /// Creates a new `TerminalOutput` instance with initial content. - /// - /// Initializes a new terminal output and populates it with the provided text. - /// - /// # Arguments - /// - /// * `text` - A string slice containing the initial text for the terminal output. - /// * `cx` - A mutable reference to the `WindowContext` for initialization. - /// - /// # Returns - /// - /// A new instance of `TerminalOutput` containing the provided text. - pub fn from(text: &str, window: &mut Window, cx: &mut App) -> Self { - let mut output = Self::new(window, cx); - output.append_text(text, cx); - output - } - - /// Appends text to the terminal output. - /// - /// Processes each byte of the input text, handling newline characters specially - /// to ensure proper cursor movement. Uses the ANSI parser to process the input - /// and update the terminal state. - /// - /// As an example, if the user runs the following Python code in this REPL: - /// - /// ```python - /// import time - /// print("Hello,", end="") - /// time.sleep(1) - /// print(" world!") - /// ``` - /// - /// Then append_text will be called twice, with the following arguments: - /// - /// ```ignore - /// terminal_output.append_text("Hello,"); - /// terminal_output.append_text(" world!"); - /// ``` - /// Resulting in a single output of "Hello, world!". - /// - /// # Arguments - /// - /// * `text` - A string slice containing the text to be appended. - pub fn append_text(&mut self, text: &str, cx: &mut App) { - for byte in text.as_bytes() { - if *byte == b'\n' { - // Dirty (?) hack to move the cursor down - self.parser.advance(&mut self.handler, &[b'\r']); - self.parser.advance(&mut self.handler, &[b'\n']); - } else { - self.parser.advance(&mut self.handler, &[*byte]); - } - } - - // This will keep the buffer up to date, though with some terminal codes it won't be perfect - if let Some(buffer) = self.full_buffer.as_ref() { - buffer.update(cx, |buffer, cx| { - buffer.edit([(buffer.len()..buffer.len(), text)], None, cx); - }); - } - } - - pub fn full_text(&self) -> String { - fn sanitize(mut line: String) -> Option { - line.retain(|ch| ch != '\u{0}' && ch != '\r'); - if line.trim().is_empty() { - return None; - } - let trimmed = line.trim_end_matches([' ', '\t']); - Some(trimmed.to_owned()) - } - - let mut lines = Vec::new(); - - // Get the total number of lines, including history - let total_lines = self.handler.grid().total_lines(); - let visible_lines = self.handler.screen_lines(); - let history_lines = total_lines - visible_lines; - - // Capture history lines in correct order (oldest to newest) - for line in (0..history_lines).rev() { - let line_index = Line(-(line as i32) - 1); - let start = Point::new(line_index, Column(0)); - let end = Point::new(line_index, Column(self.handler.columns() - 1)); - if let Some(cleaned) = sanitize(self.handler.bounds_to_string(start, end)) { - lines.push(cleaned); - } - } - - // Capture visible lines - for line in 0..visible_lines { - let line_index = Line(line as i32); - let start = Point::new(line_index, Column(0)); - let end = Point::new(line_index, Column(self.handler.columns() - 1)); - if let Some(cleaned) = sanitize(self.handler.bounds_to_string(start, end)) { - lines.push(cleaned); - } - } - - if lines.is_empty() { - String::new() - } else { - let mut full_text = lines.join("\n"); - full_text.push('\n'); - full_text - } - } -} - -impl Render for TerminalOutput { - /// Renders the terminal output as a GPUI element. - /// - /// Converts the current terminal state into a renderable GPUI element. It handles - /// the layout of the terminal grid, calculates the dimensions of the output, and - /// creates a canvas element that paints the terminal cells and background rectangles. - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let text_style = text_style(window, cx); - let text_system = window.text_system(); - - let grid = self - .handler - .renderable_content() - .display_iter - .map(|ic| terminal::IndexedCell { - point: ic.point, - cell: ic.cell.clone(), - }); - let minimum_contrast = TerminalSettings::get_global(cx).minimum_contrast; - let (rects, batched_text_runs) = - TerminalElement::layout_grid(grid, 0, &text_style, None, minimum_contrast, cx); - - // lines are 0-indexed, so we must add 1 to get the number of lines - let text_line_height = text_style.line_height_in_pixels(window.rem_size()); - let num_lines = batched_text_runs - .iter() - .map(|b| b.start_point.line) - .max() - .unwrap_or(0) - + 1; - let height = num_lines as f32 * text_line_height; - - let font_pixels = text_style.font_size.to_pixels(window.rem_size()); - let font_id = text_system.resolve_font(&text_style.font()); - - let cell_width = text_system - .advance(font_id, font_pixels, 'w') - .map(|advance| advance.width) - .unwrap_or(Pixels::ZERO); - - canvas( - // prepaint - move |_bounds, _, _| {}, - // paint - move |bounds, _, window, cx| { - for rect in rects { - rect.paint( - bounds.origin, - &terminal::TerminalBounds { - cell_width, - line_height: text_line_height, - bounds, - }, - window, - ); - } - - for batch in batched_text_runs { - batch.paint( - bounds.origin, - &terminal::TerminalBounds { - cell_width, - line_height: text_line_height, - bounds, - }, - window, - cx, - ); - } - }, - ) - // We must set the height explicitly for the editor block to size itself correctly - .h(height) - } -} - -impl OutputContent for TerminalOutput { - fn clipboard_content(&self, _window: &Window, _cx: &App) -> Option { - Some(ClipboardItem::new_string(self.full_text())) - } - - fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool { - true - } - - fn has_buffer_content(&self, _window: &Window, _cx: &App) -> bool { - true - } - - fn buffer_content(&mut self, _: &mut Window, cx: &mut App) -> Option> { - if self.full_buffer.as_ref().is_some() { - return self.full_buffer.clone(); - } - - let buffer = cx.new(|cx| { - let mut buffer = - Buffer::local(self.full_text(), cx).with_language(language::PLAIN_TEXT.clone(), cx); - buffer.set_capability(language::Capability::ReadOnly, cx); - buffer - }); - - self.full_buffer = Some(buffer.clone()); - Some(buffer) - } -} diff --git a/crates/repl/src/outputs/table.rs b/crates/repl/src/outputs/table.rs deleted file mode 100644 index f6bf30f394..0000000000 --- a/crates/repl/src/outputs/table.rs +++ /dev/null @@ -1,290 +0,0 @@ -//! # Table Output for REPL -//! -//! This module provides functionality to render tabular data in Zed's REPL output. -//! -//! It supports the [Frictionless Data Table Schema](https://specs.frictionlessdata.io/table-schema/) -//! for data interchange, implemented by Pandas in Python and Polars for Deno. -//! -//! # Python Example -//! -//! Tables can be created and displayed in two main ways: -//! -//! 1. Using raw JSON data conforming to the Tabular Data Resource specification. -//! 2. Using Pandas DataFrames (in Python kernels). -//! -//! ## Raw JSON Method -//! -//! To create a table using raw JSON, you need to provide a JSON object that conforms -//! to the Tabular Data Resource specification. Here's an example: -//! -//! ```json -//! { -//! "schema": { -//! "fields": [ -//! {"name": "id", "type": "integer"}, -//! {"name": "name", "type": "string"}, -//! {"name": "age", "type": "integer"} -//! ] -//! }, -//! "data": [ -//! {"id": 1, "name": "Alice", "age": 30}, -//! {"id": 2, "name": "Bob", "age": 28}, -//! {"id": 3, "name": "Charlie", "age": 35} -//! ] -//! } -//! ``` -//! -//! ## Pandas Method -//! -//! To create a table using Pandas in a Python kernel, you can use the following steps: -//! -//! ```python -//! import pandas as pd -//! -//! # Enable table schema output -//! pd.set_option('display.html.table_schema', True) -//! -//! # Create a DataFrame -//! df = pd.DataFrame({ -//! 'id': [1, 2, 3], -//! 'name': ['Alice', 'Bob', 'Charlie'], -//! 'age': [30, 28, 35] -//! }) -//! -//! # Display the DataFrame -//! display(df) -//! ``` -use gpui::{AnyElement, ClipboardItem, TextRun}; -use runtimelib::datatable::TableSchema; -use runtimelib::media::datatable::TabularDataResource; -use serde_json::Value; -use settings::Settings; -use theme::ThemeSettings; -use ui::{IntoElement, Styled, div, prelude::*, v_flex}; -use util::markdown::MarkdownEscaped; - -use crate::outputs::OutputContent; - -/// TableView renders a static table inline in a buffer. -/// -/// It uses the -/// specification for data interchange. -pub struct TableView { - pub table: TabularDataResource, - pub widths: Vec, - cached_clipboard_content: ClipboardItem, -} - -fn cell_content(row: &Value, field: &str) -> String { - match row.get(field) { - Some(Value::String(s)) => s.clone(), - Some(Value::Number(n)) => n.to_string(), - Some(Value::Bool(b)) => b.to_string(), - Some(Value::Array(arr)) => format!("{:?}", arr), - Some(Value::Object(obj)) => format!("{:?}", obj), - Some(Value::Null) | None => String::new(), - } -} - -// Declare constant for the padding multiple on the line height -const TABLE_Y_PADDING_MULTIPLE: f32 = 0.5; - -impl TableView { - pub fn new(table: &TabularDataResource, window: &mut Window, cx: &mut App) -> Self { - let mut widths = Vec::with_capacity(table.schema.fields.len()); - - let text_system = window.text_system(); - let text_style = window.text_style(); - let text_font = ThemeSettings::get_global(cx).buffer_font.clone(); - let font_size = ThemeSettings::get_global(cx).buffer_font_size(cx); - let mut runs = [TextRun { - len: 0, - font: text_font, - color: text_style.color, - ..Default::default() - }]; - - for field in table.schema.fields.iter() { - runs[0].len = field.name.len(); - let mut width = text_system - .layout_line(&field.name, font_size, &runs, None) - .width; - - let Some(data) = table.data.as_ref() else { - widths.push(width); - continue; - }; - - for row in data { - let content = cell_content(row, &field.name); - runs[0].len = content.len(); - let cell_width = window - .text_system() - .layout_line(&content, font_size, &runs, None) - .width; - - width = width.max(cell_width) - } - - widths.push(width) - } - - let cached_clipboard_content = Self::create_clipboard_content(table); - - Self { - table: table.clone(), - widths, - cached_clipboard_content: ClipboardItem::new_string(cached_clipboard_content), - } - } - - fn create_clipboard_content(table: &TabularDataResource) -> String { - let data = match table.data.as_ref() { - Some(data) => data, - None => &Vec::new(), - }; - let schema = table.schema.clone(); - - let mut markdown = format!( - "| {} |\n", - table - .schema - .fields - .iter() - .map(|field| field.name.clone()) - .collect::>() - .join(" | ") - ); - - markdown.push_str("|---"); - for _ in 1..table.schema.fields.len() { - markdown.push_str("|---"); - } - markdown.push_str("|\n"); - - let body = data - .iter() - .map(|record: &Value| { - let row_content = schema - .fields - .iter() - .map(|field| MarkdownEscaped(&cell_content(record, &field.name)).to_string()) - .collect::>(); - - row_content.join(" | ") - }) - .collect::>(); - - for row in body { - markdown.push_str(&format!("| {} |\n", row)); - } - - markdown - } - - pub fn render_row( - &self, - schema: &TableSchema, - is_header: bool, - row: &Value, - window: &mut Window, - cx: &mut App, - ) -> AnyElement { - let theme = cx.theme(); - - let line_height = window.line_height(); - - let row_cells = schema - .fields - .iter() - .zip(self.widths.iter()) - .map(|(field, width)| { - let container = match field.field_type { - runtimelib::datatable::FieldType::String => div(), - - runtimelib::datatable::FieldType::Number - | runtimelib::datatable::FieldType::Integer - | runtimelib::datatable::FieldType::Date - | runtimelib::datatable::FieldType::Time - | runtimelib::datatable::FieldType::Datetime - | runtimelib::datatable::FieldType::Year - | runtimelib::datatable::FieldType::Duration - | runtimelib::datatable::FieldType::Yearmonth => v_flex().items_end(), - - _ => div(), - }; - - let value = cell_content(row, &field.name); - - let mut cell = container - .min_w(*width + px(22.)) - .w(*width + px(22.)) - .child(value) - .px_2() - .py((TABLE_Y_PADDING_MULTIPLE / 2.0) * line_height) - .border_color(theme.colors().border); - - if is_header { - cell = cell.border_1().bg(theme.colors().border_focused) - } else { - cell = cell.border_1() - } - cell - }) - .collect::>(); - - let mut total_width = px(0.); - for width in self.widths.iter() { - // Width fudge factor: border + 2 (heading), padding - total_width += *width + px(22.); - } - - h_flex() - .w(total_width) - .children(row_cells) - .into_any_element() - } -} - -impl Render for TableView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let data = match &self.table.data { - Some(data) => data, - None => return div().into_any_element(), - }; - - let mut headings = serde_json::Map::new(); - for field in &self.table.schema.fields { - headings.insert(field.name.clone(), Value::String(field.name.clone())); - } - let header = self.render_row( - &self.table.schema, - true, - &Value::Object(headings), - window, - cx, - ); - - let body = data - .iter() - .map(|row| self.render_row(&self.table.schema, false, row, window, cx)); - - v_flex() - .id("table") - .overflow_x_scroll() - .w_full() - .child(header) - .children(body) - .into_any_element() - } -} - -impl OutputContent for TableView { - fn clipboard_content(&self, _window: &Window, _cx: &App) -> Option { - Some(self.cached_clipboard_content.clone()) - } - - fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool { - true - } -} diff --git a/crates/repl/src/outputs/user_error.rs b/crates/repl/src/outputs/user_error.rs deleted file mode 100644 index 4218b417c5..0000000000 --- a/crates/repl/src/outputs/user_error.rs +++ /dev/null @@ -1,45 +0,0 @@ -use gpui::{AnyElement, App, Entity, FontWeight, Window}; -use ui::{Label, h_flex, prelude::*, v_flex}; - -use crate::outputs::plain::TerminalOutput; - -/// Userspace error from the kernel -#[derive(Clone)] -pub struct ErrorView { - pub ename: String, - pub evalue: String, - pub traceback: Entity, -} - -impl ErrorView { - pub fn render(&self, window: &mut Window, cx: &mut App) -> Option { - let theme = cx.theme(); - - let padding = window.line_height() / 2.; - - Some( - v_flex() - .gap_3() - .child( - h_flex() - .font_buffer(cx) - .child( - Label::new(format!("{}: ", self.ename.clone())) - .color(Color::Error) - .weight(FontWeight::BOLD), - ) - .child(Label::new(self.evalue.clone()).weight(FontWeight::BOLD)), - ) - .child( - div() - .w_full() - .px(padding) - .py(padding) - .border_l_1() - .border_color(theme.status().error_border) - .child(self.traceback.clone()), - ) - .into_any_element(), - ) - } -} diff --git a/crates/repl/src/repl.rs b/crates/repl/src/repl.rs deleted file mode 100644 index 346cca0211..0000000000 --- a/crates/repl/src/repl.rs +++ /dev/null @@ -1,61 +0,0 @@ -pub mod components; -mod jupyter_settings; -pub mod kernels; -pub mod notebook; -mod outputs; -mod repl_editor; -mod repl_sessions_ui; -mod repl_settings; -mod repl_store; -mod session; - -use std::{sync::Arc, time::Duration}; - -use async_dispatcher::{Dispatcher, Runnable, set_dispatcher}; -use gpui::{App, PlatformDispatcher, Priority, RunnableVariant}; -use project::Fs; -pub use runtimelib::ExecutionState; - -pub use crate::jupyter_settings::JupyterSettings; -pub use crate::kernels::{Kernel, KernelSpecification, KernelStatus}; -pub use crate::repl_editor::*; -pub use crate::repl_sessions_ui::{ - ClearOutputs, Interrupt, ReplSessionsPage, Restart, Run, Sessions, Shutdown, -}; -pub use crate::repl_settings::ReplSettings; -use crate::repl_store::ReplStore; -pub use crate::session::Session; - -pub const KERNEL_DOCS_URL: &str = "https://zed.dev/docs/repl#changing-kernels"; - -pub fn init(fs: Arc, cx: &mut App) { - set_dispatcher(zed_dispatcher(cx)); - repl_sessions_ui::init(cx); - ReplStore::init(fs, cx); -} - -fn zed_dispatcher(cx: &mut App) -> impl Dispatcher { - struct ZedDispatcher { - dispatcher: Arc, - } - - // PlatformDispatcher is _super_ close to the same interface we put in - // async-dispatcher, except for the task label in dispatch. Later we should - // just make that consistent so we have this dispatcher ready to go for - // other crates in Zed. - impl Dispatcher for ZedDispatcher { - fn dispatch(&self, runnable: Runnable) { - self.dispatcher - .dispatch(RunnableVariant::Compat(runnable), None, Priority::default()); - } - - fn dispatch_after(&self, duration: Duration, runnable: Runnable) { - self.dispatcher - .dispatch_after(duration, RunnableVariant::Compat(runnable)); - } - } - - ZedDispatcher { - dispatcher: cx.background_executor().dispatcher.clone(), - } -} diff --git a/crates/repl/src/repl_editor.rs b/crates/repl/src/repl_editor.rs deleted file mode 100644 index 9e52637ab7..0000000000 --- a/crates/repl/src/repl_editor.rs +++ /dev/null @@ -1,824 +0,0 @@ -//! REPL operations on an [`Editor`]. - -use std::ops::Range; -use std::sync::Arc; - -use anyhow::{Context as _, Result}; -use editor::{Editor, MultiBufferOffset}; -use gpui::{App, Entity, WeakEntity, Window, prelude::*}; -use language::{BufferSnapshot, Language, LanguageName, Point}; -use project::{ProjectItem as _, WorktreeId}; - -use crate::repl_store::ReplStore; -use crate::session::SessionEvent; -use crate::{ - ClearOutputs, Interrupt, JupyterSettings, KernelSpecification, Restart, Session, Shutdown, -}; - -pub fn assign_kernelspec( - kernel_specification: KernelSpecification, - weak_editor: WeakEntity, - window: &mut Window, - cx: &mut App, -) -> Result<()> { - let store = ReplStore::global(cx); - if !store.read(cx).is_enabled() { - return Ok(()); - } - - let worktree_id = crate::repl_editor::worktree_id_for_editor(weak_editor.clone(), cx) - .context("editor is not in a worktree")?; - - store.update(cx, |store, cx| { - store.set_active_kernelspec(worktree_id, kernel_specification.clone(), cx); - }); - - let fs = store.read(cx).fs().clone(); - - if let Some(session) = store.read(cx).get_session(weak_editor.entity_id()).cloned() { - // Drop previous session, start new one - session.update(cx, |session, cx| { - session.clear_outputs(cx); - session.shutdown(window, cx); - cx.notify(); - }); - } - - let session = - cx.new(|cx| Session::new(weak_editor.clone(), fs, kernel_specification, window, cx)); - - weak_editor - .update(cx, |_editor, cx| { - cx.notify(); - - cx.subscribe(&session, { - let store = store.clone(); - move |_this, _session, event, cx| match event { - SessionEvent::Shutdown(shutdown_event) => { - store.update(cx, |store, _cx| { - store.remove_session(shutdown_event.entity_id()); - }); - } - } - }) - .detach(); - }) - .ok(); - - store.update(cx, |store, _cx| { - store.insert_session(weak_editor.entity_id(), session.clone()); - }); - - Ok(()) -} - -pub fn run( - editor: WeakEntity, - move_down: bool, - window: &mut Window, - cx: &mut App, -) -> Result<()> { - let store = ReplStore::global(cx); - if !store.read(cx).is_enabled() { - return Ok(()); - } - - let editor = editor.upgrade().context("editor was dropped")?; - let selected_range = editor - .update(cx, |editor, cx| { - editor - .selections - .newest_adjusted(&editor.display_snapshot(cx)) - }) - .range(); - let multibuffer = editor.read(cx).buffer().clone(); - let Some(buffer) = multibuffer.read(cx).as_singleton() else { - return Ok(()); - }; - - let Some(project_path) = buffer.read(cx).project_path(cx) else { - return Ok(()); - }; - - let (runnable_ranges, next_cell_point) = - runnable_ranges(&buffer.read(cx).snapshot(), selected_range, cx); - - for runnable_range in runnable_ranges { - let Some(language) = multibuffer.read(cx).language_at(runnable_range.start, cx) else { - continue; - }; - - let kernel_specification = store - .read(cx) - .active_kernelspec(project_path.worktree_id, Some(language.clone()), cx) - .with_context(|| format!("No kernel found for language: {}", language.name()))?; - - let fs = store.read(cx).fs().clone(); - - let session = if let Some(session) = store.read(cx).get_session(editor.entity_id()).cloned() - { - session - } else { - let weak_editor = editor.downgrade(); - let session = - cx.new(|cx| Session::new(weak_editor, fs, kernel_specification, window, cx)); - - editor.update(cx, |_editor, cx| { - cx.notify(); - - cx.subscribe(&session, { - let store = store.clone(); - move |_this, _session, event, cx| match event { - SessionEvent::Shutdown(shutdown_event) => { - store.update(cx, |store, _cx| { - store.remove_session(shutdown_event.entity_id()); - }); - } - } - }) - .detach(); - }); - - store.update(cx, |store, _cx| { - store.insert_session(editor.entity_id(), session.clone()); - }); - - session - }; - - let selected_text; - let anchor_range; - let next_cursor; - { - let snapshot = multibuffer.read(cx).read(cx); - selected_text = snapshot - .text_for_range(runnable_range.clone()) - .collect::(); - anchor_range = snapshot.anchor_before(runnable_range.start) - ..snapshot.anchor_after(runnable_range.end); - next_cursor = next_cell_point.map(|point| snapshot.anchor_after(point)); - } - - session.update(cx, |session, cx| { - session.execute( - selected_text, - anchor_range, - next_cursor, - move_down, - window, - cx, - ); - }); - } - - anyhow::Ok(()) -} - -pub enum SessionSupport { - ActiveSession(Entity), - Inactive(KernelSpecification), - RequiresSetup(LanguageName), - Unsupported, -} - -pub fn worktree_id_for_editor(editor: WeakEntity, cx: &mut App) -> Option { - editor.upgrade().and_then(|editor| { - editor - .read(cx) - .buffer() - .read(cx) - .as_singleton()? - .read(cx) - .project_path(cx) - .map(|path| path.worktree_id) - }) -} - -pub fn session(editor: WeakEntity, cx: &mut App) -> SessionSupport { - let store = ReplStore::global(cx); - let entity_id = editor.entity_id(); - - if let Some(session) = store.read(cx).get_session(entity_id).cloned() { - return SessionSupport::ActiveSession(session); - }; - - let Some(language) = get_language(editor.clone(), cx) else { - return SessionSupport::Unsupported; - }; - - let worktree_id = worktree_id_for_editor(editor, cx); - - let Some(worktree_id) = worktree_id else { - return SessionSupport::Unsupported; - }; - - let kernelspec = store - .read(cx) - .active_kernelspec(worktree_id, Some(language.clone()), cx); - - match kernelspec { - Some(kernelspec) => SessionSupport::Inactive(kernelspec), - None => { - // For language_supported, need to check available kernels for language - if language_supported(&language, cx) { - SessionSupport::RequiresSetup(language.name()) - } else { - SessionSupport::Unsupported - } - } - } -} - -pub fn clear_outputs(editor: WeakEntity, cx: &mut App) { - let store = ReplStore::global(cx); - let entity_id = editor.entity_id(); - let Some(session) = store.read(cx).get_session(entity_id).cloned() else { - return; - }; - session.update(cx, |session, cx| { - session.clear_outputs(cx); - cx.notify(); - }); -} - -pub fn interrupt(editor: WeakEntity, cx: &mut App) { - let store = ReplStore::global(cx); - let entity_id = editor.entity_id(); - let Some(session) = store.read(cx).get_session(entity_id).cloned() else { - return; - }; - - session.update(cx, |session, cx| { - session.interrupt(cx); - cx.notify(); - }); -} - -pub fn shutdown(editor: WeakEntity, window: &mut Window, cx: &mut App) { - let store = ReplStore::global(cx); - let entity_id = editor.entity_id(); - let Some(session) = store.read(cx).get_session(entity_id).cloned() else { - return; - }; - - session.update(cx, |session, cx| { - session.shutdown(window, cx); - cx.notify(); - }); -} - -pub fn restart(editor: WeakEntity, window: &mut Window, cx: &mut App) { - let Some(editor) = editor.upgrade() else { - return; - }; - - let entity_id = editor.entity_id(); - - let Some(session) = ReplStore::global(cx) - .read(cx) - .get_session(entity_id) - .cloned() - else { - return; - }; - - session.update(cx, |session, cx| { - session.restart(window, cx); - cx.notify(); - }); -} - -pub fn setup_editor_session_actions(editor: &mut Editor, editor_handle: WeakEntity) { - editor - .register_action({ - let editor_handle = editor_handle.clone(); - move |_: &ClearOutputs, _, cx| { - if !JupyterSettings::enabled(cx) { - return; - } - - crate::clear_outputs(editor_handle.clone(), cx); - } - }) - .detach(); - - editor - .register_action({ - let editor_handle = editor_handle.clone(); - move |_: &Interrupt, _, cx| { - if !JupyterSettings::enabled(cx) { - return; - } - - crate::interrupt(editor_handle.clone(), cx); - } - }) - .detach(); - - editor - .register_action({ - let editor_handle = editor_handle.clone(); - move |_: &Shutdown, window, cx| { - if !JupyterSettings::enabled(cx) { - return; - } - - crate::shutdown(editor_handle.clone(), window, cx); - } - }) - .detach(); - - editor - .register_action({ - let editor_handle = editor_handle; - move |_: &Restart, window, cx| { - if !JupyterSettings::enabled(cx) { - return; - } - - crate::restart(editor_handle.clone(), window, cx); - } - }) - .detach(); -} - -fn cell_range(buffer: &BufferSnapshot, start_row: u32, end_row: u32) -> Range { - let mut snippet_end_row = end_row; - while buffer.is_line_blank(snippet_end_row) && snippet_end_row > start_row { - snippet_end_row -= 1; - } - Point::new(start_row, 0)..Point::new(snippet_end_row, buffer.line_len(snippet_end_row)) -} - -// Returns the ranges of the snippets in the buffer and the next point for moving the cursor to -fn jupytext_cells( - buffer: &BufferSnapshot, - range: Range, -) -> (Vec>, Option) { - let mut current_row = range.start.row; - - let Some(language) = buffer.language() else { - return (Vec::new(), None); - }; - - let default_scope = language.default_scope(); - let comment_prefixes = default_scope.line_comment_prefixes(); - if comment_prefixes.is_empty() { - return (Vec::new(), None); - } - - let jupytext_prefixes = comment_prefixes - .iter() - .map(|comment_prefix| format!("{comment_prefix}%%")) - .collect::>(); - - let mut snippet_start_row = None; - loop { - if jupytext_prefixes - .iter() - .any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix)) - { - snippet_start_row = Some(current_row); - break; - } else if current_row > 0 { - current_row -= 1; - } else { - break; - } - } - - let mut snippets = Vec::new(); - if let Some(mut snippet_start_row) = snippet_start_row { - for current_row in range.start.row + 1..=buffer.max_point().row { - if jupytext_prefixes - .iter() - .any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix)) - { - snippets.push(cell_range(buffer, snippet_start_row, current_row - 1)); - - if current_row <= range.end.row { - snippet_start_row = current_row; - } else { - // Return our snippets as well as the next point for moving the cursor to - return (snippets, Some(Point::new(current_row, 0))); - } - } - } - - // Go to the end of the buffer (no more jupytext cells found) - snippets.push(cell_range( - buffer, - snippet_start_row, - buffer.max_point().row, - )); - } - - (snippets, None) -} - -fn runnable_ranges( - buffer: &BufferSnapshot, - range: Range, - cx: &mut App, -) -> (Vec>, Option) { - if let Some(language) = buffer.language() - && language.name() == "Markdown".into() - { - return (markdown_code_blocks(buffer, range, cx), None); - } - - let (jupytext_snippets, next_cursor) = jupytext_cells(buffer, range.clone()); - if !jupytext_snippets.is_empty() { - return (jupytext_snippets, next_cursor); - } - - let snippet_range = cell_range(buffer, range.start.row, range.end.row); - let start_language = buffer.language_at(snippet_range.start); - let end_language = buffer.language_at(snippet_range.end); - - if start_language - .zip(end_language) - .is_some_and(|(start, end)| start == end) - { - (vec![snippet_range], None) - } else { - (Vec::new(), None) - } -} - -// We allow markdown code blocks to end in a trailing newline in order to render the output -// below the final code fence. This is different than our behavior for selections and Jupytext cells. -fn markdown_code_blocks( - buffer: &BufferSnapshot, - range: Range, - cx: &mut App, -) -> Vec> { - buffer - .injections_intersecting_range(range) - .filter(|(_, language)| language_supported(language, cx)) - .map(|(content_range, _)| { - buffer.offset_to_point(content_range.start)..buffer.offset_to_point(content_range.end) - }) - .collect() -} - -fn language_supported(language: &Arc, cx: &mut App) -> bool { - let store = ReplStore::global(cx); - let store_read = store.read(cx); - - // Since we're just checking for general language support, we only need to look at - // the pure Jupyter kernels - these are all the globally available ones - store_read.pure_jupyter_kernel_specifications().any(|spec| { - // Convert to lowercase for case-insensitive comparison since kernels might report "python" while our language is "Python" - spec.language().as_ref().to_lowercase() == language.name().as_ref().to_lowercase() - }) -} - -fn get_language(editor: WeakEntity, cx: &mut App) -> Option> { - editor - .update(cx, |editor, cx| { - let display_snapshot = editor.display_snapshot(cx); - let selection = editor - .selections - .newest::(&display_snapshot); - display_snapshot - .buffer_snapshot() - .language_at(selection.head()) - .cloned() - }) - .ok() - .flatten() -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::App; - use indoc::indoc; - use language::{Buffer, Language, LanguageConfig, LanguageRegistry}; - - #[gpui::test] - fn test_snippet_ranges(cx: &mut App) { - // Create a test language - let test_language = Arc::new(Language::new( - LanguageConfig { - name: "TestLang".into(), - line_comments: vec!["# ".into()], - ..Default::default() - }, - None, - )); - - let buffer = cx.new(|cx| { - Buffer::local( - indoc! { r#" - print(1 + 1) - print(2 + 2) - - print(4 + 4) - - - "# }, - cx, - ) - .with_language(test_language, cx) - }); - let snapshot = buffer.read(cx).snapshot(); - - // Single-point selection - let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4), cx); - let snippets = snippets - .into_iter() - .map(|range| snapshot.text_for_range(range).collect::()) - .collect::>(); - assert_eq!(snippets, vec!["print(1 + 1)"]); - - // Multi-line selection - let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0), cx); - let snippets = snippets - .into_iter() - .map(|range| snapshot.text_for_range(range).collect::()) - .collect::>(); - assert_eq!( - snippets, - vec![indoc! { r#" - print(1 + 1) - print(2 + 2)"# }] - ); - - // Trimming multiple trailing blank lines - let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0), cx); - - let snippets = snippets - .into_iter() - .map(|range| snapshot.text_for_range(range).collect::()) - .collect::>(); - assert_eq!( - snippets, - vec![indoc! { r#" - print(1 + 1) - print(2 + 2) - - print(4 + 4)"# }] - ); - } - - #[gpui::test] - fn test_jupytext_snippet_ranges(cx: &mut App) { - // Create a test language - let test_language = Arc::new(Language::new( - LanguageConfig { - name: "TestLang".into(), - line_comments: vec!["# ".into()], - ..Default::default() - }, - None, - )); - - let buffer = cx.new(|cx| { - Buffer::local( - indoc! { r#" - # Hello! - # %% [markdown] - # This is some arithmetic - print(1 + 1) - print(2 + 2) - - # %% - print(3 + 3) - print(4 + 4) - - print(5 + 5) - - - - "# }, - cx, - ) - .with_language(test_language, cx) - }); - let snapshot = buffer.read(cx).snapshot(); - - // Jupytext snippet surrounding an empty selection - let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5), cx); - - let snippets = snippets - .into_iter() - .map(|range| snapshot.text_for_range(range).collect::()) - .collect::>(); - assert_eq!( - snippets, - vec![indoc! { r#" - # %% [markdown] - # This is some arithmetic - print(1 + 1) - print(2 + 2)"# }] - ); - - // Jupytext snippets intersecting a non-empty selection - let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2), cx); - let snippets = snippets - .into_iter() - .map(|range| snapshot.text_for_range(range).collect::()) - .collect::>(); - assert_eq!( - snippets, - vec![ - indoc! { r#" - # %% [markdown] - # This is some arithmetic - print(1 + 1) - print(2 + 2)"# - }, - indoc! { r#" - # %% - print(3 + 3) - print(4 + 4) - - print(5 + 5)"# - } - ] - ); - } - - #[gpui::test] - fn test_markdown_code_blocks(cx: &mut App) { - use crate::kernels::LocalKernelSpecification; - use jupyter_protocol::JupyterKernelspec; - - // Initialize settings - settings::init(cx); - editor::init(cx); - - // Initialize the ReplStore with a fake filesystem - let fs = Arc::new(project::RealFs::new(None, cx.background_executor().clone())); - ReplStore::init(fs, cx); - - // Add mock kernel specifications for TypeScript and Python - let store = ReplStore::global(cx); - store.update(cx, |store, cx| { - let typescript_spec = KernelSpecification::Jupyter(LocalKernelSpecification { - name: "typescript".into(), - kernelspec: JupyterKernelspec { - argv: vec![], - display_name: "TypeScript".into(), - language: "typescript".into(), - interrupt_mode: None, - metadata: None, - env: None, - }, - path: std::path::PathBuf::new(), - }); - - let python_spec = KernelSpecification::Jupyter(LocalKernelSpecification { - name: "python".into(), - kernelspec: JupyterKernelspec { - argv: vec![], - display_name: "Python".into(), - language: "python".into(), - interrupt_mode: None, - metadata: None, - env: None, - }, - path: std::path::PathBuf::new(), - }); - - store.set_kernel_specs_for_testing(vec![typescript_spec, python_spec], cx); - }); - - let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into()); - let typescript = languages::language( - "typescript", - tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), - ); - let python = languages::language("python", tree_sitter_python::LANGUAGE.into()); - let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone())); - language_registry.add(markdown.clone()); - language_registry.add(typescript); - language_registry.add(python); - - // Two code blocks intersecting with selection - let buffer = cx.new(|cx| { - let mut buffer = Buffer::local( - indoc! { r#" - Hey this is Markdown! - - ```typescript - let foo = 999; - console.log(foo + 1999); - ``` - - ```typescript - console.log("foo") - ``` - "# - }, - cx, - ); - buffer.set_language_registry(language_registry.clone()); - buffer.set_language(Some(markdown.clone()), cx); - buffer - }); - let snapshot = buffer.read(cx).snapshot(); - - let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(8, 5), cx); - let snippets = snippets - .into_iter() - .map(|range| snapshot.text_for_range(range).collect::()) - .collect::>(); - - assert_eq!( - snippets, - vec![ - indoc! { r#" - let foo = 999; - console.log(foo + 1999); - "# - }, - "console.log(\"foo\")\n" - ] - ); - - // Three code blocks intersecting with selection - let buffer = cx.new(|cx| { - let mut buffer = Buffer::local( - indoc! { r#" - Hey this is Markdown! - - ```typescript - let foo = 999; - console.log(foo + 1999); - ``` - - ```ts - console.log("foo") - ``` - - ```typescript - console.log("another code block") - ``` - "# }, - cx, - ); - buffer.set_language_registry(language_registry.clone()); - buffer.set_language(Some(markdown.clone()), cx); - buffer - }); - let snapshot = buffer.read(cx).snapshot(); - - let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(12, 5), cx); - let snippets = snippets - .into_iter() - .map(|range| snapshot.text_for_range(range).collect::()) - .collect::>(); - - assert_eq!( - snippets, - vec![ - indoc! { r#" - let foo = 999; - console.log(foo + 1999); - "# - }, - "console.log(\"foo\")\n", - "console.log(\"another code block\")\n", - ] - ); - - // Python code block - let buffer = cx.new(|cx| { - let mut buffer = Buffer::local( - indoc! { r#" - Hey this is Markdown! - - ```python - print("hello there") - print("hello there") - print("hello there") - ``` - "# }, - cx, - ); - buffer.set_language_registry(language_registry.clone()); - buffer.set_language(Some(markdown.clone()), cx); - buffer - }); - let snapshot = buffer.read(cx).snapshot(); - - let (snippets, _) = runnable_ranges(&snapshot, Point::new(4, 5)..Point::new(5, 5), cx); - let snippets = snippets - .into_iter() - .map(|range| snapshot.text_for_range(range).collect::()) - .collect::>(); - - assert_eq!( - snippets, - vec![indoc! { r#" - print("hello there") - print("hello there") - print("hello there") - "# - },] - ); - } -} diff --git a/crates/repl/src/repl_sessions_ui.rs b/crates/repl/src/repl_sessions_ui.rs deleted file mode 100644 index d8bd8869f2..0000000000 --- a/crates/repl/src/repl_sessions_ui.rs +++ /dev/null @@ -1,282 +0,0 @@ -use editor::Editor; -use gpui::{ - AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, actions, - prelude::*, -}; -use project::ProjectItem as _; -use ui::{ButtonLike, ElevationIndex, KeyBinding, prelude::*}; -use util::ResultExt as _; -use workspace::item::ItemEvent; -use workspace::{Workspace, item::Item}; - -use crate::jupyter_settings::JupyterSettings; -use crate::repl_store::ReplStore; - -actions!( - repl, - [ - /// Runs the current cell and advances to the next one. - Run, - /// Runs the current cell without advancing. - RunInPlace, - /// Clears all outputs in the REPL. - ClearOutputs, - /// Opens the REPL sessions panel. - Sessions, - /// Interrupts the currently running kernel. - Interrupt, - /// Shuts down the current kernel. - Shutdown, - /// Restarts the current kernel. - Restart, - /// Refreshes the list of available kernelspecs. - RefreshKernelspecs - ] -); - -pub fn init(cx: &mut App) { - cx.observe_new( - |workspace: &mut Workspace, _window, _cx: &mut Context| { - workspace.register_action(|workspace, _: &Sessions, window, cx| { - let existing = workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()); - - if let Some(existing) = existing { - workspace.activate_item(&existing, true, true, window, cx); - } else { - let repl_sessions_page = ReplSessionsPage::new(window, cx); - workspace.add_item_to_active_pane( - Box::new(repl_sessions_page), - None, - true, - window, - cx, - ) - } - }); - - workspace.register_action(|_workspace, _: &RefreshKernelspecs, _, cx| { - let store = ReplStore::global(cx); - store.update(cx, |store, cx| { - store.refresh_kernelspecs(cx).detach(); - }); - }); - }, - ) - .detach(); - - cx.observe_new( - move |editor: &mut Editor, window, cx: &mut Context| { - let Some(window) = window else { - return; - }; - - if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() { - return; - } - - cx.defer_in(window, |editor, window, cx| { - let workspace = Workspace::for_window(window, cx); - let project = workspace.map(|workspace| workspace.read(cx).project().clone()); - - let is_local_project = project - .as_ref() - .map(|project| project.read(cx).is_local()) - .unwrap_or(false); - - if !is_local_project { - return; - } - - let buffer = editor.buffer().read(cx).as_singleton(); - - let language = buffer - .as_ref() - .and_then(|buffer| buffer.read(cx).language()); - - let project_path = buffer.and_then(|buffer| buffer.read(cx).project_path(cx)); - - let editor_handle = cx.entity().downgrade(); - - if let Some(language) = language - && language.name() == "Python".into() - && let (Some(project_path), Some(project)) = (project_path, project) - { - let store = ReplStore::global(cx); - store.update(cx, |store, cx| { - store - .refresh_python_kernelspecs(project_path.worktree_id, &project, cx) - .detach_and_log_err(cx); - }); - } - - editor - .register_action({ - let editor_handle = editor_handle.clone(); - move |_: &Run, window, cx| { - if !JupyterSettings::enabled(cx) { - return; - } - - crate::run(editor_handle.clone(), true, window, cx).log_err(); - } - }) - .detach(); - - editor - .register_action({ - move |_: &RunInPlace, window, cx| { - if !JupyterSettings::enabled(cx) { - return; - } - - crate::run(editor_handle.clone(), false, window, cx).log_err(); - } - }) - .detach(); - }); - }, - ) - .detach(); -} - -pub struct ReplSessionsPage { - focus_handle: FocusHandle, - _subscriptions: Vec, -} - -impl ReplSessionsPage { - pub fn new(window: &mut Window, cx: &mut Context) -> Entity { - cx.new(|cx| { - let focus_handle = cx.focus_handle(); - - let subscriptions = vec![ - cx.on_focus_in(&focus_handle, window, |_this, _window, cx| cx.notify()), - cx.on_focus_out(&focus_handle, window, |_this, _event, _window, cx| { - cx.notify() - }), - ]; - - Self { - focus_handle, - _subscriptions: subscriptions, - } - }) - } -} - -impl EventEmitter for ReplSessionsPage {} - -impl Focusable for ReplSessionsPage { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Item for ReplSessionsPage { - type Event = ItemEvent; - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "REPL Sessions".into() - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - Some("REPL Session Started") - } - - fn show_toolbar(&self) -> bool { - false - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { - f(*event) - } -} - -impl Render for ReplSessionsPage { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let store = ReplStore::global(cx); - - let (kernel_specifications, sessions) = store.update(cx, |store, _cx| { - ( - store - .pure_jupyter_kernel_specifications() - .cloned() - .collect::>(), - store.sessions().cloned().collect::>(), - ) - }); - - // When there are no kernel specifications, show a link to the Zed docs explaining how to - // install kernels. It can be assumed they don't have a running kernel if we have no - // specifications. - if kernel_specifications.is_empty() { - let instructions = "To start interactively running code in your editor, you need to install and configure Jupyter kernels."; - - return ReplSessionsContainer::new("No Jupyter Kernels Available") - .child(Label::new(instructions)) - .child( - h_flex().w_full().p_4().justify_center().gap_2().child( - ButtonLike::new("install-kernels") - .style(ButtonStyle::Filled) - .size(ButtonSize::Large) - .layer(ElevationIndex::ModalSurface) - .child(Label::new("Install Kernels")) - .on_click(move |_, _, cx| { - cx.open_url( - "https://zed.dev/docs/repl#language-specific-instructions", - ) - }), - ), - ); - } - - // When there are no sessions, show the command to run code in an editor - if sessions.is_empty() { - let instructions = "To run code in a Jupyter kernel, select some code and use the 'repl::Run' command."; - - return ReplSessionsContainer::new("No Jupyter Kernel Sessions").child( - v_flex() - .child(Label::new(instructions)) - .child(KeyBinding::for_action(&Run, cx)), - ); - } - - ReplSessionsContainer::new("Jupyter Kernel Sessions").children(sessions) - } -} - -#[derive(IntoElement)] -struct ReplSessionsContainer { - title: SharedString, - children: Vec, -} - -impl ReplSessionsContainer { - pub fn new(title: impl Into) -> Self { - Self { - title: title.into(), - children: Vec::new(), - } - } -} - -impl ParentElement for ReplSessionsContainer { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for ReplSessionsContainer { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - v_flex() - .p_4() - .gap_2() - .size_full() - .child(Label::new(self.title).size(LabelSize::Large)) - .children(self.children) - } -} diff --git a/crates/repl/src/repl_settings.rs b/crates/repl/src/repl_settings.rs deleted file mode 100644 index 9faed72e55..0000000000 --- a/crates/repl/src/repl_settings.rs +++ /dev/null @@ -1,27 +0,0 @@ -use settings::{RegisterSetting, Settings}; - -/// Settings for configuring REPL display and behavior. -#[derive(Clone, Debug, RegisterSetting)] -pub struct ReplSettings { - /// Maximum number of lines to keep in REPL's scrollback buffer. - /// Clamped with [4, 256] range. - /// - /// Default: 32 - pub max_lines: usize, - /// Maximum number of columns to keep in REPL's scrollback buffer. - /// Clamped with [20, 512] range. - /// - /// Default: 128 - pub max_columns: usize, -} - -impl Settings for ReplSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let repl = content.repl.as_ref().unwrap(); - - Self { - max_lines: repl.max_lines.unwrap(), - max_columns: repl.max_columns.unwrap(), - } - } -} diff --git a/crates/repl/src/repl_store.rs b/crates/repl/src/repl_store.rs deleted file mode 100644 index a5dc7b6c7b..0000000000 --- a/crates/repl/src/repl_store.rs +++ /dev/null @@ -1,292 +0,0 @@ -use std::sync::Arc; - -use anyhow::{Context as _, Result}; -use collections::HashMap; -use command_palette_hooks::CommandPaletteFilter; -use gpui::{App, Context, Entity, EntityId, Global, Subscription, Task, prelude::*}; -use jupyter_websocket_client::RemoteServer; -use language::Language; -use project::{Fs, Project, WorktreeId}; -use settings::{Settings, SettingsStore}; - -use crate::kernels::{ - list_remote_kernelspecs, local_kernel_specifications, python_env_kernel_specifications, -}; -use crate::{JupyterSettings, KernelSpecification, Session}; - -struct GlobalReplStore(Entity); - -impl Global for GlobalReplStore {} - -pub struct ReplStore { - fs: Arc, - enabled: bool, - sessions: HashMap>, - kernel_specifications: Vec, - selected_kernel_for_worktree: HashMap, - kernel_specifications_for_worktree: HashMap>, - _subscriptions: Vec, -} - -impl ReplStore { - const NAMESPACE: &'static str = "repl"; - - pub(crate) fn init(fs: Arc, cx: &mut App) { - let store = cx.new(move |cx| Self::new(fs, cx)); - - store - .update(cx, |store, cx| store.refresh_kernelspecs(cx)) - .detach_and_log_err(cx); - - cx.set_global(GlobalReplStore(store)) - } - - pub fn global(cx: &App) -> Entity { - cx.global::().0.clone() - } - - pub fn new(fs: Arc, cx: &mut Context) -> Self { - let subscriptions = vec![cx.observe_global::(move |this, cx| { - this.set_enabled(JupyterSettings::enabled(cx), cx); - })]; - - let this = Self { - fs, - enabled: JupyterSettings::enabled(cx), - sessions: HashMap::default(), - kernel_specifications: Vec::new(), - _subscriptions: subscriptions, - kernel_specifications_for_worktree: HashMap::default(), - selected_kernel_for_worktree: HashMap::default(), - }; - this.on_enabled_changed(cx); - this - } - - pub fn fs(&self) -> &Arc { - &self.fs - } - - pub fn is_enabled(&self) -> bool { - self.enabled - } - - pub fn kernel_specifications_for_worktree( - &self, - worktree_id: WorktreeId, - ) -> impl Iterator { - self.kernel_specifications_for_worktree - .get(&worktree_id) - .into_iter() - .flat_map(|specs| specs.iter()) - .chain(self.kernel_specifications.iter()) - } - - pub fn pure_jupyter_kernel_specifications(&self) -> impl Iterator { - self.kernel_specifications.iter() - } - - pub fn sessions(&self) -> impl Iterator> { - self.sessions.values() - } - - fn set_enabled(&mut self, enabled: bool, cx: &mut Context) { - if self.enabled == enabled { - return; - } - - self.enabled = enabled; - self.on_enabled_changed(cx); - } - - fn on_enabled_changed(&self, cx: &mut Context) { - if !self.enabled { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.hide_namespace(Self::NAMESPACE); - }); - - return; - } - - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.show_namespace(Self::NAMESPACE); - }); - - cx.notify(); - } - - pub fn refresh_python_kernelspecs( - &mut self, - worktree_id: WorktreeId, - project: &Entity, - cx: &mut Context, - ) -> Task> { - let kernel_specifications = python_env_kernel_specifications(project, worktree_id, cx); - cx.spawn(async move |this, cx| { - let kernel_specifications = kernel_specifications - .await - .context("getting python kernelspecs")?; - - this.update(cx, |this, cx| { - this.kernel_specifications_for_worktree - .insert(worktree_id, kernel_specifications); - cx.notify(); - }) - }) - } - - fn get_remote_kernel_specifications( - &self, - cx: &mut Context, - ) -> Option>>> { - match ( - std::env::var("JUPYTER_SERVER"), - std::env::var("JUPYTER_TOKEN"), - ) { - (Ok(server), Ok(token)) => { - let remote_server = RemoteServer { - base_url: server, - token, - }; - let http_client = cx.http_client(); - Some(cx.spawn(async move |_, _| { - list_remote_kernelspecs(remote_server, http_client) - .await - .map(|specs| specs.into_iter().map(KernelSpecification::Remote).collect()) - })) - } - _ => None, - } - } - - pub fn refresh_kernelspecs(&mut self, cx: &mut Context) -> Task> { - let local_kernel_specifications = local_kernel_specifications(self.fs.clone()); - - let remote_kernel_specifications = self.get_remote_kernel_specifications(cx); - - let all_specs = cx.background_spawn(async move { - let mut all_specs = local_kernel_specifications - .await? - .into_iter() - .map(KernelSpecification::Jupyter) - .collect::>(); - - if let Some(remote_task) = remote_kernel_specifications - && let Ok(remote_specs) = remote_task.await - { - all_specs.extend(remote_specs); - } - - anyhow::Ok(all_specs) - }); - - cx.spawn(async move |this, cx| { - let all_specs = all_specs.await; - - if let Ok(specs) = all_specs { - this.update(cx, |this, cx| { - this.kernel_specifications = specs; - cx.notify(); - }) - .ok(); - } - - anyhow::Ok(()) - }) - } - - pub fn set_active_kernelspec( - &mut self, - worktree_id: WorktreeId, - kernelspec: KernelSpecification, - _cx: &mut Context, - ) { - self.selected_kernel_for_worktree - .insert(worktree_id, kernelspec); - } - - pub fn active_kernelspec( - &self, - worktree_id: WorktreeId, - language_at_cursor: Option>, - cx: &App, - ) -> Option { - let selected_kernelspec = self.selected_kernel_for_worktree.get(&worktree_id).cloned(); - - if let Some(language_at_cursor) = language_at_cursor { - selected_kernelspec.or_else(|| { - self.kernelspec_legacy_by_lang_only(worktree_id, language_at_cursor, cx) - }) - } else { - selected_kernelspec - } - } - - fn kernelspec_legacy_by_lang_only( - &self, - worktree_id: WorktreeId, - language_at_cursor: Arc, - cx: &App, - ) -> Option { - let settings = JupyterSettings::get_global(cx); - let selected_kernel = settings - .kernel_selections - .get(language_at_cursor.code_fence_block_name().as_ref()); - - let found_by_name = self - .kernel_specifications_for_worktree(worktree_id) - .find(|runtime_specification| { - if let (Some(selected), KernelSpecification::Jupyter(runtime_specification)) = - (selected_kernel, runtime_specification) - { - // Top priority is the selected kernel - return runtime_specification.name.to_lowercase() == selected.to_lowercase(); - } - false - }) - .cloned(); - - if let Some(found_by_name) = found_by_name { - return Some(found_by_name); - } - - self.kernel_specifications_for_worktree(worktree_id) - .find(|kernel_option| match kernel_option { - KernelSpecification::Jupyter(runtime_specification) => { - runtime_specification.kernelspec.language.to_lowercase() - == language_at_cursor.code_fence_block_name().to_lowercase() - } - KernelSpecification::PythonEnv(runtime_specification) => { - runtime_specification.kernelspec.language.to_lowercase() - == language_at_cursor.code_fence_block_name().to_lowercase() - } - KernelSpecification::Remote(remote_spec) => { - remote_spec.kernelspec.language.to_lowercase() - == language_at_cursor.code_fence_block_name().to_lowercase() - } - }) - .cloned() - } - - pub fn get_session(&self, entity_id: EntityId) -> Option<&Entity> { - self.sessions.get(&entity_id) - } - - pub fn insert_session(&mut self, entity_id: EntityId, session: Entity) { - self.sessions.insert(entity_id, session); - } - - pub fn remove_session(&mut self, entity_id: EntityId) { - self.sessions.remove(&entity_id); - } - - #[cfg(test)] - pub fn set_kernel_specs_for_testing( - &mut self, - specs: Vec, - cx: &mut Context, - ) { - self.kernel_specifications = specs; - cx.notify(); - } -} diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs deleted file mode 100644 index 1fa0bfec35..0000000000 --- a/crates/repl/src/session.rs +++ /dev/null @@ -1,702 +0,0 @@ -use crate::components::KernelListItem; -use crate::kernels::RemoteRunningKernel; -use crate::setup_editor_session_actions; -use crate::{ - KernelStatus, - kernels::{Kernel, KernelSpecification, NativeRunningKernel}, - outputs::{ExecutionStatus, ExecutionView}, -}; -use anyhow::Context as _; -use collections::{HashMap, HashSet}; -use editor::SelectionEffects; -use editor::{ - Anchor, AnchorRangeExt as _, Editor, MultiBuffer, ToPoint, - display_map::{ - BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, - RenderBlock, - }, - scroll::Autoscroll, -}; -use futures::FutureExt as _; -use gpui::{ - Context, Entity, EventEmitter, Render, Subscription, Task, WeakEntity, Window, div, prelude::*, -}; -use language::Point; -use project::Fs; -use runtimelib::{ - ExecuteRequest, ExecutionState, InterruptRequest, JupyterMessage, JupyterMessageContent, - ShutdownRequest, -}; -use std::{env::temp_dir, ops::Range, sync::Arc, time::Duration}; -use theme::ActiveTheme; -use ui::{IconButtonShape, Tooltip, prelude::*}; -use util::ResultExt as _; - -pub struct Session { - fs: Arc, - editor: WeakEntity, - pub kernel: Kernel, - blocks: HashMap, - pub kernel_specification: KernelSpecification, - _buffer_subscription: Subscription, -} - -struct EditorBlock { - code_range: Range, - invalidation_anchor: Anchor, - block_id: CustomBlockId, - execution_view: Entity, -} - -type CloseBlockFn = - Arc Fn(CustomBlockId, &'a mut Window, &mut App) + Send + Sync + 'static>; - -impl EditorBlock { - fn new( - editor: WeakEntity, - code_range: Range, - status: ExecutionStatus, - on_close: CloseBlockFn, - cx: &mut Context, - ) -> anyhow::Result { - let editor = editor.upgrade().context("editor is not open")?; - let workspace = editor.read(cx).workspace().context("workspace dropped")?; - - let execution_view = cx.new(|cx| ExecutionView::new(status, workspace.downgrade(), cx)); - - let (block_id, invalidation_anchor) = editor.update(cx, |editor, cx| { - let buffer = editor.buffer().clone(); - let buffer_snapshot = buffer.read(cx).snapshot(cx); - let end_point = code_range.end.to_point(&buffer_snapshot); - let next_row_start = end_point + Point::new(1, 0); - if next_row_start > buffer_snapshot.max_point() { - buffer.update(cx, |buffer, cx| { - buffer.edit( - [( - buffer_snapshot.max_point()..buffer_snapshot.max_point(), - "\n", - )], - None, - cx, - ) - }); - } - - let invalidation_anchor = buffer.read(cx).read(cx).anchor_before(next_row_start); - let block = BlockProperties { - placement: BlockPlacement::Below(code_range.end), - // Take up at least one height for status, allow the editor to determine the real height based on the content from render - height: Some(1), - style: BlockStyle::Sticky, - render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()), - priority: 0, - }; - - let block_id = editor.insert_blocks([block], None, cx)[0]; - (block_id, invalidation_anchor) - }); - - anyhow::Ok(Self { - code_range, - invalidation_anchor, - block_id, - execution_view, - }) - } - - fn handle_message( - &mut self, - message: &JupyterMessage, - window: &mut Window, - cx: &mut Context, - ) { - self.execution_view.update(cx, |execution_view, cx| { - execution_view.push_message(&message.content, window, cx); - }); - } - - fn create_output_area_renderer( - execution_view: Entity, - on_close: CloseBlockFn, - ) -> RenderBlock { - Arc::new(move |cx: &mut BlockContext| { - let execution_view = execution_view.clone(); - let text_style = crate::outputs::plain::text_style(cx.window, cx.app); - - let editor_margins = cx.margins; - let gutter = editor_margins.gutter; - - let block_id = cx.block_id; - let on_close = on_close.clone(); - - let rem_size = cx.window.rem_size(); - - let text_line_height = text_style.line_height_in_pixels(rem_size); - - let close_button = h_flex() - .flex_none() - .items_center() - .justify_center() - .absolute() - .top(text_line_height / 2.) - .right( - // 2px is a magic number to nudge the button just a bit closer to - // the line number start - gutter.full_width() / 2.0 - text_line_height / 2.0 - px(2.), - ) - .w(text_line_height) - .h(text_line_height) - .child( - IconButton::new("close_output_area", IconName::Close) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .size(ButtonSize::Compact) - .shape(IconButtonShape::Square) - .tooltip(Tooltip::text("Close output area")) - .on_click(move |_, window, cx| { - if let BlockId::Custom(block_id) = block_id { - (on_close)(block_id, window, cx) - } - }), - ); - - div() - .id(cx.block_id) - .block_mouse_except_scroll() - .flex() - .items_start() - .min_h(text_line_height) - .w_full() - .border_y_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - div() - .relative() - .w(gutter.full_width()) - .h(text_line_height * 2) - .child(close_button), - ) - .child( - div() - .flex_1() - .size_full() - .py(text_line_height / 2.) - .mr(editor_margins.right) - .pr_2() - .child(execution_view), - ) - .into_any_element() - }) - } -} - -impl Session { - pub fn new( - editor: WeakEntity, - fs: Arc, - kernel_specification: KernelSpecification, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let subscription = match editor.upgrade() { - Some(editor) => { - let buffer = editor.read(cx).buffer().clone(); - cx.subscribe(&buffer, Self::on_buffer_event) - } - None => Subscription::new(|| {}), - }; - - let editor_handle = editor.clone(); - - editor - .update(cx, |editor, _cx| { - setup_editor_session_actions(editor, editor_handle); - }) - .ok(); - - let mut session = Self { - fs, - editor, - kernel: Kernel::StartingKernel(Task::ready(()).shared()), - blocks: HashMap::default(), - kernel_specification, - _buffer_subscription: subscription, - }; - - session.start_kernel(window, cx); - session - } - - fn start_kernel(&mut self, window: &mut Window, cx: &mut Context) { - let kernel_language = self.kernel_specification.language(); - let entity_id = self.editor.entity_id(); - let working_directory = self - .editor - .upgrade() - .and_then(|editor| editor.read(cx).working_directory(cx)) - .unwrap_or_else(temp_dir); - - telemetry::event!( - "Kernel Status Changed", - kernel_language, - kernel_status = KernelStatus::Starting.to_string(), - repl_session_id = cx.entity_id().to_string(), - ); - - let session_view = cx.entity(); - - let kernel = match self.kernel_specification.clone() { - KernelSpecification::Jupyter(kernel_specification) - | KernelSpecification::PythonEnv(kernel_specification) => NativeRunningKernel::new( - kernel_specification, - entity_id, - working_directory, - self.fs.clone(), - session_view, - window, - cx, - ), - KernelSpecification::Remote(remote_kernel_specification) => RemoteRunningKernel::new( - remote_kernel_specification, - working_directory, - session_view, - window, - cx, - ), - }; - - let pending_kernel = cx - .spawn(async move |this, cx| { - let kernel = kernel.await; - - match kernel { - Ok(kernel) => { - this.update(cx, |session, cx| { - session.kernel(Kernel::RunningKernel(kernel), cx); - }) - .ok(); - } - Err(err) => { - this.update(cx, |session, cx| { - session.kernel_errored(err.to_string(), cx); - }) - .ok(); - } - } - }) - .shared(); - - self.kernel(Kernel::StartingKernel(pending_kernel), cx); - cx.notify(); - } - - pub fn kernel_errored(&mut self, error_message: String, cx: &mut Context) { - self.kernel(Kernel::ErroredLaunch(error_message.clone()), cx); - - self.blocks.values().for_each(|block| { - block.execution_view.update(cx, |execution_view, cx| { - match execution_view.status { - ExecutionStatus::Finished => { - // Do nothing when the output was good - } - _ => { - // All other cases, set the status to errored - execution_view.status = - ExecutionStatus::KernelErrored(error_message.clone()) - } - } - cx.notify(); - }); - }); - } - - fn on_buffer_event( - &mut self, - buffer: Entity, - event: &multi_buffer::Event, - cx: &mut Context, - ) { - if let multi_buffer::Event::Edited { .. } = event { - let snapshot = buffer.read(cx).snapshot(cx); - - let mut blocks_to_remove: HashSet = HashSet::default(); - - self.blocks.retain(|_id, block| { - if block.invalidation_anchor.is_valid(&snapshot) { - true - } else { - blocks_to_remove.insert(block.block_id); - false - } - }); - - if !blocks_to_remove.is_empty() { - self.editor - .update(cx, |editor, cx| { - editor.remove_blocks(blocks_to_remove, None, cx); - }) - .ok(); - cx.notify(); - } - } - } - - fn send(&mut self, message: JupyterMessage, _cx: &mut Context) -> anyhow::Result<()> { - if let Kernel::RunningKernel(kernel) = &mut self.kernel { - kernel.request_tx().try_send(message).ok(); - } - - anyhow::Ok(()) - } - - pub fn clear_outputs(&mut self, cx: &mut Context) { - let blocks_to_remove: HashSet = - self.blocks.values().map(|block| block.block_id).collect(); - - self.editor - .update(cx, |editor, cx| { - editor.remove_blocks(blocks_to_remove, None, cx); - }) - .ok(); - - self.blocks.clear(); - } - - pub fn execute( - &mut self, - code: String, - anchor_range: Range, - next_cell: Option, - move_down: bool, - window: &mut Window, - cx: &mut Context, - ) { - let Some(editor) = self.editor.upgrade() else { - return; - }; - - if code.is_empty() { - return; - } - - let execute_request = ExecuteRequest { - code, - ..ExecuteRequest::default() - }; - - let message: JupyterMessage = execute_request.into(); - - let mut blocks_to_remove: HashSet = HashSet::default(); - - let buffer = editor.read(cx).buffer().read(cx).snapshot(cx); - - self.blocks.retain(|_key, block| { - if anchor_range.overlaps(&block.code_range, &buffer) { - blocks_to_remove.insert(block.block_id); - false - } else { - true - } - }); - - self.editor - .update(cx, |editor, cx| { - editor.remove_blocks(blocks_to_remove, None, cx); - }) - .ok(); - - let status = match &self.kernel { - Kernel::Restarting => ExecutionStatus::Restarting, - Kernel::RunningKernel(_) => ExecutionStatus::Queued, - Kernel::StartingKernel(_) => ExecutionStatus::ConnectingToKernel, - Kernel::ErroredLaunch(error) => ExecutionStatus::KernelErrored(error.clone()), - Kernel::ShuttingDown => ExecutionStatus::ShuttingDown, - Kernel::Shutdown => ExecutionStatus::Shutdown, - }; - - let parent_message_id = message.header.msg_id.clone(); - let session_view = cx.entity().downgrade(); - let weak_editor = self.editor.clone(); - - let on_close: CloseBlockFn = Arc::new( - move |block_id: CustomBlockId, _: &mut Window, cx: &mut App| { - if let Some(session) = session_view.upgrade() { - session.update(cx, |session, cx| { - session.blocks.remove(&parent_message_id); - cx.notify(); - }); - } - - if let Some(editor) = weak_editor.upgrade() { - editor.update(cx, |editor, cx| { - let mut block_ids = HashSet::default(); - block_ids.insert(block_id); - editor.remove_blocks(block_ids, None, cx); - }); - } - }, - ); - - let Ok(editor_block) = - EditorBlock::new(self.editor.clone(), anchor_range, status, on_close, cx) - else { - return; - }; - - let new_cursor_pos = if let Some(next_cursor) = next_cell { - next_cursor - } else { - editor_block.invalidation_anchor - }; - - self.blocks - .insert(message.header.msg_id.clone(), editor_block); - - match &self.kernel { - Kernel::RunningKernel(_) => { - self.send(message, cx).ok(); - } - Kernel::StartingKernel(task) => { - // Queue up the execution as a task to run after the kernel starts - let task = task.clone(); - - cx.spawn(async move |this, cx| { - task.await; - this.update(cx, |session, cx| { - session.send(message, cx).ok(); - }) - .ok(); - }) - .detach(); - } - _ => {} - } - - if move_down { - editor.update(cx, move |editor, cx| { - editor.change_selections( - SelectionEffects::scroll(Autoscroll::top_relative(8)), - window, - cx, - |selections| { - selections.select_ranges([new_cursor_pos..new_cursor_pos]); - }, - ); - }); - } - } - - pub fn route(&mut self, message: &JupyterMessage, window: &mut Window, cx: &mut Context) { - let parent_message_id = match message.parent_header.as_ref() { - Some(header) => &header.msg_id, - None => return, - }; - - match &message.content { - JupyterMessageContent::Status(status) => { - self.kernel.set_execution_state(&status.execution_state); - - telemetry::event!( - "Kernel Status Changed", - kernel_language = self.kernel_specification.language(), - kernel_status = KernelStatus::from(&self.kernel).to_string(), - repl_session_id = cx.entity_id().to_string(), - ); - - cx.notify(); - } - JupyterMessageContent::KernelInfoReply(reply) => { - self.kernel.set_kernel_info(reply); - cx.notify(); - } - JupyterMessageContent::UpdateDisplayData(update) => { - let display_id = if let Some(display_id) = update.transient.display_id.clone() { - display_id - } else { - return; - }; - - self.blocks.iter_mut().for_each(|(_, block)| { - block.execution_view.update(cx, |execution_view, cx| { - execution_view.update_display_data(&update.data, &display_id, window, cx); - }); - }); - return; - } - _ => {} - } - - if let Some(block) = self.blocks.get_mut(parent_message_id) { - block.handle_message(message, window, cx); - } - } - - pub fn interrupt(&mut self, cx: &mut Context) { - match &mut self.kernel { - Kernel::RunningKernel(_kernel) => { - self.send(InterruptRequest {}.into(), cx).ok(); - } - Kernel::StartingKernel(_task) => { - // NOTE: If we switch to a literal queue instead of chaining on to the task, clear all queued executions - } - _ => {} - } - } - - pub fn kernel(&mut self, kernel: Kernel, cx: &mut Context) { - if let Kernel::Shutdown = kernel { - cx.emit(SessionEvent::Shutdown(self.editor.clone())); - } - - let kernel_status = KernelStatus::from(&kernel).to_string(); - let kernel_language = self.kernel_specification.language(); - - telemetry::event!( - "Kernel Status Changed", - kernel_language, - kernel_status, - repl_session_id = cx.entity_id().to_string(), - ); - - self.kernel = kernel; - } - - pub fn shutdown(&mut self, window: &mut Window, cx: &mut Context) { - let kernel = std::mem::replace(&mut self.kernel, Kernel::ShuttingDown); - - match kernel { - Kernel::RunningKernel(mut kernel) => { - let mut request_tx = kernel.request_tx(); - - let forced = kernel.force_shutdown(window, cx); - - cx.spawn(async move |this, cx| { - let message: JupyterMessage = ShutdownRequest { restart: false }.into(); - request_tx.try_send(message).ok(); - - forced.await.log_err(); - - // Give the kernel a bit of time to clean up - cx.background_executor().timer(Duration::from_secs(3)).await; - - this.update(cx, |session, cx| { - session.clear_outputs(cx); - session.kernel(Kernel::Shutdown, cx); - cx.notify(); - }) - .ok(); - }) - .detach(); - } - _ => { - self.kernel(Kernel::Shutdown, cx); - } - } - cx.notify(); - } - - pub fn restart(&mut self, window: &mut Window, cx: &mut Context) { - let kernel = std::mem::replace(&mut self.kernel, Kernel::Restarting); - - match kernel { - Kernel::Restarting => { - // Do nothing if already restarting - } - Kernel::RunningKernel(mut kernel) => { - let mut request_tx = kernel.request_tx(); - - let forced = kernel.force_shutdown(window, cx); - - cx.spawn_in(window, async move |this, cx| { - // Send shutdown request with restart flag - log::debug!("restarting kernel"); - let message: JupyterMessage = ShutdownRequest { restart: true }.into(); - request_tx.try_send(message).ok(); - - // Wait for kernel to shutdown - cx.background_executor().timer(Duration::from_secs(1)).await; - - // Force kill the kernel if it hasn't shut down - forced.await.log_err(); - - // Start a new kernel - this.update_in(cx, |session, window, cx| { - // TODO: Differentiate between restart and restart+clear-outputs - session.clear_outputs(cx); - session.start_kernel(window, cx); - }) - .ok(); - }) - .detach(); - } - _ => { - self.clear_outputs(cx); - self.start_kernel(window, cx); - } - } - cx.notify(); - } -} - -pub enum SessionEvent { - Shutdown(WeakEntity), -} - -impl EventEmitter for Session {} - -impl Render for Session { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let (status_text, interrupt_button) = match &self.kernel { - Kernel::RunningKernel(kernel) => ( - kernel - .kernel_info() - .as_ref() - .map(|info| info.language_info.name.clone()), - Some( - Button::new("interrupt", "Interrupt") - .style(ButtonStyle::Subtle) - .on_click(cx.listener(move |session, _, _, cx| { - session.interrupt(cx); - })), - ), - ), - Kernel::StartingKernel(_) => (Some("Starting".into()), None), - Kernel::ErroredLaunch(err) => (Some(format!("Error: {err}")), None), - Kernel::ShuttingDown => (Some("Shutting Down".into()), None), - Kernel::Shutdown => (Some("Shutdown".into()), None), - Kernel::Restarting => (Some("Restarting".into()), None), - }; - - KernelListItem::new(self.kernel_specification.clone()) - .status_color(match &self.kernel { - Kernel::RunningKernel(kernel) => match kernel.execution_state() { - ExecutionState::Idle => Color::Success, - ExecutionState::Busy => Color::Modified, - ExecutionState::Unknown => Color::Modified, - ExecutionState::Starting => Color::Modified, - ExecutionState::Restarting => Color::Modified, - ExecutionState::Terminating => Color::Disabled, - ExecutionState::AutoRestarting => Color::Modified, - ExecutionState::Dead => Color::Disabled, - ExecutionState::Other(_) => Color::Modified, - }, - Kernel::StartingKernel(_) => Color::Modified, - Kernel::ErroredLaunch(_) => Color::Error, - Kernel::ShuttingDown => Color::Modified, - Kernel::Shutdown => Color::Disabled, - Kernel::Restarting => Color::Modified, - }) - .child(Label::new(self.kernel_specification.name())) - .children(status_text.map(|status_text| Label::new(format!("({status_text})")))) - .button( - Button::new("shutdown", "Shutdown") - .style(ButtonStyle::Subtle) - .disabled(self.kernel.is_shutting_down()) - .on_click(cx.listener(move |session, _, window, cx| { - session.shutdown(window, cx); - })), - ) - .buttons(interrupt_button) - } -} diff --git a/crates/rich_text/Cargo.toml b/crates/rich_text/Cargo.toml deleted file mode 100644 index 17bd8d2a4b..0000000000 --- a/crates/rich_text/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "rich_text" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/rich_text.rs" -doctest = false - -[features] -test-support = [ - "gpui/test-support", - "util/test-support", -] - -[dependencies] -futures.workspace = true -gpui.workspace = true -language.workspace = true -linkify.workspace = true -pulldown-cmark.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true diff --git a/crates/rich_text/LICENSE-GPL b/crates/rich_text/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/rich_text/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/rich_text/src/rich_text.rs b/crates/rich_text/src/rich_text.rs deleted file mode 100644 index 2af9988f03..0000000000 --- a/crates/rich_text/src/rich_text.rs +++ /dev/null @@ -1,418 +0,0 @@ -use futures::FutureExt; -use gpui::{ - AnyElement, AnyView, App, ElementId, FontStyle, FontWeight, HighlightStyle, InteractiveText, - IntoElement, SharedString, StrikethroughStyle, StyledText, UnderlineStyle, Window, -}; -use language::{HighlightId, Language, LanguageRegistry}; -use std::{ops::Range, sync::Arc}; -use theme::ActiveTheme; -use ui::LinkPreview; -use util::RangeExt; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Highlight { - Code, - Id(HighlightId), - InlineCode(bool), - Highlight(HighlightStyle), - Mention, - SelfMention, -} - -impl From for Highlight { - fn from(style: HighlightStyle) -> Self { - Self::Highlight(style) - } -} - -impl From for Highlight { - fn from(style: HighlightId) -> Self { - Self::Id(style) - } -} - -#[derive(Clone, Default)] -pub struct RichText { - pub text: SharedString, - pub highlights: Vec<(Range, Highlight)>, - pub link_ranges: Vec>, - pub link_urls: Arc<[String]>, - - pub custom_ranges: Vec>, - custom_ranges_tooltip_fn: - Option, &mut Window, &mut App) -> Option>>, -} - -/// Allows one to specify extra links to the rendered markdown, which can be used -/// for e.g. mentions. -#[derive(Debug)] -pub struct Mention { - pub range: Range, - pub is_self_mention: bool, -} - -impl RichText { - pub fn new( - block: String, - mentions: &[Mention], - language_registry: &Arc, - ) -> Self { - let mut text = String::new(); - let mut highlights = Vec::new(); - let mut link_ranges = Vec::new(); - let mut link_urls = Vec::new(); - render_markdown_mut( - &block, - mentions, - language_registry, - None, - &mut text, - &mut highlights, - &mut link_ranges, - &mut link_urls, - ); - text.truncate(text.trim_end().len()); - - RichText { - text: SharedString::from(text), - link_urls: link_urls.into(), - link_ranges, - highlights, - custom_ranges: Vec::new(), - custom_ranges_tooltip_fn: None, - } - } - - pub fn set_tooltip_builder_for_custom_ranges( - &mut self, - f: impl Fn(usize, Range, &mut Window, &mut App) -> Option + 'static, - ) { - self.custom_ranges_tooltip_fn = Some(Arc::new(f)); - } - - pub fn element(&self, id: ElementId, window: &mut Window, cx: &mut App) -> AnyElement { - let theme = cx.theme(); - let code_background = theme.colors().surface_background; - - InteractiveText::new( - id, - StyledText::new(self.text.clone()).with_default_highlights( - &window.text_style(), - self.highlights.iter().map(|(range, highlight)| { - ( - range.clone(), - match highlight { - Highlight::Code => HighlightStyle { - background_color: Some(code_background), - ..Default::default() - }, - Highlight::Id(id) => HighlightStyle { - background_color: Some(code_background), - ..id.style(theme.syntax()).unwrap_or_default() - }, - Highlight::InlineCode(link) => { - if *link { - HighlightStyle { - background_color: Some(code_background), - underline: Some(UnderlineStyle { - thickness: 1.0.into(), - ..Default::default() - }), - ..Default::default() - } - } else { - HighlightStyle { - background_color: Some(code_background), - ..Default::default() - } - } - } - Highlight::Highlight(highlight) => *highlight, - Highlight::Mention => HighlightStyle { - font_weight: Some(FontWeight::BOLD), - ..Default::default() - }, - Highlight::SelfMention => HighlightStyle { - font_weight: Some(FontWeight::BOLD), - ..Default::default() - }, - }, - ) - }), - ), - ) - .on_click(self.link_ranges.clone(), { - let link_urls = self.link_urls.clone(); - move |ix, _, cx| { - let url = &link_urls[ix]; - if url.starts_with("http") { - cx.open_url(url); - } - } - }) - .tooltip({ - let link_ranges = self.link_ranges.clone(); - let link_urls = self.link_urls.clone(); - let custom_tooltip_ranges = self.custom_ranges.clone(); - let custom_tooltip_fn = self.custom_ranges_tooltip_fn.clone(); - move |idx, window, cx| { - for (ix, range) in link_ranges.iter().enumerate() { - if range.contains(&idx) { - return Some(LinkPreview::new(&link_urls[ix], cx)); - } - } - for range in &custom_tooltip_ranges { - if range.contains(&idx) - && let Some(f) = &custom_tooltip_fn - { - return f(idx, range.clone(), window, cx); - } - } - None - } - }) - .into_any_element() - } -} - -pub fn render_markdown_mut( - block: &str, - mut mentions: &[Mention], - language_registry: &Arc, - language: Option<&Arc>, - text: &mut String, - highlights: &mut Vec<(Range, Highlight)>, - link_ranges: &mut Vec>, - link_urls: &mut Vec, -) { - use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd}; - - let mut bold_depth = 0; - let mut italic_depth = 0; - let mut strikethrough_depth = 0; - let mut link_url = None; - let mut current_language = None; - let mut list_stack = Vec::new(); - - let mut options = Options::all(); - options.remove(pulldown_cmark::Options::ENABLE_DEFINITION_LIST); - - for (event, source_range) in Parser::new_ext(block, options).into_offset_iter() { - let prev_len = text.len(); - match event { - Event::Text(t) => { - if let Some(language) = ¤t_language { - render_code(text, highlights, t.as_ref(), language); - } else { - while let Some(mention) = mentions.first() { - if !source_range.contains_inclusive(&mention.range) { - break; - } - mentions = &mentions[1..]; - let range = (prev_len + mention.range.start - source_range.start) - ..(prev_len + mention.range.end - source_range.start); - highlights.push(( - range.clone(), - if mention.is_self_mention { - Highlight::SelfMention - } else { - Highlight::Mention - }, - )); - } - - text.push_str(t.as_ref()); - let mut style = HighlightStyle::default(); - if bold_depth > 0 { - style.font_weight = Some(FontWeight::BOLD); - } - if italic_depth > 0 { - style.font_style = Some(FontStyle::Italic); - } - if strikethrough_depth > 0 { - style.strikethrough = Some(StrikethroughStyle { - thickness: 1.0.into(), - ..Default::default() - }); - } - let last_run_len = if let Some(link_url) = link_url.clone() { - link_ranges.push(prev_len..text.len()); - link_urls.push(link_url); - style.underline = Some(UnderlineStyle { - thickness: 1.0.into(), - ..Default::default() - }); - prev_len - } else { - // Manually scan for links - let mut finder = linkify::LinkFinder::new(); - finder.kinds(&[linkify::LinkKind::Url]); - let mut last_link_len = prev_len; - for link in finder.links(&t) { - let start = link.start(); - let end = link.end(); - let range = (prev_len + start)..(prev_len + end); - link_ranges.push(range.clone()); - link_urls.push(link.as_str().to_string()); - - // If there is a style before we match a link, we have to add this to the highlighted ranges - if style != HighlightStyle::default() && last_link_len < link.start() { - highlights.push(( - last_link_len..link.start(), - Highlight::Highlight(style), - )); - } - - highlights.push(( - range, - Highlight::Highlight(HighlightStyle { - underline: Some(UnderlineStyle { - thickness: 1.0.into(), - ..Default::default() - }), - ..style - }), - )); - - last_link_len = end; - } - last_link_len - }; - - if style != HighlightStyle::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 == &Highlight::Highlight(style) - { - last_range.end = text.len(); - new_highlight = false; - } - if new_highlight { - highlights - .push((last_run_len..text.len(), Highlight::Highlight(style))); - } - } - } - } - Event::Code(t) => { - text.push_str(t.as_ref()); - let is_link = link_url.is_some(); - - if let Some(link_url) = link_url.clone() { - link_ranges.push(prev_len..text.len()); - link_urls.push(link_url); - } - - highlights.push((prev_len..text.len(), Highlight::InlineCode(is_link))) - } - Event::Start(tag) => match tag { - Tag::Paragraph => new_paragraph(text, &mut list_stack), - Tag::Heading { .. } => { - new_paragraph(text, &mut list_stack); - bold_depth += 1; - } - Tag::CodeBlock(kind) => { - new_paragraph(text, &mut list_stack); - current_language = if let CodeBlockKind::Fenced(language) = kind { - language_registry - .language_for_name(language.as_ref()) - .now_or_never() - .and_then(Result::ok) - } else { - language.cloned() - } - } - Tag::Emphasis => italic_depth += 1, - Tag::Strong => bold_depth += 1, - Tag::Strikethrough => strikethrough_depth += 1, - Tag::Link { dest_url, .. } => link_url = Some(dest_url.to_string()), - Tag::List(number) => { - list_stack.push((number, false)); - } - Tag::Item => { - let len = list_stack.len(); - if let Some((list_number, has_content)) = list_stack.last_mut() { - *has_content = false; - if !text.is_empty() && !text.ends_with('\n') { - text.push('\n'); - } - for _ in 0..len - 1 { - text.push_str(" "); - } - if let Some(number) = list_number { - text.push_str(&format!("{}. ", number)); - *number += 1; - *has_content = false; - } else { - text.push_str("- "); - } - } - } - _ => {} - }, - Event::End(tag) => match tag { - TagEnd::Heading(_) => bold_depth -= 1, - TagEnd::CodeBlock => current_language = None, - TagEnd::Emphasis => italic_depth -= 1, - TagEnd::Strong => bold_depth -= 1, - TagEnd::Strikethrough => strikethrough_depth -= 1, - TagEnd::Link => link_url = None, - TagEnd::List(_) => drop(list_stack.pop()), - _ => {} - }, - Event::HardBreak => text.push('\n'), - Event::SoftBreak => text.push('\n'), - _ => {} - } - } -} - -pub fn render_code( - text: &mut String, - highlights: &mut Vec<(Range, Highlight)>, - content: &str, - language: &Arc, -) { - let prev_len = text.len(); - text.push_str(content); - let mut offset = 0; - for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { - if range.start > offset { - highlights.push((prev_len + offset..prev_len + range.start, Highlight::Code)); - } - highlights.push(( - prev_len + range.start..prev_len + range.end, - Highlight::Id(highlight_id), - )); - offset = range.end; - } - if offset < content.len() { - highlights.push((prev_len + offset..prev_len + content.len(), Highlight::Code)); - } -} - -pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { - let mut is_subsequent_paragraph_of_list = false; - if let Some((_, has_content)) = list_stack.last_mut() { - if *has_content { - is_subsequent_paragraph_of_list = true; - } else { - *has_content = true; - return; - } - } - - if !text.is_empty() { - if !text.ends_with('\n') { - text.push('\n'); - } - text.push('\n'); - } - for _ in 0..list_stack.len().saturating_sub(1) { - text.push_str(" "); - } - if is_subsequent_paragraph_of_list { - text.push_str(" "); - } -} diff --git a/crates/rope/Cargo.toml b/crates/rope/Cargo.toml deleted file mode 100644 index 9f0fc2be8a..0000000000 --- a/crates/rope/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "rope" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/rope.rs" - -[dependencies] -arrayvec = "0.7.1" -log.workspace = true -rayon.workspace = true -sum_tree.workspace = true -unicode-segmentation.workspace = true -util.workspace = true -ztracing.workspace = true -tracing.workspace = true - -[dev-dependencies] -ctor.workspace = true -gpui = { workspace = true, features = ["test-support"] } -rand.workspace = true -util = { workspace = true, features = ["test-support"] } -criterion.workspace = true -zlog.workspace = true - -[[bench]] -name = "rope_benchmark" -harness = false - -[package.metadata.cargo-machete] -ignored = ["tracing"] diff --git a/crates/rope/LICENSE-GPL b/crates/rope/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/rope/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/rope/benches/rope_benchmark.rs b/crates/rope/benches/rope_benchmark.rs deleted file mode 100644 index 8599328aac..0000000000 --- a/crates/rope/benches/rope_benchmark.rs +++ /dev/null @@ -1,273 +0,0 @@ -use std::ops::Range; - -use criterion::{ - BatchSize, BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main, -}; -use rand::prelude::*; -use rand::rngs::StdRng; -use rope::{Point, Rope}; -use sum_tree::Bias; -use util::RandomCharIter; - -/// Returns a biased random string whose UTF-8 length is close to but no more than `len` bytes. -/// -/// The string is biased towards characters expected to occur in text or likely to exercise edge -/// cases. -fn generate_random_text(rng: &mut StdRng, len: usize) -> String { - let mut str = String::with_capacity(len); - let mut chars = RandomCharIter::new(rng); - loop { - let ch = chars.next().unwrap(); - if str.len() + ch.len_utf8() > len { - break; - } - str.push(ch); - } - str -} - -fn generate_random_rope(rng: &mut StdRng, text_len: usize) -> Rope { - let text = generate_random_text(rng, text_len); - let mut rope = Rope::new(); - rope.push(&text); - rope -} - -fn generate_random_rope_ranges(rng: &mut StdRng, rope: &Rope) -> Vec> { - let range_max_len = 50; - let num_ranges = rope.len() / range_max_len; - - let mut ranges = Vec::new(); - let mut start = 0; - for _ in 0..num_ranges { - let range_start = rope.clip_offset( - rng.random_range(start..=(start + range_max_len)), - sum_tree::Bias::Left, - ); - let range_end = rope.clip_offset( - rng.random_range(range_start..(range_start + range_max_len)), - sum_tree::Bias::Right, - ); - - let range = range_start..range_end; - if !range.is_empty() { - ranges.push(range); - } - - start = range_end + 1; - } - - ranges -} - -fn generate_random_rope_points(rng: &mut StdRng, rope: &Rope) -> Vec { - let num_points = rope.len() / 10; - - let mut points = Vec::new(); - for _ in 0..num_points { - points.push(rope.offset_to_point(rng.random_range(0..rope.len()))); - } - points -} - -fn rope_benchmarks(c: &mut Criterion) { - static SEED: u64 = 9999; - static KB: usize = 1024; - - let sizes = [4 * KB, 64 * KB]; - - let mut group = c.benchmark_group("push"); - for size in sizes.iter() { - group.throughput(Throughput::Bytes(*size as u64)); - group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { - let mut rng = StdRng::seed_from_u64(SEED); - let text = generate_random_text(&mut rng, *size); - - b.iter(|| { - let mut rope = Rope::new(); - for _ in 0..10 { - rope.push(&text); - } - }); - }); - } - group.finish(); - - let mut group = c.benchmark_group("append"); - for size in sizes.iter() { - group.throughput(Throughput::Bytes(*size as u64)); - group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { - let mut rng = StdRng::seed_from_u64(SEED); - let mut random_ropes = Vec::new(); - for _ in 0..5 { - let rope = generate_random_rope(&mut rng, *size); - random_ropes.push(rope); - } - - b.iter(|| { - let mut rope_b = Rope::new(); - for rope in &random_ropes { - rope_b.append(rope.clone()) - } - }); - }); - } - group.finish(); - - let mut group = c.benchmark_group("slice"); - for size in sizes.iter() { - group.throughput(Throughput::Bytes(*size as u64)); - group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { - let mut rng = StdRng::seed_from_u64(SEED); - let rope = generate_random_rope(&mut rng, *size); - - b.iter_batched( - || generate_random_rope_ranges(&mut rng, &rope), - |ranges| { - for range in ranges.iter() { - rope.slice(range.clone()); - } - }, - BatchSize::SmallInput, - ); - }); - } - group.finish(); - - let mut group = c.benchmark_group("bytes_in_range"); - for size in sizes.iter() { - group.throughput(Throughput::Bytes(*size as u64)); - group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { - let mut rng = StdRng::seed_from_u64(SEED); - let rope = generate_random_rope(&mut rng, *size); - - b.iter_batched( - || generate_random_rope_ranges(&mut rng, &rope), - |ranges| { - for range in ranges.iter() { - let bytes = rope.bytes_in_range(range.clone()); - assert!(bytes.into_iter().count() > 0); - } - }, - BatchSize::SmallInput, - ); - }); - } - group.finish(); - - let mut group = c.benchmark_group("chars"); - for size in sizes.iter() { - group.throughput(Throughput::Bytes(*size as u64)); - group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { - let mut rng = StdRng::seed_from_u64(SEED); - let rope = generate_random_rope(&mut rng, *size); - - b.iter(|| { - let chars = rope.chars().count(); - assert!(chars > 0); - }); - }); - } - group.finish(); - - let mut group = c.benchmark_group("clip_point"); - for size in sizes.iter() { - group.throughput(Throughput::Bytes(*size as u64)); - group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { - let mut rng = StdRng::seed_from_u64(SEED); - let rope = generate_random_rope(&mut rng, *size); - - b.iter_batched( - || generate_random_rope_points(&mut rng, &rope), - |offsets| { - for offset in offsets.iter() { - black_box(rope.clip_point(*offset, Bias::Left)); - black_box(rope.clip_point(*offset, Bias::Right)); - } - }, - BatchSize::SmallInput, - ); - }); - } - group.finish(); - - let mut group = c.benchmark_group("point_to_offset"); - for size in sizes.iter() { - group.throughput(Throughput::Bytes(*size as u64)); - group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { - let mut rng = StdRng::seed_from_u64(SEED); - let rope = generate_random_rope(&mut rng, *size); - - b.iter_batched( - || generate_random_rope_points(&mut rng, &rope), - |offsets| { - for offset in offsets.iter() { - black_box(rope.point_to_offset(*offset)); - } - }, - BatchSize::SmallInput, - ); - }); - } - group.finish(); - - let mut group = c.benchmark_group("cursor"); - for size in sizes.iter() { - group.throughput(Throughput::Bytes(*size as u64)); - group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { - let mut rng = StdRng::seed_from_u64(SEED); - let rope = generate_random_rope(&mut rng, *size); - - b.iter_batched( - || { - let num_points = rope.len() / 10; - - let mut points = Vec::new(); - for _ in 0..num_points { - points.push(rng.random_range(0..rope.len())); - } - points - }, - |offsets| { - for offset in offsets.iter() { - black_box(rope.cursor(*offset)); - } - }, - BatchSize::SmallInput, - ); - }); - } - group.finish(); - - let mut group = c.benchmark_group("append many"); - group.throughput(Throughput::Bytes(128 * 100_000)); - - group.bench_function("small to large", |b| { - b.iter(|| { - let mut rope = Rope::new(); - let small = Rope::from("A".repeat(128)); - for _ in 0..100_000 { - rope.append(small.clone()); - } - assert_eq!(rope.len(), 128 * 100_000); - }); - }); - - group.bench_function("large to small", |b| { - b.iter(|| { - let mut rope = Rope::new(); - let small = Rope::from("A".repeat(128)); - for _ in 0..100_000 { - let large = rope; - rope = small.clone(); - rope.append(large); - } - assert_eq!(rope.len(), 128 * 100_000); - }); - }); - - group.finish(); -} - -criterion_group!(benches, rope_benchmarks); -criterion_main!(benches); diff --git a/crates/rope/src/chunk.rs b/crates/rope/src/chunk.rs deleted file mode 100644 index a2a8e8d58d..0000000000 --- a/crates/rope/src/chunk.rs +++ /dev/null @@ -1,1141 +0,0 @@ -use crate::{OffsetUtf16, Point, PointUtf16, TextSummary, Unclipped}; -use arrayvec::ArrayString; -use std::{cmp, ops::Range}; -use sum_tree::Bias; -use unicode_segmentation::GraphemeCursor; -use util::debug_panic; - -#[cfg(not(all(test, not(rust_analyzer))))] -pub(crate) type Bitmap = u128; -#[cfg(all(test, not(rust_analyzer)))] -pub(crate) type Bitmap = u16; - -pub(crate) const MIN_BASE: usize = MAX_BASE / 2; -pub(crate) const MAX_BASE: usize = Bitmap::BITS as usize; - -#[derive(Clone, Debug, Default)] -pub struct Chunk { - /// If bit[i] is set, then the character at index i is the start of a UTF-8 character in the - /// text. - chars: Bitmap, - /// The number of set bits is the number of UTF-16 code units it would take to represent the - /// text. - /// - /// Bit[i] is set if text[i] is the start of a UTF-8 character. If the character would - /// take two UTF-16 code units, then bit[i+1] is also set. (Rust chars never take more - /// than two UTF-16 code units.) - chars_utf16: Bitmap, - /// If bit[i] is set, then the character at index i is an ascii newline. - newlines: Bitmap, - /// If bit[i] is set, then the character at index i is an ascii tab. - tabs: Bitmap, - pub text: ArrayString, -} - -#[inline(always)] -const fn saturating_shl_mask(offset: u32) -> Bitmap { - (1 as Bitmap).unbounded_shl(offset).wrapping_sub(1) -} - -#[inline(always)] -const fn saturating_shr_mask(offset: u32) -> Bitmap { - !Bitmap::MAX.unbounded_shr(offset) -} - -impl Chunk { - pub const MASK_BITS: usize = Bitmap::BITS as usize; - - #[inline(always)] - pub fn new(text: &str) -> Self { - let mut this = Chunk::default(); - this.push_str(text); - this - } - - #[inline(always)] - pub fn push_str(&mut self, text: &str) { - for (char_ix, c) in text.char_indices() { - let ix = self.text.len() + char_ix; - self.chars |= 1 << ix; - self.chars_utf16 |= 1 << ix; - self.chars_utf16 |= (c.len_utf16() as Bitmap) << ix; - self.newlines |= ((c == '\n') as Bitmap) << ix; - self.tabs |= ((c == '\t') as Bitmap) << ix; - } - self.text.push_str(text); - } - - #[inline(always)] - pub fn append(&mut self, slice: ChunkSlice) { - if slice.is_empty() { - return; - }; - - let base_ix = self.text.len(); - self.chars |= slice.chars << base_ix; - self.chars_utf16 |= slice.chars_utf16 << base_ix; - self.newlines |= slice.newlines << base_ix; - self.tabs |= slice.tabs << base_ix; - self.text.push_str(slice.text); - } - - #[inline(always)] - pub fn as_slice(&self) -> ChunkSlice<'_> { - ChunkSlice { - chars: self.chars, - chars_utf16: self.chars_utf16, - newlines: self.newlines, - tabs: self.tabs, - text: &self.text, - } - } - - #[inline(always)] - pub fn slice(&self, range: Range) -> ChunkSlice<'_> { - self.as_slice().slice(range) - } - - #[inline(always)] - pub fn chars(&self) -> Bitmap { - self.chars - } - - pub fn tabs(&self) -> Bitmap { - self.tabs - } - - #[inline(always)] - pub fn is_char_boundary(&self, offset: usize) -> bool { - (1 as Bitmap).unbounded_shl(offset as u32) & self.chars != 0 || offset == self.text.len() - } - - pub fn floor_char_boundary(&self, index: usize) -> usize { - if index >= self.text.len() { - self.text.len() - } else { - let mut i = index; - while i > 0 { - if util::is_utf8_char_boundary(self.text.as_bytes()[i]) { - break; - } - i -= 1; - } - - i - } - } - - #[track_caller] - #[inline(always)] - pub fn assert_char_boundary(&self, offset: usize) -> bool { - if self.is_char_boundary(offset) { - return true; - } - if PANIC { - panic_char_boundary(&self.text, offset); - } else { - log_err_char_boundary(&self.text, offset); - false - } - } -} - -#[derive(Clone, Copy, Debug)] -pub struct ChunkSlice<'a> { - chars: Bitmap, - chars_utf16: Bitmap, - newlines: Bitmap, - tabs: Bitmap, - text: &'a str, -} - -impl Into for ChunkSlice<'_> { - fn into(self) -> Chunk { - Chunk { - chars: self.chars, - chars_utf16: self.chars_utf16, - newlines: self.newlines, - tabs: self.tabs, - text: self.text.try_into().unwrap(), - } - } -} - -impl<'a> ChunkSlice<'a> { - #[inline(always)] - pub fn is_empty(&self) -> bool { - self.text.is_empty() - } - - #[inline(always)] - pub fn is_char_boundary(&self, offset: usize) -> bool { - (1 as Bitmap).unbounded_shl(offset as u32) & self.chars != 0 || offset == self.text.len() - } - - #[inline(always)] - pub fn split_at(self, mid: usize) -> (ChunkSlice<'a>, ChunkSlice<'a>) { - if mid == MAX_BASE { - let left = self; - let right = ChunkSlice { - chars: 0, - chars_utf16: 0, - newlines: 0, - tabs: 0, - text: "", - }; - (left, right) - } else { - let mask = ((1 as Bitmap) << mid) - 1; - let (left_text, right_text) = self.text.split_at(mid); - let left = ChunkSlice { - chars: self.chars & mask, - chars_utf16: self.chars_utf16 & mask, - newlines: self.newlines & mask, - tabs: self.tabs & mask, - text: left_text, - }; - let right = ChunkSlice { - chars: self.chars >> mid, - chars_utf16: self.chars_utf16 >> mid, - newlines: self.newlines >> mid, - tabs: self.tabs >> mid, - text: right_text, - }; - (left, right) - } - } - - #[inline(always)] - pub fn slice(self, mut range: Range) -> Self { - if range.start == MAX_BASE { - Self { - chars: 0, - chars_utf16: 0, - newlines: 0, - tabs: 0, - text: "", - } - } else { - if !self.assert_char_boundary::(range.start) { - range.start = self.text.ceil_char_boundary(range.start); - } - if !self.assert_char_boundary::(range.end) { - range.end = if range.end < range.start { - range.start - } else { - self.text.floor_char_boundary(range.end) - }; - } - let mask = (1 as Bitmap) - .unbounded_shl(range.end as u32) - .wrapping_sub(1); - Self { - chars: (self.chars & mask) >> range.start, - chars_utf16: (self.chars_utf16 & mask) >> range.start, - newlines: (self.newlines & mask) >> range.start, - tabs: (self.tabs & mask) >> range.start, - text: &self.text[range], - } - } - } - - #[inline(always)] - pub fn text_summary(&self) -> TextSummary { - let mut chars = 0; - let (longest_row, longest_row_chars) = self.longest_row(&mut chars); - TextSummary { - len: self.len(), - chars, - len_utf16: self.len_utf16(), - lines: self.lines(), - first_line_chars: self.first_line_chars(), - last_line_chars: self.last_line_chars(), - last_line_len_utf16: self.last_line_len_utf16(), - longest_row, - longest_row_chars, - } - } - - /// Get length in bytes - #[inline(always)] - pub fn len(&self) -> usize { - self.text.len() - } - - /// Get length in UTF-16 code units - #[inline(always)] - pub fn len_utf16(&self) -> OffsetUtf16 { - OffsetUtf16(self.chars_utf16.count_ones() as usize) - } - - /// Get point representing number of lines and length of last line - #[inline(always)] - pub fn lines(&self) -> Point { - let row = self.newlines.count_ones(); - let column = self.newlines.leading_zeros() - (Bitmap::BITS - self.text.len() as u32); - Point::new(row, column) - } - - /// Get number of chars in first line - #[inline(always)] - pub fn first_line_chars(&self) -> u32 { - (self.chars & saturating_shl_mask(self.newlines.trailing_zeros())).count_ones() - } - - /// Get number of chars in last line - #[inline(always)] - pub fn last_line_chars(&self) -> u32 { - (self.chars & saturating_shr_mask(self.newlines.leading_zeros())).count_ones() - } - - /// Get number of UTF-16 code units in last line - #[inline(always)] - pub fn last_line_len_utf16(&self) -> u32 { - (self.chars_utf16 & saturating_shr_mask(self.newlines.leading_zeros())).count_ones() - } - - /// Get the longest row in the chunk and its length in characters. - /// Calculate the total number of characters in the chunk along the way. - #[inline(always)] - pub fn longest_row(&self, total_chars: &mut usize) -> (u32, u32) { - let mut chars = self.chars; - let mut newlines = self.newlines; - *total_chars = 0; - let mut row = 0; - let mut longest_row = 0; - let mut longest_row_chars = 0; - while newlines > 0 { - let newline_ix = newlines.trailing_zeros(); - let row_chars = (chars & ((1 << newline_ix) - 1)).count_ones() as u8; - *total_chars += usize::from(row_chars); - if row_chars > longest_row_chars { - longest_row = row; - longest_row_chars = row_chars; - } - - newlines >>= newline_ix; - newlines >>= 1; - chars >>= newline_ix; - chars >>= 1; - row += 1; - *total_chars += 1; - } - - let row_chars = chars.count_ones() as u8; - *total_chars += usize::from(row_chars); - if row_chars > longest_row_chars { - (row, row_chars as u32) - } else { - (longest_row, longest_row_chars as u32) - } - } - - #[inline(always)] - pub fn offset_to_point(&self, offset: usize) -> Point { - let mask = (1 as Bitmap).unbounded_shl(offset as u32).wrapping_sub(1); - let row = (self.newlines & mask).count_ones(); - let newline_ix = Bitmap::BITS - (self.newlines & mask).leading_zeros(); - let column = (offset - newline_ix as usize) as u32; - Point::new(row, column) - } - - #[inline(always)] - pub fn point_to_offset(&self, point: Point) -> usize { - if point.row > self.lines().row { - debug_panic!( - "point {:?} extends beyond rows for string {:?}", - point, - self.text - ); - return self.len(); - } - - let row_offset_range = self.offset_range_for_row(point.row); - if point.column > row_offset_range.len() as u32 { - debug_panic!( - "point {:?} extends beyond row for string {:?}", - point, - self.text - ); - row_offset_range.end - } else { - row_offset_range.start + point.column as usize - } - } - - #[track_caller] - #[inline(always)] - pub fn assert_char_boundary(&self, offset: usize) -> bool { - if self.is_char_boundary(offset) { - return true; - } - if PANIC { - panic_char_boundary(self.text, offset); - } else { - log_err_char_boundary(self.text, offset); - false - } - } - - pub fn floor_char_boundary(&self, index: usize) -> usize { - self.text.floor_char_boundary(index) - } - - #[inline(always)] - pub fn point_to_offset_utf16(&self, point: Point) -> OffsetUtf16 { - if point.row > self.lines().row { - debug_panic!( - "point {:?} extends beyond rows for string {:?}", - point, - self.text - ); - return self.len_utf16(); - } - self.offset_to_offset_utf16(self.point_to_offset(point)) - } - - #[inline(always)] - pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { - let mask = (1 as Bitmap).unbounded_shl(offset as u32).wrapping_sub(1); - OffsetUtf16((self.chars_utf16 & mask).count_ones() as usize) - } - - #[inline(always)] - pub fn offset_utf16_to_offset(&self, target: OffsetUtf16) -> usize { - if target.0 == 0 { - 0 - } else { - #[cfg(not(test))] - let chars_utf16 = self.chars_utf16; - #[cfg(test)] - let chars_utf16 = self.chars_utf16 as u128; - let ix = nth_set_bit(chars_utf16, target.0) + 1; - if ix == MAX_BASE { - MAX_BASE - } else { - let utf8_additional_len = cmp::min( - (self.chars_utf16 >> ix).trailing_zeros() as usize, - self.text.len() - ix, - ); - ix + utf8_additional_len - } - } - } - - #[inline(always)] - pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 { - let mask = saturating_shl_mask(offset as u32); - let row = (self.newlines & saturating_shl_mask(offset as u32)).count_ones(); - let newline_ix = Bitmap::BITS - (self.newlines & mask).leading_zeros(); - let column = if newline_ix as usize == MAX_BASE { - 0 - } else { - ((self.chars_utf16 & mask) >> newline_ix).count_ones() - }; - PointUtf16::new(row, column) - } - - #[inline(always)] - pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 { - self.offset_to_point_utf16(self.point_to_offset(point)) - } - - #[inline(always)] - pub fn point_utf16_to_offset(&self, point: PointUtf16, clip: bool) -> usize { - let lines = self.lines(); - if point.row > lines.row { - if !clip { - debug_panic!( - "point {:?} is beyond this chunk's extent {:?}", - point, - self.text - ); - } - return self.len(); - } - - let row_offset_range = self.offset_range_for_row(point.row); - let line = self.slice(row_offset_range.clone()); - if point.column > line.last_line_len_utf16() { - if !clip { - debug_panic!( - "point {:?} is beyond the end of the line in chunk {:?}", - point, - self.text - ); - } - return line.len(); - } - - let mut offset = row_offset_range.start; - if point.column > 0 { - offset += line.offset_utf16_to_offset(OffsetUtf16(point.column as usize)); - if !self.text.is_char_boundary(offset) { - offset -= 1; - while !self.text.is_char_boundary(offset) { - offset -= 1; - } - if !clip { - debug_panic!( - "point {:?} is within character in chunk {:?}", - point, - self.text, - ); - } - } - } - offset - } - - #[inline(always)] - pub fn unclipped_point_utf16_to_point(&self, point: Unclipped) -> Point { - let max_point = self.lines(); - if point.0.row > max_point.row { - return max_point; - } - - let row_offset_range = self.offset_range_for_row(point.0.row); - let line = self.slice(row_offset_range); - if point.0.column == 0 { - Point::new(point.0.row, 0) - } else if point.0.column >= line.len_utf16().0 as u32 { - Point::new(point.0.row, line.len() as u32) - } else { - let mut column = line.offset_utf16_to_offset(OffsetUtf16(point.0.column as usize)); - while !line.text.is_char_boundary(column) { - column -= 1; - } - Point::new(point.0.row, column as u32) - } - } - - #[inline(always)] - pub fn clip_point(&self, point: Point, bias: Bias) -> Point { - let max_point = self.lines(); - if point.row > max_point.row { - return max_point; - } - - let line = self.slice(self.offset_range_for_row(point.row)); - if point.column == 0 { - point - } else if point.column >= line.len() as u32 { - Point::new(point.row, line.len() as u32) - } else { - let mut column = point.column as usize; - let bytes = line.text.as_bytes(); - if bytes[column - 1] < 128 && bytes[column] < 128 { - return Point::new(point.row, column as u32); - } - - let mut grapheme_cursor = GraphemeCursor::new(column, bytes.len(), true); - loop { - if line.is_char_boundary(column) - && grapheme_cursor.is_boundary(line.text, 0).unwrap_or(false) - { - break; - } - - match bias { - Bias::Left => column -= 1, - Bias::Right => column += 1, - } - grapheme_cursor.set_cursor(column); - } - Point::new(point.row, column as u32) - } - } - - #[inline(always)] - pub fn clip_point_utf16(&self, point: Unclipped, bias: Bias) -> PointUtf16 { - let max_point = self.lines(); - if point.0.row > max_point.row { - PointUtf16::new(max_point.row, self.last_line_len_utf16()) - } else { - let line = self.slice(self.offset_range_for_row(point.0.row)); - let column = line.clip_offset_utf16(OffsetUtf16(point.0.column as usize), bias); - PointUtf16::new(point.0.row, column.0 as u32) - } - } - - #[inline(always)] - pub fn clip_offset_utf16(&self, target: OffsetUtf16, bias: Bias) -> OffsetUtf16 { - if target == OffsetUtf16::default() { - OffsetUtf16::default() - } else if target >= self.len_utf16() { - self.len_utf16() - } else { - let mut offset = self.offset_utf16_to_offset(target); - while !self.text.is_char_boundary(offset) { - if bias == Bias::Left { - offset -= 1; - } else { - offset += 1; - } - } - self.offset_to_offset_utf16(offset) - } - } - - #[inline(always)] - fn offset_range_for_row(&self, row: u32) -> Range { - let row_start = if row > 0 { - #[cfg(not(test))] - let newlines = self.newlines; - #[cfg(test)] - let newlines = self.newlines as u128; - nth_set_bit(newlines, row as usize) + 1 - } else { - 0 - }; - let row_len = if row_start == MAX_BASE { - 0 - } else { - cmp::min( - (self.newlines >> row_start).trailing_zeros(), - (self.text.len() - row_start) as u32, - ) - }; - row_start..row_start + row_len as usize - } - - #[inline(always)] - pub fn tabs(&self) -> Tabs { - Tabs { - tabs: self.tabs, - chars: self.chars, - } - } -} - -pub struct Tabs { - tabs: Bitmap, - chars: Bitmap, -} - -#[derive(Debug, PartialEq, Eq)] -pub struct TabPosition { - pub byte_offset: usize, - pub char_offset: usize, -} - -impl Iterator for Tabs { - type Item = TabPosition; - - fn next(&mut self) -> Option { - if self.tabs == 0 { - return None; - } - - let tab_offset = self.tabs.trailing_zeros() as usize; - let chars_mask = (1 << tab_offset) - 1; - let char_offset = (self.chars & chars_mask).count_ones() as usize; - - // Since tabs are 1 byte the tab offset is the same as the byte offset - let position = TabPosition { - byte_offset: tab_offset, - char_offset, - }; - // Remove the tab we've just seen - self.tabs ^= 1 << tab_offset; - - Some(position) - } -} - -/// Finds the n-th bit that is set to 1. -#[inline(always)] -fn nth_set_bit(v: u128, n: usize) -> usize { - let low = v as u64; - let high = (v >> 64) as u64; - - let low_count = low.count_ones() as usize; - if n > low_count { - 64 + nth_set_bit_u64(high, (n - low_count) as u64) as usize - } else { - nth_set_bit_u64(low, n as u64) as usize - } -} - -#[cold] -#[inline(never)] -#[track_caller] -fn panic_char_boundary(text: &str, offset: usize) -> ! { - if offset > text.len() { - panic!( - "byte index {} is out of bounds of `{:?}` (length: {})", - offset, - text, - text.len() - ); - } - // find the character - let char_start = text.floor_char_boundary(offset); - // `char_start` must be less than len and a char boundary - let ch = text.get(char_start..).unwrap().chars().next().unwrap(); - let char_range = char_start..char_start + ch.len_utf8(); - panic!( - "byte index {} is not a char boundary; it is inside {:?} (bytes {:?})", - offset, ch, char_range, - ); -} - -#[cold] -#[inline(never)] -#[track_caller] -fn log_err_char_boundary(text: &str, offset: usize) { - if offset > text.len() { - log::error!( - "byte index {} is out of bounds of `{:?}` (length: {})", - offset, - text, - text.len() - ); - } - // find the character - let char_start = text.floor_char_boundary(offset); - // `char_start` must be less than len and a char boundary - let ch = text.get(char_start..).unwrap().chars().next().unwrap(); - let char_range = char_start..char_start + ch.len_utf8(); - log::error!( - "byte index {} is not a char boundary; it is inside {:?} (bytes {:?})", - offset, - ch, - char_range, - ); -} - -#[inline(always)] -fn nth_set_bit_u64(v: u64, mut n: u64) -> u64 { - let v = v.reverse_bits(); - let mut s: u64 = 64; - - // Parallel bit count intermediates - let a = v - ((v >> 1) & (u64::MAX / 3)); - let b = (a & (u64::MAX / 5)) + ((a >> 2) & (u64::MAX / 5)); - let c = (b + (b >> 4)) & (u64::MAX / 0x11); - let d = (c + (c >> 8)) & (u64::MAX / 0x101); - - // Branchless select - let t = (d >> 32) + (d >> 48); - s -= (t.wrapping_sub(n) & 256) >> 3; - n -= t & (t.wrapping_sub(n) >> 8); - - let t = (d >> (s - 16)) & 0xff; - s -= (t.wrapping_sub(n) & 256) >> 4; - n -= t & (t.wrapping_sub(n) >> 8); - - let t = (c >> (s - 8)) & 0xf; - s -= (t.wrapping_sub(n) & 256) >> 5; - n -= t & (t.wrapping_sub(n) >> 8); - - let t = (b >> (s - 4)) & 0x7; - s -= (t.wrapping_sub(n) & 256) >> 6; - n -= t & (t.wrapping_sub(n) >> 8); - - let t = (a >> (s - 2)) & 0x3; - s -= (t.wrapping_sub(n) & 256) >> 7; - n -= t & (t.wrapping_sub(n) >> 8); - - let t = (v >> (s - 1)) & 0x1; - s -= (t.wrapping_sub(n) & 256) >> 8; - - 65 - s - 1 -} - -#[cfg(test)] -mod tests { - use super::*; - use rand::prelude::*; - use util::RandomCharIter; - - #[gpui::test(iterations = 100)] - fn test_random_chunks(mut rng: StdRng) { - let text = random_string_with_utf8_len(&mut rng, MAX_BASE); - log::info!("Chunk: {:?}", text); - let chunk = Chunk::new(&text); - verify_chunk(chunk.as_slice(), &text); - - // Verify Chunk::chars() bitmap - let expected_chars = char_offsets(&text) - .into_iter() - .inspect(|i| assert!(*i < MAX_BASE)) - .fold(0 as Bitmap, |acc, i| acc | (1 << i)); - assert_eq!(chunk.chars(), expected_chars); - - for _ in 0..10 { - let mut start = rng.random_range(0..=chunk.text.len()); - let mut end = rng.random_range(start..=chunk.text.len()); - while !chunk.text.is_char_boundary(start) { - start -= 1; - } - while !chunk.text.is_char_boundary(end) { - end -= 1; - } - let range = start..end; - log::info!("Range: {:?}", range); - let text_slice = &text[range.clone()]; - let chunk_slice = chunk.slice(range); - verify_chunk(chunk_slice, text_slice); - } - } - - #[gpui::test(iterations = 100)] - fn test_split_chunk_slice(mut rng: StdRng) { - let text = &random_string_with_utf8_len(&mut rng, MAX_BASE); - let chunk = Chunk::new(text); - let offset = char_offsets_with_end(text) - .into_iter() - .choose(&mut rng) - .unwrap(); - let (a, b) = chunk.as_slice().split_at(offset); - let (a_str, b_str) = text.split_at(offset); - verify_chunk(a, a_str); - verify_chunk(b, b_str); - } - - #[gpui::test(iterations = 1000)] - fn test_nth_set_bit_random(mut rng: StdRng) { - let set_count = rng.random_range(0..=128); - let mut set_bits = (0..128).choose_multiple(&mut rng, set_count); - set_bits.sort(); - let mut n = 0; - for ix in set_bits.iter().copied() { - n |= 1 << ix; - } - - for (mut ix, position) in set_bits.into_iter().enumerate() { - ix += 1; - assert_eq!( - nth_set_bit(n, ix), - position, - "nth_set_bit({:0128b}, {})", - n, - ix - ); - } - } - - /// Returns a (biased) random string whose UTF-8 length is no more than `len`. - fn random_string_with_utf8_len(rng: &mut StdRng, len: usize) -> String { - let mut str = String::new(); - let mut chars = RandomCharIter::new(rng); - loop { - let ch = chars.next().unwrap(); - if str.len() + ch.len_utf8() > len { - break; - } - str.push(ch); - } - str - } - - #[gpui::test(iterations = 1000)] - fn test_append_random_strings(mut rng: StdRng) { - let len1 = rng.random_range(0..=MAX_BASE); - let len2 = rng.random_range(0..=MAX_BASE).saturating_sub(len1); - let str1 = random_string_with_utf8_len(&mut rng, len1); - let str2 = random_string_with_utf8_len(&mut rng, len2); - let mut chunk1 = Chunk::new(&str1); - let chunk2 = Chunk::new(&str2); - let char_offsets = char_offsets_with_end(&str2); - let start_index = rng.random_range(0..char_offsets.len()); - let start_offset = char_offsets[start_index]; - let end_offset = char_offsets[rng.random_range(start_index..char_offsets.len())]; - chunk1.append(chunk2.slice(start_offset..end_offset)); - verify_chunk(chunk1.as_slice(), &(str1 + &str2[start_offset..end_offset])); - } - - /// Return the byte offsets for each character in a string. - /// - /// These are valid offsets to split the string. - fn char_offsets(text: &str) -> Vec { - text.char_indices().map(|(i, _c)| i).collect() - } - - /// Return the byte offsets for each character in a string, plus the offset - /// past the end of the string. - fn char_offsets_with_end(text: &str) -> Vec { - let mut v = char_offsets(text); - v.push(text.len()); - v - } - - fn verify_chunk(chunk: ChunkSlice<'_>, text: &str) { - let mut offset = 0; - let mut offset_utf16 = OffsetUtf16(0); - let mut point = Point::zero(); - let mut point_utf16 = PointUtf16::zero(); - - log::info!("Verifying chunk {:?}", text); - assert_eq!(chunk.offset_to_point(0), Point::zero()); - - let mut expected_tab_positions = Vec::new(); - - for (char_offset, c) in text.chars().enumerate() { - let expected_point = chunk.offset_to_point(offset); - assert_eq!(point, expected_point, "mismatch at offset {}", offset); - assert_eq!( - chunk.point_to_offset(point), - offset, - "mismatch at point {:?}", - point - ); - assert_eq!( - chunk.offset_to_offset_utf16(offset), - offset_utf16, - "mismatch at offset {}", - offset - ); - assert_eq!( - chunk.offset_utf16_to_offset(offset_utf16), - offset, - "mismatch at offset_utf16 {:?}", - offset_utf16 - ); - assert_eq!( - chunk.point_to_point_utf16(point), - point_utf16, - "mismatch at point {:?}", - point - ); - assert_eq!( - chunk.point_utf16_to_offset(point_utf16, false), - offset, - "mismatch at point_utf16 {:?}", - point_utf16 - ); - assert_eq!( - chunk.unclipped_point_utf16_to_point(Unclipped(point_utf16)), - point, - "mismatch for unclipped_point_utf16_to_point at {:?}", - point_utf16 - ); - - assert_eq!( - chunk.clip_point(point, Bias::Left), - point, - "incorrect left clip at {:?}", - point - ); - assert_eq!( - chunk.clip_point(point, Bias::Right), - point, - "incorrect right clip at {:?}", - point - ); - - for i in 1..c.len_utf8() { - let test_point = Point::new(point.row, point.column + i as u32); - assert_eq!( - chunk.clip_point(test_point, Bias::Left), - point, - "incorrect left clip within multi-byte char at {:?}", - test_point - ); - assert_eq!( - chunk.clip_point(test_point, Bias::Right), - Point::new(point.row, point.column + c.len_utf8() as u32), - "incorrect right clip within multi-byte char at {:?}", - test_point - ); - } - - for i in 1..c.len_utf16() { - let test_point = Unclipped(PointUtf16::new( - point_utf16.row, - point_utf16.column + i as u32, - )); - assert_eq!( - chunk.unclipped_point_utf16_to_point(test_point), - point, - "incorrect unclipped_point_utf16_to_point within multi-byte char at {:?}", - test_point - ); - assert_eq!( - chunk.clip_point_utf16(test_point, Bias::Left), - point_utf16, - "incorrect left clip_point_utf16 within multi-byte char at {:?}", - test_point - ); - assert_eq!( - chunk.clip_point_utf16(test_point, Bias::Right), - PointUtf16::new(point_utf16.row, point_utf16.column + c.len_utf16() as u32), - "incorrect right clip_point_utf16 within multi-byte char at {:?}", - test_point - ); - - let test_offset = OffsetUtf16(offset_utf16.0 + i); - assert_eq!( - chunk.clip_offset_utf16(test_offset, Bias::Left), - offset_utf16, - "incorrect left clip_offset_utf16 within multi-byte char at {:?}", - test_offset - ); - assert_eq!( - chunk.clip_offset_utf16(test_offset, Bias::Right), - OffsetUtf16(offset_utf16.0 + c.len_utf16()), - "incorrect right clip_offset_utf16 within multi-byte char at {:?}", - test_offset - ); - } - - if c == '\n' { - point.row += 1; - point.column = 0; - point_utf16.row += 1; - point_utf16.column = 0; - } else { - point.column += c.len_utf8() as u32; - point_utf16.column += c.len_utf16() as u32; - } - - if c == '\t' { - expected_tab_positions.push(TabPosition { - byte_offset: offset, - char_offset, - }); - } - - offset += c.len_utf8(); - offset_utf16.0 += c.len_utf16(); - } - - let final_point = chunk.offset_to_point(offset); - assert_eq!(point, final_point, "mismatch at final offset {}", offset); - assert_eq!( - chunk.point_to_offset(point), - offset, - "mismatch at point {:?}", - point - ); - assert_eq!( - chunk.offset_to_offset_utf16(offset), - offset_utf16, - "mismatch at offset {}", - offset - ); - assert_eq!( - chunk.offset_utf16_to_offset(offset_utf16), - offset, - "mismatch at offset_utf16 {:?}", - offset_utf16 - ); - assert_eq!( - chunk.point_to_point_utf16(point), - point_utf16, - "mismatch at final point {:?}", - point - ); - assert_eq!( - chunk.point_utf16_to_offset(point_utf16, false), - offset, - "mismatch at final point_utf16 {:?}", - point_utf16 - ); - assert_eq!( - chunk.unclipped_point_utf16_to_point(Unclipped(point_utf16)), - point, - "mismatch for unclipped_point_utf16_to_point at final point {:?}", - point_utf16 - ); - assert_eq!( - chunk.clip_point(point, Bias::Left), - point, - "incorrect left clip at final point {:?}", - point - ); - assert_eq!( - chunk.clip_point(point, Bias::Right), - point, - "incorrect right clip at final point {:?}", - point - ); - assert_eq!( - chunk.clip_point_utf16(Unclipped(point_utf16), Bias::Left), - point_utf16, - "incorrect left clip_point_utf16 at final point {:?}", - point_utf16 - ); - assert_eq!( - chunk.clip_point_utf16(Unclipped(point_utf16), Bias::Right), - point_utf16, - "incorrect right clip_point_utf16 at final point {:?}", - point_utf16 - ); - assert_eq!( - chunk.clip_offset_utf16(offset_utf16, Bias::Left), - offset_utf16, - "incorrect left clip_offset_utf16 at final offset {:?}", - offset_utf16 - ); - assert_eq!( - chunk.clip_offset_utf16(offset_utf16, Bias::Right), - offset_utf16, - "incorrect right clip_offset_utf16 at final offset {:?}", - offset_utf16 - ); - - // Verify length methods - assert_eq!(chunk.len(), text.len()); - assert_eq!( - chunk.len_utf16().0, - text.chars().map(|c| c.len_utf16()).sum::() - ); - - // Verify line counting - let lines = chunk.lines(); - let mut newline_count = 0; - let mut last_line_len = 0; - for c in text.chars() { - if c == '\n' { - newline_count += 1; - last_line_len = 0; - } else { - last_line_len += c.len_utf8() as u32; - } - } - assert_eq!(lines, Point::new(newline_count, last_line_len)); - - // Verify first/last line chars - if !text.is_empty() { - let first_line = text.split('\n').next().unwrap(); - assert_eq!(chunk.first_line_chars(), first_line.chars().count() as u32); - - let last_line = text.split('\n').next_back().unwrap(); - assert_eq!(chunk.last_line_chars(), last_line.chars().count() as u32); - assert_eq!( - chunk.last_line_len_utf16(), - last_line.chars().map(|c| c.len_utf16() as u32).sum::() - ); - } - - // Verify longest row - let (longest_row, longest_chars) = chunk.longest_row(&mut 0); - let mut max_chars = 0; - let mut current_row = 0; - let mut current_chars = 0; - let mut max_row = 0; - - for c in text.chars() { - if c == '\n' { - if current_chars > max_chars { - max_chars = current_chars; - max_row = current_row; - } - current_row += 1; - current_chars = 0; - } else { - current_chars += 1; - } - } - - if current_chars > max_chars { - max_chars = current_chars; - max_row = current_row; - } - - assert_eq!((max_row, max_chars as u32), (longest_row, longest_chars)); - assert_eq!(chunk.tabs().collect::>(), expected_tab_positions); - } -} diff --git a/crates/rope/src/offset_utf16.rs b/crates/rope/src/offset_utf16.rs deleted file mode 100644 index 1223fbbe38..0000000000 --- a/crates/rope/src/offset_utf16.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::ops::{Add, AddAssign, Sub}; - -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] -pub struct OffsetUtf16(pub usize); - -impl<'a> Add<&'a Self> for OffsetUtf16 { - type Output = Self; - - fn add(self, other: &'a Self) -> Self::Output { - Self(self.0 + other.0) - } -} - -impl Add for OffsetUtf16 { - type Output = Self; - - fn add(self, other: Self) -> Self::Output { - Self(self.0 + other.0) - } -} - -impl<'a> Sub<&'a Self> for OffsetUtf16 { - type Output = Self; - - fn sub(self, other: &'a Self) -> Self::Output { - debug_assert!(*other <= self); - Self(self.0 - other.0) - } -} - -impl Sub for OffsetUtf16 { - type Output = OffsetUtf16; - - fn sub(self, other: Self) -> Self::Output { - Self(self.0 - other.0) - } -} - -impl<'a> AddAssign<&'a Self> for OffsetUtf16 { - fn add_assign(&mut self, other: &'a Self) { - self.0 += other.0; - } -} - -impl AddAssign for OffsetUtf16 { - fn add_assign(&mut self, other: Self) { - self.0 += other.0; - } -} diff --git a/crates/rope/src/point.rs b/crates/rope/src/point.rs deleted file mode 100644 index a2491f6b0e..0000000000 --- a/crates/rope/src/point.rs +++ /dev/null @@ -1,146 +0,0 @@ -use std::{ - cmp::Ordering, - fmt::{self, Debug}, - ops::{Add, AddAssign, Range, Sub}, -}; - -/// A zero-indexed point in a text buffer consisting of a row and column. -#[derive(Clone, Copy, Default, Eq, PartialEq, Hash)] -pub struct Point { - pub row: u32, - pub column: u32, -} - -impl Debug for Point { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Point({}:{})", self.row, self.column) - } -} - -impl Point { - pub const MAX: Self = Self { - row: u32::MAX, - column: u32::MAX, - }; - - pub fn new(row: u32, column: u32) -> Self { - Point { row, column } - } - - pub fn row_range(range: Range) -> Range { - Point { - row: range.start, - column: 0, - }..Point { - row: range.end, - column: 0, - } - } - - pub fn zero() -> Self { - Point::new(0, 0) - } - - pub fn parse_str(s: &str) -> Self { - let mut point = Self::zero(); - for (row, line) in s.split('\n').enumerate() { - point.row = row as u32; - point.column = line.len() as u32; - } - point - } - - pub fn is_zero(&self) -> bool { - self.row == 0 && self.column == 0 - } - - pub fn saturating_sub(self, other: Self) -> Self { - if self < other { - Self::zero() - } else { - self - other - } - } -} - -impl<'a> Add<&'a Self> for Point { - type Output = Point; - - fn add(self, other: &'a Self) -> Self::Output { - self + *other - } -} - -impl Add for Point { - type Output = Point; - - fn add(self, other: Self) -> Self::Output { - if other.row == 0 { - Point::new(self.row, self.column + other.column) - } else { - Point::new(self.row + other.row, other.column) - } - } -} - -impl<'a> Sub<&'a Self> for Point { - type Output = Point; - - fn sub(self, other: &'a Self) -> Self::Output { - self - *other - } -} - -impl Sub for Point { - type Output = Point; - - fn sub(self, other: Self) -> Self::Output { - debug_assert!(other <= self); - - if self.row == other.row { - Point::new(0, self.column - other.column) - } else { - Point::new(self.row - other.row, self.column) - } - } -} - -impl<'a> AddAssign<&'a Self> for Point { - fn add_assign(&mut self, other: &'a Self) { - *self += *other; - } -} - -impl AddAssign for Point { - fn add_assign(&mut self, other: Self) { - if other.row == 0 { - self.column += other.column; - } else { - self.row += other.row; - self.column = other.column; - } - } -} - -impl PartialOrd for Point { - fn partial_cmp(&self, other: &Point) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Point { - #[cfg(target_pointer_width = "64")] - fn cmp(&self, other: &Point) -> Ordering { - let a = ((self.row as usize) << 32) | self.column as usize; - let b = ((other.row as usize) << 32) | other.column as usize; - a.cmp(&b) - } - - #[cfg(target_pointer_width = "32")] - fn cmp(&self, other: &Point) -> Ordering { - match self.row.cmp(&other.row) { - Ordering::Equal => self.column.cmp(&other.column), - comparison @ _ => comparison, - } - } -} diff --git a/crates/rope/src/point_utf16.rs b/crates/rope/src/point_utf16.rs deleted file mode 100644 index 096c2defa4..0000000000 --- a/crates/rope/src/point_utf16.rs +++ /dev/null @@ -1,119 +0,0 @@ -use std::{ - cmp::Ordering, - ops::{Add, AddAssign, Sub}, -}; - -#[derive(Clone, Copy, Default, Eq, PartialEq, Debug, Hash)] -pub struct PointUtf16 { - pub row: u32, - pub column: u32, -} - -impl PointUtf16 { - pub const MAX: Self = Self { - row: u32::MAX, - column: u32::MAX, - }; - - pub fn new(row: u32, column: u32) -> Self { - PointUtf16 { row, column } - } - - pub fn zero() -> Self { - PointUtf16::new(0, 0) - } - - pub fn is_zero(&self) -> bool { - self.row == 0 && self.column == 0 - } - - pub fn saturating_sub(self, other: Self) -> Self { - if self < other { - Self::zero() - } else { - self - other - } - } -} - -impl<'a> Add<&'a Self> for PointUtf16 { - type Output = PointUtf16; - - fn add(self, other: &'a Self) -> Self::Output { - self + *other - } -} - -impl Add for PointUtf16 { - type Output = PointUtf16; - - fn add(self, other: Self) -> Self::Output { - if other.row == 0 { - PointUtf16::new(self.row, self.column + other.column) - } else { - PointUtf16::new(self.row + other.row, other.column) - } - } -} - -impl<'a> Sub<&'a Self> for PointUtf16 { - type Output = PointUtf16; - - fn sub(self, other: &'a Self) -> Self::Output { - self - *other - } -} - -impl Sub for PointUtf16 { - type Output = PointUtf16; - - fn sub(self, other: Self) -> Self::Output { - debug_assert!(other <= self); - - if self.row == other.row { - PointUtf16::new(0, self.column - other.column) - } else { - PointUtf16::new(self.row - other.row, self.column) - } - } -} - -impl<'a> AddAssign<&'a Self> for PointUtf16 { - fn add_assign(&mut self, other: &'a Self) { - *self += *other; - } -} - -impl AddAssign for PointUtf16 { - fn add_assign(&mut self, other: Self) { - if other.row == 0 { - self.column += other.column; - } else { - self.row += other.row; - self.column = other.column; - } - } -} - -impl PartialOrd for PointUtf16 { - fn partial_cmp(&self, other: &PointUtf16) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for PointUtf16 { - #[cfg(target_pointer_width = "64")] - fn cmp(&self, other: &PointUtf16) -> Ordering { - let a = ((self.row as usize) << 32) | self.column as usize; - let b = ((other.row as usize) << 32) | other.column as usize; - a.cmp(&b) - } - - #[cfg(target_pointer_width = "32")] - fn cmp(&self, other: &PointUtf16) -> Ordering { - match self.row.cmp(&other.row) { - Ordering::Equal => self.column.cmp(&other.column), - comparison @ _ => comparison, - } - } -} diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs deleted file mode 100644 index 50f9ba044d..0000000000 --- a/crates/rope/src/rope.rs +++ /dev/null @@ -1,2247 +0,0 @@ -mod chunk; -mod offset_utf16; -mod point; -mod point_utf16; -mod unclipped; - -use arrayvec::ArrayVec; -use rayon::iter::{IntoParallelIterator, ParallelIterator as _}; -use std::{ - cmp, fmt, io, mem, - ops::{self, AddAssign, Range}, - str, -}; -use sum_tree::{Bias, Dimension, Dimensions, SumTree}; -use ztracing::instrument; - -pub use chunk::{Chunk, ChunkSlice}; -pub use offset_utf16::OffsetUtf16; -pub use point::Point; -pub use point_utf16::PointUtf16; -pub use unclipped::Unclipped; - -use crate::chunk::Bitmap; - -#[derive(Clone, Default)] -pub struct Rope { - chunks: SumTree, -} - -impl Rope { - pub fn new() -> Self { - Self::default() - } - - /// Checks that `index`-th byte is the first byte in a UTF-8 code point - /// sequence or the end of the string. - /// - /// The start and end of the string (when `index == self.len()`) are - /// considered to be boundaries. - /// - /// Returns `false` if `index` is greater than `self.len()`. - pub fn is_char_boundary(&self, offset: usize) -> bool { - if self.chunks.is_empty() { - return offset == 0; - } - let (start, _, item) = self.chunks.find::((), &offset, Bias::Left); - let chunk_offset = offset - start; - item.map(|chunk| chunk.is_char_boundary(chunk_offset)) - .unwrap_or(false) - } - - #[track_caller] - #[inline(always)] - pub fn assert_char_boundary(&self, offset: usize) -> bool { - if self.chunks.is_empty() && offset == 0 { - return true; - } - let (start, _, item) = self.chunks.find::((), &offset, Bias::Left); - match item { - Some(chunk) => { - let chunk_offset = offset - start; - chunk.assert_char_boundary::(chunk_offset) - } - None if PANIC => { - panic!( - "byte index {} is out of bounds of rope (length: {})", - offset, - self.len() - ); - } - None => { - log::error!( - "byte index {} is out of bounds of rope (length: {})", - offset, - self.len() - ); - false - } - } - } - - pub fn floor_char_boundary(&self, index: usize) -> usize { - if index >= self.len() { - self.len() - } else { - let (start, _, item) = self.chunks.find::((), &index, Bias::Left); - let chunk_offset = index - start; - let lower_idx = item.map(|chunk| chunk.text.floor_char_boundary(chunk_offset)); - lower_idx.map_or_else(|| self.len(), |idx| start + idx) - } - } - - pub fn ceil_char_boundary(&self, index: usize) -> usize { - if index > self.len() { - self.len() - } else { - let (start, _, item) = self.chunks.find::((), &index, Bias::Left); - let chunk_offset = index - start; - let upper_idx = item.map(|chunk| chunk.text.ceil_char_boundary(chunk_offset)); - upper_idx.map_or_else(|| self.len(), |idx| start + idx) - } - } - - pub fn append(&mut self, rope: Rope) { - if let Some(chunk) = rope.chunks.first() - && (self - .chunks - .last() - .is_some_and(|c| c.text.len() < chunk::MIN_BASE) - || chunk.text.len() < chunk::MIN_BASE) - { - self.push_chunk(chunk.as_slice()); - - let mut chunks = rope.chunks.cursor::<()>(()); - chunks.next(); - chunks.next(); - self.chunks.append(chunks.suffix(), ()); - } else { - self.chunks.append(rope.chunks, ()); - } - self.check_invariants(); - } - - pub fn replace(&mut self, range: Range, text: &str) { - let mut new_rope = Rope::new(); - let mut cursor = self.cursor(0); - new_rope.append(cursor.slice(range.start)); - cursor.seek_forward(range.end); - new_rope.push(text); - new_rope.append(cursor.suffix()); - *self = new_rope; - } - - pub fn slice(&self, range: Range) -> Rope { - let mut cursor = self.cursor(0); - cursor.seek_forward(range.start); - cursor.slice(range.end) - } - - pub fn slice_rows(&self, range: Range) -> Rope { - // This would be more efficient with a forward advance after the first, but it's fine. - let start = self.point_to_offset(Point::new(range.start, 0)); - let end = self.point_to_offset(Point::new(range.end, 0)); - self.slice(start..end) - } - - pub fn push(&mut self, mut text: &str) { - self.chunks.update_last( - |last_chunk| { - let split_ix = if last_chunk.text.len() + text.len() <= chunk::MAX_BASE { - text.len() - } else { - let mut split_ix = cmp::min( - chunk::MIN_BASE.saturating_sub(last_chunk.text.len()), - text.len(), - ); - while !text.is_char_boundary(split_ix) { - split_ix += 1; - } - split_ix - }; - - let (suffix, remainder) = text.split_at(split_ix); - last_chunk.push_str(suffix); - text = remainder; - }, - (), - ); - - #[cfg(all(test, not(rust_analyzer)))] - const NUM_CHUNKS: usize = 16; - #[cfg(not(all(test, not(rust_analyzer))))] - const NUM_CHUNKS: usize = 4; - - // We accommodate for NUM_CHUNKS chunks of size MAX_BASE - // but given the chunk boundary can land within a character - // we need to accommodate for the worst case where every chunk gets cut short by up to 4 bytes - if text.len() > NUM_CHUNKS * chunk::MAX_BASE - NUM_CHUNKS * 4 { - return self.push_large(text); - } - // 16 is enough as otherwise we will hit the branch above - let mut new_chunks = ArrayVec::<_, NUM_CHUNKS>::new(); - - while !text.is_empty() { - let mut split_ix = cmp::min(chunk::MAX_BASE, text.len()); - while !text.is_char_boundary(split_ix) { - split_ix -= 1; - } - let (chunk, remainder) = text.split_at(split_ix); - new_chunks.push(chunk); - text = remainder; - } - self.chunks - .extend(new_chunks.into_iter().map(Chunk::new), ()); - - self.check_invariants(); - } - - /// A copy of `push` specialized for working with large quantities of text. - fn push_large(&mut self, mut text: &str) { - // To avoid frequent reallocs when loading large swaths of file contents, - // we estimate worst-case `new_chunks` capacity; - // Chunk is a fixed-capacity buffer. If a character falls on - // chunk boundary, we push it off to the following chunk (thus leaving a small bit of capacity unfilled in current chunk). - // Worst-case chunk count when loading a file is then a case where every chunk ends up with that unused capacity. - // Since we're working with UTF-8, each character is at most 4 bytes wide. It follows then that the worst case is where - // a chunk ends with 3 bytes of a 4-byte character. These 3 bytes end up being stored in the following chunk, thus wasting - // 3 bytes of storage in current chunk. - // For example, a 1024-byte string can occupy between 32 (full ASCII, 1024/32) and 36 (full 4-byte UTF-8, 1024 / 29 rounded up) chunks. - const MIN_CHUNK_SIZE: usize = chunk::MAX_BASE - 3; - - // We also round up the capacity up by one, for a good measure; we *really* don't want to realloc here, as we assume that the # of characters - // we're working with there is large. - let capacity = text.len().div_ceil(MIN_CHUNK_SIZE); - let mut new_chunks = Vec::with_capacity(capacity); - - while !text.is_empty() { - let mut split_ix = cmp::min(chunk::MAX_BASE, text.len()); - while !text.is_char_boundary(split_ix) { - split_ix -= 1; - } - let (chunk, remainder) = text.split_at(split_ix); - new_chunks.push(chunk); - text = remainder; - } - - #[cfg(all(test, not(rust_analyzer)))] - const PARALLEL_THRESHOLD: usize = 4; - #[cfg(not(all(test, not(rust_analyzer))))] - const PARALLEL_THRESHOLD: usize = 4 * (2 * sum_tree::TREE_BASE); - - if new_chunks.len() >= PARALLEL_THRESHOLD { - self.chunks - .par_extend(new_chunks.into_par_iter().map(Chunk::new), ()); - } else { - self.chunks - .extend(new_chunks.into_iter().map(Chunk::new), ()); - } - - self.check_invariants(); - } - - fn push_chunk(&mut self, mut chunk: ChunkSlice) { - self.chunks.update_last( - |last_chunk| { - let split_ix = if last_chunk.text.len() + chunk.len() <= chunk::MAX_BASE { - chunk.len() - } else { - let mut split_ix = cmp::min( - chunk::MIN_BASE.saturating_sub(last_chunk.text.len()), - chunk.len(), - ); - while !chunk.is_char_boundary(split_ix) { - split_ix += 1; - } - split_ix - }; - - let (suffix, remainder) = chunk.split_at(split_ix); - last_chunk.append(suffix); - chunk = remainder; - }, - (), - ); - - if !chunk.is_empty() { - self.chunks.push(chunk.into(), ()); - } - } - - pub fn push_front(&mut self, text: &str) { - let suffix = mem::replace(self, Rope::from(text)); - self.append(suffix); - } - - fn check_invariants(&self) { - #[cfg(test)] - { - // Ensure all chunks except maybe the last one are not underflowing. - // Allow some wiggle room for multibyte characters at chunk boundaries. - let mut chunks = self.chunks.cursor::<()>(()).peekable(); - while let Some(chunk) = chunks.next() { - if chunks.peek().is_some() { - assert!(chunk.text.len() + 3 >= chunk::MIN_BASE); - } - } - } - } - - pub fn summary(&self) -> TextSummary { - self.chunks.summary().text - } - - pub fn len(&self) -> usize { - self.chunks.extent(()) - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - pub fn max_point(&self) -> Point { - self.chunks.extent(()) - } - - pub fn max_point_utf16(&self) -> PointUtf16 { - self.chunks.extent(()) - } - - pub fn cursor(&self, offset: usize) -> Cursor<'_> { - Cursor::new(self, offset) - } - - pub fn chars(&self) -> impl Iterator + '_ { - self.chars_at(0) - } - - pub fn chars_at(&self, start: usize) -> impl Iterator + '_ { - self.chunks_in_range(start..self.len()).flat_map(str::chars) - } - - pub fn reversed_chars_at(&self, start: usize) -> impl Iterator + '_ { - self.reversed_chunks_in_range(0..start) - .flat_map(|chunk| chunk.chars().rev()) - } - - pub fn bytes_in_range(&self, range: Range) -> Bytes<'_> { - Bytes::new(self, range, false) - } - - pub fn reversed_bytes_in_range(&self, range: Range) -> Bytes<'_> { - Bytes::new(self, range, true) - } - - pub fn chunks(&self) -> Chunks<'_> { - self.chunks_in_range(0..self.len()) - } - - pub fn chunks_in_range(&self, range: Range) -> Chunks<'_> { - Chunks::new(self, range, false) - } - - pub fn reversed_chunks_in_range(&self, range: Range) -> Chunks<'_> { - Chunks::new(self, range, true) - } - - pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { - if offset >= self.summary().len { - return self.summary().len_utf16; - } - let (start, _, item) = - self.chunks - .find::, _>((), &offset, Bias::Left); - let overshoot = offset - start.0; - start.1 - + item.map_or(Default::default(), |chunk| { - chunk.as_slice().offset_to_offset_utf16(overshoot) - }) - } - - pub fn offset_utf16_to_offset(&self, offset: OffsetUtf16) -> usize { - if offset >= self.summary().len_utf16 { - return self.summary().len; - } - let (start, _, item) = - self.chunks - .find::, _>((), &offset, Bias::Left); - let overshoot = offset - start.0; - start.1 - + item.map_or(Default::default(), |chunk| { - chunk.as_slice().offset_utf16_to_offset(overshoot) - }) - } - - pub fn offset_to_point(&self, offset: usize) -> Point { - if offset >= self.summary().len { - return self.summary().lines; - } - let (start, _, item) = - self.chunks - .find::, _>((), &offset, Bias::Left); - let overshoot = offset - start.0; - start.1 - + item.map_or(Point::zero(), |chunk| { - chunk.as_slice().offset_to_point(overshoot) - }) - } - - pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 { - if offset >= self.summary().len { - return self.summary().lines_utf16(); - } - let (start, _, item) = - self.chunks - .find::, _>((), &offset, Bias::Left); - let overshoot = offset - start.0; - start.1 - + item.map_or(PointUtf16::zero(), |chunk| { - chunk.as_slice().offset_to_point_utf16(overshoot) - }) - } - - pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 { - if point >= self.summary().lines { - return self.summary().lines_utf16(); - } - let (start, _, item) = - self.chunks - .find::, _>((), &point, Bias::Left); - let overshoot = point - start.0; - start.1 - + item.map_or(PointUtf16::zero(), |chunk| { - chunk.as_slice().point_to_point_utf16(overshoot) - }) - } - - pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point { - if point >= self.summary().lines_utf16() { - return self.summary().lines; - } - let mut cursor = self.chunks.cursor::>(()); - cursor.seek(&point, Bias::Left); - let overshoot = point - cursor.start().0; - cursor.start().1 - + cursor.item().map_or(Point::zero(), |chunk| { - chunk - .as_slice() - .offset_to_point(chunk.as_slice().point_utf16_to_offset(overshoot, false)) - }) - } - - #[instrument(skip_all)] - pub fn point_to_offset(&self, point: Point) -> usize { - if point >= self.summary().lines { - return self.summary().len; - } - let (start, _, item) = - self.chunks - .find::, _>((), &point, Bias::Left); - let overshoot = point - start.0; - start.1 + item.map_or(0, |chunk| chunk.as_slice().point_to_offset(overshoot)) - } - - pub fn point_to_offset_utf16(&self, point: Point) -> OffsetUtf16 { - if point >= self.summary().lines { - return self.summary().len_utf16; - } - let mut cursor = self.chunks.cursor::>(()); - cursor.seek(&point, Bias::Left); - let overshoot = point - cursor.start().0; - cursor.start().1 - + cursor.item().map_or(OffsetUtf16(0), |chunk| { - chunk.as_slice().point_to_offset_utf16(overshoot) - }) - } - - pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { - self.point_utf16_to_offset_impl(point, false) - } - - pub fn point_utf16_to_offset_utf16(&self, point: PointUtf16) -> OffsetUtf16 { - self.point_utf16_to_offset_utf16_impl(point, false) - } - - pub fn unclipped_point_utf16_to_offset(&self, point: Unclipped) -> usize { - self.point_utf16_to_offset_impl(point.0, true) - } - - fn point_utf16_to_offset_impl(&self, point: PointUtf16, clip: bool) -> usize { - if point >= self.summary().lines_utf16() { - return self.summary().len; - } - let (start, _, item) = - self.chunks - .find::, _>((), &point, Bias::Left); - let overshoot = point - start.0; - start.1 - + item.map_or(0, |chunk| { - chunk.as_slice().point_utf16_to_offset(overshoot, clip) - }) - } - - fn point_utf16_to_offset_utf16_impl(&self, point: PointUtf16, clip: bool) -> OffsetUtf16 { - if point >= self.summary().lines_utf16() { - return self.summary().len_utf16; - } - let mut cursor = self - .chunks - .cursor::>(()); - cursor.seek(&point, Bias::Left); - let overshoot = point - cursor.start().0; - cursor.start().1 - + cursor.item().map_or(OffsetUtf16(0), |chunk| { - chunk - .as_slice() - .offset_to_offset_utf16(chunk.as_slice().point_utf16_to_offset(overshoot, clip)) - }) - } - - pub fn unclipped_point_utf16_to_point(&self, point: Unclipped) -> Point { - if point.0 >= self.summary().lines_utf16() { - return self.summary().lines; - } - let (start, _, item) = - self.chunks - .find::, _>((), &point.0, Bias::Left); - let overshoot = Unclipped(point.0 - start.0); - start.1 - + item.map_or(Point::zero(), |chunk| { - chunk.as_slice().unclipped_point_utf16_to_point(overshoot) - }) - } - - pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { - match bias { - Bias::Left => self.floor_char_boundary(offset), - Bias::Right => self.ceil_char_boundary(offset), - } - } - - pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 { - let (start, _, item) = self.chunks.find::((), &offset, Bias::Right); - if let Some(chunk) = item { - let overshoot = offset - start; - start + chunk.as_slice().clip_offset_utf16(overshoot, bias) - } else { - self.summary().len_utf16 - } - } - - pub fn clip_point(&self, point: Point, bias: Bias) -> Point { - let (start, _, item) = self.chunks.find::((), &point, Bias::Right); - if let Some(chunk) = item { - let overshoot = point - start; - start + chunk.as_slice().clip_point(overshoot, bias) - } else { - self.summary().lines - } - } - - pub fn clip_point_utf16(&self, point: Unclipped, bias: Bias) -> PointUtf16 { - let (start, _, item) = self.chunks.find::((), &point.0, Bias::Right); - if let Some(chunk) = item { - let overshoot = Unclipped(point.0 - start); - start + chunk.as_slice().clip_point_utf16(overshoot, bias) - } else { - self.summary().lines_utf16() - } - } - - pub fn line_len(&self, row: u32) -> u32 { - self.clip_point(Point::new(row, u32::MAX), Bias::Left) - .column - } -} - -impl<'a> From<&'a str> for Rope { - fn from(text: &'a str) -> Self { - let mut rope = Self::new(); - rope.push(text); - rope - } -} - -impl<'a> FromIterator<&'a str> for Rope { - fn from_iter>(iter: T) -> Self { - let mut rope = Rope::new(); - for chunk in iter { - rope.push(chunk); - } - rope - } -} - -impl From for Rope { - #[inline(always)] - fn from(text: String) -> Self { - Rope::from(text.as_str()) - } -} - -impl From<&String> for Rope { - #[inline(always)] - fn from(text: &String) -> Self { - Rope::from(text.as_str()) - } -} - -impl fmt::Display for Rope { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for chunk in self.chunks() { - write!(f, "{}", chunk)?; - } - Ok(()) - } -} - -impl fmt::Debug for Rope { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use std::fmt::Write as _; - - write!(f, "\"")?; - let mut format_string = String::new(); - for chunk in self.chunks() { - write!(&mut format_string, "{:?}", chunk)?; - write!(f, "{}", &format_string[1..format_string.len() - 1])?; - format_string.clear(); - } - write!(f, "\"")?; - Ok(()) - } -} - -pub struct Cursor<'a> { - rope: &'a Rope, - chunks: sum_tree::Cursor<'a, 'static, Chunk, usize>, - offset: usize, -} - -impl<'a> Cursor<'a> { - pub fn new(rope: &'a Rope, offset: usize) -> Self { - let mut chunks = rope.chunks.cursor(()); - chunks.seek(&offset, Bias::Right); - Self { - rope, - chunks, - offset, - } - } - - pub fn seek_forward(&mut self, end_offset: usize) { - debug_assert!(end_offset >= self.offset); - - self.chunks.seek_forward(&end_offset, Bias::Right); - self.offset = end_offset; - } - - pub fn slice(&mut self, end_offset: usize) -> Rope { - debug_assert!( - end_offset >= self.offset, - "cannot slice backwards from {} to {}", - self.offset, - end_offset - ); - - let mut slice = Rope::new(); - if let Some(start_chunk) = self.chunks.item() { - let start_ix = self.offset - self.chunks.start(); - let end_ix = cmp::min(end_offset, self.chunks.end()) - self.chunks.start(); - slice.push_chunk(start_chunk.slice(start_ix..end_ix)); - } - - if end_offset > self.chunks.end() { - self.chunks.next(); - slice.append(Rope { - chunks: self.chunks.slice(&end_offset, Bias::Right), - }); - if let Some(end_chunk) = self.chunks.item() { - let end_ix = end_offset - self.chunks.start(); - slice.push_chunk(end_chunk.slice(0..end_ix)); - } - } - - self.offset = end_offset; - slice - } - - pub fn summary(&mut self, end_offset: usize) -> D { - debug_assert!(end_offset >= self.offset); - - let mut summary = D::zero(()); - if let Some(start_chunk) = self.chunks.item() { - let start_ix = self.offset - self.chunks.start(); - let end_ix = cmp::min(end_offset, self.chunks.end()) - self.chunks.start(); - summary.add_assign(&D::from_chunk(start_chunk.slice(start_ix..end_ix))); - } - - if end_offset > self.chunks.end() { - self.chunks.next(); - summary.add_assign(&self.chunks.summary(&end_offset, Bias::Right)); - if let Some(end_chunk) = self.chunks.item() { - let end_ix = end_offset - self.chunks.start(); - summary.add_assign(&D::from_chunk(end_chunk.slice(0..end_ix))); - } - } - - self.offset = end_offset; - summary - } - - pub fn suffix(mut self) -> Rope { - self.slice(self.rope.chunks.extent(())) - } - - pub fn offset(&self) -> usize { - self.offset - } -} - -pub struct ChunkBitmaps<'a> { - /// A slice of text up to 128 bytes in size - pub text: &'a str, - /// Bitmap of character locations in text. LSB ordered - pub chars: Bitmap, - /// Bitmap of tab locations in text. LSB ordered - pub tabs: Bitmap, -} - -#[derive(Clone)] -pub struct Chunks<'a> { - chunks: sum_tree::Cursor<'a, 'static, Chunk, usize>, - range: Range, - offset: usize, - reversed: bool, -} - -impl<'a> Chunks<'a> { - pub fn new(rope: &'a Rope, range: Range, reversed: bool) -> Self { - let mut chunks = rope.chunks.cursor(()); - let offset = if reversed { - chunks.seek(&range.end, Bias::Left); - range.end - } else { - chunks.seek(&range.start, Bias::Right); - range.start - }; - let chunk_offset = offset - chunks.start(); - if let Some(chunk) = chunks.item() { - chunk.assert_char_boundary::(chunk_offset); - } - Self { - chunks, - range, - offset, - reversed, - } - } - - fn offset_is_valid(&self) -> bool { - if self.reversed { - if self.offset <= self.range.start || self.offset > self.range.end { - return false; - } - } else if self.offset < self.range.start || self.offset >= self.range.end { - return false; - } - - true - } - - pub fn offset(&self) -> usize { - self.offset - } - - pub fn seek(&mut self, mut offset: usize) { - offset = offset.clamp(self.range.start, self.range.end); - - if self.reversed { - if offset > self.chunks.end() { - self.chunks.seek_forward(&offset, Bias::Left); - } else if offset <= *self.chunks.start() { - self.chunks.seek(&offset, Bias::Left); - } - } else { - if offset >= self.chunks.end() { - self.chunks.seek_forward(&offset, Bias::Right); - } else if offset < *self.chunks.start() { - self.chunks.seek(&offset, Bias::Right); - } - }; - - self.offset = offset; - } - - pub fn set_range(&mut self, range: Range) { - self.range = range.clone(); - self.seek(range.start); - } - - /// Moves this cursor to the start of the next line in the rope. - /// - /// This method advances the cursor to the beginning of the next line. - /// If the cursor is already at the end of the rope, this method does nothing. - /// Reversed chunks iterators are not currently supported and will panic. - /// - /// Returns `true` if the cursor was successfully moved to the next line start, - /// or `false` if the cursor was already at the end of the rope. - pub fn next_line(&mut self) -> bool { - assert!(!self.reversed); - - let mut found = false; - if let Some(chunk) = self.peek() { - if let Some(newline_ix) = chunk.find('\n') { - self.offset += newline_ix + 1; - found = self.offset <= self.range.end; - } else { - self.chunks - .search_forward(|summary| summary.text.lines.row > 0); - self.offset = *self.chunks.start(); - - if let Some(newline_ix) = self.peek().and_then(|chunk| chunk.find('\n')) { - self.offset += newline_ix + 1; - found = self.offset <= self.range.end; - } else { - self.offset = self.chunks.end(); - } - } - - if self.offset == self.chunks.end() { - self.next(); - } - } - - if self.offset > self.range.end { - self.offset = cmp::min(self.offset, self.range.end); - self.chunks.seek(&self.offset, Bias::Right); - } - - found - } - - /// Move this cursor to the preceding position in the rope that starts a new line. - /// Reversed chunks iterators are not currently supported and will panic. - /// - /// If this cursor is not on the start of a line, it will be moved to the start of - /// its current line. Otherwise it will be moved to the start of the previous line. - /// It updates the cursor's position and returns true if a previous line was found, - /// or false if the cursor was already at the start of the rope. - pub fn prev_line(&mut self) -> bool { - assert!(!self.reversed); - - let initial_offset = self.offset; - - if self.offset == *self.chunks.start() { - self.chunks.prev(); - } - - if let Some(chunk) = self.chunks.item() { - let mut end_ix = self.offset - *self.chunks.start(); - if chunk.text.as_bytes()[end_ix - 1] == b'\n' { - end_ix -= 1; - } - - if let Some(newline_ix) = chunk.text[..end_ix].rfind('\n') { - self.offset = *self.chunks.start() + newline_ix + 1; - if self.offset_is_valid() { - return true; - } - } - } - - self.chunks - .search_backward(|summary| summary.text.lines.row > 0); - self.offset = *self.chunks.start(); - if let Some(chunk) = self.chunks.item() - && let Some(newline_ix) = chunk.text.rfind('\n') - { - self.offset += newline_ix + 1; - if self.offset_is_valid() { - if self.offset == self.chunks.end() { - self.chunks.next(); - } - - return true; - } - } - - if !self.offset_is_valid() || self.chunks.item().is_none() { - self.offset = self.range.start; - self.chunks.seek(&self.offset, Bias::Right); - } - - self.offset < initial_offset && self.offset == 0 - } - - pub fn peek(&self) -> Option<&'a str> { - if !self.offset_is_valid() { - return None; - } - - let chunk = self.chunks.item()?; - let chunk_start = *self.chunks.start(); - let slice_range = if self.reversed { - let slice_start = cmp::max(chunk_start, self.range.start) - chunk_start; - let slice_end = self.offset - chunk_start; - slice_start..slice_end - } else { - let slice_start = self.offset - chunk_start; - let slice_end = cmp::min(self.chunks.end(), self.range.end) - chunk_start; - slice_start..slice_end - }; - - Some(&chunk.text[slice_range]) - } - - /// Returns bitmaps that represent character positions and tab positions - pub fn peek_with_bitmaps(&self) -> Option> { - if !self.offset_is_valid() { - return None; - } - - let chunk = self.chunks.item()?; - let chunk_start = *self.chunks.start(); - let slice_range = if self.reversed { - let slice_start = cmp::max(chunk_start, self.range.start) - chunk_start; - let slice_end = self.offset - chunk_start; - slice_start..slice_end - } else { - let slice_start = self.offset - chunk_start; - let slice_end = cmp::min(self.chunks.end(), self.range.end) - chunk_start; - slice_start..slice_end - }; - let chunk_start_offset = slice_range.start; - let slice_text = &chunk.text[slice_range]; - - // Shift the tabs to align with our slice window - let shifted_tabs = chunk.tabs() >> chunk_start_offset; - let shifted_chars = chunk.chars() >> chunk_start_offset; - - Some(ChunkBitmaps { - text: slice_text, - chars: shifted_chars, - tabs: shifted_tabs, - }) - } - - pub fn lines(self) -> Lines<'a> { - let reversed = self.reversed; - Lines { - chunks: self, - current_line: String::new(), - done: false, - reversed, - } - } - - pub fn equals_str(&self, other: &str) -> bool { - let chunk = self.clone(); - if chunk.reversed { - let mut offset = other.len(); - for chunk in chunk { - if other[0..offset].ends_with(chunk) { - offset -= chunk.len(); - } else { - return false; - } - } - if offset != 0 { - return false; - } - } else { - let mut offset = 0; - for chunk in chunk { - if offset >= other.len() { - return false; - } - if other[offset..].starts_with(chunk) { - offset += chunk.len(); - } else { - return false; - } - } - if offset != other.len() { - return false; - } - } - - true - } -} - -pub struct ChunkWithBitmaps<'a>(pub Chunks<'a>); - -impl<'a> Iterator for ChunkWithBitmaps<'a> { - /// text, chars bitmap, tabs bitmap - type Item = ChunkBitmaps<'a>; - - fn next(&mut self) -> Option { - let chunk_bitmaps = self.0.peek_with_bitmaps()?; - if self.0.reversed { - self.0.offset -= chunk_bitmaps.text.len(); - if self.0.offset <= *self.0.chunks.start() { - self.0.chunks.prev(); - } - } else { - self.0.offset += chunk_bitmaps.text.len(); - if self.0.offset >= self.0.chunks.end() { - self.0.chunks.next(); - } - } - - Some(chunk_bitmaps) - } -} - -impl<'a> Iterator for Chunks<'a> { - type Item = &'a str; - - fn next(&mut self) -> Option { - let chunk = self.peek()?; - if self.reversed { - self.offset -= chunk.len(); - if self.offset <= *self.chunks.start() { - self.chunks.prev(); - } - } else { - self.offset += chunk.len(); - if self.offset >= self.chunks.end() { - self.chunks.next(); - } - } - - Some(chunk) - } -} - -pub struct Bytes<'a> { - chunks: sum_tree::Cursor<'a, 'static, Chunk, usize>, - range: Range, - reversed: bool, -} - -impl<'a> Bytes<'a> { - pub fn new(rope: &'a Rope, range: Range, reversed: bool) -> Self { - let mut chunks = rope.chunks.cursor(()); - if reversed { - chunks.seek(&range.end, Bias::Left); - } else { - chunks.seek(&range.start, Bias::Right); - } - Self { - chunks, - range, - reversed, - } - } - - pub fn peek(&self) -> Option<&'a [u8]> { - let chunk = self.chunks.item()?; - if self.reversed && self.range.start >= self.chunks.end() { - return None; - } - let chunk_start = *self.chunks.start(); - if self.range.end <= chunk_start { - return None; - } - let start = self.range.start.saturating_sub(chunk_start); - let end = self.range.end - chunk_start; - Some(&chunk.text.as_bytes()[start..chunk.text.len().min(end)]) - } -} - -impl<'a> Iterator for Bytes<'a> { - type Item = &'a [u8]; - - fn next(&mut self) -> Option { - let result = self.peek(); - if result.is_some() { - if self.reversed { - self.chunks.prev(); - } else { - self.chunks.next(); - } - } - result - } -} - -impl io::Read for Bytes<'_> { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - if let Some(chunk) = self.peek() { - let len = cmp::min(buf.len(), chunk.len()); - if self.reversed { - buf[..len].copy_from_slice(&chunk[chunk.len() - len..]); - buf[..len].reverse(); - self.range.end -= len; - } else { - buf[..len].copy_from_slice(&chunk[..len]); - self.range.start += len; - } - - if len == chunk.len() { - if self.reversed { - self.chunks.prev(); - } else { - self.chunks.next(); - } - } - Ok(len) - } else { - Ok(0) - } - } -} - -pub struct Lines<'a> { - chunks: Chunks<'a>, - current_line: String, - done: bool, - reversed: bool, -} - -impl<'a> Lines<'a> { - pub fn next(&mut self) -> Option<&str> { - if self.done { - return None; - } - - self.current_line.clear(); - - while let Some(chunk) = self.chunks.peek() { - let chunk_lines = chunk.split('\n'); - if self.reversed { - let mut chunk_lines = chunk_lines.rev().peekable(); - if let Some(chunk_line) = chunk_lines.next() { - let done = chunk_lines.peek().is_some(); - if done { - self.chunks - .seek(self.chunks.offset() - chunk_line.len() - "\n".len()); - if self.current_line.is_empty() { - return Some(chunk_line); - } - } - self.current_line.insert_str(0, chunk_line); - if done { - return Some(&self.current_line); - } - } - } else { - let mut chunk_lines = chunk_lines.peekable(); - if let Some(chunk_line) = chunk_lines.next() { - let done = chunk_lines.peek().is_some(); - if done { - self.chunks - .seek(self.chunks.offset() + chunk_line.len() + "\n".len()); - if self.current_line.is_empty() { - return Some(chunk_line); - } - } - self.current_line.push_str(chunk_line); - if done { - return Some(&self.current_line); - } - } - } - - self.chunks.next(); - } - - self.done = true; - Some(&self.current_line) - } - - pub fn seek(&mut self, offset: usize) { - self.chunks.seek(offset); - self.current_line.clear(); - self.done = false; - } - - pub fn offset(&self) -> usize { - self.chunks.offset() - } -} - -impl sum_tree::Item for Chunk { - type Summary = ChunkSummary; - - fn summary(&self, _cx: ()) -> Self::Summary { - ChunkSummary { - text: self.as_slice().text_summary(), - } - } -} - -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct ChunkSummary { - text: TextSummary, -} - -impl sum_tree::ContextLessSummary for ChunkSummary { - fn zero() -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &Self) { - self.text += &summary.text; - } -} - -/// Summary of a string of text. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub struct TextSummary { - /// Length in bytes. - pub len: usize, - /// Length in UTF-8. - pub chars: usize, - /// Length in UTF-16 code units - pub len_utf16: OffsetUtf16, - /// A point representing the number of lines and the length of the last line. - /// - /// In other words, it marks the point after the last byte in the text, (if - /// EOF was a character, this would be its position). - pub lines: Point, - /// How many `char`s are in the first line - pub first_line_chars: u32, - /// How many `char`s are in the last line - pub last_line_chars: u32, - /// How many UTF-16 code units are in the last line - pub last_line_len_utf16: u32, - /// The row idx of the longest row - pub longest_row: u32, - /// How many `char`s are in the longest row - pub longest_row_chars: u32, -} - -impl TextSummary { - pub fn lines_utf16(&self) -> PointUtf16 { - PointUtf16 { - row: self.lines.row, - column: self.last_line_len_utf16, - } - } - - pub fn newline() -> Self { - Self { - len: 1, - chars: 1, - len_utf16: OffsetUtf16(1), - first_line_chars: 0, - last_line_chars: 0, - last_line_len_utf16: 0, - lines: Point::new(1, 0), - longest_row: 0, - longest_row_chars: 0, - } - } - - pub fn add_newline(&mut self) { - self.len += 1; - self.len_utf16 += OffsetUtf16(self.len_utf16.0 + 1); - self.last_line_chars = 0; - self.last_line_len_utf16 = 0; - self.lines += Point::new(1, 0); - } -} - -impl<'a> From<&'a str> for TextSummary { - fn from(text: &'a str) -> Self { - let mut len_utf16 = OffsetUtf16(0); - let mut lines = Point::new(0, 0); - let mut first_line_chars = 0; - let mut last_line_chars = 0; - let mut last_line_len_utf16 = 0; - let mut longest_row = 0; - let mut longest_row_chars = 0; - let mut chars = 0; - for c in text.chars() { - chars += 1; - len_utf16.0 += c.len_utf16(); - - if c == '\n' { - lines += Point::new(1, 0); - last_line_len_utf16 = 0; - last_line_chars = 0; - } else { - lines.column += c.len_utf8() as u32; - last_line_len_utf16 += c.len_utf16() as u32; - last_line_chars += 1; - } - - if lines.row == 0 { - first_line_chars = last_line_chars; - } - - if last_line_chars > longest_row_chars { - longest_row = lines.row; - longest_row_chars = last_line_chars; - } - } - - TextSummary { - len: text.len(), - chars, - len_utf16, - lines, - first_line_chars, - last_line_chars, - last_line_len_utf16, - longest_row, - longest_row_chars, - } - } -} - -impl sum_tree::ContextLessSummary for TextSummary { - fn zero() -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &Self) { - *self += summary; - } -} - -impl ops::Add for TextSummary { - type Output = Self; - - fn add(mut self, rhs: Self) -> Self::Output { - AddAssign::add_assign(&mut self, &rhs); - self - } -} - -impl<'a> ops::AddAssign<&'a Self> for TextSummary { - fn add_assign(&mut self, other: &'a Self) { - let joined_chars = self.last_line_chars + other.first_line_chars; - if joined_chars > self.longest_row_chars { - self.longest_row = self.lines.row; - self.longest_row_chars = joined_chars; - } - if other.longest_row_chars > self.longest_row_chars { - self.longest_row = self.lines.row + other.longest_row; - self.longest_row_chars = other.longest_row_chars; - } - - if self.lines.row == 0 { - self.first_line_chars += other.first_line_chars; - } - - if other.lines.row == 0 { - self.last_line_chars += other.first_line_chars; - self.last_line_len_utf16 += other.last_line_len_utf16; - } else { - self.last_line_chars = other.last_line_chars; - self.last_line_len_utf16 = other.last_line_len_utf16; - } - - self.chars += other.chars; - self.len += other.len; - self.len_utf16 += other.len_utf16; - self.lines += other.lines; - } -} - -impl ops::AddAssign for TextSummary { - fn add_assign(&mut self, other: Self) { - *self += &other; - } -} - -pub trait TextDimension: - 'static + Clone + Copy + Default + for<'a> Dimension<'a, ChunkSummary> + std::fmt::Debug -{ - fn from_text_summary(summary: &TextSummary) -> Self; - fn from_chunk(chunk: ChunkSlice) -> Self; - fn add_assign(&mut self, other: &Self); -} - -impl TextDimension for Dimensions { - fn from_text_summary(summary: &TextSummary) -> Self { - Dimensions( - D1::from_text_summary(summary), - D2::from_text_summary(summary), - (), - ) - } - - fn from_chunk(chunk: ChunkSlice) -> Self { - Dimensions(D1::from_chunk(chunk), D2::from_chunk(chunk), ()) - } - - fn add_assign(&mut self, other: &Self) { - self.0.add_assign(&other.0); - self.1.add_assign(&other.1); - } -} - -impl<'a> sum_tree::Dimension<'a, ChunkSummary> for TextSummary { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a ChunkSummary, _: ()) { - *self += &summary.text; - } -} - -impl TextDimension for TextSummary { - fn from_text_summary(summary: &TextSummary) -> Self { - *summary - } - - fn from_chunk(chunk: ChunkSlice) -> Self { - chunk.text_summary() - } - - fn add_assign(&mut self, other: &Self) { - *self += other; - } -} - -impl<'a> sum_tree::Dimension<'a, ChunkSummary> for usize { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a ChunkSummary, _: ()) { - *self += summary.text.len; - } -} - -impl TextDimension for usize { - fn from_text_summary(summary: &TextSummary) -> Self { - summary.len - } - - fn from_chunk(chunk: ChunkSlice) -> Self { - chunk.len() - } - - fn add_assign(&mut self, other: &Self) { - *self += other; - } -} - -impl<'a> sum_tree::Dimension<'a, ChunkSummary> for OffsetUtf16 { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a ChunkSummary, _: ()) { - *self += summary.text.len_utf16; - } -} - -impl TextDimension for OffsetUtf16 { - fn from_text_summary(summary: &TextSummary) -> Self { - summary.len_utf16 - } - - fn from_chunk(chunk: ChunkSlice) -> Self { - chunk.len_utf16() - } - - fn add_assign(&mut self, other: &Self) { - *self += other; - } -} - -impl<'a> sum_tree::Dimension<'a, ChunkSummary> for Point { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a ChunkSummary, _: ()) { - *self += summary.text.lines; - } -} - -impl TextDimension for Point { - fn from_text_summary(summary: &TextSummary) -> Self { - summary.lines - } - - fn from_chunk(chunk: ChunkSlice) -> Self { - chunk.lines() - } - - fn add_assign(&mut self, other: &Self) { - *self += other; - } -} - -impl<'a> sum_tree::Dimension<'a, ChunkSummary> for PointUtf16 { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a ChunkSummary, _: ()) { - *self += summary.text.lines_utf16(); - } -} - -impl TextDimension for PointUtf16 { - fn from_text_summary(summary: &TextSummary) -> Self { - summary.lines_utf16() - } - - fn from_chunk(chunk: ChunkSlice) -> Self { - PointUtf16 { - row: chunk.lines().row, - column: chunk.last_line_len_utf16(), - } - } - - fn add_assign(&mut self, other: &Self) { - *self += other; - } -} - -/// A pair of text dimensions in which only the first dimension is used for comparison, -/// but both dimensions are updated during addition and subtraction. -#[derive(Clone, Copy, Debug)] -pub struct DimensionPair { - pub key: K, - pub value: Option, -} - -impl Default for DimensionPair { - fn default() -> Self { - Self { - key: Default::default(), - value: Some(Default::default()), - } - } -} - -impl cmp::Ord for DimensionPair -where - K: cmp::Ord, -{ - fn cmp(&self, other: &Self) -> cmp::Ordering { - self.key.cmp(&other.key) - } -} - -impl cmp::PartialOrd for DimensionPair -where - K: cmp::PartialOrd, -{ - fn partial_cmp(&self, other: &Self) -> Option { - self.key.partial_cmp(&other.key) - } -} - -impl cmp::PartialEq for DimensionPair -where - K: cmp::PartialEq, -{ - fn eq(&self, other: &Self) -> bool { - self.key.eq(&other.key) - } -} - -impl ops::Sub for DimensionPair -where - K: ops::Sub, - V: ops::Sub, -{ - type Output = DimensionPair; - - fn sub(self, rhs: Self) -> Self::Output { - DimensionPair { - key: self.key - rhs.key, - value: self.value.zip(rhs.value).map(|(a, b)| a - b), - } - } -} - -impl ops::AddAssign> for DimensionPair -where - K: ops::AddAssign, - V: ops::AddAssign, -{ - fn add_assign(&mut self, rhs: DimensionPair) { - self.key += rhs.key; - if let Some(value) = &mut self.value { - if let Some(other_value) = rhs.value { - *value += other_value; - } else { - self.value.take(); - } - } - } -} - -impl std::ops::AddAssign> for Point { - fn add_assign(&mut self, rhs: DimensionPair) { - *self += rhs.key; - } -} - -impl cmp::Eq for DimensionPair where K: cmp::Eq {} - -impl<'a, K, V, S> sum_tree::Dimension<'a, S> for DimensionPair -where - S: sum_tree::Summary, - K: sum_tree::Dimension<'a, S>, - V: sum_tree::Dimension<'a, S>, -{ - fn zero(cx: S::Context<'_>) -> Self { - Self { - key: K::zero(cx), - value: Some(V::zero(cx)), - } - } - - fn add_summary(&mut self, summary: &'a S, cx: S::Context<'_>) { - self.key.add_summary(summary, cx); - if let Some(value) = &mut self.value { - value.add_summary(summary, cx); - } - } -} - -impl TextDimension for DimensionPair -where - K: TextDimension, - V: TextDimension, -{ - fn add_assign(&mut self, other: &Self) { - self.key.add_assign(&other.key); - if let Some(value) = &mut self.value { - if let Some(other_value) = other.value.as_ref() { - value.add_assign(other_value); - } else { - self.value.take(); - } - } - } - - fn from_chunk(chunk: ChunkSlice) -> Self { - Self { - key: K::from_chunk(chunk), - value: Some(V::from_chunk(chunk)), - } - } - - fn from_text_summary(summary: &TextSummary) -> Self { - Self { - key: K::from_text_summary(summary), - value: Some(V::from_text_summary(summary)), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use Bias::{Left, Right}; - use rand::prelude::*; - use std::{cmp::Ordering, env, io::Read}; - use util::RandomCharIter; - - #[ctor::ctor] - fn init_logger() { - zlog::init_test(); - } - - #[test] - fn test_all_4_byte_chars() { - let mut rope = Rope::new(); - let text = "🏀".repeat(256); - rope.push(&text); - assert_eq!(rope.text(), text); - } - - #[test] - fn test_clip() { - let rope = Rope::from("🧘"); - - assert_eq!(rope.clip_offset(1, Bias::Left), 0); - assert_eq!(rope.clip_offset(1, Bias::Right), 4); - assert_eq!(rope.clip_offset(5, Bias::Right), 4); - - assert_eq!( - rope.clip_point(Point::new(0, 1), Bias::Left), - Point::new(0, 0) - ); - assert_eq!( - rope.clip_point(Point::new(0, 1), Bias::Right), - Point::new(0, 4) - ); - assert_eq!( - rope.clip_point(Point::new(0, 5), Bias::Right), - Point::new(0, 4) - ); - - assert_eq!( - rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 1)), Bias::Left), - PointUtf16::new(0, 0) - ); - assert_eq!( - rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 1)), Bias::Right), - PointUtf16::new(0, 2) - ); - assert_eq!( - rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 3)), Bias::Right), - PointUtf16::new(0, 2) - ); - - assert_eq!( - rope.clip_offset_utf16(OffsetUtf16(1), Bias::Left), - OffsetUtf16(0) - ); - assert_eq!( - rope.clip_offset_utf16(OffsetUtf16(1), Bias::Right), - OffsetUtf16(2) - ); - assert_eq!( - rope.clip_offset_utf16(OffsetUtf16(3), Bias::Right), - OffsetUtf16(2) - ); - } - - #[test] - fn test_prev_next_line() { - let rope = Rope::from("abc\ndef\nghi\njkl"); - - let mut chunks = rope.chunks(); - assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'a'); - - assert!(chunks.next_line()); - assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'd'); - - assert!(chunks.next_line()); - assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'g'); - - assert!(chunks.next_line()); - assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'j'); - - assert!(!chunks.next_line()); - assert_eq!(chunks.peek(), None); - - assert!(chunks.prev_line()); - assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'j'); - - assert!(chunks.prev_line()); - assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'g'); - - assert!(chunks.prev_line()); - assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'd'); - - assert!(chunks.prev_line()); - assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'a'); - - assert!(!chunks.prev_line()); - assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'a'); - - // Only return true when the cursor has moved to the start of a line - let mut chunks = rope.chunks_in_range(5..7); - chunks.seek(6); - assert!(!chunks.prev_line()); - assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'e'); - - assert!(!chunks.next_line()); - assert_eq!(chunks.peek(), None); - } - - #[test] - fn test_lines() { - let rope = Rope::from("abc\ndefg\nhi"); - let mut lines = rope.chunks().lines(); - assert_eq!(lines.next(), Some("abc")); - assert_eq!(lines.next(), Some("defg")); - assert_eq!(lines.next(), Some("hi")); - assert_eq!(lines.next(), None); - - let rope = Rope::from("abc\ndefg\nhi\n"); - let mut lines = rope.chunks().lines(); - assert_eq!(lines.next(), Some("abc")); - assert_eq!(lines.next(), Some("defg")); - assert_eq!(lines.next(), Some("hi")); - assert_eq!(lines.next(), Some("")); - assert_eq!(lines.next(), None); - - let rope = Rope::from("abc\ndefg\nhi"); - let mut lines = rope.reversed_chunks_in_range(0..rope.len()).lines(); - assert_eq!(lines.next(), Some("hi")); - assert_eq!(lines.next(), Some("defg")); - assert_eq!(lines.next(), Some("abc")); - assert_eq!(lines.next(), None); - - let rope = Rope::from("abc\ndefg\nhi\n"); - let mut lines = rope.reversed_chunks_in_range(0..rope.len()).lines(); - assert_eq!(lines.next(), Some("")); - assert_eq!(lines.next(), Some("hi")); - assert_eq!(lines.next(), Some("defg")); - assert_eq!(lines.next(), Some("abc")); - assert_eq!(lines.next(), None); - - let rope = Rope::from("abc\nlonger line test\nhi"); - let mut lines = rope.chunks().lines(); - assert_eq!(lines.next(), Some("abc")); - assert_eq!(lines.next(), Some("longer line test")); - assert_eq!(lines.next(), Some("hi")); - assert_eq!(lines.next(), None); - - let rope = Rope::from("abc\nlonger line test\nhi"); - let mut lines = rope.reversed_chunks_in_range(0..rope.len()).lines(); - assert_eq!(lines.next(), Some("hi")); - assert_eq!(lines.next(), Some("longer line test")); - assert_eq!(lines.next(), Some("abc")); - assert_eq!(lines.next(), None); - } - - #[gpui::test(iterations = 100)] - fn test_random_rope(mut rng: StdRng) { - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - let mut expected = String::new(); - let mut actual = Rope::new(); - for _ in 0..operations { - let end_ix = clip_offset(&expected, rng.random_range(0..=expected.len()), Right); - let start_ix = clip_offset(&expected, rng.random_range(0..=end_ix), Left); - let len = rng.random_range(0..=64); - let new_text: String = RandomCharIter::new(&mut rng).take(len).collect(); - - let mut new_actual = Rope::new(); - let mut cursor = actual.cursor(0); - new_actual.append(cursor.slice(start_ix)); - new_actual.push(&new_text); - cursor.seek_forward(end_ix); - new_actual.append(cursor.suffix()); - actual = new_actual; - - expected.replace_range(start_ix..end_ix, &new_text); - - assert_eq!(actual.text(), expected); - log::info!("text: {:?}", expected); - - for _ in 0..5 { - let end_ix = clip_offset(&expected, rng.random_range(0..=expected.len()), Right); - let start_ix = clip_offset(&expected, rng.random_range(0..=end_ix), Left); - - let actual_text = actual.chunks_in_range(start_ix..end_ix).collect::(); - assert_eq!(actual_text, &expected[start_ix..end_ix]); - - let mut actual_text = String::new(); - actual - .bytes_in_range(start_ix..end_ix) - .read_to_string(&mut actual_text) - .unwrap(); - assert_eq!(actual_text, &expected[start_ix..end_ix]); - - assert_eq!( - actual - .reversed_chunks_in_range(start_ix..end_ix) - .collect::>() - .into_iter() - .rev() - .collect::(), - &expected[start_ix..end_ix] - ); - - let mut expected_line_starts: Vec<_> = expected[start_ix..end_ix] - .match_indices('\n') - .map(|(index, _)| start_ix + index + 1) - .collect(); - - let mut chunks = actual.chunks_in_range(start_ix..end_ix); - - let mut actual_line_starts = Vec::new(); - while chunks.next_line() { - actual_line_starts.push(chunks.offset()); - } - assert_eq!( - actual_line_starts, - expected_line_starts, - "actual line starts != expected line starts when using next_line() for {:?} ({:?})", - &expected[start_ix..end_ix], - start_ix..end_ix - ); - - if start_ix < end_ix - && (start_ix == 0 || expected.as_bytes()[start_ix - 1] == b'\n') - { - expected_line_starts.insert(0, start_ix); - } - // Remove the last index if it starts at the end of the range. - if expected_line_starts.last() == Some(&end_ix) { - expected_line_starts.pop(); - } - - let mut actual_line_starts = Vec::new(); - while chunks.prev_line() { - actual_line_starts.push(chunks.offset()); - } - actual_line_starts.reverse(); - assert_eq!( - actual_line_starts, - expected_line_starts, - "actual line starts != expected line starts when using prev_line() for {:?} ({:?})", - &expected[start_ix..end_ix], - start_ix..end_ix - ); - - // Check that next_line/prev_line work correctly from random positions - let mut offset = rng.random_range(start_ix..=end_ix); - while !expected.is_char_boundary(offset) { - offset -= 1; - } - chunks.seek(offset); - - for _ in 0..5 { - if rng.random() { - let expected_next_line_start = expected[offset..end_ix] - .find('\n') - .map(|newline_ix| offset + newline_ix + 1); - - let moved = chunks.next_line(); - assert_eq!( - moved, - expected_next_line_start.is_some(), - "unexpected result from next_line after seeking to {} in range {:?} ({:?})", - offset, - start_ix..end_ix, - &expected[start_ix..end_ix] - ); - if let Some(expected_next_line_start) = expected_next_line_start { - assert_eq!( - chunks.offset(), - expected_next_line_start, - "invalid position after seeking to {} in range {:?} ({:?})", - offset, - start_ix..end_ix, - &expected[start_ix..end_ix] - ); - } else { - assert_eq!( - chunks.offset(), - end_ix, - "invalid position after seeking to {} in range {:?} ({:?})", - offset, - start_ix..end_ix, - &expected[start_ix..end_ix] - ); - } - } else { - let search_end = if offset > 0 && expected.as_bytes()[offset - 1] == b'\n' { - offset - 1 - } else { - offset - }; - - let expected_prev_line_start = expected[..search_end] - .rfind('\n') - .and_then(|newline_ix| { - let line_start_ix = newline_ix + 1; - if line_start_ix >= start_ix { - Some(line_start_ix) - } else { - None - } - }) - .or({ - if offset > 0 && start_ix == 0 { - Some(0) - } else { - None - } - }); - - let moved = chunks.prev_line(); - assert_eq!( - moved, - expected_prev_line_start.is_some(), - "unexpected result from prev_line after seeking to {} in range {:?} ({:?})", - offset, - start_ix..end_ix, - &expected[start_ix..end_ix] - ); - if let Some(expected_prev_line_start) = expected_prev_line_start { - assert_eq!( - chunks.offset(), - expected_prev_line_start, - "invalid position after seeking to {} in range {:?} ({:?})", - offset, - start_ix..end_ix, - &expected[start_ix..end_ix] - ); - } else { - assert_eq!( - chunks.offset(), - start_ix, - "invalid position after seeking to {} in range {:?} ({:?})", - offset, - start_ix..end_ix, - &expected[start_ix..end_ix] - ); - } - } - - assert!((start_ix..=end_ix).contains(&chunks.offset())); - if rng.random() { - offset = rng.random_range(start_ix..=end_ix); - while !expected.is_char_boundary(offset) { - offset -= 1; - } - chunks.seek(offset); - } else { - chunks.next(); - offset = chunks.offset(); - assert!((start_ix..=end_ix).contains(&chunks.offset())); - } - } - } - - let mut offset_utf16 = OffsetUtf16(0); - let mut point = Point::new(0, 0); - let mut point_utf16 = PointUtf16::new(0, 0); - for (ix, ch) in expected.char_indices().chain(Some((expected.len(), '\0'))) { - assert_eq!(actual.offset_to_point(ix), point, "offset_to_point({})", ix); - assert_eq!( - actual.offset_to_point_utf16(ix), - point_utf16, - "offset_to_point_utf16({})", - ix - ); - assert_eq!( - actual.point_to_offset(point), - ix, - "point_to_offset({:?})", - point - ); - assert_eq!( - actual.point_utf16_to_offset(point_utf16), - ix, - "point_utf16_to_offset({:?})", - point_utf16 - ); - assert_eq!( - actual.offset_to_offset_utf16(ix), - offset_utf16, - "offset_to_offset_utf16({:?})", - ix - ); - assert_eq!( - actual.offset_utf16_to_offset(offset_utf16), - ix, - "offset_utf16_to_offset({:?})", - offset_utf16 - ); - if ch == '\n' { - point += Point::new(1, 0); - point_utf16 += PointUtf16::new(1, 0); - } else { - point.column += ch.len_utf8() as u32; - point_utf16.column += ch.len_utf16() as u32; - } - offset_utf16.0 += ch.len_utf16(); - } - - let mut offset_utf16 = OffsetUtf16(0); - let mut point_utf16 = Unclipped(PointUtf16::zero()); - for unit in expected.encode_utf16() { - let left_offset = actual.clip_offset_utf16(offset_utf16, Bias::Left); - let right_offset = actual.clip_offset_utf16(offset_utf16, Bias::Right); - assert!(right_offset >= left_offset); - // Ensure translating UTF-16 offsets to UTF-8 offsets doesn't panic. - actual.offset_utf16_to_offset(left_offset); - actual.offset_utf16_to_offset(right_offset); - - let left_point = actual.clip_point_utf16(point_utf16, Bias::Left); - let right_point = actual.clip_point_utf16(point_utf16, Bias::Right); - assert!(right_point >= left_point); - // Ensure translating valid UTF-16 points to offsets doesn't panic. - actual.point_utf16_to_offset(left_point); - actual.point_utf16_to_offset(right_point); - - offset_utf16.0 += 1; - if unit == b'\n' as u16 { - point_utf16.0 += PointUtf16::new(1, 0); - } else { - point_utf16.0 += PointUtf16::new(0, 1); - } - } - - for _ in 0..5 { - let end_ix = clip_offset(&expected, rng.random_range(0..=expected.len()), Right); - let start_ix = clip_offset(&expected, rng.random_range(0..=end_ix), Left); - assert_eq!( - actual.cursor(start_ix).summary::(end_ix), - TextSummary::from(&expected[start_ix..end_ix]) - ); - } - - let mut expected_longest_rows = Vec::new(); - let mut longest_line_len = -1_isize; - for (row, line) in expected.split('\n').enumerate() { - let row = row as u32; - assert_eq!( - actual.line_len(row), - line.len() as u32, - "invalid line len for row {}", - row - ); - - let line_char_count = line.chars().count() as isize; - match line_char_count.cmp(&longest_line_len) { - Ordering::Less => {} - Ordering::Equal => expected_longest_rows.push(row), - Ordering::Greater => { - longest_line_len = line_char_count; - expected_longest_rows.clear(); - expected_longest_rows.push(row); - } - } - } - - let longest_row = actual.summary().longest_row; - assert!( - expected_longest_rows.contains(&longest_row), - "incorrect longest row {}. expected {:?} with length {}", - longest_row, - expected_longest_rows, - longest_line_len, - ); - } - } - - #[test] - fn test_chunks_equals_str() { - let text = "This is a multi-chunk\n& multi-line test string!"; - let rope = Rope::from(text); - for start in 0..text.len() { - for end in start..text.len() { - let range = start..end; - let correct_substring = &text[start..end]; - - // Test that correct range returns true - assert!( - rope.chunks_in_range(range.clone()) - .equals_str(correct_substring) - ); - assert!( - rope.reversed_chunks_in_range(range.clone()) - .equals_str(correct_substring) - ); - - // Test that all other ranges return false (unless they happen to match) - for other_start in 0..text.len() { - for other_end in other_start..text.len() { - if other_start == start && other_end == end { - continue; - } - let other_substring = &text[other_start..other_end]; - - // Only assert false if the substrings are actually different - if other_substring == correct_substring { - continue; - } - assert!( - !rope - .chunks_in_range(range.clone()) - .equals_str(other_substring) - ); - assert!( - !rope - .reversed_chunks_in_range(range.clone()) - .equals_str(other_substring) - ); - } - } - } - } - - let rope = Rope::from(""); - assert!(rope.chunks_in_range(0..0).equals_str("")); - assert!(rope.reversed_chunks_in_range(0..0).equals_str("")); - assert!(!rope.chunks_in_range(0..0).equals_str("foo")); - assert!(!rope.reversed_chunks_in_range(0..0).equals_str("foo")); - } - - #[test] - fn test_is_char_boundary() { - let fixture = "地"; - let rope = Rope::from("地"); - for b in 0..=fixture.len() { - assert_eq!(rope.is_char_boundary(b), fixture.is_char_boundary(b)); - } - let fixture = ""; - let rope = Rope::from(""); - for b in 0..=fixture.len() { - assert_eq!(rope.is_char_boundary(b), fixture.is_char_boundary(b)); - } - let fixture = "🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩"; - let rope = Rope::from("🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩"); - for b in 0..=fixture.len() { - assert_eq!(rope.is_char_boundary(b), fixture.is_char_boundary(b)); - } - } - - #[test] - fn test_floor_char_boundary() { - let fixture = "地"; - let rope = Rope::from("地"); - for b in 0..=fixture.len() { - assert_eq!(rope.floor_char_boundary(b), fixture.floor_char_boundary(b)); - } - - let fixture = ""; - let rope = Rope::from(""); - for b in 0..=fixture.len() { - assert_eq!(rope.floor_char_boundary(b), fixture.floor_char_boundary(b)); - } - - let fixture = "🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩"; - let rope = Rope::from("🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩"); - for b in 0..=fixture.len() { - assert_eq!(rope.floor_char_boundary(b), fixture.floor_char_boundary(b)); - } - } - - #[test] - fn test_ceil_char_boundary() { - let fixture = "地"; - let rope = Rope::from("地"); - for b in 0..=fixture.len() { - assert_eq!(rope.ceil_char_boundary(b), fixture.ceil_char_boundary(b)); - } - - let fixture = ""; - let rope = Rope::from(""); - for b in 0..=fixture.len() { - assert_eq!(rope.ceil_char_boundary(b), fixture.ceil_char_boundary(b)); - } - - let fixture = "🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩"; - let rope = Rope::from("🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩"); - for b in 0..=fixture.len() { - assert_eq!(rope.ceil_char_boundary(b), fixture.ceil_char_boundary(b)); - } - } - - fn clip_offset(text: &str, mut offset: usize, bias: Bias) -> usize { - while !text.is_char_boundary(offset) { - match bias { - Bias::Left => offset -= 1, - Bias::Right => offset += 1, - } - } - offset - } - - impl Rope { - fn text(&self) -> String { - let mut text = String::new(); - for chunk in self.chunks.cursor::<()>(()) { - text.push_str(&chunk.text); - } - text - } - } -} diff --git a/crates/rope/src/unclipped.rs b/crates/rope/src/unclipped.rs deleted file mode 100644 index abf82504ea..0000000000 --- a/crates/rope/src/unclipped.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::ChunkSummary; -use std::ops::{Add, AddAssign, Sub, SubAssign}; - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Unclipped(pub T); - -impl From for Unclipped { - fn from(value: T) -> Self { - Unclipped(value) - } -} - -impl<'a, T: sum_tree::Dimension<'a, ChunkSummary>> sum_tree::Dimension<'a, ChunkSummary> - for Unclipped -{ - fn zero(_: ()) -> Self { - Self(T::zero(())) - } - - fn add_summary(&mut self, summary: &'a ChunkSummary, _: ()) { - self.0.add_summary(summary, ()); - } -} - -impl> Add> for Unclipped { - type Output = Unclipped; - - fn add(self, rhs: Unclipped) -> Self::Output { - Unclipped(self.0 + rhs.0) - } -} - -impl> Sub> for Unclipped { - type Output = Unclipped; - - fn sub(self, rhs: Unclipped) -> Self::Output { - Unclipped(self.0 - rhs.0) - } -} - -impl> AddAssign> for Unclipped { - fn add_assign(&mut self, rhs: Unclipped) { - self.0 += rhs.0; - } -} - -impl> SubAssign> for Unclipped { - fn sub_assign(&mut self, rhs: Unclipped) { - self.0 -= rhs.0; - } -} diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml deleted file mode 100644 index 10ebde26b6..0000000000 --- a/crates/rpc/Cargo.toml +++ /dev/null @@ -1,44 +0,0 @@ -[package] -description = "Shared logic for communication between the Zed app and the zed.dev server" -edition.workspace = true -name = "rpc" -version = "0.1.0" -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/rpc.rs" -doctest = false - -[features] -gpui = ["dep:gpui"] -test-support = ["collections/test-support", "gpui/test-support", "proto/test-support"] - -[dependencies] -anyhow.workspace = true -async-tungstenite.workspace = true -base64.workspace = true -chrono.workspace = true -collections.workspace = true -futures.workspace = true -gpui = { workspace = true, optional = true } -parking_lot.workspace = true -proto.workspace = true -rand.workspace = true -rsa.workspace = true -serde.workspace = true -serde_json.workspace = true -sha2.workspace = true -strum.workspace = true -tracing = { version = "0.1.34", features = ["log"] } -util.workspace = true -zstd.workspace = true - -[dev-dependencies] -collections = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -proto = { workspace = true, features = ["test-support"] } -zlog.workspace = true diff --git a/crates/rpc/LICENSE-GPL b/crates/rpc/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/rpc/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/rpc/src/auth.rs b/crates/rpc/src/auth.rs deleted file mode 100644 index 3829f3d36b..0000000000 --- a/crates/rpc/src/auth.rs +++ /dev/null @@ -1,240 +0,0 @@ -use anyhow::{Context as _, Result}; -use base64::prelude::*; -use rand::prelude::*; -use rsa::pkcs1::{DecodeRsaPublicKey, EncodeRsaPublicKey}; -use rsa::traits::PaddingScheme; -use rsa::{Oaep, Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey}; -use sha2::Sha256; -use std::convert::TryFrom; - -fn oaep_sha256_padding() -> impl PaddingScheme { - Oaep::new::() -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum EncryptionFormat { - /// The original encryption format. - /// - /// This is using [`Pkcs1v15Encrypt`], which is vulnerable to side-channel attacks. - /// As such, we're in the process of phasing it out. - /// - /// See [here](https://people.redhat.com/~hkario/marvin/) for more details. - V0, - - /// The new encryption key format using Optimal Asymmetric Encryption Padding (OAEP) with a SHA-256 digest. - V1, -} - -pub struct PublicKey(RsaPublicKey); - -pub struct PrivateKey(RsaPrivateKey); - -/// Generate a public and private key for asymmetric encryption. -pub fn keypair() -> Result<(PublicKey, PrivateKey)> { - let mut rng = RsaRngCompat::new(); - let bits = 2048; - let private_key = RsaPrivateKey::new(&mut rng, bits)?; - let public_key = RsaPublicKey::from(&private_key); - Ok((PublicKey(public_key), PrivateKey(private_key))) -} - -/// Generate a random 64-character base64 string. -pub fn random_token() -> String { - let mut rng = rand::rng(); - let mut token_bytes = [0; 48]; - for byte in token_bytes.iter_mut() { - *byte = rng.random(); - } - BASE64_URL_SAFE.encode(token_bytes) -} - -impl PublicKey { - /// Convert a string to a base64-encoded string that can only be decoded with the corresponding - /// private key. - pub fn encrypt_string(&self, string: &str, format: EncryptionFormat) -> Result { - let mut rng = RsaRngCompat::new(); - let bytes = string.as_bytes(); - let encrypted_bytes = match format { - EncryptionFormat::V0 => self.0.encrypt(&mut rng, Pkcs1v15Encrypt, bytes), - EncryptionFormat::V1 => self.0.encrypt(&mut rng, oaep_sha256_padding(), bytes), - } - .context("failed to encrypt string with public key")?; - let encrypted_string = BASE64_URL_SAFE.encode(&encrypted_bytes); - Ok(encrypted_string) - } -} - -impl PrivateKey { - /// Decrypt a base64-encoded string that was encrypted by the corresponding public key. - pub fn decrypt_string(&self, encrypted_string: &str) -> Result { - let encrypted_bytes = BASE64_URL_SAFE - .decode(encrypted_string) - .context("failed to base64-decode encrypted string")?; - let bytes = self - .0 - .decrypt(oaep_sha256_padding(), &encrypted_bytes) - .or_else(|_err| { - // If we failed to decrypt using the new format, try decrypting with the old - // one to handle mismatches between the client and server. - self.0.decrypt(Pkcs1v15Encrypt, &encrypted_bytes) - }) - .context("failed to decrypt string with private key")?; - let string = String::from_utf8(bytes).context("decrypted content was not valid utf8")?; - Ok(string) - } -} - -impl TryFrom for String { - type Error = anyhow::Error; - fn try_from(key: PublicKey) -> Result { - let bytes = key - .0 - .to_pkcs1_der() - .context("failed to serialize public key")?; - let string = BASE64_URL_SAFE.encode(&bytes); - Ok(string) - } -} - -impl TryFrom for PublicKey { - type Error = anyhow::Error; - fn try_from(value: String) -> Result { - let bytes = BASE64_URL_SAFE - .decode(&value) - .context("failed to base64-decode public key string")?; - let key = Self(RsaPublicKey::from_pkcs1_der(&bytes).context("failed to parse public key")?); - Ok(key) - } -} - -// TODO: remove once we rsa v0.10 is released. -struct RsaRngCompat(rand::rngs::ThreadRng); - -impl RsaRngCompat { - fn new() -> Self { - Self(rand::rng()) - } -} - -impl rsa::signature::rand_core::RngCore for RsaRngCompat { - fn next_u32(&mut self) -> u32 { - self.0.next_u32() - } - - fn next_u64(&mut self) -> u64 { - self.0.next_u64() - } - - fn fill_bytes(&mut self, dest: &mut [u8]) { - self.0.fill_bytes(dest); - } - - fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rsa::signature::rand_core::Error> { - self.fill_bytes(dest); - Ok(()) - } -} - -impl rsa::signature::rand_core::CryptoRng for RsaRngCompat {} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_generate_encrypt_and_decrypt_token() { - // CLIENT: - // * generate a keypair for asymmetric encryption - // * serialize the public key to send it to the server. - let (public, private) = keypair().unwrap(); - let public_string = String::try_from(public).unwrap(); - assert_printable(&public_string); - - // SERVER: - // * parse the public key - // * generate a random token. - // * encrypt the token using the public key. - let public = PublicKey::try_from(public_string).unwrap(); - let token = random_token(); - let encrypted_token = public.encrypt_string(&token, EncryptionFormat::V1).unwrap(); - assert_eq!(token.len(), 64); - assert_ne!(encrypted_token, token); - assert_printable(&token); - assert_printable(&encrypted_token); - - // CLIENT: - // * decrypt the token using the private key. - let decrypted_token = private.decrypt_string(&encrypted_token).unwrap(); - assert_eq!(decrypted_token, token); - } - - #[test] - fn test_generate_encrypt_and_decrypt_token_with_v0_encryption_format() { - // CLIENT: - // * generate a keypair for asymmetric encryption - // * serialize the public key to send it to the server. - let (public, private) = keypair().unwrap(); - let public_string = String::try_from(public).unwrap(); - assert_printable(&public_string); - - // SERVER: - // * parse the public key - // * generate a random token. - // * encrypt the token using the public key. - let public = PublicKey::try_from(public_string).unwrap(); - let token = random_token(); - let encrypted_token = public.encrypt_string(&token, EncryptionFormat::V0).unwrap(); - assert_eq!(token.len(), 64); - assert_ne!(encrypted_token, token); - assert_printable(&token); - assert_printable(&encrypted_token); - - // CLIENT: - // * decrypt the token using the private key. - let decrypted_token = private.decrypt_string(&encrypted_token).unwrap(); - assert_eq!(decrypted_token, token); - } - - #[test] - fn test_encode_and_decode_base64_public_key() { - // A base64-encoded public key. - // - // We're using a literal string to ensure that encoding and decoding works across differences in implementations. - let encoded_public_key = "MIGJAoGBAMPvufou8wOuUIF1Wlkbtn0ZMM9nC55QJ06nTZvgMfZv5esFVU9-cQO_JC1P9ZoEcMDJweFERnQuQLqzsrMDLFbkdgL128ZU43WOLiQraxaICFIZsPUeTtWMKp2D5bPWsNxs-lnCma7vCAry6fpXuj5AKQdk7cTZJNucgvZQ0uUfAgMBAAE=".to_string(); - - // Make sure we can parse the public key. - let public_key = PublicKey::try_from(encoded_public_key.clone()).unwrap(); - - // Make sure we re-encode to the same format. - assert_eq!(encoded_public_key, String::try_from(public_key).unwrap()); - } - - #[test] - fn test_tokens_are_always_url_safe() { - for _ in 0..5 { - let token = random_token(); - let (public_key, _) = keypair().unwrap(); - let encrypted_token = public_key - .encrypt_string(&token, EncryptionFormat::V1) - .unwrap(); - let public_key_str = String::try_from(public_key).unwrap(); - - assert_printable(&token); - assert_printable(&public_key_str); - assert_printable(&encrypted_token); - } - } - - fn assert_printable(token: &str) { - for c in token.chars() { - assert!( - c.is_ascii_graphic(), - "token {:?} has non-printable char {}", - token, - c - ); - assert_ne!(c, '/', "token {:?} is not URL-safe", token); - assert_ne!(c, '&', "token {:?} is not URL-safe", token); - } - } -} diff --git a/crates/rpc/src/conn.rs b/crates/rpc/src/conn.rs deleted file mode 100644 index e598e5f7bc..0000000000 --- a/crates/rpc/src/conn.rs +++ /dev/null @@ -1,102 +0,0 @@ -use async_tungstenite::tungstenite::Message as WebSocketMessage; -use futures::{SinkExt as _, StreamExt as _}; - -pub struct Connection { - pub(crate) tx: - Box>, - pub(crate) rx: - Box>>, -} - -impl Connection { - pub fn new(stream: S) -> Self - where - S: 'static - + Send - + Unpin - + futures::Sink - + futures::Stream>, - { - let (tx, rx) = stream.split(); - Self { - tx: Box::new(tx), - rx: Box::new(rx), - } - } - - pub async fn send(&mut self, message: WebSocketMessage) -> anyhow::Result<()> { - self.tx.send(message).await - } - - #[cfg(any(test, feature = "test-support"))] - pub fn in_memory( - executor: gpui::BackgroundExecutor, - ) -> (Self, Self, std::sync::Arc) { - use std::sync::{ - Arc, - atomic::{AtomicBool, Ordering::SeqCst}, - }; - - let killed = Arc::new(AtomicBool::new(false)); - let (a_tx, a_rx) = channel(killed.clone(), executor.clone()); - let (b_tx, b_rx) = channel(killed.clone(), executor); - return ( - Self { tx: a_tx, rx: b_rx }, - Self { tx: b_tx, rx: a_rx }, - killed, - ); - - #[allow(clippy::type_complexity)] - fn channel( - killed: Arc, - executor: gpui::BackgroundExecutor, - ) -> ( - Box>, - Box>>, - ) { - use anyhow::anyhow; - use futures::channel::mpsc; - use std::io::Error; - - let (tx, rx) = mpsc::unbounded::(); - - let tx = tx.sink_map_err(|error| anyhow!(error)).with({ - let killed = killed.clone(); - let executor = executor.clone(); - move |msg| { - let killed = killed.clone(); - let executor = executor.clone(); - Box::pin(async move { - executor.simulate_random_delay().await; - - // Writes to a half-open TCP connection will error. - if killed.load(SeqCst) { - std::io::Result::Err(Error::other("connection lost"))?; - } - - Ok(msg) - }) - } - }); - - let rx = rx.then({ - move |msg| { - let killed = killed.clone(); - let executor = executor.clone(); - Box::pin(async move { - executor.simulate_random_delay().await; - - // Reads from a half-open TCP connection will hang. - if killed.load(SeqCst) { - futures::future::pending::<()>().await; - } - - Ok(msg) - }) - } - }); - - (Box::new(tx), Box::new(rx)) - } - } -} diff --git a/crates/rpc/src/extension.rs b/crates/rpc/src/extension.rs deleted file mode 100644 index 1b00312bad..0000000000 --- a/crates/rpc/src/extension.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::collections::BTreeSet; -use std::sync::Arc; - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use strum::EnumString; - -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] -pub struct ExtensionApiManifest { - pub name: String, - pub version: Arc, - pub description: Option, - pub authors: Vec, - pub repository: String, - pub schema_version: Option, - pub wasm_api_version: Option, - #[serde(default)] - pub provides: BTreeSet, -} - -#[derive( - Debug, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Clone, - Copy, - Serialize, - Deserialize, - EnumString, - strum::Display, - strum::EnumIter, -)] -#[serde(rename_all = "kebab-case")] -#[strum(serialize_all = "kebab-case")] -pub enum ExtensionProvides { - Themes, - IconThemes, - Languages, - Grammars, - LanguageServers, - ContextServers, - AgentServers, - SlashCommands, - IndexedDocsProviders, - Snippets, - DebugAdapters, -} - -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] -pub struct ExtensionMetadata { - pub id: Arc, - #[serde(flatten)] - pub manifest: ExtensionApiManifest, - pub published_at: DateTime, - pub download_count: u64, -} - -#[derive(Serialize, Deserialize)] -pub struct GetExtensionsResponse { - pub data: Vec, -} diff --git a/crates/rpc/src/macros.rs b/crates/rpc/src/macros.rs deleted file mode 100644 index f85889a97b..0000000000 --- a/crates/rpc/src/macros.rs +++ /dev/null @@ -1,73 +0,0 @@ -#[macro_export] -macro_rules! messages { - ($(($name:ident, $priority:ident)),* $(,)?) => { - pub fn build_typed_envelope(sender_id: ConnectionId, received_at: Instant, envelope: Envelope) -> Option> { - match envelope.payload { - $(Some(envelope::Payload::$name(payload)) => { - Some(Box::new(TypedEnvelope { - sender_id, - original_sender_id: envelope.original_sender_id.map(|original_sender| PeerId { - owner_id: original_sender.owner_id, - id: original_sender.id - }), - message_id: envelope.id, - payload, - received_at, - })) - }, )* - _ => None - } - } - - $( - impl EnvelopedMessage for $name { - const NAME: &'static str = std::stringify!($name); - const PRIORITY: MessagePriority = MessagePriority::$priority; - - fn into_envelope( - self, - id: u32, - responding_to: Option, - original_sender_id: Option, - ) -> Envelope { - Envelope { - id, - responding_to, - original_sender_id, - payload: Some(envelope::Payload::$name(self)), - } - } - - fn from_envelope(envelope: Envelope) -> Option { - if let Some(envelope::Payload::$name(msg)) = envelope.payload { - Some(msg) - } else { - None - } - } - } - )* - }; -} - -#[macro_export] -macro_rules! request_messages { - ($(($request_name:ident, $response_name:ident)),* $(,)?) => { - $(impl RequestMessage for $request_name { - type Response = $response_name; - })* - }; -} - -#[macro_export] -macro_rules! entity_messages { - ({$id_field:ident, $entity_type:ty}, $($name:ident),* $(,)?) => { - $(impl EntityMessage for $name { - type Entity = $entity_type; - - fn remote_entity_id(&self) -> u64 { - self.$id_field - } - })* - }; -} diff --git a/crates/rpc/src/message_stream.rs b/crates/rpc/src/message_stream.rs deleted file mode 100644 index 023e916df3..0000000000 --- a/crates/rpc/src/message_stream.rs +++ /dev/null @@ -1,143 +0,0 @@ -#![allow(non_snake_case)] - -pub use ::proto::*; - -use async_tungstenite::tungstenite::Message as WebSocketMessage; -use futures::{SinkExt as _, StreamExt as _}; -use proto::Message as _; -use std::time::Instant; -use std::{fmt::Debug, io}; -use zstd::zstd_safe::WriteBuf; - -const KIB: usize = 1024; -const MIB: usize = KIB * 1024; -const MAX_BUFFER_LEN: usize = MIB; - -/// A stream of protobuf messages. -pub struct MessageStream { - stream: S, - encoding_buffer: Vec, -} - -#[derive(Debug)] -pub enum Message { - Envelope(Envelope), - Ping, - Pong, -} - -impl MessageStream { - pub fn new(stream: S) -> Self { - Self { - stream, - encoding_buffer: Vec::new(), - } - } -} - -impl MessageStream -where - S: futures::Sink + Unpin, -{ - pub async fn write(&mut self, message: Message) -> anyhow::Result<()> { - #[cfg(any(test, feature = "test-support"))] - const COMPRESSION_LEVEL: i32 = -7; - - #[cfg(not(any(test, feature = "test-support")))] - const COMPRESSION_LEVEL: i32 = 4; - - match message { - Message::Envelope(message) => { - self.encoding_buffer.reserve(message.encoded_len()); - message - .encode(&mut self.encoding_buffer) - .map_err(io::Error::from)?; - let buffer = - zstd::stream::encode_all(self.encoding_buffer.as_slice(), COMPRESSION_LEVEL) - .unwrap(); - - self.encoding_buffer.clear(); - self.encoding_buffer.shrink_to(MAX_BUFFER_LEN); - self.stream - .send(WebSocketMessage::Binary(buffer.into())) - .await?; - } - Message::Ping => { - self.stream - .send(WebSocketMessage::Ping(Default::default())) - .await?; - } - Message::Pong => { - self.stream - .send(WebSocketMessage::Pong(Default::default())) - .await?; - } - } - - Ok(()) - } -} - -impl MessageStream -where - S: futures::Stream> + Unpin, -{ - pub async fn read(&mut self) -> anyhow::Result<(Message, Instant)> { - while let Some(bytes) = self.stream.next().await { - let received_at = Instant::now(); - match bytes? { - WebSocketMessage::Binary(bytes) => { - zstd::stream::copy_decode(bytes.as_slice(), &mut self.encoding_buffer)?; - let envelope = Envelope::decode(self.encoding_buffer.as_slice()) - .map_err(io::Error::from)?; - - self.encoding_buffer.clear(); - self.encoding_buffer.shrink_to(MAX_BUFFER_LEN); - return Ok((Message::Envelope(envelope), received_at)); - } - WebSocketMessage::Ping(_) => return Ok((Message::Ping, received_at)), - WebSocketMessage::Pong(_) => return Ok((Message::Pong, received_at)), - WebSocketMessage::Close(_) => break, - _ => {} - } - } - anyhow::bail!("connection closed"); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[gpui::test] - async fn test_buffer_size() { - let (tx, rx) = futures::channel::mpsc::unbounded(); - let mut sink = MessageStream::new(tx.sink_map_err(|_| anyhow::anyhow!(""))); - sink.write(Message::Envelope(Envelope { - payload: Some(envelope::Payload::UpdateWorktree(UpdateWorktree { - root_name: "abcdefg".repeat(10), - ..Default::default() - })), - ..Default::default() - })) - .await - .unwrap(); - assert!(sink.encoding_buffer.capacity() <= MAX_BUFFER_LEN); - sink.write(Message::Envelope(Envelope { - payload: Some(envelope::Payload::UpdateWorktree(UpdateWorktree { - root_name: "abcdefg".repeat(1000000), - ..Default::default() - })), - ..Default::default() - })) - .await - .unwrap(); - assert!(sink.encoding_buffer.capacity() <= MAX_BUFFER_LEN); - - let mut stream = MessageStream::new(rx.map(anyhow::Ok)); - stream.read().await.unwrap(); - assert!(stream.encoding_buffer.capacity() <= MAX_BUFFER_LEN); - stream.read().await.unwrap(); - assert!(stream.encoding_buffer.capacity() <= MAX_BUFFER_LEN); - } -} diff --git a/crates/rpc/src/notification.rs b/crates/rpc/src/notification.rs deleted file mode 100644 index 50364c7387..0000000000 --- a/crates/rpc/src/notification.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::proto; -use serde::{Deserialize, Serialize}; -use serde_json::{Value, map}; -use strum::VariantNames; - -const KIND: &str = "kind"; -const ENTITY_ID: &str = "entity_id"; - -/// A notification that can be stored, associated with a given recipient. -/// -/// This struct is stored in the collab database as JSON, so it shouldn't be -/// changed in a backward-incompatible way. For example, when renaming a -/// variant, add a serde alias for the old name. -/// -/// Most notification types have a special field which is aliased to -/// `entity_id`. This field is stored in its own database column, and can -/// be used to query the notification. -#[derive(Debug, Clone, PartialEq, Eq, VariantNames, Serialize, Deserialize)] -#[serde(tag = "kind")] -pub enum Notification { - ContactRequest { - #[serde(rename = "entity_id")] - sender_id: u64, - }, - ContactRequestAccepted { - #[serde(rename = "entity_id")] - responder_id: u64, - }, - ChannelInvitation { - #[serde(rename = "entity_id")] - channel_id: u64, - channel_name: String, - inviter_id: u64, - }, -} - -impl Notification { - pub fn to_proto(&self) -> proto::Notification { - let mut value = serde_json::to_value(self).unwrap(); - let mut entity_id = None; - let value = value.as_object_mut().unwrap(); - let Some(Value::String(kind)) = value.remove(KIND) else { - unreachable!("kind is the enum tag") - }; - if let map::Entry::Occupied(e) = value.entry(ENTITY_ID) - && e.get().is_u64() - { - entity_id = e.remove().as_u64(); - } - proto::Notification { - kind, - entity_id, - content: serde_json::to_string(&value).unwrap(), - ..Default::default() - } - } - - pub fn from_proto(notification: &proto::Notification) -> Option { - let mut value = serde_json::from_str::(¬ification.content).ok()?; - let object = value.as_object_mut()?; - object.insert(KIND.into(), notification.kind.to_string().into()); - if let Some(entity_id) = notification.entity_id { - object.insert(ENTITY_ID.into(), entity_id.into()); - } - serde_json::from_value(value).ok() - } - - pub fn all_variant_names() -> &'static [&'static str] { - Self::VARIANTS - } -} - -#[cfg(test)] -mod tests { - use crate::Notification; - - #[test] - fn test_notification() { - // Notifications can be serialized and deserialized. - for notification in [ - Notification::ContactRequest { sender_id: 1 }, - Notification::ContactRequestAccepted { responder_id: 2 }, - Notification::ChannelInvitation { - channel_id: 100, - channel_name: "the-channel".into(), - inviter_id: 50, - }, - ] { - let message = notification.to_proto(); - let deserialized = Notification::from_proto(&message).unwrap(); - assert_eq!(deserialized, notification); - } - - // When notifications are serialized, the `kind` and `actor_id` fields are - // stored separately, and do not appear redundantly in the JSON. - let notification = Notification::ContactRequest { sender_id: 1 }; - assert_eq!(notification.to_proto().content, "{}"); - } -} diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs deleted file mode 100644 index 73be0f19fe..0000000000 --- a/crates/rpc/src/peer.rs +++ /dev/null @@ -1,1055 +0,0 @@ -use super::{ - Connection, - message_stream::{Message, MessageStream}, - proto::{ - self, AnyTypedEnvelope, EnvelopedMessage, PeerId, Receipt, RequestMessage, TypedEnvelope, - }, -}; -use anyhow::{Context as _, Result, anyhow}; -use collections::HashMap; -use futures::{ - FutureExt, SinkExt, Stream, StreamExt, TryFutureExt, - channel::{mpsc, oneshot}, - stream::BoxStream, -}; -use parking_lot::{Mutex, RwLock}; -use proto::{ErrorCode, ErrorCodeExt, ErrorExt, RpcError}; -use serde::{Serialize, ser::SerializeStruct}; -use std::{ - fmt, future, - future::Future, - sync::atomic::Ordering::SeqCst, - sync::{ - Arc, - atomic::{self, AtomicU32}, - }, - time::Duration, - time::Instant, -}; - -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize)] -pub struct ConnectionId { - pub owner_id: u32, - pub id: u32, -} - -impl From for PeerId { - fn from(id: ConnectionId) -> Self { - PeerId { - owner_id: id.owner_id, - id: id.id, - } - } -} - -impl From for ConnectionId { - fn from(peer_id: PeerId) -> Self { - Self { - owner_id: peer_id.owner_id, - id: peer_id.id, - } - } -} - -impl fmt::Display for ConnectionId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}/{}", self.owner_id, self.id) - } -} - -pub struct Peer { - epoch: AtomicU32, - pub connections: RwLock>, - next_connection_id: AtomicU32, -} - -#[derive(Clone, Serialize)] -pub struct ConnectionState { - #[serde(skip)] - outgoing_tx: mpsc::UnboundedSender, - next_message_id: Arc, - #[allow(clippy::type_complexity)] - #[serde(skip)] - response_channels: Arc< - Mutex< - Option< - HashMap< - u32, - oneshot::Sender<(proto::Envelope, std::time::Instant, oneshot::Sender<()>)>, - >, - >, - >, - >, - #[allow(clippy::type_complexity)] - #[serde(skip)] - stream_response_channels: Arc< - Mutex< - Option< - HashMap, oneshot::Sender<()>)>>, - >, - >, - >, -} - -const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1); -const WRITE_TIMEOUT: Duration = Duration::from_secs(2); -pub const RECEIVE_TIMEOUT: Duration = Duration::from_secs(10); - -impl Peer { - pub fn new(epoch: u32) -> Arc { - Arc::new(Self { - epoch: AtomicU32::new(epoch), - connections: Default::default(), - next_connection_id: Default::default(), - }) - } - - pub fn epoch(&self) -> u32 { - self.epoch.load(SeqCst) - } - - pub fn add_connection( - self: &Arc, - connection: Connection, - create_timer: F, - ) -> ( - ConnectionId, - impl Future> + Send + use, - BoxStream<'static, Box>, - ) - where - F: Send + Fn(Duration) -> Fut, - Fut: Send + Future, - Out: Send, - { - // For outgoing messages, use an unbounded channel so that application code - // can always send messages without yielding. For incoming messages, use a - // bounded channel so that other peers will receive backpressure if they send - // messages faster than this peer can process them. - #[cfg(any(test, feature = "test-support"))] - const INCOMING_BUFFER_SIZE: usize = 1; - #[cfg(not(any(test, feature = "test-support")))] - const INCOMING_BUFFER_SIZE: usize = 256; - let (mut incoming_tx, incoming_rx) = mpsc::channel(INCOMING_BUFFER_SIZE); - let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded(); - - let connection_id = ConnectionId { - owner_id: self.epoch.load(SeqCst), - id: self.next_connection_id.fetch_add(1, SeqCst), - }; - let connection_state = ConnectionState { - outgoing_tx, - next_message_id: Default::default(), - response_channels: Arc::new(Mutex::new(Some(Default::default()))), - stream_response_channels: Arc::new(Mutex::new(Some(Default::default()))), - }; - let mut writer = MessageStream::new(connection.tx); - let mut reader = MessageStream::new(connection.rx); - - let this = self.clone(); - let response_channels = connection_state.response_channels.clone(); - let stream_response_channels = connection_state.stream_response_channels.clone(); - - let handle_io = async move { - tracing::trace!(%connection_id, "handle io future: start"); - - let _end_connection = util::defer(|| { - response_channels.lock().take(); - if let Some(channels) = stream_response_channels.lock().take() { - for channel in channels.values() { - let _ = channel.unbounded_send(( - Err(anyhow!("connection closed")), - oneshot::channel().0, - )); - } - } - this.connections.write().remove(&connection_id); - tracing::trace!(%connection_id, "handle io future: end"); - }); - - // Send messages on this frequency so the connection isn't closed. - let keepalive_timer = create_timer(KEEPALIVE_INTERVAL).fuse(); - futures::pin_mut!(keepalive_timer); - - // Disconnect if we don't receive messages at least this frequently. - let receive_timeout = create_timer(RECEIVE_TIMEOUT).fuse(); - futures::pin_mut!(receive_timeout); - - loop { - tracing::trace!(%connection_id, "outer loop iteration start"); - let read_message = reader.read().fuse(); - futures::pin_mut!(read_message); - - loop { - tracing::trace!(%connection_id, "inner loop iteration start"); - futures::select_biased! { - outgoing = outgoing_rx.next().fuse() => match outgoing { - Some(outgoing) => { - tracing::trace!(%connection_id, "outgoing rpc message: writing"); - futures::select_biased! { - result = writer.write(outgoing).fuse() => { - tracing::trace!(%connection_id, "outgoing rpc message: done writing"); - result.context("failed to write RPC message")?; - tracing::trace!(%connection_id, "keepalive interval: resetting after sending message"); - keepalive_timer.set(create_timer(KEEPALIVE_INTERVAL).fuse()); - } - _ = create_timer(WRITE_TIMEOUT).fuse() => { - tracing::trace!(%connection_id, "outgoing rpc message: writing timed out"); - anyhow::bail!("timed out writing message"); - } - } - } - None => { - tracing::trace!(%connection_id, "outgoing rpc message: channel closed"); - return Ok(()) - }, - }, - _ = keepalive_timer => { - tracing::trace!(%connection_id, "keepalive interval: pinging"); - futures::select_biased! { - result = writer.write(Message::Ping).fuse() => { - tracing::trace!(%connection_id, "keepalive interval: done pinging"); - result.context("failed to send keepalive")?; - tracing::trace!(%connection_id, "keepalive interval: resetting after pinging"); - keepalive_timer.set(create_timer(KEEPALIVE_INTERVAL).fuse()); - } - _ = create_timer(WRITE_TIMEOUT).fuse() => { - tracing::trace!(%connection_id, "keepalive interval: pinging timed out"); - anyhow::bail!("timed out sending keepalive"); - } - } - } - incoming = read_message => { - let incoming = incoming.context("error reading rpc message from socket")?; - tracing::trace!(%connection_id, "incoming rpc message: received"); - tracing::trace!(%connection_id, "receive timeout: resetting"); - receive_timeout.set(create_timer(RECEIVE_TIMEOUT).fuse()); - if let (Message::Envelope(incoming), received_at) = incoming { - tracing::trace!(%connection_id, "incoming rpc message: processing"); - futures::select_biased! { - result = incoming_tx.send((incoming, received_at)).fuse() => match result { - Ok(_) => { - tracing::trace!(%connection_id, "incoming rpc message: processed"); - } - Err(_) => { - tracing::trace!(%connection_id, "incoming rpc message: channel closed"); - return Ok(()) - } - }, - _ = create_timer(WRITE_TIMEOUT).fuse() => { - tracing::trace!(%connection_id, "incoming rpc message: processing timed out"); - anyhow::bail!("timed out processing incoming message"); - } - } - } - break; - }, - _ = receive_timeout => { - tracing::trace!(%connection_id, "receive timeout: delay between messages too long"); - anyhow::bail!("delay between messages too long"); - } - } - } - } - }; - - let response_channels = connection_state.response_channels.clone(); - let stream_response_channels = connection_state.stream_response_channels.clone(); - self.connections - .write() - .insert(connection_id, connection_state); - - let incoming_rx = incoming_rx.filter_map(move |(incoming, received_at)| { - let response_channels = response_channels.clone(); - let stream_response_channels = stream_response_channels.clone(); - async move { - let message_id = incoming.id; - tracing::trace!(?incoming, "incoming message future: start"); - let _end = util::defer(move || { - tracing::trace!(%connection_id, message_id, "incoming message future: end"); - }); - - if let Some(responding_to) = incoming.responding_to { - tracing::trace!( - %connection_id, - message_id, - responding_to, - "incoming response: received" - ); - let response_channel = - response_channels.lock().as_mut()?.remove(&responding_to); - let stream_response_channel = stream_response_channels - .lock() - .as_ref()? - .get(&responding_to) - .cloned(); - - if let Some(tx) = response_channel { - let requester_resumed = oneshot::channel(); - if let Err(error) = tx.send((incoming, received_at, requester_resumed.0)) { - tracing::trace!( - %connection_id, - message_id, - responding_to = responding_to, - ?error, - "incoming response: request future dropped", - ); - } - - tracing::trace!( - %connection_id, - message_id, - responding_to, - "incoming response: waiting to resume requester" - ); - let _ = requester_resumed.1.await; - tracing::trace!( - %connection_id, - message_id, - responding_to, - "incoming response: requester resumed" - ); - } else if let Some(tx) = stream_response_channel { - let requester_resumed = oneshot::channel(); - if let Err(error) = tx.unbounded_send((Ok(incoming), requester_resumed.0)) { - tracing::debug!( - %connection_id, - message_id, - responding_to = responding_to, - ?error, - "incoming stream response: request future dropped", - ); - } - - tracing::debug!( - %connection_id, - message_id, - responding_to, - "incoming stream response: waiting to resume requester" - ); - let _ = requester_resumed.1.await; - tracing::debug!( - %connection_id, - message_id, - responding_to, - "incoming stream response: requester resumed" - ); - } else { - let message_type = proto::build_typed_envelope( - connection_id.into(), - received_at, - incoming, - ) - .map(|p| p.payload_type_name()); - tracing::warn!( - %connection_id, - message_id, - responding_to, - message_type, - "incoming response: unknown request" - ); - } - - None - } else { - tracing::trace!(%connection_id, message_id, "incoming message: received"); - proto::build_typed_envelope(connection_id.into(), received_at, incoming) - .or_else(|| { - tracing::error!( - %connection_id, - message_id, - "unable to construct a typed envelope" - ); - None - }) - } - } - }); - (connection_id, handle_io, incoming_rx.boxed()) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn add_test_connection( - self: &Arc, - connection: Connection, - executor: gpui::BackgroundExecutor, - ) -> ( - ConnectionId, - impl Future> + Send + use<>, - BoxStream<'static, Box>, - ) { - self.add_connection(connection, move |duration| executor.timer(duration)) - } - - pub fn disconnect(&self, connection_id: ConnectionId) { - self.connections.write().remove(&connection_id); - } - - #[cfg(any(test, feature = "test-support"))] - pub fn reset(&self, epoch: u32) { - self.next_connection_id.store(0, SeqCst); - self.epoch.store(epoch, SeqCst); - } - - pub fn teardown(&self) { - self.connections.write().clear(); - } - - /// Make a request and wait for a response. - pub fn request( - &self, - receiver_id: ConnectionId, - request: T, - ) -> impl Future> + use { - self.request_internal(None, receiver_id, request) - .map_ok(|envelope| envelope.payload) - } - - pub fn request_envelope( - &self, - receiver_id: ConnectionId, - request: T, - ) -> impl Future>> + use { - self.request_internal(None, receiver_id, request) - } - - pub fn forward_request( - &self, - sender_id: ConnectionId, - receiver_id: ConnectionId, - request: T, - ) -> impl Future> { - self.request_internal(Some(sender_id), receiver_id, request) - .map_ok(|envelope| envelope.payload) - } - - fn request_internal( - &self, - original_sender_id: Option, - receiver_id: ConnectionId, - request: T, - ) -> impl Future>> + use { - let envelope = request.into_envelope(0, None, original_sender_id.map(Into::into)); - let response = self.request_dynamic(receiver_id, envelope, T::NAME); - async move { - let (response, received_at) = response.await?; - Ok(TypedEnvelope { - message_id: response.id, - sender_id: receiver_id.into(), - original_sender_id: response.original_sender_id, - payload: T::Response::from_envelope(response) - .context("received response of the wrong type")?, - received_at, - }) - } - } - - /// Make a request and wait for a response. - /// - /// The caller must make sure to deserialize the response into the request's - /// response type. This interface is only useful in trait objects, where - /// generics can't be used. If you have a concrete type, use `request`. - pub fn request_dynamic( - &self, - receiver_id: ConnectionId, - mut envelope: proto::Envelope, - type_name: &'static str, - ) -> impl Future> + use<> { - let (tx, rx) = oneshot::channel(); - let send = self.connection_state(receiver_id).and_then(|connection| { - envelope.id = connection.next_message_id.fetch_add(1, SeqCst); - connection - .response_channels - .lock() - .as_mut() - .context("connection was closed")? - .insert(envelope.id, tx); - connection - .outgoing_tx - .unbounded_send(Message::Envelope(envelope)) - .context("connection was closed")?; - Ok(()) - }); - async move { - send?; - let (response, received_at, _barrier) = rx.await.context("connection was closed")?; - if let Some(proto::envelope::Payload::Error(error)) = &response.payload { - return Err(RpcError::from_proto(error, type_name)); - } - Ok((response, received_at)) - } - } - - pub fn request_stream( - &self, - receiver_id: ConnectionId, - request: T, - ) -> impl Future>>> { - let (tx, rx) = mpsc::unbounded(); - let send = self.connection_state(receiver_id).and_then(|connection| { - let message_id = connection.next_message_id.fetch_add(1, SeqCst); - let stream_response_channels = connection.stream_response_channels.clone(); - stream_response_channels - .lock() - .as_mut() - .context("connection was closed")? - .insert(message_id, tx); - connection - .outgoing_tx - .unbounded_send(Message::Envelope( - request.into_envelope(message_id, None, None), - )) - .context("connection was closed")?; - Ok((message_id, stream_response_channels)) - }); - - async move { - let (message_id, stream_response_channels) = send?; - let stream_response_channels = Arc::downgrade(&stream_response_channels); - - Ok(rx.filter_map(move |(response, _barrier)| { - let stream_response_channels = stream_response_channels.clone(); - future::ready(match response { - Ok(response) => { - if let Some(proto::envelope::Payload::Error(error)) = &response.payload { - Some(Err(RpcError::from_proto(error, T::NAME))) - } else if let Some(proto::envelope::Payload::EndStream(_)) = - &response.payload - { - // Remove the transmitting end of the response channel to end the stream. - if let Some(channels) = stream_response_channels.upgrade() - && let Some(channels) = channels.lock().as_mut() - { - channels.remove(&message_id); - } - None - } else { - Some( - T::Response::from_envelope(response) - .context("received response of the wrong type"), - ) - } - } - Err(error) => Some(Err(error)), - }) - })) - } - } - - pub fn send(&self, receiver_id: ConnectionId, message: T) -> Result<()> { - let connection = self.connection_state(receiver_id)?; - let message_id = connection - .next_message_id - .fetch_add(1, atomic::Ordering::SeqCst); - connection.outgoing_tx.unbounded_send(Message::Envelope( - message.into_envelope(message_id, None, None), - ))?; - Ok(()) - } - - pub fn send_dynamic(&self, receiver_id: ConnectionId, message: proto::Envelope) -> Result<()> { - let connection = self.connection_state(receiver_id)?; - connection - .outgoing_tx - .unbounded_send(Message::Envelope(message))?; - Ok(()) - } - - pub fn forward_send( - &self, - sender_id: ConnectionId, - receiver_id: ConnectionId, - message: T, - ) -> Result<()> { - let connection = self.connection_state(receiver_id)?; - let message_id = connection - .next_message_id - .fetch_add(1, atomic::Ordering::SeqCst); - connection - .outgoing_tx - .unbounded_send(Message::Envelope(message.into_envelope( - message_id, - None, - Some(sender_id.into()), - )))?; - Ok(()) - } - - pub fn respond( - &self, - receipt: Receipt, - response: T::Response, - ) -> Result<()> { - let connection = self.connection_state(receipt.sender_id.into())?; - let message_id = connection - .next_message_id - .fetch_add(1, atomic::Ordering::SeqCst); - connection - .outgoing_tx - .unbounded_send(Message::Envelope(response.into_envelope( - message_id, - Some(receipt.message_id), - None, - )))?; - Ok(()) - } - - pub fn end_stream(&self, receipt: Receipt) -> Result<()> { - let connection = self.connection_state(receipt.sender_id.into())?; - let message_id = connection - .next_message_id - .fetch_add(1, atomic::Ordering::SeqCst); - - let message = proto::EndStream {}; - - connection - .outgoing_tx - .unbounded_send(Message::Envelope(message.into_envelope( - message_id, - Some(receipt.message_id), - None, - )))?; - Ok(()) - } - - pub fn respond_with_error( - &self, - receipt: Receipt, - response: proto::Error, - ) -> Result<()> { - let connection = self.connection_state(receipt.sender_id.into())?; - let message_id = connection - .next_message_id - .fetch_add(1, atomic::Ordering::SeqCst); - connection - .outgoing_tx - .unbounded_send(Message::Envelope(response.into_envelope( - message_id, - Some(receipt.message_id), - None, - )))?; - Ok(()) - } - - pub fn respond_with_unhandled_message( - &self, - sender_id: ConnectionId, - request_message_id: u32, - message_type_name: &'static str, - ) -> Result<()> { - let connection = self.connection_state(sender_id)?; - let response = ErrorCode::Internal - .message(format!("message {} was not handled", message_type_name)) - .to_proto(); - let message_id = connection - .next_message_id - .fetch_add(1, atomic::Ordering::SeqCst); - connection - .outgoing_tx - .unbounded_send(Message::Envelope(response.into_envelope( - message_id, - Some(request_message_id), - None, - )))?; - Ok(()) - } - - fn connection_state(&self, connection_id: ConnectionId) -> Result { - let connections = self.connections.read(); - let connection = connections - .get(&connection_id) - .with_context(|| format!("no such connection: {connection_id}"))?; - Ok(connection.clone()) - } -} - -impl Serialize for Peer { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let mut state = serializer.serialize_struct("Peer", 2)?; - state.serialize_field("connections", &*self.connections.read())?; - state.end() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use async_tungstenite::tungstenite::Message as WebSocketMessage; - use gpui::TestAppContext; - - fn init_logger() { - zlog::init_test(); - } - - #[gpui::test(iterations = 50)] - async fn test_request_response(cx: &mut TestAppContext) { - init_logger(); - - let executor = cx.executor(); - - // create 2 clients connected to 1 server - let server = Peer::new(0); - let client1 = Peer::new(0); - let client2 = Peer::new(0); - - let (client1_to_server_conn, server_to_client_1_conn, _kill) = - Connection::in_memory(cx.executor()); - let (client1_conn_id, io_task1, client1_incoming) = - client1.add_test_connection(client1_to_server_conn, cx.executor()); - let (_, io_task2, server_incoming1) = - server.add_test_connection(server_to_client_1_conn, cx.executor()); - - let (client2_to_server_conn, server_to_client_2_conn, _kill) = - Connection::in_memory(cx.executor()); - let (client2_conn_id, io_task3, client2_incoming) = - client2.add_test_connection(client2_to_server_conn, cx.executor()); - let (_, io_task4, server_incoming2) = - server.add_test_connection(server_to_client_2_conn, cx.executor()); - - executor.spawn(io_task1).detach(); - executor.spawn(io_task2).detach(); - executor.spawn(io_task3).detach(); - executor.spawn(io_task4).detach(); - executor - .spawn(handle_messages(server_incoming1, server.clone())) - .detach(); - executor - .spawn(handle_messages(client1_incoming, client1.clone())) - .detach(); - executor - .spawn(handle_messages(server_incoming2, server.clone())) - .detach(); - executor - .spawn(handle_messages(client2_incoming, client2.clone())) - .detach(); - - assert_eq!( - client1 - .request(client1_conn_id, proto::Ping {},) - .await - .unwrap(), - proto::Ack {} - ); - - assert_eq!( - client2 - .request(client2_conn_id, proto::Ping {},) - .await - .unwrap(), - proto::Ack {} - ); - - assert_eq!( - client1 - .request(client1_conn_id, proto::Test { id: 1 },) - .await - .unwrap(), - proto::Test { id: 1 } - ); - - assert_eq!( - client2 - .request(client2_conn_id, proto::Test { id: 2 }) - .await - .unwrap(), - proto::Test { id: 2 } - ); - - client1.disconnect(client1_conn_id); - client2.disconnect(client1_conn_id); - - async fn handle_messages( - mut messages: BoxStream<'static, Box>, - peer: Arc, - ) -> Result<()> { - while let Some(envelope) = messages.next().await { - let envelope = envelope.into_any(); - if let Some(envelope) = envelope.downcast_ref::>() { - let receipt = envelope.receipt(); - peer.respond(receipt, proto::Ack {})? - } else if let Some(envelope) = envelope.downcast_ref::>() - { - peer.respond(envelope.receipt(), envelope.payload.clone())? - } else { - panic!("unknown message type"); - } - } - - Ok(()) - } - } - - #[gpui::test(iterations = 50)] - async fn test_order_of_response_and_incoming(cx: &mut TestAppContext) { - let executor = cx.executor(); - let server = Peer::new(0); - let client = Peer::new(0); - - let (client_to_server_conn, server_to_client_conn, _kill) = - Connection::in_memory(executor.clone()); - let (client_to_server_conn_id, io_task1, mut client_incoming) = - client.add_test_connection(client_to_server_conn, executor.clone()); - - let (server_to_client_conn_id, io_task2, mut server_incoming) = - server.add_test_connection(server_to_client_conn, executor.clone()); - - executor.spawn(io_task1).detach(); - executor.spawn(io_task2).detach(); - - executor - .spawn(async move { - let future = server_incoming.next().await; - let request = future - .unwrap() - .into_any() - .downcast::>() - .unwrap(); - - server - .send( - server_to_client_conn_id, - ErrorCode::Internal - .message("message 1".to_string()) - .to_proto(), - ) - .unwrap(); - server - .send( - server_to_client_conn_id, - ErrorCode::Internal - .message("message 2".to_string()) - .to_proto(), - ) - .unwrap(); - server.respond(request.receipt(), proto::Ack {}).unwrap(); - - // Prevent the connection from being dropped - server_incoming.next().await; - }) - .detach(); - - let events = Arc::new(Mutex::new(Vec::new())); - - let response = client.request(client_to_server_conn_id, proto::Ping {}); - let response_task = executor.spawn({ - let events = events.clone(); - async move { - response.await.unwrap(); - events.lock().push("response".to_string()); - } - }); - - executor - .spawn({ - let events = events.clone(); - async move { - let incoming1 = client_incoming - .next() - .await - .unwrap() - .into_any() - .downcast::>() - .unwrap(); - events.lock().push(incoming1.payload.message); - let incoming2 = client_incoming - .next() - .await - .unwrap() - .into_any() - .downcast::>() - .unwrap(); - events.lock().push(incoming2.payload.message); - - // Prevent the connection from being dropped - client_incoming.next().await; - } - }) - .detach(); - - response_task.await; - assert_eq!( - &*events.lock(), - &[ - "message 1".to_string(), - "message 2".to_string(), - "response".to_string() - ] - ); - } - - #[gpui::test(iterations = 50)] - async fn test_dropping_request_before_completion(cx: &mut TestAppContext) { - let executor = cx.executor(); - let server = Peer::new(0); - let client = Peer::new(0); - - let (client_to_server_conn, server_to_client_conn, _kill) = - Connection::in_memory(cx.executor()); - let (client_to_server_conn_id, io_task1, mut client_incoming) = - client.add_test_connection(client_to_server_conn, cx.executor()); - let (server_to_client_conn_id, io_task2, mut server_incoming) = - server.add_test_connection(server_to_client_conn, cx.executor()); - - executor.spawn(io_task1).detach(); - executor.spawn(io_task2).detach(); - - executor - .spawn(async move { - let request1 = server_incoming - .next() - .await - .unwrap() - .into_any() - .downcast::>() - .unwrap(); - let request2 = server_incoming - .next() - .await - .unwrap() - .into_any() - .downcast::>() - .unwrap(); - - server - .send( - server_to_client_conn_id, - ErrorCode::Internal - .message("message 1".to_string()) - .to_proto(), - ) - .unwrap(); - server - .send( - server_to_client_conn_id, - ErrorCode::Internal - .message("message 2".to_string()) - .to_proto(), - ) - .unwrap(); - server.respond(request1.receipt(), proto::Ack {}).unwrap(); - server.respond(request2.receipt(), proto::Ack {}).unwrap(); - - // Prevent the connection from being dropped - server_incoming.next().await; - }) - .detach(); - - let events = Arc::new(Mutex::new(Vec::new())); - - let request1 = client.request(client_to_server_conn_id, proto::Ping {}); - let request1_task = executor.spawn(request1); - let request2 = client.request(client_to_server_conn_id, proto::Ping {}); - let request2_task = executor.spawn({ - let events = events.clone(); - async move { - request2.await.unwrap(); - events.lock().push("response 2".to_string()); - } - }); - - executor - .spawn({ - let events = events.clone(); - async move { - let incoming1 = client_incoming - .next() - .await - .unwrap() - .into_any() - .downcast::>() - .unwrap(); - events.lock().push(incoming1.payload.message); - let incoming2 = client_incoming - .next() - .await - .unwrap() - .into_any() - .downcast::>() - .unwrap(); - events.lock().push(incoming2.payload.message); - - // Prevent the connection from being dropped - client_incoming.next().await; - } - }) - .detach(); - - // Allow the request to make some progress before dropping it. - cx.executor().simulate_random_delay().await; - drop(request1_task); - - request2_task.await; - assert_eq!( - &*events.lock(), - &[ - "message 1".to_string(), - "message 2".to_string(), - "response 2".to_string() - ] - ); - } - - #[gpui::test(iterations = 50)] - async fn test_disconnect(cx: &mut TestAppContext) { - let executor = cx.executor(); - - let (client_conn, mut server_conn, _kill) = Connection::in_memory(executor.clone()); - - let client = Peer::new(0); - let (connection_id, io_handler, mut incoming) = - client.add_test_connection(client_conn, executor.clone()); - - let (io_ended_tx, io_ended_rx) = oneshot::channel(); - executor - .spawn(async move { - io_handler.await.ok(); - io_ended_tx.send(()).unwrap(); - }) - .detach(); - - let (messages_ended_tx, messages_ended_rx) = oneshot::channel(); - executor - .spawn(async move { - incoming.next().await; - messages_ended_tx.send(()).unwrap(); - }) - .detach(); - - client.disconnect(connection_id); - - let _ = io_ended_rx.await; - let _ = messages_ended_rx.await; - assert!( - server_conn - .send(WebSocketMessage::Binary(vec![].into())) - .await - .is_err() - ); - } - - #[gpui::test(iterations = 50)] - async fn test_io_error(cx: &mut TestAppContext) { - let executor = cx.executor(); - let (client_conn, mut server_conn, _kill) = Connection::in_memory(executor.clone()); - - let client = Peer::new(0); - let (connection_id, io_handler, mut incoming) = - client.add_test_connection(client_conn, executor.clone()); - executor.spawn(io_handler).detach(); - executor - .spawn(async move { incoming.next().await }) - .detach(); - - let response = executor.spawn(client.request(connection_id, proto::Ping {})); - let _request = server_conn.rx.next().await.unwrap().unwrap(); - - drop(server_conn); - assert_eq!( - response.await.unwrap_err().to_string(), - "connection was closed" - ); - } -} diff --git a/crates/rpc/src/proto_client.rs b/crates/rpc/src/proto_client.rs deleted file mode 100644 index 3850ff5820..0000000000 --- a/crates/rpc/src/proto_client.rs +++ /dev/null @@ -1,531 +0,0 @@ -use anyhow::{Context, Result}; -use collections::HashMap; -use futures::{ - Future, FutureExt as _, - channel::oneshot, - future::{BoxFuture, LocalBoxFuture}, -}; -use gpui::{AnyEntity, AnyWeakEntity, AsyncApp, BackgroundExecutor, Entity, FutureExt as _}; -use parking_lot::Mutex; -use proto::{ - AnyTypedEnvelope, EntityMessage, Envelope, EnvelopedMessage, LspRequestId, LspRequestMessage, - RequestMessage, TypedEnvelope, error::ErrorExt as _, -}; -use std::{ - any::{Any, TypeId}, - sync::{ - Arc, OnceLock, - atomic::{self, AtomicU64}, - }, - time::Duration, -}; - -#[derive(Clone)] -pub struct AnyProtoClient(Arc); - -type RequestIds = Arc< - Mutex< - HashMap< - LspRequestId, - oneshot::Sender< - Result< - Option>>>>, - >, - >, - >, - >, ->; - -static NEXT_LSP_REQUEST_ID: OnceLock> = OnceLock::new(); -static REQUEST_IDS: OnceLock = OnceLock::new(); - -struct State { - client: Arc, - next_lsp_request_id: Arc, - request_ids: RequestIds, -} - -pub trait ProtoClient: Send + Sync { - fn request( - &self, - envelope: Envelope, - request_type: &'static str, - ) -> BoxFuture<'static, Result>; - - fn send(&self, envelope: Envelope, message_type: &'static str) -> Result<()>; - - fn send_response(&self, envelope: Envelope, message_type: &'static str) -> Result<()>; - - fn message_handler_set(&self) -> &parking_lot::Mutex; - - fn is_via_collab(&self) -> bool; - fn has_wsl_interop(&self) -> bool; -} - -#[derive(Default)] -pub struct ProtoMessageHandlerSet { - pub entity_types_by_message_type: HashMap, - pub entities_by_type_and_remote_id: HashMap<(TypeId, u64), EntityMessageSubscriber>, - pub entity_id_extractors: HashMap u64>, - pub entities_by_message_type: HashMap, - pub message_handlers: HashMap, -} - -pub type ProtoMessageHandler = Arc< - dyn Send - + Sync - + Fn( - AnyEntity, - Box, - AnyProtoClient, - AsyncApp, - ) -> LocalBoxFuture<'static, Result<()>>, ->; - -impl ProtoMessageHandlerSet { - pub fn clear(&mut self) { - self.message_handlers.clear(); - self.entities_by_message_type.clear(); - self.entities_by_type_and_remote_id.clear(); - self.entity_id_extractors.clear(); - } - - fn add_message_handler( - &mut self, - message_type_id: TypeId, - entity: gpui::AnyWeakEntity, - handler: ProtoMessageHandler, - ) { - self.entities_by_message_type - .insert(message_type_id, entity); - let prev_handler = self.message_handlers.insert(message_type_id, handler); - if prev_handler.is_some() { - panic!("registered handler for the same message twice"); - } - } - - fn add_entity_message_handler( - &mut self, - message_type_id: TypeId, - entity_type_id: TypeId, - entity_id_extractor: fn(&dyn AnyTypedEnvelope) -> u64, - handler: ProtoMessageHandler, - ) { - self.entity_id_extractors - .entry(message_type_id) - .or_insert(entity_id_extractor); - self.entity_types_by_message_type - .insert(message_type_id, entity_type_id); - let prev_handler = self.message_handlers.insert(message_type_id, handler); - if prev_handler.is_some() { - panic!("registered handler for the same message twice"); - } - } - - pub fn handle_message( - this: &parking_lot::Mutex, - message: Box, - client: AnyProtoClient, - cx: AsyncApp, - ) -> Option>> { - let payload_type_id = message.payload_type_id(); - let mut this = this.lock(); - let handler = this.message_handlers.get(&payload_type_id)?.clone(); - let entity = if let Some(entity) = this.entities_by_message_type.get(&payload_type_id) { - entity.upgrade()? - } else { - let extract_entity_id = *this.entity_id_extractors.get(&payload_type_id)?; - let entity_type_id = *this.entity_types_by_message_type.get(&payload_type_id)?; - let entity_id = (extract_entity_id)(message.as_ref()); - match this - .entities_by_type_and_remote_id - .get_mut(&(entity_type_id, entity_id))? - { - EntityMessageSubscriber::Pending(pending) => { - pending.push(message); - return None; - } - EntityMessageSubscriber::Entity { handle } => handle.upgrade()?, - } - }; - drop(this); - Some(handler(entity, message, client, cx)) - } -} - -pub enum EntityMessageSubscriber { - Entity { handle: AnyWeakEntity }, - Pending(Vec>), -} - -impl std::fmt::Debug for EntityMessageSubscriber { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - EntityMessageSubscriber::Entity { handle } => f - .debug_struct("EntityMessageSubscriber::Entity") - .field("handle", handle) - .finish(), - EntityMessageSubscriber::Pending(vec) => f - .debug_struct("EntityMessageSubscriber::Pending") - .field( - "envelopes", - &vec.iter() - .map(|envelope| envelope.payload_type_name()) - .collect::>(), - ) - .finish(), - } - } -} - -impl From> for AnyProtoClient -where - T: ProtoClient + 'static, -{ - fn from(client: Arc) -> Self { - Self::new(client) - } -} - -impl AnyProtoClient { - pub fn new(client: Arc) -> Self { - Self(Arc::new(State { - client, - next_lsp_request_id: NEXT_LSP_REQUEST_ID - .get_or_init(|| Arc::new(AtomicU64::new(0))) - .clone(), - request_ids: REQUEST_IDS.get_or_init(RequestIds::default).clone(), - })) - } - - pub fn is_via_collab(&self) -> bool { - self.0.client.is_via_collab() - } - - pub fn request( - &self, - request: T, - ) -> impl Future> + use { - let envelope = request.into_envelope(0, None, None); - let response = self.0.client.request(envelope, T::NAME); - async move { - T::Response::from_envelope(response.await?) - .context("received response of the wrong type") - } - } - - pub fn send(&self, request: T) -> Result<()> { - let envelope = request.into_envelope(0, None, None); - self.0.client.send(envelope, T::NAME) - } - - pub fn send_response(&self, request_id: u32, request: T) -> Result<()> { - let envelope = request.into_envelope(0, Some(request_id), None); - self.0.client.send(envelope, T::NAME) - } - - pub fn request_lsp( - &self, - project_id: u64, - server_id: Option, - timeout: Duration, - executor: BackgroundExecutor, - request: T, - ) -> impl Future< - Output = Result>>>>, - > + use - where - T: LspRequestMessage, - { - let new_id = LspRequestId( - self.0 - .next_lsp_request_id - .fetch_add(1, atomic::Ordering::Acquire), - ); - let (tx, rx) = oneshot::channel(); - { - self.0.request_ids.lock().insert(new_id, tx); - } - - let query = proto::LspQuery { - project_id, - server_id, - lsp_request_id: new_id.0, - request: Some(request.to_proto_query()), - }; - let request = self.request(query); - let request_ids = self.0.request_ids.clone(); - async move { - match request.await { - Ok(_request_enqueued) => {} - Err(e) => { - request_ids.lock().remove(&new_id); - return Err(e).context("sending LSP proto request"); - } - } - - let response = rx.with_timeout(timeout, &executor).await; - { - request_ids.lock().remove(&new_id); - } - match response { - Ok(Ok(response)) => { - let response = response - .context("waiting for LSP proto response")? - .map(|response| { - anyhow::Ok(TypedEnvelope { - payload: response - .payload - .into_iter() - .map(|lsp_response| lsp_response.into_response::()) - .collect::>>()?, - sender_id: response.sender_id, - original_sender_id: response.original_sender_id, - message_id: response.message_id, - received_at: response.received_at, - }) - }) - .transpose() - .context("converting LSP proto response")?; - Ok(response) - } - Err(_cancelled_due_timeout) => Ok(None), - Ok(Err(_channel_dropped)) => Ok(None), - } - } - } - - pub fn send_lsp_response( - &self, - project_id: u64, - lsp_request_id: LspRequestId, - server_responses: HashMap, - ) -> Result<()> { - self.send(proto::LspQueryResponse { - project_id, - lsp_request_id: lsp_request_id.0, - responses: server_responses - .into_iter() - .map(|(server_id, response)| proto::LspResponse { - server_id, - response: Some(T::response_to_proto_query(response)), - }) - .collect(), - }) - } - - pub fn handle_lsp_response(&self, mut envelope: TypedEnvelope) { - let request_id = LspRequestId(envelope.payload.lsp_request_id); - let mut response_senders = self.0.request_ids.lock(); - if let Some(tx) = response_senders.remove(&request_id) { - let responses = envelope.payload.responses.drain(..).collect::>(); - tx.send(Ok(Some(proto::TypedEnvelope { - sender_id: envelope.sender_id, - original_sender_id: envelope.original_sender_id, - message_id: envelope.message_id, - received_at: envelope.received_at, - payload: responses - .into_iter() - .filter_map(|response| { - use proto::lsp_response::Response; - - let server_id = response.server_id; - let response = match response.response? { - Response::GetReferencesResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetDocumentColorResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetHoverResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetCodeActionsResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetSignatureHelpResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetCodeLensResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetDocumentDiagnosticsResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetDefinitionResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetDeclarationResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetTypeDefinitionResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetImplementationResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::InlayHintsResponse(response) => { - to_any_envelope(&envelope, response) - } - }; - Some(proto::ProtoLspResponse { - server_id, - response, - }) - }) - .collect(), - }))) - .ok(); - } - } - - pub fn add_request_handler(&self, entity: gpui::WeakEntity, handler: H) - where - M: RequestMessage, - E: 'static, - H: 'static + Sync + Fn(Entity, TypedEnvelope, AsyncApp) -> F + Send + Sync, - F: 'static + Future>, - { - self.0 - .client - .message_handler_set() - .lock() - .add_message_handler( - TypeId::of::(), - entity.into(), - Arc::new(move |entity, envelope, client, cx| { - let entity = entity.downcast::().unwrap(); - let envelope = envelope.into_any().downcast::>().unwrap(); - let request_id = envelope.message_id(); - handler(entity, *envelope, cx) - .then(move |result| async move { - match result { - Ok(response) => { - client.send_response(request_id, response)?; - Ok(()) - } - Err(error) => { - client.send_response(request_id, error.to_proto())?; - Err(error) - } - } - }) - .boxed_local() - }), - ) - } - - pub fn add_entity_request_handler(&self, handler: H) - where - M: EnvelopedMessage + RequestMessage + EntityMessage, - E: 'static, - H: 'static + Sync + Send + Fn(gpui::Entity, TypedEnvelope, AsyncApp) -> F, - F: 'static + Future>, - { - let message_type_id = TypeId::of::(); - let entity_type_id = TypeId::of::(); - let entity_id_extractor = |envelope: &dyn AnyTypedEnvelope| { - (envelope as &dyn Any) - .downcast_ref::>() - .unwrap() - .payload - .remote_entity_id() - }; - self.0 - .client - .message_handler_set() - .lock() - .add_entity_message_handler( - message_type_id, - entity_type_id, - entity_id_extractor, - Arc::new(move |entity, envelope, client, cx| { - let entity = entity.downcast::().unwrap(); - let envelope = envelope.into_any().downcast::>().unwrap(); - let request_id = envelope.message_id(); - handler(entity, *envelope, cx) - .then(move |result| async move { - match result { - Ok(response) => { - client.send_response(request_id, response)?; - Ok(()) - } - Err(error) => { - client.send_response(request_id, error.to_proto())?; - Err(error) - } - } - }) - .boxed_local() - }), - ); - } - - pub fn add_entity_message_handler(&self, handler: H) - where - M: EnvelopedMessage + EntityMessage, - E: 'static, - H: 'static + Sync + Send + Fn(gpui::Entity, TypedEnvelope, AsyncApp) -> F, - F: 'static + Future>, - { - let message_type_id = TypeId::of::(); - let entity_type_id = TypeId::of::(); - let entity_id_extractor = |envelope: &dyn AnyTypedEnvelope| { - (envelope as &dyn Any) - .downcast_ref::>() - .unwrap() - .payload - .remote_entity_id() - }; - self.0 - .client - .message_handler_set() - .lock() - .add_entity_message_handler( - message_type_id, - entity_type_id, - entity_id_extractor, - Arc::new(move |entity, envelope, _, cx| { - let entity = entity.downcast::().unwrap(); - let envelope = envelope.into_any().downcast::>().unwrap(); - handler(entity, *envelope, cx).boxed_local() - }), - ); - } - - pub fn subscribe_to_entity(&self, remote_id: u64, entity: &Entity) { - let id = (TypeId::of::(), remote_id); - - let mut message_handlers = self.0.client.message_handler_set().lock(); - if message_handlers - .entities_by_type_and_remote_id - .contains_key(&id) - { - panic!("already subscribed to entity"); - } - - message_handlers.entities_by_type_and_remote_id.insert( - id, - EntityMessageSubscriber::Entity { - handle: entity.downgrade().into(), - }, - ); - } - - pub fn has_wsl_interop(&self) -> bool { - self.0.client.has_wsl_interop() - } -} - -fn to_any_envelope( - envelope: &TypedEnvelope, - response: T, -) -> Box { - Box::new(proto::TypedEnvelope { - sender_id: envelope.sender_id, - original_sender_id: envelope.original_sender_id, - message_id: envelope.message_id, - received_at: envelope.received_at, - payload: response, - }) as Box<_> -} diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs deleted file mode 100644 index ad1ebb757c..0000000000 --- a/crates/rpc/src/rpc.rs +++ /dev/null @@ -1,21 +0,0 @@ -pub mod auth; -mod conn; -mod extension; -mod message_stream; -mod notification; -mod peer; - -pub use conn::Connection; -pub use extension::*; -pub use notification::*; -pub use peer::*; -pub use proto; -pub use proto::{Receipt, TypedEnvelope, error::*}; -mod macros; - -#[cfg(feature = "gpui")] -mod proto_client; -#[cfg(feature = "gpui")] -pub use proto_client::*; - -pub const PROTOCOL_VERSION: u32 = 68; diff --git a/crates/rules_library/Cargo.toml b/crates/rules_library/Cargo.toml deleted file mode 100644 index d2fdd765e0..0000000000 --- a/crates/rules_library/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "rules_library" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/rules_library.rs" - -[dependencies] -anyhow.workspace = true -collections.workspace = true -editor.workspace = true -gpui.workspace = true -language.workspace = true -language_model.workspace = true -log.workspace = true -menu.workspace = true -picker.workspace = true -prompt_store.workspace = true -release_channel.workspace = true -rope.workspace = true -serde.workspace = true -settings.workspace = true -theme.workspace = true -title_bar.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -zed_actions.workspace = true diff --git a/crates/rules_library/LICENSE-GPL b/crates/rules_library/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/rules_library/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs deleted file mode 100644 index 09b7e0b539..0000000000 --- a/crates/rules_library/src/rules_library.rs +++ /dev/null @@ -1,1431 +0,0 @@ -use anyhow::Result; -use collections::{HashMap, HashSet}; -use editor::{CompletionProvider, SelectionEffects}; -use editor::{CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle, actions::Tab}; -use gpui::{ - Action, App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable, - PromptLevel, Subscription, Task, TextStyle, TitlebarOptions, WindowBounds, WindowHandle, - WindowOptions, actions, point, size, transparent_black, -}; -use language::{Buffer, LanguageRegistry, language_settings::SoftWrap}; -use language_model::{ - ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, -}; -use picker::{Picker, PickerDelegate}; -use release_channel::ReleaseChannel; -use rope::Rope; -use settings::Settings; -use std::rc::Rc; -use std::sync::Arc; -use std::sync::atomic::AtomicBool; -use std::time::Duration; -use theme::ThemeSettings; -use title_bar::platform_title_bar::PlatformTitleBar; -use ui::{ - Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, Render, Tooltip, prelude::*, -}; -use util::{ResultExt, TryFutureExt}; -use workspace::{Workspace, WorkspaceSettings, client_side_decorations}; -use zed_actions::assistant::InlineAssist; - -use prompt_store::*; - -pub fn init(cx: &mut App) { - prompt_store::init(cx); -} - -actions!( - rules_library, - [ - /// Creates a new rule in the rules library. - NewRule, - /// Deletes the selected rule. - DeleteRule, - /// Duplicates the selected rule. - DuplicateRule, - /// Toggles whether the selected rule is a default rule. - ToggleDefaultRule - ] -); - -const BUILT_IN_TOOLTIP_TEXT: &str = concat!( - "This rule supports special functionality.\n", - "It's read-only, but you can remove it from your default rules." -); - -pub trait InlineAssistDelegate { - fn assist( - &self, - prompt_editor: &Entity, - initial_prompt: Option, - window: &mut Window, - cx: &mut Context, - ); - - /// Returns whether the Agent panel was focused. - fn focus_agent_panel( - &self, - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, - ) -> bool; -} - -/// This function opens a new rules library window if one doesn't exist already. -/// If one exists, it brings it to the foreground. -/// -/// Note that, when opening a new window, this waits for the PromptStore to be -/// initialized. If it was initialized successfully, it returns a window handle -/// to a rules library. -pub fn open_rules_library( - language_registry: Arc, - inline_assist_delegate: Box, - make_completion_provider: Rc Rc>, - prompt_to_select: Option, - cx: &mut App, -) -> Task>> { - let store = PromptStore::global(cx); - cx.spawn(async move |cx| { - // We query windows in spawn so that all windows have been returned to GPUI - let existing_window = cx - .update(|cx| { - let existing_window = cx - .windows() - .into_iter() - .find_map(|window| window.downcast::()); - if let Some(existing_window) = existing_window { - existing_window - .update(cx, |rules_library, window, cx| { - if let Some(prompt_to_select) = prompt_to_select { - rules_library.load_rule(prompt_to_select, true, window, cx); - } - window.activate_window() - }) - .ok(); - - Some(existing_window) - } else { - None - } - }) - .ok() - .flatten(); - - if let Some(existing_window) = existing_window { - return Ok(existing_window); - } - - let store = store.await?; - cx.update(|cx| { - let app_id = ReleaseChannel::global(cx).app_id(); - let bounds = Bounds::centered(None, size(px(1024.0), px(768.0)), cx); - let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") { - Ok(val) if val == "server" => gpui::WindowDecorations::Server, - Ok(val) if val == "client" => gpui::WindowDecorations::Client, - _ => match WorkspaceSettings::get_global(cx).window_decorations { - settings::WindowDecorations::Server => gpui::WindowDecorations::Server, - settings::WindowDecorations::Client => gpui::WindowDecorations::Client, - }, - }; - cx.open_window( - WindowOptions { - titlebar: Some(TitlebarOptions { - title: Some("Rules Library".into()), - appears_transparent: true, - traffic_light_position: Some(point(px(12.0), px(12.0))), - }), - app_id: Some(app_id.to_owned()), - window_bounds: Some(WindowBounds::Windowed(bounds)), - window_background: cx.theme().window_background_appearance(), - window_decorations: Some(window_decorations), - window_min_size: Some(DEFAULT_ADDITIONAL_WINDOW_SIZE), - kind: gpui::WindowKind::Floating, - ..Default::default() - }, - |window, cx| { - cx.new(|cx| { - RulesLibrary::new( - store, - language_registry, - inline_assist_delegate, - make_completion_provider, - prompt_to_select, - window, - cx, - ) - }) - }, - ) - })? - }) -} - -pub struct RulesLibrary { - title_bar: Option>, - store: Entity, - language_registry: Arc, - rule_editors: HashMap, - active_rule_id: Option, - picker: Entity>, - pending_load: Task<()>, - inline_assist_delegate: Box, - make_completion_provider: Rc Rc>, - _subscriptions: Vec, -} - -struct RuleEditor { - title_editor: Entity, - body_editor: Entity, - token_count: Option, - pending_token_count: Task>, - next_title_and_body_to_save: Option<(String, Rope)>, - pending_save: Option>>, - _subscriptions: Vec, -} - -enum RulePickerEntry { - Header(SharedString), - Rule(PromptMetadata), - Separator, -} - -struct RulePickerDelegate { - store: Entity, - selected_index: usize, - filtered_entries: Vec, -} - -enum RulePickerEvent { - Selected { prompt_id: PromptId }, - Confirmed { prompt_id: PromptId }, - Deleted { prompt_id: PromptId }, - ToggledDefault { prompt_id: PromptId }, -} - -impl EventEmitter for Picker {} - -impl PickerDelegate for RulePickerDelegate { - type ListItem = AnyElement; - - fn match_count(&self) -> usize { - self.filtered_entries.len() - } - - fn no_matches_text(&self, _window: &mut Window, cx: &mut App) -> Option { - let text = if self.store.read(cx).prompt_count() == 0 { - "No rules.".into() - } else { - "No rules found matching your search.".into() - }; - Some(text) - } - - 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)); - - if let Some(RulePickerEntry::Rule(rule)) = self.filtered_entries.get(self.selected_index) { - cx.emit(RulePickerEvent::Selected { prompt_id: rule.id }); - } - - cx.notify(); - } - - fn can_select(&mut self, ix: usize, _: &mut Window, _: &mut Context>) -> bool { - match self.filtered_entries.get(ix) { - Some(RulePickerEntry::Rule(_)) => true, - Some(RulePickerEntry::Header(_)) | Some(RulePickerEntry::Separator) | None => false, - } - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Search…".into() - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - let cancellation_flag = Arc::new(AtomicBool::default()); - let search = self.store.read(cx).search(query, cancellation_flag, cx); - - let prev_prompt_id = self - .filtered_entries - .get(self.selected_index) - .and_then(|entry| { - if let RulePickerEntry::Rule(rule) = entry { - Some(rule.id) - } else { - None - } - }); - - cx.spawn_in(window, async move |this, cx| { - let (filtered_entries, selected_index) = cx - .background_spawn(async move { - let matches = search.await; - - let (default_rules, non_default_rules): (Vec<_>, Vec<_>) = - matches.iter().partition(|rule| rule.default); - - let mut filtered_entries = Vec::new(); - - if !default_rules.is_empty() { - filtered_entries.push(RulePickerEntry::Header("Default Rules".into())); - - for rule in default_rules { - filtered_entries.push(RulePickerEntry::Rule(rule.clone())); - } - - filtered_entries.push(RulePickerEntry::Separator); - } - - for rule in non_default_rules { - filtered_entries.push(RulePickerEntry::Rule(rule.clone())); - } - - let selected_index = prev_prompt_id - .and_then(|prev_prompt_id| { - filtered_entries.iter().position(|entry| { - if let RulePickerEntry::Rule(rule) = entry { - rule.id == prev_prompt_id - } else { - false - } - }) - }) - .unwrap_or_else(|| { - filtered_entries - .iter() - .position(|entry| matches!(entry, RulePickerEntry::Rule(_))) - .unwrap_or(0) - }); - - (filtered_entries, selected_index) - }) - .await; - - this.update_in(cx, |this, window, cx| { - this.delegate.filtered_entries = filtered_entries; - this.set_selected_index( - selected_index, - Some(picker::Direction::Down), - true, - window, - cx, - ); - cx.notify(); - }) - .ok(); - }) - } - - fn confirm(&mut self, _secondary: bool, _: &mut Window, cx: &mut Context>) { - if let Some(RulePickerEntry::Rule(rule)) = self.filtered_entries.get(self.selected_index) { - cx.emit(RulePickerEvent::Confirmed { prompt_id: rule.id }); - } - } - - fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context>) {} - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - match self.filtered_entries.get(ix)? { - RulePickerEntry::Header(title) => Some( - ListSubHeader::new(title.clone()) - .end_slot( - IconButton::new("info", IconName::Info) - .style(ButtonStyle::Transparent) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .tooltip(Tooltip::text( - "Default Rules are attached by default with every new thread.", - )) - .into_any_element(), - ) - .inset(true) - .into_any_element(), - ), - RulePickerEntry::Separator => Some( - h_flex() - .py_1() - .child(Divider::horizontal()) - .into_any_element(), - ), - RulePickerEntry::Rule(rule) => { - let default = rule.default; - let prompt_id = rule.id; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child( - Label::new(rule.title.clone().unwrap_or("Untitled".into())) - .truncate() - .mr_10(), - ) - .end_slot::(default.then(|| { - IconButton::new("toggle-default-rule", IconName::Paperclip) - .toggle_state(true) - .icon_color(Color::Accent) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Remove from Default Rules")) - .on_click(cx.listener(move |_, _, _, cx| { - cx.emit(RulePickerEvent::ToggledDefault { prompt_id }) - })) - })) - .end_hover_slot( - h_flex() - .child(if prompt_id.is_built_in() { - div() - .id("built-in-rule") - .child(Icon::new(IconName::FileLock).color(Color::Muted)) - .tooltip(move |_window, cx| { - Tooltip::with_meta( - "Built-in rule", - None, - BUILT_IN_TOOLTIP_TEXT, - cx, - ) - }) - .into_any() - } else { - IconButton::new("delete-rule", IconName::Trash) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Delete Rule")) - .on_click(cx.listener(move |_, _, _, cx| { - cx.emit(RulePickerEvent::Deleted { prompt_id }) - })) - .into_any_element() - }) - .child( - IconButton::new("toggle-default-rule", IconName::Plus) - .selected_icon(IconName::Dash) - .toggle_state(default) - .icon_size(IconSize::Small) - .icon_color(if default { - Color::Accent - } else { - Color::Muted - }) - .map(|this| { - if default { - this.tooltip(Tooltip::text( - "Remove from Default Rules", - )) - } else { - this.tooltip(move |_window, cx| { - Tooltip::with_meta( - "Add to Default Rules", - None, - "Always included in every thread.", - cx, - ) - }) - } - }) - .on_click(cx.listener(move |_, _, _, cx| { - cx.emit(RulePickerEvent::ToggledDefault { prompt_id }) - })), - ), - ) - .into_any_element(), - ) - } - } - } - - fn render_editor( - &self, - editor: &Entity, - _: &mut Window, - cx: &mut Context>, - ) -> Div { - h_flex() - .py_1() - .px_1p5() - .mx_1() - .gap_1p5() - .rounded_sm() - .bg(cx.theme().colors().editor_background) - .border_1() - .border_color(cx.theme().colors().border) - .child(Icon::new(IconName::MagnifyingGlass).color(Color::Muted)) - .child(editor.clone()) - } -} - -impl RulesLibrary { - fn new( - store: Entity, - language_registry: Arc, - inline_assist_delegate: Box, - make_completion_provider: Rc Rc>, - rule_to_select: Option, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let (_selected_index, _matches) = if let Some(rule_to_select) = rule_to_select { - let matches = store.read(cx).all_prompt_metadata(); - let selected_index = matches - .iter() - .enumerate() - .find(|(_, metadata)| metadata.id == rule_to_select) - .map_or(0, |(ix, _)| ix); - (selected_index, matches) - } else { - (0, vec![]) - }; - - let picker_delegate = RulePickerDelegate { - store: store.clone(), - selected_index: 0, - filtered_entries: Vec::new(), - }; - - let picker = cx.new(|cx| { - let picker = Picker::list(picker_delegate, window, cx) - .modal(false) - .max_height(None); - picker.focus(window, cx); - picker - }); - - Self { - title_bar: if !cfg!(target_os = "macos") { - Some(cx.new(|cx| PlatformTitleBar::new("rules-library-title-bar", cx))) - } else { - None - }, - store, - language_registry, - rule_editors: HashMap::default(), - active_rule_id: None, - pending_load: Task::ready(()), - inline_assist_delegate, - make_completion_provider, - _subscriptions: vec![cx.subscribe_in(&picker, window, Self::handle_picker_event)], - picker, - } - } - - fn handle_picker_event( - &mut self, - _: &Entity>, - event: &RulePickerEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - RulePickerEvent::Selected { prompt_id } => { - self.load_rule(*prompt_id, false, window, cx); - } - RulePickerEvent::Confirmed { prompt_id } => { - self.load_rule(*prompt_id, true, window, cx); - } - RulePickerEvent::ToggledDefault { prompt_id } => { - self.toggle_default_for_rule(*prompt_id, window, cx); - } - RulePickerEvent::Deleted { prompt_id } => { - self.delete_rule(*prompt_id, window, cx); - } - } - } - - pub fn new_rule(&mut self, window: &mut Window, cx: &mut Context) { - // If we already have an untitled rule, use that instead - // of creating a new one. - if let Some(metadata) = self.store.read(cx).first() - && metadata.title.is_none() - { - self.load_rule(metadata.id, true, window, cx); - return; - } - - let prompt_id = PromptId::new(); - let save = self.store.update(cx, |store, cx| { - store.save(prompt_id, None, false, "".into(), cx) - }); - self.picker - .update(cx, |picker, cx| picker.refresh(window, cx)); - cx.spawn_in(window, async move |this, cx| { - save.await?; - this.update_in(cx, |this, window, cx| { - this.load_rule(prompt_id, true, window, cx) - }) - }) - .detach_and_log_err(cx); - } - - pub fn save_rule(&mut self, prompt_id: PromptId, window: &mut Window, cx: &mut Context) { - const SAVE_THROTTLE: Duration = Duration::from_millis(500); - - if prompt_id.is_built_in() { - return; - } - - let rule_metadata = self.store.read(cx).metadata(prompt_id).unwrap(); - let rule_editor = self.rule_editors.get_mut(&prompt_id).unwrap(); - let title = rule_editor.title_editor.read(cx).text(cx); - let body = rule_editor.body_editor.update(cx, |editor, cx| { - editor - .buffer() - .read(cx) - .as_singleton() - .unwrap() - .read(cx) - .as_rope() - .clone() - }); - - let store = self.store.clone(); - let executor = cx.background_executor().clone(); - - rule_editor.next_title_and_body_to_save = Some((title, body)); - if rule_editor.pending_save.is_none() { - rule_editor.pending_save = Some(cx.spawn_in(window, async move |this, cx| { - async move { - loop { - let title_and_body = this.update(cx, |this, _| { - this.rule_editors - .get_mut(&prompt_id)? - .next_title_and_body_to_save - .take() - })?; - - if let Some((title, body)) = title_and_body { - let title = if title.trim().is_empty() { - None - } else { - Some(SharedString::from(title)) - }; - cx.update(|_window, cx| { - store.update(cx, |store, cx| { - store.save(prompt_id, title, rule_metadata.default, body, cx) - }) - })? - .await - .log_err(); - this.update_in(cx, |this, window, cx| { - this.picker - .update(cx, |picker, cx| picker.refresh(window, cx)); - cx.notify(); - })?; - - executor.timer(SAVE_THROTTLE).await; - } else { - break; - } - } - - this.update(cx, |this, _cx| { - if let Some(rule_editor) = this.rule_editors.get_mut(&prompt_id) { - rule_editor.pending_save = None; - } - }) - } - .log_err() - .await - })); - } - } - - pub fn delete_active_rule(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(active_rule_id) = self.active_rule_id { - self.delete_rule(active_rule_id, window, cx); - } - } - - pub fn duplicate_active_rule(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(active_rule_id) = self.active_rule_id { - self.duplicate_rule(active_rule_id, window, cx); - } - } - - pub fn toggle_default_for_active_rule(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(active_rule_id) = self.active_rule_id { - self.toggle_default_for_rule(active_rule_id, window, cx); - } - } - - pub fn toggle_default_for_rule( - &mut self, - prompt_id: PromptId, - window: &mut Window, - cx: &mut Context, - ) { - self.store.update(cx, move |store, cx| { - if let Some(rule_metadata) = store.metadata(prompt_id) { - store - .save_metadata(prompt_id, rule_metadata.title, !rule_metadata.default, cx) - .detach_and_log_err(cx); - } - }); - self.picker - .update(cx, |picker, cx| picker.refresh(window, cx)); - cx.notify(); - } - - pub fn load_rule( - &mut self, - prompt_id: PromptId, - focus: bool, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(rule_editor) = self.rule_editors.get(&prompt_id) { - if focus { - rule_editor - .body_editor - .update(cx, |editor, cx| window.focus(&editor.focus_handle(cx))); - } - self.set_active_rule(Some(prompt_id), window, cx); - } else if let Some(rule_metadata) = self.store.read(cx).metadata(prompt_id) { - let language_registry = self.language_registry.clone(); - let rule = self.store.read(cx).load(prompt_id, cx); - let make_completion_provider = self.make_completion_provider.clone(); - self.pending_load = cx.spawn_in(window, async move |this, cx| { - let rule = rule.await; - let markdown = language_registry.language_for_name("Markdown").await; - this.update_in(cx, |this, window, cx| match rule { - Ok(rule) => { - let title_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Untitled", window, cx); - editor.set_text(rule_metadata.title.unwrap_or_default(), window, cx); - if prompt_id.is_built_in() { - editor.set_read_only(true); - editor.set_show_edit_predictions(Some(false), window, cx); - } - editor - }); - let body_editor = cx.new(|cx| { - let buffer = cx.new(|cx| { - let mut buffer = Buffer::local(rule, cx); - buffer.set_language(markdown.log_err(), cx); - buffer.set_language_registry(language_registry); - buffer - }); - - let mut editor = Editor::for_buffer(buffer, None, window, cx); - if prompt_id.is_built_in() { - editor.set_read_only(true); - editor.set_show_edit_predictions(Some(false), window, cx); - } - editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); - editor.set_show_gutter(false, cx); - editor.set_show_wrap_guides(false, cx); - editor.set_show_indent_guides(false, cx); - editor.set_use_modal_editing(true); - editor.set_current_line_highlight(Some(CurrentLineHighlight::None)); - editor.set_completion_provider(Some(make_completion_provider())); - if focus { - window.focus(&editor.focus_handle(cx)); - } - editor - }); - let _subscriptions = vec![ - cx.subscribe_in( - &title_editor, - window, - move |this, editor, event, window, cx| { - this.handle_rule_title_editor_event( - prompt_id, editor, event, window, cx, - ) - }, - ), - cx.subscribe_in( - &body_editor, - window, - move |this, editor, event, window, cx| { - this.handle_rule_body_editor_event( - prompt_id, editor, event, window, cx, - ) - }, - ), - ]; - this.rule_editors.insert( - prompt_id, - RuleEditor { - title_editor, - body_editor, - next_title_and_body_to_save: None, - pending_save: None, - token_count: None, - pending_token_count: Task::ready(None), - _subscriptions, - }, - ); - this.set_active_rule(Some(prompt_id), window, cx); - this.count_tokens(prompt_id, window, cx); - } - Err(error) => { - // TODO: we should show the error in the UI. - log::error!("error while loading rule: {:?}", error); - } - }) - .ok(); - }); - } - } - - fn set_active_rule( - &mut self, - prompt_id: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.active_rule_id = prompt_id; - self.picker.update(cx, |picker, cx| { - if let Some(prompt_id) = prompt_id { - if picker - .delegate - .filtered_entries - .get(picker.delegate.selected_index()) - .is_none_or(|old_selected_prompt| { - if let RulePickerEntry::Rule(rule) = old_selected_prompt { - rule.id != prompt_id - } else { - true - } - }) - && let Some(ix) = picker.delegate.filtered_entries.iter().position(|mat| { - if let RulePickerEntry::Rule(rule) = mat { - rule.id == prompt_id - } else { - false - } - }) - { - picker.set_selected_index(ix, None, true, window, cx); - } - } else { - picker.focus(window, cx); - } - }); - cx.notify(); - } - - pub fn delete_rule( - &mut self, - prompt_id: PromptId, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(metadata) = self.store.read(cx).metadata(prompt_id) { - let confirmation = window.prompt( - PromptLevel::Warning, - &format!( - "Are you sure you want to delete {}", - metadata.title.unwrap_or("Untitled".into()) - ), - None, - &["Delete", "Cancel"], - cx, - ); - - cx.spawn_in(window, async move |this, cx| { - if confirmation.await.ok() == Some(0) { - this.update_in(cx, |this, window, cx| { - if this.active_rule_id == Some(prompt_id) { - this.set_active_rule(None, window, cx); - } - this.rule_editors.remove(&prompt_id); - this.store - .update(cx, |store, cx| store.delete(prompt_id, cx)) - .detach_and_log_err(cx); - this.picker - .update(cx, |picker, cx| picker.refresh(window, cx)); - cx.notify(); - })?; - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - } - - pub fn duplicate_rule( - &mut self, - prompt_id: PromptId, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(rule) = self.rule_editors.get(&prompt_id) { - const DUPLICATE_SUFFIX: &str = " copy"; - let title_to_duplicate = rule.title_editor.read(cx).text(cx); - let existing_titles = self - .rule_editors - .iter() - .filter(|&(&id, _)| id != prompt_id) - .map(|(_, rule_editor)| rule_editor.title_editor.read(cx).text(cx)) - .filter(|title| title.starts_with(&title_to_duplicate)) - .collect::>(); - - let title = if existing_titles.is_empty() { - title_to_duplicate + DUPLICATE_SUFFIX - } else { - let mut i = 1; - loop { - let new_title = format!("{title_to_duplicate}{DUPLICATE_SUFFIX} {i}"); - if !existing_titles.contains(&new_title) { - break new_title; - } - i += 1; - } - }; - - let new_id = PromptId::new(); - let body = rule.body_editor.read(cx).text(cx); - let save = self.store.update(cx, |store, cx| { - store.save(new_id, Some(title.into()), false, body.into(), cx) - }); - self.picker - .update(cx, |picker, cx| picker.refresh(window, cx)); - cx.spawn_in(window, async move |this, cx| { - save.await?; - this.update_in(cx, |rules_library, window, cx| { - rules_library.load_rule(new_id, true, window, cx) - }) - }) - .detach_and_log_err(cx); - } - } - - fn focus_active_rule(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { - if let Some(active_rule) = self.active_rule_id { - self.rule_editors[&active_rule] - .body_editor - .update(cx, |editor, cx| window.focus(&editor.focus_handle(cx))); - cx.stop_propagation(); - } - } - - fn focus_picker(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context) { - self.picker - .update(cx, |picker, cx| picker.focus(window, cx)); - } - - pub fn inline_assist( - &mut self, - action: &InlineAssist, - window: &mut Window, - cx: &mut Context, - ) { - let Some(active_rule_id) = self.active_rule_id else { - cx.propagate(); - return; - }; - - let rule_editor = &self.rule_editors[&active_rule_id].body_editor; - let Some(ConfiguredModel { provider, .. }) = - LanguageModelRegistry::read_global(cx).inline_assistant_model() - else { - return; - }; - - let initial_prompt = action.prompt.clone(); - if provider.is_authenticated(cx) { - self.inline_assist_delegate - .assist(rule_editor, initial_prompt, window, cx); - } else { - for window in cx.windows() { - if let Some(workspace) = window.downcast::() { - let panel = workspace - .update(cx, |workspace, window, cx| { - window.activate_window(); - self.inline_assist_delegate - .focus_agent_panel(workspace, window, cx) - }) - .ok(); - if panel == Some(true) { - return; - } - } - } - } - } - - fn move_down_from_title( - &mut self, - _: &editor::actions::MoveDown, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(rule_id) = self.active_rule_id - && let Some(rule_editor) = self.rule_editors.get(&rule_id) - { - window.focus(&rule_editor.body_editor.focus_handle(cx)); - } - } - - fn move_up_from_body( - &mut self, - _: &editor::actions::MoveUp, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(rule_id) = self.active_rule_id - && let Some(rule_editor) = self.rule_editors.get(&rule_id) - { - window.focus(&rule_editor.title_editor.focus_handle(cx)); - } - } - - fn handle_rule_title_editor_event( - &mut self, - prompt_id: PromptId, - title_editor: &Entity, - event: &EditorEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - EditorEvent::BufferEdited => { - self.save_rule(prompt_id, window, cx); - self.count_tokens(prompt_id, window, cx); - } - EditorEvent::Blurred => { - title_editor.update(cx, |title_editor, cx| { - title_editor.change_selections( - SelectionEffects::no_scroll(), - window, - cx, - |selections| { - let cursor = selections.oldest_anchor().head(); - selections.select_anchor_ranges([cursor..cursor]); - }, - ); - }); - } - _ => {} - } - } - - fn handle_rule_body_editor_event( - &mut self, - prompt_id: PromptId, - body_editor: &Entity, - event: &EditorEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - EditorEvent::BufferEdited => { - self.save_rule(prompt_id, window, cx); - self.count_tokens(prompt_id, window, cx); - } - EditorEvent::Blurred => { - body_editor.update(cx, |body_editor, cx| { - body_editor.change_selections( - SelectionEffects::no_scroll(), - window, - cx, - |selections| { - let cursor = selections.oldest_anchor().head(); - selections.select_anchor_ranges([cursor..cursor]); - }, - ); - }); - } - _ => {} - } - } - - fn count_tokens(&mut self, prompt_id: PromptId, window: &mut Window, cx: &mut Context) { - let Some(ConfiguredModel { model, .. }) = - LanguageModelRegistry::read_global(cx).default_model() - else { - return; - }; - if let Some(rule) = self.rule_editors.get_mut(&prompt_id) { - let editor = &rule.body_editor.read(cx); - let buffer = &editor.buffer().read(cx).as_singleton().unwrap().read(cx); - let body = buffer.as_rope().clone(); - rule.pending_token_count = cx.spawn_in(window, async move |this, cx| { - async move { - const DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1); - - cx.background_executor().timer(DEBOUNCE_TIMEOUT).await; - let token_count = cx - .update(|_, cx| { - model.count_tokens( - LanguageModelRequest { - thread_id: None, - prompt_id: None, - intent: None, - mode: None, - messages: vec![LanguageModelRequestMessage { - role: Role::System, - content: vec![body.to_string().into()], - cache: false, - reasoning_details: None, - }], - tools: Vec::new(), - tool_choice: None, - stop: Vec::new(), - temperature: None, - thinking_allowed: true, - }, - cx, - ) - })? - .await?; - - this.update(cx, |this, cx| { - let rule_editor = this.rule_editors.get_mut(&prompt_id).unwrap(); - rule_editor.token_count = Some(token_count); - cx.notify(); - }) - } - .log_err() - .await - }); - } - } - - fn render_rule_list(&mut self, cx: &mut Context) -> impl IntoElement { - v_flex() - .id("rule-list") - .capture_action(cx.listener(Self::focus_active_rule)) - .px_1p5() - .h_full() - .w_64() - .overflow_x_hidden() - .bg(cx.theme().colors().panel_background) - .map(|this| { - if cfg!(target_os = "macos") { - this.child( - h_flex() - .p(DynamicSpacing::Base04.rems(cx)) - .h_9() - .w_full() - .flex_none() - .justify_end() - .child( - IconButton::new("new-rule", IconName::Plus) - .tooltip(move |_window, cx| { - Tooltip::for_action("New Rule", &NewRule, cx) - }) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(NewRule), cx); - }), - ), - ) - } else { - this.child( - h_flex().p_1().w_full().child( - Button::new("new-rule", "New Rule") - .full_width() - .style(ButtonStyle::Outlined) - .icon(IconName::Plus) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .icon_color(Color::Muted) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(NewRule), cx); - }), - ), - ) - } - }) - .child(div().flex_grow().child(self.picker.clone())) - } - - fn render_active_rule_editor( - &self, - editor: &Entity, - cx: &mut Context, - ) -> impl IntoElement { - let settings = ThemeSettings::get_global(cx); - - div() - .w_full() - .on_action(cx.listener(Self::move_down_from_title)) - .pl_1() - .border_1() - .border_color(transparent_black()) - .rounded_sm() - .group_hover("active-editor-header", |this| { - this.border_color(cx.theme().colors().border_variant) - }) - .child(EditorElement::new( - &editor, - EditorStyle { - background: cx.theme().system().transparent, - local_player: cx.theme().players().local(), - text: TextStyle { - color: cx.theme().colors().editor_foreground, - font_family: settings.ui_font.family.clone(), - font_features: settings.ui_font.features.clone(), - font_size: HeadlineSize::Large.rems().into(), - font_weight: settings.ui_font.weight, - line_height: relative(settings.buffer_line_height.value()), - ..Default::default() - }, - scrollbar_width: Pixels::ZERO, - syntax: cx.theme().syntax().clone(), - status: cx.theme().status().clone(), - inlay_hints_style: editor::make_inlay_hints_style(cx), - edit_prediction_styles: editor::make_suggestion_styles(cx), - ..EditorStyle::default() - }, - )) - } - - fn render_active_rule(&mut self, cx: &mut Context) -> gpui::Stateful
{ - div() - .id("rule-editor") - .h_full() - .flex_grow() - .border_l_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().editor_background) - .children(self.active_rule_id.and_then(|prompt_id| { - let rule_metadata = self.store.read(cx).metadata(prompt_id)?; - let rule_editor = &self.rule_editors[&prompt_id]; - let focus_handle = rule_editor.body_editor.focus_handle(cx); - let model = LanguageModelRegistry::read_global(cx) - .default_model() - .map(|default| default.model); - - Some( - v_flex() - .id("rule-editor-inner") - .size_full() - .relative() - .overflow_hidden() - .on_click(cx.listener(move |_, _, window, _| { - window.focus(&focus_handle); - })) - .child( - h_flex() - .group("active-editor-header") - .pt_2() - .pl_1p5() - .pr_2p5() - .gap_2() - .justify_between() - .child( - self.render_active_rule_editor(&rule_editor.title_editor, cx), - ) - .child( - h_flex() - .h_full() - .flex_shrink_0() - .children(rule_editor.token_count.map(|token_count| { - let token_count: SharedString = - token_count.to_string().into(); - let label_token_count: SharedString = - token_count.to_string().into(); - - div() - .id("token_count") - .mr_1() - .flex_shrink_0() - .tooltip(move |_window, cx| { - Tooltip::with_meta( - "Token Estimation", - None, - format!( - "Model: {}", - model - .as_ref() - .map(|model| model.name().0) - .unwrap_or_default() - ), - cx, - ) - }) - .child( - Label::new(format!( - "{} tokens", - label_token_count - )) - .color(Color::Muted), - ) - })) - .child(if prompt_id.is_built_in() { - div() - .id("built-in-rule") - .child( - Icon::new(IconName::FileLock) - .color(Color::Muted), - ) - .tooltip(move |_window, cx| { - Tooltip::with_meta( - "Built-in rule", - None, - BUILT_IN_TOOLTIP_TEXT, - cx, - ) - }) - .into_any() - } else { - IconButton::new("delete-rule", IconName::Trash) - .tooltip(move |_window, cx| { - Tooltip::for_action( - "Delete Rule", - &DeleteRule, - cx, - ) - }) - .on_click(|_, window, cx| { - window - .dispatch_action(Box::new(DeleteRule), cx); - }) - .into_any_element() - }) - .child( - IconButton::new("duplicate-rule", IconName::BookCopy) - .tooltip(move |_window, cx| { - Tooltip::for_action( - "Duplicate Rule", - &DuplicateRule, - cx, - ) - }) - .on_click(|_, window, cx| { - window.dispatch_action( - Box::new(DuplicateRule), - cx, - ); - }), - ) - .child( - IconButton::new( - "toggle-default-rule", - IconName::Paperclip, - ) - .toggle_state(rule_metadata.default) - .icon_color(if rule_metadata.default { - Color::Accent - } else { - Color::Muted - }) - .map(|this| { - if rule_metadata.default { - this.tooltip(Tooltip::text( - "Remove from Default Rules", - )) - } else { - this.tooltip(move |_window, cx| { - Tooltip::with_meta( - "Add to Default Rules", - None, - "Always included in every thread.", - cx, - ) - }) - } - }) - .on_click( - |_, window, cx| { - window.dispatch_action( - Box::new(ToggleDefaultRule), - cx, - ); - }, - ), - ), - ), - ) - .child( - div() - .on_action(cx.listener(Self::focus_picker)) - .on_action(cx.listener(Self::inline_assist)) - .on_action(cx.listener(Self::move_up_from_body)) - .h_full() - .flex_grow() - .child( - h_flex() - .py_2() - .pl_2p5() - .h_full() - .flex_1() - .child(rule_editor.body_editor.clone()), - ), - ), - ) - })) - } -} - -impl Render for RulesLibrary { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let ui_font = theme::setup_ui_font(window, cx); - let theme = cx.theme().clone(); - - client_side_decorations( - v_flex() - .id("rules-library") - .key_context("RulesLibrary") - .on_action(cx.listener(|this, &NewRule, window, cx| this.new_rule(window, cx))) - .on_action( - cx.listener(|this, &DeleteRule, window, cx| { - this.delete_active_rule(window, cx) - }), - ) - .on_action(cx.listener(|this, &DuplicateRule, window, cx| { - this.duplicate_active_rule(window, cx) - })) - .on_action(cx.listener(|this, &ToggleDefaultRule, window, cx| { - this.toggle_default_for_active_rule(window, cx) - })) - .size_full() - .overflow_hidden() - .font(ui_font) - .text_color(theme.colors().text) - .children(self.title_bar.clone()) - .bg(theme.colors().background) - .child( - h_flex() - .flex_1() - .when(!cfg!(target_os = "macos"), |this| { - this.border_t_1().border_color(cx.theme().colors().border) - }) - .child(self.render_rule_list(cx)) - .map(|el| { - if self.store.read(cx).prompt_count() == 0 { - el.child( - v_flex() - .h_full() - .flex_1() - .items_center() - .justify_center() - .border_l_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().editor_background) - .child( - Button::new("create-rule", "New Rule") - .style(ButtonStyle::Outlined) - .key_binding(KeyBinding::for_action(&NewRule, cx)) - .on_click(|_, window, cx| { - window - .dispatch_action(NewRule.boxed_clone(), cx) - }), - ), - ) - } else { - el.child(self.render_active_rule(cx)) - } - }), - ), - window, - cx, - ) - } -} diff --git a/crates/schema_generator/Cargo.toml b/crates/schema_generator/Cargo.toml deleted file mode 100644 index 865f76f4af..0000000000 --- a/crates/schema_generator/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "schema_generator" -version = "0.1.0" -publish.workspace = true -edition.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[dependencies] -anyhow.workspace = true -clap = { workspace = true, features = ["derive"] } -env_logger.workspace = true -schemars = { workspace = true, features = ["indexmap2"] } -serde.workspace = true -serde_json.workspace = true -theme.workspace = true diff --git a/crates/schema_generator/LICENSE-GPL b/crates/schema_generator/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/schema_generator/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/schema_generator/README.md b/crates/schema_generator/README.md deleted file mode 100644 index f8ef5bc234..0000000000 --- a/crates/schema_generator/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Zed Schema Generator - -Prints various Zed schemas to stdout. - -## Usage - -```sh -cargo run -p schema_generator -- --help - -cargo run -p schema_generator -- theme -cargo run -p schema_generator -- icon_theme -``` diff --git a/crates/schema_generator/src/main.rs b/crates/schema_generator/src/main.rs deleted file mode 100644 index a7e406a1a9..0000000000 --- a/crates/schema_generator/src/main.rs +++ /dev/null @@ -1,36 +0,0 @@ -use anyhow::Result; -use clap::{Parser, ValueEnum}; -use schemars::schema_for; -use theme::{IconThemeFamilyContent, ThemeFamilyContent}; - -#[derive(Parser, Debug)] -pub struct Args { - #[arg(value_enum)] - pub schema_type: SchemaType, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] -#[clap(rename_all = "snake_case")] -pub enum SchemaType { - Theme, - IconTheme, -} - -fn main() -> Result<()> { - env_logger::init(); - - let args = Args::parse(); - - match args.schema_type { - SchemaType::Theme => { - let schema = schema_for!(ThemeFamilyContent); - println!("{}", serde_json::to_string_pretty(&schema)?); - } - SchemaType::IconTheme => { - let schema = schema_for!(IconThemeFamilyContent); - println!("{}", serde_json::to_string_pretty(&schema)?); - } - } - - Ok(()) -} diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml deleted file mode 100644 index 02eb611fc2..0000000000 --- a/crates/search/Cargo.toml +++ /dev/null @@ -1,61 +0,0 @@ -[package] -name = "search" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[features] -test-support = [ - "client/test-support", - "editor/test-support", - "gpui/test-support", - "workspace/test-support", -] - -[lints] -workspace = true - -[lib] -path = "src/search.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -any_vec.workspace = true -bitflags.workspace = true -collections.workspace = true -editor.workspace = true -futures.workspace = true -gpui.workspace = true -language.workspace = true -menu.workspace = true -project.workspace = true -schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smol.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -util_macros.workspace = true -workspace.workspace = true -zed_actions.workspace = true -itertools.workspace = true -ztracing.workspace = true -tracing.workspace = true - -[dev-dependencies] -client = { workspace = true, features = ["test-support"] } -editor = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -lsp.workspace = true -pretty_assertions.workspace = true -unindent.workspace = true -workspace = { workspace = true, features = ["test-support"] } - -[package.metadata.cargo-machete] -ignored = ["tracing"] - diff --git a/crates/search/LICENSE-GPL b/crates/search/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/search/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs deleted file mode 100644 index a9c26ac9ba..0000000000 --- a/crates/search/src/buffer_search.rs +++ /dev/null @@ -1,3118 +0,0 @@ -mod registrar; - -use crate::{ - FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOption, - SearchOptions, SearchSource, SelectAllMatches, SelectNextMatch, SelectPreviousMatch, - ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord, - search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input}, -}; -use any_vec::AnyVec; -use anyhow::Context as _; -use collections::HashMap; -use editor::{ - DisplayPoint, Editor, EditorSettings, MultiBufferOffset, - actions::{Backtab, Tab}, -}; -use futures::channel::oneshot; -use gpui::{ - Action, App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _, - IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task, - Window, actions, div, -}; -use language::{Language, LanguageRegistry}; -use project::{ - search::SearchQuery, - search_history::{SearchHistory, SearchHistoryCursor}, -}; -use schemars::JsonSchema; -use serde::Deserialize; -use settings::Settings; -use std::sync::Arc; -use zed_actions::{outline::ToggleOutline, workspace::CopyPath, workspace::CopyRelativePath}; - -use ui::{ - BASE_REM_SIZE_IN_PX, IconButton, IconButtonShape, IconName, Tooltip, h_flex, prelude::*, - utils::SearchInputWidth, -}; -use util::{ResultExt, paths::PathMatcher}; -use workspace::{ - ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, - item::ItemHandle, - searchable::{ - Direction, FilteredSearchRange, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle, - }, -}; - -pub use registrar::DivRegistrar; -use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults}; - -const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50; - -/// Opens the buffer search interface with the specified configuration. -#[derive(PartialEq, Clone, Deserialize, JsonSchema, Action)] -#[action(namespace = buffer_search)] -#[serde(deny_unknown_fields)] -pub struct Deploy { - #[serde(default = "util::serde::default_true")] - pub focus: bool, - #[serde(default)] - pub replace_enabled: bool, - #[serde(default)] - pub selection_search_enabled: bool, -} - -actions!( - buffer_search, - [ - /// Deploys the search and replace interface. - DeployReplace, - /// Dismisses the search bar. - Dismiss, - /// Focuses back on the editor. - FocusEditor - ] -); - -impl Deploy { - pub fn find() -> Self { - Self { - focus: true, - replace_enabled: false, - selection_search_enabled: false, - } - } - - pub fn replace() -> Self { - Self { - focus: true, - replace_enabled: true, - selection_search_enabled: false, - } - } -} - -pub enum Event { - UpdateLocation, -} - -pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, _, _| BufferSearchBar::register(workspace)) - .detach(); -} - -pub struct BufferSearchBar { - query_editor: Entity, - query_editor_focused: bool, - replacement_editor: Entity, - replacement_editor_focused: bool, - active_searchable_item: Option>, - active_match_index: Option, - active_searchable_item_subscription: Option, - active_search: Option>, - searchable_items_with_matches: HashMap, AnyVec>, - pending_search: Option>, - search_options: SearchOptions, - default_options: SearchOptions, - configured_options: SearchOptions, - query_error: Option, - dismissed: bool, - search_history: SearchHistory, - search_history_cursor: SearchHistoryCursor, - replace_enabled: bool, - selection_search_enabled: Option, - scroll_handle: ScrollHandle, - editor_scroll_handle: ScrollHandle, - editor_needed_width: Pixels, - regex_language: Option>, -} - -impl EventEmitter for BufferSearchBar {} -impl EventEmitter for BufferSearchBar {} -impl Render for BufferSearchBar { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - if self.dismissed { - return div().id("search_bar"); - } - - let focus_handle = self.focus_handle(cx); - - let narrow_mode = - self.scroll_handle.bounds().size.width / window.rem_size() < 340. / BASE_REM_SIZE_IN_PX; - let hide_inline_icons = self.editor_needed_width - > self.editor_scroll_handle.bounds().size.width - window.rem_size() * 6.; - - let workspace::searchable::SearchOptions { - case, - word, - regex, - replacement, - selection, - find_in_results, - } = self.supported_options(cx); - - self.query_editor.update(cx, |query_editor, cx| { - if query_editor.placeholder_text(cx).is_none() { - query_editor.set_placeholder_text("Search…", window, cx); - } - }); - - self.replacement_editor.update(cx, |editor, cx| { - editor.set_placeholder_text("Replace with…", window, cx); - }); - - let mut color_override = None; - let match_text = self - .active_searchable_item - .as_ref() - .and_then(|searchable_item| { - if self.query(cx).is_empty() { - return None; - } - let matches_count = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - .map(AnyVec::len) - .unwrap_or(0); - if let Some(match_ix) = self.active_match_index { - Some(format!("{}/{}", match_ix + 1, matches_count)) - } else { - color_override = Some(Color::Error); // No matches found - None - } - }) - .unwrap_or_else(|| "0/0".to_string()); - let should_show_replace_input = self.replace_enabled && replacement; - let in_replace = self.replacement_editor.focus_handle(cx).is_focused(window); - - let theme_colors = cx.theme().colors(); - let query_border = if self.query_error.is_some() { - Color::Error.color(cx) - } else { - theme_colors.border - }; - let replacement_border = theme_colors.border; - - let container_width = window.viewport_size().width; - let input_width = SearchInputWidth::calc_width(container_width); - - let input_base_styles = - |border_color| input_base_styles(border_color, |div| div.w(input_width)); - - let query_column = input_base_styles(query_border) - .id("editor-scroll") - .track_scroll(&self.editor_scroll_handle) - .child(render_text_input(&self.query_editor, color_override, cx)) - .when(!hide_inline_icons, |div| { - div.child( - h_flex() - .gap_1() - .when(case, |div| { - div.child(SearchOption::CaseSensitive.as_button( - self.search_options, - SearchSource::Buffer, - focus_handle.clone(), - )) - }) - .when(word, |div| { - div.child(SearchOption::WholeWord.as_button( - self.search_options, - SearchSource::Buffer, - focus_handle.clone(), - )) - }) - .when(regex, |div| { - div.child(SearchOption::Regex.as_button( - self.search_options, - SearchSource::Buffer, - focus_handle.clone(), - )) - }), - ) - }); - - let mode_column = h_flex() - .gap_1() - .min_w_64() - .when(replacement, |this| { - this.child(render_action_button( - "buffer-search-bar-toggle", - IconName::Replace, - self.replace_enabled.then_some(ActionButtonState::Toggled), - "Toggle Replace", - &ToggleReplace, - focus_handle.clone(), - )) - }) - .when(selection, |this| { - this.child( - IconButton::new( - "buffer-search-bar-toggle-search-selection-button", - IconName::Quote, - ) - .style(ButtonStyle::Subtle) - .shape(IconButtonShape::Square) - .when(self.selection_search_enabled.is_some(), |button| { - button.style(ButtonStyle::Filled) - }) - .on_click(cx.listener(|this, _: &ClickEvent, window, cx| { - this.toggle_selection(&ToggleSelection, window, cx); - })) - .toggle_state(self.selection_search_enabled.is_some()) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |_window, cx| { - Tooltip::for_action_in( - "Toggle Search Selection", - &ToggleSelection, - &focus_handle, - cx, - ) - } - }), - ) - }) - .when(!find_in_results, |el| { - let query_focus = self.query_editor.focus_handle(cx); - let matches_column = h_flex() - .pl_2() - .ml_2() - .border_l_1() - .border_color(theme_colors.border_variant) - .child(render_action_button( - "buffer-search-nav-button", - ui::IconName::ChevronLeft, - self.active_match_index - .is_none() - .then_some(ActionButtonState::Disabled), - "Select Previous Match", - &SelectPreviousMatch, - query_focus.clone(), - )) - .child(render_action_button( - "buffer-search-nav-button", - ui::IconName::ChevronRight, - self.active_match_index - .is_none() - .then_some(ActionButtonState::Disabled), - "Select Next Match", - &SelectNextMatch, - query_focus.clone(), - )) - .when(!narrow_mode, |this| { - this.child(div().ml_2().min_w(rems_from_px(40.)).child( - Label::new(match_text).size(LabelSize::Small).color( - if self.active_match_index.is_some() { - Color::Default - } else { - Color::Disabled - }, - ), - )) - }); - - el.child(render_action_button( - "buffer-search-nav-button", - IconName::SelectAll, - Default::default(), - "Select All Matches", - &SelectAllMatches, - query_focus, - )) - .child(matches_column) - }) - .when(find_in_results, |el| { - el.child(render_action_button( - "buffer-search", - IconName::Close, - Default::default(), - "Close Search Bar", - &Dismiss, - focus_handle.clone(), - )) - }); - - let search_line = h_flex() - .w_full() - .gap_2() - .when(find_in_results, |el| { - el.child(Label::new("Find in results").color(Color::Hint)) - }) - .child(query_column) - .child(mode_column); - - let replace_line = - should_show_replace_input.then(|| { - let replace_column = input_base_styles(replacement_border) - .child(render_text_input(&self.replacement_editor, None, cx)); - let focus_handle = self.replacement_editor.read(cx).focus_handle(cx); - - let replace_actions = h_flex() - .min_w_64() - .gap_1() - .child(render_action_button( - "buffer-search-replace-button", - IconName::ReplaceNext, - Default::default(), - "Replace Next Match", - &ReplaceNext, - focus_handle.clone(), - )) - .child(render_action_button( - "buffer-search-replace-button", - IconName::ReplaceAll, - Default::default(), - "Replace All Matches", - &ReplaceAll, - focus_handle, - )); - h_flex() - .w_full() - .gap_2() - .child(replace_column) - .child(replace_actions) - }); - - let mut key_context = KeyContext::new_with_defaults(); - key_context.add("BufferSearchBar"); - if in_replace { - key_context.add("in_replace"); - } - - let query_error_line = self.query_error.as_ref().map(|error| { - Label::new(error) - .size(LabelSize::Small) - .color(Color::Error) - .mt_neg_1() - .ml_2() - }); - - let search_line = - h_flex() - .relative() - .child(search_line) - .when(!narrow_mode && !find_in_results, |div| { - div.child(h_flex().absolute().right_0().child(render_action_button( - "buffer-search", - IconName::Close, - Default::default(), - "Close Search Bar", - &Dismiss, - focus_handle.clone(), - ))) - .w_full() - }); - v_flex() - .id("buffer_search") - .gap_2() - .py(px(1.0)) - .w_full() - .track_scroll(&self.scroll_handle) - .key_context(key_context) - .capture_action(cx.listener(Self::tab)) - .capture_action(cx.listener(Self::backtab)) - .on_action(cx.listener(Self::previous_history_query)) - .on_action(cx.listener(Self::next_history_query)) - .on_action(cx.listener(Self::dismiss)) - .on_action(cx.listener(Self::select_next_match)) - .on_action(cx.listener(Self::select_prev_match)) - .on_action(cx.listener(|this, _: &ToggleOutline, window, cx| { - if let Some(active_searchable_item) = &mut this.active_searchable_item { - active_searchable_item.relay_action(Box::new(ToggleOutline), window, cx); - } - })) - .on_action(cx.listener(|this, _: &CopyPath, window, cx| { - if let Some(active_searchable_item) = &mut this.active_searchable_item { - active_searchable_item.relay_action(Box::new(CopyPath), window, cx); - } - })) - .on_action(cx.listener(|this, _: &CopyRelativePath, window, cx| { - if let Some(active_searchable_item) = &mut this.active_searchable_item { - active_searchable_item.relay_action(Box::new(CopyRelativePath), window, cx); - } - })) - .when(replacement, |this| { - this.on_action(cx.listener(Self::toggle_replace)) - .on_action(cx.listener(Self::replace_next)) - .on_action(cx.listener(Self::replace_all)) - }) - .when(case, |this| { - this.on_action(cx.listener(Self::toggle_case_sensitive)) - }) - .when(word, |this| { - this.on_action(cx.listener(Self::toggle_whole_word)) - }) - .when(regex, |this| { - this.on_action(cx.listener(Self::toggle_regex)) - }) - .when(selection, |this| { - this.on_action(cx.listener(Self::toggle_selection)) - }) - .child(search_line) - .children(query_error_line) - .children(replace_line) - } -} - -impl Focusable for BufferSearchBar { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.query_editor.focus_handle(cx) - } -} - -impl ToolbarItemView for BufferSearchBar { - fn contribute_context(&self, context: &mut KeyContext, _cx: &App) { - if !self.dismissed { - context.add("buffer_search_deployed"); - } - } - - fn set_active_pane_item( - &mut self, - item: Option<&dyn ItemHandle>, - window: &mut Window, - cx: &mut Context, - ) -> ToolbarItemLocation { - cx.notify(); - self.active_searchable_item_subscription.take(); - self.active_searchable_item.take(); - - self.pending_search.take(); - - if let Some(searchable_item_handle) = - item.and_then(|item| item.to_searchable_item_handle(cx)) - { - let this = cx.entity().downgrade(); - - self.active_searchable_item_subscription = - Some(searchable_item_handle.subscribe_to_search_events( - window, - cx, - Box::new(move |search_event, window, cx| { - if let Some(this) = this.upgrade() { - this.update(cx, |this, cx| { - this.on_active_searchable_item_event(search_event, window, cx) - }); - } - }), - )); - - let is_project_search = searchable_item_handle.supported_options(cx).find_in_results; - self.active_searchable_item = Some(searchable_item_handle); - drop(self.update_matches(true, false, window, cx)); - if !self.dismissed { - if is_project_search { - self.dismiss(&Default::default(), window, cx); - } else { - return ToolbarItemLocation::Secondary; - } - } - } - ToolbarItemLocation::Hidden - } -} - -impl BufferSearchBar { - pub fn query_editor_focused(&self) -> bool { - self.query_editor_focused - } - - pub fn register(registrar: &mut impl SearchActionsRegistrar) { - registrar.register_handler(ForDeployed(|this, _: &FocusSearch, window, cx| { - this.query_editor.focus_handle(cx).focus(window); - this.select_query(window, cx); - })); - registrar.register_handler(ForDeployed( - |this, action: &ToggleCaseSensitive, window, cx| { - if this.supported_options(cx).case { - this.toggle_case_sensitive(action, window, cx); - } - }, - )); - registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, window, cx| { - if this.supported_options(cx).word { - this.toggle_whole_word(action, window, cx); - } - })); - registrar.register_handler(ForDeployed(|this, action: &ToggleRegex, window, cx| { - if this.supported_options(cx).regex { - this.toggle_regex(action, window, cx); - } - })); - registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, window, cx| { - if this.supported_options(cx).selection { - this.toggle_selection(action, window, cx); - } else { - cx.propagate(); - } - })); - registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, window, cx| { - if this.supported_options(cx).replacement { - this.toggle_replace(action, window, cx); - } else { - cx.propagate(); - } - })); - registrar.register_handler(WithResults(|this, action: &SelectNextMatch, window, cx| { - if this.supported_options(cx).find_in_results { - cx.propagate(); - } else { - this.select_next_match(action, window, cx); - } - })); - registrar.register_handler(WithResults( - |this, action: &SelectPreviousMatch, window, cx| { - if this.supported_options(cx).find_in_results { - cx.propagate(); - } else { - this.select_prev_match(action, window, cx); - } - }, - )); - registrar.register_handler(WithResults( - |this, action: &SelectAllMatches, window, cx| { - if this.supported_options(cx).find_in_results { - cx.propagate(); - } else { - this.select_all_matches(action, window, cx); - } - }, - )); - registrar.register_handler(ForDeployed( - |this, _: &editor::actions::Cancel, window, cx| { - this.dismiss(&Dismiss, window, cx); - }, - )); - registrar.register_handler(ForDeployed(|this, _: &Dismiss, window, cx| { - this.dismiss(&Dismiss, window, cx); - })); - - // register deploy buffer search for both search bar states, since we want to focus into the search bar - // when the deploy action is triggered in the buffer. - registrar.register_handler(ForDeployed(|this, deploy, window, cx| { - this.deploy(deploy, window, cx); - })); - registrar.register_handler(ForDismissed(|this, deploy, window, cx| { - this.deploy(deploy, window, cx); - })); - registrar.register_handler(ForDeployed(|this, _: &DeployReplace, window, cx| { - if this.supported_options(cx).find_in_results { - cx.propagate(); - } else { - this.deploy(&Deploy::replace(), window, cx); - } - })); - registrar.register_handler(ForDismissed(|this, _: &DeployReplace, window, cx| { - if this.supported_options(cx).find_in_results { - cx.propagate(); - } else { - this.deploy(&Deploy::replace(), window, cx); - } - })); - } - - pub fn new( - languages: Option>, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let query_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_use_autoclose(false); - editor - }); - cx.subscribe_in(&query_editor, window, Self::on_query_editor_event) - .detach(); - let replacement_editor = cx.new(|cx| Editor::single_line(window, cx)); - cx.subscribe(&replacement_editor, Self::on_replacement_editor_event) - .detach(); - - let search_options = SearchOptions::from_settings(&EditorSettings::get_global(cx).search); - if let Some(languages) = languages { - let query_buffer = query_editor - .read(cx) - .buffer() - .read(cx) - .as_singleton() - .expect("query editor should be backed by a singleton buffer"); - query_buffer - .read(cx) - .set_language_registry(languages.clone()); - - cx.spawn(async move |buffer_search_bar, cx| { - let regex_language = languages - .language_for_name("regex") - .await - .context("loading regex language")?; - buffer_search_bar - .update(cx, |buffer_search_bar, cx| { - buffer_search_bar.regex_language = Some(regex_language); - buffer_search_bar.adjust_query_regex_language(cx); - }) - .ok(); - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - - Self { - query_editor, - query_editor_focused: false, - replacement_editor, - replacement_editor_focused: false, - active_searchable_item: None, - active_searchable_item_subscription: None, - active_match_index: None, - searchable_items_with_matches: Default::default(), - default_options: search_options, - configured_options: search_options, - search_options, - pending_search: None, - query_error: None, - dismissed: true, - search_history: SearchHistory::new( - Some(MAX_BUFFER_SEARCH_HISTORY_SIZE), - project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains, - ), - search_history_cursor: Default::default(), - active_search: None, - replace_enabled: false, - selection_search_enabled: None, - scroll_handle: ScrollHandle::new(), - editor_scroll_handle: ScrollHandle::new(), - editor_needed_width: px(0.), - regex_language: None, - } - } - - pub fn is_dismissed(&self) -> bool { - self.dismissed - } - - pub fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context) { - self.dismissed = true; - self.query_error = None; - self.sync_select_next_case_sensitivity(cx); - - for searchable_item in self.searchable_items_with_matches.keys() { - if let Some(searchable_item) = - WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx) - { - searchable_item.clear_matches(window, cx); - } - } - if let Some(active_editor) = self.active_searchable_item.as_mut() { - self.selection_search_enabled = None; - self.replace_enabled = false; - active_editor.search_bar_visibility_changed(false, window, cx); - active_editor.toggle_filtered_search_ranges(None, window, cx); - let handle = active_editor.item_focus_handle(cx); - self.focus(&handle, window); - } - - cx.emit(Event::UpdateLocation); - cx.emit(ToolbarItemEvent::ChangeLocation( - ToolbarItemLocation::Hidden, - )); - cx.notify(); - } - - pub fn deploy(&mut self, deploy: &Deploy, window: &mut Window, cx: &mut Context) -> bool { - let filtered_search_range = if deploy.selection_search_enabled { - Some(FilteredSearchRange::Default) - } else { - None - }; - if self.show(window, cx) { - if let Some(active_item) = self.active_searchable_item.as_mut() { - active_item.toggle_filtered_search_ranges(filtered_search_range, window, cx); - } - self.search_suggested(window, cx); - self.smartcase(window, cx); - self.sync_select_next_case_sensitivity(cx); - self.replace_enabled = deploy.replace_enabled; - self.selection_search_enabled = if deploy.selection_search_enabled { - Some(FilteredSearchRange::Default) - } else { - None - }; - if deploy.focus { - let mut handle = self.query_editor.focus_handle(cx); - let mut select_query = true; - if deploy.replace_enabled && handle.is_focused(window) { - handle = self.replacement_editor.focus_handle(cx); - select_query = false; - }; - - if select_query { - self.select_query(window, cx); - } - - window.focus(&handle); - } - return true; - } - - cx.propagate(); - false - } - - pub fn toggle(&mut self, action: &Deploy, window: &mut Window, cx: &mut Context) { - if self.is_dismissed() { - self.deploy(action, window, cx); - } else { - self.dismiss(&Dismiss, window, cx); - } - } - - pub fn show(&mut self, window: &mut Window, cx: &mut Context) -> bool { - let Some(handle) = self.active_searchable_item.as_ref() else { - return false; - }; - - let configured_options = - SearchOptions::from_settings(&EditorSettings::get_global(cx).search); - let settings_changed = configured_options != self.configured_options; - - if self.dismissed && settings_changed { - // Only update configuration options when search bar is dismissed, - // so we don't miss updates even after calling show twice - self.configured_options = configured_options; - self.search_options = configured_options; - self.default_options = configured_options; - } - - self.dismissed = false; - self.adjust_query_regex_language(cx); - handle.search_bar_visibility_changed(true, window, cx); - cx.notify(); - cx.emit(Event::UpdateLocation); - cx.emit(ToolbarItemEvent::ChangeLocation( - ToolbarItemLocation::Secondary, - )); - true - } - - fn supported_options(&self, cx: &mut Context) -> workspace::searchable::SearchOptions { - self.active_searchable_item - .as_ref() - .map(|item| item.supported_options(cx)) - .unwrap_or_default() - } - - pub fn search_suggested(&mut self, window: &mut Window, cx: &mut Context) { - let search = self.query_suggestion(window, cx).map(|suggestion| { - self.search(&suggestion, Some(self.default_options), true, window, cx) - }); - - if let Some(search) = search { - cx.spawn_in(window, async move |this, cx| { - if search.await.is_ok() { - this.update_in(cx, |this, window, cx| { - this.activate_current_match(window, cx) - }) - } else { - Ok(()) - } - }) - .detach_and_log_err(cx); - } - } - - pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(match_ix) = self.active_match_index - && let Some(active_searchable_item) = self.active_searchable_item.as_ref() - && let Some(matches) = self - .searchable_items_with_matches - .get(&active_searchable_item.downgrade()) - { - active_searchable_item.activate_match(match_ix, matches, window, cx) - } - } - - pub fn select_query(&mut self, window: &mut Window, cx: &mut Context) { - self.query_editor.update(cx, |query_editor, cx| { - query_editor.select_all(&Default::default(), window, cx); - }); - } - - pub fn query(&self, cx: &App) -> String { - self.query_editor.read(cx).text(cx) - } - - pub fn replacement(&self, cx: &mut App) -> String { - self.replacement_editor.read(cx).text(cx) - } - - pub fn query_suggestion( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Option { - self.active_searchable_item - .as_ref() - .map(|searchable_item| searchable_item.query_suggestion(window, cx)) - .filter(|suggestion| !suggestion.is_empty()) - } - - pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut Context) { - if replacement.is_none() { - self.replace_enabled = false; - return; - } - self.replace_enabled = true; - self.replacement_editor - .update(cx, |replacement_editor, cx| { - replacement_editor - .buffer() - .update(cx, |replacement_buffer, cx| { - let len = replacement_buffer.len(cx); - replacement_buffer.edit( - [(MultiBufferOffset(0)..len, replacement.unwrap())], - None, - cx, - ); - }); - }); - } - - pub fn focus_replace(&mut self, window: &mut Window, cx: &mut Context) { - self.focus(&self.replacement_editor.focus_handle(cx), window); - cx.notify(); - } - - pub fn search( - &mut self, - query: &str, - options: Option, - add_to_history: bool, - window: &mut Window, - cx: &mut Context, - ) -> oneshot::Receiver<()> { - let options = options.unwrap_or(self.default_options); - let updated = query != self.query(cx) || self.search_options != options; - if updated { - self.query_editor.update(cx, |query_editor, cx| { - query_editor.buffer().update(cx, |query_buffer, cx| { - let len = query_buffer.len(cx); - query_buffer.edit([(MultiBufferOffset(0)..len, query)], None, cx); - }); - }); - self.set_search_options(options, cx); - self.clear_matches(window, cx); - cx.notify(); - } - self.update_matches(!updated, add_to_history, window, cx) - } - - pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context) { - if let Some(active_editor) = self.active_searchable_item.as_ref() { - let handle = active_editor.item_focus_handle(cx); - window.focus(&handle); - } - } - - pub fn toggle_search_option( - &mut self, - search_option: SearchOptions, - window: &mut Window, - cx: &mut Context, - ) { - self.search_options.toggle(search_option); - self.default_options = self.search_options; - drop(self.update_matches(false, false, window, cx)); - self.adjust_query_regex_language(cx); - self.sync_select_next_case_sensitivity(cx); - cx.notify(); - } - - pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool { - self.search_options.contains(search_option) - } - - pub fn enable_search_option( - &mut self, - search_option: SearchOptions, - window: &mut Window, - cx: &mut Context, - ) { - if !self.search_options.contains(search_option) { - self.toggle_search_option(search_option, window, cx) - } - } - - pub fn set_search_within_selection( - &mut self, - search_within_selection: Option, - window: &mut Window, - cx: &mut Context, - ) -> Option> { - let active_item = self.active_searchable_item.as_mut()?; - self.selection_search_enabled = search_within_selection; - active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx); - cx.notify(); - Some(self.update_matches(false, false, window, cx)) - } - - pub fn set_search_options(&mut self, search_options: SearchOptions, cx: &mut Context) { - self.search_options = search_options; - self.adjust_query_regex_language(cx); - self.sync_select_next_case_sensitivity(cx); - cx.notify(); - } - - pub fn clear_search_within_ranges( - &mut self, - search_options: SearchOptions, - cx: &mut Context, - ) { - self.search_options = search_options; - self.adjust_query_regex_language(cx); - cx.notify(); - } - - fn select_next_match( - &mut self, - _: &SelectNextMatch, - window: &mut Window, - cx: &mut Context, - ) { - self.select_match(Direction::Next, 1, window, cx); - } - - fn select_prev_match( - &mut self, - _: &SelectPreviousMatch, - window: &mut Window, - cx: &mut Context, - ) { - self.select_match(Direction::Prev, 1, window, cx); - } - - pub fn select_all_matches( - &mut self, - _: &SelectAllMatches, - window: &mut Window, - cx: &mut Context, - ) { - if !self.dismissed - && self.active_match_index.is_some() - && let Some(searchable_item) = self.active_searchable_item.as_ref() - && let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - searchable_item.select_matches(matches, window, cx); - self.focus_editor(&FocusEditor, window, cx); - } - } - - pub fn select_match( - &mut self, - direction: Direction, - count: usize, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(index) = self.active_match_index - && let Some(searchable_item) = self.active_searchable_item.as_ref() - && let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - .filter(|matches| !matches.is_empty()) - { - // If 'wrapscan' is disabled, searches do not wrap around the end of the file. - if !EditorSettings::get_global(cx).search_wrap - && ((direction == Direction::Next && index + count >= matches.len()) - || (direction == Direction::Prev && index < count)) - { - crate::show_no_more_matches(window, cx); - return; - } - let new_match_index = searchable_item - .match_index_for_direction(matches, index, direction, count, window, cx); - - searchable_item.update_matches(matches, Some(new_match_index), window, cx); - searchable_item.activate_match(new_match_index, matches, window, cx); - } - } - - pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(searchable_item) = self.active_searchable_item.as_ref() - && let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - if matches.is_empty() { - return; - } - searchable_item.update_matches(matches, Some(0), window, cx); - searchable_item.activate_match(0, matches, window, cx); - } - } - - pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(searchable_item) = self.active_searchable_item.as_ref() - && let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - if matches.is_empty() { - return; - } - let new_match_index = matches.len() - 1; - searchable_item.update_matches(matches, Some(new_match_index), window, cx); - searchable_item.activate_match(new_match_index, matches, window, cx); - } - } - - fn on_query_editor_event( - &mut self, - editor: &Entity, - event: &editor::EditorEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - editor::EditorEvent::Focused => self.query_editor_focused = true, - editor::EditorEvent::Blurred => self.query_editor_focused = false, - editor::EditorEvent::Edited { .. } => { - self.smartcase(window, cx); - self.clear_matches(window, cx); - let search = self.update_matches(false, true, window, cx); - - let width = editor.update(cx, |editor, cx| { - let text_layout_details = editor.text_layout_details(window); - let snapshot = editor.snapshot(window, cx).display_snapshot; - - snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details) - - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details) - }); - self.editor_needed_width = width; - cx.notify(); - - cx.spawn_in(window, async move |this, cx| { - if search.await.is_ok() { - this.update_in(cx, |this, window, cx| { - this.activate_current_match(window, cx) - }) - } else { - Ok(()) - } - }) - .detach_and_log_err(cx); - } - _ => {} - } - } - - fn on_replacement_editor_event( - &mut self, - _: Entity, - event: &editor::EditorEvent, - _: &mut Context, - ) { - match event { - editor::EditorEvent::Focused => self.replacement_editor_focused = true, - editor::EditorEvent::Blurred => self.replacement_editor_focused = false, - _ => {} - } - } - - fn on_active_searchable_item_event( - &mut self, - event: &SearchEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - SearchEvent::MatchesInvalidated => { - drop(self.update_matches(false, false, window, cx)); - } - SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx), - } - } - - fn toggle_case_sensitive( - &mut self, - _: &ToggleCaseSensitive, - window: &mut Window, - cx: &mut Context, - ) { - self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx) - } - - fn toggle_whole_word( - &mut self, - _: &ToggleWholeWord, - window: &mut Window, - cx: &mut Context, - ) { - self.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx) - } - - fn toggle_selection( - &mut self, - _: &ToggleSelection, - window: &mut Window, - cx: &mut Context, - ) { - self.set_search_within_selection( - if let Some(_) = self.selection_search_enabled { - None - } else { - Some(FilteredSearchRange::Default) - }, - window, - cx, - ); - } - - fn toggle_regex(&mut self, _: &ToggleRegex, window: &mut Window, cx: &mut Context) { - self.toggle_search_option(SearchOptions::REGEX, window, cx) - } - - fn clear_active_searchable_item_matches(&mut self, window: &mut Window, cx: &mut App) { - if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { - self.active_match_index = None; - self.searchable_items_with_matches - .remove(&active_searchable_item.downgrade()); - active_searchable_item.clear_matches(window, cx); - } - } - - pub fn has_active_match(&self) -> bool { - self.active_match_index.is_some() - } - - fn clear_matches(&mut self, window: &mut Window, cx: &mut Context) { - let mut active_item_matches = None; - for (searchable_item, matches) in self.searchable_items_with_matches.drain() { - if let Some(searchable_item) = - WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx) - { - if Some(&searchable_item) == self.active_searchable_item.as_ref() { - active_item_matches = Some((searchable_item.downgrade(), matches)); - } else { - searchable_item.clear_matches(window, cx); - } - } - } - - self.searchable_items_with_matches - .extend(active_item_matches); - } - - fn update_matches( - &mut self, - reuse_existing_query: bool, - add_to_history: bool, - window: &mut Window, - cx: &mut Context, - ) -> oneshot::Receiver<()> { - let (done_tx, done_rx) = oneshot::channel(); - let query = self.query(cx); - self.pending_search.take(); - - if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { - self.query_error = None; - if query.is_empty() { - self.clear_active_searchable_item_matches(window, cx); - let _ = done_tx.send(()); - cx.notify(); - } else { - let query: Arc<_> = if let Some(search) = - self.active_search.take().filter(|_| reuse_existing_query) - { - search - } else { - // Value doesn't matter, we only construct empty matchers with it - - if self.search_options.contains(SearchOptions::REGEX) { - match SearchQuery::regex( - query, - self.search_options.contains(SearchOptions::WHOLE_WORD), - self.search_options.contains(SearchOptions::CASE_SENSITIVE), - false, - self.search_options - .contains(SearchOptions::ONE_MATCH_PER_LINE), - PathMatcher::default(), - PathMatcher::default(), - false, - None, - ) { - Ok(query) => query.with_replacement(self.replacement(cx)), - Err(e) => { - self.query_error = Some(e.to_string()); - self.clear_active_searchable_item_matches(window, cx); - cx.notify(); - return done_rx; - } - } - } else { - match SearchQuery::text( - query, - self.search_options.contains(SearchOptions::WHOLE_WORD), - self.search_options.contains(SearchOptions::CASE_SENSITIVE), - false, - PathMatcher::default(), - PathMatcher::default(), - false, - None, - ) { - Ok(query) => query.with_replacement(self.replacement(cx)), - Err(e) => { - self.query_error = Some(e.to_string()); - self.clear_active_searchable_item_matches(window, cx); - cx.notify(); - return done_rx; - } - } - } - .into() - }; - - self.active_search = Some(query.clone()); - let query_text = query.as_str().to_string(); - - let matches = active_searchable_item.find_matches(query, window, cx); - - let active_searchable_item = active_searchable_item.downgrade(); - self.pending_search = Some(cx.spawn_in(window, async move |this, cx| { - let matches = matches.await; - - this.update_in(cx, |this, window, cx| { - if let Some(active_searchable_item) = - WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx) - { - this.searchable_items_with_matches - .insert(active_searchable_item.downgrade(), matches); - - this.update_match_index(window, cx); - if add_to_history { - this.search_history - .add(&mut this.search_history_cursor, query_text); - } - if !this.dismissed { - let matches = this - .searchable_items_with_matches - .get(&active_searchable_item.downgrade()) - .unwrap(); - if matches.is_empty() { - active_searchable_item.clear_matches(window, cx); - } else { - active_searchable_item.update_matches( - matches, - this.active_match_index, - window, - cx, - ); - } - let _ = done_tx.send(()); - } - cx.notify(); - } - }) - .log_err(); - })); - } - } - done_rx - } - - fn reverse_direction_if_backwards(&self, direction: Direction) -> Direction { - if self.search_options.contains(SearchOptions::BACKWARDS) { - direction.opposite() - } else { - direction - } - } - - pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context) { - let direction = self.reverse_direction_if_backwards(Direction::Next); - let new_index = self - .active_searchable_item - .as_ref() - .and_then(|searchable_item| { - let matches = self - .searchable_items_with_matches - .get(&searchable_item.downgrade())?; - searchable_item.active_match_index(direction, matches, window, cx) - }); - if new_index != self.active_match_index { - self.active_match_index = new_index; - if !self.dismissed { - if let Some(searchable_item) = self.active_searchable_item.as_ref() { - if let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - if !matches.is_empty() { - searchable_item.update_matches(matches, new_index, window, cx); - } - } - } - } - cx.notify(); - } - } - - fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { - self.cycle_field(Direction::Next, window, cx); - } - - fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context) { - self.cycle_field(Direction::Prev, window, cx); - } - fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context) { - let mut handles = vec![self.query_editor.focus_handle(cx)]; - if self.replace_enabled { - handles.push(self.replacement_editor.focus_handle(cx)); - } - if let Some(item) = self.active_searchable_item.as_ref() { - handles.push(item.item_focus_handle(cx)); - } - let current_index = match handles.iter().position(|focus| focus.is_focused(window)) { - Some(index) => index, - None => return, - }; - - let new_index = match direction { - Direction::Next => (current_index + 1) % handles.len(), - Direction::Prev if current_index == 0 => handles.len() - 1, - Direction::Prev => (current_index - 1) % handles.len(), - }; - let next_focus_handle = &handles[new_index]; - self.focus(next_focus_handle, window); - cx.stop_propagation(); - } - - fn next_history_query( - &mut self, - _: &NextHistoryQuery, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(new_query) = self - .search_history - .next(&mut self.search_history_cursor) - .map(str::to_string) - { - drop(self.search(&new_query, Some(self.search_options), false, window, cx)); - } else { - self.search_history_cursor.reset(); - drop(self.search("", Some(self.search_options), false, window, cx)); - } - } - - fn previous_history_query( - &mut self, - _: &PreviousHistoryQuery, - window: &mut Window, - cx: &mut Context, - ) { - if self.query(cx).is_empty() - && let Some(new_query) = self - .search_history - .current(&self.search_history_cursor) - .map(str::to_string) - { - drop(self.search(&new_query, Some(self.search_options), false, window, cx)); - return; - } - - if let Some(new_query) = self - .search_history - .previous(&mut self.search_history_cursor) - .map(str::to_string) - { - drop(self.search(&new_query, Some(self.search_options), false, window, cx)); - } - } - - fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window) { - window.invalidate_character_coordinates(); - window.focus(handle); - } - - fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context) { - if self.active_searchable_item.is_some() { - self.replace_enabled = !self.replace_enabled; - let handle = if self.replace_enabled { - self.replacement_editor.focus_handle(cx) - } else { - self.query_editor.focus_handle(cx) - }; - self.focus(&handle, window); - cx.notify(); - } - } - - fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context) { - let mut should_propagate = true; - if !self.dismissed - && self.active_search.is_some() - && let Some(searchable_item) = self.active_searchable_item.as_ref() - && let Some(query) = self.active_search.as_ref() - && let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - if let Some(active_index) = self.active_match_index { - let query = query - .as_ref() - .clone() - .with_replacement(self.replacement(cx)); - searchable_item.replace(matches.at(active_index), &query, window, cx); - self.select_next_match(&SelectNextMatch, window, cx); - } - should_propagate = false; - } - if !should_propagate { - cx.stop_propagation(); - } - } - - pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context) { - if !self.dismissed - && self.active_search.is_some() - && let Some(searchable_item) = self.active_searchable_item.as_ref() - && let Some(query) = self.active_search.as_ref() - && let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - let query = query - .as_ref() - .clone() - .with_replacement(self.replacement(cx)); - searchable_item.replace_all(&mut matches.iter(), &query, window, cx); - } - } - - pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context) -> bool { - self.update_match_index(window, cx); - self.active_match_index.is_some() - } - - pub fn should_use_smartcase_search(&mut self, cx: &mut Context) -> bool { - EditorSettings::get_global(cx).use_smartcase_search - } - - pub fn is_contains_uppercase(&mut self, str: &String) -> bool { - str.chars().any(|c| c.is_uppercase()) - } - - fn smartcase(&mut self, window: &mut Window, cx: &mut Context) { - if self.should_use_smartcase_search(cx) { - let query = self.query(cx); - if !query.is_empty() { - let is_case = self.is_contains_uppercase(&query); - if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case { - self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx); - } - } - } - } - - fn adjust_query_regex_language(&self, cx: &mut App) { - let enable = self.search_options.contains(SearchOptions::REGEX); - let query_buffer = self - .query_editor - .read(cx) - .buffer() - .read(cx) - .as_singleton() - .expect("query editor should be backed by a singleton buffer"); - - if enable { - if let Some(regex_language) = self.regex_language.clone() { - query_buffer.update(cx, |query_buffer, cx| { - query_buffer.set_language(Some(regex_language), cx); - }) - } - } else { - query_buffer.update(cx, |query_buffer, cx| { - query_buffer.set_language(None, cx); - }) - } - } - - /// Updates the searchable item's case sensitivity option to match the - /// search bar's current case sensitivity setting. This ensures that - /// editor's `select_next`/ `select_previous` operations respect the buffer - /// search bar's search options. - /// - /// Clears the case sensitivity when the search bar is dismissed so that - /// only the editor's settings are respected. - fn sync_select_next_case_sensitivity(&self, cx: &mut Context) { - let case_sensitive = match self.dismissed { - true => None, - false => Some(self.search_options.contains(SearchOptions::CASE_SENSITIVE)), - }; - - if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { - active_searchable_item.set_search_is_case_sensitive(case_sensitive, cx); - } - } -} - -#[cfg(test)] -mod tests { - use std::ops::Range; - - use super::*; - use editor::{ - DisplayPoint, Editor, MultiBuffer, SearchSettings, SelectionEffects, - display_map::DisplayRow, test::editor_test_context::EditorTestContext, - }; - use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext}; - use language::{Buffer, Point}; - use settings::{SearchSettingsContent, SettingsStore}; - use smol::stream::StreamExt as _; - use unindent::Unindent as _; - use util_macros::perf; - - fn init_globals(cx: &mut TestAppContext) { - cx.update(|cx| { - let store = settings::SettingsStore::test(cx); - cx.set_global(store); - editor::init(cx); - - theme::init(theme::LoadThemes::JustBase, cx); - crate::init(cx); - }); - } - - fn init_test( - cx: &mut TestAppContext, - ) -> ( - Entity, - Entity, - &mut VisualTestContext, - ) { - init_globals(cx); - let buffer = cx.new(|cx| { - Buffer::local( - r#" - A regular expression (shortened as regex or regexp;[1] also referred to as - rational expression[2][3]) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent(), - cx, - ) - }); - let mut editor = None; - let window = cx.add_window(|window, cx| { - let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure( - "keymaps/default-macos.json", - cx, - ) - .unwrap(); - cx.bind_keys(default_key_bindings); - editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx))); - let mut search_bar = BufferSearchBar::new(None, window, cx); - search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx); - search_bar.show(window, cx); - search_bar - }); - let search_bar = window.root(cx).unwrap(); - - let cx = VisualTestContext::from_window(*window, cx).into_mut(); - - (editor.unwrap(), search_bar, cx) - } - - #[perf] - #[gpui::test] - async fn test_search_simple(cx: &mut TestAppContext) { - let (editor, search_bar, cx) = init_test(cx); - let display_points_of = |background_highlights: Vec<(Range, Hsla)>| { - background_highlights - .into_iter() - .map(|(range, _)| range) - .collect::>() - }; - // Search for a string that appears with different casing. - // By default, search is case-insensitive. - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("us", None, true, window, cx) - }) - .await - .unwrap(); - editor.update_in(cx, |editor, window, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(window, cx)), - &[ - DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19), - DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45), - ] - ); - }); - - // Switch to a case sensitive search. - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx); - }); - let mut editor_notifications = cx.notifications(&editor); - editor_notifications.next().await; - editor.update_in(cx, |editor, window, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(window, cx)), - &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),] - ); - }); - - // Search for a string that appears both as a whole word and - // within other words. By default, all results are found. - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("or", None, true, window, cx) - }) - .await - .unwrap(); - editor.update_in(cx, |editor, window, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(window, cx)), - &[ - DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26), - DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43), - DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73), - DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3), - DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13), - DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58), - DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62), - ] - ); - }); - - // Switch to a whole word search. - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx); - }); - let mut editor_notifications = cx.notifications(&editor); - editor_notifications.next().await; - editor.update_in(cx, |editor, window, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(window, cx)), - &[ - DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43), - DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13), - DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58), - ] - ); - }); - - editor.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0) - ]) - }); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - assert_eq!(search_bar.active_match_index, Some(0)); - search_bar.select_next_match(&SelectNextMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(0)); - }); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_next_match(&SelectNextMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(1)); - }); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_next_match(&SelectNextMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(2)); - }); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_next_match(&SelectNextMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(0)); - }); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_prev_match(&SelectPreviousMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(2)); - }); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_prev_match(&SelectPreviousMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(1)); - }); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_prev_match(&SelectPreviousMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(0)); - }); - - // Park the cursor in between matches and ensure that going to the previous match selects - // the closest match to the left. - editor.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0) - ]) - }); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - assert_eq!(search_bar.active_match_index, Some(1)); - search_bar.select_prev_match(&SelectPreviousMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(0)); - }); - - // Park the cursor in between matches and ensure that going to the next match selects the - // closest match to the right. - editor.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0) - ]) - }); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - assert_eq!(search_bar.active_match_index, Some(1)); - search_bar.select_next_match(&SelectNextMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(1)); - }); - - // Park the cursor after the last match and ensure that going to the previous match selects - // the last match. - editor.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60) - ]) - }); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - assert_eq!(search_bar.active_match_index, Some(2)); - search_bar.select_prev_match(&SelectPreviousMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(2)); - }); - - // Park the cursor after the last match and ensure that going to the next match selects the - // first match. - editor.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60) - ]) - }); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - assert_eq!(search_bar.active_match_index, Some(2)); - search_bar.select_next_match(&SelectNextMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(0)); - }); - - // Park the cursor before the first match and ensure that going to the previous match - // selects the last match. - editor.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0) - ]) - }); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - assert_eq!(search_bar.active_match_index, Some(0)); - search_bar.select_prev_match(&SelectPreviousMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(2)); - }); - } - - fn display_points_of( - background_highlights: Vec<(Range, Hsla)>, - ) -> Vec> { - background_highlights - .into_iter() - .map(|(range, _)| range) - .collect::>() - } - - #[perf] - #[gpui::test] - async fn test_search_option_handling(cx: &mut TestAppContext) { - let (editor, search_bar, cx) = init_test(cx); - - // show with options should make current search case sensitive - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.show(window, cx); - search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), true, window, cx) - }) - .await - .unwrap(); - editor.update_in(cx, |editor, window, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(window, cx)), - &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),] - ); - }); - - // search_suggested should restore default options - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.search_suggested(window, cx); - assert_eq!(search_bar.search_options, SearchOptions::NONE) - }); - - // toggling a search option should update the defaults - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search( - "regex", - Some(SearchOptions::CASE_SENSITIVE), - true, - window, - cx, - ) - }) - .await - .unwrap(); - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx) - }); - let mut editor_notifications = cx.notifications(&editor); - editor_notifications.next().await; - editor.update_in(cx, |editor, window, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(window, cx)), - &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),] - ); - }); - - // defaults should still include whole word - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.search_suggested(window, cx); - assert_eq!( - search_bar.search_options, - SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD - ) - }); - } - - #[perf] - #[gpui::test] - async fn test_search_select_all_matches(cx: &mut TestAppContext) { - init_globals(cx); - let buffer_text = r#" - A regular expression (shortened as regex or regexp;[1] also referred to as - rational expression[2][3]) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent(); - let expected_query_matches_count = buffer_text - .chars() - .filter(|c| c.eq_ignore_ascii_case(&'a')) - .count(); - assert!( - expected_query_matches_count > 1, - "Should pick a query with multiple results" - ); - let buffer = cx.new(|cx| Buffer::local(buffer_text, cx)); - let window = cx.add_window(|_, _| gpui::Empty); - - let editor = window.build_entity(cx, |window, cx| { - Editor::for_buffer(buffer.clone(), None, window, cx) - }); - - let search_bar = window.build_entity(cx, |window, cx| { - let mut search_bar = BufferSearchBar::new(None, window, cx); - search_bar.set_active_pane_item(Some(&editor), window, cx); - search_bar.show(window, cx); - search_bar - }); - - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.search("a", None, true, window, cx) - }) - }) - .unwrap() - .await - .unwrap(); - let initial_selections = window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - let handle = search_bar.query_editor.focus_handle(cx); - window.focus(&handle); - search_bar.activate_current_match(window, cx); - }); - assert!( - !editor.read(cx).is_focused(window), - "Initially, the editor should not be focused" - ); - let initial_selections = editor.update(cx, |editor, cx| { - let initial_selections = editor.selections.display_ranges(&editor.display_snapshot(cx)); - assert_eq!( - initial_selections.len(), 1, - "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}", - ); - initial_selections - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.active_match_index, Some(0)); - let handle = search_bar.query_editor.focus_handle(cx); - window.focus(&handle); - search_bar.select_all_matches(&SelectAllMatches, window, cx); - }); - assert!( - editor.read(cx).is_focused(window), - "Should focus editor after successful SelectAllMatches" - ); - search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx))); - assert_eq!( - all_selections.len(), - expected_query_matches_count, - "Should select all `a` characters in the buffer, but got: {all_selections:?}" - ); - assert_eq!( - search_bar.active_match_index, - Some(0), - "Match index should not change after selecting all matches" - ); - }); - - search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx)); - initial_selections - }).unwrap(); - - window - .update(cx, |_, window, cx| { - assert!( - editor.read(cx).is_focused(window), - "Should still have editor focused after SelectNextMatch" - ); - search_bar.update(cx, |search_bar, cx| { - let all_selections = editor.update(cx, |editor, cx| { - editor - .selections - .display_ranges(&editor.display_snapshot(cx)) - }); - assert_eq!( - all_selections.len(), - 1, - "On next match, should deselect items and select the next match" - ); - assert_ne!( - all_selections, initial_selections, - "Next match should be different from the first selection" - ); - assert_eq!( - search_bar.active_match_index, - Some(1), - "Match index should be updated to the next one" - ); - let handle = search_bar.query_editor.focus_handle(cx); - window.focus(&handle); - search_bar.select_all_matches(&SelectAllMatches, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, window, cx| { - assert!( - editor.read(cx).is_focused(window), - "Should focus editor after successful SelectAllMatches" - ); - search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx))); - assert_eq!( - all_selections.len(), - expected_query_matches_count, - "Should select all `a` characters in the buffer, but got: {all_selections:?}" - ); - assert_eq!( - search_bar.active_match_index, - Some(1), - "Match index should not change after selecting all matches" - ); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.select_prev_match(&SelectPreviousMatch, window, cx); - }); - }) - .unwrap(); - let last_match_selections = window - .update(cx, |_, window, cx| { - assert!( - editor.read(cx).is_focused(window), - "Should still have editor focused after SelectPreviousMatch" - ); - - search_bar.update(cx, |search_bar, cx| { - let all_selections = editor.update(cx, |editor, cx| { - editor - .selections - .display_ranges(&editor.display_snapshot(cx)) - }); - assert_eq!( - all_selections.len(), - 1, - "On previous match, should deselect items and select the previous item" - ); - assert_eq!( - all_selections, initial_selections, - "Previous match should be the same as the first selection" - ); - assert_eq!( - search_bar.active_match_index, - Some(0), - "Match index should be updated to the previous one" - ); - all_selections - }) - }) - .unwrap(); - - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - let handle = search_bar.query_editor.focus_handle(cx); - window.focus(&handle); - search_bar.search("abas_nonexistent_match", None, true, window, cx) - }) - }) - .unwrap() - .await - .unwrap(); - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.select_all_matches(&SelectAllMatches, window, cx); - }); - assert!( - editor.update(cx, |this, _cx| !this.is_focused(window)), - "Should not switch focus to editor if SelectAllMatches does not find any matches" - ); - search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx))); - assert_eq!( - all_selections, last_match_selections, - "Should not select anything new if there are no matches" - ); - assert!( - search_bar.active_match_index.is_none(), - "For no matches, there should be no active match index" - ); - }); - }) - .unwrap(); - } - - #[perf] - #[gpui::test] - async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) { - init_globals(cx); - let buffer_text = r#" - self.buffer.update(cx, |buffer, cx| { - buffer.edit( - edits, - Some(AutoindentMode::Block { - original_indent_columns, - }), - cx, - ) - }); - - this.buffer.update(cx, |buffer, cx| { - buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx) - }); - "# - .unindent(); - let buffer = cx.new(|cx| Buffer::local(buffer_text, cx)); - let cx = cx.add_empty_window(); - - let editor = - cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx)); - - let search_bar = cx.new_window_entity(|window, cx| { - let mut search_bar = BufferSearchBar::new(None, window, cx); - search_bar.set_active_pane_item(Some(&editor), window, cx); - search_bar.show(window, cx); - search_bar - }); - - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search( - "edit\\(", - Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX), - true, - window, - cx, - ) - }) - .await - .unwrap(); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_all_matches(&SelectAllMatches, window, cx); - }); - search_bar.update(cx, |_, cx| { - let all_selections = editor.update(cx, |editor, cx| { - editor - .selections - .display_ranges(&editor.display_snapshot(cx)) - }); - assert_eq!( - all_selections.len(), - 2, - "Should select all `edit(` in the buffer, but got: {all_selections:?}" - ); - }); - - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search( - "edit(", - Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE), - true, - window, - cx, - ) - }) - .await - .unwrap(); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_all_matches(&SelectAllMatches, window, cx); - }); - search_bar.update(cx, |_, cx| { - let all_selections = editor.update(cx, |editor, cx| { - editor - .selections - .display_ranges(&editor.display_snapshot(cx)) - }); - assert_eq!( - all_selections.len(), - 2, - "Should select all `edit(` in the buffer, but got: {all_selections:?}" - ); - }); - } - - #[perf] - #[gpui::test] - async fn test_search_query_history(cx: &mut TestAppContext) { - let (_editor, search_bar, cx) = init_test(cx); - - // Add 3 search items into the history. - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("a", None, true, window, cx) - }) - .await - .unwrap(); - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("b", None, true, window, cx) - }) - .await - .unwrap(); - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), true, window, cx) - }) - .await - .unwrap(); - // Ensure that the latest search is active. - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "c"); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // Next history query after the latest should set the query to the empty string. - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), ""); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), ""); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // First previous query for empty current query should set the query to the latest. - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "c"); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // Further previous items should go over the history in reverse order. - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "b"); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // Previous items should never go behind the first history item. - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "a"); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "a"); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // Next items should go over the history in the original order. - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "b"); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("ba", None, true, window, cx) - }) - .await - .unwrap(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "ba"); - assert_eq!(search_bar.search_options, SearchOptions::NONE); - }); - - // New search input should add another entry to history and move the selection to the end of the history. - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "c"); - assert_eq!(search_bar.search_options, SearchOptions::NONE); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "b"); - assert_eq!(search_bar.search_options, SearchOptions::NONE); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "c"); - assert_eq!(search_bar.search_options, SearchOptions::NONE); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "ba"); - assert_eq!(search_bar.search_options, SearchOptions::NONE); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), ""); - assert_eq!(search_bar.search_options, SearchOptions::NONE); - }); - } - - #[perf] - #[gpui::test] - async fn test_replace_simple(cx: &mut TestAppContext) { - let (editor, search_bar, cx) = init_test(cx); - - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("expression", None, true, window, cx) - }) - .await - .unwrap(); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.replacement_editor.update(cx, |editor, cx| { - // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally. - editor.set_text("expr$1", window, cx); - }); - search_bar.replace_all(&ReplaceAll, window, cx) - }); - assert_eq!( - editor.read_with(cx, |this, cx| { this.text(cx) }), - r#" - A regular expr$1 (shortened as regex or regexp;[1] also referred to as - rational expr$1[2][3]) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent() - ); - - // Search for word boundaries and replace just a single one. - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("or", Some(SearchOptions::WHOLE_WORD), true, window, cx) - }) - .await - .unwrap(); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.replacement_editor.update(cx, |editor, cx| { - editor.set_text("banana", window, cx); - }); - search_bar.replace_next(&ReplaceNext, window, cx) - }); - // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text. - assert_eq!( - editor.read_with(cx, |this, cx| { this.text(cx) }), - r#" - A regular expr$1 (shortened as regex banana regexp;[1] also referred to as - rational expr$1[2][3]) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent() - ); - // Let's turn on regex mode. - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search( - "\\[([^\\]]+)\\]", - Some(SearchOptions::REGEX), - true, - window, - cx, - ) - }) - .await - .unwrap(); - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.replacement_editor.update(cx, |editor, cx| { - editor.set_text("${1}number", window, cx); - }); - search_bar.replace_all(&ReplaceAll, window, cx) - }); - assert_eq!( - editor.read_with(cx, |this, cx| { this.text(cx) }), - r#" - A regular expr$1 (shortened as regex banana regexp;1number also referred to as - rational expr$12number3number) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent() - ); - // Now with a whole-word twist. - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search( - "a\\w+s", - Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD), - true, - window, - cx, - ) - }) - .await - .unwrap(); - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.replacement_editor.update(cx, |editor, cx| { - editor.set_text("things", window, cx); - }); - search_bar.replace_all(&ReplaceAll, window, cx) - }); - // The only word affected by this edit should be `algorithms`, even though there's a bunch - // of words in this text that would match this regex if not for WHOLE_WORD. - assert_eq!( - editor.read_with(cx, |this, cx| { this.text(cx) }), - r#" - A regular expr$1 (shortened as regex banana regexp;1number also referred to as - rational expr$12number3number) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching things - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent() - ); - } - - #[gpui::test] - async fn test_replace_focus(cx: &mut TestAppContext) { - let (editor, search_bar, cx) = init_test(cx); - - editor.update_in(cx, |editor, window, cx| { - editor.set_text("What a bad day!", window, cx) - }); - - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("bad", None, true, window, cx) - }) - .await - .unwrap(); - - // Calling `toggle_replace` in the search bar ensures that the "Replace - // *" buttons are rendered, so we can then simulate clicking the - // buttons. - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.toggle_replace(&ToggleReplace, window, cx) - }); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.replacement_editor.update(cx, |editor, cx| { - editor.set_text("great", window, cx); - }); - }); - - // Focus on the editor instead of the search bar, as we want to ensure - // that pressing the "Replace Next Match" button will work, even if the - // search bar is not focused. - cx.focus(&editor); - - // We'll not simulate clicking the "Replace Next Match " button, asserting that - // the replacement was done. - let button_bounds = cx - .debug_bounds("ICON-ReplaceNext") - .expect("'Replace Next Match' button should be visible"); - cx.simulate_click(button_bounds.center(), gpui::Modifiers::none()); - - assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), - "What a great day!" - ); - } - - struct ReplacementTestParams<'a> { - editor: &'a Entity, - search_bar: &'a Entity, - cx: &'a mut VisualTestContext, - search_text: &'static str, - search_options: Option, - replacement_text: &'static str, - replace_all: bool, - expected_text: String, - } - - async fn run_replacement_test(options: ReplacementTestParams<'_>) { - options - .search_bar - .update_in(options.cx, |search_bar, window, cx| { - if let Some(options) = options.search_options { - search_bar.set_search_options(options, cx); - } - search_bar.search( - options.search_text, - options.search_options, - true, - window, - cx, - ) - }) - .await - .unwrap(); - - options - .search_bar - .update_in(options.cx, |search_bar, window, cx| { - search_bar.replacement_editor.update(cx, |editor, cx| { - editor.set_text(options.replacement_text, window, cx); - }); - - if options.replace_all { - search_bar.replace_all(&ReplaceAll, window, cx) - } else { - search_bar.replace_next(&ReplaceNext, window, cx) - } - }); - - assert_eq!( - options - .editor - .read_with(options.cx, |this, cx| { this.text(cx) }), - options.expected_text - ); - } - - #[perf] - #[gpui::test] - async fn test_replace_special_characters(cx: &mut TestAppContext) { - let (editor, search_bar, cx) = init_test(cx); - - run_replacement_test(ReplacementTestParams { - editor: &editor, - search_bar: &search_bar, - cx, - search_text: "expression", - search_options: None, - replacement_text: r"\n", - replace_all: true, - expected_text: r#" - A regular \n (shortened as regex or regexp;[1] also referred to as - rational \n[2][3]) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent(), - }) - .await; - - run_replacement_test(ReplacementTestParams { - editor: &editor, - search_bar: &search_bar, - cx, - search_text: "or", - search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX), - replacement_text: r"\\\n\\\\", - replace_all: false, - expected_text: r#" - A regular \n (shortened as regex \ - \\ regexp;[1] also referred to as - rational \n[2][3]) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent(), - }) - .await; - - run_replacement_test(ReplacementTestParams { - editor: &editor, - search_bar: &search_bar, - cx, - search_text: r"(that|used) ", - search_options: Some(SearchOptions::REGEX), - replacement_text: r"$1\n", - replace_all: true, - expected_text: r#" - A regular \n (shortened as regex \ - \\ regexp;[1] also referred to as - rational \n[2][3]) is a sequence of characters that - specifies a search - pattern in text. Usually such patterns are used - by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent(), - }) - .await; - } - - #[perf] - #[gpui::test] - async fn test_find_matches_in_selections_singleton_buffer_multiple_selections( - cx: &mut TestAppContext, - ) { - init_globals(cx); - let buffer = cx.new(|cx| { - Buffer::local( - r#" - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - "# - .unindent(), - cx, - ) - }); - let cx = cx.add_empty_window(); - let editor = - cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx)); - - let search_bar = cx.new_window_entity(|window, cx| { - let mut search_bar = BufferSearchBar::new(None, window, cx); - search_bar.set_active_pane_item(Some(&editor), window, cx); - search_bar.show(window, cx); - search_bar - }); - - editor.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)]) - }) - }); - - search_bar.update_in(cx, |search_bar, window, cx| { - let deploy = Deploy { - focus: true, - replace_enabled: false, - selection_search_enabled: true, - }; - search_bar.deploy(&deploy, window, cx); - }); - - cx.run_until_parked(); - - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("aaa", None, true, window, cx) - }) - .await - .unwrap(); - - editor.update(cx, |editor, cx| { - assert_eq!( - editor.search_background_highlights(cx), - &[ - Point::new(1, 0)..Point::new(1, 3), - Point::new(1, 8)..Point::new(1, 11), - Point::new(2, 0)..Point::new(2, 3), - ] - ); - }); - } - - #[perf] - #[gpui::test] - async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections( - cx: &mut TestAppContext, - ) { - init_globals(cx); - let text = r#" - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - "# - .unindent(); - - let cx = cx.add_empty_window(); - let editor = cx.new_window_entity(|window, cx| { - let multibuffer = MultiBuffer::build_multi( - [ - ( - &text, - vec![ - Point::new(0, 0)..Point::new(2, 0), - Point::new(4, 0)..Point::new(5, 0), - ], - ), - (&text, vec![Point::new(9, 0)..Point::new(11, 0)]), - ], - cx, - ); - Editor::for_multibuffer(multibuffer, None, window, cx) - }); - - let search_bar = cx.new_window_entity(|window, cx| { - let mut search_bar = BufferSearchBar::new(None, window, cx); - search_bar.set_active_pane_item(Some(&editor), window, cx); - search_bar.show(window, cx); - search_bar - }); - - editor.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges(vec![ - Point::new(1, 0)..Point::new(1, 4), - Point::new(5, 3)..Point::new(6, 4), - ]) - }) - }); - - search_bar.update_in(cx, |search_bar, window, cx| { - let deploy = Deploy { - focus: true, - replace_enabled: false, - selection_search_enabled: true, - }; - search_bar.deploy(&deploy, window, cx); - }); - - cx.run_until_parked(); - - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("aaa", None, true, window, cx) - }) - .await - .unwrap(); - - editor.update(cx, |editor, cx| { - assert_eq!( - editor.search_background_highlights(cx), - &[ - Point::new(1, 0)..Point::new(1, 3), - Point::new(5, 8)..Point::new(5, 11), - Point::new(6, 0)..Point::new(6, 3), - ] - ); - }); - } - - #[perf] - #[gpui::test] - async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) { - let (editor, search_bar, cx) = init_test(cx); - // Search using valid regexp - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.enable_search_option(SearchOptions::REGEX, window, cx); - search_bar.search("expression", None, true, window, cx) - }) - .await - .unwrap(); - editor.update_in(cx, |editor, window, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(window, cx)), - &[ - DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20), - DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19), - ], - ); - }); - - // Now, the expression is invalid - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("expression (", None, true, window, cx) - }) - .await - .unwrap_err(); - editor.update_in(cx, |editor, window, cx| { - assert!( - display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(), - ); - }); - } - - #[perf] - #[gpui::test] - async fn test_search_options_changes(cx: &mut TestAppContext) { - let (_editor, search_bar, cx) = init_test(cx); - update_search_settings( - SearchSettings { - button: true, - whole_word: false, - case_sensitive: false, - include_ignored: false, - regex: false, - center_on_match: false, - }, - cx, - ); - - let deploy = Deploy { - focus: true, - replace_enabled: false, - selection_search_enabled: true, - }; - - search_bar.update_in(cx, |search_bar, window, cx| { - assert_eq!( - search_bar.search_options, - SearchOptions::NONE, - "Should have no search options enabled by default" - ); - search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx); - assert_eq!( - search_bar.search_options, - SearchOptions::WHOLE_WORD, - "Should enable the option toggled" - ); - assert!( - !search_bar.dismissed, - "Search bar should be present and visible" - ); - search_bar.deploy(&deploy, window, cx); - assert_eq!( - search_bar.search_options, - SearchOptions::WHOLE_WORD, - "After (re)deploying, the option should still be enabled" - ); - - search_bar.dismiss(&Dismiss, window, cx); - search_bar.deploy(&deploy, window, cx); - assert_eq!( - search_bar.search_options, - SearchOptions::WHOLE_WORD, - "After hiding and showing the search bar, search options should be preserved" - ); - - search_bar.toggle_search_option(SearchOptions::REGEX, window, cx); - search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx); - assert_eq!( - search_bar.search_options, - SearchOptions::REGEX, - "Should enable the options toggled" - ); - assert!( - !search_bar.dismissed, - "Search bar should be present and visible" - ); - search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx); - }); - - update_search_settings( - SearchSettings { - button: true, - whole_word: false, - case_sensitive: true, - include_ignored: false, - regex: false, - center_on_match: false, - }, - cx, - ); - search_bar.update_in(cx, |search_bar, window, cx| { - assert_eq!( - search_bar.search_options, - SearchOptions::REGEX | SearchOptions::WHOLE_WORD, - "Should have no search options enabled by default" - ); - - search_bar.deploy(&deploy, window, cx); - assert_eq!( - search_bar.search_options, - SearchOptions::REGEX | SearchOptions::WHOLE_WORD, - "Toggling a non-dismissed search bar with custom options should not change the default options" - ); - search_bar.dismiss(&Dismiss, window, cx); - search_bar.deploy(&deploy, window, cx); - assert_eq!( - search_bar.configured_options, - SearchOptions::CASE_SENSITIVE, - "After a settings update and toggling the search bar, configured options should be updated" - ); - assert_eq!( - search_bar.search_options, - SearchOptions::CASE_SENSITIVE, - "After a settings update and toggling the search bar, configured options should be used" - ); - }); - - update_search_settings( - SearchSettings { - button: true, - whole_word: true, - case_sensitive: true, - include_ignored: false, - regex: false, - center_on_match: false, - }, - cx, - ); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.deploy(&deploy, window, cx); - search_bar.dismiss(&Dismiss, window, cx); - search_bar.show(window, cx); - assert_eq!( - search_bar.search_options, - SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD, - "Calling deploy on an already deployed search bar should not prevent settings updates from being detected" - ); - }); - } - - #[gpui::test] - async fn test_select_occurrence_case_sensitivity(cx: &mut TestAppContext) { - let (editor, search_bar, cx) = init_test(cx); - let mut editor_cx = EditorTestContext::for_editor_in(editor, cx).await; - - // Start with case sensitive search settings. - let mut search_settings = SearchSettings::default(); - search_settings.case_sensitive = true; - update_search_settings(search_settings, cx); - search_bar.update(cx, |search_bar, cx| { - let mut search_options = search_bar.search_options; - search_options.insert(SearchOptions::CASE_SENSITIVE); - search_bar.set_search_options(search_options, cx); - }); - - editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo"); - editor_cx.update_editor(|e, window, cx| { - e.select_next(&Default::default(), window, cx).unwrap(); - }); - editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»"); - - // Update the search bar's case sensitivite toggle, so we can later - // confirm that `select_next` will now be case-insensitive. - editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo"); - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.toggle_case_sensitive(&Default::default(), window, cx); - }); - editor_cx.update_editor(|e, window, cx| { - e.select_next(&Default::default(), window, cx).unwrap(); - }); - editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo"); - - // Confirm that, after dismissing the search bar, only the editor's - // search settings actually affect the behavior of `select_next`. - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.dismiss(&Default::default(), window, cx); - }); - editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo"); - editor_cx.update_editor(|e, window, cx| { - e.select_next(&Default::default(), window, cx).unwrap(); - }); - editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»"); - - // Update the editor's search settings, disabling case sensitivity, to - // check that the value is respected. - let mut search_settings = SearchSettings::default(); - search_settings.case_sensitive = false; - update_search_settings(search_settings, cx); - editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo"); - editor_cx.update_editor(|e, window, cx| { - e.select_next(&Default::default(), window, cx).unwrap(); - }); - editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo"); - } - - fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) { - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.editor.search = Some(SearchSettingsContent { - button: Some(search_settings.button), - whole_word: Some(search_settings.whole_word), - case_sensitive: Some(search_settings.case_sensitive), - include_ignored: Some(search_settings.include_ignored), - regex: Some(search_settings.regex), - center_on_match: Some(search_settings.center_on_match), - }); - }); - }); - }); - } -} diff --git a/crates/search/src/buffer_search/registrar.rs b/crates/search/src/buffer_search/registrar.rs deleted file mode 100644 index 2c640e67ce..0000000000 --- a/crates/search/src/buffer_search/registrar.rs +++ /dev/null @@ -1,176 +0,0 @@ -use gpui::{Action, Context, Div, Entity, InteractiveElement, Window, div}; -use workspace::Workspace; - -use crate::BufferSearchBar; - -/// Registrar inverts the dependency between search and its downstream user, allowing said downstream user to register search action without knowing exactly what those actions are. -pub trait SearchActionsRegistrar { - fn register_handler(&mut self, callback: impl ActionExecutor); -} - -type SearchBarActionCallback = - fn(&mut BufferSearchBar, &A, &mut Window, &mut Context); - -type GetSearchBar = - for<'a, 'b> fn(&'a T, &'a mut Window, &mut Context<'b, T>) -> Option>; - -/// Registers search actions on a div that can be taken out. -pub struct DivRegistrar<'a, 'b, T: 'static> { - div: Option
, - cx: &'a mut Context<'b, T>, - search_getter: GetSearchBar, -} - -impl<'a, 'b, T: 'static> DivRegistrar<'a, 'b, T> { - pub fn new(search_getter: GetSearchBar, cx: &'a mut Context<'b, T>) -> Self { - Self { - div: Some(div()), - cx, - search_getter, - } - } - pub fn into_div(self) -> Div { - // This option is always Some; it's an option in the first place because we want to call methods - // on div that require ownership. - self.div.unwrap() - } -} - -impl SearchActionsRegistrar for DivRegistrar<'_, '_, T> { - fn register_handler(&mut self, callback: impl ActionExecutor) { - let getter = self.search_getter; - self.div = self.div.take().map(|div| { - div.on_action(self.cx.listener(move |this, action, window, cx| { - let should_notify = (getter)(this, window, cx) - .map(|search_bar| { - search_bar.update(cx, |search_bar, cx| { - callback.execute(search_bar, action, window, cx) - }) - }) - .unwrap_or(false); - if should_notify { - cx.notify(); - } else { - cx.propagate(); - } - })) - }); - } -} - -/// Register actions for an active pane. -impl SearchActionsRegistrar for Workspace { - fn register_handler(&mut self, callback: impl ActionExecutor) { - self.register_action(move |workspace, action: &A, window, cx| { - if workspace.has_active_modal(window, cx) && !workspace.hide_modal(window, cx) { - cx.propagate(); - return; - } - - let pane = workspace.active_pane(); - let callback = callback.clone(); - pane.update(cx, |this, cx| { - this.toolbar().update(cx, move |this, cx| { - if let Some(search_bar) = this.item_of_type::() { - let should_notify = search_bar.update(cx, move |search_bar, cx| { - callback.execute(search_bar, action, window, cx) - }); - if should_notify { - cx.notify(); - } else { - cx.propagate(); - } - } - }) - }); - }); - } -} - -type DidHandleAction = bool; -/// Potentially executes the underlying action if some preconditions are met (e.g. buffer search bar is visible) -pub trait ActionExecutor: 'static + Clone { - fn execute( - &self, - search_bar: &mut BufferSearchBar, - action: &A, - window: &mut Window, - cx: &mut Context, - ) -> DidHandleAction; -} - -/// Run an action when the search bar has been dismissed from the panel. -pub struct ForDismissed(pub(super) SearchBarActionCallback); -impl Clone for ForDismissed { - fn clone(&self) -> Self { - Self(self.0) - } -} - -impl ActionExecutor for ForDismissed { - fn execute( - &self, - search_bar: &mut BufferSearchBar, - action: &A, - window: &mut Window, - cx: &mut Context, - ) -> DidHandleAction { - if search_bar.is_dismissed() { - self.0(search_bar, action, window, cx); - true - } else { - false - } - } -} - -/// Run an action when the search bar is deployed. -pub struct ForDeployed(pub(super) SearchBarActionCallback); -impl Clone for ForDeployed { - fn clone(&self) -> Self { - Self(self.0) - } -} - -impl ActionExecutor for ForDeployed { - fn execute( - &self, - search_bar: &mut BufferSearchBar, - action: &A, - window: &mut Window, - cx: &mut Context, - ) -> DidHandleAction { - if search_bar.is_dismissed() || search_bar.active_searchable_item.is_none() { - false - } else { - self.0(search_bar, action, window, cx); - true - } - } -} - -/// Run an action when the search bar has any matches, regardless of whether it -/// is visible or not. -pub struct WithResults(pub(super) SearchBarActionCallback); -impl Clone for WithResults { - fn clone(&self) -> Self { - Self(self.0) - } -} - -impl ActionExecutor for WithResults { - fn execute( - &self, - search_bar: &mut BufferSearchBar, - action: &A, - window: &mut Window, - cx: &mut Context, - ) -> DidHandleAction { - if search_bar.active_match_index.is_some() { - self.0(search_bar, action, window, cx); - true - } else { - false - } - } -} diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs deleted file mode 100644 index a9ca77a5b8..0000000000 --- a/crates/search/src/project_search.rs +++ /dev/null @@ -1,4690 +0,0 @@ -use crate::{ - BufferSearchBar, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, - SearchOption, SearchOptions, SearchSource, SelectNextMatch, SelectPreviousMatch, - ToggleCaseSensitive, ToggleIncludeIgnored, ToggleRegex, ToggleReplace, ToggleWholeWord, - buffer_search::Deploy, - search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input}, -}; -use anyhow::Context as _; -use collections::HashMap; -use editor::{ - Anchor, Editor, EditorEvent, EditorSettings, MAX_TAB_TITLE_LEN, MultiBuffer, PathKey, - SelectionEffects, - actions::{Backtab, SelectAll, Tab}, - items::active_match_index, - multibuffer_context_lines, - scroll::Autoscroll, -}; -use futures::{StreamExt, stream::FuturesOrdered}; -use gpui::{ - Action, AnyElement, App, Axis, Context, Entity, EntityId, EventEmitter, FocusHandle, Focusable, - Global, Hsla, InteractiveElement, IntoElement, KeyContext, ParentElement, Point, Render, - SharedString, Styled, Subscription, Task, UpdateGlobal, WeakEntity, Window, actions, div, -}; -use itertools::Itertools; -use language::{Buffer, Language}; -use menu::Confirm; -use project::{ - Project, ProjectPath, - search::{SearchInputKind, SearchQuery}, - search_history::SearchHistoryCursor, -}; -use settings::Settings; -use std::{ - any::{Any, TypeId}, - mem, - ops::{Not, Range}, - pin::pin, - sync::Arc, -}; -use ui::{IconButtonShape, KeyBinding, Toggleable, Tooltip, prelude::*, utils::SearchInputWidth}; -use util::{ResultExt as _, paths::PathMatcher, rel_path::RelPath}; -use workspace::{ - DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation, - ToolbarItemView, Workspace, WorkspaceId, - item::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions}, - searchable::{Direction, SearchableItem, SearchableItemHandle}, -}; - -actions!( - project_search, - [ - /// Searches in a new project search tab. - SearchInNew, - /// Toggles focus between the search bar and the search results. - ToggleFocus, - /// Moves to the next input field. - NextField, - /// Toggles the search filters panel. - ToggleFilters, - /// Toggles collapse/expand state of all search result excerpts. - ToggleAllSearchResults - ] -); - -#[derive(Default)] -struct ActiveSettings(HashMap, ProjectSearchSettings>); - -impl Global for ActiveSettings {} - -pub fn init(cx: &mut App) { - cx.set_global(ActiveSettings::default()); - cx.observe_new(|workspace: &mut Workspace, _window, _cx| { - register_workspace_action(workspace, move |search_bar, _: &Deploy, window, cx| { - search_bar.focus_search(window, cx); - }); - register_workspace_action(workspace, move |search_bar, _: &FocusSearch, window, cx| { - search_bar.focus_search(window, cx); - }); - register_workspace_action( - workspace, - move |search_bar, _: &ToggleFilters, window, cx| { - search_bar.toggle_filters(window, cx); - }, - ); - register_workspace_action( - workspace, - move |search_bar, _: &ToggleCaseSensitive, window, cx| { - search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx); - }, - ); - register_workspace_action( - workspace, - move |search_bar, _: &ToggleWholeWord, window, cx| { - search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx); - }, - ); - register_workspace_action(workspace, move |search_bar, _: &ToggleRegex, window, cx| { - search_bar.toggle_search_option(SearchOptions::REGEX, window, cx); - }); - register_workspace_action( - workspace, - move |search_bar, action: &ToggleReplace, window, cx| { - search_bar.toggle_replace(action, window, cx) - }, - ); - register_workspace_action( - workspace, - move |search_bar, action: &SelectPreviousMatch, window, cx| { - search_bar.select_prev_match(action, window, cx) - }, - ); - register_workspace_action( - workspace, - move |search_bar, action: &SelectNextMatch, window, cx| { - search_bar.select_next_match(action, window, cx) - }, - ); - - // Only handle search_in_new if there is a search present - register_workspace_action_for_present_search(workspace, |workspace, action, window, cx| { - ProjectSearchView::search_in_new(workspace, action, window, cx) - }); - - register_workspace_action_for_present_search( - workspace, - |workspace, action: &ToggleAllSearchResults, window, cx| { - if let Some(search_view) = workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |search_view, cx| { - search_view.toggle_all_search_results(action, window, cx); - }); - } - }, - ); - - register_workspace_action_for_present_search( - workspace, - |workspace, _: &menu::Cancel, window, cx| { - if let Some(project_search_bar) = workspace - .active_pane() - .read(cx) - .toolbar() - .read(cx) - .item_of_type::() - { - project_search_bar.update(cx, |project_search_bar, cx| { - let search_is_focused = project_search_bar - .active_project_search - .as_ref() - .is_some_and(|search_view| { - search_view - .read(cx) - .query_editor - .read(cx) - .focus_handle(cx) - .is_focused(window) - }); - if search_is_focused { - project_search_bar.move_focus_to_results(window, cx); - } else { - project_search_bar.focus_search(window, cx) - } - }); - } else { - cx.propagate(); - } - }, - ); - - // Both on present and dismissed search, we need to unconditionally handle those actions to focus from the editor. - workspace.register_action(move |workspace, action: &DeploySearch, window, cx| { - if workspace.has_active_modal(window, cx) && !workspace.hide_modal(window, cx) { - cx.propagate(); - return; - } - ProjectSearchView::deploy_search(workspace, action, window, cx); - cx.notify(); - }); - workspace.register_action(move |workspace, action: &NewSearch, window, cx| { - if workspace.has_active_modal(window, cx) && !workspace.hide_modal(window, cx) { - cx.propagate(); - return; - } - ProjectSearchView::new_search(workspace, action, window, cx); - cx.notify(); - }); - }) - .detach(); -} - -fn contains_uppercase(str: &str) -> bool { - str.chars().any(|c| c.is_uppercase()) -} - -pub struct ProjectSearch { - project: Entity, - excerpts: Entity, - pending_search: Option>>, - match_ranges: Vec>, - active_query: Option, - last_search_query_text: Option, - search_id: usize, - no_results: Option, - limit_reached: bool, - search_history_cursor: SearchHistoryCursor, - search_included_history_cursor: SearchHistoryCursor, - search_excluded_history_cursor: SearchHistoryCursor, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -enum InputPanel { - Query, - Replacement, - Exclude, - Include, -} - -pub struct ProjectSearchView { - workspace: WeakEntity, - focus_handle: FocusHandle, - entity: Entity, - query_editor: Entity, - replacement_editor: Entity, - results_editor: Entity, - search_options: SearchOptions, - panels_with_errors: HashMap, - active_match_index: Option, - search_id: usize, - included_files_editor: Entity, - excluded_files_editor: Entity, - filters_enabled: bool, - replace_enabled: bool, - included_opened_only: bool, - regex_language: Option>, - results_collapsed: bool, - _subscriptions: Vec, -} - -#[derive(Debug, Clone)] -pub struct ProjectSearchSettings { - search_options: SearchOptions, - filters_enabled: bool, -} - -pub struct ProjectSearchBar { - active_project_search: Option>, - subscription: Option, -} - -impl ProjectSearch { - pub fn new(project: Entity, cx: &mut Context) -> Self { - let capability = project.read(cx).capability(); - - Self { - project, - excerpts: cx.new(|_| MultiBuffer::new(capability)), - pending_search: Default::default(), - match_ranges: Default::default(), - active_query: None, - last_search_query_text: None, - search_id: 0, - no_results: None, - limit_reached: false, - search_history_cursor: Default::default(), - search_included_history_cursor: Default::default(), - search_excluded_history_cursor: Default::default(), - } - } - - fn clone(&self, cx: &mut Context) -> Entity { - cx.new(|cx| Self { - project: self.project.clone(), - excerpts: self - .excerpts - .update(cx, |excerpts, cx| cx.new(|cx| excerpts.clone(cx))), - pending_search: Default::default(), - match_ranges: self.match_ranges.clone(), - active_query: self.active_query.clone(), - last_search_query_text: self.last_search_query_text.clone(), - search_id: self.search_id, - no_results: self.no_results, - limit_reached: self.limit_reached, - search_history_cursor: self.search_history_cursor.clone(), - search_included_history_cursor: self.search_included_history_cursor.clone(), - search_excluded_history_cursor: self.search_excluded_history_cursor.clone(), - }) - } - fn cursor(&self, kind: SearchInputKind) -> &SearchHistoryCursor { - match kind { - SearchInputKind::Query => &self.search_history_cursor, - SearchInputKind::Include => &self.search_included_history_cursor, - SearchInputKind::Exclude => &self.search_excluded_history_cursor, - } - } - fn cursor_mut(&mut self, kind: SearchInputKind) -> &mut SearchHistoryCursor { - match kind { - SearchInputKind::Query => &mut self.search_history_cursor, - SearchInputKind::Include => &mut self.search_included_history_cursor, - SearchInputKind::Exclude => &mut self.search_excluded_history_cursor, - } - } - - fn search(&mut self, query: SearchQuery, cx: &mut Context) { - let search = self.project.update(cx, |project, cx| { - project - .search_history_mut(SearchInputKind::Query) - .add(&mut self.search_history_cursor, query.as_str().to_string()); - let included = query.as_inner().files_to_include().sources().join(","); - if !included.is_empty() { - project - .search_history_mut(SearchInputKind::Include) - .add(&mut self.search_included_history_cursor, included); - } - let excluded = query.as_inner().files_to_exclude().sources().join(","); - if !excluded.is_empty() { - project - .search_history_mut(SearchInputKind::Exclude) - .add(&mut self.search_excluded_history_cursor, excluded); - } - project.search(query.clone(), cx) - }); - self.last_search_query_text = Some(query.as_str().to_string()); - self.search_id += 1; - self.active_query = Some(query); - self.match_ranges.clear(); - self.pending_search = Some(cx.spawn(async move |project_search, cx| { - let mut matches = pin!(search.ready_chunks(1024)); - project_search - .update(cx, |project_search, cx| { - project_search.match_ranges.clear(); - project_search - .excerpts - .update(cx, |excerpts, cx| excerpts.clear(cx)); - project_search.no_results = Some(true); - project_search.limit_reached = false; - }) - .ok()?; - - let mut limit_reached = false; - while let Some(results) = matches.next().await { - let (buffers_with_ranges, has_reached_limit) = cx - .background_executor() - .spawn(async move { - let mut limit_reached = false; - let mut buffers_with_ranges = Vec::with_capacity(results.len()); - for result in results { - match result { - project::search::SearchResult::Buffer { buffer, ranges } => { - buffers_with_ranges.push((buffer, ranges)); - } - project::search::SearchResult::LimitReached => { - limit_reached = true; - } - } - } - (buffers_with_ranges, limit_reached) - }) - .await; - limit_reached |= has_reached_limit; - let mut new_ranges = project_search - .update(cx, |project_search, cx| { - project_search.excerpts.update(cx, |excerpts, cx| { - buffers_with_ranges - .into_iter() - .map(|(buffer, ranges)| { - excerpts.set_anchored_excerpts_for_path( - PathKey::for_buffer(&buffer, cx), - buffer, - ranges, - multibuffer_context_lines(cx), - cx, - ) - }) - .collect::>() - }) - }) - .ok()?; - while let Some(new_ranges) = new_ranges.next().await { - // `new_ranges.next().await` likely never gets hit while still pending so `async_task` - // will not reschedule, starving other front end tasks, insert a yield point for that here - smol::future::yield_now().await; - project_search - .update(cx, |project_search, cx| { - project_search.match_ranges.extend(new_ranges); - cx.notify(); - }) - .ok()?; - } - } - - project_search - .update(cx, |project_search, cx| { - if !project_search.match_ranges.is_empty() { - project_search.no_results = Some(false); - } - project_search.limit_reached = limit_reached; - project_search.pending_search.take(); - cx.notify(); - }) - .ok()?; - - None - })); - cx.notify(); - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum ViewEvent { - UpdateTab, - Activate, - EditorEvent(editor::EditorEvent), - Dismiss, -} - -impl EventEmitter for ProjectSearchView {} - -impl Render for ProjectSearchView { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - if self.has_matches() { - div() - .flex_1() - .size_full() - .track_focus(&self.focus_handle(cx)) - .child(self.results_editor.clone()) - } else { - let model = self.entity.read(cx); - let has_no_results = model.no_results.unwrap_or(false); - let is_search_underway = model.pending_search.is_some(); - - let heading_text = if is_search_underway { - "Searching…" - } else if has_no_results { - "No Results" - } else { - "Search All Files" - }; - - let heading_text = div() - .justify_center() - .child(Label::new(heading_text).size(LabelSize::Large)); - - let page_content: Option = if let Some(no_results) = model.no_results { - if model.pending_search.is_none() && no_results { - Some( - Label::new("No results found in this project for the provided query") - .size(LabelSize::Small) - .into_any_element(), - ) - } else { - None - } - } else { - Some(self.landing_text_minor(cx).into_any_element()) - }; - - let page_content = page_content.map(|text| div().child(text)); - - h_flex() - .size_full() - .items_center() - .justify_center() - .overflow_hidden() - .bg(cx.theme().colors().editor_background) - .track_focus(&self.focus_handle(cx)) - .child( - v_flex() - .id("project-search-landing-page") - .overflow_y_scroll() - .gap_1() - .child(heading_text) - .children(page_content), - ) - } - } -} - -impl Focusable for ProjectSearchView { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl Item for ProjectSearchView { - type Event = ViewEvent; - fn tab_tooltip_text(&self, cx: &App) -> Option { - let query_text = self.query_editor.read(cx).text(cx); - - query_text - .is_empty() - .not() - .then(|| query_text.into()) - .or_else(|| Some("Project Search".into())) - } - - fn act_as_type<'a>( - &'a self, - type_id: TypeId, - self_handle: &'a Entity, - _: &'a App, - ) -> Option { - if type_id == TypeId::of::() { - Some(self_handle.clone().into()) - } else if type_id == TypeId::of::() { - Some(self.results_editor.clone().into()) - } else { - None - } - } - fn as_searchable(&self, _: &Entity, _: &App) -> Option> { - Some(Box::new(self.results_editor.clone())) - } - - fn deactivated(&mut self, window: &mut Window, cx: &mut Context) { - self.results_editor - .update(cx, |editor, cx| editor.deactivated(window, cx)); - } - - fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { - Some(Icon::new(IconName::MagnifyingGlass)) - } - - fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString { - let last_query: Option = self - .entity - .read(cx) - .last_search_query_text - .as_ref() - .map(|query| { - let query = query.replace('\n', ""); - let query_text = util::truncate_and_trailoff(&query, MAX_TAB_TITLE_LEN); - query_text.into() - }); - - last_query - .filter(|query| !query.is_empty()) - .unwrap_or_else(|| "Project Search".into()) - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - Some("Project Search Opened") - } - - fn for_each_project_item( - &self, - cx: &App, - f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem), - ) { - self.results_editor.for_each_project_item(cx, f) - } - - fn can_save(&self, _: &App) -> bool { - true - } - - fn is_dirty(&self, cx: &App) -> bool { - self.results_editor.read(cx).is_dirty(cx) - } - - fn has_conflict(&self, cx: &App) -> bool { - self.results_editor.read(cx).has_conflict(cx) - } - - fn save( - &mut self, - options: SaveOptions, - project: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.results_editor - .update(cx, |editor, cx| editor.save(options, project, window, cx)) - } - - fn save_as( - &mut self, - _: Entity, - _: ProjectPath, - _window: &mut Window, - _: &mut Context, - ) -> Task> { - unreachable!("save_as should not have been called") - } - - fn reload( - &mut self, - project: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.results_editor - .update(cx, |editor, cx| editor.reload(project, window, cx)) - } - - fn can_split(&self) -> bool { - true - } - - fn clone_on_split( - &self, - _workspace_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> Task>> - where - Self: Sized, - { - let model = self.entity.update(cx, |model, cx| model.clone(cx)); - Task::ready(Some(cx.new(|cx| { - Self::new(self.workspace.clone(), model, window, cx, None) - }))) - } - - fn added_to_workspace( - &mut self, - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, - ) { - self.results_editor.update(cx, |editor, cx| { - editor.added_to_workspace(workspace, window, cx) - }); - } - - fn set_nav_history( - &mut self, - nav_history: ItemNavHistory, - _: &mut Window, - cx: &mut Context, - ) { - self.results_editor.update(cx, |editor, _| { - editor.set_nav_history(Some(nav_history)); - }); - } - - fn navigate( - &mut self, - data: Box, - window: &mut Window, - cx: &mut Context, - ) -> bool { - self.results_editor - .update(cx, |editor, cx| editor.navigate(data, window, cx)) - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { - match event { - ViewEvent::UpdateTab => { - f(ItemEvent::UpdateBreadcrumbs); - f(ItemEvent::UpdateTab); - } - ViewEvent::EditorEvent(editor_event) => { - Editor::to_item_events(editor_event, f); - } - ViewEvent::Dismiss => f(ItemEvent::CloseItem), - _ => {} - } - } - - fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { - if self.has_matches() { - ToolbarItemLocation::Secondary - } else { - ToolbarItemLocation::Hidden - } - } - - fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option> { - self.results_editor.breadcrumbs(theme, cx) - } - - fn breadcrumb_prefix( - &self, - _window: &mut Window, - cx: &mut Context, - ) -> Option { - if !self.has_matches() { - return None; - } - - let is_collapsed = self.results_collapsed; - - let (icon, tooltip_label) = if is_collapsed { - (IconName::ChevronUpDown, "Expand All Search Results") - } else { - (IconName::ChevronDownUp, "Collapse All Search Results") - }; - - let focus_handle = self.query_editor.focus_handle(cx); - - Some( - IconButton::new("project-search-collapse-expand", icon) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .tooltip(move |_, cx| { - Tooltip::for_action_in( - tooltip_label, - &ToggleAllSearchResults, - &focus_handle, - cx, - ) - }) - .on_click(cx.listener(|this, _, window, cx| { - this.toggle_all_search_results(&ToggleAllSearchResults, window, cx); - })) - .into_any_element(), - ) - } -} - -impl ProjectSearchView { - pub fn get_matches(&self, cx: &App) -> Vec> { - self.entity.read(cx).match_ranges.clone() - } - - fn toggle_filters(&mut self, cx: &mut Context) { - self.filters_enabled = !self.filters_enabled; - ActiveSettings::update_global(cx, |settings, cx| { - settings.0.insert( - self.entity.read(cx).project.downgrade(), - self.current_settings(), - ); - }); - } - - fn current_settings(&self) -> ProjectSearchSettings { - ProjectSearchSettings { - search_options: self.search_options, - filters_enabled: self.filters_enabled, - } - } - - fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut Context) { - self.search_options.toggle(option); - ActiveSettings::update_global(cx, |settings, cx| { - settings.0.insert( - self.entity.read(cx).project.downgrade(), - self.current_settings(), - ); - }); - self.adjust_query_regex_language(cx); - } - - fn toggle_opened_only(&mut self, _window: &mut Window, _cx: &mut Context) { - self.included_opened_only = !self.included_opened_only; - } - - pub fn replacement(&self, cx: &App) -> String { - self.replacement_editor.read(cx).text(cx) - } - - fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context) { - if let Some(last_search_query_text) = &self.entity.read(cx).last_search_query_text - && self.query_editor.read(cx).text(cx) != *last_search_query_text - { - // search query has changed, restart search and bail - self.search(cx); - return; - } - if self.entity.read(cx).match_ranges.is_empty() { - return; - } - let Some(active_index) = self.active_match_index else { - return; - }; - - let query = self.entity.read(cx).active_query.clone(); - if let Some(query) = query { - let query = query.with_replacement(self.replacement(cx)); - - // TODO: Do we need the clone here? - let mat = self.entity.read(cx).match_ranges[active_index].clone(); - self.results_editor.update(cx, |editor, cx| { - editor.replace(&mat, &query, window, cx); - }); - self.select_match(Direction::Next, window, cx) - } - } - fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context) { - if let Some(last_search_query_text) = &self.entity.read(cx).last_search_query_text - && self.query_editor.read(cx).text(cx) != *last_search_query_text - { - // search query has changed, restart search and bail - self.search(cx); - return; - } - if self.active_match_index.is_none() { - return; - } - let Some(query) = self.entity.read(cx).active_query.as_ref() else { - return; - }; - let query = query.clone().with_replacement(self.replacement(cx)); - - let match_ranges = self - .entity - .update(cx, |model, _| mem::take(&mut model.match_ranges)); - if match_ranges.is_empty() { - return; - } - - self.results_editor.update(cx, |editor, cx| { - editor.replace_all(&mut match_ranges.iter(), &query, window, cx); - }); - - self.entity.update(cx, |model, _cx| { - model.match_ranges = match_ranges; - }); - } - - fn toggle_all_search_results( - &mut self, - _: &ToggleAllSearchResults, - _window: &mut Window, - cx: &mut Context, - ) { - self.results_collapsed = !self.results_collapsed; - self.update_results_visibility(cx); - } - - fn update_results_visibility(&mut self, cx: &mut Context) { - self.results_editor.update(cx, |editor, cx| { - let multibuffer = editor.buffer().read(cx); - let buffer_ids = multibuffer.excerpt_buffer_ids(); - - if self.results_collapsed { - for buffer_id in buffer_ids { - editor.fold_buffer(buffer_id, cx); - } - } else { - for buffer_id in buffer_ids { - editor.unfold_buffer(buffer_id, cx); - } - } - }); - cx.notify(); - } - - pub fn new( - workspace: WeakEntity, - entity: Entity, - window: &mut Window, - cx: &mut Context, - settings: Option, - ) -> Self { - let project; - let excerpts; - let mut replacement_text = None; - let mut query_text = String::new(); - let mut subscriptions = Vec::new(); - - // Read in settings if available - let (mut options, filters_enabled) = if let Some(settings) = settings { - (settings.search_options, settings.filters_enabled) - } else { - let search_options = - SearchOptions::from_settings(&EditorSettings::get_global(cx).search); - (search_options, false) - }; - - { - let entity = entity.read(cx); - project = entity.project.clone(); - excerpts = entity.excerpts.clone(); - if let Some(active_query) = entity.active_query.as_ref() { - query_text = active_query.as_str().to_string(); - replacement_text = active_query.replacement().map(ToOwned::to_owned); - options = SearchOptions::from_query(active_query); - } - } - subscriptions.push(cx.observe_in(&entity, window, |this, _, window, cx| { - this.entity_changed(window, cx) - })); - - let query_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Search all files…", window, cx); - editor.set_text(query_text, window, cx); - editor - }); - // Subscribe to query_editor in order to reraise editor events for workspace item activation purposes - subscriptions.push( - cx.subscribe(&query_editor, |this, _, event: &EditorEvent, cx| { - if let EditorEvent::Edited { .. } = event - && EditorSettings::get_global(cx).use_smartcase_search - { - let query = this.search_query_text(cx); - if !query.is_empty() - && this.search_options.contains(SearchOptions::CASE_SENSITIVE) - != contains_uppercase(&query) - { - this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); - } - } - cx.emit(ViewEvent::EditorEvent(event.clone())) - }), - ); - let replacement_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Replace in project…", window, cx); - if let Some(text) = replacement_text { - editor.set_text(text, window, cx); - } - editor - }); - let results_editor = cx.new(|cx| { - let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), window, cx); - editor.set_searchable(false); - editor.set_in_project_search(true); - editor - }); - subscriptions.push(cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))); - - subscriptions.push( - cx.subscribe(&results_editor, |this, _, event: &EditorEvent, cx| { - if matches!(event, editor::EditorEvent::SelectionsChanged { .. }) { - this.update_match_index(cx); - } - // Reraise editor events for workspace item activation purposes - cx.emit(ViewEvent::EditorEvent(event.clone())); - }), - ); - - let included_files_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Include: crates/**/*.toml", window, cx); - - editor - }); - // Subscribe to include_files_editor in order to reraise editor events for workspace item activation purposes - subscriptions.push( - cx.subscribe(&included_files_editor, |_, _, event: &EditorEvent, cx| { - cx.emit(ViewEvent::EditorEvent(event.clone())) - }), - ); - - let excluded_files_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Exclude: vendor/*, *.lock", window, cx); - - editor - }); - // Subscribe to excluded_files_editor in order to reraise editor events for workspace item activation purposes - subscriptions.push( - cx.subscribe(&excluded_files_editor, |_, _, event: &EditorEvent, cx| { - cx.emit(ViewEvent::EditorEvent(event.clone())) - }), - ); - - let focus_handle = cx.focus_handle(); - subscriptions.push(cx.on_focus(&focus_handle, window, |_, window, cx| { - cx.on_next_frame(window, |this, window, cx| { - if this.focus_handle.is_focused(window) { - if this.has_matches() { - this.results_editor.focus_handle(cx).focus(window); - } else { - this.query_editor.focus_handle(cx).focus(window); - } - } - }); - })); - - let languages = project.read(cx).languages().clone(); - cx.spawn(async move |project_search_view, cx| { - let regex_language = languages - .language_for_name("regex") - .await - .context("loading regex language")?; - project_search_view - .update(cx, |project_search_view, cx| { - project_search_view.regex_language = Some(regex_language); - project_search_view.adjust_query_regex_language(cx); - }) - .ok(); - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - - // Check if Worktrees have all been previously indexed - let mut this = ProjectSearchView { - workspace, - focus_handle, - replacement_editor, - search_id: entity.read(cx).search_id, - entity, - query_editor, - results_editor, - search_options: options, - panels_with_errors: HashMap::default(), - active_match_index: None, - included_files_editor, - excluded_files_editor, - filters_enabled, - replace_enabled: false, - included_opened_only: false, - regex_language: None, - results_collapsed: false, - _subscriptions: subscriptions, - }; - - this.entity_changed(window, cx); - this - } - - pub fn new_search_in_directory( - workspace: &mut Workspace, - dir_path: &RelPath, - window: &mut Window, - cx: &mut Context, - ) { - let filter_str = dir_path.display(workspace.path_style(cx)); - - let weak_workspace = cx.entity().downgrade(); - - let entity = cx.new(|cx| ProjectSearch::new(workspace.project().clone(), cx)); - let search = cx.new(|cx| ProjectSearchView::new(weak_workspace, entity, window, cx, None)); - workspace.add_item_to_active_pane(Box::new(search.clone()), None, true, window, cx); - search.update(cx, |search, cx| { - search - .included_files_editor - .update(cx, |editor, cx| editor.set_text(filter_str, window, cx)); - search.filters_enabled = true; - search.focus_query_editor(window, cx) - }); - } - - /// Re-activate the most recently activated search in this pane or the most recent if it has been closed. - /// If no search exists in the workspace, create a new one. - pub fn deploy_search( - workspace: &mut Workspace, - action: &workspace::DeploySearch, - window: &mut Window, - cx: &mut Context, - ) { - let existing = workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()); - - Self::existing_or_new_search(workspace, existing, action, window, cx); - } - - fn search_in_new( - workspace: &mut Workspace, - _: &SearchInNew, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(search_view) = workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - { - let new_query = search_view.update(cx, |search_view, cx| { - let open_buffers = if search_view.included_opened_only { - Some(search_view.open_buffers(cx, workspace)) - } else { - None - }; - let new_query = search_view.build_search_query(cx, open_buffers); - if new_query.is_some() - && let Some(old_query) = search_view.entity.read(cx).active_query.clone() - { - search_view.query_editor.update(cx, |editor, cx| { - editor.set_text(old_query.as_str(), window, cx); - }); - search_view.search_options = SearchOptions::from_query(&old_query); - search_view.adjust_query_regex_language(cx); - } - new_query - }); - if let Some(new_query) = new_query { - let entity = cx.new(|cx| { - let mut entity = ProjectSearch::new(workspace.project().clone(), cx); - entity.search(new_query, cx); - entity - }); - let weak_workspace = cx.entity().downgrade(); - workspace.add_item_to_active_pane( - Box::new(cx.new(|cx| { - ProjectSearchView::new(weak_workspace, entity, window, cx, None) - })), - None, - true, - window, - cx, - ); - } - } - } - - // Add another search tab to the workspace. - fn new_search( - workspace: &mut Workspace, - _: &workspace::NewSearch, - window: &mut Window, - cx: &mut Context, - ) { - Self::existing_or_new_search(workspace, None, &DeploySearch::find(), window, cx) - } - - fn existing_or_new_search( - workspace: &mut Workspace, - existing: Option>, - action: &workspace::DeploySearch, - window: &mut Window, - cx: &mut Context, - ) { - let query = workspace.active_item(cx).and_then(|item| { - if let Some(buffer_search_query) = buffer_search_query(workspace, item.as_ref(), cx) { - return Some(buffer_search_query); - } - - let editor = item.act_as::(cx)?; - let query = editor.query_suggestion(window, cx); - if query.is_empty() { None } else { Some(query) } - }); - - let search = if let Some(existing) = existing { - workspace.activate_item(&existing, true, true, window, cx); - existing - } else { - let settings = cx - .global::() - .0 - .get(&workspace.project().downgrade()); - - let settings = settings.cloned(); - - let weak_workspace = cx.entity().downgrade(); - - let project_search = cx.new(|cx| ProjectSearch::new(workspace.project().clone(), cx)); - let project_search_view = cx.new(|cx| { - ProjectSearchView::new(weak_workspace, project_search, window, cx, settings) - }); - - workspace.add_item_to_active_pane( - Box::new(project_search_view.clone()), - None, - true, - window, - cx, - ); - project_search_view - }; - - search.update(cx, |search, cx| { - search.replace_enabled = action.replace_enabled; - if let Some(query) = query { - search.set_query(&query, window, cx); - } - if let Some(included_files) = action.included_files.as_deref() { - search - .included_files_editor - .update(cx, |editor, cx| editor.set_text(included_files, window, cx)); - search.filters_enabled = true; - } - if let Some(excluded_files) = action.excluded_files.as_deref() { - search - .excluded_files_editor - .update(cx, |editor, cx| editor.set_text(excluded_files, window, cx)); - search.filters_enabled = true; - } - search.focus_query_editor(window, cx) - }); - } - - fn prompt_to_save_if_dirty_then_search( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let project = self.entity.read(cx).project.clone(); - - let can_autosave = self.results_editor.can_autosave(cx); - let autosave_setting = self.results_editor.workspace_settings(cx).autosave; - - let will_autosave = can_autosave && autosave_setting.should_save_on_close(); - - let is_dirty = self.is_dirty(cx); - - cx.spawn_in(window, async move |this, cx| { - let skip_save_on_close = this - .read_with(cx, |this, cx| { - this.workspace.read_with(cx, |workspace, cx| { - workspace::Pane::skip_save_on_close(&this.results_editor, workspace, cx) - }) - })? - .unwrap_or(false); - - let should_prompt_to_save = !skip_save_on_close && !will_autosave && is_dirty; - - let should_search = if should_prompt_to_save { - let options = &["Save", "Don't Save", "Cancel"]; - let result_channel = this.update_in(cx, |_, window, cx| { - window.prompt( - gpui::PromptLevel::Warning, - "Project search buffer contains unsaved edits. Do you want to save it?", - None, - options, - cx, - ) - })?; - let result = result_channel.await?; - let should_save = result == 0; - if should_save { - this.update_in(cx, |this, window, cx| { - this.save( - SaveOptions { - format: true, - autosave: false, - }, - project, - window, - cx, - ) - })? - .await - .log_err(); - } - - result != 2 - } else { - true - }; - if should_search { - this.update(cx, |this, cx| { - this.search(cx); - })?; - } - anyhow::Ok(()) - }) - } - - fn search(&mut self, cx: &mut Context) { - let open_buffers = if self.included_opened_only { - self.workspace - .update(cx, |workspace, cx| self.open_buffers(cx, workspace)) - .ok() - } else { - None - }; - if let Some(query) = self.build_search_query(cx, open_buffers) { - self.entity.update(cx, |model, cx| model.search(query, cx)); - } - } - - pub fn search_query_text(&self, cx: &App) -> String { - self.query_editor.read(cx).text(cx) - } - - fn build_search_query( - &mut self, - cx: &mut Context, - open_buffers: Option>>, - ) -> Option { - // Do not bail early in this function, as we want to fill out `self.panels_with_errors`. - - let text = self.search_query_text(cx); - let included_files = self - .filters_enabled - .then(|| { - match self.parse_path_matches(self.included_files_editor.read(cx).text(cx), cx) { - Ok(included_files) => { - let should_unmark_error = - self.panels_with_errors.remove(&InputPanel::Include); - if should_unmark_error.is_some() { - cx.notify(); - } - included_files - } - Err(e) => { - let should_mark_error = self - .panels_with_errors - .insert(InputPanel::Include, e.to_string()); - if should_mark_error.is_none() { - cx.notify(); - } - PathMatcher::default() - } - } - }) - .unwrap_or(PathMatcher::default()); - let excluded_files = self - .filters_enabled - .then(|| { - match self.parse_path_matches(self.excluded_files_editor.read(cx).text(cx), cx) { - Ok(excluded_files) => { - let should_unmark_error = - self.panels_with_errors.remove(&InputPanel::Exclude); - if should_unmark_error.is_some() { - cx.notify(); - } - - excluded_files - } - Err(e) => { - let should_mark_error = self - .panels_with_errors - .insert(InputPanel::Exclude, e.to_string()); - if should_mark_error.is_none() { - cx.notify(); - } - PathMatcher::default() - } - } - }) - .unwrap_or(PathMatcher::default()); - - // If the project contains multiple visible worktrees, we match the - // include/exclude patterns against full paths to allow them to be - // disambiguated. For single worktree projects we use worktree relative - // paths for convenience. - let match_full_paths = self - .entity - .read(cx) - .project - .read(cx) - .visible_worktrees(cx) - .count() - > 1; - - let query = if self.search_options.contains(SearchOptions::REGEX) { - match SearchQuery::regex( - text, - self.search_options.contains(SearchOptions::WHOLE_WORD), - self.search_options.contains(SearchOptions::CASE_SENSITIVE), - self.search_options.contains(SearchOptions::INCLUDE_IGNORED), - self.search_options - .contains(SearchOptions::ONE_MATCH_PER_LINE), - included_files, - excluded_files, - match_full_paths, - open_buffers, - ) { - Ok(query) => { - let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Query); - if should_unmark_error.is_some() { - cx.notify(); - } - - Some(query) - } - Err(e) => { - let should_mark_error = self - .panels_with_errors - .insert(InputPanel::Query, e.to_string()); - if should_mark_error.is_none() { - cx.notify(); - } - - None - } - } - } else { - match SearchQuery::text( - text, - self.search_options.contains(SearchOptions::WHOLE_WORD), - self.search_options.contains(SearchOptions::CASE_SENSITIVE), - self.search_options.contains(SearchOptions::INCLUDE_IGNORED), - included_files, - excluded_files, - match_full_paths, - open_buffers, - ) { - Ok(query) => { - let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Query); - if should_unmark_error.is_some() { - cx.notify(); - } - - Some(query) - } - Err(e) => { - let should_mark_error = self - .panels_with_errors - .insert(InputPanel::Query, e.to_string()); - if should_mark_error.is_none() { - cx.notify(); - } - - None - } - } - }; - if !self.panels_with_errors.is_empty() { - return None; - } - if query.as_ref().is_some_and(|query| query.is_empty()) { - return None; - } - query - } - - fn open_buffers(&self, cx: &App, workspace: &Workspace) -> Vec> { - let mut buffers = Vec::new(); - for editor in workspace.items_of_type::(cx) { - if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() { - buffers.push(buffer); - } - } - buffers - } - - fn parse_path_matches(&self, text: String, cx: &App) -> anyhow::Result { - let path_style = self.entity.read(cx).project.read(cx).path_style(cx); - let queries = text - .split(',') - .map(str::trim) - .filter(|maybe_glob_str| !maybe_glob_str.is_empty()) - .map(str::to_owned) - .collect::>(); - Ok(PathMatcher::new(&queries, path_style)?) - } - - fn select_match(&mut self, direction: Direction, window: &mut Window, cx: &mut Context) { - if let Some(index) = self.active_match_index { - let match_ranges = self.entity.read(cx).match_ranges.clone(); - - if !EditorSettings::get_global(cx).search_wrap - && ((direction == Direction::Next && index + 1 >= match_ranges.len()) - || (direction == Direction::Prev && index == 0)) - { - crate::show_no_more_matches(window, cx); - return; - } - - let new_index = self.results_editor.update(cx, |editor, cx| { - editor.match_index_for_direction(&match_ranges, index, direction, 1, window, cx) - }); - - let range_to_select = match_ranges[new_index].clone(); - self.results_editor.update(cx, |editor, cx| { - let range_to_select = editor.range_for_match(&range_to_select); - let autoscroll = if EditorSettings::get_global(cx).search.center_on_match { - Autoscroll::center() - } else { - Autoscroll::fit() - }; - editor.unfold_ranges(std::slice::from_ref(&range_to_select), false, true, cx); - editor.change_selections(SelectionEffects::scroll(autoscroll), window, cx, |s| { - s.select_ranges([range_to_select]) - }); - }); - self.highlight_matches(&match_ranges, Some(new_index), cx); - } - } - - fn focus_query_editor(&mut self, window: &mut Window, cx: &mut Context) { - self.query_editor.update(cx, |query_editor, cx| { - query_editor.select_all(&SelectAll, window, cx); - }); - let editor_handle = self.query_editor.focus_handle(cx); - window.focus(&editor_handle); - } - - fn set_query(&mut self, query: &str, window: &mut Window, cx: &mut Context) { - self.set_search_editor(SearchInputKind::Query, query, window, cx); - if EditorSettings::get_global(cx).use_smartcase_search - && !query.is_empty() - && self.search_options.contains(SearchOptions::CASE_SENSITIVE) - != contains_uppercase(query) - { - self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx) - } - } - - fn set_search_editor( - &mut self, - kind: SearchInputKind, - text: &str, - window: &mut Window, - cx: &mut Context, - ) { - let editor = match kind { - SearchInputKind::Query => &self.query_editor, - SearchInputKind::Include => &self.included_files_editor, - - SearchInputKind::Exclude => &self.excluded_files_editor, - }; - editor.update(cx, |included_editor, cx| { - included_editor.set_text(text, window, cx) - }); - } - - fn focus_results_editor(&mut self, window: &mut Window, cx: &mut Context) { - self.query_editor.update(cx, |query_editor, cx| { - let cursor = query_editor.selections.newest_anchor().head(); - query_editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges([cursor..cursor]) - }); - }); - let results_handle = self.results_editor.focus_handle(cx); - window.focus(&results_handle); - } - - fn entity_changed(&mut self, window: &mut Window, cx: &mut Context) { - let match_ranges = self.entity.read(cx).match_ranges.clone(); - - if match_ranges.is_empty() { - self.active_match_index = None; - self.results_editor.update(cx, |editor, cx| { - editor.clear_background_highlights::(cx); - }); - } else { - self.active_match_index = Some(0); - self.update_match_index(cx); - let prev_search_id = mem::replace(&mut self.search_id, self.entity.read(cx).search_id); - let is_new_search = self.search_id != prev_search_id; - self.results_editor.update(cx, |editor, cx| { - if is_new_search { - let range_to_select = match_ranges - .first() - .map(|range| editor.range_for_match(range)); - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges(range_to_select) - }); - editor.scroll(Point::default(), Some(Axis::Vertical), window, cx); - } - }); - if is_new_search && self.query_editor.focus_handle(cx).is_focused(window) { - self.focus_results_editor(window, cx); - } - } - - cx.emit(ViewEvent::UpdateTab); - cx.notify(); - } - - fn update_match_index(&mut self, cx: &mut Context) { - let results_editor = self.results_editor.read(cx); - let match_ranges = self.entity.read(cx).match_ranges.clone(); - let new_index = active_match_index( - Direction::Next, - &match_ranges, - &results_editor.selections.newest_anchor().head(), - &results_editor.buffer().read(cx).snapshot(cx), - ); - self.highlight_matches(&match_ranges, new_index, cx); - if self.active_match_index != new_index { - self.active_match_index = new_index; - cx.notify(); - } - } - - #[ztracing::instrument(skip_all)] - fn highlight_matches( - &self, - match_ranges: &[Range], - active_index: Option, - cx: &mut Context, - ) { - self.results_editor.update(cx, |editor, cx| { - editor.highlight_background::( - match_ranges, - move |index, theme| { - if active_index == Some(*index) { - theme.colors().search_active_match_background - } else { - theme.colors().search_match_background - } - }, - cx, - ); - }); - } - - pub fn has_matches(&self) -> bool { - self.active_match_index.is_some() - } - - fn landing_text_minor(&self, cx: &App) -> impl IntoElement { - let focus_handle = self.focus_handle.clone(); - v_flex() - .gap_1() - .child( - Label::new("Hit enter to search. For more options:") - .color(Color::Muted) - .mb_2(), - ) - .child( - Button::new("filter-paths", "Include/exclude specific paths") - .icon(IconName::Filter) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .key_binding(KeyBinding::for_action_in(&ToggleFilters, &focus_handle, cx)) - .on_click(|_event, window, cx| { - window.dispatch_action(ToggleFilters.boxed_clone(), cx) - }), - ) - .child( - Button::new("find-replace", "Find and replace") - .icon(IconName::Replace) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .key_binding(KeyBinding::for_action_in(&ToggleReplace, &focus_handle, cx)) - .on_click(|_event, window, cx| { - window.dispatch_action(ToggleReplace.boxed_clone(), cx) - }), - ) - .child( - Button::new("regex", "Match with regex") - .icon(IconName::Regex) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .key_binding(KeyBinding::for_action_in(&ToggleRegex, &focus_handle, cx)) - .on_click(|_event, window, cx| { - window.dispatch_action(ToggleRegex.boxed_clone(), cx) - }), - ) - .child( - Button::new("match-case", "Match case") - .icon(IconName::CaseSensitive) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .key_binding(KeyBinding::for_action_in( - &ToggleCaseSensitive, - &focus_handle, - cx, - )) - .on_click(|_event, window, cx| { - window.dispatch_action(ToggleCaseSensitive.boxed_clone(), cx) - }), - ) - .child( - Button::new("match-whole-words", "Match whole words") - .icon(IconName::WholeWord) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .key_binding(KeyBinding::for_action_in( - &ToggleWholeWord, - &focus_handle, - cx, - )) - .on_click(|_event, window, cx| { - window.dispatch_action(ToggleWholeWord.boxed_clone(), cx) - }), - ) - } - - fn border_color_for(&self, panel: InputPanel, cx: &App) -> Hsla { - if self.panels_with_errors.contains_key(&panel) { - Color::Error.color(cx) - } else { - cx.theme().colors().border - } - } - - fn move_focus_to_results(&mut self, window: &mut Window, cx: &mut Context) { - if !self.results_editor.focus_handle(cx).is_focused(window) - && !self.entity.read(cx).match_ranges.is_empty() - { - cx.stop_propagation(); - self.focus_results_editor(window, cx) - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn results_editor(&self) -> &Entity { - &self.results_editor - } - - fn adjust_query_regex_language(&self, cx: &mut App) { - let enable = self.search_options.contains(SearchOptions::REGEX); - let query_buffer = self - .query_editor - .read(cx) - .buffer() - .read(cx) - .as_singleton() - .expect("query editor should be backed by a singleton buffer"); - if enable { - if let Some(regex_language) = self.regex_language.clone() { - query_buffer.update(cx, |query_buffer, cx| { - query_buffer.set_language(Some(regex_language), cx); - }) - } - } else { - query_buffer.update(cx, |query_buffer, cx| { - query_buffer.set_language(None, cx); - }) - } - } -} - -fn buffer_search_query( - workspace: &mut Workspace, - item: &dyn ItemHandle, - cx: &mut Context, -) -> Option { - let buffer_search_bar = workspace - .pane_for(item) - .and_then(|pane| { - pane.read(cx) - .toolbar() - .read(cx) - .item_of_type::() - })? - .read(cx); - if buffer_search_bar.query_editor_focused() { - let buffer_search_query = buffer_search_bar.query(cx); - if !buffer_search_query.is_empty() { - return Some(buffer_search_query); - } - } - None -} - -impl Default for ProjectSearchBar { - fn default() -> Self { - Self::new() - } -} - -impl ProjectSearchBar { - pub fn new() -> Self { - Self { - active_project_search: None, - subscription: None, - } - } - - fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context) { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - if !search_view - .replacement_editor - .focus_handle(cx) - .is_focused(window) - { - cx.stop_propagation(); - search_view - .prompt_to_save_if_dirty_then_search(window, cx) - .detach_and_log_err(cx); - } - }); - } - } - - fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { - self.cycle_field(Direction::Next, window, cx); - } - - fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context) { - self.cycle_field(Direction::Prev, window, cx); - } - - fn focus_search(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.focus_handle(cx).focus(window); - }); - } - } - - fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context) { - let active_project_search = match &self.active_project_search { - Some(active_project_search) => active_project_search, - None => return, - }; - - active_project_search.update(cx, |project_view, cx| { - let mut views = vec![project_view.query_editor.focus_handle(cx)]; - if project_view.replace_enabled { - views.push(project_view.replacement_editor.focus_handle(cx)); - } - if project_view.filters_enabled { - views.extend([ - project_view.included_files_editor.focus_handle(cx), - project_view.excluded_files_editor.focus_handle(cx), - ]); - } - let current_index = match views.iter().position(|focus| focus.is_focused(window)) { - Some(index) => index, - None => return, - }; - - let new_index = match direction { - Direction::Next => (current_index + 1) % views.len(), - Direction::Prev if current_index == 0 => views.len() - 1, - Direction::Prev => (current_index - 1) % views.len(), - }; - let next_focus_handle = &views[new_index]; - window.focus(next_focus_handle); - cx.stop_propagation(); - }); - } - - pub(crate) fn toggle_search_option( - &mut self, - option: SearchOptions, - window: &mut Window, - cx: &mut Context, - ) -> bool { - if self.active_project_search.is_none() { - return false; - } - - cx.spawn_in(window, async move |this, cx| { - let task = this.update_in(cx, |this, window, cx| { - let search_view = this.active_project_search.as_ref()?; - search_view.update(cx, |search_view, cx| { - search_view.toggle_search_option(option, cx); - search_view - .entity - .read(cx) - .active_query - .is_some() - .then(|| search_view.prompt_to_save_if_dirty_then_search(window, cx)) - }) - })?; - if let Some(task) = task { - task.await?; - } - this.update(cx, |_, cx| { - cx.notify(); - })?; - anyhow::Ok(()) - }) - .detach(); - true - } - - fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context) { - if let Some(search) = &self.active_project_search { - search.update(cx, |this, cx| { - this.replace_enabled = !this.replace_enabled; - let editor_to_focus = if this.replace_enabled { - this.replacement_editor.focus_handle(cx) - } else { - this.query_editor.focus_handle(cx) - }; - window.focus(&editor_to_focus); - cx.notify(); - }); - } - } - - fn toggle_filters(&mut self, window: &mut Window, cx: &mut Context) -> bool { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - search_view.toggle_filters(cx); - search_view - .included_files_editor - .update(cx, |_, cx| cx.notify()); - search_view - .excluded_files_editor - .update(cx, |_, cx| cx.notify()); - window.refresh(); - cx.notify(); - }); - cx.notify(); - true - } else { - false - } - } - - fn toggle_opened_only(&mut self, window: &mut Window, cx: &mut Context) -> bool { - if self.active_project_search.is_none() { - return false; - } - - cx.spawn_in(window, async move |this, cx| { - let task = this.update_in(cx, |this, window, cx| { - let search_view = this.active_project_search.as_ref()?; - search_view.update(cx, |search_view, cx| { - search_view.toggle_opened_only(window, cx); - search_view - .entity - .read(cx) - .active_query - .is_some() - .then(|| search_view.prompt_to_save_if_dirty_then_search(window, cx)) - }) - })?; - if let Some(task) = task { - task.await?; - } - this.update(cx, |_, cx| { - cx.notify(); - })?; - anyhow::Ok(()) - }) - .detach(); - true - } - - fn is_opened_only_enabled(&self, cx: &App) -> bool { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.read(cx).included_opened_only - } else { - false - } - } - - fn move_focus_to_results(&self, window: &mut Window, cx: &mut Context) { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - search_view.move_focus_to_results(window, cx); - }); - cx.notify(); - } - } - - fn next_history_query( - &mut self, - _: &NextHistoryQuery, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - for (editor, kind) in [ - (search_view.query_editor.clone(), SearchInputKind::Query), - ( - search_view.included_files_editor.clone(), - SearchInputKind::Include, - ), - ( - search_view.excluded_files_editor.clone(), - SearchInputKind::Exclude, - ), - ] { - if editor.focus_handle(cx).is_focused(window) { - let new_query = search_view.entity.update(cx, |model, cx| { - let project = model.project.clone(); - - if let Some(new_query) = project.update(cx, |project, _| { - project - .search_history_mut(kind) - .next(model.cursor_mut(kind)) - .map(str::to_string) - }) { - new_query - } else { - model.cursor_mut(kind).reset(); - String::new() - } - }); - search_view.set_search_editor(kind, &new_query, window, cx); - } - } - }); - } - } - - fn previous_history_query( - &mut self, - _: &PreviousHistoryQuery, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - for (editor, kind) in [ - (search_view.query_editor.clone(), SearchInputKind::Query), - ( - search_view.included_files_editor.clone(), - SearchInputKind::Include, - ), - ( - search_view.excluded_files_editor.clone(), - SearchInputKind::Exclude, - ), - ] { - if editor.focus_handle(cx).is_focused(window) { - if editor.read(cx).text(cx).is_empty() - && let Some(new_query) = search_view - .entity - .read(cx) - .project - .read(cx) - .search_history(kind) - .current(search_view.entity.read(cx).cursor(kind)) - .map(str::to_string) - { - search_view.set_search_editor(kind, &new_query, window, cx); - return; - } - - if let Some(new_query) = search_view.entity.update(cx, |model, cx| { - let project = model.project.clone(); - project.update(cx, |project, _| { - project - .search_history_mut(kind) - .previous(model.cursor_mut(kind)) - .map(str::to_string) - }) - }) { - search_view.set_search_editor(kind, &new_query, window, cx); - } - } - } - }); - } - } - - fn select_next_match( - &mut self, - _: &SelectNextMatch, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(search) = self.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.select_match(Direction::Next, window, cx); - }) - } - } - - fn select_prev_match( - &mut self, - _: &SelectPreviousMatch, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(search) = self.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.select_match(Direction::Prev, window, cx); - }) - } - } -} - -impl Render for ProjectSearchBar { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let Some(search) = self.active_project_search.clone() else { - return div(); - }; - let search = search.read(cx); - let focus_handle = search.focus_handle(cx); - - let container_width = window.viewport_size().width; - let input_width = SearchInputWidth::calc_width(container_width); - - let input_base_styles = |panel: InputPanel| { - input_base_styles(search.border_color_for(panel, cx), |div| match panel { - InputPanel::Query | InputPanel::Replacement => div.w(input_width), - InputPanel::Include | InputPanel::Exclude => div.flex_grow(), - }) - }; - let theme_colors = cx.theme().colors(); - let project_search = search.entity.read(cx); - let limit_reached = project_search.limit_reached; - - let color_override = match ( - &project_search.pending_search, - project_search.no_results, - &project_search.active_query, - &project_search.last_search_query_text, - ) { - (None, Some(true), Some(q), Some(p)) if q.as_str() == p => Some(Color::Error), - _ => None, - }; - - let match_text = search - .active_match_index - .and_then(|index| { - let index = index + 1; - let match_quantity = project_search.match_ranges.len(); - if match_quantity > 0 { - debug_assert!(match_quantity >= index); - if limit_reached { - Some(format!("{index}/{match_quantity}+")) - } else { - Some(format!("{index}/{match_quantity}")) - } - } else { - None - } - }) - .unwrap_or_else(|| "0/0".to_string()); - - let query_focus = search.query_editor.focus_handle(cx); - - let query_column = input_base_styles(InputPanel::Query) - .on_action(cx.listener(|this, action, window, cx| this.confirm(action, window, cx))) - .on_action(cx.listener(|this, action, window, cx| { - this.previous_history_query(action, window, cx) - })) - .on_action( - cx.listener(|this, action, window, cx| this.next_history_query(action, window, cx)), - ) - .child(render_text_input(&search.query_editor, color_override, cx)) - .child( - h_flex() - .gap_1() - .child(SearchOption::CaseSensitive.as_button( - search.search_options, - SearchSource::Project(cx), - focus_handle.clone(), - )) - .child(SearchOption::WholeWord.as_button( - search.search_options, - SearchSource::Project(cx), - focus_handle.clone(), - )) - .child(SearchOption::Regex.as_button( - search.search_options, - SearchSource::Project(cx), - focus_handle.clone(), - )), - ); - - let matches_column = h_flex() - .ml_1() - .pl_1p5() - .border_l_1() - .border_color(theme_colors.border_variant) - .child(render_action_button( - "project-search-nav-button", - IconName::ChevronLeft, - search - .active_match_index - .is_none() - .then_some(ActionButtonState::Disabled), - "Select Previous Match", - &SelectPreviousMatch, - query_focus.clone(), - )) - .child(render_action_button( - "project-search-nav-button", - IconName::ChevronRight, - search - .active_match_index - .is_none() - .then_some(ActionButtonState::Disabled), - "Select Next Match", - &SelectNextMatch, - query_focus, - )) - .child( - div() - .id("matches") - .ml_2() - .min_w(rems_from_px(40.)) - .child(Label::new(match_text).size(LabelSize::Small).color( - if search.active_match_index.is_some() { - Color::Default - } else { - Color::Disabled - }, - )) - .when(limit_reached, |el| { - el.tooltip(Tooltip::text( - "Search limits reached.\nTry narrowing your search.", - )) - }), - ); - - let mode_column = h_flex() - .gap_1() - .min_w_64() - .child( - IconButton::new("project-search-filter-button", IconName::Filter) - .shape(IconButtonShape::Square) - .tooltip(|_window, cx| { - Tooltip::for_action("Toggle Filters", &ToggleFilters, cx) - }) - .on_click(cx.listener(|this, _, window, cx| { - this.toggle_filters(window, cx); - })) - .toggle_state( - self.active_project_search - .as_ref() - .map(|search| search.read(cx).filters_enabled) - .unwrap_or_default(), - ) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |_window, cx| { - Tooltip::for_action_in( - "Toggle Filters", - &ToggleFilters, - &focus_handle, - cx, - ) - } - }), - ) - .child(render_action_button( - "project-search", - IconName::Replace, - self.active_project_search - .as_ref() - .map(|search| search.read(cx).replace_enabled) - .and_then(|enabled| enabled.then_some(ActionButtonState::Toggled)), - "Toggle Replace", - &ToggleReplace, - focus_handle.clone(), - )) - .child(matches_column); - - let search_line = h_flex() - .w_full() - .gap_2() - .child(query_column) - .child(mode_column); - - let replace_line = search.replace_enabled.then(|| { - let replace_column = input_base_styles(InputPanel::Replacement) - .child(render_text_input(&search.replacement_editor, None, cx)); - - let focus_handle = search.replacement_editor.read(cx).focus_handle(cx); - - let replace_actions = h_flex() - .min_w_64() - .gap_1() - .child(render_action_button( - "project-search-replace-button", - IconName::ReplaceNext, - Default::default(), - "Replace Next Match", - &ReplaceNext, - focus_handle.clone(), - )) - .child(render_action_button( - "project-search-replace-button", - IconName::ReplaceAll, - Default::default(), - "Replace All Matches", - &ReplaceAll, - focus_handle, - )); - - h_flex() - .w_full() - .gap_2() - .child(replace_column) - .child(replace_actions) - }); - - let filter_line = search.filters_enabled.then(|| { - let include = input_base_styles(InputPanel::Include) - .on_action(cx.listener(|this, action, window, cx| { - this.previous_history_query(action, window, cx) - })) - .on_action(cx.listener(|this, action, window, cx| { - this.next_history_query(action, window, cx) - })) - .child(render_text_input(&search.included_files_editor, None, cx)); - let exclude = input_base_styles(InputPanel::Exclude) - .on_action(cx.listener(|this, action, window, cx| { - this.previous_history_query(action, window, cx) - })) - .on_action(cx.listener(|this, action, window, cx| { - this.next_history_query(action, window, cx) - })) - .child(render_text_input(&search.excluded_files_editor, None, cx)); - let mode_column = h_flex() - .gap_1() - .min_w_64() - .child( - IconButton::new("project-search-opened-only", IconName::FolderSearch) - .shape(IconButtonShape::Square) - .toggle_state(self.is_opened_only_enabled(cx)) - .tooltip(Tooltip::text("Only Search Open Files")) - .on_click(cx.listener(|this, _, window, cx| { - this.toggle_opened_only(window, cx); - })), - ) - .child(SearchOption::IncludeIgnored.as_button( - search.search_options, - SearchSource::Project(cx), - focus_handle.clone(), - )); - h_flex() - .w_full() - .gap_2() - .child( - h_flex() - .gap_2() - .w(input_width) - .child(include) - .child(exclude), - ) - .child(mode_column) - }); - - let mut key_context = KeyContext::default(); - key_context.add("ProjectSearchBar"); - if search - .replacement_editor - .focus_handle(cx) - .is_focused(window) - { - key_context.add("in_replace"); - } - - let query_error_line = search - .panels_with_errors - .get(&InputPanel::Query) - .map(|error| { - Label::new(error) - .size(LabelSize::Small) - .color(Color::Error) - .mt_neg_1() - .ml_2() - }); - - let filter_error_line = search - .panels_with_errors - .get(&InputPanel::Include) - .or_else(|| search.panels_with_errors.get(&InputPanel::Exclude)) - .map(|error| { - Label::new(error) - .size(LabelSize::Small) - .color(Color::Error) - .mt_neg_1() - .ml_2() - }); - - v_flex() - .gap_2() - .py(px(1.0)) - .w_full() - .key_context(key_context) - .on_action(cx.listener(|this, _: &ToggleFocus, window, cx| { - this.move_focus_to_results(window, cx) - })) - .on_action(cx.listener(|this, _: &ToggleFilters, window, cx| { - this.toggle_filters(window, cx); - })) - .capture_action(cx.listener(Self::tab)) - .capture_action(cx.listener(Self::backtab)) - .on_action(cx.listener(|this, action, window, cx| this.confirm(action, window, cx))) - .on_action(cx.listener(|this, action, window, cx| { - this.toggle_replace(action, window, cx); - })) - .on_action(cx.listener(|this, _: &ToggleWholeWord, window, cx| { - this.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx); - })) - .on_action(cx.listener(|this, _: &ToggleCaseSensitive, window, cx| { - this.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx); - })) - .on_action(cx.listener(|this, action, window, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.replace_next(action, window, cx); - }) - } - })) - .on_action(cx.listener(|this, action, window, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.replace_all(action, window, cx); - }) - } - })) - .when(search.filters_enabled, |this| { - this.on_action(cx.listener(|this, _: &ToggleIncludeIgnored, window, cx| { - this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, window, cx); - })) - }) - .on_action(cx.listener(Self::select_next_match)) - .on_action(cx.listener(Self::select_prev_match)) - .child(search_line) - .children(query_error_line) - .children(replace_line) - .children(filter_line) - .children(filter_error_line) - } -} - -impl EventEmitter for ProjectSearchBar {} - -impl ToolbarItemView for ProjectSearchBar { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - _: &mut Window, - cx: &mut Context, - ) -> ToolbarItemLocation { - cx.notify(); - self.subscription = None; - self.active_project_search = None; - if let Some(search) = active_pane_item.and_then(|i| i.downcast::()) { - self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify())); - self.active_project_search = Some(search); - ToolbarItemLocation::PrimaryLeft {} - } else { - ToolbarItemLocation::Hidden - } - } -} - -fn register_workspace_action( - workspace: &mut Workspace, - callback: fn(&mut ProjectSearchBar, &A, &mut Window, &mut Context), -) { - workspace.register_action(move |workspace, action: &A, window, cx| { - if workspace.has_active_modal(window, cx) && !workspace.hide_modal(window, cx) { - cx.propagate(); - return; - } - - workspace.active_pane().update(cx, |pane, cx| { - pane.toolbar().update(cx, move |workspace, cx| { - if let Some(search_bar) = workspace.item_of_type::() { - search_bar.update(cx, move |search_bar, cx| { - if search_bar.active_project_search.is_some() { - callback(search_bar, action, window, cx); - cx.notify(); - } else { - cx.propagate(); - } - }); - } - }); - }) - }); -} - -fn register_workspace_action_for_present_search( - workspace: &mut Workspace, - callback: fn(&mut Workspace, &A, &mut Window, &mut Context), -) { - workspace.register_action(move |workspace, action: &A, window, cx| { - if workspace.has_active_modal(window, cx) && !workspace.hide_modal(window, cx) { - cx.propagate(); - return; - } - - let should_notify = workspace - .active_pane() - .read(cx) - .toolbar() - .read(cx) - .item_of_type::() - .map(|search_bar| search_bar.read(cx).active_project_search.is_some()) - .unwrap_or(false); - if should_notify { - callback(workspace, action, window, cx); - cx.notify(); - } else { - cx.propagate(); - } - }); -} - -#[cfg(any(test, feature = "test-support"))] -pub fn perform_project_search( - search_view: &Entity, - text: impl Into>, - cx: &mut gpui::VisualTestContext, -) { - cx.run_until_parked(); - search_view.update_in(cx, |search_view, window, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text(text, window, cx) - }); - search_view.search(cx); - }); - cx.run_until_parked(); -} - -#[cfg(test)] -pub mod tests { - use std::{ - ops::Deref as _, - path::PathBuf, - sync::{ - Arc, - atomic::{self, AtomicUsize}, - }, - time::Duration, - }; - - use super::*; - use editor::{DisplayPoint, display_map::DisplayRow}; - use gpui::{Action, TestAppContext, VisualTestContext, WindowHandle}; - use language::{FakeLspAdapter, rust_lang}; - use pretty_assertions::assert_eq; - use project::FakeFs; - use serde_json::json; - use settings::{ - InlayHintSettingsContent, SettingsStore, ThemeColorsContent, ThemeStyleContent, - }; - use util::{path, paths::PathStyle, rel_path::rel_path}; - use util_macros::perf; - use workspace::DeploySearch; - - #[perf] - #[gpui::test] - async fn test_project_search(cx: &mut TestAppContext) { - fn dp(row: u32, col: u32) -> DisplayPoint { - DisplayPoint::new(DisplayRow(row), col) - } - - fn assert_active_match_index( - search_view: &WindowHandle, - cx: &mut TestAppContext, - expected_index: usize, - ) { - search_view - .update(cx, |search_view, _window, _cx| { - assert_eq!(search_view.active_match_index, Some(expected_index)); - }) - .unwrap(); - } - - fn assert_selection_range( - search_view: &WindowHandle, - cx: &mut TestAppContext, - expected_range: Range, - ) { - search_view - .update(cx, |search_view, _window, cx| { - assert_eq!( - search_view.results_editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [expected_range] - ); - }) - .unwrap(); - } - - fn assert_highlights( - search_view: &WindowHandle, - cx: &mut TestAppContext, - expected_highlights: Vec<(Range, &str)>, - ) { - search_view - .update(cx, |search_view, window, cx| { - let match_bg = cx.theme().colors().search_match_background; - let active_match_bg = cx.theme().colors().search_active_match_background; - let selection_bg = cx - .theme() - .colors() - .editor_document_highlight_bracket_background; - - let highlights: Vec<_> = expected_highlights - .into_iter() - .map(|(range, color_type)| { - let color = match color_type { - "active" => active_match_bg, - "match" => match_bg, - "selection" => selection_bg, - _ => panic!("Unknown color type"), - }; - (range, color) - }) - .collect(); - - assert_eq!( - search_view.results_editor.update(cx, |editor, cx| editor - .all_text_background_highlights(window, cx)), - highlights.as_slice() - ); - }) - .unwrap(); - } - - fn select_match( - search_view: &WindowHandle, - cx: &mut TestAppContext, - direction: Direction, - ) { - search_view - .update(cx, |search_view, window, cx| { - search_view.select_match(direction, window, cx); - }) - .unwrap(); - } - - init_test(cx); - - // Override active search match color since the fallback theme uses the same color - // for normal search match and active one, which can make this test less robust. - cx.update(|cx| { - SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings(cx, |settings| { - settings.theme.experimental_theme_overrides = Some(ThemeStyleContent { - colors: ThemeColorsContent { - search_active_match_background: Some("#ff0000ff".to_string()), - ..Default::default() - }, - ..Default::default() - }); - }); - }); - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }), - ) - .await; - let project = Project::test(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 search = cx.new(|cx| ProjectSearch::new(project.clone(), cx)); - let search_view = cx.add_window(|window, cx| { - ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None) - }); - - perform_search(search_view, "TWO", cx); - cx.run_until_parked(); - - search_view - .update(cx, |search_view, _window, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;" - ); - }) - .unwrap(); - - assert_active_match_index(&search_view, cx, 0); - assert_selection_range(&search_view, cx, dp(2, 32)..dp(2, 35)); - assert_highlights( - &search_view, - cx, - vec![ - (dp(2, 32)..dp(2, 35), "active"), - (dp(2, 37)..dp(2, 40), "selection"), - (dp(2, 37)..dp(2, 40), "match"), - (dp(5, 6)..dp(5, 9), "match"), - // TODO: we should be getting selection highlight here after project search - // but for some reason we are not getting it here - ], - ); - select_match(&search_view, cx, Direction::Next); - cx.run_until_parked(); - - assert_active_match_index(&search_view, cx, 1); - assert_selection_range(&search_view, cx, dp(2, 37)..dp(2, 40)); - assert_highlights( - &search_view, - cx, - vec![ - (dp(2, 32)..dp(2, 35), "selection"), - (dp(2, 32)..dp(2, 35), "match"), - (dp(2, 37)..dp(2, 40), "active"), - (dp(5, 6)..dp(5, 9), "selection"), - (dp(5, 6)..dp(5, 9), "match"), - ], - ); - select_match(&search_view, cx, Direction::Next); - cx.run_until_parked(); - - assert_active_match_index(&search_view, cx, 2); - assert_selection_range(&search_view, cx, dp(5, 6)..dp(5, 9)); - assert_highlights( - &search_view, - cx, - vec![ - (dp(2, 32)..dp(2, 35), "selection"), - (dp(2, 32)..dp(2, 35), "match"), - (dp(2, 37)..dp(2, 40), "selection"), - (dp(2, 37)..dp(2, 40), "match"), - (dp(5, 6)..dp(5, 9), "active"), - ], - ); - select_match(&search_view, cx, Direction::Next); - cx.run_until_parked(); - - assert_active_match_index(&search_view, cx, 0); - assert_selection_range(&search_view, cx, dp(2, 32)..dp(2, 35)); - assert_highlights( - &search_view, - cx, - vec![ - (dp(2, 32)..dp(2, 35), "active"), - (dp(2, 37)..dp(2, 40), "selection"), - (dp(2, 37)..dp(2, 40), "match"), - (dp(5, 6)..dp(5, 9), "selection"), - (dp(5, 6)..dp(5, 9), "match"), - ], - ); - select_match(&search_view, cx, Direction::Prev); - cx.run_until_parked(); - - assert_active_match_index(&search_view, cx, 2); - assert_selection_range(&search_view, cx, dp(5, 6)..dp(5, 9)); - assert_highlights( - &search_view, - cx, - vec![ - (dp(2, 32)..dp(2, 35), "selection"), - (dp(2, 32)..dp(2, 35), "match"), - (dp(2, 37)..dp(2, 40), "selection"), - (dp(2, 37)..dp(2, 40), "match"), - (dp(5, 6)..dp(5, 9), "active"), - ], - ); - select_match(&search_view, cx, Direction::Prev); - cx.run_until_parked(); - - assert_active_match_index(&search_view, cx, 1); - assert_selection_range(&search_view, cx, dp(2, 37)..dp(2, 40)); - assert_highlights( - &search_view, - cx, - vec![ - (dp(2, 32)..dp(2, 35), "selection"), - (dp(2, 32)..dp(2, 35), "match"), - (dp(2, 37)..dp(2, 40), "active"), - (dp(5, 6)..dp(5, 9), "selection"), - (dp(5, 6)..dp(5, 9), "match"), - ], - ); - } - - #[perf] - #[gpui::test] - async fn test_deploy_project_search_focus(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/dir", - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }), - ) - .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window; - let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); - - let active_item = cx.read(|cx| { - workspace - .read(cx) - .unwrap() - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }); - assert!( - active_item.is_none(), - "Expected no search panel to be active" - ); - - window - .update(cx, move |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 1); - workspace.panes()[0].update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); - - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::find(), - window, - cx, - ) - }) - .unwrap(); - - let Some(search_view) = cx.read(|cx| { - workspace - .read(cx) - .unwrap() - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }) else { - panic!("Search view expected to appear after new search event trigger") - }; - - cx.spawn(|mut cx| async move { - window - .update(&mut cx, |_, window, cx| { - window.dispatch_action(ToggleFocus.boxed_clone(), cx) - }) - .unwrap(); - }) - .detach(); - cx.background_executor.run_until_parked(); - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert!( - search_view.query_editor.focus_handle(cx).is_focused(window), - "Empty search view should be focused after the toggle focus event: no results panel to focus on", - ); - }); - }).unwrap(); - - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - let query_editor = &search_view.query_editor; - assert!( - query_editor.focus_handle(cx).is_focused(window), - "Search view should be focused after the new search view is activated", - ); - let query_text = query_editor.read(cx).text(cx); - assert!( - query_text.is_empty(), - "New search query should be empty but got '{query_text}'", - ); - let results_text = search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)); - assert!( - results_text.is_empty(), - "Empty search view should have no results but got '{results_text}'" - ); - }); - }) - .unwrap(); - - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", window, cx) - }); - search_view.search(cx); - }); - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - let results_text = search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)); - assert!( - results_text.is_empty(), - "Search view for mismatching query should have no results but got '{results_text}'" - ); - assert!( - search_view.query_editor.focus_handle(cx).is_focused(window), - "Search view should be focused after mismatching query had been used in search", - ); - }); - }).unwrap(); - - cx.spawn(|mut cx| async move { - window.update(&mut cx, |_, window, cx| { - window.dispatch_action(ToggleFocus.boxed_clone(), cx) - }) - }) - .detach(); - cx.background_executor.run_until_parked(); - window.update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert!( - search_view.query_editor.focus_handle(cx).is_focused(window), - "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on", - ); - }); - }).unwrap(); - - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("TWO", window, cx) - }); - search_view.search(cx); - }); - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window.update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", - "Search view results should match the query" - ); - assert!( - search_view.results_editor.focus_handle(cx).is_focused(window), - "Search view with mismatching query should be focused after search results are available", - ); - }); - }).unwrap(); - cx.spawn(|mut cx| async move { - window - .update(&mut cx, |_, window, cx| { - window.dispatch_action(ToggleFocus.boxed_clone(), cx) - }) - .unwrap(); - }) - .detach(); - cx.background_executor.run_until_parked(); - window.update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert!( - search_view.results_editor.focus_handle(cx).is_focused(window), - "Search view with matching query should still have its results editor focused after the toggle focus event", - ); - }); - }).unwrap(); - - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::find(), - window, - cx, - ) - }) - .unwrap(); - window.update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row"); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", - "Results should be unchanged after search view 2nd open in a row" - ); - assert!( - search_view.query_editor.focus_handle(cx).is_focused(window), - "Focus should be moved into query editor again after search view 2nd open in a row" - ); - }); - }).unwrap(); - - cx.spawn(|mut cx| async move { - window - .update(&mut cx, |_, window, cx| { - window.dispatch_action(ToggleFocus.boxed_clone(), cx) - }) - .unwrap(); - }) - .detach(); - cx.background_executor.run_until_parked(); - window.update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert!( - search_view.results_editor.focus_handle(cx).is_focused(window), - "Search view with matching query should switch focus to the results editor after the toggle focus event", - ); - }); - }).unwrap(); - } - - #[perf] - #[gpui::test] - async fn test_filters_consider_toggle_state(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/dir", - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }), - ) - .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window; - let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); - - window - .update(cx, move |workspace, window, cx| { - workspace.panes()[0].update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); - - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::find(), - window, - cx, - ) - }) - .unwrap(); - - let Some(search_view) = cx.read(|cx| { - workspace - .read(cx) - .unwrap() - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }) else { - panic!("Search view expected to appear after new search event trigger") - }; - - cx.spawn(|mut cx| async move { - window - .update(&mut cx, |_, window, cx| { - window.dispatch_action(ToggleFocus.boxed_clone(), cx) - }) - .unwrap(); - }) - .detach(); - cx.background_executor.run_until_parked(); - - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("const FOUR", window, cx) - }); - search_view.toggle_filters(cx); - search_view - .excluded_files_editor - .update(cx, |exclude_editor, cx| { - exclude_editor.set_text("four.rs", window, cx) - }); - search_view.search(cx); - }); - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - let results_text = search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)); - assert!( - results_text.is_empty(), - "Search view for query with the only match in an excluded file should have no results but got '{results_text}'" - ); - }); - }).unwrap(); - - cx.spawn(|mut cx| async move { - window.update(&mut cx, |_, window, cx| { - window.dispatch_action(ToggleFocus.boxed_clone(), cx) - }) - }) - .detach(); - cx.background_executor.run_until_parked(); - - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - search_view.toggle_filters(cx); - search_view.search(cx); - }); - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst FOUR: usize = one::ONE + three::THREE;", - "Search view results should contain the queried result in the previously excluded file with filters toggled off" - ); - }); - }) - .unwrap(); - } - - #[perf] - #[gpui::test] - async fn test_new_project_search_focus(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window; - let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); - - let active_item = cx.read(|cx| { - workspace - .read(cx) - .unwrap() - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }); - assert!( - active_item.is_none(), - "Expected no search panel to be active" - ); - - window - .update(cx, move |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 1); - workspace.panes()[0].update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); - - ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) - }) - .unwrap(); - - let Some(search_view) = cx.read(|cx| { - workspace - .read(cx) - .unwrap() - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }) else { - panic!("Search view expected to appear after new search event trigger") - }; - - cx.spawn(|mut cx| async move { - window - .update(&mut cx, |_, window, cx| { - window.dispatch_action(ToggleFocus.boxed_clone(), cx) - }) - .unwrap(); - }) - .detach(); - cx.background_executor.run_until_parked(); - - window.update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert!( - search_view.query_editor.focus_handle(cx).is_focused(window), - "Empty search view should be focused after the toggle focus event: no results panel to focus on", - ); - }); - }).unwrap(); - - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - let query_editor = &search_view.query_editor; - assert!( - query_editor.focus_handle(cx).is_focused(window), - "Search view should be focused after the new search view is activated", - ); - let query_text = query_editor.read(cx).text(cx); - assert!( - query_text.is_empty(), - "New search query should be empty but got '{query_text}'", - ); - let results_text = search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)); - assert!( - results_text.is_empty(), - "Empty search view should have no results but got '{results_text}'" - ); - }); - }) - .unwrap(); - - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", window, cx) - }); - search_view.search(cx); - }); - }) - .unwrap(); - - cx.background_executor.run_until_parked(); - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - let results_text = search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)); - assert!( - results_text.is_empty(), - "Search view for mismatching query should have no results but got '{results_text}'" - ); - assert!( - search_view.query_editor.focus_handle(cx).is_focused(window), - "Search view should be focused after mismatching query had been used in search", - ); - }); - }) - .unwrap(); - cx.spawn(|mut cx| async move { - window.update(&mut cx, |_, window, cx| { - window.dispatch_action(ToggleFocus.boxed_clone(), cx) - }) - }) - .detach(); - cx.background_executor.run_until_parked(); - window.update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert!( - search_view.query_editor.focus_handle(cx).is_focused(window), - "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on", - ); - }); - }).unwrap(); - - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("TWO", window, cx) - }); - search_view.search(cx); - }) - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window.update(cx, |_, window, cx| - search_view.update(cx, |search_view, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", - "Search view results should match the query" - ); - assert!( - search_view.results_editor.focus_handle(cx).is_focused(window), - "Search view with mismatching query should be focused after search results are available", - ); - })).unwrap(); - cx.spawn(|mut cx| async move { - window - .update(&mut cx, |_, window, cx| { - window.dispatch_action(ToggleFocus.boxed_clone(), cx) - }) - .unwrap(); - }) - .detach(); - cx.background_executor.run_until_parked(); - window.update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert!( - search_view.results_editor.focus_handle(cx).is_focused(window), - "Search view with matching query should still have its results editor focused after the toggle focus event", - ); - }); - }).unwrap(); - - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) - }) - .unwrap(); - cx.background_executor.run_until_parked(); - let Some(search_view_2) = cx.read(|cx| { - workspace - .read(cx) - .unwrap() - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }) else { - panic!("Search view expected to appear after new search event trigger") - }; - assert!( - search_view_2 != search_view, - "New search view should be open after `workspace::NewSearch` event" - ); - - window.update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO", "First search view should not have an updated query"); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", - "Results of the first search view should not update too" - ); - assert!( - !search_view.query_editor.focus_handle(cx).is_focused(window), - "Focus should be moved away from the first search view" - ); - }); - }).unwrap(); - - window.update(cx, |_, window, cx| { - search_view_2.update(cx, |search_view_2, cx| { - assert_eq!( - search_view_2.query_editor.read(cx).text(cx), - "two", - "New search view should get the query from the text cursor was at during the event spawn (first search view's first result)" - ); - assert_eq!( - search_view_2 - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "", - "No search results should be in the 2nd view yet, as we did not spawn a search for it" - ); - assert!( - search_view_2.query_editor.focus_handle(cx).is_focused(window), - "Focus should be moved into query editor of the new window" - ); - }); - }).unwrap(); - - window - .update(cx, |_, window, cx| { - search_view_2.update(cx, |search_view_2, cx| { - search_view_2.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("FOUR", window, cx) - }); - search_view_2.search(cx); - }); - }) - .unwrap(); - - cx.background_executor.run_until_parked(); - window.update(cx, |_, window, cx| { - search_view_2.update(cx, |search_view_2, cx| { - assert_eq!( - search_view_2 - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst FOUR: usize = one::ONE + three::THREE;", - "New search view with the updated query should have new search results" - ); - assert!( - search_view_2.results_editor.focus_handle(cx).is_focused(window), - "Search view with mismatching query should be focused after search results are available", - ); - }); - }).unwrap(); - - cx.spawn(|mut cx| async move { - window - .update(&mut cx, |_, window, cx| { - window.dispatch_action(ToggleFocus.boxed_clone(), cx) - }) - .unwrap(); - }) - .detach(); - cx.background_executor.run_until_parked(); - window.update(cx, |_, window, cx| { - search_view_2.update(cx, |search_view_2, cx| { - assert!( - search_view_2.results_editor.focus_handle(cx).is_focused(window), - "Search view with matching query should switch focus to the results editor after the toggle focus event", - ); - });}).unwrap(); - } - - #[perf] - #[gpui::test] - async fn test_new_project_search_in_directory(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - json!({ - "a": { - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - }, - "b": { - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }, - }), - ) - .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let worktree_id = project.read_with(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); - let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); - - let active_item = cx.read(|cx| { - workspace - .read(cx) - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }); - assert!( - active_item.is_none(), - "Expected no search panel to be active" - ); - - window - .update(cx, move |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 1); - workspace.panes()[0].update(cx, move |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); - }) - .unwrap(); - - let a_dir_entry = cx.update(|cx| { - workspace - .read(cx) - .project() - .read(cx) - .entry_for_path(&(worktree_id, rel_path("a")).into(), cx) - .expect("no entry for /a/ directory") - .clone() - }); - assert!(a_dir_entry.is_dir()); - window - .update(cx, |workspace, window, cx| { - ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry.path, window, cx) - }) - .unwrap(); - - let Some(search_view) = cx.read(|cx| { - workspace - .read(cx) - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }) else { - panic!("Search view expected to appear after new search in directory event trigger") - }; - cx.background_executor.run_until_parked(); - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert!( - search_view.query_editor.focus_handle(cx).is_focused(window), - "On new search in directory, focus should be moved into query editor" - ); - search_view.excluded_files_editor.update(cx, |editor, cx| { - assert!( - editor.display_text(cx).is_empty(), - "New search in directory should not have any excluded files" - ); - }); - search_view.included_files_editor.update(cx, |editor, cx| { - assert_eq!( - editor.display_text(cx), - a_dir_entry.path.display(PathStyle::local()), - "New search in directory should have included dir entry path" - ); - }); - }); - }) - .unwrap(); - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("const", window, cx) - }); - search_view.search(cx); - }); - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst ONE: usize = 1;\n\n\nconst TWO: usize = one::ONE + one::ONE;", - "New search in directory should have a filter that matches a certain directory" - ); - }) - }) - .unwrap(); - } - - #[perf] - #[gpui::test] - async fn test_search_query_history(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); - let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); - - window - .update(cx, { - let search_bar = search_bar.clone(); - |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 1); - workspace.panes()[0].update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); - - ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) - } - }) - .unwrap(); - - let search_view = cx.read(|cx| { - workspace - .read(cx) - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - .expect("Search view expected to appear after new search event trigger") - }); - - // Add 3 search items into the history + another unsubmitted one. - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.search_options = SearchOptions::CASE_SENSITIVE; - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("ONE", window, cx) - }); - search_view.search(cx); - }); - }) - .unwrap(); - - cx.background_executor.run_until_parked(); - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("TWO", window, cx) - }); - search_view.search(cx); - }); - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("THREE", window, cx) - }); - search_view.search(cx); - }) - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("JUST_TEXT_INPUT", window, cx) - }); - }) - }) - .unwrap(); - cx.background_executor.run_until_parked(); - - // Ensure that the latest input with search settings is active. - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!( - search_view.query_editor.read(cx).text(cx), - "JUST_TEXT_INPUT" - ); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - // Next history query after the latest should set the query to the empty string. - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }) - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), ""); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }) - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), ""); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - // First previous query for empty current query should set the query to the latest submitted one. - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - // Further previous items should go over the history in reverse order. - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - // Previous items should never go behind the first history item. - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - // Next items should go over the history in the original order. - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("TWO_NEW", window, cx) - }); - search_view.search(cx); - }); - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - // New search input should add another entry to history and move the selection to the end of the history. - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), ""); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - } - - #[perf] - #[gpui::test] - async fn test_search_query_history_with_multiple_views(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - json!({ - "one.rs": "const ONE: usize = 1;", - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let worktree_id = project.update(cx, |this, cx| { - this.worktrees(cx).next().unwrap().read(cx).id() - }); - - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); - - let panes: Vec<_> = window - .update(cx, |this, _, _| this.panes().to_owned()) - .unwrap(); - - let search_bar_1 = window.build_entity(cx, |_, _| ProjectSearchBar::new()); - let search_bar_2 = window.build_entity(cx, |_, _| ProjectSearchBar::new()); - - assert_eq!(panes.len(), 1); - let first_pane = panes.first().cloned().unwrap(); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 0); - window - .update(cx, |workspace, window, cx| { - workspace.open_path( - (worktree_id, rel_path("one.rs")), - Some(first_pane.downgrade()), - true, - window, - cx, - ) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1); - - // Add a project search item to the first pane - window - .update(cx, { - let search_bar = search_bar_1.clone(); - |workspace, window, cx| { - first_pane.update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); - - ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) - } - }) - .unwrap(); - let search_view_1 = cx.read(|cx| { - workspace - .read(cx) - .active_item(cx) - .and_then(|item| item.downcast::()) - .expect("Search view expected to appear after new search event trigger") - }); - - let second_pane = window - .update(cx, |workspace, window, cx| { - workspace.split_and_clone( - first_pane.clone(), - workspace::SplitDirection::Right, - window, - cx, - ) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1); - - assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 2); - - // Add a project search item to the second pane - window - .update(cx, { - let search_bar = search_bar_2.clone(); - let pane = second_pane.clone(); - move |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 2); - pane.update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); - - ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) - } - }) - .unwrap(); - - let search_view_2 = cx.read(|cx| { - workspace - .read(cx) - .active_item(cx) - .and_then(|item| item.downcast::()) - .expect("Search view expected to appear after new search event trigger") - }); - - cx.run_until_parked(); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 2); - assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 2); - - let update_search_view = - |search_view: &Entity, query: &str, cx: &mut TestAppContext| { - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text(query, window, cx) - }); - search_view.search(cx); - }); - }) - .unwrap(); - }; - - let active_query = - |search_view: &Entity, cx: &mut TestAppContext| -> String { - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.read(cx).text(cx) - }) - }) - .unwrap() - }; - - let select_prev_history_item = - |search_bar: &Entity, cx: &mut TestAppContext| { - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }) - }) - .unwrap(); - }; - - let select_next_history_item = - |search_bar: &Entity, cx: &mut TestAppContext| { - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }) - }) - .unwrap(); - }; - - update_search_view(&search_view_1, "ONE", cx); - cx.background_executor.run_until_parked(); - - update_search_view(&search_view_2, "TWO", cx); - cx.background_executor.run_until_parked(); - - assert_eq!(active_query(&search_view_1, cx), "ONE"); - assert_eq!(active_query(&search_view_2, cx), "TWO"); - - // Selecting previous history item should select the query from search view 1. - select_prev_history_item(&search_bar_2, cx); - assert_eq!(active_query(&search_view_2, cx), "ONE"); - - // Selecting the previous history item should not change the query as it is already the first item. - select_prev_history_item(&search_bar_2, cx); - assert_eq!(active_query(&search_view_2, cx), "ONE"); - - // Changing the query in search view 2 should not affect the history of search view 1. - assert_eq!(active_query(&search_view_1, cx), "ONE"); - - // Deploying a new search in search view 2 - update_search_view(&search_view_2, "THREE", cx); - cx.background_executor.run_until_parked(); - - select_next_history_item(&search_bar_2, cx); - assert_eq!(active_query(&search_view_2, cx), ""); - - select_prev_history_item(&search_bar_2, cx); - assert_eq!(active_query(&search_view_2, cx), "THREE"); - - select_prev_history_item(&search_bar_2, cx); - assert_eq!(active_query(&search_view_2, cx), "TWO"); - - select_prev_history_item(&search_bar_2, cx); - assert_eq!(active_query(&search_view_2, cx), "ONE"); - - select_prev_history_item(&search_bar_2, cx); - assert_eq!(active_query(&search_view_2, cx), "ONE"); - - // Search view 1 should now see the query from search view 2. - assert_eq!(active_query(&search_view_1, cx), "ONE"); - - select_next_history_item(&search_bar_2, cx); - assert_eq!(active_query(&search_view_2, cx), "TWO"); - - // Here is the new query from search view 2 - select_next_history_item(&search_bar_2, cx); - assert_eq!(active_query(&search_view_2, cx), "THREE"); - - select_next_history_item(&search_bar_2, cx); - assert_eq!(active_query(&search_view_2, cx), ""); - - select_next_history_item(&search_bar_1, cx); - assert_eq!(active_query(&search_view_1, cx), "TWO"); - - select_next_history_item(&search_bar_1, cx); - assert_eq!(active_query(&search_view_1, cx), "THREE"); - - select_next_history_item(&search_bar_1, cx); - assert_eq!(active_query(&search_view_1, cx), ""); - } - - #[perf] - #[gpui::test] - async fn test_deploy_search_with_multiple_panes(cx: &mut TestAppContext) { - init_test(cx); - - // Setup 2 panes, both with a file open and one with a project search. - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - json!({ - "one.rs": "const ONE: usize = 1;", - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let worktree_id = project.update(cx, |this, cx| { - this.worktrees(cx).next().unwrap().read(cx).id() - }); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let panes: Vec<_> = window - .update(cx, |this, _, _| this.panes().to_owned()) - .unwrap(); - assert_eq!(panes.len(), 1); - let first_pane = panes.first().cloned().unwrap(); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 0); - window - .update(cx, |workspace, window, cx| { - workspace.open_path( - (worktree_id, rel_path("one.rs")), - Some(first_pane.downgrade()), - true, - window, - cx, - ) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1); - let second_pane = window - .update(cx, |workspace, window, cx| { - workspace.split_and_clone( - first_pane.clone(), - workspace::SplitDirection::Right, - window, - cx, - ) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1); - assert!( - window - .update(cx, |_, window, cx| second_pane - .focus_handle(cx) - .contains_focused(window, cx)) - .unwrap() - ); - let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); - window - .update(cx, { - let search_bar = search_bar.clone(); - let pane = first_pane.clone(); - move |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 2); - pane.update(cx, move |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); - } - }) - .unwrap(); - - // Add a project search item to the second pane - window - .update(cx, { - |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 2); - second_pane.update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); - - ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) - } - }) - .unwrap(); - - cx.run_until_parked(); - assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 2); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1); - - // Focus the first pane - window - .update(cx, |workspace, window, cx| { - assert_eq!(workspace.active_pane(), &second_pane); - second_pane.update(cx, |this, cx| { - assert_eq!(this.active_item_index(), 1); - this.activate_previous_item(&Default::default(), window, cx); - assert_eq!(this.active_item_index(), 0); - }); - workspace.activate_pane_in_direction(workspace::SplitDirection::Left, window, cx); - }) - .unwrap(); - window - .update(cx, |workspace, _, cx| { - assert_eq!(workspace.active_pane(), &first_pane); - assert_eq!(first_pane.read(cx).items_len(), 1); - assert_eq!(second_pane.read(cx).items_len(), 2); - }) - .unwrap(); - - // Deploy a new search - cx.dispatch_action(window.into(), DeploySearch::find()); - - // Both panes should now have a project search in them - window - .update(cx, |workspace, window, cx| { - assert_eq!(workspace.active_pane(), &first_pane); - first_pane.read_with(cx, |this, _| { - assert_eq!(this.active_item_index(), 1); - assert_eq!(this.items_len(), 2); - }); - second_pane.update(cx, |this, cx| { - assert!(!cx.focus_handle().contains_focused(window, cx)); - assert_eq!(this.items_len(), 2); - }); - }) - .unwrap(); - - // Focus the second pane's non-search item - window - .update(cx, |_workspace, window, cx| { - second_pane.update(cx, |pane, cx| { - pane.activate_next_item(&Default::default(), window, cx) - }); - }) - .unwrap(); - - // Deploy a new search - cx.dispatch_action(window.into(), DeploySearch::find()); - - // The project search view should now be focused in the second pane - // And the number of items should be unchanged. - window - .update(cx, |_workspace, _, cx| { - second_pane.update(cx, |pane, _cx| { - assert!( - pane.active_item() - .unwrap() - .downcast::() - .is_some() - ); - - assert_eq!(pane.items_len(), 2); - }); - }) - .unwrap(); - } - - #[perf] - #[gpui::test] - async fn test_scroll_search_results_to_top(cx: &mut TestAppContext) { - init_test(cx); - - // We need many lines in the search results to be able to scroll the window - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - json!({ - "1.txt": "\n\n\n\n\n A \n\n\n\n\n", - "2.txt": "\n\n\n\n\n A \n\n\n\n\n", - "3.rs": "\n\n\n\n\n A \n\n\n\n\n", - "4.rs": "\n\n\n\n\n A \n\n\n\n\n", - "5.rs": "\n\n\n\n\n A \n\n\n\n\n", - "6.rs": "\n\n\n\n\n A \n\n\n\n\n", - "7.rs": "\n\n\n\n\n A \n\n\n\n\n", - "8.rs": "\n\n\n\n\n A \n\n\n\n\n", - "9.rs": "\n\n\n\n\n A \n\n\n\n\n", - "a.rs": "\n\n\n\n\n A \n\n\n\n\n", - "b.rs": "\n\n\n\n\n B \n\n\n\n\n", - "c.rs": "\n\n\n\n\n B \n\n\n\n\n", - "d.rs": "\n\n\n\n\n B \n\n\n\n\n", - "e.rs": "\n\n\n\n\n B \n\n\n\n\n", - "f.rs": "\n\n\n\n\n B \n\n\n\n\n", - "g.rs": "\n\n\n\n\n B \n\n\n\n\n", - "h.rs": "\n\n\n\n\n B \n\n\n\n\n", - "i.rs": "\n\n\n\n\n B \n\n\n\n\n", - "j.rs": "\n\n\n\n\n B \n\n\n\n\n", - "k.rs": "\n\n\n\n\n B \n\n\n\n\n", - }), - ) - .await; - let project = Project::test(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 search = cx.new(|cx| ProjectSearch::new(project, cx)); - let search_view = cx.add_window(|window, cx| { - ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None) - }); - - // First search - perform_search(search_view, "A", cx); - search_view - .update(cx, |search_view, window, cx| { - search_view.results_editor.update(cx, |results_editor, cx| { - // Results are correct and scrolled to the top - assert_eq!( - results_editor.display_text(cx).match_indices(" A ").count(), - 10 - ); - assert_eq!(results_editor.scroll_position(cx), Point::default()); - - // Scroll results all the way down - results_editor.scroll( - Point::new(0., f64::MAX), - Some(Axis::Vertical), - window, - cx, - ); - }); - }) - .expect("unable to update search view"); - - // Second search - perform_search(search_view, "B", cx); - search_view - .update(cx, |search_view, _, cx| { - search_view.results_editor.update(cx, |results_editor, cx| { - // Results are correct... - assert_eq!( - results_editor.display_text(cx).match_indices(" B ").count(), - 10 - ); - // ...and scrolled back to the top - assert_eq!(results_editor.scroll_position(cx), Point::default()); - }); - }) - .expect("unable to update search view"); - } - - #[perf] - #[gpui::test] - async fn test_buffer_search_query_reused(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - json!({ - "one.rs": "const ONE: usize = 1;", - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let worktree_id = project.update(cx, |this, cx| { - this.worktrees(cx).next().unwrap().read(cx).id() - }); - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); - let mut cx = VisualTestContext::from_window(*window.deref(), cx); - - let editor = workspace - .update_in(&mut cx, |workspace, window, cx| { - workspace.open_path((worktree_id, rel_path("one.rs")), None, true, window, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - // Wait for the unstaged changes to be loaded - cx.run_until_parked(); - - let buffer_search_bar = cx.new_window_entity(|window, cx| { - let mut search_bar = - BufferSearchBar::new(Some(project.read(cx).languages().clone()), window, cx); - search_bar.set_active_pane_item(Some(&editor), window, cx); - search_bar.show(window, cx); - search_bar - }); - - let panes: Vec<_> = window - .update(&mut cx, |this, _, _| this.panes().to_owned()) - .unwrap(); - assert_eq!(panes.len(), 1); - let pane = panes.first().cloned().unwrap(); - pane.update_in(&mut cx, |pane, window, cx| { - pane.toolbar().update(cx, |toolbar, cx| { - toolbar.add_item(buffer_search_bar.clone(), window, cx); - }) - }); - - let buffer_search_query = "search bar query"; - buffer_search_bar - .update_in(&mut cx, |buffer_search_bar, window, cx| { - buffer_search_bar.focus_handle(cx).focus(window); - buffer_search_bar.search(buffer_search_query, None, true, window, cx) - }) - .await - .unwrap(); - - workspace.update_in(&mut cx, |workspace, window, cx| { - ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) - }); - cx.run_until_parked(); - let project_search_view = pane - .read_with(&cx, |pane, _| { - pane.active_item() - .and_then(|item| item.downcast::()) - }) - .expect("should open a project search view after spawning a new search"); - project_search_view.update(&mut cx, |search_view, cx| { - assert_eq!( - search_view.search_query_text(cx), - buffer_search_query, - "Project search should take the query from the buffer search bar since it got focused and had a query inside" - ); - }); - } - - #[gpui::test] - async fn test_search_dismisses_modal(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - json!({ - "one.rs": "const ONE: usize = 1;", - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - struct EmptyModalView { - focus_handle: gpui::FocusHandle, - } - impl EventEmitter for EmptyModalView {} - impl Render for EmptyModalView { - fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement { - div() - } - } - impl Focusable for EmptyModalView { - fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } - } - impl workspace::ModalView for EmptyModalView {} - - window - .update(cx, |workspace, window, cx| { - workspace.toggle_modal(window, cx, |_, cx| EmptyModalView { - focus_handle: cx.focus_handle(), - }); - assert!(workspace.has_active_modal(window, cx)); - }) - .unwrap(); - - cx.dispatch_action(window.into(), Deploy::find()); - - window - .update(cx, |workspace, window, cx| { - assert!(!workspace.has_active_modal(window, cx)); - workspace.toggle_modal(window, cx, |_, cx| EmptyModalView { - focus_handle: cx.focus_handle(), - }); - assert!(workspace.has_active_modal(window, cx)); - }) - .unwrap(); - - cx.dispatch_action(window.into(), DeploySearch::find()); - - window - .update(cx, |workspace, window, cx| { - assert!(!workspace.has_active_modal(window, cx)); - }) - .unwrap(); - } - - #[perf] - #[gpui::test] - async fn test_search_with_inlays(cx: &mut TestAppContext) { - init_test(cx); - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.all_languages.defaults.inlay_hints = - Some(InlayHintSettingsContent { - enabled: Some(true), - ..InlayHintSettingsContent::default() - }) - }); - }); - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - // `\n` , a trailing line on the end, is important for the test case - json!({ - "main.rs": "fn main() { let a = 2; }\n", - }), - ) - .await; - - let requests_count = Arc::new(AtomicUsize::new(0)); - let closure_requests_count = requests_count.clone(); - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let language_registry = project.read_with(cx, |project, _| project.languages().clone()); - let language = rust_lang(); - language_registry.add(language); - let mut fake_servers = language_registry.register_fake_lsp( - "Rust", - FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..lsp::ServerCapabilities::default() - }, - initializer: Some(Box::new(move |fake_server| { - let requests_count = closure_requests_count.clone(); - fake_server.set_request_handler::({ - move |_, _| { - let requests_count = requests_count.clone(); - async move { - requests_count.fetch_add(1, atomic::Ordering::Release); - Ok(Some(vec![lsp::InlayHint { - position: lsp::Position::new(0, 17), - label: lsp::InlayHintLabel::String(": i32".to_owned()), - kind: Some(lsp::InlayHintKind::TYPE), - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }])) - } - } - }); - })), - ..FakeLspAdapter::default() - }, - ); - - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); - let search = cx.new(|cx| ProjectSearch::new(project.clone(), cx)); - let search_view = cx.add_window(|window, cx| { - ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None) - }); - - perform_search(search_view, "let ", cx); - let fake_server = fake_servers.next().await.unwrap(); - cx.executor().advance_clock(Duration::from_secs(1)); - cx.executor().run_until_parked(); - search_view - .update(cx, |search_view, _, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nfn main() { let a: i32 = 2; }\n" - ); - }) - .unwrap(); - assert_eq!( - requests_count.load(atomic::Ordering::Acquire), - 1, - "New hints should have been queried", - ); - - // Can do the 2nd search without any panics - perform_search(search_view, "let ", cx); - cx.executor().advance_clock(Duration::from_secs(1)); - cx.executor().run_until_parked(); - search_view - .update(cx, |search_view, _, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nfn main() { let a: i32 = 2; }\n" - ); - }) - .unwrap(); - assert_eq!( - requests_count.load(atomic::Ordering::Acquire), - 2, - "We did drop the previous buffer when cleared the old project search results, hence another query was made", - ); - - let singleton_editor = window - .update(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from(path!("/dir/main.rs")), - workspace::OpenOptions::default(), - window, - cx, - ) - }) - .unwrap() - .await - .unwrap() - .downcast::() - .unwrap(); - cx.executor().advance_clock(Duration::from_millis(100)); - cx.executor().run_until_parked(); - singleton_editor.update(cx, |editor, cx| { - assert_eq!( - editor.display_text(cx), - "fn main() { let a: i32 = 2; }\n", - "Newly opened editor should have the correct text with hints", - ); - }); - assert_eq!( - requests_count.load(atomic::Ordering::Acquire), - 2, - "Opening the same buffer again should reuse the cached hints", - ); - - window - .update(cx, |_, window, cx| { - singleton_editor.update(cx, |editor, cx| { - editor.handle_input("test", window, cx); - }); - }) - .unwrap(); - - cx.executor().advance_clock(Duration::from_secs(1)); - cx.executor().run_until_parked(); - singleton_editor.update(cx, |editor, cx| { - assert_eq!( - editor.display_text(cx), - "testfn main() { l: i32et a = 2; }\n", - "Newly opened editor should have the correct text with hints", - ); - }); - assert_eq!( - requests_count.load(atomic::Ordering::Acquire), - 3, - "We have edited the buffer and should send a new request", - ); - - window - .update(cx, |_, window, cx| { - singleton_editor.update(cx, |editor, cx| { - editor.undo(&editor::actions::Undo, window, cx); - }); - }) - .unwrap(); - cx.executor().advance_clock(Duration::from_secs(1)); - cx.executor().run_until_parked(); - assert_eq!( - requests_count.load(atomic::Ordering::Acquire), - 4, - "We have edited the buffer again and should send a new request again", - ); - singleton_editor.update(cx, |editor, cx| { - assert_eq!( - editor.display_text(cx), - "fn main() { let a: i32 = 2; }\n", - "Newly opened editor should have the correct text with hints", - ); - }); - project.update(cx, |_, cx| { - cx.emit(project::Event::RefreshInlayHints { - server_id: fake_server.server.server_id(), - request_id: Some(1), - }); - }); - cx.executor().advance_clock(Duration::from_secs(1)); - cx.executor().run_until_parked(); - assert_eq!( - requests_count.load(atomic::Ordering::Acquire), - 5, - "After a simulated server refresh request, we should have sent another request", - ); - - perform_search(search_view, "let ", cx); - cx.executor().advance_clock(Duration::from_secs(1)); - cx.executor().run_until_parked(); - assert_eq!( - requests_count.load(atomic::Ordering::Acquire), - 5, - "New project search should reuse the cached hints", - ); - search_view - .update(cx, |search_view, _, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nfn main() { let a: i32 = 2; }\n" - ); - }) - .unwrap(); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings = SettingsStore::test(cx); - cx.set_global(settings); - - theme::init(theme::LoadThemes::JustBase, cx); - - editor::init(cx); - crate::init(cx); - }); - } - - fn perform_search( - search_view: WindowHandle, - text: impl Into>, - cx: &mut TestAppContext, - ) { - search_view - .update(cx, |search_view, window, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text(text, window, cx) - }); - search_view.search(cx); - }) - .unwrap(); - // Ensure editor highlights appear after the search is done - cx.executor().advance_clock( - editor::SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT + Duration::from_millis(100), - ); - cx.background_executor.run_until_parked(); - } -} diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs deleted file mode 100644 index 6663f8c318..0000000000 --- a/crates/search/src/search.rs +++ /dev/null @@ -1,204 +0,0 @@ -use bitflags::bitflags; -pub use buffer_search::BufferSearchBar; -use editor::SearchSettings; -use gpui::{Action, App, ClickEvent, FocusHandle, IntoElement, actions}; -use project::search::SearchQuery; -pub use project_search::ProjectSearchView; -use ui::{ButtonStyle, IconButton, IconButtonShape}; -use ui::{Tooltip, prelude::*}; -use workspace::notifications::NotificationId; -use workspace::{Toast, Workspace}; - -pub use search_status_button::SEARCH_ICON; - -use crate::project_search::ProjectSearchBar; - -pub mod buffer_search; -pub mod project_search; -pub(crate) mod search_bar; -pub mod search_status_button; - -pub fn init(cx: &mut App) { - menu::init(); - buffer_search::init(cx); - project_search::init(cx); -} - -actions!( - search, - [ - /// Focuses on the search input field. - FocusSearch, - /// Toggles whole word matching. - ToggleWholeWord, - /// Toggles case-sensitive search. - ToggleCaseSensitive, - /// Toggles searching in ignored files. - ToggleIncludeIgnored, - /// Toggles regular expression mode. - ToggleRegex, - /// Toggles the replace interface. - ToggleReplace, - /// Toggles searching within selection only. - ToggleSelection, - /// Selects the next search match. - SelectNextMatch, - /// Selects the previous search match. - SelectPreviousMatch, - /// Selects all search matches. - SelectAllMatches, - /// Cycles through search modes. - CycleMode, - /// Navigates to the next query in search history. - NextHistoryQuery, - /// Navigates to the previous query in search history. - PreviousHistoryQuery, - /// Replaces all matches. - ReplaceAll, - /// Replaces the next match. - ReplaceNext, - ] -); - -bitflags! { - #[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] - pub struct SearchOptions: u8 { - const NONE = 0; - const WHOLE_WORD = 1 << SearchOption::WholeWord as u8; - const CASE_SENSITIVE = 1 << SearchOption::CaseSensitive as u8; - const INCLUDE_IGNORED = 1 << SearchOption::IncludeIgnored as u8; - const REGEX = 1 << SearchOption::Regex as u8; - const ONE_MATCH_PER_LINE = 1 << SearchOption::OneMatchPerLine as u8; - /// If set, reverse direction when finding the active match - const BACKWARDS = 1 << SearchOption::Backwards as u8; - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u8)] -pub enum SearchOption { - WholeWord = 0, - CaseSensitive, - IncludeIgnored, - Regex, - OneMatchPerLine, - Backwards, -} - -pub(crate) enum SearchSource<'a, 'b> { - Buffer, - Project(&'a Context<'b, ProjectSearchBar>), -} - -impl SearchOption { - pub fn as_options(&self) -> SearchOptions { - SearchOptions::from_bits(1 << *self as u8).unwrap() - } - - pub fn label(&self) -> &'static str { - match self { - SearchOption::WholeWord => "Match Whole Words", - SearchOption::CaseSensitive => "Match Case Sensitivity", - SearchOption::IncludeIgnored => "Also search files ignored by configuration", - SearchOption::Regex => "Use Regular Expressions", - SearchOption::OneMatchPerLine => "One Match Per Line", - SearchOption::Backwards => "Search Backwards", - } - } - - pub fn icon(&self) -> ui::IconName { - match self { - SearchOption::WholeWord => ui::IconName::WholeWord, - SearchOption::CaseSensitive => ui::IconName::CaseSensitive, - SearchOption::IncludeIgnored => ui::IconName::Sliders, - SearchOption::Regex => ui::IconName::Regex, - _ => panic!("{self:?} is not a named SearchOption"), - } - } - - pub fn to_toggle_action(self) -> &'static dyn Action { - match self { - SearchOption::WholeWord => &ToggleWholeWord, - SearchOption::CaseSensitive => &ToggleCaseSensitive, - SearchOption::IncludeIgnored => &ToggleIncludeIgnored, - SearchOption::Regex => &ToggleRegex, - _ => panic!("{self:?} is not a toggle action"), - } - } - - pub(crate) fn as_button( - &self, - active: SearchOptions, - search_source: SearchSource, - focus_handle: FocusHandle, - ) -> impl IntoElement { - let action = self.to_toggle_action(); - let label = self.label(); - IconButton::new( - (label, matches!(search_source, SearchSource::Buffer) as u32), - self.icon(), - ) - .map(|button| match search_source { - SearchSource::Buffer => { - let focus_handle = focus_handle.clone(); - button.on_click(move |_: &ClickEvent, window, cx| { - if !focus_handle.is_focused(window) { - window.focus(&focus_handle); - } - window.dispatch_action(action.boxed_clone(), cx); - }) - } - SearchSource::Project(cx) => { - let options = self.as_options(); - button.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| { - this.toggle_search_option(options, window, cx); - })) - } - }) - .style(ButtonStyle::Subtle) - .shape(IconButtonShape::Square) - .toggle_state(active.contains(self.as_options())) - .tooltip(move |_window, cx| Tooltip::for_action_in(label, action, &focus_handle, cx)) - } -} - -impl SearchOptions { - pub fn none() -> SearchOptions { - SearchOptions::NONE - } - - pub fn from_query(query: &SearchQuery) -> SearchOptions { - let mut options = SearchOptions::NONE; - options.set(SearchOptions::WHOLE_WORD, query.whole_word()); - options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive()); - options.set(SearchOptions::INCLUDE_IGNORED, query.include_ignored()); - options.set(SearchOptions::REGEX, query.is_regex()); - options - } - - pub fn from_settings(settings: &SearchSettings) -> SearchOptions { - let mut options = SearchOptions::NONE; - options.set(SearchOptions::WHOLE_WORD, settings.whole_word); - options.set(SearchOptions::CASE_SENSITIVE, settings.case_sensitive); - options.set(SearchOptions::INCLUDE_IGNORED, settings.include_ignored); - options.set(SearchOptions::REGEX, settings.regex); - options - } -} - -pub(crate) fn show_no_more_matches(window: &mut Window, cx: &mut App) { - window.defer(cx, |window, cx| { - struct NotifType(); - let notification_id = NotificationId::unique::(); - - let Some(workspace) = window.root::().flatten() else { - return; - }; - workspace.update(cx, |workspace, cx| { - workspace.show_toast( - Toast::new(notification_id.clone(), "No more matches").autohide(), - cx, - ); - }) - }); -} diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs deleted file mode 100644 index 13b4df9574..0000000000 --- a/crates/search/src/search_bar.rs +++ /dev/null @@ -1,91 +0,0 @@ -use editor::{Editor, EditorElement, EditorStyle}; -use gpui::{Action, Entity, FocusHandle, Hsla, IntoElement, TextStyle}; -use settings::Settings; -use theme::ThemeSettings; -use ui::{IconButton, IconButtonShape}; -use ui::{Tooltip, prelude::*}; - -pub(super) enum ActionButtonState { - Disabled, - Toggled, -} - -pub(super) fn render_action_button( - id_prefix: &'static str, - icon: ui::IconName, - button_state: Option, - tooltip: &'static str, - action: &'static dyn Action, - focus_handle: FocusHandle, -) -> impl IntoElement { - IconButton::new( - SharedString::from(format!("{id_prefix}-{}", action.name())), - icon, - ) - .shape(IconButtonShape::Square) - .on_click({ - let focus_handle = focus_handle.clone(); - move |_, window, cx| { - if !focus_handle.is_focused(window) { - window.focus(&focus_handle); - } - window.dispatch_action(action.boxed_clone(), cx); - } - }) - .tooltip(move |_window, cx| Tooltip::for_action_in(tooltip, action, &focus_handle, cx)) - .when_some(button_state, |this, state| match state { - ActionButtonState::Toggled => this.toggle_state(true), - ActionButtonState::Disabled => this.disabled(true), - }) -} - -pub(crate) fn input_base_styles(border_color: Hsla, map: impl FnOnce(Div) -> Div) -> Div { - h_flex() - .map(map) - .min_w_32() - .h_8() - .pl_2() - .pr_1() - .border_1() - .border_color(border_color) - .rounded_md() -} - -pub(crate) fn render_text_input( - editor: &Entity, - color_override: Option, - app: &App, -) -> impl IntoElement { - let (color, use_syntax) = if editor.read(app).read_only(app) { - (app.theme().colors().text_disabled, false) - } else { - match color_override { - Some(color_override) => (color_override.color(app), false), - None => (app.theme().colors().text, true), - } - }; - - let settings = ThemeSettings::get_global(app); - let text_style = TextStyle { - color, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: rems(0.875).into(), - font_weight: settings.buffer_font.weight, - line_height: relative(1.3), - ..TextStyle::default() - }; - - let mut editor_style = EditorStyle { - background: app.theme().colors().toolbar_background, - local_player: app.theme().players().local(), - text: text_style, - ..EditorStyle::default() - }; - if use_syntax { - editor_style.syntax = app.theme().syntax().clone(); - } - - EditorElement::new(editor, editor_style) -} diff --git a/crates/search/src/search_status_button.rs b/crates/search/src/search_status_button.rs deleted file mode 100644 index 712a322c10..0000000000 --- a/crates/search/src/search_status_button.rs +++ /dev/null @@ -1,45 +0,0 @@ -use editor::EditorSettings; -use settings::Settings as _; -use ui::{ButtonCommon, Clickable, Context, Render, Tooltip, Window, prelude::*}; -use workspace::{ItemHandle, StatusItemView}; - -pub const SEARCH_ICON: IconName = IconName::MagnifyingGlass; - -pub struct SearchButton; - -impl SearchButton { - pub fn new() -> Self { - Self {} - } -} - -impl Render for SearchButton { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { - let button = div(); - - if !EditorSettings::get_global(cx).search.button { - return button.hidden(); - } - - button.child( - IconButton::new("project-search-indicator", SEARCH_ICON) - .icon_size(IconSize::Small) - .tooltip(|_window, cx| { - Tooltip::for_action("Project Search", &workspace::DeploySearch::default(), cx) - }) - .on_click(cx.listener(|_this, _, window, cx| { - window.dispatch_action(Box::new(workspace::DeploySearch::default()), cx); - })), - ) - } -} - -impl StatusItemView for SearchButton { - fn set_active_pane_item( - &mut self, - _active_pane_item: Option<&dyn ItemHandle>, - _window: &mut Window, - _cx: &mut Context, - ) { - } -} diff --git a/crates/session/Cargo.toml b/crates/session/Cargo.toml deleted file mode 100644 index 15c3acb8f0..0000000000 --- a/crates/session/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "session" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/session.rs" -doctest = false - -[features] -test-support = [ - "db/test-support", -] - -[dependencies] -db.workspace = true -gpui.workspace = true -uuid.workspace = true -util.workspace = true -serde_json.workspace = true diff --git a/crates/session/LICENSE-GPL b/crates/session/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/session/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/session/src/session.rs b/crates/session/src/session.rs deleted file mode 100644 index fd45982bf4..0000000000 --- a/crates/session/src/session.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::time::Duration; - -use db::kvp::KEY_VALUE_STORE; -use gpui::{App, AppContext as _, Context, Subscription, Task, WindowId}; -use util::ResultExt; - -pub struct Session { - session_id: String, - old_session_id: Option, - old_window_ids: Option>, -} - -const SESSION_ID_KEY: &str = "session_id"; -const SESSION_WINDOW_STACK_KEY: &str = "session_window_stack"; - -impl Session { - pub async fn new(session_id: String) -> Self { - let old_session_id = KEY_VALUE_STORE.read_kvp(SESSION_ID_KEY).ok().flatten(); - - KEY_VALUE_STORE - .write_kvp(SESSION_ID_KEY.to_string(), session_id.clone()) - .await - .log_err(); - - let old_window_ids = KEY_VALUE_STORE - .read_kvp(SESSION_WINDOW_STACK_KEY) - .ok() - .flatten() - .and_then(|json| serde_json::from_str::>(&json).ok()) - .map(|vec| { - vec.into_iter() - .map(WindowId::from) - .collect::>() - }); - - Self { - session_id, - old_session_id, - old_window_ids, - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn test() -> Self { - Self { - session_id: uuid::Uuid::new_v4().to_string(), - old_session_id: None, - old_window_ids: None, - } - } - - pub fn id(&self) -> &str { - &self.session_id - } -} - -pub struct AppSession { - session: Session, - _serialization_task: Task<()>, - _subscriptions: Vec, -} - -impl AppSession { - pub fn new(session: Session, cx: &Context) -> Self { - let _subscriptions = vec![cx.on_app_quit(Self::app_will_quit)]; - - let _serialization_task = cx.spawn(async move |_, cx| { - let mut current_window_stack = Vec::new(); - loop { - if let Some(windows) = cx.update(|cx| window_stack(cx)).ok().flatten() - && windows != current_window_stack - { - store_window_stack(&windows).await; - current_window_stack = windows; - } - - cx.background_executor() - .timer(Duration::from_millis(500)) - .await; - } - }); - - Self { - session, - _subscriptions, - _serialization_task, - } - } - - fn app_will_quit(&mut self, cx: &mut Context) -> Task<()> { - if let Some(window_stack) = window_stack(cx) { - cx.background_spawn(async move { store_window_stack(&window_stack).await }) - } else { - Task::ready(()) - } - } - - pub fn id(&self) -> &str { - self.session.id() - } - - pub fn last_session_id(&self) -> Option<&str> { - self.session.old_session_id.as_deref() - } - - pub fn last_session_window_stack(&self) -> Option> { - self.session.old_window_ids.clone() - } -} - -fn window_stack(cx: &App) -> Option> { - Some( - cx.window_stack()? - .into_iter() - .map(|window| window.window_id().as_u64()) - .collect(), - ) -} - -async fn store_window_stack(windows: &[u64]) { - if let Ok(window_ids_json) = serde_json::to_string(windows) { - KEY_VALUE_STORE - .write_kvp(SESSION_WINDOW_STACK_KEY.to_string(), window_ids_json) - .await - .log_err(); - } -} diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml deleted file mode 100644 index 1f1513d621..0000000000 --- a/crates/settings/Cargo.toml +++ /dev/null @@ -1,50 +0,0 @@ -[package] -name = "settings" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/settings.rs" -doctest = false - -[features] -test-support = ["gpui/test-support", "fs/test-support"] - -[dependencies] -anyhow.workspace = true -collections.workspace = true -derive_more.workspace = true -ec4rs.workspace = true -fs.workspace = true -futures.workspace = true -gpui.workspace = true -inventory.workspace = true -log.workspace = true -migrator.workspace = true -paths.workspace = true -release_channel.workspace = true -rust-embed.workspace = true -schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_json_lenient.workspace = true -serde_repr.workspace = true -settings_json.workspace = true -settings_macros.workspace = true -smallvec.workspace = true -strum.workspace = true -util.workspace = true -zlog.workspace = true - -[dev-dependencies] -fs = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } - -indoc.workspace = true -pretty_assertions.workspace = true -unindent.workspace = true diff --git a/crates/settings/LICENSE-GPL b/crates/settings/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/settings/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/settings/src/base_keymap_setting.rs b/crates/settings/src/base_keymap_setting.rs deleted file mode 100644 index 8e872dae40..0000000000 --- a/crates/settings/src/base_keymap_setting.rs +++ /dev/null @@ -1,135 +0,0 @@ -use std::fmt::{Display, Formatter}; - -use crate::{self as settings, settings_content::BaseKeymapContent}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{RegisterSetting, Settings}; - -/// Base key bindings scheme. Base keymaps can be overridden with user keymaps. -/// -/// Default: VSCode -#[derive( - Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default, RegisterSetting, -)] -pub enum BaseKeymap { - #[default] - VSCode, - JetBrains, - SublimeText, - Atom, - TextMate, - Emacs, - Cursor, - None, -} - -impl From for BaseKeymap { - fn from(value: BaseKeymapContent) -> Self { - match value { - BaseKeymapContent::VSCode => Self::VSCode, - BaseKeymapContent::JetBrains => Self::JetBrains, - BaseKeymapContent::SublimeText => Self::SublimeText, - BaseKeymapContent::Atom => Self::Atom, - BaseKeymapContent::TextMate => Self::TextMate, - BaseKeymapContent::Emacs => Self::Emacs, - BaseKeymapContent::Cursor => Self::Cursor, - BaseKeymapContent::None => Self::None, - } - } -} -impl Into for BaseKeymap { - fn into(self) -> BaseKeymapContent { - match self { - BaseKeymap::VSCode => BaseKeymapContent::VSCode, - BaseKeymap::JetBrains => BaseKeymapContent::JetBrains, - BaseKeymap::SublimeText => BaseKeymapContent::SublimeText, - BaseKeymap::Atom => BaseKeymapContent::Atom, - BaseKeymap::TextMate => BaseKeymapContent::TextMate, - BaseKeymap::Emacs => BaseKeymapContent::Emacs, - BaseKeymap::Cursor => BaseKeymapContent::Cursor, - BaseKeymap::None => BaseKeymapContent::None, - } - } -} - -impl Display for BaseKeymap { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - BaseKeymap::VSCode => write!(f, "VS Code"), - BaseKeymap::JetBrains => write!(f, "JetBrains"), - BaseKeymap::SublimeText => write!(f, "Sublime Text"), - BaseKeymap::Atom => write!(f, "Atom"), - BaseKeymap::TextMate => write!(f, "TextMate"), - BaseKeymap::Emacs => write!(f, "Emacs (beta)"), - BaseKeymap::Cursor => write!(f, "Cursor (beta)"), - BaseKeymap::None => write!(f, "None"), - } - } -} - -impl BaseKeymap { - #[cfg(target_os = "macos")] - pub const OPTIONS: [(&'static str, Self); 7] = [ - ("VS Code (Default)", Self::VSCode), - ("Atom", Self::Atom), - ("JetBrains", Self::JetBrains), - ("Sublime Text", Self::SublimeText), - ("Emacs (beta)", Self::Emacs), - ("TextMate", Self::TextMate), - ("Cursor", Self::Cursor), - ]; - - #[cfg(not(target_os = "macos"))] - pub const OPTIONS: [(&'static str, Self); 6] = [ - ("VS Code (Default)", Self::VSCode), - ("Atom", Self::Atom), - ("JetBrains", Self::JetBrains), - ("Sublime Text", Self::SublimeText), - ("Emacs (beta)", Self::Emacs), - ("Cursor", Self::Cursor), - ]; - - pub fn asset_path(&self) -> Option<&'static str> { - #[cfg(target_os = "macos")] - match self { - BaseKeymap::JetBrains => Some("keymaps/macos/jetbrains.json"), - BaseKeymap::SublimeText => Some("keymaps/macos/sublime_text.json"), - BaseKeymap::Atom => Some("keymaps/macos/atom.json"), - BaseKeymap::TextMate => Some("keymaps/macos/textmate.json"), - BaseKeymap::Emacs => Some("keymaps/macos/emacs.json"), - BaseKeymap::Cursor => Some("keymaps/macos/cursor.json"), - BaseKeymap::VSCode => None, - BaseKeymap::None => None, - } - - #[cfg(not(target_os = "macos"))] - match self { - BaseKeymap::JetBrains => Some("keymaps/linux/jetbrains.json"), - BaseKeymap::SublimeText => Some("keymaps/linux/sublime_text.json"), - BaseKeymap::Atom => Some("keymaps/linux/atom.json"), - BaseKeymap::Emacs => Some("keymaps/linux/emacs.json"), - BaseKeymap::Cursor => Some("keymaps/linux/cursor.json"), - BaseKeymap::TextMate => None, - BaseKeymap::VSCode => None, - BaseKeymap::None => None, - } - } - - pub fn names() -> impl Iterator { - Self::OPTIONS.iter().map(|(name, _)| *name) - } - - pub fn from_names(option: &str) -> BaseKeymap { - Self::OPTIONS - .iter() - .copied() - .find_map(|(name, value)| (name == option).then_some(value)) - .unwrap_or_default() - } -} - -impl Settings for BaseKeymap { - fn from_settings(s: &crate::settings_content::SettingsContent) -> Self { - s.base_keymap.unwrap().into() - } -} diff --git a/crates/settings/src/editable_setting_control.rs b/crates/settings/src/editable_setting_control.rs deleted file mode 100644 index fd9d986962..0000000000 --- a/crates/settings/src/editable_setting_control.rs +++ /dev/null @@ -1,30 +0,0 @@ -use fs::Fs; -use gpui::{App, RenderOnce, SharedString}; - -use crate::{settings_content::SettingsContent, update_settings_file}; - -/// A UI control that can be used to edit a setting. -pub trait EditableSettingControl: RenderOnce { - /// The type of the setting value. - type Value: Send; - - /// Returns the name of this setting. - fn name(&self) -> SharedString; - - /// Reads the setting value from the settings. - fn read(cx: &App) -> Self::Value; - - /// Applies the given setting file to the settings file contents. - /// - /// This will be called when writing the setting value back to the settings file. - fn apply(settings: &mut SettingsContent, value: Self::Value, cx: &App); - - /// Writes the given setting value to the settings files. - fn write(value: Self::Value, cx: &App) { - let fs = ::global(cx); - - update_settings_file(fs, cx, move |settings, cx| { - Self::apply(settings, value, cx); - }); - } -} diff --git a/crates/settings/src/fallible_options.rs b/crates/settings/src/fallible_options.rs deleted file mode 100644 index e0eea451f1..0000000000 --- a/crates/settings/src/fallible_options.rs +++ /dev/null @@ -1,112 +0,0 @@ -use std::cell::RefCell; - -use serde::Deserialize; - -use crate::ParseStatus; - -thread_local! { - static ERRORS: RefCell>> = const { RefCell::new(None) }; -} - -pub(crate) fn parse_json<'de, T>(json: &'de str) -> (Option, ParseStatus) -where - T: Deserialize<'de>, -{ - ERRORS.with_borrow_mut(|errors| { - errors.replace(Vec::default()); - }); - - let mut deserializer = serde_json_lenient::Deserializer::from_str(json); - let value = T::deserialize(&mut deserializer); - let value = match value { - Ok(value) => value, - Err(error) => { - return ( - None, - ParseStatus::Failed { - error: error.to_string(), - }, - ); - } - }; - - if let Some(errors) = ERRORS.with_borrow_mut(|errors| errors.take().filter(|e| !e.is_empty())) { - let error = errors - .into_iter() - .map(|e| e.to_string()) - .flat_map(|e| ["\n".to_owned(), e]) - .skip(1) - .collect::(); - return (Some(value), ParseStatus::Failed { error }); - } - - (Some(value), ParseStatus::Success) -} - -pub(crate) fn deserialize<'de, D, T>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, - T: serde::Deserialize<'de> + FallibleOption, -{ - match T::deserialize(deserializer) { - Ok(value) => Ok(value), - Err(e) => ERRORS.with_borrow_mut(|errors| { - if let Some(errors) = errors { - errors.push(anyhow::anyhow!("{}", e)); - Ok(Default::default()) - } else { - Err(e) - } - }), - } -} - -pub trait FallibleOption: Default {} -impl FallibleOption for Option {} - -#[cfg(test)] -mod tests { - use serde::Deserialize; - use settings_macros::with_fallible_options; - - use crate::ParseStatus; - - #[with_fallible_options] - #[derive(Deserialize, Debug, PartialEq)] - struct Foo { - foo: Option, - bar: Option, - baz: Option, - } - - #[test] - fn test_fallible() { - let input = r#" - {"foo": "bar", - "bar": "foo", - "baz": 3, - } - "#; - - let (settings, result) = crate::fallible_options::parse_json::(&input); - assert_eq!( - settings.unwrap(), - Foo { - foo: Some("bar".into()), - bar: None, - baz: None, - } - ); - - assert!(crate::parse_json_with_comments::(&input).is_err()); - - let ParseStatus::Failed { error } = result else { - panic!("Expected parse to fail") - }; - - assert_eq!( - error, - "invalid type: string \"foo\", expected usize at line 3 column 24\ninvalid type: integer `3`, expected a boolean at line 4 column 20".to_string() - ) - } -} diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs deleted file mode 100644 index 2ef1dfc538..0000000000 --- a/crates/settings/src/keymap_file.rs +++ /dev/null @@ -1,1971 +0,0 @@ -use anyhow::{Context as _, Result}; -use collections::{BTreeMap, HashMap, IndexMap}; -use fs::Fs; -use gpui::{ - Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE, - KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, KeybindingKeystroke, Keystroke, - NoAction, SharedString, register_action, -}; -use schemars::{JsonSchema, json_schema}; -use serde::Deserialize; -use serde_json::{Value, json}; -use std::borrow::Cow; -use std::{any::TypeId, fmt::Write, rc::Rc, sync::Arc, sync::LazyLock}; -use util::ResultExt as _; -use util::{ - asset_str, - markdown::{MarkdownEscaped, MarkdownInlineCode, MarkdownString}, - schemars::AllowTrailingCommas, -}; - -use crate::SettingsAssets; -use settings_json::{ - append_top_level_array_value_in_json_text, parse_json_with_comments, - replace_top_level_array_value_in_json_text, -}; - -pub trait KeyBindingValidator: Send + Sync { - fn action_type_id(&self) -> TypeId; - fn validate(&self, binding: &KeyBinding) -> Result<(), MarkdownString>; -} - -pub struct KeyBindingValidatorRegistration(pub fn() -> Box); - -inventory::collect!(KeyBindingValidatorRegistration); - -pub(crate) static KEY_BINDING_VALIDATORS: LazyLock>> = - LazyLock::new(|| { - let mut validators = BTreeMap::new(); - for validator_registration in inventory::iter:: { - let validator = validator_registration.0(); - validators.insert(validator.action_type_id(), validator); - } - validators - }); - -// Note that the doc comments on these are shown by json-language-server when editing the keymap, so -// they should be considered user-facing documentation. Documentation is not handled well with -// schemars-0.8 - when there are newlines, it is rendered as plaintext (see -// https://github.com/GREsau/schemars/issues/38#issuecomment-2282883519). So for now these docs -// avoid newlines. -// -// TODO: Update to schemars-1.0 once it's released, and add more docs as newlines would be -// supported. Tracking issue is https://github.com/GREsau/schemars/issues/112. - -/// Keymap configuration consisting of sections. Each section may have a context predicate which -/// determines whether its bindings are used. -#[derive(Debug, Deserialize, Default, Clone, JsonSchema)] -#[serde(transparent)] -pub struct KeymapFile(Vec); - -/// Keymap section which binds keystrokes to actions. -#[derive(Debug, Deserialize, Default, Clone, JsonSchema)] -pub struct KeymapSection { - /// Determines when these bindings are active. When just a name is provided, like `Editor` or - /// `Workspace`, the bindings will be active in that context. Boolean expressions like `X && Y`, - /// `X || Y`, `!X` are also supported. Some more complex logic including checking OS and the - /// current file extension are also supported - see [the - /// documentation](https://zed.dev/docs/key-bindings#contexts) for more details. - #[serde(default)] - pub context: String, - /// This option enables specifying keys based on their position on a QWERTY keyboard, by using - /// position-equivalent mappings for some non-QWERTY keyboards. This is currently only supported - /// on macOS. See the documentation for more details. - #[serde(default)] - use_key_equivalents: bool, - /// This keymap section's bindings, as a JSON object mapping keystrokes to actions. The - /// keystrokes key is a string representing a sequence of keystrokes to type, where the - /// keystrokes are separated by whitespace. Each keystroke is a sequence of modifiers (`ctrl`, - /// `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) followed by a key, separated by `-`. The - /// order of bindings does matter. When the same keystrokes are bound at the same context depth, - /// the binding that occurs later in the file is preferred. For displaying keystrokes in the UI, - /// the later binding for the same action is preferred. - #[serde(default)] - bindings: Option>, - #[serde(flatten)] - unrecognized_fields: IndexMap, - // This struct intentionally uses permissive types for its fields, rather than validating during - // deserialization. The purpose of this is to allow loading the portion of the keymap that doesn't - // have errors. The downside of this is that the errors are not reported with line+column info. - // Unfortunately the implementations of the `Spanned` types for preserving this information are - // highly inconvenient (`serde_spanned`) and in some cases don't work at all here - // (`json_spanned_>value`). Serde should really have builtin support for this. -} - -impl KeymapSection { - pub fn bindings(&self) -> impl DoubleEndedIterator { - self.bindings.iter().flatten() - } -} - -/// Keymap action as a JSON value, since it can either be null for no action, or the name of the -/// action, or an array of the name of the action and the action input. -/// -/// Unlike the other json types involved in keymaps (including actions), this doc-comment will not -/// be included in the generated JSON schema, as it manually defines its `JsonSchema` impl. The -/// actual schema used for it is automatically generated in `KeymapFile::generate_json_schema`. -#[derive(Debug, Deserialize, Default, Clone)] -#[serde(transparent)] -pub struct KeymapAction(Value); - -impl std::fmt::Display for KeymapAction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.0 { - Value::String(s) => write!(f, "{}", s), - Value::Array(arr) => { - let strings: Vec = arr.iter().map(|v| v.to_string()).collect(); - write!(f, "{}", strings.join(", ")) - } - _ => write!(f, "{}", self.0), - } - } -} - -impl JsonSchema for KeymapAction { - /// This is used when generating the JSON schema for the `KeymapAction` type, so that it can - /// reference the keymap action schema. - fn schema_name() -> Cow<'static, str> { - "KeymapAction".into() - } - - /// This schema will be replaced with the full action schema in - /// `KeymapFile::generate_json_schema`. - fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { - json_schema!(true) - } -} - -#[derive(Debug)] -#[must_use] -pub enum KeymapFileLoadResult { - Success { - key_bindings: Vec, - }, - SomeFailedToLoad { - key_bindings: Vec, - error_message: MarkdownString, - }, - JsonParseFailure { - error: anyhow::Error, - }, -} - -impl KeymapFile { - pub fn parse(content: &str) -> anyhow::Result { - if content.trim().is_empty() { - return Ok(Self(Vec::new())); - } - parse_json_with_comments::(content) - } - - pub fn load_asset( - asset_path: &str, - source: Option, - cx: &App, - ) -> anyhow::Result> { - match Self::load(asset_str::(asset_path).as_ref(), cx) { - KeymapFileLoadResult::Success { mut key_bindings } => match source { - Some(source) => Ok({ - for key_binding in &mut key_bindings { - key_binding.set_meta(source.meta()); - } - key_bindings - }), - None => Ok(key_bindings), - }, - KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => { - anyhow::bail!("Error loading built-in keymap \"{asset_path}\": {error_message}",) - } - KeymapFileLoadResult::JsonParseFailure { error } => { - anyhow::bail!("JSON parse error in built-in keymap \"{asset_path}\": {error}") - } - } - } - - pub fn load_asset_allow_partial_failure( - asset_path: &str, - cx: &App, - ) -> anyhow::Result> { - match Self::load(asset_str::(asset_path).as_ref(), cx) { - KeymapFileLoadResult::SomeFailedToLoad { - key_bindings, - error_message, - .. - } if key_bindings.is_empty() => { - anyhow::bail!("Error loading built-in keymap \"{asset_path}\": {error_message}",) - } - KeymapFileLoadResult::Success { key_bindings, .. } - | KeymapFileLoadResult::SomeFailedToLoad { key_bindings, .. } => Ok(key_bindings), - KeymapFileLoadResult::JsonParseFailure { error } => { - anyhow::bail!("JSON parse error in built-in keymap \"{asset_path}\": {error}") - } - } - } - - #[cfg(feature = "test-support")] - pub fn load_panic_on_failure(content: &str, cx: &App) -> Vec { - match Self::load(content, cx) { - KeymapFileLoadResult::Success { key_bindings, .. } => key_bindings, - KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => { - panic!("{error_message}"); - } - KeymapFileLoadResult::JsonParseFailure { error } => { - panic!("JSON parse error: {error}"); - } - } - } - - pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult { - let keymap_file = match Self::parse(content) { - Ok(keymap_file) => keymap_file, - Err(error) => { - return KeymapFileLoadResult::JsonParseFailure { error }; - } - }; - - // Accumulate errors in order to support partial load of user keymap in the presence of - // errors in context and binding parsing. - let mut errors = Vec::new(); - let mut key_bindings = Vec::new(); - - for KeymapSection { - context, - use_key_equivalents, - bindings, - unrecognized_fields, - } in keymap_file.0.iter() - { - let context_predicate: Option> = if context.is_empty() { - None - } else { - match KeyBindingContextPredicate::parse(context) { - Ok(context_predicate) => Some(context_predicate.into()), - Err(err) => { - // Leading space is to separate from the message indicating which section - // the error occurred in. - errors.push(( - context, - format!(" Parse error in section `context` field: {}", err), - )); - continue; - } - } - }; - - let mut section_errors = String::new(); - - if !unrecognized_fields.is_empty() { - write!( - section_errors, - "\n\n - Unrecognized fields: {}", - MarkdownInlineCode(&format!("{:?}", unrecognized_fields.keys())) - ) - .unwrap(); - } - - if let Some(bindings) = bindings { - for (keystrokes, action) in bindings { - let result = Self::load_keybinding( - keystrokes, - action, - context_predicate.clone(), - *use_key_equivalents, - cx, - ); - match result { - Ok(key_binding) => { - key_bindings.push(key_binding); - } - Err(err) => { - let mut lines = err.lines(); - let mut indented_err = lines.next().unwrap().to_string(); - for line in lines { - indented_err.push_str(" "); - indented_err.push_str(line); - indented_err.push_str("\n"); - } - write!( - section_errors, - "\n\n- In binding {}, {indented_err}", - MarkdownInlineCode(&format!("\"{}\"", keystrokes)) - ) - .unwrap(); - } - } - } - } - - if !section_errors.is_empty() { - errors.push((context, section_errors)) - } - } - - if errors.is_empty() { - KeymapFileLoadResult::Success { key_bindings } - } else { - let mut error_message = "Errors in user keymap file.\n".to_owned(); - for (context, section_errors) in errors { - if context.is_empty() { - let _ = write!(error_message, "\n\nIn section without context predicate:"); - } else { - let _ = write!( - error_message, - "\n\nIn section with {}:", - MarkdownInlineCode(&format!("context = \"{}\"", context)) - ); - } - let _ = write!(error_message, "{section_errors}"); - } - KeymapFileLoadResult::SomeFailedToLoad { - key_bindings, - error_message: MarkdownString(error_message), - } - } - } - - fn load_keybinding( - keystrokes: &str, - action: &KeymapAction, - context: Option>, - use_key_equivalents: bool, - cx: &App, - ) -> std::result::Result { - let (action, action_input_string) = Self::build_keymap_action(action, cx)?; - - let key_binding = match KeyBinding::load( - keystrokes, - action, - context, - use_key_equivalents, - action_input_string.map(SharedString::from), - cx.keyboard_mapper().as_ref(), - ) { - Ok(key_binding) => key_binding, - Err(InvalidKeystrokeError { keystroke }) => { - return Err(format!( - "invalid keystroke {}. {}", - MarkdownInlineCode(&format!("\"{}\"", &keystroke)), - KEYSTROKE_PARSE_EXPECTED_MESSAGE - )); - } - }; - - if let Some(validator) = KEY_BINDING_VALIDATORS.get(&key_binding.action().type_id()) { - match validator.validate(&key_binding) { - Ok(()) => Ok(key_binding), - Err(error) => Err(error.0), - } - } else { - Ok(key_binding) - } - } - - pub fn parse_action( - action: &KeymapAction, - ) -> Result)>, String> { - let name_and_input = match &action.0 { - Value::Array(items) => { - if items.len() != 2 { - return Err(format!( - "expected two-element array of `[name, input]`. \ - Instead found {}.", - MarkdownInlineCode(&action.0.to_string()) - )); - } - let serde_json::Value::String(ref name) = items[0] else { - return Err(format!( - "expected two-element array of `[name, input]`, \ - but the first element is not a string in {}.", - MarkdownInlineCode(&action.0.to_string()) - )); - }; - Some((name, Some(&items[1]))) - } - Value::String(name) => Some((name, None)), - Value::Null => None, - _ => { - return Err(format!( - "expected two-element array of `[name, input]`. \ - Instead found {}.", - MarkdownInlineCode(&action.0.to_string()) - )); - } - }; - Ok(name_and_input) - } - - fn build_keymap_action( - action: &KeymapAction, - cx: &App, - ) -> std::result::Result<(Box, Option), String> { - let (build_result, action_input_string) = match Self::parse_action(action)? { - Some((name, action_input)) if name.as_str() == ActionSequence::name_for_type() => { - match action_input { - Some(action_input) => ( - ActionSequence::build_sequence(action_input.clone(), cx), - None, - ), - None => (Err(ActionSequence::expected_array_error()), None), - } - } - Some((name, Some(action_input))) => { - let action_input_string = action_input.to_string(); - ( - cx.build_action(name, Some(action_input.clone())), - Some(action_input_string), - ) - } - Some((name, None)) => (cx.build_action(name, None), None), - None => (Ok(NoAction.boxed_clone()), None), - }; - - let action = match build_result { - Ok(action) => action, - Err(ActionBuildError::NotFound { name }) => { - return Err(format!( - "didn't find an action named {}.", - MarkdownInlineCode(&format!("\"{}\"", &name)) - )); - } - Err(ActionBuildError::BuildError { name, error }) => match action_input_string { - Some(action_input_string) => { - return Err(format!( - "can't build {} action from input value {}: {}", - MarkdownInlineCode(&format!("\"{}\"", &name)), - MarkdownInlineCode(&action_input_string), - MarkdownEscaped(&error.to_string()) - )); - } - None => { - return Err(format!( - "can't build {} action - it requires input data via [name, input]: {}", - MarkdownInlineCode(&format!("\"{}\"", &name)), - MarkdownEscaped(&error.to_string()) - )); - } - }, - }; - - Ok((action, action_input_string)) - } - - /// Creates a JSON schema generator, suitable for generating json schemas - /// for actions - pub fn action_schema_generator() -> schemars::SchemaGenerator { - schemars::generate::SchemaSettings::draft2019_09() - .with_transform(AllowTrailingCommas) - .into_generator() - } - - pub fn generate_json_schema_for_registered_actions(cx: &mut App) -> Value { - // instead of using DefaultDenyUnknownFields, actions typically use - // `#[serde(deny_unknown_fields)]` so that these cases are reported as parse failures. This - // is because the rest of the keymap will still load in these cases, whereas other settings - // files would not. - let mut generator = Self::action_schema_generator(); - - let action_schemas = cx.action_schemas(&mut generator); - let action_documentation = cx.action_documentation(); - let deprecations = cx.deprecated_actions_to_preferred_actions(); - let deprecation_messages = cx.action_deprecation_messages(); - KeymapFile::generate_json_schema( - generator, - action_schemas, - action_documentation, - deprecations, - deprecation_messages, - ) - } - - fn generate_json_schema( - mut generator: schemars::SchemaGenerator, - action_schemas: Vec<(&'static str, Option)>, - action_documentation: &HashMap<&'static str, &'static str>, - deprecations: &HashMap<&'static str, &'static str>, - deprecation_messages: &HashMap<&'static str, &'static str>, - ) -> serde_json::Value { - fn add_deprecation(schema: &mut schemars::Schema, message: String) { - schema.insert( - // deprecationMessage is not part of the JSON Schema spec, but - // json-language-server recognizes it. - "deprecationMessage".to_string(), - Value::String(message), - ); - } - - fn add_deprecation_preferred_name(schema: &mut schemars::Schema, new_name: &str) { - add_deprecation(schema, format!("Deprecated, use {new_name}")); - } - - fn add_description(schema: &mut schemars::Schema, description: &str) { - schema.insert( - "description".to_string(), - Value::String(description.to_string()), - ); - } - - let empty_object = json_schema!({ - "type": "object" - }); - - // This is a workaround for a json-language-server issue where it matches the first - // alternative that matches the value's shape and uses that for documentation. - // - // In the case of the array validations, it would even provide an error saying that the name - // must match the name of the first alternative. - let mut empty_action_name = json_schema!({ - "type": "string", - "const": "" - }); - let no_action_message = "No action named this."; - add_description(&mut empty_action_name, no_action_message); - add_deprecation(&mut empty_action_name, no_action_message.to_string()); - let empty_action_name_with_input = json_schema!({ - "type": "array", - "items": [ - empty_action_name, - true - ], - "minItems": 2, - "maxItems": 2 - }); - let mut keymap_action_alternatives = vec![empty_action_name, empty_action_name_with_input]; - - let mut empty_schema_action_names = vec![]; - for (name, action_schema) in action_schemas.into_iter() { - let deprecation = if name == NoAction.name() { - Some("null") - } else { - deprecations.get(name).copied() - }; - - // Add an alternative for plain action names. - let mut plain_action = json_schema!({ - "type": "string", - "const": name - }); - if let Some(message) = deprecation_messages.get(name) { - add_deprecation(&mut plain_action, message.to_string()); - } else if let Some(new_name) = deprecation { - add_deprecation_preferred_name(&mut plain_action, new_name); - } - let description = action_documentation.get(name); - if let Some(description) = &description { - add_description(&mut plain_action, description); - } - keymap_action_alternatives.push(plain_action); - - // Add an alternative for actions with data specified as a [name, data] array. - // - // When a struct with no deserializable fields is added by deriving `Action`, an empty - // object schema is produced. The action should be invoked without data in this case. - if let Some(schema) = action_schema - && schema != empty_object - { - let mut matches_action_name = json_schema!({ - "const": name - }); - if let Some(description) = &description { - add_description(&mut matches_action_name, description); - } - if let Some(message) = deprecation_messages.get(name) { - add_deprecation(&mut matches_action_name, message.to_string()); - } else if let Some(new_name) = deprecation { - add_deprecation_preferred_name(&mut matches_action_name, new_name); - } - let action_with_input = json_schema!({ - "type": "array", - "items": [matches_action_name, schema], - "minItems": 2, - "maxItems": 2 - }); - keymap_action_alternatives.push(action_with_input); - } else { - empty_schema_action_names.push(name); - } - } - - if !empty_schema_action_names.is_empty() { - let action_names = json_schema!({ "enum": empty_schema_action_names }); - let no_properties_allowed = json_schema!({ - "type": "object", - "additionalProperties": false - }); - let mut actions_with_empty_input = json_schema!({ - "type": "array", - "items": [action_names, no_properties_allowed], - "minItems": 2, - "maxItems": 2 - }); - add_deprecation( - &mut actions_with_empty_input, - "This action does not take input - just the action name string should be used." - .to_string(), - ); - keymap_action_alternatives.push(actions_with_empty_input); - } - - // Placing null first causes json-language-server to default assuming actions should be - // null, so place it last. - keymap_action_alternatives.push(json_schema!({ - "type": "null" - })); - - // The `KeymapSection` schema will reference the `KeymapAction` schema by name, so setting - // the definition of `KeymapAction` results in the full action schema being used. - generator.definitions_mut().insert( - KeymapAction::schema_name().to_string(), - json!({ - "oneOf": keymap_action_alternatives - }), - ); - - generator.root_schema_for::().to_value() - } - - pub fn sections(&self) -> impl DoubleEndedIterator { - self.0.iter() - } - - pub async fn load_keymap_file(fs: &Arc) -> Result { - match fs.load(paths::keymap_file()).await { - result @ Ok(_) => result, - Err(err) => { - if let Some(e) = err.downcast_ref::() - && e.kind() == std::io::ErrorKind::NotFound - { - return Ok(crate::initial_keymap_content().to_string()); - } - Err(err) - } - } - } - - pub fn update_keybinding<'a>( - mut operation: KeybindUpdateOperation<'a>, - mut keymap_contents: String, - tab_size: usize, - keyboard_mapper: &dyn gpui::PlatformKeyboardMapper, - ) -> Result { - match operation { - // if trying to replace a keybinding that is not user-defined, treat it as an add operation - KeybindUpdateOperation::Replace { - target_keybind_source: target_source, - source, - target, - } if target_source != KeybindSource::User => { - operation = KeybindUpdateOperation::Add { - source, - from: Some(target), - }; - } - // if trying to remove a keybinding that is not user-defined, treat it as creating a binding - // that binds it to `zed::NoAction` - KeybindUpdateOperation::Remove { - target, - target_keybind_source, - } if target_keybind_source != KeybindSource::User => { - let mut source = target.clone(); - source.action_name = gpui::NoAction.name(); - source.action_arguments.take(); - operation = KeybindUpdateOperation::Add { - source, - from: Some(target), - }; - } - _ => {} - } - - // Sanity check that keymap contents are valid, even though we only use it for Replace. - // We don't want to modify the file if it's invalid. - let keymap = Self::parse(&keymap_contents).context("Failed to parse keymap")?; - - if let KeybindUpdateOperation::Remove { target, .. } = operation { - let target_action_value = target - .action_value() - .context("Failed to generate target action JSON value")?; - let Some((index, keystrokes_str)) = - find_binding(&keymap, &target, &target_action_value, keyboard_mapper) - else { - anyhow::bail!("Failed to find keybinding to remove"); - }; - let is_only_binding = keymap.0[index] - .bindings - .as_ref() - .is_none_or(|bindings| bindings.len() == 1); - let key_path: &[&str] = if is_only_binding { - &[] - } else { - &["bindings", keystrokes_str] - }; - let (replace_range, replace_value) = replace_top_level_array_value_in_json_text( - &keymap_contents, - key_path, - None, - None, - index, - tab_size, - ); - keymap_contents.replace_range(replace_range, &replace_value); - return Ok(keymap_contents); - } - - if let KeybindUpdateOperation::Replace { source, target, .. } = operation { - let target_action_value = target - .action_value() - .context("Failed to generate target action JSON value")?; - let source_action_value = source - .action_value() - .context("Failed to generate source action JSON value")?; - - if let Some((index, keystrokes_str)) = - find_binding(&keymap, &target, &target_action_value, keyboard_mapper) - { - if target.context == source.context { - // if we are only changing the keybinding (common case) - // not the context, etc. Then just update the binding in place - - let (replace_range, replace_value) = replace_top_level_array_value_in_json_text( - &keymap_contents, - &["bindings", keystrokes_str], - Some(&source_action_value), - Some(&source.keystrokes_unparsed()), - index, - tab_size, - ); - keymap_contents.replace_range(replace_range, &replace_value); - - return Ok(keymap_contents); - } else if keymap.0[index] - .bindings - .as_ref() - .is_none_or(|bindings| bindings.len() == 1) - { - // if we are replacing the only binding in the section, - // just update the section in place, updating the context - // and the binding - - let (replace_range, replace_value) = replace_top_level_array_value_in_json_text( - &keymap_contents, - &["bindings", keystrokes_str], - Some(&source_action_value), - Some(&source.keystrokes_unparsed()), - index, - tab_size, - ); - keymap_contents.replace_range(replace_range, &replace_value); - - let (replace_range, replace_value) = replace_top_level_array_value_in_json_text( - &keymap_contents, - &["context"], - source.context.map(Into::into).as_ref(), - None, - index, - tab_size, - ); - keymap_contents.replace_range(replace_range, &replace_value); - return Ok(keymap_contents); - } else { - // if we are replacing one of multiple bindings in a section - // with a context change, remove the existing binding from the - // section, then treat this operation as an add operation of the - // new binding with the updated context. - - let (replace_range, replace_value) = replace_top_level_array_value_in_json_text( - &keymap_contents, - &["bindings", keystrokes_str], - None, - None, - index, - tab_size, - ); - keymap_contents.replace_range(replace_range, &replace_value); - operation = KeybindUpdateOperation::Add { - source, - from: Some(target), - }; - } - } else { - log::warn!( - "Failed to find keybinding to update `{:?} -> {}` creating new binding for `{:?} -> {}` instead", - target.keystrokes, - target_action_value, - source.keystrokes, - source_action_value, - ); - operation = KeybindUpdateOperation::Add { - source, - from: Some(target), - }; - } - } - - if let KeybindUpdateOperation::Add { - source: keybinding, - from, - } = operation - { - let mut value = serde_json::Map::with_capacity(4); - if let Some(context) = keybinding.context { - value.insert("context".to_string(), context.into()); - } - let use_key_equivalents = from.and_then(|from| { - let action_value = from.action_value().context("Failed to serialize action value. `use_key_equivalents` on new keybinding may be incorrect.").log_err()?; - let (index, _) = find_binding(&keymap, &from, &action_value, keyboard_mapper)?; - Some(keymap.0[index].use_key_equivalents) - }).unwrap_or(false); - if use_key_equivalents { - value.insert("use_key_equivalents".to_string(), true.into()); - } - - value.insert("bindings".to_string(), { - let mut bindings = serde_json::Map::new(); - let action = keybinding.action_value()?; - bindings.insert(keybinding.keystrokes_unparsed(), action); - bindings.into() - }); - - let (replace_range, replace_value) = append_top_level_array_value_in_json_text( - &keymap_contents, - &value.into(), - tab_size, - ); - keymap_contents.replace_range(replace_range, &replace_value); - } - return Ok(keymap_contents); - - fn find_binding<'a, 'b>( - keymap: &'b KeymapFile, - target: &KeybindUpdateTarget<'a>, - target_action_value: &Value, - keyboard_mapper: &dyn gpui::PlatformKeyboardMapper, - ) -> Option<(usize, &'b str)> { - let target_context_parsed = - KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok(); - for (index, section) in keymap.sections().enumerate() { - let section_context_parsed = - KeyBindingContextPredicate::parse(§ion.context).ok(); - if section_context_parsed != target_context_parsed { - continue; - } - let Some(bindings) = §ion.bindings else { - continue; - }; - for (keystrokes_str, action) in bindings { - let Ok(keystrokes) = keystrokes_str - .split_whitespace() - .map(|source| { - let keystroke = Keystroke::parse(source)?; - Ok(KeybindingKeystroke::new_with_mapper( - keystroke, - false, - keyboard_mapper, - )) - }) - .collect::, InvalidKeystrokeError>>() - else { - continue; - }; - if keystrokes.len() != target.keystrokes.len() - || !keystrokes - .iter() - .zip(target.keystrokes) - .all(|(a, b)| a.inner().should_match(b)) - { - continue; - } - if &action.0 != target_action_value { - continue; - } - return Some((index, keystrokes_str)); - } - } - None - } - } -} - -#[derive(Clone, Debug)] -pub enum KeybindUpdateOperation<'a> { - Replace { - /// Describes the keybind to create - source: KeybindUpdateTarget<'a>, - /// Describes the keybind to remove - target: KeybindUpdateTarget<'a>, - target_keybind_source: KeybindSource, - }, - Add { - source: KeybindUpdateTarget<'a>, - from: Option>, - }, - Remove { - target: KeybindUpdateTarget<'a>, - target_keybind_source: KeybindSource, - }, -} - -impl KeybindUpdateOperation<'_> { - pub fn generate_telemetry( - &self, - ) -> ( - // The keybind that is created - String, - // The keybinding that was removed - String, - // The source of the keybinding - String, - ) { - let (new_binding, removed_binding, source) = match &self { - KeybindUpdateOperation::Replace { - source, - target, - target_keybind_source, - } => (Some(source), Some(target), Some(*target_keybind_source)), - KeybindUpdateOperation::Add { source, .. } => (Some(source), None, None), - KeybindUpdateOperation::Remove { - target, - target_keybind_source, - } => (None, Some(target), Some(*target_keybind_source)), - }; - - let new_binding = new_binding - .map(KeybindUpdateTarget::telemetry_string) - .unwrap_or("null".to_owned()); - let removed_binding = removed_binding - .map(KeybindUpdateTarget::telemetry_string) - .unwrap_or("null".to_owned()); - - let source = source - .as_ref() - .map(KeybindSource::name) - .map(ToOwned::to_owned) - .unwrap_or("null".to_owned()); - - (new_binding, removed_binding, source) - } -} - -impl<'a> KeybindUpdateOperation<'a> { - pub fn add(source: KeybindUpdateTarget<'a>) -> Self { - Self::Add { source, from: None } - } -} - -#[derive(Debug, Clone)] -pub struct KeybindUpdateTarget<'a> { - pub context: Option<&'a str>, - pub keystrokes: &'a [KeybindingKeystroke], - pub action_name: &'a str, - pub action_arguments: Option<&'a str>, -} - -impl<'a> KeybindUpdateTarget<'a> { - fn action_value(&self) -> Result { - if self.action_name == gpui::NoAction.name() { - return Ok(Value::Null); - } - let action_name: Value = self.action_name.into(); - let value = match self.action_arguments { - Some(args) if !args.is_empty() => { - let args = serde_json::from_str::(args) - .context("Failed to parse action arguments as JSON")?; - serde_json::json!([action_name, args]) - } - _ => action_name, - }; - Ok(value) - } - - fn keystrokes_unparsed(&self) -> String { - let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8); - for keystroke in self.keystrokes { - // The reason use `keystroke.unparse()` instead of `keystroke.inner.unparse()` - // here is that, we want the user to use `ctrl-shift-4` instead of `ctrl-$` - // by default on Windows. - keystrokes.push_str(&keystroke.unparse()); - keystrokes.push(' '); - } - keystrokes.pop(); - keystrokes - } - - fn telemetry_string(&self) -> String { - format!( - "action_name: {}, context: {}, action_arguments: {}, keystrokes: {}", - self.action_name, - self.context.unwrap_or("global"), - self.action_arguments.unwrap_or("none"), - self.keystrokes_unparsed() - ) - } -} - -#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug)] -pub enum KeybindSource { - User, - Vim, - Base, - #[default] - Default, - Unknown, -} - -impl KeybindSource { - const BASE: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::Base as u32); - const DEFAULT: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::Default as u32); - const VIM: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::Vim as u32); - const USER: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::User as u32); - - pub fn name(&self) -> &'static str { - match self { - KeybindSource::User => "User", - KeybindSource::Default => "Default", - KeybindSource::Base => "Base", - KeybindSource::Vim => "Vim", - KeybindSource::Unknown => "Unknown", - } - } - - pub fn meta(&self) -> KeyBindingMetaIndex { - match self { - KeybindSource::User => Self::USER, - KeybindSource::Default => Self::DEFAULT, - KeybindSource::Base => Self::BASE, - KeybindSource::Vim => Self::VIM, - KeybindSource::Unknown => KeyBindingMetaIndex(*self as u32), - } - } - - pub fn from_meta(index: KeyBindingMetaIndex) -> Self { - match index { - Self::USER => KeybindSource::User, - Self::BASE => KeybindSource::Base, - Self::DEFAULT => KeybindSource::Default, - Self::VIM => KeybindSource::Vim, - _ => KeybindSource::Unknown, - } - } -} - -impl From for KeybindSource { - fn from(index: KeyBindingMetaIndex) -> Self { - Self::from_meta(index) - } -} - -impl From for KeyBindingMetaIndex { - fn from(source: KeybindSource) -> Self { - source.meta() - } -} - -/// Runs a sequence of actions. Does not wait for asynchronous actions to complete before running -/// the next action. Currently only works in workspace windows. -/// -/// This action is special-cased in keymap parsing to allow it to access `App` while parsing, so -/// that it can parse its input actions. -pub struct ActionSequence(pub Vec>); - -register_action!(ActionSequence); - -impl ActionSequence { - fn build_sequence( - value: Value, - cx: &App, - ) -> std::result::Result, ActionBuildError> { - match value { - Value::Array(values) => { - let actions = values - .into_iter() - .enumerate() - .map(|(index, action)| { - match KeymapFile::build_keymap_action(&KeymapAction(action), cx) { - Ok((action, _)) => Ok(action), - Err(err) => { - return Err(ActionBuildError::BuildError { - name: Self::name_for_type().to_string(), - error: anyhow::anyhow!( - "error at sequence index {index}: {err}" - ), - }); - } - } - }) - .collect::, _>>()?; - Ok(Box::new(Self(actions))) - } - _ => Err(Self::expected_array_error()), - } - } - - fn expected_array_error() -> ActionBuildError { - ActionBuildError::BuildError { - name: Self::name_for_type().to_string(), - error: anyhow::anyhow!("expected array of actions"), - } - } -} - -impl Action for ActionSequence { - fn name(&self) -> &'static str { - Self::name_for_type() - } - - fn name_for_type() -> &'static str - where - Self: Sized, - { - "action::Sequence" - } - - fn partial_eq(&self, action: &dyn Action) -> bool { - action - .as_any() - .downcast_ref::() - .map_or(false, |other| { - self.0.len() == other.0.len() - && self - .0 - .iter() - .zip(other.0.iter()) - .all(|(a, b)| a.partial_eq(b.as_ref())) - }) - } - - fn boxed_clone(&self) -> Box { - Box::new(ActionSequence( - self.0 - .iter() - .map(|action| action.boxed_clone()) - .collect::>(), - )) - } - - fn build(_value: Value) -> Result> { - Err(anyhow::anyhow!( - "{} cannot be built directly", - Self::name_for_type() - )) - } - - fn action_json_schema(generator: &mut schemars::SchemaGenerator) -> Option { - let keymap_action_schema = generator.subschema_for::(); - Some(json_schema!({ - "type": "array", - "items": keymap_action_schema - })) - } - - fn deprecated_aliases() -> &'static [&'static str] { - &[] - } - - fn deprecation_message() -> Option<&'static str> { - None - } - - fn documentation() -> Option<&'static str> { - Some( - "Runs a sequence of actions.\n\n\ - NOTE: This does **not** wait for asynchronous actions to complete before running the next action.", - ) - } -} - -#[cfg(test)] -mod tests { - use gpui::{DummyKeyboardMapper, KeybindingKeystroke, Keystroke}; - use unindent::Unindent; - - use crate::{ - KeybindSource, KeymapFile, - keymap_file::{KeybindUpdateOperation, KeybindUpdateTarget}, - }; - - #[test] - fn can_deserialize_keymap_with_trailing_comma() { - let json = indoc::indoc! {"[ - // Standard macOS bindings - { - \"bindings\": { - \"up\": \"menu::SelectPrevious\", - }, - }, - ] - " - }; - KeymapFile::parse(json).unwrap(); - } - - #[track_caller] - fn check_keymap_update( - input: impl ToString, - operation: KeybindUpdateOperation, - expected: impl ToString, - ) { - let result = KeymapFile::update_keybinding( - operation, - input.to_string(), - 4, - &gpui::DummyKeyboardMapper, - ) - .expect("Update succeeded"); - pretty_assertions::assert_eq!(expected.to_string(), result); - } - - #[track_caller] - fn parse_keystrokes(keystrokes: &str) -> Vec { - keystrokes - .split(' ') - .map(|s| { - KeybindingKeystroke::new_with_mapper( - Keystroke::parse(s).expect("Keystrokes valid"), - false, - &DummyKeyboardMapper, - ) - }) - .collect() - } - - #[test] - fn keymap_update() { - zlog::init_test(); - - check_keymap_update( - "[]", - KeybindUpdateOperation::add(KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-a"), - action_name: "zed::SomeAction", - context: None, - action_arguments: None, - }), - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - "[]", - KeybindUpdateOperation::add(KeybindUpdateTarget { - keystrokes: &parse_keystrokes("\\ a"), - action_name: "zed::SomeAction", - context: None, - action_arguments: None, - }), - r#"[ - { - "bindings": { - "\\ a": "zed::SomeAction" - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - "[]", - KeybindUpdateOperation::add(KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-a"), - action_name: "zed::SomeAction", - context: None, - action_arguments: Some(""), - }), - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - } - ]"# - .unindent(), - KeybindUpdateOperation::add(KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-b"), - action_name: "zed::SomeOtherAction", - context: None, - action_arguments: None, - }), - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - }, - { - "bindings": { - "ctrl-b": "zed::SomeOtherAction" - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - } - ]"# - .unindent(), - KeybindUpdateOperation::add(KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-b"), - action_name: "zed::SomeOtherAction", - context: None, - action_arguments: Some(r#"{"foo": "bar"}"#), - }), - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - }, - { - "bindings": { - "ctrl-b": [ - "zed::SomeOtherAction", - { - "foo": "bar" - } - ] - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - } - ]"# - .unindent(), - KeybindUpdateOperation::add(KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-b"), - action_name: "zed::SomeOtherAction", - context: Some("Zed > Editor && some_condition = true"), - action_arguments: Some(r#"{"foo": "bar"}"#), - }), - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - }, - { - "context": "Zed > Editor && some_condition = true", - "bindings": { - "ctrl-b": [ - "zed::SomeOtherAction", - { - "foo": "bar" - } - ] - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - } - ]"# - .unindent(), - KeybindUpdateOperation::Replace { - target: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-a"), - action_name: "zed::SomeAction", - context: None, - action_arguments: None, - }, - source: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-b"), - action_name: "zed::SomeOtherAction", - context: None, - action_arguments: Some(r#"{"foo": "bar"}"#), - }, - target_keybind_source: KeybindSource::Base, - }, - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - }, - { - "bindings": { - "ctrl-b": [ - "zed::SomeOtherAction", - { - "foo": "bar" - } - ] - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "bindings": { - "a": "zed::SomeAction" - } - } - ]"# - .unindent(), - KeybindUpdateOperation::Replace { - target: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("a"), - action_name: "zed::SomeAction", - context: None, - action_arguments: None, - }, - source: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-b"), - action_name: "zed::SomeOtherAction", - context: None, - action_arguments: Some(r#"{"foo": "bar"}"#), - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "bindings": { - "ctrl-b": [ - "zed::SomeOtherAction", - { - "foo": "bar" - } - ] - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "bindings": { - "\\ a": "zed::SomeAction" - } - } - ]"# - .unindent(), - KeybindUpdateOperation::Replace { - target: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("\\ a"), - action_name: "zed::SomeAction", - context: None, - action_arguments: None, - }, - source: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("\\ b"), - action_name: "zed::SomeOtherAction", - context: None, - action_arguments: Some(r#"{"foo": "bar"}"#), - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "bindings": { - "\\ b": [ - "zed::SomeOtherAction", - { - "foo": "bar" - } - ] - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "bindings": { - "\\ a": "zed::SomeAction" - } - } - ]"# - .unindent(), - KeybindUpdateOperation::Replace { - target: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("\\ a"), - action_name: "zed::SomeAction", - context: None, - action_arguments: None, - }, - source: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("\\ a"), - action_name: "zed::SomeAction", - context: None, - action_arguments: None, - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "bindings": { - "\\ a": "zed::SomeAction" - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - } - ]"# - .unindent(), - KeybindUpdateOperation::Replace { - target: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-a"), - action_name: "zed::SomeNonexistentAction", - context: None, - action_arguments: None, - }, - source: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-b"), - action_name: "zed::SomeOtherAction", - context: None, - action_arguments: None, - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - }, - { - "bindings": { - "ctrl-b": "zed::SomeOtherAction" - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "bindings": { - // some comment - "ctrl-a": "zed::SomeAction" - // some other comment - } - } - ]"# - .unindent(), - KeybindUpdateOperation::Replace { - target: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-a"), - action_name: "zed::SomeAction", - context: None, - action_arguments: None, - }, - source: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-b"), - action_name: "zed::SomeOtherAction", - context: None, - action_arguments: Some(r#"{"foo": "bar"}"#), - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "bindings": { - // some comment - "ctrl-b": [ - "zed::SomeOtherAction", - { - "foo": "bar" - } - ] - // some other comment - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "context": "SomeContext", - "bindings": { - "a": "foo::bar", - "b": "baz::qux", - } - } - ]"# - .unindent(), - KeybindUpdateOperation::Replace { - target: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("a"), - action_name: "foo::bar", - context: Some("SomeContext"), - action_arguments: None, - }, - source: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("c"), - action_name: "foo::baz", - context: Some("SomeOtherContext"), - action_arguments: None, - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "context": "SomeContext", - "bindings": { - "b": "baz::qux", - } - }, - { - "context": "SomeOtherContext", - "bindings": { - "c": "foo::baz" - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "context": "SomeContext", - "bindings": { - "a": "foo::bar", - } - } - ]"# - .unindent(), - KeybindUpdateOperation::Replace { - target: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("a"), - action_name: "foo::bar", - context: Some("SomeContext"), - action_arguments: None, - }, - source: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("c"), - action_name: "foo::baz", - context: Some("SomeOtherContext"), - action_arguments: None, - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "context": "SomeOtherContext", - "bindings": { - "c": "foo::baz", - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "context": "SomeContext", - "bindings": { - "a": "foo::bar", - "c": "foo::baz", - } - }, - ]"# - .unindent(), - KeybindUpdateOperation::Remove { - target: KeybindUpdateTarget { - context: Some("SomeContext"), - keystrokes: &parse_keystrokes("a"), - action_name: "foo::bar", - action_arguments: None, - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "context": "SomeContext", - "bindings": { - "c": "foo::baz", - } - }, - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "context": "SomeContext", - "bindings": { - "\\ a": "foo::bar", - "c": "foo::baz", - } - }, - ]"# - .unindent(), - KeybindUpdateOperation::Remove { - target: KeybindUpdateTarget { - context: Some("SomeContext"), - keystrokes: &parse_keystrokes("\\ a"), - action_name: "foo::bar", - action_arguments: None, - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "context": "SomeContext", - "bindings": { - "c": "foo::baz", - } - }, - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "context": "SomeContext", - "bindings": { - "a": ["foo::bar", true], - "c": "foo::baz", - } - }, - ]"# - .unindent(), - KeybindUpdateOperation::Remove { - target: KeybindUpdateTarget { - context: Some("SomeContext"), - keystrokes: &parse_keystrokes("a"), - action_name: "foo::bar", - action_arguments: Some("true"), - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "context": "SomeContext", - "bindings": { - "c": "foo::baz", - } - }, - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "context": "SomeContext", - "bindings": { - "b": "foo::baz", - } - }, - { - "context": "SomeContext", - "bindings": { - "a": ["foo::bar", true], - } - }, - { - "context": "SomeContext", - "bindings": { - "c": "foo::baz", - } - }, - ]"# - .unindent(), - KeybindUpdateOperation::Remove { - target: KeybindUpdateTarget { - context: Some("SomeContext"), - keystrokes: &parse_keystrokes("a"), - action_name: "foo::bar", - action_arguments: Some("true"), - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "context": "SomeContext", - "bindings": { - "b": "foo::baz", - } - }, - { - "context": "SomeContext", - "bindings": { - "c": "foo::baz", - } - }, - ]"# - .unindent(), - ); - check_keymap_update( - r#"[ - { - "context": "SomeOtherContext", - "use_key_equivalents": true, - "bindings": { - "b": "foo::bar", - } - }, - ]"# - .unindent(), - KeybindUpdateOperation::Add { - source: KeybindUpdateTarget { - context: Some("SomeContext"), - keystrokes: &parse_keystrokes("a"), - action_name: "foo::baz", - action_arguments: Some("true"), - }, - from: Some(KeybindUpdateTarget { - context: Some("SomeOtherContext"), - keystrokes: &parse_keystrokes("b"), - action_name: "foo::bar", - action_arguments: None, - }), - }, - r#"[ - { - "context": "SomeOtherContext", - "use_key_equivalents": true, - "bindings": { - "b": "foo::bar", - } - }, - { - "context": "SomeContext", - "use_key_equivalents": true, - "bindings": { - "a": [ - "foo::baz", - true - ] - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "context": "SomeOtherContext", - "use_key_equivalents": true, - "bindings": { - "b": "foo::bar", - } - }, - ]"# - .unindent(), - KeybindUpdateOperation::Remove { - target: KeybindUpdateTarget { - context: Some("SomeContext"), - keystrokes: &parse_keystrokes("a"), - action_name: "foo::baz", - action_arguments: Some("true"), - }, - target_keybind_source: KeybindSource::Default, - }, - r#"[ - { - "context": "SomeOtherContext", - "use_key_equivalents": true, - "bindings": { - "b": "foo::bar", - } - }, - { - "context": "SomeContext", - "bindings": { - "a": null - } - } - ]"# - .unindent(), - ); - } - - #[test] - fn test_keymap_remove() { - zlog::init_test(); - - check_keymap_update( - r#" - [ - { - "context": "Editor", - "bindings": { - "cmd-k cmd-u": "editor::ConvertToUpperCase", - "cmd-k cmd-l": "editor::ConvertToLowerCase", - "cmd-[": "pane::GoBack", - } - }, - ] - "#, - KeybindUpdateOperation::Remove { - target: KeybindUpdateTarget { - context: Some("Editor"), - keystrokes: &parse_keystrokes("cmd-k cmd-l"), - action_name: "editor::ConvertToLowerCase", - action_arguments: None, - }, - target_keybind_source: KeybindSource::User, - }, - r#" - [ - { - "context": "Editor", - "bindings": { - "cmd-k cmd-u": "editor::ConvertToUpperCase", - "cmd-[": "pane::GoBack", - } - }, - ] - "#, - ); - } -} diff --git a/crates/settings/src/merge_from.rs b/crates/settings/src/merge_from.rs deleted file mode 100644 index 30ad0d3671..0000000000 --- a/crates/settings/src/merge_from.rs +++ /dev/null @@ -1,176 +0,0 @@ -/// Trait for recursively merging settings structures. -/// -/// When Zed starts it loads settings from `default.json` to initialize -/// everything. These may be further refined by loading the user's settings, -/// and any settings profiles; and then further refined by loading any -/// local project settings. -/// -/// The default behaviour of merging is: -/// * For objects with named keys (HashMap, structs, etc.). The values are merged deeply -/// (so if the default settings has languages.JSON.prettier.allowed = true, and the user's settings has -/// languages.JSON.tab_size = 4; the merged settings file will have both settings). -/// * For options, a None value is ignored, but Some values are merged recursively. -/// * For other types (including Vec), a merge overwrites the current value. -/// -/// If you want to break the rules you can (e.g. ExtendingVec, or SaturatingBool). -#[allow(unused)] -pub trait MergeFrom { - /// Merge from a source of the same type. - fn merge_from(&mut self, other: &Self); - - /// Merge from an optional source of the same type. - fn merge_from_option(&mut self, other: Option<&Self>) { - if let Some(other) = other { - self.merge_from(other); - } - } -} - -macro_rules! merge_from_overwrites { - ($($type:ty),+) => { - $( - impl MergeFrom for $type { - fn merge_from(&mut self, other: &Self) { - *self = other.clone(); - } - } - )+ - } -} - -merge_from_overwrites!( - u16, - u32, - u64, - usize, - i16, - i32, - i64, - bool, - f64, - f32, - char, - std::num::NonZeroUsize, - std::num::NonZeroU32, - String, - std::sync::Arc, - gpui::SharedString, - std::path::PathBuf, - gpui::Modifiers, - gpui::FontFeatures, - gpui::FontWeight -); - -impl MergeFrom for Option { - fn merge_from(&mut self, other: &Self) { - let Some(other) = other else { - return; - }; - if let Some(this) = self { - this.merge_from(other); - } else { - self.replace(other.clone()); - } - } -} - -impl MergeFrom for Vec { - fn merge_from(&mut self, other: &Self) { - *self = other.clone() - } -} - -impl MergeFrom for Box { - fn merge_from(&mut self, other: &Self) { - self.as_mut().merge_from(other.as_ref()) - } -} - -// Implementations for collections that extend/merge their contents -impl MergeFrom for collections::HashMap -where - K: Clone + std::hash::Hash + Eq, - V: Clone + MergeFrom, -{ - fn merge_from(&mut self, other: &Self) { - for (k, v) in other { - if let Some(existing) = self.get_mut(k) { - existing.merge_from(v); - } else { - self.insert(k.clone(), v.clone()); - } - } - } -} - -impl MergeFrom for collections::BTreeMap -where - K: Clone + std::hash::Hash + Eq + Ord, - V: Clone + MergeFrom, -{ - fn merge_from(&mut self, other: &Self) { - for (k, v) in other { - if let Some(existing) = self.get_mut(k) { - existing.merge_from(v); - } else { - self.insert(k.clone(), v.clone()); - } - } - } -} - -impl MergeFrom for collections::IndexMap -where - K: std::hash::Hash + Eq + Clone, - // Q: ?Sized + std::hash::Hash + collections::Equivalent + Eq, - V: Clone + MergeFrom, -{ - fn merge_from(&mut self, other: &Self) { - for (k, v) in other { - if let Some(existing) = self.get_mut(k) { - existing.merge_from(v); - } else { - self.insert(k.clone(), v.clone()); - } - } - } -} - -impl MergeFrom for collections::BTreeSet -where - T: Clone + Ord, -{ - fn merge_from(&mut self, other: &Self) { - for item in other { - self.insert(item.clone()); - } - } -} - -impl MergeFrom for collections::HashSet -where - T: Clone + std::hash::Hash + Eq, -{ - fn merge_from(&mut self, other: &Self) { - for item in other { - self.insert(item.clone()); - } - } -} - -impl MergeFrom for serde_json::Value { - fn merge_from(&mut self, other: &Self) { - match (self, other) { - (serde_json::Value::Object(this), serde_json::Value::Object(other)) => { - for (k, v) in other { - if let Some(existing) = this.get_mut(k) { - existing.merge_from(v); - } else { - this.insert(k.clone(), v.clone()); - } - } - } - (this, other) => *this = other.clone(), - } - } -} diff --git a/crates/settings/src/serde_helper.rs b/crates/settings/src/serde_helper.rs deleted file mode 100644 index 1c1826abd4..0000000000 --- a/crates/settings/src/serde_helper.rs +++ /dev/null @@ -1,135 +0,0 @@ -use serde::Serializer; - -/// Serializes an f32 value with 2 decimal places of precision. -/// -/// This function rounds the value to 2 decimal places and formats it as a string, -/// then parses it back to f64 before serialization. This ensures clean JSON output -/// without IEEE 754 floating-point artifacts. -/// -/// # Arguments -/// -/// * `value` - The f32 value to serialize -/// * `serializer` - The serde serializer to use -/// -/// # Returns -/// -/// Result of the serialization operation -/// -/// # Usage -/// -/// This function can be used with Serde's `serialize_with` attribute: -/// ``` -/// use serde::Serialize; -/// use settings::serialize_f32_with_two_decimal_places; -/// -/// #[derive(Serialize)] -/// struct ExampleStruct(#[serde(serialize_with = "serialize_f32_with_two_decimal_places")] f32); -/// ``` -pub fn serialize_f32_with_two_decimal_places( - value: &f32, - serializer: S, -) -> Result -where - S: Serializer, -{ - let rounded = (value * 100.0).round() / 100.0; - let formatted = format!("{:.2}", rounded); - let clean_value: f64 = formatted.parse().unwrap_or(rounded as f64); - serializer.serialize_f64(clean_value) -} - -/// Serializes an optional f32 value with 2 decimal places of precision. -/// -/// This function handles `Option` types, serializing `Some` values with 2 decimal -/// places of precision and `None` values as null. For `Some` values, it rounds to 2 decimal -/// places and formats as a string, then parses back to f64 before serialization. This ensures -/// clean JSON output without IEEE 754 floating-point artifacts. -/// -/// # Arguments -/// -/// * `value` - The optional f32 value to serialize -/// * `serializer` - The serde serializer to use -/// -/// # Returns -/// -/// Result of the serialization operation -/// -/// # Behavior -/// -/// * `Some(v)` - Serializes the value rounded to 2 decimal places -/// * `None` - Serializes as JSON null -/// -/// # Usage -/// -/// This function can be used with Serde's `serialize_with` attribute: -/// ``` -/// use serde::Serialize; -/// use settings::serialize_optional_f32_with_two_decimal_places; -/// -/// #[derive(Serialize)] -/// struct ExampleStruct { -/// #[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")] -/// optional_value: Option, -/// } -/// ``` -pub fn serialize_optional_f32_with_two_decimal_places( - value: &Option, - serializer: S, -) -> Result -where - S: Serializer, -{ - match value { - Some(v) => { - let rounded = (v * 100.0).round() / 100.0; - let formatted = format!("{:.2}", rounded); - let clean_value: f64 = formatted.parse().unwrap_or(rounded as f64); - serializer.serialize_some(&clean_value) - } - None => serializer.serialize_none(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde::{Deserialize, Serialize}; - - #[derive(Serialize, Deserialize)] - struct TestOptional { - #[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")] - value: Option, - } - - #[derive(Serialize, Deserialize)] - struct TestNonOptional { - #[serde(serialize_with = "serialize_f32_with_two_decimal_places")] - value: f32, - } - - #[test] - fn test_serialize_optional_f32_with_two_decimal_places() { - let cases = [ - (Some(123.456789), r#"{"value":123.46}"#), - (Some(1.2), r#"{"value":1.2}"#), - (Some(300.00000), r#"{"value":300.0}"#), - ]; - for (value, expected) in cases { - let value = TestOptional { value }; - assert_eq!(serde_json::to_string(&value).unwrap(), expected); - } - } - - #[test] - fn test_serialize_f32_with_two_decimal_places() { - let cases = [ - (123.456789, r#"{"value":123.46}"#), - (1.200, r#"{"value":1.2}"#), - (300.00000, r#"{"value":300.0}"#), - ]; - for (value, expected) in cases { - let value = TestNonOptional { value }; - assert_eq!(serde_json::to_string(&value).unwrap(), expected); - } - } -} diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs deleted file mode 100644 index 5f07ebe52f..0000000000 --- a/crates/settings/src/settings.rs +++ /dev/null @@ -1,144 +0,0 @@ -mod base_keymap_setting; -mod editable_setting_control; -mod fallible_options; -mod keymap_file; -pub mod merge_from; -mod serde_helper; -mod settings_content; -mod settings_file; -mod settings_store; -mod vscode_import; - -pub use settings_content::*; -pub use settings_macros::RegisterSetting; - -#[doc(hidden)] -pub mod private { - pub use crate::settings_store::{RegisteredSetting, SettingValue}; - pub use inventory; -} - -use gpui::{App, Global}; -use rust_embed::RustEmbed; -use std::{borrow::Cow, fmt, str}; -use util::asset_str; - -pub use base_keymap_setting::*; -pub use editable_setting_control::*; -pub use keymap_file::{ - KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation, - KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult, -}; -pub use serde_helper::*; -pub use settings_file::*; -pub use settings_json::*; -pub use settings_store::{ - InvalidSettingsError, LocalSettingsKind, MigrationStatus, ParseStatus, Settings, SettingsFile, - SettingsJsonSchemaParams, SettingsKey, SettingsLocation, SettingsParseResult, SettingsStore, -}; - -pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource}; - -pub use keymap_file::ActionSequence; - -#[derive(Clone, Debug, PartialEq)] -pub struct ActiveSettingsProfileName(pub String); - -impl Global for ActiveSettingsProfileName {} - -#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord, serde::Serialize)] -pub struct WorktreeId(usize); - -impl From for usize { - fn from(value: WorktreeId) -> Self { - value.0 - } -} - -impl WorktreeId { - pub fn from_usize(handle_id: usize) -> Self { - Self(handle_id) - } - - pub fn from_proto(id: u64) -> Self { - Self(id as usize) - } - - pub fn to_proto(self) -> u64 { - self.0 as u64 - } - - pub fn to_usize(self) -> usize { - self.0 - } -} - -impl fmt::Display for WorktreeId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - std::fmt::Display::fmt(&self.0, f) - } -} - -#[derive(RustEmbed)] -#[folder = "../../assets"] -#[include = "settings/*"] -#[include = "keymaps/*"] -#[exclude = "*.DS_Store"] -pub struct SettingsAssets; - -pub fn init(cx: &mut App) { - let settings = SettingsStore::new(cx, &default_settings()); - cx.set_global(settings); - SettingsStore::observe_active_settings_profile_name(cx).detach(); -} - -pub fn default_settings() -> Cow<'static, str> { - asset_str::("settings/default.json") -} - -#[cfg(target_os = "macos")] -pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-macos.json"; - -#[cfg(target_os = "windows")] -pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-windows.json"; - -#[cfg(not(any(target_os = "macos", target_os = "windows")))] -pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-linux.json"; - -pub fn default_keymap() -> Cow<'static, str> { - asset_str::(DEFAULT_KEYMAP_PATH) -} - -pub const VIM_KEYMAP_PATH: &str = "keymaps/vim.json"; - -pub fn vim_keymap() -> Cow<'static, str> { - asset_str::(VIM_KEYMAP_PATH) -} - -pub fn initial_user_settings_content() -> Cow<'static, str> { - asset_str::("settings/initial_user_settings.json") -} - -pub fn initial_server_settings_content() -> Cow<'static, str> { - asset_str::("settings/initial_server_settings.json") -} - -pub fn initial_project_settings_content() -> Cow<'static, str> { - asset_str::("settings/initial_local_settings.json") -} - -pub fn initial_keymap_content() -> Cow<'static, str> { - asset_str::("keymaps/initial.json") -} - -pub fn initial_tasks_content() -> Cow<'static, str> { - asset_str::("settings/initial_tasks.json") -} - -pub fn initial_debug_tasks_content() -> Cow<'static, str> { - asset_str::("settings/initial_debug_tasks.json") -} - -pub fn initial_local_debug_tasks_content() -> Cow<'static, str> { - asset_str::("settings/initial_local_debug_tasks.json") -} diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs deleted file mode 100644 index 743e22b04d..0000000000 --- a/crates/settings/src/settings_content.rs +++ /dev/null @@ -1,1053 +0,0 @@ -mod agent; -mod editor; -mod extension; -mod language; -mod language_model; -mod project; -mod terminal; -mod theme; -mod workspace; - -pub use agent::*; -pub use editor::*; -pub use extension::*; -pub use language::*; -pub use language_model::*; -pub use project::*; -pub use terminal::*; -pub use theme::*; -pub use workspace::*; - -use collections::{HashMap, IndexMap}; -use gpui::{App, SharedString}; -use release_channel::ReleaseChannel; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings_macros::{MergeFrom, with_fallible_options}; -use std::collections::BTreeSet; -use std::env; -use std::sync::Arc; -pub use util::serde::default_true; - -use crate::{ActiveSettingsProfileName, merge_from}; - -#[with_fallible_options] -#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct SettingsContent { - #[serde(flatten)] - pub project: ProjectSettingsContent, - - #[serde(flatten)] - pub theme: Box, - - #[serde(flatten)] - pub extension: ExtensionSettingsContent, - - #[serde(flatten)] - pub workspace: WorkspaceSettingsContent, - - #[serde(flatten)] - pub editor: EditorSettingsContent, - - #[serde(flatten)] - pub remote: RemoteSettingsContent, - - /// Settings related to the file finder. - pub file_finder: Option, - - pub git_panel: Option, - - pub tabs: Option, - pub tab_bar: Option, - pub status_bar: Option, - - pub preview_tabs: Option, - - pub agent: Option, - pub agent_servers: Option, - - /// Configuration of audio in Zed. - pub audio: Option, - - /// Whether or not to automatically check for updates. - /// - /// Default: true - pub auto_update: Option, - - /// This base keymap settings adjusts the default keybindings in Zed to be similar - /// to other common code editors. By default, Zed's keymap closely follows VSCode's - /// keymap, with minor adjustments, this corresponds to the "VSCode" setting. - /// - /// Default: VSCode - pub base_keymap: Option, - - /// Configuration for the collab panel visual settings. - pub collaboration_panel: Option, - - pub debugger: Option, - - /// Configuration for Diagnostics-related features. - pub diagnostics: Option, - - /// Configuration for Git-related features - pub git: Option, - - /// Common language server settings. - pub global_lsp_settings: Option, - - /// The settings for the image viewer. - pub image_viewer: Option, - - pub repl: Option, - - /// Whether or not to enable Helix mode. - /// - /// Default: false - pub helix_mode: Option, - - pub journal: Option, - - /// A map of log scopes to the desired log level. - /// Useful for filtering out noisy logs or enabling more verbose logging. - /// - /// Example: {"log": {"client": "warn"}} - pub log: Option>, - - pub line_indicator_format: Option, - - pub language_models: Option, - - pub outline_panel: Option, - - pub project_panel: Option, - - /// Configuration for the Message Editor - pub message_editor: Option, - - /// Configuration for Node-related features - pub node: Option, - - /// Configuration for the Notification Panel - pub notification_panel: Option, - - pub proxy: Option, - - /// The URL of the Zed server to connect to. - pub server_url: Option, - - /// Configuration for session-related features - pub session: Option, - /// Control what info is collected by Zed. - pub telemetry: Option, - - /// Configuration of the terminal in Zed. - pub terminal: Option, - - pub title_bar: Option, - - /// Whether or not to enable Vim mode. - /// - /// Default: false - pub vim_mode: Option, - - // Settings related to calls in Zed - pub calls: Option, - - /// Whether to disable all AI features in Zed. - /// - /// Default: false - pub disable_ai: Option, - - /// Settings related to Vim mode in Zed. - pub vim: Option, -} - -impl SettingsContent { - pub fn languages_mut(&mut self) -> &mut HashMap { - &mut self.project.all_languages.languages.0 - } -} - -#[with_fallible_options] -#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct UserSettingsContent { - #[serde(flatten)] - pub content: Box, - - pub dev: Option>, - pub nightly: Option>, - pub preview: Option>, - pub stable: Option>, - - pub macos: Option>, - pub windows: Option>, - pub linux: Option>, - - #[serde(default)] - pub profiles: IndexMap, -} - -pub struct ExtensionsSettingsContent { - pub all_languages: AllLanguageSettingsContent, -} - -impl UserSettingsContent { - pub fn for_release_channel(&self) -> Option<&SettingsContent> { - match *release_channel::RELEASE_CHANNEL { - ReleaseChannel::Dev => self.dev.as_deref(), - ReleaseChannel::Nightly => self.nightly.as_deref(), - ReleaseChannel::Preview => self.preview.as_deref(), - ReleaseChannel::Stable => self.stable.as_deref(), - } - } - - pub fn for_os(&self) -> Option<&SettingsContent> { - match env::consts::OS { - "macos" => self.macos.as_deref(), - "linux" => self.linux.as_deref(), - "windows" => self.windows.as_deref(), - _ => None, - } - } - - pub fn for_profile(&self, cx: &App) -> Option<&SettingsContent> { - let Some(active_profile) = cx.try_global::() else { - return None; - }; - self.profiles.get(&active_profile.0) - } -} - -/// Base key bindings scheme. Base keymaps can be overridden with user keymaps. -/// -/// Default: VSCode -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - Default, - strum::VariantArray, -)] -pub enum BaseKeymapContent { - #[default] - VSCode, - JetBrains, - SublimeText, - Atom, - TextMate, - Emacs, - Cursor, - None, -} - -impl strum::VariantNames for BaseKeymapContent { - const VARIANTS: &'static [&'static str] = &[ - "VSCode", - "JetBrains", - "Sublime Text", - "Atom", - "TextMate", - "Emacs", - "Cursor", - "None", - ]; -} - -#[with_fallible_options] -#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)] -pub struct TitleBarSettingsContent { - /// Whether to show the branch icon beside branch switcher in the title bar. - /// - /// Default: false - pub show_branch_icon: Option, - /// Whether to show onboarding banners in the title bar. - /// - /// Default: true - pub show_onboarding_banner: Option, - /// Whether to show user avatar in the title bar. - /// - /// Default: true - pub show_user_picture: Option, - /// Whether to show the branch name button in the titlebar. - /// - /// Default: true - pub show_branch_name: Option, - /// Whether to show the project host and name in the titlebar. - /// - /// Default: true - pub show_project_items: Option, - /// Whether to show the sign in button in the title bar. - /// - /// Default: true - pub show_sign_in: Option, - /// Whether to show the menus in the title bar. - /// - /// Default: false - pub show_menus: Option, -} - -/// Configuration of audio in Zed. -#[with_fallible_options] -#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)] -pub struct AudioSettingsContent { - /// Opt into the new audio system. - /// - /// You need to rejoin a call for this setting to apply - #[serde(rename = "experimental.rodio_audio")] - pub rodio_audio: Option, // default is false - /// Requires 'rodio_audio: true' - /// - /// Automatically increase or decrease you microphone's volume. This affects how - /// loud you sound to others. - /// - /// Recommended: off (default) - /// Microphones are too quite in zed, until everyone is on experimental - /// audio and has auto speaker volume on this will make you very loud - /// compared to other speakers. - #[serde(rename = "experimental.auto_microphone_volume")] - pub auto_microphone_volume: Option, - /// Requires 'rodio_audio: true' - /// - /// Automatically increate or decrease the volume of other call members. - /// This only affects how things sound for you. - #[serde(rename = "experimental.auto_speaker_volume")] - pub auto_speaker_volume: Option, - /// Requires 'rodio_audio: true' - /// - /// Remove background noises. Works great for typing, cars, dogs, AC. Does - /// not work well on music. - #[serde(rename = "experimental.denoise")] - pub denoise: Option, - /// Requires 'rodio_audio: true' - /// - /// Use audio parameters compatible with the previous versions of - /// experimental audio and non-experimental audio. When this is false you - /// will sound strange to anyone not on the latest experimental audio. In - /// the future we will migrate by setting this to false - /// - /// You need to rejoin a call for this setting to apply - #[serde(rename = "experimental.legacy_audio_compatible")] - pub legacy_audio_compatible: Option, -} - -/// Control what info is collected by Zed. -#[with_fallible_options] -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Debug, MergeFrom)] -pub struct TelemetrySettingsContent { - /// Send debug info like crash reports. - /// - /// Default: true - pub diagnostics: Option, - /// Send anonymized usage data like what languages you're using Zed with. - /// - /// Default: true - pub metrics: Option, -} - -impl Default for TelemetrySettingsContent { - fn default() -> Self { - Self { - diagnostics: Some(true), - metrics: Some(true), - } - } -} - -#[with_fallible_options] -#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Clone, MergeFrom)] -pub struct DebuggerSettingsContent { - /// Determines the stepping granularity. - /// - /// Default: line - pub stepping_granularity: Option, - /// Whether the breakpoints should be reused across Zed sessions. - /// - /// Default: true - pub save_breakpoints: Option, - /// Whether to show the debug button in the status bar. - /// - /// Default: true - pub button: Option, - /// Time in milliseconds until timeout error when connecting to a TCP debug adapter - /// - /// Default: 2000ms - pub timeout: Option, - /// Whether to log messages between active debug adapters and Zed - /// - /// Default: true - pub log_dap_communications: Option, - /// Whether to format dap messages in when adding them to debug adapter logger - /// - /// Default: true - pub format_dap_log_messages: Option, - /// The dock position of the debug panel - /// - /// Default: Bottom - pub dock: Option, -} - -/// The granularity of one 'step' in the stepping requests `next`, `stepIn`, `stepOut`, and `stepBack`. -#[derive( - PartialEq, - Eq, - Debug, - Hash, - Clone, - Copy, - Deserialize, - Serialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum SteppingGranularity { - /// The step should allow the program to run until the current statement has finished executing. - /// The meaning of a statement is determined by the adapter and it may be considered equivalent to a line. - /// For example 'for(int i = 0; i < 10; i++)' could be considered to have 3 statements 'int i = 0', 'i < 10', and 'i++'. - Statement, - /// The step should allow the program to run until the current source line has executed. - Line, - /// The step should allow one instruction to execute (e.g. one x86 instruction). - Instruction, -} - -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum DockPosition { - Left, - Bottom, - Right, -} - -/// Settings for slash commands. -#[with_fallible_options] -#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, MergeFrom, PartialEq, Eq)] -pub struct SlashCommandSettings { - /// Settings for the `/cargo-workspace` slash command. - pub cargo_workspace: Option, -} - -/// Settings for the `/cargo-workspace` slash command. -#[with_fallible_options] -#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, MergeFrom, PartialEq, Eq)] -pub struct CargoWorkspaceCommandSettings { - /// Whether `/cargo-workspace` is enabled. - pub enabled: Option, -} - -/// Configuration of voice calls in Zed. -#[with_fallible_options] -#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)] -pub struct CallSettingsContent { - /// Whether the microphone should be muted when joining a channel or a call. - /// - /// Default: false - pub mute_on_join: Option, - - /// Whether your current project should be shared when joining an empty channel. - /// - /// Default: false - pub share_on_join: Option, -} - -#[with_fallible_options] -#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)] -pub struct GitPanelSettingsContent { - /// Whether to show the panel button in the status bar. - /// - /// Default: true - pub button: Option, - /// Where to dock the panel. - /// - /// Default: left - pub dock: Option, - /// Default width of the panel in pixels. - /// - /// Default: 360 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_width: Option, - /// How entry statuses are displayed. - /// - /// Default: icon - pub status_style: Option, - /// How and when the scrollbar should be displayed. - /// - /// Default: inherits editor scrollbar settings - pub scrollbar: Option, - - /// What the default branch name should be when - /// `init.defaultBranch` is not set in git - /// - /// Default: main - pub fallback_branch_name: Option, - - /// Whether to sort entries in the panel by path - /// or by status (the default). - /// - /// Default: false - pub sort_by_path: Option, - - /// Whether to collapse untracked files in the diff panel. - /// - /// Default: false - pub collapse_untracked_diff: Option, - - /// Whether to show entries with tree or flat view in the panel - /// - /// Default: false - pub tree_view: Option, -} - -#[derive( - Default, - Copy, - Clone, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum StatusStyle { - #[default] - Icon, - LabelColor, -} - -#[with_fallible_options] -#[derive( - Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, -)] -pub struct ScrollbarSettings { - pub show: Option, -} - -#[with_fallible_options] -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)] -pub struct NotificationPanelSettingsContent { - /// Whether to show the panel button in the status bar. - /// - /// Default: true - pub button: Option, - /// Where to dock the panel. - /// - /// Default: right - pub dock: Option, - /// Default width of the panel in pixels. - /// - /// Default: 300 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_width: Option, -} - -#[with_fallible_options] -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)] -pub struct PanelSettingsContent { - /// Whether to show the panel button in the status bar. - /// - /// Default: true - pub button: Option, - /// Where to dock the panel. - /// - /// Default: left - pub dock: Option, - /// Default width of the panel in pixels. - /// - /// Default: 240 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_width: Option, -} - -#[with_fallible_options] -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)] -pub struct MessageEditorSettings { - /// Whether to automatically replace emoji shortcodes with emoji characters. - /// For example: typing `:wave:` gets replaced with `👋`. - /// - /// Default: false - pub auto_replace_emoji_shortcode: Option, -} - -#[with_fallible_options] -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)] -pub struct FileFinderSettingsContent { - /// Whether to show file icons in the file finder. - /// - /// Default: true - pub file_icons: Option, - /// Determines how much space the file finder can take up in relation to the available window width. - /// - /// Default: small - pub modal_max_width: Option, - /// Determines whether the file finder should skip focus for the active file in search results. - /// - /// Default: true - pub skip_focus_for_active_in_search: Option, - /// Determines whether to show the git status in the file finder - /// - /// Default: true - pub git_status: Option, - /// Whether to use gitignored files when searching. - /// Only the file Zed had indexed will be used, not necessary all the gitignored files. - /// - /// Default: Smart - pub include_ignored: Option, -} - -#[derive( - Debug, - PartialEq, - Eq, - Clone, - Copy, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum IncludeIgnoredContent { - /// Use all gitignored files - All, - /// Use only the files Zed had indexed - Indexed, - /// Be smart and search for ignored when called from a gitignored worktree - #[default] - Smart, -} - -#[derive( - Debug, - PartialEq, - Eq, - Clone, - Copy, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "lowercase")] -pub enum FileFinderWidthContent { - #[default] - Small, - Medium, - Large, - XLarge, - Full, -} - -#[with_fallible_options] -#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Debug, JsonSchema, MergeFrom)] -pub struct VimSettingsContent { - pub default_mode: Option, - pub toggle_relative_line_numbers: Option, - pub use_system_clipboard: Option, - pub use_smartcase_find: Option, - pub custom_digraphs: Option>>, - pub highlight_on_yank_duration: Option, - pub cursor_shape: Option, -} - -#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Debug)] -#[serde(rename_all = "snake_case")] -pub enum ModeContent { - #[default] - Normal, - Insert, -} - -/// Controls when to use system clipboard. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub enum UseSystemClipboard { - /// Don't use system clipboard. - Never, - /// Use system clipboard. - Always, - /// Use system clipboard for yank operations. - OnYank, -} - -/// The settings for cursor shape. -#[with_fallible_options] -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] -pub struct CursorShapeSettings { - /// Cursor shape for the normal mode. - /// - /// Default: block - pub normal: Option, - /// Cursor shape for the replace mode. - /// - /// Default: underline - pub replace: Option, - /// Cursor shape for the visual mode. - /// - /// Default: block - pub visual: Option, - /// Cursor shape for the insert mode. - /// - /// The default value follows the primary cursor_shape. - pub insert: Option, -} - -/// Settings specific to journaling -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct JournalSettingsContent { - /// The path of the directory where journal entries are stored. - /// - /// Default: `~` - pub path: Option, - /// What format to display the hours in. - /// - /// Default: hour12 - pub hour_format: Option, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum HourFormat { - #[default] - Hour12, - Hour24, -} - -#[with_fallible_options] -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)] -pub struct OutlinePanelSettingsContent { - /// Whether to show the outline panel button in the status bar. - /// - /// Default: true - pub button: Option, - /// Customize default width (in pixels) taken by outline panel - /// - /// Default: 240 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_width: Option, - /// The position of outline panel - /// - /// Default: left - pub dock: Option, - /// Whether to show file icons in the outline panel. - /// - /// Default: true - pub file_icons: Option, - /// Whether to show folder icons or chevrons for directories in the outline panel. - /// - /// Default: true - pub folder_icons: Option, - /// Whether to show the git status in the outline panel. - /// - /// Default: true - pub git_status: Option, - /// Amount of indentation (in pixels) for nested items. - /// - /// Default: 20 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub indent_size: Option, - /// Whether to reveal it in the outline panel automatically, - /// when a corresponding project entry becomes active. - /// Gitignored entries are never auto revealed. - /// - /// Default: true - pub auto_reveal_entries: Option, - /// Whether to fold directories automatically - /// when directory has only one directory inside. - /// - /// Default: true - pub auto_fold_dirs: Option, - /// Settings related to indent guides in the outline panel. - pub indent_guides: Option, - /// Scrollbar-related settings - pub scrollbar: Option, - /// Default depth to expand outline items in the current file. - /// The default depth to which outline entries are expanded on reveal. - /// - Set to 0 to collapse all items that have children - /// - Set to 1 or higher to collapse items at that depth or deeper - /// - /// Default: 100 - pub expand_outlines_with_depth: Option, -} - -#[derive( - Clone, - Copy, - Debug, - PartialEq, - Eq, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum DockSide { - Left, - Right, -} - -#[derive( - Copy, - Clone, - Debug, - PartialEq, - Eq, - Deserialize, - Serialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ShowIndentGuides { - Always, - Never, -} - -#[with_fallible_options] -#[derive( - Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default, -)] -pub struct IndentGuidesSettingsContent { - /// When to show the scrollbar in the outline panel. - pub show: Option, -} - -#[derive(Clone, Copy, Default, PartialEq, Debug, JsonSchema, MergeFrom, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum LineIndicatorFormat { - Short, - #[default] - Long, -} - -/// The settings for the image viewer. -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, Default, PartialEq)] -pub struct ImageViewerSettingsContent { - /// The unit to use for displaying image file sizes. - /// - /// Default: "binary" - pub unit: Option, -} - -#[with_fallible_options] -#[derive( - Clone, - Copy, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - Default, - PartialEq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ImageFileSizeUnit { - /// Displays file size in binary units (e.g., KiB, MiB). - #[default] - Binary, - /// Displays file size in decimal units (e.g., KB, MB). - Decimal, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct RemoteSettingsContent { - pub ssh_connections: Option>, - pub wsl_connections: Option>, - pub dev_container_connections: Option>, - pub read_ssh_config: Option, -} - -#[with_fallible_options] -#[derive( - Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom, Hash, -)] -pub struct DevContainerConnection { - pub name: SharedString, - pub container_id: SharedString, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct SshConnection { - pub host: SharedString, - pub username: Option, - pub port: Option, - #[serde(default)] - pub args: Vec, - #[serde(default)] - pub projects: collections::BTreeSet, - /// Name to use for this server in UI. - pub nickname: Option, - // By default Zed will download the binary to the host directly. - // If this is set to true, Zed will download the binary to your local machine, - // and then upload it over the SSH connection. Useful if your SSH server has - // limited outbound internet access. - pub upload_binary_over_ssh: Option, - - pub port_forwards: Option>, -} - -#[derive(Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom, Debug)] -pub struct WslConnection { - pub distro_name: SharedString, - pub user: Option, - #[serde(default)] - pub projects: BTreeSet, -} - -#[with_fallible_options] -#[derive( - Clone, Debug, Default, Serialize, PartialEq, Eq, PartialOrd, Ord, Deserialize, JsonSchema, -)] -pub struct RemoteProject { - pub paths: Vec, -} - -#[with_fallible_options] -#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema, MergeFrom)] -pub struct SshPortForwardOption { - pub local_host: Option, - pub local_port: u16, - pub remote_host: Option, - pub remote_port: u16, -} - -/// Settings for configuring REPL display and behavior. -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct ReplSettingsContent { - /// Maximum number of lines to keep in REPL's scrollback buffer. - /// Clamped with [4, 256] range. - /// - /// Default: 32 - pub max_lines: Option, - /// Maximum number of columns to keep in REPL's scrollback buffer. - /// Clamped with [20, 512] range. - /// - /// Default: 128 - pub max_columns: Option, -} - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -/// An ExtendingVec in the settings can only accumulate new values. -/// -/// This is useful for things like private files where you only want -/// to allow new values to be added. -/// -/// Consider using a HashMap instead of this type -/// (like auto_install_extensions) so that user settings files can both add -/// and remove values from the set. -pub struct ExtendingVec(pub Vec); - -impl Into> for ExtendingVec { - fn into(self) -> Vec { - self.0 - } -} -impl From> for ExtendingVec { - fn from(vec: Vec) -> Self { - ExtendingVec(vec) - } -} - -impl merge_from::MergeFrom for ExtendingVec { - fn merge_from(&mut self, other: &Self) { - self.0.extend_from_slice(other.0.as_slice()); - } -} - -/// A SaturatingBool in the settings can only ever be set to true, -/// later attempts to set it to false will be ignored. -/// -/// Used by `disable_ai`. -#[derive(Debug, Default, Copy, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct SaturatingBool(pub bool); - -impl From for SaturatingBool { - fn from(value: bool) -> Self { - SaturatingBool(value) - } -} - -impl From for bool { - fn from(value: SaturatingBool) -> bool { - value.0 - } -} - -impl merge_from::MergeFrom for SaturatingBool { - fn merge_from(&mut self, other: &Self) { - self.0 |= other.0 - } -} - -#[derive( - Copy, - Clone, - Default, - Debug, - PartialEq, - Eq, - PartialOrd, - Ord, - Serialize, - Deserialize, - MergeFrom, - JsonSchema, - derive_more::FromStr, -)] -#[serde(transparent)] -pub struct DelayMs(pub u64); - -impl From for DelayMs { - fn from(n: u64) -> Self { - Self(n) - } -} - -impl std::fmt::Display for DelayMs { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}ms", self.0) - } -} diff --git a/crates/settings/src/settings_content/agent.rs b/crates/settings/src/settings_content/agent.rs deleted file mode 100644 index 2ea9f0cd57..0000000000 --- a/crates/settings/src/settings_content/agent.rs +++ /dev/null @@ -1,379 +0,0 @@ -use collections::{HashMap, IndexMap}; -use gpui::SharedString; -use schemars::{JsonSchema, json_schema}; -use serde::{Deserialize, Serialize}; -use settings_macros::{MergeFrom, with_fallible_options}; -use std::{borrow::Cow, path::PathBuf, sync::Arc}; - -use crate::DockPosition; - -#[with_fallible_options] -#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)] -pub struct AgentSettingsContent { - /// Whether the Agent is enabled. - /// - /// Default: true - pub enabled: Option, - /// Whether to show the agent panel button in the status bar. - /// - /// Default: true - pub button: Option, - /// Where to dock the agent panel. - /// - /// Default: right - pub dock: Option, - /// Default width in pixels when the agent panel is docked to the left or right. - /// - /// Default: 640 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_width: Option, - /// Default height in pixels when the agent panel is docked to the bottom. - /// - /// Default: 320 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_height: Option, - /// The default model to use when creating new chats and for other features when a specific model is not specified. - pub default_model: Option, - /// Model to use for the inline assistant. Defaults to default_model when not specified. - pub inline_assistant_model: Option, - /// Model to use for generating git commit messages. Defaults to default_model when not specified. - pub commit_message_model: Option, - /// Model to use for generating thread summaries. Defaults to default_model when not specified. - pub thread_summary_model: Option, - /// Additional models with which to generate alternatives when performing inline assists. - pub inline_alternatives: Option>, - /// The default profile to use in the Agent. - /// - /// Default: write - pub default_profile: Option>, - /// Which view type to show by default in the agent panel. - /// - /// Default: "thread" - pub default_view: Option, - /// The available agent profiles. - pub profiles: Option, AgentProfileContent>>, - /// Whenever a tool action would normally wait for your confirmation - /// that you allow it, always choose to allow it. - /// - /// This setting has no effect on external agents that support permission modes, such as Claude Code. - /// - /// Set `agent_servers.claude.default_mode` to `bypassPermissions`, to disable all permission requests when using Claude Code. - /// - /// Default: false - pub always_allow_tool_actions: Option, - /// Where to show a popup notification when the agent is waiting for user input. - /// - /// Default: "primary_screen" - pub notify_when_agent_waiting: Option, - /// Whether to play a sound when the agent has either completed its response, or needs user input. - /// - /// Default: false - pub play_sound_when_agent_done: Option, - /// Whether to display agent edits in single-file editors in addition to the review multibuffer pane. - /// - /// Default: true - pub single_file_review: Option, - /// Additional parameters for language model requests. When making a request - /// to a model, parameters will be taken from the last entry in this list - /// that matches the model's provider and name. In each entry, both provider - /// and model are optional, so that you can specify parameters for either - /// one. - /// - /// Default: [] - #[serde(default)] - pub model_parameters: Vec, - /// What completion mode to enable for new threads - /// - /// Default: normal - pub preferred_completion_mode: Option, - /// Whether to show thumb buttons for feedback in the agent panel. - /// - /// Default: true - pub enable_feedback: Option, - /// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff. - /// - /// Default: true - pub expand_edit_card: Option, - /// Whether to have terminal cards in the agent panel expanded, showing the whole command output. - /// - /// Default: true - pub expand_terminal_card: Option, - /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel. - /// - /// Default: false - pub use_modifier_to_send: Option, - /// Minimum number of lines of height the agent message editor should have. - /// - /// Default: 4 - pub message_editor_min_lines: Option, -} - -impl AgentSettingsContent { - pub fn set_dock(&mut self, dock: DockPosition) { - self.dock = Some(dock); - } - - pub fn set_model(&mut self, language_model: LanguageModelSelection) { - // let model = language_model.id().0.to_string(); - // let provider = language_model.provider_id().0.to_string(); - // self.default_model = Some(LanguageModelSelection { - // provider: provider.into(), - // model, - // }); - self.default_model = Some(language_model) - } - - pub fn set_inline_assistant_model(&mut self, provider: String, model: String) { - self.inline_assistant_model = Some(LanguageModelSelection { - provider: provider.into(), - model, - }); - } - - pub fn set_commit_message_model(&mut self, provider: String, model: String) { - self.commit_message_model = Some(LanguageModelSelection { - provider: provider.into(), - model, - }); - } - - pub fn set_thread_summary_model(&mut self, provider: String, model: String) { - self.thread_summary_model = Some(LanguageModelSelection { - provider: provider.into(), - model, - }); - } - - pub fn set_always_allow_tool_actions(&mut self, allow: bool) { - self.always_allow_tool_actions = Some(allow); - } - - pub fn set_play_sound_when_agent_done(&mut self, allow: bool) { - self.play_sound_when_agent_done = Some(allow); - } - - pub fn set_single_file_review(&mut self, allow: bool) { - self.single_file_review = Some(allow); - } - - pub fn set_use_modifier_to_send(&mut self, always_use: bool) { - self.use_modifier_to_send = Some(always_use); - } - - pub fn set_profile(&mut self, profile_id: Arc) { - self.default_profile = Some(profile_id); - } -} - -#[with_fallible_options] -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct AgentProfileContent { - pub name: Arc, - #[serde(default)] - pub tools: IndexMap, bool>, - /// Whether all context servers are enabled by default. - pub enable_all_context_servers: Option, - #[serde(default)] - pub context_servers: IndexMap, ContextServerPresetContent>, - /// The default language model selected when using this profile. - pub default_model: Option, -} - -#[with_fallible_options] -#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct ContextServerPresetContent { - pub tools: IndexMap, bool>, -} - -#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub enum DefaultAgentView { - #[default] - Thread, - TextThread, -} - -#[derive( - Copy, - Clone, - Default, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum NotifyWhenAgentWaiting { - #[default] - PrimaryScreen, - AllScreens, - Never, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct LanguageModelSelection { - pub provider: LanguageModelProviderSetting, - pub model: String, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Default)] -#[serde(rename_all = "snake_case")] -pub enum CompletionMode { - #[default] - Normal, - #[serde(alias = "max")] - Burn, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct LanguageModelParameters { - pub provider: Option, - pub model: Option, - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub temperature: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, MergeFrom)] -pub struct LanguageModelProviderSetting(pub String); - -impl JsonSchema for LanguageModelProviderSetting { - fn schema_name() -> Cow<'static, str> { - "LanguageModelProviderSetting".into() - } - - fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { - // list the builtin providers as a subset so that we still auto complete them in the settings - json_schema!({ - "anyOf": [ - { - "type": "string", - "enum": [ - "amazon-bedrock", - "anthropic", - "copilot_chat", - "deepseek", - "google", - "lmstudio", - "mistral", - "ollama", - "openai", - "openrouter", - "vercel", - "x_ai", - "zed.dev" - ] - }, - { - "type": "string", - } - ] - }) - } -} - -impl From for LanguageModelProviderSetting { - fn from(provider: String) -> Self { - Self(provider) - } -} - -impl From<&str> for LanguageModelProviderSetting { - fn from(provider: &str) -> Self { - Self(provider.to_string()) - } -} - -#[with_fallible_options] -#[derive(Default, PartialEq, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug)] -pub struct AllAgentServersSettings { - pub gemini: Option, - pub claude: Option, - pub codex: Option, - - /// Custom agent servers configured by the user - #[serde(flatten)] - pub custom: HashMap, -} - -#[with_fallible_options] -#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)] -pub struct BuiltinAgentServerSettings { - /// Absolute path to a binary to be used when launching this agent. - /// - /// This can be used to run a specific binary without automatic downloads or searching `$PATH`. - #[serde(rename = "command")] - pub path: Option, - /// If a binary is specified in `command`, it will be passed these arguments. - pub args: Option>, - /// If a binary is specified in `command`, it will be passed these environment variables. - pub env: Option>, - /// Whether to skip searching `$PATH` for an agent server binary when - /// launching this agent. - /// - /// This has no effect if a `command` is specified. Otherwise, when this is - /// `false`, Zed will search `$PATH` for an agent server binary and, if one - /// is found, use it for threads with this agent. If no agent binary is - /// found on `$PATH`, Zed will automatically install and use its own binary. - /// When this is `true`, Zed will not search `$PATH`, and will always use - /// its own binary. - /// - /// Default: true - pub ignore_system_version: Option, - /// The default mode to use for this agent. - /// - /// Note: Not only all agents support modes. - /// - /// Default: None - pub default_mode: Option, - /// The default model to use for this agent. - /// - /// This should be the model ID as reported by the agent. - /// - /// Default: None - pub default_model: Option, -} - -#[with_fallible_options] -#[derive(Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum CustomAgentServerSettings { - Custom { - #[serde(rename = "command")] - path: PathBuf, - #[serde(default)] - args: Vec, - env: Option>, - /// The default mode to use for this agent. - /// - /// Note: Not only all agents support modes. - /// - /// Default: None - default_mode: Option, - /// The default model to use for this agent. - /// - /// This should be the model ID as reported by the agent. - /// - /// Default: None - default_model: Option, - }, - Extension { - /// The default mode to use for this agent. - /// - /// Note: Not only all agents support modes. - /// - /// Default: None - default_mode: Option, - /// The default model to use for this agent. - /// - /// This should be the model ID as reported by the agent. - /// - /// Default: None - default_model: Option, - }, -} diff --git a/crates/settings/src/settings_content/editor.rs b/crates/settings/src/settings_content/editor.rs deleted file mode 100644 index 9ec5542e9b..0000000000 --- a/crates/settings/src/settings_content/editor.rs +++ /dev/null @@ -1,983 +0,0 @@ -use std::fmt::Display; -use std::num; - -use collections::HashMap; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings_macros::{MergeFrom, with_fallible_options}; - -use crate::{ - DelayMs, DiagnosticSeverityContent, ShowScrollbar, serialize_f32_with_two_decimal_places, -}; - -#[with_fallible_options] -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct EditorSettingsContent { - /// Whether the cursor blinks in the editor. - /// - /// Default: true - pub cursor_blink: Option, - /// Cursor shape for the default editor. - /// Can be "bar", "block", "underline", or "hollow". - /// - /// Default: bar - pub cursor_shape: Option, - /// Determines when the mouse cursor should be hidden in an editor or input box. - /// - /// Default: on_typing_and_movement - pub hide_mouse: Option, - /// Determines how snippets are sorted relative to other completion items. - /// - /// Default: inline - pub snippet_sort_order: Option, - /// How to highlight the current line in the editor. - /// - /// Default: all - pub current_line_highlight: Option, - /// Whether to highlight all occurrences of the selected text in an editor. - /// - /// Default: true - pub selection_highlight: Option, - /// Whether the text selection should have rounded corners. - /// - /// Default: true - pub rounded_selection: Option, - /// The debounce delay before querying highlights from the language - /// server based on the current cursor location. - /// - /// Default: 75 - pub lsp_highlight_debounce: Option, - /// Whether to show the informational hover box when moving the mouse - /// over symbols in the editor. - /// - /// Default: true - pub hover_popover_enabled: Option, - /// Time to wait in milliseconds before showing the informational hover box. - /// - /// Default: 300 - pub hover_popover_delay: Option, - /// Toolbar related settings - pub toolbar: Option, - /// Scrollbar related settings - pub scrollbar: Option, - /// Minimap related settings - pub minimap: Option, - /// Gutter related settings - pub gutter: Option, - /// Whether the editor will scroll beyond the last line. - /// - /// Default: one_page - pub scroll_beyond_last_line: Option, - /// The number of lines to keep above/below the cursor when auto-scrolling. - /// - /// Default: 3. - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub vertical_scroll_margin: Option, - /// Whether to scroll when clicking near the edge of the visible text area. - /// - /// Default: false - pub autoscroll_on_clicks: Option, - /// The number of characters to keep on either side when scrolling with the mouse. - /// - /// Default: 5. - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub horizontal_scroll_margin: Option, - /// Scroll sensitivity multiplier. This multiplier is applied - /// to both the horizontal and vertical delta values while scrolling. - /// - /// Default: 1.0 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub scroll_sensitivity: Option, - /// Scroll sensitivity multiplier for fast scrolling. This multiplier is applied - /// to both the horizontal and vertical delta values while scrolling. Fast scrolling - /// happens when a user holds the alt or option key while scrolling. - /// - /// Default: 4.0 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub fast_scroll_sensitivity: Option, - /// Settings for sticking scopes to the top of the editor. - /// - /// Default: sticky scroll is disabled - pub sticky_scroll: Option, - /// Whether the line numbers on editors gutter are relative or not. - /// When "enabled" shows relative number of buffer lines, when "wrapped" shows - /// relative number of display lines. - /// - /// Default: "disabled" - pub relative_line_numbers: Option, - /// When to populate a new search's query based on the text under the cursor. - /// - /// Default: always - pub seed_search_query_from_cursor: Option, - pub use_smartcase_search: Option, - /// Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier. - /// - /// Default: alt - pub multi_cursor_modifier: Option, - /// Hide the values of variables in `private` files, as defined by the - /// private_files setting. This only changes the visual representation, - /// the values are still present in the file and can be selected / copied / pasted - /// - /// Default: false - pub redact_private_values: Option, - - /// How many lines to expand the multibuffer excerpts by default - /// - /// Default: 3 - pub expand_excerpt_lines: Option, - - /// How many lines of context to provide in multibuffer excerpts by default - /// - /// Default: 2 - pub excerpt_context_lines: Option, - - /// Whether to enable middle-click paste on Linux - /// - /// Default: true - pub middle_click_paste: Option, - - /// What to do when multibuffer is double clicked in some of its excerpts - /// (parts of singleton buffers). - /// - /// Default: select - pub double_click_in_multibuffer: Option, - /// Whether the editor search results will loop - /// - /// Default: true - pub search_wrap: Option, - - /// Defaults to use when opening a new buffer and project search items. - /// - /// Default: nothing is enabled - pub search: Option, - - /// Whether to automatically show a signature help pop-up or not. - /// - /// Default: false - pub auto_signature_help: Option, - - /// Whether to show the signature help pop-up after completions or bracket pairs inserted. - /// - /// Default: false - pub show_signature_help_after_edits: Option, - /// The minimum APCA perceptual contrast to maintain when - /// rendering text over highlight backgrounds in the editor. - /// - /// Values range from 0 to 106. Set to 0 to disable adjustments. - /// Default: 45 - #[schemars(range(min = 0, max = 106))] - pub minimum_contrast_for_highlights: Option, - - /// Whether to follow-up empty go to definition responses from the language server or not. - /// `FindAllReferences` allows to look up references of the same symbol instead. - /// `None` disables the fallback. - /// - /// Default: FindAllReferences - pub go_to_definition_fallback: Option, - - /// Jupyter REPL settings. - pub jupyter: Option, - - /// Which level to use to filter out diagnostics displayed in the editor. - /// - /// Affects the editor rendering only, and does not interrupt - /// the functionality of diagnostics fetching and project diagnostics editor. - /// Which files containing diagnostic errors/warnings to mark in the tabs. - /// Diagnostics are only shown when file icons are also active. - /// - /// Shows all diagnostics if not specified. - /// - /// Default: warning - pub diagnostics_max_severity: Option, - - /// Whether to show code action button at start of buffer line. - /// - /// Default: true - pub inline_code_actions: Option, - - /// Drag and drop related settings - pub drag_and_drop_selection: Option, - - /// How to render LSP `textDocument/documentColor` colors in the editor. - /// - /// Default: [`DocumentColorsRenderMode::Inlay`] - pub lsp_document_colors: Option, - /// When to show the scrollbar in the completion menu. - /// This setting can take four values: - /// - /// 1. Show the scrollbar if there's important information or - /// follow the system's configured behavior - /// "auto" - /// 2. Match the system's configured behavior: - /// "system" - /// 3. Always show the scrollbar: - /// "always" - /// 4. Never show the scrollbar: - /// "never" (default) - pub completion_menu_scrollbar: Option, -} - -#[derive( - Debug, - Clone, - Copy, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum RelativeLineNumbers { - Disabled, - Enabled, - Wrapped, -} - -impl RelativeLineNumbers { - pub fn enabled(&self) -> bool { - match self { - RelativeLineNumbers::Enabled | RelativeLineNumbers::Wrapped => true, - RelativeLineNumbers::Disabled => false, - } - } - pub fn wrapped(&self) -> bool { - match self { - RelativeLineNumbers::Enabled | RelativeLineNumbers::Disabled => false, - RelativeLineNumbers::Wrapped => true, - } - } -} - -// Toolbar related settings -#[with_fallible_options] -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)] -pub struct ToolbarContent { - /// Whether to display breadcrumbs in the editor toolbar. - /// - /// Default: true - pub breadcrumbs: Option, - /// Whether to display quick action buttons in the editor toolbar. - /// - /// Default: true - pub quick_actions: Option, - /// Whether to show the selections menu in the editor toolbar. - /// - /// Default: true - pub selections_menu: Option, - /// Whether to display Agent review buttons in the editor toolbar. - /// Only applicable while reviewing a file edited by the Agent. - /// - /// Default: true - pub agent_review: Option, - /// Whether to display code action buttons in the editor toolbar. - /// - /// Default: false - pub code_actions: Option, -} - -/// Scrollbar related settings -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Default)] -pub struct ScrollbarContent { - /// When to show the scrollbar in the editor. - /// - /// Default: auto - pub show: Option, - /// Whether to show git diff indicators in the scrollbar. - /// - /// Default: true - pub git_diff: Option, - /// Whether to show buffer search result indicators in the scrollbar. - /// - /// Default: true - pub search_results: Option, - /// Whether to show selected text occurrences in the scrollbar. - /// - /// Default: true - pub selected_text: Option, - /// Whether to show selected symbol occurrences in the scrollbar. - /// - /// Default: true - pub selected_symbol: Option, - /// Which diagnostic indicators to show in the scrollbar: - /// - /// Default: all - pub diagnostics: Option, - /// Whether to show cursor positions in the scrollbar. - /// - /// Default: true - pub cursors: Option, - /// Forcefully enable or disable the scrollbar for each axis - pub axes: Option, -} - -/// Sticky scroll related settings -#[with_fallible_options] -#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct StickyScrollContent { - /// Whether sticky scroll is enabled. - /// - /// Default: false - pub enabled: Option, -} - -/// Minimap related settings -#[with_fallible_options] -#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct MinimapContent { - /// When to show the minimap in the editor. - /// - /// Default: never - pub show: Option, - - /// Where to show the minimap in the editor. - /// - /// Default: [`DisplayIn::ActiveEditor`] - pub display_in: Option, - - /// When to show the minimap thumb. - /// - /// Default: always - pub thumb: Option, - - /// Defines the border style for the minimap's scrollbar thumb. - /// - /// Default: left_open - pub thumb_border: Option, - - /// How to highlight the current line in the minimap. - /// - /// Default: inherits editor line highlights setting - pub current_line_highlight: Option, - - /// Maximum number of columns to display in the minimap. - /// - /// Default: 80 - pub max_width_columns: Option, -} - -/// Forcefully enable or disable the scrollbar for each axis -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Default)] -pub struct ScrollbarAxesContent { - /// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings. - /// - /// Default: true - pub horizontal: Option, - - /// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings. - /// - /// Default: true - pub vertical: Option, -} - -/// Gutter related settings -#[with_fallible_options] -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)] -pub struct GutterContent { - /// Whether to show line numbers in the gutter. - /// - /// Default: true - pub line_numbers: Option, - /// Minimum number of characters to reserve space for in the gutter. - /// - /// Default: 4 - pub min_line_number_digits: Option, - /// Whether to show runnable buttons in the gutter. - /// - /// Default: true - pub runnables: Option, - /// Whether to show breakpoints in the gutter. - /// - /// Default: true - pub breakpoints: Option, - /// Whether to show fold buttons in the gutter. - /// - /// Default: true - pub folds: Option, -} - -/// How to render LSP `textDocument/documentColor` colors in the editor. -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum DocumentColorsRenderMode { - /// Do not query and render document colors. - None, - /// Render document colors as inlay hints near the color text. - #[default] - Inlay, - /// Draw a border around the color text. - Border, - /// Draw a background behind the color text. - Background, -} - -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum CurrentLineHighlight { - // Don't highlight the current line. - None, - // Highlight the gutter area. - Gutter, - // Highlight the editor area. - Line, - // Highlight the full line. - All, -} - -/// When to populate a new search's query based on the text under the cursor. -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum SeedQuerySetting { - /// Always populate the search query with the word under the cursor. - Always, - /// Only populate the search query when there is text selected. - Selection, - /// Never populate the search query - Never, -} - -/// What to do when multibuffer is double clicked in some of its excerpts (parts of singleton buffers). -#[derive( - Default, - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum DoubleClickInMultibuffer { - /// Behave as a regular buffer and select the whole word. - #[default] - Select, - /// Open the excerpt clicked as a new buffer in the new tab, if no `alt` modifier was pressed during double click. - /// Otherwise, behave as a regular buffer and select the whole word. - Open, -} - -/// When to show the minimap thumb. -/// -/// Default: always -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum MinimapThumb { - /// Show the minimap thumb only when the mouse is hovering over the minimap. - Hover, - /// Always show the minimap thumb. - #[default] - Always, -} - -/// Defines the border style for the minimap's scrollbar thumb. -/// -/// Default: left_open -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum MinimapThumbBorder { - /// Displays a border on all sides of the thumb. - Full, - /// Displays a border on all sides except the left side of the thumb. - #[default] - LeftOpen, - /// Displays a border on all sides except the right side of the thumb. - RightOpen, - /// Displays a border only on the left side of the thumb. - LeftOnly, - /// Displays the thumb without any border. - None, -} - -/// Which diagnostic indicators to show in the scrollbar. -/// -/// Default: all -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "lowercase")] -pub enum ScrollbarDiagnostics { - /// Show all diagnostic levels: hint, information, warnings, error. - All, - /// Show only the following diagnostic levels: information, warning, error. - Information, - /// Show only the following diagnostic levels: warning, error. - Warning, - /// Show only the following diagnostic level: error. - Error, - /// Do not show diagnostics. - None, -} - -/// The key to use for adding multiple cursors -/// -/// Default: alt -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum MultiCursorModifier { - Alt, - #[serde(alias = "cmd", alias = "ctrl")] - CmdOrCtrl, -} - -/// Whether the editor will scroll beyond the last line. -/// -/// Default: one_page -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ScrollBeyondLastLine { - /// The editor will not scroll beyond the last line. - Off, - - /// The editor will scroll beyond the last line by one page. - OnePage, - - /// The editor will scroll beyond the last line by the same number of lines as vertical_scroll_margin. - VerticalScrollMargin, -} - -/// The shape of a selection cursor. -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum CursorShape { - /// A vertical bar - #[default] - Bar, - /// A block that surrounds the following character - Block, - /// An underline that runs along the following character - Underline, - /// A box drawn around the following character - Hollow, -} - -/// What to do when go to definition yields no results. -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum GoToDefinitionFallback { - /// Disables the fallback. - None, - /// Looks up references of the same symbol instead. - #[default] - FindAllReferences, -} - -/// Determines when the mouse cursor should be hidden in an editor or input box. -/// -/// Default: on_typing_and_movement -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum HideMouseMode { - /// Never hide the mouse cursor - Never, - /// Hide only when typing - OnTyping, - /// Hide on both typing and cursor movement - #[default] - OnTypingAndMovement, -} - -/// Determines how snippets are sorted relative to other completion items. -/// -/// Default: inline -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum SnippetSortOrder { - /// Place snippets at the top of the completion list - Top, - /// Sort snippets normally using the default comparison logic - #[default] - Inline, - /// Place snippets at the bottom of the completion list - Bottom, - /// Do not show snippets in the completion list - None, -} - -/// Default options for buffer and project search items. -#[with_fallible_options] -#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)] -pub struct SearchSettingsContent { - /// Whether to show the project search button in the status bar. - pub button: Option, - /// Whether to only match on whole words. - pub whole_word: Option, - /// Whether to match case sensitively. - pub case_sensitive: Option, - /// Whether to include gitignored files in search results. - pub include_ignored: Option, - /// Whether to interpret the search query as a regular expression. - pub regex: Option, - /// Whether to center the cursor on each search match when navigating. - pub center_on_match: Option, -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub struct JupyterContent { - /// Whether the Jupyter feature is enabled. - /// - /// Default: true - pub enabled: Option, - - /// Default kernels to select for each language. - /// - /// Default: `{}` - pub kernel_selections: Option>, -} - -/// Whether to allow drag and drop text selection in buffer. -#[with_fallible_options] -#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)] -pub struct DragAndDropSelectionContent { - /// When true, enables drag and drop text selection in buffer. - /// - /// Default: true - pub enabled: Option, - - /// The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created. - /// - /// Default: 300 - pub delay: Option, -} - -/// When to show the minimap in the editor. -/// -/// Default: never -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ShowMinimap { - /// Follow the visibility of the scrollbar. - Auto, - /// Always show the minimap. - Always, - /// Never show the minimap. - #[default] - Never, -} - -/// Where to show the minimap in the editor. -/// -/// Default: all_editors -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum DisplayIn { - /// Show on all open editors. - AllEditors, - /// Show the minimap on the active editor only. - #[default] - ActiveEditor, -} - -/// Minimum APCA perceptual contrast for text over highlight backgrounds. -/// -/// Valid range: 0.0 to 106.0 -/// Default: 45.0 -#[derive( - Clone, - Copy, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - PartialOrd, - derive_more::FromStr, -)] -#[serde(transparent)] -pub struct MinimumContrast( - #[serde(serialize_with = "crate::serialize_f32_with_two_decimal_places")] pub f32, -); - -impl Display for MinimumContrast { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:.1}", self.0) - } -} - -impl From for MinimumContrast { - fn from(x: f32) -> Self { - Self(x) - } -} - -/// Opacity of the inactive panes. 0 means transparent, 1 means opaque. -/// -/// Valid range: 0.0 to 1.0 -/// Default: 1.0 -#[derive( - Clone, - Copy, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - PartialOrd, - derive_more::FromStr, -)] -#[serde(transparent)] -pub struct InactiveOpacity( - #[serde(serialize_with = "serialize_f32_with_two_decimal_places")] pub f32, -); - -impl Display for InactiveOpacity { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:.1}", self.0) - } -} - -impl From for InactiveOpacity { - fn from(x: f32) -> Self { - Self(x) - } -} - -/// Centered layout related setting (left/right). -/// -/// Valid range: 0.0 to 0.4 -/// Default: 2.0 -#[derive( - Clone, - Copy, - Debug, - Serialize, - Deserialize, - MergeFrom, - PartialEq, - PartialOrd, - derive_more::FromStr, -)] -#[serde(transparent)] -pub struct CenteredPaddingSettings( - #[serde(serialize_with = "serialize_f32_with_two_decimal_places")] pub f32, -); - -impl CenteredPaddingSettings { - pub const MIN_PADDING: f32 = 0.0; - // This is an f64 so serde_json can give a type hint without random numbers in the back - pub const DEFAULT_PADDING: f64 = 0.2; - pub const MAX_PADDING: f32 = 0.4; -} - -impl Display for CenteredPaddingSettings { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:.2}", self.0) - } -} - -impl From for CenteredPaddingSettings { - fn from(x: f32) -> Self { - Self(x) - } -} - -impl Default for CenteredPaddingSettings { - fn default() -> Self { - Self(Self::DEFAULT_PADDING as f32) - } -} - -impl schemars::JsonSchema for CenteredPaddingSettings { - fn schema_name() -> std::borrow::Cow<'static, str> { - "CenteredPaddingSettings".into() - } - - fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { - use schemars::json_schema; - json_schema!({ - "type": "number", - "minimum": Self::MIN_PADDING, - "maximum": Self::MAX_PADDING, - "default": Self::DEFAULT_PADDING, - "description": "Centered layout related setting (left/right)." - }) - } -} diff --git a/crates/settings/src/settings_content/extension.rs b/crates/settings/src/settings_content/extension.rs deleted file mode 100644 index 2fefd4ef38..0000000000 --- a/crates/settings/src/settings_content/extension.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::sync::Arc; - -use collections::HashMap; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings_macros::{MergeFrom, with_fallible_options}; - -#[with_fallible_options] -#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct ExtensionSettingsContent { - /// The extensions that should be automatically installed by Zed. - /// - /// This is used to make functionality provided by extensions (e.g., language support) - /// available out-of-the-box. - /// - /// Default: { "html": true } - #[serde(default)] - pub auto_install_extensions: HashMap, bool>, - #[serde(default)] - pub auto_update_extensions: HashMap, bool>, - /// The capabilities granted to extensions. - pub granted_extension_capabilities: Option>, -} - -/// A capability for an extension. -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum ExtensionCapabilityContent { - #[serde(rename = "process:exec")] - ProcessExec { - /// The command to execute. - command: String, - /// The arguments to pass to the command. Use `*` for a single wildcard argument. - /// If the last element is `**`, then any trailing arguments are allowed. - args: Vec, - }, - DownloadFile { - host: String, - path: Vec, - }, - #[serde(rename = "npm:install")] - NpmInstallPackage { - package: String, - }, -} diff --git a/crates/settings/src/settings_content/language.rs b/crates/settings/src/settings_content/language.rs deleted file mode 100644 index 25ff60e9f4..0000000000 --- a/crates/settings/src/settings_content/language.rs +++ /dev/null @@ -1,1023 +0,0 @@ -use std::num::NonZeroU32; - -use collections::{HashMap, HashSet}; -use gpui::{Modifiers, SharedString}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize, de::Error as _}; -use settings_macros::{MergeFrom, with_fallible_options}; -use std::sync::Arc; - -use crate::{ExtendingVec, merge_from}; - -#[with_fallible_options] -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct AllLanguageSettingsContent { - /// The settings for enabling/disabling features. - pub features: Option, - /// The edit prediction settings. - pub edit_predictions: Option, - /// The default language settings. - #[serde(flatten)] - pub defaults: LanguageSettingsContent, - /// The settings for individual languages. - #[serde(default)] - pub languages: LanguageToSettingsMap, - /// Settings for associating file extensions and filenames - /// with languages. - pub file_types: Option, ExtendingVec>>, -} - -impl merge_from::MergeFrom for AllLanguageSettingsContent { - fn merge_from(&mut self, other: &Self) { - self.file_types.merge_from(&other.file_types); - self.features.merge_from(&other.features); - self.edit_predictions.merge_from(&other.edit_predictions); - - // A user's global settings override the default global settings and - // all default language-specific settings. - // - self.defaults.merge_from(&other.defaults); - for language_settings in self.languages.0.values_mut() { - language_settings.merge_from(&other.defaults); - } - - // A user's language-specific settings override default language-specific settings. - for (language_name, user_language_settings) in &other.languages.0 { - if let Some(existing) = self.languages.0.get_mut(language_name) { - existing.merge_from(&user_language_settings); - } else { - let mut new_settings = self.defaults.clone(); - new_settings.merge_from(&user_language_settings); - - self.languages.0.insert(language_name.clone(), new_settings); - } - } - } -} - -/// The settings for enabling/disabling features. -#[with_fallible_options] -#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub struct FeaturesContent { - /// Determines which edit prediction provider to use. - pub edit_prediction_provider: Option, - /// Enables the experimental edit prediction context retrieval system. - pub experimental_edit_prediction_context_retrieval: Option, -} - -/// The provider that supplies edit predictions. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub enum EditPredictionProvider { - None, - #[default] - Copilot, - Supermaven, - Zed, - Codestral, - Experimental(&'static str), -} - -pub const EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME: &str = "sweep"; -pub const EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME: &str = "zeta2"; -pub const EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME: &str = "mercury"; - -impl<'de> Deserialize<'de> for EditPredictionProvider { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(rename_all = "snake_case")] - pub enum Content { - None, - Copilot, - Supermaven, - Zed, - Codestral, - Experimental(String), - } - - Ok(match Content::deserialize(deserializer)? { - Content::None => EditPredictionProvider::None, - Content::Copilot => EditPredictionProvider::Copilot, - Content::Supermaven => EditPredictionProvider::Supermaven, - Content::Zed => EditPredictionProvider::Zed, - Content::Codestral => EditPredictionProvider::Codestral, - Content::Experimental(name) - if name == EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME => - { - EditPredictionProvider::Experimental( - EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME, - ) - } - Content::Experimental(name) - if name == EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME => - { - EditPredictionProvider::Experimental( - EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME, - ) - } - Content::Experimental(name) - if name == EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME => - { - EditPredictionProvider::Experimental( - EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME, - ) - } - Content::Experimental(name) => { - return Err(D::Error::custom(format!( - "Unknown experimental edit prediction provider: {}", - name - ))); - } - }) - } -} - -impl EditPredictionProvider { - pub fn is_zed(&self) -> bool { - match self { - EditPredictionProvider::Zed => true, - EditPredictionProvider::None - | EditPredictionProvider::Copilot - | EditPredictionProvider::Supermaven - | EditPredictionProvider::Codestral - | EditPredictionProvider::Experimental(_) => false, - } - } -} - -/// The contents of the edit prediction settings. -#[with_fallible_options] -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct EditPredictionSettingsContent { - /// A list of globs representing files that edit predictions should be disabled for. - /// This list adds to a pre-existing, sensible default set of globs. - /// Any additional ones you add are combined with them. - pub disabled_globs: Option>, - /// The mode used to display edit predictions in the buffer. - /// Provider support required. - pub mode: Option, - /// Settings specific to GitHub Copilot. - pub copilot: Option, - /// Settings specific to Codestral. - pub codestral: Option, - /// Whether edit predictions are enabled in the assistant prompt editor. - /// This has no effect if globally disabled. - pub enabled_in_text_threads: Option, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct CopilotSettingsContent { - /// HTTP/HTTPS proxy to use for Copilot. - /// - /// Default: none - pub proxy: Option, - /// Disable certificate verification for the proxy (not recommended). - /// - /// Default: false - pub proxy_no_verify: Option, - /// Enterprise URI for Copilot. - /// - /// Default: none - pub enterprise_uri: Option, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct CodestralSettingsContent { - /// Model to use for completions. - /// - /// Default: "codestral-latest" - #[serde(default)] - pub model: Option, - /// Maximum tokens to generate. - /// - /// Default: 150 - #[serde(default)] - pub max_tokens: Option, - /// Api URL to use for completions. - /// - /// Default: "https://codestral.mistral.ai" - #[serde(default)] - pub api_url: Option, -} - -/// The mode in which edit predictions should be displayed. -#[derive( - Copy, - Clone, - Debug, - Default, - Eq, - PartialEq, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum EditPredictionsMode { - /// If provider supports it, display inline when holding modifier key (e.g., alt). - /// Otherwise, eager preview is used. - #[serde(alias = "auto")] - Subtle, - /// Display inline when there are no language server completions available. - #[default] - #[serde(alias = "eager_preview")] - Eager, -} - -/// Controls the soft-wrapping behavior in the editor. -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum SoftWrap { - /// Prefer a single line generally, unless an overly long line is encountered. - None, - /// Deprecated: use None instead. Left to avoid breaking existing users' configs. - /// Prefer a single line generally, unless an overly long line is encountered. - PreferLine, - /// Soft wrap lines that exceed the editor width. - EditorWidth, - /// Soft wrap lines at the preferred line length. - PreferredLineLength, - /// Soft wrap line at the preferred line length or the editor width (whichever is smaller). - Bounded, -} - -/// The settings for a particular language. -#[with_fallible_options] -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct LanguageSettingsContent { - /// How many columns a tab should occupy. - /// - /// Default: 4 - #[schemars(range(min = 1, max = 128))] - pub tab_size: Option, - /// Whether to indent lines using tab characters, as opposed to multiple - /// spaces. - /// - /// Default: false - pub hard_tabs: Option, - /// How to soft-wrap long lines of text. - /// - /// Default: none - pub soft_wrap: Option, - /// The column at which to soft-wrap lines, for buffers where soft-wrap - /// is enabled. - /// - /// Default: 80 - pub preferred_line_length: Option, - /// Whether to show wrap guides in the editor. Setting this to true will - /// show a guide at the 'preferred_line_length' value if softwrap is set to - /// 'preferred_line_length', and will show any additional guides as specified - /// by the 'wrap_guides' setting. - /// - /// Default: true - pub show_wrap_guides: Option, - /// Character counts at which to show wrap guides in the editor. - /// - /// Default: [] - pub wrap_guides: Option>, - /// Indent guide related settings. - pub indent_guides: Option, - /// Whether or not to perform a buffer format before saving. - /// - /// Default: on - pub format_on_save: Option, - /// Whether or not to remove any trailing whitespace from lines of a buffer - /// before saving it. - /// - /// Default: true - pub remove_trailing_whitespace_on_save: Option, - /// Whether or not to ensure there's a single newline at the end of a buffer - /// when saving it. - /// - /// Default: true - pub ensure_final_newline_on_save: Option, - /// How to perform a buffer format. - /// - /// Default: auto - pub formatter: Option, - /// Zed's Prettier integration settings. - /// Allows to enable/disable formatting with Prettier - /// and configure default Prettier, used when no project-level Prettier installation is found. - /// - /// Default: off - pub prettier: Option, - /// Whether to automatically close JSX tags. - pub jsx_tag_auto_close: Option, - /// Whether to use language servers to provide code intelligence. - /// - /// Default: true - pub enable_language_server: Option, - /// The list of language servers to use (or disable) for this language. - /// - /// This array should consist of language server IDs, as well as the following - /// special tokens: - /// - `"!"` - A language server ID prefixed with a `!` will be disabled. - /// - `"..."` - A placeholder to refer to the **rest** of the registered language servers for this language. - /// - /// Default: ["..."] - pub language_servers: Option>, - /// Controls where the `editor::Rewrap` action is allowed for this language. - /// - /// Note: This setting has no effect in Vim mode, as rewrap is already - /// allowed everywhere. - /// - /// Default: "in_comments" - pub allow_rewrap: Option, - /// Controls whether edit predictions are shown immediately (true) - /// or manually by triggering `editor::ShowEditPrediction` (false). - /// - /// Default: true - pub show_edit_predictions: Option, - /// Controls whether edit predictions are shown in the given language - /// scopes. - /// - /// Example: ["string", "comment"] - /// - /// Default: [] - pub edit_predictions_disabled_in: Option>, - /// Whether to show tabs and spaces in the editor. - pub show_whitespaces: Option, - /// Visible characters used to render whitespace when show_whitespaces is enabled. - /// - /// Default: "•" for spaces, "→" for tabs. - pub whitespace_map: Option, - /// Whether to start a new line with a comment when a previous line is a comment as well. - /// - /// Default: true - pub extend_comment_on_newline: Option, - /// Inlay hint related settings. - pub inlay_hints: Option, - /// Whether to automatically type closing characters for you. For example, - /// when you type '(', Zed will automatically add a closing ')' at the correct position. - /// - /// Default: true - pub use_autoclose: Option, - /// Whether to automatically surround text with characters for you. For example, - /// when you select text and type '(', Zed will automatically surround text with (). - /// - /// Default: true - pub use_auto_surround: Option, - /// Controls how the editor handles the autoclosed characters. - /// When set to `false`(default), skipping over and auto-removing of the closing characters - /// happen only for auto-inserted characters. - /// Otherwise(when `true`), the closing characters are always skipped over and auto-removed - /// no matter how they were inserted. - /// - /// Default: false - pub always_treat_brackets_as_autoclosed: Option, - /// Whether to use additional LSP queries to format (and amend) the code after - /// every "trigger" symbol input, defined by LSP server capabilities. - /// - /// Default: true - pub use_on_type_format: Option, - /// Which code actions to run on save before the formatter. - /// These are not run if formatting is off. - /// - /// Default: {} (or {"source.organizeImports": true} for Go). - pub code_actions_on_format: Option>, - /// Whether to perform linked edits of associated ranges, if the language server supports it. - /// For example, when editing opening tag, the contents of the closing tag will be edited as well. - /// - /// 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 - pub auto_indent_on_paste: Option, - /// Task configuration for this language. - /// - /// Default: {} - pub tasks: Option, - /// Whether to pop the completions menu while typing in an editor without - /// explicitly requesting it. - /// - /// Default: true - pub show_completions_on_input: Option, - /// Whether to display inline and alongside documentation for items in the - /// completions menu. - /// - /// Default: true - pub show_completion_documentation: Option, - /// Controls how completions are processed for this language. - pub completions: Option, - /// Preferred debuggers for this language. - /// - /// Default: [] - pub debuggers: Option>, - /// Whether to enable word diff highlighting in the editor. - /// - /// When enabled, changed words within modified lines are highlighted - /// to show exactly what changed. - /// - /// Default: true - pub word_diff_enabled: Option, - /// Whether to use tree-sitter bracket queries to detect and colorize the brackets in the editor. - /// - /// Default: false - pub colorize_brackets: Option, -} - -/// Controls how whitespace should be displayedin the editor. -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ShowWhitespaceSetting { - /// Draw whitespace only for the selected text. - Selection, - /// Do not draw any tabs or spaces. - None, - /// Draw all invisible symbols. - All, - /// Draw whitespaces at boundaries only. - /// - /// For a whitespace to be on a boundary, any of the following conditions need to be met: - /// - It is a tab - /// - It is adjacent to an edge (start or end) - /// - It is adjacent to a whitespace (left or right) - Boundary, - /// Draw whitespaces only after non-whitespace characters. - Trailing, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct WhitespaceMapContent { - pub space: Option, - pub tab: Option, -} - -/// The behavior of `editor::Rewrap`. -#[derive( - Debug, - PartialEq, - Clone, - Copy, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum RewrapBehavior { - /// Only rewrap within comments. - #[default] - InComments, - /// Only rewrap within the current selection(s). - InSelections, - /// Allow rewrapping anywhere. - Anywhere, -} - -#[with_fallible_options] -#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct JsxTagAutoCloseSettingsContent { - /// Enables or disables auto-closing of JSX tags. - pub enabled: Option, -} - -/// The settings for inlay hints. -#[with_fallible_options] -#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)] -pub struct InlayHintSettingsContent { - /// Global switch to toggle hints on and off. - /// - /// Default: false - pub enabled: Option, - /// Global switch to toggle inline values on and off when debugging. - /// - /// Default: true - pub show_value_hints: Option, - /// Whether type hints should be shown. - /// - /// Default: true - pub show_type_hints: Option, - /// Whether parameter hints should be shown. - /// - /// Default: true - pub show_parameter_hints: Option, - /// Whether other hints should be shown. - /// - /// Default: true - pub show_other_hints: Option, - /// Whether to show a background for inlay hints. - /// - /// If set to `true`, the background will use the `hint.background` color - /// from the current theme. - /// - /// Default: false - pub show_background: Option, - /// Whether or not to debounce inlay hints updates after buffer edits. - /// - /// Set to 0 to disable debouncing. - /// - /// Default: 700 - pub edit_debounce_ms: Option, - /// Whether or not to debounce inlay hints updates after buffer scrolls. - /// - /// Set to 0 to disable debouncing. - /// - /// Default: 50 - pub scroll_debounce_ms: Option, - /// Toggles inlay hints (hides or shows) when the user presses the modifiers specified. - /// If only a subset of the modifiers specified is pressed, hints are not toggled. - /// If no modifiers are specified, this is equivalent to `null`. - /// - /// Default: null - pub toggle_on_modifiers_press: Option, -} - -/// The kind of an inlay hint. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum InlayHintKind { - /// An inlay hint for a type. - Type, - /// An inlay hint for a parameter. - Parameter, -} - -impl InlayHintKind { - /// Returns the [`InlayHintKind`]fromthe given name. - /// - /// Returns `None` if `name` does not match any of the expected - /// string representations. - pub fn from_name(name: &str) -> Option { - match name { - "type" => Some(InlayHintKind::Type), - "parameter" => Some(InlayHintKind::Parameter), - _ => None, - } - } - - /// Returns the name of this [`InlayHintKind`]. - pub fn name(&self) -> &'static str { - match self { - InlayHintKind::Type => "type", - InlayHintKind::Parameter => "parameter", - } - } -} - -/// Controls how completions are processed for this language. -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom, Default)] -#[serde(rename_all = "snake_case")] -pub struct CompletionSettingsContent { - /// Controls how words are completed. - /// For large documents, not all words may be fetched for completion. - /// - /// Default: `fallback` - pub words: Option, - /// 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 - pub words_min_length: Option, - /// Whether to fetch LSP completions or not. - /// - /// Default: true - pub lsp: Option, - /// When fetching LSP completions, determines how long to wait for a response of a particular server. - /// When set to 0, waits indefinitely. - /// - /// Default: 0 - pub lsp_fetch_timeout_ms: Option, - /// Controls how LSP completions are inserted. - /// - /// Default: "replace_suffix" - pub lsp_insert_mode: Option, -} - -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum LspInsertMode { - /// Replaces text before the cursor, using the `insert` range described in the LSP specification. - Insert, - /// Replaces text before and after the cursor, using the `replace` range described in the LSP specification. - Replace, - /// Behaves like `"replace"` if the text that would be replaced is a subsequence of the completion text, - /// and like `"insert"` otherwise. - ReplaceSubsequence, - /// Behaves like `"replace"` if the text after the cursor is a suffix of the completion, and like - /// `"insert"` otherwise. - ReplaceSuffix, -} - -/// Controls how document's words are completed. -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum WordsCompletionMode { - /// Always fetch document's words for completions along with LSP completions. - Enabled, - /// Only if LSP response errors or times out, - /// use document's words to show completions. - Fallback, - /// Never fetch or complete document's words for completions. - /// (Word-based completions can still be queried via a separate action) - Disabled, -} - -/// Allows to enable/disable formatting with Prettier -/// and configure default Prettier, used when no project-level Prettier installation is found. -/// Prettier formatting is disabled by default. -#[with_fallible_options] -#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct PrettierSettingsContent { - /// Enables or disables formatting with Prettier for a given language. - pub allowed: Option, - - /// Forces Prettier integration to use a specific parser name when formatting files with the language. - pub parser: Option, - - /// Forces Prettier integration to use specific plugins when formatting files with the language. - /// The default Prettier will be installed with these plugins. - pub plugins: Option>, - - /// Default Prettier options, in the format as in package.json section for Prettier. - /// If project installs Prettier via its package.json, these options will be ignored. - #[serde(flatten)] - pub options: Option>, -} - -/// TODO: this should just be a bool -/// Controls the behavior of formatting files when they are saved. -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "lowercase")] -pub enum FormatOnSave { - /// Files should be formatted on save. - On, - /// Files should not be formatted on save. - Off, -} - -/// Controls which formatters should be used when formatting code. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] -#[serde(untagged)] -pub enum FormatterList { - Single(Formatter), - Vec(Vec), -} - -impl Default for FormatterList { - fn default() -> Self { - Self::Single(Formatter::default()) - } -} - -impl AsRef<[Formatter]> for FormatterList { - fn as_ref(&self) -> &[Formatter] { - match &self { - Self::Single(single) => std::slice::from_ref(single), - Self::Vec(v) => v, - } - } -} - -/// Controls which formatter should be used when formatting code. If there are multiple formatters, they are executed in the order of declaration. -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub enum Formatter { - /// Format files using Zed's Prettier integration (if applicable), - /// or falling back to formatting via language server. - #[default] - Auto, - /// Format code using Zed's Prettier integration. - Prettier, - /// Format code using an external command. - External { - /// The external program to run. - command: Arc, - /// The arguments to pass to the program. - arguments: Option>, - }, - /// Files should be formatted using a code action executed by language servers. - CodeAction(String), - /// Format code using a language server. - #[serde(untagged)] - LanguageServer(LanguageServerFormatterSpecifier), -} - -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] -#[serde( - rename_all = "snake_case", - // allow specifying language servers as "language_server" or {"language_server": {"name": ...}} - from = "LanguageServerVariantContent", - into = "LanguageServerVariantContent" -)] -pub enum LanguageServerFormatterSpecifier { - Specific { - name: String, - }, - #[default] - Current, -} - -impl From for LanguageServerFormatterSpecifier { - fn from(value: LanguageServerVariantContent) -> Self { - match value { - LanguageServerVariantContent::Specific { - language_server: LanguageServerSpecifierContent { name: Some(name) }, - } => Self::Specific { name }, - _ => Self::Current, - } - } -} - -impl From for LanguageServerVariantContent { - fn from(value: LanguageServerFormatterSpecifier) -> Self { - match value { - LanguageServerFormatterSpecifier::Specific { name } => Self::Specific { - language_server: LanguageServerSpecifierContent { name: Some(name) }, - }, - LanguageServerFormatterSpecifier::Current => { - Self::Current(CurrentLanguageServerContent::LanguageServer) - } - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case", untagged)] -enum LanguageServerVariantContent { - /// Format code using a specific language server. - Specific { - language_server: LanguageServerSpecifierContent, - }, - /// Format code using the current language server. - Current(CurrentLanguageServerContent), -} - -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -enum CurrentLanguageServerContent { - #[default] - LanguageServer, -} - -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -struct LanguageServerSpecifierContent { - /// The name of the language server to format with - name: Option, -} - -/// The settings for indent guides. -#[with_fallible_options] -#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct IndentGuideSettingsContent { - /// Whether to display indent guides in the editor. - /// - /// Default: true - pub enabled: Option, - /// The width of the indent guides in pixels, between 1 and 10. - /// - /// Default: 1 - pub line_width: Option, - /// The width of the active indent guide in pixels, between 1 and 10. - /// - /// Default: 1 - pub active_line_width: Option, - /// Determines how indent guides are colored. - /// - /// Default: Fixed - pub coloring: Option, - /// Determines how indent guide backgrounds are colored. - /// - /// Default: Disabled - pub background_coloring: Option, -} - -/// The task settings for a particular language. -#[with_fallible_options] -#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize, JsonSchema, MergeFrom)] -pub struct LanguageTaskSettingsContent { - /// Extra task variables to set for a particular language. - pub variables: Option>, - pub enabled: Option, - /// Use LSP tasks over Zed language extension ones. - /// If no LSP tasks are returned due to error/timeout or regular execution, - /// Zed language extension tasks will be used instead. - /// - /// Other Zed tasks will still be shown: - /// * Zed task from either of the task config file - /// * Zed task from history (e.g. one-off task was spawned before) - pub prefer_lsp: Option, -} - -/// Map from language name to settings. -#[with_fallible_options] -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct LanguageToSettingsMap(pub HashMap); - -/// Determines how indent guides are colored. -#[derive( - Default, - Debug, - Copy, - Clone, - PartialEq, - Eq, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum IndentGuideColoring { - /// Do not render any lines for indent guides. - Disabled, - /// Use the same color for all indentation levels. - #[default] - Fixed, - /// Use a different color for each indentation level. - IndentAware, -} - -/// Determines how indent guide backgrounds are colored. -#[derive( - Default, - Debug, - Copy, - Clone, - PartialEq, - Eq, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum IndentGuideBackgroundColoring { - /// Do not render any background for indent guides. - #[default] - Disabled, - /// Use a different color for each indentation level. - IndentAware, -} - -#[cfg(test)] -mod test { - - use crate::{ParseStatus, fallible_options}; - - use super::*; - - #[test] - fn test_formatter_deserialization() { - let raw_auto = "{\"formatter\": \"auto\"}"; - let settings: LanguageSettingsContent = serde_json::from_str(raw_auto).unwrap(); - assert_eq!( - settings.formatter, - Some(FormatterList::Single(Formatter::Auto)) - ); - let raw = "{\"formatter\": \"language_server\"}"; - let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap(); - assert_eq!( - settings.formatter, - Some(FormatterList::Single(Formatter::LanguageServer( - LanguageServerFormatterSpecifier::Current - ))) - ); - - let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}]}"; - let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap(); - assert_eq!( - settings.formatter, - Some(FormatterList::Vec(vec![Formatter::LanguageServer( - LanguageServerFormatterSpecifier::Current - )])) - ); - let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}, \"language_server\", \"prettier\"]}"; - let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap(); - assert_eq!( - settings.formatter, - Some(FormatterList::Vec(vec![ - Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current), - Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current), - Formatter::Prettier - ])) - ); - - let raw = "{\"formatter\": [{\"language_server\": {\"name\": \"ruff\"}}, \"prettier\"]}"; - let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap(); - assert_eq!( - settings.formatter, - Some(FormatterList::Vec(vec![ - Formatter::LanguageServer(LanguageServerFormatterSpecifier::Specific { - name: "ruff".to_string() - }), - Formatter::Prettier - ])) - ); - - assert_eq!( - serde_json::to_string(&LanguageServerFormatterSpecifier::Current).unwrap(), - "\"language_server\"", - ); - } - - #[test] - fn test_formatter_deserialization_invalid() { - let raw_auto = "{\"formatter\": {}}"; - let (_, result) = fallible_options::parse_json::(raw_auto); - assert!(matches!(result, ParseStatus::Failed { .. })); - } - - #[test] - fn test_prettier_options() { - let raw_prettier = r#"{"allowed": false, "tabWidth": 4, "semi": false}"#; - let result = serde_json::from_str::(raw_prettier) - .expect("Failed to parse prettier options"); - assert!( - result - .options - .as_ref() - .expect("options were flattened") - .contains_key("semi") - ); - assert!( - result - .options - .as_ref() - .expect("options were flattened") - .contains_key("tabWidth") - ); - } -} diff --git a/crates/settings/src/settings_content/language_model.rs b/crates/settings/src/settings_content/language_model.rs deleted file mode 100644 index 48f5a463a4..0000000000 --- a/crates/settings/src/settings_content/language_model.rs +++ /dev/null @@ -1,423 +0,0 @@ -use collections::HashMap; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings_macros::{MergeFrom, with_fallible_options}; - -use std::sync::Arc; - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct AllLanguageModelSettingsContent { - pub anthropic: Option, - pub bedrock: Option, - pub deepseek: Option, - pub google: Option, - pub lmstudio: Option, - pub mistral: Option, - pub ollama: Option, - pub open_router: Option, - pub openai: Option, - pub openai_compatible: Option, OpenAiCompatibleSettingsContent>>, - pub vercel: Option, - pub x_ai: Option, - #[serde(rename = "zed.dev")] - pub zed_dot_dev: Option, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct AnthropicSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct AnthropicAvailableModel { - /// The model's name in the Anthropic API. e.g. claude-3-5-sonnet-latest, claude-3-opus-20240229, etc - pub name: String, - /// The model's name in Zed's UI, such as in the model selector dropdown menu in the assistant panel. - pub display_name: Option, - /// The model's context window size. - pub max_tokens: u64, - /// A model `name` to substitute when calling tools, in case the primary model doesn't support tool calling. - pub tool_override: Option, - /// Configuration of Anthropic's caching API. - pub cache_configuration: Option, - pub max_output_tokens: Option, - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_temperature: Option, - #[serde(default)] - pub extra_beta_headers: Vec, - /// The model's mode (e.g. thinking) - pub mode: Option, -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct AmazonBedrockSettingsContent { - pub available_models: Option>, - pub endpoint_url: Option, - pub region: Option, - pub profile: Option, - pub authentication_method: Option, - pub allow_global: Option, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct BedrockAvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub cache_configuration: Option, - pub max_output_tokens: Option, - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_temperature: Option, - pub mode: Option, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub enum BedrockAuthMethodContent { - #[serde(rename = "named_profile")] - NamedProfile, - #[serde(rename = "sso")] - SingleSignOn, - /// IMDSv2, PodIdentity, env vars, etc. - #[serde(rename = "default")] - Automatic, -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct OllamaSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct OllamaAvailableModel { - /// The model name in the Ollama API (e.g. "llama3.2:latest") - pub name: String, - /// The model's name in Zed's UI, such as in the model selector dropdown menu in the assistant panel. - pub display_name: Option, - /// The Context Length parameter to the model (aka num_ctx or n_ctx) - pub max_tokens: u64, - /// The number of seconds to keep the connection open after the last request - pub keep_alive: Option, - /// Whether the model supports tools - pub supports_tools: Option, - /// Whether the model supports vision - pub supports_images: Option, - /// Whether to enable think mode - pub supports_thinking: Option, -} - -#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq, JsonSchema, MergeFrom)] -#[serde(untagged)] -pub enum KeepAlive { - /// Keep model alive for N seconds - Seconds(isize), - /// Keep model alive for a fixed duration. Accepts durations like "5m", "10m", "1h", "1d", etc. - Duration(String), -} - -impl KeepAlive { - /// Keep model alive until a new model is loaded or until Ollama shuts down - pub fn indefinite() -> Self { - Self::Seconds(-1) - } -} - -impl Default for KeepAlive { - fn default() -> Self { - Self::indefinite() - } -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct LmStudioSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct LmStudioAvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub supports_tool_calls: bool, - pub supports_images: bool, -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct DeepseekSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct DeepseekAvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct MistralSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct MistralAvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, - pub max_completion_tokens: Option, - pub supports_tools: Option, - pub supports_images: Option, - pub supports_thinking: Option, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct OpenAiSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct OpenAiAvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, - pub max_completion_tokens: Option, - pub reasoning_effort: Option, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, JsonSchema, MergeFrom)] -#[serde(rename_all = "lowercase")] -pub enum OpenAiReasoningEffort { - Minimal, - Low, - Medium, - High, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct OpenAiCompatibleSettingsContent { - pub api_url: String, - pub available_models: Vec, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct OpenAiCompatibleAvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, - pub max_completion_tokens: Option, - #[serde(default)] - pub capabilities: OpenAiCompatibleModelCapabilities, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct OpenAiCompatibleModelCapabilities { - pub tools: bool, - pub images: bool, - pub parallel_tool_calls: bool, - pub prompt_cache_key: bool, -} - -impl Default for OpenAiCompatibleModelCapabilities { - fn default() -> Self { - Self { - tools: true, - images: false, - parallel_tool_calls: false, - prompt_cache_key: false, - } - } -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct VercelSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct VercelAvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, - pub max_completion_tokens: Option, -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct GoogleSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct GoogleAvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub mode: Option, -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct XAiSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct XaiAvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, - pub max_completion_tokens: Option, - pub supports_images: Option, - pub supports_tools: Option, - pub parallel_tool_calls: Option, -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct ZedDotDevSettingsContent { - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct ZedDotDevAvailableModel { - /// The provider of the language model. - pub provider: ZedDotDevAvailableProvider, - /// The model's name in the provider's API. e.g. claude-3-5-sonnet-20240620 - pub name: String, - /// The name displayed in the UI, such as in the assistant panel model dropdown menu. - pub display_name: Option, - /// The size of the context window, indicating the maximum number of tokens the model can process. - pub max_tokens: usize, - /// The maximum number of output tokens allowed by the model. - pub max_output_tokens: Option, - /// The maximum number of completion tokens allowed by the model (o1-* only) - pub max_completion_tokens: Option, - /// Override this model with a different Anthropic model for tool calls. - pub tool_override: Option, - /// Indicates whether this custom model supports caching. - pub cache_configuration: Option, - /// The default temperature to use for this model. - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_temperature: Option, - /// Any extra beta headers to provide when using the model. - #[serde(default)] - pub extra_beta_headers: Vec, - /// The model's mode (e.g. thinking) - pub mode: Option, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "lowercase")] -pub enum ZedDotDevAvailableProvider { - Anthropic, - OpenAi, - Google, -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct OpenRouterSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct OpenRouterAvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, - pub max_completion_tokens: Option, - pub supports_tools: Option, - pub supports_images: Option, - pub mode: Option, - pub provider: Option, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct OpenRouterProvider { - order: Option>, - #[serde(default = "default_true")] - allow_fallbacks: bool, - #[serde(default)] - require_parameters: bool, - #[serde(default)] - data_collection: DataCollection, - only: Option>, - ignore: Option>, - quantizations: Option>, - sort: Option, -} - -#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "lowercase")] -pub enum DataCollection { - #[default] - Allow, - Disallow, -} - -fn default_true() -> bool { - true -} - -/// Configuration for caching language model messages. -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct LanguageModelCacheConfiguration { - pub max_cache_anchors: usize, - pub should_speculate: bool, - pub min_total_token: u64, -} - -#[derive( - Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom, -)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum ModelMode { - #[default] - Default, - Thinking { - /// The maximum number of tokens to use for reasoning. Must be lower than the model's `max_output_tokens`. - budget_tokens: Option, - }, -} diff --git a/crates/settings/src/settings_content/project.rs b/crates/settings/src/settings_content/project.rs deleted file mode 100644 index 5cd708694d..0000000000 --- a/crates/settings/src/settings_content/project.rs +++ /dev/null @@ -1,565 +0,0 @@ -use std::{path::PathBuf, sync::Arc}; - -use collections::{BTreeMap, HashMap}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings_macros::{MergeFrom, with_fallible_options}; -use util::serde::default_true; - -use crate::{ - AllLanguageSettingsContent, DelayMs, ExtendingVec, ProjectTerminalSettingsContent, - SlashCommandSettings, -}; - -#[with_fallible_options] -#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct ProjectSettingsContent { - #[serde(flatten)] - pub all_languages: AllLanguageSettingsContent, - - #[serde(flatten)] - pub worktree: WorktreeSettingsContent, - - /// Configuration for language servers. - /// - /// The following settings can be overridden for specific language servers: - /// - initialization_options - /// - /// To override settings for a language, add an entry for that language server's - /// name to the lsp value. - /// Default: null - #[serde(default)] - pub lsp: HashMap, LspSettings>, - - pub terminal: Option, - - /// Configuration for Debugger-related features - #[serde(default)] - pub dap: HashMap, DapSettingsContent>, - - /// Settings for context servers used for AI-related features. - #[serde(default)] - pub context_servers: HashMap, ContextServerSettingsContent>, - - /// Configuration for how direnv configuration should be loaded - pub load_direnv: Option, - - /// Settings for slash commands. - pub slash_commands: Option, - - /// The list of custom Git hosting providers. - pub git_hosting_providers: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct WorktreeSettingsContent { - /// The displayed name of this project. If not set or null, the root directory name - /// will be displayed. - /// - /// Default: null - pub project_name: Option, - - /// Whether to prevent this project from being shared in public channels. - /// - /// Default: false - #[serde(default)] - pub prevent_sharing_in_public_channels: bool, - - /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides - /// `file_scan_inclusions`. - /// - /// Default: [ - /// "**/.git", - /// "**/.svn", - /// "**/.hg", - /// "**/.jj", - /// "**/CVS", - /// "**/.DS_Store", - /// "**/Thumbs.db", - /// "**/.classpath", - /// "**/.settings" - /// ] - pub file_scan_exclusions: Option>, - - /// Always include files that match these globs when scanning for files, even if they're - /// ignored by git. This setting is overridden by `file_scan_exclusions`. - /// Default: [ - /// ".env*", - /// "docker-compose.*.yml", - /// ] - pub file_scan_inclusions: Option>, - - /// Treat the files matching these globs as `.env` files. - /// Default: ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"] - pub private_files: Option>, - - /// Treat the files matching these globs as hidden files. You can hide hidden files in the project panel. - /// Default: ["**/.*"] - pub hidden_files: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom, Hash)] -#[serde(rename_all = "snake_case")] -pub struct LspSettings { - pub binary: Option, - /// Options passed to the language server at startup. - /// - /// Ref: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize - /// - /// Consult the documentation for the specific language server to see which settings are supported. - pub initialization_options: Option, - /// Language server settings. - /// - /// Ref: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_configuration - /// - /// Consult the documentation for the specific language server to see which settings are supported. - pub settings: Option, - /// If the server supports sending tasks over LSP extensions, - /// this setting can be used to enable or disable them in Zed. - /// Default: true - #[serde(default = "default_true")] - pub enable_lsp_tasks: bool, - pub fetch: Option, -} - -impl Default for LspSettings { - fn default() -> Self { - Self { - binary: None, - initialization_options: None, - settings: None, - enable_lsp_tasks: true, - fetch: None, - } - } -} - -#[with_fallible_options] -#[derive( - Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom, Hash, -)] -pub struct BinarySettings { - pub path: Option, - pub arguments: Option>, - pub env: Option>, - pub ignore_system_version: Option, -} - -#[with_fallible_options] -#[derive( - Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom, Hash, -)] -pub struct FetchSettings { - // Whether to consider pre-releases for fetching - pub pre_release: Option, -} - -/// Common language server settings. -#[with_fallible_options] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct GlobalLspSettingsContent { - /// Whether to show the LSP servers button in the status bar. - /// - /// Default: `true` - pub button: Option, -} - -#[with_fallible_options] -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub struct DapSettingsContent { - pub binary: Option, - pub args: Option>, - pub env: Option>, -} - -#[with_fallible_options] -#[derive( - Default, Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, -)] -pub struct SessionSettingsContent { - /// Whether or not to restore unsaved buffers on restart. - /// - /// If this is true, user won't be prompted whether to save/discard - /// dirty files when closing the application. - /// - /// Default: true - pub restore_unsaved_buffers: Option, -} - -#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, MergeFrom, Debug)] -#[serde(untagged, rename_all = "snake_case")] -pub enum ContextServerSettingsContent { - Stdio { - /// Whether the context server is enabled. - #[serde(default = "default_true")] - enabled: bool, - - #[serde(flatten)] - command: ContextServerCommand, - }, - Http { - /// Whether the context server is enabled. - #[serde(default = "default_true")] - enabled: bool, - /// The URL of the remote context server. - url: String, - /// Optional headers to send. - #[serde(skip_serializing_if = "HashMap::is_empty", default)] - headers: HashMap, - }, - Extension { - /// Whether the context server is enabled. - #[serde(default = "default_true")] - enabled: bool, - /// The settings for this context server specified by the extension. - /// - /// Consult the documentation for the context server to see what settings - /// are supported. - settings: serde_json::Value, - }, -} - -impl ContextServerSettingsContent { - pub fn set_enabled(&mut self, enabled: bool) { - match self { - ContextServerSettingsContent::Stdio { - enabled: custom_enabled, - .. - } => { - *custom_enabled = enabled; - } - ContextServerSettingsContent::Extension { - enabled: ext_enabled, - .. - } => *ext_enabled = enabled, - ContextServerSettingsContent::Http { - enabled: remote_enabled, - .. - } => *remote_enabled = enabled, - } - } -} - -#[with_fallible_options] -#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, MergeFrom)] -pub struct ContextServerCommand { - #[serde(rename = "command")] - pub path: PathBuf, - pub args: Vec, - pub env: Option>, - /// Timeout for tool calls in milliseconds. Defaults to 60000 (60 seconds) if not specified. - pub timeout: Option, -} - -impl std::fmt::Debug for ContextServerCommand { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let filtered_env = self.env.as_ref().map(|env| { - env.iter() - .map(|(k, v)| { - ( - k, - if util::redact::should_redact(k) { - "[REDACTED]" - } else { - v - }, - ) - }) - .collect::>() - }); - - f.debug_struct("ContextServerCommand") - .field("path", &self.path) - .field("args", &self.args) - .field("env", &filtered_env) - .finish() - } -} - -#[with_fallible_options] -#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct GitSettings { - /// Whether or not to show the git gutter. - /// - /// Default: tracked_files - pub git_gutter: Option, - /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter. - /// - /// Default: 0 - pub gutter_debounce: Option, - /// Whether or not to show git blame data inline in - /// the currently focused line. - /// - /// Default: on - pub inline_blame: Option, - /// Git blame settings. - pub blame: Option, - /// Which information to show in the branch picker. - /// - /// Default: on - pub branch_picker: Option, - /// How hunks are displayed visually in the editor. - /// - /// Default: staged_hollow - pub hunk_style: Option, - /// How file paths are displayed in the git gutter. - /// - /// Default: file_name_first - pub path_style: Option, -} - -#[derive( - Clone, - Copy, - Debug, - PartialEq, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum GitGutterSetting { - /// Show git gutter in tracked files. - #[default] - TrackedFiles, - /// Hide git gutter - Hide, -} - -#[with_fallible_options] -#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub struct InlineBlameSettings { - /// Whether or not to show git blame data inline in - /// the currently focused line. - /// - /// Default: true - pub enabled: Option, - /// Whether to only show the inline blame information - /// after a delay once the cursor stops moving. - /// - /// Default: 0 - pub delay_ms: Option, - /// The amount of padding between the end of the source line and the start - /// of the inline blame in units of columns. - /// - /// Default: 7 - pub padding: Option, - /// The minimum column number to show the inline blame information at - /// - /// Default: 0 - pub min_column: Option, - /// Whether to show commit summary as part of the inline blame. - /// - /// Default: false - pub show_commit_summary: Option, -} - -#[with_fallible_options] -#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub struct BlameSettings { - /// Whether to show the avatar of the author of the commit. - /// - /// Default: true - pub show_avatar: Option, -} - -#[with_fallible_options] -#[derive(Clone, Copy, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub struct BranchPickerSettingsContent { - /// Whether to show author name as part of the commit information. - /// - /// Default: false - pub show_author_name: Option, -} - -#[derive( - Clone, - Copy, - PartialEq, - Debug, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum GitHunkStyleSetting { - /// Show unstaged hunks with a filled background and staged hunks hollow. - #[default] - StagedHollow, - /// Show unstaged hunks hollow and staged hunks with a filled background. - UnstagedHollow, -} - -#[with_fallible_options] -#[derive( - Copy, - Clone, - Debug, - PartialEq, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum GitPathStyle { - /// Show file name first, then path - #[default] - FileNameFirst, - /// Show full path first - FilePathFirst, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct DiagnosticsSettingsContent { - /// Whether to show the project diagnostics button in the status bar. - pub button: Option, - - /// Whether or not to include warning diagnostics. - pub include_warnings: Option, - - /// Settings for using LSP pull diagnostics mechanism in Zed. - pub lsp_pull_diagnostics: Option, - - /// Settings for showing inline diagnostics. - pub inline: Option, -} - -#[with_fallible_options] -#[derive( - Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, -)] -pub struct LspPullDiagnosticsSettingsContent { - /// Whether to pull for diagnostics or not. - /// - /// Default: true - pub enabled: Option, - /// Minimum time to wait before pulling diagnostics from the language server(s). - /// 0 turns the debounce off. - /// - /// Default: 50 - pub debounce_ms: Option, -} - -#[with_fallible_options] -#[derive( - Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Eq, -)] -pub struct InlineDiagnosticsSettingsContent { - /// Whether or not to show inline diagnostics - /// - /// Default: false - pub enabled: Option, - /// Whether to only show the inline diagnostics after a delay after the - /// last editor event. - /// - /// Default: 150 - pub update_debounce_ms: Option, - /// The amount of padding between the end of the source line and the start - /// of the inline diagnostic in units of columns. - /// - /// Default: 4 - pub padding: Option, - /// The minimum column to display inline diagnostics. This setting can be - /// used to horizontally align inline diagnostics at some position. Lines - /// longer than this value will still push diagnostics further to the right. - /// - /// Default: 0 - pub min_column: Option, - - pub max_severity: Option, -} - -#[with_fallible_options] -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct NodeBinarySettings { - /// The path to the Node binary. - pub path: Option, - /// The path to the npm binary Zed should use (defaults to `.path/../npm`). - pub npm_path: Option, - /// If enabled, Zed will download its own copy of Node. - pub ignore_system_version: Option, -} - -#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub enum DirenvSettings { - /// Load direnv configuration through a shell hook - ShellHook, - /// Load direnv configuration directly using `direnv export json` - #[default] - Direct, - /// Do not load direnv configuration - Disabled, -} - -#[derive( - Clone, - Copy, - Debug, - Eq, - PartialEq, - Ord, - PartialOrd, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum DiagnosticSeverityContent { - // No diagnostics are shown. - Off, - Error, - Warning, - Info, - Hint, - All, -} - -/// A custom Git hosting provider. -#[with_fallible_options] -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct GitHostingProviderConfig { - /// The type of the provider. - /// - /// Must be one of `github`, `gitlab`, `bitbucket`, `gitea`, `forgejo`, or `source_hut`. - pub provider: GitHostingProviderKind, - - /// The base URL for the provider (e.g., "https://code.corp.big.com"). - pub base_url: String, - - /// The display name for the provider (e.g., "BigCorp GitHub"). - pub name: String, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub enum GitHostingProviderKind { - Github, - Gitlab, - Bitbucket, - Gitea, - Forgejo, - SourceHut, -} diff --git a/crates/settings/src/settings_content/terminal.rs b/crates/settings/src/settings_content/terminal.rs deleted file mode 100644 index 1a30eecaa1..0000000000 --- a/crates/settings/src/settings_content/terminal.rs +++ /dev/null @@ -1,517 +0,0 @@ -use std::path::PathBuf; - -use collections::HashMap; -use gpui::{AbsoluteLength, FontFeatures, FontWeight, SharedString, px}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings_macros::{MergeFrom, with_fallible_options}; - -use crate::FontFamilyName; - -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct ProjectTerminalSettingsContent { - /// What shell to use when opening a terminal. - /// - /// Default: system - pub shell: Option, - /// What working directory to use when launching the terminal - /// - /// Default: current_project_directory - pub working_directory: Option, - /// Any key-value pairs added to this list will be added to the terminal's - /// environment. Use `:` to separate multiple values. - /// - /// Default: {} - pub env: Option>, - /// Activates the python virtual environment, if one is found, in the - /// terminal's working directory (as resolved by the working_directory - /// setting). Set this to "off" to disable this behavior. - /// - /// Default: on - pub detect_venv: Option, - /// Regexes used to identify paths for hyperlink navigation. - /// - /// Default: [ - /// // Python-style diagnostics - /// "File \"(?[^\"]+)\", line (?[0-9]+)", - /// // Common path syntax with optional line, column, description, trailing punctuation, or - /// // surrounding symbols or quotes - /// [ - /// "(?x)", - /// "# optionally starts with 0-2 opening prefix symbols", - /// "[({\\[<]{0,2}", - /// "# which may be followed by an opening quote", - /// "(?[\"'`])?", - /// "# `path` is the shortest sequence of any non-space character", - /// "(?(?[^ ]+?", - /// " # which may end with a line and optionally a column,", - /// " (?:+[0-9]+(:[0-9]+)?|:?\\([0-9]+([,:][0-9]+)?\\))?", - /// "))", - /// "# which must be followed by a matching quote", - /// "(?()\\k)", - /// "# and optionally a single closing symbol", - /// "[)}\\]>]?", - /// "# if line/column matched, may be followed by a description", - /// "(?():[^ 0-9][^ ]*)?", - /// "# which may be followed by trailing punctuation", - /// "[.,:)}\\]>]*", - /// "# and always includes trailing whitespace or end of line", - /// "([ ]+|$)" - /// ] - /// ] - pub path_hyperlink_regexes: Option>, - /// Timeout for hover and Cmd-click path hyperlink discovery in milliseconds. - /// - /// Default: 1 - pub path_hyperlink_timeout_ms: Option, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct TerminalSettingsContent { - #[serde(flatten)] - pub project: ProjectTerminalSettingsContent, - /// Sets the terminal's font size. - /// - /// If this option is not included, - /// the terminal will default to matching the buffer's font size. - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub font_size: Option, - /// Sets the terminal's font family. - /// - /// If this option is not included, - /// the terminal will default to matching the buffer's font family. - pub font_family: Option, - - /// Sets the terminal's font fallbacks. - /// - /// If this option is not included, - /// the terminal will default to matching the buffer's font fallbacks. - #[schemars(extend("uniqueItems" = true))] - pub font_fallbacks: Option>, - - /// Sets the terminal's line height. - /// - /// Default: comfortable - pub line_height: Option, - pub font_features: Option, - /// Sets the terminal's font weight in CSS weight units 0-900. - pub font_weight: Option, - /// Default cursor shape for the terminal. - /// Can be "bar", "block", "underline", or "hollow". - /// - /// Default: "block" - pub cursor_shape: Option, - /// Sets the cursor blinking behavior in the terminal. - /// - /// Default: terminal_controlled - pub blinking: Option, - /// Sets whether Alternate Scroll mode (code: ?1007) is active by default. - /// Alternate Scroll mode converts mouse scroll events into up / down key - /// presses when in the alternate screen (e.g. when running applications - /// like vim or less). The terminal can still set and unset this mode. - /// - /// Default: on - pub alternate_scroll: Option, - /// Sets whether the option key behaves as the meta key. - /// - /// Default: false - pub option_as_meta: Option, - /// Whether or not selecting text in the terminal will automatically - /// copy to the system clipboard. - /// - /// Default: false - pub copy_on_select: Option, - /// Whether to keep the text selection after copying it to the clipboard. - /// - /// Default: true - pub keep_selection_on_copy: Option, - /// Whether to show the terminal button in the status bar. - /// - /// Default: true - pub button: Option, - pub dock: Option, - /// Default width when the terminal is docked to the left or right. - /// - /// Default: 640 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_width: Option, - /// Default height when the terminal is docked to the bottom. - /// - /// Default: 320 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_height: Option, - /// The maximum number of lines to keep in the scrollback history. - /// Maximum allowed value is 100_000, all values above that will be treated as 100_000. - /// 0 disables the scrolling. - /// Existing terminals will not pick up this change until they are recreated. - /// See Alacritty documentation for more information. - /// - /// Default: 10_000 - pub max_scroll_history_lines: Option, - /// The multiplier for scrolling with the mouse wheel. - /// - /// Default: 1.0 - pub scroll_multiplier: Option, - /// Toolbar related settings - pub toolbar: Option, - /// Scrollbar-related settings - pub scrollbar: Option, - /// The minimum APCA perceptual contrast between foreground and background colors. - /// - /// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x, - /// especially for dark mode. Values range from 0 to 106. - /// - /// Based on APCA Readability Criterion (ARC) Bronze Simple Mode: - /// https://readtech.org/ARC/tests/bronze-simple-mode/ - /// - 0: No contrast adjustment - /// - 45: Minimum for large fluent text (36px+) - /// - 60: Minimum for other content text - /// - 75: Minimum for body text - /// - 90: Preferred for body text - /// - /// Default: 45 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub minimum_contrast: Option, -} - -/// Shell configuration to open the terminal with. -#[derive( - Clone, - Debug, - Default, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::EnumDiscriminants, -)] -#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))] -#[serde(rename_all = "snake_case")] -pub enum Shell { - /// Use the system's default terminal configuration in /etc/passwd - #[default] - System, - /// Use a specific program with no arguments. - Program(String), - /// Use a specific program with arguments. - WithArguments { - /// The program to run. - program: String, - /// The arguments to pass to the program. - args: Vec, - /// An optional string to override the title of the terminal tab - title_override: Option, - }, -} - -#[derive( - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::EnumDiscriminants, -)] -#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))] -#[serde(rename_all = "snake_case")] -pub enum WorkingDirectory { - /// Use the current file's project directory. Fallback to the - /// first project directory strategy if unsuccessful. - CurrentProjectDirectory, - /// Use the first project in this workspace's directory. Fallback to using - /// this platform's home directory. - FirstProjectDirectory, - /// Always use this platform's home directory (if it can be found). - AlwaysHome, - /// Always use a specific directory. This value will be shell expanded. - /// If this path is not a valid directory the terminal will default to - /// this platform's home directory (if it can be found). - Always { directory: String }, -} - -#[with_fallible_options] -#[derive( - Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default, -)] -pub struct ScrollbarSettingsContent { - /// When to show the scrollbar in the terminal. - /// - /// Default: inherits editor scrollbar settings - pub show: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom, Default)] -#[serde(rename_all = "snake_case")] -pub enum TerminalLineHeight { - /// Use a line height that's comfortable for reading, 1.618 - #[default] - Comfortable, - /// Use a standard line height, 1.3. This option is useful for TUIs, - /// particularly if they use box characters - Standard, - /// Use a custom line height. - Custom(#[serde(serialize_with = "crate::serialize_f32_with_two_decimal_places")] f32), -} - -impl TerminalLineHeight { - pub fn value(&self) -> AbsoluteLength { - let value = match self { - TerminalLineHeight::Comfortable => 1.618, - TerminalLineHeight::Standard => 1.3, - TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.), - }; - px(value).into() - } -} - -/// When to show the scrollbar. -/// -/// Default: auto -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ShowScrollbar { - /// Show the scrollbar if there's important information or - /// follow the system's configured behavior. - #[default] - Auto, - /// Match the system's configured behavior. - System, - /// Always show the scrollbar. - Always, - /// Never show the scrollbar. - Never, -} - -#[derive( - Clone, - Copy, - Debug, - Default, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -// todo() -> combine with CursorShape -pub enum CursorShapeContent { - /// Cursor is a block like `█`. - #[default] - Block, - /// Cursor is an underscore like `_`. - Underline, - /// Cursor is a vertical bar like `⎸`. - Bar, - /// Cursor is a hollow box like `▯`. - Hollow, -} - -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum TerminalBlink { - /// Never blink the cursor, ignoring the terminal mode. - Off, - /// Default the cursor blink to off, but allow the terminal to - /// set blinking. - TerminalControlled, - /// Always blink the cursor, ignoring the terminal mode. - On, -} - -#[derive( - Clone, - Copy, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum AlternateScroll { - On, - Off, -} - -// Toolbar related settings -#[with_fallible_options] -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)] -pub struct TerminalToolbarContent { - /// Whether to display the terminal title in breadcrumbs inside the terminal pane. - /// Only shown if the terminal title is not empty. - /// - /// The shell running in the terminal needs to be configured to emit the title. - /// Example: `echo -e "\e]2;New Title\007";` - /// - /// Default: true - pub breadcrumbs: Option, -} - -#[derive( - Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom, -)] -#[serde(rename_all = "snake_case")] -pub enum CondaManager { - /// Automatically detect the conda manager - #[default] - Auto, - /// Use conda - Conda, - /// Use mamba - Mamba, - /// Use micromamba - Micromamba, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub enum VenvSettings { - #[default] - Off, - On { - /// Default directories to search for virtual environments, relative - /// to the current working directory. We recommend overriding this - /// in your project's settings, rather than globally. - activate_script: Option, - venv_name: Option, - directories: Option>, - /// Preferred Conda manager to use when activating Conda environments. - /// - /// Default: auto - conda_manager: Option, - }, -} -#[with_fallible_options] -pub struct VenvSettingsContent<'a> { - pub activate_script: ActivateScript, - pub venv_name: &'a str, - pub directories: &'a [PathBuf], - pub conda_manager: CondaManager, -} - -impl VenvSettings { - pub fn as_option(&self) -> Option> { - match self { - VenvSettings::Off => None, - VenvSettings::On { - activate_script, - venv_name, - directories, - conda_manager, - } => Some(VenvSettingsContent { - activate_script: activate_script.unwrap_or(ActivateScript::Default), - venv_name: venv_name.as_deref().unwrap_or(""), - directories: directories.as_deref().unwrap_or(&[]), - conda_manager: conda_manager.unwrap_or(CondaManager::Auto), - }), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -#[serde(untagged)] -pub enum PathHyperlinkRegex { - SingleLine(String), - MultiLine(Vec), -} - -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum TerminalDockPosition { - Left, - Bottom, - Right, -} - -#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub enum ActivateScript { - #[default] - Default, - Csh, - Fish, - Nushell, - PowerShell, - Pyenv, -} - -#[cfg(test)] -mod test { - use serde_json::json; - - use crate::{ProjectSettingsContent, Shell, UserSettingsContent}; - - #[test] - fn test_project_settings() { - let project_content = - json!({"terminal": {"shell": {"program": "/bin/project"}}, "option_as_meta": true}); - - let user_content = - json!({"terminal": {"shell": {"program": "/bin/user"}}, "option_as_meta": false}); - - let user_settings = serde_json::from_value::(user_content).unwrap(); - let project_settings = - serde_json::from_value::(project_content).unwrap(); - - assert_eq!( - user_settings.content.terminal.unwrap().project.shell, - Some(Shell::Program("/bin/user".to_owned())) - ); - assert_eq!(user_settings.content.project.terminal, None); - assert_eq!( - project_settings.terminal.unwrap().shell, - Some(Shell::Program("/bin/project".to_owned())) - ); - } -} diff --git a/crates/settings/src/settings_content/theme.rs b/crates/settings/src/settings_content/theme.rs deleted file mode 100644 index 94045b75a1..0000000000 --- a/crates/settings/src/settings_content/theme.rs +++ /dev/null @@ -1,1269 +0,0 @@ -use collections::{HashMap, IndexMap}; -use gpui::{FontFallbacks, FontFeatures, FontStyle, FontWeight, SharedString}; -use schemars::{JsonSchema, JsonSchema_repr}; -use serde::{Deserialize, Deserializer, Serialize}; -use serde_json::Value; -use serde_repr::{Deserialize_repr, Serialize_repr}; -use settings_macros::{MergeFrom, with_fallible_options}; -use std::{fmt::Display, sync::Arc}; - -use crate::serialize_f32_with_two_decimal_places; - -/// Settings for rendering text in UI and text buffers. - -#[with_fallible_options] -#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct ThemeSettingsContent { - /// The default font size for text in the UI. - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub ui_font_size: Option, - /// The name of a font to use for rendering in the UI. - pub ui_font_family: Option, - /// The font fallbacks to use for rendering in the UI. - #[schemars(default = "default_font_fallbacks")] - #[schemars(extend("uniqueItems" = true))] - pub ui_font_fallbacks: Option>, - /// The OpenType features to enable for text in the UI. - #[schemars(default = "default_font_features")] - pub ui_font_features: Option, - /// The weight of the UI font in CSS units from 100 to 900. - #[schemars(default = "default_buffer_font_weight")] - pub ui_font_weight: Option, - /// The name of a font to use for rendering in text buffers. - pub buffer_font_family: Option, - /// The font fallbacks to use for rendering in text buffers. - #[schemars(extend("uniqueItems" = true))] - pub buffer_font_fallbacks: Option>, - /// The default font size for rendering in text buffers. - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub buffer_font_size: Option, - /// The weight of the editor font in CSS units from 100 to 900. - #[schemars(default = "default_buffer_font_weight")] - pub buffer_font_weight: Option, - /// The buffer's line height. - pub buffer_line_height: Option, - /// The OpenType features to enable for rendering in text buffers. - #[schemars(default = "default_font_features")] - pub buffer_font_features: Option, - /// The font size for agent responses in the agent panel. Falls back to the UI font size if unset. - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub agent_ui_font_size: Option, - /// The font size for user messages in the agent panel. - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub agent_buffer_font_size: Option, - /// The name of the Zed theme to use. - pub theme: Option, - /// The name of the icon theme to use. - pub icon_theme: Option, - - /// UNSTABLE: Expect many elements to be broken. - /// - // Controls the density of the UI. - #[serde(rename = "unstable.ui_density")] - pub ui_density: Option, - - /// How much to fade out unused code. - #[schemars(range(min = 0.0, max = 0.9))] - pub unnecessary_code_fade: Option, - - /// EXPERIMENTAL: Overrides for the current theme. - /// - /// These values will override the ones on the current theme specified in `theme`. - #[serde(rename = "experimental.theme_overrides")] - pub experimental_theme_overrides: Option, - - /// Overrides per theme - /// - /// These values will override the ones on the specified theme - #[serde(default)] - pub theme_overrides: HashMap, -} - -#[derive( - Clone, - Copy, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - PartialOrd, - derive_more::FromStr, -)] -#[serde(transparent)] -pub struct CodeFade(#[serde(serialize_with = "serialize_f32_with_two_decimal_places")] pub f32); - -impl Display for CodeFade { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:.2}", self.0) - } -} - -impl From for CodeFade { - fn from(x: f32) -> Self { - Self(x) - } -} - -fn default_font_features() -> Option { - Some(FontFeatures::default()) -} - -fn default_font_fallbacks() -> Option { - Some(FontFallbacks::default()) -} - -fn default_buffer_font_weight() -> Option { - Some(FontWeight::default()) -} - -/// Represents the selection of a theme, which can be either static or dynamic. -#[derive( - Clone, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::EnumDiscriminants, -)] -#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))] -#[serde(untagged)] -pub enum ThemeSelection { - /// A static theme selection, represented by a single theme name. - Static(ThemeName), - /// A dynamic theme selection, which can change based the [ThemeMode]. - Dynamic { - /// The mode used to determine which theme to use. - #[serde(default)] - mode: ThemeAppearanceMode, - /// The theme to use for light mode. - light: ThemeName, - /// The theme to use for dark mode. - dark: ThemeName, - }, -} - -/// Represents the selection of an icon theme, which can be either static or dynamic. -#[derive( - Clone, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::EnumDiscriminants, -)] -#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))] -#[serde(untagged)] -pub enum IconThemeSelection { - /// A static icon theme selection, represented by a single icon theme name. - Static(IconThemeName), - /// A dynamic icon theme selection, which can change based on the [`ThemeMode`]. - Dynamic { - /// The mode used to determine which theme to use. - #[serde(default)] - mode: ThemeAppearanceMode, - /// The icon theme to use for light mode. - light: IconThemeName, - /// The icon theme to use for dark mode. - dark: IconThemeName, - }, -} - -/// The mode use to select a theme. -/// -/// `Light` and `Dark` will select their respective themes. -/// -/// `System` will select the theme based on the system's appearance. -#[derive( - Debug, - PartialEq, - Eq, - Clone, - Copy, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ThemeAppearanceMode { - /// Use the specified `light` theme. - Light, - - /// Use the specified `dark` theme. - Dark, - - /// Use the theme based on the system's appearance. - #[default] - System, -} - -/// Specifies the density of the UI. -/// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078) -#[derive( - Debug, - Default, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Clone, - Copy, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, -)] -#[serde(rename_all = "snake_case")] -pub enum UiDensity { - /// A denser UI with tighter spacing and smaller elements. - #[serde(alias = "compact")] - Compact, - #[default] - #[serde(alias = "default")] - /// The default UI density. - Default, - #[serde(alias = "comfortable")] - /// A looser UI with more spacing and larger elements. - Comfortable, -} - -impl UiDensity { - /// The spacing ratio of a given density. - /// TODO: Standardize usage throughout the app or remove - pub fn spacing_ratio(self) -> f32 { - match self { - UiDensity::Compact => 0.75, - UiDensity::Default => 1.0, - UiDensity::Comfortable => 1.25, - } - } -} - -/// Font family name. -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)] -#[serde(transparent)] -pub struct FontFamilyName(pub Arc); - -impl AsRef for FontFamilyName { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl From for FontFamilyName { - fn from(value: SharedString) -> Self { - Self(Arc::from(value)) - } -} - -impl From for SharedString { - fn from(value: FontFamilyName) -> Self { - SharedString::new(value.0) - } -} - -impl From for FontFamilyName { - fn from(value: String) -> Self { - Self(Arc::from(value)) - } -} - -impl From for String { - fn from(value: FontFamilyName) -> Self { - value.0.to_string() - } -} - -/// The buffer's line height. -#[derive( - Clone, - Copy, - Debug, - Serialize, - Deserialize, - PartialEq, - JsonSchema, - MergeFrom, - Default, - strum::EnumDiscriminants, -)] -#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))] -#[serde(rename_all = "snake_case")] -pub enum BufferLineHeight { - /// A less dense line height. - #[default] - Comfortable, - /// The default line height. - Standard, - /// A custom line height, where 1.0 is the font's height. Must be at least 1.0. - Custom(#[serde(deserialize_with = "deserialize_line_height")] f32), -} - -fn deserialize_line_height<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - let value = f32::deserialize(deserializer)?; - if value < 1.0 { - return Err(serde::de::Error::custom( - "buffer_line_height.custom must be at least 1.0", - )); - } - - Ok(value) -} - -/// The content of a serialized theme. -#[with_fallible_options] -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -#[serde(default)] -pub struct ThemeStyleContent { - #[serde(rename = "background.appearance")] - pub window_background_appearance: Option, - - #[serde(default)] - pub accents: Vec, - - #[serde(flatten, default)] - pub colors: ThemeColorsContent, - - #[serde(flatten, default)] - pub status: StatusColorsContent, - - #[serde(default)] - pub players: Vec, - - /// The styles for syntax nodes. - #[serde(default)] - pub syntax: IndexMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct AccentContent(pub Option); - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct PlayerColorContent { - pub cursor: Option, - pub background: Option, - pub selection: Option, -} - -/// Theme name. -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)] -#[serde(transparent)] -pub struct ThemeName(pub Arc); - -/// Icon Theme Name -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)] -#[serde(transparent)] -pub struct IconThemeName(pub Arc); - -#[with_fallible_options] -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -#[serde(default)] -pub struct ThemeColorsContent { - /// Border color. Used for most borders, is usually a high contrast color. - #[serde(rename = "border")] - pub border: Option, - - /// Border color. Used for deemphasized borders, like a visual divider between two sections - #[serde(rename = "border.variant")] - pub border_variant: Option, - - /// Border color. Used for focused elements, like keyboard focused list item. - #[serde(rename = "border.focused")] - pub border_focused: Option, - - /// Border color. Used for selected elements, like an active search filter or selected checkbox. - #[serde(rename = "border.selected")] - pub border_selected: Option, - - /// Border color. Used for transparent borders. Used for placeholder borders when an element gains a border on state change. - #[serde(rename = "border.transparent")] - pub border_transparent: Option, - - /// Border color. Used for disabled elements, like a disabled input or button. - #[serde(rename = "border.disabled")] - pub border_disabled: Option, - - /// Background color. Used for elevated surfaces, like a context menu, popup, or dialog. - #[serde(rename = "elevated_surface.background")] - pub elevated_surface_background: Option, - - /// Background Color. Used for grounded surfaces like a panel or tab. - #[serde(rename = "surface.background")] - pub surface_background: Option, - - /// Background Color. Used for the app background and blank panels or windows. - #[serde(rename = "background")] - pub background: Option, - - /// Background Color. Used for the background of an element that should have a different background than the surface it's on. - /// - /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons... - /// - /// For an element that should have the same background as the surface it's on, use `ghost_element_background`. - #[serde(rename = "element.background")] - pub element_background: Option, - - /// Background Color. Used for the hover state of an element that should have a different background than the surface it's on. - /// - /// Hover states are triggered by the mouse entering an element, or a finger touching an element on a touch screen. - #[serde(rename = "element.hover")] - pub element_hover: Option, - - /// Background Color. Used for the active state of an element that should have a different background than the surface it's on. - /// - /// Active states are triggered by the mouse button being pressed down on an element, or the Return button or other activator being pressed. - #[serde(rename = "element.active")] - pub element_active: Option, - - /// Background Color. Used for the selected state of an element that should have a different background than the surface it's on. - /// - /// Selected states are triggered by the element being selected (or "activated") by the user. - /// - /// This could include a selected checkbox, a toggleable button that is toggled on, etc. - #[serde(rename = "element.selected")] - pub element_selected: Option, - - /// Background Color. Used for the disabled state of an element that should have a different background than the surface it's on. - /// - /// Disabled states are shown when a user cannot interact with an element, like a disabled button or input. - #[serde(rename = "element.disabled")] - pub element_disabled: Option, - - /// Background Color. Used for the background of selections in a UI element. - #[serde(rename = "element.selection_background")] - pub element_selection_background: Option, - - /// Background Color. Used for the area that shows where a dragged element will be dropped. - #[serde(rename = "drop_target.background")] - pub drop_target_background: Option, - - /// Border Color. Used for the border that shows where a dragged element will be dropped. - #[serde(rename = "drop_target.border")] - pub drop_target_border: Option, - - /// Used for the background of a ghost element that should have the same background as the surface it's on. - /// - /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons... - /// - /// For an element that should have a different background than the surface it's on, use `element_background`. - #[serde(rename = "ghost_element.background")] - pub ghost_element_background: Option, - - /// Background Color. Used for the hover state of a ghost element that should have the same background as the surface it's on. - /// - /// Hover states are triggered by the mouse entering an element, or a finger touching an element on a touch screen. - #[serde(rename = "ghost_element.hover")] - pub ghost_element_hover: Option, - - /// Background Color. Used for the active state of a ghost element that should have the same background as the surface it's on. - /// - /// Active states are triggered by the mouse button being pressed down on an element, or the Return button or other activator being pressed. - #[serde(rename = "ghost_element.active")] - pub ghost_element_active: Option, - - /// Background Color. Used for the selected state of a ghost element that should have the same background as the surface it's on. - /// - /// Selected states are triggered by the element being selected (or "activated") by the user. - /// - /// This could include a selected checkbox, a toggleable button that is toggled on, etc. - #[serde(rename = "ghost_element.selected")] - pub ghost_element_selected: Option, - - /// Background Color. Used for the disabled state of a ghost element that should have the same background as the surface it's on. - /// - /// Disabled states are shown when a user cannot interact with an element, like a disabled button or input. - #[serde(rename = "ghost_element.disabled")] - pub ghost_element_disabled: Option, - - /// Text Color. Default text color used for most text. - #[serde(rename = "text")] - pub text: Option, - - /// Text Color. Color of muted or deemphasized text. It is a subdued version of the standard text color. - #[serde(rename = "text.muted")] - pub text_muted: Option, - - /// Text Color. Color of the placeholder text typically shown in input fields to guide the user to enter valid data. - #[serde(rename = "text.placeholder")] - pub text_placeholder: Option, - - /// Text Color. Color used for text denoting disabled elements. Typically, the color is faded or grayed out to emphasize the disabled state. - #[serde(rename = "text.disabled")] - pub text_disabled: Option, - - /// Text Color. Color used for emphasis or highlighting certain text, like an active filter or a matched character in a search. - #[serde(rename = "text.accent")] - pub text_accent: Option, - - /// Fill Color. Used for the default fill color of an icon. - #[serde(rename = "icon")] - pub icon: Option, - - /// Fill Color. Used for the muted or deemphasized fill color of an icon. - /// - /// This might be used to show an icon in an inactive pane, or to deemphasize a series of icons to give them less visual weight. - #[serde(rename = "icon.muted")] - pub icon_muted: Option, - - /// Fill Color. Used for the disabled fill color of an icon. - /// - /// Disabled states are shown when a user cannot interact with an element, like a icon button. - #[serde(rename = "icon.disabled")] - pub icon_disabled: Option, - - /// Fill Color. Used for the placeholder fill color of an icon. - /// - /// This might be used to show an icon in an input that disappears when the user enters text. - #[serde(rename = "icon.placeholder")] - pub icon_placeholder: Option, - - /// Fill Color. Used for the accent fill color of an icon. - /// - /// This might be used to show when a toggleable icon button is selected. - #[serde(rename = "icon.accent")] - pub icon_accent: Option, - - /// Color used to accent some of the debuggers elements - /// Only accent breakpoint & breakpoint related symbols right now - #[serde(rename = "debugger.accent")] - pub debugger_accent: Option, - - #[serde(rename = "status_bar.background")] - pub status_bar_background: Option, - - #[serde(rename = "title_bar.background")] - pub title_bar_background: Option, - - #[serde(rename = "title_bar.inactive_background")] - pub title_bar_inactive_background: Option, - - #[serde(rename = "toolbar.background")] - pub toolbar_background: Option, - - #[serde(rename = "tab_bar.background")] - pub tab_bar_background: Option, - - #[serde(rename = "tab.inactive_background")] - pub tab_inactive_background: Option, - - #[serde(rename = "tab.active_background")] - pub tab_active_background: Option, - - #[serde(rename = "search.match_background")] - pub search_match_background: Option, - - #[serde(rename = "search.active_match_background")] - pub search_active_match_background: Option, - - #[serde(rename = "panel.background")] - pub panel_background: Option, - - #[serde(rename = "panel.focused_border")] - pub panel_focused_border: Option, - - #[serde(rename = "panel.indent_guide")] - pub panel_indent_guide: Option, - - #[serde(rename = "panel.indent_guide_hover")] - pub panel_indent_guide_hover: Option, - - #[serde(rename = "panel.indent_guide_active")] - pub panel_indent_guide_active: Option, - - #[serde(rename = "panel.overlay_background")] - pub panel_overlay_background: Option, - - #[serde(rename = "panel.overlay_hover")] - pub panel_overlay_hover: Option, - - #[serde(rename = "pane.focused_border")] - pub pane_focused_border: Option, - - #[serde(rename = "pane_group.border")] - pub pane_group_border: Option, - - /// The deprecated version of `scrollbar.thumb.background`. - /// - /// Don't use this field. - #[serde(rename = "scrollbar_thumb.background", skip_serializing)] - #[schemars(skip)] - pub deprecated_scrollbar_thumb_background: Option, - - /// The color of the scrollbar thumb. - #[serde(rename = "scrollbar.thumb.background")] - pub scrollbar_thumb_background: Option, - - /// The color of the scrollbar thumb when hovered over. - #[serde(rename = "scrollbar.thumb.hover_background")] - pub scrollbar_thumb_hover_background: Option, - - /// The color of the scrollbar thumb whilst being actively dragged. - #[serde(rename = "scrollbar.thumb.active_background")] - pub scrollbar_thumb_active_background: Option, - - /// The border color of the scrollbar thumb. - #[serde(rename = "scrollbar.thumb.border")] - pub scrollbar_thumb_border: Option, - - /// The background color of the scrollbar track. - #[serde(rename = "scrollbar.track.background")] - pub scrollbar_track_background: Option, - - /// The border color of the scrollbar track. - #[serde(rename = "scrollbar.track.border")] - pub scrollbar_track_border: Option, - - /// The color of the minimap thumb. - #[serde(rename = "minimap.thumb.background")] - pub minimap_thumb_background: Option, - - /// The color of the minimap thumb when hovered over. - #[serde(rename = "minimap.thumb.hover_background")] - pub minimap_thumb_hover_background: Option, - - /// The color of the minimap thumb whilst being actively dragged. - #[serde(rename = "minimap.thumb.active_background")] - pub minimap_thumb_active_background: Option, - - /// The border color of the minimap thumb. - #[serde(rename = "minimap.thumb.border")] - pub minimap_thumb_border: Option, - - #[serde(rename = "editor.foreground")] - pub editor_foreground: Option, - - #[serde(rename = "editor.background")] - pub editor_background: Option, - - #[serde(rename = "editor.gutter.background")] - pub editor_gutter_background: Option, - - #[serde(rename = "editor.subheader.background")] - pub editor_subheader_background: Option, - - #[serde(rename = "editor.active_line.background")] - pub editor_active_line_background: Option, - - #[serde(rename = "editor.highlighted_line.background")] - pub editor_highlighted_line_background: Option, - - /// Background of active line of debugger - #[serde(rename = "editor.debugger_active_line.background")] - pub editor_debugger_active_line_background: Option, - - /// Text Color. Used for the text of the line number in the editor gutter. - #[serde(rename = "editor.line_number")] - pub editor_line_number: Option, - - /// Text Color. Used for the text of the line number in the editor gutter when the line is highlighted. - #[serde(rename = "editor.active_line_number")] - pub editor_active_line_number: Option, - - /// Text Color. Used for the text of the line number in the editor gutter when the line is hovered over. - #[serde(rename = "editor.hover_line_number")] - pub editor_hover_line_number: Option, - - /// Text Color. Used to mark invisible characters in the editor. - /// - /// Example: spaces, tabs, carriage returns, etc. - #[serde(rename = "editor.invisible")] - pub editor_invisible: Option, - - #[serde(rename = "editor.wrap_guide")] - pub editor_wrap_guide: Option, - - #[serde(rename = "editor.active_wrap_guide")] - pub editor_active_wrap_guide: Option, - - #[serde(rename = "editor.indent_guide")] - pub editor_indent_guide: Option, - - #[serde(rename = "editor.indent_guide_active")] - pub editor_indent_guide_active: Option, - - /// Read-access of a symbol, like reading a variable. - /// - /// A document highlight is a range inside a text document which deserves - /// special attention. Usually a document highlight is visualized by changing - /// the background color of its range. - #[serde(rename = "editor.document_highlight.read_background")] - pub editor_document_highlight_read_background: Option, - - /// Read-access of a symbol, like reading a variable. - /// - /// A document highlight is a range inside a text document which deserves - /// special attention. Usually a document highlight is visualized by changing - /// the background color of its range. - #[serde(rename = "editor.document_highlight.write_background")] - pub editor_document_highlight_write_background: Option, - - /// Highlighted brackets background color. - /// - /// Matching brackets in the cursor scope are highlighted with this background color. - #[serde(rename = "editor.document_highlight.bracket_background")] - pub editor_document_highlight_bracket_background: Option, - - /// Terminal background color. - #[serde(rename = "terminal.background")] - pub terminal_background: Option, - - /// Terminal foreground color. - #[serde(rename = "terminal.foreground")] - pub terminal_foreground: Option, - - /// Terminal ANSI background color. - #[serde(rename = "terminal.ansi.background")] - pub terminal_ansi_background: Option, - - /// Bright terminal foreground color. - #[serde(rename = "terminal.bright_foreground")] - pub terminal_bright_foreground: Option, - - /// Dim terminal foreground color. - #[serde(rename = "terminal.dim_foreground")] - pub terminal_dim_foreground: Option, - - /// Black ANSI terminal color. - #[serde(rename = "terminal.ansi.black")] - pub terminal_ansi_black: Option, - - /// Bright black ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_black")] - pub terminal_ansi_bright_black: Option, - - /// Dim black ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_black")] - pub terminal_ansi_dim_black: Option, - - /// Red ANSI terminal color. - #[serde(rename = "terminal.ansi.red")] - pub terminal_ansi_red: Option, - - /// Bright red ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_red")] - pub terminal_ansi_bright_red: Option, - - /// Dim red ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_red")] - pub terminal_ansi_dim_red: Option, - - /// Green ANSI terminal color. - #[serde(rename = "terminal.ansi.green")] - pub terminal_ansi_green: Option, - - /// Bright green ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_green")] - pub terminal_ansi_bright_green: Option, - - /// Dim green ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_green")] - pub terminal_ansi_dim_green: Option, - - /// Yellow ANSI terminal color. - #[serde(rename = "terminal.ansi.yellow")] - pub terminal_ansi_yellow: Option, - - /// Bright yellow ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_yellow")] - pub terminal_ansi_bright_yellow: Option, - - /// Dim yellow ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_yellow")] - pub terminal_ansi_dim_yellow: Option, - - /// Blue ANSI terminal color. - #[serde(rename = "terminal.ansi.blue")] - pub terminal_ansi_blue: Option, - - /// Bright blue ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_blue")] - pub terminal_ansi_bright_blue: Option, - - /// Dim blue ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_blue")] - pub terminal_ansi_dim_blue: Option, - - /// Magenta ANSI terminal color. - #[serde(rename = "terminal.ansi.magenta")] - pub terminal_ansi_magenta: Option, - - /// Bright magenta ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_magenta")] - pub terminal_ansi_bright_magenta: Option, - - /// Dim magenta ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_magenta")] - pub terminal_ansi_dim_magenta: Option, - - /// Cyan ANSI terminal color. - #[serde(rename = "terminal.ansi.cyan")] - pub terminal_ansi_cyan: Option, - - /// Bright cyan ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_cyan")] - pub terminal_ansi_bright_cyan: Option, - - /// Dim cyan ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_cyan")] - pub terminal_ansi_dim_cyan: Option, - - /// White ANSI terminal color. - #[serde(rename = "terminal.ansi.white")] - pub terminal_ansi_white: Option, - - /// Bright white ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_white")] - pub terminal_ansi_bright_white: Option, - - /// Dim white ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_white")] - pub terminal_ansi_dim_white: Option, - - #[serde(rename = "link_text.hover")] - pub link_text_hover: Option, - - /// Added version control color. - #[serde(rename = "version_control.added")] - pub version_control_added: Option, - - /// Deleted version control color. - #[serde(rename = "version_control.deleted")] - pub version_control_deleted: Option, - - /// Modified version control color. - #[serde(rename = "version_control.modified")] - pub version_control_modified: Option, - - /// Renamed version control color. - #[serde(rename = "version_control.renamed")] - pub version_control_renamed: Option, - - /// Conflict version control color. - #[serde(rename = "version_control.conflict")] - pub version_control_conflict: Option, - - /// Ignored version control color. - #[serde(rename = "version_control.ignored")] - pub version_control_ignored: Option, - - /// Color for added words in word diffs. - #[serde(rename = "version_control.word_added")] - pub version_control_word_added: Option, - - /// Color for deleted words in word diffs. - #[serde(rename = "version_control.word_deleted")] - pub version_control_word_deleted: Option, - - /// Background color for row highlights of "ours" regions in merge conflicts. - #[serde(rename = "version_control.conflict_marker.ours")] - pub version_control_conflict_marker_ours: Option, - - /// Background color for row highlights of "theirs" regions in merge conflicts. - #[serde(rename = "version_control.conflict_marker.theirs")] - pub version_control_conflict_marker_theirs: Option, - - /// Deprecated in favor of `version_control_conflict_marker_ours`. - #[deprecated] - pub version_control_conflict_ours_background: Option, - - /// Deprecated in favor of `version_control_conflict_marker_theirs`. - #[deprecated] - pub version_control_conflict_theirs_background: Option, - - /// Background color for Vim Normal mode indicator. - #[serde(rename = "vim.normal.background")] - pub vim_normal_background: Option, - /// Background color for Vim Insert mode indicator. - #[serde(rename = "vim.insert.background")] - pub vim_insert_background: Option, - /// Background color for Vim Replace mode indicator. - #[serde(rename = "vim.replace.background")] - pub vim_replace_background: Option, - /// Background color for Vim Visual mode indicator. - #[serde(rename = "vim.visual.background")] - pub vim_visual_background: Option, - /// Background color for Vim Visual Line mode indicator. - #[serde(rename = "vim.visual_line.background")] - pub vim_visual_line_background: Option, - /// Background color for Vim Visual Block mode indicator. - #[serde(rename = "vim.visual_block.background")] - pub vim_visual_block_background: Option, - /// Background color for Vim Helix Normal mode indicator. - #[serde(rename = "vim.helix_normal.background")] - pub vim_helix_normal_background: Option, - /// Background color for Vim Helix Select mode indicator. - #[serde(rename = "vim.helix_select.background")] - pub vim_helix_select_background: Option, - - /// Text color for Vim mode indicator label. - #[serde(rename = "vim.mode.text")] - pub vim_mode_text: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -#[serde(default)] -pub struct HighlightStyleContent { - pub color: Option, - - #[serde( - skip_serializing_if = "Option::is_none", - deserialize_with = "treat_error_as_none" - )] - pub background_color: Option, - - #[serde( - skip_serializing_if = "Option::is_none", - deserialize_with = "treat_error_as_none" - )] - pub font_style: Option, - - #[serde( - skip_serializing_if = "Option::is_none", - deserialize_with = "treat_error_as_none" - )] - pub font_weight: Option, -} - -impl HighlightStyleContent { - pub fn is_empty(&self) -> bool { - self.color.is_none() - && self.background_color.is_none() - && self.font_style.is_none() - && self.font_weight.is_none() - } -} - -fn treat_error_as_none<'de, T, D>(deserializer: D) -> Result, D::Error> -where - T: Deserialize<'de>, - D: Deserializer<'de>, -{ - let value: Value = Deserialize::deserialize(deserializer)?; - Ok(T::deserialize(value).ok()) -} - -#[with_fallible_options] -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -#[serde(default)] -pub struct StatusColorsContent { - /// Indicates some kind of conflict, like a file changed on disk while it was open, or - /// merge conflicts in a Git repository. - #[serde(rename = "conflict")] - pub conflict: Option, - - #[serde(rename = "conflict.background")] - pub conflict_background: Option, - - #[serde(rename = "conflict.border")] - pub conflict_border: Option, - - /// Indicates something new, like a new file added to a Git repository. - #[serde(rename = "created")] - pub created: Option, - - #[serde(rename = "created.background")] - pub created_background: Option, - - #[serde(rename = "created.border")] - pub created_border: Option, - - /// Indicates that something no longer exists, like a deleted file. - #[serde(rename = "deleted")] - pub deleted: Option, - - #[serde(rename = "deleted.background")] - pub deleted_background: Option, - - #[serde(rename = "deleted.border")] - pub deleted_border: Option, - - /// Indicates a system error, a failed operation or a diagnostic error. - #[serde(rename = "error")] - pub error: Option, - - #[serde(rename = "error.background")] - pub error_background: Option, - - #[serde(rename = "error.border")] - pub error_border: Option, - - /// Represents a hidden status, such as a file being hidden in a file tree. - #[serde(rename = "hidden")] - pub hidden: Option, - - #[serde(rename = "hidden.background")] - pub hidden_background: Option, - - #[serde(rename = "hidden.border")] - pub hidden_border: Option, - - /// Indicates a hint or some kind of additional information. - #[serde(rename = "hint")] - pub hint: Option, - - #[serde(rename = "hint.background")] - pub hint_background: Option, - - #[serde(rename = "hint.border")] - pub hint_border: Option, - - /// Indicates that something is deliberately ignored, such as a file or operation ignored by Git. - #[serde(rename = "ignored")] - pub ignored: Option, - - #[serde(rename = "ignored.background")] - pub ignored_background: Option, - - #[serde(rename = "ignored.border")] - pub ignored_border: Option, - - /// Represents informational status updates or messages. - #[serde(rename = "info")] - pub info: Option, - - #[serde(rename = "info.background")] - pub info_background: Option, - - #[serde(rename = "info.border")] - pub info_border: Option, - - /// Indicates a changed or altered status, like a file that has been edited. - #[serde(rename = "modified")] - pub modified: Option, - - #[serde(rename = "modified.background")] - pub modified_background: Option, - - #[serde(rename = "modified.border")] - pub modified_border: Option, - - /// Indicates something that is predicted, like automatic code completion, or generated code. - #[serde(rename = "predictive")] - pub predictive: Option, - - #[serde(rename = "predictive.background")] - pub predictive_background: Option, - - #[serde(rename = "predictive.border")] - pub predictive_border: Option, - - /// Represents a renamed status, such as a file that has been renamed. - #[serde(rename = "renamed")] - pub renamed: Option, - - #[serde(rename = "renamed.background")] - pub renamed_background: Option, - - #[serde(rename = "renamed.border")] - pub renamed_border: Option, - - /// Indicates a successful operation or task completion. - #[serde(rename = "success")] - pub success: Option, - - #[serde(rename = "success.background")] - pub success_background: Option, - - #[serde(rename = "success.border")] - pub success_border: Option, - - /// Indicates some kind of unreachable status, like a block of code that can never be reached. - #[serde(rename = "unreachable")] - pub unreachable: Option, - - #[serde(rename = "unreachable.background")] - pub unreachable_background: Option, - - #[serde(rename = "unreachable.border")] - pub unreachable_border: Option, - - /// Represents a warning status, like an operation that is about to fail. - #[serde(rename = "warning")] - pub warning: Option, - - #[serde(rename = "warning.background")] - pub warning_background: Option, - - #[serde(rename = "warning.border")] - pub warning_border: Option, -} - -/// The background appearance of the window. -#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub enum WindowBackgroundContent { - Opaque, - Transparent, - Blurred, -} - -impl Into for WindowBackgroundContent { - fn into(self) -> gpui::WindowBackgroundAppearance { - match self { - WindowBackgroundContent::Opaque => gpui::WindowBackgroundAppearance::Opaque, - WindowBackgroundContent::Transparent => gpui::WindowBackgroundAppearance::Transparent, - WindowBackgroundContent::Blurred => gpui::WindowBackgroundAppearance::Blurred, - } - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum FontStyleContent { - Normal, - Italic, - Oblique, -} - -impl From for FontStyle { - fn from(value: FontStyleContent) -> Self { - match value { - FontStyleContent::Normal => FontStyle::Normal, - FontStyleContent::Italic => FontStyle::Italic, - FontStyleContent::Oblique => FontStyle::Oblique, - } - } -} - -#[derive( - Debug, Clone, Copy, Serialize_repr, Deserialize_repr, JsonSchema_repr, PartialEq, MergeFrom, -)] -#[repr(u16)] -pub enum FontWeightContent { - Thin = 100, - ExtraLight = 200, - Light = 300, - Normal = 400, - Medium = 500, - Semibold = 600, - Bold = 700, - ExtraBold = 800, - Black = 900, -} - -impl From for FontWeight { - fn from(value: FontWeightContent) -> Self { - match value { - FontWeightContent::Thin => FontWeight::THIN, - FontWeightContent::ExtraLight => FontWeight::EXTRA_LIGHT, - FontWeightContent::Light => FontWeight::LIGHT, - FontWeightContent::Normal => FontWeight::NORMAL, - FontWeightContent::Medium => FontWeight::MEDIUM, - FontWeightContent::Semibold => FontWeight::SEMIBOLD, - FontWeightContent::Bold => FontWeight::BOLD, - FontWeightContent::ExtraBold => FontWeight::EXTRA_BOLD, - FontWeightContent::Black => FontWeight::BLACK, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn test_buffer_line_height_deserialize_valid() { - assert_eq!( - serde_json::from_value::(json!("comfortable")).unwrap(), - BufferLineHeight::Comfortable - ); - assert_eq!( - serde_json::from_value::(json!("standard")).unwrap(), - BufferLineHeight::Standard - ); - assert_eq!( - serde_json::from_value::(json!({"custom": 1.0})).unwrap(), - BufferLineHeight::Custom(1.0) - ); - assert_eq!( - serde_json::from_value::(json!({"custom": 1.5})).unwrap(), - BufferLineHeight::Custom(1.5) - ); - } - - #[test] - fn test_buffer_line_height_deserialize_invalid() { - assert!( - serde_json::from_value::(json!({"custom": 0.99})) - .err() - .unwrap() - .to_string() - .contains("buffer_line_height.custom must be at least 1.0") - ); - assert!( - serde_json::from_value::(json!({"custom": 0.0})) - .err() - .unwrap() - .to_string() - .contains("buffer_line_height.custom must be at least 1.0") - ); - assert!( - serde_json::from_value::(json!({"custom": -1.0})) - .err() - .unwrap() - .to_string() - .contains("buffer_line_height.custom must be at least 1.0") - ); - } - - #[test] - fn test_buffer_font_weight_schema_has_default() { - use schemars::schema_for; - - let schema = schema_for!(ThemeSettingsContent); - let schema_value = serde_json::to_value(&schema).unwrap(); - - let properties = &schema_value["properties"]; - let buffer_font_weight = &properties["buffer_font_weight"]; - - assert!( - buffer_font_weight.get("default").is_some(), - "buffer_font_weight should have a default value in the schema" - ); - - let default_value = &buffer_font_weight["default"]; - assert_eq!( - default_value.as_f64(), - Some(FontWeight::NORMAL.0 as f64), - "buffer_font_weight default should be 400.0 (FontWeight::NORMAL)" - ); - - let defs = &schema_value["$defs"]; - let font_weight_def = &defs["FontWeight"]; - - assert_eq!( - font_weight_def["minimum"].as_f64(), - Some(FontWeight::THIN.0 as f64), - "FontWeight should have minimum of 100.0" - ); - assert_eq!( - font_weight_def["maximum"].as_f64(), - Some(FontWeight::BLACK.0 as f64), - "FontWeight should have maximum of 900.0" - ); - assert_eq!( - font_weight_def["default"].as_f64(), - Some(FontWeight::NORMAL.0 as f64), - "FontWeight should have default of 400.0" - ); - } -} diff --git a/crates/settings/src/settings_content/workspace.rs b/crates/settings/src/settings_content/workspace.rs deleted file mode 100644 index b809a8fa85..0000000000 --- a/crates/settings/src/settings_content/workspace.rs +++ /dev/null @@ -1,713 +0,0 @@ -use std::num::NonZeroUsize; - -use collections::HashMap; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings_macros::{MergeFrom, with_fallible_options}; - -use crate::{ - CenteredPaddingSettings, DelayMs, DockPosition, DockSide, InactiveOpacity, - ScrollbarSettingsContent, ShowIndentGuides, serialize_optional_f32_with_two_decimal_places, -}; - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct WorkspaceSettingsContent { - /// Active pane styling settings. - pub active_pane_modifiers: Option, - /// Layout mode for the bottom dock - /// - /// Default: contained - pub bottom_dock_layout: Option, - /// Direction to split horizontally. - /// - /// Default: "up" - pub pane_split_direction_horizontal: Option, - /// Direction to split vertically. - /// - /// Default: "left" - pub pane_split_direction_vertical: Option, - /// Centered layout related settings. - pub centered_layout: Option, - /// Whether or not to prompt the user to confirm before closing the application. - /// - /// Default: false - pub confirm_quit: Option, - /// Whether or not to show the call status icon in the status bar. - /// - /// Default: true - pub show_call_status_icon: Option, - /// When to automatically save edited buffers. - /// - /// Default: off - pub autosave: Option, - /// Controls previous session restoration in freshly launched Zed instance. - /// Values: none, last_workspace, last_session - /// Default: last_session - pub restore_on_startup: Option, - /// Whether to attempt to restore previous file's state when opening it again. - /// The state is stored per pane. - /// When disabled, defaults are applied instead of the state restoration. - /// - /// E.g. for editors, selections, folds and scroll positions are restored, if the same file is closed and, later, opened again in the same pane. - /// When disabled, a single selection in the very beginning of the file, zero scroll position and no folds state is used as a default. - /// - /// Default: true - pub restore_on_file_reopen: Option, - /// The size of the workspace split drop targets on the outer edges. - /// Given as a fraction that will be multiplied by the smaller dimension of the workspace. - /// - /// Default: `0.2` (20% of the smaller dimension of the workspace) - #[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")] - pub drop_target_size: Option, - /// Whether to close the window when using 'close active item' on a workspace with no tabs - /// - /// Default: auto ("on" on macOS, "off" otherwise) - pub when_closing_with_no_tabs: Option, - /// 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. - /// - /// Default: true - pub use_system_path_prompts: Option, - /// Whether to use the system provided prompts. - /// When set to false, Zed will use the built-in prompts. - /// Note that this setting has no effect on Linux, where Zed will always - /// use the built-in prompts. - /// - /// Default: true - pub use_system_prompts: Option, - /// Aliases for the command palette. When you type a key in this map, - /// it will be assumed to equal the value. - /// - /// Default: true - #[serde(default)] - pub command_aliases: HashMap, - /// Maximum open tabs in a pane. Will not close an unsaved - /// tab. Set to `None` for unlimited tabs. - /// - /// Default: none - pub max_tabs: Option, - /// What to do when the last window is closed - /// - /// Default: auto (nothing on macOS, "app quit" otherwise) - pub on_last_window_closed: Option, - /// Whether to resize all the panels in a dock when resizing the dock. - /// - /// Default: ["left"] - pub resize_all_panels_in_dock: Option>, - /// Whether to automatically close files that have been deleted on disk. - /// - /// Default: false - pub close_on_file_delete: Option, - /// Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). - /// - /// Default: false - pub use_system_window_tabs: Option, - /// Whether to show padding for zoomed panels. - /// When enabled, zoomed bottom panels will have some top padding, - /// while zoomed left/right panels will have padding to the right/left (respectively). - /// - /// Default: true - pub zoomed_padding: Option, - /// What draws window decorations/titlebar, the client application (Zed) or display server - /// Default: client - pub window_decorations: Option, -} - -#[with_fallible_options] -#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct ItemSettingsContent { - /// Whether to show the Git file status on a tab item. - /// - /// Default: false - pub git_status: Option, - /// Position of the close button in a tab. - /// - /// Default: right - pub close_position: Option, - /// Whether to show the file icon for a tab. - /// - /// Default: false - pub file_icons: Option, - /// What to do after closing the current tab. - /// - /// Default: history - pub activate_on_close: Option, - /// Which files containing diagnostic errors/warnings to mark in the tabs. - /// This setting can take the following three values: - /// - /// Default: off - pub show_diagnostics: Option, - /// Whether to always show the close button on tabs. - /// - /// Default: false - pub show_close_button: Option, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct PreviewTabsSettingsContent { - /// Whether to show opened editors as preview tabs. - /// Preview tabs do not stay open, are reused until explicitly set to be kept open opened (via double-click or editing) and show file names in italic. - /// - /// Default: true - pub enabled: Option, - /// Whether to open tabs in preview mode when opened from the project panel with a single click. - /// - /// Default: true - pub enable_preview_from_project_panel: Option, - /// Whether to open tabs in preview mode when selected from the file finder. - /// - /// Default: false - pub enable_preview_from_file_finder: Option, - /// Whether to open tabs in preview mode when opened from a multibuffer. - /// - /// Default: true - pub enable_preview_from_multibuffer: Option, - /// Whether to open tabs in preview mode when code navigation is used to open a multibuffer. - /// - /// Default: false - pub enable_preview_multibuffer_from_code_navigation: Option, - /// Whether to open tabs in preview mode when code navigation is used to open a single file. - /// - /// Default: true - pub enable_preview_file_from_code_navigation: Option, - /// Whether to keep tabs in preview mode when code navigation is used to navigate away from them. - /// If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one. - /// - /// Default: false - pub enable_keep_preview_on_code_navigation: Option, -} - -#[derive( - Copy, - Clone, - Debug, - PartialEq, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "lowercase")] -pub enum ClosePosition { - Left, - #[default] - Right, -} - -#[derive( - Copy, - Clone, - Debug, - PartialEq, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "lowercase")] -pub enum ShowCloseButton { - Always, - #[default] - Hover, - Hidden, -} - -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ShowDiagnostics { - #[default] - Off, - Errors, - All, -} - -#[derive( - Copy, - Clone, - Debug, - PartialEq, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ActivateOnClose { - #[default] - History, - Neighbour, - LeftNeighbour, -} - -#[with_fallible_options] -#[derive(Copy, Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub struct ActivePaneModifiers { - /// Size of the border surrounding the active pane. - /// When set to 0, the active pane doesn't have any border. - /// The border is drawn inset. - /// - /// Default: `0.0` - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub border_size: Option, - /// Opacity of inactive panels. - /// When set to 1.0, the inactive panes have the same opacity as the active one. - /// If set to 0, the inactive panes content will not be visible at all. - /// Values are clamped to the [0.0, 1.0] range. - /// - /// Default: `1.0` - #[schemars(range(min = 0.0, max = 1.0))] - pub inactive_opacity: Option, -} - -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - PartialEq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum BottomDockLayout { - /// Contained between the left and right docks - #[default] - Contained, - /// Takes up the full width of the window - Full, - /// Extends under the left dock while snapping to the right dock - LeftAligned, - /// Extends under the right dock while snapping to the left dock - RightAligned, -} - -#[derive( - Copy, - Clone, - Default, - Debug, - Serialize, - Deserialize, - PartialEq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum WindowDecorations { - /// Zed draws its own window decorations/titlebar (client-side decoration) - #[default] - Client, - /// Show system's window titlebar (server-side decoration; not supported by GNOME Wayland) - Server, -} - -#[derive( - Copy, - Clone, - PartialEq, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - Debug, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum CloseWindowWhenNoItems { - /// Match platform conventions by default, so "on" on macOS and "off" everywhere else - #[default] - PlatformDefault, - /// Close the window when there are no tabs - CloseWindow, - /// Leave the window open when there are no tabs - KeepWindowOpen, -} - -impl CloseWindowWhenNoItems { - pub fn should_close(&self) -> bool { - match self { - CloseWindowWhenNoItems::PlatformDefault => cfg!(target_os = "macos"), - CloseWindowWhenNoItems::CloseWindow => true, - CloseWindowWhenNoItems::KeepWindowOpen => false, - } - } -} - -#[derive( - Copy, - Clone, - PartialEq, - Eq, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - Debug, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum RestoreOnStartupBehavior { - /// Always start with an empty editor - None, - /// Restore the workspace that was closed last. - LastWorkspace, - /// Restore all workspaces that were open when quitting Zed. - #[default] - LastSession, -} - -#[with_fallible_options] -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)] -pub struct TabBarSettingsContent { - /// Whether or not to show the tab bar in the editor. - /// - /// Default: true - pub show: Option, - /// Whether or not to show the navigation history buttons in the tab bar. - /// - /// Default: true - pub show_nav_history_buttons: Option, - /// Whether or not to show the tab bar buttons. - /// - /// Default: true - pub show_tab_bar_buttons: Option, -} - -#[with_fallible_options] -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq, Eq)] -pub struct StatusBarSettingsContent { - /// Whether to show the status bar. - /// - /// Default: true - #[serde(rename = "experimental.show")] - pub show: Option, - /// 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, - /// Whether to show active line endings button in the status bar. - /// - /// Default: false - pub line_endings_button: Option, -} - -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::EnumDiscriminants, -)] -#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))] -#[serde(rename_all = "snake_case")] -pub enum AutosaveSetting { - /// Disable autosave. - Off, - /// Save after inactivity period of `milliseconds`. - AfterDelay { milliseconds: DelayMs }, - /// Autosave when focus changes. - OnFocusChange, - /// Autosave when the active window changes. - OnWindowChange, -} - -impl AutosaveSetting { - pub fn should_save_on_close(&self) -> bool { - matches!( - &self, - AutosaveSetting::OnFocusChange - | AutosaveSetting::OnWindowChange - | AutosaveSetting::AfterDelay { .. } - ) - } -} - -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum PaneSplitDirectionHorizontal { - Up, - Down, -} - -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum PaneSplitDirectionVertical { - Left, - Right, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Default)] -#[serde(rename_all = "snake_case")] -#[with_fallible_options] -pub struct CenteredLayoutSettings { - /// The relative width of the left padding of the central pane from the - /// workspace when the centered layout is used. - /// - /// Default: 0.2 - pub left_padding: Option, - // The relative width of the right padding of the central pane from the - // workspace when the centered layout is used. - /// - /// Default: 0.2 - pub right_padding: Option, -} - -#[derive( - Copy, - Clone, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Debug, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum OnLastWindowClosed { - /// Match platform conventions by default, so don't quit on macOS, and quit on other platforms - #[default] - PlatformDefault, - /// Quit the application the last window is closed - QuitApp, -} - -impl OnLastWindowClosed { - pub fn is_quit_app(&self) -> bool { - match self { - OnLastWindowClosed::PlatformDefault => false, - OnLastWindowClosed::QuitApp => true, - } - } -} - -#[with_fallible_options] -#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)] -pub struct ProjectPanelAutoOpenSettings { - /// Whether to automatically open newly created files in the editor. - /// - /// Default: true - pub on_create: Option, - /// Whether to automatically open files after pasting or duplicating them. - /// - /// Default: true - pub on_paste: Option, - /// Whether to automatically open files dropped from external sources. - /// - /// Default: true - pub on_drop: Option, -} - -#[with_fallible_options] -#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)] -pub struct ProjectPanelSettingsContent { - /// Whether to show the project panel button in the status bar. - /// - /// Default: true - pub button: Option, - /// Whether to hide gitignore files in the project panel. - /// - /// Default: false - pub hide_gitignore: Option, - /// Customize default width (in pixels) taken by project panel - /// - /// Default: 240 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_width: Option, - /// The position of project panel - /// - /// Default: left - pub dock: Option, - /// Spacing between worktree entries in the project panel. - /// - /// Default: comfortable - pub entry_spacing: Option, - /// Whether to show file icons in the project panel. - /// - /// Default: true - pub file_icons: Option, - /// Whether to show folder icons or chevrons for directories in the project panel. - /// - /// Default: true - pub folder_icons: Option, - /// Whether to show the git status in the project panel. - /// - /// Default: true - pub git_status: Option, - /// Amount of indentation (in pixels) for nested items. - /// - /// Default: 20 - #[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")] - pub indent_size: Option, - /// Whether to reveal it in the project panel automatically, - /// when a corresponding project entry becomes active. - /// Gitignored entries are never auto revealed. - /// - /// Default: true - pub auto_reveal_entries: Option, - /// Whether to fold directories automatically - /// when directory has only one directory inside. - /// - /// Default: true - pub auto_fold_dirs: Option, - /// Whether the project panel should open on startup. - /// - /// Default: true - pub starts_open: Option, - /// Scrollbar-related settings - pub scrollbar: Option, - /// Which files containing diagnostic errors/warnings to mark in the project panel. - /// - /// Default: all - pub show_diagnostics: Option, - /// Settings related to indent guides in the project panel. - pub indent_guides: Option, - /// Whether to hide the root entry when only one folder is open in the window. - /// - /// Default: false - pub hide_root: Option, - /// Whether to hide the hidden entries in the project panel. - /// - /// Default: false - pub hide_hidden: Option, - /// Whether to stick parent directories at top of the project panel. - /// - /// Default: true - pub sticky_scroll: Option, - /// Whether to enable drag-and-drop operations in the project panel. - /// - /// Default: true - pub drag_and_drop: Option, - /// Settings for automatically opening files. - pub auto_open: Option, - /// How to order sibling entries in the project panel. - /// - /// Default: directories_first - pub sort_mode: Option, -} - -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ProjectPanelEntrySpacing { - /// Comfortable spacing of entries. - #[default] - Comfortable, - /// The standard spacing of entries. - Standard, -} - -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ProjectPanelSortMode { - /// Show directories first, then files - #[default] - DirectoriesFirst, - /// Mix directories and files together - Mixed, - /// Show files first, then directories - FilesFirst, -} - -#[with_fallible_options] -#[derive( - Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default, -)] -pub struct ProjectPanelIndentGuidesSettings { - pub show: Option, -} diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs deleted file mode 100644 index df6aa8bbb4..0000000000 --- a/crates/settings/src/settings_file.rs +++ /dev/null @@ -1,135 +0,0 @@ -use crate::{settings_content::SettingsContent, settings_store::SettingsStore}; -use collections::HashSet; -use fs::{Fs, PathEventKind}; -use futures::{StreamExt, channel::mpsc}; -use gpui::{App, BackgroundExecutor, ReadGlobal}; -use std::{path::PathBuf, sync::Arc, time::Duration}; - -pub const EMPTY_THEME_NAME: &str = "empty-theme"; - -#[cfg(any(test, feature = "test-support"))] -pub fn test_settings() -> String { - let mut value = - crate::parse_json_with_comments::(crate::default_settings().as_ref()) - .unwrap(); - #[cfg(not(target_os = "windows"))] - util::merge_non_null_json_value_into( - serde_json::json!({ - "ui_font_family": "Courier", - "ui_font_features": {}, - "ui_font_size": 14, - "ui_font_fallback": [], - "buffer_font_family": "Courier", - "buffer_font_features": {}, - "buffer_font_size": 14, - "buffer_font_fallbacks": [], - "theme": EMPTY_THEME_NAME, - }), - &mut value, - ); - #[cfg(target_os = "windows")] - util::merge_non_null_json_value_into( - serde_json::json!({ - "ui_font_family": "Courier New", - "ui_font_features": {}, - "ui_font_size": 14, - "ui_font_fallback": [], - "buffer_font_family": "Courier New", - "buffer_font_features": {}, - "buffer_font_size": 14, - "buffer_font_fallbacks": [], - "theme": EMPTY_THEME_NAME, - }), - &mut value, - ); - value.as_object_mut().unwrap().remove("languages"); - serde_json::to_string(&value).unwrap() -} - -pub fn watch_config_file( - executor: &BackgroundExecutor, - fs: Arc, - path: PathBuf, -) -> mpsc::UnboundedReceiver { - let (tx, rx) = mpsc::unbounded(); - executor - .spawn(async move { - let (events, _) = fs.watch(&path, Duration::from_millis(100)).await; - futures::pin_mut!(events); - - let contents = fs.load(&path).await.unwrap_or_default(); - if tx.unbounded_send(contents).is_err() { - return; - } - - loop { - if events.next().await.is_none() { - break; - } - - if let Ok(contents) = fs.load(&path).await - && tx.unbounded_send(contents).is_err() - { - break; - } - } - }) - .detach(); - rx -} - -pub fn watch_config_dir( - executor: &BackgroundExecutor, - fs: Arc, - dir_path: PathBuf, - config_paths: HashSet, -) -> mpsc::UnboundedReceiver { - let (tx, rx) = mpsc::unbounded(); - executor - .spawn(async move { - for file_path in &config_paths { - if fs.metadata(file_path).await.is_ok_and(|v| v.is_some()) - && let Ok(contents) = fs.load(file_path).await - && tx.unbounded_send(contents).is_err() - { - return; - } - } - - let (events, _) = fs.watch(&dir_path, Duration::from_millis(100)).await; - futures::pin_mut!(events); - - while let Some(event_batch) = events.next().await { - for event in event_batch { - if config_paths.contains(&event.path) { - match event.kind { - Some(PathEventKind::Removed) => { - if tx.unbounded_send(String::new()).is_err() { - return; - } - } - Some(PathEventKind::Created) | Some(PathEventKind::Changed) => { - if let Ok(contents) = fs.load(&event.path).await - && tx.unbounded_send(contents).is_err() - { - return; - } - } - _ => {} - } - } - } - } - }) - .detach(); - - rx -} - -pub fn update_settings_file( - fs: Arc, - cx: &App, - update: impl 'static + Send + FnOnce(&mut SettingsContent, &App), -) { - SettingsStore::global(cx).update_settings_file(fs, update); -} diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs deleted file mode 100644 index 72e2d3ef09..0000000000 --- a/crates/settings/src/settings_store.rs +++ /dev/null @@ -1,2296 +0,0 @@ -use anyhow::{Context as _, Result}; -use collections::{BTreeMap, HashMap, btree_map, hash_map}; -use ec4rs::{ConfigParser, PropertiesSource, Section}; -use fs::Fs; -use futures::{ - FutureExt, StreamExt, - channel::{mpsc, oneshot}, - future::LocalBoxFuture, -}; -use gpui::{App, AsyncApp, BorrowAppContext, Global, SharedString, Task, UpdateGlobal}; - -use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name}; -use schemars::{JsonSchema, json_schema}; -use serde_json::Value; -use smallvec::SmallVec; -use std::{ - any::{Any, TypeId, type_name}, - fmt::Debug, - ops::Range, - path::PathBuf, - rc::Rc, - str::{self, FromStr}, - sync::Arc, -}; -use util::{ - ResultExt as _, - rel_path::RelPath, - schemars::{AllowTrailingCommas, DefaultDenyUnknownFields, replace_subschema}, -}; - -pub type EditorconfigProperties = ec4rs::Properties; - -use crate::{ - ActiveSettingsProfileName, FontFamilyName, IconThemeName, LanguageSettingsContent, - LanguageToSettingsMap, ThemeName, VsCodeSettings, WorktreeId, fallible_options, - merge_from::MergeFrom, - settings_content::{ - ExtensionsSettingsContent, ProjectSettingsContent, SettingsContent, UserSettingsContent, - }, -}; - -use settings_json::{infer_json_indent_size, parse_json_with_comments, update_value_in_json_text}; - -pub trait SettingsKey: 'static + Send + Sync { - /// The name of a key within the JSON file from which this setting should - /// be deserialized. If this is `None`, then the setting will be deserialized - /// from the root object. - const KEY: Option<&'static str>; - - const FALLBACK_KEY: Option<&'static str> = None; -} - -/// A value that can be defined as a user setting. -/// -/// Settings can be loaded from a combination of multiple JSON files. -pub trait Settings: 'static + Send + Sync + Sized { - /// The name of the keys in the [`FileContent`](Self::FileContent) that should - /// always be written to a settings file, even if their value matches the default - /// value. - /// - /// This is useful for tagged [`FileContent`](Self::FileContent)s where the tag - /// is a "version" field that should always be persisted, even if the current - /// user settings match the current version of the settings. - const PRESERVED_KEYS: Option<&'static [&'static str]> = None; - - /// Read the value from default.json. - /// - /// This function *should* panic if default values are missing, - /// and you should add a default to default.json for documentation. - fn from_settings(content: &SettingsContent) -> Self; - - #[track_caller] - fn register(cx: &mut App) - where - Self: Sized, - { - SettingsStore::update_global(cx, |store, _| { - store.register_setting::(); - }); - } - - #[track_caller] - fn get<'a>(path: Option, cx: &'a App) -> &'a Self - where - Self: Sized, - { - cx.global::().get(path) - } - - #[track_caller] - fn get_global(cx: &App) -> &Self - where - Self: Sized, - { - cx.global::().get(None) - } - - #[track_caller] - fn try_get(cx: &App) -> Option<&Self> - where - Self: Sized, - { - if cx.has_global::() { - cx.global::().try_get(None) - } else { - None - } - } - - #[track_caller] - fn try_read_global(cx: &AsyncApp, f: impl FnOnce(&Self) -> R) -> Option - where - Self: Sized, - { - cx.try_read_global(|s: &SettingsStore, _| f(s.get(None))) - } - - #[track_caller] - fn override_global(settings: Self, cx: &mut App) - where - Self: Sized, - { - cx.global_mut::().override_global(settings) - } -} - -pub struct RegisteredSetting { - pub settings_value: fn() -> Box, - pub from_settings: fn(&SettingsContent) -> Box, - pub id: fn() -> TypeId, -} - -inventory::collect!(RegisteredSetting); - -#[derive(Clone, Copy, Debug)] -pub struct SettingsLocation<'a> { - pub worktree_id: WorktreeId, - pub path: &'a RelPath, -} - -pub struct SettingsStore { - setting_values: HashMap>, - default_settings: Rc, - user_settings: Option, - global_settings: Option>, - - extension_settings: Option>, - server_settings: Option>, - - merged_settings: Rc, - - local_settings: BTreeMap<(WorktreeId, Arc), SettingsContent>, - raw_editorconfig_settings: BTreeMap<(WorktreeId, Arc), (String, Option)>, - - _setting_file_updates: Task<()>, - setting_file_updates_tx: - mpsc::UnboundedSender LocalBoxFuture<'static, Result<()>>>>, - file_errors: BTreeMap, -} - -#[derive(Clone, PartialEq, Eq, Debug)] -pub enum SettingsFile { - Default, - Global, - User, - Server, - /// Represents project settings in ssh projects as well as local projects - Project((WorktreeId, Arc)), -} - -impl PartialOrd for SettingsFile { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -/// Sorted in order of precedence -impl Ord for SettingsFile { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - use SettingsFile::*; - use std::cmp::Ordering; - match (self, other) { - (User, User) => Ordering::Equal, - (Server, Server) => Ordering::Equal, - (Default, Default) => Ordering::Equal, - (Project((id1, rel_path1)), Project((id2, rel_path2))) => id1 - .cmp(id2) - .then_with(|| rel_path1.cmp(rel_path2).reverse()), - (Project(_), _) => Ordering::Less, - (_, Project(_)) => Ordering::Greater, - (Server, _) => Ordering::Less, - (_, Server) => Ordering::Greater, - (User, _) => Ordering::Less, - (_, User) => Ordering::Greater, - (Global, _) => Ordering::Less, - (_, Global) => Ordering::Greater, - } - } -} - -#[derive(Clone)] -pub struct Editorconfig { - pub is_root: bool, - pub sections: SmallVec<[Section; 5]>, -} - -impl FromStr for Editorconfig { - type Err = anyhow::Error; - - fn from_str(contents: &str) -> Result { - let parser = ConfigParser::new_buffered(contents.as_bytes()) - .context("creating editorconfig parser")?; - let is_root = parser.is_root; - let sections = parser - .collect::, _>>() - .context("parsing editorconfig sections")?; - Ok(Self { is_root, sections }) - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -pub enum LocalSettingsKind { - Settings, - Tasks, - Editorconfig, - Debug, -} - -impl Global for SettingsStore {} - -#[doc(hidden)] -#[derive(Debug)] -pub struct SettingValue { - #[doc(hidden)] - pub global_value: Option, - #[doc(hidden)] - pub local_values: Vec<(WorktreeId, Arc, T)>, -} - -#[doc(hidden)] -pub trait AnySettingValue: 'static + Send + Sync { - fn setting_type_name(&self) -> &'static str; - - fn from_settings(&self, s: &SettingsContent) -> Box; - - fn value_for_path(&self, path: Option) -> &dyn Any; - fn all_local_values(&self) -> Vec<(WorktreeId, Arc, &dyn Any)>; - fn set_global_value(&mut self, value: Box); - fn set_local_value(&mut self, root_id: WorktreeId, path: Arc, value: Box); -} - -/// Parameters that are used when generating some JSON schemas at runtime. -pub struct SettingsJsonSchemaParams<'a> { - pub language_names: &'a [String], - pub font_names: &'a [String], - pub theme_names: &'a [SharedString], - pub icon_theme_names: &'a [SharedString], -} - -impl SettingsStore { - pub fn new(cx: &App, default_settings: &str) -> Self { - let (setting_file_updates_tx, mut setting_file_updates_rx) = mpsc::unbounded(); - let default_settings: Rc = - parse_json_with_comments(default_settings).unwrap(); - let mut this = Self { - setting_values: Default::default(), - default_settings: default_settings.clone(), - global_settings: None, - server_settings: None, - user_settings: None, - extension_settings: None, - - merged_settings: default_settings, - local_settings: BTreeMap::default(), - raw_editorconfig_settings: BTreeMap::default(), - setting_file_updates_tx, - _setting_file_updates: cx.spawn(async move |cx| { - while let Some(setting_file_update) = setting_file_updates_rx.next().await { - (setting_file_update)(cx.clone()).await.log_err(); - } - }), - file_errors: BTreeMap::default(), - }; - - this.load_settings_types(); - - this - } - - pub fn observe_active_settings_profile_name(cx: &mut App) -> gpui::Subscription { - cx.observe_global::(|cx| { - Self::update_global(cx, |store, cx| { - store.recompute_values(None, cx); - }); - }) - } - - pub fn update(cx: &mut C, f: impl FnOnce(&mut Self, &mut C) -> R) -> R - where - C: BorrowAppContext, - { - cx.update_global(f) - } - - /// Add a new type of setting to the store. - pub fn register_setting(&mut self) { - self.register_setting_internal(&RegisteredSetting { - settings_value: || { - Box::new(SettingValue:: { - global_value: None, - local_values: Vec::new(), - }) - }, - from_settings: |content| Box::new(T::from_settings(content)), - id: || TypeId::of::(), - }); - } - - fn load_settings_types(&mut self) { - for registered_setting in inventory::iter::() { - self.register_setting_internal(registered_setting); - } - } - - fn register_setting_internal(&mut self, registered_setting: &RegisteredSetting) { - let entry = self.setting_values.entry((registered_setting.id)()); - - if matches!(entry, hash_map::Entry::Occupied(_)) { - return; - } - - let setting_value = entry.or_insert((registered_setting.settings_value)()); - let value = (registered_setting.from_settings)(&self.merged_settings); - setting_value.set_global_value(value); - } - - /// Get the value of a setting. - /// - /// Panics if the given setting type has not been registered, or if there is no - /// value for this setting. - pub fn get(&self, path: Option) -> &T { - self.setting_values - .get(&TypeId::of::()) - .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) - .value_for_path(path) - .downcast_ref::() - .expect("no default value for setting type") - } - - /// Get the value of a setting. - /// - /// Does not panic - pub fn try_get(&self, path: Option) -> Option<&T> { - self.setting_values - .get(&TypeId::of::()) - .map(|value| value.value_for_path(path)) - .and_then(|value| value.downcast_ref::()) - } - - /// Get all values from project specific settings - pub fn get_all_locals(&self) -> Vec<(WorktreeId, Arc, &T)> { - self.setting_values - .get(&TypeId::of::()) - .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) - .all_local_values() - .into_iter() - .map(|(id, path, any)| { - ( - id, - path, - any.downcast_ref::() - .expect("wrong value type for setting"), - ) - }) - .collect() - } - - /// Override the global value for a setting. - /// - /// The given value will be overwritten if the user settings file changes. - pub fn override_global(&mut self, value: T) { - self.setting_values - .get_mut(&TypeId::of::()) - .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) - .set_global_value(Box::new(value)) - } - - /// Get the user's settings content. - /// - /// For user-facing functionality use the typed setting interface. - /// (e.g. ProjectSettings::get_global(cx)) - pub fn raw_user_settings(&self) -> Option<&UserSettingsContent> { - self.user_settings.as_ref() - } - - /// Get the default settings content as a raw JSON value. - pub fn raw_default_settings(&self) -> &SettingsContent { - &self.default_settings - } - - /// Get the configured settings profile names. - pub fn configured_settings_profiles(&self) -> impl Iterator { - self.user_settings - .iter() - .flat_map(|settings| settings.profiles.keys().map(|k| k.as_str())) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn test(cx: &mut App) -> Self { - Self::new(cx, &crate::test_settings()) - } - - /// Updates the value of a setting in the user's global configuration. - /// - /// This is only for tests. Normally, settings are only loaded from - /// JSON files. - #[cfg(any(test, feature = "test-support"))] - pub fn update_user_settings( - &mut self, - cx: &mut App, - update: impl FnOnce(&mut SettingsContent), - ) { - let mut content = self.user_settings.clone().unwrap_or_default().content; - update(&mut content); - let new_text = serde_json::to_string(&UserSettingsContent { - content, - ..Default::default() - }) - .unwrap(); - _ = self.set_user_settings(&new_text, cx); - } - - pub async fn load_settings(fs: &Arc) -> Result { - match fs.load(paths::settings_file()).await { - result @ Ok(_) => result, - Err(err) => { - if let Some(e) = err.downcast_ref::() - && e.kind() == std::io::ErrorKind::NotFound - { - return Ok(crate::initial_user_settings_content().to_string()); - } - Err(err) - } - } - } - - fn update_settings_file_inner( - &self, - fs: Arc, - update: impl 'static + Send + FnOnce(String, AsyncApp) -> Result, - ) -> oneshot::Receiver> { - let (tx, rx) = oneshot::channel::>(); - self.setting_file_updates_tx - .unbounded_send(Box::new(move |cx: AsyncApp| { - async move { - let res = async move { - let old_text = Self::load_settings(&fs).await?; - let new_text = update(old_text, cx)?; - let settings_path = paths::settings_file().as_path(); - if fs.is_file(settings_path).await { - let resolved_path = - fs.canonicalize(settings_path).await.with_context(|| { - format!( - "Failed to canonicalize settings path {:?}", - settings_path - ) - })?; - - fs.atomic_write(resolved_path.clone(), new_text) - .await - .with_context(|| { - format!("Failed to write settings to file {:?}", resolved_path) - })?; - } else { - fs.atomic_write(settings_path.to_path_buf(), new_text) - .await - .with_context(|| { - format!("Failed to write settings to file {:?}", settings_path) - })?; - } - anyhow::Ok(()) - } - .await; - - let new_res = match &res { - Ok(_) => anyhow::Ok(()), - Err(e) => Err(anyhow::anyhow!("Failed to write settings to file {:?}", e)), - }; - - _ = tx.send(new_res); - res - } - .boxed_local() - })) - .map_err(|err| anyhow::format_err!("Failed to update settings file: {}", err)) - .log_with_level(log::Level::Warn); - return rx; - } - - pub fn update_settings_file( - &self, - fs: Arc, - update: impl 'static + Send + FnOnce(&mut SettingsContent, &App), - ) { - _ = self.update_settings_file_inner(fs, move |old_text: String, cx: AsyncApp| { - cx.read_global(|store: &SettingsStore, cx| { - store.new_text_for_update(old_text, |content| update(content, cx)) - }) - }); - } - - pub fn import_vscode_settings( - &self, - fs: Arc, - vscode_settings: VsCodeSettings, - ) -> oneshot::Receiver> { - self.update_settings_file_inner(fs, move |old_text: String, cx: AsyncApp| { - cx.read_global(|store: &SettingsStore, _cx| { - store.get_vscode_edits(old_text, &vscode_settings) - }) - }) - } - - pub fn get_all_files(&self) -> Vec { - let mut files = Vec::from_iter( - self.local_settings - .keys() - // rev because these are sorted by path, so highest precedence is last - .rev() - .cloned() - .map(SettingsFile::Project), - ); - - if self.server_settings.is_some() { - files.push(SettingsFile::Server); - } - // ignoring profiles - // ignoring os profiles - // ignoring release channel profiles - // ignoring global - // ignoring extension - - if self.user_settings.is_some() { - files.push(SettingsFile::User); - } - files.push(SettingsFile::Default); - files - } - - pub fn get_content_for_file(&self, file: SettingsFile) -> Option<&SettingsContent> { - match file { - SettingsFile::User => self - .user_settings - .as_ref() - .map(|settings| settings.content.as_ref()), - SettingsFile::Default => Some(self.default_settings.as_ref()), - SettingsFile::Server => self.server_settings.as_deref(), - SettingsFile::Project(ref key) => self.local_settings.get(key), - SettingsFile::Global => self.global_settings.as_deref(), - } - } - - pub fn get_overrides_for_field( - &self, - target_file: SettingsFile, - get: fn(&SettingsContent) -> &Option, - ) -> Vec { - let all_files = self.get_all_files(); - let mut found_file = false; - let mut overrides = Vec::new(); - - for file in all_files.into_iter().rev() { - if !found_file { - found_file = file == target_file; - continue; - } - - if let SettingsFile::Project((wt_id, ref path)) = file - && let SettingsFile::Project((target_wt_id, ref target_path)) = target_file - && (wt_id != target_wt_id || !target_path.starts_with(path)) - { - // if requesting value from a local file, don't return values from local files in different worktrees - continue; - } - - let Some(content) = self.get_content_for_file(file.clone()) else { - continue; - }; - if get(content).is_some() { - overrides.push(file); - } - } - - overrides - } - - /// Checks the given file, and files that the passed file overrides for the given field. - /// Returns the first file found that contains the value. - /// The value will only be None if no file contains the value. - /// I.e. if no file contains the value, returns `(File::Default, None)` - pub fn get_value_from_file<'a, T: 'a>( - &'a self, - target_file: SettingsFile, - pick: fn(&'a SettingsContent) -> Option, - ) -> (SettingsFile, Option) { - self.get_value_from_file_inner(target_file, pick, true) - } - - /// Same as `Self::get_value_from_file` except that it does not include the current file. - /// Therefore it returns the value that was potentially overloaded by the target file. - pub fn get_value_up_to_file<'a, T: 'a>( - &'a self, - target_file: SettingsFile, - pick: fn(&'a SettingsContent) -> Option, - ) -> (SettingsFile, Option) { - self.get_value_from_file_inner(target_file, pick, false) - } - - fn get_value_from_file_inner<'a, T: 'a>( - &'a self, - target_file: SettingsFile, - pick: fn(&'a SettingsContent) -> Option, - include_target_file: bool, - ) -> (SettingsFile, Option) { - // todo(settings_ui): Add a metadata field for overriding the "overrides" tag, for contextually different settings - // e.g. disable AI isn't overridden, or a vec that gets extended instead or some such - - // todo(settings_ui) cache all files - let all_files = self.get_all_files(); - let mut found_file = false; - - for file in all_files.into_iter() { - if !found_file && file != SettingsFile::Default { - if file != target_file { - continue; - } - found_file = true; - if !include_target_file { - continue; - } - } - - if let SettingsFile::Project((worktree_id, ref path)) = file - && let SettingsFile::Project((target_worktree_id, ref target_path)) = target_file - && (worktree_id != target_worktree_id || !target_path.starts_with(&path)) - { - // if requesting value from a local file, don't return values from local files in different worktrees - continue; - } - - let Some(content) = self.get_content_for_file(file.clone()) else { - continue; - }; - if let Some(value) = pick(content) { - return (file, Some(value)); - } - } - - (SettingsFile::Default, None) - } - - #[inline(always)] - fn parse_and_migrate_zed_settings( - &mut self, - user_settings_content: &str, - file: SettingsFile, - ) -> (Option, SettingsParseResult) { - let mut migration_status = MigrationStatus::NotNeeded; - let (settings, parse_status) = if user_settings_content.is_empty() { - fallible_options::parse_json("{}") - } else { - let migration_res = migrator::migrate_settings(user_settings_content); - migration_status = match &migration_res { - Ok(Some(_)) => MigrationStatus::Succeeded, - Ok(None) => MigrationStatus::NotNeeded, - Err(err) => MigrationStatus::Failed { - error: err.to_string(), - }, - }; - let content = match &migration_res { - Ok(Some(content)) => content, - Ok(None) => user_settings_content, - Err(_) => user_settings_content, - }; - fallible_options::parse_json(content) - }; - - let result = SettingsParseResult { - parse_status, - migration_status, - }; - self.file_errors.insert(file, result.clone()); - return (settings, result); - } - - pub fn error_for_file(&self, file: SettingsFile) -> Option { - self.file_errors - .get(&file) - .filter(|parse_result| parse_result.requires_user_action()) - .cloned() - } -} - -impl SettingsStore { - /// Updates the value of a setting in a JSON file, returning the new text - /// for that JSON file. - pub fn new_text_for_update( - &self, - old_text: String, - update: impl FnOnce(&mut SettingsContent), - ) -> String { - let edits = self.edits_for_update(&old_text, update); - let mut new_text = old_text; - for (range, replacement) in edits.into_iter() { - new_text.replace_range(range, &replacement); - } - new_text - } - - pub fn get_vscode_edits(&self, old_text: String, vscode: &VsCodeSettings) -> String { - self.new_text_for_update(old_text, |content| { - content.merge_from(&vscode.settings_content()) - }) - } - - /// Updates the value of a setting in a JSON file, returning a list - /// of edits to apply to the JSON file. - pub fn edits_for_update( - &self, - text: &str, - update: impl FnOnce(&mut SettingsContent), - ) -> Vec<(Range, String)> { - let old_content: UserSettingsContent = - parse_json_with_comments(text).log_err().unwrap_or_default(); - let mut new_content = old_content.clone(); - update(&mut new_content.content); - - let old_value = serde_json::to_value(&old_content).unwrap(); - let new_value = serde_json::to_value(new_content).unwrap(); - - let mut key_path = Vec::new(); - let mut edits = Vec::new(); - let tab_size = infer_json_indent_size(&text); - let mut text = text.to_string(); - update_value_in_json_text( - &mut text, - &mut key_path, - tab_size, - &old_value, - &new_value, - &mut edits, - ); - edits - } - - /// Sets the default settings via a JSON string. - /// - /// The string should contain a JSON object with a default value for every setting. - pub fn set_default_settings( - &mut self, - default_settings_content: &str, - cx: &mut App, - ) -> Result<()> { - self.default_settings = parse_json_with_comments(default_settings_content)?; - self.recompute_values(None, cx); - Ok(()) - } - - /// Sets the user settings via a JSON string. - #[must_use] - pub fn set_user_settings( - &mut self, - user_settings_content: &str, - cx: &mut App, - ) -> SettingsParseResult { - let (settings, parse_result) = self.parse_and_migrate_zed_settings::( - user_settings_content, - SettingsFile::User, - ); - - if let Some(settings) = settings { - self.user_settings = Some(settings); - self.recompute_values(None, cx); - } - return parse_result; - } - - /// Sets the global settings via a JSON string. - #[must_use] - pub fn set_global_settings( - &mut self, - global_settings_content: &str, - cx: &mut App, - ) -> SettingsParseResult { - let (settings, parse_result) = self.parse_and_migrate_zed_settings::( - global_settings_content, - SettingsFile::Global, - ); - - if let Some(settings) = settings { - self.global_settings = Some(Box::new(settings)); - self.recompute_values(None, cx); - } - return parse_result; - } - - pub fn set_server_settings( - &mut self, - server_settings_content: &str, - cx: &mut App, - ) -> Result<()> { - let settings: Option = if server_settings_content.is_empty() { - None - } else { - parse_json_with_comments(server_settings_content)? - }; - - // Rewrite the server settings into a content type - self.server_settings = settings.map(|settings| Box::new(settings)); - - self.recompute_values(None, cx); - Ok(()) - } - - /// Add or remove a set of local settings via a JSON string. - pub fn set_local_settings( - &mut self, - root_id: WorktreeId, - directory_path: Arc, - kind: LocalSettingsKind, - settings_content: Option<&str>, - cx: &mut App, - ) -> std::result::Result<(), InvalidSettingsError> { - let mut zed_settings_changed = false; - match ( - kind, - settings_content - .map(|content| content.trim()) - .filter(|content| !content.is_empty()), - ) { - (LocalSettingsKind::Tasks, _) => { - return Err(InvalidSettingsError::Tasks { - message: "Attempted to submit tasks into the settings store".to_string(), - path: directory_path - .join(RelPath::unix(task_file_name()).unwrap()) - .as_std_path() - .to_path_buf(), - }); - } - (LocalSettingsKind::Debug, _) => { - return Err(InvalidSettingsError::Debug { - message: "Attempted to submit debugger config into the settings store" - .to_string(), - path: directory_path - .join(RelPath::unix(task_file_name()).unwrap()) - .as_std_path() - .to_path_buf(), - }); - } - (LocalSettingsKind::Settings, None) => { - zed_settings_changed = self - .local_settings - .remove(&(root_id, directory_path.clone())) - .is_some(); - self.file_errors - .remove(&SettingsFile::Project((root_id, directory_path.clone()))); - } - (LocalSettingsKind::Editorconfig, None) => { - self.raw_editorconfig_settings - .remove(&(root_id, directory_path.clone())); - } - (LocalSettingsKind::Settings, Some(settings_contents)) => { - let (new_settings, parse_result) = self - .parse_and_migrate_zed_settings::( - settings_contents, - SettingsFile::Project((root_id, directory_path.clone())), - ); - match parse_result.parse_status { - ParseStatus::Success => Ok(()), - ParseStatus::Failed { error } => Err(InvalidSettingsError::LocalSettings { - path: directory_path.join(local_settings_file_relative_path()), - message: error, - }), - }?; - if let Some(new_settings) = new_settings { - match self.local_settings.entry((root_id, directory_path.clone())) { - btree_map::Entry::Vacant(v) => { - v.insert(SettingsContent { - project: new_settings, - ..Default::default() - }); - zed_settings_changed = true; - } - btree_map::Entry::Occupied(mut o) => { - if &o.get().project != &new_settings { - o.insert(SettingsContent { - project: new_settings, - ..Default::default() - }); - zed_settings_changed = true; - } - } - } - } - } - (LocalSettingsKind::Editorconfig, Some(editorconfig_contents)) => { - match self - .raw_editorconfig_settings - .entry((root_id, directory_path.clone())) - { - btree_map::Entry::Vacant(v) => match editorconfig_contents.parse() { - Ok(new_contents) => { - v.insert((editorconfig_contents.to_owned(), Some(new_contents))); - } - Err(e) => { - v.insert((editorconfig_contents.to_owned(), None)); - return Err(InvalidSettingsError::Editorconfig { - message: e.to_string(), - path: directory_path - .join(RelPath::unix(EDITORCONFIG_NAME).unwrap()), - }); - } - }, - btree_map::Entry::Occupied(mut o) => { - if o.get().0 != editorconfig_contents { - match editorconfig_contents.parse() { - Ok(new_contents) => { - o.insert(( - editorconfig_contents.to_owned(), - Some(new_contents), - )); - } - Err(e) => { - o.insert((editorconfig_contents.to_owned(), None)); - return Err(InvalidSettingsError::Editorconfig { - message: e.to_string(), - path: directory_path - .join(RelPath::unix(EDITORCONFIG_NAME).unwrap()), - }); - } - } - } - } - } - } - }; - - if zed_settings_changed { - self.recompute_values(Some((root_id, &directory_path)), cx); - } - Ok(()) - } - - pub fn set_extension_settings( - &mut self, - content: ExtensionsSettingsContent, - cx: &mut App, - ) -> Result<()> { - self.extension_settings = Some(Box::new(SettingsContent { - project: ProjectSettingsContent { - all_languages: content.all_languages, - ..Default::default() - }, - ..Default::default() - })); - self.recompute_values(None, cx); - Ok(()) - } - - /// Add or remove a set of local settings via a JSON string. - pub fn clear_local_settings(&mut self, root_id: WorktreeId, cx: &mut App) -> Result<()> { - self.local_settings - .retain(|(worktree_id, _), _| worktree_id != &root_id); - self.recompute_values(Some((root_id, RelPath::empty())), cx); - Ok(()) - } - - pub fn local_settings( - &self, - root_id: WorktreeId, - ) -> impl '_ + Iterator, &ProjectSettingsContent)> { - self.local_settings - .range( - (root_id, RelPath::empty().into()) - ..( - WorktreeId::from_usize(root_id.to_usize() + 1), - RelPath::empty().into(), - ), - ) - .map(|((_, path), content)| (path.clone(), &content.project)) - } - - pub fn local_editorconfig_settings( - &self, - root_id: WorktreeId, - ) -> impl '_ + Iterator, String, Option)> { - self.raw_editorconfig_settings - .range( - (root_id, RelPath::empty().into()) - ..( - WorktreeId::from_usize(root_id.to_usize() + 1), - RelPath::empty().into(), - ), - ) - .map(|((_, path), (content, parsed_content))| { - (path.clone(), content.clone(), parsed_content.clone()) - }) - } - - pub fn json_schema(&self, params: &SettingsJsonSchemaParams) -> Value { - let mut generator = schemars::generate::SchemaSettings::draft2019_09() - .with_transform(DefaultDenyUnknownFields) - .with_transform(AllowTrailingCommas) - .into_generator(); - - UserSettingsContent::json_schema(&mut generator); - - let language_settings_content_ref = generator - .subschema_for::() - .to_value(); - - replace_subschema::(&mut generator, || { - json_schema!({ - "type": "object", - "properties": params - .language_names - .iter() - .map(|name| { - ( - name.clone(), - language_settings_content_ref.clone(), - ) - }) - .collect::>(), - "errorMessage": "No language with this name is installed." - }) - }); - - replace_subschema::(&mut generator, || { - json_schema!({ - "type": "string", - "enum": params.font_names, - }) - }); - - replace_subschema::(&mut generator, || { - json_schema!({ - "type": "string", - "enum": params.theme_names, - }) - }); - - replace_subschema::(&mut generator, || { - json_schema!({ - "type": "string", - "enum": params.icon_theme_names, - }) - }); - - generator - .root_schema_for::() - .to_value() - } - - fn recompute_values( - &mut self, - changed_local_path: Option<(WorktreeId, &RelPath)>, - cx: &mut App, - ) { - // Reload the global and local values for every setting. - let mut project_settings_stack = Vec::::new(); - let mut paths_stack = Vec::>::new(); - - if changed_local_path.is_none() { - let mut merged = self.default_settings.as_ref().clone(); - merged.merge_from_option(self.extension_settings.as_deref()); - merged.merge_from_option(self.global_settings.as_deref()); - if let Some(user_settings) = self.user_settings.as_ref() { - merged.merge_from(&user_settings.content); - merged.merge_from_option(user_settings.for_release_channel()); - merged.merge_from_option(user_settings.for_os()); - merged.merge_from_option(user_settings.for_profile(cx)); - } - merged.merge_from_option(self.server_settings.as_deref()); - self.merged_settings = Rc::new(merged); - - for setting_value in self.setting_values.values_mut() { - let value = setting_value.from_settings(&self.merged_settings); - setting_value.set_global_value(value); - } - } - - for ((root_id, directory_path), local_settings) in &self.local_settings { - // Build a stack of all of the local values for that setting. - while let Some(prev_entry) = paths_stack.last() { - if let Some((prev_root_id, prev_path)) = prev_entry - && (root_id != prev_root_id || !directory_path.starts_with(prev_path)) - { - paths_stack.pop(); - project_settings_stack.pop(); - continue; - } - break; - } - - paths_stack.push(Some((*root_id, directory_path.as_ref()))); - let mut merged_local_settings = if let Some(deepest) = project_settings_stack.last() { - (*deepest).clone() - } else { - self.merged_settings.as_ref().clone() - }; - merged_local_settings.merge_from(local_settings); - - project_settings_stack.push(merged_local_settings); - - // If a local settings file changed, then avoid recomputing local - // settings for any path outside of that directory. - if changed_local_path.is_some_and(|(changed_root_id, changed_local_path)| { - *root_id != changed_root_id || !directory_path.starts_with(changed_local_path) - }) { - continue; - } - - for setting_value in self.setting_values.values_mut() { - let value = setting_value.from_settings(&project_settings_stack.last().unwrap()); - setting_value.set_local_value(*root_id, directory_path.clone(), value); - } - } - } - - pub fn editorconfig_properties( - &self, - for_worktree: WorktreeId, - for_path: &RelPath, - ) -> Option { - let mut properties = EditorconfigProperties::new(); - - for (directory_with_config, _, parsed_editorconfig) in - self.local_editorconfig_settings(for_worktree) - { - if !for_path.starts_with(&directory_with_config) { - properties.use_fallbacks(); - return Some(properties); - } - let parsed_editorconfig = parsed_editorconfig?; - if parsed_editorconfig.is_root { - properties = EditorconfigProperties::new(); - } - for section in parsed_editorconfig.sections { - section - .apply_to(&mut properties, for_path.as_std_path()) - .log_err()?; - } - } - - properties.use_fallbacks(); - Some(properties) - } -} - -/// The result of parsing settings, including any migration attempts -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SettingsParseResult { - /// The result of parsing the settings file (possibly after migration) - pub parse_status: ParseStatus, - /// The result of attempting to migrate the settings file - pub migration_status: MigrationStatus, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ParseStatus { - /// Settings were parsed successfully - Success, - /// Settings failed to parse - Failed { error: String }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MigrationStatus { - /// No migration was needed - settings are up to date - NotNeeded, - /// Settings were automatically migrated in memory, but the file needs to be updated - Succeeded, - /// Migration was attempted but failed. Original settings were parsed instead. - Failed { error: String }, -} - -impl Default for SettingsParseResult { - fn default() -> Self { - Self { - parse_status: ParseStatus::Success, - migration_status: MigrationStatus::NotNeeded, - } - } -} - -impl SettingsParseResult { - pub fn unwrap(self) -> bool { - self.result().unwrap() - } - - pub fn expect(self, message: &str) -> bool { - self.result().expect(message) - } - - /// Formats the ParseResult as a Result type. This is a lossy conversion - pub fn result(self) -> Result { - let migration_result = match self.migration_status { - MigrationStatus::NotNeeded => Ok(false), - MigrationStatus::Succeeded => Ok(true), - MigrationStatus::Failed { error } => { - Err(anyhow::format_err!(error)).context("Failed to migrate settings") - } - }; - - let parse_result = match self.parse_status { - ParseStatus::Success => Ok(()), - ParseStatus::Failed { error } => { - Err(anyhow::format_err!(error)).context("Failed to parse settings") - } - }; - - match (migration_result, parse_result) { - (migration_result @ Ok(_), Ok(())) => migration_result, - (Err(migration_err), Ok(())) => Err(migration_err), - (_, Err(parse_err)) => Err(parse_err), - } - } - - /// Returns true if there were any errors migrating and parsing the settings content or if migration was required but there were no errors - pub fn requires_user_action(&self) -> bool { - matches!(self.parse_status, ParseStatus::Failed { .. }) - || matches!( - self.migration_status, - MigrationStatus::Succeeded | MigrationStatus::Failed { .. } - ) - } - - pub fn ok(self) -> Option { - self.result().ok() - } - - pub fn parse_error(&self) -> Option { - match &self.parse_status { - ParseStatus::Failed { error } => Some(error.clone()), - ParseStatus::Success => None, - } - } -} - -#[derive(Debug, Clone, PartialEq)] -pub enum InvalidSettingsError { - LocalSettings { path: Arc, message: String }, - UserSettings { message: String }, - ServerSettings { message: String }, - DefaultSettings { message: String }, - Editorconfig { path: Arc, message: String }, - Tasks { path: PathBuf, message: String }, - Debug { path: PathBuf, message: String }, -} - -impl std::fmt::Display for InvalidSettingsError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - InvalidSettingsError::LocalSettings { message, .. } - | InvalidSettingsError::UserSettings { message } - | InvalidSettingsError::ServerSettings { message } - | InvalidSettingsError::DefaultSettings { message } - | InvalidSettingsError::Tasks { message, .. } - | InvalidSettingsError::Editorconfig { message, .. } - | InvalidSettingsError::Debug { message, .. } => { - write!(f, "{message}") - } - } - } -} -impl std::error::Error for InvalidSettingsError {} - -impl Debug for SettingsStore { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SettingsStore") - .field( - "types", - &self - .setting_values - .values() - .map(|value| value.setting_type_name()) - .collect::>(), - ) - .field("default_settings", &self.default_settings) - .field("user_settings", &self.user_settings) - .field("local_settings", &self.local_settings) - .finish_non_exhaustive() - } -} - -impl AnySettingValue for SettingValue { - fn from_settings(&self, s: &SettingsContent) -> Box { - Box::new(T::from_settings(s)) as _ - } - - fn setting_type_name(&self) -> &'static str { - type_name::() - } - - fn all_local_values(&self) -> Vec<(WorktreeId, Arc, &dyn Any)> { - self.local_values - .iter() - .map(|(id, path, value)| (*id, path.clone(), value as _)) - .collect() - } - - fn value_for_path(&self, path: Option) -> &dyn Any { - if let Some(SettingsLocation { worktree_id, path }) = path { - for (settings_root_id, settings_path, value) in self.local_values.iter().rev() { - if worktree_id == *settings_root_id && path.starts_with(settings_path) { - return value; - } - } - } - - self.global_value - .as_ref() - .unwrap_or_else(|| panic!("no default value for setting {}", self.setting_type_name())) - } - - fn set_global_value(&mut self, value: Box) { - self.global_value = Some(*value.downcast().unwrap()); - } - - fn set_local_value(&mut self, root_id: WorktreeId, path: Arc, value: Box) { - let value = *value.downcast().unwrap(); - match self - .local_values - .binary_search_by_key(&(root_id, &path), |e| (e.0, &e.1)) - { - Ok(ix) => self.local_values[ix].2 = value, - Err(ix) => self.local_values.insert(ix, (root_id, path, value)), - } - } -} - -#[cfg(test)] -mod tests { - use std::num::NonZeroU32; - - use crate::{ - ClosePosition, ItemSettingsContent, VsCodeSettingsSource, default_settings, - settings_content::LanguageSettingsContent, test_settings, - }; - - use super::*; - use unindent::Unindent; - use util::rel_path::rel_path; - - #[derive(Debug, PartialEq)] - struct AutoUpdateSetting { - auto_update: bool, - } - - impl Settings for AutoUpdateSetting { - fn from_settings(content: &SettingsContent) -> Self { - AutoUpdateSetting { - auto_update: content.auto_update.unwrap(), - } - } - } - - #[derive(Debug, PartialEq)] - struct ItemSettings { - close_position: ClosePosition, - git_status: bool, - } - - impl Settings for ItemSettings { - fn from_settings(content: &SettingsContent) -> Self { - let content = content.tabs.clone().unwrap(); - ItemSettings { - close_position: content.close_position.unwrap(), - git_status: content.git_status.unwrap(), - } - } - } - - #[derive(Debug, PartialEq)] - struct DefaultLanguageSettings { - tab_size: NonZeroU32, - preferred_line_length: u32, - } - - impl Settings for DefaultLanguageSettings { - fn from_settings(content: &SettingsContent) -> Self { - let content = &content.project.all_languages.defaults; - DefaultLanguageSettings { - tab_size: content.tab_size.unwrap(), - preferred_line_length: content.preferred_line_length.unwrap(), - } - } - } - - #[derive(Debug, PartialEq)] - struct ThemeSettings { - buffer_font_family: FontFamilyName, - buffer_font_fallbacks: Vec, - } - - impl Settings for ThemeSettings { - fn from_settings(content: &SettingsContent) -> Self { - let content = content.theme.clone(); - ThemeSettings { - buffer_font_family: content.buffer_font_family.unwrap(), - buffer_font_fallbacks: content.buffer_font_fallbacks.unwrap(), - } - } - } - - #[gpui::test] - fn test_settings_store_basic(cx: &mut App) { - let mut store = SettingsStore::new(cx, &default_settings()); - store.register_setting::(); - store.register_setting::(); - store.register_setting::(); - - assert_eq!( - store.get::(None), - &AutoUpdateSetting { auto_update: true } - ); - assert_eq!( - store.get::(None).close_position, - ClosePosition::Right - ); - - store - .set_user_settings( - r#"{ - "auto_update": false, - "tabs": { - "close_position": "left" - } - }"#, - cx, - ) - .unwrap(); - - assert_eq!( - store.get::(None), - &AutoUpdateSetting { auto_update: false } - ); - assert_eq!( - store.get::(None).close_position, - ClosePosition::Left - ); - - store - .set_local_settings( - WorktreeId::from_usize(1), - rel_path("root1").into(), - LocalSettingsKind::Settings, - Some(r#"{ "tab_size": 5 }"#), - cx, - ) - .unwrap(); - store - .set_local_settings( - WorktreeId::from_usize(1), - rel_path("root1/subdir").into(), - LocalSettingsKind::Settings, - Some(r#"{ "preferred_line_length": 50 }"#), - cx, - ) - .unwrap(); - - store - .set_local_settings( - WorktreeId::from_usize(1), - rel_path("root2").into(), - LocalSettingsKind::Settings, - Some(r#"{ "tab_size": 9, "auto_update": true}"#), - cx, - ) - .unwrap(); - - assert_eq!( - store.get::(Some(SettingsLocation { - worktree_id: WorktreeId::from_usize(1), - path: rel_path("root1/something"), - })), - &DefaultLanguageSettings { - preferred_line_length: 80, - tab_size: 5.try_into().unwrap(), - } - ); - assert_eq!( - store.get::(Some(SettingsLocation { - worktree_id: WorktreeId::from_usize(1), - path: rel_path("root1/subdir/something"), - })), - &DefaultLanguageSettings { - preferred_line_length: 50, - tab_size: 5.try_into().unwrap(), - } - ); - assert_eq!( - store.get::(Some(SettingsLocation { - worktree_id: WorktreeId::from_usize(1), - path: rel_path("root2/something"), - })), - &DefaultLanguageSettings { - preferred_line_length: 80, - tab_size: 9.try_into().unwrap(), - } - ); - assert_eq!( - store.get::(Some(SettingsLocation { - worktree_id: WorktreeId::from_usize(1), - path: rel_path("root2/something") - })), - &AutoUpdateSetting { auto_update: false } - ); - } - - #[gpui::test] - fn test_setting_store_assign_json_before_register(cx: &mut App) { - let mut store = SettingsStore::new(cx, &test_settings()); - store - .set_user_settings(r#"{ "auto_update": false }"#, cx) - .unwrap(); - store.register_setting::(); - - assert_eq!( - store.get::(None), - &AutoUpdateSetting { auto_update: false } - ); - } - - #[track_caller] - fn check_settings_update( - store: &mut SettingsStore, - old_json: String, - update: fn(&mut SettingsContent), - expected_new_json: String, - cx: &mut App, - ) { - store.set_user_settings(&old_json, cx).ok(); - let edits = store.edits_for_update(&old_json, update); - let mut new_json = old_json; - for (range, replacement) in edits.into_iter() { - new_json.replace_range(range, &replacement); - } - pretty_assertions::assert_eq!(new_json, expected_new_json); - } - - #[gpui::test] - fn test_setting_store_update(cx: &mut App) { - let mut store = SettingsStore::new(cx, &test_settings()); - - // entries added and updated - check_settings_update( - &mut store, - r#"{ - "languages": { - "JSON": { - "auto_indent": true - } - } - }"# - .unindent(), - |settings| { - settings - .languages_mut() - .get_mut("JSON") - .unwrap() - .auto_indent = Some(false); - - settings.languages_mut().insert( - "Rust".into(), - LanguageSettingsContent { - auto_indent: Some(true), - ..Default::default() - }, - ); - }, - r#"{ - "languages": { - "Rust": { - "auto_indent": true - }, - "JSON": { - "auto_indent": false - } - } - }"# - .unindent(), - cx, - ); - - // entries removed - check_settings_update( - &mut store, - r#"{ - "languages": { - "Rust": { - "language_setting_2": true - }, - "JSON": { - "language_setting_1": false - } - } - }"# - .unindent(), - |settings| { - settings.languages_mut().remove("JSON").unwrap(); - }, - r#"{ - "languages": { - "Rust": { - "language_setting_2": true - } - } - }"# - .unindent(), - cx, - ); - - check_settings_update( - &mut store, - r#"{ - "languages": { - "Rust": { - "language_setting_2": true - }, - "JSON": { - "language_setting_1": false - } - } - }"# - .unindent(), - |settings| { - settings.languages_mut().remove("Rust").unwrap(); - }, - r#"{ - "languages": { - "JSON": { - "language_setting_1": false - } - } - }"# - .unindent(), - cx, - ); - - // weird formatting - check_settings_update( - &mut store, - r#"{ - "tabs": { "close_position": "left", "name": "Max" } - }"# - .unindent(), - |settings| { - settings.tabs.as_mut().unwrap().close_position = Some(ClosePosition::Left); - }, - r#"{ - "tabs": { "close_position": "left", "name": "Max" } - }"# - .unindent(), - cx, - ); - - // single-line formatting, other keys - check_settings_update( - &mut store, - r#"{ "one": 1, "two": 2 }"#.to_owned(), - |settings| settings.auto_update = Some(true), - r#"{ "auto_update": true, "one": 1, "two": 2 }"#.to_owned(), - cx, - ); - - // empty object - check_settings_update( - &mut store, - r#"{ - "tabs": {} - }"# - .unindent(), - |settings| settings.tabs.as_mut().unwrap().close_position = Some(ClosePosition::Left), - r#"{ - "tabs": { - "close_position": "left" - } - }"# - .unindent(), - cx, - ); - - // no content - check_settings_update( - &mut store, - r#""#.unindent(), - |settings| { - settings.tabs = Some(ItemSettingsContent { - git_status: Some(true), - ..Default::default() - }) - }, - r#"{ - "tabs": { - "git_status": true - } - } - "# - .unindent(), - cx, - ); - - check_settings_update( - &mut store, - r#"{ - } - "# - .unindent(), - |settings| settings.title_bar.get_or_insert_default().show_branch_name = Some(true), - r#"{ - "title_bar": { - "show_branch_name": true - } - } - "# - .unindent(), - cx, - ); - } - - #[gpui::test] - fn test_vscode_import(cx: &mut App) { - let mut store = SettingsStore::new(cx, &test_settings()); - store.register_setting::(); - store.register_setting::(); - store.register_setting::(); - store.register_setting::(); - - // create settings that werent present - check_vscode_import( - &mut store, - r#"{ - } - "# - .unindent(), - r#" { "editor.tabSize": 37 } "#.to_owned(), - r#"{ - "base_keymap": "VSCode", - "tab_size": 37 - } - "# - .unindent(), - cx, - ); - - // persist settings that were present - check_vscode_import( - &mut store, - r#"{ - "preferred_line_length": 99, - } - "# - .unindent(), - r#"{ "editor.tabSize": 42 }"#.to_owned(), - r#"{ - "base_keymap": "VSCode", - "tab_size": 42, - "preferred_line_length": 99, - } - "# - .unindent(), - cx, - ); - - // don't clobber settings that aren't present in vscode - check_vscode_import( - &mut store, - r#"{ - "preferred_line_length": 99, - "tab_size": 42 - } - "# - .unindent(), - r#"{}"#.to_owned(), - r#"{ - "base_keymap": "VSCode", - "preferred_line_length": 99, - "tab_size": 42 - } - "# - .unindent(), - cx, - ); - - // custom enum - check_vscode_import( - &mut store, - r#"{ - } - "# - .unindent(), - r#"{ "git.decorations.enabled": true }"#.to_owned(), - r#"{ - "project_panel": { - "git_status": true - }, - "outline_panel": { - "git_status": true - }, - "base_keymap": "VSCode", - "tabs": { - "git_status": true - } - } - "# - .unindent(), - cx, - ); - - // font-family - check_vscode_import( - &mut store, - r#"{ - } - "# - .unindent(), - r#"{ "editor.fontFamily": "Cascadia Code, 'Consolas', Courier New" }"#.to_owned(), - r#"{ - "base_keymap": "VSCode", - "buffer_font_fallbacks": [ - "Consolas", - "Courier New" - ], - "buffer_font_family": "Cascadia Code" - } - "# - .unindent(), - cx, - ); - } - - #[track_caller] - fn check_vscode_import( - store: &mut SettingsStore, - old: String, - vscode: String, - expected: String, - cx: &mut App, - ) { - store.set_user_settings(&old, cx).ok(); - let new = store.get_vscode_edits( - old, - &VsCodeSettings::from_str(&vscode, VsCodeSettingsSource::VsCode).unwrap(), - ); - pretty_assertions::assert_eq!(new, expected); - } - - #[gpui::test] - fn test_update_git_settings(cx: &mut App) { - let store = SettingsStore::new(cx, &test_settings()); - - let actual = store.new_text_for_update("{}".to_string(), |current| { - current - .git - .get_or_insert_default() - .inline_blame - .get_or_insert_default() - .enabled = Some(true); - }); - pretty_assertions::assert_str_eq!( - actual, - r#"{ - "git": { - "inline_blame": { - "enabled": true - } - } - } - "# - .unindent() - ); - } - - #[gpui::test] - fn test_global_settings(cx: &mut App) { - let mut store = SettingsStore::new(cx, &test_settings()); - store.register_setting::(); - - // Set global settings - these should override defaults but not user settings - store - .set_global_settings( - r#"{ - "tabs": { - "close_position": "right", - "git_status": true, - } - }"#, - cx, - ) - .unwrap(); - - // Before user settings, global settings should apply - assert_eq!( - store.get::(None), - &ItemSettings { - close_position: ClosePosition::Right, - git_status: true, - } - ); - - // Set user settings - these should override both defaults and global - store - .set_user_settings( - r#"{ - "tabs": { - "close_position": "left" - } - }"#, - cx, - ) - .unwrap(); - - // User settings should override global settings - assert_eq!( - store.get::(None), - &ItemSettings { - close_position: ClosePosition::Left, - git_status: true, // Staff from global settings - } - ); - } - - #[gpui::test] - fn test_get_value_for_field_basic(cx: &mut App) { - let mut store = SettingsStore::new(cx, &test_settings()); - store.register_setting::(); - - store - .set_user_settings(r#"{"preferred_line_length": 0}"#, cx) - .unwrap(); - let local = (WorktreeId::from_usize(0), RelPath::empty().into_arc()); - store - .set_local_settings( - local.0, - local.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{}"#), - cx, - ) - .unwrap(); - - fn get(content: &SettingsContent) -> Option<&u32> { - content - .project - .all_languages - .defaults - .preferred_line_length - .as_ref() - } - - let default_value = *get(&store.default_settings).unwrap(); - - assert_eq!( - store.get_value_from_file(SettingsFile::Project(local.clone()), get), - (SettingsFile::User, Some(&0)) - ); - assert_eq!( - store.get_value_from_file(SettingsFile::User, get), - (SettingsFile::User, Some(&0)) - ); - store.set_user_settings(r#"{}"#, cx).unwrap(); - assert_eq!( - store.get_value_from_file(SettingsFile::Project(local.clone()), get), - (SettingsFile::Default, Some(&default_value)) - ); - store - .set_local_settings( - local.0, - local.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{"preferred_line_length": 80}"#), - cx, - ) - .unwrap(); - assert_eq!( - store.get_value_from_file(SettingsFile::Project(local.clone()), get), - (SettingsFile::Project(local), Some(&80)) - ); - assert_eq!( - store.get_value_from_file(SettingsFile::User, get), - (SettingsFile::Default, Some(&default_value)) - ); - } - - #[gpui::test] - fn test_get_value_for_field_local_worktrees_dont_interfere(cx: &mut App) { - let mut store = SettingsStore::new(cx, &test_settings()); - store.register_setting::(); - store.register_setting::(); - - let local_1 = (WorktreeId::from_usize(0), RelPath::empty().into_arc()); - - let local_1_child = ( - WorktreeId::from_usize(0), - RelPath::new( - std::path::Path::new("child1"), - util::paths::PathStyle::Posix, - ) - .unwrap() - .into_arc(), - ); - - let local_2 = (WorktreeId::from_usize(1), RelPath::empty().into_arc()); - let local_2_child = ( - WorktreeId::from_usize(1), - RelPath::new( - std::path::Path::new("child2"), - util::paths::PathStyle::Posix, - ) - .unwrap() - .into_arc(), - ); - - fn get(content: &SettingsContent) -> Option<&u32> { - content - .project - .all_languages - .defaults - .preferred_line_length - .as_ref() - } - - store - .set_local_settings( - local_1.0, - local_1.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{"preferred_line_length": 1}"#), - cx, - ) - .unwrap(); - store - .set_local_settings( - local_1_child.0, - local_1_child.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{}"#), - cx, - ) - .unwrap(); - store - .set_local_settings( - local_2.0, - local_2.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{"preferred_line_length": 2}"#), - cx, - ) - .unwrap(); - store - .set_local_settings( - local_2_child.0, - local_2_child.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{}"#), - cx, - ) - .unwrap(); - - // each local child should only inherit from it's parent - assert_eq!( - store.get_value_from_file(SettingsFile::Project(local_2_child), get), - (SettingsFile::Project(local_2), Some(&2)) - ); - assert_eq!( - store.get_value_from_file(SettingsFile::Project(local_1_child.clone()), get), - (SettingsFile::Project(local_1.clone()), Some(&1)) - ); - - // adjacent children should be treated as siblings not inherit from each other - let local_1_adjacent_child = (local_1.0, rel_path("adjacent_child").into_arc()); - store - .set_local_settings( - local_1_adjacent_child.0, - local_1_adjacent_child.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{}"#), - cx, - ) - .unwrap(); - store - .set_local_settings( - local_1_child.0, - local_1_child.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{"preferred_line_length": 3}"#), - cx, - ) - .unwrap(); - - assert_eq!( - store.get_value_from_file(SettingsFile::Project(local_1_adjacent_child.clone()), get), - (SettingsFile::Project(local_1.clone()), Some(&1)) - ); - store - .set_local_settings( - local_1_adjacent_child.0, - local_1_adjacent_child.1, - LocalSettingsKind::Settings, - Some(r#"{"preferred_line_length": 3}"#), - cx, - ) - .unwrap(); - store - .set_local_settings( - local_1_child.0, - local_1_child.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{}"#), - cx, - ) - .unwrap(); - assert_eq!( - store.get_value_from_file(SettingsFile::Project(local_1_child), get), - (SettingsFile::Project(local_1), Some(&1)) - ); - } - - #[gpui::test] - fn test_get_overrides_for_field(cx: &mut App) { - let mut store = SettingsStore::new(cx, &test_settings()); - store.register_setting::(); - - let wt0_root = (WorktreeId::from_usize(0), RelPath::empty().into_arc()); - let wt0_child1 = (WorktreeId::from_usize(0), rel_path("child1").into_arc()); - let wt0_child2 = (WorktreeId::from_usize(0), rel_path("child2").into_arc()); - - let wt1_root = (WorktreeId::from_usize(1), RelPath::empty().into_arc()); - let wt1_subdir = (WorktreeId::from_usize(1), rel_path("subdir").into_arc()); - - fn get(content: &SettingsContent) -> &Option { - &content.project.all_languages.defaults.preferred_line_length - } - - store - .set_user_settings(r#"{"preferred_line_length": 100}"#, cx) - .unwrap(); - - store - .set_local_settings( - wt0_root.0, - wt0_root.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{"preferred_line_length": 80}"#), - cx, - ) - .unwrap(); - store - .set_local_settings( - wt0_child1.0, - wt0_child1.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{"preferred_line_length": 120}"#), - cx, - ) - .unwrap(); - store - .set_local_settings( - wt0_child2.0, - wt0_child2.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{}"#), - cx, - ) - .unwrap(); - - store - .set_local_settings( - wt1_root.0, - wt1_root.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{"preferred_line_length": 90}"#), - cx, - ) - .unwrap(); - store - .set_local_settings( - wt1_subdir.0, - wt1_subdir.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{}"#), - cx, - ) - .unwrap(); - - let overrides = store.get_overrides_for_field(SettingsFile::Default, get); - assert_eq!( - overrides, - vec![ - SettingsFile::User, - SettingsFile::Project(wt0_root.clone()), - SettingsFile::Project(wt0_child1.clone()), - SettingsFile::Project(wt1_root.clone()), - ] - ); - - let overrides = store.get_overrides_for_field(SettingsFile::User, get); - assert_eq!( - overrides, - vec![ - SettingsFile::Project(wt0_root.clone()), - SettingsFile::Project(wt0_child1.clone()), - SettingsFile::Project(wt1_root.clone()), - ] - ); - - let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_root), get); - assert_eq!(overrides, vec![]); - - let overrides = - store.get_overrides_for_field(SettingsFile::Project(wt0_child1.clone()), get); - assert_eq!(overrides, vec![]); - - let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_child2), get); - assert_eq!(overrides, vec![]); - - let overrides = store.get_overrides_for_field(SettingsFile::Project(wt1_root), get); - assert_eq!(overrides, vec![]); - - let overrides = store.get_overrides_for_field(SettingsFile::Project(wt1_subdir), get); - assert_eq!(overrides, vec![]); - - let wt0_deep_child = ( - WorktreeId::from_usize(0), - rel_path("child1/subdir").into_arc(), - ); - store - .set_local_settings( - wt0_deep_child.0, - wt0_deep_child.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{"preferred_line_length": 140}"#), - cx, - ) - .unwrap(); - - let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_deep_child), get); - assert_eq!(overrides, vec![]); - - let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_child1), get); - assert_eq!(overrides, vec![]); - } - - #[test] - fn test_file_ord() { - let wt0_root = - SettingsFile::Project((WorktreeId::from_usize(0), RelPath::empty().into_arc())); - let wt0_child1 = - SettingsFile::Project((WorktreeId::from_usize(0), rel_path("child1").into_arc())); - let wt0_child2 = - SettingsFile::Project((WorktreeId::from_usize(0), rel_path("child2").into_arc())); - - let wt1_root = - SettingsFile::Project((WorktreeId::from_usize(1), RelPath::empty().into_arc())); - let wt1_subdir = - SettingsFile::Project((WorktreeId::from_usize(1), rel_path("subdir").into_arc())); - - let mut files = vec![ - &wt1_root, - &SettingsFile::Default, - &wt0_root, - &wt1_subdir, - &wt0_child2, - &SettingsFile::Server, - &wt0_child1, - &SettingsFile::User, - ]; - - files.sort(); - pretty_assertions::assert_eq!( - files, - vec![ - &wt0_child2, - &wt0_child1, - &wt0_root, - &wt1_subdir, - &wt1_root, - &SettingsFile::Server, - &SettingsFile::User, - &SettingsFile::Default, - ] - ) - } -} diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs deleted file mode 100644 index 587850303f..0000000000 --- a/crates/settings/src/vscode_import.rs +++ /dev/null @@ -1,914 +0,0 @@ -use crate::*; -use anyhow::{Context as _, Result, anyhow}; -use collections::HashMap; -use fs::Fs; -use paths::{cursor_settings_file_paths, vscode_settings_file_paths}; -use serde::Deserialize; -use serde_json::{Map, Value}; -use std::{ - num::{NonZeroU32, NonZeroUsize}, - path::{Path, PathBuf}, - sync::Arc, -}; - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum VsCodeSettingsSource { - VsCode, - Cursor, -} - -impl std::fmt::Display for VsCodeSettingsSource { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - VsCodeSettingsSource::VsCode => write!(f, "VS Code"), - VsCodeSettingsSource::Cursor => write!(f, "Cursor"), - } - } -} - -pub struct VsCodeSettings { - pub source: VsCodeSettingsSource, - pub path: Arc, - content: Map, -} - -impl VsCodeSettings { - #[cfg(any(test, feature = "test-support"))] - pub fn from_str(content: &str, source: VsCodeSettingsSource) -> Result { - Ok(Self { - source, - path: Path::new("/example-path/Code/User/settings.json").into(), - content: serde_json_lenient::from_str(content)?, - }) - } - - pub async fn load_user_settings(source: VsCodeSettingsSource, fs: Arc) -> Result { - let candidate_paths = match source { - VsCodeSettingsSource::VsCode => vscode_settings_file_paths(), - VsCodeSettingsSource::Cursor => cursor_settings_file_paths(), - }; - let mut path = None; - for candidate_path in candidate_paths.iter() { - if fs.is_file(candidate_path).await { - path = Some(candidate_path.clone()); - } - } - let Some(path) = path else { - return Err(anyhow!( - "No settings file found, expected to find it in one of the following paths:\n{}", - candidate_paths - .into_iter() - .map(|path| path.to_string_lossy().into_owned()) - .collect::>() - .join("\n") - )); - }; - let content = fs.load(&path).await.with_context(|| { - format!( - "Error loading {} settings file from {}", - source, - path.display() - ) - })?; - let content = serde_json_lenient::from_str(&content).with_context(|| { - format!( - "Error parsing {} settings file from {}", - source, - path.display() - ) - })?; - Ok(Self { - source, - path: path.into(), - content, - }) - } - - fn read_value(&self, setting: &str) -> Option<&Value> { - self.content.get(setting) - } - - fn read_str(&self, setting: &str) -> Option<&str> { - self.read_value(setting).and_then(|v| v.as_str()) - } - - fn read_string(&self, setting: &str) -> Option { - self.read_value(setting) - .and_then(|v| v.as_str()) - .map(|s| s.to_owned()) - } - - fn read_bool(&self, setting: &str) -> Option { - self.read_value(setting).and_then(|v| v.as_bool()) - } - - fn read_f32(&self, setting: &str) -> Option { - self.read_value(setting) - .and_then(|v| v.as_f64()) - .map(|v| v as f32) - } - - fn read_u64(&self, setting: &str) -> Option { - self.read_value(setting).and_then(|v| v.as_u64()) - } - - fn read_usize(&self, setting: &str) -> Option { - self.read_value(setting) - .and_then(|v| v.as_u64()) - .and_then(|v| v.try_into().ok()) - } - - fn read_u32(&self, setting: &str) -> Option { - self.read_value(setting) - .and_then(|v| v.as_u64()) - .and_then(|v| v.try_into().ok()) - } - - fn read_enum(&self, key: &str, f: impl FnOnce(&str) -> Option) -> Option { - self.content.get(key).and_then(Value::as_str).and_then(f) - } - - fn read_fonts(&self, key: &str) -> (Option, Option>) { - let Some(css_name) = self.content.get(key).and_then(Value::as_str) else { - return (None, None); - }; - - let mut name_buffer = String::new(); - let mut quote_char: Option = None; - let mut fonts = Vec::new(); - let mut add_font = |buffer: &mut String| { - let trimmed = buffer.trim(); - if !trimmed.is_empty() { - fonts.push(trimmed.to_string().into()); - } - - buffer.clear(); - }; - - for ch in css_name.chars() { - match (ch, quote_char) { - ('"' | '\'', None) => { - quote_char = Some(ch); - } - (_, Some(q)) if ch == q => { - quote_char = None; - } - (',', None) => { - add_font(&mut name_buffer); - } - _ => { - name_buffer.push(ch); - } - } - } - - add_font(&mut name_buffer); - if fonts.is_empty() { - return (None, None); - } - (Some(fonts.remove(0)), skip_default(fonts)) - } - - pub fn settings_content(&self) -> SettingsContent { - SettingsContent { - agent: self.agent_settings_content(), - agent_servers: None, - audio: None, - auto_update: None, - base_keymap: Some(BaseKeymapContent::VSCode), - calls: None, - collaboration_panel: None, - debugger: None, - diagnostics: None, - disable_ai: None, - editor: self.editor_settings_content(), - extension: ExtensionSettingsContent::default(), - file_finder: None, - git: self.git_settings_content(), - git_panel: self.git_panel_settings_content(), - global_lsp_settings: None, - helix_mode: None, - image_viewer: None, - journal: None, - language_models: None, - line_indicator_format: None, - log: None, - message_editor: None, - node: self.node_binary_settings(), - notification_panel: None, - outline_panel: self.outline_panel_settings_content(), - preview_tabs: self.preview_tabs_settings_content(), - project: self.project_settings_content(), - project_panel: self.project_panel_settings_content(), - proxy: self.read_string("http.proxy"), - remote: RemoteSettingsContent::default(), - repl: None, - server_url: None, - session: None, - status_bar: self.status_bar_settings_content(), - tab_bar: self.tab_bar_settings_content(), - tabs: self.item_settings_content(), - telemetry: self.telemetry_settings_content(), - terminal: self.terminal_settings_content(), - theme: Box::new(self.theme_settings_content()), - title_bar: None, - vim: None, - vim_mode: None, - workspace: self.workspace_settings_content(), - } - } - - fn agent_settings_content(&self) -> Option { - let enabled = self.read_bool("chat.agent.enabled"); - skip_default(AgentSettingsContent { - enabled: enabled, - button: enabled, - ..Default::default() - }) - } - - fn editor_settings_content(&self) -> EditorSettingsContent { - EditorSettingsContent { - auto_signature_help: self.read_bool("editor.parameterHints.enabled"), - autoscroll_on_clicks: None, - cursor_blink: self.read_enum("editor.cursorBlinking", |s| match s { - "blink" | "phase" | "expand" | "smooth" => Some(true), - "solid" => Some(false), - _ => None, - }), - cursor_shape: self.read_enum("editor.cursorStyle", |s| match s { - "block" => Some(CursorShape::Block), - "block-outline" => Some(CursorShape::Hollow), - "line" | "line-thin" => Some(CursorShape::Bar), - "underline" | "underline-thin" => Some(CursorShape::Underline), - _ => None, - }), - current_line_highlight: self.read_enum("editor.renderLineHighlight", |s| match s { - "gutter" => Some(CurrentLineHighlight::Gutter), - "line" => Some(CurrentLineHighlight::Line), - "all" => Some(CurrentLineHighlight::All), - _ => None, - }), - diagnostics_max_severity: None, - double_click_in_multibuffer: None, - drag_and_drop_selection: None, - excerpt_context_lines: None, - expand_excerpt_lines: None, - fast_scroll_sensitivity: self.read_f32("editor.fastScrollSensitivity"), - sticky_scroll: self.sticky_scroll_content(), - go_to_definition_fallback: None, - gutter: self.gutter_content(), - hide_mouse: None, - horizontal_scroll_margin: None, - hover_popover_delay: self.read_u64("editor.hover.delay").map(Into::into), - hover_popover_enabled: self.read_bool("editor.hover.enabled"), - inline_code_actions: None, - jupyter: None, - lsp_document_colors: None, - lsp_highlight_debounce: None, - middle_click_paste: None, - minimap: self.minimap_content(), - minimum_contrast_for_highlights: None, - multi_cursor_modifier: self.read_enum("editor.multiCursorModifier", |s| match s { - "ctrlCmd" => Some(MultiCursorModifier::CmdOrCtrl), - "alt" => Some(MultiCursorModifier::Alt), - _ => None, - }), - redact_private_values: None, - relative_line_numbers: self.read_enum("editor.lineNumbers", |s| match s { - "relative" => Some(RelativeLineNumbers::Enabled), - _ => None, - }), - rounded_selection: self.read_bool("editor.roundedSelection"), - scroll_beyond_last_line: None, - scroll_sensitivity: self.read_f32("editor.mouseWheelScrollSensitivity"), - scrollbar: self.scrollbar_content(), - search: self.search_content(), - search_wrap: None, - seed_search_query_from_cursor: self.read_enum( - "editor.find.seedSearchStringFromSelection", - |s| match s { - "always" => Some(SeedQuerySetting::Always), - "selection" => Some(SeedQuerySetting::Selection), - "never" => Some(SeedQuerySetting::Never), - _ => None, - }, - ), - selection_highlight: self.read_bool("editor.selectionHighlight"), - show_signature_help_after_edits: self.read_bool("editor.parameterHints.enabled"), - snippet_sort_order: None, - toolbar: None, - use_smartcase_search: self.read_bool("search.smartCase"), - vertical_scroll_margin: self.read_f32("editor.cursorSurroundingLines"), - completion_menu_scrollbar: None, - } - } - - fn sticky_scroll_content(&self) -> Option { - skip_default(StickyScrollContent { - enabled: self.read_bool("editor.stickyScroll.enabled"), - }) - } - - fn gutter_content(&self) -> Option { - skip_default(GutterContent { - line_numbers: self.read_enum("editor.lineNumbers", |s| match s { - "on" | "relative" => Some(true), - "off" => Some(false), - _ => None, - }), - min_line_number_digits: None, - runnables: None, - breakpoints: None, - folds: self.read_enum("editor.showFoldingControls", |s| match s { - "always" | "mouseover" => Some(true), - "never" => Some(false), - _ => None, - }), - }) - } - - fn scrollbar_content(&self) -> Option { - let scrollbar_axes = skip_default(ScrollbarAxesContent { - horizontal: self.read_enum("editor.scrollbar.horizontal", |s| match s { - "auto" | "visible" => Some(true), - "hidden" => Some(false), - _ => None, - }), - vertical: self.read_enum("editor.scrollbar.vertical", |s| match s { - "auto" | "visible" => Some(true), - "hidden" => Some(false), - _ => None, - }), - })?; - - Some(ScrollbarContent { - axes: Some(scrollbar_axes), - ..Default::default() - }) - } - - fn search_content(&self) -> Option { - skip_default(SearchSettingsContent { - include_ignored: self.read_bool("search.useIgnoreFiles"), - ..Default::default() - }) - } - - fn minimap_content(&self) -> Option { - let minimap_enabled = self.read_bool("editor.minimap.enabled"); - let autohide = self.read_bool("editor.minimap.autohide"); - let show = match (minimap_enabled, autohide) { - (Some(true), Some(false)) => Some(ShowMinimap::Always), - (Some(true), _) => Some(ShowMinimap::Auto), - (Some(false), _) => Some(ShowMinimap::Never), - _ => None, - }; - - skip_default(MinimapContent { - show, - thumb: self.read_enum("editor.minimap.showSlider", |s| match s { - "always" => Some(MinimapThumb::Always), - "mouseover" => Some(MinimapThumb::Hover), - _ => None, - }), - max_width_columns: self - .read_u32("editor.minimap.maxColumn") - .and_then(|v| NonZeroU32::new(v)), - ..Default::default() - }) - } - - fn git_panel_settings_content(&self) -> Option { - skip_default(GitPanelSettingsContent { - button: self.read_bool("git.enabled"), - fallback_branch_name: self.read_string("git.defaultBranchName"), - ..Default::default() - }) - } - - fn project_settings_content(&self) -> ProjectSettingsContent { - ProjectSettingsContent { - all_languages: AllLanguageSettingsContent { - features: None, - edit_predictions: self.edit_predictions_settings_content(), - defaults: self.default_language_settings_content(), - languages: Default::default(), - file_types: self.file_types(), - }, - worktree: self.worktree_settings_content(), - lsp: Default::default(), - terminal: None, - dap: Default::default(), - context_servers: self.context_servers(), - load_direnv: None, - slash_commands: None, - git_hosting_providers: None, - } - } - - fn default_language_settings_content(&self) -> LanguageSettingsContent { - LanguageSettingsContent { - allow_rewrap: None, - always_treat_brackets_as_autoclosed: None, - auto_indent: None, - auto_indent_on_paste: self.read_bool("editor.formatOnPaste"), - code_actions_on_format: None, - completions: skip_default(CompletionSettingsContent { - words: self.read_bool("editor.suggest.showWords").map(|b| { - if b { - WordsCompletionMode::Enabled - } else { - WordsCompletionMode::Disabled - } - }), - ..Default::default() - }), - debuggers: None, - edit_predictions_disabled_in: None, - enable_language_server: None, - ensure_final_newline_on_save: self.read_bool("files.insertFinalNewline"), - extend_comment_on_newline: None, - format_on_save: self.read_bool("editor.guides.formatOnSave").map(|b| { - if b { - FormatOnSave::On - } else { - FormatOnSave::Off - } - }), - formatter: None, - hard_tabs: self.read_bool("editor.insertSpaces").map(|v| !v), - indent_guides: skip_default(IndentGuideSettingsContent { - enabled: self.read_bool("editor.guides.indentation"), - ..Default::default() - }), - inlay_hints: None, - jsx_tag_auto_close: None, - language_servers: None, - linked_edits: self.read_bool("editor.linkedEditing"), - preferred_line_length: self.read_u32("editor.wordWrapColumn"), - prettier: None, - remove_trailing_whitespace_on_save: self.read_bool("editor.trimAutoWhitespace"), - show_completion_documentation: None, - colorize_brackets: self.read_bool("editor.bracketPairColorization.enabled"), - show_completions_on_input: self.read_bool("editor.suggestOnTriggerCharacters"), - show_edit_predictions: self.read_bool("editor.inlineSuggest.enabled"), - show_whitespaces: self.read_enum("editor.renderWhitespace", |s| { - Some(match s { - "boundary" => ShowWhitespaceSetting::Boundary, - "trailing" => ShowWhitespaceSetting::Trailing, - "selection" => ShowWhitespaceSetting::Selection, - "all" => ShowWhitespaceSetting::All, - _ => ShowWhitespaceSetting::None, - }) - }), - show_wrap_guides: None, - soft_wrap: self.read_enum("editor.wordWrap", |s| match s { - "on" => Some(SoftWrap::EditorWidth), - "wordWrapColumn" => Some(SoftWrap::PreferLine), - "bounded" => Some(SoftWrap::Bounded), - "off" => Some(SoftWrap::None), - _ => None, - }), - tab_size: self - .read_u32("editor.tabSize") - .and_then(|n| NonZeroU32::new(n)), - tasks: None, - use_auto_surround: self.read_enum("editor.autoSurround", |s| match s { - "languageDefined" | "quotes" | "brackets" => Some(true), - "never" => Some(false), - _ => None, - }), - use_autoclose: None, - use_on_type_format: self.read_bool("editor.formatOnType"), - whitespace_map: None, - wrap_guides: self - .read_value("editor.rulers") - .and_then(|v| v.as_array()) - .map(|v| { - v.iter() - .flat_map(|n| n.as_u64().map(|n| n as usize)) - .collect() - }), - word_diff_enabled: None, - } - } - - fn file_types(&self) -> Option, ExtendingVec>> { - // vscodes file association map is inverted from ours, so we flip the mapping before merging - let mut associations: HashMap, ExtendingVec> = HashMap::default(); - let map = self.read_value("files.associations")?.as_object()?; - for (k, v) in map { - let Some(v) = v.as_str() else { continue }; - associations.entry(v.into()).or_default().0.push(k.clone()); - } - skip_default(associations) - } - - fn edit_predictions_settings_content(&self) -> Option { - let disabled_globs = self - .read_value("cursor.general.globalCursorIgnoreList")? - .as_array()?; - - skip_default(EditPredictionSettingsContent { - disabled_globs: skip_default( - disabled_globs - .iter() - .filter_map(|glob| glob.as_str()) - .map(|s| s.to_string()) - .collect(), - ), - ..Default::default() - }) - } - - fn outline_panel_settings_content(&self) -> Option { - skip_default(OutlinePanelSettingsContent { - file_icons: self.read_bool("outline.icons"), - folder_icons: self.read_bool("outline.icons"), - git_status: self.read_bool("git.decorations.enabled"), - ..Default::default() - }) - } - - fn node_binary_settings(&self) -> Option { - // this just sets the binary name instead of a full path so it relies on path lookup - // resolving to the one you want - skip_default(NodeBinarySettings { - npm_path: self.read_enum("npm.packageManager", |s| match s { - v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()), - _ => None, - }), - ..Default::default() - }) - } - - fn git_settings_content(&self) -> Option { - let inline_blame = self.read_bool("git.blame.editorDecoration.enabled")?; - skip_default(GitSettings { - inline_blame: Some(InlineBlameSettings { - enabled: Some(inline_blame), - ..Default::default() - }), - ..Default::default() - }) - } - - fn context_servers(&self) -> HashMap, ContextServerSettingsContent> { - #[derive(Deserialize)] - struct VsCodeContextServerCommand { - command: PathBuf, - args: Option>, - env: Option>, - // note: we don't support envFile and type - } - let Some(mcp) = self.read_value("mcp").and_then(|v| v.as_object()) else { - return Default::default(); - }; - mcp.iter() - .filter_map(|(k, v)| { - Some(( - k.clone().into(), - ContextServerSettingsContent::Stdio { - enabled: true, - command: serde_json::from_value::(v.clone()) - .ok() - .map(|cmd| ContextServerCommand { - path: cmd.command, - args: cmd.args.unwrap_or_default(), - env: cmd.env, - timeout: None, - })?, - }, - )) - }) - .collect() - } - - fn item_settings_content(&self) -> Option { - skip_default(ItemSettingsContent { - git_status: self.read_bool("git.decorations.enabled"), - close_position: self.read_enum("workbench.editor.tabActionLocation", |s| match s { - "right" => Some(ClosePosition::Right), - "left" => Some(ClosePosition::Left), - _ => None, - }), - file_icons: self.read_bool("workbench.editor.showIcons"), - activate_on_close: self - .read_bool("workbench.editor.focusRecentEditorAfterClose") - .map(|b| { - if b { - ActivateOnClose::History - } else { - ActivateOnClose::LeftNeighbour - } - }), - show_diagnostics: None, - show_close_button: self - .read_bool("workbench.editor.tabActionCloseVisibility") - .map(|b| { - if b { - ShowCloseButton::Always - } else { - ShowCloseButton::Hidden - } - }), - }) - } - - fn preview_tabs_settings_content(&self) -> Option { - skip_default(PreviewTabsSettingsContent { - enabled: self.read_bool("workbench.editor.enablePreview"), - enable_preview_from_project_panel: None, - enable_preview_from_file_finder: self - .read_bool("workbench.editor.enablePreviewFromQuickOpen"), - enable_preview_from_multibuffer: None, - enable_preview_multibuffer_from_code_navigation: None, - enable_preview_file_from_code_navigation: None, - enable_keep_preview_on_code_navigation: self - .read_bool("workbench.editor.enablePreviewFromCodeNavigation"), - }) - } - - fn tab_bar_settings_content(&self) -> Option { - skip_default(TabBarSettingsContent { - show: self.read_enum("workbench.editor.showTabs", |s| match s { - "multiple" => Some(true), - "single" | "none" => Some(false), - _ => None, - }), - show_nav_history_buttons: None, - show_tab_bar_buttons: self - .read_str("workbench.editor.editorActionsLocation") - .and_then(|str| if str == "hidden" { Some(false) } else { None }), - }) - } - - fn status_bar_settings_content(&self) -> Option { - skip_default(StatusBarSettingsContent { - show: self.read_bool("workbench.statusBar.visible"), - active_language_button: None, - cursor_position_button: None, - line_endings_button: None, - }) - } - - fn project_panel_settings_content(&self) -> Option { - let mut project_panel_settings = ProjectPanelSettingsContent { - auto_fold_dirs: self.read_bool("explorer.compactFolders"), - auto_reveal_entries: self.read_bool("explorer.autoReveal"), - button: None, - default_width: None, - dock: None, - drag_and_drop: None, - entry_spacing: None, - file_icons: None, - folder_icons: None, - git_status: self.read_bool("git.decorations.enabled"), - hide_gitignore: self.read_bool("explorer.excludeGitIgnore"), - hide_hidden: None, - hide_root: None, - indent_guides: None, - indent_size: None, - scrollbar: None, - show_diagnostics: self - .read_bool("problems.decorations.enabled") - .and_then(|b| if b { Some(ShowDiagnostics::Off) } else { None }), - sort_mode: None, - starts_open: None, - sticky_scroll: None, - auto_open: None, - }; - - if let (Some(false), Some(false)) = ( - self.read_bool("explorer.decorations.badges"), - self.read_bool("explorer.decorations.colors"), - ) { - project_panel_settings.git_status = Some(false); - project_panel_settings.show_diagnostics = Some(ShowDiagnostics::Off); - } - - skip_default(project_panel_settings) - } - - fn telemetry_settings_content(&self) -> Option { - self.read_enum("telemetry.telemetryLevel", |level| { - let (metrics, diagnostics) = match level { - "all" => (true, true), - "error" | "crash" => (false, true), - "off" => (false, false), - _ => return None, - }; - Some(TelemetrySettingsContent { - metrics: Some(metrics), - diagnostics: Some(diagnostics), - }) - }) - } - - fn terminal_settings_content(&self) -> Option { - let (font_family, font_fallbacks) = self.read_fonts("terminal.integrated.fontFamily"); - skip_default(TerminalSettingsContent { - alternate_scroll: None, - blinking: self - .read_bool("terminal.integrated.cursorBlinking") - .map(|b| { - if b { - TerminalBlink::On - } else { - TerminalBlink::Off - } - }), - button: None, - copy_on_select: self.read_bool("terminal.integrated.copyOnSelection"), - cursor_shape: self.read_enum("terminal.integrated.cursorStyle", |s| match s { - "block" => Some(CursorShapeContent::Block), - "line" => Some(CursorShapeContent::Bar), - "underline" => Some(CursorShapeContent::Underline), - _ => None, - }), - default_height: None, - default_width: None, - dock: None, - font_fallbacks, - font_family, - font_features: None, - font_size: self.read_f32("terminal.integrated.fontSize"), - font_weight: None, - keep_selection_on_copy: None, - line_height: self - .read_f32("terminal.integrated.lineHeight") - .map(|lh| TerminalLineHeight::Custom(lh)), - max_scroll_history_lines: self.read_usize("terminal.integrated.scrollback"), - minimum_contrast: None, - option_as_meta: self.read_bool("terminal.integrated.macOptionIsMeta"), - project: self.project_terminal_settings_content(), - scrollbar: None, - scroll_multiplier: None, - toolbar: None, - }) - } - - fn project_terminal_settings_content(&self) -> ProjectTerminalSettingsContent { - #[cfg(target_os = "windows")] - let platform = "windows"; - #[cfg(target_os = "linux")] - let platform = "linux"; - #[cfg(target_os = "macos")] - let platform = "osx"; - #[cfg(target_os = "freebsd")] - let platform = "freebsd"; - let env = self - .read_value(&format!("terminal.integrated.env.{platform}")) - .and_then(|v| v.as_object()) - .map(|v| { - v.iter() - .map(|(k, v)| (k.clone(), v.to_string())) - // zed does not support substitutions, so this can break env vars - .filter(|(_, v)| !v.contains('$')) - .collect() - }); - - ProjectTerminalSettingsContent { - // TODO: handle arguments - shell: self - .read_string(&format!("terminal.integrated.{platform}Exec")) - .map(|s| Shell::Program(s)), - working_directory: None, - env, - detect_venv: None, - path_hyperlink_regexes: None, - path_hyperlink_timeout_ms: None, - } - } - - fn theme_settings_content(&self) -> ThemeSettingsContent { - let (buffer_font_family, buffer_font_fallbacks) = self.read_fonts("editor.fontFamily"); - ThemeSettingsContent { - ui_font_size: None, - ui_font_family: None, - ui_font_fallbacks: None, - ui_font_features: None, - ui_font_weight: None, - buffer_font_family, - buffer_font_fallbacks, - buffer_font_size: self.read_f32("editor.fontSize"), - buffer_font_weight: self.read_f32("editor.fontWeight").map(|w| w.into()), - buffer_line_height: None, - buffer_font_features: None, - agent_ui_font_size: None, - agent_buffer_font_size: None, - theme: None, - icon_theme: None, - ui_density: None, - unnecessary_code_fade: None, - experimental_theme_overrides: None, - theme_overrides: Default::default(), - } - } - - fn workspace_settings_content(&self) -> WorkspaceSettingsContent { - WorkspaceSettingsContent { - active_pane_modifiers: self.active_pane_modifiers(), - autosave: self.read_enum("files.autoSave", |s| match s { - "off" => Some(AutosaveSetting::Off), - "afterDelay" => Some(AutosaveSetting::AfterDelay { - milliseconds: self - .read_value("files.autoSaveDelay") - .and_then(|v| v.as_u64()) - .unwrap_or(1000) - .into(), - }), - "onFocusChange" => Some(AutosaveSetting::OnFocusChange), - "onWindowChange" => Some(AutosaveSetting::OnWindowChange), - _ => None, - }), - bottom_dock_layout: None, - centered_layout: None, - close_on_file_delete: None, - command_aliases: Default::default(), - confirm_quit: self.read_enum("window.confirmBeforeClose", |s| match s { - "always" | "keyboardOnly" => Some(true), - "never" => Some(false), - _ => None, - }), - drop_target_size: None, - // workbench.editor.limit contains "enabled", "value", and "perEditorGroup" - // our semantics match if those are set to true, some N, and true respectively. - // we'll ignore "perEditorGroup" for now since we only support a global max - max_tabs: if self.read_bool("workbench.editor.limit.enabled") == Some(true) { - self.read_usize("workbench.editor.limit.value") - .and_then(|n| NonZeroUsize::new(n)) - } else { - None - }, - on_last_window_closed: None, - pane_split_direction_horizontal: None, - pane_split_direction_vertical: None, - resize_all_panels_in_dock: None, - restore_on_file_reopen: self.read_bool("workbench.editor.restoreViewState"), - restore_on_startup: None, - window_decorations: None, - show_call_status_icon: None, - use_system_path_prompts: self.read_bool("files.simpleDialog.enable"), - use_system_prompts: None, - use_system_window_tabs: self.read_bool("window.nativeTabs"), - when_closing_with_no_tabs: self.read_bool("window.closeWhenEmpty").map(|b| { - if b { - CloseWindowWhenNoItems::CloseWindow - } else { - CloseWindowWhenNoItems::KeepWindowOpen - } - }), - zoomed_padding: None, - } - } - - fn active_pane_modifiers(&self) -> Option { - if self.read_bool("accessibility.dimUnfocused.enabled") == Some(true) - && let Some(opacity) = self.read_f32("accessibility.dimUnfocused.opacity") - { - Some(ActivePaneModifiers { - border_size: None, - inactive_opacity: Some(InactiveOpacity(opacity)), - }) - } else { - None - } - } - - fn worktree_settings_content(&self) -> WorktreeSettingsContent { - WorktreeSettingsContent { - project_name: None, - prevent_sharing_in_public_channels: false, - file_scan_exclusions: self - .read_value("files.watcherExclude") - .and_then(|v| v.as_array()) - .map(|v| { - v.iter() - .filter_map(|n| n.as_str().map(str::to_owned)) - .collect::>() - }) - .filter(|r| !r.is_empty()), - file_scan_inclusions: self - .read_value("files.watcherInclude") - .and_then(|v| v.as_array()) - .map(|v| { - v.iter() - .filter_map(|n| n.as_str().map(str::to_owned)) - .collect::>() - }) - .filter(|r| !r.is_empty()), - private_files: None, - hidden_files: None, - } - } -} - -fn skip_default(value: T) -> Option { - if value == T::default() { - None - } else { - Some(value) - } -} diff --git a/crates/settings_json/Cargo.toml b/crates/settings_json/Cargo.toml deleted file mode 100644 index 2ba9887ca0..0000000000 --- a/crates/settings_json/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "settings_json" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/settings_json.rs" - -[features] -default = [] - -[dependencies] -anyhow.workspace = true -tree-sitter.workspace = true -tree-sitter-json.workspace = true -util.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_json_lenient.workspace = true -serde_path_to_error.workspace = true - -[dev-dependencies] -unindent.workspace = true -pretty_assertions.workspace = true - -# Uncomment other workspace dependencies as needed -# assistant.workspace = true -# client.workspace = true -# project.workspace = true -# settings.workspace = true diff --git a/crates/settings_json/LICENSE-GPL b/crates/settings_json/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/settings_json/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/settings_json/src/settings_json.rs b/crates/settings_json/src/settings_json.rs deleted file mode 100644 index 5198e475af..0000000000 --- a/crates/settings_json/src/settings_json.rs +++ /dev/null @@ -1,2635 +0,0 @@ -use anyhow::Result; -use serde::{Serialize, de::DeserializeOwned}; -use serde_json::Value; -use std::{ops::Range, sync::LazyLock}; -use tree_sitter::{Query, StreamingIterator as _}; -use util::RangeExt; - -pub fn update_value_in_json_text<'a>( - text: &mut String, - key_path: &mut Vec<&'a str>, - tab_size: usize, - old_value: &'a Value, - new_value: &'a Value, - edits: &mut Vec<(Range, String)>, -) { - // If the old and new values are both objects, then compare them key by key, - // preserving the comments and formatting of the unchanged parts. Otherwise, - // replace the old value with the new value. - if let (Value::Object(old_object), Value::Object(new_object)) = (old_value, new_value) { - for (key, old_sub_value) in old_object.iter() { - key_path.push(key); - if let Some(new_sub_value) = new_object.get(key) { - // Key exists in both old and new, recursively update - update_value_in_json_text( - text, - key_path, - tab_size, - old_sub_value, - new_sub_value, - edits, - ); - } else { - // Key was removed from new object, remove the entire key-value pair - let (range, replacement) = - replace_value_in_json_text(text, key_path, 0, None, None); - text.replace_range(range.clone(), &replacement); - edits.push((range, replacement)); - } - key_path.pop(); - } - for (key, new_sub_value) in new_object.iter() { - key_path.push(key); - if !old_object.contains_key(key) { - update_value_in_json_text( - text, - key_path, - tab_size, - &Value::Null, - new_sub_value, - edits, - ); - } - key_path.pop(); - } - } else if old_value != new_value { - let mut new_value = new_value.clone(); - if let Some(new_object) = new_value.as_object_mut() { - new_object.retain(|_, v| !v.is_null()); - } - let (range, replacement) = - replace_value_in_json_text(text, key_path, tab_size, Some(&new_value), None); - text.replace_range(range.clone(), &replacement); - edits.push((range, replacement)); - } -} - -/// * `replace_key` - When an exact key match according to `key_path` is found, replace the key with `replace_key` if `Some`. -pub fn replace_value_in_json_text>( - text: &str, - key_path: &[T], - tab_size: usize, - new_value: Option<&Value>, - replace_key: Option<&str>, -) -> (Range, String) { - static PAIR_QUERY: LazyLock = LazyLock::new(|| { - Query::new( - &tree_sitter_json::LANGUAGE.into(), - "(pair key: (string) @key value: (_) @value)", - ) - .expect("Failed to create PAIR_QUERY") - }); - - let mut parser = tree_sitter::Parser::new(); - parser - .set_language(&tree_sitter_json::LANGUAGE.into()) - .unwrap(); - let syntax_tree = parser.parse(text, None).unwrap(); - - let mut cursor = tree_sitter::QueryCursor::new(); - - let mut depth = 0; - let mut last_value_range = 0..0; - let mut first_key_start = None; - let mut existing_value_range = 0..text.len(); - - let mut matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes()); - while let Some(mat) = matches.next() { - if mat.captures.len() != 2 { - continue; - } - - let key_range = mat.captures[0].node.byte_range(); - let value_range = mat.captures[1].node.byte_range(); - - // Don't enter sub objects until we find an exact - // match for the current keypath - if last_value_range.contains_inclusive(&value_range) { - continue; - } - - last_value_range = value_range.clone(); - - if key_range.start > existing_value_range.end { - break; - } - - first_key_start.get_or_insert(key_range.start); - - let found_key = text - .get(key_range.clone()) - .zip(key_path.get(depth)) - .and_then(|(key_text, key_path_value)| { - serde_json::to_string(key_path_value.as_ref()) - .ok() - .map(|key_path| depth < key_path.len() && key_text == key_path) - }) - .unwrap_or(false); - - if found_key { - existing_value_range = value_range; - // Reset last value range when increasing in depth - last_value_range = existing_value_range.start..existing_value_range.start; - depth += 1; - - if depth == key_path.len() { - break; - } - - if let Some(array_replacement) = handle_possible_array_value( - &mat.captures[0].node, - &mat.captures[1].node, - text, - &key_path[depth..], - new_value, - replace_key, - tab_size, - ) { - return array_replacement; - } - - first_key_start = None; - } - } - - // We found the exact key we want - if depth == key_path.len() { - if let Some(new_value) = new_value { - let new_val = to_pretty_json(new_value, tab_size, tab_size * depth); - if let Some(replace_key) = replace_key.and_then(|str| serde_json::to_string(str).ok()) { - let new_key = format!("{}: ", replace_key); - if let Some(key_start) = text[..existing_value_range.start].rfind('"') { - if let Some(prev_key_start) = text[..key_start].rfind('"') { - existing_value_range.start = prev_key_start; - } else { - existing_value_range.start = key_start; - } - } - (existing_value_range, new_key + &new_val) - } else { - (existing_value_range, new_val) - } - } else { - let mut removal_start = first_key_start.unwrap_or(existing_value_range.start); - let mut removal_end = existing_value_range.end; - - // Find the actual key position by looking for the key in the pair - // We need to extend the range to include the key, not just the value - if let Some(key_start) = text[..existing_value_range.start].rfind('"') { - if let Some(prev_key_start) = text[..key_start].rfind('"') { - removal_start = prev_key_start; - } else { - removal_start = key_start; - } - } - - let mut removed_comma = false; - // Look backward for a preceding comma first - let preceding_text = text.get(0..removal_start).unwrap_or(""); - if let Some(comma_pos) = preceding_text.rfind(',') { - // Check if there are only whitespace characters between the comma and our key - let between_comma_and_key = text.get(comma_pos + 1..removal_start).unwrap_or(""); - if between_comma_and_key.trim().is_empty() { - removal_start = comma_pos; - removed_comma = true; - } - } - if let Some(remaining_text) = text.get(existing_value_range.end..) - && !removed_comma - { - let mut chars = remaining_text.char_indices(); - while let Some((offset, ch)) = chars.next() { - if ch == ',' { - removal_end = existing_value_range.end + offset + 1; - // Also consume whitespace after the comma - for (_, next_ch) in chars.by_ref() { - if next_ch.is_whitespace() { - removal_end += next_ch.len_utf8(); - } else { - break; - } - } - break; - } else if !ch.is_whitespace() { - break; - } - } - } - (removal_start..removal_end, String::new()) - } - } else { - if let Some(first_key_start) = first_key_start { - // We have key paths, construct the sub objects - let new_key = key_path[depth].as_ref(); - // We don't have the key, construct the nested objects - let new_value = construct_json_value(&key_path[(depth + 1)..], new_value); - - let mut row = 0; - let mut column = 0; - for (ix, char) in text.char_indices() { - if ix == first_key_start { - break; - } - if char == '\n' { - row += 1; - column = 0; - } else { - column += char.len_utf8(); - } - } - - if row > 0 { - // depth is 0 based, but division needs to be 1 based. - let new_val = to_pretty_json(&new_value, column / (depth + 1), column); - let space = ' '; - let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column); - (first_key_start..first_key_start, content) - } else { - let new_val = serde_json::to_string(&new_value).unwrap(); - let mut content = format!(r#""{new_key}": {new_val},"#); - content.push(' '); - (first_key_start..first_key_start, content) - } - } else { - // We don't have the key, construct the nested objects - let new_value = construct_json_value(&key_path[depth..], new_value); - let indent_prefix_len = tab_size * depth; - let mut new_val = to_pretty_json(&new_value, tab_size, indent_prefix_len); - if depth == 0 { - new_val.push('\n'); - } - // best effort to keep comments with best effort indentation - let mut replace_text = &text[existing_value_range.clone()]; - while let Some(comment_start) = replace_text.rfind("//") { - if let Some(comment_end) = replace_text[comment_start..].find('\n') { - let mut comment_with_indent_start = replace_text[..comment_start] - .rfind('\n') - .unwrap_or(comment_start); - if !replace_text[comment_with_indent_start..comment_start] - .trim() - .is_empty() - { - comment_with_indent_start = comment_start; - } - new_val.insert_str( - 1, - &replace_text[comment_with_indent_start..comment_start + comment_end], - ); - } - replace_text = &replace_text[..comment_start]; - } - - (existing_value_range, new_val) - } - } -} - -fn construct_json_value( - key_path: &[impl AsRef], - new_value: Option<&serde_json::Value>, -) -> serde_json::Value { - let mut new_value = - serde_json::to_value(new_value.unwrap_or(&serde_json::Value::Null)).unwrap(); - for key in key_path.iter().rev() { - if parse_index_key(key.as_ref()).is_some() { - new_value = serde_json::json!([new_value]); - } else { - new_value = serde_json::json!({ key.as_ref().to_string(): new_value }); - } - } - return new_value; -} - -fn parse_index_key(index_key: &str) -> Option { - index_key.strip_prefix('#')?.parse().ok() -} - -fn handle_possible_array_value( - key_node: &tree_sitter::Node, - value_node: &tree_sitter::Node, - text: &str, - remaining_key_path: &[impl AsRef], - new_value: Option<&Value>, - replace_key: Option<&str>, - tab_size: usize, -) -> Option<(Range, String)> { - if remaining_key_path.is_empty() { - return None; - } - let key_path = remaining_key_path; - let index = parse_index_key(key_path[0].as_ref())?; - - let value_is_array = value_node.kind() == TS_ARRAY_KIND; - - let array_str = if value_is_array { - &text[value_node.byte_range()] - } else { - "" - }; - - let (mut replace_range, mut replace_value) = replace_top_level_array_value_in_json_text( - array_str, - &key_path[1..], - new_value, - replace_key, - index, - tab_size, - ); - - if value_is_array { - replace_range.start += value_node.start_byte(); - replace_range.end += value_node.start_byte(); - } else { - // replace the full value if it wasn't an array - replace_range = value_node.byte_range(); - } - let non_whitespace_char_count = replace_value.len() - - replace_value - .chars() - .filter(char::is_ascii_whitespace) - .count(); - let needs_indent = replace_value.ends_with('\n') - || (replace_value - .chars() - .zip(replace_value.chars().skip(1)) - .any(|(c, next_c)| c == '\n' && !next_c.is_ascii_whitespace())); - let contains_comment = (replace_value.contains("//") && replace_value.contains('\n')) - || (replace_value.contains("/*") && replace_value.contains("*/")); - if needs_indent { - let indent_width = key_node.start_position().column; - let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width); - replace_value = replace_value.replace('\n', &increased_indent); - } else if non_whitespace_char_count < 32 && !contains_comment { - // remove indentation - while let Some(idx) = replace_value.find("\n ") { - replace_value.remove(idx); - } - while let Some(idx) = replace_value.find(" ") { - replace_value.remove(idx); - } - } - return Some((replace_range, replace_value)); -} - -const TS_DOCUMENT_KIND: &str = "document"; -const TS_ARRAY_KIND: &str = "array"; -const TS_COMMENT_KIND: &str = "comment"; - -pub fn replace_top_level_array_value_in_json_text( - text: &str, - key_path: &[impl AsRef], - new_value: Option<&Value>, - replace_key: Option<&str>, - array_index: usize, - tab_size: usize, -) -> (Range, String) { - let mut parser = tree_sitter::Parser::new(); - parser - .set_language(&tree_sitter_json::LANGUAGE.into()) - .unwrap(); - - let syntax_tree = parser.parse(text, None).unwrap(); - - let mut cursor = syntax_tree.walk(); - - if cursor.node().kind() == TS_DOCUMENT_KIND { - cursor.goto_first_child(); - } - - while cursor.node().kind() != TS_ARRAY_KIND { - if !cursor.goto_next_sibling() { - let json_value = construct_json_value(key_path, new_value); - let json_value = serde_json::json!([json_value]); - return (0..text.len(), to_pretty_json(&json_value, tab_size, 0)); - } - } - - // false if no children - // - cursor.goto_first_child(); - debug_assert_eq!(cursor.node().kind(), "["); - - let mut index = 0; - - while index <= array_index { - let node = cursor.node(); - if !matches!(node.kind(), "[" | "]" | TS_COMMENT_KIND | ",") - && !node.is_extra() - && !node.is_missing() - { - if index == array_index { - break; - } - index += 1; - } - if !cursor.goto_next_sibling() { - if let Some(new_value) = new_value { - return append_top_level_array_value_in_json_text(text, new_value, tab_size); - } else { - return (0..0, String::new()); - } - } - } - - let range = cursor.node().range(); - let indent_width = range.start_point.column; - let offset = range.start_byte; - let text_range = range.start_byte..range.end_byte; - let value_str = &text[text_range.clone()]; - let needs_indent = range.start_point.row > 0; - - if new_value.is_none() && key_path.is_empty() { - let mut remove_range = text_range; - if index == 0 { - while cursor.goto_next_sibling() - && (cursor.node().is_extra() || cursor.node().is_missing()) - {} - if cursor.node().kind() == "," { - remove_range.end = cursor.node().range().end_byte; - } - if let Some(next_newline) = &text[remove_range.end + 1..].find('\n') - && text[remove_range.end + 1..remove_range.end + next_newline] - .chars() - .all(|c| c.is_ascii_whitespace()) - { - remove_range.end = remove_range.end + next_newline; - } - } else { - while cursor.goto_previous_sibling() - && (cursor.node().is_extra() || cursor.node().is_missing()) - {} - if cursor.node().kind() == "," { - remove_range.start = cursor.node().range().start_byte; - } - } - (remove_range, String::new()) - } else { - if let Some(array_replacement) = handle_possible_array_value( - &cursor.node(), - &cursor.node(), - text, - key_path, - new_value, - replace_key, - tab_size, - ) { - return array_replacement; - } - let (mut replace_range, mut replace_value) = - replace_value_in_json_text(value_str, key_path, tab_size, new_value, replace_key); - - replace_range.start += offset; - replace_range.end += offset; - - if needs_indent { - let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width); - replace_value = replace_value.replace('\n', &increased_indent); - } else { - while let Some(idx) = replace_value.find("\n ") { - replace_value.remove(idx + 1); - } - while let Some(idx) = replace_value.find("\n") { - replace_value.replace_range(idx..idx + 1, " "); - } - } - - (replace_range, replace_value) - } -} - -pub fn append_top_level_array_value_in_json_text( - text: &str, - new_value: &Value, - tab_size: usize, -) -> (Range, String) { - let mut parser = tree_sitter::Parser::new(); - parser - .set_language(&tree_sitter_json::LANGUAGE.into()) - .unwrap(); - let syntax_tree = parser.parse(text, None).unwrap(); - - let mut cursor = syntax_tree.walk(); - - if cursor.node().kind() == TS_DOCUMENT_KIND { - cursor.goto_first_child(); - } - - while cursor.node().kind() != TS_ARRAY_KIND { - if !cursor.goto_next_sibling() { - let json_value = serde_json::json!([new_value]); - return (0..text.len(), to_pretty_json(&json_value, tab_size, 0)); - } - } - - let went_to_last_child = cursor.goto_last_child(); - debug_assert!( - went_to_last_child && cursor.node().kind() == "]", - "Malformed JSON syntax tree, expected `]` at end of array" - ); - let close_bracket_start = cursor.node().start_byte(); - while cursor.goto_previous_sibling() - && (cursor.node().is_extra() || cursor.node().is_missing()) - && !cursor.node().is_error() - {} - - let mut comma_range = None; - let mut prev_item_range = None; - - if cursor.node().kind() == "," || is_error_of_kind(&mut cursor, ",") { - comma_range = Some(cursor.node().byte_range()); - while cursor.goto_previous_sibling() - && (cursor.node().is_extra() || cursor.node().is_missing()) - {} - - debug_assert_ne!(cursor.node().kind(), "["); - prev_item_range = Some(cursor.node().range()); - } else { - while (cursor.node().is_extra() || cursor.node().is_missing()) - && cursor.goto_previous_sibling() - {} - if cursor.node().kind() != "[" { - prev_item_range = Some(cursor.node().range()); - } - } - - let (mut replace_range, mut replace_value) = - replace_value_in_json_text::<&str>("", &[], tab_size, Some(new_value), None); - - replace_range.start = close_bracket_start; - replace_range.end = close_bracket_start; - - let space = ' '; - if let Some(prev_item_range) = prev_item_range { - let needs_newline = prev_item_range.start_point.row > 0; - let indent_width = text[..prev_item_range.start_byte].rfind('\n').map_or( - prev_item_range.start_point.column, - |idx| { - prev_item_range.start_point.column - - text[idx + 1..prev_item_range.start_byte].trim_start().len() - }, - ); - - let prev_item_end = comma_range - .as_ref() - .map_or(prev_item_range.end_byte, |range| range.end); - if text[prev_item_end..replace_range.start].trim().is_empty() { - replace_range.start = prev_item_end; - } - - if needs_newline { - let increased_indent = format!("\n{space:width$}", width = indent_width); - replace_value = replace_value.replace('\n', &increased_indent); - replace_value.push('\n'); - replace_value.insert_str(0, &format!("\n{space:width$}", width = indent_width)); - } else { - while let Some(idx) = replace_value.find("\n ") { - replace_value.remove(idx + 1); - } - while let Some(idx) = replace_value.find('\n') { - replace_value.replace_range(idx..idx + 1, " "); - } - replace_value.insert(0, ' '); - } - - if comma_range.is_none() { - replace_value.insert(0, ','); - } - } else if replace_value.contains('\n') || text.contains('\n') { - if let Some(prev_newline) = text[..replace_range.start].rfind('\n') - && text[prev_newline..replace_range.start].trim().is_empty() - { - replace_range.start = prev_newline; - } - let indent = format!("\n{space:width$}", width = tab_size); - replace_value = replace_value.replace('\n', &indent); - replace_value.insert_str(0, &indent); - replace_value.push('\n'); - } - return (replace_range, replace_value); - - fn is_error_of_kind(cursor: &mut tree_sitter::TreeCursor<'_>, kind: &str) -> bool { - if cursor.node().kind() != "ERROR" { - return false; - } - - let descendant_index = cursor.descendant_index(); - let res = cursor.goto_first_child() && cursor.node().kind() == kind; - cursor.goto_descendant(descendant_index); - res - } -} - -/// Infers the indentation size used in JSON text by analyzing the tree structure. -/// Returns the detected indent size, or a default of 2 if no indentation is found. -pub fn infer_json_indent_size(text: &str) -> usize { - const MAX_INDENT_SIZE: usize = 64; - - let mut parser = tree_sitter::Parser::new(); - parser - .set_language(&tree_sitter_json::LANGUAGE.into()) - .unwrap(); - - let Some(syntax_tree) = parser.parse(text, None) else { - return 4; - }; - - let mut cursor = syntax_tree.walk(); - let mut indent_counts = [0u32; MAX_INDENT_SIZE]; - - // Traverse the tree to find indentation patterns - fn visit_node( - cursor: &mut tree_sitter::TreeCursor, - indent_counts: &mut [u32; MAX_INDENT_SIZE], - depth: usize, - ) { - if depth >= 3 { - return; - } - let node = cursor.node(); - let node_kind = node.kind(); - - // For objects and arrays, check the indentation of their first content child - if matches!(node_kind, "object" | "array") { - let container_column = node.start_position().column; - let container_row = node.start_position().row; - - if cursor.goto_first_child() { - // Skip the opening bracket - loop { - let child = cursor.node(); - let child_kind = child.kind(); - - // Look for the first actual content (pair for objects, value for arrays) - if (node_kind == "object" && child_kind == "pair") - || (node_kind == "array" - && !matches!(child_kind, "[" | "]" | "," | "comment")) - { - let child_column = child.start_position().column; - let child_row = child.start_position().row; - - // Only count if the child is on a different line - if child_row > container_row && child_column > container_column { - let indent = child_column - container_column; - if indent > 0 && indent < MAX_INDENT_SIZE { - indent_counts[indent] += 1; - } - } - break; - } - - if !cursor.goto_next_sibling() { - break; - } - } - cursor.goto_parent(); - } - } - - // Recurse to children - if cursor.goto_first_child() { - loop { - visit_node(cursor, indent_counts, depth + 1); - if !cursor.goto_next_sibling() { - break; - } - } - cursor.goto_parent(); - } - } - - visit_node(&mut cursor, &mut indent_counts, 0); - - // Find the indent size with the highest count - let mut max_count = 0; - let mut max_indent = 4; - - for (indent, &count) in indent_counts.iter().enumerate() { - if count > max_count { - max_count = count; - max_indent = indent; - } - } - - if max_count == 0 { 2 } else { max_indent } -} - -pub fn to_pretty_json( - value: &impl Serialize, - indent_size: usize, - indent_prefix_len: usize, -) -> String { - let mut output = Vec::new(); - let indent = " ".repeat(indent_size); - let mut ser = serde_json::Serializer::with_formatter( - &mut output, - serde_json::ser::PrettyFormatter::with_indent(indent.as_bytes()), - ); - - value.serialize(&mut ser).unwrap(); - let text = String::from_utf8(output).unwrap(); - - let mut adjusted_text = String::new(); - for (i, line) in text.split('\n').enumerate() { - if i > 0 { - adjusted_text.extend(std::iter::repeat(' ').take(indent_prefix_len)); - } - adjusted_text.push_str(line); - adjusted_text.push('\n'); - } - adjusted_text.pop(); - adjusted_text -} - -pub fn parse_json_with_comments(content: &str) -> Result { - let mut deserializer = serde_json_lenient::Deserializer::from_str(content); - Ok(serde_path_to_error::deserialize(&mut deserializer)?) -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::{Value, json}; - use unindent::Unindent; - - #[test] - fn object_replace() { - #[track_caller] - fn check_object_replace( - input: String, - key_path: &[&str], - value: Option, - expected: String, - ) { - let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None); - let mut result_str = input; - result_str.replace_range(result.0, &result.1); - pretty_assertions::assert_eq!(expected, result_str); - } - check_object_replace( - r#"{ - "a": 1, - "b": 2 - }"# - .unindent(), - &["b"], - Some(json!(3)), - r#"{ - "a": 1, - "b": 3 - }"# - .unindent(), - ); - check_object_replace( - r#"{ - "a": 1, - "b": 2 - }"# - .unindent(), - &["b"], - None, - r#"{ - "a": 1 - }"# - .unindent(), - ); - check_object_replace( - r#"{ - "a": 1, - "b": 2 - }"# - .unindent(), - &["c"], - Some(json!(3)), - r#"{ - "c": 3, - "a": 1, - "b": 2 - }"# - .unindent(), - ); - check_object_replace( - r#"{ - "a": 1, - "b": { - "c": 2, - "d": 3, - } - }"# - .unindent(), - &["b", "c"], - Some(json!([1, 2, 3])), - r#"{ - "a": 1, - "b": { - "c": [ - 1, - 2, - 3 - ], - "d": 3, - } - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - "name": "old_name", - "id": 123 - }"# - .unindent(), - &["name"], - Some(json!("new_name")), - r#"{ - "name": "new_name", - "id": 123 - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - "enabled": false, - "count": 5 - }"# - .unindent(), - &["enabled"], - Some(json!(true)), - r#"{ - "enabled": true, - "count": 5 - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - "value": null, - "other": "test" - }"# - .unindent(), - &["value"], - Some(json!(42)), - r#"{ - "value": 42, - "other": "test" - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - "config": { - "old": true - }, - "name": "test" - }"# - .unindent(), - &["config"], - Some(json!({"new": false, "count": 3})), - r#"{ - "config": { - "new": false, - "count": 3 - }, - "name": "test" - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - // This is a comment - "a": 1, - "b": 2 // Another comment - }"# - .unindent(), - &["b"], - Some(json!({"foo": "bar"})), - r#"{ - // This is a comment - "a": 1, - "b": { - "foo": "bar" - } // Another comment - }"# - .unindent(), - ); - - check_object_replace( - r#"{}"#.to_string(), - &["new_key"], - Some(json!("value")), - r#"{ - "new_key": "value" - } - "# - .unindent(), - ); - - check_object_replace( - r#"{ - "only_key": 123 - }"# - .unindent(), - &["only_key"], - None, - "{\n \n}".to_string(), - ); - - check_object_replace( - r#"{ - "level1": { - "level2": { - "level3": { - "target": "old" - } - } - } - }"# - .unindent(), - &["level1", "level2", "level3", "target"], - Some(json!("new")), - r#"{ - "level1": { - "level2": { - "level3": { - "target": "new" - } - } - } - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - "parent": {} - }"# - .unindent(), - &["parent", "child"], - Some(json!("value")), - r#"{ - "parent": { - "child": "value" - } - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - "a": 1, - "b": 2, - }"# - .unindent(), - &["b"], - Some(json!(3)), - r#"{ - "a": 1, - "b": 3, - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - "items": [1, 2, 3], - "count": 3 - }"# - .unindent(), - &["items", "1"], - Some(json!(5)), - r#"{ - "items": { - "1": 5 - }, - "count": 3 - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - "items": [1, 2, 3], - "count": 3 - }"# - .unindent(), - &["items", "1"], - None, - r#"{ - "items": { - "1": null - }, - "count": 3 - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - "items": [1, 2, 3], - "count": 3 - }"# - .unindent(), - &["items"], - Some(json!(["a", "b", "c", "d"])), - r#"{ - "items": [ - "a", - "b", - "c", - "d" - ], - "count": 3 - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - "0": "zero", - "1": "one" - }"# - .unindent(), - &["1"], - Some(json!("ONE")), - r#"{ - "0": "zero", - "1": "ONE" - }"# - .unindent(), - ); - // Test with comments between object members - check_object_replace( - r#"{ - "a": 1, - // Comment between members - "b": 2, - /* Block comment */ - "c": 3 - }"# - .unindent(), - &["b"], - Some(json!({"nested": true})), - r#"{ - "a": 1, - // Comment between members - "b": { - "nested": true - }, - /* Block comment */ - "c": 3 - }"# - .unindent(), - ); - - // Test with trailing comments on replaced value - check_object_replace( - r#"{ - "a": 1, // keep this comment - "b": 2 // this should stay - }"# - .unindent(), - &["a"], - Some(json!("changed")), - r#"{ - "a": "changed", // keep this comment - "b": 2 // this should stay - }"# - .unindent(), - ); - - // Test with deep indentation - check_object_replace( - r#"{ - "deeply": { - "nested": { - "value": "old" - } - } - }"# - .unindent(), - &["deeply", "nested", "value"], - Some(json!("new")), - r#"{ - "deeply": { - "nested": { - "value": "new" - } - } - }"# - .unindent(), - ); - - // Test removing value with comment preservation - check_object_replace( - r#"{ - // Header comment - "a": 1, - // This comment belongs to b - "b": 2, - // This comment belongs to c - "c": 3 - }"# - .unindent(), - &["b"], - None, - r#"{ - // Header comment - "a": 1, - // This comment belongs to b - // This comment belongs to c - "c": 3 - }"# - .unindent(), - ); - - // Test with multiline block comments - check_object_replace( - r#"{ - /* - * This is a multiline - * block comment - */ - "value": "old", - /* Another block */ "other": 123 - }"# - .unindent(), - &["value"], - Some(json!("new")), - r#"{ - /* - * This is a multiline - * block comment - */ - "value": "new", - /* Another block */ "other": 123 - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - // This object is empty - }"# - .unindent(), - &["key"], - Some(json!("value")), - r#"{ - // This object is empty - "key": "value" - } - "# - .unindent(), - ); - - // Test replacing in object with only comments - check_object_replace( - r#"{ - // Comment 1 - // Comment 2 - }"# - .unindent(), - &["new"], - Some(json!(42)), - r#"{ - // Comment 1 - // Comment 2 - "new": 42 - } - "# - .unindent(), - ); - - // Test with inconsistent spacing - check_object_replace( - r#"{ - "a":1, - "b" : 2 , - "c": 3 - }"# - .unindent(), - &["b"], - Some(json!("spaced")), - r#"{ - "a":1, - "b" : "spaced" , - "c": 3 - }"# - .unindent(), - ); - } - - #[test] - fn object_replace_array() { - // Tests replacing values within arrays that are nested inside objects. - // Uses "#N" syntax in key paths to indicate array indices. - #[track_caller] - fn check_object_replace_array( - input: String, - key_path: &[&str], - value: Option, - expected: String, - ) { - let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None); - let mut result_str = input; - result_str.replace_range(result.0, &result.1); - pretty_assertions::assert_eq!(expected, result_str); - } - - // Basic array element replacement - check_object_replace_array( - r#"{ - "a": [1, 3], - }"# - .unindent(), - &["a", "#1"], - Some(json!(2)), - r#"{ - "a": [1, 2], - }"# - .unindent(), - ); - - // Replace first element - check_object_replace_array( - r#"{ - "items": [1, 2, 3] - }"# - .unindent(), - &["items", "#0"], - Some(json!(10)), - r#"{ - "items": [10, 2, 3] - }"# - .unindent(), - ); - - // Replace last element - check_object_replace_array( - r#"{ - "items": [1, 2, 3] - }"# - .unindent(), - &["items", "#2"], - Some(json!(30)), - r#"{ - "items": [1, 2, 30] - }"# - .unindent(), - ); - - // Replace string in array - check_object_replace_array( - r#"{ - "names": ["alice", "bob", "charlie"] - }"# - .unindent(), - &["names", "#1"], - Some(json!("robert")), - r#"{ - "names": ["alice", "robert", "charlie"] - }"# - .unindent(), - ); - - // Replace boolean - check_object_replace_array( - r#"{ - "flags": [true, false, true] - }"# - .unindent(), - &["flags", "#0"], - Some(json!(false)), - r#"{ - "flags": [false, false, true] - }"# - .unindent(), - ); - - // Replace null with value - check_object_replace_array( - r#"{ - "values": [null, 2, null] - }"# - .unindent(), - &["values", "#0"], - Some(json!(1)), - r#"{ - "values": [1, 2, null] - }"# - .unindent(), - ); - - // Replace value with null - check_object_replace_array( - r#"{ - "data": [1, 2, 3] - }"# - .unindent(), - &["data", "#1"], - Some(json!(null)), - r#"{ - "data": [1, null, 3] - }"# - .unindent(), - ); - - // Replace simple value with object - check_object_replace_array( - r#"{ - "list": [1, 2, 3] - }"# - .unindent(), - &["list", "#1"], - Some(json!({"value": 2, "label": "two"})), - r#"{ - "list": [1, { "value": 2, "label": "two" }, 3] - }"# - .unindent(), - ); - - // Replace simple value with nested array - check_object_replace_array( - r#"{ - "matrix": [1, 2, 3] - }"# - .unindent(), - &["matrix", "#1"], - Some(json!([20, 21, 22])), - r#"{ - "matrix": [1, [ 20, 21, 22 ], 3] - }"# - .unindent(), - ); - - // Replace object in array - check_object_replace_array( - r#"{ - "users": [ - {"name": "alice"}, - {"name": "bob"}, - {"name": "charlie"} - ] - }"# - .unindent(), - &["users", "#1"], - Some(json!({"name": "robert", "age": 30})), - r#"{ - "users": [ - {"name": "alice"}, - { "name": "robert", "age": 30 }, - {"name": "charlie"} - ] - }"# - .unindent(), - ); - - // Replace property within object in array - check_object_replace_array( - r#"{ - "users": [ - {"name": "alice", "age": 25}, - {"name": "bob", "age": 30}, - {"name": "charlie", "age": 35} - ] - }"# - .unindent(), - &["users", "#1", "age"], - Some(json!(31)), - r#"{ - "users": [ - {"name": "alice", "age": 25}, - {"name": "bob", "age": 31}, - {"name": "charlie", "age": 35} - ] - }"# - .unindent(), - ); - - // Add new property to object in array - check_object_replace_array( - r#"{ - "items": [ - {"id": 1}, - {"id": 2}, - {"id": 3} - ] - }"# - .unindent(), - &["items", "#1", "name"], - Some(json!("Item Two")), - r#"{ - "items": [ - {"id": 1}, - {"name": "Item Two", "id": 2}, - {"id": 3} - ] - }"# - .unindent(), - ); - - // Remove property from object in array - check_object_replace_array( - r#"{ - "items": [ - {"id": 1, "name": "one"}, - {"id": 2, "name": "two"}, - {"id": 3, "name": "three"} - ] - }"# - .unindent(), - &["items", "#1", "name"], - None, - r#"{ - "items": [ - {"id": 1, "name": "one"}, - {"id": 2}, - {"id": 3, "name": "three"} - ] - }"# - .unindent(), - ); - - // Deeply nested: array in object in array - check_object_replace_array( - r#"{ - "data": [ - { - "values": [1, 2, 3] - }, - { - "values": [4, 5, 6] - } - ] - }"# - .unindent(), - &["data", "#0", "values", "#1"], - Some(json!(20)), - r#"{ - "data": [ - { - "values": [1, 20, 3] - }, - { - "values": [4, 5, 6] - } - ] - }"# - .unindent(), - ); - - // Multiple levels of nesting - check_object_replace_array( - r#"{ - "root": { - "level1": [ - { - "level2": { - "level3": [10, 20, 30] - } - } - ] - } - }"# - .unindent(), - &["root", "level1", "#0", "level2", "level3", "#2"], - Some(json!(300)), - r#"{ - "root": { - "level1": [ - { - "level2": { - "level3": [10, 20, 300] - } - } - ] - } - }"# - .unindent(), - ); - - // Array with mixed types - check_object_replace_array( - r#"{ - "mixed": [1, "two", true, null, {"five": 5}] - }"# - .unindent(), - &["mixed", "#3"], - Some(json!({"four": 4})), - r#"{ - "mixed": [1, "two", true, { "four": 4 }, {"five": 5}] - }"# - .unindent(), - ); - - // Replace with complex object - check_object_replace_array( - r#"{ - "config": [ - "simple", - "values" - ] - }"# - .unindent(), - &["config", "#0"], - Some(json!({ - "type": "complex", - "settings": { - "enabled": true, - "level": 5 - } - })), - r#"{ - "config": [ - { - "type": "complex", - "settings": { - "enabled": true, - "level": 5 - } - }, - "values" - ] - }"# - .unindent(), - ); - - // Array with trailing comma - check_object_replace_array( - r#"{ - "items": [ - 1, - 2, - 3, - ] - }"# - .unindent(), - &["items", "#1"], - Some(json!(20)), - r#"{ - "items": [ - 1, - 20, - 3, - ] - }"# - .unindent(), - ); - - // Array with comments - check_object_replace_array( - r#"{ - "items": [ - 1, // first item - 2, // second item - 3 // third item - ] - }"# - .unindent(), - &["items", "#1"], - Some(json!(20)), - r#"{ - "items": [ - 1, // first item - 20, // second item - 3 // third item - ] - }"# - .unindent(), - ); - - // Multiple arrays in object - check_object_replace_array( - r#"{ - "first": [1, 2, 3], - "second": [4, 5, 6], - "third": [7, 8, 9] - }"# - .unindent(), - &["second", "#1"], - Some(json!(50)), - r#"{ - "first": [1, 2, 3], - "second": [4, 50, 6], - "third": [7, 8, 9] - }"# - .unindent(), - ); - - // Empty array - add first element - check_object_replace_array( - r#"{ - "empty": [] - }"# - .unindent(), - &["empty", "#0"], - Some(json!("first")), - r#"{ - "empty": ["first"] - }"# - .unindent(), - ); - - // Array of arrays - check_object_replace_array( - r#"{ - "matrix": [ - [1, 2], - [3, 4], - [5, 6] - ] - }"# - .unindent(), - &["matrix", "#1", "#0"], - Some(json!(30)), - r#"{ - "matrix": [ - [1, 2], - [30, 4], - [5, 6] - ] - }"# - .unindent(), - ); - - // Replace nested object property in array element - check_object_replace_array( - r#"{ - "users": [ - { - "name": "alice", - "address": { - "city": "NYC", - "zip": "10001" - } - } - ] - }"# - .unindent(), - &["users", "#0", "address", "city"], - Some(json!("Boston")), - r#"{ - "users": [ - { - "name": "alice", - "address": { - "city": "Boston", - "zip": "10001" - } - } - ] - }"# - .unindent(), - ); - - // Add element past end of array - check_object_replace_array( - r#"{ - "items": [1, 2] - }"# - .unindent(), - &["items", "#5"], - Some(json!(6)), - r#"{ - "items": [1, 2, 6] - }"# - .unindent(), - ); - - // Complex nested structure - check_object_replace_array( - r#"{ - "app": { - "modules": [ - { - "name": "auth", - "routes": [ - {"path": "/login", "method": "POST"}, - {"path": "/logout", "method": "POST"} - ] - }, - { - "name": "api", - "routes": [ - {"path": "/users", "method": "GET"}, - {"path": "/users", "method": "POST"} - ] - } - ] - } - }"# - .unindent(), - &["app", "modules", "#1", "routes", "#0", "method"], - Some(json!("PUT")), - r#"{ - "app": { - "modules": [ - { - "name": "auth", - "routes": [ - {"path": "/login", "method": "POST"}, - {"path": "/logout", "method": "POST"} - ] - }, - { - "name": "api", - "routes": [ - {"path": "/users", "method": "PUT"}, - {"path": "/users", "method": "POST"} - ] - } - ] - } - }"# - .unindent(), - ); - - // Escaped strings in array - check_object_replace_array( - r#"{ - "messages": ["hello", "world"] - }"# - .unindent(), - &["messages", "#0"], - Some(json!("hello \"quoted\" world")), - r#"{ - "messages": ["hello \"quoted\" world", "world"] - }"# - .unindent(), - ); - - // Block comments - check_object_replace_array( - r#"{ - "data": [ - /* first */ 1, - /* second */ 2, - /* third */ 3 - ] - }"# - .unindent(), - &["data", "#1"], - Some(json!(20)), - r#"{ - "data": [ - /* first */ 1, - /* second */ 20, - /* third */ 3 - ] - }"# - .unindent(), - ); - - // Inline array - check_object_replace_array( - r#"{"items": [1, 2, 3], "count": 3}"#.to_string(), - &["items", "#1"], - Some(json!(20)), - r#"{"items": [1, 20, 3], "count": 3}"#.to_string(), - ); - - // Single element array - check_object_replace_array( - r#"{ - "single": [42] - }"# - .unindent(), - &["single", "#0"], - Some(json!(100)), - r#"{ - "single": [100] - }"# - .unindent(), - ); - - // Inconsistent formatting - check_object_replace_array( - r#"{ - "messy": [1, - 2, - 3, - 4] - }"# - .unindent(), - &["messy", "#2"], - Some(json!(30)), - r#"{ - "messy": [1, - 2, - 30, - 4] - }"# - .unindent(), - ); - - // Creates array if has numbered key - check_object_replace_array( - r#"{ - "array": {"foo": "bar"} - }"# - .unindent(), - &["array", "#3"], - Some(json!(4)), - r#"{ - "array": [ - 4 - ] - }"# - .unindent(), - ); - - // Replace non-array element within array with array - check_object_replace_array( - r#"{ - "matrix": [ - [1, 2], - [3, 4], - [5, 6] - ] - }"# - .unindent(), - &["matrix", "#1", "#0"], - Some(json!(["foo", "bar"])), - r#"{ - "matrix": [ - [1, 2], - [[ "foo", "bar" ], 4], - [5, 6] - ] - }"# - .unindent(), - ); - // Replace non-array element within array with array - check_object_replace_array( - r#"{ - "matrix": [ - [1, 2], - [3, 4], - [5, 6] - ] - }"# - .unindent(), - &["matrix", "#1", "#0", "#3"], - Some(json!(["foo", "bar"])), - r#"{ - "matrix": [ - [1, 2], - [[ [ "foo", "bar" ] ], 4], - [5, 6] - ] - }"# - .unindent(), - ); - - // Create array in key that doesn't exist - check_object_replace_array( - r#"{ - "foo": {} - }"# - .unindent(), - &["foo", "bar", "#0"], - Some(json!({"is_object": true})), - r#"{ - "foo": { - "bar": [ - { - "is_object": true - } - ] - } - }"# - .unindent(), - ); - } - - #[test] - fn array_replace() { - #[track_caller] - fn check_array_replace( - input: impl ToString, - index: usize, - key_path: &[&str], - value: Option, - expected: impl ToString, - ) { - let input = input.to_string(); - let result = replace_top_level_array_value_in_json_text( - &input, - key_path, - value.as_ref(), - None, - index, - 4, - ); - let mut result_str = input; - result_str.replace_range(result.0, &result.1); - pretty_assertions::assert_eq!(expected.to_string(), result_str); - } - - check_array_replace(r#"[1, 3, 3]"#, 1, &[], Some(json!(2)), r#"[1, 2, 3]"#); - check_array_replace(r#"[1, 3, 3]"#, 2, &[], Some(json!(2)), r#"[1, 3, 2]"#); - check_array_replace(r#"[1, 3, 3,]"#, 3, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#); - check_array_replace(r#"[1, 3, 3,]"#, 100, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#); - check_array_replace( - r#"[ - 1, - 2, - 3, - ]"# - .unindent(), - 1, - &[], - Some(json!({"foo": "bar", "baz": "qux"})), - r#"[ - 1, - { - "foo": "bar", - "baz": "qux" - }, - 3, - ]"# - .unindent(), - ); - check_array_replace( - r#"[1, 3, 3,]"#, - 1, - &[], - Some(json!({"foo": "bar", "baz": "qux"})), - r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#, - ); - - check_array_replace( - r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#, - 1, - &["baz"], - Some(json!({"qux": "quz"})), - r#"[1, { "foo": "bar", "baz": { "qux": "quz" } }, 3,]"#, - ); - - check_array_replace( - r#"[ - 1, - { - "foo": "bar", - "baz": "qux" - }, - 3 - ]"#, - 1, - &["baz"], - Some(json!({"qux": "quz"})), - r#"[ - 1, - { - "foo": "bar", - "baz": { - "qux": "quz" - } - }, - 3 - ]"#, - ); - - check_array_replace( - r#"[ - 1, - { - "foo": "bar", - "baz": { - "qux": "quz" - } - }, - 3 - ]"#, - 1, - &["baz"], - Some(json!("qux")), - r#"[ - 1, - { - "foo": "bar", - "baz": "qux" - }, - 3 - ]"#, - ); - - check_array_replace( - r#"[ - 1, - { - "foo": "bar", - // some comment to keep - "baz": { - // some comment to remove - "qux": "quz" - } - // some other comment to keep - }, - 3 - ]"#, - 1, - &["baz"], - Some(json!("qux")), - r#"[ - 1, - { - "foo": "bar", - // some comment to keep - "baz": "qux" - // some other comment to keep - }, - 3 - ]"#, - ); - - // Test with comments between array elements - check_array_replace( - r#"[ - 1, - // This is element 2 - 2, - /* Block comment */ 3, - 4 // Trailing comment - ]"#, - 2, - &[], - Some(json!("replaced")), - r#"[ - 1, - // This is element 2 - 2, - /* Block comment */ "replaced", - 4 // Trailing comment - ]"#, - ); - - // Test empty array with comments - check_array_replace( - r#"[ - // Empty array with comment - ]"# - .unindent(), - 0, - &[], - Some(json!("first")), - r#"[ - // Empty array with comment - "first" - ]"# - .unindent(), - ); - check_array_replace( - r#"[]"#.unindent(), - 0, - &[], - Some(json!("first")), - r#"["first"]"#.unindent(), - ); - - // Test array with leading comments - check_array_replace( - r#"[ - // Leading comment - // Another leading comment - 1, - 2 - ]"#, - 0, - &[], - Some(json!({"new": "object"})), - r#"[ - // Leading comment - // Another leading comment - { - "new": "object" - }, - 2 - ]"#, - ); - - // Test with deep indentation - check_array_replace( - r#"[ - 1, - 2, - 3 - ]"#, - 1, - &[], - Some(json!("deep")), - r#"[ - 1, - "deep", - 3 - ]"#, - ); - - // Test with mixed spacing - check_array_replace( - r#"[1,2, 3, 4]"#, - 2, - &[], - Some(json!("spaced")), - r#"[1,2, "spaced", 4]"#, - ); - - // Test replacing nested array element - check_array_replace( - r#"[ - [1, 2, 3], - [4, 5, 6], - [7, 8, 9] - ]"#, - 1, - &[], - Some(json!(["a", "b", "c", "d"])), - r#"[ - [1, 2, 3], - [ - "a", - "b", - "c", - "d" - ], - [7, 8, 9] - ]"#, - ); - - // Test with multiline block comments - check_array_replace( - r#"[ - /* - * This is a - * multiline comment - */ - "first", - "second" - ]"#, - 0, - &[], - Some(json!("updated")), - r#"[ - /* - * This is a - * multiline comment - */ - "updated", - "second" - ]"#, - ); - - // Test replacing with null - check_array_replace( - r#"[true, false, true]"#, - 1, - &[], - Some(json!(null)), - r#"[true, null, true]"#, - ); - - // Test single element array - check_array_replace( - r#"[42]"#, - 0, - &[], - Some(json!({"answer": 42})), - r#"[{ "answer": 42 }]"#, - ); - - // Test array with only comments - check_array_replace( - r#"[ - // Comment 1 - // Comment 2 - // Comment 3 - ]"# - .unindent(), - 10, - &[], - Some(json!(123)), - r#"[ - // Comment 1 - // Comment 2 - // Comment 3 - 123 - ]"# - .unindent(), - ); - - check_array_replace( - r#"[ - { - "key": "value" - }, - { - "key": "value2" - } - ]"# - .unindent(), - 0, - &[], - None, - r#"[ - { - "key": "value2" - } - ]"# - .unindent(), - ); - - check_array_replace( - r#"[ - { - "key": "value" - }, - { - "key": "value2" - }, - { - "key": "value3" - }, - ]"# - .unindent(), - 1, - &[], - None, - r#"[ - { - "key": "value" - }, - { - "key": "value3" - }, - ]"# - .unindent(), - ); - - check_array_replace( - r#""#, - 2, - &[], - Some(json!(42)), - r#"[ - 42 - ]"# - .unindent(), - ); - - check_array_replace( - r#""#, - 2, - &["foo", "bar"], - Some(json!(42)), - r#"[ - { - "foo": { - "bar": 42 - } - } - ]"# - .unindent(), - ); - } - - #[test] - fn array_append() { - #[track_caller] - fn check_array_append(input: impl ToString, value: Value, expected: impl ToString) { - let input = input.to_string(); - let result = append_top_level_array_value_in_json_text(&input, &value, 4); - let mut result_str = input; - result_str.replace_range(result.0, &result.1); - pretty_assertions::assert_eq!(expected.to_string(), result_str); - } - check_array_append(r#"[1, 3, 3]"#, json!(4), r#"[1, 3, 3, 4]"#); - check_array_append(r#"[1, 3, 3,]"#, json!(4), r#"[1, 3, 3, 4]"#); - check_array_append(r#"[1, 3, 3 ]"#, json!(4), r#"[1, 3, 3, 4]"#); - check_array_append(r#"[1, 3, 3, ]"#, json!(4), r#"[1, 3, 3, 4]"#); - check_array_append( - r#"[ - 1, - 2, - 3 - ]"# - .unindent(), - json!(4), - r#"[ - 1, - 2, - 3, - 4 - ]"# - .unindent(), - ); - check_array_append( - r#"[ - 1, - 2, - 3, - ]"# - .unindent(), - json!(4), - r#"[ - 1, - 2, - 3, - 4 - ]"# - .unindent(), - ); - check_array_append( - r#"[ - 1, - 2, - 3, - ]"# - .unindent(), - json!({"foo": "bar", "baz": "qux"}), - r#"[ - 1, - 2, - 3, - { - "foo": "bar", - "baz": "qux" - } - ]"# - .unindent(), - ); - check_array_append( - r#"[ 1, 2, 3, ]"#.unindent(), - json!({"foo": "bar", "baz": "qux"}), - r#"[ 1, 2, 3, { "foo": "bar", "baz": "qux" }]"#.unindent(), - ); - check_array_append( - r#"[]"#, - json!({"foo": "bar"}), - r#"[ - { - "foo": "bar" - } - ]"# - .unindent(), - ); - - // Test with comments between array elements - check_array_append( - r#"[ - 1, - // Comment between elements - 2, - /* Block comment */ 3 - ]"# - .unindent(), - json!(4), - r#"[ - 1, - // Comment between elements - 2, - /* Block comment */ 3, - 4 - ]"# - .unindent(), - ); - - // Test with trailing comment on last element - check_array_append( - r#"[ - 1, - 2, - 3 // Trailing comment - ]"# - .unindent(), - json!("new"), - r#"[ - 1, - 2, - 3 // Trailing comment - , - "new" - ]"# - .unindent(), - ); - - // Test empty array with comments - check_array_append( - r#"[ - // Empty array with comment - ]"# - .unindent(), - json!("first"), - r#"[ - // Empty array with comment - "first" - ]"# - .unindent(), - ); - - // Test with multiline block comment at end - check_array_append( - r#"[ - 1, - 2 - /* - * This is a - * multiline comment - */ - ]"# - .unindent(), - json!(3), - r#"[ - 1, - 2 - /* - * This is a - * multiline comment - */ - , - 3 - ]"# - .unindent(), - ); - - // Test with deep indentation - check_array_append( - r#"[ - 1, - 2, - 3 - ]"# - .unindent(), - json!("deep"), - r#"[ - 1, - 2, - 3, - "deep" - ]"# - .unindent(), - ); - - // Test with no spacing - check_array_append(r#"[1,2,3]"#, json!(4), r#"[1,2,3, 4]"#); - - // Test appending complex nested structure - check_array_append( - r#"[ - {"a": 1}, - {"b": 2} - ]"# - .unindent(), - json!({"c": {"nested": [1, 2, 3]}}), - r#"[ - {"a": 1}, - {"b": 2}, - { - "c": { - "nested": [ - 1, - 2, - 3 - ] - } - } - ]"# - .unindent(), - ); - - // Test array ending with comment after bracket - check_array_append( - r#"[ - 1, - 2, - 3 - ] // Comment after array"# - .unindent(), - json!(4), - r#"[ - 1, - 2, - 3, - 4 - ] // Comment after array"# - .unindent(), - ); - - // Test with inconsistent element formatting - check_array_append( - r#"[1, - 2, - 3, - ]"# - .unindent(), - json!(4), - r#"[1, - 2, - 3, - 4 - ]"# - .unindent(), - ); - - // Test appending to single-line array with trailing comma - check_array_append( - r#"[1, 2, 3,]"#, - json!({"key": "value"}), - r#"[1, 2, 3, { "key": "value" }]"#, - ); - - // Test appending null value - check_array_append(r#"[true, false]"#, json!(null), r#"[true, false, null]"#); - - // Test appending to array with only comments - check_array_append( - r#"[ - // Just comments here - // More comments - ]"# - .unindent(), - json!(42), - r#"[ - // Just comments here - // More comments - 42 - ]"# - .unindent(), - ); - - check_array_append( - r#""#, - json!(42), - r#"[ - 42 - ]"# - .unindent(), - ) - } - - #[test] - fn test_infer_json_indent_size() { - let json_2_spaces = r#"{ - "key1": "value1", - "nested": { - "key2": "value2", - "array": [ - 1, - 2, - 3 - ] - } -}"#; - assert_eq!(infer_json_indent_size(json_2_spaces), 2); - - let json_4_spaces = r#"{ - "key1": "value1", - "nested": { - "key2": "value2", - "array": [ - 1, - 2, - 3 - ] - } -}"#; - assert_eq!(infer_json_indent_size(json_4_spaces), 4); - - let json_8_spaces = r#"{ - "key1": "value1", - "nested": { - "key2": "value2" - } -}"#; - assert_eq!(infer_json_indent_size(json_8_spaces), 8); - - let json_single_line = r#"{"key": "value", "nested": {"inner": "data"}}"#; - assert_eq!(infer_json_indent_size(json_single_line), 2); - - let json_empty = r#"{}"#; - assert_eq!(infer_json_indent_size(json_empty), 2); - - let json_array = r#"[ - { - "id": 1, - "name": "first" - }, - { - "id": 2, - "name": "second" - } -]"#; - assert_eq!(infer_json_indent_size(json_array), 2); - - let json_mixed = r#"{ - "a": { - "b": { - "c": "value" - } - }, - "d": "value2" -}"#; - assert_eq!(infer_json_indent_size(json_mixed), 2); - } -} diff --git a/crates/settings_macros/Cargo.toml b/crates/settings_macros/Cargo.toml deleted file mode 100644 index 175c2f26a3..0000000000 --- a/crates/settings_macros/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "settings_macros" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lib] -path = "src/settings_macros.rs" -proc-macro = true - -[lints] -workspace = true - -[features] -default = [] - -[dependencies] -quote.workspace = true -syn.workspace = true - -[dev-dependencies] -settings.workspace = true diff --git a/crates/settings_macros/LICENSE-GPL b/crates/settings_macros/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/settings_macros/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/settings_macros/src/settings_macros.rs b/crates/settings_macros/src/settings_macros.rs deleted file mode 100644 index bad786991d..0000000000 --- a/crates/settings_macros/src/settings_macros.rs +++ /dev/null @@ -1,152 +0,0 @@ -use proc_macro::TokenStream; - -use quote::quote; -use syn::{ - Data, DeriveInput, Field, Fields, ItemEnum, ItemStruct, Type, parse_macro_input, parse_quote, -}; - -/// Derives the `MergeFrom` trait for a struct. -/// -/// This macro automatically implements `MergeFrom` by calling `merge_from` -/// on all fields in the struct. -/// -/// # Example -/// -/// ```ignore -/// #[derive(Clone, MergeFrom)] -/// struct MySettings { -/// field1: Option, -/// field2: SomeOtherSettings, -/// } -/// ``` -#[proc_macro_derive(MergeFrom)] -pub fn derive_merge_from(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - - let name = &input.ident; - let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - - let merge_body = match &input.data { - Data::Struct(data_struct) => match &data_struct.fields { - Fields::Named(fields) => { - let field_merges = fields.named.iter().map(|field| { - let field_name = &field.ident; - quote! { - self.#field_name.merge_from(&other.#field_name); - } - }); - - quote! { - #(#field_merges)* - } - } - Fields::Unnamed(fields) => { - let field_merges = fields.unnamed.iter().enumerate().map(|(i, _)| { - let field_index = syn::Index::from(i); - quote! { - self.#field_index.merge_from(&other.#field_index); - } - }); - - quote! { - #(#field_merges)* - } - } - Fields::Unit => { - quote! { - // No fields to merge for unit structs - } - } - }, - Data::Enum(_) => { - quote! { - *self = other.clone(); - } - } - Data::Union(_) => { - panic!("MergeFrom cannot be derived for unions"); - } - }; - - let expanded = quote! { - impl #impl_generics crate::merge_from::MergeFrom for #name #ty_generics #where_clause { - fn merge_from(&mut self, other: &Self) { - use crate::merge_from::MergeFrom as _; - #merge_body - } - } - }; - - TokenStream::from(expanded) -} - -/// Registers the setting type with the SettingsStore. Note that you need to -/// have `gpui` in your dependencies for this to work. -#[proc_macro_derive(RegisterSetting)] -pub fn derive_register_setting(input: TokenStream) -> TokenStream { - let input = syn::parse_macro_input!(input as DeriveInput); - let type_name = &input.ident; - - quote! { - settings::private::inventory::submit! { - settings::private::RegisteredSetting { - settings_value: || { - Box::new(settings::private::SettingValue::<#type_name> { - global_value: None, - local_values: Vec::new(), - }) - }, - from_settings: |content| Box::new(<#type_name as settings::Settings>::from_settings(content)), - id: || std::any::TypeId::of::<#type_name>(), - } - } - } - .into() -} - -// Adds serde attributes to each field with type Option: -// #serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "settings::deserialize_fallible") -#[proc_macro_attribute] -pub fn with_fallible_options(_args: TokenStream, input: TokenStream) -> TokenStream { - fn apply_on_fields(fields: &mut Fields) { - match fields { - Fields::Unit => {} - Fields::Named(fields) => { - for field in &mut fields.named { - add_if_option(field) - } - } - Fields::Unnamed(fields) => { - for field in &mut fields.unnamed { - add_if_option(field) - } - } - } - } - - fn add_if_option(field: &mut Field) { - match &field.ty { - Type::Path(syn::TypePath { qself: None, path }) - if path.leading_colon.is_none() - && path.segments.len() == 1 - && path.segments[0].ident == "Option" => {} - _ => return, - } - let attr = parse_quote!( - #[serde(default, skip_serializing_if = "Option::is_none", deserialize_with="crate::fallible_options::deserialize")] - ); - field.attrs.push(attr); - } - - if let Ok(mut input) = syn::parse::(input.clone()) { - apply_on_fields(&mut input.fields); - quote!(#input).into() - } else if let Ok(mut input) = syn::parse::(input) { - for variant in &mut input.variants { - apply_on_fields(&mut variant.fields); - } - quote!(#input).into() - } else { - panic!("with_fallible_options can only be applied to struct or enum definitions."); - } -} diff --git a/crates/settings_profile_selector/Cargo.toml b/crates/settings_profile_selector/Cargo.toml deleted file mode 100644 index 23ccac2e43..0000000000 --- a/crates/settings_profile_selector/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "settings_profile_selector" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/settings_profile_selector.rs" -doctest = false - -[dependencies] -fuzzy.workspace = true -gpui.workspace = true -picker.workspace = true -settings.workspace = true -ui.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -client = { workspace = true, features = ["test-support"] } -editor = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -menu.workspace = true -project = { workspace = true, features = ["test-support"] } -serde_json.workspace = true -settings = { workspace = true, features = ["test-support"] } -theme = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/settings_profile_selector/LICENSE-GPL b/crates/settings_profile_selector/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/settings_profile_selector/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/settings_profile_selector/src/settings_profile_selector.rs b/crates/settings_profile_selector/src/settings_profile_selector.rs deleted file mode 100644 index 42d714283a..0000000000 --- a/crates/settings_profile_selector/src/settings_profile_selector.rs +++ /dev/null @@ -1,612 +0,0 @@ -use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; -use gpui::{ - App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, Task, WeakEntity, Window, -}; -use picker::{Picker, PickerDelegate}; -use settings::{ActiveSettingsProfileName, SettingsStore}; -use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*}; -use workspace::{ModalView, Workspace}; - -pub fn init(cx: &mut App) { - cx.on_action(|_: &zed_actions::settings_profile_selector::Toggle, cx| { - workspace::with_active_or_new_workspace(cx, |workspace, window, cx| { - toggle_settings_profile_selector(workspace, window, cx); - }); - }); -} - -fn toggle_settings_profile_selector( - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, -) { - workspace.toggle_modal(window, cx, |window, cx| { - let delegate = SettingsProfileSelectorDelegate::new(cx.entity().downgrade(), window, cx); - SettingsProfileSelector::new(delegate, window, cx) - }); -} - -pub struct SettingsProfileSelector { - picker: Entity>, -} - -impl ModalView for SettingsProfileSelector {} - -impl EventEmitter for SettingsProfileSelector {} - -impl Focusable for SettingsProfileSelector { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl Render for SettingsProfileSelector { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - v_flex().w(rems(22.)).child(self.picker.clone()) - } -} - -impl SettingsProfileSelector { - pub fn new( - delegate: SettingsProfileSelectorDelegate, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); - Self { picker } - } -} - -pub struct SettingsProfileSelectorDelegate { - matches: Vec, - profile_names: Vec>, - original_profile_name: Option, - selected_profile_name: Option, - selected_index: usize, - selection_completed: bool, - selector: WeakEntity, -} - -impl SettingsProfileSelectorDelegate { - fn new( - selector: WeakEntity, - _: &mut Window, - cx: &mut Context, - ) -> Self { - let settings_store = cx.global::(); - let mut profile_names: Vec> = settings_store - .configured_settings_profiles() - .map(|s| Some(s.to_string())) - .collect(); - profile_names.insert(0, None); - - let matches = profile_names - .iter() - .enumerate() - .map(|(ix, profile_name)| StringMatch { - candidate_id: ix, - score: 0.0, - positions: Default::default(), - string: display_name(profile_name), - }) - .collect(); - - let profile_name = cx - .try_global::() - .map(|p| p.0.clone()); - - let mut this = Self { - matches, - profile_names, - original_profile_name: profile_name.clone(), - selected_profile_name: None, - selected_index: 0, - selection_completed: false, - selector, - }; - - if let Some(profile_name) = profile_name { - this.select_if_matching(&profile_name); - } - - this - } - - fn select_if_matching(&mut self, profile_name: &str) { - self.selected_index = self - .matches - .iter() - .position(|mat| mat.string == profile_name) - .unwrap_or(self.selected_index); - } - - fn set_selected_profile( - &self, - cx: &mut Context>, - ) -> Option { - let mat = self.matches.get(self.selected_index)?; - let profile_name = self.profile_names.get(mat.candidate_id)?; - Self::update_active_profile_name_global(profile_name.clone(), cx) - } - - fn update_active_profile_name_global( - profile_name: Option, - cx: &mut Context>, - ) -> Option { - if let Some(profile_name) = profile_name { - cx.set_global(ActiveSettingsProfileName(profile_name.clone())); - return Some(profile_name); - } - - if cx.has_global::() { - cx.remove_global::(); - } - - None - } -} - -impl PickerDelegate for SettingsProfileSelectorDelegate { - type ListItem = ListItem; - - fn placeholder_text(&self, _: &mut Window, _: &mut App) -> std::sync::Arc { - "Select a settings profile...".into() - } - - fn match_count(&self) -> usize { - self.matches.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; - self.selected_profile_name = self.set_selected_profile(cx); - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - let background = cx.background_executor().clone(); - let candidates = self - .profile_names - .iter() - .enumerate() - .map(|(id, profile_name)| StringMatchCandidate::new(id, &display_name(profile_name))) - .collect::>(); - - cx.spawn_in(window, async move |this, cx| { - let matches = if query.is_empty() { - candidates - .into_iter() - .enumerate() - .map(|(index, candidate)| StringMatch { - candidate_id: index, - string: candidate.string, - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - match_strings( - &candidates, - &query, - false, - true, - 100, - &Default::default(), - background, - ) - .await - }; - - this.update_in(cx, |this, _, cx| { - this.delegate.matches = matches; - this.delegate.selected_index = this - .delegate - .selected_index - .min(this.delegate.matches.len().saturating_sub(1)); - this.delegate.selected_profile_name = this.delegate.set_selected_profile(cx); - }) - .ok(); - }) - } - - fn confirm( - &mut self, - _: bool, - _: &mut Window, - cx: &mut Context>, - ) { - self.selection_completed = true; - self.selector - .update(cx, |_, cx| { - cx.emit(DismissEvent); - }) - .ok(); - } - - fn dismissed( - &mut self, - _: &mut Window, - cx: &mut Context>, - ) { - if !self.selection_completed { - SettingsProfileSelectorDelegate::update_active_profile_name_global( - self.original_profile_name.clone(), - cx, - ); - } - self.selector.update(cx, |_, cx| cx.emit(DismissEvent)).ok(); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - _: &mut Context>, - ) -> Option { - let mat = &self.matches.get(ix)?; - let profile_name = &self.profile_names.get(mat.candidate_id)?; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child(HighlightedLabel::new( - display_name(profile_name), - mat.positions.clone(), - )), - ) - } -} - -fn display_name(profile_name: &Option) -> String { - profile_name.clone().unwrap_or("Disabled".into()) -} - -#[cfg(test)] -mod tests { - use super::*; - use editor; - use gpui::{TestAppContext, UpdateGlobal, VisualTestContext}; - use menu::{Cancel, Confirm, SelectNext, SelectPrevious}; - use project::{FakeFs, Project}; - use serde_json::json; - use settings::Settings; - use theme::{self, ThemeSettings}; - use workspace::{self, AppState}; - use zed_actions::settings_profile_selector; - - async fn init_test( - profiles_json: serde_json::Value, - cx: &mut TestAppContext, - ) -> (Entity, &mut VisualTestContext) { - cx.update(|cx| { - let state = AppState::test(cx); - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - settings::init(cx); - theme::init(theme::LoadThemes::JustBase, cx); - super::init(cx); - editor::init(cx); - state - }); - - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - let settings_json = json!({ - "buffer_font_size": 10.0, - "profiles": profiles_json, - }); - - store - .set_user_settings(&settings_json.to_string(), cx) - .unwrap(); - }); - }); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, ["/test".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - cx.update(|_, cx| { - assert!(!cx.has_global::()); - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(10.0)); - }); - - (workspace, cx) - } - - #[track_caller] - fn active_settings_profile_picker( - workspace: &Entity, - cx: &mut VisualTestContext, - ) -> Entity> { - workspace.update(cx, |workspace, cx| { - workspace - .active_modal::(cx) - .expect("settings profile selector is not open") - .read(cx) - .picker - .clone() - }) - } - - #[gpui::test] - async fn test_settings_profile_selector_state(cx: &mut TestAppContext) { - let classroom_and_streaming_profile_name = "Classroom / Streaming".to_string(); - let demo_videos_profile_name = "Demo Videos".to_string(); - - let profiles_json = json!({ - classroom_and_streaming_profile_name.clone(): { - "buffer_font_size": 20.0, - }, - demo_videos_profile_name.clone(): { - "buffer_font_size": 15.0 - } - }); - let (workspace, cx) = init_test(profiles_json.clone(), cx).await; - - cx.dispatch_action(settings_profile_selector::Toggle); - let picker = active_settings_profile_picker(&workspace, cx); - - picker.read_with(cx, |picker, cx| { - assert_eq!(picker.delegate.matches.len(), 3); - assert_eq!(picker.delegate.matches[0].string, display_name(&None)); - assert_eq!( - picker.delegate.matches[1].string, - classroom_and_streaming_profile_name - ); - assert_eq!(picker.delegate.matches[2].string, demo_videos_profile_name); - assert_eq!(picker.delegate.matches.get(3), None); - - assert_eq!(picker.delegate.selected_index, 0); - assert_eq!(picker.delegate.selected_profile_name, None); - - assert_eq!(cx.try_global::(), None); - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(10.0)); - }); - - cx.dispatch_action(Confirm); - - cx.update(|_, cx| { - assert_eq!(cx.try_global::(), None); - }); - - cx.dispatch_action(settings_profile_selector::Toggle); - let picker = active_settings_profile_picker(&workspace, cx); - cx.dispatch_action(SelectNext); - - picker.read_with(cx, |picker, cx| { - assert_eq!(picker.delegate.selected_index, 1); - assert_eq!( - picker.delegate.selected_profile_name, - Some(classroom_and_streaming_profile_name.clone()) - ); - - assert_eq!( - cx.try_global::() - .map(|p| p.0.clone()), - Some(classroom_and_streaming_profile_name.clone()) - ); - - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(20.0)); - }); - - cx.dispatch_action(Cancel); - - cx.update(|_, cx| { - assert_eq!(cx.try_global::(), None); - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(10.0)); - }); - - cx.dispatch_action(settings_profile_selector::Toggle); - let picker = active_settings_profile_picker(&workspace, cx); - - cx.dispatch_action(SelectNext); - - picker.read_with(cx, |picker, cx| { - assert_eq!(picker.delegate.selected_index, 1); - assert_eq!( - picker.delegate.selected_profile_name, - Some(classroom_and_streaming_profile_name.clone()) - ); - - assert_eq!( - cx.try_global::() - .map(|p| p.0.clone()), - Some(classroom_and_streaming_profile_name.clone()) - ); - - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(20.0)); - }); - - cx.dispatch_action(SelectNext); - - picker.read_with(cx, |picker, cx| { - assert_eq!(picker.delegate.selected_index, 2); - assert_eq!( - picker.delegate.selected_profile_name, - Some(demo_videos_profile_name.clone()) - ); - - assert_eq!( - cx.try_global::() - .map(|p| p.0.clone()), - Some(demo_videos_profile_name.clone()) - ); - - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(15.0)); - }); - - cx.dispatch_action(Confirm); - - cx.update(|_, cx| { - assert_eq!( - cx.try_global::() - .map(|p| p.0.clone()), - Some(demo_videos_profile_name.clone()) - ); - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(15.0)); - }); - - cx.dispatch_action(settings_profile_selector::Toggle); - let picker = active_settings_profile_picker(&workspace, cx); - - picker.read_with(cx, |picker, cx| { - assert_eq!(picker.delegate.selected_index, 2); - assert_eq!( - picker.delegate.selected_profile_name, - Some(demo_videos_profile_name.clone()) - ); - - assert_eq!( - cx.try_global::() - .map(|p| p.0.clone()), - Some(demo_videos_profile_name.clone()) - ); - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(15.0)); - }); - - cx.dispatch_action(SelectPrevious); - - picker.read_with(cx, |picker, cx| { - assert_eq!(picker.delegate.selected_index, 1); - assert_eq!( - picker.delegate.selected_profile_name, - Some(classroom_and_streaming_profile_name.clone()) - ); - - assert_eq!( - cx.try_global::() - .map(|p| p.0.clone()), - Some(classroom_and_streaming_profile_name.clone()) - ); - - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(20.0)); - }); - - cx.dispatch_action(Cancel); - - cx.update(|_, cx| { - assert_eq!( - cx.try_global::() - .map(|p| p.0.clone()), - Some(demo_videos_profile_name.clone()) - ); - - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(15.0)); - }); - - cx.dispatch_action(settings_profile_selector::Toggle); - let picker = active_settings_profile_picker(&workspace, cx); - - picker.read_with(cx, |picker, cx| { - assert_eq!(picker.delegate.selected_index, 2); - assert_eq!( - picker.delegate.selected_profile_name, - Some(demo_videos_profile_name.clone()) - ); - - assert_eq!( - cx.try_global::() - .map(|p| p.0.clone()), - Some(demo_videos_profile_name) - ); - - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(15.0)); - }); - - cx.dispatch_action(SelectPrevious); - - picker.read_with(cx, |picker, cx| { - assert_eq!(picker.delegate.selected_index, 1); - assert_eq!( - picker.delegate.selected_profile_name, - Some(classroom_and_streaming_profile_name.clone()) - ); - - assert_eq!( - cx.try_global::() - .map(|p| p.0.clone()), - Some(classroom_and_streaming_profile_name) - ); - - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(20.0)); - }); - - cx.dispatch_action(SelectPrevious); - - picker.read_with(cx, |picker, cx| { - assert_eq!(picker.delegate.selected_index, 0); - assert_eq!(picker.delegate.selected_profile_name, None); - - assert_eq!( - cx.try_global::() - .map(|p| p.0.clone()), - None - ); - - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(10.0)); - }); - - cx.dispatch_action(Confirm); - - cx.update(|_, cx| { - assert_eq!(cx.try_global::(), None); - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(10.0)); - }); - } - - #[gpui::test] - async fn test_settings_profile_selector_is_in_user_configuration_order( - cx: &mut TestAppContext, - ) { - // Must be unique names (HashMap) - let profiles_json = json!({ - "z": {}, - "e": {}, - "d": {}, - " ": {}, - "r": {}, - "u": {}, - "l": {}, - "3": {}, - "s": {}, - "!": {}, - }); - let (workspace, cx) = init_test(profiles_json.clone(), cx).await; - - cx.dispatch_action(settings_profile_selector::Toggle); - let picker = active_settings_profile_picker(&workspace, cx); - - picker.read_with(cx, |picker, _| { - assert_eq!(picker.delegate.matches.len(), 11); - assert_eq!(picker.delegate.matches[0].string, display_name(&None)); - assert_eq!(picker.delegate.matches[1].string, "z"); - assert_eq!(picker.delegate.matches[2].string, "e"); - assert_eq!(picker.delegate.matches[3].string, "d"); - assert_eq!(picker.delegate.matches[4].string, " "); - assert_eq!(picker.delegate.matches[5].string, "r"); - assert_eq!(picker.delegate.matches[6].string, "u"); - assert_eq!(picker.delegate.matches[7].string, "l"); - assert_eq!(picker.delegate.matches[8].string, "3"); - assert_eq!(picker.delegate.matches[9].string, "s"); - assert_eq!(picker.delegate.matches[10].string, "!"); - }); - } -} diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml deleted file mode 100644 index b5a259a3b9..0000000000 --- a/crates/settings_ui/Cargo.toml +++ /dev/null @@ -1,58 +0,0 @@ -[package] -name = "settings_ui" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/settings_ui.rs" - -[features] -default = [] -test-support = [] - -[dependencies] -anyhow.workspace = true -bm25 = "2.3.2" -editor.workspace = true -feature_flags.workspace = true -fs.workspace = true -fuzzy.workspace = true -gpui.workspace = true -heck.workspace = true -log.workspace = true -menu.workspace = true -paths.workspace = true -picker.workspace = true -project.workspace = true -release_channel.workspace = true -schemars.workspace = true -search.workspace = true -serde.workspace = true -settings.workspace = true -strum.workspace = true -telemetry.workspace = true -theme.workspace = true -title_bar.workspace = true -ui.workspace = true -ui_input.workspace = true -util.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -assets.workspace = true -client.workspace = true -futures.workspace = true -gpui = { workspace = true, features = ["test-support"] } -language.workspace = true -node_runtime.workspace = true -paths.workspace = true -pretty_assertions.workspace = true -session.workspace = true -settings.workspace = true -zlog.workspace = true diff --git a/crates/settings_ui/LICENSE-GPL b/crates/settings_ui/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/settings_ui/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/settings_ui/src/components.rs b/crates/settings_ui/src/components.rs deleted file mode 100644 index b073372ac9..0000000000 --- a/crates/settings_ui/src/components.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod dropdown; -mod font_picker; -mod icon_theme_picker; -mod input_field; -mod theme_picker; - -pub use dropdown::*; -pub use font_picker::font_picker; -pub use icon_theme_picker::icon_theme_picker; -pub use input_field::*; -pub use theme_picker::theme_picker; diff --git a/crates/settings_ui/src/components/dropdown.rs b/crates/settings_ui/src/components/dropdown.rs deleted file mode 100644 index ec9ecb4eaf..0000000000 --- a/crates/settings_ui/src/components/dropdown.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::rc::Rc; - -use gpui::{App, ElementId, IntoElement, RenderOnce}; -use heck::ToTitleCase as _; -use ui::{ - ButtonSize, ContextMenu, DropdownMenu, DropdownStyle, FluentBuilder as _, IconPosition, px, -}; - -#[derive(IntoElement)] -pub struct EnumVariantDropdown -where - T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static, -{ - id: ElementId, - current_value: T, - variants: &'static [T], - labels: &'static [&'static str], - should_do_title_case: bool, - tab_index: Option, - on_change: Rc, -} - -impl EnumVariantDropdown -where - T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static, -{ - pub fn new( - id: impl Into, - current_value: T, - variants: &'static [T], - labels: &'static [&'static str], - on_change: impl Fn(T, &mut App) + 'static, - ) -> Self { - Self { - id: id.into(), - current_value, - variants, - labels, - should_do_title_case: true, - tab_index: None, - on_change: Rc::new(on_change), - } - } - - pub fn title_case(mut self, title_case: bool) -> Self { - self.should_do_title_case = title_case; - self - } - - pub fn tab_index(mut self, tab_index: isize) -> Self { - self.tab_index = Some(tab_index); - self - } -} - -impl RenderOnce for EnumVariantDropdown -where - T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static, -{ - fn render(self, window: &mut ui::Window, cx: &mut ui::App) -> impl gpui::IntoElement { - let current_value_label = self.labels[self - .variants - .iter() - .position(|v| *v == self.current_value) - .unwrap()]; - - let context_menu = window.use_keyed_state(current_value_label, cx, |window, cx| { - ContextMenu::new(window, cx, move |mut menu, _, _| { - for (&value, &label) in std::iter::zip(self.variants, self.labels) { - let on_change = self.on_change.clone(); - let current_value = self.current_value; - menu = menu.toggleable_entry( - if self.should_do_title_case { - label.to_title_case() - } else { - label.to_string() - }, - value == current_value, - IconPosition::End, - None, - move |_, cx| { - on_change(value, cx); - }, - ); - } - menu - }) - }); - - DropdownMenu::new( - self.id, - if self.should_do_title_case { - current_value_label.to_title_case() - } else { - current_value_label.to_string() - }, - context_menu, - ) - .when_some(self.tab_index, |elem, tab_index| elem.tab_index(tab_index)) - .trigger_size(ButtonSize::Medium) - .style(DropdownStyle::Outlined) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }) - .into_any_element() - } -} diff --git a/crates/settings_ui/src/components/font_picker.rs b/crates/settings_ui/src/components/font_picker.rs deleted file mode 100644 index 7a79009efd..0000000000 --- a/crates/settings_ui/src/components/font_picker.rs +++ /dev/null @@ -1,181 +0,0 @@ -use std::sync::Arc; - -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{AnyElement, App, Context, DismissEvent, SharedString, Task, Window}; -use picker::{Picker, PickerDelegate}; -use theme::FontFamilyCache; -use ui::{ListItem, ListItemSpacing, prelude::*}; - -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 = font_family_cache - .try_list_font_families() - .unwrap_or_else(|| vec![current_font.clone()]); - let selected_index = fonts - .iter() - .position(|font| *font == current_font) - .unwrap_or(0); - - let filtered_fonts = fonts - .iter() - .enumerate() - .map(|(index, font)| StringMatch { - candidate_id: index, - string: font.to_string(), - positions: Vec::new(), - score: 0.0, - }) - .collect(); - - Self { - fonts, - filtered_fonts, - 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) { - cx.defer_in(window, |picker, window, cx| { - picker.set_query("", window, cx); - }); - cx.emit(DismissEvent); - } - - 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(), - ) - } -} - -pub 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(18.).into())) -} diff --git a/crates/settings_ui/src/components/icon_theme_picker.rs b/crates/settings_ui/src/components/icon_theme_picker.rs deleted file mode 100644 index 33a648f81b..0000000000 --- a/crates/settings_ui/src/components/icon_theme_picker.rs +++ /dev/null @@ -1,189 +0,0 @@ -use std::sync::Arc; - -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{AnyElement, App, Context, DismissEvent, SharedString, Task, Window}; -use picker::{Picker, PickerDelegate}; -use theme::ThemeRegistry; -use ui::{ListItem, ListItemSpacing, prelude::*}; - -type IconThemePicker = Picker; - -pub struct IconThemePickerDelegate { - icon_themes: Vec, - filtered_themes: Vec, - selected_index: usize, - current_theme: SharedString, - on_theme_changed: Arc, -} - -impl IconThemePickerDelegate { - fn new( - current_theme: SharedString, - on_theme_changed: impl Fn(SharedString, &mut App) + 'static, - cx: &mut Context, - ) -> Self { - let theme_registry = ThemeRegistry::global(cx); - - let icon_themes: Vec = theme_registry - .list_icon_themes() - .into_iter() - .map(|theme_meta| theme_meta.name) - .collect(); - - let selected_index = icon_themes - .iter() - .position(|icon_themes| *icon_themes == current_theme) - .unwrap_or(0); - - let filtered_themes = icon_themes - .iter() - .enumerate() - .map(|(index, icon_themes)| StringMatch { - candidate_id: index, - string: icon_themes.to_string(), - positions: Vec::new(), - score: 0.0, - }) - .collect(); - - Self { - icon_themes, - filtered_themes, - selected_index, - current_theme, - on_theme_changed: Arc::new(on_theme_changed), - } - } -} - -impl PickerDelegate for IconThemePickerDelegate { - type ListItem = AnyElement; - - fn match_count(&self) -> usize { - self.filtered_themes.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_themes.len().saturating_sub(1)); - cx.notify(); - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Search icon theme…".into() - } - - fn update_matches( - &mut self, - query: String, - _window: &mut Window, - cx: &mut Context, - ) -> Task<()> { - let icon_themes = self.icon_themes.clone(); - let current_theme = self.current_theme.clone(); - - let matches: Vec = if query.is_empty() { - icon_themes - .iter() - .enumerate() - .map(|(index, icon_theme)| StringMatch { - candidate_id: index, - string: icon_theme.to_string(), - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - let _candidates: Vec = icon_themes - .iter() - .enumerate() - .map(|(id, icon_theme)| StringMatchCandidate::new(id, icon_theme.as_ref())) - .collect(); - - icon_themes - .iter() - .enumerate() - .filter(|(_, icon_theme)| icon_theme.to_lowercase().contains(&query.to_lowercase())) - .map(|(index, icon_theme)| StringMatch { - candidate_id: index, - string: icon_theme.to_string(), - positions: Vec::new(), - score: 0.0, - }) - .collect() - }; - - let selected_index = if query.is_empty() { - icon_themes - .iter() - .position(|icon_theme| *icon_theme == current_theme) - .unwrap_or(0) - } else { - matches - .iter() - .position(|m| icon_themes[m.candidate_id] == current_theme) - .unwrap_or(0) - }; - - self.filtered_themes = 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(theme_match) = self.filtered_themes.get(self.selected_index) { - let theme = theme_match.string.clone(); - (self.on_theme_changed)(theme.into(), cx); - } - } - - fn dismissed(&mut self, window: &mut Window, cx: &mut Context) { - cx.defer_in(window, |picker, window, cx| { - picker.set_query("", window, cx); - }); - cx.emit(DismissEvent); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - let theme_match = self.filtered_themes.get(ix)?; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child(Label::new(theme_match.string.clone())) - .into_any_element(), - ) - } -} - -pub fn icon_theme_picker( - current_theme: SharedString, - on_theme_changed: impl Fn(SharedString, &mut App) + 'static, - window: &mut Window, - cx: &mut Context, -) -> IconThemePicker { - let delegate = IconThemePickerDelegate::new(current_theme, on_theme_changed, cx); - - Picker::uniform_list(delegate, window, cx) - .show_scrollbar(true) - .width(rems_from_px(210.)) - .max_height(Some(rems(18.).into())) -} diff --git a/crates/settings_ui/src/components/input_field.rs b/crates/settings_ui/src/components/input_field.rs deleted file mode 100644 index 57917c3211..0000000000 --- a/crates/settings_ui/src/components/input_field.rs +++ /dev/null @@ -1,96 +0,0 @@ -use editor::Editor; -use gpui::{Focusable, div}; -use ui::{ - ActiveTheme as _, App, FluentBuilder as _, InteractiveElement as _, IntoElement, - ParentElement as _, RenderOnce, Styled as _, Window, -}; - -#[derive(IntoElement)] -pub struct SettingsInputField { - initial_text: Option, - placeholder: Option<&'static str>, - confirm: Option, &mut App)>>, - tab_index: Option, -} - -impl SettingsInputField { - pub fn new() -> Self { - Self { - initial_text: None, - placeholder: None, - confirm: None, - tab_index: None, - } - } - - pub fn with_initial_text(mut self, initial_text: String) -> Self { - self.initial_text = Some(initial_text); - self - } - - pub fn with_placeholder(mut self, placeholder: &'static str) -> Self { - self.placeholder = Some(placeholder); - self - } - - pub fn on_confirm(mut self, confirm: impl Fn(Option, &mut App) + 'static) -> Self { - self.confirm = Some(Box::new(confirm)); - self - } - - pub(crate) fn tab_index(mut self, arg: isize) -> Self { - self.tab_index = Some(arg); - self - } -} - -impl RenderOnce for SettingsInputField { - fn render(self, window: &mut Window, cx: &mut App) -> impl ui::IntoElement { - let editor = window.use_state(cx, { - move |window, cx| { - let mut editor = Editor::single_line(window, cx); - if let Some(text) = self.initial_text { - editor.set_text(text, window, cx); - } - - if let Some(placeholder) = self.placeholder { - editor.set_placeholder_text(placeholder, window, cx); - } - // todo(settings_ui): We should have an observe global use for settings store - // so whenever a settings file is updated, the settings ui updates too - editor - } - }); - - let weak_editor = editor.downgrade(); - - let theme_colors = cx.theme().colors(); - - div() - .py_1() - .px_2() - .min_w_64() - .rounded_md() - .border_1() - .border_color(theme_colors.border) - .bg(theme_colors.editor_background) - .when_some(self.tab_index, |this, tab_index| { - let focus_handle = editor.focus_handle(cx).tab_index(tab_index).tab_stop(true); - this.track_focus(&focus_handle) - .focus(|s| s.border_color(theme_colors.border_focused)) - }) - .child(editor) - .when_some(self.confirm, |this, confirm| { - this.on_action::({ - move |_, _, cx| { - let Some(editor) = weak_editor.upgrade() else { - return; - }; - let new_value = editor.read_with(cx, |editor, cx| editor.text(cx)); - let new_value = (!new_value.is_empty()).then_some(new_value); - confirm(new_value, cx); - } - }) - }) - } -} diff --git a/crates/settings_ui/src/components/theme_picker.rs b/crates/settings_ui/src/components/theme_picker.rs deleted file mode 100644 index 2146ab314f..0000000000 --- a/crates/settings_ui/src/components/theme_picker.rs +++ /dev/null @@ -1,179 +0,0 @@ -use std::sync::Arc; - -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{AnyElement, App, Context, DismissEvent, SharedString, Task, Window}; -use picker::{Picker, PickerDelegate}; -use theme::ThemeRegistry; -use ui::{ListItem, ListItemSpacing, prelude::*}; - -type ThemePicker = Picker; - -pub struct ThemePickerDelegate { - themes: Vec, - filtered_themes: Vec, - selected_index: usize, - current_theme: SharedString, - on_theme_changed: Arc, -} - -impl ThemePickerDelegate { - fn new( - current_theme: SharedString, - on_theme_changed: impl Fn(SharedString, &mut App) + 'static, - cx: &mut Context, - ) -> Self { - let theme_registry = ThemeRegistry::global(cx); - - let themes = theme_registry.list_names(); - let selected_index = themes - .iter() - .position(|theme| *theme == current_theme) - .unwrap_or(0); - - let filtered_themes = themes - .iter() - .enumerate() - .map(|(index, theme)| StringMatch { - candidate_id: index, - string: theme.to_string(), - positions: Vec::new(), - score: 0.0, - }) - .collect(); - - Self { - themes, - filtered_themes, - selected_index, - current_theme, - on_theme_changed: Arc::new(on_theme_changed), - } - } -} - -impl PickerDelegate for ThemePickerDelegate { - type ListItem = AnyElement; - - fn match_count(&self) -> usize { - self.filtered_themes.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_themes.len().saturating_sub(1)); - cx.notify(); - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Search theme…".into() - } - - fn update_matches( - &mut self, - query: String, - _window: &mut Window, - cx: &mut Context, - ) -> Task<()> { - let themes = self.themes.clone(); - let current_theme = self.current_theme.clone(); - - let matches: Vec = if query.is_empty() { - themes - .iter() - .enumerate() - .map(|(index, theme)| StringMatch { - candidate_id: index, - string: theme.to_string(), - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - let _candidates: Vec = themes - .iter() - .enumerate() - .map(|(id, theme)| StringMatchCandidate::new(id, theme.as_ref())) - .collect(); - - themes - .iter() - .enumerate() - .filter(|(_, theme)| theme.to_lowercase().contains(&query.to_lowercase())) - .map(|(index, theme)| StringMatch { - candidate_id: index, - string: theme.to_string(), - positions: Vec::new(), - score: 0.0, - }) - .collect() - }; - - let selected_index = if query.is_empty() { - themes - .iter() - .position(|theme| *theme == current_theme) - .unwrap_or(0) - } else { - matches - .iter() - .position(|m| themes[m.candidate_id] == current_theme) - .unwrap_or(0) - }; - - self.filtered_themes = 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(theme_match) = self.filtered_themes.get(self.selected_index) { - let theme = theme_match.string.clone(); - (self.on_theme_changed)(theme.into(), cx); - } - } - - fn dismissed(&mut self, window: &mut Window, cx: &mut Context) { - cx.defer_in(window, |picker, window, cx| { - picker.set_query("", window, cx); - }); - cx.emit(DismissEvent); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - let theme_match = self.filtered_themes.get(ix)?; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child(Label::new(theme_match.string.clone())) - .into_any_element(), - ) - } -} - -pub fn theme_picker( - current_theme: SharedString, - on_theme_changed: impl Fn(SharedString, &mut App) + 'static, - window: &mut Window, - cx: &mut Context, -) -> ThemePicker { - let delegate = ThemePickerDelegate::new(current_theme, on_theme_changed, cx); - - Picker::uniform_list(delegate, window, cx) - .show_scrollbar(true) - .width(rems_from_px(210.)) - .max_height(Some(rems(18.).into())) -} diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs deleted file mode 100644 index 8652ccf68b..0000000000 --- a/crates/settings_ui/src/page_data.rs +++ /dev/null @@ -1,7549 +0,0 @@ -use gpui::App; -use settings::{LanguageSettingsContent, SettingsContent}; -use std::sync::Arc; -use strum::IntoDiscriminant as _; -use ui::{IntoElement, SharedString}; - -use crate::{ - DynamicItem, PROJECT, SettingField, SettingItem, SettingsFieldMetadata, SettingsPage, - SettingsPageItem, SubPageLink, USER, all_language_names, sub_page_stack, -}; - -const DEFAULT_STRING: String = String::new(); -/// A default empty string reference. Useful in `pick` functions for cases either in dynamic item fields, or when dealing with `settings::Maybe` -/// to avoid the "NO DEFAULT" case. -const DEFAULT_EMPTY_STRING: Option<&String> = Some(&DEFAULT_STRING); - -const DEFAULT_SHARED_STRING: SharedString = SharedString::new_static(""); -/// A default empty string reference. Useful in `pick` functions for cases either in dynamic item fields, or when dealing with `settings::Maybe` -/// to avoid the "NO DEFAULT" case. -const DEFAULT_EMPTY_SHARED_STRING: Option<&SharedString> = Some(&DEFAULT_SHARED_STRING); - -pub(crate) fn settings_data(cx: &App) -> Vec { - vec![ - SettingsPage { - title: "General", - items: vec![ - SettingsPageItem::SectionHeader("General Settings"), - SettingsPageItem::SettingItem(SettingItem { - files: PROJECT, - title: "Project Name", - description: "The displayed name of this project. If left empty, the root directory name will be displayed.", - field: Box::new( - SettingField { - json_path: Some("project_name"), - pick: |settings_content| { - settings_content.project.worktree.project_name.as_ref().or(DEFAULT_EMPTY_STRING) - }, - write: |settings_content, value| { - settings_content.project.worktree.project_name = value.filter(|name| !name.is_empty()); - }, - } - ), - metadata: Some(Box::new(SettingsFieldMetadata { placeholder: Some("Project Name"), ..Default::default() })), - }), - SettingsPageItem::SettingItem(SettingItem { - title: "When Closing With No Tabs", - description: "What to do when using the 'close active item' action with no tabs.", - field: Box::new(SettingField { - json_path: Some("when_closing_with_no_tabs"), - pick: |settings_content| { - settings_content - .workspace - .when_closing_with_no_tabs - .as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.when_closing_with_no_tabs = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "On Last Window Closed", - description: "What to do when the last window is closed.", - field: Box::new(SettingField { - json_path: Some("on_last_window_closed"), - pick: |settings_content| { - settings_content.workspace.on_last_window_closed.as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.on_last_window_closed = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Use System Path Prompts", - description: "Use native OS dialogs for 'Open' and 'Save As'.", - field: Box::new(SettingField { - json_path: Some("use_system_path_prompts"), - pick: |settings_content| { - settings_content.workspace.use_system_path_prompts.as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.use_system_path_prompts = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Use System Prompts", - description: "Use native OS dialogs for confirmations.", - field: Box::new(SettingField { - json_path: Some("use_system_prompts"), - pick: |settings_content| { - settings_content.workspace.use_system_prompts.as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.use_system_prompts = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Redact Private Values", - description: "Hide the values of variables in private files.", - field: Box::new(SettingField { - json_path: Some("redact_private_values"), - pick: |settings_content| { - settings_content.editor.redact_private_values.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.redact_private_values = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Private Files", - description: "Globs to match against file paths to determine if a file is private.", - field: Box::new( - SettingField { - json_path: Some("worktree.private_files"), - pick: |settings_content| { - settings_content.project.worktree.private_files.as_ref() - }, - write: |settings_content, value| { - settings_content.project.worktree.private_files = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Workspace Restoration"), - SettingsPageItem::SettingItem(SettingItem { - title: "Restore Unsaved Buffers", - description: "Whether or not to restore unsaved buffers on restart.", - field: Box::new(SettingField { - json_path: Some("session.restore_unsaved_buffers"), - pick: |settings_content| { - settings_content - .session - .as_ref() - .and_then(|session| session.restore_unsaved_buffers.as_ref()) - }, - write: |settings_content, value| { - settings_content - .session - .get_or_insert_default() - .restore_unsaved_buffers = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Restore On Startup", - description: "What to restore from the previous session when opening Zed.", - field: Box::new(SettingField { - json_path: Some("restore_on_startup"), - pick: |settings_content| { - settings_content.workspace.restore_on_startup.as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.restore_on_startup = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Scoped Settings"), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Preview Channel", - description: "Which settings should be activated only in Preview build of Zed.", - field: Box::new( - SettingField { - json_path: Some("preview_channel_settings"), - pick: |settings_content| { - Some(settings_content) - }, - write: |_settings_content, _value| { - - }, - } - .unimplemented(), - ), - metadata: None, - }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Settings Profiles", - description: "Any number of settings profiles that are temporarily applied on top of your existing user settings.", - field: Box::new( - SettingField { - json_path: Some("settings_profiles"), - pick: |settings_content| { - Some(settings_content) - }, - write: |_settings_content, _value| { - }, - } - .unimplemented(), - ), - metadata: None, - }), - SettingsPageItem::SectionHeader("Privacy"), - SettingsPageItem::SettingItem(SettingItem { - title: "Telemetry Diagnostics", - description: "Send debug information like crash reports.", - field: Box::new(SettingField { - json_path: Some("telemetry.diagnostics"), - pick: |settings_content| { - settings_content - .telemetry - .as_ref() - .and_then(|telemetry| telemetry.diagnostics.as_ref()) - }, - write: |settings_content, value| { - settings_content - .telemetry - .get_or_insert_default() - .diagnostics = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Telemetry Metrics", - description: "Send anonymized usage data like what languages you're using Zed with.", - field: Box::new(SettingField { - json_path: Some("telemetry.metrics"), - pick: |settings_content| { - settings_content - .telemetry - .as_ref() - .and_then(|telemetry| telemetry.metrics.as_ref()) - }, - write: |settings_content, value| { - settings_content.telemetry.get_or_insert_default().metrics = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Auto Update"), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Update", - description: "Whether or not to automatically check for updates.", - field: Box::new(SettingField { - json_path: Some("auto_update"), - pick: |settings_content| settings_content.auto_update.as_ref(), - write: |settings_content, value| { - settings_content.auto_update = value; - }, - }), - metadata: None, - files: USER, - }), - ], - }, - SettingsPage { - title: "Appearance", - items: vec![ - SettingsPageItem::SectionHeader("Theme"), - SettingsPageItem::DynamicItem(DynamicItem { - discriminant: SettingItem { - files: USER, - title: "Theme Mode", - description: "Choose a static, fixed theme or dynamically select themes based on appearance and light/dark modes.", - field: Box::new(SettingField { - json_path: Some("theme$"), - pick: |settings_content| { - Some(&dynamic_variants::()[ - settings_content - .theme - .theme - .as_ref()? - .discriminant() as usize]) - }, - write: |settings_content, value| { - let Some(value) = value else { - settings_content.theme.theme = None; - return; - }; - let settings_value = settings_content.theme.theme.get_or_insert_with(|| { - settings::ThemeSelection::Static(theme::ThemeName(theme::default_theme(theme::SystemAppearance::default().0).into())) - }); - *settings_value = match value { - settings::ThemeSelectionDiscriminants::Static => { - let name = match settings_value { - settings::ThemeSelection::Static(_) => return, - settings::ThemeSelection::Dynamic { mode, light, dark } => { - match mode { - theme::ThemeAppearanceMode::Light => light.clone(), - theme::ThemeAppearanceMode::Dark => dark.clone(), - theme::ThemeAppearanceMode::System => dark.clone(), // no cx, can't determine correct choice - } - }, - }; - settings::ThemeSelection::Static(name) - }, - settings::ThemeSelectionDiscriminants::Dynamic => { - let static_name = match settings_value { - settings::ThemeSelection::Static(theme_name) => theme_name.clone(), - settings::ThemeSelection::Dynamic {..} => return, - }; - - settings::ThemeSelection::Dynamic { - mode: settings::ThemeAppearanceMode::System, - light: static_name.clone(), - dark: static_name, - } - }, - }; - }, - }), - metadata: None, - }, - pick_discriminant: |settings_content| { - Some(settings_content.theme.theme.as_ref()?.discriminant() as usize) - }, - fields: dynamic_variants::().into_iter().map(|variant| { - match variant { - settings::ThemeSelectionDiscriminants::Static => vec![ - SettingItem { - files: USER, - title: "Theme Name", - description: "The name of your selected theme.", - field: Box::new(SettingField { - json_path: Some("theme"), - pick: |settings_content| { - match settings_content.theme.theme.as_ref() { - Some(settings::ThemeSelection::Static(name)) => Some(name), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .theme - .theme.as_mut() { - Some(settings::ThemeSelection::Static(theme_name)) => *theme_name = value, - _ => return - } - }, - }), - metadata: None, - } - ], - settings::ThemeSelectionDiscriminants::Dynamic => vec![ - SettingItem { - files: USER, - title: "Mode", - description: "Choose whether to use the selected light or dark theme or to follow your OS appearance configuration.", - field: Box::new(SettingField { - json_path: Some("theme.mode"), - pick: |settings_content| { - match settings_content.theme.theme.as_ref() { - Some(settings::ThemeSelection::Dynamic { mode, ..}) => Some(mode), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .theme - .theme.as_mut() { - Some(settings::ThemeSelection::Dynamic{ mode, ..}) => *mode = value, - _ => return - } - }, - }), - metadata: None, - }, - SettingItem { - files: USER, - title: "Light Theme", - description: "The theme to use when mode is set to light, or when mode is set to system and it is in light mode.", - field: Box::new(SettingField { - json_path: Some("theme.light"), - pick: |settings_content| { - match settings_content.theme.theme.as_ref() { - Some(settings::ThemeSelection::Dynamic { light, ..}) => Some(light), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .theme - .theme.as_mut() { - Some(settings::ThemeSelection::Dynamic{ light, ..}) => *light = value, - _ => return - } - }, - }), - metadata: None, - }, - SettingItem { - files: USER, - title: "Dark Theme", - description: "The theme to use when mode is set to dark, or when mode is set to system and it is in dark mode.", - field: Box::new(SettingField { - json_path: Some("theme.dark"), - pick: |settings_content| { - match settings_content.theme.theme.as_ref() { - Some(settings::ThemeSelection::Dynamic { dark, ..}) => Some(dark), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .theme - .theme.as_mut() { - Some(settings::ThemeSelection::Dynamic{ dark, ..}) => *dark = value, - _ => return - } - }, - }), - metadata: None, - } - ], - } - }).collect(), - }), - SettingsPageItem::DynamicItem(DynamicItem { - discriminant: SettingItem { - files: USER, - title: "Icon Theme", - description: "The custom set of icons Zed will associate with files and directories.", - field: Box::new(SettingField { - json_path: Some("icon_theme$"), - pick: |settings_content| { - Some(&dynamic_variants::()[ - settings_content - .theme - .icon_theme - .as_ref()? - .discriminant() as usize]) - }, - write: |settings_content, value| { - let Some(value) = value else { - settings_content.theme.icon_theme = None; - return; - }; - let settings_value = settings_content.theme.icon_theme.get_or_insert_with(|| { - settings::IconThemeSelection::Static(settings::IconThemeName(theme::default_icon_theme().name.clone().into())) - }); - *settings_value = match value { - settings::IconThemeSelectionDiscriminants::Static => { - let name = match settings_value { - settings::IconThemeSelection::Static(_) => return, - settings::IconThemeSelection::Dynamic { mode, light, dark } => { - match mode { - theme::ThemeAppearanceMode::Light => light.clone(), - theme::ThemeAppearanceMode::Dark => dark.clone(), - theme::ThemeAppearanceMode::System => dark.clone(), // no cx, can't determine correct choice - } - }, - }; - settings::IconThemeSelection::Static(name) - }, - settings::IconThemeSelectionDiscriminants::Dynamic => { - let static_name = match settings_value { - settings::IconThemeSelection::Static(theme_name) => theme_name.clone(), - settings::IconThemeSelection::Dynamic {..} => return, - }; - - settings::IconThemeSelection::Dynamic { - mode: settings::ThemeAppearanceMode::System, - light: static_name.clone(), - dark: static_name, - } - }, - }; - }, - }), - metadata: None, - }, - pick_discriminant: |settings_content| { - Some(settings_content.theme.icon_theme.as_ref()?.discriminant() as usize) - }, - fields: dynamic_variants::().into_iter().map(|variant| { - match variant { - settings::IconThemeSelectionDiscriminants::Static => vec![ - SettingItem { - files: USER, - title: "Icon Theme Name", - description: "The name of your selected icon theme.", - field: Box::new(SettingField { - json_path: Some("icon_theme$string"), - pick: |settings_content| { - match settings_content.theme.icon_theme.as_ref() { - Some(settings::IconThemeSelection::Static(name)) => Some(name), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .theme - .icon_theme.as_mut() { - Some(settings::IconThemeSelection::Static(theme_name)) => *theme_name = value, - _ => return - } - }, - }), - metadata: None, - } - ], - settings::IconThemeSelectionDiscriminants::Dynamic => vec![ - SettingItem { - files: USER, - title: "Mode", - description: "Choose whether to use the selected light or dark icon theme or to follow your OS appearance configuration.", - field: Box::new(SettingField { - json_path: Some("icon_theme"), - pick: |settings_content| { - match settings_content.theme.icon_theme.as_ref() { - Some(settings::IconThemeSelection::Dynamic { mode, ..}) => Some(mode), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .theme - .icon_theme.as_mut() { - Some(settings::IconThemeSelection::Dynamic{ mode, ..}) => *mode = value, - _ => return - } - }, - }), - metadata: None, - }, - SettingItem { - files: USER, - title: "Light Icon Theme", - description: "The icon theme to use when mode is set to light, or when mode is set to system and it is in light mode.", - field: Box::new(SettingField { - json_path: Some("icon_theme.light"), - pick: |settings_content| { - match settings_content.theme.icon_theme.as_ref() { - Some(settings::IconThemeSelection::Dynamic { light, ..}) => Some(light), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .theme - .icon_theme.as_mut() { - Some(settings::IconThemeSelection::Dynamic{ light, ..}) => *light = value, - _ => return - } - }, - }), - metadata: None, - }, - SettingItem { - files: USER, - title: "Dark Icon Theme", - description: "The icon theme to use when mode is set to dark, or when mode is set to system and it is in dark mode.", - field: Box::new(SettingField { - json_path: Some("icon_theme.dark"), - pick: |settings_content| { - match settings_content.theme.icon_theme.as_ref() { - Some(settings::IconThemeSelection::Dynamic { dark, ..}) => Some(dark), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .theme - .icon_theme.as_mut() { - Some(settings::IconThemeSelection::Dynamic{ dark, ..}) => *dark = value, - _ => return - } - }, - }), - metadata: None, - } - ], - } - }).collect(), - }), - SettingsPageItem::SectionHeader("Buffer Font"), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Family", - description: "Font family for editor text.", - field: Box::new(SettingField { - json_path: Some("buffer_font_family"), - pick: |settings_content| settings_content.theme.buffer_font_family.as_ref(), - write: |settings_content, value|{ settings_content.theme.buffer_font_family = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Size", - description: "Font size for editor text.", - field: Box::new(SettingField { - json_path: Some("buffer_font_size"), - pick: |settings_content| settings_content.theme.buffer_font_size.as_ref(), - write: |settings_content, value|{ settings_content.theme.buffer_font_size = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Weight", - description: "Font weight for editor text (100-900).", - field: Box::new(SettingField { - json_path: Some("buffer_font_weight"), - pick: |settings_content| settings_content.theme.buffer_font_weight.as_ref(), - write: |settings_content, value|{ settings_content.theme.buffer_font_weight = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::DynamicItem(DynamicItem { - discriminant: SettingItem { - files: USER, - title: "Line Height", - description: "Line height for editor text.", - field: Box::new(SettingField { - json_path: Some("buffer_line_height$"), - pick: |settings_content| { - Some(&dynamic_variants::()[ - settings_content - .theme - .buffer_line_height - .as_ref()? - .discriminant() as usize]) - }, - write: |settings_content, value| { - let Some(value) = value else { - settings_content.theme.buffer_line_height = None; - return; - }; - let settings_value = settings_content.theme.buffer_line_height.get_or_insert_with(|| { - settings::BufferLineHeight::default() - }); - *settings_value = match value { - settings::BufferLineHeightDiscriminants::Comfortable => { - settings::BufferLineHeight::Comfortable - }, - settings::BufferLineHeightDiscriminants::Standard => { - settings::BufferLineHeight::Standard - }, - settings::BufferLineHeightDiscriminants::Custom => { - let custom_value = theme::BufferLineHeight::from(*settings_value).value(); - settings::BufferLineHeight::Custom(custom_value) - }, - }; - }, - }), - metadata: None, - }, - pick_discriminant: |settings_content| { - Some(settings_content.theme.buffer_line_height.as_ref()?.discriminant() as usize) - }, - fields: dynamic_variants::().into_iter().map(|variant| { - match variant { - settings::BufferLineHeightDiscriminants::Comfortable => vec![], - settings::BufferLineHeightDiscriminants::Standard => vec![], - settings::BufferLineHeightDiscriminants::Custom => vec![ - SettingItem { - files: USER, - title: "Custom Line Height", - description: "Custom line height value (must be at least 1.0).", - field: Box::new(SettingField { - json_path: Some("buffer_line_height"), - pick: |settings_content| { - match settings_content.theme.buffer_line_height.as_ref() { - Some(settings::BufferLineHeight::Custom(value)) => Some(value), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .theme - .buffer_line_height.as_mut() { - Some(settings::BufferLineHeight::Custom(line_height)) => *line_height = f32::max(value, 1.0), - _ => return - } - }, - }), - metadata: None, - } - ], - } - }).collect(), - }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Font Features", - description: "The OpenType features to enable for rendering in text buffers.", - field: Box::new( - SettingField { - json_path: Some("buffer_font_features"), - pick: |settings_content| { - settings_content.theme.buffer_font_features.as_ref() - }, - write: |settings_content, value| { - settings_content.theme.buffer_font_features = value; - - }, - } - .unimplemented(), - ), - metadata: None, - }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Font Fallbacks", - description: "The font fallbacks to use for rendering in text buffers.", - field: Box::new( - SettingField { - json_path: Some("buffer_font_fallbacks"), - pick: |settings_content| { - settings_content.theme.buffer_font_fallbacks.as_ref() - }, - write: |settings_content, value| { - settings_content.theme.buffer_font_fallbacks = value; - - }, - } - .unimplemented(), - ), - metadata: None, - }), - SettingsPageItem::SectionHeader("UI Font"), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Family", - description: "Font family for UI elements.", - field: Box::new(SettingField { - json_path: Some("ui_font_family"), - pick: |settings_content| settings_content.theme.ui_font_family.as_ref(), - write: |settings_content, value|{ settings_content.theme.ui_font_family = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Size", - description: "Font size for UI elements.", - field: Box::new(SettingField { - json_path: Some("ui_font_size"), - pick: |settings_content| settings_content.theme.ui_font_size.as_ref(), - write: |settings_content, value|{ settings_content.theme.ui_font_size = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Weight", - description: "Font weight for UI elements (100-900).", - field: Box::new(SettingField { - json_path: Some("ui_font_weight"), - pick: |settings_content| settings_content.theme.ui_font_weight.as_ref(), - write: |settings_content, value|{ settings_content.theme.ui_font_weight = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Font Features", - description: "The OpenType features to enable for rendering in UI elements.", - field: Box::new( - SettingField { - json_path: Some("ui_font_features"), - pick: |settings_content| { - settings_content.theme.ui_font_features.as_ref() - }, - write: |settings_content, value| { - settings_content.theme.ui_font_features = value; - - }, - } - .unimplemented(), - ), - metadata: None, - }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Font Fallbacks", - description: "The font fallbacks to use for rendering in the UI.", - field: Box::new( - SettingField { - json_path: Some("ui_font_fallbacks"), - pick: |settings_content| { - settings_content.theme.ui_font_fallbacks.as_ref() - }, - write: |settings_content, value| { - settings_content.theme.ui_font_fallbacks = value; - - }, - } - .unimplemented(), - ), - metadata: None, - }), - SettingsPageItem::SectionHeader("Agent Panel Font"), - SettingsPageItem::SettingItem(SettingItem { - title: "UI Font Size", - description: "Font size for agent response text in the agent panel. Falls back to the regular UI font size.", - field: Box::new(SettingField { - json_path: Some("agent_ui_font_size"), - pick: |settings_content| { - settings_content - .theme - .agent_ui_font_size - .as_ref() - .or(settings_content.theme.ui_font_size.as_ref()) - }, - write: |settings_content, value|{ settings_content.theme.agent_ui_font_size = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Buffer Font Size", - description: "Font size for user messages text in the agent panel.", - field: Box::new(SettingField { - json_path: Some("agent_buffer_font_size"), - pick: |settings_content| { - settings_content - .theme - .agent_buffer_font_size - .as_ref() - .or(settings_content.theme.buffer_font_size.as_ref()) - }, - write: |settings_content, value| { - settings_content.theme.agent_buffer_font_size = value; - - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Cursor"), - SettingsPageItem::SettingItem(SettingItem { - title: "Multi Cursor Modifier", - description: "Modifier key for adding multiple cursors.", - field: Box::new(SettingField { - json_path: Some("multi_cursor_modifier"), - pick: |settings_content| { - settings_content.editor.multi_cursor_modifier.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.multi_cursor_modifier = value; - - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Cursor Blink", - description: "Whether the cursor blinks in the editor.", - field: Box::new(SettingField { - json_path: Some("cursor_blink"), - pick: |settings_content| settings_content.editor.cursor_blink.as_ref(), - write: |settings_content, value|{ settings_content.editor.cursor_blink = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Cursor Shape", - description: "Cursor shape for the editor.", - field: Box::new(SettingField { - json_path: Some("cursor_shape"), - pick: |settings_content| settings_content.editor.cursor_shape.as_ref(), - write: |settings_content, value|{ settings_content.editor.cursor_shape = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Hide Mouse", - description: "When to hide the mouse cursor.", - field: Box::new(SettingField { - json_path: Some("hide_mouse"), - pick: |settings_content| settings_content.editor.hide_mouse.as_ref(), - write: |settings_content, value|{ settings_content.editor.hide_mouse = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Highlighting"), - SettingsPageItem::SettingItem(SettingItem { - title: "Unnecessary Code Fade", - description: "How much to fade out unused code (0.0 - 0.9).", - field: Box::new(SettingField { - json_path: Some("unnecessary_code_fade"), - pick: |settings_content| { - settings_content.theme.unnecessary_code_fade.as_ref() - }, - write: |settings_content, value| { - settings_content.theme.unnecessary_code_fade = value; - - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Current Line Highlight", - description: "How to highlight the current line.", - field: Box::new(SettingField { - json_path: Some("current_line_highlight"), - pick: |settings_content| { - settings_content.editor.current_line_highlight.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.current_line_highlight = value; - - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Selection Highlight", - description: "Highlight all occurrences of selected text.", - field: Box::new(SettingField { - json_path: Some("selection_highlight"), - pick: |settings_content| { - settings_content.editor.selection_highlight.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.selection_highlight = value; - - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Rounded Selection", - description: "Whether the text selection should have rounded corners.", - field: Box::new(SettingField { - json_path: Some("rounded_selection"), - pick: |settings_content| settings_content.editor.rounded_selection.as_ref(), - write: |settings_content, value|{ settings_content.editor.rounded_selection = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Minimum Contrast For Highlights", - description: "The minimum APCA perceptual contrast to maintain when rendering text over highlight backgrounds.", - field: Box::new(SettingField { - json_path: Some("minimum_contrast_for_highlights"), - pick: |settings_content| { - settings_content - .editor - .minimum_contrast_for_highlights - .as_ref() - }, - write: |settings_content, value| { - settings_content.editor.minimum_contrast_for_highlights = value; - - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Guides"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Wrap Guides", - description: "Show wrap guides (vertical rulers).", - field: Box::new(SettingField { - json_path: Some("show_wrap_guides"), - pick: |settings_content| { - settings_content - .project - .all_languages - .defaults - .show_wrap_guides - .as_ref() - }, - write: |settings_content, value| { - settings_content - - .project - .all_languages - .defaults - .show_wrap_guides = value; - }, - }), - metadata: None, - files: USER | PROJECT, - }), - // todo(settings_ui): This needs a custom component - SettingsPageItem::SettingItem(SettingItem { - title: "Wrap Guides", - description: "Character counts at which to show wrap guides.", - field: Box::new( - SettingField { - json_path: Some("wrap_guides"), - pick: |settings_content| { - settings_content - .project - .all_languages - .defaults - .wrap_guides - .as_ref() - }, - write: |settings_content, value| { - settings_content.project.all_languages.defaults.wrap_guides = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - ], - }, - SettingsPage { - title: "Keymap", - items: vec![ - SettingsPageItem::SectionHeader("Base Keymap"), - SettingsPageItem::SettingItem(SettingItem { - title: "Base Keymap", - description: "The name of a base set of key bindings to use.", - field: Box::new(SettingField { - json_path: Some("base_keymap"), - pick: |settings_content| settings_content.base_keymap.as_ref(), - write: |settings_content, value| { - settings_content.base_keymap = value; - }, - }), - metadata: Some(Box::new(SettingsFieldMetadata { - should_do_titlecase: Some(false), - ..Default::default() - })), - files: USER, - }), - SettingsPageItem::SectionHeader("Modal Editing"), - // todo(settings_ui): Vim/Helix Mode should be apart of one type because it's undefined - // behavior to have them both enabled at the same time - SettingsPageItem::SettingItem(SettingItem { - title: "Vim Mode", - description: "Enable Vim mode and key bindings.", - field: Box::new(SettingField { - json_path: Some("vim_mode"), - pick: |settings_content| settings_content.vim_mode.as_ref(), - write: |settings_content, value| { - settings_content.vim_mode = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Helix Mode", - description: "Enable Helix mode and key bindings.", - field: Box::new(SettingField { - json_path: Some("helix_mode"), - pick: |settings_content| settings_content.helix_mode.as_ref(), - write: |settings_content, value| { - settings_content.helix_mode = value; - }, - }), - metadata: None, - files: USER, - }), - ], - }, - SettingsPage { - title: "Editor", - items: { - let mut items = vec![ - SettingsPageItem::SectionHeader("Auto Save"), - SettingsPageItem::DynamicItem(DynamicItem { - discriminant: SettingItem { - files: USER, - title: "Auto Save Mode", - description: "When to auto save buffer changes.", - field: Box::new(SettingField { - json_path: Some("autosave$"), - pick: |settings_content| { - Some(&dynamic_variants::()[ - settings_content - .workspace - .autosave - .as_ref()? - .discriminant() as usize]) - }, - write: |settings_content, value| { - let Some(value) = value else { - settings_content.workspace.autosave = None; - return; - }; - let settings_value = settings_content.workspace.autosave.get_or_insert_with(|| { - settings::AutosaveSetting::Off - }); - *settings_value = match value { - settings::AutosaveSettingDiscriminants::Off => { - settings::AutosaveSetting::Off - }, - settings::AutosaveSettingDiscriminants::AfterDelay => { - let milliseconds = match settings_value { - settings::AutosaveSetting::AfterDelay { milliseconds } => *milliseconds, - _ => settings::DelayMs(1000), - }; - settings::AutosaveSetting::AfterDelay { milliseconds } - }, - settings::AutosaveSettingDiscriminants::OnFocusChange => { - settings::AutosaveSetting::OnFocusChange - }, - settings::AutosaveSettingDiscriminants::OnWindowChange => { - settings::AutosaveSetting::OnWindowChange - }, - }; - }, - }), - metadata: None, - }, - pick_discriminant: |settings_content| { - Some(settings_content.workspace.autosave.as_ref()?.discriminant() as usize) - }, - fields: dynamic_variants::().into_iter().map(|variant| { - match variant { - settings::AutosaveSettingDiscriminants::Off => vec![], - settings::AutosaveSettingDiscriminants::AfterDelay => vec![ - SettingItem { - files: USER, - title: "Delay (milliseconds)", - description: "Save after inactivity period (in milliseconds).", - field: Box::new(SettingField { - json_path: Some("autosave.after_delay.milliseconds"), - pick: |settings_content| { - match settings_content.workspace.autosave.as_ref() { - Some(settings::AutosaveSetting::AfterDelay { milliseconds }) => Some(milliseconds), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - settings_content.workspace.autosave = None; - return; - }; - match settings_content - .workspace - .autosave.as_mut() { - Some(settings::AutosaveSetting::AfterDelay { milliseconds }) => *milliseconds = value, - _ => return - } - }, - }), - metadata: None, - } - ], - settings::AutosaveSettingDiscriminants::OnFocusChange => vec![], - settings::AutosaveSettingDiscriminants::OnWindowChange => vec![], - } - }).collect(), - }), - SettingsPageItem::SectionHeader("Multibuffer"), - SettingsPageItem::SettingItem(SettingItem { - title: "Double Click In Multibuffer", - description: "What to do when multibuffer is double-clicked in some of its excerpts.", - field: Box::new(SettingField { - json_path: Some("double_click_in_multibuffer"), - pick: |settings_content| { - settings_content.editor.double_click_in_multibuffer.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.double_click_in_multibuffer = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Expand Excerpt Lines", - description: "How many lines to expand the multibuffer excerpts by default.", - field: Box::new(SettingField { - json_path: Some("expand_excerpt_lines"), - pick: |settings_content| { - settings_content.editor.expand_excerpt_lines.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.expand_excerpt_lines = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Excerpt Context Lines", - description: "How many lines of context to provide in multibuffer excerpts by default.", - field: Box::new(SettingField { - json_path: Some("excerpt_context_lines"), - pick: |settings_content| { - settings_content.editor.excerpt_context_lines.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.excerpt_context_lines = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Expand Outlines With Depth", - description: "Default depth to expand outline items in the current file.", - field: Box::new(SettingField { - json_path: Some("outline_panel.expand_outlines_with_depth"), - pick: |settings_content| { - settings_content - .outline_panel - .as_ref() - .and_then(|outline_panel| { - outline_panel.expand_outlines_with_depth.as_ref() - }) - }, - write: |settings_content, value| { - settings_content - .outline_panel - .get_or_insert_default() - .expand_outlines_with_depth = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Scrolling"), - SettingsPageItem::SettingItem(SettingItem { - title: "Scroll Beyond Last Line", - description: "Whether the editor will scroll beyond the last line.", - field: Box::new(SettingField { - json_path: Some("scroll_beyond_last_line"), - pick: |settings_content| { - settings_content.editor.scroll_beyond_last_line.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.scroll_beyond_last_line = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Vertical Scroll Margin", - description: "The number of lines to keep above/below the cursor when auto-scrolling.", - field: Box::new(SettingField { - json_path: Some("vertical_scroll_margin"), - pick: |settings_content| { - settings_content.editor.vertical_scroll_margin.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.vertical_scroll_margin = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Horizontal Scroll Margin", - description: "The number of characters to keep on either side when scrolling with the mouse.", - field: Box::new(SettingField { - json_path: Some("horizontal_scroll_margin"), - pick: |settings_content| { - settings_content.editor.horizontal_scroll_margin.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.horizontal_scroll_margin = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Scroll Sensitivity", - description: "Scroll sensitivity multiplier for both horizontal and vertical scrolling.", - field: Box::new(SettingField { - json_path: Some("scroll_sensitivity"), - pick: |settings_content| { - settings_content.editor.scroll_sensitivity.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.scroll_sensitivity = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Fast Scroll Sensitivity", - description: "Fast scroll sensitivity multiplier for both horizontal and vertical scrolling.", - field: Box::new(SettingField { - json_path: Some("fast_scroll_sensitivity"), - pick: |settings_content| { - settings_content.editor.fast_scroll_sensitivity.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.fast_scroll_sensitivity = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Autoscroll On Clicks", - description: "Whether to scroll when clicking near the edge of the visible text area.", - field: Box::new(SettingField { - json_path: Some("autoscroll_on_clicks"), - pick: |settings_content| { - settings_content.editor.autoscroll_on_clicks.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.autoscroll_on_clicks = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Sticky Scroll", - description: "Whether to stick scopes to the top of the editor", - field: Box::new(SettingField { - json_path: Some("sticky_scroll.enabled"), - pick: |settings_content| { - settings_content.editor.sticky_scroll.as_ref().and_then(|sticky_scroll| sticky_scroll.enabled.as_ref()) - }, - write: |settings_content, value| { - settings_content.editor.sticky_scroll.get_or_insert_default().enabled = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Signature Help"), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Signature Help", - description: "Automatically show a signature help pop-up.", - field: Box::new(SettingField { - json_path: Some("auto_signature_help"), - pick: |settings_content| { - settings_content.editor.auto_signature_help.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.auto_signature_help = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Signature Help After Edits", - description: "Show the signature help pop-up after completions or bracket pairs are inserted.", - field: Box::new(SettingField { - json_path: Some("show_signature_help_after_edits"), - pick: |settings_content| { - settings_content - .editor - .show_signature_help_after_edits - .as_ref() - }, - write: |settings_content, value| { - settings_content.editor.show_signature_help_after_edits = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Snippet Sort Order", - description: "Determines how snippets are sorted relative to other completion items.", - field: Box::new(SettingField { - json_path: Some("snippet_sort_order"), - pick: |settings_content| { - settings_content.editor.snippet_sort_order.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.snippet_sort_order = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Hover Popover"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enabled", - description: "Show the informational hover box when moving the mouse over symbols in the editor.", - field: Box::new(SettingField { - json_path: Some("hover_popover_enabled"), - pick: |settings_content| { - settings_content.editor.hover_popover_enabled.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.hover_popover_enabled = value; - }, - }), - metadata: None, - files: USER, - }), - // todo(settings ui): add units to this number input - SettingsPageItem::SettingItem(SettingItem { - title: "Delay", - description: "Time to wait in milliseconds before showing the informational hover box.", - field: Box::new(SettingField { - json_path: Some("hover_popover_enabled"), - pick: |settings_content| { - settings_content.editor.hover_popover_delay.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.hover_popover_delay = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Drag And Drop Selection"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enabled", - description: "Enable drag and drop selection.", - field: Box::new(SettingField { - json_path: Some("drag_and_drop_selection.enabled"), - pick: |settings_content| { - settings_content - .editor - .drag_and_drop_selection - .as_ref() - .and_then(|drag_and_drop| drag_and_drop.enabled.as_ref()) - }, - write: |settings_content, value| { - settings_content - .editor - .drag_and_drop_selection - .get_or_insert_default() - .enabled = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Delay", - description: "Delay in milliseconds before drag and drop selection starts.", - field: Box::new(SettingField { - json_path: Some("drag_and_drop_selection.delay"), - pick: |settings_content| { - settings_content - .editor - .drag_and_drop_selection - .as_ref() - .and_then(|drag_and_drop| drag_and_drop.delay.as_ref()) - }, - write: |settings_content, value| { - settings_content - .editor - .drag_and_drop_selection - .get_or_insert_default() - .delay = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Gutter"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Line Numbers", - description: "Show line numbers in the gutter.", - field: Box::new(SettingField { - json_path: Some("gutter.line_numbers"), - pick: |settings_content| { - settings_content - .editor - .gutter - .as_ref() - .and_then(|gutter| gutter.line_numbers.as_ref()) - }, - write: |settings_content, value| { - settings_content - .editor - .gutter - .get_or_insert_default() - .line_numbers = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Relative Line Numbers", - description: "Controls line number display in the editor's gutter. \"disabled\" shows absolute line numbers, \"enabled\" shows relative line numbers for each absolute line, and \"wrapped\" shows relative line numbers for every line, absolute or wrapped.", - field: Box::new(SettingField { - json_path: Some("relative_line_numbers"), - pick: |settings_content| { - settings_content.editor.relative_line_numbers.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.relative_line_numbers = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Runnables", - description: "Show runnable buttons in the gutter.", - field: Box::new(SettingField { - json_path: Some("gutter.runnables"), - pick: |settings_content| { - settings_content - .editor - .gutter - .as_ref() - .and_then(|gutter| gutter.runnables.as_ref()) - }, - write: |settings_content, value| { - settings_content - .editor - .gutter - .get_or_insert_default() - .runnables = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Breakpoints", - description: "Show breakpoints in the gutter.", - field: Box::new(SettingField { - json_path: Some("gutter.breakpoints"), - pick: |settings_content| { - settings_content - .editor - .gutter - .as_ref() - .and_then(|gutter| gutter.breakpoints.as_ref()) - }, - write: |settings_content, value| { - settings_content - .editor - .gutter - .get_or_insert_default() - .breakpoints = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Folds", - description: "Show code folding controls in the gutter.", - field: Box::new(SettingField { - json_path: Some("gutter.folds"), - pick: |settings_content| { - settings_content - .editor - .gutter - .as_ref() - .and_then(|gutter| gutter.folds.as_ref()) - }, - write: |settings_content, value| { - settings_content.editor.gutter.get_or_insert_default().folds = - value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Min Line Number Digits", - description: "Minimum number of characters to reserve space for in the gutter.", - field: Box::new(SettingField { - json_path: Some("gutter.min_line_number_digits"), - pick: |settings_content| { - settings_content - .editor - .gutter - .as_ref() - .and_then(|gutter| gutter.min_line_number_digits.as_ref()) - }, - write: |settings_content, value| { - settings_content - .editor - .gutter - .get_or_insert_default() - .min_line_number_digits = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Inline Code Actions", - description: "Show code action button at start of buffer line.", - field: Box::new(SettingField { - json_path: Some("inline_code_actions"), - pick: |settings_content| { - settings_content.editor.inline_code_actions.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.inline_code_actions = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Scrollbar"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show", - description: "When to show the scrollbar in the editor.", - field: Box::new(SettingField { - json_path: Some("scrollbar"), - pick: |settings_content| { - settings_content.editor.scrollbar.as_ref()?.show.as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .scrollbar - .get_or_insert_default() - .show = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Cursors", - description: "Show cursor positions in the scrollbar.", - field: Box::new(SettingField { - json_path: Some("scrollbar.cursors"), - pick: |settings_content| { - settings_content.editor.scrollbar.as_ref()?.cursors.as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .scrollbar - .get_or_insert_default() - .cursors = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Git Diff", - description: "Show Git diff indicators in the scrollbar.", - field: Box::new(SettingField { - json_path: Some("scrollbar.git_diff"), - pick: |settings_content| { - settings_content - .editor - .scrollbar - .as_ref()? - .git_diff - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .scrollbar - .get_or_insert_default() - .git_diff = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Search Results", - description: "Show buffer search result indicators in the scrollbar.", - field: Box::new(SettingField { - json_path: Some("scrollbar.search_results"), - pick: |settings_content| { - settings_content - .editor - .scrollbar - .as_ref()? - .search_results - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .scrollbar - .get_or_insert_default() - .search_results = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Selected Text", - description: "Show selected text occurrences in the scrollbar.", - field: Box::new(SettingField { - json_path: Some("scrollbar.selected_text"), - pick: |settings_content| { - settings_content - .editor - .scrollbar - .as_ref()? - .selected_text - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .scrollbar - .get_or_insert_default() - .selected_text = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Selected Symbol", - description: "Show selected symbol occurrences in the scrollbar.", - field: Box::new(SettingField { - json_path: Some("scrollbar.selected_symbol"), - pick: |settings_content| { - settings_content - .editor - .scrollbar - .as_ref()? - .selected_symbol - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .scrollbar - .get_or_insert_default() - .selected_symbol = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Diagnostics", - description: "Which diagnostic indicators to show in the scrollbar.", - field: Box::new(SettingField { - json_path: Some("scrollbar.diagnostics"), - pick: |settings_content| { - settings_content - .editor - .scrollbar - .as_ref()? - .diagnostics - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .scrollbar - .get_or_insert_default() - .diagnostics = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Horizontal Scrollbar", - description: "When false, forcefully disables the horizontal scrollbar.", - field: Box::new(SettingField { - json_path: Some("scrollbar.axes.horizontal"), - pick: |settings_content| { - settings_content - .editor - .scrollbar - .as_ref()? - .axes - .as_ref()? - .horizontal - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .scrollbar - .get_or_insert_default() - .axes - .get_or_insert_default() - .horizontal = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Vertical Scrollbar", - description: "When false, forcefully disables the vertical scrollbar.", - field: Box::new(SettingField { - json_path: Some("scrollbar.axes.vertical"), - pick: |settings_content| { - settings_content - .editor - .scrollbar - .as_ref()? - .axes - .as_ref()? - .vertical - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .scrollbar - .get_or_insert_default() - .axes - .get_or_insert_default() - .vertical = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Minimap"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show", - description: "When to show the minimap in the editor.", - field: Box::new(SettingField { - json_path: Some("minimap.show"), - pick: |settings_content| { - settings_content.editor.minimap.as_ref()?.show.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.minimap.get_or_insert_default().show = - value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Display In", - description: "Where to show the minimap in the editor.", - field: Box::new(SettingField { - json_path: Some("minimap.display_in"), - pick: |settings_content| { - settings_content - .editor - .minimap - .as_ref()? - .display_in - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .minimap - .get_or_insert_default() - .display_in = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Thumb", - description: "When to show the minimap thumb.", - field: Box::new(SettingField { - json_path: Some("minimap.thumb"), - pick: |settings_content| { - settings_content.editor.minimap.as_ref()?.thumb.as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .minimap - .get_or_insert_default() - .thumb = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Thumb Border", - description: "Border style for the minimap's scrollbar thumb.", - field: Box::new(SettingField { - json_path: Some("minimap.thumb_border"), - pick: |settings_content| { - settings_content - .editor - .minimap - .as_ref()? - .thumb_border - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .minimap - .get_or_insert_default() - .thumb_border = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Current Line Highlight", - description: "How to highlight the current line in the minimap.", - field: Box::new(SettingField { - json_path: Some("minimap.current_line_highlight"), - pick: |settings_content| { - settings_content - .editor - .minimap - .as_ref() - .and_then(|minimap| minimap.current_line_highlight.as_ref()) - .or(settings_content.editor.current_line_highlight.as_ref()) - }, - write: |settings_content, value| { - settings_content - .editor - .minimap - .get_or_insert_default() - .current_line_highlight = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Max Width Columns", - description: "Maximum number of columns to display in the minimap.", - field: Box::new(SettingField { - json_path: Some("minimap.max_width_columns"), - pick: |settings_content| { - settings_content - .editor - .minimap - .as_ref()? - .max_width_columns - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .minimap - .get_or_insert_default() - .max_width_columns = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Toolbar"), - SettingsPageItem::SettingItem(SettingItem { - title: "Breadcrumbs", - description: "Show breadcrumbs.", - field: Box::new(SettingField { - json_path: Some("toolbar.breadcrumbs"), - pick: |settings_content| { - settings_content - .editor - .toolbar - .as_ref()? - .breadcrumbs - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .toolbar - .get_or_insert_default() - .breadcrumbs = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Quick Actions", - description: "Show quick action buttons (e.g., search, selection, editor controls, etc.).", - field: Box::new(SettingField { - json_path: Some("toolbar.quick_actions"), - pick: |settings_content| { - settings_content - .editor - .toolbar - .as_ref()? - .quick_actions - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .toolbar - .get_or_insert_default() - .quick_actions = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Selections Menu", - description: "Show the selections menu in the editor toolbar.", - field: Box::new(SettingField { - json_path: Some("toolbar.selections_menu"), - pick: |settings_content| { - settings_content - .editor - .toolbar - .as_ref()? - .selections_menu - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .toolbar - .get_or_insert_default() - .selections_menu = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Agent Review", - description: "Show agent review buttons in the editor toolbar.", - field: Box::new(SettingField { - json_path: Some("toolbar.agent_review"), - pick: |settings_content| { - settings_content - .editor - .toolbar - .as_ref()? - .agent_review - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .toolbar - .get_or_insert_default() - .agent_review = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Code Actions", - description: "Show code action buttons in the editor toolbar.", - field: Box::new(SettingField { - json_path: Some("toolbar.code_actions"), - pick: |settings_content| { - settings_content - .editor - .toolbar - .as_ref()? - .code_actions - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .toolbar - .get_or_insert_default() - .code_actions = value; - }, - }), - metadata: None, - files: USER, - }), - ]; - items.extend(language_settings_data()); - items - }, - }, - SettingsPage { - title: "Languages & Tools", - items: { - let mut items = vec![]; - items.extend(non_editor_language_settings_data()); - items.extend([ - SettingsPageItem::SectionHeader("File Types"), - SettingsPageItem::SettingItem(SettingItem { - title: "File Type Associations", - description: "A mapping from languages to files and file extensions that should be treated as that language.", - field: Box::new( - SettingField { - json_path: Some("file_type_associations"), - pick: |settings_content| { - settings_content.project.all_languages.file_types.as_ref() - }, - write: |settings_content, value| { - settings_content.project.all_languages.file_types = value; - - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - ]); - - items.extend([ - SettingsPageItem::SectionHeader("Diagnostics"), - SettingsPageItem::SettingItem(SettingItem { - title: "Max Severity", - description: "Which level to use to filter out diagnostics displayed in the editor.", - field: Box::new(SettingField { - json_path: Some("diagnostics_max_severity"), - pick: |settings_content| settings_content.editor.diagnostics_max_severity.as_ref(), - write: |settings_content, value| { - settings_content.editor.diagnostics_max_severity = value; - - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Include Warnings", - description: "Whether to show warnings or not by default.", - field: Box::new(SettingField { - json_path: Some("diagnostics.include_warnings"), - pick: |settings_content| { - settings_content.diagnostics.as_ref()?.include_warnings.as_ref() - }, - write: |settings_content, value| { - settings_content - - .diagnostics - .get_or_insert_default() - .include_warnings - = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Inline Diagnostics"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enabled", - description: "Whether to show diagnostics inline or not.", - field: Box::new(SettingField { - json_path: Some("diagnostics.inline.enabled"), - pick: |settings_content| { - settings_content.diagnostics.as_ref()?.inline.as_ref()?.enabled.as_ref() - }, - write: |settings_content, value| { - settings_content - - .diagnostics - .get_or_insert_default() - .inline - .get_or_insert_default() - .enabled - = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Update Debounce", - description: "The delay in milliseconds to show inline diagnostics after the last diagnostic update.", - field: Box::new(SettingField { - json_path: Some("diagnostics.inline.update_debounce_ms"), - pick: |settings_content| { - settings_content.diagnostics.as_ref()?.inline.as_ref()?.update_debounce_ms.as_ref() - }, - write: |settings_content, value| { - settings_content - - .diagnostics - .get_or_insert_default() - .inline - .get_or_insert_default() - .update_debounce_ms - = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Padding", - description: "The amount of padding between the end of the source line and the start of the inline diagnostic.", - field: Box::new(SettingField { - json_path: Some("diagnostics.inline.padding"), - pick: |settings_content| { - settings_content.diagnostics.as_ref()?.inline.as_ref()?.padding.as_ref() - }, - write: |settings_content, value| { - settings_content - - .diagnostics - .get_or_insert_default() - .inline - .get_or_insert_default() - .padding - = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Minimum Column", - description: "The minimum column at which to display inline diagnostics.", - field: Box::new(SettingField { - json_path: Some("diagnostics.inline.min_column"), - pick: |settings_content| { - settings_content.diagnostics.as_ref()?.inline.as_ref()?.min_column.as_ref() - }, - write: |settings_content, value| { - settings_content - - .diagnostics - .get_or_insert_default() - .inline - .get_or_insert_default() - .min_column - = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("LSP Pull Diagnostics"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enabled", - description: "Whether to pull for language server-powered diagnostics or not.", - field: Box::new(SettingField { - json_path: Some("diagnostics.lsp_pull_diagnostics.enabled"), - pick: |settings_content| { - settings_content.diagnostics.as_ref()?.lsp_pull_diagnostics.as_ref()?.enabled.as_ref() - }, - write: |settings_content, value| { - settings_content - - .diagnostics - .get_or_insert_default() - .lsp_pull_diagnostics - .get_or_insert_default() - .enabled - = value; - }, - }), - metadata: None, - files: USER, - }), - // todo(settings_ui): Needs unit - SettingsPageItem::SettingItem(SettingItem { - title: "Debounce", - description: "Minimum time to wait before pulling diagnostics from the language server(s).", - field: Box::new(SettingField { - json_path: Some("diagnostics.lsp_pull_diagnostics.debounce_ms"), - pick: |settings_content| { - settings_content.diagnostics.as_ref()?.lsp_pull_diagnostics.as_ref()?.debounce_ms.as_ref() - }, - write: |settings_content, value| { - settings_content - - .diagnostics - .get_or_insert_default() - .lsp_pull_diagnostics - .get_or_insert_default() - .debounce_ms - = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("LSP Highlights"), - SettingsPageItem::SettingItem(SettingItem { - title: "Debounce", - description: "The debounce delay before querying highlights from the language.", - field: Box::new(SettingField { - json_path: Some("lsp_highlight_debounce"), - pick: |settings_content| settings_content.editor.lsp_highlight_debounce.as_ref(), - write: |settings_content, value| { - settings_content.editor.lsp_highlight_debounce = value; - }, - }), - metadata: None, - files: USER, - }), - ]); - - // todo(settings_ui): Refresh on extension (un)/installed - // Note that `crates/json_schema_store` solves the same problem, there is probably a way to unify the two - items.push(SettingsPageItem::SectionHeader(LANGUAGES_SECTION_HEADER)); - items.extend(all_language_names(cx).into_iter().map(|language_name| { - SettingsPageItem::SubPageLink(SubPageLink { - title: language_name, - files: USER | PROJECT, - render: Arc::new(|this, window, cx| { - this.render_sub_page_items( - language_settings_data() - .iter() - .chain(non_editor_language_settings_data().iter()) - .chain(edit_prediction_language_settings_section().iter()) - .enumerate(), - None, - window, - cx, - ) - .into_any_element() - }), - }) - })); - items - }, - }, - SettingsPage { - title: "Search & Files", - items: vec![ - SettingsPageItem::SectionHeader("Search"), - SettingsPageItem::SettingItem(SettingItem { - title: "Whole Word", - description: "Search for whole words by default.", - field: Box::new(SettingField { - json_path: Some("search.whole_word"), - pick: |settings_content| { - settings_content.editor.search.as_ref()?.whole_word.as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .search - .get_or_insert_default() - .whole_word = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Case Sensitive", - description: "Search case-sensitively by default.", - field: Box::new(SettingField { - json_path: Some("search.case_sensitive"), - pick: |settings_content| { - settings_content - .editor - .search - .as_ref()? - .case_sensitive - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .search - .get_or_insert_default() - .case_sensitive = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Use Smartcase Search", - description: "Whether to automatically enable case-sensitive search based on the search query.", - field: Box::new(SettingField { - json_path: Some("use_smartcase_search"), - pick: |settings_content| { - settings_content.editor.use_smartcase_search.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.use_smartcase_search = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Include Ignored", - description: "Include ignored files in search results by default.", - field: Box::new(SettingField { - json_path: Some("search.include_ignored"), - pick: |settings_content| { - settings_content - .editor - .search - .as_ref()? - .include_ignored - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .search - .get_or_insert_default() - .include_ignored = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Regex", - description: "Use regex search by default.", - field: Box::new(SettingField { - json_path: Some("search.regex"), - pick: |settings_content| { - settings_content.editor.search.as_ref()?.regex.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.search.get_or_insert_default().regex = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Search Wrap", - description: "Whether the editor search results will loop.", - field: Box::new(SettingField { - json_path: Some("search_wrap"), - pick: |settings_content| settings_content.editor.search_wrap.as_ref(), - write: |settings_content, value| { - settings_content.editor.search_wrap = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Center on Match", - description: "Whether to center the current match in the editor", - field: Box::new(SettingField { - json_path: Some("editor.search.center_on_match"), - pick: |settings_content| { - settings_content - .editor - .search - .as_ref() - .and_then(|search| search.center_on_match.as_ref()) - }, - write: |settings_content, value| { - settings_content - .editor - .search - .get_or_insert_default() - .center_on_match = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Seed Search Query From Cursor", - description: "When to populate a new search's query based on the text under the cursor.", - field: Box::new(SettingField { - json_path: Some("seed_search_query_from_cursor"), - pick: |settings_content| { - settings_content - .editor - .seed_search_query_from_cursor - .as_ref() - }, - write: |settings_content, value| { - settings_content.editor.seed_search_query_from_cursor = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("File Finder"), - // todo: null by default - SettingsPageItem::SettingItem(SettingItem { - title: "Include Ignored in Search", - description: "Use gitignored files when searching.", - field: Box::new( - SettingField { - json_path: Some("file_finder.include_ignored"), - pick: |settings_content| { - settings_content - .file_finder - .as_ref()? - .include_ignored - .as_ref() - }, - write: |settings_content, value| { - settings_content - .file_finder - .get_or_insert_default() - .include_ignored = value; - }, - } - ), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "File Icons", - description: "Show file icons in the file finder.", - field: Box::new(SettingField { - json_path: Some("file_finder.file_icons"), - pick: |settings_content| { - settings_content.file_finder.as_ref()?.file_icons.as_ref() - }, - write: |settings_content, value| { - settings_content - .file_finder - .get_or_insert_default() - .file_icons = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Modal Max Width", - description: "Determines how much space the file finder can take up in relation to the available window width.", - field: Box::new(SettingField { - json_path: Some("file_finder.modal_max_width"), - pick: |settings_content| { - settings_content - .file_finder - .as_ref()? - .modal_max_width - .as_ref() - }, - write: |settings_content, value| { - settings_content - .file_finder - .get_or_insert_default() - .modal_max_width = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Skip Focus For Active In Search", - description: "Whether the file finder should skip focus for the active file in search results.", - field: Box::new(SettingField { - json_path: Some("file_finder.skip_focus_for_active_in_search"), - pick: |settings_content| { - settings_content - .file_finder - .as_ref()? - .skip_focus_for_active_in_search - .as_ref() - }, - write: |settings_content, value| { - settings_content - .file_finder - .get_or_insert_default() - .skip_focus_for_active_in_search = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Git Status", - description: "Show the Git status in the file finder.", - field: Box::new(SettingField { - json_path: Some("file_finder.git_status"), - pick: |settings_content| { - settings_content.file_finder.as_ref()?.git_status.as_ref() - }, - write: |settings_content, value| { - settings_content - .file_finder - .get_or_insert_default() - .git_status = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("File Scan"), - SettingsPageItem::SettingItem(SettingItem { - title: "File Scan Exclusions", - description: "Files or globs of files that will be excluded by Zed entirely. They will be skipped during file scans, file searches, and not be displayed in the project file tree. Takes precedence over \"File Scan Inclusions\"", - field: Box::new( - SettingField { - json_path: Some("file_scan_exclusions"), - pick: |settings_content| { - settings_content - .project - .worktree - .file_scan_exclusions - .as_ref() - }, - write: |settings_content, value| { - settings_content.project.worktree.file_scan_exclusions = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "File Scan Inclusions", - description: "Files or globs of files that will be included by Zed, even when ignored by git. This is useful for files that are not tracked by git, but are still important to your project. Note that globs that are overly broad can slow down Zed's file scanning. \"File Scan Exclusions\" takes precedence over these inclusions", - field: Box::new( - SettingField { - json_path: Some("file_scan_inclusions"), - pick: |settings_content| { - settings_content - .project - .worktree - .file_scan_inclusions - .as_ref() - }, - write: |settings_content, value| { - settings_content.project.worktree.file_scan_inclusions = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Restore File State", - description: "Restore previous file state when reopening.", - field: Box::new(SettingField { - json_path: Some("restore_on_file_reopen"), - pick: |settings_content| { - settings_content.workspace.restore_on_file_reopen.as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.restore_on_file_reopen = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Close on File Delete", - description: "Automatically close files that have been deleted.", - field: Box::new(SettingField { - json_path: Some("close_on_file_delete"), - pick: |settings_content| { - settings_content.workspace.close_on_file_delete.as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.close_on_file_delete = value; - }, - }), - metadata: None, - files: USER, - }), - ], - }, - SettingsPage { - title: "Window & Layout", - items: vec![ - SettingsPageItem::SectionHeader("Status Bar"), - SettingsPageItem::SettingItem(SettingItem { - title: "Project Panel Button", - description: "Show the project panel button in the status bar.", - field: Box::new(SettingField { - json_path: Some("project_panel.button"), - pick: |settings_content| { - settings_content.project_panel.as_ref()?.button.as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Active Language Button", - description: "Show the active language button in the status bar.", - field: Box::new(SettingField { - json_path: Some("status_bar.active_language_button"), - pick: |settings_content| { - settings_content - .status_bar - .as_ref()? - .active_language_button - .as_ref() - }, - write: |settings_content, value| { - settings_content - .status_bar - .get_or_insert_default() - .active_language_button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Cursor Position Button", - description: "Show the cursor position button in the status bar.", - field: Box::new(SettingField { - json_path: Some("status_bar.cursor_position_button"), - pick: |settings_content| { - settings_content - .status_bar - .as_ref()? - .cursor_position_button - .as_ref() - }, - write: |settings_content, value| { - settings_content - .status_bar - .get_or_insert_default() - .cursor_position_button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Terminal Button", - description: "Show the terminal button in the status bar.", - field: Box::new(SettingField { - json_path: Some("terminal.button"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.button.as_ref() - }, - write: |settings_content, value| { - settings_content.terminal.get_or_insert_default().button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Diagnostics Button", - description: "Show the project diagnostics button in the status bar.", - field: Box::new(SettingField { - json_path: Some("diagnostics.button"), - pick: |settings_content| { - settings_content.diagnostics.as_ref()?.button.as_ref() - }, - write: |settings_content, value| { - settings_content.diagnostics.get_or_insert_default().button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Project Search Button", - description: "Show the project search button in the status bar.", - field: Box::new(SettingField { - json_path: Some("search.button"), - pick: |settings_content| { - settings_content.editor.search.as_ref()?.button.as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .search - .get_or_insert_default() - .button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Debugger Button", - description: "Show the debugger button in the status bar.", - field: Box::new(SettingField { - json_path: Some("debugger.button"), - pick: |settings_content| { - settings_content.debugger.as_ref()?.button.as_ref() - }, - write: |settings_content, value| { - settings_content.debugger.get_or_insert_default().button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Title Bar"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Branch Icon", - description: "Show the branch icon beside branch switcher in the titlebar.", - field: Box::new(SettingField { - json_path: Some("title_bar.show_branch_icon"), - pick: |settings_content| { - settings_content - .title_bar - .as_ref()? - .show_branch_icon - .as_ref() - }, - write: |settings_content, value| { - settings_content - .title_bar - .get_or_insert_default() - .show_branch_icon = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Branch Name", - description: "Show the branch name button in the titlebar.", - field: Box::new(SettingField { - json_path: Some("title_bar.show_branch_name"), - pick: |settings_content| { - settings_content - .title_bar - .as_ref()? - .show_branch_name - .as_ref() - }, - write: |settings_content, value| { - settings_content - .title_bar - .get_or_insert_default() - .show_branch_name = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Project Items", - description: "Show the project host and name in the titlebar.", - field: Box::new(SettingField { - json_path: Some("title_bar.show_project_items"), - pick: |settings_content| { - settings_content - .title_bar - .as_ref()? - .show_project_items - .as_ref() - }, - write: |settings_content, value| { - settings_content - .title_bar - .get_or_insert_default() - .show_project_items = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Onboarding Banner", - description: "Show banners announcing new features in the titlebar.", - field: Box::new(SettingField { - json_path: Some("title_bar.show_onboarding_banner"), - pick: |settings_content| { - settings_content - .title_bar - .as_ref()? - .show_onboarding_banner - .as_ref() - }, - write: |settings_content, value| { - settings_content - .title_bar - .get_or_insert_default() - .show_onboarding_banner = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show User Picture", - description: "Show user picture in the titlebar.", - field: Box::new(SettingField { - json_path: Some("title_bar.show_user_picture"), - pick: |settings_content| { - settings_content - .title_bar - .as_ref()? - .show_user_picture - .as_ref() - }, - write: |settings_content, value| { - settings_content - .title_bar - .get_or_insert_default() - .show_user_picture = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Sign In", - description: "Show the sign in button in the titlebar.", - field: Box::new(SettingField { - json_path: Some("title_bar.show_sign_in"), - pick: |settings_content| { - settings_content.title_bar.as_ref()?.show_sign_in.as_ref() - }, - write: |settings_content, value| { - settings_content - .title_bar - .get_or_insert_default() - .show_sign_in = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Menus", - description: "Show the menus in the titlebar.", - field: Box::new(SettingField { - json_path: Some("title_bar.show_menus"), - pick: |settings_content| { - settings_content.title_bar.as_ref()?.show_menus.as_ref() - }, - write: |settings_content, value| { - settings_content - .title_bar - .get_or_insert_default() - .show_menus = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Tab Bar"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Tab Bar", - description: "Show the tab bar in the editor.", - field: Box::new(SettingField { - json_path: Some("tab_bar.show"), - pick: |settings_content| settings_content.tab_bar.as_ref()?.show.as_ref(), - write: |settings_content, value| { - settings_content.tab_bar.get_or_insert_default().show = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Git Status In Tabs", - description: "Show the Git file status on a tab item.", - field: Box::new(SettingField { - json_path: Some("tabs.git_status"), - pick: |settings_content| { - settings_content.tabs.as_ref()?.git_status.as_ref() - }, - write: |settings_content, value| { - settings_content.tabs.get_or_insert_default().git_status = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show File Icons In Tabs", - description: "Show the file icon for a tab.", - field: Box::new(SettingField { - json_path: Some("tabs.file_icons"), - pick: |settings_content| { - settings_content.tabs.as_ref()?.file_icons.as_ref() - }, - write: |settings_content, value| { - settings_content.tabs.get_or_insert_default().file_icons = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Tab Close Position", - description: "Position of the close button in a tab.", - field: Box::new(SettingField { - json_path: Some("tabs.close_position"), - pick: |settings_content| { - settings_content.tabs.as_ref()?.close_position.as_ref() - }, - write: |settings_content, value| { - settings_content.tabs.get_or_insert_default().close_position = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Maximum Tabs", - description: "Maximum open tabs in a pane. Will not close an unsaved tab.", - // todo(settings_ui): The default for this value is null and it's use in code - // is complex, so I'm going to come back to this later - field: Box::new( - SettingField { - json_path: Some("max_tabs"), - pick: |settings_content| settings_content.workspace.max_tabs.as_ref(), - write: |settings_content, value| { - settings_content.workspace.max_tabs = value; - }, - } - .unimplemented(), - ), - metadata: None, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Navigation History Buttons", - description: "Show the navigation history buttons in the tab bar.", - field: Box::new(SettingField { - json_path: Some("tab_bar.show_nav_history_buttons"), - pick: |settings_content| { - settings_content - .tab_bar - .as_ref()? - .show_nav_history_buttons - .as_ref() - }, - write: |settings_content, value| { - settings_content - .tab_bar - .get_or_insert_default() - .show_nav_history_buttons = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Tab Bar Buttons", - description: "Show the tab bar buttons (New, Split Pane, Zoom).", - field: Box::new(SettingField { - json_path: Some("tab_bar.show_tab_bar_buttons"), - pick: |settings_content| { - settings_content - .tab_bar - .as_ref()? - .show_tab_bar_buttons - .as_ref() - }, - write: |settings_content, value| { - settings_content - .tab_bar - .get_or_insert_default() - .show_tab_bar_buttons = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Tab Settings"), - SettingsPageItem::SettingItem(SettingItem { - title: "Activate On Close", - description: "What to do after closing the current tab.", - field: Box::new(SettingField { - json_path: Some("tabs.activate_on_close"), - pick: |settings_content| { - settings_content.tabs.as_ref()?.activate_on_close.as_ref() - }, - write: |settings_content, value| { - settings_content - .tabs - .get_or_insert_default() - .activate_on_close = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Tab Show Diagnostics", - description: "Which files containing diagnostic errors/warnings to mark in the tabs.", - field: Box::new(SettingField { - json_path: Some("tabs.show_diagnostics"), - pick: |settings_content| { - settings_content.tabs.as_ref()?.show_diagnostics.as_ref() - }, - write: |settings_content, value| { - settings_content - .tabs - .get_or_insert_default() - .show_diagnostics = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Close Button", - description: "Controls the appearance behavior of the tab's close button.", - field: Box::new(SettingField { - json_path: Some("tabs.show_close_button"), - pick: |settings_content| { - settings_content.tabs.as_ref()?.show_close_button.as_ref() - }, - write: |settings_content, value| { - settings_content - .tabs - .get_or_insert_default() - .show_close_button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Preview Tabs"), - SettingsPageItem::SettingItem(SettingItem { - title: "Preview Tabs Enabled", - description: "Show opened editors as preview tabs.", - field: Box::new(SettingField { - json_path: Some("preview_tabs.enabled"), - pick: |settings_content| { - settings_content.preview_tabs.as_ref()?.enabled.as_ref() - }, - write: |settings_content, value| { - settings_content - .preview_tabs - .get_or_insert_default() - .enabled = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Enable Preview From Project Panel", - description: "Whether to open tabs in preview mode when opened from the project panel with a single click.", - field: Box::new(SettingField { - json_path: Some("preview_tabs.enable_preview_from_project_panel"), - pick: |settings_content| { - settings_content - .preview_tabs - .as_ref()? - .enable_preview_from_project_panel - .as_ref() - }, - write: |settings_content, value| { - settings_content - .preview_tabs - .get_or_insert_default() - .enable_preview_from_project_panel = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Enable Preview From File Finder", - description: "Whether to open tabs in preview mode when selected from the file finder.", - field: Box::new(SettingField { - json_path: Some("preview_tabs.enable_preview_from_file_finder"), - pick: |settings_content| { - settings_content - .preview_tabs - .as_ref()? - .enable_preview_from_file_finder - .as_ref() - }, - write: |settings_content, value| { - settings_content - .preview_tabs - .get_or_insert_default() - .enable_preview_from_file_finder = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Enable Preview From Multibuffer", - description: "Whether to open tabs in preview mode when opened from a multibuffer.", - field: Box::new(SettingField { - json_path: Some("preview_tabs.enable_preview_from_multibuffer"), - pick: |settings_content| { - settings_content - .preview_tabs - .as_ref()? - .enable_preview_from_multibuffer - .as_ref() - }, - write: |settings_content, value| { - settings_content - .preview_tabs - .get_or_insert_default() - .enable_preview_from_multibuffer = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Enable Preview Multibuffer From Code Navigation", - description: "Whether to open tabs in preview mode when code navigation is used to open a multibuffer.", - field: Box::new(SettingField { - json_path: Some("preview_tabs.enable_preview_multibuffer_from_code_navigation"), - pick: |settings_content| { - settings_content - .preview_tabs - .as_ref()? - .enable_preview_multibuffer_from_code_navigation - .as_ref() - }, - write: |settings_content, value| { - settings_content - .preview_tabs - .get_or_insert_default() - .enable_preview_multibuffer_from_code_navigation = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Enable Preview File From Code Navigation", - description: "Whether to open tabs in preview mode when code navigation is used to open a single file.", - field: Box::new(SettingField { - json_path: Some("preview_tabs.enable_preview_file_from_code_navigation"), - pick: |settings_content| { - settings_content - .preview_tabs - .as_ref()? - .enable_preview_file_from_code_navigation - .as_ref() - }, - write: |settings_content, value| { - settings_content - .preview_tabs - .get_or_insert_default() - .enable_preview_file_from_code_navigation = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Enable Keep Preview On Code Navigation", - description: "Whether to keep tabs in preview mode when code navigation is used to navigate away from them. If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one.", - field: Box::new(SettingField { - json_path: Some("preview_tabs.enable_keep_preview_on_code_navigation"), - pick: |settings_content| { - settings_content - .preview_tabs - .as_ref()? - .enable_keep_preview_on_code_navigation - .as_ref() - }, - write: |settings_content, value| { - settings_content - .preview_tabs - .get_or_insert_default() - .enable_keep_preview_on_code_navigation = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Layout"), - SettingsPageItem::SettingItem(SettingItem { - title: "Bottom Dock Layout", - description: "Layout mode for the bottom dock.", - field: Box::new(SettingField { - json_path: Some("bottom_dock_layout"), - pick: |settings_content| { - settings_content.workspace.bottom_dock_layout.as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.bottom_dock_layout = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Centered Layout Left Padding", - description: "Left padding for centered layout.", - field: Box::new(SettingField { - json_path: Some("centered_layout.left_padding"), - pick: |settings_content| { - settings_content - .workspace - .centered_layout - .as_ref()? - .left_padding - .as_ref() - }, - write: |settings_content, value| { - settings_content - .workspace - .centered_layout - .get_or_insert_default() - .left_padding = value; - }, - }), - metadata: None, - }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Centered Layout Right Padding", - description: "Right padding for centered layout.", - field: Box::new(SettingField { - json_path: Some("centered_layout.right_padding"), - pick: |settings_content| { - settings_content - .workspace - .centered_layout - .as_ref()? - .right_padding - .as_ref() - }, - write: |settings_content, value| { - settings_content - .workspace - .centered_layout - .get_or_insert_default() - .right_padding = value; - }, - }), - metadata: None, - }), - SettingsPageItem::SectionHeader("Window"), - // todo(settings_ui): Should we filter by platform.as_ref()? - SettingsPageItem::SettingItem(SettingItem { - title: "Use System Window Tabs", - description: "(macOS only) whether to allow Windows to tab together.", - field: Box::new(SettingField { - json_path: Some("use_system_window_tabs"), - pick: |settings_content| { - settings_content.workspace.use_system_window_tabs.as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.use_system_window_tabs = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Window Decorations", - description: "(Linux only) whether Zed or your compositor should draw window decorations.", - field: Box::new(SettingField { - json_path: Some("window_decorations"), - pick: |settings_content| { - settings_content.workspace.window_decorations.as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.window_decorations = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Pane Modifiers"), - SettingsPageItem::SettingItem(SettingItem { - title: "Inactive Opacity", - description: "Opacity of inactive panels (0.0 - 1.0).", - field: Box::new(SettingField { - json_path: Some("active_pane_modifiers.inactive_opacity"), - pick: |settings_content| { - settings_content - .workspace - .active_pane_modifiers - .as_ref()? - .inactive_opacity - .as_ref() - }, - write: |settings_content, value| { - settings_content - .workspace - .active_pane_modifiers - .get_or_insert_default() - .inactive_opacity = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Border Size", - description: "Size of the border surrounding the active pane.", - field: Box::new(SettingField { - json_path: Some("active_pane_modifiers.border_size"), - pick: |settings_content| { - settings_content - .workspace - .active_pane_modifiers - .as_ref()? - .border_size - .as_ref() - }, - write: |settings_content, value| { - settings_content - .workspace - .active_pane_modifiers - .get_or_insert_default() - .border_size = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Zoomed Padding", - description: "Show padding for zoomed panes.", - field: Box::new(SettingField { - json_path: Some("zoomed_padding"), - pick: |settings_content| settings_content.workspace.zoomed_padding.as_ref(), - write: |settings_content, value| { - settings_content.workspace.zoomed_padding = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Pane Split Direction"), - SettingsPageItem::SettingItem(SettingItem { - title: "Vertical Split Direction", - description: "Direction to split vertically.", - field: Box::new(SettingField { - json_path: Some("pane_split_direction_vertical"), - pick: |settings_content| { - settings_content - .workspace - .pane_split_direction_vertical - .as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.pane_split_direction_vertical = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Horizontal Split Direction", - description: "Direction to split horizontally.", - field: Box::new(SettingField { - json_path: Some("pane_split_direction_horizontal"), - pick: |settings_content| { - settings_content - .workspace - .pane_split_direction_horizontal - .as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.pane_split_direction_horizontal = value; - }, - }), - metadata: None, - files: USER, - }), - ], - }, - SettingsPage { - title: "Panels", - items: vec![ - SettingsPageItem::SectionHeader("Project Panel"), - SettingsPageItem::SettingItem(SettingItem { - title: "Project Panel Dock", - description: "Where to dock the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.dock"), - pick: |settings_content| { - settings_content.project_panel.as_ref()?.dock.as_ref() - }, - write: |settings_content, value| { - settings_content.project_panel.get_or_insert_default().dock = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Project Panel Default Width", - description: "Default width of the project panel in pixels.", - field: Box::new(SettingField { - json_path: Some("project_panel.default_width"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .default_width - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .default_width = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Hide .gitignore", - description: "Whether to hide the gitignore entries in the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.hide_gitignore"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .hide_gitignore - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .hide_gitignore = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Entry Spacing", - description: "Spacing between worktree entries in the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.entry_spacing"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .entry_spacing - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .entry_spacing = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "File Icons", - description: "Show file icons in the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.file_icons"), - pick: |settings_content| { - settings_content.project_panel.as_ref()?.file_icons.as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .file_icons = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Folder Icons", - description: "Whether to show folder icons or chevrons for directories in the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.folder_icons"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .folder_icons - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .folder_icons = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Git Status", - description: "Show the Git status in the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.git_status"), - pick: |settings_content| { - settings_content.project_panel.as_ref()?.git_status.as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .git_status = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Indent Size", - description: "Amount of indentation for nested items.", - field: Box::new(SettingField { - json_path: Some("project_panel.indent_size"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .indent_size - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .indent_size = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Reveal Entries", - description: "Whether to reveal entries in the project panel automatically when a corresponding project entry becomes active.", - field: Box::new(SettingField { - json_path: Some("project_panel.auto_reveal_entries"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .auto_reveal_entries - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .auto_reveal_entries = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Starts Open", - description: "Whether the project panel should open on startup.", - field: Box::new(SettingField { - json_path: Some("project_panel.starts_open"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .starts_open - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .starts_open = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Fold Directories", - description: "Whether to fold directories automatically and show compact folders when a directory has only one subdirectory inside.", - field: Box::new(SettingField { - json_path: Some("project_panel.auto_fold_dirs"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .auto_fold_dirs - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .auto_fold_dirs = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Scrollbar", - description: "Show the scrollbar in the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.scrollbar.show"), - pick: |settings_content| { - show_scrollbar_or_editor(settings_content, |settings_content| { - settings_content - .project_panel - .as_ref()? - .scrollbar - .as_ref()? - .show - .as_ref() - }) - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .scrollbar - .get_or_insert_default() - .show = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Diagnostics", - description: "Which files containing diagnostic errors/warnings to mark in the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.show_diagnostics"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .show_diagnostics - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .show_diagnostics = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Sticky Scroll", - description: "Whether to stick parent directories at top of the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.sticky_scroll"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .sticky_scroll - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .sticky_scroll = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Show Indent Guides", - description: "Show indent guides in the project panel.", - field: Box::new( - SettingField { - json_path: Some("project_panel.indent_guides.show"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .indent_guides - .as_ref()? - .show - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .indent_guides - .get_or_insert_default() - .show = value; - }, - } - ), - metadata: None, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Drag and Drop", - description: "Whether to enable drag-and-drop operations in the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.drag_and_drop"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .drag_and_drop - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .drag_and_drop = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Hide Root", - description: "Whether to hide the root entry when only one folder is open in the window.", - field: Box::new(SettingField { - json_path: Some("project_panel.drag_and_drop"), - pick: |settings_content| { - settings_content.project_panel.as_ref()?.hide_root.as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .hide_root = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Hide Hidden", - description: "Whether to hide the hidden entries in the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.hide_hidden"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .hide_hidden - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .hide_hidden = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Hidden Files", - description: "Globs to match files that will be considered \"hidden\" and can be hidden from the project panel.", - field: Box::new( - SettingField { - json_path: Some("worktree.hidden_files"), - pick: |settings_content| { - settings_content.project.worktree.hidden_files.as_ref() - }, - write: |settings_content, value| { - settings_content.project.worktree.hidden_files = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Auto Open Files"), - SettingsPageItem::SettingItem(SettingItem { - title: "On Create", - description: "Whether to automatically open newly created files in the editor.", - field: Box::new(SettingField { - json_path: Some("project_panel.auto_open.on_create"), - pick: |settings_content| { - settings_content.project_panel.as_ref()?.auto_open.as_ref()?.on_create.as_ref() - }, - write: |settings_content, value| { - settings_content.project_panel.get_or_insert_default().auto_open.get_or_insert_default().on_create = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "On Paste", - description: "Whether to automatically open files after pasting or duplicating them.", - field: Box::new(SettingField { - json_path: Some("project_panel.auto_open.on_paste"), - pick: |settings_content| { - settings_content.project_panel.as_ref()?.auto_open.as_ref()?.on_paste.as_ref() - }, - write: |settings_content, value| { - settings_content.project_panel.get_or_insert_default().auto_open.get_or_insert_default().on_paste = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "On Drop", - description: "Whether to automatically open files dropped from external sources.", - field: Box::new(SettingField { - json_path: Some("project_panel.auto_open.on_drop"), - pick: |settings_content| { - settings_content.project_panel.as_ref()?.auto_open.as_ref()?.on_drop.as_ref() - }, - write: |settings_content, value| { - settings_content.project_panel.get_or_insert_default().auto_open.get_or_insert_default().on_drop = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Sort Mode", - description: "Sort order for entries in the project panel.", - field: Box::new(SettingField { - pick: |settings_content| { - settings_content.project_panel.as_ref()?.sort_mode.as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .sort_mode = value; - }, - json_path: Some("project_panel.sort_mode"), - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Terminal Panel"), - SettingsPageItem::SettingItem(SettingItem { - title: "Terminal Dock", - description: "Where to dock the terminal panel.", - field: Box::new(SettingField { - json_path: Some("terminal.dock"), - pick: |settings_content| settings_content.terminal.as_ref()?.dock.as_ref(), - write: |settings_content, value| { - settings_content.terminal.get_or_insert_default().dock = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Outline Panel"), - SettingsPageItem::SettingItem(SettingItem { - title: "Outline Panel Button", - description: "Show the outline panel button in the status bar.", - field: Box::new(SettingField { - json_path: Some("outline_panel.button"), - pick: |settings_content| { - settings_content.outline_panel.as_ref()?.button.as_ref() - }, - write: |settings_content, value| { - settings_content - .outline_panel - .get_or_insert_default() - .button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Outline Panel Dock", - description: "Where to dock the outline panel.", - field: Box::new(SettingField { - json_path: Some("outline_panel.dock"), - pick: |settings_content| { - settings_content.outline_panel.as_ref()?.dock.as_ref() - }, - write: |settings_content, value| { - settings_content.outline_panel.get_or_insert_default().dock = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Outline Panel Default Width", - description: "Default width of the outline panel in pixels.", - field: Box::new(SettingField { - json_path: Some("outline_panel.default_width"), - pick: |settings_content| { - settings_content - .outline_panel - .as_ref()? - .default_width - .as_ref() - }, - write: |settings_content, value| { - settings_content - .outline_panel - .get_or_insert_default() - .default_width = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "File Icons", - description: "Show file icons in the outline panel.", - field: Box::new(SettingField { - json_path: Some("outline_panel.file_icons"), - pick: |settings_content| { - settings_content.outline_panel.as_ref()?.file_icons.as_ref() - }, - write: |settings_content, value| { - settings_content - .outline_panel - .get_or_insert_default() - .file_icons = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Folder Icons", - description: "Whether to show folder icons or chevrons for directories in the outline panel.", - field: Box::new(SettingField { - json_path: Some("outline_panel.folder_icons"), - pick: |settings_content| { - settings_content - .outline_panel - .as_ref()? - .folder_icons - .as_ref() - }, - write: |settings_content, value| { - settings_content - .outline_panel - .get_or_insert_default() - .folder_icons = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Git Status", - description: "Show the Git status in the outline panel.", - field: Box::new(SettingField { - json_path: Some("outline_panel.git_status"), - pick: |settings_content| { - settings_content.outline_panel.as_ref()?.git_status.as_ref() - }, - write: |settings_content, value| { - settings_content - .outline_panel - .get_or_insert_default() - .git_status = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Indent Size", - description: "Amount of indentation for nested items.", - field: Box::new(SettingField { - json_path: Some("outline_panel.indent_size"), - pick: |settings_content| { - settings_content - .outline_panel - .as_ref()? - .indent_size - .as_ref() - }, - write: |settings_content, value| { - settings_content - .outline_panel - .get_or_insert_default() - .indent_size = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Reveal Entries", - description: "Whether to reveal when a corresponding outline entry becomes active.", - field: Box::new(SettingField { - json_path: Some("outline_panel.auto_reveal_entries"), - pick: |settings_content| { - settings_content - .outline_panel - .as_ref()? - .auto_reveal_entries - .as_ref() - }, - write: |settings_content, value| { - settings_content - .outline_panel - .get_or_insert_default() - .auto_reveal_entries = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Fold Directories", - description: "Whether to fold directories automatically when a directory contains only one subdirectory.", - field: Box::new(SettingField { - json_path: Some("outline_panel.auto_fold_dirs"), - pick: |settings_content| { - settings_content - .outline_panel - .as_ref()? - .auto_fold_dirs - .as_ref() - }, - write: |settings_content, value| { - settings_content - .outline_panel - .get_or_insert_default() - .auto_fold_dirs = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Show Indent Guides", - description: "When to show indent guides in the outline panel.", - field: Box::new( - SettingField { - json_path: Some("outline_panel.indent_guides.show"), - pick: |settings_content| { - settings_content - .outline_panel - .as_ref()? - .indent_guides - .as_ref()? - .show - .as_ref() - }, - write: |settings_content, value| { - settings_content - .outline_panel - .get_or_insert_default() - .indent_guides - .get_or_insert_default() - .show = value; - }, - } - ), - metadata: None, - }), - SettingsPageItem::SectionHeader("Git Panel"), - SettingsPageItem::SettingItem(SettingItem { - title: "Git Panel Button", - description: "Show the Git panel button in the status bar.", - field: Box::new(SettingField { - json_path: Some("git_panel.button"), - pick: |settings_content| { - settings_content.git_panel.as_ref()?.button.as_ref() - }, - write: |settings_content, value| { - settings_content.git_panel.get_or_insert_default().button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Git Panel Dock", - description: "Where to dock the Git panel.", - field: Box::new(SettingField { - json_path: Some("git_panel.dock"), - pick: |settings_content| settings_content.git_panel.as_ref()?.dock.as_ref(), - write: |settings_content, value| { - settings_content.git_panel.get_or_insert_default().dock = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Git Panel Default Width", - description: "Default width of the Git panel in pixels.", - field: Box::new(SettingField { - json_path: Some("git_panel.default_width"), - pick: |settings_content| { - settings_content.git_panel.as_ref()?.default_width.as_ref() - }, - write: |settings_content, value| { - settings_content - .git_panel - .get_or_insert_default() - .default_width = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Git Panel Status Style", - description: "How entry statuses are displayed.", - field: Box::new(SettingField { - json_path: Some("git_panel.status_style"), - pick: |settings_content| { - settings_content.git_panel.as_ref()?.status_style.as_ref() - }, - write: |settings_content, value| { - settings_content - .git_panel - .get_or_insert_default() - .status_style = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Fallback Branch Name", - description: "Default branch name will be when init.defaultbranch is not set in Git.", - field: Box::new(SettingField { - json_path: Some("git_panel.fallback_branch_name"), - pick: |settings_content| { - settings_content - .git_panel - .as_ref()? - .fallback_branch_name - .as_ref() - }, - write: |settings_content, value| { - settings_content - .git_panel - .get_or_insert_default() - .fallback_branch_name = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Sort By Path", - description: "Enable to sort entries in the panel by path, disable to sort by status.", - field: Box::new(SettingField { - json_path: Some("git_panel.sort_by_path"), - pick: |settings_content| { - settings_content.git_panel.as_ref()?.sort_by_path.as_ref() - }, - write: |settings_content, value| { - settings_content - .git_panel - .get_or_insert_default() - .sort_by_path = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Collapse Untracked Diff", - description: "Whether to collapse untracked files in the diff panel.", - field: Box::new(SettingField { - json_path: Some("git_panel.collapse_untracked_diff"), - pick: |settings_content| { - settings_content - .git_panel - .as_ref()? - .collapse_untracked_diff - .as_ref() - }, - write: |settings_content, value| { - settings_content - .git_panel - .get_or_insert_default() - .collapse_untracked_diff = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Tree View", - description: "Enable to show entries in tree view list, disable to show in flat view list.", - field: Box::new(SettingField { - json_path: Some("git_panel.tree_view"), - pick: |settings_content| { - settings_content.git_panel.as_ref()?.tree_view.as_ref() - }, - write: |settings_content, value| { - settings_content - .git_panel - .get_or_insert_default() - .tree_view = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Scroll Bar", - description: "How and when the scrollbar should be displayed.", - field: Box::new(SettingField { - json_path: Some("git_panel.scrollbar.show"), - pick: |settings_content| { - show_scrollbar_or_editor(settings_content, |settings_content| { - settings_content - .git_panel - .as_ref()? - .scrollbar - .as_ref()? - .show - .as_ref() - }) - }, - write: |settings_content, value| { - settings_content - .git_panel - .get_or_insert_default() - .scrollbar - .get_or_insert_default() - .show = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Debugger Panel"), - SettingsPageItem::SettingItem(SettingItem { - title: "Debugger Panel Dock", - description: "The dock position of the debug panel.", - field: Box::new(SettingField { - json_path: Some("debugger.dock"), - pick: |settings_content| settings_content.debugger.as_ref()?.dock.as_ref(), - write: |settings_content, value| { - settings_content.debugger.get_or_insert_default().dock = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Notification Panel"), - SettingsPageItem::SettingItem(SettingItem { - title: "Notification Panel Button", - description: "Show the notification panel button in the status bar.", - field: Box::new(SettingField { - json_path: Some("notification_panel.button"), - pick: |settings_content| { - settings_content - .notification_panel - .as_ref()? - .button - .as_ref() - }, - write: |settings_content, value| { - settings_content - .notification_panel - .get_or_insert_default() - .button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Notification Panel Dock", - description: "Where to dock the notification panel.", - field: Box::new(SettingField { - json_path: Some("notification_panel.dock"), - pick: |settings_content| { - settings_content.notification_panel.as_ref()?.dock.as_ref() - }, - write: |settings_content, value| { - settings_content - .notification_panel - .get_or_insert_default() - .dock = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Notification Panel Default Width", - description: "Default width of the notification panel in pixels.", - field: Box::new(SettingField { - json_path: Some("notification_panel.default_width"), - pick: |settings_content| { - settings_content - .notification_panel - .as_ref()? - .default_width - .as_ref() - }, - write: |settings_content, value| { - settings_content - .notification_panel - .get_or_insert_default() - .default_width = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Collaboration Panel"), - SettingsPageItem::SettingItem(SettingItem { - title: "Collaboration Panel Button", - description: "Show the collaboration panel button in the status bar.", - field: Box::new(SettingField { - json_path: Some("collaboration_panel.button"), - pick: |settings_content| { - settings_content - .collaboration_panel - .as_ref()? - .button - .as_ref() - }, - write: |settings_content, value| { - settings_content - .collaboration_panel - .get_or_insert_default() - .button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Collaboration Panel Dock", - description: "Where to dock the collaboration panel.", - field: Box::new(SettingField { - json_path: Some("collaboration_panel.dock"), - pick: |settings_content| { - settings_content.collaboration_panel.as_ref()?.dock.as_ref() - }, - write: |settings_content, value| { - settings_content - .collaboration_panel - .get_or_insert_default() - .dock = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Collaboration Panel Default Width", - description: "Default width of the collaboration panel in pixels.", - field: Box::new(SettingField { - json_path: Some("collaboration_panel.dock"), - pick: |settings_content| { - settings_content - .collaboration_panel - .as_ref()? - .default_width - .as_ref() - }, - write: |settings_content, value| { - settings_content - .collaboration_panel - .get_or_insert_default() - .default_width = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Agent Panel"), - SettingsPageItem::SettingItem(SettingItem { - title: "Agent Panel Button", - description: "Whether to show the agent panel button in the status bar.", - field: Box::new(SettingField { - json_path: Some("agent.button"), - pick: |settings_content| settings_content.agent.as_ref()?.button.as_ref(), - write: |settings_content, value| { - settings_content.agent.get_or_insert_default().button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Agent Panel Dock", - description: "Where to dock the agent panel.", - field: Box::new(SettingField { - json_path: Some("agent.dock"), - pick: |settings_content| settings_content.agent.as_ref()?.dock.as_ref(), - write: |settings_content, value| { - settings_content.agent.get_or_insert_default().dock = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Agent Panel Default Width", - description: "Default width when the agent panel is docked to the left or right.", - field: Box::new(SettingField { - json_path: Some("agent.default_width"), - pick: |settings_content| { - settings_content.agent.as_ref()?.default_width.as_ref() - }, - write: |settings_content, value| { - settings_content.agent.get_or_insert_default().default_width = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Agent Panel Default Height", - description: "Default height when the agent panel is docked to the bottom.", - field: Box::new(SettingField { - json_path: Some("agent.default_height"), - pick: |settings_content| { - settings_content.agent.as_ref()?.default_height.as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .default_height = value; - }, - }), - metadata: None, - files: USER, - }), - ], - }, - SettingsPage { - title: "Debugger", - items: vec![ - SettingsPageItem::SectionHeader("General"), - SettingsPageItem::SettingItem(SettingItem { - title: "Stepping Granularity", - description: "Determines the stepping granularity for debug operations.", - field: Box::new(SettingField { - json_path: Some("debugger.stepping_granularity"), - pick: |settings_content| { - settings_content - .debugger - .as_ref()? - .stepping_granularity - .as_ref() - }, - write: |settings_content, value| { - settings_content - .debugger - .get_or_insert_default() - .stepping_granularity = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Save Breakpoints", - description: "Whether breakpoints should be reused across Zed sessions.", - field: Box::new(SettingField { - json_path: Some("debugger.save_breakpoints"), - pick: |settings_content| { - settings_content - .debugger - .as_ref()? - .save_breakpoints - .as_ref() - }, - write: |settings_content, value| { - settings_content - .debugger - .get_or_insert_default() - .save_breakpoints = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Timeout", - description: "Time in milliseconds until timeout error when connecting to a TCP debug adapter.", - field: Box::new(SettingField { - json_path: Some("debugger.timeout"), - pick: |settings_content| { - settings_content.debugger.as_ref()?.timeout.as_ref() - }, - write: |settings_content, value| { - settings_content.debugger.get_or_insert_default().timeout = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Log DAP Communications", - description: "Whether to log messages between active debug adapters and Zed.", - field: Box::new(SettingField { - json_path: Some("debugger.log_dap_communications"), - pick: |settings_content| { - settings_content - .debugger - .as_ref()? - .log_dap_communications - .as_ref() - }, - write: |settings_content, value| { - settings_content - .debugger - .get_or_insert_default() - .log_dap_communications = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Format DAP Log Messages", - description: "Whether to format DAP messages when adding them to debug adapter logger.", - field: Box::new(SettingField { - json_path: Some("debugger.format_dap_log_messages"), - pick: |settings_content| { - settings_content - .debugger - .as_ref()? - .format_dap_log_messages - .as_ref() - }, - write: |settings_content, value| { - settings_content - .debugger - .get_or_insert_default() - .format_dap_log_messages = value; - }, - }), - metadata: None, - files: USER, - }), - ], - }, - SettingsPage { - title: "Terminal", - items: vec![ - SettingsPageItem::SectionHeader("Environment"), - SettingsPageItem::DynamicItem(DynamicItem { - discriminant: SettingItem { - files: USER | PROJECT, - title: "Shell", - description: "What shell to use when opening a terminal.", - field: Box::new(SettingField { - json_path: Some("terminal.shell$"), - pick: |settings_content| { - Some(&dynamic_variants::()[ - settings_content - .terminal - .as_ref()? - .project - .shell - .as_ref()? - .discriminant() as usize]) - }, - write: |settings_content, value| { - let Some(value) = value else { - if let Some(terminal) = settings_content.terminal.as_mut() { - terminal.project.shell = None; - } - return; - }; - let settings_value = settings_content - .terminal - .get_or_insert_default() - .project - .shell - .get_or_insert_with(|| settings::Shell::default()); - let default_shell = if cfg!(target_os = "windows") { - "powershell.exe" - } else { - "sh" - }; - *settings_value = match value { - settings::ShellDiscriminants::System => { - settings::Shell::System - }, - settings::ShellDiscriminants::Program => { - let program = match settings_value { - settings::Shell::Program(p) => p.clone(), - settings::Shell::WithArguments { program, .. } => program.clone(), - _ => String::from(default_shell), - }; - settings::Shell::Program(program) - }, - settings::ShellDiscriminants::WithArguments => { - let (program, args, title_override) = match settings_value { - settings::Shell::Program(p) => (p.clone(), vec![], None), - settings::Shell::WithArguments { program, args, title_override } => { - (program.clone(), args.clone(), title_override.clone()) - }, - _ => (String::from(default_shell), vec![], None), - }; - settings::Shell::WithArguments { - program, - args, - title_override, - } - }, - }; - }, - }), - metadata: None, - }, - pick_discriminant: |settings_content| { - Some(settings_content.terminal.as_ref()?.project.shell.as_ref()?.discriminant() as usize) - }, - fields: dynamic_variants::().into_iter().map(|variant| { - match variant { - settings::ShellDiscriminants::System => vec![], - settings::ShellDiscriminants::Program => vec![ - SettingItem { - files: USER | PROJECT, - title: "Program", - description: "The shell program to use.", - field: Box::new(SettingField { - json_path: Some("terminal.shell"), - pick: |settings_content| { - match settings_content.terminal.as_ref()?.project.shell.as_ref() { - Some(settings::Shell::Program(program)) => Some(program), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .terminal - .get_or_insert_default() - .project - .shell.as_mut() { - Some(settings::Shell::Program(program)) => *program = value, - _ => return - } - }, - }), - metadata: None, - } - ], - settings::ShellDiscriminants::WithArguments => vec![ - SettingItem { - files: USER | PROJECT, - title: "Program", - description: "The shell program to run.", - field: Box::new(SettingField { - json_path: Some("terminal.shell.program"), - pick: |settings_content| { - match settings_content.terminal.as_ref()?.project.shell.as_ref() { - Some(settings::Shell::WithArguments { program, .. }) => Some(program), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .terminal - .get_or_insert_default() - .project - .shell.as_mut() { - Some(settings::Shell::WithArguments { program, .. }) => *program = value, - _ => return - } - }, - }), - metadata: None, - }, - SettingItem { - files: USER | PROJECT, - title: "Arguments", - description: "The arguments to pass to the shell program.", - field: Box::new( - SettingField { - json_path: Some("terminal.shell.args"), - pick: |settings_content| { - match settings_content.terminal.as_ref()?.project.shell.as_ref() { - Some(settings::Shell::WithArguments { args, .. }) => Some(args), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .terminal - .get_or_insert_default() - .project - .shell.as_mut() { - Some(settings::Shell::WithArguments { args, .. }) => *args = value, - _ => return - } - }, - } - .unimplemented(), - ), - metadata: None, - }, - SettingItem { - files: USER | PROJECT, - title: "Title Override", - description: "An optional string to override the title of the terminal tab.", - field: Box::new(SettingField { - json_path: Some("terminal.shell.title_override"), - pick: |settings_content| { - match settings_content.terminal.as_ref()?.project.shell.as_ref() { - Some(settings::Shell::WithArguments { title_override, .. }) => title_override.as_ref().or(DEFAULT_EMPTY_SHARED_STRING), - _ => None - } - }, - write: |settings_content, value| { - match settings_content - .terminal - .get_or_insert_default() - .project - .shell.as_mut() { - Some(settings::Shell::WithArguments { title_override, .. }) => *title_override = value.filter(|s| !s.is_empty()), - _ => return - } - }, - }), - metadata: None, - } - ], - } - }).collect(), - }), - SettingsPageItem::DynamicItem(DynamicItem { - discriminant: SettingItem { - files: USER | PROJECT, - title: "Working Directory", - description: "What working directory to use when launching the terminal.", - field: Box::new(SettingField { - json_path: Some("terminal.working_directory$"), - pick: |settings_content| { - Some(&dynamic_variants::()[ - settings_content - .terminal - .as_ref()? - .project - .working_directory - .as_ref()? - .discriminant() as usize]) - }, - write: |settings_content, value| { - let Some(value) = value else { - if let Some(terminal) = settings_content.terminal.as_mut() { - terminal.project.working_directory = None; - } - return; - }; - let settings_value = settings_content - .terminal - .get_or_insert_default() - .project - .working_directory - .get_or_insert_with(|| settings::WorkingDirectory::CurrentProjectDirectory); - *settings_value = match value { - settings::WorkingDirectoryDiscriminants::CurrentProjectDirectory => { - settings::WorkingDirectory::CurrentProjectDirectory - }, - settings::WorkingDirectoryDiscriminants::FirstProjectDirectory => { - settings::WorkingDirectory::FirstProjectDirectory - }, - settings::WorkingDirectoryDiscriminants::AlwaysHome => { - settings::WorkingDirectory::AlwaysHome - }, - settings::WorkingDirectoryDiscriminants::Always => { - let directory = match settings_value { - settings::WorkingDirectory::Always { .. } => return, - _ => String::new(), - }; - settings::WorkingDirectory::Always { directory } - }, - }; - }, - }), - metadata: None, - }, - pick_discriminant: |settings_content| { - Some(settings_content.terminal.as_ref()?.project.working_directory.as_ref()?.discriminant() as usize) - }, - fields: dynamic_variants::().into_iter().map(|variant| { - match variant { - settings::WorkingDirectoryDiscriminants::CurrentProjectDirectory => vec![], - settings::WorkingDirectoryDiscriminants::FirstProjectDirectory => vec![], - settings::WorkingDirectoryDiscriminants::AlwaysHome => vec![], - settings::WorkingDirectoryDiscriminants::Always => vec![ - SettingItem { - files: USER | PROJECT, - title: "Directory", - description: "The directory path to use (will be shell expanded).", - field: Box::new(SettingField { - json_path: Some("terminal.working_directory.always"), - pick: |settings_content| { - match settings_content.terminal.as_ref()?.project.working_directory.as_ref() { - Some(settings::WorkingDirectory::Always { directory }) => Some(directory), - _ => None - } - }, - write: |settings_content, value| { - let value = value.unwrap_or_default(); - match settings_content - .terminal - .get_or_insert_default() - .project - .working_directory.as_mut() { - Some(settings::WorkingDirectory::Always { directory }) => *directory = value, - _ => return - } - }, - }), - metadata: None, - } - ], - } - }).collect(), - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Environment Variables", - description: "Key-value pairs to add to the terminal's environment.", - field: Box::new( - SettingField { - json_path: Some("terminal.env"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.project.env.as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .project - .env = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Detect Virtual Environment", - description: "Activates the Python virtual environment, if one is found, in the terminal's working directory.", - field: Box::new( - SettingField { - json_path: Some("terminal.detect_venv"), - pick: |settings_content| { - settings_content - .terminal - .as_ref()? - .project - .detect_venv - .as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .project - .detect_venv = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SectionHeader("Font"), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Size", - description: "Font size for terminal text. If not set, defaults to buffer font size.", - field: Box::new(SettingField { - json_path: Some("terminal.font_size"), - pick: |settings_content| { - settings_content - .terminal - .as_ref() - .and_then(|terminal| terminal.font_size.as_ref()) - .or(settings_content.theme.buffer_font_size.as_ref()) - }, - write: |settings_content, value| { - settings_content.terminal.get_or_insert_default().font_size = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Family", - description: "Font family for terminal text. If not set, defaults to buffer font family.", - field: Box::new(SettingField { - json_path: Some("terminal.font_family"), - pick: |settings_content| { - settings_content - .terminal - .as_ref() - .and_then(|terminal| terminal.font_family.as_ref()) - .or(settings_content.theme.buffer_font_family.as_ref()) - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .font_family = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Fallbacks", - description: "Font fallbacks for terminal text. If not set, defaults to buffer font fallbacks.", - field: Box::new( - SettingField { - json_path: Some("terminal.font_fallbacks"), - pick: |settings_content| { - settings_content - .terminal - .as_ref() - .and_then(|terminal| terminal.font_fallbacks.as_ref()) - .or(settings_content.theme.buffer_font_fallbacks.as_ref()) - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .font_fallbacks = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Weight", - description: "Font weight for terminal text in CSS weight units (100-900).", - field: Box::new(SettingField { - json_path: Some("terminal.font_weight"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.font_weight.as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .font_weight = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Features", - description: "Font features for terminal text.", - field: Box::new( - SettingField { - json_path: Some("terminal.font_features"), - pick: |settings_content| { - settings_content - .terminal - .as_ref() - .and_then(|terminal| terminal.font_features.as_ref()) - .or(settings_content.theme.buffer_font_features.as_ref()) - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .font_features = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Display Settings"), - SettingsPageItem::SettingItem(SettingItem { - title: "Line Height", - description: "Line height for terminal text.", - field: Box::new( - SettingField { - json_path: Some("terminal.line_height"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.line_height.as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .line_height = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Cursor Shape", - description: "Default cursor shape for the terminal (bar, block, underline, or hollow).", - field: Box::new(SettingField { - json_path: Some("terminal.cursor_shape"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.cursor_shape.as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .cursor_shape = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Cursor Blinking", - description: "Sets the cursor blinking behavior in the terminal.", - field: Box::new(SettingField { - json_path: Some("terminal.blinking"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.blinking.as_ref() - }, - write: |settings_content, value| { - settings_content.terminal.get_or_insert_default().blinking = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Alternate Scroll", - description: "Whether alternate scroll mode is active by default (converts mouse scroll to arrow keys in apps like Vim).", - field: Box::new(SettingField { - json_path: Some("terminal.alternate_scroll"), - pick: |settings_content| { - settings_content - .terminal - .as_ref()? - .alternate_scroll - .as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .alternate_scroll = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Minimum Contrast", - description: "The minimum APCA perceptual contrast between foreground and background colors (0-106).", - field: Box::new(SettingField { - json_path: Some("terminal.minimum_contrast"), - pick: |settings_content| { - settings_content - .terminal - .as_ref()? - .minimum_contrast - .as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .minimum_contrast = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Behavior Settings"), - SettingsPageItem::SettingItem(SettingItem { - title: "Option As Meta", - description: "Whether the option key behaves as the meta key.", - field: Box::new(SettingField { - json_path: Some("terminal.option_as_meta"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.option_as_meta.as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .option_as_meta = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Copy On Select", - description: "Whether selecting text in the terminal automatically copies to the system clipboard.", - field: Box::new(SettingField { - json_path: Some("terminal.copy_on_select"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.copy_on_select.as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .copy_on_select = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Keep Selection On Copy", - description: "Whether to keep the text selection after copying it to the clipboard.", - field: Box::new(SettingField { - json_path: Some("terminal.keep_selection_on_copy"), - pick: |settings_content| { - settings_content - .terminal - .as_ref()? - .keep_selection_on_copy - .as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .keep_selection_on_copy = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Layout Settings"), - SettingsPageItem::SettingItem(SettingItem { - title: "Default Width", - description: "Default width when the terminal is docked to the left or right (in pixels).", - field: Box::new(SettingField { - json_path: Some("terminal.default_width"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.default_width.as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .default_width = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Default Height", - description: "Default height when the terminal is docked to the bottom (in pixels).", - field: Box::new(SettingField { - json_path: Some("terminal.default_height"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.default_height.as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .default_height = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Advanced Settings"), - SettingsPageItem::SettingItem(SettingItem { - title: "Max Scroll History Lines", - description: "Maximum number of lines to keep in scrollback history (max: 100,000; 0 disables scrolling).", - field: Box::new(SettingField { - json_path: Some("terminal.max_scroll_history_lines"), - pick: |settings_content| { - settings_content - .terminal - .as_ref()? - .max_scroll_history_lines - .as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .max_scroll_history_lines = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Scroll Multiplier", - description: "The multiplier for scrolling in the terminal with the mouse wheel", - field: Box::new(SettingField { - json_path: Some("terminal.scroll_multiplier"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.scroll_multiplier.as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .scroll_multiplier = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Toolbar"), - SettingsPageItem::SettingItem(SettingItem { - title: "Breadcrumbs", - description: "Display the terminal title in breadcrumbs inside the terminal pane.", - field: Box::new(SettingField { - json_path: Some("terminal.toolbar.breadcrumbs"), - pick: |settings_content| { - settings_content - .terminal - .as_ref()? - .toolbar - .as_ref()? - .breadcrumbs - .as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .toolbar - .get_or_insert_default() - .breadcrumbs = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Scrollbar"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Scrollbar", - description: "When to show the scrollbar in the terminal.", - field: Box::new(SettingField { - json_path: Some("terminal.scrollbar.show"), - pick: |settings_content| { - show_scrollbar_or_editor(settings_content, |settings_content| { - settings_content - .terminal - .as_ref()? - .scrollbar - .as_ref()? - .show - .as_ref() - }) - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .scrollbar - .get_or_insert_default() - .show = value; - }, - }), - metadata: None, - files: USER, - }), - ], - }, - SettingsPage { - title: "Version Control", - items: vec![ - SettingsPageItem::SectionHeader("Git Gutter"), - SettingsPageItem::SettingItem(SettingItem { - title: "Visibility", - description: "Control whether Git status is shown in the editor's gutter.", - field: Box::new(SettingField { - json_path: Some("git.git_gutter"), - pick: |settings_content| settings_content.git.as_ref()?.git_gutter.as_ref(), - write: |settings_content, value| { - settings_content.git.get_or_insert_default().git_gutter = value; - }, - }), - metadata: None, - files: USER, - }), - // todo(settings_ui): Figure out the right default for this value in default.json - SettingsPageItem::SettingItem(SettingItem { - title: "Debounce", - description: "Debounce threshold in milliseconds after which changes are reflected in the Git gutter.", - field: Box::new(SettingField { - json_path: Some("git.gutter_debounce"), - pick: |settings_content| { - settings_content.git.as_ref()?.gutter_debounce.as_ref() - }, - write: |settings_content, value| { - settings_content.git.get_or_insert_default().gutter_debounce = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Inline Git Blame"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enabled", - description: "Whether or not to show Git blame data inline in the currently focused line.", - field: Box::new(SettingField { - json_path: Some("git.inline_blame.enabled"), - pick: |settings_content| { - settings_content - .git - .as_ref()? - .inline_blame - .as_ref()? - .enabled - .as_ref() - }, - write: |settings_content, value| { - settings_content - .git - .get_or_insert_default() - .inline_blame - .get_or_insert_default() - .enabled = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Delay", - description: "The delay after which the inline blame information is shown.", - field: Box::new(SettingField { - json_path: Some("git.inline_blame.delay_ms"), - pick: |settings_content| { - settings_content - .git - .as_ref()? - .inline_blame - .as_ref()? - .delay_ms - .as_ref() - }, - write: |settings_content, value| { - settings_content - .git - .get_or_insert_default() - .inline_blame - .get_or_insert_default() - .delay_ms = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Padding", - description: "Padding between the end of the source line and the start of the inline blame in columns.", - field: Box::new(SettingField { - json_path: Some("git.inline_blame.padding"), - pick: |settings_content| { - settings_content - .git - .as_ref()? - .inline_blame - .as_ref()? - .padding - .as_ref() - }, - write: |settings_content, value| { - settings_content - .git - .get_or_insert_default() - .inline_blame - .get_or_insert_default() - .padding = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Minimum Column", - description: "The minimum column number at which to show the inline blame information.", - field: Box::new(SettingField { - json_path: Some("git.inline_blame.min_column"), - pick: |settings_content| { - settings_content - .git - .as_ref()? - .inline_blame - .as_ref()? - .min_column - .as_ref() - }, - write: |settings_content, value| { - settings_content - .git - .get_or_insert_default() - .inline_blame - .get_or_insert_default() - .min_column = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Commit Summary", - description: "Show commit summary as part of the inline blame.", - field: Box::new(SettingField { - json_path: Some("git.inline_blame.show_commit_summary"), - pick: |settings_content| { - settings_content - .git - .as_ref()? - .inline_blame - .as_ref()? - .show_commit_summary - .as_ref() - }, - write: |settings_content, value| { - settings_content - .git - .get_or_insert_default() - .inline_blame - .get_or_insert_default() - .show_commit_summary = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Git Blame View"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Avatar", - description: "Show the avatar of the author of the commit.", - field: Box::new(SettingField { - json_path: Some("git.blame.show_avatar"), - pick: |settings_content| { - settings_content - .git - .as_ref()? - .blame - .as_ref()? - .show_avatar - .as_ref() - }, - write: |settings_content, value| { - settings_content - .git - .get_or_insert_default() - .blame - .get_or_insert_default() - .show_avatar = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Branch Picker"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Author Name", - description: "Show author name as part of the commit information in branch picker.", - field: Box::new(SettingField { - json_path: Some("git.branch_picker.show_author_name"), - pick: |settings_content| { - settings_content - .git - .as_ref()? - .branch_picker - .as_ref()? - .show_author_name - .as_ref() - }, - write: |settings_content, value| { - settings_content - .git - .get_or_insert_default() - .branch_picker - .get_or_insert_default() - .show_author_name = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Git Hunks"), - SettingsPageItem::SettingItem(SettingItem { - title: "Hunk Style", - description: "How Git hunks are displayed visually in the editor.", - field: Box::new(SettingField { - json_path: Some("git.hunk_style"), - pick: |settings_content| settings_content.git.as_ref()?.hunk_style.as_ref(), - write: |settings_content, value| { - settings_content.git.get_or_insert_default().hunk_style = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Path Style", - description: "Should the name or path be displayed first in the git view.", - field: Box::new(SettingField { - json_path: Some("git.path_style"), - pick: |settings_content| settings_content.git.as_ref()?.path_style.as_ref(), - write: |settings_content, value| { - settings_content.git.get_or_insert_default().path_style = value; - }, - }), - metadata: None, - files: USER, - }), - ], - }, - SettingsPage { - title: "Collaboration", - items: vec![ - SettingsPageItem::SectionHeader("Calls"), - SettingsPageItem::SettingItem(SettingItem { - title: "Mute On Join", - description: "Whether the microphone should be muted when joining a channel or a call.", - field: Box::new(SettingField { - json_path: Some("calls.mute_on_join"), - pick: |settings_content| { - settings_content.calls.as_ref()?.mute_on_join.as_ref() - }, - write: |settings_content, value| { - settings_content.calls.get_or_insert_default().mute_on_join = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Share On Join", - description: "Whether your current project should be shared when joining an empty channel.", - field: Box::new(SettingField { - json_path: Some("calls.share_on_join"), - pick: |settings_content| { - settings_content.calls.as_ref()?.share_on_join.as_ref() - }, - write: |settings_content, value| { - settings_content.calls.get_or_insert_default().share_on_join = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Experimental"), - SettingsPageItem::SettingItem(SettingItem { - title: "Rodio Audio", - description: "Opt into the new audio system.", - field: Box::new(SettingField { - json_path: Some("audio.experimental.rodio_audio"), - pick: |settings_content| { - settings_content.audio.as_ref()?.rodio_audio.as_ref() - }, - write: |settings_content, value| { - settings_content.audio.get_or_insert_default().rodio_audio = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Microphone Volume", - description: "Automatically adjust microphone volume (requires rodio audio).", - field: Box::new(SettingField { - json_path: Some("audio.experimental.auto_microphone_volume"), - pick: |settings_content| { - settings_content - .audio - .as_ref()? - .auto_microphone_volume - .as_ref() - }, - write: |settings_content, value| { - settings_content - .audio - .get_or_insert_default() - .auto_microphone_volume = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Speaker Volume", - description: "Automatically adjust volume of other call members (requires rodio audio).", - field: Box::new(SettingField { - json_path: Some("audio.experimental.auto_speaker_volume"), - pick: |settings_content| { - settings_content - .audio - .as_ref()? - .auto_speaker_volume - .as_ref() - }, - write: |settings_content, value| { - settings_content - .audio - .get_or_insert_default() - .auto_speaker_volume = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Denoise", - description: "Remove background noises (requires rodio audio).", - field: Box::new(SettingField { - json_path: Some("audio.experimental.denoise"), - pick: |settings_content| settings_content.audio.as_ref()?.denoise.as_ref(), - write: |settings_content, value| { - settings_content.audio.get_or_insert_default().denoise = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Legacy Audio Compatible", - description: "Use audio parameters compatible with previous versions (requires rodio audio).", - field: Box::new(SettingField { - json_path: Some("audio.experimental.legacy_audio_compatible"), - pick: |settings_content| { - settings_content - .audio - .as_ref()? - .legacy_audio_compatible - .as_ref() - }, - write: |settings_content, value| { - settings_content - .audio - .get_or_insert_default() - .legacy_audio_compatible = value; - }, - }), - metadata: None, - files: USER, - }), - ], - }, - SettingsPage { - title: "AI", - items: { - let mut items = vec![ - SettingsPageItem::SectionHeader("General"), - SettingsPageItem::SettingItem(SettingItem { - title: "Disable AI", - description: "Whether to disable all AI features in Zed.", - field: Box::new(SettingField { - json_path: Some("disable_ai"), - pick: |settings_content| settings_content.disable_ai.as_ref(), - write: |settings_content, value| { - settings_content.disable_ai = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Agent Configuration"), - SettingsPageItem::SettingItem(SettingItem { - title: "Always Allow Tool Actions", - description: "When enabled, the agent can run potentially destructive actions without asking for your confirmation. This setting has no effect on external agents.", - field: Box::new(SettingField { - json_path: Some("agent.always_allow_tool_actions"), - pick: |settings_content| { - settings_content - .agent - .as_ref()? - .always_allow_tool_actions - .as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .always_allow_tool_actions = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Single File Review", - description: "When enabled, agent edits will also be displayed in single-file buffers for review.", - field: Box::new(SettingField { - json_path: Some("agent.single_file_review"), - pick: |settings_content| { - settings_content.agent.as_ref()?.single_file_review.as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .single_file_review = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Enable Feedback", - description: "Show voting thumbs up/down icon buttons for feedback on agent edits.", - field: Box::new(SettingField { - json_path: Some("agent.enable_feedback"), - pick: |settings_content| { - settings_content.agent.as_ref()?.enable_feedback.as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .enable_feedback = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Notify When Agent Waiting", - description: "Where to show notifications when the agent has completed its response or needs confirmation before running a tool action.", - field: Box::new(SettingField { - json_path: Some("agent.notify_when_agent_waiting"), - pick: |settings_content| { - settings_content - .agent - .as_ref()? - .notify_when_agent_waiting - .as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .notify_when_agent_waiting = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Play Sound When Agent Done", - description: "Whether to play a sound when the agent has either completed its response, or needs user input.", - field: Box::new(SettingField { - json_path: Some("agent.play_sound_when_agent_done"), - pick: |settings_content| { - settings_content - .agent - .as_ref()? - .play_sound_when_agent_done - .as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .play_sound_when_agent_done = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Expand Edit Card", - description: "Whether to have edit cards in the agent panel expanded, showing a Preview of the diff.", - field: Box::new(SettingField { - json_path: Some("agent.expand_edit_card"), - pick: |settings_content| { - settings_content.agent.as_ref()?.expand_edit_card.as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .expand_edit_card = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Expand Terminal Card", - description: "Whether to have terminal cards in the agent panel expanded, showing the whole command output.", - field: Box::new(SettingField { - json_path: Some("agent.expand_terminal_card"), - pick: |settings_content| { - settings_content - .agent - .as_ref()? - .expand_terminal_card - .as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .expand_terminal_card = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Use Modifier To Send", - description: "Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages.", - field: Box::new(SettingField { - json_path: Some("agent.use_modifier_to_send"), - pick: |settings_content| { - settings_content - .agent - .as_ref()? - .use_modifier_to_send - .as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .use_modifier_to_send = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Message Editor Min Lines", - description: "Minimum number of lines to display in the agent message editor.", - field: Box::new(SettingField { - json_path: Some("agent.message_editor_min_lines"), - pick: |settings_content| { - settings_content - .agent - .as_ref()? - .message_editor_min_lines - .as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .message_editor_min_lines = value; - }, - }), - metadata: None, - files: USER, - }), - ]; - items.extend(edit_prediction_language_settings_section()); - items.extend( - [ - SettingsPageItem::SettingItem(SettingItem { - title: "Display Mode", - description: "When to show edit predictions previews in buffer. The eager mode displays them inline, while the subtle mode displays them only when holding a modifier key.", - field: Box::new(SettingField { - json_path: Some("edit_prediction.display_mode"), - pick: |settings_content| { - settings_content.project.all_languages.edit_predictions.as_ref()?.mode.as_ref() - }, - write: |settings_content, value| { - settings_content.project.all_languages.edit_predictions.get_or_insert_default().mode = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "In Text Threads", - description: "Whether edit predictions are enabled when editing text threads in the agent panel.", - field: Box::new(SettingField { - json_path: Some("edit_prediction.in_text_threads"), - pick: |settings_content| { - settings_content.project.all_languages.edit_predictions.as_ref()?.enabled_in_text_threads.as_ref() - }, - write: |settings_content, value| { - settings_content.project.all_languages.edit_predictions.get_or_insert_default().enabled_in_text_threads = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Copilot Provider", - description: "Use GitHub Copilot as your edit prediction provider.", - field: Box::new( - SettingField { - json_path: Some("edit_prediction.copilot_provider"), - pick: |settings_content| { - settings_content.project.all_languages.edit_predictions.as_ref()?.copilot.as_ref() - }, - write: |settings_content, value| { - settings_content.project.all_languages.edit_predictions.get_or_insert_default().copilot = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Codestral Provider", - description: "Use Mistral's Codestral as your edit prediction provider.", - field: Box::new( - SettingField { - json_path: Some("edit_prediction.codestral_provider"), - pick: |settings_content| { - settings_content.project.all_languages.edit_predictions.as_ref()?.codestral.as_ref() - }, - write: |settings_content, value| { - settings_content.project.all_languages.edit_predictions.get_or_insert_default().codestral = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - ] - ); - items - }, - }, - SettingsPage { - title: "Network", - items: vec![ - SettingsPageItem::SectionHeader("Network"), - // todo(settings_ui): Proxy needs a default - SettingsPageItem::SettingItem(SettingItem { - title: "Proxy", - description: "The proxy to use for network requests.", - field: Box::new( - SettingField { - json_path: Some("proxy"), - pick: |settings_content| settings_content.proxy.as_ref(), - write: |settings_content, value| { - settings_content.proxy = value; - }, - } - .unimplemented(), - ), - metadata: Some(Box::new(SettingsFieldMetadata { - placeholder: Some("socks5h://localhost:10808"), - ..Default::default() - })), - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Server URL", - description: "The URL of the Zed server to connect to.", - field: Box::new(SettingField { - json_path: Some("server_url"), - pick: |settings_content| settings_content.server_url.as_ref(), - write: |settings_content, value| { - settings_content.server_url = value; - }, - }), - metadata: Some(Box::new(SettingsFieldMetadata { - placeholder: Some("https://zed.dev"), - ..Default::default() - })), - files: USER, - }), - ], - }, - ] -} - -const LANGUAGES_SECTION_HEADER: &'static str = "Languages"; - -fn current_language() -> Option { - sub_page_stack().iter().find_map(|page| { - (page.section_header == LANGUAGES_SECTION_HEADER).then(|| page.link.title.clone()) - }) -} - -fn language_settings_field( - settings_content: &SettingsContent, - get: fn(&LanguageSettingsContent) -> Option<&T>, -) -> Option<&T> { - let all_languages = &settings_content.project.all_languages; - if let Some(current_language_name) = current_language() { - if let Some(current_language) = all_languages.languages.0.get(¤t_language_name) { - let value = get(current_language); - if value.is_some() { - return value; - } - } - } - let default_value = get(&all_languages.defaults); - return default_value; -} - -fn language_settings_field_mut( - settings_content: &mut SettingsContent, - value: Option, - write: fn(&mut LanguageSettingsContent, Option), -) { - let all_languages = &mut settings_content.project.all_languages; - let language_content = if let Some(current_language) = current_language() { - all_languages - .languages - .0 - .entry(current_language) - .or_default() - } else { - &mut all_languages.defaults - }; - write(language_content, value); -} - -fn language_settings_data() -> Vec { - let mut items = vec![ - SettingsPageItem::SectionHeader("Indentation"), - SettingsPageItem::SettingItem(SettingItem { - title: "Tab Size", - description: "How many columns a tab should occupy.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).tab_size"), // TODO(cameron): not JQ syntax because not URL-safe - pick: |settings_content| { - language_settings_field(settings_content, |language| language.tab_size.as_ref()) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.tab_size = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Hard Tabs", - description: "Whether to indent lines using tab characters, as opposed to multiple spaces.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).hard_tabs"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.hard_tabs.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.hard_tabs = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Indent", - description: "Whether indentation should be adjusted based on the context whilst typing.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).auto_indent"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.auto_indent.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.auto_indent = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Indent On Paste", - description: "Whether indentation of pasted content should be adjusted based on the context.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).auto_indent_on_paste"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.auto_indent_on_paste.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.auto_indent_on_paste = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SectionHeader("Wrapping"), - SettingsPageItem::SettingItem(SettingItem { - title: "Soft Wrap", - description: "How to soft-wrap long lines of text.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).soft_wrap"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.soft_wrap.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.soft_wrap = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Wrap Guides", - description: "Show wrap guides in the editor.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).show_wrap_guides"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.show_wrap_guides.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.show_wrap_guides = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Preferred Line Length", - description: "The column at which to soft-wrap lines, for buffers where soft-wrap is enabled.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).preferred_line_length"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.preferred_line_length.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.preferred_line_length = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Wrap Guides", - description: "Character counts at which to show wrap guides in the editor.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).wrap_guides"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.wrap_guides.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.wrap_guides = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Allow Rewrap", - description: "Controls where the `editor::rewrap` action is allowed for this language.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).allow_rewrap"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.allow_rewrap.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.allow_rewrap = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SectionHeader("Indent Guides"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enabled", - description: "Display indent guides in the editor.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).indent_guides.enabled"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language - .indent_guides - .as_ref() - .and_then(|indent_guides| indent_guides.enabled.as_ref()) - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.indent_guides.get_or_insert_default().enabled = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Line Width", - description: "The width of the indent guides in pixels, between 1 and 10.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).indent_guides.line_width"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language - .indent_guides - .as_ref() - .and_then(|indent_guides| indent_guides.line_width.as_ref()) - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.indent_guides.get_or_insert_default().line_width = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Active Line Width", - description: "The width of the active indent guide in pixels, between 1 and 10.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).indent_guides.active_line_width"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language - .indent_guides - .as_ref() - .and_then(|indent_guides| indent_guides.active_line_width.as_ref()) - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language - .indent_guides - .get_or_insert_default() - .active_line_width = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Coloring", - description: "Determines how indent guides are colored.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).indent_guides.coloring"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language - .indent_guides - .as_ref() - .and_then(|indent_guides| indent_guides.coloring.as_ref()) - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.indent_guides.get_or_insert_default().coloring = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Background Coloring", - description: "Determines how indent guide backgrounds are colored.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).indent_guides.background_coloring"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language - .indent_guides - .as_ref() - .and_then(|indent_guides| indent_guides.background_coloring.as_ref()) - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language - .indent_guides - .get_or_insert_default() - .background_coloring = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SectionHeader("Formatting"), - SettingsPageItem::SettingItem(SettingItem { - title: "Format On Save", - description: "Whether or not to perform a buffer format before saving.", - field: Box::new( - // TODO(settings_ui): this setting should just be a bool - SettingField { - json_path: Some("languages.$(language).format_on_save"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.format_on_save.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.format_on_save = value; - }) - }, - }, - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Remove Trailing Whitespace On Save", - description: "Whether or not to remove any trailing whitespace from lines of a buffer before saving it.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).remove_trailing_whitespace_on_save"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.remove_trailing_whitespace_on_save.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.remove_trailing_whitespace_on_save = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Ensure Final Newline On Save", - description: "Whether or not to ensure there's a single newline at the end of a buffer when saving it.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).ensure_final_newline_on_save"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.ensure_final_newline_on_save.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.ensure_final_newline_on_save = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Formatter", - description: "How to perform a buffer format.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).formatter"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.formatter.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.formatter = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Use On Type Format", - description: "Whether to use additional LSP queries to format (and amend) the code after every \"trigger\" symbol input, defined by LSP server capabilities", - field: Box::new(SettingField { - json_path: Some("languages.$(language).use_on_type_format"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.use_on_type_format.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.use_on_type_format = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Code Actions On Format", - description: "Additional code actions to run when formatting.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).code_actions_on_format"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.code_actions_on_format.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.code_actions_on_format = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SectionHeader("Autoclose"), - SettingsPageItem::SettingItem(SettingItem { - title: "Use Autoclose", - description: "Whether to automatically type closing characters for you. For example, when you type '(', Zed will automatically add a closing ')' at the correct position.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).use_autoclose"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.use_autoclose.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.use_autoclose = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Use Auto Surround", - description: "Whether to automatically surround text with characters for you. For example, when you select text and type '(', Zed will automatically surround text with ().", - field: Box::new(SettingField { - json_path: Some("languages.$(language).use_auto_surround"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.use_auto_surround.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.use_auto_surround = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Always Treat Brackets As Autoclosed", - description: "Controls whether the closing characters are always skipped over and auto-removed no matter how they were inserted.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).always_treat_brackets_as_autoclosed"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.always_treat_brackets_as_autoclosed.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.always_treat_brackets_as_autoclosed = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "JSX Tag Auto Close", - description: "Whether to automatically close JSX tags.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).jsx_tag_auto_close"), - // TODO(settings_ui): this setting should just be a bool - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.jsx_tag_auto_close.as_ref()?.enabled.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.jsx_tag_auto_close.get_or_insert_default().enabled = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SectionHeader("Whitespace"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Whitespaces", - description: "Whether to show tabs and spaces in the editor.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).show_whitespaces"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.show_whitespaces.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.show_whitespaces = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Space Whitespace Indicator", - description: "Visible character used to render space characters when show_whitespaces is enabled (default: \"•\")", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).whitespace_map.space"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.whitespace_map.as_ref()?.space.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.whitespace_map.get_or_insert_default().space = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Tab Whitespace Indicator", - description: "Visible character used to render tab characters when show_whitespaces is enabled (default: \"→\")", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).whitespace_map.tab"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.whitespace_map.as_ref()?.tab.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.whitespace_map.get_or_insert_default().tab = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SectionHeader("Completions"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Completions On Input", - description: "Whether to pop the completions menu while typing in an editor without explicitly requesting it.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).show_completions_on_input"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.show_completions_on_input.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.show_completions_on_input = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Completion Documentation", - description: "Whether to display inline and alongside documentation for items in the completions menu.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).show_completion_documentation"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.show_completion_documentation.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.show_completion_documentation = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Words", - description: "Controls how words are completed.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).completions.words"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.completions.as_ref()?.words.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.completions.get_or_insert_default().words = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Words Min Length", - description: "How many characters has to be in the completions query to automatically show the words-based completions.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).completions.words_min_length"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.completions.as_ref()?.words_min_length.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language - .completions - .get_or_insert_default() - .words_min_length = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Completion Menu Scrollbar", - description: "When to show the scrollbar in the completion menu.", - field: Box::new(SettingField { - json_path: Some("editor.completion_menu_scrollbar"), - pick: |settings_content| settings_content.editor.completion_menu_scrollbar.as_ref(), - write: |settings_content, value| { - settings_content.editor.completion_menu_scrollbar = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Inlay Hints"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enabled", - description: "Global switch to toggle hints on and off.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).inlay_hints.enabled"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.inlay_hints.as_ref()?.enabled.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.inlay_hints.get_or_insert_default().enabled = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Value Hints", - description: "Global switch to toggle inline values on and off when debugging.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).inlay_hints.show_value_hints"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.inlay_hints.as_ref()?.show_value_hints.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language - .inlay_hints - .get_or_insert_default() - .show_value_hints = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Type Hints", - description: "Whether type hints should be shown.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).inlay_hints.show_type_hints"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.inlay_hints.as_ref()?.show_type_hints.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.inlay_hints.get_or_insert_default().show_type_hints = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Parameter Hints", - description: "Whether parameter hints should be shown.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).inlay_hints.show_parameter_hints"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.inlay_hints.as_ref()?.show_parameter_hints.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language - .inlay_hints - .get_or_insert_default() - .show_parameter_hints = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Other Hints", - description: "Whether other hints should be shown.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).inlay_hints.show_other_hints"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.inlay_hints.as_ref()?.show_other_hints.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language - .inlay_hints - .get_or_insert_default() - .show_other_hints = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Background", - description: "Show a background for inlay hints.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).inlay_hints.show_background"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.inlay_hints.as_ref()?.show_background.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.inlay_hints.get_or_insert_default().show_background = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Edit Debounce Ms", - description: "Whether or not to debounce inlay hints updates after buffer edits (set to 0 to disable debouncing).", - field: Box::new(SettingField { - json_path: Some("languages.$(language).inlay_hints.edit_debounce_ms"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.inlay_hints.as_ref()?.edit_debounce_ms.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language - .inlay_hints - .get_or_insert_default() - .edit_debounce_ms = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Scroll Debounce Ms", - description: "Whether or not to debounce inlay hints updates after buffer scrolls (set to 0 to disable debouncing).", - field: Box::new(SettingField { - json_path: Some("languages.$(language).inlay_hints.scroll_debounce_ms"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.inlay_hints.as_ref()?.scroll_debounce_ms.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language - .inlay_hints - .get_or_insert_default() - .scroll_debounce_ms = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Toggle On Modifiers Press", - description: "Toggles inlay hints (hides or shows) when the user presses the modifiers specified.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).inlay_hints.toggle_on_modifiers_press"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language - .inlay_hints - .as_ref()? - .toggle_on_modifiers_press - .as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language - .inlay_hints - .get_or_insert_default() - .toggle_on_modifiers_press = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - ]; - if current_language().is_none() { - items.push(SettingsPageItem::SettingItem(SettingItem { - title: "LSP Document Colors", - description: "How to render LSP color previews in the editor.", - field: Box::new(SettingField { - json_path: Some("lsp_document_colors"), - pick: |settings_content| settings_content.editor.lsp_document_colors.as_ref(), - write: |settings_content, value| { - settings_content.editor.lsp_document_colors = value; - }, - }), - metadata: None, - files: USER, - })) - } - items.extend([ - SettingsPageItem::SectionHeader("Tasks"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enabled", - description: "Whether tasks are enabled for this language.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).tasks.enabled"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.tasks.as_ref()?.enabled.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.tasks.get_or_insert_default().enabled = value; - - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Variables", - description: "Extra task variables to set for a particular language.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).tasks.variables"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.tasks.as_ref()?.variables.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.tasks.get_or_insert_default().variables = value; - - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Prefer LSP", - description: "Use LSP tasks over Zed language extension tasks.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).tasks.prefer_lsp"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.tasks.as_ref()?.prefer_lsp.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.tasks.get_or_insert_default().prefer_lsp = value; - - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SectionHeader("Miscellaneous"), - SettingsPageItem::SettingItem(SettingItem { - title: "Word Diff Enabled", - description: "Whether to enable word diff highlighting in the editor. When enabled, changed words within modified lines are highlighted to show exactly what changed.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).word_diff_enabled"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.word_diff_enabled.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.word_diff_enabled = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Debuggers", - description: "Preferred debuggers for this language.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).debuggers"), - pick: |settings_content| { - language_settings_field(settings_content, |language| language.debuggers.as_ref()) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.debuggers = value; - - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Middle Click Paste", - description: "Enable middle-click paste on Linux.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).editor.middle_click_paste"), - pick: |settings_content| settings_content.editor.middle_click_paste.as_ref(), - write: |settings_content, value| {settings_content.editor.middle_click_paste = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Extend Comment On Newline", - description: "Whether to start a new line with a comment when a previous line is a comment as well.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).extend_comment_on_newline"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.extend_comment_on_newline.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.extend_comment_on_newline = value; - - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Colorize Brackets", - description: "Whether to colorize brackets in the editor.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).colorize_brackets"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.colorize_brackets.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.colorize_brackets = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - ]); - - if current_language().is_none() { - items.extend([ - SettingsPageItem::SettingItem(SettingItem { - title: "Image Viewer", - description: "The unit for image file sizes.", - field: Box::new(SettingField { - json_path: Some("image_viewer.unit"), - pick: |settings_content| { - settings_content.image_viewer.as_ref().and_then(|image_viewer| image_viewer.unit.as_ref()) - }, - write: |settings_content, value| { - settings_content.image_viewer.get_or_insert_default().unit = value; - - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Replace Emoji Shortcode", - description: "Whether to automatically replace emoji shortcodes with emoji characters.", - field: Box::new(SettingField { - json_path: Some("message_editor.auto_replace_emoji_shortcode"), - pick: |settings_content| { - settings_content.message_editor.as_ref().and_then(|message_editor| message_editor.auto_replace_emoji_shortcode.as_ref()) - }, - write: |settings_content, value| { - settings_content.message_editor.get_or_insert_default().auto_replace_emoji_shortcode = value; - - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Drop Size Target", - description: "Relative size of the drop target in the editor that will open dropped file as a split pane.", - field: Box::new(SettingField { - json_path: Some("drop_target_size"), - pick: |settings_content| { - settings_content.workspace.drop_target_size.as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.drop_target_size = value; - - }, - }), - metadata: None, - files: USER, - }), - ]); - } - items -} - -/// LanguageSettings items that should be included in the "Languages & Tools" page -/// not the "Editor" page -fn non_editor_language_settings_data() -> Vec { - vec![ - SettingsPageItem::SectionHeader("LSP"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enable Language Server", - description: "Whether to use language servers to provide code intelligence.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).enable_language_server"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.enable_language_server.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.enable_language_server = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Language Servers", - description: "The list of language servers to use (or disable) for this language.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).language_servers"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.language_servers.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.language_servers = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Linked Edits", - description: "Whether to perform linked edits of associated ranges, if the LS supports it. For example, when editing opening tag, the contents of the closing tag will be edited as well.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).linked_edits"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.linked_edits.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.linked_edits = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Go To Definition Fallback", - description: "Whether to follow-up empty Go to definition responses from the language server.", - field: Box::new(SettingField { - json_path: Some("go_to_definition_fallback"), - pick: |settings_content| settings_content.editor.go_to_definition_fallback.as_ref(), - write: |settings_content, value| { - settings_content.editor.go_to_definition_fallback = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("LSP Completions"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enabled", - description: "Whether to fetch LSP completions or not.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).completions.lsp"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.completions.as_ref()?.lsp.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.completions.get_or_insert_default().lsp = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Fetch Timeout (milliseconds)", - description: "When fetching LSP completions, determines how long to wait for a response of a particular server (set to 0 to wait indefinitely).", - field: Box::new(SettingField { - json_path: Some("languages.$(language).completions.lsp_fetch_timeout_ms"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.completions.as_ref()?.lsp_fetch_timeout_ms.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language - .completions - .get_or_insert_default() - .lsp_fetch_timeout_ms = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Insert Mode", - description: "Controls how LSP completions are inserted.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).completions.lsp_insert_mode"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.completions.as_ref()?.lsp_insert_mode.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.completions.get_or_insert_default().lsp_insert_mode = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SectionHeader("Debuggers"), - SettingsPageItem::SettingItem(SettingItem { - title: "Debuggers", - description: "Preferred debuggers for this language.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).debuggers"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.debuggers.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.debuggers = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SectionHeader("Prettier"), - SettingsPageItem::SettingItem(SettingItem { - title: "Allowed", - description: "Enables or disables formatting with Prettier for a given language.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).prettier.allowed"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.prettier.as_ref()?.allowed.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.prettier.get_or_insert_default().allowed = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Parser", - description: "Forces Prettier integration to use a specific parser name when formatting files with the language.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).prettier.parser"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.prettier.as_ref()?.parser.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.prettier.get_or_insert_default().parser = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Plugins", - description: "Forces Prettier integration to use specific plugins when formatting files with the language.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).prettier.plugins"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.prettier.as_ref()?.plugins.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.prettier.get_or_insert_default().plugins = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Options", - description: "Default Prettier options, in the format as in package.json section for Prettier.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).prettier.options"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.prettier.as_ref()?.options.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.prettier.get_or_insert_default().options = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - ] -} - -fn edit_prediction_language_settings_section() -> Vec { - vec![ - SettingsPageItem::SectionHeader("Edit Predictions"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Edit Predictions", - description: "Controls whether edit predictions are shown immediately or manually by triggering `editor::showeditprediction` (false).", - field: Box::new(SettingField { - json_path: Some("languages.$(language).show_edit_predictions"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.show_edit_predictions.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.show_edit_predictions = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Edit Predictions Disabled In", - description: "Controls whether edit predictions are shown in the given language scopes.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).edit_predictions_disabled_in"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.edit_predictions_disabled_in.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.edit_predictions_disabled_in = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - ] -} - -fn show_scrollbar_or_editor( - settings_content: &SettingsContent, - show: fn(&SettingsContent) -> Option<&settings::ShowScrollbar>, -) -> Option<&settings::ShowScrollbar> { - show(settings_content).or(settings_content - .editor - .scrollbar - .as_ref() - .and_then(|scrollbar| scrollbar.show.as_ref())) -} - -fn dynamic_variants() -> &'static [T::Discriminant] -where - T: strum::IntoDiscriminant, - T::Discriminant: strum::VariantArray, -{ - <::Discriminant as strum::VariantArray>::VARIANTS -} diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs deleted file mode 100644 index 4464d3bdd9..0000000000 --- a/crates/settings_ui/src/settings_ui.rs +++ /dev/null @@ -1,4047 +0,0 @@ -mod components; -mod page_data; - -use anyhow::Result; -use editor::{Editor, EditorEvent}; -use feature_flags::FeatureFlag; -use fuzzy::StringMatchCandidate; -use gpui::{ - Action, App, ClipboardItem, DEFAULT_ADDITIONAL_WINDOW_SIZE, Div, Entity, FocusHandle, - Focusable, Global, KeyContext, ListState, ReadGlobal as _, ScrollHandle, Stateful, - Subscription, Task, TitlebarOptions, UniformListScrollHandle, Window, WindowBounds, - WindowHandle, WindowOptions, actions, div, list, point, prelude::*, px, uniform_list, -}; -use project::{Project, WorktreeId}; -use release_channel::ReleaseChannel; -use schemars::JsonSchema; -use serde::Deserialize; -use settings::{Settings, SettingsContent, SettingsStore, initial_project_settings_content}; -use std::{ - any::{Any, TypeId, type_name}, - cell::RefCell, - collections::{HashMap, HashSet}, - num::{NonZero, NonZeroU32}, - ops::Range, - rc::Rc, - sync::{Arc, LazyLock, RwLock}, - time::Duration, -}; -use title_bar::platform_title_bar::PlatformTitleBar; -use ui::{ - Banner, ContextMenu, Divider, DividerColor, DropdownMenu, DropdownStyle, IconButtonShape, - KeyBinding, KeybindingHint, PopoverMenu, Switch, Tooltip, TreeViewItem, WithScrollbar, - prelude::*, -}; -use ui_input::{NumberField, NumberFieldType}; -use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath}; -use workspace::{AppState, OpenOptions, OpenVisible, Workspace, client_side_decorations}; -use zed_actions::{OpenProjectSettings, OpenSettings, OpenSettingsAt}; - -use crate::components::{ - EnumVariantDropdown, SettingsInputField, font_picker, icon_theme_picker, theme_picker, -}; - -const NAVBAR_CONTAINER_TAB_INDEX: isize = 0; -const NAVBAR_GROUP_TAB_INDEX: isize = 1; - -const HEADER_CONTAINER_TAB_INDEX: isize = 2; -const HEADER_GROUP_TAB_INDEX: isize = 3; - -const CONTENT_CONTAINER_TAB_INDEX: isize = 4; -const CONTENT_GROUP_TAB_INDEX: isize = 5; - -actions!( - settings_editor, - [ - /// Minimizes the settings UI window. - Minimize, - /// Toggles focus between the navbar and the main content. - ToggleFocusNav, - /// Expands the navigation entry. - ExpandNavEntry, - /// Collapses the navigation entry. - CollapseNavEntry, - /// Focuses the next file in the file list. - FocusNextFile, - /// Focuses the previous file in the file list. - FocusPreviousFile, - /// Opens an editor for the current file - OpenCurrentFile, - /// Focuses the previous root navigation entry. - FocusPreviousRootNavEntry, - /// Focuses the next root navigation entry. - FocusNextRootNavEntry, - /// Focuses the first navigation entry. - FocusFirstNavEntry, - /// Focuses the last navigation entry. - FocusLastNavEntry, - /// Focuses and opens the next navigation entry without moving focus to content. - FocusNextNavEntry, - /// Focuses and opens the previous navigation entry without moving focus to content. - FocusPreviousNavEntry - ] -); - -#[derive(Action, PartialEq, Eq, Clone, Copy, Debug, JsonSchema, Deserialize)] -#[action(namespace = settings_editor)] -struct FocusFile(pub u32); - -struct SettingField { - pick: fn(&SettingsContent) -> Option<&T>, - write: fn(&mut SettingsContent, Option), - - /// A json-path-like string that gives a unique-ish string that identifies - /// where in the JSON the setting is defined. - /// - /// The syntax is `jq`-like, but modified slightly to be URL-safe (and - /// without the leading dot), e.g. `foo.bar`. - /// - /// They are URL-safe (this is important since links are the main use-case - /// for these paths). - /// - /// There are a couple of special cases: - /// - discrimminants are represented with a trailing `$`, for example - /// `terminal.working_directory$`. This is to distinguish the discrimminant - /// setting (i.e. the setting that changes whether the value is a string or - /// an object) from the setting in the case that it is a string. - /// - language-specific settings begin `languages.$(language)`. Links - /// targeting these settings should take the form `languages/Rust/...`, for - /// example, but are not currently supported. - json_path: Option<&'static str>, -} - -impl Clone for SettingField { - fn clone(&self) -> Self { - *self - } -} - -// manual impl because derive puts a Copy bound on T, which is inaccurate in our case -impl Copy for SettingField {} - -/// Helper for unimplemented settings, used in combination with `SettingField::unimplemented` -/// to keep the setting around in the UI with valid pick and write implementations, but don't actually try to render it. -/// TODO(settings_ui): In non-dev builds (`#[cfg(not(debug_assertions))]`) make this render as edit-in-json -#[derive(Clone, Copy)] -struct UnimplementedSettingField; - -impl PartialEq for UnimplementedSettingField { - fn eq(&self, _other: &Self) -> bool { - true - } -} - -impl SettingField { - /// Helper for settings with types that are not yet implemented. - #[allow(unused)] - fn unimplemented(self) -> SettingField { - SettingField { - pick: |_| Some(&UnimplementedSettingField), - write: |_, _| unreachable!(), - json_path: self.json_path, - } - } -} - -trait AnySettingField { - fn as_any(&self) -> &dyn Any; - fn type_name(&self) -> &'static str; - fn type_id(&self) -> TypeId; - // Returns the file this value was set in and true, or File::Default and false to indicate it was not found in any file (missing default) - fn file_set_in(&self, file: SettingsUiFile, cx: &App) -> (settings::SettingsFile, bool); - fn reset_to_default_fn( - &self, - current_file: &SettingsUiFile, - file_set_in: &settings::SettingsFile, - cx: &App, - ) -> Option>; - - fn json_path(&self) -> Option<&'static str>; -} - -impl AnySettingField for SettingField { - fn as_any(&self) -> &dyn Any { - self - } - - fn type_name(&self) -> &'static str { - type_name::() - } - - fn type_id(&self) -> TypeId { - TypeId::of::() - } - - fn file_set_in(&self, file: SettingsUiFile, cx: &App) -> (settings::SettingsFile, bool) { - let (file, value) = cx - .global::() - .get_value_from_file(file.to_settings(), self.pick); - return (file, value.is_some()); - } - - fn reset_to_default_fn( - &self, - current_file: &SettingsUiFile, - file_set_in: &settings::SettingsFile, - cx: &App, - ) -> Option> { - if file_set_in == &settings::SettingsFile::Default { - return None; - } - if file_set_in != ¤t_file.to_settings() { - return None; - } - let this = *self; - let store = SettingsStore::global(cx); - let default_value = (this.pick)(store.raw_default_settings()); - let is_default = store - .get_content_for_file(file_set_in.clone()) - .map_or(None, this.pick) - == default_value; - if is_default { - return None; - } - let current_file = current_file.clone(); - - return Some(Box::new(move |cx| { - let store = SettingsStore::global(cx); - let default_value = (this.pick)(store.raw_default_settings()); - let is_set_somewhere_other_than_default = store - .get_value_up_to_file(current_file.to_settings(), this.pick) - .0 - != settings::SettingsFile::Default; - let value_to_set = if is_set_somewhere_other_than_default { - default_value.cloned() - } else { - None - }; - update_settings_file(current_file.clone(), None, cx, move |settings, _| { - (this.write)(settings, value_to_set); - }) - // todo(settings_ui): Don't log err - .log_err(); - })); - } - - fn json_path(&self) -> Option<&'static str> { - self.json_path - } -} - -#[derive(Default, Clone)] -struct SettingFieldRenderer { - renderers: Rc< - RefCell< - HashMap< - TypeId, - Box< - dyn Fn( - &SettingsWindow, - &SettingItem, - SettingsUiFile, - Option<&SettingsFieldMetadata>, - bool, - &mut Window, - &mut Context, - ) -> Stateful
, - >, - >, - >, - >, -} - -impl Global for SettingFieldRenderer {} - -impl SettingFieldRenderer { - fn add_basic_renderer( - &mut self, - render_control: impl Fn( - SettingField, - SettingsUiFile, - Option<&SettingsFieldMetadata>, - &mut Window, - &mut App, - ) -> AnyElement - + 'static, - ) -> &mut Self { - self.add_renderer( - move |settings_window: &SettingsWindow, - item: &SettingItem, - field: SettingField, - settings_file: SettingsUiFile, - metadata: Option<&SettingsFieldMetadata>, - sub_field: bool, - window: &mut Window, - cx: &mut Context| { - render_settings_item( - settings_window, - item, - settings_file.clone(), - render_control(field, settings_file, metadata, window, cx), - sub_field, - cx, - ) - }, - ) - } - - fn add_renderer( - &mut self, - renderer: impl Fn( - &SettingsWindow, - &SettingItem, - SettingField, - SettingsUiFile, - Option<&SettingsFieldMetadata>, - bool, - &mut Window, - &mut Context, - ) -> Stateful
- + 'static, - ) -> &mut Self { - let key = TypeId::of::(); - let renderer = Box::new( - move |settings_window: &SettingsWindow, - item: &SettingItem, - settings_file: SettingsUiFile, - metadata: Option<&SettingsFieldMetadata>, - sub_field: bool, - window: &mut Window, - cx: &mut Context| { - let field = *item - .field - .as_ref() - .as_any() - .downcast_ref::>() - .unwrap(); - renderer( - settings_window, - item, - field, - settings_file, - metadata, - sub_field, - window, - cx, - ) - }, - ); - self.renderers.borrow_mut().insert(key, renderer); - self - } -} - -struct NonFocusableHandle { - handle: FocusHandle, - _subscription: Subscription, -} - -impl NonFocusableHandle { - fn new(tab_index: isize, tab_stop: bool, window: &mut Window, cx: &mut App) -> Entity { - let handle = cx.focus_handle().tab_index(tab_index).tab_stop(tab_stop); - Self::from_handle(handle, window, cx) - } - - fn from_handle(handle: FocusHandle, window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| { - let _subscription = cx.on_focus(&handle, window, { - move |_, window, _| { - window.focus_next(); - } - }); - Self { - handle, - _subscription, - } - }) - } -} - -impl Focusable for NonFocusableHandle { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.handle.clone() - } -} - -#[derive(Default)] -struct SettingsFieldMetadata { - placeholder: Option<&'static str>, - should_do_titlecase: Option, -} - -pub struct SettingsUiFeatureFlag; - -impl FeatureFlag for SettingsUiFeatureFlag { - const NAME: &'static str = "settings-ui"; -} - -pub fn init(cx: &mut App) { - init_renderers(cx); - - cx.observe_new(|workspace: &mut workspace::Workspace, _, _| { - workspace - .register_action( - |workspace, OpenSettingsAt { path }: &OpenSettingsAt, window, cx| { - let window_handle = window - .window_handle() - .downcast::() - .expect("Workspaces are root Windows"); - open_settings_editor(workspace, Some(&path), false, window_handle, cx); - }, - ) - .register_action(|workspace, _: &OpenSettings, window, cx| { - let window_handle = window - .window_handle() - .downcast::() - .expect("Workspaces are root Windows"); - open_settings_editor(workspace, None, false, window_handle, cx); - }) - .register_action(|workspace, _: &OpenProjectSettings, window, cx| { - let window_handle = window - .window_handle() - .downcast::() - .expect("Workspaces are root Windows"); - open_settings_editor(workspace, None, true, window_handle, cx); - }); - }) - .detach(); -} - -fn init_renderers(cx: &mut App) { - cx.default_global::() - .add_renderer::( - |settings_window, item, _, settings_file, _, sub_field, _, cx| { - render_settings_item( - settings_window, - item, - settings_file, - Button::new("open-in-settings-file", "Edit in settings.json") - .style(ButtonStyle::Outlined) - .size(ButtonSize::Medium) - .tab_index(0_isize) - .tooltip(Tooltip::for_action_title_in( - "Edit in settings.json", - &OpenCurrentFile, - &settings_window.focus_handle, - )) - .on_click(cx.listener(|this, _, window, cx| { - this.open_current_settings_file(window, cx); - })) - .into_any_element(), - sub_field, - cx, - ) - }, - ) - .add_basic_renderer::(render_toggle_button) - .add_basic_renderer::(render_text_field) - .add_basic_renderer::(render_text_field) - .add_basic_renderer::(render_toggle_button) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_font_picker) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::>(render_number_field) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_theme_picker) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_icon_theme_picker) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - // please semicolon stay on next line - ; -} - -pub fn open_settings_editor( - _workspace: &mut Workspace, - path: Option<&str>, - open_project_settings: bool, - workspace_handle: WindowHandle, - cx: &mut App, -) { - telemetry::event!("Settings Viewed"); - - /// Assumes a settings GUI window is already open - fn open_path( - path: &str, - // Note: This option is unsupported right now - _open_project_settings: bool, - settings_window: &mut SettingsWindow, - window: &mut Window, - cx: &mut Context, - ) { - if path.starts_with("languages.$(language)") { - log::error!("language-specific settings links are not currently supported"); - return; - } - - settings_window.search_bar.update(cx, |editor, cx| { - editor.set_text(format!("#{path}"), window, cx); - }); - settings_window.update_matches(cx); - } - - let existing_window = cx - .windows() - .into_iter() - .find_map(|window| window.downcast::()); - - if let Some(existing_window) = existing_window { - existing_window - .update(cx, |settings_window, window, cx| { - settings_window.original_window = Some(workspace_handle); - window.activate_window(); - if let Some(path) = path { - open_path(path, open_project_settings, settings_window, window, cx); - } else if open_project_settings { - if let Some(file_index) = settings_window - .files - .iter() - .position(|(file, _)| file.worktree_id().is_some()) - { - settings_window.change_file(file_index, window, cx); - } - - cx.notify(); - } - }) - .ok(); - return; - } - - // We have to defer this to get the workspace off the stack. - - let path = path.map(ToOwned::to_owned); - cx.defer(move |cx| { - let current_rem_size: f32 = theme::ThemeSettings::get_global(cx).ui_font_size(cx).into(); - - let default_bounds = DEFAULT_ADDITIONAL_WINDOW_SIZE; - let default_rem_size = 16.0; - let scale_factor = current_rem_size / default_rem_size; - let scaled_bounds: gpui::Size = default_bounds.map(|axis| axis * scale_factor); - - let app_id = ReleaseChannel::global(cx).app_id(); - let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") { - Ok(val) if val == "server" => gpui::WindowDecorations::Server, - Ok(val) if val == "client" => gpui::WindowDecorations::Client, - _ => gpui::WindowDecorations::Client, - }; - - cx.open_window( - WindowOptions { - titlebar: Some(TitlebarOptions { - title: Some("Zed — Settings".into()), - appears_transparent: true, - traffic_light_position: Some(point(px(12.0), px(12.0))), - }), - focus: true, - show: true, - is_movable: true, - kind: gpui::WindowKind::Floating, - window_background: cx.theme().window_background_appearance(), - app_id: Some(app_id.to_owned()), - window_decorations: Some(window_decorations), - window_min_size: Some(gpui::Size { - width: px(360.0), - height: px(240.0), - }), - window_bounds: Some(WindowBounds::centered(scaled_bounds, cx)), - ..Default::default() - }, - |window, cx| { - let settings_window = - cx.new(|cx| SettingsWindow::new(Some(workspace_handle), window, cx)); - settings_window.update(cx, |settings_window, cx| { - if let Some(path) = path { - open_path(&path, open_project_settings, settings_window, window, cx); - } else if open_project_settings { - if let Some(file_index) = settings_window - .files - .iter() - .position(|(file, _)| file.worktree_id().is_some()) - { - settings_window.change_file(file_index, window, cx); - } - - settings_window.fetch_files(window, cx); - } - }); - - settings_window - }, - ) - .log_err(); - }); -} - -/// The current sub page path that is selected. -/// If this is empty the selected page is rendered, -/// otherwise the last sub page gets rendered. -/// -/// Global so that `pick` and `write` callbacks can access it -/// and use it to dynamically render sub pages (e.g. for language settings) -static SUB_PAGE_STACK: LazyLock>> = LazyLock::new(|| RwLock::new(Vec::new())); - -fn sub_page_stack() -> std::sync::RwLockReadGuard<'static, Vec> { - SUB_PAGE_STACK - .read() - .expect("SUB_PAGE_STACK is never poisoned") -} - -fn sub_page_stack_mut() -> std::sync::RwLockWriteGuard<'static, Vec> { - SUB_PAGE_STACK - .write() - .expect("SUB_PAGE_STACK is never poisoned") -} - -pub struct SettingsWindow { - title_bar: Option>, - original_window: Option>, - files: Vec<(SettingsUiFile, FocusHandle)>, - worktree_root_dirs: HashMap, - current_file: SettingsUiFile, - pages: Vec, - search_bar: Entity, - search_task: Option>, - /// Index into navbar_entries - navbar_entry: usize, - navbar_entries: Vec, - navbar_scroll_handle: UniformListScrollHandle, - /// [page_index][page_item_index] will be false - /// when the item is filtered out either by searches - /// or by the current file - navbar_focus_subscriptions: Vec, - filter_table: Vec>, - has_query: bool, - content_handles: Vec>>, - sub_page_scroll_handle: ScrollHandle, - focus_handle: FocusHandle, - navbar_focus_handle: Entity, - content_focus_handle: Entity, - files_focus_handle: FocusHandle, - search_index: Option>, - list_state: ListState, - shown_errors: HashSet, -} - -struct SearchIndex { - bm25_engine: bm25::SearchEngine, - fuzzy_match_candidates: Vec, - key_lut: Vec, -} - -struct SearchKeyLUTEntry { - page_index: usize, - header_index: usize, - item_index: usize, - json_path: Option<&'static str>, -} - -struct SubPage { - link: SubPageLink, - section_header: &'static str, -} - -#[derive(Debug)] -struct NavBarEntry { - title: &'static str, - is_root: bool, - expanded: bool, - page_index: usize, - item_index: Option, - focus_handle: FocusHandle, -} - -struct SettingsPage { - title: &'static str, - items: Vec, -} - -#[derive(PartialEq)] -enum SettingsPageItem { - SectionHeader(&'static str), - SettingItem(SettingItem), - SubPageLink(SubPageLink), - DynamicItem(DynamicItem), -} - -impl std::fmt::Debug for SettingsPageItem { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SettingsPageItem::SectionHeader(header) => write!(f, "SectionHeader({})", header), - SettingsPageItem::SettingItem(setting_item) => { - write!(f, "SettingItem({})", setting_item.title) - } - SettingsPageItem::SubPageLink(sub_page_link) => { - write!(f, "SubPageLink({})", sub_page_link.title) - } - SettingsPageItem::DynamicItem(dynamic_item) => { - write!(f, "DynamicItem({})", dynamic_item.discriminant.title) - } - } - } -} - -impl SettingsPageItem { - fn render( - &self, - settings_window: &SettingsWindow, - item_index: usize, - is_last: bool, - window: &mut Window, - cx: &mut Context, - ) -> AnyElement { - let file = settings_window.current_file.clone(); - - let apply_padding = |element: Stateful
| -> Stateful
{ - let element = element.pt_4(); - if is_last { - element.pb_10() - } else { - element.pb_4() - } - }; - - let mut render_setting_item_inner = - |setting_item: &SettingItem, - padding: bool, - sub_field: bool, - cx: &mut Context| { - let renderer = cx.default_global::().clone(); - let (_, found) = setting_item.field.file_set_in(file.clone(), cx); - - let renderers = renderer.renderers.borrow(); - - let field_renderer = - renderers.get(&AnySettingField::type_id(setting_item.field.as_ref())); - let field_renderer_or_warning = - field_renderer.ok_or("NO RENDERER").and_then(|renderer| { - if cfg!(debug_assertions) && !found { - Err("NO DEFAULT") - } else { - Ok(renderer) - } - }); - - let field = match field_renderer_or_warning { - Ok(field_renderer) => window.with_id(item_index, |window| { - field_renderer( - settings_window, - setting_item, - file.clone(), - setting_item.metadata.as_deref(), - sub_field, - window, - cx, - ) - }), - Err(warning) => render_settings_item( - settings_window, - setting_item, - file.clone(), - Button::new("error-warning", warning) - .style(ButtonStyle::Outlined) - .size(ButtonSize::Medium) - .icon(Some(IconName::Debug)) - .icon_position(IconPosition::Start) - .icon_color(Color::Error) - .tab_index(0_isize) - .tooltip(Tooltip::text(setting_item.field.type_name())) - .into_any_element(), - sub_field, - cx, - ), - }; - - let field = if padding { - field.map(apply_padding) - } else { - field - }; - - (field, field_renderer_or_warning.is_ok()) - }; - - match self { - SettingsPageItem::SectionHeader(header) => v_flex() - .w_full() - .px_8() - .gap_1p5() - .child( - Label::new(SharedString::new_static(header)) - .size(LabelSize::Small) - .color(Color::Muted) - .buffer_font(cx), - ) - .child(Divider::horizontal().color(DividerColor::BorderFaded)) - .into_any_element(), - SettingsPageItem::SettingItem(setting_item) => { - let (field_with_padding, _) = - render_setting_item_inner(setting_item, true, false, cx); - - v_flex() - .group("setting-item") - .px_8() - .child(field_with_padding) - .when(!is_last, |this| this.child(Divider::horizontal())) - .into_any_element() - } - SettingsPageItem::SubPageLink(sub_page_link) => v_flex() - .group("setting-item") - .px_8() - .child( - h_flex() - .id(sub_page_link.title.clone()) - .w_full() - .min_w_0() - .justify_between() - .map(apply_padding) - .child( - v_flex() - .w_full() - .max_w_1_2() - .child(Label::new(sub_page_link.title.clone())), - ) - .child( - Button::new( - ("sub-page".into(), sub_page_link.title.clone()), - "Configure", - ) - .icon(IconName::ChevronRight) - .tab_index(0_isize) - .icon_position(IconPosition::End) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .style(ButtonStyle::OutlinedGhost) - .size(ButtonSize::Medium) - .on_click({ - let sub_page_link = sub_page_link.clone(); - cx.listener(move |this, _, _, cx| { - let mut section_index = item_index; - let current_page = this.current_page(); - - while !matches!( - current_page.items[section_index], - SettingsPageItem::SectionHeader(_) - ) { - section_index -= 1; - } - - let SettingsPageItem::SectionHeader(header) = - current_page.items[section_index] - else { - unreachable!( - "All items always have a section header above them" - ) - }; - - this.push_sub_page(sub_page_link.clone(), header, cx) - }) - }), - ), - ) - .when(!is_last, |this| this.child(Divider::horizontal())) - .into_any_element(), - SettingsPageItem::DynamicItem(DynamicItem { - discriminant: discriminant_setting_item, - pick_discriminant, - fields, - }) => { - let file = file.to_settings(); - let discriminant = SettingsStore::global(cx) - .get_value_from_file(file, *pick_discriminant) - .1; - - let (discriminant_element, rendered_ok) = - render_setting_item_inner(discriminant_setting_item, true, false, cx); - - let has_sub_fields = - rendered_ok && discriminant.map(|d| !fields[d].is_empty()).unwrap_or(false); - - let mut content = v_flex() - .id("dynamic-item") - .child( - div() - .group("setting-item") - .px_8() - .child(discriminant_element.when(has_sub_fields, |this| this.pb_4())), - ) - .when(!has_sub_fields && !is_last, |this| { - this.child(h_flex().px_8().child(Divider::horizontal())) - }); - - if rendered_ok { - let discriminant = - discriminant.expect("This should be Some if rendered_ok is true"); - let sub_fields = &fields[discriminant]; - let sub_field_count = sub_fields.len(); - - for (index, field) in sub_fields.iter().enumerate() { - let is_last_sub_field = index == sub_field_count - 1; - let (raw_field, _) = render_setting_item_inner(field, false, true, cx); - - content = content.child( - raw_field - .group("setting-sub-item") - .mx_8() - .p_4() - .border_t_1() - .when(is_last_sub_field, |this| this.border_b_1()) - .when(is_last_sub_field && is_last, |this| this.mb_8()) - .border_dashed() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().element_background.opacity(0.2)), - ); - } - } - - return content.into_any_element(); - } - } - } -} - -fn render_settings_item( - settings_window: &SettingsWindow, - setting_item: &SettingItem, - file: SettingsUiFile, - control: AnyElement, - sub_field: bool, - cx: &mut Context<'_, SettingsWindow>, -) -> Stateful
{ - let (found_in_file, _) = setting_item.field.file_set_in(file.clone(), cx); - let file_set_in = SettingsUiFile::from_settings(found_in_file.clone()); - - let clipboard_has_link = cx - .read_from_clipboard() - .and_then(|entry| entry.text()) - .map_or(false, |maybe_url| { - setting_item.field.json_path().is_some() - && maybe_url.strip_prefix("zed://settings/") == setting_item.field.json_path() - }); - - let (link_icon, link_icon_color) = if clipboard_has_link { - (IconName::Check, Color::Success) - } else { - (IconName::Link, Color::Muted) - }; - - h_flex() - .id(setting_item.title) - .min_w_0() - .justify_between() - .child( - v_flex() - .relative() - .w_1_2() - .child( - h_flex() - .w_full() - .gap_1() - .child(Label::new(SharedString::new_static(setting_item.title))) - .when_some( - if sub_field { - None - } else { - setting_item - .field - .reset_to_default_fn(&file, &found_in_file, cx) - }, - |this, reset_to_default| { - this.child( - IconButton::new("reset-to-default-btn", IconName::Undo) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Reset to Default")) - .on_click({ - move |_, _, cx| { - reset_to_default(cx); - } - }), - ) - }, - ) - .when_some( - file_set_in.filter(|file_set_in| file_set_in != &file), - |this, file_set_in| { - this.child( - Label::new(format!( - "— Modified in {}", - settings_window - .display_name(&file_set_in) - .expect("File name should exist") - )) - .color(Color::Muted) - .size(LabelSize::Small), - ) - }, - ), - ) - .child( - Label::new(SharedString::new_static(setting_item.description)) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - .child(control) - .when(sub_page_stack().is_empty(), |this| { - // Intentionally using the description to make the icon button - // unique because some items share the same title (e.g., "Font Size") - let icon_button_id = - SharedString::new(format!("copy-link-btn-{}", setting_item.description)); - - this.child( - div() - .absolute() - .top(rems_from_px(18.)) - .map(|this| { - if sub_field { - this.visible_on_hover("setting-sub-item") - .left(rems_from_px(-8.5)) - } else { - this.visible_on_hover("setting-item") - .left(rems_from_px(-22.)) - } - }) - .child({ - IconButton::new(icon_button_id, link_icon) - .icon_color(link_icon_color) - .icon_size(IconSize::Small) - .shape(IconButtonShape::Square) - .tooltip(Tooltip::text("Copy Link")) - .when_some(setting_item.field.json_path(), |this, path| { - this.on_click(cx.listener(move |_, _, _, cx| { - let link = format!("zed://settings/{}", path); - cx.write_to_clipboard(ClipboardItem::new_string(link)); - cx.notify(); - })) - }) - }), - ) - }) -} - -struct SettingItem { - title: &'static str, - description: &'static str, - field: Box, - metadata: Option>, - files: FileMask, -} - -struct DynamicItem { - discriminant: SettingItem, - pick_discriminant: fn(&SettingsContent) -> Option, - fields: Vec>, -} - -impl PartialEq for DynamicItem { - fn eq(&self, other: &Self) -> bool { - self.discriminant == other.discriminant && self.fields == other.fields - } -} - -#[derive(PartialEq, Eq, Clone, Copy)] -struct FileMask(u8); - -impl std::fmt::Debug for FileMask { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "FileMask(")?; - let mut items = vec![]; - - if self.contains(USER) { - items.push("USER"); - } - if self.contains(PROJECT) { - items.push("LOCAL"); - } - if self.contains(SERVER) { - items.push("SERVER"); - } - - write!(f, "{})", items.join(" | ")) - } -} - -const USER: FileMask = FileMask(1 << 0); -const PROJECT: FileMask = FileMask(1 << 2); -const SERVER: FileMask = FileMask(1 << 3); - -impl std::ops::BitAnd for FileMask { - type Output = Self; - - fn bitand(self, other: Self) -> Self { - Self(self.0 & other.0) - } -} - -impl std::ops::BitOr for FileMask { - type Output = Self; - - fn bitor(self, other: Self) -> Self { - Self(self.0 | other.0) - } -} - -impl FileMask { - fn contains(&self, other: FileMask) -> bool { - self.0 & other.0 != 0 - } -} - -impl PartialEq for SettingItem { - fn eq(&self, other: &Self) -> bool { - self.title == other.title - && self.description == other.description - && (match (&self.metadata, &other.metadata) { - (None, None) => true, - (Some(m1), Some(m2)) => m1.placeholder == m2.placeholder, - _ => false, - }) - } -} - -#[derive(Clone)] -struct SubPageLink { - title: SharedString, - files: FileMask, - render: Arc< - dyn Fn(&mut SettingsWindow, &mut Window, &mut Context) -> AnyElement - + 'static - + Send - + Sync, - >, -} - -impl PartialEq for SubPageLink { - fn eq(&self, other: &Self) -> bool { - self.title == other.title - } -} - -fn all_language_names(cx: &App) -> Vec { - workspace::AppState::global(cx) - .upgrade() - .map_or(vec![], |state| { - state - .languages - .language_names() - .into_iter() - .filter(|name| name.as_ref() != "Zed Keybind Context") - .map(Into::into) - .collect() - }) -} - -#[allow(unused)] -#[derive(Clone, PartialEq, Debug)] -enum SettingsUiFile { - User, // Uses all settings. - Project((WorktreeId, Arc)), // Has a special name, and special set of settings - Server(&'static str), // Uses a special name, and the user settings -} - -impl SettingsUiFile { - fn setting_type(&self) -> &'static str { - match self { - SettingsUiFile::User => "User", - SettingsUiFile::Project(_) => "Project", - SettingsUiFile::Server(_) => "Server", - } - } - - fn is_server(&self) -> bool { - matches!(self, SettingsUiFile::Server(_)) - } - - fn worktree_id(&self) -> Option { - match self { - SettingsUiFile::User => None, - SettingsUiFile::Project((worktree_id, _)) => Some(*worktree_id), - SettingsUiFile::Server(_) => None, - } - } - - fn from_settings(file: settings::SettingsFile) -> Option { - Some(match file { - settings::SettingsFile::User => SettingsUiFile::User, - settings::SettingsFile::Project(location) => SettingsUiFile::Project(location), - settings::SettingsFile::Server => SettingsUiFile::Server("todo: server name"), - settings::SettingsFile::Default => return None, - settings::SettingsFile::Global => return None, - }) - } - - fn to_settings(&self) -> settings::SettingsFile { - match self { - SettingsUiFile::User => settings::SettingsFile::User, - SettingsUiFile::Project(location) => settings::SettingsFile::Project(location.clone()), - SettingsUiFile::Server(_) => settings::SettingsFile::Server, - } - } - - fn mask(&self) -> FileMask { - match self { - SettingsUiFile::User => USER, - SettingsUiFile::Project(_) => PROJECT, - SettingsUiFile::Server(_) => SERVER, - } - } -} - -impl SettingsWindow { - fn new( - original_window: Option>, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let font_family_cache = theme::FontFamilyCache::global(cx); - - cx.spawn(async move |this, cx| { - font_family_cache.prefetch(cx).await; - this.update(cx, |_, cx| { - cx.notify(); - }) - }) - .detach(); - - let current_file = SettingsUiFile::User; - let search_bar = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Search settings…", window, cx); - editor - }); - - cx.subscribe(&search_bar, |this, _, event: &EditorEvent, cx| { - let EditorEvent::Edited { transaction_id: _ } = event else { - return; - }; - - this.update_matches(cx); - }) - .detach(); - - cx.observe_global_in::(window, move |this, window, cx| { - this.fetch_files(window, cx); - cx.notify(); - }) - .detach(); - - cx.on_window_closed(|cx| { - if let Some(existing_window) = cx - .windows() - .into_iter() - .find_map(|window| window.downcast::()) - && cx.windows().len() == 1 - { - cx.update_window(*existing_window, |_, window, _| { - window.remove_window(); - }) - .ok(); - - telemetry::event!("Settings Closed") - } - }) - .detach(); - - if let Some(app_state) = AppState::global(cx).upgrade() { - for project in app_state - .workspace_store - .read(cx) - .workspaces() - .iter() - .filter_map(|space| { - space - .read(cx) - .ok() - .map(|workspace| workspace.project().clone()) - }) - .collect::>() - { - cx.observe_release_in(&project, window, |this, _, window, cx| { - this.fetch_files(window, cx) - }) - .detach(); - cx.subscribe_in(&project, window, Self::handle_project_event) - .detach(); - } - - for workspace in app_state - .workspace_store - .read(cx) - .workspaces() - .iter() - .filter_map(|space| space.entity(cx).ok()) - { - cx.observe_release_in(&workspace, window, |this, _, window, cx| { - this.fetch_files(window, cx) - }) - .detach(); - } - } else { - log::error!("App state doesn't exist when creating a new settings window"); - } - - let this_weak = cx.weak_entity(); - cx.observe_new::({ - let this_weak = this_weak.clone(); - - move |_, window, cx| { - let project = cx.entity(); - let Some(window) = window else { - return; - }; - - this_weak - .update(cx, |this, cx| { - this.fetch_files(window, cx); - cx.observe_release_in(&project, window, |_, _, window, cx| { - cx.defer_in(window, |this, window, cx| this.fetch_files(window, cx)); - }) - .detach(); - - cx.subscribe_in(&project, window, Self::handle_project_event) - .detach(); - }) - .ok(); - } - }) - .detach(); - - cx.observe_new::(move |_, window, cx| { - let workspace = cx.entity(); - let Some(window) = window else { - return; - }; - - this_weak - .update(cx, |this, cx| { - this.fetch_files(window, cx); - cx.observe_release_in(&workspace, window, |this, _, window, cx| { - this.fetch_files(window, cx) - }) - .detach(); - }) - .ok(); - }) - .detach(); - - let title_bar = if !cfg!(target_os = "macos") { - Some(cx.new(|cx| PlatformTitleBar::new("settings-title-bar", cx))) - } else { - None - }; - - // high overdraw value so the list scrollbar len doesn't change too much - let list_state = gpui::ListState::new(0, gpui::ListAlignment::Top, px(0.0)).measure_all(); - list_state.set_scroll_handler(|_, _, _| {}); - - let mut this = Self { - title_bar, - original_window, - - worktree_root_dirs: HashMap::default(), - files: vec![], - - current_file: current_file, - pages: vec![], - navbar_entries: vec![], - navbar_entry: 0, - navbar_scroll_handle: UniformListScrollHandle::default(), - search_bar, - search_task: None, - filter_table: vec![], - has_query: false, - content_handles: vec![], - sub_page_scroll_handle: ScrollHandle::new(), - focus_handle: cx.focus_handle(), - navbar_focus_handle: NonFocusableHandle::new( - NAVBAR_CONTAINER_TAB_INDEX, - false, - window, - cx, - ), - navbar_focus_subscriptions: vec![], - content_focus_handle: NonFocusableHandle::new( - CONTENT_CONTAINER_TAB_INDEX, - false, - window, - cx, - ), - files_focus_handle: cx - .focus_handle() - .tab_index(HEADER_CONTAINER_TAB_INDEX) - .tab_stop(false), - search_index: None, - shown_errors: HashSet::default(), - list_state, - }; - - this.fetch_files(window, cx); - this.build_ui(window, cx); - this.build_search_index(); - - this.search_bar.update(cx, |editor, cx| { - editor.focus_handle(cx).focus(window); - }); - - this - } - - fn handle_project_event( - &mut self, - _: &Entity, - event: &project::Event, - window: &mut Window, - cx: &mut Context, - ) { - match event { - project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => { - cx.defer_in(window, |this, window, cx| { - this.fetch_files(window, cx); - }); - } - _ => {} - } - } - - fn toggle_navbar_entry(&mut self, nav_entry_index: usize) { - // We can only toggle root entries - if !self.navbar_entries[nav_entry_index].is_root { - return; - } - - let expanded = &mut self.navbar_entries[nav_entry_index].expanded; - *expanded = !*expanded; - self.navbar_entry = nav_entry_index; - self.reset_list_state(); - } - - fn build_navbar(&mut self, cx: &App) { - let mut navbar_entries = Vec::new(); - - for (page_index, page) in self.pages.iter().enumerate() { - navbar_entries.push(NavBarEntry { - title: page.title, - is_root: true, - expanded: false, - page_index, - item_index: None, - focus_handle: cx.focus_handle().tab_index(0).tab_stop(true), - }); - - for (item_index, item) in page.items.iter().enumerate() { - let SettingsPageItem::SectionHeader(title) = item else { - continue; - }; - navbar_entries.push(NavBarEntry { - title, - is_root: false, - expanded: false, - page_index, - item_index: Some(item_index), - focus_handle: cx.focus_handle().tab_index(0).tab_stop(true), - }); - } - } - - self.navbar_entries = navbar_entries; - } - - fn setup_navbar_focus_subscriptions( - &mut self, - window: &mut Window, - cx: &mut Context, - ) { - let mut focus_subscriptions = Vec::new(); - - for entry_index in 0..self.navbar_entries.len() { - let focus_handle = self.navbar_entries[entry_index].focus_handle.clone(); - - let subscription = cx.on_focus( - &focus_handle, - window, - move |this: &mut SettingsWindow, - window: &mut Window, - cx: &mut Context| { - this.open_and_scroll_to_navbar_entry(entry_index, None, false, window, cx); - }, - ); - focus_subscriptions.push(subscription); - } - self.navbar_focus_subscriptions = focus_subscriptions; - } - - fn visible_navbar_entries(&self) -> impl Iterator { - let mut index = 0; - let entries = &self.navbar_entries; - let search_matches = &self.filter_table; - let has_query = self.has_query; - std::iter::from_fn(move || { - while index < entries.len() { - let entry = &entries[index]; - let included_in_search = if let Some(item_index) = entry.item_index { - search_matches[entry.page_index][item_index] - } else { - search_matches[entry.page_index].iter().any(|b| *b) - || search_matches[entry.page_index].is_empty() - }; - if included_in_search { - break; - } - index += 1; - } - if index >= self.navbar_entries.len() { - return None; - } - let entry = &entries[index]; - let entry_index = index; - - index += 1; - if entry.is_root && !entry.expanded && !has_query { - while index < entries.len() { - if entries[index].is_root { - break; - } - index += 1; - } - } - - return Some((entry_index, entry)); - }) - } - - fn filter_matches_to_file(&mut self) { - let current_file = self.current_file.mask(); - for (page, page_filter) in std::iter::zip(&self.pages, &mut self.filter_table) { - let mut header_index = 0; - let mut any_found_since_last_header = true; - - for (index, item) in page.items.iter().enumerate() { - match item { - SettingsPageItem::SectionHeader(_) => { - if !any_found_since_last_header { - page_filter[header_index] = false; - } - header_index = index; - any_found_since_last_header = false; - } - SettingsPageItem::SettingItem(SettingItem { files, .. }) - | SettingsPageItem::SubPageLink(SubPageLink { files, .. }) - | SettingsPageItem::DynamicItem(DynamicItem { - discriminant: SettingItem { files, .. }, - .. - }) => { - if !files.contains(current_file) { - page_filter[index] = false; - } else { - any_found_since_last_header = true; - } - } - } - } - if let Some(last_header) = page_filter.get_mut(header_index) - && !any_found_since_last_header - { - *last_header = false; - } - } - } - - fn update_matches(&mut self, cx: &mut Context) { - self.search_task.take(); - let mut query = self.search_bar.read(cx).text(cx); - if query.is_empty() || self.search_index.is_none() { - for page in &mut self.filter_table { - page.fill(true); - } - self.has_query = false; - self.filter_matches_to_file(); - self.reset_list_state(); - cx.notify(); - return; - } - - let is_json_link_query; - if query.starts_with("#") { - query.remove(0); - is_json_link_query = true; - } else { - is_json_link_query = false; - } - - let search_index = self.search_index.as_ref().unwrap().clone(); - - fn update_matches_inner( - this: &mut SettingsWindow, - search_index: &SearchIndex, - match_indices: impl Iterator, - cx: &mut Context, - ) { - for page in &mut this.filter_table { - page.fill(false); - } - - for match_index in match_indices { - let SearchKeyLUTEntry { - page_index, - header_index, - item_index, - .. - } = search_index.key_lut[match_index]; - let page = &mut this.filter_table[page_index]; - page[header_index] = true; - page[item_index] = true; - } - this.has_query = true; - this.filter_matches_to_file(); - this.open_first_nav_page(); - this.reset_list_state(); - cx.notify(); - } - - self.search_task = Some(cx.spawn(async move |this, cx| { - if is_json_link_query { - let mut indices = vec![]; - for (index, SearchKeyLUTEntry { json_path, .. }) in - search_index.key_lut.iter().enumerate() - { - let Some(json_path) = json_path else { - continue; - }; - - if let Some(post) = query.strip_prefix(json_path) - && (post.is_empty() || post.starts_with('.')) - { - indices.push(index); - } - } - if !indices.is_empty() { - this.update(cx, |this, cx| { - update_matches_inner(this, search_index.as_ref(), indices.into_iter(), cx); - }) - .ok(); - return; - } - } - let bm25_task = cx.background_spawn({ - let search_index = search_index.clone(); - let max_results = search_index.key_lut.len(); - let query = query.clone(); - async move { search_index.bm25_engine.search(&query, max_results) } - }); - let cancel_flag = std::sync::atomic::AtomicBool::new(false); - let fuzzy_search_task = fuzzy::match_strings( - search_index.fuzzy_match_candidates.as_slice(), - &query, - false, - true, - search_index.fuzzy_match_candidates.len(), - &cancel_flag, - cx.background_executor().clone(), - ); - - let fuzzy_matches = fuzzy_search_task.await; - - _ = this - .update(cx, |this, cx| { - // For tuning the score threshold - // for fuzzy_match in &fuzzy_matches { - // let SearchItemKey { - // page_index, - // header_index, - // item_index, - // } = search_index.key_lut[fuzzy_match.candidate_id]; - // let SettingsPageItem::SectionHeader(header) = - // this.pages[page_index].items[header_index] - // else { - // continue; - // }; - // let SettingsPageItem::SettingItem(SettingItem { - // title, description, .. - // }) = this.pages[page_index].items[item_index] - // else { - // continue; - // }; - // let score = fuzzy_match.score; - // eprint!("# {header} :: QUERY = {query} :: SCORE = {score}\n{title}\n{description}\n\n"); - // } - update_matches_inner( - this, - search_index.as_ref(), - fuzzy_matches - .into_iter() - // MAGIC NUMBER: Was found to have right balance between not too many weird matches, but also - // flexible enough to catch misspellings and <4 letter queries - // More flexible is good for us here because fuzzy matches will only be used for things that don't - // match using bm25 - .take_while(|fuzzy_match| fuzzy_match.score >= 0.3) - .map(|fuzzy_match| fuzzy_match.candidate_id), - cx, - ); - }) - .ok(); - - let bm25_matches = bm25_task.await; - - _ = this - .update(cx, |this, cx| { - if bm25_matches.is_empty() { - return; - } - update_matches_inner( - this, - search_index.as_ref(), - bm25_matches - .into_iter() - .map(|bm25_match| bm25_match.document.id), - cx, - ); - }) - .ok(); - - cx.background_executor().timer(Duration::from_secs(1)).await; - telemetry::event!("Settings Searched", query = query) - })); - } - - fn build_filter_table(&mut self) { - self.filter_table = self - .pages - .iter() - .map(|page| vec![true; page.items.len()]) - .collect::>(); - } - - fn build_search_index(&mut self) { - let mut key_lut: Vec = vec![]; - let mut documents = Vec::default(); - let mut fuzzy_match_candidates = Vec::default(); - - fn push_candidates( - fuzzy_match_candidates: &mut Vec, - key_index: usize, - input: &str, - ) { - for word in input.split_ascii_whitespace() { - fuzzy_match_candidates.push(StringMatchCandidate::new(key_index, word)); - } - } - - // PERF: We are currently searching all items even in project files - // where many settings are filtered out, using the logic in filter_matches_to_file - // we could only search relevant items based on the current file - for (page_index, page) in self.pages.iter().enumerate() { - let mut header_index = 0; - let mut header_str = ""; - for (item_index, item) in page.items.iter().enumerate() { - let key_index = key_lut.len(); - let mut json_path = None; - match item { - SettingsPageItem::DynamicItem(DynamicItem { - discriminant: item, .. - }) - | SettingsPageItem::SettingItem(item) => { - json_path = item - .field - .json_path() - .map(|path| path.trim_end_matches('$')); - documents.push(bm25::Document { - id: key_index, - contents: [page.title, header_str, item.title, item.description] - .join("\n"), - }); - push_candidates(&mut fuzzy_match_candidates, key_index, item.title); - push_candidates(&mut fuzzy_match_candidates, key_index, item.description); - } - SettingsPageItem::SectionHeader(header) => { - documents.push(bm25::Document { - id: key_index, - contents: header.to_string(), - }); - push_candidates(&mut fuzzy_match_candidates, key_index, header); - header_index = item_index; - header_str = *header; - } - SettingsPageItem::SubPageLink(sub_page_link) => { - documents.push(bm25::Document { - id: key_index, - contents: [page.title, header_str, sub_page_link.title.as_ref()] - .join("\n"), - }); - push_candidates( - &mut fuzzy_match_candidates, - key_index, - sub_page_link.title.as_ref(), - ); - } - } - push_candidates(&mut fuzzy_match_candidates, key_index, page.title); - push_candidates(&mut fuzzy_match_candidates, key_index, header_str); - - key_lut.push(SearchKeyLUTEntry { - page_index, - header_index, - item_index, - json_path, - }); - } - } - let engine = - bm25::SearchEngineBuilder::with_documents(bm25::Language::English, documents).build(); - self.search_index = Some(Arc::new(SearchIndex { - bm25_engine: engine, - key_lut, - fuzzy_match_candidates, - })); - } - - fn build_content_handles(&mut self, window: &mut Window, cx: &mut Context) { - self.content_handles = self - .pages - .iter() - .map(|page| { - std::iter::repeat_with(|| NonFocusableHandle::new(0, false, window, cx)) - .take(page.items.len()) - .collect() - }) - .collect::>(); - } - - fn reset_list_state(&mut self) { - // plus one for the title - let mut visible_items_count = self.visible_page_items().count(); - - if visible_items_count > 0 { - // show page title if page is non empty - visible_items_count += 1; - } - - self.list_state.reset(visible_items_count); - } - - fn build_ui(&mut self, window: &mut Window, cx: &mut Context) { - if self.pages.is_empty() { - self.pages = page_data::settings_data(cx); - self.build_navbar(cx); - self.setup_navbar_focus_subscriptions(window, cx); - self.build_content_handles(window, cx); - } - sub_page_stack_mut().clear(); - // PERF: doesn't have to be rebuilt, can just be filled with true. pages is constant once it is built - self.build_filter_table(); - self.reset_list_state(); - self.update_matches(cx); - - cx.notify(); - } - - #[track_caller] - fn fetch_files(&mut self, window: &mut Window, cx: &mut Context) { - self.worktree_root_dirs.clear(); - let prev_files = self.files.clone(); - let settings_store = cx.global::(); - let mut ui_files = vec![]; - let mut all_files = settings_store.get_all_files(); - if !all_files.contains(&settings::SettingsFile::User) { - all_files.push(settings::SettingsFile::User); - } - for file in all_files { - let Some(settings_ui_file) = SettingsUiFile::from_settings(file) else { - continue; - }; - if settings_ui_file.is_server() { - continue; - } - - if let Some(worktree_id) = settings_ui_file.worktree_id() { - let directory_name = all_projects(cx) - .find_map(|project| project.read(cx).worktree_for_id(worktree_id, cx)) - .and_then(|worktree| worktree.read(cx).root_dir()) - .and_then(|root_dir| { - root_dir - .file_name() - .map(|os_string| os_string.to_string_lossy().to_string()) - }); - - let Some(directory_name) = directory_name else { - log::error!( - "No directory name found for settings file at worktree ID: {}", - worktree_id - ); - continue; - }; - - self.worktree_root_dirs.insert(worktree_id, directory_name); - } - - let focus_handle = prev_files - .iter() - .find_map(|(prev_file, handle)| { - (prev_file == &settings_ui_file).then(|| handle.clone()) - }) - .unwrap_or_else(|| cx.focus_handle().tab_index(0).tab_stop(true)); - ui_files.push((settings_ui_file, focus_handle)); - } - - ui_files.reverse(); - - let mut missing_worktrees = Vec::new(); - - for worktree in all_projects(cx) - .flat_map(|project| project.read(cx).visible_worktrees(cx)) - .filter(|tree| !self.worktree_root_dirs.contains_key(&tree.read(cx).id())) - { - let worktree = worktree.read(cx); - let worktree_id = worktree.id(); - let Some(directory_name) = worktree.root_dir().and_then(|file| { - file.file_name() - .map(|os_string| os_string.to_string_lossy().to_string()) - }) else { - continue; - }; - - missing_worktrees.push((worktree_id, directory_name.clone())); - let path = RelPath::empty().to_owned().into_arc(); - - let settings_ui_file = SettingsUiFile::Project((worktree_id, path)); - - let focus_handle = prev_files - .iter() - .find_map(|(prev_file, handle)| { - (prev_file == &settings_ui_file).then(|| handle.clone()) - }) - .unwrap_or_else(|| cx.focus_handle().tab_index(0).tab_stop(true)); - - ui_files.push((settings_ui_file, focus_handle)); - } - - self.worktree_root_dirs.extend(missing_worktrees); - - self.files = ui_files; - let current_file_still_exists = self - .files - .iter() - .any(|(file, _)| file == &self.current_file); - if !current_file_still_exists { - self.change_file(0, window, cx); - } - } - - fn open_navbar_entry_page(&mut self, navbar_entry: usize) { - if !self.is_nav_entry_visible(navbar_entry) { - self.open_first_nav_page(); - } - - let is_new_page = self.navbar_entries[self.navbar_entry].page_index - != self.navbar_entries[navbar_entry].page_index; - self.navbar_entry = navbar_entry; - - // We only need to reset visible items when updating matches - // and selecting a new page - if is_new_page { - self.reset_list_state(); - } - - sub_page_stack_mut().clear(); - } - - fn open_first_nav_page(&mut self) { - let Some(first_navbar_entry_index) = self.visible_navbar_entries().next().map(|e| e.0) - else { - return; - }; - self.open_navbar_entry_page(first_navbar_entry_index); - } - - fn change_file(&mut self, ix: usize, window: &mut Window, cx: &mut Context) { - if ix >= self.files.len() { - self.current_file = SettingsUiFile::User; - self.build_ui(window, cx); - return; - } - - if self.files[ix].0 == self.current_file { - return; - } - self.current_file = self.files[ix].0.clone(); - - if let SettingsUiFile::Project((_, _)) = &self.current_file { - telemetry::event!("Setting Project Clicked"); - } - - self.build_ui(window, cx); - - if self - .visible_navbar_entries() - .any(|(index, _)| index == self.navbar_entry) - { - self.open_and_scroll_to_navbar_entry(self.navbar_entry, None, true, window, cx); - } else { - self.open_first_nav_page(); - }; - } - - fn render_files_header( - &self, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - static OVERFLOW_LIMIT: usize = 1; - - let file_button = - |ix, file: &SettingsUiFile, focus_handle, cx: &mut Context| { - Button::new( - ix, - self.display_name(&file) - .expect("Files should always have a name"), - ) - .toggle_state(file == &self.current_file) - .selected_style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .track_focus(focus_handle) - .on_click(cx.listener({ - let focus_handle = focus_handle.clone(); - move |this, _: &gpui::ClickEvent, window, cx| { - this.change_file(ix, window, cx); - focus_handle.focus(window); - } - })) - }; - - let this = cx.entity(); - - let selected_file_ix = self - .files - .iter() - .enumerate() - .skip(OVERFLOW_LIMIT) - .find_map(|(ix, (file, _))| { - if file == &self.current_file { - Some(ix) - } else { - None - } - }) - .unwrap_or(OVERFLOW_LIMIT); - let edit_in_json_id = SharedString::new(format!("edit-in-json-{}", selected_file_ix)); - - h_flex() - .w_full() - .gap_1() - .justify_between() - .track_focus(&self.files_focus_handle) - .tab_group() - .tab_index(HEADER_GROUP_TAB_INDEX) - .child( - h_flex() - .gap_1() - .children( - self.files.iter().enumerate().take(OVERFLOW_LIMIT).map( - |(ix, (file, focus_handle))| file_button(ix, file, focus_handle, cx), - ), - ) - .when(self.files.len() > OVERFLOW_LIMIT, |div| { - let (file, focus_handle) = &self.files[selected_file_ix]; - - div.child(file_button(selected_file_ix, file, focus_handle, cx)) - .when(self.files.len() > OVERFLOW_LIMIT + 1, |div| { - div.child( - DropdownMenu::new( - "more-files", - format!("+{}", self.files.len() - (OVERFLOW_LIMIT + 1)), - ContextMenu::build(window, cx, move |mut menu, _, _| { - for (mut ix, (file, focus_handle)) in self - .files - .iter() - .enumerate() - .skip(OVERFLOW_LIMIT + 1) - { - let (display_name, focus_handle) = - if selected_file_ix == ix { - ix = OVERFLOW_LIMIT; - ( - self.display_name(&self.files[ix].0), - self.files[ix].1.clone(), - ) - } else { - ( - self.display_name(&file), - focus_handle.clone(), - ) - }; - - menu = menu.entry( - display_name - .expect("Files should always have a name"), - None, - { - let this = this.clone(); - move |window, cx| { - this.update(cx, |this, cx| { - this.change_file(ix, window, cx); - }); - focus_handle.focus(window); - } - }, - ); - } - - menu - }), - ) - .style(DropdownStyle::Subtle) - .trigger_tooltip(Tooltip::text("View Other Projects")) - .trigger_icon(IconName::ChevronDown) - .attach(gpui::Corner::BottomLeft) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }) - .tab_index(0), - ) - }) - }), - ) - .child( - Button::new(edit_in_json_id, "Edit in settings.json") - .tab_index(0_isize) - .style(ButtonStyle::OutlinedGhost) - .tooltip(Tooltip::for_action_title_in( - "Edit in settings.json", - &OpenCurrentFile, - &self.focus_handle, - )) - .on_click(cx.listener(|this, _, window, cx| { - this.open_current_settings_file(window, cx); - })), - ) - } - - pub(crate) fn display_name(&self, file: &SettingsUiFile) -> Option { - match file { - SettingsUiFile::User => Some("User".to_string()), - SettingsUiFile::Project((worktree_id, path)) => self - .worktree_root_dirs - .get(&worktree_id) - .map(|directory_name| { - let path_style = PathStyle::local(); - if path.is_empty() { - directory_name.clone() - } else { - format!( - "{}{}{}", - directory_name, - path_style.primary_separator(), - path.display(path_style) - ) - } - }), - SettingsUiFile::Server(file) => Some(file.to_string()), - } - } - - // TODO: - // Reconsider this after preview launch - // fn file_location_str(&self) -> String { - // match &self.current_file { - // SettingsUiFile::User => "settings.json".to_string(), - // SettingsUiFile::Project((worktree_id, path)) => self - // .worktree_root_dirs - // .get(&worktree_id) - // .map(|directory_name| { - // let path_style = PathStyle::local(); - // let file_path = path.join(paths::local_settings_file_relative_path()); - // format!( - // "{}{}{}", - // directory_name, - // path_style.separator(), - // file_path.display(path_style) - // ) - // }) - // .expect("Current file should always be present in root dir map"), - // SettingsUiFile::Server(file) => file.to_string(), - // } - // } - - fn render_search(&self, _window: &mut Window, cx: &mut App) -> Div { - h_flex() - .py_1() - .px_1p5() - .mb_3() - .gap_1p5() - .rounded_sm() - .bg(cx.theme().colors().editor_background) - .border_1() - .border_color(cx.theme().colors().border) - .child(Icon::new(IconName::MagnifyingGlass).color(Color::Muted)) - .child(self.search_bar.clone()) - } - - fn render_nav( - &self, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let visible_count = self.visible_navbar_entries().count(); - - let focus_keybind_label = if self - .navbar_focus_handle - .read(cx) - .handle - .contains_focused(window, cx) - || self - .visible_navbar_entries() - .any(|(_, entry)| entry.focus_handle.is_focused(window)) - { - "Focus Content" - } else { - "Focus Navbar" - }; - - let mut key_context = KeyContext::new_with_defaults(); - key_context.add("NavigationMenu"); - key_context.add("menu"); - if self.search_bar.focus_handle(cx).is_focused(window) { - key_context.add("search"); - } - - v_flex() - .key_context(key_context) - .on_action(cx.listener(|this, _: &CollapseNavEntry, window, cx| { - let Some(focused_entry) = this.focused_nav_entry(window, cx) else { - return; - }; - let focused_entry_parent = this.root_entry_containing(focused_entry); - if this.navbar_entries[focused_entry_parent].expanded { - this.toggle_navbar_entry(focused_entry_parent); - window.focus(&this.navbar_entries[focused_entry_parent].focus_handle); - } - cx.notify(); - })) - .on_action(cx.listener(|this, _: &ExpandNavEntry, window, cx| { - let Some(focused_entry) = this.focused_nav_entry(window, cx) else { - return; - }; - if !this.navbar_entries[focused_entry].is_root { - return; - } - if !this.navbar_entries[focused_entry].expanded { - this.toggle_navbar_entry(focused_entry); - } - cx.notify(); - })) - .on_action( - cx.listener(|this, _: &FocusPreviousRootNavEntry, window, cx| { - let entry_index = this - .focused_nav_entry(window, cx) - .unwrap_or(this.navbar_entry); - let mut root_index = None; - for (index, entry) in this.visible_navbar_entries() { - if index >= entry_index { - break; - } - if entry.is_root { - root_index = Some(index); - } - } - let Some(previous_root_index) = root_index else { - return; - }; - this.focus_and_scroll_to_nav_entry(previous_root_index, window, cx); - }), - ) - .on_action(cx.listener(|this, _: &FocusNextRootNavEntry, window, cx| { - let entry_index = this - .focused_nav_entry(window, cx) - .unwrap_or(this.navbar_entry); - let mut root_index = None; - for (index, entry) in this.visible_navbar_entries() { - if index <= entry_index { - continue; - } - if entry.is_root { - root_index = Some(index); - break; - } - } - let Some(next_root_index) = root_index else { - return; - }; - this.focus_and_scroll_to_nav_entry(next_root_index, window, cx); - })) - .on_action(cx.listener(|this, _: &FocusFirstNavEntry, window, cx| { - if let Some((first_entry_index, _)) = this.visible_navbar_entries().next() { - this.focus_and_scroll_to_nav_entry(first_entry_index, window, cx); - } - })) - .on_action(cx.listener(|this, _: &FocusLastNavEntry, window, cx| { - if let Some((last_entry_index, _)) = this.visible_navbar_entries().last() { - this.focus_and_scroll_to_nav_entry(last_entry_index, window, cx); - } - })) - .on_action(cx.listener(|this, _: &FocusNextNavEntry, window, cx| { - let entry_index = this - .focused_nav_entry(window, cx) - .unwrap_or(this.navbar_entry); - let mut next_index = None; - for (index, _) in this.visible_navbar_entries() { - if index > entry_index { - next_index = Some(index); - break; - } - } - let Some(next_entry_index) = next_index else { - return; - }; - this.open_and_scroll_to_navbar_entry( - next_entry_index, - Some(gpui::ScrollStrategy::Bottom), - false, - window, - cx, - ); - })) - .on_action(cx.listener(|this, _: &FocusPreviousNavEntry, window, cx| { - let entry_index = this - .focused_nav_entry(window, cx) - .unwrap_or(this.navbar_entry); - let mut prev_index = None; - for (index, _) in this.visible_navbar_entries() { - if index >= entry_index { - break; - } - prev_index = Some(index); - } - let Some(prev_entry_index) = prev_index else { - return; - }; - this.open_and_scroll_to_navbar_entry( - prev_entry_index, - Some(gpui::ScrollStrategy::Top), - false, - window, - cx, - ); - })) - .w_56() - .h_full() - .p_2p5() - .when(cfg!(target_os = "macos"), |this| this.pt_10()) - .flex_none() - .border_r_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().panel_background) - .child(self.render_search(window, cx)) - .child( - v_flex() - .flex_1() - .overflow_hidden() - .track_focus(&self.navbar_focus_handle.focus_handle(cx)) - .tab_group() - .tab_index(NAVBAR_GROUP_TAB_INDEX) - .child( - uniform_list( - "settings-ui-nav-bar", - visible_count + 1, - cx.processor(move |this, range: Range, _, cx| { - this.visible_navbar_entries() - .skip(range.start.saturating_sub(1)) - .take(range.len()) - .map(|(entry_index, entry)| { - TreeViewItem::new( - ("settings-ui-navbar-entry", entry_index), - entry.title, - ) - .track_focus(&entry.focus_handle) - .root_item(entry.is_root) - .toggle_state(this.is_navbar_entry_selected(entry_index)) - .when(entry.is_root, |item| { - item.expanded(entry.expanded || this.has_query) - .on_toggle(cx.listener( - move |this, _, window, cx| { - this.toggle_navbar_entry(entry_index); - window.focus( - &this.navbar_entries[entry_index] - .focus_handle, - ); - cx.notify(); - }, - )) - }) - .on_click({ - let category = this.pages[entry.page_index].title; - let subcategory = - (!entry.is_root).then_some(entry.title); - - cx.listener(move |this, _, window, cx| { - telemetry::event!( - "Settings Navigation Clicked", - category = category, - subcategory = subcategory - ); - - this.open_and_scroll_to_navbar_entry( - entry_index, - None, - true, - window, - cx, - ); - }) - }) - }) - .collect() - }), - ) - .size_full() - .track_scroll(&self.navbar_scroll_handle), - ) - .vertical_scrollbar_for(&self.navbar_scroll_handle, window, cx), - ) - .child( - h_flex() - .w_full() - .h_8() - .p_2() - .pb_0p5() - .flex_shrink_0() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child( - KeybindingHint::new( - KeyBinding::for_action_in( - &ToggleFocusNav, - &self.navbar_focus_handle.focus_handle(cx), - cx, - ), - cx.theme().colors().surface_background.opacity(0.5), - ) - .suffix(focus_keybind_label), - ), - ) - } - - fn open_and_scroll_to_navbar_entry( - &mut self, - navbar_entry_index: usize, - scroll_strategy: Option, - focus_content: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.open_navbar_entry_page(navbar_entry_index); - cx.notify(); - - let mut handle_to_focus = None; - - if self.navbar_entries[navbar_entry_index].is_root - || !self.is_nav_entry_visible(navbar_entry_index) - { - self.sub_page_scroll_handle - .set_offset(point(px(0.), px(0.))); - if focus_content { - let Some(first_item_index) = - self.visible_page_items().next().map(|(index, _)| index) - else { - return; - }; - handle_to_focus = Some(self.focus_handle_for_content_element(first_item_index, cx)); - } else if !self.is_nav_entry_visible(navbar_entry_index) { - let Some(first_visible_nav_entry_index) = - self.visible_navbar_entries().next().map(|(index, _)| index) - else { - return; - }; - self.focus_and_scroll_to_nav_entry(first_visible_nav_entry_index, window, cx); - } else { - handle_to_focus = - Some(self.navbar_entries[navbar_entry_index].focus_handle.clone()); - } - } else { - let entry_item_index = self.navbar_entries[navbar_entry_index] - .item_index - .expect("Non-root items should have an item index"); - self.scroll_to_content_item(entry_item_index, window, cx); - if focus_content { - handle_to_focus = Some(self.focus_handle_for_content_element(entry_item_index, cx)); - } else { - handle_to_focus = - Some(self.navbar_entries[navbar_entry_index].focus_handle.clone()); - } - } - - if let Some(scroll_strategy) = scroll_strategy - && let Some(logical_entry_index) = self - .visible_navbar_entries() - .into_iter() - .position(|(index, _)| index == navbar_entry_index) - { - self.navbar_scroll_handle - .scroll_to_item(logical_entry_index + 1, scroll_strategy); - } - - // Page scroll handle updates the active item index - // in it's next paint call after using scroll_handle.scroll_to_top_of_item - // The call after that updates the offset of the scroll handle. So to - // ensure the scroll handle doesn't lag behind we need to render three frames - // back to back. - cx.on_next_frame(window, move |_, window, cx| { - if let Some(handle) = handle_to_focus.as_ref() { - window.focus(handle); - } - - cx.on_next_frame(window, |_, _, cx| { - cx.notify(); - }); - cx.notify(); - }); - cx.notify(); - } - - fn scroll_to_content_item( - &self, - content_item_index: usize, - _window: &mut Window, - cx: &mut Context, - ) { - let index = self - .visible_page_items() - .position(|(index, _)| index == content_item_index) - .unwrap_or(0); - if index == 0 { - self.sub_page_scroll_handle - .set_offset(point(px(0.), px(0.))); - self.list_state.scroll_to(gpui::ListOffset { - item_ix: 0, - offset_in_item: px(0.), - }); - return; - } - self.list_state.scroll_to(gpui::ListOffset { - item_ix: index + 1, - offset_in_item: px(0.), - }); - cx.notify(); - } - - fn is_nav_entry_visible(&self, nav_entry_index: usize) -> bool { - self.visible_navbar_entries() - .any(|(index, _)| index == nav_entry_index) - } - - fn focus_and_scroll_to_first_visible_nav_entry( - &self, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(nav_entry_index) = self.visible_navbar_entries().next().map(|(index, _)| index) - { - self.focus_and_scroll_to_nav_entry(nav_entry_index, window, cx); - } - } - - fn focus_and_scroll_to_nav_entry( - &self, - nav_entry_index: usize, - window: &mut Window, - cx: &mut Context, - ) { - let Some(position) = self - .visible_navbar_entries() - .position(|(index, _)| index == nav_entry_index) - else { - return; - }; - self.navbar_scroll_handle - .scroll_to_item(position, gpui::ScrollStrategy::Top); - window.focus(&self.navbar_entries[nav_entry_index].focus_handle); - cx.notify(); - } - - fn visible_page_items(&self) -> impl Iterator { - let page_idx = self.current_page_index(); - - self.current_page() - .items - .iter() - .enumerate() - .filter_map(move |(item_index, item)| { - self.filter_table[page_idx][item_index].then_some((item_index, item)) - }) - } - - fn render_sub_page_breadcrumbs(&self) -> impl IntoElement { - let mut items = vec![]; - items.push(self.current_page().title.into()); - items.extend( - sub_page_stack() - .iter() - .flat_map(|page| [page.section_header.into(), page.link.title.clone()]), - ); - - let last = items.pop().unwrap(); - h_flex() - .gap_1() - .children( - items - .into_iter() - .flat_map(|item| [item, "/".into()]) - .map(|item| Label::new(item).color(Color::Muted)), - ) - .child(Label::new(last)) - } - - fn render_empty_state(&self, search_query: SharedString) -> impl IntoElement { - v_flex() - .size_full() - .items_center() - .justify_center() - .gap_1() - .child(Label::new("No Results")) - .child( - Label::new(search_query) - .size(LabelSize::Small) - .color(Color::Muted), - ) - } - - fn render_page_items( - &mut self, - page_index: usize, - _window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let mut page_content = v_flex().id("settings-ui-page").size_full(); - - let has_active_search = !self.search_bar.read(cx).is_empty(cx); - let has_no_results = self.visible_page_items().next().is_none() && has_active_search; - - if has_no_results { - let search_query = self.search_bar.read(cx).text(cx); - page_content = page_content.child( - self.render_empty_state(format!("No settings match \"{}\"", search_query).into()), - ) - } else { - let last_non_header_index = self - .visible_page_items() - .filter_map(|(index, item)| { - (!matches!(item, SettingsPageItem::SectionHeader(_))).then_some(index) - }) - .last(); - - let root_nav_label = self - .navbar_entries - .iter() - .find(|entry| entry.is_root && entry.page_index == self.current_page_index()) - .map(|entry| entry.title); - - let list_content = list( - self.list_state.clone(), - cx.processor(move |this, index, window, cx| { - if index == 0 { - return div() - .px_8() - .when(sub_page_stack().is_empty(), |this| { - this.when_some(root_nav_label, |this, title| { - this.child( - Label::new(title).size(LabelSize::Large).mt_2().mb_3(), - ) - }) - }) - .into_any_element(); - } - - let mut visible_items = this.visible_page_items(); - let Some((actual_item_index, item)) = visible_items.nth(index - 1) else { - return gpui::Empty.into_any_element(); - }; - - let no_bottom_border = visible_items - .next() - .map(|(_, item)| matches!(item, SettingsPageItem::SectionHeader(_))) - .unwrap_or(false); - - let is_last = Some(actual_item_index) == last_non_header_index; - - let item_focus_handle = - this.content_handles[page_index][actual_item_index].focus_handle(cx); - - v_flex() - .id(("settings-page-item", actual_item_index)) - .track_focus(&item_focus_handle) - .w_full() - .min_w_0() - .child(item.render( - this, - actual_item_index, - no_bottom_border || is_last, - window, - cx, - )) - .into_any_element() - }), - ); - - page_content = page_content.child(list_content.size_full()) - } - page_content - } - - fn render_sub_page_items<'a, Items: Iterator>( - &self, - items: Items, - page_index: Option, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let mut page_content = v_flex() - .id("settings-ui-page") - .size_full() - .overflow_y_scroll() - .track_scroll(&self.sub_page_scroll_handle); - - let items: Vec<_> = items.collect(); - let items_len = items.len(); - let mut section_header = None; - - let has_active_search = !self.search_bar.read(cx).is_empty(cx); - let has_no_results = items_len == 0 && has_active_search; - - if has_no_results { - let search_query = self.search_bar.read(cx).text(cx); - page_content = page_content.child( - self.render_empty_state(format!("No settings match \"{}\"", search_query).into()), - ) - } else { - let last_non_header_index = items - .iter() - .enumerate() - .rev() - .find(|(_, (_, item))| !matches!(item, SettingsPageItem::SectionHeader(_))) - .map(|(index, _)| index); - - let root_nav_label = self - .navbar_entries - .iter() - .find(|entry| entry.is_root && entry.page_index == self.current_page_index()) - .map(|entry| entry.title); - - page_content = page_content - .when(sub_page_stack().is_empty(), |this| { - this.when_some(root_nav_label, |this, title| { - this.child(Label::new(title).size(LabelSize::Large).mt_2().mb_3()) - }) - }) - .children(items.clone().into_iter().enumerate().map( - |(index, (actual_item_index, item))| { - let no_bottom_border = items - .get(index + 1) - .map(|(_, next_item)| { - matches!(next_item, SettingsPageItem::SectionHeader(_)) - }) - .unwrap_or(false); - let is_last = Some(index) == last_non_header_index; - - if let SettingsPageItem::SectionHeader(header) = item { - section_header = Some(*header); - } - v_flex() - .w_full() - .min_w_0() - .id(("settings-page-item", actual_item_index)) - .when_some(page_index, |element, page_index| { - element.track_focus( - &self.content_handles[page_index][actual_item_index] - .focus_handle(cx), - ) - }) - .child(item.render( - self, - actual_item_index, - no_bottom_border || is_last, - window, - cx, - )) - }, - )) - } - page_content - } - - fn render_page( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let page_header; - let page_content; - - if sub_page_stack().is_empty() { - page_header = self.render_files_header(window, cx).into_any_element(); - - page_content = self - .render_page_items(self.current_page_index(), window, cx) - .into_any_element(); - } else { - page_header = h_flex() - .w_full() - .justify_between() - .child( - h_flex() - .ml_neg_1p5() - .gap_1() - .child( - IconButton::new("back-btn", IconName::ArrowLeft) - .icon_size(IconSize::Small) - .shape(IconButtonShape::Square) - .on_click(cx.listener(|this, _, _, cx| { - this.pop_sub_page(cx); - })), - ) - .child(self.render_sub_page_breadcrumbs()), - ) - .child( - Button::new("open-in-settings-file", "Edit in settings.json") - .tab_index(0_isize) - .style(ButtonStyle::OutlinedGhost) - .tooltip(Tooltip::for_action_title_in( - "Edit in settings.json", - &OpenCurrentFile, - &self.focus_handle, - )) - .on_click(cx.listener(|this, _, window, cx| { - this.open_current_settings_file(window, cx); - })), - ) - .into_any_element(); - - let active_page_render_fn = sub_page_stack().last().unwrap().link.render.clone(); - page_content = (active_page_render_fn)(self, window, cx); - } - - let mut warning_banner = gpui::Empty.into_any_element(); - if let Some(error) = - SettingsStore::global(cx).error_for_file(self.current_file.to_settings()) - { - fn banner( - label: &'static str, - error: String, - shown_errors: &mut HashSet, - cx: &mut Context, - ) -> impl IntoElement { - if shown_errors.insert(error.clone()) { - telemetry::event!("Settings Error Shown", label = label, error = &error); - } - Banner::new() - .severity(Severity::Warning) - .child( - v_flex() - .my_0p5() - .gap_0p5() - .child(Label::new(label)) - .child(Label::new(error).size(LabelSize::Small).color(Color::Muted)), - ) - .action_slot( - div().pr_1().pb_1().child( - Button::new("fix-in-json", "Fix in settings.json") - .tab_index(0_isize) - .style(ButtonStyle::Tinted(ui::TintColor::Warning)) - .on_click(cx.listener(|this, _, window, cx| { - this.open_current_settings_file(window, cx); - })), - ), - ) - } - - let parse_error = error.parse_error(); - let parse_failed = parse_error.is_some(); - - warning_banner = v_flex() - .gap_2() - .when_some(parse_error, |this, err| { - this.child(banner( - "Failed to load your settings. Some values may be incorrect and changes may be lost.", - err, - &mut self.shown_errors, - cx, - )) - }) - .map(|this| match &error.migration_status { - settings::MigrationStatus::Succeeded => this.child(banner( - "Your settings are out of date, and need to be updated.", - match &self.current_file { - SettingsUiFile::User => "They can be automatically migrated to the latest version.", - SettingsUiFile::Server(_) | SettingsUiFile::Project(_) => "They must be manually migrated to the latest version." - }.to_string(), - &mut self.shown_errors, - cx, - )), - settings::MigrationStatus::Failed { error: err } if !parse_failed => this - .child(banner( - "Your settings file is out of date, automatic migration failed", - err.clone(), - &mut self.shown_errors, - cx, - )), - _ => this, - }) - .into_any_element() - } - - return v_flex() - .id("settings-ui-page") - .on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| { - if !sub_page_stack().is_empty() { - window.focus_next(); - return; - } - for (logical_index, (actual_index, _)) in this.visible_page_items().enumerate() { - let handle = this.content_handles[this.current_page_index()][actual_index] - .focus_handle(cx); - let mut offset = 1; // for page header - - if let Some((_, next_item)) = this.visible_page_items().nth(logical_index + 1) - && matches!(next_item, SettingsPageItem::SectionHeader(_)) - { - offset += 1; - } - if handle.contains_focused(window, cx) { - let next_logical_index = logical_index + offset + 1; - this.list_state.scroll_to_reveal_item(next_logical_index); - // We need to render the next item to ensure it's focus handle is in the element tree - cx.on_next_frame(window, |_, window, cx| { - cx.notify(); - cx.on_next_frame(window, |_, window, cx| { - window.focus_next(); - cx.notify(); - }); - }); - cx.notify(); - return; - } - } - window.focus_next(); - })) - .on_action(cx.listener(|this, _: &menu::SelectPrevious, window, cx| { - if !sub_page_stack().is_empty() { - window.focus_prev(); - return; - } - let mut prev_was_header = false; - for (logical_index, (actual_index, item)) in this.visible_page_items().enumerate() { - let is_header = matches!(item, SettingsPageItem::SectionHeader(_)); - let handle = this.content_handles[this.current_page_index()][actual_index] - .focus_handle(cx); - let mut offset = 1; // for page header - - if prev_was_header { - offset -= 1; - } - if handle.contains_focused(window, cx) { - let next_logical_index = logical_index + offset - 1; - this.list_state.scroll_to_reveal_item(next_logical_index); - // We need to render the next item to ensure it's focus handle is in the element tree - cx.on_next_frame(window, |_, window, cx| { - cx.notify(); - cx.on_next_frame(window, |_, window, cx| { - window.focus_prev(); - cx.notify(); - }); - }); - cx.notify(); - return; - } - prev_was_header = is_header; - } - window.focus_prev(); - })) - .when(sub_page_stack().is_empty(), |this| { - this.vertical_scrollbar_for(&self.list_state, window, cx) - }) - .when(!sub_page_stack().is_empty(), |this| { - this.vertical_scrollbar_for(&self.sub_page_scroll_handle, window, cx) - }) - .track_focus(&self.content_focus_handle.focus_handle(cx)) - .pt_6() - .gap_4() - .flex_1() - .bg(cx.theme().colors().editor_background) - .child( - v_flex() - .px_8() - .gap_2() - .child(page_header) - .child(warning_banner), - ) - .child( - div() - .flex_1() - .size_full() - .tab_group() - .tab_index(CONTENT_GROUP_TAB_INDEX) - .child(page_content), - ); - } - - /// This function will create a new settings file if one doesn't exist - /// if the current file is a project settings with a valid worktree id - /// We do this because the settings ui allows initializing project settings - fn open_current_settings_file(&mut self, window: &mut Window, cx: &mut Context) { - match &self.current_file { - SettingsUiFile::User => { - let Some(original_window) = self.original_window else { - return; - }; - original_window - .update(cx, |workspace, window, cx| { - workspace - .with_local_workspace(window, cx, |workspace, window, cx| { - let create_task = workspace.project().update(cx, |project, cx| { - project.find_or_create_worktree( - paths::config_dir().as_path(), - false, - cx, - ) - }); - let open_task = workspace.open_paths( - vec![paths::settings_file().to_path_buf()], - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - None, - window, - cx, - ); - - cx.spawn_in(window, async move |workspace, cx| { - create_task.await.ok(); - open_task.await; - - workspace.update_in(cx, |_, window, cx| { - window.activate_window(); - cx.notify(); - }) - }) - .detach(); - }) - .detach(); - }) - .ok(); - - window.remove_window(); - } - SettingsUiFile::Project((worktree_id, path)) => { - let settings_path = path.join(paths::local_settings_file_relative_path()); - let Some(app_state) = workspace::AppState::global(cx).upgrade() else { - return; - }; - - let Some((worktree, corresponding_workspace)) = app_state - .workspace_store - .read(cx) - .workspaces() - .iter() - .find_map(|workspace| { - workspace - .read_with(cx, |workspace, cx| { - workspace - .project() - .read(cx) - .worktree_for_id(*worktree_id, cx) - }) - .ok() - .flatten() - .zip(Some(*workspace)) - }) - else { - log::error!( - "No corresponding workspace contains worktree id: {}", - worktree_id - ); - - return; - }; - - let create_task = if worktree.read(cx).entry_for_path(&settings_path).is_some() { - None - } else { - Some(worktree.update(cx, |tree, cx| { - tree.create_entry( - settings_path.clone(), - false, - Some(initial_project_settings_content().as_bytes().to_vec()), - cx, - ) - })) - }; - - let worktree_id = *worktree_id; - - // TODO: move zed::open_local_file() APIs to this crate, and - // re-implement the "initial_contents" behavior - corresponding_workspace - .update(cx, |_, window, cx| { - cx.spawn_in(window, async move |workspace, cx| { - if let Some(create_task) = create_task { - create_task.await.ok()?; - }; - - workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_path( - (worktree_id, settings_path.clone()), - None, - true, - window, - cx, - ) - }) - .ok()? - .await - .log_err()?; - - workspace - .update_in(cx, |_, window, cx| { - window.activate_window(); - cx.notify(); - }) - .ok(); - - Some(()) - }) - .detach(); - }) - .ok(); - - window.remove_window(); - } - SettingsUiFile::Server(_) => { - // Server files are not editable - return; - } - }; - } - - fn current_page_index(&self) -> usize { - self.page_index_from_navbar_index(self.navbar_entry) - } - - fn current_page(&self) -> &SettingsPage { - &self.pages[self.current_page_index()] - } - - fn page_index_from_navbar_index(&self, index: usize) -> usize { - if self.navbar_entries.is_empty() { - return 0; - } - - self.navbar_entries[index].page_index - } - - fn is_navbar_entry_selected(&self, ix: usize) -> bool { - ix == self.navbar_entry - } - - fn push_sub_page( - &mut self, - sub_page_link: SubPageLink, - section_header: &'static str, - cx: &mut Context, - ) { - sub_page_stack_mut().push(SubPage { - link: sub_page_link, - section_header, - }); - cx.notify(); - } - - fn pop_sub_page(&mut self, cx: &mut Context) { - sub_page_stack_mut().pop(); - cx.notify(); - } - - fn focus_file_at_index(&mut self, index: usize, window: &mut Window) { - if let Some((_, handle)) = self.files.get(index) { - handle.focus(window); - } - } - - fn focused_file_index(&self, window: &Window, cx: &Context) -> usize { - if self.files_focus_handle.contains_focused(window, cx) - && let Some(index) = self - .files - .iter() - .position(|(_, handle)| handle.is_focused(window)) - { - return index; - } - if let Some(current_file_index) = self - .files - .iter() - .position(|(file, _)| file == &self.current_file) - { - return current_file_index; - } - 0 - } - - fn focus_handle_for_content_element( - &self, - actual_item_index: usize, - cx: &Context, - ) -> FocusHandle { - let page_index = self.current_page_index(); - self.content_handles[page_index][actual_item_index].focus_handle(cx) - } - - fn focused_nav_entry(&self, window: &Window, cx: &App) -> Option { - if !self - .navbar_focus_handle - .focus_handle(cx) - .contains_focused(window, cx) - { - return None; - } - for (index, entry) in self.navbar_entries.iter().enumerate() { - if entry.focus_handle.is_focused(window) { - return Some(index); - } - } - None - } - - fn root_entry_containing(&self, nav_entry_index: usize) -> usize { - let mut index = Some(nav_entry_index); - while let Some(prev_index) = index - && !self.navbar_entries[prev_index].is_root - { - index = prev_index.checked_sub(1); - } - return index.expect("No root entry found"); - } -} - -impl Render for SettingsWindow { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let ui_font = theme::setup_ui_font(window, cx); - - client_side_decorations( - v_flex() - .text_color(cx.theme().colors().text) - .size_full() - .children(self.title_bar.clone()) - .child( - div() - .id("settings-window") - .key_context("SettingsWindow") - .track_focus(&self.focus_handle) - .on_action(cx.listener(|this, _: &OpenCurrentFile, window, cx| { - this.open_current_settings_file(window, cx); - })) - .on_action(|_: &Minimize, window, _cx| { - window.minimize_window(); - }) - .on_action(cx.listener(|this, _: &search::FocusSearch, window, cx| { - this.search_bar.focus_handle(cx).focus(window); - })) - .on_action(cx.listener(|this, _: &ToggleFocusNav, window, cx| { - if this - .navbar_focus_handle - .focus_handle(cx) - .contains_focused(window, cx) - { - this.open_and_scroll_to_navbar_entry( - this.navbar_entry, - None, - true, - window, - cx, - ); - } else { - this.focus_and_scroll_to_nav_entry(this.navbar_entry, window, cx); - } - })) - .on_action(cx.listener( - |this, FocusFile(file_index): &FocusFile, window, _| { - this.focus_file_at_index(*file_index as usize, window); - }, - )) - .on_action(cx.listener(|this, _: &FocusNextFile, window, cx| { - let next_index = usize::min( - this.focused_file_index(window, cx) + 1, - this.files.len().saturating_sub(1), - ); - this.focus_file_at_index(next_index, window); - })) - .on_action(cx.listener(|this, _: &FocusPreviousFile, window, cx| { - let prev_index = this.focused_file_index(window, cx).saturating_sub(1); - this.focus_file_at_index(prev_index, window); - })) - .on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| { - if this - .search_bar - .focus_handle(cx) - .contains_focused(window, cx) - { - this.focus_and_scroll_to_first_visible_nav_entry(window, cx); - } else { - window.focus_next(); - } - })) - .on_action(|_: &menu::SelectPrevious, window, _| { - window.focus_prev(); - }) - .flex() - .flex_row() - .flex_1() - .min_h_0() - .font(ui_font) - .bg(cx.theme().colors().background) - .text_color(cx.theme().colors().text) - .when(!cfg!(target_os = "macos"), |this| { - this.border_t_1().border_color(cx.theme().colors().border) - }) - .child(self.render_nav(window, cx)) - .child(self.render_page(window, cx)), - ), - window, - cx, - ) - } -} - -fn all_projects(cx: &App) -> impl Iterator> { - workspace::AppState::global(cx) - .upgrade() - .map(|app_state| { - app_state - .workspace_store - .read(cx) - .workspaces() - .iter() - .filter_map(|workspace| Some(workspace.read(cx).ok()?.project().clone())) - }) - .into_iter() - .flatten() -} - -fn update_settings_file( - file: SettingsUiFile, - file_name: Option<&'static str>, - cx: &mut App, - update: impl 'static + Send + FnOnce(&mut SettingsContent, &App), -) -> Result<()> { - telemetry::event!("Settings Change", setting = file_name, type = file.setting_type()); - - match file { - SettingsUiFile::Project((worktree_id, rel_path)) => { - let rel_path = rel_path.join(paths::local_settings_file_relative_path()); - let Some((worktree, project)) = all_projects(cx).find_map(|project| { - project - .read(cx) - .worktree_for_id(worktree_id, cx) - .zip(Some(project)) - }) else { - anyhow::bail!("Could not find project with worktree id: {}", worktree_id); - }; - - project.update(cx, |project, cx| { - let task = if project.contains_local_settings_file(worktree_id, &rel_path, cx) { - None - } else { - Some(worktree.update(cx, |worktree, cx| { - worktree.create_entry(rel_path.clone(), false, None, cx) - })) - }; - - cx.spawn(async move |project, cx| { - if let Some(task) = task - && task.await.is_err() - { - return; - }; - - project - .update(cx, |project, cx| { - project.update_local_settings_file(worktree_id, rel_path, cx, update); - }) - .ok(); - }) - .detach(); - }); - - return Ok(()); - } - SettingsUiFile::User => { - // todo(settings_ui) error? - SettingsStore::global(cx).update_settings_file(::global(cx), update); - Ok(()) - } - SettingsUiFile::Server(_) => unimplemented!(), - } -} - -fn render_text_field + Into + AsRef + Clone>( - field: SettingField, - file: SettingsUiFile, - metadata: Option<&SettingsFieldMetadata>, - _window: &mut Window, - cx: &mut App, -) -> AnyElement { - let (_, initial_text) = - SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick); - let initial_text = initial_text.filter(|s| !s.as_ref().is_empty()); - - SettingsInputField::new() - .tab_index(0) - .when_some(initial_text, |editor, text| { - editor.with_initial_text(text.as_ref().to_string()) - }) - .when_some( - metadata.and_then(|metadata| metadata.placeholder), - |editor, placeholder| editor.with_placeholder(placeholder), - ) - .on_confirm({ - move |new_text, cx| { - update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| { - (field.write)(settings, new_text.map(Into::into)); - }) - .log_err(); // todo(settings_ui) don't log err - } - }) - .into_any_element() -} - -fn render_toggle_button + From + Copy>( - field: SettingField, - file: SettingsUiFile, - _metadata: Option<&SettingsFieldMetadata>, - _window: &mut Window, - cx: &mut App, -) -> AnyElement { - let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick); - - let toggle_state = if value.copied().map_or(false, Into::into) { - ToggleState::Selected - } else { - ToggleState::Unselected - }; - - Switch::new("toggle_button", toggle_state) - .tab_index(0_isize) - .on_click({ - move |state, _window, cx| { - telemetry::event!("Settings Change", setting = field.json_path, type = file.setting_type()); - - let state = *state == ui::ToggleState::Selected; - update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| { - (field.write)(settings, Some(state.into())); - }) - .log_err(); // todo(settings_ui) don't log err - } - }) - .into_any_element() -} - -fn render_number_field( - field: SettingField, - file: SettingsUiFile, - _metadata: Option<&SettingsFieldMetadata>, - window: &mut Window, - cx: &mut App, -) -> AnyElement { - let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick); - let value = value.copied().unwrap_or_else(T::min_value); - NumberField::new("numeric_stepper", value, window, cx) - .on_change({ - move |value, _window, cx| { - let value = *value; - update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| { - (field.write)(settings, Some(value)); - }) - .log_err(); // todo(settings_ui) don't log err - } - }) - .into_any_element() -} - -fn render_dropdown( - field: SettingField, - file: SettingsUiFile, - metadata: Option<&SettingsFieldMetadata>, - _window: &mut Window, - cx: &mut App, -) -> AnyElement -where - T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static, -{ - let variants = || -> &'static [T] { ::VARIANTS }; - let labels = || -> &'static [&'static str] { ::VARIANTS }; - let should_do_titlecase = metadata - .and_then(|metadata| metadata.should_do_titlecase) - .unwrap_or(true); - - let (_, current_value) = - SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick); - let current_value = current_value.copied().unwrap_or(variants()[0]); - - EnumVariantDropdown::new("dropdown", current_value, variants(), labels(), { - move |value, cx| { - if value == current_value { - return; - } - update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| { - (field.write)(settings, Some(value)); - }) - .log_err(); // todo(settings_ui) don't log err - } - }) - .tab_index(0) - .title_case(should_do_titlecase) - .into_any_element() -} - -fn render_picker_trigger_button(id: SharedString, label: SharedString) -> Button { - Button::new(id, label) - .tab_index(0_isize) - .style(ButtonStyle::Outlined) - .size(ButtonSize::Medium) - .icon(IconName::ChevronUpDown) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .icon_position(IconPosition::End) -} - -fn render_font_picker( - field: SettingField, - file: SettingsUiFile, - _metadata: Option<&SettingsFieldMetadata>, - _window: &mut Window, - cx: &mut App, -) -> AnyElement { - let current_value = SettingsStore::global(cx) - .get_value_from_file(file.to_settings(), field.pick) - .1 - .cloned() - .unwrap_or_else(|| SharedString::default().into()); - - PopoverMenu::new("font-picker") - .trigger(render_picker_trigger_button( - "font_family_picker_trigger".into(), - current_value.clone().into(), - )) - .menu(move |window, cx| { - let file = file.clone(); - let current_value = current_value.clone(); - - Some(cx.new(move |cx| { - font_picker( - current_value.clone().into(), - move |font_name, cx| { - update_settings_file( - file.clone(), - field.json_path, - cx, - move |settings, _cx| { - (field.write)(settings, Some(font_name.into())); - }, - ) - .log_err(); // todo(settings_ui) don't log err - }, - window, - cx, - ) - })) - }) - .anchor(gpui::Corner::TopLeft) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }) - .with_handle(ui::PopoverMenuHandle::default()) - .into_any_element() -} - -fn render_theme_picker( - field: SettingField, - file: SettingsUiFile, - _metadata: Option<&SettingsFieldMetadata>, - _window: &mut Window, - cx: &mut App, -) -> AnyElement { - let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick); - let current_value = value - .cloned() - .map(|theme_name| theme_name.0.into()) - .unwrap_or_else(|| cx.theme().name.clone()); - - PopoverMenu::new("theme-picker") - .trigger(render_picker_trigger_button( - "theme_picker_trigger".into(), - current_value.clone(), - )) - .menu(move |window, cx| { - Some(cx.new(|cx| { - let file = file.clone(); - let current_value = current_value.clone(); - theme_picker( - current_value, - move |theme_name, cx| { - update_settings_file( - file.clone(), - field.json_path, - cx, - move |settings, _cx| { - (field.write)( - settings, - Some(settings::ThemeName(theme_name.into())), - ); - }, - ) - .log_err(); // todo(settings_ui) don't log err - }, - window, - cx, - ) - })) - }) - .anchor(gpui::Corner::TopLeft) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }) - .with_handle(ui::PopoverMenuHandle::default()) - .into_any_element() -} - -fn render_icon_theme_picker( - field: SettingField, - file: SettingsUiFile, - _metadata: Option<&SettingsFieldMetadata>, - _window: &mut Window, - cx: &mut App, -) -> AnyElement { - let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick); - let current_value = value - .cloned() - .map(|theme_name| theme_name.0.into()) - .unwrap_or_else(|| cx.theme().name.clone()); - - PopoverMenu::new("icon-theme-picker") - .trigger(render_picker_trigger_button( - "icon_theme_picker_trigger".into(), - current_value.clone(), - )) - .menu(move |window, cx| { - Some(cx.new(|cx| { - let file = file.clone(); - let current_value = current_value.clone(); - icon_theme_picker( - current_value, - move |theme_name, cx| { - update_settings_file( - file.clone(), - field.json_path, - cx, - move |settings, _cx| { - (field.write)( - settings, - Some(settings::IconThemeName(theme_name.into())), - ); - }, - ) - .log_err(); // todo(settings_ui) don't log err - }, - window, - cx, - ) - })) - }) - .anchor(gpui::Corner::TopLeft) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }) - .with_handle(ui::PopoverMenuHandle::default()) - .into_any_element() -} - -#[cfg(test)] -pub mod test { - - use super::*; - - impl SettingsWindow { - fn navbar_entry(&self) -> usize { - self.navbar_entry - } - } - - impl PartialEq for NavBarEntry { - fn eq(&self, other: &Self) -> bool { - self.title == other.title - && self.is_root == other.is_root - && self.expanded == other.expanded - && self.page_index == other.page_index - && self.item_index == other.item_index - // ignoring focus_handle - } - } - - pub fn register_settings(cx: &mut App) { - settings::init(cx); - theme::init(theme::LoadThemes::JustBase, cx); - editor::init(cx); - menu::init(); - } - - fn parse(input: &'static str, window: &mut Window, cx: &mut App) -> SettingsWindow { - let mut pages: Vec = Vec::new(); - let mut expanded_pages = Vec::new(); - let mut selected_idx = None; - let mut index = 0; - let mut in_expanded_section = false; - - for mut line in input - .lines() - .map(|line| line.trim()) - .filter(|line| !line.is_empty()) - { - if let Some(pre) = line.strip_suffix('*') { - assert!(selected_idx.is_none(), "Only one selected entry allowed"); - selected_idx = Some(index); - line = pre; - } - let (kind, title) = line.split_once(" ").unwrap(); - assert_eq!(kind.len(), 1); - let kind = kind.chars().next().unwrap(); - if kind == 'v' { - let page_idx = pages.len(); - expanded_pages.push(page_idx); - pages.push(SettingsPage { - title, - items: vec![], - }); - index += 1; - in_expanded_section = true; - } else if kind == '>' { - pages.push(SettingsPage { - title, - items: vec![], - }); - index += 1; - in_expanded_section = false; - } else if kind == '-' { - pages - .last_mut() - .unwrap() - .items - .push(SettingsPageItem::SectionHeader(title)); - if selected_idx == Some(index) && !in_expanded_section { - panic!("Items in unexpanded sections cannot be selected"); - } - index += 1; - } else { - panic!( - "Entries must start with one of 'v', '>', or '-'\n line: {}", - line - ); - } - } - - let mut settings_window = SettingsWindow { - title_bar: None, - original_window: None, - worktree_root_dirs: HashMap::default(), - files: Vec::default(), - current_file: crate::SettingsUiFile::User, - pages, - search_bar: cx.new(|cx| Editor::single_line(window, cx)), - navbar_entry: selected_idx.expect("Must have a selected navbar entry"), - navbar_entries: Vec::default(), - navbar_scroll_handle: UniformListScrollHandle::default(), - navbar_focus_subscriptions: vec![], - filter_table: vec![], - has_query: false, - content_handles: vec![], - search_task: None, - sub_page_scroll_handle: ScrollHandle::new(), - focus_handle: cx.focus_handle(), - navbar_focus_handle: NonFocusableHandle::new( - NAVBAR_CONTAINER_TAB_INDEX, - false, - window, - cx, - ), - content_focus_handle: NonFocusableHandle::new( - CONTENT_CONTAINER_TAB_INDEX, - false, - window, - cx, - ), - files_focus_handle: cx.focus_handle(), - search_index: None, - list_state: ListState::new(0, gpui::ListAlignment::Top, px(0.0)), - shown_errors: HashSet::default(), - }; - - settings_window.build_filter_table(); - settings_window.build_navbar(cx); - for expanded_page_index in expanded_pages { - for entry in &mut settings_window.navbar_entries { - if entry.page_index == expanded_page_index && entry.is_root { - entry.expanded = true; - } - } - } - settings_window - } - - #[track_caller] - fn check_navbar_toggle( - before: &'static str, - toggle_page: &'static str, - after: &'static str, - window: &mut Window, - cx: &mut App, - ) { - let mut settings_window = parse(before, window, cx); - let toggle_page_idx = settings_window - .pages - .iter() - .position(|page| page.title == toggle_page) - .expect("page not found"); - let toggle_idx = settings_window - .navbar_entries - .iter() - .position(|entry| entry.page_index == toggle_page_idx) - .expect("page not found"); - settings_window.toggle_navbar_entry(toggle_idx); - - let expected_settings_window = parse(after, window, cx); - - pretty_assertions::assert_eq!( - settings_window - .visible_navbar_entries() - .map(|(_, entry)| entry) - .collect::>(), - expected_settings_window - .visible_navbar_entries() - .map(|(_, entry)| entry) - .collect::>(), - ); - pretty_assertions::assert_eq!( - settings_window.navbar_entries[settings_window.navbar_entry()], - expected_settings_window.navbar_entries[expected_settings_window.navbar_entry()], - ); - } - - macro_rules! check_navbar_toggle { - ($name:ident, before: $before:expr, toggle_page: $toggle_page:expr, after: $after:expr) => { - #[gpui::test] - fn $name(cx: &mut gpui::TestAppContext) { - let window = cx.add_empty_window(); - window.update(|window, cx| { - register_settings(cx); - check_navbar_toggle($before, $toggle_page, $after, window, cx); - }); - } - }; - } - - check_navbar_toggle!( - navbar_basic_open, - before: r" - v General - - General - - Privacy* - v Project - - Project Settings - ", - toggle_page: "General", - after: r" - > General* - v Project - - Project Settings - " - ); - - check_navbar_toggle!( - navbar_basic_close, - before: r" - > General* - - General - - Privacy - v Project - - Project Settings - ", - toggle_page: "General", - after: r" - v General* - - General - - Privacy - v Project - - Project Settings - " - ); - - check_navbar_toggle!( - navbar_basic_second_root_entry_close, - before: r" - > General - - General - - Privacy - v Project - - Project Settings* - ", - toggle_page: "Project", - after: r" - > General - > Project* - " - ); - - check_navbar_toggle!( - navbar_toggle_subroot, - before: r" - v General Page - - General - - Privacy - v Project - - Worktree Settings Content* - v AI - - General - > Appearance & Behavior - ", - toggle_page: "Project", - after: r" - v General Page - - General - - Privacy - > Project* - v AI - - General - > Appearance & Behavior - " - ); - - check_navbar_toggle!( - navbar_toggle_close_propagates_selected_index, - before: r" - v General Page - - General - - Privacy - v Project - - Worktree Settings Content - v AI - - General* - > Appearance & Behavior - ", - toggle_page: "General Page", - after: r" - > General Page* - v Project - - Worktree Settings Content - v AI - - General - > Appearance & Behavior - " - ); - - check_navbar_toggle!( - navbar_toggle_expand_propagates_selected_index, - before: r" - > General Page - - General - - Privacy - v Project - - Worktree Settings Content - v AI - - General* - > Appearance & Behavior - ", - toggle_page: "General Page", - after: r" - v General Page* - - General - - Privacy - v Project - - Worktree Settings Content - v AI - - General - > Appearance & Behavior - " - ); -} diff --git a/crates/snippet/Cargo.toml b/crates/snippet/Cargo.toml deleted file mode 100644 index 2dde5c2d00..0000000000 --- a/crates/snippet/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "snippet" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/snippet.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -smallvec.workspace = true diff --git a/crates/snippet/LICENSE-GPL b/crates/snippet/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/snippet/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/snippet/src/snippet.rs b/crates/snippet/src/snippet.rs deleted file mode 100644 index 4be4281d9a..0000000000 --- a/crates/snippet/src/snippet.rs +++ /dev/null @@ -1,334 +0,0 @@ -use anyhow::{Context as _, Result}; -use smallvec::SmallVec; -use std::{collections::BTreeMap, ops::Range}; - -#[derive(Clone, Debug, Default, PartialEq)] -pub struct Snippet { - pub text: String, - pub tabstops: Vec, -} - -#[derive(Clone, Debug, Default, PartialEq)] -pub struct TabStop { - pub ranges: SmallVec<[Range; 2]>, - pub choices: Option>, -} - -impl Snippet { - pub fn parse(source: &str) -> Result { - let mut text = String::with_capacity(source.len()); - let mut tabstops = BTreeMap::new(); - parse_snippet(source, false, &mut text, &mut tabstops) - .context("failed to parse snippet")?; - - let len = text.len() as isize; - let final_tabstop = tabstops.remove(&0); - let mut tabstops = tabstops.into_values().collect::>(); - - if let Some(final_tabstop) = final_tabstop { - tabstops.push(final_tabstop); - } else { - let end_tabstop = TabStop { - ranges: [len..len].into_iter().collect(), - choices: None, - }; - - if !tabstops.last().is_some_and(|t| *t == end_tabstop) { - tabstops.push(end_tabstop); - } - } - - Ok(Snippet { text, tabstops }) - } -} - -fn parse_snippet<'a>( - mut source: &'a str, - nested: bool, - text: &mut String, - tabstops: &mut BTreeMap, -) -> Result<&'a str> { - loop { - match source.chars().next() { - None => return Ok(""), - Some('$') => { - source = parse_tabstop(&source[1..], text, tabstops)?; - } - Some('\\') => { - // As specified in the LSP spec (`Grammar` section), - // backslashes can escape some characters: - // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#snippet_syntax - source = &source[1..]; - if let Some(c) = source.chars().next() { - if c == '$' || c == '\\' || c == '}' { - text.push(c); - // All escapable characters are 1 byte long: - source = &source[1..]; - } else { - text.push('\\'); - } - } else { - text.push('\\'); - } - } - Some('}') => { - if nested { - return Ok(source); - } else { - text.push('}'); - source = &source[1..]; - } - } - Some(_) => { - let chunk_end = source.find(['}', '$', '\\']).unwrap_or(source.len()); - let (chunk, rest) = source.split_at(chunk_end); - text.push_str(chunk); - source = rest; - } - } - } -} - -fn parse_tabstop<'a>( - mut source: &'a str, - text: &mut String, - tabstops: &mut BTreeMap, -) -> Result<&'a str> { - let tabstop_start = text.len(); - let tabstop_index; - let mut choices = None; - - if source.starts_with('{') { - let (index, rest) = parse_int(&source[1..])?; - tabstop_index = index; - source = rest; - - if source.starts_with("|") { - (source, choices) = parse_choices(&source[1..], text)?; - } - - if source.starts_with(':') { - source = parse_snippet(&source[1..], true, text, tabstops)?; - } - - if source.starts_with('}') { - source = &source[1..]; - } else { - anyhow::bail!("expected a closing brace"); - } - } else { - let (index, rest) = parse_int(source)?; - tabstop_index = index; - source = rest; - } - - tabstops - .entry(tabstop_index) - .or_insert_with(|| TabStop { - ranges: Default::default(), - choices, - }) - .ranges - .push(tabstop_start as isize..text.len() as isize); - Ok(source) -} - -fn parse_int(source: &str) -> Result<(usize, &str)> { - let len = source - .find(|c: char| !c.is_ascii_digit()) - .unwrap_or(source.len()); - anyhow::ensure!(len > 0, "expected an integer"); - let (prefix, suffix) = source.split_at(len); - Ok((prefix.parse()?, suffix)) -} - -fn parse_choices<'a>( - mut source: &'a str, - text: &mut String, -) -> Result<(&'a str, Option>)> { - let mut found_default_choice = false; - let mut current_choice = String::new(); - let mut choices = Vec::new(); - - loop { - match source.chars().next() { - None => return Ok(("", Some(choices))), - Some('\\') => { - source = &source[1..]; - - if let Some(c) = source.chars().next() { - if !found_default_choice { - current_choice.push(c); - text.push(c); - } - source = &source[c.len_utf8()..]; - } - } - Some(',') => { - found_default_choice = true; - source = &source[1..]; - choices.push(current_choice); - current_choice = String::new(); - } - Some('|') => { - source = &source[1..]; - choices.push(current_choice); - return Ok((source, Some(choices))); - } - Some(_) => { - let chunk_end = source.find([',', '|', '\\']); - - anyhow::ensure!( - chunk_end.is_some(), - "Placeholder choice doesn't contain closing pipe-character '|'" - ); - - let (chunk, rest) = source.split_at(chunk_end.unwrap()); - - if !found_default_choice { - text.push_str(chunk); - } - - current_choice.push_str(chunk); - source = rest; - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_snippet_without_tabstops() { - let snippet = Snippet::parse("one-two-three").unwrap(); - assert_eq!(snippet.text, "one-two-three"); - assert_eq!(tabstops(&snippet), &[vec![13..13]]); - } - - #[test] - fn test_snippet_with_tabstops() { - let snippet = Snippet::parse("one$1two").unwrap(); - assert_eq!(snippet.text, "onetwo"); - assert_eq!(tabstops(&snippet), &[vec![3..3], vec![6..6]]); - assert_eq!(tabstop_choices(&snippet), &[&None, &None]); - - // Multi-digit numbers - let snippet = Snippet::parse("one$123-$99-two").unwrap(); - assert_eq!(snippet.text, "one--two"); - assert_eq!(tabstops(&snippet), &[vec![4..4], vec![3..3], vec![8..8]]); - assert_eq!(tabstop_choices(&snippet), &[&None, &None, &None]); - } - - #[test] - fn test_snippet_with_last_tabstop_at_end() { - let snippet = Snippet::parse(r#"foo.$1"#).unwrap(); - - // If the final tabstop is already at the end of the text, don't insert - // an additional tabstop at the end. - assert_eq!(snippet.text, r#"foo."#); - assert_eq!(tabstops(&snippet), &[vec![4..4]]); - assert_eq!(tabstop_choices(&snippet), &[&None]); - } - - #[test] - fn test_snippet_with_explicit_final_tabstop() { - let snippet = Snippet::parse(r#"
$0
"#).unwrap(); - - // If the final tabstop is explicitly specified via '$0', then - // don't insert an additional tabstop at the end. - assert_eq!(snippet.text, r#"
"#); - assert_eq!(tabstops(&snippet), &[vec![12..12], vec![14..14]]); - assert_eq!(tabstop_choices(&snippet), &[&None, &None]); - } - - #[test] - fn test_snippet_with_placeholders() { - let snippet = Snippet::parse("one${1:two}three${2:four}").unwrap(); - assert_eq!(snippet.text, "onetwothreefour"); - assert_eq!( - tabstops(&snippet), - &[vec![3..6], vec![11..15], vec![15..15]] - ); - assert_eq!(tabstop_choices(&snippet), &[&None, &None, &None]); - } - - #[test] - fn test_snippet_with_choice_placeholders() { - let snippet = Snippet::parse("type ${1|i32, u32|} = $2") - .expect("Should be able to unpack choice placeholders"); - - assert_eq!(snippet.text, "type i32 = "); - assert_eq!(tabstops(&snippet), &[vec![5..8], vec![11..11],]); - assert_eq!( - tabstop_choices(&snippet), - &[&Some(vec!["i32".to_string(), " u32".to_string()]), &None] - ); - - let snippet = Snippet::parse(r"${1|\$\{1\|one\,two\,tree\|\}|}") - .expect("Should be able to parse choice with escape characters"); - - assert_eq!(snippet.text, "${1|one,two,tree|}"); - assert_eq!(tabstops(&snippet), &[vec![0..18], vec![18..18]]); - assert_eq!( - tabstop_choices(&snippet), - &[&Some(vec!["${1|one,two,tree|}".to_string(),]), &None] - ); - } - - #[test] - fn test_snippet_with_nested_placeholders() { - let snippet = Snippet::parse( - "for (${1:var ${2:i} = 0; ${2:i} < ${3:${4:array}.length}; ${2:i}++}) {$0}", - ) - .unwrap(); - assert_eq!(snippet.text, "for (var i = 0; i < array.length; i++) {}"); - assert_eq!( - tabstops(&snippet), - &[ - vec![5..37], - vec![9..10, 16..17, 34..35], - vec![20..32], - vec![20..25], - vec![40..40], - ] - ); - assert_eq!( - tabstop_choices(&snippet), - &[&None, &None, &None, &None, &None] - ); - } - - #[test] - fn test_snippet_parsing_with_escaped_chars() { - let snippet = Snippet::parse("\"\\$schema\": $1").unwrap(); - assert_eq!(snippet.text, "\"$schema\": "); - assert_eq!(tabstops(&snippet), &[vec![11..11]]); - assert_eq!(tabstop_choices(&snippet), &[&None]); - - let snippet = Snippet::parse("{a\\}").unwrap(); - assert_eq!(snippet.text, "{a}"); - assert_eq!(tabstops(&snippet), &[vec![3..3]]); - assert_eq!(tabstop_choices(&snippet), &[&None]); - - // backslash not functioning as an escape - let snippet = Snippet::parse("a\\b").unwrap(); - assert_eq!(snippet.text, "a\\b"); - assert_eq!(tabstops(&snippet), &[vec![3..3]]); - - // first backslash cancelling escaping that would - // have happened with second backslash - let snippet = Snippet::parse("one\\\\$1two").unwrap(); - assert_eq!(snippet.text, "one\\two"); - assert_eq!(tabstops(&snippet), &[vec![4..4], vec![7..7]]); - } - - fn tabstops(snippet: &Snippet) -> Vec>> { - snippet.tabstops.iter().map(|t| t.ranges.to_vec()).collect() - } - - fn tabstop_choices(snippet: &Snippet) -> Vec<&Option>> { - snippet.tabstops.iter().map(|t| &t.choices).collect() - } -} diff --git a/crates/snippet_provider/Cargo.toml b/crates/snippet_provider/Cargo.toml deleted file mode 100644 index c1f04117d4..0000000000 --- a/crates/snippet_provider/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "snippet_provider" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[features] -test-support = [] - -[dependencies] -anyhow.workspace = true -collections.workspace = true -extension.workspace = true -fs.workspace = true -futures.workspace = true -gpui.workspace = true -parking_lot.workspace = true -paths.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_json_lenient.workspace = true -snippet.workspace = true -util.workspace = true -schemars.workspace = true - -[dev-dependencies] -fs = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -indoc.workspace = true diff --git a/crates/snippet_provider/LICENSE-GPL b/crates/snippet_provider/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/snippet_provider/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/snippet_provider/src/extension_snippet.rs b/crates/snippet_provider/src/extension_snippet.rs deleted file mode 100644 index cd5fb083f9..0000000000 --- a/crates/snippet_provider/src/extension_snippet.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::path::PathBuf; -use std::sync::Arc; - -use anyhow::Result; -use extension::{ExtensionHostProxy, ExtensionSnippetProxy}; -use gpui::App; - -use crate::SnippetRegistry; - -pub fn init(cx: &mut App) { - let proxy = ExtensionHostProxy::default_global(cx); - proxy.register_snippet_proxy(SnippetRegistryProxy { - snippet_registry: SnippetRegistry::global(cx), - }); -} - -struct SnippetRegistryProxy { - snippet_registry: Arc, -} - -impl ExtensionSnippetProxy for SnippetRegistryProxy { - fn register_snippet(&self, path: &PathBuf, snippet_contents: &str) -> Result<()> { - self.snippet_registry - .register_snippets(path, snippet_contents) - } -} diff --git a/crates/snippet_provider/src/format.rs b/crates/snippet_provider/src/format.rs deleted file mode 100644 index f9abb987d9..0000000000 --- a/crates/snippet_provider/src/format.rs +++ /dev/null @@ -1,78 +0,0 @@ -use collections::HashMap; -use schemars::{JsonSchema, json_schema}; -use serde::Deserialize; -use std::borrow::Cow; -use util::schemars::{AllowTrailingCommas, DefaultDenyUnknownFields}; - -#[derive(Deserialize)] -pub struct VsSnippetsFile { - #[serde(flatten)] - pub(crate) snippets: HashMap, -} - -impl VsSnippetsFile { - pub fn generate_json_schema() -> serde_json::Value { - let schema = schemars::generate::SchemaSettings::draft2019_09() - .with_transform(DefaultDenyUnknownFields) - .with_transform(AllowTrailingCommas) - .into_generator() - .root_schema_for::(); - - serde_json::to_value(schema).unwrap() - } -} - -impl JsonSchema for VsSnippetsFile { - fn schema_name() -> Cow<'static, str> { - "VsSnippetsFile".into() - } - - fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - let snippet_schema = generator.subschema_for::(); - json_schema!({ - "type": "object", - "additionalProperties": snippet_schema - }) - } -} - -#[derive(Deserialize, JsonSchema)] -#[serde(untagged)] -pub(crate) enum ListOrDirect { - Single(String), - List(Vec), -} - -impl From for Vec { - fn from(list: ListOrDirect) -> Self { - match list { - ListOrDirect::Single(entry) => vec![entry], - ListOrDirect::List(entries) => entries, - } - } -} - -impl std::fmt::Display for ListOrDirect { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Single(v) => v.to_owned(), - Self::List(v) => v.join("\n"), - } - ) - } -} - -#[derive(Deserialize, JsonSchema)] -pub(crate) struct VsCodeSnippet { - /// The snippet prefix used to decide whether a completion menu should be shown. - pub(crate) prefix: Option, - - /// The snippet content. Use `$1` and `${1:defaultText}` to define cursor positions and `$0` for final cursor position. - pub(crate) body: ListOrDirect, - - /// The snippet description displayed inside the completion menu. - pub(crate) description: Option, -} diff --git a/crates/snippet_provider/src/lib.rs b/crates/snippet_provider/src/lib.rs deleted file mode 100644 index 5eff6c917f..0000000000 --- a/crates/snippet_provider/src/lib.rs +++ /dev/null @@ -1,295 +0,0 @@ -mod extension_snippet; -pub mod format; -mod registry; - -use std::{ - path::{Path, PathBuf}, - sync::Arc, - time::Duration, -}; - -use anyhow::Result; -use collections::{BTreeMap, BTreeSet, HashMap}; -use format::VsSnippetsFile; -use fs::Fs; -use futures::stream::StreamExt; -use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task, WeakEntity}; -pub use registry::*; -use util::ResultExt; - -pub fn init(cx: &mut App) { - SnippetRegistry::init_global(cx); - extension_snippet::init(cx); -} - -/// Language name, or `None` if the snippet file is global. -type SnippetKind = Option; -fn file_stem_to_key(stem: &str) -> SnippetKind { - if stem == "snippets" { - None - } else { - Some(stem.to_owned()) - } -} - -fn file_to_snippets(file_contents: VsSnippetsFile) -> Vec> { - let mut snippets = vec![]; - for (name, snippet) in file_contents.snippets { - let snippet_name = name.clone(); - let prefixes = snippet - .prefix - .map_or_else(move || vec![snippet_name], |prefixes| prefixes.into()); - let description = snippet - .description - .map(|description| description.to_string()); - let body = snippet.body.to_string(); - if snippet::Snippet::parse(&body).log_err().is_none() { - continue; - }; - snippets.push(Arc::new(Snippet { - body, - prefix: prefixes, - description, - name, - })); - } - snippets -} -// Snippet with all of the metadata -#[derive(Debug)] -pub struct Snippet { - pub prefix: Vec, - pub body: String, - pub description: Option, - pub name: String, -} - -async fn process_updates( - this: WeakEntity, - entries: Vec, - mut cx: AsyncApp, -) -> Result<()> { - let fs = this.read_with(&cx, |this, _| this.fs.clone())?; - for entry_path in entries { - if entry_path - .extension() - .is_none_or(|extension| extension != "json") - { - continue; - } - let entry_metadata = fs.metadata(&entry_path).await; - // Entry could have been removed, in which case we should no longer show completions for it. - let entry_exists = entry_metadata.is_ok(); - if entry_metadata.is_ok_and(|entry| entry.is_some_and(|e| e.is_dir)) { - // Don't process dirs. - continue; - } - let Some(stem) = entry_path.file_stem().and_then(|s| s.to_str()) else { - continue; - }; - let key = file_stem_to_key(stem); - - let contents = if entry_exists { - fs.load(&entry_path).await.ok() - } else { - None - }; - - this.update(&mut cx, move |this, _| { - let snippets_of_kind = this.snippets.entry(key).or_default(); - if entry_exists { - let Some(file_contents) = contents else { - return; - }; - let Ok(as_json) = serde_json_lenient::from_str::(&file_contents) - else { - return; - }; - let snippets = file_to_snippets(as_json); - *snippets_of_kind.entry(entry_path).or_default() = snippets; - } else { - snippets_of_kind.remove(&entry_path); - } - })?; - } - Ok(()) -} - -async fn initial_scan( - this: WeakEntity, - path: Arc, - cx: AsyncApp, -) -> Result<()> { - let fs = this.read_with(&cx, |this, _| this.fs.clone())?; - let entries = fs.read_dir(&path).await; - if let Ok(entries) = entries { - let entries = entries - .collect::>() - .await - .into_iter() - .collect::>>()?; - process_updates(this, entries, cx).await?; - } - Ok(()) -} - -pub struct SnippetProvider { - fs: Arc, - snippets: HashMap>>>, - watch_tasks: Vec>>, -} - -// Watches global snippet directory, is created just once and reused across multiple projects -struct GlobalSnippetWatcher(Entity); - -impl GlobalSnippetWatcher { - fn new(fs: Arc, cx: &mut App) -> Self { - let global_snippets_dir = paths::snippets_dir(); - let provider = cx.new(|_cx| SnippetProvider { - fs, - snippets: Default::default(), - watch_tasks: vec![], - }); - provider.update(cx, |this, cx| this.watch_directory(global_snippets_dir, cx)); - Self(provider) - } -} - -impl gpui::Global for GlobalSnippetWatcher {} - -impl SnippetProvider { - pub fn new(fs: Arc, dirs_to_watch: BTreeSet, cx: &mut App) -> Entity { - cx.new(move |cx| { - if !cx.has_global::() { - let global_watcher = GlobalSnippetWatcher::new(fs.clone(), cx); - cx.set_global(global_watcher); - } - let mut this = Self { - fs, - watch_tasks: Vec::new(), - snippets: Default::default(), - }; - - for dir in dirs_to_watch { - this.watch_directory(&dir, cx); - } - - this - }) - } - - /// Add directory to be watched for content changes - fn watch_directory(&mut self, path: &Path, cx: &Context) { - let path: Arc = Arc::from(path); - - self.watch_tasks.push(cx.spawn(async move |this, cx| { - let fs = this.read_with(cx, |this, _| this.fs.clone())?; - let watched_path = path.clone(); - let watcher = fs.watch(&watched_path, Duration::from_secs(1)); - initial_scan(this.clone(), path, cx.clone()).await?; - - let (mut entries, _) = watcher.await; - while let Some(entries) = entries.next().await { - process_updates( - this.clone(), - entries.into_iter().map(|event| event.path).collect(), - cx.clone(), - ) - .await?; - } - Ok(()) - })); - } - - fn lookup_snippets<'a, const LOOKUP_GLOBALS: bool>( - &'a self, - language: &'a SnippetKind, - cx: &App, - ) -> Vec> { - let mut user_snippets: Vec<_> = self - .snippets - .get(language) - .cloned() - .unwrap_or_default() - .into_iter() - .flat_map(|(_, snippets)| snippets.into_iter()) - .collect(); - if LOOKUP_GLOBALS { - if let Some(global_watcher) = cx.try_global::() { - user_snippets.extend( - global_watcher - .0 - .read(cx) - .lookup_snippets::(language, cx), - ); - } - - let Some(registry) = SnippetRegistry::try_global(cx) else { - return user_snippets; - }; - - let registry_snippets = registry.get_snippets(language); - user_snippets.extend(registry_snippets); - } - - user_snippets - } - - #[cfg(any(test, feature = "test-support"))] - pub fn add_snippet_for_test( - &mut self, - language: SnippetKind, - path: PathBuf, - snippet: Vec>, - ) { - self.snippets - .entry(language) - .or_default() - .insert(path, snippet); - } - - pub fn snippets_for(&self, language: SnippetKind, cx: &App) -> Vec> { - let mut requested_snippets = self.lookup_snippets::(&language, cx); - - if language.is_some() { - // Look up global snippets as well. - requested_snippets.extend(self.lookup_snippets::(&None, cx)); - } - requested_snippets - } -} - -#[cfg(test)] -mod tests { - use super::*; - use fs::FakeFs; - use gpui; - use gpui::TestAppContext; - use indoc::indoc; - - #[gpui::test] - fn test_lookup_snippets_dup_registry_snippets(cx: &mut TestAppContext) { - let fs = FakeFs::new(cx.background_executor.clone()); - cx.update(|cx| { - SnippetRegistry::init_global(cx); - SnippetRegistry::global(cx) - .register_snippets( - "ruby".as_ref(), - indoc! {r#" - { - "Log to console": { - "prefix": "log", - "body": ["console.info(\"Hello, ${1:World}!\")", "$0"], - "description": "Logs to console" - } - } - "#}, - ) - .unwrap(); - let provider = SnippetProvider::new(fs.clone(), Default::default(), cx); - cx.update_entity(&provider, |provider, cx| { - assert_eq!(1, provider.snippets_for(Some("ruby".to_owned()), cx).len()); - }); - }); - } -} diff --git a/crates/snippet_provider/src/registry.rs b/crates/snippet_provider/src/registry.rs deleted file mode 100644 index 65850d650e..0000000000 --- a/crates/snippet_provider/src/registry.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::{path::Path, sync::Arc}; - -use anyhow::Result; -use collections::HashMap; -use gpui::{App, Global, ReadGlobal, UpdateGlobal}; -use parking_lot::RwLock; - -use crate::{Snippet, SnippetKind, file_stem_to_key}; - -struct GlobalSnippetRegistry(Arc); - -impl Global for GlobalSnippetRegistry {} - -#[derive(Default)] -pub struct SnippetRegistry { - snippets: RwLock>>>, -} - -impl SnippetRegistry { - pub fn global(cx: &App) -> Arc { - GlobalSnippetRegistry::global(cx).0.clone() - } - - pub fn try_global(cx: &App) -> Option> { - cx.try_global::() - .map(|registry| registry.0.clone()) - } - - pub fn init_global(cx: &mut App) { - GlobalSnippetRegistry::set_global(cx, GlobalSnippetRegistry(Arc::new(Self::new()))) - } - - pub fn new() -> Self { - Self { - snippets: RwLock::new(HashMap::default()), - } - } - - pub fn register_snippets(&self, file_path: &Path, contents: &str) -> Result<()> { - let snippets_in_file: crate::format::VsSnippetsFile = - serde_json_lenient::from_str(contents)?; - let kind = file_path - .file_stem() - .and_then(|stem| stem.to_str().and_then(file_stem_to_key)); - let snippets = crate::file_to_snippets(snippets_in_file); - self.snippets.write().insert(kind, snippets); - - Ok(()) - } - - pub fn get_snippets(&self, kind: &SnippetKind) -> Vec> { - self.snippets.read().get(kind).cloned().unwrap_or_default() - } -} diff --git a/crates/snippets_ui/Cargo.toml b/crates/snippets_ui/Cargo.toml deleted file mode 100644 index 3139a41dad..0000000000 --- a/crates/snippets_ui/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "snippets_ui" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/snippets_ui.rs" - -[dependencies] -file_finder.workspace = true -file_icons.workspace = true -fuzzy.workspace = true -gpui.workspace = true -language.workspace = true -paths.workspace = true -picker.workspace = true -settings.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true diff --git a/crates/snippets_ui/LICENSE-GPL b/crates/snippets_ui/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/snippets_ui/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/snippets_ui/src/snippets_ui.rs b/crates/snippets_ui/src/snippets_ui.rs deleted file mode 100644 index cfe41144ba..0000000000 --- a/crates/snippets_ui/src/snippets_ui.rs +++ /dev/null @@ -1,364 +0,0 @@ -use file_finder::file_finder_settings::FileFinderSettings; -use file_icons::FileIcons; -use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; -use gpui::{ - App, Context, DismissEvent, Entity, EventEmitter, Focusable, ParentElement, Render, Styled, - WeakEntity, Window, actions, -}; -use language::{LanguageMatcher, LanguageName, LanguageRegistry}; -use paths::snippets_dir; -use picker::{Picker, PickerDelegate}; -use settings::Settings; -use std::{ - borrow::{Borrow, Cow}, - collections::HashSet, - fs, - path::Path, - sync::Arc, -}; -use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*}; -use util::ResultExt; -use workspace::{ModalView, OpenOptions, OpenVisible, Workspace, notifications::NotifyResultExt}; - -#[derive(Eq, Hash, PartialEq)] -struct ScopeName(Cow<'static, str>); - -struct ScopeFileName(Cow<'static, str>); - -impl ScopeFileName { - fn with_extension(self) -> String { - format!("{}.json", self.0) - } -} - -const GLOBAL_SCOPE_NAME: &str = "global"; -const GLOBAL_SCOPE_FILE_NAME: &str = "snippets"; - -impl From for ScopeFileName { - fn from(value: ScopeName) -> Self { - if value.0 == GLOBAL_SCOPE_NAME { - ScopeFileName(Cow::Borrowed(GLOBAL_SCOPE_FILE_NAME)) - } else { - ScopeFileName(value.0) - } - } -} - -impl From for ScopeName { - fn from(value: ScopeFileName) -> Self { - if value.0 == GLOBAL_SCOPE_FILE_NAME { - ScopeName(Cow::Borrowed(GLOBAL_SCOPE_NAME)) - } else { - ScopeName(value.0) - } - } -} - -actions!( - snippets, - [ - /// Opens the snippets configuration file. - ConfigureSnippets, - /// Opens the snippets folder in the file manager. - OpenFolder - ] -); - -pub fn init(cx: &mut App) { - cx.observe_new(register).detach(); -} - -fn register(workspace: &mut Workspace, _window: Option<&mut Window>, _: &mut Context) { - workspace.register_action(configure_snippets); - workspace.register_action(open_folder); -} - -fn configure_snippets( - workspace: &mut Workspace, - _: &ConfigureSnippets, - window: &mut Window, - cx: &mut Context, -) { - let language_registry = workspace.app_state().languages.clone(); - let workspace_handle = workspace.weak_handle(); - - workspace.toggle_modal(window, cx, move |window, cx| { - ScopeSelector::new(language_registry, workspace_handle, window, cx) - }); -} - -fn open_folder( - workspace: &mut Workspace, - _: &OpenFolder, - _: &mut Window, - cx: &mut Context, -) { - fs::create_dir_all(snippets_dir()).notify_err(workspace, cx); - cx.open_with_system(snippets_dir().borrow()); -} - -pub struct ScopeSelector { - picker: Entity>, -} - -impl ScopeSelector { - fn new( - language_registry: Arc, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let delegate = - ScopeSelectorDelegate::new(workspace, cx.entity().downgrade(), language_registry); - - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); - - Self { picker } - } -} - -impl ModalView for ScopeSelector {} - -impl EventEmitter for ScopeSelector {} - -impl Focusable for ScopeSelector { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl Render for ScopeSelector { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - v_flex().w(rems(34.)).child(self.picker.clone()) - } -} - -pub struct ScopeSelectorDelegate { - workspace: WeakEntity, - scope_selector: WeakEntity, - language_registry: Arc, - candidates: Vec, - matches: Vec, - selected_index: usize, - existing_scopes: HashSet, -} - -impl ScopeSelectorDelegate { - fn new( - workspace: WeakEntity, - scope_selector: WeakEntity, - language_registry: Arc, - ) -> Self { - let languages = language_registry.language_names().into_iter(); - - let candidates = std::iter::once(LanguageName::new(GLOBAL_SCOPE_NAME)) - .chain(languages) - .enumerate() - .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name.as_ref())) - .collect::>(); - - let mut existing_scopes = HashSet::new(); - - if let Some(read_dir) = fs::read_dir(snippets_dir()).log_err() { - for entry in read_dir { - if let Some(entry) = entry.log_err() { - let path = entry.path(); - if let (Some(stem), Some(extension)) = (path.file_stem(), path.extension()) - && extension.to_os_string().to_str() == Some("json") - && let Ok(file_name) = stem.to_os_string().into_string() - { - existing_scopes - .insert(ScopeName::from(ScopeFileName(Cow::Owned(file_name)))); - } - } - } - } - - Self { - workspace, - scope_selector, - language_registry, - candidates, - matches: Vec::new(), - selected_index: 0, - existing_scopes, - } - } - - fn scope_icon(&self, matcher: &LanguageMatcher, cx: &App) -> Option { - matcher - .path_suffixes - .iter() - .find_map(|extension| FileIcons::get_icon(Path::new(extension), cx)) - .or(FileIcons::get(cx).get_icon_for_type("default", cx)) - .map(Icon::from_path) - .map(|icon| icon.color(Color::Muted)) - } -} - -impl PickerDelegate for ScopeSelectorDelegate { - type ListItem = ListItem; - - fn placeholder_text(&self, _window: &mut Window, _: &mut App) -> Arc { - "Select snippet scope...".into() - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { - if let Some(mat) = self.matches.get(self.selected_index) { - let scope_name = self.candidates[mat.candidate_id].string.clone(); - let language = self.language_registry.language_for_name(&scope_name); - - if let Some(workspace) = self.workspace.upgrade() { - cx.spawn_in(window, async move |_, cx| { - let scope_file_name = ScopeFileName(match scope_name.to_lowercase().as_str() { - GLOBAL_SCOPE_NAME => Cow::Borrowed(GLOBAL_SCOPE_FILE_NAME), - _ => Cow::Owned(language.await?.lsp_id()), - }); - - workspace.update_in(cx, |workspace, window, cx| { - workspace - .with_local_workspace(window, cx, |workspace, window, cx| { - workspace - .open_abs_path( - snippets_dir().join(scope_file_name.with_extension()), - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - window, - cx, - ) - .detach(); - }) - .detach(); - }) - }) - .detach_and_log_err(cx); - }; - } - self.dismissed(window, cx); - } - - fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { - self.scope_selector - .update(cx, |_, cx| cx.emit(DismissEvent)) - .log_err(); - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _window: &mut Window, - _: &mut Context>, - ) { - self.selected_index = ix; - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> gpui::Task<()> { - let background = cx.background_executor().clone(); - let candidates = self.candidates.clone(); - cx.spawn_in(window, async move |this, cx| { - let matches = if query.is_empty() { - candidates - .into_iter() - .enumerate() - .map(|(index, candidate)| StringMatch { - candidate_id: index, - string: candidate.string, - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - match_strings( - &candidates, - &query, - false, - true, - 100, - &Default::default(), - background, - ) - .await - }; - - this.update(cx, |this, cx| { - let delegate = &mut this.delegate; - delegate.matches = matches; - delegate.selected_index = delegate - .selected_index - .min(delegate.matches.len().saturating_sub(1)); - cx.notify(); - }) - .log_err(); - }) - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _window: &mut Window, - cx: &mut Context>, - ) -> Option { - let mat = &self.matches.get(ix)?; - let name_label = mat.string.clone(); - - let scope_name = ScopeName(Cow::Owned( - LanguageName::new(&self.candidates[mat.candidate_id].string).lsp_id(), - )); - let file_label = if self.existing_scopes.contains(&scope_name) { - Some(ScopeFileName::from(scope_name).with_extension()) - } else { - None - }; - - let language_icon = if FileFinderSettings::get_global(cx).file_icons { - let language_name = LanguageName::new(mat.string.as_str()); - self.language_registry - .available_language_for_name(language_name.as_ref()) - .and_then(|available_language| self.scope_icon(available_language.matcher(), cx)) - .or_else(|| { - Some( - Icon::from_path(IconName::ToolWeb.path()) - .map(|icon| icon.color(Color::Muted)), - ) - }) - } else { - None - }; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .start_slot::(language_icon) - .child( - h_flex() - .gap_x_2() - .child(HighlightedLabel::new(name_label, mat.positions.clone())) - .when_some(file_label, |item, path_label| { - item.child( - Label::new(path_label) - .color(Color::Muted) - .size(LabelSize::Small), - ) - }), - ), - ) - } -} diff --git a/crates/sqlez/.gitignore b/crates/sqlez/.gitignore deleted file mode 100644 index 8130c3ab47..0000000000 --- a/crates/sqlez/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -debug/ -target/ diff --git a/crates/sqlez/Cargo.toml b/crates/sqlez/Cargo.toml deleted file mode 100644 index 5f4a0bef67..0000000000 --- a/crates/sqlez/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "sqlez" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[dependencies] -anyhow.workspace = true -collections.workspace = true -futures.workspace = true -indoc.workspace = true -libsqlite3-sys.workspace = true -log.workspace = true -parking_lot.workspace = true -smol.workspace = true -sqlformat.workspace = true -thread_local = "1.1.4" -util.workspace = true -uuid.workspace = true diff --git a/crates/sqlez/LICENSE-GPL b/crates/sqlez/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/sqlez/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/sqlez/src/bindable.rs b/crates/sqlez/src/bindable.rs deleted file mode 100644 index 9370389ff4..0000000000 --- a/crates/sqlez/src/bindable.rs +++ /dev/null @@ -1,466 +0,0 @@ -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; - -use anyhow::{Context as _, Result}; -use util::paths::PathExt; - -use crate::statement::{SqlType, Statement}; - -/// Define the number of columns that a type occupies in a query/database -pub trait StaticColumnCount { - fn column_count() -> usize { - 1 - } -} - -/// Bind values of different types to placeholders in a prepared SQL statement. -pub trait Bind { - fn bind(&self, statement: &Statement, start_index: i32) -> Result; -} - -pub trait Column: Sized { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)>; -} - -impl StaticColumnCount for bool {} -impl Bind for bool { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement - .bind(&self.then_some(1).unwrap_or(0), start_index) - .with_context(|| format!("Failed to bind bool at index {start_index}")) - } -} - -impl Column for bool { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - i32::column(statement, start_index) - .map(|(i, next_index)| (i != 0, next_index)) - .with_context(|| format!("Failed to read bool at index {start_index}")) - } -} - -impl StaticColumnCount for &[u8] {} -impl Bind for &[u8] { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement - .bind_blob(start_index, self) - .with_context(|| format!("Failed to bind &[u8] at index {start_index}"))?; - Ok(start_index + 1) - } -} - -impl StaticColumnCount for &[u8; C] {} -impl Bind for &[u8; C] { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement - .bind_blob(start_index, self.as_slice()) - .with_context(|| format!("Failed to bind &[u8; C] at index {start_index}"))?; - Ok(start_index + 1) - } -} - -impl Column for [u8; C] { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let bytes_slice = statement.column_blob(start_index)?; - let array = bytes_slice.try_into()?; - Ok((array, start_index + 1)) - } -} - -impl StaticColumnCount for Vec {} -impl Bind for Vec { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement - .bind_blob(start_index, self) - .with_context(|| format!("Failed to bind Vec at index {start_index}"))?; - Ok(start_index + 1) - } -} - -impl Column for Vec { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement - .column_blob(start_index) - .with_context(|| format!("Failed to read Vec at index {start_index}"))?; - - Ok((Vec::from(result), start_index + 1)) - } -} - -impl StaticColumnCount for f64 {} -impl Bind for f64 { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement - .bind_double(start_index, *self) - .with_context(|| format!("Failed to bind f64 at index {start_index}"))?; - Ok(start_index + 1) - } -} - -impl Column for f64 { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement - .column_double(start_index) - .with_context(|| format!("Failed to parse f64 at index {start_index}"))?; - - Ok((result, start_index + 1)) - } -} - -impl StaticColumnCount for f32 {} -impl Bind for f32 { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement - .bind_double(start_index, *self as f64) - .with_context(|| format!("Failed to bind f64 at index {start_index}"))?; - Ok(start_index + 1) - } -} - -impl Column for f32 { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement - .column_double(start_index) - .with_context(|| format!("Failed to parse f32 at index {start_index}"))? - as f32; - - Ok((result, start_index + 1)) - } -} - -impl StaticColumnCount for i32 {} -impl Bind for i32 { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement - .bind_int(start_index, *self) - .with_context(|| format!("Failed to bind i32 at index {start_index}"))?; - - Ok(start_index + 1) - } -} - -impl Column for i32 { - fn column<'a>(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement.column_int(start_index)?; - Ok((result, start_index + 1)) - } -} - -impl StaticColumnCount for i64 {} -impl Bind for i64 { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement - .bind_int64(start_index, *self) - .with_context(|| format!("Failed to bind i64 at index {start_index}"))?; - Ok(start_index + 1) - } -} - -impl Column for i64 { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement.column_int64(start_index)?; - Ok((result, start_index + 1)) - } -} - -impl StaticColumnCount for u64 {} -impl Bind for u64 { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement - .bind_int64(start_index, (*self) as i64) - .with_context(|| format!("Failed to bind i64 at index {start_index}"))?; - Ok(start_index + 1) - } -} - -impl Column for u64 { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement.column_int64(start_index)? as u64; - Ok((result, start_index + 1)) - } -} - -impl StaticColumnCount for u32 {} -impl Bind for u32 { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - (*self as i64) - .bind(statement, start_index) - .with_context(|| format!("Failed to bind usize at index {start_index}")) - } -} - -impl Column for u32 { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement.column_int64(start_index)?; - Ok((result as u32, start_index + 1)) - } -} - -impl StaticColumnCount for u16 {} -impl Bind for u16 { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - (*self as i64) - .bind(statement, start_index) - .with_context(|| format!("Failed to bind usize at index {start_index}")) - } -} - -impl Column for u16 { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement.column_int64(start_index)?; - Ok((result as u16, start_index + 1)) - } -} - -impl StaticColumnCount for usize {} -impl Bind for usize { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - (*self as i64) - .bind(statement, start_index) - .with_context(|| format!("Failed to bind usize at index {start_index}")) - } -} - -impl Column for usize { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement.column_int64(start_index)?; - Ok((result as usize, start_index + 1)) - } -} - -impl StaticColumnCount for &str {} -impl Bind for &str { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement.bind_text(start_index, self)?; - Ok(start_index + 1) - } -} - -impl StaticColumnCount for Arc {} -impl Bind for Arc { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement.bind_text(start_index, self.as_ref())?; - Ok(start_index + 1) - } -} - -impl StaticColumnCount for String {} -impl Bind for String { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement.bind_text(start_index, self)?; - Ok(start_index + 1) - } -} - -impl Column for Arc { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement.column_text(start_index)?; - Ok((Arc::from(result), start_index + 1)) - } -} - -impl Column for String { - fn column<'a>(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement.column_text(start_index)?; - Ok((result.to_owned(), start_index + 1)) - } -} - -impl StaticColumnCount for Option { - fn column_count() -> usize { - T::column_count() - } -} -impl Bind for Option { - fn bind(&self, statement: &Statement, mut start_index: i32) -> Result { - if let Some(this) = self { - this.bind(statement, start_index) - } else { - for _ in 0..T::column_count() { - statement.bind_null(start_index)?; - start_index += 1; - } - Ok(start_index) - } - } -} - -impl Column for Option { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - if let SqlType::Null = statement.column_type(start_index)? { - Ok((None, start_index + T::column_count() as i32)) - } else { - T::column(statement, start_index).map(|(result, next_index)| (Some(result), next_index)) - } - } -} - -impl StaticColumnCount for [T; COUNT] { - fn column_count() -> usize { - T::column_count() * COUNT - } -} -impl Bind for [T; COUNT] { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let mut current_index = start_index; - for binding in self { - current_index = binding.bind(statement, current_index)? - } - - Ok(current_index) - } -} - -impl StaticColumnCount for &Path {} -impl Bind for &Path { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - self.as_os_str() - .as_encoded_bytes() - .bind(statement, start_index) - } -} - -impl StaticColumnCount for Arc {} -impl Bind for Arc { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - self.as_ref().bind(statement, start_index) - } -} -impl Column for Arc { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let blob = statement.column_blob(start_index)?; - - PathBuf::try_from_bytes(blob).map(|path| (Arc::from(path.as_path()), start_index + 1)) - } -} - -impl StaticColumnCount for PathBuf {} -impl Bind for PathBuf { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - (self.as_ref() as &Path).bind(statement, start_index) - } -} - -impl Column for PathBuf { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let blob = statement.column_blob(start_index)?; - - PathBuf::try_from_bytes(blob).map(|path| (path, start_index + 1)) - } -} - -impl StaticColumnCount for uuid::Uuid { - fn column_count() -> usize { - 1 - } -} - -impl Bind for uuid::Uuid { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - self.as_bytes().bind(statement, start_index) - } -} - -impl Column for uuid::Uuid { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let (bytes, next_index) = Column::column(statement, start_index)?; - Ok((uuid::Uuid::from_bytes(bytes), next_index)) - } -} - -impl StaticColumnCount for () { - fn column_count() -> usize { - 0 - } -} -/// Unit impls do nothing. This simplifies query macros -impl Bind for () { - fn bind(&self, _statement: &Statement, start_index: i32) -> Result { - Ok(start_index) - } -} - -impl Column for () { - fn column(_statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - Ok(((), start_index)) - } -} - -macro_rules! impl_tuple_row_traits { - ( $($local:ident: $type:ident),+ ) => { - impl<$($type: StaticColumnCount),+> StaticColumnCount for ($($type,)+) { - fn column_count() -> usize { - let mut count = 0; - $(count += $type::column_count();)+ - count - } - } - - impl<$($type: Bind),+> Bind for ($($type,)+) { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let mut next_index = start_index; - let ($($local,)+) = self; - $(next_index = $local.bind(statement, next_index)?;)+ - Ok(next_index) - } - } - - impl<$($type: Column),+> Column for ($($type,)+) { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let mut next_index = start_index; - Ok(( - ( - $({ - let value; - (value, next_index) = $type::column(statement, next_index)?; - value - },)+ - ), - next_index, - )) - } - } - } -} - -impl_tuple_row_traits!(t1: T1, t2: T2); -impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3); -impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3, t4: T4); -impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5); -impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6); -impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7); -impl_tuple_row_traits!( - t1: T1, - t2: T2, - t3: T3, - t4: T4, - t5: T5, - t6: T6, - t7: T7, - t8: T8 -); -impl_tuple_row_traits!( - t1: T1, - t2: T2, - t3: T3, - t4: T4, - t5: T5, - t6: T6, - t7: T7, - t8: T8, - t9: T9 -); -impl_tuple_row_traits!( - t1: T1, - t2: T2, - t3: T3, - t4: T4, - t5: T5, - t6: T6, - t7: T7, - t8: T8, - t9: T9, - t10: T10 -); diff --git a/crates/sqlez/src/connection.rs b/crates/sqlez/src/connection.rs deleted file mode 100644 index 53f0d4e261..0000000000 --- a/crates/sqlez/src/connection.rs +++ /dev/null @@ -1,445 +0,0 @@ -use std::{ - cell::RefCell, - ffi::{CStr, CString}, - marker::PhantomData, - path::Path, - ptr, -}; - -use anyhow::Result; -use libsqlite3_sys::*; - -pub struct Connection { - pub(crate) sqlite3: *mut sqlite3, - persistent: bool, - pub(crate) write: RefCell, - _sqlite: PhantomData, -} -unsafe impl Send for Connection {} - -impl Connection { - pub(crate) fn open(uri: &str, persistent: bool) -> Result { - let mut connection = Self { - sqlite3: ptr::null_mut(), - persistent, - write: RefCell::new(true), - _sqlite: PhantomData, - }; - - let flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX | SQLITE_OPEN_READWRITE; - unsafe { - sqlite3_open_v2( - CString::new(uri)?.as_ptr(), - &mut connection.sqlite3, - flags, - ptr::null(), - ); - - // Turn on extended error codes - sqlite3_extended_result_codes(connection.sqlite3, 1); - - connection.last_error()?; - } - - Ok(connection) - } - - /// Attempts to open the database at uri. If it fails, a shared memory db will be opened - /// instead. - pub fn open_file(uri: &str) -> Self { - Self::open(uri, true).unwrap_or_else(|_| Self::open_memory(Some(uri))) - } - - pub fn open_memory(uri: Option<&str>) -> Self { - let in_memory_path = if let Some(uri) = uri { - format!("file:{}?mode=memory&cache=shared", uri) - } else { - ":memory:".to_string() - }; - - Self::open(&in_memory_path, false).expect("Could not create fallback in memory db") - } - - pub fn persistent(&self) -> bool { - self.persistent - } - - pub fn can_write(&self) -> bool { - *self.write.borrow() - } - - pub fn backup_main(&self, destination: &Connection) -> Result<()> { - unsafe { - let backup = sqlite3_backup_init( - destination.sqlite3, - CString::new("main")?.as_ptr(), - self.sqlite3, - CString::new("main")?.as_ptr(), - ); - sqlite3_backup_step(backup, -1); - sqlite3_backup_finish(backup); - destination.last_error() - } - } - - pub fn backup_main_to(&self, destination: impl AsRef) -> Result<()> { - let destination = Self::open_file(destination.as_ref().to_string_lossy().as_ref()); - self.backup_main(&destination) - } - - pub fn sql_has_syntax_error(&self, sql: &str) -> Option<(String, usize)> { - let sql = CString::new(sql).unwrap(); - let mut remaining_sql = sql.as_c_str(); - let sql_start = remaining_sql.as_ptr(); - - let mut alter_table = None; - while { - let remaining_sql_str = remaining_sql.to_str().unwrap().trim(); - let any_remaining_sql = remaining_sql_str != ";" && !remaining_sql_str.is_empty(); - if any_remaining_sql { - alter_table = parse_alter_table(remaining_sql_str); - } - any_remaining_sql - } { - let mut raw_statement = ptr::null_mut::(); - let mut remaining_sql_ptr = ptr::null(); - - let (res, offset, message, _conn) = if let Some((table_to_alter, column)) = alter_table - { - // ALTER TABLE is a weird statement. When preparing the statement the table's - // existence is checked *before* syntax checking any other part of the statement. - // Therefore, we need to make sure that the table has been created before calling - // prepare. As we don't want to trash whatever database this is connected to, we - // create a new in-memory DB to test. - - let temp_connection = Connection::open_memory(None); - //This should always succeed, if it doesn't then you really should know about it - temp_connection - .exec(&format!("CREATE TABLE {table_to_alter}({column})")) - .unwrap()() - .unwrap(); - - unsafe { - sqlite3_prepare_v2( - temp_connection.sqlite3, - remaining_sql.as_ptr(), - -1, - &mut raw_statement, - &mut remaining_sql_ptr, - ) - }; - - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - let offset = unsafe { sqlite3_error_offset(temp_connection.sqlite3) }; - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - let offset = 0; - - unsafe { - ( - sqlite3_errcode(temp_connection.sqlite3), - offset, - sqlite3_errmsg(temp_connection.sqlite3), - Some(temp_connection), - ) - } - } else { - unsafe { - sqlite3_prepare_v2( - self.sqlite3, - remaining_sql.as_ptr(), - -1, - &mut raw_statement, - &mut remaining_sql_ptr, - ) - }; - - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - let offset = unsafe { sqlite3_error_offset(self.sqlite3) }; - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - let offset = 0; - - unsafe { - ( - sqlite3_errcode(self.sqlite3), - offset, - sqlite3_errmsg(self.sqlite3), - None, - ) - } - }; - - unsafe { sqlite3_finalize(raw_statement) }; - - if res == 1 && offset >= 0 { - let sub_statement_correction = remaining_sql.as_ptr() as usize - sql_start as usize; - let err_msg = String::from_utf8_lossy(unsafe { - CStr::from_ptr(message as *const _).to_bytes() - }) - .into_owned(); - - return Some((err_msg, offset as usize + sub_statement_correction)); - } - remaining_sql = unsafe { CStr::from_ptr(remaining_sql_ptr) }; - alter_table = None; - } - None - } - - pub(crate) fn last_error(&self) -> Result<()> { - unsafe { - let code = sqlite3_errcode(self.sqlite3); - const NON_ERROR_CODES: &[i32] = &[SQLITE_OK, SQLITE_ROW]; - if NON_ERROR_CODES.contains(&code) { - return Ok(()); - } - - let message = sqlite3_errmsg(self.sqlite3); - let message = if message.is_null() { - None - } else { - Some( - String::from_utf8_lossy(CStr::from_ptr(message as *const _).to_bytes()) - .into_owned(), - ) - }; - - anyhow::bail!("Sqlite call failed with code {code} and message: {message:?}") - } - } - - pub(crate) fn with_write(&self, callback: impl FnOnce(&Connection) -> T) -> T { - *self.write.borrow_mut() = true; - let result = callback(self); - *self.write.borrow_mut() = false; - result - } -} - -fn parse_alter_table(remaining_sql_str: &str) -> Option<(String, String)> { - let remaining_sql_str = remaining_sql_str.to_lowercase(); - if remaining_sql_str.starts_with("alter") - && let Some(table_offset) = remaining_sql_str.find("table") - { - let after_table_offset = table_offset + "table".len(); - let table_to_alter = remaining_sql_str - .chars() - .skip(after_table_offset) - .skip_while(|c| c.is_whitespace()) - .take_while(|c| !c.is_whitespace()) - .collect::(); - if !table_to_alter.is_empty() { - let column_name = if let Some(rename_offset) = remaining_sql_str.find("rename column") { - let after_rename_offset = rename_offset + "rename column".len(); - remaining_sql_str - .chars() - .skip(after_rename_offset) - .skip_while(|c| c.is_whitespace()) - .take_while(|c| !c.is_whitespace()) - .collect::() - } else if let Some(drop_offset) = remaining_sql_str.find("drop column") { - let after_drop_offset = drop_offset + "drop column".len(); - remaining_sql_str - .chars() - .skip(after_drop_offset) - .skip_while(|c| c.is_whitespace()) - .take_while(|c| !c.is_whitespace()) - .collect::() - } else { - "__place_holder_column_for_syntax_checking".to_string() - }; - return Some((table_to_alter, column_name)); - } - } - None -} - -impl Drop for Connection { - fn drop(&mut self) { - unsafe { sqlite3_close(self.sqlite3) }; - } -} - -#[cfg(test)] -mod test { - use anyhow::Result; - use indoc::indoc; - - use crate::connection::Connection; - - #[test] - fn string_round_trips() -> Result<()> { - let connection = Connection::open_memory(Some("string_round_trips")); - connection - .exec(indoc! {" - CREATE TABLE text ( - text TEXT - );"}) - .unwrap()() - .unwrap(); - - let text = "Some test text"; - - connection - .exec_bound("INSERT INTO text (text) VALUES (?);") - .unwrap()(text) - .unwrap(); - - assert_eq!( - connection.select_row("SELECT text FROM text;").unwrap()().unwrap(), - Some(text.to_string()) - ); - - Ok(()) - } - - #[test] - fn tuple_round_trips() { - let connection = Connection::open_memory(Some("tuple_round_trips")); - connection - .exec(indoc! {" - CREATE TABLE test ( - text TEXT, - integer INTEGER, - blob BLOB - );"}) - .unwrap()() - .unwrap(); - - let tuple1 = ("test".to_string(), 64, vec![0, 1, 2, 4, 8, 16, 32, 64]); - let tuple2 = ("test2".to_string(), 32, vec![64, 32, 16, 8, 4, 2, 1, 0]); - - let mut insert = connection - .exec_bound::<(String, usize, Vec)>( - "INSERT INTO test (text, integer, blob) VALUES (?, ?, ?)", - ) - .unwrap(); - - insert(tuple1.clone()).unwrap(); - insert(tuple2.clone()).unwrap(); - - assert_eq!( - connection - .select::<(String, usize, Vec)>("SELECT * FROM test") - .unwrap()() - .unwrap(), - vec![tuple1, tuple2] - ); - } - - #[test] - fn bool_round_trips() { - let connection = Connection::open_memory(Some("bool_round_trips")); - connection - .exec(indoc! {" - CREATE TABLE bools ( - t INTEGER, - f INTEGER - );"}) - .unwrap()() - .unwrap(); - - connection - .exec_bound("INSERT INTO bools(t, f) VALUES (?, ?)") - .unwrap()((true, false)) - .unwrap(); - - assert_eq!( - connection - .select_row::<(bool, bool)>("SELECT * FROM bools;") - .unwrap()() - .unwrap(), - Some((true, false)) - ); - } - - #[test] - fn backup_works() { - let connection1 = Connection::open_memory(Some("backup_works")); - connection1 - .exec(indoc! {" - CREATE TABLE blobs ( - data BLOB - );"}) - .unwrap()() - .unwrap(); - let blob = vec![0, 1, 2, 4, 8, 16, 32, 64]; - connection1 - .exec_bound::>("INSERT INTO blobs (data) VALUES (?);") - .unwrap()(blob.clone()) - .unwrap(); - - // Backup connection1 to connection2 - let connection2 = Connection::open_memory(Some("backup_works_other")); - connection1.backup_main(&connection2).unwrap(); - - // Delete the added blob and verify its deleted on the other side - let read_blobs = connection1 - .select::>("SELECT * FROM blobs;") - .unwrap()() - .unwrap(); - assert_eq!(read_blobs, vec![blob]); - } - - #[test] - fn multi_step_statement_works() { - let connection = Connection::open_memory(Some("multi_step_statement_works")); - - connection - .exec(indoc! {" - CREATE TABLE test ( - col INTEGER - )"}) - .unwrap()() - .unwrap(); - - connection - .exec(indoc! {" - INSERT INTO test(col) VALUES (2)"}) - .unwrap()() - .unwrap(); - - assert_eq!( - connection - .select_row::("SELECT * FROM test") - .unwrap()() - .unwrap(), - Some(2) - ); - } - - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - #[test] - fn test_sql_has_syntax_errors() { - let connection = Connection::open_memory(Some("test_sql_has_syntax_errors")); - let first_stmt = - "CREATE TABLE kv_store(key TEXT PRIMARY KEY, value TEXT NOT NULL) STRICT ;"; - let second_stmt = "SELECT FROM"; - - let second_offset = connection.sql_has_syntax_error(second_stmt).unwrap().1; - - let res = connection - .sql_has_syntax_error(&format!("{}\n{}", first_stmt, second_stmt)) - .map(|(_, offset)| offset); - - assert_eq!(res, Some(first_stmt.len() + second_offset + 1)); - } - - #[test] - fn test_alter_table_syntax() { - let connection = Connection::open_memory(Some("test_alter_table_syntax")); - - assert!( - connection - .sql_has_syntax_error("ALTER TABLE test ADD x TEXT") - .is_none() - ); - - assert!( - connection - .sql_has_syntax_error("ALTER TABLE test AAD x TEXT") - .is_some() - ); - } -} diff --git a/crates/sqlez/src/domain.rs b/crates/sqlez/src/domain.rs deleted file mode 100644 index 5744a67da2..0000000000 --- a/crates/sqlez/src/domain.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::connection::Connection; - -pub trait Domain: 'static { - const NAME: &str; - const MIGRATIONS: &[&str]; - - fn should_allow_migration_change(_index: usize, _old: &str, _new: &str) -> bool { - false - } -} - -pub trait Migrator: 'static { - fn migrate(connection: &Connection) -> anyhow::Result<()>; -} - -impl Migrator for () { - fn migrate(_connection: &Connection) -> anyhow::Result<()> { - Ok(()) // Do nothing - } -} - -impl Migrator for D { - fn migrate(connection: &Connection) -> anyhow::Result<()> { - connection.migrate( - Self::NAME, - Self::MIGRATIONS, - Self::should_allow_migration_change, - ) - } -} - -impl Migrator for (D1, D2) { - fn migrate(connection: &Connection) -> anyhow::Result<()> { - D1::migrate(connection)?; - D2::migrate(connection) - } -} - -impl Migrator for (D1, D2, D3) { - fn migrate(connection: &Connection) -> anyhow::Result<()> { - D1::migrate(connection)?; - D2::migrate(connection)?; - D3::migrate(connection) - } -} - -impl Migrator for (D1, D2, D3, D4) { - fn migrate(connection: &Connection) -> anyhow::Result<()> { - D1::migrate(connection)?; - D2::migrate(connection)?; - D3::migrate(connection)?; - D4::migrate(connection) - } -} - -impl Migrator for (D1, D2, D3, D4, D5) { - fn migrate(connection: &Connection) -> anyhow::Result<()> { - D1::migrate(connection)?; - D2::migrate(connection)?; - D3::migrate(connection)?; - D4::migrate(connection)?; - D5::migrate(connection) - } -} diff --git a/crates/sqlez/src/lib.rs b/crates/sqlez/src/lib.rs deleted file mode 100644 index a22cfff2b3..0000000000 --- a/crates/sqlez/src/lib.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod bindable; -pub mod connection; -pub mod domain; -pub mod migrations; -pub mod savepoint; -pub mod statement; -pub mod thread_safe_connection; -pub mod typed_statements; -mod util; - -pub use anyhow; diff --git a/crates/sqlez/src/migrations.rs b/crates/sqlez/src/migrations.rs deleted file mode 100644 index 567d82f9af..0000000000 --- a/crates/sqlez/src/migrations.rs +++ /dev/null @@ -1,390 +0,0 @@ -// Migrations are constructed by domain, and stored in a table in the connection db with domain name, -// effected tables, actual query text, and order. -// If a migration is run and any of the query texts don't match, the app panics on startup (maybe fallback -// to creating a new db?) -// Otherwise any missing migrations are run on the connection - -use std::ffi::CString; - -use anyhow::{Context as _, Result}; -use indoc::{formatdoc, indoc}; -use libsqlite3_sys::sqlite3_exec; - -use crate::connection::Connection; - -impl Connection { - fn eager_exec(&self, sql: &str) -> anyhow::Result<()> { - let sql_str = CString::new(sql).context("Error creating cstr")?; - unsafe { - sqlite3_exec( - self.sqlite3, - sql_str.as_c_str().as_ptr(), - None, - std::ptr::null_mut(), - std::ptr::null_mut(), - ); - } - self.last_error() - .with_context(|| format!("Prepare call failed for query:\n{}", sql))?; - - Ok(()) - } - - /// Migrate the database, for the given domain. - /// Note: Unlike everything else in SQLez, migrations are run eagerly, without first - /// preparing the SQL statements. This makes it possible to do multi-statement schema - /// updates in a single string without running into prepare errors. - pub fn migrate( - &self, - domain: &'static str, - migrations: &[&'static str], - mut should_allow_migration_change: impl FnMut(usize, &str, &str) -> bool, - ) -> Result<()> { - self.with_savepoint("migrating", || { - // Setup the migrations table unconditionally - self.exec(indoc! {" - CREATE TABLE IF NOT EXISTS migrations ( - domain TEXT, - step INTEGER, - migration TEXT - )"})?()?; - - let completed_migrations = - self.select_bound::<&str, (String, usize, String)>(indoc! {" - SELECT domain, step, migration FROM migrations - WHERE domain = ? - ORDER BY step - "})?(domain)?; - - let mut store_completed_migration = self - .exec_bound("INSERT INTO migrations (domain, step, migration) VALUES (?, ?, ?)")?; - - let mut did_migrate = false; - for (index, migration) in migrations.iter().enumerate() { - let migration = - sqlformat::format(migration, &sqlformat::QueryParams::None, Default::default()); - if let Some((_, _, completed_migration)) = completed_migrations.get(index) { - // Reformat completed migrations with the current `sqlformat` version, so that past migrations stored - // conform to the new formatting rules. - let completed_migration = sqlformat::format( - completed_migration, - &sqlformat::QueryParams::None, - Default::default(), - ); - if completed_migration == migration { - // Migration already run. Continue - continue; - } else if should_allow_migration_change(index, &completed_migration, &migration) - { - continue; - } else { - anyhow::bail!(formatdoc! {" - Migration changed for {domain} at step {index} - - Stored migration: - {completed_migration} - - Proposed migration: - {migration}"}); - } - } - - self.eager_exec(&migration)?; - did_migrate = true; - store_completed_migration((domain, index, migration))?; - } - - if did_migrate { - self.delete_rows_with_orphaned_foreign_key_references()?; - self.exec("PRAGMA foreign_key_check;")?()?; - } - - Ok(()) - }) - } - - /// Delete any rows that were orphaned by a migration. This is needed - /// because we disable foreign key constraints during migrations, so - /// that it's possible to re-create a table with the same name, without - /// deleting all associated data. - fn delete_rows_with_orphaned_foreign_key_references(&self) -> Result<()> { - let foreign_key_info: Vec<(String, String, String, String)> = self.select( - r#" - SELECT DISTINCT - schema.name as child_table, - foreign_keys.[from] as child_key, - foreign_keys.[table] as parent_table, - foreign_keys.[to] as parent_key - FROM sqlite_schema schema - JOIN pragma_foreign_key_list(schema.name) foreign_keys - WHERE - schema.type = 'table' AND - schema.name NOT LIKE "sqlite_%" - "#, - )?()?; - - if !foreign_key_info.is_empty() { - log::info!( - "Found {} foreign key relationships to check", - foreign_key_info.len() - ); - } - - for (child_table, child_key, parent_table, parent_key) in foreign_key_info { - self.exec(&format!( - " - DELETE FROM {child_table} - WHERE {child_key} IS NOT NULL and {child_key} NOT IN - (SELECT {parent_key} FROM {parent_table}) - " - ))?()?; - } - - Ok(()) - } -} - -#[cfg(test)] -mod test { - use indoc::indoc; - - use crate::connection::Connection; - - #[test] - fn test_migrations_are_added_to_table() { - let connection = Connection::open_memory(Some("migrations_are_added_to_table")); - - // Create first migration with a single step and run it - connection - .migrate( - "test", - &[indoc! {" - CREATE TABLE test1 ( - a TEXT, - b TEXT - )"}], - disallow_migration_change, - ) - .unwrap(); - - // Verify it got added to the migrations table - assert_eq!( - &connection - .select::("SELECT (migration) FROM migrations") - .unwrap()() - .unwrap()[..], - &[indoc! {"CREATE TABLE test1 (a TEXT, b TEXT)"}], - ); - - // Add another step to the migration and run it again - connection - .migrate( - "test", - &[ - indoc! {" - CREATE TABLE test1 ( - a TEXT, - b TEXT - )"}, - indoc! {" - CREATE TABLE test2 ( - c TEXT, - d TEXT - )"}, - ], - disallow_migration_change, - ) - .unwrap(); - - // Verify it is also added to the migrations table - assert_eq!( - &connection - .select::("SELECT (migration) FROM migrations") - .unwrap()() - .unwrap()[..], - &[ - indoc! {"CREATE TABLE test1 (a TEXT, b TEXT)"}, - indoc! {"CREATE TABLE test2 (c TEXT, d TEXT)"}, - ], - ); - } - - #[test] - fn test_migration_setup_works() { - let connection = Connection::open_memory(Some("migration_setup_works")); - - connection - .exec(indoc! {" - CREATE TABLE IF NOT EXISTS migrations ( - domain TEXT, - step INTEGER, - migration TEXT - );"}) - .unwrap()() - .unwrap(); - - let mut store_completed_migration = connection - .exec_bound::<(&str, usize, String)>(indoc! {" - INSERT INTO migrations (domain, step, migration) - VALUES (?, ?, ?)"}) - .unwrap(); - - let domain = "test_domain"; - for i in 0..5 { - // Create a table forcing a schema change - connection - .exec(&format!("CREATE TABLE table{} ( test TEXT );", i)) - .unwrap()() - .unwrap(); - - store_completed_migration((domain, i, i.to_string())).unwrap(); - } - } - - #[test] - fn migrations_dont_rerun() { - let connection = Connection::open_memory(Some("migrations_dont_rerun")); - - // Create migration which clears a table - - // Manually create the table for that migration with a row - connection - .exec(indoc! {" - CREATE TABLE test_table ( - test_column INTEGER - );"}) - .unwrap()() - .unwrap(); - connection - .exec(indoc! {" - INSERT INTO test_table (test_column) VALUES (1);"}) - .unwrap()() - .unwrap(); - - assert_eq!( - connection - .select_row::("SELECT * FROM test_table") - .unwrap()() - .unwrap(), - Some(1) - ); - - // Run the migration verifying that the row got dropped - connection - .migrate( - "test", - &["DELETE FROM test_table"], - disallow_migration_change, - ) - .unwrap(); - assert_eq!( - connection - .select_row::("SELECT * FROM test_table") - .unwrap()() - .unwrap(), - None - ); - - // Recreate the dropped row - connection - .exec("INSERT INTO test_table (test_column) VALUES (2)") - .unwrap()() - .unwrap(); - - // Run the same migration again and verify that the table was left unchanged - connection - .migrate( - "test", - &["DELETE FROM test_table"], - disallow_migration_change, - ) - .unwrap(); - assert_eq!( - connection - .select_row::("SELECT * FROM test_table") - .unwrap()() - .unwrap(), - Some(2) - ); - } - - #[test] - fn changed_migration_fails() { - let connection = Connection::open_memory(Some("changed_migration_fails")); - - // Create a migration with two steps and run it - connection - .migrate( - "test migration", - &[ - "CREATE TABLE test (col INTEGER)", - "INSERT INTO test (col) VALUES (1)", - ], - disallow_migration_change, - ) - .unwrap(); - - let mut migration_changed = false; - - // Create another migration with the same domain but different steps - let second_migration_result = connection.migrate( - "test migration", - &[ - "CREATE TABLE test (color INTEGER )", - "INSERT INTO test (color) VALUES (1)", - ], - |_, old, new| { - assert_eq!(old, "CREATE TABLE test (col INTEGER)"); - assert_eq!(new, "CREATE TABLE test (color INTEGER)"); - migration_changed = true; - false - }, - ); - - // Verify new migration returns error when run - assert!(second_migration_result.is_err()) - } - - #[test] - fn test_create_alter_drop() { - let connection = Connection::open_memory(Some("test_create_alter_drop")); - - connection - .migrate( - "first_migration", - &["CREATE TABLE table1(a TEXT) STRICT;"], - disallow_migration_change, - ) - .unwrap(); - - connection - .exec("INSERT INTO table1(a) VALUES (\"test text\");") - .unwrap()() - .unwrap(); - - connection - .migrate( - "second_migration", - &[indoc! {" - CREATE TABLE table2(b TEXT) STRICT; - - INSERT INTO table2 (b) - SELECT a FROM table1; - - DROP TABLE table1; - - ALTER TABLE table2 RENAME TO table1; - "}], - disallow_migration_change, - ) - .unwrap(); - - let res = &connection.select::("SELECT b FROM table1").unwrap()().unwrap()[0]; - - assert_eq!(res, "test text"); - } - - fn disallow_migration_change(_: usize, _: &str, _: &str) -> bool { - false - } -} diff --git a/crates/sqlez/src/savepoint.rs b/crates/sqlez/src/savepoint.rs deleted file mode 100644 index 3177cea39f..0000000000 --- a/crates/sqlez/src/savepoint.rs +++ /dev/null @@ -1,150 +0,0 @@ -use anyhow::Result; -use indoc::formatdoc; - -use crate::connection::Connection; - -impl Connection { - // Run a set of commands within the context of a `SAVEPOINT name`. If the callback - // returns Err(_), the savepoint will be rolled back. Otherwise, the save - // point is released. - pub fn with_savepoint(&self, name: impl AsRef, f: F) -> Result - where - F: FnOnce() -> Result, - { - let name = name.as_ref(); - self.exec(&format!("SAVEPOINT {name}"))?()?; - let result = f(); - match result { - Ok(_) => { - self.exec(&format!("RELEASE {name}"))?()?; - } - Err(_) => { - self.exec(&formatdoc! {" - ROLLBACK TO {name}; - RELEASE {name}"})?()?; - } - } - result - } - - // Run a set of commands within the context of a `SAVEPOINT name`. If the callback - // returns Ok(None) or Err(_), the savepoint will be rolled back. Otherwise, the save - // point is released. - pub fn with_savepoint_rollback(&self, name: impl AsRef, f: F) -> Result> - where - F: FnOnce() -> Result>, - { - let name = name.as_ref(); - self.exec(&format!("SAVEPOINT {name}"))?()?; - let result = f(); - match result { - Ok(Some(_)) => { - self.exec(&format!("RELEASE {name}"))?()?; - } - Ok(None) | Err(_) => { - self.exec(&formatdoc! {" - ROLLBACK TO {name}; - RELEASE {name}"})?()?; - } - } - result - } -} - -#[cfg(test)] -mod tests { - use crate::connection::Connection; - use anyhow::Result; - use indoc::indoc; - - #[test] - fn test_nested_savepoints() -> Result<()> { - let connection = Connection::open_memory(Some("nested_savepoints")); - - connection - .exec(indoc! {" - CREATE TABLE text ( - text TEXT, - idx INTEGER - );"}) - .unwrap()() - .unwrap(); - - let save1_text = "test save1"; - let save2_text = "test save2"; - - connection.with_savepoint("first", || { - connection.exec_bound("INSERT INTO text(text, idx) VALUES (?, ?)")?((save1_text, 1))?; - - assert!( - connection - .with_savepoint("second", || -> anyhow::Result> { - connection.exec_bound("INSERT INTO text(text, idx) VALUES (?, ?)")?(( - save2_text, 2, - ))?; - - assert_eq!( - connection - .select::("SELECT text FROM text ORDER BY text.idx ASC")?( - )?, - vec![save1_text, save2_text], - ); - - anyhow::bail!("Failed second save point :(") - }) - .err() - .is_some() - ); - - assert_eq!( - connection.select::("SELECT text FROM text ORDER BY text.idx ASC")?()?, - vec![save1_text], - ); - - connection.with_savepoint_rollback::<(), _>("second", || { - connection.exec_bound("INSERT INTO text(text, idx) VALUES (?, ?)")?(( - save2_text, 2, - ))?; - - assert_eq!( - connection.select::("SELECT text FROM text ORDER BY text.idx ASC")?()?, - vec![save1_text, save2_text], - ); - - Ok(None) - })?; - - assert_eq!( - connection.select::("SELECT text FROM text ORDER BY text.idx ASC")?()?, - vec![save1_text], - ); - - connection.with_savepoint_rollback("second", || { - connection.exec_bound("INSERT INTO text(text, idx) VALUES (?, ?)")?(( - save2_text, 2, - ))?; - - assert_eq!( - connection.select::("SELECT text FROM text ORDER BY text.idx ASC")?()?, - vec![save1_text, save2_text], - ); - - Ok(Some(())) - })?; - - assert_eq!( - connection.select::("SELECT text FROM text ORDER BY text.idx ASC")?()?, - vec![save1_text, save2_text], - ); - - Ok(()) - })?; - - assert_eq!( - connection.select::("SELECT text FROM text ORDER BY text.idx ASC")?()?, - vec![save1_text, save2_text], - ); - - Ok(()) - } -} diff --git a/crates/sqlez/src/statement.rs b/crates/sqlez/src/statement.rs deleted file mode 100644 index d08e58a6f9..0000000000 --- a/crates/sqlez/src/statement.rs +++ /dev/null @@ -1,497 +0,0 @@ -use std::ffi::{CStr, CString, c_int}; -use std::marker::PhantomData; -use std::{ptr, slice, str}; - -use anyhow::{Context as _, Result, bail}; -use libsqlite3_sys::*; - -use crate::bindable::{Bind, Column}; -use crate::connection::Connection; - -pub struct Statement<'a> { - /// vector of pointers to the raw SQLite statement objects. - /// it holds the actual prepared statements that will be executed. - pub raw_statements: Vec<*mut sqlite3_stmt>, - /// Index of the current statement being executed from the `raw_statements` vector. - current_statement: usize, - /// A reference to the database connection. - /// This is used to execute the statements and check for errors. - connection: &'a Connection, - ///Indicates that the `Statement` struct is tied to the lifetime of the SQLite statement - phantom: PhantomData, -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum StepResult { - Row, - Done, -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum SqlType { - Text, - Integer, - Blob, - Float, - Null, -} - -impl<'a> Statement<'a> { - pub fn prepare>(connection: &'a Connection, query: T) -> Result { - let mut statement = Self { - raw_statements: Default::default(), - current_statement: 0, - connection, - phantom: PhantomData, - }; - let sql = CString::new(query.as_ref()).context("Error creating cstr")?; - let mut remaining_sql = sql.as_c_str(); - while { - let remaining_sql_str = remaining_sql - .to_str() - .context("Parsing remaining sql")? - .trim(); - remaining_sql_str != ";" && !remaining_sql_str.is_empty() - } { - let mut raw_statement = ptr::null_mut::(); - let mut remaining_sql_ptr = ptr::null(); - unsafe { - sqlite3_prepare_v2( - connection.sqlite3, - remaining_sql.as_ptr(), - -1, - &mut raw_statement, - &mut remaining_sql_ptr, - ) - }; - - connection - .last_error() - .with_context(|| format!("Prepare call failed for query:\n{}", query.as_ref()))?; - - remaining_sql = unsafe { CStr::from_ptr(remaining_sql_ptr) }; - statement.raw_statements.push(raw_statement); - - if !connection.can_write() && unsafe { sqlite3_stmt_readonly(raw_statement) == 0 } { - let sql = unsafe { CStr::from_ptr(sqlite3_sql(raw_statement)) }; - - bail!( - "Write statement prepared with connection that is not write capable. SQL:\n{} ", - sql.to_str()? - ) - } - } - - Ok(statement) - } - - fn current_statement(&self) -> *mut sqlite3_stmt { - *self.raw_statements.get(self.current_statement).unwrap() - } - - pub fn reset(&mut self) { - unsafe { - for raw_statement in self.raw_statements.iter() { - sqlite3_reset(*raw_statement); - } - } - self.current_statement = 0; - } - - pub fn parameter_count(&self) -> i32 { - unsafe { - self.raw_statements - .iter() - .map(|raw_statement| sqlite3_bind_parameter_count(*raw_statement)) - .max() - .unwrap_or(0) - } - } - - fn bind_index_with(&self, index: i32, bind: impl Fn(&*mut sqlite3_stmt)) -> Result<()> { - let mut any_succeed = false; - unsafe { - for raw_statement in self.raw_statements.iter() { - if index <= sqlite3_bind_parameter_count(*raw_statement) { - bind(raw_statement); - self.connection - .last_error() - .with_context(|| format!("Failed to bind value at index {index}"))?; - any_succeed = true; - } else { - continue; - } - } - } - if any_succeed { - Ok(()) - } else { - anyhow::bail!("Failed to bind parameters") - } - } - - pub fn bind_blob(&self, index: i32, blob: &[u8]) -> Result<()> { - let index = index as c_int; - let blob_pointer = blob.as_ptr() as *const _; - let len = blob.len() as c_int; - - self.bind_index_with(index, |raw_statement| unsafe { - sqlite3_bind_blob(*raw_statement, index, blob_pointer, len, SQLITE_TRANSIENT()); - }) - } - - pub fn column_blob(&mut self, index: i32) -> Result<&[u8]> { - let index = index as c_int; - let pointer = unsafe { sqlite3_column_blob(self.current_statement(), index) }; - - self.connection - .last_error() - .with_context(|| format!("Failed to read blob at index {index}"))?; - if pointer.is_null() { - return Ok(&[]); - } - let len = unsafe { sqlite3_column_bytes(self.current_statement(), index) as usize }; - self.connection - .last_error() - .with_context(|| format!("Failed to read length of blob at index {index}"))?; - - unsafe { Ok(slice::from_raw_parts(pointer as *const u8, len)) } - } - - pub fn bind_double(&self, index: i32, double: f64) -> Result<()> { - let index = index as c_int; - - self.bind_index_with(index, |raw_statement| unsafe { - sqlite3_bind_double(*raw_statement, index, double); - }) - } - - pub fn column_double(&self, index: i32) -> Result { - let index = index as c_int; - let result = unsafe { sqlite3_column_double(self.current_statement(), index) }; - self.connection - .last_error() - .with_context(|| format!("Failed to read double at index {index}"))?; - Ok(result) - } - - pub fn bind_int(&self, index: i32, int: i32) -> Result<()> { - let index = index as c_int; - self.bind_index_with(index, |raw_statement| unsafe { - sqlite3_bind_int(*raw_statement, index, int); - }) - } - - pub fn column_int(&self, index: i32) -> Result { - let index = index as c_int; - let result = unsafe { sqlite3_column_int(self.current_statement(), index) }; - self.connection - .last_error() - .with_context(|| format!("Failed to read int at index {index}"))?; - Ok(result) - } - - pub fn bind_int64(&self, index: i32, int: i64) -> Result<()> { - let index = index as c_int; - self.bind_index_with(index, |raw_statement| unsafe { - sqlite3_bind_int64(*raw_statement, index, int); - }) - } - - pub fn column_int64(&self, index: i32) -> Result { - let index = index as c_int; - let result = unsafe { sqlite3_column_int64(self.current_statement(), index) }; - self.connection - .last_error() - .with_context(|| format!("Failed to read i64 at index {index}"))?; - Ok(result) - } - - pub fn bind_null(&self, index: i32) -> Result<()> { - let index = index as c_int; - self.bind_index_with(index, |raw_statement| unsafe { - sqlite3_bind_null(*raw_statement, index); - }) - } - - pub fn bind_text(&self, index: i32, text: &str) -> Result<()> { - let index = index as c_int; - let text_pointer = text.as_ptr() as *const _; - let len = text.len() as c_int; - - self.bind_index_with(index, |raw_statement| unsafe { - sqlite3_bind_text(*raw_statement, index, text_pointer, len, SQLITE_TRANSIENT()); - }) - } - - pub fn column_text(&mut self, index: i32) -> Result<&str> { - let index = index as c_int; - let pointer = unsafe { sqlite3_column_text(self.current_statement(), index) }; - - self.connection - .last_error() - .with_context(|| format!("Failed to read text from column {index}"))?; - if pointer.is_null() { - return Ok(""); - } - let len = unsafe { sqlite3_column_bytes(self.current_statement(), index) as usize }; - self.connection - .last_error() - .with_context(|| format!("Failed to read text length at {index}"))?; - - let slice = unsafe { slice::from_raw_parts(pointer, len) }; - Ok(str::from_utf8(slice)?) - } - - pub fn bind(&self, value: &T, index: i32) -> Result { - debug_assert!(index > 0); - value.bind(self, index) - } - - pub fn column(&mut self) -> Result { - Ok(T::column(self, 0)?.0) - } - - pub fn column_type(&mut self, index: i32) -> Result { - let result = unsafe { sqlite3_column_type(self.current_statement(), index) }; - self.connection.last_error()?; - match result { - SQLITE_INTEGER => Ok(SqlType::Integer), - SQLITE_FLOAT => Ok(SqlType::Float), - SQLITE_TEXT => Ok(SqlType::Text), - SQLITE_BLOB => Ok(SqlType::Blob), - SQLITE_NULL => Ok(SqlType::Null), - _ => anyhow::bail!("Column type returned was incorrect"), - } - } - - pub fn with_bindings(&mut self, bindings: &impl Bind) -> Result<&mut Self> { - self.bind(bindings, 1)?; - Ok(self) - } - - fn step(&mut self) -> Result { - match unsafe { sqlite3_step(self.current_statement()) } { - SQLITE_ROW => Ok(StepResult::Row), - SQLITE_DONE => { - if self.current_statement >= self.raw_statements.len() - 1 { - Ok(StepResult::Done) - } else { - self.current_statement += 1; - self.step() - } - } - SQLITE_MISUSE => anyhow::bail!("Statement step returned SQLITE_MISUSE"), - _other_error => { - self.connection.last_error()?; - unreachable!("Step returned error code and last error failed to catch it"); - } - } - } - - pub fn exec(&mut self) -> Result<()> { - fn logic(this: &mut Statement) -> Result<()> { - while this.step()? == StepResult::Row {} - Ok(()) - } - let result = logic(self); - self.reset(); - result - } - - pub fn map(&mut self, callback: impl FnMut(&mut Statement) -> Result) -> Result> { - fn logic( - this: &mut Statement, - mut callback: impl FnMut(&mut Statement) -> Result, - ) -> Result> { - let mut mapped_rows = Vec::new(); - while this.step()? == StepResult::Row { - mapped_rows.push(callback(this)?); - } - Ok(mapped_rows) - } - - let result = logic(self, callback); - self.reset(); - result - } - - pub fn rows(&mut self) -> Result> { - self.map(|s| s.column::()) - } - - pub fn single(&mut self, callback: impl FnOnce(&mut Statement) -> Result) -> Result { - fn logic( - this: &mut Statement, - callback: impl FnOnce(&mut Statement) -> Result, - ) -> Result { - println!("{:?}", std::any::type_name::()); - anyhow::ensure!( - this.step()? == StepResult::Row, - "single called with query that returns no rows." - ); - let result = callback(this)?; - - anyhow::ensure!( - this.step()? == StepResult::Done, - "single called with a query that returns more than one row." - ); - - Ok(result) - } - let result = logic(self, callback); - self.reset(); - result - } - - pub fn row(&mut self) -> Result { - self.single(|this| this.column::()) - } - - pub fn maybe( - &mut self, - callback: impl FnOnce(&mut Statement) -> Result, - ) -> Result> { - fn logic( - this: &mut Statement, - callback: impl FnOnce(&mut Statement) -> Result, - ) -> Result> { - if this.step().context("Failed on step call")? != StepResult::Row { - return Ok(None); - } - - let result = callback(this) - .map(|r| Some(r)) - .context("Failed to parse row result")?; - - anyhow::ensure!( - this.step().context("Second step call")? == StepResult::Done, - "maybe called with a query that returns more than one row." - ); - - Ok(result) - } - let result = logic(self, callback); - self.reset(); - result - } - - pub fn maybe_row(&mut self) -> Result> { - self.maybe(|this| this.column::()) - } -} - -impl Drop for Statement<'_> { - fn drop(&mut self) { - unsafe { - for raw_statement in self.raw_statements.iter() { - sqlite3_finalize(*raw_statement); - } - } - } -} - -#[cfg(test)] -mod test { - use indoc::indoc; - - use crate::{ - connection::Connection, - statement::{Statement, StepResult}, - }; - - #[test] - fn binding_multiple_statements_with_parameter_gaps() { - let connection = - Connection::open_memory(Some("binding_multiple_statements_with_parameter_gaps")); - - connection - .exec(indoc! {" - CREATE TABLE test ( - col INTEGER - )"}) - .unwrap()() - .unwrap(); - - let statement = Statement::prepare( - &connection, - indoc! {" - INSERT INTO test(col) VALUES (?3); - SELECT * FROM test WHERE col = ?1"}, - ) - .unwrap(); - - statement - .bind_int(1, 1) - .expect("Could not bind parameter to first index"); - statement - .bind_int(2, 2) - .expect("Could not bind parameter to second index"); - statement - .bind_int(3, 3) - .expect("Could not bind parameter to third index"); - } - - #[test] - fn blob_round_trips() { - let connection1 = Connection::open_memory(Some("blob_round_trips")); - connection1 - .exec(indoc! {" - CREATE TABLE blobs ( - data BLOB - )"}) - .unwrap()() - .unwrap(); - - let blob = &[0, 1, 2, 4, 8, 16, 32, 64]; - - let mut write = - Statement::prepare(&connection1, "INSERT INTO blobs (data) VALUES (?)").unwrap(); - write.bind_blob(1, blob).unwrap(); - assert_eq!(write.step().unwrap(), StepResult::Done); - - // Read the blob from the - let connection2 = Connection::open_memory(Some("blob_round_trips")); - let mut read = Statement::prepare(&connection2, "SELECT * FROM blobs").unwrap(); - assert_eq!(read.step().unwrap(), StepResult::Row); - assert_eq!(read.column_blob(0).unwrap(), blob); - assert_eq!(read.step().unwrap(), StepResult::Done); - - // Delete the added blob and verify its deleted on the other side - connection2.exec("DELETE FROM blobs").unwrap()().unwrap(); - let mut read = Statement::prepare(&connection1, "SELECT * FROM blobs").unwrap(); - assert_eq!(read.step().unwrap(), StepResult::Done); - } - - #[test] - pub fn maybe_returns_options() { - let connection = Connection::open_memory(Some("maybe_returns_options")); - connection - .exec(indoc! {" - CREATE TABLE texts ( - text TEXT - )"}) - .unwrap()() - .unwrap(); - - assert!( - connection - .select_row::("SELECT text FROM texts") - .unwrap()() - .unwrap() - .is_none() - ); - - let text_to_insert = "This is a test"; - - connection - .exec_bound("INSERT INTO texts VALUES (?)") - .unwrap()(text_to_insert) - .unwrap(); - - assert_eq!( - connection.select_row("SELECT text FROM texts").unwrap()().unwrap(), - Some(text_to_insert.to_string()) - ); - } -} diff --git a/crates/sqlez/src/thread_safe_connection.rs b/crates/sqlez/src/thread_safe_connection.rs deleted file mode 100644 index 966f14a9c2..0000000000 --- a/crates/sqlez/src/thread_safe_connection.rs +++ /dev/null @@ -1,355 +0,0 @@ -use anyhow::Context as _; -use collections::HashMap; -use futures::{Future, FutureExt, channel::oneshot}; -use parking_lot::{Mutex, RwLock}; -use std::{ - marker::PhantomData, - ops::Deref, - sync::{Arc, LazyLock}, - thread, -}; -use thread_local::ThreadLocal; - -use crate::{connection::Connection, domain::Migrator, util::UnboundedSyncSender}; - -const MIGRATION_RETRIES: usize = 10; - -type QueuedWrite = Box; -type WriteQueue = Box; -type WriteQueueConstructor = Box WriteQueue>; - -/// List of queues of tasks by database uri. This lets us serialize writes to the database -/// and have a single worker thread per db file. This means many thread safe connections -/// (possibly with different migrations) could all be communicating with the same background -/// thread. -static QUEUES: LazyLock, WriteQueue>>> = LazyLock::new(Default::default); - -/// Thread safe connection to a given database file or in memory db. This can be cloned, shared, static, -/// whatever. It derefs to a synchronous connection by thread that is read only. A write capable connection -/// may be accessed by passing a callback to the `write` function which will queue the callback -#[derive(Clone)] -pub struct ThreadSafeConnection { - uri: Arc, - persistent: bool, - connection_initialize_query: Option<&'static str>, - connections: Arc>, -} - -unsafe impl Send for ThreadSafeConnection {} -unsafe impl Sync for ThreadSafeConnection {} - -pub struct ThreadSafeConnectionBuilder { - db_initialize_query: Option<&'static str>, - write_queue_constructor: Option, - connection: ThreadSafeConnection, - _migrator: PhantomData<*mut M>, -} - -impl ThreadSafeConnectionBuilder { - /// Sets the query to run every time a connection is opened. This must - /// be infallible (EG only use pragma statements) and not cause writes. - /// to the db or it will panic. - pub fn with_connection_initialize_query(mut self, initialize_query: &'static str) -> Self { - self.connection.connection_initialize_query = Some(initialize_query); - self - } - - /// Queues an initialization query for the database file. This must be infallible - /// but may cause changes to the database file such as with `PRAGMA journal_mode` - pub fn with_db_initialization_query(mut self, initialize_query: &'static str) -> Self { - self.db_initialize_query = Some(initialize_query); - self - } - - /// Specifies how the thread safe connection should serialize writes. If provided - /// the connection will call the write_queue_constructor for each database file in - /// this process. The constructor is responsible for setting up a background thread or - /// async task which handles queued writes with the provided connection. - pub fn with_write_queue_constructor( - mut self, - write_queue_constructor: WriteQueueConstructor, - ) -> Self { - self.write_queue_constructor = Some(write_queue_constructor); - self - } - - pub async fn build(self) -> anyhow::Result { - self.connection - .initialize_queues(self.write_queue_constructor); - - let db_initialize_query = self.db_initialize_query; - - self.connection - .write(move |connection| { - if let Some(db_initialize_query) = db_initialize_query { - connection.exec(db_initialize_query).with_context(|| { - format!( - "Db initialize query failed to execute: {}", - db_initialize_query - ) - })?()?; - } - - // Retry failed migrations in case they were run in parallel from different - // processes. This gives a best attempt at migrating before bailing - let mut migration_result = - anyhow::Result::<()>::Err(anyhow::anyhow!("Migration never run")); - - let foreign_keys_enabled: bool = - connection.select_row::("PRAGMA foreign_keys")?() - .unwrap_or(None) - .map(|enabled| enabled != 0) - .unwrap_or(false); - - connection.exec("PRAGMA foreign_keys = OFF;")?()?; - - for _ in 0..MIGRATION_RETRIES { - migration_result = connection - .with_savepoint("thread_safe_multi_migration", || M::migrate(connection)); - - if migration_result.is_ok() { - break; - } - } - - if foreign_keys_enabled { - connection.exec("PRAGMA foreign_keys = ON;")?()?; - } - migration_result - }) - .await?; - - Ok(self.connection) - } -} - -impl ThreadSafeConnection { - fn initialize_queues(&self, write_queue_constructor: Option) -> bool { - if !QUEUES.read().contains_key(&self.uri) { - let mut queues = QUEUES.write(); - if !queues.contains_key(&self.uri) { - let mut write_queue_constructor = - write_queue_constructor.unwrap_or_else(background_thread_queue); - queues.insert(self.uri.clone(), write_queue_constructor()); - return true; - } - } - false - } - - pub fn builder(uri: &str, persistent: bool) -> ThreadSafeConnectionBuilder { - ThreadSafeConnectionBuilder:: { - db_initialize_query: None, - write_queue_constructor: None, - connection: Self { - uri: Arc::from(uri), - persistent, - connection_initialize_query: None, - connections: Default::default(), - }, - _migrator: PhantomData, - } - } - - /// Opens a new db connection with the initialized file path. This is internal and only - /// called from the deref function. - fn open_file(uri: &str) -> Connection { - Connection::open_file(uri) - } - - /// Opens a shared memory connection using the file path as the identifier. This is internal - /// and only called from the deref function. - fn open_shared_memory(uri: &str) -> Connection { - Connection::open_memory(Some(uri)) - } - - pub fn write( - &self, - callback: impl 'static + Send + FnOnce(&Connection) -> T, - ) -> impl Future { - // Check and invalidate queue and maybe recreate queue - let queues = QUEUES.read(); - let write_channel = queues - .get(&self.uri) - .expect("Queues are inserted when build is called. This should always succeed"); - - // Create a one shot channel for the result of the queued write - // so we can await on the result - let (sender, receiver) = oneshot::channel(); - - let thread_safe_connection = (*self).clone(); - write_channel(Box::new(move || { - let connection = thread_safe_connection.deref(); - let result = connection.with_write(|connection| callback(connection)); - sender.send(result).ok(); - })); - receiver.map(|response| response.expect("Write queue unexpectedly closed")) - } - - pub(crate) fn create_connection( - persistent: bool, - uri: &str, - connection_initialize_query: Option<&'static str>, - ) -> Connection { - let mut connection = if persistent { - Self::open_file(uri) - } else { - Self::open_shared_memory(uri) - }; - - // Disallow writes on the connection. The only writes allowed for thread safe connections - // are from the background thread that can serialize them. - *connection.write.get_mut() = false; - - if let Some(initialize_query) = connection_initialize_query { - connection.exec(initialize_query).unwrap_or_else(|_| { - panic!("Initialize query failed to execute: {}", initialize_query) - })() - .unwrap() - } - - connection - } -} - -impl ThreadSafeConnection { - /// Special constructor for ThreadSafeConnection which disallows db initialization and migrations. - /// This allows construction to be infallible and not write to the db. - pub fn new( - uri: &str, - persistent: bool, - connection_initialize_query: Option<&'static str>, - write_queue_constructor: Option, - ) -> Self { - let connection = Self { - uri: Arc::from(uri), - persistent, - connection_initialize_query, - connections: Default::default(), - }; - - connection.initialize_queues(write_queue_constructor); - connection - } -} - -impl Deref for ThreadSafeConnection { - type Target = Connection; - - fn deref(&self) -> &Self::Target { - self.connections.get_or(|| { - Self::create_connection(self.persistent, &self.uri, self.connection_initialize_query) - }) - } -} - -pub fn background_thread_queue() -> WriteQueueConstructor { - use std::sync::mpsc::channel; - - Box::new(|| { - let (sender, receiver) = channel::(); - - thread::Builder::new() - .name("sqlezWorker".to_string()) - .spawn(move || { - while let Ok(write) = receiver.recv() { - write() - } - }) - .unwrap(); - - let sender = UnboundedSyncSender::new(sender); - Box::new(move |queued_write| { - sender - .send(queued_write) - .expect("Could not send write action to background thread"); - }) - }) -} - -pub fn locking_queue() -> WriteQueueConstructor { - Box::new(|| { - let write_mutex = Mutex::new(()); - Box::new(move |queued_write| { - let _lock = write_mutex.lock(); - queued_write(); - }) - }) -} - -#[cfg(test)] -mod test { - use indoc::indoc; - use std::ops::Deref; - - use std::thread; - - use crate::{domain::Domain, thread_safe_connection::ThreadSafeConnection}; - - #[test] - fn many_initialize_and_migrate_queries_at_once() { - let mut handles = vec![]; - - enum TestDomain {} - impl Domain for TestDomain { - const NAME: &str = "test"; - const MIGRATIONS: &[&str] = &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"]; - } - - for _ in 0..100 { - handles.push(thread::spawn(|| { - let builder = - ThreadSafeConnection::builder::("annoying-test.db", false) - .with_db_initialization_query("PRAGMA journal_mode=WAL") - .with_connection_initialize_query(indoc! {" - PRAGMA synchronous=NORMAL; - PRAGMA busy_timeout=1; - PRAGMA foreign_keys=TRUE; - PRAGMA case_sensitive_like=TRUE; - "}); - - let _ = smol::block_on(builder.build()).unwrap().deref(); - })); - } - - for handle in handles { - let _ = handle.join(); - } - } - - #[test] - #[should_panic] - fn wild_zed_lost_failure() { - enum TestWorkspace {} - impl Domain for TestWorkspace { - const NAME: &str = "workspace"; - - const MIGRATIONS: &[&str] = &[" - CREATE TABLE workspaces( - workspace_id INTEGER PRIMARY KEY, - dock_visible INTEGER, -- Boolean - dock_anchor TEXT, -- Enum: 'Bottom' / 'Right' / 'Expanded' - dock_pane INTEGER, -- NULL indicates that we don't have a dock pane yet - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - FOREIGN KEY(dock_pane) REFERENCES panes(pane_id), - FOREIGN KEY(active_pane) REFERENCES panes(pane_id) - ) STRICT; - - CREATE TABLE panes( - pane_id INTEGER PRIMARY KEY, - workspace_id INTEGER NOT NULL, - active INTEGER NOT NULL, -- Boolean - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE - ) STRICT; - "]; - } - - let builder = - ThreadSafeConnection::builder::("wild_zed_lost_failure", false) - .with_connection_initialize_query("PRAGMA FOREIGN_KEYS=true"); - - smol::block_on(builder.build()).unwrap(); - } -} diff --git a/crates/sqlez/src/typed_statements.rs b/crates/sqlez/src/typed_statements.rs deleted file mode 100644 index e300a6e7d5..0000000000 --- a/crates/sqlez/src/typed_statements.rs +++ /dev/null @@ -1,96 +0,0 @@ -use anyhow::{Context as _, Result}; - -use crate::{ - bindable::{Bind, Column}, - connection::Connection, - statement::Statement, -}; - -impl Connection { - /// Prepare a statement which has no bindings and returns nothing. - /// - /// Note: If there are multiple statements that depend upon each other - /// (such as those which make schema changes), preparation will fail. - /// Use a true migration instead. - pub fn exec<'a>(&'a self, query: &str) -> Result Result<()>> { - let mut statement = Statement::prepare(self, query)?; - Ok(move || statement.exec()) - } - - /// Prepare a statement which takes a binding, but returns nothing. - /// The bindings for a given invocation should be passed to the returned - /// closure - /// - /// Note: If there are multiple statements that depend upon each other - /// (such as those which make schema changes), preparation will fail. - /// Use a true migration instead. - pub fn exec_bound<'a, B: Bind>( - &'a self, - query: &str, - ) -> Result Result<()>> { - let mut statement = Statement::prepare(self, query)?; - Ok(move |bindings| statement.with_bindings(&bindings)?.exec()) - } - - /// Prepare a statement which has no bindings and returns a `Vec`. - /// - /// Note: If there are multiple statements that depend upon each other - /// (such as those which make schema changes), preparation will fail. - /// Use a true migration instead. - pub fn select<'a, C: Column>( - &'a self, - query: &str, - ) -> Result Result>> { - let mut statement = Statement::prepare(self, query)?; - Ok(move || statement.rows::()) - } - - /// Prepare a statement which takes a binding and returns a `Vec`. - /// - /// Note: If there are multiple statements that depend upon each other - /// (such as those which make schema changes), preparation will fail. - /// Use a true migration instead. - pub fn select_bound<'a, B: Bind, C: Column>( - &'a self, - query: &str, - ) -> Result Result>> { - let mut statement = Statement::prepare(self, query)?; - Ok(move |bindings| statement.with_bindings(&bindings)?.rows::()) - } - - /// Prepare a statement that selects a single row from the database. - /// Will return none if no rows are returned and will error if more than - /// 1 row - /// - /// Note: If there are multiple statements that depend upon each other - /// (such as those which make schema changes), preparation will fail. - /// Use a true migration instead. - pub fn select_row<'a, C: Column>( - &'a self, - query: &str, - ) -> Result Result>> { - let mut statement = Statement::prepare(self, query)?; - Ok(move || statement.maybe_row::()) - } - - /// Prepare a statement which takes a binding and selects a single row - /// from the database. Will return none if no rows are returned and will - /// error if more than 1 row is returned. - /// - /// Note: If there are multiple statements that depend upon each other - /// (such as those which make schema changes), preparation will fail. - /// Use a true migration instead. - pub fn select_row_bound<'a, B: Bind, C: Column>( - &'a self, - query: &str, - ) -> Result Result>> { - let mut statement = Statement::prepare(self, query)?; - Ok(move |bindings| { - statement - .with_bindings(&bindings) - .context("Bindings failed")? - .maybe_row::() - .context("Maybe row failed") - }) - } -} diff --git a/crates/sqlez/src/util.rs b/crates/sqlez/src/util.rs deleted file mode 100644 index 8be6d3f4e0..0000000000 --- a/crates/sqlez/src/util.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::ops::Deref; -use std::sync::mpsc::Sender; - -use parking_lot::Mutex; -use thread_local::ThreadLocal; - -/// Unbounded standard library sender which is stored per thread to get around -/// the lack of sync on the standard library version while still being unbounded -/// Note: this locks on the cloneable sender, but its done once per thread, so it -/// shouldn't result in too much contention -pub struct UnboundedSyncSender { - cloneable_sender: Mutex>, - local_senders: ThreadLocal>, -} - -impl UnboundedSyncSender { - pub fn new(sender: Sender) -> Self { - Self { - cloneable_sender: Mutex::new(sender), - local_senders: ThreadLocal::new(), - } - } -} - -impl Deref for UnboundedSyncSender { - type Target = Sender; - - fn deref(&self) -> &Self::Target { - self.local_senders - .get_or(|| self.cloneable_sender.lock().clone()) - } -} diff --git a/crates/sqlez_macros/Cargo.toml b/crates/sqlez_macros/Cargo.toml deleted file mode 100644 index cff96d0b89..0000000000 --- a/crates/sqlez_macros/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "sqlez_macros" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/sqlez_macros.rs" -proc-macro = true -doctest = false - -[dependencies] -sqlez.workspace = true -sqlformat.workspace = true -syn.workspace = true diff --git a/crates/sqlez_macros/LICENSE-GPL b/crates/sqlez_macros/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/sqlez_macros/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/sqlez_macros/src/sqlez_macros.rs b/crates/sqlez_macros/src/sqlez_macros.rs deleted file mode 100644 index 5ddfe4cd87..0000000000 --- a/crates/sqlez_macros/src/sqlez_macros.rs +++ /dev/null @@ -1,102 +0,0 @@ -use proc_macro::{Delimiter, Span, TokenStream, TokenTree}; -use syn::Error; - -#[cfg(not(any(target_os = "linux", target_os = "freebsd")))] -static SQLITE: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - sqlez::thread_safe_connection::ThreadSafeConnection::new( - ":memory:", - false, - None, - Some(sqlez::thread_safe_connection::locking_queue()), - ) - }); - -#[proc_macro] -pub fn sql(tokens: TokenStream) -> TokenStream { - let (spans, sql) = make_sql(tokens); - - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - let error = SQLITE.sql_has_syntax_error(sql.trim()); - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - let error: Option<(String, usize)> = None; - - let formatted_sql = sqlformat::format(&sql, &sqlformat::QueryParams::None, Default::default()); - - if let Some((error, error_offset)) = error { - create_error(spans, error_offset, error, &formatted_sql) - } else { - format!("r#\"{}\"#", &formatted_sql).parse().unwrap() - } -} - -fn create_error( - spans: Vec<(usize, Span)>, - error_offset: usize, - error: String, - formatted_sql: &String, -) -> TokenStream { - let error_span = spans - .into_iter() - .skip_while(|(offset, _)| offset <= &error_offset) - .map(|(_, span)| span) - .next() - .unwrap_or_else(Span::call_site); - let error_text = format!("Sql Error: {}\nFor Query: {}", error, formatted_sql); - TokenStream::from(Error::new(error_span.into(), error_text).into_compile_error()) -} - -fn make_sql(tokens: TokenStream) -> (Vec<(usize, Span)>, String) { - let mut sql_tokens = vec![]; - flatten_stream(tokens, &mut sql_tokens); - // Lookup of spans by offset at the end of the token - let mut spans: Vec<(usize, Span)> = Vec::new(); - let mut sql = String::new(); - for (token_text, span) in sql_tokens { - sql.push_str(&token_text); - spans.push((sql.len(), span)); - } - (spans, sql) -} - -/// This method exists to normalize the representation of groups -/// to always include spaces between tokens. This is why we don't use the usual .to_string(). -/// This allows our token search in token_at_offset to resolve -/// ambiguity of '(tokens)' vs. '( token )', due to sqlite requiring byte offsets -fn flatten_stream(tokens: TokenStream, result: &mut Vec<(String, Span)>) { - for token_tree in tokens.into_iter() { - match token_tree { - TokenTree::Group(group) => { - // push open delimiter - result.push((open_delimiter(group.delimiter()), group.span())); - // recurse - flatten_stream(group.stream(), result); - // push close delimiter - result.push((close_delimiter(group.delimiter()), group.span())); - } - TokenTree::Ident(ident) => { - result.push((format!("{} ", ident), ident.span())); - } - leaf_tree => result.push((leaf_tree.to_string(), leaf_tree.span())), - } - } -} - -fn open_delimiter(delimiter: Delimiter) -> String { - match delimiter { - Delimiter::Parenthesis => "( ".to_string(), - Delimiter::Brace => "[ ".to_string(), - Delimiter::Bracket => "{ ".to_string(), - Delimiter::None => "".to_string(), - } -} - -fn close_delimiter(delimiter: Delimiter) -> String { - match delimiter { - Delimiter::Parenthesis => " ) ".to_string(), - Delimiter::Brace => " ] ".to_string(), - Delimiter::Bracket => " } ".to_string(), - Delimiter::None => "".to_string(), - } -} diff --git a/crates/story/Cargo.toml b/crates/story/Cargo.toml deleted file mode 100644 index 798461402d..0000000000 --- a/crates/story/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "story" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lib] -path = "src/story.rs" - -[lints] -workspace = true - -[dependencies] -gpui.workspace = true -itertools.workspace = true -smallvec.workspace = true diff --git a/crates/story/LICENSE-GPL b/crates/story/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/story/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/story/src/story.rs b/crates/story/src/story.rs deleted file mode 100644 index b59cb6fb99..0000000000 --- a/crates/story/src/story.rs +++ /dev/null @@ -1,209 +0,0 @@ -use gpui::{ - AnyElement, App, Div, SharedString, Window, colors::DefaultColors, div, prelude::*, px, rems, -}; -use itertools::Itertools; -use smallvec::SmallVec; - -pub struct Story {} - -impl Story { - pub fn container(cx: &App) -> gpui::Stateful
{ - div() - .id("story_container") - .overflow_y_scroll() - .w_full() - .min_h_full() - .flex() - .flex_col() - .text_color(cx.default_colors().text) - .bg(cx.default_colors().background) - } - - pub fn title(title: impl Into, cx: &App) -> impl Element { - div() - .text_xs() - .text_color(cx.default_colors().text) - .child(title.into()) - } - - pub fn title_for(cx: &App) -> impl Element { - Self::title(std::any::type_name::(), cx) - } - - pub fn section(cx: &App) -> Div { - div() - .p_4() - .m_4() - .border_1() - .border_color(cx.default_colors().separator) - } - - pub fn section_title(cx: &App) -> Div { - div().text_lg().text_color(cx.default_colors().text) - } - - pub fn group(cx: &App) -> Div { - div().my_2().bg(cx.default_colors().container) - } - - pub fn code_block(code: impl Into, cx: &App) -> Div { - div() - .size_full() - .p_2() - .max_w(rems(36.)) - .bg(cx.default_colors().container) - .rounded_sm() - .text_sm() - .text_color(cx.default_colors().text) - .overflow_hidden() - .child(code.into()) - } - - pub fn divider(cx: &App) -> Div { - div().my_2().h(px(1.)).bg(cx.default_colors().separator) - } - - pub fn description(description: impl Into, cx: &App) -> impl Element { - div() - .text_sm() - .text_color(cx.default_colors().text) - .min_w_96() - .child(description.into()) - } - - pub fn label(label: impl Into, cx: &App) -> impl Element { - div() - .text_xs() - .text_color(cx.default_colors().text) - .child(label.into()) - } - - /// Note: Not `ui::v_flex` as the `story` crate doesn't depend on the `ui` crate. - pub fn v_flex() -> Div { - div().flex().flex_col().gap_1() - } -} - -#[derive(IntoElement)] -pub struct StoryItem { - label: SharedString, - item: AnyElement, - description: Option, - usage: Option, -} - -impl StoryItem { - pub fn new(label: impl Into, item: impl IntoElement) -> Self { - Self { - label: label.into(), - item: item.into_any_element(), - description: None, - usage: None, - } - } - - pub fn description(mut self, description: impl Into) -> Self { - self.description = Some(description.into()); - self - } - - pub fn usage(mut self, code: impl Into) -> Self { - self.usage = Some(code.into()); - self - } -} - -impl RenderOnce for StoryItem { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let colors = cx.default_colors(); - - div() - .my_2() - .flex() - .gap_4() - .w_full() - .child( - Story::v_flex() - .px_2() - .w_1_2() - .min_h_px() - .child(Story::label(self.label, cx)) - .child( - div() - .rounded_sm() - .bg(colors.background) - .border_1() - .border_color(colors.border) - .py_1() - .px_2() - .overflow_hidden() - .child(self.item), - ) - .when_some(self.description, |this, description| { - this.child(Story::description(description, cx)) - }), - ) - .child( - Story::v_flex() - .px_2() - .flex_none() - .w_1_2() - .min_h_px() - .when_some(self.usage, |this, usage| { - this.child(Story::label("Example Usage", cx)) - .child(Story::code_block(usage, cx)) - }), - ) - } -} - -#[derive(IntoElement)] -pub struct StorySection { - description: Option, - children: SmallVec<[AnyElement; 2]>, -} - -impl Default for StorySection { - fn default() -> Self { - Self::new() - } -} - -impl StorySection { - pub fn new() -> Self { - Self { - description: None, - children: SmallVec::new(), - } - } - - pub fn description(mut self, description: impl Into) -> Self { - self.description = Some(description.into()); - self - } -} - -impl RenderOnce for StorySection { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let children: SmallVec<[AnyElement; 2]> = SmallVec::from_iter(Itertools::intersperse_with( - self.children.into_iter(), - || Story::divider(cx).into_any_element(), - )); - - Story::section(cx) - // Section title - .py_2() - // Section description - .when_some(self.description, |section, description| { - section.child(Story::description(description, cx)) - }) - .child(div().flex().flex_col().gap_2().children(children)) - .child(Story::divider(cx)) - } -} - -impl ParentElement for StorySection { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} diff --git a/crates/storybook/Cargo.toml b/crates/storybook/Cargo.toml deleted file mode 100644 index 148f036134..0000000000 --- a/crates/storybook/Cargo.toml +++ /dev/null @@ -1,40 +0,0 @@ -[package] -name = "storybook" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[[bin]] -name = "storybook" -path = "src/storybook.rs" - -[dependencies] -anyhow.workspace = true -clap = { workspace = true, features = ["derive", "string"] } -collab_ui = { workspace = true, features = ["stories"] } -ctrlc = "3.4" -dialoguer = { version = "0.11.0", features = ["fuzzy-select"] } -editor.workspace = true -fuzzy.workspace = true -gpui = { workspace = true, default-features = true } -indoc.workspace = true -language.workspace = true -log.workspace = true -menu.workspace = true -picker.workspace = true -reqwest_client.workspace = true -rust-embed.workspace = true -settings.workspace = true -simplelog.workspace = true -story.workspace = true -strum = { workspace = true, features = ["derive"] } -theme.workspace = true -title_bar = { workspace = true, features = ["stories"] } -ui = { workspace = true, features = ["stories"] } - -[dev-dependencies] -gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/storybook/LICENSE-GPL b/crates/storybook/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/storybook/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/storybook/build.rs b/crates/storybook/build.rs deleted file mode 100644 index 66791cae42..0000000000 --- a/crates/storybook/build.rs +++ /dev/null @@ -1,9 +0,0 @@ -fn main() { - #[cfg(target_os = "windows")] - { - #[cfg(target_env = "msvc")] - { - println!("cargo:rustc-link-arg=/stack:{}", 8 * 1024 * 1024); - } - } -} diff --git a/crates/storybook/docs/thoughts.md b/crates/storybook/docs/thoughts.md deleted file mode 100644 index cdeef621f3..0000000000 --- a/crates/storybook/docs/thoughts.md +++ /dev/null @@ -1,57 +0,0 @@ -Much of element styling is now handled by an external engine. - -How do I make an element hover. - -There's a hover style. - -Hoverable needs to wrap another element. That element can be styled. - -```rs -struct Hoverable { - -} - -impl Element for Hoverable { - -} -``` - -```rs -#[derive(Styled, Interactive)] -pub struct Div { - declared_style: StyleRefinement, - interactions: Interactions -} - -pub trait Styled { - fn declared_style(&mut self) -> &mut StyleRefinement; - fn compute_style(&mut self) -> Style { - Style::default().refine(self.declared_style()) - } - - // All the tailwind classes, modifying self.declared_style() -} - -impl Style { - pub fn paint_background(layout: Layout, cx: &mut PaintContext); - pub fn paint_foreground(layout: Layout, cx: &mut PaintContext); -} - -pub trait Interactive { - fn interactions(&mut self) -> &mut Interactions; - - fn on_click(self, ) -} - -struct Interactions { - click: SmallVec<[; 1]>, -} -``` - -```rs -trait Stylable { - type Style; - - fn with_style(self, style: Self::Style) -> Self; -} -``` diff --git a/crates/storybook/src/actions.rs b/crates/storybook/src/actions.rs deleted file mode 100644 index 03ee5b580c..0000000000 --- a/crates/storybook/src/actions.rs +++ /dev/null @@ -1,2 +0,0 @@ -use gpui::actions; -actions!(storybook, [Quit]); diff --git a/crates/storybook/src/app_menus.rs b/crates/storybook/src/app_menus.rs deleted file mode 100644 index 4e84b4c85d..0000000000 --- a/crates/storybook/src/app_menus.rs +++ /dev/null @@ -1,10 +0,0 @@ -use gpui::{Menu, MenuItem}; - -pub fn app_menus() -> Vec { - use crate::actions::Quit; - - vec![Menu { - name: "Storybook".into(), - items: vec![MenuItem::action("Quit", Quit)], - }] -} diff --git a/crates/storybook/src/assets.rs b/crates/storybook/src/assets.rs deleted file mode 100644 index 4da4081212..0000000000 --- a/crates/storybook/src/assets.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::borrow::Cow; - -use anyhow::{Context as _, Result}; -use gpui::{AssetSource, SharedString}; -use rust_embed::RustEmbed; - -#[derive(RustEmbed)] -#[folder = "../../assets"] -#[include = "fonts/**/*"] -#[include = "icons/**/*"] -#[include = "images/**/*"] -#[include = "themes/**/*"] -#[include = "sounds/**/*"] -#[include = "*.md"] -#[exclude = "*.DS_Store"] -pub struct Assets; - -impl AssetSource for Assets { - fn load(&self, path: &str) -> Result>> { - Self::get(path) - .map(|f| f.data) - .with_context(|| format!("could not find asset at path {path:?}")) - .map(Some) - } - - fn list(&self, path: &str) -> Result> { - Ok(Self::iter() - .filter(|p| p.starts_with(path)) - .map(SharedString::from) - .collect()) - } -} diff --git a/crates/storybook/src/stories.rs b/crates/storybook/src/stories.rs deleted file mode 100644 index 63992d259c..0000000000 --- a/crates/storybook/src/stories.rs +++ /dev/null @@ -1,23 +0,0 @@ -mod auto_height_editor; -mod cursor; -mod focus; -mod indent_guides; -mod kitchen_sink; -mod overflow_scroll; -mod picker; -mod scroll; -mod text; -mod viewport_units; -mod with_rem_size; - -pub use auto_height_editor::*; -pub use cursor::*; -pub use focus::*; -pub use indent_guides::*; -pub use kitchen_sink::*; -pub use overflow_scroll::*; -pub use picker::*; -pub use scroll::*; -pub use text::*; -pub use viewport_units::*; -pub use with_rem_size::*; diff --git a/crates/storybook/src/stories/auto_height_editor.rs b/crates/storybook/src/stories/auto_height_editor.rs deleted file mode 100644 index 702d5774f2..0000000000 --- a/crates/storybook/src/stories/auto_height_editor.rs +++ /dev/null @@ -1,36 +0,0 @@ -use editor::Editor; -use gpui::{ - App, AppContext as _, Context, Entity, IntoElement, KeyBinding, ParentElement, Render, Styled, - Window, div, white, -}; - -pub struct AutoHeightEditorStory { - editor: Entity, -} - -impl AutoHeightEditorStory { - pub fn new(window: &mut Window, cx: &mut App) -> gpui::Entity { - cx.bind_keys([KeyBinding::new( - "enter", - editor::actions::Newline, - Some("Editor"), - )]); - cx.new(|cx| Self { - editor: cx.new(|cx| { - let mut editor = Editor::auto_height(1, 3, window, cx); - editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); - editor - }), - }) - } -} - -impl Render for AutoHeightEditorStory { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .size_full() - .bg(white()) - .text_sm() - .child(div().w_32().bg(gpui::black()).child(self.editor.clone())) - } -} diff --git a/crates/storybook/src/stories/cursor.rs b/crates/storybook/src/stories/cursor.rs deleted file mode 100644 index 00bae99917..0000000000 --- a/crates/storybook/src/stories/cursor.rs +++ /dev/null @@ -1,109 +0,0 @@ -use gpui::{Div, Render, Stateful}; -use story::Story; -use ui::prelude::*; - -pub struct CursorStory; - -impl Render for CursorStory { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let all_cursors: [(&str, Box) -> Stateful
>); 19] = [ - ( - "cursor_default", - Box::new(|el: Stateful
| el.cursor_default()), - ), - ( - "cursor_pointer", - Box::new(|el: Stateful
| el.cursor_pointer()), - ), - ( - "cursor_text", - Box::new(|el: Stateful
| el.cursor_text()), - ), - ( - "cursor_move", - Box::new(|el: Stateful
| el.cursor_move()), - ), - ( - "cursor_not_allowed", - Box::new(|el: Stateful
| el.cursor_not_allowed()), - ), - ( - "cursor_context_menu", - Box::new(|el: Stateful
| el.cursor_context_menu()), - ), - ( - "cursor_crosshair", - Box::new(|el: Stateful
| el.cursor_crosshair()), - ), - ( - "cursor_vertical_text", - Box::new(|el: Stateful
| el.cursor_vertical_text()), - ), - ( - "cursor_alias", - Box::new(|el: Stateful
| el.cursor_alias()), - ), - ( - "cursor_copy", - Box::new(|el: Stateful
| el.cursor_copy()), - ), - ( - "cursor_no_drop", - Box::new(|el: Stateful
| el.cursor_no_drop()), - ), - ( - "cursor_grab", - Box::new(|el: Stateful
| el.cursor_grab()), - ), - ( - "cursor_grabbing", - Box::new(|el: Stateful
| el.cursor_grabbing()), - ), - ( - "cursor_col_resize", - Box::new(|el: Stateful
| el.cursor_col_resize()), - ), - ( - "cursor_row_resize", - Box::new(|el: Stateful
| el.cursor_row_resize()), - ), - ( - "cursor_n_resize", - Box::new(|el: Stateful
| el.cursor_n_resize()), - ), - ( - "cursor_e_resize", - Box::new(|el: Stateful
| el.cursor_e_resize()), - ), - ( - "cursor_s_resize", - Box::new(|el: Stateful
| el.cursor_s_resize()), - ), - ( - "cursor_w_resize", - Box::new(|el: Stateful
| el.cursor_w_resize()), - ), - ]; - - Story::container(cx) - .flex() - .gap_1() - .child(Story::title("cursor", cx)) - .children(all_cursors.map(|(name, apply_cursor)| { - div().gap_1().flex().text_color(gpui::white()).child( - div() - .flex() - .items_center() - .justify_center() - .id(name) - .map(apply_cursor) - .w_64() - .h_8() - .bg(gpui::red()) - .active(|style| style.bg(gpui::green())) - .text_sm() - .child(Story::label(name, cx)), - ) - })) - } -} diff --git a/crates/storybook/src/stories/focus.rs b/crates/storybook/src/stories/focus.rs deleted file mode 100644 index a64c272ba7..0000000000 --- a/crates/storybook/src/stories/focus.rs +++ /dev/null @@ -1,123 +0,0 @@ -use gpui::{ - App, Entity, FocusHandle, KeyBinding, Render, Subscription, Window, actions, div, prelude::*, -}; -use ui::prelude::*; - -actions!(focus, [ActionA, ActionB, ActionC]); - -pub struct FocusStory { - parent_focus: FocusHandle, - child_1_focus: FocusHandle, - child_2_focus: FocusHandle, - _focus_subscriptions: Vec, -} - -impl FocusStory { - pub fn model(window: &mut Window, cx: &mut App) -> Entity { - cx.bind_keys([ - KeyBinding::new("cmd-a", ActionA, Some("parent")), - KeyBinding::new("cmd-a", ActionB, Some("child-1")), - KeyBinding::new("cmd-c", ActionC, None), - ]); - - cx.new(|cx| { - let parent_focus = cx.focus_handle(); - let child_1_focus = cx.focus_handle(); - let child_2_focus = cx.focus_handle(); - let _focus_subscriptions = vec![ - cx.on_focus(&parent_focus, window, |_, _, _| { - println!("Parent focused"); - }), - cx.on_blur(&parent_focus, window, |_, _, _| { - println!("Parent blurred"); - }), - cx.on_focus(&child_1_focus, window, |_, _, _| { - println!("Child 1 focused"); - }), - cx.on_blur(&child_1_focus, window, |_, _, _| { - println!("Child 1 blurred"); - }), - cx.on_focus(&child_2_focus, window, |_, _, _| { - println!("Child 2 focused"); - }), - cx.on_blur(&child_2_focus, window, |_, _, _| { - println!("Child 2 blurred"); - }), - ]; - - Self { - parent_focus, - child_1_focus, - child_2_focus, - _focus_subscriptions, - } - }) - } -} - -impl Render for FocusStory { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let theme = cx.theme(); - let color_1 = theme.status().created; - let color_2 = theme.status().modified; - let color_4 = theme.status().conflict; - let color_5 = theme.status().ignored; - let color_6 = theme.status().renamed; - let color_7 = theme.status().hint; - - div() - .id("parent") - .active(|style| style.bg(color_7)) - .track_focus(&self.parent_focus) - .key_context("parent") - .on_action(cx.listener(|_, _action: &ActionA, _window, _cx| { - println!("Action A dispatched on parent"); - })) - .on_action(cx.listener(|_, _action: &ActionB, _window, _cx| { - println!("Action B dispatched on parent"); - })) - .on_key_down(cx.listener(|_, event, _, _| println!("Key down on parent {:?}", event))) - .on_key_up(cx.listener(|_, event, _, _| println!("Key up on parent {:?}", event))) - .size_full() - .bg(color_1) - .focus(|style| style.bg(color_2)) - .child( - div() - .track_focus(&self.child_1_focus) - .key_context("child-1") - .on_action(cx.listener(|_, _action: &ActionB, _window, _cx| { - println!("Action B dispatched on child 1 during"); - })) - .w_full() - .h_6() - .bg(color_4) - .focus(|style| style.bg(color_5)) - .in_focus(|style| style.bg(color_6)) - .on_key_down( - cx.listener(|_, event, _, _| println!("Key down on child 1 {:?}", event)), - ) - .on_key_up( - cx.listener(|_, event, _, _| println!("Key up on child 1 {:?}", event)), - ) - .child("Child 1"), - ) - .child( - div() - .track_focus(&self.child_2_focus) - .key_context("child-2") - .on_action(cx.listener(|_, _action: &ActionC, _window, _cx| { - println!("Action C dispatched on child 2"); - })) - .w_full() - .h_6() - .bg(color_4) - .on_key_down( - cx.listener(|_, event, _, _| println!("Key down on child 2 {:?}", event)), - ) - .on_key_up( - cx.listener(|_, event, _, _| println!("Key up on child 2 {:?}", event)), - ) - .child("Child 2"), - ) - } -} diff --git a/crates/storybook/src/stories/indent_guides.rs b/crates/storybook/src/stories/indent_guides.rs deleted file mode 100644 index db23ea79bd..0000000000 --- a/crates/storybook/src/stories/indent_guides.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::ops::Range; - -use gpui::{Entity, Render, div, uniform_list}; -use gpui::{prelude::*, *}; -use ui::{AbsoluteLength, Color, DefiniteLength, Label, LabelCommon, px, v_flex}; - -use story::Story; - -const LENGTH: usize = 100; - -pub struct IndentGuidesStory { - depths: Vec, -} - -impl IndentGuidesStory { - pub fn model(_window: &mut Window, cx: &mut App) -> Entity { - let mut depths = Vec::new(); - depths.push(0); - depths.push(1); - depths.push(2); - for _ in 0..LENGTH - 6 { - depths.push(3); - } - depths.push(2); - depths.push(1); - depths.push(0); - - cx.new(|_cx| Self { depths }) - } -} - -impl Render for IndentGuidesStory { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - Story::container(cx) - .child(Story::title("Indent guides", cx)) - .child( - v_flex().size_full().child( - uniform_list( - "some-list", - self.depths.len(), - cx.processor(move |this, range: Range, _window, _cx| { - this.depths - .iter() - .enumerate() - .skip(range.start) - .take(range.end - range.start) - .map(|(i, depth)| { - div() - .pl(DefiniteLength::Absolute(AbsoluteLength::Pixels(px( - 16. * (*depth as f32), - )))) - .child(Label::new(format!("Item {}", i)).color(Color::Info)) - }) - .collect() - }), - ) - .with_sizing_behavior(gpui::ListSizingBehavior::Infer) - .with_decoration( - ui::indent_guides( - px(16.), - ui::IndentGuideColors { - default: Color::Info.color(cx), - hover: Color::Accent.color(cx), - active: Color::Accent.color(cx), - }, - ) - .with_compute_indents_fn( - cx.entity(), - |this, range, _cx, _context| { - this.depths - .iter() - .skip(range.start) - .take(range.end - range.start) - .cloned() - .collect() - }, - ), - ), - ), - ) - } -} diff --git a/crates/storybook/src/stories/kitchen_sink.rs b/crates/storybook/src/stories/kitchen_sink.rs deleted file mode 100644 index aaddf733f8..0000000000 --- a/crates/storybook/src/stories/kitchen_sink.rs +++ /dev/null @@ -1,32 +0,0 @@ -use gpui::{Entity, Render, prelude::*}; -use story::Story; -use strum::IntoEnumIterator; -use ui::prelude::*; - -use crate::story_selector::ComponentStory; - -pub struct KitchenSinkStory; - -impl KitchenSinkStory { - pub fn model(cx: &mut App) -> Entity { - cx.new(|_| Self) - } -} - -impl Render for KitchenSinkStory { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let component_stories = ComponentStory::iter() - .map(|selector| selector.story(window, cx)) - .collect::>(); - - Story::container(cx) - .id("kitchen-sink") - .overflow_y_scroll() - .child(Story::title("Kitchen Sink", cx)) - .child(Story::label("Components", cx)) - .child(div().flex().flex_col().children(component_stories)) - // Add a bit of space at the bottom of the kitchen sink so elements - // don't end up squished right up against the bottom of the screen. - .child(div().p_4()) - } -} diff --git a/crates/storybook/src/stories/overflow_scroll.rs b/crates/storybook/src/stories/overflow_scroll.rs deleted file mode 100644 index a9ba09d6a3..0000000000 --- a/crates/storybook/src/stories/overflow_scroll.rs +++ /dev/null @@ -1,41 +0,0 @@ -use gpui::Render; -use story::Story; - -use ui::prelude::*; - -pub struct OverflowScrollStory; - -impl Render for OverflowScrollStory { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - Story::container(cx) - .child(Story::title("Overflow Scroll", cx)) - .child(Story::label("`overflow_x_scroll`", cx)) - .child( - h_flex() - .id("overflow_x_scroll") - .gap_2() - .overflow_x_scroll() - .children((0..100).map(|i| { - div() - .p_4() - .debug_bg_cyan() - .child(SharedString::from(format!("Child {}", i + 1))) - })), - ) - .child(Story::label("`overflow_y_scroll`", cx)) - .child( - v_flex() - .w_full() - .flex_1() - .id("overflow_y_scroll") - .gap_2() - .overflow_y_scroll() - .children((0..100).map(|i| { - div() - .p_4() - .debug_bg_green() - .child(SharedString::from(format!("Child {}", i + 1))) - })), - ) - } -} diff --git a/crates/storybook/src/stories/picker.rs b/crates/storybook/src/stories/picker.rs deleted file mode 100644 index d2d9a854a1..0000000000 --- a/crates/storybook/src/stories/picker.rs +++ /dev/null @@ -1,206 +0,0 @@ -use fuzzy::StringMatchCandidate; -use gpui::{App, Entity, KeyBinding, Render, SharedString, Styled, Task, Window, div, prelude::*}; -use picker::{Picker, PickerDelegate}; -use std::sync::Arc; -use ui::{Label, ListItem}; -use ui::{ListItemSpacing, prelude::*}; - -pub struct PickerStory { - picker: Entity>, -} - -struct Delegate { - candidates: Arc<[StringMatchCandidate]>, - matches: Vec, - selected_ix: usize, -} - -impl Delegate { - fn new(strings: &[&str]) -> Self { - Self { - candidates: strings - .iter() - .copied() - .enumerate() - .map(|(id, string)| StringMatchCandidate::new(id, string)) - .collect(), - matches: vec![], - selected_ix: 0, - } - } -} - -impl PickerDelegate for Delegate { - type ListItem = ListItem; - - fn match_count(&self) -> usize { - self.candidates.len() - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Test".into() - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _window: &mut Window, - _cx: &mut Context>, - ) -> Option { - let candidate_ix = self.matches.get(ix)?; - // TASK: Make StringMatchCandidate::string a SharedString - let candidate = SharedString::from(self.candidates[*candidate_ix].string.clone()); - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child(Label::new(candidate)), - ) - } - - fn selected_index(&self) -> usize { - self.selected_ix - } - - fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context>) { - self.selected_ix = ix; - cx.notify(); - } - - fn confirm(&mut self, secondary: bool, _window: &mut Window, _cx: &mut Context>) { - let candidate_ix = self.matches[self.selected_ix]; - let candidate = self.candidates[candidate_ix].string.clone(); - - if secondary { - eprintln!("Secondary confirmed {}", candidate) - } else { - eprintln!("Confirmed {}", candidate) - } - } - - fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { - cx.quit(); - } - - fn update_matches( - &mut self, - query: String, - _: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - let candidates = self.candidates.clone(); - self.matches = cx - .background_executor() - .block(fuzzy::match_strings( - &candidates, - &query, - true, - true, - 100, - &Default::default(), - cx.background_executor().clone(), - )) - .into_iter() - .map(|r| r.candidate_id) - .collect(); - self.selected_ix = 0; - Task::ready(()) - } -} - -impl PickerStory { - pub fn new(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| { - cx.bind_keys([ - KeyBinding::new("up", menu::SelectPrevious, Some("picker")), - KeyBinding::new("pageup", menu::SelectFirst, Some("picker")), - KeyBinding::new("shift-pageup", menu::SelectFirst, Some("picker")), - KeyBinding::new("ctrl-p", menu::SelectPrevious, Some("picker")), - KeyBinding::new("down", menu::SelectNext, Some("picker")), - KeyBinding::new("pagedown", menu::SelectLast, Some("picker")), - KeyBinding::new("shift-pagedown", menu::SelectFirst, Some("picker")), - KeyBinding::new("ctrl-n", menu::SelectNext, Some("picker")), - KeyBinding::new("cmd-up", menu::SelectFirst, Some("picker")), - KeyBinding::new("cmd-down", menu::SelectLast, Some("picker")), - KeyBinding::new("enter", menu::Confirm, Some("picker")), - KeyBinding::new("ctrl-enter", menu::SecondaryConfirm, Some("picker")), - KeyBinding::new("cmd-enter", menu::SecondaryConfirm, Some("picker")), - KeyBinding::new("escape", menu::Cancel, Some("picker")), - KeyBinding::new("ctrl-c", menu::Cancel, Some("picker")), - ]); - - PickerStory { - picker: cx.new(|cx| { - let mut delegate = Delegate::new(&[ - "Baguette (France)", - "Baklava (Turkey)", - "Beef Wellington (UK)", - "Biryani (India)", - "Borscht (Ukraine)", - "Bratwurst (Germany)", - "Bulgogi (Korea)", - "Burrito (USA)", - "Ceviche (Peru)", - "Chicken Tikka Masala (India)", - "Churrasco (Brazil)", - "Couscous (North Africa)", - "Croissant (France)", - "Dim Sum (China)", - "Empanada (Argentina)", - "Fajitas (Mexico)", - "Falafel (Middle East)", - "Feijoada (Brazil)", - "Fish and Chips (UK)", - "Fondue (Switzerland)", - "Goulash (Hungary)", - "Haggis (Scotland)", - "Kebab (Middle East)", - "Kimchi (Korea)", - "Lasagna (Italy)", - "Maple Syrup Pancakes (Canada)", - "Moussaka (Greece)", - "Pad Thai (Thailand)", - "Paella (Spain)", - "Pancakes (USA)", - "Pasta Carbonara (Italy)", - "Pavlova (Australia)", - "Peking Duck (China)", - "Pho (Vietnam)", - "Pierogi (Poland)", - "Pizza (Italy)", - "Poutine (Canada)", - "Pretzel (Germany)", - "Ramen (Japan)", - "Rendang (Indonesia)", - "Sashimi (Japan)", - "Satay (Indonesia)", - "Shepherd's Pie (Ireland)", - "Sushi (Japan)", - "Tacos (Mexico)", - "Tandoori Chicken (India)", - "Tortilla (Spain)", - "Tzatziki (Greece)", - "Wiener Schnitzel (Austria)", - ]); - delegate.update_matches("".into(), window, cx).detach(); - - let picker = Picker::uniform_list(delegate, window, cx); - picker.focus(window, cx); - picker - }), - } - }) - } -} - -impl Render for PickerStory { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .bg(cx.theme().styles.colors.background) - .size_full() - .child(self.picker.clone()) - } -} diff --git a/crates/storybook/src/stories/scroll.rs b/crates/storybook/src/stories/scroll.rs deleted file mode 100644 index 8a4c7ea768..0000000000 --- a/crates/storybook/src/stories/scroll.rs +++ /dev/null @@ -1,52 +0,0 @@ -use gpui::{App, Entity, Render, SharedString, Styled, Window, div, prelude::*, px}; -use ui::Tooltip; -use ui::prelude::*; - -pub struct ScrollStory; - -impl ScrollStory { - pub fn model(cx: &mut App) -> Entity { - cx.new(|_| ScrollStory) - } -} - -impl Render for ScrollStory { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let theme = cx.theme(); - let color_1 = theme.status().created; - let color_2 = theme.status().modified; - - div() - .id("parent") - .bg(theme.colors().background) - .size_full() - .overflow_scroll() - .children((0..10).map(|row| { - div() - .w(px(1000.)) - .h(px(100.)) - .flex() - .flex_row() - .children((0..10).map(|column| { - let id = SharedString::from(format!("{}, {}", row, column)); - let bg = if row % 2 == column % 2 { - color_1 - } else { - color_2 - }; - div() - .id(id.clone()) - .tooltip(Tooltip::text(id)) - .bg(bg) - .size(px(100_f32)) - .when(row >= 5 && column >= 5, |d| { - d.overflow_scroll() - .child(div().size(px(50.)).bg(color_1)) - .child(div().size(px(50.)).bg(color_2)) - .child(div().size(px(50.)).bg(color_1)) - .child(div().size(px(50.)).bg(color_2)) - }) - })) - })) - } -} diff --git a/crates/storybook/src/stories/text.rs b/crates/storybook/src/stories/text.rs deleted file mode 100644 index 7ba2378307..0000000000 --- a/crates/storybook/src/stories/text.rs +++ /dev/null @@ -1,120 +0,0 @@ -use gpui::{ - App, AppContext as _, Context, Entity, HighlightStyle, InteractiveText, IntoElement, - ParentElement, Render, Styled, StyledText, Window, div, green, red, -}; -use indoc::indoc; -use story::*; - -pub struct TextStory; - -impl TextStory { - pub fn model(cx: &mut App) -> Entity { - cx.new(|_| Self) - } -} - -impl Render for TextStory { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - Story::container(cx) - .child(Story::title("Text", cx)) - .children(vec![ - StorySection::new() - .child( - StoryItem::new("Default", div().bg(gpui::blue()).child("Hello World!")) - .usage(indoc! {r##" - div() - .child("Hello World!") - "## - }), - ) - .child( - StoryItem::new( - "Wrapping Text", - div().max_w_96().child(concat!( - "The quick brown fox jumps over the lazy dog. ", - "Meanwhile, the lazy dog decided it was time for a change. ", - "He started daily workout routines, ate healthier and became the fastest dog in town.", - )), - ) - .description("Set a width or max-width to enable text wrapping.") - .usage(indoc! {r##" - div() - .max_w_96() - .child("Some text that you want to wrap.") - "## - }), - ) - .child( - StoryItem::new( - "tbd", - div().flex().w_96().child( - div().overflow_hidden().child(concat!( - "flex-row. width 96. overflow-hidden. The quick brown fox jumps over the lazy dog. ", - "Meanwhile, the lazy dog decided it was time for a change. ", - "He started daily workout routines, ate healthier and became the fastest dog in town.", - )), - ), - ), - ) - .child( - StoryItem::new( - "Text in Horizontal Flex", - div().flex().w_96().bg(red()).child(concat!( - "flex-row. width 96. The quick brown fox jumps over the lazy dog. ", - "Meanwhile, the lazy dog decided it was time for a change. ", - "He started daily workout routines, ate healthier and became the fastest dog in town.", - )), - ) - .usage(indoc! {r##" - // NOTE: When rendering text in a horizontal flex container, - // Taffy will not pass width constraints down from the parent. - // To fix this, render text in a parent with overflow: hidden - - div() - .max_w_96() - .child("Some text that you want to wrap.") - "## - }), - ) - .child( - StoryItem::new( - "Interactive Text", - InteractiveText::new( - "interactive", - StyledText::new("Hello world, how is it going?").with_default_highlights( - &window.text_style(), - [ - ( - 6..11, - HighlightStyle { - background_color: Some(green()), - ..Default::default() - }, - ), - ], - ), - ) - .on_click(vec![2..4, 1..3, 7..9], |range_ix, _, _cx| { - println!("Clicked range {range_ix}"); - }), - ) - .usage(indoc! {r##" - InteractiveText::new( - "interactive", - StyledText::new("Hello world, how is it going?").with_highlights(&window.text_style(), [ - (6..11, HighlightStyle { - background_color: Some(green()), - ..Default::default() - }), - ]), - ) - .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| { - println!("Clicked range {range_ix}"); - }) - "## - }), - ), - ]) - .into_element() - } -} diff --git a/crates/storybook/src/stories/viewport_units.rs b/crates/storybook/src/stories/viewport_units.rs deleted file mode 100644 index 1259a713ee..0000000000 --- a/crates/storybook/src/stories/viewport_units.rs +++ /dev/null @@ -1,32 +0,0 @@ -use gpui::Render; -use story::Story; - -use ui::prelude::*; - -pub struct ViewportUnitsStory; - -impl Render for ViewportUnitsStory { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - Story::container(cx).child( - div() - .flex() - .flex_row() - .child( - div() - .w(vw(0.5, window)) - .h(vh(0.8, window)) - .bg(gpui::red()) - .text_color(gpui::white()) - .child("50vw, 80vh"), - ) - .child( - div() - .w(vw(0.25, window)) - .h(vh(0.33, window)) - .bg(gpui::green()) - .text_color(gpui::white()) - .child("25vw, 33vh"), - ), - ) - } -} diff --git a/crates/storybook/src/stories/with_rem_size.rs b/crates/storybook/src/stories/with_rem_size.rs deleted file mode 100644 index eeca3fb89f..0000000000 --- a/crates/storybook/src/stories/with_rem_size.rs +++ /dev/null @@ -1,61 +0,0 @@ -use gpui::{AnyElement, Hsla, Render}; -use story::Story; - -use ui::{prelude::*, utils::WithRemSize}; - -pub struct WithRemSizeStory; - -impl Render for WithRemSizeStory { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - Story::container(cx).child( - Example::new(16., gpui::red()) - .child( - Example::new(24., gpui::green()) - .child(Example::new(8., gpui::blue())) - .child(Example::new(16., gpui::yellow())), - ) - .child( - Example::new(12., gpui::green()) - .child(Example::new(48., gpui::blue())) - .child(Example::new(16., gpui::yellow())), - ), - ) - } -} - -#[derive(IntoElement)] -struct Example { - rem_size: Pixels, - border_color: Hsla, - children: Vec, -} - -impl Example { - pub fn new(rem_size: impl Into, border_color: Hsla) -> Self { - Self { - rem_size: rem_size.into(), - border_color, - children: Vec::new(), - } - } -} - -impl ParentElement for Example { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements); - } -} - -impl RenderOnce for Example { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - WithRemSize::new(self.rem_size).child( - v_flex() - .gap_2() - .p_2() - .border_2() - .border_color(self.border_color) - .child(Label::new(format!("1rem = {}px", f32::from(self.rem_size)))) - .children(self.children), - ) - } -} diff --git a/crates/storybook/src/story_selector.rs b/crates/storybook/src/story_selector.rs deleted file mode 100644 index 7f70d58b3b..0000000000 --- a/crates/storybook/src/story_selector.rs +++ /dev/null @@ -1,113 +0,0 @@ -use std::str::FromStr; -use std::sync::OnceLock; - -use crate::stories::*; -use clap::ValueEnum; -use clap::builder::PossibleValue; -use gpui::AnyView; -use strum::{EnumIter, EnumString, IntoEnumIterator}; -use ui::prelude::*; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)] -#[strum(serialize_all = "snake_case")] -pub enum ComponentStory { - ApplicationMenu, - AutoHeightEditor, - CollabNotification, - ContextMenu, - Cursor, - Focus, - OverflowScroll, - Picker, - Scroll, - Text, - ViewportUnits, - WithRemSize, - IndentGuides, -} - -impl ComponentStory { - pub fn story(&self, window: &mut Window, cx: &mut App) -> AnyView { - match self { - Self::ApplicationMenu => cx - .new(|cx| title_bar::ApplicationMenuStory::new(window, cx)) - .into(), - Self::AutoHeightEditor => AutoHeightEditorStory::new(window, cx).into(), - Self::CollabNotification => cx - .new(|_| collab_ui::notifications::CollabNotificationStory) - .into(), - Self::ContextMenu => cx.new(|_| ui::ContextMenuStory).into(), - Self::Cursor => cx.new(|_| crate::stories::CursorStory).into(), - Self::Focus => FocusStory::model(window, cx).into(), - Self::OverflowScroll => cx.new(|_| crate::stories::OverflowScrollStory).into(), - Self::Picker => PickerStory::new(window, cx).into(), - Self::Scroll => ScrollStory::model(cx).into(), - Self::Text => TextStory::model(cx).into(), - Self::ViewportUnits => cx.new(|_| crate::stories::ViewportUnitsStory).into(), - Self::WithRemSize => cx.new(|_| crate::stories::WithRemSizeStory).into(), - Self::IndentGuides => crate::stories::IndentGuidesStory::model(window, cx).into(), - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum StorySelector { - Component(ComponentStory), - KitchenSink, -} - -impl FromStr for StorySelector { - type Err = anyhow::Error; - - fn from_str(raw_story_name: &str) -> std::result::Result { - use anyhow::Context as _; - - let story = raw_story_name.to_ascii_lowercase(); - - if story == "kitchen_sink" { - return Ok(Self::KitchenSink); - } - - if let Some((_, story)) = story.split_once("components/") { - let component_story = ComponentStory::from_str(story) - .with_context(|| format!("story not found for component '{story}'"))?; - - return Ok(Self::Component(component_story)); - } - - anyhow::bail!("story not found for '{raw_story_name}'") - } -} - -impl StorySelector { - pub fn story(&self, window: &mut Window, cx: &mut App) -> AnyView { - match self { - Self::Component(component_story) => component_story.story(window, cx), - Self::KitchenSink => KitchenSinkStory::model(cx).into(), - } - } -} - -/// The list of all stories available in the storybook. -static ALL_STORY_SELECTORS: OnceLock> = OnceLock::new(); - -impl ValueEnum for StorySelector { - fn value_variants<'a>() -> &'a [Self] { - (ALL_STORY_SELECTORS.get_or_init(|| { - let component_stories = ComponentStory::iter().map(StorySelector::Component); - - component_stories - .chain(std::iter::once(StorySelector::KitchenSink)) - .collect::>() - })) as _ - } - - fn to_possible_value(&self) -> Option { - let value = match self { - Self::Component(story) => format!("components/{story}"), - Self::KitchenSink => "kitchen_sink".to_string(), - }; - - Some(PossibleValue::new(value)) - } -} diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs deleted file mode 100644 index 42ca921e63..0000000000 --- a/crates/storybook/src/storybook.rs +++ /dev/null @@ -1,161 +0,0 @@ -mod actions; -mod app_menus; -mod assets; -mod stories; -mod story_selector; - -use std::sync::Arc; - -use clap::Parser; -use dialoguer::FuzzySelect; -use gpui::{ - AnyView, App, Bounds, Context, Render, Window, WindowBounds, WindowOptions, - colors::{Colors, GlobalColors}, - div, px, size, -}; -use log::LevelFilter; -use reqwest_client::ReqwestClient; -use settings::{KeymapFile, Settings}; -use simplelog::SimpleLogger; -use strum::IntoEnumIterator; -use theme::ThemeSettings; -use ui::prelude::*; - -use crate::app_menus::app_menus; -use crate::assets::Assets; -use crate::story_selector::{ComponentStory, StorySelector}; -use actions::Quit; -pub use indoc::indoc; - -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Args { - #[arg(value_enum)] - story: Option, - - /// The name of the theme to use in the storybook. - /// - /// If not provided, the default theme will be used. - #[arg(long)] - theme: Option, -} - -fn main() { - SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); - - menu::init(); - let args = Args::parse(); - - let story_selector = args.story.unwrap_or_else(|| { - let stories = ComponentStory::iter().collect::>(); - - ctrlc::set_handler(move || {}).unwrap(); - - let result = FuzzySelect::new() - .with_prompt("Choose a story to run:") - .items(&stories) - .interact(); - - let Ok(selection) = result else { - dialoguer::console::Term::stderr().show_cursor().unwrap(); - std::process::exit(0); - }; - - StorySelector::Component(stories[selection]) - }); - let theme_name = args.theme.unwrap_or("One Dark".to_string()); - - gpui::Application::new().with_assets(Assets).run(move |cx| { - load_embedded_fonts(cx).unwrap(); - - cx.set_global(GlobalColors(Arc::new(Colors::default()))); - - let http_client = ReqwestClient::user_agent("zed_storybook").unwrap(); - cx.set_http_client(Arc::new(http_client)); - - settings::init(cx); - theme::init(theme::LoadThemes::All(Box::new(Assets)), cx); - - let selector = story_selector; - - let mut theme_settings = ThemeSettings::get_global(cx).clone(); - theme_settings.theme = - theme::ThemeSelection::Static(settings::ThemeName(theme_name.into())); - ThemeSettings::override_global(theme_settings, cx); - - editor::init(cx); - init(cx); - load_storybook_keymap(cx); - cx.set_menus(app_menus()); - - let size = size(px(1500.), px(780.)); - let bounds = Bounds::centered(None, size, cx); - let _window = cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - move |window, cx| { - theme::setup_ui_font(window, cx); - - cx.new(|cx| StoryWrapper::new(selector.story(window, cx))) - }, - ); - - cx.activate(true); - }); -} - -#[derive(Clone)] -pub struct StoryWrapper { - story: AnyView, -} - -impl StoryWrapper { - pub(crate) fn new(story: AnyView) -> Self { - Self { story } - } -} - -impl Render for StoryWrapper { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .flex() - .flex_col() - .size_full() - .font_family(".ZedMono") - .child(self.story.clone()) - } -} - -fn load_embedded_fonts(cx: &App) -> anyhow::Result<()> { - let font_paths = cx.asset_source().list("fonts")?; - let mut embedded_fonts = Vec::new(); - for font_path in font_paths { - if font_path.ends_with(".ttf") { - let font_bytes = cx - .asset_source() - .load(&font_path)? - .expect("Should never be None in the storybook"); - embedded_fonts.push(font_bytes); - } - } - - cx.text_system().add_fonts(embedded_fonts) -} - -fn load_storybook_keymap(cx: &mut App) { - cx.bind_keys(KeymapFile::load_asset("keymaps/storybook.json", None, cx).unwrap()); -} - -pub fn init(cx: &mut App) { - cx.on_action(quit); -} - -fn quit(_: &Quit, cx: &mut App) { - cx.spawn(async move |cx| { - cx.update(|cx| cx.quit())?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); -} diff --git a/crates/streaming_diff/Cargo.toml b/crates/streaming_diff/Cargo.toml deleted file mode 100644 index b3645a182c..0000000000 --- a/crates/streaming_diff/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "streaming_diff" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/streaming_diff.rs" - -[dependencies] -ordered-float.workspace = true -rope.workspace = true - -[dev-dependencies] -rand.workspace = true -util = { workspace = true, features = ["test-support"] } diff --git a/crates/streaming_diff/LICENSE-GPL b/crates/streaming_diff/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/streaming_diff/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/streaming_diff/src/streaming_diff.rs b/crates/streaming_diff/src/streaming_diff.rs deleted file mode 100644 index 5677981b0d..0000000000 --- a/crates/streaming_diff/src/streaming_diff.rs +++ /dev/null @@ -1,1104 +0,0 @@ -use ordered_float::OrderedFloat; -use rope::{Point, Rope, TextSummary}; -use std::collections::{BTreeSet, HashMap}; -use std::{ - cmp, - fmt::{self, Debug}, - ops::Range, -}; - -#[derive(Default)] -struct Matrix { - cells: Vec, - rows: usize, - cols: usize, -} - -impl Matrix { - fn new() -> Self { - Self { - cells: Vec::new(), - rows: 0, - cols: 0, - } - } - - fn resize(&mut self, rows: usize, cols: usize) { - self.cells.resize(rows * cols, 0.); - self.rows = rows; - self.cols = cols; - } - - fn swap_columns(&mut self, col1: usize, col2: usize) { - if col1 == col2 { - return; - } - - if col1 >= self.cols { - panic!("column out of bounds"); - } - - if col2 >= self.cols { - panic!("column out of bounds"); - } - - unsafe { - let ptr = self.cells.as_mut_ptr(); - std::ptr::swap_nonoverlapping( - ptr.add(col1 * self.rows), - ptr.add(col2 * self.rows), - self.rows, - ); - } - } - - fn get(&self, row: usize, col: usize) -> f64 { - if row >= self.rows { - panic!("row out of bounds") - } - - if col >= self.cols { - panic!("column out of bounds") - } - self.cells[col * self.rows + row] - } - - fn set(&mut self, row: usize, col: usize, value: f64) { - if row >= self.rows { - panic!("row out of bounds") - } - - if col >= self.cols { - panic!("column out of bounds") - } - - self.cells[col * self.rows + row] = value; - } -} - -impl Debug for Matrix { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f)?; - for i in 0..self.rows { - for j in 0..self.cols { - write!(f, "{:5}", self.get(i, j))?; - } - writeln!(f)?; - } - Ok(()) - } -} - -#[derive(Debug, Clone)] -pub enum CharOperation { - Insert { text: String }, - Delete { bytes: usize }, - Keep { bytes: usize }, -} - -#[derive(Default)] -pub struct StreamingDiff { - old: Vec, - new: Vec, - scores: Matrix, - old_text_ix: usize, - new_text_ix: usize, - equal_runs: HashMap<(usize, usize), u32>, -} - -impl StreamingDiff { - const INSERTION_SCORE: f64 = -1.; - const DELETION_SCORE: f64 = -20.; - const EQUALITY_BASE: f64 = 1.8; - const MAX_EQUALITY_EXPONENT: i32 = 16; - - pub fn new(old: String) -> Self { - let old = old.chars().collect::>(); - let mut scores = Matrix::new(); - scores.resize(old.len() + 1, 1); - for i in 0..=old.len() { - scores.set(i, 0, i as f64 * Self::DELETION_SCORE); - } - Self { - old, - new: Vec::new(), - scores, - old_text_ix: 0, - new_text_ix: 0, - equal_runs: Default::default(), - } - } - - pub fn push_new(&mut self, text: &str) -> Vec { - self.new.extend(text.chars()); - self.scores.swap_columns(0, self.scores.cols - 1); - self.scores - .resize(self.old.len() + 1, self.new.len() - self.new_text_ix + 1); - self.equal_runs.retain(|(_i, j), _| *j == self.new_text_ix); - - for j in self.new_text_ix + 1..=self.new.len() { - let relative_j = j - self.new_text_ix; - - self.scores - .set(0, relative_j, j as f64 * Self::INSERTION_SCORE); - for i in 1..=self.old.len() { - let insertion_score = self.scores.get(i, relative_j - 1) + Self::INSERTION_SCORE; - let deletion_score = self.scores.get(i - 1, relative_j) + Self::DELETION_SCORE; - let equality_score = if self.old[i - 1] == self.new[j - 1] { - let mut equal_run = self.equal_runs.get(&(i - 1, j - 1)).copied().unwrap_or(0); - equal_run += 1; - self.equal_runs.insert((i, j), equal_run); - - let exponent = cmp::min(equal_run as i32 / 4, Self::MAX_EQUALITY_EXPONENT); - self.scores.get(i - 1, relative_j - 1) + Self::EQUALITY_BASE.powi(exponent) - } else { - f64::NEG_INFINITY - }; - - let score = insertion_score.max(deletion_score).max(equality_score); - self.scores.set(i, relative_j, score); - } - } - - let mut max_score = f64::NEG_INFINITY; - let mut next_old_text_ix = self.old_text_ix; - let next_new_text_ix = self.new.len(); - for i in self.old_text_ix..=self.old.len() { - let score = self.scores.get(i, next_new_text_ix - self.new_text_ix); - if score > max_score { - max_score = score; - next_old_text_ix = i; - } - } - - let hunks = self.backtrack(next_old_text_ix, next_new_text_ix); - self.old_text_ix = next_old_text_ix; - self.new_text_ix = next_new_text_ix; - hunks - } - - fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec { - let mut pending_insert: Option> = None; - let mut hunks = Vec::new(); - let mut i = old_text_ix; - let mut j = new_text_ix; - while (i, j) != (self.old_text_ix, self.new_text_ix) { - let insertion_score = if j > self.new_text_ix { - Some((i, j - 1)) - } else { - None - }; - let deletion_score = if i > self.old_text_ix { - Some((i - 1, j)) - } else { - None - }; - let equality_score = if i > self.old_text_ix && j > self.new_text_ix { - if self.old[i - 1] == self.new[j - 1] { - Some((i - 1, j - 1)) - } else { - None - } - } else { - None - }; - - let (prev_i, prev_j) = [insertion_score, deletion_score, equality_score] - .iter() - .max_by_key(|cell| { - cell.map(|(i, j)| OrderedFloat(self.scores.get(i, j - self.new_text_ix))) - }) - .unwrap() - .unwrap(); - - if prev_i == i && prev_j == j - 1 { - if let Some(pending_insert) = pending_insert.as_mut() { - pending_insert.start = prev_j; - } else { - pending_insert = Some(prev_j..j); - } - } else { - if let Some(range) = pending_insert.take() { - hunks.push(CharOperation::Insert { - text: self.new[range].iter().collect(), - }); - } - - let char_len = self.old[i - 1].len_utf8(); - if prev_i == i - 1 && prev_j == j { - if let Some(CharOperation::Delete { bytes: len }) = hunks.last_mut() { - *len += char_len; - } else { - hunks.push(CharOperation::Delete { bytes: char_len }) - } - } else if let Some(CharOperation::Keep { bytes: len }) = hunks.last_mut() { - *len += char_len; - } else { - hunks.push(CharOperation::Keep { bytes: char_len }) - } - } - - i = prev_i; - j = prev_j; - } - - if let Some(range) = pending_insert.take() { - hunks.push(CharOperation::Insert { - text: self.new[range].iter().collect(), - }); - } - - hunks.reverse(); - hunks - } - - pub fn finish(self) -> Vec { - self.backtrack(self.old.len(), self.new.len()) - } -} - -#[derive(Debug, Clone, PartialEq)] -pub enum LineOperation { - Insert { lines: u32 }, - Delete { lines: u32 }, - Keep { lines: u32 }, -} - -#[derive(Debug, Default)] -pub struct LineDiff { - inserted_newline_at_end: bool, - /// The extent of kept and deleted text. - old_end: Point, - /// The extent of kept and inserted text. - new_end: Point, - /// Deleted rows, expressed in terms of the old text. - deleted_rows: BTreeSet, - /// Inserted rows, expressed in terms of the new text. - inserted_rows: BTreeSet, - buffered_insert: String, - /// After deleting a newline, we buffer deletion until we keep or insert a character. - buffered_delete: usize, -} - -impl LineDiff { - pub fn push_char_operations<'a>( - &mut self, - operations: impl IntoIterator, - old_text: &Rope, - ) { - for operation in operations { - self.push_char_operation(operation, old_text); - } - } - - pub fn push_char_operation(&mut self, operation: &CharOperation, old_text: &Rope) { - match operation { - CharOperation::Insert { text } => { - self.flush_delete(old_text); - - if is_line_start(self.old_end) { - if let Some(newline_ix) = text.rfind('\n') { - let (prefix, suffix) = text.split_at(newline_ix + 1); - self.buffered_insert.push_str(prefix); - self.flush_insert(old_text); - self.buffered_insert.push_str(suffix); - } else { - self.buffered_insert.push_str(text); - } - } else { - self.buffered_insert.push_str(text); - if !text.ends_with('\n') { - self.flush_insert(old_text); - } - } - } - CharOperation::Delete { bytes } => { - self.buffered_delete += bytes; - - let common_suffix_len = self.trim_buffered_end(old_text); - self.flush_insert(old_text); - - if common_suffix_len > 0 || !is_line_end(self.old_end, old_text) { - self.flush_delete(old_text); - self.keep(common_suffix_len, old_text); - } - } - CharOperation::Keep { bytes } => { - self.flush_delete(old_text); - self.flush_insert(old_text); - self.keep(*bytes, old_text); - } - } - } - - fn flush_insert(&mut self, old_text: &Rope) { - if self.buffered_insert.is_empty() { - return; - } - - let new_start = self.new_end; - let lines = TextSummary::from(self.buffered_insert.as_str()).lines; - self.new_end += lines; - - if is_line_start(self.old_end) { - if self.new_end.column == 0 { - self.inserted_rows.extend(new_start.row..self.new_end.row); - } else { - self.deleted_rows.insert(self.old_end.row); - self.inserted_rows.extend(new_start.row..=self.new_end.row); - } - } else if is_line_end(self.old_end, old_text) { - if self.buffered_insert.starts_with('\n') { - self.inserted_rows - .extend(new_start.row + 1..=self.new_end.row); - self.inserted_newline_at_end = true; - } else { - if !self.inserted_newline_at_end { - self.deleted_rows.insert(self.old_end.row); - } - self.inserted_rows.extend(new_start.row..=self.new_end.row); - } - } else { - self.deleted_rows.insert(self.old_end.row); - self.inserted_rows.extend(new_start.row..=self.new_end.row); - } - - self.buffered_insert.clear(); - } - - fn flush_delete(&mut self, old_text: &Rope) { - if self.buffered_delete == 0 { - return; - } - - let old_start = self.old_end; - self.old_end = - old_text.offset_to_point(old_text.point_to_offset(self.old_end) + self.buffered_delete); - - if is_line_end(old_start, old_text) && is_line_end(self.old_end, old_text) { - self.deleted_rows - .extend(old_start.row + 1..=self.old_end.row); - } else if is_line_start(old_start) - && (is_line_start(self.old_end) && self.old_end < old_text.max_point()) - && self.new_end.column == 0 - { - self.deleted_rows.extend(old_start.row..self.old_end.row); - } else { - self.inserted_rows.insert(self.new_end.row); - self.deleted_rows.extend(old_start.row..=self.old_end.row); - } - - self.inserted_newline_at_end = false; - self.buffered_delete = 0; - } - - fn keep(&mut self, bytes: usize, old_text: &Rope) { - if bytes == 0 { - return; - } - - let lines = - old_text.offset_to_point(old_text.point_to_offset(self.old_end) + bytes) - self.old_end; - self.old_end += lines; - self.new_end += lines; - self.inserted_newline_at_end = false; - } - - fn trim_buffered_end(&mut self, old_text: &Rope) -> usize { - let old_start_offset = old_text.point_to_offset(self.old_end); - let old_end_offset = old_start_offset + self.buffered_delete; - - let new_chars = self.buffered_insert.chars().rev(); - let old_chars = old_text - .chunks_in_range(old_start_offset..old_end_offset) - .flat_map(|chunk| chunk.chars().rev()); - - let mut common_suffix_len = 0; - for (new_ch, old_ch) in new_chars.zip(old_chars) { - if new_ch == old_ch { - common_suffix_len += new_ch.len_utf8(); - } else { - break; - } - } - - self.buffered_delete -= common_suffix_len; - self.buffered_insert - .truncate(self.buffered_insert.len() - common_suffix_len); - - common_suffix_len - } - - pub fn finish(&mut self, old_text: &Rope) { - self.flush_insert(old_text); - self.flush_delete(old_text); - - let old_start = self.old_end; - self.old_end = old_text.max_point(); - self.new_end += self.old_end - old_start; - } - - pub fn line_operations(&self) -> Vec { - let mut ops = Vec::new(); - let mut deleted_rows = self.deleted_rows.iter().copied().peekable(); - let mut inserted_rows = self.inserted_rows.iter().copied().peekable(); - let mut old_row = 0; - let mut new_row = 0; - - while deleted_rows.peek().is_some() || inserted_rows.peek().is_some() { - // Check for a run of deleted lines at current old row. - if Some(old_row) == deleted_rows.peek().copied() { - if let Some(LineOperation::Delete { lines }) = ops.last_mut() { - *lines += 1; - } else { - ops.push(LineOperation::Delete { lines: 1 }); - } - old_row += 1; - deleted_rows.next(); - } else if Some(new_row) == inserted_rows.peek().copied() { - if let Some(LineOperation::Insert { lines }) = ops.last_mut() { - *lines += 1; - } else { - ops.push(LineOperation::Insert { lines: 1 }); - } - new_row += 1; - inserted_rows.next(); - } else { - // Keep lines until the next deletion, insertion, or the end of the old text. - let lines_to_next_deletion = inserted_rows - .peek() - .copied() - .unwrap_or(self.new_end.row + 1) - - new_row; - let lines_to_next_insertion = - deleted_rows.peek().copied().unwrap_or(self.old_end.row + 1) - old_row; - let kept_lines = - cmp::max(1, cmp::min(lines_to_next_insertion, lines_to_next_deletion)); - if kept_lines > 0 { - ops.push(LineOperation::Keep { lines: kept_lines }); - old_row += kept_lines; - new_row += kept_lines; - } - } - } - - if old_row < self.old_end.row + 1 { - ops.push(LineOperation::Keep { - lines: self.old_end.row + 1 - old_row, - }); - } - - ops - } -} - -fn is_line_start(point: Point) -> bool { - point.column == 0 -} - -fn is_line_end(point: Point, text: &Rope) -> bool { - text.line_len(point.row) == point.column -} - -#[cfg(test)] -mod tests { - use super::*; - use rand::prelude::*; - use std::env; - - #[test] - fn test_delete_first_of_two_lines() { - let old_text = "aaaa\nbbbb"; - let char_ops = vec![ - CharOperation::Delete { bytes: 5 }, - CharOperation::Keep { bytes: 4 }, - ]; - let expected_line_ops = vec![ - LineOperation::Delete { lines: 1 }, - LineOperation::Keep { lines: 1 }, - ]; - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &expected_line_ops) - ); - - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!(line_ops, expected_line_ops); - } - - #[test] - fn test_delete_second_of_two_lines() { - let old_text = "aaaa\nbbbb"; - let char_ops = vec![ - CharOperation::Keep { bytes: 5 }, - CharOperation::Delete { bytes: 4 }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Keep { lines: 1 }, - LineOperation::Delete { lines: 1 }, - LineOperation::Insert { lines: 1 } - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_add_new_line() { - let old_text = "aaaa\nbbbb"; - let char_ops = vec![ - CharOperation::Keep { bytes: 9 }, - CharOperation::Insert { - text: "\ncccc".into(), - }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Keep { lines: 2 }, - LineOperation::Insert { lines: 1 } - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_delete_line_in_middle() { - let old_text = "aaaa\nbbbb\ncccc"; - let char_ops = vec![ - CharOperation::Keep { bytes: 5 }, - CharOperation::Delete { bytes: 5 }, - CharOperation::Keep { bytes: 4 }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Keep { lines: 1 }, - LineOperation::Delete { lines: 1 }, - LineOperation::Keep { lines: 1 } - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_replace_line() { - let old_text = "aaaa\nbbbb\ncccc"; - let char_ops = vec![ - CharOperation::Keep { bytes: 5 }, - CharOperation::Delete { bytes: 4 }, - CharOperation::Insert { - text: "BBBB".into(), - }, - CharOperation::Keep { bytes: 5 }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Keep { lines: 1 }, - LineOperation::Delete { lines: 1 }, - LineOperation::Insert { lines: 1 }, - LineOperation::Keep { lines: 1 } - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_multiple_edits_on_different_lines() { - let old_text = "aaaa\nbbbb\ncccc\ndddd"; - let char_ops = vec![ - CharOperation::Insert { text: "A".into() }, - CharOperation::Keep { bytes: 9 }, - CharOperation::Delete { bytes: 5 }, - CharOperation::Keep { bytes: 4 }, - CharOperation::Insert { - text: "\nEEEE".into(), - }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Delete { lines: 1 }, - LineOperation::Insert { lines: 1 }, - LineOperation::Keep { lines: 1 }, - LineOperation::Delete { lines: 2 }, - LineOperation::Insert { lines: 2 }, - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_edit_at_end_of_line() { - let old_text = "aaaa\nbbbb\ncccc"; - let char_ops = vec![ - CharOperation::Keep { bytes: 4 }, - CharOperation::Insert { text: "A".into() }, - CharOperation::Keep { bytes: 10 }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Delete { lines: 1 }, - LineOperation::Insert { lines: 1 }, - LineOperation::Keep { lines: 2 } - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_insert_newline_character() { - let old_text = "aaaabbbb"; - let char_ops = vec![ - CharOperation::Keep { bytes: 4 }, - CharOperation::Insert { text: "\n".into() }, - CharOperation::Keep { bytes: 4 }, - ]; - let new_text = apply_char_operations(old_text, &char_ops); - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Delete { lines: 1 }, - LineOperation::Insert { lines: 2 } - ] - ); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_insert_newline_at_beginning() { - let old_text = "aaaa\nbbbb"; - let char_ops = vec![ - CharOperation::Insert { text: "\n".into() }, - CharOperation::Keep { bytes: 9 }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Insert { lines: 1 }, - LineOperation::Keep { lines: 2 } - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_delete_newline() { - let old_text = "aaaa\nbbbb"; - let char_ops = vec![ - CharOperation::Keep { bytes: 4 }, - CharOperation::Delete { bytes: 1 }, - CharOperation::Keep { bytes: 4 }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Delete { lines: 2 }, - LineOperation::Insert { lines: 1 } - ] - ); - - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_insert_multiple_newlines() { - let old_text = "aaaa\nbbbb"; - let char_ops = vec![ - CharOperation::Keep { bytes: 5 }, - CharOperation::Insert { - text: "\n\n".into(), - }, - CharOperation::Keep { bytes: 4 }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Keep { lines: 1 }, - LineOperation::Insert { lines: 2 }, - LineOperation::Keep { lines: 1 } - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_delete_multiple_newlines() { - let old_text = "aaaa\n\n\nbbbb"; - let char_ops = vec![ - CharOperation::Keep { bytes: 5 }, - CharOperation::Delete { bytes: 2 }, - CharOperation::Keep { bytes: 4 }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Keep { lines: 1 }, - LineOperation::Delete { lines: 2 }, - LineOperation::Keep { lines: 1 } - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_complex_scenario() { - let old_text = "line1\nline2\nline3\nline4"; - let char_ops = vec![ - CharOperation::Keep { bytes: 6 }, - CharOperation::Insert { - text: "inserted\n".into(), - }, - CharOperation::Delete { bytes: 6 }, - CharOperation::Keep { bytes: 5 }, - CharOperation::Insert { - text: "\nnewline".into(), - }, - CharOperation::Keep { bytes: 6 }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Keep { lines: 1 }, - LineOperation::Delete { lines: 1 }, - LineOperation::Insert { lines: 1 }, - LineOperation::Keep { lines: 1 }, - LineOperation::Insert { lines: 1 }, - LineOperation::Keep { lines: 1 } - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!(new_text, "line1\ninserted\nline3\nnewline\nline4"); - assert_eq!( - apply_line_operations(old_text, &new_text, &line_ops), - new_text, - ); - } - - #[test] - fn test_cleaning_up_common_suffix() { - let old_text = concat!( - " for y in 0..size.y() {\n", - " let a = 10;\n", - " let b = 20;\n", - " }", - ); - let char_ops = [ - CharOperation::Keep { bytes: 8 }, - CharOperation::Insert { text: "let".into() }, - CharOperation::Insert { - text: " mut".into(), - }, - CharOperation::Insert { text: " y".into() }, - CharOperation::Insert { text: " =".into() }, - CharOperation::Insert { text: " 0".into() }, - CharOperation::Insert { text: ";".into() }, - CharOperation::Insert { text: "\n".into() }, - CharOperation::Insert { - text: " while".into(), - }, - CharOperation::Insert { text: " y".into() }, - CharOperation::Insert { - text: " < size".into(), - }, - CharOperation::Insert { text: ".".into() }, - CharOperation::Insert { text: "y".into() }, - CharOperation::Insert { text: "()".into() }, - CharOperation::Insert { text: " {".into() }, - CharOperation::Insert { text: "\n".into() }, - CharOperation::Delete { bytes: 23 }, - CharOperation::Keep { bytes: 23 }, - CharOperation::Keep { bytes: 1 }, - CharOperation::Keep { bytes: 23 }, - CharOperation::Keep { bytes: 1 }, - CharOperation::Keep { bytes: 8 }, - CharOperation::Insert { - text: " y".into(), - }, - CharOperation::Insert { text: " +=".into() }, - CharOperation::Insert { text: " 1".into() }, - CharOperation::Insert { text: ";".into() }, - CharOperation::Insert { text: "\n".into() }, - CharOperation::Insert { - text: " ".into(), - }, - CharOperation::Keep { bytes: 1 }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Delete { lines: 1 }, - LineOperation::Insert { lines: 2 }, - LineOperation::Keep { lines: 2 }, - LineOperation::Delete { lines: 1 }, - LineOperation::Insert { lines: 2 }, - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_random_diffs() { - random_test(|mut rng| { - let old_text_len = env::var("OLD_TEXT_LEN") - .map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable")) - .unwrap_or(10); - - let old = random_text(&mut rng, old_text_len); - println!("old text: {:?}", old); - - let new = randomly_edit(&old, &mut rng); - println!("new text: {:?}", new); - - let char_operations = random_streaming_diff(&mut rng, &old, &new); - println!("char operations: {:?}", char_operations); - - // Use apply_char_operations to verify the result - let patched = apply_char_operations(&old, &char_operations); - assert_eq!(patched, new); - - // Test char_ops_to_line_ops - let line_ops = char_ops_to_line_ops(&old, &char_operations); - println!("line operations: {:?}", line_ops); - let patched = apply_line_operations(&old, &new, &line_ops); - assert_eq!(patched, new); - }); - } - - fn char_ops_to_line_ops(old_text: &str, char_ops: &[CharOperation]) -> Vec { - let old_rope = Rope::from(old_text); - let mut diff = LineDiff::default(); - for op in char_ops { - diff.push_char_operation(op, &old_rope); - } - diff.finish(&old_rope); - diff.line_operations() - } - - fn random_streaming_diff(rng: &mut impl Rng, old: &str, new: &str) -> Vec { - let mut diff = StreamingDiff::new(old.to_string()); - let mut char_operations = Vec::new(); - let mut new_len = 0; - - while new_len < new.len() { - let mut chunk_len = rng.random_range(1..=new.len() - new_len); - while !new.is_char_boundary(new_len + chunk_len) { - chunk_len += 1; - } - let chunk = &new[new_len..new_len + chunk_len]; - let new_hunks = diff.push_new(chunk); - char_operations.extend(new_hunks); - new_len += chunk_len; - } - - char_operations.extend(diff.finish()); - char_operations - } - - fn random_test(mut test_fn: F) - where - F: FnMut(StdRng), - { - let iterations = env::var("ITERATIONS") - .map(|i| i.parse().expect("invalid `ITERATIONS` variable")) - .unwrap_or(100); - - let seed: u64 = env::var("SEED") - .map(|s| s.parse().expect("invalid `SEED` variable")) - .unwrap_or(0); - - println!( - "Running test with {} iterations and seed {}", - iterations, seed - ); - - for i in 0..iterations { - println!("Iteration {}", i + 1); - let rng = StdRng::seed_from_u64(seed + i); - test_fn(rng); - } - } - - fn apply_line_operations(old_text: &str, new_text: &str, line_ops: &[LineOperation]) -> String { - let mut result: Vec<&str> = Vec::new(); - - let old_lines: Vec<&str> = old_text.split('\n').collect(); - let new_lines: Vec<&str> = new_text.split('\n').collect(); - let mut old_start = 0_usize; - let mut new_start = 0_usize; - - for op in line_ops { - match op { - LineOperation::Keep { lines } => { - let old_end = old_start + *lines as usize; - result.extend(&old_lines[old_start..old_end]); - old_start = old_end; - new_start += *lines as usize; - } - LineOperation::Delete { lines } => { - old_start += *lines as usize; - } - LineOperation::Insert { lines } => { - let new_end = new_start + *lines as usize; - result.extend(&new_lines[new_start..new_end]); - new_start = new_end; - } - } - } - - result.join("\n") - } - - #[test] - fn test_apply_char_operations() { - let old_text = "Hello, world!"; - let char_ops = vec![ - CharOperation::Keep { bytes: 7 }, - CharOperation::Delete { bytes: 5 }, - CharOperation::Insert { - text: "Rust".to_string(), - }, - CharOperation::Keep { bytes: 1 }, - ]; - let result = apply_char_operations(old_text, &char_ops); - assert_eq!(result, "Hello, Rust!"); - } - - fn random_text(rng: &mut impl Rng, length: usize) -> String { - util::RandomCharIter::new(rng).take(length).collect() - } - - fn randomly_edit(text: &str, rng: &mut impl Rng) -> String { - let mut result = String::from(text); - let edit_count = rng.random_range(1..=5); - - fn random_char_range(text: &str, rng: &mut impl Rng) -> (usize, usize) { - let mut start = rng.random_range(0..=text.len()); - while !text.is_char_boundary(start) { - start -= 1; - } - let mut end = rng.random_range(start..=text.len()); - while !text.is_char_boundary(end) { - end += 1; - } - (start, end) - } - - for _ in 0..edit_count { - match rng.random_range(0..3) { - 0 => { - // Insert - let (pos, _) = random_char_range(&result, rng); - let insert_len = rng.random_range(1..=5); - let insert_text: String = random_text(rng, insert_len); - result.insert_str(pos, &insert_text); - } - 1 => { - // Delete - if !result.is_empty() { - let (start, end) = random_char_range(&result, rng); - result.replace_range(start..end, ""); - } - } - 2 => { - // Replace - if !result.is_empty() { - let (start, end) = random_char_range(&result, rng); - let replace_len = end - start; - let replace_text: String = random_text(rng, replace_len); - result.replace_range(start..end, &replace_text); - } - } - _ => unreachable!(), - } - } - - result - } - - fn apply_char_operations(old_text: &str, char_ops: &[CharOperation]) -> String { - let mut result = String::new(); - let mut old_ix = 0; - - for operation in char_ops { - match operation { - CharOperation::Keep { bytes } => { - result.push_str(&old_text[old_ix..old_ix + bytes]); - old_ix += bytes; - } - CharOperation::Delete { bytes } => { - old_ix += bytes; - } - CharOperation::Insert { text } => { - result.push_str(text); - } - } - } - - result - } -} diff --git a/crates/sum_tree/Cargo.toml b/crates/sum_tree/Cargo.toml index 3e06ede162..d9924a4210 100644 --- a/crates/sum_tree/Cargo.toml +++ b/crates/sum_tree/Cargo.toml @@ -17,13 +17,11 @@ doctest = false arrayvec = "0.7.1" rayon.workspace = true log.workspace = true -ztracing.workspace = true tracing.workspace = true [dev-dependencies] ctor.workspace = true rand.workspace = true -zlog.workspace = true [package.metadata.cargo-machete] ignored = ["tracing"] diff --git a/crates/sum_tree/src/cursor.rs b/crates/sum_tree/src/cursor.rs index 589ae96a2a..efdb6ac13a 100644 --- a/crates/sum_tree/src/cursor.rs +++ b/crates/sum_tree/src/cursor.rs @@ -1,7 +1,7 @@ use super::*; use arrayvec::ArrayVec; use std::{cmp::Ordering, mem, sync::Arc}; -use ztracing::instrument; +use tracing::instrument; #[derive(Clone)] struct StackEntry<'a, T: Item, D> { diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index bfc4587969..4a83730ebd 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -8,7 +8,7 @@ use std::marker::PhantomData; use std::mem; use std::{cmp::Ordering, fmt, iter::FromIterator, sync::Arc}; pub use tree_map::{MapSeekTarget, TreeMap, TreeSet}; -use ztracing::instrument; +use tracing::instrument; #[cfg(test)] pub const TREE_BASE: usize = 2; diff --git a/crates/supermaven/Cargo.toml b/crates/supermaven/Cargo.toml deleted file mode 100644 index c2d0c48a9e..0000000000 --- a/crates/supermaven/Cargo.toml +++ /dev/null @@ -1,44 +0,0 @@ -[package] -name = "supermaven" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/supermaven.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -client.workspace = true -collections.workspace = true -edit_prediction_types.workspace = true -futures.workspace = true -gpui.workspace = true -language.workspace = true -log.workspace = true -postage.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smol.workspace = true -supermaven_api.workspace = true -text.workspace = true -ui.workspace = true -unicode-segmentation.workspace = true -util.workspace = true - -[dev-dependencies] -editor = { workspace = true, features = ["test-support"] } -env_logger.workspace = true -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } -settings = { workspace = true, features = ["test-support"] } -theme = { workspace = true, features = ["test-support"] } -util = { workspace = true, features = ["test-support"] } -http_client = { workspace = true, features = ["test-support"] } diff --git a/crates/supermaven/LICENSE-GPL b/crates/supermaven/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/supermaven/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/supermaven/src/messages.rs b/crates/supermaven/src/messages.rs deleted file mode 100644 index f515d6353b..0000000000 --- a/crates/supermaven/src/messages.rs +++ /dev/null @@ -1,153 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SetApiKey { - pub api_key: String, -} - -// Outbound messages -#[derive(Debug, Serialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum OutboundMessage { - SetApiKey(SetApiKey), - StateUpdate(StateUpdateMessage), - #[allow(dead_code)] - UseFreeVersion, - Logout, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct StateUpdateMessage { - pub new_id: String, - pub updates: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum StateUpdate { - FileUpdate(FileUpdateMessage), - CursorUpdate(CursorPositionUpdateMessage), -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub struct FileUpdateMessage { - pub path: String, - pub content: String, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub struct CursorPositionUpdateMessage { - pub path: String, - pub offset: usize, -} - -// Inbound messages coming in on stdout - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum ResponseItem { - // A completion - Text { text: String }, - // Vestigial message type from old versions -- safe to ignore - Del { text: String }, - // Be able to delete whitespace prior to the cursor, likely for the rest of the completion - Dedent { text: String }, - // When the completion is over - End, - // Got the closing parentheses and shouldn't show any more after - Barrier, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SupermavenResponse { - pub state_id: String, - pub items: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct SupermavenMetadataMessage { - pub dust_strings: Option>, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct SupermavenTaskUpdateMessage { - pub task: String, - pub status: TaskStatus, - pub percent_complete: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum TaskStatus { - InProgress, - Complete, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct SupermavenActiveRepoMessage { - pub repo_simple_name: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum SupermavenPopupAction { - OpenUrl { label: String, url: String }, - NoOp { label: String }, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub struct SupermavenPopupMessage { - pub message: String, - pub actions: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "camelCase")] -pub struct ActivationRequest { - pub activate_url: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SupermavenSetMessage { - pub key: String, - pub value: serde_json::Value, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum ServiceTier { - FreeNoLicense, - #[serde(other)] - Unknown, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum SupermavenMessage { - Response(SupermavenResponse), - Metadata(SupermavenMetadataMessage), - Apology { - message: Option, - }, - ActivationRequest(ActivationRequest), - ActivationSuccess, - Passthrough { - passthrough: Box, - }, - Popup(SupermavenPopupMessage), - TaskStatus(SupermavenTaskUpdateMessage), - ActiveRepo(SupermavenActiveRepoMessage), - ServiceTier { - service_tier: ServiceTier, - }, - - Set(SupermavenSetMessage), - #[serde(other)] - Unknown, -} diff --git a/crates/supermaven/src/supermaven.rs b/crates/supermaven/src/supermaven.rs deleted file mode 100644 index 527f4ec37d..0000000000 --- a/crates/supermaven/src/supermaven.rs +++ /dev/null @@ -1,511 +0,0 @@ -mod messages; -mod supermaven_edit_prediction_delegate; - -pub use supermaven_edit_prediction_delegate::*; - -use anyhow::{Context as _, Result}; -#[allow(unused_imports)] -use client::{Client, proto}; -use collections::BTreeMap; - -use futures::{AsyncBufReadExt, StreamExt, channel::mpsc, io::BufReader}; -use gpui::{App, AsyncApp, Context, Entity, EntityId, Global, Task, WeakEntity, actions}; -use language::{ - Anchor, Buffer, BufferSnapshot, ToOffset, language_settings::all_language_settings, -}; -use messages::*; -use postage::watch; -use serde::{Deserialize, Serialize}; -use settings::SettingsStore; -use smol::{ - io::AsyncWriteExt, - process::{Child, ChildStdin, ChildStdout}, -}; -use std::{path::PathBuf, process::Stdio, sync::Arc}; -use ui::prelude::*; -use util::ResultExt; - -actions!( - supermaven, - [ - /// Signs out of Supermaven. - SignOut - ] -); - -pub fn init(client: Arc, cx: &mut App) { - let supermaven = cx.new(|_| Supermaven::Starting); - Supermaven::set_global(supermaven.clone(), cx); - - let mut provider = all_language_settings(None, cx).edit_predictions.provider; - if provider == language::language_settings::EditPredictionProvider::Supermaven { - supermaven.update(cx, |supermaven, cx| supermaven.start(client.clone(), cx)); - } - - cx.observe_global::(move |cx| { - let new_provider = all_language_settings(None, cx).edit_predictions.provider; - if new_provider != provider { - provider = new_provider; - if provider == language::language_settings::EditPredictionProvider::Supermaven { - supermaven.update(cx, |supermaven, cx| supermaven.start(client.clone(), cx)); - } else { - supermaven.update(cx, |supermaven, _cx| supermaven.stop()); - } - } - }) - .detach(); - - cx.on_action(|_: &SignOut, cx| { - if let Some(supermaven) = Supermaven::global(cx) { - supermaven.update(cx, |supermaven, _cx| supermaven.sign_out()); - } - }); -} - -pub enum Supermaven { - Starting, - FailedDownload { error: anyhow::Error }, - Spawned(SupermavenAgent), - Error { error: anyhow::Error }, -} - -#[derive(Clone)] -pub enum AccountStatus { - Unknown, - NeedsActivation { activate_url: String }, - Ready, -} - -#[derive(Clone)] -struct SupermavenGlobal(Entity); - -impl Global for SupermavenGlobal {} - -impl Supermaven { - pub fn global(cx: &App) -> Option> { - cx.try_global::() - .map(|model| model.0.clone()) - } - - pub fn set_global(supermaven: Entity, cx: &mut App) { - cx.set_global(SupermavenGlobal(supermaven)); - } - - pub fn start(&mut self, client: Arc, cx: &mut Context) { - if let Self::Starting = self { - cx.spawn(async move |this, cx| { - let binary_path = - supermaven_api::get_supermaven_agent_path(client.http_client()).await?; - - this.update(cx, |this, cx| { - if let Self::Starting = this { - *this = - Self::Spawned(SupermavenAgent::new(binary_path, client.clone(), cx)?); - } - anyhow::Ok(()) - }) - }) - .detach_and_log_err(cx) - } - } - - pub fn stop(&mut self) { - *self = Self::Starting; - } - - pub fn is_enabled(&self) -> bool { - matches!(self, Self::Spawned { .. }) - } - - pub fn complete( - &mut self, - buffer: &Entity, - cursor_position: Anchor, - cx: &App, - ) -> Option { - if let Self::Spawned(agent) = self { - let buffer_id = buffer.entity_id(); - let buffer = buffer.read(cx); - let path = buffer - .file() - .and_then(|file| Some(file.as_local()?.abs_path(cx))) - .unwrap_or_else(|| PathBuf::from("untitled")) - .to_string_lossy() - .to_string(); - let content = buffer.text(); - let offset = cursor_position.to_offset(buffer); - let state_id = agent.next_state_id; - agent.next_state_id.0 += 1; - - let (updates_tx, mut updates_rx) = watch::channel(); - postage::stream::Stream::try_recv(&mut updates_rx).unwrap(); - - agent.states.insert( - state_id, - SupermavenCompletionState { - buffer_id, - prefix_anchor: cursor_position, - prefix_offset: offset, - text: String::new(), - dedent: String::new(), - updates_tx, - }, - ); - // ensure the states map is max 1000 elements - if agent.states.len() > 1000 { - // state id is monotonic so it's sufficient to remove the first element - agent - .states - .remove(&agent.states.keys().next().unwrap().clone()); - } - - let _ = agent - .outgoing_tx - .unbounded_send(OutboundMessage::StateUpdate(StateUpdateMessage { - new_id: state_id.0.to_string(), - updates: vec![ - StateUpdate::FileUpdate(FileUpdateMessage { - path: path.clone(), - content, - }), - StateUpdate::CursorUpdate(CursorPositionUpdateMessage { path, offset }), - ], - })); - - Some(SupermavenCompletion { - id: state_id, - updates: updates_rx, - }) - } else { - None - } - } - - pub fn completion( - &self, - buffer: &Entity, - cursor_position: Anchor, - cx: &App, - ) -> Option<&str> { - if let Self::Spawned(agent) = self { - find_relevant_completion( - &agent.states, - buffer.entity_id(), - &buffer.read(cx).snapshot(), - cursor_position, - ) - } else { - None - } - } - - pub fn sign_out(&mut self) { - if let Self::Spawned(agent) = self { - agent - .outgoing_tx - .unbounded_send(OutboundMessage::Logout) - .ok(); - // The account status will get set to RequiresActivation or Ready when the next - // message from the agent comes in. Until that happens, set the status to Unknown - // to disable the button. - agent.account_status = AccountStatus::Unknown; - } - } -} - -fn find_relevant_completion<'a>( - states: &'a BTreeMap, - buffer_id: EntityId, - buffer: &BufferSnapshot, - cursor_position: Anchor, -) -> Option<&'a str> { - let mut best_completion: Option<&str> = None; - 'completions: for state in states.values() { - if state.buffer_id != buffer_id { - continue; - } - let Some(state_completion) = state.text.strip_prefix(&state.dedent) else { - continue; - }; - - let current_cursor_offset = cursor_position.to_offset(buffer); - if current_cursor_offset < state.prefix_offset { - continue; - } - - let original_cursor_offset = buffer.clip_offset(state.prefix_offset, text::Bias::Left); - let text_inserted_since_completion_request: String = buffer - .text_for_range(original_cursor_offset..current_cursor_offset) - .collect(); - let trimmed_completion = - match state_completion.strip_prefix(&text_inserted_since_completion_request) { - Some(suffix) => suffix, - None => continue 'completions, - }; - - if best_completion.is_some_and(|best| best.len() > trimmed_completion.len()) { - continue; - } - - best_completion = Some(trimmed_completion); - } - best_completion -} - -pub struct SupermavenAgent { - _process: Child, - next_state_id: SupermavenCompletionStateId, - states: BTreeMap, - outgoing_tx: mpsc::UnboundedSender, - _handle_outgoing_messages: Task>, - _handle_incoming_messages: Task>, - pub account_status: AccountStatus, - service_tier: Option, - #[allow(dead_code)] - client: Arc, -} - -impl SupermavenAgent { - fn new( - binary_path: PathBuf, - client: Arc, - cx: &mut Context, - ) -> Result { - let mut process = util::command::new_smol_command(&binary_path) - .arg("stdio") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .kill_on_drop(true) - .spawn() - .context("failed to start the binary")?; - - let stdin = process - .stdin - .take() - .context("failed to get stdin for process")?; - let stdout = process - .stdout - .take() - .context("failed to get stdout for process")?; - - let (outgoing_tx, outgoing_rx) = mpsc::unbounded(); - - cx.spawn({ - let client = client.clone(); - let outgoing_tx = outgoing_tx.clone(); - async move |this, cx| { - let mut status = client.status(); - while let Some(status) = status.next().await { - if status.is_connected() { - let api_key = client.request(proto::GetSupermavenApiKey {}).await?.api_key; - outgoing_tx - .unbounded_send(OutboundMessage::SetApiKey(SetApiKey { api_key })) - .ok(); - this.update(cx, |this, cx| { - if let Supermaven::Spawned(this) = this { - this.account_status = AccountStatus::Ready; - cx.notify(); - } - })?; - break; - } - } - anyhow::Ok(()) - } - }) - .detach(); - - Ok(Self { - _process: process, - next_state_id: SupermavenCompletionStateId::default(), - states: BTreeMap::default(), - outgoing_tx, - _handle_outgoing_messages: cx.spawn(async move |_, _cx| { - Self::handle_outgoing_messages(outgoing_rx, stdin).await - }), - _handle_incoming_messages: cx.spawn(async move |this, cx| { - Self::handle_incoming_messages(this, stdout, cx).await - }), - account_status: AccountStatus::Unknown, - service_tier: None, - client, - }) - } - - async fn handle_outgoing_messages( - mut outgoing: mpsc::UnboundedReceiver, - mut stdin: ChildStdin, - ) -> Result<()> { - while let Some(message) = outgoing.next().await { - let bytes = serde_json::to_vec(&message)?; - stdin.write_all(&bytes).await?; - stdin.write_all(&[b'\n']).await?; - } - Ok(()) - } - - async fn handle_incoming_messages( - this: WeakEntity, - stdout: ChildStdout, - cx: &mut AsyncApp, - ) -> Result<()> { - const MESSAGE_PREFIX: &str = "SM-MESSAGE "; - - let stdout = BufReader::new(stdout); - let mut lines = stdout.lines(); - while let Some(line) = lines.next().await { - let Some(line) = line.context("failed to read line from stdout").log_err() else { - continue; - }; - let Some(line) = line.strip_prefix(MESSAGE_PREFIX) else { - continue; - }; - let Some(message) = serde_json::from_str::(line) - .with_context(|| format!("failed to deserialize line from stdout: {:?}", line)) - .log_err() - else { - continue; - }; - - this.update(cx, |this, _cx| { - if let Supermaven::Spawned(this) = this { - this.handle_message(message); - } - Task::ready(anyhow::Ok(())) - })? - .await?; - } - - Ok(()) - } - - fn handle_message(&mut self, message: SupermavenMessage) { - match message { - SupermavenMessage::ActivationRequest(request) => { - self.account_status = match request.activate_url { - Some(activate_url) => AccountStatus::NeedsActivation { activate_url }, - None => AccountStatus::Ready, - }; - } - SupermavenMessage::ActivationSuccess => { - self.account_status = AccountStatus::Ready; - } - SupermavenMessage::ServiceTier { service_tier } => { - self.account_status = AccountStatus::Ready; - self.service_tier = Some(service_tier); - } - SupermavenMessage::Response(response) => { - let state_id = SupermavenCompletionStateId(response.state_id.parse().unwrap()); - if let Some(state) = self.states.get_mut(&state_id) { - for item in &response.items { - match item { - ResponseItem::Text { text } => state.text.push_str(text), - ResponseItem::Dedent { text } => state.dedent.push_str(text), - _ => {} - } - } - *state.updates_tx.borrow_mut() = (); - } - } - SupermavenMessage::Passthrough { passthrough } => self.handle_message(*passthrough), - _ => { - log::warn!("unhandled message: {:?}", message); - } - } - } -} - -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] -pub struct SupermavenCompletionStateId(usize); - -#[allow(dead_code)] -pub struct SupermavenCompletionState { - buffer_id: EntityId, - prefix_anchor: Anchor, - // prefix_offset is tracked independently because the anchor biases left which - // doesn't allow us to determine if the prior text has been deleted. - prefix_offset: usize, - text: String, - dedent: String, - updates_tx: watch::Sender<()>, -} - -pub struct SupermavenCompletion { - pub id: SupermavenCompletionStateId, - pub updates: watch::Receiver<()>, -} - -#[cfg(test)] -mod tests { - use super::*; - use collections::BTreeMap; - use gpui::TestAppContext; - use language::Buffer; - - #[gpui::test] - async fn test_find_relevant_completion_no_first_letter_skip(cx: &mut TestAppContext) { - let buffer = cx.new(|cx| Buffer::local("hello world", cx)); - let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); - - let mut states = BTreeMap::new(); - let state_id = SupermavenCompletionStateId(1); - let (updates_tx, _) = watch::channel(); - - states.insert( - state_id, - SupermavenCompletionState { - buffer_id: buffer.entity_id(), - prefix_anchor: buffer_snapshot.anchor_before(0), // Start of buffer - prefix_offset: 0, - text: "hello".to_string(), - dedent: String::new(), - updates_tx, - }, - ); - - let cursor_position = buffer_snapshot.anchor_after(1); - - let result = find_relevant_completion( - &states, - buffer.entity_id(), - &buffer_snapshot, - cursor_position, - ); - - assert_eq!(result, Some("ello")); - } - - #[gpui::test] - async fn test_find_relevant_completion_with_multiple_chars(cx: &mut TestAppContext) { - let buffer = cx.new(|cx| Buffer::local("hello world", cx)); - let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); - - let mut states = BTreeMap::new(); - let state_id = SupermavenCompletionStateId(1); - let (updates_tx, _) = watch::channel(); - - states.insert( - state_id, - SupermavenCompletionState { - buffer_id: buffer.entity_id(), - prefix_anchor: buffer_snapshot.anchor_before(0), // Start of buffer - prefix_offset: 0, - text: "hello".to_string(), - dedent: String::new(), - updates_tx, - }, - ); - - let cursor_position = buffer_snapshot.anchor_after(3); - - let result = find_relevant_completion( - &states, - buffer.entity_id(), - &buffer_snapshot, - cursor_position, - ); - - assert_eq!(result, Some("lo")); - } -} diff --git a/crates/supermaven/src/supermaven_edit_prediction_delegate.rs b/crates/supermaven/src/supermaven_edit_prediction_delegate.rs deleted file mode 100644 index 578bc894f2..0000000000 --- a/crates/supermaven/src/supermaven_edit_prediction_delegate.rs +++ /dev/null @@ -1,302 +0,0 @@ -use crate::{Supermaven, SupermavenCompletionStateId}; -use anyhow::Result; -use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate}; -use futures::StreamExt as _; -use gpui::{App, Context, Entity, EntityId, Task}; -use language::{Anchor, Buffer, BufferSnapshot}; -use std::{ - ops::{AddAssign, Range}, - path::Path, - sync::Arc, - time::Duration, -}; -use text::{ToOffset, ToPoint}; -use unicode_segmentation::UnicodeSegmentation; - -pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); - -pub struct SupermavenEditPredictionDelegate { - supermaven: Entity, - buffer_id: Option, - completion_id: Option, - completion_text: Option, - file_extension: Option, - pending_refresh: Option>>, - completion_position: Option, -} - -impl SupermavenEditPredictionDelegate { - pub fn new(supermaven: Entity) -> Self { - Self { - supermaven, - buffer_id: None, - completion_id: None, - completion_text: None, - file_extension: None, - pending_refresh: None, - completion_position: None, - } - } -} - -// Computes the edit prediction from the difference between the completion text. -// This is defined by greedily matching the buffer text against the completion text. -// Inlays are inserted for parts of the completion text that are not present in the buffer text. -// For example, given the completion text "axbyc" and the buffer text "xy", the rendered output in the editor would be "[a]x[b]y[c]". -// The parts in brackets are the inlays. -fn completion_from_diff( - snapshot: BufferSnapshot, - completion_text: &str, - position: Anchor, - delete_range: Range, -) -> EditPrediction { - let buffer_text = snapshot.text_for_range(delete_range).collect::(); - - let mut edits: Vec<(Range, Arc)> = Vec::new(); - - let completion_graphemes: Vec<&str> = completion_text.graphemes(true).collect(); - let buffer_graphemes: Vec<&str> = buffer_text.graphemes(true).collect(); - - let mut offset = position.to_offset(&snapshot); - - let mut i = 0; - let mut j = 0; - while i < completion_graphemes.len() && j < buffer_graphemes.len() { - // find the next instance of the buffer text in the completion text. - let k = completion_graphemes[i..] - .iter() - .position(|c| *c == buffer_graphemes[j]); - match k { - Some(k) => { - if k != 0 { - let offset = snapshot.anchor_after(offset); - // the range from the current position to item is an inlay. - let edit = ( - offset..offset, - completion_graphemes[i..i + k].join("").into(), - ); - edits.push(edit); - } - i += k + 1; - j += 1; - offset.add_assign(buffer_graphemes[j - 1].len()); - } - None => { - // there are no more matching completions, so drop the remaining - // completion text as an inlay. - break; - } - } - } - - if j == buffer_graphemes.len() && i < completion_graphemes.len() { - let offset = snapshot.anchor_after(offset); - // there is leftover completion text, so drop it as an inlay. - let edit_range = offset..offset; - let edit_text = completion_graphemes[i..].join(""); - edits.push((edit_range, edit_text.into())); - } - - EditPrediction::Local { - id: None, - edits, - edit_preview: None, - } -} - -impl EditPredictionDelegate for SupermavenEditPredictionDelegate { - fn name() -> &'static str { - "supermaven" - } - - fn display_name() -> &'static str { - "Supermaven" - } - - fn show_predictions_in_menu() -> bool { - true - } - - fn show_tab_accept_marker() -> bool { - true - } - - fn supports_jump_to_edit() -> bool { - false - } - - fn is_enabled(&self, _buffer: &Entity, _cursor_position: Anchor, cx: &App) -> bool { - self.supermaven.read(cx).is_enabled() - } - - fn is_refreshing(&self, _cx: &App) -> bool { - self.pending_refresh.is_some() && self.completion_id.is_none() - } - - fn refresh( - &mut self, - buffer_handle: Entity, - cursor_position: Anchor, - debounce: bool, - cx: &mut Context, - ) { - // Only make new completion requests when debounce is true (i.e., when text is typed) - // When debounce is false (i.e., cursor movement), we should not make new requests - if !debounce { - return; - } - - reset_completion_cache(self, cx); - - let Some(mut completion) = self.supermaven.update(cx, |supermaven, cx| { - supermaven.complete(&buffer_handle, cursor_position, cx) - }) else { - return; - }; - - self.pending_refresh = Some(cx.spawn(async move |this, cx| { - if debounce { - cx.background_executor().timer(DEBOUNCE_TIMEOUT).await; - } - - while let Some(()) = completion.updates.next().await { - this.update(cx, |this, cx| { - // Get the completion text and cache it - if let Some(text) = - this.supermaven - .read(cx) - .completion(&buffer_handle, cursor_position, cx) - { - this.completion_text = Some(text.to_string()); - - this.completion_position = Some(cursor_position); - } - - this.completion_id = Some(completion.id); - this.buffer_id = Some(buffer_handle.entity_id()); - this.file_extension = buffer_handle.read(cx).file().and_then(|file| { - Some( - Path::new(file.file_name(cx)) - .extension()? - .to_str()? - .to_string(), - ) - }); - cx.notify(); - })?; - } - Ok(()) - })); - } - - fn cycle( - &mut self, - _buffer: Entity, - _cursor_position: Anchor, - _direction: Direction, - _cx: &mut Context, - ) { - } - - fn accept(&mut self, _cx: &mut Context) { - reset_completion_cache(self, _cx); - } - - fn discard(&mut self, _cx: &mut Context) { - reset_completion_cache(self, _cx); - } - - fn suggest( - &mut self, - buffer: &Entity, - cursor_position: Anchor, - cx: &mut Context, - ) -> Option { - if self.buffer_id != Some(buffer.entity_id()) { - return None; - } - - if self.completion_id.is_none() { - return None; - } - - let completion_text = if let Some(cached_text) = &self.completion_text { - cached_text.as_str() - } else { - let text = self - .supermaven - .read(cx) - .completion(buffer, cursor_position, cx)?; - self.completion_text = Some(text.to_string()); - text - }; - - // Check if the cursor is still at the same position as the completion request - // If we don't have a completion position stored, don't show the completion - if let Some(completion_position) = self.completion_position { - if cursor_position != completion_position { - return None; - } - } else { - return None; - } - - let completion_text = trim_to_end_of_line_unless_leading_newline(completion_text); - - let completion_text = completion_text.trim_end(); - - if !completion_text.trim().is_empty() { - let snapshot = buffer.read(cx).snapshot(); - - // Calculate the range from cursor to end of line correctly - let cursor_point = cursor_position.to_point(&snapshot); - let end_of_line = snapshot.anchor_after(language::Point::new( - cursor_point.row, - snapshot.line_len(cursor_point.row), - )); - let delete_range = cursor_position..end_of_line; - - Some(completion_from_diff( - snapshot, - completion_text, - cursor_position, - delete_range, - )) - } else { - None - } - } -} - -fn reset_completion_cache( - provider: &mut SupermavenEditPredictionDelegate, - _cx: &mut Context, -) { - provider.pending_refresh = None; - provider.completion_id = None; - provider.completion_text = None; - provider.completion_position = None; - provider.buffer_id = None; -} - -fn trim_to_end_of_line_unless_leading_newline(text: &str) -> &str { - if has_leading_newline(text) { - text - } else if let Some(i) = text.find('\n') { - &text[..i] - } else { - text - } -} - -fn has_leading_newline(text: &str) -> bool { - for c in text.chars() { - if c == '\n' { - return true; - } - if !c.is_whitespace() { - return false; - } - } - false -} diff --git a/crates/supermaven_api/Cargo.toml b/crates/supermaven_api/Cargo.toml deleted file mode 100644 index 28868a9a74..0000000000 --- a/crates/supermaven_api/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "supermaven_api" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/supermaven_api.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -futures.workspace = true -http_client.workspace = true -paths.workspace = true -serde.workspace = true -serde_json.workspace = true -smol.workspace = true -util.workspace = true diff --git a/crates/supermaven_api/LICENSE-GPL b/crates/supermaven_api/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/supermaven_api/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/supermaven_api/src/supermaven_api.rs b/crates/supermaven_api/src/supermaven_api.rs deleted file mode 100644 index 539826c817..0000000000 --- a/crates/supermaven_api/src/supermaven_api.rs +++ /dev/null @@ -1,291 +0,0 @@ -use anyhow::{Context as _, Result, anyhow}; -use futures::AsyncReadExt; -use futures::io::BufReader; -use http_client::{AsyncBody, HttpClient, Request as HttpRequest}; -use paths::supermaven_dir; -use serde::{Deserialize, Serialize}; -use smol::fs::{self, File}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use util::fs::{make_file_executable, remove_matching}; - -#[derive(Serialize)] -pub struct GetExternalUserRequest { - pub id: String, -} - -#[derive(Serialize)] -pub struct CreateExternalUserRequest { - pub id: String, - pub email: String, -} - -#[derive(Serialize)] -pub struct DeleteExternalUserRequest { - pub id: String, -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateExternalUserResponse { - pub api_key: String, -} - -#[derive(Deserialize)] -pub struct SupermavenApiError { - pub message: String, -} - -pub struct SupermavenBinary {} - -pub struct SupermavenAdminApi { - admin_api_key: String, - api_url: String, - http_client: Arc, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SupermavenDownloadResponse { - pub download_url: String, - pub version: u64, - pub sha256_hash: String, -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SupermavenUser { - #[expect( - unused, - reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove" - )] - id: String, - #[expect( - unused, - reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove" - )] - email: String, - api_key: String, -} - -impl SupermavenAdminApi { - pub fn new(admin_api_key: String, http_client: Arc) -> Self { - Self { - admin_api_key, - api_url: "https://supermaven.com/api/".to_string(), - http_client, - } - } - - pub async fn try_get_user( - &self, - request: GetExternalUserRequest, - ) -> Result> { - let uri = format!("{}external-user/{}", &self.api_url, &request.id); - - let request = HttpRequest::get(&uri).header("Authorization", self.admin_api_key.clone()); - - let mut response = self - .http_client - .send(request.body(AsyncBody::default())?) - .await - .with_context(|| "Unable to get Supermaven API Key".to_string())?; - - let mut body = Vec::new(); - response.body_mut().read_to_end(&mut body).await?; - - if response.status().is_client_error() { - let error: SupermavenApiError = serde_json::from_slice(&body)?; - if error.message == "User not found" { - return Ok(None); - } else { - anyhow::bail!("Supermaven API error: {}", error.message); - } - } else if response.status().is_server_error() { - let error: SupermavenApiError = serde_json::from_slice(&body)?; - return Err(anyhow!("Supermaven API server error").context(error.message)); - } - - let body_str = std::str::from_utf8(&body)?; - - Ok(Some( - serde_json::from_str::(body_str) - .with_context(|| "Unable to parse Supermaven user response".to_string())?, - )) - } - - pub async fn try_create_user( - &self, - request: CreateExternalUserRequest, - ) -> Result { - let uri = format!("{}external-user", &self.api_url); - - let request = HttpRequest::post(&uri) - .header("Authorization", self.admin_api_key.clone()) - .body(AsyncBody::from(serde_json::to_vec(&request)?))?; - - let mut response = self - .http_client - .send(request) - .await - .with_context(|| "Unable to create Supermaven API Key".to_string())?; - - let mut body = Vec::new(); - response.body_mut().read_to_end(&mut body).await?; - - let body_str = std::str::from_utf8(&body)?; - - if !response.status().is_success() { - let error: SupermavenApiError = serde_json::from_slice(&body)?; - return Err(anyhow!("Supermaven API server error").context(error.message)); - } - - serde_json::from_str::(body_str) - .with_context(|| "Unable to parse Supermaven API Key response".to_string()) - } - - pub async fn try_delete_user(&self, request: DeleteExternalUserRequest) -> Result<()> { - let uri = format!("{}external-user/{}", &self.api_url, &request.id); - - let request = HttpRequest::delete(&uri).header("Authorization", self.admin_api_key.clone()); - - let mut response = self - .http_client - .send(request.body(AsyncBody::default())?) - .await - .with_context(|| "Unable to delete Supermaven User".to_string())?; - - let mut body = Vec::new(); - response.body_mut().read_to_end(&mut body).await?; - - if response.status().is_client_error() { - let error: SupermavenApiError = serde_json::from_slice(&body)?; - if error.message == "User not found" { - return Ok(()); - } else { - anyhow::bail!("Supermaven API error: {}", error.message); - } - } else if response.status().is_server_error() { - let error: SupermavenApiError = serde_json::from_slice(&body)?; - return Err(anyhow!("Supermaven API server error").context(error.message)); - } - - Ok(()) - } - - pub async fn try_get_or_create_user( - &self, - request: CreateExternalUserRequest, - ) -> Result { - let get_user_request = GetExternalUserRequest { - id: request.id.clone(), - }; - - match self.try_get_user(get_user_request).await? { - None => self.try_create_user(request).await, - Some(SupermavenUser { api_key, .. }) => Ok(CreateExternalUserResponse { api_key }), - } - } -} - -pub async fn latest_release( - client: Arc, - platform: &str, - arch: &str, -) -> Result { - let uri = format!( - "https://supermaven.com/api/download-path?platform={}&arch={}", - platform, arch - ); - - // Download is not authenticated - let request = HttpRequest::get(&uri); - - let mut response = client - .send(request.body(AsyncBody::default())?) - .await - .with_context(|| "Unable to acquire Supermaven Agent".to_string())?; - - let mut body = Vec::new(); - response.body_mut().read_to_end(&mut body).await?; - - if response.status().is_client_error() || response.status().is_server_error() { - let body_str = std::str::from_utf8(&body)?; - let error: SupermavenApiError = serde_json::from_str(body_str)?; - anyhow::bail!("Supermaven API error: {}", error.message); - } - - serde_json::from_slice::(&body) - .with_context(|| "Unable to parse Supermaven Agent response".to_string()) -} - -pub fn version_path(version: u64) -> PathBuf { - supermaven_dir().join(format!( - "sm-agent-{}{}", - version, - std::env::consts::EXE_SUFFIX - )) -} - -pub async fn has_version(version_path: &Path) -> bool { - fs::metadata(version_path).await.is_ok_and(|m| m.is_file()) -} - -pub async fn get_supermaven_agent_path(client: Arc) -> Result { - fs::create_dir_all(supermaven_dir()) - .await - .with_context(|| { - format!( - "Could not create Supermaven Agent Directory at {:?}", - supermaven_dir() - ) - })?; - - let platform = match std::env::consts::OS { - "macos" => "darwin", - "windows" => "windows", - "linux" => "linux", - unsupported => anyhow::bail!("unsupported platform {unsupported}"), - }; - - let arch = match std::env::consts::ARCH { - "x86_64" => "amd64", - "aarch64" => "arm64", - unsupported => anyhow::bail!("unsupported architecture {unsupported}"), - }; - - let download_info = latest_release(client.clone(), platform, arch).await?; - - let binary_path = version_path(download_info.version); - - if has_version(&binary_path).await { - // Due to an issue with the Supermaven binary not being made executable on - // earlier Zed versions and Supermaven releases not occurring that frequently, - // we ensure here that the found binary is actually executable. - make_file_executable(&binary_path).await?; - - return Ok(binary_path); - } - - let request = HttpRequest::get(&download_info.download_url); - - let mut response = client - .send(request.body(AsyncBody::default())?) - .await - .with_context(|| "Unable to download Supermaven Agent".to_string())?; - - let mut file = File::create(&binary_path) - .await - .with_context(|| format!("Unable to create file at {:?}", binary_path))?; - - futures::io::copy(BufReader::new(response.body_mut()), &mut file) - .await - .with_context(|| format!("Unable to write binary to file at {:?}", binary_path))?; - - make_file_executable(&binary_path).await?; - - remove_matching(supermaven_dir(), |file| file != binary_path).await; - - Ok(binary_path) -} diff --git a/crates/svg_preview/Cargo.toml b/crates/svg_preview/Cargo.toml deleted file mode 100644 index 18f55e28d5..0000000000 --- a/crates/svg_preview/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "svg_preview" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/svg_preview.rs" - -[dependencies] -multi_buffer.workspace = true -file_icons.workspace = true -gpui.workspace = true -language.workspace = true -ui.workspace = true -workspace.workspace = true diff --git a/crates/svg_preview/LICENSE-GPL b/crates/svg_preview/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/svg_preview/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/svg_preview/src/svg_preview.rs b/crates/svg_preview/src/svg_preview.rs deleted file mode 100644 index ca1891394d..0000000000 --- a/crates/svg_preview/src/svg_preview.rs +++ /dev/null @@ -1,26 +0,0 @@ -use gpui::{App, actions}; -use workspace::Workspace; - -pub mod svg_preview_view; - -actions!( - svg, - [ - /// Opens an SVG preview for the current file. - OpenPreview, - /// Opens an SVG preview in a split pane. - OpenPreviewToTheSide, - /// Opens a following SVG preview that syncs with the editor. - OpenFollowingPreview - ] -); - -pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, window, cx| { - let Some(window) = window else { - return; - }; - crate::svg_preview_view::SvgPreviewView::register(workspace, window, cx); - }) - .detach(); -} diff --git a/crates/svg_preview/src/svg_preview_view.rs b/crates/svg_preview/src/svg_preview_view.rs deleted file mode 100644 index a286dba437..0000000000 --- a/crates/svg_preview/src/svg_preview_view.rs +++ /dev/null @@ -1,341 +0,0 @@ -use std::mem; -use std::sync::Arc; - -use file_icons::FileIcons; -use gpui::{ - App, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, - RenderImage, Styled, Subscription, Task, WeakEntity, Window, div, img, -}; -use language::{Buffer, BufferEvent}; -use multi_buffer::MultiBuffer; -use ui::prelude::*; -use workspace::item::Item; -use workspace::{Pane, Workspace}; - -use crate::{OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide}; - -pub struct SvgPreviewView { - focus_handle: FocusHandle, - buffer: Option>, - current_svg: Option, SharedString>>, - _refresh: Task<()>, - _buffer_subscription: Option, - _workspace_subscription: Option, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum SvgPreviewMode { - /// The preview will always show the contents of the provided editor. - Default, - /// The preview will "follow" the last active editor of an SVG file. - Follow, -} - -impl SvgPreviewView { - pub fn new( - mode: SvgPreviewMode, - active_buffer: Entity, - workspace_handle: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - cx.new(|cx| { - let workspace_subscription = if mode == SvgPreviewMode::Follow - && let Some(workspace) = workspace_handle.upgrade() - { - Some(Self::subscribe_to_workspace(workspace, window, cx)) - } else { - None - }; - - let buffer = active_buffer.read_with(cx, |buffer, _cx| buffer.as_singleton()); - - let subscription = buffer - .as_ref() - .map(|buffer| Self::create_buffer_subscription(buffer, window, cx)); - - let mut this = Self { - focus_handle: cx.focus_handle(), - buffer, - current_svg: None, - _buffer_subscription: subscription, - _workspace_subscription: workspace_subscription, - _refresh: Task::ready(()), - }; - this.render_image(window, cx); - - this - }) - } - - fn subscribe_to_workspace( - workspace: Entity, - window: &Window, - cx: &mut Context, - ) -> Subscription { - cx.subscribe_in( - &workspace, - window, - move |this: &mut SvgPreviewView, workspace, event: &workspace::Event, window, cx| { - if let workspace::Event::ActiveItemChanged = event { - let workspace = workspace.read(cx); - if let Some(active_item) = workspace.active_item(cx) - && let Some(buffer) = active_item.downcast::() - && Self::is_svg_file(&buffer, cx) - { - let Some(buffer) = buffer.read(cx).as_singleton() else { - return; - }; - if this.buffer.as_ref() != Some(&buffer) { - this._buffer_subscription = - Some(Self::create_buffer_subscription(&buffer, window, cx)); - this.buffer = Some(buffer); - this.render_image(window, cx); - cx.notify(); - } - } else { - this.set_current(None, window, cx); - } - } - }, - ) - } - - fn render_image(&mut self, window: &Window, cx: &mut Context) { - let Some(buffer) = self.buffer.as_ref() else { - return; - }; - const SCALE_FACTOR: f32 = 1.0; - - let renderer = cx.svg_renderer(); - let content = buffer.read(cx).snapshot(); - let background_task = cx.background_spawn(async move { - renderer.render_single_frame(content.text().as_bytes(), SCALE_FACTOR, true) - }); - - self._refresh = cx.spawn_in(window, async move |this, cx| { - let result = background_task.await; - - this.update_in(cx, |view, window, cx| { - let current = result.map_err(|e| e.to_string().into()); - view.set_current(Some(current), window, cx); - }) - .ok(); - }); - } - - fn set_current( - &mut self, - image: Option, SharedString>>, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(Ok(image)) = mem::replace(&mut self.current_svg, image) { - window.drop_image(image).ok(); - } - cx.notify(); - } - - fn find_existing_preview_item_idx( - pane: &Pane, - buffer: &Entity, - cx: &App, - ) -> Option { - let buffer_id = buffer.entity_id(); - pane.items_of_type::() - .find(|view| { - view.read(cx) - .buffer - .as_ref() - .is_some_and(|buffer| buffer.entity_id() == buffer_id) - }) - .and_then(|view| pane.index_for_item(&view)) - } - - pub fn resolve_active_item_as_svg_buffer( - workspace: &Workspace, - cx: &mut Context, - ) -> Option> { - workspace - .active_item(cx)? - .act_as::(cx) - .filter(|buffer| Self::is_svg_file(&buffer, cx)) - } - - fn create_svg_view( - mode: SvgPreviewMode, - workspace: &mut Workspace, - buffer: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let workspace_handle = workspace.weak_handle(); - SvgPreviewView::new(mode, buffer, workspace_handle, window, cx) - } - - fn create_buffer_subscription( - buffer: &Entity, - window: &Window, - cx: &mut Context, - ) -> Subscription { - cx.subscribe_in( - buffer, - window, - move |this, _buffer, event: &BufferEvent, window, cx| match event { - BufferEvent::Edited | BufferEvent::Saved => { - this.render_image(window, cx); - } - _ => {} - }, - ) - } - - pub fn is_svg_file(buffer: &Entity, cx: &App) -> bool { - buffer - .read(cx) - .as_singleton() - .and_then(|buffer| buffer.read(cx).file()) - .is_some_and(|file| { - file.path() - .extension() - .is_some_and(|ext| ext.eq_ignore_ascii_case("svg")) - }) - } - - pub fn register(workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context) { - workspace.register_action(move |workspace, _: &OpenPreview, window, cx| { - if let Some(buffer) = Self::resolve_active_item_as_svg_buffer(workspace, cx) - && Self::is_svg_file(&buffer, cx) - { - let view = Self::create_svg_view( - SvgPreviewMode::Default, - workspace, - buffer.clone(), - window, - cx, - ); - workspace.active_pane().update(cx, |pane, cx| { - if let Some(existing_view_idx) = - Self::find_existing_preview_item_idx(pane, &buffer, cx) - { - pane.activate_item(existing_view_idx, true, true, window, cx); - } else { - pane.add_item(Box::new(view), true, true, None, window, cx) - } - }); - cx.notify(); - } - }); - - workspace.register_action(move |workspace, _: &OpenPreviewToTheSide, window, cx| { - if let Some(editor) = Self::resolve_active_item_as_svg_buffer(workspace, cx) - && Self::is_svg_file(&editor, cx) - { - let editor_clone = editor.clone(); - let view = Self::create_svg_view( - SvgPreviewMode::Default, - workspace, - editor_clone, - window, - cx, - ); - let pane = workspace - .find_pane_in_direction(workspace::SplitDirection::Right, cx) - .unwrap_or_else(|| { - workspace.split_pane( - workspace.active_pane().clone(), - workspace::SplitDirection::Right, - window, - cx, - ) - }); - pane.update(cx, |pane, cx| { - if let Some(existing_view_idx) = - Self::find_existing_preview_item_idx(pane, &editor, cx) - { - pane.activate_item(existing_view_idx, true, true, window, cx); - } else { - pane.add_item(Box::new(view), false, false, None, window, cx) - } - }); - cx.notify(); - } - }); - - workspace.register_action(move |workspace, _: &OpenFollowingPreview, window, cx| { - if let Some(editor) = Self::resolve_active_item_as_svg_buffer(workspace, cx) - && Self::is_svg_file(&editor, cx) - { - let view = - Self::create_svg_view(SvgPreviewMode::Follow, workspace, editor, window, cx); - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item(Box::new(view), true, true, None, window, cx) - }); - cx.notify(); - } - }); - } -} - -impl Render for SvgPreviewView { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .id("SvgPreview") - .key_context("SvgPreview") - .track_focus(&self.focus_handle(cx)) - .size_full() - .bg(cx.theme().colors().editor_background) - .flex() - .justify_center() - .items_center() - .map(|this| match self.current_svg.clone() { - Some(Ok(image)) => { - this.child(img(image).max_w_full().max_h_full().with_fallback(|| { - h_flex() - .p_4() - .gap_2() - .child(Icon::new(IconName::Warning)) - .child("Failed to load SVG image") - .into_any_element() - })) - } - Some(Err(e)) => this.child(div().p_4().child(e).into_any_element()), - None => this.child(div().p_4().child("No SVG file selected")), - }) - } -} - -impl Focusable for SvgPreviewView { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl EventEmitter<()> for SvgPreviewView {} - -impl Item for SvgPreviewView { - type Event = (); - - fn tab_icon(&self, _window: &Window, cx: &App) -> Option { - self.buffer - .as_ref() - .and_then(|buffer| buffer.read(cx).file()) - .and_then(|file| FileIcons::get_icon(file.path().as_std_path(), cx)) - .map(Icon::from_path) - .or_else(|| Some(Icon::new(IconName::Image))) - } - - fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString { - self.buffer - .as_ref() - .and_then(|svg_path| svg_path.read(cx).file()) - .map(|name| format!("Preview {}", name.file_name(cx)).into()) - .unwrap_or_else(|| "SVG Preview".into()) - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - Some("svg preview: open") - } - - fn to_item_events(_event: &Self::Event, _f: impl FnMut(workspace::item::ItemEvent)) {} -} diff --git a/crates/system_specs/Cargo.toml b/crates/system_specs/Cargo.toml deleted file mode 100644 index 15d6822b38..0000000000 --- a/crates/system_specs/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "system_specs" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/system_specs.rs" - -[features] -default = [] - -[dependencies] -anyhow.workspace = true -client.workspace = true -gpui.workspace = true -human_bytes.workspace = true -release_channel.workspace = true -semver.workspace = true -serde.workspace = true -sysinfo.workspace = true - -[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies] -pciid-parser.workspace = true diff --git a/crates/system_specs/LICENSE-GPL b/crates/system_specs/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/system_specs/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/system_specs/src/system_specs.rs b/crates/system_specs/src/system_specs.rs deleted file mode 100644 index 139f23d193..0000000000 --- a/crates/system_specs/src/system_specs.rs +++ /dev/null @@ -1,295 +0,0 @@ -use client::telemetry; -pub use gpui::GpuSpecs; -use gpui::{App, AppContext as _, Task, Window, actions}; -use human_bytes::human_bytes; -use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; -use semver::Version; -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, - release_channel: &'static str, - os_name: String, - os_version: String, - memory: u64, - architecture: &'static str, - commit_sha: Option, - bundle_type: Option, - gpu_specs: Option, -} - -impl SystemSpecs { - pub fn new(window: &mut Window, cx: &mut App) -> Task { - let app_version = AppVersion::global(cx).to_string(); - let release_channel = ReleaseChannel::global(cx); - let os_name = telemetry::os_name(); - let system = System::new_with_specifics( - RefreshKind::nothing().with_memory(MemoryRefreshKind::everything()), - ); - let memory = system.total_memory(); - let architecture = env::consts::ARCH; - let commit_sha = match release_channel { - ReleaseChannel::Dev | ReleaseChannel::Nightly => { - AppCommitSha::try_global(cx).map(|sha| sha.full()) - } - _ => None, - }; - let bundle_type = bundle_type(); - - let gpu_specs = window.gpu_specs().map(|specs| { - format!( - "{} || {} || {}", - specs.device_name, specs.driver_name, specs.driver_info - ) - }); - - cx.background_spawn(async move { - let os_version = telemetry::os_version(); - SystemSpecs { - app_version, - release_channel: release_channel.display_name(), - bundle_type, - os_name, - os_version, - memory, - architecture, - commit_sha, - gpu_specs, - } - }) - } - - pub fn new_stateless( - app_version: Version, - app_commit_sha: Option, - release_channel: ReleaseChannel, - ) -> Self { - let os_name = telemetry::os_name(); - let os_version = telemetry::os_version(); - let system = System::new_with_specifics( - RefreshKind::nothing().with_memory(MemoryRefreshKind::everything()), - ); - let memory = system.total_memory(); - let architecture = env::consts::ARCH; - let commit_sha = match release_channel { - ReleaseChannel::Dev | ReleaseChannel::Nightly => app_commit_sha.map(|sha| sha.full()), - _ => None, - }; - let bundle_type = bundle_type(); - - Self { - app_version: app_version.to_string(), - release_channel: release_channel.display_name(), - os_name, - os_version, - memory, - architecture, - commit_sha, - bundle_type, - gpu_specs: try_determine_available_gpus(), - } - } -} - -impl Display for SystemSpecs { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let os_information = format!("OS: {} {}", self.os_name, self.os_version); - let app_version_information = format!( - "Zed: v{} ({}) {}{}", - self.app_version, - match &self.commit_sha { - Some(commit_sha) => format!("{} {}", self.release_channel, commit_sha), - None => self.release_channel.to_string(), - }, - if let Some(bundle_type) = &self.bundle_type { - format!("({bundle_type})") - } else { - "".to_string() - }, - if cfg!(debug_assertions) { - "(Taylor's Version)" - } else { - "" - }, - ); - let system_specs = [ - app_version_information, - os_information, - format!("Memory: {}", human_bytes(self.memory as f64)), - format!("Architecture: {}", self.architecture), - ] - .into_iter() - .chain( - self.gpu_specs - .as_ref() - .map(|specs| format!("GPU: {}", specs)), - ) - .collect::>() - .join("\n"); - - write!(f, "{system_specs}") - } -} - -fn try_determine_available_gpus() -> Option { - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - { - #[allow( - clippy::disallowed_methods, - reason = "we are not running in an executor" - )] - std::process::Command::new("vulkaninfo") - .args(&["--summary"]) - .output() - .ok() - .map(|output| { - [ - "
`vulkaninfo --summary` output", - "", - "```", - String::from_utf8_lossy(&output.stdout).as_ref(), - "```", - "
", - ] - .join("\n") - }) - .or(Some("Failed to run `vulkaninfo --summary`".to_string())) - } - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - { - 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 -/// runtime environment variable. -/// -/// The runtime value is used by snap since the Zed snaps use release binaries directly, and so -/// cannot have this baked in. -fn bundle_type() -> Option { - option_env!("ZED_BUNDLE_TYPE") - .map(|bundle_type| bundle_type.to_string()) - .or_else(|| env::var("ZED_BUNDLE_TYPE").ok()) -} diff --git a/crates/tab_switcher/Cargo.toml b/crates/tab_switcher/Cargo.toml deleted file mode 100644 index 36e4ba7734..0000000000 --- a/crates/tab_switcher/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "tab_switcher" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/tab_switcher.rs" -doctest = false - -[dependencies] -collections.workspace = true -editor.workspace = true -fuzzy.workspace = true -gpui.workspace = true -menu.workspace = true -picker.workspace = true -project.workspace = true -schemars.workspace = true -serde.workspace = true -settings.workspace = true -smol.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true - -[dev-dependencies] -anyhow.workspace = true -ctor.workspace = true -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -serde_json.workspace = true -theme = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } -zlog.workspace = true diff --git a/crates/tab_switcher/LICENSE-GPL b/crates/tab_switcher/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/tab_switcher/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs deleted file mode 100644 index 85186ad504..0000000000 --- a/crates/tab_switcher/src/tab_switcher.rs +++ /dev/null @@ -1,758 +0,0 @@ -#[cfg(test)] -mod tab_switcher_tests; - -use collections::HashMap; -use editor::items::{ - entry_diagnostic_aware_icon_decoration_and_color, entry_git_aware_label_color, -}; -use fuzzy::StringMatchCandidate; -use gpui::{ - Action, AnyElement, App, Context, DismissEvent, Entity, EntityId, EventEmitter, FocusHandle, - Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Point, - Render, Styled, Task, WeakEntity, Window, actions, rems, -}; -use picker::{Picker, PickerDelegate}; -use project::Project; -use schemars::JsonSchema; -use serde::Deserialize; -use settings::Settings; -use std::{cmp::Reverse, sync::Arc}; -use ui::{ - DecoratedIcon, IconDecoration, IconDecorationKind, ListItem, ListItemSpacing, Tooltip, - prelude::*, -}; -use util::ResultExt; -use workspace::{ - Event as WorkspaceEvent, ModalView, Pane, SaveIntent, Workspace, - item::{ItemHandle, ItemSettings, ShowDiagnostics, TabContentParams}, - pane::{render_item_indicator, tab_details}, -}; - -const PANEL_WIDTH_REMS: f32 = 28.; - -/// Toggles the tab switcher interface. -#[derive(PartialEq, Clone, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = tab_switcher)] -#[serde(deny_unknown_fields)] -pub struct Toggle { - #[serde(default)] - pub select_last: bool, -} -actions!( - tab_switcher, - [ - /// Closes the selected item in the tab switcher. - CloseSelectedItem, - /// Toggles between showing all tabs or just the current pane's tabs. - ToggleAll - ] -); - -pub struct TabSwitcher { - picker: Entity>, - init_modifiers: Option, -} - -impl ModalView for TabSwitcher {} - -pub fn init(cx: &mut App) { - cx.observe_new(TabSwitcher::register).detach(); -} - -impl TabSwitcher { - fn register( - workspace: &mut Workspace, - _window: Option<&mut Window>, - _: &mut Context, - ) { - workspace.register_action(|workspace, action: &Toggle, window, cx| { - let Some(tab_switcher) = workspace.active_modal::(cx) else { - Self::open(workspace, action.select_last, false, window, cx); - return; - }; - - tab_switcher.update(cx, |tab_switcher, cx| { - tab_switcher - .picker - .update(cx, |picker, cx| picker.cycle_selection(window, cx)) - }); - }); - workspace.register_action(|workspace, _action: &ToggleAll, window, cx| { - let Some(tab_switcher) = workspace.active_modal::(cx) else { - Self::open(workspace, false, true, window, cx); - return; - }; - - tab_switcher.update(cx, |tab_switcher, cx| { - tab_switcher - .picker - .update(cx, |picker, cx| picker.cycle_selection(window, cx)) - }); - }); - } - - fn open( - workspace: &mut Workspace, - select_last: bool, - is_global: bool, - window: &mut Window, - cx: &mut Context, - ) { - let mut weak_pane = workspace.active_pane().downgrade(); - for dock in [ - workspace.left_dock(), - workspace.bottom_dock(), - workspace.right_dock(), - ] { - dock.update(cx, |this, cx| { - let Some(panel) = this - .active_panel() - .filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx)) - else { - return; - }; - if let Some(pane) = panel.pane(cx) { - weak_pane = pane.downgrade(); - } - }) - } - - let weak_workspace = workspace.weak_handle(); - - let project = workspace.project().clone(); - let original_items: Vec<_> = workspace - .panes() - .iter() - .map(|p| (p.clone(), p.read(cx).active_item_index())) - .collect(); - workspace.toggle_modal(window, cx, |window, cx| { - let delegate = TabSwitcherDelegate::new( - project, - select_last, - cx.entity().downgrade(), - weak_pane, - weak_workspace, - is_global, - window, - cx, - original_items, - ); - TabSwitcher::new(delegate, window, is_global, cx) - }); - } - - fn new( - delegate: TabSwitcherDelegate, - window: &mut Window, - is_global: bool, - cx: &mut Context, - ) -> Self { - let init_modifiers = if is_global { - None - } else { - window.modifiers().modified().then_some(window.modifiers()) - }; - Self { - picker: cx.new(|cx| { - if is_global { - Picker::list(delegate, window, cx) - } else { - Picker::nonsearchable_list(delegate, window, cx) - } - }), - init_modifiers, - } - } - - fn handle_modifiers_changed( - &mut self, - event: &ModifiersChangedEvent, - window: &mut Window, - cx: &mut Context, - ) { - let Some(init_modifiers) = self.init_modifiers else { - return; - }; - if !event.modified() || !init_modifiers.is_subset_of(event) { - self.init_modifiers = None; - if self.picker.read(cx).delegate.matches.is_empty() { - cx.emit(DismissEvent) - } else { - window.dispatch_action(menu::Confirm.boxed_clone(), cx); - } - } - } - - fn handle_close_selected_item( - &mut self, - _: &CloseSelectedItem, - window: &mut Window, - cx: &mut Context, - ) { - self.picker.update(cx, |picker, cx| { - picker - .delegate - .close_item_at(picker.delegate.selected_index(), window, cx) - }); - } -} - -impl EventEmitter for TabSwitcher {} - -impl Focusable for TabSwitcher { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl Render for TabSwitcher { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .key_context("TabSwitcher") - .w(rems(PANEL_WIDTH_REMS)) - .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) - .on_action(cx.listener(Self::handle_close_selected_item)) - .child(self.picker.clone()) - } -} - -#[derive(Clone)] -struct TabMatch { - pane: WeakEntity, - item_index: usize, - item: Box, - detail: usize, - preview: bool, -} - -pub struct TabSwitcherDelegate { - select_last: bool, - tab_switcher: WeakEntity, - selected_index: usize, - pane: WeakEntity, - workspace: WeakEntity, - project: Entity, - matches: Vec, - original_items: Vec<(Entity, usize)>, - is_all_panes: bool, - restored_items: bool, -} - -impl TabMatch { - fn icon( - &self, - project: &Entity, - selected: bool, - window: &Window, - cx: &App, - ) -> Option { - let icon = self.item.tab_icon(window, cx)?; - let item_settings = ItemSettings::get_global(cx); - let show_diagnostics = item_settings.show_diagnostics; - let git_status_color = item_settings - .git_status - .then(|| { - let path = self.item.project_path(cx)?; - let project = project.read(cx); - let entry = project.entry_for_path(&path, cx)?; - let git_status = project - .project_path_git_status(&path, cx) - .map(|status| status.summary()) - .unwrap_or_default(); - Some(entry_git_aware_label_color( - git_status, - entry.is_ignored, - selected, - )) - }) - .flatten(); - let colored_icon = icon.color(git_status_color.unwrap_or_default()); - - let most_severe_diagnostic_level = if show_diagnostics == ShowDiagnostics::Off { - None - } else { - let buffer_store = project.read(cx).buffer_store().read(cx); - let buffer = self - .item - .project_path(cx) - .and_then(|path| buffer_store.get_by_path(&path)) - .map(|buffer| buffer.read(cx)); - buffer.and_then(|buffer| { - buffer - .buffer_diagnostics(None) - .iter() - .map(|diagnostic_entry| diagnostic_entry.diagnostic.severity) - .min() - }) - }; - - let decorations = - entry_diagnostic_aware_icon_decoration_and_color(most_severe_diagnostic_level) - .filter(|(d, _)| { - *d != IconDecorationKind::Triangle - || show_diagnostics != ShowDiagnostics::Errors - }) - .map(|(icon, color)| { - let knockout_item_color = if selected { - cx.theme().colors().element_selected - } else { - cx.theme().colors().element_background - }; - IconDecoration::new(icon, knockout_item_color, cx) - .color(color.color(cx)) - .position(Point { - x: px(-2.), - y: px(-2.), - }) - }); - Some(DecoratedIcon::new(colored_icon, decorations)) - } -} - -impl TabSwitcherDelegate { - #[allow(clippy::complexity)] - fn new( - project: Entity, - select_last: bool, - tab_switcher: WeakEntity, - pane: WeakEntity, - workspace: WeakEntity, - is_all_panes: bool, - window: &mut Window, - cx: &mut Context, - original_items: Vec<(Entity, usize)>, - ) -> Self { - Self::subscribe_to_updates(&workspace, window, cx); - Self { - select_last, - tab_switcher, - selected_index: 0, - pane, - workspace, - project, - matches: Vec::new(), - is_all_panes, - original_items, - restored_items: false, - } - } - - fn subscribe_to_updates( - workspace: &WeakEntity, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = workspace.upgrade() else { - return; - }; - cx.subscribe_in(&workspace, window, |tab_switcher, _, event, window, cx| { - match event { - WorkspaceEvent::ItemAdded { .. } | WorkspaceEvent::PaneRemoved => { - tab_switcher.picker.update(cx, |picker, cx| { - let query = picker.query(cx); - picker.delegate.update_matches(query, window, cx); - cx.notify(); - }) - } - WorkspaceEvent::ItemRemoved { .. } => { - tab_switcher.picker.update(cx, |picker, cx| { - let query = picker.query(cx); - picker.delegate.update_matches(query, window, cx); - - // When the Tab Switcher is being used and an item is - // removed, there's a chance that the new selected index - // will not match the actual tab that is now being displayed - // by the pane, as such, the selected index needs to be - // updated to match the pane's state. - picker.delegate.sync_selected_index(cx); - cx.notify(); - }) - } - _ => {} - }; - }) - .detach(); - } - - fn update_all_pane_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) { - let Some(workspace) = self.workspace.upgrade() else { - return; - }; - let mut all_items = Vec::new(); - let mut item_index = 0; - for pane_handle in workspace.read(cx).panes() { - let pane = pane_handle.read(cx); - let items: Vec> = - pane.items().map(|item| item.boxed_clone()).collect(); - for ((_detail, item), detail) in items - .iter() - .enumerate() - .zip(tab_details(&items, window, cx)) - { - all_items.push(TabMatch { - pane: pane_handle.downgrade(), - item_index, - item: item.clone(), - detail, - preview: pane.is_active_preview_item(item.item_id()), - }); - item_index += 1; - } - } - - let matches = if query.is_empty() { - let history = workspace.read(cx).recently_activated_items(cx); - all_items - .sort_by_key(|tab| (Reverse(history.get(&tab.item.item_id())), tab.item_index)); - all_items - } else { - let candidates = all_items - .iter() - .enumerate() - .flat_map(|(ix, tab_match)| { - Some(StringMatchCandidate::new( - ix, - &tab_match.item.tab_content_text(0, cx), - )) - }) - .collect::>(); - smol::block_on(fuzzy::match_strings( - &candidates, - &query, - true, - true, - 10000, - &Default::default(), - cx.background_executor().clone(), - )) - .into_iter() - .map(|m| all_items[m.candidate_id].clone()) - .collect() - }; - - let selected_item_id = self.selected_item_id(); - self.matches = matches; - self.selected_index = self.compute_selected_index(selected_item_id, window, cx); - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) { - if self.is_all_panes { - // needed because we need to borrow the workspace, but that may be borrowed when the picker - // calls update_matches. - let this = cx.entity(); - window.defer(cx, move |window, cx| { - this.update(cx, |this, cx| { - this.delegate.update_all_pane_matches(query, window, cx); - }) - }); - return; - } - let selected_item_id = self.selected_item_id(); - self.matches.clear(); - let Some(pane) = self.pane.upgrade() else { - return; - }; - - let pane = pane.read(cx); - let mut history_indices = HashMap::default(); - pane.activation_history().iter().rev().enumerate().for_each( - |(history_index, history_entry)| { - history_indices.insert(history_entry.entity_id, history_index); - }, - ); - - let items: Vec> = pane.items().map(|item| item.boxed_clone()).collect(); - items - .iter() - .enumerate() - .zip(tab_details(&items, window, cx)) - .map(|((item_index, item), detail)| TabMatch { - pane: self.pane.clone(), - item_index, - item: item.boxed_clone(), - detail, - preview: pane.is_active_preview_item(item.item_id()), - }) - .for_each(|tab_match| self.matches.push(tab_match)); - - let non_history_base = history_indices.len(); - self.matches.sort_by(move |a, b| { - let a_score = *history_indices - .get(&a.item.item_id()) - .unwrap_or(&(a.item_index + non_history_base)); - let b_score = *history_indices - .get(&b.item.item_id()) - .unwrap_or(&(b.item_index + non_history_base)); - a_score.cmp(&b_score) - }); - - self.selected_index = self.compute_selected_index(selected_item_id, window, cx); - } - - fn selected_item_id(&self) -> Option { - self.matches - .get(self.selected_index()) - .map(|tab_match| tab_match.item.item_id()) - } - - fn compute_selected_index( - &mut self, - prev_selected_item_id: Option, - window: &mut Window, - cx: &mut Context>, - ) -> usize { - if self.matches.is_empty() { - return 0; - } - - if let Some(selected_item_id) = prev_selected_item_id { - // If the previously selected item is still in the list, select its new position. - if let Some(item_index) = self - .matches - .iter() - .position(|tab_match| tab_match.item.item_id() == selected_item_id) - { - return item_index; - } - // Otherwise, try to preserve the previously selected index. - return self.selected_index.min(self.matches.len() - 1); - } - - if self.select_last { - return self.matches.len() - 1; - } - - // This only runs when initially opening the picker - // Index 0 is already active, so don't preselect it for switching. - if self.matches.len() > 1 { - self.set_selected_index(1, window, cx); - return 1; - } - - 0 - } - - fn close_item_at( - &mut self, - ix: usize, - window: &mut Window, - cx: &mut Context>, - ) { - let Some(tab_match) = self.matches.get(ix) else { - return; - }; - let Some(pane) = tab_match.pane.upgrade() else { - return; - }; - - pane.update(cx, |pane, cx| { - pane.close_item_by_id(tab_match.item.item_id(), SaveIntent::Close, window, cx) - .detach_and_log_err(cx); - }); - } - - /// Updates the selected index to ensure it matches the pane's active item, - /// as the pane's active item can be indirectly updated and this method - /// ensures that the picker can react to those changes. - fn sync_selected_index(&mut self, cx: &mut Context>) { - let item = if self.is_all_panes { - self.workspace - .read_with(cx, |workspace, cx| workspace.active_item(cx)) - } else { - self.pane.read_with(cx, |pane, _cx| pane.active_item()) - }; - - let Ok(Some(item)) = item else { - return; - }; - - let item_id = item.item_id(); - let Some((index, _tab_match)) = self - .matches - .iter() - .enumerate() - .find(|(_index, tab_match)| tab_match.item.item_id() == item_id) - else { - return; - }; - - self.selected_index = index; - } -} - -impl PickerDelegate for TabSwitcherDelegate { - type ListItem = ListItem; - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Search all tabs…".into() - } - - fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option { - Some("No tabs".into()) - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index( - &mut self, - ix: usize, - window: &mut Window, - cx: &mut Context>, - ) { - self.selected_index = ix; - - let Some(selected_match) = self.matches.get(self.selected_index()) else { - return; - }; - selected_match - .pane - .update(cx, |pane, cx| { - if let Some(index) = pane.index_for_item(selected_match.item.as_ref()) { - pane.activate_item(index, false, false, window, cx); - } - }) - .ok(); - cx.notify(); - } - - fn separators_after_indices(&self) -> Vec { - Vec::new() - } - - fn update_matches( - &mut self, - raw_query: String, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - self.update_matches(raw_query, window, cx); - Task::ready(()) - } - - fn confirm( - &mut self, - _secondary: bool, - window: &mut Window, - cx: &mut Context>, - ) { - let Some(selected_match) = self.matches.get(self.selected_index()) else { - return; - }; - - self.restored_items = true; - for (pane, index) in self.original_items.iter() { - pane.update(cx, |this, cx| { - this.activate_item(*index, false, false, window, cx); - }) - } - selected_match - .pane - .update(cx, |pane, cx| { - if let Some(index) = pane.index_for_item(selected_match.item.as_ref()) { - pane.activate_item(index, true, true, window, cx); - } - }) - .ok(); - } - - fn dismissed(&mut self, window: &mut Window, cx: &mut Context>) { - if !self.restored_items { - for (pane, index) in self.original_items.iter() { - pane.update(cx, |this, cx| { - this.activate_item(*index, false, false, window, cx); - }) - } - } - - self.tab_switcher - .update(cx, |_, cx| cx.emit(DismissEvent)) - .log_err(); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - window: &mut Window, - cx: &mut Context>, - ) -> Option { - let tab_match = self.matches.get(ix)?; - - let params = TabContentParams { - detail: Some(tab_match.detail), - selected: true, - preview: tab_match.preview, - deemphasized: false, - }; - let label = tab_match.item.tab_content(params, window, cx); - - let icon = tab_match.icon(&self.project, selected, window, cx); - - let indicator = render_item_indicator(tab_match.item.boxed_clone(), cx); - let indicator_color = if let Some(ref indicator) = indicator { - indicator.color - } else { - Color::default() - }; - let indicator = h_flex() - .flex_shrink_0() - .children(indicator) - .child(div().w_2()) - .into_any_element(); - let close_button = div() - .id("close-button") - .on_mouse_up( - // We need this on_mouse_up here because on macOS you may have ctrl held - // down to open the menu, and a ctrl-click comes through as a right click. - MouseButton::Right, - cx.listener(move |picker, _: &MouseUpEvent, window, cx| { - cx.stop_propagation(); - picker.delegate.close_item_at(ix, window, cx); - }), - ) - .child( - IconButton::new("close_tab", IconName::Close) - .icon_size(IconSize::Small) - .icon_color(indicator_color) - .tooltip(Tooltip::for_action_title("Close", &CloseSelectedItem)) - .on_click(cx.listener(move |picker, _, window, cx| { - cx.stop_propagation(); - picker.delegate.close_item_at(ix, window, cx); - })), - ) - .into_any_element(); - - Some( - ListItem::new(ix) - .spacing(ListItemSpacing::Sparse) - .inset(true) - .toggle_state(selected) - .child(h_flex().w_full().child(label)) - .start_slot::(icon) - .map(|el| { - if self.selected_index == ix { - el.end_slot::(close_button) - } else { - el.end_slot::(indicator) - .end_hover_slot::(close_button) - } - }), - ) - } -} diff --git a/crates/tab_switcher/src/tab_switcher_tests.rs b/crates/tab_switcher/src/tab_switcher_tests.rs deleted file mode 100644 index 85177f29ed..0000000000 --- a/crates/tab_switcher/src/tab_switcher_tests.rs +++ /dev/null @@ -1,345 +0,0 @@ -use super::*; -use editor::Editor; -use gpui::{TestAppContext, VisualTestContext}; -use menu::SelectPrevious; -use project::{Project, ProjectPath}; -use serde_json::json; -use util::{path, rel_path::rel_path}; -use workspace::{ActivatePreviousItem, AppState, Workspace}; - -#[ctor::ctor] -fn init_logger() { - zlog::init_test(); -} - -#[gpui::test] -async fn test_open_with_prev_tab_selected_and_cycle_on_toggle_action( - cx: &mut gpui::TestAppContext, -) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "1.txt": "First file", - "2.txt": "Second file", - "3.txt": "Third file", - "4.txt": "Fourth file", - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let tab_1 = open_buffer("1.txt", &workspace, cx).await; - let tab_2 = open_buffer("2.txt", &workspace, cx).await; - let tab_3 = open_buffer("3.txt", &workspace, cx).await; - let tab_4 = open_buffer("4.txt", &workspace, cx).await; - - // Starts with the previously opened item selected - let tab_switcher = open_tab_switcher(false, &workspace, cx); - tab_switcher.update(cx, |tab_switcher, _| { - assert_eq!(tab_switcher.delegate.matches.len(), 4); - assert_match_at_position(tab_switcher, 0, tab_4.boxed_clone()); - assert_match_selection(tab_switcher, 1, tab_3.boxed_clone()); - assert_match_at_position(tab_switcher, 2, tab_2.boxed_clone()); - assert_match_at_position(tab_switcher, 3, tab_1.boxed_clone()); - }); - - cx.dispatch_action(Toggle { select_last: false }); - cx.dispatch_action(Toggle { select_last: false }); - tab_switcher.update(cx, |tab_switcher, _| { - assert_eq!(tab_switcher.delegate.matches.len(), 4); - assert_match_at_position(tab_switcher, 0, tab_4.boxed_clone()); - assert_match_at_position(tab_switcher, 1, tab_3.boxed_clone()); - assert_match_at_position(tab_switcher, 2, tab_2.boxed_clone()); - assert_match_selection(tab_switcher, 3, tab_1.boxed_clone()); - }); - - cx.dispatch_action(SelectPrevious); - tab_switcher.update(cx, |tab_switcher, _| { - assert_eq!(tab_switcher.delegate.matches.len(), 4); - assert_match_at_position(tab_switcher, 0, tab_4.boxed_clone()); - assert_match_at_position(tab_switcher, 1, tab_3.boxed_clone()); - assert_match_selection(tab_switcher, 2, tab_2.boxed_clone()); - assert_match_at_position(tab_switcher, 3, tab_1.boxed_clone()); - }); -} - -#[gpui::test] -async fn test_open_with_last_tab_selected(cx: &mut gpui::TestAppContext) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "1.txt": "First file", - "2.txt": "Second file", - "3.txt": "Third file", - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let tab_1 = open_buffer("1.txt", &workspace, cx).await; - let tab_2 = open_buffer("2.txt", &workspace, cx).await; - let tab_3 = open_buffer("3.txt", &workspace, cx).await; - - // Starts with the last item selected - let tab_switcher = open_tab_switcher(true, &workspace, cx); - tab_switcher.update(cx, |tab_switcher, _| { - assert_eq!(tab_switcher.delegate.matches.len(), 3); - assert_match_at_position(tab_switcher, 0, tab_3); - assert_match_at_position(tab_switcher, 1, tab_2); - assert_match_selection(tab_switcher, 2, tab_1); - }); -} - -#[gpui::test] -async fn test_open_item_on_modifiers_release(cx: &mut gpui::TestAppContext) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "1.txt": "First file", - "2.txt": "Second file", - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let tab_1 = open_buffer("1.txt", &workspace, cx).await; - let tab_2 = open_buffer("2.txt", &workspace, cx).await; - - cx.simulate_modifiers_change(Modifiers::control()); - let tab_switcher = open_tab_switcher(false, &workspace, cx); - tab_switcher.update(cx, |tab_switcher, _| { - assert_eq!(tab_switcher.delegate.matches.len(), 2); - assert_match_at_position(tab_switcher, 0, tab_2.boxed_clone()); - assert_match_selection(tab_switcher, 1, tab_1.boxed_clone()); - }); - - cx.simulate_modifiers_change(Modifiers::none()); - cx.read(|cx| { - let active_editor = workspace.read(cx).active_item_as::(cx).unwrap(); - assert_eq!(active_editor.read(cx).title(cx), "1.txt"); - }); - assert_tab_switcher_is_closed(workspace, cx); -} - -#[gpui::test] -async fn test_open_on_empty_pane(cx: &mut gpui::TestAppContext) { - let app_state = init_test(cx); - app_state.fs.as_fake().insert_tree("/root", json!({})).await; - - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - cx.simulate_modifiers_change(Modifiers::control()); - let tab_switcher = open_tab_switcher(false, &workspace, cx); - tab_switcher.update(cx, |tab_switcher, _| { - assert!(tab_switcher.delegate.matches.is_empty()); - }); - - cx.simulate_modifiers_change(Modifiers::none()); - assert_tab_switcher_is_closed(workspace, cx); -} - -#[gpui::test] -async fn test_open_with_single_item(cx: &mut gpui::TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree(path!("/root"), json!({"1.txt": "Single file"})) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let tab = open_buffer("1.txt", &workspace, cx).await; - - let tab_switcher = open_tab_switcher(false, &workspace, cx); - tab_switcher.update(cx, |tab_switcher, _| { - assert_eq!(tab_switcher.delegate.matches.len(), 1); - assert_match_selection(tab_switcher, 0, tab); - }); -} - -#[gpui::test] -async fn test_close_selected_item(cx: &mut gpui::TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "1.txt": "First file", - "2.txt": "Second file", - "3.txt": "Third file", - "4.txt": "Fourth file", - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let tab_1 = open_buffer("1.txt", &workspace, cx).await; - let tab_3 = open_buffer("3.txt", &workspace, cx).await; - let tab_2 = open_buffer("2.txt", &workspace, cx).await; - let tab_4 = open_buffer("4.txt", &workspace, cx).await; - - // After opening all buffers, let's navigate to the previous item two times, finishing with: - // - // 1.txt | [3.txt] | 2.txt | 4.txt - // - // With 3.txt being the active item in the pane. - cx.dispatch_action(ActivatePreviousItem); - cx.dispatch_action(ActivatePreviousItem); - cx.run_until_parked(); - - cx.simulate_modifiers_change(Modifiers::control()); - let tab_switcher = open_tab_switcher(false, &workspace, cx); - tab_switcher.update(cx, |tab_switcher, _| { - assert_eq!(tab_switcher.delegate.matches.len(), 4); - assert_match_at_position(tab_switcher, 0, tab_3.boxed_clone()); - assert_match_selection(tab_switcher, 1, tab_2.boxed_clone()); - assert_match_at_position(tab_switcher, 2, tab_4.boxed_clone()); - assert_match_at_position(tab_switcher, 3, tab_1.boxed_clone()); - }); - - cx.simulate_modifiers_change(Modifiers::control()); - cx.dispatch_action(CloseSelectedItem); - tab_switcher.update(cx, |tab_switcher, _| { - assert_eq!(tab_switcher.delegate.matches.len(), 3); - assert_match_selection(tab_switcher, 0, tab_3); - assert_match_at_position(tab_switcher, 1, tab_4); - assert_match_at_position(tab_switcher, 2, tab_1); - }); - - // Still switches tab on modifiers release - cx.simulate_modifiers_change(Modifiers::none()); - cx.read(|cx| { - let active_editor = workspace.read(cx).active_item_as::(cx).unwrap(); - assert_eq!(active_editor.read(cx).title(cx), "3.txt"); - }); - assert_tab_switcher_is_closed(workspace, cx); -} - -fn init_test(cx: &mut TestAppContext) -> Arc { - cx.update(|cx| { - let state = AppState::test(cx); - theme::init(theme::LoadThemes::JustBase, cx); - super::init(cx); - editor::init(cx); - state - }) -} - -#[track_caller] -fn open_tab_switcher( - select_last: bool, - workspace: &Entity, - cx: &mut VisualTestContext, -) -> Entity> { - cx.dispatch_action(Toggle { select_last }); - get_active_tab_switcher(workspace, cx) -} - -#[track_caller] -fn get_active_tab_switcher( - workspace: &Entity, - cx: &mut VisualTestContext, -) -> Entity> { - workspace.update(cx, |workspace, cx| { - workspace - .active_modal::(cx) - .expect("tab switcher is not open") - .read(cx) - .picker - .clone() - }) -} - -async fn open_buffer( - file_path: &str, - workspace: &Entity, - cx: &mut gpui::VisualTestContext, -) -> Box { - let project = workspace.read_with(cx, |workspace, _| workspace.project().clone()); - let worktree_id = project.update(cx, |project, cx| { - let worktree = project.worktrees(cx).last().expect("worktree not found"); - worktree.read(cx).id() - }); - let project_path = ProjectPath { - worktree_id, - path: rel_path(file_path).into(), - }; - workspace - .update_in(cx, move |workspace, window, cx| { - workspace.open_path(project_path, None, true, window, cx) - }) - .await - .unwrap() -} - -#[track_caller] -fn assert_match_selection( - tab_switcher: &Picker, - expected_selection_index: usize, - expected_item: Box, -) { - assert_eq!( - tab_switcher.delegate.selected_index(), - expected_selection_index, - "item is not selected" - ); - assert_match_at_position(tab_switcher, expected_selection_index, expected_item); -} - -#[track_caller] -fn assert_match_at_position( - tab_switcher: &Picker, - match_index: usize, - expected_item: Box, -) { - let match_item = tab_switcher - .delegate - .matches - .get(match_index) - .unwrap_or_else(|| panic!("Tab Switcher has no match for index {match_index}")); - assert_eq!(match_item.item.item_id(), expected_item.item_id()); -} - -#[track_caller] -fn assert_tab_switcher_is_closed(workspace: Entity, cx: &mut VisualTestContext) { - workspace.update(cx, |workspace, cx| { - assert!( - workspace.active_modal::(cx).is_none(), - "tab switcher is still open" - ); - }); -} diff --git a/crates/task/Cargo.toml b/crates/task/Cargo.toml deleted file mode 100644 index b3cb63bf00..0000000000 --- a/crates/task/Cargo.toml +++ /dev/null @@ -1,41 +0,0 @@ -[package] -name = "task" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[features] -test-support = [ - "gpui/test-support", - "util/test-support" -] - -[lib] -path = "src/task.rs" -doctest = false - -[lints] -workspace = true - -[dependencies] -anyhow.workspace = true -collections.workspace = true -futures.workspace = true -gpui.workspace = true -hex.workspace = true -log.workspace = true -parking_lot.workspace = true -proto.workspace = true -schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_json_lenient.workspace = true -sha2.workspace = true -shellexpand.workspace = true -util.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -gpui = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true diff --git a/crates/task/LICENSE-GPL b/crates/task/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/task/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/task/src/adapter_schema.rs b/crates/task/src/adapter_schema.rs deleted file mode 100644 index 2c58bc0eab..0000000000 --- a/crates/task/src/adapter_schema.rs +++ /dev/null @@ -1,16 +0,0 @@ -use gpui::SharedString; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -/// JSON schema for a specific adapter -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -pub struct AdapterSchema { - /// The adapter name identifier - pub adapter: SharedString, - /// The JSON schema for this adapter's configuration - pub schema: serde_json::Value, -} - -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(transparent)] -pub struct AdapterSchemas(pub Vec); diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs deleted file mode 100644 index 5609e2565c..0000000000 --- a/crates/task/src/debug_format.rs +++ /dev/null @@ -1,534 +0,0 @@ -use anyhow::{Context as _, Result}; -use collections::FxHashMap; -use gpui::SharedString; -use log as _; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::net::Ipv4Addr; -use std::path::PathBuf; -use util::{debug_panic, schemars::add_new_subschema}; - -use crate::{TaskTemplate, adapter_schema::AdapterSchemas}; - -/// Represents the host information of the debug adapter -#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -pub struct TcpArgumentsTemplate { - /// The port that the debug adapter is listening on - /// - /// Default: We will try to find an open port - pub port: Option, - /// The host that the debug adapter is listening too - /// - /// Default: 127.0.0.1 - pub host: Option, - /// The max amount of time in milliseconds to connect to a tcp DAP before returning an error - /// - /// Default: 2000ms - pub timeout: Option, -} - -impl TcpArgumentsTemplate { - /// Get the host or fallback to the default host - pub fn host(&self) -> Ipv4Addr { - self.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)) - } - - pub fn from_proto(proto: proto::TcpHost) -> Result { - Ok(Self { - port: proto.port.map(|p| p.try_into()).transpose()?, - host: proto.host.map(|h| h.parse()).transpose()?, - timeout: proto.timeout, - }) - } - - pub fn to_proto(&self) -> proto::TcpHost { - proto::TcpHost { - port: self.port.map(|p| p.into()), - host: self.host.map(|h| h.to_string()), - timeout: self.timeout, - } - } -} - -/// Represents the attach request information of the debug adapter -#[derive(Default, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -pub struct AttachRequest { - /// The processId to attach to, if left empty we will show a process picker - pub process_id: Option, -} - -impl<'de> Deserialize<'de> for AttachRequest { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - struct Helper { - process_id: Option, - } - - let helper = Helper::deserialize(deserializer)?; - - // Skip creating an AttachRequest if process_id is None - if helper.process_id.is_none() { - return Err(serde::de::Error::custom("process_id is required")); - } - - Ok(AttachRequest { - process_id: helper.process_id, - }) - } -} - -/// Represents the launch request information of the debug adapter -#[derive(Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, Clone, Debug)] -pub struct LaunchRequest { - /// The program that you trying to debug - pub program: String, - /// The current working directory of your project - #[serde(default)] - pub cwd: Option, - /// Arguments to pass to a debuggee - #[serde(default)] - pub args: Vec, - #[serde(default)] - pub env: FxHashMap, -} - -impl LaunchRequest { - pub fn env_json(&self) -> serde_json::Value { - serde_json::Value::Object( - self.env - .iter() - .map(|(k, v)| (k.clone(), v.to_owned().into())) - .collect::>(), - ) - } -} - -/// Represents the type that will determine which request to call on the debug adapter -#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -#[serde(rename_all = "lowercase", tag = "request")] -pub enum DebugRequest { - /// Call the `launch` request on the debug adapter - Launch(LaunchRequest), - /// Call the `attach` request on the debug adapter - Attach(AttachRequest), -} - -impl DebugRequest { - pub fn to_proto(&self) -> proto::DebugRequest { - match self { - DebugRequest::Launch(launch_request) => proto::DebugRequest { - request: Some(proto::debug_request::Request::DebugLaunchRequest( - proto::DebugLaunchRequest { - program: launch_request.program.clone(), - cwd: launch_request - .cwd - .as_ref() - .map(|cwd| cwd.to_string_lossy().into_owned()), - args: launch_request.args.clone(), - env: launch_request - .env - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(), - }, - )), - }, - DebugRequest::Attach(attach_request) => proto::DebugRequest { - request: Some(proto::debug_request::Request::DebugAttachRequest( - proto::DebugAttachRequest { - process_id: attach_request - .process_id - .expect("The process ID to be already filled out."), - }, - )), - }, - } - } - - pub fn from_proto(val: proto::DebugRequest) -> Result { - let request = val.request.context("Missing debug request")?; - match request { - proto::debug_request::Request::DebugLaunchRequest(proto::DebugLaunchRequest { - program, - cwd, - args, - env, - }) => Ok(DebugRequest::Launch(LaunchRequest { - program, - cwd: cwd.map(From::from), - args, - env: env.into_iter().collect(), - })), - - proto::debug_request::Request::DebugAttachRequest(proto::DebugAttachRequest { - process_id, - }) => Ok(DebugRequest::Attach(AttachRequest { - process_id: Some(process_id), - })), - } - } -} - -impl From for DebugRequest { - fn from(launch_config: LaunchRequest) -> Self { - DebugRequest::Launch(launch_config) - } -} - -impl From for DebugRequest { - fn from(attach_config: AttachRequest) -> Self { - DebugRequest::Attach(attach_config) - } -} - -#[derive(Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -#[serde(untagged)] -pub enum BuildTaskDefinition { - ByName(SharedString), - Template { - #[serde(flatten)] - task_template: TaskTemplate, - #[serde(skip)] - locator_name: Option, - }, -} - -impl<'de> Deserialize<'de> for BuildTaskDefinition { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - struct TemplateHelper { - #[serde(default)] - label: Option, - #[serde(flatten)] - rest: serde_json::Value, - } - - let value = serde_json::Value::deserialize(deserializer)?; - - if let Ok(name) = serde_json::from_value::(value.clone()) { - return Ok(BuildTaskDefinition::ByName(name)); - } - - let helper: TemplateHelper = - serde_json::from_value(value).map_err(serde::de::Error::custom)?; - - let mut template_value = helper.rest; - if let serde_json::Value::Object(ref mut map) = template_value { - map.insert( - "label".to_string(), - serde_json::to_value(helper.label.unwrap_or_else(|| "debug-build".to_owned())) - .map_err(serde::de::Error::custom)?, - ); - } - - let task_template: TaskTemplate = - serde_json::from_value(template_value).map_err(serde::de::Error::custom)?; - - Ok(BuildTaskDefinition::Template { - task_template, - locator_name: None, - }) - } -} - -#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)] -pub enum Request { - Launch, - Attach, -} - -/// This struct represent a user created debug task from the new process modal -#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct ZedDebugConfig { - /// Name of the debug task - pub label: SharedString, - /// The debug adapter to use - pub adapter: SharedString, - #[serde(flatten)] - pub request: DebugRequest, - /// Whether to tell the debug adapter to stop on entry - #[serde(default, skip_serializing_if = "Option::is_none")] - pub stop_on_entry: Option, -} - -/// This struct represent a user created debug task -#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct DebugScenario { - pub adapter: SharedString, - /// Name of the debug task - pub label: SharedString, - /// A task to run prior to spawning the debuggee. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub build: Option, - /// The main arguments to be sent to the debug adapter - #[serde(default, flatten)] - pub config: serde_json::Value, - /// Optional TCP connection information - /// - /// If provided, this will be used to connect to the debug adapter instead of - /// spawning a new process. This is useful for connecting to a debug adapter - /// that is already running or is started by another process. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tcp_connection: Option, -} - -/// A group of Debug Tasks defined in a JSON file. -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(transparent)] -pub struct DebugTaskFile(pub Vec); - -impl DebugTaskFile { - pub fn generate_json_schema(schemas: &AdapterSchemas) -> serde_json::Value { - let mut generator = schemars::generate::SchemaSettings::draft2019_09().into_generator(); - - let mut build_task_value = BuildTaskDefinition::json_schema(&mut generator).to_value(); - - if let Some(template_object) = build_task_value - .get_mut("anyOf") - .and_then(|array| array.as_array_mut()) - .and_then(|array| array.get_mut(1)) - { - if let Some(properties) = template_object - .get_mut("properties") - .and_then(|value| value.as_object_mut()) - && properties.remove("label").is_none() - { - debug_panic!( - "Generated TaskTemplate json schema did not have expected 'label' field. \ - Schema of 2nd alternative is: {template_object:?}" - ); - } - - if let Some(arr) = template_object - .get_mut("required") - .and_then(|array| array.as_array_mut()) - { - arr.retain(|v| v.as_str() != Some("label")); - } - } else { - debug_panic!( - "Generated TaskTemplate json schema did not match expectations. \ - Schema is: {build_task_value:?}" - ); - } - - let adapter_conditions = schemas - .0 - .iter() - .map(|adapter_schema| { - let adapter_name = adapter_schema.adapter.to_string(); - add_new_subschema( - &mut generator, - &format!("{adapter_name}DebugSettings"), - serde_json::json!({ - "if": { - "properties": { - "adapter": { "const": adapter_name } - } - }, - "then": adapter_schema.schema - }), - ) - }) - .collect::>(); - - let build_task_definition_ref = add_new_subschema( - &mut generator, - BuildTaskDefinition::schema_name().as_ref(), - build_task_value, - ); - - let meta_schema = generator - .settings() - .meta_schema - .as_ref() - .expect("meta_schema should be present in schemars settings") - .to_string(); - - serde_json::json!({ - "$schema": meta_schema, - "title": "Debug Configurations", - "description": "Configuration for debug scenarios", - "allowTrailingCommas": true, - "type": "array", - "items": { - "type": "object", - "required": ["adapter", "label"], - // TODO: Uncommenting this will cause json-language-server to provide warnings for - // unrecognized properties. It should be enabled if/when there's an adapter JSON - // schema that's comprehensive. In order to not get warnings for the other schemas, - // `additionalProperties` or `unevaluatedProperties` (to handle "allOf" etc style - // schema combinations) could be set to `true` for that schema. - // - // "unevaluatedProperties": false, - "properties": { - "adapter": { - "type": "string", - "description": "The name of the debug adapter" - }, - "label": { - "type": "string", - "description": "The name of the debug configuration" - }, - "build": build_task_definition_ref, - "tcp_connection": { - "type": "object", - "description": "Optional TCP connection information for connecting to an already running debug adapter", - "properties": { - "port": { - "type": "integer", - "description": "The port that the debug adapter is listening on (default: auto-find open port)" - }, - "host": { - "type": "string", - "pattern": "^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$", - "description": "The host that the debug adapter is listening to (default: 127.0.0.1)" - }, - "timeout": { - "type": "integer", - "description": "The max amount of time in milliseconds to connect to a tcp DAP before returning an error (default: 2000ms)" - } - } - } - }, - "allOf": adapter_conditions - }, - "$defs": generator.take_definitions(true), - }) - } -} - -#[cfg(test)] -mod tests { - use crate::DebugScenario; - use serde_json::json; - - #[test] - fn test_just_build_args() { - let json = r#"{ - "label": "Build & debug rust", - "adapter": "CodeLLDB", - "build": { - "command": "rust", - "args": ["build"] - } - }"#; - - let deserialized: DebugScenario = serde_json::from_str(json).unwrap(); - assert!(deserialized.build.is_some()); - match deserialized.build.as_ref().unwrap() { - crate::BuildTaskDefinition::Template { task_template, .. } => { - assert_eq!("debug-build", task_template.label); - assert_eq!("rust", task_template.command); - assert_eq!(vec!["build"], task_template.args); - } - _ => panic!("Expected Template variant"), - } - assert_eq!(json!({}), deserialized.config); - assert_eq!("CodeLLDB", deserialized.adapter.as_ref()); - assert_eq!("Build & debug rust", deserialized.label.as_ref()); - } - - #[test] - fn test_empty_scenario_has_none_request() { - let json = r#"{ - "label": "Build & debug rust", - "build": "rust", - "adapter": "CodeLLDB" - }"#; - - let deserialized: DebugScenario = serde_json::from_str(json).unwrap(); - - assert_eq!(json!({}), deserialized.config); - assert_eq!("CodeLLDB", deserialized.adapter.as_ref()); - assert_eq!("Build & debug rust", deserialized.label.as_ref()); - } - - #[test] - fn test_launch_scenario_deserialization() { - let json = r#"{ - "label": "Launch program", - "adapter": "CodeLLDB", - "request": "launch", - "program": "target/debug/myapp", - "args": ["--test"] - }"#; - - let deserialized: DebugScenario = serde_json::from_str(json).unwrap(); - - assert_eq!( - json!({ "request": "launch", "program": "target/debug/myapp", "args": ["--test"] }), - deserialized.config - ); - assert_eq!("CodeLLDB", deserialized.adapter.as_ref()); - assert_eq!("Launch program", deserialized.label.as_ref()); - } - - #[test] - fn test_attach_scenario_deserialization() { - let json = r#"{ - "label": "Attach to process", - "adapter": "CodeLLDB", - "process_id": 1234, - "request": "attach" - }"#; - - let deserialized: DebugScenario = serde_json::from_str(json).unwrap(); - - assert_eq!( - json!({ "request": "attach", "process_id": 1234 }), - deserialized.config - ); - assert_eq!("CodeLLDB", deserialized.adapter.as_ref()); - assert_eq!("Attach to process", deserialized.label.as_ref()); - } - - #[test] - fn test_build_task_definition_without_label() { - use crate::BuildTaskDefinition; - - let json = r#""my_build_task""#; - let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap(); - match deserialized { - BuildTaskDefinition::ByName(name) => assert_eq!("my_build_task", name.as_ref()), - _ => panic!("Expected ByName variant"), - } - - let json = r#"{ - "command": "cargo", - "args": ["build", "--release"] - }"#; - let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap(); - match deserialized { - BuildTaskDefinition::Template { task_template, .. } => { - assert_eq!("debug-build", task_template.label); - assert_eq!("cargo", task_template.command); - assert_eq!(vec!["build", "--release"], task_template.args); - } - _ => panic!("Expected Template variant"), - } - - let json = r#"{ - "label": "Build Release", - "command": "cargo", - "args": ["build", "--release"] - }"#; - let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap(); - match deserialized { - BuildTaskDefinition::Template { task_template, .. } => { - assert_eq!("Build Release", task_template.label); - assert_eq!("cargo", task_template.command); - assert_eq!(vec!["build", "--release"], task_template.args); - } - _ => panic!("Expected Template variant"), - } - } -} diff --git a/crates/task/src/serde_helpers.rs b/crates/task/src/serde_helpers.rs deleted file mode 100644 index a95214d8b0..0000000000 --- a/crates/task/src/serde_helpers.rs +++ /dev/null @@ -1,37 +0,0 @@ -use serde::de::{self, Deserializer, Visitor}; -use std::fmt; - -/// Deserializes a non-empty string array. -pub fn non_empty_string_vec<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - struct NonEmptyStringVecVisitor; - - impl<'de> Visitor<'de> for NonEmptyStringVecVisitor { - type Value = Vec; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a list of non-empty strings") - } - - fn visit_seq(self, mut seq: V) -> Result, V::Error> - where - V: de::SeqAccess<'de>, - { - let mut vec = Vec::new(); - while let Some(value) = seq.next_element::()? { - if value.is_empty() { - return Err(de::Error::invalid_value( - de::Unexpected::Str(&value), - &"a non-empty string", - )); - } - vec.push(value); - } - Ok(vec) - } - } - - deserializer.deserialize_seq(NonEmptyStringVecVisitor) -} diff --git a/crates/task/src/static_source.rs b/crates/task/src/static_source.rs deleted file mode 100644 index 9e4051ef97..0000000000 --- a/crates/task/src/static_source.rs +++ /dev/null @@ -1,123 +0,0 @@ -//! A source of tasks, based on a static configuration, deserialized from the tasks config file, and related infrastructure for tracking changes to the file. - -use std::sync::Arc; - -use futures::{StreamExt, channel::mpsc::UnboundedSender}; -use gpui::{App, AppContext}; -use parking_lot::RwLock; -use serde::Deserialize; -use util::ResultExt; - -use crate::TaskTemplates; -use futures::channel::mpsc::UnboundedReceiver; - -/// The source of tasks defined in a tasks config file. -pub struct StaticSource { - tasks: TrackedFile, -} - -/// A Wrapper around deserializable T that keeps track of its contents -/// via a provided channel. -pub struct TrackedFile { - parsed_contents: Arc>, -} - -impl TrackedFile { - /// Initializes new [`TrackedFile`] with a type that's deserializable. - pub fn new( - mut tracker: UnboundedReceiver, - notification_outlet: UnboundedSender<()>, - cx: &App, - ) -> Self - where - T: for<'a> Deserialize<'a> + Default + Send, - { - let parsed_contents: Arc> = Arc::default(); - cx.background_spawn({ - let parsed_contents = parsed_contents.clone(); - async move { - while let Some(new_contents) = tracker.next().await { - if Arc::strong_count(&parsed_contents) == 1 { - // We're no longer being observed. Stop polling. - break; - } - if !new_contents.trim().is_empty() { - let Some(new_contents) = - serde_json_lenient::from_str::(&new_contents).log_err() - else { - continue; - }; - let mut contents = parsed_contents.write(); - if *contents != new_contents { - *contents = new_contents; - if notification_outlet.unbounded_send(()).is_err() { - // Whoever cared about contents is not around anymore. - break; - } - } - } - } - anyhow::Ok(()) - } - }) - .detach_and_log_err(cx); - Self { parsed_contents } - } - - /// Initializes new [`TrackedFile`] with a type that's convertible from another deserializable type. - pub fn new_convertible Deserialize<'a> + TryInto>( - mut tracker: UnboundedReceiver, - notification_outlet: UnboundedSender<()>, - cx: &App, - ) -> Self - where - T: Default + Send, - { - let parsed_contents: Arc> = Arc::default(); - cx.background_spawn({ - async move { - while let Some(new_contents) = tracker.next().await { - if Arc::strong_count(&parsed_contents) == 1 { - // We're no longer being observed. Stop polling. - break; - } - - if !new_contents.trim().is_empty() { - let Some(new_contents) = - serde_json_lenient::from_str::(&new_contents).log_err() - else { - continue; - }; - let Some(new_contents) = new_contents.try_into().log_err() else { - continue; - }; - let mut contents = parsed_contents.write(); - if *contents != new_contents { - *contents = new_contents; - if notification_outlet.unbounded_send(()).is_err() { - // Whoever cared about contents is not around anymore. - break; - } - } - } - } - anyhow::Ok(()) - } - }) - .detach_and_log_err(cx); - Self { - parsed_contents: Default::default(), - } - } -} - -impl StaticSource { - /// Initializes the static source, reacting on tasks config changes. - pub fn new(tasks: TrackedFile) -> Self { - Self { tasks } - } - /// Returns current list of tasks - pub fn tasks_to_schedule(&self) -> TaskTemplates { - self.tasks.parsed_contents.read().clone() - } -} diff --git a/crates/task/src/task.rs b/crates/task/src/task.rs deleted file mode 100644 index 92d5909419..0000000000 --- a/crates/task/src/task.rs +++ /dev/null @@ -1,431 +0,0 @@ -//! Baseline interface of Tasks in Zed: all tasks in Zed are intended to use those for implementing their own logic. - -mod adapter_schema; -mod debug_format; -mod serde_helpers; -pub mod static_source; -mod task_template; -mod vscode_debug_format; -mod vscode_format; - -use anyhow::Context as _; -use collections::{HashMap, HashSet, hash_map}; -use gpui::SharedString; -use serde::{Deserialize, Serialize}; -use std::borrow::Cow; -use std::path::PathBuf; -use std::str::FromStr; - -pub use adapter_schema::{AdapterSchema, AdapterSchemas}; -pub use debug_format::{ - AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, - Request, TcpArgumentsTemplate, ZedDebugConfig, -}; -pub use task_template::{ - DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates, - substitute_variables_in_map, substitute_variables_in_str, -}; -pub use util::shell::{Shell, ShellKind}; -pub use util::shell_builder::ShellBuilder; -pub use vscode_debug_format::VsCodeDebugTaskFile; -pub use vscode_format::VsCodeTaskFile; -pub use zed_actions::RevealTarget; - -/// Task identifier, unique within the application. -/// Based on it, task reruns and terminal tabs are managed. -#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize)] -pub struct TaskId(pub String); - -/// Contains all information needed by Zed to spawn a new terminal tab for the given task. -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct SpawnInTerminal { - /// Id of the task to use when determining task tab affinity. - pub id: TaskId, - /// Full unshortened form of `label` field. - pub full_label: String, - /// Human readable name of the terminal tab. - pub label: String, - /// Executable command to spawn. - pub command: Option, - /// Arguments to the command, potentially unsubstituted, - /// to let the shell that spawns the command to do the substitution, if needed. - pub args: Vec, - /// A human-readable label, containing command and all of its arguments, joined and substituted. - pub command_label: String, - /// Current working directory to spawn the command into. - pub cwd: Option, - /// Env overrides for the command, will be appended to the terminal's environment from the settings. - pub env: HashMap, - /// Whether to use a new terminal tab or reuse the existing one to spawn the process. - pub use_new_terminal: bool, - /// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish. - pub allow_concurrent_runs: bool, - /// What to do with the terminal pane and tab, after the command was started. - pub reveal: RevealStrategy, - /// Where to show tasks' terminal output. - pub reveal_target: RevealTarget, - /// What to do with the terminal pane and tab, after the command had finished. - pub hide: HideStrategy, - /// Which shell to use when spawning the task. - pub shell: Shell, - /// Whether to show the task summary line in the task output (sucess/failure). - pub show_summary: bool, - /// Whether to show the command line in the task output. - pub show_command: bool, - /// Whether to show the rerun button in the terminal tab. - pub show_rerun: bool, -} - -impl SpawnInTerminal { - pub fn to_proto(&self) -> proto::SpawnInTerminal { - proto::SpawnInTerminal { - label: self.label.clone(), - command: self.command.clone(), - args: self.args.clone(), - env: self - .env - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(), - cwd: self - .cwd - .clone() - .map(|cwd| cwd.to_string_lossy().into_owned()), - } - } - - pub fn from_proto(proto: proto::SpawnInTerminal) -> Self { - Self { - label: proto.label.clone(), - command: proto.command.clone(), - args: proto.args.clone(), - env: proto.env.into_iter().collect(), - cwd: proto.cwd.map(PathBuf::from), - ..Default::default() - } - } -} - -/// A final form of the [`TaskTemplate`], that got resolved with a particular [`TaskContext`] and now is ready to spawn the actual task. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ResolvedTask { - /// A way to distinguish tasks produced by the same template, but different contexts. - /// NOTE: Resolved tasks may have the same labels, commands and do the same things, - /// but still may have different ids if the context was different during the resolution. - /// Since the template has `env` field, for a generic task that may be a bash command, - /// so it's impossible to determine the id equality without more context in a generic case. - pub id: TaskId, - /// A template the task got resolved from. - original_task: TaskTemplate, - /// Full, unshortened label of the task after all resolutions are made. - pub resolved_label: String, - /// Variables that were substituted during the task template resolution. - substituted_variables: HashSet, - /// Further actions that need to take place after the resolved task is spawned, - /// with all task variables resolved. - pub resolved: SpawnInTerminal, -} - -impl ResolvedTask { - /// A task template before the resolution. - pub fn original_task(&self) -> &TaskTemplate { - &self.original_task - } - - /// Variables that were substituted during the task template resolution. - pub fn substituted_variables(&self) -> &HashSet { - &self.substituted_variables - } - - /// A human-readable label to display in the UI. - pub fn display_label(&self) -> &str { - self.resolved.label.as_str() - } -} - -/// Variables, available for use in [`TaskContext`] when a Zed's [`TaskTemplate`] gets resolved into a [`ResolvedTask`]. -/// Name of the variable must be a valid shell variable identifier, which generally means that it is -/// a word consisting only of alphanumeric characters and underscores, -/// and beginning with an alphabetic character or an underscore. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] -pub enum VariableName { - /// An absolute path of the currently opened file. - File, - /// A path of the currently opened file (relative to worktree root). - RelativeFile, - /// A path of the currently opened file's directory (relative to worktree root). - RelativeDir, - /// The currently opened filename. - Filename, - /// The path to a parent directory of a currently opened file. - Dirname, - /// Stem (filename without extension) of the currently opened file. - Stem, - /// An absolute path of the currently opened worktree, that contains the file. - WorktreeRoot, - /// A symbol text, that contains latest cursor/selection position. - Symbol, - /// A row with the latest cursor/selection position. - Row, - /// A column with the latest cursor/selection position. - Column, - /// Text from the latest selection. - SelectedText, - /// The symbol selected by the symbol tagging system, specifically the @run capture in a runnables.scm - RunnableSymbol, - /// Open a Picker to select a process ID to use in place - /// Can only be used to debug configurations - PickProcessId, - /// Custom variable, provided by the plugin or other external source. - /// Will be printed with `CUSTOM_` prefix to avoid potential conflicts with other variables. - Custom(Cow<'static, str>), -} - -impl VariableName { - /// Generates a `$VARIABLE`-like string value to be used in templates. - pub fn template_value(&self) -> String { - format!("${self}") - } - /// Generates a `"$VARIABLE"`-like string, to be used instead of `Self::template_value` when expanded value could contain spaces or special characters. - pub fn template_value_with_whitespace(&self) -> String { - format!("\"${self}\"") - } -} - -impl FromStr for VariableName { - type Err = (); - - fn from_str(s: &str) -> Result { - let without_prefix = s.strip_prefix(ZED_VARIABLE_NAME_PREFIX).ok_or(())?; - let value = match without_prefix { - "FILE" => Self::File, - "FILENAME" => Self::Filename, - "RELATIVE_FILE" => Self::RelativeFile, - "RELATIVE_DIR" => Self::RelativeDir, - "DIRNAME" => Self::Dirname, - "STEM" => Self::Stem, - "WORKTREE_ROOT" => Self::WorktreeRoot, - "SYMBOL" => Self::Symbol, - "RUNNABLE_SYMBOL" => Self::RunnableSymbol, - "SELECTED_TEXT" => Self::SelectedText, - "ROW" => Self::Row, - "COLUMN" => Self::Column, - _ => { - if let Some(custom_name) = - without_prefix.strip_prefix(ZED_CUSTOM_VARIABLE_NAME_PREFIX) - { - Self::Custom(Cow::Owned(custom_name.to_owned())) - } else { - return Err(()); - } - } - }; - Ok(value) - } -} - -/// A prefix that all [`VariableName`] variants are prefixed with when used in environment variables and similar template contexts. -pub const ZED_VARIABLE_NAME_PREFIX: &str = "ZED_"; -const ZED_CUSTOM_VARIABLE_NAME_PREFIX: &str = "CUSTOM_"; - -impl std::fmt::Display for VariableName { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Self::File => write!(f, "{ZED_VARIABLE_NAME_PREFIX}FILE"), - Self::Filename => write!(f, "{ZED_VARIABLE_NAME_PREFIX}FILENAME"), - Self::RelativeFile => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RELATIVE_FILE"), - Self::RelativeDir => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RELATIVE_DIR"), - Self::Dirname => write!(f, "{ZED_VARIABLE_NAME_PREFIX}DIRNAME"), - Self::Stem => write!(f, "{ZED_VARIABLE_NAME_PREFIX}STEM"), - Self::WorktreeRoot => write!(f, "{ZED_VARIABLE_NAME_PREFIX}WORKTREE_ROOT"), - Self::Symbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SYMBOL"), - Self::Row => write!(f, "{ZED_VARIABLE_NAME_PREFIX}ROW"), - Self::Column => write!(f, "{ZED_VARIABLE_NAME_PREFIX}COLUMN"), - Self::SelectedText => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SELECTED_TEXT"), - Self::RunnableSymbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RUNNABLE_SYMBOL"), - Self::PickProcessId => write!(f, "{ZED_VARIABLE_NAME_PREFIX}PICK_PID"), - Self::Custom(s) => write!( - f, - "{ZED_VARIABLE_NAME_PREFIX}{ZED_CUSTOM_VARIABLE_NAME_PREFIX}{s}" - ), - } - } -} - -/// Container for predefined environment variables that describe state of Zed at the time the task was spawned. -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)] -pub struct TaskVariables(HashMap); - -impl TaskVariables { - /// Inserts another variable into the container, overwriting the existing one if it already exists — in this case, the old value is returned. - pub fn insert(&mut self, variable: VariableName, value: String) -> Option { - self.0.insert(variable, value) - } - - /// Extends the container with another one, overwriting the existing variables on collision. - pub fn extend(&mut self, other: Self) { - self.0.extend(other.0); - } - /// Get the value associated with given variable name, if there is one. - pub fn get(&self, key: &VariableName) -> Option<&str> { - self.0.get(key).map(|s| s.as_str()) - } - /// Clear out variables obtained from tree-sitter queries, which are prefixed with '_' character - pub fn sweep(&mut self) { - self.0.retain(|name, _| { - if let VariableName::Custom(name) = name { - !name.starts_with('_') - } else { - true - } - }) - } - - pub fn iter(&self) -> impl Iterator { - self.0.iter() - } -} - -impl FromIterator<(VariableName, String)> for TaskVariables { - fn from_iter>(iter: T) -> Self { - Self(HashMap::from_iter(iter)) - } -} - -impl IntoIterator for TaskVariables { - type Item = (VariableName, String); - - type IntoIter = hash_map::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -/// Keeps track of the file associated with a task and context of tasks execution (i.e. current file or current function). -/// Keeps all Zed-related state inside, used to produce a resolved task out of its template. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct TaskContext { - /// A path to a directory in which the task should be executed. - pub cwd: Option, - /// Additional environment variables associated with a given task. - pub task_variables: TaskVariables, - /// Environment variables obtained when loading the project into Zed. - /// This is the environment one would get when `cd`ing in a terminal - /// into the project's root directory. - pub project_env: HashMap, -} - -/// This is a new type representing a 'tag' on a 'runnable symbol', typically a test of main() function, found via treesitter. -#[derive(Clone, Debug)] -pub struct RunnableTag(pub SharedString); - -pub fn shell_from_proto(proto: proto::Shell) -> anyhow::Result { - let shell_type = proto.shell_type.context("invalid shell type")?; - let shell = match shell_type { - proto::shell::ShellType::System(_) => Shell::System, - proto::shell::ShellType::Program(program) => Shell::Program(program), - proto::shell::ShellType::WithArguments(program) => Shell::WithArguments { - program: program.program, - args: program.args, - title_override: None, - }, - }; - Ok(shell) -} - -pub fn shell_to_proto(shell: Shell) -> proto::Shell { - let shell_type = match shell { - Shell::System => proto::shell::ShellType::System(proto::System {}), - Shell::Program(program) => proto::shell::ShellType::Program(program), - Shell::WithArguments { - program, - args, - title_override: _, - } => proto::shell::ShellType::WithArguments(proto::shell::WithArguments { program, args }), - }; - proto::Shell { - shell_type: Some(shell_type), - } -} - -type VsCodeEnvVariable = String; -type VsCodeCommand = String; -type ZedEnvVariable = String; - -struct EnvVariableReplacer { - variables: HashMap, - commands: HashMap, -} - -impl EnvVariableReplacer { - fn new(variables: HashMap) -> Self { - Self { - variables, - commands: HashMap::default(), - } - } - - fn with_commands( - mut self, - commands: impl IntoIterator, - ) -> Self { - self.commands = commands.into_iter().collect(); - self - } - - fn replace_value(&self, input: serde_json::Value) -> serde_json::Value { - match input { - serde_json::Value::String(s) => serde_json::Value::String(self.replace(&s)), - serde_json::Value::Array(arr) => { - serde_json::Value::Array(arr.into_iter().map(|v| self.replace_value(v)).collect()) - } - serde_json::Value::Object(obj) => serde_json::Value::Object( - obj.into_iter() - .map(|(k, v)| (self.replace(&k), self.replace_value(v))) - .collect(), - ), - _ => input, - } - } - // Replaces occurrences of VsCode-specific environment variables with Zed equivalents. - fn replace(&self, input: &str) -> String { - shellexpand::env_with_context_no_errors(&input, |var: &str| { - // Colons denote a default value in case the variable is not set. We want to preserve that default, as otherwise shellexpand will substitute it for us. - let colon_position = var.find(':').unwrap_or(var.len()); - let (left, right) = var.split_at(colon_position); - if left == "env" && !right.is_empty() { - let variable_name = &right[1..]; - return Some(format!("${{{variable_name}}}")); - } else if left == "command" && !right.is_empty() { - let command_name = &right[1..]; - if let Some(replacement_command) = self.commands.get(command_name) { - return Some(format!("${{{replacement_command}}}")); - } - } - - let (variable_name, default) = (left, right); - let append_previous_default = |ret: &mut String| { - if !default.is_empty() { - ret.push_str(default); - } - }; - if let Some(substitution) = self.variables.get(variable_name) { - // Got a VSCode->Zed hit, perform a substitution - let mut name = format!("${{{substitution}"); - append_previous_default(&mut name); - name.push('}'); - return Some(name); - } - // This is an unknown variable. - // We should not error out, as they may come from user environment (e.g. $PATH). That means that the variable substitution might not be perfect. - // If there's a default, we need to return the string verbatim as otherwise shellexpand will apply that default for us. - if !default.is_empty() { - return Some(format!("${{{var}}}")); - } - // Else we can just return None and that variable will be left as is. - None - }) - .into_owned() - } -} diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs deleted file mode 100644 index 0c319db061..0000000000 --- a/crates/task/src/task_template.rs +++ /dev/null @@ -1,974 +0,0 @@ -use anyhow::{Context as _, bail}; -use collections::{HashMap, HashSet}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; -use std::path::PathBuf; -use util::schemars::{AllowTrailingCommas, DefaultDenyUnknownFields}; -use util::serde::default_true; -use util::{ResultExt, truncate_and_remove_front}; - -use crate::{ - AttachRequest, ResolvedTask, RevealTarget, Shell, SpawnInTerminal, TaskContext, TaskId, - VariableName, ZED_VARIABLE_NAME_PREFIX, serde_helpers::non_empty_string_vec, -}; - -/// A template definition of a Zed task to run. -/// May use the [`VariableName`] to get the corresponding substitutions into its fields. -/// -/// Template itself is not ready to spawn a task, it needs to be resolved with a [`TaskContext`] first, that -/// contains all relevant Zed state in task variables. -/// A single template may produce different tasks (or none) for different contexts. -#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct TaskTemplate { - /// Human readable name of the task to display in the UI. - pub label: String, - /// Executable command to spawn. - pub command: String, - /// Arguments to the command. - #[serde(default)] - pub args: Vec, - /// Env overrides for the command, will be appended to the terminal's environment from the settings. - #[serde(default)] - pub env: HashMap, - /// Current working directory to spawn the command into, defaults to current project root. - #[serde(default)] - pub cwd: Option, - /// Whether to use a new terminal tab or reuse the existing one to spawn the process. - #[serde(default)] - pub use_new_terminal: bool, - /// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish. - #[serde(default)] - pub allow_concurrent_runs: bool, - /// What to do with the terminal pane and tab, after the command was started: - /// * `always` — always show the task's pane, and focus the corresponding tab in it (default) - // * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it - // * `never` — do not alter focus, but still add/reuse the task's tab in its pane - #[serde(default)] - pub reveal: RevealStrategy, - /// Where to place the task's terminal item after starting the task. - /// * `dock` — in the terminal dock, "regular" terminal items' place (default). - /// * `center` — in the central pane group, "main" editor area. - #[serde(default)] - pub reveal_target: RevealTarget, - /// What to do with the terminal pane and tab, after the command had finished: - /// * `never` — do nothing when the command finishes (default) - /// * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it - /// * `on_success` — hide the terminal tab on task success only, otherwise behaves similar to `always`. - #[serde(default)] - pub hide: HideStrategy, - /// Represents the tags which this template attaches to. - /// Adding this removes this task from other UI and gives you ability to run it by tag. - #[serde(default, deserialize_with = "non_empty_string_vec")] - #[schemars(length(min = 1))] - pub tags: Vec, - /// Which shell to use when spawning the task. - #[serde(default)] - pub shell: Shell, - /// Whether to show the task line in the task output. - #[serde(default = "default_true")] - pub show_summary: bool, - /// Whether to show the command line in the task output. - #[serde(default = "default_true")] - pub show_command: bool, -} - -#[derive(Deserialize, Eq, PartialEq, Clone, Debug)] -/// Use to represent debug request type -pub enum DebugArgsRequest { - /// launch (program, cwd) are stored in TaskTemplate as (command, cwd) - Launch, - /// Attach - Attach(AttachRequest), -} - -/// What to do with the terminal pane and tab, after the command was started. -#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum RevealStrategy { - /// Always show the task's pane, and focus the corresponding tab in it. - #[default] - Always, - /// Always show the task's pane, add the task's tab in it, but don't focus it. - NoFocus, - /// Do not alter focus, but still add/reuse the task's tab in its pane. - Never, -} - -/// What to do with the terminal pane and tab, after the command has finished. -#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum HideStrategy { - /// Do nothing when the command finishes. - #[default] - Never, - /// Always hide the terminal tab, hide the pane also if it was the last tab in it. - Always, - /// Hide the terminal tab on task success only, otherwise behaves similar to `Always`. - OnSuccess, -} - -/// A group of Tasks defined in a JSON file. -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct TaskTemplates(pub Vec); - -impl TaskTemplates { - /// Generates JSON schema of Tasks JSON template format. - pub fn generate_json_schema() -> serde_json::Value { - let schema = schemars::generate::SchemaSettings::draft2019_09() - .with_transform(DefaultDenyUnknownFields) - .with_transform(AllowTrailingCommas) - .into_generator() - .root_schema_for::(); - - serde_json::to_value(schema).unwrap() - } -} - -impl TaskTemplate { - /// Replaces all `VariableName` task variables in the task template string fields. - /// If any replacement fails or the new string substitutions still have [`ZED_VARIABLE_NAME_PREFIX`], - /// `None` is returned. - /// - /// Every [`ResolvedTask`] gets a [`TaskId`], based on the `id_base` (to avoid collision with various task sources), - /// and hashes of its template and [`TaskContext`], see [`ResolvedTask`] fields' documentation for more details. - pub fn resolve_task(&self, id_base: &str, cx: &TaskContext) -> Option { - if self.label.trim().is_empty() || self.command.trim().is_empty() { - return None; - } - - let mut variable_names = HashMap::default(); - let mut substituted_variables = HashSet::default(); - let task_variables = cx - .task_variables - .0 - .iter() - .map(|(key, value)| { - let key_string = key.to_string(); - if !variable_names.contains_key(&key_string) { - variable_names.insert(key_string.clone(), key.clone()); - } - (key_string, value.as_str()) - }) - .collect::>(); - let truncated_variables = truncate_variables(&task_variables); - let cwd = match self.cwd.as_deref() { - Some(cwd) => { - let substituted_cwd = substitute_all_template_variables_in_str( - cwd, - &task_variables, - &variable_names, - &mut substituted_variables, - )?; - Some(PathBuf::from(substituted_cwd)) - } - None => None, - } - .or(cx.cwd.clone()); - let full_label = substitute_all_template_variables_in_str( - &self.label, - &task_variables, - &variable_names, - &mut substituted_variables, - )?; - - // Arbitrarily picked threshold below which we don't truncate any variables. - const TRUNCATION_THRESHOLD: usize = 64; - - let human_readable_label = if full_label.len() > TRUNCATION_THRESHOLD { - substitute_all_template_variables_in_str( - &self.label, - &truncated_variables, - &variable_names, - &mut substituted_variables, - )? - } else { - #[allow( - clippy::redundant_clone, - reason = "We want to clone the full_label to avoid borrowing it in the fold closure" - )] - full_label.clone() - } - .lines() - .fold(String::new(), |mut string, line| { - if string.is_empty() { - string.push_str(line); - } else { - string.push_str("\\n"); - string.push_str(line); - } - string - }); - - let command = substitute_all_template_variables_in_str( - &self.command, - &task_variables, - &variable_names, - &mut substituted_variables, - )?; - let args_with_substitutions = substitute_all_template_variables_in_vec( - &self.args, - &task_variables, - &variable_names, - &mut substituted_variables, - )?; - - let task_hash = to_hex_hash(self) - .context("hashing task template") - .log_err()?; - let variables_hash = to_hex_hash(&task_variables) - .context("hashing task variables") - .log_err()?; - let id = TaskId(format!("{id_base}_{task_hash}_{variables_hash}")); - - let env = { - // Start with the project environment as the base. - let mut env = cx.project_env.clone(); - - // Extend that environment with what's defined in the TaskTemplate - env.extend(self.env.clone()); - - // Then we replace all task variables that could be set in environment variables - let mut env = substitute_all_template_variables_in_map( - &env, - &task_variables, - &variable_names, - &mut substituted_variables, - )?; - - // Last step: set the task variables as environment variables too - env.extend(task_variables.into_iter().map(|(k, v)| (k, v.to_owned()))); - env - }; - - Some(ResolvedTask { - id: id.clone(), - substituted_variables, - original_task: self.clone(), - resolved_label: full_label.clone(), - resolved: SpawnInTerminal { - id, - cwd, - full_label, - label: human_readable_label, - command_label: args_with_substitutions.iter().fold( - command.clone(), - |mut command_label, arg| { - command_label.push(' '); - command_label.push_str(arg); - command_label - }, - ), - command: Some(command), - args: args_with_substitutions, - env, - use_new_terminal: self.use_new_terminal, - allow_concurrent_runs: self.allow_concurrent_runs, - reveal: self.reveal, - reveal_target: self.reveal_target, - hide: self.hide, - shell: self.shell.clone(), - show_summary: self.show_summary, - show_command: self.show_command, - show_rerun: true, - }, - }) - } -} - -const MAX_DISPLAY_VARIABLE_LENGTH: usize = 15; - -fn truncate_variables(task_variables: &HashMap) -> HashMap { - task_variables - .iter() - .map(|(key, value)| { - ( - key.clone(), - truncate_and_remove_front(value, MAX_DISPLAY_VARIABLE_LENGTH), - ) - }) - .collect() -} - -fn to_hex_hash(object: impl Serialize) -> anyhow::Result { - let json = serde_json_lenient::to_string(&object).context("serializing the object")?; - let mut hasher = Sha256::new(); - hasher.update(json.as_bytes()); - Ok(hex::encode(hasher.finalize())) -} - -pub fn substitute_variables_in_str(template_str: &str, context: &TaskContext) -> Option { - let mut variable_names = HashMap::default(); - let mut substituted_variables = HashSet::default(); - let task_variables = context - .task_variables - .0 - .iter() - .map(|(key, value)| { - let key_string = key.to_string(); - if !variable_names.contains_key(&key_string) { - variable_names.insert(key_string.clone(), key.clone()); - } - (key_string, value.as_str()) - }) - .collect::>(); - substitute_all_template_variables_in_str( - template_str, - &task_variables, - &variable_names, - &mut substituted_variables, - ) -} -fn substitute_all_template_variables_in_str>( - template_str: &str, - task_variables: &HashMap, - variable_names: &HashMap, - substituted_variables: &mut HashSet, -) -> Option { - let substituted_string = shellexpand::env_with_context(template_str, |var| { - // Colons denote a default value in case the variable is not set. We want to preserve that default, as otherwise shellexpand will substitute it for us. - let colon_position = var.find(':').unwrap_or(var.len()); - let (variable_name, default) = var.split_at(colon_position); - if let Some(name) = task_variables.get(variable_name) { - if let Some(substituted_variable) = variable_names.get(variable_name) { - substituted_variables.insert(substituted_variable.clone()); - } - // Got a task variable hit - use the variable value, ignore default - return Ok(Some(name.as_ref().to_owned())); - } else if variable_name.starts_with(ZED_VARIABLE_NAME_PREFIX) { - // Unknown ZED variable - use default if available - if !default.is_empty() { - // Strip the colon and return the default value - return Ok(Some(default[1..].to_owned())); - } else { - bail!("Unknown variable name: {variable_name}"); - } - } - // This is an unknown variable. - // We should not error out, as they may come from user environment (e.g. $PATH). That means that the variable substitution might not be perfect. - // If there's a default, we need to return the string verbatim as otherwise shellexpand will apply that default for us. - if !default.is_empty() { - return Ok(Some(format!("${{{var}}}"))); - } - // Else we can just return None and that variable will be left as is. - Ok(None) - }) - .ok()?; - Some(substituted_string.into_owned()) -} - -fn substitute_all_template_variables_in_vec( - template_strs: &[String], - task_variables: &HashMap, - variable_names: &HashMap, - substituted_variables: &mut HashSet, -) -> Option> { - let mut expanded = Vec::with_capacity(template_strs.len()); - for variable in template_strs { - let new_value = substitute_all_template_variables_in_str( - variable, - task_variables, - variable_names, - substituted_variables, - )?; - expanded.push(new_value); - } - Some(expanded) -} - -pub fn substitute_variables_in_map( - keys_and_values: &HashMap, - context: &TaskContext, -) -> Option> { - let mut variable_names = HashMap::default(); - let mut substituted_variables = HashSet::default(); - let task_variables = context - .task_variables - .0 - .iter() - .map(|(key, value)| { - let key_string = key.to_string(); - if !variable_names.contains_key(&key_string) { - variable_names.insert(key_string.clone(), key.clone()); - } - (key_string, value.as_str()) - }) - .collect::>(); - substitute_all_template_variables_in_map( - keys_and_values, - &task_variables, - &variable_names, - &mut substituted_variables, - ) -} -fn substitute_all_template_variables_in_map( - keys_and_values: &HashMap, - task_variables: &HashMap, - variable_names: &HashMap, - substituted_variables: &mut HashSet, -) -> Option> { - let mut new_map: HashMap = Default::default(); - for (key, value) in keys_and_values { - let new_value = substitute_all_template_variables_in_str( - value, - task_variables, - variable_names, - substituted_variables, - )?; - let new_key = substitute_all_template_variables_in_str( - key, - task_variables, - variable_names, - substituted_variables, - )?; - new_map.insert(new_key, new_value); - } - Some(new_map) -} - -#[cfg(test)] -mod tests { - use std::{borrow::Cow, path::Path}; - - use crate::{TaskVariables, VariableName}; - - use super::*; - - const TEST_ID_BASE: &str = "test_base"; - - #[test] - fn test_resolving_templates_with_blank_command_and_label() { - let task_with_all_properties = TaskTemplate { - label: "test_label".to_string(), - command: "test_command".to_string(), - args: vec!["test_arg".to_string()], - env: HashMap::from_iter([("test_env_key".to_string(), "test_env_var".to_string())]), - ..TaskTemplate::default() - }; - - for task_with_blank_property in &[ - TaskTemplate { - label: "".to_string(), - ..task_with_all_properties.clone() - }, - TaskTemplate { - command: "".to_string(), - ..task_with_all_properties.clone() - }, - TaskTemplate { - label: "".to_string(), - command: "".to_string(), - ..task_with_all_properties - }, - ] { - assert_eq!( - task_with_blank_property.resolve_task(TEST_ID_BASE, &TaskContext::default()), - None, - "should not resolve task with blank label and/or command: {task_with_blank_property:?}" - ); - } - } - - #[test] - fn test_template_cwd_resolution() { - let task_without_cwd = TaskTemplate { - cwd: None, - label: "test task".to_string(), - command: "echo 4".to_string(), - ..TaskTemplate::default() - }; - - let resolved_task = |task_template: &TaskTemplate, task_cx| { - let resolved_task = task_template - .resolve_task(TEST_ID_BASE, task_cx) - .unwrap_or_else(|| panic!("failed to resolve task {task_without_cwd:?}")); - assert_substituted_variables(&resolved_task, Vec::new()); - resolved_task.resolved - }; - - let cx = TaskContext { - cwd: None, - task_variables: TaskVariables::default(), - project_env: HashMap::default(), - }; - assert_eq!( - resolved_task(&task_without_cwd, &cx).cwd, - None, - "When neither task nor task context have cwd, it should be None" - ); - - let context_cwd = Path::new("a").join("b").join("c"); - let cx = TaskContext { - cwd: Some(context_cwd.clone()), - task_variables: TaskVariables::default(), - project_env: HashMap::default(), - }; - assert_eq!( - resolved_task(&task_without_cwd, &cx).cwd, - Some(context_cwd.clone()), - "TaskContext's cwd should be taken on resolve if task's cwd is None" - ); - - let task_cwd = Path::new("d").join("e").join("f"); - let mut task_with_cwd = task_without_cwd.clone(); - task_with_cwd.cwd = Some(task_cwd.display().to_string()); - let task_with_cwd = task_with_cwd; - - let cx = TaskContext { - cwd: None, - task_variables: TaskVariables::default(), - project_env: HashMap::default(), - }; - assert_eq!( - resolved_task(&task_with_cwd, &cx).cwd, - Some(task_cwd.clone()), - "TaskTemplate's cwd should be taken on resolve if TaskContext's cwd is None" - ); - - let cx = TaskContext { - cwd: Some(context_cwd), - task_variables: TaskVariables::default(), - project_env: HashMap::default(), - }; - assert_eq!( - resolved_task(&task_with_cwd, &cx).cwd, - Some(task_cwd), - "TaskTemplate's cwd should be taken on resolve if TaskContext's cwd is not None" - ); - } - - #[test] - fn test_template_variables_resolution() { - let custom_variable_1 = VariableName::Custom(Cow::Borrowed("custom_variable_1")); - let custom_variable_2 = VariableName::Custom(Cow::Borrowed("custom_variable_2")); - let long_value = "01".repeat(MAX_DISPLAY_VARIABLE_LENGTH * 2); - let all_variables = [ - (VariableName::Row, "1234".to_string()), - (VariableName::Column, "5678".to_string()), - (VariableName::File, "test_file".to_string()), - (VariableName::SelectedText, "test_selected_text".to_string()), - (VariableName::Symbol, long_value.clone()), - (VariableName::WorktreeRoot, "/test_root/".to_string()), - ( - custom_variable_1.clone(), - "test_custom_variable_1".to_string(), - ), - ( - custom_variable_2.clone(), - "test_custom_variable_2".to_string(), - ), - ]; - - let task_with_all_variables = TaskTemplate { - label: format!( - "test label for {} and {}", - VariableName::Row.template_value(), - VariableName::Symbol.template_value(), - ), - command: format!( - "echo {} {}", - VariableName::File.template_value(), - VariableName::Symbol.template_value(), - ), - args: vec![ - format!("arg1 {}", VariableName::SelectedText.template_value()), - format!("arg2 {}", VariableName::Column.template_value()), - format!("arg3 {}", VariableName::Symbol.template_value()), - ], - env: HashMap::from_iter([ - ("test_env_key".to_string(), "test_env_var".to_string()), - ( - "env_key_1".to_string(), - VariableName::WorktreeRoot.template_value(), - ), - ( - "env_key_2".to_string(), - format!( - "env_var_2 {} {}", - custom_variable_1.template_value(), - custom_variable_2.template_value() - ), - ), - ( - "env_key_3".to_string(), - format!("env_var_3 {}", VariableName::Symbol.template_value()), - ), - ]), - ..TaskTemplate::default() - }; - - let mut first_resolved_id = None; - for i in 0..15 { - let resolved_task = task_with_all_variables.resolve_task( - TEST_ID_BASE, - &TaskContext { - cwd: None, - task_variables: TaskVariables::from_iter(all_variables.clone()), - project_env: HashMap::default(), - }, - ).unwrap_or_else(|| panic!("Should successfully resolve task {task_with_all_variables:?} with variables {all_variables:?}")); - - match &first_resolved_id { - None => first_resolved_id = Some(resolved_task.id.clone()), - Some(first_id) => assert_eq!( - &resolved_task.id, first_id, - "Step {i}, for the same task template and context, there should be the same resolved task id" - ), - } - - assert_eq!( - resolved_task.original_task, task_with_all_variables, - "Resolved task should store its template without changes" - ); - assert_eq!( - resolved_task.resolved_label, - format!("test label for 1234 and {long_value}"), - "Resolved task label should be substituted with variables and those should not be shortened" - ); - assert_substituted_variables( - &resolved_task, - all_variables.iter().map(|(name, _)| name.clone()).collect(), - ); - - let spawn_in_terminal = &resolved_task.resolved; - assert_eq!( - spawn_in_terminal.label, - format!( - "test label for 1234 and …{}", - &long_value[long_value.len() - MAX_DISPLAY_VARIABLE_LENGTH..] - ), - "Human-readable label should have long substitutions trimmed" - ); - assert_eq!( - spawn_in_terminal.command.clone().unwrap(), - format!("echo test_file {long_value}"), - "Command should be substituted with variables and those should not be shortened" - ); - assert_eq!( - spawn_in_terminal.args, - &[ - "arg1 test_selected_text", - "arg2 5678", - "arg3 010101010101010101010101010101010101010101010101010101010101", - ], - "Args should be substituted with variables" - ); - assert_eq!( - spawn_in_terminal.command_label, - format!( - "{} arg1 test_selected_text arg2 5678 arg3 {long_value}", - spawn_in_terminal.command.clone().unwrap() - ), - "Command label args should be substituted with variables and those should not be shortened" - ); - - assert_eq!( - spawn_in_terminal - .env - .get("test_env_key") - .map(|s| s.as_str()), - Some("test_env_var") - ); - assert_eq!( - spawn_in_terminal.env.get("env_key_1").map(|s| s.as_str()), - Some("/test_root/") - ); - assert_eq!( - spawn_in_terminal.env.get("env_key_2").map(|s| s.as_str()), - Some("env_var_2 test_custom_variable_1 test_custom_variable_2") - ); - assert_eq!( - spawn_in_terminal.env.get("env_key_3"), - Some(&format!("env_var_3 {long_value}")), - "Env vars should be substituted with variables and those should not be shortened" - ); - } - - for i in 0..all_variables.len() { - let mut not_all_variables = all_variables.to_vec(); - let removed_variable = not_all_variables.remove(i); - let resolved_task_attempt = task_with_all_variables.resolve_task( - TEST_ID_BASE, - &TaskContext { - cwd: None, - task_variables: TaskVariables::from_iter(not_all_variables), - project_env: HashMap::default(), - }, - ); - assert_eq!( - resolved_task_attempt, None, - "If any of the Zed task variables is not substituted, the task should not be resolved, but got some resolution without the variable {removed_variable:?} (index {i})" - ); - } - } - - #[test] - fn test_can_resolve_free_variables() { - let task = TaskTemplate { - label: "My task".into(), - command: "echo".into(), - args: vec!["$PATH".into()], - ..TaskTemplate::default() - }; - let resolved_task = task - .resolve_task(TEST_ID_BASE, &TaskContext::default()) - .unwrap(); - assert_substituted_variables(&resolved_task, Vec::new()); - let resolved = resolved_task.resolved; - assert_eq!(resolved.label, task.label); - assert_eq!(resolved.command, Some(task.command)); - assert_eq!(resolved.args, task.args); - } - - #[test] - fn test_errors_on_missing_zed_variable() { - let task = TaskTemplate { - label: "My task".into(), - command: "echo".into(), - args: vec!["$ZED_VARIABLE".into()], - ..TaskTemplate::default() - }; - assert!( - task.resolve_task(TEST_ID_BASE, &TaskContext::default()) - .is_none() - ); - } - - #[test] - fn test_symbol_dependent_tasks() { - let task_with_all_properties = TaskTemplate { - label: "test_label".to_string(), - command: "test_command".to_string(), - args: vec!["test_arg".to_string()], - env: HashMap::from_iter([("test_env_key".to_string(), "test_env_var".to_string())]), - ..TaskTemplate::default() - }; - let cx = TaskContext { - cwd: None, - task_variables: TaskVariables::from_iter(Some(( - VariableName::Symbol, - "test_symbol".to_string(), - ))), - project_env: HashMap::default(), - }; - - for (i, symbol_dependent_task) in [ - TaskTemplate { - label: format!("test_label_{}", VariableName::Symbol.template_value()), - ..task_with_all_properties.clone() - }, - TaskTemplate { - command: format!("test_command_{}", VariableName::Symbol.template_value()), - ..task_with_all_properties.clone() - }, - TaskTemplate { - args: vec![format!( - "test_arg_{}", - VariableName::Symbol.template_value() - )], - ..task_with_all_properties.clone() - }, - TaskTemplate { - env: HashMap::from_iter([( - "test_env_key".to_string(), - format!("test_env_var_{}", VariableName::Symbol.template_value()), - )]), - ..task_with_all_properties - }, - ] - .into_iter() - .enumerate() - { - let resolved = symbol_dependent_task - .resolve_task(TEST_ID_BASE, &cx) - .unwrap_or_else(|| panic!("Failed to resolve task {symbol_dependent_task:?}")); - assert_eq!( - resolved.substituted_variables, - HashSet::from_iter(Some(VariableName::Symbol)), - "(index {i}) Expected the task to depend on symbol task variable: {resolved:?}" - ) - } - } - - #[track_caller] - fn assert_substituted_variables(resolved_task: &ResolvedTask, mut expected: Vec) { - let mut resolved_variables = resolved_task - .substituted_variables - .iter() - .cloned() - .collect::>(); - resolved_variables.sort_by_key(|var| var.to_string()); - expected.sort_by_key(|var| var.to_string()); - assert_eq!(resolved_variables, expected) - } - - #[test] - fn substitute_funky_labels() { - let faulty_go_test = TaskTemplate { - label: format!( - "go test {}/{}", - VariableName::Symbol.template_value(), - VariableName::Symbol.template_value(), - ), - command: "go".into(), - args: vec![format!( - "^{}$/^{}$", - VariableName::Symbol.template_value(), - VariableName::Symbol.template_value() - )], - ..TaskTemplate::default() - }; - let mut context = TaskContext::default(); - context - .task_variables - .insert(VariableName::Symbol, "my-symbol".to_string()); - assert!(faulty_go_test.resolve_task("base", &context).is_some()); - } - - #[test] - fn test_project_env() { - let all_variables = [ - (VariableName::Row, "1234".to_string()), - (VariableName::Column, "5678".to_string()), - (VariableName::File, "test_file".to_string()), - (VariableName::Symbol, "my symbol".to_string()), - ]; - - let template = TaskTemplate { - label: "my task".to_string(), - command: format!( - "echo {} {}", - VariableName::File.template_value(), - VariableName::Symbol.template_value(), - ), - args: vec![], - env: HashMap::from_iter([ - ( - "TASK_ENV_VAR1".to_string(), - "TASK_ENV_VAR1_VALUE".to_string(), - ), - ( - "TASK_ENV_VAR2".to_string(), - format!( - "env_var_2 {} {}", - VariableName::Row.template_value(), - VariableName::Column.template_value() - ), - ), - ( - "PROJECT_ENV_WILL_BE_OVERWRITTEN".to_string(), - "overwritten".to_string(), - ), - ]), - ..TaskTemplate::default() - }; - - let project_env = HashMap::from_iter([ - ( - "PROJECT_ENV_VAR1".to_string(), - "PROJECT_ENV_VAR1_VALUE".to_string(), - ), - ( - "PROJECT_ENV_WILL_BE_OVERWRITTEN".to_string(), - "PROJECT_ENV_WILL_BE_OVERWRITTEN_VALUE".to_string(), - ), - ]); - - let context = TaskContext { - cwd: None, - task_variables: TaskVariables::from_iter(all_variables), - project_env, - }; - - let resolved = template - .resolve_task(TEST_ID_BASE, &context) - .unwrap() - .resolved; - - assert_eq!(resolved.env["TASK_ENV_VAR1"], "TASK_ENV_VAR1_VALUE"); - assert_eq!(resolved.env["TASK_ENV_VAR2"], "env_var_2 1234 5678"); - assert_eq!(resolved.env["PROJECT_ENV_VAR1"], "PROJECT_ENV_VAR1_VALUE"); - assert_eq!( - resolved.env["PROJECT_ENV_WILL_BE_OVERWRITTEN"], - "overwritten" - ); - } - - #[test] - fn test_variable_default_values() { - let task_with_defaults = TaskTemplate { - label: "test with defaults".to_string(), - command: format!( - "echo ${{{}}}", - VariableName::File.to_string() + ":fallback.txt" - ), - args: vec![ - "${ZED_MISSING_VAR:default_value}".to_string(), - format!("${{{}}}", VariableName::Row.to_string() + ":42"), - ], - ..TaskTemplate::default() - }; - - // Test 1: When ZED_FILE exists, should use actual value and ignore default - let context_with_file = TaskContext { - cwd: None, - task_variables: TaskVariables::from_iter(vec![ - (VariableName::File, "actual_file.rs".to_string()), - (VariableName::Row, "123".to_string()), - ]), - project_env: HashMap::default(), - }; - - let resolved = task_with_defaults - .resolve_task(TEST_ID_BASE, &context_with_file) - .expect("Should resolve task with existing variables"); - - assert_eq!( - resolved.resolved.command.unwrap(), - "echo actual_file.rs", - "Should use actual ZED_FILE value, not default" - ); - assert_eq!( - resolved.resolved.args, - vec!["default_value", "123"], - "Should use default for missing var, actual value for existing var" - ); - - // Test 2: When ZED_FILE doesn't exist, should use default value - let context_without_file = TaskContext { - cwd: None, - task_variables: TaskVariables::from_iter(vec![(VariableName::Row, "456".to_string())]), - project_env: HashMap::default(), - }; - - let resolved = task_with_defaults - .resolve_task(TEST_ID_BASE, &context_without_file) - .expect("Should resolve task using default values"); - - assert_eq!( - resolved.resolved.command.unwrap(), - "echo fallback.txt", - "Should use default value when ZED_FILE is missing" - ); - assert_eq!( - resolved.resolved.args, - vec!["default_value", "456"], - "Should use defaults for missing vars" - ); - - // Test 3: Missing ZED variable without default should fail - let task_no_default = TaskTemplate { - label: "test no default".to_string(), - command: "${ZED_MISSING_NO_DEFAULT}".to_string(), - ..TaskTemplate::default() - }; - - assert!( - task_no_default - .resolve_task(TEST_ID_BASE, &TaskContext::default()) - .is_none(), - "Should fail when ZED variable has no default and doesn't exist" - ); - } -} diff --git a/crates/task/src/vscode_debug_format.rs b/crates/task/src/vscode_debug_format.rs deleted file mode 100644 index bef64c8d40..0000000000 --- a/crates/task/src/vscode_debug_format.rs +++ /dev/null @@ -1,194 +0,0 @@ -use collections::HashMap; -use serde::Deserialize; -use util::ResultExt as _; - -use crate::{ - DebugScenario, DebugTaskFile, EnvVariableReplacer, TcpArgumentsTemplate, VariableName, -}; - -// TODO support preLaunchTask linkage with other tasks -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -struct VsCodeDebugTaskDefinition { - r#type: String, - name: String, - #[serde(default)] - port: Option, - #[serde(flatten)] - other_attributes: serde_json::Value, -} - -impl VsCodeDebugTaskDefinition { - fn try_to_zed(mut self, replacer: &EnvVariableReplacer) -> anyhow::Result { - let label = replacer.replace(&self.name); - let mut config = replacer.replace_value(self.other_attributes); - let adapter = task_type_to_adapter_name(&self.r#type); - if let Some(config) = config.as_object_mut() - && adapter == "JavaScript" - { - config.insert("type".to_owned(), self.r#type.clone().into()); - if let Some(port) = self.port.take() { - config.insert("port".to_owned(), port.into()); - } - } - let definition = DebugScenario { - label: label.into(), - build: None, - adapter: adapter.into(), - tcp_connection: self.port.map(|port| TcpArgumentsTemplate { - port: Some(port), - host: None, - timeout: None, - }), - config, - }; - Ok(definition) - } -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct VsCodeDebugTaskFile { - #[serde(default)] - version: Option, - configurations: Vec, -} - -impl TryFrom for DebugTaskFile { - type Error = anyhow::Error; - - fn try_from(file: VsCodeDebugTaskFile) -> Result { - let replacer = EnvVariableReplacer::new(HashMap::from_iter([ - ( - "workspaceFolder".to_owned(), - VariableName::WorktreeRoot.to_string(), - ), - ( - "relativeFile".to_owned(), - VariableName::RelativeFile.to_string(), - ), - ("file".to_owned(), VariableName::File.to_string()), - ])) - .with_commands([( - "pickMyProcess".to_owned(), - VariableName::PickProcessId.to_string(), - )]); - let templates = file - .configurations - .into_iter() - .filter_map(|config| config.try_to_zed(&replacer).log_err()) - .collect::>(); - Ok(DebugTaskFile(templates)) - } -} - -fn task_type_to_adapter_name(task_type: &str) -> String { - match task_type { - "pwa-node" | "node" | "node-terminal" | "chrome" | "pwa-chrome" | "edge" | "pwa-edge" - | "msedge" | "pwa-msedge" => "JavaScript", - "go" => "Delve", - "php" => "Xdebug", - "cppdbg" | "lldb" => "CodeLLDB", - "debugpy" => "Debugpy", - "rdbg" => "rdbg", - _ => task_type, - } - .to_owned() -} - -#[cfg(test)] -mod tests { - use serde_json::json; - - use crate::{DebugScenario, DebugTaskFile, VariableName}; - - use super::VsCodeDebugTaskFile; - - #[test] - fn test_parsing_vscode_launch_json() { - let raw = r#" - { - "version": "0.2.0", - "configurations": [ - { - "name": "Debug my JS app", - "request": "launch", - "type": "node", - "program": "${workspaceFolder}/xyz.js", - "showDevDebugOutput": false, - "stopOnEntry": true, - "args": ["--foo", "${workspaceFolder}/thing"], - "cwd": "${workspaceFolder}/${env:FOO}/sub", - "env": { - "X": "Y" - }, - "port": 17 - }, - ] - } - "#; - let parsed: VsCodeDebugTaskFile = - serde_json_lenient::from_str(raw).expect("deserializing launch.json"); - let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates"); - pretty_assertions::assert_eq!( - zed, - DebugTaskFile(vec![DebugScenario { - label: "Debug my JS app".into(), - adapter: "JavaScript".into(), - config: json!({ - "request": "launch", - "program": "${ZED_WORKTREE_ROOT}/xyz.js", - "showDevDebugOutput": false, - "stopOnEntry": true, - "args": [ - "--foo", - "${ZED_WORKTREE_ROOT}/thing", - ], - "cwd": "${ZED_WORKTREE_ROOT}/${FOO}/sub", - "env": { - "X": "Y", - }, - "type": "node", - "port": 17, - }), - tcp_connection: None, - build: None - }]) - ); - } - - #[test] - fn test_command_pickmyprocess_replacement() { - let raw = r#" - { - "version": "0.2.0", - "configurations": [ - { - "name": "Attach to Process", - "request": "attach", - "type": "cppdbg", - "processId": "${command:pickMyProcess}" - } - ] - } - "#; - let parsed: VsCodeDebugTaskFile = - serde_json_lenient::from_str(raw).expect("deserializing launch.json"); - let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates"); - - let expected_placeholder = format!("${{{}}}", VariableName::PickProcessId); - pretty_assertions::assert_eq!( - zed, - DebugTaskFile(vec![DebugScenario { - label: "Attach to Process".into(), - adapter: "CodeLLDB".into(), - config: json!({ - "request": "attach", - "processId": expected_placeholder, - }), - tcp_connection: None, - build: None - }]) - ); - } -} diff --git a/crates/task/src/vscode_format.rs b/crates/task/src/vscode_format.rs deleted file mode 100644 index 9078a73fbb..0000000000 --- a/crates/task/src/vscode_format.rs +++ /dev/null @@ -1,361 +0,0 @@ -use anyhow::bail; -use collections::HashMap; -use serde::Deserialize; -use util::ResultExt; - -use crate::{EnvVariableReplacer, TaskTemplate, TaskTemplates, VariableName}; - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -struct TaskOptions { - cwd: Option, - #[serde(default)] - env: HashMap, -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -struct VsCodeTaskDefinition { - label: String, - #[serde(flatten)] - command: Option, - #[serde(flatten)] - other_attributes: HashMap, - options: Option, -} - -#[derive(Clone, Deserialize, PartialEq, Debug)] -#[serde(tag = "type")] -#[serde(rename_all = "camelCase")] -enum Command { - Npm { - script: String, - }, - Shell { - command: String, - #[serde(default)] - args: Vec, - }, - Gulp { - task: String, - }, -} - -impl VsCodeTaskDefinition { - fn into_zed_format( - self, - replacer: &EnvVariableReplacer, - ) -> anyhow::Result> { - if self.other_attributes.contains_key("dependsOn") { - log::warn!( - "Skipping deserializing of a task `{}` with the unsupported `dependsOn` key", - self.label - ); - return Ok(None); - } - // `type` might not be set in e.g. tasks that use `dependsOn`; we still want to deserialize the whole object though (hence command is an Option), - // as that way we can provide more specific description of why deserialization failed. - // E.g. if the command is missing due to `dependsOn` presence, we can check other_attributes first before doing this (and provide nice error message) - // catch-all if on value.command presence. - let Some(command) = self.command else { - bail!("Missing `type` field in task"); - }; - - let (command, args) = match command { - Command::Npm { script } => ("npm".to_owned(), vec!["run".to_string(), script]), - Command::Shell { command, args } => (command, args), - Command::Gulp { task } => ("gulp".to_owned(), vec![task]), - }; - // Per VSC docs, only `command`, `args` and `options` support variable substitution. - let command = replacer.replace(&command); - let args = args.into_iter().map(|arg| replacer.replace(&arg)).collect(); - let mut template = TaskTemplate { - label: self.label, - command, - args, - ..TaskTemplate::default() - }; - if let Some(options) = self.options { - template.cwd = options.cwd.map(|cwd| replacer.replace(&cwd)); - template.env = options.env; - } - Ok(Some(template)) - } -} - -/// [`VsCodeTaskFile`] is a superset of Code's task definition format. -#[derive(Debug, Deserialize, PartialEq)] -pub struct VsCodeTaskFile { - tasks: Vec, -} - -impl TryFrom for TaskTemplates { - type Error = anyhow::Error; - - fn try_from(value: VsCodeTaskFile) -> Result { - let replacer = EnvVariableReplacer::new(HashMap::from_iter([ - ( - "workspaceFolder".to_owned(), - VariableName::WorktreeRoot.to_string(), - ), - ("file".to_owned(), VariableName::File.to_string()), - ("lineNumber".to_owned(), VariableName::Row.to_string()), - ( - "selectedText".to_owned(), - VariableName::SelectedText.to_string(), - ), - ])); - let templates = value - .tasks - .into_iter() - .filter_map(|vscode_definition| { - vscode_definition - .into_zed_format(&replacer) - .log_err() - .flatten() - }) - .collect(); - Ok(Self(templates)) - } -} - -#[cfg(test)] -mod tests { - use std::collections::HashMap; - - use crate::{ - TaskTemplate, TaskTemplates, VsCodeTaskFile, - vscode_format::{Command, VsCodeTaskDefinition}, - }; - - use super::EnvVariableReplacer; - - fn compare_without_other_attributes(lhs: VsCodeTaskDefinition, rhs: VsCodeTaskDefinition) { - assert_eq!( - VsCodeTaskDefinition { - other_attributes: Default::default(), - ..lhs - }, - VsCodeTaskDefinition { - other_attributes: Default::default(), - ..rhs - }, - ); - } - - #[test] - fn test_variable_substitution() { - let replacer = EnvVariableReplacer::new(Default::default()); - assert_eq!(replacer.replace("Food"), "Food"); - // Unknown variables are left in tact. - assert_eq!( - replacer.replace("$PATH is an environment variable"), - "$PATH is an environment variable" - ); - assert_eq!(replacer.replace("${PATH}"), "${PATH}"); - assert_eq!(replacer.replace("${PATH:food}"), "${PATH:food}"); - // And now, the actual replacing - let replacer = EnvVariableReplacer::new(HashMap::from_iter([( - "PATH".to_owned(), - "ZED_PATH".to_owned(), - )])); - assert_eq!(replacer.replace("Food"), "Food"); - assert_eq!( - replacer.replace("$PATH is an environment variable"), - "${ZED_PATH} is an environment variable" - ); - assert_eq!(replacer.replace("${PATH}"), "${ZED_PATH}"); - assert_eq!(replacer.replace("${PATH:food}"), "${ZED_PATH:food}"); - } - - #[test] - fn can_deserialize_ts_tasks() { - const TYPESCRIPT_TASKS: &str = include_str!("../test_data/typescript.json"); - let vscode_definitions: VsCodeTaskFile = - serde_json_lenient::from_str(TYPESCRIPT_TASKS).unwrap(); - - let expected = vec![ - VsCodeTaskDefinition { - label: "gulp: tests".to_string(), - command: Some(Command::Npm { - script: "build:tests:notypecheck".to_string(), - }), - other_attributes: Default::default(), - options: None, - }, - VsCodeTaskDefinition { - label: "tsc: watch ./src".to_string(), - command: Some(Command::Shell { - command: "node".to_string(), - args: vec![ - "${workspaceFolder}/node_modules/typescript/lib/tsc.js".to_string(), - "--build".to_string(), - "${workspaceFolder}/src".to_string(), - "--watch".to_string(), - ], - }), - other_attributes: Default::default(), - options: None, - }, - VsCodeTaskDefinition { - label: "npm: build:compiler".to_string(), - command: Some(Command::Npm { - script: "build:compiler".to_string(), - }), - other_attributes: Default::default(), - options: None, - }, - VsCodeTaskDefinition { - label: "npm: build:tests".to_string(), - command: Some(Command::Npm { - script: "build:tests:notypecheck".to_string(), - }), - other_attributes: Default::default(), - options: None, - }, - ]; - - assert_eq!(vscode_definitions.tasks.len(), expected.len()); - vscode_definitions - .tasks - .iter() - .zip(expected) - .for_each(|(lhs, rhs)| compare_without_other_attributes(lhs.clone(), rhs)); - - let expected = vec![ - TaskTemplate { - label: "gulp: tests".to_string(), - command: "npm".to_string(), - args: vec!["run".to_string(), "build:tests:notypecheck".to_string()], - ..Default::default() - }, - TaskTemplate { - label: "tsc: watch ./src".to_string(), - command: "node".to_string(), - args: vec![ - "${ZED_WORKTREE_ROOT}/node_modules/typescript/lib/tsc.js".to_string(), - "--build".to_string(), - "${ZED_WORKTREE_ROOT}/src".to_string(), - "--watch".to_string(), - ], - ..Default::default() - }, - TaskTemplate { - label: "npm: build:compiler".to_string(), - command: "npm".to_string(), - args: vec!["run".to_string(), "build:compiler".to_string()], - ..Default::default() - }, - TaskTemplate { - label: "npm: build:tests".to_string(), - command: "npm".to_string(), - args: vec!["run".to_string(), "build:tests:notypecheck".to_string()], - ..Default::default() - }, - ]; - - let tasks: TaskTemplates = vscode_definitions.try_into().unwrap(); - assert_eq!(tasks.0, expected); - } - - #[test] - fn can_deserialize_rust_analyzer_tasks() { - const RUST_ANALYZER_TASKS: &str = include_str!("../test_data/rust-analyzer.json"); - let vscode_definitions: VsCodeTaskFile = - serde_json_lenient::from_str(RUST_ANALYZER_TASKS).unwrap(); - let expected = vec![ - VsCodeTaskDefinition { - label: "Build Extension in Background".to_string(), - command: Some(Command::Npm { - script: "watch".to_string(), - }), - options: None, - other_attributes: Default::default(), - }, - VsCodeTaskDefinition { - label: "Build Extension".to_string(), - command: Some(Command::Npm { - script: "build".to_string(), - }), - options: None, - other_attributes: Default::default(), - }, - VsCodeTaskDefinition { - label: "Build Server".to_string(), - command: Some(Command::Shell { - command: "cargo build --package rust-analyzer".to_string(), - args: Default::default(), - }), - options: None, - other_attributes: Default::default(), - }, - VsCodeTaskDefinition { - label: "Build Server (Release)".to_string(), - command: Some(Command::Shell { - command: "cargo build --release --package rust-analyzer".to_string(), - args: Default::default(), - }), - options: None, - other_attributes: Default::default(), - }, - VsCodeTaskDefinition { - label: "Pretest".to_string(), - command: Some(Command::Npm { - script: "pretest".to_string(), - }), - options: None, - other_attributes: Default::default(), - }, - VsCodeTaskDefinition { - label: "Build Server and Extension".to_string(), - command: None, - options: None, - other_attributes: Default::default(), - }, - VsCodeTaskDefinition { - label: "Build Server (Release) and Extension".to_string(), - command: None, - options: None, - other_attributes: Default::default(), - }, - ]; - assert_eq!(vscode_definitions.tasks.len(), expected.len()); - vscode_definitions - .tasks - .iter() - .zip(expected) - .for_each(|(lhs, rhs)| compare_without_other_attributes(lhs.clone(), rhs)); - let expected = vec![ - TaskTemplate { - label: "Build Extension in Background".to_string(), - command: "npm".to_string(), - args: vec!["run".to_string(), "watch".to_string()], - ..Default::default() - }, - TaskTemplate { - label: "Build Extension".to_string(), - command: "npm".to_string(), - args: vec!["run".to_string(), "build".to_string()], - ..Default::default() - }, - TaskTemplate { - label: "Build Server".to_string(), - command: "cargo build --package rust-analyzer".to_string(), - ..Default::default() - }, - TaskTemplate { - label: "Build Server (Release)".to_string(), - command: "cargo build --release --package rust-analyzer".to_string(), - ..Default::default() - }, - TaskTemplate { - label: "Pretest".to_string(), - command: "npm".to_string(), - args: vec!["run".to_string(), "pretest".to_string()], - ..Default::default() - }, - ]; - let tasks: TaskTemplates = vscode_definitions.try_into().unwrap(); - assert_eq!(tasks.0, expected); - } -} diff --git a/crates/task/test_data/rust-analyzer.json b/crates/task/test_data/rust-analyzer.json deleted file mode 100644 index 0ea585c4b8..0000000000 --- a/crates/task/test_data/rust-analyzer.json +++ /dev/null @@ -1,67 +0,0 @@ -// See https://go.microsoft.com/fwlink/?LinkId=733558 -// for the documentation about the tasks.json format -{ - "version": "2.0.0", - "tasks": [ - { - "label": "Build Extension in Background", - "group": "build", - "type": "npm", - "script": "watch", - "path": "editors/code/", - "problemMatcher": { - "base": "$tsc-watch", - "fileLocation": ["relative", "${workspaceFolder}/editors/code/"] - }, - "isBackground": true - }, - { - "label": "Build Extension", - "group": "build", - "type": "npm", - "script": "build", - "path": "editors/code/", - "problemMatcher": { - "base": "$tsc", - "fileLocation": ["relative", "${workspaceFolder}/editors/code/"] - } - }, - { - "label": "Build Server", - "group": "build", - "type": "shell", - "command": "cargo build --package rust-analyzer", - "problemMatcher": "$rustc" - }, - { - "label": "Build Server (Release)", - "group": "build", - "type": "shell", - "command": "cargo build --release --package rust-analyzer", - "problemMatcher": "$rustc" - }, - { - "label": "Pretest", - "group": "build", - "isBackground": false, - "type": "npm", - "script": "pretest", - "path": "editors/code/", - "problemMatcher": { - "base": "$tsc", - "fileLocation": ["relative", "${workspaceFolder}/editors/code/"] - } - }, - - { - "label": "Build Server and Extension", - "dependsOn": ["Build Server", "Build Extension"], - "problemMatcher": "$rustc" - }, - { - "label": "Build Server (Release) and Extension", - "dependsOn": ["Build Server (Release)", "Build Extension"], - "problemMatcher": "$rustc" - } - ] -} diff --git a/crates/task/test_data/typescript.json b/crates/task/test_data/typescript.json deleted file mode 100644 index 91d2343682..0000000000 --- a/crates/task/test_data/typescript.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "2.0.0", - "tasks": [ - { - // Kept for backwards compat for old launch.json files so it's - // less annoying if moving up to the new build or going back to - // the old build. - // - // This is first because the actual "npm: build:tests" task - // below has the same script value, and VS Code ignores labels - // and deduplicates them. - // https://github.com/microsoft/vscode/issues/93001 - "label": "gulp: tests", - "type": "npm", - "script": "build:tests:notypecheck", - "group": "build", - "hide": true, - "problemMatcher": ["$tsc"] - }, - { - "label": "tsc: watch ./src", - "type": "shell", - "command": "node", - "args": [ - "${workspaceFolder}/node_modules/typescript/lib/tsc.js", - "--build", - "${workspaceFolder}/src", - "--watch" - ], - "group": "build", - "isBackground": true, - "problemMatcher": ["$tsc-watch"] - }, - { - "label": "npm: build:compiler", - "type": "npm", - "script": "build:compiler", - "group": "build", - "problemMatcher": ["$tsc"] - }, - { - "label": "npm: build:tests", - "type": "npm", - "script": "build:tests:notypecheck", - "group": "build", - "problemMatcher": ["$tsc"] - } - ] -} diff --git a/crates/tasks_ui/Cargo.toml b/crates/tasks_ui/Cargo.toml deleted file mode 100644 index 2f75a0b57c..0000000000 --- a/crates/tasks_ui/Cargo.toml +++ /dev/null @@ -1,41 +0,0 @@ -[package] -name = "tasks_ui" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/tasks_ui.rs" - -[dependencies] -anyhow.workspace = true -collections.workspace = true -editor.workspace = true -file_icons.workspace = true -fuzzy.workspace = true -itertools.workspace = true -gpui.workspace = true -menu.workspace = true -picker.workspace = true -project.workspace = true -task.workspace = true -serde.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -language.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -editor = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } -serde_json.workspace = true -tree-sitter-rust.workspace = true -tree-sitter-typescript.workspace = true -workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/tasks_ui/LICENSE-GPL b/crates/tasks_ui/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/tasks_ui/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs deleted file mode 100644 index 644f82285b..0000000000 --- a/crates/tasks_ui/src/modal.rs +++ /dev/null @@ -1,1312 +0,0 @@ -use std::sync::Arc; - -use crate::TaskContexts; -use editor::Editor; -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{ - Action, AnyElement, App, AppContext as _, Context, DismissEvent, Entity, EventEmitter, - Focusable, InteractiveElement, ParentElement, Render, Styled, Subscription, Task, WeakEntity, - Window, rems, -}; -use itertools::Itertools; -use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; -use project::{TaskSourceKind, task_store::TaskStore}; -use task::{DebugScenario, ResolvedTask, RevealTarget, TaskContext, TaskTemplate}; -use ui::{ - ActiveTheme, Clickable, FluentBuilder as _, IconButtonShape, IconWithIndicator, Indicator, - IntoElement, KeyBinding, ListItem, ListItemSpacing, RenderOnce, Toggleable, Tooltip, div, - prelude::*, -}; - -use util::{ResultExt, truncate_and_trailoff}; -use workspace::{ModalView, Workspace}; -pub use zed_actions::{Rerun, Spawn}; - -/// A modal used to spawn new tasks. -pub struct TasksModalDelegate { - task_store: Entity, - candidates: Option>, - task_overrides: Option, - last_used_candidate_index: Option, - divider_index: Option, - matches: Vec, - selected_index: usize, - workspace: WeakEntity, - prompt: String, - task_contexts: Arc, - placeholder_text: Arc, -} - -/// Task template amendments to do before resolving the context. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct TaskOverrides { - /// See [`RevealTarget`]. - pub reveal_target: Option, -} - -impl TasksModalDelegate { - fn new( - task_store: Entity, - task_contexts: Arc, - task_overrides: Option, - workspace: WeakEntity, - ) -> Self { - let placeholder_text = if let Some(TaskOverrides { - reveal_target: Some(RevealTarget::Center), - }) = &task_overrides - { - Arc::from("Find a task, or run a command in the central pane") - } else { - Arc::from("Find a task, or run a command") - }; - Self { - task_store, - workspace, - candidates: None, - matches: Vec::new(), - last_used_candidate_index: None, - divider_index: None, - selected_index: 0, - prompt: String::default(), - task_contexts, - task_overrides, - placeholder_text, - } - } - - fn spawn_oneshot(&mut self) -> Option<(TaskSourceKind, ResolvedTask)> { - if self.prompt.trim().is_empty() { - return None; - } - - let default_context = TaskContext::default(); - let active_context = self - .task_contexts - .active_context() - .unwrap_or(&default_context); - let source_kind = TaskSourceKind::UserInput; - let id_base = source_kind.to_id_base(); - let mut new_oneshot = TaskTemplate { - label: self.prompt.clone(), - command: self.prompt.clone(), - ..TaskTemplate::default() - }; - if let Some(TaskOverrides { - reveal_target: Some(reveal_target), - }) = &self.task_overrides - { - new_oneshot.reveal_target = *reveal_target; - } - Some(( - source_kind, - new_oneshot.resolve_task(&id_base, active_context)?, - )) - } - - fn delete_previously_used(&mut self, ix: usize, cx: &mut App) { - let Some(candidates) = self.candidates.as_mut() else { - return; - }; - let Some(task) = candidates.get(ix).map(|(_, task)| task.clone()) else { - return; - }; - // We remove this candidate manually instead of .taking() the candidates, as we already know the index; - // it doesn't make sense to requery the inventory for new candidates, as that's potentially costly and more often than not it should just return back - // the original list without a removed entry. - candidates.remove(ix); - if let Some(inventory) = self.task_store.read(cx).task_inventory().cloned() { - inventory.update(cx, |inventory, _| { - inventory.delete_previously_used(&task.id); - }) - }; - } -} - -pub struct TasksModal { - pub picker: Entity>, - _subscription: [Subscription; 2], -} - -impl TasksModal { - pub fn new( - task_store: Entity, - task_contexts: Arc, - task_overrides: Option, - is_modal: bool, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let picker = cx.new(|cx| { - Picker::uniform_list( - TasksModalDelegate::new(task_store, task_contexts, task_overrides, workspace), - window, - cx, - ) - .modal(is_modal) - }); - let _subscription = [ - cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| { - cx.emit(DismissEvent); - }), - cx.subscribe(&picker, |_, _, event: &ShowAttachModal, cx| { - cx.emit(ShowAttachModal { - debug_config: event.debug_config.clone(), - }); - }), - ]; - Self { - picker, - _subscription, - } - } - - pub fn tasks_loaded( - &mut self, - task_contexts: Arc, - lsp_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>, - used_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>, - current_resolved_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>, - add_current_language_tasks: bool, - window: &mut Window, - cx: &mut Context, - ) { - let last_used_candidate_index = if used_tasks.is_empty() { - None - } else { - Some(used_tasks.len() - 1) - }; - let mut new_candidates = used_tasks; - new_candidates.extend(lsp_tasks); - let hide_vscode = current_resolved_tasks.iter().any(|(kind, _)| match kind { - TaskSourceKind::Worktree { - id: _, - directory_in_worktree: dir, - id_base: _, - } => dir.file_name().is_some_and(|name| name == ".zed"), - _ => false, - }); - // todo(debugger): We're always adding lsp tasks here even if prefer_lsp is false - // We should move the filter to new_candidates instead of on current - // and add a test for this - new_candidates.extend(current_resolved_tasks.into_iter().filter(|(task_kind, _)| { - match task_kind { - TaskSourceKind::Worktree { - directory_in_worktree: dir, - .. - } => !(hide_vscode && dir.file_name().is_some_and(|name| name == ".vscode")), - TaskSourceKind::Language { .. } => add_current_language_tasks, - _ => true, - } - })); - self.picker.update(cx, |picker, cx| { - picker.delegate.task_contexts = task_contexts; - picker.delegate.last_used_candidate_index = last_used_candidate_index; - picker.delegate.candidates = Some(new_candidates); - picker.refresh(window, cx); - cx.notify(); - }) - } -} - -impl Render for TasksModal { - fn render( - &mut self, - _window: &mut Window, - _: &mut Context, - ) -> impl gpui::prelude::IntoElement { - v_flex() - .key_context("TasksModal") - .w(rems(34.)) - .child(self.picker.clone()) - } -} - -pub struct ShowAttachModal { - pub debug_config: DebugScenario, -} - -impl EventEmitter for TasksModal {} -impl EventEmitter for TasksModal {} -impl EventEmitter for Picker {} - -impl Focusable for TasksModal { - fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle { - self.picker.read(cx).focus_handle(cx) - } -} - -impl ModalView for TasksModal {} - -const MAX_TAGS_LINE_LEN: usize = 30; - -impl PickerDelegate for TasksModalDelegate { - type ListItem = ListItem; - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _window: &mut Window, - _cx: &mut Context>, - ) { - self.selected_index = ix; - } - - fn placeholder_text(&self, _window: &mut Window, _: &mut App) -> Arc { - self.placeholder_text.clone() - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - let candidates = match &self.candidates { - Some(candidates) => Task::ready(string_match_candidates(candidates)), - None => { - if let Some(task_inventory) = self.task_store.read(cx).task_inventory().cloned() { - let task_list = task_inventory.update(cx, |this, cx| { - this.used_and_current_resolved_tasks(self.task_contexts.clone(), cx) - }); - let workspace = self.workspace.clone(); - let lsp_task_sources = self.task_contexts.lsp_task_sources.clone(); - let task_position = self.task_contexts.latest_selection; - cx.spawn(async move |picker, cx| { - let (used, current) = task_list.await; - let Ok((lsp_tasks, prefer_lsp)) = workspace.update(cx, |workspace, cx| { - let lsp_tasks = editor::lsp_tasks( - workspace.project().clone(), - &lsp_task_sources, - task_position, - cx, - ); - let prefer_lsp = workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - .map(|editor| { - editor - .read(cx) - .buffer() - .read(cx) - .language_settings(cx) - .tasks - .prefer_lsp - }) - .unwrap_or(false); - (lsp_tasks, prefer_lsp) - }) else { - return Vec::new(); - }; - - let lsp_tasks = lsp_tasks.await; - picker - .update(cx, |picker, _| { - picker.delegate.last_used_candidate_index = if used.is_empty() { - None - } else { - Some(used.len() - 1) - }; - - let mut new_candidates = used; - let add_current_language_tasks = - !prefer_lsp || lsp_tasks.is_empty(); - new_candidates.extend(lsp_tasks.into_iter().flat_map( - |(kind, tasks_with_locations)| { - tasks_with_locations - .into_iter() - .sorted_by_key(|(location, task)| { - (location.is_none(), task.resolved_label.clone()) - }) - .map(move |(_, task)| (kind.clone(), task)) - }, - )); - // todo(debugger): We're always adding lsp tasks here even if prefer_lsp is false - // We should move the filter to new_candidates instead of on current - // and add a test for this - new_candidates.extend(current.into_iter().filter( - |(task_kind, _)| { - add_current_language_tasks - || !matches!(task_kind, TaskSourceKind::Language { .. }) - }, - )); - let match_candidates = string_match_candidates(&new_candidates); - let _ = picker.delegate.candidates.insert(new_candidates); - match_candidates - }) - .ok() - .unwrap_or_default() - }) - } else { - Task::ready(Vec::new()) - } - } - }; - - cx.spawn_in(window, async move |picker, cx| { - let candidates = candidates.await; - let matches = fuzzy::match_strings( - &candidates, - &query, - true, - true, - 1000, - &Default::default(), - cx.background_executor().clone(), - ) - .await; - picker - .update(cx, |picker, _| { - let delegate = &mut picker.delegate; - delegate.matches = matches; - if let Some(index) = delegate.last_used_candidate_index { - delegate.matches.sort_by_key(|m| m.candidate_id > index); - } - - delegate.prompt = query; - delegate.divider_index = delegate.last_used_candidate_index.and_then(|index| { - let index = delegate - .matches - .partition_point(|matching_task| matching_task.candidate_id <= index); - Some(index).and_then(|index| (index != 0).then(|| index - 1)) - }); - - if delegate.matches.is_empty() { - delegate.selected_index = 0; - } else { - delegate.selected_index = - delegate.selected_index.min(delegate.matches.len() - 1); - } - }) - .log_err(); - }) - } - - fn confirm( - &mut self, - omit_history_entry: bool, - window: &mut Window, - cx: &mut Context>, - ) { - let current_match_index = self.selected_index(); - let task = self - .matches - .get(current_match_index) - .and_then(|current_match| { - let ix = current_match.candidate_id; - self.candidates - .as_ref() - .map(|candidates| candidates[ix].clone()) - }); - let Some((task_source_kind, mut task)) = task else { - return; - }; - if let Some(TaskOverrides { - reveal_target: Some(reveal_target), - }) = &self.task_overrides - { - task.resolved.reveal_target = *reveal_target; - } - - self.workspace - .update(cx, |workspace, cx| { - workspace.schedule_resolved_task( - task_source_kind, - task, - omit_history_entry, - window, - cx, - ); - }) - .ok(); - - cx.emit(DismissEvent); - } - - fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { - cx.emit(DismissEvent); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - window: &mut Window, - cx: &mut Context>, - ) -> Option { - let candidates = self.candidates.as_ref()?; - let hit = &self.matches.get(ix)?; - let (source_kind, resolved_task) = &candidates.get(hit.candidate_id)?; - let template = resolved_task.original_task(); - let display_label = resolved_task.display_label(); - - let mut tooltip_label_text = - if display_label != &template.label || source_kind == &TaskSourceKind::UserInput { - resolved_task.resolved_label.clone() - } else { - String::new() - }; - - if resolved_task.resolved.command_label != resolved_task.resolved_label { - if !tooltip_label_text.trim().is_empty() { - tooltip_label_text.push('\n'); - } - tooltip_label_text.push_str(&resolved_task.resolved.command_label); - } - - if !template.tags.is_empty() { - tooltip_label_text.push('\n'); - tooltip_label_text.push_str( - template - .tags - .iter() - .map(|tag| format!("\n#{}", tag)) - .collect::>() - .join("") - .as_str(), - ); - } - let tooltip_label = if tooltip_label_text.trim().is_empty() { - None - } else { - Some(Tooltip::simple(tooltip_label_text, cx)) - }; - - let highlighted_location = HighlightedMatch { - text: hit.string.clone(), - highlight_positions: hit.positions.clone(), - color: Color::Default, - }; - let icon = match source_kind { - TaskSourceKind::UserInput => Some(Icon::new(IconName::Terminal)), - TaskSourceKind::AbsPath { .. } => Some(Icon::new(IconName::Settings)), - TaskSourceKind::Worktree { .. } => Some(Icon::new(IconName::FileTree)), - TaskSourceKind::Lsp { - language_name: name, - .. - } - | TaskSourceKind::Language { name, .. } => file_icons::FileIcons::get(cx) - .get_icon_for_type(&name.to_lowercase(), cx) - .map(Icon::from_path), - } - .map(|icon| icon.color(Color::Muted).size(IconSize::Small)); - let indicator = if matches!(source_kind, TaskSourceKind::Lsp { .. }) { - Some(Indicator::icon( - Icon::new(IconName::BoltOutlined).size(IconSize::Small), - )) - } else { - None - }; - let icon = icon.map(|icon| { - IconWithIndicator::new(icon, indicator) - .indicator_border_color(Some(cx.theme().colors().border_transparent)) - }); - let history_run_icon = if Some(ix) <= self.divider_index { - Some( - Icon::new(IconName::HistoryRerun) - .color(Color::Muted) - .size(IconSize::Small) - .into_any_element(), - ) - } else { - Some( - v_flex() - .flex_none() - .size(IconSize::Small.rems()) - .into_any_element(), - ) - }; - - Some( - ListItem::new(format!("tasks-modal-{ix}")) - .inset(true) - .start_slot::(icon) - .end_slot::( - h_flex() - .gap_1() - .child(Label::new(truncate_and_trailoff( - &template - .tags - .iter() - .map(|tag| format!("#{}", tag)) - .collect::>() - .join(" "), - MAX_TAGS_LINE_LEN, - ))) - .flex_none() - .child(history_run_icon.unwrap()) - .into_any_element(), - ) - .spacing(ListItemSpacing::Sparse) - .when_some(tooltip_label, |list_item, item_label| { - list_item.tooltip(move |_, _| item_label.clone()) - }) - .map(|item| { - if matches!(source_kind, TaskSourceKind::UserInput) - || Some(ix) <= self.divider_index - { - let task_index = hit.candidate_id; - let delete_button = div().child( - IconButton::new("delete", IconName::Close) - .shape(IconButtonShape::Square) - .icon_color(Color::Muted) - .size(ButtonSize::None) - .icon_size(IconSize::XSmall) - .on_click(cx.listener(move |picker, _event, window, cx| { - cx.stop_propagation(); - window.prevent_default(); - - picker.delegate.delete_previously_used(task_index, cx); - picker.delegate.last_used_candidate_index = picker - .delegate - .last_used_candidate_index - .unwrap_or(0) - .checked_sub(1); - picker.refresh(window, cx); - })) - .tooltip(|_, cx| { - Tooltip::simple("Delete Previously Scheduled Task", cx) - }), - ); - item.end_hover_slot(delete_button) - } else { - item - } - }) - .toggle_state(selected) - .child(highlighted_location.render(window, cx)), - ) - } - - fn confirm_completion( - &mut self, - _: String, - _window: &mut Window, - _: &mut Context>, - ) -> Option { - let task_index = self.matches.get(self.selected_index())?.candidate_id; - let tasks = self.candidates.as_ref()?; - let (_, task) = tasks.get(task_index)?; - Some(task.resolved.command_label.clone()) - } - - fn confirm_input( - &mut self, - omit_history_entry: bool, - window: &mut Window, - cx: &mut Context>, - ) { - let Some((task_source_kind, mut task)) = self.spawn_oneshot() else { - return; - }; - - if let Some(TaskOverrides { - reveal_target: Some(reveal_target), - }) = self.task_overrides - { - task.resolved.reveal_target = reveal_target; - } - self.workspace - .update(cx, |workspace, cx| { - workspace.schedule_resolved_task( - task_source_kind, - task, - omit_history_entry, - window, - cx, - ) - }) - .ok(); - cx.emit(DismissEvent); - } - - fn separators_after_indices(&self) -> Vec { - if let Some(i) = self.divider_index { - vec![i] - } else { - Vec::new() - } - } - - fn render_footer( - &self, - window: &mut Window, - cx: &mut Context>, - ) -> Option { - let is_recent_selected = self.divider_index >= Some(self.selected_index); - let current_modifiers = window.modifiers(); - let left_button = if self - .task_store - .read(cx) - .task_inventory()? - .read(cx) - .last_scheduled_task(None) - .is_some() - { - Some(("Rerun Last Task", Rerun::default().boxed_clone())) - } else { - None - }; - Some( - h_flex() - .w_full() - .p_1p5() - .justify_between() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child( - left_button - .map(|(label, action)| { - let keybind = KeyBinding::for_action(&*action, cx); - - Button::new("edit-current-task", label) - .key_binding(keybind) - .on_click(move |_, window, cx| { - window.dispatch_action(action.boxed_clone(), cx); - }) - .into_any_element() - }) - .unwrap_or_else(|| h_flex().into_any_element()), - ) - .map(|this| { - if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() - { - let action = picker::ConfirmInput { - secondary: current_modifiers.secondary(), - } - .boxed_clone(); - this.child({ - let spawn_oneshot_label = if current_modifiers.secondary() { - "Spawn Oneshot Without History" - } else { - "Spawn Oneshot" - }; - - Button::new("spawn-onehshot", spawn_oneshot_label) - .key_binding(KeyBinding::for_action(&*action, cx)) - .on_click(move |_, window, cx| { - window.dispatch_action(action.boxed_clone(), cx) - }) - }) - } else if current_modifiers.secondary() { - this.child({ - let label = if is_recent_selected { - "Rerun Without History" - } else { - "Spawn Without History" - }; - Button::new("spawn", label) - .key_binding(KeyBinding::for_action(&menu::SecondaryConfirm, cx)) - .on_click(move |_, window, cx| { - window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx) - }) - }) - } else { - this.child({ - let run_entry_label = - if is_recent_selected { "Rerun" } else { "Spawn" }; - - Button::new("spawn", run_entry_label) - .key_binding(KeyBinding::for_action(&menu::Confirm, cx)) - .on_click(|_, window, cx| { - window.dispatch_action(menu::Confirm.boxed_clone(), cx); - }) - }) - } - }) - .into_any_element(), - ) - } -} - -fn string_match_candidates<'a>( - candidates: impl IntoIterator + 'a, -) -> Vec { - candidates - .into_iter() - .enumerate() - .map(|(index, (_, candidate))| StringMatchCandidate::new(index, candidate.display_label())) - .collect() -} - -#[cfg(test)] -mod tests { - use std::{path::PathBuf, sync::Arc}; - - use editor::{Editor, SelectionEffects}; - use gpui::{TestAppContext, VisualTestContext}; - use language::{Language, LanguageConfig, LanguageMatcher, Point}; - use project::{ContextProviderWithTasks, FakeFs, Project}; - use serde_json::json; - use task::TaskTemplates; - use util::path; - use workspace::{CloseInactiveTabsAndPanes, OpenOptions, OpenVisible}; - - use crate::{modal::Spawn, tests::init_test}; - - use super::*; - - #[gpui::test] - async fn test_spawn_tasks_modal_query_reuse(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({ - ".zed": { - "tasks.json": r#"[ - { - "label": "example task", - "command": "echo", - "args": ["4"] - }, - { - "label": "another one", - "command": "echo", - "args": ["55"] - }, - ]"#, - }, - "a.ts": "a" - }), - ) - .await; - - let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - query(&tasks_picker, cx), - "", - "Initial query should be empty" - ); - assert_eq!( - task_names(&tasks_picker, cx), - vec!["another one", "example task"], - "With no global tasks and no open item, a single worktree should be used and its tasks listed" - ); - drop(tasks_picker); - - let _ = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from(path!("/dir/a.ts")), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .await - .unwrap(); - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - task_names(&tasks_picker, cx), - vec!["another one", "example task"], - "Initial tasks should be listed in alphabetical order" - ); - - let query_str = "tas"; - cx.simulate_input(query_str); - assert_eq!(query(&tasks_picker, cx), query_str); - assert_eq!( - task_names(&tasks_picker, cx), - vec!["example task"], - "Only one task should match the query {query_str}" - ); - - cx.dispatch_action(picker::ConfirmCompletion); - assert_eq!( - query(&tasks_picker, cx), - "echo 4", - "Query should be set to the selected task's command" - ); - assert_eq!( - task_names(&tasks_picker, cx), - Vec::::new(), - "No task should be listed" - ); - cx.dispatch_action(picker::ConfirmInput { secondary: false }); - - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - query(&tasks_picker, cx), - "", - "Query should be reset after confirming" - ); - assert_eq!( - task_names(&tasks_picker, cx), - vec!["echo 4", "another one", "example task"], - "New oneshot task should be listed first" - ); - - let query_str = "echo 4"; - cx.simulate_input(query_str); - assert_eq!(query(&tasks_picker, cx), query_str); - assert_eq!( - task_names(&tasks_picker, cx), - vec!["echo 4"], - "New oneshot should match custom command query" - ); - - cx.dispatch_action(picker::ConfirmInput { secondary: false }); - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - query(&tasks_picker, cx), - "", - "Query should be reset after confirming" - ); - assert_eq!( - task_names(&tasks_picker, cx), - vec![query_str, "another one", "example task"], - "Last recently used one show task should be listed first" - ); - - cx.dispatch_action(picker::ConfirmCompletion); - assert_eq!( - query(&tasks_picker, cx), - query_str, - "Query should be set to the custom task's name" - ); - assert_eq!( - task_names(&tasks_picker, cx), - vec![query_str], - "Only custom task should be listed" - ); - - let query_str = "0"; - cx.simulate_input(query_str); - assert_eq!(query(&tasks_picker, cx), "echo 40"); - assert_eq!( - task_names(&tasks_picker, cx), - Vec::::new(), - "New oneshot should not match any command query" - ); - - cx.dispatch_action(picker::ConfirmInput { secondary: true }); - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - query(&tasks_picker, cx), - "", - "Query should be reset after confirming" - ); - assert_eq!( - task_names(&tasks_picker, cx), - vec!["echo 4", "another one", "example task"], - "No query should be added to the list, as it was submitted with secondary action (that maps to omit_history = true)" - ); - - cx.dispatch_action(Spawn::ByName { - task_name: "example task".to_string(), - reveal_target: None, - }); - let tasks_picker = workspace.update(cx, |workspace, cx| { - workspace - .active_modal::(cx) - .unwrap() - .read(cx) - .picker - .clone() - }); - assert_eq!( - task_names(&tasks_picker, cx), - vec!["echo 4", "another one", "example task"], - ); - } - - #[gpui::test] - async fn test_basic_context_for_simple_files(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({ - ".zed": { - "tasks.json": r#"[ - { - "label": "hello from $ZED_FILE:$ZED_ROW:$ZED_COLUMN", - "command": "echo", - "args": ["hello", "from", "$ZED_FILE", ":", "$ZED_ROW", ":", "$ZED_COLUMN"] - }, - { - "label": "opened now: $ZED_WORKTREE_ROOT", - "command": "echo", - "args": ["opened", "now:", "$ZED_WORKTREE_ROOT"] - } - ]"#, - }, - "file_without_extension": "aaaaaaaaaaaaaaaaaaaa\naaaaaaaaaaaaaaaaaa", - "file_with.odd_extension": "b", - }), - ) - .await; - - let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - task_names(&tasks_picker, cx), - vec![concat!("opened now: ", path!("/dir")).to_string()], - "When no file is open for a single worktree, should autodetect all worktree-related tasks" - ); - tasks_picker.update(cx, |_, cx| { - cx.emit(DismissEvent); - }); - drop(tasks_picker); - cx.executor().run_until_parked(); - - let _ = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from(path!("/dir/file_with.odd_extension")), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .await - .unwrap(); - cx.executor().run_until_parked(); - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - task_names(&tasks_picker, cx), - vec![ - concat!("hello from ", path!("/dir/file_with.odd_extension:1:1")).to_string(), - concat!("opened now: ", path!("/dir")).to_string(), - ], - "Second opened buffer should fill the context, labels should be trimmed if long enough" - ); - tasks_picker.update(cx, |_, cx| { - cx.emit(DismissEvent); - }); - drop(tasks_picker); - cx.executor().run_until_parked(); - - let second_item = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from(path!("/dir/file_without_extension")), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .await - .unwrap(); - - let editor = cx - .update(|_window, cx| second_item.act_as::(cx)) - .unwrap(); - editor.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges(Some(Point::new(1, 2)..Point::new(1, 5))) - }) - }); - cx.executor().run_until_parked(); - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - task_names(&tasks_picker, cx), - vec![ - concat!("hello from ", path!("/dir/file_without_extension:2:3")).to_string(), - concat!("opened now: ", path!("/dir")).to_string(), - ], - "Opened buffer should fill the context, labels should be trimmed if long enough" - ); - tasks_picker.update(cx, |_, cx| { - cx.emit(DismissEvent); - }); - drop(tasks_picker); - cx.executor().run_until_parked(); - } - - #[gpui::test] - async fn test_language_task_filtering(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({ - "a1.ts": "// a1", - "a2.ts": "// a2", - "b.rs": "// b", - }), - ) - .await; - - let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - project.read_with(cx, |project, _| { - let language_registry = project.languages(); - language_registry.add(Arc::new( - Language::new( - LanguageConfig { - name: "TypeScript".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["ts".to_string()], - ..LanguageMatcher::default() - }, - ..LanguageConfig::default() - }, - None, - ) - .with_context_provider(Some(Arc::new( - ContextProviderWithTasks::new(TaskTemplates(vec![ - TaskTemplate { - label: "Task without variables".to_string(), - command: "npm run clean".to_string(), - ..TaskTemplate::default() - }, - TaskTemplate { - label: "TypeScript task from file $ZED_FILE".to_string(), - command: "npm run build".to_string(), - ..TaskTemplate::default() - }, - TaskTemplate { - label: "Another task from file $ZED_FILE".to_string(), - command: "npm run lint".to_string(), - ..TaskTemplate::default() - }, - ])), - ))), - )); - language_registry.add(Arc::new( - Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..LanguageMatcher::default() - }, - ..LanguageConfig::default() - }, - None, - ) - .with_context_provider(Some(Arc::new( - ContextProviderWithTasks::new(TaskTemplates(vec![TaskTemplate { - label: "Rust task".to_string(), - command: "cargo check".into(), - ..TaskTemplate::default() - }])), - ))), - )); - }); - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let _ts_file_1 = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from(path!("/dir/a1.ts")), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .await - .unwrap(); - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - task_names(&tasks_picker, cx), - vec![ - concat!("Another task from file ", path!("/dir/a1.ts")), - concat!("TypeScript task from file ", path!("/dir/a1.ts")), - "Task without variables", - ], - "Should open spawn TypeScript tasks for the opened file, tasks with most template variables above, all groups sorted alphanumerically" - ); - - emulate_task_schedule( - tasks_picker, - &project, - concat!("TypeScript task from file ", path!("/dir/a1.ts")), - cx, - ); - - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - task_names(&tasks_picker, cx), - vec![ - concat!("TypeScript task from file ", path!("/dir/a1.ts")), - concat!("Another task from file ", path!("/dir/a1.ts")), - "Task without variables", - ], - "After spawning the task and getting it into the history, it should be up in the sort as recently used. - Tasks with the same labels and context are deduplicated." - ); - tasks_picker.update(cx, |_, cx| { - cx.emit(DismissEvent); - }); - drop(tasks_picker); - cx.executor().run_until_parked(); - - let _ts_file_2 = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from(path!("/dir/a2.ts")), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .await - .unwrap(); - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - task_names(&tasks_picker, cx), - vec![ - concat!("TypeScript task from file ", path!("/dir/a1.ts")), - concat!("Another task from file ", path!("/dir/a2.ts")), - concat!("TypeScript task from file ", path!("/dir/a2.ts")), - "Task without variables", - ], - "Even when both TS files are open, should only show the history (on the top), and tasks, resolved for the current file" - ); - tasks_picker.update(cx, |_, cx| { - cx.emit(DismissEvent); - }); - drop(tasks_picker); - cx.executor().run_until_parked(); - - let _rs_file = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from(path!("/dir/b.rs")), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .await - .unwrap(); - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - task_names(&tasks_picker, cx), - vec!["Rust task"], - "Even when both TS files are open and one TS task spawned, opened file's language tasks should be displayed only" - ); - - cx.dispatch_action(CloseInactiveTabsAndPanes::default()); - emulate_task_schedule(tasks_picker, &project, "Rust task", cx); - let _ts_file_2 = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from(path!("/dir/a2.ts")), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .await - .unwrap(); - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - task_names(&tasks_picker, cx), - vec![ - concat!("TypeScript task from file ", path!("/dir/a1.ts")), - concat!("Another task from file ", path!("/dir/a2.ts")), - concat!("TypeScript task from file ", path!("/dir/a2.ts")), - "Task without variables", - ], - "After closing all but *.rs tabs, running a Rust task and switching back to TS tasks, \ - same TS spawn history should be restored" - ); - } - - fn emulate_task_schedule( - tasks_picker: Entity>, - project: &Entity, - scheduled_task_label: &str, - cx: &mut VisualTestContext, - ) { - let scheduled_task = tasks_picker.read_with(cx, |tasks_picker, _| { - tasks_picker - .delegate - .candidates - .iter() - .flatten() - .find(|(_, task)| task.resolved_label == scheduled_task_label) - .cloned() - .unwrap() - }); - project.update(cx, |project, cx| { - if let Some(task_inventory) = project.task_store().read(cx).task_inventory().cloned() { - task_inventory.update(cx, |inventory, _| { - let (kind, task) = scheduled_task; - inventory.task_scheduled(kind, task); - }); - } - }); - tasks_picker.update(cx, |_, cx| { - cx.emit(DismissEvent); - }); - drop(tasks_picker); - cx.executor().run_until_parked() - } - - fn open_spawn_tasks( - workspace: &Entity, - cx: &mut VisualTestContext, - ) -> Entity> { - cx.dispatch_action(Spawn::modal()); - workspace.update(cx, |workspace, cx| { - workspace - .active_modal::(cx) - .expect("no task modal after `Spawn` action was dispatched") - .read(cx) - .picker - .clone() - }) - } - - fn query( - spawn_tasks: &Entity>, - cx: &mut VisualTestContext, - ) -> String { - spawn_tasks.read_with(cx, |spawn_tasks, cx| spawn_tasks.query(cx)) - } - - fn task_names( - spawn_tasks: &Entity>, - cx: &mut VisualTestContext, - ) -> Vec { - spawn_tasks.read_with(cx, |spawn_tasks, _| { - spawn_tasks - .delegate - .matches - .iter() - .map(|hit| hit.string.clone()) - .collect::>() - }) - } -} diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs deleted file mode 100644 index 35c8a2ee22..0000000000 --- a/crates/tasks_ui/src/tasks_ui.rs +++ /dev/null @@ -1,611 +0,0 @@ -use std::{path::Path, sync::Arc}; - -use collections::HashMap; -use editor::Editor; -use gpui::{App, AppContext as _, Context, Entity, Task, Window}; -use project::{Location, TaskContexts, TaskSourceKind, Worktree}; -use task::{RevealTarget, TaskContext, TaskId, TaskTemplate, TaskVariables, VariableName}; -use workspace::Workspace; - -mod modal; - -pub use modal::{Rerun, ShowAttachModal, Spawn, TaskOverrides, TasksModal}; - -pub fn init(cx: &mut App) { - cx.observe_new( - |workspace: &mut Workspace, _: Option<&mut Window>, _: &mut Context| { - workspace - .register_action(spawn_task_or_modal) - .register_action(move |workspace, action: &modal::Rerun, window, cx| { - if let Some((task_source_kind, mut last_scheduled_task)) = workspace - .project() - .read(cx) - .task_store() - .read(cx) - .task_inventory() - .and_then(|inventory| { - inventory.read(cx).last_scheduled_task( - action - .task_id - .as_ref() - .map(|id| TaskId(id.clone())) - .as_ref(), - ) - }) - { - if action.reevaluate_context { - let mut original_task = last_scheduled_task.original_task().clone(); - if let Some(allow_concurrent_runs) = action.allow_concurrent_runs { - original_task.allow_concurrent_runs = allow_concurrent_runs; - } - if let Some(use_new_terminal) = action.use_new_terminal { - original_task.use_new_terminal = use_new_terminal; - } - let task_contexts = task_contexts(workspace, window, cx); - cx.spawn_in(window, async move |workspace, cx| { - let task_contexts = task_contexts.await; - let default_context = TaskContext::default(); - workspace - .update_in(cx, |workspace, window, cx| { - workspace.schedule_task( - task_source_kind, - &original_task, - task_contexts - .active_context() - .unwrap_or(&default_context), - false, - window, - cx, - ) - }) - .ok() - }) - .detach() - } else { - let resolved = &mut last_scheduled_task.resolved; - - if let Some(allow_concurrent_runs) = action.allow_concurrent_runs { - resolved.allow_concurrent_runs = allow_concurrent_runs; - } - if let Some(use_new_terminal) = action.use_new_terminal { - resolved.use_new_terminal = use_new_terminal; - } - - workspace.schedule_resolved_task( - task_source_kind, - last_scheduled_task, - false, - window, - cx, - ); - } - } else { - spawn_task_or_modal( - workspace, - &Spawn::ViaModal { - reveal_target: None, - }, - window, - cx, - ); - }; - }); - }, - ) - .detach(); -} - -fn spawn_task_or_modal( - workspace: &mut Workspace, - action: &Spawn, - window: &mut Window, - cx: &mut Context, -) { - if let Some(provider) = workspace.debugger_provider() { - provider.spawn_task_or_modal(workspace, action, window, cx); - return; - } - - match action { - Spawn::ByName { - task_name, - reveal_target, - } => { - let overrides = reveal_target.map(|reveal_target| TaskOverrides { - reveal_target: Some(reveal_target), - }); - let name = task_name.clone(); - spawn_tasks_filtered(move |(_, task)| task.label.eq(&name), overrides, window, cx) - .detach_and_log_err(cx) - } - Spawn::ByTag { - task_tag, - reveal_target, - } => { - let overrides = reveal_target.map(|reveal_target| TaskOverrides { - reveal_target: Some(reveal_target), - }); - let tag = task_tag.clone(); - spawn_tasks_filtered( - move |(_, task)| task.tags.contains(&tag), - overrides, - window, - cx, - ) - .detach_and_log_err(cx) - } - Spawn::ViaModal { reveal_target } => { - toggle_modal(workspace, *reveal_target, window, cx).detach() - } - } -} - -pub fn toggle_modal( - workspace: &mut Workspace, - reveal_target: Option, - window: &mut Window, - cx: &mut Context, -) -> Task<()> { - let task_store = workspace.project().read(cx).task_store().clone(); - let workspace_handle = workspace.weak_handle(); - let can_open_modal = workspace - .project() - .read_with(cx, |project, _| !project.is_via_collab()); - if can_open_modal { - let task_contexts = task_contexts(workspace, window, cx); - cx.spawn_in(window, async move |workspace, cx| { - let task_contexts = Arc::new(task_contexts.await); - workspace - .update_in(cx, |workspace, window, cx| { - workspace.toggle_modal(window, cx, |window, cx| { - TasksModal::new( - task_store.clone(), - task_contexts, - reveal_target.map(|target| TaskOverrides { - reveal_target: Some(target), - }), - true, - workspace_handle, - window, - cx, - ) - }) - }) - .ok(); - }) - } else { - Task::ready(()) - } -} - -pub fn spawn_tasks_filtered( - mut predicate: F, - overrides: Option, - window: &mut Window, - cx: &mut Context, -) -> Task> -where - F: FnMut((&TaskSourceKind, &TaskTemplate)) -> bool + 'static, -{ - cx.spawn_in(window, async move |workspace, cx| { - let task_contexts = workspace.update_in(cx, |workspace, window, cx| { - task_contexts(workspace, window, cx) - })?; - let task_contexts = task_contexts.await; - let mut tasks = workspace - .update(cx, |workspace, cx| { - let Some(task_inventory) = workspace - .project() - .read(cx) - .task_store() - .read(cx) - .task_inventory() - .cloned() - else { - return Task::ready(Vec::new()); - }; - let (file, language) = task_contexts - .location() - .map(|location| { - let buffer = location.buffer.read(cx); - ( - buffer.file().cloned(), - buffer.language_at(location.range.start), - ) - }) - .unwrap_or_default(); - task_inventory - .read(cx) - .list_tasks(file, language, task_contexts.worktree(), cx) - })? - .await; - - let did_spawn = workspace - .update_in(cx, |workspace, window, cx| { - let default_context = TaskContext::default(); - let active_context = task_contexts.active_context().unwrap_or(&default_context); - - tasks.retain_mut(|(task_source_kind, target_task)| { - if predicate((task_source_kind, target_task)) { - if let Some(overrides) = &overrides - && let Some(target_override) = overrides.reveal_target - { - target_task.reveal_target = target_override; - } - workspace.schedule_task( - task_source_kind.clone(), - target_task, - active_context, - false, - window, - cx, - ); - true - } else { - false - } - }); - - if tasks.is_empty() { None } else { Some(()) } - })? - .is_some(); - if !did_spawn { - workspace - .update_in(cx, |workspace, window, cx| { - spawn_task_or_modal( - workspace, - &Spawn::ViaModal { - reveal_target: overrides.and_then(|overrides| overrides.reveal_target), - }, - window, - cx, - ); - }) - .ok(); - } - - Ok(()) - }) -} - -pub fn task_contexts( - workspace: &Workspace, - window: &mut Window, - cx: &mut App, -) -> Task { - let active_item = workspace.active_item(cx); - let active_worktree = active_item - .as_ref() - .and_then(|item| item.project_path(cx)) - .map(|project_path| project_path.worktree_id) - .filter(|worktree_id| { - workspace - .project() - .read(cx) - .worktree_for_id(*worktree_id, cx) - .is_some_and(|worktree| is_visible_directory(&worktree, cx)) - }) - .or_else(|| { - workspace - .visible_worktrees(cx) - .next() - .map(|tree| tree.read(cx).id()) - }); - - let active_editor = active_item.and_then(|item| item.act_as::(cx)); - - let editor_context_task = active_editor.as_ref().map(|active_editor| { - active_editor.update(cx, |editor, cx| editor.task_context(window, cx)) - }); - - let location = active_editor.as_ref().and_then(|editor| { - editor.update(cx, |editor, cx| { - let selection = editor.selections.newest_anchor(); - let multi_buffer = editor.buffer().clone(); - let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx); - let (buffer_snapshot, buffer_offset) = - multi_buffer_snapshot.point_to_buffer_offset(selection.head())?; - let buffer_anchor = buffer_snapshot.anchor_before(buffer_offset); - let buffer = multi_buffer.read(cx).buffer(buffer_snapshot.remote_id())?; - Some(Location { - buffer, - range: buffer_anchor..buffer_anchor, - }) - }) - }); - - let lsp_task_sources = active_editor - .as_ref() - .map(|active_editor| active_editor.update(cx, |editor, cx| editor.lsp_task_sources(cx))) - .unwrap_or_default(); - - let latest_selection = active_editor.as_ref().map(|active_editor| { - active_editor - .read(cx) - .selections - .newest_anchor() - .head() - .text_anchor - }); - - let mut worktree_abs_paths = workspace - .worktrees(cx) - .filter(|worktree| is_visible_directory(worktree, cx)) - .map(|worktree| { - let worktree = worktree.read(cx); - (worktree.id(), worktree.abs_path()) - }) - .collect::>(); - - cx.background_spawn(async move { - let mut task_contexts = TaskContexts::default(); - - task_contexts.lsp_task_sources = lsp_task_sources; - task_contexts.latest_selection = latest_selection; - - if let Some(editor_context_task) = editor_context_task - && let Some(editor_context) = editor_context_task.await - { - task_contexts.active_item_context = Some((active_worktree, location, editor_context)); - } - - if let Some(active_worktree) = active_worktree { - if let Some(active_worktree_abs_path) = worktree_abs_paths.remove(&active_worktree) { - task_contexts.active_worktree_context = - Some((active_worktree, worktree_context(&active_worktree_abs_path))); - } - } else if worktree_abs_paths.len() == 1 { - task_contexts.active_worktree_context = worktree_abs_paths - .drain() - .next() - .map(|(id, abs_path)| (id, worktree_context(&abs_path))); - } - - task_contexts.other_worktree_contexts.extend( - worktree_abs_paths - .into_iter() - .map(|(id, abs_path)| (id, worktree_context(&abs_path))), - ); - task_contexts - }) -} - -fn is_visible_directory(worktree: &Entity, cx: &App) -> bool { - let worktree = worktree.read(cx); - worktree.is_visible() && worktree.root_entry().is_some_and(|entry| entry.is_dir()) -} - -fn worktree_context(worktree_abs_path: &Path) -> TaskContext { - let mut task_variables = TaskVariables::default(); - task_variables.insert( - VariableName::WorktreeRoot, - worktree_abs_path.to_string_lossy().into_owned(), - ); - TaskContext { - cwd: Some(worktree_abs_path.to_path_buf()), - task_variables, - project_env: HashMap::default(), - } -} - -#[cfg(test)] -mod tests { - use std::{collections::HashMap, sync::Arc}; - - use editor::{Editor, MultiBufferOffset, SelectionEffects}; - use gpui::TestAppContext; - use language::{Language, LanguageConfig}; - use project::{BasicContextProvider, FakeFs, Project, task_store::TaskStore}; - use serde_json::json; - use task::{TaskContext, TaskVariables, VariableName}; - use ui::VisualContext; - use util::{path, rel_path::rel_path}; - use workspace::{AppState, Workspace}; - - use crate::task_contexts; - - #[gpui::test] - async fn test_default_language_context(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({ - ".zed": { - "tasks.json": r#"[ - { - "label": "example task", - "command": "echo", - "args": ["4"] - }, - { - "label": "another one", - "command": "echo", - "args": ["55"] - }, - ]"#, - }, - "a.ts": "function this_is_a_test() { }", - "rust": { - "b.rs": "use std; fn this_is_a_rust_file() { }", - } - - }), - ) - .await; - let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let worktree_store = project.read_with(cx, |project, _| project.worktree_store()); - let rust_language = Arc::new( - Language::new( - LanguageConfig::default(), - Some(tree_sitter_rust::LANGUAGE.into()), - ) - .with_outline_query( - r#"(function_item - "fn" @context - name: (_) @name) @item"#, - ) - .unwrap() - .with_context_provider(Some(Arc::new(BasicContextProvider::new( - worktree_store.clone(), - )))), - ); - - let typescript_language = Arc::new( - Language::new( - LanguageConfig::default(), - Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()), - ) - .with_outline_query( - r#"(function_declaration - "async"? @context - "function" @context - name: (_) @name - parameters: (formal_parameters - "(" @context - ")" @context)) @item"#, - ) - .unwrap() - .with_context_provider(Some(Arc::new(BasicContextProvider::new( - worktree_store.clone(), - )))), - ); - - let worktree_id = project.update(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }); - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let buffer1 = workspace - .update(cx, |this, cx| { - this.project().update(cx, |this, cx| { - this.open_buffer((worktree_id, rel_path("a.ts")), cx) - }) - }) - .await - .unwrap(); - buffer1.update(cx, |this, cx| { - this.set_language(Some(typescript_language), cx) - }); - let editor1 = cx.new_window_entity(|window, cx| { - Editor::for_buffer(buffer1, Some(project.clone()), window, cx) - }); - let buffer2 = workspace - .update(cx, |this, cx| { - this.project().update(cx, |this, cx| { - this.open_buffer((worktree_id, rel_path("rust/b.rs")), cx) - }) - }) - .await - .unwrap(); - buffer2.update(cx, |this, cx| this.set_language(Some(rust_language), cx)); - let editor2 = cx - .new_window_entity(|window, cx| Editor::for_buffer(buffer2, Some(project), window, cx)); - - let first_context = workspace - .update_in(cx, |workspace, window, cx| { - workspace.add_item_to_center(Box::new(editor1.clone()), window, cx); - workspace.add_item_to_center(Box::new(editor2.clone()), window, cx); - assert_eq!( - workspace.active_item(cx).unwrap().item_id(), - editor2.entity_id() - ); - task_contexts(workspace, window, cx) - }) - .await; - - assert_eq!( - first_context - .active_context() - .expect("Should have an active context"), - &TaskContext { - cwd: Some(path!("/dir").into()), - task_variables: TaskVariables::from_iter([ - (VariableName::File, path!("/dir/rust/b.rs").into()), - (VariableName::Filename, "b.rs".into()), - (VariableName::RelativeFile, path!("rust/b.rs").into()), - (VariableName::RelativeDir, "rust".into()), - (VariableName::Dirname, path!("/dir/rust").into()), - (VariableName::Stem, "b".into()), - (VariableName::WorktreeRoot, path!("/dir").into()), - (VariableName::Row, "1".into()), - (VariableName::Column, "1".into()), - ]), - project_env: HashMap::default(), - } - ); - - // And now, let's select an identifier. - editor2.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { - selections.select_ranges([MultiBufferOffset(14)..MultiBufferOffset(18)]) - }) - }); - - assert_eq!( - workspace - .update_in(cx, |workspace, window, cx| { - task_contexts(workspace, window, cx) - }) - .await - .active_context() - .expect("Should have an active context"), - &TaskContext { - cwd: Some(path!("/dir").into()), - task_variables: TaskVariables::from_iter([ - (VariableName::File, path!("/dir/rust/b.rs").into()), - (VariableName::Filename, "b.rs".into()), - (VariableName::RelativeFile, path!("rust/b.rs").into()), - (VariableName::RelativeDir, "rust".into()), - (VariableName::Dirname, path!("/dir/rust").into()), - (VariableName::Stem, "b".into()), - (VariableName::WorktreeRoot, path!("/dir").into()), - (VariableName::Row, "1".into()), - (VariableName::Column, "15".into()), - (VariableName::SelectedText, "is_i".into()), - (VariableName::Symbol, "this_is_a_rust_file".into()), - ]), - project_env: HashMap::default(), - } - ); - - assert_eq!( - workspace - .update_in(cx, |workspace, window, cx| { - // Now, let's switch the active item to .ts file. - workspace.activate_item(&editor1, true, true, window, cx); - task_contexts(workspace, window, cx) - }) - .await - .active_context() - .expect("Should have an active context"), - &TaskContext { - cwd: Some(path!("/dir").into()), - task_variables: TaskVariables::from_iter([ - (VariableName::File, path!("/dir/a.ts").into()), - (VariableName::Filename, "a.ts".into()), - (VariableName::RelativeFile, "a.ts".into()), - (VariableName::RelativeDir, ".".into()), - (VariableName::Dirname, path!("/dir").into()), - (VariableName::Stem, "a".into()), - (VariableName::WorktreeRoot, path!("/dir").into()), - (VariableName::Row, "1".into()), - (VariableName::Column, "1".into()), - (VariableName::Symbol, "this_is_a_test".into()), - ]), - project_env: HashMap::default(), - } - ); - } - - pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc { - cx.update(|cx| { - let state = AppState::test(cx); - crate::init(cx); - editor::init(cx); - TaskStore::init(None); - state - }) - } -} diff --git a/crates/telemetry/Cargo.toml b/crates/telemetry/Cargo.toml deleted file mode 100644 index ed166ea4c7..0000000000 --- a/crates/telemetry/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "telemetry" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/telemetry.rs" - -[dependencies] -serde.workspace = true -serde_json.workspace = true -telemetry_events.workspace = true -futures.workspace = true diff --git a/crates/telemetry/LICENSE-GPL b/crates/telemetry/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/telemetry/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/telemetry/src/telemetry.rs b/crates/telemetry/src/telemetry.rs deleted file mode 100644 index e6c8516967..0000000000 --- a/crates/telemetry/src/telemetry.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! See [Telemetry in Zed](https://zed.dev/docs/telemetry) for additional information. -use futures::channel::mpsc; -pub use serde_json; -use std::sync::OnceLock; -pub use telemetry_events::FlexibleEvent as Event; - -/// Macro to create telemetry events and send them to the telemetry queue. -/// -/// By convention, the name should be "Noun Verbed", e.g. "Keymap Changed" -/// or "Project Diagnostics Opened". -/// -/// The properties can be any value that implements serde::Serialize. -/// -/// ``` -/// # let url = "https://example.com"; -/// telemetry::event!("Keymap Changed", version = "1.0.0"); -/// telemetry::event!("Documentation Viewed", url, source = "Extension Upsell"); -/// ``` -/// -/// If you want to debug logging in development, export `RUST_LOG=telemetry=trace` -#[macro_export] -macro_rules! event { - ($name:expr) => {{ - let event = $crate::Event { - event_type: $name.to_string(), - event_properties: std::collections::HashMap::new(), - }; - $crate::send_event(event); - }}; - ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {{ - let event = $crate::Event { - event_type: $name.to_string(), - event_properties: std::collections::HashMap::from([ - $( - (stringify!($key).to_string(), - $crate::serde_json::value::to_value(&$crate::serialize_property!($key $(= $value)?)) - .unwrap_or_else(|_| $crate::serde_json::to_value(&()).unwrap()) - ), - )+ - ]), - }; - $crate::send_event(event); - }}; -} - -#[macro_export] -macro_rules! serialize_property { - ($key:ident) => { - $key - }; - ($key:ident = $value:expr) => { - $value - }; -} - -pub fn send_event(event: Event) { - if let Some(queue) = TELEMETRY_QUEUE.get() { - queue.unbounded_send(event).ok(); - } -} - -pub fn init(tx: mpsc::UnboundedSender) { - TELEMETRY_QUEUE.set(tx).ok(); -} - -static TELEMETRY_QUEUE: OnceLock> = OnceLock::new(); diff --git a/crates/telemetry_events/Cargo.toml b/crates/telemetry_events/Cargo.toml deleted file mode 100644 index 6a5149c545..0000000000 --- a/crates/telemetry_events/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "telemetry_events" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/telemetry_events.rs" - -[dependencies] -semver.workspace = true -serde.workspace = true -serde_json.workspace = true diff --git a/crates/telemetry_events/LICENSE-GPL b/crates/telemetry_events/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/telemetry_events/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/telemetry_events/src/telemetry_events.rs b/crates/telemetry_events/src/telemetry_events.rs deleted file mode 100644 index 83ec2c0644..0000000000 --- a/crates/telemetry_events/src/telemetry_events.rs +++ /dev/null @@ -1,126 +0,0 @@ -//! See [Telemetry in Zed](https://zed.dev/docs/telemetry) for additional information. - -use semver::Version; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fmt::Display, time::Duration}; - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct EventRequestBody { - /// Identifier unique to each system Zed is installed on - pub system_id: Option, - /// Identifier unique to each Zed installation (differs for stable, preview, dev) - pub installation_id: Option, - /// Identifier unique to each logged in Zed user (randomly generated on first sign in) - /// Identifier unique to each Zed session (differs for each time you open Zed) - pub session_id: Option, - pub metrics_id: Option, - /// True for Zed staff, otherwise false - #[serde(skip_serializing_if = "Option::is_none")] - pub is_staff: Option, - /// Zed version number - pub app_version: String, - pub os_name: String, - pub os_version: Option, - pub architecture: String, - /// Zed release channel (stable, preview, dev) - pub release_channel: Option, - pub events: Vec, -} - -impl EventRequestBody { - pub fn semver(&self) -> Option { - self.app_version.parse().ok() - } -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct EventWrapper { - pub signed_in: bool, - /// Duration between this event's timestamp and the timestamp of the first event in the current batch - pub milliseconds_since_first_event: i64, - /// The event itself - #[serde(flatten)] - pub event: Event, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum AssistantKind { - Panel, - Inline, - InlineTerminal, -} -impl Display for AssistantKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Panel => "panel", - Self::Inline => "inline", - Self::InlineTerminal => "inline_terminal", - } - ) - } -} - -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum AssistantPhase { - #[default] - Response, - Invoked, - Accepted, - Rejected, -} - -impl Display for AssistantPhase { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Response => "response", - Self::Invoked => "invoked", - Self::Accepted => "accepted", - Self::Rejected => "rejected", - } - ) - } -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(tag = "type")] -pub enum Event { - Flexible(FlexibleEvent), -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct FlexibleEvent { - pub event_type: String, - pub event_properties: HashMap, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub enum EditPredictionRating { - Positive, - Negative, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct AssistantEventData { - /// Unique random identifier for each assistant tab (None for inline assist) - pub conversation_id: Option, - /// Server-generated message ID (only supported for some providers) - pub message_id: Option, - /// The kind of assistant (Panel, Inline) - pub kind: AssistantKind, - #[serde(default)] - pub phase: AssistantPhase, - /// Name of the AI model used (gpt-4o, claude-3-5-sonnet, etc) - pub model: String, - pub model_provider: String, - pub response_latency: Option, - pub error_message: Option, - pub language_name: Option, -} diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml deleted file mode 100644 index 1266c5a6e5..0000000000 --- a/crates/terminal/Cargo.toml +++ /dev/null @@ -1,53 +0,0 @@ -[package] -name = "terminal" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[features] -test-support = [ - "collections/test-support", - "gpui/test-support", - "settings/test-support", -] - -[lints] -workspace = true - -[lib] -path = "src/terminal.rs" -doctest = false - -[dependencies] -alacritty_terminal.workspace = true -anyhow.workspace = true -collections.workspace = true -futures.workspace = true -gpui.workspace = true -itertools.workspace = true -libc.workspace = true -log.workspace = true -regex.workspace = true -release_channel.workspace = true -schemars.workspace = true -serde.workspace = true -settings.workspace = true -sysinfo.workspace = true -smol.workspace = true -task.workspace = true -theme.workspace = true -thiserror.workspace = true -util.workspace = true -urlencoding.workspace = true - -[target.'cfg(windows)'.dependencies] -windows.workspace = true - -[dev-dependencies] -gpui = { workspace = true, features = ["test-support"] } -rand.workspace = true -serde_json.workspace = true -settings = { workspace = true, features = ["test-support"] } -url.workspace = true -util_macros.workspace = true diff --git a/crates/terminal/LICENSE-GPL b/crates/terminal/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/terminal/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/terminal/src/mappings/colors.rs b/crates/terminal/src/mappings/colors.rs deleted file mode 100644 index 876e366606..0000000000 --- a/crates/terminal/src/mappings/colors.rs +++ /dev/null @@ -1,11 +0,0 @@ -use alacritty_terminal::vte::ansi::Rgb as AlacRgb; -use gpui::Rgba; - -//Convenience method to convert from a GPUI color to an alacritty Rgb -pub fn to_alac_rgb(color: impl Into) -> AlacRgb { - let color = color.into(); - let r = ((color.r * color.a) * 255.) as u8; - let g = ((color.g * color.a) * 255.) as u8; - let b = ((color.b * color.a) * 255.) as u8; - AlacRgb { r, g, b } -} diff --git a/crates/terminal/src/mappings/keys.rs b/crates/terminal/src/mappings/keys.rs deleted file mode 100644 index 8073961fc5..0000000000 --- a/crates/terminal/src/mappings/keys.rs +++ /dev/null @@ -1,449 +0,0 @@ -use std::borrow::Cow; - -/// The mappings defined in this file where created from reading the alacritty source -use alacritty_terminal::term::TermMode; -use gpui::Keystroke; - -#[derive(Debug, PartialEq, Eq)] -enum AlacModifiers { - None, - Alt, - Ctrl, - Shift, - CtrlShift, - Other, -} - -impl AlacModifiers { - fn new(ks: &Keystroke) -> Self { - match ( - ks.modifiers.alt, - ks.modifiers.control, - ks.modifiers.shift, - ks.modifiers.platform, - ) { - (false, false, false, false) => AlacModifiers::None, - (true, false, false, false) => AlacModifiers::Alt, - (false, true, false, false) => AlacModifiers::Ctrl, - (false, false, true, false) => AlacModifiers::Shift, - (false, true, true, false) => AlacModifiers::CtrlShift, - _ => AlacModifiers::Other, - } - } - - fn any(&self) -> bool { - match &self { - AlacModifiers::None => false, - AlacModifiers::Alt => true, - AlacModifiers::Ctrl => true, - AlacModifiers::Shift => true, - AlacModifiers::CtrlShift => true, - AlacModifiers::Other => true, - } - } -} - -pub fn to_esc_str( - keystroke: &Keystroke, - mode: &TermMode, - option_as_meta: bool, -) -> Option> { - let modifiers = AlacModifiers::new(keystroke); - - // Manual Bindings including modifiers - let manual_esc_str: Option<&'static str> = match (keystroke.key.as_ref(), &modifiers) { - //Basic special keys - ("tab", AlacModifiers::None) => Some("\x09"), - ("escape", AlacModifiers::None) => Some("\x1b"), - ("enter", AlacModifiers::None) => Some("\x0d"), - ("enter", AlacModifiers::Shift) => Some("\x0a"), - ("enter", AlacModifiers::Alt) => Some("\x1b\x0d"), - ("backspace", AlacModifiers::None) => Some("\x7f"), - //Interesting escape codes - ("tab", AlacModifiers::Shift) => Some("\x1b[Z"), - ("backspace", AlacModifiers::Ctrl) => Some("\x08"), - ("backspace", AlacModifiers::Alt) => Some("\x1b\x7f"), - ("backspace", AlacModifiers::Shift) => Some("\x7f"), - ("space", AlacModifiers::Ctrl) => Some("\x00"), - ("home", AlacModifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => Some("\x1b[1;2H"), - ("end", AlacModifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => Some("\x1b[1;2F"), - ("pageup", AlacModifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => { - Some("\x1b[5;2~") - } - ("pagedown", AlacModifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => { - Some("\x1b[6;2~") - } - ("home", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOH"), - ("home", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[H"), - ("end", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOF"), - ("end", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[F"), - ("up", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOA"), - ("up", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[A"), - ("down", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOB"), - ("down", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[B"), - ("right", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOC"), - ("right", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[C"), - ("left", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOD"), - ("left", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[D"), - ("back", AlacModifiers::None) => Some("\x7f"), - ("insert", AlacModifiers::None) => Some("\x1b[2~"), - ("delete", AlacModifiers::None) => Some("\x1b[3~"), - ("pageup", AlacModifiers::None) => Some("\x1b[5~"), - ("pagedown", AlacModifiers::None) => Some("\x1b[6~"), - ("f1", AlacModifiers::None) => Some("\x1bOP"), - ("f2", AlacModifiers::None) => Some("\x1bOQ"), - ("f3", AlacModifiers::None) => Some("\x1bOR"), - ("f4", AlacModifiers::None) => Some("\x1bOS"), - ("f5", AlacModifiers::None) => Some("\x1b[15~"), - ("f6", AlacModifiers::None) => Some("\x1b[17~"), - ("f7", AlacModifiers::None) => Some("\x1b[18~"), - ("f8", AlacModifiers::None) => Some("\x1b[19~"), - ("f9", AlacModifiers::None) => Some("\x1b[20~"), - ("f10", AlacModifiers::None) => Some("\x1b[21~"), - ("f11", AlacModifiers::None) => Some("\x1b[23~"), - ("f12", AlacModifiers::None) => Some("\x1b[24~"), - ("f13", AlacModifiers::None) => Some("\x1b[25~"), - ("f14", AlacModifiers::None) => Some("\x1b[26~"), - ("f15", AlacModifiers::None) => Some("\x1b[28~"), - ("f16", AlacModifiers::None) => Some("\x1b[29~"), - ("f17", AlacModifiers::None) => Some("\x1b[31~"), - ("f18", AlacModifiers::None) => Some("\x1b[32~"), - ("f19", AlacModifiers::None) => Some("\x1b[33~"), - ("f20", AlacModifiers::None) => Some("\x1b[34~"), - // NumpadEnter, Action::Esc("\n".into()); - //Mappings for caret notation keys - ("a", AlacModifiers::Ctrl) => Some("\x01"), //1 - ("A", AlacModifiers::CtrlShift) => Some("\x01"), //1 - ("b", AlacModifiers::Ctrl) => Some("\x02"), //2 - ("B", AlacModifiers::CtrlShift) => Some("\x02"), //2 - ("c", AlacModifiers::Ctrl) => Some("\x03"), //3 - ("C", AlacModifiers::CtrlShift) => Some("\x03"), //3 - ("d", AlacModifiers::Ctrl) => Some("\x04"), //4 - ("D", AlacModifiers::CtrlShift) => Some("\x04"), //4 - ("e", AlacModifiers::Ctrl) => Some("\x05"), //5 - ("E", AlacModifiers::CtrlShift) => Some("\x05"), //5 - ("f", AlacModifiers::Ctrl) => Some("\x06"), //6 - ("F", AlacModifiers::CtrlShift) => Some("\x06"), //6 - ("g", AlacModifiers::Ctrl) => Some("\x07"), //7 - ("G", AlacModifiers::CtrlShift) => Some("\x07"), //7 - ("h", AlacModifiers::Ctrl) => Some("\x08"), //8 - ("H", AlacModifiers::CtrlShift) => Some("\x08"), //8 - ("i", AlacModifiers::Ctrl) => Some("\x09"), //9 - ("I", AlacModifiers::CtrlShift) => Some("\x09"), //9 - ("j", AlacModifiers::Ctrl) => Some("\x0a"), //10 - ("J", AlacModifiers::CtrlShift) => Some("\x0a"), //10 - ("k", AlacModifiers::Ctrl) => Some("\x0b"), //11 - ("K", AlacModifiers::CtrlShift) => Some("\x0b"), //11 - ("l", AlacModifiers::Ctrl) => Some("\x0c"), //12 - ("L", AlacModifiers::CtrlShift) => Some("\x0c"), //12 - ("m", AlacModifiers::Ctrl) => Some("\x0d"), //13 - ("M", AlacModifiers::CtrlShift) => Some("\x0d"), //13 - ("n", AlacModifiers::Ctrl) => Some("\x0e"), //14 - ("N", AlacModifiers::CtrlShift) => Some("\x0e"), //14 - ("o", AlacModifiers::Ctrl) => Some("\x0f"), //15 - ("O", AlacModifiers::CtrlShift) => Some("\x0f"), //15 - ("p", AlacModifiers::Ctrl) => Some("\x10"), //16 - ("P", AlacModifiers::CtrlShift) => Some("\x10"), //16 - ("q", AlacModifiers::Ctrl) => Some("\x11"), //17 - ("Q", AlacModifiers::CtrlShift) => Some("\x11"), //17 - ("r", AlacModifiers::Ctrl) => Some("\x12"), //18 - ("R", AlacModifiers::CtrlShift) => Some("\x12"), //18 - ("s", AlacModifiers::Ctrl) => Some("\x13"), //19 - ("S", AlacModifiers::CtrlShift) => Some("\x13"), //19 - ("t", AlacModifiers::Ctrl) => Some("\x14"), //20 - ("T", AlacModifiers::CtrlShift) => Some("\x14"), //20 - ("u", AlacModifiers::Ctrl) => Some("\x15"), //21 - ("U", AlacModifiers::CtrlShift) => Some("\x15"), //21 - ("v", AlacModifiers::Ctrl) => Some("\x16"), //22 - ("V", AlacModifiers::CtrlShift) => Some("\x16"), //22 - ("w", AlacModifiers::Ctrl) => Some("\x17"), //23 - ("W", AlacModifiers::CtrlShift) => Some("\x17"), //23 - ("x", AlacModifiers::Ctrl) => Some("\x18"), //24 - ("X", AlacModifiers::CtrlShift) => Some("\x18"), //24 - ("y", AlacModifiers::Ctrl) => Some("\x19"), //25 - ("Y", AlacModifiers::CtrlShift) => Some("\x19"), //25 - ("z", AlacModifiers::Ctrl) => Some("\x1a"), //26 - ("Z", AlacModifiers::CtrlShift) => Some("\x1a"), //26 - ("@", AlacModifiers::Ctrl) => Some("\x00"), //0 - ("[", AlacModifiers::Ctrl) => Some("\x1b"), //27 - ("\\", AlacModifiers::Ctrl) => Some("\x1c"), //28 - ("]", AlacModifiers::Ctrl) => Some("\x1d"), //29 - ("^", AlacModifiers::Ctrl) => Some("\x1e"), //30 - ("_", AlacModifiers::Ctrl) => Some("\x1f"), //31 - ("?", AlacModifiers::Ctrl) => Some("\x7f"), //127 - _ => None, - }; - if let Some(esc_str) = manual_esc_str { - return Some(Cow::Borrowed(esc_str)); - } - - // Automated bindings applying modifiers - if modifiers.any() { - let modifier_code = modifier_code(keystroke); - let modified_esc_str = match keystroke.key.as_ref() { - "up" => Some(format!("\x1b[1;{}A", modifier_code)), - "down" => Some(format!("\x1b[1;{}B", modifier_code)), - "right" => Some(format!("\x1b[1;{}C", modifier_code)), - "left" => Some(format!("\x1b[1;{}D", modifier_code)), - "f1" => Some(format!("\x1b[1;{}P", modifier_code)), - "f2" => Some(format!("\x1b[1;{}Q", modifier_code)), - "f3" => Some(format!("\x1b[1;{}R", modifier_code)), - "f4" => Some(format!("\x1b[1;{}S", modifier_code)), - "F5" => Some(format!("\x1b[15;{}~", modifier_code)), - "f6" => Some(format!("\x1b[17;{}~", modifier_code)), - "f7" => Some(format!("\x1b[18;{}~", modifier_code)), - "f8" => Some(format!("\x1b[19;{}~", modifier_code)), - "f9" => Some(format!("\x1b[20;{}~", modifier_code)), - "f10" => Some(format!("\x1b[21;{}~", modifier_code)), - "f11" => Some(format!("\x1b[23;{}~", modifier_code)), - "f12" => Some(format!("\x1b[24;{}~", modifier_code)), - "f13" => Some(format!("\x1b[25;{}~", modifier_code)), - "f14" => Some(format!("\x1b[26;{}~", modifier_code)), - "f15" => Some(format!("\x1b[28;{}~", modifier_code)), - "f16" => Some(format!("\x1b[29;{}~", modifier_code)), - "f17" => Some(format!("\x1b[31;{}~", modifier_code)), - "f18" => Some(format!("\x1b[32;{}~", modifier_code)), - "f19" => Some(format!("\x1b[33;{}~", modifier_code)), - "f20" => Some(format!("\x1b[34;{}~", modifier_code)), - _ if modifier_code == 2 => None, - "insert" => Some(format!("\x1b[2;{}~", modifier_code)), - "pageup" => Some(format!("\x1b[5;{}~", modifier_code)), - "pagedown" => Some(format!("\x1b[6;{}~", modifier_code)), - "end" => Some(format!("\x1b[1;{}F", modifier_code)), - "home" => Some(format!("\x1b[1;{}H", modifier_code)), - _ => None, - }; - if let Some(esc_str) = modified_esc_str { - return Some(Cow::Owned(esc_str)); - } - } - - if !cfg!(target_os = "macos") || option_as_meta { - let is_alt_lowercase_ascii = modifiers == AlacModifiers::Alt && keystroke.key.is_ascii(); - let is_alt_uppercase_ascii = - keystroke.modifiers.alt && keystroke.modifiers.shift && keystroke.key.is_ascii(); - if is_alt_lowercase_ascii || is_alt_uppercase_ascii { - let key = if is_alt_uppercase_ascii { - &keystroke.key.to_ascii_uppercase() - } else { - &keystroke.key - }; - return Some(Cow::Owned(format!("\x1b{}", key))); - } - } - - None -} - -/// Code Modifiers -/// ---------+--------------------------- -/// 2 | Shift -/// 3 | Alt -/// 4 | Shift + Alt -/// 5 | Control -/// 6 | Shift + Control -/// 7 | Alt + Control -/// 8 | Shift + Alt + Control -/// ---------+--------------------------- -/// from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys -fn modifier_code(keystroke: &Keystroke) -> u32 { - let mut modifier_code = 0; - if keystroke.modifiers.shift { - modifier_code |= 1; - } - if keystroke.modifiers.alt { - modifier_code |= 1 << 1; - } - if keystroke.modifiers.control { - modifier_code |= 1 << 2; - } - modifier_code + 1 -} - -#[cfg(test)] -mod test { - use gpui::Modifiers; - - use super::*; - - #[test] - fn test_scroll_keys() { - //These keys should be handled by the scrolling element directly - //Need to signify this by returning 'None' - let shift_pageup = Keystroke::parse("shift-pageup").unwrap(); - let shift_pagedown = Keystroke::parse("shift-pagedown").unwrap(); - let shift_home = Keystroke::parse("shift-home").unwrap(); - let shift_end = Keystroke::parse("shift-end").unwrap(); - - let none = TermMode::NONE; - assert_eq!(to_esc_str(&shift_pageup, &none, false), None); - assert_eq!(to_esc_str(&shift_pagedown, &none, false), None); - assert_eq!(to_esc_str(&shift_home, &none, false), None); - assert_eq!(to_esc_str(&shift_end, &none, false), None); - - let alt_screen = TermMode::ALT_SCREEN; - assert_eq!( - to_esc_str(&shift_pageup, &alt_screen, false), - Some("\x1b[5;2~".into()) - ); - assert_eq!( - to_esc_str(&shift_pagedown, &alt_screen, false), - Some("\x1b[6;2~".into()) - ); - assert_eq!( - to_esc_str(&shift_home, &alt_screen, false), - Some("\x1b[1;2H".into()) - ); - assert_eq!( - to_esc_str(&shift_end, &alt_screen, false), - Some("\x1b[1;2F".into()) - ); - - let pageup = Keystroke::parse("pageup").unwrap(); - let pagedown = Keystroke::parse("pagedown").unwrap(); - let any = TermMode::ANY; - - assert_eq!(to_esc_str(&pageup, &any, false), Some("\x1b[5~".into())); - assert_eq!(to_esc_str(&pagedown, &any, false), Some("\x1b[6~".into())); - } - - #[test] - fn test_plain_inputs() { - let ks = Keystroke { - modifiers: Modifiers { - control: false, - alt: false, - shift: false, - platform: false, - function: false, - }, - key: "🖖🏻".to_string(), //2 char string - key_char: None, - }; - assert_eq!(to_esc_str(&ks, &TermMode::NONE, false), None); - } - - #[test] - fn test_application_mode() { - let app_cursor = TermMode::APP_CURSOR; - let none = TermMode::NONE; - - let up = Keystroke::parse("up").unwrap(); - let down = Keystroke::parse("down").unwrap(); - let left = Keystroke::parse("left").unwrap(); - let right = Keystroke::parse("right").unwrap(); - - assert_eq!(to_esc_str(&up, &none, false), Some("\x1b[A".into())); - assert_eq!(to_esc_str(&down, &none, false), Some("\x1b[B".into())); - assert_eq!(to_esc_str(&right, &none, false), Some("\x1b[C".into())); - assert_eq!(to_esc_str(&left, &none, false), Some("\x1b[D".into())); - - assert_eq!(to_esc_str(&up, &app_cursor, false), Some("\x1bOA".into())); - assert_eq!(to_esc_str(&down, &app_cursor, false), Some("\x1bOB".into())); - assert_eq!( - to_esc_str(&right, &app_cursor, false), - Some("\x1bOC".into()) - ); - assert_eq!(to_esc_str(&left, &app_cursor, false), Some("\x1bOD".into())); - } - - #[test] - fn test_ctrl_codes() { - let letters_lower = 'a'..='z'; - let letters_upper = 'A'..='Z'; - let mode = TermMode::ANY; - - for (lower, upper) in letters_lower.zip(letters_upper) { - assert_eq!( - to_esc_str( - &Keystroke::parse(&format!("ctrl-shift-{}", lower)).unwrap(), - &mode, - false - ), - to_esc_str( - &Keystroke::parse(&format!("ctrl-{}", upper)).unwrap(), - &mode, - false - ), - "On letter: {}/{}", - lower, - upper - ) - } - } - - #[test] - fn alt_is_meta() { - let ascii_printable = ' '..='~'; - for character in ascii_printable { - assert_eq!( - to_esc_str( - &Keystroke::parse(&format!("alt-{}", character)).unwrap(), - &TermMode::NONE, - true - ) - .unwrap(), - format!("\x1b{}", character) - ); - } - - let gpui_keys = [ - "up", "down", "right", "left", "f1", "f2", "f3", "f4", "F5", "f6", "f7", "f8", "f9", - "f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19", "f20", "insert", - "pageup", "pagedown", "end", "home", - ]; - - for key in gpui_keys { - assert_ne!( - to_esc_str( - &Keystroke::parse(&format!("alt-{}", key)).unwrap(), - &TermMode::NONE, - true - ) - .unwrap(), - format!("\x1b{}", key) - ); - } - } - - #[test] - fn test_shift_enter_newline() { - let shift_enter = Keystroke::parse("shift-enter").unwrap(); - let regular_enter = Keystroke::parse("enter").unwrap(); - let mode = TermMode::NONE; - - // Shift-enter should send line feed (newline) - assert_eq!(to_esc_str(&shift_enter, &mode, false), Some("\x0a".into())); - - // Regular enter should still send carriage return - assert_eq!( - to_esc_str(®ular_enter, &mode, false), - Some("\x0d".into()) - ); - } - - #[test] - fn test_modifier_code_calc() { - // Code Modifiers - // ---------+--------------------------- - // 2 | Shift - // 3 | Alt - // 4 | Shift + Alt - // 5 | Control - // 6 | Shift + Control - // 7 | Alt + Control - // 8 | Shift + Alt + Control - // ---------+--------------------------- - // from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys - assert_eq!(2, modifier_code(&Keystroke::parse("shift-a").unwrap())); - assert_eq!(3, modifier_code(&Keystroke::parse("alt-a").unwrap())); - assert_eq!(4, modifier_code(&Keystroke::parse("shift-alt-a").unwrap())); - assert_eq!(5, modifier_code(&Keystroke::parse("ctrl-a").unwrap())); - assert_eq!(6, modifier_code(&Keystroke::parse("shift-ctrl-a").unwrap())); - assert_eq!(7, modifier_code(&Keystroke::parse("alt-ctrl-a").unwrap())); - assert_eq!( - 8, - modifier_code(&Keystroke::parse("shift-ctrl-alt-a").unwrap()) - ); - } -} diff --git a/crates/terminal/src/mappings/mod.rs b/crates/terminal/src/mappings/mod.rs deleted file mode 100644 index d58dd27f96..0000000000 --- a/crates/terminal/src/mappings/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod colors; -pub mod keys; -pub mod mouse; diff --git a/crates/terminal/src/mappings/mouse.rs b/crates/terminal/src/mappings/mouse.rs deleted file mode 100644 index 8c3eed8b54..0000000000 --- a/crates/terminal/src/mappings/mouse.rs +++ /dev/null @@ -1,277 +0,0 @@ -use std::cmp::{self, max, min}; -use std::iter::repeat; - -use alacritty_terminal::grid::Dimensions; -/// Most of the code, and specifically the constants, in this are copied from Alacritty, -/// with modifications for our circumstances -use alacritty_terminal::index::{Column as GridCol, Line as GridLine, Point as AlacPoint, Side}; -use alacritty_terminal::term::TermMode; -use gpui::{Modifiers, MouseButton, Pixels, Point, ScrollWheelEvent, px}; - -use crate::TerminalBounds; - -enum MouseFormat { - Sgr, - Normal(bool), -} - -impl MouseFormat { - fn from_mode(mode: TermMode) -> Self { - if mode.contains(TermMode::SGR_MOUSE) { - MouseFormat::Sgr - } else if mode.contains(TermMode::UTF8_MOUSE) { - MouseFormat::Normal(true) - } else { - MouseFormat::Normal(false) - } - } -} - -#[derive(Debug)] -enum AlacMouseButton { - LeftButton = 0, - MiddleButton = 1, - RightButton = 2, - LeftMove = 32, - MiddleMove = 33, - RightMove = 34, - NoneMove = 35, - ScrollUp = 64, - ScrollDown = 65, - Other = 99, -} - -impl AlacMouseButton { - fn from_move_button(e: Option) -> Self { - match e { - Some(gpui::MouseButton::Left) => AlacMouseButton::LeftMove, - Some(gpui::MouseButton::Middle) => AlacMouseButton::MiddleMove, - Some(gpui::MouseButton::Right) => AlacMouseButton::RightMove, - Some(gpui::MouseButton::Navigate(_)) => AlacMouseButton::Other, - None => AlacMouseButton::NoneMove, - } - } - - fn from_button(e: MouseButton) -> Self { - match e { - gpui::MouseButton::Left => AlacMouseButton::LeftButton, - gpui::MouseButton::Right => AlacMouseButton::MiddleButton, - gpui::MouseButton::Middle => AlacMouseButton::RightButton, - gpui::MouseButton::Navigate(_) => AlacMouseButton::Other, - } - } - - fn from_scroll(e: &ScrollWheelEvent) -> Self { - let is_positive = match e.delta { - gpui::ScrollDelta::Pixels(pixels) => pixels.y > px(0.), - gpui::ScrollDelta::Lines(lines) => lines.y > 0., - }; - - if is_positive { - AlacMouseButton::ScrollUp - } else { - AlacMouseButton::ScrollDown - } - } - - fn is_other(&self) -> bool { - matches!(self, AlacMouseButton::Other) - } -} - -pub fn scroll_report( - point: AlacPoint, - scroll_lines: i32, - e: &ScrollWheelEvent, - mode: TermMode, -) -> Option>> { - if mode.intersects(TermMode::MOUSE_MODE) { - mouse_report( - point, - AlacMouseButton::from_scroll(e), - true, - e.modifiers, - MouseFormat::from_mode(mode), - ) - .map(|report| repeat(report).take(max(scroll_lines, 1) as usize)) - } else { - None - } -} - -pub fn alt_scroll(scroll_lines: i32) -> Vec { - let cmd = if scroll_lines > 0 { b'A' } else { b'B' }; - - let mut content = Vec::with_capacity(scroll_lines.unsigned_abs() as usize * 3); - for _ in 0..scroll_lines.abs() { - content.push(0x1b); - content.push(b'O'); - content.push(cmd); - } - content -} - -pub fn mouse_button_report( - point: AlacPoint, - button: gpui::MouseButton, - modifiers: Modifiers, - pressed: bool, - mode: TermMode, -) -> Option> { - let button = AlacMouseButton::from_button(button); - if !button.is_other() && mode.intersects(TermMode::MOUSE_MODE) { - mouse_report( - point, - button, - pressed, - modifiers, - MouseFormat::from_mode(mode), - ) - } else { - None - } -} - -pub fn mouse_moved_report( - point: AlacPoint, - button: Option, - modifiers: Modifiers, - mode: TermMode, -) -> Option> { - let button = AlacMouseButton::from_move_button(button); - - if !button.is_other() && mode.intersects(TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG) { - //Only drags are reported in drag mode, so block NoneMove. - if mode.contains(TermMode::MOUSE_DRAG) && matches!(button, AlacMouseButton::NoneMove) { - None - } else { - mouse_report(point, button, true, modifiers, MouseFormat::from_mode(mode)) - } - } else { - None - } -} - -pub fn grid_point( - pos: Point, - cur_size: TerminalBounds, - display_offset: usize, -) -> AlacPoint { - grid_point_and_side(pos, cur_size, display_offset).0 -} - -pub fn grid_point_and_side( - pos: Point, - cur_size: TerminalBounds, - display_offset: usize, -) -> (AlacPoint, Side) { - let mut col = GridCol((pos.x / cur_size.cell_width) as usize); - let cell_x = cmp::max(px(0.), pos.x) % cur_size.cell_width; - let half_cell_width = cur_size.cell_width / 2.0; - let mut side = if cell_x > half_cell_width { - Side::Right - } else { - Side::Left - }; - - if col > cur_size.last_column() { - col = cur_size.last_column(); - side = Side::Right; - } - let col = min(col, cur_size.last_column()); - let mut line = (pos.y / cur_size.line_height) as i32; - if line > cur_size.bottommost_line() { - line = cur_size.bottommost_line().0; - side = Side::Right; - } else if line < 0 { - side = Side::Left; - } - - ( - AlacPoint::new(GridLine(line - display_offset as i32), col), - side, - ) -} - -///Generate the bytes to send to the terminal, from the cell location, a mouse event, and the terminal mode -fn mouse_report( - point: AlacPoint, - button: AlacMouseButton, - pressed: bool, - modifiers: Modifiers, - format: MouseFormat, -) -> Option> { - if point.line < 0 { - return None; - } - - let mut mods = 0; - if modifiers.shift { - mods += 4; - } - if modifiers.alt { - mods += 8; - } - if modifiers.control { - mods += 16; - } - - match format { - MouseFormat::Sgr => { - Some(sgr_mouse_report(point, button as u8 + mods, pressed).into_bytes()) - } - MouseFormat::Normal(utf8) => { - if pressed { - normal_mouse_report(point, button as u8 + mods, utf8) - } else { - normal_mouse_report(point, 3 + mods, utf8) - } - } - } -} - -fn normal_mouse_report(point: AlacPoint, button: u8, utf8: bool) -> Option> { - let AlacPoint { line, column } = point; - let max_point = if utf8 { 2015 } else { 223 }; - - if line >= max_point || column >= max_point { - return None; - } - - let mut msg = vec![b'\x1b', b'[', b'M', 32 + button]; - - let mouse_pos_encode = |pos: usize| -> Vec { - let pos = 32 + 1 + pos; - let first = 0xC0 + pos / 64; - let second = 0x80 + (pos & 63); - vec![first as u8, second as u8] - }; - - if utf8 && column >= 95 { - msg.append(&mut mouse_pos_encode(column.0)); - } else { - msg.push(32 + 1 + column.0 as u8); - } - - if utf8 && line >= 95 { - msg.append(&mut mouse_pos_encode(line.0 as usize)); - } else { - msg.push(32 + 1 + line.0 as u8); - } - - Some(msg) -} - -fn sgr_mouse_report(point: AlacPoint, button: u8, pressed: bool) -> String { - let c = if pressed { 'M' } else { 'm' }; - - let msg = format!( - "\x1b[<{};{};{}{}", - button, - point.column + 1, - point.line + 1, - c - ); - - msg -} diff --git a/crates/terminal/src/pty_info.rs b/crates/terminal/src/pty_info.rs deleted file mode 100644 index c92de2f23b..0000000000 --- a/crates/terminal/src/pty_info.rs +++ /dev/null @@ -1,170 +0,0 @@ -use alacritty_terminal::tty::Pty; -#[cfg(target_os = "windows")] -use std::num::NonZeroU32; -#[cfg(unix)] -use std::os::fd::AsRawFd; -use std::path::PathBuf; - -#[cfg(target_os = "windows")] -use windows::Win32::{Foundation::HANDLE, System::Threading::GetProcessId}; - -use sysinfo::{Pid, Process, ProcessRefreshKind, RefreshKind, System, UpdateKind}; - -pub struct ProcessIdGetter { - handle: i32, - fallback_pid: u32, -} - -impl ProcessIdGetter { - pub fn fallback_pid(&self) -> Pid { - Pid::from_u32(self.fallback_pid) - } -} - -#[cfg(unix)] -impl ProcessIdGetter { - fn new(pty: &Pty) -> ProcessIdGetter { - ProcessIdGetter { - handle: pty.file().as_raw_fd(), - fallback_pid: pty.child().id(), - } - } - - fn pid(&self) -> Option { - let pid = unsafe { libc::tcgetpgrp(self.handle) }; - if pid < 0 { - return Some(Pid::from_u32(self.fallback_pid)); - } - Some(Pid::from_u32(pid as u32)) - } -} - -#[cfg(windows)] -impl ProcessIdGetter { - fn new(pty: &Pty) -> ProcessIdGetter { - let child = pty.child_watcher(); - let handle = child.raw_handle(); - let fallback_pid = child.pid().unwrap_or_else(|| unsafe { - NonZeroU32::new_unchecked(GetProcessId(HANDLE(handle as _))) - }); - - ProcessIdGetter { - handle: handle as i32, - fallback_pid: u32::from(fallback_pid), - } - } - - fn pid(&self) -> Option { - let pid = unsafe { GetProcessId(HANDLE(self.handle as _)) }; - // the GetProcessId may fail and returns zero, which will lead to a stack overflow issue - if pid == 0 { - // in the builder process, there is a small chance, almost negligible, - // that this value could be zero, which means child_watcher returns None, - // GetProcessId returns 0. - if self.fallback_pid == 0 { - return None; - } - return Some(Pid::from_u32(self.fallback_pid)); - } - Some(Pid::from_u32(pid)) - } -} - -#[derive(Clone, Debug)] -pub struct ProcessInfo { - pub name: String, - pub cwd: PathBuf, - pub argv: Vec, -} - -/// Fetches Zed-relevant Pseudo-Terminal (PTY) process information -pub struct PtyProcessInfo { - system: System, - refresh_kind: ProcessRefreshKind, - pid_getter: ProcessIdGetter, - pub current: Option, -} - -impl PtyProcessInfo { - pub fn new(pty: &Pty) -> PtyProcessInfo { - let process_refresh_kind = ProcessRefreshKind::nothing() - .with_cmd(UpdateKind::Always) - .with_cwd(UpdateKind::Always) - .with_exe(UpdateKind::Always); - let refresh_kind = RefreshKind::nothing().with_processes(process_refresh_kind); - let system = System::new_with_specifics(refresh_kind); - - PtyProcessInfo { - system, - refresh_kind: process_refresh_kind, - pid_getter: ProcessIdGetter::new(pty), - current: None, - } - } - - pub fn pid_getter(&self) -> &ProcessIdGetter { - &self.pid_getter - } - - fn refresh(&mut self) -> Option<&Process> { - let pid = self.pid_getter.pid()?; - if self.system.refresh_processes_specifics( - sysinfo::ProcessesToUpdate::Some(&[pid]), - true, - self.refresh_kind, - ) == 1 - { - self.system.process(pid) - } else { - None - } - } - - fn get_child(&self) -> Option<&Process> { - let pid = self.pid_getter.fallback_pid(); - self.system.process(pid) - } - - pub(crate) fn kill_current_process(&mut self) -> bool { - self.refresh().is_some_and(|process| process.kill()) - } - - pub(crate) fn kill_child_process(&mut self) -> bool { - self.get_child().is_some_and(|process| process.kill()) - } - - fn load(&mut self) -> Option { - let process = self.refresh()?; - let cwd = process.cwd().map_or(PathBuf::new(), |p| p.to_owned()); - - let info = ProcessInfo { - name: process.name().to_str()?.to_owned(), - cwd, - argv: process - .cmd() - .iter() - .filter_map(|s| s.to_str().map(ToOwned::to_owned)) - .collect(), - }; - self.current = Some(info.clone()); - Some(info) - } - - /// Updates the cached process info, returns whether the Zed-relevant info has changed - pub fn has_changed(&mut self) -> bool { - let current = self.load(); - let has_changed = match (self.current.as_ref(), current.as_ref()) { - (None, None) => false, - (Some(prev), Some(now)) => prev.cwd != now.cwd || prev.name != now.name, - _ => true, - }; - if has_changed { - self.current = current; - } - has_changed - } - - pub fn pid(&self) -> Option { - self.pid_getter.pid() - } -} diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs deleted file mode 100644 index caca93eac5..0000000000 --- a/crates/terminal/src/terminal.rs +++ /dev/null @@ -1,2854 +0,0 @@ -pub mod mappings; - -pub use alacritty_terminal; - -mod pty_info; -mod terminal_hyperlinks; -pub mod terminal_settings; - -use alacritty_terminal::{ - Term, - event::{Event as AlacTermEvent, EventListener, Notify, WindowSize}, - event_loop::{EventLoop, Msg, Notifier}, - grid::{Dimensions, Grid, Row, Scroll as AlacScroll}, - index::{Boundary, Column, Direction as AlacDirection, Line, Point as AlacPoint}, - selection::{Selection, SelectionRange, SelectionType}, - sync::FairMutex, - term::{ - Config, RenderableCursor, TermMode, - cell::{Cell, Flags}, - search::{Match, RegexIter, RegexSearch}, - }, - tty::{self}, - vi_mode::{ViModeCursor, ViMotion}, - vte::ansi::{ - ClearMode, CursorStyle as AlacCursorStyle, Handler, NamedPrivateMode, PrivateMode, - }, -}; -use anyhow::{Context as _, Result, bail}; -use log::trace; - -use futures::{ - FutureExt, - channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded}, -}; - -use itertools::Itertools as _; -use mappings::mouse::{ - alt_scroll, grid_point, grid_point_and_side, mouse_button_report, mouse_moved_report, - scroll_report, -}; - -use collections::{HashMap, VecDeque}; -use futures::StreamExt; -use pty_info::{ProcessIdGetter, PtyProcessInfo}; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use smol::channel::{Receiver, Sender}; -use task::{HideStrategy, Shell, SpawnInTerminal}; -use terminal_hyperlinks::RegexSearches; -use terminal_settings::{AlternateScroll, CursorShape, TerminalSettings}; -use theme::{ActiveTheme, Theme}; -use urlencoding; -use util::truncate_and_trailoff; - -use std::{ - borrow::Cow, - cmp::{self, min}, - fmt::Display, - ops::{Deref, RangeInclusive}, - path::PathBuf, - process::ExitStatus, - sync::Arc, - time::Instant, -}; -use thiserror::Error; - -use gpui::{ - App, AppContext as _, Bounds, ClipboardItem, Context, EventEmitter, Hsla, Keystroke, Modifiers, - MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Rgba, - ScrollWheelEvent, Size, Task, TouchPhase, Window, actions, black, px, -}; - -use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str}; - -actions!( - terminal, - [ - /// Clears the terminal screen. - Clear, - /// Copies selected text to the clipboard. - Copy, - /// Pastes from the clipboard. - Paste, - /// Shows the character palette for special characters. - ShowCharacterPalette, - /// Searches for text in the terminal. - SearchTest, - /// Scrolls up by one line. - ScrollLineUp, - /// Scrolls down by one line. - ScrollLineDown, - /// Scrolls up by one page. - ScrollPageUp, - /// Scrolls down by one page. - ScrollPageDown, - /// Scrolls up by half a page. - ScrollHalfPageUp, - /// Scrolls down by half a page. - ScrollHalfPageDown, - /// Scrolls to the top of the terminal buffer. - ScrollToTop, - /// Scrolls to the bottom of the terminal buffer. - ScrollToBottom, - /// Toggles vi mode in the terminal. - ToggleViMode, - /// Selects all text in the terminal. - SelectAll, - ] -); - -const DEBUG_TERMINAL_WIDTH: Pixels = px(500.); -const DEBUG_TERMINAL_HEIGHT: Pixels = px(30.); -const DEBUG_CELL_WIDTH: Pixels = px(5.); -const DEBUG_LINE_HEIGHT: Pixels = px(5.); - -///Upward flowing events, for changing the title and such -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Event { - TitleChanged, - BreadcrumbsChanged, - CloseTerminal, - Bell, - Wakeup, - BlinkChanged(bool), - SelectionsChanged, - NewNavigationTarget(Option), - Open(MaybeNavigationTarget), -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PathLikeTarget { - /// File system path, absolute or relative, existing or not. - /// Might have line and column number(s) attached as `file.rs:1:23` - pub maybe_path: String, - /// Current working directory of the terminal - pub terminal_dir: Option, -} - -/// A string inside terminal, potentially useful as a URI that can be opened. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum MaybeNavigationTarget { - /// HTTP, git, etc. string determined by the `URL_REGEX` regex. - Url(String), - /// File system path, absolute or relative, existing or not. - /// Might have line and column number(s) attached as `file.rs:1:23` - PathLike(PathLikeTarget), -} - -#[derive(Clone)] -enum InternalEvent { - Resize(TerminalBounds), - Clear, - // FocusNextMatch, - Scroll(AlacScroll), - ScrollToAlacPoint(AlacPoint), - SetSelection(Option<(Selection, AlacPoint)>), - UpdateSelection(Point), - // Adjusted mouse position, should open - FindHyperlink(Point, bool), - // Whether keep selection when copy - Copy(Option), - // Vi mode events - ToggleViMode, - ViMotion(ViMotion), - MoveViCursorToAlacPoint(AlacPoint), -} - -///A translation struct for Alacritty to communicate with us from their event loop -#[derive(Clone)] -pub struct ZedListener(pub UnboundedSender); - -impl EventListener for ZedListener { - fn send_event(&self, event: AlacTermEvent) { - self.0.unbounded_send(event).ok(); - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct TerminalBounds { - pub cell_width: Pixels, - pub line_height: Pixels, - pub bounds: Bounds, -} - -impl TerminalBounds { - pub fn new(line_height: Pixels, cell_width: Pixels, bounds: Bounds) -> Self { - TerminalBounds { - cell_width, - line_height, - bounds, - } - } - - pub fn num_lines(&self) -> usize { - (self.bounds.size.height / self.line_height).floor() as usize - } - - pub fn num_columns(&self) -> usize { - (self.bounds.size.width / self.cell_width).floor() as usize - } - - pub fn height(&self) -> Pixels { - self.bounds.size.height - } - - pub fn width(&self) -> Pixels { - self.bounds.size.width - } - - pub fn cell_width(&self) -> Pixels { - self.cell_width - } - - pub fn line_height(&self) -> Pixels { - self.line_height - } -} - -impl Default for TerminalBounds { - fn default() -> Self { - TerminalBounds::new( - DEBUG_LINE_HEIGHT, - DEBUG_CELL_WIDTH, - Bounds { - origin: Point::default(), - size: Size { - width: DEBUG_TERMINAL_WIDTH, - height: DEBUG_TERMINAL_HEIGHT, - }, - }, - ) - } -} - -impl From for WindowSize { - fn from(val: TerminalBounds) -> Self { - WindowSize { - num_lines: val.num_lines() as u16, - num_cols: val.num_columns() as u16, - cell_width: f32::from(val.cell_width()) as u16, - cell_height: f32::from(val.line_height()) as u16, - } - } -} - -impl Dimensions for TerminalBounds { - /// Note: this is supposed to be for the back buffer's length, - /// but we exclusively use it to resize the terminal, which does not - /// use this method. We still have to implement it for the trait though, - /// hence, this comment. - fn total_lines(&self) -> usize { - self.screen_lines() - } - - fn screen_lines(&self) -> usize { - self.num_lines() - } - - fn columns(&self) -> usize { - self.num_columns() - } -} - -#[derive(Error, Debug)] -pub struct TerminalError { - pub directory: Option, - pub program: Option, - pub args: Option>, - pub title_override: Option, - pub source: std::io::Error, -} - -impl TerminalError { - pub fn fmt_directory(&self) -> String { - self.directory - .clone() - .map(|path| { - match path - .into_os_string() - .into_string() - .map_err(|os_str| format!(" {}", os_str.to_string_lossy())) - { - Ok(s) => s, - Err(s) => s, - } - }) - .unwrap_or_else(|| "".to_string()) - } - - pub fn fmt_shell(&self) -> String { - if let Some(title_override) = &self.title_override { - format!( - "{} {} ({})", - self.program.as_deref().unwrap_or(""), - self.args.as_ref().into_iter().flatten().format(" "), - title_override - ) - } else { - format!( - "{} {}", - self.program.as_deref().unwrap_or(""), - self.args.as_ref().into_iter().flatten().format(" ") - ) - } - } -} - -impl Display for TerminalError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let dir_string: String = self.fmt_directory(); - let shell = self.fmt_shell(); - - write!( - f, - "Working directory: {} Shell command: `{}`, IOError: {}", - dir_string, shell, self.source - ) - } -} - -// https://github.com/alacritty/alacritty/blob/cb3a79dbf6472740daca8440d5166c1d4af5029e/extra/man/alacritty.5.scd?plain=1#L207-L213 -const DEFAULT_SCROLL_HISTORY_LINES: usize = 10_000; -pub const MAX_SCROLL_HISTORY_LINES: usize = 100_000; - -pub struct TerminalBuilder { - terminal: Terminal, - events_rx: UnboundedReceiver, -} - -impl TerminalBuilder { - pub fn new_display_only( - cursor_shape: CursorShape, - alternate_scroll: AlternateScroll, - max_scroll_history_lines: Option, - window_id: u64, - ) -> Result { - // Create a display-only terminal (no actual PTY). - let default_cursor_style = AlacCursorStyle::from(cursor_shape); - let scrolling_history = max_scroll_history_lines - .unwrap_or(DEFAULT_SCROLL_HISTORY_LINES) - .min(MAX_SCROLL_HISTORY_LINES); - let config = Config { - scrolling_history, - default_cursor_style, - ..Config::default() - }; - - let (events_tx, events_rx) = unbounded(); - let mut term = Term::new( - config.clone(), - &TerminalBounds::default(), - ZedListener(events_tx), - ); - - if let AlternateScroll::Off = alternate_scroll { - term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll)); - } - - let term = Arc::new(FairMutex::new(term)); - - let terminal = Terminal { - task: None, - terminal_type: TerminalType::DisplayOnly, - completion_tx: None, - term, - term_config: config, - title_override: None, - events: VecDeque::with_capacity(10), - last_content: Default::default(), - last_mouse: None, - matches: Vec::new(), - selection_head: None, - breadcrumb_text: String::new(), - scroll_px: px(0.), - next_link_id: 0, - selection_phase: SelectionPhase::Ended, - hyperlink_regex_searches: RegexSearches::default(), - vi_mode_enabled: false, - is_remote_terminal: false, - last_mouse_move_time: Instant::now(), - last_hyperlink_search_position: None, - #[cfg(windows)] - shell_program: None, - activation_script: Vec::new(), - template: CopyTemplate { - shell: Shell::System, - env: HashMap::default(), - cursor_shape, - alternate_scroll, - max_scroll_history_lines, - path_hyperlink_regexes: Vec::default(), - path_hyperlink_timeout_ms: 0, - window_id, - }, - child_exited: None, - event_loop_task: Task::ready(Ok(())), - }; - - Ok(TerminalBuilder { - terminal, - events_rx, - }) - } - - pub fn new( - working_directory: Option, - task: Option, - shell: Shell, - mut env: HashMap, - cursor_shape: CursorShape, - alternate_scroll: AlternateScroll, - max_scroll_history_lines: Option, - path_hyperlink_regexes: Vec, - path_hyperlink_timeout_ms: u64, - is_remote_terminal: bool, - window_id: u64, - completion_tx: Option>>, - cx: &App, - activation_script: Vec, - ) -> Task> { - let version = release_channel::AppVersion::global(cx); - let fut = async move { - // If the parent environment doesn't have a locale set - // (As is the case when launched from a .app on MacOS), - // and the Project doesn't have a locale set, then - // set a fallback for our child environment to use. - if std::env::var("LANG").is_err() { - env.entry("LANG".to_string()) - .or_insert_with(|| "en_US.UTF-8".to_string()); - } - - env.insert("ZED_TERM".to_string(), "true".to_string()); - env.insert("TERM_PROGRAM".to_string(), "zed".to_string()); - env.insert("TERM".to_string(), "xterm-256color".to_string()); - env.insert("COLORTERM".to_string(), "truecolor".to_string()); - env.insert("TERM_PROGRAM_VERSION".to_string(), version.to_string()); - - #[derive(Default)] - struct ShellParams { - program: String, - args: Option>, - title_override: Option, - } - - impl ShellParams { - fn new( - program: String, - args: Option>, - title_override: Option, - ) -> Self { - log::debug!("Using {program} as shell"); - Self { - program, - args, - title_override, - } - } - } - - let shell_params = match shell.clone() { - Shell::System => { - if cfg!(windows) { - Some(ShellParams::new( - util::shell::get_windows_system_shell(), - None, - None, - )) - } else { - None - } - } - Shell::Program(program) => Some(ShellParams::new(program, None, None)), - Shell::WithArguments { - program, - args, - title_override, - } => Some(ShellParams::new(program, Some(args), title_override)), - }; - let terminal_title_override = - shell_params.as_ref().and_then(|e| e.title_override.clone()); - - #[cfg(windows)] - let shell_program = shell_params.as_ref().map(|params| { - use util::ResultExt; - - Self::resolve_path(¶ms.program) - .log_err() - .unwrap_or(params.program.clone()) - }); - - // Note: when remoting, this shell_kind will scrutinize `ssh` or - // `wsl.exe` as a shell and fall back to posix or powershell based on - // the compilation target. This is fine right now due to the restricted - // way we use the return value, but would become incorrect if we - // supported remoting into windows. - let shell_kind = shell.shell_kind(cfg!(windows)); - - let pty_options = { - let alac_shell = shell_params.as_ref().map(|params| { - alacritty_terminal::tty::Shell::new( - params.program.clone(), - params.args.clone().unwrap_or_default(), - ) - }); - - alacritty_terminal::tty::Options { - shell: alac_shell, - working_directory: working_directory.clone(), - drain_on_exit: true, - env: env.clone().into_iter().collect(), - #[cfg(windows)] - escape_args: shell_kind.tty_escape_args(), - } - }; - - let default_cursor_style = AlacCursorStyle::from(cursor_shape); - let scrolling_history = if task.is_some() { - // Tasks like `cargo build --all` may produce a lot of output, ergo allow maximum scrolling. - // After the task finishes, we do not allow appending to that terminal, so small tasks output should not - // cause excessive memory usage over time. - MAX_SCROLL_HISTORY_LINES - } else { - max_scroll_history_lines - .unwrap_or(DEFAULT_SCROLL_HISTORY_LINES) - .min(MAX_SCROLL_HISTORY_LINES) - }; - let config = Config { - scrolling_history, - default_cursor_style, - ..Config::default() - }; - - //Setup the pty... - let pty = match tty::new(&pty_options, TerminalBounds::default().into(), window_id) { - Ok(pty) => pty, - Err(error) => { - bail!(TerminalError { - directory: working_directory, - program: shell_params.as_ref().map(|params| params.program.clone()), - args: shell_params.as_ref().and_then(|params| params.args.clone()), - title_override: terminal_title_override, - source: error, - }); - } - }; - - //Spawn a task so the Alacritty EventLoop can communicate with us - //TODO: Remove with a bounded sender which can be dispatched on &self - let (events_tx, events_rx) = unbounded(); - //Set up the terminal... - let mut term = Term::new( - config.clone(), - &TerminalBounds::default(), - ZedListener(events_tx.clone()), - ); - - //Alacritty defaults to alternate scrolling being on, so we just need to turn it off. - if let AlternateScroll::Off = alternate_scroll { - term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll)); - } - - let term = Arc::new(FairMutex::new(term)); - - let pty_info = PtyProcessInfo::new(&pty); - - //And connect them together - let event_loop = EventLoop::new( - term.clone(), - ZedListener(events_tx), - pty, - pty_options.drain_on_exit, - false, - ) - .context("failed to create event loop")?; - - let pty_tx = event_loop.channel(); - let _io_thread = event_loop.spawn(); // DANGER - - let no_task = task.is_none(); - let terminal = Terminal { - task, - terminal_type: TerminalType::Pty { - pty_tx: Notifier(pty_tx), - info: pty_info, - }, - completion_tx, - term, - term_config: config, - title_override: terminal_title_override, - events: VecDeque::with_capacity(10), //Should never get this high. - last_content: Default::default(), - last_mouse: None, - matches: Vec::new(), - selection_head: None, - breadcrumb_text: String::new(), - scroll_px: px(0.), - next_link_id: 0, - selection_phase: SelectionPhase::Ended, - hyperlink_regex_searches: RegexSearches::new( - &path_hyperlink_regexes, - path_hyperlink_timeout_ms, - ), - vi_mode_enabled: false, - is_remote_terminal, - last_mouse_move_time: Instant::now(), - last_hyperlink_search_position: None, - #[cfg(windows)] - shell_program, - activation_script: activation_script.clone(), - template: CopyTemplate { - shell, - env, - cursor_shape, - alternate_scroll, - max_scroll_history_lines, - path_hyperlink_regexes, - path_hyperlink_timeout_ms, - window_id, - }, - child_exited: None, - event_loop_task: Task::ready(Ok(())), - }; - - if !activation_script.is_empty() && no_task { - for activation_script in activation_script { - terminal.write_to_pty(activation_script.into_bytes()); - // Simulate enter key press - // NOTE(PowerShell): using `\r\n` will put PowerShell in a continuation mode (infamous >> character) - // and generally mess up the rendering. - terminal.write_to_pty(b"\x0d"); - } - // In order to clear the screen at this point, we have two options: - // 1. We can send a shell-specific command such as "clear" or "cls" - // 2. We can "echo" a marker message that we will then catch when handling a Wakeup event - // and clear the screen using `terminal.clear()` method - // We cannot issue a `terminal.clear()` command at this point as alacritty is evented - // and while we have sent the activation script to the pty, it will be executed asynchronously. - // Therefore, we somehow need to wait for the activation script to finish executing before we - // can proceed with clearing the screen. - terminal.write_to_pty(shell_kind.clear_screen_command().as_bytes()); - // Simulate enter key press - terminal.write_to_pty(b"\x0d"); - } - - Ok(TerminalBuilder { - terminal, - events_rx, - }) - }; - // the thread we spawn things on has an effect on signal handling - if !cfg!(target_os = "windows") { - cx.spawn(async move |_| fut.await) - } else { - cx.background_spawn(fut) - } - } - - pub fn subscribe(mut self, cx: &Context) -> Terminal { - //Event loop - self.terminal.event_loop_task = cx.spawn(async move |terminal, cx| { - while let Some(event) = self.events_rx.next().await { - terminal.update(cx, |terminal, cx| { - //Process the first event immediately for lowered latency - terminal.process_event(event, cx); - })?; - - 'outer: loop { - let mut events = Vec::new(); - - #[cfg(any(test, feature = "test-support"))] - let mut timer = cx.background_executor().simulate_random_delay().fuse(); - #[cfg(not(any(test, feature = "test-support")))] - let mut timer = cx - .background_executor() - .timer(std::time::Duration::from_millis(4)) - .fuse(); - - let mut wakeup = false; - loop { - futures::select_biased! { - _ = timer => break, - event = self.events_rx.next() => { - if let Some(event) = event { - if matches!(event, AlacTermEvent::Wakeup) { - wakeup = true; - } else { - events.push(event); - } - - if events.len() > 100 { - break; - } - } else { - break; - } - }, - } - } - - if events.is_empty() && !wakeup { - smol::future::yield_now().await; - break 'outer; - } - - terminal.update(cx, |this, cx| { - if wakeup { - this.process_event(AlacTermEvent::Wakeup, cx); - } - - for event in events { - this.process_event(event, cx); - } - })?; - smol::future::yield_now().await; - } - } - anyhow::Ok(()) - }); - self.terminal - } - - #[cfg(windows)] - fn resolve_path(path: &str) -> Result { - use windows::Win32::Storage::FileSystem::SearchPathW; - use windows::core::HSTRING; - - let path = if path.starts_with(r"\\?\") || !path.contains(&['/', '\\']) { - path.to_string() - } else { - r"\\?\".to_string() + path - }; - - let required_length = unsafe { SearchPathW(None, &HSTRING::from(&path), None, None, None) }; - let mut buf = vec![0u16; required_length as usize]; - let size = unsafe { SearchPathW(None, &HSTRING::from(&path), None, Some(&mut buf), None) }; - - Ok(String::from_utf16(&buf[..size as usize])?) - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct IndexedCell { - pub point: AlacPoint, - pub cell: Cell, -} - -impl Deref for IndexedCell { - type Target = Cell; - - #[inline] - fn deref(&self) -> &Cell { - &self.cell - } -} - -// TODO: Un-pub -#[derive(Clone)] -pub struct TerminalContent { - pub cells: Vec, - pub mode: TermMode, - pub display_offset: usize, - pub selection_text: Option, - pub selection: Option, - pub cursor: RenderableCursor, - pub cursor_char: char, - pub terminal_bounds: TerminalBounds, - pub last_hovered_word: Option, - pub scrolled_to_top: bool, - pub scrolled_to_bottom: bool, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct HoveredWord { - pub word: String, - pub word_match: RangeInclusive, - pub id: usize, -} - -impl Default for TerminalContent { - fn default() -> Self { - TerminalContent { - cells: Default::default(), - mode: Default::default(), - display_offset: Default::default(), - selection_text: Default::default(), - selection: Default::default(), - cursor: RenderableCursor { - shape: alacritty_terminal::vte::ansi::CursorShape::Block, - point: AlacPoint::new(Line(0), Column(0)), - }, - cursor_char: Default::default(), - terminal_bounds: Default::default(), - last_hovered_word: None, - scrolled_to_top: false, - scrolled_to_bottom: false, - } - } -} - -#[derive(PartialEq, Eq)] -pub enum SelectionPhase { - Selecting, - Ended, -} - -enum TerminalType { - Pty { - pty_tx: Notifier, - info: PtyProcessInfo, - }, - DisplayOnly, -} - -pub struct Terminal { - terminal_type: TerminalType, - completion_tx: Option>>, - term: Arc>>, - term_config: Config, - events: VecDeque, - /// This is only used for mouse mode cell change detection - last_mouse: Option<(AlacPoint, AlacDirection)>, - pub matches: Vec>, - pub last_content: TerminalContent, - pub selection_head: Option, - pub breadcrumb_text: String, - title_override: Option, - scroll_px: Pixels, - next_link_id: usize, - selection_phase: SelectionPhase, - hyperlink_regex_searches: RegexSearches, - task: Option, - vi_mode_enabled: bool, - is_remote_terminal: bool, - last_mouse_move_time: Instant, - last_hyperlink_search_position: Option>, - #[cfg(windows)] - shell_program: Option, - template: CopyTemplate, - activation_script: Vec, - child_exited: Option, - event_loop_task: Task>, -} - -struct CopyTemplate { - shell: Shell, - env: HashMap, - cursor_shape: CursorShape, - alternate_scroll: AlternateScroll, - max_scroll_history_lines: Option, - path_hyperlink_regexes: Vec, - path_hyperlink_timeout_ms: u64, - window_id: u64, -} - -#[derive(Debug)] -pub struct TaskState { - pub status: TaskStatus, - pub completion_rx: Receiver>, - pub spawned_task: SpawnInTerminal, -} - -/// A status of the current terminal tab's task. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TaskStatus { - /// The task had been started, but got cancelled or somehow otherwise it did not - /// report its exit code before the terminal event loop was shut down. - Unknown, - /// The task is started and running currently. - Running, - /// After the start, the task stopped running and reported its error code back. - Completed { success: bool }, -} - -impl TaskStatus { - fn register_terminal_exit(&mut self) { - if self == &Self::Running { - *self = Self::Unknown; - } - } - - fn register_task_exit(&mut self, error_code: i32) { - *self = TaskStatus::Completed { - success: error_code == 0, - }; - } -} - -impl Terminal { - fn process_event(&mut self, event: AlacTermEvent, cx: &mut Context) { - match event { - AlacTermEvent::Title(title) => { - // ignore default shell program title change as windows always sends those events - // and it would end up showing the shell executable path in breadcrumbs - #[cfg(windows)] - { - if self - .shell_program - .as_ref() - .map(|e| *e == title) - .unwrap_or(false) - { - return; - } - } - - self.breadcrumb_text = title; - cx.emit(Event::BreadcrumbsChanged); - } - AlacTermEvent::ResetTitle => { - self.breadcrumb_text = String::new(); - cx.emit(Event::BreadcrumbsChanged); - } - AlacTermEvent::ClipboardStore(_, data) => { - cx.write_to_clipboard(ClipboardItem::new_string(data)) - } - AlacTermEvent::ClipboardLoad(_, format) => { - self.write_to_pty( - match &cx.read_from_clipboard().and_then(|item| item.text()) { - // The terminal only supports pasting strings, not images. - Some(text) => format(text), - _ => format(""), - } - .into_bytes(), - ) - } - AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.into_bytes()), - AlacTermEvent::TextAreaSizeRequest(format) => { - self.write_to_pty(format(self.last_content.terminal_bounds.into()).into_bytes()) - } - AlacTermEvent::CursorBlinkingChange => { - let terminal = self.term.lock(); - let blinking = terminal.cursor_style().blinking; - cx.emit(Event::BlinkChanged(blinking)); - } - AlacTermEvent::Bell => { - cx.emit(Event::Bell); - } - AlacTermEvent::Exit => self.register_task_finished(None, cx), - AlacTermEvent::MouseCursorDirty => { - //NOOP, Handled in render - } - AlacTermEvent::Wakeup => { - cx.emit(Event::Wakeup); - - if let TerminalType::Pty { info, .. } = &mut self.terminal_type { - if info.has_changed() { - cx.emit(Event::TitleChanged); - } - } - } - AlacTermEvent::ColorRequest(index, format) => { - // It's important that the color request is processed here to retain relative order - // with other PTY writes. Otherwise applications might witness out-of-order - // responses to requests. For example: An application sending `OSC 11 ; ? ST` - // (color request) followed by `CSI c` (request device attributes) would receive - // the response to `CSI c` first. - // Instead of locking, we could store the colors in `self.last_content`. But then - // we might respond with out of date value if a "set color" sequence is immediately - // followed by a color request sequence. - let color = self.term.lock().colors()[index] - .unwrap_or_else(|| to_alac_rgb(get_color_at_index(index, cx.theme().as_ref()))); - self.write_to_pty(format(color).into_bytes()); - } - AlacTermEvent::ChildExit(error_code) => { - self.register_task_finished(Some(error_code), cx); - } - } - } - - pub fn selection_started(&self) -> bool { - self.selection_phase == SelectionPhase::Selecting - } - - fn process_terminal_event( - &mut self, - event: &InternalEvent, - term: &mut Term, - window: &mut Window, - cx: &mut Context, - ) { - match event { - &InternalEvent::Resize(mut new_bounds) => { - trace!("Resizing: new_bounds={new_bounds:?}"); - new_bounds.bounds.size.height = - cmp::max(new_bounds.line_height, new_bounds.height()); - new_bounds.bounds.size.width = cmp::max(new_bounds.cell_width, new_bounds.width()); - - self.last_content.terminal_bounds = new_bounds; - - if let TerminalType::Pty { pty_tx, .. } = &self.terminal_type { - pty_tx.0.send(Msg::Resize(new_bounds.into())).ok(); - } - - term.resize(new_bounds); - // If there are matches we need to emit a wake up event to - // invalidate the matches and recalculate their locations - // in the new terminal layout - if !self.matches.is_empty() { - cx.emit(Event::Wakeup); - } - } - InternalEvent::Clear => { - trace!("Clearing"); - // Clear back buffer - term.clear_screen(ClearMode::Saved); - - let cursor = term.grid().cursor.point; - - // Clear the lines above - term.grid_mut().reset_region(..cursor.line); - - // Copy the current line up - let line = term.grid()[cursor.line][..Column(term.grid().columns())] - .iter() - .cloned() - .enumerate() - .collect::>(); - - for (i, cell) in line { - term.grid_mut()[Line(0)][Column(i)] = cell; - } - - // Reset the cursor - term.grid_mut().cursor.point = - AlacPoint::new(Line(0), term.grid_mut().cursor.point.column); - let new_cursor = term.grid().cursor.point; - - // Clear the lines below the new cursor - if (new_cursor.line.0 as usize) < term.screen_lines() - 1 { - term.grid_mut().reset_region((new_cursor.line + 1)..); - } - - cx.emit(Event::Wakeup); - } - InternalEvent::Scroll(scroll) => { - trace!("Scrolling: scroll={scroll:?}"); - term.scroll_display(*scroll); - self.refresh_hovered_word(window); - - if self.vi_mode_enabled { - match *scroll { - AlacScroll::Delta(delta) => { - term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, delta); - } - AlacScroll::PageUp => { - let lines = term.screen_lines() as i32; - term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, lines); - } - AlacScroll::PageDown => { - let lines = -(term.screen_lines() as i32); - term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, lines); - } - AlacScroll::Top => { - let point = AlacPoint::new(term.topmost_line(), Column(0)); - term.vi_mode_cursor = ViModeCursor::new(point); - } - AlacScroll::Bottom => { - let point = AlacPoint::new(term.bottommost_line(), Column(0)); - term.vi_mode_cursor = ViModeCursor::new(point); - } - } - if let Some(mut selection) = term.selection.take() { - let point = term.vi_mode_cursor.point; - selection.update(point, AlacDirection::Right); - term.selection = Some(selection); - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - if let Some(selection_text) = term.selection_to_string() { - cx.write_to_primary(ClipboardItem::new_string(selection_text)); - } - - self.selection_head = Some(point); - cx.emit(Event::SelectionsChanged) - } - } - } - InternalEvent::SetSelection(selection) => { - trace!("Setting selection: selection={selection:?}"); - term.selection = selection.as_ref().map(|(sel, _)| sel.clone()); - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - if let Some(selection_text) = term.selection_to_string() { - cx.write_to_primary(ClipboardItem::new_string(selection_text)); - } - - if let Some((_, head)) = selection { - self.selection_head = Some(*head); - } - cx.emit(Event::SelectionsChanged) - } - InternalEvent::UpdateSelection(position) => { - trace!("Updating selection: position={position:?}"); - if let Some(mut selection) = term.selection.take() { - let (point, side) = grid_point_and_side( - *position, - self.last_content.terminal_bounds, - term.grid().display_offset(), - ); - - selection.update(point, side); - term.selection = Some(selection); - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - if let Some(selection_text) = term.selection_to_string() { - cx.write_to_primary(ClipboardItem::new_string(selection_text)); - } - - self.selection_head = Some(point); - cx.emit(Event::SelectionsChanged) - } - } - - InternalEvent::Copy(keep_selection) => { - trace!("Copying selection: keep_selection={keep_selection:?}"); - if let Some(txt) = term.selection_to_string() { - cx.write_to_clipboard(ClipboardItem::new_string(txt)); - if !keep_selection.unwrap_or_else(|| { - let settings = TerminalSettings::get_global(cx); - settings.keep_selection_on_copy - }) { - self.events.push_back(InternalEvent::SetSelection(None)); - } - } - } - InternalEvent::ScrollToAlacPoint(point) => { - trace!("Scrolling to point: point={point:?}"); - term.scroll_to_point(*point); - self.refresh_hovered_word(window); - } - InternalEvent::MoveViCursorToAlacPoint(point) => { - trace!("Move vi cursor to point: point={point:?}"); - term.vi_goto_point(*point); - self.refresh_hovered_word(window); - } - InternalEvent::ToggleViMode => { - trace!("Toggling vi mode"); - self.vi_mode_enabled = !self.vi_mode_enabled; - term.toggle_vi_mode(); - } - InternalEvent::ViMotion(motion) => { - trace!("Performing vi motion: motion={motion:?}"); - term.vi_motion(*motion); - } - InternalEvent::FindHyperlink(position, open) => { - trace!("Finding hyperlink at position: position={position:?}, open={open:?}"); - let prev_hovered_word = self.last_content.last_hovered_word.take(); - - let point = grid_point( - *position, - self.last_content.terminal_bounds, - term.grid().display_offset(), - ) - .grid_clamp(term, Boundary::Grid); - - match terminal_hyperlinks::find_from_grid_point( - term, - point, - &mut self.hyperlink_regex_searches, - ) { - Some((maybe_url_or_path, is_url, url_match)) => { - let target = if is_url { - // Treat "file://" URLs like file paths to ensure - // that line numbers at the end of the path are - // handled correctly. - // file://{path} should be urldecoded, returning a urldecoded {path} - if let Some(path) = maybe_url_or_path.strip_prefix("file://") { - let decoded_path = urlencoding::decode(path) - .map(|decoded| decoded.into_owned()) - .unwrap_or(path.to_owned()); - - MaybeNavigationTarget::PathLike(PathLikeTarget { - maybe_path: decoded_path, - terminal_dir: self.working_directory(), - }) - } else { - MaybeNavigationTarget::Url(maybe_url_or_path.clone()) - } - } else { - MaybeNavigationTarget::PathLike(PathLikeTarget { - maybe_path: maybe_url_or_path.clone(), - terminal_dir: self.working_directory(), - }) - }; - if *open { - cx.emit(Event::Open(target)); - } else { - self.update_selected_word( - prev_hovered_word, - url_match, - maybe_url_or_path, - target, - cx, - ); - } - } - None => { - cx.emit(Event::NewNavigationTarget(None)); - } - } - } - } - } - - fn update_selected_word( - &mut self, - prev_word: Option, - word_match: RangeInclusive, - word: String, - navigation_target: MaybeNavigationTarget, - cx: &mut Context, - ) { - if let Some(prev_word) = prev_word - && prev_word.word == word - && prev_word.word_match == word_match - { - self.last_content.last_hovered_word = Some(HoveredWord { - word, - word_match, - id: prev_word.id, - }); - return; - } - - self.last_content.last_hovered_word = Some(HoveredWord { - word, - word_match, - id: self.next_link_id(), - }); - cx.emit(Event::NewNavigationTarget(Some(navigation_target))); - cx.notify() - } - - fn next_link_id(&mut self) -> usize { - let res = self.next_link_id; - self.next_link_id = self.next_link_id.wrapping_add(1); - res - } - - pub fn last_content(&self) -> &TerminalContent { - &self.last_content - } - - pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape) { - self.term_config.default_cursor_style = cursor_shape.into(); - self.term.lock().set_options(self.term_config.clone()); - } - - pub fn write_output(&mut self, bytes: &[u8], cx: &mut Context) { - // Inject bytes directly into the terminal emulator and refresh the UI. - // This bypasses the PTY/event loop for display-only terminals. - // - // We first convert LF to CRLF, to get the expected line wrapping in Alacritty. - // When output comes from piped commands (not a PTY) such as codex-acp, and that - // output only contains LF (\n) without a CR (\r) after it, such as the output - // of the `ls` command when running outside a PTY, Alacritty moves the cursor - // cursor down a line but does not move it back to the initial column. This makes - // the rendered output look ridiculous. To prevent this, we insert a CR (\r) before - // each LF that didn't already have one. (Alacritty doesn't have a setting for this.) - let mut converted = Vec::with_capacity(bytes.len()); - let mut prev_byte = 0u8; - for &byte in bytes { - if byte == b'\n' && prev_byte != b'\r' { - converted.push(b'\r'); - } - converted.push(byte); - prev_byte = byte; - } - - let mut processor = alacritty_terminal::vte::ansi::Processor::< - alacritty_terminal::vte::ansi::StdSyncHandler, - >::new(); - { - let mut term = self.term.lock(); - processor.advance(&mut *term, &converted); - } - cx.emit(Event::Wakeup); - } - - pub fn total_lines(&self) -> usize { - self.term.lock_unfair().total_lines() - } - - pub fn viewport_lines(&self) -> usize { - self.term.lock_unfair().screen_lines() - } - - //To test: - //- Activate match on terminal (scrolling and selection) - //- Editor search snapping behavior - - pub fn activate_match(&mut self, index: usize) { - if let Some(search_match) = self.matches.get(index).cloned() { - self.set_selection(Some((make_selection(&search_match), *search_match.end()))); - if self.vi_mode_enabled { - self.events - .push_back(InternalEvent::MoveViCursorToAlacPoint(*search_match.end())); - } else { - self.events - .push_back(InternalEvent::ScrollToAlacPoint(*search_match.start())); - } - } - } - - pub fn select_matches(&mut self, matches: &[RangeInclusive]) { - let matches_to_select = self - .matches - .iter() - .filter(|self_match| matches.contains(self_match)) - .cloned() - .collect::>(); - for match_to_select in matches_to_select { - self.set_selection(Some(( - make_selection(&match_to_select), - *match_to_select.end(), - ))); - } - } - - pub fn select_all(&mut self) { - let term = self.term.lock(); - let start = AlacPoint::new(term.topmost_line(), Column(0)); - let end = AlacPoint::new(term.bottommost_line(), term.last_column()); - drop(term); - self.set_selection(Some((make_selection(&(start..=end)), end))); - } - - fn set_selection(&mut self, selection: Option<(Selection, AlacPoint)>) { - self.events - .push_back(InternalEvent::SetSelection(selection)); - } - - pub fn copy(&mut self, keep_selection: Option) { - self.events.push_back(InternalEvent::Copy(keep_selection)); - } - - pub fn clear(&mut self) { - self.events.push_back(InternalEvent::Clear) - } - - pub fn scroll_line_up(&mut self) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::Delta(1))); - } - - pub fn scroll_up_by(&mut self, lines: usize) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::Delta(lines as i32))); - } - - pub fn scroll_line_down(&mut self) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::Delta(-1))); - } - - pub fn scroll_down_by(&mut self, lines: usize) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::Delta(-(lines as i32)))); - } - - pub fn scroll_page_up(&mut self) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::PageUp)); - } - - pub fn scroll_page_down(&mut self) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::PageDown)); - } - - pub fn scroll_to_top(&mut self) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::Top)); - } - - pub fn scroll_to_bottom(&mut self) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::Bottom)); - } - - pub fn scrolled_to_top(&self) -> bool { - self.last_content.scrolled_to_top - } - - pub fn scrolled_to_bottom(&self) -> bool { - self.last_content.scrolled_to_bottom - } - - ///Resize the terminal and the PTY. - pub fn set_size(&mut self, new_bounds: TerminalBounds) { - if self.last_content.terminal_bounds != new_bounds { - self.events.push_back(InternalEvent::Resize(new_bounds)) - } - } - - /// Write the Input payload to the PTY, if applicable. - /// (This is a no-op for display-only terminals.) - fn write_to_pty(&self, input: impl Into>) { - if let TerminalType::Pty { pty_tx, .. } = &self.terminal_type { - let input = input.into(); - if log::log_enabled!(log::Level::Debug) { - if let Ok(str) = str::from_utf8(&input) { - log::debug!("Writing to PTY: {:?}", str); - } else { - log::debug!("Writing to PTY: {:?}", input); - } - } - pty_tx.notify(input); - } - } - - pub fn input(&mut self, input: impl Into>) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::Bottom)); - self.events.push_back(InternalEvent::SetSelection(None)); - - self.write_to_pty(input); - } - - pub fn toggle_vi_mode(&mut self) { - self.events.push_back(InternalEvent::ToggleViMode); - } - - pub fn vi_motion(&mut self, keystroke: &Keystroke) { - if !self.vi_mode_enabled { - return; - } - - let key: Cow<'_, str> = if keystroke.modifiers.shift { - Cow::Owned(keystroke.key.to_uppercase()) - } else { - Cow::Borrowed(keystroke.key.as_str()) - }; - - let motion: Option = match key.as_ref() { - "h" | "left" => Some(ViMotion::Left), - "j" | "down" => Some(ViMotion::Down), - "k" | "up" => Some(ViMotion::Up), - "l" | "right" => Some(ViMotion::Right), - "w" => Some(ViMotion::WordRight), - "b" if !keystroke.modifiers.control => Some(ViMotion::WordLeft), - "e" => Some(ViMotion::WordRightEnd), - "%" => Some(ViMotion::Bracket), - "$" => Some(ViMotion::Last), - "0" => Some(ViMotion::First), - "^" => Some(ViMotion::FirstOccupied), - "H" => Some(ViMotion::High), - "M" => Some(ViMotion::Middle), - "L" => Some(ViMotion::Low), - _ => None, - }; - - if let Some(motion) = motion { - let cursor = self.last_content.cursor.point; - let cursor_pos = Point { - x: cursor.column.0 as f32 * self.last_content.terminal_bounds.cell_width, - y: cursor.line.0 as f32 * self.last_content.terminal_bounds.line_height, - }; - self.events - .push_back(InternalEvent::UpdateSelection(cursor_pos)); - self.events.push_back(InternalEvent::ViMotion(motion)); - return; - } - - let scroll_motion = match key.as_ref() { - "g" => Some(AlacScroll::Top), - "G" => Some(AlacScroll::Bottom), - "b" if keystroke.modifiers.control => Some(AlacScroll::PageUp), - "f" if keystroke.modifiers.control => Some(AlacScroll::PageDown), - "d" if keystroke.modifiers.control => { - let amount = self.last_content.terminal_bounds.line_height().to_f64() as i32 / 2; - Some(AlacScroll::Delta(-amount)) - } - "u" if keystroke.modifiers.control => { - let amount = self.last_content.terminal_bounds.line_height().to_f64() as i32 / 2; - Some(AlacScroll::Delta(amount)) - } - _ => None, - }; - - if let Some(scroll_motion) = scroll_motion { - self.events.push_back(InternalEvent::Scroll(scroll_motion)); - return; - } - - match key.as_ref() { - "v" => { - let point = self.last_content.cursor.point; - let selection_type = SelectionType::Simple; - let side = AlacDirection::Right; - let selection = Selection::new(selection_type, point, side); - self.events - .push_back(InternalEvent::SetSelection(Some((selection, point)))); - } - - "escape" => { - self.events.push_back(InternalEvent::SetSelection(None)); - } - - "y" => { - self.copy(Some(false)); - } - - "i" => { - self.scroll_to_bottom(); - self.toggle_vi_mode(); - } - _ => {} - } - } - - pub fn try_keystroke(&mut self, keystroke: &Keystroke, option_as_meta: bool) -> bool { - if self.vi_mode_enabled { - self.vi_motion(keystroke); - return true; - } - - // Keep default terminal behavior - let esc = to_esc_str(keystroke, &self.last_content.mode, option_as_meta); - if let Some(esc) = esc { - match esc { - Cow::Borrowed(string) => self.input(string.as_bytes()), - Cow::Owned(string) => self.input(string.into_bytes()), - }; - true - } else { - false - } - } - - pub fn try_modifiers_change( - &mut self, - modifiers: &Modifiers, - window: &Window, - cx: &mut Context, - ) { - if self - .last_content - .terminal_bounds - .bounds - .contains(&window.mouse_position()) - && modifiers.secondary() - { - self.refresh_hovered_word(window); - } - cx.notify(); - } - - ///Paste text into the terminal - pub fn paste(&mut self, text: &str) { - let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) { - format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~") - } else { - text.replace("\r\n", "\r").replace('\n', "\r") - }; - - self.input(paste_text.into_bytes()); - } - - pub fn sync(&mut self, window: &mut Window, cx: &mut Context) { - let term = self.term.clone(); - let mut terminal = term.lock_unfair(); - //Note that the ordering of events matters for event processing - while let Some(e) = self.events.pop_front() { - self.process_terminal_event(&e, &mut terminal, window, cx) - } - - self.last_content = Self::make_content(&terminal, &self.last_content); - } - - fn make_content(term: &Term, last_content: &TerminalContent) -> TerminalContent { - let content = term.renderable_content(); - - // Pre-allocate with estimated size to reduce reallocations - let estimated_size = content.display_iter.size_hint().0; - let mut cells = Vec::with_capacity(estimated_size); - - cells.extend(content.display_iter.map(|ic| IndexedCell { - point: ic.point, - cell: ic.cell.clone(), - })); - - let selection_text = if content.selection.is_some() { - term.selection_to_string() - } else { - None - }; - - TerminalContent { - cells, - mode: content.mode, - display_offset: content.display_offset, - selection_text, - selection: content.selection, - cursor: content.cursor, - cursor_char: term.grid()[content.cursor.point].c, - terminal_bounds: last_content.terminal_bounds, - last_hovered_word: last_content.last_hovered_word.clone(), - scrolled_to_top: content.display_offset == term.history_size(), - scrolled_to_bottom: content.display_offset == 0, - } - } - - pub fn get_content(&self) -> String { - let term = self.term.lock_unfair(); - let start = AlacPoint::new(term.topmost_line(), Column(0)); - let end = AlacPoint::new(term.bottommost_line(), term.last_column()); - term.bounds_to_string(start, end) - } - - pub fn last_n_non_empty_lines(&self, n: usize) -> Vec { - let term = self.term.clone(); - let terminal = term.lock_unfair(); - let grid = terminal.grid(); - let mut lines = Vec::new(); - - let mut current_line = grid.bottommost_line().0; - let topmost_line = grid.topmost_line().0; - - while current_line >= topmost_line && lines.len() < n { - let logical_line_start = self.find_logical_line_start(grid, current_line, topmost_line); - let logical_line = self.construct_logical_line(grid, logical_line_start, current_line); - - if let Some(line) = self.process_line(logical_line) { - lines.push(line); - } - - // Move to the line above the start of the current logical line - current_line = logical_line_start - 1; - } - - lines.reverse(); - lines - } - - fn find_logical_line_start(&self, grid: &Grid, current: i32, topmost: i32) -> i32 { - let mut line_start = current; - while line_start > topmost { - let prev_line = Line(line_start - 1); - let last_cell = &grid[prev_line][Column(grid.columns() - 1)]; - if !last_cell.flags.contains(Flags::WRAPLINE) { - break; - } - line_start -= 1; - } - line_start - } - - fn construct_logical_line(&self, grid: &Grid, start: i32, end: i32) -> String { - let mut logical_line = String::new(); - for row in start..=end { - let grid_row = &grid[Line(row)]; - logical_line.push_str(&row_to_string(grid_row)); - } - logical_line - } - - fn process_line(&self, line: String) -> Option { - let trimmed = line.trim_end().to_string(); - if !trimmed.is_empty() { - Some(trimmed) - } else { - None - } - } - - pub fn focus_in(&self) { - if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) { - self.write_to_pty("\x1b[I".as_bytes()); - } - } - - pub fn focus_out(&mut self) { - if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) { - self.write_to_pty("\x1b[O".as_bytes()); - } - } - - pub fn mouse_changed(&mut self, point: AlacPoint, side: AlacDirection) -> bool { - match self.last_mouse { - Some((old_point, old_side)) => { - if old_point == point && old_side == side { - false - } else { - self.last_mouse = Some((point, side)); - true - } - } - None => { - self.last_mouse = Some((point, side)); - true - } - } - } - - pub fn mouse_mode(&self, shift: bool) -> bool { - self.last_content.mode.intersects(TermMode::MOUSE_MODE) && !shift - } - - pub fn mouse_move(&mut self, e: &MouseMoveEvent, cx: &mut Context) { - let position = e.position - self.last_content.terminal_bounds.bounds.origin; - if self.mouse_mode(e.modifiers.shift) { - let (point, side) = grid_point_and_side( - position, - self.last_content.terminal_bounds, - self.last_content.display_offset, - ); - - if self.mouse_changed(point, side) - && let Some(bytes) = - mouse_moved_report(point, e.pressed_button, e.modifiers, self.last_content.mode) - { - self.write_to_pty(bytes); - } - } else if e.modifiers.secondary() { - self.word_from_position(e.position); - } - cx.notify(); - } - - fn word_from_position(&mut self, position: Point) { - if self.selection_phase == SelectionPhase::Selecting { - self.last_content.last_hovered_word = None; - } else if self.last_content.terminal_bounds.bounds.contains(&position) { - // Throttle hyperlink searches to avoid excessive processing - let now = Instant::now(); - let should_search = if let Some(last_pos) = self.last_hyperlink_search_position { - // Only search if mouse moved significantly or enough time passed - let distance_moved = - ((position.x - last_pos.x).abs() + (position.y - last_pos.y).abs()) > px(5.0); - let time_elapsed = now.duration_since(self.last_mouse_move_time).as_millis() > 100; - distance_moved || time_elapsed - } else { - true - }; - - if should_search { - self.last_mouse_move_time = now; - self.last_hyperlink_search_position = Some(position); - self.events.push_back(InternalEvent::FindHyperlink( - position - self.last_content.terminal_bounds.bounds.origin, - false, - )); - } - } else { - self.last_content.last_hovered_word = None; - } - } - - pub fn select_word_at_event_position(&mut self, e: &MouseDownEvent) { - let position = e.position - self.last_content.terminal_bounds.bounds.origin; - let (point, side) = grid_point_and_side( - position, - self.last_content.terminal_bounds, - self.last_content.display_offset, - ); - let selection = Selection::new(SelectionType::Semantic, point, side); - self.events - .push_back(InternalEvent::SetSelection(Some((selection, point)))); - } - - pub fn mouse_drag( - &mut self, - e: &MouseMoveEvent, - region: Bounds, - cx: &mut Context, - ) { - let position = e.position - self.last_content.terminal_bounds.bounds.origin; - if !self.mouse_mode(e.modifiers.shift) { - self.selection_phase = SelectionPhase::Selecting; - // Alacritty has the same ordering, of first updating the selection - // then scrolling 15ms later - self.events - .push_back(InternalEvent::UpdateSelection(position)); - - // Doesn't make sense to scroll the alt screen - if !self.last_content.mode.contains(TermMode::ALT_SCREEN) { - let scroll_lines = match self.drag_line_delta(e, region) { - Some(value) => value, - None => return, - }; - - self.events - .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines))); - } - - cx.notify(); - } - } - - fn drag_line_delta(&self, e: &MouseMoveEvent, region: Bounds) -> Option { - let top = region.origin.y; - let bottom = region.bottom_left().y; - - let scroll_lines = if e.position.y < top { - let scroll_delta = (top - e.position.y).pow(1.1); - (scroll_delta / self.last_content.terminal_bounds.line_height).ceil() as i32 - } else if e.position.y > bottom { - let scroll_delta = -((e.position.y - bottom).pow(1.1)); - (scroll_delta / self.last_content.terminal_bounds.line_height).floor() as i32 - } else { - return None; - }; - - Some(scroll_lines.clamp(-3, 3)) - } - - pub fn mouse_down(&mut self, e: &MouseDownEvent, _cx: &mut Context) { - let position = e.position - self.last_content.terminal_bounds.bounds.origin; - let point = grid_point( - position, - self.last_content.terminal_bounds, - self.last_content.display_offset, - ); - - if self.mouse_mode(e.modifiers.shift) { - if let Some(bytes) = - mouse_button_report(point, e.button, e.modifiers, true, self.last_content.mode) - { - self.write_to_pty(bytes); - } - } else { - match e.button { - MouseButton::Left => { - let (point, side) = grid_point_and_side( - position, - self.last_content.terminal_bounds, - self.last_content.display_offset, - ); - - let selection_type = match e.click_count { - 0 => return, //This is a release - 1 => Some(SelectionType::Simple), - 2 => Some(SelectionType::Semantic), - 3 => Some(SelectionType::Lines), - _ => None, - }; - - if selection_type == Some(SelectionType::Simple) && e.modifiers.shift { - self.events - .push_back(InternalEvent::UpdateSelection(position)); - return; - } - - let selection = selection_type - .map(|selection_type| Selection::new(selection_type, point, side)); - - if let Some(sel) = selection { - self.events - .push_back(InternalEvent::SetSelection(Some((sel, point)))); - } - } - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - MouseButton::Middle => { - if let Some(item) = _cx.read_from_primary() { - let text = item.text().unwrap_or_default(); - self.input(text.into_bytes()); - } - } - _ => {} - } - } - } - - pub fn mouse_up(&mut self, e: &MouseUpEvent, cx: &Context) { - let setting = TerminalSettings::get_global(cx); - - let position = e.position - self.last_content.terminal_bounds.bounds.origin; - if self.mouse_mode(e.modifiers.shift) { - let point = grid_point( - position, - self.last_content.terminal_bounds, - self.last_content.display_offset, - ); - - if let Some(bytes) = - mouse_button_report(point, e.button, e.modifiers, false, self.last_content.mode) - { - self.write_to_pty(bytes); - } - } else { - if e.button == MouseButton::Left && setting.copy_on_select { - self.copy(Some(true)); - } - - //Hyperlinks - if self.selection_phase == SelectionPhase::Ended { - let mouse_cell_index = - content_index_for_mouse(position, &self.last_content.terminal_bounds); - if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() { - cx.open_url(link.uri()); - } else if e.modifiers.secondary() { - self.events - .push_back(InternalEvent::FindHyperlink(position, true)); - } - } - } - - self.selection_phase = SelectionPhase::Ended; - self.last_mouse = None; - } - - ///Scroll the terminal - pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, scroll_multiplier: f32) { - let mouse_mode = self.mouse_mode(e.shift); - let scroll_multiplier = if mouse_mode { 1. } else { scroll_multiplier }; - - if let Some(scroll_lines) = self.determine_scroll_lines(e, scroll_multiplier) { - if mouse_mode { - let point = grid_point( - e.position - self.last_content.terminal_bounds.bounds.origin, - self.last_content.terminal_bounds, - self.last_content.display_offset, - ); - - if let Some(scrolls) = scroll_report(point, scroll_lines, e, self.last_content.mode) - { - for scroll in scrolls { - self.write_to_pty(scroll); - } - }; - } else if self - .last_content - .mode - .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL) - && !e.shift - { - self.write_to_pty(alt_scroll(scroll_lines)); - } else if scroll_lines != 0 { - let scroll = AlacScroll::Delta(scroll_lines); - - self.events.push_back(InternalEvent::Scroll(scroll)); - } - } - } - - fn refresh_hovered_word(&mut self, window: &Window) { - self.word_from_position(window.mouse_position()); - } - - fn determine_scroll_lines( - &mut self, - e: &ScrollWheelEvent, - scroll_multiplier: f32, - ) -> Option { - let line_height = self.last_content.terminal_bounds.line_height; - match e.touch_phase { - /* Reset scroll state on started */ - TouchPhase::Started => { - self.scroll_px = px(0.); - None - } - /* Calculate the appropriate scroll lines */ - TouchPhase::Moved => { - let old_offset = (self.scroll_px / line_height) as i32; - - self.scroll_px += e.delta.pixel_delta(line_height).y * scroll_multiplier; - - let new_offset = (self.scroll_px / line_height) as i32; - - // Whenever we hit the edges, reset our stored scroll to 0 - // so we can respond to changes in direction quickly - self.scroll_px %= self.last_content.terminal_bounds.height(); - - Some(new_offset - old_offset) - } - TouchPhase::Ended => None, - } - } - - pub fn find_matches( - &self, - mut searcher: RegexSearch, - cx: &Context, - ) -> Task>> { - let term = self.term.clone(); - cx.background_spawn(async move { - let term = term.lock(); - - all_search_matches(&term, &mut searcher).collect() - }) - } - - pub fn working_directory(&self) -> Option { - if self.is_remote_terminal { - // We can't yet reliably detect the working directory of a shell on the - // SSH host. Until we can do that, it doesn't make sense to display - // the working directory on the client and persist that. - None - } else { - self.client_side_working_directory() - } - } - - /// Returns the working directory of the process that's connected to the PTY. - /// That means it returns the working directory of the local shell or program - /// that's running inside the terminal. - /// - /// This does *not* return the working directory of the shell that runs on the - /// remote host, in case Zed is connected to a remote host. - fn client_side_working_directory(&self) -> Option { - match &self.terminal_type { - TerminalType::Pty { info, .. } => { - info.current.as_ref().map(|process| process.cwd.clone()) - } - TerminalType::DisplayOnly => None, - } - } - - pub fn title(&self, truncate: bool) -> String { - const MAX_CHARS: usize = 25; - match &self.task { - Some(task_state) => { - if truncate { - truncate_and_trailoff(&task_state.spawned_task.label, MAX_CHARS) - } else { - task_state.spawned_task.full_label.clone() - } - } - None => self - .title_override - .as_ref() - .map(|title_override| title_override.to_string()) - .unwrap_or_else(|| match &self.terminal_type { - TerminalType::Pty { info, .. } => info - .current - .as_ref() - .map(|fpi| { - let process_file = fpi - .cwd - .file_name() - .map(|name| name.to_string_lossy().into_owned()) - .unwrap_or_default(); - - let argv = fpi.argv.as_slice(); - let process_name = format!( - "{}{}", - fpi.name, - if !argv.is_empty() { - format!(" {}", (argv[1..]).join(" ")) - } else { - "".to_string() - } - ); - let (process_file, process_name) = if truncate { - ( - truncate_and_trailoff(&process_file, MAX_CHARS), - truncate_and_trailoff(&process_name, MAX_CHARS), - ) - } else { - (process_file, process_name) - }; - format!("{process_file} — {process_name}") - }) - .unwrap_or_else(|| "Terminal".to_string()), - TerminalType::DisplayOnly => "Terminal".to_string(), - }), - } - } - - pub fn kill_active_task(&mut self) { - if let Some(task) = self.task() - && task.status == TaskStatus::Running - { - if let TerminalType::Pty { info, .. } = &mut self.terminal_type { - info.kill_current_process(); - } - } - } - - pub fn pid(&self) -> Option { - match &self.terminal_type { - TerminalType::Pty { info, .. } => info.pid(), - TerminalType::DisplayOnly => None, - } - } - - pub fn pid_getter(&self) -> Option<&ProcessIdGetter> { - match &self.terminal_type { - TerminalType::Pty { info, .. } => Some(info.pid_getter()), - TerminalType::DisplayOnly => None, - } - } - - pub fn task(&self) -> Option<&TaskState> { - self.task.as_ref() - } - - pub fn wait_for_completed_task(&self, cx: &App) -> Task> { - if let Some(task) = self.task() { - if task.status == TaskStatus::Running { - let completion_receiver = task.completion_rx.clone(); - return cx.spawn(async move |_| completion_receiver.recv().await.ok().flatten()); - } else if let Ok(status) = task.completion_rx.try_recv() { - return Task::ready(status); - } - } - Task::ready(None) - } - - fn register_task_finished(&mut self, error_code: Option, cx: &mut Context) { - let e: Option = error_code.map(|code| { - #[cfg(unix)] - { - std::os::unix::process::ExitStatusExt::from_raw(code) - } - #[cfg(windows)] - { - std::os::windows::process::ExitStatusExt::from_raw(code as u32) - } - }); - - if let Some(tx) = &self.completion_tx { - tx.try_send(e).ok(); - } - if let Some(e) = e { - self.child_exited = Some(e); - } - let task = match &mut self.task { - Some(task) => task, - None => { - if self.child_exited.is_none_or(|e| e.code() == Some(0)) { - cx.emit(Event::CloseTerminal); - } - return; - } - }; - if task.status != TaskStatus::Running { - return; - } - match error_code { - Some(error_code) => { - task.status.register_task_exit(error_code); - } - None => { - task.status.register_terminal_exit(); - } - }; - - let (finished_successfully, task_line, command_line) = task_summary(task, error_code); - let mut lines_to_show = Vec::new(); - if task.spawned_task.show_summary { - lines_to_show.push(task_line.as_str()); - } - if task.spawned_task.show_command { - lines_to_show.push(command_line.as_str()); - } - - if !lines_to_show.is_empty() { - // SAFETY: the invocation happens on non `TaskStatus::Running` tasks, once, - // after either `AlacTermEvent::Exit` or `AlacTermEvent::ChildExit` events that are spawned - // when Zed task finishes and no more output is made. - // After the task summary is output once, no more text is appended to the terminal. - unsafe { append_text_to_term(&mut self.term.lock(), &lines_to_show) }; - } - - match task.spawned_task.hide { - HideStrategy::Never => {} - HideStrategy::Always => { - cx.emit(Event::CloseTerminal); - } - HideStrategy::OnSuccess => { - if finished_successfully { - cx.emit(Event::CloseTerminal); - } - } - } - } - - pub fn vi_mode_enabled(&self) -> bool { - self.vi_mode_enabled - } - - pub fn clone_builder(&self, cx: &App, cwd: Option) -> Task> { - let working_directory = self.working_directory().or_else(|| cwd); - TerminalBuilder::new( - working_directory, - None, - self.template.shell.clone(), - self.template.env.clone(), - self.template.cursor_shape, - self.template.alternate_scroll, - self.template.max_scroll_history_lines, - self.template.path_hyperlink_regexes.clone(), - self.template.path_hyperlink_timeout_ms, - self.is_remote_terminal, - self.template.window_id, - None, - cx, - self.activation_script.clone(), - ) - } -} - -// Helper function to convert a grid row to a string -pub fn row_to_string(row: &Row) -> String { - row[..Column(row.len())] - .iter() - .map(|cell| cell.c) - .collect::() -} - -const TASK_DELIMITER: &str = "⏵ "; -fn task_summary(task: &TaskState, error_code: Option) -> (bool, String, String) { - let escaped_full_label = task - .spawned_task - .full_label - .replace("\r\n", "\r") - .replace('\n', "\r"); - let success = error_code == Some(0); - let task_line = match error_code { - Some(0) => format!("{TASK_DELIMITER}Task `{escaped_full_label}` finished successfully"), - Some(error_code) => format!( - "{TASK_DELIMITER}Task `{escaped_full_label}` finished with non-zero error code: {error_code}" - ), - None => format!("{TASK_DELIMITER}Task `{escaped_full_label}` finished"), - }; - let escaped_command_label = task - .spawned_task - .command_label - .replace("\r\n", "\r") - .replace('\n', "\r"); - let command_line = format!("{TASK_DELIMITER}Command: {escaped_command_label}"); - (success, task_line, command_line) -} - -/// Appends a stringified task summary to the terminal, after its output. -/// -/// SAFETY: This function should only be called after terminal's PTY is no longer alive. -/// New text being added to the terminal here, uses "less public" APIs, -/// which are not maintaining the entire terminal state intact. -/// -/// -/// The library -/// -/// * does not increment inner grid cursor's _lines_ on `input` calls -/// (but displaying the lines correctly and incrementing cursor's columns) -/// -/// * ignores `\n` and \r` character input, requiring the `newline` call instead -/// -/// * does not alter grid state after `newline` call -/// so its `bottommost_line` is always the same additions, and -/// the cursor's `point` is not updated to the new line and column values -/// -/// * ??? there could be more consequences, and any further "proper" streaming from the PTY might bug and/or panic. -/// Still, subsequent `append_text_to_term` invocations are possible and display the contents correctly. -/// -/// Despite the quirks, this is the simplest approach to appending text to the terminal: its alternative, `grid_mut` manipulations, -/// do not properly set the scrolling state and display odd text after appending; also those manipulations are more tedious and error-prone. -/// The function achieves proper display and scrolling capabilities, at a cost of grid state not properly synchronized. -/// This is enough for printing moderately-sized texts like task summaries, but might break or perform poorly for larger texts. -unsafe fn append_text_to_term(term: &mut Term, text_lines: &[&str]) { - term.newline(); - term.grid_mut().cursor.point.column = Column(0); - for line in text_lines { - for c in line.chars() { - term.input(c); - } - term.newline(); - term.grid_mut().cursor.point.column = Column(0); - } -} - -impl Drop for Terminal { - fn drop(&mut self) { - if let TerminalType::Pty { pty_tx, info } = &mut self.terminal_type { - info.kill_child_process(); - pty_tx.0.send(Msg::Shutdown).ok(); - } - } -} - -impl EventEmitter for Terminal {} - -fn make_selection(range: &RangeInclusive) -> Selection { - let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left); - selection.update(*range.end(), AlacDirection::Right); - selection -} - -fn all_search_matches<'a, T>( - term: &'a Term, - regex: &'a mut RegexSearch, -) -> impl Iterator + 'a { - let start = AlacPoint::new(term.grid().topmost_line(), Column(0)); - let end = AlacPoint::new(term.grid().bottommost_line(), term.grid().last_column()); - RegexIter::new(start, end, AlacDirection::Right, term, regex) -} - -fn content_index_for_mouse(pos: Point, terminal_bounds: &TerminalBounds) -> usize { - let col = (pos.x / terminal_bounds.cell_width()).round() as usize; - let clamped_col = min(col, terminal_bounds.columns() - 1); - let row = (pos.y / terminal_bounds.line_height()).round() as usize; - let clamped_row = min(row, terminal_bounds.screen_lines() - 1); - clamped_row * terminal_bounds.columns() + clamped_col -} - -/// Converts an 8 bit ANSI color to its GPUI equivalent. -/// Accepts `usize` for compatibility with the `alacritty::Colors` interface, -/// Other than that use case, should only be called with values in the `[0,255]` range -pub fn get_color_at_index(index: usize, theme: &Theme) -> Hsla { - let colors = theme.colors(); - - match index { - // 0-15 are the same as the named colors above - 0 => colors.terminal_ansi_black, - 1 => colors.terminal_ansi_red, - 2 => colors.terminal_ansi_green, - 3 => colors.terminal_ansi_yellow, - 4 => colors.terminal_ansi_blue, - 5 => colors.terminal_ansi_magenta, - 6 => colors.terminal_ansi_cyan, - 7 => colors.terminal_ansi_white, - 8 => colors.terminal_ansi_bright_black, - 9 => colors.terminal_ansi_bright_red, - 10 => colors.terminal_ansi_bright_green, - 11 => colors.terminal_ansi_bright_yellow, - 12 => colors.terminal_ansi_bright_blue, - 13 => colors.terminal_ansi_bright_magenta, - 14 => colors.terminal_ansi_bright_cyan, - 15 => colors.terminal_ansi_bright_white, - // 16-231 are a 6x6x6 RGB color cube, mapped to 0-255 using steps defined by XTerm. - // See: https://github.com/xterm-x11/xterm-snapshots/blob/master/256colres.pl - 16..=231 => { - let (r, g, b) = rgb_for_index(index as u8); - rgba_color( - if r == 0 { 0 } else { r * 40 + 55 }, - if g == 0 { 0 } else { g * 40 + 55 }, - if b == 0 { 0 } else { b * 40 + 55 }, - ) - } - // 232-255 are a 24-step grayscale ramp from (8, 8, 8) to (238, 238, 238). - 232..=255 => { - let i = index as u8 - 232; // Align index to 0..24 - let value = i * 10 + 8; - rgba_color(value, value, value) - } - // For compatibility with the alacritty::Colors interface - // See: https://github.com/alacritty/alacritty/blob/master/alacritty_terminal/src/term/color.rs - 256 => colors.terminal_foreground, - 257 => colors.terminal_background, - 258 => theme.players().local().cursor, - 259 => colors.terminal_ansi_dim_black, - 260 => colors.terminal_ansi_dim_red, - 261 => colors.terminal_ansi_dim_green, - 262 => colors.terminal_ansi_dim_yellow, - 263 => colors.terminal_ansi_dim_blue, - 264 => colors.terminal_ansi_dim_magenta, - 265 => colors.terminal_ansi_dim_cyan, - 266 => colors.terminal_ansi_dim_white, - 267 => colors.terminal_bright_foreground, - 268 => colors.terminal_ansi_black, // 'Dim Background', non-standard color - - _ => black(), - } -} - -/// Generates the RGB channels in [0, 5] for a given index into the 6x6x6 ANSI color cube. -/// -/// See: [8 bit ANSI color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit). -/// -/// Wikipedia gives a formula for calculating the index for a given color: -/// -/// ```text -/// index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5) -/// ``` -/// -/// This function does the reverse, calculating the `r`, `g`, and `b` components from a given index. -fn rgb_for_index(i: u8) -> (u8, u8, u8) { - debug_assert!((16..=231).contains(&i)); - let i = i - 16; - let r = (i - (i % 36)) / 36; - let g = ((i % 36) - (i % 6)) / 6; - let b = (i % 36) % 6; - (r, g, b) -} - -pub fn rgba_color(r: u8, g: u8, b: u8) -> Hsla { - Rgba { - r: (r as f32 / 255.), - g: (g as f32 / 255.), - b: (b as f32 / 255.), - a: 1., - } - .into() -} - -#[cfg(test)] -mod tests { - use std::time::Duration; - - use super::*; - use crate::{ - IndexedCell, TerminalBounds, TerminalBuilder, TerminalContent, content_index_for_mouse, - rgb_for_index, - }; - use alacritty_terminal::{ - index::{Column, Line, Point as AlacPoint}, - term::cell::Cell, - }; - use collections::HashMap; - use gpui::{Pixels, Point, TestAppContext, bounds, point, size, smol_timeout}; - use rand::{Rng, distr, rngs::ThreadRng}; - use task::ShellBuilder; - - #[gpui::test] - async fn test_basic_terminal(cx: &mut TestAppContext) { - cx.executor().allow_parking(); - - let (completion_tx, completion_rx) = smol::channel::unbounded(); - let (program, args) = ShellBuilder::new(&Shell::System, false) - .build(Some("echo".to_owned()), &["hello".to_owned()]); - let builder = cx - .update(|cx| { - TerminalBuilder::new( - None, - None, - task::Shell::WithArguments { - program, - args, - title_override: None, - }, - HashMap::default(), - CursorShape::default(), - AlternateScroll::On, - None, - vec![], - 0, - false, - 0, - Some(completion_tx), - cx, - vec![], - ) - }) - .await - .unwrap(); - let terminal = cx.new(|cx| builder.subscribe(cx)); - assert_eq!( - completion_rx.recv().await.unwrap(), - Some(ExitStatus::default()) - ); - assert_eq!( - terminal.update(cx, |term, _| term.get_content()).trim(), - "hello" - ); - - // Inject additional output directly into the emulator (display-only path) - terminal.update(cx, |term, cx| { - term.write_output(b"\nfrom_injection", cx); - }); - - let content_after = terminal.update(cx, |term, _| term.get_content()); - assert!( - content_after.contains("from_injection"), - "expected injected output to appear, got: {content_after}" - ); - } - - // TODO should be tested on Linux too, but does not work there well - #[cfg(target_os = "macos")] - #[gpui::test(iterations = 10)] - async fn test_terminal_eof(cx: &mut TestAppContext) { - cx.executor().allow_parking(); - - let (completion_tx, completion_rx) = smol::channel::unbounded(); - let builder = cx - .update(|cx| { - TerminalBuilder::new( - None, - None, - task::Shell::System, - HashMap::default(), - CursorShape::default(), - AlternateScroll::On, - None, - vec![], - 0, - false, - 0, - Some(completion_tx), - cx, - Vec::new(), - ) - }) - .await - .unwrap(); - // Build an empty command, which will result in a tty shell spawned. - let terminal = cx.new(|cx| builder.subscribe(cx)); - - let (event_tx, event_rx) = smol::channel::unbounded::(); - cx.update(|cx| { - cx.subscribe(&terminal, move |_, e, _| { - event_tx.send_blocking(e.clone()).unwrap(); - }) - }) - .detach(); - cx.background_spawn(async move { - assert_eq!( - completion_rx.recv().await.unwrap(), - Some(ExitStatus::default()), - "EOF should result in the tty shell exiting successfully", - ); - }) - .detach(); - - let first_event = event_rx.recv().await.expect("No wakeup event received"); - - terminal.update(cx, |terminal, _| { - let success = terminal.try_keystroke(&Keystroke::parse("ctrl-c").unwrap(), false); - assert!(success, "Should have registered ctrl-c sequence"); - }); - terminal.update(cx, |terminal, _| { - let success = terminal.try_keystroke(&Keystroke::parse("ctrl-d").unwrap(), false); - assert!(success, "Should have registered ctrl-d sequence"); - }); - - let mut all_events = vec![first_event]; - while let Ok(Ok(new_event)) = smol_timeout(Duration::from_secs(1), event_rx.recv()).await { - all_events.push(new_event.clone()); - if new_event == Event::CloseTerminal { - break; - } - } - assert!( - all_events.contains(&Event::CloseTerminal), - "EOF command sequence should have triggered a TTY terminal exit, but got events: {all_events:?}", - ); - } - - #[gpui::test(iterations = 10)] - async fn test_terminal_no_exit_on_spawn_failure(cx: &mut TestAppContext) { - cx.executor().allow_parking(); - - let (completion_tx, completion_rx) = smol::channel::unbounded(); - let (program, args) = ShellBuilder::new(&Shell::System, false) - .build(Some("asdasdasdasd".to_owned()), &["@@@@@".to_owned()]); - let builder = cx - .update(|cx| { - TerminalBuilder::new( - None, - None, - task::Shell::WithArguments { - program, - args, - title_override: None, - }, - HashMap::default(), - CursorShape::default(), - AlternateScroll::On, - None, - Vec::new(), - 0, - false, - 0, - Some(completion_tx), - cx, - Vec::new(), - ) - }) - .await - .unwrap(); - let terminal = cx.new(|cx| builder.subscribe(cx)); - - let (event_tx, event_rx) = smol::channel::unbounded::(); - cx.update(|cx| { - cx.subscribe(&terminal, move |_, e, _| { - event_tx.send_blocking(e.clone()).unwrap(); - }) - }) - .detach(); - cx.background_spawn(async move { - #[cfg(target_os = "windows")] - { - let exit_status = completion_rx.recv().await.ok().flatten(); - if let Some(exit_status) = exit_status { - assert!( - !exit_status.success(), - "Wrong shell command should result in a failure" - ); - assert_eq!(exit_status.code(), Some(1)); - } - } - #[cfg(not(target_os = "windows"))] - { - let exit_status = completion_rx.recv().await.unwrap().unwrap(); - assert!( - !exit_status.success(), - "Wrong shell command should result in a failure" - ); - assert_eq!(exit_status.code(), None); - } - }) - .detach(); - - let mut all_events = Vec::new(); - while let Ok(Ok(new_event)) = - smol_timeout(Duration::from_millis(500), event_rx.recv()).await - { - all_events.push(new_event.clone()); - } - - assert!( - !all_events - .iter() - .any(|event| event == &Event::CloseTerminal), - "Wrong shell command should update the title but not should not close the terminal to show the error message, but got events: {all_events:?}", - ); - } - - #[test] - fn test_rgb_for_index() { - // Test every possible value in the color cube. - for i in 16..=231 { - let (r, g, b) = rgb_for_index(i); - assert_eq!(i, 16 + 36 * r + 6 * g + b); - } - } - - #[test] - fn test_mouse_to_cell_test() { - let mut rng = rand::rng(); - const ITERATIONS: usize = 10; - const PRECISION: usize = 1000; - - for _ in 0..ITERATIONS { - let viewport_cells = rng.random_range(15..20); - let cell_size = - rng.random_range(5 * PRECISION..20 * PRECISION) as f32 / PRECISION as f32; - - let size = crate::TerminalBounds { - cell_width: Pixels::from(cell_size), - line_height: Pixels::from(cell_size), - bounds: bounds( - Point::default(), - size( - Pixels::from(cell_size * (viewport_cells as f32)), - Pixels::from(cell_size * (viewport_cells as f32)), - ), - ), - }; - - let cells = get_cells(size, &mut rng); - let content = convert_cells_to_content(size, &cells); - - for row in 0..(viewport_cells - 1) { - let row = row as usize; - for col in 0..(viewport_cells - 1) { - let col = col as usize; - - let row_offset = rng.random_range(0..PRECISION) as f32 / PRECISION as f32; - let col_offset = rng.random_range(0..PRECISION) as f32 / PRECISION as f32; - - let mouse_pos = point( - Pixels::from(col as f32 * cell_size + col_offset), - Pixels::from(row as f32 * cell_size + row_offset), - ); - - let content_index = - content_index_for_mouse(mouse_pos, &content.terminal_bounds); - let mouse_cell = content.cells[content_index].c; - let real_cell = cells[row][col]; - - assert_eq!(mouse_cell, real_cell); - } - } - } - } - - #[test] - fn test_mouse_to_cell_clamp() { - let mut rng = rand::rng(); - - let size = crate::TerminalBounds { - cell_width: Pixels::from(10.), - line_height: Pixels::from(10.), - bounds: bounds( - Point::default(), - size(Pixels::from(100.), Pixels::from(100.)), - ), - }; - - let cells = get_cells(size, &mut rng); - let content = convert_cells_to_content(size, &cells); - - assert_eq!( - content.cells[content_index_for_mouse( - point(Pixels::from(-10.), Pixels::from(-10.)), - &content.terminal_bounds, - )] - .c, - cells[0][0] - ); - assert_eq!( - content.cells[content_index_for_mouse( - point(Pixels::from(1000.), Pixels::from(1000.)), - &content.terminal_bounds, - )] - .c, - cells[9][9] - ); - } - - fn get_cells(size: TerminalBounds, rng: &mut ThreadRng) -> Vec> { - let mut cells = Vec::new(); - - for _ in 0..((size.height() / size.line_height()) as usize) { - let mut row_vec = Vec::new(); - for _ in 0..((size.width() / size.cell_width()) as usize) { - let cell_char = rng.sample(distr::Alphanumeric) as char; - row_vec.push(cell_char) - } - cells.push(row_vec) - } - - cells - } - - fn convert_cells_to_content( - terminal_bounds: TerminalBounds, - cells: &[Vec], - ) -> TerminalContent { - let mut ic = Vec::new(); - - for (index, row) in cells.iter().enumerate() { - for (cell_index, cell_char) in row.iter().enumerate() { - ic.push(IndexedCell { - point: AlacPoint::new(Line(index as i32), Column(cell_index)), - cell: Cell { - c: *cell_char, - ..Default::default() - }, - }); - } - } - - TerminalContent { - cells: ic, - terminal_bounds, - ..Default::default() - } - } - - #[gpui::test] - async fn test_write_output_converts_lf_to_crlf(cx: &mut TestAppContext) { - let terminal = cx.new(|cx| { - TerminalBuilder::new_display_only(CursorShape::default(), AlternateScroll::On, None, 0) - .unwrap() - .subscribe(cx) - }); - - // Test simple LF conversion - terminal.update(cx, |terminal, cx| { - terminal.write_output(b"line1\nline2\n", cx); - }); - - // Get the content by directly accessing the term - let content = terminal.update(cx, |terminal, _cx| { - let term = terminal.term.lock_unfair(); - Terminal::make_content(&term, &terminal.last_content) - }); - - // If LF is properly converted to CRLF, each line should start at column 0 - // The diagonal staircase bug would cause increasing column positions - - // Get the cells and check that lines start at column 0 - let cells = &content.cells; - let mut line1_col0 = false; - let mut line2_col0 = false; - - for cell in cells { - if cell.c == 'l' && cell.point.column.0 == 0 { - if cell.point.line.0 == 0 && !line1_col0 { - line1_col0 = true; - } else if cell.point.line.0 == 1 && !line2_col0 { - line2_col0 = true; - } - } - } - - assert!(line1_col0, "First line should start at column 0"); - assert!(line2_col0, "Second line should start at column 0"); - } - - #[gpui::test] - async fn test_write_output_preserves_existing_crlf(cx: &mut TestAppContext) { - let terminal = cx.new(|cx| { - TerminalBuilder::new_display_only(CursorShape::default(), AlternateScroll::On, None, 0) - .unwrap() - .subscribe(cx) - }); - - // Test that existing CRLF doesn't get doubled - terminal.update(cx, |terminal, cx| { - terminal.write_output(b"line1\r\nline2\r\n", cx); - }); - - // Get the content by directly accessing the term - let content = terminal.update(cx, |terminal, _cx| { - let term = terminal.term.lock_unfair(); - Terminal::make_content(&term, &terminal.last_content) - }); - - let cells = &content.cells; - - // Check that both lines start at column 0 - let mut found_lines_at_column_0 = 0; - for cell in cells { - if cell.c == 'l' && cell.point.column.0 == 0 { - found_lines_at_column_0 += 1; - } - } - - assert!( - found_lines_at_column_0 >= 2, - "Both lines should start at column 0" - ); - } - - #[gpui::test] - async fn test_write_output_preserves_bare_cr(cx: &mut TestAppContext) { - let terminal = cx.new(|cx| { - TerminalBuilder::new_display_only(CursorShape::default(), AlternateScroll::On, None, 0) - .unwrap() - .subscribe(cx) - }); - - // Test that bare CR (without LF) is preserved - terminal.update(cx, |terminal, cx| { - terminal.write_output(b"hello\rworld", cx); - }); - - // Get the content by directly accessing the term - let content = terminal.update(cx, |terminal, _cx| { - let term = terminal.term.lock_unfair(); - Terminal::make_content(&term, &terminal.last_content) - }); - - let cells = &content.cells; - - // Check that we have "world" at the beginning of the line - let mut text = String::new(); - for cell in cells.iter().take(5) { - if cell.point.line.0 == 0 { - text.push(cell.c); - } - } - - assert!( - text.starts_with("world"), - "Bare CR should allow overwriting: got '{}'", - text - ); - } -} diff --git a/crates/terminal/src/terminal_hyperlinks.rs b/crates/terminal/src/terminal_hyperlinks.rs deleted file mode 100644 index 71a1634076..0000000000 --- a/crates/terminal/src/terminal_hyperlinks.rs +++ /dev/null @@ -1,1673 +0,0 @@ -use alacritty_terminal::{ - Term, - event::EventListener, - grid::Dimensions, - index::{Boundary, Column, Direction as AlacDirection, Point as AlacPoint}, - term::{ - cell::Flags, - search::{Match, RegexIter, RegexSearch}, - }, -}; -use log::{info, warn}; -use regex::Regex; -use std::{ - ops::{Index, Range}, - time::{Duration, Instant}, -}; - -const URL_REGEX: &str = r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`']+"#; -const WIDE_CHAR_SPACERS: Flags = - Flags::from_bits(Flags::LEADING_WIDE_CHAR_SPACER.bits() | Flags::WIDE_CHAR_SPACER.bits()) - .unwrap(); - -pub(super) struct RegexSearches { - url_regex: RegexSearch, - path_hyperlink_regexes: Vec, - path_hyperlink_timeout: Duration, -} - -impl Default for RegexSearches { - fn default() -> Self { - Self { - url_regex: RegexSearch::new(URL_REGEX).unwrap(), - path_hyperlink_regexes: Vec::default(), - path_hyperlink_timeout: Duration::default(), - } - } -} -impl RegexSearches { - pub(super) fn new( - path_hyperlink_regexes: impl IntoIterator>, - path_hyperlink_timeout_ms: u64, - ) -> Self { - Self { - url_regex: RegexSearch::new(URL_REGEX).unwrap(), - path_hyperlink_regexes: path_hyperlink_regexes - .into_iter() - .filter_map(|regex| { - Regex::new(regex.as_ref()) - .inspect_err(|error| { - warn!( - concat!( - "Ignoring path hyperlink regex specified in ", - "`terminal.path_hyperlink_regexes`:\n\n\t{}\n\nError: {}", - ), - regex.as_ref(), - error - ); - }) - .ok() - }) - .collect(), - path_hyperlink_timeout: Duration::from_millis(path_hyperlink_timeout_ms), - } - } -} - -pub(super) fn find_from_grid_point( - term: &Term, - point: AlacPoint, - regex_searches: &mut RegexSearches, -) -> Option<(String, bool, Match)> { - let grid = term.grid(); - let link = grid.index(point).hyperlink(); - let found_word = if let Some(ref url) = link { - let mut min_index = point; - loop { - let new_min_index = min_index.sub(term, Boundary::Cursor, 1); - if new_min_index == min_index || grid.index(new_min_index).hyperlink() != link { - break; - } else { - min_index = new_min_index - } - } - - let mut max_index = point; - loop { - let new_max_index = max_index.add(term, Boundary::Cursor, 1); - if new_max_index == max_index || grid.index(new_max_index).hyperlink() != link { - break; - } else { - max_index = new_max_index - } - } - - let url = url.uri().to_owned(); - let url_match = min_index..=max_index; - - Some((url, true, url_match)) - } else { - let (line_start, line_end) = (term.line_search_left(point), term.line_search_right(point)); - if let Some((url, url_match)) = RegexIter::new( - line_start, - line_end, - AlacDirection::Right, - term, - &mut regex_searches.url_regex, - ) - .find(|rm| rm.contains(&point)) - .map(|url_match| { - let url = term.bounds_to_string(*url_match.start(), *url_match.end()); - sanitize_url_punctuation(url, url_match, term) - }) { - Some((url, true, url_match)) - } else { - path_match( - &term, - line_start, - line_end, - point, - &mut regex_searches.path_hyperlink_regexes, - regex_searches.path_hyperlink_timeout, - ) - .map(|(path, path_match)| (path, false, path_match)) - } - }; - - found_word.map(|(maybe_url_or_path, is_url, word_match)| { - if is_url { - // Treat "file://" IRIs like file paths to ensure - // that line numbers at the end of the path are - // handled correctly - if let Some(path) = maybe_url_or_path.strip_prefix("file://") { - (path.to_string(), false, word_match) - } else { - (maybe_url_or_path, true, word_match) - } - } else { - (maybe_url_or_path, false, word_match) - } - }) -} - -fn sanitize_url_punctuation( - url: String, - url_match: Match, - term: &Term, -) -> (String, Match) { - let mut sanitized_url = url; - let mut chars_trimmed = 0; - - // First, handle parentheses balancing using single traversal - let (open_parens, close_parens) = - sanitized_url - .chars() - .fold((0, 0), |(opens, closes), c| match c { - '(' => (opens + 1, closes), - ')' => (opens, closes + 1), - _ => (opens, closes), - }); - - // Trim unbalanced closing parentheses - if close_parens > open_parens { - let mut remaining_close = close_parens; - while sanitized_url.ends_with(')') && remaining_close > open_parens { - sanitized_url.pop(); - chars_trimmed += 1; - remaining_close -= 1; - } - } - - // Handle trailing periods - if sanitized_url.ends_with('.') { - let trailing_periods = sanitized_url - .chars() - .rev() - .take_while(|&c| c == '.') - .count(); - - if trailing_periods > 1 { - sanitized_url.truncate(sanitized_url.len() - trailing_periods); - chars_trimmed += trailing_periods; - } else if trailing_periods == 1 - && let Some(second_last_char) = sanitized_url.chars().rev().nth(1) - && (second_last_char.is_alphanumeric() || second_last_char == '/') - { - sanitized_url.pop(); - chars_trimmed += 1; - } - } - - if chars_trimmed > 0 { - let new_end = url_match.end().sub(term, Boundary::Grid, chars_trimmed); - let sanitized_match = Match::new(*url_match.start(), new_end); - (sanitized_url, sanitized_match) - } else { - (sanitized_url, url_match) - } -} - -fn path_match( - term: &Term, - line_start: AlacPoint, - line_end: AlacPoint, - hovered: AlacPoint, - path_hyperlink_regexes: &mut Vec, - path_hyperlink_timeout: Duration, -) -> Option<(String, Match)> { - if path_hyperlink_regexes.is_empty() || path_hyperlink_timeout.as_millis() == 0 { - return None; - } - debug_assert!(line_start <= hovered); - debug_assert!(line_end >= hovered); - let search_start_time = Instant::now(); - - let timed_out = || { - let elapsed_time = Instant::now().saturating_duration_since(search_start_time); - (elapsed_time > path_hyperlink_timeout) - .then_some((elapsed_time.as_millis(), path_hyperlink_timeout.as_millis())) - }; - - // This used to be: `let line = term.bounds_to_string(line_start, line_end)`, however, that - // api compresses tab characters into a single space, whereas we require a cell accurate - // string representation of the line. The below algorithm does this, but seems a bit odd. - // Maybe there is a clean api for doing this, but I couldn't find it. - let mut line = String::with_capacity( - (line_end.line.0 - line_start.line.0 + 1) as usize * term.grid().columns(), - ); - let first_cell = &term.grid()[line_start]; - line.push(first_cell.c); - let mut start_offset = 0; - let mut hovered_point_byte_offset = None; - - if !first_cell.flags.intersects(WIDE_CHAR_SPACERS) { - start_offset += first_cell.c.len_utf8(); - if line_start == hovered { - hovered_point_byte_offset = Some(0); - } - } - - for cell in term.grid().iter_from(line_start) { - if cell.point > line_end { - break; - } - let is_spacer = cell.flags.intersects(WIDE_CHAR_SPACERS); - if cell.point == hovered { - debug_assert!(hovered_point_byte_offset.is_none()); - if start_offset > 0 && cell.flags.contains(Flags::WIDE_CHAR_SPACER) { - // If we hovered on a trailing spacer, back up to the end of the previous char's bytes. - start_offset -= 1; - } - hovered_point_byte_offset = Some(start_offset); - } else if cell.point < hovered && !is_spacer { - start_offset += cell.c.len_utf8(); - } - - if !is_spacer { - line.push(match cell.c { - '\t' => ' ', - c @ _ => c, - }); - } - } - let line = line.trim_ascii_end(); - let hovered_point_byte_offset = hovered_point_byte_offset?; - let found_from_range = |path_range: Range, - link_range: Range, - position: Option<(u32, Option)>| { - let advance_point_by_str = |mut point: AlacPoint, s: &str| { - for _ in s.chars() { - point = term - .expand_wide(point, AlacDirection::Right) - .add(term, Boundary::Grid, 1); - } - - // There does not appear to be an alacritty api that is - // "move to start of current wide char", so we have to do it ourselves. - let flags = term.grid().index(point).flags; - if flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) { - AlacPoint::new(point.line + 1, Column(0)) - } else if flags.contains(Flags::WIDE_CHAR_SPACER) { - AlacPoint::new(point.line, point.column - 1) - } else { - point - } - }; - - let link_start = advance_point_by_str(line_start, &line[..link_range.start]); - let link_end = advance_point_by_str(link_start, &line[link_range]); - let link_match = link_start - ..=term - .expand_wide(link_end, AlacDirection::Left) - .sub(term, Boundary::Grid, 1); - - ( - { - let mut path = line[path_range].to_string(); - position.inspect(|(line, column)| { - path += &format!(":{line}"); - column.inspect(|column| path += &format!(":{column}")); - }); - path - }, - link_match, - ) - }; - - for regex in path_hyperlink_regexes { - let mut path_found = false; - - for captures in regex.captures_iter(&line) { - path_found = true; - let match_range = captures.get(0).unwrap().range(); - let (path_range, line_column) = if let Some(path) = captures.name("path") { - let parse = |name: &str| { - captures - .name(name) - .and_then(|capture| capture.as_str().parse().ok()) - }; - - ( - path.range(), - parse("line").map(|line| (line, parse("column"))), - ) - } else { - (match_range.clone(), None) - }; - let link_range = captures - .name("link") - .map_or_else(|| match_range.clone(), |link| link.range()); - - if !link_range.contains(&hovered_point_byte_offset) { - // No match, just skip. - continue; - } - let found = found_from_range(path_range, link_range, line_column); - - if found.1.contains(&hovered) { - return Some(found); - } - } - - if path_found { - return None; - } - - if let Some((timed_out_ms, timeout_ms)) = timed_out() { - warn!("Timed out processing path hyperlink regexes after {timed_out_ms}ms"); - info!("{timeout_ms}ms time out specified in `terminal.path_hyperlink_timeout_ms`"); - return None; - } - } - - None -} - -#[cfg(test)] -mod tests { - use crate::terminal_settings::TerminalSettings; - - use super::*; - use alacritty_terminal::{ - event::VoidListener, - grid::Dimensions, - index::{Boundary, Column, Line, Point as AlacPoint}, - term::{Config, cell::Flags, test::TermSize}, - vte::ansi::Handler, - }; - use regex::Regex; - use settings::{self, Settings, SettingsContent}; - use std::{cell::RefCell, ops::RangeInclusive, path::PathBuf, rc::Rc}; - use url::Url; - use util::paths::PathWithPosition; - - fn re_test(re: &str, hay: &str, expected: Vec<&str>) { - let results: Vec<_> = Regex::new(re) - .unwrap() - .find_iter(hay) - .map(|m| m.as_str()) - .collect(); - assert_eq!(results, expected); - } - - #[test] - fn test_url_regex() { - re_test( - URL_REGEX, - "test http://example.com test 'https://website1.com' test mailto:bob@example.com train", - vec![ - "http://example.com", - "https://website1.com", - "mailto:bob@example.com", - ], - ); - } - - #[test] - fn test_url_parentheses_sanitization() { - // Test our sanitize_url_parentheses function directly - let test_cases = vec![ - // Cases that should be sanitized (unbalanced parentheses) - ("https://www.google.com/)", "https://www.google.com/"), - ("https://example.com/path)", "https://example.com/path"), - ("https://test.com/))", "https://test.com/"), - // Cases that should NOT be sanitized (balanced parentheses) - ( - "https://en.wikipedia.org/wiki/Example_(disambiguation)", - "https://en.wikipedia.org/wiki/Example_(disambiguation)", - ), - ("https://test.com/(hello)", "https://test.com/(hello)"), - ( - "https://example.com/path(1)(2)", - "https://example.com/path(1)(2)", - ), - // Edge cases - ("https://test.com/", "https://test.com/"), - ("https://example.com", "https://example.com"), - ]; - - for (input, expected) in test_cases { - // Create a minimal terminal for testing - let term = Term::new(Config::default(), &TermSize::new(80, 24), VoidListener); - - // Create a dummy match that spans the entire input - let start_point = AlacPoint::new(Line(0), Column(0)); - let end_point = AlacPoint::new(Line(0), Column(input.len())); - let dummy_match = Match::new(start_point, end_point); - - let (result, _) = sanitize_url_punctuation(input.to_string(), dummy_match, &term); - assert_eq!(result, expected, "Failed for input: {}", input); - } - } - - #[test] - fn test_url_periods_sanitization() { - // Test URLs with trailing periods (sentence punctuation) - let test_cases = vec![ - // Cases that should be sanitized (trailing periods likely punctuation) - ("https://example.com.", "https://example.com"), - ( - "https://github.com/zed-industries/zed.", - "https://github.com/zed-industries/zed", - ), - ( - "https://example.com/path/file.html.", - "https://example.com/path/file.html", - ), - ( - "https://example.com/file.pdf.", - "https://example.com/file.pdf", - ), - ("https://example.com:8080.", "https://example.com:8080"), - ("https://example.com..", "https://example.com"), - ( - "https://en.wikipedia.org/wiki/C.E.O.", - "https://en.wikipedia.org/wiki/C.E.O", - ), - // Cases that should NOT be sanitized (periods are part of URL structure) - ( - "https://example.com/v1.0/api", - "https://example.com/v1.0/api", - ), - ("https://192.168.1.1", "https://192.168.1.1"), - ("https://sub.domain.com", "https://sub.domain.com"), - ]; - - for (input, expected) in test_cases { - // Create a minimal terminal for testing - let term = Term::new(Config::default(), &TermSize::new(80, 24), VoidListener); - - // Create a dummy match that spans the entire input - let start_point = AlacPoint::new(Line(0), Column(0)); - let end_point = AlacPoint::new(Line(0), Column(input.len())); - let dummy_match = Match::new(start_point, end_point); - - // This test should initially fail since we haven't implemented period sanitization yet - let (result, _) = sanitize_url_punctuation(input.to_string(), dummy_match, &term); - assert_eq!(result, expected, "Failed for input: {}", input); - } - } - - macro_rules! test_hyperlink { - ($($lines:expr),+; $hyperlink_kind:ident) => { { - use crate::terminal_hyperlinks::tests::line_cells_count; - use std::cmp; - - let test_lines = vec![$($lines),+]; - let (total_cells, longest_line_cells) = - test_lines.iter().copied() - .map(line_cells_count) - .fold((0, 0), |state, cells| (state.0 + cells, cmp::max(state.1, cells))); - let contains_tab_char = test_lines.iter().copied() - .map(str::chars).flatten().find(|&c| c == '\t'); - let columns = if contains_tab_char.is_some() { - // This avoids tabs at end of lines causing whitespace-eating line wraps... - vec![longest_line_cells + 1] - } else { - // Alacritty has issues with 2 columns, use 3 as the minimum for now. - vec![3, longest_line_cells / 2, longest_line_cells + 1] - }; - test_hyperlink!( - columns; - total_cells; - test_lines.iter().copied(); - $hyperlink_kind - ) - } }; - - ($columns:expr; $total_cells:expr; $lines:expr; $hyperlink_kind:ident) => { { - use crate::terminal_hyperlinks::tests::{ test_hyperlink, HyperlinkKind }; - - let source_location = format!("{}:{}", std::file!(), std::line!()); - for columns in $columns { - test_hyperlink(columns, $total_cells, $lines, HyperlinkKind::$hyperlink_kind, - &source_location); - } - } }; - } - - mod path { - /// 👉 := **hovered** on following char - /// - /// 👈 := **hovered** on wide char spacer of previous full width char - /// - /// **`‹›`** := expected **hyperlink** match - /// - /// **`«»`** := expected **path**, **row**, and **column** capture groups - /// - /// [**`c₀, c₁, …, cₙ;`**]ₒₚₜ := use specified terminal widths of `c₀, c₁, …, cₙ` **columns** - /// (defaults to `3, longest_line_cells / 2, longest_line_cells + 1;`) - /// - macro_rules! test_path { - ($($lines:literal),+) => { test_hyperlink!($($lines),+; Path) }; - } - - #[test] - fn simple() { - // Rust paths - // Just the path - test_path!("‹«/👉test/cool.rs»›"); - test_path!("‹«/test/cool👉.rs»›"); - - // path and line - test_path!("‹«/👉test/cool.rs»:«4»›"); - test_path!("‹«/test/cool.rs»👉:«4»›"); - test_path!("‹«/test/cool.rs»:«👉4»›"); - test_path!("‹«/👉test/cool.rs»(«4»)›"); - test_path!("‹«/test/cool.rs»👉(«4»)›"); - test_path!("‹«/test/cool.rs»(«👉4»)›"); - test_path!("‹«/test/cool.rs»(«4»👉)›"); - - // path, line, and column - test_path!("‹«/👉test/cool.rs»:«4»:«2»›"); - test_path!("‹«/test/cool.rs»:«4»:«👉2»›"); - test_path!("‹«/👉test/cool.rs»(«4»,«2»)›"); - test_path!("‹«/test/cool.rs»(«4»👉,«2»)›"); - - // path, line, column, and ':' suffix - test_path!("‹«/👉test/cool.rs»:«4»:«2»›:"); - test_path!("‹«/test/cool.rs»:«4»:«👉2»›:"); - test_path!("‹«/👉test/cool.rs»(«4»,«2»)›:"); - test_path!("‹«/test/cool.rs»(«4»,«2»👉)›:"); - test_path!("‹«/👉test/cool.rs»:(«4»,«2»)›:"); - test_path!("‹«/test/cool.rs»:(«4»,«2»👉)›:"); - test_path!("‹«/👉test/cool.rs»:(«4»:«2»)›:"); - test_path!("‹«/test/cool.rs»:(«4»:«2»👉)›:"); - test_path!("/test/cool.rs:4:2👉:", "What is this?"); - test_path!("/test/cool.rs(4,2)👉:", "What is this?"); - - // path, line, column, and description - test_path!("‹«/test/co👉ol.rs»:«4»:«2»›:Error!"); - test_path!("‹«/test/co👉ol.rs»(«4»,«2»)›:Error!"); - - // Cargo output - test_path!(" Compiling Cool 👉(/test/Cool)"); - test_path!(" Compiling Cool (‹«/👉test/Cool»›)"); - test_path!(" Compiling Cool (/test/Cool👉)"); - - // Python - test_path!("‹«awe👉some.py»›"); - test_path!("‹«👉a»› "); - - test_path!(" ‹F👉ile \"«/awesome.py»\", line «42»›: Wat?"); - test_path!(" ‹File \"«/awe👉some.py»\", line «42»›"); - test_path!(" ‹File \"«/awesome.py»👉\", line «42»›: Wat?"); - test_path!(" ‹File \"«/awesome.py»\", line «4👉2»›"); - } - - #[test] - fn simple_with_descriptions() { - // path, line, column and description - test_path!("‹«/👉test/cool.rs»:«4»:«2»›:例Desc例例例"); - test_path!("‹«/test/cool.rs»:«4»:«👉2»›:例Desc例例例"); - test_path!("‹«/👉test/cool.rs»(«4»,«2»)›:例Desc例例例"); - test_path!("‹«/test/cool.rs»(«4»👉,«2»)›:例Desc例例例"); - - // path, line, column and description w/extra colons - test_path!("‹«/👉test/cool.rs»:«4»:«2»›::例Desc例例例"); - test_path!("‹«/test/cool.rs»:«4»:«👉2»›::例Desc例例例"); - test_path!("‹«/👉test/cool.rs»(«4»,«2»)›::例Desc例例例"); - test_path!("‹«/test/cool.rs»(«4»,«2»👉)›::例Desc例例例"); - } - - #[test] - fn multiple_same_line() { - test_path!("‹«/👉test/cool.rs»› /test/cool.rs"); - test_path!("/test/cool.rs ‹«/👉test/cool.rs»›"); - - test_path!( - "‹«🦀 multiple_👉same_line 🦀» 🚣«4» 🏛️«2»›: 🦀 multiple_same_line 🦀 🚣4 🏛️2:" - ); - test_path!( - "🦀 multiple_same_line 🦀 🚣4 🏛️2 ‹«🦀 multiple_👉same_line 🦀» 🚣«4» 🏛️«2»›:" - ); - - // ls output (tab separated) - test_path!( - "‹«Carg👉o.toml»›\t\texperiments\t\tnotebooks\t\trust-toolchain.toml\ttooling" - ); - test_path!( - "Cargo.toml\t\t‹«exper👉iments»›\t\tnotebooks\t\trust-toolchain.toml\ttooling" - ); - test_path!( - "Cargo.toml\t\texperiments\t\t‹«note👉books»›\t\trust-toolchain.toml\ttooling" - ); - test_path!( - "Cargo.toml\t\texperiments\t\tnotebooks\t\t‹«rust-t👉oolchain.toml»›\ttooling" - ); - test_path!( - "Cargo.toml\t\texperiments\t\tnotebooks\t\trust-toolchain.toml\t‹«too👉ling»›" - ); - } - - #[test] - fn colons_galore() { - test_path!("‹«/test/co👉ol.rs»:«4»›"); - test_path!("‹«/test/co👉ol.rs»:«4»›:"); - test_path!("‹«/test/co👉ol.rs»:«4»:«2»›"); - test_path!("‹«/test/co👉ol.rs»:«4»:«2»›:"); - test_path!("‹«/test/co👉ol.rs»(«1»)›"); - test_path!("‹«/test/co👉ol.rs»(«1»)›:"); - test_path!("‹«/test/co👉ol.rs»(«1»,«618»)›"); - test_path!("‹«/test/co👉ol.rs»(«1»,«618»)›:"); - test_path!("‹«/test/co👉ol.rs»::«42»›"); - test_path!("‹«/test/co👉ol.rs»::«42»›:"); - test_path!("‹«/test/co👉ol.rs»(«1»,«618»)›::"); - } - - #[test] - fn quotes_and_brackets() { - test_path!("\"‹«/test/co👉ol.rs»:«4»›\""); - test_path!("'‹«/test/co👉ol.rs»:«4»›'"); - test_path!("`‹«/test/co👉ol.rs»:«4»›`"); - - test_path!("[‹«/test/co👉ol.rs»:«4»›]"); - test_path!("(‹«/test/co👉ol.rs»:«4»›)"); - test_path!("{‹«/test/co👉ol.rs»:«4»›}"); - test_path!("<‹«/test/co👉ol.rs»:«4»›>"); - - test_path!("[\"‹«/test/co👉ol.rs»:«4»›\"]"); - test_path!("'(‹«/test/co👉ol.rs»:«4»›)'"); - - test_path!("\"‹«/test/co👉ol.rs»:«4»:«2»›\""); - test_path!("'‹«/test/co👉ol.rs»:«4»:«2»›'"); - test_path!("`‹«/test/co👉ol.rs»:«4»:«2»›`"); - - test_path!("[‹«/test/co👉ol.rs»:«4»:«2»›]"); - test_path!("(‹«/test/co👉ol.rs»:«4»:«2»›)"); - test_path!("{‹«/test/co👉ol.rs»:«4»:«2»›}"); - test_path!("<‹«/test/co👉ol.rs»:«4»:«2»›>"); - - test_path!("[\"‹«/test/co👉ol.rs»:«4»:«2»›\"]"); - - test_path!("\"‹«/test/co👉ol.rs»(«4»)›\""); - test_path!("'‹«/test/co👉ol.rs»(«4»)›'"); - test_path!("`‹«/test/co👉ol.rs»(«4»)›`"); - - test_path!("[‹«/test/co👉ol.rs»(«4»)›]"); - test_path!("(‹«/test/co👉ol.rs»(«4»)›)"); - test_path!("{‹«/test/co👉ol.rs»(«4»)›}"); - test_path!("<‹«/test/co👉ol.rs»(«4»)›>"); - - test_path!("[\"‹«/test/co👉ol.rs»(«4»)›\"]"); - - test_path!("\"‹«/test/co👉ol.rs»(«4»,«2»)›\""); - test_path!("'‹«/test/co👉ol.rs»(«4»,«2»)›'"); - test_path!("`‹«/test/co👉ol.rs»(«4»,«2»)›`"); - - test_path!("[‹«/test/co👉ol.rs»(«4»,«2»)›]"); - test_path!("(‹«/test/co👉ol.rs»(«4»,«2»)›)"); - test_path!("{‹«/test/co👉ol.rs»(«4»,«2»)›}"); - test_path!("<‹«/test/co👉ol.rs»(«4»,«2»)›>"); - - test_path!("[\"‹«/test/co👉ol.rs»(«4»,«2»)›\"]"); - - // Imbalanced - test_path!("([‹«/test/co👉ol.rs»:«4»›] was here...)"); - test_path!("[Here's <‹«/test/co👉ol.rs»:«4»›>]"); - test_path!("('‹«/test/co👉ol.rs»:«4»›' was here...)"); - test_path!("[Here's `‹«/test/co👉ol.rs»:«4»›`]"); - } - - #[test] - fn trailing_punctuation() { - test_path!("‹«/test/co👉ol.rs»›:,.."); - test_path!("/test/cool.rs:,👉.."); - test_path!("‹«/test/co👉ol.rs»:«4»›:,"); - test_path!("/test/cool.rs:4:👉,"); - test_path!("[\"‹«/test/co👉ol.rs»:«4»›\"]:,"); - test_path!("'(‹«/test/co👉ol.rs»:«4»›),,'..."); - test_path!("('‹«/test/co👉ol.rs»:«4»›'::: was here...)"); - test_path!("[Here's <‹«/test/co👉ol.rs»:«4»›>]::: "); - } - - #[test] - fn word_wide_chars() { - // Rust paths - test_path!("‹«/👉例/cool.rs»›"); - test_path!("‹«/例👈/cool.rs»›"); - test_path!("‹«/例/cool.rs»:«👉4»›"); - test_path!("‹«/例/cool.rs»:«4»:«👉2»›"); - - // Cargo output - test_path!(" Compiling Cool (‹«/👉例/Cool»›)"); - test_path!(" Compiling Cool (‹«/例👈/Cool»›)"); - - test_path!(" Compiling Cool (‹«/👉例/Cool Spaces»›)"); - test_path!(" Compiling Cool (‹«/例👈/Cool Spaces»›)"); - test_path!(" Compiling Cool (‹«/👉例/Cool Spaces»:«4»:«2»›)"); - test_path!(" Compiling Cool (‹«/例👈/Cool Spaces»(«4»,«2»)›)"); - - test_path!(" --> ‹«/👉例/Cool Spaces»›"); - test_path!(" ::: ‹«/例👈/Cool Spaces»›"); - test_path!(" --> ‹«/👉例/Cool Spaces»:«4»:«2»›"); - test_path!(" ::: ‹«/例👈/Cool Spaces»(«4»,«2»)›"); - test_path!(" panicked at ‹«/👉例/Cool Spaces»:«4»:«2»›:"); - test_path!(" panicked at ‹«/例👈/Cool Spaces»(«4»,«2»)›:"); - test_path!(" at ‹«/👉例/Cool Spaces»:«4»:«2»›"); - test_path!(" at ‹«/例👈/Cool Spaces»(«4»,«2»)›"); - - // Python - test_path!("‹«👉例wesome.py»›"); - test_path!("‹«例👈wesome.py»›"); - test_path!(" ‹File \"«/👉例wesome.py»\", line «42»›: Wat?"); - test_path!(" ‹File \"«/例👈wesome.py»\", line «42»›: Wat?"); - } - - #[test] - fn non_word_wide_chars() { - // Mojo diagnostic message - test_path!(" ‹File \"«/awe👉some.🔥»\", line «42»›: Wat?"); - test_path!(" ‹File \"«/awesome👉.🔥»\", line «42»›: Wat?"); - test_path!(" ‹File \"«/awesome.👉🔥»\", line «42»›: Wat?"); - test_path!(" ‹File \"«/awesome.🔥👈»\", line «42»›: Wat?"); - } - - /// These likely rise to the level of being worth fixing. - mod issues { - #[test] - // - fn issue_alacritty_8586() { - // Rust paths - test_path!("‹«/👉例/cool.rs»›"); - test_path!("‹«/例👈/cool.rs»›"); - test_path!("‹«/例/cool.rs»:«👉4»›"); - test_path!("‹«/例/cool.rs»:«4»:«👉2»›"); - - // Cargo output - test_path!(" Compiling Cool (‹«/👉例/Cool»›)"); - test_path!(" Compiling Cool (‹«/例👈/Cool»›)"); - - // Python - test_path!("‹«👉例wesome.py»›"); - test_path!("‹«例👈wesome.py»›"); - test_path!(" ‹File \"«/👉例wesome.py»\", line «42»›: Wat?"); - test_path!(" ‹File \"«/例👈wesome.py»\", line «42»›: Wat?"); - } - - #[test] - // - fn issue_12338_regex() { - // Issue #12338 - test_path!(".rw-r--r-- 0 staff 05-27 14:03 ‹«'test file 👉1.txt'»›"); - test_path!(".rw-r--r-- 0 staff 05-27 14:03 ‹«👉'test file 1.txt'»›"); - } - - #[test] - // - fn issue_12338() { - // Issue #12338 - test_path!(".rw-r--r-- 0 staff 05-27 14:03 ‹«test👉、2.txt»›"); - test_path!(".rw-r--r-- 0 staff 05-27 14:03 ‹«test、👈2.txt»›"); - test_path!(".rw-r--r-- 0 staff 05-27 14:03 ‹«test👉。3.txt»›"); - test_path!(".rw-r--r-- 0 staff 05-27 14:03 ‹«test。👈3.txt»›"); - - // Rust paths - test_path!("‹«/👉🏃/🦀.rs»›"); - test_path!("‹«/🏃👈/🦀.rs»›"); - test_path!("‹«/🏃/👉🦀.rs»:«4»›"); - test_path!("‹«/🏃/🦀👈.rs»:«4»:«2»›"); - - // Cargo output - test_path!(" Compiling Cool (‹«/👉🏃/Cool»›)"); - test_path!(" Compiling Cool (‹«/🏃👈/Cool»›)"); - - // Python - test_path!("‹«👉🏃wesome.py»›"); - test_path!("‹«🏃👈wesome.py»›"); - test_path!(" ‹File \"«/👉🏃wesome.py»\", line «42»›: Wat?"); - test_path!(" ‹File \"«/🏃👈wesome.py»\", line «42»›: Wat?"); - - // Mojo - test_path!("‹«/awe👉some.🔥»› is some good Mojo!"); - test_path!("‹«/awesome👉.🔥»› is some good Mojo!"); - test_path!("‹«/awesome.👉🔥»› is some good Mojo!"); - test_path!("‹«/awesome.🔥👈»› is some good Mojo!"); - test_path!(" ‹File \"«/👉🏃wesome.🔥»\", line «42»›: Wat?"); - test_path!(" ‹File \"«/🏃👈wesome.🔥»\", line «42»›: Wat?"); - } - - #[test] - // - fn issue_40202() { - // Elixir - test_path!("[‹«lib/blitz_apex_👉server/stats/aggregate_rank_stats.ex»:«35»›: BlitzApexServer.Stats.AggregateRankStats.update/2] - 1 #=> 1"); - } - - #[test] - // - fn issue_28194() { - test_path!( - "‹«test/c👉ontrollers/template_items_controller_test.rb»:«20»›:in 'block (2 levels) in '" - ); - } - - #[test] - #[cfg_attr( - not(target_os = "windows"), - should_panic( - expected = "Path = «/test/cool.rs:4:NotDesc», at grid cells (0, 1)..=(7, 2)" - ) - )] - #[cfg_attr( - target_os = "windows", - should_panic( - expected = r#"Path = «C:\\test\\cool.rs:4:NotDesc», at grid cells (0, 1)..=(8, 1)"# - ) - )] - // PathWithPosition::parse_str considers "/test/co👉ol.rs:4:NotDesc" invalid input, but - // still succeeds and truncates the part after the position. Ideally this would be - // parsed as the path "/test/co👉ol.rs:4:NotDesc" with no position. - fn path_with_position_parse_str() { - test_path!("`‹«/test/co👉ol.rs:4:NotDesc»›`"); - test_path!("<‹«/test/co👉ol.rs:4:NotDesc»›>"); - - test_path!("'‹«(/test/co👉ol.rs:4:2)»›'"); - test_path!("'‹«(/test/co👉ol.rs(4))»›'"); - test_path!("'‹«(/test/co👉ol.rs(4,2))»›'"); - } - } - - /// Minor issues arguably not important enough to fix/workaround... - mod nits { - #[test] - fn alacritty_bugs_with_two_columns() { - test_path!("‹«/👉test/cool.rs»(«4»)›"); - test_path!("‹«/test/cool.rs»(«👉4»)›"); - test_path!("‹«/test/cool.rs»(«4»,«👉2»)›"); - - // Python - test_path!("‹«awe👉some.py»›"); - } - - #[test] - #[cfg_attr( - not(target_os = "windows"), - should_panic( - expected = "Path = «/test/cool.rs», line = 1, at grid cells (0, 0)..=(9, 0)" - ) - )] - #[cfg_attr( - target_os = "windows", - should_panic( - expected = r#"Path = «C:\\test\\cool.rs», line = 1, at grid cells (0, 0)..=(9, 2)"# - ) - )] - fn invalid_row_column_should_be_part_of_path() { - test_path!("‹«/👉test/cool.rs:1:618033988749»›"); - test_path!("‹«/👉test/cool.rs(1,618033988749)»›"); - } - - #[test] - #[cfg_attr( - not(target_os = "windows"), - should_panic(expected = "Path = «/te:st/co:ol.r:s:4:2::::::»") - )] - #[cfg_attr( - target_os = "windows", - should_panic(expected = r#"Path = «C:\\te:st\\co:ol.r:s:4:2::::::»"#) - )] - fn many_trailing_colons_should_be_parsed_as_part_of_the_path() { - test_path!("‹«/te:st/👉co:ol.r:s:4:2::::::»›"); - test_path!("/test/cool.rs:::👉:"); - } - } - - mod windows { - // Lots of fun to be had with long file paths (verbatim) and UNC paths on Windows. - // See - // See - // See - - #[test] - fn default_prompts() { - // Windows command prompt - test_path!(r#"‹«C:\Users\someone\👉test»›>"#); - test_path!(r#"C:\Users\someone\test👉>"#); - - // Windows PowerShell - test_path!(r#"PS ‹«C:\Users\someone\👉test\cool.rs»›>"#); - test_path!(r#"PS C:\Users\someone\test\cool.rs👉>"#); - } - - #[test] - fn unc() { - test_path!(r#"‹«\\server\share\👉test\cool.rs»›"#); - test_path!(r#"‹«\\server\share\test\cool👉.rs»›"#); - } - - mod issues { - #[test] - fn issue_verbatim() { - test_path!(r#"‹«\\?\C:\👉test\cool.rs»›"#); - test_path!(r#"‹«\\?\C:\test\cool👉.rs»›"#); - } - - #[test] - fn issue_verbatim_unc() { - test_path!(r#"‹«\\?\UNC\server\share\👉test\cool.rs»›"#); - test_path!(r#"‹«\\?\UNC\server\share\test\cool👉.rs»›"#); - } - } - } - - mod perf { - use super::super::*; - use crate::TerminalSettings; - use alacritty_terminal::{ - event::VoidListener, - grid::Dimensions, - index::{Column, Point as AlacPoint}, - term::test::mock_term, - term::{Term, search::Match}, - }; - use settings::{self, Settings, SettingsContent}; - use std::{cell::RefCell, rc::Rc}; - use util_macros::perf; - - fn build_test_term(line: &str) -> (Term, AlacPoint) { - let content = line.repeat(500); - let term = mock_term(&content); - let point = AlacPoint::new( - term.grid().bottommost_line() - 1, - Column(term.grid().last_column().0 / 2), - ); - - (term, point) - } - - #[perf] - pub fn cargo_hyperlink_benchmark() { - const LINE: &str = " Compiling terminal v0.1.0 (/Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal)\r\n"; - thread_local! { - static TEST_TERM_AND_POINT: (Term, AlacPoint) = - build_test_term(LINE); - } - TEST_TERM_AND_POINT.with(|(term, point)| { - assert!( - find_from_grid_point_bench(term, *point).is_some(), - "Hyperlink should have been found" - ); - }); - } - - #[perf] - pub fn rust_hyperlink_benchmark() { - const LINE: &str = " --> /Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal/terminal.rs:1000:42\r\n"; - thread_local! { - static TEST_TERM_AND_POINT: (Term, AlacPoint) = - build_test_term(LINE); - } - TEST_TERM_AND_POINT.with(|(term, point)| { - assert!( - find_from_grid_point_bench(term, *point).is_some(), - "Hyperlink should have been found" - ); - }); - } - - #[perf] - pub fn ls_hyperlink_benchmark() { - const LINE: &str = "Cargo.toml experiments notebooks rust-toolchain.toml tooling\r\n"; - thread_local! { - static TEST_TERM_AND_POINT: (Term, AlacPoint) = - build_test_term(LINE); - } - TEST_TERM_AND_POINT.with(|(term, point)| { - assert!( - find_from_grid_point_bench(term, *point).is_some(), - "Hyperlink should have been found" - ); - }); - } - - pub fn find_from_grid_point_bench( - term: &Term, - point: AlacPoint, - ) -> Option<(String, bool, Match)> { - const PATH_HYPERLINK_TIMEOUT_MS: u64 = 1000; - - thread_local! { - static TEST_REGEX_SEARCHES: RefCell = - RefCell::new({ - let default_settings_content: Rc = - settings::parse_json_with_comments(&settings::default_settings()) - .unwrap(); - let default_terminal_settings = - TerminalSettings::from_settings(&default_settings_content); - - RegexSearches::new( - &default_terminal_settings.path_hyperlink_regexes, - PATH_HYPERLINK_TIMEOUT_MS - ) - }); - } - - TEST_REGEX_SEARCHES.with(|regex_searches| { - find_from_grid_point(&term, point, &mut regex_searches.borrow_mut()) - }) - } - } - } - - mod file_iri { - // File IRIs have a ton of use cases, most of which we currently do not support. A few of - // those cases are documented here as tests which are expected to fail. - // See https://en.wikipedia.org/wiki/File_URI_scheme - - /// [**`c₀, c₁, …, cₙ;`**]ₒₚₜ := use specified terminal widths of `c₀, c₁, …, cₙ` **columns** - /// (defaults to `3, longest_line_cells / 2, longest_line_cells + 1;`) - /// - macro_rules! test_file_iri { - ($file_iri:literal) => { { test_hyperlink!(concat!("‹«👉", $file_iri, "»›"); FileIri) } }; - } - - #[cfg(not(target_os = "windows"))] - #[test] - fn absolute_file_iri() { - test_file_iri!("file:///test/cool/index.rs"); - test_file_iri!("file:///test/cool/"); - } - - mod issues { - #[cfg(not(target_os = "windows"))] - #[test] - #[should_panic(expected = "Path = «/test/Ῥόδος/», at grid cells (0, 0)..=(15, 1)")] - fn issue_file_iri_with_percent_encoded_characters() { - // Non-space characters - // file:///test/Ῥόδος/ - test_file_iri!("file:///test/%E1%BF%AC%CF%8C%CE%B4%CE%BF%CF%82/"); // URI - - // Spaces - test_file_iri!("file:///te%20st/co%20ol/index.rs"); - test_file_iri!("file:///te%20st/co%20ol/"); - } - } - - #[cfg(target_os = "windows")] - mod windows { - mod issues { - // The test uses Url::to_file_path(), but it seems that the Url crate doesn't - // support relative file IRIs. - #[test] - #[should_panic( - expected = r#"Failed to interpret file IRI `file:/test/cool/index.rs` as a path"# - )] - fn issue_relative_file_iri() { - test_file_iri!("file:/test/cool/index.rs"); - test_file_iri!("file:/test/cool/"); - } - - // See https://en.wikipedia.org/wiki/File_URI_scheme - // https://github.com/zed-industries/zed/issues/39189 - #[test] - #[should_panic( - expected = r#"Path = «C:\\test\\cool\\index.rs», at grid cells (0, 0)..=(9, 1)"# - )] - fn issue_39189() { - test_file_iri!("file:///C:/test/cool/index.rs"); - test_file_iri!("file:///C:/test/cool/"); - } - - #[test] - #[should_panic( - expected = r#"Path = «C:\\test\\Ῥόδος\\», at grid cells (0, 0)..=(16, 1)"# - )] - fn issue_file_iri_with_percent_encoded_characters() { - // Non-space characters - // file:///test/Ῥόδος/ - test_file_iri!("file:///C:/test/%E1%BF%AC%CF%8C%CE%B4%CE%BF%CF%82/"); // URI - - // Spaces - test_file_iri!("file:///C:/te%20st/co%20ol/index.rs"); - test_file_iri!("file:///C:/te%20st/co%20ol/"); - } - } - } - } - - mod iri { - /// [**`c₀, c₁, …, cₙ;`**]ₒₚₜ := use specified terminal widths of `c₀, c₁, …, cₙ` **columns** - /// (defaults to `3, longest_line_cells / 2, longest_line_cells + 1;`) - /// - macro_rules! test_iri { - ($iri:literal) => { { test_hyperlink!(concat!("‹«👉", $iri, "»›"); Iri) } }; - } - - #[test] - fn simple() { - // In the order they appear in URL_REGEX, except 'file://' which is treated as a path - test_iri!("ipfs://test/cool.ipfs"); - test_iri!("ipns://test/cool.ipns"); - test_iri!("magnet://test/cool.git"); - test_iri!("mailto:someone@somewhere.here"); - test_iri!("gemini://somewhere.here"); - test_iri!("gopher://somewhere.here"); - test_iri!("http://test/cool/index.html"); - test_iri!("http://10.10.10.10:1111/cool.html"); - test_iri!("http://test/cool/index.html?amazing=1"); - test_iri!("http://test/cool/index.html#right%20here"); - test_iri!("http://test/cool/index.html?amazing=1#right%20here"); - test_iri!("https://test/cool/index.html"); - test_iri!("https://10.10.10.10:1111/cool.html"); - test_iri!("https://test/cool/index.html?amazing=1"); - test_iri!("https://test/cool/index.html#right%20here"); - test_iri!("https://test/cool/index.html?amazing=1#right%20here"); - test_iri!("news://test/cool.news"); - test_iri!("git://test/cool.git"); - test_iri!("ssh://user@somewhere.over.here:12345/test/cool.git"); - test_iri!("ftp://test/cool.ftp"); - } - - #[test] - fn wide_chars() { - // In the order they appear in URL_REGEX, except 'file://' which is treated as a path - test_iri!("ipfs://例🏃🦀/cool.ipfs"); - test_iri!("ipns://例🏃🦀/cool.ipns"); - test_iri!("magnet://例🏃🦀/cool.git"); - test_iri!("mailto:someone@somewhere.here"); - test_iri!("gemini://somewhere.here"); - test_iri!("gopher://somewhere.here"); - test_iri!("http://例🏃🦀/cool/index.html"); - test_iri!("http://10.10.10.10:1111/cool.html"); - test_iri!("http://例🏃🦀/cool/index.html?amazing=1"); - test_iri!("http://例🏃🦀/cool/index.html#right%20here"); - test_iri!("http://例🏃🦀/cool/index.html?amazing=1#right%20here"); - test_iri!("https://例🏃🦀/cool/index.html"); - test_iri!("https://10.10.10.10:1111/cool.html"); - test_iri!("https://例🏃🦀/cool/index.html?amazing=1"); - test_iri!("https://例🏃🦀/cool/index.html#right%20here"); - test_iri!("https://例🏃🦀/cool/index.html?amazing=1#right%20here"); - test_iri!("news://例🏃🦀/cool.news"); - test_iri!("git://例/cool.git"); - test_iri!("ssh://user@somewhere.over.here:12345/例🏃🦀/cool.git"); - test_iri!("ftp://例🏃🦀/cool.ftp"); - } - - // There are likely more tests needed for IRI vs URI - #[test] - fn iris() { - // These refer to the same location, see example here: - // - test_iri!("https://en.wiktionary.org/wiki/Ῥόδος"); // IRI - test_iri!("https://en.wiktionary.org/wiki/%E1%BF%AC%CF%8C%CE%B4%CE%BF%CF%82"); // URI - } - - #[test] - #[should_panic(expected = "Expected a path, but was a iri")] - fn file_is_a_path() { - test_iri!("file://test/cool/index.rs"); - } - } - - #[derive(Debug, PartialEq)] - enum HyperlinkKind { - FileIri, - Iri, - Path, - } - - struct ExpectedHyperlink { - hovered_grid_point: AlacPoint, - hovered_char: char, - hyperlink_kind: HyperlinkKind, - iri_or_path: String, - row: Option, - column: Option, - hyperlink_match: RangeInclusive, - } - - /// Converts to Windows style paths on Windows, like path!(), but at runtime for improved test - /// readability. - fn build_term_from_test_lines<'a>( - hyperlink_kind: HyperlinkKind, - term_size: TermSize, - test_lines: impl Iterator, - ) -> (Term, ExpectedHyperlink) { - #[derive(Default, Eq, PartialEq)] - enum HoveredState { - #[default] - HoveredScan, - HoveredNextChar, - Done, - } - - #[derive(Default, Eq, PartialEq)] - enum MatchState { - #[default] - MatchScan, - MatchNextChar, - Match(AlacPoint), - Done, - } - - #[derive(Default, Eq, PartialEq)] - enum CapturesState { - #[default] - PathScan, - PathNextChar, - Path(AlacPoint), - RowScan, - Row(String), - ColumnScan, - Column(String), - Done, - } - - fn prev_input_point_from_term(term: &Term) -> AlacPoint { - let grid = term.grid(); - let cursor = &grid.cursor; - let mut point = cursor.point; - - if !cursor.input_needs_wrap { - point = point.sub(term, Boundary::Grid, 1); - } - - if grid.index(point).flags.contains(Flags::WIDE_CHAR_SPACER) { - point.column -= 1; - } - - point - } - - fn end_point_from_prev_input_point( - term: &Term, - prev_input_point: AlacPoint, - ) -> AlacPoint { - if term - .grid() - .index(prev_input_point) - .flags - .contains(Flags::WIDE_CHAR) - { - prev_input_point.add(term, Boundary::Grid, 1) - } else { - prev_input_point - } - } - - fn process_input(term: &mut Term, c: char) { - match c { - '\t' => term.put_tab(1), - c @ _ => term.input(c), - } - } - - let mut hovered_grid_point: Option = None; - let mut hyperlink_match = AlacPoint::default()..=AlacPoint::default(); - let mut iri_or_path = String::default(); - let mut row = None; - let mut column = None; - let mut prev_input_point = AlacPoint::default(); - let mut hovered_state = HoveredState::default(); - let mut match_state = MatchState::default(); - let mut captures_state = CapturesState::default(); - let mut term = Term::new(Config::default(), &term_size, VoidListener); - - for text in test_lines { - let chars: Box> = - if cfg!(windows) && hyperlink_kind == HyperlinkKind::Path { - Box::new(text.chars().map(|c| if c == '/' { '\\' } else { c })) as _ - } else { - Box::new(text.chars()) as _ - }; - let mut chars = chars.peekable(); - while let Some(c) = chars.next() { - match c { - '👉' => { - hovered_state = HoveredState::HoveredNextChar; - } - '👈' => { - hovered_grid_point = Some(prev_input_point.add(&term, Boundary::Grid, 1)); - } - '«' | '»' => { - captures_state = match captures_state { - CapturesState::PathScan => CapturesState::PathNextChar, - CapturesState::PathNextChar => { - panic!("Should have been handled by char input") - } - CapturesState::Path(start_point) => { - iri_or_path = term.bounds_to_string( - start_point, - end_point_from_prev_input_point(&term, prev_input_point), - ); - CapturesState::RowScan - } - CapturesState::RowScan => CapturesState::Row(String::new()), - CapturesState::Row(number) => { - row = Some(number.parse::().unwrap()); - CapturesState::ColumnScan - } - CapturesState::ColumnScan => CapturesState::Column(String::new()), - CapturesState::Column(number) => { - column = Some(number.parse::().unwrap()); - CapturesState::Done - } - CapturesState::Done => { - panic!("Extra '«', '»'") - } - } - } - '‹' | '›' => { - match_state = match match_state { - MatchState::MatchScan => MatchState::MatchNextChar, - MatchState::MatchNextChar => { - panic!("Should have been handled by char input") - } - MatchState::Match(start_point) => { - hyperlink_match = start_point - ..=end_point_from_prev_input_point(&term, prev_input_point); - MatchState::Done - } - MatchState::Done => { - panic!("Extra '‹', '›'") - } - } - } - _ => { - if let CapturesState::Row(number) | CapturesState::Column(number) = - &mut captures_state - { - number.push(c) - } - - let is_windows_abs_path_start = captures_state - == CapturesState::PathNextChar - && cfg!(windows) - && hyperlink_kind == HyperlinkKind::Path - && c == '\\' - && chars.peek().is_some_and(|c| *c != '\\'); - - if is_windows_abs_path_start { - // Convert Unix abs path start into Windows abs path start so that the - // same test can be used for both OSes. - term.input('C'); - prev_input_point = prev_input_point_from_term(&term); - term.input(':'); - process_input(&mut term, c); - } else { - process_input(&mut term, c); - prev_input_point = prev_input_point_from_term(&term); - } - - if hovered_state == HoveredState::HoveredNextChar { - hovered_grid_point = Some(prev_input_point); - hovered_state = HoveredState::Done; - } - if captures_state == CapturesState::PathNextChar { - captures_state = CapturesState::Path(prev_input_point); - } - if match_state == MatchState::MatchNextChar { - match_state = MatchState::Match(prev_input_point); - } - } - } - } - term.move_down_and_cr(1); - } - - if hyperlink_kind == HyperlinkKind::FileIri { - let Ok(url) = Url::parse(&iri_or_path) else { - panic!("Failed to parse file IRI `{iri_or_path}`"); - }; - let Ok(path) = url.to_file_path() else { - panic!("Failed to interpret file IRI `{iri_or_path}` as a path"); - }; - iri_or_path = path.to_string_lossy().into_owned(); - } - - let hovered_grid_point = hovered_grid_point.expect("Missing hovered point (👉 or 👈)"); - let hovered_char = term.grid().index(hovered_grid_point).c; - ( - term, - ExpectedHyperlink { - hovered_grid_point, - hovered_char, - hyperlink_kind, - iri_or_path, - row, - column, - hyperlink_match, - }, - ) - } - - fn line_cells_count(line: &str) -> usize { - // This avoids taking a dependency on the unicode-width crate - fn width(c: char) -> usize { - match c { - // Fullwidth unicode characters used in tests - '例' | '🏃' | '🦀' | '🔥' => 2, - '\t' => 8, // it's really 0-8, use the max always - _ => 1, - } - } - const CONTROL_CHARS: &str = "‹«👉👈»›"; - line.chars() - .filter(|c| !CONTROL_CHARS.contains(*c)) - .map(width) - .sum::() - } - - struct CheckHyperlinkMatch<'a> { - term: &'a Term, - expected_hyperlink: &'a ExpectedHyperlink, - source_location: &'a str, - } - - impl<'a> CheckHyperlinkMatch<'a> { - fn new( - term: &'a Term, - expected_hyperlink: &'a ExpectedHyperlink, - source_location: &'a str, - ) -> Self { - Self { - term, - expected_hyperlink, - source_location, - } - } - - fn check_path_with_position_and_match( - &self, - path_with_position: PathWithPosition, - hyperlink_match: &Match, - ) { - let format_path_with_position_and_match = - |path_with_position: &PathWithPosition, hyperlink_match: &Match| { - let mut result = - format!("Path = «{}»", &path_with_position.path.to_string_lossy()); - if let Some(row) = path_with_position.row { - result += &format!(", line = {row}"); - if let Some(column) = path_with_position.column { - result += &format!(", column = {column}"); - } - } - - result += &format!( - ", at grid cells {}", - Self::format_hyperlink_match(hyperlink_match) - ); - result - }; - - assert_ne!( - self.expected_hyperlink.hyperlink_kind, - HyperlinkKind::Iri, - "\n at {}\nExpected a path, but was a iri:\n{}", - self.source_location, - self.format_renderable_content() - ); - - assert_eq!( - format_path_with_position_and_match( - &PathWithPosition { - path: PathBuf::from(self.expected_hyperlink.iri_or_path.clone()), - row: self.expected_hyperlink.row, - column: self.expected_hyperlink.column - }, - &self.expected_hyperlink.hyperlink_match - ), - format_path_with_position_and_match(&path_with_position, hyperlink_match), - "\n at {}:\n{}", - self.source_location, - self.format_renderable_content() - ); - } - - fn check_iri_and_match(&self, iri: String, hyperlink_match: &Match) { - let format_iri_and_match = |iri: &String, hyperlink_match: &Match| { - format!( - "Url = «{iri}», at grid cells {}", - Self::format_hyperlink_match(hyperlink_match) - ) - }; - - assert_eq!( - self.expected_hyperlink.hyperlink_kind, - HyperlinkKind::Iri, - "\n at {}\nExpected a iri, but was a path:\n{}", - self.source_location, - self.format_renderable_content() - ); - - assert_eq!( - format_iri_and_match( - &self.expected_hyperlink.iri_or_path, - &self.expected_hyperlink.hyperlink_match - ), - format_iri_and_match(&iri, hyperlink_match), - "\n at {}:\n{}", - self.source_location, - self.format_renderable_content() - ); - } - - fn format_hyperlink_match(hyperlink_match: &Match) -> String { - format!( - "({}, {})..=({}, {})", - hyperlink_match.start().line.0, - hyperlink_match.start().column.0, - hyperlink_match.end().line.0, - hyperlink_match.end().column.0 - ) - } - - fn format_renderable_content(&self) -> String { - let mut result = format!("\nHovered on '{}'\n", self.expected_hyperlink.hovered_char); - - let mut first_header_row = String::new(); - let mut second_header_row = String::new(); - let mut marker_header_row = String::new(); - for index in 0..self.term.columns() { - let remainder = index % 10; - if index > 0 && remainder == 0 { - first_header_row.push_str(&format!("{:>10}", (index / 10))); - } - second_header_row += &remainder.to_string(); - if index == self.expected_hyperlink.hovered_grid_point.column.0 { - marker_header_row.push('↓'); - } else { - marker_header_row.push(' '); - } - } - - let remainder = (self.term.columns() - 1) % 10; - if remainder != 0 { - first_header_row.push_str(&" ".repeat(remainder)); - } - - result += &format!("\n [ {}]\n", first_header_row); - result += &format!(" [{}]\n", second_header_row); - result += &format!(" {}", marker_header_row); - - for cell in self - .term - .renderable_content() - .display_iter - .filter(|cell| !cell.flags.intersects(WIDE_CHAR_SPACERS)) - { - if cell.point.column.0 == 0 { - let prefix = - if cell.point.line == self.expected_hyperlink.hovered_grid_point.line { - '→' - } else { - ' ' - }; - result += &format!("\n{prefix}[{:>3}] ", cell.point.line.to_string()); - } - - match cell.c { - '\t' => result.push(' '), - c @ _ => result.push(c), - } - } - - result - } - } - - fn test_hyperlink<'a>( - columns: usize, - total_cells: usize, - test_lines: impl Iterator, - hyperlink_kind: HyperlinkKind, - source_location: &str, - ) { - const CARGO_DIR_REGEX: &str = - r#"\s+(Compiling|Checking|Documenting) [^(]+\((?(?.+))\)"#; - const RUST_DIAGNOSTIC_REGEX: &str = r#"\s+(-->|:::|at) (?(?.+?))(:$|$)"#; - const ISSUE_12338_REGEX: &str = - r#"[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2} (?(?.+))"#; - const MULTIPLE_SAME_LINE_REGEX: &str = - r#"(?(?🦀 multiple_same_line 🦀) 🚣(?[0-9]+) 🏛(?[0-9]+)):"#; - const PATH_HYPERLINK_TIMEOUT_MS: u64 = 1000; - - thread_local! { - static TEST_REGEX_SEARCHES: RefCell = - RefCell::new({ - let default_settings_content: Rc = - settings::parse_json_with_comments(&settings::default_settings()).unwrap(); - let default_terminal_settings = TerminalSettings::from_settings(&default_settings_content); - - RegexSearches::new([ - RUST_DIAGNOSTIC_REGEX, - CARGO_DIR_REGEX, - ISSUE_12338_REGEX, - MULTIPLE_SAME_LINE_REGEX, - ] - .into_iter() - .chain(default_terminal_settings.path_hyperlink_regexes - .iter() - .map(AsRef::as_ref)), - PATH_HYPERLINK_TIMEOUT_MS) - }); - } - - let term_size = TermSize::new(columns, total_cells / columns + 2); - let (term, expected_hyperlink) = - build_term_from_test_lines(hyperlink_kind, term_size, test_lines); - let hyperlink_found = TEST_REGEX_SEARCHES.with(|regex_searches| { - find_from_grid_point( - &term, - expected_hyperlink.hovered_grid_point, - &mut regex_searches.borrow_mut(), - ) - }); - let check_hyperlink_match = - CheckHyperlinkMatch::new(&term, &expected_hyperlink, source_location); - match hyperlink_found { - Some((hyperlink_word, false, hyperlink_match)) => { - check_hyperlink_match.check_path_with_position_and_match( - PathWithPosition::parse_str(&hyperlink_word), - &hyperlink_match, - ); - } - Some((hyperlink_word, true, hyperlink_match)) => { - check_hyperlink_match.check_iri_and_match(hyperlink_word, &hyperlink_match); - } - None => { - if expected_hyperlink.hyperlink_match.start() - != expected_hyperlink.hyperlink_match.end() - { - assert!( - false, - "No hyperlink found\n at {source_location}:\n{}", - check_hyperlink_match.format_renderable_content() - ) - } - } - } - } -} diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs deleted file mode 100644 index 3d70d85f35..0000000000 --- a/crates/terminal/src/terminal_settings.rs +++ /dev/null @@ -1,178 +0,0 @@ -use alacritty_terminal::vte::ansi::{ - CursorShape as AlacCursorShape, CursorStyle as AlacCursorStyle, -}; -use collections::HashMap; -use gpui::{FontFallbacks, FontFeatures, FontWeight, Pixels, px}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -pub use settings::AlternateScroll; - -use settings::{ - PathHyperlinkRegex, RegisterSetting, ShowScrollbar, TerminalBlink, TerminalDockPosition, - TerminalLineHeight, VenvSettings, WorkingDirectory, merge_from::MergeFrom, -}; -use task::Shell; -use theme::FontFamilyName; - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct Toolbar { - pub breadcrumbs: bool, -} - -#[derive(Clone, Debug, Deserialize, RegisterSetting)] -pub struct TerminalSettings { - pub shell: Shell, - pub working_directory: WorkingDirectory, - pub font_size: Option, // todo(settings_refactor) can be non-optional... - pub font_family: Option, - pub font_fallbacks: Option, - pub font_features: Option, - pub font_weight: Option, - pub line_height: TerminalLineHeight, - pub env: HashMap, - pub cursor_shape: CursorShape, - pub blinking: TerminalBlink, - pub alternate_scroll: AlternateScroll, - pub option_as_meta: bool, - pub copy_on_select: bool, - pub keep_selection_on_copy: bool, - pub button: bool, - pub dock: TerminalDockPosition, - pub default_width: Pixels, - pub default_height: Pixels, - pub detect_venv: VenvSettings, - pub max_scroll_history_lines: Option, - pub scroll_multiplier: f32, - pub toolbar: Toolbar, - pub scrollbar: ScrollbarSettings, - pub minimum_contrast: f32, - pub path_hyperlink_regexes: Vec, - pub path_hyperlink_timeout_ms: u64, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct ScrollbarSettings { - /// When to show the scrollbar in the terminal. - /// - /// Default: inherits editor scrollbar settings - pub show: Option, -} - -fn settings_shell_to_task_shell(shell: settings::Shell) -> Shell { - match shell { - settings::Shell::System => Shell::System, - settings::Shell::Program(program) => Shell::Program(program), - settings::Shell::WithArguments { - program, - args, - title_override, - } => Shell::WithArguments { - program, - args, - title_override: title_override.map(Into::into), - }, - } -} - -impl settings::Settings for TerminalSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let user_content = content.terminal.clone().unwrap(); - // Note: we allow a subset of "terminal" settings in the project files. - let mut project_content = user_content.project.clone(); - project_content.merge_from_option(content.project.terminal.as_ref()); - TerminalSettings { - shell: settings_shell_to_task_shell(project_content.shell.unwrap()), - working_directory: project_content.working_directory.unwrap(), - font_size: user_content.font_size.map(px), - font_family: user_content.font_family, - font_fallbacks: user_content.font_fallbacks.map(|fallbacks| { - FontFallbacks::from_fonts( - fallbacks - .into_iter() - .map(|family| family.0.to_string()) - .collect(), - ) - }), - font_features: user_content.font_features, - font_weight: user_content.font_weight, - line_height: user_content.line_height.unwrap(), - env: project_content.env.unwrap(), - cursor_shape: user_content.cursor_shape.unwrap().into(), - blinking: user_content.blinking.unwrap(), - alternate_scroll: user_content.alternate_scroll.unwrap(), - option_as_meta: user_content.option_as_meta.unwrap(), - copy_on_select: user_content.copy_on_select.unwrap(), - keep_selection_on_copy: user_content.keep_selection_on_copy.unwrap(), - button: user_content.button.unwrap(), - dock: user_content.dock.unwrap(), - default_width: px(user_content.default_width.unwrap()), - default_height: px(user_content.default_height.unwrap()), - detect_venv: project_content.detect_venv.unwrap(), - scroll_multiplier: user_content.scroll_multiplier.unwrap(), - max_scroll_history_lines: user_content.max_scroll_history_lines, - toolbar: Toolbar { - breadcrumbs: user_content.toolbar.unwrap().breadcrumbs.unwrap(), - }, - scrollbar: ScrollbarSettings { - show: user_content.scrollbar.unwrap().show, - }, - minimum_contrast: user_content.minimum_contrast.unwrap(), - path_hyperlink_regexes: project_content - .path_hyperlink_regexes - .unwrap() - .into_iter() - .map(|regex| match regex { - PathHyperlinkRegex::SingleLine(regex) => regex, - PathHyperlinkRegex::MultiLine(regex) => regex.join("\n"), - }) - .collect(), - path_hyperlink_timeout_ms: project_content.path_hyperlink_timeout_ms.unwrap(), - } - } -} - -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum CursorShape { - /// Cursor is a block like `█`. - #[default] - Block, - /// Cursor is an underscore like `_`. - Underline, - /// Cursor is a vertical bar like `⎸`. - Bar, - /// Cursor is a hollow box like `▯`. - Hollow, -} - -impl From for CursorShape { - fn from(value: settings::CursorShapeContent) -> Self { - match value { - settings::CursorShapeContent::Block => CursorShape::Block, - settings::CursorShapeContent::Underline => CursorShape::Underline, - settings::CursorShapeContent::Bar => CursorShape::Bar, - settings::CursorShapeContent::Hollow => CursorShape::Hollow, - } - } -} - -impl From for AlacCursorShape { - fn from(value: CursorShape) -> Self { - match value { - CursorShape::Block => AlacCursorShape::Block, - CursorShape::Underline => AlacCursorShape::Underline, - CursorShape::Bar => AlacCursorShape::Beam, - CursorShape::Hollow => AlacCursorShape::HollowBlock, - } - } -} - -impl From for AlacCursorStyle { - fn from(value: CursorShape) -> Self { - AlacCursorStyle { - shape: value.into(), - blinking: false, - } - } -} diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml deleted file mode 100644 index eadd00bcbb..0000000000 --- a/crates/terminal_view/Cargo.toml +++ /dev/null @@ -1,58 +0,0 @@ -[package] -name = "terminal_view" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[features] -test-support = ["editor/test-support", "gpui/test-support"] - -[lib] -path = "src/terminal_view.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -async-recursion.workspace = true -assistant_slash_command.workspace = true -breadcrumbs.workspace = true -collections.workspace = true -db.workspace = true -dirs.workspace = true -editor.workspace = true -futures.workspace = true -gpui.workspace = true -itertools.workspace = true -language.workspace = true -log.workspace = true -pretty_assertions.workspace = true -project.workspace = true -regex.workspace = true -task.workspace = true -schemars.workspace = true -search.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -shellexpand.workspace = true -terminal.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -client = { workspace = true, features = ["test-support"] } -editor = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } -rand.workspace = true -workspace = { workspace = true, features = ["test-support"] } - -[package.metadata.cargo-machete] -ignored = ["log"] diff --git a/crates/terminal_view/LICENSE-GPL b/crates/terminal_view/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/terminal_view/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/terminal_view/README.md b/crates/terminal_view/README.md deleted file mode 100644 index ca48f54542..0000000000 --- a/crates/terminal_view/README.md +++ /dev/null @@ -1,23 +0,0 @@ -Design notes: - -This crate is split into two conceptual halves: -- The terminal.rs file and the src/mappings/ folder, these contain the code for interacting with Alacritty and maintaining the pty event loop. Some behavior in this file is constrained by terminal protocols and standards. The Zed init function is also placed here. -- Everything else. These other files integrate the `Terminal` struct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file. - -ttys are created externally, and so can fail in unexpected ways. However, GPUI currently does not have an API for models than can fail to instantiate. `TerminalBuilder` solves this by using Rust's type system to split tty instantiation into a 2 step process: first attempt to create the file handles with `TerminalBuilder::new()`, check the result, then call `TerminalBuilder::subscribe(cx)` from within a model context. - -The TerminalView struct abstracts over failed and successful terminals, passing focus through to the associated view and allowing clients to build a terminal without worrying about errors. - -#Input - -There are currently many distinct paths for getting keystrokes to the terminal: - -1. Terminal specific characters and bindings. Things like ctrl-a mapping to ASCII control character 1, ANSI escape codes associated with the function keys, etc. These are caught with a raw key-down handler in the element and are processed immediately. This is done with the `try_keystroke()` method on Terminal - -2. GPU Action handlers. GPUI clobbers a few vital keys by adding bindings to them in the global context. These keys are synthesized and then dispatched through the same `try_keystroke()` API as the above mappings - -3. IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the `View::replace_text_in_range()` method, and we then send that to the terminal directly, bypassing `try_keystroke()`. - -4. Pasted text has a separate pathway. - -Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal diff --git a/crates/terminal_view/scripts/print256color.sh b/crates/terminal_view/scripts/print256color.sh deleted file mode 100755 index 9cb3b1c47c..0000000000 --- a/crates/terminal_view/scripts/print256color.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash - -# Tom Hale, 2016. MIT Licence. -# Print out 256 colours, with each number printed in its corresponding colour -# See http://askubuntu.com/questions/821157/print-a-256-color-test-pattern-in-the-terminal/821163#821163 - -set -eu # Fail on errors or undeclared variables - -printable_colours=256 - -# Return a colour that contrasts with the given colour -# Bash only does integer division, so keep it integral -function contrast_colour { - local r g b luminance - colour="$1" - - if (( colour < 16 )); then # Initial 16 ANSI colours - (( colour == 0 )) && printf "15" || printf "0" - return - fi - - # Greyscale # rgb_R = rgb_G = rgb_B = (number - 232) * 10 + 8 - if (( colour > 231 )); then # Greyscale ramp - (( colour < 244 )) && printf "15" || printf "0" - return - fi - - # All other colours: - # 6x6x6 colour cube = 16 + 36*R + 6*G + B # Where RGB are [0..5] - # See http://stackoverflow.com/a/27165165/5353461 - - # r=$(( (colour-16) / 36 )) - g=$(( ((colour-16) % 36) / 6 )) - # b=$(( (colour-16) % 6 )) - - # If luminance is bright, print number in black, white otherwise. - # Green contributes 587/1000 to human perceived luminance - ITU R-REC-BT.601 - (( g > 2)) && printf "0" || printf "15" - return - - # Uncomment the below for more precise luminance calculations - - # # Calculate perceived brightness - # # See https://www.w3.org/TR/AERT#color-contrast - # # and http://www.itu.int/rec/R-REC-BT.601 - # # Luminance is in range 0..5000 as each value is 0..5 - # luminance=$(( (r * 299) + (g * 587) + (b * 114) )) - # (( $luminance > 2500 )) && printf "0" || printf "15" -} - -# Print a coloured block with the number of that colour -function print_colour { - local colour="$1" contrast - contrast=$(contrast_colour "$1") - printf "\e[48;5;%sm" "$colour" # Start block of colour - printf "\e[38;5;%sm%3d" "$contrast" "$colour" # In contrast, print number - printf "\e[0m " # Reset colour -} - -# Starting at $1, print a run of $2 colours -function print_run { - local i - for (( i = "$1"; i < "$1" + "$2" && i < printable_colours; i++ )) do - print_colour "$i" - done - printf " " -} - -# Print blocks of colours -function print_blocks { - local start="$1" i - local end="$2" # inclusive - local block_cols="$3" - local block_rows="$4" - local blocks_per_line="$5" - local block_length=$((block_cols * block_rows)) - - # Print sets of blocks - for (( i = start; i <= end; i += (blocks_per_line-1) * block_length )) do - printf "\n" # Space before each set of blocks - # For each block row - for (( row = 0; row < block_rows; row++ )) do - # Print block columns for all blocks on the line - for (( block = 0; block < blocks_per_line; block++ )) do - print_run $(( i + (block * block_length) )) "$block_cols" - done - (( i += block_cols )) # Prepare to print the next row - printf "\n" - done - done -} - -print_run 0 16 # The first 16 colours are spread over the whole spectrum -printf "\n" -print_blocks 16 231 6 6 3 # 6x6x6 colour cube between 16 and 231 inclusive -print_blocks 232 255 12 2 1 # Not 50, but 24 Shades of Grey diff --git a/crates/terminal_view/scripts/truecolor.sh b/crates/terminal_view/scripts/truecolor.sh deleted file mode 100755 index 622051f242..0000000000 --- a/crates/terminal_view/scripts/truecolor.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# Copied from: https://unix.stackexchange.com/a/696756 -# Based on: https://gist.github.com/XVilka/8346728 and https://unix.stackexchange.com/a/404415/395213 - -awk -v term_cols="${width:-$(tput cols || echo 80)}" -v term_lines="${height:-1}" 'BEGIN{ - s="/\\"; - total_cols=term_cols*term_lines; - for (colnum = 0; colnum255) g = 510-g; - printf "\033[48;2;%d;%d;%dm", r,g,b; - printf "\033[38;2;%d;%d;%dm", 255-r,255-g,255-b; - printf "%s\033[0m", substr(s,colnum%2+1,1); - if (colnum%term_cols==term_cols) printf "\n"; - } - printf "\n"; -}' diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs deleted file mode 100644 index 8d6ef03fd7..0000000000 --- a/crates/terminal_view/src/persistence.rs +++ /dev/null @@ -1,487 +0,0 @@ -use anyhow::Result; -use async_recursion::async_recursion; -use collections::HashSet; -use futures::{StreamExt as _, stream::FuturesUnordered}; -use gpui::{AppContext as _, AsyncWindowContext, Axis, Entity, Task, WeakEntity}; -use project::Project; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; -use ui::{App, Context, Pixels, Window}; -use util::ResultExt as _; - -use db::{ - query, - sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, -}; -use workspace::{ - ItemHandle, ItemId, Member, Pane, PaneAxis, PaneGroup, SerializableItem as _, Workspace, - WorkspaceDb, WorkspaceId, -}; - -use crate::{ - TerminalView, default_working_directory, - terminal_panel::{TerminalPanel, new_terminal_pane}, -}; - -pub(crate) fn serialize_pane_group( - pane_group: &PaneGroup, - active_pane: &Entity, - cx: &mut App, -) -> SerializedPaneGroup { - build_serialized_pane_group(&pane_group.root, active_pane, cx) -} - -fn build_serialized_pane_group( - pane_group: &Member, - active_pane: &Entity, - cx: &mut App, -) -> SerializedPaneGroup { - match pane_group { - Member::Axis(PaneAxis { - axis, - members, - flexes, - bounding_boxes: _, - }) => SerializedPaneGroup::Group { - axis: SerializedAxis(*axis), - children: members - .iter() - .map(|member| build_serialized_pane_group(member, active_pane, cx)) - .collect::>(), - flexes: Some(flexes.lock().clone()), - }, - Member::Pane(pane_handle) => { - SerializedPaneGroup::Pane(serialize_pane(pane_handle, pane_handle == active_pane, cx)) - } - } -} - -fn serialize_pane(pane: &Entity, active: bool, cx: &mut App) -> SerializedPane { - let mut items_to_serialize = HashSet::default(); - let pane = pane.read(cx); - let children = pane - .items() - .filter_map(|item| { - let terminal_view = item.act_as::(cx)?; - if terminal_view.read(cx).terminal().read(cx).task().is_some() { - None - } else { - let id = item.item_id().as_u64(); - items_to_serialize.insert(id); - Some(id) - } - }) - .collect::>(); - let active_item = pane - .active_item() - .map(|item| item.item_id().as_u64()) - .filter(|active_id| items_to_serialize.contains(active_id)); - - let pinned_count = pane.pinned_count(); - SerializedPane { - active, - children, - active_item, - pinned_count, - } -} - -pub(crate) fn deserialize_terminal_panel( - workspace: WeakEntity, - project: Entity, - database_id: WorkspaceId, - serialized_panel: SerializedTerminalPanel, - window: &mut Window, - cx: &mut App, -) -> Task>> { - window.spawn(cx, async move |cx| { - let terminal_panel = workspace.update_in(cx, |workspace, window, cx| { - cx.new(|cx| { - let mut panel = TerminalPanel::new(workspace, window, cx); - panel.height = serialized_panel.height.map(|h| h.round()); - panel.width = serialized_panel.width.map(|w| w.round()); - panel - }) - })?; - match &serialized_panel.items { - SerializedItems::NoSplits(item_ids) => { - let items = deserialize_terminal_views( - database_id, - project, - workspace, - item_ids.as_slice(), - cx, - ) - .await; - let active_item = serialized_panel.active_item_id; - terminal_panel.update_in(cx, |terminal_panel, window, cx| { - terminal_panel.active_pane.update(cx, |pane, cx| { - populate_pane_items(pane, items, active_item, window, cx); - }); - })?; - } - SerializedItems::WithSplits(serialized_pane_group) => { - let center_pane = deserialize_pane_group( - workspace, - project, - terminal_panel.clone(), - database_id, - serialized_pane_group, - cx, - ) - .await; - if let Some((center_group, active_pane)) = center_pane { - terminal_panel.update(cx, |terminal_panel, _| { - terminal_panel.center = PaneGroup::with_root(center_group); - terminal_panel.active_pane = - active_pane.unwrap_or_else(|| terminal_panel.center.first_pane()); - })?; - } - } - } - - Ok(terminal_panel) - }) -} - -fn populate_pane_items( - pane: &mut Pane, - items: Vec>, - active_item: Option, - window: &mut Window, - cx: &mut Context, -) { - let mut item_index = pane.items_len(); - let mut active_item_index = None; - for item in items { - if Some(item.item_id().as_u64()) == active_item { - active_item_index = Some(item_index); - } - pane.add_item(Box::new(item), false, false, None, window, cx); - item_index += 1; - } - if let Some(index) = active_item_index { - pane.activate_item(index, false, false, window, cx); - } -} - -#[async_recursion(?Send)] -async fn deserialize_pane_group( - workspace: WeakEntity, - project: Entity, - panel: Entity, - workspace_id: WorkspaceId, - serialized: &SerializedPaneGroup, - cx: &mut AsyncWindowContext, -) -> Option<(Member, Option>)> { - match serialized { - SerializedPaneGroup::Group { - axis, - flexes, - children, - } => { - let mut current_active_pane = None; - let mut members = Vec::new(); - for child in children { - if let Some((new_member, active_pane)) = deserialize_pane_group( - workspace.clone(), - project.clone(), - panel.clone(), - workspace_id, - child, - cx, - ) - .await - { - members.push(new_member); - current_active_pane = current_active_pane.or(active_pane); - } - } - - if members.is_empty() { - return None; - } - - if members.len() == 1 { - return Some((members.remove(0), current_active_pane)); - } - - Some(( - Member::Axis(PaneAxis::load(axis.0, members, flexes.clone())), - current_active_pane, - )) - } - SerializedPaneGroup::Pane(serialized_pane) => { - let active = serialized_pane.active; - - let pane = panel - .update_in(cx, |terminal_panel, window, cx| { - new_terminal_pane( - workspace.clone(), - project.clone(), - terminal_panel.active_pane.read(cx).is_zoomed(), - window, - cx, - ) - }) - .log_err()?; - let active_item = serialized_pane.active_item; - let pinned_count = serialized_pane.pinned_count; - let new_items = deserialize_terminal_views( - workspace_id, - project.clone(), - workspace.clone(), - serialized_pane.children.as_slice(), - cx, - ); - cx.spawn({ - let pane = pane.downgrade(); - async move |cx| { - let new_items = new_items.await; - - let items = pane.update_in(cx, |pane, window, cx| { - populate_pane_items(pane, new_items, active_item, window, cx); - pane.set_pinned_count(pinned_count); - pane.items_len() - }); - // Avoid blank panes in splits - if items.is_ok_and(|items| items == 0) { - let working_directory = workspace - .update(cx, |workspace, cx| default_working_directory(workspace, cx)) - .ok() - .flatten(); - let Some(terminal) = project - .update(cx, |project, cx| { - project.create_terminal_shell(working_directory, cx) - }) - .log_err() - else { - return; - }; - - let terminal = terminal.await.log_err(); - pane.update_in(cx, |pane, window, cx| { - if let Some(terminal) = terminal { - let terminal_view = Box::new(cx.new(|cx| { - TerminalView::new( - terminal, - workspace.clone(), - Some(workspace_id), - project.downgrade(), - window, - cx, - ) - })); - pane.add_item(terminal_view, true, false, None, window, cx); - } - }) - .ok(); - } - } - }) - .await; - Some((Member::Pane(pane.clone()), active.then_some(pane))) - } - } -} - -fn deserialize_terminal_views( - workspace_id: WorkspaceId, - project: Entity, - workspace: WeakEntity, - item_ids: &[u64], - cx: &mut AsyncWindowContext, -) -> impl Future>> + use<> { - let mut deserialized_items = item_ids - .iter() - .map(|item_id| { - cx.update(|window, cx| { - TerminalView::deserialize( - project.clone(), - workspace.clone(), - workspace_id, - *item_id, - window, - cx, - ) - }) - .unwrap_or_else(|e| Task::ready(Err(e.context("no window present")))) - }) - .collect::>(); - async move { - let mut items = Vec::with_capacity(deserialized_items.len()); - while let Some(item) = deserialized_items.next().await { - if let Some(item) = item.log_err() { - items.push(item); - } - } - items - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub(crate) struct SerializedTerminalPanel { - pub items: SerializedItems, - // A deprecated field, kept for backwards compatibility for the code before terminal splits were introduced. - pub active_item_id: Option, - pub width: Option, - pub height: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub(crate) enum SerializedItems { - // The data stored before terminal splits were introduced. - NoSplits(Vec), - WithSplits(SerializedPaneGroup), -} - -#[derive(Debug, Serialize, Deserialize)] -pub(crate) enum SerializedPaneGroup { - Pane(SerializedPane), - Group { - axis: SerializedAxis, - flexes: Option>, - children: Vec, - }, -} - -#[derive(Debug, Serialize, Deserialize)] -pub(crate) struct SerializedPane { - pub active: bool, - pub children: Vec, - pub active_item: Option, - #[serde(default)] - pub pinned_count: usize, -} - -#[derive(Debug)] -pub(crate) struct SerializedAxis(pub Axis); - -impl Serialize for SerializedAxis { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self.0 { - Axis::Horizontal => serializer.serialize_str("horizontal"), - Axis::Vertical => serializer.serialize_str("vertical"), - } - } -} - -impl<'de> Deserialize<'de> for SerializedAxis { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - match s.as_str() { - "horizontal" => Ok(SerializedAxis(Axis::Horizontal)), - "vertical" => Ok(SerializedAxis(Axis::Vertical)), - invalid => Err(serde::de::Error::custom(format!( - "Invalid axis value: '{invalid}'" - ))), - } - } -} - -pub struct TerminalDb(ThreadSafeConnection); - -impl Domain for TerminalDb { - const NAME: &str = stringify!(TerminalDb); - - const MIGRATIONS: &[&str] = &[ - sql!( - CREATE TABLE terminals ( - workspace_id INTEGER, - item_id INTEGER UNIQUE, - working_directory BLOB, - PRIMARY KEY(workspace_id, item_id), - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ) STRICT; - ), - // Remove the unique constraint on the item_id table - // SQLite doesn't have a way of doing this automatically, so - // we have to do this silly copying. - sql!( - CREATE TABLE terminals2 ( - workspace_id INTEGER, - item_id INTEGER, - working_directory BLOB, - PRIMARY KEY(workspace_id, item_id), - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ) STRICT; - - INSERT INTO terminals2 (workspace_id, item_id, working_directory) - SELECT workspace_id, item_id, working_directory FROM terminals; - - DROP TABLE terminals; - - ALTER TABLE terminals2 RENAME TO terminals; - ), - sql! ( - ALTER TABLE terminals ADD COLUMN working_directory_path TEXT; - UPDATE terminals SET working_directory_path = CAST(working_directory AS TEXT); - ), - ]; -} - -db::static_connection!(TERMINAL_DB, TerminalDb, [WorkspaceDb]); - -impl TerminalDb { - query! { - pub async fn update_workspace_id( - new_id: WorkspaceId, - old_id: WorkspaceId, - item_id: ItemId - ) -> Result<()> { - UPDATE terminals - SET workspace_id = ? - WHERE workspace_id = ? AND item_id = ? - } - } - - pub async fn save_working_directory( - &self, - item_id: ItemId, - workspace_id: WorkspaceId, - working_directory: PathBuf, - ) -> Result<()> { - log::debug!( - "Saving working directory {working_directory:?} for item {item_id} in workspace {workspace_id:?}" - ); - let query = - "INSERT INTO terminals(item_id, workspace_id, working_directory, working_directory_path) - VALUES (?1, ?2, ?3, ?4) - ON CONFLICT DO UPDATE SET - item_id = ?1, - workspace_id = ?2, - working_directory = ?3, - working_directory_path = ?4" - ; - self.write(move |conn| { - let mut statement = Statement::prepare(conn, query)?; - let mut next_index = statement.bind(&item_id, 1)?; - next_index = statement.bind(&workspace_id, next_index)?; - next_index = statement.bind(&working_directory, next_index)?; - statement.bind( - &working_directory.to_string_lossy().into_owned(), - next_index, - )?; - statement.exec() - }) - .await - } - - query! { - pub fn get_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result> { - SELECT working_directory - FROM terminals - WHERE item_id = ? AND workspace_id = ? - } - } -} diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs deleted file mode 100644 index fd9568b0c5..0000000000 --- a/crates/terminal_view/src/terminal_element.rs +++ /dev/null @@ -1,2049 +0,0 @@ -use editor::{CursorLayout, EditorSettings, HighlightedRange, HighlightedRangeLine}; -use gpui::{ - AbsoluteLength, AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, DispatchPhase, - Element, ElementId, Entity, FocusHandle, Font, FontFeatures, FontStyle, FontWeight, - GlobalElementId, HighlightStyle, Hitbox, Hsla, InputHandler, InteractiveElement, Interactivity, - IntoElement, LayoutId, Length, ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, - Point, ShapedLine, StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, TextStyle, - UTF16Selection, UnderlineStyle, WeakEntity, WhiteSpace, Window, div, fill, point, px, relative, - size, -}; -use itertools::Itertools; -use language::CursorShape; -use settings::Settings; -use std::time::Instant; -use terminal::{ - IndexedCell, Terminal, TerminalBounds, TerminalContent, - alacritty_terminal::{ - grid::Dimensions, - index::Point as AlacPoint, - term::{TermMode, cell::Flags}, - vte::ansi::{ - Color::{self as AnsiColor, Named}, - CursorShape as AlacCursorShape, NamedColor, - }, - }, - terminal_settings::TerminalSettings, -}; -use theme::{ActiveTheme, Theme, ThemeSettings}; -use ui::utils::ensure_minimum_contrast; -use ui::{ParentElement, Tooltip}; -use util::ResultExt; -use workspace::Workspace; - -use std::mem; -use std::{fmt::Debug, ops::RangeInclusive, rc::Rc}; - -use crate::{BlockContext, BlockProperties, ContentMode, TerminalMode, TerminalView}; - -/// The information generated during layout that is necessary for painting. -pub struct LayoutState { - hitbox: Hitbox, - batched_text_runs: Vec, - rects: Vec, - relative_highlighted_ranges: Vec<(RangeInclusive, Hsla)>, - cursor: Option, - background_color: Hsla, - dimensions: TerminalBounds, - mode: TermMode, - display_offset: usize, - hyperlink_tooltip: Option, - gutter: Pixels, - block_below_cursor_element: Option, - base_text_style: TextStyle, - content_mode: ContentMode, -} - -/// Helper struct for converting data between Alacritty's cursor points, and displayed cursor points. -struct DisplayCursor { - line: i32, - col: usize, -} - -impl DisplayCursor { - fn from(cursor_point: AlacPoint, display_offset: usize) -> Self { - Self { - line: cursor_point.line.0 + display_offset as i32, - col: cursor_point.column.0, - } - } - - pub fn line(&self) -> i32 { - self.line - } - - pub fn col(&self) -> usize { - self.col - } -} - -/// A batched text run that combines multiple adjacent cells with the same style -#[derive(Debug)] -pub struct BatchedTextRun { - pub start_point: AlacPoint, - pub text: String, - pub cell_count: usize, - pub style: TextRun, - pub font_size: AbsoluteLength, -} - -impl BatchedTextRun { - fn new_from_char( - start_point: AlacPoint, - c: char, - style: TextRun, - font_size: AbsoluteLength, - ) -> Self { - let mut text = String::with_capacity(100); // Pre-allocate for typical line length - text.push(c); - BatchedTextRun { - start_point, - text, - cell_count: 1, - style, - font_size, - } - } - - fn can_append(&self, other_style: &TextRun) -> bool { - self.style.font == other_style.font - && self.style.color == other_style.color - && self.style.background_color == other_style.background_color - && self.style.underline == other_style.underline - && self.style.strikethrough == other_style.strikethrough - } - - fn append_char(&mut self, c: char) { - self.append_char_internal(c, true); - } - - fn append_zero_width_chars(&mut self, chars: &[char]) { - for &c in chars { - self.append_char_internal(c, false); - } - } - - fn append_char_internal(&mut self, c: char, counts_cell: bool) { - self.text.push(c); - if counts_cell { - self.cell_count += 1; - } - self.style.len += c.len_utf8(); - } - - pub fn paint( - &self, - origin: Point, - dimensions: &TerminalBounds, - window: &mut Window, - cx: &mut App, - ) { - let pos = Point::new( - origin.x + self.start_point.column as f32 * dimensions.cell_width, - origin.y + self.start_point.line as f32 * dimensions.line_height, - ); - - let _ = window - .text_system() - .shape_line( - self.text.clone().into(), - self.font_size.to_pixels(window.rem_size()), - std::slice::from_ref(&self.style), - Some(dimensions.cell_width), - ) - .paint(pos, dimensions.line_height, window, cx); - } -} - -#[derive(Clone, Debug, Default)] -pub struct LayoutRect { - point: AlacPoint, - num_of_cells: usize, - color: Hsla, -} - -impl LayoutRect { - fn new(point: AlacPoint, num_of_cells: usize, color: Hsla) -> LayoutRect { - LayoutRect { - point, - num_of_cells, - color, - } - } - - pub fn paint(&self, origin: Point, dimensions: &TerminalBounds, window: &mut Window) { - let position = { - let alac_point = self.point; - point( - (origin.x + alac_point.column as f32 * dimensions.cell_width).floor(), - origin.y + alac_point.line as f32 * dimensions.line_height, - ) - }; - let size = point( - (dimensions.cell_width * self.num_of_cells as f32).ceil(), - dimensions.line_height, - ) - .into(); - - window.paint_quad(fill(Bounds::new(position, size), self.color)); - } -} - -/// Represents a rectangular region with a specific background color -#[derive(Debug, Clone)] -struct BackgroundRegion { - start_line: i32, - start_col: i32, - end_line: i32, - end_col: i32, - color: Hsla, -} - -impl BackgroundRegion { - fn new(line: i32, col: i32, color: Hsla) -> Self { - BackgroundRegion { - start_line: line, - start_col: col, - end_line: line, - end_col: col, - color, - } - } - - /// Check if this region can be merged with another region - fn can_merge_with(&self, other: &BackgroundRegion) -> bool { - if self.color != other.color { - return false; - } - - // Check if regions are adjacent horizontally - if self.start_line == other.start_line && self.end_line == other.end_line { - return self.end_col + 1 == other.start_col || other.end_col + 1 == self.start_col; - } - - // Check if regions are adjacent vertically with same column span - if self.start_col == other.start_col && self.end_col == other.end_col { - return self.end_line + 1 == other.start_line || other.end_line + 1 == self.start_line; - } - - false - } - - /// Merge this region with another region - fn merge_with(&mut self, other: &BackgroundRegion) { - self.start_line = self.start_line.min(other.start_line); - self.start_col = self.start_col.min(other.start_col); - self.end_line = self.end_line.max(other.end_line); - self.end_col = self.end_col.max(other.end_col); - } -} - -/// Merge background regions to minimize the number of rectangles -fn merge_background_regions(regions: Vec) -> Vec { - if regions.is_empty() { - return regions; - } - - let mut merged = regions; - let mut changed = true; - - // Keep merging until no more merges are possible - while changed { - changed = false; - let mut i = 0; - - while i < merged.len() { - let mut j = i + 1; - while j < merged.len() { - if merged[i].can_merge_with(&merged[j]) { - let other = merged.remove(j); - merged[i].merge_with(&other); - changed = true; - } else { - j += 1; - } - } - i += 1; - } - } - - merged -} - -/// The GPUI element that paints the terminal. -/// We need to keep a reference to the model for mouse events, do we need it for any other terminal stuff, or can we move that to connection? -pub struct TerminalElement { - terminal: Entity, - terminal_view: Entity, - workspace: WeakEntity, - focus: FocusHandle, - focused: bool, - cursor_visible: bool, - interactivity: Interactivity, - mode: TerminalMode, - block_below_cursor: Option>, -} - -impl InteractiveElement for TerminalElement { - fn interactivity(&mut self) -> &mut Interactivity { - &mut self.interactivity - } -} - -impl StatefulInteractiveElement for TerminalElement {} - -impl TerminalElement { - pub fn new( - terminal: Entity, - terminal_view: Entity, - workspace: WeakEntity, - focus: FocusHandle, - focused: bool, - cursor_visible: bool, - block_below_cursor: Option>, - mode: TerminalMode, - ) -> TerminalElement { - TerminalElement { - terminal, - terminal_view, - workspace, - focused, - focus: focus.clone(), - cursor_visible, - block_below_cursor, - mode, - interactivity: Default::default(), - } - .track_focus(&focus) - } - - //Vec> -> Clip out the parts of the ranges - - pub fn layout_grid( - grid: impl Iterator, - start_line_offset: i32, - text_style: &TextStyle, - hyperlink: Option<(HighlightStyle, &RangeInclusive)>, - minimum_contrast: f32, - cx: &App, - ) -> (Vec, Vec) { - let start_time = Instant::now(); - let theme = cx.theme(); - - // Pre-allocate with estimated capacity to reduce reallocations - let estimated_cells = grid.size_hint().0; - let estimated_runs = estimated_cells / 10; // Estimate ~10 cells per run - let estimated_regions = estimated_cells / 20; // Estimate ~20 cells per background region - - let mut batched_runs = Vec::with_capacity(estimated_runs); - let mut cell_count = 0; - - // Collect background regions for efficient merging - let mut background_regions: Vec = Vec::with_capacity(estimated_regions); - let mut current_batch: Option = None; - - // First pass: collect all cells and their backgrounds - let linegroups = grid.into_iter().chunk_by(|i| i.point.line); - for (line_index, (_, line)) in linegroups.into_iter().enumerate() { - let alac_line = start_line_offset + line_index as i32; - - // Flush any existing batch at line boundaries - if let Some(batch) = current_batch.take() { - batched_runs.push(batch); - } - - let mut previous_cell_had_extras = false; - - for cell in line { - let mut fg = cell.fg; - let mut bg = cell.bg; - if cell.flags.contains(Flags::INVERSE) { - mem::swap(&mut fg, &mut bg); - } - - // Collect background regions (skip default background) - if !matches!(bg, Named(NamedColor::Background)) { - let color = convert_color(&bg, theme); - let col = cell.point.column.0 as i32; - - // Try to extend the last region if it's on the same line with the same color - if let Some(last_region) = background_regions.last_mut() { - if last_region.color == color - && last_region.start_line == alac_line - && last_region.end_line == alac_line - && last_region.end_col + 1 == col - { - last_region.end_col = col; - } else { - background_regions.push(BackgroundRegion::new(alac_line, col, color)); - } - } else { - background_regions.push(BackgroundRegion::new(alac_line, col, color)); - } - } - // Skip wide character spacers - they're just placeholders for the second cell of wide characters - if cell.flags.contains(Flags::WIDE_CHAR_SPACER) { - continue; - } - - // Skip spaces that follow cells with extras (emoji variation sequences) - if cell.c == ' ' && previous_cell_had_extras { - previous_cell_had_extras = false; - continue; - } - // Update tracking for next iteration - previous_cell_had_extras = - matches!(cell.zerowidth(), Some(chars) if !chars.is_empty()); - - //Layout current cell text - { - if !is_blank(&cell) { - cell_count += 1; - let cell_style = TerminalElement::cell_style( - &cell, - fg, - bg, - theme, - text_style, - hyperlink, - minimum_contrast, - ); - - let cell_point = AlacPoint::new(alac_line, cell.point.column.0 as i32); - let zero_width_chars = cell.zerowidth(); - - // Try to batch with existing run - if let Some(ref mut batch) = current_batch { - if batch.can_append(&cell_style) - && batch.start_point.line == cell_point.line - && batch.start_point.column + batch.cell_count as i32 - == cell_point.column - { - batch.append_char(cell.c); - if let Some(chars) = zero_width_chars { - batch.append_zero_width_chars(chars); - } - } else { - // Flush current batch and start new one - let old_batch = current_batch.take().unwrap(); - batched_runs.push(old_batch); - let mut new_batch = BatchedTextRun::new_from_char( - cell_point, - cell.c, - cell_style, - text_style.font_size, - ); - if let Some(chars) = zero_width_chars { - new_batch.append_zero_width_chars(chars); - } - current_batch = Some(new_batch); - } - } else { - // Start new batch - let mut new_batch = BatchedTextRun::new_from_char( - cell_point, - cell.c, - cell_style, - text_style.font_size, - ); - if let Some(chars) = zero_width_chars { - new_batch.append_zero_width_chars(chars); - } - current_batch = Some(new_batch); - } - }; - } - } - } - - // Flush any remaining batch - if let Some(batch) = current_batch { - batched_runs.push(batch); - } - - // Second pass: merge background regions and convert to layout rects - let region_count = background_regions.len(); - let merged_regions = merge_background_regions(background_regions); - let mut rects = Vec::with_capacity(merged_regions.len() * 2); // Estimate 2 rects per merged region - - // Convert merged regions to layout rects - // Since LayoutRect only supports single-line rectangles, we need to split multi-line regions - for region in merged_regions { - for line in region.start_line..=region.end_line { - rects.push(LayoutRect::new( - AlacPoint::new(line, region.start_col), - (region.end_col - region.start_col + 1) as usize, - region.color, - )); - } - } - - let layout_time = start_time.elapsed(); - log::debug!( - "Terminal layout_grid: {} cells processed, {} batched runs created, {} rects (from {} merged regions), layout took {:?}", - cell_count, - batched_runs.len(), - rects.len(), - region_count, - layout_time - ); - - (rects, batched_runs) - } - - /// Computes the cursor position and expected block width, may return a zero width if x_for_index returns - /// the same position for sequential indexes. Use em_width instead - fn shape_cursor( - cursor_point: DisplayCursor, - size: TerminalBounds, - text_fragment: &ShapedLine, - ) -> Option<(Point, Pixels)> { - if cursor_point.line() < size.total_lines() as i32 { - let cursor_width = if text_fragment.width == Pixels::ZERO { - size.cell_width() - } else { - text_fragment.width - }; - - // Cursor should always surround as much of the text as possible, - // hence when on pixel boundaries round the origin down and the width up - Some(( - point( - (cursor_point.col() as f32 * size.cell_width()).floor(), - (cursor_point.line() as f32 * size.line_height()).floor(), - ), - cursor_width.ceil(), - )) - } else { - None - } - } - - /// Checks if a character is a decorative block/box-like character that should - /// preserve its exact colors without contrast adjustment. - /// - /// This specifically targets characters used as visual connectors, separators, - /// and borders where color matching with adjacent backgrounds is critical. - /// Regular icons (git, folders, etc.) are excluded as they need to remain readable. - /// - /// Fixes https://github.com/zed-industries/zed/issues/34234 - fn is_decorative_character(ch: char) -> bool { - matches!( - ch as u32, - // Unicode Box Drawing and Block Elements - 0x2500..=0x257F // Box Drawing (└ ┐ ─ │ etc.) - | 0x2580..=0x259F // Block Elements (▀ ▄ █ ░ ▒ ▓ etc.) - | 0x25A0..=0x25FF // Geometric Shapes (■ ▶ ● etc. - includes triangular/circular separators) - - // Private Use Area - Powerline separator symbols only - | 0xE0B0..=0xE0B7 // Powerline separators: triangles (E0B0-E0B3) and half circles (E0B4-E0B7) - | 0xE0B8..=0xE0BF // Powerline separators: corner triangles - | 0xE0C0..=0xE0CA // Powerline separators: flames (E0C0-E0C3), pixelated (E0C4-E0C7), and ice (E0C8 & E0CA) - | 0xE0CC..=0xE0D1 // Powerline separators: honeycombs (E0CC-E0CD) and lego (E0CE-E0D1) - | 0xE0D2..=0xE0D7 // Powerline separators: trapezoid (E0D2 & E0D4) and inverted triangles (E0D6-E0D7) - ) - } - - /// Converts the Alacritty cell styles to GPUI text styles and background color. - fn cell_style( - indexed: &IndexedCell, - fg: terminal::alacritty_terminal::vte::ansi::Color, - bg: terminal::alacritty_terminal::vte::ansi::Color, - colors: &Theme, - text_style: &TextStyle, - hyperlink: Option<(HighlightStyle, &RangeInclusive)>, - minimum_contrast: f32, - ) -> TextRun { - let flags = indexed.cell.flags; - let mut fg = convert_color(&fg, colors); - let bg = convert_color(&bg, colors); - - // Only apply contrast adjustment to non-decorative characters - if !Self::is_decorative_character(indexed.c) { - fg = ensure_minimum_contrast(fg, bg, minimum_contrast); - } - - // Ghostty uses (175/255) as the multiplier (~0.69), Alacritty uses 0.66, Kitty - // uses 0.75. We're using 0.7 because it's pretty well in the middle of that. - if flags.intersects(Flags::DIM) { - fg.a *= 0.7; - } - - let underline = (flags.intersects(Flags::ALL_UNDERLINES) - || indexed.cell.hyperlink().is_some()) - .then(|| UnderlineStyle { - color: Some(fg), - thickness: Pixels::from(1.0), - wavy: flags.contains(Flags::UNDERCURL), - }); - - let strikethrough = flags - .intersects(Flags::STRIKEOUT) - .then(|| StrikethroughStyle { - color: Some(fg), - thickness: Pixels::from(1.0), - }); - - let weight = if flags.intersects(Flags::BOLD) { - FontWeight::BOLD - } else { - text_style.font_weight - }; - - let style = if flags.intersects(Flags::ITALIC) { - FontStyle::Italic - } else { - FontStyle::Normal - }; - - let mut result = TextRun { - len: indexed.c.len_utf8(), - color: fg, - background_color: None, - font: Font { - weight, - style, - ..text_style.font() - }, - underline, - strikethrough, - }; - - if let Some((style, range)) = hyperlink - && range.contains(&indexed.point) - { - if let Some(underline) = style.underline { - result.underline = Some(underline); - } - - if let Some(color) = style.color { - result.color = color; - } - } - - result - } - - fn generic_button_handler( - connection: Entity, - focus_handle: FocusHandle, - steal_focus: bool, - f: impl Fn(&mut Terminal, &E, &mut Context), - ) -> impl Fn(&E, &mut Window, &mut App) { - move |event, window, cx| { - if steal_focus { - window.focus(&focus_handle); - } else if !focus_handle.is_focused(window) { - return; - } - connection.update(cx, |terminal, cx| { - f(terminal, event, cx); - - cx.notify(); - }) - } - } - - fn register_mouse_listeners( - &mut self, - mode: TermMode, - hitbox: &Hitbox, - content_mode: &ContentMode, - window: &mut Window, - ) { - let focus = self.focus.clone(); - let terminal = self.terminal.clone(); - let terminal_view = self.terminal_view.clone(); - - self.interactivity.on_mouse_down(MouseButton::Left, { - let terminal = terminal.clone(); - let focus = focus.clone(); - let terminal_view = terminal_view.clone(); - - move |e, window, cx| { - window.focus(&focus); - - let scroll_top = terminal_view.read(cx).scroll_top; - terminal.update(cx, |terminal, cx| { - let mut adjusted_event = e.clone(); - if scroll_top > Pixels::ZERO { - adjusted_event.position.y += scroll_top; - } - terminal.mouse_down(&adjusted_event, cx); - cx.notify(); - }) - } - }); - - window.on_mouse_event({ - let terminal = self.terminal.clone(); - let hitbox = hitbox.clone(); - let focus = focus.clone(); - let terminal_view = terminal_view; - move |e: &MouseMoveEvent, phase, window, cx| { - if phase != DispatchPhase::Bubble { - return; - } - - if e.pressed_button.is_some() && !cx.has_active_drag() && focus.is_focused(window) { - let hovered = hitbox.is_hovered(window); - - let scroll_top = terminal_view.read(cx).scroll_top; - terminal.update(cx, |terminal, cx| { - if terminal.selection_started() || hovered { - let mut adjusted_event = e.clone(); - if scroll_top > Pixels::ZERO { - adjusted_event.position.y += scroll_top; - } - terminal.mouse_drag(&adjusted_event, hitbox.bounds, cx); - cx.notify(); - } - }) - } - - if hitbox.is_hovered(window) { - terminal.update(cx, |terminal, cx| { - terminal.mouse_move(e, cx); - }) - } - } - }); - - self.interactivity.on_mouse_up( - MouseButton::Left, - TerminalElement::generic_button_handler( - terminal.clone(), - focus.clone(), - false, - move |terminal, e, cx| { - terminal.mouse_up(e, cx); - }, - ), - ); - self.interactivity.on_mouse_down( - MouseButton::Middle, - TerminalElement::generic_button_handler( - terminal.clone(), - focus.clone(), - true, - move |terminal, e, cx| { - terminal.mouse_down(e, cx); - }, - ), - ); - - if content_mode.is_scrollable() { - self.interactivity.on_scroll_wheel({ - let terminal_view = self.terminal_view.downgrade(); - move |e, window, cx| { - terminal_view - .update(cx, |terminal_view, cx| { - if matches!(terminal_view.mode, TerminalMode::Standalone) - || terminal_view.focus_handle.is_focused(window) - { - terminal_view.scroll_wheel(e, cx); - cx.notify(); - } - }) - .ok(); - } - }); - } - - // Mouse mode handlers: - // All mouse modes need the extra click handlers - if mode.intersects(TermMode::MOUSE_MODE) { - self.interactivity.on_mouse_down( - MouseButton::Right, - TerminalElement::generic_button_handler( - terminal.clone(), - focus.clone(), - true, - move |terminal, e, cx| { - terminal.mouse_down(e, cx); - }, - ), - ); - self.interactivity.on_mouse_up( - MouseButton::Right, - TerminalElement::generic_button_handler( - terminal.clone(), - focus.clone(), - false, - move |terminal, e, cx| { - terminal.mouse_up(e, cx); - }, - ), - ); - self.interactivity.on_mouse_up( - MouseButton::Middle, - TerminalElement::generic_button_handler( - terminal, - focus, - false, - move |terminal, e, cx| { - terminal.mouse_up(e, cx); - }, - ), - ); - } - } - - fn rem_size(&self, cx: &mut App) -> Option { - let settings = ThemeSettings::get_global(cx).clone(); - let buffer_font_size = settings.buffer_font_size(cx); - let rem_size_scale = { - // Our default UI font size is 14px on a 16px base scale. - // This means the default UI font size is 0.875rems. - let default_font_size_scale = 14. / ui::BASE_REM_SIZE_IN_PX; - - // We then determine the delta between a single rem and the default font - // size scale. - let default_font_size_delta = 1. - default_font_size_scale; - - // Finally, we add this delta to 1rem to get the scale factor that - // should be used to scale up the UI. - 1. + default_font_size_delta - }; - - Some(buffer_font_size * rem_size_scale) - } -} - -impl Element for TerminalElement { - type RequestLayoutState = (); - type PrepaintState = LayoutState; - - fn id(&self) -> Option { - self.interactivity.element_id.clone() - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - let height: Length = match self.terminal_view.read(cx).content_mode(window, cx) { - ContentMode::Inline { - displayed_lines, - total_lines: _, - } => { - let rem_size = window.rem_size(); - let line_height = f32::from(window.text_style().font_size.to_pixels(rem_size)) - * TerminalSettings::get_global(cx) - .line_height - .value() - .to_pixels(rem_size); - (displayed_lines * line_height).into() - } - ContentMode::Scrollable => { - if let TerminalMode::Embedded { .. } = &self.mode { - let term = self.terminal.read(cx); - if !term.scrolled_to_top() && !term.scrolled_to_bottom() && self.focused { - self.interactivity.occlude_mouse(); - } - } - - relative(1.).into() - } - }; - - let layout_id = self.interactivity.request_layout( - global_id, - inspector_id, - window, - cx, - |mut style, window, cx| { - style.size.width = relative(1.).into(); - style.size.height = height; - - window.request_layout(style, None, cx) - }, - ); - (layout_id, ()) - } - - fn prepaint( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - _: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - let rem_size = self.rem_size(cx); - self.interactivity.prepaint( - global_id, - inspector_id, - bounds, - bounds.size, - window, - cx, - |_, _, hitbox, window, cx| { - let hitbox = hitbox.unwrap(); - let settings = ThemeSettings::get_global(cx).clone(); - - let buffer_font_size = settings.buffer_font_size(cx); - - let terminal_settings = TerminalSettings::get_global(cx); - let minimum_contrast = terminal_settings.minimum_contrast; - - let font_family = terminal_settings.font_family.as_ref().map_or_else( - || settings.buffer_font.family.clone(), - |font_family| font_family.0.clone().into(), - ); - - let font_fallbacks = terminal_settings - .font_fallbacks - .as_ref() - .or(settings.buffer_font.fallbacks.as_ref()) - .cloned(); - - let font_features = terminal_settings - .font_features - .as_ref() - .unwrap_or(&FontFeatures::disable_ligatures()) - .clone(); - - let font_weight = terminal_settings.font_weight.unwrap_or_default(); - - let line_height = terminal_settings.line_height.value(); - - let font_size = match &self.mode { - TerminalMode::Embedded { .. } => { - window.text_style().font_size.to_pixels(window.rem_size()) - } - TerminalMode::Standalone => terminal_settings - .font_size - .map_or(buffer_font_size, |size| theme::adjusted_font_size(size, cx)), - }; - - let theme = cx.theme().clone(); - - let link_style = HighlightStyle { - color: Some(theme.colors().link_text_hover), - font_weight: Some(font_weight), - font_style: None, - background_color: None, - underline: Some(UnderlineStyle { - thickness: px(1.0), - color: Some(theme.colors().link_text_hover), - wavy: false, - }), - strikethrough: None, - fade_out: None, - }; - - let text_style = TextStyle { - font_family, - font_features, - font_weight, - font_fallbacks, - font_size: font_size.into(), - font_style: FontStyle::Normal, - line_height: line_height.into(), - background_color: Some(theme.colors().terminal_ansi_background), - white_space: WhiteSpace::Normal, - // These are going to be overridden per-cell - color: theme.colors().terminal_foreground, - ..Default::default() - }; - - let text_system = cx.text_system(); - let player_color = theme.players().local(); - let match_color = theme.colors().search_match_background; - let gutter; - let (dimensions, line_height_px) = { - let rem_size = window.rem_size(); - let font_pixels = text_style.font_size.to_pixels(rem_size); - // TODO: line_height should be an f32 not an AbsoluteLength. - let line_height = f32::from(font_pixels) * line_height.to_pixels(rem_size); - let font_id = cx.text_system().resolve_font(&text_style.font()); - - let cell_width = text_system - .advance(font_id, font_pixels, 'm') - .unwrap() - .width; - gutter = cell_width; - - let mut size = bounds.size; - size.width -= gutter; - - // https://github.com/zed-industries/zed/issues/2750 - // if the terminal is one column wide, rendering 🦀 - // causes alacritty to misbehave. - if size.width < cell_width * 2.0 { - size.width = cell_width * 2.0; - } - - let mut origin = bounds.origin; - origin.x += gutter; - - ( - TerminalBounds::new(line_height, cell_width, Bounds { origin, size }), - line_height, - ) - }; - - let search_matches = self.terminal.read(cx).matches.clone(); - - let background_color = theme.colors().terminal_background; - - let (last_hovered_word, hover_tooltip) = - self.terminal.update(cx, |terminal, cx| { - terminal.set_size(dimensions); - terminal.sync(window, cx); - - if window.modifiers().secondary() - && bounds.contains(&window.mouse_position()) - && self.terminal_view.read(cx).hover.is_some() - { - let registered_hover = self.terminal_view.read(cx).hover.as_ref(); - if terminal.last_content.last_hovered_word.as_ref() - == registered_hover.map(|hover| &hover.hovered_word) - { - ( - terminal.last_content.last_hovered_word.clone(), - registered_hover.map(|hover| hover.tooltip.clone()), - ) - } else { - (None, None) - } - } else { - (None, None) - } - }); - - let scroll_top = self.terminal_view.read(cx).scroll_top; - let hyperlink_tooltip = hover_tooltip.map(|hover_tooltip| { - let offset = bounds.origin + point(gutter, px(0.)) - point(px(0.), scroll_top); - let mut element = div() - .size_full() - .id("terminal-element") - .tooltip(Tooltip::text(hover_tooltip)) - .into_any_element(); - element.prepaint_as_root(offset, bounds.size.into(), window, cx); - element - }); - - let TerminalContent { - cells, - mode, - display_offset, - cursor_char, - selection, - cursor, - .. - } = &self.terminal.read(cx).last_content; - let mode = *mode; - let display_offset = *display_offset; - - // searches, highlights to a single range representations - let mut relative_highlighted_ranges = Vec::new(); - for search_match in search_matches { - relative_highlighted_ranges.push((search_match, match_color)) - } - if let Some(selection) = selection { - relative_highlighted_ranges - .push((selection.start..=selection.end, player_color.selection)); - } - - // then have that representation be converted to the appropriate highlight data structure - - let content_mode = self.terminal_view.read(cx).content_mode(window, cx); - let (rects, batched_text_runs) = match content_mode { - ContentMode::Scrollable => { - // In scrollable mode, the terminal already provides cells - // that are correctly positioned for the current viewport - // based on its display_offset. We don't need additional filtering. - TerminalElement::layout_grid( - cells.iter().cloned(), - 0, - &text_style, - last_hovered_word.as_ref().map(|last_hovered_word| { - (link_style, &last_hovered_word.word_match) - }), - minimum_contrast, - cx, - ) - } - ContentMode::Inline { .. } => { - let intersection = window.content_mask().bounds.intersect(&bounds); - let start_row = (intersection.top() - bounds.top()) / line_height_px; - let end_row = start_row + intersection.size.height / line_height_px; - let line_range = (start_row as i32)..=(end_row as i32); - - TerminalElement::layout_grid( - cells - .iter() - .skip_while(|i| &i.point.line < line_range.start()) - .take_while(|i| &i.point.line <= line_range.end()) - .cloned(), - *line_range.start(), - &text_style, - last_hovered_word.as_ref().map(|last_hovered_word| { - (link_style, &last_hovered_word.word_match) - }), - minimum_contrast, - cx, - ) - } - }; - - // Layout cursor. Rectangle is used for IME, so we should lay it out even - // if we don't end up showing it. - let cursor = if let AlacCursorShape::Hidden = cursor.shape { - None - } else { - let cursor_point = DisplayCursor::from(cursor.point, display_offset); - let cursor_text = { - let str_trxt = cursor_char.to_string(); - let len = str_trxt.len(); - window.text_system().shape_line( - str_trxt.into(), - text_style.font_size.to_pixels(window.rem_size()), - &[TextRun { - len, - font: text_style.font(), - color: theme.colors().terminal_ansi_background, - ..Default::default() - }], - None, - ) - }; - - let focused = self.focused; - TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map( - move |(cursor_position, block_width)| { - let (shape, text) = match cursor.shape { - AlacCursorShape::Block if !focused => (CursorShape::Hollow, None), - AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)), - AlacCursorShape::Underline => (CursorShape::Underline, None), - AlacCursorShape::Beam => (CursorShape::Bar, None), - AlacCursorShape::HollowBlock => (CursorShape::Hollow, None), - //This case is handled in the if wrapping the whole cursor layout - AlacCursorShape::Hidden => unreachable!(), - }; - - CursorLayout::new( - cursor_position, - block_width, - dimensions.line_height, - theme.players().local().cursor, - shape, - text, - ) - }, - ) - }; - - let block_below_cursor_element = if let Some(block) = &self.block_below_cursor { - let terminal = self.terminal.read(cx); - if terminal.last_content.display_offset == 0 { - let target_line = terminal.last_content.cursor.point.line.0 + 1; - let render = &block.render; - let mut block_cx = BlockContext { - window, - context: cx, - dimensions, - }; - let element = render(&mut block_cx); - let mut element = div().occlude().child(element).into_any_element(); - let available_space = size( - AvailableSpace::Definite(dimensions.width() + gutter), - AvailableSpace::Definite( - block.height as f32 * dimensions.line_height(), - ), - ); - let origin = bounds.origin - + point(px(0.), target_line as f32 * dimensions.line_height()) - - point(px(0.), scroll_top); - window.with_rem_size(rem_size, |window| { - element.prepaint_as_root(origin, available_space, window, cx); - }); - Some(element) - } else { - None - } - } else { - None - }; - - LayoutState { - hitbox, - batched_text_runs, - cursor, - background_color, - dimensions, - rects, - relative_highlighted_ranges, - mode, - display_offset, - hyperlink_tooltip, - gutter, - block_below_cursor_element, - base_text_style: text_style, - content_mode, - } - }, - ) - } - - fn paint( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - _: &mut Self::RequestLayoutState, - layout: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - let paint_start = Instant::now(); - window.with_content_mask(Some(ContentMask { bounds }), |window| { - let scroll_top = self.terminal_view.read(cx).scroll_top; - - window.paint_quad(fill(bounds, layout.background_color)); - let origin = - bounds.origin + Point::new(layout.gutter, px(0.)) - Point::new(px(0.), scroll_top); - - let marked_text_cloned: Option = { - let ime_state = &self.terminal_view.read(cx).ime_state; - ime_state.as_ref().map(|state| state.marked_text.clone()) - }; - - let terminal_input_handler = TerminalInputHandler { - terminal: self.terminal.clone(), - terminal_view: self.terminal_view.clone(), - cursor_bounds: layout - .cursor - .as_ref() - .map(|cursor| cursor.bounding_rect(origin)), - workspace: self.workspace.clone(), - }; - - self.register_mouse_listeners( - layout.mode, - &layout.hitbox, - &layout.content_mode, - window, - ); - if window.modifiers().secondary() - && bounds.contains(&window.mouse_position()) - && self.terminal_view.read(cx).hover.is_some() - { - window.set_cursor_style(gpui::CursorStyle::PointingHand, &layout.hitbox); - } else { - window.set_cursor_style(gpui::CursorStyle::IBeam, &layout.hitbox); - } - - let original_cursor = layout.cursor.take(); - let hyperlink_tooltip = layout.hyperlink_tooltip.take(); - let block_below_cursor_element = layout.block_below_cursor_element.take(); - self.interactivity.paint( - global_id, - inspector_id, - bounds, - Some(&layout.hitbox), - window, - cx, - |_, window, cx| { - window.handle_input(&self.focus, terminal_input_handler, cx); - - window.on_key_event({ - let this = self.terminal.clone(); - move |event: &ModifiersChangedEvent, phase, window, cx| { - if phase != DispatchPhase::Bubble { - return; - } - - this.update(cx, |term, cx| { - term.try_modifiers_change(&event.modifiers, window, cx) - }); - } - }); - - for rect in &layout.rects { - rect.paint(origin, &layout.dimensions, window); - } - - for (relative_highlighted_range, color) in -& layout.relative_highlighted_ranges - { - if let Some((start_y, highlighted_range_lines)) = - to_highlighted_range_lines(relative_highlighted_range, layout, origin) - { - let corner_radius = if EditorSettings::get_global(cx).rounded_selection { - 0.15 * layout.dimensions.line_height - } else { - Pixels::ZERO - }; - let hr = HighlightedRange { - start_y, - line_height: layout.dimensions.line_height, - lines: highlighted_range_lines, - color: *color, - corner_radius: corner_radius, - }; - hr.paint(true, bounds, window); - } - } - - // Paint batched text runs instead of individual cells - let text_paint_start = Instant::now(); - for batch in &layout.batched_text_runs { - batch.paint(origin, &layout.dimensions, window, cx); - } - let text_paint_time = text_paint_start.elapsed(); - - if let Some(text_to_mark) = &marked_text_cloned - && !text_to_mark.is_empty() - && let Some(cursor_layout) = &original_cursor { - let ime_position = cursor_layout.bounding_rect(origin).origin; - let mut ime_style = layout.base_text_style.clone(); - ime_style.underline = Some(UnderlineStyle { - color: Some(ime_style.color), - thickness: px(1.0), - wavy: false, - }); - - let shaped_line = window.text_system().shape_line( - text_to_mark.clone().into(), - ime_style.font_size.to_pixels(window.rem_size()), - &[TextRun { - len: text_to_mark.len(), - font: ime_style.font(), - color: ime_style.color, - underline: ime_style.underline, - ..Default::default() - }], - None - ); - shaped_line - .paint(ime_position, layout.dimensions.line_height, window, cx) - .log_err(); - } - - if self.cursor_visible && marked_text_cloned.is_none() - && let Some(mut cursor) = original_cursor { - cursor.paint(origin, window, cx); - } - - if let Some(mut element) = block_below_cursor_element { - element.paint(window, cx); - } - - if let Some(mut element) = hyperlink_tooltip { - element.paint(window, cx); - } - let total_paint_time = paint_start.elapsed(); - log::debug!( - "Terminal paint: {} text runs, {} rects, text paint took {:?}, total paint took {:?}", - layout.batched_text_runs.len(), - layout.rects.len(), - text_paint_time, - total_paint_time - ); - }, - ); - }); - } -} - -impl IntoElement for TerminalElement { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -struct TerminalInputHandler { - terminal: Entity, - terminal_view: Entity, - workspace: WeakEntity, - cursor_bounds: Option>, -} - -impl InputHandler for TerminalInputHandler { - fn selected_text_range( - &mut self, - _ignore_disabled_input: bool, - _: &mut Window, - cx: &mut App, - ) -> Option { - if self - .terminal - .read(cx) - .last_content - .mode - .contains(TermMode::ALT_SCREEN) - { - None - } else { - Some(UTF16Selection { - range: 0..0, - reversed: false, - }) - } - } - - fn marked_text_range( - &mut self, - _window: &mut Window, - cx: &mut App, - ) -> Option> { - self.terminal_view.read(cx).marked_text_range() - } - - fn text_for_range( - &mut self, - _: std::ops::Range, - _: &mut Option>, - _: &mut Window, - _: &mut App, - ) -> Option { - None - } - - fn replace_text_in_range( - &mut self, - _replacement_range: Option>, - text: &str, - window: &mut Window, - cx: &mut App, - ) { - self.terminal_view.update(cx, |view, view_cx| { - view.clear_marked_text(view_cx); - view.commit_text(text, view_cx); - }); - - self.workspace - .update(cx, |this, cx| { - window.invalidate_character_coordinates(); - let project = this.project().read(cx); - let telemetry = project.client().telemetry().clone(); - telemetry.log_edit_event("terminal", project.is_via_remote_server()); - }) - .ok(); - } - - fn replace_and_mark_text_in_range( - &mut self, - _range_utf16: Option>, - new_text: &str, - new_marked_range: Option>, - _window: &mut Window, - cx: &mut App, - ) { - self.terminal_view.update(cx, |view, view_cx| { - view.set_marked_text(new_text.to_string(), new_marked_range, view_cx); - }); - } - - fn unmark_text(&mut self, _window: &mut Window, cx: &mut App) { - self.terminal_view.update(cx, |view, view_cx| { - view.clear_marked_text(view_cx); - }); - } - - fn bounds_for_range( - &mut self, - range_utf16: std::ops::Range, - _window: &mut Window, - cx: &mut App, - ) -> Option> { - let term_bounds = self.terminal_view.read(cx).terminal_bounds(cx); - - let mut bounds = self.cursor_bounds?; - let offset_x = term_bounds.cell_width * range_utf16.start as f32; - bounds.origin.x += offset_x; - - Some(bounds) - } - - fn apple_press_and_hold_enabled(&mut self) -> bool { - false - } - - fn character_index_for_point( - &mut self, - _point: Point, - _window: &mut Window, - _cx: &mut App, - ) -> Option { - None - } -} - -pub fn is_blank(cell: &IndexedCell) -> bool { - if cell.c != ' ' { - return false; - } - - if cell.bg != AnsiColor::Named(NamedColor::Background) { - return false; - } - - if cell.hyperlink().is_some() { - return false; - } - - if cell - .flags - .intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT) - { - return false; - } - - true -} - -fn to_highlighted_range_lines( - range: &RangeInclusive, - layout: &LayoutState, - origin: Point, -) -> Option<(Pixels, Vec)> { - // Step 1. Normalize the points to be viewport relative. - // When display_offset = 1, here's how the grid is arranged: - //-2,0 -2,1... - //--- Viewport top - //-1,0 -1,1... - //--------- Terminal Top - // 0,0 0,1... - // 1,0 1,1... - //--- Viewport Bottom - // 2,0 2,1... - //--------- Terminal Bottom - - // Normalize to viewport relative, from terminal relative. - // lines are i32s, which are negative above the top left corner of the terminal - // If the user has scrolled, we use the display_offset to tell us which offset - // of the grid data we should be looking at. But for the rendering step, we don't - // want negatives. We want things relative to the 'viewport' (the area of the grid - // which is currently shown according to the display offset) - let unclamped_start = AlacPoint::new( - range.start().line + layout.display_offset, - range.start().column, - ); - let unclamped_end = - AlacPoint::new(range.end().line + layout.display_offset, range.end().column); - - // Step 2. Clamp range to viewport, and return None if it doesn't overlap - if unclamped_end.line.0 < 0 || unclamped_start.line.0 > layout.dimensions.num_lines() as i32 { - return None; - } - - let clamped_start_line = unclamped_start.line.0.max(0) as usize; - - let clamped_end_line = unclamped_end - .line - .0 - .min(layout.dimensions.num_lines() as i32) as usize; - - // Convert the start of the range to pixels - let start_y = origin.y + clamped_start_line as f32 * layout.dimensions.line_height; - - // Step 3. Expand ranges that cross lines into a collection of single-line ranges. - // (also convert to pixels) - let mut highlighted_range_lines = Vec::new(); - for line in clamped_start_line..=clamped_end_line { - let mut line_start = 0; - let mut line_end = layout.dimensions.columns(); - - if line == clamped_start_line && unclamped_start.line.0 >= 0 { - line_start = unclamped_start.column.0; - } - if line == clamped_end_line && unclamped_end.line.0 <= layout.dimensions.num_lines() as i32 - { - line_end = unclamped_end.column.0 + 1; // +1 for inclusive - } - - highlighted_range_lines.push(HighlightedRangeLine { - start_x: origin.x + line_start as f32 * layout.dimensions.cell_width, - end_x: origin.x + line_end as f32 * layout.dimensions.cell_width, - }); - } - - Some((start_y, highlighted_range_lines)) -} - -/// Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent. -pub fn convert_color(fg: &terminal::alacritty_terminal::vte::ansi::Color, theme: &Theme) -> Hsla { - let colors = theme.colors(); - match fg { - // Named and theme defined colors - terminal::alacritty_terminal::vte::ansi::Color::Named(n) => match n { - NamedColor::Black => colors.terminal_ansi_black, - NamedColor::Red => colors.terminal_ansi_red, - NamedColor::Green => colors.terminal_ansi_green, - NamedColor::Yellow => colors.terminal_ansi_yellow, - NamedColor::Blue => colors.terminal_ansi_blue, - NamedColor::Magenta => colors.terminal_ansi_magenta, - NamedColor::Cyan => colors.terminal_ansi_cyan, - NamedColor::White => colors.terminal_ansi_white, - NamedColor::BrightBlack => colors.terminal_ansi_bright_black, - NamedColor::BrightRed => colors.terminal_ansi_bright_red, - NamedColor::BrightGreen => colors.terminal_ansi_bright_green, - NamedColor::BrightYellow => colors.terminal_ansi_bright_yellow, - NamedColor::BrightBlue => colors.terminal_ansi_bright_blue, - NamedColor::BrightMagenta => colors.terminal_ansi_bright_magenta, - NamedColor::BrightCyan => colors.terminal_ansi_bright_cyan, - NamedColor::BrightWhite => colors.terminal_ansi_bright_white, - NamedColor::Foreground => colors.terminal_foreground, - NamedColor::Background => colors.terminal_ansi_background, - NamedColor::Cursor => theme.players().local().cursor, - NamedColor::DimBlack => colors.terminal_ansi_dim_black, - NamedColor::DimRed => colors.terminal_ansi_dim_red, - NamedColor::DimGreen => colors.terminal_ansi_dim_green, - NamedColor::DimYellow => colors.terminal_ansi_dim_yellow, - NamedColor::DimBlue => colors.terminal_ansi_dim_blue, - NamedColor::DimMagenta => colors.terminal_ansi_dim_magenta, - NamedColor::DimCyan => colors.terminal_ansi_dim_cyan, - NamedColor::DimWhite => colors.terminal_ansi_dim_white, - NamedColor::BrightForeground => colors.terminal_bright_foreground, - NamedColor::DimForeground => colors.terminal_dim_foreground, - }, - // 'True' colors - terminal::alacritty_terminal::vte::ansi::Color::Spec(rgb) => { - terminal::rgba_color(rgb.r, rgb.g, rgb.b) - } - // 8 bit, indexed colors - terminal::alacritty_terminal::vte::ansi::Color::Indexed(i) => { - terminal::get_color_at_index(*i as usize, theme) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::{AbsoluteLength, Hsla, font}; - use ui::utils::apca_contrast; - - #[test] - fn test_is_decorative_character() { - // Box Drawing characters (U+2500 to U+257F) - assert!(TerminalElement::is_decorative_character('─')); // U+2500 - assert!(TerminalElement::is_decorative_character('│')); // U+2502 - assert!(TerminalElement::is_decorative_character('┌')); // U+250C - assert!(TerminalElement::is_decorative_character('┐')); // U+2510 - assert!(TerminalElement::is_decorative_character('└')); // U+2514 - assert!(TerminalElement::is_decorative_character('┘')); // U+2518 - assert!(TerminalElement::is_decorative_character('┼')); // U+253C - - // Block Elements (U+2580 to U+259F) - assert!(TerminalElement::is_decorative_character('▀')); // U+2580 - assert!(TerminalElement::is_decorative_character('▄')); // U+2584 - assert!(TerminalElement::is_decorative_character('█')); // U+2588 - assert!(TerminalElement::is_decorative_character('░')); // U+2591 - assert!(TerminalElement::is_decorative_character('▒')); // U+2592 - assert!(TerminalElement::is_decorative_character('▓')); // U+2593 - - // Geometric Shapes - block/box-like subset (U+25A0 to U+25D7) - assert!(TerminalElement::is_decorative_character('■')); // U+25A0 - assert!(TerminalElement::is_decorative_character('□')); // U+25A1 - assert!(TerminalElement::is_decorative_character('▲')); // U+25B2 - assert!(TerminalElement::is_decorative_character('▼')); // U+25BC - assert!(TerminalElement::is_decorative_character('◆')); // U+25C6 - assert!(TerminalElement::is_decorative_character('●')); // U+25CF - - // The specific character from the issue - assert!(TerminalElement::is_decorative_character('◗')); // U+25D7 - assert!(TerminalElement::is_decorative_character('◘')); // U+25D8 (now included in Geometric Shapes) - assert!(TerminalElement::is_decorative_character('◙')); // U+25D9 (now included in Geometric Shapes) - - // Powerline symbols (Private Use Area) - assert!(TerminalElement::is_decorative_character('\u{E0B0}')); // Powerline right triangle - assert!(TerminalElement::is_decorative_character('\u{E0B2}')); // Powerline left triangle - assert!(TerminalElement::is_decorative_character('\u{E0B4}')); // Powerline right half circle (the actual issue!) - assert!(TerminalElement::is_decorative_character('\u{E0B6}')); // Powerline left half circle - assert!(TerminalElement::is_decorative_character('\u{E0CA}')); // Powerline mirrored ice waveform - assert!(TerminalElement::is_decorative_character('\u{E0D7}')); // Powerline left triangle inverted - - // Characters that should NOT be considered decorative - assert!(!TerminalElement::is_decorative_character('A')); // Regular letter - assert!(!TerminalElement::is_decorative_character('$')); // Symbol - assert!(!TerminalElement::is_decorative_character(' ')); // Space - assert!(!TerminalElement::is_decorative_character('←')); // U+2190 (Arrow, not in our ranges) - assert!(!TerminalElement::is_decorative_character('→')); // U+2192 (Arrow, not in our ranges) - assert!(!TerminalElement::is_decorative_character('\u{F00C}')); // Font Awesome check (icon, needs contrast) - assert!(!TerminalElement::is_decorative_character('\u{E711}')); // Devicons (icon, needs contrast) - assert!(!TerminalElement::is_decorative_character('\u{EA71}')); // Codicons folder (icon, needs contrast) - assert!(!TerminalElement::is_decorative_character('\u{F401}')); // Octicons (icon, needs contrast) - assert!(!TerminalElement::is_decorative_character('\u{1F600}')); // Emoji (not in our ranges) - } - - #[test] - fn test_decorative_character_boundary_cases() { - // Test exact boundaries of our ranges - // Box Drawing range boundaries - assert!(TerminalElement::is_decorative_character('\u{2500}')); // First char - assert!(TerminalElement::is_decorative_character('\u{257F}')); // Last char - assert!(!TerminalElement::is_decorative_character('\u{24FF}')); // Just before - - // Block Elements range boundaries - assert!(TerminalElement::is_decorative_character('\u{2580}')); // First char - assert!(TerminalElement::is_decorative_character('\u{259F}')); // Last char - - // Geometric Shapes subset boundaries - assert!(TerminalElement::is_decorative_character('\u{25A0}')); // First char - assert!(TerminalElement::is_decorative_character('\u{25FF}')); // Last char - assert!(!TerminalElement::is_decorative_character('\u{2600}')); // Just after - } - - #[test] - fn test_decorative_characters_bypass_contrast_adjustment() { - // Decorative characters should not be affected by contrast adjustment - - // The specific character from issue #34234 - let problematic_char = '◗'; // U+25D7 - assert!( - TerminalElement::is_decorative_character(problematic_char), - "Character ◗ (U+25D7) should be recognized as decorative" - ); - - // Verify some other commonly used decorative characters - assert!(TerminalElement::is_decorative_character('│')); // Vertical line - assert!(TerminalElement::is_decorative_character('─')); // Horizontal line - assert!(TerminalElement::is_decorative_character('█')); // Full block - assert!(TerminalElement::is_decorative_character('▓')); // Dark shade - assert!(TerminalElement::is_decorative_character('■')); // Black square - assert!(TerminalElement::is_decorative_character('●')); // Black circle - - // Verify normal text characters are NOT decorative - assert!(!TerminalElement::is_decorative_character('A')); - assert!(!TerminalElement::is_decorative_character('1')); - assert!(!TerminalElement::is_decorative_character('$')); - assert!(!TerminalElement::is_decorative_character(' ')); - } - - #[test] - fn test_contrast_adjustment_logic() { - // Test the core contrast adjustment logic without needing full app context - - // Test case 1: Light colors (poor contrast) - let white_fg = gpui::Hsla { - h: 0.0, - s: 0.0, - l: 1.0, - a: 1.0, - }; - let light_gray_bg = gpui::Hsla { - h: 0.0, - s: 0.0, - l: 0.95, - a: 1.0, - }; - - // Should have poor contrast - let actual_contrast = apca_contrast(white_fg, light_gray_bg).abs(); - assert!( - actual_contrast < 30.0, - "White on light gray should have poor APCA contrast: {}", - actual_contrast - ); - - // After adjustment with minimum APCA contrast of 45, should be darker - let adjusted = ensure_minimum_contrast(white_fg, light_gray_bg, 45.0); - assert!( - adjusted.l < white_fg.l, - "Adjusted color should be darker than original" - ); - let adjusted_contrast = apca_contrast(adjusted, light_gray_bg).abs(); - assert!(adjusted_contrast >= 45.0, "Should meet minimum contrast"); - - // Test case 2: Dark colors (poor contrast) - let black_fg = gpui::Hsla { - h: 0.0, - s: 0.0, - l: 0.0, - a: 1.0, - }; - let dark_gray_bg = gpui::Hsla { - h: 0.0, - s: 0.0, - l: 0.05, - a: 1.0, - }; - - // Should have poor contrast - let actual_contrast = apca_contrast(black_fg, dark_gray_bg).abs(); - assert!( - actual_contrast < 30.0, - "Black on dark gray should have poor APCA contrast: {}", - actual_contrast - ); - - // After adjustment with minimum APCA contrast of 45, should be lighter - let adjusted = ensure_minimum_contrast(black_fg, dark_gray_bg, 45.0); - assert!( - adjusted.l > black_fg.l, - "Adjusted color should be lighter than original" - ); - let adjusted_contrast = apca_contrast(adjusted, dark_gray_bg).abs(); - assert!(adjusted_contrast >= 45.0, "Should meet minimum contrast"); - - // Test case 3: Already good contrast - let good_contrast = ensure_minimum_contrast(black_fg, white_fg, 45.0); - assert_eq!( - good_contrast, black_fg, - "Good contrast should not be adjusted" - ); - } - - #[test] - fn test_white_on_white_contrast_issue() { - // This test reproduces the exact issue from the bug report - // where white ANSI text on white background should be adjusted - - // Simulate One Light theme colors - let white_fg = gpui::Hsla { - h: 0.0, - s: 0.0, - l: 0.98, // #fafafaff is approximately 98% lightness - a: 1.0, - }; - let white_bg = gpui::Hsla { - h: 0.0, - s: 0.0, - l: 0.98, // Same as foreground - this is the problem! - a: 1.0, - }; - - // With minimum contrast of 0.0, no adjustment should happen - let no_adjust = ensure_minimum_contrast(white_fg, white_bg, 0.0); - assert_eq!(no_adjust, white_fg, "No adjustment with min_contrast 0.0"); - - // With minimum APCA contrast of 15, it should adjust to a darker color - let adjusted = ensure_minimum_contrast(white_fg, white_bg, 15.0); - assert!( - adjusted.l < white_fg.l, - "White on white should become darker, got l={}", - adjusted.l - ); - - // Verify the contrast is now acceptable - let new_contrast = apca_contrast(adjusted, white_bg).abs(); - assert!( - new_contrast >= 15.0, - "Adjusted APCA contrast {} should be >= 15.0", - new_contrast - ); - } - - #[test] - fn test_batched_text_run_can_append() { - let style1 = TextRun { - len: 1, - font: font("Helvetica"), - color: Hsla::red(), - ..Default::default() - }; - - let style2 = TextRun { - len: 1, - font: font("Helvetica"), - color: Hsla::red(), - ..Default::default() - }; - - let style3 = TextRun { - len: 1, - font: font("Helvetica"), - color: Hsla::blue(), // Different color - ..Default::default() - }; - - let font_size = AbsoluteLength::Pixels(px(12.0)); - let batch = BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'a', style1, font_size); - - // Should be able to append same style - assert!(batch.can_append(&style2)); - - // Should not be able to append different style - assert!(!batch.can_append(&style3)); - } - - #[test] - fn test_batched_text_run_append() { - let style = TextRun { - len: 1, - font: font("Helvetica"), - color: Hsla::red(), - ..Default::default() - }; - - let font_size = AbsoluteLength::Pixels(px(12.0)); - let mut batch = BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'a', style, font_size); - - assert_eq!(batch.text, "a"); - assert_eq!(batch.cell_count, 1); - assert_eq!(batch.style.len, 1); - - batch.append_char('b'); - - assert_eq!(batch.text, "ab"); - assert_eq!(batch.cell_count, 2); - assert_eq!(batch.style.len, 2); - - batch.append_char('c'); - - assert_eq!(batch.text, "abc"); - assert_eq!(batch.cell_count, 3); - assert_eq!(batch.style.len, 3); - } - - #[test] - fn test_batched_text_run_append_char() { - let style = TextRun { - len: 1, - font: font("Helvetica"), - color: Hsla::red(), - ..Default::default() - }; - - let font_size = AbsoluteLength::Pixels(px(12.0)); - let mut batch = BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'x', style, font_size); - - assert_eq!(batch.text, "x"); - assert_eq!(batch.cell_count, 1); - assert_eq!(batch.style.len, 1); - - batch.append_char('y'); - - assert_eq!(batch.text, "xy"); - assert_eq!(batch.cell_count, 2); - assert_eq!(batch.style.len, 2); - - // Test with multi-byte character - batch.append_char('😀'); - - assert_eq!(batch.text, "xy😀"); - assert_eq!(batch.cell_count, 3); - assert_eq!(batch.style.len, 6); // 1 + 1 + 4 bytes for emoji - } - - #[test] - fn test_batched_text_run_append_zero_width_char() { - let style = TextRun { - len: 1, - font: font("Helvetica"), - color: Hsla::red(), - ..Default::default() - }; - - let font_size = AbsoluteLength::Pixels(px(12.0)); - let mut batch = BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'x', style, font_size); - - let combining = '\u{0301}'; - batch.append_zero_width_chars(&[combining]); - - assert_eq!(batch.text, format!("x{}", combining)); - assert_eq!(batch.cell_count, 1); - assert_eq!(batch.style.len, 1 + combining.len_utf8()); - } - - #[test] - fn test_background_region_can_merge() { - let color1 = Hsla::red(); - let color2 = Hsla::blue(); - - // Test horizontal merging - let mut region1 = BackgroundRegion::new(0, 0, color1); - region1.end_col = 5; - let region2 = BackgroundRegion::new(0, 6, color1); - assert!(region1.can_merge_with(®ion2)); - - // Test vertical merging with same column span - let mut region3 = BackgroundRegion::new(0, 0, color1); - region3.end_col = 5; - let mut region4 = BackgroundRegion::new(1, 0, color1); - region4.end_col = 5; - assert!(region3.can_merge_with(®ion4)); - - // Test cannot merge different colors - let region5 = BackgroundRegion::new(0, 0, color1); - let region6 = BackgroundRegion::new(0, 1, color2); - assert!(!region5.can_merge_with(®ion6)); - - // Test cannot merge non-adjacent regions - let region7 = BackgroundRegion::new(0, 0, color1); - let region8 = BackgroundRegion::new(0, 2, color1); - assert!(!region7.can_merge_with(®ion8)); - - // Test cannot merge vertical regions with different column spans - let mut region9 = BackgroundRegion::new(0, 0, color1); - region9.end_col = 5; - let mut region10 = BackgroundRegion::new(1, 0, color1); - region10.end_col = 6; - assert!(!region9.can_merge_with(®ion10)); - } - - #[test] - fn test_background_region_merge() { - let color = Hsla::red(); - - // Test horizontal merge - let mut region1 = BackgroundRegion::new(0, 0, color); - region1.end_col = 5; - let mut region2 = BackgroundRegion::new(0, 6, color); - region2.end_col = 10; - region1.merge_with(®ion2); - assert_eq!(region1.start_col, 0); - assert_eq!(region1.end_col, 10); - assert_eq!(region1.start_line, 0); - assert_eq!(region1.end_line, 0); - - // Test vertical merge - let mut region3 = BackgroundRegion::new(0, 0, color); - region3.end_col = 5; - let mut region4 = BackgroundRegion::new(1, 0, color); - region4.end_col = 5; - region3.merge_with(®ion4); - assert_eq!(region3.start_col, 0); - assert_eq!(region3.end_col, 5); - assert_eq!(region3.start_line, 0); - assert_eq!(region3.end_line, 1); - } - - #[test] - fn test_merge_background_regions() { - let color = Hsla::red(); - - // Test merging multiple adjacent regions - let regions = vec![ - BackgroundRegion::new(0, 0, color), - BackgroundRegion::new(0, 1, color), - BackgroundRegion::new(0, 2, color), - BackgroundRegion::new(1, 0, color), - BackgroundRegion::new(1, 1, color), - BackgroundRegion::new(1, 2, color), - ]; - - let merged = merge_background_regions(regions); - assert_eq!(merged.len(), 1); - assert_eq!(merged[0].start_line, 0); - assert_eq!(merged[0].end_line, 1); - assert_eq!(merged[0].start_col, 0); - assert_eq!(merged[0].end_col, 2); - - // Test with non-mergeable regions - let color2 = Hsla::blue(); - let regions2 = vec![ - BackgroundRegion::new(0, 0, color), - BackgroundRegion::new(0, 2, color), // Gap at column 1 - BackgroundRegion::new(1, 0, color2), // Different color - ]; - - let merged2 = merge_background_regions(regions2); - assert_eq!(merged2.len(), 3); - } -} diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs deleted file mode 100644 index ab89787fc8..0000000000 --- a/crates/terminal_view/src/terminal_panel.rs +++ /dev/null @@ -1,1978 +0,0 @@ -use std::{cmp, ops::ControlFlow, path::PathBuf, process::ExitStatus, sync::Arc, time::Duration}; - -use crate::{ - TerminalView, default_working_directory, - persistence::{ - SerializedItems, SerializedTerminalPanel, deserialize_terminal_panel, serialize_pane_group, - }, -}; -use breadcrumbs::Breadcrumbs; -use collections::HashMap; -use db::kvp::KEY_VALUE_STORE; -use futures::{channel::oneshot, future::join_all}; -use gpui::{ - Action, AnyView, App, AsyncApp, AsyncWindowContext, Context, Corner, Entity, EventEmitter, - ExternalPaths, FocusHandle, Focusable, IntoElement, ParentElement, Pixels, Render, Styled, - Task, WeakEntity, Window, actions, -}; -use itertools::Itertools; -use project::{Fs, Project, ProjectEntryId}; -use search::{BufferSearchBar, buffer_search::DivRegistrar}; -use settings::{Settings, TerminalDockPosition}; -use task::{RevealStrategy, RevealTarget, Shell, ShellBuilder, SpawnInTerminal, TaskId}; -use terminal::{Terminal, terminal_settings::TerminalSettings}; -use ui::{ - ButtonLike, Clickable, ContextMenu, FluentBuilder, PopoverMenu, SplitButton, Toggleable, - Tooltip, prelude::*, -}; -use util::{ResultExt, TryFutureExt}; -use workspace::{ - ActivateNextPane, ActivatePane, ActivatePaneDown, ActivatePaneLeft, ActivatePaneRight, - ActivatePaneUp, ActivatePreviousPane, DraggedSelection, DraggedTab, ItemId, MoveItemToPane, - MoveItemToPaneInDirection, MovePaneDown, MovePaneLeft, MovePaneRight, MovePaneUp, NewTerminal, - Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitRight, SplitUp, SwapPaneDown, - SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom, Workspace, - dock::{DockPosition, Panel, PanelEvent, PanelHandle}, - item::SerializableItem, - move_active_item, move_item, pane, -}; - -use anyhow::{Result, anyhow}; -use zed_actions::assistant::InlineAssist; - -const TERMINAL_PANEL_KEY: &str = "TerminalPanel"; - -actions!( - terminal_panel, - [ - /// Toggles the terminal panel. - Toggle, - /// Toggles focus on the terminal panel. - ToggleFocus - ] -); - -pub fn init(cx: &mut App) { - cx.observe_new( - |workspace: &mut Workspace, _window, _: &mut Context| { - workspace.register_action(TerminalPanel::new_terminal); - workspace.register_action(TerminalPanel::open_terminal); - workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { - if is_enabled_in_workspace(workspace, cx) { - workspace.toggle_panel_focus::(window, cx); - } - }); - workspace.register_action(|workspace, _: &Toggle, window, cx| { - if is_enabled_in_workspace(workspace, cx) { - if !workspace.toggle_panel_focus::(window, cx) { - workspace.close_panel::(window, cx); - } - } - }); - }, - ) - .detach(); -} - -pub struct TerminalPanel { - pub(crate) active_pane: Entity, - pub(crate) center: PaneGroup, - fs: Arc, - workspace: WeakEntity, - pub(crate) width: Option, - pub(crate) height: Option, - pending_serialization: Task>, - pending_terminals_to_add: usize, - deferred_tasks: HashMap>, - assistant_enabled: bool, - assistant_tab_bar_button: Option, - active: bool, -} - -impl TerminalPanel { - pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context) -> Self { - let project = workspace.project(); - let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, window, cx); - let center = PaneGroup::new(pane.clone()); - let terminal_panel = Self { - center, - active_pane: pane, - fs: workspace.app_state().fs.clone(), - workspace: workspace.weak_handle(), - pending_serialization: Task::ready(None), - width: None, - height: None, - pending_terminals_to_add: 0, - deferred_tasks: HashMap::default(), - assistant_enabled: false, - assistant_tab_bar_button: None, - active: false, - }; - terminal_panel.apply_tab_bar_buttons(&terminal_panel.active_pane, cx); - terminal_panel - } - - pub fn set_assistant_enabled(&mut self, enabled: bool, cx: &mut Context) { - self.assistant_enabled = enabled; - if enabled { - let focus_handle = self - .active_pane - .read(cx) - .active_item() - .map(|item| item.item_focus_handle(cx)) - .unwrap_or(self.focus_handle(cx)); - self.assistant_tab_bar_button = Some( - cx.new(move |_| InlineAssistTabBarButton { focus_handle }) - .into(), - ); - } else { - self.assistant_tab_bar_button = None; - } - for pane in self.center.panes() { - self.apply_tab_bar_buttons(pane, cx); - } - } - - fn apply_tab_bar_buttons(&self, terminal_pane: &Entity, cx: &mut Context) { - let assistant_tab_bar_button = self.assistant_tab_bar_button.clone(); - terminal_pane.update(cx, |pane, cx| { - pane.set_render_tab_bar_buttons(cx, move |pane, window, cx| { - let split_context = pane - .active_item() - .and_then(|item| item.downcast::()) - .map(|terminal_view| terminal_view.read(cx).focus_handle.clone()); - if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) { - return (None, None); - } - let focus_handle = pane.focus_handle(cx); - let right_children = h_flex() - .gap(DynamicSpacing::Base02.rems(cx)) - .child( - PopoverMenu::new("terminal-tab-bar-popover-menu") - .trigger_with_tooltip( - IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small), - Tooltip::text("New…"), - ) - .anchor(Corner::TopRight) - .with_handle(pane.new_item_context_menu_handle.clone()) - .menu(move |window, cx| { - let focus_handle = focus_handle.clone(); - let menu = ContextMenu::build(window, cx, |menu, _, _| { - menu.context(focus_handle.clone()) - .action( - "New Terminal", - workspace::NewTerminal.boxed_clone(), - ) - // We want the focus to go back to terminal panel once task modal is dismissed, - // hence we focus that first. Otherwise, we'd end up without a focused element, as - // context menu will be gone the moment we spawn the modal. - .action( - "Spawn Task", - zed_actions::Spawn::modal().boxed_clone(), - ) - }); - - Some(menu) - }), - ) - .children(assistant_tab_bar_button.clone()) - .child( - PopoverMenu::new("terminal-pane-tab-bar-split") - .trigger_with_tooltip( - IconButton::new("terminal-pane-split", IconName::Split) - .icon_size(IconSize::Small), - Tooltip::text("Split Pane"), - ) - .anchor(Corner::TopRight) - .with_handle(pane.split_item_context_menu_handle.clone()) - .menu({ - move |window, cx| { - ContextMenu::build(window, cx, |menu, _, _| { - menu.when_some( - split_context.clone(), - |menu, split_context| menu.context(split_context), - ) - .action("Split Right", SplitRight.boxed_clone()) - .action("Split Left", SplitLeft.boxed_clone()) - .action("Split Up", SplitUp.boxed_clone()) - .action("Split Down", SplitDown.boxed_clone()) - }) - .into() - } - }), - ) - .child({ - let zoomed = pane.is_zoomed(); - IconButton::new("toggle_zoom", IconName::Maximize) - .icon_size(IconSize::Small) - .toggle_state(zoomed) - .selected_icon(IconName::Minimize) - .on_click(cx.listener(|pane, _, window, cx| { - pane.toggle_zoom(&workspace::ToggleZoom, window, cx); - })) - .tooltip(move |_window, cx| { - Tooltip::for_action( - if zoomed { "Zoom Out" } else { "Zoom In" }, - &ToggleZoom, - cx, - ) - }) - }) - .into_any_element() - .into(); - (None, right_children) - }); - }); - } - - fn serialization_key(workspace: &Workspace) -> Option { - workspace - .database_id() - .map(|id| i64::from(id).to_string()) - .or(workspace.session_id()) - .map(|id| format!("{:?}-{:?}", TERMINAL_PANEL_KEY, id)) - } - - pub async fn load( - workspace: WeakEntity, - mut cx: AsyncWindowContext, - ) -> Result> { - let mut terminal_panel = None; - - if let Some((database_id, serialization_key)) = workspace - .read_with(&cx, |workspace, _| { - workspace - .database_id() - .zip(TerminalPanel::serialization_key(workspace)) - }) - .ok() - .flatten() - && let Some(serialized_panel) = cx - .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) }) - .await - .log_err() - .flatten() - .map(|panel| serde_json::from_str::(&panel)) - .transpose() - .log_err() - .flatten() - && let Ok(serialized) = workspace - .update_in(&mut cx, |workspace, window, cx| { - deserialize_terminal_panel( - workspace.weak_handle(), - workspace.project().clone(), - database_id, - serialized_panel, - window, - cx, - ) - })? - .await - { - terminal_panel = Some(serialized); - } - - let terminal_panel = if let Some(panel) = terminal_panel { - panel - } else { - workspace.update_in(&mut cx, |workspace, window, cx| { - cx.new(|cx| TerminalPanel::new(workspace, window, cx)) - })? - }; - - if let Some(workspace) = workspace.upgrade() { - workspace - .update(&mut cx, |workspace, _| { - workspace.set_terminal_provider(TerminalProvider(terminal_panel.clone())) - }) - .ok(); - } - - // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace. - if let Some(workspace) = workspace.upgrade() { - let cleanup_task = workspace.update_in(&mut cx, |workspace, window, cx| { - let alive_item_ids = terminal_panel - .read(cx) - .center - .panes() - .into_iter() - .flat_map(|pane| pane.read(cx).items()) - .map(|item| item.item_id().as_u64() as ItemId) - .collect(); - workspace.database_id().map(|workspace_id| { - TerminalView::cleanup(workspace_id, alive_item_ids, window, cx) - }) - })?; - if let Some(task) = cleanup_task { - task.await.log_err(); - } - } - - if let Some(workspace) = workspace.upgrade() { - let should_focus = workspace - .update_in(&mut cx, |workspace, window, cx| { - workspace.active_item(cx).is_none() - && workspace - .is_dock_at_position_open(terminal_panel.position(window, cx), cx) - }) - .unwrap_or(false); - - if should_focus { - terminal_panel - .update_in(&mut cx, |panel, window, cx| { - panel.active_pane.update(cx, |pane, cx| { - pane.focus_active_item(window, cx); - }); - }) - .ok(); - } - } - Ok(terminal_panel) - } - - fn handle_pane_event( - &mut self, - pane: &Entity, - event: &pane::Event, - window: &mut Window, - cx: &mut Context, - ) { - match event { - pane::Event::ActivateItem { .. } => self.serialize(cx), - pane::Event::RemovedItem { .. } => self.serialize(cx), - pane::Event::Remove { focus_on_pane } => { - let pane_count_before_removal = self.center.panes().len(); - let _removal_result = self.center.remove(pane); - if pane_count_before_removal == 1 { - self.center.first_pane().update(cx, |pane, cx| { - pane.set_zoomed(false, cx); - }); - cx.emit(PanelEvent::Close); - } else if let Some(focus_on_pane) = - focus_on_pane.as_ref().or_else(|| self.center.panes().pop()) - { - focus_on_pane.focus_handle(cx).focus(window); - } - } - pane::Event::ZoomIn => { - for pane in self.center.panes() { - pane.update(cx, |pane, cx| { - pane.set_zoomed(true, cx); - }) - } - cx.emit(PanelEvent::ZoomIn); - cx.notify(); - } - pane::Event::ZoomOut => { - for pane in self.center.panes() { - pane.update(cx, |pane, cx| { - pane.set_zoomed(false, cx); - }) - } - cx.emit(PanelEvent::ZoomOut); - cx.notify(); - } - pane::Event::AddItem { item } => { - if let Some(workspace) = self.workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - item.added_to_pane(workspace, pane.clone(), window, cx) - }) - } - self.serialize(cx); - } - &pane::Event::Split { - direction, - clone_active_item, - } => { - if clone_active_item { - let fut = self.new_pane_with_cloned_active_terminal(window, cx); - let pane = pane.clone(); - cx.spawn_in(window, async move |panel, cx| { - let Some(new_pane) = fut.await else { - return; - }; - panel - .update_in(cx, |panel, window, cx| { - panel.center.split(&pane, &new_pane, direction).log_err(); - window.focus(&new_pane.focus_handle(cx)); - }) - .ok(); - }) - .detach(); - } else { - let Some(item) = pane.update(cx, |pane, cx| pane.take_active_item(window, cx)) - else { - return; - }; - let Ok(project) = self - .workspace - .update(cx, |workspace, _| workspace.project().clone()) - else { - return; - }; - let new_pane = - new_terminal_pane(self.workspace.clone(), project, false, window, cx); - new_pane.update(cx, |pane, cx| { - pane.add_item(item, true, true, None, window, cx); - }); - self.center.split(&pane, &new_pane, direction).log_err(); - window.focus(&new_pane.focus_handle(cx)); - } - } - pane::Event::Focus => { - self.active_pane = pane.clone(); - } - pane::Event::ItemPinned | pane::Event::ItemUnpinned => { - self.serialize(cx); - } - - _ => {} - } - } - - fn new_pane_with_cloned_active_terminal( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let Some(workspace) = self.workspace.upgrade() else { - return Task::ready(None); - }; - let workspace = workspace.read(cx); - let database_id = workspace.database_id(); - let weak_workspace = self.workspace.clone(); - let project = workspace.project().clone(); - let active_pane = &self.active_pane; - let terminal_view = active_pane - .read(cx) - .active_item() - .and_then(|item| item.downcast::()); - let working_directory = terminal_view - .as_ref() - .and_then(|terminal_view| { - terminal_view - .read(cx) - .terminal() - .read(cx) - .working_directory() - }) - .or_else(|| default_working_directory(workspace, cx)); - let is_zoomed = active_pane.read(cx).is_zoomed(); - cx.spawn_in(window, async move |panel, cx| { - let terminal = project - .update(cx, |project, cx| match terminal_view { - Some(view) => project.clone_terminal( - &view.read(cx).terminal.clone(), - cx, - working_directory, - ), - None => project.create_terminal_shell(working_directory, cx), - }) - .ok()? - .await - .log_err()?; - - panel - .update_in(cx, move |terminal_panel, window, cx| { - let terminal_view = Box::new(cx.new(|cx| { - TerminalView::new( - terminal.clone(), - weak_workspace.clone(), - database_id, - project.downgrade(), - window, - cx, - ) - })); - let pane = new_terminal_pane(weak_workspace, project, is_zoomed, window, cx); - terminal_panel.apply_tab_bar_buttons(&pane, cx); - pane.update(cx, |pane, cx| { - pane.add_item(terminal_view, true, true, None, window, cx); - }); - Some(pane) - }) - .ok() - .flatten() - }) - } - - pub fn open_terminal( - workspace: &mut Workspace, - action: &workspace::OpenTerminal, - window: &mut Window, - cx: &mut Context, - ) { - let Some(terminal_panel) = workspace.panel::(cx) else { - return; - }; - - terminal_panel - .update(cx, |panel, cx| { - panel.add_terminal_shell( - Some(action.working_directory.clone()), - RevealStrategy::Always, - window, - cx, - ) - }) - .detach_and_log_err(cx); - } - - pub fn spawn_task( - &mut self, - task: &SpawnInTerminal, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let Some(workspace) = self.workspace.upgrade() else { - return Task::ready(Err(anyhow!("failed to read workspace"))); - }; - - let project = workspace.read(cx).project().read(cx); - - if project.is_via_collab() { - return Task::ready(Err(anyhow!("cannot spawn tasks as a guest"))); - } - - let remote_client = project.remote_client(); - let is_windows = project.path_style(cx).is_windows(); - let remote_shell = remote_client - .as_ref() - .and_then(|remote_client| remote_client.read(cx).shell()); - - let shell = if let Some(remote_shell) = remote_shell - && task.shell == Shell::System - { - Shell::Program(remote_shell) - } else { - task.shell.clone() - }; - - let builder = ShellBuilder::new(&shell, is_windows); - let command_label = builder.command_label(task.command.as_deref().unwrap_or("")); - let (command, args) = builder.build_no_quote(task.command.clone(), &task.args); - - let task = SpawnInTerminal { - command_label, - command: Some(command), - args, - ..task.clone() - }; - - if task.allow_concurrent_runs && task.use_new_terminal { - return self.spawn_in_new_terminal(task, window, cx); - } - - let mut terminals_for_task = self.terminals_for_task(&task.full_label, cx); - let Some(existing) = terminals_for_task.pop() else { - return self.spawn_in_new_terminal(task, window, cx); - }; - - let (existing_item_index, task_pane, existing_terminal) = existing; - if task.allow_concurrent_runs { - return self.replace_terminal( - task, - task_pane, - existing_item_index, - existing_terminal, - window, - cx, - ); - } - - let (tx, rx) = oneshot::channel(); - - self.deferred_tasks.insert( - task.id.clone(), - cx.spawn_in(window, async move |terminal_panel, cx| { - wait_for_terminals_tasks(terminals_for_task, cx).await; - let task = terminal_panel.update_in(cx, |terminal_panel, window, cx| { - if task.use_new_terminal { - terminal_panel.spawn_in_new_terminal(task, window, cx) - } else { - terminal_panel.replace_terminal( - task, - task_pane, - existing_item_index, - existing_terminal, - window, - cx, - ) - } - }); - if let Ok(task) = task { - tx.send(task.await).ok(); - } - }), - ); - - cx.spawn(async move |_, _| rx.await?) - } - - fn spawn_in_new_terminal( - &mut self, - spawn_task: SpawnInTerminal, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let reveal = spawn_task.reveal; - let reveal_target = spawn_task.reveal_target; - match reveal_target { - RevealTarget::Center => self - .workspace - .update(cx, |workspace, cx| { - Self::add_center_terminal(workspace, window, cx, |project, cx| { - project.create_terminal_task(spawn_task, cx) - }) - }) - .unwrap_or_else(|e| Task::ready(Err(e))), - RevealTarget::Dock => self.add_terminal_task(spawn_task, reveal, window, cx), - } - } - - /// Create a new Terminal in the current working directory or the user's home directory - fn new_terminal( - workspace: &mut Workspace, - _: &workspace::NewTerminal, - window: &mut Window, - cx: &mut Context, - ) { - let Some(terminal_panel) = workspace.panel::(cx) else { - return; - }; - - terminal_panel - .update(cx, |this, cx| { - this.add_terminal_shell( - default_working_directory(workspace, cx), - RevealStrategy::Always, - window, - cx, - ) - }) - .detach_and_log_err(cx); - } - - fn terminals_for_task( - &self, - label: &str, - cx: &mut App, - ) -> Vec<(usize, Entity, Entity)> { - let Some(workspace) = self.workspace.upgrade() else { - return Vec::new(); - }; - - let pane_terminal_views = |pane: Entity| { - pane.read(cx) - .items() - .enumerate() - .filter_map(|(index, item)| Some((index, item.act_as::(cx)?))) - .filter_map(|(index, terminal_view)| { - let task_state = terminal_view.read(cx).terminal().read(cx).task()?; - if &task_state.spawned_task.full_label == label { - Some((index, terminal_view)) - } else { - None - } - }) - .map(move |(index, terminal_view)| (index, pane.clone(), terminal_view)) - }; - - self.center - .panes() - .into_iter() - .cloned() - .flat_map(pane_terminal_views) - .chain( - workspace - .read(cx) - .panes() - .iter() - .cloned() - .flat_map(pane_terminal_views), - ) - .sorted_by_key(|(_, _, terminal_view)| terminal_view.entity_id()) - .collect() - } - - fn activate_terminal_view( - &self, - pane: &Entity, - item_index: usize, - focus: bool, - window: &mut Window, - cx: &mut App, - ) { - pane.update(cx, |pane, cx| { - pane.activate_item(item_index, true, focus, window, cx) - }) - } - - pub fn add_center_terminal( - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, - create_terminal: impl FnOnce( - &mut Project, - &mut Context, - ) -> Task>> - + 'static, - ) -> Task>> { - if !is_enabled_in_workspace(workspace, cx) { - return Task::ready(Err(anyhow!( - "terminal not yet supported for remote projects" - ))); - } - let project = workspace.project().downgrade(); - cx.spawn_in(window, async move |workspace, cx| { - let terminal = project.update(cx, create_terminal)?.await?; - - workspace.update_in(cx, |workspace, window, cx| { - let terminal_view = cx.new(|cx| { - TerminalView::new( - terminal.clone(), - workspace.weak_handle(), - workspace.database_id(), - workspace.project().downgrade(), - window, - cx, - ) - }); - workspace.add_item_to_active_pane(Box::new(terminal_view), None, true, window, cx); - })?; - Ok(terminal.downgrade()) - }) - } - - pub fn add_terminal_task( - &mut self, - task: SpawnInTerminal, - reveal_strategy: RevealStrategy, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let workspace = self.workspace.clone(); - cx.spawn_in(window, async move |terminal_panel, cx| { - if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? { - anyhow::bail!("terminal not yet supported for remote projects"); - } - let pane = terminal_panel.update(cx, |terminal_panel, _| { - terminal_panel.pending_terminals_to_add += 1; - terminal_panel.active_pane.clone() - })?; - let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?; - let terminal = project - .update(cx, |project, cx| project.create_terminal_task(task, cx))? - .await?; - let result = workspace.update_in(cx, |workspace, window, cx| { - let terminal_view = Box::new(cx.new(|cx| { - TerminalView::new( - terminal.clone(), - workspace.weak_handle(), - workspace.database_id(), - workspace.project().downgrade(), - window, - cx, - ) - })); - - match reveal_strategy { - RevealStrategy::Always => { - workspace.focus_panel::(window, cx); - } - RevealStrategy::NoFocus => { - workspace.open_panel::(window, cx); - } - RevealStrategy::Never => {} - } - - pane.update(cx, |pane, cx| { - let focus = pane.has_focus(window, cx) - || matches!(reveal_strategy, RevealStrategy::Always); - pane.add_item(terminal_view, true, focus, None, window, cx); - }); - - Ok(terminal.downgrade()) - })?; - terminal_panel.update(cx, |terminal_panel, cx| { - terminal_panel.pending_terminals_to_add = - terminal_panel.pending_terminals_to_add.saturating_sub(1); - terminal_panel.serialize(cx) - })?; - result - }) - } - - fn add_terminal_shell( - &mut self, - cwd: Option, - reveal_strategy: RevealStrategy, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let workspace = self.workspace.clone(); - - cx.spawn_in(window, async move |terminal_panel, cx| { - if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? { - anyhow::bail!("terminal not yet supported for collaborative projects"); - } - let pane = terminal_panel.update(cx, |terminal_panel, _| { - terminal_panel.pending_terminals_to_add += 1; - terminal_panel.active_pane.clone() - })?; - let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?; - let terminal = project - .update(cx, |project, cx| project.create_terminal_shell(cwd, cx))? - .await; - - match terminal { - Ok(terminal) => { - let result = workspace.update_in(cx, |workspace, window, cx| { - let terminal_view = Box::new(cx.new(|cx| { - TerminalView::new( - terminal.clone(), - workspace.weak_handle(), - workspace.database_id(), - workspace.project().downgrade(), - window, - cx, - ) - })); - - match reveal_strategy { - RevealStrategy::Always => { - workspace.focus_panel::(window, cx); - } - RevealStrategy::NoFocus => { - workspace.open_panel::(window, cx); - } - RevealStrategy::Never => {} - } - - pane.update(cx, |pane, cx| { - let focus = pane.has_focus(window, cx) - || matches!(reveal_strategy, RevealStrategy::Always); - pane.add_item(terminal_view, true, focus, None, window, cx); - }); - - Ok(terminal.downgrade()) - })?; - terminal_panel.update(cx, |terminal_panel, cx| { - terminal_panel.pending_terminals_to_add = - terminal_panel.pending_terminals_to_add.saturating_sub(1); - terminal_panel.serialize(cx) - })?; - result - } - Err(error) => { - pane.update_in(cx, |pane, window, cx| { - let focus = pane.has_focus(window, cx); - let failed_to_spawn = cx.new(|cx| FailedToSpawnTerminal { - error: error.to_string(), - focus_handle: cx.focus_handle(), - }); - pane.add_item(Box::new(failed_to_spawn), true, focus, None, window, cx); - })?; - Err(error) - } - } - }) - } - - fn serialize(&mut self, cx: &mut Context) { - let height = self.height; - let width = self.width; - let Some(serialization_key) = self - .workspace - .read_with(cx, |workspace, _| { - TerminalPanel::serialization_key(workspace) - }) - .ok() - .flatten() - else { - return; - }; - self.pending_serialization = cx.spawn(async move |terminal_panel, cx| { - cx.background_executor() - .timer(Duration::from_millis(50)) - .await; - let terminal_panel = terminal_panel.upgrade()?; - let items = terminal_panel - .update(cx, |terminal_panel, cx| { - SerializedItems::WithSplits(serialize_pane_group( - &terminal_panel.center, - &terminal_panel.active_pane, - cx, - )) - }) - .ok()?; - cx.background_spawn( - async move { - KEY_VALUE_STORE - .write_kvp( - serialization_key, - serde_json::to_string(&SerializedTerminalPanel { - items, - active_item_id: None, - height, - width, - })?, - ) - .await?; - anyhow::Ok(()) - } - .log_err(), - ) - .await; - Some(()) - }); - } - - fn replace_terminal( - &self, - spawn_task: SpawnInTerminal, - task_pane: Entity, - terminal_item_index: usize, - terminal_to_replace: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let reveal = spawn_task.reveal; - let reveal_target = spawn_task.reveal_target; - let task_workspace = self.workspace.clone(); - cx.spawn_in(window, async move |terminal_panel, cx| { - let project = terminal_panel.update(cx, |this, cx| { - this.workspace - .update(cx, |workspace, _| workspace.project().clone()) - })??; - let new_terminal = project - .update(cx, |project, cx| { - project.create_terminal_task(spawn_task, cx) - })? - .await?; - terminal_to_replace.update_in(cx, |terminal_to_replace, window, cx| { - terminal_to_replace.set_terminal(new_terminal.clone(), window, cx); - })?; - - match reveal { - RevealStrategy::Always => match reveal_target { - RevealTarget::Center => { - task_workspace.update_in(cx, |workspace, window, cx| { - let did_activate = workspace.activate_item( - &terminal_to_replace, - true, - true, - window, - cx, - ); - - anyhow::ensure!(did_activate, "Failed to retrieve terminal pane"); - - anyhow::Ok(()) - })??; - } - RevealTarget::Dock => { - terminal_panel.update_in(cx, |terminal_panel, window, cx| { - terminal_panel.activate_terminal_view( - &task_pane, - terminal_item_index, - true, - window, - cx, - ) - })?; - - cx.spawn(async move |cx| { - task_workspace - .update_in(cx, |workspace, window, cx| { - workspace.focus_panel::(window, cx) - }) - .ok() - }) - .detach(); - } - }, - RevealStrategy::NoFocus => match reveal_target { - RevealTarget::Center => { - task_workspace.update_in(cx, |workspace, window, cx| { - workspace.active_pane().focus_handle(cx).focus(window); - })?; - } - RevealTarget::Dock => { - terminal_panel.update_in(cx, |terminal_panel, window, cx| { - terminal_panel.activate_terminal_view( - &task_pane, - terminal_item_index, - false, - window, - cx, - ) - })?; - - cx.spawn(async move |cx| { - task_workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_panel::(window, cx) - }) - .ok() - }) - .detach(); - } - }, - RevealStrategy::Never => {} - } - - Ok(new_terminal.downgrade()) - }) - } - - fn has_no_terminals(&self, cx: &App) -> bool { - self.active_pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0 - } - - pub fn assistant_enabled(&self) -> bool { - self.assistant_enabled - } - - fn is_enabled(&self, cx: &App) -> bool { - self.workspace - .upgrade() - .is_some_and(|workspace| is_enabled_in_workspace(workspace.read(cx), cx)) - } - - fn activate_pane_in_direction( - &mut self, - direction: SplitDirection, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(pane) = self - .center - .find_pane_in_direction(&self.active_pane, direction, cx) - { - window.focus(&pane.focus_handle(cx)); - } else { - self.workspace - .update(cx, |workspace, cx| { - workspace.activate_pane_in_direction(direction, window, cx) - }) - .ok(); - } - } - - fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context) { - if let Some(to) = self - .center - .find_pane_in_direction(&self.active_pane, direction, cx) - .cloned() - { - self.center.swap(&self.active_pane, &to); - cx.notify(); - } - } - - fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context) { - if self - .center - .move_to_border(&self.active_pane, direction) - .unwrap() - { - cx.notify(); - } - } -} - -fn is_enabled_in_workspace(workspace: &Workspace, cx: &App) -> bool { - workspace.project().read(cx).supports_terminal(cx) -} - -pub fn new_terminal_pane( - workspace: WeakEntity, - project: Entity, - zoomed: bool, - window: &mut Window, - cx: &mut Context, -) -> Entity { - let is_local = project.read(cx).is_local(); - let terminal_panel = cx.entity(); - let pane = cx.new(|cx| { - let mut pane = Pane::new( - workspace.clone(), - project.clone(), - Default::default(), - None, - NewTerminal.boxed_clone(), - false, - window, - cx, - ); - pane.set_zoomed(zoomed, cx); - pane.set_can_navigate(false, cx); - pane.display_nav_history_buttons(None); - pane.set_should_display_tab_bar(|_, _| true); - pane.set_zoom_out_on_close(false); - - let split_closure_terminal_panel = terminal_panel.downgrade(); - pane.set_can_split(Some(Arc::new(move |pane, dragged_item, _window, cx| { - if let Some(tab) = dragged_item.downcast_ref::() { - let is_current_pane = tab.pane == cx.entity(); - let Some(can_drag_away) = split_closure_terminal_panel - .read_with(cx, |terminal_panel, _| { - let current_panes = terminal_panel.center.panes(); - !current_panes.contains(&&tab.pane) - || current_panes.len() > 1 - || (!is_current_pane || pane.items_len() > 1) - }) - .ok() - else { - return false; - }; - if can_drag_away { - let item = if is_current_pane { - pane.item_for_index(tab.ix) - } else { - tab.pane.read(cx).item_for_index(tab.ix) - }; - if let Some(item) = item { - return item.downcast::().is_some(); - } - } - } - false - }))); - - let buffer_search_bar = cx.new(|cx| { - search::BufferSearchBar::new(Some(project.read(cx).languages().clone()), window, cx) - }); - let breadcrumbs = cx.new(|_| Breadcrumbs::new()); - pane.toolbar().update(cx, |toolbar, cx| { - toolbar.add_item(buffer_search_bar, window, cx); - toolbar.add_item(breadcrumbs, window, cx); - }); - - let drop_closure_project = project.downgrade(); - let drop_closure_terminal_panel = terminal_panel.downgrade(); - pane.set_custom_drop_handle(cx, move |pane, dropped_item, window, cx| { - let Some(project) = drop_closure_project.upgrade() else { - return ControlFlow::Break(()); - }; - if let Some(tab) = dropped_item.downcast_ref::() { - let this_pane = cx.entity(); - let item = if tab.pane == this_pane { - pane.item_for_index(tab.ix) - } else { - tab.pane.read(cx).item_for_index(tab.ix) - }; - if let Some(item) = item { - if item.downcast::().is_some() { - let source = tab.pane.clone(); - let item_id_to_move = item.item_id(); - - let Ok(new_split_pane) = pane - .drag_split_direction() - .map(|split_direction| { - drop_closure_terminal_panel.update(cx, |terminal_panel, cx| { - let is_zoomed = if terminal_panel.active_pane == this_pane { - pane.is_zoomed() - } else { - terminal_panel.active_pane.read(cx).is_zoomed() - }; - let new_pane = new_terminal_pane( - workspace.clone(), - project.clone(), - is_zoomed, - window, - cx, - ); - terminal_panel.apply_tab_bar_buttons(&new_pane, cx); - terminal_panel.center.split( - &this_pane, - &new_pane, - split_direction, - )?; - anyhow::Ok(new_pane) - }) - }) - .transpose() - else { - return ControlFlow::Break(()); - }; - - match new_split_pane.transpose() { - // Source pane may be the one currently updated, so defer the move. - Ok(Some(new_pane)) => cx - .spawn_in(window, async move |_, cx| { - cx.update(|window, cx| { - move_item( - &source, - &new_pane, - item_id_to_move, - new_pane.read(cx).active_item_index(), - true, - window, - cx, - ); - }) - .ok(); - }) - .detach(), - // If we drop into existing pane or current pane, - // regular pane drop handler will take care of it, - // using the right tab index for the operation. - Ok(None) => return ControlFlow::Continue(()), - err @ Err(_) => { - err.log_err(); - return ControlFlow::Break(()); - } - }; - } else if let Some(project_path) = item.project_path(cx) - && let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx) - { - add_paths_to_terminal(pane, &[entry_path], window, cx); - } - } - } else if let Some(selection) = dropped_item.downcast_ref::() { - let project = project.read(cx); - let paths_to_add = selection - .items() - .map(|selected_entry| selected_entry.entry_id) - .filter_map(|entry_id| project.path_for_entry(entry_id, cx)) - .filter_map(|project_path| project.absolute_path(&project_path, cx)) - .collect::>(); - if !paths_to_add.is_empty() { - add_paths_to_terminal(pane, &paths_to_add, window, cx); - } - } else if let Some(&entry_id) = dropped_item.downcast_ref::() { - if let Some(entry_path) = project - .read(cx) - .path_for_entry(entry_id, cx) - .and_then(|project_path| project.read(cx).absolute_path(&project_path, cx)) - { - add_paths_to_terminal(pane, &[entry_path], window, cx); - } - } else if is_local && let Some(paths) = dropped_item.downcast_ref::() { - add_paths_to_terminal(pane, paths.paths(), window, cx); - } - - ControlFlow::Break(()) - }); - - pane - }); - - cx.subscribe_in(&pane, window, TerminalPanel::handle_pane_event) - .detach(); - cx.observe(&pane, |_, _, cx| cx.notify()).detach(); - - pane -} - -async fn wait_for_terminals_tasks( - terminals_for_task: Vec<(usize, Entity, Entity)>, - cx: &mut AsyncApp, -) { - let pending_tasks = terminals_for_task.iter().filter_map(|(_, _, terminal)| { - terminal - .update(cx, |terminal_view, cx| { - terminal_view - .terminal() - .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx)) - }) - .ok() - }); - join_all(pending_tasks).await; -} - -fn add_paths_to_terminal( - pane: &mut Pane, - paths: &[PathBuf], - window: &mut Window, - cx: &mut Context, -) { - if let Some(terminal_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - window.focus(&terminal_view.focus_handle(cx)); - let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join(""); - new_text.push(' '); - terminal_view.update(cx, |terminal_view, cx| { - terminal_view.terminal().update(cx, |terminal, _| { - terminal.paste(&new_text); - }); - }); - } -} - -struct FailedToSpawnTerminal { - error: String, - focus_handle: FocusHandle, -} - -impl Focusable for FailedToSpawnTerminal { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for FailedToSpawnTerminal { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let popover_menu = PopoverMenu::new("settings-popover") - .trigger( - IconButton::new("icon-button-popover", IconName::ChevronDown) - .icon_size(IconSize::XSmall), - ) - .menu(move |window, cx| { - Some(ContextMenu::build(window, cx, |context_menu, _, _| { - context_menu - .action("Open Settings", zed_actions::OpenSettings.boxed_clone()) - .action( - "Edit settings.json", - zed_actions::OpenSettingsFile.boxed_clone(), - ) - })) - }) - .anchor(Corner::TopRight) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }); - - v_flex() - .track_focus(&self.focus_handle) - .size_full() - .p_4() - .items_center() - .justify_center() - .bg(cx.theme().colors().editor_background) - .child( - v_flex() - .max_w_112() - .items_center() - .justify_center() - .text_center() - .child(Label::new("Failed to spawn terminal")) - .child( - Label::new(self.error.to_string()) - .size(LabelSize::Small) - .color(Color::Muted) - .mb_4(), - ) - .child(SplitButton::new( - ButtonLike::new("open-settings-ui") - .child(Label::new("Edit Settings").size(LabelSize::Small)) - .on_click(|_, window, cx| { - window.dispatch_action(zed_actions::OpenSettings.boxed_clone(), cx); - }), - popover_menu.into_any_element(), - )), - ) - } -} - -impl EventEmitter<()> for FailedToSpawnTerminal {} - -impl workspace::Item for FailedToSpawnTerminal { - type Event = (); - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - SharedString::new_static("Failed to spawn terminal") - } -} - -impl EventEmitter for TerminalPanel {} - -impl Render for TerminalPanel { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let mut registrar = DivRegistrar::new( - |panel, _, cx| { - panel - .active_pane - .read(cx) - .toolbar() - .read(cx) - .item_of_type::() - }, - cx, - ); - BufferSearchBar::register(&mut registrar); - let registrar = registrar.into_div(); - self.workspace - .update(cx, |workspace, cx| { - registrar.size_full().child(self.center.render( - workspace.zoomed_item(), - &workspace::PaneRenderContext { - follower_states: &HashMap::default(), - active_call: workspace.active_call(), - active_pane: &self.active_pane, - app_state: workspace.app_state(), - project: workspace.project(), - workspace: &workspace.weak_handle(), - }, - window, - cx, - )) - }) - .ok() - .map(|div| { - div.on_action({ - cx.listener(|terminal_panel, _: &ActivatePaneLeft, window, cx| { - terminal_panel.activate_pane_in_direction(SplitDirection::Left, window, cx); - }) - }) - .on_action({ - cx.listener(|terminal_panel, _: &ActivatePaneRight, window, cx| { - terminal_panel.activate_pane_in_direction( - SplitDirection::Right, - window, - cx, - ); - }) - }) - .on_action({ - cx.listener(|terminal_panel, _: &ActivatePaneUp, window, cx| { - terminal_panel.activate_pane_in_direction(SplitDirection::Up, window, cx); - }) - }) - .on_action({ - cx.listener(|terminal_panel, _: &ActivatePaneDown, window, cx| { - terminal_panel.activate_pane_in_direction(SplitDirection::Down, window, cx); - }) - }) - .on_action( - cx.listener(|terminal_panel, _action: &ActivateNextPane, window, cx| { - let panes = terminal_panel.center.panes(); - if let Some(ix) = panes - .iter() - .position(|pane| **pane == terminal_panel.active_pane) - { - let next_ix = (ix + 1) % panes.len(); - window.focus(&panes[next_ix].focus_handle(cx)); - } - }), - ) - .on_action(cx.listener( - |terminal_panel, _action: &ActivatePreviousPane, window, cx| { - let panes = terminal_panel.center.panes(); - if let Some(ix) = panes - .iter() - .position(|pane| **pane == terminal_panel.active_pane) - { - let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1); - window.focus(&panes[prev_ix].focus_handle(cx)); - } - }, - )) - .on_action( - cx.listener(|terminal_panel, action: &ActivatePane, window, cx| { - let panes = terminal_panel.center.panes(); - if let Some(&pane) = panes.get(action.0) { - window.focus(&pane.read(cx).focus_handle(cx)); - } else { - let future = - terminal_panel.new_pane_with_cloned_active_terminal(window, cx); - cx.spawn_in(window, async move |terminal_panel, cx| { - if let Some(new_pane) = future.await { - _ = terminal_panel.update_in( - cx, - |terminal_panel, window, cx| { - terminal_panel - .center - .split( - &terminal_panel.active_pane, - &new_pane, - SplitDirection::Right, - ) - .log_err(); - let new_pane = new_pane.read(cx); - window.focus(&new_pane.focus_handle(cx)); - }, - ); - } - }) - .detach(); - } - }), - ) - .on_action(cx.listener(|terminal_panel, _: &SwapPaneLeft, _, cx| { - terminal_panel.swap_pane_in_direction(SplitDirection::Left, cx); - })) - .on_action(cx.listener(|terminal_panel, _: &SwapPaneRight, _, cx| { - terminal_panel.swap_pane_in_direction(SplitDirection::Right, cx); - })) - .on_action(cx.listener(|terminal_panel, _: &SwapPaneUp, _, cx| { - terminal_panel.swap_pane_in_direction(SplitDirection::Up, cx); - })) - .on_action(cx.listener(|terminal_panel, _: &SwapPaneDown, _, cx| { - terminal_panel.swap_pane_in_direction(SplitDirection::Down, cx); - })) - .on_action(cx.listener(|terminal_panel, _: &MovePaneLeft, _, cx| { - terminal_panel.move_pane_to_border(SplitDirection::Left, cx); - })) - .on_action(cx.listener(|terminal_panel, _: &MovePaneRight, _, cx| { - terminal_panel.move_pane_to_border(SplitDirection::Right, cx); - })) - .on_action(cx.listener(|terminal_panel, _: &MovePaneUp, _, cx| { - terminal_panel.move_pane_to_border(SplitDirection::Up, cx); - })) - .on_action(cx.listener(|terminal_panel, _: &MovePaneDown, _, cx| { - terminal_panel.move_pane_to_border(SplitDirection::Down, cx); - })) - .on_action( - cx.listener(|terminal_panel, action: &MoveItemToPane, window, cx| { - let Some(&target_pane) = - terminal_panel.center.panes().get(action.destination) - else { - return; - }; - move_active_item( - &terminal_panel.active_pane, - target_pane, - action.focus, - true, - window, - cx, - ); - }), - ) - .on_action(cx.listener( - |terminal_panel, action: &MoveItemToPaneInDirection, window, cx| { - let source_pane = &terminal_panel.active_pane; - if let Some(destination_pane) = terminal_panel - .center - .find_pane_in_direction(source_pane, action.direction, cx) - { - move_active_item( - source_pane, - destination_pane, - action.focus, - true, - window, - cx, - ); - }; - }, - )) - }) - .unwrap_or_else(|| div()) - } -} - -impl Focusable for TerminalPanel { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.active_pane.focus_handle(cx) - } -} - -impl Panel for TerminalPanel { - fn position(&self, _window: &Window, cx: &App) -> DockPosition { - match TerminalSettings::get_global(cx).dock { - TerminalDockPosition::Left => DockPosition::Left, - TerminalDockPosition::Bottom => DockPosition::Bottom, - TerminalDockPosition::Right => DockPosition::Right, - } - } - - fn position_is_valid(&self, _: DockPosition) -> bool { - true - } - - fn set_position( - &mut self, - position: DockPosition, - _window: &mut Window, - cx: &mut Context, - ) { - settings::update_settings_file(self.fs.clone(), cx, move |settings, _| { - let dock = match position { - DockPosition::Left => TerminalDockPosition::Left, - DockPosition::Bottom => TerminalDockPosition::Bottom, - DockPosition::Right => TerminalDockPosition::Right, - }; - settings.terminal.get_or_insert_default().dock = Some(dock); - }); - } - - fn size(&self, window: &Window, cx: &App) -> Pixels { - let settings = TerminalSettings::get_global(cx); - match self.position(window, cx) { - DockPosition::Left | DockPosition::Right => { - self.width.unwrap_or(settings.default_width) - } - DockPosition::Bottom => self.height.unwrap_or(settings.default_height), - } - } - - fn set_size(&mut self, size: Option, window: &mut Window, cx: &mut Context) { - match self.position(window, cx) { - DockPosition::Left | DockPosition::Right => self.width = size, - DockPosition::Bottom => self.height = size, - } - cx.notify(); - cx.defer_in(window, |this, _, cx| { - this.serialize(cx); - }) - } - - fn is_zoomed(&self, _window: &Window, cx: &App) -> bool { - self.active_pane.read(cx).is_zoomed() - } - - fn set_zoomed(&mut self, zoomed: bool, _: &mut Window, cx: &mut Context) { - for pane in self.center.panes() { - pane.update(cx, |pane, cx| { - pane.set_zoomed(zoomed, cx); - }) - } - cx.notify(); - } - - fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context) { - let old_active = self.active; - self.active = active; - if !active || old_active == active || !self.has_no_terminals(cx) { - return; - } - cx.defer_in(window, |this, window, cx| { - let Ok(kind) = this - .workspace - .update(cx, |workspace, cx| default_working_directory(workspace, cx)) - else { - return; - }; - - this.add_terminal_shell(kind, RevealStrategy::Always, window, cx) - .detach_and_log_err(cx) - }) - } - - fn icon_label(&self, _window: &Window, cx: &App) -> Option { - let count = self - .center - .panes() - .into_iter() - .map(|pane| pane.read(cx).items_len()) - .sum::(); - if count == 0 { - None - } else { - Some(count.to_string()) - } - } - - fn persistent_name() -> &'static str { - "TerminalPanel" - } - - fn panel_key() -> &'static str { - TERMINAL_PANEL_KEY - } - - fn icon(&self, _window: &Window, cx: &App) -> Option { - if (self.is_enabled(cx) || !self.has_no_terminals(cx)) - && TerminalSettings::get_global(cx).button - { - Some(IconName::TerminalAlt) - } else { - None - } - } - - fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> { - Some("Terminal Panel") - } - - fn toggle_action(&self) -> Box { - Box::new(ToggleFocus) - } - - fn pane(&self) -> Option> { - Some(self.active_pane.clone()) - } - - fn activation_priority(&self) -> u32 { - 1 - } -} - -struct TerminalProvider(Entity); - -impl workspace::TerminalProvider for TerminalProvider { - fn spawn( - &self, - task: SpawnInTerminal, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - let terminal_panel = self.0.clone(); - window.spawn(cx, async move |cx| { - let terminal = terminal_panel - .update_in(cx, |terminal_panel, window, cx| { - terminal_panel.spawn_task(&task, window, cx) - }) - .ok()? - .await; - match terminal { - Ok(terminal) => { - let exit_status = terminal - .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx)) - .ok()? - .await?; - Some(Ok(exit_status)) - } - Err(e) => Some(Err(e)), - } - }) - } -} - -struct InlineAssistTabBarButton { - focus_handle: FocusHandle, -} - -impl Render for InlineAssistTabBarButton { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let focus_handle = self.focus_handle.clone(); - IconButton::new("terminal_inline_assistant", IconName::ZedAssistant) - .icon_size(IconSize::Small) - .on_click(cx.listener(|_, _, window, cx| { - window.dispatch_action(InlineAssist::default().boxed_clone(), cx); - })) - .tooltip(move |_window, cx| { - Tooltip::for_action_in("Inline Assist", &InlineAssist::default(), &focus_handle, cx) - }) - } -} - -#[cfg(test)] -mod tests { - use std::num::NonZero; - - use super::*; - use gpui::{TestAppContext, UpdateGlobal as _}; - use pretty_assertions::assert_eq; - use project::FakeFs; - use settings::SettingsStore; - - #[gpui::test] - async fn test_spawn_an_empty_task(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - - let (window_handle, terminal_panel) = workspace - .update(cx, |workspace, window, cx| { - let window_handle = window.window_handle(); - let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx)); - (window_handle, terminal_panel) - }) - .unwrap(); - - let task = window_handle - .update(cx, |_, window, cx| { - terminal_panel.update(cx, |terminal_panel, cx| { - terminal_panel.spawn_task(&SpawnInTerminal::default(), window, cx) - }) - }) - .unwrap(); - - let terminal = task.await.unwrap(); - let expected_shell = util::get_system_shell(); - terminal - .update(cx, |terminal, _| { - let task_metadata = terminal - .task() - .expect("When spawning a task, should have the task metadata") - .spawned_task - .clone(); - assert_eq!(task_metadata.env, HashMap::default()); - assert_eq!(task_metadata.cwd, None); - assert_eq!(task_metadata.shell, task::Shell::System); - assert_eq!( - task_metadata.command, - Some(expected_shell.clone()), - "Empty tasks should spawn a -i shell" - ); - assert_eq!(task_metadata.args, Vec::::new()); - assert_eq!( - task_metadata.command_label, expected_shell, - "We show the shell launch for empty commands" - ); - }) - .unwrap(); - } - - #[gpui::test] - async fn test_bypass_max_tabs_limit(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - - let (window_handle, terminal_panel) = workspace - .update(cx, |workspace, window, cx| { - let window_handle = window.window_handle(); - let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx)); - (window_handle, terminal_panel) - }) - .unwrap(); - - set_max_tabs(cx, Some(3)); - - for _ in 0..5 { - let task = window_handle - .update(cx, |_, window, cx| { - terminal_panel.update(cx, |panel, cx| { - panel.add_terminal_shell(None, RevealStrategy::Always, window, cx) - }) - }) - .unwrap(); - task.await.unwrap(); - } - - cx.run_until_parked(); - - let item_count = - terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len()); - - assert_eq!( - item_count, 5, - "Terminal panel should bypass max_tabs limit and have all 5 terminals" - ); - } - - // A complex Unix command won't be properly parsed by the Windows terminal hence omit the test there. - #[cfg(unix)] - #[gpui::test] - async fn test_spawn_script_like_task(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - - let (window_handle, terminal_panel) = workspace - .update(cx, |workspace, window, cx| { - let window_handle = window.window_handle(); - let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx)); - (window_handle, terminal_panel) - }) - .unwrap(); - - let user_command = r#"REPO_URL=$(git remote get-url origin | sed -e \"s/^git@\\(.*\\):\\(.*\\)\\.git$/https:\\/\\/\\1\\/\\2/\"); COMMIT_SHA=$(git log -1 --format=\"%H\" -- \"${ZED_RELATIVE_FILE}\"); echo \"${REPO_URL}/blob/${COMMIT_SHA}/${ZED_RELATIVE_FILE}#L${ZED_ROW}-$(echo $(($(wc -l <<< \"$ZED_SELECTED_TEXT\") + $ZED_ROW - 1)))\" | xclip -selection clipboard"#.to_string(); - - let expected_cwd = PathBuf::from("/some/work"); - let task = window_handle - .update(cx, |_, window, cx| { - terminal_panel.update(cx, |terminal_panel, cx| { - terminal_panel.spawn_task( - &SpawnInTerminal { - command: Some(user_command.clone()), - cwd: Some(expected_cwd.clone()), - ..SpawnInTerminal::default() - }, - window, - cx, - ) - }) - }) - .unwrap(); - - let terminal = task.await.unwrap(); - let shell = util::get_system_shell(); - terminal - .update(cx, |terminal, _| { - let task_metadata = terminal - .task() - .expect("When spawning a task, should have the task metadata") - .spawned_task - .clone(); - assert_eq!(task_metadata.env, HashMap::default()); - assert_eq!(task_metadata.cwd, Some(expected_cwd)); - assert_eq!(task_metadata.shell, task::Shell::System); - assert_eq!(task_metadata.command, Some(shell.clone())); - assert_eq!( - task_metadata.args, - vec!["-i".to_string(), "-c".to_string(), user_command.clone(),], - "Use command should have been moved into the arguments, as we're spawning a new -i shell", - ); - assert_eq!( - task_metadata.command_label, - format!("{shell} {interactive}-c '{user_command}'", interactive = if cfg!(windows) {""} else {"-i "}), - "We want to show to the user the entire command spawned"); - }) - .unwrap(); - } - - #[gpui::test] - async fn renders_error_if_default_shell_fails(cx: &mut TestAppContext) { - init_test(cx); - - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.terminal.get_or_insert_default().project.shell = - Some(settings::Shell::Program("asdf".to_owned())); - }); - }); - }); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - - let (window_handle, terminal_panel) = workspace - .update(cx, |workspace, window, cx| { - let window_handle = window.window_handle(); - let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx)); - (window_handle, terminal_panel) - }) - .unwrap(); - - window_handle - .update(cx, |_, window, cx| { - terminal_panel.update(cx, |terminal_panel, cx| { - terminal_panel.add_terminal_shell(None, RevealStrategy::Always, window, cx) - }) - }) - .unwrap() - .await - .unwrap_err(); - - window_handle - .update(cx, |_, _, cx| { - terminal_panel.update(cx, |terminal_panel, cx| { - assert!( - terminal_panel - .active_pane - .read(cx) - .items() - .any(|item| item.downcast::().is_some()), - "should spawn `FailedToSpawnTerminal` pane" - ); - }) - }) - .unwrap(); - } - - fn set_max_tabs(cx: &mut TestAppContext, value: Option) { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| { - settings.workspace.max_tabs = value.map(|v| NonZero::new(v).unwrap()) - }); - }); - } - - pub fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let store = SettingsStore::test(cx); - cx.set_global(store); - theme::init(theme::LoadThemes::JustBase, cx); - editor::init(cx); - crate::init(cx); - }); - } -} diff --git a/crates/terminal_view/src/terminal_path_like_target.rs b/crates/terminal_view/src/terminal_path_like_target.rs deleted file mode 100644 index fa40196645..0000000000 --- a/crates/terminal_view/src/terminal_path_like_target.rs +++ /dev/null @@ -1,1338 +0,0 @@ -use super::{HoverTarget, HoveredWord, TerminalView}; -use anyhow::{Context as _, Result}; -use editor::Editor; -use gpui::{App, AppContext, Context, Task, WeakEntity, Window}; -use itertools::Itertools; -use project::{Entry, Metadata}; -use std::path::PathBuf; -use terminal::PathLikeTarget; -use util::{ - ResultExt, debug_panic, - paths::{PathStyle, PathWithPosition}, - rel_path::RelPath, -}; -use workspace::{OpenOptions, OpenVisible, Workspace}; - -/// The way we found the open target. This is important to have for test assertions. -/// For example, remote projects never look in the file system. -#[cfg(test)] -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -enum OpenTargetFoundBy { - WorktreeExact, - WorktreeScan, - FileSystemBackground, -} - -#[cfg(test)] -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -enum BackgroundFsChecks { - Enabled, - Disabled, -} - -#[derive(Debug, Clone)] -enum OpenTarget { - Worktree(PathWithPosition, Entry, #[cfg(test)] OpenTargetFoundBy), - File(PathWithPosition, Metadata), -} - -impl OpenTarget { - fn is_file(&self) -> bool { - match self { - OpenTarget::Worktree(_, entry, ..) => entry.is_file(), - OpenTarget::File(_, metadata) => !metadata.is_dir, - } - } - - fn is_dir(&self) -> bool { - match self { - OpenTarget::Worktree(_, entry, ..) => entry.is_dir(), - OpenTarget::File(_, metadata) => metadata.is_dir, - } - } - - fn path(&self) -> &PathWithPosition { - match self { - OpenTarget::Worktree(path, ..) => path, - OpenTarget::File(path, _) => path, - } - } - - #[cfg(test)] - fn found_by(&self) -> OpenTargetFoundBy { - match self { - OpenTarget::Worktree(.., found_by) => *found_by, - OpenTarget::File(..) => OpenTargetFoundBy::FileSystemBackground, - } - } -} - -pub(super) fn hover_path_like_target( - workspace: &WeakEntity, - hovered_word: HoveredWord, - path_like_target: &PathLikeTarget, - cx: &mut Context, -) -> Task<()> { - #[cfg(not(test))] - { - possible_hover_target(workspace, hovered_word, path_like_target, cx) - } - #[cfg(test)] - { - possible_hover_target( - workspace, - hovered_word, - path_like_target, - cx, - BackgroundFsChecks::Enabled, - ) - } -} - -fn possible_hover_target( - workspace: &WeakEntity, - hovered_word: HoveredWord, - path_like_target: &PathLikeTarget, - cx: &mut Context, - #[cfg(test)] background_fs_checks: BackgroundFsChecks, -) -> Task<()> { - let file_to_open_task = possible_open_target( - workspace, - path_like_target, - cx, - #[cfg(test)] - background_fs_checks, - ); - cx.spawn(async move |terminal_view, cx| { - let file_to_open = file_to_open_task.await; - terminal_view - .update(cx, |terminal_view, _| match file_to_open { - Some(OpenTarget::File(path, _) | OpenTarget::Worktree(path, ..)) => { - terminal_view.hover = Some(HoverTarget { - tooltip: path.to_string(|path| path.to_string_lossy().into_owned()), - hovered_word, - }); - } - None => { - terminal_view.hover = None; - } - }) - .ok(); - }) -} - -fn possible_open_target( - workspace: &WeakEntity, - path_like_target: &PathLikeTarget, - cx: &App, - #[cfg(test)] background_fs_checks: BackgroundFsChecks, -) -> Task> { - let Some(workspace) = workspace.upgrade() else { - return Task::ready(None); - }; - // We have to check for both paths, as on Unix, certain paths with positions are valid file paths too. - // We can be on FS remote part, without real FS, so cannot canonicalize or check for existence the path right away. - let mut potential_paths = Vec::new(); - let cwd = path_like_target.terminal_dir.as_ref(); - let maybe_path = &path_like_target.maybe_path; - let original_path = PathWithPosition::from_path(PathBuf::from(maybe_path)); - let path_with_position = PathWithPosition::parse_str(maybe_path); - let worktree_candidates = workspace - .read(cx) - .worktrees(cx) - .sorted_by_key(|worktree| { - let worktree_root = worktree.read(cx).abs_path(); - match cwd.and_then(|cwd| worktree_root.strip_prefix(cwd).ok()) { - Some(cwd_child) => cwd_child.components().count(), - None => usize::MAX, - } - }) - .collect::>(); - // Since we do not check paths via FS and joining, we need to strip off potential `./`, `a/`, `b/` prefixes out of it. - const GIT_DIFF_PATH_PREFIXES: &[&str] = &["a", "b"]; - for prefix_str in GIT_DIFF_PATH_PREFIXES.iter().chain(std::iter::once(&".")) { - if let Some(stripped) = original_path.path.strip_prefix(prefix_str).ok() { - potential_paths.push(PathWithPosition { - path: stripped.to_owned(), - row: original_path.row, - column: original_path.column, - }); - } - if let Some(stripped) = path_with_position.path.strip_prefix(prefix_str).ok() { - potential_paths.push(PathWithPosition { - path: stripped.to_owned(), - row: path_with_position.row, - column: path_with_position.column, - }); - } - } - - let insert_both_paths = original_path != path_with_position; - potential_paths.insert(0, original_path); - if insert_both_paths { - potential_paths.insert(1, path_with_position); - } - - // If we won't find paths "easily", we can traverse the entire worktree to look what ends with the potential path suffix. - // That will be slow, though, so do the fast checks first. - let mut worktree_paths_to_check = Vec::new(); - let mut is_cwd_in_worktree = false; - let mut open_target = None; - 'worktree_loop: for worktree in &worktree_candidates { - let worktree_root = worktree.read(cx).abs_path(); - let mut paths_to_check = Vec::with_capacity(potential_paths.len()); - let relative_cwd = cwd - .and_then(|cwd| cwd.strip_prefix(&worktree_root).ok()) - .and_then(|cwd| RelPath::new(cwd, PathStyle::local()).ok()) - .and_then(|cwd_stripped| { - (cwd_stripped.as_ref() != RelPath::empty()).then(|| { - is_cwd_in_worktree = true; - cwd_stripped - }) - }); - - for path_with_position in &potential_paths { - let path_to_check = if worktree_root.ends_with(&path_with_position.path) { - let root_path_with_position = PathWithPosition { - path: worktree_root.to_path_buf(), - row: path_with_position.row, - column: path_with_position.column, - }; - match worktree.read(cx).root_entry() { - Some(root_entry) => { - open_target = Some(OpenTarget::Worktree( - root_path_with_position, - root_entry.clone(), - #[cfg(test)] - OpenTargetFoundBy::WorktreeExact, - )); - break 'worktree_loop; - } - None => root_path_with_position, - } - } else { - PathWithPosition { - path: path_with_position - .path - .strip_prefix(&worktree_root) - .unwrap_or(&path_with_position.path) - .to_owned(), - row: path_with_position.row, - column: path_with_position.column, - } - }; - - if let Ok(relative_path_to_check) = - RelPath::new(&path_to_check.path, PathStyle::local()) - && !worktree.read(cx).is_single_file() - && let Some(entry) = relative_cwd - .clone() - .and_then(|relative_cwd| { - worktree - .read(cx) - .entry_for_path(&relative_cwd.join(&relative_path_to_check)) - }) - .or_else(|| worktree.read(cx).entry_for_path(&relative_path_to_check)) - { - open_target = Some(OpenTarget::Worktree( - PathWithPosition { - path: worktree.read(cx).absolutize(&entry.path), - row: path_to_check.row, - column: path_to_check.column, - }, - entry.clone(), - #[cfg(test)] - OpenTargetFoundBy::WorktreeExact, - )); - break 'worktree_loop; - } - - paths_to_check.push(path_to_check); - } - - if !paths_to_check.is_empty() { - worktree_paths_to_check.push((worktree.clone(), paths_to_check)); - } - } - - #[cfg(not(test))] - let enable_background_fs_checks = workspace.read(cx).project().read(cx).is_local(); - #[cfg(test)] - let enable_background_fs_checks = background_fs_checks == BackgroundFsChecks::Enabled; - - if open_target.is_some() { - // We we want to prefer open targets found via background fs checks over worktree matches, - // however we can return early if either: - // - This is a remote project, or - // - If the terminal working directory is inside of at least one worktree - if !enable_background_fs_checks || is_cwd_in_worktree { - return Task::ready(open_target); - } - } - - // Before entire worktree traversal(s), make an attempt to do FS checks if available. - let fs_paths_to_check = - if enable_background_fs_checks { - let fs_cwd_paths_to_check = cwd - .iter() - .flat_map(|cwd| { - let mut paths_to_check = Vec::new(); - for path_to_check in &potential_paths { - let maybe_path = &path_to_check.path; - if path_to_check.path.is_relative() { - paths_to_check.push(PathWithPosition { - path: cwd.join(&maybe_path), - row: path_to_check.row, - column: path_to_check.column, - }); - } - } - paths_to_check - }) - .collect::>(); - fs_cwd_paths_to_check - .into_iter() - .chain( - potential_paths - .into_iter() - .flat_map(|path_to_check| { - let mut paths_to_check = Vec::new(); - let maybe_path = &path_to_check.path; - if maybe_path.starts_with("~") { - if let Some(home_path) = maybe_path.strip_prefix("~").ok().and_then( - |stripped_maybe_path| { - Some(dirs::home_dir()?.join(stripped_maybe_path)) - }, - ) { - paths_to_check.push(PathWithPosition { - path: home_path, - row: path_to_check.row, - column: path_to_check.column, - }); - } - } else { - paths_to_check.push(PathWithPosition { - path: maybe_path.clone(), - row: path_to_check.row, - column: path_to_check.column, - }); - if maybe_path.is_relative() { - for worktree in &worktree_candidates { - if !worktree.read(cx).is_single_file() { - paths_to_check.push(PathWithPosition { - path: worktree.read(cx).abs_path().join(maybe_path), - row: path_to_check.row, - column: path_to_check.column, - }); - } - } - } - } - paths_to_check - }) - .collect::>(), - ) - .collect() - } else { - Vec::new() - }; - - let fs = workspace.read(cx).project().read(cx).fs().clone(); - let background_fs_checks_task = cx.background_spawn(async move { - for mut path_to_check in fs_paths_to_check { - if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok() - && let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten() - { - if open_target - .as_ref() - .map(|open_target| open_target.path().path != fs_path_to_check) - .unwrap_or(true) - { - path_to_check.path = fs_path_to_check; - return Some(OpenTarget::File(path_to_check, metadata)); - } - - break; - } - } - - open_target - }); - - cx.spawn(async move |cx| { - background_fs_checks_task.await.or_else(|| { - for (worktree, worktree_paths_to_check) in worktree_paths_to_check { - let found_entry = worktree - .update(cx, |worktree, _| -> Option { - let traversal = - worktree.traverse_from_path(true, true, false, RelPath::empty()); - for entry in traversal { - if let Some(path_in_worktree) = - worktree_paths_to_check.iter().find(|path_to_check| { - RelPath::new(&path_to_check.path, PathStyle::local()) - .is_ok_and(|path| entry.path.ends_with(&path)) - }) - { - return Some(OpenTarget::Worktree( - PathWithPosition { - path: worktree.absolutize(&entry.path), - row: path_in_worktree.row, - column: path_in_worktree.column, - }, - entry.clone(), - #[cfg(test)] - OpenTargetFoundBy::WorktreeScan, - )); - } - } - None - }) - .ok()?; - if let Some(found_entry) = found_entry { - return Some(found_entry); - } - } - None - }) - }) -} - -pub(super) fn open_path_like_target( - workspace: &WeakEntity, - terminal_view: &mut TerminalView, - path_like_target: &PathLikeTarget, - window: &mut Window, - cx: &mut Context, -) { - #[cfg(not(test))] - { - possibly_open_target(workspace, terminal_view, path_like_target, window, cx) - .detach_and_log_err(cx) - } - #[cfg(test)] - { - possibly_open_target( - workspace, - terminal_view, - path_like_target, - window, - cx, - BackgroundFsChecks::Enabled, - ) - .detach_and_log_err(cx) - } -} - -fn possibly_open_target( - workspace: &WeakEntity, - terminal_view: &mut TerminalView, - path_like_target: &PathLikeTarget, - window: &mut Window, - cx: &mut Context, - #[cfg(test)] background_fs_checks: BackgroundFsChecks, -) -> Task>> { - if terminal_view.hover.is_none() { - return Task::ready(Ok(None)); - } - let workspace = workspace.clone(); - let path_like_target = path_like_target.clone(); - cx.spawn_in(window, async move |terminal_view, cx| { - let Some(open_target) = terminal_view - .update(cx, |_, cx| { - possible_open_target( - &workspace, - &path_like_target, - cx, - #[cfg(test)] - background_fs_checks, - ) - })? - .await - else { - return Ok(None); - }; - - let path_to_open = open_target.path(); - let opened_items = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_paths( - vec![path_to_open.path.clone()], - OpenOptions { - visible: Some(OpenVisible::OnlyDirectories), - ..Default::default() - }, - None, - window, - cx, - ) - }) - .context("workspace update")? - .await; - if opened_items.len() != 1 { - debug_panic!( - "Received {} items for one path {path_to_open:?}", - opened_items.len(), - ); - } - - if let Some(opened_item) = opened_items.first() { - if open_target.is_file() { - if let Some(Ok(opened_item)) = opened_item { - if let Some(row) = path_to_open.row { - let col = path_to_open.column.unwrap_or(0); - if let Some(active_editor) = opened_item.downcast::() { - active_editor - .downgrade() - .update_in(cx, |editor, window, cx| { - editor.go_to_singleton_buffer_point( - language::Point::new( - row.saturating_sub(1), - col.saturating_sub(1), - ), - window, - cx, - ) - }) - .log_err(); - } - } - return Ok(Some(open_target)); - } - } else if open_target.is_dir() { - workspace.update(cx, |workspace, cx| { - workspace.project().update(cx, |_, cx| { - cx.emit(project::Event::ActivateProjectPanel); - }) - })?; - return Ok(Some(open_target)); - } - } - Ok(None) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::TestAppContext; - use project::Project; - use serde_json::json; - use std::path::{Path, PathBuf}; - use terminal::{HoveredWord, alacritty_terminal::index::Point as AlacPoint}; - use util::path; - use workspace::AppState; - - async fn init_test( - app_cx: &mut TestAppContext, - trees: impl IntoIterator, - worktree_roots: impl IntoIterator, - ) -> impl AsyncFnMut( - HoveredWord, - PathLikeTarget, - BackgroundFsChecks, - ) -> (Option, Option) { - let fs = app_cx.update(AppState::test).fs.as_fake().clone(); - - app_cx.update(|cx| { - theme::init(theme::LoadThemes::JustBase, cx); - editor::init(cx); - }); - - for (path, tree) in trees { - fs.insert_tree(path, tree).await; - } - - let project: gpui::Entity = Project::test( - fs.clone(), - worktree_roots.into_iter().map(Path::new), - app_cx, - ) - .await; - - let (workspace, cx) = - app_cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let cwd = std::env::current_dir().expect("Failed to get working directory"); - let terminal = project - .update(cx, |project: &mut Project, cx| { - project.create_terminal_shell(Some(cwd), cx) - }) - .await - .expect("Failed to create a terminal"); - - let workspace_a = workspace.clone(); - let (terminal_view, cx) = app_cx.add_window_view(|window, cx| { - TerminalView::new( - terminal, - workspace_a.downgrade(), - None, - project.downgrade(), - window, - cx, - ) - }); - - async move |hovered_word: HoveredWord, - path_like_target: PathLikeTarget, - background_fs_checks: BackgroundFsChecks| - -> (Option, Option) { - let workspace_a = workspace.clone(); - terminal_view - .update(cx, |_, cx| { - possible_hover_target( - &workspace_a.downgrade(), - hovered_word, - &path_like_target, - cx, - background_fs_checks, - ) - }) - .await; - - let hover_target = - terminal_view.read_with(cx, |terminal_view, _| terminal_view.hover.clone()); - - let open_target = terminal_view - .update_in(cx, |terminal_view, window, cx| { - possibly_open_target( - &workspace.downgrade(), - terminal_view, - &path_like_target, - window, - cx, - background_fs_checks, - ) - }) - .await - .expect("Failed to possibly open target"); - - (hover_target, open_target) - } - } - - async fn test_path_like_simple( - test_path_like: &mut impl AsyncFnMut( - HoveredWord, - PathLikeTarget, - BackgroundFsChecks, - ) -> (Option, Option), - maybe_path: &str, - tooltip: &str, - terminal_dir: Option, - background_fs_checks: BackgroundFsChecks, - mut open_target_found_by: OpenTargetFoundBy, - file: &str, - line: u32, - ) { - let (hover_target, open_target) = test_path_like( - HoveredWord { - word: maybe_path.to_string(), - word_match: AlacPoint::default()..=AlacPoint::default(), - id: 0, - }, - PathLikeTarget { - maybe_path: maybe_path.to_string(), - terminal_dir, - }, - background_fs_checks, - ) - .await; - - let Some(hover_target) = hover_target else { - assert!( - hover_target.is_some(), - "Hover target should not be `None` at {file}:{line}:" - ); - return; - }; - - assert_eq!( - hover_target.tooltip, tooltip, - "Tooltip mismatch at {file}:{line}:" - ); - assert_eq!( - hover_target.hovered_word.word, maybe_path, - "Hovered word mismatch at {file}:{line}:" - ); - - let Some(open_target) = open_target else { - assert!( - open_target.is_some(), - "Open target should not be `None` at {file}:{line}:" - ); - return; - }; - - assert_eq!( - open_target.path().path, - Path::new(tooltip), - "Open target path mismatch at {file}:{line}:" - ); - - if background_fs_checks == BackgroundFsChecks::Disabled - && open_target_found_by == OpenTargetFoundBy::FileSystemBackground - { - open_target_found_by = OpenTargetFoundBy::WorktreeScan; - } - - assert_eq!( - open_target.found_by(), - open_target_found_by, - "Open target found by mismatch at {file}:{line}:" - ); - } - - macro_rules! none_or_some_pathbuf { - (None) => { - None - }; - ($cwd:literal) => { - Some($crate::PathBuf::from(path!($cwd))) - }; - } - - macro_rules! test_path_like { - ( - $test_path_like:expr, - $maybe_path:literal, - $tooltip:literal, - $cwd:tt, - $found_by:expr - ) => {{ - test_path_like!( - $test_path_like, - $maybe_path, - $tooltip, - $cwd, - BackgroundFsChecks::Enabled, - $found_by - ); - test_path_like!( - $test_path_like, - $maybe_path, - $tooltip, - $cwd, - BackgroundFsChecks::Disabled, - $found_by - ); - }}; - - ( - $test_path_like:expr, - $maybe_path:literal, - $tooltip:literal, - $cwd:tt, - $background_fs_checks:path, - $found_by:expr - ) => { - test_path_like_simple( - &mut $test_path_like, - path!($maybe_path), - path!($tooltip), - none_or_some_pathbuf!($cwd), - $background_fs_checks, - $found_by, - std::file!(), - std::line!(), - ) - .await - }; - } - - // Note the arms of `test`, `test_local`, and `test_remote` should be collapsed once macro - // metavariable expressions (#![feature(macro_metavar_expr)]) are stabilized. - // See https://github.com/rust-lang/rust/issues/83527 - #[doc = "test_path_likes!(, , , { $(;)+ })"] - macro_rules! test_path_likes { - ($cx:expr, $trees:expr, $worktrees:expr, { $($tests:expr;)+ }) => { { - let mut test_path_like = init_test($cx, $trees, $worktrees).await; - #[doc ="test!(, , "] - #[doc ="\\[, found by \\])"] - #[allow(unused_macros)] - macro_rules! test { - ($maybe_path:literal, $tooltip:literal, $cwd:tt) => { - test_path_like!( - test_path_like, - $maybe_path, - $tooltip, - $cwd, - OpenTargetFoundBy::WorktreeExact - ) - }; - ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => { - test_path_like!( - test_path_like, - $maybe_path, - $tooltip, - $cwd, - OpenTargetFoundBy::$found_by - ) - } - } - #[doc ="test_local!(, , "] - #[doc ="\\[, found by \\])"] - #[allow(unused_macros)] - macro_rules! test_local { - ($maybe_path:literal, $tooltip:literal, $cwd:tt) => { - test_path_like!( - test_path_like, - $maybe_path, - $tooltip, - $cwd, - BackgroundFsChecks::Enabled, - OpenTargetFoundBy::WorktreeExact - ) - }; - ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => { - test_path_like!( - test_path_like, - $maybe_path, - $tooltip, - $cwd, - BackgroundFsChecks::Enabled, - OpenTargetFoundBy::$found_by - ) - } - } - #[doc ="test_remote!(, , "] - #[doc ="\\[, found by \\])"] - #[allow(unused_macros)] - macro_rules! test_remote { - ($maybe_path:literal, $tooltip:literal, $cwd:tt) => { - test_path_like!( - test_path_like, - $maybe_path, - $tooltip, - $cwd, - BackgroundFsChecks::Disabled, - OpenTargetFoundBy::WorktreeExact - ) - }; - ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => { - test_path_like!( - test_path_like, - $maybe_path, - $tooltip, - $cwd, - BackgroundFsChecks::Disabled, - OpenTargetFoundBy::$found_by - ) - } - } - $($tests);+ - } } - } - - #[gpui::test] - async fn one_folder_worktree(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/test"), - json!({ - "lib.rs": "", - "test.rs": "", - }), - )], - vec![path!("/test")], - { - test!("lib.rs", "/test/lib.rs", None); - test!("/test/lib.rs", "/test/lib.rs", None); - test!("test.rs", "/test/test.rs", None); - test!("/test/test.rs", "/test/test.rs", None); - } - ) - } - - #[gpui::test] - async fn mixed_worktrees(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![ - ( - path!("/"), - json!({ - "file.txt": "", - }), - ), - ( - path!("/test"), - json!({ - "lib.rs": "", - "test.rs": "", - "file.txt": "", - }), - ), - ], - vec![path!("/file.txt"), path!("/test")], - { - test!("file.txt", "/file.txt", "/"); - test!("/file.txt", "/file.txt", "/"); - - test!("lib.rs", "/test/lib.rs", "/test"); - test!("test.rs", "/test/test.rs", "/test"); - test!("file.txt", "/test/file.txt", "/test"); - - test!("/test/lib.rs", "/test/lib.rs", "/test"); - test!("/test/test.rs", "/test/test.rs", "/test"); - test!("/test/file.txt", "/test/file.txt", "/test"); - } - ) - } - - #[gpui::test] - async fn worktree_file_preferred(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![ - ( - path!("/"), - json!({ - "file.txt": "", - }), - ), - ( - path!("/test"), - json!({ - "file.txt": "", - }), - ), - ], - vec![path!("/test")], - { - test!("file.txt", "/test/file.txt", "/test"); - } - ) - } - - mod issues { - use super::*; - - // https://github.com/zed-industries/zed/issues/28407 - #[gpui::test] - async fn issue_28407_siblings(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/dir1"), - json!({ - "dir 2": { - "C.py": "" - }, - "dir 3": { - "C.py": "" - }, - }), - )], - vec![path!("/dir1")], - { - test!("C.py", "/dir1/dir 2/C.py", "/dir1", WorktreeScan); - test!("C.py", "/dir1/dir 2/C.py", "/dir1/dir 2"); - test!("C.py", "/dir1/dir 3/C.py", "/dir1/dir 3"); - } - ) - } - - // https://github.com/zed-industries/zed/issues/28407 - // See https://github.com/zed-industries/zed/issues/34027 - // See https://github.com/zed-industries/zed/issues/33498 - #[gpui::test] - async fn issue_28407_nesting(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/project"), - json!({ - "lib": { - "src": { - "main.rs": "", - "only_in_lib.rs": "" - }, - }, - "src": { - "main.rs": "" - }, - }), - )], - vec![path!("/project")], - { - test!("main.rs", "/project/src/main.rs", "/project/src"); - test!("main.rs", "/project/lib/src/main.rs", "/project/lib/src"); - - test!("src/main.rs", "/project/src/main.rs", "/project"); - test!("src/main.rs", "/project/src/main.rs", "/project/src"); - test!("src/main.rs", "/project/lib/src/main.rs", "/project/lib"); - - test!("lib/src/main.rs", "/project/lib/src/main.rs", "/project"); - test!( - "lib/src/main.rs", - "/project/lib/src/main.rs", - "/project/src" - ); - test!( - "lib/src/main.rs", - "/project/lib/src/main.rs", - "/project/lib" - ); - test!( - "lib/src/main.rs", - "/project/lib/src/main.rs", - "/project/lib/src" - ); - test!( - "src/only_in_lib.rs", - "/project/lib/src/only_in_lib.rs", - "/project/lib/src", - WorktreeScan - ); - } - ) - } - - // https://github.com/zed-industries/zed/issues/28339 - // Note: These could all be found by WorktreeExact if we used - // `fs::normalize_path(&maybe_path)` - #[gpui::test] - async fn issue_28339(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/tmp"), - json!({ - "issue28339": { - "foo": { - "bar.txt": "" - }, - }, - }), - )], - vec![path!("/tmp")], - { - test_local!( - "foo/./bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339", - WorktreeExact - ); - test_local!( - "foo/../foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339", - WorktreeExact - ); - test_local!( - "foo/..///foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339", - WorktreeExact - ); - test_local!( - "issue28339/../issue28339/foo/../foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339", - WorktreeExact - ); - test_local!( - "./bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339/foo", - WorktreeExact - ); - test_local!( - "../foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339/foo", - FileSystemBackground - ); - } - ) - } - - // https://github.com/zed-industries/zed/issues/28339 - // Note: These could all be found by WorktreeExact if we used - // `fs::normalize_path(&maybe_path)` - #[gpui::test] - #[should_panic(expected = "Hover target should not be `None`")] - async fn issue_28339_remote(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/tmp"), - json!({ - "issue28339": { - "foo": { - "bar.txt": "" - }, - }, - }), - )], - vec![path!("/tmp")], - { - test_remote!( - "foo/./bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339" - ); - test_remote!( - "foo/../foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339" - ); - test_remote!( - "foo/..///foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339" - ); - test_remote!( - "issue28339/../issue28339/foo/../foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339" - ); - test_remote!( - "./bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339/foo" - ); - test_remote!( - "../foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339/foo" - ); - } - ) - } - - // https://github.com/zed-industries/zed/issues/34027 - #[gpui::test] - async fn issue_34027(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/tmp/issue34027"), - json!({ - "test.txt": "", - "foo": { - "test.txt": "", - } - }), - ),], - vec![path!("/tmp/issue34027")], - { - test!("test.txt", "/tmp/issue34027/test.txt", "/tmp/issue34027"); - test!( - "test.txt", - "/tmp/issue34027/foo/test.txt", - "/tmp/issue34027/foo" - ); - } - ) - } - - // https://github.com/zed-industries/zed/issues/34027 - #[gpui::test] - async fn issue_34027_siblings(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/test"), - json!({ - "sub1": { - "file.txt": "", - }, - "sub2": { - "file.txt": "", - } - }), - ),], - vec![path!("/test")], - { - test!("file.txt", "/test/sub1/file.txt", "/test/sub1"); - test!("file.txt", "/test/sub2/file.txt", "/test/sub2"); - test!("sub1/file.txt", "/test/sub1/file.txt", "/test/sub1"); - test!("sub2/file.txt", "/test/sub2/file.txt", "/test/sub2"); - test!("sub1/file.txt", "/test/sub1/file.txt", "/test/sub2"); - test!("sub2/file.txt", "/test/sub2/file.txt", "/test/sub1"); - } - ) - } - - // https://github.com/zed-industries/zed/issues/34027 - #[gpui::test] - async fn issue_34027_nesting(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/test"), - json!({ - "sub1": { - "file.txt": "", - "subsub1": { - "file.txt": "", - } - }, - "sub2": { - "file.txt": "", - "subsub1": { - "file.txt": "", - } - } - }), - ),], - vec![path!("/test")], - { - test!( - "file.txt", - "/test/sub1/subsub1/file.txt", - "/test/sub1/subsub1" - ); - test!( - "file.txt", - "/test/sub2/subsub1/file.txt", - "/test/sub2/subsub1" - ); - test!( - "subsub1/file.txt", - "/test/sub1/subsub1/file.txt", - "/test", - WorktreeScan - ); - test!( - "subsub1/file.txt", - "/test/sub1/subsub1/file.txt", - "/test", - WorktreeScan - ); - test!( - "subsub1/file.txt", - "/test/sub1/subsub1/file.txt", - "/test/sub1" - ); - test!( - "subsub1/file.txt", - "/test/sub2/subsub1/file.txt", - "/test/sub2" - ); - test!( - "subsub1/file.txt", - "/test/sub1/subsub1/file.txt", - "/test/sub1/subsub1", - WorktreeScan - ); - } - ) - } - - // https://github.com/zed-industries/zed/issues/34027 - #[gpui::test] - async fn issue_34027_non_worktree_local_file(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![ - ( - path!("/"), - json!({ - "file.txt": "", - }), - ), - ( - path!("/test"), - json!({ - "file.txt": "", - }), - ), - ], - vec![path!("/test")], - { - // Note: Opening a non-worktree file adds that file as a single file worktree. - test_local!("file.txt", "/file.txt", "/", FileSystemBackground); - } - ) - } - - // https://github.com/zed-industries/zed/issues/34027 - #[gpui::test] - async fn issue_34027_non_worktree_remote_file(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![ - ( - path!("/"), - json!({ - "file.txt": "", - }), - ), - ( - path!("/test"), - json!({ - "file.txt": "", - }), - ), - ], - vec![path!("/test")], - { - // Note: Opening a non-worktree file adds that file as a single file worktree. - test_remote!("file.txt", "/test/file.txt", "/"); - test_remote!("/test/file.txt", "/test/file.txt", "/"); - } - ) - } - - // See https://github.com/zed-industries/zed/issues/34027 - #[gpui::test] - #[should_panic(expected = "Tooltip mismatch")] - async fn issue_34027_gaps(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/project"), - json!({ - "lib": { - "src": { - "main.rs": "" - }, - }, - "src": { - "main.rs": "" - }, - }), - )], - vec![path!("/project")], - { - test!("main.rs", "/project/src/main.rs", "/project"); - test!("main.rs", "/project/lib/src/main.rs", "/project/lib"); - } - ) - } - - // See https://github.com/zed-industries/zed/issues/34027 - #[gpui::test] - #[should_panic(expected = "Tooltip mismatch")] - async fn issue_34027_overlap(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/project"), - json!({ - "lib": { - "src": { - "main.rs": "" - }, - }, - "src": { - "main.rs": "" - }, - }), - )], - vec![path!("/project")], - { - // Finds "/project/src/main.rs" - test!( - "src/main.rs", - "/project/lib/src/main.rs", - "/project/lib/src" - ); - } - ) - } - } -} diff --git a/crates/terminal_view/src/terminal_scrollbar.rs b/crates/terminal_view/src/terminal_scrollbar.rs deleted file mode 100644 index 871bb60230..0000000000 --- a/crates/terminal_view/src/terminal_scrollbar.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::{ - cell::{Cell, RefCell}, - rc::Rc, -}; - -use gpui::{Bounds, Point, Size, size}; -use terminal::Terminal; -use ui::{Pixels, ScrollableHandle, px}; - -#[derive(Debug)] -struct ScrollHandleState { - line_height: Pixels, - total_lines: usize, - viewport_lines: usize, - display_offset: usize, -} - -impl ScrollHandleState { - fn new(terminal: &Terminal) -> Self { - Self { - line_height: terminal.last_content().terminal_bounds.line_height, - total_lines: terminal.total_lines(), - viewport_lines: terminal.viewport_lines(), - display_offset: terminal.last_content().display_offset, - } - } -} - -#[derive(Debug, Clone)] -pub struct TerminalScrollHandle { - state: Rc>, - pub future_display_offset: Rc>>, -} - -impl TerminalScrollHandle { - pub fn new(terminal: &Terminal) -> Self { - Self { - state: Rc::new(RefCell::new(ScrollHandleState::new(terminal))), - future_display_offset: Rc::new(Cell::new(None)), - } - } - - pub fn update(&self, terminal: &Terminal) { - *self.state.borrow_mut() = ScrollHandleState::new(terminal); - } -} - -impl ScrollableHandle for TerminalScrollHandle { - fn max_offset(&self) -> Size { - let state = self.state.borrow(); - size( - Pixels::ZERO, - state - .total_lines - .checked_sub(state.viewport_lines) - .unwrap_or(0) as f32 - * state.line_height, - ) - } - - fn offset(&self) -> Point { - let state = self.state.borrow(); - let scroll_offset = state.total_lines - state.viewport_lines - state.display_offset; - Point::new( - Pixels::ZERO, - -(scroll_offset as f32 * self.state.borrow().line_height), - ) - } - - fn set_offset(&self, point: Point) { - let state = self.state.borrow(); - let offset_delta = (point.y / state.line_height).round() as i32; - - let max_offset = state.total_lines - state.viewport_lines; - let display_offset = (max_offset as i32 + offset_delta).clamp(0, max_offset as i32); - - self.future_display_offset - .set(Some(display_offset as usize)); - } - - fn viewport(&self) -> Bounds { - let state = self.state.borrow(); - Bounds::new( - Point::new(px(0.), px(0.)), - size( - Pixels::ZERO, - state.viewport_lines as f32 * state.line_height, - ), - ) - } -} diff --git a/crates/terminal_view/src/terminal_slash_command.rs b/crates/terminal_view/src/terminal_slash_command.rs deleted file mode 100644 index 13c2cef48c..0000000000 --- a/crates/terminal_view/src/terminal_slash_command.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::sync::Arc; -use std::sync::atomic::AtomicBool; - -use crate::{TerminalView, terminal_panel::TerminalPanel}; -use anyhow::Result; -use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, - SlashCommandResult, -}; -use gpui::{App, Entity, Task, WeakEntity}; -use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate}; -use ui::prelude::*; -use workspace::{Workspace, dock::Panel}; - -use assistant_slash_command::create_label_for_command; - -pub struct TerminalSlashCommand; - -const LINE_COUNT_ARG: &str = "--line-count"; - -const DEFAULT_CONTEXT_LINES: usize = 50; - -impl SlashCommand for TerminalSlashCommand { - fn name(&self) -> String { - "terminal".into() - } - - fn label(&self, cx: &App) -> CodeLabel { - create_label_for_command("terminal", &[LINE_COUNT_ARG], cx) - } - - fn description(&self) -> String { - "Insert terminal output".into() - } - - fn icon(&self) -> IconName { - IconName::Terminal - } - - fn menu_text(&self) -> String { - self.description() - } - - fn requires_argument(&self) -> bool { - false - } - - fn accepts_arguments(&self) -> bool { - true - } - - fn complete_argument( - self: Arc, - _arguments: &[String], - _cancel: Arc, - _workspace: Option>, - _window: &mut Window, - _cx: &mut App, - ) -> Task>> { - Task::ready(Ok(Vec::new())) - } - - 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 { - let Some(workspace) = workspace.upgrade() else { - return Task::ready(Err(anyhow::anyhow!("workspace was dropped"))); - }; - - let Some(active_terminal) = resolve_active_terminal(&workspace, cx) else { - return Task::ready(Err(anyhow::anyhow!("no active terminal"))); - }; - - let line_count = arguments - .get(0) - .and_then(|s| s.parse::().ok()) - .unwrap_or(DEFAULT_CONTEXT_LINES); - - let lines = active_terminal - .read(cx) - .entity() - .read(cx) - .last_n_non_empty_lines(line_count); - - let mut text = String::new(); - text.push_str("Terminal output:\n"); - text.push_str(&lines.join("\n")); - let range = 0..text.len(); - - Task::ready(Ok(SlashCommandOutput { - text, - sections: vec![SlashCommandOutputSection { - range, - icon: IconName::Terminal, - label: "Terminal".into(), - metadata: None, - }], - run_commands_in_text: false, - } - .into_event_stream())) - } -} - -fn resolve_active_terminal( - workspace: &Entity, - cx: &mut App, -) -> Option> { - if let Some(terminal_view) = workspace - .read(cx) - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - { - return Some(terminal_view); - } - - let terminal_panel = workspace.read(cx).panel::(cx)?; - terminal_panel.read(cx).pane().and_then(|pane| { - pane.read(cx) - .active_item() - .and_then(|t| t.downcast::()) - }) -} diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs deleted file mode 100644 index 98f7a17a27..0000000000 --- a/crates/terminal_view/src/terminal_view.rs +++ /dev/null @@ -1,1786 +0,0 @@ -mod persistence; -pub mod terminal_element; -pub mod terminal_panel; -mod terminal_path_like_target; -pub mod terminal_scrollbar; -mod terminal_slash_command; - -use assistant_slash_command::SlashCommandRegistry; -use editor::{EditorSettings, actions::SelectAll, blink_manager::BlinkManager}; -use gpui::{ - Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, - KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render, - ScrollWheelEvent, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred, div, -}; -use persistence::TERMINAL_DB; -use project::{Project, search::SearchQuery}; -use schemars::JsonSchema; -use task::TaskId; -use terminal::{ - Clear, Copy, Event, HoveredWord, MaybeNavigationTarget, Paste, ScrollLineDown, ScrollLineUp, - ScrollPageDown, ScrollPageUp, ScrollToBottom, ScrollToTop, ShowCharacterPalette, TaskState, - TaskStatus, Terminal, TerminalBounds, ToggleViMode, - alacritty_terminal::{ - index::Point, - term::{TermMode, point_to_viewport, search::RegexSearch}, - }, - terminal_settings::{CursorShape, TerminalSettings}, -}; -use terminal_element::TerminalElement; -use terminal_panel::TerminalPanel; -use terminal_path_like_target::{hover_path_like_target, open_path_like_target}; -use terminal_scrollbar::TerminalScrollHandle; -use terminal_slash_command::TerminalSlashCommand; -use ui::{ - ContextMenu, Divider, ScrollAxes, Scrollbars, Tooltip, WithScrollbar, - prelude::*, - scrollbars::{self, GlobalSetting, ScrollbarVisibility}, -}; -use util::ResultExt; -use workspace::{ - CloseActiveItem, NewCenterTerminal, NewTerminal, ToolbarItemLocation, Workspace, WorkspaceId, - delete_unloaded_items, - item::{ - BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams, TabTooltipContent, - }, - register_serializable_item, - searchable::{Direction, SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, -}; - -use serde::Deserialize; -use settings::{Settings, SettingsStore, TerminalBlink, WorkingDirectory}; -use zed_actions::assistant::InlineAssist; - -use std::{ - cmp, - ops::{Range, RangeInclusive}, - path::{Path, PathBuf}, - rc::Rc, - sync::Arc, - time::Duration, -}; - -struct ImeState { - marked_text: String, - marked_range_utf16: Option>, -} - -const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); - -/// Event to transmit the scroll from the element to the view -#[derive(Clone, Debug, PartialEq)] -pub struct ScrollTerminal(pub i32); - -/// Sends the specified text directly to the terminal. -#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = terminal)] -pub struct SendText(String); - -/// Sends a keystroke sequence to the terminal. -#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = terminal)] -pub struct SendKeystroke(String); - -actions!( - terminal, - [ - /// Reruns the last executed task in the terminal. - RerunTask - ] -); - -pub fn init(cx: &mut App) { - assistant_slash_command::init(cx); - terminal_panel::init(cx); - - register_serializable_item::(cx); - - cx.observe_new(|workspace: &mut Workspace, _window, _cx| { - workspace.register_action(TerminalView::deploy); - }) - .detach(); - SlashCommandRegistry::global(cx).register_command(TerminalSlashCommand, true); -} - -pub struct BlockProperties { - pub height: u8, - pub render: Box AnyElement>, -} - -pub struct BlockContext<'a, 'b> { - pub window: &'a mut Window, - pub context: &'b mut App, - pub dimensions: TerminalBounds, -} - -///A terminal view, maintains the PTY's file handles and communicates with the terminal -pub struct TerminalView { - terminal: Entity, - workspace: WeakEntity, - project: WeakEntity, - focus_handle: FocusHandle, - //Currently using iTerm bell, show bell emoji in tab until input is received - has_bell: bool, - context_menu: Option<(Entity, gpui::Point, Subscription)>, - cursor_shape: CursorShape, - blink_manager: Entity, - mode: TerminalMode, - blinking_terminal_enabled: bool, - cwd_serialized: bool, - hover: Option, - hover_tooltip_update: Task<()>, - workspace_id: Option, - show_breadcrumbs: bool, - block_below_cursor: Option>, - scroll_top: Pixels, - scroll_handle: TerminalScrollHandle, - ime_state: Option, - _subscriptions: Vec, - _terminal_subscriptions: Vec, -} - -#[derive(Default, Clone)] -pub enum TerminalMode { - #[default] - Standalone, - Embedded { - max_lines_when_unfocused: Option, - }, -} - -#[derive(Clone)] -pub enum ContentMode { - Scrollable, - Inline { - displayed_lines: usize, - total_lines: usize, - }, -} - -impl ContentMode { - pub fn is_limited(&self) -> bool { - match self { - ContentMode::Scrollable => false, - ContentMode::Inline { - displayed_lines, - total_lines, - } => displayed_lines < total_lines, - } - } - - pub fn is_scrollable(&self) -> bool { - matches!(self, ContentMode::Scrollable) - } -} - -#[derive(Debug)] -#[cfg_attr(test, derive(Clone, Eq, PartialEq))] -struct HoverTarget { - tooltip: String, - hovered_word: HoveredWord, -} - -impl EventEmitter for TerminalView {} -impl EventEmitter for TerminalView {} -impl EventEmitter for TerminalView {} - -impl Focusable for TerminalView { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl TerminalView { - ///Create a new Terminal in the current working directory or the user's home directory - pub fn deploy( - workspace: &mut Workspace, - _: &NewCenterTerminal, - window: &mut Window, - cx: &mut Context, - ) { - let working_directory = default_working_directory(workspace, cx); - TerminalPanel::add_center_terminal(workspace, window, cx, |project, cx| { - project.create_terminal_shell(working_directory, cx) - }) - .detach_and_log_err(cx); - } - - pub fn new( - terminal: Entity, - workspace: WeakEntity, - workspace_id: Option, - project: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let workspace_handle = workspace.clone(); - let terminal_subscriptions = - subscribe_for_terminal_events(&terminal, workspace, window, cx); - - let focus_handle = cx.focus_handle(); - let focus_in = cx.on_focus_in(&focus_handle, window, |terminal_view, window, cx| { - terminal_view.focus_in(window, cx); - }); - let focus_out = cx.on_focus_out( - &focus_handle, - window, - |terminal_view, _event, window, cx| { - terminal_view.focus_out(window, cx); - }, - ); - let cursor_shape = TerminalSettings::get_global(cx).cursor_shape; - - let scroll_handle = TerminalScrollHandle::new(terminal.read(cx)); - - let blink_manager = cx.new(|cx| { - BlinkManager::new( - CURSOR_BLINK_INTERVAL, - |cx| { - !matches!( - TerminalSettings::get_global(cx).blinking, - TerminalBlink::Off - ) - }, - cx, - ) - }); - - let _subscriptions = vec![ - focus_in, - focus_out, - cx.observe(&blink_manager, |_, _, cx| cx.notify()), - cx.observe_global::(Self::settings_changed), - ]; - Self { - terminal, - workspace: workspace_handle, - project, - has_bell: false, - focus_handle, - context_menu: None, - cursor_shape, - blink_manager, - blinking_terminal_enabled: false, - hover: None, - hover_tooltip_update: Task::ready(()), - mode: TerminalMode::Standalone, - workspace_id, - show_breadcrumbs: TerminalSettings::get_global(cx).toolbar.breadcrumbs, - block_below_cursor: None, - scroll_top: Pixels::ZERO, - scroll_handle, - cwd_serialized: false, - ime_state: None, - _subscriptions, - _terminal_subscriptions: terminal_subscriptions, - } - } - - /// Enable 'embedded' mode where the terminal displays the full content with an optional limit of lines. - pub fn set_embedded_mode( - &mut self, - max_lines_when_unfocused: Option, - cx: &mut Context, - ) { - self.mode = TerminalMode::Embedded { - max_lines_when_unfocused, - }; - cx.notify(); - } - - const MAX_EMBEDDED_LINES: usize = 1_000; - - /// Returns the current `ContentMode` depending on the set `TerminalMode` and the current number of lines - /// - /// Note: Even in embedded mode, the terminal will fallback to scrollable when its content exceeds `MAX_EMBEDDED_LINES` - pub fn content_mode(&self, window: &Window, cx: &App) -> ContentMode { - match &self.mode { - TerminalMode::Standalone => ContentMode::Scrollable, - TerminalMode::Embedded { - max_lines_when_unfocused, - } => { - let total_lines = self.terminal.read(cx).total_lines(); - - if total_lines > Self::MAX_EMBEDDED_LINES { - ContentMode::Scrollable - } else { - let mut displayed_lines = total_lines; - - if !self.focus_handle.is_focused(window) - && let Some(max_lines) = max_lines_when_unfocused - { - displayed_lines = displayed_lines.min(*max_lines) - } - - ContentMode::Inline { - displayed_lines, - total_lines, - } - } - } - } - } - - /// Sets the marked (pre-edit) text from the IME. - pub(crate) fn set_marked_text( - &mut self, - text: String, - range: Option>, - cx: &mut Context, - ) { - self.ime_state = Some(ImeState { - marked_text: text, - marked_range_utf16: range, - }); - cx.notify(); - } - - /// Gets the current marked range (UTF-16). - pub(crate) fn marked_text_range(&self) -> Option> { - self.ime_state - .as_ref() - .and_then(|state| state.marked_range_utf16.clone()) - } - - /// Clears the marked (pre-edit) text state. - pub(crate) fn clear_marked_text(&mut self, cx: &mut Context) { - if self.ime_state.is_some() { - self.ime_state = None; - cx.notify(); - } - } - - /// Commits (sends) the given text to the PTY. Called by InputHandler::replace_text_in_range. - pub(crate) fn commit_text(&mut self, text: &str, cx: &mut Context) { - if !text.is_empty() { - self.terminal.update(cx, |term, _| { - term.input(text.to_string().into_bytes()); - }); - } - } - - pub(crate) fn terminal_bounds(&self, cx: &App) -> TerminalBounds { - self.terminal.read(cx).last_content().terminal_bounds - } - - pub fn entity(&self) -> &Entity { - &self.terminal - } - - pub fn has_bell(&self) -> bool { - self.has_bell - } - - pub fn clear_bell(&mut self, cx: &mut Context) { - self.has_bell = false; - cx.emit(Event::Wakeup); - } - - pub fn deploy_context_menu( - &mut self, - position: gpui::Point, - window: &mut Window, - cx: &mut Context, - ) { - let assistant_enabled = self - .workspace - .upgrade() - .and_then(|workspace| workspace.read(cx).panel::(cx)) - .is_some_and(|terminal_panel| terminal_panel.read(cx).assistant_enabled()); - let context_menu = ContextMenu::build(window, cx, |menu, _, _| { - menu.context(self.focus_handle.clone()) - .action("New Terminal", Box::new(NewTerminal)) - .separator() - .action("Copy", Box::new(Copy)) - .action("Paste", Box::new(Paste)) - .action("Select All", Box::new(SelectAll)) - .action("Clear", Box::new(Clear)) - .when(assistant_enabled, |menu| { - menu.separator() - .action("Inline Assist", Box::new(InlineAssist::default())) - }) - .separator() - .action( - "Close Terminal Tab", - Box::new(CloseActiveItem { - save_intent: None, - close_pinned: true, - }), - ) - }); - - window.focus(&context_menu.focus_handle(cx)); - let subscription = cx.subscribe_in( - &context_menu, - window, - |this, _, _: &DismissEvent, window, cx| { - if this.context_menu.as_ref().is_some_and(|context_menu| { - context_menu.0.focus_handle(cx).contains_focused(window, cx) - }) { - cx.focus_self(window); - } - this.context_menu.take(); - cx.notify(); - }, - ); - - self.context_menu = Some((context_menu, position, subscription)); - } - - fn settings_changed(&mut self, cx: &mut Context) { - let settings = TerminalSettings::get_global(cx); - let breadcrumb_visibility_changed = self.show_breadcrumbs != settings.toolbar.breadcrumbs; - self.show_breadcrumbs = settings.toolbar.breadcrumbs; - - let should_blink = match settings.blinking { - TerminalBlink::Off => false, - TerminalBlink::On => true, - TerminalBlink::TerminalControlled => self.blinking_terminal_enabled, - }; - let new_cursor_shape = settings.cursor_shape; - let old_cursor_shape = self.cursor_shape; - if old_cursor_shape != new_cursor_shape { - self.cursor_shape = new_cursor_shape; - self.terminal.update(cx, |term, _| { - term.set_cursor_shape(self.cursor_shape); - }); - } - - self.blink_manager.update( - cx, - if should_blink { - BlinkManager::enable - } else { - BlinkManager::disable - }, - ); - - if breadcrumb_visibility_changed { - cx.emit(ItemEvent::UpdateBreadcrumbs); - } - cx.notify(); - } - - fn show_character_palette( - &mut self, - _: &ShowCharacterPalette, - window: &mut Window, - cx: &mut Context, - ) { - if self - .terminal - .read(cx) - .last_content - .mode - .contains(TermMode::ALT_SCREEN) - { - self.terminal.update(cx, |term, cx| { - term.try_keystroke( - &Keystroke::parse("ctrl-cmd-space").unwrap(), - TerminalSettings::get_global(cx).option_as_meta, - ) - }); - } else { - window.show_character_palette(); - } - } - - fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context) { - self.terminal.update(cx, |term, _| term.select_all()); - cx.notify(); - } - - fn rerun_task(&mut self, _: &RerunTask, window: &mut Window, cx: &mut Context) { - let task = self - .terminal - .read(cx) - .task() - .map(|task| terminal_rerun_override(&task.spawned_task.id)) - .unwrap_or_default(); - window.dispatch_action(Box::new(task), cx); - } - - fn clear(&mut self, _: &Clear, _: &mut Window, cx: &mut Context) { - self.scroll_top = px(0.); - self.terminal.update(cx, |term, _| term.clear()); - cx.notify(); - } - - fn max_scroll_top(&self, cx: &App) -> Pixels { - let terminal = self.terminal.read(cx); - - let Some(block) = self.block_below_cursor.as_ref() else { - return Pixels::ZERO; - }; - - let line_height = terminal.last_content().terminal_bounds.line_height; - let viewport_lines = terminal.viewport_lines(); - let cursor = point_to_viewport( - terminal.last_content.display_offset, - terminal.last_content.cursor.point, - ) - .unwrap_or_default(); - let max_scroll_top_in_lines = - (block.height as usize).saturating_sub(viewport_lines.saturating_sub(cursor.line + 1)); - - max_scroll_top_in_lines as f32 * line_height - } - - fn scroll_wheel(&mut self, event: &ScrollWheelEvent, cx: &mut Context) { - let terminal_content = self.terminal.read(cx).last_content(); - - if self.block_below_cursor.is_some() && terminal_content.display_offset == 0 { - let line_height = terminal_content.terminal_bounds.line_height; - let y_delta = event.delta.pixel_delta(line_height).y; - if y_delta < Pixels::ZERO || self.scroll_top > Pixels::ZERO { - self.scroll_top = cmp::max( - Pixels::ZERO, - cmp::min(self.scroll_top - y_delta, self.max_scroll_top(cx)), - ); - cx.notify(); - return; - } - } - self.terminal.update(cx, |term, cx| { - term.scroll_wheel( - event, - TerminalSettings::get_global(cx).scroll_multiplier.max(0.01), - ) - }); - } - - fn scroll_line_up(&mut self, _: &ScrollLineUp, _: &mut Window, cx: &mut Context) { - let terminal_content = self.terminal.read(cx).last_content(); - if self.block_below_cursor.is_some() - && terminal_content.display_offset == 0 - && self.scroll_top > Pixels::ZERO - { - let line_height = terminal_content.terminal_bounds.line_height; - self.scroll_top = cmp::max(self.scroll_top - line_height, Pixels::ZERO); - return; - } - - self.terminal.update(cx, |term, _| term.scroll_line_up()); - cx.notify(); - } - - fn scroll_line_down(&mut self, _: &ScrollLineDown, _: &mut Window, cx: &mut Context) { - let terminal_content = self.terminal.read(cx).last_content(); - if self.block_below_cursor.is_some() && terminal_content.display_offset == 0 { - let max_scroll_top = self.max_scroll_top(cx); - if self.scroll_top < max_scroll_top { - let line_height = terminal_content.terminal_bounds.line_height; - self.scroll_top = cmp::min(self.scroll_top + line_height, max_scroll_top); - } - return; - } - - self.terminal.update(cx, |term, _| term.scroll_line_down()); - cx.notify(); - } - - fn scroll_page_up(&mut self, _: &ScrollPageUp, _: &mut Window, cx: &mut Context) { - if self.scroll_top == Pixels::ZERO { - self.terminal.update(cx, |term, _| term.scroll_page_up()); - } else { - let line_height = self - .terminal - .read(cx) - .last_content - .terminal_bounds - .line_height(); - let visible_block_lines = (self.scroll_top / line_height) as usize; - let viewport_lines = self.terminal.read(cx).viewport_lines(); - let visible_content_lines = viewport_lines - visible_block_lines; - - if visible_block_lines >= viewport_lines { - self.scroll_top = ((visible_block_lines - viewport_lines) as f32) * line_height; - } else { - self.scroll_top = px(0.); - self.terminal - .update(cx, |term, _| term.scroll_up_by(visible_content_lines)); - } - } - cx.notify(); - } - - fn scroll_page_down(&mut self, _: &ScrollPageDown, _: &mut Window, cx: &mut Context) { - self.terminal.update(cx, |term, _| term.scroll_page_down()); - let terminal = self.terminal.read(cx); - if terminal.last_content().display_offset < terminal.viewport_lines() { - self.scroll_top = self.max_scroll_top(cx); - } - cx.notify(); - } - - fn scroll_to_top(&mut self, _: &ScrollToTop, _: &mut Window, cx: &mut Context) { - self.terminal.update(cx, |term, _| term.scroll_to_top()); - cx.notify(); - } - - fn scroll_to_bottom(&mut self, _: &ScrollToBottom, _: &mut Window, cx: &mut Context) { - self.terminal.update(cx, |term, _| term.scroll_to_bottom()); - if self.block_below_cursor.is_some() { - self.scroll_top = self.max_scroll_top(cx); - } - cx.notify(); - } - - fn toggle_vi_mode(&mut self, _: &ToggleViMode, _: &mut Window, cx: &mut Context) { - self.terminal.update(cx, |term, _| term.toggle_vi_mode()); - cx.notify(); - } - - pub fn should_show_cursor(&self, focused: bool, cx: &mut Context) -> bool { - // Always show cursor when not focused or in special modes - if !focused - || self - .terminal - .read(cx) - .last_content - .mode - .contains(TermMode::ALT_SCREEN) - { - return true; - } - - // When focused, check blinking settings and blink manager state - match TerminalSettings::get_global(cx).blinking { - TerminalBlink::Off => true, - TerminalBlink::TerminalControlled => { - !self.blinking_terminal_enabled || self.blink_manager.read(cx).visible() - } - TerminalBlink::On => self.blink_manager.read(cx).visible(), - } - } - - pub fn pause_cursor_blinking(&mut self, _window: &mut Window, cx: &mut Context) { - self.blink_manager.update(cx, BlinkManager::pause_blinking); - } - - pub fn terminal(&self) -> &Entity { - &self.terminal - } - - pub fn set_block_below_cursor( - &mut self, - block: BlockProperties, - window: &mut Window, - cx: &mut Context, - ) { - self.block_below_cursor = Some(Rc::new(block)); - self.scroll_to_bottom(&ScrollToBottom, window, cx); - cx.notify(); - } - - pub fn clear_block_below_cursor(&mut self, cx: &mut Context) { - self.block_below_cursor = None; - self.scroll_top = Pixels::ZERO; - cx.notify(); - } - - ///Attempt to paste the clipboard into the terminal - fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context) { - self.terminal.update(cx, |term, _| term.copy(None)); - cx.notify(); - } - - ///Attempt to paste the clipboard into the terminal - fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context) { - if let Some(clipboard_string) = cx.read_from_clipboard().and_then(|item| item.text()) { - self.terminal - .update(cx, |terminal, _cx| terminal.paste(&clipboard_string)); - } - } - - fn send_text(&mut self, text: &SendText, _: &mut Window, cx: &mut Context) { - self.clear_bell(cx); - self.terminal.update(cx, |term, _| { - term.input(text.0.to_string().into_bytes()); - }); - } - - fn send_keystroke(&mut self, text: &SendKeystroke, _: &mut Window, cx: &mut Context) { - if let Some(keystroke) = Keystroke::parse(&text.0).log_err() { - self.clear_bell(cx); - self.terminal.update(cx, |term, cx| { - let processed = - term.try_keystroke(&keystroke, TerminalSettings::get_global(cx).option_as_meta); - if processed && term.vi_mode_enabled() { - cx.notify(); - } - processed - }); - } - } - - fn dispatch_context(&self, cx: &App) -> KeyContext { - let mut dispatch_context = KeyContext::new_with_defaults(); - dispatch_context.add("Terminal"); - - if self.terminal.read(cx).vi_mode_enabled() { - dispatch_context.add("vi_mode"); - } - - let mode = self.terminal.read(cx).last_content.mode; - dispatch_context.set( - "screen", - if mode.contains(TermMode::ALT_SCREEN) { - "alt" - } else { - "normal" - }, - ); - - if mode.contains(TermMode::APP_CURSOR) { - dispatch_context.add("DECCKM"); - } - if mode.contains(TermMode::APP_KEYPAD) { - dispatch_context.add("DECPAM"); - } else { - dispatch_context.add("DECPNM"); - } - if mode.contains(TermMode::SHOW_CURSOR) { - dispatch_context.add("DECTCEM"); - } - if mode.contains(TermMode::LINE_WRAP) { - dispatch_context.add("DECAWM"); - } - if mode.contains(TermMode::ORIGIN) { - dispatch_context.add("DECOM"); - } - if mode.contains(TermMode::INSERT) { - dispatch_context.add("IRM"); - } - //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html - if mode.contains(TermMode::LINE_FEED_NEW_LINE) { - dispatch_context.add("LNM"); - } - if mode.contains(TermMode::FOCUS_IN_OUT) { - dispatch_context.add("report_focus"); - } - if mode.contains(TermMode::ALTERNATE_SCROLL) { - dispatch_context.add("alternate_scroll"); - } - if mode.contains(TermMode::BRACKETED_PASTE) { - dispatch_context.add("bracketed_paste"); - } - if mode.intersects(TermMode::MOUSE_MODE) { - dispatch_context.add("any_mouse_reporting"); - } - { - let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) { - "click" - } else if mode.contains(TermMode::MOUSE_DRAG) { - "drag" - } else if mode.contains(TermMode::MOUSE_MOTION) { - "motion" - } else { - "off" - }; - dispatch_context.set("mouse_reporting", mouse_reporting); - } - { - let format = if mode.contains(TermMode::SGR_MOUSE) { - "sgr" - } else if mode.contains(TermMode::UTF8_MOUSE) { - "utf8" - } else { - "normal" - }; - dispatch_context.set("mouse_format", format); - }; - - if self.terminal.read(cx).last_content.selection.is_some() { - dispatch_context.add("selection"); - } - - dispatch_context - } - - fn set_terminal( - &mut self, - terminal: Entity, - window: &mut Window, - cx: &mut Context, - ) { - self._terminal_subscriptions = - subscribe_for_terminal_events(&terminal, self.workspace.clone(), window, cx); - self.terminal = terminal; - } - - fn rerun_button(task: &TaskState) -> Option { - if !task.spawned_task.show_rerun { - return None; - } - - let task_id = task.spawned_task.id.clone(); - Some( - IconButton::new("rerun-icon", IconName::Rerun) - .icon_size(IconSize::Small) - .size(ButtonSize::Compact) - .icon_color(Color::Default) - .shape(ui::IconButtonShape::Square) - .tooltip(move |_window, cx| Tooltip::for_action("Rerun task", &RerunTask, cx)) - .on_click(move |_, window, cx| { - window.dispatch_action(Box::new(terminal_rerun_override(&task_id)), cx); - }), - ) - } -} - -fn terminal_rerun_override(task: &TaskId) -> zed_actions::Rerun { - zed_actions::Rerun { - task_id: Some(task.0.clone()), - allow_concurrent_runs: Some(true), - use_new_terminal: Some(false), - reevaluate_context: false, - } -} - -fn subscribe_for_terminal_events( - terminal: &Entity, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, -) -> Vec { - let terminal_subscription = cx.observe(terminal, |_, _, cx| cx.notify()); - let mut previous_cwd = None; - let terminal_events_subscription = cx.subscribe_in( - terminal, - window, - move |terminal_view, terminal, event, window, cx| { - let current_cwd = terminal.read(cx).working_directory(); - if current_cwd != previous_cwd { - previous_cwd = current_cwd; - terminal_view.cwd_serialized = false; - } - - match event { - Event::Wakeup => { - cx.notify(); - cx.emit(Event::Wakeup); - cx.emit(ItemEvent::UpdateTab); - cx.emit(SearchEvent::MatchesInvalidated); - } - - Event::Bell => { - terminal_view.has_bell = true; - cx.emit(Event::Wakeup); - } - - Event::BlinkChanged(blinking) => { - terminal_view.blinking_terminal_enabled = *blinking; - - // If in terminal-controlled mode and focused, update blink manager - if matches!( - TerminalSettings::get_global(cx).blinking, - TerminalBlink::TerminalControlled - ) && terminal_view.focus_handle.is_focused(window) - { - terminal_view.blink_manager.update(cx, |manager, cx| { - if *blinking { - manager.enable(cx); - } else { - manager.disable(cx); - } - }); - } - } - - Event::TitleChanged => { - cx.emit(ItemEvent::UpdateTab); - } - - Event::NewNavigationTarget(maybe_navigation_target) => { - match maybe_navigation_target - .as_ref() - .zip(terminal.read(cx).last_content.last_hovered_word.as_ref()) - { - Some((MaybeNavigationTarget::Url(url), hovered_word)) => { - if Some(hovered_word) - != terminal_view - .hover - .as_ref() - .map(|hover| &hover.hovered_word) - { - terminal_view.hover = Some(HoverTarget { - tooltip: url.clone(), - hovered_word: hovered_word.clone(), - }); - terminal_view.hover_tooltip_update = Task::ready(()); - cx.notify(); - } - } - Some((MaybeNavigationTarget::PathLike(path_like_target), hovered_word)) => { - if Some(hovered_word) - != terminal_view - .hover - .as_ref() - .map(|hover| &hover.hovered_word) - { - terminal_view.hover = None; - terminal_view.hover_tooltip_update = hover_path_like_target( - &workspace, - hovered_word.clone(), - path_like_target, - cx, - ); - cx.notify(); - } - } - None => { - terminal_view.hover = None; - terminal_view.hover_tooltip_update = Task::ready(()); - cx.notify(); - } - } - } - - Event::Open(maybe_navigation_target) => match maybe_navigation_target { - MaybeNavigationTarget::Url(url) => cx.open_url(url), - MaybeNavigationTarget::PathLike(path_like_target) => open_path_like_target( - &workspace, - terminal_view, - path_like_target, - window, - cx, - ), - }, - Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs), - Event::CloseTerminal => cx.emit(ItemEvent::CloseItem), - Event::SelectionsChanged => { - window.invalidate_character_coordinates(); - cx.emit(SearchEvent::ActiveMatchChanged) - } - } - }, - ); - vec![terminal_subscription, terminal_events_subscription] -} - -fn regex_search_for_query(query: &project::search::SearchQuery) -> Option { - let str = query.as_str(); - if query.is_regex() { - if str == "." { - return None; - } - RegexSearch::new(str).ok() - } else { - RegexSearch::new(®ex::escape(str)).ok() - } -} - -struct TerminalScrollbarSettingsWrapper; - -impl GlobalSetting for TerminalScrollbarSettingsWrapper { - fn get_value(_cx: &App) -> &Self { - &Self - } -} - -impl ScrollbarVisibility for TerminalScrollbarSettingsWrapper { - fn visibility(&self, cx: &App) -> scrollbars::ShowScrollbar { - TerminalSettings::get_global(cx) - .scrollbar - .show - .map(Into::into) - .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show) - } -} - -impl TerminalView { - fn key_down(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context) { - self.clear_bell(cx); - self.pause_cursor_blinking(window, cx); - - self.terminal.update(cx, |term, cx| { - let handled = term.try_keystroke( - &event.keystroke, - TerminalSettings::get_global(cx).option_as_meta, - ); - if handled { - cx.stop_propagation(); - } - }); - } - - fn focus_in(&mut self, window: &mut Window, cx: &mut Context) { - self.terminal.update(cx, |terminal, _| { - terminal.set_cursor_shape(self.cursor_shape); - terminal.focus_in(); - }); - - let should_blink = match TerminalSettings::get_global(cx).blinking { - TerminalBlink::Off => false, - TerminalBlink::On => true, - TerminalBlink::TerminalControlled => self.blinking_terminal_enabled, - }; - - if should_blink { - self.blink_manager.update(cx, BlinkManager::enable); - } - - window.invalidate_character_coordinates(); - cx.notify(); - } - - fn focus_out(&mut self, _window: &mut Window, cx: &mut Context) { - self.blink_manager.update(cx, BlinkManager::disable); - self.terminal.update(cx, |terminal, _| { - terminal.focus_out(); - terminal.set_cursor_shape(CursorShape::Hollow); - }); - cx.notify(); - } -} - -impl Render for TerminalView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - // TODO: this should be moved out of render - self.scroll_handle.update(self.terminal.read(cx)); - - if let Some(new_display_offset) = self.scroll_handle.future_display_offset.take() { - self.terminal.update(cx, |term, _| { - let delta = new_display_offset as i32 - term.last_content.display_offset as i32; - match delta.cmp(&0) { - std::cmp::Ordering::Greater => term.scroll_up_by(delta as usize), - std::cmp::Ordering::Less => term.scroll_down_by(-delta as usize), - std::cmp::Ordering::Equal => {} - } - }); - } - - let terminal_handle = self.terminal.clone(); - let terminal_view_handle = cx.entity(); - - let focused = self.focus_handle.is_focused(window); - - div() - .id("terminal-view") - .size_full() - .relative() - .track_focus(&self.focus_handle(cx)) - .key_context(self.dispatch_context(cx)) - .on_action(cx.listener(TerminalView::send_text)) - .on_action(cx.listener(TerminalView::send_keystroke)) - .on_action(cx.listener(TerminalView::copy)) - .on_action(cx.listener(TerminalView::paste)) - .on_action(cx.listener(TerminalView::clear)) - .on_action(cx.listener(TerminalView::scroll_line_up)) - .on_action(cx.listener(TerminalView::scroll_line_down)) - .on_action(cx.listener(TerminalView::scroll_page_up)) - .on_action(cx.listener(TerminalView::scroll_page_down)) - .on_action(cx.listener(TerminalView::scroll_to_top)) - .on_action(cx.listener(TerminalView::scroll_to_bottom)) - .on_action(cx.listener(TerminalView::toggle_vi_mode)) - .on_action(cx.listener(TerminalView::show_character_palette)) - .on_action(cx.listener(TerminalView::select_all)) - .on_action(cx.listener(TerminalView::rerun_task)) - .on_key_down(cx.listener(Self::key_down)) - .on_mouse_down( - MouseButton::Right, - cx.listener(|this, event: &MouseDownEvent, window, cx| { - if !this.terminal.read(cx).mouse_mode(event.modifiers.shift) { - if this.terminal.read(cx).last_content.selection.is_none() { - this.terminal.update(cx, |terminal, _| { - terminal.select_word_at_event_position(event); - }); - }; - this.deploy_context_menu(event.position, window, cx); - cx.notify(); - } - }), - ) - .child( - // TODO: Oddly this wrapper div is needed for TerminalElement to not steal events from the context menu - div() - .id("terminal-view-container") - .size_full() - .bg(cx.theme().colors().editor_background) - .child(TerminalElement::new( - terminal_handle, - terminal_view_handle, - self.workspace.clone(), - self.focus_handle.clone(), - focused, - self.should_show_cursor(focused, cx), - self.block_below_cursor.clone(), - self.mode.clone(), - )) - .when(self.content_mode(window, cx).is_scrollable(), |div| { - div.custom_scrollbars( - Scrollbars::for_settings::() - .show_along(ScrollAxes::Vertical) - .with_track_along( - ScrollAxes::Vertical, - cx.theme().colors().editor_background, - ) - .tracked_scroll_handle(&self.scroll_handle), - window, - cx, - ) - }), - ) - .children(self.context_menu.as_ref().map(|(menu, position, _)| { - deferred( - anchored() - .position(*position) - .anchor(gpui::Corner::TopLeft) - .child(menu.clone()), - ) - .with_priority(1) - })) - } -} - -impl Item for TerminalView { - type Event = ItemEvent; - - fn tab_tooltip_content(&self, cx: &App) -> Option { - Some(TabTooltipContent::Custom(Box::new(Tooltip::element({ - let terminal = self.terminal().read(cx); - let title = terminal.title(false); - let pid = terminal.pid_getter()?.fallback_pid(); - - move |_, _| { - v_flex() - .gap_1() - .child(Label::new(title.clone())) - .child(h_flex().flex_grow().child(Divider::horizontal())) - .child( - Label::new(format!("Process ID (PID): {}", pid)) - .color(Color::Muted) - .size(LabelSize::Small), - ) - .into_any_element() - } - })))) - } - - fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement { - let terminal = self.terminal().read(cx); - let title = terminal.title(true); - - let (icon, icon_color, rerun_button) = match terminal.task() { - Some(terminal_task) => match &terminal_task.status { - TaskStatus::Running => ( - IconName::PlayFilled, - Color::Disabled, - TerminalView::rerun_button(terminal_task), - ), - TaskStatus::Unknown => ( - IconName::Warning, - Color::Warning, - TerminalView::rerun_button(terminal_task), - ), - TaskStatus::Completed { success } => { - let rerun_button = TerminalView::rerun_button(terminal_task); - - if *success { - (IconName::Check, Color::Success, rerun_button) - } else { - (IconName::XCircle, Color::Error, rerun_button) - } - } - }, - None => (IconName::Terminal, Color::Muted, None), - }; - - h_flex() - .gap_1() - .group("term-tab-icon") - .child( - h_flex() - .group("term-tab-icon") - .child( - div() - .when(rerun_button.is_some(), |this| { - this.hover(|style| style.invisible().w_0()) - }) - .child(Icon::new(icon).color(icon_color)), - ) - .when_some(rerun_button, |this, rerun_button| { - this.child( - div() - .absolute() - .visible_on_hover("term-tab-icon") - .child(rerun_button), - ) - }), - ) - .child(Label::new(title).color(params.text_color())) - .into_any() - } - - fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString { - let terminal = self.terminal().read(cx); - terminal.title(detail == 0).into() - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - None - } - - fn buffer_kind(&self, _: &App) -> workspace::item::ItemBufferKind { - workspace::item::ItemBufferKind::Singleton - } - - fn can_split(&self) -> bool { - true - } - - fn clone_on_split( - &self, - workspace_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let Ok(terminal) = self.project.update(cx, |project, cx| { - let cwd = project - .active_project_directory(cx) - .map(|it| it.to_path_buf()); - project.clone_terminal(self.terminal(), cx, cwd) - }) else { - return Task::ready(None); - }; - cx.spawn_in(window, async move |this, cx| { - let terminal = terminal.await.log_err()?; - this.update_in(cx, |this, window, cx| { - cx.new(|cx| { - TerminalView::new( - terminal, - this.workspace.clone(), - workspace_id, - this.project.clone(), - window, - cx, - ) - }) - }) - .ok() - }) - } - - fn is_dirty(&self, cx: &gpui::App) -> bool { - match self.terminal.read(cx).task() { - Some(task) => task.status == TaskStatus::Running, - None => self.has_bell(), - } - } - - fn has_conflict(&self, _cx: &App) -> bool { - false - } - - fn can_save_as(&self, _cx: &App) -> bool { - false - } - - fn as_searchable( - &self, - handle: &Entity, - _: &App, - ) -> Option> { - Some(Box::new(handle.clone())) - } - - fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation { - if self.show_breadcrumbs && !self.terminal().read(cx).breadcrumb_text.trim().is_empty() { - ToolbarItemLocation::PrimaryLeft - } else { - ToolbarItemLocation::Hidden - } - } - - fn breadcrumbs(&self, _: &theme::Theme, cx: &App) -> Option> { - Some(vec![BreadcrumbText { - text: self.terminal().read(cx).breadcrumb_text.clone(), - highlights: None, - font: None, - }]) - } - - fn added_to_workspace( - &mut self, - workspace: &mut Workspace, - _: &mut Window, - cx: &mut Context, - ) { - if self.terminal().read(cx).task().is_none() { - if let Some((new_id, old_id)) = workspace.database_id().zip(self.workspace_id) { - log::debug!( - "Updating workspace id for the terminal, old: {old_id:?}, new: {new_id:?}", - ); - cx.background_spawn(TERMINAL_DB.update_workspace_id( - new_id, - old_id, - cx.entity_id().as_u64(), - )) - .detach(); - } - self.workspace_id = workspace.database_id(); - } - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { - f(*event) - } -} - -impl SerializableItem for TerminalView { - fn serialized_item_kind() -> &'static str { - "Terminal" - } - - fn cleanup( - workspace_id: WorkspaceId, - alive_items: Vec, - _window: &mut Window, - cx: &mut App, - ) -> Task> { - delete_unloaded_items(alive_items, workspace_id, "terminals", &TERMINAL_DB, cx) - } - - fn serialize( - &mut self, - _workspace: &mut Workspace, - item_id: workspace::ItemId, - _closing: bool, - _: &mut Window, - cx: &mut Context, - ) -> Option>> { - let terminal = self.terminal().read(cx); - if terminal.task().is_some() { - return None; - } - - if let Some((cwd, workspace_id)) = terminal.working_directory().zip(self.workspace_id) { - self.cwd_serialized = true; - Some(cx.background_spawn(async move { - TERMINAL_DB - .save_working_directory(item_id, workspace_id, cwd) - .await - })) - } else { - None - } - } - - fn should_serialize(&self, _: &Self::Event) -> bool { - !self.cwd_serialized - } - - fn deserialize( - project: Entity, - workspace: WeakEntity, - workspace_id: workspace::WorkspaceId, - item_id: workspace::ItemId, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - window.spawn(cx, async move |cx| { - let cwd = cx - .update(|_window, cx| { - let from_db = TERMINAL_DB - .get_working_directory(item_id, workspace_id) - .log_err() - .flatten(); - if from_db - .as_ref() - .is_some_and(|from_db| !from_db.as_os_str().is_empty()) - { - from_db - } else { - workspace - .upgrade() - .and_then(|workspace| default_working_directory(workspace.read(cx), cx)) - } - }) - .ok() - .flatten(); - - let terminal = project - .update(cx, |project, cx| project.create_terminal_shell(cwd, cx))? - .await?; - cx.update(|window, cx| { - cx.new(|cx| { - TerminalView::new( - terminal, - workspace, - Some(workspace_id), - project.downgrade(), - window, - cx, - ) - }) - }) - }) - } -} - -impl SearchableItem for TerminalView { - type Match = RangeInclusive; - - fn supported_options(&self) -> SearchOptions { - SearchOptions { - case: false, - word: false, - regex: true, - replacement: false, - selection: false, - find_in_results: false, - } - } - - /// Clear stored matches - fn clear_matches(&mut self, _window: &mut Window, cx: &mut Context) { - self.terminal().update(cx, |term, _| term.matches.clear()) - } - - /// Store matches returned from find_matches somewhere for rendering - fn update_matches( - &mut self, - matches: &[Self::Match], - _active_match_index: Option, - _window: &mut Window, - cx: &mut Context, - ) { - self.terminal() - .update(cx, |term, _| term.matches = matches.to_vec()) - } - - /// Returns the selection content to pre-load into this search - fn query_suggestion(&mut self, _window: &mut Window, cx: &mut Context) -> String { - self.terminal() - .read(cx) - .last_content - .selection_text - .clone() - .unwrap_or_default() - } - - /// Focus match at given index into the Vec of matches - fn activate_match( - &mut self, - index: usize, - _: &[Self::Match], - _window: &mut Window, - cx: &mut Context, - ) { - self.terminal() - .update(cx, |term, _| term.activate_match(index)); - cx.notify(); - } - - /// Add selections for all matches given. - fn select_matches(&mut self, matches: &[Self::Match], _: &mut Window, cx: &mut Context) { - self.terminal() - .update(cx, |term, _| term.select_matches(matches)); - cx.notify(); - } - - /// Get all of the matches for this query, should be done on the background - fn find_matches( - &mut self, - query: Arc, - _: &mut Window, - cx: &mut Context, - ) -> Task> { - if let Some(s) = regex_search_for_query(&query) { - self.terminal() - .update(cx, |term, cx| term.find_matches(s, cx)) - } else { - Task::ready(vec![]) - } - } - - /// Reports back to the search toolbar what the active match should be (the selection) - fn active_match_index( - &mut self, - direction: Direction, - matches: &[Self::Match], - _: &mut Window, - cx: &mut Context, - ) -> Option { - // Selection head might have a value if there's a selection that isn't - // associated with a match. Therefore, if there are no matches, we should - // report None, no matter the state of the terminal - - if !matches.is_empty() { - if let Some(selection_head) = self.terminal().read(cx).selection_head { - // If selection head is contained in a match. Return that match - match direction { - Direction::Prev => { - // If no selection before selection head, return the first match - Some( - matches - .iter() - .enumerate() - .rev() - .find(|(_, search_match)| { - search_match.contains(&selection_head) - || search_match.start() < &selection_head - }) - .map(|(ix, _)| ix) - .unwrap_or(0), - ) - } - Direction::Next => { - // If no selection after selection head, return the last match - Some( - matches - .iter() - .enumerate() - .find(|(_, search_match)| { - search_match.contains(&selection_head) - || search_match.start() > &selection_head - }) - .map(|(ix, _)| ix) - .unwrap_or(matches.len().saturating_sub(1)), - ) - } - } - } else { - // Matches found but no active selection, return the first last one (closest to cursor) - Some(matches.len().saturating_sub(1)) - } - } else { - None - } - } - fn replace( - &mut self, - _: &Self::Match, - _: &SearchQuery, - _window: &mut Window, - _: &mut Context, - ) { - // Replacement is not supported in terminal view, so this is a no-op. - } -} - -///Gets the working directory for the given workspace, respecting the user's settings. -/// None implies "~" on whichever machine we end up on. -pub(crate) fn default_working_directory(workspace: &Workspace, cx: &App) -> Option { - match &TerminalSettings::get_global(cx).working_directory { - WorkingDirectory::CurrentProjectDirectory => workspace - .project() - .read(cx) - .active_project_directory(cx) - .as_deref() - .map(Path::to_path_buf) - .or_else(|| first_project_directory(workspace, cx)), - WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx), - WorkingDirectory::AlwaysHome => None, - WorkingDirectory::Always { directory } => { - shellexpand::full(&directory) //TODO handle this better - .ok() - .map(|dir| Path::new(&dir.to_string()).to_path_buf()) - .filter(|dir| dir.is_dir()) - } - } -} -///Gets the first project's home directory, or the home directory -fn first_project_directory(workspace: &Workspace, cx: &App) -> Option { - let worktree = workspace.worktrees(cx).next()?.read(cx); - let worktree_path = worktree.abs_path(); - if worktree.root_entry()?.is_dir() { - Some(worktree_path.to_path_buf()) - } else { - // If worktree is a file, return its parent directory - worktree_path.parent().map(|p| p.to_path_buf()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::TestAppContext; - use project::{Entry, Project, ProjectPath, Worktree}; - use std::path::Path; - use util::rel_path::RelPath; - use workspace::AppState; - - // Working directory calculation tests - - // No Worktrees in project -> home_dir() - #[gpui::test] - async fn no_worktree(cx: &mut TestAppContext) { - let (project, workspace) = init_test(cx).await; - cx.read(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - //Make sure environment is as expected - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_none()); - - let res = default_working_directory(workspace, cx); - assert_eq!(res, None); - let res = first_project_directory(workspace, cx); - assert_eq!(res, None); - }); - } - - // No active entry, but a worktree, worktree is a file -> parent directory - #[gpui::test] - async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) { - let (project, workspace) = init_test(cx).await; - - create_file_wt(project.clone(), "/root.txt", cx).await; - cx.read(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - //Make sure environment is as expected - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_some()); - - let res = default_working_directory(workspace, cx); - assert_eq!(res, Some(Path::new("/").to_path_buf())); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some(Path::new("/").to_path_buf())); - }); - } - - // No active entry, but a worktree, worktree is a folder -> worktree_folder - #[gpui::test] - async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) { - let (project, workspace) = init_test(cx).await; - - let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await; - cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_some()); - - let res = default_working_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); - }); - } - - // Active entry with a work tree, worktree is a file -> worktree_folder() - #[gpui::test] - async fn active_entry_worktree_is_file(cx: &mut TestAppContext) { - let (project, workspace) = init_test(cx).await; - - let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await; - let (wt2, entry2) = create_file_wt(project.clone(), "/root2.txt", cx).await; - insert_active_entry_for(wt2, entry2, project.clone(), cx); - - cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_some()); - - let res = default_working_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); - }); - } - - // Active entry, with a worktree, worktree is a folder -> worktree_folder - #[gpui::test] - async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) { - let (project, workspace) = init_test(cx).await; - - let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await; - let (wt2, entry2) = create_folder_wt(project.clone(), "/root2/", cx).await; - insert_active_entry_for(wt2, entry2, project.clone(), cx); - - cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_some()); - - let res = default_working_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root2/")).to_path_buf())); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); - }); - } - - /// Creates a worktree with 1 file: /root.txt - pub async fn init_test(cx: &mut TestAppContext) -> (Entity, Entity) { - let params = cx.update(AppState::test); - cx.update(|cx| { - theme::init(theme::LoadThemes::JustBase, cx); - }); - - let project = Project::test(params.fs.clone(), [], cx).await; - let workspace = cx - .add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)) - .root(cx) - .unwrap(); - - (project, workspace) - } - - /// Creates a worktree with 1 folder: /root{suffix}/ - async fn create_folder_wt( - project: Entity, - path: impl AsRef, - cx: &mut TestAppContext, - ) -> (Entity, Entry) { - create_wt(project, true, path, cx).await - } - - /// Creates a worktree with 1 file: /root{suffix}.txt - async fn create_file_wt( - project: Entity, - path: impl AsRef, - cx: &mut TestAppContext, - ) -> (Entity, Entry) { - create_wt(project, false, path, cx).await - } - - async fn create_wt( - project: Entity, - is_dir: bool, - path: impl AsRef, - cx: &mut TestAppContext, - ) -> (Entity, Entry) { - let (wt, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path, true, cx) - }) - .await - .unwrap(); - - let entry = cx - .update(|cx| { - wt.update(cx, |wt, cx| { - wt.create_entry(RelPath::empty().into(), is_dir, None, cx) - }) - }) - .await - .unwrap() - .into_included() - .unwrap(); - - (wt, entry) - } - - pub fn insert_active_entry_for( - wt: Entity, - entry: Entry, - project: Entity, - cx: &mut TestAppContext, - ) { - cx.update(|cx| { - let p = ProjectPath { - worktree_id: wt.read(cx).id(), - path: entry.path, - }; - project.update(cx, |project, cx| project.set_active_path(Some(p), cx)); - }); - } -} diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml deleted file mode 100644 index ed02381eb8..0000000000 --- a/crates/text/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "text" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/text.rs" -doctest = false - -[features] -test-support = ["rand", "util/test-support"] - -[dependencies] -anyhow.workspace = true -clock.workspace = true -collections.workspace = true -log.workspace = true -parking_lot.workspace = true -postage.workspace = true -rand = { workspace = true, optional = true } -regex.workspace = true -rope.workspace = true -smallvec.workspace = true -sum_tree.workspace = true -util.workspace = true - -[dev-dependencies] -collections = { workspace = true, features = ["test-support"] } -ctor.workspace = true -gpui = { workspace = true, features = ["test-support"] } -rand.workspace = true -util = { workspace = true, features = ["test-support"] } -http_client = { workspace = true, features = ["test-support"] } -zlog.workspace = true diff --git a/crates/text/LICENSE-GPL b/crates/text/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/text/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs deleted file mode 100644 index bf660b1302..0000000000 --- a/crates/text/src/anchor.rs +++ /dev/null @@ -1,219 +0,0 @@ -use crate::{ - BufferId, BufferSnapshot, Point, PointUtf16, TextDimension, ToOffset, ToPoint, ToPointUtf16, - locator::Locator, -}; -use std::{cmp::Ordering, fmt::Debug, ops::Range}; -use sum_tree::{Bias, Dimensions}; - -/// A timestamped position in a buffer -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct Anchor { - /// The timestamp of the operation that inserted the text - /// in which this anchor is located. - pub timestamp: clock::Lamport, - /// The byte offset into the text inserted in the operation - /// at `timestamp`. - pub offset: usize, - /// Whether this anchor stays attached to the character *before* or *after* - /// the offset. - pub bias: Bias, - pub buffer_id: Option, -} - -impl Debug for Anchor { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.is_min() { - return write!(f, "Anchor::min({:?})", self.buffer_id); - } - if self.is_max() { - return write!(f, "Anchor::max({:?})", self.buffer_id); - } - - f.debug_struct("Anchor") - .field("timestamp", &self.timestamp) - .field("offset", &self.offset) - .field("bias", &self.bias) - .field("buffer_id", &self.buffer_id) - .finish() - } -} - -impl Anchor { - pub const MIN: Self = Self { - timestamp: clock::Lamport::MIN, - offset: usize::MIN, - bias: Bias::Left, - buffer_id: None, - }; - - pub const MAX: Self = Self { - timestamp: clock::Lamport::MAX, - offset: usize::MAX, - bias: Bias::Right, - buffer_id: None, - }; - - pub fn min_for_buffer(buffer_id: BufferId) -> Self { - Self { - timestamp: clock::Lamport::MIN, - offset: usize::MIN, - bias: Bias::Left, - buffer_id: Some(buffer_id), - } - } - - pub fn max_for_buffer(buffer_id: BufferId) -> Self { - Self { - timestamp: clock::Lamport::MAX, - offset: usize::MAX, - bias: Bias::Right, - buffer_id: Some(buffer_id), - } - } - - pub fn min_min_range_for_buffer(buffer_id: BufferId) -> std::ops::Range { - let min = Self::min_for_buffer(buffer_id); - min..min - } - pub fn max_max_range_for_buffer(buffer_id: BufferId) -> std::ops::Range { - let max = Self::max_for_buffer(buffer_id); - max..max - } - pub fn min_max_range_for_buffer(buffer_id: BufferId) -> std::ops::Range { - Self::min_for_buffer(buffer_id)..Self::max_for_buffer(buffer_id) - } - - pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Ordering { - let fragment_id_comparison = if self.timestamp == other.timestamp { - Ordering::Equal - } else { - buffer - .fragment_id_for_anchor(self) - .cmp(buffer.fragment_id_for_anchor(other)) - }; - - fragment_id_comparison - .then_with(|| self.offset.cmp(&other.offset)) - .then_with(|| self.bias.cmp(&other.bias)) - } - - pub fn min<'a>(&'a self, other: &'a Self, buffer: &BufferSnapshot) -> &'a Self { - if self.cmp(other, buffer).is_le() { - self - } else { - other - } - } - - pub fn max<'a>(&'a self, other: &'a Self, buffer: &BufferSnapshot) -> &'a Self { - if self.cmp(other, buffer).is_ge() { - self - } else { - other - } - } - - pub fn bias(&self, bias: Bias, buffer: &BufferSnapshot) -> Anchor { - match bias { - Bias::Left => self.bias_left(buffer), - Bias::Right => self.bias_right(buffer), - } - } - - pub fn bias_left(&self, buffer: &BufferSnapshot) -> Anchor { - match self.bias { - Bias::Left => *self, - Bias::Right => buffer.anchor_before(self), - } - } - - pub fn bias_right(&self, buffer: &BufferSnapshot) -> Anchor { - match self.bias { - Bias::Left => buffer.anchor_after(self), - Bias::Right => *self, - } - } - - pub fn summary(&self, content: &BufferSnapshot) -> D - where - D: TextDimension, - { - content.summary_for_anchor(self) - } - - /// Returns true when the [`Anchor`] is located inside a visible fragment. - pub fn is_valid(&self, buffer: &BufferSnapshot) -> bool { - if self.is_min() || self.is_max() { - true - } else if self.buffer_id.is_none_or(|id| id != buffer.remote_id) { - false - } else { - let Some(fragment_id) = buffer.try_fragment_id_for_anchor(self) else { - return false; - }; - let (.., item) = buffer - .fragments - .find::, usize>, _>( - &None, - &Some(fragment_id), - Bias::Left, - ); - item.is_some_and(|fragment| fragment.visible) - } - } - - pub fn is_min(&self) -> bool { - self.timestamp == clock::Lamport::MIN - && self.offset == usize::MIN - && self.bias == Bias::Left - } - - pub fn is_max(&self) -> bool { - self.timestamp == clock::Lamport::MAX - && self.offset == usize::MAX - && self.bias == Bias::Right - } -} - -pub trait OffsetRangeExt { - fn to_offset(&self, snapshot: &BufferSnapshot) -> Range; - fn to_point(&self, snapshot: &BufferSnapshot) -> Range; - fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> Range; -} - -impl OffsetRangeExt for Range -where - T: ToOffset, -{ - fn to_offset(&self, snapshot: &BufferSnapshot) -> Range { - self.start.to_offset(snapshot)..self.end.to_offset(snapshot) - } - - fn to_point(&self, snapshot: &BufferSnapshot) -> Range { - self.start.to_offset(snapshot).to_point(snapshot) - ..self.end.to_offset(snapshot).to_point(snapshot) - } - - fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> Range { - self.start.to_offset(snapshot).to_point_utf16(snapshot) - ..self.end.to_offset(snapshot).to_point_utf16(snapshot) - } -} - -pub trait AnchorRangeExt { - fn cmp(&self, b: &Range, buffer: &BufferSnapshot) -> Ordering; - fn overlaps(&self, b: &Range, buffer: &BufferSnapshot) -> bool; -} - -impl AnchorRangeExt for Range { - fn cmp(&self, other: &Range, buffer: &BufferSnapshot) -> Ordering { - match self.start.cmp(&other.start, buffer) { - Ordering::Equal => other.end.cmp(&self.end, buffer), - ord => ord, - } - } - - fn overlaps(&self, other: &Range, buffer: &BufferSnapshot) -> bool { - self.start.cmp(&other.end, buffer).is_lt() && other.start.cmp(&self.end, buffer).is_lt() - } -} diff --git a/crates/text/src/locator.rs b/crates/text/src/locator.rs deleted file mode 100644 index cc94441a3d..0000000000 --- a/crates/text/src/locator.rs +++ /dev/null @@ -1,130 +0,0 @@ -use smallvec::SmallVec; -use std::iter; - -/// An identifier for a position in a ordered collection. -/// -/// Allows prepending and appending without needing to renumber existing locators -/// using `Locator::between(lhs, rhs)`. -/// -/// The initial location for a collection should be `Locator::between(Locator::min(), Locator::max())`, -/// leaving room for items to be inserted before and after it. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Locator(SmallVec<[u64; 4]>); - -impl Locator { - pub const fn min() -> Self { - // SAFETY: 1 is <= 4 - Self(unsafe { SmallVec::from_const_with_len_unchecked([u64::MIN; 4], 1) }) - } - - pub const fn max() -> Self { - // SAFETY: 1 is <= 4 - Self(unsafe { SmallVec::from_const_with_len_unchecked([u64::MAX; 4], 1) }) - } - - pub const fn min_ref() -> &'static Self { - const { &Self::min() } - } - - pub const fn max_ref() -> &'static Self { - const { &Self::max() } - } - - pub fn assign(&mut self, other: &Self) { - self.0.resize(other.0.len(), 0); - self.0.copy_from_slice(&other.0); - } - - pub fn between(lhs: &Self, rhs: &Self) -> Self { - let lhs = lhs.0.iter().copied().chain(iter::repeat(u64::MIN)); - let rhs = rhs.0.iter().copied().chain(iter::repeat(u64::MAX)); - let mut location = SmallVec::new(); - for (lhs, rhs) in lhs.zip(rhs) { - let mid = lhs + ((rhs.saturating_sub(lhs)) >> 48); - location.push(mid); - if mid > lhs { - break; - } - } - Self(location) - } - - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -impl Default for Locator { - fn default() -> Self { - Self::min() - } -} - -impl sum_tree::Item for Locator { - type Summary = Locator; - - fn summary(&self, _cx: ()) -> Self::Summary { - self.clone() - } -} - -impl sum_tree::KeyedItem for Locator { - type Key = Locator; - - fn key(&self) -> Self::Key { - self.clone() - } -} - -impl sum_tree::ContextLessSummary for Locator { - fn zero() -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &Self) { - self.assign(summary); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use rand::prelude::*; - use std::mem; - - #[gpui::test(iterations = 100)] - fn test_locators(mut rng: StdRng) { - let mut lhs = Default::default(); - let mut rhs = Default::default(); - while lhs == rhs { - lhs = Locator( - (0..rng.random_range(1..=5)) - .map(|_| rng.random_range(0..=100)) - .collect(), - ); - rhs = Locator( - (0..rng.random_range(1..=5)) - .map(|_| rng.random_range(0..=100)) - .collect(), - ); - } - - if lhs > rhs { - mem::swap(&mut lhs, &mut rhs); - } - - let middle = Locator::between(&lhs, &rhs); - assert!(middle > lhs); - assert!(middle < rhs); - for ix in 0..middle.0.len() - 1 { - assert!( - middle.0[ix] == *lhs.0.get(ix).unwrap_or(&0) - || middle.0[ix] == *rhs.0.get(ix).unwrap_or(&0) - ); - } - } -} diff --git a/crates/text/src/network.rs b/crates/text/src/network.rs deleted file mode 100644 index d0d1b650ad..0000000000 --- a/crates/text/src/network.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::fmt::Debug; - -use clock::ReplicaId; -use collections::{BTreeMap, HashSet}; - -pub struct Network { - inboxes: BTreeMap>>, - disconnected_peers: HashSet, - rng: R, -} - -#[derive(Clone, Debug)] -struct Envelope { - message: T, -} - -impl Network { - pub fn new(rng: R) -> Self { - Network { - inboxes: BTreeMap::default(), - disconnected_peers: HashSet::default(), - rng, - } - } - - pub fn add_peer(&mut self, id: ReplicaId) { - self.inboxes.insert(id, Vec::new()); - } - - pub fn disconnect_peer(&mut self, id: ReplicaId) { - self.disconnected_peers.insert(id); - self.inboxes.get_mut(&id).unwrap().clear(); - } - - pub fn reconnect_peer(&mut self, id: ReplicaId, replicate_from: ReplicaId) { - assert!(self.disconnected_peers.remove(&id)); - self.replicate(replicate_from, id); - } - - pub fn is_disconnected(&self, id: ReplicaId) -> bool { - self.disconnected_peers.contains(&id) - } - - pub fn contains_disconnected_peers(&self) -> bool { - !self.disconnected_peers.is_empty() - } - - pub fn replicate(&mut self, old_replica_id: ReplicaId, new_replica_id: ReplicaId) { - self.inboxes - .insert(new_replica_id, self.inboxes[&old_replica_id].clone()); - } - - pub fn is_idle(&self) -> bool { - self.inboxes.values().all(|i| i.is_empty()) - } - - pub fn broadcast(&mut self, sender: ReplicaId, messages: Vec) { - // Drop messages from disconnected peers. - if self.disconnected_peers.contains(&sender) { - return; - } - - for (replica, inbox) in self.inboxes.iter_mut() { - if *replica != sender && !self.disconnected_peers.contains(replica) { - for message in &messages { - // Insert one or more duplicates of this message, potentially *before* the previous - // message sent by this peer to simulate out-of-order delivery. - for _ in 0..self.rng.random_range(1..4) { - let insertion_index = self.rng.random_range(0..inbox.len() + 1); - inbox.insert( - insertion_index, - Envelope { - message: message.clone(), - }, - ); - } - } - } - } - } - - pub fn has_unreceived(&self, receiver: ReplicaId) -> bool { - !self.inboxes[&receiver].is_empty() - } - - pub fn receive(&mut self, receiver: ReplicaId) -> Vec { - let inbox = self.inboxes.get_mut(&receiver).unwrap(); - let count = self.rng.random_range(0..inbox.len() + 1); - inbox - .drain(0..count) - .map(|envelope| envelope.message) - .collect() - } -} diff --git a/crates/text/src/operation_queue.rs b/crates/text/src/operation_queue.rs deleted file mode 100644 index f87af381ff..0000000000 --- a/crates/text/src/operation_queue.rs +++ /dev/null @@ -1,165 +0,0 @@ -use clock::Lamport; -use std::{fmt::Debug, ops::Add}; -use sum_tree::{ContextLessSummary, Dimension, Edit, Item, KeyedItem, SumTree}; - -pub trait Operation: Clone + Debug { - fn lamport_timestamp(&self) -> clock::Lamport; -} - -#[derive(Clone, Debug)] -struct OperationItem(T); - -#[derive(Clone, Debug)] -pub struct OperationQueue(SumTree>); - -#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub struct OperationKey(clock::Lamport); - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct OperationSummary { - pub key: OperationKey, - pub len: usize, -} - -impl OperationKey { - pub fn new(timestamp: clock::Lamport) -> Self { - Self(timestamp) - } -} - -impl Default for OperationQueue { - fn default() -> Self { - OperationQueue::new() - } -} - -impl OperationQueue { - pub fn new() -> Self { - OperationQueue(SumTree::default()) - } - - pub fn len(&self) -> usize { - self.0.summary().len - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - pub fn insert(&mut self, mut ops: Vec) { - ops.sort_by_key(|op| op.lamport_timestamp()); - ops.dedup_by_key(|op| op.lamport_timestamp()); - self.0.edit( - ops.into_iter() - .map(|op| Edit::Insert(OperationItem(op))) - .collect(), - (), - ); - } - - pub fn drain(&mut self) -> Self { - let clone = self.clone(); - self.0 = SumTree::default(); - clone - } - - pub fn iter(&self) -> impl Iterator { - self.0.iter().map(|i| &i.0) - } -} - -impl ContextLessSummary for OperationSummary { - fn zero() -> Self { - OperationSummary { - key: OperationKey::new(Lamport::MIN), - len: 0, - } - } - - fn add_summary(&mut self, other: &Self) { - assert!(self.key < other.key); - self.key = other.key; - self.len += other.len; - } -} - -impl Add<&Self> for OperationSummary { - type Output = Self; - - fn add(self, other: &Self) -> Self { - assert!(self.key < other.key); - OperationSummary { - key: other.key, - len: self.len + other.len, - } - } -} - -impl Dimension<'_, OperationSummary> for OperationKey { - fn zero(_cx: ()) -> Self { - OperationKey::new(Lamport::MIN) - } - - fn add_summary(&mut self, summary: &OperationSummary, _: ()) { - assert!(*self <= summary.key); - *self = summary.key; - } -} - -impl Item for OperationItem { - type Summary = OperationSummary; - - fn summary(&self, _cx: ()) -> Self::Summary { - OperationSummary { - key: OperationKey::new(self.0.lamport_timestamp()), - len: 1, - } - } -} - -impl KeyedItem for OperationItem { - type Key = OperationKey; - - fn key(&self) -> Self::Key { - OperationKey::new(self.0.lamport_timestamp()) - } -} - -#[cfg(test)] -mod tests { - use clock::ReplicaId; - - use super::*; - - #[test] - fn test_len() { - let mut clock = clock::Lamport::new(ReplicaId::LOCAL); - - let mut queue = OperationQueue::new(); - assert_eq!(queue.len(), 0); - - queue.insert(vec![ - TestOperation(clock.tick()), - TestOperation(clock.tick()), - ]); - assert_eq!(queue.len(), 2); - - queue.insert(vec![TestOperation(clock.tick())]); - assert_eq!(queue.len(), 3); - - drop(queue.drain()); - assert_eq!(queue.len(), 0); - - queue.insert(vec![TestOperation(clock.tick())]); - assert_eq!(queue.len(), 1); - } - - #[derive(Clone, Debug, Eq, PartialEq)] - struct TestOperation(clock::Lamport); - - impl Operation for TestOperation { - fn lamport_timestamp(&self) -> clock::Lamport { - self.0 - } - } -} diff --git a/crates/text/src/patch.rs b/crates/text/src/patch.rs deleted file mode 100644 index ec495f60fd..0000000000 --- a/crates/text/src/patch.rs +++ /dev/null @@ -1,608 +0,0 @@ -use crate::Edit; -use std::{ - cmp, mem, - ops::{Add, AddAssign, Sub}, -}; - -#[derive(Clone, Default, Debug, PartialEq, Eq)] -pub struct Patch(Vec>); - -impl Patch -where - T: 'static + Clone + Copy + Ord + Default, -{ - pub fn new(edits: Vec>) -> Self { - #[cfg(debug_assertions)] - { - let mut last_edit: Option<&Edit> = None; - for edit in &edits { - if let Some(last_edit) = last_edit { - assert!(edit.old.start > last_edit.old.end); - assert!(edit.new.start > last_edit.new.end); - } - last_edit = Some(edit); - } - } - Self(edits) - } - - pub fn edits(&self) -> &[Edit] { - &self.0 - } - - pub fn into_inner(self) -> Vec> { - self.0 - } - pub fn invert(&mut self) -> &mut Self { - for edit in &mut self.0 { - mem::swap(&mut edit.old, &mut edit.new); - } - self - } - - pub fn clear(&mut self) { - self.0.clear(); - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - pub fn push(&mut self, edit: Edit) { - if edit.is_empty() { - return; - } - - if let Some(last) = self.0.last_mut() { - if last.old.end >= edit.old.start { - last.old.end = edit.old.end; - last.new.end = edit.new.end; - } else { - self.0.push(edit); - } - } else { - self.0.push(edit); - } - } -} - -impl Patch -where - T: 'static - + Copy - + Ord - + Sub - + Add - + AddAssign - + Default, - TDelta: Ord + Copy, -{ - #[must_use] - pub fn compose(&self, new_edits_iter: impl IntoIterator>) -> Self { - let mut old_edits_iter = self.0.iter().cloned().peekable(); - let mut new_edits_iter = new_edits_iter.into_iter().peekable(); - let mut composed = Patch(Vec::new()); - - let mut old_start = T::default(); - let mut new_start = T::default(); - loop { - let old_edit = old_edits_iter.peek_mut(); - let new_edit = new_edits_iter.peek_mut(); - - // Push the old edit if its new end is before the new edit's old start. - if let Some(old_edit) = old_edit.as_ref() { - let new_edit = new_edit.as_ref(); - if new_edit.is_none_or(|new_edit| old_edit.new.end < new_edit.old.start) { - let catchup = old_edit.old.start - old_start; - old_start += catchup; - new_start += catchup; - - let old_end = old_start + old_edit.old_len(); - let new_end = new_start + old_edit.new_len(); - composed.push(Edit { - old: old_start..old_end, - new: new_start..new_end, - }); - old_start = old_end; - new_start = new_end; - old_edits_iter.next(); - continue; - } - } - - // Push the new edit if its old end is before the old edit's new start. - if let Some(new_edit) = new_edit.as_ref() { - let old_edit = old_edit.as_ref(); - if old_edit.is_none_or(|old_edit| new_edit.old.end < old_edit.new.start) { - let catchup = new_edit.new.start - new_start; - old_start += catchup; - new_start += catchup; - - let old_end = old_start + new_edit.old_len(); - let new_end = new_start + new_edit.new_len(); - composed.push(Edit { - old: old_start..old_end, - new: new_start..new_end, - }); - old_start = old_end; - new_start = new_end; - new_edits_iter.next(); - continue; - } - } - - // If we still have edits by this point then they must intersect, so we compose them. - if let Some((old_edit, new_edit)) = old_edit.zip(new_edit) { - if old_edit.new.start < new_edit.old.start { - let catchup = old_edit.old.start - old_start; - old_start += catchup; - new_start += catchup; - - let overshoot = new_edit.old.start - old_edit.new.start; - let old_end = cmp::min(old_start + overshoot, old_edit.old.end); - let new_end = new_start + overshoot; - composed.push(Edit { - old: old_start..old_end, - new: new_start..new_end, - }); - - old_edit.old.start = old_end; - old_edit.new.start += overshoot; - old_start = old_end; - new_start = new_end; - } else { - let catchup = new_edit.new.start - new_start; - old_start += catchup; - new_start += catchup; - - let overshoot = old_edit.new.start - new_edit.old.start; - let old_end = old_start + overshoot; - let new_end = cmp::min(new_start + overshoot, new_edit.new.end); - composed.push(Edit { - old: old_start..old_end, - new: new_start..new_end, - }); - - new_edit.old.start += overshoot; - new_edit.new.start = new_end; - old_start = old_end; - new_start = new_end; - } - - if old_edit.new.end > new_edit.old.end { - let old_end = old_start + cmp::min(old_edit.old_len(), new_edit.old_len()); - let new_end = new_start + new_edit.new_len(); - composed.push(Edit { - old: old_start..old_end, - new: new_start..new_end, - }); - - old_edit.old.start = old_end; - old_edit.new.start = new_edit.old.end; - old_start = old_end; - new_start = new_end; - new_edits_iter.next(); - } else { - let old_end = old_start + old_edit.old_len(); - let new_end = new_start + cmp::min(old_edit.new_len(), new_edit.new_len()); - composed.push(Edit { - old: old_start..old_end, - new: new_start..new_end, - }); - - new_edit.old.start = old_edit.new.end; - new_edit.new.start = new_end; - old_start = old_end; - new_start = new_end; - old_edits_iter.next(); - } - } else { - break; - } - } - - composed - } - - pub fn old_to_new(&self, old: T) -> T { - let ix = match self.0.binary_search_by(|probe| probe.old.start.cmp(&old)) { - Ok(ix) => ix, - Err(ix) => { - if ix == 0 { - return old; - } else { - ix - 1 - } - } - }; - if let Some(edit) = self.0.get(ix) { - if old >= edit.old.end { - edit.new.end + (old - edit.old.end) - } else { - edit.new.start - } - } else { - old - } - } -} - -impl Patch { - pub fn retain_mut(&mut self, f: F) - where - F: FnMut(&mut Edit) -> bool, - { - self.0.retain_mut(f); - } -} - -impl IntoIterator for Patch { - type Item = Edit; - type IntoIter = std::vec::IntoIter>; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -impl<'a, T: Clone> IntoIterator for &'a Patch { - type Item = Edit; - type IntoIter = std::iter::Cloned>>; - - fn into_iter(self) -> Self::IntoIter { - self.0.iter().cloned() - } -} - -impl<'a, T: Clone> IntoIterator for &'a mut Patch { - type Item = Edit; - type IntoIter = std::iter::Cloned>>; - - fn into_iter(self) -> Self::IntoIter { - self.0.iter().cloned() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use rand::prelude::*; - use std::env; - - #[gpui::test] - fn test_one_disjoint_edit() { - assert_patch_composition( - Patch(vec![Edit { - old: 1..3, - new: 1..4, - }]), - Patch(vec![Edit { - old: 0..0, - new: 0..4, - }]), - Patch(vec![ - Edit { - old: 0..0, - new: 0..4, - }, - Edit { - old: 1..3, - new: 5..8, - }, - ]), - ); - - assert_patch_composition( - Patch(vec![Edit { - old: 1..3, - new: 1..4, - }]), - Patch(vec![Edit { - old: 5..9, - new: 5..7, - }]), - Patch(vec![ - Edit { - old: 1..3, - new: 1..4, - }, - Edit { - old: 4..8, - new: 5..7, - }, - ]), - ); - } - - #[gpui::test] - fn test_one_overlapping_edit() { - assert_patch_composition( - Patch(vec![Edit { - old: 1..3, - new: 1..4, - }]), - Patch(vec![Edit { - old: 3..5, - new: 3..6, - }]), - Patch(vec![Edit { - old: 1..4, - new: 1..6, - }]), - ); - } - - #[gpui::test] - fn test_two_disjoint_and_overlapping() { - assert_patch_composition( - Patch(vec![ - Edit { - old: 1..3, - new: 1..4, - }, - Edit { - old: 8..12, - new: 9..11, - }, - ]), - Patch(vec![ - Edit { - old: 0..0, - new: 0..4, - }, - Edit { - old: 3..10, - new: 7..9, - }, - ]), - Patch(vec![ - Edit { - old: 0..0, - new: 0..4, - }, - Edit { - old: 1..12, - new: 5..10, - }, - ]), - ); - } - - #[gpui::test] - fn test_two_new_edits_overlapping_one_old_edit() { - assert_patch_composition( - Patch(vec![Edit { - old: 0..0, - new: 0..3, - }]), - Patch(vec![ - Edit { - old: 0..0, - new: 0..1, - }, - Edit { - old: 1..2, - new: 2..2, - }, - ]), - Patch(vec![Edit { - old: 0..0, - new: 0..3, - }]), - ); - - assert_patch_composition( - Patch(vec![Edit { - old: 2..3, - new: 2..4, - }]), - Patch(vec![ - Edit { - old: 0..2, - new: 0..1, - }, - Edit { - old: 3..3, - new: 2..5, - }, - ]), - Patch(vec![Edit { - old: 0..3, - new: 0..6, - }]), - ); - - assert_patch_composition( - Patch(vec![Edit { - old: 0..0, - new: 0..2, - }]), - Patch(vec![ - Edit { - old: 0..0, - new: 0..2, - }, - Edit { - old: 2..5, - new: 4..4, - }, - ]), - Patch(vec![Edit { - old: 0..3, - new: 0..4, - }]), - ); - } - - #[gpui::test] - fn test_two_new_edits_touching_one_old_edit() { - assert_patch_composition( - Patch(vec![ - Edit { - old: 2..3, - new: 2..4, - }, - Edit { - old: 7..7, - new: 8..11, - }, - ]), - Patch(vec![ - Edit { - old: 2..3, - new: 2..2, - }, - Edit { - old: 4..4, - new: 3..4, - }, - ]), - Patch(vec![ - Edit { - old: 2..3, - new: 2..4, - }, - Edit { - old: 7..7, - new: 8..11, - }, - ]), - ); - } - - #[gpui::test] - fn test_old_to_new() { - let patch = Patch(vec![ - Edit { - old: 2..4, - new: 2..4, - }, - Edit { - old: 7..8, - new: 7..11, - }, - ]); - assert_eq!(patch.old_to_new(0), 0); - assert_eq!(patch.old_to_new(1), 1); - assert_eq!(patch.old_to_new(2), 2); - assert_eq!(patch.old_to_new(3), 2); - assert_eq!(patch.old_to_new(4), 4); - assert_eq!(patch.old_to_new(5), 5); - assert_eq!(patch.old_to_new(6), 6); - assert_eq!(patch.old_to_new(7), 7); - assert_eq!(patch.old_to_new(8), 11); - assert_eq!(patch.old_to_new(9), 12); - } - - #[gpui::test(iterations = 100)] - fn test_random_patch_compositions(mut rng: StdRng) { - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(20); - - let initial_chars = (0..rng.random_range(0..=100)) - .map(|_| rng.random_range(b'a'..=b'z') as char) - .collect::>(); - log::info!("initial chars: {:?}", initial_chars); - - // Generate two sequential patches - let mut patches = Vec::new(); - let mut expected_chars = initial_chars.clone(); - for i in 0..2 { - log::info!("patch {}:", i); - - let mut delta = 0i32; - let mut last_edit_end = 0; - let mut edits = Vec::new(); - - for _ in 0..operations { - if last_edit_end >= expected_chars.len() { - break; - } - - let end = rng.random_range(last_edit_end..=expected_chars.len()); - let start = rng.random_range(last_edit_end..=end); - let old_len = end - start; - - let mut new_len = rng.random_range(0..=3); - if start == end && new_len == 0 { - new_len += 1; - } - - last_edit_end = start + new_len + 1; - - let new_chars = (0..new_len) - .map(|_| rng.random_range(b'A'..=b'Z') as char) - .collect::>(); - log::info!( - " editing {:?}: {:?}", - start..end, - new_chars.iter().collect::() - ); - edits.push(Edit { - old: (start as i32 - delta) as u32..(end as i32 - delta) as u32, - new: start as u32..(start + new_len) as u32, - }); - expected_chars.splice(start..end, new_chars); - - delta += new_len as i32 - old_len as i32; - } - - patches.push(Patch(edits)); - } - - log::info!("old patch: {:?}", &patches[0]); - log::info!("new patch: {:?}", &patches[1]); - log::info!("initial chars: {:?}", initial_chars); - log::info!("final chars: {:?}", expected_chars); - - // Compose the patches, and verify that it has the same effect as applying the - // two patches separately. - let composed = patches[0].compose(&patches[1]); - log::info!("composed patch: {:?}", &composed); - - let mut actual_chars = initial_chars; - for edit in composed.0 { - actual_chars.splice( - edit.new.start as usize..edit.new.start as usize + edit.old.len(), - expected_chars[edit.new.start as usize..edit.new.end as usize] - .iter() - .copied(), - ); - } - - assert_eq!(actual_chars, expected_chars); - } - - #[track_caller] - #[allow(clippy::almost_complete_range)] - fn assert_patch_composition(old: Patch, new: Patch, composed: Patch) { - let original = ('a'..'z').collect::>(); - let inserted = ('A'..'Z').collect::>(); - - let mut expected = original.clone(); - apply_patch(&mut expected, &old, &inserted); - apply_patch(&mut expected, &new, &inserted); - - let mut actual = original; - apply_patch(&mut actual, &composed, &expected); - assert_eq!( - actual.into_iter().collect::(), - expected.into_iter().collect::(), - "expected patch is incorrect" - ); - - assert_eq!(old.compose(&new), composed); - } - - fn apply_patch(text: &mut Vec, patch: &Patch, new_text: &[char]) { - for edit in patch.0.iter().rev() { - text.splice( - edit.old.start as usize..edit.old.end as usize, - new_text[edit.new.start as usize..edit.new.end as usize] - .iter() - .copied(), - ); - } - } -} diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs deleted file mode 100644 index e355f70c49..0000000000 --- a/crates/text/src/selection.rs +++ /dev/null @@ -1,169 +0,0 @@ -use crate::{Anchor, BufferSnapshot, TextDimension}; -use std::cmp::Ordering; -use std::ops::Range; - -#[derive(Default, Copy, Clone, Debug, PartialEq)] -pub enum SelectionGoal { - #[default] - None, - HorizontalPosition(f64), - HorizontalRange { - start: f64, - end: f64, - }, - WrappedHorizontalPosition((u32, f32)), -} - -#[derive(Clone, Debug, PartialEq)] -pub struct Selection { - pub id: usize, - pub start: T, - pub end: T, - pub reversed: bool, - pub goal: SelectionGoal, -} - -impl Selection { - /// A place where the selection had stopped at. - pub fn head(&self) -> T { - if self.reversed { - self.start.clone() - } else { - self.end.clone() - } - } - - /// A place where selection was initiated from. - pub fn tail(&self) -> T { - if self.reversed { - self.end.clone() - } else { - self.start.clone() - } - } - - pub fn map(&self, f: F) -> Selection - where - F: Fn(T) -> S, - { - Selection:: { - id: self.id, - start: f(self.start.clone()), - end: f(self.end.clone()), - reversed: self.reversed, - goal: self.goal, - } - } - - pub fn collapse_to(&mut self, point: T, new_goal: SelectionGoal) { - self.start = point.clone(); - self.end = point; - self.goal = new_goal; - self.reversed = false; - } -} - -impl Selection { - pub fn is_empty(&self) -> bool { - self.start == self.end - } - - pub fn set_head(&mut self, head: T, new_goal: SelectionGoal) { - if head.cmp(&self.tail()) < Ordering::Equal { - if !self.reversed { - self.end = self.start; - self.reversed = true; - } - self.start = head; - } else { - if self.reversed { - self.start = self.end; - self.reversed = false; - } - self.end = head; - } - self.goal = new_goal; - } - - pub fn set_tail(&mut self, tail: T, new_goal: SelectionGoal) { - if tail.cmp(&self.head()) <= Ordering::Equal { - if self.reversed { - self.end = self.start; - self.reversed = false; - } - self.start = tail; - } else { - if !self.reversed { - self.start = self.end; - self.reversed = true; - } - self.end = tail; - } - self.goal = new_goal; - } - - pub fn set_head_tail(&mut self, head: T, tail: T, new_goal: SelectionGoal) { - if head < tail { - self.reversed = true; - self.start = head; - self.end = tail; - } else { - self.reversed = false; - self.start = tail; - self.end = head; - } - self.goal = new_goal; - } - - pub fn swap_head_tail(&mut self) { - if self.reversed { - self.reversed = false; - } else { - std::mem::swap(&mut self.start, &mut self.end); - } - } -} - -impl Selection { - pub fn range(&self) -> Range { - self.start..self.end - } -} - -impl Selection { - pub fn len(&self) -> ::Output { - self.end - self.start - } -} - -impl Selection { - #[cfg(feature = "test-support")] - pub fn from_offset(offset: T) -> Self { - Selection { - id: 0, - start: offset, - end: offset, - goal: SelectionGoal::None, - reversed: false, - } - } - - pub fn equals(&self, offset_range: &Range) -> bool { - self.start == offset_range.start && self.end == offset_range.end - } -} - -impl Selection { - pub fn resolve<'a, D: 'a + TextDimension>( - &'a self, - snapshot: &'a BufferSnapshot, - ) -> Selection { - Selection { - id: self.id, - start: snapshot.summary_for_anchor(&self.start), - end: snapshot.summary_for_anchor(&self.end), - reversed: self.reversed, - goal: self.goal, - } - } -} diff --git a/crates/text/src/subscription.rs b/crates/text/src/subscription.rs deleted file mode 100644 index 50857a2de4..0000000000 --- a/crates/text/src/subscription.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::{Edit, Patch}; -use parking_lot::Mutex; -use std::{ - mem, - sync::{Arc, Weak}, -}; - -#[derive(Default)] -pub struct Topic(Mutex>>>>); - -pub struct Subscription(Arc>>); - -impl Topic -where - T: 'static - + Copy - + Ord - + std::ops::Sub - + std::ops::Add - + std::ops::AddAssign - + Default, - TDelta: Ord + Copy, -{ - pub fn subscribe(&mut self) -> Subscription { - let subscription = Subscription(Default::default()); - self.0.get_mut().push(Arc::downgrade(&subscription.0)); - subscription - } - - pub fn publish(&self, edits: impl Clone + IntoIterator>) { - publish(&mut self.0.lock(), edits); - } - - pub fn publish_mut(&mut self, edits: impl Clone + IntoIterator>) { - publish(self.0.get_mut(), edits); - } -} - -impl Subscription { - pub fn consume(&self) -> Patch { - mem::take(&mut *self.0.lock()) - } -} - -fn publish( - subscriptions: &mut Vec>>>, - edits: impl Clone + IntoIterator>, -) where - T: 'static - + Copy - + Ord - + std::ops::Sub - + std::ops::Add - + std::ops::AddAssign - + Default, - TDelta: Ord + Copy, -{ - subscriptions.retain(|subscription| { - if let Some(subscription) = subscription.upgrade() { - let mut patch = subscription.lock(); - *patch = patch.compose(edits.clone()); - true - } else { - false - } - }); -} diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs deleted file mode 100644 index c9e04e407f..0000000000 --- a/crates/text/src/tests.rs +++ /dev/null @@ -1,812 +0,0 @@ -use super::{network::Network, *}; -use clock::ReplicaId; -use rand::prelude::*; -use std::{ - cmp::Ordering, - env, - iter::Iterator, - time::{Duration, Instant}, -}; - -#[cfg(test)] -#[ctor::ctor] -fn init_logger() { - zlog::init_test(); -} - -#[test] -fn test_edit() { - let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), "abc"); - assert_eq!(buffer.text(), "abc"); - buffer.edit([(3..3, "def")]); - assert_eq!(buffer.text(), "abcdef"); - buffer.edit([(0..0, "ghi")]); - assert_eq!(buffer.text(), "ghiabcdef"); - buffer.edit([(5..5, "jkl")]); - assert_eq!(buffer.text(), "ghiabjklcdef"); - buffer.edit([(6..7, "")]); - assert_eq!(buffer.text(), "ghiabjlcdef"); - buffer.edit([(4..9, "mno")]); - assert_eq!(buffer.text(), "ghiamnoef"); -} - -#[gpui::test(iterations = 100)] -fn test_random_edits(mut rng: StdRng) { - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - let reference_string_len = rng.random_range(0..3); - let mut reference_string = RandomCharIter::new(&mut rng) - .take(reference_string_len) - .collect::(); - let mut buffer = Buffer::new( - ReplicaId::LOCAL, - BufferId::new(1).unwrap(), - reference_string.clone(), - ); - LineEnding::normalize(&mut reference_string); - - buffer.set_group_interval(Duration::from_millis(rng.random_range(0..=200))); - let mut buffer_versions = Vec::new(); - log::info!( - "buffer text {:?}, version: {:?}", - buffer.text(), - buffer.version() - ); - - for _i in 0..operations { - let (edits, _) = buffer.randomly_edit(&mut rng, 5); - for (old_range, new_text) in edits.iter().rev() { - reference_string.replace_range(old_range.clone(), new_text); - } - - assert_eq!(buffer.text(), reference_string); - log::info!( - "buffer text {:?}, version: {:?}", - buffer.text(), - buffer.version() - ); - - if rng.random_bool(0.25) { - buffer.randomly_undo_redo(&mut rng); - reference_string = buffer.text(); - log::info!( - "buffer text {:?}, version: {:?}", - buffer.text(), - buffer.version() - ); - } - - let range = buffer.random_byte_range(0, &mut rng); - assert_eq!( - buffer.text_summary_for_range::(range.clone()), - TextSummary::from(&reference_string[range]) - ); - - buffer.check_invariants(); - - if rng.random_bool(0.3) { - buffer_versions.push((buffer.clone(), buffer.subscribe())); - } - } - - for (old_buffer, subscription) in buffer_versions { - let edits = buffer - .edits_since::(&old_buffer.version) - .collect::>(); - - log::info!( - "applying edits since version {:?} to old text: {:?}: {:?}", - old_buffer.version(), - old_buffer.text(), - edits, - ); - - let mut text = old_buffer.visible_text.clone(); - for edit in edits { - let new_text: String = buffer.text_for_range(edit.new.clone()).collect(); - text.replace(edit.new.start..edit.new.start + edit.old.len(), &new_text); - } - assert_eq!(text.to_string(), buffer.text()); - - assert_eq!( - buffer.rope_for_version(old_buffer.version()).to_string(), - old_buffer.text() - ); - - for _ in 0..5 { - let end_ix = - old_buffer.clip_offset(rng.random_range(0..=old_buffer.len()), Bias::Right); - let start_ix = old_buffer.clip_offset(rng.random_range(0..=end_ix), Bias::Left); - let range = old_buffer.anchor_before(start_ix)..old_buffer.anchor_after(end_ix); - let mut old_text = old_buffer.text_for_range(range.clone()).collect::(); - let edits = buffer - .edits_since_in_range::(&old_buffer.version, range.clone()) - .collect::>(); - log::info!( - "applying edits since version {:?} to old text in range {:?}: {:?}: {:?}", - old_buffer.version(), - start_ix..end_ix, - old_text, - edits, - ); - - let new_text = buffer.text_for_range(range).collect::(); - for edit in edits { - old_text.replace_range( - edit.new.start..edit.new.start + edit.old_len(), - &new_text[edit.new], - ); - } - assert_eq!(old_text, new_text); - } - - assert_eq!( - buffer.has_edits_since(&old_buffer.version), - buffer - .edits_since::(&old_buffer.version) - .next() - .is_some(), - ); - - let subscription_edits = subscription.consume(); - log::info!( - "applying subscription edits since version {:?} to old text: {:?}: {:?}", - old_buffer.version(), - old_buffer.text(), - subscription_edits, - ); - - let mut text = old_buffer.visible_text.clone(); - for edit in subscription_edits.into_inner() { - let new_text: String = buffer.text_for_range(edit.new.clone()).collect(); - text.replace(edit.new.start..edit.new.start + edit.old.len(), &new_text); - } - assert_eq!(text.to_string(), buffer.text()); - } -} - -#[test] -fn test_line_endings() { - assert_eq!(LineEnding::detect(&"🍐✅\n".repeat(1000)), LineEnding::Unix); - assert_eq!(LineEnding::detect(&"abcd\n".repeat(1000)), LineEnding::Unix); - assert_eq!( - LineEnding::detect(&"🍐✅\r\n".repeat(1000)), - LineEnding::Windows - ); - assert_eq!( - LineEnding::detect(&"abcd\r\n".repeat(1000)), - LineEnding::Windows - ); - - let mut buffer = Buffer::new( - ReplicaId::LOCAL, - BufferId::new(1).unwrap(), - "one\r\ntwo\rthree", - ); - assert_eq!(buffer.text(), "one\ntwo\nthree"); - assert_eq!(buffer.line_ending(), LineEnding::Windows); - buffer.check_invariants(); - - buffer.edit([(buffer.len()..buffer.len(), "\r\nfour")]); - buffer.edit([(0..0, "zero\r\n")]); - assert_eq!(buffer.text(), "zero\none\ntwo\nthree\nfour"); - assert_eq!(buffer.line_ending(), LineEnding::Windows); - buffer.check_invariants(); -} - -#[test] -fn test_line_len() { - let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), ""); - buffer.edit([(0..0, "abcd\nefg\nhij")]); - buffer.edit([(12..12, "kl\nmno")]); - buffer.edit([(18..18, "\npqrs\n")]); - buffer.edit([(18..21, "\nPQ")]); - - assert_eq!(buffer.line_len(0), 4); - assert_eq!(buffer.line_len(1), 3); - assert_eq!(buffer.line_len(2), 5); - assert_eq!(buffer.line_len(3), 3); - assert_eq!(buffer.line_len(4), 4); - assert_eq!(buffer.line_len(5), 0); -} - -#[test] -fn test_common_prefix_at_position() { - let text = "a = str; b = δα"; - let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), text); - - let offset1 = offset_after(text, "str"); - let offset2 = offset_after(text, "δα"); - - // the preceding word is a prefix of the suggestion - assert_eq!( - buffer.common_prefix_at(offset1, "string"), - range_of(text, "str"), - ); - // a suffix of the preceding word is a prefix of the suggestion - assert_eq!( - buffer.common_prefix_at(offset1, "tree"), - range_of(text, "tr"), - ); - // the preceding word is a substring of the suggestion, but not a prefix - assert_eq!( - buffer.common_prefix_at(offset1, "astro"), - empty_range_after(text, "str"), - ); - - // prefix matching is case insensitive. - assert_eq!( - buffer.common_prefix_at(offset1, "Strαngε"), - range_of(text, "str"), - ); - assert_eq!( - buffer.common_prefix_at(offset2, "ΔΑΜΝ"), - range_of(text, "δα"), - ); - - fn offset_after(text: &str, part: &str) -> usize { - text.find(part).unwrap() + part.len() - } - - fn empty_range_after(text: &str, part: &str) -> Range { - let offset = offset_after(text, part); - offset..offset - } - - fn range_of(text: &str, part: &str) -> Range { - let start = text.find(part).unwrap(); - start..start + part.len() - } -} - -#[test] -fn test_text_summary_for_range() { - let buffer = Buffer::new( - ReplicaId::LOCAL, - BufferId::new(1).unwrap(), - "ab\nefg\nhklm\nnopqrs\ntuvwxyz", - ); - assert_eq!( - buffer.text_summary_for_range::(0..2), - TextSummary { - len: 2, - chars: 2, - len_utf16: OffsetUtf16(2), - lines: Point::new(0, 2), - first_line_chars: 2, - last_line_chars: 2, - last_line_len_utf16: 2, - longest_row: 0, - longest_row_chars: 2, - } - ); - assert_eq!( - buffer.text_summary_for_range::(1..3), - TextSummary { - len: 2, - chars: 2, - len_utf16: OffsetUtf16(2), - lines: Point::new(1, 0), - first_line_chars: 1, - last_line_chars: 0, - last_line_len_utf16: 0, - longest_row: 0, - longest_row_chars: 1, - } - ); - assert_eq!( - buffer.text_summary_for_range::(1..12), - TextSummary { - len: 11, - chars: 11, - len_utf16: OffsetUtf16(11), - lines: Point::new(3, 0), - first_line_chars: 1, - last_line_chars: 0, - last_line_len_utf16: 0, - longest_row: 2, - longest_row_chars: 4, - } - ); - assert_eq!( - buffer.text_summary_for_range::(0..20), - TextSummary { - len: 20, - chars: 20, - len_utf16: OffsetUtf16(20), - lines: Point::new(4, 1), - first_line_chars: 2, - last_line_chars: 1, - last_line_len_utf16: 1, - longest_row: 3, - longest_row_chars: 6, - } - ); - assert_eq!( - buffer.text_summary_for_range::(0..22), - TextSummary { - len: 22, - chars: 22, - len_utf16: OffsetUtf16(22), - lines: Point::new(4, 3), - first_line_chars: 2, - last_line_chars: 3, - last_line_len_utf16: 3, - longest_row: 3, - longest_row_chars: 6, - } - ); - assert_eq!( - buffer.text_summary_for_range::(7..22), - TextSummary { - len: 15, - chars: 15, - len_utf16: OffsetUtf16(15), - lines: Point::new(2, 3), - first_line_chars: 4, - last_line_chars: 3, - last_line_len_utf16: 3, - longest_row: 1, - longest_row_chars: 6, - } - ); -} - -#[test] -fn test_chars_at() { - let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), ""); - buffer.edit([(0..0, "abcd\nefgh\nij")]); - buffer.edit([(12..12, "kl\nmno")]); - buffer.edit([(18..18, "\npqrs")]); - buffer.edit([(18..21, "\nPQ")]); - - let chars = buffer.chars_at(Point::new(0, 0)); - assert_eq!(chars.collect::(), "abcd\nefgh\nijkl\nmno\nPQrs"); - - let chars = buffer.chars_at(Point::new(1, 0)); - assert_eq!(chars.collect::(), "efgh\nijkl\nmno\nPQrs"); - - let chars = buffer.chars_at(Point::new(2, 0)); - assert_eq!(chars.collect::(), "ijkl\nmno\nPQrs"); - - let chars = buffer.chars_at(Point::new(3, 0)); - assert_eq!(chars.collect::(), "mno\nPQrs"); - - let chars = buffer.chars_at(Point::new(4, 0)); - assert_eq!(chars.collect::(), "PQrs"); - - // Regression test: - let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), ""); - buffer.edit([(0..0, "[workspace]\nmembers = [\n \"xray_core\",\n \"xray_server\",\n \"xray_cli\",\n \"xray_wasm\",\n]\n")]); - buffer.edit([(60..60, "\n")]); - - let chars = buffer.chars_at(Point::new(6, 0)); - assert_eq!(chars.collect::(), " \"xray_wasm\",\n]\n"); -} - -#[test] -fn test_anchors() { - let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), ""); - buffer.edit([(0..0, "abc")]); - let left_anchor = buffer.anchor_before(2); - let right_anchor = buffer.anchor_after(2); - - buffer.edit([(1..1, "def\n")]); - assert_eq!(buffer.text(), "adef\nbc"); - assert_eq!(left_anchor.to_offset(&buffer), 6); - assert_eq!(right_anchor.to_offset(&buffer), 6); - assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); - assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 1 }); - - buffer.edit([(2..3, "")]); - assert_eq!(buffer.text(), "adf\nbc"); - assert_eq!(left_anchor.to_offset(&buffer), 5); - assert_eq!(right_anchor.to_offset(&buffer), 5); - assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); - assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 1 }); - - buffer.edit([(5..5, "ghi\n")]); - assert_eq!(buffer.text(), "adf\nbghi\nc"); - assert_eq!(left_anchor.to_offset(&buffer), 5); - assert_eq!(right_anchor.to_offset(&buffer), 9); - assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); - assert_eq!(right_anchor.to_point(&buffer), Point { row: 2, column: 0 }); - - buffer.edit([(7..9, "")]); - assert_eq!(buffer.text(), "adf\nbghc"); - assert_eq!(left_anchor.to_offset(&buffer), 5); - assert_eq!(right_anchor.to_offset(&buffer), 7); - assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 },); - assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 3 }); - - // Ensure anchoring to a point is equivalent to anchoring to an offset. - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 0 }), - buffer.anchor_before(0) - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 1 }), - buffer.anchor_before(1) - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 2 }), - buffer.anchor_before(2) - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 3 }), - buffer.anchor_before(3) - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 0 }), - buffer.anchor_before(4) - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 1 }), - buffer.anchor_before(5) - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 2 }), - buffer.anchor_before(6) - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 3 }), - buffer.anchor_before(7) - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 4 }), - buffer.anchor_before(8) - ); - - // Comparison between anchors. - let anchor_at_offset_0 = buffer.anchor_before(0); - let anchor_at_offset_1 = buffer.anchor_before(1); - let anchor_at_offset_2 = buffer.anchor_before(2); - - assert_eq!( - anchor_at_offset_0.cmp(&anchor_at_offset_0, &buffer), - Ordering::Equal - ); - assert_eq!( - anchor_at_offset_1.cmp(&anchor_at_offset_1, &buffer), - Ordering::Equal - ); - assert_eq!( - anchor_at_offset_2.cmp(&anchor_at_offset_2, &buffer), - Ordering::Equal - ); - - assert_eq!( - anchor_at_offset_0.cmp(&anchor_at_offset_1, &buffer), - Ordering::Less - ); - assert_eq!( - anchor_at_offset_1.cmp(&anchor_at_offset_2, &buffer), - Ordering::Less - ); - assert_eq!( - anchor_at_offset_0.cmp(&anchor_at_offset_2, &buffer), - Ordering::Less - ); - - assert_eq!( - anchor_at_offset_1.cmp(&anchor_at_offset_0, &buffer), - Ordering::Greater - ); - assert_eq!( - anchor_at_offset_2.cmp(&anchor_at_offset_1, &buffer), - Ordering::Greater - ); - assert_eq!( - anchor_at_offset_2.cmp(&anchor_at_offset_0, &buffer), - Ordering::Greater - ); -} - -#[test] -fn test_anchors_at_start_and_end() { - let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), ""); - let before_start_anchor = buffer.anchor_before(0); - let after_end_anchor = buffer.anchor_after(0); - - buffer.edit([(0..0, "abc")]); - assert_eq!(buffer.text(), "abc"); - assert_eq!(before_start_anchor.to_offset(&buffer), 0); - assert_eq!(after_end_anchor.to_offset(&buffer), 3); - - let after_start_anchor = buffer.anchor_after(0); - let before_end_anchor = buffer.anchor_before(3); - - buffer.edit([(3..3, "def")]); - buffer.edit([(0..0, "ghi")]); - assert_eq!(buffer.text(), "ghiabcdef"); - assert_eq!(before_start_anchor.to_offset(&buffer), 0); - assert_eq!(after_start_anchor.to_offset(&buffer), 3); - assert_eq!(before_end_anchor.to_offset(&buffer), 6); - assert_eq!(after_end_anchor.to_offset(&buffer), 9); -} - -#[test] -fn test_undo_redo() { - let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), "1234"); - // Set group interval to zero so as to not group edits in the undo stack. - buffer.set_group_interval(Duration::from_secs(0)); - - buffer.edit([(1..1, "abx")]); - buffer.edit([(3..4, "yzef")]); - buffer.edit([(3..5, "cd")]); - assert_eq!(buffer.text(), "1abcdef234"); - - let entries = buffer.history.undo_stack.clone(); - assert_eq!(entries.len(), 3); - - buffer.undo_or_redo(entries[0].transaction.clone()); - assert_eq!(buffer.text(), "1cdef234"); - buffer.undo_or_redo(entries[0].transaction.clone()); - assert_eq!(buffer.text(), "1abcdef234"); - - buffer.undo_or_redo(entries[1].transaction.clone()); - assert_eq!(buffer.text(), "1abcdx234"); - buffer.undo_or_redo(entries[2].transaction.clone()); - assert_eq!(buffer.text(), "1abx234"); - buffer.undo_or_redo(entries[1].transaction.clone()); - assert_eq!(buffer.text(), "1abyzef234"); - buffer.undo_or_redo(entries[2].transaction.clone()); - assert_eq!(buffer.text(), "1abcdef234"); - - buffer.undo_or_redo(entries[2].transaction.clone()); - assert_eq!(buffer.text(), "1abyzef234"); - buffer.undo_or_redo(entries[0].transaction.clone()); - assert_eq!(buffer.text(), "1yzef234"); - buffer.undo_or_redo(entries[1].transaction.clone()); - assert_eq!(buffer.text(), "1234"); -} - -#[test] -fn test_history() { - let mut now = Instant::now(); - let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), "123456"); - buffer.set_group_interval(Duration::from_millis(300)); - - let transaction_1 = buffer.start_transaction_at(now).unwrap(); - buffer.edit([(2..4, "cd")]); - buffer.end_transaction_at(now); - assert_eq!(buffer.text(), "12cd56"); - - buffer.start_transaction_at(now); - buffer.edit([(4..5, "e")]); - buffer.end_transaction_at(now).unwrap(); - assert_eq!(buffer.text(), "12cde6"); - - now += buffer.transaction_group_interval() + Duration::from_millis(1); - buffer.start_transaction_at(now); - buffer.edit([(0..1, "a")]); - buffer.edit([(1..1, "b")]); - buffer.end_transaction_at(now).unwrap(); - assert_eq!(buffer.text(), "ab2cde6"); - - // Last transaction happened past the group interval, undo it on its own. - buffer.undo(); - assert_eq!(buffer.text(), "12cde6"); - - // First two transactions happened within the group interval, undo them together. - buffer.undo(); - assert_eq!(buffer.text(), "123456"); - - // Redo the first two transactions together. - buffer.redo(); - assert_eq!(buffer.text(), "12cde6"); - - // Redo the last transaction on its own. - buffer.redo(); - assert_eq!(buffer.text(), "ab2cde6"); - - buffer.start_transaction_at(now); - assert!(buffer.end_transaction_at(now).is_none()); - buffer.undo(); - assert_eq!(buffer.text(), "12cde6"); - - // Redo stack gets cleared after performing an edit. - buffer.start_transaction_at(now); - buffer.edit([(0..0, "X")]); - buffer.end_transaction_at(now); - assert_eq!(buffer.text(), "X12cde6"); - buffer.redo(); - assert_eq!(buffer.text(), "X12cde6"); - buffer.undo(); - assert_eq!(buffer.text(), "12cde6"); - buffer.undo(); - assert_eq!(buffer.text(), "123456"); - - // Transactions can be grouped manually. - buffer.redo(); - buffer.redo(); - assert_eq!(buffer.text(), "X12cde6"); - buffer.group_until_transaction(transaction_1); - buffer.undo(); - assert_eq!(buffer.text(), "123456"); - buffer.redo(); - assert_eq!(buffer.text(), "X12cde6"); -} - -#[test] -fn test_finalize_last_transaction() { - let now = Instant::now(); - let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), "123456"); - buffer.history.group_interval = Duration::from_millis(1); - - buffer.start_transaction_at(now); - buffer.edit([(2..4, "cd")]); - buffer.end_transaction_at(now); - assert_eq!(buffer.text(), "12cd56"); - - buffer.finalize_last_transaction(); - buffer.start_transaction_at(now); - buffer.edit([(4..5, "e")]); - buffer.end_transaction_at(now).unwrap(); - assert_eq!(buffer.text(), "12cde6"); - - buffer.start_transaction_at(now); - buffer.edit([(0..1, "a")]); - buffer.edit([(1..1, "b")]); - buffer.end_transaction_at(now).unwrap(); - assert_eq!(buffer.text(), "ab2cde6"); - - buffer.undo(); - assert_eq!(buffer.text(), "12cd56"); - - buffer.undo(); - assert_eq!(buffer.text(), "123456"); - - buffer.redo(); - assert_eq!(buffer.text(), "12cd56"); - - buffer.redo(); - assert_eq!(buffer.text(), "ab2cde6"); -} - -#[test] -fn test_edited_ranges_for_transaction() { - let now = Instant::now(); - let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), "1234567"); - - buffer.start_transaction_at(now); - buffer.edit([(2..4, "cd")]); - buffer.edit([(6..6, "efg")]); - buffer.end_transaction_at(now); - assert_eq!(buffer.text(), "12cd56efg7"); - - let tx = buffer.finalize_last_transaction().unwrap().clone(); - assert_eq!( - buffer - .edited_ranges_for_transaction::(&tx) - .collect::>(), - [2..4, 6..9] - ); - - buffer.edit([(5..5, "hijk")]); - assert_eq!(buffer.text(), "12cd5hijk6efg7"); - assert_eq!( - buffer - .edited_ranges_for_transaction::(&tx) - .collect::>(), - [2..4, 10..13] - ); - - buffer.edit([(4..4, "l")]); - assert_eq!(buffer.text(), "12cdl5hijk6efg7"); - assert_eq!( - buffer - .edited_ranges_for_transaction::(&tx) - .collect::>(), - [2..4, 11..14] - ); -} - -#[test] -fn test_concurrent_edits() { - let text = "abcdef"; - - let mut buffer1 = Buffer::new(ReplicaId::new(1), BufferId::new(1).unwrap(), text); - let mut buffer2 = Buffer::new(ReplicaId::new(2), BufferId::new(1).unwrap(), text); - let mut buffer3 = Buffer::new(ReplicaId::new(3), BufferId::new(1).unwrap(), text); - - let buf1_op = buffer1.edit([(1..2, "12")]); - assert_eq!(buffer1.text(), "a12cdef"); - let buf2_op = buffer2.edit([(3..4, "34")]); - assert_eq!(buffer2.text(), "abc34ef"); - let buf3_op = buffer3.edit([(5..6, "56")]); - assert_eq!(buffer3.text(), "abcde56"); - - buffer1.apply_op(buf2_op.clone()); - buffer1.apply_op(buf3_op.clone()); - buffer2.apply_op(buf1_op.clone()); - buffer2.apply_op(buf3_op); - buffer3.apply_op(buf1_op); - buffer3.apply_op(buf2_op); - - assert_eq!(buffer1.text(), "a12c34e56"); - assert_eq!(buffer2.text(), "a12c34e56"); - assert_eq!(buffer3.text(), "a12c34e56"); -} - -#[gpui::test(iterations = 100)] -fn test_random_concurrent_edits(mut rng: StdRng) { - let peers = env::var("PEERS") - .map(|i| i.parse().expect("invalid `PEERS` variable")) - .unwrap_or(5); - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - let base_text_len = rng.random_range(0..10); - let base_text = RandomCharIter::new(&mut rng) - .take(base_text_len) - .collect::(); - let mut replica_ids = Vec::new(); - let mut buffers = Vec::new(); - let mut network = Network::new(rng.clone()); - - for i in 0..peers { - let mut buffer = Buffer::new( - ReplicaId::new(i as u16), - BufferId::new(1).unwrap(), - base_text.clone(), - ); - buffer.history.group_interval = Duration::from_millis(rng.random_range(0..=200)); - buffers.push(buffer); - replica_ids.push(ReplicaId::new(i as u16)); - network.add_peer(ReplicaId::new(i as u16)); - } - - log::info!("initial text: {:?}", base_text); - - let mut mutation_count = operations; - loop { - let replica_index = rng.random_range(0..peers); - let replica_id = replica_ids[replica_index]; - let buffer = &mut buffers[replica_index]; - match rng.random_range(0..=100) { - 0..=50 if mutation_count != 0 => { - let op = buffer.randomly_edit(&mut rng, 5).1; - network.broadcast(buffer.replica_id, vec![op]); - log::info!("buffer {:?} text: {:?}", buffer.replica_id, buffer.text()); - mutation_count -= 1; - } - 51..=70 if mutation_count != 0 => { - let ops = buffer.randomly_undo_redo(&mut rng); - network.broadcast(buffer.replica_id, ops); - mutation_count -= 1; - } - 71..=100 if network.has_unreceived(replica_id) => { - let ops = network.receive(replica_id); - if !ops.is_empty() { - log::info!( - "peer {:?} applying {} ops from the network.", - replica_id, - ops.len() - ); - buffer.apply_ops(ops); - } - } - _ => {} - } - buffer.check_invariants(); - - if mutation_count == 0 && network.is_idle() { - break; - } - } - - let first_buffer = &buffers[0]; - for buffer in &buffers[1..] { - assert_eq!( - buffer.text(), - first_buffer.text(), - "Replica {:?} text != Replica 0 text", - buffer.replica_id - ); - buffer.check_invariants(); - } -} diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs deleted file mode 100644 index 866552e4e5..0000000000 --- a/crates/text/src/text.rs +++ /dev/null @@ -1,3564 +0,0 @@ -mod anchor; -pub mod locator; -#[cfg(any(test, feature = "test-support"))] -pub mod network; -pub mod operation_queue; -mod patch; -mod selection; -pub mod subscription; -#[cfg(test)] -mod tests; -mod undo_map; - -pub use anchor::*; -use anyhow::{Context as _, Result}; -use clock::Lamport; -pub use clock::ReplicaId; -use collections::{HashMap, HashSet}; -use locator::Locator; -use operation_queue::OperationQueue; -pub use patch::Patch; -use postage::{oneshot, prelude::*}; - -use regex::Regex; -pub use rope::*; -pub use selection::*; -use std::{ - borrow::Cow, - cmp::{self, Ordering, Reverse}, - fmt::Display, - future::Future, - iter::Iterator, - num::NonZeroU64, - ops::{self, Deref, Range, Sub}, - str, - sync::{Arc, LazyLock}, - time::{Duration, Instant}, -}; -pub use subscription::*; -pub use sum_tree::Bias; -use sum_tree::{Dimensions, FilterCursor, SumTree, TreeMap, TreeSet}; -use undo_map::UndoMap; -use util::debug_panic; - -#[cfg(any(test, feature = "test-support"))] -use util::RandomCharIter; - -static LINE_SEPARATORS_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r"\r\n|\r").expect("Failed to create LINE_SEPARATORS_REGEX")); - -pub type TransactionId = clock::Lamport; - -pub struct Buffer { - snapshot: BufferSnapshot, - history: History, - deferred_ops: OperationQueue, - deferred_replicas: HashSet, - pub lamport_clock: clock::Lamport, - subscriptions: Topic, - edit_id_resolvers: HashMap>>, - wait_for_version_txs: Vec<(clock::Global, oneshot::Sender<()>)>, -} - -#[repr(transparent)] -#[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd, Ord, Eq)] -pub struct BufferId(NonZeroU64); - -impl Display for BufferId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for BufferId { - fn from(id: NonZeroU64) -> Self { - BufferId(id) - } -} - -impl BufferId { - /// Returns Err if `id` is outside of BufferId domain. - pub fn new(id: u64) -> anyhow::Result { - let id = NonZeroU64::new(id).context("Buffer id cannot be 0.")?; - Ok(Self(id)) - } - - /// Increments this buffer id, returning the old value. - /// So that's a post-increment operator in disguise. - pub fn next(&mut self) -> Self { - let old = *self; - self.0 = self.0.saturating_add(1); - old - } - - pub fn to_proto(self) -> u64 { - self.into() - } -} - -impl From for u64 { - fn from(id: BufferId) -> Self { - id.0.get() - } -} - -#[derive(Clone)] -pub struct BufferSnapshot { - replica_id: ReplicaId, - remote_id: BufferId, - visible_text: Rope, - deleted_text: Rope, - line_ending: LineEnding, - undo_map: UndoMap, - fragments: SumTree, - insertions: SumTree, - insertion_slices: TreeSet, - pub version: clock::Global, -} - -#[derive(Clone, Debug)] -pub struct HistoryEntry { - transaction: Transaction, - first_edit_at: Instant, - last_edit_at: Instant, - suppress_grouping: bool, -} - -#[derive(Clone, Debug)] -pub struct Transaction { - pub id: TransactionId, - pub edit_ids: Vec, - pub start: clock::Global, -} - -impl Transaction { - pub fn merge_in(&mut self, other: Transaction) { - self.edit_ids.extend(other.edit_ids); - } -} - -impl HistoryEntry { - pub fn transaction_id(&self) -> TransactionId { - self.transaction.id - } -} - -struct History { - base_text: Rope, - operations: TreeMap, - undo_stack: Vec, - redo_stack: Vec, - transaction_depth: usize, - group_interval: Duration, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct InsertionSlice { - edit_id: clock::Lamport, - insertion_id: clock::Lamport, - range: Range, -} - -impl Ord for InsertionSlice { - fn cmp(&self, other: &Self) -> Ordering { - self.edit_id - .cmp(&other.edit_id) - .then_with(|| self.insertion_id.cmp(&other.insertion_id)) - .then_with(|| self.range.start.cmp(&other.range.start)) - .then_with(|| self.range.end.cmp(&other.range.end)) - } -} - -impl PartialOrd for InsertionSlice { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl InsertionSlice { - fn from_fragment(edit_id: clock::Lamport, fragment: &Fragment) -> Self { - Self { - edit_id, - insertion_id: fragment.timestamp, - range: fragment.insertion_offset..fragment.insertion_offset + fragment.len, - } - } -} - -impl History { - pub fn new(base_text: Rope) -> Self { - Self { - base_text, - operations: Default::default(), - undo_stack: Vec::new(), - redo_stack: Vec::new(), - transaction_depth: 0, - // Don't group transactions in tests unless we opt in, because it's a footgun. - #[cfg(any(test, feature = "test-support"))] - group_interval: Duration::ZERO, - #[cfg(not(any(test, feature = "test-support")))] - group_interval: Duration::from_millis(300), - } - } - - fn push(&mut self, op: Operation) { - self.operations.insert(op.timestamp(), op); - } - - fn start_transaction( - &mut self, - start: clock::Global, - now: Instant, - clock: &mut clock::Lamport, - ) -> Option { - self.transaction_depth += 1; - if self.transaction_depth == 1 { - let id = clock.tick(); - self.undo_stack.push(HistoryEntry { - transaction: Transaction { - id, - start, - edit_ids: Default::default(), - }, - first_edit_at: now, - last_edit_at: now, - suppress_grouping: false, - }); - Some(id) - } else { - None - } - } - - fn end_transaction(&mut self, now: Instant) -> Option<&HistoryEntry> { - assert_ne!(self.transaction_depth, 0); - self.transaction_depth -= 1; - if self.transaction_depth == 0 { - if self - .undo_stack - .last() - .unwrap() - .transaction - .edit_ids - .is_empty() - { - self.undo_stack.pop(); - None - } else { - self.redo_stack.clear(); - let entry = self.undo_stack.last_mut().unwrap(); - entry.last_edit_at = now; - Some(entry) - } - } else { - None - } - } - - fn group(&mut self) -> Option { - let mut count = 0; - let mut entries = self.undo_stack.iter(); - if let Some(mut entry) = entries.next_back() { - while let Some(prev_entry) = entries.next_back() { - if !prev_entry.suppress_grouping - && entry.first_edit_at - prev_entry.last_edit_at < self.group_interval - { - entry = prev_entry; - count += 1; - } else { - break; - } - } - } - self.group_trailing(count) - } - - fn group_until(&mut self, transaction_id: TransactionId) { - let mut count = 0; - for entry in self.undo_stack.iter().rev() { - if entry.transaction_id() == transaction_id { - self.group_trailing(count); - break; - } else if entry.suppress_grouping { - break; - } else { - count += 1; - } - } - } - - fn group_trailing(&mut self, n: usize) -> Option { - let new_len = self.undo_stack.len() - n; - let (entries_to_keep, entries_to_merge) = self.undo_stack.split_at_mut(new_len); - if let Some(last_entry) = entries_to_keep.last_mut() { - for entry in &*entries_to_merge { - for edit_id in &entry.transaction.edit_ids { - last_entry.transaction.edit_ids.push(*edit_id); - } - } - - if let Some(entry) = entries_to_merge.last_mut() { - last_entry.last_edit_at = entry.last_edit_at; - } - } - - self.undo_stack.truncate(new_len); - self.undo_stack.last().map(|e| e.transaction.id) - } - - fn finalize_last_transaction(&mut self) -> Option<&Transaction> { - self.undo_stack.last_mut().map(|entry| { - entry.suppress_grouping = true; - &entry.transaction - }) - } - - fn push_transaction(&mut self, transaction: Transaction, now: Instant) { - assert_eq!(self.transaction_depth, 0); - self.undo_stack.push(HistoryEntry { - transaction, - first_edit_at: now, - last_edit_at: now, - suppress_grouping: false, - }); - } - - /// Differs from `push_transaction` in that it does not clear the redo - /// stack. Intended to be used to create a parent transaction to merge - /// potential child transactions into. - /// - /// The caller is responsible for removing it from the undo history using - /// `forget_transaction` if no edits are merged into it. Otherwise, if edits - /// are merged into this transaction, the caller is responsible for ensuring - /// the redo stack is cleared. The easiest way to ensure the redo stack is - /// cleared is to create transactions with the usual `start_transaction` and - /// `end_transaction` methods and merging the resulting transactions into - /// the transaction created by this method - fn push_empty_transaction( - &mut self, - start: clock::Global, - now: Instant, - clock: &mut clock::Lamport, - ) -> TransactionId { - assert_eq!(self.transaction_depth, 0); - let id = clock.tick(); - let transaction = Transaction { - id, - start, - edit_ids: Vec::new(), - }; - self.undo_stack.push(HistoryEntry { - transaction, - first_edit_at: now, - last_edit_at: now, - suppress_grouping: false, - }); - id - } - - fn push_undo(&mut self, op_id: clock::Lamport) { - assert_ne!(self.transaction_depth, 0); - if let Some(Operation::Edit(_)) = self.operations.get(&op_id) { - let last_transaction = self.undo_stack.last_mut().unwrap(); - last_transaction.transaction.edit_ids.push(op_id); - } - } - - fn pop_undo(&mut self) -> Option<&HistoryEntry> { - assert_eq!(self.transaction_depth, 0); - if let Some(entry) = self.undo_stack.pop() { - self.redo_stack.push(entry); - self.redo_stack.last() - } else { - None - } - } - - fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&HistoryEntry> { - assert_eq!(self.transaction_depth, 0); - - let entry_ix = self - .undo_stack - .iter() - .rposition(|entry| entry.transaction.id == transaction_id)?; - let entry = self.undo_stack.remove(entry_ix); - self.redo_stack.push(entry); - self.redo_stack.last() - } - - fn remove_from_undo_until(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] { - assert_eq!(self.transaction_depth, 0); - - let redo_stack_start_len = self.redo_stack.len(); - if let Some(entry_ix) = self - .undo_stack - .iter() - .rposition(|entry| entry.transaction.id == transaction_id) - { - self.redo_stack - .extend(self.undo_stack.drain(entry_ix..).rev()); - } - &self.redo_stack[redo_stack_start_len..] - } - - fn forget(&mut self, transaction_id: TransactionId) -> Option { - assert_eq!(self.transaction_depth, 0); - if let Some(entry_ix) = self - .undo_stack - .iter() - .rposition(|entry| entry.transaction.id == transaction_id) - { - Some(self.undo_stack.remove(entry_ix).transaction) - } else if let Some(entry_ix) = self - .redo_stack - .iter() - .rposition(|entry| entry.transaction.id == transaction_id) - { - Some(self.redo_stack.remove(entry_ix).transaction) - } else { - None - } - } - - fn transaction(&self, transaction_id: TransactionId) -> Option<&Transaction> { - let entry = self - .undo_stack - .iter() - .rfind(|entry| entry.transaction.id == transaction_id) - .or_else(|| { - self.redo_stack - .iter() - .rfind(|entry| entry.transaction.id == transaction_id) - })?; - Some(&entry.transaction) - } - - fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> { - let entry = self - .undo_stack - .iter_mut() - .rfind(|entry| entry.transaction.id == transaction_id) - .or_else(|| { - self.redo_stack - .iter_mut() - .rfind(|entry| entry.transaction.id == transaction_id) - })?; - Some(&mut entry.transaction) - } - - fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) { - if let Some(transaction) = self.forget(transaction) - && let Some(destination) = self.transaction_mut(destination) - { - destination.edit_ids.extend(transaction.edit_ids); - } - } - - fn pop_redo(&mut self) -> Option<&HistoryEntry> { - assert_eq!(self.transaction_depth, 0); - if let Some(entry) = self.redo_stack.pop() { - self.undo_stack.push(entry); - self.undo_stack.last() - } else { - None - } - } - - fn remove_from_redo(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] { - assert_eq!(self.transaction_depth, 0); - - let undo_stack_start_len = self.undo_stack.len(); - if let Some(entry_ix) = self - .redo_stack - .iter() - .rposition(|entry| entry.transaction.id == transaction_id) - { - self.undo_stack - .extend(self.redo_stack.drain(entry_ix..).rev()); - } - &self.undo_stack[undo_stack_start_len..] - } -} - -struct Edits<'a, D: TextDimension, F: FnMut(&FragmentSummary) -> bool> { - visible_cursor: rope::Cursor<'a>, - deleted_cursor: rope::Cursor<'a>, - fragments_cursor: Option>, - undos: &'a UndoMap, - since: &'a clock::Global, - old_end: D, - new_end: D, - range: Range<(&'a Locator, usize)>, - buffer_id: BufferId, -} - -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct Edit { - pub old: Range, - pub new: Range, -} -impl Edit -where - D: PartialEq, -{ - pub fn is_empty(&self) -> bool { - self.old.start == self.old.end && self.new.start == self.new.end - } -} - -impl Edit -where - D: Sub + Copy, -{ - pub fn old_len(&self) -> DDelta { - self.old.end - self.old.start - } - - pub fn new_len(&self) -> DDelta { - self.new.end - self.new.start - } -} - -impl Edit<(D1, D2)> { - pub fn flatten(self) -> (Edit, Edit) { - ( - Edit { - old: self.old.start.0..self.old.end.0, - new: self.new.start.0..self.new.end.0, - }, - Edit { - old: self.old.start.1..self.old.end.1, - new: self.new.start.1..self.new.end.1, - }, - ) - } -} - -#[derive(Eq, PartialEq, Clone, Debug)] -pub struct Fragment { - pub id: Locator, - pub timestamp: clock::Lamport, - pub insertion_offset: usize, - pub len: usize, - pub visible: bool, - pub deletions: HashSet, - pub max_undos: clock::Global, -} - -#[derive(Eq, PartialEq, Clone, Debug)] -pub struct FragmentSummary { - text: FragmentTextSummary, - max_id: Locator, - max_version: clock::Global, - min_insertion_version: clock::Global, - max_insertion_version: clock::Global, -} - -#[derive(Copy, Default, Clone, Debug, PartialEq, Eq)] -struct FragmentTextSummary { - visible: usize, - deleted: usize, -} - -impl<'a> sum_tree::Dimension<'a, FragmentSummary> for FragmentTextSummary { - fn zero(_: &Option) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a FragmentSummary, _: &Option) { - self.visible += summary.text.visible; - self.deleted += summary.text.deleted; - } -} - -#[derive(Eq, PartialEq, Clone, Debug)] -struct InsertionFragment { - timestamp: clock::Lamport, - split_offset: usize, - fragment_id: Locator, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -struct InsertionFragmentKey { - timestamp: clock::Lamport, - split_offset: usize, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum Operation { - Edit(EditOperation), - Undo(UndoOperation), -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct EditOperation { - pub timestamp: clock::Lamport, - pub version: clock::Global, - pub ranges: Vec>, - pub new_text: Vec>, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct UndoOperation { - pub timestamp: clock::Lamport, - pub version: clock::Global, - pub counts: HashMap, -} - -/// Stores information about the indentation of a line (tabs and spaces). -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct LineIndent { - pub tabs: u32, - pub spaces: u32, - pub line_blank: bool, -} - -impl LineIndent { - pub fn from_chunks(chunks: &mut Chunks) -> Self { - let mut tabs = 0; - let mut spaces = 0; - let mut line_blank = true; - - 'outer: while let Some(chunk) = chunks.peek() { - for ch in chunk.chars() { - if ch == '\t' { - tabs += 1; - } else if ch == ' ' { - spaces += 1; - } else { - if ch != '\n' { - line_blank = false; - } - break 'outer; - } - } - - chunks.next(); - } - - Self { - tabs, - spaces, - line_blank, - } - } - - /// Constructs a new `LineIndent` which only contains spaces. - pub fn spaces(spaces: u32) -> Self { - Self { - tabs: 0, - spaces, - line_blank: true, - } - } - - /// Constructs a new `LineIndent` which only contains tabs. - pub fn tabs(tabs: u32) -> Self { - Self { - tabs, - spaces: 0, - line_blank: true, - } - } - - /// Indicates whether the line is empty. - pub fn is_line_empty(&self) -> bool { - self.tabs == 0 && self.spaces == 0 && self.line_blank - } - - /// Indicates whether the line is blank (contains only whitespace). - pub fn is_line_blank(&self) -> bool { - self.line_blank - } - - /// Returns the number of indentation characters (tabs or spaces). - pub fn raw_len(&self) -> u32 { - self.tabs + self.spaces - } - - /// Returns the number of indentation characters (tabs or spaces), taking tab size into account. - pub fn len(&self, tab_size: u32) -> u32 { - self.tabs * tab_size + self.spaces - } -} - -impl From<&str> for LineIndent { - fn from(value: &str) -> Self { - Self::from_iter(value.chars()) - } -} - -impl FromIterator for LineIndent { - fn from_iter>(chars: T) -> Self { - let mut tabs = 0; - let mut spaces = 0; - let mut line_blank = true; - for c in chars { - if c == '\t' { - tabs += 1; - } else if c == ' ' { - spaces += 1; - } else { - if c != '\n' { - line_blank = false; - } - break; - } - } - Self { - tabs, - spaces, - line_blank, - } - } -} - -impl Buffer { - pub fn new(replica_id: ReplicaId, remote_id: BufferId, base_text: impl Into) -> Buffer { - let mut base_text = base_text.into(); - let line_ending = LineEnding::detect(&base_text); - LineEnding::normalize(&mut base_text); - Self::new_normalized(replica_id, remote_id, line_ending, Rope::from(&*base_text)) - } - - pub fn new_normalized( - replica_id: ReplicaId, - remote_id: BufferId, - line_ending: LineEnding, - normalized: Rope, - ) -> Buffer { - let history = History::new(normalized); - let mut fragments = SumTree::new(&None); - let mut insertions = SumTree::default(); - - let mut lamport_clock = clock::Lamport::new(replica_id); - let mut version = clock::Global::new(); - - let visible_text = history.base_text.clone(); - if !visible_text.is_empty() { - let insertion_timestamp = clock::Lamport::new(ReplicaId::LOCAL); - lamport_clock.observe(insertion_timestamp); - version.observe(insertion_timestamp); - let fragment_id = Locator::between(&Locator::min(), &Locator::max()); - let fragment = Fragment { - id: fragment_id, - timestamp: insertion_timestamp, - insertion_offset: 0, - len: visible_text.len(), - visible: true, - deletions: Default::default(), - max_undos: Default::default(), - }; - insertions.push(InsertionFragment::new(&fragment), ()); - fragments.push(fragment, &None); - } - - Buffer { - snapshot: BufferSnapshot { - replica_id, - remote_id, - visible_text, - deleted_text: Rope::new(), - line_ending, - fragments, - insertions, - version, - undo_map: Default::default(), - insertion_slices: Default::default(), - }, - history, - deferred_ops: OperationQueue::new(), - deferred_replicas: HashSet::default(), - lamport_clock, - subscriptions: Default::default(), - edit_id_resolvers: Default::default(), - wait_for_version_txs: Default::default(), - } - } - - pub fn version(&self) -> clock::Global { - self.version.clone() - } - - pub fn snapshot(&self) -> BufferSnapshot { - self.snapshot.clone() - } - - pub fn branch(&self) -> Self { - Self { - snapshot: self.snapshot.clone(), - history: History::new(self.base_text().clone()), - deferred_ops: OperationQueue::new(), - deferred_replicas: HashSet::default(), - lamport_clock: clock::Lamport::new(ReplicaId::LOCAL_BRANCH), - subscriptions: Default::default(), - edit_id_resolvers: Default::default(), - wait_for_version_txs: Default::default(), - } - } - - pub fn replica_id(&self) -> ReplicaId { - self.lamport_clock.replica_id - } - - pub fn remote_id(&self) -> BufferId { - self.remote_id - } - - pub fn deferred_ops_len(&self) -> usize { - self.deferred_ops.len() - } - - pub fn transaction_group_interval(&self) -> Duration { - self.history.group_interval - } - - pub fn edit(&mut self, edits: R) -> Operation - where - R: IntoIterator, - I: ExactSizeIterator, T)>, - S: ToOffset, - T: Into>, - { - let edits = edits - .into_iter() - .map(|(range, new_text)| (range, new_text.into())); - - self.start_transaction(); - let timestamp = self.lamport_clock.tick(); - let operation = Operation::Edit(self.apply_local_edit(edits, timestamp)); - - self.history.push(operation.clone()); - self.history.push_undo(operation.timestamp()); - self.snapshot.version.observe(operation.timestamp()); - self.end_transaction(); - operation - } - - fn apply_local_edit>>( - &mut self, - edits: impl ExactSizeIterator, T)>, - timestamp: clock::Lamport, - ) -> EditOperation { - let mut edits_patch = Patch::default(); - let mut edit_op = EditOperation { - timestamp, - version: self.version(), - ranges: Vec::with_capacity(edits.len()), - new_text: Vec::with_capacity(edits.len()), - }; - let mut new_insertions = Vec::new(); - let mut insertion_offset = 0; - let mut insertion_slices = Vec::new(); - - let mut edits = edits - .map(|(range, new_text)| (range.to_offset(&*self), new_text)) - .peekable(); - - let mut new_ropes = - RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); - let mut old_fragments = self.fragments.cursor::(&None); - let mut new_fragments = old_fragments.slice(&edits.peek().unwrap().0.start, Bias::Right); - new_ropes.append(new_fragments.summary().text); - - let mut fragment_start = old_fragments.start().visible; - for (range, new_text) in edits { - let new_text = LineEnding::normalize_arc(new_text.into()); - let fragment_end = old_fragments.end().visible; - - // If the current fragment ends before this range, then jump ahead to the first fragment - // that extends past the start of this range, reusing any intervening fragments. - if fragment_end < range.start { - // If the current fragment has been partially consumed, then consume the rest of it - // and advance to the next fragment before slicing. - if fragment_start > old_fragments.start().visible { - if fragment_end > fragment_start { - let mut suffix = old_fragments.item().unwrap().clone(); - suffix.len = fragment_end - fragment_start; - suffix.insertion_offset += fragment_start - old_fragments.start().visible; - new_insertions.push(InsertionFragment::insert_new(&suffix)); - new_ropes.push_fragment(&suffix, suffix.visible); - new_fragments.push(suffix, &None); - } - old_fragments.next(); - } - - let slice = old_fragments.slice(&range.start, Bias::Right); - new_ropes.append(slice.summary().text); - new_fragments.append(slice, &None); - fragment_start = old_fragments.start().visible; - } - - let full_range_start = FullOffset(range.start + old_fragments.start().deleted); - - // Preserve any portion of the current fragment that precedes this range. - if fragment_start < range.start { - let mut prefix = old_fragments.item().unwrap().clone(); - prefix.len = range.start - fragment_start; - prefix.insertion_offset += fragment_start - old_fragments.start().visible; - prefix.id = Locator::between(&new_fragments.summary().max_id, &prefix.id); - new_insertions.push(InsertionFragment::insert_new(&prefix)); - new_ropes.push_fragment(&prefix, prefix.visible); - new_fragments.push(prefix, &None); - fragment_start = range.start; - } - - // Insert the new text before any existing fragments within the range. - if !new_text.is_empty() { - let new_start = new_fragments.summary().text.visible; - - let fragment = Fragment { - id: Locator::between( - &new_fragments.summary().max_id, - old_fragments - .item() - .map_or(&Locator::max(), |old_fragment| &old_fragment.id), - ), - timestamp, - insertion_offset, - len: new_text.len(), - deletions: Default::default(), - max_undos: Default::default(), - visible: true, - }; - edits_patch.push(Edit { - old: fragment_start..fragment_start, - new: new_start..new_start + new_text.len(), - }); - insertion_slices.push(InsertionSlice::from_fragment(timestamp, &fragment)); - new_insertions.push(InsertionFragment::insert_new(&fragment)); - new_ropes.push_str(new_text.as_ref()); - new_fragments.push(fragment, &None); - insertion_offset += new_text.len(); - } - - // Advance through every fragment that intersects this range, marking the intersecting - // portions as deleted. - while fragment_start < range.end { - let fragment = old_fragments.item().unwrap(); - let fragment_end = old_fragments.end().visible; - let mut intersection = fragment.clone(); - let intersection_end = cmp::min(range.end, fragment_end); - if fragment.visible { - intersection.len = intersection_end - fragment_start; - intersection.insertion_offset += fragment_start - old_fragments.start().visible; - intersection.id = - Locator::between(&new_fragments.summary().max_id, &intersection.id); - intersection.deletions.insert(timestamp); - intersection.visible = false; - } - if intersection.len > 0 { - if fragment.visible && !intersection.visible { - let new_start = new_fragments.summary().text.visible; - edits_patch.push(Edit { - old: fragment_start..intersection_end, - new: new_start..new_start, - }); - insertion_slices - .push(InsertionSlice::from_fragment(timestamp, &intersection)); - } - new_insertions.push(InsertionFragment::insert_new(&intersection)); - new_ropes.push_fragment(&intersection, fragment.visible); - new_fragments.push(intersection, &None); - fragment_start = intersection_end; - } - if fragment_end <= range.end { - old_fragments.next(); - } - } - - let full_range_end = FullOffset(range.end + old_fragments.start().deleted); - edit_op.ranges.push(full_range_start..full_range_end); - edit_op.new_text.push(new_text); - } - - // If the current fragment has been partially consumed, then consume the rest of it - // and advance to the next fragment before slicing. - if fragment_start > old_fragments.start().visible { - let fragment_end = old_fragments.end().visible; - if fragment_end > fragment_start { - let mut suffix = old_fragments.item().unwrap().clone(); - suffix.len = fragment_end - fragment_start; - suffix.insertion_offset += fragment_start - old_fragments.start().visible; - new_insertions.push(InsertionFragment::insert_new(&suffix)); - new_ropes.push_fragment(&suffix, suffix.visible); - new_fragments.push(suffix, &None); - } - old_fragments.next(); - } - - let suffix = old_fragments.suffix(); - new_ropes.append(suffix.summary().text); - new_fragments.append(suffix, &None); - let (visible_text, deleted_text) = new_ropes.finish(); - drop(old_fragments); - - self.snapshot.fragments = new_fragments; - self.snapshot.insertions.edit(new_insertions, ()); - self.snapshot.visible_text = visible_text; - self.snapshot.deleted_text = deleted_text; - self.subscriptions.publish_mut(&edits_patch); - self.snapshot.insertion_slices.extend(insertion_slices); - edit_op - } - - pub fn set_line_ending(&mut self, line_ending: LineEnding) { - self.snapshot.line_ending = line_ending; - } - - pub fn apply_ops>(&mut self, ops: I) { - let mut deferred_ops = Vec::new(); - for op in ops { - self.history.push(op.clone()); - if self.can_apply_op(&op) { - self.apply_op(op); - } else { - self.deferred_replicas.insert(op.replica_id()); - deferred_ops.push(op); - } - } - self.deferred_ops.insert(deferred_ops); - self.flush_deferred_ops(); - } - - fn apply_op(&mut self, op: Operation) { - match op { - Operation::Edit(edit) => { - if !self.version.observed(edit.timestamp) { - self.apply_remote_edit( - &edit.version, - &edit.ranges, - &edit.new_text, - edit.timestamp, - ); - self.snapshot.version.observe(edit.timestamp); - self.lamport_clock.observe(edit.timestamp); - self.resolve_edit(edit.timestamp); - } - } - Operation::Undo(undo) => { - if !self.version.observed(undo.timestamp) { - self.apply_undo(&undo); - self.snapshot.version.observe(undo.timestamp); - self.lamport_clock.observe(undo.timestamp); - } - } - } - self.wait_for_version_txs.retain_mut(|(version, tx)| { - if self.snapshot.version().observed_all(version) { - tx.try_send(()).ok(); - false - } else { - true - } - }); - } - - fn apply_remote_edit( - &mut self, - version: &clock::Global, - ranges: &[Range], - new_text: &[Arc], - timestamp: clock::Lamport, - ) { - if ranges.is_empty() { - return; - } - - let edits = ranges.iter().zip(new_text.iter()); - let mut edits_patch = Patch::default(); - let mut insertion_slices = Vec::new(); - let cx = Some(version.clone()); - let mut new_insertions = Vec::new(); - let mut insertion_offset = 0; - let mut new_ropes = - RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); - let mut old_fragments = self - .fragments - .cursor::>(&cx); - let mut new_fragments = - old_fragments.slice(&VersionedFullOffset::Offset(ranges[0].start), Bias::Left); - new_ropes.append(new_fragments.summary().text); - - let mut fragment_start = old_fragments.start().0.full_offset(); - for (range, new_text) in edits { - let fragment_end = old_fragments.end().0.full_offset(); - - // If the current fragment ends before this range, then jump ahead to the first fragment - // that extends past the start of this range, reusing any intervening fragments. - if fragment_end < range.start { - // If the current fragment has been partially consumed, then consume the rest of it - // and advance to the next fragment before slicing. - if fragment_start > old_fragments.start().0.full_offset() { - if fragment_end > fragment_start { - let mut suffix = old_fragments.item().unwrap().clone(); - suffix.len = fragment_end.0 - fragment_start.0; - suffix.insertion_offset += - fragment_start - old_fragments.start().0.full_offset(); - new_insertions.push(InsertionFragment::insert_new(&suffix)); - new_ropes.push_fragment(&suffix, suffix.visible); - new_fragments.push(suffix, &None); - } - old_fragments.next(); - } - - let slice = - old_fragments.slice(&VersionedFullOffset::Offset(range.start), Bias::Left); - new_ropes.append(slice.summary().text); - new_fragments.append(slice, &None); - fragment_start = old_fragments.start().0.full_offset(); - } - - // If we are at the end of a non-concurrent fragment, advance to the next one. - let fragment_end = old_fragments.end().0.full_offset(); - if fragment_end == range.start && fragment_end > fragment_start { - let mut fragment = old_fragments.item().unwrap().clone(); - fragment.len = fragment_end.0 - fragment_start.0; - fragment.insertion_offset += fragment_start - old_fragments.start().0.full_offset(); - new_insertions.push(InsertionFragment::insert_new(&fragment)); - new_ropes.push_fragment(&fragment, fragment.visible); - new_fragments.push(fragment, &None); - old_fragments.next(); - fragment_start = old_fragments.start().0.full_offset(); - } - - // Skip over insertions that are concurrent to this edit, but have a lower lamport - // timestamp. - while let Some(fragment) = old_fragments.item() { - if fragment_start == range.start && fragment.timestamp > timestamp { - new_ropes.push_fragment(fragment, fragment.visible); - new_fragments.push(fragment.clone(), &None); - old_fragments.next(); - debug_assert_eq!(fragment_start, range.start); - } else { - break; - } - } - debug_assert!(fragment_start <= range.start); - - // Preserve any portion of the current fragment that precedes this range. - if fragment_start < range.start { - let mut prefix = old_fragments.item().unwrap().clone(); - prefix.len = range.start.0 - fragment_start.0; - prefix.insertion_offset += fragment_start - old_fragments.start().0.full_offset(); - prefix.id = Locator::between(&new_fragments.summary().max_id, &prefix.id); - new_insertions.push(InsertionFragment::insert_new(&prefix)); - fragment_start = range.start; - new_ropes.push_fragment(&prefix, prefix.visible); - new_fragments.push(prefix, &None); - } - - // Insert the new text before any existing fragments within the range. - if !new_text.is_empty() { - let mut old_start = old_fragments.start().1; - if old_fragments.item().is_some_and(|f| f.visible) { - old_start += fragment_start.0 - old_fragments.start().0.full_offset().0; - } - let new_start = new_fragments.summary().text.visible; - let fragment = Fragment { - id: Locator::between( - &new_fragments.summary().max_id, - old_fragments - .item() - .map_or(&Locator::max(), |old_fragment| &old_fragment.id), - ), - timestamp, - insertion_offset, - len: new_text.len(), - deletions: Default::default(), - max_undos: Default::default(), - visible: true, - }; - edits_patch.push(Edit { - old: old_start..old_start, - new: new_start..new_start + new_text.len(), - }); - insertion_slices.push(InsertionSlice::from_fragment(timestamp, &fragment)); - new_insertions.push(InsertionFragment::insert_new(&fragment)); - new_ropes.push_str(new_text); - new_fragments.push(fragment, &None); - insertion_offset += new_text.len(); - } - - // Advance through every fragment that intersects this range, marking the intersecting - // portions as deleted. - while fragment_start < range.end { - let fragment = old_fragments.item().unwrap(); - let fragment_end = old_fragments.end().0.full_offset(); - let mut intersection = fragment.clone(); - let intersection_end = cmp::min(range.end, fragment_end); - if fragment.was_visible(version, &self.undo_map) { - intersection.len = intersection_end.0 - fragment_start.0; - intersection.insertion_offset += - fragment_start - old_fragments.start().0.full_offset(); - intersection.id = - Locator::between(&new_fragments.summary().max_id, &intersection.id); - intersection.deletions.insert(timestamp); - intersection.visible = false; - insertion_slices.push(InsertionSlice::from_fragment(timestamp, &intersection)); - } - if intersection.len > 0 { - if fragment.visible && !intersection.visible { - let old_start = old_fragments.start().1 - + (fragment_start.0 - old_fragments.start().0.full_offset().0); - let new_start = new_fragments.summary().text.visible; - edits_patch.push(Edit { - old: old_start..old_start + intersection.len, - new: new_start..new_start, - }); - } - new_insertions.push(InsertionFragment::insert_new(&intersection)); - new_ropes.push_fragment(&intersection, fragment.visible); - new_fragments.push(intersection, &None); - fragment_start = intersection_end; - } - if fragment_end <= range.end { - old_fragments.next(); - } - } - } - - // If the current fragment has been partially consumed, then consume the rest of it - // and advance to the next fragment before slicing. - if fragment_start > old_fragments.start().0.full_offset() { - let fragment_end = old_fragments.end().0.full_offset(); - if fragment_end > fragment_start { - let mut suffix = old_fragments.item().unwrap().clone(); - suffix.len = fragment_end.0 - fragment_start.0; - suffix.insertion_offset += fragment_start - old_fragments.start().0.full_offset(); - new_insertions.push(InsertionFragment::insert_new(&suffix)); - new_ropes.push_fragment(&suffix, suffix.visible); - new_fragments.push(suffix, &None); - } - old_fragments.next(); - } - - let suffix = old_fragments.suffix(); - new_ropes.append(suffix.summary().text); - new_fragments.append(suffix, &None); - let (visible_text, deleted_text) = new_ropes.finish(); - drop(old_fragments); - - self.snapshot.fragments = new_fragments; - self.snapshot.visible_text = visible_text; - self.snapshot.deleted_text = deleted_text; - self.snapshot.insertions.edit(new_insertions, ()); - self.snapshot.insertion_slices.extend(insertion_slices); - self.subscriptions.publish_mut(&edits_patch) - } - - fn fragment_ids_for_edits<'a>( - &'a self, - edit_ids: impl Iterator, - ) -> Vec<&'a Locator> { - // Get all of the insertion slices changed by the given edits. - let mut insertion_slices = Vec::new(); - for edit_id in edit_ids { - let insertion_slice = InsertionSlice { - edit_id: *edit_id, - insertion_id: clock::Lamport::MIN, - range: 0..0, - }; - let slices = self - .snapshot - .insertion_slices - .iter_from(&insertion_slice) - .take_while(|slice| slice.edit_id == *edit_id); - insertion_slices.extend(slices) - } - insertion_slices - .sort_unstable_by_key(|s| (s.insertion_id, s.range.start, Reverse(s.range.end))); - - // Get all of the fragments corresponding to these insertion slices. - let mut fragment_ids = Vec::new(); - let mut insertions_cursor = self.insertions.cursor::(()); - for insertion_slice in &insertion_slices { - if insertion_slice.insertion_id != insertions_cursor.start().timestamp - || insertion_slice.range.start > insertions_cursor.start().split_offset - { - insertions_cursor.seek_forward( - &InsertionFragmentKey { - timestamp: insertion_slice.insertion_id, - split_offset: insertion_slice.range.start, - }, - Bias::Left, - ); - } - while let Some(item) = insertions_cursor.item() { - if item.timestamp != insertion_slice.insertion_id - || item.split_offset >= insertion_slice.range.end - { - break; - } - fragment_ids.push(&item.fragment_id); - insertions_cursor.next(); - } - } - fragment_ids.sort_unstable(); - fragment_ids - } - - fn apply_undo(&mut self, undo: &UndoOperation) { - self.snapshot.undo_map.insert(undo); - - let mut edits = Patch::default(); - let mut old_fragments = self - .fragments - .cursor::, usize>>(&None); - let mut new_fragments = SumTree::new(&None); - let mut new_ropes = - RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); - - for fragment_id in self.fragment_ids_for_edits(undo.counts.keys()) { - let preceding_fragments = old_fragments.slice(&Some(fragment_id), Bias::Left); - new_ropes.append(preceding_fragments.summary().text); - new_fragments.append(preceding_fragments, &None); - - if let Some(fragment) = old_fragments.item() { - let mut fragment = fragment.clone(); - let fragment_was_visible = fragment.visible; - - fragment.visible = fragment.is_visible(&self.undo_map); - fragment.max_undos.observe(undo.timestamp); - - let old_start = old_fragments.start().1; - let new_start = new_fragments.summary().text.visible; - if fragment_was_visible && !fragment.visible { - edits.push(Edit { - old: old_start..old_start + fragment.len, - new: new_start..new_start, - }); - } else if !fragment_was_visible && fragment.visible { - edits.push(Edit { - old: old_start..old_start, - new: new_start..new_start + fragment.len, - }); - } - new_ropes.push_fragment(&fragment, fragment_was_visible); - new_fragments.push(fragment, &None); - - old_fragments.next(); - } - } - - let suffix = old_fragments.suffix(); - new_ropes.append(suffix.summary().text); - new_fragments.append(suffix, &None); - - drop(old_fragments); - let (visible_text, deleted_text) = new_ropes.finish(); - self.snapshot.fragments = new_fragments; - self.snapshot.visible_text = visible_text; - self.snapshot.deleted_text = deleted_text; - self.subscriptions.publish_mut(&edits); - } - - fn flush_deferred_ops(&mut self) { - self.deferred_replicas.clear(); - let mut deferred_ops = Vec::new(); - for op in self.deferred_ops.drain().iter().cloned() { - if self.can_apply_op(&op) { - self.apply_op(op); - } else { - self.deferred_replicas.insert(op.replica_id()); - deferred_ops.push(op); - } - } - self.deferred_ops.insert(deferred_ops); - } - - fn can_apply_op(&self, op: &Operation) -> bool { - if self.deferred_replicas.contains(&op.replica_id()) { - false - } else { - self.version.observed_all(match op { - Operation::Edit(edit) => &edit.version, - Operation::Undo(undo) => &undo.version, - }) - } - } - - pub fn has_deferred_ops(&self) -> bool { - !self.deferred_ops.is_empty() - } - - pub fn peek_undo_stack(&self) -> Option<&HistoryEntry> { - self.history.undo_stack.last() - } - - pub fn peek_redo_stack(&self) -> Option<&HistoryEntry> { - self.history.redo_stack.last() - } - - pub fn start_transaction(&mut self) -> Option { - self.start_transaction_at(Instant::now()) - } - - pub fn start_transaction_at(&mut self, now: Instant) -> Option { - self.history - .start_transaction(self.version.clone(), now, &mut self.lamport_clock) - } - - pub fn end_transaction(&mut self) -> Option<(TransactionId, clock::Global)> { - self.end_transaction_at(Instant::now()) - } - - pub fn end_transaction_at(&mut self, now: Instant) -> Option<(TransactionId, clock::Global)> { - if let Some(entry) = self.history.end_transaction(now) { - let since = entry.transaction.start.clone(); - let id = self.history.group().unwrap(); - Some((id, since)) - } else { - None - } - } - - pub fn finalize_last_transaction(&mut self) -> Option<&Transaction> { - self.history.finalize_last_transaction() - } - - pub fn group_until_transaction(&mut self, transaction_id: TransactionId) { - self.history.group_until(transaction_id); - } - - pub fn base_text(&self) -> &Rope { - &self.history.base_text - } - - pub fn operations(&self) -> &TreeMap { - &self.history.operations - } - - pub fn undo(&mut self) -> Option<(TransactionId, Operation)> { - if let Some(entry) = self.history.pop_undo() { - let transaction = entry.transaction.clone(); - let transaction_id = transaction.id; - let op = self.undo_or_redo(transaction); - Some((transaction_id, op)) - } else { - None - } - } - - pub fn undo_transaction(&mut self, transaction_id: TransactionId) -> Option { - let transaction = self - .history - .remove_from_undo(transaction_id)? - .transaction - .clone(); - Some(self.undo_or_redo(transaction)) - } - - pub fn undo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec { - let transactions = self - .history - .remove_from_undo_until(transaction_id) - .iter() - .map(|entry| entry.transaction.clone()) - .collect::>(); - - transactions - .into_iter() - .map(|transaction| self.undo_or_redo(transaction)) - .collect() - } - - pub fn forget_transaction(&mut self, transaction_id: TransactionId) -> Option { - self.history.forget(transaction_id) - } - - pub fn get_transaction(&self, transaction_id: TransactionId) -> Option<&Transaction> { - self.history.transaction(transaction_id) - } - - pub fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) { - self.history.merge_transactions(transaction, destination); - } - - pub fn redo(&mut self) -> Option<(TransactionId, Operation)> { - if let Some(entry) = self.history.pop_redo() { - let transaction = entry.transaction.clone(); - let transaction_id = transaction.id; - let op = self.undo_or_redo(transaction); - Some((transaction_id, op)) - } else { - None - } - } - - pub fn redo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec { - let transactions = self - .history - .remove_from_redo(transaction_id) - .iter() - .map(|entry| entry.transaction.clone()) - .collect::>(); - - transactions - .into_iter() - .map(|transaction| self.undo_or_redo(transaction)) - .collect() - } - - fn undo_or_redo(&mut self, transaction: Transaction) -> Operation { - let mut counts = HashMap::default(); - for edit_id in transaction.edit_ids { - counts.insert(edit_id, self.undo_map.undo_count(edit_id).saturating_add(1)); - } - - let operation = self.undo_operations(counts); - self.history.push(operation.clone()); - operation - } - - pub fn undo_operations(&mut self, counts: HashMap) -> Operation { - let timestamp = self.lamport_clock.tick(); - let version = self.version(); - self.snapshot.version.observe(timestamp); - let undo = UndoOperation { - timestamp, - version, - counts, - }; - self.apply_undo(&undo); - Operation::Undo(undo) - } - - pub fn push_transaction(&mut self, transaction: Transaction, now: Instant) { - self.history.push_transaction(transaction, now); - } - - /// Differs from `push_transaction` in that it does not clear the redo stack. - /// The caller responsible for - /// Differs from `push_transaction` in that it does not clear the redo - /// stack. Intended to be used to create a parent transaction to merge - /// potential child transactions into. - /// - /// The caller is responsible for removing it from the undo history using - /// `forget_transaction` if no edits are merged into it. Otherwise, if edits - /// are merged into this transaction, the caller is responsible for ensuring - /// the redo stack is cleared. The easiest way to ensure the redo stack is - /// cleared is to create transactions with the usual `start_transaction` and - /// `end_transaction` methods and merging the resulting transactions into - /// the transaction created by this method - pub fn push_empty_transaction(&mut self, now: Instant) -> TransactionId { - self.history - .push_empty_transaction(self.version.clone(), now, &mut self.lamport_clock) - } - - pub fn edited_ranges_for_transaction_id( - &self, - transaction_id: TransactionId, - ) -> impl '_ + Iterator> - where - D: TextDimension, - { - self.history - .transaction(transaction_id) - .into_iter() - .flat_map(|transaction| self.edited_ranges_for_transaction(transaction)) - } - - pub fn edited_ranges_for_edit_ids<'a, D>( - &'a self, - edit_ids: impl IntoIterator, - ) -> impl 'a + Iterator> - where - D: TextDimension, - { - // get fragment ranges - let mut cursor = self - .fragments - .cursor::, usize>>(&None); - let offset_ranges = self - .fragment_ids_for_edits(edit_ids.into_iter()) - .into_iter() - .filter_map(move |fragment_id| { - cursor.seek_forward(&Some(fragment_id), Bias::Left); - let fragment = cursor.item()?; - let start_offset = cursor.start().1; - let end_offset = start_offset + if fragment.visible { fragment.len } else { 0 }; - Some(start_offset..end_offset) - }); - - // combine adjacent ranges - let mut prev_range: Option> = None; - let disjoint_ranges = offset_ranges - .map(Some) - .chain([None]) - .filter_map(move |range| { - if let Some((range, prev_range)) = range.as_ref().zip(prev_range.as_mut()) - && prev_range.end == range.start - { - prev_range.end = range.end; - return None; - } - let result = prev_range.clone(); - prev_range = range; - result - }); - - // convert to the desired text dimension. - let mut position = D::zero(()); - let mut rope_cursor = self.visible_text.cursor(0); - disjoint_ranges.map(move |range| { - position.add_assign(&rope_cursor.summary(range.start)); - let start = position; - position.add_assign(&rope_cursor.summary(range.end)); - let end = position; - start..end - }) - } - - pub fn edited_ranges_for_transaction<'a, D>( - &'a self, - transaction: &'a Transaction, - ) -> impl 'a + Iterator> - where - D: TextDimension, - { - self.edited_ranges_for_edit_ids(&transaction.edit_ids) - } - - pub fn subscribe(&mut self) -> Subscription { - self.subscriptions.subscribe() - } - - pub fn wait_for_edits>( - &mut self, - edit_ids: It, - ) -> impl 'static + Future> + use { - let mut futures = Vec::new(); - for edit_id in edit_ids { - if !self.version.observed(edit_id) { - let (tx, rx) = oneshot::channel(); - self.edit_id_resolvers.entry(edit_id).or_default().push(tx); - futures.push(rx); - } - } - - async move { - for mut future in futures { - if future.recv().await.is_none() { - anyhow::bail!("gave up waiting for edits"); - } - } - Ok(()) - } - } - - pub fn wait_for_anchors>( - &mut self, - anchors: It, - ) -> impl 'static + Future> + use { - let mut futures = Vec::new(); - for anchor in anchors { - if !self.version.observed(anchor.timestamp) && !anchor.is_max() && !anchor.is_min() { - let (tx, rx) = oneshot::channel(); - self.edit_id_resolvers - .entry(anchor.timestamp) - .or_default() - .push(tx); - futures.push(rx); - } - } - - async move { - for mut future in futures { - if future.recv().await.is_none() { - anyhow::bail!("gave up waiting for anchors"); - } - } - Ok(()) - } - } - - pub fn wait_for_version( - &mut self, - version: clock::Global, - ) -> impl Future> + use<> { - let mut rx = None; - if !self.snapshot.version.observed_all(&version) { - let channel = oneshot::channel(); - self.wait_for_version_txs.push((version, channel.0)); - rx = Some(channel.1); - } - async move { - if let Some(mut rx) = rx - && rx.recv().await.is_none() - { - anyhow::bail!("gave up waiting for version"); - } - Ok(()) - } - } - - pub fn give_up_waiting(&mut self) { - self.edit_id_resolvers.clear(); - self.wait_for_version_txs.clear(); - } - - fn resolve_edit(&mut self, edit_id: clock::Lamport) { - for mut tx in self - .edit_id_resolvers - .remove(&edit_id) - .into_iter() - .flatten() - { - tx.try_send(()).ok(); - } - } -} - -#[cfg(any(test, feature = "test-support"))] -impl Buffer { - #[track_caller] - pub fn edit_via_marked_text(&mut self, marked_string: &str) { - let edits = self.edits_for_marked_text(marked_string); - self.edit(edits); - } - - #[track_caller] - pub fn edits_for_marked_text(&self, marked_string: &str) -> Vec<(Range, String)> { - let old_text = self.text(); - let (new_text, mut ranges) = util::test::marked_text_ranges(marked_string, false); - if ranges.is_empty() { - ranges.push(0..new_text.len()); - } - - assert_eq!( - old_text[..ranges[0].start], - new_text[..ranges[0].start], - "invalid edit" - ); - - let mut delta = 0; - let mut edits = Vec::new(); - let mut ranges = ranges.into_iter().peekable(); - - while let Some(inserted_range) = ranges.next() { - let new_start = inserted_range.start; - let old_start = (new_start as isize - delta) as usize; - - let following_text = if let Some(next_range) = ranges.peek() { - &new_text[inserted_range.end..next_range.start] - } else { - &new_text[inserted_range.end..] - }; - - let inserted_len = inserted_range.len(); - let deleted_len = old_text[old_start..] - .find(following_text) - .expect("invalid edit"); - - let old_range = old_start..old_start + deleted_len; - edits.push((old_range, new_text[inserted_range].to_string())); - delta += inserted_len as isize - deleted_len as isize; - } - - assert_eq!( - old_text.len() as isize + delta, - new_text.len() as isize, - "invalid edit" - ); - - edits - } - - pub fn check_invariants(&self) { - // Ensure every fragment is ordered by locator in the fragment tree and corresponds - // to an insertion fragment in the insertions tree. - let mut prev_fragment_id = Locator::min(); - for fragment in self.snapshot.fragments.items(&None) { - assert!(fragment.id > prev_fragment_id); - prev_fragment_id = fragment.id.clone(); - - let insertion_fragment = self - .snapshot - .insertions - .get( - &InsertionFragmentKey { - timestamp: fragment.timestamp, - split_offset: fragment.insertion_offset, - }, - (), - ) - .unwrap(); - assert_eq!( - insertion_fragment.fragment_id, fragment.id, - "fragment: {:?}\ninsertion: {:?}", - fragment, insertion_fragment - ); - } - - let mut cursor = self.snapshot.fragments.cursor::>(&None); - for insertion_fragment in self.snapshot.insertions.cursor::<()>(()) { - cursor.seek(&Some(&insertion_fragment.fragment_id), Bias::Left); - let fragment = cursor.item().unwrap(); - assert_eq!(insertion_fragment.fragment_id, fragment.id); - assert_eq!(insertion_fragment.split_offset, fragment.insertion_offset); - } - - let fragment_summary = self.snapshot.fragments.summary(); - assert_eq!( - fragment_summary.text.visible, - self.snapshot.visible_text.len() - ); - assert_eq!( - fragment_summary.text.deleted, - self.snapshot.deleted_text.len() - ); - - assert!(!self.text().contains("\r\n")); - } - - pub fn set_group_interval(&mut self, group_interval: Duration) { - self.history.group_interval = group_interval; - } - - pub fn random_byte_range(&self, start_offset: usize, rng: &mut impl rand::Rng) -> Range { - let end = self.clip_offset(rng.random_range(start_offset..=self.len()), Bias::Right); - let start = self.clip_offset(rng.random_range(start_offset..=end), Bias::Right); - start..end - } - - pub fn get_random_edits( - &self, - rng: &mut T, - edit_count: usize, - ) -> Vec<(Range, Arc)> - where - T: rand::Rng, - { - 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 >= self.len()) { - break; - } - let new_start = last_end.map_or(0, |last_end| last_end + 1); - let range = self.random_byte_range(new_start, rng); - last_end = Some(range.end); - - let new_text_len = rng.random_range(0..10); - let new_text: String = RandomCharIter::new(&mut *rng).take(new_text_len).collect(); - - edits.push((range, new_text.into())); - } - edits - } - - pub fn randomly_edit( - &mut self, - rng: &mut T, - edit_count: usize, - ) -> (Vec<(Range, Arc)>, Operation) - where - T: rand::Rng, - { - let mut edits = self.get_random_edits(rng, edit_count); - log::info!("mutating buffer {:?} with {:?}", self.replica_id, edits); - - let op = self.edit(edits.iter().cloned()); - if let Operation::Edit(edit) = &op { - assert_eq!(edits.len(), edit.new_text.len()); - for (edit, new_text) in edits.iter_mut().zip(&edit.new_text) { - edit.1 = new_text.clone(); - } - } else { - unreachable!() - } - - (edits, op) - } - - pub fn randomly_undo_redo(&mut self, rng: &mut impl rand::Rng) -> Vec { - use rand::prelude::*; - - let mut ops = Vec::new(); - for _ in 0..rng.random_range(1..=5) { - if let Some(entry) = self.history.undo_stack.choose(rng) { - let transaction = entry.transaction.clone(); - log::info!( - "undoing buffer {:?} transaction {:?}", - self.replica_id, - transaction - ); - ops.push(self.undo_or_redo(transaction)); - } - } - ops - } -} - -impl Deref for Buffer { - type Target = BufferSnapshot; - - fn deref(&self) -> &Self::Target { - &self.snapshot - } -} - -impl BufferSnapshot { - pub fn as_rope(&self) -> &Rope { - &self.visible_text - } - - pub fn rope_for_version(&self, version: &clock::Global) -> Rope { - let mut rope = Rope::new(); - - let mut cursor = self - .fragments - .filter::<_, FragmentTextSummary>(&None, move |summary| { - !version.observed_all(&summary.max_version) - }); - cursor.next(); - - let mut visible_cursor = self.visible_text.cursor(0); - let mut deleted_cursor = self.deleted_text.cursor(0); - - while let Some(fragment) = cursor.item() { - if cursor.start().visible > visible_cursor.offset() { - let text = visible_cursor.slice(cursor.start().visible); - rope.append(text); - } - - if fragment.was_visible(version, &self.undo_map) { - if fragment.visible { - let text = visible_cursor.slice(cursor.end().visible); - rope.append(text); - } else { - deleted_cursor.seek_forward(cursor.start().deleted); - let text = deleted_cursor.slice(cursor.end().deleted); - rope.append(text); - } - } else if fragment.visible { - visible_cursor.seek_forward(cursor.end().visible); - } - - cursor.next(); - } - - if cursor.start().visible > visible_cursor.offset() { - let text = visible_cursor.slice(cursor.start().visible); - rope.append(text); - } - - rope - } - - pub fn remote_id(&self) -> BufferId { - self.remote_id - } - - pub fn replica_id(&self) -> ReplicaId { - self.replica_id - } - - pub fn row_count(&self) -> u32 { - self.max_point().row + 1 - } - - pub fn len(&self) -> usize { - self.visible_text.len() - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - pub fn chars(&self) -> impl Iterator + '_ { - self.chars_at(0) - } - - pub fn chars_for_range(&self, range: Range) -> impl Iterator + '_ { - self.text_for_range(range).flat_map(str::chars) - } - - pub fn reversed_chars_for_range( - &self, - range: Range, - ) -> impl Iterator + '_ { - self.reversed_chunks_in_range(range) - .flat_map(|chunk| chunk.chars().rev()) - } - - pub fn contains_str_at(&self, position: T, needle: &str) -> bool - where - T: ToOffset, - { - let position = position.to_offset(self); - position == self.clip_offset(position, Bias::Left) - && self - .bytes_in_range(position..self.len()) - .flatten() - .copied() - .take(needle.len()) - .eq(needle.bytes()) - } - - pub fn common_prefix_at(&self, position: T, needle: &str) -> Range - where - T: ToOffset + TextDimension, - { - let offset = position.to_offset(self); - let common_prefix_len = needle - .char_indices() - .map(|(index, _)| index) - .chain([needle.len()]) - .take_while(|&len| len <= offset) - .filter(|&len| { - let left = self - .chars_for_range(offset - len..offset) - .flat_map(char::to_lowercase); - let right = needle[..len].chars().flat_map(char::to_lowercase); - left.eq(right) - }) - .last() - .unwrap_or(0); - let start_offset = offset - common_prefix_len; - let start = self.text_summary_for_range(0..start_offset); - start..position - } - - pub fn text(&self) -> String { - self.visible_text.to_string() - } - - pub fn line_ending(&self) -> LineEnding { - self.line_ending - } - - pub fn deleted_text(&self) -> String { - self.deleted_text.to_string() - } - - pub fn fragments(&self) -> impl Iterator { - self.fragments.iter() - } - - pub fn text_summary(&self) -> TextSummary { - self.visible_text.summary() - } - - pub fn max_point(&self) -> Point { - self.visible_text.max_point() - } - - pub fn max_point_utf16(&self) -> PointUtf16 { - self.visible_text.max_point_utf16() - } - - pub fn point_to_offset(&self, point: Point) -> usize { - self.visible_text.point_to_offset(point) - } - - pub fn point_to_offset_utf16(&self, point: Point) -> OffsetUtf16 { - self.visible_text.point_to_offset_utf16(point) - } - - pub fn point_utf16_to_offset_utf16(&self, point: PointUtf16) -> OffsetUtf16 { - self.visible_text.point_utf16_to_offset_utf16(point) - } - - pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { - self.visible_text.point_utf16_to_offset(point) - } - - pub fn unclipped_point_utf16_to_offset(&self, point: Unclipped) -> usize { - self.visible_text.unclipped_point_utf16_to_offset(point) - } - - pub fn unclipped_point_utf16_to_point(&self, point: Unclipped) -> Point { - self.visible_text.unclipped_point_utf16_to_point(point) - } - - pub fn offset_utf16_to_offset(&self, offset: OffsetUtf16) -> usize { - self.visible_text.offset_utf16_to_offset(offset) - } - - pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { - self.visible_text.offset_to_offset_utf16(offset) - } - - pub fn offset_to_point(&self, offset: usize) -> Point { - self.visible_text.offset_to_point(offset) - } - - pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 { - self.visible_text.offset_to_point_utf16(offset) - } - - pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 { - self.visible_text.point_to_point_utf16(point) - } - - pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point { - self.visible_text.point_utf16_to_point(point) - } - - pub fn version(&self) -> &clock::Global { - &self.version - } - - pub fn chars_at(&self, position: T) -> impl Iterator + '_ { - let offset = position.to_offset(self); - self.visible_text.chars_at(offset) - } - - pub fn reversed_chars_at(&self, position: T) -> impl Iterator + '_ { - let offset = position.to_offset(self); - self.visible_text.reversed_chars_at(offset) - } - - pub fn reversed_chunks_in_range(&self, range: Range) -> rope::Chunks<'_> { - let range = range.start.to_offset(self)..range.end.to_offset(self); - self.visible_text.reversed_chunks_in_range(range) - } - - pub fn bytes_in_range(&self, range: Range) -> rope::Bytes<'_> { - let start = range.start.to_offset(self); - let end = range.end.to_offset(self); - self.visible_text.bytes_in_range(start..end) - } - - pub fn reversed_bytes_in_range(&self, range: Range) -> rope::Bytes<'_> { - let start = range.start.to_offset(self); - let end = range.end.to_offset(self); - self.visible_text.reversed_bytes_in_range(start..end) - } - - pub fn text_for_range(&self, range: Range) -> Chunks<'_> { - let start = range.start.to_offset(self); - let end = range.end.to_offset(self); - self.visible_text.chunks_in_range(start..end) - } - - pub fn line_len(&self, row: u32) -> u32 { - let row_start_offset = Point::new(row, 0).to_offset(self); - let row_end_offset = if row >= self.max_point().row { - self.len() - } else { - Point::new(row + 1, 0).to_previous_offset(self) - }; - (row_end_offset - row_start_offset) as u32 - } - - pub fn line_indents_in_row_range( - &self, - row_range: Range, - ) -> impl Iterator + '_ { - let start = Point::new(row_range.start, 0).to_offset(self); - let end = Point::new(row_range.end, self.line_len(row_range.end)).to_offset(self); - - let mut chunks = self.as_rope().chunks_in_range(start..end); - let mut row = row_range.start; - let mut done = false; - std::iter::from_fn(move || { - if done { - None - } else { - let indent = (row, LineIndent::from_chunks(&mut chunks)); - done = !chunks.next_line(); - row += 1; - Some(indent) - } - }) - } - - /// Returns the line indents in the given row range, exclusive of end row, in reversed order. - pub fn reversed_line_indents_in_row_range( - &self, - row_range: Range, - ) -> impl Iterator + '_ { - let start = Point::new(row_range.start, 0).to_offset(self); - - let end_point; - let end; - if row_range.end > row_range.start { - end_point = Point::new(row_range.end - 1, self.line_len(row_range.end - 1)); - end = end_point.to_offset(self); - } else { - end_point = Point::new(row_range.start, 0); - end = start; - }; - - let mut chunks = self.as_rope().chunks_in_range(start..end); - // Move the cursor to the start of the last line if it's not empty. - chunks.seek(end); - if end_point.column > 0 { - chunks.prev_line(); - } - - let mut row = end_point.row; - let mut done = false; - std::iter::from_fn(move || { - if done { - None - } else { - let initial_offset = chunks.offset(); - let indent = (row, LineIndent::from_chunks(&mut chunks)); - if chunks.offset() > initial_offset { - chunks.prev_line(); - } - done = !chunks.prev_line(); - if !done { - row -= 1; - } - - Some(indent) - } - }) - } - - pub fn line_indent_for_row(&self, row: u32) -> LineIndent { - LineIndent::from_iter(self.chars_at(Point::new(row, 0))) - } - - pub fn is_line_blank(&self, row: u32) -> bool { - self.text_for_range(Point::new(row, 0)..Point::new(row, self.line_len(row))) - .all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none()) - } - - pub fn text_summary_for_range(&self, range: Range) -> D - where - D: TextDimension, - { - self.visible_text - .cursor(range.start.to_offset(self)) - .summary(range.end.to_offset(self)) - } - - pub fn summaries_for_anchors<'a, D, A>(&'a self, anchors: A) -> impl 'a + Iterator - where - D: 'a + TextDimension, - A: 'a + IntoIterator, - { - let anchors = anchors.into_iter(); - self.summaries_for_anchors_with_payload::(anchors.map(|a| (a, ()))) - .map(|d| d.0) - } - - pub fn summaries_for_anchors_with_payload<'a, D, A, T>( - &'a self, - anchors: A, - ) -> impl 'a + Iterator - where - D: 'a + TextDimension, - A: 'a + IntoIterator, - { - let anchors = anchors.into_iter(); - let mut insertion_cursor = self.insertions.cursor::(()); - let mut fragment_cursor = self - .fragments - .cursor::, usize>>(&None); - let mut text_cursor = self.visible_text.cursor(0); - let mut position = D::zero(()); - - anchors.map(move |(anchor, payload)| { - if anchor.is_min() { - return (D::zero(()), payload); - } else if anchor.is_max() { - return (D::from_text_summary(&self.visible_text.summary()), payload); - } - - let anchor_key = InsertionFragmentKey { - timestamp: anchor.timestamp, - split_offset: anchor.offset, - }; - insertion_cursor.seek(&anchor_key, anchor.bias); - if let Some(insertion) = insertion_cursor.item() { - let comparison = sum_tree::KeyedItem::key(insertion).cmp(&anchor_key); - if comparison == Ordering::Greater - || (anchor.bias == Bias::Left - && comparison == Ordering::Equal - && anchor.offset > 0) - { - insertion_cursor.prev(); - } - } else { - insertion_cursor.prev(); - } - let Some(insertion) = insertion_cursor.item() else { - panic!( - "invalid insertion for buffer {}@{:?} with anchor {:?}", - self.remote_id(), - self.version, - anchor - ); - }; - assert_eq!( - insertion.timestamp, - anchor.timestamp, - "invalid insertion for buffer {}@{:?} and anchor {:?}", - self.remote_id(), - self.version, - anchor - ); - - fragment_cursor.seek_forward(&Some(&insertion.fragment_id), Bias::Left); - let fragment = fragment_cursor.item().unwrap(); - let mut fragment_offset = fragment_cursor.start().1; - if fragment.visible { - fragment_offset += anchor.offset - insertion.split_offset; - } - - position.add_assign(&text_cursor.summary(fragment_offset)); - (position, payload) - }) - } - - pub fn summary_for_anchor(&self, anchor: &Anchor) -> D - where - D: TextDimension, - { - self.text_summary_for_range(0..self.offset_for_anchor(anchor)) - } - - pub fn offset_for_anchor(&self, anchor: &Anchor) -> usize { - if anchor.is_min() { - 0 - } else if anchor.is_max() { - self.visible_text.len() - } else { - debug_assert_eq!(anchor.buffer_id, Some(self.remote_id)); - debug_assert!( - self.version.observed(anchor.timestamp), - "Anchor timestamp {:?} not observed by buffer {:?}", - anchor.timestamp, - self.version - ); - let anchor_key = InsertionFragmentKey { - timestamp: anchor.timestamp, - split_offset: anchor.offset, - }; - let mut insertion_cursor = self.insertions.cursor::(()); - insertion_cursor.seek(&anchor_key, anchor.bias); - if let Some(insertion) = insertion_cursor.item() { - let comparison = sum_tree::KeyedItem::key(insertion).cmp(&anchor_key); - if comparison == Ordering::Greater - || (anchor.bias == Bias::Left - && comparison == Ordering::Equal - && anchor.offset > 0) - { - insertion_cursor.prev(); - } - } else { - insertion_cursor.prev(); - } - - let Some(insertion) = insertion_cursor - .item() - .filter(|insertion| insertion.timestamp == anchor.timestamp) - else { - self.panic_bad_anchor(anchor); - }; - - let (start, _, item) = self - .fragments - .find::, usize>, _>( - &None, - &Some(&insertion.fragment_id), - Bias::Left, - ); - let fragment = item.unwrap(); - let mut fragment_offset = start.1; - if fragment.visible { - fragment_offset += anchor.offset - insertion.split_offset; - } - fragment_offset - } - } - - #[cold] - fn panic_bad_anchor(&self, anchor: &Anchor) -> ! { - if anchor.buffer_id.is_some_and(|id| id != self.remote_id) { - panic!( - "invalid anchor - buffer id does not match: anchor {anchor:?}; buffer id: {}, version: {:?}", - self.remote_id, self.version - ); - } else if !self.version.observed(anchor.timestamp) { - panic!( - "invalid anchor - snapshot has not observed lamport: {:?}; version: {:?}", - anchor, self.version - ); - } else { - panic!( - "invalid anchor {:?}. buffer id: {}, version: {:?}", - anchor, self.remote_id, self.version - ); - } - } - - fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator { - self.try_fragment_id_for_anchor(anchor) - .unwrap_or_else(|| self.panic_bad_anchor(anchor)) - } - - fn try_fragment_id_for_anchor(&self, anchor: &Anchor) -> Option<&Locator> { - if anchor.is_min() { - Some(Locator::min_ref()) - } else if anchor.is_max() { - Some(Locator::max_ref()) - } else { - let anchor_key = InsertionFragmentKey { - timestamp: anchor.timestamp, - split_offset: anchor.offset, - }; - let mut insertion_cursor = self.insertions.cursor::(()); - insertion_cursor.seek(&anchor_key, anchor.bias); - if let Some(insertion) = insertion_cursor.item() { - let comparison = sum_tree::KeyedItem::key(insertion).cmp(&anchor_key); - if comparison == Ordering::Greater - || (anchor.bias == Bias::Left - && comparison == Ordering::Equal - && anchor.offset > 0) - { - insertion_cursor.prev(); - } - } else { - insertion_cursor.prev(); - } - - insertion_cursor - .item() - .filter(|insertion| { - !cfg!(debug_assertions) || insertion.timestamp == anchor.timestamp - }) - .map(|insertion| &insertion.fragment_id) - } - } - - pub fn anchor_before(&self, position: T) -> Anchor { - self.anchor_at(position, Bias::Left) - } - - pub fn anchor_after(&self, position: T) -> Anchor { - self.anchor_at(position, Bias::Right) - } - - pub fn anchor_at(&self, position: T, bias: Bias) -> Anchor { - self.anchor_at_offset(position.to_offset(self), bias) - } - - fn anchor_at_offset(&self, mut offset: usize, bias: Bias) -> Anchor { - if bias == Bias::Left && offset == 0 { - Anchor::min_for_buffer(self.remote_id) - } else if bias == Bias::Right - && ((!cfg!(debug_assertions) && offset >= self.len()) || offset == self.len()) - { - Anchor::max_for_buffer(self.remote_id) - } else { - if self - .visible_text - .assert_char_boundary::<{ cfg!(debug_assertions) }>(offset) - { - offset = match bias { - Bias::Left => self.visible_text.floor_char_boundary(offset), - Bias::Right => self.visible_text.ceil_char_boundary(offset), - }; - } - let (start, _, item) = self.fragments.find::(&None, &offset, bias); - let Some(fragment) = item else { - // We got a bad offset, likely out of bounds - debug_panic!( - "Failed to find fragment at offset {} (len: {})", - offset, - self.len() - ); - return Anchor::max_for_buffer(self.remote_id); - }; - let overshoot = offset - start; - Anchor { - timestamp: fragment.timestamp, - offset: fragment.insertion_offset + overshoot, - bias, - buffer_id: Some(self.remote_id), - } - } - } - - pub fn can_resolve(&self, anchor: &Anchor) -> bool { - anchor.is_min() - || anchor.is_max() - || (Some(self.remote_id) == anchor.buffer_id && self.version.observed(anchor.timestamp)) - } - - pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { - self.visible_text.clip_offset(offset, bias) - } - - pub fn clip_point(&self, point: Point, bias: Bias) -> Point { - self.visible_text.clip_point(point, bias) - } - - pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 { - self.visible_text.clip_offset_utf16(offset, bias) - } - - pub fn clip_point_utf16(&self, point: Unclipped, bias: Bias) -> PointUtf16 { - self.visible_text.clip_point_utf16(point, bias) - } - - pub fn edits_since<'a, D>( - &'a self, - since: &'a clock::Global, - ) -> impl 'a + Iterator> - where - D: TextDimension + Ord, - { - self.edits_since_in_range(since, Anchor::MIN..Anchor::MAX) - } - - pub fn anchored_edits_since<'a, D>( - &'a self, - since: &'a clock::Global, - ) -> impl 'a + Iterator, Range)> - where - D: TextDimension + Ord, - { - self.anchored_edits_since_in_range(since, Anchor::MIN..Anchor::MAX) - } - - pub fn edits_since_in_range<'a, D>( - &'a self, - since: &'a clock::Global, - range: Range, - ) -> impl 'a + Iterator> - where - D: TextDimension + Ord, - { - self.anchored_edits_since_in_range(since, range) - .map(|item| item.0) - } - - pub fn anchored_edits_since_in_range<'a, D>( - &'a self, - since: &'a clock::Global, - range: Range, - ) -> impl 'a + Iterator, Range)> - where - D: TextDimension + Ord, - { - let fragments_cursor = if *since == self.version { - None - } else { - let mut cursor = self.fragments.filter(&None, move |summary| { - !since.observed_all(&summary.max_version) - }); - cursor.next(); - Some(cursor) - }; - let start_fragment_id = self.fragment_id_for_anchor(&range.start); - let (start, _, item) = self - .fragments - .find::, FragmentTextSummary>, _>( - &None, - &Some(start_fragment_id), - Bias::Left, - ); - let mut visible_start = start.1.visible; - let mut deleted_start = start.1.deleted; - if let Some(fragment) = item { - let overshoot = range.start.offset - fragment.insertion_offset; - if fragment.visible { - visible_start += overshoot; - } else { - deleted_start += overshoot; - } - } - let end_fragment_id = self.fragment_id_for_anchor(&range.end); - - Edits { - visible_cursor: self.visible_text.cursor(visible_start), - deleted_cursor: self.deleted_text.cursor(deleted_start), - fragments_cursor, - undos: &self.undo_map, - since, - old_end: D::zero(()), - new_end: D::zero(()), - range: (start_fragment_id, range.start.offset)..(end_fragment_id, range.end.offset), - buffer_id: self.remote_id, - } - } - - pub fn has_edits_since_in_range(&self, since: &clock::Global, range: Range) -> bool { - if *since != self.version { - let start_fragment_id = self.fragment_id_for_anchor(&range.start); - let end_fragment_id = self.fragment_id_for_anchor(&range.end); - let mut cursor = self.fragments.filter::<_, usize>(&None, move |summary| { - !since.observed_all(&summary.max_version) - }); - cursor.next(); - while let Some(fragment) = cursor.item() { - if fragment.id > *end_fragment_id { - break; - } - if fragment.id > *start_fragment_id { - let was_visible = fragment.was_visible(since, &self.undo_map); - let is_visible = fragment.visible; - if was_visible != is_visible { - return true; - } - } - cursor.next(); - } - } - false - } - - pub fn has_edits_since(&self, since: &clock::Global) -> bool { - if *since != self.version { - let mut cursor = self.fragments.filter::<_, usize>(&None, move |summary| { - !since.observed_all(&summary.max_version) - }); - cursor.next(); - while let Some(fragment) = cursor.item() { - let was_visible = fragment.was_visible(since, &self.undo_map); - let is_visible = fragment.visible; - if was_visible != is_visible { - return true; - } - cursor.next(); - } - } - false - } - - pub fn range_to_version(&self, range: Range, version: &clock::Global) -> Range { - let mut offsets = self.offsets_to_version([range.start, range.end], version); - offsets.next().unwrap()..offsets.next().unwrap() - } - - /// Converts the given sequence of offsets into their corresponding offsets - /// at a prior version of this buffer. - pub fn offsets_to_version<'a>( - &'a self, - offsets: impl 'a + IntoIterator, - version: &'a clock::Global, - ) -> impl 'a + Iterator { - let mut edits = self.edits_since(version).peekable(); - let mut last_old_end = 0; - let mut last_new_end = 0; - offsets.into_iter().map(move |new_offset| { - while let Some(edit) = edits.peek() { - if edit.new.start > new_offset { - break; - } - - if edit.new.end <= new_offset { - last_new_end = edit.new.end; - last_old_end = edit.old.end; - edits.next(); - continue; - } - - let overshoot = new_offset - edit.new.start; - return (edit.old.start + overshoot).min(edit.old.end); - } - - last_old_end + new_offset.saturating_sub(last_new_end) - }) - } - - /// Visually annotates a position or range with the `Debug` representation of a value. The - /// callsite of this function is used as a key - previous annotations will be removed. - #[cfg(debug_assertions)] - #[track_caller] - pub fn debug(&self, ranges: &R, value: V) - where - R: debug::ToDebugRanges, - V: std::fmt::Debug, - { - self.debug_with_key(std::panic::Location::caller(), ranges, value); - } - - /// Visually annotates a position or range with the `Debug` representation of a value. Previous - /// debug annotations with the same key will be removed. The key is also used to determine the - /// annotation's color. - #[cfg(debug_assertions)] - pub fn debug_with_key(&self, key: &K, ranges: &R, value: V) - where - K: std::hash::Hash + 'static, - R: debug::ToDebugRanges, - V: std::fmt::Debug, - { - let ranges = ranges - .to_debug_ranges(self) - .into_iter() - .map(|range| self.anchor_after(range.start)..self.anchor_before(range.end)) - .collect(); - debug::GlobalDebugRanges::with_locked(|debug_ranges| { - debug_ranges.insert(key, ranges, format!("{value:?}").into()); - }); - } -} - -struct RopeBuilder<'a> { - old_visible_cursor: rope::Cursor<'a>, - old_deleted_cursor: rope::Cursor<'a>, - new_visible: Rope, - new_deleted: Rope, -} - -impl<'a> RopeBuilder<'a> { - fn new(old_visible_cursor: rope::Cursor<'a>, old_deleted_cursor: rope::Cursor<'a>) -> Self { - Self { - old_visible_cursor, - old_deleted_cursor, - new_visible: Rope::new(), - new_deleted: Rope::new(), - } - } - - fn append(&mut self, len: FragmentTextSummary) { - self.push(len.visible, true, true); - self.push(len.deleted, false, false); - } - - fn push_fragment(&mut self, fragment: &Fragment, was_visible: bool) { - debug_assert!(fragment.len > 0); - self.push(fragment.len, was_visible, fragment.visible) - } - - fn push(&mut self, len: usize, was_visible: bool, is_visible: bool) { - let text = if was_visible { - self.old_visible_cursor - .slice(self.old_visible_cursor.offset() + len) - } else { - self.old_deleted_cursor - .slice(self.old_deleted_cursor.offset() + len) - }; - if is_visible { - self.new_visible.append(text); - } else { - self.new_deleted.append(text); - } - } - - fn push_str(&mut self, text: &str) { - self.new_visible.push(text); - } - - fn finish(mut self) -> (Rope, Rope) { - self.new_visible.append(self.old_visible_cursor.suffix()); - self.new_deleted.append(self.old_deleted_cursor.suffix()); - (self.new_visible, self.new_deleted) - } -} - -impl bool> Iterator for Edits<'_, D, F> { - type Item = (Edit, Range); - - fn next(&mut self) -> Option { - let mut pending_edit: Option = None; - let cursor = self.fragments_cursor.as_mut()?; - - while let Some(fragment) = cursor.item() { - if fragment.id < *self.range.start.0 { - cursor.next(); - continue; - } else if fragment.id > *self.range.end.0 { - break; - } - - if cursor.start().visible > self.visible_cursor.offset() { - let summary = self.visible_cursor.summary(cursor.start().visible); - self.old_end.add_assign(&summary); - self.new_end.add_assign(&summary); - } - - if pending_edit - .as_ref() - .is_some_and(|(change, _)| change.new.end < self.new_end) - { - break; - } - - let start_anchor = Anchor { - timestamp: fragment.timestamp, - offset: fragment.insertion_offset, - bias: Bias::Right, - buffer_id: Some(self.buffer_id), - }; - let end_anchor = Anchor { - timestamp: fragment.timestamp, - offset: fragment.insertion_offset + fragment.len, - bias: Bias::Left, - buffer_id: Some(self.buffer_id), - }; - - if !fragment.was_visible(self.since, self.undos) && fragment.visible { - let mut visible_end = cursor.end().visible; - if fragment.id == *self.range.end.0 { - visible_end = cmp::min( - visible_end, - cursor.start().visible + (self.range.end.1 - fragment.insertion_offset), - ); - } - - let fragment_summary = self.visible_cursor.summary(visible_end); - let mut new_end = self.new_end; - new_end.add_assign(&fragment_summary); - if let Some((edit, range)) = pending_edit.as_mut() { - edit.new.end = new_end; - range.end = end_anchor; - } else { - pending_edit = Some(( - Edit { - old: self.old_end..self.old_end, - new: self.new_end..new_end, - }, - start_anchor..end_anchor, - )); - } - - self.new_end = new_end; - } else if fragment.was_visible(self.since, self.undos) && !fragment.visible { - let mut deleted_end = cursor.end().deleted; - if fragment.id == *self.range.end.0 { - deleted_end = cmp::min( - deleted_end, - cursor.start().deleted + (self.range.end.1 - fragment.insertion_offset), - ); - } - - if cursor.start().deleted > self.deleted_cursor.offset() { - self.deleted_cursor.seek_forward(cursor.start().deleted); - } - let fragment_summary = self.deleted_cursor.summary(deleted_end); - let mut old_end = self.old_end; - old_end.add_assign(&fragment_summary); - if let Some((edit, range)) = pending_edit.as_mut() { - edit.old.end = old_end; - range.end = end_anchor; - } else { - pending_edit = Some(( - Edit { - old: self.old_end..old_end, - new: self.new_end..self.new_end, - }, - start_anchor..end_anchor, - )); - } - - self.old_end = old_end; - } - - cursor.next(); - } - - pending_edit - } -} - -impl Fragment { - fn is_visible(&self, undos: &UndoMap) -> bool { - !undos.is_undone(self.timestamp) && self.deletions.iter().all(|d| undos.is_undone(*d)) - } - - fn was_visible(&self, version: &clock::Global, undos: &UndoMap) -> bool { - (version.observed(self.timestamp) && !undos.was_undone(self.timestamp, version)) - && self - .deletions - .iter() - .all(|d| !version.observed(*d) || undos.was_undone(*d, version)) - } -} - -impl sum_tree::Item for Fragment { - type Summary = FragmentSummary; - - fn summary(&self, _cx: &Option) -> Self::Summary { - let mut max_version = clock::Global::new(); - max_version.observe(self.timestamp); - for deletion in &self.deletions { - max_version.observe(*deletion); - } - max_version.join(&self.max_undos); - - let mut min_insertion_version = clock::Global::new(); - min_insertion_version.observe(self.timestamp); - let max_insertion_version = min_insertion_version.clone(); - if self.visible { - FragmentSummary { - max_id: self.id.clone(), - text: FragmentTextSummary { - visible: self.len, - deleted: 0, - }, - max_version, - min_insertion_version, - max_insertion_version, - } - } else { - FragmentSummary { - max_id: self.id.clone(), - text: FragmentTextSummary { - visible: 0, - deleted: self.len, - }, - max_version, - min_insertion_version, - max_insertion_version, - } - } - } -} - -impl sum_tree::Summary for FragmentSummary { - type Context<'a> = &'a Option; - - fn zero(_cx: Self::Context<'_>) -> Self { - Default::default() - } - - fn add_summary(&mut self, other: &Self, _: Self::Context<'_>) { - self.max_id.assign(&other.max_id); - self.text.visible += &other.text.visible; - self.text.deleted += &other.text.deleted; - self.max_version.join(&other.max_version); - self.min_insertion_version - .meet(&other.min_insertion_version); - self.max_insertion_version - .join(&other.max_insertion_version); - } -} - -impl Default for FragmentSummary { - fn default() -> Self { - FragmentSummary { - max_id: Locator::min(), - text: FragmentTextSummary::default(), - max_version: clock::Global::new(), - min_insertion_version: clock::Global::new(), - max_insertion_version: clock::Global::new(), - } - } -} - -impl sum_tree::Item for InsertionFragment { - type Summary = InsertionFragmentKey; - - fn summary(&self, _cx: ()) -> Self::Summary { - InsertionFragmentKey { - timestamp: self.timestamp, - split_offset: self.split_offset, - } - } -} - -impl sum_tree::KeyedItem for InsertionFragment { - type Key = InsertionFragmentKey; - - fn key(&self) -> Self::Key { - sum_tree::Item::summary(self, ()) - } -} - -impl InsertionFragment { - fn new(fragment: &Fragment) -> Self { - Self { - timestamp: fragment.timestamp, - split_offset: fragment.insertion_offset, - fragment_id: fragment.id.clone(), - } - } - - fn insert_new(fragment: &Fragment) -> sum_tree::Edit { - sum_tree::Edit::Insert(Self::new(fragment)) - } -} - -impl sum_tree::ContextLessSummary for InsertionFragmentKey { - fn zero() -> Self { - InsertionFragmentKey { - timestamp: Lamport::MIN, - split_offset: 0, - } - } - - fn add_summary(&mut self, summary: &Self) { - *self = *summary; - } -} - -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct FullOffset(pub usize); - -impl ops::AddAssign for FullOffset { - fn add_assign(&mut self, rhs: usize) { - self.0 += rhs; - } -} - -impl ops::Add for FullOffset { - type Output = Self; - - fn add(mut self, rhs: usize) -> Self::Output { - self += rhs; - self - } -} - -impl ops::Sub for FullOffset { - type Output = usize; - - fn sub(self, rhs: Self) -> Self::Output { - self.0 - rhs.0 - } -} - -impl sum_tree::Dimension<'_, FragmentSummary> for usize { - fn zero(_: &Option) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &FragmentSummary, _: &Option) { - *self += summary.text.visible; - } -} - -impl sum_tree::Dimension<'_, FragmentSummary> for FullOffset { - fn zero(_: &Option) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &FragmentSummary, _: &Option) { - self.0 += summary.text.visible + summary.text.deleted; - } -} - -impl<'a> sum_tree::Dimension<'a, FragmentSummary> for Option<&'a Locator> { - fn zero(_: &Option) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a FragmentSummary, _: &Option) { - *self = Some(&summary.max_id); - } -} - -impl sum_tree::SeekTarget<'_, FragmentSummary, FragmentTextSummary> for usize { - fn cmp( - &self, - cursor_location: &FragmentTextSummary, - _: &Option, - ) -> cmp::Ordering { - Ord::cmp(self, &cursor_location.visible) - } -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -enum VersionedFullOffset { - Offset(FullOffset), - Invalid, -} - -impl VersionedFullOffset { - fn full_offset(&self) -> FullOffset { - if let Self::Offset(position) = self { - *position - } else { - panic!("invalid version") - } - } -} - -impl Default for VersionedFullOffset { - fn default() -> Self { - Self::Offset(Default::default()) - } -} - -impl<'a> sum_tree::Dimension<'a, FragmentSummary> for VersionedFullOffset { - fn zero(_cx: &Option) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a FragmentSummary, cx: &Option) { - if let Self::Offset(offset) = self { - let version = cx.as_ref().unwrap(); - if version.observed_all(&summary.max_insertion_version) { - *offset += summary.text.visible + summary.text.deleted; - } else if version.observed_any(&summary.min_insertion_version) { - *self = Self::Invalid; - } - } - } -} - -impl sum_tree::SeekTarget<'_, FragmentSummary, Self> for VersionedFullOffset { - fn cmp(&self, cursor_position: &Self, _: &Option) -> cmp::Ordering { - match (self, cursor_position) { - (Self::Offset(a), Self::Offset(b)) => Ord::cmp(a, b), - (Self::Offset(_), Self::Invalid) => cmp::Ordering::Less, - (Self::Invalid, _) => unreachable!(), - } - } -} - -impl Operation { - fn replica_id(&self) -> ReplicaId { - operation_queue::Operation::lamport_timestamp(self).replica_id - } - - pub fn timestamp(&self) -> clock::Lamport { - match self { - Operation::Edit(edit) => edit.timestamp, - Operation::Undo(undo) => undo.timestamp, - } - } - - pub fn as_edit(&self) -> Option<&EditOperation> { - match self { - Operation::Edit(edit) => Some(edit), - _ => None, - } - } - - pub fn is_edit(&self) -> bool { - matches!(self, Operation::Edit { .. }) - } -} - -impl operation_queue::Operation for Operation { - fn lamport_timestamp(&self) -> clock::Lamport { - match self { - Operation::Edit(edit) => edit.timestamp, - Operation::Undo(undo) => undo.timestamp, - } - } -} - -pub trait ToOffset { - fn to_offset(&self, snapshot: &BufferSnapshot) -> usize; - /// Turns this point into the next offset in the buffer that comes after this, respecting utf8 boundaries. - fn to_next_offset(&self, snapshot: &BufferSnapshot) -> usize { - snapshot - .visible_text - .ceil_char_boundary(self.to_offset(snapshot) + 1) - } - /// Turns this point into the previous offset in the buffer that comes before this, respecting utf8 boundaries. - fn to_previous_offset(&self, snapshot: &BufferSnapshot) -> usize { - snapshot - .visible_text - .floor_char_boundary(self.to_offset(snapshot).saturating_sub(1)) - } -} - -impl ToOffset for Point { - #[inline] - fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { - snapshot.point_to_offset(*self) - } -} - -impl ToOffset for usize { - fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { - if snapshot - .as_rope() - .assert_char_boundary::<{ cfg!(debug_assertions) }>(*self) - { - snapshot.as_rope().floor_char_boundary(*self) - } else { - *self - } - } -} - -impl ToOffset for Anchor { - #[inline] - fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { - snapshot.summary_for_anchor(self) - } -} - -impl ToOffset for &T { - #[inline] - fn to_offset(&self, content: &BufferSnapshot) -> usize { - (*self).to_offset(content) - } -} - -impl ToOffset for PointUtf16 { - #[inline] - fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { - snapshot.point_utf16_to_offset(*self) - } -} - -impl ToOffset for Unclipped { - #[inline] - fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { - snapshot.unclipped_point_utf16_to_offset(*self) - } -} - -pub trait ToPoint { - fn to_point(&self, snapshot: &BufferSnapshot) -> Point; -} - -impl ToPoint for Anchor { - #[inline] - fn to_point(&self, snapshot: &BufferSnapshot) -> Point { - snapshot.summary_for_anchor(self) - } -} - -impl ToPoint for usize { - #[inline] - fn to_point(&self, snapshot: &BufferSnapshot) -> Point { - snapshot.offset_to_point(*self) - } -} - -impl ToPoint for Point { - #[inline] - fn to_point(&self, _: &BufferSnapshot) -> Point { - *self - } -} - -impl ToPoint for Unclipped { - #[inline] - fn to_point(&self, snapshot: &BufferSnapshot) -> Point { - snapshot.unclipped_point_utf16_to_point(*self) - } -} - -pub trait ToPointUtf16 { - fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16; -} - -impl ToPointUtf16 for Anchor { - #[inline] - fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16 { - snapshot.summary_for_anchor(self) - } -} - -impl ToPointUtf16 for usize { - #[inline] - fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16 { - snapshot.offset_to_point_utf16(*self) - } -} - -impl ToPointUtf16 for PointUtf16 { - #[inline] - fn to_point_utf16(&self, _: &BufferSnapshot) -> PointUtf16 { - *self - } -} - -impl ToPointUtf16 for Point { - #[inline] - fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16 { - snapshot.point_to_point_utf16(*self) - } -} - -pub trait ToOffsetUtf16 { - fn to_offset_utf16(&self, snapshot: &BufferSnapshot) -> OffsetUtf16; -} - -impl ToOffsetUtf16 for Anchor { - #[inline] - fn to_offset_utf16(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 { - snapshot.summary_for_anchor(self) - } -} - -impl ToOffsetUtf16 for usize { - #[inline] - fn to_offset_utf16(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 { - snapshot.offset_to_offset_utf16(*self) - } -} - -impl ToOffsetUtf16 for OffsetUtf16 { - #[inline] - fn to_offset_utf16(&self, _snapshot: &BufferSnapshot) -> OffsetUtf16 { - *self - } -} - -pub trait FromAnchor { - fn from_anchor(anchor: &Anchor, snapshot: &BufferSnapshot) -> Self; -} - -impl FromAnchor for Anchor { - #[inline] - fn from_anchor(anchor: &Anchor, _snapshot: &BufferSnapshot) -> Self { - *anchor - } -} - -impl FromAnchor for Point { - #[inline] - fn from_anchor(anchor: &Anchor, snapshot: &BufferSnapshot) -> Self { - snapshot.summary_for_anchor(anchor) - } -} - -impl FromAnchor for PointUtf16 { - #[inline] - fn from_anchor(anchor: &Anchor, snapshot: &BufferSnapshot) -> Self { - snapshot.summary_for_anchor(anchor) - } -} - -impl FromAnchor for usize { - #[inline] - fn from_anchor(anchor: &Anchor, snapshot: &BufferSnapshot) -> Self { - snapshot.summary_for_anchor(anchor) - } -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum LineEnding { - Unix, - Windows, -} - -impl Default for LineEnding { - fn default() -> Self { - #[cfg(unix)] - return Self::Unix; - - #[cfg(not(unix))] - return Self::Windows; - } -} - -impl LineEnding { - pub fn as_str(&self) -> &'static str { - match self { - LineEnding::Unix => "\n", - LineEnding::Windows => "\r\n", - } - } - - pub fn label(&self) -> &'static str { - match self { - LineEnding::Unix => "LF", - LineEnding::Windows => "CRLF", - } - } - - pub fn detect(text: &str) -> Self { - let mut max_ix = cmp::min(text.len(), 1000); - while !text.is_char_boundary(max_ix) { - max_ix -= 1; - } - - if let Some(ix) = text[..max_ix].find(['\n']) { - if ix > 0 && text.as_bytes()[ix - 1] == b'\r' { - Self::Windows - } else { - Self::Unix - } - } else { - Self::default() - } - } - - pub fn normalize(text: &mut String) { - if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(text, "\n") { - *text = replaced; - } - } - - pub fn normalize_arc(text: Arc) -> Arc { - if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") { - replaced.into() - } else { - text - } - } - - pub fn normalize_cow(text: Cow) -> Cow { - if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") { - replaced.into() - } else { - text - } - } -} - -pub fn chunks_with_line_ending(rope: &Rope, line_ending: LineEnding) -> impl Iterator { - rope.chunks().flat_map(move |chunk| { - let mut newline = false; - let end_with_newline = chunk.ends_with('\n').then_some(line_ending.as_str()); - chunk - .lines() - .flat_map(move |line| { - let ending = if newline { - Some(line_ending.as_str()) - } else { - None - }; - newline = true; - ending.into_iter().chain([line]) - }) - .chain(end_with_newline) - }) -} - -#[cfg(debug_assertions)] -pub mod debug { - use super::*; - use parking_lot::Mutex; - use std::any::TypeId; - use std::hash::{Hash, Hasher}; - - static GLOBAL_DEBUG_RANGES: Mutex> = Mutex::new(None); - - pub struct GlobalDebugRanges { - pub ranges: Vec, - key_to_occurrence_index: HashMap, - next_occurrence_index: usize, - } - - pub struct DebugRange { - key: Key, - pub ranges: Vec>, - pub value: Arc, - pub occurrence_index: usize, - } - - #[derive(Debug, Clone, PartialEq, Eq, Hash)] - struct Key { - type_id: TypeId, - hash: u64, - } - - impl GlobalDebugRanges { - pub fn with_locked(f: impl FnOnce(&mut Self) -> R) -> R { - let mut state = GLOBAL_DEBUG_RANGES.lock(); - if state.is_none() { - *state = Some(GlobalDebugRanges { - ranges: Vec::new(), - key_to_occurrence_index: HashMap::default(), - next_occurrence_index: 0, - }); - } - if let Some(global_debug_ranges) = state.as_mut() { - f(global_debug_ranges) - } else { - unreachable!() - } - } - - pub fn insert( - &mut self, - key: &K, - ranges: Vec>, - value: Arc, - ) { - let occurrence_index = *self - .key_to_occurrence_index - .entry(Key::new(key)) - .or_insert_with(|| { - let occurrence_index = self.next_occurrence_index; - self.next_occurrence_index += 1; - occurrence_index - }); - let key = Key::new(key); - let existing = self - .ranges - .iter() - .enumerate() - .rfind(|(_, existing)| existing.key == key); - if let Some((existing_ix, _)) = existing { - self.ranges.remove(existing_ix); - } - self.ranges.push(DebugRange { - ranges, - key, - value, - occurrence_index, - }); - } - - pub fn remove(&mut self, key: &K) { - self.remove_impl(&Key::new(key)); - } - - fn remove_impl(&mut self, key: &Key) { - let existing = self - .ranges - .iter() - .enumerate() - .rfind(|(_, existing)| &existing.key == key); - if let Some((existing_ix, _)) = existing { - self.ranges.remove(existing_ix); - } - } - - pub fn remove_all_with_key_type(&mut self) { - self.ranges - .retain(|item| item.key.type_id != TypeId::of::()); - } - } - - impl Key { - fn new(key: &K) -> Self { - let type_id = TypeId::of::(); - let mut hasher = collections::FxHasher::default(); - key.hash(&mut hasher); - Key { - type_id, - hash: hasher.finish(), - } - } - } - - pub trait ToDebugRanges { - fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec>; - } - - impl ToDebugRanges for T { - fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec> { - [self.to_offset(snapshot)].to_debug_ranges(snapshot) - } - } - - impl ToDebugRanges for Range { - fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec> { - [self.clone()].to_debug_ranges(snapshot) - } - } - - impl ToDebugRanges for Vec { - fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec> { - self.as_slice().to_debug_ranges(snapshot) - } - } - - impl ToDebugRanges for Vec> { - fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec> { - self.as_slice().to_debug_ranges(snapshot) - } - } - - impl ToDebugRanges for [T] { - fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec> { - self.iter() - .map(|item| { - let offset = item.to_offset(snapshot); - offset..offset - }) - .collect() - } - } - - impl ToDebugRanges for [Range] { - fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec> { - self.iter() - .map(|range| range.start.to_offset(snapshot)..range.end.to_offset(snapshot)) - .collect() - } - } -} diff --git a/crates/text/src/undo_map.rs b/crates/text/src/undo_map.rs deleted file mode 100644 index 2c2eba8de6..0000000000 --- a/crates/text/src/undo_map.rs +++ /dev/null @@ -1,115 +0,0 @@ -use crate::UndoOperation; -use clock::Lamport; -use std::cmp; -use sum_tree::{Bias, SumTree}; - -#[derive(Copy, Clone, Debug)] -struct UndoMapEntry { - key: UndoMapKey, - undo_count: u32, -} - -impl sum_tree::Item for UndoMapEntry { - type Summary = UndoMapKey; - - fn summary(&self, _cx: ()) -> Self::Summary { - self.key - } -} - -impl sum_tree::KeyedItem for UndoMapEntry { - type Key = UndoMapKey; - - fn key(&self) -> Self::Key { - self.key - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -struct UndoMapKey { - edit_id: clock::Lamport, - undo_id: clock::Lamport, -} - -impl sum_tree::ContextLessSummary for UndoMapKey { - fn zero() -> Self { - UndoMapKey { - edit_id: Lamport::MIN, - undo_id: Lamport::MIN, - } - } - - fn add_summary(&mut self, summary: &Self) { - *self = cmp::max(*self, *summary); - } -} - -#[derive(Clone, Default)] -pub struct UndoMap(SumTree); - -impl UndoMap { - pub fn insert(&mut self, undo: &UndoOperation) { - let edits = undo - .counts - .iter() - .map(|(edit_id, count)| { - sum_tree::Edit::Insert(UndoMapEntry { - key: UndoMapKey { - edit_id: *edit_id, - undo_id: undo.timestamp, - }, - undo_count: *count, - }) - }) - .collect::>(); - self.0.edit(edits, ()); - } - - pub fn is_undone(&self, edit_id: clock::Lamport) -> bool { - self.undo_count(edit_id) % 2 == 1 - } - pub fn was_undone(&self, edit_id: clock::Lamport, version: &clock::Global) -> bool { - let mut cursor = self.0.cursor::(()); - cursor.seek( - &UndoMapKey { - edit_id, - undo_id: Lamport::MIN, - }, - Bias::Left, - ); - - let mut undo_count = 0; - for entry in cursor { - if entry.key.edit_id != edit_id { - break; - } - - if version.observed(entry.key.undo_id) { - undo_count = cmp::max(undo_count, entry.undo_count); - } - } - - undo_count % 2 == 1 - } - - pub fn undo_count(&self, edit_id: clock::Lamport) -> u32 { - let mut cursor = self.0.cursor::(()); - cursor.seek( - &UndoMapKey { - edit_id, - undo_id: Lamport::MIN, - }, - Bias::Left, - ); - - let mut undo_count = 0; - for entry in cursor { - if entry.key.edit_id != edit_id { - break; - } - - undo_count = cmp::max(undo_count, entry.undo_count); - } - undo_count - } -} diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml deleted file mode 100644 index ef193c500d..0000000000 --- a/crates/theme/Cargo.toml +++ /dev/null @@ -1,43 +0,0 @@ -[package] -name = "theme" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[features] -default = [] -test-support = ["gpui/test-support", "fs/test-support", "settings/test-support"] - -[lib] -path = "src/theme.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -collections.workspace = true -derive_more.workspace = true -fs.workspace = true -futures.workspace = true -gpui.workspace = true -log.workspace = true -palette = { workspace = true, default-features = false, features = ["std"] } -parking_lot.workspace = true -refineable.workspace = true -schemars = { workspace = true, features = ["indexmap2"] } -serde.workspace = true -serde_json.workspace = true -serde_json_lenient.workspace = true -settings.workspace = true -strum.workspace = true -thiserror.workspace = true -util.workspace = true -uuid.workspace = true - -[dev-dependencies] -fs = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -settings = { workspace = true, features = ["test-support"] } diff --git a/crates/theme/LICENSE-GPL b/crates/theme/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/theme/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/theme/src/default_colors.rs b/crates/theme/src/default_colors.rs deleted file mode 100644 index 82be2896c6..0000000000 --- a/crates/theme/src/default_colors.rs +++ /dev/null @@ -1,2504 +0,0 @@ -use gpui::{Hsla, Rgba}; - -use crate::ColorScale; -use crate::scale::{ColorScaleSet, ColorScales}; -use crate::{SystemColors, ThemeColors}; - -pub(crate) fn neutral() -> ColorScaleSet { - sand() -} - -const ADDED_COLOR: Hsla = Hsla { - h: 134. / 360., - s: 0.55, - l: 0.40, - a: 1.0, -}; -const WORD_ADDED_COLOR: Hsla = Hsla { - h: 134. / 360., - s: 0.55, - l: 0.40, - a: 0.35, -}; -const MODIFIED_COLOR: Hsla = Hsla { - h: 48. / 360., - s: 0.76, - l: 0.47, - a: 1.0, -}; -const REMOVED_COLOR: Hsla = Hsla { - h: 350. / 360., - s: 0.88, - l: 0.25, - a: 1.0, -}; -const WORD_DELETED_COLOR: Hsla = Hsla { - h: 350. / 360., - s: 0.88, - l: 0.25, - a: 0.80, -}; - -/// The default colors for the theme. -/// -/// Themes that do not specify all colors are refined off of these defaults. -impl ThemeColors { - /// Returns the default colors for light themes. - /// - /// Themes that do not specify all colors are refined off of these defaults. - pub fn light() -> Self { - let system = SystemColors::default(); - - Self { - border: neutral().light().step_6(), - border_variant: neutral().light().step_5(), - border_focused: blue().light().step_5(), - border_selected: blue().light().step_5(), - border_transparent: system.transparent, - border_disabled: neutral().light().step_3(), - elevated_surface_background: neutral().light().step_2(), - surface_background: neutral().light().step_2(), - background: neutral().light().step_1(), - element_background: neutral().light().step_3(), - element_hover: neutral().light_alpha().step_4(), - element_active: neutral().light_alpha().step_5(), - element_selected: neutral().light_alpha().step_5(), - element_disabled: neutral().light_alpha().step_3(), - element_selection_background: blue().light().step_3().alpha(0.25), - drop_target_background: blue().light_alpha().step_2(), - drop_target_border: neutral().light().step_12(), - ghost_element_background: system.transparent, - ghost_element_hover: neutral().light_alpha().step_3(), - ghost_element_active: neutral().light_alpha().step_4(), - ghost_element_selected: neutral().light_alpha().step_5(), - ghost_element_disabled: neutral().light_alpha().step_3(), - text: neutral().light().step_12(), - text_muted: neutral().light().step_10(), - text_placeholder: neutral().light().step_10(), - text_disabled: neutral().light().step_9(), - text_accent: blue().light().step_11(), - icon: neutral().light().step_11(), - icon_muted: neutral().light().step_10(), - icon_disabled: neutral().light().step_9(), - icon_placeholder: neutral().light().step_10(), - icon_accent: blue().light().step_11(), - debugger_accent: red().light().step_10(), - status_bar_background: neutral().light().step_2(), - title_bar_background: neutral().light().step_2(), - title_bar_inactive_background: neutral().light().step_3(), - toolbar_background: neutral().light().step_1(), - tab_bar_background: neutral().light().step_2(), - tab_inactive_background: neutral().light().step_2(), - tab_active_background: neutral().light().step_1(), - search_match_background: neutral().light().step_5(), - search_active_match_background: neutral().light().step_7(), - panel_background: neutral().light().step_2(), - panel_focused_border: blue().light().step_10(), - panel_indent_guide: neutral().light_alpha().step_5(), - panel_indent_guide_hover: neutral().light_alpha().step_6(), - panel_indent_guide_active: neutral().light_alpha().step_6(), - panel_overlay_background: neutral().light().step_2(), - panel_overlay_hover: neutral().light().step_4(), - pane_focused_border: blue().light().step_5(), - pane_group_border: neutral().light().step_6(), - scrollbar_thumb_background: neutral().light_alpha().step_3(), - scrollbar_thumb_hover_background: neutral().light_alpha().step_4(), - scrollbar_thumb_active_background: neutral().light_alpha().step_5(), - scrollbar_thumb_border: gpui::transparent_black(), - scrollbar_track_background: gpui::transparent_black(), - scrollbar_track_border: neutral().light().step_5(), - minimap_thumb_background: neutral().light_alpha().step_3().alpha(0.7), - minimap_thumb_hover_background: neutral().light_alpha().step_4().alpha(0.7), - minimap_thumb_active_background: neutral().light_alpha().step_5().alpha(0.7), - minimap_thumb_border: gpui::transparent_black(), - editor_foreground: neutral().light().step_12(), - editor_background: neutral().light().step_1(), - editor_gutter_background: neutral().light().step_1(), - editor_subheader_background: neutral().light().step_2(), - editor_active_line_background: neutral().light_alpha().step_3(), - editor_highlighted_line_background: neutral().light_alpha().step_3(), - editor_debugger_active_line_background: yellow().dark_alpha().step_3(), - editor_line_number: neutral().light().step_10(), - editor_hover_line_number: neutral().light().step_12(), - editor_active_line_number: neutral().light().step_11(), - editor_invisible: neutral().light().step_10(), - editor_wrap_guide: neutral().light_alpha().step_7(), - editor_active_wrap_guide: neutral().light_alpha().step_8(), - editor_indent_guide: neutral().light_alpha().step_5(), - editor_indent_guide_active: neutral().light_alpha().step_6(), - editor_document_highlight_read_background: neutral().light_alpha().step_3(), - editor_document_highlight_write_background: neutral().light_alpha().step_4(), - editor_document_highlight_bracket_background: green().light_alpha().step_5(), - terminal_background: neutral().light().step_1(), - terminal_foreground: black().light().step_12(), - terminal_bright_foreground: black().light().step_11(), - terminal_dim_foreground: black().light().step_10(), - terminal_ansi_background: neutral().light().step_1(), - terminal_ansi_bright_black: black().light().step_11(), - terminal_ansi_bright_red: red().light().step_10(), - terminal_ansi_bright_green: green().light().step_10(), - terminal_ansi_bright_yellow: yellow().light().step_10(), - terminal_ansi_bright_blue: blue().light().step_10(), - terminal_ansi_bright_magenta: violet().light().step_10(), - terminal_ansi_bright_cyan: cyan().light().step_10(), - terminal_ansi_bright_white: neutral().light().step_11(), - terminal_ansi_black: black().light().step_12(), - terminal_ansi_red: red().light().step_11(), - terminal_ansi_green: green().light().step_11(), - terminal_ansi_yellow: yellow().light().step_11(), - terminal_ansi_blue: blue().light().step_11(), - terminal_ansi_magenta: violet().light().step_11(), - terminal_ansi_cyan: cyan().light().step_11(), - terminal_ansi_white: neutral().light().step_12(), - terminal_ansi_dim_black: black().light().step_11(), - terminal_ansi_dim_red: red().light().step_10(), - terminal_ansi_dim_green: green().light().step_10(), - terminal_ansi_dim_yellow: yellow().light().step_10(), - terminal_ansi_dim_blue: blue().light().step_10(), - terminal_ansi_dim_magenta: violet().light().step_10(), - terminal_ansi_dim_cyan: cyan().light().step_10(), - terminal_ansi_dim_white: neutral().light().step_11(), - link_text_hover: orange().light().step_10(), - version_control_added: ADDED_COLOR, - version_control_deleted: REMOVED_COLOR, - version_control_modified: MODIFIED_COLOR, - version_control_renamed: MODIFIED_COLOR, - version_control_conflict: orange().light().step_12(), - version_control_ignored: gray().light().step_12(), - version_control_word_added: WORD_ADDED_COLOR, - version_control_word_deleted: WORD_DELETED_COLOR, - version_control_conflict_marker_ours: green().light().step_10().alpha(0.5), - version_control_conflict_marker_theirs: blue().light().step_10().alpha(0.5), - vim_normal_background: system.transparent, - vim_insert_background: system.transparent, - vim_replace_background: system.transparent, - vim_visual_background: system.transparent, - vim_visual_line_background: system.transparent, - vim_visual_block_background: system.transparent, - vim_helix_normal_background: system.transparent, - vim_helix_select_background: system.transparent, - vim_mode_text: system.transparent, - } - } - - /// Returns the default colors for dark themes. - /// - /// Themes that do not specify all colors are refined off of these defaults. - pub fn dark() -> Self { - let system = SystemColors::default(); - - Self { - border: neutral().dark().step_6(), - border_variant: neutral().dark().step_5(), - border_focused: blue().dark().step_5(), - border_selected: blue().dark().step_5(), - border_transparent: system.transparent, - border_disabled: neutral().dark().step_3(), - elevated_surface_background: neutral().dark().step_2(), - surface_background: neutral().dark().step_2(), - background: neutral().dark().step_1(), - element_background: neutral().dark().step_3(), - element_hover: neutral().dark_alpha().step_4(), - element_active: neutral().dark_alpha().step_5(), - element_selected: neutral().dark_alpha().step_5(), - element_disabled: neutral().dark_alpha().step_3(), - element_selection_background: blue().dark().step_3().alpha(0.25), - drop_target_background: blue().dark_alpha().step_2(), - drop_target_border: neutral().dark().step_12(), - ghost_element_background: system.transparent, - ghost_element_hover: neutral().dark_alpha().step_4(), - ghost_element_active: neutral().dark_alpha().step_5(), - ghost_element_selected: neutral().dark_alpha().step_5(), - ghost_element_disabled: neutral().dark_alpha().step_3(), - text: neutral().dark().step_12(), - text_muted: neutral().dark().step_11(), - text_placeholder: neutral().dark().step_10(), - text_disabled: neutral().dark().step_9(), - text_accent: blue().dark().step_11(), - icon: neutral().dark().step_11(), - icon_muted: neutral().dark().step_10(), - icon_disabled: neutral().dark().step_9(), - icon_placeholder: neutral().dark().step_10(), - icon_accent: blue().dark().step_11(), - debugger_accent: red().light().step_10(), - status_bar_background: neutral().dark().step_2(), - title_bar_background: neutral().dark().step_2(), - title_bar_inactive_background: neutral().dark().step_3(), - toolbar_background: neutral().dark().step_1(), - tab_bar_background: neutral().dark().step_2(), - tab_inactive_background: neutral().dark().step_2(), - tab_active_background: neutral().dark().step_1(), - search_match_background: neutral().dark().step_5(), - search_active_match_background: neutral().dark().step_3(), - panel_background: neutral().dark().step_2(), - panel_focused_border: blue().dark().step_8(), - panel_indent_guide: neutral().dark_alpha().step_4(), - panel_indent_guide_hover: neutral().dark_alpha().step_6(), - panel_indent_guide_active: neutral().dark_alpha().step_6(), - panel_overlay_background: neutral().dark().step_2(), - panel_overlay_hover: neutral().dark().step_4(), - pane_focused_border: blue().dark().step_5(), - pane_group_border: neutral().dark().step_6(), - scrollbar_thumb_background: neutral().dark_alpha().step_3(), - scrollbar_thumb_hover_background: neutral().dark_alpha().step_4(), - scrollbar_thumb_active_background: neutral().dark_alpha().step_5(), - scrollbar_thumb_border: gpui::transparent_black(), - scrollbar_track_background: gpui::transparent_black(), - scrollbar_track_border: neutral().dark().step_5(), - minimap_thumb_background: neutral().dark_alpha().step_3().alpha(0.7), - minimap_thumb_hover_background: neutral().dark_alpha().step_4().alpha(0.7), - minimap_thumb_active_background: neutral().dark_alpha().step_5().alpha(0.7), - minimap_thumb_border: gpui::transparent_black(), - editor_foreground: neutral().dark().step_12(), - editor_background: neutral().dark().step_1(), - editor_gutter_background: neutral().dark().step_1(), - editor_subheader_background: neutral().dark().step_3(), - editor_active_line_background: neutral().dark_alpha().step_3(), - editor_highlighted_line_background: yellow().dark_alpha().step_4(), - editor_debugger_active_line_background: yellow().dark_alpha().step_3(), - editor_line_number: neutral().dark_alpha().step_10(), - editor_hover_line_number: neutral().dark_alpha().step_12(), - editor_active_line_number: neutral().dark_alpha().step_11(), - editor_invisible: neutral().dark_alpha().step_4(), - editor_wrap_guide: neutral().dark_alpha().step_4(), - editor_active_wrap_guide: neutral().dark_alpha().step_4(), - editor_indent_guide: neutral().dark_alpha().step_4(), - editor_indent_guide_active: neutral().dark_alpha().step_6(), - editor_document_highlight_read_background: neutral().dark_alpha().step_4(), - editor_document_highlight_write_background: neutral().dark_alpha().step_4(), - editor_document_highlight_bracket_background: green().dark_alpha().step_6(), - terminal_background: neutral().dark().step_1(), - terminal_ansi_background: neutral().dark().step_1(), - terminal_foreground: white().dark().step_12(), - terminal_bright_foreground: white().dark().step_11(), - terminal_dim_foreground: white().dark().step_10(), - terminal_ansi_black: black().dark().step_12(), - terminal_ansi_bright_black: black().dark().step_11(), - terminal_ansi_dim_black: black().dark().step_10(), - terminal_ansi_red: red().dark().step_11(), - terminal_ansi_bright_red: red().dark().step_10(), - terminal_ansi_dim_red: red().dark().step_9(), - terminal_ansi_green: green().dark().step_11(), - terminal_ansi_bright_green: green().dark().step_10(), - terminal_ansi_dim_green: green().dark().step_9(), - terminal_ansi_yellow: yellow().dark().step_11(), - terminal_ansi_bright_yellow: yellow().dark().step_10(), - terminal_ansi_dim_yellow: yellow().dark().step_9(), - terminal_ansi_blue: blue().dark().step_11(), - terminal_ansi_bright_blue: blue().dark().step_10(), - terminal_ansi_dim_blue: blue().dark().step_9(), - terminal_ansi_magenta: violet().dark().step_11(), - terminal_ansi_bright_magenta: violet().dark().step_10(), - terminal_ansi_dim_magenta: violet().dark().step_9(), - terminal_ansi_cyan: cyan().dark().step_11(), - terminal_ansi_bright_cyan: cyan().dark().step_10(), - terminal_ansi_dim_cyan: cyan().dark().step_9(), - terminal_ansi_white: neutral().dark().step_12(), - terminal_ansi_bright_white: neutral().dark().step_11(), - terminal_ansi_dim_white: neutral().dark().step_10(), - link_text_hover: orange().dark().step_10(), - version_control_added: ADDED_COLOR, - version_control_deleted: REMOVED_COLOR, - version_control_modified: MODIFIED_COLOR, - version_control_renamed: MODIFIED_COLOR, - version_control_conflict: orange().dark().step_12(), - version_control_ignored: gray().dark().step_12(), - version_control_word_added: WORD_ADDED_COLOR, - version_control_word_deleted: WORD_DELETED_COLOR, - version_control_conflict_marker_ours: green().dark().step_10().alpha(0.5), - version_control_conflict_marker_theirs: blue().dark().step_10().alpha(0.5), - vim_normal_background: system.transparent, - vim_insert_background: system.transparent, - vim_replace_background: system.transparent, - vim_visual_background: system.transparent, - vim_visual_line_background: system.transparent, - vim_visual_block_background: system.transparent, - vim_helix_normal_background: system.transparent, - vim_helix_select_background: system.transparent, - vim_mode_text: system.transparent, - } - } -} - -type StaticColorScale = [&'static str; 12]; - -struct StaticColorScaleSet { - scale: &'static str, - light: StaticColorScale, - light_alpha: StaticColorScale, - dark: StaticColorScale, - dark_alpha: StaticColorScale, -} - -impl TryFrom for ColorScaleSet { - type Error = anyhow::Error; - - fn try_from(value: StaticColorScaleSet) -> Result { - fn to_color_scale(scale: StaticColorScale) -> anyhow::Result { - scale - .into_iter() - .map(|color| Rgba::try_from(color).map(Hsla::from)) - .collect::, _>>() - .map(ColorScale::from_iter) - } - - Ok(Self::new( - value.scale, - to_color_scale(value.light)?, - to_color_scale(value.light_alpha)?, - to_color_scale(value.dark)?, - to_color_scale(value.dark_alpha)?, - )) - } -} - -/// Color scales used to build the default themes. -pub fn default_color_scales() -> ColorScales { - ColorScales { - gray: gray(), - mauve: mauve(), - slate: slate(), - sage: sage(), - olive: olive(), - sand: sand(), - gold: gold(), - bronze: bronze(), - brown: brown(), - yellow: yellow(), - amber: amber(), - orange: orange(), - tomato: tomato(), - red: red(), - ruby: ruby(), - crimson: crimson(), - pink: pink(), - plum: plum(), - purple: purple(), - violet: violet(), - iris: iris(), - indigo: indigo(), - blue: blue(), - cyan: cyan(), - teal: teal(), - jade: jade(), - green: green(), - grass: grass(), - lime: lime(), - mint: mint(), - sky: sky(), - black: black(), - white: white(), - } -} - -pub(crate) fn gray() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Gray", - light: [ - "#fcfcfcff", - "#f9f9f9ff", - "#f0f0f0ff", - "#e8e8e8ff", - "#e0e0e0ff", - "#d9d9d9ff", - "#cececeff", - "#bbbbbbff", - "#8d8d8dff", - "#838383ff", - "#646464ff", - "#202020ff", - ], - light_alpha: [ - "#00000003", - "#00000006", - "#0000000f", - "#00000017", - "#0000001f", - "#00000026", - "#00000031", - "#00000044", - "#00000072", - "#0000007c", - "#0000009b", - "#000000df", - ], - dark: [ - "#111111ff", - "#191919ff", - "#222222ff", - "#2a2a2aff", - "#313131ff", - "#3a3a3aff", - "#484848ff", - "#606060ff", - "#6e6e6eff", - "#7b7b7bff", - "#b4b4b4ff", - "#eeeeeeff", - ], - dark_alpha: [ - "#00000000", - "#ffffff09", - "#ffffff12", - "#ffffff1b", - "#ffffff22", - "#ffffff2c", - "#ffffff3b", - "#ffffff55", - "#ffffff64", - "#ffffff72", - "#ffffffaf", - "#ffffffed", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn mauve() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Mauve", - light: [ - "#fdfcfdff", - "#faf9fbff", - "#f2eff3ff", - "#eae7ecff", - "#e3dfe6ff", - "#dbd8e0ff", - "#d0cdd7ff", - "#bcbac7ff", - "#8e8c99ff", - "#84828eff", - "#65636dff", - "#211f26ff", - ], - light_alpha: [ - "#55005503", - "#2b005506", - "#30004010", - "#20003618", - "#20003820", - "#14003527", - "#10003332", - "#08003145", - "#05001d73", - "#0500197d", - "#0400119c", - "#020008e0", - ], - dark: [ - "#121113ff", - "#1a191bff", - "#232225ff", - "#2b292dff", - "#323035ff", - "#3c393fff", - "#49474eff", - "#625f69ff", - "#6f6d78ff", - "#7c7a85ff", - "#b5b2bcff", - "#eeeef0ff", - ], - dark_alpha: [ - "#00000000", - "#f5f4f609", - "#ebeaf814", - "#eee5f81d", - "#efe6fe25", - "#f1e6fd30", - "#eee9ff40", - "#eee7ff5d", - "#eae6fd6e", - "#ece9fd7c", - "#f5f1ffb7", - "#fdfdffef", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn slate() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Slate", - light: [ - "#fcfcfdff", - "#f9f9fbff", - "#f0f0f3ff", - "#e8e8ecff", - "#e0e1e6ff", - "#d9d9e0ff", - "#cdced6ff", - "#b9bbc6ff", - "#8b8d98ff", - "#80838dff", - "#60646cff", - "#1c2024ff", - ], - light_alpha: [ - "#00005503", - "#00005506", - "#0000330f", - "#00002d17", - "#0009321f", - "#00002f26", - "#00062e32", - "#00083046", - "#00051d74", - "#00071b7f", - "#0007149f", - "#000509e3", - ], - dark: [ - "#111113ff", - "#18191bff", - "#212225ff", - "#272a2dff", - "#2e3135ff", - "#363a3fff", - "#43484eff", - "#5a6169ff", - "#696e77ff", - "#777b84ff", - "#b0b4baff", - "#edeef0ff", - ], - dark_alpha: [ - "#00000000", - "#d8f4f609", - "#ddeaf814", - "#d3edf81d", - "#d9edfe25", - "#d6ebfd30", - "#d9edff40", - "#d9edff5d", - "#dfebfd6d", - "#e5edfd7b", - "#f1f7feb5", - "#fcfdffef", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn sage() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Sage", - light: [ - "#fbfdfcff", - "#f7f9f8ff", - "#eef1f0ff", - "#e6e9e8ff", - "#dfe2e0ff", - "#d7dad9ff", - "#cbcfcdff", - "#b8bcbaff", - "#868e8bff", - "#7c8481ff", - "#5f6563ff", - "#1a211eff", - ], - light_alpha: [ - "#00804004", - "#00402008", - "#002d1e11", - "#001f1519", - "#00180820", - "#00140d28", - "#00140a34", - "#000f0847", - "#00110b79", - "#00100a83", - "#000a07a0", - "#000805e5", - ], - dark: [ - "#101211ff", - "#171918ff", - "#202221ff", - "#272a29ff", - "#2e3130ff", - "#373b39ff", - "#444947ff", - "#5b625fff", - "#63706bff", - "#717d79ff", - "#adb5b2ff", - "#eceeedff", - ], - dark_alpha: [ - "#00000000", - "#f0f2f108", - "#f3f5f412", - "#f2fefd1a", - "#f1fbfa22", - "#edfbf42d", - "#edfcf73c", - "#ebfdf657", - "#dffdf266", - "#e5fdf674", - "#f4fefbb0", - "#fdfffeed", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn olive() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Olive", - light: [ - "#fcfdfcff", - "#f8faf8ff", - "#eff1efff", - "#e7e9e7ff", - "#dfe2dfff", - "#d7dad7ff", - "#cccfccff", - "#b9bcb8ff", - "#898e87ff", - "#7f847dff", - "#60655fff", - "#1d211cff", - ], - light_alpha: [ - "#00550003", - "#00490007", - "#00200010", - "#00160018", - "#00180020", - "#00140028", - "#000f0033", - "#040f0047", - "#050f0078", - "#040e0082", - "#020a00a0", - "#010600e3", - ], - dark: [ - "#111210ff", - "#181917ff", - "#212220ff", - "#282a27ff", - "#2f312eff", - "#383a36ff", - "#454843ff", - "#5c625bff", - "#687066ff", - "#767d74ff", - "#afb5adff", - "#eceeecff", - ], - dark_alpha: [ - "#00000000", - "#f1f2f008", - "#f4f5f312", - "#f3fef21a", - "#f2fbf122", - "#f4faed2c", - "#f2fced3b", - "#edfdeb57", - "#ebfde766", - "#f0fdec74", - "#f6fef4b0", - "#fdfffded", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn sand() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Sand", - light: [ - "#fdfdfcff", - "#f9f9f8ff", - "#f1f0efff", - "#e9e8e6ff", - "#e2e1deff", - "#dad9d6ff", - "#cfcecaff", - "#bcbbb5ff", - "#8d8d86ff", - "#82827cff", - "#63635eff", - "#21201cff", - ], - light_alpha: [ - "#55550003", - "#25250007", - "#20100010", - "#1f150019", - "#1f180021", - "#19130029", - "#19140035", - "#1915014a", - "#0f0f0079", - "#0c0c0083", - "#080800a1", - "#060500e3", - ], - dark: [ - "#111110ff", - "#191918ff", - "#222221ff", - "#2a2a28ff", - "#31312eff", - "#3b3a37ff", - "#494844ff", - "#62605bff", - "#6f6d66ff", - "#7c7b74ff", - "#b5b3adff", - "#eeeeecff", - ], - dark_alpha: [ - "#00000000", - "#f4f4f309", - "#f6f6f513", - "#fefef31b", - "#fbfbeb23", - "#fffaed2d", - "#fffbed3c", - "#fff9eb57", - "#fffae965", - "#fffdee73", - "#fffcf4b0", - "#fffffded", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn gold() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Gold", - light: [ - "#fdfdfcff", - "#faf9f2ff", - "#f2f0e7ff", - "#eae6dbff", - "#e1dccfff", - "#d8d0bfff", - "#cbc0aaff", - "#b9a88dff", - "#978365ff", - "#8c7a5eff", - "#71624bff", - "#3b352bff", - ], - light_alpha: [ - "#55550003", - "#9d8a000d", - "#75600018", - "#6b4e0024", - "#60460030", - "#64440040", - "#63420055", - "#633d0072", - "#5332009a", - "#492d00a1", - "#362100b4", - "#130c00d4", - ], - dark: [ - "#121211ff", - "#1b1a17ff", - "#24231fff", - "#2d2b26ff", - "#38352eff", - "#444039ff", - "#544f46ff", - "#696256ff", - "#978365ff", - "#a39073ff", - "#cbb99fff", - "#e8e2d9ff", - ], - dark_alpha: [ - "#91911102", - "#f9e29d0b", - "#f8ecbb15", - "#ffeec41e", - "#feecc22a", - "#feebcb37", - "#ffedcd48", - "#fdeaca5f", - "#ffdba690", - "#fedfb09d", - "#fee7c6c8", - "#fef7ede7", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn bronze() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Bronze", - light: [ - "#fdfcfcff", - "#fdf7f5ff", - "#f6edeaff", - "#efe4dfff", - "#e7d9d3ff", - "#dfcdc5ff", - "#d3bcb3ff", - "#c2a499ff", - "#a18072ff", - "#957468ff", - "#7d5e54ff", - "#43302bff", - ], - light_alpha: [ - "#55000003", - "#cc33000a", - "#92250015", - "#80280020", - "#7423002c", - "#7324003a", - "#6c1f004c", - "#671c0066", - "#551a008d", - "#4c150097", - "#3d0f00ab", - "#1d0600d4", - ], - dark: [ - "#141110ff", - "#1c1917ff", - "#262220ff", - "#302a27ff", - "#3b3330ff", - "#493e3aff", - "#5a4c47ff", - "#6f5f58ff", - "#a18072ff", - "#ae8c7eff", - "#d4b3a5ff", - "#ede0d9ff", - ], - dark_alpha: [ - "#d1110004", - "#fbbc910c", - "#faceb817", - "#facdb622", - "#ffd2c12d", - "#ffd1c03c", - "#fdd0c04f", - "#ffd6c565", - "#fec7b09b", - "#fecab5a9", - "#ffd7c6d1", - "#fff1e9ec", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn brown() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Brown", - light: [ - "#fefdfcff", - "#fcf9f6ff", - "#f6eee7ff", - "#f0e4d9ff", - "#ebdacaff", - "#e4cdb7ff", - "#dcbc9fff", - "#cea37eff", - "#ad7f58ff", - "#a07553ff", - "#815e46ff", - "#3e332eff", - ], - light_alpha: [ - "#aa550003", - "#aa550009", - "#a04b0018", - "#9b4a0026", - "#9f4d0035", - "#a04e0048", - "#a34e0060", - "#9f4a0081", - "#823c00a7", - "#723300ac", - "#522100b9", - "#140600d1", - ], - dark: [ - "#12110fff", - "#1c1816ff", - "#28211dff", - "#322922ff", - "#3e3128ff", - "#4d3c2fff", - "#614a39ff", - "#7c5f46ff", - "#ad7f58ff", - "#b88c67ff", - "#dbb594ff", - "#f2e1caff", - ], - dark_alpha: [ - "#91110002", - "#fba67c0c", - "#fcb58c19", - "#fbbb8a24", - "#fcb88931", - "#fdba8741", - "#ffbb8856", - "#ffbe8773", - "#feb87da8", - "#ffc18cb3", - "#fed1aad9", - "#feecd4f2", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn yellow() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Yellow", - light: [ - "#fdfdf9ff", - "#fefce9ff", - "#fffab8ff", - "#fff394ff", - "#ffe770ff", - "#f3d768ff", - "#e4c767ff", - "#d5ae39ff", - "#ffe629ff", - "#ffdc00ff", - "#9e6c00ff", - "#473b1fff", - ], - light_alpha: [ - "#aaaa0006", - "#f4dd0016", - "#ffee0047", - "#ffe3016b", - "#ffd5008f", - "#ebbc0097", - "#d2a10098", - "#c99700c6", - "#ffe100d6", - "#ffdc00ff", - "#9e6c00ff", - "#2e2000e0", - ], - dark: [ - "#14120bff", - "#1b180fff", - "#2d2305ff", - "#362b00ff", - "#433500ff", - "#524202ff", - "#665417ff", - "#836a21ff", - "#ffe629ff", - "#ffff57ff", - "#f5e147ff", - "#f6eeb4ff", - ], - dark_alpha: [ - "#d1510004", - "#f9b4000b", - "#ffaa001e", - "#fdb70028", - "#febb0036", - "#fec40046", - "#fdcb225c", - "#fdca327b", - "#ffe629ff", - "#ffff57ff", - "#fee949f5", - "#fef6baf6", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn amber() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Amber", - light: [ - "#fefdfbff", - "#fefbe9ff", - "#fff7c2ff", - "#ffee9cff", - "#fbe577ff", - "#f3d673ff", - "#e9c162ff", - "#e2a336ff", - "#ffc53dff", - "#ffba18ff", - "#ab6400ff", - "#4f3422ff", - ], - light_alpha: [ - "#c0800004", - "#f4d10016", - "#ffde003d", - "#ffd40063", - "#f8cf0088", - "#eab5008c", - "#dc9b009d", - "#da8a00c9", - "#ffb300c2", - "#ffb300e7", - "#ab6400ff", - "#341500dd", - ], - dark: [ - "#16120cff", - "#1d180fff", - "#302008ff", - "#3f2700ff", - "#4d3000ff", - "#5c3d05ff", - "#714f19ff", - "#8f6424ff", - "#ffc53dff", - "#ffd60aff", - "#ffca16ff", - "#ffe7b3ff", - ], - dark_alpha: [ - "#e63c0006", - "#fd9b000d", - "#fa820022", - "#fc820032", - "#fd8b0041", - "#fd9b0051", - "#ffab2567", - "#ffae3587", - "#ffc53dff", - "#ffd60aff", - "#ffca16ff", - "#ffe7b3ff", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn orange() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Orange", - light: [ - "#fefcfbff", - "#fff7edff", - "#ffefd6ff", - "#ffdfb5ff", - "#ffd19aff", - "#ffc182ff", - "#f5ae73ff", - "#ec9455ff", - "#f76b15ff", - "#ef5f00ff", - "#cc4e00ff", - "#582d1dff", - ], - light_alpha: [ - "#c0400004", - "#ff8e0012", - "#ff9c0029", - "#ff91014a", - "#ff8b0065", - "#ff81007d", - "#ed6c008c", - "#e35f00aa", - "#f65e00ea", - "#ef5f00ff", - "#cc4e00ff", - "#431200e2", - ], - dark: [ - "#17120eff", - "#1e160fff", - "#331e0bff", - "#462100ff", - "#562800ff", - "#66350cff", - "#7e451dff", - "#a35829ff", - "#f76b15ff", - "#ff801fff", - "#ffa057ff", - "#ffe0c2ff", - ], - dark_alpha: [ - "#ec360007", - "#fe6d000e", - "#fb6a0025", - "#ff590039", - "#ff61004a", - "#fd75045c", - "#ff832c75", - "#fe84389d", - "#fe6d15f7", - "#ff801fff", - "#ffa057ff", - "#ffe0c2ff", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn tomato() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Tomato", - light: [ - "#fffcfcff", - "#fff8f7ff", - "#feebe7ff", - "#ffdcd3ff", - "#ffcdc2ff", - "#fdbdafff", - "#f5a898ff", - "#ec8e7bff", - "#e54d2eff", - "#dd4425ff", - "#d13415ff", - "#5c271fff", - ], - light_alpha: [ - "#ff000003", - "#ff200008", - "#f52b0018", - "#ff35002c", - "#ff2e003d", - "#f92d0050", - "#e7280067", - "#db250084", - "#df2600d1", - "#d72400da", - "#cd2200ea", - "#460900e0", - ], - dark: [ - "#181111ff", - "#1f1513ff", - "#391714ff", - "#4e1511ff", - "#5e1c16ff", - "#6e2920ff", - "#853a2dff", - "#ac4d39ff", - "#e54d2eff", - "#ec6142ff", - "#ff977dff", - "#fbd3cbff", - ], - dark_alpha: [ - "#f1121208", - "#ff55330f", - "#ff35232b", - "#fd201142", - "#fe332153", - "#ff4f3864", - "#fd644a7d", - "#fe6d4ea7", - "#fe5431e4", - "#ff6847eb", - "#ff977dff", - "#ffd6cefb", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn red() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Red", - light: [ - "#fffcfcff", - "#fff7f7ff", - "#feebecff", - "#ffdbdcff", - "#ffcdceff", - "#fdbdbeff", - "#f4a9aaff", - "#eb8e90ff", - "#e5484dff", - "#dc3e42ff", - "#ce2c31ff", - "#641723ff", - ], - light_alpha: [ - "#ff000003", - "#ff000008", - "#f3000d14", - "#ff000824", - "#ff000632", - "#f8000442", - "#df000356", - "#d2000571", - "#db0007b7", - "#d10005c1", - "#c40006d3", - "#55000de8", - ], - dark: [ - "#191111ff", - "#201314ff", - "#3b1219ff", - "#500f1cff", - "#611623ff", - "#72232dff", - "#8c333aff", - "#b54548ff", - "#e5484dff", - "#ec5d5eff", - "#ff9592ff", - "#ffd1d9ff", - ], - dark_alpha: [ - "#f4121209", - "#f22f3e11", - "#ff173f2d", - "#fe0a3b44", - "#ff204756", - "#ff3e5668", - "#ff536184", - "#ff5d61b0", - "#fe4e54e4", - "#ff6465eb", - "#ff9592ff", - "#ffd1d9ff", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn ruby() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Ruby", - light: [ - "#fffcfdff", - "#fff7f8ff", - "#feeaedff", - "#ffdce1ff", - "#ffced6ff", - "#f8bfc8ff", - "#efacb8ff", - "#e592a3ff", - "#e54666ff", - "#dc3b5dff", - "#ca244dff", - "#64172bff", - ], - light_alpha: [ - "#ff005503", - "#ff002008", - "#f3002515", - "#ff002523", - "#ff002a31", - "#e4002440", - "#ce002553", - "#c300286d", - "#db002cb9", - "#d2002cc4", - "#c10030db", - "#550016e8", - ], - dark: [ - "#191113ff", - "#1e1517ff", - "#3a141eff", - "#4e1325ff", - "#5e1a2eff", - "#6f2539ff", - "#883447ff", - "#b3445aff", - "#e54666ff", - "#ec5a72ff", - "#ff949dff", - "#fed2e1ff", - ], - dark_alpha: [ - "#f4124a09", - "#fe5a7f0e", - "#ff235d2c", - "#fd195e42", - "#fe2d6b53", - "#ff447665", - "#ff577d80", - "#ff5c7cae", - "#fe4c70e4", - "#ff617beb", - "#ff949dff", - "#ffd3e2fe", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn crimson() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Crimson", - light: [ - "#fffcfdff", - "#fef7f9ff", - "#ffe9f0ff", - "#fedce7ff", - "#faceddff", - "#f3bed1ff", - "#eaacc3ff", - "#e093b2ff", - "#e93d82ff", - "#df3478ff", - "#cb1d63ff", - "#621639ff", - ], - light_alpha: [ - "#ff005503", - "#e0004008", - "#ff005216", - "#f8005123", - "#e5004f31", - "#d0004b41", - "#bf004753", - "#b6004a6c", - "#e2005bc2", - "#d70056cb", - "#c4004fe2", - "#530026e9", - ], - dark: [ - "#191114ff", - "#201318ff", - "#381525ff", - "#4d122fff", - "#5c1839ff", - "#6d2545ff", - "#873356ff", - "#b0436eff", - "#e93d82ff", - "#ee518aff", - "#ff92adff", - "#fdd3e8ff", - ], - dark_alpha: [ - "#f4126709", - "#f22f7a11", - "#fe2a8b2a", - "#fd158741", - "#fd278f51", - "#fe459763", - "#fd559b7f", - "#fe5b9bab", - "#fe418de8", - "#ff5693ed", - "#ff92adff", - "#ffd5eafd", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn pink() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Pink", - light: [ - "#fffcfeff", - "#fef7fbff", - "#fee9f5ff", - "#fbdcefff", - "#f6cee7ff", - "#efbfddff", - "#e7acd0ff", - "#dd93c2ff", - "#d6409fff", - "#cf3897ff", - "#c2298aff", - "#651249ff", - ], - light_alpha: [ - "#ff00aa03", - "#e0008008", - "#f4008c16", - "#e2008b23", - "#d1008331", - "#c0007840", - "#b6006f53", - "#af006f6c", - "#c8007fbf", - "#c2007ac7", - "#b60074d6", - "#59003bed", - ], - dark: [ - "#191117ff", - "#21121dff", - "#37172fff", - "#4b143dff", - "#591c47ff", - "#692955ff", - "#833869ff", - "#a84885ff", - "#d6409fff", - "#de51a8ff", - "#ff8dccff", - "#fdd1eaff", - ], - dark_alpha: [ - "#f412bc09", - "#f420bb12", - "#fe37cc29", - "#fc1ec43f", - "#fd35c24e", - "#fd51c75f", - "#fd62c87b", - "#ff68c8a2", - "#fe49bcd4", - "#ff5cc0dc", - "#ff8dccff", - "#ffd3ecfd", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn plum() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Plum", - light: [ - "#fefcffff", - "#fdf7fdff", - "#fbebfbff", - "#f7def8ff", - "#f2d1f3ff", - "#e9c2ecff", - "#deade3ff", - "#cf91d8ff", - "#ab4abaff", - "#a144afff", - "#953ea3ff", - "#53195dff", - ], - light_alpha: [ - "#aa00ff03", - "#c000c008", - "#cc00cc14", - "#c200c921", - "#b700bd2e", - "#a400b03d", - "#9900a852", - "#9000a56e", - "#89009eb5", - "#7f0092bb", - "#730086c1", - "#40004be6", - ], - dark: [ - "#181118ff", - "#201320ff", - "#351a35ff", - "#451d47ff", - "#512454ff", - "#5e3061ff", - "#734079ff", - "#92549cff", - "#ab4abaff", - "#b658c4ff", - "#e796f3ff", - "#f4d4f4ff", - ], - dark_alpha: [ - "#f112f108", - "#f22ff211", - "#fd4cfd27", - "#f646ff3a", - "#f455ff48", - "#f66dff56", - "#f07cfd70", - "#ee84ff95", - "#e961feb6", - "#ed70ffc0", - "#f19cfef3", - "#feddfef4", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn purple() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Purple", - light: [ - "#fefcfeff", - "#fbf7feff", - "#f7edfeff", - "#f2e2fcff", - "#ead5f9ff", - "#e0c4f4ff", - "#d1afecff", - "#be93e4ff", - "#8e4ec6ff", - "#8347b9ff", - "#8145b5ff", - "#402060ff", - ], - light_alpha: [ - "#aa00aa03", - "#8000e008", - "#8e00f112", - "#8d00e51d", - "#8000db2a", - "#7a01d03b", - "#6d00c350", - "#6600c06c", - "#5c00adb1", - "#53009eb8", - "#52009aba", - "#250049df", - ], - dark: [ - "#18111bff", - "#1e1523ff", - "#301c3bff", - "#3d224eff", - "#48295cff", - "#54346bff", - "#664282ff", - "#8457aaff", - "#8e4ec6ff", - "#9a5cd0ff", - "#d19dffff", - "#ecd9faff", - ], - dark_alpha: [ - "#b412f90b", - "#b744f714", - "#c150ff2d", - "#bb53fd42", - "#be5cfd51", - "#c16dfd61", - "#c378fd7a", - "#c47effa4", - "#b661ffc2", - "#bc6fffcd", - "#d19dffff", - "#f1ddfffa", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn violet() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Violet", - light: [ - "#fdfcfeff", - "#faf8ffff", - "#f4f0feff", - "#ebe4ffff", - "#e1d9ffff", - "#d4cafeff", - "#c2b5f5ff", - "#aa99ecff", - "#6e56cfff", - "#654dc4ff", - "#6550b9ff", - "#2f265fff", - ], - light_alpha: [ - "#5500aa03", - "#4900ff07", - "#4400ee0f", - "#4300ff1b", - "#3600ff26", - "#3100fb35", - "#2d01dd4a", - "#2b00d066", - "#2400b7a9", - "#2300abb2", - "#1f0099af", - "#0b0043d9", - ], - dark: [ - "#14121fff", - "#1b1525ff", - "#291f43ff", - "#33255bff", - "#3c2e69ff", - "#473876ff", - "#56468bff", - "#6958adff", - "#6e56cfff", - "#7d66d9ff", - "#baa7ffff", - "#e2ddfeff", - ], - dark_alpha: [ - "#4422ff0f", - "#853ff916", - "#8354fe36", - "#7d51fd50", - "#845ffd5f", - "#8f6cfd6d", - "#9879ff83", - "#977dfea8", - "#8668ffcc", - "#9176fed7", - "#baa7ffff", - "#e3defffe", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn iris() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Iris", - light: [ - "#fdfdffff", - "#f8f8ffff", - "#f0f1feff", - "#e6e7ffff", - "#dadcffff", - "#cbcdffff", - "#b8baf8ff", - "#9b9ef0ff", - "#5b5bd6ff", - "#5151cdff", - "#5753c6ff", - "#272962ff", - ], - light_alpha: [ - "#0000ff02", - "#0000ff07", - "#0011ee0f", - "#000bff19", - "#000eff25", - "#000aff34", - "#0008e647", - "#0008d964", - "#0000c0a4", - "#0000b6ae", - "#0600abac", - "#000246d8", - ], - dark: [ - "#13131eff", - "#171625ff", - "#202248ff", - "#262a65ff", - "#303374ff", - "#3d3e82ff", - "#4a4a95ff", - "#5958b1ff", - "#5b5bd6ff", - "#6e6adeff", - "#b1a9ffff", - "#e0dffeff", - ], - dark_alpha: [ - "#3636fe0e", - "#564bf916", - "#525bff3b", - "#4d58ff5a", - "#5b62fd6b", - "#6d6ffd7a", - "#7777fe8e", - "#7b7afeac", - "#6a6afed4", - "#7d79ffdc", - "#b1a9ffff", - "#e1e0fffe", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn indigo() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Indigo", - light: [ - "#fdfdfeff", - "#f7f9ffff", - "#edf2feff", - "#e1e9ffff", - "#d2deffff", - "#c1d0ffff", - "#abbdf9ff", - "#8da4efff", - "#3e63ddff", - "#3358d4ff", - "#3a5bc7ff", - "#1f2d5cff", - ], - light_alpha: [ - "#00008002", - "#0040ff08", - "#0047f112", - "#0044ff1e", - "#0044ff2d", - "#003eff3e", - "#0037ed54", - "#0034dc72", - "#0031d2c1", - "#002ec9cc", - "#002bb7c5", - "#001046e0", - ], - dark: [ - "#11131fff", - "#141726ff", - "#182449ff", - "#1d2e62ff", - "#253974ff", - "#304384ff", - "#3a4f97ff", - "#435db1ff", - "#3e63ddff", - "#5472e4ff", - "#9eb1ffff", - "#d6e1ffff", - ], - dark_alpha: [ - "#1133ff0f", - "#3354fa17", - "#2f62ff3c", - "#3566ff57", - "#4171fd6b", - "#5178fd7c", - "#5a7fff90", - "#5b81feac", - "#4671ffdb", - "#5c7efee3", - "#9eb1ffff", - "#d6e1ffff", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn blue() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Blue", - light: [ - "#fbfdffff", - "#f4faffff", - "#e6f4feff", - "#d5efffff", - "#c2e5ffff", - "#acd8fcff", - "#8ec8f6ff", - "#5eb1efff", - "#0090ffff", - "#0588f0ff", - "#0d74ceff", - "#113264ff", - ], - light_alpha: [ - "#0080ff04", - "#008cff0b", - "#008ff519", - "#009eff2a", - "#0093ff3d", - "#0088f653", - "#0083eb71", - "#0084e6a1", - "#0090ffff", - "#0086f0fa", - "#006dcbf2", - "#002359ee", - ], - dark: [ - "#0d1520ff", - "#111927ff", - "#0d2847ff", - "#003362ff", - "#004074ff", - "#104d87ff", - "#205d9eff", - "#2870bdff", - "#0090ffff", - "#3b9effff", - "#70b8ffff", - "#c2e6ffff", - ], - dark_alpha: [ - "#004df211", - "#1166fb18", - "#0077ff3a", - "#0075ff57", - "#0081fd6b", - "#0f89fd7f", - "#2a91fe98", - "#3094feb9", - "#0090ffff", - "#3b9effff", - "#70b8ffff", - "#c2e6ffff", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn cyan() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Cyan", - light: [ - "#fafdfeff", - "#f2fafbff", - "#def7f9ff", - "#caf1f6ff", - "#b5e9f0ff", - "#9ddde7ff", - "#7dcedcff", - "#3db9cfff", - "#00a2c7ff", - "#0797b9ff", - "#107d98ff", - "#0d3c48ff", - ], - light_alpha: [ - "#0099cc05", - "#009db10d", - "#00c2d121", - "#00bcd435", - "#01b4cc4a", - "#00a7c162", - "#009fbb82", - "#00a3c0c2", - "#00a2c7ff", - "#0094b7f8", - "#007491ef", - "#00323ef2", - ], - dark: [ - "#0b161aff", - "#101b20ff", - "#082c36ff", - "#003848ff", - "#004558ff", - "#045468ff", - "#12677eff", - "#11809cff", - "#00a2c7ff", - "#23afd0ff", - "#4ccce6ff", - "#b6ecf7ff", - ], - dark_alpha: [ - "#0091f70a", - "#02a7f211", - "#00befd28", - "#00baff3b", - "#00befd4d", - "#00c7fd5e", - "#14cdff75", - "#11cfff95", - "#00cfffc3", - "#28d6ffcd", - "#52e1fee5", - "#bbf3fef7", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn teal() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Teal", - light: [ - "#fafefdff", - "#f3fbf9ff", - "#e0f8f3ff", - "#ccf3eaff", - "#b8eae0ff", - "#a1ded2ff", - "#83cdc1ff", - "#53b9abff", - "#12a594ff", - "#0d9b8aff", - "#008573ff", - "#0d3d38ff", - ], - light_alpha: [ - "#00cc9905", - "#00aa800c", - "#00c69d1f", - "#00c39633", - "#00b49047", - "#00a6855e", - "#0099807c", - "#009783ac", - "#009e8ced", - "#009684f2", - "#008573ff", - "#00332df2", - ], - dark: [ - "#0d1514ff", - "#111c1bff", - "#0d2d2aff", - "#023b37ff", - "#084843ff", - "#145750ff", - "#1c6961ff", - "#207e73ff", - "#12a594ff", - "#0eb39eff", - "#0bd8b6ff", - "#adf0ddff", - ], - dark_alpha: [ - "#00deab05", - "#12fbe60c", - "#00ffe61e", - "#00ffe92d", - "#00ffea3b", - "#1cffe84b", - "#2efde85f", - "#32ffe775", - "#13ffe49f", - "#0dffe0ae", - "#0afed5d6", - "#b8ffebef", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn jade() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Jade", - light: [ - "#fbfefdff", - "#f4fbf7ff", - "#e6f7edff", - "#d6f1e3ff", - "#c3e9d7ff", - "#acdec8ff", - "#8bceb6ff", - "#56ba9fff", - "#29a383ff", - "#26997bff", - "#208368ff", - "#1d3b31ff", - ], - light_alpha: [ - "#00c08004", - "#00a3460b", - "#00ae4819", - "#00a85129", - "#00a2553c", - "#009a5753", - "#00945f74", - "#00976ea9", - "#00916bd6", - "#008764d9", - "#007152df", - "#002217e2", - ], - dark: [ - "#0d1512ff", - "#121c18ff", - "#0f2e22ff", - "#0b3b2cff", - "#114837ff", - "#1b5745ff", - "#246854ff", - "#2a7e68ff", - "#29a383ff", - "#27b08bff", - "#1fd8a4ff", - "#adf0d4ff", - ], - dark_alpha: [ - "#00de4505", - "#27fba60c", - "#02f99920", - "#00ffaa2d", - "#11ffb63b", - "#34ffc24b", - "#45fdc75e", - "#48ffcf75", - "#38feca9d", - "#31fec7ab", - "#21fec0d6", - "#b8ffe1ef", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn green() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Green", - light: [ - "#fbfefcff", - "#f4fbf6ff", - "#e6f6ebff", - "#d6f1dfff", - "#c4e8d1ff", - "#adddc0ff", - "#8eceaaff", - "#5bb98bff", - "#30a46cff", - "#2b9a66ff", - "#218358ff", - "#193b2dff", - ], - light_alpha: [ - "#00c04004", - "#00a32f0b", - "#00a43319", - "#00a83829", - "#019c393b", - "#00963c52", - "#00914071", - "#00924ba4", - "#008f4acf", - "#008647d4", - "#00713fde", - "#002616e6", - ], - dark: [ - "#0e1512ff", - "#121b17ff", - "#132d21ff", - "#113b29ff", - "#174933ff", - "#20573eff", - "#28684aff", - "#2f7c57ff", - "#30a46cff", - "#33b074ff", - "#3dd68cff", - "#b1f1cbff", - ], - dark_alpha: [ - "#00de4505", - "#29f99d0b", - "#22ff991e", - "#11ff992d", - "#2bffa23c", - "#44ffaa4b", - "#50fdac5e", - "#54ffad73", - "#44ffa49e", - "#43fea4ab", - "#46fea5d4", - "#bbffd7f0", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn grass() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Grass", - light: [ - "#fbfefbff", - "#f5fbf5ff", - "#e9f6e9ff", - "#daf1dbff", - "#c9e8caff", - "#b2ddb5ff", - "#94ce9aff", - "#65ba74ff", - "#46a758ff", - "#3e9b4fff", - "#2a7e3bff", - "#203c25ff", - ], - light_alpha: [ - "#00c00004", - "#0099000a", - "#00970016", - "#009f0725", - "#00930536", - "#008f0a4d", - "#018b0f6b", - "#008d199a", - "#008619b9", - "#007b17c1", - "#006514d5", - "#002006df", - ], - dark: [ - "#0e1511ff", - "#141a15ff", - "#1b2a1eff", - "#1d3a24ff", - "#25482dff", - "#2d5736ff", - "#366740ff", - "#3e7949ff", - "#46a758ff", - "#53b365ff", - "#71d083ff", - "#c2f0c2ff", - ], - dark_alpha: [ - "#00de1205", - "#5ef7780a", - "#70fe8c1b", - "#57ff802c", - "#68ff8b3b", - "#71ff8f4b", - "#77fd925d", - "#77fd9070", - "#65ff82a1", - "#72ff8dae", - "#89ff9fcd", - "#ceffceef", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn lime() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Lime", - light: [ - "#fcfdfaff", - "#f8faf3ff", - "#eef6d6ff", - "#e2f0bdff", - "#d3e7a6ff", - "#c2da91ff", - "#abc978ff", - "#8db654ff", - "#bdee63ff", - "#b0e64cff", - "#5c7c2fff", - "#37401cff", - ], - light_alpha: [ - "#66990005", - "#6b95000c", - "#96c80029", - "#8fc60042", - "#81bb0059", - "#72aa006e", - "#61990087", - "#559200ab", - "#93e4009c", - "#8fdc00b3", - "#375f00d0", - "#1e2900e3", - ], - dark: [ - "#11130cff", - "#151a10ff", - "#1f2917ff", - "#29371dff", - "#334423ff", - "#3d522aff", - "#496231ff", - "#577538ff", - "#bdee63ff", - "#d4ff70ff", - "#bde56cff", - "#e3f7baff", - ], - dark_alpha: [ - "#11bb0003", - "#78f7000a", - "#9bfd4c1a", - "#a7fe5c29", - "#affe6537", - "#b2fe6d46", - "#b6ff6f57", - "#b6fd6d6c", - "#caff69ed", - "#d4ff70ff", - "#d1fe77e4", - "#e9febff7", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn mint() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Mint", - light: [ - "#f9fefdff", - "#f2fbf9ff", - "#ddf9f2ff", - "#c8f4e9ff", - "#b3ecdeff", - "#9ce0d0ff", - "#7ecfbdff", - "#4cbba5ff", - "#86ead4ff", - "#7de0cbff", - "#027864ff", - "#16433cff", - ], - light_alpha: [ - "#00d5aa06", - "#00b18a0d", - "#00d29e22", - "#00cc9937", - "#00c0914c", - "#00b08663", - "#00a17d81", - "#009e7fb3", - "#00d3a579", - "#00c39982", - "#007763fd", - "#00312ae9", - ], - dark: [ - "#0e1515ff", - "#0f1b1bff", - "#092c2bff", - "#003a38ff", - "#004744ff", - "#105650ff", - "#1e685fff", - "#277f70ff", - "#86ead4ff", - "#a8f5e5ff", - "#58d5baff", - "#c4f5e1ff", - ], - dark_alpha: [ - "#00dede05", - "#00f9f90b", - "#00fff61d", - "#00fff42c", - "#00fff23a", - "#0effeb4a", - "#34fde55e", - "#41ffdf76", - "#92ffe7e9", - "#aefeedf5", - "#67ffded2", - "#cbfee9f5", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn sky() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Sky", - light: [ - "#f9feffff", - "#f1fafdff", - "#e1f6fdff", - "#d1f0faff", - "#bee7f5ff", - "#a9daedff", - "#8dcae3ff", - "#60b3d7ff", - "#7ce2feff", - "#74daf8ff", - "#00749eff", - "#1d3e56ff", - ], - light_alpha: [ - "#00d5ff06", - "#00a4db0e", - "#00b3ee1e", - "#00ace42e", - "#00a1d841", - "#0092ca56", - "#0089c172", - "#0085bf9f", - "#00c7fe83", - "#00bcf38b", - "#00749eff", - "#002540e2", - ], - dark: [ - "#0d141fff", - "#111a27ff", - "#112840ff", - "#113555ff", - "#154467ff", - "#1b537bff", - "#1f6692ff", - "#197caeff", - "#7ce2feff", - "#a8eeffff", - "#75c7f0ff", - "#c2f3ffff", - ], - dark_alpha: [ - "#0044ff0f", - "#1171fb18", - "#1184fc33", - "#128fff49", - "#1c9dfd5d", - "#28a5ff72", - "#2badfe8b", - "#1db2fea9", - "#7ce3fffe", - "#a8eeffff", - "#7cd3ffef", - "#c2f3ffff", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn black() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Black", - light: [ - "#0000000d", - "#0000001a", - "#00000026", - "#00000033", - "#0000004d", - "#00000066", - "#00000080", - "#00000099", - "#000000b3", - "#000000cc", - "#000000e6", - "#000000f2", - ], - light_alpha: [ - "#0000000d", - "#0000001a", - "#00000026", - "#00000033", - "#0000004d", - "#00000066", - "#00000080", - "#00000099", - "#000000b3", - "#000000cc", - "#000000e6", - "#000000f2", - ], - dark: [ - "#0000000d", - "#0000001a", - "#00000026", - "#00000033", - "#0000004d", - "#00000066", - "#00000080", - "#00000099", - "#000000b3", - "#000000cc", - "#000000e6", - "#000000f2", - ], - dark_alpha: [ - "#0000000d", - "#0000001a", - "#00000026", - "#00000033", - "#0000004d", - "#00000066", - "#00000080", - "#00000099", - "#000000b3", - "#000000cc", - "#000000e6", - "#000000f2", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn white() -> ColorScaleSet { - StaticColorScaleSet { - scale: "White", - light: [ - "#ffffff0d", - "#ffffff1a", - "#ffffff26", - "#ffffff33", - "#ffffff4d", - "#ffffff66", - "#ffffff80", - "#ffffff99", - "#ffffffb3", - "#ffffffcc", - "#ffffffe6", - "#fffffff2", - ], - light_alpha: [ - "#ffffff0d", - "#ffffff1a", - "#ffffff26", - "#ffffff33", - "#ffffff4d", - "#ffffff66", - "#ffffff80", - "#ffffff99", - "#ffffffb3", - "#ffffffcc", - "#ffffffe6", - "#fffffff2", - ], - dark: [ - "#ffffff0d", - "#ffffff1a", - "#ffffff26", - "#ffffff33", - "#ffffff4d", - "#ffffff66", - "#ffffff80", - "#ffffff99", - "#ffffffb3", - "#ffffffcc", - "#ffffffe6", - "#fffffff2", - ], - dark_alpha: [ - "#ffffff0d", - "#ffffff1a", - "#ffffff26", - "#ffffff33", - "#ffffff4d", - "#ffffff66", - "#ffffff80", - "#ffffff99", - "#ffffffb3", - "#ffffffcc", - "#ffffffe6", - "#fffffff2", - ], - } - .try_into() - .unwrap() -} diff --git a/crates/theme/src/fallback_themes.rs b/crates/theme/src/fallback_themes.rs deleted file mode 100644 index 6bfcb1c868..0000000000 --- a/crates/theme/src/fallback_themes.rs +++ /dev/null @@ -1,373 +0,0 @@ -use std::sync::Arc; - -use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla, WindowBackgroundAppearance, hsla}; - -use crate::{ - AccentColors, Appearance, DEFAULT_DARK_THEME, PlayerColors, StatusColors, - StatusColorsRefinement, SyntaxTheme, SystemColors, Theme, ThemeColors, ThemeColorsRefinement, - ThemeFamily, ThemeStyles, default_color_scales, -}; - -/// The default theme family for Zed. -/// -/// This is used to construct the default theme fallback values, as well as to -/// have a theme available at compile time for tests. -pub fn zed_default_themes() -> ThemeFamily { - ThemeFamily { - id: "zed-default".to_string(), - name: "Zed Default".into(), - author: "".into(), - themes: vec![zed_default_dark()], - scales: default_color_scales(), - } -} - -// If a theme customizes a foreground version of a status color, but does not -// customize the background color, then use a partly-transparent version of the -// foreground color for the background color. -pub(crate) fn apply_status_color_defaults(status: &mut StatusColorsRefinement) { - for (fg_color, bg_color) in [ - (&status.deleted, &mut status.deleted_background), - (&status.created, &mut status.created_background), - (&status.modified, &mut status.modified_background), - (&status.conflict, &mut status.conflict_background), - (&status.error, &mut status.error_background), - (&status.hidden, &mut status.hidden_background), - ] { - if bg_color.is_none() - && let Some(fg_color) = fg_color - { - *bg_color = Some(fg_color.opacity(0.25)); - } - } -} - -pub(crate) fn apply_theme_color_defaults( - theme_colors: &mut ThemeColorsRefinement, - player_colors: &PlayerColors, -) { - if theme_colors.element_selection_background.is_none() { - let mut selection = player_colors.local().selection; - if selection.a == 1.0 { - selection.a = 0.25; - } - theme_colors.element_selection_background = Some(selection); - } -} - -pub(crate) fn zed_default_dark() -> Theme { - let bg = hsla(215. / 360., 12. / 100., 15. / 100., 1.); - let editor = hsla(220. / 360., 12. / 100., 18. / 100., 1.); - let elevated_surface = hsla(225. / 360., 12. / 100., 17. / 100., 1.); - let hover = hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0); - - let blue = hsla(207.8 / 360., 81. / 100., 66. / 100., 1.0); - let gray = hsla(218.8 / 360., 10. / 100., 40. / 100., 1.0); - let green = hsla(95. / 360., 38. / 100., 62. / 100., 1.0); - let orange = hsla(29. / 360., 54. / 100., 61. / 100., 1.0); - let purple = hsla(286. / 360., 51. / 100., 64. / 100., 1.0); - let red = hsla(355. / 360., 65. / 100., 65. / 100., 1.0); - let teal = hsla(187. / 360., 47. / 100., 55. / 100., 1.0); - let yellow = hsla(39. / 360., 67. / 100., 69. / 100., 1.0); - - const ADDED_COLOR: Hsla = Hsla { - h: 134. / 360., - s: 0.55, - l: 0.40, - a: 1.0, - }; - const WORD_ADDED_COLOR: Hsla = Hsla { - h: 134. / 360., - s: 0.55, - l: 0.40, - a: 0.35, - }; - const MODIFIED_COLOR: Hsla = Hsla { - h: 48. / 360., - s: 0.76, - l: 0.47, - a: 1.0, - }; - const REMOVED_COLOR: Hsla = Hsla { - h: 350. / 360., - s: 0.88, - l: 0.25, - a: 1.0, - }; - const WORD_DELETED_COLOR: Hsla = Hsla { - h: 350. / 360., - s: 0.88, - l: 0.25, - a: 0.80, - }; - - let player = PlayerColors::dark(); - Theme { - id: "one_dark".to_string(), - name: DEFAULT_DARK_THEME.into(), - appearance: Appearance::Dark, - styles: ThemeStyles { - window_background_appearance: WindowBackgroundAppearance::Opaque, - system: SystemColors::default(), - accents: AccentColors(vec![blue, orange, purple, teal, red, green, yellow]), - colors: ThemeColors { - border: hsla(225. / 360., 13. / 100., 12. / 100., 1.), - border_variant: hsla(228. / 360., 8. / 100., 25. / 100., 1.), - border_focused: hsla(223. / 360., 78. / 100., 65. / 100., 1.), - border_selected: hsla(222.6 / 360., 77.5 / 100., 65.1 / 100., 1.0), - border_transparent: SystemColors::default().transparent, - border_disabled: hsla(222.0 / 360., 11.6 / 100., 33.7 / 100., 1.0), - elevated_surface_background: elevated_surface, - surface_background: bg, - background: bg, - element_background: hsla(223.0 / 360., 13. / 100., 21. / 100., 1.0), - element_hover: hover, - element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0), - element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), - element_disabled: SystemColors::default().transparent, - element_selection_background: player.local().selection.alpha(0.25), - drop_target_background: hsla(220.0 / 360., 8.3 / 100., 21.4 / 100., 1.0), - drop_target_border: hsla(221. / 360., 11. / 100., 86. / 100., 1.0), - ghost_element_background: SystemColors::default().transparent, - ghost_element_hover: hover, - ghost_element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0), - ghost_element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), - ghost_element_disabled: SystemColors::default().transparent, - text: hsla(221. / 360., 11. / 100., 86. / 100., 1.0), - text_muted: hsla(218.0 / 360., 7. / 100., 46. / 100., 1.0), - text_placeholder: hsla(220.0 / 360., 6.6 / 100., 44.5 / 100., 1.0), - text_disabled: hsla(220.0 / 360., 6.6 / 100., 44.5 / 100., 1.0), - text_accent: hsla(222.6 / 360., 77.5 / 100., 65.1 / 100., 1.0), - icon: hsla(222.9 / 360., 9.9 / 100., 86.1 / 100., 1.0), - icon_muted: hsla(220.0 / 360., 12.1 / 100., 66.1 / 100., 1.0), - icon_disabled: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0), - icon_placeholder: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0), - icon_accent: blue, - debugger_accent: red, - status_bar_background: bg, - title_bar_background: bg, - title_bar_inactive_background: bg, - toolbar_background: editor, - tab_bar_background: bg, - tab_inactive_background: bg, - tab_active_background: editor, - search_match_background: bg, - search_active_match_background: bg, - - editor_background: editor, - editor_gutter_background: editor, - editor_subheader_background: bg, - editor_active_line_background: hsla(222.9 / 360., 13.5 / 100., 20.4 / 100., 1.0), - editor_highlighted_line_background: hsla(207.8 / 360., 81. / 100., 66. / 100., 0.1), - editor_debugger_active_line_background: hsla( - 207.8 / 360., - 81. / 100., - 66. / 100., - 0.2, - ), - editor_line_number: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0), - editor_active_line_number: hsla(216.0 / 360., 5.9 / 100., 49.6 / 100., 1.0), - editor_hover_line_number: hsla(216.0 / 360., 5.9 / 100., 56.7 / 100., 1.0), - editor_invisible: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0), - editor_wrap_guide: hsla(228. / 360., 8. / 100., 25. / 100., 1.), - editor_active_wrap_guide: hsla(228. / 360., 8. / 100., 25. / 100., 1.), - editor_indent_guide: hsla(228. / 360., 8. / 100., 25. / 100., 1.), - editor_indent_guide_active: hsla(225. / 360., 13. / 100., 12. / 100., 1.), - editor_document_highlight_read_background: hsla( - 207.8 / 360., - 81. / 100., - 66. / 100., - 0.2, - ), - editor_document_highlight_write_background: gpui::red(), - editor_document_highlight_bracket_background: gpui::green(), - - terminal_background: bg, - // todo("Use one colors for terminal") - terminal_ansi_background: crate::black().dark().step_12(), - terminal_foreground: crate::white().dark().step_12(), - terminal_bright_foreground: crate::white().dark().step_11(), - terminal_dim_foreground: crate::white().dark().step_10(), - terminal_ansi_black: crate::black().dark().step_12(), - terminal_ansi_red: crate::red().dark().step_11(), - terminal_ansi_green: crate::green().dark().step_11(), - terminal_ansi_yellow: crate::yellow().dark().step_11(), - terminal_ansi_blue: crate::blue().dark().step_11(), - terminal_ansi_magenta: crate::violet().dark().step_11(), - terminal_ansi_cyan: crate::cyan().dark().step_11(), - terminal_ansi_white: crate::neutral().dark().step_12(), - terminal_ansi_bright_black: crate::black().dark().step_11(), - terminal_ansi_bright_red: crate::red().dark().step_10(), - terminal_ansi_bright_green: crate::green().dark().step_10(), - terminal_ansi_bright_yellow: crate::yellow().dark().step_10(), - terminal_ansi_bright_blue: crate::blue().dark().step_10(), - terminal_ansi_bright_magenta: crate::violet().dark().step_10(), - terminal_ansi_bright_cyan: crate::cyan().dark().step_10(), - terminal_ansi_bright_white: crate::neutral().dark().step_11(), - terminal_ansi_dim_black: crate::black().dark().step_10(), - terminal_ansi_dim_red: crate::red().dark().step_9(), - terminal_ansi_dim_green: crate::green().dark().step_9(), - terminal_ansi_dim_yellow: crate::yellow().dark().step_9(), - terminal_ansi_dim_blue: crate::blue().dark().step_9(), - terminal_ansi_dim_magenta: crate::violet().dark().step_9(), - terminal_ansi_dim_cyan: crate::cyan().dark().step_9(), - terminal_ansi_dim_white: crate::neutral().dark().step_10(), - panel_background: bg, - panel_focused_border: blue, - panel_indent_guide: hsla(228. / 360., 8. / 100., 25. / 100., 1.), - panel_indent_guide_hover: hsla(225. / 360., 13. / 100., 12. / 100., 1.), - panel_indent_guide_active: hsla(225. / 360., 13. / 100., 12. / 100., 1.), - panel_overlay_background: bg, - panel_overlay_hover: hover, - pane_focused_border: blue, - pane_group_border: hsla(225. / 360., 13. / 100., 12. / 100., 1.), - scrollbar_thumb_background: gpui::transparent_black(), - scrollbar_thumb_hover_background: hover, - scrollbar_thumb_active_background: hsla( - 225.0 / 360., - 11.8 / 100., - 26.7 / 100., - 1.0, - ), - scrollbar_thumb_border: hsla(228. / 360., 8. / 100., 25. / 100., 1.), - scrollbar_track_background: gpui::transparent_black(), - scrollbar_track_border: hsla(228. / 360., 8. / 100., 25. / 100., 1.), - minimap_thumb_background: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 0.7), - minimap_thumb_hover_background: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 0.7), - minimap_thumb_active_background: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 0.7), - minimap_thumb_border: hsla(228. / 360., 8. / 100., 25. / 100., 1.), - editor_foreground: hsla(218. / 360., 14. / 100., 71. / 100., 1.), - link_text_hover: blue, - version_control_added: ADDED_COLOR, - version_control_deleted: REMOVED_COLOR, - version_control_modified: MODIFIED_COLOR, - version_control_renamed: MODIFIED_COLOR, - version_control_conflict: crate::orange().light().step_12(), - version_control_ignored: crate::gray().light().step_12(), - version_control_word_added: WORD_ADDED_COLOR, - version_control_word_deleted: WORD_DELETED_COLOR, - version_control_conflict_marker_ours: crate::green().light().step_12().alpha(0.5), - version_control_conflict_marker_theirs: crate::blue().light().step_12().alpha(0.5), - - vim_normal_background: SystemColors::default().transparent, - vim_insert_background: SystemColors::default().transparent, - vim_replace_background: SystemColors::default().transparent, - vim_visual_background: SystemColors::default().transparent, - vim_visual_line_background: SystemColors::default().transparent, - vim_visual_block_background: SystemColors::default().transparent, - vim_helix_normal_background: SystemColors::default().transparent, - vim_helix_select_background: SystemColors::default().transparent, - vim_mode_text: SystemColors::default().transparent, - }, - status: StatusColors { - conflict: yellow, - conflict_background: yellow, - conflict_border: yellow, - created: green, - created_background: green, - created_border: green, - deleted: red, - deleted_background: red, - deleted_border: red, - error: red, - error_background: red, - error_border: red, - hidden: gray, - hidden_background: gray, - hidden_border: gray, - hint: blue, - hint_background: blue, - hint_border: blue, - ignored: gray, - ignored_background: gray, - ignored_border: gray, - info: blue, - info_background: blue, - info_border: blue, - modified: yellow, - modified_background: yellow, - modified_border: yellow, - predictive: gray, - predictive_background: gray, - predictive_border: gray, - renamed: blue, - renamed_background: blue, - renamed_border: blue, - success: green, - success_background: green, - success_border: green, - unreachable: gray, - unreachable_background: gray, - unreachable_border: gray, - warning: yellow, - warning_background: yellow, - warning_border: yellow, - }, - player, - syntax: Arc::new(SyntaxTheme { - highlights: vec![ - ("attribute".into(), purple.into()), - ("boolean".into(), orange.into()), - ("comment".into(), gray.into()), - ("comment.doc".into(), gray.into()), - ("constant".into(), yellow.into()), - ("constructor".into(), blue.into()), - ("embedded".into(), HighlightStyle::default()), - ( - "emphasis".into(), - HighlightStyle { - font_style: Some(FontStyle::Italic), - ..HighlightStyle::default() - }, - ), - ( - "emphasis.strong".into(), - HighlightStyle { - font_weight: Some(FontWeight::BOLD), - ..HighlightStyle::default() - }, - ), - ("enum".into(), HighlightStyle::default()), - ("function".into(), blue.into()), - ("function.method".into(), blue.into()), - ("function.definition".into(), blue.into()), - ("hint".into(), blue.into()), - ("keyword".into(), purple.into()), - ("label".into(), HighlightStyle::default()), - ("link_text".into(), blue.into()), - ( - "link_uri".into(), - HighlightStyle { - color: Some(teal), - font_style: Some(FontStyle::Italic), - ..HighlightStyle::default() - }, - ), - ("number".into(), orange.into()), - ("operator".into(), HighlightStyle::default()), - ("predictive".into(), HighlightStyle::default()), - ("preproc".into(), HighlightStyle::default()), - ("primary".into(), HighlightStyle::default()), - ("property".into(), red.into()), - ("punctuation".into(), HighlightStyle::default()), - ("punctuation.bracket".into(), HighlightStyle::default()), - ("punctuation.delimiter".into(), HighlightStyle::default()), - ("punctuation.list_marker".into(), HighlightStyle::default()), - ("punctuation.special".into(), HighlightStyle::default()), - ("string".into(), green.into()), - ("string.escape".into(), HighlightStyle::default()), - ("string.regex".into(), red.into()), - ("string.special".into(), HighlightStyle::default()), - ("string.special.symbol".into(), HighlightStyle::default()), - ("tag".into(), HighlightStyle::default()), - ("text.literal".into(), HighlightStyle::default()), - ("title".into(), HighlightStyle::default()), - ("type".into(), teal.into()), - ("variable".into(), HighlightStyle::default()), - ("variable.special".into(), red.into()), - ("variant".into(), HighlightStyle::default()), - ], - }), - }, - } -} diff --git a/crates/theme/src/font_family_cache.rs b/crates/theme/src/font_family_cache.rs deleted file mode 100644 index 411cf9b4d4..0000000000 --- a/crates/theme/src/font_family_cache.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::sync::Arc; -use std::time::Instant; - -use gpui::{App, Global, ReadGlobal, SharedString}; -use parking_lot::RwLock; - -#[derive(Default)] -struct FontFamilyCacheState { - loaded_at: Option, - font_families: Vec, -} - -/// A cache for the list of font families. -/// -/// Listing the available font families from the text system is expensive, -/// so we do it once and then use the cached values each render. -#[derive(Default)] -pub struct FontFamilyCache { - state: Arc>, -} - -#[derive(Default)] -struct GlobalFontFamilyCache(Arc); - -impl Global for GlobalFontFamilyCache {} - -impl FontFamilyCache { - /// Initializes the global font family cache. - pub fn init_global(cx: &mut App) { - cx.default_global::(); - } - - /// Returns the global font family cache. - pub fn global(cx: &App) -> Arc { - GlobalFontFamilyCache::global(cx).0.clone() - } - - /// Returns the list of font families. - pub fn list_font_families(&self, cx: &App) -> Vec { - if self.state.read().loaded_at.is_some() { - return self.state.read().font_families.clone(); - } - - let mut lock = self.state.write(); - lock.font_families = cx - .text_system() - .all_font_names() - .into_iter() - .map(SharedString::from) - .collect(); - lock.loaded_at = Some(Instant::now()); - - lock.font_families.clone() - } - - /// Returns the list of font families if they have been loaded - pub fn try_list_font_families(&self) -> Option> { - self.state - .try_read() - .filter(|state| state.loaded_at.is_some()) - .map(|state| state.font_families.clone()) - } - - /// Prefetch all font names in the background - pub async fn prefetch(&self, cx: &gpui::AsyncApp) { - if self - .state - .try_read() - .is_none_or(|state| state.loaded_at.is_some()) - { - return; - } - - let Ok(text_system) = cx.update(|cx| App::text_system(cx).clone()) else { - return; - }; - - let state = self.state.clone(); - - cx.background_executor() - .spawn(async move { - // We take this lock in the background executor to ensure that synchronous calls to `list_font_families` are blocked while we are prefetching, - // while not blocking the main thread and risking deadlocks - let mut lock = state.write(); - let all_font_names = text_system - .all_font_names() - .into_iter() - .map(SharedString::from) - .collect(); - lock.font_families = all_font_names; - lock.loaded_at = Some(Instant::now()); - }) - .await; - } -} diff --git a/crates/theme/src/icon_theme.rs b/crates/theme/src/icon_theme.rs deleted file mode 100644 index 818bf1b2f1..0000000000 --- a/crates/theme/src/icon_theme.rs +++ /dev/null @@ -1,422 +0,0 @@ -use std::sync::{Arc, LazyLock}; - -use collections::HashMap; -use gpui::SharedString; - -use crate::Appearance; - -/// A family of icon themes. -pub struct IconThemeFamily { - /// The unique ID for the icon theme family. - pub id: String, - /// The name of the icon theme family. - pub name: SharedString, - /// The author of the icon theme family. - pub author: SharedString, - /// The list of icon themes in the family. - pub themes: Vec, -} - -/// An icon theme. -#[derive(Debug, PartialEq)] -pub struct IconTheme { - /// The unique ID for the icon theme. - pub id: String, - /// The name of the icon theme. - pub name: SharedString, - /// The appearance of the icon theme (e.g., light or dark). - pub appearance: Appearance, - /// The icons used for directories. - pub directory_icons: DirectoryIcons, - /// The icons used for named directories. - pub named_directory_icons: HashMap, - /// The icons used for chevrons. - pub chevron_icons: ChevronIcons, - /// The mapping of file stems to their associated icon keys. - pub file_stems: HashMap, - /// The mapping of file suffixes to their associated icon keys. - pub file_suffixes: HashMap, - /// The mapping of icon keys to icon definitions. - pub file_icons: HashMap, -} - -/// The icons used for directories. -#[derive(Debug, PartialEq, Clone)] -pub struct DirectoryIcons { - /// The path to the icon to use for a collapsed directory. - pub collapsed: Option, - /// The path to the icon to use for an expanded directory. - pub expanded: Option, -} - -/// The icons used for chevrons. -#[derive(Debug, PartialEq)] -pub struct ChevronIcons { - /// The path to the icon to use for a collapsed chevron. - pub collapsed: Option, - /// The path to the icon to use for an expanded chevron. - pub expanded: Option, -} - -/// An icon definition. -#[derive(Debug, PartialEq)] -pub struct IconDefinition { - /// The path to the icon file. - pub path: SharedString, -} - -const FILE_STEMS_BY_ICON_KEY: &[(&str, &[&str])] = &[ - ("docker", &["Dockerfile"]), - ("ruby", &["Podfile"]), - ("heroku", &["Procfile"]), -]; - -const FILE_SUFFIXES_BY_ICON_KEY: &[(&str, &[&str])] = &[ - ("astro", &["astro"]), - ( - "audio", - &[ - "aac", "flac", "m4a", "mka", "mp3", "ogg", "opus", "wav", "wma", "wv", - ], - ), - ("backup", &["bak"]), - ("bicep", &["bicep"]), - ("bun", &["lockb"]), - ("c", &["c", "h"]), - ("cairo", &["cairo"]), - ("code", &["handlebars", "metadata", "rkt", "scm"]), - ("coffeescript", &["coffee"]), - ( - "cpp", - &[ - "c++", "h++", "cc", "cpp", "cxx", "hh", "hpp", "hxx", "inl", "ixx", - ], - ), - ("crystal", &["cr", "ecr"]), - ("csharp", &["cs"]), - ("csproj", &["csproj"]), - ("css", &["css", "pcss", "postcss"]), - ("cue", &["cue"]), - ("dart", &["dart"]), - ("diff", &["diff"]), - ( - "document", - &[ - "doc", "docx", "mdx", "odp", "ods", "odt", "pdf", "ppt", "pptx", "rtf", "txt", "xls", - "xlsx", - ], - ), - ("elixir", &["eex", "ex", "exs", "heex"]), - ("elm", &["elm"]), - ( - "erlang", - &[ - "Emakefile", - "app.src", - "erl", - "escript", - "hrl", - "rebar.config", - "xrl", - "yrl", - ], - ), - ( - "eslint", - &[ - "eslint.config.cjs", - "eslint.config.cts", - "eslint.config.js", - "eslint.config.mjs", - "eslint.config.mts", - "eslint.config.ts", - "eslintrc", - "eslintrc.js", - "eslintrc.json", - ], - ), - ("font", &["otf", "ttf", "woff", "woff2"]), - ("fsharp", &["fs"]), - ("fsproj", &["fsproj"]), - ("gitlab", &["gitlab-ci.yml"]), - ("gleam", &["gleam"]), - ("go", &["go", "mod", "work"]), - ("graphql", &["gql", "graphql", "graphqls"]), - ("haskell", &["hs"]), - ("hcl", &["hcl"]), - ("html", &["htm", "html"]), - ( - "image", - &[ - "avif", "bmp", "gif", "heic", "heif", "ico", "j2k", "jfif", "jp2", "jpeg", "jpg", - "jxl", "png", "psd", "qoi", "svg", "tiff", "webp", - ], - ), - ("java", &["java"]), - ("javascript", &["cjs", "js", "mjs"]), - ("json", &["json", "jsonc"]), - ("julia", &["jl"]), - ("kdl", &["kdl"]), - ("kotlin", &["kt"]), - ("lock", &["lock"]), - ("log", &["log"]), - ("lua", &["lua"]), - ("luau", &["luau"]), - ("markdown", &["markdown", "md"]), - ("metal", &["metal"]), - ("nim", &["nim"]), - ("nix", &["nix"]), - ("ocaml", &["ml", "mli"]), - ("odin", &["odin"]), - ("php", &["php"]), - ( - "prettier", - &[ - "prettier.config.cjs", - "prettier.config.js", - "prettier.config.mjs", - "prettierignore", - "prettierrc", - "prettierrc.cjs", - "prettierrc.js", - "prettierrc.json", - "prettierrc.json5", - "prettierrc.mjs", - "prettierrc.toml", - "prettierrc.yaml", - "prettierrc.yml", - ], - ), - ("prisma", &["prisma"]), - ("puppet", &["pp"]), - ("python", &["py"]), - ("r", &["r", "R"]), - ("react", &["cjsx", "ctsx", "jsx", "mjsx", "mtsx", "tsx"]), - ("roc", &["roc"]), - ("ruby", &["rb"]), - ("rust", &["rs"]), - ("sass", &["sass", "scss"]), - ("scala", &["scala", "sc"]), - ("settings", &["conf", "ini", "yaml", "yml"]), - ("solidity", &["sol"]), - ( - "storage", - &[ - "accdb", "csv", "dat", "db", "dbf", "dll", "fmp", "fp7", "frm", "gdb", "ib", "ldf", - "mdb", "mdf", "myd", "myi", "pdb", "RData", "rdata", "sav", "sdf", "sql", "sqlite", - "tsv", - ], - ), - ( - "stylelint", - &[ - "stylelint.config.cjs", - "stylelint.config.js", - "stylelint.config.mjs", - "stylelintignore", - "stylelintrc", - "stylelintrc.cjs", - "stylelintrc.js", - "stylelintrc.json", - "stylelintrc.mjs", - "stylelintrc.yaml", - "stylelintrc.yml", - ], - ), - ("surrealql", &["surql"]), - ("svelte", &["svelte"]), - ("swift", &["swift"]), - ("tcl", &["tcl"]), - ("template", &["hbs", "plist", "xml"]), - ( - "terminal", - &[ - "bash", - "bash_aliases", - "bash_login", - "bash_logout", - "bash_profile", - "bashrc", - "fish", - "nu", - "profile", - "ps1", - "sh", - "zlogin", - "zlogout", - "zprofile", - "zsh", - "zsh_aliases", - "zsh_histfile", - "zsh_history", - "zshenv", - "zshrc", - ], - ), - ("terraform", &["tf", "tfvars"]), - ("toml", &["toml"]), - ("typescript", &["cts", "mts", "ts"]), - ("v", &["v", "vsh", "vv"]), - ( - "vcs", - &[ - "COMMIT_EDITMSG", - "EDIT_DESCRIPTION", - "MERGE_MSG", - "NOTES_EDITMSG", - "TAG_EDITMSG", - "gitattributes", - "gitignore", - "gitkeep", - "gitmodules", - ], - ), - ("vbproj", &["vbproj"]), - ("video", &["avi", "m4v", "mkv", "mov", "mp4", "webm", "wmv"]), - ("vs_sln", &["sln"]), - ("vs_suo", &["suo"]), - ("vue", &["vue"]), - ("vyper", &["vy", "vyi"]), - ("wgsl", &["wgsl"]), - ("zig", &["zig"]), -]; - -/// A mapping of a file type identifier to its corresponding icon. -const FILE_ICONS: &[(&str, &str)] = &[ - ("astro", "icons/file_icons/astro.svg"), - ("audio", "icons/file_icons/audio.svg"), - ("bicep", "icons/file_icons/file.svg"), - ("bun", "icons/file_icons/bun.svg"), - ("c", "icons/file_icons/c.svg"), - ("cairo", "icons/file_icons/cairo.svg"), - ("code", "icons/file_icons/code.svg"), - ("coffeescript", "icons/file_icons/coffeescript.svg"), - ("cpp", "icons/file_icons/cpp.svg"), - ("crystal", "icons/file_icons/file.svg"), - ("csharp", "icons/file_icons/file.svg"), - ("csproj", "icons/file_icons/file.svg"), - ("css", "icons/file_icons/css.svg"), - ("cue", "icons/file_icons/file.svg"), - ("dart", "icons/file_icons/dart.svg"), - ("default", "icons/file_icons/file.svg"), - ("diff", "icons/file_icons/diff.svg"), - ("docker", "icons/file_icons/docker.svg"), - ("document", "icons/file_icons/book.svg"), - ("elixir", "icons/file_icons/elixir.svg"), - ("elm", "icons/file_icons/elm.svg"), - ("erlang", "icons/file_icons/erlang.svg"), - ("eslint", "icons/file_icons/eslint.svg"), - ("font", "icons/file_icons/font.svg"), - ("fsharp", "icons/file_icons/fsharp.svg"), - ("fsproj", "icons/file_icons/file.svg"), - ("gitlab", "icons/file_icons/settings.svg"), - ("gleam", "icons/file_icons/gleam.svg"), - ("go", "icons/file_icons/go.svg"), - ("graphql", "icons/file_icons/graphql.svg"), - ("haskell", "icons/file_icons/haskell.svg"), - ("hcl", "icons/file_icons/hcl.svg"), - ("heroku", "icons/file_icons/heroku.svg"), - ("html", "icons/file_icons/html.svg"), - ("image", "icons/file_icons/image.svg"), - ("java", "icons/file_icons/java.svg"), - ("javascript", "icons/file_icons/javascript.svg"), - ("json", "icons/file_icons/code.svg"), - ("julia", "icons/file_icons/julia.svg"), - ("kdl", "icons/file_icons/kdl.svg"), - ("kotlin", "icons/file_icons/kotlin.svg"), - ("lock", "icons/file_icons/lock.svg"), - ("log", "icons/file_icons/info.svg"), - ("lua", "icons/file_icons/lua.svg"), - ("luau", "icons/file_icons/luau.svg"), - ("markdown", "icons/file_icons/book.svg"), - ("metal", "icons/file_icons/metal.svg"), - ("nim", "icons/file_icons/nim.svg"), - ("nix", "icons/file_icons/nix.svg"), - ("ocaml", "icons/file_icons/ocaml.svg"), - ("odin", "icons/file_icons/odin.svg"), - ("phoenix", "icons/file_icons/phoenix.svg"), - ("php", "icons/file_icons/php.svg"), - ("prettier", "icons/file_icons/prettier.svg"), - ("prisma", "icons/file_icons/prisma.svg"), - ("puppet", "icons/file_icons/puppet.svg"), - ("python", "icons/file_icons/python.svg"), - ("r", "icons/file_icons/r.svg"), - ("react", "icons/file_icons/react.svg"), - ("roc", "icons/file_icons/roc.svg"), - ("ruby", "icons/file_icons/ruby.svg"), - ("rust", "icons/file_icons/rust.svg"), - ("sass", "icons/file_icons/sass.svg"), - ("scala", "icons/file_icons/scala.svg"), - ("settings", "icons/file_icons/settings.svg"), - ("solidity", "icons/file_icons/file.svg"), - ("storage", "icons/file_icons/database.svg"), - ("stylelint", "icons/file_icons/javascript.svg"), - ("surrealql", "icons/file_icons/surrealql.svg"), - ("svelte", "icons/file_icons/html.svg"), - ("swift", "icons/file_icons/swift.svg"), - ("tcl", "icons/file_icons/tcl.svg"), - ("template", "icons/file_icons/html.svg"), - ("terminal", "icons/file_icons/terminal.svg"), - ("terraform", "icons/file_icons/terraform.svg"), - ("toml", "icons/file_icons/toml.svg"), - ("typescript", "icons/file_icons/typescript.svg"), - ("v", "icons/file_icons/v.svg"), - ("vbproj", "icons/file_icons/file.svg"), - ("vcs", "icons/file_icons/git.svg"), - ("video", "icons/file_icons/video.svg"), - ("vs_sln", "icons/file_icons/file.svg"), - ("vs_suo", "icons/file_icons/file.svg"), - ("vue", "icons/file_icons/vue.svg"), - ("vyper", "icons/file_icons/vyper.svg"), - ("wgsl", "icons/file_icons/wgsl.svg"), - ("zig", "icons/file_icons/zig.svg"), -]; - -/// Returns a mapping of file associations to icon keys. -fn icon_keys_by_association( - associations_by_icon_key: &[(&str, &[&str])], -) -> HashMap { - let mut icon_keys_by_association = HashMap::default(); - for (icon_key, associations) in associations_by_icon_key { - for association in *associations { - icon_keys_by_association.insert(association.to_string(), icon_key.to_string()); - } - } - - icon_keys_by_association -} - -/// The name of the default icon theme. -pub(crate) const DEFAULT_ICON_THEME_NAME: &str = "Zed (Default)"; - -static DEFAULT_ICON_THEME: LazyLock> = LazyLock::new(|| { - Arc::new(IconTheme { - id: "zed".into(), - name: DEFAULT_ICON_THEME_NAME.into(), - appearance: Appearance::Dark, - directory_icons: DirectoryIcons { - collapsed: Some("icons/file_icons/folder.svg".into()), - expanded: Some("icons/file_icons/folder_open.svg".into()), - }, - named_directory_icons: HashMap::default(), - chevron_icons: ChevronIcons { - collapsed: Some("icons/file_icons/chevron_right.svg".into()), - expanded: Some("icons/file_icons/chevron_down.svg".into()), - }, - file_stems: icon_keys_by_association(FILE_STEMS_BY_ICON_KEY), - file_suffixes: icon_keys_by_association(FILE_SUFFIXES_BY_ICON_KEY), - file_icons: HashMap::from_iter(FILE_ICONS.iter().map(|(ty, path)| { - ( - ty.to_string(), - IconDefinition { - path: (*path).into(), - }, - ) - })), - }) -}); - -/// Returns the default icon theme. -pub fn default_icon_theme() -> Arc { - DEFAULT_ICON_THEME.clone() -} diff --git a/crates/theme/src/icon_theme_schema.rs b/crates/theme/src/icon_theme_schema.rs deleted file mode 100644 index 45ac985ae9..0000000000 --- a/crates/theme/src/icon_theme_schema.rs +++ /dev/null @@ -1,50 +0,0 @@ -#![allow(missing_docs)] - -use gpui::SharedString; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -use crate::AppearanceContent; - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct IconThemeFamilyContent { - pub name: String, - pub author: String, - pub themes: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct IconThemeContent { - pub name: String, - pub appearance: AppearanceContent, - #[serde(default)] - pub directory_icons: DirectoryIconsContent, - #[serde(default)] - pub named_directory_icons: HashMap, - #[serde(default)] - pub chevron_icons: ChevronIconsContent, - #[serde(default)] - pub file_stems: HashMap, - #[serde(default)] - pub file_suffixes: HashMap, - #[serde(default)] - pub file_icons: HashMap, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] -pub struct DirectoryIconsContent { - pub collapsed: Option, - pub expanded: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] -pub struct ChevronIconsContent { - pub collapsed: Option, - pub expanded: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct IconDefinitionContent { - pub path: SharedString, -} diff --git a/crates/theme/src/registry.rs b/crates/theme/src/registry.rs deleted file mode 100644 index c362b62704..0000000000 --- a/crates/theme/src/registry.rs +++ /dev/null @@ -1,364 +0,0 @@ -use std::sync::Arc; -use std::{fmt::Debug, path::Path}; - -use anyhow::{Context as _, Result}; -use collections::HashMap; -use derive_more::{Deref, DerefMut}; -use fs::Fs; -use futures::StreamExt; -use gpui::{App, AssetSource, Global, SharedString}; -use parking_lot::RwLock; -use thiserror::Error; -use util::ResultExt; - -use crate::{ - Appearance, AppearanceContent, ChevronIcons, DEFAULT_ICON_THEME_NAME, DirectoryIcons, - IconDefinition, IconTheme, Theme, ThemeFamily, ThemeFamilyContent, default_icon_theme, - read_icon_theme, read_user_theme, refine_theme_family, -}; - -/// The metadata for a theme. -#[derive(Debug, Clone)] -pub struct ThemeMeta { - /// The name of the theme. - pub name: SharedString, - /// The appearance of the theme. - pub appearance: Appearance, -} - -/// An error indicating that the theme with the given name was not found. -#[derive(Debug, Error, Clone)] -#[error("theme not found: {0}")] -pub struct ThemeNotFoundError(pub SharedString); - -/// An error indicating that the icon theme with the given name was not found. -#[derive(Debug, Error, Clone)] -#[error("icon theme not found: {0}")] -pub struct IconThemeNotFoundError(pub SharedString); - -/// The global [`ThemeRegistry`]. -/// -/// This newtype exists for obtaining a unique [`TypeId`](std::any::TypeId) when -/// inserting the [`ThemeRegistry`] into the context as a global. -/// -/// This should not be exposed outside of this module. -#[derive(Default, Deref, DerefMut)] -struct GlobalThemeRegistry(Arc); - -impl Global for GlobalThemeRegistry {} - -struct ThemeRegistryState { - themes: HashMap>, - icon_themes: HashMap>, - /// Whether the extensions have been loaded yet. - extensions_loaded: bool, -} - -/// The registry for themes. -pub struct ThemeRegistry { - state: RwLock, - assets: Box, -} - -impl ThemeRegistry { - /// Returns the global [`ThemeRegistry`]. - pub fn global(cx: &App) -> Arc { - cx.global::().0.clone() - } - - /// Returns the global [`ThemeRegistry`]. - /// - /// Inserts a default [`ThemeRegistry`] if one does not yet exist. - pub fn default_global(cx: &mut App) -> Arc { - cx.default_global::().0.clone() - } - - /// Returns the global [`ThemeRegistry`] if it exists. - pub fn try_global(cx: &mut App) -> Option> { - cx.try_global::().map(|t| t.0.clone()) - } - - /// Sets the global [`ThemeRegistry`]. - pub(crate) fn set_global(assets: Box, cx: &mut App) { - cx.set_global(GlobalThemeRegistry(Arc::new(ThemeRegistry::new(assets)))); - } - - /// Creates a new [`ThemeRegistry`] with the given [`AssetSource`]. - pub fn new(assets: Box) -> Self { - let registry = Self { - state: RwLock::new(ThemeRegistryState { - themes: HashMap::default(), - icon_themes: HashMap::default(), - extensions_loaded: false, - }), - assets, - }; - - // We're loading the Zed default theme, as we need a theme to be loaded - // for tests. - registry.insert_theme_families([crate::fallback_themes::zed_default_themes()]); - - let default_icon_theme = crate::default_icon_theme(); - registry - .state - .write() - .icon_themes - .insert(default_icon_theme.name.clone(), default_icon_theme); - - registry - } - - /// Returns whether the extensions have been loaded. - pub fn extensions_loaded(&self) -> bool { - self.state.read().extensions_loaded - } - - /// Sets the flag indicating that the extensions have loaded. - pub fn set_extensions_loaded(&self) { - self.state.write().extensions_loaded = true; - } - - fn insert_theme_families(&self, families: impl IntoIterator) { - for family in families.into_iter() { - self.insert_themes(family.themes); - } - } - - fn insert_themes(&self, themes: impl IntoIterator) { - let mut state = self.state.write(); - for theme in themes.into_iter() { - state.themes.insert(theme.name.clone(), Arc::new(theme)); - } - } - - #[allow(unused)] - fn insert_user_theme_families(&self, families: impl IntoIterator) { - for family in families.into_iter() { - let refined_family = refine_theme_family(family); - - self.insert_themes(refined_family.themes); - } - } - - /// Removes the themes with the given names from the registry. - pub fn remove_user_themes(&self, themes_to_remove: &[SharedString]) { - self.state - .write() - .themes - .retain(|name, _| !themes_to_remove.contains(name)) - } - - /// Removes all themes from the registry. - pub fn clear(&self) { - self.state.write().themes.clear(); - } - - /// Returns the names of all themes in the registry. - pub fn list_names(&self) -> Vec { - let mut names = self.state.read().themes.keys().cloned().collect::>(); - names.sort(); - names - } - - /// Returns the metadata of all themes in the registry. - pub fn list(&self) -> Vec { - self.state - .read() - .themes - .values() - .map(|theme| ThemeMeta { - name: theme.name.clone(), - appearance: theme.appearance(), - }) - .collect() - } - - /// Returns the theme with the given name. - pub fn get(&self, name: &str) -> Result, ThemeNotFoundError> { - self.state - .read() - .themes - .get(name) - .ok_or_else(|| ThemeNotFoundError(name.to_string().into())) - .cloned() - } - - /// Loads the themes bundled with the Zed binary and adds them to the registry. - pub fn load_bundled_themes(&self) { - let theme_paths = self - .assets - .list("themes/") - .expect("failed to list theme assets") - .into_iter() - .filter(|path| path.ends_with(".json")); - - for path in theme_paths { - let Some(theme) = self.assets.load(&path).log_err().flatten() else { - continue; - }; - - let Some(theme_family) = serde_json::from_slice(&theme) - .with_context(|| format!("failed to parse theme at path \"{path}\"")) - .log_err() - else { - continue; - }; - - self.insert_user_theme_families([theme_family]); - } - } - - /// Loads the user themes from the specified directory and adds them to the registry. - pub async fn load_user_themes(&self, themes_path: &Path, fs: Arc) -> Result<()> { - let mut theme_paths = fs - .read_dir(themes_path) - .await - .with_context(|| format!("reading themes from {themes_path:?}"))?; - - while let Some(theme_path) = theme_paths.next().await { - let Some(theme_path) = theme_path.log_err() else { - continue; - }; - - self.load_user_theme(&theme_path, fs.clone()) - .await - .log_err(); - } - - Ok(()) - } - - /// Loads the user theme from the specified path and adds it to the registry. - pub async fn load_user_theme(&self, theme_path: &Path, fs: Arc) -> Result<()> { - let theme = read_user_theme(theme_path, fs).await?; - - self.insert_user_theme_families([theme]); - - Ok(()) - } - - /// Returns the default icon theme. - pub fn default_icon_theme(&self) -> Result, IconThemeNotFoundError> { - self.get_icon_theme(DEFAULT_ICON_THEME_NAME) - } - - /// Returns the metadata of all icon themes in the registry. - pub fn list_icon_themes(&self) -> Vec { - self.state - .read() - .icon_themes - .values() - .map(|theme| ThemeMeta { - name: theme.name.clone(), - appearance: theme.appearance, - }) - .collect() - } - - /// Returns the icon theme with the specified name. - pub fn get_icon_theme(&self, name: &str) -> Result, IconThemeNotFoundError> { - self.state - .read() - .icon_themes - .get(name) - .ok_or_else(|| IconThemeNotFoundError(name.to_string().into())) - .cloned() - } - - /// Removes the icon themes with the given names from the registry. - pub fn remove_icon_themes(&self, icon_themes_to_remove: &[SharedString]) { - self.state - .write() - .icon_themes - .retain(|name, _| !icon_themes_to_remove.contains(name)) - } - - /// Loads the icon theme from the specified path and adds it to the registry. - /// - /// The `icons_root_dir` parameter indicates the root directory from which - /// the relative paths to icons in the theme should be resolved against. - pub async fn load_icon_theme( - &self, - icon_theme_path: &Path, - icons_root_dir: &Path, - fs: Arc, - ) -> Result<()> { - let icon_theme_family = read_icon_theme(icon_theme_path, fs).await?; - - let resolve_icon_path = |path: SharedString| { - icons_root_dir - .join(path.as_ref()) - .to_string_lossy() - .to_string() - .into() - }; - - let default_icon_theme = default_icon_theme(); - - let mut state = self.state.write(); - for icon_theme in icon_theme_family.themes { - let mut file_stems = default_icon_theme.file_stems.clone(); - file_stems.extend(icon_theme.file_stems); - - let mut file_suffixes = default_icon_theme.file_suffixes.clone(); - file_suffixes.extend(icon_theme.file_suffixes); - - let mut named_directory_icons = default_icon_theme.named_directory_icons.clone(); - named_directory_icons.extend(icon_theme.named_directory_icons.into_iter().map( - |(key, value)| { - ( - key, - DirectoryIcons { - collapsed: value.collapsed.map(resolve_icon_path), - expanded: value.expanded.map(resolve_icon_path), - }, - ) - }, - )); - - let icon_theme = IconTheme { - id: uuid::Uuid::new_v4().to_string(), - name: icon_theme.name.into(), - appearance: match icon_theme.appearance { - AppearanceContent::Light => Appearance::Light, - AppearanceContent::Dark => Appearance::Dark, - }, - directory_icons: DirectoryIcons { - collapsed: icon_theme.directory_icons.collapsed.map(resolve_icon_path), - expanded: icon_theme.directory_icons.expanded.map(resolve_icon_path), - }, - named_directory_icons, - chevron_icons: ChevronIcons { - collapsed: icon_theme.chevron_icons.collapsed.map(resolve_icon_path), - expanded: icon_theme.chevron_icons.expanded.map(resolve_icon_path), - }, - file_stems, - file_suffixes, - file_icons: icon_theme - .file_icons - .into_iter() - .map(|(key, icon)| { - ( - key, - IconDefinition { - path: resolve_icon_path(icon.path), - }, - ) - }) - .collect(), - }; - - state - .icon_themes - .insert(icon_theme.name.clone(), Arc::new(icon_theme)); - } - - Ok(()) - } -} - -impl Default for ThemeRegistry { - fn default() -> Self { - Self::new(Box::new(())) - } -} diff --git a/crates/theme/src/scale.rs b/crates/theme/src/scale.rs deleted file mode 100644 index 70c6114cc6..0000000000 --- a/crates/theme/src/scale.rs +++ /dev/null @@ -1,298 +0,0 @@ -#![allow(missing_docs)] -use gpui::{App, Hsla, SharedString}; - -use crate::{ActiveTheme, Appearance}; - -/// A collection of colors that are used to style the UI. -/// -/// Each step has a semantic meaning, and is used to style different parts of the UI. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub struct ColorScaleStep(usize); - -impl ColorScaleStep { - pub const ONE: Self = Self(1); - pub const TWO: Self = Self(2); - pub const THREE: Self = Self(3); - pub const FOUR: Self = Self(4); - pub const FIVE: Self = Self(5); - pub const SIX: Self = Self(6); - pub const SEVEN: Self = Self(7); - pub const EIGHT: Self = Self(8); - pub const NINE: Self = Self(9); - pub const TEN: Self = Self(10); - pub const ELEVEN: Self = Self(11); - pub const TWELVE: Self = Self(12); - - /// All of the steps in a [`ColorScale`]. - pub const ALL: [ColorScaleStep; 12] = [ - Self::ONE, - Self::TWO, - Self::THREE, - Self::FOUR, - Self::FIVE, - Self::SIX, - Self::SEVEN, - Self::EIGHT, - Self::NINE, - Self::TEN, - Self::ELEVEN, - Self::TWELVE, - ]; -} - -/// A scale of colors for a given [`ColorScaleSet`]. -/// -/// Each [`ColorScale`] contains exactly 12 colors. Refer to -/// [`ColorScaleStep`] for a reference of what each step is used for. -pub struct ColorScale(Vec); - -impl FromIterator for ColorScale { - fn from_iter>(iter: T) -> Self { - Self(Vec::from_iter(iter)) - } -} - -impl ColorScale { - /// Returns the specified step in the [`ColorScale`]. - #[inline] - pub fn step(&self, step: ColorScaleStep) -> Hsla { - // Steps are one-based, so we need convert to the zero-based vec index. - self.0[step.0 - 1] - } - - /// `Step 1` - Used for main application backgrounds. - /// - /// This step provides a neutral base for any overlaying components, ideal for applications' main backdrop or empty spaces such as canvas areas. - /// - #[inline] - pub fn step_1(&self) -> Hsla { - self.step(ColorScaleStep::ONE) - } - - /// `Step 2` - Used for both main application backgrounds and subtle component backgrounds. - /// - /// Like `Step 1`, this step allows variations in background styles, from striped tables, sidebar backgrounds, to card backgrounds. - #[inline] - pub fn step_2(&self) -> Hsla { - self.step(ColorScaleStep::TWO) - } - - /// `Step 3` - Used for UI component backgrounds in their normal states. - /// - /// This step maintains accessibility by guaranteeing a contrast ratio of 4.5:1 with steps 11 and 12 for text. It could also suit hover states for transparent components. - #[inline] - pub fn step_3(&self) -> Hsla { - self.step(ColorScaleStep::THREE) - } - - /// `Step 4` - Used for UI component backgrounds in their hover states. - /// - /// Also suited for pressed or selected states of components with a transparent background. - #[inline] - pub fn step_4(&self) -> Hsla { - self.step(ColorScaleStep::FOUR) - } - - /// `Step 5` - Used for UI component backgrounds in their pressed or selected states. - #[inline] - pub fn step_5(&self) -> Hsla { - self.step(ColorScaleStep::FIVE) - } - - /// `Step 6` - Used for subtle borders on non-interactive components. - /// - /// Its usage spans from sidebars' borders, headers' dividers, cards' outlines, to alerts' edges and separators. - #[inline] - pub fn step_6(&self) -> Hsla { - self.step(ColorScaleStep::SIX) - } - - /// `Step 7` - Used for subtle borders on interactive components. - /// - /// This step subtly delineates the boundary of elements users interact with. - #[inline] - pub fn step_7(&self) -> Hsla { - self.step(ColorScaleStep::SEVEN) - } - - /// `Step 8` - Used for stronger borders on interactive components and focus rings. - /// - /// It strengthens the visibility and accessibility of active elements and their focus states. - #[inline] - pub fn step_8(&self) -> Hsla { - self.step(ColorScaleStep::EIGHT) - } - - /// `Step 9` - Used for solid backgrounds. - /// - /// `Step 9` is the most saturated step, having the least mix of white or black. - /// - /// Due to its high chroma, `Step 9` is versatile and particularly useful for semantic colors such as - /// error, warning, and success indicators. - #[inline] - pub fn step_9(&self) -> Hsla { - self.step(ColorScaleStep::NINE) - } - - /// `Step 10` - Used for hovered or active solid backgrounds, particularly when `Step 9` is their normal state. - /// - /// May also be used for extremely low contrast text. This should be used sparingly, as it may be difficult to read. - #[inline] - pub fn step_10(&self) -> Hsla { - self.step(ColorScaleStep::TEN) - } - - /// `Step 11` - Used for text and icons requiring low contrast or less emphasis. - #[inline] - pub fn step_11(&self) -> Hsla { - self.step(ColorScaleStep::ELEVEN) - } - - /// `Step 12` - Used for text and icons requiring high contrast or prominence. - #[inline] - pub fn step_12(&self) -> Hsla { - self.step(ColorScaleStep::TWELVE) - } -} - -pub struct ColorScales { - pub gray: ColorScaleSet, - pub mauve: ColorScaleSet, - pub slate: ColorScaleSet, - pub sage: ColorScaleSet, - pub olive: ColorScaleSet, - pub sand: ColorScaleSet, - pub gold: ColorScaleSet, - pub bronze: ColorScaleSet, - pub brown: ColorScaleSet, - pub yellow: ColorScaleSet, - pub amber: ColorScaleSet, - pub orange: ColorScaleSet, - pub tomato: ColorScaleSet, - pub red: ColorScaleSet, - pub ruby: ColorScaleSet, - pub crimson: ColorScaleSet, - pub pink: ColorScaleSet, - pub plum: ColorScaleSet, - pub purple: ColorScaleSet, - pub violet: ColorScaleSet, - pub iris: ColorScaleSet, - pub indigo: ColorScaleSet, - pub blue: ColorScaleSet, - pub cyan: ColorScaleSet, - pub teal: ColorScaleSet, - pub jade: ColorScaleSet, - pub green: ColorScaleSet, - pub grass: ColorScaleSet, - pub lime: ColorScaleSet, - pub mint: ColorScaleSet, - pub sky: ColorScaleSet, - pub black: ColorScaleSet, - pub white: ColorScaleSet, -} - -impl IntoIterator for ColorScales { - type Item = ColorScaleSet; - - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - vec![ - self.gray, - self.mauve, - self.slate, - self.sage, - self.olive, - self.sand, - self.gold, - self.bronze, - self.brown, - self.yellow, - self.amber, - self.orange, - self.tomato, - self.red, - self.ruby, - self.crimson, - self.pink, - self.plum, - self.purple, - self.violet, - self.iris, - self.indigo, - self.blue, - self.cyan, - self.teal, - self.jade, - self.green, - self.grass, - self.lime, - self.mint, - self.sky, - self.black, - self.white, - ] - .into_iter() - } -} - -/// Provides groups of [`ColorScale`]s for light and dark themes, as well as transparent versions of each scale. -pub struct ColorScaleSet { - name: SharedString, - light: ColorScale, - dark: ColorScale, - light_alpha: ColorScale, - dark_alpha: ColorScale, -} - -impl ColorScaleSet { - pub fn new( - name: impl Into, - light: ColorScale, - light_alpha: ColorScale, - dark: ColorScale, - dark_alpha: ColorScale, - ) -> Self { - Self { - name: name.into(), - light, - light_alpha, - dark, - dark_alpha, - } - } - - pub fn name(&self) -> &SharedString { - &self.name - } - - pub fn light(&self) -> &ColorScale { - &self.light - } - - pub fn light_alpha(&self) -> &ColorScale { - &self.light_alpha - } - - pub fn dark(&self) -> &ColorScale { - &self.dark - } - - pub fn dark_alpha(&self) -> &ColorScale { - &self.dark_alpha - } - - pub fn step(&self, cx: &App, step: ColorScaleStep) -> Hsla { - match cx.theme().appearance { - Appearance::Light => self.light().step(step), - Appearance::Dark => self.dark().step(step), - } - } - - pub fn step_alpha(&self, cx: &App, step: ColorScaleStep) -> Hsla { - match cx.theme().appearance { - Appearance::Light => self.light_alpha.step(step), - Appearance::Dark => self.dark_alpha.step(step), - } - } -} diff --git a/crates/theme/src/schema.rs b/crates/theme/src/schema.rs deleted file mode 100644 index f52b2cf0e5..0000000000 --- a/crates/theme/src/schema.rs +++ /dev/null @@ -1,826 +0,0 @@ -#![allow(missing_docs)] - -use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla}; -use palette::FromColor; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -pub use settings::{FontWeightContent, WindowBackgroundContent}; - -use crate::{StatusColorsRefinement, ThemeColorsRefinement}; - -fn ensure_non_opaque(color: Hsla) -> Hsla { - const MAXIMUM_OPACITY: f32 = 0.7; - if color.a <= MAXIMUM_OPACITY { - color - } else { - Hsla { - a: MAXIMUM_OPACITY, - ..color - } - } -} - -fn ensure_opaque(color: Hsla) -> Hsla { - Hsla { a: 1.0, ..color } -} - -#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum AppearanceContent { - Light, - Dark, -} - -/// The content of a serialized theme family. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct ThemeFamilyContent { - pub name: String, - pub author: String, - pub themes: Vec, -} - -/// The content of a serialized theme. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct ThemeContent { - pub name: String, - pub appearance: AppearanceContent, - pub style: settings::ThemeStyleContent, -} - -/// Returns the syntax style overrides in the [`ThemeContent`]. -pub fn syntax_overrides(this: &settings::ThemeStyleContent) -> Vec<(String, HighlightStyle)> { - this.syntax - .iter() - .map(|(key, style)| { - ( - key.clone(), - HighlightStyle { - color: style - .color - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - background_color: style - .background_color - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - font_style: style.font_style.map(FontStyle::from), - font_weight: style.font_weight.map(FontWeight::from), - ..Default::default() - }, - ) - }) - .collect() -} - -pub fn status_colors_refinement(colors: &settings::StatusColorsContent) -> StatusColorsRefinement { - StatusColorsRefinement { - conflict: colors - .conflict - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - conflict_background: colors - .conflict_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - conflict_border: colors - .conflict_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - created: colors - .created - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - created_background: colors - .created_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - created_border: colors - .created_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - deleted: colors - .deleted - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - deleted_background: colors - .deleted_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - deleted_border: colors - .deleted_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - error: colors - .error - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - error_background: colors - .error_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - error_border: colors - .error_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - hidden: colors - .hidden - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - hidden_background: colors - .hidden_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - hidden_border: colors - .hidden_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - hint: colors - .hint - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - hint_background: colors - .hint_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - hint_border: colors - .hint_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ignored: colors - .ignored - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ignored_background: colors - .ignored_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ignored_border: colors - .ignored_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - info: colors - .info - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - info_background: colors - .info_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - info_border: colors - .info_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - modified: colors - .modified - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - modified_background: colors - .modified_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - modified_border: colors - .modified_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - predictive: colors - .predictive - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - predictive_background: colors - .predictive_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - predictive_border: colors - .predictive_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - renamed: colors - .renamed - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - renamed_background: colors - .renamed_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - renamed_border: colors - .renamed_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - success: colors - .success - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - success_background: colors - .success_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - success_border: colors - .success_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - unreachable: colors - .unreachable - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - unreachable_background: colors - .unreachable_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - unreachable_border: colors - .unreachable_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - warning: colors - .warning - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - warning_background: colors - .warning_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - warning_border: colors - .warning_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - } -} - -pub fn theme_colors_refinement( - this: &settings::ThemeColorsContent, - status_colors: &StatusColorsRefinement, -) -> ThemeColorsRefinement { - let border = this - .border - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let editor_document_highlight_read_background = this - .editor_document_highlight_read_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let scrollbar_thumb_background = this - .scrollbar_thumb_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or_else(|| { - this.deprecated_scrollbar_thumb_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - }); - let scrollbar_thumb_hover_background = this - .scrollbar_thumb_hover_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let scrollbar_thumb_active_background = this - .scrollbar_thumb_active_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(scrollbar_thumb_background); - let scrollbar_thumb_border = this - .scrollbar_thumb_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let element_hover = this - .element_hover - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let panel_background = this - .panel_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let search_match_background = this - .search_match_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let search_active_match_background = this - .search_active_match_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(search_match_background); - ThemeColorsRefinement { - border, - border_variant: this - .border_variant - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - border_focused: this - .border_focused - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - border_selected: this - .border_selected - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - border_transparent: this - .border_transparent - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - border_disabled: this - .border_disabled - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - elevated_surface_background: this - .elevated_surface_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - surface_background: this - .surface_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - background: this - .background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - element_background: this - .element_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - element_hover, - element_active: this - .element_active - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - element_selected: this - .element_selected - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - element_disabled: this - .element_disabled - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - element_selection_background: this - .element_selection_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - drop_target_background: this - .drop_target_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - drop_target_border: this - .drop_target_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ghost_element_background: this - .ghost_element_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ghost_element_hover: this - .ghost_element_hover - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ghost_element_active: this - .ghost_element_active - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ghost_element_selected: this - .ghost_element_selected - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ghost_element_disabled: this - .ghost_element_disabled - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - text: this - .text - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - text_muted: this - .text_muted - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - text_placeholder: this - .text_placeholder - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - text_disabled: this - .text_disabled - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - text_accent: this - .text_accent - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - icon: this - .icon - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - icon_muted: this - .icon_muted - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - icon_disabled: this - .icon_disabled - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - icon_placeholder: this - .icon_placeholder - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - icon_accent: this - .icon_accent - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - debugger_accent: this - .debugger_accent - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - status_bar_background: this - .status_bar_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - title_bar_background: this - .title_bar_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - title_bar_inactive_background: this - .title_bar_inactive_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - toolbar_background: this - .toolbar_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - tab_bar_background: this - .tab_bar_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - tab_inactive_background: this - .tab_inactive_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - tab_active_background: this - .tab_active_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - search_match_background: search_match_background, - search_active_match_background: search_active_match_background, - panel_background, - panel_focused_border: this - .panel_focused_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - panel_indent_guide: this - .panel_indent_guide - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - panel_indent_guide_hover: this - .panel_indent_guide_hover - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - panel_indent_guide_active: this - .panel_indent_guide_active - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - panel_overlay_background: this - .panel_overlay_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(panel_background.map(ensure_opaque)), - panel_overlay_hover: this - .panel_overlay_hover - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(panel_background - .zip(element_hover) - .map(|(panel_bg, hover_bg)| panel_bg.blend(hover_bg)) - .map(ensure_opaque)), - pane_focused_border: this - .pane_focused_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - pane_group_border: this - .pane_group_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(border), - scrollbar_thumb_background, - scrollbar_thumb_hover_background, - scrollbar_thumb_active_background, - scrollbar_thumb_border, - scrollbar_track_background: this - .scrollbar_track_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - scrollbar_track_border: this - .scrollbar_track_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - minimap_thumb_background: this - .minimap_thumb_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(scrollbar_thumb_background.map(ensure_non_opaque)), - minimap_thumb_hover_background: this - .minimap_thumb_hover_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(scrollbar_thumb_hover_background.map(ensure_non_opaque)), - minimap_thumb_active_background: this - .minimap_thumb_active_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(scrollbar_thumb_active_background.map(ensure_non_opaque)), - minimap_thumb_border: this - .minimap_thumb_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(scrollbar_thumb_border), - editor_foreground: this - .editor_foreground - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_background: this - .editor_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_gutter_background: this - .editor_gutter_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_subheader_background: this - .editor_subheader_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_active_line_background: this - .editor_active_line_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_highlighted_line_background: this - .editor_highlighted_line_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_debugger_active_line_background: this - .editor_debugger_active_line_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_line_number: this - .editor_line_number - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_hover_line_number: this - .editor_hover_line_number - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_active_line_number: this - .editor_active_line_number - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_invisible: this - .editor_invisible - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_wrap_guide: this - .editor_wrap_guide - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_active_wrap_guide: this - .editor_active_wrap_guide - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_indent_guide: this - .editor_indent_guide - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_indent_guide_active: this - .editor_indent_guide_active - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_document_highlight_read_background, - editor_document_highlight_write_background: this - .editor_document_highlight_write_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_document_highlight_bracket_background: this - .editor_document_highlight_bracket_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `editor.document_highlight.read_background`, for backwards compatibility. - .or(editor_document_highlight_read_background), - terminal_background: this - .terminal_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_background: this - .terminal_ansi_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_foreground: this - .terminal_foreground - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_bright_foreground: this - .terminal_bright_foreground - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_dim_foreground: this - .terminal_dim_foreground - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_black: this - .terminal_ansi_black - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_black: this - .terminal_ansi_bright_black - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_black: this - .terminal_ansi_dim_black - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_red: this - .terminal_ansi_red - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_red: this - .terminal_ansi_bright_red - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_red: this - .terminal_ansi_dim_red - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_green: this - .terminal_ansi_green - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_green: this - .terminal_ansi_bright_green - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_green: this - .terminal_ansi_dim_green - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_yellow: this - .terminal_ansi_yellow - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_yellow: this - .terminal_ansi_bright_yellow - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_yellow: this - .terminal_ansi_dim_yellow - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_blue: this - .terminal_ansi_blue - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_blue: this - .terminal_ansi_bright_blue - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_blue: this - .terminal_ansi_dim_blue - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_magenta: this - .terminal_ansi_magenta - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_magenta: this - .terminal_ansi_bright_magenta - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_magenta: this - .terminal_ansi_dim_magenta - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_cyan: this - .terminal_ansi_cyan - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_cyan: this - .terminal_ansi_bright_cyan - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_cyan: this - .terminal_ansi_dim_cyan - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_white: this - .terminal_ansi_white - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_white: this - .terminal_ansi_bright_white - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_white: this - .terminal_ansi_dim_white - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - link_text_hover: this - .link_text_hover - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - version_control_added: this - .version_control_added - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `created`, for backwards compatibility. - .or(status_colors.created), - version_control_deleted: this - .version_control_deleted - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `deleted`, for backwards compatibility. - .or(status_colors.deleted), - version_control_modified: this - .version_control_modified - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `modified`, for backwards compatibility. - .or(status_colors.modified), - version_control_renamed: this - .version_control_renamed - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `modified`, for backwards compatibility. - .or(status_colors.modified), - version_control_conflict: this - .version_control_conflict - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `ignored`, for backwards compatibility. - .or(status_colors.ignored), - version_control_ignored: this - .version_control_ignored - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `conflict`, for backwards compatibility. - .or(status_colors.ignored), - version_control_word_added: this - .version_control_word_added - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - version_control_word_deleted: this - .version_control_word_deleted - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - #[allow(deprecated)] - version_control_conflict_marker_ours: this - .version_control_conflict_marker_ours - .as_ref() - .or(this.version_control_conflict_ours_background.as_ref()) - .and_then(|color| try_parse_color(color).ok()), - #[allow(deprecated)] - version_control_conflict_marker_theirs: this - .version_control_conflict_marker_theirs - .as_ref() - .or(this.version_control_conflict_theirs_background.as_ref()) - .and_then(|color| try_parse_color(color).ok()), - vim_normal_background: this - .vim_normal_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - vim_insert_background: this - .vim_insert_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - vim_replace_background: this - .vim_replace_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - vim_visual_background: this - .vim_visual_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - vim_visual_line_background: this - .vim_visual_line_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - vim_visual_block_background: this - .vim_visual_block_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - vim_helix_normal_background: this - .vim_helix_normal_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - vim_helix_select_background: this - .vim_helix_select_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - vim_mode_text: this - .vim_mode_text - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - } -} - -pub(crate) fn try_parse_color(color: &str) -> anyhow::Result { - let rgba = gpui::Rgba::try_from(color)?; - let rgba = palette::rgb::Srgba::from_components((rgba.r, rgba.g, rgba.b, rgba.a)); - let hsla = palette::Hsla::from_color(rgba); - - let hsla = gpui::hsla( - hsla.hue.into_positive_degrees() / 360., - hsla.saturation, - hsla.lightness, - hsla.alpha, - ); - - Ok(hsla) -} diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs deleted file mode 100644 index d60d4882a6..0000000000 --- a/crates/theme/src/settings.rs +++ /dev/null @@ -1,749 +0,0 @@ -use crate::{ - Appearance, DEFAULT_ICON_THEME_NAME, SyntaxTheme, Theme, status_colors_refinement, - syntax_overrides, theme_colors_refinement, -}; -use collections::HashMap; -use derive_more::{Deref, DerefMut}; -use gpui::{ - App, Context, Font, FontFallbacks, FontStyle, FontWeight, Global, Pixels, Subscription, Window, - px, -}; -use refineable::Refineable; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -pub use settings::{FontFamilyName, IconThemeName, ThemeAppearanceMode, ThemeName}; -use settings::{RegisterSetting, Settings, SettingsContent}; -use std::sync::Arc; - -const MIN_FONT_SIZE: Pixels = px(6.0); -const MAX_FONT_SIZE: Pixels = px(100.0); -const MIN_LINE_HEIGHT: f32 = 1.0; - -#[derive( - Debug, - Default, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Clone, - Copy, - Serialize, - Deserialize, - JsonSchema, -)] - -/// Specifies the density of the UI. -/// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078) -#[serde(rename_all = "snake_case")] -pub enum UiDensity { - /// A denser UI with tighter spacing and smaller elements. - #[serde(alias = "compact")] - Compact, - #[default] - #[serde(alias = "default")] - /// The default UI density. - Default, - #[serde(alias = "comfortable")] - /// A looser UI with more spacing and larger elements. - Comfortable, -} - -impl UiDensity { - /// The spacing ratio of a given density. - /// TODO: Standardize usage throughout the app or remove - pub fn spacing_ratio(self) -> f32 { - match self { - UiDensity::Compact => 0.75, - UiDensity::Default => 1.0, - UiDensity::Comfortable => 1.25, - } - } -} - -impl From for UiDensity { - fn from(s: String) -> Self { - match s.as_str() { - "compact" => Self::Compact, - "default" => Self::Default, - "comfortable" => Self::Comfortable, - _ => Self::default(), - } - } -} - -impl From for String { - fn from(val: UiDensity) -> Self { - match val { - UiDensity::Compact => "compact".to_string(), - UiDensity::Default => "default".to_string(), - UiDensity::Comfortable => "comfortable".to_string(), - } - } -} - -impl From for UiDensity { - fn from(val: settings::UiDensity) -> Self { - match val { - settings::UiDensity::Compact => Self::Compact, - settings::UiDensity::Default => Self::Default, - settings::UiDensity::Comfortable => Self::Comfortable, - } - } -} - -/// Customizable settings for the UI and theme system. -#[derive(Clone, PartialEq, RegisterSetting)] -pub struct ThemeSettings { - /// The UI font size. Determines the size of text in the UI, - /// as well as the size of a [gpui::Rems] unit. - /// - /// Changing this will impact the size of all UI elements. - ui_font_size: Pixels, - /// The font used for UI elements. - pub ui_font: Font, - /// The font size used for buffers, and the terminal. - /// - /// The terminal font size can be overridden using it's own setting. - buffer_font_size: Pixels, - /// The font used for buffers, and the terminal. - /// - /// The terminal font family can be overridden using it's own setting. - pub buffer_font: Font, - /// The agent font size. Determines the size of text in the agent panel. Falls back to the UI font size if unset. - agent_ui_font_size: Option, - /// The agent buffer font size. Determines the size of user messages in the agent panel. - agent_buffer_font_size: Option, - /// The line height for buffers, and the terminal. - /// - /// Changing this may affect the spacing of some UI elements. - /// - /// The terminal font family can be overridden using it's own setting. - pub buffer_line_height: BufferLineHeight, - /// The current theme selection. - pub theme: ThemeSelection, - /// Manual overrides for the active theme. - /// - /// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078) - pub experimental_theme_overrides: Option, - /// Manual overrides per theme - pub theme_overrides: HashMap, - /// The current icon theme selection. - pub icon_theme: IconThemeSelection, - /// The density of the UI. - /// Note: This setting is still experimental. See [this tracking issue]( - pub ui_density: UiDensity, - /// The amount of fading applied to unnecessary code. - pub unnecessary_code_fade: f32, -} - -pub(crate) const DEFAULT_LIGHT_THEME: &'static str = "One Light"; -pub(crate) const DEFAULT_DARK_THEME: &'static str = "One Dark"; - -/// Returns the name of the default theme for the given [`Appearance`]. -pub fn default_theme(appearance: Appearance) -> &'static str { - match appearance { - Appearance::Light => DEFAULT_LIGHT_THEME, - Appearance::Dark => DEFAULT_DARK_THEME, - } -} - -/// The appearance of the system. -#[derive(Debug, Clone, Copy, Deref)] -pub struct SystemAppearance(pub Appearance); - -impl Default for SystemAppearance { - fn default() -> Self { - Self(Appearance::Dark) - } -} - -#[derive(Deref, DerefMut, Default)] -struct GlobalSystemAppearance(SystemAppearance); - -impl Global for GlobalSystemAppearance {} - -impl SystemAppearance { - /// Initializes the [`SystemAppearance`] for the application. - pub fn init(cx: &mut App) { - *cx.default_global::() = - GlobalSystemAppearance(SystemAppearance(cx.window_appearance().into())); - } - - /// Returns the global [`SystemAppearance`]. - pub fn global(cx: &App) -> Self { - cx.global::().0 - } - - /// Returns a mutable reference to the global [`SystemAppearance`]. - pub fn global_mut(cx: &mut App) -> &mut Self { - cx.global_mut::() - } -} - -#[derive(Default)] -struct BufferFontSize(Pixels); - -impl Global for BufferFontSize {} - -#[derive(Default)] -pub(crate) struct UiFontSize(Pixels); - -impl Global for UiFontSize {} - -/// In-memory override for the font size in the agent panel. -#[derive(Default)] -pub struct AgentFontSize(Pixels); - -impl Global for AgentFontSize {} - -/// Represents the selection of a theme, which can be either static or dynamic. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(untagged)] -pub enum ThemeSelection { - /// A static theme selection, represented by a single theme name. - Static(ThemeName), - /// A dynamic theme selection, which can change based the [ThemeMode]. - Dynamic { - /// The mode used to determine which theme to use. - #[serde(default)] - mode: ThemeAppearanceMode, - /// The theme to use for light mode. - light: ThemeName, - /// The theme to use for dark mode. - dark: ThemeName, - }, -} - -impl From for ThemeSelection { - fn from(selection: settings::ThemeSelection) -> Self { - match selection { - settings::ThemeSelection::Static(theme) => ThemeSelection::Static(theme), - settings::ThemeSelection::Dynamic { mode, light, dark } => { - ThemeSelection::Dynamic { mode, light, dark } - } - } - } -} - -impl ThemeSelection { - /// Returns the theme name for the selected [ThemeMode]. - pub fn name(&self, system_appearance: Appearance) -> ThemeName { - match self { - Self::Static(theme) => theme.clone(), - Self::Dynamic { mode, light, dark } => match mode { - ThemeAppearanceMode::Light => light.clone(), - ThemeAppearanceMode::Dark => dark.clone(), - ThemeAppearanceMode::System => match system_appearance { - Appearance::Light => light.clone(), - Appearance::Dark => dark.clone(), - }, - }, - } - } - - /// Returns the [ThemeMode] for the [ThemeSelection]. - pub fn mode(&self) -> Option { - match self { - ThemeSelection::Static(_) => None, - ThemeSelection::Dynamic { mode, .. } => Some(*mode), - } - } -} - -/// Represents the selection of an icon theme, which can be either static or dynamic. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum IconThemeSelection { - /// A static icon theme selection, represented by a single icon theme name. - Static(IconThemeName), - /// A dynamic icon theme selection, which can change based on the [`ThemeMode`]. - Dynamic { - /// The mode used to determine which theme to use. - mode: ThemeAppearanceMode, - /// The icon theme to use for light mode. - light: IconThemeName, - /// The icon theme to use for dark mode. - dark: IconThemeName, - }, -} - -impl From for IconThemeSelection { - fn from(selection: settings::IconThemeSelection) -> Self { - match selection { - settings::IconThemeSelection::Static(theme) => IconThemeSelection::Static(theme), - settings::IconThemeSelection::Dynamic { mode, light, dark } => { - IconThemeSelection::Dynamic { mode, light, dark } - } - } - } -} - -impl IconThemeSelection { - /// Returns the icon theme name based on the given [`Appearance`]. - pub fn name(&self, system_appearance: Appearance) -> IconThemeName { - match self { - Self::Static(theme) => theme.clone(), - Self::Dynamic { mode, light, dark } => match mode { - ThemeAppearanceMode::Light => light.clone(), - ThemeAppearanceMode::Dark => dark.clone(), - ThemeAppearanceMode::System => match system_appearance { - Appearance::Light => light.clone(), - Appearance::Dark => dark.clone(), - }, - }, - } - } - - /// Returns the [`ThemeMode`] for the [`IconThemeSelection`]. - pub fn mode(&self) -> Option { - match self { - IconThemeSelection::Static(_) => None, - IconThemeSelection::Dynamic { mode, .. } => Some(*mode), - } - } -} - -/// Sets the theme for the given appearance to the theme with the specified name. -/// -/// The caller should make sure that the [`Appearance`] matches the theme associated with the name. -/// -/// If the current [`ThemeAppearanceMode`] is set to [`System`] and the user's system [`Appearance`] -/// is different than the new theme's [`Appearance`], this function will update the -/// [`ThemeAppearanceMode`] to the new theme's appearance in order to display the new theme. -/// -/// [`System`]: ThemeAppearanceMode::System -pub fn set_theme( - current: &mut SettingsContent, - theme_name: impl Into>, - theme_appearance: Appearance, - system_appearance: Appearance, -) { - let theme_name = ThemeName(theme_name.into()); - - let Some(selection) = current.theme.theme.as_mut() else { - current.theme.theme = Some(settings::ThemeSelection::Static(theme_name)); - return; - }; - - match selection { - settings::ThemeSelection::Static(theme) => { - *theme = theme_name; - } - settings::ThemeSelection::Dynamic { mode, light, dark } => { - // Update the appropriate theme slot based on appearance. - match theme_appearance { - Appearance::Light => *light = theme_name, - Appearance::Dark => *dark = theme_name, - } - - // Don't update the theme mode if it is set to system and the new theme has the same - // appearance. - let should_update_mode = - !(mode == &ThemeAppearanceMode::System && theme_appearance == system_appearance); - - if should_update_mode { - // Update the mode to the specified appearance (otherwise we might set the theme and - // nothing gets updated because the system specified the other mode appearance). - *mode = ThemeAppearanceMode::from(theme_appearance); - } - } - } -} - -/// Sets the icon theme for the given appearance to the icon theme with the specified name. -pub fn set_icon_theme( - current: &mut SettingsContent, - icon_theme_name: IconThemeName, - appearance: Appearance, -) { - if let Some(selection) = current.theme.icon_theme.as_mut() { - let icon_theme_to_update = match selection { - settings::IconThemeSelection::Static(theme) => theme, - settings::IconThemeSelection::Dynamic { mode, light, dark } => match mode { - ThemeAppearanceMode::Light => light, - ThemeAppearanceMode::Dark => dark, - ThemeAppearanceMode::System => match appearance { - Appearance::Light => light, - Appearance::Dark => dark, - }, - }, - }; - - *icon_theme_to_update = icon_theme_name; - } else { - current.theme.icon_theme = Some(settings::IconThemeSelection::Static(icon_theme_name)); - } -} - -/// Sets the mode for the theme. -pub fn set_mode(content: &mut SettingsContent, mode: ThemeAppearanceMode) { - let theme = content.theme.as_mut(); - - if let Some(selection) = theme.theme.as_mut() { - match selection { - settings::ThemeSelection::Static(theme) => { - // If the theme was previously set to a single static theme, - // we don't know whether it was a light or dark theme, so we - // just use it for both. - *selection = settings::ThemeSelection::Dynamic { - mode, - light: theme.clone(), - dark: theme.clone(), - }; - } - settings::ThemeSelection::Dynamic { - mode: mode_to_update, - .. - } => *mode_to_update = mode, - } - } else { - theme.theme = Some(settings::ThemeSelection::Dynamic { - mode, - light: ThemeName(DEFAULT_LIGHT_THEME.into()), - dark: ThemeName(DEFAULT_DARK_THEME.into()), - }); - } - - if let Some(selection) = theme.icon_theme.as_mut() { - match selection { - settings::IconThemeSelection::Static(icon_theme) => { - // If the icon theme was previously set to a single static - // theme, we don't know whether it was a light or dark - // theme, so we just use it for both. - *selection = settings::IconThemeSelection::Dynamic { - mode, - light: icon_theme.clone(), - dark: icon_theme.clone(), - }; - } - settings::IconThemeSelection::Dynamic { - mode: mode_to_update, - .. - } => *mode_to_update = mode, - } - } else { - theme.icon_theme = Some(settings::IconThemeSelection::Static(IconThemeName( - DEFAULT_ICON_THEME_NAME.into(), - ))); - } -} -// } - -/// The buffer's line height. -#[derive(Clone, Copy, Debug, PartialEq, Default)] -pub enum BufferLineHeight { - /// A less dense line height. - #[default] - Comfortable, - /// The default line height. - Standard, - /// A custom line height, where 1.0 is the font's height. Must be at least 1.0. - Custom(f32), -} - -impl From for BufferLineHeight { - fn from(value: settings::BufferLineHeight) -> Self { - match value { - settings::BufferLineHeight::Comfortable => BufferLineHeight::Comfortable, - settings::BufferLineHeight::Standard => BufferLineHeight::Standard, - settings::BufferLineHeight::Custom(line_height) => { - BufferLineHeight::Custom(line_height) - } - } - } -} - -impl BufferLineHeight { - /// Returns the value of the line height. - pub fn value(&self) -> f32 { - match self { - BufferLineHeight::Comfortable => 1.618, - BufferLineHeight::Standard => 1.3, - BufferLineHeight::Custom(line_height) => *line_height, - } - } -} - -impl ThemeSettings { - /// Returns the buffer font size. - pub fn buffer_font_size(&self, cx: &App) -> Pixels { - let font_size = cx - .try_global::() - .map(|size| size.0) - .unwrap_or(self.buffer_font_size); - clamp_font_size(font_size) - } - - /// Returns the UI font size. - pub fn ui_font_size(&self, cx: &App) -> Pixels { - let font_size = cx - .try_global::() - .map(|size| size.0) - .unwrap_or(self.ui_font_size); - clamp_font_size(font_size) - } - - /// Returns the agent panel font size. Falls back to the UI font size if unset. - pub fn agent_ui_font_size(&self, cx: &App) -> Pixels { - cx.try_global::() - .map(|size| size.0) - .or(self.agent_ui_font_size) - .map(clamp_font_size) - .unwrap_or_else(|| self.ui_font_size(cx)) - } - - /// Returns the agent panel buffer font size. - pub fn agent_buffer_font_size(&self, cx: &App) -> Pixels { - cx.try_global::() - .map(|size| size.0) - .or(self.agent_buffer_font_size) - .map(clamp_font_size) - .unwrap_or_else(|| self.buffer_font_size(cx)) - } - - /// Returns the buffer font size, read from the settings. - /// - /// The real buffer font size is stored in-memory, to support temporary font size changes. - /// Use [`Self::buffer_font_size`] to get the real font size. - pub fn buffer_font_size_settings(&self) -> Pixels { - self.buffer_font_size - } - - /// Returns the UI font size, read from the settings. - /// - /// The real UI font size is stored in-memory, to support temporary font size changes. - /// Use [`Self::ui_font_size`] to get the real font size. - pub fn ui_font_size_settings(&self) -> Pixels { - self.ui_font_size - } - - /// Returns the agent font size, read from the settings. - /// - /// The real agent font size is stored in-memory, to support temporary font size changes. - /// Use [`Self::agent_ui_font_size`] to get the real font size. - pub fn agent_ui_font_size_settings(&self) -> Option { - self.agent_ui_font_size - } - - /// Returns the agent buffer font size, read from the settings. - /// - /// The real agent buffer font size is stored in-memory, to support temporary font size changes. - /// Use [`Self::agent_buffer_font_size`] to get the real font size. - pub fn agent_buffer_font_size_settings(&self) -> Option { - self.agent_buffer_font_size - } - - // TODO: Rename: `line_height` -> `buffer_line_height` - /// Returns the buffer's line height. - pub fn line_height(&self) -> f32 { - f32::max(self.buffer_line_height.value(), MIN_LINE_HEIGHT) - } - - /// Applies the theme overrides, if there are any, to the current theme. - pub fn apply_theme_overrides(&self, mut arc_theme: Arc) -> Arc { - // Apply the old overrides setting first, so that the new setting can override those. - if let Some(experimental_theme_overrides) = &self.experimental_theme_overrides { - let mut theme = (*arc_theme).clone(); - ThemeSettings::modify_theme(&mut theme, experimental_theme_overrides); - arc_theme = Arc::new(theme); - } - - if let Some(theme_overrides) = self.theme_overrides.get(arc_theme.name.as_ref()) { - let mut theme = (*arc_theme).clone(); - ThemeSettings::modify_theme(&mut theme, theme_overrides); - arc_theme = Arc::new(theme); - } - - arc_theme - } - - fn modify_theme(base_theme: &mut Theme, theme_overrides: &settings::ThemeStyleContent) { - if let Some(window_background_appearance) = theme_overrides.window_background_appearance { - base_theme.styles.window_background_appearance = window_background_appearance.into(); - } - let status_color_refinement = status_colors_refinement(&theme_overrides.status); - - base_theme.styles.colors.refine(&theme_colors_refinement( - &theme_overrides.colors, - &status_color_refinement, - )); - base_theme.styles.status.refine(&status_color_refinement); - base_theme.styles.player.merge(&theme_overrides.players); - base_theme.styles.accents.merge(&theme_overrides.accents); - base_theme.styles.syntax = SyntaxTheme::merge( - base_theme.styles.syntax.clone(), - syntax_overrides(&theme_overrides), - ); - } -} - -/// Observe changes to the adjusted buffer font size. -pub fn observe_buffer_font_size_adjustment( - cx: &mut Context, - f: impl 'static + Fn(&mut V, &mut Context), -) -> Subscription { - cx.observe_global::(f) -} - -/// Gets the font size, adjusted by the difference between the current buffer font size and the one set in the settings. -pub fn adjusted_font_size(size: Pixels, cx: &App) -> Pixels { - let adjusted_font_size = - if let Some(BufferFontSize(adjusted_size)) = cx.try_global::() { - let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size; - let delta = *adjusted_size - buffer_font_size; - size + delta - } else { - size - }; - clamp_font_size(adjusted_font_size) -} - -/// Adjusts the buffer font size. -pub fn adjust_buffer_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) { - let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size; - let adjusted_size = cx - .try_global::() - .map_or(buffer_font_size, |adjusted_size| adjusted_size.0); - cx.set_global(BufferFontSize(clamp_font_size(f(adjusted_size)))); - cx.refresh_windows(); -} - -/// Resets the buffer font size to the default value. -pub fn reset_buffer_font_size(cx: &mut App) { - if cx.has_global::() { - cx.remove_global::(); - cx.refresh_windows(); - } -} - -// TODO: Make private, change usages to use `get_ui_font_size` instead. -#[allow(missing_docs)] -pub fn setup_ui_font(window: &mut Window, cx: &mut App) -> gpui::Font { - let (ui_font, ui_font_size) = { - let theme_settings = ThemeSettings::get_global(cx); - let font = theme_settings.ui_font.clone(); - (font, theme_settings.ui_font_size(cx)) - }; - - window.set_rem_size(ui_font_size); - ui_font -} - -/// Sets the adjusted UI font size. -pub fn adjust_ui_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) { - let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx); - let adjusted_size = cx - .try_global::() - .map_or(ui_font_size, |adjusted_size| adjusted_size.0); - cx.set_global(UiFontSize(clamp_font_size(f(adjusted_size)))); - cx.refresh_windows(); -} - -/// Resets the UI font size to the default value. -pub fn reset_ui_font_size(cx: &mut App) { - if cx.has_global::() { - cx.remove_global::(); - cx.refresh_windows(); - } -} - -/// Sets the adjusted font size of agent responses in the agent panel. -pub fn adjust_agent_ui_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) { - let agent_ui_font_size = ThemeSettings::get_global(cx).agent_ui_font_size(cx); - let adjusted_size = cx - .try_global::() - .map_or(agent_ui_font_size, |adjusted_size| adjusted_size.0); - cx.set_global(AgentFontSize(clamp_font_size(f(adjusted_size)))); - cx.refresh_windows(); -} - -/// Resets the agent response font size in the agent panel to the default value. -pub fn reset_agent_ui_font_size(cx: &mut App) { - if cx.has_global::() { - cx.remove_global::(); - cx.refresh_windows(); - } -} - -/// Sets the adjusted font size of user messages in the agent panel. -pub fn adjust_agent_buffer_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) { - let agent_buffer_font_size = ThemeSettings::get_global(cx).agent_buffer_font_size(cx); - let adjusted_size = cx - .try_global::() - .map_or(agent_buffer_font_size, |adjusted_size| adjusted_size.0); - cx.set_global(AgentFontSize(clamp_font_size(f(adjusted_size)))); - cx.refresh_windows(); -} - -/// Resets the user message font size in the agent panel to the default value. -pub fn reset_agent_buffer_font_size(cx: &mut App) { - if cx.has_global::() { - cx.remove_global::(); - cx.refresh_windows(); - } -} - -/// Ensures font size is within the valid range. -pub fn clamp_font_size(size: Pixels) -> Pixels { - size.clamp(MIN_FONT_SIZE, MAX_FONT_SIZE) -} - -fn clamp_font_weight(weight: f32) -> FontWeight { - FontWeight(weight.clamp(100., 950.)) -} - -/// font fallback from settings -pub fn font_fallbacks_from_settings( - fallbacks: Option>, -) -> Option { - fallbacks.map(|fallbacks| { - FontFallbacks::from_fonts( - fallbacks - .into_iter() - .map(|font_family| font_family.0.to_string()) - .collect(), - ) - }) -} - -impl settings::Settings for ThemeSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let content = &content.theme; - let theme_selection: ThemeSelection = content.theme.clone().unwrap().into(); - let icon_theme_selection: IconThemeSelection = content.icon_theme.clone().unwrap().into(); - Self { - ui_font_size: clamp_font_size(content.ui_font_size.unwrap().into()), - ui_font: Font { - family: content.ui_font_family.as_ref().unwrap().0.clone().into(), - features: content.ui_font_features.clone().unwrap(), - fallbacks: font_fallbacks_from_settings(content.ui_font_fallbacks.clone()), - weight: clamp_font_weight(content.ui_font_weight.unwrap().0), - style: Default::default(), - }, - buffer_font: Font { - family: content - .buffer_font_family - .as_ref() - .unwrap() - .0 - .clone() - .into(), - features: content.buffer_font_features.clone().unwrap(), - fallbacks: font_fallbacks_from_settings(content.buffer_font_fallbacks.clone()), - weight: clamp_font_weight(content.buffer_font_weight.unwrap().0), - style: FontStyle::default(), - }, - buffer_font_size: clamp_font_size(content.buffer_font_size.unwrap().into()), - buffer_line_height: content.buffer_line_height.unwrap().into(), - agent_ui_font_size: content.agent_ui_font_size.map(Into::into), - agent_buffer_font_size: content.agent_buffer_font_size.map(Into::into), - theme: theme_selection, - experimental_theme_overrides: content.experimental_theme_overrides.clone(), - theme_overrides: content.theme_overrides.clone(), - icon_theme: icon_theme_selection, - ui_density: content.ui_density.unwrap_or_default().into(), - unnecessary_code_fade: content.unnecessary_code_fade.unwrap().0.clamp(0.0, 0.9), - } - } -} diff --git a/crates/theme/src/styles.rs b/crates/theme/src/styles.rs deleted file mode 100644 index da22f8de1f..0000000000 --- a/crates/theme/src/styles.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod accents; -mod colors; -mod players; -mod status; -mod syntax; -mod system; - -pub use accents::*; -pub use colors::*; -pub use players::*; -pub use status::*; -pub use syntax::*; -pub use system::*; diff --git a/crates/theme/src/styles/accents.rs b/crates/theme/src/styles/accents.rs deleted file mode 100644 index 49ae755bf8..0000000000 --- a/crates/theme/src/styles/accents.rs +++ /dev/null @@ -1,88 +0,0 @@ -use gpui::Hsla; -use serde::Deserialize; - -use crate::{ - amber, blue, cyan, gold, grass, indigo, iris, jade, lime, orange, pink, purple, tomato, - try_parse_color, -}; - -/// A collection of colors that are used to color indent aware lines in the editor. -#[derive(Clone, Debug, Deserialize, PartialEq)] -pub struct AccentColors(pub Vec); - -impl Default for AccentColors { - /// Don't use this! - /// We have to have a default to be `[refineable::Refinable]`. - /// TODO "Find a way to not need this for Refinable" - fn default() -> Self { - Self::dark() - } -} - -impl AccentColors { - /// Returns the set of dark accent colors. - pub fn dark() -> Self { - Self(vec![ - blue().dark().step_9(), - orange().dark().step_9(), - pink().dark().step_9(), - lime().dark().step_9(), - purple().dark().step_9(), - amber().dark().step_9(), - jade().dark().step_9(), - tomato().dark().step_9(), - cyan().dark().step_9(), - gold().dark().step_9(), - grass().dark().step_9(), - indigo().dark().step_9(), - iris().dark().step_9(), - ]) - } - - /// Returns the set of light accent colors. - pub fn light() -> Self { - Self(vec![ - blue().light().step_9(), - orange().light().step_9(), - pink().light().step_9(), - lime().light().step_9(), - purple().light().step_9(), - amber().light().step_9(), - jade().light().step_9(), - tomato().light().step_9(), - cyan().light().step_9(), - gold().light().step_9(), - grass().light().step_9(), - indigo().light().step_9(), - iris().light().step_9(), - ]) - } -} - -impl AccentColors { - /// Returns the color for the given index. - pub fn color_for_index(&self, index: u32) -> Hsla { - self.0[index as usize % self.0.len()] - } - - /// Merges the given accent colors into this [`AccentColors`] instance. - pub fn merge(&mut self, accent_colors: &[settings::AccentContent]) { - if accent_colors.is_empty() { - return; - } - - let colors = accent_colors - .iter() - .filter_map(|accent_color| { - accent_color - .0 - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - }) - .collect::>(); - - if !colors.is_empty() { - self.0 = colors; - } - } -} diff --git a/crates/theme/src/styles/colors.rs b/crates/theme/src/styles/colors.rs deleted file mode 100644 index 905f2245e0..0000000000 --- a/crates/theme/src/styles/colors.rs +++ /dev/null @@ -1,655 +0,0 @@ -#![allow(missing_docs)] - -use gpui::{App, Hsla, SharedString, WindowBackgroundAppearance}; -use refineable::Refineable; -use std::sync::Arc; -use strum::{AsRefStr, EnumIter, IntoEnumIterator}; - -use crate::{ - AccentColors, ActiveTheme, PlayerColors, StatusColors, StatusColorsRefinement, SyntaxTheme, - SystemColors, -}; - -#[derive(Refineable, Clone, Debug, PartialEq)] -#[refineable(Debug, serde::Deserialize)] -pub struct ThemeColors { - /// Border color. Used for most borders, is usually a high contrast color. - pub border: Hsla, - /// Border color. Used for deemphasized borders, like a visual divider between two sections - pub border_variant: Hsla, - /// Border color. Used for focused elements, like keyboard focused list item. - pub border_focused: Hsla, - /// Border color. Used for selected elements, like an active search filter or selected checkbox. - pub border_selected: Hsla, - /// Border color. Used for transparent borders. Used for placeholder borders when an element gains a border on state change. - pub border_transparent: Hsla, - /// Border color. Used for disabled elements, like a disabled input or button. - pub border_disabled: Hsla, - /// Border color. Used for elevated surfaces, like a context menu, popup, or dialog. - pub elevated_surface_background: Hsla, - /// Background Color. Used for grounded surfaces like a panel or tab. - pub surface_background: Hsla, - /// Background Color. Used for the app background and blank panels or windows. - pub background: Hsla, - /// Background Color. Used for the background of an element that should have a different background than the surface it's on. - /// - /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons... - /// - /// For an element that should have the same background as the surface it's on, use `ghost_element_background`. - pub element_background: Hsla, - /// Background Color. Used for the hover state of an element that should have a different background than the surface it's on. - /// - /// Hover states are triggered by the mouse entering an element, or a finger touching an element on a touch screen. - pub element_hover: Hsla, - /// Background Color. Used for the active state of an element that should have a different background than the surface it's on. - /// - /// Active states are triggered by the mouse button being pressed down on an element, or the Return button or other activator being pressed. - pub element_active: Hsla, - /// Background Color. Used for the selected state of an element that should have a different background than the surface it's on. - /// - /// Selected states are triggered by the element being selected (or "activated") by the user. - /// - /// This could include a selected checkbox, a toggleable button that is toggled on, etc. - pub element_selected: Hsla, - /// Background Color. Used for the background of selections in a UI element. - pub element_selection_background: Hsla, - /// Background Color. Used for the disabled state of an element that should have a different background than the surface it's on. - /// - /// Disabled states are shown when a user cannot interact with an element, like a disabled button or input. - pub element_disabled: Hsla, - /// Background Color. Used for the area that shows where a dragged element will be dropped. - pub drop_target_background: Hsla, - /// Border Color. Used for the border that shows where a dragged element will be dropped. - pub drop_target_border: Hsla, - /// Used for the background of a ghost element that should have the same background as the surface it's on. - /// - /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons... - /// - /// For an element that should have a different background than the surface it's on, use `element_background`. - pub ghost_element_background: Hsla, - /// Background Color. Used for the hover state of a ghost element that should have the same background as the surface it's on. - /// - /// Hover states are triggered by the mouse entering an element, or a finger touching an element on a touch screen. - pub ghost_element_hover: Hsla, - /// Background Color. Used for the active state of a ghost element that should have the same background as the surface it's on. - /// - /// Active states are triggered by the mouse button being pressed down on an element, or the Return button or other activator being pressed. - pub ghost_element_active: Hsla, - /// Background Color. Used for the selected state of a ghost element that should have the same background as the surface it's on. - /// - /// Selected states are triggered by the element being selected (or "activated") by the user. - /// - /// This could include a selected checkbox, a toggleable button that is toggled on, etc. - pub ghost_element_selected: Hsla, - /// Background Color. Used for the disabled state of a ghost element that should have the same background as the surface it's on. - /// - /// Disabled states are shown when a user cannot interact with an element, like a disabled button or input. - pub ghost_element_disabled: Hsla, - /// Text Color. Default text color used for most text. - pub text: Hsla, - /// Text Color. Color of muted or deemphasized text. It is a subdued version of the standard text color. - pub text_muted: Hsla, - /// Text Color. Color of the placeholder text typically shown in input fields to guide the user to enter valid data. - pub text_placeholder: Hsla, - /// Text Color. Color used for text denoting disabled elements. Typically, the color is faded or grayed out to emphasize the disabled state. - pub text_disabled: Hsla, - /// Text Color. Color used for emphasis or highlighting certain text, like an active filter or a matched character in a search. - pub text_accent: Hsla, - /// Fill Color. Used for the default fill color of an icon. - pub icon: Hsla, - /// Fill Color. Used for the muted or deemphasized fill color of an icon. - /// - /// This might be used to show an icon in an inactive pane, or to deemphasize a series of icons to give them less visual weight. - pub icon_muted: Hsla, - /// Fill Color. Used for the disabled fill color of an icon. - /// - /// Disabled states are shown when a user cannot interact with an element, like a icon button. - pub icon_disabled: Hsla, - /// Fill Color. Used for the placeholder fill color of an icon. - /// - /// This might be used to show an icon in an input that disappears when the user enters text. - pub icon_placeholder: Hsla, - /// Fill Color. Used for the accent fill color of an icon. - /// - /// This might be used to show when a toggleable icon button is selected. - pub icon_accent: Hsla, - /// Color used to accent some debugger elements - /// Is used by breakpoints - pub debugger_accent: Hsla, - - // === - // UI Elements - // === - pub status_bar_background: Hsla, - pub title_bar_background: Hsla, - pub title_bar_inactive_background: Hsla, - pub toolbar_background: Hsla, - pub tab_bar_background: Hsla, - pub tab_inactive_background: Hsla, - pub tab_active_background: Hsla, - pub search_match_background: Hsla, - pub search_active_match_background: Hsla, - pub panel_background: Hsla, - pub panel_focused_border: Hsla, - pub panel_indent_guide: Hsla, - pub panel_indent_guide_hover: Hsla, - pub panel_indent_guide_active: Hsla, - - /// The color of the overlay surface on top of panel. - pub panel_overlay_background: Hsla, - /// The color of the overlay surface on top of panel when hovered over. - pub panel_overlay_hover: Hsla, - - pub pane_focused_border: Hsla, - pub pane_group_border: Hsla, - /// The color of the scrollbar thumb. - pub scrollbar_thumb_background: Hsla, - /// The color of the scrollbar thumb when hovered over. - pub scrollbar_thumb_hover_background: Hsla, - /// The color of the scrollbar thumb whilst being actively dragged. - pub scrollbar_thumb_active_background: Hsla, - /// The border color of the scrollbar thumb. - pub scrollbar_thumb_border: Hsla, - /// The background color of the scrollbar track. - pub scrollbar_track_background: Hsla, - /// The border color of the scrollbar track. - pub scrollbar_track_border: Hsla, - /// The color of the minimap thumb. - pub minimap_thumb_background: Hsla, - /// The color of the minimap thumb when hovered over. - pub minimap_thumb_hover_background: Hsla, - /// The color of the minimap thumb whilst being actively dragged. - pub minimap_thumb_active_background: Hsla, - /// The border color of the minimap thumb. - pub minimap_thumb_border: Hsla, - - /// Background color for Vim Normal mode indicator. - pub vim_normal_background: Hsla, - /// Background color for Vim Insert mode indicator. - pub vim_insert_background: Hsla, - /// Background color for Vim Replace mode indicator. - pub vim_replace_background: Hsla, - /// Background color for Vim Visual mode indicator. - pub vim_visual_background: Hsla, - /// Background color for Vim Visual Line mode indicator. - pub vim_visual_line_background: Hsla, - /// Background color for Vim Visual Block mode indicator. - pub vim_visual_block_background: Hsla, - /// Background color for Vim Helix Normal mode indicator. - pub vim_helix_normal_background: Hsla, - /// Background color for Vim Helix Select mode indicator. - pub vim_helix_select_background: Hsla, - /// Text color for Vim mode indicator label. - pub vim_mode_text: Hsla, - - // === - // Editor - // === - pub editor_foreground: Hsla, - pub editor_background: Hsla, - pub editor_gutter_background: Hsla, - pub editor_subheader_background: Hsla, - pub editor_active_line_background: Hsla, - pub editor_highlighted_line_background: Hsla, - /// Line color of the line a debugger is currently stopped at - pub editor_debugger_active_line_background: Hsla, - /// Text Color. Used for the text of the line number in the editor gutter. - pub editor_line_number: Hsla, - /// Text Color. Used for the text of the line number in the editor gutter when the line is highlighted. - pub editor_active_line_number: Hsla, - /// Text Color. Used for the text of the line number in the editor gutter when the line is hovered over. - pub editor_hover_line_number: Hsla, - /// Text Color. Used to mark invisible characters in the editor. - /// - /// Example: spaces, tabs, carriage returns, etc. - pub editor_invisible: Hsla, - pub editor_wrap_guide: Hsla, - pub editor_active_wrap_guide: Hsla, - pub editor_indent_guide: Hsla, - pub editor_indent_guide_active: Hsla, - /// Read-access of a symbol, like reading a variable. - /// - /// A document highlight is a range inside a text document which deserves - /// special attention. Usually a document highlight is visualized by changing - /// the background color of its range. - pub editor_document_highlight_read_background: Hsla, - /// Read-access of a symbol, like reading a variable. - /// - /// A document highlight is a range inside a text document which deserves - /// special attention. Usually a document highlight is visualized by changing - /// the background color of its range. - pub editor_document_highlight_write_background: Hsla, - /// Highlighted brackets background color. - /// - /// Matching brackets in the cursor scope are highlighted with this background color. - pub editor_document_highlight_bracket_background: Hsla, - - // === - // Terminal - // === - /// Terminal layout background color. - pub terminal_background: Hsla, - /// Terminal foreground color. - pub terminal_foreground: Hsla, - /// Bright terminal foreground color. - pub terminal_bright_foreground: Hsla, - /// Dim terminal foreground color. - pub terminal_dim_foreground: Hsla, - /// Terminal ANSI background color. - pub terminal_ansi_background: Hsla, - /// Black ANSI terminal color. - pub terminal_ansi_black: Hsla, - /// Bright black ANSI terminal color. - pub terminal_ansi_bright_black: Hsla, - /// Dim black ANSI terminal color. - pub terminal_ansi_dim_black: Hsla, - /// Red ANSI terminal color. - pub terminal_ansi_red: Hsla, - /// Bright red ANSI terminal color. - pub terminal_ansi_bright_red: Hsla, - /// Dim red ANSI terminal color. - pub terminal_ansi_dim_red: Hsla, - /// Green ANSI terminal color. - pub terminal_ansi_green: Hsla, - /// Bright green ANSI terminal color. - pub terminal_ansi_bright_green: Hsla, - /// Dim green ANSI terminal color. - pub terminal_ansi_dim_green: Hsla, - /// Yellow ANSI terminal color. - pub terminal_ansi_yellow: Hsla, - /// Bright yellow ANSI terminal color. - pub terminal_ansi_bright_yellow: Hsla, - /// Dim yellow ANSI terminal color. - pub terminal_ansi_dim_yellow: Hsla, - /// Blue ANSI terminal color. - pub terminal_ansi_blue: Hsla, - /// Bright blue ANSI terminal color. - pub terminal_ansi_bright_blue: Hsla, - /// Dim blue ANSI terminal color. - pub terminal_ansi_dim_blue: Hsla, - /// Magenta ANSI terminal color. - pub terminal_ansi_magenta: Hsla, - /// Bright magenta ANSI terminal color. - pub terminal_ansi_bright_magenta: Hsla, - /// Dim magenta ANSI terminal color. - pub terminal_ansi_dim_magenta: Hsla, - /// Cyan ANSI terminal color. - pub terminal_ansi_cyan: Hsla, - /// Bright cyan ANSI terminal color. - pub terminal_ansi_bright_cyan: Hsla, - /// Dim cyan ANSI terminal color. - pub terminal_ansi_dim_cyan: Hsla, - /// White ANSI terminal color. - pub terminal_ansi_white: Hsla, - /// Bright white ANSI terminal color. - pub terminal_ansi_bright_white: Hsla, - /// Dim white ANSI terminal color. - pub terminal_ansi_dim_white: Hsla, - - /// Represents a link text hover color. - pub link_text_hover: Hsla, - - /// Represents an added entry or hunk in vcs, like git. - pub version_control_added: Hsla, - /// Represents a deleted entry in version control systems. - pub version_control_deleted: Hsla, - /// Represents a modified entry in version control systems. - pub version_control_modified: Hsla, - /// Represents a renamed entry in version control systems. - pub version_control_renamed: Hsla, - /// Represents a conflicting entry in version control systems. - pub version_control_conflict: Hsla, - /// Represents an ignored entry in version control systems. - pub version_control_ignored: Hsla, - /// Represents an added word in a word diff. - pub version_control_word_added: Hsla, - /// Represents a deleted word in a word diff. - pub version_control_word_deleted: Hsla, - /// Represents the "ours" region of a merge conflict. - pub version_control_conflict_marker_ours: Hsla, - /// Represents the "theirs" region of a merge conflict. - pub version_control_conflict_marker_theirs: Hsla, -} - -#[derive(EnumIter, Debug, Clone, Copy, AsRefStr)] -#[strum(serialize_all = "snake_case")] -pub enum ThemeColorField { - Border, - BorderVariant, - BorderFocused, - BorderSelected, - BorderTransparent, - BorderDisabled, - ElevatedSurfaceBackground, - SurfaceBackground, - Background, - ElementBackground, - ElementHover, - ElementActive, - ElementSelected, - ElementDisabled, - DropTargetBackground, - DropTargetBorder, - GhostElementBackground, - GhostElementHover, - GhostElementActive, - GhostElementSelected, - GhostElementDisabled, - Text, - TextMuted, - TextPlaceholder, - TextDisabled, - TextAccent, - Icon, - IconMuted, - IconDisabled, - IconPlaceholder, - IconAccent, - StatusBarBackground, - TitleBarBackground, - TitleBarInactiveBackground, - ToolbarBackground, - TabBarBackground, - TabInactiveBackground, - TabActiveBackground, - SearchMatchBackground, - SearchActiveMatchBackground, - PanelBackground, - PanelFocusedBorder, - PanelIndentGuide, - PanelIndentGuideHover, - PanelIndentGuideActive, - PanelOverlayBackground, - PanelOverlayHover, - PaneFocusedBorder, - PaneGroupBorder, - ScrollbarThumbBackground, - ScrollbarThumbHoverBackground, - ScrollbarThumbActiveBackground, - ScrollbarThumbBorder, - ScrollbarTrackBackground, - ScrollbarTrackBorder, - MinimapThumbBackground, - MinimapThumbHoverBackground, - MinimapThumbActiveBackground, - MinimapThumbBorder, - EditorForeground, - EditorBackground, - EditorGutterBackground, - EditorSubheaderBackground, - EditorActiveLineBackground, - EditorHighlightedLineBackground, - EditorLineNumber, - EditorActiveLineNumber, - EditorInvisible, - EditorWrapGuide, - EditorActiveWrapGuide, - EditorIndentGuide, - EditorIndentGuideActive, - EditorDocumentHighlightReadBackground, - EditorDocumentHighlightWriteBackground, - EditorDocumentHighlightBracketBackground, - TerminalBackground, - TerminalForeground, - TerminalBrightForeground, - TerminalDimForeground, - TerminalAnsiBackground, - TerminalAnsiBlack, - TerminalAnsiBrightBlack, - TerminalAnsiDimBlack, - TerminalAnsiRed, - TerminalAnsiBrightRed, - TerminalAnsiDimRed, - TerminalAnsiGreen, - TerminalAnsiBrightGreen, - TerminalAnsiDimGreen, - TerminalAnsiYellow, - TerminalAnsiBrightYellow, - TerminalAnsiDimYellow, - TerminalAnsiBlue, - TerminalAnsiBrightBlue, - TerminalAnsiDimBlue, - TerminalAnsiMagenta, - TerminalAnsiBrightMagenta, - TerminalAnsiDimMagenta, - TerminalAnsiCyan, - TerminalAnsiBrightCyan, - TerminalAnsiDimCyan, - TerminalAnsiWhite, - TerminalAnsiBrightWhite, - TerminalAnsiDimWhite, - LinkTextHover, - VersionControlAdded, - VersionControlDeleted, - VersionControlModified, - VersionControlRenamed, - VersionControlConflict, - VersionControlIgnored, -} - -impl ThemeColors { - pub fn color(&self, field: ThemeColorField) -> Hsla { - match field { - ThemeColorField::Border => self.border, - ThemeColorField::BorderVariant => self.border_variant, - ThemeColorField::BorderFocused => self.border_focused, - ThemeColorField::BorderSelected => self.border_selected, - ThemeColorField::BorderTransparent => self.border_transparent, - ThemeColorField::BorderDisabled => self.border_disabled, - ThemeColorField::ElevatedSurfaceBackground => self.elevated_surface_background, - ThemeColorField::SurfaceBackground => self.surface_background, - ThemeColorField::Background => self.background, - ThemeColorField::ElementBackground => self.element_background, - ThemeColorField::ElementHover => self.element_hover, - ThemeColorField::ElementActive => self.element_active, - ThemeColorField::ElementSelected => self.element_selected, - ThemeColorField::ElementDisabled => self.element_disabled, - ThemeColorField::DropTargetBackground => self.drop_target_background, - ThemeColorField::DropTargetBorder => self.drop_target_border, - ThemeColorField::GhostElementBackground => self.ghost_element_background, - ThemeColorField::GhostElementHover => self.ghost_element_hover, - ThemeColorField::GhostElementActive => self.ghost_element_active, - ThemeColorField::GhostElementSelected => self.ghost_element_selected, - ThemeColorField::GhostElementDisabled => self.ghost_element_disabled, - ThemeColorField::Text => self.text, - ThemeColorField::TextMuted => self.text_muted, - ThemeColorField::TextPlaceholder => self.text_placeholder, - ThemeColorField::TextDisabled => self.text_disabled, - ThemeColorField::TextAccent => self.text_accent, - ThemeColorField::Icon => self.icon, - ThemeColorField::IconMuted => self.icon_muted, - ThemeColorField::IconDisabled => self.icon_disabled, - ThemeColorField::IconPlaceholder => self.icon_placeholder, - ThemeColorField::IconAccent => self.icon_accent, - ThemeColorField::StatusBarBackground => self.status_bar_background, - ThemeColorField::TitleBarBackground => self.title_bar_background, - ThemeColorField::TitleBarInactiveBackground => self.title_bar_inactive_background, - ThemeColorField::ToolbarBackground => self.toolbar_background, - ThemeColorField::TabBarBackground => self.tab_bar_background, - ThemeColorField::TabInactiveBackground => self.tab_inactive_background, - ThemeColorField::TabActiveBackground => self.tab_active_background, - ThemeColorField::SearchMatchBackground => self.search_match_background, - ThemeColorField::SearchActiveMatchBackground => self.search_active_match_background, - ThemeColorField::PanelBackground => self.panel_background, - ThemeColorField::PanelFocusedBorder => self.panel_focused_border, - ThemeColorField::PanelIndentGuide => self.panel_indent_guide, - ThemeColorField::PanelIndentGuideHover => self.panel_indent_guide_hover, - ThemeColorField::PanelIndentGuideActive => self.panel_indent_guide_active, - ThemeColorField::PanelOverlayBackground => self.panel_overlay_background, - ThemeColorField::PanelOverlayHover => self.panel_overlay_hover, - ThemeColorField::PaneFocusedBorder => self.pane_focused_border, - ThemeColorField::PaneGroupBorder => self.pane_group_border, - ThemeColorField::ScrollbarThumbBackground => self.scrollbar_thumb_background, - ThemeColorField::ScrollbarThumbHoverBackground => self.scrollbar_thumb_hover_background, - ThemeColorField::ScrollbarThumbActiveBackground => { - self.scrollbar_thumb_active_background - } - ThemeColorField::ScrollbarThumbBorder => self.scrollbar_thumb_border, - ThemeColorField::ScrollbarTrackBackground => self.scrollbar_track_background, - ThemeColorField::ScrollbarTrackBorder => self.scrollbar_track_border, - ThemeColorField::MinimapThumbBackground => self.minimap_thumb_background, - ThemeColorField::MinimapThumbHoverBackground => self.minimap_thumb_hover_background, - ThemeColorField::MinimapThumbActiveBackground => self.minimap_thumb_active_background, - ThemeColorField::MinimapThumbBorder => self.minimap_thumb_border, - ThemeColorField::EditorForeground => self.editor_foreground, - ThemeColorField::EditorBackground => self.editor_background, - ThemeColorField::EditorGutterBackground => self.editor_gutter_background, - ThemeColorField::EditorSubheaderBackground => self.editor_subheader_background, - ThemeColorField::EditorActiveLineBackground => self.editor_active_line_background, - ThemeColorField::EditorHighlightedLineBackground => { - self.editor_highlighted_line_background - } - ThemeColorField::EditorLineNumber => self.editor_line_number, - ThemeColorField::EditorActiveLineNumber => self.editor_active_line_number, - ThemeColorField::EditorInvisible => self.editor_invisible, - ThemeColorField::EditorWrapGuide => self.editor_wrap_guide, - ThemeColorField::EditorActiveWrapGuide => self.editor_active_wrap_guide, - ThemeColorField::EditorIndentGuide => self.editor_indent_guide, - ThemeColorField::EditorIndentGuideActive => self.editor_indent_guide_active, - ThemeColorField::EditorDocumentHighlightReadBackground => { - self.editor_document_highlight_read_background - } - ThemeColorField::EditorDocumentHighlightWriteBackground => { - self.editor_document_highlight_write_background - } - ThemeColorField::EditorDocumentHighlightBracketBackground => { - self.editor_document_highlight_bracket_background - } - ThemeColorField::TerminalBackground => self.terminal_background, - ThemeColorField::TerminalForeground => self.terminal_foreground, - ThemeColorField::TerminalBrightForeground => self.terminal_bright_foreground, - ThemeColorField::TerminalDimForeground => self.terminal_dim_foreground, - ThemeColorField::TerminalAnsiBackground => self.terminal_ansi_background, - ThemeColorField::TerminalAnsiBlack => self.terminal_ansi_black, - ThemeColorField::TerminalAnsiBrightBlack => self.terminal_ansi_bright_black, - ThemeColorField::TerminalAnsiDimBlack => self.terminal_ansi_dim_black, - ThemeColorField::TerminalAnsiRed => self.terminal_ansi_red, - ThemeColorField::TerminalAnsiBrightRed => self.terminal_ansi_bright_red, - ThemeColorField::TerminalAnsiDimRed => self.terminal_ansi_dim_red, - ThemeColorField::TerminalAnsiGreen => self.terminal_ansi_green, - ThemeColorField::TerminalAnsiBrightGreen => self.terminal_ansi_bright_green, - ThemeColorField::TerminalAnsiDimGreen => self.terminal_ansi_dim_green, - ThemeColorField::TerminalAnsiYellow => self.terminal_ansi_yellow, - ThemeColorField::TerminalAnsiBrightYellow => self.terminal_ansi_bright_yellow, - ThemeColorField::TerminalAnsiDimYellow => self.terminal_ansi_dim_yellow, - ThemeColorField::TerminalAnsiBlue => self.terminal_ansi_blue, - ThemeColorField::TerminalAnsiBrightBlue => self.terminal_ansi_bright_blue, - ThemeColorField::TerminalAnsiDimBlue => self.terminal_ansi_dim_blue, - ThemeColorField::TerminalAnsiMagenta => self.terminal_ansi_magenta, - ThemeColorField::TerminalAnsiBrightMagenta => self.terminal_ansi_bright_magenta, - ThemeColorField::TerminalAnsiDimMagenta => self.terminal_ansi_dim_magenta, - ThemeColorField::TerminalAnsiCyan => self.terminal_ansi_cyan, - ThemeColorField::TerminalAnsiBrightCyan => self.terminal_ansi_bright_cyan, - ThemeColorField::TerminalAnsiDimCyan => self.terminal_ansi_dim_cyan, - ThemeColorField::TerminalAnsiWhite => self.terminal_ansi_white, - ThemeColorField::TerminalAnsiBrightWhite => self.terminal_ansi_bright_white, - ThemeColorField::TerminalAnsiDimWhite => self.terminal_ansi_dim_white, - ThemeColorField::LinkTextHover => self.link_text_hover, - ThemeColorField::VersionControlAdded => self.version_control_added, - ThemeColorField::VersionControlDeleted => self.version_control_deleted, - ThemeColorField::VersionControlModified => self.version_control_modified, - ThemeColorField::VersionControlRenamed => self.version_control_renamed, - ThemeColorField::VersionControlConflict => self.version_control_conflict, - ThemeColorField::VersionControlIgnored => self.version_control_ignored, - } - } - - pub fn iter(&self) -> impl Iterator + '_ { - ThemeColorField::iter().map(move |field| (field, self.color(field))) - } - - pub fn to_vec(&self) -> Vec<(ThemeColorField, Hsla)> { - self.iter().collect() - } -} - -pub fn all_theme_colors(cx: &mut App) -> Vec<(Hsla, SharedString)> { - let theme = cx.theme(); - ThemeColorField::iter() - .map(|field| { - let color = theme.colors().color(field); - let name = field.as_ref().to_string(); - (color, SharedString::from(name)) - }) - .collect() -} - -#[derive(Refineable, Clone, Debug, PartialEq)] -pub struct ThemeStyles { - /// The background appearance of the window. - pub window_background_appearance: WindowBackgroundAppearance, - pub system: SystemColors, - /// An array of colors used for theme elements that iterate through a series of colors. - /// - /// Example: Player colors, rainbow brackets and indent guides, etc. - pub accents: AccentColors, - - #[refineable] - pub colors: ThemeColors, - - #[refineable] - pub status: StatusColors, - - pub player: PlayerColors, - - pub syntax: Arc, -} - -#[cfg(test)] -mod tests { - use serde_json::json; - - use super::*; - - #[test] - fn override_a_single_theme_color() { - let mut colors = ThemeColors::light(); - - let magenta: Hsla = gpui::rgb(0xff00ff).into(); - - assert_ne!(colors.text, magenta); - - let overrides = ThemeColorsRefinement { - text: Some(magenta), - ..Default::default() - }; - - colors.refine(&overrides); - - assert_eq!(colors.text, magenta); - } - - #[test] - fn override_multiple_theme_colors() { - let mut colors = ThemeColors::light(); - - let magenta: Hsla = gpui::rgb(0xff00ff).into(); - let green: Hsla = gpui::rgb(0x00ff00).into(); - - assert_ne!(colors.text, magenta); - assert_ne!(colors.background, green); - - let overrides = ThemeColorsRefinement { - text: Some(magenta), - background: Some(green), - ..Default::default() - }; - - colors.refine(&overrides); - - assert_eq!(colors.text, magenta); - assert_eq!(colors.background, green); - } - - #[test] - fn deserialize_theme_colors_refinement_from_json() { - let colors: ThemeColorsRefinement = serde_json::from_value(json!({ - "background": "#ff00ff", - "text": "#ff0000" - })) - .unwrap(); - - assert_eq!(colors.background, Some(gpui::rgb(0xff00ff).into())); - assert_eq!(colors.text, Some(gpui::rgb(0xff0000).into())); - } -} diff --git a/crates/theme/src/styles/players.rs b/crates/theme/src/styles/players.rs deleted file mode 100644 index 439dbdd437..0000000000 --- a/crates/theme/src/styles/players.rs +++ /dev/null @@ -1,187 +0,0 @@ -#![allow(missing_docs)] - -use gpui::Hsla; -use serde::Deserialize; - -use crate::{amber, blue, jade, lime, orange, pink, purple, red, try_parse_color}; - -#[derive(Debug, Clone, Copy, Deserialize, Default, PartialEq)] -pub struct PlayerColor { - pub cursor: Hsla, - pub background: Hsla, - pub selection: Hsla, -} - -/// A collection of colors that are used to color players in the editor. -/// -/// The first color is always the local player's color, usually a blue. -/// -/// The rest of the default colors crisscross back and forth on the -/// color wheel so that the colors are as distinct as possible. -#[derive(Clone, Debug, Deserialize, PartialEq)] -pub struct PlayerColors(pub Vec); - -impl Default for PlayerColors { - /// Don't use this! - /// We have to have a default to be `[refineable::Refinable]`. - /// TODO "Find a way to not need this for Refinable" - fn default() -> Self { - Self::dark() - } -} - -impl PlayerColors { - pub fn dark() -> Self { - Self(vec![ - PlayerColor { - cursor: blue().dark().step_9(), - background: blue().dark().step_5(), - selection: blue().dark().step_3(), - }, - PlayerColor { - cursor: orange().dark().step_9(), - background: orange().dark().step_5(), - selection: orange().dark().step_3(), - }, - PlayerColor { - cursor: pink().dark().step_9(), - background: pink().dark().step_5(), - selection: pink().dark().step_3(), - }, - PlayerColor { - cursor: lime().dark().step_9(), - background: lime().dark().step_5(), - selection: lime().dark().step_3(), - }, - PlayerColor { - cursor: purple().dark().step_9(), - background: purple().dark().step_5(), - selection: purple().dark().step_3(), - }, - PlayerColor { - cursor: amber().dark().step_9(), - background: amber().dark().step_5(), - selection: amber().dark().step_3(), - }, - PlayerColor { - cursor: jade().dark().step_9(), - background: jade().dark().step_5(), - selection: jade().dark().step_3(), - }, - PlayerColor { - cursor: red().dark().step_9(), - background: red().dark().step_5(), - selection: red().dark().step_3(), - }, - ]) - } - - pub fn light() -> Self { - Self(vec![ - PlayerColor { - cursor: blue().light().step_9(), - background: blue().light().step_4(), - selection: blue().light().step_3(), - }, - PlayerColor { - cursor: orange().light().step_9(), - background: orange().light().step_4(), - selection: orange().light().step_3(), - }, - PlayerColor { - cursor: pink().light().step_9(), - background: pink().light().step_4(), - selection: pink().light().step_3(), - }, - PlayerColor { - cursor: lime().light().step_9(), - background: lime().light().step_4(), - selection: lime().light().step_3(), - }, - PlayerColor { - cursor: purple().light().step_9(), - background: purple().light().step_4(), - selection: purple().light().step_3(), - }, - PlayerColor { - cursor: amber().light().step_9(), - background: amber().light().step_4(), - selection: amber().light().step_3(), - }, - PlayerColor { - cursor: jade().light().step_9(), - background: jade().light().step_4(), - selection: jade().light().step_3(), - }, - PlayerColor { - cursor: red().light().step_9(), - background: red().light().step_4(), - selection: red().light().step_3(), - }, - ]) - } -} - -impl PlayerColors { - pub fn local(&self) -> PlayerColor { - *self.0.first().unwrap() - } - - pub fn agent(&self) -> PlayerColor { - *self.0.last().unwrap() - } - - pub fn absent(&self) -> PlayerColor { - *self.0.last().unwrap() - } - - pub fn read_only(&self) -> PlayerColor { - let local = self.local(); - PlayerColor { - cursor: local.cursor.grayscale(), - background: local.background.grayscale(), - selection: local.selection.grayscale(), - } - } - - pub fn color_for_participant(&self, participant_index: u32) -> PlayerColor { - let len = self.0.len() - 1; - self.0[(participant_index as usize % len) + 1] - } - - /// Merges the given player colors into this [`PlayerColors`] instance. - pub fn merge(&mut self, user_player_colors: &[settings::PlayerColorContent]) { - if user_player_colors.is_empty() { - return; - } - - for (idx, player) in user_player_colors.iter().enumerate() { - let cursor = player - .cursor - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let background = player - .background - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let selection = player - .selection - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - - if let Some(player_color) = self.0.get_mut(idx) { - *player_color = PlayerColor { - cursor: cursor.unwrap_or(player_color.cursor), - background: background.unwrap_or(player_color.background), - selection: selection.unwrap_or(player_color.selection), - }; - } else { - self.0.push(PlayerColor { - cursor: cursor.unwrap_or_default(), - background: background.unwrap_or_default(), - selection: selection.unwrap_or_default(), - }); - } - } - } -} diff --git a/crates/theme/src/styles/status.rs b/crates/theme/src/styles/status.rs deleted file mode 100644 index ab333ecb3e..0000000000 --- a/crates/theme/src/styles/status.rs +++ /dev/null @@ -1,191 +0,0 @@ -#![allow(missing_docs)] - -use gpui::Hsla; -use refineable::Refineable; - -use crate::{blue, grass, neutral, red, yellow}; - -#[derive(Refineable, Clone, Debug, PartialEq)] -#[refineable(Debug, serde::Deserialize)] -pub struct StatusColors { - /// Indicates some kind of conflict, like a file changed on disk while it was open, or - /// merge conflicts in a Git repository. - pub conflict: Hsla, - pub conflict_background: Hsla, - pub conflict_border: Hsla, - - /// Indicates something new, like a new file added to a Git repository. - pub created: Hsla, - pub created_background: Hsla, - pub created_border: Hsla, - - /// Indicates that something no longer exists, like a deleted file. - pub deleted: Hsla, - pub deleted_background: Hsla, - pub deleted_border: Hsla, - - /// Indicates a system error, a failed operation or a diagnostic error. - pub error: Hsla, - pub error_background: Hsla, - pub error_border: Hsla, - - /// Represents a hidden status, such as a file being hidden in a file tree. - pub hidden: Hsla, - pub hidden_background: Hsla, - pub hidden_border: Hsla, - - /// Indicates a hint or some kind of additional information. - pub hint: Hsla, - pub hint_background: Hsla, - pub hint_border: Hsla, - - /// Indicates that something is deliberately ignored, such as a file or operation ignored by Git. - pub ignored: Hsla, - pub ignored_background: Hsla, - pub ignored_border: Hsla, - - /// Represents informational status updates or messages. - pub info: Hsla, - pub info_background: Hsla, - pub info_border: Hsla, - - /// Indicates a changed or altered status, like a file that has been edited. - pub modified: Hsla, - pub modified_background: Hsla, - pub modified_border: Hsla, - - /// Indicates something that is predicted, like automatic code completion, or generated code. - pub predictive: Hsla, - pub predictive_background: Hsla, - pub predictive_border: Hsla, - - /// Represents a renamed status, such as a file that has been renamed. - pub renamed: Hsla, - pub renamed_background: Hsla, - pub renamed_border: Hsla, - - /// Indicates a successful operation or task completion. - pub success: Hsla, - pub success_background: Hsla, - pub success_border: Hsla, - - /// Indicates some kind of unreachable status, like a block of code that can never be reached. - pub unreachable: Hsla, - pub unreachable_background: Hsla, - pub unreachable_border: Hsla, - - /// Represents a warning status, like an operation that is about to fail. - pub warning: Hsla, - pub warning_background: Hsla, - pub warning_border: Hsla, -} - -pub struct DiagnosticColors { - pub error: Hsla, - pub warning: Hsla, - pub info: Hsla, -} - -impl StatusColors { - pub fn dark() -> Self { - Self { - conflict: red().dark().step_9(), - conflict_background: red().dark().step_9(), - conflict_border: red().dark().step_9(), - created: grass().dark().step_9(), - created_background: grass().dark().step_9().opacity(0.25), - created_border: grass().dark().step_9(), - deleted: red().dark().step_9(), - deleted_background: red().dark().step_9().opacity(0.25), - deleted_border: red().dark().step_9(), - error: red().dark().step_9(), - error_background: red().dark().step_9(), - error_border: red().dark().step_9(), - hidden: neutral().dark().step_9(), - hidden_background: neutral().dark().step_9(), - hidden_border: neutral().dark().step_9(), - hint: blue().dark().step_9(), - hint_background: blue().dark().step_9(), - hint_border: blue().dark().step_9(), - ignored: neutral().dark().step_9(), - ignored_background: neutral().dark().step_9(), - ignored_border: neutral().dark().step_9(), - info: blue().dark().step_9(), - info_background: blue().dark().step_9(), - info_border: blue().dark().step_9(), - modified: yellow().dark().step_9(), - modified_background: yellow().dark().step_9().opacity(0.25), - modified_border: yellow().dark().step_9(), - predictive: neutral().dark_alpha().step_9(), - predictive_background: neutral().dark_alpha().step_9(), - predictive_border: neutral().dark_alpha().step_9(), - renamed: blue().dark().step_9(), - renamed_background: blue().dark().step_9(), - renamed_border: blue().dark().step_9(), - success: grass().dark().step_9(), - success_background: grass().dark().step_9(), - success_border: grass().dark().step_9(), - unreachable: neutral().dark().step_10(), - unreachable_background: neutral().dark().step_10(), - unreachable_border: neutral().dark().step_10(), - warning: yellow().dark().step_9(), - warning_background: yellow().dark().step_9(), - warning_border: yellow().dark().step_9(), - } - } - - pub fn light() -> Self { - Self { - conflict: red().light().step_9(), - conflict_background: red().light().step_9(), - conflict_border: red().light().step_9(), - created: grass().light().step_9(), - created_background: grass().light().step_9(), - created_border: grass().light().step_9(), - deleted: red().light().step_9(), - deleted_background: red().light().step_9(), - deleted_border: red().light().step_9(), - error: red().light().step_9(), - error_background: red().light().step_9(), - error_border: red().light().step_9(), - hidden: neutral().light().step_9(), - hidden_background: neutral().light().step_9(), - hidden_border: neutral().light().step_9(), - hint: blue().light().step_9(), - hint_background: blue().light().step_9(), - hint_border: blue().light().step_9(), - ignored: neutral().light().step_9(), - ignored_background: neutral().light().step_9(), - ignored_border: neutral().light().step_9(), - info: blue().light().step_9(), - info_background: blue().light().step_9(), - info_border: blue().light().step_9(), - modified: yellow().light().step_9(), - modified_background: yellow().light().step_9(), - modified_border: yellow().light().step_9(), - predictive: neutral().light_alpha().step_9(), - predictive_background: neutral().light_alpha().step_9(), - predictive_border: neutral().light_alpha().step_9(), - renamed: blue().light().step_9(), - renamed_background: blue().light().step_9(), - renamed_border: blue().light().step_9(), - success: grass().light().step_9(), - success_background: grass().light().step_9(), - success_border: grass().light().step_9(), - unreachable: neutral().light().step_10(), - unreachable_background: neutral().light().step_10(), - unreachable_border: neutral().light().step_10(), - warning: yellow().light().step_9(), - warning_background: yellow().light().step_9(), - warning_border: yellow().light().step_9(), - } - } - - pub fn diagnostic(&self) -> DiagnosticColors { - DiagnosticColors { - error: self.error, - warning: self.warning, - info: self.info, - } - } -} diff --git a/crates/theme/src/styles/syntax.rs b/crates/theme/src/styles/syntax.rs deleted file mode 100644 index 0a97ff77f2..0000000000 --- a/crates/theme/src/styles/syntax.rs +++ /dev/null @@ -1,197 +0,0 @@ -#![allow(missing_docs)] - -use std::sync::Arc; - -use gpui::{HighlightStyle, Hsla}; - -#[derive(Debug, PartialEq, Eq, Clone, Default)] -pub struct SyntaxTheme { - pub highlights: Vec<(String, HighlightStyle)>, -} - -impl SyntaxTheme { - #[cfg(any(test, feature = "test-support"))] - pub fn new_test(colors: impl IntoIterator) -> Self { - Self::new_test_styles(colors.into_iter().map(|(key, color)| { - ( - key, - HighlightStyle { - color: Some(color), - ..Default::default() - }, - ) - })) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn new_test_styles( - colors: impl IntoIterator, - ) -> Self { - Self { - highlights: colors - .into_iter() - .map(|(key, style)| (key.to_owned(), style)) - .collect(), - } - } - - pub fn get(&self, name: &str) -> HighlightStyle { - self.highlights - .iter() - .find_map(|entry| if entry.0 == name { Some(entry.1) } else { None }) - .unwrap_or_default() - } - - pub fn color(&self, name: &str) -> Hsla { - self.get(name).color.unwrap_or_default() - } - - pub fn highlight_id(&self, name: &str) -> Option { - let ix = self.highlights.iter().position(|entry| entry.0 == name)?; - Some(ix as u32) - } - - /// Returns a new [`Arc`] with the given syntax styles merged in. - pub fn merge(base: Arc, user_syntax_styles: Vec<(String, HighlightStyle)>) -> Arc { - if user_syntax_styles.is_empty() { - return base; - } - - let mut merged_highlights = base.highlights.clone(); - - for (name, highlight) in user_syntax_styles { - if let Some((_, existing_highlight)) = merged_highlights - .iter_mut() - .find(|(existing_name, _)| existing_name == &name) - { - existing_highlight.color = highlight.color.or(existing_highlight.color); - existing_highlight.font_weight = - highlight.font_weight.or(existing_highlight.font_weight); - existing_highlight.font_style = - highlight.font_style.or(existing_highlight.font_style); - existing_highlight.background_color = highlight - .background_color - .or(existing_highlight.background_color); - existing_highlight.underline = highlight.underline.or(existing_highlight.underline); - existing_highlight.strikethrough = - highlight.strikethrough.or(existing_highlight.strikethrough); - existing_highlight.fade_out = highlight.fade_out.or(existing_highlight.fade_out); - } else { - merged_highlights.push((name, highlight)); - } - } - - Arc::new(Self { - highlights: merged_highlights, - }) - } -} - -#[cfg(test)] -mod tests { - use gpui::FontStyle; - - use super::*; - - #[test] - fn test_syntax_theme_merge() { - // Merging into an empty `SyntaxTheme` keeps all the user-defined styles. - let syntax_theme = SyntaxTheme::merge( - Arc::new(SyntaxTheme::new_test([])), - vec![ - ( - "foo".to_string(), - HighlightStyle { - color: Some(gpui::red()), - ..Default::default() - }, - ), - ( - "foo.bar".to_string(), - HighlightStyle { - color: Some(gpui::green()), - ..Default::default() - }, - ), - ], - ); - assert_eq!( - syntax_theme, - Arc::new(SyntaxTheme::new_test([ - ("foo", gpui::red()), - ("foo.bar", gpui::green()) - ])) - ); - - // Merging empty user-defined styles keeps all the base styles. - let syntax_theme = SyntaxTheme::merge( - Arc::new(SyntaxTheme::new_test([ - ("foo", gpui::blue()), - ("foo.bar", gpui::red()), - ])), - Vec::new(), - ); - assert_eq!( - syntax_theme, - Arc::new(SyntaxTheme::new_test([ - ("foo", gpui::blue()), - ("foo.bar", gpui::red()) - ])) - ); - - let syntax_theme = SyntaxTheme::merge( - Arc::new(SyntaxTheme::new_test([ - ("foo", gpui::red()), - ("foo.bar", gpui::green()), - ])), - vec![( - "foo.bar".to_string(), - HighlightStyle { - color: Some(gpui::yellow()), - ..Default::default() - }, - )], - ); - assert_eq!( - syntax_theme, - Arc::new(SyntaxTheme::new_test([ - ("foo", gpui::red()), - ("foo.bar", gpui::yellow()) - ])) - ); - - let syntax_theme = SyntaxTheme::merge( - Arc::new(SyntaxTheme::new_test([ - ("foo", gpui::red()), - ("foo.bar", gpui::green()), - ])), - vec![( - "foo.bar".to_string(), - HighlightStyle { - font_style: Some(FontStyle::Italic), - ..Default::default() - }, - )], - ); - assert_eq!( - syntax_theme, - Arc::new(SyntaxTheme::new_test_styles([ - ( - "foo", - HighlightStyle { - color: Some(gpui::red()), - ..Default::default() - } - ), - ( - "foo.bar", - HighlightStyle { - color: Some(gpui::green()), - font_style: Some(FontStyle::Italic), - ..Default::default() - } - ) - ])) - ); - } -} diff --git a/crates/theme/src/styles/system.rs b/crates/theme/src/styles/system.rs deleted file mode 100644 index 676577bfb4..0000000000 --- a/crates/theme/src/styles/system.rs +++ /dev/null @@ -1,22 +0,0 @@ -#![allow(missing_docs)] - -use gpui::{Hsla, hsla}; - -#[derive(Clone, Debug, PartialEq)] -pub struct SystemColors { - pub transparent: Hsla, - pub mac_os_traffic_light_red: Hsla, - pub mac_os_traffic_light_yellow: Hsla, - pub mac_os_traffic_light_green: Hsla, -} - -impl Default for SystemColors { - fn default() -> Self { - Self { - transparent: hsla(0.0, 0.0, 0.0, 0.0), - mac_os_traffic_light_red: hsla(0.0139, 0.79, 0.65, 1.0), - mac_os_traffic_light_yellow: hsla(0.114, 0.88, 0.63, 1.0), - mac_os_traffic_light_green: hsla(0.313, 0.49, 0.55, 1.0), - } - } -} diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs deleted file mode 100644 index c94e0d60bf..0000000000 --- a/crates/theme/src/theme.rs +++ /dev/null @@ -1,527 +0,0 @@ -#![deny(missing_docs)] - -//! # Theme -//! -//! This crate provides the theme system for Zed. -//! -//! ## Overview -//! -//! A theme is a collection of colors used to build a consistent appearance for UI components across the application. - -mod default_colors; -mod fallback_themes; -mod font_family_cache; -mod icon_theme; -mod icon_theme_schema; -mod registry; -mod scale; -mod schema; -mod settings; -mod styles; - -use std::path::Path; -use std::sync::Arc; - -use ::settings::Settings; -use ::settings::SettingsStore; -use anyhow::Result; -use fallback_themes::apply_status_color_defaults; -use fs::Fs; -use gpui::BorrowAppContext; -use gpui::Global; -use gpui::{ - App, AssetSource, HighlightStyle, Hsla, Pixels, Refineable, SharedString, WindowAppearance, - WindowBackgroundAppearance, px, -}; -use serde::Deserialize; -use uuid::Uuid; - -pub use crate::default_colors::*; -use crate::fallback_themes::apply_theme_color_defaults; -pub use crate::font_family_cache::*; -pub use crate::icon_theme::*; -pub use crate::icon_theme_schema::*; -pub use crate::registry::*; -pub use crate::scale::*; -pub use crate::schema::*; -pub use crate::settings::*; -pub use crate::styles::*; -pub use ::settings::{ - FontStyleContent, HighlightStyleContent, StatusColorsContent, ThemeColorsContent, - ThemeStyleContent, -}; - -/// Defines window border radius for platforms that use client side decorations. -pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0); -/// Defines window shadow size for platforms that use client side decorations. -pub const CLIENT_SIDE_DECORATION_SHADOW: Pixels = px(10.0); - -/// The appearance of the theme. -#[derive(Debug, PartialEq, Clone, Copy, Deserialize)] -pub enum Appearance { - /// A light appearance. - Light, - /// A dark appearance. - Dark, -} - -impl Appearance { - /// Returns whether the appearance is light. - pub fn is_light(&self) -> bool { - match self { - Self::Light => true, - Self::Dark => false, - } - } -} - -impl From for Appearance { - fn from(value: WindowAppearance) -> Self { - match value { - WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark, - WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light, - } - } -} - -impl From for ThemeAppearanceMode { - fn from(value: Appearance) -> Self { - match value { - Appearance::Light => Self::Light, - Appearance::Dark => Self::Dark, - } - } -} - -/// Which themes should be loaded. This is used primarily for testing. -pub enum LoadThemes { - /// Only load the base theme. - /// - /// No user themes will be loaded. - JustBase, - - /// Load all of the built-in themes. - All(Box), -} - -/// Initialize the theme system. -pub fn init(themes_to_load: LoadThemes, cx: &mut App) { - SystemAppearance::init(cx); - let (assets, load_user_themes) = match themes_to_load { - LoadThemes::JustBase => (Box::new(()) as Box, false), - LoadThemes::All(assets) => (assets, true), - }; - ThemeRegistry::set_global(assets, cx); - - if load_user_themes { - ThemeRegistry::global(cx).load_bundled_themes(); - } - - FontFamilyCache::init_global(cx); - - let theme = GlobalTheme::configured_theme(cx); - let icon_theme = GlobalTheme::configured_icon_theme(cx); - cx.set_global(GlobalTheme { theme, icon_theme }); - - let settings = ThemeSettings::get_global(cx); - - let mut prev_buffer_font_size_settings = settings.buffer_font_size_settings(); - let mut prev_ui_font_size_settings = settings.ui_font_size_settings(); - let mut prev_agent_ui_font_size_settings = settings.agent_ui_font_size_settings(); - let mut prev_agent_buffer_font_size_settings = settings.agent_buffer_font_size_settings(); - let mut prev_theme_name = settings.theme.name(SystemAppearance::global(cx).0); - let mut prev_icon_theme_name = settings.icon_theme.name(SystemAppearance::global(cx).0); - let mut prev_theme_overrides = ( - settings.experimental_theme_overrides.clone(), - settings.theme_overrides.clone(), - ); - - cx.observe_global::(move |cx| { - let settings = ThemeSettings::get_global(cx); - - let buffer_font_size_settings = settings.buffer_font_size_settings(); - let ui_font_size_settings = settings.ui_font_size_settings(); - let agent_ui_font_size_settings = settings.agent_ui_font_size_settings(); - let agent_buffer_font_size_settings = settings.agent_buffer_font_size_settings(); - let theme_name = settings.theme.name(SystemAppearance::global(cx).0); - let icon_theme_name = settings.icon_theme.name(SystemAppearance::global(cx).0); - let theme_overrides = ( - settings.experimental_theme_overrides.clone(), - settings.theme_overrides.clone(), - ); - - if buffer_font_size_settings != prev_buffer_font_size_settings { - prev_buffer_font_size_settings = buffer_font_size_settings; - reset_buffer_font_size(cx); - } - - if ui_font_size_settings != prev_ui_font_size_settings { - prev_ui_font_size_settings = ui_font_size_settings; - reset_ui_font_size(cx); - } - - if agent_ui_font_size_settings != prev_agent_ui_font_size_settings { - prev_agent_ui_font_size_settings = agent_ui_font_size_settings; - reset_agent_ui_font_size(cx); - } - - if agent_buffer_font_size_settings != prev_agent_buffer_font_size_settings { - prev_agent_buffer_font_size_settings = agent_buffer_font_size_settings; - reset_agent_buffer_font_size(cx); - } - - if theme_name != prev_theme_name || theme_overrides != prev_theme_overrides { - prev_theme_name = theme_name; - prev_theme_overrides = theme_overrides; - GlobalTheme::reload_theme(cx); - } - - if icon_theme_name != prev_icon_theme_name { - prev_icon_theme_name = icon_theme_name; - GlobalTheme::reload_icon_theme(cx); - } - }) - .detach(); -} - -/// Implementing this trait allows accessing the active theme. -pub trait ActiveTheme { - /// Returns the active theme. - fn theme(&self) -> &Arc; -} - -impl ActiveTheme for App { - fn theme(&self) -> &Arc { - GlobalTheme::theme(self) - } -} - -/// A theme family is a grouping of themes under a single name. -/// -/// For example, the "One" theme family contains the "One Light" and "One Dark" themes. -/// -/// It can also be used to package themes with many variants. -/// -/// For example, the "Atelier" theme family contains "Cave", "Dune", "Estuary", "Forest", "Heath", etc. -pub struct ThemeFamily { - /// The unique identifier for the theme family. - pub id: String, - /// The name of the theme family. This will be displayed in the UI, such as when adding or removing a theme family. - pub name: SharedString, - /// The author of the theme family. - pub author: SharedString, - /// The [Theme]s in the family. - pub themes: Vec, - /// The color scales used by the themes in the family. - /// Note: This will be removed in the future. - pub scales: ColorScales, -} - -impl ThemeFamily { - // This is on ThemeFamily because we will have variables here we will need - // in the future to resolve @references. - /// Refines ThemeContent into a theme, merging it's contents with the base theme. - pub fn refine_theme(&self, theme: &ThemeContent) -> Theme { - let appearance = match theme.appearance { - AppearanceContent::Light => Appearance::Light, - AppearanceContent::Dark => Appearance::Dark, - }; - - let mut refined_status_colors = match theme.appearance { - AppearanceContent::Light => StatusColors::light(), - AppearanceContent::Dark => StatusColors::dark(), - }; - let mut status_colors_refinement = status_colors_refinement(&theme.style.status); - apply_status_color_defaults(&mut status_colors_refinement); - refined_status_colors.refine(&status_colors_refinement); - - let mut refined_player_colors = match theme.appearance { - AppearanceContent::Light => PlayerColors::light(), - AppearanceContent::Dark => PlayerColors::dark(), - }; - refined_player_colors.merge(&theme.style.players); - - let mut refined_theme_colors = match theme.appearance { - AppearanceContent::Light => ThemeColors::light(), - AppearanceContent::Dark => ThemeColors::dark(), - }; - let mut theme_colors_refinement = - theme_colors_refinement(&theme.style.colors, &status_colors_refinement); - apply_theme_color_defaults(&mut theme_colors_refinement, &refined_player_colors); - refined_theme_colors.refine(&theme_colors_refinement); - - let mut refined_accent_colors = match theme.appearance { - AppearanceContent::Light => AccentColors::light(), - AppearanceContent::Dark => AccentColors::dark(), - }; - refined_accent_colors.merge(&theme.style.accents); - - let syntax_highlights = theme - .style - .syntax - .iter() - .map(|(syntax_token, highlight)| { - ( - syntax_token.clone(), - HighlightStyle { - color: highlight - .color - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - background_color: highlight - .background_color - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - font_style: highlight.font_style.map(Into::into), - font_weight: highlight.font_weight.map(Into::into), - ..Default::default() - }, - ) - }) - .collect::>(); - let syntax_theme = SyntaxTheme::merge(Arc::new(SyntaxTheme::default()), syntax_highlights); - - let window_background_appearance = theme - .style - .window_background_appearance - .map(Into::into) - .unwrap_or_default(); - - Theme { - id: uuid::Uuid::new_v4().to_string(), - name: theme.name.clone().into(), - appearance, - styles: ThemeStyles { - system: SystemColors::default(), - window_background_appearance, - accents: refined_accent_colors, - colors: refined_theme_colors, - status: refined_status_colors, - player: refined_player_colors, - syntax: syntax_theme, - }, - } - } -} - -/// Refines a [ThemeFamilyContent] and it's [ThemeContent]s into a [ThemeFamily]. -pub fn refine_theme_family(theme_family_content: ThemeFamilyContent) -> ThemeFamily { - let id = Uuid::new_v4().to_string(); - let name = theme_family_content.name.clone(); - let author = theme_family_content.author.clone(); - - let mut theme_family = ThemeFamily { - id, - name: name.into(), - author: author.into(), - themes: vec![], - scales: default_color_scales(), - }; - - let refined_themes = theme_family_content - .themes - .iter() - .map(|theme_content| theme_family.refine_theme(theme_content)) - .collect(); - - theme_family.themes = refined_themes; - - theme_family -} - -/// A theme is the primary mechanism for defining the appearance of the UI. -#[derive(Clone, Debug, PartialEq)] -pub struct Theme { - /// The unique identifier for the theme. - pub id: String, - /// The name of the theme. - pub name: SharedString, - /// The appearance of the theme (light or dark). - pub appearance: Appearance, - /// The colors and other styles for the theme. - pub styles: ThemeStyles, -} - -impl Theme { - /// Returns the [`SystemColors`] for the theme. - #[inline(always)] - pub fn system(&self) -> &SystemColors { - &self.styles.system - } - - /// Returns the [`AccentColors`] for the theme. - #[inline(always)] - pub fn accents(&self) -> &AccentColors { - &self.styles.accents - } - - /// Returns the [`PlayerColors`] for the theme. - #[inline(always)] - pub fn players(&self) -> &PlayerColors { - &self.styles.player - } - - /// Returns the [`ThemeColors`] for the theme. - #[inline(always)] - pub fn colors(&self) -> &ThemeColors { - &self.styles.colors - } - - /// Returns the [`SyntaxTheme`] for the theme. - #[inline(always)] - pub fn syntax(&self) -> &Arc { - &self.styles.syntax - } - - /// Returns the [`StatusColors`] for the theme. - #[inline(always)] - pub fn status(&self) -> &StatusColors { - &self.styles.status - } - - /// Returns the color for the syntax node with the given name. - #[inline(always)] - pub fn syntax_color(&self, name: &str) -> Hsla { - self.syntax().color(name) - } - - /// Returns the [`Appearance`] for the theme. - #[inline(always)] - pub fn appearance(&self) -> Appearance { - self.appearance - } - - /// Returns the [`WindowBackgroundAppearance`] for the theme. - #[inline(always)] - pub fn window_background_appearance(&self) -> WindowBackgroundAppearance { - self.styles.window_background_appearance - } - - /// Darkens the color by reducing its lightness. - /// The resulting lightness is clamped to ensure it doesn't go below 0.0. - /// - /// The first value darkens light appearance mode, the second darkens appearance dark mode. - /// - /// Note: This is a tentative solution and may be replaced with a more robust color system. - pub fn darken(&self, color: Hsla, light_amount: f32, dark_amount: f32) -> Hsla { - let amount = match self.appearance { - Appearance::Light => light_amount, - Appearance::Dark => dark_amount, - }; - let mut hsla = color; - hsla.l = (hsla.l - amount).max(0.0); - hsla - } -} - -/// Asynchronously reads the user theme from the specified path. -pub async fn read_user_theme(theme_path: &Path, fs: Arc) -> Result { - let bytes = fs.load_bytes(theme_path).await?; - let theme_family: ThemeFamilyContent = serde_json_lenient::from_slice(&bytes)?; - - for theme in &theme_family.themes { - if theme - .style - .colors - .deprecated_scrollbar_thumb_background - .is_some() - { - log::warn!( - r#"Theme "{theme_name}" is using a deprecated style property: scrollbar_thumb.background. Use `scrollbar.thumb.background` instead."#, - theme_name = theme.name - ) - } - } - - Ok(theme_family) -} - -/// Asynchronously reads the icon theme from the specified path. -pub async fn read_icon_theme( - icon_theme_path: &Path, - fs: Arc, -) -> Result { - let bytes = fs.load_bytes(icon_theme_path).await?; - let icon_theme_family: IconThemeFamilyContent = serde_json_lenient::from_slice(&bytes)?; - - Ok(icon_theme_family) -} - -/// The active theme -pub struct GlobalTheme { - theme: Arc, - icon_theme: Arc, -} -impl Global for GlobalTheme {} - -impl GlobalTheme { - fn configured_theme(cx: &mut App) -> Arc { - let themes = ThemeRegistry::default_global(cx); - let theme_settings = ThemeSettings::get_global(cx); - let system_appearance = SystemAppearance::global(cx); - - let theme_name = theme_settings.theme.name(*system_appearance); - - let theme = match themes.get(&theme_name.0) { - Ok(theme) => theme, - Err(err) => { - if themes.extensions_loaded() { - log::error!("{err}"); - } - themes - .get(default_theme(*system_appearance)) - // fallback for tests. - .unwrap_or_else(|_| themes.get(DEFAULT_DARK_THEME).unwrap()) - } - }; - theme_settings.apply_theme_overrides(theme) - } - - /// Reloads the current theme. - /// - /// Reads the [`ThemeSettings`] to know which theme should be loaded, - /// taking into account the current [`SystemAppearance`]. - pub fn reload_theme(cx: &mut App) { - let theme = Self::configured_theme(cx); - cx.update_global::(|this, _| this.theme = theme); - cx.refresh_windows(); - } - - fn configured_icon_theme(cx: &mut App) -> Arc { - let themes = ThemeRegistry::default_global(cx); - let theme_settings = ThemeSettings::get_global(cx); - let system_appearance = SystemAppearance::global(cx); - - let icon_theme_name = theme_settings.icon_theme.name(*system_appearance); - - match themes.get_icon_theme(&icon_theme_name.0) { - Ok(theme) => theme, - Err(err) => { - if themes.extensions_loaded() { - log::error!("{err}"); - } - themes.get_icon_theme(DEFAULT_ICON_THEME_NAME).unwrap() - } - } - } - - /// Reloads the current icon theme. - /// - /// Reads the [`ThemeSettings`] to know which icon theme should be loaded, - /// taking into account the current [`SystemAppearance`]. - pub fn reload_icon_theme(cx: &mut App) { - let icon_theme = Self::configured_icon_theme(cx); - cx.update_global::(|this, _| this.icon_theme = icon_theme); - cx.refresh_windows(); - } - - /// the active theme - pub fn theme(cx: &App) -> &Arc { - &cx.global::().theme - } - - /// the active icon theme - pub fn icon_theme(cx: &App) -> &Arc { - &cx.global::().icon_theme - } -} diff --git a/crates/theme_extension/Cargo.toml b/crates/theme_extension/Cargo.toml deleted file mode 100644 index d94e15914b..0000000000 --- a/crates/theme_extension/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "theme_extension" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/theme_extension.rs" - -[dependencies] -anyhow.workspace = true -extension.workspace = true -fs.workspace = true -gpui.workspace = true -theme.workspace = true diff --git a/crates/theme_extension/LICENSE-GPL b/crates/theme_extension/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/theme_extension/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/theme_extension/src/theme_extension.rs b/crates/theme_extension/src/theme_extension.rs deleted file mode 100644 index 10df2349c8..0000000000 --- a/crates/theme_extension/src/theme_extension.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::path::PathBuf; -use std::sync::Arc; - -use anyhow::Result; -use extension::{ExtensionHostProxy, ExtensionThemeProxy}; -use fs::Fs; -use gpui::{App, BackgroundExecutor, SharedString, Task}; -use theme::{GlobalTheme, ThemeRegistry}; - -pub fn init( - extension_host_proxy: Arc, - theme_registry: Arc, - executor: BackgroundExecutor, -) { - extension_host_proxy.register_theme_proxy(ThemeRegistryProxy { - theme_registry, - executor, - }); -} - -struct ThemeRegistryProxy { - theme_registry: Arc, - executor: BackgroundExecutor, -} - -impl ExtensionThemeProxy for ThemeRegistryProxy { - fn set_extensions_loaded(&self) { - self.theme_registry.set_extensions_loaded(); - } - - fn list_theme_names(&self, theme_path: PathBuf, fs: Arc) -> Task>> { - self.executor.spawn(async move { - let themes = theme::read_user_theme(&theme_path, fs).await?; - Ok(themes.themes.into_iter().map(|theme| theme.name).collect()) - }) - } - - fn remove_user_themes(&self, themes: Vec) { - self.theme_registry.remove_user_themes(&themes); - } - - fn load_user_theme(&self, theme_path: PathBuf, fs: Arc) -> Task> { - let theme_registry = self.theme_registry.clone(); - self.executor - .spawn(async move { theme_registry.load_user_theme(&theme_path, fs).await }) - } - - fn reload_current_theme(&self, cx: &mut App) { - GlobalTheme::reload_theme(cx) - } - - fn list_icon_theme_names( - &self, - icon_theme_path: PathBuf, - fs: Arc, - ) -> Task>> { - self.executor.spawn(async move { - let icon_theme_family = theme::read_icon_theme(&icon_theme_path, fs).await?; - Ok(icon_theme_family - .themes - .into_iter() - .map(|theme| theme.name) - .collect()) - }) - } - - fn remove_icon_themes(&self, icon_themes: Vec) { - self.theme_registry.remove_icon_themes(&icon_themes); - } - - fn load_icon_theme( - &self, - icon_theme_path: PathBuf, - icons_root_dir: PathBuf, - fs: Arc, - ) -> Task> { - let theme_registry = self.theme_registry.clone(); - self.executor.spawn(async move { - theme_registry - .load_icon_theme(&icon_theme_path, &icons_root_dir, fs) - .await - }) - } - - fn reload_current_icon_theme(&self, cx: &mut App) { - GlobalTheme::reload_icon_theme(cx) - } -} diff --git a/crates/theme_importer/Cargo.toml b/crates/theme_importer/Cargo.toml deleted file mode 100644 index a91ffc4454..0000000000 --- a/crates/theme_importer/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "theme_importer" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[dependencies] -anyhow.workspace = true -clap = { workspace = true, features = ["derive"] } -collections.workspace = true -gpui.workspace = true -indexmap.workspace = true -log.workspace = true -palette.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_json_lenient.workspace = true -simplelog.workspace= true -strum = { workspace = true, features = ["derive"] } -theme.workspace = true -vscode_theme = "0.2.0" diff --git a/crates/theme_importer/LICENSE-GPL b/crates/theme_importer/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/theme_importer/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/theme_importer/README.md b/crates/theme_importer/README.md deleted file mode 100644 index 20b7d063ad..0000000000 --- a/crates/theme_importer/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Zed Theme Importer - -```sh -cargo run -p theme_importer -- dark-plus-syntax-color-theme.json --output output-theme.json -``` diff --git a/crates/theme_importer/src/color.rs b/crates/theme_importer/src/color.rs deleted file mode 100644 index 921f7a376b..0000000000 --- a/crates/theme_importer/src/color.rs +++ /dev/null @@ -1,56 +0,0 @@ -use anyhow::Result; -use gpui::Hsla; -use palette::FromColor; - -#[allow(unused)] -pub(crate) fn try_parse_color(color: &str) -> Result { - let rgba = gpui::Rgba::try_from(color)?; - let rgba = palette::rgb::Srgba::from_components((rgba.r, rgba.g, rgba.b, rgba.a)); - let hsla = palette::Hsla::from_color(rgba); - - let hsla = gpui::hsla( - hsla.hue.into_positive_degrees() / 360., - hsla.saturation, - hsla.lightness, - hsla.alpha, - ); - - Ok(hsla) -} - -#[allow(unused)] -pub(crate) fn pack_color(color: Hsla) -> u32 { - let hsla = palette::Hsla::from_components((color.h * 360., color.s, color.l, color.a)); - let rgba = palette::rgb::Srgba::from_color(hsla); - let rgba = rgba.into_format::(); - - u32::from(rgba) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - pub fn test_serialize_color() { - let color = "#b4637aff"; - let hsla = try_parse_color(color).unwrap(); - let packed = pack_color(hsla); - - assert_eq!(format!("#{:x}", packed), color); - } - - #[test] - pub fn test_serialize_color_with_palette() { - let color = "#b4637aff"; - - let rgba = gpui::Rgba::try_from(color).unwrap(); - let rgba = palette::rgb::Srgba::from_components((rgba.r, rgba.g, rgba.b, rgba.a)); - let hsla = palette::Hsla::from_color(rgba); - - let rgba = palette::rgb::Srgba::from_color(hsla); - let rgba = rgba.into_format::(); - - assert_eq!(format!("#{:x}", rgba), color); - } -} diff --git a/crates/theme_importer/src/main.rs b/crates/theme_importer/src/main.rs deleted file mode 100644 index 24291fc511..0000000000 --- a/crates/theme_importer/src/main.rs +++ /dev/null @@ -1,130 +0,0 @@ -mod color; -mod vscode; - -use std::fs::File; -use std::io::{Read, Write}; -use std::path::PathBuf; - -use anyhow::{Context as _, Result}; -use clap::Parser; -use collections::IndexMap; -use log::LevelFilter; -use serde::Deserialize; -use simplelog::ColorChoice; -use simplelog::{TermLogger, TerminalMode}; -use theme::{Appearance, AppearanceContent}; - -use crate::vscode::VsCodeTheme; -use crate::vscode::VsCodeThemeConverter; - -const ZED_THEME_SCHEMA_URL: &str = "https://zed.dev/schema/themes/v0.2.0.json"; - -#[derive(Debug, Clone, Copy, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum ThemeAppearanceJson { - Light, - Dark, -} - -impl From for AppearanceContent { - fn from(value: ThemeAppearanceJson) -> Self { - match value { - ThemeAppearanceJson::Light => Self::Light, - ThemeAppearanceJson::Dark => Self::Dark, - } - } -} - -impl From for Appearance { - fn from(value: ThemeAppearanceJson) -> Self { - match value { - ThemeAppearanceJson::Light => Self::Light, - ThemeAppearanceJson::Dark => Self::Dark, - } - } -} - -#[derive(Debug, Deserialize)] -pub struct ThemeMetadata { - pub name: String, - pub file_name: String, - pub appearance: ThemeAppearanceJson, -} - -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Args { - /// The path to the theme to import. - theme_path: PathBuf, - - /// Whether to warn when values are missing from the theme. - #[arg(long)] - warn_on_missing: bool, - - /// The path to write the output to. - #[arg(long, short)] - output: Option, -} - -fn main() -> Result<()> { - let args = Args::parse(); - - let log_config = { - let mut config = simplelog::ConfigBuilder::new(); - - if !args.warn_on_missing { - config.add_filter_ignore_str("theme_printer"); - } - - config.build() - }; - - TermLogger::init( - LevelFilter::Trace, - log_config, - TerminalMode::Stderr, - ColorChoice::Auto, - ) - .expect("could not initialize logger"); - - let theme_file_path = args.theme_path; - - let mut buffer = Vec::new(); - match File::open(&theme_file_path).and_then(|mut file| file.read_to_end(&mut buffer)) { - Ok(_) => {} - Err(err) => { - log::info!("Failed to open file at path: {:?}", theme_file_path); - return Err(err)?; - } - }; - - let vscode_theme: VsCodeTheme = serde_json_lenient::from_slice(&buffer) - .context(format!("failed to parse theme {theme_file_path:?}"))?; - - let theme_metadata = ThemeMetadata { - name: vscode_theme.name.clone().unwrap_or("".to_string()), - appearance: ThemeAppearanceJson::Dark, - file_name: "".to_string(), - }; - - let converter = VsCodeThemeConverter::new(vscode_theme, theme_metadata, IndexMap::default()); - - let theme = converter.convert()?; - let mut theme = serde_json::to_value(theme).unwrap(); - theme.as_object_mut().unwrap().insert( - "$schema".to_string(), - serde_json::Value::String(ZED_THEME_SCHEMA_URL.to_string()), - ); - let theme_json = serde_json::to_string_pretty(&theme).unwrap(); - - if let Some(output) = args.output { - let mut file = File::create(output)?; - file.write_all(theme_json.as_bytes())?; - } else { - println!("{}", theme_json); - } - - log::info!("Done!"); - - Ok(()) -} diff --git a/crates/theme_importer/src/vscode.rs b/crates/theme_importer/src/vscode.rs deleted file mode 100644 index 6933bbaa8f..0000000000 --- a/crates/theme_importer/src/vscode.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod converter; -mod syntax; -mod theme; - -pub use converter::*; -pub use syntax::*; -pub use theme::*; diff --git a/crates/theme_importer/src/vscode/converter.rs b/crates/theme_importer/src/vscode/converter.rs deleted file mode 100644 index e4a9769978..0000000000 --- a/crates/theme_importer/src/vscode/converter.rs +++ /dev/null @@ -1,276 +0,0 @@ -use anyhow::Result; -use collections::IndexMap; -use strum::IntoEnumIterator; -use theme::{ - FontStyleContent, FontWeightContent, HighlightStyleContent, StatusColorsContent, - ThemeColorsContent, ThemeContent, ThemeStyleContent, WindowBackgroundContent, -}; - -use crate::ThemeMetadata; -use crate::vscode::{VsCodeTheme, VsCodeTokenScope}; - -use super::ZedSyntaxToken; - -pub(crate) fn try_parse_font_weight(font_style: &str) -> Option { - match font_style { - style if style.contains("bold") => Some(FontWeightContent::Bold), - _ => None, - } -} - -pub(crate) fn try_parse_font_style(font_style: &str) -> Option { - match font_style { - style if style.contains("italic") => Some(FontStyleContent::Italic), - style if style.contains("oblique") => Some(FontStyleContent::Oblique), - _ => None, - } -} - -pub struct VsCodeThemeConverter { - theme: VsCodeTheme, - theme_metadata: ThemeMetadata, - syntax_overrides: IndexMap>, -} - -impl VsCodeThemeConverter { - pub fn new( - theme: VsCodeTheme, - theme_metadata: ThemeMetadata, - syntax_overrides: IndexMap>, - ) -> Self { - Self { - theme, - theme_metadata, - syntax_overrides, - } - } - - pub fn convert(self) -> Result { - let appearance = self.theme_metadata.appearance.into(); - - let status_colors = self.convert_status_colors()?; - let theme_colors = self.convert_theme_colors()?; - let syntax_theme = self.convert_syntax_theme()?; - - Ok(ThemeContent { - name: self.theme_metadata.name, - appearance, - style: ThemeStyleContent { - window_background_appearance: Some(WindowBackgroundContent::Opaque), - accents: Vec::new(), //TODO can we read this from the theme? - colors: theme_colors, - status: status_colors, - players: Vec::new(), - syntax: syntax_theme, - }, - }) - } - - fn convert_status_colors(&self) -> Result { - let vscode_colors = &self.theme.colors; - - let vscode_base_status_colors = StatusColorsContent { - hint: Some("#969696ff".to_string()), - ..Default::default() - }; - - Ok(StatusColorsContent { - conflict: vscode_colors - .git_decoration - .conflicting_resource_foreground - .clone(), - created: vscode_colors.editor_gutter.added_background.clone(), - deleted: vscode_colors.editor_gutter.deleted_background.clone(), - error: vscode_colors.editor_error.foreground.clone(), - error_background: vscode_colors.editor_error.background.clone(), - error_border: vscode_colors.editor_error.border.clone(), - hidden: vscode_colors.tab.inactive_foreground.clone(), - hint: vscode_colors - .editor_inlay_hint - .foreground - .clone() - .or(vscode_base_status_colors.hint), - hint_border: vscode_colors.editor_hint.border.clone(), - ignored: vscode_colors - .git_decoration - .ignored_resource_foreground - .clone(), - info: vscode_colors.editor_info.foreground.clone(), - info_background: vscode_colors.editor_info.background.clone(), - info_border: vscode_colors.editor_info.border.clone(), - modified: vscode_colors.editor_gutter.modified_background.clone(), - // renamed: None, - // success: None, - warning: vscode_colors.editor_warning.foreground.clone(), - warning_background: vscode_colors.editor_warning.background.clone(), - warning_border: vscode_colors.editor_warning.border.clone(), - ..Default::default() - }) - } - - fn convert_theme_colors(&self) -> Result { - let vscode_colors = &self.theme.colors; - - let vscode_panel_border = vscode_colors.panel.border.clone(); - let vscode_tab_inactive_background = vscode_colors.tab.inactive_background.clone(); - let vscode_editor_foreground = vscode_colors.editor.foreground.clone(); - let vscode_editor_background = vscode_colors.editor.background.clone(); - let vscode_scrollbar_slider_background = vscode_colors.scrollbar_slider.background.clone(); - let vscode_token_colors_foreground = self - .theme - .token_colors - .iter() - .find(|token_color| token_color.scope.is_none()) - .and_then(|token_color| token_color.settings.foreground.as_ref()) - .cloned(); - - Ok(ThemeColorsContent { - border: vscode_panel_border.clone(), - border_variant: vscode_panel_border.clone(), - border_focused: vscode_colors.focus_border.clone(), - border_selected: vscode_panel_border.clone(), - border_transparent: vscode_panel_border.clone(), - border_disabled: vscode_panel_border.clone(), - elevated_surface_background: vscode_colors.dropdown.background.clone(), - surface_background: vscode_colors.panel.background.clone(), - background: vscode_editor_background.clone(), - element_background: vscode_colors.button.background.clone(), - element_hover: vscode_colors.list.hover_background.clone(), - element_selected: vscode_colors.list.active_selection_background.clone(), - drop_target_background: vscode_colors.list.drop_background.clone(), - ghost_element_hover: vscode_colors.list.hover_background.clone(), - ghost_element_selected: vscode_colors.list.active_selection_background.clone(), - text: vscode_colors - .foreground - .clone() - .or(vscode_token_colors_foreground.clone()), - text_muted: vscode_colors.tab.inactive_foreground.clone(), - status_bar_background: vscode_colors.status_bar.background.clone(), - title_bar_background: vscode_colors.title_bar.active_background.clone(), - toolbar_background: vscode_colors - .breadcrumb - .background - .clone() - .or(vscode_editor_background.clone()), - tab_bar_background: vscode_colors.editor_group_header.tabs_background.clone(), - tab_inactive_background: vscode_tab_inactive_background.clone(), - tab_active_background: vscode_colors - .tab - .active_background - .clone() - .or(vscode_tab_inactive_background), - search_match_background: vscode_colors.editor.find_match_background.clone(), - panel_background: vscode_colors.panel.background.clone(), - pane_group_border: vscode_colors.editor_group.border.clone(), - scrollbar_thumb_background: vscode_scrollbar_slider_background.clone(), - scrollbar_thumb_hover_background: vscode_colors - .scrollbar_slider - .hover_background - .clone(), - scrollbar_thumb_active_background: vscode_colors - .scrollbar_slider - .active_background - .clone(), - scrollbar_thumb_border: vscode_scrollbar_slider_background, - scrollbar_track_background: vscode_editor_background.clone(), - scrollbar_track_border: vscode_colors.editor_overview_ruler.border.clone(), - minimap_thumb_background: vscode_colors.minimap_slider.background.clone(), - minimap_thumb_hover_background: vscode_colors.minimap_slider.hover_background.clone(), - minimap_thumb_active_background: vscode_colors.minimap_slider.active_background.clone(), - editor_foreground: vscode_editor_foreground.or(vscode_token_colors_foreground), - editor_background: vscode_editor_background.clone(), - editor_gutter_background: vscode_editor_background, - editor_active_line_background: vscode_colors.editor.line_highlight_background.clone(), - editor_line_number: vscode_colors.editor_line_number.foreground.clone(), - editor_active_line_number: vscode_colors.editor.foreground.clone(), - editor_wrap_guide: vscode_panel_border.clone(), - editor_active_wrap_guide: vscode_panel_border, - editor_document_highlight_bracket_background: vscode_colors - .editor_bracket_match - .background - .clone(), - terminal_background: vscode_colors.terminal.background.clone(), - terminal_ansi_black: vscode_colors.terminal.ansi_black.clone(), - terminal_ansi_bright_black: vscode_colors.terminal.ansi_bright_black.clone(), - terminal_ansi_red: vscode_colors.terminal.ansi_red.clone(), - terminal_ansi_bright_red: vscode_colors.terminal.ansi_bright_red.clone(), - terminal_ansi_green: vscode_colors.terminal.ansi_green.clone(), - terminal_ansi_bright_green: vscode_colors.terminal.ansi_bright_green.clone(), - terminal_ansi_yellow: vscode_colors.terminal.ansi_yellow.clone(), - terminal_ansi_bright_yellow: vscode_colors.terminal.ansi_bright_yellow.clone(), - terminal_ansi_blue: vscode_colors.terminal.ansi_blue.clone(), - terminal_ansi_bright_blue: vscode_colors.terminal.ansi_bright_blue.clone(), - terminal_ansi_magenta: vscode_colors.terminal.ansi_magenta.clone(), - terminal_ansi_bright_magenta: vscode_colors.terminal.ansi_bright_magenta.clone(), - terminal_ansi_cyan: vscode_colors.terminal.ansi_cyan.clone(), - terminal_ansi_bright_cyan: vscode_colors.terminal.ansi_bright_cyan.clone(), - terminal_ansi_white: vscode_colors.terminal.ansi_white.clone(), - terminal_ansi_bright_white: vscode_colors.terminal.ansi_bright_white.clone(), - link_text_hover: vscode_colors.text_link.active_foreground.clone(), - ..Default::default() - }) - } - - fn convert_syntax_theme(&self) -> Result> { - let mut highlight_styles = IndexMap::default(); - - for syntax_token in ZedSyntaxToken::iter() { - let override_match = self - .syntax_overrides - .get(&syntax_token.to_string()) - .and_then(|scope| { - self.theme.token_colors.iter().find(|token_color| { - token_color.scope == Some(VsCodeTokenScope::Many(scope.clone())) - }) - }); - - let best_match = override_match - .or_else(|| syntax_token.find_best_token_color_match(&self.theme.token_colors)) - .or_else(|| { - syntax_token.fallbacks().iter().find_map(|fallback| { - fallback.find_best_token_color_match(&self.theme.token_colors) - }) - }); - - let Some(token_color) = best_match else { - log::warn!("No matching token color found for '{syntax_token}'"); - continue; - }; - - log::info!( - "Matched '{syntax_token}' to '{}'", - token_color - .name - .clone() - .or_else(|| token_color - .scope - .as_ref() - .map(|scope| format!("{:?}", scope))) - .unwrap_or_else(|| "no identifier".to_string()) - ); - - let highlight_style = HighlightStyleContent { - color: token_color.settings.foreground.clone(), - background_color: token_color.settings.background.clone(), - font_style: token_color - .settings - .font_style - .as_ref() - .and_then(|style| try_parse_font_style(style)), - font_weight: token_color - .settings - .font_style - .as_ref() - .and_then(|style| try_parse_font_weight(style)), - }; - - if highlight_style.is_empty() { - continue; - } - - highlight_styles.insert(syntax_token.to_string(), highlight_style); - } - - Ok(highlight_styles) - } -} diff --git a/crates/theme_importer/src/vscode/syntax.rs b/crates/theme_importer/src/vscode/syntax.rs deleted file mode 100644 index 7b134089b7..0000000000 --- a/crates/theme_importer/src/vscode/syntax.rs +++ /dev/null @@ -1,310 +0,0 @@ -use indexmap::IndexMap; -use serde::Deserialize; -use strum::EnumIter; - -#[derive(Debug, PartialEq, Eq, Deserialize)] -#[serde(untagged)] -pub enum VsCodeTokenScope { - One(String), - Many(Vec), -} - -#[derive(Debug, Deserialize)] -pub struct VsCodeTokenColor { - pub name: Option, - pub scope: Option, - pub settings: VsCodeTokenColorSettings, -} - -#[derive(Debug, Deserialize)] -pub struct VsCodeTokenColorSettings { - pub foreground: Option, - pub background: Option, - #[serde(rename = "fontStyle")] - pub font_style: Option, -} - -#[derive(Debug, PartialEq, Copy, Clone, EnumIter)] -pub enum ZedSyntaxToken { - Attribute, - Boolean, - Comment, - CommentDoc, - Constant, - Constructor, - Embedded, - Emphasis, - EmphasisStrong, - Enum, - Function, - Hint, - Keyword, - Label, - LinkText, - LinkUri, - Number, - Operator, - Predictive, - Preproc, - Primary, - Property, - Punctuation, - PunctuationBracket, - PunctuationDelimiter, - PunctuationListMarker, - PunctuationSpecial, - String, - StringEscape, - StringRegex, - StringSpecial, - StringSpecialSymbol, - Tag, - TextLiteral, - Title, - Type, - Variable, - VariableSpecial, - Variant, -} - -impl std::fmt::Display for ZedSyntaxToken { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - ZedSyntaxToken::Attribute => "attribute", - ZedSyntaxToken::Boolean => "boolean", - ZedSyntaxToken::Comment => "comment", - ZedSyntaxToken::CommentDoc => "comment.doc", - ZedSyntaxToken::Constant => "constant", - ZedSyntaxToken::Constructor => "constructor", - ZedSyntaxToken::Embedded => "embedded", - ZedSyntaxToken::Emphasis => "emphasis", - ZedSyntaxToken::EmphasisStrong => "emphasis.strong", - ZedSyntaxToken::Enum => "enum", - ZedSyntaxToken::Function => "function", - ZedSyntaxToken::Hint => "hint", - ZedSyntaxToken::Keyword => "keyword", - ZedSyntaxToken::Label => "label", - ZedSyntaxToken::LinkText => "link_text", - ZedSyntaxToken::LinkUri => "link_uri", - ZedSyntaxToken::Number => "number", - ZedSyntaxToken::Operator => "operator", - ZedSyntaxToken::Predictive => "predictive", - ZedSyntaxToken::Preproc => "preproc", - ZedSyntaxToken::Primary => "primary", - ZedSyntaxToken::Property => "property", - ZedSyntaxToken::Punctuation => "punctuation", - ZedSyntaxToken::PunctuationBracket => "punctuation.bracket", - ZedSyntaxToken::PunctuationDelimiter => "punctuation.delimiter", - ZedSyntaxToken::PunctuationListMarker => "punctuation.list_marker", - ZedSyntaxToken::PunctuationSpecial => "punctuation.special", - ZedSyntaxToken::String => "string", - ZedSyntaxToken::StringEscape => "string.escape", - ZedSyntaxToken::StringRegex => "string.regex", - ZedSyntaxToken::StringSpecial => "string.special", - ZedSyntaxToken::StringSpecialSymbol => "string.special.symbol", - ZedSyntaxToken::Tag => "tag", - ZedSyntaxToken::TextLiteral => "text.literal", - ZedSyntaxToken::Title => "title", - ZedSyntaxToken::Type => "type", - ZedSyntaxToken::Variable => "variable", - ZedSyntaxToken::VariableSpecial => "variable.special", - ZedSyntaxToken::Variant => "variant", - } - ) - } -} - -impl ZedSyntaxToken { - pub fn find_best_token_color_match<'a>( - &self, - token_colors: &'a [VsCodeTokenColor], - ) -> Option<&'a VsCodeTokenColor> { - let mut ranked_matches = IndexMap::new(); - - for (ix, token_color) in token_colors.iter().enumerate() { - if token_color.settings.foreground.is_none() { - continue; - } - - let Some(rank) = self.rank_match(token_color) else { - continue; - }; - - if rank > 0 { - ranked_matches.insert(ix, rank); - } - } - - ranked_matches - .into_iter() - .max_by_key(|(_, rank)| *rank) - .map(|(ix, _)| &token_colors[ix]) - } - - fn rank_match(&self, token_color: &VsCodeTokenColor) -> Option { - let candidate_scopes = match token_color.scope.as_ref()? { - VsCodeTokenScope::One(scope) => vec![scope], - VsCodeTokenScope::Many(scopes) => scopes.iter().collect(), - } - .iter() - .flat_map(|scope| scope.split(',').map(|s| s.trim())) - .collect::>(); - - let scopes_to_match = self.to_vscode(); - let number_of_scopes_to_match = scopes_to_match.len(); - - let mut matches = 0; - - for (ix, scope) in scopes_to_match.into_iter().enumerate() { - // Assign each entry a weight that is inversely proportional to its - // position in the list. - // - // Entries towards the front are weighted higher than those towards the end. - let weight = (number_of_scopes_to_match - ix) as u32; - - if candidate_scopes.contains(&scope) { - matches += 1 + weight; - } - } - - Some(matches) - } - - pub fn fallbacks(&self) -> &[Self] { - match self { - ZedSyntaxToken::CommentDoc => &[ZedSyntaxToken::Comment], - ZedSyntaxToken::Number => &[ZedSyntaxToken::Constant], - ZedSyntaxToken::VariableSpecial => &[ZedSyntaxToken::Variable], - ZedSyntaxToken::PunctuationBracket - | ZedSyntaxToken::PunctuationDelimiter - | ZedSyntaxToken::PunctuationListMarker - | ZedSyntaxToken::PunctuationSpecial => &[ZedSyntaxToken::Punctuation], - ZedSyntaxToken::StringEscape - | ZedSyntaxToken::StringRegex - | ZedSyntaxToken::StringSpecial - | ZedSyntaxToken::StringSpecialSymbol => &[ZedSyntaxToken::String], - _ => &[], - } - } - - fn to_vscode(self) -> Vec<&'static str> { - match self { - ZedSyntaxToken::Attribute => vec!["entity.other.attribute-name"], - ZedSyntaxToken::Boolean => vec!["constant.language"], - ZedSyntaxToken::Comment => vec!["comment"], - ZedSyntaxToken::CommentDoc => vec!["comment.block.documentation"], - ZedSyntaxToken::Constant => vec!["constant", "constant.language", "constant.character"], - ZedSyntaxToken::Constructor => { - vec![ - "entity.name.tag", - "entity.name.function.definition.special.constructor", - ] - } - ZedSyntaxToken::Embedded => vec!["meta.embedded"], - ZedSyntaxToken::Emphasis => vec!["markup.italic"], - ZedSyntaxToken::EmphasisStrong => vec![ - "markup.bold", - "markup.italic markup.bold", - "markup.bold markup.italic", - ], - ZedSyntaxToken::Enum => vec!["support.type.enum"], - ZedSyntaxToken::Function => vec![ - "entity.function", - "entity.name.function", - "variable.function", - ], - ZedSyntaxToken::Hint => vec![], - ZedSyntaxToken::Keyword => vec![ - "keyword", - "keyword.other.fn.rust", - "keyword.control", - "keyword.control.fun", - "keyword.control.class", - "punctuation.accessor", - "entity.name.tag", - ], - ZedSyntaxToken::Label => vec![ - "label", - "entity.name", - "entity.name.import", - "entity.name.package", - ], - ZedSyntaxToken::LinkText => vec!["markup.underline.link", "string.other.link"], - ZedSyntaxToken::LinkUri => vec!["markup.underline.link", "string.other.link"], - ZedSyntaxToken::Number => vec!["constant.numeric", "number"], - ZedSyntaxToken::Operator => vec!["operator", "keyword.operator"], - ZedSyntaxToken::Predictive => vec![], - ZedSyntaxToken::Preproc => vec![ - "preproc", - "meta.preprocessor", - "punctuation.definition.preprocessor", - ], - ZedSyntaxToken::Primary => vec![], - ZedSyntaxToken::Property => vec![ - "variable.member", - "support.type.property-name", - "variable.object.property", - "variable.other.field", - ], - ZedSyntaxToken::Punctuation => vec![ - "punctuation", - "punctuation.section", - "punctuation.accessor", - "punctuation.separator", - "punctuation.definition.tag", - ], - ZedSyntaxToken::PunctuationBracket => vec![ - "punctuation.bracket", - "punctuation.definition.tag.begin", - "punctuation.definition.tag.end", - ], - ZedSyntaxToken::PunctuationDelimiter => vec![ - "punctuation.delimiter", - "punctuation.separator", - "punctuation.terminator", - ], - ZedSyntaxToken::PunctuationListMarker => { - vec!["markup.list punctuation.definition.list.begin"] - } - ZedSyntaxToken::PunctuationSpecial => vec!["punctuation.special"], - ZedSyntaxToken::String => vec!["string"], - ZedSyntaxToken::StringEscape => { - vec!["string.escape", "constant.character", "constant.other"] - } - ZedSyntaxToken::StringRegex => vec!["string.regex"], - ZedSyntaxToken::StringSpecial => vec!["string.special", "constant.other.symbol"], - ZedSyntaxToken::StringSpecialSymbol => { - vec!["string.special.symbol", "constant.other.symbol"] - } - ZedSyntaxToken::Tag => vec!["tag", "entity.name.tag", "meta.tag.sgml"], - ZedSyntaxToken::TextLiteral => vec!["text.literal", "string"], - ZedSyntaxToken::Title => vec!["title", "entity.name"], - ZedSyntaxToken::Type => vec![ - "entity.name.type", - "entity.name.type.primitive", - "entity.name.type.numeric", - "keyword.type", - "support.type", - "support.type.primitive", - "support.class", - ], - ZedSyntaxToken::Variable => vec![ - "variable", - "variable.language", - "variable.member", - "variable.parameter", - "variable.parameter.function-call", - ], - ZedSyntaxToken::VariableSpecial => vec![ - "variable.special", - "variable.member", - "variable.annotation", - "variable.language", - ], - ZedSyntaxToken::Variant => vec!["variant"], - } - } -} diff --git a/crates/theme_importer/src/vscode/theme.rs b/crates/theme_importer/src/vscode/theme.rs deleted file mode 100644 index 2479dd312e..0000000000 --- a/crates/theme_importer/src/vscode/theme.rs +++ /dev/null @@ -1,40 +0,0 @@ -use serde::Deserialize; -use vscode_theme::Colors; - -use crate::vscode::VsCodeTokenColor; - -#[derive(Deserialize, Debug)] -pub struct VsCodeTheme { - #[serde(rename = "$schema")] - #[expect( - unused, - reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove" - )] - pub schema: Option, - pub name: Option, - #[expect( - unused, - reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove" - )] - pub author: Option, - #[expect( - unused, - reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove" - )] - pub maintainers: Option>, - #[serde(rename = "semanticClass")] - #[expect( - unused, - reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove" - )] - pub semantic_class: Option, - #[expect( - unused, - reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove" - )] - #[serde(rename = "semanticHighlighting")] - pub semantic_highlighting: Option, - pub colors: Colors, - #[serde(rename = "tokenColors")] - pub token_colors: Vec, -} diff --git a/crates/theme_selector/Cargo.toml b/crates/theme_selector/Cargo.toml deleted file mode 100644 index 1a563e81f2..0000000000 --- a/crates/theme_selector/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "theme_selector" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/theme_selector.rs" -doctest = false - -[dependencies] -fs.workspace = true -fuzzy.workspace = true -gpui.workspace = true -log.workspace = true -picker.workspace = true -serde.workspace = true -settings.workspace = true -telemetry.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[dev-dependencies] diff --git a/crates/theme_selector/LICENSE-GPL b/crates/theme_selector/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/theme_selector/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/theme_selector/src/icon_theme_selector.rs b/crates/theme_selector/src/icon_theme_selector.rs deleted file mode 100644 index 2ea3436d43..0000000000 --- a/crates/theme_selector/src/icon_theme_selector.rs +++ /dev/null @@ -1,338 +0,0 @@ -use fs::Fs; -use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; -use gpui::{ - App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, UpdateGlobal, WeakEntity, - Window, -}; -use picker::{Picker, PickerDelegate}; -use settings::{Settings as _, SettingsStore, update_settings_file}; -use std::sync::Arc; -use theme::{ - Appearance, IconThemeName, IconThemeSelection, SystemAppearance, ThemeMeta, ThemeRegistry, - ThemeSettings, -}; -use ui::{ListItem, ListItemSpacing, prelude::*, v_flex}; -use util::ResultExt; -use workspace::{ModalView, ui::HighlightedLabel}; -use zed_actions::{ExtensionCategoryFilter, Extensions}; - -pub(crate) struct IconThemeSelector { - picker: Entity>, -} - -impl EventEmitter for IconThemeSelector {} - -impl Focusable for IconThemeSelector { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl ModalView for IconThemeSelector {} - -impl IconThemeSelector { - pub fn new( - delegate: IconThemeSelectorDelegate, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); - Self { picker } - } -} - -impl Render for IconThemeSelector { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - v_flex() - .key_context("IconThemeSelector") - .w(rems(34.)) - .child(self.picker.clone()) - } -} - -pub(crate) struct IconThemeSelectorDelegate { - fs: Arc, - themes: Vec, - matches: Vec, - original_theme: IconThemeName, - selection_completed: bool, - selected_theme: Option, - selected_index: usize, - selector: WeakEntity, -} - -impl IconThemeSelectorDelegate { - pub fn new( - selector: WeakEntity, - fs: Arc, - themes_filter: Option<&Vec>, - cx: &mut Context, - ) -> Self { - let theme_settings = ThemeSettings::get_global(cx); - let original_theme = theme_settings - .icon_theme - .name(SystemAppearance::global(cx).0); - - let registry = ThemeRegistry::global(cx); - let mut themes = registry - .list_icon_themes() - .into_iter() - .filter(|meta| { - if let Some(theme_filter) = themes_filter { - theme_filter.contains(&meta.name.to_string()) - } else { - true - } - }) - .collect::>(); - - themes.sort_unstable_by(|a, b| { - a.appearance - .is_light() - .cmp(&b.appearance.is_light()) - .then(a.name.cmp(&b.name)) - }); - let matches = themes - .iter() - .map(|meta| StringMatch { - candidate_id: 0, - score: 0.0, - positions: Default::default(), - string: meta.name.to_string(), - }) - .collect(); - let mut this = Self { - fs, - themes, - matches, - original_theme: original_theme.clone(), - selected_index: 0, - selected_theme: None, - selection_completed: false, - selector, - }; - - this.select_if_matching(&original_theme.0); - this - } - - fn show_selected_theme( - &mut self, - cx: &mut Context>, - ) -> Option { - let mat = self.matches.get(self.selected_index)?; - let name = IconThemeName(mat.string.clone().into()); - Self::set_icon_theme(name.clone(), cx); - Some(name) - } - - fn select_if_matching(&mut self, theme_name: &str) { - self.selected_index = self - .matches - .iter() - .position(|mat| mat.string == theme_name) - .unwrap_or(self.selected_index); - } - - fn set_icon_theme(name: IconThemeName, cx: &mut App) { - SettingsStore::update_global(cx, |store, _| { - let mut theme_settings = store.get::(None).clone(); - theme_settings.icon_theme = IconThemeSelection::Static(name); - store.override_global(theme_settings); - }); - } -} - -impl PickerDelegate for IconThemeSelectorDelegate { - type ListItem = ui::ListItem; - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Select Icon Theme...".into() - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn confirm( - &mut self, - _: bool, - window: &mut Window, - cx: &mut Context>, - ) { - self.selection_completed = true; - - let theme_settings = ThemeSettings::get_global(cx); - let theme_name = theme_settings - .icon_theme - .name(SystemAppearance::global(cx).0); - - telemetry::event!( - "Settings Changed", - setting = "icon_theme", - value = theme_name - ); - - let appearance = Appearance::from(window.appearance()); - - update_settings_file(self.fs.clone(), cx, move |settings, _| { - theme::set_icon_theme(settings, theme_name, appearance); - }); - - self.selector - .update(cx, |_, cx| { - cx.emit(DismissEvent); - }) - .ok(); - } - - fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { - if !self.selection_completed { - Self::set_icon_theme(self.original_theme.clone(), cx); - self.selection_completed = true; - } - - self.selector - .update(cx, |_, cx| cx.emit(DismissEvent)) - .log_err(); - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _: &mut Window, - cx: &mut Context>, - ) { - self.selected_index = ix; - self.selected_theme = self.show_selected_theme(cx); - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> gpui::Task<()> { - let background = cx.background_executor().clone(); - let candidates = self - .themes - .iter() - .enumerate() - .map(|(id, meta)| StringMatchCandidate::new(id, &meta.name)) - .collect::>(); - - cx.spawn_in(window, async move |this, cx| { - let matches = if query.is_empty() { - candidates - .into_iter() - .enumerate() - .map(|(index, candidate)| StringMatch { - candidate_id: index, - string: candidate.string, - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - match_strings( - &candidates, - &query, - false, - true, - 100, - &Default::default(), - background, - ) - .await - }; - - this.update(cx, |this, cx| { - this.delegate.matches = matches; - if query.is_empty() && this.delegate.selected_theme.is_none() { - this.delegate.selected_index = this - .delegate - .selected_index - .min(this.delegate.matches.len().saturating_sub(1)); - } else if let Some(selected) = this.delegate.selected_theme.as_ref() { - this.delegate.selected_index = this - .delegate - .matches - .iter() - .enumerate() - .find(|(_, mtch)| mtch.string.as_str() == selected.0.as_ref()) - .map(|(ix, _)| ix) - .unwrap_or_default(); - } else { - this.delegate.selected_index = 0; - } - this.delegate.selected_theme = this.delegate.show_selected_theme(cx); - }) - .log_err(); - }) - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _window: &mut Window, - _cx: &mut Context>, - ) -> Option { - let theme_match = &self.matches.get(ix)?; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child(HighlightedLabel::new( - theme_match.string.clone(), - theme_match.positions.clone(), - )), - ) - } - - fn render_footer( - &self, - _window: &mut Window, - cx: &mut Context>, - ) -> Option { - Some( - h_flex() - .p_2() - .w_full() - .justify_between() - .gap_2() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child( - Button::new("docs", "View Icon Theme Docs") - .icon(IconName::ArrowUpRight) - .icon_position(IconPosition::End) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .on_click(|_event, _window, cx| { - cx.open_url("https://zed.dev/docs/icon-themes"); - }), - ) - .child( - Button::new("more-icon-themes", "Install Icon Themes").on_click( - move |_event, window, cx| { - window.dispatch_action( - Box::new(Extensions { - category_filter: Some(ExtensionCategoryFilter::IconThemes), - id: None, - }), - cx, - ); - }, - ), - ) - .into_any_element(), - ) - } -} diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs deleted file mode 100644 index 74b242dd0b..0000000000 --- a/crates/theme_selector/src/theme_selector.rs +++ /dev/null @@ -1,524 +0,0 @@ -mod icon_theme_selector; - -use fs::Fs; -use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; -use gpui::{ - App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, UpdateGlobal, WeakEntity, - Window, actions, -}; -use picker::{Picker, PickerDelegate}; -use settings::{Settings, SettingsStore, update_settings_file}; -use std::sync::Arc; -use theme::{ - Appearance, SystemAppearance, Theme, ThemeAppearanceMode, ThemeMeta, ThemeName, ThemeRegistry, - ThemeSelection, ThemeSettings, -}; -use ui::{ListItem, ListItemSpacing, prelude::*, v_flex}; -use util::ResultExt; -use workspace::{ModalView, Workspace, ui::HighlightedLabel, with_active_or_new_workspace}; -use zed_actions::{ExtensionCategoryFilter, Extensions}; - -use crate::icon_theme_selector::{IconThemeSelector, IconThemeSelectorDelegate}; - -actions!( - theme_selector, - [ - /// Reloads all themes from disk. - Reload - ] -); - -pub fn init(cx: &mut App) { - cx.on_action(|action: &zed_actions::theme_selector::Toggle, cx| { - let action = action.clone(); - with_active_or_new_workspace(cx, move |workspace, window, cx| { - toggle_theme_selector(workspace, &action, window, cx); - }); - }); - cx.on_action(|action: &zed_actions::icon_theme_selector::Toggle, cx| { - let action = action.clone(); - with_active_or_new_workspace(cx, move |workspace, window, cx| { - toggle_icon_theme_selector(workspace, &action, window, cx); - }); - }); -} - -fn toggle_theme_selector( - workspace: &mut Workspace, - toggle: &zed_actions::theme_selector::Toggle, - window: &mut Window, - cx: &mut Context, -) { - let fs = workspace.app_state().fs.clone(); - workspace.toggle_modal(window, cx, |window, cx| { - let delegate = ThemeSelectorDelegate::new( - cx.entity().downgrade(), - fs, - toggle.themes_filter.as_ref(), - cx, - ); - ThemeSelector::new(delegate, window, cx) - }); -} - -fn toggle_icon_theme_selector( - workspace: &mut Workspace, - toggle: &zed_actions::icon_theme_selector::Toggle, - window: &mut Window, - cx: &mut Context, -) { - let fs = workspace.app_state().fs.clone(); - workspace.toggle_modal(window, cx, |window, cx| { - let delegate = IconThemeSelectorDelegate::new( - cx.entity().downgrade(), - fs, - toggle.themes_filter.as_ref(), - cx, - ); - IconThemeSelector::new(delegate, window, cx) - }); -} - -impl ModalView for ThemeSelector {} - -struct ThemeSelector { - picker: Entity>, -} - -impl EventEmitter for ThemeSelector {} - -impl Focusable for ThemeSelector { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl Render for ThemeSelector { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - v_flex() - .key_context("ThemeSelector") - .w(rems(34.)) - .child(self.picker.clone()) - } -} - -impl ThemeSelector { - pub fn new( - delegate: ThemeSelectorDelegate, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); - Self { picker } - } -} - -struct ThemeSelectorDelegate { - fs: Arc, - themes: Vec, - matches: Vec, - /// The theme that was selected before the `ThemeSelector` menu was opened. - /// - /// We use this to return back to theme that was set if the user dismisses the menu. - original_theme_settings: ThemeSettings, - /// The current system appearance. - original_system_appearance: Appearance, - /// The currently selected new theme. - new_theme: Arc, - selection_completed: bool, - selected_theme: Option>, - selected_index: usize, - selector: WeakEntity, -} - -impl ThemeSelectorDelegate { - fn new( - selector: WeakEntity, - fs: Arc, - themes_filter: Option<&Vec>, - cx: &mut Context, - ) -> Self { - let original_theme = cx.theme().clone(); - let original_theme_settings = ThemeSettings::get_global(cx).clone(); - let original_system_appearance = SystemAppearance::global(cx).0; - - let registry = ThemeRegistry::global(cx); - let mut themes = registry - .list() - .into_iter() - .filter(|meta| { - if let Some(theme_filter) = themes_filter { - theme_filter.contains(&meta.name.to_string()) - } else { - true - } - }) - .collect::>(); - - // Sort by dark vs light, then by name. - themes.sort_unstable_by(|a, b| { - a.appearance - .is_light() - .cmp(&b.appearance.is_light()) - .then(a.name.cmp(&b.name)) - }); - - let matches: Vec = themes - .iter() - .map(|meta| StringMatch { - candidate_id: 0, - score: 0.0, - positions: Default::default(), - string: meta.name.to_string(), - }) - .collect(); - - // The current theme is likely in this list, so default to first showing that. - let selected_index = matches - .iter() - .position(|mat| mat.string == original_theme.name) - .unwrap_or(0); - - Self { - fs, - themes, - matches, - original_theme_settings, - original_system_appearance, - new_theme: original_theme, // Start with the original theme. - selected_index, - selection_completed: false, - selected_theme: None, - selector, - } - } - - fn show_selected_theme( - &mut self, - cx: &mut Context>, - ) -> Option> { - if let Some(mat) = self.matches.get(self.selected_index) { - let registry = ThemeRegistry::global(cx); - - match registry.get(&mat.string) { - Ok(theme) => { - self.set_theme(theme.clone(), cx); - Some(theme) - } - Err(error) => { - log::error!("error loading theme {}: {}", mat.string, error); - None - } - } - } else { - None - } - } - - fn set_theme(&mut self, new_theme: Arc, cx: &mut App) { - // Update the global (in-memory) theme settings. - SettingsStore::update_global(cx, |store, _| { - override_global_theme( - store, - &new_theme, - &self.original_theme_settings.theme, - self.original_system_appearance, - ) - }); - - self.new_theme = new_theme; - } -} - -/// Overrides the global (in-memory) theme settings. -/// -/// Note that this does **not** update the user's `settings.json` file (see the -/// [`ThemeSelectorDelegate::confirm`] method and [`theme::set_theme`] function). -fn override_global_theme( - store: &mut SettingsStore, - new_theme: &Theme, - original_theme: &ThemeSelection, - system_appearance: Appearance, -) { - let theme_name = ThemeName(new_theme.name.clone().into()); - let new_appearance = new_theme.appearance(); - let new_theme_is_light = new_appearance.is_light(); - - let mut curr_theme_settings = store.get::(None).clone(); - - match (original_theme, &curr_theme_settings.theme) { - // Override the currently selected static theme. - (ThemeSelection::Static(_), ThemeSelection::Static(_)) => { - curr_theme_settings.theme = ThemeSelection::Static(theme_name); - } - - // If the current theme selection is dynamic, then only override the global setting for the - // specific mode (light or dark). - ( - ThemeSelection::Dynamic { - mode: original_mode, - light: original_light, - dark: original_dark, - }, - ThemeSelection::Dynamic { .. }, - ) => { - let new_mode = update_mode_if_new_appearance_is_different_from_system( - original_mode, - system_appearance, - new_appearance, - ); - - let updated_theme = retain_original_opposing_theme( - new_theme_is_light, - new_mode, - theme_name, - original_light, - original_dark, - ); - - curr_theme_settings.theme = updated_theme; - } - - // The theme selection mode changed while selecting new themes (someone edited the settings - // file on disk while we had the dialogue open), so don't do anything. - _ => return, - }; - - store.override_global(curr_theme_settings); -} - -/// Helper function for determining the new [`ThemeAppearanceMode`] for the new theme. -/// -/// If the the original theme mode was [`System`] and the new theme's appearance matches the system -/// appearance, we don't need to change the mode setting. -/// -/// Otherwise, we need to change the mode in order to see the new theme. -/// -/// [`System`]: ThemeAppearanceMode::System -fn update_mode_if_new_appearance_is_different_from_system( - original_mode: &ThemeAppearanceMode, - system_appearance: Appearance, - new_appearance: Appearance, -) -> ThemeAppearanceMode { - if original_mode == &ThemeAppearanceMode::System && system_appearance == new_appearance { - ThemeAppearanceMode::System - } else { - ThemeAppearanceMode::from(new_appearance) - } -} - -/// Helper function for updating / displaying the [`ThemeSelection`] while using the theme selector. -/// -/// We want to retain the alternate theme selection of the original settings (before the menu was -/// opened), not the currently selected theme (which likely has changed multiple times while the -/// menu has been open). -fn retain_original_opposing_theme( - new_theme_is_light: bool, - new_mode: ThemeAppearanceMode, - theme_name: ThemeName, - original_light: &ThemeName, - original_dark: &ThemeName, -) -> ThemeSelection { - if new_theme_is_light { - ThemeSelection::Dynamic { - mode: new_mode, - light: theme_name, - dark: original_dark.clone(), - } - } else { - ThemeSelection::Dynamic { - mode: new_mode, - light: original_light.clone(), - dark: theme_name, - } - } -} - -impl PickerDelegate for ThemeSelectorDelegate { - type ListItem = ui::ListItem; - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Select Theme...".into() - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn confirm( - &mut self, - _secondary: bool, - _window: &mut Window, - cx: &mut Context>, - ) { - self.selection_completed = true; - - let theme_name: Arc = self.new_theme.name.as_str().into(); - let theme_appearance = self.new_theme.appearance; - let system_appearance = SystemAppearance::global(cx).0; - - telemetry::event!("Settings Changed", setting = "theme", value = theme_name); - - update_settings_file(self.fs.clone(), cx, move |settings, _| { - theme::set_theme(settings, theme_name, theme_appearance, system_appearance); - }); - - self.selector - .update(cx, |_, cx| { - cx.emit(DismissEvent); - }) - .ok(); - } - - fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { - if !self.selection_completed { - SettingsStore::update_global(cx, |store, _| { - store.override_global(self.original_theme_settings.clone()); - }); - self.selection_completed = true; - } - - self.selector - .update(cx, |_, cx| cx.emit(DismissEvent)) - .log_err(); - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _: &mut Window, - cx: &mut Context>, - ) { - self.selected_index = ix; - self.selected_theme = self.show_selected_theme(cx); - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> gpui::Task<()> { - let background = cx.background_executor().clone(); - let candidates = self - .themes - .iter() - .enumerate() - .map(|(id, meta)| StringMatchCandidate::new(id, &meta.name)) - .collect::>(); - - cx.spawn_in(window, async move |this, cx| { - let matches = if query.is_empty() { - candidates - .into_iter() - .enumerate() - .map(|(index, candidate)| StringMatch { - candidate_id: index, - string: candidate.string, - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - match_strings( - &candidates, - &query, - false, - true, - 100, - &Default::default(), - background, - ) - .await - }; - - this.update(cx, |this, cx| { - this.delegate.matches = matches; - if query.is_empty() && this.delegate.selected_theme.is_none() { - this.delegate.selected_index = this - .delegate - .selected_index - .min(this.delegate.matches.len().saturating_sub(1)); - } else if let Some(selected) = this.delegate.selected_theme.as_ref() { - this.delegate.selected_index = this - .delegate - .matches - .iter() - .enumerate() - .find(|(_, mtch)| mtch.string == selected.name) - .map(|(ix, _)| ix) - .unwrap_or_default(); - } else { - this.delegate.selected_index = 0; - } - this.delegate.selected_theme = this.delegate.show_selected_theme(cx); - }) - .log_err(); - }) - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _window: &mut Window, - _cx: &mut Context>, - ) -> Option { - let theme_match = &self.matches.get(ix)?; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child(HighlightedLabel::new( - theme_match.string.clone(), - theme_match.positions.clone(), - )), - ) - } - - fn render_footer( - &self, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - Some( - h_flex() - .p_2() - .w_full() - .justify_between() - .gap_2() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child( - Button::new("docs", "View Theme Docs") - .icon(IconName::ArrowUpRight) - .icon_position(IconPosition::End) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .on_click(cx.listener(|_, _, _, cx| { - cx.open_url("https://zed.dev/docs/themes"); - })), - ) - .child( - Button::new("more-themes", "Install Themes").on_click(cx.listener({ - move |_, _, window, cx| { - window.dispatch_action( - Box::new(Extensions { - category_filter: Some(ExtensionCategoryFilter::Themes), - id: None, - }), - cx, - ); - } - })), - ) - .into_any_element(), - ) - } -} diff --git a/crates/time_format/Cargo.toml b/crates/time_format/Cargo.toml deleted file mode 100644 index b598d19887..0000000000 --- a/crates/time_format/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "time_format" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/time_format.rs" -doctest = false - -[dependencies] -sys-locale.workspace = true -time.workspace = true - -[target.'cfg(target_os = "macos")'.dependencies] -core-foundation.workspace = true -core-foundation-sys.workspace = true diff --git a/crates/time_format/LICENSE-GPL b/crates/time_format/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/time_format/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/time_format/src/time_format.rs b/crates/time_format/src/time_format.rs deleted file mode 100644 index 37d4201d1b..0000000000 --- a/crates/time_format/src/time_format.rs +++ /dev/null @@ -1,973 +0,0 @@ -use time::{OffsetDateTime, UtcOffset}; - -/// The formatting style for a timestamp. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TimestampFormat { - /// Formats the timestamp as an absolute time, e.g. "2021-12-31 3:00AM". - Absolute, - /// Formats the timestamp as an absolute time. - /// If the message is from today or yesterday the date will be replaced with "Today at x" or "Yesterday at x" respectively. - /// E.g. "Today at 12:00 PM", "Yesterday at 11:00 AM", "2021-12-31 3:00AM". - EnhancedAbsolute, - /// Formats the timestamp as an absolute time, using month name, day of month, year. e.g. "Feb. 24, 2024". - MediumAbsolute, - /// Formats the timestamp as a relative time, e.g. "just now", "1 minute ago", "2 hours ago", "2 months ago". - Relative, -} - -/// Formats a timestamp, which respects the user's date and time preferences/custom format. -pub fn format_localized_timestamp( - timestamp: OffsetDateTime, - reference: OffsetDateTime, - timezone: UtcOffset, - format: TimestampFormat, -) -> String { - let timestamp_local = timestamp.to_offset(timezone); - let reference_local = reference.to_offset(timezone); - format_local_timestamp(timestamp_local, reference_local, format) -} - -/// Formats a timestamp, which respects the user's date and time preferences/custom format. -pub fn format_local_timestamp( - timestamp: OffsetDateTime, - reference: OffsetDateTime, - format: TimestampFormat, -) -> String { - match format { - TimestampFormat::Absolute => format_absolute_timestamp(timestamp, reference, false), - TimestampFormat::EnhancedAbsolute => format_absolute_timestamp(timestamp, reference, true), - TimestampFormat::MediumAbsolute => format_absolute_timestamp_medium(timestamp, reference), - TimestampFormat::Relative => format_relative_time(timestamp, reference) - .unwrap_or_else(|| format_relative_date(timestamp, reference)), - } -} - -/// Formats the date component of a timestamp -pub fn format_date( - timestamp: OffsetDateTime, - reference: OffsetDateTime, - enhanced_formatting: bool, -) -> String { - format_absolute_date(timestamp, reference, enhanced_formatting) -} - -/// Formats the time component of a timestamp -pub fn format_time(timestamp: OffsetDateTime) -> String { - format_absolute_time(timestamp) -} - -/// Formats the date component of a timestamp in medium style -pub fn format_date_medium( - timestamp: OffsetDateTime, - reference: OffsetDateTime, - enhanced_formatting: bool, -) -> String { - format_absolute_date_medium(timestamp, reference, enhanced_formatting) -} - -fn format_absolute_date( - timestamp: OffsetDateTime, - reference: OffsetDateTime, - #[allow(unused_variables)] enhanced_date_formatting: bool, -) -> String { - #[cfg(target_os = "macos")] - { - if !enhanced_date_formatting { - return macos::format_date(×tamp); - } - - let timestamp_date = timestamp.date(); - let reference_date = reference.date(); - if timestamp_date == reference_date { - "Today".to_string() - } else if reference_date.previous_day() == Some(timestamp_date) { - "Yesterday".to_string() - } else { - macos::format_date(×tamp) - } - } - #[cfg(not(target_os = "macos"))] - { - // todo(linux) respect user's date/time preferences - // todo(windows) respect user's date/time preferences - let current_locale = CURRENT_LOCALE - .get_or_init(|| sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"))); - format_timestamp_naive_date( - timestamp, - reference, - is_12_hour_time_by_locale(current_locale.as_str()), - ) - } -} - -fn format_absolute_time(timestamp: OffsetDateTime) -> String { - #[cfg(target_os = "macos")] - { - macos::format_time(×tamp) - } - #[cfg(not(target_os = "macos"))] - { - // todo(linux) respect user's date/time preferences - // todo(windows) respect user's date/time preferences - let current_locale = CURRENT_LOCALE - .get_or_init(|| sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"))); - format_timestamp_naive_time( - timestamp, - is_12_hour_time_by_locale(current_locale.as_str()), - ) - } -} - -fn format_absolute_timestamp( - timestamp: OffsetDateTime, - reference: OffsetDateTime, - #[allow(unused_variables)] enhanced_date_formatting: bool, -) -> String { - #[cfg(target_os = "macos")] - { - if !enhanced_date_formatting { - return format!( - "{} {}", - format_absolute_date(timestamp, reference, enhanced_date_formatting), - format_absolute_time(timestamp) - ); - } - - let timestamp_date = timestamp.date(); - let reference_date = reference.date(); - if timestamp_date == reference_date { - format!("Today at {}", format_absolute_time(timestamp)) - } else if reference_date.previous_day() == Some(timestamp_date) { - format!("Yesterday at {}", format_absolute_time(timestamp)) - } else { - format!( - "{} {}", - format_absolute_date(timestamp, reference, enhanced_date_formatting), - format_absolute_time(timestamp) - ) - } - } - #[cfg(not(target_os = "macos"))] - { - // todo(linux) respect user's date/time preferences - // todo(windows) respect user's date/time preferences - format_timestamp_fallback(timestamp, reference) - } -} - -fn format_absolute_date_medium( - timestamp: OffsetDateTime, - reference: OffsetDateTime, - enhanced_formatting: bool, -) -> String { - #[cfg(target_os = "macos")] - { - if !enhanced_formatting { - return macos::format_date_medium(×tamp); - } - - let timestamp_date = timestamp.date(); - let reference_date = reference.date(); - if timestamp_date == reference_date { - "Today".to_string() - } else if reference_date.previous_day() == Some(timestamp_date) { - "Yesterday".to_string() - } else { - macos::format_date_medium(×tamp) - } - } - #[cfg(not(target_os = "macos"))] - { - // todo(linux) respect user's date/time preferences - // todo(windows) respect user's date/time preferences - let current_locale = CURRENT_LOCALE - .get_or_init(|| sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"))); - if !enhanced_formatting { - return format_timestamp_naive_date_medium( - timestamp, - is_12_hour_time_by_locale(current_locale.as_str()), - ); - } - - let timestamp_date = timestamp.date(); - let reference_date = reference.date(); - if timestamp_date == reference_date { - "Today".to_string() - } else if reference_date.previous_day() == Some(timestamp_date) { - "Yesterday".to_string() - } else { - format_timestamp_naive_date_medium( - timestamp, - is_12_hour_time_by_locale(current_locale.as_str()), - ) - } - } -} - -fn format_absolute_timestamp_medium( - timestamp: OffsetDateTime, - reference: OffsetDateTime, -) -> String { - #[cfg(target_os = "macos")] - { - format_absolute_date_medium(timestamp, reference, false) - } - #[cfg(not(target_os = "macos"))] - { - // todo(linux) respect user's date/time preferences - // todo(windows) respect user's date/time preferences - format_timestamp_fallback(timestamp, reference) - } -} - -fn format_relative_time(timestamp: OffsetDateTime, reference: OffsetDateTime) -> Option { - let difference = reference - timestamp; - let minutes = difference.whole_minutes(); - match minutes { - 0 => Some("Just now".to_string()), - 1 => Some("1 minute ago".to_string()), - 2..=59 => Some(format!("{} minutes ago", minutes)), - _ => { - let hours = difference.whole_hours(); - match hours { - 1 => Some("1 hour ago".to_string()), - 2..=23 => Some(format!("{} hours ago", hours)), - _ => None, - } - } - } -} - -fn format_relative_date(timestamp: OffsetDateTime, reference: OffsetDateTime) -> String { - let timestamp_date = timestamp.date(); - let reference_date = reference.date(); - let difference = reference_date - timestamp_date; - let days = difference.whole_days(); - match days { - 0 => "Today".to_string(), - 1 => "Yesterday".to_string(), - 2..=6 => format!("{} days ago", days), - _ => { - let weeks = difference.whole_weeks(); - match weeks { - 1 => "1 week ago".to_string(), - 2..=4 => format!("{} weeks ago", weeks), - _ => { - let month_diff = calculate_month_difference(timestamp, reference); - match month_diff { - 0..=1 => "1 month ago".to_string(), - 2..=11 => format!("{} months ago", month_diff), - _ => { - let timestamp_year = timestamp_date.year(); - let reference_year = reference_date.year(); - let years = reference_year - timestamp_year; - match years { - 1 => "1 year ago".to_string(), - _ => format!("{} years ago", years), - } - } - } - } - } - } - } -} - -/// Calculates the difference in months between two timestamps. -/// The reference timestamp should always be greater than the timestamp. -fn calculate_month_difference(timestamp: OffsetDateTime, reference: OffsetDateTime) -> usize { - let timestamp_year = timestamp.year(); - let reference_year = reference.year(); - let timestamp_month: u8 = timestamp.month().into(); - let reference_month: u8 = reference.month().into(); - - let month_diff = if reference_month >= timestamp_month { - reference_month as usize - timestamp_month as usize - } else { - 12 - timestamp_month as usize + reference_month as usize - }; - - let year_diff = (reference_year - timestamp_year) as usize; - if year_diff == 0 { - reference_month as usize - timestamp_month as usize - } else if month_diff == 0 { - year_diff * 12 - } else if timestamp_month > reference_month { - (year_diff - 1) * 12 + month_diff - } else { - year_diff * 12 + month_diff - } -} - -/// Formats a timestamp, which is either in 12-hour or 24-hour time format. -/// Note: -/// This function does not respect the user's date and time preferences. -/// This should only be used as a fallback mechanism when the OS time formatting fails. -fn format_timestamp_naive_time(timestamp_local: OffsetDateTime, is_12_hour_time: bool) -> String { - let timestamp_local_hour = timestamp_local.hour(); - let timestamp_local_minute = timestamp_local.minute(); - - let (hour, meridiem) = if is_12_hour_time { - let meridiem = if timestamp_local_hour >= 12 { - "PM" - } else { - "AM" - }; - - let hour_12 = match timestamp_local_hour { - 0 => 12, // Midnight - 13..=23 => timestamp_local_hour - 12, // PM hours - _ => timestamp_local_hour, // AM hours - }; - - (hour_12, Some(meridiem)) - } else { - (timestamp_local_hour, None) - }; - - match meridiem { - Some(meridiem) => format!("{}:{:02} {}", hour, timestamp_local_minute, meridiem), - None => format!("{:02}:{:02}", hour, timestamp_local_minute), - } -} - -#[cfg(not(target_os = "macos"))] -fn format_timestamp_naive_date( - timestamp_local: OffsetDateTime, - reference_local: OffsetDateTime, - is_12_hour_time: bool, -) -> String { - let reference_local_date = reference_local.date(); - let timestamp_local_date = timestamp_local.date(); - - if timestamp_local_date == reference_local_date { - "Today".to_string() - } else if reference_local_date.previous_day() == Some(timestamp_local_date) { - "Yesterday".to_string() - } else { - match is_12_hour_time { - true => format!( - "{:02}/{:02}/{}", - timestamp_local_date.month() as u32, - timestamp_local_date.day(), - timestamp_local_date.year() - ), - false => format!( - "{:02}/{:02}/{}", - timestamp_local_date.day(), - timestamp_local_date.month() as u32, - timestamp_local_date.year() - ), - } - } -} - -#[cfg(not(target_os = "macos"))] -fn format_timestamp_naive_date_medium( - timestamp_local: OffsetDateTime, - is_12_hour_time: bool, -) -> String { - let timestamp_local_date = timestamp_local.date(); - - match is_12_hour_time { - true => format!( - "{:02}/{:02}/{}", - timestamp_local_date.month() as u32, - timestamp_local_date.day(), - timestamp_local_date.year() - ), - false => format!( - "{:02}/{:02}/{}", - timestamp_local_date.day(), - timestamp_local_date.month() as u32, - timestamp_local_date.year() - ), - } -} - -pub fn format_timestamp_naive( - timestamp_local: OffsetDateTime, - reference_local: OffsetDateTime, - is_12_hour_time: bool, -) -> String { - let formatted_time = format_timestamp_naive_time(timestamp_local, is_12_hour_time); - let reference_local_date = reference_local.date(); - let timestamp_local_date = timestamp_local.date(); - - if timestamp_local_date == reference_local_date { - format!("Today at {}", formatted_time) - } else if reference_local_date.previous_day() == Some(timestamp_local_date) { - format!("Yesterday at {}", formatted_time) - } else { - let formatted_date = match is_12_hour_time { - true => format!( - "{:02}/{:02}/{}", - timestamp_local_date.month() as u32, - timestamp_local_date.day(), - timestamp_local_date.year() - ), - false => format!( - "{:02}/{:02}/{}", - timestamp_local_date.day(), - timestamp_local_date.month() as u32, - timestamp_local_date.year() - ), - }; - format!("{} {}", formatted_date, formatted_time) - } -} - -#[cfg(not(target_os = "macos"))] -static CURRENT_LOCALE: std::sync::OnceLock = std::sync::OnceLock::new(); - -#[cfg(not(target_os = "macos"))] -fn format_timestamp_fallback(timestamp: OffsetDateTime, reference: OffsetDateTime) -> String { - let current_locale = CURRENT_LOCALE - .get_or_init(|| sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"))); - - let is_12_hour_time = is_12_hour_time_by_locale(current_locale.as_str()); - format_timestamp_naive(timestamp, reference, is_12_hour_time) -} - -/// Returns `true` if the locale is recognized as a 12-hour time locale. -#[cfg(not(target_os = "macos"))] -fn is_12_hour_time_by_locale(locale: &str) -> bool { - [ - "es-MX", "es-CO", "es-SV", "es-NI", - "es-HN", // Mexico, Colombia, El Salvador, Nicaragua, Honduras - "en-US", "en-CA", "en-AU", "en-NZ", // U.S, Canada, Australia, New Zealand - "ar-SA", "ar-EG", "ar-JO", // Saudi Arabia, Egypt, Jordan - "en-IN", "hi-IN", // India, Hindu - "en-PK", "ur-PK", // Pakistan, Urdu - "en-PH", "fil-PH", // Philippines, Filipino - "bn-BD", "ccp-BD", // Bangladesh, Chakma - "en-IE", "ga-IE", // Ireland, Irish - "en-MY", "ms-MY", // Malaysia, Malay - ] - .contains(&locale) -} - -#[cfg(target_os = "macos")] -mod macos { - use core_foundation::base::TCFType; - use core_foundation::date::CFAbsoluteTime; - use core_foundation::string::CFString; - use core_foundation_sys::date_formatter::CFDateFormatterCreateStringWithAbsoluteTime; - use core_foundation_sys::date_formatter::CFDateFormatterRef; - use core_foundation_sys::locale::CFLocaleRef; - use core_foundation_sys::{ - base::kCFAllocatorDefault, - date_formatter::{ - CFDateFormatterCreate, kCFDateFormatterMediumStyle, kCFDateFormatterNoStyle, - kCFDateFormatterShortStyle, - }, - locale::CFLocaleCopyCurrent, - }; - - pub fn format_time(timestamp: &time::OffsetDateTime) -> String { - format_with_date_formatter(timestamp, TIME_FORMATTER.with(|f| *f)) - } - - pub fn format_date(timestamp: &time::OffsetDateTime) -> String { - format_with_date_formatter(timestamp, DATE_FORMATTER.with(|f| *f)) - } - - pub fn format_date_medium(timestamp: &time::OffsetDateTime) -> String { - format_with_date_formatter(timestamp, MEDIUM_DATE_FORMATTER.with(|f| *f)) - } - - fn format_with_date_formatter( - timestamp: &time::OffsetDateTime, - fmt: CFDateFormatterRef, - ) -> String { - const UNIX_TO_CF_ABSOLUTE_TIME_OFFSET: i64 = 978307200; - // Convert timestamp to macOS absolute time - let timestamp_macos = timestamp.unix_timestamp() - UNIX_TO_CF_ABSOLUTE_TIME_OFFSET; - let cf_absolute_time = timestamp_macos as CFAbsoluteTime; - unsafe { - let s = CFDateFormatterCreateStringWithAbsoluteTime( - kCFAllocatorDefault, - fmt, - cf_absolute_time, - ); - CFString::wrap_under_create_rule(s).to_string() - } - } - - thread_local! { - static CURRENT_LOCALE: CFLocaleRef = unsafe { CFLocaleCopyCurrent() }; - static TIME_FORMATTER: CFDateFormatterRef = unsafe { - CFDateFormatterCreate( - kCFAllocatorDefault, - CURRENT_LOCALE.with(|locale| *locale), - kCFDateFormatterNoStyle, - kCFDateFormatterShortStyle, - ) - }; - static DATE_FORMATTER: CFDateFormatterRef = unsafe { - CFDateFormatterCreate( - kCFAllocatorDefault, - CURRENT_LOCALE.with(|locale| *locale), - kCFDateFormatterShortStyle, - kCFDateFormatterNoStyle, - ) - }; - - static MEDIUM_DATE_FORMATTER: CFDateFormatterRef = unsafe { - CFDateFormatterCreate( - kCFAllocatorDefault, - CURRENT_LOCALE.with(|locale| *locale), - kCFDateFormatterMediumStyle, - kCFDateFormatterNoStyle, - ) - }; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_format_date() { - let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0); - - // Test with same date (today) - let timestamp_today = create_offset_datetime(1990, 4, 12, 9, 30, 0); - assert_eq!(format_date(timestamp_today, reference, true), "Today"); - - // Test with previous day (yesterday) - let timestamp_yesterday = create_offset_datetime(1990, 4, 11, 9, 30, 0); - assert_eq!( - format_date(timestamp_yesterday, reference, true), - "Yesterday" - ); - - // Test with other date - let timestamp_other = create_offset_datetime(1990, 4, 10, 9, 30, 0); - let result = format_date(timestamp_other, reference, true); - assert!(!result.is_empty()); - assert_ne!(result, "Today"); - assert_ne!(result, "Yesterday"); - } - - #[test] - fn test_format_time() { - let timestamp = create_offset_datetime(1990, 4, 12, 9, 30, 0); - - // We can't assert the exact output as it depends on the platform and locale - // But we can at least confirm it doesn't panic and returns a non-empty string - let result = format_time(timestamp); - assert!(!result.is_empty()); - } - - #[test] - fn test_format_date_medium() { - let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0); - let timestamp = create_offset_datetime(1990, 4, 12, 9, 30, 0); - - // Test with enhanced formatting (today) - let result_enhanced = format_date_medium(timestamp, reference, true); - assert_eq!(result_enhanced, "Today"); - - // Test with standard formatting - let result_standard = format_date_medium(timestamp, reference, false); - assert!(!result_standard.is_empty()); - - // Test yesterday with enhanced formatting - let timestamp_yesterday = create_offset_datetime(1990, 4, 11, 9, 30, 0); - let result_yesterday = format_date_medium(timestamp_yesterday, reference, true); - assert_eq!(result_yesterday, "Yesterday"); - - // Test other date with enhanced formatting - let timestamp_other = create_offset_datetime(1990, 4, 10, 9, 30, 0); - let result_other = format_date_medium(timestamp_other, reference, true); - assert!(!result_other.is_empty()); - assert_ne!(result_other, "Today"); - assert_ne!(result_other, "Yesterday"); - } - - #[test] - fn test_format_absolute_time() { - let timestamp = create_offset_datetime(1990, 4, 12, 9, 30, 0); - - // We can't assert the exact output as it depends on the platform and locale - // But we can at least confirm it doesn't panic and returns a non-empty string - let result = format_absolute_time(timestamp); - assert!(!result.is_empty()); - } - - #[test] - fn test_format_absolute_date() { - let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0); - - // Test with same date (today) - let timestamp_today = create_offset_datetime(1990, 4, 12, 9, 30, 0); - assert_eq!( - format_absolute_date(timestamp_today, reference, true), - "Today" - ); - - // Test with previous day (yesterday) - let timestamp_yesterday = create_offset_datetime(1990, 4, 11, 9, 30, 0); - assert_eq!( - format_absolute_date(timestamp_yesterday, reference, true), - "Yesterday" - ); - - // Test with other date - let timestamp_other = create_offset_datetime(1990, 4, 10, 9, 30, 0); - let result = format_absolute_date(timestamp_other, reference, true); - assert!(!result.is_empty()); - assert_ne!(result, "Today"); - assert_ne!(result, "Yesterday"); - } - - #[test] - fn test_format_absolute_date_medium() { - let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0); - let timestamp = create_offset_datetime(1990, 4, 12, 9, 30, 0); - - // Test with enhanced formatting (today) - let result_enhanced = format_absolute_date_medium(timestamp, reference, true); - assert_eq!(result_enhanced, "Today"); - - // Test with standard formatting - let result_standard = format_absolute_date_medium(timestamp, reference, false); - assert!(!result_standard.is_empty()); - - // Test yesterday with enhanced formatting - let timestamp_yesterday = create_offset_datetime(1990, 4, 11, 9, 30, 0); - let result_yesterday = format_absolute_date_medium(timestamp_yesterday, reference, true); - assert_eq!(result_yesterday, "Yesterday"); - } - - #[test] - fn test_format_timestamp_naive_time() { - let timestamp = create_offset_datetime(1990, 4, 12, 9, 30, 0); - assert_eq!(format_timestamp_naive_time(timestamp, true), "9:30 AM"); - assert_eq!(format_timestamp_naive_time(timestamp, false), "09:30"); - - let timestamp_pm = create_offset_datetime(1990, 4, 12, 15, 45, 0); - assert_eq!(format_timestamp_naive_time(timestamp_pm, true), "3:45 PM"); - assert_eq!(format_timestamp_naive_time(timestamp_pm, false), "15:45"); - } - - #[test] - fn test_format_24_hour_time() { - let reference = create_offset_datetime(1990, 4, 12, 16, 45, 0); - let timestamp = create_offset_datetime(1990, 4, 12, 15, 30, 0); - - assert_eq!( - format_timestamp_naive(timestamp, reference, false), - "Today at 15:30" - ); - } - - #[test] - fn test_format_today() { - let reference = create_offset_datetime(1990, 4, 12, 16, 45, 0); - let timestamp = create_offset_datetime(1990, 4, 12, 15, 30, 0); - - assert_eq!( - format_timestamp_naive(timestamp, reference, true), - "Today at 3:30 PM" - ); - } - - #[test] - fn test_format_yesterday() { - let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0); - let timestamp = create_offset_datetime(1990, 4, 11, 9, 0, 0); - - assert_eq!( - format_timestamp_naive(timestamp, reference, true), - "Yesterday at 9:00 AM" - ); - } - - #[test] - fn test_format_yesterday_less_than_24_hours_ago() { - let reference = create_offset_datetime(1990, 4, 12, 19, 59, 0); - let timestamp = create_offset_datetime(1990, 4, 11, 20, 0, 0); - - assert_eq!( - format_timestamp_naive(timestamp, reference, true), - "Yesterday at 8:00 PM" - ); - } - - #[test] - fn test_format_yesterday_more_than_24_hours_ago() { - let reference = create_offset_datetime(1990, 4, 12, 19, 59, 0); - let timestamp = create_offset_datetime(1990, 4, 11, 18, 0, 0); - - assert_eq!( - format_timestamp_naive(timestamp, reference, true), - "Yesterday at 6:00 PM" - ); - } - - #[test] - fn test_format_yesterday_over_midnight() { - let reference = create_offset_datetime(1990, 4, 12, 0, 5, 0); - let timestamp = create_offset_datetime(1990, 4, 11, 23, 55, 0); - - assert_eq!( - format_timestamp_naive(timestamp, reference, true), - "Yesterday at 11:55 PM" - ); - } - - #[test] - fn test_format_yesterday_over_month() { - let reference = create_offset_datetime(1990, 4, 2, 9, 0, 0); - let timestamp = create_offset_datetime(1990, 4, 1, 20, 0, 0); - - assert_eq!( - format_timestamp_naive(timestamp, reference, true), - "Yesterday at 8:00 PM" - ); - } - - #[test] - fn test_format_before_yesterday() { - let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0); - let timestamp = create_offset_datetime(1990, 4, 10, 20, 20, 0); - - assert_eq!( - format_timestamp_naive(timestamp, reference, true), - "04/10/1990 8:20 PM" - ); - } - - #[test] - fn test_relative_format_minutes() { - let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0); - let mut current_timestamp = reference; - - let mut next_minute = || { - current_timestamp = if current_timestamp.minute() == 0 { - current_timestamp - .replace_hour(current_timestamp.hour() - 1) - .unwrap() - .replace_minute(59) - .unwrap() - } else { - current_timestamp - .replace_minute(current_timestamp.minute() - 1) - .unwrap() - }; - current_timestamp - }; - - assert_eq!( - format_relative_time(reference, reference), - Some("Just now".to_string()) - ); - - assert_eq!( - format_relative_time(next_minute(), reference), - Some("1 minute ago".to_string()) - ); - - for i in 2..=59 { - assert_eq!( - format_relative_time(next_minute(), reference), - Some(format!("{} minutes ago", i)) - ); - } - - assert_eq!( - format_relative_time(next_minute(), reference), - Some("1 hour ago".to_string()) - ); - } - - #[test] - fn test_relative_format_hours() { - let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0); - let mut current_timestamp = reference; - - let mut next_hour = || { - current_timestamp = if current_timestamp.hour() == 0 { - let date = current_timestamp.date().previous_day().unwrap(); - current_timestamp.replace_date(date) - } else { - current_timestamp - .replace_hour(current_timestamp.hour() - 1) - .unwrap() - }; - current_timestamp - }; - - assert_eq!( - format_relative_time(next_hour(), reference), - Some("1 hour ago".to_string()) - ); - - for i in 2..=23 { - assert_eq!( - format_relative_time(next_hour(), reference), - Some(format!("{} hours ago", i)) - ); - } - - assert_eq!(format_relative_time(next_hour(), reference), None); - } - - #[test] - fn test_relative_format_days() { - let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0); - let mut current_timestamp = reference; - - let mut next_day = || { - let date = current_timestamp.date().previous_day().unwrap(); - current_timestamp = current_timestamp.replace_date(date); - current_timestamp - }; - - assert_eq!( - format_relative_date(reference, reference), - "Today".to_string() - ); - - assert_eq!( - format_relative_date(next_day(), reference), - "Yesterday".to_string() - ); - - for i in 2..=6 { - assert_eq!( - format_relative_date(next_day(), reference), - format!("{} days ago", i) - ); - } - - assert_eq!(format_relative_date(next_day(), reference), "1 week ago"); - } - - #[test] - fn test_relative_format_weeks() { - let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0); - let mut current_timestamp = reference; - - let mut next_week = || { - for _ in 0..7 { - let date = current_timestamp.date().previous_day().unwrap(); - current_timestamp = current_timestamp.replace_date(date); - } - current_timestamp - }; - - assert_eq!( - format_relative_date(next_week(), reference), - "1 week ago".to_string() - ); - - for i in 2..=4 { - assert_eq!( - format_relative_date(next_week(), reference), - format!("{} weeks ago", i) - ); - } - - assert_eq!(format_relative_date(next_week(), reference), "1 month ago"); - } - - #[test] - fn test_relative_format_months() { - let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0); - let mut current_timestamp = reference; - - let mut next_month = || { - if current_timestamp.month() == time::Month::January { - current_timestamp = current_timestamp - .replace_month(time::Month::December) - .unwrap() - .replace_year(current_timestamp.year() - 1) - .unwrap(); - } else { - current_timestamp = current_timestamp - .replace_month(current_timestamp.month().previous()) - .unwrap(); - } - current_timestamp - }; - - assert_eq!( - format_relative_date(next_month(), reference), - "4 weeks ago".to_string() - ); - - for i in 2..=11 { - assert_eq!( - format_relative_date(next_month(), reference), - format!("{} months ago", i) - ); - } - - assert_eq!(format_relative_date(next_month(), reference), "1 year ago"); - } - - #[test] - fn test_calculate_month_difference() { - let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0); - - assert_eq!(calculate_month_difference(reference, reference), 0); - - assert_eq!( - calculate_month_difference(create_offset_datetime(1990, 1, 12, 23, 0, 0), reference), - 3 - ); - - assert_eq!( - calculate_month_difference(create_offset_datetime(1989, 11, 12, 23, 0, 0), reference), - 5 - ); - - assert_eq!( - calculate_month_difference(create_offset_datetime(1989, 4, 12, 23, 0, 0), reference), - 12 - ); - - assert_eq!( - calculate_month_difference(create_offset_datetime(1989, 3, 12, 23, 0, 0), reference), - 13 - ); - - assert_eq!( - calculate_month_difference(create_offset_datetime(1987, 5, 12, 23, 0, 0), reference), - 35 - ); - - assert_eq!( - calculate_month_difference(create_offset_datetime(1987, 4, 12, 23, 0, 0), reference), - 36 - ); - - assert_eq!( - calculate_month_difference(create_offset_datetime(1987, 3, 12, 23, 0, 0), reference), - 37 - ); - } - - fn test_timezone() -> UtcOffset { - UtcOffset::from_hms(0, 0, 0).expect("Valid timezone offset") - } - - fn create_offset_datetime( - year: i32, - month: u8, - day: u8, - hour: u8, - minute: u8, - second: u8, - ) -> OffsetDateTime { - let date = time::Date::from_calendar_date(year, time::Month::try_from(month).unwrap(), day) - .unwrap(); - let time = time::Time::from_hms(hour, minute, second).unwrap(); - let date = date.with_time(time).assume_utc(); // Assume UTC for simplicity - date.to_offset(test_timezone()) - } -} diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml deleted file mode 100644 index 6d5d0ce170..0000000000 --- a/crates/title_bar/Cargo.toml +++ /dev/null @@ -1,71 +0,0 @@ -[package] -name = "title_bar" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/title_bar.rs" -doctest = false - -[features] -default = [] -stories = ["dep:story"] -test-support = [ - "call/test-support", - "client/test-support", - "collections/test-support", - "gpui/test-support", - "http_client/test-support", - "project/test-support", - "util/test-support", - "workspace/test-support", -] - -[dependencies] -anyhow.workspace = true -auto_update.workspace = true -call.workspace = true -channel.workspace = true -chrono.workspace = true -client.workspace = true -cloud_llm_client.workspace = true -db.workspace = true -gpui = { workspace = true, features = ["screen-capture"] } -notifications.workspace = true -project.workspace = true -remote.workspace = true -rpc.workspace = true -schemars.workspace = true -serde.workspace = true -settings.workspace = true -smallvec.workspace = true -story = { workspace = true, optional = true } -telemetry.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[target.'cfg(windows)'.dependencies] -windows.workspace = true - -[dev-dependencies] -call = { workspace = true, features = ["test-support"] } -client = { workspace = true, features = ["test-support"] } -collections = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -http_client = { workspace = true, features = ["test-support"] } -notifications = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true -project = { workspace = true, features = ["test-support"] } -rpc = { workspace = true, features = ["test-support"] } -settings = { workspace = true, features = ["test-support"] } -tree-sitter-md.workspace = true -util = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/title_bar/LICENSE-GPL b/crates/title_bar/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/title_bar/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/title_bar/src/application_menu.rs b/crates/title_bar/src/application_menu.rs deleted file mode 100644 index 817b73c45e..0000000000 --- a/crates/title_bar/src/application_menu.rs +++ /dev/null @@ -1,324 +0,0 @@ -use gpui::{Entity, OwnedMenu, OwnedMenuItem}; -use settings::Settings; - -#[cfg(not(target_os = "macos"))] -use gpui::{Action, actions}; - -#[cfg(not(target_os = "macos"))] -use schemars::JsonSchema; -#[cfg(not(target_os = "macos"))] -use serde::Deserialize; - -use smallvec::SmallVec; -use ui::{ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*}; - -use crate::title_bar_settings::TitleBarSettings; - -#[cfg(not(target_os = "macos"))] -actions!( - app_menu, - [ - /// Navigates to the menu item on the right. - ActivateMenuRight, - /// Navigates to the menu item on the left. - ActivateMenuLeft - ] -); - -#[cfg(not(target_os = "macos"))] -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Default, Action)] -#[action(namespace = app_menu)] -pub struct OpenApplicationMenu(String); - -#[cfg(not(target_os = "macos"))] -pub enum ActivateDirection { - Left, - Right, -} - -#[derive(Clone)] -struct MenuEntry { - menu: OwnedMenu, - handle: PopoverMenuHandle, -} - -pub struct ApplicationMenu { - entries: SmallVec<[MenuEntry; 8]>, - pending_menu_open: Option, -} - -impl ApplicationMenu { - pub fn new(_: &mut Window, cx: &mut Context) -> Self { - let menus = cx.get_menus().unwrap_or_default(); - Self { - entries: menus - .into_iter() - .map(|menu| MenuEntry { - menu, - handle: PopoverMenuHandle::default(), - }) - .collect(), - pending_menu_open: None, - } - } - - fn sanitize_menu_items(items: Vec) -> Vec { - let mut cleaned = Vec::new(); - let mut last_was_separator = false; - - for item in items { - match item { - OwnedMenuItem::Separator => { - if !last_was_separator { - cleaned.push(item); - last_was_separator = true; - } - } - OwnedMenuItem::Submenu(submenu) => { - // Skip empty submenus - if !submenu.items.is_empty() { - cleaned.push(OwnedMenuItem::Submenu(submenu)); - last_was_separator = false; - } - } - item => { - cleaned.push(item); - last_was_separator = false; - } - } - } - - // Remove trailing separator - if let Some(OwnedMenuItem::Separator) = cleaned.last() { - cleaned.pop(); - } - - cleaned - } - - fn build_menu_from_items( - entry: MenuEntry, - window: &mut Window, - cx: &mut App, - ) -> Entity { - ContextMenu::build(window, cx, |menu, window, cx| { - // Grab current focus handle so menu can shown items in context with the focused element - let menu = menu.when_some(window.focused(cx), |menu, focused| menu.context(focused)); - let sanitized_items = Self::sanitize_menu_items(entry.menu.items); - - sanitized_items - .into_iter() - .fold(menu, |menu, item| match item { - OwnedMenuItem::Separator => menu.separator(), - OwnedMenuItem::Action { - name, - action, - checked, - .. - } => menu.action_checked(name, action, checked), - OwnedMenuItem::Submenu(submenu) => { - submenu - .items - .into_iter() - .fold(menu, |menu, item| match item { - OwnedMenuItem::Separator => menu.separator(), - OwnedMenuItem::Action { - name, - action, - checked, - .. - } => menu.action_checked(name, action, checked), - OwnedMenuItem::Submenu(_) => menu, - OwnedMenuItem::SystemMenu(_) => { - // A system menu doesn't make sense in this context, so ignore it - menu - } - }) - } - OwnedMenuItem::SystemMenu(_) => { - // A system menu doesn't make sense in this context, so ignore it - menu - } - }) - }) - } - - fn render_application_menu(&self, entry: &MenuEntry) -> impl IntoElement { - let handle = entry.handle.clone(); - - let menu_name = entry.menu.name.clone(); - let entry = entry.clone(); - - // Application menu must have same ids as first menu item in standard menu - div() - .id(format!("{}-menu-item", menu_name)) - .occlude() - .child( - PopoverMenu::new(format!("{}-menu-popover", menu_name)) - .menu(move |window, cx| { - Self::build_menu_from_items(entry.clone(), window, cx).into() - }) - .trigger_with_tooltip( - IconButton::new( - SharedString::from(format!("{}-menu-trigger", menu_name)), - ui::IconName::Menu, - ) - .style(ButtonStyle::Subtle) - .icon_size(IconSize::Small), - Tooltip::text("Open Application Menu"), - ) - .with_handle(handle), - ) - } - - fn render_standard_menu(&self, entry: &MenuEntry) -> impl IntoElement { - let current_handle = entry.handle.clone(); - - let menu_name = entry.menu.name.clone(); - let entry = entry.clone(); - - let all_handles: Vec<_> = self - .entries - .iter() - .map(|entry| entry.handle.clone()) - .collect(); - - div() - .id(format!("{}-menu-item", menu_name)) - .occlude() - .child( - PopoverMenu::new(format!("{}-menu-popover", menu_name)) - .menu(move |window, cx| { - Self::build_menu_from_items(entry.clone(), window, cx).into() - }) - .trigger( - Button::new( - SharedString::from(format!("{}-menu-trigger", menu_name)), - menu_name, - ) - .style(ButtonStyle::Subtle) - .label_size(LabelSize::Small), - ) - .with_handle(current_handle.clone()), - ) - .on_hover(move |hover_enter, window, cx| { - if *hover_enter && !current_handle.is_deployed() { - all_handles.iter().for_each(|h| h.hide(cx)); - - // We need to defer this so that this menu handle can take focus from the previous menu - let handle = current_handle.clone(); - window.defer(cx, move |window, cx| handle.show(window, cx)); - } - }) - } - - #[cfg(not(target_os = "macos"))] - pub fn open_menu( - &mut self, - action: &OpenApplicationMenu, - _window: &mut Window, - _cx: &mut Context, - ) { - self.pending_menu_open = Some(action.0.clone()); - } - - #[cfg(not(target_os = "macos"))] - pub fn navigate_menus_in_direction( - &mut self, - direction: ActivateDirection, - window: &mut Window, - cx: &mut Context, - ) { - let current_index = self - .entries - .iter() - .position(|entry| entry.handle.is_deployed()); - let Some(current_index) = current_index else { - return; - }; - - let next_index = match direction { - ActivateDirection::Left => { - if current_index == 0 { - self.entries.len() - 1 - } else { - current_index - 1 - } - } - ActivateDirection::Right => { - if current_index == self.entries.len() - 1 { - 0 - } else { - current_index + 1 - } - } - }; - - self.entries[current_index].handle.hide(cx); - - // We need to defer this so that this menu handle can take focus from the previous menu - let next_handle = self.entries[next_index].handle.clone(); - cx.defer_in(window, move |_, window, cx| next_handle.show(window, cx)); - } - - pub fn all_menus_shown(&self, cx: &mut Context) -> bool { - show_menus(cx) - || self.entries.iter().any(|entry| entry.handle.is_deployed()) - || self.pending_menu_open.is_some() - } -} - -pub(crate) fn show_menus(cx: &mut App) -> bool { - TitleBarSettings::get_global(cx).show_menus - && (cfg!(not(target_os = "macos")) || option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some()) -} - -impl Render for ApplicationMenu { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let all_menus_shown = self.all_menus_shown(cx); - - if let Some(pending_menu_open) = self.pending_menu_open.take() - && let Some(entry) = self - .entries - .iter() - .find(|entry| entry.menu.name == pending_menu_open && !entry.handle.is_deployed()) - { - let handle_to_show = entry.handle.clone(); - let handles_to_hide: Vec<_> = self - .entries - .iter() - .filter(|e| e.menu.name != pending_menu_open && e.handle.is_deployed()) - .map(|e| e.handle.clone()) - .collect(); - - if handles_to_hide.is_empty() { - // We need to wait for the next frame to show all menus first, - // before we can handle show/hide operations - window.on_next_frame(move |window, cx| { - handles_to_hide.iter().for_each(|handle| handle.hide(cx)); - window.defer(cx, move |window, cx| handle_to_show.show(window, cx)); - }); - } else { - // Since menus are already shown, we can directly handle show/hide operations - handles_to_hide.iter().for_each(|handle| handle.hide(cx)); - cx.defer_in(window, move |_, window, cx| handle_to_show.show(window, cx)); - } - } - - div() - .key_context("ApplicationMenu") - .flex() - .flex_row() - .gap_x_1() - .when(!all_menus_shown && !self.entries.is_empty(), |this| { - this.child(self.render_application_menu(&self.entries[0])) - }) - .when(all_menus_shown, |this| { - this.children( - self.entries - .iter() - .map(|entry| self.render_standard_menu(entry)), - ) - }) - } -} diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs deleted file mode 100644 index 8a2d23dd26..0000000000 --- a/crates/title_bar/src/collab.rs +++ /dev/null @@ -1,627 +0,0 @@ -use std::rc::Rc; -use std::sync::Arc; - -use call::{ActiveCall, ParticipantLocation, Room}; -use channel::ChannelStore; -use client::{User, proto::PeerId}; -use gpui::{ - AnyElement, Hsla, IntoElement, MouseButton, Path, ScreenCaptureSource, Styled, WeakEntity, - canvas, point, -}; -use gpui::{App, Task, Window}; -use project::WorktreeSettings; -use rpc::proto::{self}; -use settings::{Settings as _, SettingsLocation}; -use theme::ActiveTheme; -use ui::{ - Avatar, AvatarAudioStatusIndicator, ContextMenu, ContextMenuItem, Divider, DividerColor, - Facepile, PopoverMenu, SplitButton, SplitButtonStyle, TintColor, Tooltip, prelude::*, -}; -use util::rel_path::RelPath; -use workspace::notifications::DetachAndPromptErr; - -use crate::TitleBar; - -pub fn toggle_screen_sharing( - screen: anyhow::Result>>, - window: &mut Window, - cx: &mut App, -) { - let call = ActiveCall::global(cx).read(cx); - let toggle_screen_sharing = match screen { - Ok(screen) => { - let Some(room) = call.room().cloned() else { - return; - }; - - room.update(cx, |room, cx| { - let clicked_on_currently_shared_screen = - room.shared_screen_id().is_some_and(|screen_id| { - Some(screen_id) - == screen - .as_deref() - .and_then(|s| s.metadata().ok().map(|meta| meta.id)) - }); - let should_unshare_current_screen = room.is_sharing_screen(); - let unshared_current_screen = should_unshare_current_screen.then(|| { - telemetry::event!( - "Screen Share Disabled", - room_id = room.id(), - channel_id = room.channel_id(), - ); - room.unshare_screen(clicked_on_currently_shared_screen || screen.is_none(), cx) - }); - if let Some(screen) = screen { - if !should_unshare_current_screen { - telemetry::event!( - "Screen Share Enabled", - room_id = room.id(), - channel_id = room.channel_id(), - ); - } - cx.spawn(async move |room, cx| { - unshared_current_screen.transpose()?; - if !clicked_on_currently_shared_screen { - room.update(cx, |room, cx| room.share_screen(screen, cx))? - .await - } else { - Ok(()) - } - }) - } else { - Task::ready(Ok(())) - } - }) - } - Err(e) => Task::ready(Err(e)), - }; - toggle_screen_sharing.detach_and_prompt_err("Sharing Screen Failed", window, cx, |e, _, _| Some(format!("{:?}\n\nPlease check that you have given Zed permissions to record your screen in Settings.", e))); -} - -pub fn toggle_mute(cx: &mut App) { - let call = ActiveCall::global(cx).read(cx); - if let Some(room) = call.room().cloned() { - room.update(cx, |room, cx| { - let operation = if room.is_muted() { - "Microphone Enabled" - } else { - "Microphone Disabled" - }; - telemetry::event!( - operation, - room_id = room.id(), - channel_id = room.channel_id(), - ); - - room.toggle_mute(cx) - }); - } -} - -pub fn toggle_deafen(cx: &mut App) { - if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - room.update(cx, |room, cx| room.toggle_deafen(cx)); - } -} - -fn render_color_ribbon(color: Hsla) -> impl Element { - canvas( - move |_, _, _| {}, - move |bounds, _, window, _| { - let height = bounds.size.height; - let horizontal_offset = height; - let vertical_offset = height / 2.0; - let mut path = Path::new(bounds.bottom_left()); - path.curve_to( - bounds.origin + point(horizontal_offset, vertical_offset), - bounds.origin + point(px(0.0), vertical_offset), - ); - path.line_to(bounds.top_right() + point(-horizontal_offset, vertical_offset)); - path.curve_to( - bounds.bottom_right(), - bounds.top_right() + point(px(0.0), vertical_offset), - ); - path.line_to(bounds.bottom_left()); - window.paint_path(path, color); - }, - ) - .h_1() - .w_full() -} - -impl TitleBar { - pub(crate) fn render_collaborator_list( - &self, - _: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let room = ActiveCall::global(cx).read(cx).room().cloned(); - let current_user = self.user_store.read(cx).current_user(); - let client = self.client.clone(); - let project_id = self.project.read(cx).remote_id(); - let workspace = self.workspace.upgrade(); - - h_flex() - .id("collaborator-list") - .w_full() - .gap_1() - .overflow_x_scroll() - .when_some( - current_user.zip(client.peer_id()).zip(room), - |this, ((current_user, peer_id), room)| { - let player_colors = cx.theme().players(); - let room = room.read(cx); - let mut remote_participants = - room.remote_participants().values().collect::>(); - remote_participants.sort_by_key(|p| p.participant_index.0); - - let current_user_face_pile = self.render_collaborator( - ¤t_user, - peer_id, - true, - room.is_speaking(), - room.is_muted(), - None, - room, - project_id, - ¤t_user, - cx, - ); - - this.children(current_user_face_pile.map(|face_pile| { - v_flex() - .on_mouse_down(MouseButton::Left, |_, window, _| { - window.prevent_default() - }) - .child(face_pile) - .child(render_color_ribbon(player_colors.local().cursor)) - })) - .children(remote_participants.iter().filter_map(|collaborator| { - let player_color = - player_colors.color_for_participant(collaborator.participant_index.0); - let is_following = workspace - .as_ref()? - .read(cx) - .is_being_followed(collaborator.peer_id); - let is_present = project_id.is_some_and(|project_id| { - collaborator.location - == ParticipantLocation::SharedProject { project_id } - }); - - let facepile = self.render_collaborator( - &collaborator.user, - collaborator.peer_id, - is_present, - collaborator.speaking, - collaborator.muted, - is_following.then_some(player_color.selection), - room, - project_id, - ¤t_user, - cx, - )?; - - Some( - v_flex() - .id(("collaborator", collaborator.user.id)) - .child(facepile) - .child(render_color_ribbon(player_color.cursor)) - .cursor_pointer() - .on_mouse_down(MouseButton::Left, |_, window, _| { - window.prevent_default() - }) - .on_click({ - let peer_id = collaborator.peer_id; - cx.listener(move |this, _, window, cx| { - cx.stop_propagation(); - - this.workspace - .update(cx, |workspace, cx| { - if is_following { - workspace.unfollow(peer_id, window, cx); - } else { - workspace.follow(peer_id, window, cx); - } - }) - .ok(); - }) - }) - .tooltip({ - let login = collaborator.user.github_login.clone(); - Tooltip::text(format!("Follow {login}")) - }), - ) - })) - }, - ) - } - - fn render_collaborator( - &self, - user: &Arc, - peer_id: PeerId, - is_present: bool, - is_speaking: bool, - is_muted: bool, - leader_selection_color: Option, - room: &Room, - project_id: Option, - current_user: &Arc, - cx: &App, - ) -> Option
{ - if room.role_for_user(user.id) == Some(proto::ChannelRole::Guest) { - return None; - } - - const FACEPILE_LIMIT: usize = 3; - let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id)); - let extra_count = followers.len().saturating_sub(FACEPILE_LIMIT); - - Some( - div() - .m_0p5() - .p_0p5() - // When the collaborator is not followed, still draw this wrapper div, but leave - // it transparent, so that it does not shift the layout when following. - .when_some(leader_selection_color, |div, color| { - div.rounded_sm().bg(color) - }) - .child( - Facepile::empty() - .child( - Avatar::new(user.avatar_uri.clone()) - .grayscale(!is_present) - .border_color(if is_speaking { - cx.theme().status().info - } else { - // We draw the border in a transparent color rather to avoid - // the layout shift that would come with adding/removing the border. - gpui::transparent_black() - }) - .when(is_muted, |avatar| { - avatar.indicator( - AvatarAudioStatusIndicator::new(ui::AudioStatus::Muted) - .tooltip({ - let github_login = user.github_login.clone(); - Tooltip::text(format!("{} is muted", github_login)) - }), - ) - }), - ) - .children(followers.iter().take(FACEPILE_LIMIT).filter_map( - |follower_peer_id| { - let follower = room - .remote_participants() - .values() - .find_map(|p| { - (p.peer_id == *follower_peer_id).then_some(&p.user) - }) - .or_else(|| { - (self.client.peer_id() == Some(*follower_peer_id)) - .then_some(current_user) - })? - .clone(); - - Some(div().mt(-px(4.)).child( - Avatar::new(follower.avatar_uri.clone()).size(rems(0.75)), - )) - }, - )) - .children(if extra_count > 0 { - Some( - Label::new(format!("+{extra_count}")) - .ml_1() - .into_any_element(), - ) - } else { - None - }), - ), - ) - } - - pub(crate) fn render_call_controls( - &self, - window: &mut Window, - cx: &mut Context, - ) -> Vec { - let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() else { - return Vec::new(); - }; - - let is_connecting_to_project = self - .workspace - .update(cx, |workspace, cx| workspace.has_active_modal(window, cx)) - .unwrap_or(false); - - let room = room.read(cx); - let project = self.project.read(cx); - let is_local = project.is_local() || project.is_via_remote_server(); - let is_shared = is_local && project.is_shared(); - let is_muted = room.is_muted(); - let muted_by_user = room.muted_by_user(); - let is_deafened = room.is_deafened().unwrap_or(false); - let is_screen_sharing = room.is_sharing_screen(); - let can_use_microphone = room.can_use_microphone(); - let can_share_projects = room.can_share_projects(); - let screen_sharing_supported = cx.is_screen_capture_supported(); - - let channel_store = ChannelStore::global(cx); - let channel = room - .channel_id() - .and_then(|channel_id| channel_store.read(cx).channel_for_id(channel_id).cloned()); - - let mut children = Vec::new(); - - children.push( - h_flex() - .gap_1() - .child( - IconButton::new("leave-call", IconName::Exit) - .style(ButtonStyle::Subtle) - .tooltip(Tooltip::text("Leave Call")) - .icon_size(IconSize::Small) - .on_click(move |_, _window, cx| { - ActiveCall::global(cx) - .update(cx, |call, cx| call.hang_up(cx)) - .detach_and_log_err(cx); - }), - ) - .child(Divider::vertical().color(DividerColor::Border)) - .into_any_element(), - ); - - if is_local && can_share_projects && !is_connecting_to_project { - let is_sharing_disabled = channel.is_some_and(|channel| match channel.visibility { - proto::ChannelVisibility::Public => project.visible_worktrees(cx).any(|worktree| { - let worktree_id = worktree.read(cx).id(); - - let settings_location = Some(SettingsLocation { - worktree_id, - path: RelPath::empty(), - }); - - WorktreeSettings::get(settings_location, cx).prevent_sharing_in_public_channels - }), - proto::ChannelVisibility::Members => false, - }); - - children.push( - Button::new( - "toggle_sharing", - if is_shared { "Unshare" } else { "Share" }, - ) - .tooltip(Tooltip::text(if is_shared { - "Stop sharing project with call participants" - } else { - "Share project with call participants" - })) - .style(ButtonStyle::Subtle) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .toggle_state(is_shared) - .label_size(LabelSize::Small) - .when(is_sharing_disabled, |parent| { - parent.disabled(true).tooltip(Tooltip::text( - "This project may not be shared in a public channel.", - )) - }) - .on_click(cx.listener(move |this, _, window, cx| { - if is_shared { - this.unshare_project(window, cx); - } else { - this.share_project(cx); - } - })) - .into_any_element(), - ); - } - - if can_use_microphone { - children.push( - IconButton::new( - "mute-microphone", - if is_muted { - IconName::MicMute - } else { - IconName::Mic - }, - ) - .tooltip(move |_window, cx| { - if is_muted { - if is_deafened { - Tooltip::with_meta( - "Unmute Microphone", - None, - "Audio will be unmuted", - cx, - ) - } else { - Tooltip::simple("Unmute Microphone", cx) - } - } else { - Tooltip::simple("Mute Microphone", cx) - } - }) - .style(ButtonStyle::Subtle) - .icon_size(IconSize::Small) - .toggle_state(is_muted) - .selected_style(ButtonStyle::Tinted(TintColor::Error)) - .on_click(move |_, _window, cx| toggle_mute(cx)) - .into_any_element(), - ); - } - - children.push( - IconButton::new( - "mute-sound", - if is_deafened { - IconName::AudioOff - } else { - IconName::AudioOn - }, - ) - .style(ButtonStyle::Subtle) - .selected_style(ButtonStyle::Tinted(TintColor::Error)) - .icon_size(IconSize::Small) - .toggle_state(is_deafened) - .tooltip(move |_window, cx| { - if is_deafened { - let label = "Unmute Audio"; - - if !muted_by_user { - Tooltip::with_meta(label, None, "Microphone will be unmuted", cx) - } else { - Tooltip::simple(label, cx) - } - } else { - let label = "Mute Audio"; - - if !muted_by_user { - Tooltip::with_meta(label, None, "Microphone will be muted", cx) - } else { - Tooltip::simple(label, cx) - } - } - }) - .on_click(move |_, _, cx| toggle_deafen(cx)) - .into_any_element(), - ); - - if can_use_microphone && screen_sharing_supported { - let trigger = IconButton::new("screen-share", IconName::Screen) - .style(ButtonStyle::Subtle) - .icon_size(IconSize::Small) - .toggle_state(is_screen_sharing) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .tooltip(Tooltip::text(if is_screen_sharing { - "Stop Sharing Screen" - } else { - "Share Screen" - })) - .on_click(move |_, window, cx| { - let should_share = ActiveCall::global(cx) - .read(cx) - .room() - .is_some_and(|room| !room.read(cx).is_sharing_screen()); - - window - .spawn(cx, async move |cx| { - let screen = if should_share { - cx.update(|_, cx| pick_default_screen(cx))?.await - } else { - Ok(None) - }; - cx.update(|window, cx| toggle_screen_sharing(screen, window, cx))?; - - Result::<_, anyhow::Error>::Ok(()) - }) - .detach(); - }); - - children.push( - SplitButton::new( - trigger.render(window, cx), - self.render_screen_list().into_any_element(), - ) - .style(SplitButtonStyle::Transparent) - .into_any_element(), - ); - } - - children.push(div().pr_2().into_any_element()); - - children - } - - fn render_screen_list(&self) -> impl IntoElement { - PopoverMenu::new("screen-share-screen-list") - .with_handle(self.screen_share_popover_handle.clone()) - .trigger( - ui::ButtonLike::new_rounded_right("screen-share-screen-list-trigger") - .child( - h_flex() - .mx_neg_0p5() - .h_full() - .justify_center() - .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)), - ) - .toggle_state(self.screen_share_popover_handle.is_deployed()), - ) - .menu(|window, cx| { - let screens = cx.screen_capture_sources(); - Some(ContextMenu::build(window, cx, |context_menu, _, cx| { - cx.spawn(async move |this: WeakEntity, cx| { - let screens = screens.await??; - this.update(cx, |this, cx| { - let active_screenshare_id = ActiveCall::global(cx) - .read(cx) - .room() - .and_then(|room| room.read(cx).shared_screen_id()); - for screen in screens { - let Ok(meta) = screen.metadata() else { - continue; - }; - - let label = meta - .label - .clone() - .unwrap_or_else(|| SharedString::from("Unknown screen")); - let resolution = SharedString::from(format!( - "{} × {}", - meta.resolution.width.0, meta.resolution.height.0 - )); - this.push_item(ContextMenuItem::CustomEntry { - entry_render: Box::new(move |_, _| { - h_flex() - .gap_2() - .child( - Icon::new(IconName::Screen) - .size(IconSize::XSmall) - .map(|this| { - if active_screenshare_id == Some(meta.id) { - this.color(Color::Accent) - } else { - this.color(Color::Muted) - } - }), - ) - .child(Label::new(label.clone())) - .child( - Label::new(resolution.clone()) - .color(Color::Muted) - .size(LabelSize::Small), - ) - .into_any() - }), - selectable: true, - documentation_aside: None, - handler: Rc::new(move |_, window, cx| { - toggle_screen_sharing(Ok(Some(screen.clone())), window, cx); - }), - }); - } - }) - }) - .detach_and_log_err(cx); - context_menu - })) - }) - } -} - -/// Picks the screen to share when clicking on the main screen sharing button. -fn pick_default_screen(cx: &App) -> Task>>> { - let source = cx.screen_capture_sources(); - cx.spawn(async move |_| { - let available_sources = source.await??; - Ok(available_sources - .iter() - .find(|it| { - it.as_ref() - .metadata() - .is_ok_and(|meta| meta.is_main.unwrap_or_default()) - }) - .or_else(|| available_sources.first()) - .cloned()) - }) -} diff --git a/crates/title_bar/src/onboarding_banner.rs b/crates/title_bar/src/onboarding_banner.rs deleted file mode 100644 index 750ef0a6cd..0000000000 --- a/crates/title_bar/src/onboarding_banner.rs +++ /dev/null @@ -1,170 +0,0 @@ -use gpui::{Action, Entity, Global, Render, SharedString}; -use ui::{ButtonLike, Tooltip, prelude::*}; -use util::ResultExt; - -/// Prompts the user to try newly released Zed's features -pub struct OnboardingBanner { - dismissed: bool, - source: String, - details: BannerDetails, - visible_when: Option bool>>, -} - -#[derive(Clone)] -struct BannerGlobal { - entity: Entity, -} -impl Global for BannerGlobal {} - -pub struct BannerDetails { - pub action: Box, - pub icon_name: IconName, - pub label: SharedString, - pub subtitle: Option, -} - -impl OnboardingBanner { - pub fn new( - source: &str, - icon_name: IconName, - label: impl Into, - subtitle: Option, - action: Box, - cx: &mut Context, - ) -> Self { - cx.set_global(BannerGlobal { - entity: cx.entity(), - }); - Self { - source: source.to_string(), - details: BannerDetails { - action, - icon_name, - label: label.into(), - subtitle: subtitle.or(Some(SharedString::from("Introducing:"))), - }, - visible_when: None, - dismissed: get_dismissed(source), - } - } - - pub fn visible_when(mut self, predicate: impl Fn(&mut App) -> bool + 'static) -> Self { - self.visible_when = Some(Box::new(predicate)); - self - } - - fn should_show(&self, cx: &mut App) -> bool { - !self.dismissed && self.visible_when.as_ref().map_or(true, |f| f(cx)) - } - - fn dismiss(&mut self, cx: &mut Context) { - persist_dismissed(&self.source, cx); - self.dismissed = true; - cx.notify(); - } -} - -fn dismissed_at_key(source: &str) -> String { - if source == "Git Onboarding" { - "zed_git_banner_dismissed_at".to_string() - } else { - format!( - "{}_banner_dismissed_at", - source.to_lowercase().trim().replace(" ", "_") - ) - } -} - -fn get_dismissed(source: &str) -> bool { - let dismissed_at = dismissed_at_key(source); - db::kvp::KEY_VALUE_STORE - .read_kvp(&dismissed_at) - .log_err() - .is_some_and(|dismissed| dismissed.is_some()) -} - -fn persist_dismissed(source: &str, cx: &mut App) { - let dismissed_at = dismissed_at_key(source); - cx.spawn(async |_| { - let time = chrono::Utc::now().to_rfc3339(); - db::kvp::KEY_VALUE_STORE.write_kvp(dismissed_at, time).await - }) - .detach_and_log_err(cx); -} - -pub fn restore_banner(cx: &mut App) { - cx.defer(|cx| { - cx.global::() - .entity - .clone() - .update(cx, |this, cx| { - this.dismissed = false; - cx.notify(); - }); - }); - - let source = &cx.global::().entity.read(cx).source; - let dismissed_at = dismissed_at_key(source); - cx.spawn(async |_| db::kvp::KEY_VALUE_STORE.delete_kvp(dismissed_at).await) - .detach_and_log_err(cx); -} - -impl Render for OnboardingBanner { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - if !self.should_show(cx) { - return div(); - } - - let border_color = cx.theme().colors().editor_foreground.opacity(0.3); - let banner = h_flex() - .rounded_sm() - .border_1() - .border_color(border_color) - .child( - ButtonLike::new("try-a-feature") - .child( - h_flex() - .h_full() - .gap_1() - .child(Icon::new(self.details.icon_name).size(IconSize::XSmall)) - .child( - h_flex() - .gap_0p5() - .when_some(self.details.subtitle.as_ref(), |this, subtitle| { - this.child( - Label::new(subtitle) - .size(LabelSize::Small) - .color(Color::Muted), - ) - }) - .child(Label::new(&self.details.label).size(LabelSize::Small)), - ), - ) - .on_click(cx.listener(|this, _, window, cx| { - telemetry::event!("Banner Clicked", source = this.source); - this.dismiss(cx); - window.dispatch_action(this.details.action.boxed_clone(), cx) - })), - ) - .child( - div().border_l_1().border_color(border_color).child( - IconButton::new("close", IconName::Close) - .icon_size(IconSize::Indicator) - .on_click(cx.listener(|this, _, _window, cx| { - telemetry::event!("Banner Dismissed", source = this.source); - this.dismiss(cx) - })) - .tooltip(|_window, cx| { - Tooltip::with_meta( - "Close Announcement Banner", - None, - "It won't show again for this feature", - cx, - ) - }), - ), - ); - - div().pr_2().child(banner) - } -} diff --git a/crates/title_bar/src/platform_title_bar.rs b/crates/title_bar/src/platform_title_bar.rs deleted file mode 100644 index 6ce7d089bb..0000000000 --- a/crates/title_bar/src/platform_title_bar.rs +++ /dev/null @@ -1,192 +0,0 @@ -use gpui::{ - AnyElement, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement, MouseButton, - ParentElement, Pixels, StatefulInteractiveElement, Styled, Window, WindowControlArea, div, px, -}; -use smallvec::SmallVec; -use std::mem; -use ui::prelude::*; - -use crate::{ - platforms::{platform_linux, platform_mac, platform_windows}, - system_window_tabs::SystemWindowTabs, -}; - -pub struct PlatformTitleBar { - id: ElementId, - platform_style: PlatformStyle, - children: SmallVec<[AnyElement; 2]>, - should_move: bool, - system_window_tabs: Entity, -} - -impl PlatformTitleBar { - pub fn new(id: impl Into, cx: &mut Context) -> Self { - let platform_style = PlatformStyle::platform(); - let system_window_tabs = cx.new(|_cx| SystemWindowTabs::new()); - - Self { - id: id.into(), - platform_style, - children: SmallVec::new(), - should_move: false, - system_window_tabs, - } - } - - #[cfg(not(target_os = "windows"))] - pub fn height(window: &mut Window) -> Pixels { - (1.75 * window.rem_size()).max(px(34.)) - } - - #[cfg(target_os = "windows")] - pub fn height(_window: &mut Window) -> Pixels { - // todo(windows) instead of hard coded size report the actual size to the Windows platform API - px(32.) - } - - pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context) -> Hsla { - if cfg!(any(target_os = "linux", target_os = "freebsd")) { - if window.is_window_active() && !self.should_move { - cx.theme().colors().title_bar_background - } else { - cx.theme().colors().title_bar_inactive_background - } - } else { - cx.theme().colors().title_bar_background - } - } - - pub fn set_children(&mut self, children: T) - where - T: IntoIterator, - { - self.children = children.into_iter().collect(); - } -} - -impl Render for PlatformTitleBar { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let supported_controls = window.window_controls(); - let decorations = window.window_decorations(); - let height = Self::height(window); - let titlebar_color = self.title_bar_color(window, cx); - let close_action = Box::new(workspace::CloseWindow); - let children = mem::take(&mut self.children); - - let title_bar = h_flex() - .window_control_area(WindowControlArea::Drag) - .w_full() - .h(height) - .map(|this| { - this.on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| { - this.should_move = false; - })) - .on_mouse_up( - gpui::MouseButton::Left, - cx.listener(move |this, _ev, _window, _cx| { - this.should_move = false; - }), - ) - .on_mouse_down( - gpui::MouseButton::Left, - cx.listener(move |this, _ev, _window, _cx| { - this.should_move = true; - }), - ) - .on_mouse_move(cx.listener(move |this, _ev, window, _| { - if this.should_move { - this.should_move = false; - window.start_window_move(); - } - })) - }) - .map(|this| { - // Note: On Windows the title bar behavior is handled by the platform implementation. - this.id(self.id.clone()) - .when(self.platform_style == PlatformStyle::Mac, |this| { - this.on_click(|event, window, _| { - if event.click_count() == 2 { - window.titlebar_double_click(); - } - }) - }) - .when(self.platform_style == PlatformStyle::Linux, |this| { - this.on_click(|event, window, _| { - if event.click_count() == 2 { - window.zoom_window(); - } - }) - }) - }) - .map(|this| { - if window.is_fullscreen() { - this.pl_2() - } else if self.platform_style == PlatformStyle::Mac { - this.pl(px(platform_mac::TRAFFIC_LIGHT_PADDING)) - } else { - this.pl_2() - } - }) - .map(|el| match decorations { - Decorations::Server => el, - Decorations::Client { tiling, .. } => el - .when(!(tiling.top || tiling.right), |el| { - el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.top || tiling.left), |el| { - el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - // this border is to avoid a transparent gap in the rounded corners - .mt(px(-1.)) - .mb(px(-1.)) - .border(px(1.)) - .border_color(titlebar_color), - }) - .bg(titlebar_color) - .content_stretch() - .child( - div() - .id(self.id.clone()) - .flex() - .flex_row() - .items_center() - .justify_between() - .overflow_x_hidden() - .w_full() - .children(children), - ) - .when(!window.is_fullscreen(), |title_bar| { - match self.platform_style { - PlatformStyle::Mac => title_bar, - PlatformStyle::Linux => { - if matches!(decorations, Decorations::Client { .. }) { - title_bar - .child(platform_linux::LinuxWindowControls::new(close_action)) - .when(supported_controls.window_menu, |titlebar| { - titlebar - .on_mouse_down(MouseButton::Right, move |ev, window, _| { - window.show_window_menu(ev.position) - }) - }) - } else { - title_bar - } - } - PlatformStyle::Windows => { - title_bar.child(platform_windows::WindowsWindowControls::new(height)) - } - } - }); - - v_flex() - .w_full() - .child(title_bar) - .child(self.system_window_tabs.clone().into_any_element()) - } -} - -impl ParentElement for PlatformTitleBar { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} diff --git a/crates/title_bar/src/platforms.rs b/crates/title_bar/src/platforms.rs deleted file mode 100644 index 67e87d45ea..0000000000 --- a/crates/title_bar/src/platforms.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod platform_linux; -pub mod platform_mac; -pub mod platform_windows; diff --git a/crates/title_bar/src/platforms/platform_linux.rs b/crates/title_bar/src/platforms/platform_linux.rs deleted file mode 100644 index 0e7af80f80..0000000000 --- a/crates/title_bar/src/platforms/platform_linux.rs +++ /dev/null @@ -1,208 +0,0 @@ -use gpui::{Action, Hsla, MouseButton, prelude::*, svg}; -use ui::prelude::*; - -#[derive(IntoElement)] -pub struct LinuxWindowControls { - close_window_action: Box, -} - -impl LinuxWindowControls { - pub fn new(close_window_action: Box) -> Self { - Self { - close_window_action, - } - } -} - -impl RenderOnce for LinuxWindowControls { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - h_flex() - .id("generic-window-controls") - .px_3() - .gap_3() - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .child(WindowControl::new( - "minimize", - WindowControlType::Minimize, - cx, - )) - .child(WindowControl::new( - "maximize-or-restore", - if window.is_maximized() { - WindowControlType::Restore - } else { - WindowControlType::Maximize - }, - cx, - )) - .child(WindowControl::new_close( - "close", - WindowControlType::Close, - self.close_window_action, - cx, - )) - } -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum WindowControlType { - Minimize, - Restore, - Maximize, - Close, -} - -impl WindowControlType { - /// Returns the icon name for the window control type. - /// - /// Will take a [PlatformStyle] in the future to return a different - /// icon name based on the platform. - pub fn icon(&self) -> IconName { - match self { - WindowControlType::Minimize => IconName::GenericMinimize, - WindowControlType::Restore => IconName::GenericRestore, - WindowControlType::Maximize => IconName::GenericMaximize, - WindowControlType::Close => IconName::GenericClose, - } - } -} - -#[allow(unused)] -pub struct WindowControlStyle { - background: Hsla, - background_hover: Hsla, - icon: Hsla, - icon_hover: Hsla, -} - -impl WindowControlStyle { - pub fn default(cx: &mut App) -> Self { - let colors = cx.theme().colors(); - - Self { - background: colors.ghost_element_background, - background_hover: colors.ghost_element_hover, - icon: colors.icon, - icon_hover: colors.icon_muted, - } - } - - #[allow(unused)] - /// Sets the background color of the control. - pub fn background(mut self, color: impl Into) -> Self { - self.background = color.into(); - self - } - - #[allow(unused)] - /// Sets the background color of the control when hovered. - pub fn background_hover(mut self, color: impl Into) -> Self { - self.background_hover = color.into(); - self - } - - #[allow(unused)] - /// Sets the color of the icon. - pub fn icon(mut self, color: impl Into) -> Self { - self.icon = color.into(); - self - } - - #[allow(unused)] - /// Sets the color of the icon when hovered. - pub fn icon_hover(mut self, color: impl Into) -> Self { - self.icon_hover = color.into(); - self - } -} - -#[derive(IntoElement)] -pub struct WindowControl { - id: ElementId, - icon: WindowControlType, - style: WindowControlStyle, - close_action: Option>, -} - -impl WindowControl { - pub fn new(id: impl Into, icon: WindowControlType, cx: &mut App) -> Self { - let style = WindowControlStyle::default(cx); - - Self { - id: id.into(), - icon, - style, - close_action: None, - } - } - - pub fn new_close( - id: impl Into, - icon: WindowControlType, - close_action: Box, - cx: &mut App, - ) -> Self { - let style = WindowControlStyle::default(cx); - - Self { - id: id.into(), - icon, - style, - close_action: Some(close_action.boxed_clone()), - } - } - - #[allow(unused)] - pub fn custom_style( - id: impl Into, - icon: WindowControlType, - style: WindowControlStyle, - ) -> Self { - Self { - id: id.into(), - icon, - style, - close_action: None, - } - } -} - -impl RenderOnce for WindowControl { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let icon = svg() - .size_4() - .flex_none() - .path(self.icon.icon().path()) - .text_color(self.style.icon) - .group_hover("", |this| this.text_color(self.style.icon_hover)); - - h_flex() - .id(self.id) - .group("") - .cursor_pointer() - .justify_center() - .content_center() - .rounded_2xl() - .w_5() - .h_5() - .hover(|this| this.bg(self.style.background_hover)) - .active(|this| this.bg(self.style.background_hover)) - .child(icon) - .on_mouse_move(|_, _, cx| cx.stop_propagation()) - .on_click(move |_, window, cx| { - cx.stop_propagation(); - match self.icon { - WindowControlType::Minimize => window.minimize_window(), - WindowControlType::Restore => window.zoom_window(), - WindowControlType::Maximize => window.zoom_window(), - WindowControlType::Close => window.dispatch_action( - self.close_action - .as_ref() - .expect("Use WindowControl::new_close() for close control.") - .boxed_clone(), - cx, - ), - } - }) - } -} diff --git a/crates/title_bar/src/platforms/platform_mac.rs b/crates/title_bar/src/platforms/platform_mac.rs deleted file mode 100644 index c7becde6c1..0000000000 --- a/crates/title_bar/src/platforms/platform_mac.rs +++ /dev/null @@ -1,6 +0,0 @@ -/// Use pixels here instead of a rem-based size because the macOS traffic -/// lights are a static size, and don't scale with the rest of the UI. -/// -/// Magic number: There is one extra pixel of padding on the left side due to -/// the 1px border around the window on macOS apps. -pub const TRAFFIC_LIGHT_PADDING: f32 = 71.; diff --git a/crates/title_bar/src/platforms/platform_windows.rs b/crates/title_bar/src/platforms/platform_windows.rs deleted file mode 100644 index 1df75ee8a9..0000000000 --- a/crates/title_bar/src/platforms/platform_windows.rs +++ /dev/null @@ -1,143 +0,0 @@ -use gpui::{Hsla, Rgba, WindowControlArea, prelude::*}; - -use ui::prelude::*; - -#[derive(IntoElement)] -pub struct WindowsWindowControls { - button_height: Pixels, -} - -impl WindowsWindowControls { - pub fn new(button_height: Pixels) -> Self { - Self { button_height } - } - - #[cfg(not(target_os = "windows"))] - fn get_font() -> &'static str { - "Segoe Fluent Icons" - } - - #[cfg(target_os = "windows")] - fn get_font() -> &'static str { - use windows::Wdk::System::SystemServices::RtlGetVersion; - - let mut version = unsafe { std::mem::zeroed() }; - let status = unsafe { RtlGetVersion(&mut version) }; - - if status.is_ok() && version.dwBuildNumber >= 22000 { - "Segoe Fluent Icons" - } else { - "Segoe MDL2 Assets" - } - } -} - -impl RenderOnce for WindowsWindowControls { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let close_button_hover_color = Rgba { - r: 232.0 / 255.0, - g: 17.0 / 255.0, - b: 32.0 / 255.0, - a: 1.0, - }; - - let button_hover_color = cx.theme().colors().ghost_element_hover; - let button_active_color = cx.theme().colors().ghost_element_active; - - div() - .id("windows-window-controls") - .font_family(Self::get_font()) - .flex() - .flex_row() - .justify_center() - .content_stretch() - .max_h(self.button_height) - .min_h(self.button_height) - .child(WindowsCaptionButton::new( - "minimize", - WindowsCaptionButtonIcon::Minimize, - button_hover_color, - button_active_color, - )) - .child(WindowsCaptionButton::new( - "maximize-or-restore", - if window.is_maximized() { - WindowsCaptionButtonIcon::Restore - } else { - WindowsCaptionButtonIcon::Maximize - }, - button_hover_color, - button_active_color, - )) - .child(WindowsCaptionButton::new( - "close", - WindowsCaptionButtonIcon::Close, - close_button_hover_color, - button_active_color, - )) - } -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -enum WindowsCaptionButtonIcon { - Minimize, - Restore, - Maximize, - Close, -} - -#[derive(IntoElement)] -struct WindowsCaptionButton { - id: ElementId, - icon: WindowsCaptionButtonIcon, - hover_background_color: Hsla, - active_background_color: Hsla, -} - -impl WindowsCaptionButton { - pub fn new( - id: impl Into, - icon: WindowsCaptionButtonIcon, - hover_background_color: impl Into, - active_background_color: impl Into, - ) -> Self { - Self { - id: id.into(), - icon, - hover_background_color: hover_background_color.into(), - active_background_color: active_background_color.into(), - } - } -} - -impl RenderOnce for WindowsCaptionButton { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - h_flex() - .id(self.id) - .justify_center() - .content_center() - .occlude() - .w(px(36.)) - .h_full() - .text_size(px(10.0)) - .hover(|style| style.bg(self.hover_background_color)) - .active(|style| style.bg(self.active_background_color)) - .map(|this| match self.icon { - WindowsCaptionButtonIcon::Close => { - this.window_control_area(WindowControlArea::Close) - } - WindowsCaptionButtonIcon::Maximize | WindowsCaptionButtonIcon::Restore => { - this.window_control_area(WindowControlArea::Max) - } - WindowsCaptionButtonIcon::Minimize => { - this.window_control_area(WindowControlArea::Min) - } - }) - .child(match self.icon { - WindowsCaptionButtonIcon::Minimize => "\u{e921}", - WindowsCaptionButtonIcon::Restore => "\u{e923}", - WindowsCaptionButtonIcon::Maximize => "\u{e922}", - WindowsCaptionButtonIcon::Close => "\u{e8bb}", - }) - } -} diff --git a/crates/title_bar/src/stories.rs b/crates/title_bar/src/stories.rs deleted file mode 100644 index 21ed2268db..0000000000 --- a/crates/title_bar/src/stories.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod application_menu; - -pub use application_menu::*; diff --git a/crates/title_bar/src/stories/application_menu.rs b/crates/title_bar/src/stories/application_menu.rs deleted file mode 100644 index f47f2a6c76..0000000000 --- a/crates/title_bar/src/stories/application_menu.rs +++ /dev/null @@ -1,29 +0,0 @@ -use gpui::{Entity, Render}; -use story::{Story, StoryItem, StorySection}; - -use ui::prelude::*; - -use crate::application_menu::ApplicationMenu; - -pub struct ApplicationMenuStory { - menu: Entity, -} - -impl ApplicationMenuStory { - pub fn new(window: &mut Window, cx: &mut App) -> Self { - Self { - menu: cx.new(|cx| ApplicationMenu::new(window, cx)), - } - } -} - -impl Render for ApplicationMenuStory { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - Story::container(cx) - .child(Story::title_for::(cx)) - .child(StorySection::new().child(StoryItem::new( - "Application Menu", - h_flex().child(self.menu.clone()), - ))) - } -} diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs deleted file mode 100644 index a9bf46cc4f..0000000000 --- a/crates/title_bar/src/system_window_tabs.rs +++ /dev/null @@ -1,529 +0,0 @@ -use settings::{Settings, SettingsStore}; - -use gpui::{ - AnyWindowHandle, Context, Hsla, InteractiveElement, MouseButton, ParentElement, ScrollHandle, - Styled, SystemWindowTab, SystemWindowTabController, Window, WindowId, actions, canvas, div, -}; - -use theme::ThemeSettings; -use ui::{ - Color, ContextMenu, DynamicSpacing, IconButton, IconButtonShape, IconName, IconSize, Label, - LabelSize, Tab, h_flex, prelude::*, right_click_menu, -}; -use workspace::{ - CloseWindow, ItemSettings, Workspace, WorkspaceSettings, - item::{ClosePosition, ShowCloseButton}, -}; - -actions!( - window, - [ - ShowNextWindowTab, - ShowPreviousWindowTab, - MergeAllWindows, - MoveTabToNewWindow - ] -); - -#[derive(Clone)] -pub struct DraggedWindowTab { - pub id: WindowId, - pub ix: usize, - pub handle: AnyWindowHandle, - pub title: String, - pub width: Pixels, - pub is_active: bool, - pub active_background_color: Hsla, - pub inactive_background_color: Hsla, -} - -pub struct SystemWindowTabs { - tab_bar_scroll_handle: ScrollHandle, - measured_tab_width: Pixels, - last_dragged_tab: Option, -} - -impl SystemWindowTabs { - pub fn new() -> Self { - Self { - tab_bar_scroll_handle: ScrollHandle::new(), - measured_tab_width: px(0.), - last_dragged_tab: None, - } - } - - pub fn init(cx: &mut App) { - let mut was_use_system_window_tabs = - WorkspaceSettings::get_global(cx).use_system_window_tabs; - - cx.observe_global::(move |cx| { - let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs; - if use_system_window_tabs == was_use_system_window_tabs { - return; - } - was_use_system_window_tabs = use_system_window_tabs; - - let tabbing_identifier = if use_system_window_tabs { - Some(String::from("zed")) - } else { - None - }; - - if use_system_window_tabs { - SystemWindowTabController::init(cx); - } - - cx.windows().iter().for_each(|handle| { - let _ = handle.update(cx, |_, window, cx| { - window.set_tabbing_identifier(tabbing_identifier.clone()); - if use_system_window_tabs { - let tabs = if let Some(tabs) = window.tabbed_windows() { - tabs - } else { - vec![SystemWindowTab::new( - SharedString::from(window.window_title()), - window.window_handle(), - )] - }; - - SystemWindowTabController::add_tab(cx, handle.window_id(), tabs); - } - }); - }); - }) - .detach(); - - cx.observe_new(|workspace: &mut Workspace, _, _| { - workspace.register_action_renderer(|div, _, window, cx| { - let window_id = window.window_handle().window_id(); - let controller = cx.global::(); - - let tab_groups = controller.tab_groups(); - let tabs = controller.tabs(window_id); - let Some(tabs) = tabs else { - return div; - }; - - div.when(tabs.len() > 1, |div| { - div.on_action(move |_: &ShowNextWindowTab, window, cx| { - SystemWindowTabController::select_next_tab( - cx, - window.window_handle().window_id(), - ); - }) - .on_action(move |_: &ShowPreviousWindowTab, window, cx| { - SystemWindowTabController::select_previous_tab( - cx, - window.window_handle().window_id(), - ); - }) - .on_action(move |_: &MoveTabToNewWindow, window, cx| { - SystemWindowTabController::move_tab_to_new_window( - cx, - window.window_handle().window_id(), - ); - window.move_tab_to_new_window(); - }) - }) - .when(tab_groups.len() > 1, |div| { - div.on_action(move |_: &MergeAllWindows, window, cx| { - SystemWindowTabController::merge_all_windows( - cx, - window.window_handle().window_id(), - ); - window.merge_all_windows(); - }) - }) - }); - }) - .detach(); - } - - fn render_tab( - &self, - ix: usize, - item: SystemWindowTab, - tabs: Vec, - active_background_color: Hsla, - inactive_background_color: Hsla, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement + use<> { - let entity = cx.entity(); - let settings = ItemSettings::get_global(cx); - let close_side = &settings.close_position; - let show_close_button = &settings.show_close_button; - - let rem_size = window.rem_size(); - let width = self.measured_tab_width.max(rem_size * 10); - let is_active = window.window_handle().window_id() == item.id; - let title = item.title.to_string(); - - let label = Label::new(&title) - .size(LabelSize::Small) - .truncate() - .color(if is_active { - Color::Default - } else { - Color::Muted - }); - - let tab = h_flex() - .id(ix) - .group("tab") - .w_full() - .overflow_hidden() - .h(Tab::content_height(cx)) - .relative() - .px(DynamicSpacing::Base16.px(cx)) - .justify_center() - .border_l_1() - .border_color(cx.theme().colors().border) - .cursor_pointer() - .on_drag( - DraggedWindowTab { - id: item.id, - ix, - handle: item.handle, - title: item.title.to_string(), - width, - is_active, - active_background_color, - inactive_background_color, - }, - move |tab, _, _, cx| { - entity.update(cx, |this, _cx| { - this.last_dragged_tab = Some(tab.clone()); - }); - cx.new(|_| tab.clone()) - }, - ) - .drag_over::({ - let tab_ix = ix; - move |element, dragged_tab: &DraggedWindowTab, _, cx| { - let mut styled_tab = element - .bg(cx.theme().colors().drop_target_background) - .border_color(cx.theme().colors().drop_target_border) - .border_0(); - - if tab_ix < dragged_tab.ix { - styled_tab = styled_tab.border_l_2(); - } else if tab_ix > dragged_tab.ix { - styled_tab = styled_tab.border_r_2(); - } - - styled_tab - } - }) - .on_drop({ - let tab_ix = ix; - cx.listener(move |this, dragged_tab: &DraggedWindowTab, _window, cx| { - this.last_dragged_tab = None; - Self::handle_tab_drop(dragged_tab, tab_ix, cx); - }) - }) - .on_click(move |_, _, cx| { - let _ = item.handle.update(cx, |_, window, _| { - window.activate_window(); - }); - }) - .on_mouse_up(MouseButton::Middle, move |_, window, cx| { - if item.handle.window_id() == window.window_handle().window_id() { - window.dispatch_action(Box::new(CloseWindow), cx); - } else { - let _ = item.handle.update(cx, |_, window, cx| { - window.dispatch_action(Box::new(CloseWindow), cx); - }); - } - }) - .child(label) - .map(|this| match show_close_button { - ShowCloseButton::Hidden => this, - _ => this.child( - div() - .absolute() - .top_2() - .w_4() - .h_4() - .map(|this| match close_side { - ClosePosition::Left => this.left_1(), - ClosePosition::Right => this.right_1(), - }) - .child( - IconButton::new("close", IconName::Close) - .shape(IconButtonShape::Square) - .icon_color(Color::Muted) - .icon_size(IconSize::XSmall) - .on_click({ - move |_, window, cx| { - if item.handle.window_id() - == window.window_handle().window_id() - { - window.dispatch_action(Box::new(CloseWindow), cx); - } else { - let _ = item.handle.update(cx, |_, window, cx| { - window.dispatch_action(Box::new(CloseWindow), cx); - }); - } - } - }) - .map(|this| match show_close_button { - ShowCloseButton::Hover => this.visible_on_hover("tab"), - _ => this, - }), - ), - ), - }) - .into_any(); - - let menu = right_click_menu(ix) - .trigger(|_, _, _| tab) - .menu(move |window, cx| { - let focus_handle = cx.focus_handle(); - let tabs = tabs.clone(); - let other_tabs = tabs.clone(); - let move_tabs = tabs.clone(); - let merge_tabs = tabs.clone(); - - ContextMenu::build(window, cx, move |mut menu, _window_, _cx| { - menu = menu.entry("Close Tab", None, move |window, cx| { - Self::handle_right_click_action( - cx, - window, - &tabs, - |tab| tab.id == item.id, - |window, cx| { - window.dispatch_action(Box::new(CloseWindow), cx); - }, - ); - }); - - menu = menu.entry("Close Other Tabs", None, move |window, cx| { - Self::handle_right_click_action( - cx, - window, - &other_tabs, - |tab| tab.id != item.id, - |window, cx| { - window.dispatch_action(Box::new(CloseWindow), cx); - }, - ); - }); - - menu = menu.entry("Move Tab to New Window", None, move |window, cx| { - Self::handle_right_click_action( - cx, - window, - &move_tabs, - |tab| tab.id == item.id, - |window, cx| { - SystemWindowTabController::move_tab_to_new_window( - cx, - window.window_handle().window_id(), - ); - window.move_tab_to_new_window(); - }, - ); - }); - - menu = menu.entry("Show All Tabs", None, move |window, cx| { - Self::handle_right_click_action( - cx, - window, - &merge_tabs, - |tab| tab.id == item.id, - |window, _cx| { - window.toggle_window_tab_overview(); - }, - ); - }); - - menu.context(focus_handle) - }) - }); - - div() - .flex_1() - .min_w(rem_size * 10) - .when(is_active, |this| this.bg(active_background_color)) - .border_t_1() - .border_color(if is_active { - active_background_color - } else { - cx.theme().colors().border - }) - .child(menu) - } - - fn handle_tab_drop(dragged_tab: &DraggedWindowTab, ix: usize, cx: &mut Context) { - SystemWindowTabController::update_tab_position(cx, dragged_tab.id, ix); - } - - fn handle_right_click_action( - cx: &mut App, - window: &mut Window, - tabs: &Vec, - predicate: P, - mut action: F, - ) where - P: Fn(&SystemWindowTab) -> bool, - F: FnMut(&mut Window, &mut App), - { - for tab in tabs { - if predicate(tab) { - if tab.id == window.window_handle().window_id() { - action(window, cx); - } else { - let _ = tab.handle.update(cx, |_view, window, cx| { - action(window, cx); - }); - } - } - } - } -} - -impl Render for SystemWindowTabs { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs; - let active_background_color = cx.theme().colors().title_bar_background; - let inactive_background_color = cx.theme().colors().tab_bar_background; - let entity = cx.entity(); - - let controller = cx.global::(); - let visible = controller.is_visible(); - let current_window_tab = vec![SystemWindowTab::new( - SharedString::from(window.window_title()), - window.window_handle(), - )]; - let tabs = controller - .tabs(window.window_handle().window_id()) - .unwrap_or(¤t_window_tab) - .clone(); - - let tab_items = tabs - .iter() - .enumerate() - .map(|(ix, item)| { - self.render_tab( - ix, - item.clone(), - tabs.clone(), - active_background_color, - inactive_background_color, - window, - cx, - ) - }) - .collect::>(); - - let number_of_tabs = tab_items.len().max(1); - if (!window.tab_bar_visible() && !visible) - || (!use_system_window_tabs && number_of_tabs == 1) - { - return h_flex().into_any_element(); - } - - h_flex() - .w_full() - .h(Tab::container_height(cx)) - .bg(inactive_background_color) - .on_mouse_up_out( - MouseButton::Left, - cx.listener(|this, _event, window, cx| { - if let Some(tab) = this.last_dragged_tab.take() { - SystemWindowTabController::move_tab_to_new_window(cx, tab.id); - if tab.id == window.window_handle().window_id() { - window.move_tab_to_new_window(); - } else { - let _ = tab.handle.update(cx, |_, window, _cx| { - window.move_tab_to_new_window(); - }); - } - } - }), - ) - .child( - h_flex() - .id("window tabs") - .w_full() - .h(Tab::container_height(cx)) - .bg(inactive_background_color) - .overflow_x_scroll() - .track_scroll(&self.tab_bar_scroll_handle) - .children(tab_items) - .child( - canvas( - |_, _, _| (), - move |bounds, _, _, cx| { - let entity = entity.clone(); - entity.update(cx, |this, cx| { - let width = bounds.size.width / number_of_tabs as f32; - if width != this.measured_tab_width { - this.measured_tab_width = width; - cx.notify(); - } - }); - }, - ) - .absolute() - .size_full(), - ), - ) - .child( - h_flex() - .h_full() - .px(DynamicSpacing::Base06.rems(cx)) - .border_t_1() - .border_l_1() - .border_color(cx.theme().colors().border) - .child( - IconButton::new("plus", IconName::Plus) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .on_click(|_event, window, cx| { - window.dispatch_action( - Box::new(zed_actions::OpenRecent { - create_new_window: true, - }), - cx, - ); - }), - ), - ) - .into_any_element() - } -} - -impl Render for DraggedWindowTab { - fn render( - &mut self, - _window: &mut gpui::Window, - cx: &mut gpui::Context, - ) -> impl gpui::IntoElement { - let ui_font = ThemeSettings::get_global(cx).ui_font.clone(); - let label = Label::new(self.title.clone()) - .size(LabelSize::Small) - .truncate() - .color(if self.is_active { - Color::Default - } else { - Color::Muted - }); - - h_flex() - .h(Tab::container_height(cx)) - .w(self.width) - .px(DynamicSpacing::Base16.px(cx)) - .justify_center() - .bg(if self.is_active { - self.active_background_color - } else { - self.inactive_background_color - }) - .border_1() - .border_color(cx.theme().colors().border) - .font(ui_font) - .child(label) - } -} diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs deleted file mode 100644 index 680c455e73..0000000000 --- a/crates/title_bar/src/title_bar.rs +++ /dev/null @@ -1,777 +0,0 @@ -mod application_menu; -pub mod collab; -mod onboarding_banner; -pub mod platform_title_bar; -mod platforms; -mod system_window_tabs; -mod title_bar_settings; - -#[cfg(feature = "stories")] -mod stories; - -use crate::{ - application_menu::{ApplicationMenu, show_menus}, - platform_title_bar::PlatformTitleBar, - system_window_tabs::SystemWindowTabs, -}; - -#[cfg(not(target_os = "macos"))] -use crate::application_menu::{ - ActivateDirection, ActivateMenuLeft, ActivateMenuRight, OpenApplicationMenu, -}; - -use auto_update::AutoUpdateStatus; -use call::ActiveCall; -use client::{Client, UserStore, zed_urls}; -use cloud_llm_client::{Plan, PlanV1, PlanV2}; -use gpui::{ - Action, AnyElement, App, Context, Corner, Element, Entity, Focusable, InteractiveElement, - IntoElement, MouseButton, ParentElement, Render, StatefulInteractiveElement, Styled, - Subscription, WeakEntity, Window, actions, div, -}; -use onboarding_banner::OnboardingBanner; -use project::{Project, WorktreeSettings, git_store::GitStoreEvent}; -use remote::RemoteConnectionOptions; -use settings::{Settings, SettingsLocation}; -use std::sync::Arc; -use theme::ActiveTheme; -use title_bar_settings::TitleBarSettings; -use ui::{ - Avatar, Button, ButtonLike, ButtonStyle, Chip, ContextMenu, Icon, IconName, IconSize, - IconWithIndicator, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, h_flex, prelude::*, -}; -use util::{ResultExt, rel_path::RelPath}; -use workspace::{Workspace, notifications::NotifyResultExt}; -use zed_actions::{OpenRecent, OpenRemote}; - -pub use onboarding_banner::restore_banner; - -#[cfg(feature = "stories")] -pub use stories::*; - -const MAX_PROJECT_NAME_LENGTH: usize = 40; -const MAX_BRANCH_NAME_LENGTH: usize = 40; -const MAX_SHORT_SHA_LENGTH: usize = 8; - -actions!( - collab, - [ - /// Toggles the user menu dropdown. - ToggleUserMenu, - /// Toggles the project menu dropdown. - ToggleProjectMenu, - /// Switches to a different git branch. - SwitchBranch - ] -); - -pub fn init(cx: &mut App) { - SystemWindowTabs::init(cx); - - cx.observe_new(|workspace: &mut Workspace, window, cx| { - let Some(window) = window else { - return; - }; - let item = cx.new(|cx| TitleBar::new("title-bar", workspace, window, cx)); - workspace.set_titlebar_item(item.into(), window, cx); - - #[cfg(not(target_os = "macos"))] - workspace.register_action(|workspace, action: &OpenApplicationMenu, window, cx| { - if let Some(titlebar) = workspace - .titlebar_item() - .and_then(|item| item.downcast::().ok()) - { - titlebar.update(cx, |titlebar, cx| { - if let Some(ref menu) = titlebar.application_menu { - menu.update(cx, |menu, cx| menu.open_menu(action, window, cx)); - } - }); - } - }); - - #[cfg(not(target_os = "macos"))] - workspace.register_action(|workspace, _: &ActivateMenuRight, window, cx| { - if let Some(titlebar) = workspace - .titlebar_item() - .and_then(|item| item.downcast::().ok()) - { - titlebar.update(cx, |titlebar, cx| { - if let Some(ref menu) = titlebar.application_menu { - menu.update(cx, |menu, cx| { - menu.navigate_menus_in_direction(ActivateDirection::Right, window, cx) - }); - } - }); - } - }); - - #[cfg(not(target_os = "macos"))] - workspace.register_action(|workspace, _: &ActivateMenuLeft, window, cx| { - if let Some(titlebar) = workspace - .titlebar_item() - .and_then(|item| item.downcast::().ok()) - { - titlebar.update(cx, |titlebar, cx| { - if let Some(ref menu) = titlebar.application_menu { - menu.update(cx, |menu, cx| { - menu.navigate_menus_in_direction(ActivateDirection::Left, window, cx) - }); - } - }); - } - }); - }) - .detach(); -} - -pub struct TitleBar { - platform_titlebar: Entity, - project: Entity, - user_store: Entity, - client: Arc, - workspace: WeakEntity, - application_menu: Option>, - _subscriptions: Vec, - banner: Entity, - screen_share_popover_handle: PopoverMenuHandle, -} - -impl Render for TitleBar { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let title_bar_settings = *TitleBarSettings::get_global(cx); - - let show_menus = show_menus(cx); - - let mut children = Vec::new(); - - children.push( - h_flex() - .gap_1() - .map(|title_bar| { - let mut render_project_items = title_bar_settings.show_branch_name - || title_bar_settings.show_project_items; - title_bar - .when_some( - self.application_menu.clone().filter(|_| !show_menus), - |title_bar, menu| { - render_project_items &= - !menu.update(cx, |menu, cx| menu.all_menus_shown(cx)); - title_bar.child(menu) - }, - ) - .when(render_project_items, |title_bar| { - title_bar - .when(title_bar_settings.show_project_items, |title_bar| { - title_bar - .children(self.render_project_host(cx)) - .child(self.render_project_name(cx)) - }) - .when(title_bar_settings.show_branch_name, |title_bar| { - title_bar.children(self.render_project_branch(cx)) - }) - }) - }) - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .into_any_element(), - ); - - children.push(self.render_collaborator_list(window, cx).into_any_element()); - - if title_bar_settings.show_onboarding_banner { - children.push(self.banner.clone().into_any_element()) - } - - let status = self.client.status(); - let status = &*status.borrow(); - let user = self.user_store.read(cx).current_user(); - - let signed_in = user.is_some(); - - children.push( - h_flex() - .map(|this| { - if signed_in { - this.pr_1p5() - } else { - this.pr_1() - } - }) - .gap_1() - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .children(self.render_call_controls(window, cx)) - .children(self.render_connection_status(status, cx)) - .when( - user.is_none() && TitleBarSettings::get_global(cx).show_sign_in, - |el| el.child(self.render_sign_in_button(cx)), - ) - .child(self.render_app_menu_button(cx)) - .into_any_element(), - ); - - if show_menus { - self.platform_titlebar.update(cx, |this, _| { - this.set_children( - self.application_menu - .clone() - .map(|menu| menu.into_any_element()), - ); - }); - - let height = PlatformTitleBar::height(window); - let title_bar_color = self.platform_titlebar.update(cx, |platform_titlebar, cx| { - platform_titlebar.title_bar_color(window, cx) - }); - - v_flex() - .w_full() - .child(self.platform_titlebar.clone().into_any_element()) - .child( - h_flex() - .bg(title_bar_color) - .h(height) - .pl_2() - .justify_between() - .w_full() - .children(children), - ) - .into_any_element() - } else { - self.platform_titlebar.update(cx, |this, _| { - this.set_children(children); - }); - self.platform_titlebar.clone().into_any_element() - } - } -} - -impl TitleBar { - pub fn new( - id: impl Into, - workspace: &Workspace, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let project = workspace.project().clone(); - let git_store = project.read(cx).git_store().clone(); - let user_store = workspace.app_state().user_store.clone(); - let client = workspace.app_state().client.clone(); - let active_call = ActiveCall::global(cx); - - let platform_style = PlatformStyle::platform(); - let application_menu = match platform_style { - PlatformStyle::Mac => { - if option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some() { - Some(cx.new(|cx| ApplicationMenu::new(window, cx))) - } else { - None - } - } - PlatformStyle::Linux | PlatformStyle::Windows => { - Some(cx.new(|cx| ApplicationMenu::new(window, cx))) - } - }; - - let mut subscriptions = Vec::new(); - subscriptions.push( - cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| { - cx.notify() - }), - ); - subscriptions.push(cx.subscribe(&project, |_, _, _: &project::Event, cx| cx.notify())); - subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx))); - subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed)); - subscriptions.push( - cx.subscribe(&git_store, move |_, _, event, cx| match event { - GitStoreEvent::ActiveRepositoryChanged(_) - | GitStoreEvent::RepositoryUpdated(_, _, true) => { - cx.notify(); - } - _ => {} - }), - ); - subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify())); - - let banner = cx.new(|cx| { - OnboardingBanner::new( - "ACP Claude Code Onboarding", - IconName::AiClaude, - "Claude Code", - Some("Introducing:".into()), - zed_actions::agent::OpenClaudeCodeOnboardingModal.boxed_clone(), - cx, - ) - // When updating this to a non-AI feature release, remove this line. - .visible_when(|cx| !project::DisableAiSettings::get_global(cx).disable_ai) - }); - - let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, cx)); - - Self { - platform_titlebar, - application_menu, - workspace: workspace.weak_handle(), - project, - user_store, - client, - _subscriptions: subscriptions, - banner, - screen_share_popover_handle: Default::default(), - } - } - - fn render_remote_project_connection(&self, cx: &mut Context) -> Option { - let options = self.project.read(cx).remote_connection_options(cx)?; - let host: SharedString = options.display_name().into(); - - let (nickname, tooltip_title, icon) = match options { - RemoteConnectionOptions::Ssh(options) => ( - options.nickname.map(|nick| nick.into()), - "Remote Project", - IconName::Server, - ), - RemoteConnectionOptions::Wsl(_) => (None, "Remote Project", IconName::Linux), - RemoteConnectionOptions::Docker(_dev_container_connection) => { - (None, "Dev Container", IconName::Box) - } - }; - - let nickname = nickname.unwrap_or_else(|| host.clone()); - - let (indicator_color, meta) = match self.project.read(cx).remote_connection_state(cx)? { - remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")), - remote::ConnectionState::Connected => (Color::Success, format!("Connected to: {host}")), - remote::ConnectionState::HeartbeatMissed => ( - Color::Warning, - format!("Connection attempt to {host} missed. Retrying..."), - ), - remote::ConnectionState::Reconnecting => ( - Color::Warning, - format!("Lost connection to {host}. Reconnecting..."), - ), - remote::ConnectionState::Disconnected => { - (Color::Error, format!("Disconnected from {host}")) - } - }; - - let icon_color = match self.project.read(cx).remote_connection_state(cx)? { - remote::ConnectionState::Connecting => Color::Info, - remote::ConnectionState::Connected => Color::Default, - remote::ConnectionState::HeartbeatMissed => Color::Warning, - remote::ConnectionState::Reconnecting => Color::Warning, - remote::ConnectionState::Disconnected => Color::Error, - }; - - let meta = SharedString::from(meta); - - Some( - ButtonLike::new("ssh-server-icon") - .child( - h_flex() - .gap_2() - .max_w_32() - .child( - IconWithIndicator::new( - Icon::new(icon).size(IconSize::Small).color(icon_color), - Some(Indicator::dot().color(indicator_color)), - ) - .indicator_border_color(Some(cx.theme().colors().title_bar_background)) - .into_any_element(), - ) - .child(Label::new(nickname).size(LabelSize::Small).truncate()), - ) - .tooltip(move |_window, cx| { - Tooltip::with_meta( - tooltip_title, - Some(&OpenRemote { - from_existing_connection: false, - create_new_window: false, - }), - meta.clone(), - cx, - ) - }) - .on_click(|_, window, cx| { - window.dispatch_action( - OpenRemote { - from_existing_connection: false, - create_new_window: false, - } - .boxed_clone(), - cx, - ); - }) - .into_any_element(), - ) - } - - pub fn render_project_host(&self, cx: &mut Context) -> Option { - if self.project.read(cx).is_via_remote_server() { - return self.render_remote_project_connection(cx); - } - - if self.project.read(cx).is_disconnected(cx) { - return Some( - Button::new("disconnected", "Disconnected") - .disabled(true) - .color(Color::Disabled) - .style(ButtonStyle::Subtle) - .label_size(LabelSize::Small) - .into_any_element(), - ); - } - - let host = self.project.read(cx).host()?; - let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?; - let participant_index = self - .user_store - .read(cx) - .participant_indices() - .get(&host_user.id)?; - Some( - Button::new("project_owner_trigger", host_user.github_login.clone()) - .color(Color::Player(participant_index.0)) - .style(ButtonStyle::Subtle) - .label_size(LabelSize::Small) - .tooltip(Tooltip::text(format!( - "{} is sharing this project. Click to follow.", - host_user.github_login - ))) - .on_click({ - let host_peer_id = host.peer_id; - cx.listener(move |this, _, window, cx| { - this.workspace - .update(cx, |workspace, cx| { - workspace.follow(host_peer_id, window, cx); - }) - .log_err(); - }) - }) - .into_any_element(), - ) - } - - pub fn render_project_name(&self, cx: &mut Context) -> impl IntoElement { - let name = self - .project - .read(cx) - .visible_worktrees(cx) - .map(|worktree| { - let worktree = worktree.read(cx); - let settings_location = SettingsLocation { - worktree_id: worktree.id(), - path: RelPath::empty(), - }; - - let settings = WorktreeSettings::get(Some(settings_location), cx); - match &settings.project_name { - Some(name) => name.as_str(), - None => worktree.root_name_str(), - } - }) - .next(); - let is_project_selected = name.is_some(); - let name = if let Some(name) = name { - util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH) - } else { - "Open recent project".to_string() - }; - - Button::new("project_name_trigger", name) - .when(!is_project_selected, |b| b.color(Color::Muted)) - .style(ButtonStyle::Subtle) - .label_size(LabelSize::Small) - .tooltip(move |_window, cx| { - Tooltip::for_action( - "Recent Projects", - &zed_actions::OpenRecent { - create_new_window: false, - }, - cx, - ) - }) - .on_click(cx.listener(move |_, _, window, cx| { - window.dispatch_action( - OpenRecent { - create_new_window: false, - } - .boxed_clone(), - cx, - ); - })) - } - - pub fn render_project_branch(&self, cx: &mut Context) -> Option { - let settings = TitleBarSettings::get_global(cx); - let repository = self.project.read(cx).active_repository(cx)?; - let workspace = self.workspace.upgrade()?; - let repo = repository.read(cx); - let branch_name = repo - .branch - .as_ref() - .map(|branch| branch.name()) - .map(|name| util::truncate_and_trailoff(name, MAX_BRANCH_NAME_LENGTH)) - .or_else(|| { - repo.head_commit.as_ref().map(|commit| { - commit - .sha - .chars() - .take(MAX_SHORT_SHA_LENGTH) - .collect::() - }) - })?; - - Some( - Button::new("project_branch_trigger", branch_name) - .color(Color::Muted) - .style(ButtonStyle::Subtle) - .label_size(LabelSize::Small) - .tooltip(move |_window, cx| { - Tooltip::with_meta( - "Recent Branches", - Some(&zed_actions::git::Branch), - "Local branches only", - cx, - ) - }) - .on_click(move |_, window, cx| { - let _ = workspace.update(cx, |this, cx| { - window.focus(&this.active_pane().focus_handle(cx)); - window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx); - }); - }) - .when(settings.show_branch_icon, |branch_button| { - let (icon, icon_color) = { - let status = repo.status_summary(); - let tracked = status.index + status.worktree; - if status.conflict > 0 { - (IconName::Warning, Color::VersionControlConflict) - } else if tracked.modified > 0 { - (IconName::SquareDot, Color::VersionControlModified) - } else if tracked.added > 0 || status.untracked > 0 { - (IconName::SquarePlus, Color::VersionControlAdded) - } else if tracked.deleted > 0 { - (IconName::SquareMinus, Color::VersionControlDeleted) - } else { - (IconName::GitBranch, Color::Muted) - } - }; - - branch_button - .icon(icon) - .icon_position(IconPosition::Start) - .icon_color(icon_color) - .icon_size(IconSize::Indicator) - }), - ) - } - - fn window_activation_changed(&mut self, window: &mut Window, cx: &mut Context) { - if window.is_window_active() { - ActiveCall::global(cx) - .update(cx, |call, cx| call.set_location(Some(&self.project), cx)) - .detach_and_log_err(cx); - } else if cx.active_window().is_none() { - ActiveCall::global(cx) - .update(cx, |call, cx| call.set_location(None, cx)) - .detach_and_log_err(cx); - } - self.workspace - .update(cx, |workspace, cx| { - workspace.update_active_view_for_followers(window, cx); - }) - .ok(); - } - - fn active_call_changed(&mut self, cx: &mut Context) { - cx.notify(); - } - - fn share_project(&mut self, cx: &mut Context) { - let active_call = ActiveCall::global(cx); - let project = self.project.clone(); - active_call - .update(cx, |call, cx| call.share_project(project, cx)) - .detach_and_log_err(cx); - } - - fn unshare_project(&mut self, _: &mut Window, cx: &mut Context) { - let active_call = ActiveCall::global(cx); - let project = self.project.clone(); - active_call - .update(cx, |call, cx| call.unshare_project(project, cx)) - .log_err(); - } - - fn render_connection_status( - &self, - status: &client::Status, - cx: &mut Context, - ) -> Option { - match status { - client::Status::ConnectionError - | client::Status::ConnectionLost - | client::Status::Reauthenticating - | client::Status::Reconnecting - | client::Status::ReconnectionError { .. } => Some( - div() - .id("disconnected") - .child(Icon::new(IconName::Disconnected).size(IconSize::Small)) - .tooltip(Tooltip::text("Disconnected")) - .into_any_element(), - ), - client::Status::UpgradeRequired => { - let auto_updater = auto_update::AutoUpdater::get(cx); - let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) { - Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate", - Some(AutoUpdateStatus::Installing { .. }) - | Some(AutoUpdateStatus::Downloading { .. }) - | Some(AutoUpdateStatus::Checking) => "Updating...", - Some(AutoUpdateStatus::Idle) - | Some(AutoUpdateStatus::Errored { .. }) - | None => "Please update Zed to Collaborate", - }; - - Some( - Button::new("connection-status", label) - .label_size(LabelSize::Small) - .on_click(|_, window, cx| { - if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) - && auto_updater.read(cx).status().is_updated() - { - workspace::reload(cx); - return; - } - auto_update::check(&Default::default(), window, cx); - }) - .into_any_element(), - ) - } - _ => None, - } - } - - pub fn render_sign_in_button(&mut self, _: &mut Context) -> Button { - let client = self.client.clone(); - Button::new("sign_in", "Sign in") - .label_size(LabelSize::Small) - .on_click(move |_, window, cx| { - let client = client.clone(); - window - .spawn(cx, async move |cx| { - client - .sign_in_with_optional_connect(true, cx) - .await - .notify_async_err(cx); - }) - .detach(); - }) - } - - pub fn render_app_menu_button(&mut self, cx: &mut Context) -> impl Element { - let user_store = self.user_store.read(cx); - let user = user_store.current_user(); - - let user_avatar = user.as_ref().map(|u| u.avatar_uri.clone()); - let user_login = user.as_ref().map(|u| u.github_login.clone()); - - let is_signed_in = user.is_some(); - - let has_subscription_period = user_store.subscription_period().is_some(); - let plan = user_store.plan().filter(|_| { - // Since the user might be on the legacy free plan we filter based on whether we have a subscription period. - has_subscription_period - }); - - let free_chip_bg = cx - .theme() - .colors() - .editor_background - .opacity(0.5) - .blend(cx.theme().colors().text_accent.opacity(0.05)); - - let pro_chip_bg = cx - .theme() - .colors() - .editor_background - .opacity(0.5) - .blend(cx.theme().colors().text_accent.opacity(0.2)); - - PopoverMenu::new("user-menu") - .anchor(Corner::TopRight) - .menu(move |window, cx| { - ContextMenu::build(window, cx, |menu, _, _cx| { - let user_login = user_login.clone(); - - let (plan_name, label_color, bg_color) = match plan { - None | Some(Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree)) => { - ("Free", Color::Default, free_chip_bg) - } - Some(Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial)) => { - ("Pro Trial", Color::Accent, pro_chip_bg) - } - Some(Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro)) => { - ("Pro", Color::Accent, pro_chip_bg) - } - }; - - menu.when(is_signed_in, |this| { - this.custom_entry( - move |_window, _cx| { - let user_login = user_login.clone().unwrap_or_default(); - - h_flex() - .w_full() - .justify_between() - .child(Label::new(user_login)) - .child( - Chip::new(plan_name.to_string()) - .bg_color(bg_color) - .label_color(label_color), - ) - .into_any_element() - }, - move |_, cx| { - cx.open_url(&zed_urls::account_url(cx)); - }, - ) - .separator() - }) - .action("Settings", zed_actions::OpenSettings.boxed_clone()) - .action("Keymap", Box::new(zed_actions::OpenKeymap)) - .action( - "Themes…", - zed_actions::theme_selector::Toggle::default().boxed_clone(), - ) - .action( - "Icon Themes…", - zed_actions::icon_theme_selector::Toggle::default().boxed_clone(), - ) - .action( - "Extensions", - zed_actions::Extensions::default().boxed_clone(), - ) - .when(is_signed_in, |this| { - this.separator() - .action("Sign Out", client::SignOut.boxed_clone()) - }) - }) - .into() - }) - .map(|this| { - if is_signed_in && TitleBarSettings::get_global(cx).show_user_picture { - this.trigger_with_tooltip( - ButtonLike::new("user-menu") - .children(user_avatar.clone().map(|avatar| Avatar::new(avatar))), - Tooltip::text("Toggle User Menu"), - ) - } else { - this.trigger_with_tooltip( - IconButton::new("user-menu", IconName::ChevronDown) - .icon_size(IconSize::Small), - Tooltip::text("Toggle User Menu"), - ) - } - }) - .anchor(gpui::Corner::TopRight) - } -} diff --git a/crates/title_bar/src/title_bar_settings.rs b/crates/title_bar/src/title_bar_settings.rs deleted file mode 100644 index 29fae4d31e..0000000000 --- a/crates/title_bar/src/title_bar_settings.rs +++ /dev/null @@ -1,27 +0,0 @@ -use settings::{RegisterSetting, Settings, SettingsContent}; - -#[derive(Copy, Clone, Debug, RegisterSetting)] -pub struct TitleBarSettings { - pub show_branch_icon: bool, - pub show_onboarding_banner: bool, - pub show_user_picture: bool, - pub show_branch_name: bool, - pub show_project_items: bool, - pub show_sign_in: bool, - pub show_menus: bool, -} - -impl Settings for TitleBarSettings { - fn from_settings(s: &SettingsContent) -> Self { - let content = s.title_bar.clone().unwrap(); - TitleBarSettings { - show_branch_icon: content.show_branch_icon.unwrap(), - show_onboarding_banner: content.show_onboarding_banner.unwrap(), - show_user_picture: content.show_user_picture.unwrap(), - show_branch_name: content.show_branch_name.unwrap(), - show_project_items: content.show_project_items.unwrap(), - show_sign_in: content.show_sign_in.unwrap(), - show_menus: content.show_menus.unwrap(), - } - } -} diff --git a/crates/toolchain_selector/Cargo.toml b/crates/toolchain_selector/Cargo.toml deleted file mode 100644 index 94a655b727..0000000000 --- a/crates/toolchain_selector/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "toolchain_selector" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[dependencies] -anyhow.workspace = true -convert_case.workspace = true -editor.workspace = true -file_finder.workspace = true -futures.workspace = true -fuzzy.workspace = true -gpui.workspace = true -language.workspace = true -menu.workspace = true -picker.workspace = true -project.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true - -[lints] -workspace = true - -[lib] -path = "src/toolchain_selector.rs" -doctest = false diff --git a/crates/toolchain_selector/LICENSE-GPL b/crates/toolchain_selector/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/toolchain_selector/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/toolchain_selector/src/active_toolchain.rs b/crates/toolchain_selector/src/active_toolchain.rs deleted file mode 100644 index 03c152e3fd..0000000000 --- a/crates/toolchain_selector/src/active_toolchain.rs +++ /dev/null @@ -1,264 +0,0 @@ -use std::sync::Arc; - -use editor::Editor; -use gpui::{ - AsyncWindowContext, Context, Entity, IntoElement, ParentElement, Render, Styled, Subscription, - Task, WeakEntity, Window, div, -}; -use language::{Buffer, BufferEvent, LanguageName, Toolchain, ToolchainScope}; -use project::{Project, ProjectPath, Toolchains, WorktreeId, toolchain_store::ToolchainStoreEvent}; -use ui::{Button, ButtonCommon, Clickable, LabelSize, SharedString, Tooltip}; -use util::{maybe, rel_path::RelPath}; -use workspace::{StatusItemView, Workspace, item::ItemHandle}; - -use crate::ToolchainSelector; - -pub struct ActiveToolchain { - active_toolchain: Option, - term: SharedString, - workspace: WeakEntity, - active_buffer: Option<(WorktreeId, WeakEntity, Subscription)>, - _update_toolchain_task: Task>, -} - -impl ActiveToolchain { - pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context) -> Self { - if let Some(store) = workspace.project().read(cx).toolchain_store() { - cx.subscribe_in( - &store, - window, - |this, _, _: &ToolchainStoreEvent, window, cx| { - let editor = this - .workspace - .update(cx, |workspace, cx| { - workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - }) - .ok() - .flatten(); - if let Some(editor) = editor { - this.update_lister(editor, window, cx); - } - }, - ) - .detach(); - } - Self { - active_toolchain: None, - active_buffer: None, - term: SharedString::new_static("Toolchain"), - workspace: workspace.weak_handle(), - - _update_toolchain_task: Self::spawn_tracker_task(window, cx), - } - } - fn spawn_tracker_task(window: &mut Window, cx: &mut Context) -> Task> { - cx.spawn_in(window, async move |this, cx| { - let did_set_toolchain = maybe!(async { - let active_file = this - .read_with(cx, |this, _| { - this.active_buffer - .as_ref() - .map(|(_, buffer, _)| buffer.clone()) - }) - .ok() - .flatten()?; - let workspace = this.read_with(cx, |this, _| this.workspace.clone()).ok()?; - let language_name = active_file - .read_with(cx, |this, _| Some(this.language()?.name())) - .ok() - .flatten()?; - let meta = workspace - .update(cx, |workspace, cx| { - let languages = workspace.project().read(cx).languages(); - Project::toolchain_metadata(languages.clone(), language_name.clone()) - }) - .ok()? - .await?; - let _ = this.update(cx, |this, cx| { - this.term = meta.term; - cx.notify(); - }); - let (worktree_id, path) = active_file - .update(cx, |this, cx| { - this.file().and_then(|file| { - Some((file.worktree_id(cx), file.path().parent()?.into())) - }) - }) - .ok() - .flatten()?; - let toolchain = - Self::active_toolchain(workspace, worktree_id, path, language_name, cx).await?; - this.update(cx, |this, cx| { - this.active_toolchain = Some(toolchain); - - cx.notify(); - }) - .ok() - }) - .await - .is_some(); - if !did_set_toolchain { - this.update(cx, |this, cx| { - this.active_toolchain = None; - cx.notify(); - }) - .ok(); - } - did_set_toolchain.then_some(()) - }) - } - - fn update_lister( - &mut self, - editor: Entity, - window: &mut Window, - cx: &mut Context, - ) { - let editor = editor.read(cx); - if let Some((_, buffer, _)) = editor.active_excerpt(cx) - && let Some(worktree_id) = buffer.read(cx).file().map(|file| file.worktree_id(cx)) - { - let subscription = cx.subscribe_in( - &buffer, - window, - |this, _, event: &BufferEvent, window, cx| { - if matches!(event, BufferEvent::LanguageChanged(_)) { - this._update_toolchain_task = Self::spawn_tracker_task(window, cx); - } - }, - ); - self.active_buffer = Some((worktree_id, buffer.downgrade(), subscription)); - self._update_toolchain_task = Self::spawn_tracker_task(window, cx); - } - - cx.notify(); - } - - fn active_toolchain( - workspace: WeakEntity, - worktree_id: WorktreeId, - relative_path: Arc, - language_name: LanguageName, - cx: &mut AsyncWindowContext, - ) -> Task> { - cx.spawn(async move |cx| { - let workspace_id = workspace - .read_with(cx, |this, _| this.database_id()) - .ok() - .flatten()?; - let selected_toolchain = workspace - .update(cx, |this, cx| { - this.project().read(cx).active_toolchain( - ProjectPath { - worktree_id, - path: relative_path.clone(), - }, - language_name.clone(), - cx, - ) - }) - .ok()? - .await; - if let Some(toolchain) = selected_toolchain { - Some(toolchain) - } else { - let project = workspace - .read_with(cx, |this, _| this.project().clone()) - .ok()?; - let Toolchains { - toolchains, - root_path: relative_path, - user_toolchains, - } = cx - .update(|_, cx| { - project.read(cx).available_toolchains( - ProjectPath { - worktree_id, - path: relative_path.clone(), - }, - language_name, - cx, - ) - }) - .ok()? - .await?; - // Since we don't have a selected toolchain, pick one for user here. - let default_choice = user_toolchains - .iter() - .find_map(|(scope, toolchains)| { - if scope == &ToolchainScope::Global { - // Ignore global toolchains when making a default choice. They're unlikely to be the right choice. - None - } else { - toolchains.first() - } - }) - .or_else(|| toolchains.toolchains.first()) - .cloned(); - if let Some(toolchain) = &default_choice { - workspace::WORKSPACE_DB - .set_toolchain( - workspace_id, - worktree_id, - relative_path.clone(), - toolchain.clone(), - ) - .await - .ok()?; - project - .update(cx, |this, cx| { - this.activate_toolchain( - ProjectPath { - worktree_id, - path: relative_path, - }, - toolchain.clone(), - cx, - ) - }) - .ok()? - .await; - } - - default_choice - } - }) - } -} - -impl Render for ActiveToolchain { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let Some(active_toolchain) = self.active_toolchain.as_ref() else { - return div().hidden(); - }; - - div().child( - Button::new("change-toolchain", active_toolchain.name.clone()) - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - if let Some(workspace) = this.workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - ToolchainSelector::toggle(workspace, window, cx) - }); - } - })) - .tooltip(Tooltip::text(format!("Select {}", &self.term))), - ) - } -} - -impl StatusItemView for ActiveToolchain { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(editor) = active_pane_item.and_then(|item| item.downcast::()) { - self.update_lister(editor, window, cx); - } - cx.notify(); - } -} diff --git a/crates/toolchain_selector/src/toolchain_selector.rs b/crates/toolchain_selector/src/toolchain_selector.rs deleted file mode 100644 index 138f99066f..0000000000 --- a/crates/toolchain_selector/src/toolchain_selector.rs +++ /dev/null @@ -1,1142 +0,0 @@ -mod active_toolchain; - -pub use active_toolchain::ActiveToolchain; -use convert_case::Casing as _; -use editor::Editor; -use file_finder::OpenPathDelegate; -use futures::channel::oneshot; -use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; -use gpui::{ - Action, Animation, AnimationExt, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, - Focusable, KeyContext, ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, - actions, pulsating_between, -}; -use language::{Language, LanguageName, Toolchain, ToolchainScope}; -use picker::{Picker, PickerDelegate}; -use project::{DirectoryLister, Project, ProjectPath, Toolchains, WorktreeId}; -use std::{ - borrow::Cow, - path::{Path, PathBuf}, - sync::Arc, - time::Duration, -}; -use ui::{ - Divider, HighlightedLabel, KeyBinding, List, ListItem, ListItemSpacing, Navigable, - NavigableEntry, prelude::*, -}; -use util::{ResultExt, maybe, paths::PathStyle, rel_path::RelPath}; -use workspace::{ModalView, Workspace}; - -actions!( - toolchain, - [ - /// Selects a toolchain for the current project. - Select, - /// Adds a new toolchain for the current project. - AddToolchain - ] -); - -pub fn init(cx: &mut App) { - cx.observe_new(ToolchainSelector::register).detach(); -} - -pub struct ToolchainSelector { - state: State, - create_search_state: Arc) -> SearchState + 'static>, - language: Option>, - project: Entity, - language_name: LanguageName, - worktree_id: WorktreeId, - relative_path: Arc, -} - -#[derive(Clone)] -struct SearchState { - picker: Entity>, -} - -struct AddToolchainState { - state: AddState, - project: Entity, - language_name: LanguageName, - root_path: ProjectPath, - weak: WeakEntity, -} - -struct ScopePickerState { - entries: [NavigableEntry; 3], - selected_scope: ToolchainScope, -} - -#[expect( - dead_code, - reason = "These tasks have to be kept alive to run to completion" -)] -enum PathInputState { - WaitingForPath(Task<()>), - Resolving(Task<()>), -} - -enum AddState { - Path { - picker: Entity>, - error: Option>, - input_state: PathInputState, - _subscription: Subscription, - }, - Name { - toolchain: Toolchain, - editor: Entity, - scope_picker: ScopePickerState, - }, -} - -impl AddToolchainState { - fn new( - project: Entity, - language_name: LanguageName, - root_path: ProjectPath, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let weak = cx.weak_entity(); - - cx.new(|cx| { - let (lister, rx) = Self::create_path_browser_delegate(project.clone(), cx); - let picker = cx.new(|cx| Picker::uniform_list(lister, window, cx)); - Self { - state: AddState::Path { - _subscription: cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| { - cx.stop_propagation(); - }), - picker, - error: None, - input_state: Self::wait_for_path(rx, window, cx), - }, - project, - language_name, - root_path, - weak, - } - }) - } - - fn create_path_browser_delegate( - project: Entity, - cx: &mut Context, - ) -> (OpenPathDelegate, oneshot::Receiver>>) { - let (tx, rx) = oneshot::channel(); - let weak = cx.weak_entity(); - let path_style = project.read(cx).path_style(cx); - let lister = - OpenPathDelegate::new(tx, DirectoryLister::Project(project), false, path_style) - .show_hidden() - .with_footer(Arc::new(move |_, cx| { - let error = weak - .read_with(cx, |this, _| { - if let AddState::Path { error, .. } = &this.state { - error.clone() - } else { - None - } - }) - .ok() - .flatten(); - let is_loading = weak - .read_with(cx, |this, _| { - matches!( - this.state, - AddState::Path { - input_state: PathInputState::Resolving(_), - .. - } - ) - }) - .unwrap_or_default(); - Some( - v_flex() - .child(Divider::horizontal()) - .child( - h_flex() - .p_1() - .justify_between() - .gap_2() - .child( - Label::new("Select Toolchain Path") - .color(Color::Muted) - .map(|this| { - if is_loading { - this.with_animation( - "select-toolchain-label", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between( - 0.4, 0.8, - )), - |label, delta| label.alpha(delta), - ) - .into_any() - } else { - this.into_any_element() - } - }), - ) - .when_some(error, |this, error| { - this.child(Label::new(error).color(Color::Error)) - }), - ) - .into_any(), - ) - })); - - (lister, rx) - } - fn resolve_path( - path: PathBuf, - root_path: ProjectPath, - language_name: LanguageName, - project: Entity, - window: &mut Window, - cx: &mut Context, - ) -> PathInputState { - PathInputState::Resolving(cx.spawn_in(window, async move |this, cx| { - _ = maybe!(async move { - let toolchain = project - .update(cx, |this, cx| { - this.resolve_toolchain(path.clone(), language_name, cx) - })? - .await; - let Ok(toolchain) = toolchain else { - // Go back to the path input state - _ = this.update_in(cx, |this, window, cx| { - if let AddState::Path { - input_state, - picker, - error, - .. - } = &mut this.state - && matches!(input_state, PathInputState::Resolving(_)) - { - let Err(e) = toolchain else { unreachable!() }; - *error = Some(Arc::from(e.to_string())); - let (delegate, rx) = - Self::create_path_browser_delegate(this.project.clone(), cx); - picker.update(cx, |picker, cx| { - *picker = Picker::uniform_list(delegate, window, cx); - picker.set_query( - Arc::from(path.to_string_lossy().as_ref()), - window, - cx, - ); - }); - *input_state = Self::wait_for_path(rx, window, cx); - this.focus_handle(cx).focus(window); - } - }); - return Err(anyhow::anyhow!("Failed to resolve toolchain")); - }; - let resolved_toolchain_path = project.read_with(cx, |this, cx| { - this.find_project_path(&toolchain.path.as_ref(), cx) - })?; - - // Suggest a default scope based on the applicability. - let scope = if let Some(project_path) = resolved_toolchain_path { - if !root_path.path.as_ref().is_empty() && project_path.starts_with(&root_path) { - ToolchainScope::Subproject(root_path.worktree_id, root_path.path) - } else { - ToolchainScope::Project - } - } else { - // This path lies outside of the project. - ToolchainScope::Global - }; - - _ = this.update_in(cx, |this, window, cx| { - let scope_picker = ScopePickerState { - entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)), - selected_scope: scope, - }; - this.state = AddState::Name { - editor: cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_text(toolchain.name.as_ref(), window, cx); - editor - }), - toolchain, - scope_picker, - }; - this.focus_handle(cx).focus(window); - }); - - Result::<_, anyhow::Error>::Ok(()) - }) - .await; - })) - } - - fn wait_for_path( - rx: oneshot::Receiver>>, - window: &mut Window, - cx: &mut Context, - ) -> PathInputState { - let task = cx.spawn_in(window, async move |this, cx| { - maybe!(async move { - let result = rx.await.log_err()?; - - let path = result - .into_iter() - .flat_map(|paths| paths.into_iter()) - .next()?; - this.update_in(cx, |this, window, cx| { - if let AddState::Path { - input_state, error, .. - } = &mut this.state - && matches!(input_state, PathInputState::WaitingForPath(_)) - { - error.take(); - *input_state = Self::resolve_path( - path, - this.root_path.clone(), - this.language_name.clone(), - this.project.clone(), - window, - cx, - ); - } - }) - .ok()?; - Some(()) - }) - .await; - }); - PathInputState::WaitingForPath(task) - } - - fn confirm_toolchain( - &mut self, - _: &menu::Confirm, - window: &mut Window, - cx: &mut Context, - ) { - let AddState::Name { - toolchain, - editor, - scope_picker, - } = &mut self.state - else { - return; - }; - - let text = editor.read(cx).text(cx); - if text.is_empty() { - return; - } - - toolchain.name = SharedString::from(text); - self.project.update(cx, |this, cx| { - this.add_toolchain(toolchain.clone(), scope_picker.selected_scope.clone(), cx); - }); - _ = self.weak.update(cx, |this, cx| { - this.state = State::Search((this.create_search_state)(window, cx)); - this.focus_handle(cx).focus(window); - cx.notify(); - }); - } -} -impl Focusable for AddToolchainState { - fn focus_handle(&self, cx: &App) -> FocusHandle { - match &self.state { - AddState::Path { picker, .. } => picker.focus_handle(cx), - AddState::Name { editor, .. } => editor.focus_handle(cx), - } - } -} - -impl AddToolchainState { - fn select_scope(&mut self, scope: ToolchainScope, cx: &mut Context) { - if let AddState::Name { scope_picker, .. } = &mut self.state { - scope_picker.selected_scope = scope; - cx.notify(); - } - } -} - -impl Focusable for State { - fn focus_handle(&self, cx: &App) -> FocusHandle { - match self { - State::Search(state) => state.picker.focus_handle(cx), - State::AddToolchain(state) => state.focus_handle(cx), - } - } -} -impl Render for AddToolchainState { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let theme = cx.theme().clone(); - let weak = self.weak.upgrade(); - let label = SharedString::new_static("Add"); - - v_flex() - .size_full() - // todo: These modal styles shouldn't be needed as the modal picker already has `elevation_3` - // They get duplicated in the middle state of adding a virtual env, but then are needed for this last state - .bg(cx.theme().colors().elevated_surface_background) - .border_1() - .border_color(cx.theme().colors().border_variant) - .rounded_lg() - .when_some(weak, |this, weak| { - this.on_action(window.listener_for( - &weak, - |this: &mut ToolchainSelector, _: &menu::Cancel, window, cx| { - this.state = State::Search((this.create_search_state)(window, cx)); - this.state.focus_handle(cx).focus(window); - cx.notify(); - }, - )) - }) - .on_action(cx.listener(Self::confirm_toolchain)) - .map(|this| match &self.state { - AddState::Path { picker, .. } => this.child(picker.clone()), - AddState::Name { - editor, - scope_picker, - .. - } => { - let scope_options = [ - ToolchainScope::Global, - ToolchainScope::Project, - ToolchainScope::Subproject( - self.root_path.worktree_id, - self.root_path.path.clone(), - ), - ]; - - let mut navigable_scope_picker = Navigable::new( - v_flex() - .child( - h_flex() - .w_full() - .p_2() - .border_b_1() - .border_color(theme.colors().border) - .child(editor.clone()), - ) - .child( - v_flex() - .child( - Label::new("Scope") - .size(LabelSize::Small) - .color(Color::Muted) - .mt_1() - .ml_2(), - ) - .child(List::new().children( - scope_options.iter().enumerate().map(|(i, scope)| { - let is_selected = *scope == scope_picker.selected_scope; - let label = scope.label(); - let description = scope.description(); - let scope_clone_for_action = scope.clone(); - let scope_clone_for_click = scope.clone(); - - div() - .id(SharedString::from(format!("scope-option-{i}"))) - .track_focus(&scope_picker.entries[i].focus_handle) - .on_action(cx.listener( - move |this, _: &menu::Confirm, _, cx| { - this.select_scope( - scope_clone_for_action.clone(), - cx, - ); - }, - )) - .child( - ListItem::new(SharedString::from(format!( - "scope-{i}" - ))) - .toggle_state( - is_selected - || scope_picker.entries[i] - .focus_handle - .contains_focused(window, cx), - ) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .child( - h_flex() - .gap_2() - .child(Label::new(label)) - .child( - Label::new(description) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - .on_click(cx.listener(move |this, _, _, cx| { - this.select_scope( - scope_clone_for_click.clone(), - cx, - ); - })), - ) - }), - )) - .child(Divider::horizontal()) - .child(h_flex().p_1p5().justify_end().map(|this| { - let is_disabled = editor.read(cx).is_empty(cx); - let handle = self.focus_handle(cx); - this.child( - Button::new("add-toolchain", label) - .disabled(is_disabled) - .key_binding(KeyBinding::for_action_in( - &menu::Confirm, - &handle, - cx, - )) - .on_click(cx.listener(|this, _, window, cx| { - this.confirm_toolchain( - &menu::Confirm, - window, - cx, - ); - })) - .map(|this| { - if false { - this.with_animation( - "inspecting-user-toolchain", - Animation::new(Duration::from_millis( - 500, - )) - .repeat() - .with_easing(pulsating_between( - 0.4, 0.8, - )), - |label, delta| label.alpha(delta), - ) - .into_any() - } else { - this.into_any_element() - } - }), - ) - })), - ) - .into_any_element(), - ); - - for entry in &scope_picker.entries { - navigable_scope_picker = navigable_scope_picker.entry(entry.clone()); - } - - this.child(navigable_scope_picker.render(window, cx)) - } - }) - } -} - -#[derive(Clone)] -enum State { - Search(SearchState), - AddToolchain(Entity), -} - -impl RenderOnce for State { - fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { - match self { - State::Search(state) => state.picker.into_any_element(), - State::AddToolchain(state) => state.into_any_element(), - } - } -} -impl ToolchainSelector { - fn register( - workspace: &mut Workspace, - _window: Option<&mut Window>, - _: &mut Context, - ) { - workspace.register_action(move |workspace, _: &Select, window, cx| { - Self::toggle(workspace, window, cx); - }); - workspace.register_action(move |workspace, _: &AddToolchain, window, cx| { - let Some(toolchain_selector) = workspace.active_modal::(cx) else { - Self::toggle(workspace, window, cx); - return; - }; - - toolchain_selector.update(cx, |toolchain_selector, cx| { - toolchain_selector.handle_add_toolchain(&AddToolchain, window, cx); - }); - }); - } - - fn toggle( - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - let (_, buffer, _) = workspace - .active_item(cx)? - .act_as::(cx)? - .read(cx) - .active_excerpt(cx)?; - let project = workspace.project().clone(); - - let language_name = buffer.read(cx).language()?.name(); - let worktree_id = buffer.read(cx).file()?.worktree_id(cx); - let relative_path: Arc = buffer.read(cx).file()?.path().parent()?.into(); - let worktree_root_path = project - .read(cx) - .worktree_for_id(worktree_id, cx)? - .read(cx) - .abs_path(); - let weak = workspace.weak_handle(); - cx.spawn_in(window, async move |workspace, cx| { - let active_toolchain = project - .read_with(cx, |this, cx| { - this.active_toolchain( - ProjectPath { - worktree_id, - path: relative_path.clone(), - }, - language_name.clone(), - cx, - ) - })? - .await; - workspace - .update_in(cx, |this, window, cx| { - this.toggle_modal(window, cx, move |window, cx| { - ToolchainSelector::new( - weak, - project, - active_toolchain, - worktree_id, - worktree_root_path, - relative_path, - language_name, - window, - cx, - ) - }); - }) - .ok(); - anyhow::Ok(()) - }) - .detach(); - - Some(()) - } - - fn new( - workspace: WeakEntity, - project: Entity, - active_toolchain: Option, - worktree_id: WorktreeId, - worktree_root: Arc, - relative_path: Arc, - language_name: LanguageName, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let language_registry = project.read(cx).languages().clone(); - cx.spawn({ - let language_name = language_name.clone(); - async move |this, cx| { - let language = language_registry - .language_for_name(&language_name.0) - .await - .ok(); - this.update(cx, |this, cx| { - this.language = language; - cx.notify(); - }) - .ok(); - } - }) - .detach(); - let project_clone = project.clone(); - let language_name_clone = language_name.clone(); - let relative_path_clone = relative_path.clone(); - - let create_search_state = Arc::new(move |window: &mut Window, cx: &mut Context| { - let toolchain_selector = cx.entity().downgrade(); - let picker = cx.new(|cx| { - let delegate = ToolchainSelectorDelegate::new( - active_toolchain.clone(), - toolchain_selector, - workspace.clone(), - worktree_id, - worktree_root.clone(), - project_clone.clone(), - relative_path_clone.clone(), - language_name_clone.clone(), - window, - cx, - ); - Picker::uniform_list(delegate, window, cx) - }); - let picker_focus_handle = picker.focus_handle(cx); - picker.update(cx, |picker, _| { - picker.delegate.focus_handle = picker_focus_handle.clone(); - }); - SearchState { picker } - }); - - Self { - state: State::Search(create_search_state(window, cx)), - create_search_state, - language: None, - project, - language_name, - worktree_id, - relative_path, - } - } - - fn handle_add_toolchain( - &mut self, - _: &AddToolchain, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.state, State::Search(_)) { - self.state = State::AddToolchain(AddToolchainState::new( - self.project.clone(), - self.language_name.clone(), - ProjectPath { - worktree_id: self.worktree_id, - path: self.relative_path.clone(), - }, - window, - cx, - )); - self.state.focus_handle(cx).focus(window); - cx.notify(); - } - } -} - -impl Render for ToolchainSelector { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let mut key_context = KeyContext::new_with_defaults(); - key_context.add("ToolchainSelector"); - - v_flex() - .key_context(key_context) - .w(rems(34.)) - .on_action(cx.listener(Self::handle_add_toolchain)) - .child(self.state.clone().render(window, cx)) - } -} - -impl Focusable for ToolchainSelector { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.state.focus_handle(cx) - } -} - -impl EventEmitter for ToolchainSelector {} -impl ModalView for ToolchainSelector {} - -pub struct ToolchainSelectorDelegate { - toolchain_selector: WeakEntity, - candidates: Arc<[(Toolchain, Option)]>, - matches: Vec, - selected_index: usize, - workspace: WeakEntity, - worktree_id: WorktreeId, - worktree_abs_path_root: Arc, - relative_path: Arc, - placeholder_text: Arc, - add_toolchain_text: Arc, - project: Entity, - focus_handle: FocusHandle, - _fetch_candidates_task: Task>, -} - -impl ToolchainSelectorDelegate { - fn new( - active_toolchain: Option, - toolchain_selector: WeakEntity, - workspace: WeakEntity, - worktree_id: WorktreeId, - worktree_abs_path_root: Arc, - project: Entity, - relative_path: Arc, - language_name: LanguageName, - window: &mut Window, - cx: &mut Context>, - ) -> Self { - let _project = project.clone(); - let path_style = project.read(cx).path_style(cx); - - let _fetch_candidates_task = cx.spawn_in(window, { - async move |this, cx| { - let meta = _project - .read_with(cx, |this, _| { - Project::toolchain_metadata(this.languages().clone(), language_name.clone()) - }) - .ok()? - .await?; - let relative_path = this - .update(cx, |this, cx| { - this.delegate.add_toolchain_text = format!( - "Add {}", - meta.term.as_ref().to_case(convert_case::Case::Title) - ) - .into(); - cx.notify(); - this.delegate.relative_path.clone() - }) - .ok()?; - - let Toolchains { - toolchains: available_toolchains, - root_path: relative_path, - user_toolchains, - } = _project - .update(cx, |this, cx| { - this.available_toolchains( - ProjectPath { - worktree_id, - path: relative_path.clone(), - }, - language_name, - cx, - ) - }) - .ok()? - .await?; - let pretty_path = { - if relative_path.is_empty() { - Cow::Borrowed("worktree root") - } else { - Cow::Owned(format!("`{}`", relative_path.display(path_style))) - } - }; - let placeholder_text = - format!("Select a {} for {pretty_path}…", meta.term.to_lowercase(),).into(); - let _ = this.update_in(cx, move |this, window, cx| { - this.delegate.relative_path = relative_path; - this.delegate.placeholder_text = placeholder_text; - this.refresh_placeholder(window, cx); - }); - - let _ = this.update_in(cx, move |this, window, cx| { - this.delegate.candidates = user_toolchains - .into_iter() - .flat_map(|(scope, toolchains)| { - toolchains - .into_iter() - .map(move |toolchain| (toolchain, Some(scope.clone()))) - }) - .chain( - available_toolchains - .toolchains - .into_iter() - .map(|toolchain| (toolchain, None)), - ) - .collect(); - - if let Some(active_toolchain) = active_toolchain - && let Some(position) = this - .delegate - .candidates - .iter() - .position(|(toolchain, _)| *toolchain == active_toolchain) - { - this.delegate.set_selected_index(position, window, cx); - } - this.update_matches(this.query(cx), window, cx); - }); - - Some(()) - } - }); - let placeholder_text = "Select a toolchain…".to_string().into(); - Self { - toolchain_selector, - candidates: Default::default(), - matches: vec![], - selected_index: 0, - workspace, - worktree_id, - worktree_abs_path_root, - placeholder_text, - relative_path, - _fetch_candidates_task, - project, - focus_handle: cx.focus_handle(), - add_toolchain_text: Arc::from("Add Toolchain"), - } - } - fn relativize_path( - path: SharedString, - worktree_root: &Path, - path_style: PathStyle, - ) -> SharedString { - Path::new(&path.as_ref()) - .strip_prefix(&worktree_root) - .ok() - .and_then(|suffix| suffix.to_str()) - .map(|suffix| format!(".{}{suffix}", path_style.primary_separator()).into()) - .unwrap_or(path) - } -} - -impl PickerDelegate for ToolchainSelectorDelegate { - type ListItem = ListItem; - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - self.placeholder_text.clone() - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { - if let Some(string_match) = self.matches.get(self.selected_index) { - let (toolchain, _) = self.candidates[string_match.candidate_id].clone(); - if let Some(workspace_id) = self - .workspace - .read_with(cx, |this, _| this.database_id()) - .ok() - .flatten() - { - let workspace = self.workspace.clone(); - let worktree_id = self.worktree_id; - let path = self.relative_path.clone(); - let relative_path = self.relative_path.clone(); - cx.spawn_in(window, async move |_, cx| { - workspace::WORKSPACE_DB - .set_toolchain(workspace_id, worktree_id, relative_path, toolchain.clone()) - .await - .log_err(); - workspace - .update(cx, |this, cx| { - this.project().update(cx, |this, cx| { - this.activate_toolchain( - ProjectPath { worktree_id, path }, - toolchain, - cx, - ) - }) - }) - .ok()? - .await; - Some(()) - }) - .detach(); - } - } - self.dismissed(window, cx); - } - - fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { - self.toolchain_selector - .update(cx, |_, cx| cx.emit(DismissEvent)) - .log_err(); - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _window: &mut Window, - _: &mut Context>, - ) { - self.selected_index = ix; - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> gpui::Task<()> { - let background = cx.background_executor().clone(); - let candidates = self.candidates.clone(); - let worktree_root_path = self.worktree_abs_path_root.clone(); - let path_style = self.project.read(cx).path_style(cx); - cx.spawn_in(window, async move |this, cx| { - let matches = if query.is_empty() { - candidates - .into_iter() - .enumerate() - .map(|(index, (candidate, _))| { - let path = Self::relativize_path( - candidate.path.clone(), - &worktree_root_path, - path_style, - ); - let string = format!("{}{}", candidate.name, path); - StringMatch { - candidate_id: index, - string, - positions: Vec::new(), - score: 0.0, - } - }) - .collect() - } else { - let candidates = candidates - .into_iter() - .enumerate() - .map(|(candidate_id, (toolchain, _))| { - let path = Self::relativize_path( - toolchain.path.clone(), - &worktree_root_path, - path_style, - ); - let string = format!("{}{}", toolchain.name, path); - StringMatchCandidate::new(candidate_id, &string) - }) - .collect::>(); - match_strings( - &candidates, - &query, - false, - true, - 100, - &Default::default(), - background, - ) - .await - }; - - this.update(cx, |this, cx| { - let delegate = &mut this.delegate; - delegate.matches = matches; - delegate.selected_index = delegate - .selected_index - .min(delegate.matches.len().saturating_sub(1)); - cx.notify(); - }) - .log_err(); - }) - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - let mat = &self.matches.get(ix)?; - let (toolchain, scope) = &self.candidates.get(mat.candidate_id)?; - - let label = toolchain.name.clone(); - let path_style = self.project.read(cx).path_style(cx); - let path = Self::relativize_path( - toolchain.path.clone(), - &self.worktree_abs_path_root, - path_style, - ); - let (name_highlights, mut path_highlights) = mat - .positions - .iter() - .cloned() - .partition::, _>(|index| *index < label.len()); - path_highlights.iter_mut().for_each(|index| { - *index -= label.len(); - }); - let id: SharedString = format!("toolchain-{ix}",).into(); - Some( - ListItem::new(id) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child(HighlightedLabel::new(label, name_highlights)) - .child( - HighlightedLabel::new(path, path_highlights) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .when_some(scope.as_ref(), |this, scope| { - let id: SharedString = format!( - "delete-custom-toolchain-{}-{}", - toolchain.name, toolchain.path - ) - .into(); - let toolchain = toolchain.clone(); - let scope = scope.clone(); - - this.end_slot(IconButton::new(id, IconName::Trash).on_click(cx.listener( - move |this, _, _, cx| { - this.delegate.project.update(cx, |this, cx| { - this.remove_toolchain(toolchain.clone(), scope.clone(), cx) - }); - - this.delegate.matches.retain_mut(|m| { - if m.candidate_id == ix { - return false; - } else if m.candidate_id > ix { - m.candidate_id -= 1; - } - true - }); - - this.delegate.candidates = this - .delegate - .candidates - .iter() - .enumerate() - .filter_map(|(i, toolchain)| (ix != i).then_some(toolchain.clone())) - .collect(); - - if this.delegate.selected_index >= ix { - this.delegate.selected_index = - this.delegate.selected_index.saturating_sub(1); - } - cx.stop_propagation(); - cx.notify(); - }, - ))) - }), - ) - } - fn render_footer( - &self, - _window: &mut Window, - cx: &mut Context>, - ) -> Option { - Some( - v_flex() - .rounded_b_md() - .child(Divider::horizontal()) - .child( - h_flex() - .p_1p5() - .gap_0p5() - .justify_end() - .child( - Button::new("xd", self.add_toolchain_text.clone()) - .key_binding(KeyBinding::for_action_in( - &AddToolchain, - &self.focus_handle, - cx, - )) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(AddToolchain), cx) - }), - ) - .child( - Button::new("select", "Select") - .key_binding(KeyBinding::for_action_in( - &menu::Confirm, - &self.focus_handle, - cx, - )) - .on_click(|_, window, cx| { - window.dispatch_action(menu::Confirm.boxed_clone(), cx) - }), - ), - ) - .into_any_element(), - ) - } -} diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml deleted file mode 100644 index 5eb58bf1da..0000000000 --- a/crates/ui/Cargo.toml +++ /dev/null @@ -1,42 +0,0 @@ -[package] -name = "ui" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -name = "ui" -path = "src/ui.rs" - -[dependencies] -chrono.workspace = true -component.workspace = true -documented.workspace = true -gpui.workspace = true -gpui_macros.workspace = true -icons.workspace = true -itertools.workspace = true -menu.workspace = true -schemars.workspace = true -serde.workspace = true -settings.workspace = true -smallvec.workspace = true -story = { workspace = true, optional = true } -strum.workspace = true -theme.workspace = true -ui_macros.workspace = true -util.workspace = true - -[target.'cfg(windows)'.dependencies] -windows.workspace = true - -[dev-dependencies] -gpui = { workspace = true, features = ["test-support"] } - -[features] -default = [] -stories = ["dep:story"] diff --git a/crates/ui/LICENSE-GPL b/crates/ui/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/ui/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/ui/src/component_prelude.rs b/crates/ui/src/component_prelude.rs deleted file mode 100644 index 0a01372970..0000000000 --- a/crates/ui/src/component_prelude.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub use component::{ - Component, ComponentId, ComponentScope, ComponentStatus, example_group, - example_group_with_title, single_example, -}; -pub use documented::Documented; -pub use ui_macros::RegisterComponent; diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs deleted file mode 100644 index b6318f18c9..0000000000 --- a/crates/ui/src/components.rs +++ /dev/null @@ -1,89 +0,0 @@ -mod avatar; -mod banner; -mod button; -mod callout; -mod chip; -mod content_group; -mod context_menu; -mod data_table; -mod diff_stat; -mod disclosure; -mod divider; -mod dropdown_menu; -mod facepile; -mod group; -mod icon; -mod image; -mod indent_guides; -mod indicator; -mod keybinding; -mod keybinding_hint; -mod label; -mod list; -mod modal; -mod navigable; -mod notification; -mod popover; -mod popover_menu; -mod progress; -mod radio; -mod right_click_menu; -mod scrollbar; -mod settings_container; -mod settings_group; -mod stack; -mod sticky_items; -mod tab; -mod tab_bar; -mod thread_item; -mod toggle; -mod tooltip; -mod tree_view_item; - -#[cfg(feature = "stories")] -mod stories; - -pub use avatar::*; -pub use banner::*; -pub use button::*; -pub use callout::*; -pub use chip::*; -pub use content_group::*; -pub use context_menu::*; -pub use data_table::*; -pub use diff_stat::*; -pub use disclosure::*; -pub use divider::*; -pub use dropdown_menu::*; -pub use facepile::*; -pub use group::*; -pub use icon::*; -pub use image::*; -pub use indent_guides::*; -pub use indicator::*; -pub use keybinding::*; -pub use keybinding_hint::*; -pub use label::*; -pub use list::*; -pub use modal::*; -pub use navigable::*; -pub use notification::*; -pub use popover::*; -pub use popover_menu::*; -pub use progress::*; -pub use radio::*; -pub use right_click_menu::*; -pub use scrollbar::*; -pub use settings_container::*; -pub use settings_group::*; -pub use stack::*; -pub use sticky_items::*; -pub use tab::*; -pub use tab_bar::*; -pub use thread_item::*; -pub use toggle::*; -pub use tooltip::*; -pub use tree_view_item::*; - -#[cfg(feature = "stories")] -pub use stories::*; diff --git a/crates/ui/src/components/avatar.rs b/crates/ui/src/components/avatar.rs deleted file mode 100644 index 7b2ba8ce5c..0000000000 --- a/crates/ui/src/components/avatar.rs +++ /dev/null @@ -1,303 +0,0 @@ -use crate::prelude::*; - -use documented::Documented; -use gpui::{AnyElement, Hsla, ImageSource, Img, IntoElement, Styled, img}; - -/// An element that renders a user avatar with customizable appearance options. -/// -/// # Examples -/// -/// ``` -/// use ui::Avatar; -/// -/// Avatar::new("path/to/image.png") -/// .grayscale(true) -/// .border_color(gpui::red()); -/// ``` -#[derive(IntoElement, Documented, RegisterComponent)] -pub struct Avatar { - image: Img, - size: Option, - border_color: Option, - indicator: Option, -} - -impl Avatar { - /// Creates a new avatar element with the specified image source. - pub fn new(src: impl Into) -> Self { - Avatar { - image: img(src), - size: None, - border_color: None, - indicator: None, - } - } - - /// Applies a grayscale filter to the avatar image. - /// - /// # Examples - /// - /// ``` - /// use ui::Avatar; - /// - /// let avatar = Avatar::new("path/to/image.png").grayscale(true); - /// ``` - pub fn grayscale(mut self, grayscale: bool) -> Self { - self.image = self.image.grayscale(grayscale); - self - } - - /// Sets the border color of the avatar. - /// - /// This might be used to match the border to the background color of - /// the parent element to create the illusion of cropping another - /// shape underneath (for example in face piles.) - pub fn border_color(mut self, color: impl Into) -> Self { - self.border_color = Some(color.into()); - self - } - - /// Size overrides the avatar size. By default they are 1rem. - pub fn size>(mut self, size: impl Into>) -> Self { - self.size = size.into().map(Into::into); - self - } - - /// Sets the current indicator to be displayed on the avatar, if any. - pub fn indicator(mut self, indicator: impl Into>) -> Self { - self.indicator = indicator.into().map(IntoElement::into_any_element); - self - } -} - -impl RenderOnce for Avatar { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let border_width = if self.border_color.is_some() { - px(2.) - } else { - px(0.) - }; - - let image_size = self.size.unwrap_or_else(|| rems(1.).into()); - let container_size = image_size.to_pixels(window.rem_size()) + border_width * 2.; - - div() - .size(container_size) - .rounded_full() - .when_some(self.border_color, |this, color| { - this.border(border_width).border_color(color) - }) - .child( - self.image - .size(image_size) - .rounded_full() - .bg(cx.theme().colors().element_disabled) - .with_fallback(|| { - h_flex() - .size_full() - .justify_center() - .child( - Icon::new(IconName::Person) - .color(Color::Muted) - .size(IconSize::Small), - ) - .into_any_element() - }), - ) - .children(self.indicator.map(|indicator| div().child(indicator))) - } -} - -use gpui::AnyView; - -/// The audio status of an player, for use in representing -/// their status visually on their avatar. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum AudioStatus { - /// The player's microphone is muted. - Muted, - /// The player's microphone is muted, and collaboration audio is disabled. - Deafened, -} - -/// An indicator that shows the audio status of a player. -#[derive(IntoElement)] -pub struct AvatarAudioStatusIndicator { - audio_status: AudioStatus, - tooltip: Option AnyView>>, -} - -impl AvatarAudioStatusIndicator { - /// Creates a new `AvatarAudioStatusIndicator` - pub fn new(audio_status: AudioStatus) -> Self { - Self { - audio_status, - tooltip: None, - } - } - - /// Sets the tooltip for the indicator. - pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.tooltip = Some(Box::new(tooltip)); - self - } -} - -impl RenderOnce for AvatarAudioStatusIndicator { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let icon_size = IconSize::Indicator; - - let width_in_px = icon_size.rems() * window.rem_size(); - let padding_x = px(4.); - - div() - .absolute() - .bottom(rems_from_px(-3.)) - .right(rems_from_px(-6.)) - .w(width_in_px + padding_x) - .h(icon_size.rems()) - .child( - h_flex() - .id("muted-indicator") - .justify_center() - .px(padding_x) - .py(px(2.)) - .bg(cx.theme().status().error_background) - .rounded_sm() - .child( - Icon::new(match self.audio_status { - AudioStatus::Muted => IconName::MicMute, - AudioStatus::Deafened => IconName::AudioOff, - }) - .size(icon_size) - .color(Color::Error), - ) - .when_some(self.tooltip, |this, tooltip| { - this.tooltip(move |window, cx| tooltip(window, cx)) - }), - ) - } -} - -/// Represents the availability status of a collaborator. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum CollaboratorAvailability { - Free, - Busy, -} - -/// Represents the availability and presence status of a collaborator. -#[derive(IntoElement)] -pub struct AvatarAvailabilityIndicator { - availability: CollaboratorAvailability, - avatar_size: Option, -} - -impl AvatarAvailabilityIndicator { - /// Creates a new indicator - pub fn new(availability: CollaboratorAvailability) -> Self { - Self { - availability, - avatar_size: None, - } - } - - /// Sets the size of the [`Avatar`](crate::Avatar) this indicator appears on. - pub fn avatar_size(mut self, size: impl Into>) -> Self { - self.avatar_size = size.into(); - self - } -} - -impl RenderOnce for AvatarAvailabilityIndicator { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let avatar_size = self.avatar_size.unwrap_or_else(|| window.rem_size()); - - // HACK: non-integer sizes result in oval indicators. - let indicator_size = (avatar_size * 0.4).round(); - - div() - .absolute() - .bottom_0() - .right_0() - .size(indicator_size) - .rounded(indicator_size) - .bg(match self.availability { - CollaboratorAvailability::Free => cx.theme().status().created, - CollaboratorAvailability::Busy => cx.theme().status().deleted, - }) - } -} - -// View this component preview using `workspace: open component-preview` -impl Component for Avatar { - fn scope() -> ComponentScope { - ComponentScope::Collaboration - } - - fn description() -> Option<&'static str> { - Some(Avatar::DOCS) - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - let example_avatar = "https://avatars.githubusercontent.com/u/1714999?v=4"; - - Some( - v_flex() - .gap_6() - .children(vec![ - example_group(vec![ - single_example("Default", Avatar::new(example_avatar).into_any_element()), - single_example( - "Grayscale", - Avatar::new(example_avatar) - .grayscale(true) - .into_any_element(), - ), - single_example( - "Border", - Avatar::new(example_avatar) - .border_color(cx.theme().colors().border) - .into_any_element(), - ).description("Can be used to create visual space by setting the border color to match the background, which creates the appearance of a gap around the avatar."), - ]), - example_group_with_title( - "Indicator Styles", - vec![ - single_example( - "Muted", - Avatar::new(example_avatar) - .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted)) - .into_any_element(), - ).description("Indicates the collaborator's mic is muted."), - single_example( - "Deafened", - Avatar::new(example_avatar) - .indicator(AvatarAudioStatusIndicator::new( - AudioStatus::Deafened, - )) - .into_any_element(), - ).description("Indicates that both the collaborator's mic and audio are muted."), - single_example( - "Availability: Free", - Avatar::new(example_avatar) - .indicator(AvatarAvailabilityIndicator::new( - CollaboratorAvailability::Free, - )) - .into_any_element(), - ).description("Indicates that the person is free, usually meaning they are not in a call."), - single_example( - "Availability: Busy", - Avatar::new(example_avatar) - .indicator(AvatarAvailabilityIndicator::new( - CollaboratorAvailability::Busy, - )) - .into_any_element(), - ).description("Indicates that the person is busy, usually meaning they are in a channel or direct call."), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/banner.rs b/crates/ui/src/components/banner.rs deleted file mode 100644 index a09170f5b2..0000000000 --- a/crates/ui/src/components/banner.rs +++ /dev/null @@ -1,182 +0,0 @@ -use crate::prelude::*; -use gpui::{AnyElement, IntoElement, ParentElement, Styled}; - -/// Banners provide informative and brief messages without interrupting the user. -/// This component offers four severity levels that can be used depending on the message. -/// -/// # Usage Example -/// -/// ``` -/// use ui::prelude::*; -/// use ui::{Banner, Button, IconName, IconPosition, IconSize, Label, Severity}; -/// -/// Banner::new() -/// .severity(Severity::Success) -/// .children([Label::new("This is a success message")]) -/// .action_slot( -/// Button::new("learn-more", "Learn More") -/// .icon(IconName::ArrowUpRight) -/// .icon_size(IconSize::Small) -/// .icon_position(IconPosition::End) -/// ); -/// ``` -#[derive(IntoElement, RegisterComponent)] -pub struct Banner { - severity: Severity, - children: Vec, - action_slot: Option, -} - -impl Banner { - /// Creates a new `Banner` component with default styling. - pub fn new() -> Self { - Self { - severity: Severity::Info, - children: Vec::new(), - action_slot: None, - } - } - - /// Sets the severity of the banner. - pub fn severity(mut self, severity: Severity) -> Self { - self.severity = severity; - self - } - - /// A slot for actions, such as CTA or dismissal buttons. - pub fn action_slot(mut self, element: impl IntoElement) -> Self { - self.action_slot = Some(element.into_any_element()); - self - } -} - -impl ParentElement for Banner { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for Banner { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let banner = h_flex() - .py_0p5() - .gap_1p5() - .flex_wrap() - .justify_between() - .rounded_sm() - .border_1(); - - let (icon, icon_color, bg_color, border_color) = match self.severity { - Severity::Info => ( - IconName::Info, - Color::Muted, - cx.theme().status().info_background.opacity(0.5), - cx.theme().colors().border.opacity(0.5), - ), - Severity::Success => ( - IconName::Check, - Color::Success, - cx.theme().status().success.opacity(0.1), - cx.theme().status().success.opacity(0.2), - ), - Severity::Warning => ( - IconName::Warning, - Color::Warning, - cx.theme().status().warning_background.opacity(0.5), - cx.theme().status().warning_border.opacity(0.4), - ), - Severity::Error => ( - IconName::XCircle, - Color::Error, - cx.theme().status().error.opacity(0.1), - cx.theme().status().error.opacity(0.2), - ), - }; - - let mut banner = banner.bg(bg_color).border_color(border_color); - - let icon_and_child = h_flex() - .items_start() - .min_w_0() - .gap_1p5() - .child( - h_flex() - .h(window.line_height()) - .flex_shrink_0() - .child(Icon::new(icon).size(IconSize::XSmall).color(icon_color)), - ) - .child(div().min_w_0().children(self.children)); - - if let Some(action_slot) = self.action_slot { - banner = banner - .pl_2() - .pr_1() - .child(icon_and_child) - .child(action_slot); - } else { - banner = banner.px_2().child(icon_and_child); - } - - banner - } -} - -impl Component for Banner { - fn scope() -> ComponentScope { - ComponentScope::DataDisplay - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - let severity_examples = vec![ - single_example( - "Default", - Banner::new() - .child(Label::new("This is a default banner with no customization")) - .into_any_element(), - ), - single_example( - "Info", - Banner::new() - .severity(Severity::Info) - .child(Label::new("This is an informational message")) - .action_slot( - Button::new("learn-more", "Learn More") - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) - .icon_position(IconPosition::End), - ) - .into_any_element(), - ), - single_example( - "Success", - Banner::new() - .severity(Severity::Success) - .child(Label::new("Operation completed successfully")) - .action_slot(Button::new("dismiss", "Dismiss")) - .into_any_element(), - ), - single_example( - "Warning", - Banner::new() - .severity(Severity::Warning) - .child(Label::new("Your settings file uses deprecated settings")) - .action_slot(Button::new("update", "Update Settings")) - .into_any_element(), - ), - single_example( - "Error", - Banner::new() - .severity(Severity::Error) - .child(Label::new("Connection error: unable to connect to server")) - .action_slot(Button::new("reconnect", "Retry")) - .into_any_element(), - ), - ]; - - Some( - example_group(severity_examples) - .vertical() - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/button.rs b/crates/ui/src/components/button.rs deleted file mode 100644 index 23e7702f62..0000000000 --- a/crates/ui/src/components/button.rs +++ /dev/null @@ -1,12 +0,0 @@ -mod button; -mod button_icon; -mod button_like; -mod icon_button; -mod split_button; -mod toggle_button; - -pub use button::*; -pub use button_like::*; -pub use icon_button::*; -pub use split_button::*; -pub use toggle_button::*; diff --git a/crates/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs deleted file mode 100644 index 83b50b6341..0000000000 --- a/crates/ui/src/components/button/button.rs +++ /dev/null @@ -1,612 +0,0 @@ -use crate::component_prelude::*; -use gpui::{AnyElement, AnyView, DefiniteLength}; -use ui_macros::RegisterComponent; - -use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize, Label}; -use crate::{ - Color, DynamicSpacing, ElevationIndex, IconPosition, KeyBinding, KeybindingPosition, TintColor, - prelude::*, -}; - -use super::button_icon::ButtonIcon; - -/// An element that creates a button with a label and an optional icon. -/// -/// Common buttons: -/// - Label, Icon + Label: [`Button`] (this component) -/// - Icon only: [`IconButton`] -/// - Custom: [`ButtonLike`] -/// -/// To create a more complex button than what the [`Button`] or [`IconButton`] components provide, use -/// [`ButtonLike`] directly. -/// -/// # Examples -/// -/// **A button with a label**, is typically used in scenarios such as a form, where the button's label -/// indicates what action will be performed when the button is clicked. -/// -/// ``` -/// use ui::prelude::*; -/// -/// Button::new("button_id", "Click me!") -/// .on_click(|event, window, cx| { -/// // Handle click event -/// }); -/// ``` -/// -/// **A toggleable button**, is typically used in scenarios such as a toolbar, -/// where the button's state indicates whether a feature is enabled or not, or -/// a trigger for a popover menu, where clicking the button toggles the visibility of the menu. -/// -/// ``` -/// use ui::prelude::*; -/// -/// Button::new("button_id", "Click me!") -/// .icon(IconName::Check) -/// .toggle_state(true) -/// .on_click(|event, window, cx| { -/// // Handle click event -/// }); -/// ``` -/// -/// To change the style of the button when it is selected use the [`selected_style`][Button::selected_style] method. -/// -/// ``` -/// use ui::prelude::*; -/// use ui::TintColor; -/// -/// Button::new("button_id", "Click me!") -/// .toggle_state(true) -/// .selected_style(ButtonStyle::Tinted(TintColor::Accent)) -/// .on_click(|event, window, cx| { -/// // Handle click event -/// }); -/// ``` -/// This will create a button with a blue tinted background when selected. -/// -/// **A full-width button**, is typically used in scenarios such as the bottom of a modal or form, where it occupies the entire width of its container. -/// The button's content, including text and icons, is centered by default. -/// -/// ``` -/// use ui::prelude::*; -/// -/// let button = Button::new("button_id", "Click me!") -/// .full_width() -/// .on_click(|event, window, cx| { -/// // Handle click event -/// }); -/// ``` -/// -#[derive(IntoElement, Documented, RegisterComponent)] -pub struct Button { - base: ButtonLike, - label: SharedString, - label_color: Option, - label_size: Option, - selected_label: Option, - selected_label_color: Option, - icon: Option, - icon_position: Option, - icon_size: Option, - icon_color: Option, - selected_icon: Option, - selected_icon_color: Option, - key_binding: Option, - key_binding_position: KeybindingPosition, - alpha: Option, - truncate: bool, -} - -impl Button { - /// Creates a new [`Button`] with a specified identifier and label. - /// - /// This is the primary constructor for a [`Button`] component. It initializes - /// the button with the provided identifier and label text, setting all other - /// properties to their default values, which can be customized using the - /// builder pattern methods provided by this struct. - pub fn new(id: impl Into, label: impl Into) -> Self { - Self { - base: ButtonLike::new(id), - label: label.into(), - label_color: None, - label_size: None, - selected_label: None, - selected_label_color: None, - icon: None, - icon_position: None, - icon_size: None, - icon_color: None, - selected_icon: None, - selected_icon_color: None, - key_binding: None, - key_binding_position: KeybindingPosition::default(), - alpha: None, - truncate: false, - } - } - - /// Sets the color of the button's label. - pub fn color(mut self, label_color: impl Into>) -> Self { - self.label_color = label_color.into(); - self - } - - /// Defines the size of the button's label. - pub fn label_size(mut self, label_size: impl Into>) -> Self { - self.label_size = label_size.into(); - self - } - - /// Sets the label used when the button is in a selected state. - pub fn selected_label>(mut self, label: impl Into>) -> Self { - self.selected_label = label.into().map(Into::into); - self - } - - /// Sets the label color used when the button is in a selected state. - pub fn selected_label_color(mut self, color: impl Into>) -> Self { - self.selected_label_color = color.into(); - self - } - - /// Assigns an icon to the button. - pub fn icon(mut self, icon: impl Into>) -> Self { - self.icon = icon.into(); - self - } - - /// Sets the position of the icon relative to the label. - pub fn icon_position(mut self, icon_position: impl Into>) -> Self { - self.icon_position = icon_position.into(); - self - } - - /// Specifies the size of the button's icon. - pub fn icon_size(mut self, icon_size: impl Into>) -> Self { - self.icon_size = icon_size.into(); - self - } - - /// Sets the color of the button's icon. - pub fn icon_color(mut self, icon_color: impl Into>) -> Self { - self.icon_color = icon_color.into(); - self - } - - /// Chooses an icon to display when the button is in a selected state. - pub fn selected_icon(mut self, icon: impl Into>) -> Self { - self.selected_icon = icon.into(); - self - } - - /// Sets the icon color used when the button is in a selected state. - pub fn selected_icon_color(mut self, color: impl Into>) -> Self { - self.selected_icon_color = color.into(); - self - } - - /// Display the keybinding that triggers the button action. - pub fn key_binding(mut self, key_binding: impl Into>) -> Self { - self.key_binding = key_binding.into(); - self - } - - /// Sets the position of the keybinding relative to the button label. - /// - /// This method allows you to specify where the keybinding should be displayed - /// in relation to the button's label. - pub fn key_binding_position(mut self, position: KeybindingPosition) -> Self { - self.key_binding_position = position; - self - } - - /// Sets the alpha property of the color of label. - pub fn alpha(mut self, alpha: f32) -> Self { - self.alpha = Some(alpha); - self - } - - /// Truncates overflowing labels with an ellipsis (`…`) if needed. - /// - /// Buttons with static labels should _never_ be truncated, ensure - /// this is only used when the label is dynamic and may overflow. - pub fn truncate(mut self, truncate: bool) -> Self { - self.truncate = truncate; - self - } -} - -impl Toggleable for Button { - /// Sets the selected state of the button. - /// - /// This method allows the selection state of the button to be specified. - /// It modifies the button's appearance to reflect its selected state. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// Button::new("button_id", "Click me!") - /// .toggle_state(true) - /// .on_click(|event, window, cx| { - /// // Handle click event - /// }); - /// ``` - /// - /// Use [`selected_style`](Button::selected_style) to change the style of the button when it is selected. - fn toggle_state(mut self, selected: bool) -> Self { - self.base = self.base.toggle_state(selected); - self - } -} - -impl SelectableButton for Button { - /// Sets the style for the button when selected. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// use ui::TintColor; - /// - /// Button::new("button_id", "Click me!") - /// .toggle_state(true) - /// .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - /// .on_click(|event, window, cx| { - /// // Handle click event - /// }); - /// ``` - /// This results in a button with a blue tinted background when selected. - fn selected_style(mut self, style: ButtonStyle) -> Self { - self.base = self.base.selected_style(style); - self - } -} - -impl Disableable for Button { - /// Disables the button. - /// - /// This method allows the button to be disabled. When a button is disabled, - /// it doesn't react to user interactions and its appearance is updated to reflect this. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// Button::new("button_id", "Click me!") - /// .disabled(true) - /// .on_click(|event, window, cx| { - /// // Handle click event - /// }); - /// ``` - /// - /// This results in a button that is disabled and does not respond to click events. - fn disabled(mut self, disabled: bool) -> Self { - self.base = self.base.disabled(disabled); - self.key_binding = self - .key_binding - .take() - .map(|binding| binding.disabled(disabled)); - self - } -} - -impl Clickable for Button { - /// Sets the click event handler for the button. - fn on_click( - mut self, - handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.base = self.base.on_click(handler); - self - } - - fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self { - self.base = self.base.cursor_style(cursor_style); - self - } -} - -impl FixedWidth for Button { - /// Sets a fixed width for the button. - /// - /// This function allows a button to have a fixed width instead of automatically growing or shrinking. - /// Sets a fixed width for the button. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// Button::new("button_id", "Click me!") - /// .width(px(100.)) - /// .on_click(|event, window, cx| { - /// // Handle click event - /// }); - /// ``` - /// - /// This sets the button's width to be exactly 100 pixels. - fn width(mut self, width: impl Into) -> Self { - self.base = self.base.width(width); - self - } - - /// Sets the button to occupy the full width of its container. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// Button::new("button_id", "Click me!") - /// .full_width() - /// .on_click(|event, window, cx| { - /// // Handle click event - /// }); - /// ``` - /// - /// This stretches the button to the full width of its container. - fn full_width(mut self) -> Self { - self.base = self.base.full_width(); - self - } -} - -impl ButtonCommon for Button { - /// Sets the button's id. - fn id(&self) -> &ElementId { - self.base.id() - } - - /// Sets the visual style of the button using a [`ButtonStyle`]. - fn style(mut self, style: ButtonStyle) -> Self { - self.base = self.base.style(style); - self - } - - /// Sets the button's size using a [`ButtonSize`]. - fn size(mut self, size: ButtonSize) -> Self { - self.base = self.base.size(size); - self - } - - /// Sets a tooltip for the button. - /// - /// This method allows a tooltip to be set for the button. The tooltip is a function that - /// takes a mutable references to [`Window`] and [`App`], and returns an [`AnyView`]. The - /// tooltip is displayed when the user hovers over the button. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// use ui::Tooltip; - /// - /// Button::new("button_id", "Click me!") - /// .tooltip(Tooltip::text("This is a tooltip")) - /// .on_click(|event, window, cx| { - /// // Handle click event - /// }); - /// ``` - /// - /// This will create a button with a tooltip that displays "This is a tooltip" when hovered over. - fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.base = self.base.tooltip(tooltip); - self - } - - fn tab_index(mut self, tab_index: impl Into) -> Self { - self.base = self.base.tab_index(tab_index); - self - } - - fn layer(mut self, elevation: ElevationIndex) -> Self { - self.base = self.base.layer(elevation); - self - } - - fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self { - self.base = self.base.track_focus(focus_handle); - self - } -} - -impl RenderOnce for Button { - #[allow(refining_impl_trait)] - fn render(self, _window: &mut Window, cx: &mut App) -> ButtonLike { - let is_disabled = self.base.disabled; - let is_selected = self.base.selected; - - let label = self - .selected_label - .filter(|_| is_selected) - .unwrap_or(self.label); - - let label_color = if is_disabled { - Color::Disabled - } else if is_selected { - self.selected_label_color.unwrap_or(Color::Selected) - } else { - self.label_color.unwrap_or_default() - }; - - self.base.child( - h_flex() - .gap(DynamicSpacing::Base04.rems(cx)) - .when(self.icon_position == Some(IconPosition::Start), |this| { - this.children(self.icon.map(|icon| { - ButtonIcon::new(icon) - .disabled(is_disabled) - .toggle_state(is_selected) - .selected_icon(self.selected_icon) - .selected_icon_color(self.selected_icon_color) - .size(self.icon_size) - .color(self.icon_color) - })) - }) - .child( - h_flex() - .when( - self.key_binding_position == KeybindingPosition::Start, - |this| this.flex_row_reverse(), - ) - .gap(DynamicSpacing::Base06.rems(cx)) - .justify_between() - .child( - Label::new(label) - .color(label_color) - .size(self.label_size.unwrap_or_default()) - .when_some(self.alpha, |this, alpha| this.alpha(alpha)) - .when(self.truncate, |this| this.truncate()), - ) - .children(self.key_binding), - ) - .when(self.icon_position != Some(IconPosition::Start), |this| { - this.children(self.icon.map(|icon| { - ButtonIcon::new(icon) - .disabled(is_disabled) - .toggle_state(is_selected) - .selected_icon(self.selected_icon) - .selected_icon_color(self.selected_icon_color) - .size(self.icon_size) - .color(self.icon_color) - })) - }), - ) - } -} - -impl Component for Button { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn sort_name() -> &'static str { - "ButtonA" - } - - fn description() -> Option<&'static str> { - Some("A button triggers an event or action.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Button Styles", - vec![ - single_example( - "Default", - Button::new("default", "Default").into_any_element(), - ), - single_example( - "Filled", - Button::new("filled", "Filled") - .style(ButtonStyle::Filled) - .into_any_element(), - ), - single_example( - "Subtle", - Button::new("outline", "Subtle") - .style(ButtonStyle::Subtle) - .into_any_element(), - ), - single_example( - "Tinted", - Button::new("tinted_accent_style", "Accent") - .style(ButtonStyle::Tinted(TintColor::Accent)) - .into_any_element(), - ), - single_example( - "Transparent", - Button::new("transparent", "Transparent") - .style(ButtonStyle::Transparent) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Tint Styles", - vec![ - single_example( - "Accent", - Button::new("tinted_accent", "Accent") - .style(ButtonStyle::Tinted(TintColor::Accent)) - .into_any_element(), - ), - single_example( - "Error", - Button::new("tinted_negative", "Error") - .style(ButtonStyle::Tinted(TintColor::Error)) - .into_any_element(), - ), - single_example( - "Warning", - Button::new("tinted_warning", "Warning") - .style(ButtonStyle::Tinted(TintColor::Warning)) - .into_any_element(), - ), - single_example( - "Success", - Button::new("tinted_positive", "Success") - .style(ButtonStyle::Tinted(TintColor::Success)) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Special States", - vec![ - single_example( - "Default", - Button::new("default_state", "Default").into_any_element(), - ), - single_example( - "Disabled", - Button::new("disabled", "Disabled") - .disabled(true) - .into_any_element(), - ), - single_example( - "Selected", - Button::new("selected", "Selected") - .toggle_state(true) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Buttons with Icons", - vec![ - single_example( - "Icon Start", - Button::new("icon_start", "Icon Start") - .icon(IconName::Check) - .icon_position(IconPosition::Start) - .into_any_element(), - ), - single_example( - "Icon End", - Button::new("icon_end", "Icon End") - .icon(IconName::Check) - .icon_position(IconPosition::End) - .into_any_element(), - ), - single_example( - "Icon Color", - Button::new("icon_color", "Icon Color") - .icon(IconName::Check) - .icon_color(Color::Accent) - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/button/button_icon.rs b/crates/ui/src/components/button/button_icon.rs deleted file mode 100644 index 510c418714..0000000000 --- a/crates/ui/src/components/button/button_icon.rs +++ /dev/null @@ -1,199 +0,0 @@ -use crate::{Icon, IconName, IconSize, IconWithIndicator, Indicator, prelude::*}; -use gpui::Hsla; - -/// An icon that appears within a button. -/// -/// Can be used as either an icon alongside a label, like in [`Button`](crate::Button), -/// or as a standalone icon, like in [`IconButton`](crate::IconButton). -#[derive(IntoElement, RegisterComponent)] -pub(super) struct ButtonIcon { - icon: IconName, - size: IconSize, - color: Color, - disabled: bool, - selected: bool, - selected_icon: Option, - selected_icon_color: Option, - selected_style: Option, - indicator: Option, - indicator_border_color: Option, -} - -impl ButtonIcon { - pub fn new(icon: IconName) -> Self { - Self { - icon, - size: IconSize::default(), - color: Color::default(), - disabled: false, - selected: false, - selected_icon: None, - selected_icon_color: None, - selected_style: None, - indicator: None, - indicator_border_color: None, - } - } - - pub fn size(mut self, size: impl Into>) -> Self { - if let Some(size) = size.into() { - self.size = size; - } - self - } - - pub fn color(mut self, color: impl Into>) -> Self { - if let Some(color) = color.into() { - self.color = color; - } - self - } - - pub fn selected_icon(mut self, icon: impl Into>) -> Self { - self.selected_icon = icon.into(); - self - } - - pub fn selected_icon_color(mut self, color: impl Into>) -> Self { - self.selected_icon_color = color.into(); - self - } - - pub fn indicator(mut self, indicator: Indicator) -> Self { - self.indicator = Some(indicator); - self - } - - pub fn indicator_border_color(mut self, color: Option) -> Self { - self.indicator_border_color = color; - self - } -} - -impl Disableable for ButtonIcon { - fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } -} - -impl Toggleable for ButtonIcon { - fn toggle_state(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -impl SelectableButton for ButtonIcon { - fn selected_style(mut self, style: ButtonStyle) -> Self { - self.selected_style = Some(style); - self - } -} - -impl RenderOnce for ButtonIcon { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let icon = self - .selected_icon - .filter(|_| self.selected) - .unwrap_or(self.icon); - - let icon_color = if self.disabled { - Color::Disabled - } else if self.selected_style.is_some() && self.selected { - self.selected_style.unwrap().into() - } else if self.selected { - self.selected_icon_color.unwrap_or(Color::Selected) - } else { - self.color - }; - - let icon = Icon::new(icon).size(self.size).color(icon_color); - - match self.indicator { - Some(indicator) => IconWithIndicator::new(icon, Some(indicator)) - .indicator_border_color(self.indicator_border_color) - .into_any_element(), - None => icon.into_any_element(), - } - } -} - -impl Component for ButtonIcon { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn name() -> &'static str { - "ButtonIcon" - } - - fn description() -> Option<&'static str> { - Some("An icon component specifically designed for use within buttons.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Usage", - vec![ - single_example( - "Default", - ButtonIcon::new(IconName::Star).into_any_element(), - ), - single_example( - "Custom Size", - ButtonIcon::new(IconName::Star) - .size(IconSize::Medium) - .into_any_element(), - ), - single_example( - "Custom Color", - ButtonIcon::new(IconName::Star) - .color(Color::Accent) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "States", - vec![ - single_example( - "Selected", - ButtonIcon::new(IconName::Star) - .toggle_state(true) - .into_any_element(), - ), - single_example( - "Disabled", - ButtonIcon::new(IconName::Star) - .disabled(true) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "With Indicator", - vec![ - single_example( - "Default Indicator", - ButtonIcon::new(IconName::Star) - .indicator(Indicator::dot()) - .into_any_element(), - ), - single_example( - "Custom Indicator", - ButtonIcon::new(IconName::Star) - .indicator(Indicator::dot().color(Color::Error)) - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs deleted file mode 100644 index 4ce7aeed0d..0000000000 --- a/crates/ui/src/components/button/button_like.rs +++ /dev/null @@ -1,849 +0,0 @@ -use documented::Documented; -use gpui::{ - AnyElement, AnyView, ClickEvent, CursorStyle, DefiniteLength, FocusHandle, Hsla, MouseButton, - MouseClickEvent, MouseDownEvent, MouseUpEvent, Rems, StyleRefinement, relative, - transparent_black, -}; -use smallvec::SmallVec; - -use crate::{DynamicSpacing, ElevationIndex, prelude::*}; - -/// A trait for buttons that can be Selected. Enables setting the [`ButtonStyle`] of a button when it is selected. -pub trait SelectableButton: Toggleable { - fn selected_style(self, style: ButtonStyle) -> Self; -} - -/// A common set of traits all buttons must implement. -pub trait ButtonCommon: Clickable + Disableable { - /// A unique element ID to identify the button. - fn id(&self) -> &ElementId; - - /// The visual style of the button. - /// - /// Most commonly will be [`ButtonStyle::Subtle`], or [`ButtonStyle::Filled`] - /// for an emphasized button. - fn style(self, style: ButtonStyle) -> Self; - - /// The size of the button. - /// - /// Most buttons will use the default size. - /// - /// [`ButtonSize`] can also be used to help build non-button elements - /// that are consistently sized with buttons. - fn size(self, size: ButtonSize) -> Self; - - /// The tooltip that shows when a user hovers over the button. - /// - /// Nearly all interactable elements should have a tooltip. Some example - /// exceptions might a scroll bar, or a slider. - fn tooltip(self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self; - - fn tab_index(self, tab_index: impl Into) -> Self; - - fn layer(self, elevation: ElevationIndex) -> Self; - - fn track_focus(self, focus_handle: &FocusHandle) -> Self; -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] -pub enum IconPosition { - #[default] - Start, - End, -} - -#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum KeybindingPosition { - Start, - #[default] - End, -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] -pub enum TintColor { - #[default] - Accent, - Error, - Warning, - Success, -} - -impl TintColor { - fn button_like_style(self, cx: &mut App) -> ButtonLikeStyles { - match self { - TintColor::Accent => ButtonLikeStyles { - background: cx.theme().status().info_background, - border_color: cx.theme().status().info_border, - label_color: cx.theme().colors().text, - icon_color: cx.theme().colors().text, - }, - TintColor::Error => ButtonLikeStyles { - background: cx.theme().status().error_background, - border_color: cx.theme().status().error_border, - label_color: cx.theme().colors().text, - icon_color: cx.theme().colors().text, - }, - TintColor::Warning => ButtonLikeStyles { - background: cx.theme().status().warning_background, - border_color: cx.theme().status().warning_border, - label_color: cx.theme().colors().text, - icon_color: cx.theme().colors().text, - }, - TintColor::Success => ButtonLikeStyles { - background: cx.theme().status().success_background, - border_color: cx.theme().status().success_border, - label_color: cx.theme().colors().text, - icon_color: cx.theme().colors().text, - }, - } - } -} - -impl From for Color { - fn from(tint: TintColor) -> Self { - match tint { - TintColor::Accent => Color::Accent, - TintColor::Error => Color::Error, - TintColor::Warning => Color::Warning, - TintColor::Success => Color::Success, - } - } -} - -// Used to go from ButtonStyle -> Color through tint colors. -impl From for Color { - fn from(style: ButtonStyle) -> Self { - match style { - ButtonStyle::Tinted(tint) => tint.into(), - _ => Color::Default, - } - } -} - -/// The visual appearance of a button. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] -pub enum ButtonStyle { - /// A filled button with a solid background color. Provides emphasis versus - /// the more common subtle button. - Filled, - - /// Used to emphasize a button in some way, like a selected state, or a semantic - /// coloring like an error or success button. - Tinted(TintColor), - - /// Usually used as a secondary action that should have more emphasis than - /// a fully transparent button. - Outlined, - - /// A more de-emphasized version of the outlined button. - OutlinedGhost, - - /// The default button style, used for most buttons. Has a transparent background, - /// but has a background color to indicate states like hover and active. - #[default] - Subtle, - - /// Used for buttons that only change foreground color on hover and active states. - /// - /// TODO: Better docs for this. - Transparent, -} - -/// Rounding for a button that may have straight edges. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub(crate) struct ButtonLikeRounding { - /// Top-left corner rounding - pub top_left: bool, - /// Top-right corner rounding - pub top_right: bool, - /// Bottom-right corner rounding - pub bottom_right: bool, - /// Bottom-left corner rounding - pub bottom_left: bool, -} - -impl ButtonLikeRounding { - pub const ALL: Self = Self { - top_left: true, - top_right: true, - bottom_right: true, - bottom_left: true, - }; - pub const LEFT: Self = Self { - top_left: true, - top_right: false, - bottom_right: false, - bottom_left: true, - }; - pub const RIGHT: Self = Self { - top_left: false, - top_right: true, - bottom_right: true, - bottom_left: false, - }; -} - -#[derive(Debug, Clone)] -pub(crate) struct ButtonLikeStyles { - pub background: Hsla, - #[allow(unused)] - pub border_color: Hsla, - #[allow(unused)] - pub label_color: Hsla, - #[allow(unused)] - pub icon_color: Hsla, -} - -fn element_bg_from_elevation(elevation: Option, cx: &mut App) -> Hsla { - match elevation { - Some(ElevationIndex::Background) => cx.theme().colors().element_background, - Some(ElevationIndex::ElevatedSurface) => cx.theme().colors().elevated_surface_background, - Some(ElevationIndex::Surface) => cx.theme().colors().surface_background, - Some(ElevationIndex::ModalSurface) => cx.theme().colors().background, - _ => cx.theme().colors().element_background, - } -} - -impl ButtonStyle { - pub(crate) fn enabled( - self, - elevation: Option, - - cx: &mut App, - ) -> ButtonLikeStyles { - match self { - ButtonStyle::Filled => ButtonLikeStyles { - background: element_bg_from_elevation(elevation, cx), - border_color: transparent_black(), - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Tinted(tint) => tint.button_like_style(cx), - ButtonStyle::Outlined => ButtonLikeStyles { - background: element_bg_from_elevation(elevation, cx), - border_color: cx.theme().colors().border_variant, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::OutlinedGhost => ButtonLikeStyles { - background: transparent_black(), - border_color: cx.theme().colors().border_variant, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Subtle => ButtonLikeStyles { - background: cx.theme().colors().ghost_element_background, - border_color: transparent_black(), - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Transparent => ButtonLikeStyles { - background: transparent_black(), - border_color: transparent_black(), - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - } - } - - pub(crate) fn hovered( - self, - elevation: Option, - - cx: &mut App, - ) -> ButtonLikeStyles { - match self { - ButtonStyle::Filled => { - let mut filled_background = element_bg_from_elevation(elevation, cx); - filled_background.fade_out(0.5); - - ButtonLikeStyles { - background: filled_background, - border_color: transparent_black(), - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - } - } - ButtonStyle::Tinted(tint) => { - let mut styles = tint.button_like_style(cx); - let theme = cx.theme(); - styles.background = theme.darken(styles.background, 0.05, 0.2); - styles - } - ButtonStyle::Outlined => ButtonLikeStyles { - background: cx.theme().colors().ghost_element_hover, - border_color: cx.theme().colors().border, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::OutlinedGhost => ButtonLikeStyles { - background: cx.theme().colors().ghost_element_hover, - border_color: cx.theme().colors().border, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Subtle => ButtonLikeStyles { - background: cx.theme().colors().ghost_element_hover, - border_color: transparent_black(), - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Transparent => ButtonLikeStyles { - background: transparent_black(), - border_color: transparent_black(), - // TODO: These are not great - label_color: Color::Muted.color(cx), - // TODO: These are not great - icon_color: Color::Muted.color(cx), - }, - } - } - - pub(crate) fn active(self, cx: &mut App) -> ButtonLikeStyles { - match self { - ButtonStyle::Filled => ButtonLikeStyles { - background: cx.theme().colors().element_active, - border_color: transparent_black(), - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Tinted(tint) => tint.button_like_style(cx), - ButtonStyle::Subtle => ButtonLikeStyles { - background: cx.theme().colors().ghost_element_active, - border_color: transparent_black(), - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Outlined => ButtonLikeStyles { - background: cx.theme().colors().element_active, - border_color: cx.theme().colors().border_variant, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::OutlinedGhost => ButtonLikeStyles { - background: transparent_black(), - border_color: cx.theme().colors().border_variant, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Transparent => ButtonLikeStyles { - background: transparent_black(), - border_color: transparent_black(), - // TODO: These are not great - label_color: Color::Muted.color(cx), - // TODO: These are not great - icon_color: Color::Muted.color(cx), - }, - } - } - - #[allow(unused)] - pub(crate) fn focused(self, window: &mut Window, cx: &mut App) -> ButtonLikeStyles { - match self { - ButtonStyle::Filled => ButtonLikeStyles { - background: cx.theme().colors().element_background, - border_color: cx.theme().colors().border_focused, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Tinted(tint) => tint.button_like_style(cx), - ButtonStyle::Subtle => ButtonLikeStyles { - background: cx.theme().colors().ghost_element_background, - border_color: cx.theme().colors().border_focused, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Outlined => ButtonLikeStyles { - background: cx.theme().colors().ghost_element_background, - border_color: cx.theme().colors().border, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::OutlinedGhost => ButtonLikeStyles { - background: transparent_black(), - border_color: cx.theme().colors().border, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Transparent => ButtonLikeStyles { - background: transparent_black(), - border_color: cx.theme().colors().border_focused, - label_color: Color::Accent.color(cx), - icon_color: Color::Accent.color(cx), - }, - } - } - - #[allow(unused)] - pub(crate) fn disabled( - self, - elevation: Option, - window: &mut Window, - cx: &mut App, - ) -> ButtonLikeStyles { - match self { - ButtonStyle::Filled => ButtonLikeStyles { - background: cx.theme().colors().element_disabled, - border_color: cx.theme().colors().border_disabled, - label_color: Color::Disabled.color(cx), - icon_color: Color::Disabled.color(cx), - }, - ButtonStyle::Tinted(tint) => tint.button_like_style(cx), - ButtonStyle::Subtle => ButtonLikeStyles { - background: cx.theme().colors().ghost_element_disabled, - border_color: cx.theme().colors().border_disabled, - label_color: Color::Disabled.color(cx), - icon_color: Color::Disabled.color(cx), - }, - ButtonStyle::Outlined => ButtonLikeStyles { - background: cx.theme().colors().element_disabled, - border_color: cx.theme().colors().border_disabled, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::OutlinedGhost => ButtonLikeStyles { - background: transparent_black(), - border_color: cx.theme().colors().border_disabled, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Transparent => ButtonLikeStyles { - background: transparent_black(), - border_color: transparent_black(), - label_color: Color::Disabled.color(cx), - icon_color: Color::Disabled.color(cx), - }, - } - } -} - -/// The height of a button. -/// -/// Can also be used to size non-button elements to align with [`Button`]s. -#[derive(Default, PartialEq, Clone, Copy)] -pub enum ButtonSize { - Large, - Medium, - #[default] - Default, - Compact, - None, -} - -impl ButtonSize { - pub fn rems(self) -> Rems { - match self { - ButtonSize::Large => rems_from_px(32.), - ButtonSize::Medium => rems_from_px(28.), - ButtonSize::Default => rems_from_px(22.), - ButtonSize::Compact => rems_from_px(18.), - ButtonSize::None => rems_from_px(16.), - } - } -} - -/// A button-like element that can be used to create a custom button when -/// prebuilt buttons are not sufficient. Use this sparingly, as it is -/// unconstrained and may make the UI feel less consistent. -/// -/// This is also used to build the prebuilt buttons. -#[derive(IntoElement, Documented, RegisterComponent)] -pub struct ButtonLike { - pub(super) base: Div, - id: ElementId, - pub(super) style: ButtonStyle, - pub(super) disabled: bool, - pub(super) selected: bool, - pub(super) selected_style: Option, - pub(super) width: Option, - pub(super) height: Option, - pub(super) layer: Option, - tab_index: Option, - size: ButtonSize, - rounding: Option, - tooltip: Option AnyView>>, - hoverable_tooltip: Option AnyView>>, - cursor_style: CursorStyle, - on_click: Option>, - on_right_click: Option>, - children: SmallVec<[AnyElement; 2]>, - focus_handle: Option, -} - -impl ButtonLike { - pub fn new(id: impl Into) -> Self { - Self { - base: div(), - id: id.into(), - style: ButtonStyle::default(), - disabled: false, - selected: false, - selected_style: None, - width: None, - height: None, - size: ButtonSize::Default, - rounding: Some(ButtonLikeRounding::ALL), - tooltip: None, - hoverable_tooltip: None, - children: SmallVec::new(), - cursor_style: CursorStyle::PointingHand, - on_click: None, - on_right_click: None, - layer: None, - tab_index: None, - focus_handle: None, - } - } - - pub fn new_rounded_left(id: impl Into) -> Self { - Self::new(id).rounding(ButtonLikeRounding::LEFT) - } - - pub fn new_rounded_right(id: impl Into) -> Self { - Self::new(id).rounding(ButtonLikeRounding::RIGHT) - } - - pub fn new_rounded_all(id: impl Into) -> Self { - Self::new(id).rounding(ButtonLikeRounding::ALL) - } - - pub fn opacity(mut self, opacity: f32) -> Self { - self.base = self.base.opacity(opacity); - self - } - - pub fn height(mut self, height: DefiniteLength) -> Self { - self.height = Some(height); - self - } - - pub(crate) fn rounding(mut self, rounding: impl Into>) -> Self { - self.rounding = rounding.into(); - self - } - - pub fn on_right_click( - mut self, - handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_right_click = Some(Box::new(handler)); - self - } - - pub fn hoverable_tooltip( - mut self, - tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, - ) -> Self { - self.hoverable_tooltip = Some(Box::new(tooltip)); - self - } -} - -impl Disableable for ButtonLike { - fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } -} - -impl Toggleable for ButtonLike { - fn toggle_state(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -impl SelectableButton for ButtonLike { - fn selected_style(mut self, style: ButtonStyle) -> Self { - self.selected_style = Some(style); - self - } -} - -impl Clickable for ButtonLike { - fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self { - self.on_click = Some(Box::new(handler)); - self - } - - fn cursor_style(mut self, cursor_style: CursorStyle) -> Self { - self.cursor_style = cursor_style; - self - } -} - -impl FixedWidth for ButtonLike { - fn width(mut self, width: impl Into) -> Self { - self.width = Some(width.into()); - self - } - - fn full_width(mut self) -> Self { - self.width = Some(relative(1.)); - self - } -} - -impl ButtonCommon for ButtonLike { - fn id(&self) -> &ElementId { - &self.id - } - - fn style(mut self, style: ButtonStyle) -> Self { - self.style = style; - self - } - - fn size(mut self, size: ButtonSize) -> Self { - self.size = size; - self - } - - fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.tooltip = Some(Box::new(tooltip)); - self - } - - fn tab_index(mut self, tab_index: impl Into) -> Self { - self.tab_index = Some(tab_index.into()); - self - } - - fn layer(mut self, elevation: ElevationIndex) -> Self { - self.layer = Some(elevation); - self - } - - fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self { - self.focus_handle = Some(focus_handle.clone()); - self - } -} - -impl VisibleOnHover for ButtonLike { - fn visible_on_hover(mut self, group_name: impl Into) -> Self { - self.base = self.base.visible_on_hover(group_name); - self - } -} - -impl ParentElement for ButtonLike { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for ButtonLike { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let style = self - .selected_style - .filter(|_| self.selected) - .unwrap_or(self.style); - - let is_outlined = matches!( - self.style, - ButtonStyle::Outlined | ButtonStyle::OutlinedGhost - ); - - self.base - .h_flex() - .id(self.id.clone()) - .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)) - .when_some(self.focus_handle, |this, focus_handle| { - this.track_focus(&focus_handle) - }) - .font_ui(cx) - .group("") - .flex_none() - .h(self.height.unwrap_or(self.size.rems().into())) - .when_some(self.width, |this, width| { - this.w(width).justify_center().text_center() - }) - .when(is_outlined, |this| this.border_1()) - .when_some(self.rounding, |this, rounding| { - this.when(rounding.top_left, |this| this.rounded_tl_sm()) - .when(rounding.top_right, |this| this.rounded_tr_sm()) - .when(rounding.bottom_right, |this| this.rounded_br_sm()) - .when(rounding.bottom_left, |this| this.rounded_bl_sm()) - }) - .gap(DynamicSpacing::Base04.rems(cx)) - .map(|this| match self.size { - ButtonSize::Large | ButtonSize::Medium => this.px(DynamicSpacing::Base08.rems(cx)), - ButtonSize::Default | ButtonSize::Compact => { - this.px(DynamicSpacing::Base04.rems(cx)) - } - ButtonSize::None => this.px_px(), - }) - .border_color(style.enabled(self.layer, cx).border_color) - .bg(style.enabled(self.layer, cx).background) - .when(self.disabled, |this| { - if self.cursor_style == CursorStyle::PointingHand { - this.cursor_not_allowed() - } else { - this.cursor(self.cursor_style) - } - }) - .when(!self.disabled, |this| { - let hovered_style = style.hovered(self.layer, cx); - let focus_color = - |refinement: StyleRefinement| refinement.bg(hovered_style.background); - - this.cursor(self.cursor_style) - .hover(focus_color) - .map(|this| { - if is_outlined { - this.focus_visible(|s| { - s.border_color(cx.theme().colors().border_focused) - }) - } else { - this.focus_visible(focus_color) - } - }) - .active(|active| active.bg(style.active(cx).background)) - }) - .when_some( - self.on_right_click.filter(|_| !self.disabled), - |this, on_right_click| { - this.on_mouse_down(MouseButton::Right, |_event, window, cx| { - window.prevent_default(); - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Right, - move |event, window, cx| { - cx.stop_propagation(); - let click_event = ClickEvent::Mouse(MouseClickEvent { - down: MouseDownEvent { - button: MouseButton::Right, - position: event.position, - modifiers: event.modifiers, - click_count: 1, - first_mouse: false, - }, - up: MouseUpEvent { - button: MouseButton::Right, - position: event.position, - modifiers: event.modifiers, - click_count: 1, - }, - }); - (on_right_click)(&click_event, window, cx) - }, - ) - }, - ) - .when_some( - self.on_click.filter(|_| !self.disabled), - |this, on_click| { - this.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default()) - .on_click(move |event, window, cx| { - cx.stop_propagation(); - (on_click)(event, window, cx) - }) - }, - ) - .when_some(self.tooltip, |this, tooltip| { - this.tooltip(move |window, cx| tooltip(window, cx)) - }) - .when_some(self.hoverable_tooltip, |this, tooltip| { - this.hoverable_tooltip(move |window, cx| tooltip(window, cx)) - }) - .children(self.children) - } -} - -impl Component for ButtonLike { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn sort_name() -> &'static str { - // ButtonLike should be at the bottom of the button list - "ButtonZ" - } - - fn description() -> Option<&'static str> { - Some(ButtonLike::DOCS) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group(vec![ - single_example( - "Default", - ButtonLike::new("default") - .child(Label::new("Default")) - .into_any_element(), - ), - single_example( - "Filled", - ButtonLike::new("filled") - .style(ButtonStyle::Filled) - .child(Label::new("Filled")) - .into_any_element(), - ), - single_example( - "Subtle", - ButtonLike::new("outline") - .style(ButtonStyle::Subtle) - .child(Label::new("Subtle")) - .into_any_element(), - ), - single_example( - "Tinted", - ButtonLike::new("tinted_accent_style") - .style(ButtonStyle::Tinted(TintColor::Accent)) - .child(Label::new("Accent")) - .into_any_element(), - ), - single_example( - "Transparent", - ButtonLike::new("transparent") - .style(ButtonStyle::Transparent) - .child(Label::new("Transparent")) - .into_any_element(), - ), - ]), - example_group_with_title( - "Button Group Constructors", - vec![ - single_example( - "Left Rounded", - ButtonLike::new_rounded_left("left_rounded") - .child(Label::new("Left Rounded")) - .style(ButtonStyle::Filled) - .into_any_element(), - ), - single_example( - "Right Rounded", - ButtonLike::new_rounded_right("right_rounded") - .child(Label::new("Right Rounded")) - .style(ButtonStyle::Filled) - .into_any_element(), - ), - single_example( - "Button Group", - h_flex() - .gap_px() - .child( - ButtonLike::new_rounded_left("bg_left") - .child(Label::new("Left")) - .style(ButtonStyle::Filled), - ) - .child( - ButtonLike::new_rounded_right("bg_right") - .child(Label::new("Right")) - .style(ButtonStyle::Filled), - ) - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs deleted file mode 100644 index 961176ed6c..0000000000 --- a/crates/ui/src/components/button/icon_button.rs +++ /dev/null @@ -1,388 +0,0 @@ -use gpui::{AnyView, DefiniteLength, Hsla}; - -use super::button_like::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle}; -use crate::{ElevationIndex, Indicator, SelectableButton, TintColor, prelude::*}; -use crate::{IconName, IconSize}; - -use super::button_icon::ButtonIcon; - -/// The shape of an [`IconButton`]. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum IconButtonShape { - Square, - Wide, -} - -#[derive(IntoElement, RegisterComponent)] -pub struct IconButton { - base: ButtonLike, - shape: IconButtonShape, - icon: IconName, - icon_size: IconSize, - icon_color: Color, - selected_icon: Option, - selected_icon_color: Option, - indicator: Option, - indicator_border_color: Option, - alpha: Option, -} - -impl IconButton { - pub fn new(id: impl Into, icon: IconName) -> Self { - let mut this = Self { - base: ButtonLike::new(id), - shape: IconButtonShape::Wide, - icon, - icon_size: IconSize::default(), - icon_color: Color::Default, - selected_icon: None, - selected_icon_color: None, - indicator: None, - indicator_border_color: None, - alpha: None, - }; - this.base.base = this.base.base.debug_selector(|| format!("ICON-{:?}", icon)); - this - } - - pub fn shape(mut self, shape: IconButtonShape) -> Self { - self.shape = shape; - self - } - - pub fn icon_size(mut self, icon_size: IconSize) -> Self { - self.icon_size = icon_size; - self - } - - pub fn icon_color(mut self, icon_color: Color) -> Self { - self.icon_color = icon_color; - self - } - - pub fn alpha(mut self, alpha: f32) -> Self { - self.alpha = Some(alpha); - self - } - - pub fn selected_icon(mut self, icon: impl Into>) -> Self { - self.selected_icon = icon.into(); - self - } - - pub fn on_right_click( - mut self, - handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.base = self.base.on_right_click(handler); - self - } - - /// Sets the icon color used when the button is in a selected state. - pub fn selected_icon_color(mut self, color: impl Into>) -> Self { - self.selected_icon_color = color.into(); - self - } - - pub fn indicator(mut self, indicator: Indicator) -> Self { - self.indicator = Some(indicator); - self - } - - pub fn indicator_border_color(mut self, color: Option) -> Self { - self.indicator_border_color = color; - - self - } -} - -impl Disableable for IconButton { - fn disabled(mut self, disabled: bool) -> Self { - self.base = self.base.disabled(disabled); - self - } -} - -impl Toggleable for IconButton { - fn toggle_state(mut self, selected: bool) -> Self { - self.base = self.base.toggle_state(selected); - self - } -} - -impl SelectableButton for IconButton { - fn selected_style(mut self, style: ButtonStyle) -> Self { - self.base = self.base.selected_style(style); - self - } -} - -impl Clickable for IconButton { - fn on_click( - mut self, - handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.base = self.base.on_click(handler); - self - } - - fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self { - self.base = self.base.cursor_style(cursor_style); - self - } -} - -impl FixedWidth for IconButton { - fn width(mut self, width: impl Into) -> Self { - self.base = self.base.width(width); - self - } - - fn full_width(mut self) -> Self { - self.base = self.base.full_width(); - self - } -} - -impl ButtonCommon for IconButton { - fn id(&self) -> &ElementId { - self.base.id() - } - - fn style(mut self, style: ButtonStyle) -> Self { - self.base = self.base.style(style); - self - } - - fn size(mut self, size: ButtonSize) -> Self { - self.base = self.base.size(size); - self - } - - fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.base = self.base.tooltip(tooltip); - self - } - - fn tab_index(mut self, tab_index: impl Into) -> Self { - self.base = self.base.tab_index(tab_index); - self - } - - fn layer(mut self, elevation: ElevationIndex) -> Self { - self.base = self.base.layer(elevation); - self - } - - fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self { - self.base = self.base.track_focus(focus_handle); - self - } -} - -impl VisibleOnHover for IconButton { - fn visible_on_hover(mut self, group_name: impl Into) -> Self { - self.base = self.base.visible_on_hover(group_name); - self - } -} - -impl RenderOnce for IconButton { - #[allow(refining_impl_trait)] - fn render(self, window: &mut Window, cx: &mut App) -> ButtonLike { - let is_disabled = self.base.disabled; - let is_selected = self.base.selected; - let selected_style = self.base.selected_style; - - let color = self.icon_color.color(cx).opacity(self.alpha.unwrap_or(1.0)); - self.base - .map(|this| match self.shape { - IconButtonShape::Square => { - let size = self.icon_size.square(window, cx); - this.width(size).height(size.into()) - } - IconButtonShape::Wide => this, - }) - .child( - ButtonIcon::new(self.icon) - .disabled(is_disabled) - .toggle_state(is_selected) - .selected_icon(self.selected_icon) - .selected_icon_color(self.selected_icon_color) - .when_some(selected_style, |this, style| this.selected_style(style)) - .when_some(self.indicator, |this, indicator| { - this.indicator(indicator) - .indicator_border_color(self.indicator_border_color) - }) - .size(self.icon_size) - .color(Color::Custom(color)), - ) - } -} - -impl Component for IconButton { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn sort_name() -> &'static str { - "ButtonB" - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Icon Button Styles", - vec![ - single_example( - "Default", - IconButton::new("default", IconName::Check) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - single_example( - "Filled", - IconButton::new("filled", IconName::Check) - .layer(ElevationIndex::Background) - .style(ButtonStyle::Filled) - .into_any_element(), - ), - single_example( - "Subtle", - IconButton::new("subtle", IconName::Check) - .layer(ElevationIndex::Background) - .style(ButtonStyle::Subtle) - .into_any_element(), - ), - single_example( - "Tinted", - IconButton::new("tinted", IconName::Check) - .layer(ElevationIndex::Background) - .style(ButtonStyle::Tinted(TintColor::Accent)) - .into_any_element(), - ), - single_example( - "Transparent", - IconButton::new("transparent", IconName::Check) - .layer(ElevationIndex::Background) - .style(ButtonStyle::Transparent) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Icon Button Shapes", - vec![ - single_example( - "Square", - IconButton::new("square", IconName::Check) - .shape(IconButtonShape::Square) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - single_example( - "Wide", - IconButton::new("wide", IconName::Check) - .shape(IconButtonShape::Wide) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Icon Button Sizes", - vec![ - single_example( - "XSmall", - IconButton::new("xsmall", IconName::Check) - .icon_size(IconSize::XSmall) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - single_example( - "Small", - IconButton::new("small", IconName::Check) - .icon_size(IconSize::Small) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - single_example( - "Medium", - IconButton::new("medium", IconName::Check) - .icon_size(IconSize::Medium) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - single_example( - "XLarge", - IconButton::new("xlarge", IconName::Check) - .icon_size(IconSize::XLarge) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Special States", - vec![ - single_example( - "Disabled", - IconButton::new("disabled", IconName::Check) - .disabled(true) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - single_example( - "Selected", - IconButton::new("selected", IconName::Check) - .toggle_state(true) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - single_example( - "With Indicator", - IconButton::new("indicator", IconName::Check) - .indicator(Indicator::dot().color(Color::Success)) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Custom Colors", - vec![ - single_example( - "Custom Icon Color", - IconButton::new("custom_color", IconName::Check) - .icon_color(Color::Accent) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - single_example( - "With Alpha", - IconButton::new("alpha", IconName::Check) - .alpha(0.5) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/button/split_button.rs b/crates/ui/src/components/button/split_button.rs deleted file mode 100644 index 48f06ff378..0000000000 --- a/crates/ui/src/components/button/split_button.rs +++ /dev/null @@ -1,97 +0,0 @@ -use gpui::{ - AnyElement, App, BoxShadow, IntoElement, ParentElement, RenderOnce, Styled, Window, div, hsla, - point, prelude::FluentBuilder, px, -}; -use theme::ActiveTheme; - -use crate::{ElevationIndex, IconButton, h_flex}; - -use super::ButtonLike; - -#[derive(Clone, Copy, PartialEq)] -pub enum SplitButtonStyle { - Filled, - Outlined, - Transparent, -} - -pub enum SplitButtonKind { - ButtonLike(ButtonLike), - IconButton(IconButton), -} - -impl From for SplitButtonKind { - fn from(icon_button: IconButton) -> Self { - Self::IconButton(icon_button) - } -} - -impl From for SplitButtonKind { - fn from(button_like: ButtonLike) -> Self { - Self::ButtonLike(button_like) - } -} - -/// /// A button with two parts: a primary action on the left and a secondary action on the right. -/// -/// The left side is a [`ButtonLike`] with the main action, while the right side can contain -/// any element (typically a dropdown trigger or similar). -/// -/// The two sections are visually separated by a divider, but presented as a unified control. -#[derive(IntoElement)] -pub struct SplitButton { - left: SplitButtonKind, - right: AnyElement, - style: SplitButtonStyle, -} - -impl SplitButton { - pub fn new(left: impl Into, right: AnyElement) -> Self { - Self { - left: left.into(), - right, - style: SplitButtonStyle::Filled, - } - } - - pub fn style(mut self, style: SplitButtonStyle) -> Self { - self.style = style; - self - } -} - -impl RenderOnce for SplitButton { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let is_filled_or_outlined = matches!( - self.style, - SplitButtonStyle::Filled | SplitButtonStyle::Outlined - ); - - h_flex() - .rounded_sm() - .when(is_filled_or_outlined, |this| { - this.border_1() - .border_color(cx.theme().colors().border.opacity(0.8)) - }) - .child(div().flex_grow().child(match self.left { - SplitButtonKind::ButtonLike(button) => button.into_any_element(), - SplitButtonKind::IconButton(icon) => icon.into_any_element(), - })) - .child( - div() - .h_full() - .w_px() - .bg(cx.theme().colors().border.opacity(0.5)), - ) - .child(self.right) - .when(self.style == SplitButtonStyle::Filled, |this| { - this.bg(ElevationIndex::Surface.on_elevation_bg(cx)) - .shadow(vec![BoxShadow { - color: hsla(0.0, 0.0, 0.0, 0.16), - offset: point(px(0.), px(1.)), - blur_radius: px(0.), - spread_radius: px(0.), - }]) - }) - } -} diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs deleted file mode 100644 index 5cecfef062..0000000000 --- a/crates/ui/src/components/button/toggle_button.rs +++ /dev/null @@ -1,769 +0,0 @@ -use std::rc::Rc; - -use gpui::{AnyView, ClickEvent, relative}; - -use crate::{ButtonLike, ButtonLikeRounding, TintColor, Tooltip, prelude::*}; - -/// The position of a [`ToggleButton`] within a group of buttons. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub struct ToggleButtonPosition { - /// The toggle button is one of the leftmost of the group. - leftmost: bool, - /// The toggle button is one of the rightmost of the group. - rightmost: bool, - /// The toggle button is one of the topmost of the group. - topmost: bool, - /// The toggle button is one of the bottommost of the group. - bottommost: bool, -} - -impl ToggleButtonPosition { - pub const HORIZONTAL_FIRST: Self = Self { - leftmost: true, - ..Self::HORIZONTAL_MIDDLE - }; - pub const HORIZONTAL_MIDDLE: Self = Self { - leftmost: false, - rightmost: false, - topmost: true, - bottommost: true, - }; - pub const HORIZONTAL_LAST: Self = Self { - rightmost: true, - ..Self::HORIZONTAL_MIDDLE - }; - - pub(crate) fn to_rounding(self) -> ButtonLikeRounding { - ButtonLikeRounding { - top_left: self.topmost && self.leftmost, - top_right: self.topmost && self.rightmost, - bottom_right: self.bottommost && self.rightmost, - bottom_left: self.bottommost && self.leftmost, - } - } -} - -pub struct ButtonConfiguration { - label: SharedString, - icon: Option, - on_click: Box, - selected: bool, - tooltip: Option AnyView>>, -} - -mod private { - pub trait ToggleButtonStyle {} -} - -pub trait ButtonBuilder: 'static + private::ToggleButtonStyle { - fn into_configuration(self) -> ButtonConfiguration; -} - -pub struct ToggleButtonSimple { - label: SharedString, - on_click: Box, - selected: bool, - tooltip: Option AnyView>>, -} - -impl ToggleButtonSimple { - pub fn new( - label: impl Into, - on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - Self { - label: label.into(), - on_click: Box::new(on_click), - selected: false, - tooltip: None, - } - } - - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - - pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.tooltip = Some(Rc::new(tooltip)); - self - } -} - -impl private::ToggleButtonStyle for ToggleButtonSimple {} - -impl ButtonBuilder for ToggleButtonSimple { - fn into_configuration(self) -> ButtonConfiguration { - ButtonConfiguration { - label: self.label, - icon: None, - on_click: self.on_click, - selected: self.selected, - tooltip: self.tooltip, - } - } -} - -pub struct ToggleButtonWithIcon { - label: SharedString, - icon: IconName, - on_click: Box, - selected: bool, - tooltip: Option AnyView>>, -} - -impl ToggleButtonWithIcon { - pub fn new( - label: impl Into, - icon: IconName, - on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - Self { - label: label.into(), - icon, - on_click: Box::new(on_click), - selected: false, - tooltip: None, - } - } - - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - - pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.tooltip = Some(Rc::new(tooltip)); - self - } -} - -impl private::ToggleButtonStyle for ToggleButtonWithIcon {} - -impl ButtonBuilder for ToggleButtonWithIcon { - fn into_configuration(self) -> ButtonConfiguration { - ButtonConfiguration { - label: self.label, - icon: Some(self.icon), - on_click: self.on_click, - selected: self.selected, - tooltip: self.tooltip, - } - } -} - -#[derive(Clone, Copy, PartialEq)] -pub enum ToggleButtonGroupStyle { - Transparent, - Filled, - Outlined, -} - -#[derive(Clone, Copy, PartialEq)] -pub enum ToggleButtonGroupSize { - Default, - Medium, - Large, - Custom(Rems), -} - -#[derive(IntoElement)] -pub struct ToggleButtonGroup -where - T: ButtonBuilder, -{ - group_name: SharedString, - rows: [[T; COLS]; ROWS], - style: ToggleButtonGroupStyle, - size: ToggleButtonGroupSize, - label_size: LabelSize, - group_width: Option, - auto_width: bool, - selected_index: usize, - tab_index: Option, -} - -impl ToggleButtonGroup { - pub fn single_row(group_name: impl Into, buttons: [T; COLS]) -> Self { - Self { - group_name: group_name.into(), - rows: [buttons], - style: ToggleButtonGroupStyle::Transparent, - size: ToggleButtonGroupSize::Default, - label_size: LabelSize::Small, - group_width: None, - auto_width: false, - selected_index: 0, - tab_index: None, - } - } -} - -impl ToggleButtonGroup { - pub fn two_rows( - group_name: impl Into, - first_row: [T; COLS], - second_row: [T; COLS], - ) -> Self { - Self { - group_name: group_name.into(), - rows: [first_row, second_row], - style: ToggleButtonGroupStyle::Transparent, - size: ToggleButtonGroupSize::Default, - label_size: LabelSize::Small, - group_width: None, - auto_width: false, - selected_index: 0, - tab_index: None, - } - } -} - -impl ToggleButtonGroup { - pub fn style(mut self, style: ToggleButtonGroupStyle) -> Self { - self.style = style; - self - } - - pub fn size(mut self, size: ToggleButtonGroupSize) -> Self { - self.size = size; - self - } - - pub fn selected_index(mut self, index: usize) -> Self { - self.selected_index = index; - self - } - - /// Makes the button group size itself to fit the content of the buttons, - /// rather than filling the full width of its parent. - pub fn auto_width(mut self) -> Self { - self.auto_width = true; - self - } - - pub fn label_size(mut self, label_size: LabelSize) -> Self { - self.label_size = label_size; - self - } - - /// Sets the tab index for the toggle button group. - /// The tab index is set to the initial value provided, then the - /// value is incremented by the number of buttons in the group. - pub fn tab_index(mut self, tab_index: &mut isize) -> Self { - self.tab_index = Some(*tab_index); - *tab_index += (COLS * ROWS) as isize; - self - } - - const fn button_width() -> DefiniteLength { - relative(1. / COLS as f32) - } -} - -impl FixedWidth - for ToggleButtonGroup -{ - fn width(mut self, width: impl Into) -> Self { - self.group_width = Some(width.into()); - self - } - - fn full_width(mut self) -> Self { - self.group_width = Some(relative(1.)); - self - } -} - -impl RenderOnce - for ToggleButtonGroup -{ - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let custom_height = match self.size { - ToggleButtonGroupSize::Custom(height) => Some(height), - _ => None, - }; - - let entries = - self.rows.into_iter().enumerate().map(|(row_index, row)| { - let group_name = self.group_name.clone(); - row.into_iter().enumerate().map(move |(col_index, button)| { - let ButtonConfiguration { - label, - icon, - on_click, - selected, - tooltip, - } = button.into_configuration(); - - let entry_index = row_index * COLS + col_index; - - ButtonLike::new((group_name.clone(), entry_index)) - .when(!self.auto_width, |this| this.full_width()) - .rounding(Some( - ToggleButtonPosition { - leftmost: col_index == 0, - rightmost: col_index == COLS - 1, - topmost: row_index == 0, - bottommost: row_index == ROWS - 1, - } - .to_rounding(), - )) - .when_some(self.tab_index, |this, tab_index| { - this.tab_index(tab_index + entry_index as isize) - }) - .when(entry_index == self.selected_index || selected, |this| { - this.toggle_state(true) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - }) - .when(self.style == ToggleButtonGroupStyle::Filled, |button| { - button.style(ButtonStyle::Filled) - }) - .when(self.size == ToggleButtonGroupSize::Medium, |button| { - button.size(ButtonSize::Medium) - }) - .when(self.size == ToggleButtonGroupSize::Large, |button| { - button.size(ButtonSize::Large) - }) - .when_some(custom_height, |button, height| button.height(height.into())) - .child( - h_flex() - .w_full() - .px_2() - .gap_1p5() - .justify_center() - .flex_none() - .when_some(icon, |this, icon| { - this.py_2() - .child(Icon::new(icon).size(IconSize::XSmall).map(|this| { - if entry_index == self.selected_index || selected { - this.color(Color::Accent) - } else { - this.color(Color::Muted) - } - })) - }) - .child(Label::new(label).size(self.label_size).when( - entry_index == self.selected_index || selected, - |this| this.color(Color::Accent), - )), - ) - .when_some(tooltip, |this, tooltip| { - this.tooltip(move |window, cx| tooltip(window, cx)) - }) - .on_click(on_click) - .into_any_element() - }) - }); - - let border_color = cx.theme().colors().border.opacity(0.6); - let is_outlined_or_filled = self.style == ToggleButtonGroupStyle::Outlined - || self.style == ToggleButtonGroupStyle::Filled; - let is_transparent = self.style == ToggleButtonGroupStyle::Transparent; - - v_flex() - .map(|this| { - if let Some(width) = self.group_width { - this.w(width) - } else if self.auto_width { - this - } else { - this.w_full() - } - }) - .rounded_md() - .overflow_hidden() - .map(|this| { - if is_transparent { - this.gap_px() - } else { - this.border_1().border_color(border_color) - } - }) - .children(entries.enumerate().map(|(row_index, row)| { - let last_row = row_index == ROWS - 1; - h_flex() - .when(!is_outlined_or_filled, |this| this.gap_px()) - .when(is_outlined_or_filled && !last_row, |this| { - this.border_b_1().border_color(border_color) - }) - .children(row.enumerate().map(|(item_index, item)| { - let last_item = item_index == COLS - 1; - div() - .when(is_outlined_or_filled && !last_item, |this| { - this.border_r_1().border_color(border_color) - }) - .when(!self.auto_width, |this| this.w(Self::button_width())) - .overflow_hidden() - .child(item) - })) - })) - } -} - -fn register_toggle_button_group() { - component::register_component::>(); -} - -component::__private::inventory::submit! { - component::ComponentFn::new(register_toggle_button_group) -} - -impl Component - for ToggleButtonGroup -{ - fn name() -> &'static str { - "ToggleButtonGroup" - } - - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn sort_name() -> &'static str { - "ButtonG" - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![example_group_with_title( - "Transparent Variant", - vec![ - single_example( - "Single Row Group", - ToggleButtonGroup::single_row( - "single_row_test", - [ - ToggleButtonSimple::new("First", |_, _, _| {}), - ToggleButtonSimple::new("Second", |_, _, _| {}), - ToggleButtonSimple::new("Third", |_, _, _| {}), - ], - ) - .selected_index(1) - .into_any_element(), - ), - single_example( - "Single Row Group with icons", - ToggleButtonGroup::single_row( - "single_row_test_icon", - [ - ToggleButtonWithIcon::new( - "First", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Second", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Third", - IconName::AiZed, - |_, _, _| {}, - ), - ], - ) - .selected_index(1) - .into_any_element(), - ), - single_example( - "Multiple Row Group", - ToggleButtonGroup::two_rows( - "multiple_row_test", - [ - ToggleButtonSimple::new("First", |_, _, _| {}), - ToggleButtonSimple::new("Second", |_, _, _| {}), - ToggleButtonSimple::new("Third", |_, _, _| {}), - ], - [ - ToggleButtonSimple::new("Fourth", |_, _, _| {}), - ToggleButtonSimple::new("Fifth", |_, _, _| {}), - ToggleButtonSimple::new("Sixth", |_, _, _| {}), - ], - ) - .selected_index(3) - .into_any_element(), - ), - single_example( - "Multiple Row Group with Icons", - ToggleButtonGroup::two_rows( - "multiple_row_test_icons", - [ - ToggleButtonWithIcon::new( - "First", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Second", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Third", - IconName::AiZed, - |_, _, _| {}, - ), - ], - [ - ToggleButtonWithIcon::new( - "Fourth", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Fifth", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Sixth", - IconName::AiZed, - |_, _, _| {}, - ), - ], - ) - .selected_index(3) - .into_any_element(), - ), - ], - )]) - .children(vec![example_group_with_title( - "Outlined Variant", - vec![ - single_example( - "Single Row Group", - ToggleButtonGroup::single_row( - "single_row_test_outline", - [ - ToggleButtonSimple::new("First", |_, _, _| {}), - ToggleButtonSimple::new("Second", |_, _, _| {}), - ToggleButtonSimple::new("Third", |_, _, _| {}), - ], - ) - .selected_index(1) - .style(ToggleButtonGroupStyle::Outlined) - .into_any_element(), - ), - single_example( - "Single Row Group with icons", - ToggleButtonGroup::single_row( - "single_row_test_icon_outlined", - [ - ToggleButtonWithIcon::new( - "First", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Second", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Third", - IconName::AiZed, - |_, _, _| {}, - ), - ], - ) - .selected_index(1) - .style(ToggleButtonGroupStyle::Outlined) - .into_any_element(), - ), - single_example( - "Multiple Row Group", - ToggleButtonGroup::two_rows( - "multiple_row_test", - [ - ToggleButtonSimple::new("First", |_, _, _| {}), - ToggleButtonSimple::new("Second", |_, _, _| {}), - ToggleButtonSimple::new("Third", |_, _, _| {}), - ], - [ - ToggleButtonSimple::new("Fourth", |_, _, _| {}), - ToggleButtonSimple::new("Fifth", |_, _, _| {}), - ToggleButtonSimple::new("Sixth", |_, _, _| {}), - ], - ) - .selected_index(3) - .style(ToggleButtonGroupStyle::Outlined) - .into_any_element(), - ), - single_example( - "Multiple Row Group with Icons", - ToggleButtonGroup::two_rows( - "multiple_row_test", - [ - ToggleButtonWithIcon::new( - "First", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Second", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Third", - IconName::AiZed, - |_, _, _| {}, - ), - ], - [ - ToggleButtonWithIcon::new( - "Fourth", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Fifth", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Sixth", - IconName::AiZed, - |_, _, _| {}, - ), - ], - ) - .selected_index(3) - .style(ToggleButtonGroupStyle::Outlined) - .into_any_element(), - ), - ], - )]) - .children(vec![example_group_with_title( - "Filled Variant", - vec![ - single_example( - "Single Row Group", - ToggleButtonGroup::single_row( - "single_row_test_outline", - [ - ToggleButtonSimple::new("First", |_, _, _| {}), - ToggleButtonSimple::new("Second", |_, _, _| {}), - ToggleButtonSimple::new("Third", |_, _, _| {}), - ], - ) - .selected_index(2) - .style(ToggleButtonGroupStyle::Filled) - .into_any_element(), - ), - single_example( - "Single Row Group with icons", - ToggleButtonGroup::single_row( - "single_row_test_icon_outlined", - [ - ToggleButtonWithIcon::new( - "First", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Second", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Third", - IconName::AiZed, - |_, _, _| {}, - ), - ], - ) - .selected_index(1) - .style(ToggleButtonGroupStyle::Filled) - .into_any_element(), - ), - single_example( - "Multiple Row Group", - ToggleButtonGroup::two_rows( - "multiple_row_test", - [ - ToggleButtonSimple::new("First", |_, _, _| {}), - ToggleButtonSimple::new("Second", |_, _, _| {}), - ToggleButtonSimple::new("Third", |_, _, _| {}), - ], - [ - ToggleButtonSimple::new("Fourth", |_, _, _| {}), - ToggleButtonSimple::new("Fifth", |_, _, _| {}), - ToggleButtonSimple::new("Sixth", |_, _, _| {}), - ], - ) - .selected_index(3) - .width(rems_from_px(100.)) - .style(ToggleButtonGroupStyle::Filled) - .into_any_element(), - ), - single_example( - "Multiple Row Group with Icons", - ToggleButtonGroup::two_rows( - "multiple_row_test", - [ - ToggleButtonWithIcon::new( - "First", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Second", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Third", - IconName::AiZed, - |_, _, _| {}, - ), - ], - [ - ToggleButtonWithIcon::new( - "Fourth", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Fifth", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Sixth", - IconName::AiZed, - |_, _, _| {}, - ), - ], - ) - .selected_index(3) - .width(rems_from_px(100.)) - .style(ToggleButtonGroupStyle::Filled) - .into_any_element(), - ), - ], - )]) - .children(vec![single_example( - "With Tooltips", - ToggleButtonGroup::single_row( - "with_tooltips", - [ - ToggleButtonSimple::new("First", |_, _, _| {}) - .tooltip(Tooltip::text("This is a tooltip. Hello!")), - ToggleButtonSimple::new("Second", |_, _, _| {}) - .tooltip(Tooltip::text("This is a tooltip. Hey?")), - ToggleButtonSimple::new("Third", |_, _, _| {}) - .tooltip(Tooltip::text("This is a tooltip. Get out of here now!")), - ], - ) - .selected_index(1) - .into_any_element(), - )]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/callout.rs b/crates/ui/src/components/callout.rs deleted file mode 100644 index 4eb849d7f6..0000000000 --- a/crates/ui/src/components/callout.rs +++ /dev/null @@ -1,327 +0,0 @@ -use gpui::AnyElement; - -use crate::prelude::*; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BorderPosition { - Top, - Bottom, -} - -/// A callout component for displaying important information that requires user attention. -/// -/// # Usage Example -/// -/// ``` -/// use ui::prelude::*; -/// use ui::{Button, Callout, IconName, Label, Severity}; -/// -/// let callout = Callout::new() -/// .severity(Severity::Warning) -/// .icon(IconName::Warning) -/// .title("Be aware of your subscription!") -/// .description("Your subscription is about to expire. Renew now!") -/// .actions_slot(Button::new("renew", "Renew Now")); -/// ``` -/// -#[derive(IntoElement, RegisterComponent)] -pub struct Callout { - severity: Severity, - icon: Option, - title: Option, - description: Option, - description_slot: Option, - actions_slot: Option, - dismiss_action: Option, - line_height: Option, - border_position: BorderPosition, -} - -impl Callout { - /// Creates a new `Callout` component with default styling. - pub fn new() -> Self { - Self { - severity: Severity::Info, - icon: None, - title: None, - description: None, - description_slot: None, - actions_slot: None, - dismiss_action: None, - line_height: None, - border_position: BorderPosition::Top, - } - } - - /// Sets the severity of the callout. - pub fn severity(mut self, severity: Severity) -> Self { - self.severity = severity; - self - } - - /// Sets the icon to display in the callout. - pub fn icon(mut self, icon: IconName) -> Self { - self.icon = Some(icon); - self - } - - /// Sets the title of the callout. - pub fn title(mut self, title: impl Into) -> Self { - self.title = Some(title.into()); - self - } - - /// Sets the description of the callout. - /// The description can be single or multi-line text. - pub fn description(mut self, description: impl Into) -> Self { - self.description = Some(description.into()); - self - } - - /// Allows for any element—like markdown elements—to fill the description slot of the callout. - /// This method wins over `description` if both happen to be set. - pub fn description_slot(mut self, description: impl IntoElement) -> Self { - self.description_slot = Some(description.into_any_element()); - self - } - - /// Sets the primary call-to-action button. - pub fn actions_slot(mut self, action: impl IntoElement) -> Self { - self.actions_slot = Some(action.into_any_element()); - self - } - - /// Sets an optional dismiss button, which is usually an icon button with a close icon. - /// This button is always rendered as the last one to the far right. - pub fn dismiss_action(mut self, action: impl IntoElement) -> Self { - self.dismiss_action = Some(action.into_any_element()); - self - } - - /// Sets a custom line height for the callout content. - pub fn line_height(mut self, line_height: Pixels) -> Self { - self.line_height = Some(line_height); - self - } - - /// Sets the border position in the callout. - pub fn border_position(mut self, border_position: BorderPosition) -> Self { - self.border_position = border_position; - self - } -} - -impl RenderOnce for Callout { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let line_height = self.line_height.unwrap_or(window.line_height()); - - let has_actions = self.actions_slot.is_some() || self.dismiss_action.is_some(); - - let (icon, icon_color, bg_color) = match self.severity { - Severity::Info => ( - IconName::Info, - Color::Muted, - cx.theme().colors().panel_background.opacity(0.), - ), - Severity::Success => ( - IconName::Check, - Color::Success, - cx.theme().status().success.opacity(0.1), - ), - Severity::Warning => ( - IconName::Warning, - Color::Warning, - cx.theme().status().warning_background.opacity(0.2), - ), - Severity::Error => ( - IconName::XCircle, - Color::Error, - cx.theme().status().error.opacity(0.08), - ), - }; - - h_flex() - .min_w_0() - .w_full() - .p_2() - .gap_2() - .items_start() - .map(|this| match self.border_position { - BorderPosition::Top => this.border_t_1(), - BorderPosition::Bottom => this.border_b_1(), - }) - .border_color(cx.theme().colors().border) - .bg(bg_color) - .overflow_x_hidden() - .when(self.icon.is_some(), |this| { - this.child( - h_flex() - .h(line_height) - .justify_center() - .child(Icon::new(icon).size(IconSize::Small).color(icon_color)), - ) - }) - .child( - v_flex() - .min_w_0() - .w_full() - .child( - h_flex() - .min_h(line_height) - .w_full() - .gap_1() - .justify_between() - .flex_wrap() - .when_some(self.title, |this, title| { - this.child(h_flex().child(Label::new(title).size(LabelSize::Small))) - }) - .when(has_actions, |this| { - this.child( - h_flex() - .gap_0p5() - .when_some(self.actions_slot, |this, action| { - this.child(action) - }) - .when_some(self.dismiss_action, |this, action| { - this.child(action) - }), - ) - }), - ) - .map(|this| { - if let Some(description_slot) = self.description_slot { - this.child( - div() - .w_full() - .flex_1() - .text_ui_sm(cx) - .child(description_slot), - ) - } else if let Some(description) = self.description { - this.child( - div() - .w_full() - .flex_1() - .text_ui_sm(cx) - .text_color(cx.theme().colors().text_muted) - .child(description), - ) - } else { - this - } - }), - ) - } -} - -impl Component for Callout { - fn scope() -> ComponentScope { - ComponentScope::DataDisplay - } - - fn description() -> Option<&'static str> { - Some( - "Used to display a callout for situations where the user needs to know some information, and likely make a decision. This might be a thread running out of tokens, or running out of prompts on a plan and needing to upgrade.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - let single_action = || Button::new("got-it", "Got it").label_size(LabelSize::Small); - let multiple_actions = || { - h_flex() - .gap_0p5() - .child(Button::new("update", "Backup & Update").label_size(LabelSize::Small)) - .child(Button::new("dismiss", "Dismiss").label_size(LabelSize::Small)) - }; - - let basic_examples = vec![ - single_example( - "Simple with Title Only", - Callout::new() - .icon(IconName::Info) - .title("System maintenance scheduled for tonight") - .actions_slot(single_action()) - .into_any_element(), - ) - .width(px(580.)), - single_example( - "With Title and Description", - Callout::new() - .icon(IconName::Warning) - .title("Your settings contain deprecated values") - .description( - "We'll backup your current settings and update them to the new format.", - ) - .actions_slot(single_action()) - .into_any_element(), - ) - .width(px(580.)), - single_example( - "Error with Multiple Actions", - Callout::new() - .icon(IconName::Close) - .title("Thread reached the token limit") - .description("Start a new thread from a summary to continue the conversation.") - .actions_slot(multiple_actions()) - .into_any_element(), - ) - .width(px(580.)), - single_example( - "Multi-line Description", - Callout::new() - .icon(IconName::Sparkle) - .title("Upgrade to Pro") - .description("• Unlimited threads\n• Priority support\n• Advanced analytics") - .actions_slot(multiple_actions()) - .into_any_element(), - ) - .width(px(580.)), - ]; - - let severity_examples = vec![ - single_example( - "Info", - Callout::new() - .icon(IconName::Info) - .title("System maintenance scheduled for tonight") - .actions_slot(single_action()) - .into_any_element(), - ), - single_example( - "Warning", - Callout::new() - .severity(Severity::Warning) - .icon(IconName::Triangle) - .title("System maintenance scheduled for tonight") - .actions_slot(single_action()) - .into_any_element(), - ), - single_example( - "Error", - Callout::new() - .severity(Severity::Error) - .icon(IconName::XCircle) - .title("System maintenance scheduled for tonight") - .actions_slot(single_action()) - .into_any_element(), - ), - single_example( - "Success", - Callout::new() - .severity(Severity::Success) - .icon(IconName::Check) - .title("System maintenance scheduled for tonight") - .actions_slot(single_action()) - .into_any_element(), - ), - ]; - - Some( - v_flex() - .gap_4() - .child(example_group(basic_examples).vertical()) - .child(example_group_with_title("Severity", severity_examples).vertical()) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/chip.rs b/crates/ui/src/components/chip.rs deleted file mode 100644 index 8d0000db94..0000000000 --- a/crates/ui/src/components/chip.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::prelude::*; -use gpui::{AnyElement, Hsla, IntoElement, ParentElement, Styled}; - -/// Chips provide a container for an informative label. -/// -/// # Usage Example -/// -/// ``` -/// use ui::Chip; -/// -/// let chip = Chip::new("This Chip"); -/// ``` -#[derive(IntoElement, RegisterComponent)] -pub struct Chip { - label: SharedString, - label_color: Color, - label_size: LabelSize, - bg_color: Option, -} - -impl Chip { - /// Creates a new `Chip` component with the specified label. - pub fn new(label: impl Into) -> Self { - Self { - label: label.into(), - label_color: Color::Default, - label_size: LabelSize::XSmall, - bg_color: None, - } - } - - /// Sets the color of the label. - pub fn label_color(mut self, color: Color) -> Self { - self.label_color = color; - self - } - - /// Sets the size of the label. - pub fn label_size(mut self, size: LabelSize) -> Self { - self.label_size = size; - self - } - - /// Sets a custom background color for the callout content. - pub fn bg_color(mut self, color: Hsla) -> Self { - self.bg_color = Some(color); - self - } -} - -impl RenderOnce for Chip { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let bg_color = self - .bg_color - .unwrap_or(cx.theme().colors().element_background); - - h_flex() - .min_w_0() - .flex_initial() - .px_1() - .border_1() - .rounded_sm() - .border_color(cx.theme().colors().border) - .bg(bg_color) - .overflow_hidden() - .child( - Label::new(self.label) - .size(self.label_size) - .color(self.label_color) - .buffer_font(cx), - ) - } -} - -impl Component for Chip { - fn scope() -> ComponentScope { - ComponentScope::DataDisplay - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - let chip_examples = vec![ - single_example("Default", Chip::new("Chip Example").into_any_element()), - single_example( - "Customized Label Color", - Chip::new("Chip Example") - .label_color(Color::Accent) - .into_any_element(), - ), - single_example( - "Customized Label Size", - Chip::new("Chip Example") - .label_size(LabelSize::Large) - .label_color(Color::Accent) - .into_any_element(), - ), - single_example( - "Customized Background Color", - Chip::new("Chip Example") - .bg_color(cx.theme().colors().text_accent.opacity(0.1)) - .into_any_element(), - ), - ]; - - Some(example_group(chip_examples).vertical().into_any_element()) - } -} diff --git a/crates/ui/src/components/content_group.rs b/crates/ui/src/components/content_group.rs deleted file mode 100644 index f89d38d153..0000000000 --- a/crates/ui/src/components/content_group.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::component_prelude::*; -use crate::prelude::*; -use gpui::{AnyElement, IntoElement, ParentElement, StyleRefinement, Styled}; -use smallvec::SmallVec; - -/// Creates a new [ContentGroup]. -pub fn content_group() -> ContentGroup { - ContentGroup::new() -} - -/// A [ContentGroup] that vertically stacks its children. -/// -/// This is a convenience function that simply combines [`ContentGroup`] and [`v_flex`](crate::v_flex). -pub fn v_container() -> ContentGroup { - content_group().v_flex() -} - -/// Creates a new horizontal [ContentGroup]. -/// -/// This is a convenience function that simply combines [`ContentGroup`] and [`h_flex`](crate::h_flex). -pub fn h_container() -> ContentGroup { - content_group().h_flex() -} - -/// A flexible container component that can hold other elements. -#[derive(IntoElement, Documented, RegisterComponent)] -pub struct ContentGroup { - base: Div, - border: bool, - fill: bool, - children: SmallVec<[AnyElement; 2]>, -} - -impl ContentGroup { - /// Creates a new [`ContentGroup`]. - pub fn new() -> Self { - Self { - base: div(), - border: true, - fill: true, - children: SmallVec::new(), - } - } - - /// Removes the border from the [`ContentGroup`]. - pub fn borderless(mut self) -> Self { - self.border = false; - self - } - - /// Removes the background fill from the [`ContentGroup`]. - pub fn unfilled(mut self) -> Self { - self.fill = false; - self - } -} - -impl ParentElement for ContentGroup { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl Styled for ContentGroup { - fn style(&mut self) -> &mut StyleRefinement { - self.base.style() - } -} - -impl RenderOnce for ContentGroup { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - // TODO: - // Baked in padding will make scrollable views inside of content boxes awkward. - // - // Do we make the padding optional, or do we push to use a different component? - - self.base - .when(self.fill, |this| { - this.bg(cx.theme().colors().text.opacity(0.05)) - }) - .when(self.border, |this| { - this.border_1().border_color(cx.theme().colors().border) - }) - .rounded_sm() - .children(self.children) - } -} - -impl Component for ContentGroup { - fn scope() -> ComponentScope { - ComponentScope::Layout - } - - fn description() -> Option<&'static str> { - Some(ContentGroup::DOCS) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - example_group(vec![ - single_example( - "Default", - ContentGroup::new() - .flex_1() - .items_center() - .justify_center() - .h_48() - .child(Label::new("Default ContentGroup")) - .into_any_element(), - ).description("A contained style for laying out groups of content. Has a default background and border color."), - single_example( - "Without Border", - ContentGroup::new() - .flex_1() - .items_center() - .justify_center() - .h_48() - .borderless() - .child(Label::new("Borderless ContentGroup")) - .into_any_element(), - ), - single_example( - "Without Fill", - ContentGroup::new() - .flex_1() - .items_center() - .justify_center() - .h_48() - .unfilled() - .child(Label::new("Unfilled ContentGroup")) - .into_any_element(), - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs deleted file mode 100644 index a4bae64740..0000000000 --- a/crates/ui/src/components/context_menu.rs +++ /dev/null @@ -1,1392 +0,0 @@ -use crate::{ - Icon, IconButtonShape, IconName, IconSize, KeyBinding, Label, List, ListItem, ListSeparator, - ListSubHeader, h_flex, prelude::*, utils::WithRemSize, v_flex, -}; -use gpui::{ - Action, AnyElement, App, AppContext as _, DismissEvent, Entity, EventEmitter, FocusHandle, - Focusable, IntoElement, Render, Subscription, px, -}; -use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious}; -use settings::Settings; -use std::{rc::Rc, time::Duration}; -use theme::ThemeSettings; - -use super::Tooltip; - -pub enum ContextMenuItem { - Separator, - Header(SharedString), - /// title, link_label, link_url - HeaderWithLink(SharedString, SharedString, SharedString), // This could be folded into header - Label(SharedString), - Entry(ContextMenuEntry), - CustomEntry { - entry_render: Box AnyElement>, - handler: Rc, &mut Window, &mut App)>, - selectable: bool, - documentation_aside: Option, - }, -} - -impl ContextMenuItem { - pub fn custom_entry( - entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static, - handler: impl Fn(&mut Window, &mut App) + 'static, - documentation_aside: Option, - ) -> Self { - Self::CustomEntry { - entry_render: Box::new(entry_render), - handler: Rc::new(move |_, window, cx| handler(window, cx)), - selectable: true, - documentation_aside, - } - } -} - -pub struct ContextMenuEntry { - toggle: Option<(IconPosition, bool)>, - label: SharedString, - icon: Option, - custom_icon_path: Option, - custom_icon_svg: Option, - icon_position: IconPosition, - icon_size: IconSize, - icon_color: Option, - handler: Rc, &mut Window, &mut App)>, - action: Option>, - disabled: bool, - documentation_aside: Option, - end_slot_icon: Option, - end_slot_title: Option, - end_slot_handler: Option, &mut Window, &mut App)>>, - show_end_slot_on_hover: bool, -} - -impl ContextMenuEntry { - pub fn new(label: impl Into) -> Self { - ContextMenuEntry { - toggle: None, - label: label.into(), - icon: None, - custom_icon_path: None, - custom_icon_svg: None, - icon_position: IconPosition::Start, - icon_size: IconSize::Small, - icon_color: None, - handler: Rc::new(|_, _, _| {}), - action: None, - disabled: false, - documentation_aside: None, - end_slot_icon: None, - end_slot_title: None, - end_slot_handler: None, - show_end_slot_on_hover: false, - } - } - - pub fn toggleable(mut self, toggle_position: IconPosition, toggled: bool) -> Self { - self.toggle = Some((toggle_position, toggled)); - self - } - - pub fn icon(mut self, icon: IconName) -> Self { - self.icon = Some(icon); - self - } - - pub fn custom_icon_path(mut self, path: impl Into) -> Self { - self.custom_icon_path = Some(path.into()); - self.custom_icon_svg = None; // Clear other icon sources if custom path is set - self.icon = None; - self - } - - pub fn custom_icon_svg(mut self, svg: impl Into) -> Self { - self.custom_icon_svg = Some(svg.into()); - self.custom_icon_path = None; // Clear other icon sources if custom path is set - self.icon = None; - self - } - - pub fn icon_position(mut self, position: IconPosition) -> Self { - self.icon_position = position; - self - } - - pub fn icon_size(mut self, icon_size: IconSize) -> Self { - self.icon_size = icon_size; - self - } - - pub fn icon_color(mut self, icon_color: Color) -> Self { - self.icon_color = Some(icon_color); - self - } - - pub fn toggle(mut self, toggle_position: IconPosition, toggled: bool) -> Self { - self.toggle = Some((toggle_position, toggled)); - self - } - - pub fn action(mut self, action: Box) -> Self { - self.action = Some(action); - self - } - - pub fn handler(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { - self.handler = Rc::new(move |_, window, cx| handler(window, cx)); - self - } - - pub fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } - - pub fn documentation_aside( - mut self, - side: DocumentationSide, - edge: DocumentationEdge, - render: impl Fn(&mut App) -> AnyElement + 'static, - ) -> Self { - self.documentation_aside = Some(DocumentationAside { - side, - edge, - render: Rc::new(render), - }); - - self - } -} - -impl FluentBuilder for ContextMenuEntry {} - -impl From for ContextMenuItem { - fn from(entry: ContextMenuEntry) -> Self { - ContextMenuItem::Entry(entry) - } -} - -pub struct ContextMenu { - builder: Option) -> Self>>, - items: Vec, - focus_handle: FocusHandle, - action_context: Option, - selected_index: Option, - delayed: bool, - clicked: bool, - end_slot_action: Option>, - key_context: SharedString, - _on_blur_subscription: Subscription, - keep_open_on_confirm: bool, - documentation_aside: Option<(usize, DocumentationAside)>, - fixed_width: Option, -} - -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum DocumentationSide { - Left, - Right, -} - -#[derive(Copy, Default, Clone, PartialEq, Eq)] -pub enum DocumentationEdge { - #[default] - Top, - Bottom, -} - -#[derive(Clone)] -pub struct DocumentationAside { - pub side: DocumentationSide, - pub edge: DocumentationEdge, - pub render: Rc AnyElement>, -} - -impl DocumentationAside { - pub fn new( - side: DocumentationSide, - edge: DocumentationEdge, - render: Rc AnyElement>, - ) -> Self { - Self { side, edge, render } - } -} - -impl Focusable for ContextMenu { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl EventEmitter for ContextMenu {} - -impl FluentBuilder for ContextMenu {} - -impl ContextMenu { - pub fn new( - window: &mut Window, - cx: &mut Context, - f: impl FnOnce(Self, &mut Window, &mut Context) -> Self, - ) -> Self { - let focus_handle = cx.focus_handle(); - let _on_blur_subscription = cx.on_blur( - &focus_handle, - window, - |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx), - ); - window.refresh(); - - f( - Self { - builder: None, - items: Default::default(), - focus_handle, - action_context: None, - selected_index: None, - delayed: false, - clicked: false, - key_context: "menu".into(), - _on_blur_subscription, - keep_open_on_confirm: false, - documentation_aside: None, - fixed_width: None, - end_slot_action: None, - }, - window, - cx, - ) - } - - pub fn build( - window: &mut Window, - cx: &mut App, - f: impl FnOnce(Self, &mut Window, &mut Context) -> Self, - ) -> Entity { - cx.new(|cx| Self::new(window, cx, f)) - } - - /// Builds a [`ContextMenu`] that will stay open when making changes instead of closing after each confirmation. - /// - /// The main difference from [`ContextMenu::build`] is the type of the `builder`, as we need to be able to hold onto - /// it to call it again. - pub fn build_persistent( - window: &mut Window, - cx: &mut App, - builder: impl Fn(Self, &mut Window, &mut Context) -> Self + 'static, - ) -> Entity { - cx.new(|cx| { - let builder = Rc::new(builder); - - let focus_handle = cx.focus_handle(); - let _on_blur_subscription = cx.on_blur( - &focus_handle, - window, - |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx), - ); - window.refresh(); - - (builder.clone())( - Self { - builder: Some(builder), - items: Default::default(), - focus_handle, - action_context: None, - selected_index: None, - delayed: false, - clicked: false, - key_context: "menu".into(), - _on_blur_subscription, - keep_open_on_confirm: true, - documentation_aside: None, - fixed_width: None, - end_slot_action: None, - }, - window, - cx, - ) - }) - } - - /// Rebuilds the menu. - /// - /// This is used to refresh the menu entries when entries are toggled when the menu is configured with - /// `keep_open_on_confirm = true`. - /// - /// This only works if the [`ContextMenu`] was constructed using [`ContextMenu::build_persistent`]. Otherwise it is - /// a no-op. - pub fn rebuild(&mut self, window: &mut Window, cx: &mut Context) { - let Some(builder) = self.builder.clone() else { - return; - }; - - // The way we rebuild the menu is a bit of a hack. - let focus_handle = cx.focus_handle(); - let new_menu = (builder.clone())( - Self { - builder: Some(builder), - items: Default::default(), - focus_handle: focus_handle.clone(), - action_context: None, - selected_index: None, - delayed: false, - clicked: false, - key_context: "menu".into(), - _on_blur_subscription: cx.on_blur( - &focus_handle, - window, - |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx), - ), - keep_open_on_confirm: false, - documentation_aside: None, - fixed_width: None, - end_slot_action: None, - }, - window, - cx, - ); - - self.items = new_menu.items; - - cx.notify(); - } - - pub fn context(mut self, focus: FocusHandle) -> Self { - self.action_context = Some(focus); - self - } - - pub fn header(mut self, title: impl Into) -> Self { - self.items.push(ContextMenuItem::Header(title.into())); - self - } - - pub fn header_with_link( - mut self, - title: impl Into, - link_label: impl Into, - link_url: impl Into, - ) -> Self { - self.items.push(ContextMenuItem::HeaderWithLink( - title.into(), - link_label.into(), - link_url.into(), - )); - self - } - - pub fn separator(mut self) -> Self { - self.items.push(ContextMenuItem::Separator); - self - } - - pub fn extend>(mut self, items: impl IntoIterator) -> Self { - self.items.extend(items.into_iter().map(Into::into)); - self - } - - pub fn item(mut self, item: impl Into) -> Self { - self.items.push(item.into()); - self - } - - pub fn push_item(&mut self, item: impl Into) { - self.items.push(item.into()); - } - - pub fn entry( - mut self, - label: impl Into, - action: Option>, - handler: impl Fn(&mut Window, &mut App) + 'static, - ) -> Self { - self.items.push(ContextMenuItem::Entry(ContextMenuEntry { - toggle: None, - label: label.into(), - handler: Rc::new(move |_, window, cx| handler(window, cx)), - icon: None, - custom_icon_path: None, - custom_icon_svg: None, - icon_position: IconPosition::End, - icon_size: IconSize::Small, - icon_color: None, - action, - disabled: false, - documentation_aside: None, - end_slot_icon: None, - end_slot_title: None, - end_slot_handler: None, - show_end_slot_on_hover: false, - })); - self - } - - pub fn entry_with_end_slot( - mut self, - label: impl Into, - action: Option>, - handler: impl Fn(&mut Window, &mut App) + 'static, - end_slot_icon: IconName, - end_slot_title: SharedString, - end_slot_handler: impl Fn(&mut Window, &mut App) + 'static, - ) -> Self { - self.items.push(ContextMenuItem::Entry(ContextMenuEntry { - toggle: None, - label: label.into(), - handler: Rc::new(move |_, window, cx| handler(window, cx)), - icon: None, - custom_icon_path: None, - custom_icon_svg: None, - icon_position: IconPosition::End, - icon_size: IconSize::Small, - icon_color: None, - action, - disabled: false, - documentation_aside: None, - end_slot_icon: Some(end_slot_icon), - end_slot_title: Some(end_slot_title), - end_slot_handler: Some(Rc::new(move |_, window, cx| end_slot_handler(window, cx))), - show_end_slot_on_hover: false, - })); - self - } - - pub fn entry_with_end_slot_on_hover( - mut self, - label: impl Into, - action: Option>, - handler: impl Fn(&mut Window, &mut App) + 'static, - end_slot_icon: IconName, - end_slot_title: SharedString, - end_slot_handler: impl Fn(&mut Window, &mut App) + 'static, - ) -> Self { - self.items.push(ContextMenuItem::Entry(ContextMenuEntry { - toggle: None, - label: label.into(), - handler: Rc::new(move |_, window, cx| handler(window, cx)), - icon: None, - custom_icon_path: None, - custom_icon_svg: None, - icon_position: IconPosition::End, - icon_size: IconSize::Small, - icon_color: None, - action, - disabled: false, - documentation_aside: None, - end_slot_icon: Some(end_slot_icon), - end_slot_title: Some(end_slot_title), - end_slot_handler: Some(Rc::new(move |_, window, cx| end_slot_handler(window, cx))), - show_end_slot_on_hover: true, - })); - self - } - - pub fn toggleable_entry( - mut self, - label: impl Into, - toggled: bool, - position: IconPosition, - action: Option>, - handler: impl Fn(&mut Window, &mut App) + 'static, - ) -> Self { - self.items.push(ContextMenuItem::Entry(ContextMenuEntry { - toggle: Some((position, toggled)), - label: label.into(), - handler: Rc::new(move |_, window, cx| handler(window, cx)), - icon: None, - custom_icon_path: None, - custom_icon_svg: None, - icon_position: position, - icon_size: IconSize::Small, - icon_color: None, - action, - disabled: false, - documentation_aside: None, - end_slot_icon: None, - end_slot_title: None, - end_slot_handler: None, - show_end_slot_on_hover: false, - })); - self - } - - pub fn custom_row( - mut self, - entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static, - ) -> Self { - self.items.push(ContextMenuItem::CustomEntry { - entry_render: Box::new(entry_render), - handler: Rc::new(|_, _, _| {}), - selectable: false, - documentation_aside: None, - }); - self - } - - pub fn custom_entry( - mut self, - entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static, - handler: impl Fn(&mut Window, &mut App) + 'static, - ) -> Self { - self.items.push(ContextMenuItem::CustomEntry { - entry_render: Box::new(entry_render), - handler: Rc::new(move |_, window, cx| handler(window, cx)), - selectable: true, - documentation_aside: None, - }); - self - } - - pub fn label(mut self, label: impl Into) -> Self { - self.items.push(ContextMenuItem::Label(label.into())); - self - } - - pub fn action(self, label: impl Into, action: Box) -> Self { - self.action_checked(label, action, false) - } - - pub fn action_checked( - mut self, - label: impl Into, - action: Box, - checked: bool, - ) -> Self { - self.items.push(ContextMenuItem::Entry(ContextMenuEntry { - toggle: if checked { - Some((IconPosition::Start, true)) - } else { - None - }, - label: label.into(), - action: Some(action.boxed_clone()), - handler: Rc::new(move |context, window, cx| { - if let Some(context) = &context { - window.focus(context); - } - window.dispatch_action(action.boxed_clone(), cx); - }), - icon: None, - custom_icon_path: None, - custom_icon_svg: None, - icon_position: IconPosition::End, - icon_size: IconSize::Small, - icon_color: None, - disabled: false, - documentation_aside: None, - end_slot_icon: None, - end_slot_title: None, - end_slot_handler: None, - show_end_slot_on_hover: false, - })); - self - } - - pub fn action_disabled_when( - mut self, - disabled: bool, - label: impl Into, - action: Box, - ) -> Self { - self.items.push(ContextMenuItem::Entry(ContextMenuEntry { - toggle: None, - label: label.into(), - action: Some(action.boxed_clone()), - handler: Rc::new(move |context, window, cx| { - if let Some(context) = &context { - window.focus(context); - } - window.dispatch_action(action.boxed_clone(), cx); - }), - icon: None, - custom_icon_path: None, - custom_icon_svg: None, - icon_size: IconSize::Small, - icon_position: IconPosition::End, - icon_color: None, - disabled, - documentation_aside: None, - end_slot_icon: None, - end_slot_title: None, - end_slot_handler: None, - show_end_slot_on_hover: false, - })); - self - } - - pub fn link(mut self, label: impl Into, action: Box) -> Self { - self.items.push(ContextMenuItem::Entry(ContextMenuEntry { - toggle: None, - label: label.into(), - action: Some(action.boxed_clone()), - handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)), - icon: Some(IconName::ArrowUpRight), - custom_icon_path: None, - custom_icon_svg: None, - icon_size: IconSize::XSmall, - icon_position: IconPosition::End, - icon_color: None, - disabled: false, - documentation_aside: None, - end_slot_icon: None, - end_slot_title: None, - end_slot_handler: None, - show_end_slot_on_hover: false, - })); - self - } - - pub fn keep_open_on_confirm(mut self, keep_open: bool) -> Self { - self.keep_open_on_confirm = keep_open; - self - } - - pub fn trigger_end_slot_handler(&mut self, window: &mut Window, cx: &mut Context) { - let Some(entry) = self.selected_index.and_then(|ix| self.items.get(ix)) else { - return; - }; - let ContextMenuItem::Entry(entry) = entry else { - return; - }; - let Some(handler) = entry.end_slot_handler.as_ref() else { - return; - }; - handler(None, window, cx); - } - - pub fn fixed_width(mut self, width: DefiniteLength) -> Self { - self.fixed_width = Some(width); - self - } - - pub fn end_slot_action(mut self, action: Box) -> Self { - self.end_slot_action = Some(action); - self - } - - pub fn key_context(mut self, context: impl Into) -> Self { - self.key_context = context.into(); - self - } - - pub fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { - let context = self.action_context.as_ref(); - if let Some( - ContextMenuItem::Entry(ContextMenuEntry { - handler, - disabled: false, - .. - }) - | ContextMenuItem::CustomEntry { handler, .. }, - ) = self.selected_index.and_then(|ix| self.items.get(ix)) - { - (handler)(context, window, cx) - } - - if self.keep_open_on_confirm { - self.rebuild(window, cx); - } else { - cx.emit(DismissEvent); - } - } - - pub fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { - cx.emit(DismissEvent); - cx.emit(DismissEvent); - } - - pub fn end_slot(&mut self, _: &dyn Action, window: &mut Window, cx: &mut Context) { - let Some(item) = self.selected_index.and_then(|ix| self.items.get(ix)) else { - return; - }; - let ContextMenuItem::Entry(entry) = item else { - return; - }; - let Some(handler) = entry.end_slot_handler.as_ref() else { - return; - }; - handler(None, window, cx); - self.rebuild(window, cx); - cx.notify(); - } - - pub fn clear_selected(&mut self) { - self.selected_index = None; - } - - pub fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context) { - if let Some(ix) = self.items.iter().position(|item| item.is_selectable()) { - self.select_index(ix, window, cx); - } - cx.notify(); - } - - pub fn select_last(&mut self, window: &mut Window, cx: &mut Context) -> Option { - for (ix, item) in self.items.iter().enumerate().rev() { - if item.is_selectable() { - return self.select_index(ix, window, cx); - } - } - None - } - - fn handle_select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context) { - if self.select_last(window, cx).is_some() { - cx.notify(); - } - } - - pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context) { - if let Some(ix) = self.selected_index { - let next_index = ix + 1; - if self.items.len() <= next_index { - self.select_first(&SelectFirst, window, cx); - return; - } else { - for (ix, item) in self.items.iter().enumerate().skip(next_index) { - if item.is_selectable() { - self.select_index(ix, window, cx); - cx.notify(); - return; - } - } - } - } - self.select_first(&SelectFirst, window, cx); - } - - pub fn select_previous( - &mut self, - _: &SelectPrevious, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(ix) = self.selected_index { - for (ix, item) in self.items.iter().enumerate().take(ix).rev() { - if item.is_selectable() { - self.select_index(ix, window, cx); - cx.notify(); - return; - } - } - } - self.handle_select_last(&SelectLast, window, cx); - } - - fn select_index( - &mut self, - ix: usize, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - self.documentation_aside = None; - let item = self.items.get(ix)?; - if item.is_selectable() { - self.selected_index = Some(ix); - match item { - ContextMenuItem::Entry(entry) => { - if let Some(callback) = &entry.documentation_aside { - self.documentation_aside = Some((ix, callback.clone())); - } - } - ContextMenuItem::CustomEntry { - documentation_aside: Some(callback), - .. - } => { - self.documentation_aside = Some((ix, callback.clone())); - } - _ => (), - } - } - Some(ix) - } - - pub fn on_action_dispatch( - &mut self, - dispatched: &dyn Action, - window: &mut Window, - cx: &mut Context, - ) { - if self.clicked { - cx.propagate(); - return; - } - - if let Some(ix) = self.items.iter().position(|item| { - if let ContextMenuItem::Entry(ContextMenuEntry { - action: Some(action), - disabled: false, - .. - }) = item - { - action.partial_eq(dispatched) - } else { - false - } - }) { - self.select_index(ix, window, cx); - self.delayed = true; - cx.notify(); - let action = dispatched.boxed_clone(); - cx.spawn_in(window, async move |this, cx| { - cx.background_executor() - .timer(Duration::from_millis(50)) - .await; - cx.update(|window, cx| { - this.update(cx, |this, cx| { - this.cancel(&menu::Cancel, window, cx); - window.dispatch_action(action, cx); - }) - }) - }) - .detach_and_log_err(cx); - } else { - cx.propagate() - } - } - - pub fn on_blur_subscription(mut self, new_subscription: Subscription) -> Self { - self._on_blur_subscription = new_subscription; - self - } - - fn render_menu_item( - &self, - ix: usize, - item: &ContextMenuItem, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement + use<> { - match item { - ContextMenuItem::Separator => ListSeparator.into_any_element(), - ContextMenuItem::Header(header) => ListSubHeader::new(header.clone()) - .inset(true) - .into_any_element(), - ContextMenuItem::HeaderWithLink(header, label, url) => { - let url = url.clone(); - let link_id = ElementId::Name(format!("link-{}", url).into()); - ListSubHeader::new(header.clone()) - .inset(true) - .end_slot( - Button::new(link_id, label.clone()) - .color(Color::Muted) - .label_size(LabelSize::Small) - .size(ButtonSize::None) - .style(ButtonStyle::Transparent) - .on_click(move |_, _, cx| { - let url = url.clone(); - cx.open_url(&url); - }) - .into_any_element(), - ) - .into_any_element() - } - ContextMenuItem::Label(label) => ListItem::new(ix) - .inset(true) - .disabled(true) - .child(Label::new(label.clone())) - .into_any_element(), - ContextMenuItem::Entry(entry) => { - self.render_menu_entry(ix, entry, cx).into_any_element() - } - ContextMenuItem::CustomEntry { - entry_render, - handler, - selectable, - .. - } => { - let handler = handler.clone(); - let menu = cx.entity().downgrade(); - let selectable = *selectable; - ListItem::new(ix) - .inset(true) - .toggle_state(if selectable { - Some(ix) == self.selected_index - } else { - false - }) - .selectable(selectable) - .when(selectable, |item| { - item.on_click({ - let context = self.action_context.clone(); - let keep_open_on_confirm = self.keep_open_on_confirm; - move |_, window, cx| { - handler(context.as_ref(), window, cx); - menu.update(cx, |menu, cx| { - menu.clicked = true; - - if keep_open_on_confirm { - menu.rebuild(window, cx); - } else { - cx.emit(DismissEvent); - } - }) - .ok(); - } - }) - }) - .child(entry_render(window, cx)) - .into_any_element() - } - } - } - - fn render_menu_entry( - &self, - ix: usize, - entry: &ContextMenuEntry, - cx: &mut Context, - ) -> impl IntoElement { - let ContextMenuEntry { - toggle, - label, - handler, - icon, - custom_icon_path, - custom_icon_svg, - icon_position, - icon_size, - icon_color, - action, - disabled, - documentation_aside, - end_slot_icon, - end_slot_title, - end_slot_handler, - show_end_slot_on_hover, - } = entry; - let this = cx.weak_entity(); - - let handler = handler.clone(); - let menu = cx.entity().downgrade(); - - let icon_color = if *disabled { - Color::Muted - } else if toggle.is_some() { - icon_color.unwrap_or(Color::Accent) - } else { - icon_color.unwrap_or(Color::Default) - }; - - let label_color = if *disabled { - Color::Disabled - } else { - Color::Default - }; - - let label_element = if let Some(custom_path) = custom_icon_path { - h_flex() - .gap_1p5() - .when( - *icon_position == IconPosition::Start && toggle.is_none(), - |flex| { - flex.child( - Icon::from_path(custom_path.clone()) - .size(*icon_size) - .color(icon_color), - ) - }, - ) - .child(Label::new(label.clone()).color(label_color).truncate()) - .when(*icon_position == IconPosition::End, |flex| { - flex.child( - Icon::from_path(custom_path.clone()) - .size(*icon_size) - .color(icon_color), - ) - }) - .into_any_element() - } else if let Some(custom_icon_svg) = custom_icon_svg { - h_flex() - .gap_1p5() - .when( - *icon_position == IconPosition::Start && toggle.is_none(), - |flex| { - flex.child( - Icon::from_external_svg(custom_icon_svg.clone()) - .size(*icon_size) - .color(icon_color), - ) - }, - ) - .child(Label::new(label.clone()).color(label_color).truncate()) - .when(*icon_position == IconPosition::End, |flex| { - flex.child( - Icon::from_external_svg(custom_icon_svg.clone()) - .size(*icon_size) - .color(icon_color), - ) - }) - .into_any_element() - } else if let Some(icon_name) = icon { - h_flex() - .gap_1p5() - .when( - *icon_position == IconPosition::Start && toggle.is_none(), - |flex| flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color)), - ) - .child(Label::new(label.clone()).color(label_color).truncate()) - .when(*icon_position == IconPosition::End, |flex| { - flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color)) - }) - .into_any_element() - } else { - Label::new(label.clone()) - .color(label_color) - .truncate() - .into_any_element() - }; - - div() - .id(("context-menu-child", ix)) - .when_some(documentation_aside.clone(), |this, documentation_aside| { - this.occlude() - .on_hover(cx.listener(move |menu, hovered, _, cx| { - if *hovered { - menu.documentation_aside = Some((ix, documentation_aside.clone())); - } else if matches!(menu.documentation_aside, Some((id, _)) if id == ix) { - menu.documentation_aside = None; - } - cx.notify(); - })) - }) - .child( - ListItem::new(ix) - .group_name("label_container") - .inset(true) - .disabled(*disabled) - .toggle_state(Some(ix) == self.selected_index) - .when_some(*toggle, |list_item, (position, toggled)| { - let contents = div() - .flex_none() - .child( - Icon::new(icon.unwrap_or(IconName::Check)) - .color(icon_color) - .size(*icon_size), - ) - .when(!toggled, |contents| contents.invisible()); - - match position { - IconPosition::Start => list_item.start_slot(contents), - IconPosition::End => list_item.end_slot(contents), - } - }) - .child( - h_flex() - .w_full() - .justify_between() - .child(label_element) - .debug_selector(|| format!("MENU_ITEM-{}", label)) - .children(action.as_ref().map(|action| { - let binding = self - .action_context - .as_ref() - .map(|focus| KeyBinding::for_action_in(&**action, focus, cx)) - .unwrap_or_else(|| KeyBinding::for_action(&**action, cx)); - - div() - .ml_4() - .child(binding.disabled(*disabled)) - .when(*disabled && documentation_aside.is_some(), |parent| { - parent.invisible() - }) - })) - .when(*disabled && documentation_aside.is_some(), |parent| { - parent.child( - Icon::new(IconName::Info) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - }), - ) - .when_some( - end_slot_icon - .as_ref() - .zip(self.end_slot_action.as_ref()) - .zip(end_slot_title.as_ref()) - .zip(end_slot_handler.as_ref()), - |el, (((icon, action), title), handler)| { - el.end_slot({ - let icon_button = IconButton::new("end-slot-icon", *icon) - .shape(IconButtonShape::Square) - .tooltip({ - let action_context = self.action_context.clone(); - let title = title.clone(); - let action = action.boxed_clone(); - move |_window, cx| { - action_context - .as_ref() - .map(|focus| { - Tooltip::for_action_in( - title.clone(), - &*action, - focus, - cx, - ) - }) - .unwrap_or_else(|| { - Tooltip::for_action(title.clone(), &*action, cx) - }) - } - }) - .on_click({ - let handler = handler.clone(); - move |_, window, cx| { - handler(None, window, cx); - this.update(cx, |this, cx| { - this.rebuild(window, cx); - cx.notify(); - }) - .ok(); - } - }); - - if *show_end_slot_on_hover { - div() - .visible_on_hover("label_container") - .child(icon_button) - .into_any_element() - } else { - icon_button.into_any_element() - } - }) - }, - ) - .on_click({ - let context = self.action_context.clone(); - let keep_open_on_confirm = self.keep_open_on_confirm; - move |_, window, cx| { - handler(context.as_ref(), window, cx); - menu.update(cx, |menu, cx| { - menu.clicked = true; - if keep_open_on_confirm { - menu.rebuild(window, cx); - } else { - cx.emit(DismissEvent); - } - }) - .ok(); - } - }), - ) - .into_any_element() - } -} - -impl ContextMenuItem { - fn is_selectable(&self) -> bool { - match self { - ContextMenuItem::Header(_) - | ContextMenuItem::HeaderWithLink(_, _, _) - | ContextMenuItem::Separator - | ContextMenuItem::Label { .. } => false, - ContextMenuItem::Entry(ContextMenuEntry { disabled, .. }) => !disabled, - ContextMenuItem::CustomEntry { selectable, .. } => *selectable, - } - } -} - -impl Render for ContextMenu { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx); - let window_size = window.viewport_size(); - let rem_size = window.rem_size(); - let is_wide_window = window_size.width / rem_size > rems_from_px(800.).0; - - let aside = self.documentation_aside.clone(); - let render_aside = |aside: DocumentationAside, cx: &mut Context| { - WithRemSize::new(ui_font_size) - .occlude() - .elevation_2(cx) - .w_full() - .p_2() - .overflow_hidden() - .when(is_wide_window, |this| this.max_w_96()) - .when(!is_wide_window, |this| this.max_w_48()) - .child((aside.render)(cx)) - }; - - let render_menu = - |cx: &mut Context, window: &mut Window| { - WithRemSize::new(ui_font_size) - .occlude() - .elevation_2(cx) - .flex() - .flex_row() - .flex_shrink_0() - .child( - v_flex() - .id("context-menu") - .max_h(vh(0.75, window)) - .flex_shrink_0() - .when_some(self.fixed_width, |this, width| { - this.w(width).overflow_x_hidden() - }) - .when(self.fixed_width.is_none(), |this| { - this.min_w(px(200.)).flex_1() - }) - .overflow_y_scroll() - .track_focus(&self.focus_handle(cx)) - .on_mouse_down_out(cx.listener(|this, _, window, cx| { - this.cancel(&menu::Cancel, window, cx) - })) - .key_context(self.key_context.as_ref()) - .on_action(cx.listener(ContextMenu::select_first)) - .on_action(cx.listener(ContextMenu::handle_select_last)) - .on_action(cx.listener(ContextMenu::select_next)) - .on_action(cx.listener(ContextMenu::select_previous)) - .on_action(cx.listener(ContextMenu::confirm)) - .on_action(cx.listener(ContextMenu::cancel)) - .when_some(self.end_slot_action.as_ref(), |el, action| { - el.on_boxed_action(&**action, cx.listener(ContextMenu::end_slot)) - }) - .when(!self.delayed, |mut el| { - for item in self.items.iter() { - if let ContextMenuItem::Entry(ContextMenuEntry { - action: Some(action), - disabled: false, - .. - }) = item - { - el = el.on_boxed_action( - &**action, - cx.listener(ContextMenu::on_action_dispatch), - ); - } - } - el - }) - .child( - List::new().children( - self.items.iter().enumerate().map(|(ix, item)| { - self.render_menu_item(ix, item, window, cx) - }), - ), - ), - ) - }; - - if is_wide_window { - div() - .relative() - .child(render_menu(cx, window)) - .children(aside.map(|(_item_index, aside)| { - h_flex() - .absolute() - .when(aside.side == DocumentationSide::Left, |this| { - this.right_full().mr_1() - }) - .when(aside.side == DocumentationSide::Right, |this| { - this.left_full().ml_1() - }) - .when(aside.edge == DocumentationEdge::Top, |this| this.top_0()) - .when(aside.edge == DocumentationEdge::Bottom, |this| { - this.bottom_0() - }) - .child(render_aside(aside, cx)) - })) - } else { - v_flex() - .w_full() - .gap_1() - .justify_end() - .children(aside.map(|(_, aside)| render_aside(aside, cx))) - .child(render_menu(cx, window)) - } - } -} - -#[cfg(test)] -mod tests { - use gpui::TestAppContext; - - use super::*; - - #[gpui::test] - fn can_navigate_back_over_headers(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - let context_menu = cx.update(|window, cx| { - ContextMenu::build(window, cx, |menu, _, _| { - menu.header("First header") - .separator() - .entry("First entry", None, |_, _| {}) - .separator() - .separator() - .entry("Last entry", None, |_, _| {}) - .header("Last header") - }) - }); - - context_menu.update_in(cx, |context_menu, window, cx| { - assert_eq!( - None, context_menu.selected_index, - "No selection is in the menu initially" - ); - - context_menu.select_first(&SelectFirst, window, cx); - assert_eq!( - Some(2), - context_menu.selected_index, - "Should select first selectable entry, skipping the header and the separator" - ); - - context_menu.select_next(&SelectNext, window, cx); - assert_eq!( - Some(5), - context_menu.selected_index, - "Should select next selectable entry, skipping 2 separators along the way" - ); - - context_menu.select_next(&SelectNext, window, cx); - assert_eq!( - Some(2), - context_menu.selected_index, - "Should wrap around to first selectable entry" - ); - }); - - context_menu.update_in(cx, |context_menu, window, cx| { - assert_eq!( - Some(2), - context_menu.selected_index, - "Should start from the first selectable entry" - ); - - context_menu.select_previous(&SelectPrevious, window, cx); - assert_eq!( - Some(5), - context_menu.selected_index, - "Should wrap around to previous selectable entry (last)" - ); - - context_menu.select_previous(&SelectPrevious, window, cx); - assert_eq!( - Some(2), - context_menu.selected_index, - "Should go back to previous selectable entry (first)" - ); - }); - - context_menu.update_in(cx, |context_menu, window, cx| { - context_menu.select_first(&SelectFirst, window, cx); - assert_eq!( - Some(2), - context_menu.selected_index, - "Should start from the first selectable entry" - ); - - context_menu.select_previous(&SelectPrevious, window, cx); - assert_eq!( - Some(5), - context_menu.selected_index, - "Should wrap around to last selectable entry" - ); - context_menu.select_next(&SelectNext, window, cx); - assert_eq!( - Some(2), - context_menu.selected_index, - "Should wrap around to first selectable entry" - ); - }); - } -} diff --git a/crates/ui/src/components/data_table.rs b/crates/ui/src/components/data_table.rs deleted file mode 100644 index 9cd2a5cb7a..0000000000 --- a/crates/ui/src/components/data_table.rs +++ /dev/null @@ -1,1390 +0,0 @@ -use std::{ops::Range, rc::Rc}; - -use gpui::{ - AbsoluteLength, AppContext, Context, DefiniteLength, DragMoveEvent, Entity, EntityId, - FocusHandle, Length, ListHorizontalSizingBehavior, ListSizingBehavior, Point, Stateful, - UniformListScrollHandle, WeakEntity, transparent_black, uniform_list, -}; - -use crate::{ - ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component, - ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator, - InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce, - ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement, Styled, StyledExt as _, - StyledTypography, Window, WithScrollbar, div, example_group_with_title, h_flex, px, - single_example, v_flex, -}; -use itertools::intersperse_with; - -const RESIZE_COLUMN_WIDTH: f32 = 8.0; - -#[derive(Debug)] -struct DraggedColumn(usize); - -struct UniformListData { - render_item_fn: Box, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>>, - element_id: ElementId, - row_count: usize, -} - -enum TableContents { - Vec(Vec<[AnyElement; COLS]>), - UniformList(UniformListData), -} - -impl TableContents { - fn rows_mut(&mut self) -> Option<&mut Vec<[AnyElement; COLS]>> { - match self { - TableContents::Vec(rows) => Some(rows), - TableContents::UniformList(_) => None, - } - } - - fn len(&self) -> usize { - match self { - TableContents::Vec(rows) => rows.len(), - TableContents::UniformList(data) => data.row_count, - } - } - - fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -pub struct TableInteractionState { - pub focus_handle: FocusHandle, - pub scroll_handle: UniformListScrollHandle, - pub custom_scrollbar: Option, -} - -impl TableInteractionState { - pub fn new(cx: &mut App) -> Self { - Self { - focus_handle: cx.focus_handle(), - scroll_handle: UniformListScrollHandle::new(), - custom_scrollbar: None, - } - } - - pub fn with_custom_scrollbar(mut self, custom_scrollbar: Scrollbars) -> Self { - self.custom_scrollbar = Some(custom_scrollbar); - self - } - - pub fn scroll_offset(&self) -> Point { - self.scroll_handle.offset() - } - - pub fn set_scroll_offset(&self, offset: Point) { - self.scroll_handle.set_offset(offset); - } - - pub fn listener( - this: &Entity, - f: impl Fn(&mut Self, &E, &mut Window, &mut Context) + 'static, - ) -> impl Fn(&E, &mut Window, &mut App) + 'static { - let view = this.downgrade(); - move |e: &E, window: &mut Window, cx: &mut App| { - view.update(cx, |view, cx| f(view, e, window, cx)).ok(); - } - } - - fn render_resize_handles( - &self, - column_widths: &[Length; COLS], - resizable_columns: &[TableResizeBehavior; COLS], - initial_sizes: [DefiniteLength; COLS], - columns: Option>>, - window: &mut Window, - cx: &mut App, - ) -> AnyElement { - let spacers = column_widths - .iter() - .map(|width| base_cell_style(Some(*width)).into_any_element()); - - let mut column_ix = 0; - let resizable_columns_slice = *resizable_columns; - let mut resizable_columns = resizable_columns.iter(); - - let dividers = intersperse_with(spacers, || { - window.with_id(column_ix, |window| { - let mut resize_divider = div() - // This is required because this is evaluated at a different time than the use_state call above - .id(column_ix) - .relative() - .top_0() - .w_px() - .h_full() - .bg(cx.theme().colors().border.opacity(0.8)); - - let mut resize_handle = div() - .id("column-resize-handle") - .absolute() - .left_neg_0p5() - .w(px(RESIZE_COLUMN_WIDTH)) - .h_full(); - - if resizable_columns - .next() - .is_some_and(TableResizeBehavior::is_resizable) - { - let hovered = window.use_state(cx, |_window, _cx| false); - - resize_divider = resize_divider.when(*hovered.read(cx), |div| { - div.bg(cx.theme().colors().border_focused) - }); - - resize_handle = resize_handle - .on_hover(move |&was_hovered, _, cx| hovered.write(cx, was_hovered)) - .cursor_col_resize() - .when_some(columns.clone(), |this, columns| { - this.on_click(move |event, window, cx| { - if event.click_count() >= 2 { - columns.update(cx, |columns, _| { - columns.on_double_click( - column_ix, - &initial_sizes, - &resizable_columns_slice, - window, - ); - }) - } - - cx.stop_propagation(); - }) - }) - .on_drag(DraggedColumn(column_ix), |_, _offset, _window, cx| { - cx.new(|_cx| gpui::Empty) - }) - } - - column_ix += 1; - resize_divider.child(resize_handle).into_any_element() - }) - }); - - h_flex() - .id("resize-handles") - .absolute() - .inset_0() - .w_full() - .children(dividers) - .into_any_element() - } -} - -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum TableResizeBehavior { - None, - Resizable, - MinSize(f32), -} - -impl TableResizeBehavior { - pub fn is_resizable(&self) -> bool { - *self != TableResizeBehavior::None - } - - pub fn min_size(&self) -> Option { - match self { - TableResizeBehavior::None => None, - TableResizeBehavior::Resizable => Some(0.05), - TableResizeBehavior::MinSize(min_size) => Some(*min_size), - } - } -} - -pub struct TableColumnWidths { - widths: [DefiniteLength; COLS], - visible_widths: [DefiniteLength; COLS], - cached_bounds_width: Pixels, - initialized: bool, -} - -impl TableColumnWidths { - pub fn new(_: &mut App) -> Self { - Self { - widths: [DefiniteLength::default(); COLS], - visible_widths: [DefiniteLength::default(); COLS], - cached_bounds_width: Default::default(), - initialized: false, - } - } - - fn get_fraction(length: &DefiniteLength, bounds_width: Pixels, rem_size: Pixels) -> f32 { - match length { - DefiniteLength::Absolute(AbsoluteLength::Pixels(pixels)) => *pixels / bounds_width, - DefiniteLength::Absolute(AbsoluteLength::Rems(rems_width)) => { - rems_width.to_pixels(rem_size) / bounds_width - } - DefiniteLength::Fraction(fraction) => *fraction, - } - } - - fn on_double_click( - &mut self, - double_click_position: usize, - initial_sizes: &[DefiniteLength; COLS], - resize_behavior: &[TableResizeBehavior; COLS], - window: &mut Window, - ) { - let bounds_width = self.cached_bounds_width; - let rem_size = window.rem_size(); - let initial_sizes = - initial_sizes.map(|length| Self::get_fraction(&length, bounds_width, rem_size)); - let widths = self - .widths - .map(|length| Self::get_fraction(&length, bounds_width, rem_size)); - - let updated_widths = Self::reset_to_initial_size( - double_click_position, - widths, - initial_sizes, - resize_behavior, - ); - self.widths = updated_widths.map(DefiniteLength::Fraction); - self.visible_widths = self.widths; - } - - fn reset_to_initial_size( - col_idx: usize, - mut widths: [f32; COLS], - initial_sizes: [f32; COLS], - resize_behavior: &[TableResizeBehavior; COLS], - ) -> [f32; COLS] { - // RESET: - // Part 1: - // Figure out if we should shrink/grow the selected column - // Get diff which represents the change in column we want to make initial size delta curr_size = diff - // - // Part 2: We need to decide which side column we should move and where - // - // If we want to grow our column we should check the left/right columns diff to see what side - // has a greater delta than their initial size. Likewise, if we shrink our column we should check - // the left/right column diffs to see what side has the smallest delta. - // - // Part 3: resize - // - // col_idx represents the column handle to the right of an active column - // - // If growing and right has the greater delta { - // shift col_idx to the right - // } else if growing and left has the greater delta { - // shift col_idx - 1 to the left - // } else if shrinking and the right has the greater delta { - // shift - // } { - // - // } - // } - // - // if we need to shrink, then if the right - // - - // DRAGGING - // we get diff which represents the change in the _drag handle_ position - // -diff => dragging left -> - // grow the column to the right of the handle as much as we can shrink columns to the left of the handle - // +diff => dragging right -> growing handles column - // grow the column to the left of the handle as much as we can shrink columns to the right of the handle - // - - let diff = initial_sizes[col_idx] - widths[col_idx]; - - let left_diff = - initial_sizes[..col_idx].iter().sum::() - widths[..col_idx].iter().sum::(); - let right_diff = initial_sizes[col_idx + 1..].iter().sum::() - - widths[col_idx + 1..].iter().sum::(); - - let go_left_first = if diff < 0.0 { - left_diff > right_diff - } else { - left_diff < right_diff - }; - - if !go_left_first { - let diff_remaining = - Self::propagate_resize_diff(diff, col_idx, &mut widths, resize_behavior, 1); - - if diff_remaining != 0.0 && col_idx > 0 { - Self::propagate_resize_diff( - diff_remaining, - col_idx, - &mut widths, - resize_behavior, - -1, - ); - } - } else { - let diff_remaining = - Self::propagate_resize_diff(diff, col_idx, &mut widths, resize_behavior, -1); - - if diff_remaining != 0.0 { - Self::propagate_resize_diff( - diff_remaining, - col_idx, - &mut widths, - resize_behavior, - 1, - ); - } - } - - widths - } - - fn on_drag_move( - &mut self, - drag_event: &DragMoveEvent, - resize_behavior: &[TableResizeBehavior; COLS], - window: &mut Window, - cx: &mut Context, - ) { - let drag_position = drag_event.event.position; - let bounds = drag_event.bounds; - - let mut col_position = 0.0; - let rem_size = window.rem_size(); - let bounds_width = bounds.right() - bounds.left(); - let col_idx = drag_event.drag(cx).0; - - let column_handle_width = Self::get_fraction( - &DefiniteLength::Absolute(AbsoluteLength::Pixels(px(RESIZE_COLUMN_WIDTH))), - bounds_width, - rem_size, - ); - - let mut widths = self - .widths - .map(|length| Self::get_fraction(&length, bounds_width, rem_size)); - - for length in widths[0..=col_idx].iter() { - col_position += length + column_handle_width; - } - - let mut total_length_ratio = col_position; - for length in widths[col_idx + 1..].iter() { - total_length_ratio += length; - } - total_length_ratio += (COLS - 1 - col_idx) as f32 * column_handle_width; - - let drag_fraction = (drag_position.x - bounds.left()) / bounds_width; - let drag_fraction = drag_fraction * total_length_ratio; - let diff = drag_fraction - col_position - column_handle_width / 2.0; - - Self::drag_column_handle(diff, col_idx, &mut widths, resize_behavior); - - self.visible_widths = widths.map(DefiniteLength::Fraction); - } - - fn drag_column_handle( - diff: f32, - col_idx: usize, - widths: &mut [f32; COLS], - resize_behavior: &[TableResizeBehavior; COLS], - ) { - // if diff > 0.0 then go right - if diff > 0.0 { - Self::propagate_resize_diff(diff, col_idx, widths, resize_behavior, 1); - } else { - Self::propagate_resize_diff(-diff, col_idx + 1, widths, resize_behavior, -1); - } - } - - fn propagate_resize_diff( - diff: f32, - col_idx: usize, - widths: &mut [f32; COLS], - resize_behavior: &[TableResizeBehavior; COLS], - direction: i8, - ) -> f32 { - let mut diff_remaining = diff; - if resize_behavior[col_idx].min_size().is_none() { - return diff; - } - - let step_right; - let step_left; - if direction < 0 { - step_right = 0; - step_left = 1; - } else { - step_right = 1; - step_left = 0; - } - if col_idx == 0 && direction < 0 { - return diff; - } - let mut curr_column = col_idx + step_right - step_left; - - while diff_remaining != 0.0 && curr_column < COLS { - let Some(min_size) = resize_behavior[curr_column].min_size() else { - if curr_column == 0 { - break; - } - curr_column -= step_left; - curr_column += step_right; - continue; - }; - - let curr_width = widths[curr_column] - diff_remaining; - widths[curr_column] = curr_width; - - if min_size > curr_width { - diff_remaining = min_size - curr_width; - widths[curr_column] = min_size; - } else { - diff_remaining = 0.0; - break; - } - if curr_column == 0 { - break; - } - curr_column -= step_left; - curr_column += step_right; - } - widths[col_idx] = widths[col_idx] + (diff - diff_remaining); - - diff_remaining - } -} - -pub struct TableWidths { - initial: [DefiniteLength; COLS], - current: Option>>, - resizable: [TableResizeBehavior; COLS], -} - -impl TableWidths { - pub fn new(widths: [impl Into; COLS]) -> Self { - let widths = widths.map(Into::into); - - TableWidths { - initial: widths, - current: None, - resizable: [TableResizeBehavior::None; COLS], - } - } - - fn lengths(&self, cx: &App) -> [Length; COLS] { - self.current - .as_ref() - .map(|entity| entity.read(cx).visible_widths.map(Length::Definite)) - .unwrap_or(self.initial.map(Length::Definite)) - } -} - -/// A table component -#[derive(RegisterComponent, IntoElement)] -pub struct Table { - striped: bool, - width: Option, - headers: Option<[AnyElement; COLS]>, - rows: TableContents, - interaction_state: Option>, - col_widths: Option>, - map_row: Option), &mut Window, &mut App) -> AnyElement>>, - use_ui_font: bool, - empty_table_callback: Option AnyElement>>, -} - -impl Table { - /// number of headers provided. - pub fn new() -> Self { - Self { - striped: false, - width: None, - headers: None, - rows: TableContents::Vec(Vec::new()), - interaction_state: None, - map_row: None, - use_ui_font: true, - empty_table_callback: None, - col_widths: None, - } - } - - /// Enables uniform list rendering. - /// The provided function will be passed directly to the `uniform_list` element. - /// Therefore, if this method is called, any calls to [`Table::row`] before or after - /// this method is called will be ignored. - pub fn uniform_list( - mut self, - id: impl Into, - row_count: usize, - render_item_fn: impl Fn(Range, &mut Window, &mut App) -> Vec<[AnyElement; COLS]> - + 'static, - ) -> Self { - self.rows = TableContents::UniformList(UniformListData { - element_id: id.into(), - row_count, - render_item_fn: Box::new(render_item_fn), - }); - self - } - - /// Enables row striping. - pub fn striped(mut self) -> Self { - self.striped = true; - self - } - - /// Sets the width of the table. - /// Will enable horizontal scrolling if [`Self::interactable`] is also called. - pub fn width(mut self, width: impl Into) -> Self { - self.width = Some(width.into()); - self - } - - /// Enables interaction (primarily scrolling) with the table. - /// - /// Vertical scrolling will be enabled by default if the table is taller than its container. - /// - /// Horizontal scrolling will only be enabled if [`Self::width`] is also called, otherwise - /// the list will always shrink the table columns to fit their contents I.e. If [`Self::uniform_list`] - /// is used without a width and with [`Self::interactable`], the [`ListHorizontalSizingBehavior`] will - /// be set to [`ListHorizontalSizingBehavior::FitList`]. - pub fn interactable(mut self, interaction_state: &Entity) -> Self { - self.interaction_state = Some(interaction_state.downgrade()); - self - } - - pub fn header(mut self, headers: [impl IntoElement; COLS]) -> Self { - self.headers = Some(headers.map(IntoElement::into_any_element)); - self - } - - pub fn row(mut self, items: [impl IntoElement; COLS]) -> Self { - if let Some(rows) = self.rows.rows_mut() { - rows.push(items.map(IntoElement::into_any_element)); - } - self - } - - pub fn column_widths(mut self, widths: [impl Into; COLS]) -> Self { - if self.col_widths.is_none() { - self.col_widths = Some(TableWidths::new(widths)); - } - self - } - - pub fn resizable_columns( - mut self, - resizable: [TableResizeBehavior; COLS], - column_widths: &Entity>, - cx: &mut App, - ) -> Self { - if let Some(table_widths) = self.col_widths.as_mut() { - table_widths.resizable = resizable; - let column_widths = table_widths - .current - .get_or_insert_with(|| column_widths.clone()); - - column_widths.update(cx, |widths, _| { - if !widths.initialized { - widths.initialized = true; - widths.widths = table_widths.initial; - widths.visible_widths = widths.widths; - } - }) - } - self - } - - pub fn no_ui_font(mut self) -> Self { - self.use_ui_font = false; - self - } - - pub fn map_row( - mut self, - callback: impl Fn((usize, Stateful
), &mut Window, &mut App) -> AnyElement + 'static, - ) -> Self { - self.map_row = Some(Rc::new(callback)); - self - } - - /// Provide a callback that is invoked when the table is rendered without any rows - pub fn empty_table_callback( - mut self, - callback: impl Fn(&mut Window, &mut App) -> AnyElement + 'static, - ) -> Self { - self.empty_table_callback = Some(Rc::new(callback)); - self - } -} - -fn base_cell_style(width: Option) -> Div { - div() - .px_1p5() - .when_some(width, |this, width| this.w(width)) - .when(width.is_none(), |this| this.flex_1()) - .whitespace_nowrap() - .text_ellipsis() - .overflow_hidden() -} - -fn base_cell_style_text(width: Option, use_ui_font: bool, cx: &App) -> Div { - base_cell_style(width).when(use_ui_font, |el| el.text_ui(cx)) -} - -pub fn render_table_row( - row_index: usize, - items: [impl IntoElement; COLS], - table_context: TableRenderContext, - window: &mut Window, - cx: &mut App, -) -> AnyElement { - let is_striped = table_context.striped; - let is_last = row_index == table_context.total_row_count - 1; - let bg = if row_index % 2 == 1 && is_striped { - Some(cx.theme().colors().text.opacity(0.05)) - } else { - None - }; - let column_widths = table_context - .column_widths - .map_or([None; COLS], |widths| widths.map(Some)); - - let mut row = h_flex() - .id(("table_row", row_index)) - .size_full() - .when_some(bg, |row, bg| row.bg(bg)) - .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.6))) - .when(!is_striped, |row| { - row.border_b_1() - .border_color(transparent_black()) - .when(!is_last, |row| row.border_color(cx.theme().colors().border)) - }); - - row = row.children( - items - .map(IntoElement::into_any_element) - .into_iter() - .zip(column_widths) - .map(|(cell, width)| { - base_cell_style_text(width, table_context.use_ui_font, cx) - .px_1() - .py_0p5() - .child(cell) - }), - ); - - let row = if let Some(map_row) = table_context.map_row { - map_row((row_index, row), window, cx) - } else { - row.into_any_element() - }; - - div().size_full().child(row).into_any_element() -} - -pub fn render_table_header( - headers: [impl IntoElement; COLS], - table_context: TableRenderContext, - columns_widths: Option<( - WeakEntity>, - [TableResizeBehavior; COLS], - [DefiniteLength; COLS], - )>, - entity_id: Option, - cx: &mut App, -) -> impl IntoElement { - let column_widths = table_context - .column_widths - .map_or([None; COLS], |widths| widths.map(Some)); - - let element_id = entity_id - .map(|entity| entity.to_string()) - .unwrap_or_default(); - - let shared_element_id: SharedString = format!("table-{}", element_id).into(); - - div() - .flex() - .flex_row() - .items_center() - .justify_between() - .w_full() - .p_2() - .border_b_1() - .border_color(cx.theme().colors().border) - .children(headers.into_iter().enumerate().zip(column_widths).map( - |((header_idx, h), width)| { - base_cell_style_text(width, table_context.use_ui_font, cx) - .child(h) - .id(ElementId::NamedInteger( - shared_element_id.clone(), - header_idx as u64, - )) - .when_some( - columns_widths.as_ref().cloned(), - |this, (column_widths, resizables, initial_sizes)| { - if resizables[header_idx].is_resizable() { - this.on_click(move |event, window, cx| { - if event.click_count() > 1 { - column_widths - .update(cx, |column, _| { - column.on_double_click( - header_idx, - &initial_sizes, - &resizables, - window, - ); - }) - .ok(); - } - }) - } else { - this - } - }, - ) - }, - )) -} - -#[derive(Clone)] -pub struct TableRenderContext { - pub striped: bool, - pub total_row_count: usize, - pub column_widths: Option<[Length; COLS]>, - pub map_row: Option), &mut Window, &mut App) -> AnyElement>>, - pub use_ui_font: bool, -} - -impl TableRenderContext { - fn new(table: &Table, cx: &App) -> Self { - Self { - striped: table.striped, - total_row_count: table.rows.len(), - column_widths: table.col_widths.as_ref().map(|widths| widths.lengths(cx)), - map_row: table.map_row.clone(), - use_ui_font: table.use_ui_font, - } - } -} - -impl RenderOnce for Table { - fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let table_context = TableRenderContext::new(&self, cx); - let interaction_state = self.interaction_state.and_then(|state| state.upgrade()); - let current_widths = self - .col_widths - .as_ref() - .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable))) - .map(|(curr, resize_behavior)| (curr.downgrade(), resize_behavior)); - - let current_widths_with_initial_sizes = self - .col_widths - .as_ref() - .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable, widths.initial))) - .map(|(curr, resize_behavior, initial)| (curr.downgrade(), resize_behavior, initial)); - - let width = self.width; - let no_rows_rendered = self.rows.is_empty(); - - let table = div() - .when_some(width, |this, width| this.w(width)) - .h_full() - .v_flex() - .when_some(self.headers.take(), |this, headers| { - this.child(render_table_header( - headers, - table_context.clone(), - current_widths_with_initial_sizes, - interaction_state.as_ref().map(Entity::entity_id), - cx, - )) - }) - .when_some(current_widths, { - |this, (widths, resize_behavior)| { - this.on_drag_move::({ - let widths = widths.clone(); - move |e, window, cx| { - widths - .update(cx, |widths, cx| { - widths.on_drag_move(e, &resize_behavior, window, cx); - }) - .ok(); - } - }) - .on_children_prepainted({ - let widths = widths.clone(); - move |bounds, _, cx| { - widths - .update(cx, |widths, _| { - // This works because all children x axis bounds are the same - widths.cached_bounds_width = - bounds[0].right() - bounds[0].left(); - }) - .ok(); - } - }) - .on_drop::(move |_, _, cx| { - widths - .update(cx, |widths, _| { - widths.widths = widths.visible_widths; - }) - .ok(); - // Finish the resize operation - }) - } - }) - .child({ - let content = div() - .flex_grow() - .w_full() - .relative() - .overflow_hidden() - .map(|parent| match self.rows { - TableContents::Vec(items) => { - parent.children(items.into_iter().enumerate().map(|(index, row)| { - div().child(render_table_row( - index, - row, - table_context.clone(), - window, - cx, - )) - })) - } - TableContents::UniformList(uniform_list_data) => parent.child( - uniform_list( - uniform_list_data.element_id, - uniform_list_data.row_count, - { - let render_item_fn = uniform_list_data.render_item_fn; - move |range: Range, window, cx| { - let elements = render_item_fn(range.clone(), window, cx); - elements - .into_iter() - .zip(range) - .map(|(row, row_index)| { - render_table_row( - row_index, - row, - table_context.clone(), - window, - cx, - ) - }) - .collect() - } - }, - ) - .size_full() - .flex_grow() - .with_sizing_behavior(ListSizingBehavior::Auto) - .with_horizontal_sizing_behavior(if width.is_some() { - ListHorizontalSizingBehavior::Unconstrained - } else { - ListHorizontalSizingBehavior::FitList - }) - .when_some( - interaction_state.as_ref(), - |this, state| { - this.track_scroll( - &state.read_with(cx, |s, _| s.scroll_handle.clone()), - ) - }, - ), - ), - }) - .when_some( - self.col_widths.as_ref().zip(interaction_state.as_ref()), - |parent, (table_widths, state)| { - parent.child(state.update(cx, |state, cx| { - let resizable_columns = table_widths.resizable; - let column_widths = table_widths.lengths(cx); - let columns = table_widths.current.clone(); - let initial_sizes = table_widths.initial; - state.render_resize_handles( - &column_widths, - &resizable_columns, - initial_sizes, - columns, - window, - cx, - ) - })) - }, - ); - - if let Some(state) = interaction_state.as_ref() { - let scrollbars = state - .read(cx) - .custom_scrollbar - .clone() - .unwrap_or_else(|| Scrollbars::new(super::ScrollAxes::Both)); - content - .custom_scrollbars( - scrollbars.tracked_scroll_handle(&state.read(cx).scroll_handle), - window, - cx, - ) - .into_any_element() - } else { - content.into_any_element() - } - }) - .when_some( - no_rows_rendered - .then_some(self.empty_table_callback) - .flatten(), - |this, callback| { - this.child( - h_flex() - .size_full() - .p_3() - .items_start() - .justify_center() - .child(callback(window, cx)), - ) - }, - ); - - if let Some(interaction_state) = interaction_state.as_ref() { - table - .track_focus(&interaction_state.read(cx).focus_handle) - .id(("table", interaction_state.entity_id())) - .into_any_element() - } else { - table.into_any_element() - } - } -} - -impl Component for Table<3> { - fn scope() -> ComponentScope { - ComponentScope::Layout - } - - fn description() -> Option<&'static str> { - Some("A table component for displaying data in rows and columns with optional styling.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Tables", - vec![ - single_example( - "Simple Table", - Table::new() - .width(px(400.)) - .header(["Name", "Age", "City"]) - .row(["Alice", "28", "New York"]) - .row(["Bob", "32", "San Francisco"]) - .row(["Charlie", "25", "London"]) - .into_any_element(), - ), - single_example( - "Two Column Table", - Table::new() - .header(["Category", "Value"]) - .width(px(300.)) - .row(["Revenue", "$100,000"]) - .row(["Expenses", "$75,000"]) - .row(["Profit", "$25,000"]) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Styled Tables", - vec![ - single_example( - "Default", - Table::new() - .width(px(400.)) - .header(["Product", "Price", "Stock"]) - .row(["Laptop", "$999", "In Stock"]) - .row(["Phone", "$599", "Low Stock"]) - .row(["Tablet", "$399", "Out of Stock"]) - .into_any_element(), - ), - single_example( - "Striped", - Table::new() - .width(px(400.)) - .striped() - .header(["Product", "Price", "Stock"]) - .row(["Laptop", "$999", "In Stock"]) - .row(["Phone", "$599", "Low Stock"]) - .row(["Tablet", "$399", "Out of Stock"]) - .row(["Headphones", "$199", "In Stock"]) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Mixed Content Table", - vec![single_example( - "Table with Elements", - Table::new() - .width(px(840.)) - .header(["Status", "Name", "Priority", "Deadline", "Action"]) - .row([ - Indicator::dot().color(Color::Success).into_any_element(), - "Project A".into_any_element(), - "High".into_any_element(), - "2023-12-31".into_any_element(), - Button::new("view_a", "View") - .style(ButtonStyle::Filled) - .full_width() - .into_any_element(), - ]) - .row([ - Indicator::dot().color(Color::Warning).into_any_element(), - "Project B".into_any_element(), - "Medium".into_any_element(), - "2024-03-15".into_any_element(), - Button::new("view_b", "View") - .style(ButtonStyle::Filled) - .full_width() - .into_any_element(), - ]) - .row([ - Indicator::dot().color(Color::Error).into_any_element(), - "Project C".into_any_element(), - "Low".into_any_element(), - "2024-06-30".into_any_element(), - Button::new("view_c", "View") - .style(ButtonStyle::Filled) - .full_width() - .into_any_element(), - ]) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} - -#[cfg(test)] -mod test { - use super::*; - - fn is_almost_eq(a: &[f32], b: &[f32]) -> bool { - a.len() == b.len() && a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-6) - } - - fn cols_to_str(cols: &[f32; COLS], total_size: f32) -> String { - cols.map(|f| "*".repeat(f32::round(f * total_size) as usize)) - .join("|") - } - - fn parse_resize_behavior( - input: &str, - total_size: f32, - ) -> [TableResizeBehavior; COLS] { - let mut resize_behavior = [TableResizeBehavior::None; COLS]; - let mut max_index = 0; - for (index, col) in input.split('|').enumerate() { - if col.starts_with('X') || col.is_empty() { - resize_behavior[index] = TableResizeBehavior::None; - } else if col.starts_with('*') { - resize_behavior[index] = - TableResizeBehavior::MinSize(col.len() as f32 / total_size); - } else { - panic!("invalid test input: unrecognized resize behavior: {}", col); - } - max_index = index; - } - - if max_index + 1 != COLS { - panic!("invalid test input: too many columns"); - } - resize_behavior - } - - mod reset_column_size { - use super::*; - - fn parse(input: &str) -> ([f32; COLS], f32, Option) { - let mut widths = [f32::NAN; COLS]; - let mut column_index = None; - for (index, col) in input.split('|').enumerate() { - widths[index] = col.len() as f32; - if col.starts_with('X') { - column_index = Some(index); - } - } - - for w in widths { - assert!(w.is_finite(), "incorrect number of columns"); - } - let total = widths.iter().sum::(); - for width in &mut widths { - *width /= total; - } - (widths, total, column_index) - } - - #[track_caller] - fn check_reset_size( - initial_sizes: &str, - widths: &str, - expected: &str, - resize_behavior: &str, - ) { - let (initial_sizes, total_1, None) = parse::(initial_sizes) else { - panic!("invalid test input: initial sizes should not be marked"); - }; - let (widths, total_2, Some(column_index)) = parse::(widths) else { - panic!("invalid test input: widths should be marked"); - }; - assert_eq!( - total_1, total_2, - "invalid test input: total width not the same {total_1}, {total_2}" - ); - let (expected, total_3, None) = parse::(expected) else { - panic!("invalid test input: expected should not be marked: {expected:?}"); - }; - assert_eq!( - total_2, total_3, - "invalid test input: total width not the same" - ); - let resize_behavior = parse_resize_behavior::(resize_behavior, total_1); - let result = TableColumnWidths::reset_to_initial_size( - column_index, - widths, - initial_sizes, - &resize_behavior, - ); - let is_eq = is_almost_eq(&result, &expected); - if !is_eq { - let result_str = cols_to_str(&result, total_1); - let expected_str = cols_to_str(&expected, total_1); - panic!( - "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}" - ); - } - } - - macro_rules! check_reset_size { - (columns: $cols:expr, starting: $initial:expr, snapshot: $current:expr, expected: $expected:expr, resizing: $resizing:expr $(,)?) => { - check_reset_size::<$cols>($initial, $current, $expected, $resizing); - }; - ($name:ident, columns: $cols:expr, starting: $initial:expr, snapshot: $current:expr, expected: $expected:expr, minimums: $resizing:expr $(,)?) => { - #[test] - fn $name() { - check_reset_size::<$cols>($initial, $current, $expected, $resizing); - } - }; - } - - check_reset_size!( - basic_right, - columns: 5, - starting: "**|**|**|**|**", - snapshot: "**|**|X|***|**", - expected: "**|**|**|**|**", - minimums: "X|*|*|*|*", - ); - - check_reset_size!( - basic_left, - columns: 5, - starting: "**|**|**|**|**", - snapshot: "**|**|***|X|**", - expected: "**|**|**|**|**", - minimums: "X|*|*|*|**", - ); - - check_reset_size!( - squashed_left_reset_col2, - columns: 6, - starting: "*|***|**|**|****|*", - snapshot: "*|*|X|*|*|********", - expected: "*|*|**|*|*|*******", - minimums: "X|*|*|*|*|*", - ); - - check_reset_size!( - grow_cascading_right, - columns: 6, - starting: "*|***|****|**|***|*", - snapshot: "*|***|X|**|**|*****", - expected: "*|***|****|*|*|****", - minimums: "X|*|*|*|*|*", - ); - - check_reset_size!( - squashed_right_reset_col4, - columns: 6, - starting: "*|***|**|**|****|*", - snapshot: "*|********|*|*|X|*", - expected: "*|*****|*|*|****|*", - minimums: "X|*|*|*|*|*", - ); - - check_reset_size!( - reset_col6_right, - columns: 6, - starting: "*|***|**|***|***|**", - snapshot: "*|***|**|***|**|XXX", - expected: "*|***|**|***|***|**", - minimums: "X|*|*|*|*|*", - ); - - check_reset_size!( - reset_col6_left, - columns: 6, - starting: "*|***|**|***|***|**", - snapshot: "*|***|**|***|****|X", - expected: "*|***|**|***|***|**", - minimums: "X|*|*|*|*|*", - ); - - check_reset_size!( - last_column_grow_cascading, - columns: 6, - starting: "*|***|**|**|**|***", - snapshot: "*|*******|*|**|*|X", - expected: "*|******|*|*|*|***", - minimums: "X|*|*|*|*|*", - ); - - check_reset_size!( - goes_left_when_left_has_extreme_diff, - columns: 6, - starting: "*|***|****|**|**|***", - snapshot: "*|********|X|*|**|**", - expected: "*|*****|****|*|**|**", - minimums: "X|*|*|*|*|*", - ); - - check_reset_size!( - basic_shrink_right, - columns: 6, - starting: "**|**|**|**|**|**", - snapshot: "**|**|XXX|*|**|**", - expected: "**|**|**|**|**|**", - minimums: "X|*|*|*|*|*", - ); - - check_reset_size!( - shrink_should_go_left, - columns: 6, - starting: "*|***|**|*|*|*", - snapshot: "*|*|XXX|**|*|*", - expected: "*|**|**|**|*|*", - minimums: "X|*|*|*|*|*", - ); - - check_reset_size!( - shrink_should_go_right, - columns: 6, - starting: "*|***|**|**|**|*", - snapshot: "*|****|XXX|*|*|*", - expected: "*|****|**|**|*|*", - minimums: "X|*|*|*|*|*", - ); - } - - mod drag_handle { - use super::*; - - fn parse(input: &str) -> ([f32; COLS], f32, Option) { - let mut widths = [f32::NAN; COLS]; - let column_index = input.replace("*", "").find("I"); - for (index, col) in input.replace("I", "|").split('|').enumerate() { - widths[index] = col.len() as f32; - } - - for w in widths { - assert!(w.is_finite(), "incorrect number of columns"); - } - let total = widths.iter().sum::(); - for width in &mut widths { - *width /= total; - } - (widths, total, column_index) - } - - #[track_caller] - fn check( - distance: i32, - widths: &str, - expected: &str, - resize_behavior: &str, - ) { - let (mut widths, total_1, Some(column_index)) = parse::(widths) else { - panic!("invalid test input: widths should be marked"); - }; - let (expected, total_2, None) = parse::(expected) else { - panic!("invalid test input: expected should not be marked: {expected:?}"); - }; - assert_eq!( - total_1, total_2, - "invalid test input: total width not the same" - ); - let resize_behavior = parse_resize_behavior::(resize_behavior, total_1); - - let distance = distance as f32 / total_1; - - let result = TableColumnWidths::drag_column_handle( - distance, - column_index, - &mut widths, - &resize_behavior, - ); - - let is_eq = is_almost_eq(&widths, &expected); - if !is_eq { - let result_str = cols_to_str(&widths, total_1); - let expected_str = cols_to_str(&expected, total_1); - panic!( - "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}" - ); - } - } - - macro_rules! check { - (columns: $cols:expr, distance: $dist:expr, snapshot: $current:expr, expected: $expected:expr, resizing: $resizing:expr $(,)?) => { - check!($cols, $dist, $snapshot, $expected, $resizing); - }; - ($name:ident, columns: $cols:expr, distance: $dist:expr, snapshot: $current:expr, expected: $expected:expr, minimums: $resizing:expr $(,)?) => { - #[test] - fn $name() { - check::<$cols>($dist, $current, $expected, $resizing); - } - }; - } - - check!( - basic_right_drag, - columns: 3, - distance: 1, - snapshot: "**|**I**", - expected: "**|***|*", - minimums: "X|*|*", - ); - - check!( - drag_left_against_mins, - columns: 5, - distance: -1, - snapshot: "*|*|*|*I*******", - expected: "*|*|*|*|*******", - minimums: "X|*|*|*|*", - ); - - check!( - drag_left, - columns: 5, - distance: -2, - snapshot: "*|*|*|*****I***", - expected: "*|*|*|***|*****", - minimums: "X|*|*|*|*", - ); - } -} diff --git a/crates/ui/src/components/diff_stat.rs b/crates/ui/src/components/diff_stat.rs deleted file mode 100644 index 2606963555..0000000000 --- a/crates/ui/src/components/diff_stat.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::prelude::*; - -#[derive(IntoElement, RegisterComponent)] -pub struct DiffStat { - id: ElementId, - added: usize, - removed: usize, -} - -impl DiffStat { - pub fn new(id: impl Into, added: usize, removed: usize) -> Self { - Self { - id: id.into(), - added, - removed, - } - } -} - -impl RenderOnce for DiffStat { - fn render(self, _: &mut Window, _cx: &mut App) -> impl IntoElement { - h_flex() - .id(self.id) - .gap_1() - .child( - h_flex() - .gap_0p5() - .child( - Icon::new(IconName::Plus) - .size(IconSize::XSmall) - .color(Color::Success), - ) - .child( - Label::new(self.added.to_string()) - .color(Color::Success) - .size(LabelSize::Small), - ), - ) - .child( - h_flex() - .gap_0p5() - .child( - Icon::new(IconName::Dash) - .size(IconSize::XSmall) - .color(Color::Error), - ) - .child( - Label::new(self.removed.to_string()) - .color(Color::Error) - .size(LabelSize::Small), - ), - ) - } -} - -impl Component for DiffStat { - fn scope() -> ComponentScope { - ComponentScope::VersionControl - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - let container = || { - h_flex() - .py_4() - .w_72() - .justify_center() - .border_1() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().panel_background) - }; - - let diff_stat_example = vec![single_example( - "Default", - container() - .child(DiffStat::new("id", 1, 2)) - .into_any_element(), - )]; - - Some( - example_group(diff_stat_example) - .vertical() - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/disclosure.rs b/crates/ui/src/components/disclosure.rs deleted file mode 100644 index 84282db2e3..0000000000 --- a/crates/ui/src/components/disclosure.rs +++ /dev/null @@ -1,152 +0,0 @@ -use std::sync::Arc; - -use gpui::{ClickEvent, CursorStyle, SharedString}; - -use crate::{Color, IconButton, IconButtonShape, IconName, IconSize, prelude::*}; - -#[derive(IntoElement, RegisterComponent)] -pub struct Disclosure { - id: ElementId, - is_open: bool, - selected: bool, - disabled: bool, - on_toggle_expanded: Option>, - cursor_style: CursorStyle, - opened_icon: IconName, - closed_icon: IconName, - visible_on_hover: Option, -} - -impl Disclosure { - pub fn new(id: impl Into, is_open: bool) -> Self { - Self { - id: id.into(), - is_open, - selected: false, - disabled: false, - on_toggle_expanded: None, - cursor_style: CursorStyle::PointingHand, - opened_icon: IconName::ChevronDown, - closed_icon: IconName::ChevronRight, - visible_on_hover: None, - } - } - - pub fn on_toggle_expanded( - mut self, - handler: impl Into>>, - ) -> Self { - self.on_toggle_expanded = handler.into(); - self - } - - pub fn opened_icon(mut self, icon: IconName) -> Self { - self.opened_icon = icon; - self - } - - pub fn closed_icon(mut self, icon: IconName) -> Self { - self.closed_icon = icon; - self - } - - pub fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } -} - -impl Toggleable for Disclosure { - fn toggle_state(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -impl Clickable for Disclosure { - fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self { - self.on_toggle_expanded = Some(Arc::new(handler)); - self - } - - fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self { - self.cursor_style = cursor_style; - self - } -} - -impl VisibleOnHover for Disclosure { - fn visible_on_hover(mut self, group_name: impl Into) -> Self { - self.visible_on_hover = Some(group_name.into()); - self - } -} - -impl RenderOnce for Disclosure { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - IconButton::new( - self.id, - match self.is_open { - true => self.opened_icon, - false => self.closed_icon, - }, - ) - .shape(IconButtonShape::Square) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .disabled(self.disabled) - .toggle_state(self.selected) - .when_some(self.visible_on_hover.clone(), |this, group_name| { - this.visible_on_hover(group_name) - }) - .when_some(self.on_toggle_expanded, move |this, on_toggle| { - this.on_click(move |event, window, cx| on_toggle(event, window, cx)) - }) - } -} - -impl Component for Disclosure { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn description() -> Option<&'static str> { - Some( - "An interactive element used to show or hide content, typically used in expandable sections or tree-like structures.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Disclosure States", - vec![ - single_example( - "Closed", - Disclosure::new("closed", false).into_any_element(), - ), - single_example( - "Open", - Disclosure::new("open", true).into_any_element(), - ), - ], - ), - example_group_with_title( - "Interactive Example", - vec![single_example( - "Toggleable", - v_flex() - .gap_2() - .child(Disclosure::new("interactive", false).into_any_element()) - .child(Label::new("Click to toggle")) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/divider.rs b/crates/ui/src/components/divider.rs deleted file mode 100644 index d6101f2320..0000000000 --- a/crates/ui/src/components/divider.rs +++ /dev/null @@ -1,248 +0,0 @@ -use gpui::{Hsla, IntoElement, PathBuilder, canvas, point}; - -use crate::prelude::*; - -pub fn divider() -> Divider { - Divider { - style: DividerStyle::Solid, - direction: DividerDirection::Horizontal, - color: DividerColor::default(), - inset: false, - } -} - -pub fn vertical_divider() -> Divider { - Divider { - style: DividerStyle::Solid, - direction: DividerDirection::Vertical, - color: DividerColor::default(), - inset: false, - } -} - -#[derive(Clone, Copy, PartialEq)] -enum DividerStyle { - Solid, - Dashed, -} - -#[derive(Clone, Copy, PartialEq)] -enum DividerDirection { - Horizontal, - Vertical, -} - -/// The color of a [`Divider`]. -#[derive(Default)] -pub enum DividerColor { - Border, - BorderFaded, - #[default] - BorderVariant, -} - -impl DividerColor { - pub fn hsla(self, cx: &mut App) -> Hsla { - match self { - DividerColor::Border => cx.theme().colors().border, - DividerColor::BorderFaded => cx.theme().colors().border.opacity(0.6), - DividerColor::BorderVariant => cx.theme().colors().border_variant, - } - } -} - -#[derive(IntoElement, RegisterComponent)] -pub struct Divider { - style: DividerStyle, - direction: DividerDirection, - color: DividerColor, - inset: bool, -} - -impl Divider { - pub fn horizontal() -> Self { - Self { - style: DividerStyle::Solid, - direction: DividerDirection::Horizontal, - color: DividerColor::default(), - inset: false, - } - } - - pub fn vertical() -> Self { - Self { - style: DividerStyle::Solid, - direction: DividerDirection::Vertical, - color: DividerColor::default(), - inset: false, - } - } - - pub fn horizontal_dashed() -> Self { - Self { - style: DividerStyle::Dashed, - direction: DividerDirection::Horizontal, - color: DividerColor::default(), - inset: false, - } - } - - pub fn vertical_dashed() -> Self { - Self { - style: DividerStyle::Dashed, - direction: DividerDirection::Vertical, - color: DividerColor::default(), - inset: false, - } - } - - pub fn inset(mut self) -> Self { - self.inset = true; - self - } - - pub fn color(mut self, color: DividerColor) -> Self { - self.color = color; - self - } - - pub fn render_solid(self, base: Div, cx: &mut App) -> impl IntoElement { - base.bg(self.color.hsla(cx)) - } - - pub fn render_dashed(self, base: Div) -> impl IntoElement { - base.relative().child( - canvas( - |_, _, _| {}, - move |bounds, _, window, cx| { - let mut builder = PathBuilder::stroke(px(1.)).dash_array(&[px(4.), px(2.)]); - let (start, end) = match self.direction { - DividerDirection::Horizontal => { - let x = bounds.origin.x; - let y = bounds.origin.y + px(0.5); - (point(x, y), point(x + bounds.size.width, y)) - } - DividerDirection::Vertical => { - let x = bounds.origin.x + px(0.5); - let y = bounds.origin.y; - (point(x, y), point(x, y + bounds.size.height)) - } - }; - builder.move_to(start); - builder.line_to(end); - if let Ok(line) = builder.build() { - window.paint_path(line, self.color.hsla(cx)); - } - }, - ) - .absolute() - .size_full(), - ) - } -} - -impl RenderOnce for Divider { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let base = match self.direction { - DividerDirection::Horizontal => { - div().h_px().w_full().when(self.inset, |this| this.mx_1p5()) - } - DividerDirection::Vertical => { - div().w_px().h_full().when(self.inset, |this| this.my_1p5()) - } - }; - - match self.style { - DividerStyle::Solid => self.render_solid(base, cx).into_any_element(), - DividerStyle::Dashed => self.render_dashed(base).into_any_element(), - } - } -} - -impl Component for Divider { - fn scope() -> ComponentScope { - ComponentScope::Layout - } - - fn description() -> Option<&'static str> { - Some( - "Visual separator used to create divisions between groups of content or sections in a layout.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Horizontal Dividers", - vec![ - single_example("Default", Divider::horizontal().into_any_element()), - single_example( - "Border Color", - Divider::horizontal() - .color(DividerColor::Border) - .into_any_element(), - ), - single_example( - "Inset", - Divider::horizontal().inset().into_any_element(), - ), - single_example( - "Dashed", - Divider::horizontal_dashed().into_any_element(), - ), - ], - ), - example_group_with_title( - "Vertical Dividers", - vec![ - single_example( - "Default", - div().h_16().child(Divider::vertical()).into_any_element(), - ), - single_example( - "Border Color", - div() - .h_16() - .child(Divider::vertical().color(DividerColor::Border)) - .into_any_element(), - ), - single_example( - "Inset", - div() - .h_16() - .child(Divider::vertical().inset()) - .into_any_element(), - ), - single_example( - "Dashed", - div() - .h_16() - .child(Divider::vertical_dashed()) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Example Usage", - vec![single_example( - "Between Content", - v_flex() - .w_full() - .gap_4() - .px_4() - .child(Label::new("Section One")) - .child(Divider::horizontal()) - .child(Label::new("Section Two")) - .child(Divider::horizontal_dashed()) - .child(Label::new("Section Three")) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/dropdown_menu.rs b/crates/ui/src/components/dropdown_menu.rs deleted file mode 100644 index 5b5de7a257..0000000000 --- a/crates/ui/src/components/dropdown_menu.rs +++ /dev/null @@ -1,304 +0,0 @@ -use gpui::{AnyView, Corner, Entity, Pixels, Point}; - -use crate::{ButtonLike, ContextMenu, PopoverMenu, prelude::*}; - -use super::PopoverMenuHandle; - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] -pub enum DropdownStyle { - #[default] - Solid, - Outlined, - Subtle, - Ghost, -} - -enum LabelKind { - Text(SharedString), - Element(AnyElement), -} - -#[derive(IntoElement, RegisterComponent)] -pub struct DropdownMenu { - id: ElementId, - label: LabelKind, - trigger_size: ButtonSize, - trigger_tooltip: Option AnyView + 'static>>, - trigger_icon: Option, - style: DropdownStyle, - menu: Entity, - full_width: bool, - disabled: bool, - handle: Option>, - attach: Option, - offset: Option>, - tab_index: Option, - chevron: bool, -} - -impl DropdownMenu { - pub fn new( - id: impl Into, - label: impl Into, - menu: Entity, - ) -> Self { - Self { - id: id.into(), - label: LabelKind::Text(label.into()), - trigger_size: ButtonSize::Default, - trigger_tooltip: None, - trigger_icon: Some(IconName::ChevronUpDown), - style: DropdownStyle::default(), - menu, - full_width: false, - disabled: false, - handle: None, - attach: None, - offset: None, - tab_index: None, - chevron: true, - } - } - - pub fn new_with_element( - id: impl Into, - label: AnyElement, - menu: Entity, - ) -> Self { - Self { - id: id.into(), - label: LabelKind::Element(label), - trigger_size: ButtonSize::Default, - trigger_tooltip: None, - trigger_icon: Some(IconName::ChevronUpDown), - style: DropdownStyle::default(), - menu, - full_width: false, - disabled: false, - handle: None, - attach: None, - offset: None, - tab_index: None, - chevron: true, - } - } - - pub fn style(mut self, style: DropdownStyle) -> Self { - self.style = style; - self - } - - pub fn trigger_size(mut self, size: ButtonSize) -> Self { - self.trigger_size = size; - self - } - - pub fn trigger_tooltip( - mut self, - tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, - ) -> Self { - self.trigger_tooltip = Some(Box::new(tooltip)); - self - } - - pub fn trigger_icon(mut self, icon: IconName) -> Self { - self.trigger_icon = Some(icon); - self - } - - pub fn full_width(mut self, full_width: bool) -> Self { - self.full_width = full_width; - self - } - - pub fn handle(mut self, handle: PopoverMenuHandle) -> Self { - self.handle = Some(handle); - self - } - - /// Defines which corner of the handle to attach the menu's anchor to. - pub fn attach(mut self, attach: Corner) -> Self { - self.attach = Some(attach); - self - } - - /// Offsets the position of the menu by that many pixels. - pub fn offset(mut self, offset: Point) -> Self { - self.offset = Some(offset); - self - } - - pub fn tab_index(mut self, arg: isize) -> Self { - self.tab_index = Some(arg); - self - } - - pub fn no_chevron(mut self) -> Self { - self.chevron = false; - self - } -} - -impl Disableable for DropdownMenu { - fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } -} - -impl RenderOnce for DropdownMenu { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let button_style = match self.style { - DropdownStyle::Solid => ButtonStyle::Filled, - DropdownStyle::Subtle => ButtonStyle::Subtle, - DropdownStyle::Outlined => ButtonStyle::Outlined, - DropdownStyle::Ghost => ButtonStyle::Transparent, - }; - - let full_width = self.full_width; - let trigger_size = self.trigger_size; - - let (text_button, element_button) = match self.label { - LabelKind::Text(text) => ( - Some( - Button::new(self.id.clone(), text) - .style(button_style) - .when(self.chevron, |this| { - this.icon(self.trigger_icon) - .icon_position(IconPosition::End) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - }) - .when(full_width, |this| this.full_width()) - .size(trigger_size) - .disabled(self.disabled) - .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)), - ), - None, - ), - LabelKind::Element(element) => ( - None, - Some( - ButtonLike::new(self.id.clone()) - .child(element) - .style(button_style) - .when(self.chevron, |this| { - this.child( - Icon::new(IconName::ChevronUpDown) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - }) - .when(full_width, |this| this.full_width()) - .size(trigger_size) - .disabled(self.disabled) - .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)), - ), - ), - }; - - let mut popover = PopoverMenu::new((self.id.clone(), "popover")) - .full_width(self.full_width) - .menu(move |_window, _cx| Some(self.menu.clone())); - - popover = match (text_button, element_button, self.trigger_tooltip) { - (Some(text_button), None, Some(tooltip)) => { - popover.trigger_with_tooltip(text_button, tooltip) - } - (Some(text_button), None, None) => popover.trigger(text_button), - (None, Some(element_button), Some(tooltip)) => { - popover.trigger_with_tooltip(element_button, tooltip) - } - (None, Some(element_button), None) => popover.trigger(element_button), - _ => popover, - }; - - popover - .attach(match self.attach { - Some(attach) => attach, - None => Corner::BottomRight, - }) - .when_some(self.offset, |this, offset| this.offset(offset)) - .when_some(self.handle, |this, handle| this.with_handle(handle)) - } -} - -impl Component for DropdownMenu { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn name() -> &'static str { - "DropdownMenu" - } - - fn description() -> Option<&'static str> { - Some( - "A dropdown menu displays a list of actions or options. A dropdown menu is always activated by clicking a trigger (or via a keybinding).", - ) - } - - fn preview(window: &mut Window, cx: &mut App) -> Option { - let menu = ContextMenu::build(window, cx, |this, _, _| { - this.entry("Option 1", None, |_, _| {}) - .entry("Option 2", None, |_, _| {}) - .entry("Option 3", None, |_, _| {}) - .separator() - .entry("Option 4", None, |_, _| {}) - }); - - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Usage", - vec![ - single_example( - "Default", - DropdownMenu::new("default", "Select an option", menu.clone()) - .into_any_element(), - ), - single_example( - "Full Width", - DropdownMenu::new( - "full-width", - "Full Width Dropdown", - menu.clone(), - ) - .full_width(true) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Styles", - vec![ - single_example( - "Outlined", - DropdownMenu::new("outlined", "Outlined Dropdown", menu.clone()) - .style(DropdownStyle::Outlined) - .into_any_element(), - ), - single_example( - "Ghost", - DropdownMenu::new("ghost", "Ghost Dropdown", menu.clone()) - .style(DropdownStyle::Ghost) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "States", - vec![single_example( - "Disabled", - DropdownMenu::new("disabled", "Disabled Dropdown", menu) - .disabled(true) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/facepile.rs b/crates/ui/src/components/facepile.rs deleted file mode 100644 index 79a36871d6..0000000000 --- a/crates/ui/src/components/facepile.rs +++ /dev/null @@ -1,135 +0,0 @@ -use crate::component_prelude::*; -use crate::prelude::*; -use gpui::{AnyElement, StyleRefinement}; -use smallvec::SmallVec; - -use super::Avatar; - -/// An element that displays a collection of (usually) faces stacked -/// horizontally, with the left-most face on top, visually descending -/// from left to right. -/// -/// Facepiles are used to display a group of people or things, -/// such as a list of participants in a collaboration session. -/// -/// # Examples -/// -/// ## Default -/// -/// A default, horizontal facepile. -/// -/// ``` -/// use gpui::IntoElement; -/// use ui::{Avatar, Facepile, EXAMPLE_FACES}; -/// -/// let facepile = Facepile::new( -/// EXAMPLE_FACES.iter().take(3).map(|&url| -/// Avatar::new(url).into_any_element()).collect() -/// ); -/// ``` -#[derive(IntoElement, Documented, RegisterComponent)] -pub struct Facepile { - base: Div, - faces: SmallVec<[AnyElement; 2]>, -} - -impl Facepile { - /// Creates a new empty facepile. - pub fn empty() -> Self { - Self::new(SmallVec::new()) - } - - /// Creates a new facepile with the given faces. - pub fn new(faces: SmallVec<[AnyElement; 2]>) -> Self { - Self { base: div(), faces } - } -} - -impl ParentElement for Facepile { - fn extend(&mut self, elements: impl IntoIterator) { - self.faces.extend(elements); - } -} - -// Style methods. -impl Facepile { - fn style(&mut self) -> &mut StyleRefinement { - self.base.style() - } - - gpui::padding_style_methods!({ - visibility: pub - }); -} - -impl RenderOnce for Facepile { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - // Lay the faces out in reverse so they overlap in the desired order (left to right, front to back) - self.base - .flex() - .flex_row_reverse() - .items_center() - .justify_start() - .children( - self.faces - .into_iter() - .enumerate() - .rev() - .map(|(ix, player)| div().when(ix > 0, |div| div.ml_neg_1()).child(player)), - ) - } -} - -pub const EXAMPLE_FACES: [&str; 6] = [ - "https://avatars.githubusercontent.com/u/326587?s=60&v=4", - "https://avatars.githubusercontent.com/u/2280405?s=60&v=4", - "https://avatars.githubusercontent.com/u/1789?s=60&v=4", - "https://avatars.githubusercontent.com/u/67129314?s=60&v=4", - "https://avatars.githubusercontent.com/u/482957?s=60&v=4", - "https://avatars.githubusercontent.com/u/1714999?s=60&v=4", -]; - -impl Component for Facepile { - fn scope() -> ComponentScope { - ComponentScope::Collaboration - } - - fn description() -> Option<&'static str> { - Some( - "Displays a collection of avatars or initials in a compact format. Often used to represent active collaborators or a subset of contributors.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![example_group_with_title( - "Facepile Examples", - vec![ - single_example( - "Default", - Facepile::new( - EXAMPLE_FACES - .iter() - .map(|&url| Avatar::new(url).into_any_element()) - .collect(), - ) - .into_any_element(), - ), - single_example( - "Custom Size", - Facepile::new( - EXAMPLE_FACES - .iter() - .map(|&url| Avatar::new(url).size(px(24.)).into_any_element()) - .collect(), - ) - .into_any_element(), - ), - ], - )]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/group.rs b/crates/ui/src/components/group.rs deleted file mode 100644 index 12462bb24b..0000000000 --- a/crates/ui/src/components/group.rs +++ /dev/null @@ -1,57 +0,0 @@ -use gpui::{Div, div, prelude::*}; - -/// Creates a horizontal group with tight, consistent spacing. -/// -/// xs: ~2px @16px/rem -pub fn h_group_sm() -> Div { - div().flex().gap_0p5() -} - -/// Creates a horizontal group with consistent spacing. -/// -/// s: ~4px @16px/rem -pub fn h_group() -> Div { - div().flex().gap_1() -} - -/// Creates a horizontal group with consistent spacing. -/// -/// m: ~6px @16px/rem -pub fn h_group_lg() -> Div { - div().flex().gap_1p5() -} - -/// Creates a horizontal group with consistent spacing. -/// -/// l: ~8px @16px/rem -pub fn h_group_xl() -> Div { - div().flex().gap_2() -} - -/// Creates a vertical group with tight, consistent spacing. -/// -/// xs: ~2px @16px/rem -pub fn v_group_sm() -> Div { - div().flex().flex_col().gap_0p5() -} - -/// Creates a vertical group with consistent spacing. -/// -/// s: ~4px @16px/rem -pub fn v_group() -> Div { - div().flex().flex_col().gap_1() -} - -/// Creates a vertical group with consistent spacing. -/// -/// m: ~6px @16px/rem -pub fn v_group_lg() -> Div { - div().flex().flex_col().gap_1p5() -} - -/// Creates a vertical group with consistent spacing. -/// -/// l: ~8px @16px/rem -pub fn v_group_xl() -> Div { - div().flex().flex_col().gap_2() -} diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs deleted file mode 100644 index 1c8e36ec18..0000000000 --- a/crates/ui/src/components/icon.rs +++ /dev/null @@ -1,371 +0,0 @@ -mod decorated_icon; -mod icon_decoration; - -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -pub use decorated_icon::*; -use gpui::{AnimationElement, AnyElement, Hsla, IntoElement, Rems, Transformation, img, svg}; -pub use icon_decoration::*; -pub use icons::*; - -use crate::traits::transformable::Transformable; -use crate::{Indicator, prelude::*}; - -#[derive(IntoElement)] -pub enum AnyIcon { - Icon(Icon), - AnimatedIcon(AnimationElement), -} - -impl AnyIcon { - /// Returns a new [`AnyIcon`] after applying the given mapping function - /// to the contained [`Icon`]. - pub fn map(self, f: impl FnOnce(Icon) -> Icon) -> Self { - match self { - Self::Icon(icon) => Self::Icon(f(icon)), - Self::AnimatedIcon(animated_icon) => Self::AnimatedIcon(animated_icon.map_element(f)), - } - } -} - -impl From for AnyIcon { - fn from(value: Icon) -> Self { - Self::Icon(value) - } -} - -impl From> for AnyIcon { - fn from(value: AnimationElement) -> Self { - Self::AnimatedIcon(value) - } -} - -impl RenderOnce for AnyIcon { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - match self { - Self::Icon(icon) => icon.into_any_element(), - Self::AnimatedIcon(animated_icon) => animated_icon.into_any_element(), - } - } -} - -#[derive(Default, PartialEq, Copy, Clone)] -pub enum IconSize { - /// 10px - Indicator, - /// 12px - XSmall, - /// 14px - Small, - #[default] - /// 16px - Medium, - /// 48px - XLarge, - Custom(Rems), -} - -impl IconSize { - pub fn rems(self) -> Rems { - match self { - IconSize::Indicator => rems_from_px(10.), - IconSize::XSmall => rems_from_px(12.), - IconSize::Small => rems_from_px(14.), - IconSize::Medium => rems_from_px(16.), - IconSize::XLarge => rems_from_px(48.), - IconSize::Custom(size) => size, - } - } - - /// Returns the individual components of the square that contains this [`IconSize`]. - /// - /// The returned tuple contains: - /// 1. The length of one side of the square - /// 2. The padding of one side of the square - pub fn square_components(&self, window: &mut Window, cx: &mut App) -> (Pixels, Pixels) { - let icon_size = self.rems() * window.rem_size(); - let padding = match self { - IconSize::Indicator => DynamicSpacing::Base00.px(cx), - IconSize::XSmall => DynamicSpacing::Base02.px(cx), - IconSize::Small => DynamicSpacing::Base02.px(cx), - IconSize::Medium => DynamicSpacing::Base02.px(cx), - IconSize::XLarge => DynamicSpacing::Base02.px(cx), - // TODO: Wire into dynamic spacing - IconSize::Custom(size) => size.to_pixels(window.rem_size()), - }; - - (icon_size, padding) - } - - /// Returns the length of a side of the square that contains this [`IconSize`], with padding. - pub fn square(&self, window: &mut Window, cx: &mut App) -> Pixels { - let (icon_size, padding) = self.square_components(window, cx); - - icon_size + padding * 2. - } -} - -impl From for Icon { - fn from(icon: IconName) -> Self { - Icon::new(icon) - } -} - -/// The source of an icon. -enum IconSource { - /// An SVG embedded in the Zed binary. - Embedded(SharedString), - /// An image file located at the specified path. - /// - /// Currently our SVG renderer is missing support for rendering polychrome SVGs. - /// - /// In order to support icon themes, we render the icons as images instead. - External(Arc), - /// An SVG not embedded in the Zed binary. - ExternalSvg(SharedString), -} - -impl IconSource { - fn from_path(path: impl Into) -> Self { - let path = path.into(); - if path.starts_with("icons/") { - Self::Embedded(path) - } else { - Self::External(Arc::from(PathBuf::from(path.as_ref()))) - } - } -} - -#[derive(IntoElement, RegisterComponent)] -pub struct Icon { - source: IconSource, - color: Color, - size: Rems, - transformation: Transformation, -} - -impl Icon { - pub fn new(icon: IconName) -> Self { - Self { - source: IconSource::Embedded(icon.path().into()), - color: Color::default(), - size: IconSize::default().rems(), - transformation: Transformation::default(), - } - } - - pub fn from_path(path: impl Into) -> Self { - Self { - source: IconSource::from_path(path), - color: Color::default(), - size: IconSize::default().rems(), - transformation: Transformation::default(), - } - } - - pub fn from_external_svg(svg: SharedString) -> Self { - Self { - source: IconSource::ExternalSvg(svg), - color: Color::default(), - size: IconSize::default().rems(), - transformation: Transformation::default(), - } - } - - pub fn color(mut self, color: Color) -> Self { - self.color = color; - self - } - - pub fn size(mut self, size: IconSize) -> Self { - self.size = size.rems(); - self - } - - /// Sets a custom size for the icon, in [`Rems`]. - /// - /// Not to be exposed outside of the `ui` crate. - pub(crate) fn custom_size(mut self, size: Rems) -> Self { - self.size = size; - self - } -} - -impl Transformable for Icon { - fn transform(mut self, transformation: Transformation) -> Self { - self.transformation = transformation; - self - } -} - -impl RenderOnce for Icon { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - match self.source { - IconSource::Embedded(path) => svg() - .with_transformation(self.transformation) - .size(self.size) - .flex_none() - .path(path) - .text_color(self.color.color(cx)) - .into_any_element(), - IconSource::ExternalSvg(path) => svg() - .external_path(path) - .with_transformation(self.transformation) - .size(self.size) - .flex_none() - .text_color(self.color.color(cx)) - .into_any_element(), - IconSource::External(path) => img(path) - .size(self.size) - .flex_none() - .text_color(self.color.color(cx)) - .into_any_element(), - } - } -} - -#[derive(IntoElement)] -pub struct IconWithIndicator { - icon: Icon, - indicator: Option, - indicator_border_color: Option, -} - -impl IconWithIndicator { - pub fn new(icon: Icon, indicator: Option) -> Self { - Self { - icon, - indicator, - indicator_border_color: None, - } - } - - pub fn indicator(mut self, indicator: Option) -> Self { - self.indicator = indicator; - self - } - - pub fn indicator_color(mut self, color: Color) -> Self { - if let Some(indicator) = self.indicator.as_mut() { - indicator.color = color; - } - self - } - - pub fn indicator_border_color(mut self, color: Option) -> Self { - self.indicator_border_color = color; - self - } -} - -impl RenderOnce for IconWithIndicator { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let indicator_border_color = self - .indicator_border_color - .unwrap_or_else(|| cx.theme().colors().elevated_surface_background); - - div() - .relative() - .child(self.icon) - .when_some(self.indicator, |this, indicator| { - this.child( - div() - .absolute() - .size_2p5() - .border_2() - .border_color(indicator_border_color) - .rounded_full() - .bottom_neg_0p5() - .right_neg_0p5() - .child(indicator), - ) - }) - } -} - -impl Component for Icon { - fn scope() -> ComponentScope { - ComponentScope::Images - } - - fn description() -> Option<&'static str> { - Some( - "A versatile icon component that supports SVG and image-based icons with customizable size, color, and transformations.", - ) - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Sizes", - vec![single_example( - "XSmall, Small, Default, Large", - h_flex() - .gap_1() - .child( - Icon::new(IconName::Star) - .size(IconSize::XSmall) - .into_any_element(), - ) - .child( - Icon::new(IconName::Star) - .size(IconSize::Small) - .into_any_element(), - ) - .child(Icon::new(IconName::Star).into_any_element()) - .child( - Icon::new(IconName::Star) - .size(IconSize::XLarge) - .into_any_element(), - ) - .into_any_element(), - )], - ), - example_group_with_title( - "Colors", - vec![single_example( - "Default & Custom", - h_flex() - .gap_1() - .child(Icon::new(IconName::Star).into_any_element()) - .child( - Icon::new(IconName::Star) - .color(Color::Error) - .into_any_element(), - ) - .into_any_element(), - )], - ), - example_group_with_title( - "All Icons", - vec![single_example( - "All Icons", - h_flex() - .image_cache(gpui::retain_all("all icons")) - .flex_wrap() - .gap_2() - .children(::iter().map( - |icon_name| { - h_flex() - .p_1() - .gap_1() - .border_1() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().element_disabled) - .rounded_sm() - .child(Icon::new(icon_name).into_any_element()) - .child(SharedString::new_static(icon_name.into())) - }, - )) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/icon/decorated_icon.rs b/crates/ui/src/components/icon/decorated_icon.rs deleted file mode 100644 index 82ca844c38..0000000000 --- a/crates/ui/src/components/icon/decorated_icon.rs +++ /dev/null @@ -1,106 +0,0 @@ -use gpui::{AnyElement, IntoElement, Point}; - -use crate::{IconDecoration, IconDecorationKind, prelude::*}; - -#[derive(IntoElement, RegisterComponent)] -pub struct DecoratedIcon { - icon: Icon, - decoration: Option, -} - -impl DecoratedIcon { - pub fn new(icon: Icon, decoration: Option) -> Self { - Self { icon, decoration } - } -} - -impl RenderOnce for DecoratedIcon { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - div() - .relative() - .size(self.icon.size) - .child(self.icon) - .children(self.decoration) - } -} - -impl Component for DecoratedIcon { - fn scope() -> ComponentScope { - ComponentScope::Images - } - - fn description() -> Option<&'static str> { - Some( - "An icon with an optional decoration overlay (like an X, triangle, or dot) that can be positioned relative to the icon", - ) - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - let decoration_x = IconDecoration::new( - IconDecorationKind::X, - cx.theme().colors().surface_background, - cx, - ) - .color(cx.theme().status().error) - .position(Point { - x: px(-2.), - y: px(-2.), - }); - - let decoration_triangle = IconDecoration::new( - IconDecorationKind::Triangle, - cx.theme().colors().surface_background, - cx, - ) - .color(cx.theme().status().error) - .position(Point { - x: px(-2.), - y: px(-2.), - }); - - let decoration_dot = IconDecoration::new( - IconDecorationKind::Dot, - cx.theme().colors().surface_background, - cx, - ) - .color(cx.theme().status().error) - .position(Point { - x: px(-2.), - y: px(-2.), - }); - - Some( - v_flex() - .gap_6() - .children(vec![example_group_with_title( - "Decorations", - vec![ - single_example( - "No Decoration", - DecoratedIcon::new(Icon::new(IconName::FileDoc), None) - .into_any_element(), - ), - single_example( - "X Decoration", - DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_x)) - .into_any_element(), - ), - single_example( - "Triangle Decoration", - DecoratedIcon::new( - Icon::new(IconName::FileDoc), - Some(decoration_triangle), - ) - .into_any_element(), - ), - single_example( - "Dot Decoration", - DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_dot)) - .into_any_element(), - ), - ], - )]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/icon/icon_decoration.rs b/crates/ui/src/components/icon/icon_decoration.rs deleted file mode 100644 index 9f84a8bcf4..0000000000 --- a/crates/ui/src/components/icon/icon_decoration.rs +++ /dev/null @@ -1,159 +0,0 @@ -use std::sync::Arc; - -use gpui::{Hsla, IntoElement, Point, svg}; -use strum::{EnumIter, EnumString, IntoStaticStr}; - -use crate::prelude::*; - -const ICON_DECORATION_SIZE: Pixels = px(11.); - -/// An icon silhouette used to knockout the background of an element for an icon -/// to sit on top of it, emulating a stroke/border. -#[derive(Debug, PartialEq, Eq, Copy, Clone, EnumIter, EnumString, IntoStaticStr)] -#[strum(serialize_all = "snake_case")] -pub enum KnockoutIconName { - XFg, - XBg, - DotFg, - DotBg, - TriangleFg, - TriangleBg, -} - -impl KnockoutIconName { - /// Returns the path to this icon. - pub fn path(&self) -> Arc { - let file_stem: &'static str = self.into(); - format!("icons/knockouts/{file_stem}.svg").into() - } -} - -#[derive(Debug, PartialEq, Eq, Copy, Clone, EnumIter, EnumString)] -pub enum IconDecorationKind { - X, - Dot, - Triangle, -} - -impl IconDecorationKind { - fn fg(&self) -> KnockoutIconName { - match self { - Self::X => KnockoutIconName::XFg, - Self::Dot => KnockoutIconName::DotFg, - Self::Triangle => KnockoutIconName::TriangleFg, - } - } - - fn bg(&self) -> KnockoutIconName { - match self { - Self::X => KnockoutIconName::XBg, - Self::Dot => KnockoutIconName::DotBg, - Self::Triangle => KnockoutIconName::TriangleBg, - } - } -} - -/// The decoration for an icon. -/// -/// For example, this can show an indicator, an "x", or a diagonal strikethrough -/// to indicate something is disabled. -#[derive(IntoElement)] -pub struct IconDecoration { - kind: IconDecorationKind, - color: Hsla, - knockout_color: Hsla, - knockout_hover_color: Hsla, - position: Point, - group_name: Option, -} - -impl IconDecoration { - /// Creates a new [`IconDecoration`]. - pub fn new(kind: IconDecorationKind, knockout_color: Hsla, cx: &App) -> Self { - let color = cx.theme().colors().icon; - let position = Point::default(); - - Self { - kind, - color, - knockout_color, - knockout_hover_color: knockout_color, - position, - group_name: None, - } - } - - /// Sets the kind of decoration. - pub fn kind(mut self, kind: IconDecorationKind) -> Self { - self.kind = kind; - self - } - - /// Sets the color of the decoration. - pub fn color(mut self, color: Hsla) -> Self { - self.color = color; - self - } - - /// Sets the color of the decoration's knockout - /// - /// Match this to the background of the element the icon will be rendered - /// on. - pub fn knockout_color(mut self, color: Hsla) -> Self { - self.knockout_color = color; - self - } - - /// Sets the color of the decoration that is used on hover. - pub fn knockout_hover_color(mut self, color: Hsla) -> Self { - self.knockout_hover_color = color; - self - } - - /// Sets the position of the decoration. - pub fn position(mut self, position: Point) -> Self { - self.position = position; - self - } - - /// Sets the name of the group the decoration belongs to - pub fn group_name(mut self, name: Option) -> Self { - self.group_name = name; - self - } -} - -impl RenderOnce for IconDecoration { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let foreground = svg() - .absolute() - .bottom_0() - .right_0() - .size(ICON_DECORATION_SIZE) - .path(self.kind.fg().path()) - .text_color(self.color); - - let background = svg() - .absolute() - .bottom_0() - .right_0() - .size(ICON_DECORATION_SIZE) - .path(self.kind.bg().path()) - .text_color(self.knockout_color) - .map(|this| match self.group_name { - Some(group_name) => this.group_hover(group_name, |style| { - style.text_color(self.knockout_hover_color) - }), - None => this.hover(|style| style.text_color(self.knockout_hover_color)), - }); - - div() - .size(ICON_DECORATION_SIZE) - .flex_none() - .absolute() - .bottom(self.position.y) - .right(self.position.x) - .child(foreground) - .child(background) - } -} diff --git a/crates/ui/src/components/image.rs b/crates/ui/src/components/image.rs deleted file mode 100644 index 3e8cbd8fff..0000000000 --- a/crates/ui/src/components/image.rs +++ /dev/null @@ -1,184 +0,0 @@ -use std::sync::Arc; - -use gpui::Transformation; -use gpui::{App, IntoElement, Rems, RenderOnce, Size, Styled, Window, svg}; -use serde::{Deserialize, Serialize}; -use strum::{EnumIter, EnumString, IntoStaticStr}; - -use crate::Color; -use crate::prelude::*; -use crate::traits::transformable::Transformable; - -#[derive( - Debug, PartialEq, Eq, Copy, Clone, EnumIter, EnumString, IntoStaticStr, Serialize, Deserialize, -)] -#[strum(serialize_all = "snake_case")] -pub enum VectorName { - AcpGrid, - AcpLogo, - AcpLogoSerif, - AiGrid, - DebuggerGrid, - Grid, - ProTrialStamp, - ProUserStamp, - ZedLogo, - ZedXCopilot, -} - -impl VectorName { - /// Returns the path to this vector image. - pub fn path(&self) -> Arc { - let file_stem: &'static str = self.into(); - format!("images/{file_stem}.svg").into() - } -} - -/// A vector image, such as an SVG. -/// -/// A [`Vector`] is different from an [`crate::Icon`] in that it is intended -/// to be displayed at a specific size, or series of sizes, rather -/// than conforming to the standard size of an icon. -#[derive(IntoElement, RegisterComponent)] -pub struct Vector { - path: Arc, - color: Color, - size: Size, - transformation: Transformation, -} - -impl Vector { - /// Creates a new [`Vector`] image with the given [`VectorName`] and size. - pub fn new(vector: VectorName, width: Rems, height: Rems) -> Self { - Self { - path: vector.path(), - color: Color::default(), - size: Size { width, height }, - transformation: Transformation::default(), - } - } - - /// Creates a new [`Vector`] image where the width and height are the same. - pub fn square(vector: VectorName, size: Rems) -> Self { - Self::new(vector, size, size) - } - - /// Sets the vector color. - pub fn color(mut self, color: Color) -> Self { - self.color = color; - self - } - - /// Sets the vector size. - pub fn size(mut self, size: impl Into>) -> Self { - let size = size.into(); - self.size = size; - self - } -} - -impl Transformable for Vector { - fn transform(mut self, transformation: Transformation) -> Self { - self.transformation = transformation; - self - } -} - -impl RenderOnce for Vector { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let width = self.size.width; - let height = self.size.height; - - svg() - // By default, prevent the SVG from stretching - // to fill its container. - .flex_none() - .w(width) - .h(height) - .path(self.path) - .text_color(self.color.color(cx)) - .with_transformation(self.transformation) - } -} - -impl Component for Vector { - fn scope() -> ComponentScope { - ComponentScope::Images - } - - fn name() -> &'static str { - "Vector" - } - - fn description() -> Option<&'static str> { - Some("A vector image component that can be displayed at specific sizes.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - let size = rems_from_px(60.); - - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Usage", - vec![ - single_example( - "Default", - Vector::square(VectorName::ZedLogo, size).into_any_element(), - ), - single_example( - "Custom Size", - h_flex() - .h(rems_from_px(120.)) - .justify_center() - .child(Vector::new( - VectorName::ZedLogo, - rems_from_px(120.), - rems_from_px(200.), - )) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Colored", - vec![ - single_example( - "Accent Color", - Vector::square(VectorName::ZedLogo, size) - .color(Color::Accent) - .into_any_element(), - ), - single_example( - "Error Color", - Vector::square(VectorName::ZedLogo, size) - .color(Color::Error) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Different Vectors", - vec![single_example( - "Zed X Copilot", - Vector::square(VectorName::ZedXCopilot, rems_from_px(100.)) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn vector_path() { - assert_eq!(VectorName::ZedLogo.path().as_ref(), "images/zed_logo.svg"); - } -} diff --git a/crates/ui/src/components/indent_guides.rs b/crates/ui/src/components/indent_guides.rs deleted file mode 100644 index 60aa23b44c..0000000000 --- a/crates/ui/src/components/indent_guides.rs +++ /dev/null @@ -1,596 +0,0 @@ -use std::{cmp::Ordering, ops::Range, rc::Rc}; - -use gpui::{AnyElement, App, Bounds, Entity, Hsla, Point, fill, point, size}; -use gpui::{DispatchPhase, Hitbox, HitboxBehavior, MouseButton, MouseDownEvent, MouseMoveEvent}; -use smallvec::SmallVec; - -use crate::prelude::*; - -/// Represents the colors used for different states of indent guides. -#[derive(Debug, Clone)] -pub struct IndentGuideColors { - /// The color of the indent guide when it's neither active nor hovered. - pub default: Hsla, - /// The color of the indent guide when it's hovered. - pub hover: Hsla, - /// The color of the indent guide when it's active. - pub active: Hsla, -} - -impl IndentGuideColors { - /// Returns the indent guide colors that should be used for panels. - pub fn panel(cx: &App) -> Self { - Self { - default: cx.theme().colors().panel_indent_guide, - hover: cx.theme().colors().panel_indent_guide_hover, - active: cx.theme().colors().panel_indent_guide_active, - } - } -} - -pub struct IndentGuides { - colors: IndentGuideColors, - indent_size: Pixels, - compute_indents_fn: - Option, &mut Window, &mut App) -> SmallVec<[usize; 64]>>>, - render_fn: Option< - Box< - dyn Fn( - RenderIndentGuideParams, - &mut Window, - &mut App, - ) -> SmallVec<[RenderedIndentGuide; 12]>, - >, - >, - on_click: Option>, -} - -pub fn indent_guides(indent_size: Pixels, colors: IndentGuideColors) -> IndentGuides { - IndentGuides { - colors, - indent_size, - compute_indents_fn: None, - render_fn: None, - on_click: None, - } -} - -impl IndentGuides { - /// Sets the callback that will be called when the user clicks on an indent guide. - pub fn on_click( - mut self, - on_click: impl Fn(&IndentGuideLayout, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_click = Some(Rc::new(on_click)); - self - } - - /// Sets the function that computes indents for uniform list decoration. - pub fn with_compute_indents_fn( - mut self, - entity: Entity, - compute_indents_fn: impl Fn( - &mut V, - Range, - &mut Window, - &mut Context, - ) -> SmallVec<[usize; 64]> - + 'static, - ) -> Self { - let compute_indents_fn = Box::new(move |range, window: &mut Window, cx: &mut App| { - entity.update(cx, |this, cx| compute_indents_fn(this, range, window, cx)) - }); - self.compute_indents_fn = Some(compute_indents_fn); - self - } - - /// Sets a custom callback that will be called when the indent guides need to be rendered. - pub fn with_render_fn( - mut self, - entity: Entity, - render_fn: impl Fn( - &mut V, - RenderIndentGuideParams, - &mut Window, - &mut App, - ) -> SmallVec<[RenderedIndentGuide; 12]> - + 'static, - ) -> Self { - let render_fn = move |params, window: &mut Window, cx: &mut App| { - entity.update(cx, |this, cx| render_fn(this, params, window, cx)) - }; - self.render_fn = Some(Box::new(render_fn)); - self - } - - fn render_from_layout( - &self, - indent_guides: SmallVec<[IndentGuideLayout; 12]>, - bounds: Bounds, - item_height: Pixels, - window: &mut Window, - cx: &mut App, - ) -> AnyElement { - let mut indent_guides = if let Some(ref custom_render) = self.render_fn { - let params = RenderIndentGuideParams { - indent_guides, - indent_size: self.indent_size, - item_height, - }; - custom_render(params, window, cx) - } else { - indent_guides - .into_iter() - .map(|layout| RenderedIndentGuide { - bounds: Bounds::new( - point( - layout.offset.x * self.indent_size, - layout.offset.y * item_height, - ), - size(px(1.), layout.length * item_height), - ), - layout, - is_active: false, - hitbox: None, - }) - .collect() - }; - for guide in &mut indent_guides { - guide.bounds.origin += bounds.origin; - if let Some(hitbox) = guide.hitbox.as_mut() { - hitbox.origin += bounds.origin; - } - } - - let indent_guides = IndentGuidesElement { - indent_guides: Rc::new(indent_guides), - colors: self.colors.clone(), - on_hovered_indent_guide_click: self.on_click.clone(), - }; - indent_guides.into_any_element() - } -} - -/// Parameters for rendering indent guides. -pub struct RenderIndentGuideParams { - /// The calculated layouts for the indent guides to be rendered. - pub indent_guides: SmallVec<[IndentGuideLayout; 12]>, - /// The size of each indentation level in pixels. - pub indent_size: Pixels, - /// The height of each item in pixels. - pub item_height: Pixels, -} - -/// Represents a rendered indent guide with its visual properties and interaction areas. -pub struct RenderedIndentGuide { - /// The bounds of the rendered indent guide in pixels. - pub bounds: Bounds, - /// The layout information for the indent guide. - pub layout: IndentGuideLayout, - /// Indicates whether the indent guide is currently active. - pub is_active: bool, - /// Can be used to customize the hitbox of the indent guide, - /// if this is set to `None`, the bounds of the indent guide will be used. - pub hitbox: Option>, -} - -/// Represents the layout information for an indent guide. -#[derive(Debug, PartialEq, Eq, Hash)] -pub struct IndentGuideLayout { - /// The starting position of the indent guide, where x is the indentation level - /// and y is the starting row. - pub offset: Point, - /// The length of the indent guide in rows. - pub length: usize, - /// Indicates whether the indent guide continues beyond the visible bounds. - pub continues_offscreen: bool, -} - -/// Implements the necessary functionality for rendering indent guides inside a uniform list. -mod uniform_list { - use gpui::UniformListDecoration; - - use super::*; - - impl UniformListDecoration for IndentGuides { - fn compute( - &self, - mut visible_range: Range, - bounds: Bounds, - _scroll_offset: Point, - item_height: Pixels, - item_count: usize, - window: &mut Window, - cx: &mut App, - ) -> AnyElement { - let includes_trailing_indent = visible_range.end < item_count; - // Check if we have entries after the visible range, - // if so extend the visible range so we can fetch a trailing indent, - // which is needed to compute indent guides correctly. - if includes_trailing_indent { - visible_range.end += 1; - } - let Some(ref compute_indents_fn) = self.compute_indents_fn else { - panic!("compute_indents_fn is required for UniformListDecoration"); - }; - let visible_entries = &compute_indents_fn(visible_range.clone(), window, cx); - let indent_guides = compute_indent_guides( - visible_entries, - visible_range.start, - includes_trailing_indent, - ); - self.render_from_layout(indent_guides, bounds, item_height, window, cx) - } - } -} - -/// Implements the necessary functionality for rendering indent guides inside a sticky items. -mod sticky_items { - use crate::StickyItemsDecoration; - - use super::*; - - impl StickyItemsDecoration for IndentGuides { - fn compute( - &self, - indents: &SmallVec<[usize; 8]>, - bounds: Bounds, - _scroll_offset: Point, - item_height: Pixels, - window: &mut Window, - cx: &mut App, - ) -> AnyElement { - let indent_guides = compute_indent_guides(indents, 0, false); - self.render_from_layout(indent_guides, bounds, item_height, window, cx) - } - } -} - -struct IndentGuidesElement { - colors: IndentGuideColors, - indent_guides: Rc>, - on_hovered_indent_guide_click: Option>, -} - -enum IndentGuidesElementPrepaintState { - Static, - Interactive { - hitboxes: Rc>, - on_hovered_indent_guide_click: Rc, - }, -} - -impl Element for IndentGuidesElement { - type RequestLayoutState = (); - type PrepaintState = IndentGuidesElementPrepaintState; - - fn id(&self) -> Option { - None - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _id: Option<&gpui::GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (gpui::LayoutId, Self::RequestLayoutState) { - (window.request_layout(gpui::Style::default(), [], cx), ()) - } - - fn prepaint( - &mut self, - _id: Option<&gpui::GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - _bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - _cx: &mut App, - ) -> Self::PrepaintState { - if let Some(on_hovered_indent_guide_click) = self.on_hovered_indent_guide_click.clone() { - let hitboxes = self - .indent_guides - .as_ref() - .iter() - .map(|guide| { - window - .insert_hitbox(guide.hitbox.unwrap_or(guide.bounds), HitboxBehavior::Normal) - }) - .collect(); - Self::PrepaintState::Interactive { - hitboxes: Rc::new(hitboxes), - on_hovered_indent_guide_click, - } - } else { - Self::PrepaintState::Static - } - } - - fn paint( - &mut self, - _id: Option<&gpui::GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - _bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - prepaint: &mut Self::PrepaintState, - window: &mut Window, - _cx: &mut App, - ) { - let current_view = window.current_view(); - - match prepaint { - IndentGuidesElementPrepaintState::Static => { - for indent_guide in self.indent_guides.as_ref() { - let fill_color = if indent_guide.is_active { - self.colors.active - } else { - self.colors.default - }; - - window.paint_quad(fill(indent_guide.bounds, fill_color)); - } - } - IndentGuidesElementPrepaintState::Interactive { - hitboxes, - on_hovered_indent_guide_click, - } => { - window.on_mouse_event({ - let hitboxes = hitboxes.clone(); - let indent_guides = self.indent_guides.clone(); - let on_hovered_indent_guide_click = on_hovered_indent_guide_click.clone(); - move |event: &MouseDownEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble && event.button == MouseButton::Left { - let mut active_hitbox_ix = None; - for (i, hitbox) in hitboxes.iter().enumerate() { - if hitbox.is_hovered(window) { - active_hitbox_ix = Some(i); - break; - } - } - - let Some(active_hitbox_ix) = active_hitbox_ix else { - return; - }; - - let active_indent_guide = &indent_guides[active_hitbox_ix].layout; - on_hovered_indent_guide_click(active_indent_guide, window, cx); - - cx.stop_propagation(); - window.prevent_default(); - } - } - }); - let mut hovered_hitbox_id = None; - for (i, hitbox) in hitboxes.iter().enumerate() { - window.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox); - let indent_guide = &self.indent_guides[i]; - let fill_color = if hitbox.is_hovered(window) { - hovered_hitbox_id = Some(hitbox.id); - self.colors.hover - } else if indent_guide.is_active { - self.colors.active - } else { - self.colors.default - }; - - window.paint_quad(fill(indent_guide.bounds, fill_color)); - } - - window.on_mouse_event({ - let prev_hovered_hitbox_id = hovered_hitbox_id; - let hitboxes = hitboxes.clone(); - move |_: &MouseMoveEvent, phase, window, cx| { - let mut hovered_hitbox_id = None; - for hitbox in hitboxes.as_ref() { - if hitbox.is_hovered(window) { - hovered_hitbox_id = Some(hitbox.id); - break; - } - } - if phase == DispatchPhase::Capture { - // If the hovered hitbox has changed, we need to re-paint the indent guides. - match (prev_hovered_hitbox_id, hovered_hitbox_id) { - (Some(prev_id), Some(id)) => { - if prev_id != id { - cx.notify(current_view) - } - } - (None, Some(_)) => cx.notify(current_view), - (Some(_), None) => cx.notify(current_view), - (None, None) => {} - } - } - } - }); - } - } - } -} - -impl IntoElement for IndentGuidesElement { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -fn compute_indent_guides( - indents: &[usize], - offset: usize, - includes_trailing_indent: bool, -) -> SmallVec<[IndentGuideLayout; 12]> { - let mut indent_guides = SmallVec::<[IndentGuideLayout; 12]>::new(); - let mut indent_stack = SmallVec::<[IndentGuideLayout; 8]>::new(); - - let mut min_depth = usize::MAX; - for (row, &depth) in indents.iter().enumerate() { - if includes_trailing_indent && row == indents.len() - 1 { - continue; - } - - let current_row = row + offset; - let current_depth = indent_stack.len(); - if depth < min_depth { - min_depth = depth; - } - - match depth.cmp(¤t_depth) { - Ordering::Less => { - for _ in 0..(current_depth - depth) { - if let Some(guide) = indent_stack.pop() { - indent_guides.push(guide); - } - } - } - Ordering::Greater => { - for new_depth in current_depth..depth { - indent_stack.push(IndentGuideLayout { - offset: Point::new(new_depth, current_row), - length: current_row, - continues_offscreen: false, - }); - } - } - _ => {} - } - - for indent in indent_stack.iter_mut() { - indent.length = current_row - indent.offset.y + 1; - } - } - - indent_guides.extend(indent_stack); - - for guide in indent_guides.iter_mut() { - if includes_trailing_indent - && guide.offset.y + guide.length == offset + indents.len().saturating_sub(1) - { - guide.continues_offscreen = indents - .last() - .map(|last_indent| guide.offset.x < *last_indent) - .unwrap_or(false); - } - } - - indent_guides -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_compute_indent_guides() { - fn assert_compute_indent_guides( - input: &[usize], - offset: usize, - includes_trailing_indent: bool, - expected: Vec, - ) { - use std::collections::HashSet; - assert_eq!( - compute_indent_guides(input, offset, includes_trailing_indent) - .into_vec() - .into_iter() - .collect::>(), - expected.into_iter().collect::>(), - ); - } - - assert_compute_indent_guides( - &[0, 1, 2, 2, 1, 0], - 0, - false, - vec![ - IndentGuideLayout { - offset: Point::new(0, 1), - length: 4, - continues_offscreen: false, - }, - IndentGuideLayout { - offset: Point::new(1, 2), - length: 2, - continues_offscreen: false, - }, - ], - ); - - assert_compute_indent_guides( - &[2, 2, 2, 1, 1], - 0, - false, - vec![ - IndentGuideLayout { - offset: Point::new(0, 0), - length: 5, - continues_offscreen: false, - }, - IndentGuideLayout { - offset: Point::new(1, 0), - length: 3, - continues_offscreen: false, - }, - ], - ); - - assert_compute_indent_guides( - &[1, 2, 3, 2, 1], - 0, - false, - vec![ - IndentGuideLayout { - offset: Point::new(0, 0), - length: 5, - continues_offscreen: false, - }, - IndentGuideLayout { - offset: Point::new(1, 1), - length: 3, - continues_offscreen: false, - }, - IndentGuideLayout { - offset: Point::new(2, 2), - length: 1, - continues_offscreen: false, - }, - ], - ); - - assert_compute_indent_guides( - &[0, 1, 0], - 0, - true, - vec![IndentGuideLayout { - offset: Point::new(0, 1), - length: 1, - continues_offscreen: false, - }], - ); - - assert_compute_indent_guides( - &[0, 1, 1], - 0, - true, - vec![IndentGuideLayout { - offset: Point::new(0, 1), - length: 1, - continues_offscreen: true, - }], - ); - assert_compute_indent_guides( - &[0, 1, 2], - 0, - true, - vec![IndentGuideLayout { - offset: Point::new(0, 1), - length: 1, - continues_offscreen: true, - }], - ); - } -} diff --git a/crates/ui/src/components/indicator.rs b/crates/ui/src/components/indicator.rs deleted file mode 100644 index 59d69a068b..0000000000 --- a/crates/ui/src/components/indicator.rs +++ /dev/null @@ -1,177 +0,0 @@ -use super::AnyIcon; -use crate::prelude::*; - -#[derive(Default)] -enum IndicatorKind { - #[default] - Dot, - Bar, - Icon(AnyIcon), -} - -#[derive(IntoElement, RegisterComponent)] -pub struct Indicator { - kind: IndicatorKind, - border_color: Option, - pub color: Color, -} - -impl Indicator { - pub fn dot() -> Self { - Self { - kind: IndicatorKind::Dot, - border_color: None, - color: Color::Default, - } - } - - pub fn bar() -> Self { - Self { - kind: IndicatorKind::Bar, - border_color: None, - - color: Color::Default, - } - } - - pub fn icon(icon: impl Into) -> Self { - Self { - kind: IndicatorKind::Icon(icon.into()), - border_color: None, - - color: Color::Default, - } - } - - pub fn color(mut self, color: Color) -> Self { - self.color = color; - self - } - - pub fn border_color(mut self, color: Color) -> Self { - self.border_color = Some(color); - self - } -} - -impl RenderOnce for Indicator { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let container = div().flex_none(); - let container = if let Some(border_color) = self.border_color { - if matches!(self.kind, IndicatorKind::Dot | IndicatorKind::Bar) { - container.border_1().border_color(border_color.color(cx)) - } else { - container - } - } else { - container - }; - - match self.kind { - IndicatorKind::Icon(icon) => container - .child(icon.map(|icon| icon.custom_size(rems_from_px(8.)).color(self.color))), - IndicatorKind::Dot => container - .w_1p5() - .h_1p5() - .rounded_full() - .bg(self.color.color(cx)), - IndicatorKind::Bar => container - .w_full() - .h_1p5() - .rounded_t_sm() - .bg(self.color.color(cx)), - } - } -} - -impl Component for Indicator { - fn scope() -> ComponentScope { - ComponentScope::Status - } - - fn description() -> Option<&'static str> { - Some( - "Visual indicators used to represent status, notifications, or draw attention to specific elements.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Dot Indicators", - vec![ - single_example("Default", Indicator::dot().into_any_element()), - single_example( - "Success", - Indicator::dot().color(Color::Success).into_any_element(), - ), - single_example( - "Warning", - Indicator::dot().color(Color::Warning).into_any_element(), - ), - single_example( - "Error", - Indicator::dot().color(Color::Error).into_any_element(), - ), - single_example( - "With Border", - Indicator::dot() - .color(Color::Accent) - .border_color(Color::Default) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Bar Indicators", - vec![ - single_example("Default", Indicator::bar().into_any_element()), - single_example( - "Success", - Indicator::bar().color(Color::Success).into_any_element(), - ), - single_example( - "Warning", - Indicator::bar().color(Color::Warning).into_any_element(), - ), - single_example( - "Error", - Indicator::bar().color(Color::Error).into_any_element(), - ), - ], - ), - example_group_with_title( - "Icon Indicators", - vec![ - single_example( - "Default", - Indicator::icon(Icon::new(IconName::Circle)).into_any_element(), - ), - single_example( - "Success", - Indicator::icon(Icon::new(IconName::Check)) - .color(Color::Success) - .into_any_element(), - ), - single_example( - "Warning", - Indicator::icon(Icon::new(IconName::Warning)) - .color(Color::Warning) - .into_any_element(), - ), - single_example( - "Error", - Indicator::icon(Icon::new(IconName::Close)) - .color(Color::Error) - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs deleted file mode 100644 index e22669995d..0000000000 --- a/crates/ui/src/components/keybinding.rs +++ /dev/null @@ -1,740 +0,0 @@ -use std::rc::Rc; - -use crate::PlatformStyle; -use crate::{Icon, IconName, IconSize, h_flex, prelude::*}; -use gpui::{ - Action, AnyElement, App, FocusHandle, Global, IntoElement, KeybindingKeystroke, Keystroke, - Modifiers, Window, relative, -}; -use itertools::Itertools; -use settings::KeybindSource; - -#[derive(Debug)] -enum Source { - Action { - action: Box, - focus_handle: Option, - }, - Keystrokes { - /// A keybinding consists of a set of keystrokes, - /// where each keystroke is a key and a set of modifier keys. - /// More than one keystroke produces a chord. - /// - /// This should always contain at least one keystroke. - keystrokes: Rc<[KeybindingKeystroke]>, - }, -} - -impl Clone for Source { - fn clone(&self) -> Self { - match self { - Source::Action { - action, - focus_handle, - } => Source::Action { - action: action.boxed_clone(), - focus_handle: focus_handle.clone(), - }, - Source::Keystrokes { keystrokes } => Source::Keystrokes { - keystrokes: keystrokes.clone(), - }, - } - } -} - -#[derive(Clone, Debug, IntoElement, RegisterComponent)] -pub struct KeyBinding { - source: Source, - size: Option, - /// The [`PlatformStyle`] to use when displaying this keybinding. - platform_style: PlatformStyle, - /// Determines whether the keybinding is meant for vim mode. - vim_mode: bool, - /// Indicates whether the keybinding is currently disabled. - disabled: bool, -} - -struct VimStyle(bool); -impl Global for VimStyle {} - -impl KeyBinding { - /// Returns the highest precedence keybinding for an action. This is the last binding added to - /// the keymap. User bindings are added after built-in bindings so that they take precedence. - pub fn for_action(action: &dyn Action, cx: &App) -> Self { - Self::new(action, None, cx) - } - - /// Like `for_action`, but lets you specify the context from which keybindings are matched. - pub fn for_action_in(action: &dyn Action, focus: &FocusHandle, cx: &App) -> Self { - Self::new(action, Some(focus.clone()), cx) - } - pub fn has_binding(&self, window: &Window) -> bool { - match &self.source { - Source::Action { - action, - focus_handle: Some(focus), - } => window - .highest_precedence_binding_for_action_in(action.as_ref(), focus) - .or_else(|| window.highest_precedence_binding_for_action(action.as_ref())) - .is_some(), - _ => false, - } - } - - pub fn set_vim_mode(cx: &mut App, enabled: bool) { - cx.set_global(VimStyle(enabled)); - } - - fn is_vim_mode(cx: &App) -> bool { - cx.try_global::().is_some_and(|g| g.0) - } - - pub fn new(action: &dyn Action, focus_handle: Option, cx: &App) -> Self { - Self { - source: Source::Action { - action: action.boxed_clone(), - focus_handle, - }, - size: None, - vim_mode: KeyBinding::is_vim_mode(cx), - platform_style: PlatformStyle::platform(), - disabled: false, - } - } - - pub fn from_keystrokes(keystrokes: Rc<[KeybindingKeystroke]>, source: KeybindSource) -> Self { - Self { - source: Source::Keystrokes { keystrokes }, - size: None, - vim_mode: source == KeybindSource::Vim, - platform_style: PlatformStyle::platform(), - disabled: false, - } - } - - /// Sets the [`PlatformStyle`] for this [`KeyBinding`]. - pub fn platform_style(mut self, platform_style: PlatformStyle) -> Self { - self.platform_style = platform_style; - self - } - - /// Sets the size for this [`KeyBinding`]. - pub fn size(mut self, size: impl Into) -> Self { - self.size = Some(size.into()); - self - } - - /// Sets whether this keybinding is currently disabled. - /// Disabled keybinds will be rendered in a dimmed state. - pub fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } -} - -fn render_key( - key: &str, - color: Option, - platform_style: PlatformStyle, - size: impl Into>, -) -> AnyElement { - let key_icon = icon_for_key(key, platform_style); - match key_icon { - Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(), - None => { - let key = util::capitalize(key); - Key::new(&key, color).size(size).into_any_element() - } - } -} - -impl RenderOnce for KeyBinding { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let render_keybinding = |keystrokes: &[KeybindingKeystroke]| { - let color = self.disabled.then_some(Color::Disabled); - - h_flex() - .debug_selector(|| { - format!( - "KEY_BINDING-{}", - keystrokes - .iter() - .map(|k| k.key().to_string()) - .collect::>() - .join(" ") - ) - }) - .gap(DynamicSpacing::Base04.rems(cx)) - .flex_none() - .children(keystrokes.iter().map(|keystroke| { - h_flex() - .flex_none() - .py_0p5() - .rounded_xs() - .text_color(cx.theme().colors().text_muted) - .children(render_keybinding_keystroke( - keystroke, - color, - self.size, - PlatformStyle::platform(), - self.vim_mode, - )) - })) - .into_any_element() - }; - - match self.source { - Source::Action { - action, - focus_handle, - } => focus_handle - .or_else(|| window.focused(cx)) - .and_then(|focus| { - window.highest_precedence_binding_for_action_in(action.as_ref(), &focus) - }) - .or_else(|| window.highest_precedence_binding_for_action(action.as_ref())) - .map(|binding| render_keybinding(binding.keystrokes())), - Source::Keystrokes { keystrokes } => Some(render_keybinding(keystrokes.as_ref())), - } - .unwrap_or_else(|| gpui::Empty.into_any_element()) - } -} - -pub fn render_keybinding_keystroke( - keystroke: &KeybindingKeystroke, - color: Option, - size: impl Into>, - platform_style: PlatformStyle, - vim_mode: bool, -) -> Vec { - let use_text = vim_mode - || matches!( - platform_style, - PlatformStyle::Linux | PlatformStyle::Windows - ); - let size = size.into(); - - if use_text { - let element = Key::new( - keystroke_text( - keystroke.modifiers(), - keystroke.key(), - platform_style, - vim_mode, - ), - color, - ) - .size(size) - .into_any_element(); - vec![element] - } else { - let mut elements = Vec::new(); - elements.extend(render_modifiers( - keystroke.modifiers(), - platform_style, - color, - size, - true, - )); - elements.push(render_key(keystroke.key(), color, platform_style, size)); - elements - } -} - -fn icon_for_key(key: &str, platform_style: PlatformStyle) -> Option { - match key { - "left" => Some(IconName::ArrowLeft), - "right" => Some(IconName::ArrowRight), - "up" => Some(IconName::ArrowUp), - "down" => Some(IconName::ArrowDown), - "backspace" => Some(IconName::Backspace), - "delete" => Some(IconName::Backspace), - "return" => Some(IconName::Return), - "enter" => Some(IconName::Return), - "tab" => Some(IconName::Tab), - "space" => Some(IconName::Space), - "escape" => Some(IconName::Escape), - "pagedown" => Some(IconName::PageDown), - "pageup" => Some(IconName::PageUp), - "shift" if platform_style == PlatformStyle::Mac => Some(IconName::Shift), - "control" if platform_style == PlatformStyle::Mac => Some(IconName::Control), - "platform" if platform_style == PlatformStyle::Mac => Some(IconName::Command), - "function" if platform_style == PlatformStyle::Mac => Some(IconName::Control), - "alt" if platform_style == PlatformStyle::Mac => Some(IconName::Option), - _ => None, - } -} - -pub fn render_modifiers( - modifiers: &Modifiers, - platform_style: PlatformStyle, - color: Option, - size: Option, - trailing_separator: bool, -) -> impl Iterator { - #[derive(Clone)] - enum KeyOrIcon { - Key(&'static str), - Plus, - Icon(IconName), - } - - struct Modifier { - enabled: bool, - mac: KeyOrIcon, - linux: KeyOrIcon, - windows: KeyOrIcon, - } - - let table = { - use KeyOrIcon::*; - - [ - Modifier { - enabled: modifiers.function, - mac: Icon(IconName::Control), - linux: Key("Fn"), - windows: Key("Fn"), - }, - Modifier { - enabled: modifiers.control, - mac: Icon(IconName::Control), - linux: Key("Ctrl"), - windows: Key("Ctrl"), - }, - Modifier { - enabled: modifiers.alt, - mac: Icon(IconName::Option), - linux: Key("Alt"), - windows: Key("Alt"), - }, - Modifier { - enabled: modifiers.platform, - mac: Icon(IconName::Command), - linux: Key("Super"), - windows: Key("Win"), - }, - Modifier { - enabled: modifiers.shift, - mac: Icon(IconName::Shift), - linux: Key("Shift"), - windows: Key("Shift"), - }, - ] - }; - - let filtered = table - .into_iter() - .filter(|modifier| modifier.enabled) - .collect::>(); - - let platform_keys = filtered - .into_iter() - .map(move |modifier| match platform_style { - PlatformStyle::Mac => Some(modifier.mac), - PlatformStyle::Linux => Some(modifier.linux), - PlatformStyle::Windows => Some(modifier.windows), - }); - - let separator = match platform_style { - PlatformStyle::Mac => None, - PlatformStyle::Linux => Some(KeyOrIcon::Plus), - PlatformStyle::Windows => Some(KeyOrIcon::Plus), - }; - - let platform_keys = itertools::intersperse(platform_keys, separator.clone()); - - platform_keys - .chain(if modifiers.modified() && trailing_separator { - Some(separator) - } else { - None - }) - .flatten() - .map(move |key_or_icon| match key_or_icon { - KeyOrIcon::Key(key) => Key::new(key, color).size(size).into_any_element(), - KeyOrIcon::Icon(icon) => KeyIcon::new(icon, color).size(size).into_any_element(), - KeyOrIcon::Plus => "+".into_any_element(), - }) -} - -#[derive(IntoElement)] -pub struct Key { - key: SharedString, - color: Option, - size: Option, -} - -impl RenderOnce for Key { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let single_char = self.key.len() == 1; - let size = self - .size - .unwrap_or_else(|| TextSize::default().rems(cx).into()); - - div() - .py_0() - .map(|this| { - if single_char { - this.w(size).flex().flex_none().justify_center() - } else { - this.px_0p5() - } - }) - .h(size) - .text_size(size) - .line_height(relative(1.)) - .text_color(self.color.unwrap_or(Color::Muted).color(cx)) - .child(self.key) - } -} - -impl Key { - pub fn new(key: impl Into, color: Option) -> Self { - Self { - key: key.into(), - color, - size: None, - } - } - - pub fn size(mut self, size: impl Into>) -> Self { - self.size = size.into(); - self - } -} - -#[derive(IntoElement)] -pub struct KeyIcon { - icon: IconName, - color: Option, - size: Option, -} - -impl RenderOnce for KeyIcon { - fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { - let size = self.size.unwrap_or(IconSize::Small.rems().into()); - - Icon::new(self.icon) - .size(IconSize::Custom(size.to_rems(window.rem_size()))) - .color(self.color.unwrap_or(Color::Muted)) - } -} - -impl KeyIcon { - pub fn new(icon: IconName, color: Option) -> Self { - Self { - icon, - color, - size: None, - } - } - - pub fn size(mut self, size: impl Into>) -> Self { - self.size = size.into(); - self - } -} - -/// Returns a textual representation of the key binding for the given [`Action`]. -pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option { - let key_binding = window.highest_precedence_binding_for_action(action)?; - Some(text_for_keybinding_keystrokes(key_binding.keystrokes(), cx)) -} - -pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String { - let platform_style = PlatformStyle::platform(); - let vim_enabled = KeyBinding::is_vim_mode(cx); - keystrokes - .iter() - .map(|keystroke| { - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - platform_style, - vim_enabled, - ) - }) - .join(" ") -} - -pub fn text_for_keybinding_keystrokes(keystrokes: &[KeybindingKeystroke], cx: &App) -> String { - let platform_style = PlatformStyle::platform(); - let vim_enabled = KeyBinding::is_vim_mode(cx); - keystrokes - .iter() - .map(|keystroke| { - keystroke_text( - keystroke.modifiers(), - keystroke.key(), - platform_style, - vim_enabled, - ) - }) - .join(" ") -} - -pub fn text_for_keystroke(modifiers: &Modifiers, key: &str, cx: &App) -> String { - let platform_style = PlatformStyle::platform(); - keystroke_text(modifiers, key, platform_style, KeyBinding::is_vim_mode(cx)) -} - -/// Returns a textual representation of the given [`Keystroke`]. -fn keystroke_text( - modifiers: &Modifiers, - key: &str, - platform_style: PlatformStyle, - vim_mode: bool, -) -> String { - let mut text = String::new(); - let delimiter = '-'; - - if modifiers.function { - match vim_mode { - false => text.push_str("Fn"), - true => text.push_str("fn"), - } - - text.push(delimiter); - } - - if modifiers.control { - match (platform_style, vim_mode) { - (PlatformStyle::Mac, false) => text.push_str("Control"), - (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Ctrl"), - (_, true) => text.push_str("ctrl"), - } - - text.push(delimiter); - } - - if modifiers.platform { - match (platform_style, vim_mode) { - (PlatformStyle::Mac, false) => text.push_str("Command"), - (PlatformStyle::Mac, true) => text.push_str("cmd"), - (PlatformStyle::Linux, false) => text.push_str("Super"), - (PlatformStyle::Linux, true) => text.push_str("super"), - (PlatformStyle::Windows, false) => text.push_str("Win"), - (PlatformStyle::Windows, true) => text.push_str("win"), - } - - text.push(delimiter); - } - - if modifiers.alt { - match (platform_style, vim_mode) { - (PlatformStyle::Mac, false) => text.push_str("Option"), - (PlatformStyle::Mac, true) => text.push_str("option"), - (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Alt"), - (_, true) => text.push_str("alt"), - } - - text.push(delimiter); - } - - if modifiers.shift { - match (platform_style, vim_mode) { - (_, false) => text.push_str("Shift"), - (_, true) => text.push_str("shift"), - } - text.push(delimiter); - } - - if vim_mode { - text.push_str(key) - } else { - let key = match key { - "pageup" => "PageUp", - "pagedown" => "PageDown", - key => &util::capitalize(key), - }; - text.push_str(key); - } - - text -} - -impl Component for KeyBinding { - fn scope() -> ComponentScope { - ComponentScope::Typography - } - - fn name() -> &'static str { - "KeyBinding" - } - - fn description() -> Option<&'static str> { - Some( - "A component that displays a key binding, supporting different platform styles and vim mode.", - ) - } - - // fn preview(_window: &mut Window, cx: &mut App) -> Option { - // Some( - // v_flex() - // .gap_6() - // .children(vec![ - // example_group_with_title( - // "Basic Usage", - // vec![ - // single_example( - // "Default", - // KeyBinding::new_from_gpui( - // gpui::KeyBinding::new("ctrl-s", gpui::NoAction, None), - // cx, - // ) - // .into_any_element(), - // ), - // single_example( - // "Mac Style", - // KeyBinding::new_from_gpui( - // gpui::KeyBinding::new("cmd-s", gpui::NoAction, None), - // cx, - // ) - // .platform_style(PlatformStyle::Mac) - // .into_any_element(), - // ), - // single_example( - // "Windows Style", - // KeyBinding::new_from_gpui( - // gpui::KeyBinding::new("ctrl-s", gpui::NoAction, None), - // cx, - // ) - // .platform_style(PlatformStyle::Windows) - // .into_any_element(), - // ), - // ], - // ), - // example_group_with_title( - // "Vim Mode", - // vec![single_example( - // "Vim Mode Enabled", - // KeyBinding::new_from_gpui( - // gpui::KeyBinding::new("dd", gpui::NoAction, None), - // cx, - // ) - // .vim_mode(true) - // .into_any_element(), - // )], - // ), - // example_group_with_title( - // "Complex Bindings", - // vec![ - // single_example( - // "Multiple Keys", - // KeyBinding::new_from_gpui( - // gpui::KeyBinding::new("ctrl-k ctrl-b", gpui::NoAction, None), - // cx, - // ) - // .into_any_element(), - // ), - // single_example( - // "With Shift", - // KeyBinding::new_from_gpui( - // gpui::KeyBinding::new("shift-cmd-p", gpui::NoAction, None), - // cx, - // ) - // .into_any_element(), - // ), - // ], - // ), - // ]) - // .into_any_element(), - // ) - // } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_text_for_keystroke() { - let keystroke = Keystroke::parse("cmd-c").unwrap(); - assert_eq!( - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - PlatformStyle::Mac, - false - ), - "Command-C".to_string() - ); - assert_eq!( - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - PlatformStyle::Linux, - false - ), - "Super-C".to_string() - ); - assert_eq!( - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - PlatformStyle::Windows, - false - ), - "Win-C".to_string() - ); - - let keystroke = Keystroke::parse("ctrl-alt-delete").unwrap(); - assert_eq!( - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - PlatformStyle::Mac, - false - ), - "Control-Option-Delete".to_string() - ); - assert_eq!( - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - PlatformStyle::Linux, - false - ), - "Ctrl-Alt-Delete".to_string() - ); - assert_eq!( - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - PlatformStyle::Windows, - false - ), - "Ctrl-Alt-Delete".to_string() - ); - - let keystroke = Keystroke::parse("shift-pageup").unwrap(); - assert_eq!( - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - PlatformStyle::Mac, - false - ), - "Shift-PageUp".to_string() - ); - assert_eq!( - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - PlatformStyle::Linux, - false, - ), - "Shift-PageUp".to_string() - ); - assert_eq!( - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - PlatformStyle::Windows, - false - ), - "Shift-PageUp".to_string() - ); - } -} diff --git a/crates/ui/src/components/keybinding_hint.rs b/crates/ui/src/components/keybinding_hint.rs deleted file mode 100644 index c998e29f0e..0000000000 --- a/crates/ui/src/components/keybinding_hint.rs +++ /dev/null @@ -1,341 +0,0 @@ -use crate::KeyBinding; -use crate::prelude::*; -use gpui::{AnyElement, App, BoxShadow, FontStyle, Hsla, IntoElement, Window, point}; -use theme::Appearance; - -/// Represents a hint for a keybinding, optionally with a prefix and suffix. -/// -/// This struct allows for the creation and customization of a keybinding hint, -/// which can be used to display keyboard shortcuts or commands in a user interface. -/// -/// # Examples -/// -/// ```no_run -/// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; -/// use ui::prelude::*; -/// use ui::{KeyBinding, KeybindingHint}; -/// use settings::KeybindSource; -/// -/// # fn example(cx: &App) { -/// let hint = KeybindingHint::new( -/// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-s").unwrap())].into(), KeybindSource::Base), -/// Hsla::black() -/// ) -/// .prefix("Save:") -/// .size(Pixels::from(14.0)); -/// # } -/// ``` -#[derive(Debug, IntoElement, RegisterComponent)] -pub struct KeybindingHint { - prefix: Option, - suffix: Option, - keybinding: KeyBinding, - size: Option, - background_color: Hsla, -} - -impl KeybindingHint { - /// Creates a new `KeybindingHint` with the specified keybinding. - /// - /// This method initializes a new `KeybindingHint` instance with the given keybinding, - /// setting all other fields to their default values. - /// - /// # Examples - /// - /// ```no_run - /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; - /// use ui::prelude::*; - /// use ui::{KeyBinding, KeybindingHint}; - /// use settings::KeybindSource; - /// - /// # fn example(cx: &App) { - /// let hint = KeybindingHint::new( - /// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-c").unwrap())].into(), KeybindSource::Base), - /// Hsla::black() - /// ); - /// # } - /// ``` - pub fn new(keybinding: KeyBinding, background_color: Hsla) -> Self { - Self { - prefix: None, - suffix: None, - keybinding, - size: None, - background_color, - } - } - - /// Creates a new `KeybindingHint` with a prefix and keybinding. - /// - /// This method initializes a new `KeybindingHint` instance with the given prefix and keybinding, - /// setting all other fields to their default values. - /// - /// # Examples - /// - /// ```no_run - /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; - /// use ui::prelude::*; - /// use ui::{KeyBinding, KeybindingHint}; - /// use settings::KeybindSource; - /// - /// # fn example(cx: &App) { - /// let hint = KeybindingHint::with_prefix( - /// "Copy:", - /// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-c").unwrap())].into(), KeybindSource::Base), - /// Hsla::black() - /// ); - /// # } - /// ``` - pub fn with_prefix( - prefix: impl Into, - keybinding: KeyBinding, - background_color: Hsla, - ) -> Self { - Self { - prefix: Some(prefix.into()), - suffix: None, - keybinding, - size: None, - background_color, - } - } - - /// Creates a new `KeybindingHint` with a keybinding and suffix. - /// - /// This method initializes a new `KeybindingHint` instance with the given keybinding and suffix, - /// setting all other fields to their default values. - /// - /// # Examples - /// - /// ```no_run - /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; - /// use ui::prelude::*; - /// use ui::{KeyBinding, KeybindingHint}; - /// use settings::KeybindSource; - /// - /// # fn example(cx: &App) { - /// let hint = KeybindingHint::with_suffix( - /// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-v").unwrap())].into(), KeybindSource::Base), - /// "Paste", - /// Hsla::black() - /// ); - /// # } - /// ``` - pub fn with_suffix( - keybinding: KeyBinding, - suffix: impl Into, - background_color: Hsla, - ) -> Self { - Self { - prefix: None, - suffix: Some(suffix.into()), - keybinding, - size: None, - background_color, - } - } - - /// Sets the prefix for the keybinding hint. - /// - /// This method allows adding or changing the prefix text that appears before the keybinding. - /// - /// # Examples - /// - /// ```no_run - /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; - /// use ui::prelude::*; - /// use ui::{KeyBinding, KeybindingHint}; - /// use settings::KeybindSource; - /// - /// # fn example(cx: &App) { - /// let hint = KeybindingHint::new( - /// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-x").unwrap())].into(), KeybindSource::Base), - /// Hsla::black() - /// ) - /// .prefix("Cut:"); - /// # } - /// ``` - pub fn prefix(mut self, prefix: impl Into) -> Self { - self.prefix = Some(prefix.into()); - self - } - - /// Sets the suffix for the keybinding hint. - /// - /// This method allows adding or changing the suffix text that appears after the keybinding. - /// - /// # Examples - /// - /// ```no_run - /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; - /// use ui::prelude::*; - /// use ui::{KeyBinding, KeybindingHint}; - /// use settings::KeybindSource; - /// - /// # fn example(cx: &App) { - /// let hint = KeybindingHint::new( - /// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-f").unwrap())].into(), KeybindSource::Base), - /// Hsla::black() - /// ) - /// .suffix("Find"); - /// # } - /// ``` - pub fn suffix(mut self, suffix: impl Into) -> Self { - self.suffix = Some(suffix.into()); - self - } - - /// Sets the size of the keybinding hint. - /// - /// This method allows specifying the size of the keybinding hint in pixels. - /// - /// # Examples - /// - /// ```no_run - /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; - /// use ui::prelude::*; - /// use ui::{KeyBinding, KeybindingHint}; - /// use settings::KeybindSource; - /// - /// # fn example(cx: &App) { - /// let hint = KeybindingHint::new( - /// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-z").unwrap())].into(), KeybindSource::Base), - /// Hsla::black() - /// ) - /// .size(Pixels::from(16.0)); - /// # } - /// ``` - pub fn size(mut self, size: impl Into>) -> Self { - self.size = size.into(); - self - } -} - -impl RenderOnce for KeybindingHint { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let colors = cx.theme().colors(); - let is_light = cx.theme().appearance() == Appearance::Light; - - let border_color = - self.background_color - .blend(colors.text.alpha(if is_light { 0.08 } else { 0.16 })); - - let bg_color = self - .background_color - .blend(colors.text_accent.alpha(if is_light { 0.05 } else { 0.1 })); - - let shadow_color = colors.text.alpha(if is_light { 0.04 } else { 0.08 }); - - let size = self - .size - .unwrap_or(TextSize::Small.rems(cx).to_pixels(window.rem_size())); - - let kb_size = size - px(2.0); - - let mut base = h_flex(); - - base.text_style() - .get_or_insert_with(Default::default) - .font_style = Some(FontStyle::Italic); - - base.gap_1() - .font_buffer(cx) - .text_size(size) - .text_color(colors.text_disabled) - .children(self.prefix) - .child( - h_flex() - .rounded_sm() - .px_0p5() - .mr_0p5() - .border_1() - .border_color(border_color) - .bg(bg_color) - .shadow(vec![BoxShadow { - color: shadow_color, - offset: point(px(0.), px(1.)), - blur_radius: px(0.), - spread_radius: px(0.), - }]) - .child(self.keybinding.size(rems_from_px(kb_size))), - ) - .children(self.suffix) - } -} - -impl Component for KeybindingHint { - fn scope() -> ComponentScope { - ComponentScope::DataDisplay - } - - fn description() -> Option<&'static str> { - Some("Displays a keyboard shortcut hint with optional prefix and suffix text") - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - let enter = KeyBinding::for_action(&menu::Confirm, cx); - - let bg_color = cx.theme().colors().surface_background; - - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic", - vec![ - single_example( - "With Prefix", - KeybindingHint::with_prefix( - "Go to Start:", - enter.clone(), - bg_color, - ) - .into_any_element(), - ), - single_example( - "With Suffix", - KeybindingHint::with_suffix(enter.clone(), "Go to End", bg_color) - .into_any_element(), - ), - single_example( - "With Prefix and Suffix", - KeybindingHint::new(enter.clone(), bg_color) - .prefix("Confirm:") - .suffix("Execute selected action") - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Sizes", - vec![ - single_example( - "Small", - KeybindingHint::new(enter.clone(), bg_color) - .size(Pixels::from(12.0)) - .prefix("Small:") - .into_any_element(), - ), - single_example( - "Medium", - KeybindingHint::new(enter.clone(), bg_color) - .size(Pixels::from(16.0)) - .suffix("Medium") - .into_any_element(), - ), - single_example( - "Large", - KeybindingHint::new(enter, bg_color) - .size(Pixels::from(20.0)) - .prefix("Large:") - .suffix("Size") - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/label.rs b/crates/ui/src/components/label.rs deleted file mode 100644 index dc830559ca..0000000000 --- a/crates/ui/src/components/label.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod highlighted_label; -mod label; -mod label_like; -mod loading_label; -mod spinner_label; - -pub use highlighted_label::*; -pub use label::*; -pub use label_like::*; -pub use loading_label::*; -pub use spinner_label::*; diff --git a/crates/ui/src/components/label/highlighted_label.rs b/crates/ui/src/components/label/highlighted_label.rs deleted file mode 100644 index 840bba7b17..0000000000 --- a/crates/ui/src/components/label/highlighted_label.rs +++ /dev/null @@ -1,242 +0,0 @@ -use std::ops::Range; - -use gpui::{FontWeight, HighlightStyle, StyledText}; - -use crate::{LabelCommon, LabelLike, LabelSize, LineHeightStyle, prelude::*}; - -#[derive(IntoElement, RegisterComponent)] -pub struct HighlightedLabel { - base: LabelLike, - label: SharedString, - highlight_indices: Vec, -} - -impl HighlightedLabel { - /// Constructs a label with the given characters highlighted. - /// Characters are identified by UTF-8 byte position. - pub fn new(label: impl Into, highlight_indices: Vec) -> Self { - let label = label.into(); - for &run in &highlight_indices { - assert!( - label.is_char_boundary(run), - "highlight index {run} is not a valid UTF-8 boundary" - ); - } - Self { - base: LabelLike::new(), - label, - highlight_indices, - } - } - - pub fn text(&self) -> &str { - self.label.as_str() - } - - pub fn highlight_indices(&self) -> &[usize] { - &self.highlight_indices - } -} - -impl LabelCommon for HighlightedLabel { - fn size(mut self, size: LabelSize) -> Self { - self.base = self.base.size(size); - self - } - - fn weight(mut self, weight: FontWeight) -> Self { - self.base = self.base.weight(weight); - self - } - - fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self { - self.base = self.base.line_height_style(line_height_style); - self - } - - fn color(mut self, color: Color) -> Self { - self.base = self.base.color(color); - self - } - - fn strikethrough(mut self) -> Self { - self.base = self.base.strikethrough(); - self - } - - fn italic(mut self) -> Self { - self.base = self.base.italic(); - self - } - - fn alpha(mut self, alpha: f32) -> Self { - self.base = self.base.alpha(alpha); - self - } - - fn underline(mut self) -> Self { - self.base = self.base.underline(); - self - } - - fn truncate(mut self) -> Self { - self.base = self.base.truncate(); - self - } - - fn single_line(mut self) -> Self { - self.base = self.base.single_line(); - self - } - - fn buffer_font(mut self, cx: &App) -> Self { - self.base = self.base.buffer_font(cx); - self - } - - fn inline_code(mut self, cx: &App) -> Self { - self.base = self.base.inline_code(cx); - self - } -} - -pub fn highlight_ranges( - text: &str, - indices: &[usize], - style: HighlightStyle, -) -> Vec<(Range, HighlightStyle)> { - let mut highlight_indices = indices.iter().copied().peekable(); - let mut highlights: Vec<(Range, HighlightStyle)> = Vec::new(); - - while let Some(start_ix) = highlight_indices.next() { - let mut end_ix = start_ix; - - loop { - end_ix += text[end_ix..].chars().next().map_or(0, |c| c.len_utf8()); - if highlight_indices.next_if(|&ix| ix == end_ix).is_none() { - break; - } - } - - highlights.push((start_ix..end_ix, style)); - } - - highlights -} - -impl RenderOnce for HighlightedLabel { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let highlight_color = cx.theme().colors().text_accent; - - let highlights = highlight_ranges( - &self.label, - &self.highlight_indices, - HighlightStyle { - color: Some(highlight_color), - ..Default::default() - }, - ); - - let mut text_style = window.text_style(); - text_style.color = self.base.color.color(cx); - - self.base - .child(StyledText::new(self.label).with_default_highlights(&text_style, highlights)) - } -} - -impl Component for HighlightedLabel { - fn scope() -> ComponentScope { - ComponentScope::Typography - } - - fn name() -> &'static str { - "HighlightedLabel" - } - - fn description() -> Option<&'static str> { - Some("A label with highlighted characters based on specified indices.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Usage", - vec![ - single_example( - "Default", - HighlightedLabel::new("Highlighted Text", vec![0, 1, 2, 3]).into_any_element(), - ), - single_example( - "Custom Color", - HighlightedLabel::new("Colored Highlight", vec![0, 1, 7, 8, 9]) - .color(Color::Accent) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Styles", - vec![ - single_example( - "Bold", - HighlightedLabel::new("Bold Highlight", vec![0, 1, 2, 3]) - .weight(FontWeight::BOLD) - .into_any_element(), - ), - single_example( - "Italic", - HighlightedLabel::new("Italic Highlight", vec![0, 1, 6, 7, 8]) - .italic() - .into_any_element(), - ), - single_example( - "Underline", - HighlightedLabel::new("Underlined Highlight", vec![0, 1, 10, 11, 12]) - .underline() - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Sizes", - vec![ - single_example( - "Small", - HighlightedLabel::new("Small Highlight", vec![0, 1, 5, 6, 7]) - .size(LabelSize::Small) - .into_any_element(), - ), - single_example( - "Large", - HighlightedLabel::new("Large Highlight", vec![0, 1, 5, 6, 7]) - .size(LabelSize::Large) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Special Cases", - vec![ - single_example( - "Single Line", - HighlightedLabel::new("Single Line Highlight\nWith Newline", vec![0, 1, 7, 8, 9]) - .single_line() - .into_any_element(), - ), - single_example( - "Truncate", - HighlightedLabel::new("This is a very long text that should be truncated with highlights", vec![0, 1, 2, 3, 4, 5]) - .truncate() - .into_any_element(), - ), - ], - ), - ]) - .into_any_element() - ) - } -} diff --git a/crates/ui/src/components/label/label.rs b/crates/ui/src/components/label/label.rs deleted file mode 100644 index 49e2de94a1..0000000000 --- a/crates/ui/src/components/label/label.rs +++ /dev/null @@ -1,266 +0,0 @@ -use crate::{LabelLike, prelude::*}; -use gpui::StyleRefinement; - -/// A struct representing a label element in the UI. -/// -/// The `Label` struct stores the label text and common properties for a label element. -/// It provides methods for modifying these properties. -/// -/// # Examples -/// -/// ``` -/// use ui::prelude::*; -/// -/// Label::new("Hello, World!"); -/// ``` -/// -/// **A colored label**, for example labeling a dangerous action: -/// -/// ``` -/// use ui::prelude::*; -/// -/// let my_label = Label::new("Delete").color(Color::Error); -/// ``` -/// -/// **A label with a strikethrough**, for example labeling something that has been deleted: -/// -/// ``` -/// use ui::prelude::*; -/// -/// let my_label = Label::new("Deleted").strikethrough(); -/// ``` -#[derive(IntoElement, RegisterComponent)] -pub struct Label { - base: LabelLike, - label: SharedString, -} - -impl Label { - /// Creates a new [`Label`] with the given text. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// let my_label = Label::new("Hello, World!"); - /// ``` - pub fn new(label: impl Into) -> Self { - Self { - base: LabelLike::new(), - label: label.into(), - } - } - - /// Sets the text of the [`Label`]. - pub fn set_text(&mut self, text: impl Into) { - self.label = text.into(); - } -} - -// Style methods. -impl Label { - fn style(&mut self) -> &mut StyleRefinement { - self.base.base.style() - } - - gpui::margin_style_methods!({ - visibility: pub - }); -} - -impl LabelCommon for Label { - /// Sets the size of the label using a [`LabelSize`]. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// let my_label = Label::new("Hello, World!").size(LabelSize::Small); - /// ``` - fn size(mut self, size: LabelSize) -> Self { - self.base = self.base.size(size); - self - } - - /// Sets the weight of the label using a [`FontWeight`]. - /// - /// # Examples - /// - /// ``` - /// use gpui::FontWeight; - /// use ui::prelude::*; - /// - /// let my_label = Label::new("Hello, World!").weight(FontWeight::BOLD); - /// ``` - fn weight(mut self, weight: gpui::FontWeight) -> Self { - self.base = self.base.weight(weight); - self - } - - /// Sets the line height style of the label using a [`LineHeightStyle`]. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// let my_label = Label::new("Hello, World!").line_height_style(LineHeightStyle::UiLabel); - /// ``` - fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self { - self.base = self.base.line_height_style(line_height_style); - self - } - - /// Sets the color of the label using a [`Color`]. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// let my_label = Label::new("Hello, World!").color(Color::Accent); - /// ``` - fn color(mut self, color: Color) -> Self { - self.base = self.base.color(color); - self - } - - /// Sets the strikethrough property of the label. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// let my_label = Label::new("Hello, World!").strikethrough(); - /// ``` - fn strikethrough(mut self) -> Self { - self.base = self.base.strikethrough(); - self - } - - /// Sets the italic property of the label. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// let my_label = Label::new("Hello, World!").italic(); - /// ``` - fn italic(mut self) -> Self { - self.base = self.base.italic(); - self - } - - /// Sets the alpha property of the color of label. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// let my_label = Label::new("Hello, World!").alpha(0.5); - /// ``` - fn alpha(mut self, alpha: f32) -> Self { - self.base = self.base.alpha(alpha); - self - } - - fn underline(mut self) -> Self { - self.base = self.base.underline(); - self - } - - /// Truncates overflowing text with an ellipsis (`…`) if needed. - fn truncate(mut self) -> Self { - self.base = self.base.truncate(); - self - } - - fn single_line(mut self) -> Self { - self.label = SharedString::from(self.label.replace('\n', "⏎")); - self.base = self.base.single_line(); - self - } - - fn buffer_font(mut self, cx: &App) -> Self { - self.base = self.base.buffer_font(cx); - self - } - - /// Styles the label to look like inline code. - fn inline_code(mut self, cx: &App) -> Self { - self.base = self.base.inline_code(cx); - self - } -} - -impl RenderOnce for Label { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - self.base.child(self.label) - } -} - -impl Component for Label { - fn scope() -> ComponentScope { - ComponentScope::Typography - } - - fn description() -> Option<&'static str> { - Some("A text label component that supports various styles, sizes, and formatting options.") - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Sizes", - vec![ - single_example("Default", Label::new("Project Explorer").into_any_element()), - single_example("Small", Label::new("File: main.rs").size(LabelSize::Small).into_any_element()), - single_example("Large", Label::new("Welcome to Zed").size(LabelSize::Large).into_any_element()), - ], - ), - example_group_with_title( - "Colors", - vec![ - single_example("Default", Label::new("Status: Ready").into_any_element()), - single_example("Accent", Label::new("New Update Available").color(Color::Accent).into_any_element()), - single_example("Error", Label::new("Build Failed").color(Color::Error).into_any_element()), - ], - ), - example_group_with_title( - "Styles", - vec![ - single_example("Default", Label::new("Normal Text").into_any_element()), - single_example("Bold", Label::new("Important Notice").weight(gpui::FontWeight::BOLD).into_any_element()), - single_example("Italic", Label::new("Code Comment").italic().into_any_element()), - single_example("Strikethrough", Label::new("Deprecated Feature").strikethrough().into_any_element()), - single_example("Underline", Label::new("Clickable Link").underline().into_any_element()), - single_example("Inline Code", Label::new("fn main() {}").inline_code(cx).into_any_element()), - ], - ), - example_group_with_title( - "Line Height Styles", - vec![ - single_example("Default", Label::new("Multi-line\nText\nExample").into_any_element()), - single_example("UI Label", Label::new("Compact\nUI\nLabel").line_height_style(LineHeightStyle::UiLabel).into_any_element()), - ], - ), - example_group_with_title( - "Special Cases", - vec![ - single_example("Single Line", Label::new("Line 1\nLine 2\nLine 3").single_line().into_any_element()), - single_example("Text Ellipsis", div().max_w_24().child(Label::new("This is a very long file name that should be truncated: very_long_file_name_with_many_words.rs").truncate()).into_any_element()), - ], - ), - ]) - .into_any_element() - ) - } -} diff --git a/crates/ui/src/components/label/label_like.rs b/crates/ui/src/components/label/label_like.rs deleted file mode 100644 index 1fa6b14c83..0000000000 --- a/crates/ui/src/components/label/label_like.rs +++ /dev/null @@ -1,315 +0,0 @@ -use crate::prelude::*; -use gpui::{FontWeight, StyleRefinement, UnderlineStyle}; -use settings::Settings; -use smallvec::SmallVec; -use theme::ThemeSettings; - -/// Sets the size of a label -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] -pub enum LabelSize { - /// The default size of a label. - #[default] - Default, - /// The large size of a label. - Large, - /// The small size of a label. - Small, - /// The extra small size of a label. - XSmall, -} - -/// Sets the line height of a label -#[derive(Default, PartialEq, Copy, Clone)] -pub enum LineHeightStyle { - /// The default line height style of a label, - /// set by either the UI's default line height, - /// or the developer's default buffer line height. - #[default] - TextLabel, - /// Sets the line height to 1. - UiLabel, -} - -/// A common set of traits all labels must implement. -pub trait LabelCommon { - /// Sets the size of the label using a [`LabelSize`]. - fn size(self, size: LabelSize) -> Self; - - /// Sets the font weight of the label. - fn weight(self, weight: FontWeight) -> Self; - - /// Sets the line height style of the label using a [`LineHeightStyle`]. - fn line_height_style(self, line_height_style: LineHeightStyle) -> Self; - - /// Sets the color of the label using a [`Color`]. - fn color(self, color: Color) -> Self; - - /// Sets the strikethrough property of the label. - fn strikethrough(self) -> Self; - - /// Sets the italic property of the label. - fn italic(self) -> Self; - - /// Sets the underline property of the label - fn underline(self) -> Self; - - /// Sets the alpha property of the label, overwriting the alpha value of the color. - fn alpha(self, alpha: f32) -> Self; - - /// Truncates overflowing text with an ellipsis (`…`) if needed. - fn truncate(self) -> Self; - - /// Sets the label to render as a single line. - fn single_line(self) -> Self; - - /// Sets the font to the buffer's - fn buffer_font(self, cx: &App) -> Self; - - /// Styles the label to look like inline code. - fn inline_code(self, cx: &App) -> Self; -} - -/// A label-like element that can be used to create a custom label when -/// prebuilt labels are not sufficient. Use this sparingly, as it is -/// unconstrained and may make the UI feel less consistent. -/// -/// This is also used to build the prebuilt labels. -#[derive(IntoElement)] -pub struct LabelLike { - pub(super) base: Div, - size: LabelSize, - weight: Option, - line_height_style: LineHeightStyle, - pub(crate) color: Color, - strikethrough: bool, - italic: bool, - children: SmallVec<[AnyElement; 2]>, - alpha: Option, - underline: bool, - single_line: bool, - truncate: bool, -} - -impl Default for LabelLike { - fn default() -> Self { - Self::new() - } -} - -impl LabelLike { - /// Creates a new, fully custom label. - /// Prefer using [`Label`] or [`HighlightedLabel`] where possible. - pub fn new() -> Self { - Self { - base: div(), - size: LabelSize::Default, - weight: None, - line_height_style: LineHeightStyle::default(), - color: Color::Default, - strikethrough: false, - italic: false, - children: SmallVec::new(), - alpha: None, - underline: false, - single_line: false, - truncate: false, - } - } -} - -// Style methods. -impl LabelLike { - fn style(&mut self) -> &mut StyleRefinement { - self.base.style() - } - - gpui::margin_style_methods!({ - visibility: pub - }); -} - -impl LabelCommon for LabelLike { - fn size(mut self, size: LabelSize) -> Self { - self.size = size; - self - } - - fn weight(mut self, weight: FontWeight) -> Self { - self.weight = Some(weight); - self - } - - fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self { - self.line_height_style = line_height_style; - self - } - - fn color(mut self, color: Color) -> Self { - self.color = color; - self - } - - fn strikethrough(mut self) -> Self { - self.strikethrough = true; - self - } - - fn italic(mut self) -> Self { - self.italic = true; - self - } - - fn underline(mut self) -> Self { - self.underline = true; - self - } - - fn alpha(mut self, alpha: f32) -> Self { - self.alpha = Some(alpha); - self - } - - /// Truncates overflowing text with an ellipsis (`…`) if needed. - fn truncate(mut self) -> Self { - self.truncate = true; - self - } - - fn single_line(mut self) -> Self { - self.single_line = true; - self - } - - fn buffer_font(mut self, cx: &App) -> Self { - self.base = self - .base - .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()); - self - } - - fn inline_code(mut self, cx: &App) -> Self { - self.base = self - .base - .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) - .bg(cx.theme().colors().element_background) - .rounded_sm() - .px_0p5(); - self - } -} - -impl ParentElement for LabelLike { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for LabelLike { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let mut color = self.color.color(cx); - if let Some(alpha) = self.alpha { - color.fade_out(1.0 - alpha); - } - - self.base - .map(|this| match self.size { - LabelSize::Large => this.text_ui_lg(cx), - LabelSize::Default => this.text_ui(cx), - LabelSize::Small => this.text_ui_sm(cx), - LabelSize::XSmall => this.text_ui_xs(cx), - }) - .when(self.line_height_style == LineHeightStyle::UiLabel, |this| { - this.line_height(relative(1.)) - }) - .when(self.italic, |this| this.italic()) - .when(self.underline, |mut this| { - this.text_style() - .get_or_insert_with(Default::default) - .underline = Some(UnderlineStyle { - thickness: px(1.), - color: None, - wavy: false, - }); - this - }) - .when(self.strikethrough, |this| this.line_through()) - .when(self.single_line, |this| this.whitespace_nowrap()) - .when(self.truncate, |this| { - this.overflow_x_hidden().text_ellipsis() - }) - .text_color(color) - .font_weight( - self.weight - .unwrap_or(ThemeSettings::get_global(cx).ui_font.weight), - ) - .children(self.children) - } -} - -impl Component for LabelLike { - fn scope() -> ComponentScope { - ComponentScope::Typography - } - - fn name() -> &'static str { - "LabelLike" - } - - fn description() -> Option<&'static str> { - Some( - "A flexible, customizable label-like component that serves as a base for other label types.", - ) - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Sizes", - vec![ - single_example("Default", LabelLike::new().child("Default size").into_any_element()), - single_example("Large", LabelLike::new().size(LabelSize::Large).child("Large size").into_any_element()), - single_example("Small", LabelLike::new().size(LabelSize::Small).child("Small size").into_any_element()), - single_example("XSmall", LabelLike::new().size(LabelSize::XSmall).child("Extra small size").into_any_element()), - ], - ), - example_group_with_title( - "Styles", - vec![ - single_example("Bold", LabelLike::new().weight(FontWeight::BOLD).child("Bold text").into_any_element()), - single_example("Italic", LabelLike::new().italic().child("Italic text").into_any_element()), - single_example("Underline", LabelLike::new().underline().child("Underlined text").into_any_element()), - single_example("Strikethrough", LabelLike::new().strikethrough().child("Strikethrough text").into_any_element()), - single_example("Inline Code", LabelLike::new().inline_code(cx).child("const value = 42;").into_any_element()), - ], - ), - example_group_with_title( - "Colors", - vec![ - single_example("Default", LabelLike::new().child("Default color").into_any_element()), - single_example("Accent", LabelLike::new().color(Color::Accent).child("Accent color").into_any_element()), - single_example("Error", LabelLike::new().color(Color::Error).child("Error color").into_any_element()), - single_example("Alpha", LabelLike::new().alpha(0.5).child("50% opacity").into_any_element()), - ], - ), - example_group_with_title( - "Line Height", - vec![ - single_example("Default", LabelLike::new().child("Default line height\nMulti-line text").into_any_element()), - single_example("UI Label", LabelLike::new().line_height_style(LineHeightStyle::UiLabel).child("UI label line height\nMulti-line text").into_any_element()), - ], - ), - example_group_with_title( - "Special Cases", - vec![ - single_example("Single Line", LabelLike::new().single_line().child("This is a very long text that should be displayed in a single line").into_any_element()), - single_example("Truncate", LabelLike::new().truncate().child("This is a very long text that should be truncated with an ellipsis").into_any_element()), - ], - ), - ]) - .into_any_element() - ) - } -} diff --git a/crates/ui/src/components/label/loading_label.rs b/crates/ui/src/components/label/loading_label.rs deleted file mode 100644 index 0b6b027e47..0000000000 --- a/crates/ui/src/components/label/loading_label.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::prelude::*; -use gpui::{Animation, AnimationExt, FontWeight}; -use std::time::Duration; - -#[derive(IntoElement)] -pub struct LoadingLabel { - base: Label, - text: SharedString, -} - -impl LoadingLabel { - pub fn new(text: impl Into) -> Self { - let text = text.into(); - LoadingLabel { - base: Label::new(text.clone()), - text, - } - } -} - -impl LabelCommon for LoadingLabel { - fn size(mut self, size: LabelSize) -> Self { - self.base = self.base.size(size); - self - } - - fn weight(mut self, weight: FontWeight) -> Self { - self.base = self.base.weight(weight); - self - } - - fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self { - self.base = self.base.line_height_style(line_height_style); - self - } - - fn color(mut self, color: Color) -> Self { - self.base = self.base.color(color); - self - } - - fn strikethrough(mut self) -> Self { - self.base = self.base.strikethrough(); - self - } - - fn italic(mut self) -> Self { - self.base = self.base.italic(); - self - } - - fn alpha(mut self, alpha: f32) -> Self { - self.base = self.base.alpha(alpha); - self - } - - fn underline(mut self) -> Self { - self.base = self.base.underline(); - self - } - - fn truncate(mut self) -> Self { - self.base = self.base.truncate(); - self - } - - fn single_line(mut self) -> Self { - self.base = self.base.single_line(); - self - } - - fn buffer_font(mut self, cx: &App) -> Self { - self.base = self.base.buffer_font(cx); - self - } - - fn inline_code(mut self, cx: &App) -> Self { - self.base = self.base.inline_code(cx); - self - } -} - -impl RenderOnce for LoadingLabel { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let text = self.text.clone(); - - self.base.color(Color::Muted).with_animations( - "loading_label", - vec![ - Animation::new(Duration::from_secs(1)), - Animation::new(Duration::from_secs(1)).repeat(), - ], - move |mut label, animation_ix, delta| { - match animation_ix { - 0 => { - let chars_to_show = (delta * text.len() as f32).ceil() as usize; - let text = SharedString::from(text[0..chars_to_show].to_string()); - label.set_text(text); - } - 1 => match delta { - d if d < 0.25 => label.set_text(text.clone()), - d if d < 0.5 => label.set_text(format!("{}.", text)), - d if d < 0.75 => label.set_text(format!("{}..", text)), - _ => label.set_text(format!("{}...", text)), - }, - _ => {} - } - label - }, - ) - } -} diff --git a/crates/ui/src/components/label/spinner_label.rs b/crates/ui/src/components/label/spinner_label.rs deleted file mode 100644 index 33eeeae125..0000000000 --- a/crates/ui/src/components/label/spinner_label.rs +++ /dev/null @@ -1,205 +0,0 @@ -use crate::prelude::*; -use gpui::{Animation, AnimationExt, FontWeight}; -use std::time::Duration; - -/// Different types of spinner animations -#[derive(Debug, Default, Clone, Copy, PartialEq)] -pub enum SpinnerVariant { - #[default] - Dots, - DotsVariant, - Sand, -} - -/// A spinner indication, based on the label component, that loops through -/// frames of the specified animation. It implements `LabelCommon` as well. -/// -/// # Default Example -/// -/// ``` -/// use ui::{SpinnerLabel}; -/// -/// SpinnerLabel::new(); -/// ``` -/// -/// # Variant Example -/// -/// ``` -/// use ui::{SpinnerLabel}; -/// -/// SpinnerLabel::dots_variant(); -/// ``` -#[derive(IntoElement, RegisterComponent)] -pub struct SpinnerLabel { - base: Label, - variant: SpinnerVariant, - frames: Vec<&'static str>, - duration: Duration, -} - -impl SpinnerVariant { - fn frames(&self) -> Vec<&'static str> { - match self { - SpinnerVariant::Dots => vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"], - SpinnerVariant::DotsVariant => vec!["⣼", "⣹", "⢻", "⠿", "⡟", "⣏", "⣧", "⣶"], - SpinnerVariant::Sand => vec![ - "⠁", "⠂", "⠄", "⡀", "⡈", "⡐", "⡠", "⣀", "⣁", "⣂", "⣄", "⣌", "⣔", "⣤", "⣥", "⣦", - "⣮", "⣶", "⣷", "⣿", "⡿", "⠿", "⢟", "⠟", "⡛", "⠛", "⠫", "⢋", "⠋", "⠍", "⡉", "⠉", - "⠑", "⠡", "⢁", - ], - } - } - - fn duration(&self) -> Duration { - match self { - SpinnerVariant::Dots => Duration::from_millis(1000), - SpinnerVariant::DotsVariant => Duration::from_millis(1000), - SpinnerVariant::Sand => Duration::from_millis(2000), - } - } - - fn animation_id(&self) -> &'static str { - match self { - SpinnerVariant::Dots => "spinner_label_dots", - SpinnerVariant::DotsVariant => "spinner_label_dots_variant", - SpinnerVariant::Sand => "spinner_label_dots_variant_2", - } - } -} - -impl SpinnerLabel { - pub fn new() -> Self { - Self::with_variant(SpinnerVariant::default()) - } - - pub fn with_variant(variant: SpinnerVariant) -> Self { - let frames = variant.frames(); - let duration = variant.duration(); - - SpinnerLabel { - base: Label::new(frames[0]).color(Color::Muted), - variant, - frames, - duration, - } - } - - pub fn dots() -> Self { - Self::with_variant(SpinnerVariant::Dots) - } - - pub fn dots_variant() -> Self { - Self::with_variant(SpinnerVariant::DotsVariant) - } - - pub fn sand() -> Self { - Self::with_variant(SpinnerVariant::Sand) - } -} - -impl LabelCommon for SpinnerLabel { - fn size(mut self, size: LabelSize) -> Self { - self.base = self.base.size(size); - self - } - - fn weight(mut self, weight: FontWeight) -> Self { - self.base = self.base.weight(weight); - self - } - - fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self { - self.base = self.base.line_height_style(line_height_style); - self - } - - fn color(mut self, color: Color) -> Self { - self.base = self.base.color(color); - self - } - - fn strikethrough(mut self) -> Self { - self.base = self.base.strikethrough(); - self - } - - fn italic(mut self) -> Self { - self.base = self.base.italic(); - self - } - - fn alpha(mut self, alpha: f32) -> Self { - self.base = self.base.alpha(alpha); - self - } - - fn underline(mut self) -> Self { - self.base = self.base.underline(); - self - } - - fn truncate(mut self) -> Self { - self.base = self.base.truncate(); - self - } - - fn single_line(mut self) -> Self { - self.base = self.base.single_line(); - self - } - - fn buffer_font(mut self, cx: &App) -> Self { - self.base = self.base.buffer_font(cx); - self - } - - fn inline_code(mut self, cx: &App) -> Self { - self.base = self.base.inline_code(cx); - self - } -} - -impl RenderOnce for SpinnerLabel { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let frames = self.frames.clone(); - let duration = self.duration; - - self.base.with_animation( - self.variant.animation_id(), - Animation::new(duration).repeat(), - move |mut label, delta| { - let frame_index = (delta * frames.len() as f32) as usize % frames.len(); - - label.set_text(frames[frame_index]); - label - }, - ) - } -} - -impl Component for SpinnerLabel { - fn scope() -> ComponentScope { - ComponentScope::Loading - } - - fn name() -> &'static str { - "Spinner Label" - } - - fn sort_name() -> &'static str { - "Spinner Label" - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - let examples = vec![ - single_example("Default", SpinnerLabel::new().into_any_element()), - single_example( - "Dots Variant", - SpinnerLabel::dots_variant().into_any_element(), - ), - single_example("Sand Variant", SpinnerLabel::sand().into_any_element()), - ]; - - Some(example_group(examples).vertical().into_any_element()) - } -} diff --git a/crates/ui/src/components/list.rs b/crates/ui/src/components/list.rs deleted file mode 100644 index 6876f290ce..0000000000 --- a/crates/ui/src/components/list.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod list; -mod list_bullet_item; -mod list_header; -mod list_item; -mod list_separator; -mod list_sub_header; - -pub use list::*; -pub use list_bullet_item::*; -pub use list_header::*; -pub use list_item::*; -pub use list_separator::*; -pub use list_sub_header::*; diff --git a/crates/ui/src/components/list/list.rs b/crates/ui/src/components/list/list.rs deleted file mode 100644 index ccae5bed23..0000000000 --- a/crates/ui/src/components/list/list.rs +++ /dev/null @@ -1,142 +0,0 @@ -use component::{Component, ComponentScope, example_group_with_title, single_example}; -use gpui::AnyElement; -use smallvec::SmallVec; - -use crate::{Label, ListHeader, ListItem, prelude::*}; - -pub enum EmptyMessage { - Text(SharedString), - Element(AnyElement), -} - -#[derive(IntoElement, RegisterComponent)] -pub struct List { - /// Message to display when the list is empty - /// Defaults to "No items" - empty_message: EmptyMessage, - header: Option, - toggle: Option, - children: SmallVec<[AnyElement; 2]>, -} - -impl Default for List { - fn default() -> Self { - Self::new() - } -} - -impl List { - pub fn new() -> Self { - Self { - empty_message: EmptyMessage::Text("No items".into()), - header: None, - toggle: None, - children: SmallVec::new(), - } - } - - pub fn empty_message(mut self, message: impl Into) -> Self { - self.empty_message = message.into(); - self - } - - pub fn header(mut self, header: impl Into>) -> Self { - self.header = header.into(); - self - } - - pub fn toggle(mut self, toggle: impl Into>) -> Self { - self.toggle = toggle.into(); - self - } -} - -impl ParentElement for List { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl From for EmptyMessage { - fn from(s: String) -> Self { - EmptyMessage::Text(SharedString::from(s)) - } -} - -impl From<&str> for EmptyMessage { - fn from(s: &str) -> Self { - EmptyMessage::Text(SharedString::from(s.to_owned())) - } -} - -impl From for EmptyMessage { - fn from(e: AnyElement) -> Self { - EmptyMessage::Element(e) - } -} - -impl RenderOnce for List { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - v_flex() - .w_full() - .py(DynamicSpacing::Base04.rems(cx)) - .children(self.header) - .map(|this| match (self.children.is_empty(), self.toggle) { - (false, _) => this.children(self.children), - (true, Some(false)) => this, - (true, _) => match self.empty_message { - EmptyMessage::Text(text) => { - this.px_2().child(Label::new(text).color(Color::Muted)) - } - EmptyMessage::Element(element) => this.child(element), - }, - }) - } -} - -impl Component for List { - fn scope() -> ComponentScope { - ComponentScope::Layout - } - - fn description() -> Option<&'static str> { - Some( - "A container component for displaying a collection of list items with optional header and empty state.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![example_group_with_title( - "Basic Lists", - vec![ - single_example( - "Simple List", - List::new() - .child(ListItem::new("item1").child(Label::new("Item 1"))) - .child(ListItem::new("item2").child(Label::new("Item 2"))) - .child(ListItem::new("item3").child(Label::new("Item 3"))) - .into_any_element(), - ), - single_example( - "With Header", - List::new() - .header(ListHeader::new("Section Header")) - .child(ListItem::new("item1").child(Label::new("Item 1"))) - .child(ListItem::new("item2").child(Label::new("Item 2"))) - .into_any_element(), - ), - single_example( - "Empty List", - List::new() - .empty_message("No items to display") - .into_any_element(), - ), - ], - )]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/list/list_bullet_item.rs b/crates/ui/src/components/list/list_bullet_item.rs deleted file mode 100644 index 17731488f7..0000000000 --- a/crates/ui/src/components/list/list_bullet_item.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::{ListItem, prelude::*}; -use component::{Component, ComponentScope, example_group_with_title, single_example}; -use gpui::{IntoElement, ParentElement, SharedString}; - -#[derive(IntoElement, RegisterComponent)] -pub struct ListBulletItem { - label: SharedString, -} - -impl ListBulletItem { - pub fn new(label: impl Into) -> Self { - Self { - label: label.into(), - } - } -} - -impl RenderOnce for ListBulletItem { - fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { - let line_height = window.line_height() * 0.85; - - 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() - } -} - -impl Component for ListBulletItem { - fn scope() -> ComponentScope { - ComponentScope::DataDisplay - } - - fn description() -> Option<&'static str> { - Some("A list item with a bullet point indicator for unordered lists.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .child(example_group_with_title( - "Bullet Items", - vec![ - single_example( - "Simple", - ListBulletItem::new("First bullet item").into_any_element(), - ), - single_example( - "Multiple Lines", - v_flex() - .child(ListBulletItem::new("First item")) - .child(ListBulletItem::new("Second item")) - .child(ListBulletItem::new("Third item")) - .into_any_element(), - ), - single_example( - "Long Text", - ListBulletItem::new( - "A longer bullet item that demonstrates text wrapping behavior", - ) - .into_any_element(), - ), - ], - )) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/list/list_header.rs b/crates/ui/src/components/list/list_header.rs deleted file mode 100644 index 8726dca50d..0000000000 --- a/crates/ui/src/components/list/list_header.rs +++ /dev/null @@ -1,218 +0,0 @@ -use std::sync::Arc; - -use crate::{Disclosure, prelude::*}; -use component::{Component, ComponentScope, example_group_with_title, single_example}; -use gpui::{AnyElement, ClickEvent}; -use settings::Settings; -use theme::ThemeSettings; - -#[derive(IntoElement, RegisterComponent)] -pub struct ListHeader { - /// The label of the header. - label: SharedString, - /// A slot for content that appears before the label, like an icon or avatar. - start_slot: Option, - /// A slot for content that appears after the label, usually on the other side of the header. - /// This might be a button, a disclosure arrow, a face pile, etc. - end_slot: Option, - /// A slot for content that appears on hover after the label - /// It will obscure the `end_slot` when visible. - end_hover_slot: Option, - toggle: Option, - on_toggle: Option>, - inset: bool, - selected: bool, -} - -impl ListHeader { - pub fn new(label: impl Into) -> Self { - Self { - label: label.into(), - start_slot: None, - end_slot: None, - end_hover_slot: None, - inset: false, - toggle: None, - on_toggle: None, - selected: false, - } - } - - pub fn toggle(mut self, toggle: impl Into>) -> Self { - self.toggle = toggle.into(); - self - } - - pub fn on_toggle( - mut self, - on_toggle: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_toggle = Some(Arc::new(on_toggle)); - self - } - - pub fn start_slot(mut self, start_slot: impl Into>) -> Self { - self.start_slot = start_slot.into().map(IntoElement::into_any_element); - self - } - - pub fn end_slot(mut self, end_slot: impl Into>) -> Self { - self.end_slot = end_slot.into().map(IntoElement::into_any_element); - self - } - - pub fn end_hover_slot(mut self, end_hover_slot: impl Into>) -> Self { - self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element); - self - } - - pub fn inset(mut self, inset: bool) -> Self { - self.inset = inset; - self - } -} - -impl Toggleable for ListHeader { - fn toggle_state(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -impl RenderOnce for ListHeader { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let ui_density = ThemeSettings::get_global(cx).ui_density; - - h_flex() - .id(self.label.clone()) - .w_full() - .relative() - .group("list_header") - .child( - div() - .map(|this| match ui_density { - theme::UiDensity::Comfortable => this.h_5(), - _ => this.h_7(), - }) - .when(self.inset, |this| this.px_2()) - .when(self.selected, |this| { - this.bg(cx.theme().colors().ghost_element_selected) - }) - .flex() - .flex_1() - .items_center() - .justify_between() - .w_full() - .gap(DynamicSpacing::Base04.rems(cx)) - .child( - h_flex() - .gap(DynamicSpacing::Base04.rems(cx)) - .children(self.toggle.map(|is_open| { - Disclosure::new("toggle", is_open) - .on_toggle_expanded(self.on_toggle.clone()) - })) - .child( - div() - .id("label_container") - .flex() - .gap(DynamicSpacing::Base04.rems(cx)) - .items_center() - .children(self.start_slot) - .child(Label::new(self.label.clone()).color(Color::Muted)) - .when_some(self.on_toggle, |this, on_toggle| { - this.on_click(move |event, window, cx| { - on_toggle(event, window, cx) - }) - }), - ), - ) - .child(h_flex().children(self.end_slot)) - .when_some(self.end_hover_slot, |this, end_hover_slot| { - this.child( - div() - .absolute() - .right_0() - .visible_on_hover("list_header") - .child(end_hover_slot), - ) - }), - ) - } -} - -impl Component for ListHeader { - fn scope() -> ComponentScope { - ComponentScope::DataDisplay - } - - fn description() -> Option<&'static str> { - Some( - "A header component for lists with support for icons, actions, and collapsible sections.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Headers", - vec![ - single_example( - "Simple", - ListHeader::new("Section Header").into_any_element(), - ), - single_example( - "With Icon", - ListHeader::new("Files") - .start_slot(Icon::new(IconName::File)) - .into_any_element(), - ), - single_example( - "With End Slot", - ListHeader::new("Recent") - .end_slot(Label::new("5").color(Color::Muted)) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Collapsible Headers", - vec![ - single_example( - "Expanded", - ListHeader::new("Expanded Section") - .toggle(Some(true)) - .into_any_element(), - ), - single_example( - "Collapsed", - ListHeader::new("Collapsed Section") - .toggle(Some(false)) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "States", - vec![ - single_example( - "Selected", - ListHeader::new("Selected Header") - .toggle_state(true) - .into_any_element(), - ), - single_example( - "Inset", - ListHeader::new("Inset Header") - .inset(true) - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/list/list_item.rs b/crates/ui/src/components/list/list_item.rs deleted file mode 100644 index d581fad945..0000000000 --- a/crates/ui/src/components/list/list_item.rs +++ /dev/null @@ -1,470 +0,0 @@ -use std::sync::Arc; - -use component::{Component, ComponentScope, example_group_with_title, single_example}; -use gpui::{AnyElement, AnyView, ClickEvent, MouseButton, MouseDownEvent, Pixels, px}; -use smallvec::SmallVec; - -use crate::{Disclosure, prelude::*}; - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] -pub enum ListItemSpacing { - #[default] - Dense, - ExtraDense, - Sparse, -} - -#[derive(IntoElement, RegisterComponent)] -pub struct ListItem { - id: ElementId, - group_name: Option, - disabled: bool, - selected: bool, - spacing: ListItemSpacing, - indent_level: usize, - indent_step_size: Pixels, - /// A slot for content that appears before the children, like an icon or avatar. - start_slot: Option, - /// A slot for content that appears after the children, usually on the other side of the header. - /// This might be a button, a disclosure arrow, a face pile, etc. - end_slot: Option, - /// A slot for content that appears on hover after the children - /// It will obscure the `end_slot` when visible. - end_hover_slot: Option, - toggle: Option, - inset: bool, - on_click: Option>, - on_hover: Option>, - on_toggle: Option>, - tooltip: Option AnyView + 'static>>, - on_secondary_mouse_down: Option>, - children: SmallVec<[AnyElement; 2]>, - selectable: bool, - always_show_disclosure_icon: bool, - outlined: bool, - rounded: bool, - overflow_x: bool, - focused: Option, -} - -impl ListItem { - pub fn new(id: impl Into) -> Self { - Self { - id: id.into(), - group_name: None, - disabled: false, - selected: false, - spacing: ListItemSpacing::Dense, - indent_level: 0, - indent_step_size: px(12.), - start_slot: None, - end_slot: None, - end_hover_slot: None, - toggle: None, - inset: false, - on_click: None, - on_secondary_mouse_down: None, - on_toggle: None, - on_hover: None, - tooltip: None, - children: SmallVec::new(), - selectable: true, - always_show_disclosure_icon: false, - outlined: false, - rounded: false, - overflow_x: false, - focused: None, - } - } - - pub fn group_name(mut self, group_name: impl Into) -> Self { - self.group_name = Some(group_name.into()); - self - } - - pub fn spacing(mut self, spacing: ListItemSpacing) -> Self { - self.spacing = spacing; - self - } - - pub fn selectable(mut self, has_hover: bool) -> Self { - self.selectable = has_hover; - self - } - - pub fn always_show_disclosure_icon(mut self, show: bool) -> Self { - self.always_show_disclosure_icon = show; - self - } - - pub fn on_click( - mut self, - handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_click = Some(Box::new(handler)); - self - } - - pub fn on_hover(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { - self.on_hover = Some(Box::new(handler)); - self - } - - pub fn on_secondary_mouse_down( - mut self, - handler: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_secondary_mouse_down = Some(Box::new(handler)); - self - } - - pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.tooltip = Some(Box::new(tooltip)); - self - } - - pub fn inset(mut self, inset: bool) -> Self { - self.inset = inset; - self - } - - pub fn indent_level(mut self, indent_level: usize) -> Self { - self.indent_level = indent_level; - self - } - - pub fn indent_step_size(mut self, indent_step_size: Pixels) -> Self { - self.indent_step_size = indent_step_size; - self - } - - pub fn toggle(mut self, toggle: impl Into>) -> Self { - self.toggle = toggle.into(); - self - } - - pub fn on_toggle( - mut self, - on_toggle: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_toggle = Some(Arc::new(on_toggle)); - self - } - - pub fn start_slot(mut self, start_slot: impl Into>) -> Self { - self.start_slot = start_slot.into().map(IntoElement::into_any_element); - self - } - - pub fn end_slot(mut self, end_slot: impl Into>) -> Self { - self.end_slot = end_slot.into().map(IntoElement::into_any_element); - self - } - - pub fn end_hover_slot(mut self, end_hover_slot: impl Into>) -> Self { - self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element); - self - } - - pub fn outlined(mut self) -> Self { - self.outlined = true; - self - } - - pub fn rounded(mut self) -> Self { - self.rounded = true; - self - } - - pub fn overflow_x(mut self) -> Self { - self.overflow_x = true; - self - } - - pub fn focused(mut self, focused: bool) -> Self { - self.focused = Some(focused); - self - } -} - -impl Disableable for ListItem { - fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } -} - -impl Toggleable for ListItem { - fn toggle_state(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -impl ParentElement for ListItem { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for ListItem { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - h_flex() - .id(self.id) - .when_some(self.group_name, |this, group| this.group(group)) - .w_full() - .relative() - // When an item is inset draw the indent spacing outside of the item - .when(self.inset, |this| { - this.ml(self.indent_level as f32 * self.indent_step_size) - .px(DynamicSpacing::Base04.rems(cx)) - }) - .when(!self.inset && !self.disabled, |this| { - this - // TODO: Add focus state - // .when(self.state == InteractionState::Focused, |this| { - .when_some(self.focused, |this, focused| { - if focused { - this.border_1() - .border_color(cx.theme().colors().border_focused) - } else { - this.border_1() - } - }) - .when(self.selectable, |this| { - this.hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) - .active(|style| style.bg(cx.theme().colors().ghost_element_active)) - .when(self.outlined, |this| this.rounded_sm()) - .when(self.selected, |this| { - this.bg(cx.theme().colors().ghost_element_selected) - }) - }) - }) - .when(self.rounded, |this| this.rounded_sm()) - .when_some(self.on_hover, |this, on_hover| this.on_hover(on_hover)) - .child( - h_flex() - .id("inner_list_item") - .group("list_item") - .w_full() - .relative() - .gap_1() - .px(DynamicSpacing::Base06.rems(cx)) - .map(|this| match self.spacing { - ListItemSpacing::Dense => this, - ListItemSpacing::ExtraDense => this.py_neg_px(), - ListItemSpacing::Sparse => this.py_1(), - }) - .when(self.inset && !self.disabled, |this| { - this - // TODO: Add focus state - //.when(self.state == InteractionState::Focused, |this| { - .when_some(self.focused, |this, focused| { - if focused { - this.border_1() - .border_color(cx.theme().colors().border_focused) - } else { - this.border_1() - } - }) - .when(self.selectable, |this| { - this.hover(|style| { - style.bg(cx.theme().colors().ghost_element_hover) - }) - .active(|style| style.bg(cx.theme().colors().ghost_element_active)) - .when(self.selected, |this| { - this.bg(cx.theme().colors().ghost_element_selected) - }) - }) - }) - .when_some( - self.on_click.filter(|_| !self.disabled), - |this, on_click| this.cursor_pointer().on_click(on_click), - ) - .when(self.outlined, |this| { - this.border_1() - .border_color(cx.theme().colors().border) - .rounded_sm() - .overflow_hidden() - }) - .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| { - this.on_mouse_down(MouseButton::Right, move |event, window, cx| { - (on_mouse_down)(event, window, cx) - }) - }) - .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)) - .map(|this| { - if self.inset { - this.rounded_sm() - } else { - // When an item is not inset draw the indent spacing inside of the item - this.ml(self.indent_level as f32 * self.indent_step_size) - } - }) - .children(self.toggle.map(|is_open| { - div() - .flex() - .absolute() - .left(rems(-1.)) - .when(is_open && !self.always_show_disclosure_icon, |this| { - this.visible_on_hover("") - }) - .child( - Disclosure::new("toggle", is_open) - .on_toggle_expanded(self.on_toggle), - ) - })) - .child( - h_flex() - .flex_grow() - .flex_shrink_0() - .flex_basis(relative(0.25)) - .gap(DynamicSpacing::Base06.rems(cx)) - .map(|list_content| { - if self.overflow_x { - list_content - } else { - list_content.overflow_hidden() - } - }) - .children(self.start_slot) - .children(self.children), - ) - .when_some(self.end_slot, |this, end_slot| { - this.justify_between().child( - h_flex() - .flex_shrink() - .overflow_hidden() - .when(self.end_hover_slot.is_some(), |this| { - this.visible() - .group_hover("list_item", |this| this.invisible()) - }) - .child(end_slot), - ) - }) - .when_some(self.end_hover_slot, |this, end_hover_slot| { - this.child( - h_flex() - .h_full() - .absolute() - .right(DynamicSpacing::Base06.rems(cx)) - .top_0() - .visible_on_hover("list_item") - .child(end_hover_slot), - ) - }), - ) - } -} - -impl Component for ListItem { - fn scope() -> ComponentScope { - ComponentScope::DataDisplay - } - - fn description() -> Option<&'static str> { - Some( - "A flexible list item component with support for icons, actions, disclosure toggles, and hierarchical display.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic List Items", - vec![ - single_example( - "Simple", - ListItem::new("simple") - .child(Label::new("Simple list item")) - .into_any_element(), - ), - single_example( - "With Icon", - ListItem::new("with_icon") - .start_slot(Icon::new(IconName::File)) - .child(Label::new("List item with icon")) - .into_any_element(), - ), - single_example( - "Selected", - ListItem::new("selected") - .toggle_state(true) - .start_slot(Icon::new(IconName::Check)) - .child(Label::new("Selected item")) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "List Item Spacing", - vec![ - single_example( - "Dense", - ListItem::new("dense") - .spacing(ListItemSpacing::Dense) - .child(Label::new("Dense spacing")) - .into_any_element(), - ), - single_example( - "Extra Dense", - ListItem::new("extra_dense") - .spacing(ListItemSpacing::ExtraDense) - .child(Label::new("Extra dense spacing")) - .into_any_element(), - ), - single_example( - "Sparse", - ListItem::new("sparse") - .spacing(ListItemSpacing::Sparse) - .child(Label::new("Sparse spacing")) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "With Slots", - vec![ - single_example( - "End Slot", - ListItem::new("end_slot") - .child(Label::new("Item with end slot")) - .end_slot(Icon::new(IconName::ChevronRight)) - .into_any_element(), - ), - single_example( - "With Toggle", - ListItem::new("with_toggle") - .toggle(Some(true)) - .child(Label::new("Expandable item")) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "States", - vec![ - single_example( - "Disabled", - ListItem::new("disabled") - .disabled(true) - .child(Label::new("Disabled item")) - .into_any_element(), - ), - single_example( - "Non-selectable", - ListItem::new("non_selectable") - .selectable(false) - .child(Label::new("Non-selectable item")) - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/list/list_separator.rs b/crates/ui/src/components/list/list_separator.rs deleted file mode 100644 index 92a7c987c7..0000000000 --- a/crates/ui/src/components/list/list_separator.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::prelude::*; - -#[derive(IntoElement)] -pub struct ListSeparator; - -impl RenderOnce for ListSeparator { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - div() - .h_px() - .w_full() - .my(DynamicSpacing::Base06.rems(cx)) - .bg(cx.theme().colors().border_variant) - } -} diff --git a/crates/ui/src/components/list/list_sub_header.rs b/crates/ui/src/components/list/list_sub_header.rs deleted file mode 100644 index b4a82fb2ed..0000000000 --- a/crates/ui/src/components/list/list_sub_header.rs +++ /dev/null @@ -1,149 +0,0 @@ -use crate::prelude::*; -use component::{Component, ComponentScope, example_group_with_title, single_example}; - -#[derive(IntoElement, RegisterComponent)] -pub struct ListSubHeader { - label: SharedString, - start_slot: Option, - end_slot: Option, - inset: bool, - selected: bool, -} - -impl ListSubHeader { - pub fn new(label: impl Into) -> Self { - Self { - label: label.into(), - start_slot: None, - end_slot: None, - inset: false, - selected: false, - } - } - - pub fn left_icon(mut self, left_icon: Option) -> Self { - self.start_slot = left_icon; - self - } - - pub fn end_slot(mut self, end_slot: AnyElement) -> Self { - self.end_slot = Some(end_slot); - self - } - - pub fn inset(mut self, inset: bool) -> Self { - self.inset = inset; - self - } -} - -impl Toggleable for ListSubHeader { - fn toggle_state(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -impl RenderOnce for ListSubHeader { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - h_flex() - .flex_1() - .w_full() - .relative() - .pb(DynamicSpacing::Base04.rems(cx)) - .px(DynamicSpacing::Base02.rems(cx)) - .child( - div() - .h_5() - .when(self.inset, |this| this.px_2()) - .when(self.selected, |this| { - this.bg(cx.theme().colors().ghost_element_selected) - }) - .flex() - .flex_1() - .w_full() - .gap_1() - .items_center() - .justify_between() - .child( - div() - .flex() - .gap_1() - .items_center() - .children( - self.start_slot.map(|i| { - Icon::new(i).color(Color::Muted).size(IconSize::Small) - }), - ) - .child( - Label::new(self.label.clone()) - .color(Color::Muted) - .size(LabelSize::Small), - ), - ) - .children(self.end_slot), - ) - } -} - -impl Component for ListSubHeader { - fn scope() -> ComponentScope { - ComponentScope::DataDisplay - } - - fn description() -> Option<&'static str> { - Some( - "A sub-header component for organizing list content into subsections with optional icons and end slots.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Sub-headers", - vec![ - single_example( - "Simple", - ListSubHeader::new("Subsection").into_any_element(), - ), - single_example( - "With Icon", - ListSubHeader::new("Documents") - .left_icon(Some(IconName::File)) - .into_any_element(), - ), - single_example( - "With End Slot", - ListSubHeader::new("Recent") - .end_slot( - Label::new("3").color(Color::Muted).into_any_element(), - ) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "States", - vec![ - single_example( - "Selected", - ListSubHeader::new("Selected") - .toggle_state(true) - .into_any_element(), - ), - single_example( - "Inset", - ListSubHeader::new("Inset Sub-header") - .inset(true) - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/modal.rs b/crates/ui/src/components/modal.rs deleted file mode 100644 index 85565f5488..0000000000 --- a/crates/ui/src/components/modal.rs +++ /dev/null @@ -1,462 +0,0 @@ -use crate::{ - Clickable, Color, DynamicSpacing, Headline, HeadlineSize, Icon, IconButton, IconButtonShape, - IconName, Label, LabelCommon, LabelSize, h_flex, v_flex, -}; -use gpui::{prelude::FluentBuilder, *}; -use smallvec::SmallVec; -use theme::ActiveTheme; - -#[derive(IntoElement)] -pub struct Modal { - id: ElementId, - header: ModalHeader, - children: SmallVec<[AnyElement; 2]>, - footer: Option, - container_id: ElementId, - container_scroll_handler: Option, -} - -impl Modal { - pub fn new(id: impl Into, scroll_handle: Option) -> Self { - let id = id.into(); - - let container_id = ElementId::Name(format!("{}_container", id).into()); - Self { - id: ElementId::Name(id), - header: ModalHeader::new(), - children: SmallVec::new(), - footer: None, - container_id, - container_scroll_handler: scroll_handle, - } - } - - pub fn header(mut self, header: ModalHeader) -> Self { - self.header = header; - self - } - - pub fn section(mut self, section: Section) -> Self { - self.children.push(section.into_any_element()); - self - } - - pub fn footer(mut self, footer: ModalFooter) -> Self { - self.footer = Some(footer); - self - } - - pub fn show_dismiss(mut self, show: bool) -> Self { - self.header.show_dismiss_button = show; - self - } - - pub fn show_back(mut self, show: bool) -> Self { - self.header.show_back_button = show; - self - } -} - -impl ParentElement for Modal { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for Modal { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - v_flex() - .id(self.id.clone()) - .size_full() - .flex_1() - .overflow_hidden() - .child(self.header) - .child( - v_flex() - .id(self.container_id.clone()) - .w_full() - .flex_1() - .gap(DynamicSpacing::Base08.rems(cx)) - .when(self.footer.is_some(), |this| this.pb_4()) - .when_some( - self.container_scroll_handler, - |this, container_scroll_handle| { - this.overflow_y_scroll() - .track_scroll(&container_scroll_handle) - }, - ) - .children(self.children), - ) - .children(self.footer) - } -} - -#[derive(IntoElement)] -pub struct ModalHeader { - icon: Option, - headline: Option, - description: Option, - children: SmallVec<[AnyElement; 2]>, - show_dismiss_button: bool, - show_back_button: bool, -} - -impl Default for ModalHeader { - fn default() -> Self { - Self::new() - } -} - -impl ModalHeader { - pub fn new() -> Self { - Self { - icon: None, - headline: None, - description: None, - children: SmallVec::new(), - show_dismiss_button: false, - show_back_button: false, - } - } - - pub fn icon(mut self, icon: Icon) -> Self { - self.icon = Some(icon); - self - } - - /// Set the headline of the modal. - /// - /// This will insert the headline as the first item - /// of `children` if it is not already present. - pub fn headline(mut self, headline: impl Into) -> Self { - self.headline = Some(headline.into()); - self - } - - pub fn description(mut self, description: impl Into) -> Self { - self.description = Some(description.into()); - self - } - - pub fn show_dismiss_button(mut self, show: bool) -> Self { - self.show_dismiss_button = show; - self - } - - pub fn show_back_button(mut self, show: bool) -> Self { - self.show_back_button = show; - self - } -} - -impl ParentElement for ModalHeader { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for ModalHeader { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let mut children = self.children; - - if self.headline.is_some() { - children.insert( - 0, - Headline::new(self.headline.unwrap()) - .size(HeadlineSize::XSmall) - .color(Color::Muted) - .into_any_element(), - ); - } - - h_flex() - .flex_none() - .justify_between() - .w_full() - .px(DynamicSpacing::Base12.rems(cx)) - .pt(DynamicSpacing::Base08.rems(cx)) - .pb(DynamicSpacing::Base04.rems(cx)) - .gap(DynamicSpacing::Base08.rems(cx)) - .when(self.show_back_button, |this| { - this.child( - IconButton::new("back", IconName::ArrowLeft) - .shape(IconButtonShape::Square) - .on_click(|_, window, cx| { - window.dispatch_action(menu::Cancel.boxed_clone(), cx); - }), - ) - }) - .child( - v_flex() - .flex_1() - .child( - h_flex() - .gap_1() - .when_some(self.icon, |this, icon| this.child(icon)) - .children(children), - ) - .when_some(self.description, |this, description| { - this.child(Label::new(description).color(Color::Muted).mb_2()) - }), - ) - .when(self.show_dismiss_button, |this| { - this.child( - IconButton::new("dismiss", IconName::Close) - .shape(IconButtonShape::Square) - .on_click(|_, window, cx| { - window.dispatch_action(menu::Cancel.boxed_clone(), cx); - }), - ) - }) - } -} - -#[derive(IntoElement)] -pub struct ModalRow { - children: SmallVec<[AnyElement; 2]>, -} - -impl Default for ModalRow { - fn default() -> Self { - Self::new() - } -} - -impl ModalRow { - pub fn new() -> Self { - Self { - children: SmallVec::new(), - } - } -} - -impl ParentElement for ModalRow { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for ModalRow { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - h_flex().w_full().py_1().children(self.children) - } -} - -#[derive(IntoElement)] -pub struct ModalFooter { - start_slot: Option, - end_slot: Option, -} - -impl Default for ModalFooter { - fn default() -> Self { - Self::new() - } -} - -impl ModalFooter { - pub fn new() -> Self { - Self { - start_slot: None, - end_slot: None, - } - } - - pub fn start_slot(mut self, start_slot: impl Into>) -> Self { - self.start_slot = start_slot.into().map(IntoElement::into_any_element); - self - } - - pub fn end_slot(mut self, end_slot: impl Into>) -> Self { - self.end_slot = end_slot.into().map(IntoElement::into_any_element); - self - } -} - -impl RenderOnce for ModalFooter { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - h_flex() - .w_full() - .p(DynamicSpacing::Base08.rems(cx)) - .flex_none() - .justify_between() - .gap_1() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child(div().when_some(self.start_slot, |this, start_slot| this.child(start_slot))) - .child(div().when_some(self.end_slot, |this, end_slot| this.child(end_slot))) - } -} - -#[derive(IntoElement)] -pub struct Section { - contained: bool, - padded: bool, - header: Option, - meta: Option, - children: SmallVec<[AnyElement; 2]>, -} - -impl Default for Section { - fn default() -> Self { - Self::new() - } -} - -impl Section { - pub fn new() -> Self { - Self { - contained: false, - padded: true, - header: None, - meta: None, - children: SmallVec::new(), - } - } - - pub fn new_contained() -> Self { - Self { - contained: true, - padded: true, - header: None, - meta: None, - children: SmallVec::new(), - } - } - - pub fn contained(mut self, contained: bool) -> Self { - self.contained = contained; - self - } - - pub fn header(mut self, header: SectionHeader) -> Self { - self.header = Some(header); - self - } - - pub fn meta(mut self, meta: impl Into) -> Self { - self.meta = Some(meta.into()); - self - } - pub fn padded(mut self, padded: bool) -> Self { - self.padded = padded; - self - } -} - -impl ParentElement for Section { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for Section { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let mut section_bg = cx.theme().colors().text; - section_bg.fade_out(0.96); - - let children = if self.contained { - v_flex() - .flex_1() - .when(self.padded, |this| this.px(DynamicSpacing::Base12.rems(cx))) - .child( - v_flex() - .w_full() - .rounded_sm() - .border_1() - .border_color(cx.theme().colors().border) - .bg(section_bg) - .py(DynamicSpacing::Base06.rems(cx)) - .gap_y(DynamicSpacing::Base04.rems(cx)) - .child(div().flex().flex_1().size_full().children(self.children)), - ) - } else { - v_flex() - .w_full() - .flex_1() - .gap_y(DynamicSpacing::Base04.rems(cx)) - .when(self.padded, |this| { - this.px(DynamicSpacing::Base06.rems(cx) + DynamicSpacing::Base06.rems(cx)) - }) - .children(self.children) - }; - - v_flex() - .size_full() - .flex_1() - .child( - v_flex() - .flex_none() - .px(DynamicSpacing::Base12.rems(cx)) - .children(self.header) - .when_some(self.meta, |this, meta| { - this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted)) - }), - ) - .child(children) - // fill any leftover space - .child(div().flex().flex_1()) - } -} - -#[derive(IntoElement)] -pub struct SectionHeader { - /// The label of the header. - label: SharedString, - /// A slot for content that appears after the label, usually on the other side of the header. - /// This might be a button, a disclosure arrow, a face pile, etc. - end_slot: Option, -} - -impl SectionHeader { - pub fn new(label: impl Into) -> Self { - Self { - label: label.into(), - end_slot: None, - } - } - - pub fn end_slot(mut self, end_slot: impl Into>) -> Self { - self.end_slot = end_slot.into().map(IntoElement::into_any_element); - self - } -} - -impl RenderOnce for SectionHeader { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - h_flex() - .id(self.label.clone()) - .w_full() - .px(DynamicSpacing::Base08.rems(cx)) - .child( - div() - .h_7() - .flex() - .items_center() - .justify_between() - .w_full() - .gap(DynamicSpacing::Base04.rems(cx)) - .child( - div().flex_1().child( - Label::new(self.label.clone()) - .size(LabelSize::Small) - .into_element(), - ), - ) - .child(h_flex().children(self.end_slot)), - ) - } -} - -impl From for SectionHeader { - fn from(val: SharedString) -> Self { - SectionHeader::new(val) - } -} - -impl From<&'static str> for SectionHeader { - fn from(val: &'static str) -> Self { - let label: SharedString = val.into(); - SectionHeader::new(label) - } -} diff --git a/crates/ui/src/components/navigable.rs b/crates/ui/src/components/navigable.rs deleted file mode 100644 index a592bcc36f..0000000000 --- a/crates/ui/src/components/navigable.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::prelude::*; -use gpui::{AnyElement, FocusHandle, ScrollAnchor, ScrollHandle}; - -/// An element that can be navigated through via keyboard. Intended for use with scrollable views that want to use -#[derive(IntoElement)] -pub struct Navigable { - child: AnyElement, - selectable_children: Vec, -} - -/// An entry of [Navigable] that can be navigated to. -#[derive(Clone)] -pub struct NavigableEntry { - #[allow(missing_docs)] - pub focus_handle: FocusHandle, - #[allow(missing_docs)] - pub scroll_anchor: Option, -} - -impl NavigableEntry { - /// Creates a new [NavigableEntry] for a given scroll handle. - pub fn new(scroll_handle: &ScrollHandle, cx: &App) -> Self { - Self { - focus_handle: cx.focus_handle(), - scroll_anchor: Some(ScrollAnchor::for_handle(scroll_handle.clone())), - } - } - /// Create a new [NavigableEntry] that cannot be scrolled to. - pub fn focusable(cx: &App) -> Self { - Self { - focus_handle: cx.focus_handle(), - scroll_anchor: None, - } - } -} -impl Navigable { - /// Creates new empty [Navigable] wrapper. - pub fn new(child: AnyElement) -> Self { - Self { - child, - selectable_children: vec![], - } - } - - /// Add a new entry that can be navigated to via keyboard. - /// - /// The order of calls to [Navigable::entry] determines the order of traversal of - /// elements via successive uses of `menu:::SelectNext/SelectPrevious` - pub fn entry(mut self, child: NavigableEntry) -> Self { - self.selectable_children.push(child); - self - } - - fn find_focused( - selectable_children: &[NavigableEntry], - window: &mut Window, - cx: &mut App, - ) -> Option { - selectable_children - .iter() - .position(|entry| entry.focus_handle.contains_focused(window, cx)) - } -} - -impl RenderOnce for Navigable { - fn render(self, _window: &mut Window, _: &mut App) -> impl crate::IntoElement { - div() - .on_action({ - let children = self.selectable_children.clone(); - - move |_: &menu::SelectNext, window, cx| { - let target = Self::find_focused(&children, window, cx) - .and_then(|index| { - index.checked_add(1).filter(|index| *index < children.len()) - }) - .unwrap_or(0); - if let Some(entry) = children.get(target) { - entry.focus_handle.focus(window); - if let Some(anchor) = &entry.scroll_anchor { - anchor.scroll_to(window, cx); - } - } - } - }) - .on_action({ - let children = self.selectable_children; - move |_: &menu::SelectPrevious, window, cx| { - let target = Self::find_focused(&children, window, cx) - .and_then(|index| index.checked_sub(1)) - .or(children.len().checked_sub(1)); - if let Some(entry) = target.and_then(|target| children.get(target)) { - entry.focus_handle.focus(window); - if let Some(anchor) = &entry.scroll_anchor { - anchor.scroll_to(window, cx); - } - } - } - }) - .size_full() - .child(self.child) - } -} diff --git a/crates/ui/src/components/notification.rs b/crates/ui/src/components/notification.rs deleted file mode 100644 index 61109550f7..0000000000 --- a/crates/ui/src/components/notification.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod alert_modal; - -pub use alert_modal::*; diff --git a/crates/ui/src/components/notification/alert_modal.rs b/crates/ui/src/components/notification/alert_modal.rs deleted file mode 100644 index 9990dc1ce5..0000000000 --- a/crates/ui/src/components/notification/alert_modal.rs +++ /dev/null @@ -1,113 +0,0 @@ -use crate::component_prelude::*; -use crate::prelude::*; -use gpui::IntoElement; -use smallvec::{SmallVec, smallvec}; - -#[derive(IntoElement, RegisterComponent)] -pub struct AlertModal { - id: ElementId, - children: SmallVec<[AnyElement; 2]>, - title: SharedString, - primary_action: SharedString, - dismiss_label: SharedString, -} - -impl AlertModal { - pub fn new(id: impl Into, title: impl Into) -> Self { - Self { - id: id.into(), - children: smallvec![], - title: title.into(), - primary_action: "Ok".into(), - dismiss_label: "Cancel".into(), - } - } - - pub fn primary_action(mut self, primary_action: impl Into) -> Self { - self.primary_action = primary_action.into(); - self - } - - pub fn dismiss_label(mut self, dismiss_label: impl Into) -> Self { - self.dismiss_label = dismiss_label.into(); - self - } -} - -impl RenderOnce for AlertModal { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - v_flex() - .id(self.id) - .elevation_3(cx) - .w(px(440.)) - .p_5() - .child( - v_flex() - .text_ui(cx) - .text_color(Color::Muted.color(cx)) - .gap_1() - .child(Headline::new(self.title).size(HeadlineSize::Small)) - .children(self.children), - ) - .child( - h_flex() - .h(rems(1.75)) - .items_center() - .child(div().flex_1()) - .child( - h_flex() - .items_center() - .gap_1() - .child( - Button::new(self.dismiss_label.clone(), self.dismiss_label.clone()) - .color(Color::Muted), - ) - .child(Button::new( - self.primary_action.clone(), - self.primary_action, - )), - ), - ) - } -} - -impl ParentElement for AlertModal { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl Component for AlertModal { - fn scope() -> ComponentScope { - ComponentScope::Notification - } - - fn status() -> ComponentStatus { - ComponentStatus::WorkInProgress - } - - fn description() -> Option<&'static str> { - Some("A modal dialog that presents an alert message with primary and dismiss actions.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .p_4() - .children(vec![example_group( - vec![ - single_example( - "Basic Alert", - AlertModal::new("simple-modal", "Do you want to leave the current call?") - .child("The current window will be closed, and connections to any shared projects will be terminated." - ) - .primary_action("Leave Call") - .into_any_element(), - ) - ], - )]) - .into_any_element() - ) - } -} diff --git a/crates/ui/src/components/popover.rs b/crates/ui/src/components/popover.rs deleted file mode 100644 index 7143514c52..0000000000 --- a/crates/ui/src/components/popover.rs +++ /dev/null @@ -1,94 +0,0 @@ -use crate::prelude::*; -use crate::v_flex; -use gpui::{ - AnyElement, App, Element, IntoElement, ParentElement, Pixels, RenderOnce, Styled, Window, div, -}; -use smallvec::SmallVec; - -/// Y height added beyond the size of the contents. -pub const POPOVER_Y_PADDING: Pixels = px(8.); - -/// A popover is used to display a menu or show some options. -/// -/// Clicking the element that launches the popover should not change the current view, -/// and the popover should be statically positioned relative to that element (not the -/// user's mouse.) -/// -/// Example: A "new" menu with options like "new file", "new folder", etc, -/// Linear's "Display" menu, a profile menu that appears when you click your avatar. -/// -/// Related elements: -/// -/// [`ContextMenu`](crate::ContextMenu): -/// -/// Used to display a popover menu that only contains a list of items. Context menus are always -/// launched by secondary clicking on an element. The menu is positioned relative to the user's cursor. -/// -/// Example: Right clicking a file in the file tree to get a list of actions, right clicking -/// a tab to in the tab bar to get a list of actions. -/// -/// `Dropdown`: -/// -/// Used to display a list of options when the user clicks an element. The menu is -/// positioned relative the element that was clicked, and clicking an item in the -/// dropdown should change the value of the element that was clicked. -/// -/// Example: A theme select control. Displays "One Dark", clicking it opens a list of themes. -/// When one is selected, the theme select control displays the selected theme. -#[derive(IntoElement)] -pub struct Popover { - children: SmallVec<[AnyElement; 2]>, - aside: Option, -} - -impl RenderOnce for Popover { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - div() - .flex() - .gap_1() - .child( - v_flex() - .elevation_2(cx) - .py(POPOVER_Y_PADDING / 2.) - .child(div().children(self.children)), - ) - .when_some(self.aside, |this, aside| { - this.child( - v_flex() - .elevation_2(cx) - .bg(cx.theme().colors().surface_background) - .px_1() - .child(aside), - ) - }) - } -} - -impl Default for Popover { - fn default() -> Self { - Self::new() - } -} - -impl Popover { - pub fn new() -> Self { - Self { - children: SmallVec::new(), - aside: None, - } - } - - pub fn aside(mut self, aside: impl IntoElement) -> Self - where - Self: Sized, - { - self.aside = Some(aside.into_element().into_any()); - self - } -} - -impl ParentElement for Popover { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs deleted file mode 100644 index b1a52bec8f..0000000000 --- a/crates/ui/src/components/popover_menu.rs +++ /dev/null @@ -1,488 +0,0 @@ -use std::{cell::RefCell, rc::Rc}; - -use gpui::{ - AnyElement, AnyView, App, Bounds, Corner, DismissEvent, DispatchPhase, Element, ElementId, - Entity, Focusable as _, GlobalElementId, HitboxBehavior, HitboxId, InteractiveElement, - IntoElement, LayoutId, Length, ManagedView, MouseDownEvent, ParentElement, Pixels, Point, - Style, Window, anchored, deferred, div, point, prelude::FluentBuilder, px, size, -}; - -use crate::prelude::*; - -pub trait PopoverTrigger: IntoElement + Clickable + Toggleable + 'static {} - -impl PopoverTrigger for T {} - -impl Clickable for gpui::AnimationElement -where - T: Clickable + 'static, -{ - fn on_click( - self, - handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.map_element(|e| e.on_click(handler)) - } - - fn cursor_style(self, cursor_style: gpui::CursorStyle) -> Self { - self.map_element(|e| e.cursor_style(cursor_style)) - } -} - -impl Toggleable for gpui::AnimationElement -where - T: Toggleable + 'static, -{ - fn toggle_state(self, selected: bool) -> Self { - self.map_element(|e| e.toggle_state(selected)) - } -} - -pub struct PopoverMenuHandle(Rc>>>); - -impl Clone for PopoverMenuHandle { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - -impl Default for PopoverMenuHandle { - fn default() -> Self { - Self(Rc::default()) - } -} - -struct PopoverMenuHandleState { - menu_builder: Rc Option>>, - menu: Rc>>>, - on_open: Option>, -} - -impl PopoverMenuHandle { - pub fn show(&self, window: &mut Window, cx: &mut App) { - if let Some(state) = self.0.borrow().as_ref() { - show_menu( - &state.menu_builder, - &state.menu, - state.on_open.clone(), - window, - cx, - ); - } - } - - pub fn hide(&self, cx: &mut App) { - if let Some(state) = self.0.borrow().as_ref() - && let Some(menu) = state.menu.borrow().as_ref() - { - menu.update(cx, |_, cx| cx.emit(DismissEvent)); - } - } - - pub fn toggle(&self, window: &mut Window, cx: &mut App) { - if let Some(state) = self.0.borrow().as_ref() { - if state.menu.borrow().is_some() { - self.hide(cx); - } else { - self.show(window, cx); - } - } - } - - pub fn is_deployed(&self) -> bool { - self.0 - .borrow() - .as_ref() - .is_some_and(|state| state.menu.borrow().as_ref().is_some()) - } - - pub fn is_focused(&self, window: &Window, cx: &App) -> bool { - self.0.borrow().as_ref().is_some_and(|state| { - state - .menu - .borrow() - .as_ref() - .is_some_and(|model| model.focus_handle(cx).is_focused(window)) - }) - } - - pub fn refresh_menu( - &self, - window: &mut Window, - cx: &mut App, - new_menu_builder: Rc Option>>, - ) { - let show_menu = if let Some(state) = self.0.borrow_mut().as_mut() { - state.menu_builder = new_menu_builder; - state.menu.borrow().is_some() - } else { - false - }; - - if show_menu { - self.show(window, cx); - } - } -} - -pub struct PopoverMenu { - id: ElementId, - child_builder: Option< - Box< - dyn FnOnce( - Rc>>>, - Option Option> + 'static>>, - ) -> AnyElement - + 'static, - >, - >, - menu_builder: Option Option> + 'static>>, - anchor: Corner, - attach: Option, - offset: Option>, - trigger_handle: Option>, - on_open: Option>, - full_width: bool, -} - -impl PopoverMenu { - /// Returns a new [`PopoverMenu`]. - pub fn new(id: impl Into) -> Self { - Self { - id: id.into(), - child_builder: None, - menu_builder: None, - anchor: Corner::TopLeft, - attach: None, - offset: None, - trigger_handle: None, - on_open: None, - full_width: false, - } - } - - pub fn full_width(mut self, full_width: bool) -> Self { - self.full_width = full_width; - self - } - - pub fn menu( - mut self, - f: impl Fn(&mut Window, &mut App) -> Option> + 'static, - ) -> Self { - self.menu_builder = Some(Rc::new(f)); - self - } - - pub fn with_handle(mut self, handle: PopoverMenuHandle) -> Self { - self.trigger_handle = Some(handle); - self - } - - pub fn trigger(mut self, t: T) -> Self { - let on_open = self.on_open.clone(); - self.child_builder = Some(Box::new(move |menu, builder| { - let open = menu.borrow().is_some(); - t.toggle_state(open) - .when_some(builder, |el, builder| { - el.on_click(move |_event, window, cx| { - show_menu(&builder, &menu, on_open.clone(), window, cx) - }) - }) - .into_any_element() - })); - self - } - - /// This method prevents the trigger button tooltip from being seen when the menu is open. - pub fn trigger_with_tooltip( - mut self, - t: T, - tooltip_builder: impl Fn(&mut Window, &mut App) -> AnyView + 'static, - ) -> Self { - let on_open = self.on_open.clone(); - self.child_builder = Some(Box::new(move |menu, builder| { - let open = menu.borrow().is_some(); - t.toggle_state(open) - .when_some(builder, |el, builder| { - el.on_click(move |_, window, cx| { - show_menu(&builder, &menu, on_open.clone(), window, cx) - }) - .when(!open, |t| { - t.tooltip(move |window, cx| tooltip_builder(window, cx)) - }) - }) - .into_any_element() - })); - self - } - - /// Defines which corner of the menu to anchor to the attachment point. - /// By default, it uses the cursor position. Also see the `attach` method. - pub fn anchor(mut self, anchor: Corner) -> Self { - self.anchor = anchor; - self - } - - /// Defines which corner of the handle to attach the menu's anchor to. - pub fn attach(mut self, attach: Corner) -> Self { - self.attach = Some(attach); - self - } - - /// Offsets the position of the content by that many pixels. - pub fn offset(mut self, offset: Point) -> Self { - self.offset = Some(offset); - self - } - - /// Attaches something upon opening the menu. - pub fn on_open(mut self, on_open: Rc) -> Self { - self.on_open = Some(on_open); - self - } - - fn resolved_attach(&self) -> Corner { - self.attach.unwrap_or(match self.anchor { - Corner::TopLeft => Corner::BottomLeft, - Corner::TopRight => Corner::BottomRight, - Corner::BottomLeft => Corner::TopLeft, - Corner::BottomRight => Corner::TopRight, - }) - } - - fn resolved_offset(&self, window: &mut Window) -> Point { - self.offset.unwrap_or_else(|| { - // Default offset = 4px padding + 1px border - let offset = rems_from_px(5.) * window.rem_size(); - match self.anchor { - Corner::TopRight | Corner::BottomRight => point(offset, px(0.)), - Corner::TopLeft | Corner::BottomLeft => point(-offset, px(0.)), - } - }) - } -} - -fn show_menu( - builder: &Rc Option>>, - menu: &Rc>>>, - on_open: Option>, - window: &mut Window, - cx: &mut App, -) { - let previous_focus_handle = window.focused(cx); - let Some(new_menu) = (builder)(window, cx) else { - return; - }; - let menu2 = menu.clone(); - - window - .subscribe(&new_menu, cx, move |modal, _: &DismissEvent, window, cx| { - if modal.focus_handle(cx).contains_focused(window, cx) - && let Some(previous_focus_handle) = previous_focus_handle.as_ref() - { - window.focus(previous_focus_handle); - } - *menu2.borrow_mut() = None; - window.refresh(); - }) - .detach(); - window.focus(&new_menu.focus_handle(cx)); - *menu.borrow_mut() = Some(new_menu); - window.refresh(); - - if let Some(on_open) = on_open { - on_open(window, cx); - } -} - -pub struct PopoverMenuElementState { - menu: Rc>>>, - child_bounds: Option>, -} - -impl Clone for PopoverMenuElementState { - fn clone(&self) -> Self { - Self { - menu: Rc::clone(&self.menu), - child_bounds: self.child_bounds, - } - } -} - -impl Default for PopoverMenuElementState { - fn default() -> Self { - Self { - menu: Rc::default(), - child_bounds: None, - } - } -} - -pub struct PopoverMenuFrameState { - child_layout_id: Option, - child_element: Option, - menu_element: Option, - menu_handle: Rc>>>, -} - -impl Element for PopoverMenu { - type RequestLayoutState = PopoverMenuFrameState; - type PrepaintState = Option; - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - global_id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (gpui::LayoutId, Self::RequestLayoutState) { - window.with_element_state( - global_id.unwrap(), - |element_state: Option>, window| { - let element_state = element_state.unwrap_or_default(); - let mut menu_layout_id = None; - - let menu_element = element_state.menu.borrow_mut().as_mut().map(|menu| { - let offset = self.resolved_offset(window); - let mut anchored = anchored() - .snap_to_window_with_margin(px(8.)) - .anchor(self.anchor) - .offset(offset); - if let Some(child_bounds) = element_state.child_bounds { - anchored = - anchored.position(child_bounds.corner(self.resolved_attach()) + offset); - } - let mut element = deferred(anchored.child(div().occlude().child(menu.clone()))) - .with_priority(1) - .into_any(); - - menu_layout_id = Some(element.request_layout(window, cx)); - element - }); - - let mut child_element = self.child_builder.take().map(|child_builder| { - (child_builder)(element_state.menu.clone(), self.menu_builder.clone()) - }); - - if let Some(trigger_handle) = self.trigger_handle.take() - && let Some(menu_builder) = self.menu_builder.clone() - { - *trigger_handle.0.borrow_mut() = Some(PopoverMenuHandleState { - menu_builder, - menu: element_state.menu.clone(), - on_open: self.on_open.clone(), - }); - } - - let child_layout_id = child_element - .as_mut() - .map(|child_element| child_element.request_layout(window, cx)); - - let mut style = Style::default(); - if self.full_width { - style.size = size(relative(1.).into(), Length::Auto); - } - - let layout_id = window.request_layout( - style, - menu_layout_id.into_iter().chain(child_layout_id), - cx, - ); - - ( - ( - layout_id, - PopoverMenuFrameState { - child_element, - child_layout_id, - menu_element, - menu_handle: element_state.menu.clone(), - }, - ), - element_state, - ) - }, - ) - } - - fn prepaint( - &mut self, - global_id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - _bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Option { - if let Some(child) = request_layout.child_element.as_mut() { - child.prepaint(window, cx); - } - - if let Some(menu) = request_layout.menu_element.as_mut() { - menu.prepaint(window, cx); - } - - request_layout.child_layout_id.map(|layout_id| { - let bounds = window.layout_bounds(layout_id); - window.with_element_state(global_id.unwrap(), |element_state, _cx| { - let mut element_state: PopoverMenuElementState = element_state.unwrap(); - element_state.child_bounds = Some(bounds); - ((), element_state) - }); - - window.insert_hitbox(bounds, HitboxBehavior::Normal).id - }) - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - _: Bounds, - request_layout: &mut Self::RequestLayoutState, - child_hitbox: &mut Option, - window: &mut Window, - cx: &mut App, - ) { - if let Some(mut child) = request_layout.child_element.take() { - child.paint(window, cx); - } - - if let Some(mut menu) = request_layout.menu_element.take() { - menu.paint(window, cx); - - if let Some(child_hitbox) = *child_hitbox { - let menu_handle = request_layout.menu_handle.clone(); - // Mouse-downing outside the menu dismisses it, so we don't - // want a click on the toggle to re-open it. - window.on_mouse_event(move |_: &MouseDownEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble && child_hitbox.is_hovered(window) { - if let Some(menu) = menu_handle.borrow().as_ref() { - menu.update(cx, |_, cx| { - cx.emit(DismissEvent); - }); - } - cx.stop_propagation(); - } - }) - } - } - } -} - -impl IntoElement for PopoverMenu { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} diff --git a/crates/ui/src/components/progress.rs b/crates/ui/src/components/progress.rs deleted file mode 100644 index bfaf7f3dcf..0000000000 --- a/crates/ui/src/components/progress.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod progress_bar; -pub use progress_bar::*; diff --git a/crates/ui/src/components/progress/progress_bar.rs b/crates/ui/src/components/progress/progress_bar.rs deleted file mode 100644 index 5cc5abd36d..0000000000 --- a/crates/ui/src/components/progress/progress_bar.rs +++ /dev/null @@ -1,156 +0,0 @@ -use documented::Documented; -use gpui::{Hsla, point}; - -use crate::components::Label; -use crate::prelude::*; - -/// A progress bar is a horizontal bar that communicates the status of a process. -/// -/// A progress bar should not be used to represent indeterminate progress. -#[derive(IntoElement, RegisterComponent, Documented)] -pub struct ProgressBar { - id: ElementId, - value: f32, - max_value: f32, - bg_color: Hsla, - over_color: Hsla, - fg_color: Hsla, -} - -impl ProgressBar { - pub fn new(id: impl Into, value: f32, max_value: f32, cx: &App) -> Self { - Self { - id: id.into(), - value, - max_value, - bg_color: cx.theme().colors().background, - over_color: cx.theme().status().error, - fg_color: cx.theme().status().info, - } - } - - /// Sets the current value of the progress bar. - pub fn value(mut self, value: f32) -> Self { - self.value = value; - self - } - - /// Sets the maximum value of the progress bar. - pub fn max_value(mut self, max_value: f32) -> Self { - self.max_value = max_value; - self - } - - /// Sets the background color of the progress bar. - pub fn bg_color(mut self, color: Hsla) -> Self { - self.bg_color = color; - self - } - - /// Sets the foreground color of the progress bar. - pub fn fg_color(mut self, color: Hsla) -> Self { - self.fg_color = color; - self - } - - /// Sets the over limit color of the progress bar. - pub fn over_color(mut self, color: Hsla) -> Self { - self.over_color = color; - self - } -} - -impl RenderOnce for ProgressBar { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let fill_width = (self.value / self.max_value).clamp(0.02, 1.0); - - div() - .id(self.id.clone()) - .w_full() - .h(px(8.0)) - .rounded_full() - .p(px(2.0)) - .bg(self.bg_color) - .shadow(vec![gpui::BoxShadow { - color: gpui::black().opacity(0.08), - offset: point(px(0.), px(1.)), - blur_radius: px(0.), - spread_radius: px(0.), - }]) - .child( - div() - .h_full() - .rounded_full() - .when(self.value > self.max_value, |div| div.bg(self.over_color)) - .when(self.value <= self.max_value, |div| div.bg(self.fg_color)) - .w(relative(fill_width)), - ) - } -} - -impl Component for ProgressBar { - fn scope() -> ComponentScope { - ComponentScope::Status - } - - fn description() -> Option<&'static str> { - Some(Self::DOCS) - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - let max_value = 180.0; - - Some( - div() - .flex() - .flex_col() - .gap_4() - .p_4() - .w(px(240.0)) - .child(div().child("Progress Bar")) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .flex() - .justify_between() - .child(Label::new("0%")) - .child(Label::new("Empty")), - ) - .child(ProgressBar::new("empty", 0.0, max_value, cx)), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .flex() - .justify_between() - .child(Label::new("38%")) - .child(Label::new("Partial")), - ) - .child(ProgressBar::new("partial", max_value * 0.35, max_value, cx)), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .flex() - .justify_between() - .child(Label::new("100%")) - .child(Label::new("Complete")), - ) - .child(ProgressBar::new("filled", max_value, max_value, cx)), - ) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/radio.rs b/crates/ui/src/components/radio.rs deleted file mode 100644 index c7e19f5c34..0000000000 --- a/crates/ui/src/components/radio.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::sync::Arc; - -use crate::prelude::*; - -#[derive(IntoElement)] -pub struct RadioWithLabel { - id: ElementId, - label: Label, - selected: bool, - on_click: Arc, -} - -impl RadioWithLabel { - pub fn new( - id: impl Into, - label: Label, - selected: bool, - on_click: impl Fn(&bool, &mut Window, &mut App) + 'static, - ) -> Self { - Self { - id: id.into(), - label, - selected, - on_click: Arc::new(on_click), - } - } -} - -impl RenderOnce for RadioWithLabel { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let inner_diameter = rems_from_px(6.); - let outer_diameter = rems_from_px(16.); - let border_width = rems_from_px(1.); - h_flex() - .id(self.id) - .gap(DynamicSpacing::Base08.rems(cx)) - .group("") - .child( - div() - .size(outer_diameter) - .rounded(outer_diameter / 2.) - .border_color(cx.theme().colors().border) - .border(border_width) - .group_hover("", |el| el.bg(cx.theme().colors().element_hover)) - .when(self.selected, |el| { - el.child( - div() - .m((outer_diameter - inner_diameter) / 2. - border_width) - .size(inner_diameter) - .rounded(inner_diameter / 2.) - .bg(cx.theme().colors().icon_accent), - ) - }), - ) - .child(self.label) - .on_click(move |_event, window, cx| { - (self.on_click)(&true, window, cx); - }) - } -} diff --git a/crates/ui/src/components/right_click_menu.rs b/crates/ui/src/components/right_click_menu.rs deleted file mode 100644 index dff4230737..0000000000 --- a/crates/ui/src/components/right_click_menu.rs +++ /dev/null @@ -1,287 +0,0 @@ -use std::{cell::RefCell, rc::Rc}; - -use gpui::{ - AnyElement, App, Bounds, Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, - Focusable as _, GlobalElementId, Hitbox, HitboxBehavior, InteractiveElement, IntoElement, - LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Window, - anchored, deferred, div, px, -}; - -pub struct RightClickMenu { - id: ElementId, - child_builder: Option AnyElement + 'static>>, - menu_builder: Option Entity + 'static>>, - anchor: Option, - attach: Option, -} - -impl RightClickMenu { - pub fn menu(mut self, f: impl Fn(&mut Window, &mut App) -> Entity + 'static) -> Self { - self.menu_builder = Some(Rc::new(f)); - self - } - - pub fn trigger(mut self, e: F) -> Self - where - F: FnOnce(bool, &mut Window, &mut App) -> E + 'static, - E: IntoElement + 'static, - { - self.child_builder = Some(Box::new(move |is_menu_active, window, cx| { - e(is_menu_active, window, cx).into_any_element() - })); - self - } - - /// anchor defines which corner of the menu to anchor to the attachment point - /// (by default the cursor position, but see attach) - pub fn anchor(mut self, anchor: Corner) -> Self { - self.anchor = Some(anchor); - self - } - - /// attach defines which corner of the handle to attach the menu's anchor to - pub fn attach(mut self, attach: Corner) -> Self { - self.attach = Some(attach); - self - } - - fn with_element_state( - &mut self, - global_id: &GlobalElementId, - window: &mut Window, - cx: &mut App, - f: impl FnOnce(&mut Self, &mut MenuHandleElementState, &mut Window, &mut App) -> R, - ) -> R { - window.with_optional_element_state::, _>( - Some(global_id), - |element_state, window| { - let mut element_state = element_state.unwrap().unwrap_or_default(); - let result = f(self, &mut element_state, window, cx); - (result, Some(element_state)) - }, - ) - } -} - -/// Creates a [`RightClickMenu`] -pub fn right_click_menu(id: impl Into) -> RightClickMenu { - RightClickMenu { - id: id.into(), - child_builder: None, - menu_builder: None, - anchor: None, - attach: None, - } -} - -pub struct MenuHandleElementState { - menu: Rc>>>, - position: Rc>>, -} - -impl Clone for MenuHandleElementState { - fn clone(&self) -> Self { - Self { - menu: Rc::clone(&self.menu), - position: Rc::clone(&self.position), - } - } -} - -impl Default for MenuHandleElementState { - fn default() -> Self { - Self { - menu: Rc::default(), - position: Rc::default(), - } - } -} - -pub struct RequestLayoutState { - child_layout_id: Option, - child_element: Option, - menu_element: Option, -} - -pub struct PrepaintState { - hitbox: Hitbox, - child_bounds: Option>, -} - -impl Element for RightClickMenu { - type RequestLayoutState = RequestLayoutState; - type PrepaintState = PrepaintState; - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (gpui::LayoutId, Self::RequestLayoutState) { - self.with_element_state( - id.unwrap(), - window, - cx, - |this, element_state, window, cx| { - let mut menu_layout_id = None; - - let menu_element = element_state.menu.borrow_mut().as_mut().map(|menu| { - let mut anchored = anchored().snap_to_window_with_margin(px(8.)); - if let Some(anchor) = this.anchor { - anchored = anchored.anchor(anchor); - } - anchored = anchored.position(*element_state.position.borrow()); - - let mut element = deferred(anchored.child(div().occlude().child(menu.clone()))) - .with_priority(1) - .into_any(); - - menu_layout_id = Some(element.request_layout(window, cx)); - element - }); - - let mut child_element = this.child_builder.take().map(|child_builder| { - (child_builder)(element_state.menu.borrow().is_some(), window, cx) - }); - - let child_layout_id = child_element - .as_mut() - .map(|child_element| child_element.request_layout(window, cx)); - - let layout_id = window.request_layout( - gpui::Style::default(), - menu_layout_id.into_iter().chain(child_layout_id), - cx, - ); - - ( - layout_id, - RequestLayoutState { - child_element, - child_layout_id, - menu_element, - }, - ) - }, - ) - } - - fn prepaint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> PrepaintState { - let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); - - if let Some(child) = request_layout.child_element.as_mut() { - child.prepaint(window, cx); - } - - if let Some(menu) = request_layout.menu_element.as_mut() { - menu.prepaint(window, cx); - } - - PrepaintState { - hitbox, - child_bounds: request_layout - .child_layout_id - .map(|layout_id| window.layout_bounds(layout_id)), - } - } - - fn paint( - &mut self, - id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - _bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - prepaint_state: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - self.with_element_state( - id.unwrap(), - window, - cx, - |this, element_state, window, cx| { - if let Some(mut child) = request_layout.child_element.take() { - child.paint(window, cx); - } - - if let Some(mut menu) = request_layout.menu_element.take() { - menu.paint(window, cx); - } - - let Some(builder) = this.menu_builder.take() else { - return; - }; - - let attach = this.attach; - let menu = element_state.menu.clone(); - let position = element_state.position.clone(); - let child_bounds = prepaint_state.child_bounds; - - let hitbox_id = prepaint_state.hitbox.id; - window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble - && event.button == MouseButton::Right - && hitbox_id.is_hovered(window) - { - cx.stop_propagation(); - window.prevent_default(); - - let new_menu = (builder)(window, cx); - let menu2 = menu.clone(); - let previous_focus_handle = window.focused(cx); - - window - .subscribe(&new_menu, cx, move |modal, _: &DismissEvent, window, cx| { - if modal.focus_handle(cx).contains_focused(window, cx) - && let Some(previous_focus_handle) = - previous_focus_handle.as_ref() - { - window.focus(previous_focus_handle); - } - *menu2.borrow_mut() = None; - window.refresh(); - }) - .detach(); - window.focus(&new_menu.focus_handle(cx)); - *menu.borrow_mut() = Some(new_menu); - *position.borrow_mut() = if let Some(child_bounds) = child_bounds { - if let Some(attach) = attach { - child_bounds.corner(attach) - } else { - window.mouse_position() - } - } else { - window.mouse_position() - }; - window.refresh(); - } - }); - }, - ) - } -} - -impl IntoElement for RightClickMenu { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs deleted file mode 100644 index 391d480fb3..0000000000 --- a/crates/ui/src/components/scrollbar.rs +++ /dev/null @@ -1,1466 +0,0 @@ -use std::{ - any::Any, - fmt::Debug, - ops::Not, - time::{Duration, Instant}, -}; - -use gpui::{ - Along, App, AppContext as _, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Context, - Corner, Corners, CursorStyle, DispatchPhase, Div, Edges, Element, ElementId, Entity, EntityId, - GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, - LayoutId, ListState, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Negate, - ParentElement, Pixels, Point, Position, Render, ScrollHandle, ScrollWheelEvent, Size, Stateful, - StatefulInteractiveElement, Style, Styled, Task, UniformListDecoration, - UniformListScrollHandle, Window, ease_in_out, prelude::FluentBuilder as _, px, quad, relative, - size, -}; -use settings::SettingsStore; -use smallvec::SmallVec; -use theme::ActiveTheme as _; -use util::ResultExt; - -use std::ops::Range; - -use crate::scrollbars::{ScrollbarAutoHide, ScrollbarVisibility, ShowScrollbar}; - -const SCROLLBAR_HIDE_DELAY_INTERVAL: Duration = Duration::from_secs(1); -const SCROLLBAR_HIDE_DURATION: Duration = Duration::from_millis(400); -const SCROLLBAR_SHOW_DURATION: Duration = Duration::from_millis(50); - -const SCROLLBAR_PADDING: Pixels = px(4.); - -pub mod scrollbars { - use gpui::{App, Global}; - use schemars::JsonSchema; - use serde::{Deserialize, Serialize}; - use settings::Settings; - - /// When to show the scrollbar in the editor. - /// - /// Default: auto - #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] - #[serde(rename_all = "snake_case")] - pub enum ShowScrollbar { - /// Show the scrollbar if there's important information or - /// follow the system's configured behavior. - #[default] - Auto, - /// Match the system's configured behavior. - System, - /// Always show the scrollbar. - Always, - /// Never show the scrollbar. - Never, - } - - impl From for ShowScrollbar { - fn from(value: settings::ShowScrollbar) -> Self { - match value { - settings::ShowScrollbar::Auto => ShowScrollbar::Auto, - settings::ShowScrollbar::System => ShowScrollbar::System, - settings::ShowScrollbar::Always => ShowScrollbar::Always, - settings::ShowScrollbar::Never => ShowScrollbar::Never, - } - } - } - - pub trait GlobalSetting { - fn get_value(cx: &App) -> &Self; - } - - impl GlobalSetting for T { - fn get_value(cx: &App) -> &T { - T::get_global(cx) - } - } - - pub trait ScrollbarVisibility: GlobalSetting + 'static { - fn visibility(&self, cx: &App) -> ShowScrollbar; - } - - #[derive(Default)] - pub struct ScrollbarAutoHide(pub bool); - - impl ScrollbarAutoHide { - pub fn should_hide(&self) -> bool { - self.0 - } - } - - impl Global for ScrollbarAutoHide {} -} - -fn get_scrollbar_state( - mut config: Scrollbars, - caller_location: &'static std::panic::Location, - window: &mut Window, - cx: &mut App, -) -> Entity> -where - T: ScrollableHandle, -{ - let element_id = config.id.take().unwrap_or_else(|| caller_location.into()); - let track_color = config.track_color; - - let state = window.use_keyed_state(element_id, cx, |window, cx| { - let parent_id = cx.entity_id(); - ScrollbarStateWrapper( - cx.new(|cx| ScrollbarState::new_from_config(config, parent_id, window, cx)), - ) - }); - - state.update(cx, |state, cx| { - state - .0 - .update(cx, |state, _cx| state.update_track_color(track_color)) - }); - state -} - -pub trait WithScrollbar: Sized { - type Output; - - fn custom_scrollbars( - self, - config: Scrollbars, - window: &mut Window, - cx: &mut App, - ) -> Self::Output - where - T: ScrollableHandle; - - // TODO: account for these cases properly - // #[track_caller] - // fn horizontal_scrollbar(self, window: &mut Window, cx: &mut App) -> Self::Output { - // self.custom_scrollbars( - // Scrollbars::new(ScrollAxes::Horizontal).ensure_id(core::panic::Location::caller()), - // window, - // cx, - // ) - // } - - // #[track_caller] - // fn vertical_scrollbar(self, window: &mut Window, cx: &mut App) -> Self::Output { - // self.custom_scrollbars( - // Scrollbars::new(ScrollAxes::Vertical).ensure_id(core::panic::Location::caller()), - // window, - // cx, - // ) - // } - - #[track_caller] - fn vertical_scrollbar_for( - self, - scroll_handle: &ScrollHandle, - window: &mut Window, - cx: &mut App, - ) -> Self::Output { - self.custom_scrollbars( - Scrollbars::new(ScrollAxes::Vertical) - .tracked_scroll_handle(scroll_handle) - .ensure_id(core::panic::Location::caller()), - window, - cx, - ) - } -} - -impl WithScrollbar for Stateful
{ - type Output = Self; - - #[track_caller] - fn custom_scrollbars( - self, - config: Scrollbars, - window: &mut Window, - cx: &mut App, - ) -> Self::Output - where - T: ScrollableHandle, - { - render_scrollbar( - get_scrollbar_state(config, std::panic::Location::caller(), window, cx), - self, - cx, - ) - } -} - -impl WithScrollbar for Div { - type Output = Stateful
; - - #[track_caller] - fn custom_scrollbars( - self, - config: Scrollbars, - window: &mut Window, - cx: &mut App, - ) -> Self::Output - where - T: ScrollableHandle, - { - let scrollbar = get_scrollbar_state(config, std::panic::Location::caller(), window, cx); - // We know this ID stays consistent as long as the element is rendered for - // consecutive frames, which is sufficient for our use case here - let scrollbar_entity_id = scrollbar.entity_id(); - - render_scrollbar( - scrollbar, - self.id(("track-scroll", scrollbar_entity_id)), - cx, - ) - } -} - -fn render_scrollbar( - scrollbar: Entity>, - div: Stateful
, - cx: &App, -) -> Stateful
-where - T: ScrollableHandle, -{ - let state = &scrollbar.read(cx).0; - - div.when_some(state.read(cx).handle_to_track(), |this, handle| { - this.track_scroll(handle).when_some( - state.read(cx).visible_axes(), - |this, axes| match axes { - ScrollAxes::Horizontal => this.overflow_x_scroll(), - ScrollAxes::Vertical => this.overflow_y_scroll(), - ScrollAxes::Both => this.overflow_scroll(), - }, - ) - }) - .when_some( - state - .read(cx) - .space_to_reserve_for(ScrollbarAxis::Horizontal), - |this, space| this.pb(space), - ) - .when_some( - state.read(cx).space_to_reserve_for(ScrollbarAxis::Vertical), - |this, space| this.pr(space), - ) - .child(state.clone()) -} - -impl UniformListDecoration for ScrollbarStateWrapper { - fn compute( - &self, - _visible_range: Range, - _bounds: Bounds, - scroll_offset: Point, - _item_height: Pixels, - _item_count: usize, - _window: &mut Window, - _cx: &mut App, - ) -> gpui::AnyElement { - ScrollbarElement { - origin: scroll_offset.negate(), - state: self.0.clone(), - } - .into_any() - } -} - -// impl WithScrollbar for UniformList { -// type Output = Self; - -// #[track_caller] -// fn custom_scrollbars( -// self, -// config: Scrollbars, -// window: &mut Window, -// cx: &mut App, -// ) -> Self::Output -// where -// S: ScrollbarVisibilitySetting, -// T: ScrollableHandle, -// { -// let scrollbar = get_scrollbar_state(config, std::panic::Location::caller(), window, cx); -// self.when_some( -// scrollbar.read_with(cx, |wrapper, cx| { -// wrapper -// .0 -// .read(cx) -// .handle_to_track::() -// .cloned() -// }), -// |this, handle| this.track_scroll(handle), -// ) -// .with_decoration(scrollbar) -// } -// } - -#[derive(Copy, Clone, PartialEq, Eq)] -enum ShowBehavior { - Always, - Autohide, - Never, -} - -impl ShowBehavior { - fn from_setting(setting: ShowScrollbar, cx: &mut App) -> Self { - match setting { - ShowScrollbar::Never => Self::Never, - ShowScrollbar::Auto => Self::Autohide, - ShowScrollbar::System => { - if cx.default_global::().should_hide() { - Self::Autohide - } else { - Self::Always - } - } - ShowScrollbar::Always => Self::Always, - } - } -} - -pub enum ScrollAxes { - Horizontal, - Vertical, - Both, -} - -impl ScrollAxes { - fn apply_to(self, point: Point, value: T) -> Point - where - T: Debug + Default + PartialEq + Clone, - { - match self { - Self::Horizontal => point.apply_along(ScrollbarAxis::Horizontal, |_| value), - Self::Vertical => point.apply_along(ScrollbarAxis::Vertical, |_| value), - Self::Both => Point::new(value.clone(), value), - } - } -} - -#[derive(Clone, Debug, Default, PartialEq)] -enum ReservedSpace { - #[default] - None, - Thumb, - Track, -} - -impl ReservedSpace { - fn is_visible(&self) -> bool { - *self != ReservedSpace::None - } - - fn needs_scroll_track(&self) -> bool { - *self == ReservedSpace::Track - } -} - -#[derive(Debug, Default, Clone, Copy)] -enum ScrollbarWidth { - #[default] - Normal, - Small, - XSmall, -} - -impl ScrollbarWidth { - fn to_pixels(&self) -> Pixels { - match self { - ScrollbarWidth::Normal => px(8.), - ScrollbarWidth::Small => px(6.), - ScrollbarWidth::XSmall => px(4.), - } - } -} - -#[derive(Clone)] -enum Handle { - Tracked(T), - Untracked(fn() -> T), -} - -#[derive(Clone)] -pub struct Scrollbars { - id: Option, - get_visibility: fn(&App) -> ShowScrollbar, - tracked_entity: Option>, - scrollable_handle: Handle, - visibility: Point, - track_color: Option, - scrollbar_width: ScrollbarWidth, -} - -impl Scrollbars { - pub fn new(show_along: ScrollAxes) -> Self { - Self::new_with_setting(show_along, |_| ShowScrollbar::default()) - } - - pub fn for_settings() -> Scrollbars { - Scrollbars::new_with_setting(ScrollAxes::Both, |cx| S::get_value(cx).visibility(cx)) - } -} - -impl Scrollbars { - fn new_with_setting(show_along: ScrollAxes, get_visibility: fn(&App) -> ShowScrollbar) -> Self { - Self { - id: None, - get_visibility, - scrollable_handle: Handle::Untracked(ScrollHandle::new), - tracked_entity: None, - visibility: show_along.apply_to(Default::default(), ReservedSpace::Thumb), - track_color: None, - scrollbar_width: ScrollbarWidth::Normal, - } - } -} - -impl Scrollbars { - pub fn id(mut self, id: impl Into) -> Self { - self.id = Some(id.into()); - self - } - - fn ensure_id(mut self, id: impl Into) -> Self { - if self.id.is_none() { - self.id = Some(id.into()); - } - self - } - - /// Notify the current context whenever this scrollbar gets a scroll event - pub fn notify_content(mut self) -> Self { - self.tracked_entity = Some(None); - self - } - - /// Set a parent model which should be notified whenever this scrollbar gets a scroll event. - pub fn tracked_entity(mut self, entity_id: EntityId) -> Self { - self.tracked_entity = Some(Some(entity_id)); - self - } - - pub fn tracked_scroll_handle( - self, - tracked_scroll_handle: &TrackedHandle, - ) -> Scrollbars { - let Self { - id, - tracked_entity: tracked_entity_id, - scrollbar_width, - visibility, - get_visibility, - track_color, - .. - } = self; - - Scrollbars { - scrollable_handle: Handle::Tracked(tracked_scroll_handle.clone()), - id, - tracked_entity: tracked_entity_id, - visibility, - scrollbar_width, - track_color, - get_visibility, - } - } - - pub fn show_along(mut self, along: ScrollAxes) -> Self { - self.visibility = along.apply_to(self.visibility, ReservedSpace::Thumb); - self - } - - pub fn with_track_along(mut self, along: ScrollAxes, background_color: Hsla) -> Self { - self.visibility = along.apply_to(self.visibility, ReservedSpace::Track); - self.track_color = Some(background_color); - self - } - - pub fn width_sm(mut self) -> Self { - self.scrollbar_width = ScrollbarWidth::Small; - self - } - - pub fn width_xs(mut self) -> Self { - self.scrollbar_width = ScrollbarWidth::XSmall; - self - } -} - -#[derive(PartialEq, Clone, Debug)] -enum VisibilityState { - Visible, - Animating { showing: bool, delta: f32 }, - Hidden, - Disabled, -} - -const DELTA_MAX: f32 = 1.0; - -impl VisibilityState { - fn from_behavior(behavior: ShowBehavior) -> Self { - match behavior { - ShowBehavior::Always => Self::Visible, - ShowBehavior::Never => Self::Disabled, - ShowBehavior::Autohide => Self::for_show(), - } - } - - fn for_show() -> Self { - Self::Animating { - showing: true, - delta: Default::default(), - } - } - - fn for_autohide() -> Self { - Self::Animating { - showing: Default::default(), - delta: Default::default(), - } - } - - fn is_visible(&self) -> bool { - matches!(self, Self::Visible | Self::Animating { .. }) - } - - #[inline] - fn is_disabled(&self) -> bool { - *self == VisibilityState::Disabled - } - - fn animation_progress(&self) -> Option<(f32, Duration, bool)> { - match self { - Self::Animating { showing, delta } => Some(( - *delta, - if *showing { - SCROLLBAR_SHOW_DURATION - } else { - SCROLLBAR_HIDE_DURATION - }, - *showing, - )), - _ => None, - } - } - - fn set_delta(&mut self, new_delta: f32) { - match self { - Self::Animating { showing, .. } if new_delta >= DELTA_MAX => { - if *showing { - *self = Self::Visible; - } else { - *self = Self::Hidden; - } - } - Self::Animating { delta, .. } => *delta = new_delta, - _ => {} - } - } - - fn toggle_visible(&self, show_behavior: ShowBehavior) -> Self { - match self { - Self::Hidden => { - if show_behavior == ShowBehavior::Autohide { - Self::for_show() - } else { - Self::Visible - } - } - Self::Animating { - showing: false, - delta: progress, - } => Self::Animating { - showing: true, - delta: DELTA_MAX - progress, - }, - _ => self.clone(), - } - } -} - -enum ParentHoverEvent { - Within, - Entered, - Exited, - Outside, -} - -/// This is used to ensure notifies within the state do not notify the parent -/// unintentionally. -struct ScrollbarStateWrapper(Entity>); - -/// A scrollbar state that should be persisted across frames. -struct ScrollbarState { - thumb_state: ThumbState, - notify_id: Option, - manually_added: bool, - scroll_handle: T, - width: ScrollbarWidth, - show_behavior: ShowBehavior, - get_visibility: fn(&App) -> ShowScrollbar, - visibility: Point, - track_color: Option, - show_state: VisibilityState, - mouse_in_parent: bool, - last_prepaint_state: Option, - _auto_hide_task: Option>, -} - -impl ScrollbarState { - fn new_from_config( - config: Scrollbars, - parent_id: EntityId, - window: &mut Window, - cx: &mut Context, - ) -> Self { - cx.observe_global_in::(window, Self::settings_changed) - .detach(); - - let (manually_added, scroll_handle) = match config.scrollable_handle { - Handle::Tracked(handle) => (true, handle), - Handle::Untracked(func) => (false, func()), - }; - - let show_behavior = ShowBehavior::from_setting((config.get_visibility)(cx), cx); - ScrollbarState { - thumb_state: Default::default(), - notify_id: config.tracked_entity.map(|id| id.unwrap_or(parent_id)), - manually_added, - scroll_handle, - width: config.scrollbar_width, - visibility: config.visibility, - track_color: config.track_color, - show_behavior, - get_visibility: config.get_visibility, - show_state: VisibilityState::from_behavior(show_behavior), - mouse_in_parent: true, - last_prepaint_state: None, - _auto_hide_task: None, - } - } - - fn settings_changed(&mut self, window: &mut Window, cx: &mut Context) { - self.set_show_behavior( - ShowBehavior::from_setting((self.get_visibility)(cx), cx), - window, - cx, - ); - } - - /// Schedules a scrollbar auto hide if no auto hide is currently in progress yet. - fn schedule_auto_hide(&mut self, window: &mut Window, cx: &mut Context) { - if self._auto_hide_task.is_none() { - self._auto_hide_task = (self.visible() && self.show_behavior == ShowBehavior::Autohide) - .then(|| { - cx.spawn_in(window, async move |scrollbar_state, cx| { - cx.background_executor() - .timer(SCROLLBAR_HIDE_DELAY_INTERVAL) - .await; - scrollbar_state - .update(cx, |state, cx| { - if state.thumb_state == ThumbState::Inactive { - state.set_visibility(VisibilityState::for_autohide(), cx); - } - state._auto_hide_task.take(); - }) - .log_err(); - }) - }); - } - } - - fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context) { - let visibility = self.show_state.toggle_visible(self.show_behavior); - self.set_visibility(visibility, cx); - self._auto_hide_task.take(); - self.schedule_auto_hide(window, cx); - } - - fn set_show_behavior( - &mut self, - behavior: ShowBehavior, - window: &mut Window, - cx: &mut Context, - ) { - if self.show_behavior != behavior { - self.show_behavior = behavior; - self.set_visibility(VisibilityState::from_behavior(behavior), cx); - self.schedule_auto_hide(window, cx); - cx.notify(); - } - } - - fn set_visibility(&mut self, visibility: VisibilityState, cx: &mut Context) { - if self.show_state != visibility { - self.show_state = visibility; - cx.notify(); - } - } - - #[inline] - fn visible_axes(&self) -> Option { - match (&self.visibility.x, &self.visibility.y) { - (ReservedSpace::None, ReservedSpace::None) => None, - (ReservedSpace::None, _) => Some(ScrollAxes::Vertical), - (_, ReservedSpace::None) => Some(ScrollAxes::Horizontal), - _ => Some(ScrollAxes::Both), - } - } - - fn space_to_reserve_for(&self, axis: ScrollbarAxis) -> Option { - (self.show_state.is_disabled().not() - && self.visibility.along(axis).needs_scroll_track() - && self - .scroll_handle() - .max_offset() - .along(axis) - .is_zero() - .not()) - .then(|| self.space_to_reserve()) - } - - fn space_to_reserve(&self) -> Pixels { - self.width.to_pixels() + 2 * SCROLLBAR_PADDING - } - - fn handle_to_track(&self) -> Option<&Handle> { - (!self.manually_added) - .then(|| (self.scroll_handle() as &dyn Any).downcast_ref::()) - .flatten() - } - - fn scroll_handle(&self) -> &T { - &self.scroll_handle - } - - fn set_offset(&mut self, offset: Point, cx: &mut Context) { - self.scroll_handle.set_offset(offset); - self.notify_parent(cx); - cx.notify(); - } - - fn is_dragging(&self) -> bool { - self.thumb_state.is_dragging() - } - - fn set_dragging( - &mut self, - axis: ScrollbarAxis, - drag_offset: Pixels, - window: &mut Window, - cx: &mut Context, - ) { - self.set_thumb_state(ThumbState::Dragging(axis, drag_offset), window, cx); - self.scroll_handle().drag_started(); - } - - fn update_hovered_thumb( - &mut self, - position: &Point, - window: &mut Window, - cx: &mut Context, - ) { - self.set_thumb_state( - if let Some(&ScrollbarLayout { axis, .. }) = - self.last_prepaint_state.as_ref().and_then(|state| { - state - .thumb_for_position(position) - .filter(|thumb| thumb.cursor_hitbox.is_hovered(window)) - }) - { - ThumbState::Hover(axis) - } else { - ThumbState::Inactive - }, - window, - cx, - ); - } - - fn set_thumb_state(&mut self, state: ThumbState, window: &mut Window, cx: &mut Context) { - if self.thumb_state != state { - if state == ThumbState::Inactive { - self.schedule_auto_hide(window, cx); - } else { - self.set_visibility(self.show_state.toggle_visible(self.show_behavior), cx); - self._auto_hide_task.take(); - } - self.thumb_state = state; - cx.notify(); - } - } - - fn update_parent_hovered(&mut self, window: &Window) -> ParentHoverEvent { - let last_parent_hovered = self.mouse_in_parent; - self.mouse_in_parent = self.parent_hovered(window); - let state_changed = self.mouse_in_parent != last_parent_hovered; - match (self.mouse_in_parent, state_changed) { - (true, true) => ParentHoverEvent::Entered, - (true, false) => ParentHoverEvent::Within, - (false, true) => ParentHoverEvent::Exited, - (false, false) => ParentHoverEvent::Outside, - } - } - - fn update_track_color(&mut self, track_color: Option) { - self.track_color = track_color; - } - - fn parent_hovered(&self, window: &Window) -> bool { - self.last_prepaint_state - .as_ref() - .is_some_and(|state| state.parent_bounds_hitbox.is_hovered(window)) - } - - fn hit_for_position(&self, position: &Point) -> Option<&ScrollbarLayout> { - self.last_prepaint_state - .as_ref() - .and_then(|state| state.hit_for_position(position)) - } - - fn thumb_for_axis(&self, axis: ScrollbarAxis) -> Option<&ScrollbarLayout> { - self.last_prepaint_state - .as_ref() - .and_then(|state| state.thumbs.iter().find(|thumb| thumb.axis == axis)) - } - - fn thumb_ranges( - &self, - ) -> impl Iterator, ReservedSpace)> + '_ { - const MINIMUM_THUMB_SIZE: Pixels = px(25.); - let max_offset = self.scroll_handle().max_offset(); - let viewport_size = self.scroll_handle().viewport().size; - let current_offset = self.scroll_handle().offset(); - - [ScrollbarAxis::Horizontal, ScrollbarAxis::Vertical] - .into_iter() - .filter(|&axis| self.visibility.along(axis).is_visible()) - .flat_map(move |axis| { - let max_offset = max_offset.along(axis); - let viewport_size = viewport_size.along(axis); - if max_offset.is_zero() || viewport_size.is_zero() { - return None; - } - let content_size = viewport_size + max_offset; - let visible_percentage = viewport_size / content_size; - let thumb_size = MINIMUM_THUMB_SIZE.max(viewport_size * visible_percentage); - if thumb_size > viewport_size { - return None; - } - let current_offset = current_offset - .along(axis) - .clamp(-max_offset, Pixels::ZERO) - .abs(); - let start_offset = (current_offset / max_offset) * (viewport_size - thumb_size); - let thumb_percentage_start = start_offset / viewport_size; - let thumb_percentage_end = (start_offset + thumb_size) / viewport_size; - Some(( - axis, - thumb_percentage_start..thumb_percentage_end, - self.visibility.along(axis), - )) - }) - } - - fn visible(&self) -> bool { - self.show_state.is_visible() - } - - #[inline] - fn disabled(&self) -> bool { - self.show_state.is_disabled() - } - - fn notify_parent(&self, cx: &mut App) { - if let Some(entity_id) = self.notify_id { - cx.notify(entity_id); - } - } -} - -impl Render for ScrollbarState { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - ScrollbarElement { - state: cx.entity(), - origin: Default::default(), - } - } -} - -struct ScrollbarElement { - origin: Point, - state: Entity>, -} - -#[derive(Default, Debug, PartialEq, Eq)] -enum ThumbState { - #[default] - Inactive, - Hover(ScrollbarAxis), - Dragging(ScrollbarAxis, Pixels), -} - -impl ThumbState { - fn is_dragging(&self) -> bool { - matches!(*self, ThumbState::Dragging(..)) - } -} - -impl ScrollableHandle for UniformListScrollHandle { - fn max_offset(&self) -> Size { - self.0.borrow().base_handle.max_offset() - } - - fn set_offset(&self, point: Point) { - self.0.borrow().base_handle.set_offset(point); - } - - fn offset(&self) -> Point { - self.0.borrow().base_handle.offset() - } - - fn viewport(&self) -> Bounds { - self.0.borrow().base_handle.bounds() - } -} - -impl ScrollableHandle for ListState { - fn max_offset(&self) -> Size { - self.max_offset_for_scrollbar() - } - - fn set_offset(&self, point: Point) { - self.set_offset_from_scrollbar(point); - } - - fn offset(&self) -> Point { - self.scroll_px_offset_for_scrollbar() - } - - fn drag_started(&self) { - self.scrollbar_drag_started(); - } - - fn drag_ended(&self) { - self.scrollbar_drag_ended(); - } - - fn viewport(&self) -> Bounds { - self.viewport_bounds() - } -} - -impl ScrollableHandle for ScrollHandle { - fn max_offset(&self) -> Size { - self.max_offset() - } - - fn set_offset(&self, point: Point) { - self.set_offset(point); - } - - fn offset(&self) -> Point { - self.offset() - } - - fn viewport(&self) -> Bounds { - self.bounds() - } -} - -pub trait ScrollableHandle: 'static + Any + Sized + Clone { - fn max_offset(&self) -> Size; - fn set_offset(&self, point: Point); - fn offset(&self) -> Point; - fn viewport(&self) -> Bounds; - fn drag_started(&self) {} - fn drag_ended(&self) {} - - fn scrollable_along(&self, axis: ScrollbarAxis) -> bool { - self.max_offset().along(axis) > Pixels::ZERO - } - fn content_size(&self) -> Size { - self.viewport().size + self.max_offset() - } -} - -enum ScrollbarMouseEvent { - TrackClick, - ThumbDrag(Pixels), -} - -struct ScrollbarLayout { - thumb_bounds: Bounds, - track_bounds: Bounds, - cursor_hitbox: Hitbox, - reserved_space: ReservedSpace, - track_background: Option<(Bounds, Hsla)>, - axis: ScrollbarAxis, -} - -impl ScrollbarLayout { - fn compute_click_offset( - &self, - event_position: Point, - max_offset: Size, - event_type: ScrollbarMouseEvent, - ) -> Pixels { - let Self { - track_bounds, - thumb_bounds, - axis, - .. - } = self; - let axis = *axis; - - let viewport_size = track_bounds.size.along(axis); - let thumb_size = thumb_bounds.size.along(axis); - let thumb_offset = match event_type { - ScrollbarMouseEvent::TrackClick => thumb_size / 2., - ScrollbarMouseEvent::ThumbDrag(thumb_offset) => thumb_offset, - }; - - let thumb_start = - (event_position.along(axis) - track_bounds.origin.along(axis) - thumb_offset) - .clamp(px(0.), viewport_size - thumb_size); - - let max_offset = max_offset.along(axis); - let percentage = if viewport_size > thumb_size { - thumb_start / (viewport_size - thumb_size) - } else { - 0. - }; - - -max_offset * percentage - } -} - -impl PartialEq for ScrollbarLayout { - fn eq(&self, other: &Self) -> bool { - self.axis == other.axis && self.thumb_bounds == other.thumb_bounds - } -} - -pub struct ScrollbarPrepaintState { - parent_bounds_hitbox: Hitbox, - thumbs: SmallVec<[ScrollbarLayout; 2]>, -} - -impl ScrollbarPrepaintState { - fn thumb_for_position(&self, position: &Point) -> Option<&ScrollbarLayout> { - self.thumbs - .iter() - .find(|info| info.thumb_bounds.contains(position)) - } - - fn hit_for_position(&self, position: &Point) -> Option<&ScrollbarLayout> { - self.thumbs.iter().find(|info| { - if info.reserved_space.needs_scroll_track() { - info.track_bounds.contains(position) - } else { - info.thumb_bounds.contains(position) - } - }) - } -} - -impl PartialEq for ScrollbarPrepaintState { - fn eq(&self, other: &Self) -> bool { - self.thumbs == other.thumbs - } -} - -impl Element for ScrollbarElement { - type RequestLayoutState = (); - type PrepaintState = Option<(ScrollbarPrepaintState, Option)>; - - fn id(&self) -> Option { - Some(("scrollbar_animation", self.state.entity_id()).into()) - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - let scrollbar_style = Style { - position: Position::Absolute, - inset: Edges::default(), - size: size(relative(1.), relative(1.)).map(Into::into), - ..Default::default() - }; - - (window.request_layout(scrollbar_style, None, cx), ()) - } - - fn prepaint( - &mut self, - id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - let prepaint_state = self - .state - .read(cx) - .disabled() - .not() - .then(|| ScrollbarPrepaintState { - thumbs: { - let state = self.state.read(cx); - let thumb_ranges = state.thumb_ranges().collect::>(); - let width = state.width.to_pixels(); - let track_color = state.track_color; - - let additional_padding = if thumb_ranges.len() == 2 { - width - } else { - Pixels::ZERO - }; - - thumb_ranges - .into_iter() - .map(|(axis, thumb_range, reserved_space)| { - let track_anchor = match axis { - ScrollbarAxis::Horizontal => Corner::BottomLeft, - ScrollbarAxis::Vertical => Corner::TopRight, - }; - let Bounds { origin, size } = Bounds::from_corner_and_size( - track_anchor, - bounds - .corner(track_anchor) - .apply_along(axis.invert(), |corner| { - corner - SCROLLBAR_PADDING - }), - bounds.size.apply_along(axis.invert(), |_| width), - ); - let scroll_track_bounds = Bounds::new(self.origin + origin, size); - - let padded_bounds = scroll_track_bounds.extend(match axis { - ScrollbarAxis::Horizontal => Edges { - right: -SCROLLBAR_PADDING, - left: -SCROLLBAR_PADDING, - ..Default::default() - }, - ScrollbarAxis::Vertical => Edges { - top: -SCROLLBAR_PADDING, - bottom: -SCROLLBAR_PADDING, - ..Default::default() - }, - }); - - let available_space = - padded_bounds.size.along(axis) - additional_padding; - - let thumb_offset = thumb_range.start * available_space; - let thumb_end = thumb_range.end * available_space; - let thumb_bounds = Bounds::new( - padded_bounds - .origin - .apply_along(axis, |origin| origin + thumb_offset), - padded_bounds - .size - .apply_along(axis, |_| thumb_end - thumb_offset), - ); - - let needs_scroll_track = reserved_space.needs_scroll_track(); - - ScrollbarLayout { - thumb_bounds, - track_bounds: padded_bounds, - axis, - cursor_hitbox: window.insert_hitbox( - if needs_scroll_track { - padded_bounds - } else { - thumb_bounds - }, - HitboxBehavior::BlockMouseExceptScroll, - ), - track_background: track_color - .filter(|_| needs_scroll_track) - .map(|color| (padded_bounds.dilate(SCROLLBAR_PADDING), color)), - reserved_space, - } - }) - .collect() - }, - parent_bounds_hitbox: window.insert_hitbox(bounds, HitboxBehavior::Normal), - }); - if prepaint_state - .as_ref() - .is_some_and(|state| Some(state) != self.state.read(cx).last_prepaint_state.as_ref()) - { - self.state - .update(cx, |state, cx| state.show_scrollbars(window, cx)); - } - - prepaint_state.map(|state| { - let autohide_delta = self.state.read(cx).show_state.animation_progress().map( - |(delta, delta_duration, should_invert)| { - window.with_element_state(id.unwrap(), |state, window| { - let state = state.unwrap_or_else(|| Instant::now()); - let current = Instant::now(); - - let new_delta = DELTA_MAX - .min(delta + (current - state).div_duration_f32(delta_duration)); - self.state - .update(cx, |state, _| state.show_state.set_delta(new_delta)); - - window.request_animation_frame(); - let delta = if should_invert { - DELTA_MAX - delta - } else { - delta - }; - (ease_in_out(delta), current) - }) - }, - ); - - (state, autohide_delta) - }) - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - Bounds { origin, size }: Bounds, - _request_layout: &mut Self::RequestLayoutState, - prepaint_state: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - let Some((prepaint_state, autohide_fade)) = prepaint_state.take() else { - return; - }; - - let bounds = Bounds::new(self.origin + origin, size); - window.with_content_mask(Some(ContentMask { bounds }), |window| { - let colors = cx.theme().colors(); - - let capture_phase; - - if self.state.read(cx).visible() { - let thumb_state = &self.state.read(cx).thumb_state; - - if thumb_state.is_dragging() { - capture_phase = DispatchPhase::Capture; - } else { - capture_phase = DispatchPhase::Bubble; - } - - for ScrollbarLayout { - thumb_bounds, - cursor_hitbox, - axis, - reserved_space, - track_background, - .. - } in &prepaint_state.thumbs - { - const MAXIMUM_OPACITY: f32 = 0.7; - let (thumb_base_color, hovered) = match thumb_state { - ThumbState::Dragging(dragged_axis, _) if dragged_axis == axis => { - (colors.scrollbar_thumb_active_background, false) - } - ThumbState::Hover(hovered_axis) if hovered_axis == axis => { - (colors.scrollbar_thumb_hover_background, true) - } - _ => (colors.scrollbar_thumb_background, false), - }; - - let blending_color = if hovered || reserved_space.needs_scroll_track() { - track_background - .map(|(_, background)| background) - .unwrap_or(colors.surface_background) - } else { - let blend_color = colors.surface_background; - blend_color.min(blend_color.alpha(MAXIMUM_OPACITY)) - }; - - let mut thumb_color = blending_color.blend(thumb_base_color); - - if !hovered && let Some(fade) = autohide_fade { - thumb_color.fade_out(fade); - } - - if let Some((track_bounds, color)) = track_background { - let mut color = *color; - if let Some(fade) = autohide_fade { - color.fade_out(fade); - } - - window.paint_quad(quad( - *track_bounds, - Corners::default(), - color, - Edges::default(), - Hsla::transparent_black(), - BorderStyle::default(), - )); - } - - window.paint_quad(quad( - *thumb_bounds, - Corners::all(Pixels::MAX).clamp_radii_for_quad_size(thumb_bounds.size), - thumb_color, - Edges::default(), - Hsla::transparent_black(), - BorderStyle::default(), - )); - - if thumb_state.is_dragging() { - window.set_window_cursor_style(CursorStyle::Arrow); - } else { - window.set_cursor_style(CursorStyle::Arrow, cursor_hitbox); - } - } - } else { - capture_phase = DispatchPhase::Bubble; - } - - self.state.update(cx, |state, _| { - state.last_prepaint_state = Some(prepaint_state) - }); - - window.on_mouse_event({ - let state = self.state.clone(); - - move |event: &MouseDownEvent, phase, window, cx| { - state.update(cx, |state, cx| { - let Some(scrollbar_layout) = (phase == capture_phase - && event.button == MouseButton::Left) - .then(|| state.hit_for_position(&event.position)) - .flatten() - else { - return; - }; - - let ScrollbarLayout { - thumb_bounds, axis, .. - } = scrollbar_layout; - - if thumb_bounds.contains(&event.position) { - let offset = - event.position.along(*axis) - thumb_bounds.origin.along(*axis); - state.set_dragging(*axis, offset, window, cx); - } else { - let scroll_handle = state.scroll_handle(); - let click_offset = scrollbar_layout.compute_click_offset( - event.position, - scroll_handle.max_offset(), - ScrollbarMouseEvent::TrackClick, - ); - state.set_offset( - scroll_handle.offset().apply_along(*axis, |_| click_offset), - cx, - ); - }; - - cx.stop_propagation(); - }); - } - }); - - window.on_mouse_event({ - let state = self.state.clone(); - - move |event: &ScrollWheelEvent, phase, window, cx| { - state.update(cx, |state, cx| { - if phase.capture() && state.parent_hovered(window) { - state.update_hovered_thumb(&event.position, window, cx) - } - }); - } - }); - - window.on_mouse_event({ - let state = self.state.clone(); - - move |event: &MouseMoveEvent, phase, window, cx| { - if phase != capture_phase { - return; - } - - match state.read(cx).thumb_state { - ThumbState::Dragging(axis, drag_state) if event.dragging() => { - if let Some(scrollbar_layout) = state.read(cx).thumb_for_axis(axis) { - let scroll_handle = state.read(cx).scroll_handle(); - let drag_offset = scrollbar_layout.compute_click_offset( - event.position, - scroll_handle.max_offset(), - ScrollbarMouseEvent::ThumbDrag(drag_state), - ); - let new_offset = - scroll_handle.offset().apply_along(axis, |_| drag_offset); - - state.update(cx, |state, cx| state.set_offset(new_offset, cx)); - cx.stop_propagation(); - } - } - _ => state.update(cx, |state, cx| { - match state.update_parent_hovered(window) { - hover @ ParentHoverEvent::Entered - | hover @ ParentHoverEvent::Within - if event.pressed_button.is_none() => - { - if matches!(hover, ParentHoverEvent::Entered) { - state.show_scrollbars(window, cx); - } - state.update_hovered_thumb(&event.position, window, cx); - if state.thumb_state != ThumbState::Inactive { - cx.stop_propagation(); - } - } - ParentHoverEvent::Exited => { - state.set_thumb_state(ThumbState::Inactive, window, cx); - } - _ => {} - } - }), - } - } - }); - - window.on_mouse_event({ - let state = self.state.clone(); - move |event: &MouseUpEvent, phase, window, cx| { - if phase != capture_phase { - return; - } - - state.update(cx, |state, cx| { - if state.is_dragging() { - state.scroll_handle().drag_ended(); - } - - if !state.parent_hovered(window) { - state.schedule_auto_hide(window, cx); - return; - } - - state.update_hovered_thumb(&event.position, window, cx); - }); - } - }); - }) - } -} - -impl IntoElement for ScrollbarElement { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} diff --git a/crates/ui/src/components/settings_container.rs b/crates/ui/src/components/settings_container.rs deleted file mode 100644 index 31cb1b32f8..0000000000 --- a/crates/ui/src/components/settings_container.rs +++ /dev/null @@ -1,89 +0,0 @@ -use gpui::AnyElement; -use smallvec::SmallVec; - -use crate::prelude::*; - -use super::Checkbox; - -#[derive(IntoElement, RegisterComponent)] -pub struct SettingsContainer { - children: SmallVec<[AnyElement; 2]>, -} - -impl Default for SettingsContainer { - fn default() -> Self { - Self::new() - } -} - -impl SettingsContainer { - pub fn new() -> Self { - Self { - children: SmallVec::new(), - } - } -} - -impl ParentElement for SettingsContainer { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for SettingsContainer { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - v_flex().px_2().gap_1().children(self.children) - } -} - -impl Component for SettingsContainer { - fn scope() -> ComponentScope { - ComponentScope::Layout - } - - fn name() -> &'static str { - "SettingsContainer" - } - - fn description() -> Option<&'static str> { - Some("A container for organizing and displaying settings in a structured manner.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Usage", - vec![ - single_example( - "Empty Container", - SettingsContainer::new().into_any_element(), - ), - single_example( - "With Content", - SettingsContainer::new() - .child(Label::new("Setting 1")) - .child(Label::new("Setting 2")) - .child(Label::new("Setting 3")) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "With Different Elements", - vec![single_example( - "Mixed Content", - SettingsContainer::new() - .child(Label::new("Text Setting")) - .child(Checkbox::new("checkbox", ToggleState::Unselected)) - .child(Button::new("button", "Click me")) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/settings_group.rs b/crates/ui/src/components/settings_group.rs deleted file mode 100644 index 1812a1bec4..0000000000 --- a/crates/ui/src/components/settings_group.rs +++ /dev/null @@ -1,110 +0,0 @@ -use gpui::AnyElement; -use smallvec::SmallVec; - -use crate::{ListHeader, prelude::*}; - -use super::Checkbox; - -/// A group of settings. -#[derive(IntoElement, RegisterComponent)] -pub struct SettingsGroup { - header: SharedString, - children: SmallVec<[AnyElement; 2]>, -} - -impl SettingsGroup { - pub fn new(header: impl Into) -> Self { - Self { - header: header.into(), - children: SmallVec::new(), - } - } -} - -impl ParentElement for SettingsGroup { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for SettingsGroup { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - v_flex() - .p_1() - .gap_2() - .child(ListHeader::new(self.header)) - .children(self.children) - } -} - -impl Component for SettingsGroup { - fn scope() -> ComponentScope { - ComponentScope::Layout - } - - fn name() -> &'static str { - "SettingsGroup" - } - - fn description() -> Option<&'static str> { - Some("A group of settings with a header, used to organize related settings.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Usage", - vec![ - single_example( - "Empty Group", - SettingsGroup::new("General Settings").into_any_element(), - ), - single_example( - "With Children", - SettingsGroup::new("Appearance") - .child( - Checkbox::new("dark_mode", ToggleState::Unselected) - .label("Dark Mode"), - ) - .child( - Checkbox::new("high_contrast", ToggleState::Unselected) - .label("High Contrast"), - ) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Multiple Groups", - vec![single_example( - "Two Groups", - v_flex() - .gap_4() - .child( - SettingsGroup::new("General").child( - Checkbox::new("auto_update", ToggleState::Selected) - .label("Auto Update"), - ), - ) - .child( - SettingsGroup::new("Editor") - .child( - Checkbox::new("line_numbers", ToggleState::Selected) - .label("Show Line Numbers"), - ) - .child( - Checkbox::new("word_wrap", ToggleState::Unselected) - .label("Word Wrap"), - ), - ) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/stack.rs b/crates/ui/src/components/stack.rs deleted file mode 100644 index 2118757548..0000000000 --- a/crates/ui/src/components/stack.rs +++ /dev/null @@ -1,15 +0,0 @@ -use gpui::{Div, div}; - -use crate::StyledExt; - -/// Horizontally stacks elements. Sets `flex()`, `flex_row()`, `items_center()` -#[track_caller] -pub fn h_flex() -> Div { - div().h_flex() -} - -/// Vertically stacks elements. Sets `flex()`, `flex_col()` -#[track_caller] -pub fn v_flex() -> Div { - div().v_flex() -} diff --git a/crates/ui/src/components/sticky_items.rs b/crates/ui/src/components/sticky_items.rs deleted file mode 100644 index bf64622b29..0000000000 --- a/crates/ui/src/components/sticky_items.rs +++ /dev/null @@ -1,335 +0,0 @@ -use std::{ops::Range, rc::Rc}; - -use gpui::{ - AnyElement, App, AvailableSpace, Bounds, Context, Element, ElementId, Entity, GlobalElementId, - InspectorElementId, IntoElement, LayoutId, Pixels, Point, Render, Style, UniformListDecoration, - Window, point, px, size, -}; -use smallvec::SmallVec; - -pub trait StickyCandidate { - fn depth(&self) -> usize; -} - -pub struct StickyItems { - compute_fn: Rc, &mut Window, &mut App) -> SmallVec<[T; 8]>>, - render_fn: Rc SmallVec<[AnyElement; 8]>>, - decorations: Vec>, -} - -pub fn sticky_items( - entity: Entity, - compute_fn: impl Fn(&mut V, Range, &mut Window, &mut Context) -> SmallVec<[T; 8]> - + 'static, - render_fn: impl Fn(&mut V, T, &mut Window, &mut Context) -> SmallVec<[AnyElement; 8]> + 'static, -) -> StickyItems -where - V: Render, - T: StickyCandidate + Clone + 'static, -{ - let entity_compute = entity.clone(); - let entity_render = entity; - - let compute_fn = Rc::new( - move |range: Range, window: &mut Window, cx: &mut App| -> SmallVec<[T; 8]> { - entity_compute.update(cx, |view, cx| compute_fn(view, range, window, cx)) - }, - ); - let render_fn = Rc::new( - move |entry: T, window: &mut Window, cx: &mut App| -> SmallVec<[AnyElement; 8]> { - entity_render.update(cx, |view, cx| render_fn(view, entry, window, cx)) - }, - ); - - StickyItems { - compute_fn, - render_fn, - decorations: Vec::new(), - } -} - -impl StickyItems -where - T: StickyCandidate + Clone + 'static, -{ - /// Adds a decoration element to the sticky items. - pub fn with_decoration(mut self, decoration: impl StickyItemsDecoration + 'static) -> Self { - self.decorations.push(Box::new(decoration)); - self - } -} - -struct StickyItemsElement { - drifting_element: Option, - drifting_decoration: Option, - rest_elements: SmallVec<[AnyElement; 8]>, - rest_decorations: SmallVec<[AnyElement; 1]>, -} - -impl IntoElement for StickyItemsElement { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -impl Element for StickyItemsElement { - type RequestLayoutState = (); - type PrepaintState = (); - - fn id(&self) -> Option { - None - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - (window.request_layout(Style::default(), [], cx), ()) - } - - fn prepaint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - _window: &mut Window, - _cx: &mut App, - ) -> Self::PrepaintState { - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - _prepaint: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - if let Some(ref mut drifting_element) = self.drifting_element { - drifting_element.paint(window, cx); - } - if let Some(ref mut drifting_decoration) = self.drifting_decoration { - drifting_decoration.paint(window, cx); - } - for item in self.rest_elements.iter_mut().rev() { - item.paint(window, cx); - } - for item in self.rest_decorations.iter_mut() { - item.paint(window, cx); - } - } -} - -impl UniformListDecoration for StickyItems -where - T: StickyCandidate + Clone + 'static, -{ - fn compute( - &self, - visible_range: Range, - bounds: Bounds, - scroll_offset: Point, - item_height: Pixels, - _item_count: usize, - window: &mut Window, - cx: &mut App, - ) -> AnyElement { - let entries = (self.compute_fn)(visible_range.clone(), window, cx); - - let Some(sticky_anchor) = find_sticky_anchor(&entries, visible_range.start) else { - return StickyItemsElement { - drifting_element: None, - drifting_decoration: None, - rest_elements: SmallVec::new(), - rest_decorations: SmallVec::new(), - } - .into_any_element(); - }; - - let anchor_depth = sticky_anchor.entry.depth(); - let mut elements = (self.render_fn)(sticky_anchor.entry, window, cx); - let items_count = elements.len(); - - let indents: SmallVec<[usize; 8]> = (0..items_count) - .map(|ix| anchor_depth.saturating_sub(items_count.saturating_sub(ix))) - .collect(); - - let mut last_decoration_element = None; - let mut rest_decoration_elements = SmallVec::new(); - - let expanded_width = bounds.size.width + scroll_offset.x.abs(); - - let decor_available_space = size( - AvailableSpace::Definite(expanded_width), - AvailableSpace::Definite(bounds.size.height), - ); - - let drifting_y_offset = if sticky_anchor.drifting { - let scroll_top = -scroll_offset.y; - let anchor_top = item_height * (sticky_anchor.index + 1); - let sticky_area_height = item_height * items_count; - (anchor_top - scroll_top - sticky_area_height).min(Pixels::ZERO) - } else { - Pixels::ZERO - }; - - let (drifting_indent, rest_indents) = if sticky_anchor.drifting && !indents.is_empty() { - let last = indents[indents.len() - 1]; - let rest: SmallVec<[usize; 8]> = indents[..indents.len() - 1].iter().copied().collect(); - (Some(last), rest) - } else { - (None, indents) - }; - - let base_origin = bounds.origin - point(px(0.), scroll_offset.y); - - for decoration in &self.decorations { - if let Some(drifting_indent) = drifting_indent { - let drifting_indent_vec: SmallVec<[usize; 8]> = - [drifting_indent].into_iter().collect(); - - let sticky_origin = base_origin - + point(px(0.), item_height * rest_indents.len() + drifting_y_offset); - let decoration_bounds = Bounds::new(sticky_origin, bounds.size); - - let mut drifting_dec = decoration.as_ref().compute( - &drifting_indent_vec, - decoration_bounds, - scroll_offset, - item_height, - window, - cx, - ); - drifting_dec.layout_as_root(decor_available_space, window, cx); - drifting_dec.prepaint_at(sticky_origin, window, cx); - last_decoration_element = Some(drifting_dec); - } - - if !rest_indents.is_empty() { - let decoration_bounds = Bounds::new(base_origin, bounds.size); - let mut rest_dec = decoration.as_ref().compute( - &rest_indents, - decoration_bounds, - scroll_offset, - item_height, - window, - cx, - ); - rest_dec.layout_as_root(decor_available_space, window, cx); - rest_dec.prepaint_at(bounds.origin, window, cx); - rest_decoration_elements.push(rest_dec); - } - } - - let (mut drifting_element, mut rest_elements) = - if sticky_anchor.drifting && !elements.is_empty() { - let last = elements.pop().unwrap(); - (Some(last), elements) - } else { - (None, elements) - }; - - let element_available_space = size( - AvailableSpace::Definite(expanded_width), - AvailableSpace::Definite(item_height), - ); - - // order of prepaint is important here - // mouse events checks hitboxes in reverse insertion order - if let Some(ref mut drifting_element) = drifting_element { - let sticky_origin = base_origin - + point( - px(0.), - item_height * rest_elements.len() + drifting_y_offset, - ); - - drifting_element.layout_as_root(element_available_space, window, cx); - drifting_element.prepaint_at(sticky_origin, window, cx); - } - - for (ix, element) in rest_elements.iter_mut().enumerate() { - let sticky_origin = base_origin + point(px(0.), item_height * ix); - - element.layout_as_root(element_available_space, window, cx); - element.prepaint_at(sticky_origin, window, cx); - } - - StickyItemsElement { - drifting_element, - drifting_decoration: last_decoration_element, - rest_elements, - rest_decorations: rest_decoration_elements, - } - .into_any_element() - } -} - -struct StickyAnchor { - entry: T, - index: usize, - drifting: bool, -} - -fn find_sticky_anchor( - entries: &SmallVec<[T; 8]>, - visible_range_start: usize, -) -> Option> { - let mut iter = entries.iter().enumerate().peekable(); - while let Some((ix, current_entry)) = iter.next() { - let depth = current_entry.depth(); - - if depth < ix { - return Some(StickyAnchor { - entry: current_entry.clone(), - index: visible_range_start + ix, - drifting: false, - }); - } - - if let Some(&(_next_ix, next_entry)) = iter.peek() { - let next_depth = next_entry.depth(); - let next_item_outdented = next_depth + 1 == depth; - - let depth_same_as_index = depth == ix; - let depth_greater_than_index = depth == ix + 1; - - if next_item_outdented && (depth_same_as_index || depth_greater_than_index) { - return Some(StickyAnchor { - entry: current_entry.clone(), - index: visible_range_start + ix, - drifting: depth_greater_than_index, - }); - } - } - } - - None -} - -/// A decoration for a [`StickyItems`]. This can be used for various things, -/// such as rendering indent guides, or other visual effects. -pub trait StickyItemsDecoration { - /// Compute the decoration element, given the visible range of list items, - /// the bounds of the list, and the height of each item. - fn compute( - &self, - indents: &SmallVec<[usize; 8]>, - bounds: Bounds, - scroll_offset: Point, - item_height: Pixels, - window: &mut Window, - cx: &mut App, - ) -> AnyElement; -} diff --git a/crates/ui/src/components/stories.rs b/crates/ui/src/components/stories.rs deleted file mode 100644 index bcfcfd04c3..0000000000 --- a/crates/ui/src/components/stories.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod context_menu; - -pub use context_menu::*; diff --git a/crates/ui/src/components/stories/context_menu.rs b/crates/ui/src/components/stories/context_menu.rs deleted file mode 100644 index 197964adc8..0000000000 --- a/crates/ui/src/components/stories/context_menu.rs +++ /dev/null @@ -1,81 +0,0 @@ -use gpui::{Corner, Entity, Render, actions}; -use story::Story; - -use crate::prelude::*; -use crate::{ContextMenu, Label, right_click_menu}; - -actions!(stories, [PrintCurrentDate, PrintBestFood]); - -fn build_menu( - window: &mut Window, - cx: &mut App, - header: impl Into, -) -> Entity { - ContextMenu::build(window, cx, |menu, _, _| { - menu.header(header) - .separator() - .action("Print current time", Box::new(PrintCurrentDate)) - .entry( - "Print best food", - Some(Box::new(PrintBestFood)), - |window, cx| window.dispatch_action(Box::new(PrintBestFood), cx), - ) - }) -} - -pub struct ContextMenuStory; - -impl Render for ContextMenuStory { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - Story::container(cx) - .on_action(|_: &PrintCurrentDate, _, _| { - println!("printing unix time!"); - if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() { - println!("Current Unix time is {:?}", unix_time.as_secs()); - } - }) - .on_action(|_: &PrintBestFood, _, _| { - println!("burrito"); - }) - .flex() - .flex_row() - .justify_between() - .child( - div() - .flex() - .flex_col() - .justify_between() - .child( - right_click_menu("test2") - .trigger(|_, _, _| Label::new("TOP LEFT")) - .menu(move |window, cx| build_menu(window, cx, "top left")), - ) - .child( - right_click_menu("test1") - .trigger(|_, _, _| Label::new("BOTTOM LEFT")) - .anchor(Corner::BottomLeft) - .attach(Corner::TopLeft) - .menu(move |window, cx| build_menu(window, cx, "bottom left")), - ), - ) - .child( - div() - .flex() - .flex_col() - .justify_between() - .child( - right_click_menu("test3") - .trigger(|_, _, _| Label::new("TOP RIGHT")) - .anchor(Corner::TopRight) - .menu(move |window, cx| build_menu(window, cx, "top right")), - ) - .child( - right_click_menu("test4") - .trigger(|_, _, _| Label::new("BOTTOM RIGHT")) - .anchor(Corner::BottomRight) - .attach(Corner::TopRight) - .menu(move |window, cx| build_menu(window, cx, "bottom right")), - ), - ) - } -} diff --git a/crates/ui/src/components/tab.rs b/crates/ui/src/components/tab.rs deleted file mode 100644 index e6823f46b7..0000000000 --- a/crates/ui/src/components/tab.rs +++ /dev/null @@ -1,238 +0,0 @@ -use std::cmp::Ordering; - -use gpui::{AnyElement, IntoElement, Stateful}; -use smallvec::SmallVec; - -use crate::prelude::*; - -const START_TAB_SLOT_SIZE: Pixels = px(12.); -const END_TAB_SLOT_SIZE: Pixels = px(14.); - -/// The position of a [`Tab`] within a list of tabs. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum TabPosition { - /// The tab is first in the list. - First, - - /// The tab is in the middle of the list (i.e., it is not the first or last tab). - /// - /// The [`Ordering`] is where this tab is positioned with respect to the selected tab. - Middle(Ordering), - - /// The tab is last in the list. - Last, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum TabCloseSide { - Start, - End, -} - -#[derive(IntoElement, RegisterComponent)] -pub struct Tab { - div: Stateful
, - selected: bool, - position: TabPosition, - close_side: TabCloseSide, - start_slot: Option, - end_slot: Option, - children: SmallVec<[AnyElement; 2]>, -} - -impl Tab { - pub fn new(id: impl Into) -> Self { - let id = id.into(); - Self { - div: div() - .id(id.clone()) - .debug_selector(|| format!("TAB-{}", id)), - selected: false, - position: TabPosition::First, - close_side: TabCloseSide::End, - start_slot: None, - end_slot: None, - children: SmallVec::new(), - } - } - - pub fn position(mut self, position: TabPosition) -> Self { - self.position = position; - self - } - - pub fn close_side(mut self, close_side: TabCloseSide) -> Self { - self.close_side = close_side; - self - } - - pub fn start_slot(mut self, element: impl Into>) -> Self { - self.start_slot = element.into().map(IntoElement::into_any_element); - self - } - - pub fn end_slot(mut self, element: impl Into>) -> Self { - self.end_slot = element.into().map(IntoElement::into_any_element); - self - } - - pub fn content_height(cx: &App) -> Pixels { - DynamicSpacing::Base32.px(cx) - px(1.) - } - - pub fn container_height(cx: &App) -> Pixels { - DynamicSpacing::Base32.px(cx) - } -} - -impl InteractiveElement for Tab { - fn interactivity(&mut self) -> &mut gpui::Interactivity { - self.div.interactivity() - } -} - -impl StatefulInteractiveElement for Tab {} - -impl Toggleable for Tab { - fn toggle_state(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -impl ParentElement for Tab { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for Tab { - #[allow(refining_impl_trait)] - fn render(self, _: &mut Window, cx: &mut App) -> Stateful
{ - let (text_color, tab_bg, _tab_hover_bg, _tab_active_bg) = match self.selected { - false => ( - cx.theme().colors().text_muted, - cx.theme().colors().tab_inactive_background, - cx.theme().colors().ghost_element_hover, - cx.theme().colors().ghost_element_active, - ), - true => ( - cx.theme().colors().text, - cx.theme().colors().tab_active_background, - cx.theme().colors().element_hover, - cx.theme().colors().element_active, - ), - }; - - let (start_slot, end_slot) = { - let start_slot = h_flex() - .size(START_TAB_SLOT_SIZE) - .justify_center() - .children(self.start_slot); - - let end_slot = h_flex() - .size(END_TAB_SLOT_SIZE) - .justify_center() - .children(self.end_slot); - - match self.close_side { - TabCloseSide::End => (start_slot, end_slot), - TabCloseSide::Start => (end_slot, start_slot), - } - }; - - self.div - .h(Tab::container_height(cx)) - .bg(tab_bg) - .border_color(cx.theme().colors().border) - .map(|this| match self.position { - TabPosition::First => { - if self.selected { - this.pl_px().border_r_1().pb_px() - } else { - this.pl_px().pr_px().border_b_1() - } - } - TabPosition::Last => { - if self.selected { - this.border_l_1().border_r_1().pb_px() - } else { - this.pl_px().border_b_1().border_r_1() - } - } - TabPosition::Middle(Ordering::Equal) => this.border_l_1().border_r_1().pb_px(), - TabPosition::Middle(Ordering::Less) => this.border_l_1().pr_px().border_b_1(), - TabPosition::Middle(Ordering::Greater) => this.border_r_1().pl_px().border_b_1(), - }) - .cursor_pointer() - .child( - h_flex() - .group("") - .relative() - .h(Tab::content_height(cx)) - .px(DynamicSpacing::Base04.px(cx)) - .gap(DynamicSpacing::Base04.rems(cx)) - .text_color(text_color) - .child(start_slot) - .children(self.children) - .child(end_slot), - ) - } -} - -impl Component for Tab { - fn scope() -> ComponentScope { - ComponentScope::Navigation - } - - fn description() -> Option<&'static str> { - Some( - "A tab component that can be used in a tabbed interface, supporting different positions and states.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![example_group_with_title( - "Variations", - vec![ - single_example( - "Default", - Tab::new("default").child("Default Tab").into_any_element(), - ), - single_example( - "Selected", - Tab::new("selected") - .toggle_state(true) - .child("Selected Tab") - .into_any_element(), - ), - single_example( - "First", - Tab::new("first") - .position(TabPosition::First) - .child("First Tab") - .into_any_element(), - ), - single_example( - "Middle", - Tab::new("middle") - .position(TabPosition::Middle(Ordering::Equal)) - .child("Middle Tab") - .into_any_element(), - ), - single_example( - "Last", - Tab::new("last") - .position(TabPosition::Last) - .child("Last Tab") - .into_any_element(), - ), - ], - )]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/tab_bar.rs b/crates/ui/src/components/tab_bar.rs deleted file mode 100644 index 5d41466e3c..0000000000 --- a/crates/ui/src/components/tab_bar.rs +++ /dev/null @@ -1,207 +0,0 @@ -use gpui::{AnyElement, ScrollHandle}; -use smallvec::SmallVec; - -use crate::Tab; -use crate::prelude::*; - -#[derive(IntoElement, RegisterComponent)] -pub struct TabBar { - id: ElementId, - start_children: SmallVec<[AnyElement; 2]>, - children: SmallVec<[AnyElement; 2]>, - end_children: SmallVec<[AnyElement; 2]>, - scroll_handle: Option, -} - -impl TabBar { - pub fn new(id: impl Into) -> Self { - Self { - id: id.into(), - start_children: SmallVec::new(), - children: SmallVec::new(), - end_children: SmallVec::new(), - scroll_handle: None, - } - } - - pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self { - self.scroll_handle = Some(scroll_handle.clone()); - self - } - - pub fn start_children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.start_children - } - - pub fn start_child(mut self, start_child: impl IntoElement) -> Self - where - Self: Sized, - { - self.start_children_mut() - .push(start_child.into_element().into_any()); - self - } - - pub fn start_children( - mut self, - start_children: impl IntoIterator, - ) -> Self - where - Self: Sized, - { - self.start_children_mut().extend( - start_children - .into_iter() - .map(|child| child.into_any_element()), - ); - self - } - - pub fn end_children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.end_children - } - - pub fn end_child(mut self, end_child: impl IntoElement) -> Self - where - Self: Sized, - { - self.end_children_mut() - .push(end_child.into_element().into_any()); - self - } - - pub fn end_children(mut self, end_children: impl IntoIterator) -> Self - where - Self: Sized, - { - self.end_children_mut().extend( - end_children - .into_iter() - .map(|child| child.into_any_element()), - ); - self - } -} - -impl ParentElement for TabBar { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for TabBar { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - div() - .id(self.id) - .group("tab_bar") - .flex() - .flex_none() - .w_full() - .h(Tab::container_height(cx)) - .bg(cx.theme().colors().tab_bar_background) - .when(!self.start_children.is_empty(), |this| { - this.child( - h_flex() - .flex_none() - .gap(DynamicSpacing::Base04.rems(cx)) - .px(DynamicSpacing::Base06.rems(cx)) - .border_b_1() - .border_r_1() - .border_color(cx.theme().colors().border) - .children(self.start_children), - ) - }) - .child( - div() - .relative() - .flex_1() - .h_full() - .overflow_x_hidden() - .child( - div() - .absolute() - .top_0() - .left_0() - .size_full() - .border_b_1() - .border_color(cx.theme().colors().border), - ) - .child( - h_flex() - .id("tabs") - .flex_grow() - .overflow_x_scroll() - .when_some(self.scroll_handle, |cx, scroll_handle| { - cx.track_scroll(&scroll_handle) - }) - .children(self.children), - ), - ) - .when(!self.end_children.is_empty(), |this| { - this.child( - h_flex() - .flex_none() - .gap(DynamicSpacing::Base04.rems(cx)) - .px(DynamicSpacing::Base06.rems(cx)) - .border_b_1() - .border_l_1() - .border_color(cx.theme().colors().border) - .children(self.end_children), - ) - }) - } -} - -impl Component for TabBar { - fn scope() -> ComponentScope { - ComponentScope::Navigation - } - - fn name() -> &'static str { - "TabBar" - } - - fn description() -> Option<&'static str> { - Some("A horizontal bar containing tabs for navigation between different views or sections.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Usage", - vec![ - single_example( - "Empty TabBar", - TabBar::new("empty_tab_bar").into_any_element(), - ), - single_example( - "With Tabs", - TabBar::new("tab_bar_with_tabs") - .child(Tab::new("tab1")) - .child(Tab::new("tab2")) - .child(Tab::new("tab3")) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "With Start and End Children", - vec![single_example( - "Full TabBar", - TabBar::new("full_tab_bar") - .start_child(Button::new("start_button", "Start")) - .child(Tab::new("tab1")) - .child(Tab::new("tab2")) - .child(Tab::new("tab3")) - .end_child(Button::new("end_button", "End")) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/thread_item.rs b/crates/ui/src/components/thread_item.rs deleted file mode 100644 index a4f6a8a533..0000000000 --- a/crates/ui/src/components/thread_item.rs +++ /dev/null @@ -1,260 +0,0 @@ -use crate::{ - Chip, DecoratedIcon, DiffStat, IconDecoration, IconDecorationKind, SpinnerLabel, prelude::*, -}; -use gpui::{ClickEvent, SharedString}; - -#[derive(IntoElement, RegisterComponent)] -pub struct ThreadItem { - id: ElementId, - icon: IconName, - title: SharedString, - timestamp: SharedString, - running: bool, - generation_done: bool, - selected: bool, - added: Option, - removed: Option, - worktree: Option, - on_click: Option>, -} - -impl ThreadItem { - pub fn new(id: impl Into, title: impl Into) -> Self { - Self { - id: id.into(), - icon: IconName::ZedAgent, - title: title.into(), - timestamp: "".into(), - running: false, - generation_done: false, - selected: false, - added: None, - removed: None, - worktree: None, - on_click: None, - } - } - - pub fn timestamp(mut self, timestamp: impl Into) -> Self { - self.timestamp = timestamp.into(); - self - } - - pub fn icon(mut self, icon: IconName) -> Self { - self.icon = icon; - self - } - - pub fn running(mut self, running: bool) -> Self { - self.running = running; - self - } - - pub fn generation_done(mut self, generation_done: bool) -> Self { - self.generation_done = generation_done; - self - } - - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - - pub fn added(mut self, added: usize) -> Self { - self.added = Some(added); - self - } - - pub fn removed(mut self, removed: usize) -> Self { - self.removed = Some(removed); - self - } - - pub fn worktree(mut self, worktree: impl Into) -> Self { - self.worktree = Some(worktree.into()); - self - } - - pub fn on_click( - mut self, - handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_click = Some(Box::new(handler)); - self - } -} - -impl RenderOnce for ThreadItem { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let icon_container = || h_flex().size_4().justify_center(); - let agent_icon = Icon::new(self.icon) - .color(Color::Muted) - .size(IconSize::Small); - - let icon = if self.generation_done { - DecoratedIcon::new( - agent_icon, - Some( - IconDecoration::new( - IconDecorationKind::Dot, - cx.theme().colors().surface_background, - cx, - ) - .color(cx.theme().colors().text_accent) - .position(gpui::Point { - x: px(-2.), - y: px(-2.), - }), - ), - ) - .into_any_element() - } else { - agent_icon.into_any_element() - }; - - let has_no_changes = self.added.is_none() && self.removed.is_none(); - - v_flex() - .id(self.id.clone()) - .cursor_pointer() - .p_2() - .when(self.selected, |this| { - this.bg(cx.theme().colors().element_active) - }) - .hover(|s| s.bg(cx.theme().colors().element_hover)) - .child( - h_flex() - .w_full() - .gap_1p5() - .child(icon) - .child(Label::new(self.title).truncate()) - .when(self.running, |this| { - this.child(icon_container().child(SpinnerLabel::new().color(Color::Accent))) - }), - ) - .child( - h_flex() - .gap_1p5() - .child(icon_container()) // Icon Spacing - .when_some(self.worktree, |this, name| { - this.child(Chip::new(name).label_size(LabelSize::XSmall)) - }) - .child( - Label::new(self.timestamp) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( - Label::new("•") - .size(LabelSize::Small) - .color(Color::Muted) - .alpha(0.5), - ) - .when(has_no_changes, |this| { - this.child( - Label::new("No Changes") - .size(LabelSize::Small) - .color(Color::Muted), - ) - }) - .when(self.added.is_some() || self.removed.is_some(), |this| { - this.child(DiffStat::new( - self.id, - self.added.unwrap_or(0), - self.removed.unwrap_or(0), - )) - }), - ) - .when_some(self.on_click, |this, on_click| this.on_click(on_click)) - } -} - -impl Component for ThreadItem { - fn scope() -> ComponentScope { - ComponentScope::Agent - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - let container = || { - v_flex() - .w_72() - .border_1() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().panel_background) - }; - - let thread_item_examples = vec![ - single_example( - "Default", - container() - .child( - ThreadItem::new("ti-1", "Linking to the Agent Panel Depending on Settings") - .icon(IconName::AiOpenAi) - .timestamp("1:33 AM"), - ) - .into_any_element(), - ), - single_example( - "Generation Done", - container() - .child( - ThreadItem::new("ti-2", "Refine thread view scrolling behavior") - .timestamp("12:12 AM") - .generation_done(true), - ) - .into_any_element(), - ), - single_example( - "Running Agent", - container() - .child( - ThreadItem::new("ti-3", "Add line numbers option to FileEditBlock") - .icon(IconName::AiClaude) - .timestamp("7:30 PM") - .running(true), - ) - .into_any_element(), - ), - single_example( - "In Worktree", - container() - .child( - ThreadItem::new("ti-4", "Add line numbers option to FileEditBlock") - .icon(IconName::AiClaude) - .timestamp("7:37 PM") - .worktree("link-agent-panel"), - ) - .into_any_element(), - ), - single_example( - "With Changes", - container() - .child( - ThreadItem::new("ti-5", "Managing user and project settings interactions") - .icon(IconName::AiClaude) - .timestamp("7:37 PM") - .added(10) - .removed(3), - ) - .into_any_element(), - ), - single_example( - "Selected Item", - container() - .child( - ThreadItem::new("ti-6", "Refine textarea interaction behavior") - .icon(IconName::AiGemini) - .timestamp("3:00 PM") - .selected(true), - ) - .into_any_element(), - ), - ]; - - Some( - example_group(thread_item_examples) - .vertical() - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs deleted file mode 100644 index 86ff1d8eff..0000000000 --- a/crates/ui/src/components/toggle.rs +++ /dev/null @@ -1,1063 +0,0 @@ -use gpui::{ - AnyElement, AnyView, ClickEvent, ElementId, Hsla, IntoElement, KeybindingKeystroke, Keystroke, - Styled, Window, div, hsla, prelude::*, -}; -use settings::KeybindSource; -use std::{rc::Rc, sync::Arc}; - -use crate::utils::is_light; -use crate::{Color, Icon, IconName, ToggleState, Tooltip}; -use crate::{ElevationIndex, KeyBinding, prelude::*}; - -// TODO: Checkbox, CheckboxWithLabel, and Switch could all be -// restructured to use a ToggleLike, similar to Button/Buttonlike, Label/Labellike - -/// Creates a new checkbox. -pub fn checkbox(id: impl Into, toggle_state: ToggleState) -> Checkbox { - Checkbox::new(id, toggle_state) -} - -/// Creates a new switch. -pub fn switch(id: impl Into, toggle_state: ToggleState) -> Switch { - Switch::new(id, toggle_state) -} - -/// The visual style of a toggle. -#[derive(Debug, Default, Clone, PartialEq, Eq)] -pub enum ToggleStyle { - /// Toggle has a transparent background - #[default] - Ghost, - /// Toggle has a filled background based on the - /// elevation index of the parent container - ElevationBased(ElevationIndex), - /// A custom style using a color to tint the toggle - Custom(Hsla), -} - -/// # Checkbox -/// -/// Checkboxes are used for multiple choices, not for mutually exclusive choices. -/// Each checkbox works independently from other checkboxes in the list, -/// therefore checking an additional box does not affect any other selections. -#[derive(IntoElement, RegisterComponent)] -pub struct Checkbox { - id: ElementId, - toggle_state: ToggleState, - style: ToggleStyle, - disabled: bool, - placeholder: bool, - filled: bool, - visualization: bool, - label: Option, - label_size: LabelSize, - label_color: Color, - tooltip: Option AnyView>>, - on_click: Option>, -} - -impl Checkbox { - /// Creates a new [`Checkbox`]. - pub fn new(id: impl Into, checked: ToggleState) -> Self { - Self { - id: id.into(), - toggle_state: checked, - style: ToggleStyle::default(), - disabled: false, - placeholder: false, - filled: false, - visualization: false, - label: None, - label_size: LabelSize::Default, - label_color: Color::Muted, - tooltip: None, - on_click: None, - } - } - - /// Sets the disabled state of the [`Checkbox`]. - pub fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } - - /// Sets the disabled state of the [`Checkbox`]. - pub fn placeholder(mut self, placeholder: bool) -> Self { - self.placeholder = placeholder; - self - } - - /// Binds a handler to the [`Checkbox`] that will be called when clicked. - pub fn on_click( - mut self, - handler: impl Fn(&ToggleState, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_click = Some(Box::new(move |state, _, window, cx| { - handler(state, window, cx) - })); - self - } - - pub fn on_click_ext( - mut self, - handler: impl Fn(&ToggleState, &ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_click = Some(Box::new(handler)); - self - } - - /// Sets the `fill` setting of the checkbox, indicating whether it should be filled. - pub fn fill(mut self) -> Self { - self.filled = true; - self - } - - /// Makes the checkbox look enabled but without pointer cursor and hover styles. - /// Primarily used for uninteractive markdown previews. - pub fn visualization_only(mut self, visualization: bool) -> Self { - self.visualization = visualization; - self - } - - /// Sets the style of the checkbox using the specified [`ToggleStyle`]. - pub fn style(mut self, style: ToggleStyle) -> Self { - self.style = style; - self - } - - /// Match the style of the checkbox to the current elevation using [`ToggleStyle::ElevationBased`]. - pub fn elevation(mut self, elevation: ElevationIndex) -> Self { - self.style = ToggleStyle::ElevationBased(elevation); - self - } - - /// Sets the tooltip for the checkbox. - pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.tooltip = Some(Box::new(tooltip)); - self - } - - /// Set the label for the checkbox. - pub fn label(mut self, label: impl Into) -> Self { - self.label = Some(label.into()); - self - } - - pub fn label_size(mut self, size: LabelSize) -> Self { - self.label_size = size; - self - } - - pub fn label_color(mut self, color: Color) -> Self { - self.label_color = color; - self - } -} - -impl Checkbox { - fn bg_color(&self, cx: &App) -> Hsla { - let style = self.style.clone(); - match (style, self.filled) { - (ToggleStyle::Ghost, false) => cx.theme().colors().ghost_element_background, - (ToggleStyle::Ghost, true) => cx.theme().colors().element_background, - (ToggleStyle::ElevationBased(_), false) => gpui::transparent_black(), - (ToggleStyle::ElevationBased(elevation), true) => elevation.darker_bg(cx), - (ToggleStyle::Custom(_), false) => gpui::transparent_black(), - (ToggleStyle::Custom(color), true) => color.opacity(0.2), - } - } - - fn border_color(&self, cx: &App) -> Hsla { - if self.disabled { - return cx.theme().colors().border_variant; - } - - match self.style.clone() { - ToggleStyle::Ghost => cx.theme().colors().border, - ToggleStyle::ElevationBased(_) => cx.theme().colors().border, - ToggleStyle::Custom(color) => color.opacity(0.3), - } - } - - pub fn container_size() -> Pixels { - px(20.0) - } -} - -impl RenderOnce for Checkbox { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let group_id = format!("checkbox_group_{:?}", self.id); - let color = if self.disabled { - Color::Disabled - } else { - Color::Selected - }; - - let icon = match self.toggle_state { - ToggleState::Selected => { - if self.placeholder { - None - } else { - Some( - Icon::new(IconName::Check) - .size(IconSize::Small) - .color(color), - ) - } - } - ToggleState::Indeterminate => { - Some(Icon::new(IconName::Dash).size(IconSize::Small).color(color)) - } - ToggleState::Unselected => None, - }; - - let bg_color = self.bg_color(cx); - let border_color = self.border_color(cx); - let hover_border_color = border_color.alpha(0.7); - - let size = Self::container_size(); - - let checkbox = h_flex() - .group(group_id.clone()) - .id(self.id.clone()) - .size(size) - .justify_center() - .child( - div() - .flex() - .flex_none() - .justify_center() - .items_center() - .m_1() - .size_4() - .rounded_xs() - .bg(bg_color) - .border_1() - .border_color(border_color) - .when(self.disabled, |this| this.cursor_not_allowed()) - .when(self.disabled, |this| { - this.bg(cx.theme().colors().element_disabled.opacity(0.6)) - }) - .when(!self.disabled && !self.visualization, |this| { - this.group_hover(group_id.clone(), |el| el.border_color(hover_border_color)) - }) - .when(self.placeholder, |this| { - this.child( - div() - .flex_none() - .rounded_full() - .bg(color.color(cx).alpha(0.5)) - .size(px(4.)), - ) - }) - .children(icon), - ); - - h_flex() - .id(self.id) - .map(|this| { - if self.disabled { - this.cursor_not_allowed() - } else if self.visualization { - this.cursor_default() - } else { - this.cursor_pointer() - } - }) - .gap(DynamicSpacing::Base06.rems(cx)) - .child(checkbox) - .when_some(self.label, |this, label| { - this.child( - Label::new(label) - .color(self.label_color) - .size(self.label_size), - ) - }) - .when_some(self.tooltip, |this, tooltip| { - this.tooltip(move |window, cx| tooltip(window, cx)) - }) - .when_some( - self.on_click.filter(|_| !self.disabled), - |this, on_click| { - this.on_click(move |click, window, cx| { - on_click(&self.toggle_state.inverse(), click, window, cx) - }) - }, - ) - } -} - -/// Defines the color for a switch component. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] -pub enum SwitchColor { - #[default] - Accent, - Custom(Hsla), -} - -impl SwitchColor { - fn get_colors(&self, is_on: bool, cx: &App) -> (Hsla, Hsla) { - if !is_on { - return ( - cx.theme().colors().element_disabled, - cx.theme().colors().border, - ); - } - - match self { - SwitchColor::Accent => { - let status = cx.theme().status(); - let colors = cx.theme().colors(); - (status.info.opacity(0.4), colors.text_accent.opacity(0.2)) - } - SwitchColor::Custom(color) => (*color, color.opacity(0.6)), - } - } -} - -impl From for Color { - fn from(color: SwitchColor) -> Self { - match color { - SwitchColor::Accent => Color::Accent, - SwitchColor::Custom(_) => Color::Default, - } - } -} - -/// Defines the color for a switch component. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] -pub enum SwitchLabelPosition { - Start, - #[default] - End, -} - -/// # Switch -/// -/// Switches are used to represent opposite states, such as enabled or disabled. -#[derive(IntoElement, RegisterComponent)] -pub struct Switch { - id: ElementId, - toggle_state: ToggleState, - disabled: bool, - on_click: Option>, - label: Option, - label_position: Option, - label_size: LabelSize, - full_width: bool, - key_binding: Option, - color: SwitchColor, - tab_index: Option, -} - -impl Switch { - /// Creates a new [`Switch`]. - pub fn new(id: impl Into, state: ToggleState) -> Self { - Self { - id: id.into(), - toggle_state: state, - disabled: false, - on_click: None, - label: None, - label_position: None, - label_size: LabelSize::Small, - full_width: false, - key_binding: None, - color: SwitchColor::default(), - tab_index: None, - } - } - - /// Sets the color of the switch using the specified [`SwitchColor`]. - pub fn color(mut self, color: SwitchColor) -> Self { - self.color = color; - self - } - - /// Sets the disabled state of the [`Switch`]. - pub fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } - - /// Binds a handler to the [`Switch`] that will be called when clicked. - pub fn on_click( - mut self, - handler: impl Fn(&ToggleState, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_click = Some(Rc::new(handler)); - self - } - - /// Sets the label of the [`Switch`]. - pub fn label(mut self, label: impl Into) -> Self { - self.label = Some(label.into()); - self - } - - pub fn label_position( - mut self, - label_position: impl Into>, - ) -> Self { - self.label_position = label_position.into(); - self - } - - pub fn label_size(mut self, size: LabelSize) -> Self { - self.label_size = size; - self - } - - pub fn full_width(mut self, full_width: bool) -> Self { - self.full_width = full_width; - self - } - - /// Display the keybinding that triggers the switch action. - pub fn key_binding(mut self, key_binding: impl Into>) -> Self { - self.key_binding = key_binding.into(); - self - } - - pub fn tab_index(mut self, tab_index: impl Into) -> Self { - self.tab_index = Some(tab_index.into()); - self - } -} - -impl RenderOnce for Switch { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let is_on = self.toggle_state == ToggleState::Selected; - let adjust_ratio = if is_light(cx) { 1.5 } else { 1.0 }; - - let base_color = cx.theme().colors().text; - let thumb_color = base_color; - let (bg_color, border_color) = self.color.get_colors(is_on, cx); - - let bg_hover_color = if is_on { - bg_color.blend(base_color.opacity(0.16 * adjust_ratio)) - } else { - bg_color.blend(base_color.opacity(0.05 * adjust_ratio)) - }; - - let thumb_opacity = match (is_on, self.disabled) { - (_, true) => 0.2, - (true, false) => 1.0, - (false, false) => 0.5, - }; - - let group_id = format!("switch_group_{:?}", self.id); - let label = self.label; - - let switch = div() - .id((self.id.clone(), "switch")) - .p(px(1.0)) - .border_2() - .border_color(cx.theme().colors().border_transparent) - .rounded_full() - .when_some( - self.tab_index.filter(|_| !self.disabled), - |this, tab_index| { - this.tab_index(tab_index) - .focus_visible(|mut style| { - style.border_color = Some(cx.theme().colors().border_focused); - style - }) - .when_some(self.on_click.clone(), |this, on_click| { - this.on_click(move |_, window, cx| { - on_click(&self.toggle_state.inverse(), window, cx) - }) - }) - }, - ) - .child( - h_flex() - .w(DynamicSpacing::Base32.rems(cx)) - .h(DynamicSpacing::Base20.rems(cx)) - .group(group_id.clone()) - .child( - h_flex() - .when(is_on, |on| on.justify_end()) - .when(!is_on, |off| off.justify_start()) - .size_full() - .rounded_full() - .px(DynamicSpacing::Base02.px(cx)) - .bg(bg_color) - .when(!self.disabled, |this| { - this.group_hover(group_id.clone(), |el| el.bg(bg_hover_color)) - }) - .border_1() - .border_color(border_color) - .child( - div() - .size(DynamicSpacing::Base12.rems(cx)) - .rounded_full() - .bg(thumb_color) - .opacity(thumb_opacity), - ), - ), - ); - - h_flex() - .id(self.id) - .cursor_pointer() - .gap(DynamicSpacing::Base06.rems(cx)) - .when(self.full_width, |this| this.w_full().justify_between()) - .when( - self.label_position == Some(SwitchLabelPosition::Start), - |this| { - this.when_some(label.clone(), |this, label| { - this.child(Label::new(label).size(self.label_size)) - }) - }, - ) - .child(switch) - .when( - self.label_position == Some(SwitchLabelPosition::End), - |this| { - this.when_some(label, |this, label| { - this.child(Label::new(label).size(self.label_size)) - }) - }, - ) - .children(self.key_binding) - .when_some( - self.on_click.filter(|_| !self.disabled), - |this, on_click| { - this.on_click(move |_, window, cx| { - on_click(&self.toggle_state.inverse(), window, cx) - }) - }, - ) - } -} - -/// # SwitchField -/// -/// A field component that combines a label, description, and switch into one reusable component. -/// -/// # Examples -/// -/// ``` -/// use ui::prelude::*; -/// use ui::{SwitchField, ToggleState}; -/// -/// let switch_field = SwitchField::new( -/// "feature-toggle", -/// Some("Enable feature"), -/// Some("This feature adds new functionality to the app.".into()), -/// ToggleState::Unselected, -/// |state, window, cx| { -/// // Logic here -/// } -/// ); -/// ``` -#[derive(IntoElement, RegisterComponent)] -pub struct SwitchField { - id: ElementId, - label: Option, - description: Option, - toggle_state: ToggleState, - on_click: Arc, - disabled: bool, - color: SwitchColor, - tooltip: Option AnyView>>, - tab_index: Option, -} - -impl SwitchField { - pub fn new( - id: impl Into, - label: Option>, - description: Option, - toggle_state: impl Into, - on_click: impl Fn(&ToggleState, &mut Window, &mut App) + 'static, - ) -> Self { - Self { - id: id.into(), - label: label.map(Into::into), - description, - toggle_state: toggle_state.into(), - on_click: Arc::new(on_click), - disabled: false, - color: SwitchColor::Accent, - tooltip: None, - tab_index: None, - } - } - - pub fn description(mut self, description: impl Into) -> Self { - self.description = Some(description.into()); - self - } - - pub fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } - - /// Sets the color of the switch using the specified [`SwitchColor`]. - /// This changes the color scheme of the switch when it's in the "on" state. - pub fn color(mut self, color: SwitchColor) -> Self { - self.color = color; - self - } - - pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.tooltip = Some(Rc::new(tooltip)); - self - } - - pub fn tab_index(mut self, tab_index: isize) -> Self { - self.tab_index = Some(tab_index); - self - } -} - -impl RenderOnce for SwitchField { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let tooltip = self - .tooltip - .zip(self.label.clone()) - .map(|(tooltip_fn, label)| { - h_flex().gap_0p5().child(Label::new(label)).child( - IconButton::new("tooltip_button", IconName::Info) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .shape(crate::IconButtonShape::Square) - .style(ButtonStyle::Transparent) - .tooltip({ - let tooltip = tooltip_fn.clone(); - move |window, cx| tooltip(window, cx) - }) - .on_click(|_, _, _| {}), // Intentional empty on click handler so that clicking on the info tooltip icon doesn't trigger the switch toggle - ) - }); - - h_flex() - .id((self.id.clone(), "container")) - .when(!self.disabled, |this| { - this.hover(|this| this.cursor_pointer()) - }) - .w_full() - .gap_4() - .justify_between() - .flex_wrap() - .child(match (&self.description, tooltip) { - (Some(description), Some(tooltip)) => v_flex() - .gap_0p5() - .max_w_5_6() - .child(tooltip) - .child(Label::new(description.clone()).color(Color::Muted)) - .into_any_element(), - (Some(description), None) => v_flex() - .gap_0p5() - .max_w_5_6() - .when_some(self.label, |this, label| this.child(Label::new(label))) - .child(Label::new(description.clone()).color(Color::Muted)) - .into_any_element(), - (None, Some(tooltip)) => tooltip.into_any_element(), - (None, None) => { - if let Some(label) = self.label.clone() { - Label::new(label).into_any_element() - } else { - gpui::Empty.into_any_element() - } - } - }) - .child( - Switch::new((self.id.clone(), "switch"), self.toggle_state) - .color(self.color) - .disabled(self.disabled) - .when_some( - self.tab_index.filter(|_| !self.disabled), - |this, tab_index| this.tab_index(tab_index), - ) - .on_click({ - let on_click = self.on_click.clone(); - move |state, window, cx| { - (on_click)(state, window, cx); - } - }), - ) - .when(!self.disabled, |this| { - this.on_click({ - let on_click = self.on_click.clone(); - let toggle_state = self.toggle_state; - move |_click, window, cx| { - (on_click)(&toggle_state.inverse(), window, cx); - } - }) - }) - } -} - -impl Component for SwitchField { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn description() -> Option<&'static str> { - Some("A field component that combines a label, description, and switch") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "States", - vec![ - single_example( - "Unselected", - SwitchField::new( - "switch_field_unselected", - Some("Enable notifications"), - Some("Receive notifications when new messages arrive.".into()), - ToggleState::Unselected, - |_, _, _| {}, - ) - .into_any_element(), - ), - single_example( - "Selected", - SwitchField::new( - "switch_field_selected", - Some("Enable notifications"), - Some("Receive notifications when new messages arrive.".into()), - ToggleState::Selected, - |_, _, _| {}, - ) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Colors", - vec![ - single_example( - "Default", - SwitchField::new( - "switch_field_default", - Some("Default color"), - Some("This uses the default switch color.".into()), - ToggleState::Selected, - |_, _, _| {}, - ) - .into_any_element(), - ), - single_example( - "Accent", - SwitchField::new( - "switch_field_accent", - Some("Accent color"), - Some("This uses the accent color scheme.".into()), - ToggleState::Selected, - |_, _, _| {}, - ) - .color(SwitchColor::Accent) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Disabled", - vec![single_example( - "Disabled", - SwitchField::new( - "switch_field_disabled", - Some("Disabled field"), - Some("This field is disabled and cannot be toggled.".into()), - ToggleState::Selected, - |_, _, _| {}, - ) - .disabled(true) - .into_any_element(), - )], - ), - example_group_with_title( - "No Description", - vec![single_example( - "No Description", - SwitchField::new( - "switch_field_disabled", - Some("Disabled field"), - None, - ToggleState::Selected, - |_, _, _| {}, - ) - .into_any_element(), - )], - ), - example_group_with_title( - "With Tooltip", - vec![ - single_example( - "Tooltip with Description", - SwitchField::new( - "switch_field_tooltip_with_desc", - Some("Nice Feature"), - Some("Enable advanced configuration options.".into()), - ToggleState::Unselected, - |_, _, _| {}, - ) - .tooltip(Tooltip::text("This is content for this tooltip!")) - .into_any_element(), - ), - single_example( - "Tooltip without Description", - SwitchField::new( - "switch_field_tooltip_no_desc", - Some("Nice Feature"), - None, - ToggleState::Selected, - |_, _, _| {}, - ) - .tooltip(Tooltip::text("This is content for this tooltip!")) - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } -} - -impl Component for Checkbox { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn description() -> Option<&'static str> { - Some("A checkbox component that can be used for multiple choice selections") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "States", - vec![ - single_example( - "Unselected", - Checkbox::new("checkbox_unselected", ToggleState::Unselected) - .into_any_element(), - ), - single_example( - "Placeholder", - Checkbox::new("checkbox_indeterminate", ToggleState::Selected) - .placeholder(true) - .into_any_element(), - ), - single_example( - "Indeterminate", - Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate) - .into_any_element(), - ), - single_example( - "Selected", - Checkbox::new("checkbox_selected", ToggleState::Selected) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Styles", - vec![ - single_example( - "Default", - Checkbox::new("checkbox_default", ToggleState::Selected) - .into_any_element(), - ), - single_example( - "Filled", - Checkbox::new("checkbox_filled", ToggleState::Selected) - .fill() - .into_any_element(), - ), - single_example( - "ElevationBased", - Checkbox::new("checkbox_elevation", ToggleState::Selected) - .style(ToggleStyle::ElevationBased( - ElevationIndex::EditorSurface, - )) - .into_any_element(), - ), - single_example( - "Custom Color", - Checkbox::new("checkbox_custom", ToggleState::Selected) - .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Disabled", - vec![ - single_example( - "Unselected", - Checkbox::new( - "checkbox_disabled_unselected", - ToggleState::Unselected, - ) - .disabled(true) - .into_any_element(), - ), - single_example( - "Selected", - Checkbox::new("checkbox_disabled_selected", ToggleState::Selected) - .disabled(true) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "With Label", - vec![single_example( - "Default", - Checkbox::new("checkbox_with_label", ToggleState::Selected) - .label("Always save on quit") - .into_any_element(), - )], - ), - example_group_with_title( - "Extra", - vec![single_example( - "Visualization-Only", - Checkbox::new("viz_only", ToggleState::Selected) - .visualization_only(true) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} - -impl Component for Switch { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn description() -> Option<&'static str> { - Some("A switch component that represents binary states like on/off") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "States", - vec![ - single_example( - "Off", - Switch::new("switch_off", ToggleState::Unselected) - .on_click(|_, _, _cx| {}) - .into_any_element(), - ), - single_example( - "On", - Switch::new("switch_on", ToggleState::Selected) - .on_click(|_, _, _cx| {}) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Colors", - vec![ - single_example( - "Accent (Default)", - Switch::new("switch_accent_style", ToggleState::Selected) - .on_click(|_, _, _cx| {}) - .into_any_element(), - ), - single_example( - "Custom", - Switch::new("switch_custom_style", ToggleState::Selected) - .color(SwitchColor::Custom(hsla(300.0 / 360.0, 0.6, 0.6, 1.0))) - .on_click(|_, _, _cx| {}) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Disabled", - vec![ - single_example( - "Off", - Switch::new("switch_disabled_off", ToggleState::Unselected) - .disabled(true) - .into_any_element(), - ), - single_example( - "On", - Switch::new("switch_disabled_on", ToggleState::Selected) - .disabled(true) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "With Label", - vec![ - single_example( - "Start Label", - Switch::new("switch_with_label_start", ToggleState::Selected) - .label("Always save on quit") - .label_position(SwitchLabelPosition::Start) - .into_any_element(), - ), - single_example( - "End Label", - Switch::new("switch_with_label_end", ToggleState::Selected) - .label("Always save on quit") - .label_position(SwitchLabelPosition::End) - .into_any_element(), - ), - single_example( - "Default Size Label", - Switch::new( - "switch_with_label_default_size", - ToggleState::Selected, - ) - .label("Always save on quit") - .label_size(LabelSize::Default) - .into_any_element(), - ), - single_example( - "Small Size Label", - Switch::new("switch_with_label_small_size", ToggleState::Selected) - .label("Always save on quit") - .label_size(LabelSize::Small) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "With Keybinding", - vec![single_example( - "Keybinding", - Switch::new("switch_with_keybinding", ToggleState::Selected) - .key_binding(Some(KeyBinding::from_keystrokes( - vec![KeybindingKeystroke::from_keystroke( - Keystroke::parse("cmd-s").unwrap(), - )] - .into(), - KeybindSource::Base, - ))) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs deleted file mode 100644 index 8b4ff3f731..0000000000 --- a/crates/ui/src/components/tooltip.rs +++ /dev/null @@ -1,297 +0,0 @@ -use std::borrow::Borrow; -use std::rc::Rc; - -use gpui::{Action, AnyElement, AnyView, AppContext, FocusHandle, IntoElement, Render}; -use settings::Settings; -use theme::ThemeSettings; - -use crate::prelude::*; -use crate::{Color, KeyBinding, Label, LabelSize, StyledExt, h_flex, v_flex}; - -#[derive(RegisterComponent)] -pub struct Tooltip { - title: Title, - meta: Option, - key_binding: Option, -} - -#[derive(Clone, IntoElement)] -enum Title { - Str(SharedString), - Callback(Rc AnyElement>), -} - -impl From for Title { - fn from(value: SharedString) -> Self { - Title::Str(value) - } -} - -impl RenderOnce for Title { - fn render(self, window: &mut Window, cx: &mut App) -> impl gpui::IntoElement { - match self { - Title::Str(title) => title.into_any_element(), - Title::Callback(element) => element(window, cx), - } - } -} - -impl Tooltip { - pub fn simple(title: impl Into, cx: &mut App) -> AnyView { - cx.new(|_| Self { - title: Title::Str(title.into()), - meta: None, - key_binding: None, - }) - .into() - } - - pub fn text(title: impl Into) -> impl Fn(&mut Window, &mut App) -> AnyView { - let title = title.into(); - move |_, cx| { - cx.new(|_| Self { - title: title.clone().into(), - meta: None, - key_binding: None, - }) - .into() - } - } - - pub fn for_action_title>( - title: T, - action: &dyn Action, - ) -> impl Fn(&mut Window, &mut App) -> AnyView + use { - let title = title.into(); - let action = action.boxed_clone(); - move |_, cx| { - cx.new(|cx| Self { - title: Title::Str(title.clone()), - meta: None, - key_binding: Some(KeyBinding::for_action(action.as_ref(), cx)), - }) - .into() - } - } - - pub fn for_action_title_in>( - title: Str, - action: &dyn Action, - focus_handle: &FocusHandle, - ) -> impl Fn(&mut Window, &mut App) -> AnyView + use { - let title = title.into(); - let action = action.boxed_clone(); - let focus_handle = focus_handle.clone(); - move |_, cx| { - cx.new(|cx| Self { - title: Title::Str(title.clone()), - meta: None, - key_binding: Some(KeyBinding::for_action_in( - action.as_ref(), - &focus_handle, - cx, - )), - }) - .into() - } - } - - pub fn for_action( - title: impl Into, - action: &dyn Action, - cx: &mut App, - ) -> AnyView { - cx.new(|cx| Self { - title: Title::Str(title.into()), - meta: None, - key_binding: Some(KeyBinding::for_action(action, cx)), - }) - .into() - } - - pub fn for_action_in( - title: impl Into, - action: &dyn Action, - focus_handle: &FocusHandle, - cx: &mut App, - ) -> AnyView { - cx.new(|cx| Self { - title: title.into().into(), - meta: None, - key_binding: Some(KeyBinding::for_action_in(action, focus_handle, cx)), - }) - .into() - } - - pub fn with_meta( - title: impl Into, - action: Option<&dyn Action>, - meta: impl Into, - cx: &mut App, - ) -> AnyView { - cx.new(|cx| Self { - title: title.into().into(), - meta: Some(meta.into()), - key_binding: action.map(|action| KeyBinding::for_action(action, cx)), - }) - .into() - } - - pub fn with_meta_in( - title: impl Into, - action: Option<&dyn Action>, - meta: impl Into, - focus_handle: &FocusHandle, - cx: &mut App, - ) -> AnyView { - cx.new(|cx| Self { - title: title.into().into(), - meta: Some(meta.into()), - key_binding: action.map(|action| KeyBinding::for_action_in(action, focus_handle, cx)), - }) - .into() - } - - pub fn new(title: impl Into) -> Self { - Self { - title: title.into().into(), - meta: None, - key_binding: None, - } - } - - pub fn new_element(title: impl Fn(&mut Window, &mut App) -> AnyElement + 'static) -> Self { - Self { - title: Title::Callback(Rc::new(title)), - meta: None, - key_binding: None, - } - } - - pub fn element( - title: impl Fn(&mut Window, &mut App) -> AnyElement + 'static, - ) -> impl Fn(&mut Window, &mut App) -> AnyView { - let title = Title::Callback(Rc::new(title)); - move |_, cx| { - let title = title.clone(); - cx.new(|_| Self { - title, - meta: None, - key_binding: None, - }) - .into() - } - } - - pub fn meta(mut self, meta: impl Into) -> Self { - self.meta = Some(meta.into()); - self - } - - pub fn key_binding(mut self, key_binding: impl Into>) -> Self { - self.key_binding = key_binding.into(); - self - } -} - -impl Render for Tooltip { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - tooltip_container(cx, |el, _| { - el.child( - h_flex() - .gap_4() - .child(div().max_w_72().child(self.title.clone())) - .when_some(self.key_binding.clone(), |this, key_binding| { - this.justify_between().child(key_binding) - }), - ) - .when_some(self.meta.clone(), |this, meta| { - this.child( - div() - .max_w_72() - .child(Label::new(meta).size(LabelSize::Small).color(Color::Muted)), - ) - }) - }) - } -} - -pub fn tooltip_container(cx: &mut C, f: impl FnOnce(Div, &mut C) -> Div) -> impl IntoElement -where - C: AppContext + Borrow, -{ - let app = (*cx).borrow(); - let ui_font = ThemeSettings::get_global(app).ui_font.clone(); - - // padding to avoid tooltip appearing right below the mouse cursor - div().pl_2().pt_2p5().child( - v_flex() - .elevation_2(app) - .font(ui_font) - .text_ui(app) - .text_color(app.theme().colors().text) - .py_1() - .px_2() - .map(|el| f(el, cx)), - ) -} - -pub struct LinkPreview { - link: SharedString, -} - -impl LinkPreview { - pub fn new(url: &str, cx: &mut App) -> AnyView { - let mut wrapped_url = String::new(); - for (i, ch) in url.chars().enumerate() { - if i == 500 { - wrapped_url.push('…'); - break; - } - if i % 100 == 0 && i != 0 { - wrapped_url.push('\n'); - } - wrapped_url.push(ch); - } - cx.new(|_| LinkPreview { - link: wrapped_url.into(), - }) - .into() - } -} - -impl Render for LinkPreview { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - tooltip_container(cx, |el, _| { - el.child( - Label::new(self.link.clone()) - .size(LabelSize::XSmall) - .color(Color::Muted), - ) - }) - } -} - -impl Component for Tooltip { - fn scope() -> ComponentScope { - ComponentScope::DataDisplay - } - - fn description() -> Option<&'static str> { - Some( - "A tooltip that appears when hovering over an element, optionally showing a keybinding or additional metadata.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - example_group(vec![single_example( - "Text only", - Button::new("delete-example", "Delete") - .tooltip(Tooltip::text("This is a tooltip!")) - .into_any_element(), - )]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/tree_view_item.rs b/crates/ui/src/components/tree_view_item.rs deleted file mode 100644 index c96800223d..0000000000 --- a/crates/ui/src/components/tree_view_item.rs +++ /dev/null @@ -1,294 +0,0 @@ -use std::sync::Arc; - -use gpui::{AnyElement, AnyView, ClickEvent, MouseButton, MouseDownEvent}; - -use crate::{Disclosure, prelude::*}; - -#[derive(IntoElement, RegisterComponent)] -pub struct TreeViewItem { - id: ElementId, - group_name: Option, - label: SharedString, - expanded: bool, - selected: bool, - disabled: bool, - focused: bool, - default_expanded: bool, - root_item: bool, - tooltip: Option AnyView + 'static>>, - on_click: Option>, - on_hover: Option>, - on_toggle: Option>, - on_secondary_mouse_down: Option>, - tab_index: Option, - focus_handle: Option, -} - -impl TreeViewItem { - pub fn new(id: impl Into, label: impl Into) -> Self { - Self { - id: id.into(), - group_name: None, - label: label.into(), - expanded: false, - selected: false, - disabled: false, - focused: false, - default_expanded: false, - root_item: false, - tooltip: None, - on_click: None, - on_hover: None, - on_toggle: None, - on_secondary_mouse_down: None, - tab_index: None, - focus_handle: None, - } - } - - pub fn group_name(mut self, group_name: impl Into) -> Self { - self.group_name = Some(group_name.into()); - self - } - - pub fn on_click( - mut self, - handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_click = Some(Box::new(handler)); - self - } - - pub fn on_hover(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { - self.on_hover = Some(Box::new(handler)); - self - } - - pub fn on_secondary_mouse_down( - mut self, - handler: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_secondary_mouse_down = Some(Box::new(handler)); - self - } - - pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.tooltip = Some(Box::new(tooltip)); - self - } - - pub fn tab_index(mut self, tab_index: isize) -> Self { - self.tab_index = Some(tab_index); - self - } - - pub fn expanded(mut self, toggle: bool) -> Self { - self.expanded = toggle; - self - } - - pub fn default_expanded(mut self, default_expanded: bool) -> Self { - self.default_expanded = default_expanded; - self - } - - pub fn on_toggle( - mut self, - on_toggle: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_toggle = Some(Arc::new(on_toggle)); - self - } - - pub fn root_item(mut self, root_item: bool) -> Self { - self.root_item = root_item; - self - } - - pub fn focused(mut self, focused: bool) -> Self { - self.focused = focused; - self - } - - pub fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self { - self.focus_handle = Some(focus_handle.clone()); - self - } -} - -impl Disableable for TreeViewItem { - fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } -} - -impl Toggleable for TreeViewItem { - fn toggle_state(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -impl RenderOnce for TreeViewItem { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let selected_bg = cx.theme().colors().element_active.opacity(0.5); - - let transparent_border = cx.theme().colors().border.opacity(0.); - let selected_border = cx.theme().colors().border.opacity(0.4); - let focused_border = cx.theme().colors().border_focused; - - let item_size = rems_from_px(28.); - let indentation_line = h_flex().size(item_size).flex_none().justify_center().child( - div() - .w_px() - .h_full() - .bg(cx.theme().colors().border.opacity(0.5)), - ); - - h_flex() - .id(self.id) - .when_some(self.group_name, |this, group| this.group(group)) - .w_full() - .child( - h_flex() - .id("inner_tree_view_item") - .cursor_pointer() - .size_full() - .h(item_size) - .rounded_sm() - .border_1() - .border_color(transparent_border) - .focus_visible(|s| s.border_color(focused_border)) - .when(self.selected, |this| { - this.border_color(selected_border).bg(selected_bg) - }) - .hover(|s| s.bg(cx.theme().colors().element_hover)) - .map(|this| { - let label = self.label; - - if self.root_item { - this.px_1() - .gap_2p5() - .child( - Disclosure::new("toggle", self.expanded) - .when_some( - self.on_toggle.clone(), - |disclosure, on_toggle| { - disclosure.on_toggle_expanded(on_toggle) - }, - ) - .opened_icon(IconName::ChevronDown) - .closed_icon(IconName::ChevronRight), - ) - .child( - Label::new(label) - .when(!self.selected, |this| this.color(Color::Muted)), - ) - } else { - this.child(indentation_line).child( - h_flex() - .id("nested_inner_tree_view_item") - .w_full() - .flex_grow() - .px_1() - .child( - Label::new(label) - .when(!self.selected, |this| this.color(Color::Muted)), - ), - ) - } - }) - .when_some(self.focus_handle, |this, handle| this.track_focus(&handle)) - .when_some(self.tab_index, |this, index| this.tab_index(index)) - .when_some(self.on_hover, |this, on_hover| this.on_hover(on_hover)) - .when_some( - self.on_click.filter(|_| !self.disabled), - |this, on_click| this.on_click(on_click), - ) - .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| { - this.on_mouse_down(MouseButton::Right, move |event, window, cx| { - (on_mouse_down)(event, window, cx) - }) - }) - .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)), - ) - } -} - -impl Component for TreeViewItem { - fn scope() -> ComponentScope { - ComponentScope::Navigation - } - - fn description() -> Option<&'static str> { - Some( - "A hierarchical list of items that may have a parent-child relationship where children can be toggled into view by expanding or collapsing their parent item.", - ) - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - let container = || { - v_flex() - .p_2() - .w_64() - .border_1() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().panel_background) - }; - - Some( - example_group(vec![ - single_example( - "Basic Tree View", - container() - .child( - TreeViewItem::new("index-1", "Tree Item Root #1") - .root_item(true) - .toggle_state(true), - ) - .child(TreeViewItem::new("index-2", "Tree Item #2")) - .child(TreeViewItem::new("index-3", "Tree Item #3")) - .child(TreeViewItem::new("index-4", "Tree Item Root #2").root_item(true)) - .child(TreeViewItem::new("index-5", "Tree Item #5")) - .child(TreeViewItem::new("index-6", "Tree Item #6")) - .into_any_element(), - ), - single_example( - "Active Child", - container() - .child(TreeViewItem::new("index-1", "Tree Item Root #1").root_item(true)) - .child(TreeViewItem::new("index-2", "Tree Item #2").toggle_state(true)) - .child(TreeViewItem::new("index-3", "Tree Item #3")) - .into_any_element(), - ), - single_example( - "Focused Parent", - container() - .child( - TreeViewItem::new("index-1", "Tree Item Root #1") - .root_item(true) - .focused(true) - .toggle_state(true), - ) - .child(TreeViewItem::new("index-2", "Tree Item #2")) - .child(TreeViewItem::new("index-3", "Tree Item #3")) - .into_any_element(), - ), - single_example( - "Focused Child", - container() - .child( - TreeViewItem::new("index-1", "Tree Item Root #1") - .root_item(true) - .toggle_state(true), - ) - .child(TreeViewItem::new("index-2", "Tree Item #2").focused(true)) - .child(TreeViewItem::new("index-3", "Tree Item #3")) - .into_any_element(), - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs deleted file mode 100644 index 0357e498bb..0000000000 --- a/crates/ui/src/prelude.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! The prelude of this crate. When building UI in Zed you almost always want to import this. - -pub use gpui::prelude::*; -pub use gpui::{ - AbsoluteLength, AnyElement, App, Context, DefiniteLength, Div, Element, ElementId, - InteractiveElement, ParentElement, Pixels, Rems, RenderOnce, SharedString, Styled, Window, div, - px, relative, rems, -}; - -pub use component::{ - Component, ComponentScope, example_group, example_group_with_title, single_example, -}; -pub use ui_macros::RegisterComponent; - -pub use crate::DynamicSpacing; -pub use crate::animation::{AnimationDirection, AnimationDuration, DefaultAnimations}; -pub use crate::styles::{ - PlatformStyle, Severity, StyledTypography, TextSize, rems_from_px, vh, vw, -}; -pub use crate::traits::clickable::*; -pub use crate::traits::disableable::*; -pub use crate::traits::fixed::*; -pub use crate::traits::styled_ext::*; -pub use crate::traits::toggleable::*; -pub use crate::traits::visible_on_hover::*; -pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton}; -pub use crate::{ButtonCommon, Color}; -pub use crate::{Headline, HeadlineSize}; -pub use crate::{Icon, IconName, IconPosition, IconSize}; -pub use crate::{Label, LabelCommon, LabelSize, LineHeightStyle, LoadingLabel}; -pub use crate::{h_container, h_flex, v_container, v_flex}; -pub use crate::{ - h_group, h_group_lg, h_group_sm, h_group_xl, v_group, v_group_lg, v_group_sm, v_group_xl, -}; -pub use theme::ActiveTheme; diff --git a/crates/ui/src/styles.rs b/crates/ui/src/styles.rs deleted file mode 100644 index bc2399f54b..0000000000 --- a/crates/ui/src/styles.rs +++ /dev/null @@ -1,18 +0,0 @@ -pub mod animation; -mod appearance; -mod color; -mod elevation; -mod platform; -mod severity; -mod spacing; -mod typography; -mod units; - -pub use appearance::*; -pub use color::*; -pub use elevation::*; -pub use platform::*; -pub use severity::*; -pub use spacing::*; -pub use typography::*; -pub use units::*; diff --git a/crates/ui/src/styles/animation.rs b/crates/ui/src/styles/animation.rs deleted file mode 100644 index acea834548..0000000000 --- a/crates/ui/src/styles/animation.rs +++ /dev/null @@ -1,290 +0,0 @@ -use crate::{ContentGroup, prelude::*}; -use gpui::{AnimationElement, AnimationExt, Styled}; -use std::time::Duration; - -use gpui::ease_out_quint; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum AnimationDuration { - Instant = 50, - Fast = 150, - Slow = 300, -} - -impl AnimationDuration { - pub fn duration(&self) -> Duration { - Duration::from_millis(*self as u64) - } -} - -impl Into for AnimationDuration { - fn into(self) -> Duration { - self.duration() - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum AnimationDirection { - FromBottom, - FromLeft, - FromRight, - FromTop, -} - -pub trait DefaultAnimations: Styled + Sized + Element { - fn animate_in( - self, - animation_type: AnimationDirection, - fade_in: bool, - ) -> AnimationElement { - let animation_name = match animation_type { - AnimationDirection::FromBottom => "animate_from_bottom", - AnimationDirection::FromLeft => "animate_from_left", - AnimationDirection::FromRight => "animate_from_right", - AnimationDirection::FromTop => "animate_from_top", - }; - - let animation_id = self.id().map_or_else( - || ElementId::from(animation_name), - |id| (id, animation_name).into(), - ); - - self.with_animation( - animation_id, - gpui::Animation::new(AnimationDuration::Fast.into()).with_easing(ease_out_quint()), - move |mut this, delta| { - let start_opacity = 0.4; - let start_pos = 0.0; - let end_pos = 40.0; - - if fade_in { - this = this.opacity(start_opacity + delta * (1.0 - start_opacity)); - } - - match animation_type { - AnimationDirection::FromBottom => { - this.bottom(px(start_pos + delta * (end_pos - start_pos))) - } - AnimationDirection::FromLeft => { - this.left(px(start_pos + delta * (end_pos - start_pos))) - } - AnimationDirection::FromRight => { - this.right(px(start_pos + delta * (end_pos - start_pos))) - } - AnimationDirection::FromTop => { - this.top(px(start_pos + delta * (end_pos - start_pos))) - } - } - }, - ) - } - - fn animate_in_from_bottom(self, fade: bool) -> AnimationElement { - self.animate_in(AnimationDirection::FromBottom, fade) - } - - fn animate_in_from_left(self, fade: bool) -> AnimationElement { - self.animate_in(AnimationDirection::FromLeft, fade) - } - - fn animate_in_from_right(self, fade: bool) -> AnimationElement { - self.animate_in(AnimationDirection::FromRight, fade) - } - - fn animate_in_from_top(self, fade: bool) -> AnimationElement { - self.animate_in(AnimationDirection::FromTop, fade) - } -} - -impl DefaultAnimations for E {} - -// Don't use this directly, it only exists to show animation previews -#[derive(RegisterComponent)] -struct Animation {} - -impl Component for Animation { - fn scope() -> ComponentScope { - ComponentScope::Utilities - } - - fn description() -> Option<&'static str> { - Some("Demonstrates various animation patterns and transitions available in the UI system.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - let container_size = 128.0; - let element_size = 32.0; - let offset = container_size / 2.0 - element_size / 2.0; - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Animate In", - vec![ - single_example( - "From Bottom", - ContentGroup::new() - .relative() - .items_center() - .justify_center() - .size(px(container_size)) - .child( - div() - .id("animate-in-from-bottom") - .absolute() - .size(px(element_size)) - .left(px(offset)) - .rounded_md() - .bg(gpui::red()) - .animate_in_from_bottom(false), - ) - .into_any_element(), - ), - single_example( - "From Top", - ContentGroup::new() - .relative() - .items_center() - .justify_center() - .size(px(container_size)) - .child( - div() - .id("animate-in-from-top") - .absolute() - .size(px(element_size)) - .left(px(offset)) - .rounded_md() - .bg(gpui::blue()) - .animate_in_from_top(false), - ) - .into_any_element(), - ), - single_example( - "From Left", - ContentGroup::new() - .relative() - .items_center() - .justify_center() - .size(px(container_size)) - .child( - div() - .id("animate-in-from-left") - .absolute() - .size(px(element_size)) - .top(px(offset)) - .rounded_md() - .bg(gpui::green()) - .animate_in_from_left(false), - ) - .into_any_element(), - ), - single_example( - "From Right", - ContentGroup::new() - .relative() - .items_center() - .justify_center() - .size(px(container_size)) - .child( - div() - .id("animate-in-from-right") - .absolute() - .size(px(element_size)) - .top(px(offset)) - .rounded_md() - .bg(gpui::yellow()) - .animate_in_from_right(false), - ) - .into_any_element(), - ), - ], - ) - .grow(), - example_group_with_title( - "Fade and Animate In", - vec![ - single_example( - "From Bottom", - ContentGroup::new() - .relative() - .items_center() - .justify_center() - .size(px(container_size)) - .child( - div() - .id("fade-animate-in-from-bottom") - .absolute() - .size(px(element_size)) - .left(px(offset)) - .rounded_md() - .bg(gpui::red()) - .animate_in_from_bottom(true), - ) - .into_any_element(), - ), - single_example( - "From Top", - ContentGroup::new() - .relative() - .items_center() - .justify_center() - .size(px(container_size)) - .child( - div() - .id("fade-animate-in-from-top") - .absolute() - .size(px(element_size)) - .left(px(offset)) - .rounded_md() - .bg(gpui::blue()) - .animate_in_from_top(true), - ) - .into_any_element(), - ), - single_example( - "From Left", - ContentGroup::new() - .relative() - .items_center() - .justify_center() - .size(px(container_size)) - .child( - div() - .id("fade-animate-in-from-left") - .absolute() - .size(px(element_size)) - .top(px(offset)) - .rounded_md() - .bg(gpui::green()) - .animate_in_from_left(true), - ) - .into_any_element(), - ), - single_example( - "From Right", - ContentGroup::new() - .relative() - .items_center() - .justify_center() - .size(px(container_size)) - .child( - div() - .id("fade-animate-in-from-right") - .absolute() - .size(px(element_size)) - .top(px(offset)) - .rounded_md() - .bg(gpui::yellow()) - .animate_in_from_right(true), - ) - .into_any_element(), - ), - ], - ) - .grow(), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/styles/appearance.rs b/crates/ui/src/styles/appearance.rs deleted file mode 100644 index 007511584e..0000000000 --- a/crates/ui/src/styles/appearance.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::prelude::*; -use gpui::{App, WindowBackgroundAppearance}; - -/// Returns the [WindowBackgroundAppearance]. -fn window_appearance(cx: &mut App) -> WindowBackgroundAppearance { - cx.theme().styles.window_background_appearance -} - -/// Returns if the window and it's surfaces are expected -/// to be transparent. -/// -/// Helps determine if you need to take extra steps to prevent -/// transparent backgrounds. -pub fn theme_is_transparent(cx: &mut App) -> bool { - matches!( - window_appearance(cx), - WindowBackgroundAppearance::Transparent | WindowBackgroundAppearance::Blurred - ) -} diff --git a/crates/ui/src/styles/color.rs b/crates/ui/src/styles/color.rs deleted file mode 100644 index 586b2ccc57..0000000000 --- a/crates/ui/src/styles/color.rs +++ /dev/null @@ -1,244 +0,0 @@ -use crate::{Label, LabelCommon, component_prelude::*, v_flex}; -use documented::{DocumentedFields, DocumentedVariants}; -use gpui::{App, Hsla, IntoElement, ParentElement, Styled}; -use theme::ActiveTheme; - -/// Sets a color that has a consistent meaning across all themes. -#[derive( - Debug, - Default, - Eq, - PartialEq, - Copy, - Clone, - RegisterComponent, - Documented, - DocumentedFields, - DocumentedVariants, -)] -pub enum Color { - #[default] - /// The default text color. Might be known as "foreground" or "primary" in - /// some theme systems. - /// - /// For less emphasis, consider using [`Color::Muted`] or [`Color::Hidden`]. - Default, - /// A text color used for accents, such as links or highlights. - Accent, - /// A color used to indicate a conflict, such as a version control merge conflict, or a conflict between a file in the editor and the file system. - Conflict, - /// A color used to indicate a newly created item, such as a new file in - /// version control, or a new file on disk. - Created, - /// It is highly, HIGHLY recommended not to use this! Using this color - /// means detaching it from any semantic meaning across themes. - /// - /// A custom color specified by an HSLA value. - Custom(Hsla), - /// A color used for all debugger UI elements. - Debugger, - /// A color used to indicate a deleted item, such as a file removed from version control. - Deleted, - /// A color used for disabled UI elements or text, like a disabled button or menu item. - Disabled, - /// A color used to indicate an error condition, or something the user - /// cannot do. In very rare cases, it might be used to indicate dangerous or - /// destructive action. - Error, - /// A color used for elements that represent something that is hidden, like - /// a hidden file, or an element that should be visually de-emphasized. - Hidden, - /// A color used for hint or suggestion text, often a blue color. Use this - /// color to represent helpful, or semantically neutral information. - Hint, - /// A color used for items that are intentionally ignored, such as files ignored by version control. - Ignored, - /// A color used for informational messages or status indicators, often a blue color. - Info, - /// A color used to indicate a modified item, such as an edited file, or a modified entry in version control. - Modified, - /// A color used for text or UI elements that should be visually muted or de-emphasized. - /// - /// For more emphasis, consider using [`Color::Default`]. - /// - /// For less emphasis, consider using [`Color::Hidden`]. - Muted, - /// A color used for placeholder text in input fields. - Placeholder, - /// A color associated with a specific player number. - Player(u32), - /// A color used to indicate selected text or UI elements. - Selected, - /// A color used to indicate a successful operation or status. - Success, - /// A version control color used to indicate a newly added file or content in version control. - VersionControlAdded, - /// A version control color used to indicate conflicting changes that need resolution. - VersionControlConflict, - /// A version control color used to indicate a file or content that has been deleted in version control. - VersionControlDeleted, - /// A version control color used to indicate files or content that is being ignored by version control. - VersionControlIgnored, - /// A version control color used to indicate modified files or content in version control. - VersionControlModified, - /// A color used to indicate a warning condition. - Warning, -} - -impl Color { - /// Returns the Color's HSLA value. - pub fn color(&self, cx: &App) -> Hsla { - match self { - Color::Default => cx.theme().colors().text, - Color::Muted => cx.theme().colors().text_muted, - Color::Created => cx.theme().status().created, - Color::Modified => cx.theme().status().modified, - Color::Conflict => cx.theme().status().conflict, - Color::Ignored => cx.theme().status().ignored, - Color::Debugger => cx.theme().colors().debugger_accent, - Color::Deleted => cx.theme().status().deleted, - Color::Disabled => cx.theme().colors().text_disabled, - Color::Hidden => cx.theme().status().hidden, - Color::Hint => cx.theme().status().hint, - Color::Info => cx.theme().status().info, - Color::Placeholder => cx.theme().colors().text_placeholder, - Color::Accent => cx.theme().colors().text_accent, - Color::Player(i) => cx.theme().styles.player.color_for_participant(*i).cursor, - Color::Error => cx.theme().status().error, - Color::Selected => cx.theme().colors().text_accent, - Color::Success => cx.theme().status().success, - Color::VersionControlAdded => cx.theme().colors().version_control_added, - Color::VersionControlConflict => cx.theme().colors().version_control_conflict, - Color::VersionControlDeleted => cx.theme().colors().version_control_deleted, - Color::VersionControlIgnored => cx.theme().colors().version_control_ignored, - Color::VersionControlModified => cx.theme().colors().version_control_modified, - Color::Warning => cx.theme().status().warning, - Color::Custom(color) => *color, - } - } -} - -impl From for Color { - fn from(color: Hsla) -> Self { - Color::Custom(color) - } -} - -impl Component for Color { - fn scope() -> ComponentScope { - ComponentScope::Utilities - } - - fn description() -> Option<&'static str> { - Some(Color::DOCS) - } - - fn preview(_window: &mut gpui::Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Text Colors", - vec![ - single_example( - "Default", - Label::new("Default text color") - .color(Color::Default) - .into_any_element(), - ) - .description(Color::Default.get_variant_docs()), - single_example( - "Muted", - Label::new("Muted text color") - .color(Color::Muted) - .into_any_element(), - ) - .description(Color::Muted.get_variant_docs()), - single_example( - "Accent", - Label::new("Accent text color") - .color(Color::Accent) - .into_any_element(), - ) - .description(Color::Accent.get_variant_docs()), - single_example( - "Disabled", - Label::new("Disabled text color") - .color(Color::Disabled) - .into_any_element(), - ) - .description(Color::Disabled.get_variant_docs()), - ], - ), - example_group_with_title( - "Status Colors", - vec![ - single_example( - "Success", - Label::new("Success status") - .color(Color::Success) - .into_any_element(), - ) - .description(Color::Success.get_variant_docs()), - single_example( - "Warning", - Label::new("Warning status") - .color(Color::Warning) - .into_any_element(), - ) - .description(Color::Warning.get_variant_docs()), - single_example( - "Error", - Label::new("Error status") - .color(Color::Error) - .into_any_element(), - ) - .description(Color::Error.get_variant_docs()), - single_example( - "Info", - Label::new("Info status") - .color(Color::Info) - .into_any_element(), - ) - .description(Color::Info.get_variant_docs()), - ], - ), - example_group_with_title( - "Version Control Colors", - vec![ - single_example( - "Created", - Label::new("Created item") - .color(Color::Created) - .into_any_element(), - ) - .description(Color::Created.get_variant_docs()), - single_example( - "Modified", - Label::new("Modified item") - .color(Color::Modified) - .into_any_element(), - ) - .description(Color::Modified.get_variant_docs()), - single_example( - "Deleted", - Label::new("Deleted item") - .color(Color::Deleted) - .into_any_element(), - ) - .description(Color::Deleted.get_variant_docs()), - single_example( - "Conflict", - Label::new("Conflict item") - .color(Color::Conflict) - .into_any_element(), - ) - .description(Color::Conflict.get_variant_docs()), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/styles/elevation.rs b/crates/ui/src/styles/elevation.rs deleted file mode 100644 index 35e8e499b9..0000000000 --- a/crates/ui/src/styles/elevation.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::fmt::{self, Display, Formatter}; - -use gpui::{App, BoxShadow, Hsla, hsla, point, px}; -use theme::{ActiveTheme, Appearance}; - -/// Today, elevation is primarily used to add shadows to elements, and set the correct background for elements like buttons. -/// -/// Elevation can be thought of as the physical closeness of an element to the -/// user. Elements with lower elevations are physically further away on the -/// z-axis and appear to be underneath elements with higher elevations. -/// -/// In the future, a more complete approach to elevation may be added. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ElevationIndex { - /// On the layer of the app background. This is under panels, panes, and - /// other surfaces. - Background, - /// The primary surface – Contains panels, panes, containers, etc. - Surface, - /// The same elevation as the primary surface, but used for the editable areas, like buffers - EditorSurface, - /// A surface that is elevated above the primary surface. but below washes, models, and dragged elements. - ElevatedSurface, - /// A surface above the [ElevationIndex::ElevatedSurface] that is used for dialogs, alerts, modals, etc. - ModalSurface, -} - -impl Display for ElevationIndex { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - ElevationIndex::Background => write!(f, "Background"), - ElevationIndex::Surface => write!(f, "Surface"), - ElevationIndex::EditorSurface => write!(f, "Editor Surface"), - ElevationIndex::ElevatedSurface => write!(f, "Elevated Surface"), - ElevationIndex::ModalSurface => write!(f, "Modal Surface"), - } - } -} - -impl ElevationIndex { - /// Returns an appropriate shadow for the given elevation index. - pub fn shadow(self, cx: &App) -> Vec { - let is_light = cx.theme().appearance() == Appearance::Light; - - match self { - ElevationIndex::Surface => vec![], - ElevationIndex::EditorSurface => vec![], - - ElevationIndex::ElevatedSurface => vec![ - BoxShadow { - color: hsla(0., 0., 0., 0.12), - offset: point(px(0.), px(2.)), - blur_radius: px(3.), - spread_radius: px(0.), - }, - BoxShadow { - color: hsla(0., 0., 0., if is_light { 0.03 } else { 0.06 }), - offset: point(px(1.), px(1.)), - blur_radius: px(0.), - spread_radius: px(0.), - }, - ], - - ElevationIndex::ModalSurface => vec![ - BoxShadow { - color: hsla(0., 0., 0., if is_light { 0.06 } else { 0.12 }), - offset: point(px(0.), px(2.)), - blur_radius: px(3.), - spread_radius: px(0.), - }, - BoxShadow { - color: hsla(0., 0., 0., if is_light { 0.06 } else { 0.08 }), - offset: point(px(0.), px(3.)), - blur_radius: px(6.), - spread_radius: px(0.), - }, - BoxShadow { - color: hsla(0., 0., 0., 0.04), - offset: point(px(0.), px(6.)), - blur_radius: px(12.), - spread_radius: px(0.), - }, - BoxShadow { - color: hsla(0., 0., 0., if is_light { 0.04 } else { 0.12 }), - offset: point(px(1.), px(1.)), - blur_radius: px(0.), - spread_radius: px(0.), - }, - ], - - _ => vec![], - } - } - - /// Returns the background color for the given elevation index. - pub fn bg(&self, cx: &mut App) -> Hsla { - match self { - ElevationIndex::Background => cx.theme().colors().background, - ElevationIndex::Surface => cx.theme().colors().surface_background, - ElevationIndex::EditorSurface => cx.theme().colors().editor_background, - ElevationIndex::ElevatedSurface => cx.theme().colors().elevated_surface_background, - ElevationIndex::ModalSurface => cx.theme().colors().elevated_surface_background, - } - } - - /// Returns a color that is appropriate a filled element on this elevation - pub fn on_elevation_bg(&self, cx: &App) -> Hsla { - match self { - ElevationIndex::Background => cx.theme().colors().surface_background, - ElevationIndex::Surface => cx.theme().colors().background, - ElevationIndex::EditorSurface => cx.theme().colors().surface_background, - ElevationIndex::ElevatedSurface => cx.theme().colors().background, - ElevationIndex::ModalSurface => cx.theme().colors().background, - } - } - - /// Attempts to return a darker background color than the current elevation index's background. - /// - /// If the current background color is already dark, it will return a lighter color instead. - pub fn darker_bg(&self, cx: &App) -> Hsla { - match self { - ElevationIndex::Background => cx.theme().colors().surface_background, - ElevationIndex::Surface => cx.theme().colors().editor_background, - ElevationIndex::EditorSurface => cx.theme().colors().surface_background, - ElevationIndex::ElevatedSurface => cx.theme().colors().editor_background, - ElevationIndex::ModalSurface => cx.theme().colors().editor_background, - } - } -} diff --git a/crates/ui/src/styles/platform.rs b/crates/ui/src/styles/platform.rs deleted file mode 100644 index 0d873a2ff2..0000000000 --- a/crates/ui/src/styles/platform.rs +++ /dev/null @@ -1,25 +0,0 @@ -/// The platform style to use when rendering UI. -/// -/// This can be used to abstract over platform differences. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum PlatformStyle { - /// Display in macOS style. - Mac, - /// Display in Linux style. - Linux, - /// Display in Windows style. - Windows, -} - -impl PlatformStyle { - /// Returns the [`PlatformStyle`] for the current platform. - pub const fn platform() -> Self { - if cfg!(any(target_os = "linux", target_os = "freebsd")) { - Self::Linux - } else if cfg!(target_os = "windows") { - Self::Windows - } else { - Self::Mac - } - } -} diff --git a/crates/ui/src/styles/severity.rs b/crates/ui/src/styles/severity.rs deleted file mode 100644 index 464f835186..0000000000 --- a/crates/ui/src/styles/severity.rs +++ /dev/null @@ -1,10 +0,0 @@ -/// Severity levels that determine the style of the component. -/// Usually, it affects the background. Most of the time, -/// it also follows with an icon corresponding the severity level. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Severity { - Info, - Success, - Warning, - Error, -} diff --git a/crates/ui/src/styles/spacing.rs b/crates/ui/src/styles/spacing.rs deleted file mode 100644 index c6629f5d88..0000000000 --- a/crates/ui/src/styles/spacing.rs +++ /dev/null @@ -1,55 +0,0 @@ -use gpui::{App, Pixels, Rems, px, rems}; -use settings::Settings; -use theme::{ThemeSettings, UiDensity}; -use ui_macros::derive_dynamic_spacing; - -// Derives [DynamicSpacing]. See [ui_macros::derive_dynamic_spacing]. -// -// There are 3 UI density settings: Compact, Default, and Comfortable. -// -// When a tuple of three values is provided, the values are used directly. -// -// Example: (1, 2, 4) => Compact: 1px, Default: 2px, Comfortable: 4px -// -// When a single value is provided, the standard spacing formula is -// used to derive the of spacing values. This formula can be found in -// the macro. -// -// Example: -// -// Assuming the standard formula is (n-4, n, n+4) -// -// 24 => Compact: 20px, Default: 24px, Comfortable: 28px -// -// The [DynamicSpacing] enum variants use a BaseXX format, -// where XX = the pixel value @ default rem size and the default UI density. -// -// Example: -// -// DynamicSpacing::Base16 would return 16px at the default UI scale & density. -derive_dynamic_spacing![ - (0, 0, 0), - (1, 1, 2), - (1, 2, 4), - (2, 3, 4), - (2, 4, 6), - (3, 6, 8), - (4, 8, 10), - (10, 12, 14), - (14, 16, 18), - (18, 20, 22), - 24, - 32, - 40, - 48 -]; - -/// Returns the current [`UiDensity`] setting. Use this to -/// modify or show something in the UI other than spacing. -/// -/// Do not use this to calculate spacing values. -/// -/// Always use [DynamicSpacing] for spacing values. -pub fn ui_density(cx: &mut App) -> UiDensity { - ThemeSettings::get_global(cx).ui_density -} diff --git a/crates/ui/src/styles/typography.rs b/crates/ui/src/styles/typography.rs deleted file mode 100644 index 2bb0b35720..0000000000 --- a/crates/ui/src/styles/typography.rs +++ /dev/null @@ -1,295 +0,0 @@ -use crate::prelude::*; -use gpui::{ - AnyElement, App, IntoElement, ParentElement, Rems, RenderOnce, SharedString, Styled, Window, - div, rems, -}; -use settings::Settings; -use theme::{ActiveTheme, ThemeSettings}; - -use crate::{Color, rems_from_px}; - -/// Extends [`gpui::Styled`] with typography-related styling methods. -pub trait StyledTypography: Styled + Sized { - /// Sets the font family to the buffer font. - fn font_buffer(self, cx: &App) -> Self { - let settings = ThemeSettings::get_global(cx); - let buffer_font_family = settings.buffer_font.family.clone(); - - self.font_family(buffer_font_family) - } - - /// Sets the font family to the UI font. - fn font_ui(self, cx: &App) -> Self { - let settings = ThemeSettings::get_global(cx); - let ui_font_family = settings.ui_font.family.clone(); - - self.font_family(ui_font_family) - } - - /// Sets the text size using a [`TextSize`]. - fn text_ui_size(self, size: TextSize, cx: &App) -> Self { - self.text_size(size.rems(cx)) - } - - /// The large size for UI text. - /// - /// `1rem` or `16px` at the default scale of `1rem` = `16px`. - /// - /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. - /// - /// Use `text_ui` for regular-sized text. - fn text_ui_lg(self, cx: &App) -> Self { - self.text_size(TextSize::Large.rems(cx)) - } - - /// The default size for UI text. - /// - /// `0.825rem` or `14px` at the default scale of `1rem` = `16px`. - /// - /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. - /// - /// Use `text_ui_sm` for smaller text. - fn text_ui(self, cx: &App) -> Self { - self.text_size(TextSize::default().rems(cx)) - } - - /// The small size for UI text. - /// - /// `0.75rem` or `12px` at the default scale of `1rem` = `16px`. - /// - /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. - /// - /// Use `text_ui` for regular-sized text. - fn text_ui_sm(self, cx: &App) -> Self { - self.text_size(TextSize::Small.rems(cx)) - } - - /// The extra small size for UI text. - /// - /// `0.625rem` or `10px` at the default scale of `1rem` = `16px`. - /// - /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. - /// - /// Use `text_ui` for regular-sized text. - fn text_ui_xs(self, cx: &App) -> Self { - self.text_size(TextSize::XSmall.rems(cx)) - } - - /// The font size for buffer text. - /// - /// Retrieves the default font size, or the user's custom font size if set. - /// - /// This should only be used for text that is displayed in a buffer, - /// or other places that text needs to match the user's buffer font size. - fn text_buffer(self, cx: &App) -> Self { - let settings = ThemeSettings::get_global(cx); - self.text_size(settings.buffer_font_size(cx)) - } -} - -impl StyledTypography for E {} - -/// A utility for getting the size of various semantic text sizes. -#[derive(Debug, Default, Clone)] -pub enum TextSize { - /// The default size for UI text. - /// - /// `0.825rem` or `14px` at the default scale of `1rem` = `16px`. - /// - /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. - #[default] - Default, - /// The large size for UI text. - /// - /// `1rem` or `16px` at the default scale of `1rem` = `16px`. - /// - /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. - Large, - - /// The small size for UI text. - /// - /// `0.75rem` or `12px` at the default scale of `1rem` = `16px`. - /// - /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. - Small, - - /// The extra small size for UI text. - /// - /// `0.625rem` or `10px` at the default scale of `1rem` = `16px`. - /// - /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. - XSmall, - - /// The `ui_font_size` set by the user. - Ui, - /// The `buffer_font_size` set by the user. - Editor, - // TODO: The terminal settings will need to be passed to - // ThemeSettings before we can enable this. - //// The `terminal.font_size` set by the user. - // Terminal, -} - -impl TextSize { - /// Returns the text size in rems. - pub fn rems(self, cx: &App) -> Rems { - let theme_settings = ThemeSettings::get_global(cx); - - match self { - Self::Large => rems_from_px(16.), - Self::Default => rems_from_px(14.), - Self::Small => rems_from_px(12.), - Self::XSmall => rems_from_px(10.), - Self::Ui => rems_from_px(theme_settings.ui_font_size(cx)), - Self::Editor => rems_from_px(theme_settings.buffer_font_size(cx)), - } - } - - pub fn pixels(self, cx: &App) -> Pixels { - let theme_settings = ThemeSettings::get_global(cx); - - match self { - Self::Large => px(16.), - Self::Default => px(14.), - Self::Small => px(12.), - Self::XSmall => px(10.), - Self::Ui => theme_settings.ui_font_size(cx), - Self::Editor => theme_settings.buffer_font_size(cx), - } - } -} - -/// The size of a [`Headline`] element -/// -/// Defaults to a Major Second scale. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] -pub enum HeadlineSize { - /// An extra small headline - `~14px` @16px/rem - XSmall, - /// A small headline - `16px` @16px/rem - Small, - #[default] - /// A medium headline - `~18px` @16px/rem - Medium, - /// A large headline - `~20px` @16px/rem - Large, - /// An extra large headline - `~22px` @16px/rem - XLarge, -} - -impl HeadlineSize { - /// Returns the headline size in rems. - pub fn rems(self) -> Rems { - match self { - Self::XSmall => rems(0.88), - Self::Small => rems(1.0), - Self::Medium => rems(1.125), - Self::Large => rems(1.27), - Self::XLarge => rems(1.43), - } - } - - /// Returns the line height for the headline size. - pub fn line_height(self) -> Rems { - match self { - Self::XSmall => rems(1.6), - Self::Small => rems(1.6), - Self::Medium => rems(1.6), - Self::Large => rems(1.6), - Self::XLarge => rems(1.6), - } - } -} - -/// A headline element, used to emphasize some text and -/// create a visual hierarchy. -#[derive(IntoElement, RegisterComponent)] -pub struct Headline { - size: HeadlineSize, - text: SharedString, - color: Color, -} - -impl RenderOnce for Headline { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let ui_font = ThemeSettings::get_global(cx).ui_font.clone(); - - div() - .font(ui_font) - .line_height(self.size.line_height()) - .text_size(self.size.rems()) - .text_color(cx.theme().colors().text) - .child(self.text) - } -} - -impl Headline { - /// Create a new headline element. - pub fn new(text: impl Into) -> Self { - Self { - size: HeadlineSize::default(), - text: text.into(), - color: Color::default(), - } - } - - /// Set the size of the headline. - pub fn size(mut self, size: HeadlineSize) -> Self { - self.size = size; - self - } - - /// Set the color of the headline. - pub fn color(mut self, color: Color) -> Self { - self.color = color; - self - } -} - -impl Component for Headline { - fn scope() -> ComponentScope { - ComponentScope::Typography - } - - fn description() -> Option<&'static str> { - Some("A headline element used to emphasize text and create visual hierarchy in the UI.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_1() - .children(vec![ - single_example( - "XLarge", - Headline::new("XLarge Headline") - .size(HeadlineSize::XLarge) - .into_any_element(), - ), - single_example( - "Large", - Headline::new("Large Headline") - .size(HeadlineSize::Large) - .into_any_element(), - ), - single_example( - "Medium (Default)", - Headline::new("Medium Headline").into_any_element(), - ), - single_example( - "Small", - Headline::new("Small Headline") - .size(HeadlineSize::Small) - .into_any_element(), - ), - single_example( - "XSmall", - Headline::new("XSmall Headline") - .size(HeadlineSize::XSmall) - .into_any_element(), - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/styles/units.rs b/crates/ui/src/styles/units.rs deleted file mode 100644 index 3fa6520204..0000000000 --- a/crates/ui/src/styles/units.rs +++ /dev/null @@ -1,29 +0,0 @@ -use gpui::{Length, Rems, Window, rems}; - -/// The base size of a rem, in pixels. -pub const BASE_REM_SIZE_IN_PX: f32 = 16.; - -/// Returns a rem value derived from the provided pixel value and the base rem size (16px). -/// -/// This can be used to compute rem values relative to pixel sizes, without -/// needing to hard-code the rem value. -/// -/// For instance, instead of writing `rems(0.875)` you can write `rems_from_px(14.)` -#[inline(always)] -pub fn rems_from_px(px: impl Into) -> Rems { - rems(px.into() / BASE_REM_SIZE_IN_PX) -} - -/// Returns a [`Length`] corresponding to the specified percentage of the viewport's width. -/// -/// `percent` should be a value between `0.0` and `1.0`. -pub fn vw(percent: f32, window: &mut Window) -> Length { - Length::from(window.viewport_size().width * percent) -} - -/// Returns a [`Length`] corresponding to the specified percentage of the viewport's height. -/// -/// `percent` should be a value between `0.0` and `1.0`. -pub fn vh(percent: f32, window: &mut Window) -> Length { - Length::from(window.viewport_size().height * percent) -} diff --git a/crates/ui/src/traits.rs b/crates/ui/src/traits.rs deleted file mode 100644 index 9627f6d6ad..0000000000 --- a/crates/ui/src/traits.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod animation_ext; -pub mod clickable; -pub mod disableable; -pub mod fixed; -pub mod styled_ext; -pub mod toggleable; -pub mod transformable; -pub mod visible_on_hover; diff --git a/crates/ui/src/traits/animation_ext.rs b/crates/ui/src/traits/animation_ext.rs deleted file mode 100644 index 4907c71ff2..0000000000 --- a/crates/ui/src/traits/animation_ext.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::time::Duration; - -use gpui::{Animation, AnimationElement, AnimationExt, Transformation, percentage}; - -use crate::{prelude::*, traits::transformable::Transformable}; - -/// An extension trait for adding common animations to animatable components. -pub trait CommonAnimationExt: AnimationExt { - /// Render this component as rotating over the given duration. - /// - /// NOTE: This method uses the location of the caller to generate an ID for this state. - /// If this is not sufficient to identify your state (e.g. you're rendering a list item), - /// you can provide a custom ElementID using the `use_keyed_rotate_animation` method. - #[track_caller] - fn with_rotate_animation(self, duration: u64) -> AnimationElement - where - Self: Transformable + Sized, - { - self.with_keyed_rotate_animation( - ElementId::CodeLocation(*std::panic::Location::caller()), - duration, - ) - } - - /// Render this component as rotating with the given element ID over the given duration. - fn with_keyed_rotate_animation( - self, - id: impl Into, - duration: u64, - ) -> AnimationElement - where - Self: Transformable + Sized, - { - self.with_animation( - id, - Animation::new(Duration::from_secs(duration)).repeat(), - |component, delta| component.transform(Transformation::rotate(percentage(delta))), - ) - } -} - -impl CommonAnimationExt for T {} diff --git a/crates/ui/src/traits/clickable.rs b/crates/ui/src/traits/clickable.rs deleted file mode 100644 index 55a4986b20..0000000000 --- a/crates/ui/src/traits/clickable.rs +++ /dev/null @@ -1,9 +0,0 @@ -use gpui::{App, ClickEvent, CursorStyle, Window}; - -/// A trait for elements that can be clicked. Enables the use of the `on_click` method. -pub trait Clickable { - /// Sets the click handler that will fire whenever the element is clicked. - fn on_click(self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self; - /// Sets the cursor style when hovering over the element. - fn cursor_style(self, cursor_style: CursorStyle) -> Self; -} diff --git a/crates/ui/src/traits/disableable.rs b/crates/ui/src/traits/disableable.rs deleted file mode 100644 index 9f08ed8d12..0000000000 --- a/crates/ui/src/traits/disableable.rs +++ /dev/null @@ -1,5 +0,0 @@ -/// A trait for elements that can be disabled. Generally used to implement disabling an element's interactivity and changing its appearance to reflect that it is disabled. -pub trait Disableable { - /// Sets whether the element is disabled. - fn disabled(self, disabled: bool) -> Self; -} diff --git a/crates/ui/src/traits/fixed.rs b/crates/ui/src/traits/fixed.rs deleted file mode 100644 index 6ca9c8617f..0000000000 --- a/crates/ui/src/traits/fixed.rs +++ /dev/null @@ -1,10 +0,0 @@ -use gpui::DefiniteLength; - -/// A trait for elements that can have a fixed with. Enables the use of the `width` and `full_width` methods. -pub trait FixedWidth { - /// Sets the width of the element. - fn width(self, width: impl Into) -> Self; - - /// Sets the element's width to the full width of its container. - fn full_width(self) -> Self; -} diff --git a/crates/ui/src/traits/styled_ext.rs b/crates/ui/src/traits/styled_ext.rs deleted file mode 100644 index 849e56a024..0000000000 --- a/crates/ui/src/traits/styled_ext.rs +++ /dev/null @@ -1,134 +0,0 @@ -use gpui::{App, Styled, hsla}; - -use crate::ElevationIndex; -use crate::prelude::*; - -fn elevated(this: E, cx: &App, index: ElevationIndex) -> E { - this.bg(cx.theme().colors().elevated_surface_background) - .rounded_lg() - .border_1() - .border_color(cx.theme().colors().border_variant) - .shadow(index.shadow(cx)) -} - -fn elevated_borderless(this: E, cx: &mut App, index: ElevationIndex) -> E { - this.bg(cx.theme().colors().elevated_surface_background) - .rounded_lg() - .shadow(index.shadow(cx)) -} - -/// Extends [`gpui::Styled`] with Zed-specific styling methods. -// gate on rust-analyzer so rust-analyzer never needs to expand this macro, it takes up to 10 seconds to expand due to inefficiencies in rust-analyzers proc-macro srv -#[cfg_attr( - all(debug_assertions, not(rust_analyzer)), - gpui_macros::derive_inspector_reflection -)] -pub trait StyledExt: Styled + Sized { - /// Horizontally stacks elements. - /// - /// Sets `flex()`, `flex_row()`, `items_center()` - fn h_flex(self) -> Self { - self.flex().flex_row().items_center() - } - - /// Vertically stacks elements. - /// - /// Sets `flex()`, `flex_col()` - fn v_flex(self) -> Self { - self.flex().flex_col() - } - - /// The [`Surface`](ElevationIndex::Surface) elevation level, located above the app background, is the standard level for all elements - /// - /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` - /// - /// Example Elements: Title Bar, Panel, Tab Bar, Editor - fn elevation_1(self, cx: &App) -> Self { - elevated(self, cx, ElevationIndex::Surface) - } - - /// See [`elevation_1`](Self::elevation_1). - /// - /// Renders a borderless version [`elevation_1`](Self::elevation_1). - fn elevation_1_borderless(self, cx: &mut App) -> Self { - elevated_borderless(self, cx, ElevationIndex::Surface) - } - - /// Non-Modal Elevated Surfaces appear above the [`Surface`](ElevationIndex::Surface) layer and is used for things that should appear above most UI elements like an editor or panel, but not elements like popovers, context menus, modals, etc. - /// - /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` - /// - /// Examples: Notifications, Palettes, Detached/Floating Windows, Detached/Floating Panels - fn elevation_2(self, cx: &App) -> Self { - elevated(self, cx, ElevationIndex::ElevatedSurface) - } - - /// See [`elevation_2`](Self::elevation_2). - /// - /// Renders a borderless version [`elevation_2`](Self::elevation_2). - fn elevation_2_borderless(self, cx: &mut App) -> Self { - elevated_borderless(self, cx, ElevationIndex::ElevatedSurface) - } - - /// Modal Surfaces are used for elements that should appear above all other UI elements and are located above the wash layer. This is the maximum elevation at which UI elements can be rendered in their default state. - /// - /// Elements rendered at this layer should have an enforced behavior: Any interaction outside of the modal will either dismiss the modal or prompt an action (Save your progress, etc) then dismiss the modal. - /// - /// If the element does not have this behavior, it should be rendered at the [`Elevated Surface`](ElevationIndex::ElevatedSurface) layer. - /// - /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` - /// - /// Examples: Settings Modal, Channel Management, Wizards/Setup UI, Dialogs - fn elevation_3(self, cx: &App) -> Self { - elevated(self, cx, ElevationIndex::ModalSurface) - } - - /// See [`elevation_3`](Self::elevation_3). - /// - /// Renders a borderless version [`elevation_3`](Self::elevation_3). - fn elevation_3_borderless(self, cx: &mut App) -> Self { - elevated_borderless(self, cx, ElevationIndex::ModalSurface) - } - - /// The theme's primary border color. - fn border_primary(self, cx: &mut App) -> Self { - self.border_color(cx.theme().colors().border) - } - - /// The theme's secondary or muted border color. - fn border_muted(self, cx: &mut App) -> Self { - self.border_color(cx.theme().colors().border_variant) - } - - /// Sets the background color to red for debugging when building UI. - fn debug_bg_red(self) -> Self { - self.bg(hsla(0. / 360., 1., 0.5, 1.)) - } - - /// Sets the background color to green for debugging when building UI. - fn debug_bg_green(self) -> Self { - self.bg(hsla(120. / 360., 1., 0.5, 1.)) - } - - /// Sets the background color to blue for debugging when building UI. - fn debug_bg_blue(self) -> Self { - self.bg(hsla(240. / 360., 1., 0.5, 1.)) - } - - /// Sets the background color to yellow for debugging when building UI. - fn debug_bg_yellow(self) -> Self { - self.bg(hsla(60. / 360., 1., 0.5, 1.)) - } - - /// Sets the background color to cyan for debugging when building UI. - fn debug_bg_cyan(self) -> Self { - self.bg(hsla(160. / 360., 1., 0.5, 1.)) - } - - /// Sets the background color to magenta for debugging when building UI. - fn debug_bg_magenta(self) -> Self { - self.bg(hsla(300. / 360., 1., 0.5, 1.)) - } -} - -impl StyledExt for E {} diff --git a/crates/ui/src/traits/toggleable.rs b/crates/ui/src/traits/toggleable.rs deleted file mode 100644 index f731f9965e..0000000000 --- a/crates/ui/src/traits/toggleable.rs +++ /dev/null @@ -1,69 +0,0 @@ -/// A trait for elements that can be toggled. -/// -/// Implement this for elements that are visually distinct -/// when in two opposing states, like checkboxes or switches. -pub trait Toggleable { - /// Sets whether the element is selected. - fn toggle_state(self, selected: bool) -> Self; -} - -/// Represents the selection status of an element. -#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)] -pub enum ToggleState { - /// The element is not selected. - #[default] - Unselected, - /// The selection state of the element is indeterminate. - Indeterminate, - /// The element is selected. - Selected, -} - -impl ToggleState { - /// Returns the inverse of the current selection status. - /// - /// Indeterminate states become selected if inverted. - pub fn inverse(&self) -> Self { - match self { - Self::Unselected | Self::Indeterminate => Self::Selected, - Self::Selected => Self::Unselected, - } - } - - /// Creates a `ToggleState` from the given `any_checked` and `all_checked` flags. - pub fn from_any_and_all(any_checked: bool, all_checked: bool) -> Self { - match (any_checked, all_checked) { - (true, true) => Self::Selected, - (false, false) => Self::Unselected, - _ => Self::Indeterminate, - } - } - - /// Returns whether this toggle state is selected - pub fn selected(&self) -> bool { - match self { - ToggleState::Indeterminate | ToggleState::Unselected => false, - ToggleState::Selected => true, - } - } -} - -impl From for ToggleState { - fn from(selected: bool) -> Self { - if selected { - Self::Selected - } else { - Self::Unselected - } - } -} - -impl From> for ToggleState { - fn from(selected: Option) -> Self { - match selected { - Some(true) => Self::Selected, - Some(false) => Self::Unselected, - None => Self::Indeterminate, - } - } -} diff --git a/crates/ui/src/traits/transformable.rs b/crates/ui/src/traits/transformable.rs deleted file mode 100644 index f52141f304..0000000000 --- a/crates/ui/src/traits/transformable.rs +++ /dev/null @@ -1,7 +0,0 @@ -use gpui::Transformation; - -/// A trait for components that can be transformed. -pub trait Transformable { - /// Sets the transformation for the element. - fn transform(self, transformation: Transformation) -> Self; -} diff --git a/crates/ui/src/traits/visible_on_hover.rs b/crates/ui/src/traits/visible_on_hover.rs deleted file mode 100644 index fc0bb837d7..0000000000 --- a/crates/ui/src/traits/visible_on_hover.rs +++ /dev/null @@ -1,17 +0,0 @@ -use gpui::{InteractiveElement, SharedString, Styled}; - -/// A trait for elements that can be made visible on hover by -/// tracking a specific group. -pub trait VisibleOnHover { - /// Sets the element to only be visible when the specified group is hovered. - /// - /// Pass `""` as the `group_name` to use the global group. - fn visible_on_hover(self, group_name: impl Into) -> Self; -} - -impl VisibleOnHover for E { - fn visible_on_hover(self, group_name: impl Into) -> Self { - self.invisible() - .group_hover(group_name, |style| style.visible()) - } -} diff --git a/crates/ui/src/ui.rs b/crates/ui/src/ui.rs deleted file mode 100644 index 17e707f11b..0000000000 --- a/crates/ui/src/ui.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! # UI – Zed UI Primitives & Components -//! -//! This crate provides a set of UI primitives and components that are used to build all of the elements in Zed's UI. -//! -//! ## Related Crates: -//! -//! - [`ui_macros`] - proc_macros support for this crate -//! - `ui_input` - the single line input component - -pub mod component_prelude; -mod components; -pub mod prelude; -mod styles; -mod traits; -pub mod utils; - -pub use components::*; -pub use prelude::*; -pub use styles::*; -pub use traits::animation_ext::*; diff --git a/crates/ui/src/utils.rs b/crates/ui/src/utils.rs deleted file mode 100644 index cd7d8eb497..0000000000 --- a/crates/ui/src/utils.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! UI-related utilities - -use gpui::App; -use theme::ActiveTheme; - -mod apca_contrast; -mod color_contrast; -mod corner_solver; -mod format_distance; -mod search_input; -mod with_rem_size; - -pub use apca_contrast::*; -pub use color_contrast::*; -pub use corner_solver::{CornerSolver, inner_corner_radius}; -pub use format_distance::*; -pub use search_input::*; -pub use with_rem_size::*; - -/// Returns true if the current theme is light or vibrant light. -pub fn is_light(cx: &mut App) -> bool { - cx.theme().appearance.is_light() -} diff --git a/crates/ui/src/utils/apca_contrast.rs b/crates/ui/src/utils/apca_contrast.rs deleted file mode 100644 index 341b44670d..0000000000 --- a/crates/ui/src/utils/apca_contrast.rs +++ /dev/null @@ -1,479 +0,0 @@ -use gpui::Hsla; - -/// APCA (Accessible Perceptual Contrast Algorithm) constants -/// Based on APCA 0.0.98G-4g W3 compatible constants -/// https://github.com/Myndex/apca-w3 -struct APCAConstants { - // Main TRC exponent for monitor perception - main_trc: f32, - - // sRGB coefficients - s_rco: f32, - s_gco: f32, - s_bco: f32, - - // G-4g constants for use with 2.4 exponent - norm_bg: f32, - norm_txt: f32, - rev_txt: f32, - rev_bg: f32, - - // G-4g Clamps and Scalers - blk_thrs: f32, - blk_clmp: f32, - scale_bow: f32, - scale_wob: f32, - lo_bow_offset: f32, - lo_wob_offset: f32, - delta_y_min: f32, - lo_clip: f32, -} - -impl Default for APCAConstants { - fn default() -> Self { - Self { - main_trc: 2.4, - s_rco: 0.2126729, - s_gco: 0.7151522, - s_bco: 0.0721750, - norm_bg: 0.56, - norm_txt: 0.57, - rev_txt: 0.62, - rev_bg: 0.65, - blk_thrs: 0.022, - blk_clmp: 1.414, - scale_bow: 1.14, - scale_wob: 1.14, - lo_bow_offset: 0.027, - lo_wob_offset: 0.027, - delta_y_min: 0.0005, - lo_clip: 0.1, - } - } -} - -/// Calculates the perceptual lightness contrast using APCA. -/// Returns a value between approximately -108 and 106. -/// Negative values indicate light text on dark background. -/// Positive values indicate dark text on light background. -/// -/// The APCA algorithm is more perceptually accurate than WCAG 2.x, -/// especially for dark mode interfaces. Key improvements include: -/// - Better accuracy for dark backgrounds -/// - Polarity-aware (direction matters) -/// - Perceptually uniform across the range -/// -/// Common APCA Lc thresholds per ARC Bronze Simple Mode: -/// https://readtech.org/ARC/tests/bronze-simple-mode/ -/// - Lc 45: Minimum for large fluent text (36px+) -/// - Lc 60: Minimum for other content text -/// - Lc 75: Minimum for body text -/// - Lc 90: Preferred for body text -/// -/// Most terminal themes use colors with APCA values of 40-70. -/// -/// https://github.com/Myndex/apca-w3 -pub fn apca_contrast(text_color: Hsla, background_color: Hsla) -> f32 { - let constants = APCAConstants::default(); - - let text_y = srgb_to_y(text_color, &constants); - let bg_y = srgb_to_y(background_color, &constants); - - // Apply soft clamp to near-black colors - let text_y_clamped = if text_y > constants.blk_thrs { - text_y - } else { - text_y + (constants.blk_thrs - text_y).powf(constants.blk_clmp) - }; - - let bg_y_clamped = if bg_y > constants.blk_thrs { - bg_y - } else { - bg_y + (constants.blk_thrs - bg_y).powf(constants.blk_clmp) - }; - - // Return 0 for extremely low delta Y - if (bg_y_clamped - text_y_clamped).abs() < constants.delta_y_min { - return 0.0; - } - - let sapc; - let output_contrast; - - if bg_y_clamped > text_y_clamped { - // Normal polarity: dark text on light background - sapc = (bg_y_clamped.powf(constants.norm_bg) - text_y_clamped.powf(constants.norm_txt)) - * constants.scale_bow; - - // Low contrast smooth rollout to prevent polarity reversal - output_contrast = if sapc < constants.lo_clip { - 0.0 - } else { - sapc - constants.lo_bow_offset - }; - } else { - // Reverse polarity: light text on dark background - sapc = (bg_y_clamped.powf(constants.rev_bg) - text_y_clamped.powf(constants.rev_txt)) - * constants.scale_wob; - - output_contrast = if sapc > -constants.lo_clip { - 0.0 - } else { - sapc + constants.lo_wob_offset - }; - } - - // Return Lc (lightness contrast) scaled to percentage - output_contrast * 100.0 -} - -/// Converts sRGB color to Y (luminance) for APCA calculation -fn srgb_to_y(color: Hsla, constants: &APCAConstants) -> f32 { - let rgba = color.to_rgb(); - - // Linearize and apply coefficients - let r_linear = (rgba.r).powf(constants.main_trc); - let g_linear = (rgba.g).powf(constants.main_trc); - let b_linear = (rgba.b).powf(constants.main_trc); - - constants.s_rco * r_linear + constants.s_gco * g_linear + constants.s_bco * b_linear -} - -/// Adjusts the foreground color to meet the minimum APCA contrast against the background. -/// The minimum_apca_contrast should be an absolute value (e.g., 75 for Lc 75). -/// -/// This implementation gradually adjusts the lightness while preserving the hue and -/// saturation as much as possible, only falling back to black/white when necessary. -pub fn ensure_minimum_contrast( - foreground: Hsla, - background: Hsla, - minimum_apca_contrast: f32, -) -> Hsla { - if minimum_apca_contrast <= 0.0 { - return foreground; - } - - let current_contrast = apca_contrast(foreground, background).abs(); - - if current_contrast >= minimum_apca_contrast { - return foreground; - } - - // First, try to adjust lightness while preserving hue and saturation - let adjusted = adjust_lightness_for_contrast(foreground, background, minimum_apca_contrast); - - let adjusted_contrast = apca_contrast(adjusted, background).abs(); - if adjusted_contrast >= minimum_apca_contrast { - return adjusted; - } - - // If that's not enough, gradually reduce saturation while adjusting lightness - let desaturated = - adjust_lightness_and_saturation_for_contrast(foreground, background, minimum_apca_contrast); - - let desaturated_contrast = apca_contrast(desaturated, background).abs(); - if desaturated_contrast >= minimum_apca_contrast { - return desaturated; - } - - // Last resort: use black or white - let black = Hsla { - h: 0.0, - s: 0.0, - l: 0.0, - a: foreground.a, - }; - - let white = Hsla { - h: 0.0, - s: 0.0, - l: 1.0, - a: foreground.a, - }; - - let black_contrast = apca_contrast(black, background).abs(); - let white_contrast = apca_contrast(white, background).abs(); - - if white_contrast > black_contrast { - white - } else { - black - } -} - -/// Adjusts only the lightness to meet the minimum contrast, preserving hue and saturation -fn adjust_lightness_for_contrast( - foreground: Hsla, - background: Hsla, - minimum_apca_contrast: f32, -) -> Hsla { - // Determine if we need to go lighter or darker - let bg_luminance = srgb_to_y(background, &APCAConstants::default()); - let should_go_darker = bg_luminance > 0.5; - - // Binary search for the optimal lightness - let mut low = if should_go_darker { 0.0 } else { foreground.l }; - let mut high = if should_go_darker { foreground.l } else { 1.0 }; - let mut best_l = foreground.l; - - for _ in 0..20 { - let mid = (low + high) / 2.0; - let test_color = Hsla { - h: foreground.h, - s: foreground.s, - l: mid, - a: foreground.a, - }; - - let contrast = apca_contrast(test_color, background).abs(); - - if contrast >= minimum_apca_contrast { - best_l = mid; - // Try to get closer to the minimum - if should_go_darker { - low = mid; - } else { - high = mid; - } - } else if should_go_darker { - high = mid; - } else { - low = mid; - } - - // If we're close enough to the target, stop - if (contrast - minimum_apca_contrast).abs() < 1.0 { - best_l = mid; - break; - } - } - - Hsla { - h: foreground.h, - s: foreground.s, - l: best_l, - a: foreground.a, - } -} - -/// Adjusts both lightness and saturation to meet the minimum contrast -fn adjust_lightness_and_saturation_for_contrast( - foreground: Hsla, - background: Hsla, - minimum_apca_contrast: f32, -) -> Hsla { - // Try different saturation levels - let saturation_steps = [1.0, 0.8, 0.6, 0.4, 0.2, 0.0]; - - for &sat_multiplier in &saturation_steps { - let test_color = Hsla { - h: foreground.h, - s: foreground.s * sat_multiplier, - l: foreground.l, - a: foreground.a, - }; - - let adjusted = adjust_lightness_for_contrast(test_color, background, minimum_apca_contrast); - let contrast = apca_contrast(adjusted, background).abs(); - - if contrast >= minimum_apca_contrast { - return adjusted; - } - } - - // If we get here, even grayscale didn't work, so return the grayscale attempt - Hsla { - h: foreground.h, - s: 0.0, - l: foreground.l, - a: foreground.a, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla { - Hsla { h, s, l, a } - } - - fn hsla_from_hex(hex: u32) -> Hsla { - let r = ((hex >> 16) & 0xFF) as f32 / 255.0; - let g = ((hex >> 8) & 0xFF) as f32 / 255.0; - let b = (hex & 0xFF) as f32 / 255.0; - - let max = r.max(g).max(b); - let min = r.min(g).min(b); - let l = (max + min) / 2.0; - - if max == min { - // Achromatic - Hsla { - h: 0.0, - s: 0.0, - l, - a: 1.0, - } - } else { - let d = max - min; - let s = if l > 0.5 { - d / (2.0 - max - min) - } else { - d / (max + min) - }; - - let h = if max == r { - (g - b) / d + if g < b { 6.0 } else { 0.0 } - } else if max == g { - (b - r) / d + 2.0 - } else { - (r - g) / d + 4.0 - } / 6.0; - - Hsla { h, s, l, a: 1.0 } - } - } - - #[test] - fn test_apca_contrast() { - // Test black text on white background (should be positive) - let black = hsla(0.0, 0.0, 0.0, 1.0); - let white = hsla(0.0, 0.0, 1.0, 1.0); - let contrast = apca_contrast(black, white); - assert!( - contrast > 100.0, - "Black on white should have high positive contrast, got {}", - contrast - ); - - // Test white text on black background (should be negative) - let contrast_reversed = apca_contrast(white, black); - assert!( - contrast_reversed < -100.0, - "White on black should have high negative contrast, got {}", - contrast_reversed - ); - - // Same color should have zero contrast - let gray = hsla(0.0, 0.0, 0.5, 1.0); - let contrast_same = apca_contrast(gray, gray); - assert!( - contrast_same.abs() < 1.0, - "Same color should have near-zero contrast, got {}", - contrast_same - ); - - // APCA is NOT commutative - polarity matters - assert!( - (contrast + contrast_reversed).abs() > 1.0, - "APCA should not be commutative" - ); - } - - #[test] - fn test_srgb_to_y() { - let constants = APCAConstants::default(); - - // Test known Y values - let black = hsla(0.0, 0.0, 0.0, 1.0); - let y_black = srgb_to_y(black, &constants); - assert!( - y_black.abs() < 0.001, - "Black should have Y near 0, got {}", - y_black - ); - - let white = hsla(0.0, 0.0, 1.0, 1.0); - let y_white = srgb_to_y(white, &constants); - assert!( - (y_white - 1.0).abs() < 0.001, - "White should have Y near 1, got {}", - y_white - ); - } - - #[test] - fn test_srgb_to_y_nan_issue() { - let dark_red = hsla_from_hex(0x5f0000); - let y_dark_red = srgb_to_y(dark_red, &APCAConstants::default()); - assert!(!y_dark_red.is_nan()); - } - - #[test] - fn test_ensure_minimum_contrast() { - let white_bg = hsla(0.0, 0.0, 1.0, 1.0); - let light_gray = hsla(0.0, 0.0, 0.9, 1.0); - - // Light gray on white has poor contrast - let initial_contrast = apca_contrast(light_gray, white_bg).abs(); - assert!( - initial_contrast < 15.0, - "Initial contrast should be low, got {}", - initial_contrast - ); - - // Should be adjusted to black for better contrast (using APCA Lc 45 as minimum) - let adjusted = ensure_minimum_contrast(light_gray, white_bg, 45.0); - assert_eq!(adjusted.l, 0.0); // Should be black - assert_eq!(adjusted.a, light_gray.a); // Alpha preserved - - // Test with dark background - let black_bg = hsla(0.0, 0.0, 0.0, 1.0); - let dark_gray = hsla(0.0, 0.0, 0.1, 1.0); - - // Dark gray on black has poor contrast - let initial_contrast = apca_contrast(dark_gray, black_bg).abs(); - assert!( - initial_contrast < 15.0, - "Initial contrast should be low, got {}", - initial_contrast - ); - - // Should be adjusted to white for better contrast - let adjusted = ensure_minimum_contrast(dark_gray, black_bg, 45.0); - assert_eq!(adjusted.l, 1.0); // Should be white - - // Test when contrast is already sufficient - let black = hsla(0.0, 0.0, 0.0, 1.0); - let adjusted = ensure_minimum_contrast(black, white_bg, 45.0); - assert_eq!(adjusted, black); // Should remain unchanged - } - - #[test] - fn test_one_light_theme_exact_colors() { - // Test with exact colors from One Light theme - // terminal.background and terminal.ansi.white are both #fafafaff - let fafafa = hsla_from_hex(0xfafafa); - - // They should be identical - let bg = fafafa; - let fg = fafafa; - - // Contrast should be 0 (no contrast) - let contrast = apca_contrast(fg, bg); - assert!( - contrast.abs() < 1.0, - "Same color should have near-zero APCA contrast, got {}", - contrast - ); - - // With minimum APCA contrast of 15 (very low, but detectable), it should adjust - let adjusted = ensure_minimum_contrast(fg, bg, 15.0); - // The new algorithm preserves colors, so we just need to check contrast - let new_contrast = apca_contrast(adjusted, bg).abs(); - assert!( - new_contrast >= 15.0, - "Adjusted contrast {} should be >= 15.0", - new_contrast - ); - - // The adjusted color should have sufficient contrast - let new_contrast = apca_contrast(adjusted, bg).abs(); - assert!( - new_contrast >= 15.0, - "Adjusted APCA contrast {} should be >= 15.0", - new_contrast - ); - } -} diff --git a/crates/ui/src/utils/color_contrast.rs b/crates/ui/src/utils/color_contrast.rs deleted file mode 100644 index 2a6b4bf281..0000000000 --- a/crates/ui/src/utils/color_contrast.rs +++ /dev/null @@ -1,70 +0,0 @@ -use gpui::{Hsla, Rgba}; - -/// Calculates the contrast ratio between two colors according to WCAG 2.0 standards. -/// -/// The formula used is: -/// (L1 + 0.05) / (L2 + 0.05), where L1 is the lighter of the two luminances and L2 is the darker. -/// -/// Returns a float representing the contrast ratio. A higher value indicates more contrast. -/// The range of the returned value is 1 to 21 (commonly written as 1:1 to 21:1). -pub fn calculate_contrast_ratio(fg: Hsla, bg: Hsla) -> f32 { - let l1 = relative_luminance(fg); - let l2 = relative_luminance(bg); - - let (lighter, darker) = if l1 > l2 { (l1, l2) } else { (l2, l1) }; - - (lighter + 0.05) / (darker + 0.05) -} - -/// Calculates the relative luminance of a color. -/// -/// The relative luminance is the relative brightness of any point in a colorspace, -/// normalized to 0 for darkest black and 1 for lightest white. -fn relative_luminance(color: Hsla) -> f32 { - let rgba: Rgba = color.into(); - let r = linearize(rgba.r); - let g = linearize(rgba.g); - let b = linearize(rgba.b); - - 0.2126 * r + 0.7152 * g + 0.0722 * b -} - -/// Linearizes an RGB component. -fn linearize(component: f32) -> f32 { - if component <= 0.03928 { - component / 12.92 - } else { - ((component + 0.055) / 1.055).powf(2.4) - } -} - -#[cfg(test)] -mod tests { - use gpui::hsla; - - use super::*; - - // Test the contrast ratio formula with some common color combinations to - // prevent regressions in either the color conversions or the formula itself. - #[test] - fn test_contrast_ratio_formula() { - // White on Black (should be close to 21:1) - let white = hsla(0.0, 0.0, 1.0, 1.0); - let black = hsla(0.0, 0.0, 0.0, 1.0); - assert!((calculate_contrast_ratio(white, black) - 21.0).abs() < 0.1); - - // Black on White (should be close to 21:1) - assert!((calculate_contrast_ratio(black, white) - 21.0).abs() < 0.1); - - // Mid-gray on Black (should be close to 5.32:1) - let mid_gray = hsla(0.0, 0.0, 0.5, 1.0); - assert!((calculate_contrast_ratio(mid_gray, black) - 5.32).abs() < 0.1); - - // White on Mid-gray (should be close to 3.95:1) - assert!((calculate_contrast_ratio(white, mid_gray) - 3.95).abs() < 0.1); - - // Same color (should be 1:1) - let red = hsla(0.0, 1.0, 0.5, 1.0); - assert!((calculate_contrast_ratio(red, red) - 1.0).abs() < 0.01); - } -} diff --git a/crates/ui/src/utils/corner_solver.rs b/crates/ui/src/utils/corner_solver.rs deleted file mode 100644 index c49bccc445..0000000000 --- a/crates/ui/src/utils/corner_solver.rs +++ /dev/null @@ -1,61 +0,0 @@ -use gpui::Pixels; - -/// Calculates the child’s content-corner radius for a single nested level. -/// -/// child_content_radius = max(0, parent_radius - parent_border - parent_padding + self_border) -/// -/// - parent_radius: outer corner radius of the parent element -/// - parent_border: border width of the parent element -/// - parent_padding: padding of the parent element -/// - self_border: border width of this child element (for content inset) -pub fn inner_corner_radius( - parent_radius: Pixels, - parent_border: Pixels, - parent_padding: Pixels, - self_border: Pixels, -) -> Pixels { - (parent_radius - parent_border - parent_padding + self_border).max(Pixels::ZERO) -} - -/// Solver for arbitrarily deep nested corner radii. -/// -/// Each nested level’s outer border-box radius is: -/// R₀ = max(0, root_radius - root_border - root_padding) -/// Rᵢ = max(0, Rᵢ₋₁ - childᵢ₋₁_border - childᵢ₋₁_padding) for i > 0 -pub struct CornerSolver { - root_radius: Pixels, - root_border: Pixels, - root_padding: Pixels, - children: Vec<(Pixels, Pixels)>, // (border, padding) -} - -impl CornerSolver { - pub fn new(root_radius: Pixels, root_border: Pixels, root_padding: Pixels) -> Self { - Self { - root_radius, - root_border, - root_padding, - children: Vec::new(), - } - } - - pub fn add_child(mut self, border: Pixels, padding: Pixels) -> Self { - self.children.push((border, padding)); - self - } - - pub fn corner_radius(&self, level: usize) -> Pixels { - if level == 0 { - return (self.root_radius - self.root_border - self.root_padding).max(Pixels::ZERO); - } - if level >= self.children.len() { - return Pixels::ZERO; - } - let mut r = (self.root_radius - self.root_border - self.root_padding).max(Pixels::ZERO); - for i in 0..level { - let (b, p) = self.children[i]; - r = (r - b - p).max(Pixels::ZERO); - } - r - } -} diff --git a/crates/ui/src/utils/format_distance.rs b/crates/ui/src/utils/format_distance.rs deleted file mode 100644 index 6ec497edee..0000000000 --- a/crates/ui/src/utils/format_distance.rs +++ /dev/null @@ -1,396 +0,0 @@ -// This won't be documented further as it is intended to be removed, or merged with the `time_format` crate. - -use chrono::{DateTime, Local, NaiveDateTime}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum DateTimeType { - Naive(NaiveDateTime), - Local(DateTime), -} - -impl DateTimeType { - /// Converts the [`DateTimeType`] to a [`NaiveDateTime`]. - /// - /// If the [`DateTimeType`] is already a [`NaiveDateTime`], it will be returned as is. - /// If the [`DateTimeType`] is a [`DateTime`], it will be converted to a [`NaiveDateTime`]. - pub fn to_naive(self) -> NaiveDateTime { - match self { - DateTimeType::Naive(naive) => naive, - DateTimeType::Local(local) => local.naive_local(), - } - } -} - -pub struct FormatDistance { - date: DateTimeType, - base_date: DateTimeType, - include_seconds: bool, - add_suffix: bool, - hide_prefix: bool, -} - -impl FormatDistance { - pub fn new(date: DateTimeType, base_date: DateTimeType) -> Self { - Self { - date, - base_date, - include_seconds: false, - add_suffix: false, - hide_prefix: false, - } - } - - pub fn from_now(date: DateTimeType) -> Self { - Self::new(date, DateTimeType::Local(Local::now())) - } - - pub fn include_seconds(mut self, include_seconds: bool) -> Self { - self.include_seconds = include_seconds; - self - } - - pub fn add_suffix(mut self, add_suffix: bool) -> Self { - self.add_suffix = add_suffix; - self - } - - pub fn hide_prefix(mut self, hide_prefix: bool) -> Self { - self.hide_prefix = hide_prefix; - self - } -} - -impl std::fmt::Display for FormatDistance { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - format_distance( - self.date, - self.base_date.to_naive(), - self.include_seconds, - self.add_suffix, - self.hide_prefix, - ) - ) - } -} -/// Calculates the distance in seconds between two [`NaiveDateTime`] objects. -/// It returns a signed integer denoting the difference. If `date` is earlier than `base_date`, the returned value will be negative. -/// -/// ## Arguments -/// -/// * `date` - A [NaiveDateTime`] object representing the date of interest -/// * `base_date` - A [NaiveDateTime`] object representing the base date against which the comparison is made -fn distance_in_seconds(date: NaiveDateTime, base_date: NaiveDateTime) -> i64 { - let duration = date.signed_duration_since(base_date); - -duration.num_seconds() -} - -/// Generates a string describing the time distance between two dates in a human-readable way. -fn distance_string( - distance: i64, - include_seconds: bool, - add_suffix: bool, - hide_prefix: bool, -) -> String { - let suffix = if distance < 0 { " from now" } else { " ago" }; - - let distance = distance.abs(); - - let minutes = distance / 60; - let hours = distance / 3_600; - let days = distance / 86_400; - let months = distance / 2_592_000; - - let string = if distance < 5 && include_seconds { - if hide_prefix { - "5 seconds" - } else { - "less than 5 seconds" - } - .to_string() - } else if distance < 10 && include_seconds { - if hide_prefix { - "10 seconds" - } else { - "less than 10 seconds" - } - .to_string() - } else if distance < 20 && include_seconds { - if hide_prefix { - "20 seconds" - } else { - "less than 20 seconds" - } - .to_string() - } else if distance < 40 && include_seconds { - "half a minute".to_string() - } else if distance < 60 && include_seconds { - if hide_prefix { - "a minute" - } else { - "less than a minute" - } - .to_string() - } else if distance < 90 && include_seconds { - "1 minute".to_string() - } else if distance < 30 { - if hide_prefix { - "a minute" - } else { - "less than a minute" - } - .to_string() - } else if distance < 90 { - "1 minute".to_string() - } else if distance < 2_700 { - format!("{} minutes", minutes) - } else if distance < 5_400 { - if hide_prefix { - "1 hour" - } else { - "about 1 hour" - } - .to_string() - } else if distance < 86_400 { - if hide_prefix { - format!("{} hours", hours) - } else { - format!("about {} hours", hours) - } - } else if distance < 172_800 { - "1 day".to_string() - } else if distance < 2_592_000 { - format!("{} days", days) - } else if distance < 5_184_000 { - if hide_prefix { - "1 month" - } else { - "about 1 month" - } - .to_string() - } else if distance < 7_776_000 { - if hide_prefix { - "2 months" - } else { - "about 2 months" - } - .to_string() - } else if distance < 31_540_000 { - format!("{} months", months) - } else if distance < 39_425_000 { - if hide_prefix { - "1 year" - } else { - "about 1 year" - } - .to_string() - } else if distance < 55_195_000 { - if hide_prefix { "1 year" } else { "over 1 year" }.to_string() - } else if distance < 63_080_000 { - if hide_prefix { - "2 years" - } else { - "almost 2 years" - } - .to_string() - } else { - let years = distance / 31_536_000; - let remaining_months = (distance % 31_536_000) / 2_592_000; - - if remaining_months < 3 { - if hide_prefix { - format!("{} years", years) - } else { - format!("about {} years", years) - } - } else if remaining_months < 9 { - if hide_prefix { - format!("{} years", years) - } else { - format!("over {} years", years) - } - } else if hide_prefix { - format!("{} years", years + 1) - } else { - format!("almost {} years", years + 1) - } - }; - - if add_suffix { - format!("{}{}", string, suffix) - } else { - string - } -} - -/// Get the time difference between two dates into a relative human readable string. -/// -/// For example, "less than a minute ago", "about 2 hours ago", "3 months from now", etc. -/// -/// Use [`format_distance_from_now`] to compare a NaiveDateTime against now. -pub fn format_distance( - date: DateTimeType, - base_date: NaiveDateTime, - include_seconds: bool, - add_suffix: bool, - hide_prefix: bool, -) -> String { - let distance = distance_in_seconds(date.to_naive(), base_date); - - distance_string(distance, include_seconds, add_suffix, hide_prefix) -} - -/// Get the time difference between a date and now as relative human readable string. -/// -/// For example, "less than a minute ago", "about 2 hours ago", "3 months from now", etc. -pub fn format_distance_from_now( - datetime: DateTimeType, - include_seconds: bool, - add_suffix: bool, - hide_prefix: bool, -) -> String { - let now = chrono::offset::Local::now().naive_local(); - - format_distance(datetime, now, include_seconds, add_suffix, hide_prefix) -} - -#[cfg(test)] -mod tests { - use super::*; - use chrono::NaiveDateTime; - - #[test] - fn test_format_distance() { - let date = DateTimeType::Naive( - #[allow(deprecated)] - NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"), - ); - let base_date = DateTimeType::Naive( - #[allow(deprecated)] - NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"), - ); - - assert_eq!( - "about 2 hours", - format_distance(date, base_date.to_naive(), false, false, false) - ); - } - - #[test] - fn test_format_distance_with_suffix() { - let date = DateTimeType::Naive( - #[allow(deprecated)] - NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"), - ); - let base_date = DateTimeType::Naive( - #[allow(deprecated)] - NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"), - ); - - assert_eq!( - "about 2 hours from now", - format_distance(date, base_date.to_naive(), false, true, false) - ); - } - - #[test] - fn test_format_distance_from_hms() { - let date = DateTimeType::Naive( - NaiveDateTime::parse_from_str("1969-07-20T11:22:33Z", "%Y-%m-%dT%H:%M:%SZ") - .expect("Invalid NaiveDateTime for date"), - ); - let base_date = DateTimeType::Naive( - NaiveDateTime::parse_from_str("2024-02-01T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ") - .expect("Invalid NaiveDateTime for base_date"), - ); - - assert_eq!( - "over 54 years ago", - format_distance(date, base_date.to_naive(), false, true, false) - ); - } - - #[test] - fn test_format_distance_string() { - assert_eq!( - distance_string(3, false, false, false), - "less than a minute" - ); - assert_eq!( - distance_string(7, false, false, false), - "less than a minute" - ); - assert_eq!( - distance_string(13, false, false, false), - "less than a minute" - ); - assert_eq!( - distance_string(21, false, false, false), - "less than a minute" - ); - assert_eq!(distance_string(45, false, false, false), "1 minute"); - assert_eq!(distance_string(61, false, false, false), "1 minute"); - assert_eq!(distance_string(1920, false, false, false), "32 minutes"); - assert_eq!(distance_string(3902, false, false, false), "about 1 hour"); - assert_eq!(distance_string(18002, false, false, false), "about 5 hours"); - assert_eq!(distance_string(86470, false, false, false), "1 day"); - assert_eq!(distance_string(345880, false, false, false), "4 days"); - assert_eq!( - distance_string(2764800, false, false, false), - "about 1 month" - ); - assert_eq!( - distance_string(5184000, false, false, false), - "about 2 months" - ); - assert_eq!(distance_string(10368000, false, false, false), "4 months"); - assert_eq!( - distance_string(34694000, false, false, false), - "about 1 year" - ); - assert_eq!( - distance_string(47310000, false, false, false), - "over 1 year" - ); - assert_eq!( - distance_string(61503000, false, false, false), - "almost 2 years" - ); - assert_eq!( - distance_string(160854000, false, false, false), - "about 5 years" - ); - assert_eq!( - distance_string(236550000, false, false, false), - "over 7 years" - ); - assert_eq!( - distance_string(249166000, false, false, false), - "almost 8 years" - ); - } - - #[test] - fn test_format_distance_string_include_seconds() { - assert_eq!( - distance_string(3, true, false, false), - "less than 5 seconds" - ); - assert_eq!( - distance_string(7, true, false, false), - "less than 10 seconds" - ); - assert_eq!( - distance_string(13, true, false, false), - "less than 20 seconds" - ); - assert_eq!(distance_string(21, true, false, false), "half a minute"); - assert_eq!( - distance_string(45, true, false, false), - "less than a minute" - ); - assert_eq!(distance_string(61, true, false, false), "1 minute"); - } -} diff --git a/crates/ui/src/utils/search_input.rs b/crates/ui/src/utils/search_input.rs deleted file mode 100644 index c677b203d5..0000000000 --- a/crates/ui/src/utils/search_input.rs +++ /dev/null @@ -1,20 +0,0 @@ -use gpui::{Pixels, px}; - -pub struct SearchInputWidth; - -impl SearchInputWidth { - /// The container size in which the input stops filling the whole width. - pub const THRESHOLD_WIDTH: Pixels = px(1200.0); - - /// The maximum width for the search input when the container is larger than the threshold. - pub const MAX_WIDTH: Pixels = px(1200.0); - - /// Calculates the actual width in pixels based on the container width. - pub fn calc_width(container_width: Pixels) -> Pixels { - if container_width < Self::THRESHOLD_WIDTH { - container_width - } else { - container_width.min(Self::MAX_WIDTH) - } - } -} diff --git a/crates/ui/src/utils/with_rem_size.rs b/crates/ui/src/utils/with_rem_size.rs deleted file mode 100644 index b9770b086c..0000000000 --- a/crates/ui/src/utils/with_rem_size.rs +++ /dev/null @@ -1,114 +0,0 @@ -use gpui::{ - AnyElement, App, Bounds, Div, DivFrameState, Element, ElementId, GlobalElementId, Hitbox, - InteractiveElement as _, IntoElement, LayoutId, ParentElement, Pixels, StyleRefinement, Styled, - Window, div, -}; - -/// An element that sets a particular rem size for its children. -pub struct WithRemSize { - div: Div, - rem_size: Pixels, -} - -impl WithRemSize { - /// Create a new [WithRemSize] element, which sets a - /// particular rem size for its children. - pub fn new(rem_size: impl Into) -> Self { - Self { - div: div(), - rem_size: rem_size.into(), - } - } - - /// Block the mouse from interacting with this element or any of its children - /// The fluent API equivalent to [`Interactivity::occlude_mouse`] - /// - /// [`Interactivity::occlude_mouse`]: gpui::Interactivity::occlude_mouse - pub fn occlude(mut self) -> Self { - self.div = self.div.occlude(); - self - } -} - -impl Styled for WithRemSize { - fn style(&mut self) -> &mut StyleRefinement { - self.div.style() - } -} - -impl ParentElement for WithRemSize { - fn extend(&mut self, elements: impl IntoIterator) { - self.div.extend(elements) - } -} - -impl Element for WithRemSize { - type RequestLayoutState = DivFrameState; - type PrepaintState = Option; - - fn id(&self) -> Option { - Element::id(&self.div) - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - Element::source_location(&self.div) - } - - fn request_layout( - &mut self, - id: Option<&GlobalElementId>, - inspector_id: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - window.with_rem_size(Some(self.rem_size), |window| { - self.div.request_layout(id, inspector_id, window, cx) - }) - } - - fn prepaint( - &mut self, - id: Option<&GlobalElementId>, - inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - window.with_rem_size(Some(self.rem_size), |window| { - self.div - .prepaint(id, inspector_id, bounds, request_layout, window, cx) - }) - } - - fn paint( - &mut self, - id: Option<&GlobalElementId>, - inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - prepaint: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - window.with_rem_size(Some(self.rem_size), |window| { - self.div.paint( - id, - inspector_id, - bounds, - request_layout, - prepaint, - window, - cx, - ) - }) - } -} - -impl IntoElement for WithRemSize { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} diff --git a/crates/ui_input/Cargo.toml b/crates/ui_input/Cargo.toml deleted file mode 100644 index 4e7b08241d..0000000000 --- a/crates/ui_input/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "ui_input" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/ui_input.rs" - -[dependencies] -component.workspace = true -editor.workspace = true -gpui.workspace = true -menu.workspace = true -settings.workspace = true -theme.workspace = true -ui.workspace = true - -[features] -default = [] diff --git a/crates/ui_input/LICENSE-GPL b/crates/ui_input/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/ui_input/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/ui_input/src/input_field.rs b/crates/ui_input/src/input_field.rs deleted file mode 100644 index 2bae8c172d..0000000000 --- a/crates/ui_input/src/input_field.rs +++ /dev/null @@ -1,259 +0,0 @@ -use component::{example_group, single_example}; -use editor::{Editor, EditorElement, EditorStyle}; -use gpui::{App, Entity, FocusHandle, Focusable, FontStyle, Hsla, Length, TextStyle}; -use settings::Settings; -use std::sync::Arc; -use theme::ThemeSettings; -use ui::prelude::*; - -pub struct InputFieldStyle { - text_color: Hsla, - background_color: Hsla, - border_color: Hsla, -} - -/// An Input Field component that can be used to create text fields like search inputs, form fields, etc. -/// -/// It wraps a single line [`Editor`] and allows for common field properties like labels, placeholders, icons, etc. -#[derive(RegisterComponent)] -pub struct InputField { - /// An optional label for the text field. - /// - /// Its position is determined by the [`FieldLabelLayout`]. - label: Option, - /// The size of the label text. - label_size: LabelSize, - /// The placeholder text for the text field. - placeholder: SharedString, - /// Exposes the underlying [`Entity`] to allow for customizing the editor beyond the provided API. - /// - /// This likely will only be public in the short term, ideally the API will be expanded to cover necessary use cases. - pub editor: Entity, - /// An optional icon that is displayed at the start of the text field. - /// - /// For example, a magnifying glass icon in a search field. - start_icon: Option, - /// Whether the text field is disabled. - disabled: bool, - /// The minimum width of for the input - min_width: Length, - /// The tab index for keyboard navigation order. - tab_index: Option, - /// Whether this field is a tab stop (can be focused via Tab key). - tab_stop: bool, -} - -impl Focusable for InputField { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.editor.focus_handle(cx) - } -} - -impl InputField { - pub fn new(window: &mut Window, cx: &mut App, placeholder: impl Into) -> Self { - let placeholder_text = placeholder.into(); - - let editor = cx.new(|cx| { - let mut input = Editor::single_line(window, cx); - input.set_placeholder_text(&placeholder_text, window, cx); - input - }); - - Self { - label: None, - label_size: LabelSize::Small, - placeholder: placeholder_text, - editor, - start_icon: None, - disabled: false, - min_width: px(192.).into(), - tab_index: None, - tab_stop: true, - } - } - - pub fn start_icon(mut self, icon: IconName) -> Self { - self.start_icon = Some(icon); - self - } - - pub fn label(mut self, label: impl Into) -> Self { - self.label = Some(label.into()); - self - } - - pub fn label_size(mut self, size: LabelSize) -> Self { - self.label_size = size; - self - } - - pub fn label_min_width(mut self, width: impl Into) -> Self { - self.min_width = width.into(); - self - } - - pub fn tab_index(mut self, index: isize) -> Self { - self.tab_index = Some(index); - self - } - - pub fn tab_stop(mut self, tab_stop: bool) -> Self { - self.tab_stop = tab_stop; - self - } - - pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context) { - self.disabled = disabled; - self.editor - .update(cx, |editor, _| editor.set_read_only(disabled)) - } - - pub fn is_empty(&self, cx: &App) -> bool { - self.editor().read(cx).text(cx).trim().is_empty() - } - - pub fn editor(&self) -> &Entity { - &self.editor - } - - pub fn text(&self, cx: &App) -> String { - self.editor().read(cx).text(cx) - } - - pub fn clear(&self, window: &mut Window, cx: &mut App) { - self.editor() - .update(cx, |editor, cx| editor.clear(window, cx)) - } - - pub fn set_text(&self, text: impl Into>, window: &mut Window, cx: &mut App) { - self.editor() - .update(cx, |editor, cx| editor.set_text(text, window, cx)) - } -} - -impl Render for InputField { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let editor = self.editor.clone(); - let settings = ThemeSettings::get_global(cx); - let theme_color = cx.theme().colors(); - - let mut style = InputFieldStyle { - text_color: theme_color.text, - background_color: theme_color.editor_background, - border_color: theme_color.border_variant, - }; - - if self.disabled { - style.text_color = theme_color.text_disabled; - style.background_color = theme_color.editor_background; - style.border_color = theme_color.border_disabled; - } - - // if self.error_message.is_some() { - // style.text_color = cx.theme().status().error; - // style.border_color = cx.theme().status().error_border - // } - - let text_style = TextStyle { - font_family: settings.ui_font.family.clone(), - font_features: settings.ui_font.features.clone(), - font_size: rems(0.875).into(), - font_weight: settings.buffer_font.weight, - font_style: FontStyle::Normal, - line_height: relative(1.2), - color: style.text_color, - ..Default::default() - }; - - let editor_style = EditorStyle { - background: theme_color.ghost_element_background, - local_player: cx.theme().players().local(), - syntax: cx.theme().syntax().clone(), - text: text_style, - ..Default::default() - }; - - let focus_handle = self.editor.focus_handle(cx); - - let configured_handle = if let Some(tab_index) = self.tab_index { - focus_handle.tab_index(tab_index).tab_stop(self.tab_stop) - } else if !self.tab_stop { - focus_handle.tab_stop(false) - } else { - focus_handle - }; - - v_flex() - .id(self.placeholder.clone()) - .w_full() - .gap_1() - .when_some(self.label.clone(), |this, label| { - this.child( - Label::new(label) - .size(self.label_size) - .color(if self.disabled { - Color::Disabled - } else { - Color::Default - }), - ) - }) - .child( - h_flex() - .track_focus(&configured_handle) - .min_w(self.min_width) - .min_h_8() - .w_full() - .px_2() - .py_1p5() - .flex_grow() - .text_color(style.text_color) - .rounded_md() - .bg(style.background_color) - .border_1() - .border_color(style.border_color) - .when( - editor.focus_handle(cx).contains_focused(window, cx), - |this| this.border_color(theme_color.border_focused), - ) - .when_some(self.start_icon, |this, icon| { - this.gap_1() - .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)) - }) - .child(EditorElement::new(&self.editor, editor_style)), - ) - } -} - -impl Component for InputField { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn preview(window: &mut Window, cx: &mut App) -> Option { - let input_small = - cx.new(|cx| InputField::new(window, cx, "placeholder").label("Small Label")); - - let input_regular = cx.new(|cx| { - InputField::new(window, cx, "placeholder") - .label("Regular Label") - .label_size(LabelSize::Default) - }); - - Some( - v_flex() - .gap_6() - .children(vec![example_group(vec![ - single_example( - "Small Label (Default)", - div().child(input_small).into_any_element(), - ), - single_example( - "Regular Label", - div().child(input_regular).into_any_element(), - ), - ])]) - .into_any_element(), - ) - } -} diff --git a/crates/ui_input/src/number_field.rs b/crates/ui_input/src/number_field.rs deleted file mode 100644 index ee5c57b43b..0000000000 --- a/crates/ui_input/src/number_field.rs +++ /dev/null @@ -1,577 +0,0 @@ -use std::{ - fmt::Display, - num::{NonZero, NonZeroU32, NonZeroU64}, - rc::Rc, - str::FromStr, -}; - -use editor::{Editor, EditorStyle}; -use gpui::{ClickEvent, Entity, FocusHandle, Focusable, FontWeight, Modifiers}; - -use settings::{CenteredPaddingSettings, CodeFade, DelayMs, InactiveOpacity, MinimumContrast}; -use ui::prelude::*; - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] -pub enum NumberFieldMode { - #[default] - Read, - Edit, -} - -pub trait NumberFieldType: Display + Copy + Clone + Sized + PartialOrd + FromStr + 'static { - fn default_format(value: &Self) -> String { - format!("{}", value) - } - fn default_step() -> Self; - fn large_step() -> Self; - fn small_step() -> Self; - fn min_value() -> Self; - fn max_value() -> Self; - fn saturating_add(self, rhs: Self) -> Self; - fn saturating_sub(self, rhs: Self) -> Self; -} - -macro_rules! impl_newtype_numeric_stepper_float { - ($type:ident, $default:expr, $large:expr, $small:expr, $min:expr, $max:expr) => { - impl NumberFieldType for $type { - fn default_step() -> Self { - $default.into() - } - - fn large_step() -> Self { - $large.into() - } - - fn small_step() -> Self { - $small.into() - } - - fn min_value() -> Self { - $min.into() - } - - fn max_value() -> Self { - $max.into() - } - - fn saturating_add(self, rhs: Self) -> Self { - $type((self.0 + rhs.0).min(Self::max_value().0)) - } - - fn saturating_sub(self, rhs: Self) -> Self { - $type((self.0 - rhs.0).max(Self::min_value().0)) - } - } - }; -} - -macro_rules! impl_newtype_numeric_stepper_int { - ($type:ident, $default:expr, $large:expr, $small:expr, $min:expr, $max:expr) => { - impl NumberFieldType for $type { - fn default_step() -> Self { - $default.into() - } - - fn large_step() -> Self { - $large.into() - } - - fn small_step() -> Self { - $small.into() - } - - fn min_value() -> Self { - $min.into() - } - - fn max_value() -> Self { - $max.into() - } - - fn saturating_add(self, rhs: Self) -> Self { - $type(self.0.saturating_add(rhs.0).min(Self::max_value().0)) - } - - fn saturating_sub(self, rhs: Self) -> Self { - $type(self.0.saturating_sub(rhs.0).max(Self::min_value().0)) - } - } - }; -} - -#[rustfmt::skip] -impl_newtype_numeric_stepper_float!(FontWeight, 50., 100., 10., FontWeight::THIN, FontWeight::BLACK); -impl_newtype_numeric_stepper_float!(CodeFade, 0.1, 0.2, 0.05, 0.0, 0.9); -impl_newtype_numeric_stepper_float!(InactiveOpacity, 0.1, 0.2, 0.05, 0.0, 1.0); -impl_newtype_numeric_stepper_float!(MinimumContrast, 1., 10., 0.5, 0.0, 106.0); -impl_newtype_numeric_stepper_int!(DelayMs, 100, 500, 10, 0, 2000); -impl_newtype_numeric_stepper_float!( - CenteredPaddingSettings, - 0.05, - 0.2, - 0.1, - CenteredPaddingSettings::MIN_PADDING, - CenteredPaddingSettings::MAX_PADDING -); - -macro_rules! impl_numeric_stepper_int { - ($type:ident) => { - impl NumberFieldType for $type { - fn default_step() -> Self { - 1 - } - - fn large_step() -> Self { - 10 - } - - fn small_step() -> Self { - 1 - } - - fn min_value() -> Self { - <$type>::MIN - } - - fn max_value() -> Self { - <$type>::MAX - } - - fn saturating_add(self, rhs: Self) -> Self { - self.saturating_add(rhs) - } - - fn saturating_sub(self, rhs: Self) -> Self { - self.saturating_sub(rhs) - } - } - }; -} - -macro_rules! impl_numeric_stepper_nonzero_int { - ($nonzero:ty, $inner:ty) => { - impl NumberFieldType for $nonzero { - fn default_step() -> Self { - <$nonzero>::new(1).unwrap() - } - - fn large_step() -> Self { - <$nonzero>::new(10).unwrap() - } - - fn small_step() -> Self { - <$nonzero>::new(1).unwrap() - } - - fn min_value() -> Self { - <$nonzero>::MIN - } - - fn max_value() -> Self { - <$nonzero>::MAX - } - - fn saturating_add(self, rhs: Self) -> Self { - let result = self.get().saturating_add(rhs.get()); - <$nonzero>::new(result.max(1)).unwrap() - } - - fn saturating_sub(self, rhs: Self) -> Self { - let result = self.get().saturating_sub(rhs.get()).max(1); - <$nonzero>::new(result).unwrap() - } - } - }; -} - -macro_rules! impl_numeric_stepper_float { - ($type:ident) => { - impl NumberFieldType for $type { - fn default_format(value: &Self) -> String { - format!("{:.2}", value) - } - - fn default_step() -> Self { - 1.0 - } - - fn large_step() -> Self { - 10.0 - } - - fn small_step() -> Self { - 0.1 - } - - fn min_value() -> Self { - <$type>::MIN - } - - fn max_value() -> Self { - <$type>::MAX - } - - fn saturating_add(self, rhs: Self) -> Self { - (self + rhs).clamp(Self::min_value(), Self::max_value()) - } - - fn saturating_sub(self, rhs: Self) -> Self { - (self - rhs).clamp(Self::min_value(), Self::max_value()) - } - } - }; -} - -impl_numeric_stepper_float!(f32); -impl_numeric_stepper_float!(f64); -impl_numeric_stepper_int!(isize); -impl_numeric_stepper_int!(usize); -impl_numeric_stepper_int!(i32); -impl_numeric_stepper_int!(u32); -impl_numeric_stepper_int!(i64); -impl_numeric_stepper_int!(u64); - -impl_numeric_stepper_nonzero_int!(NonZeroU32, u32); -impl_numeric_stepper_nonzero_int!(NonZeroU64, u64); -impl_numeric_stepper_nonzero_int!(NonZero, usize); - -#[derive(RegisterComponent)] -pub struct NumberField { - id: ElementId, - value: T, - focus_handle: FocusHandle, - mode: Entity, - format: Box String>, - large_step: T, - small_step: T, - step: T, - min_value: T, - max_value: T, - on_reset: Option>, - on_change: Rc, - tab_index: Option, -} - -impl NumberField { - pub fn new(id: impl Into, value: T, window: &mut Window, cx: &mut App) -> Self { - let id = id.into(); - - let (mode, focus_handle) = window.with_id(id.clone(), |window| { - let mode = window.use_state(cx, |_, _| NumberFieldMode::default()); - let focus_handle = window.use_state(cx, |_, cx| cx.focus_handle()); - (mode, focus_handle) - }); - - Self { - id, - mode, - value, - focus_handle: focus_handle.read(cx).clone(), - format: Box::new(T::default_format), - large_step: T::large_step(), - step: T::default_step(), - small_step: T::small_step(), - min_value: T::min_value(), - max_value: T::max_value(), - on_reset: None, - on_change: Rc::new(|_, _, _| {}), - tab_index: None, - } - } - - pub fn format(mut self, format: impl FnOnce(&T) -> String + 'static) -> Self { - self.format = Box::new(format); - self - } - - pub fn small_step(mut self, step: T) -> Self { - self.small_step = step; - self - } - - pub fn normal_step(mut self, step: T) -> Self { - self.step = step; - self - } - - pub fn large_step(mut self, step: T) -> Self { - self.large_step = step; - self - } - - pub fn min(mut self, min: T) -> Self { - self.min_value = min; - self - } - - pub fn max(mut self, max: T) -> Self { - self.max_value = max; - self - } - - pub fn on_reset( - mut self, - on_reset: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_reset = Some(Box::new(on_reset)); - self - } - - pub fn tab_index(mut self, tab_index: isize) -> Self { - self.tab_index = Some(tab_index); - self - } - - pub fn on_change(mut self, on_change: impl Fn(&T, &mut Window, &mut App) + 'static) -> Self { - self.on_change = Rc::new(on_change); - self - } -} - -impl IntoElement for NumberField { - type Element = gpui::Component; - - fn into_element(self) -> Self::Element { - gpui::Component::new(self) - } -} - -impl RenderOnce for NumberField { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let mut tab_index = self.tab_index; - - let get_step = { - let large_step = self.large_step; - let step = self.step; - let small_step = self.small_step; - move |modifiers: Modifiers| -> T { - if modifiers.shift { - large_step - } else if modifiers.alt { - small_step - } else { - step - } - } - }; - - let bg_color = cx.theme().colors().surface_background; - let hover_bg_color = cx.theme().colors().element_hover; - - let border_color = cx.theme().colors().border_variant; - let focus_border_color = cx.theme().colors().border_focused; - - let base_button = |icon: IconName| { - h_flex() - .cursor_pointer() - .p_1p5() - .size_full() - .justify_center() - .overflow_hidden() - .border_1() - .border_color(border_color) - .bg(bg_color) - .hover(|s| s.bg(hover_bg_color)) - .focus_visible(|s| s.border_color(focus_border_color).bg(hover_bg_color)) - .child(Icon::new(icon).size(IconSize::Small)) - }; - - h_flex() - .id(self.id.clone()) - .track_focus(&self.focus_handle) - .gap_1() - .when_some(self.on_reset, |this, on_reset| { - this.child( - IconButton::new("reset", IconName::RotateCcw) - .icon_size(IconSize::Small) - .when_some(tab_index.as_mut(), |this, tab_index| { - *tab_index += 1; - this.tab_index(*tab_index - 1) - }) - .on_click(on_reset), - ) - }) - .child( - h_flex() - .map(|decrement| { - let decrement_handler = { - let value = self.value; - let on_change = self.on_change.clone(); - let min = self.min_value; - move |click: &ClickEvent, window: &mut Window, cx: &mut App| { - let step = get_step(click.modifiers()); - let new_value = value.saturating_sub(step); - let new_value = if new_value < min { min } else { new_value }; - on_change(&new_value, window, cx); - } - }; - - decrement.child( - base_button(IconName::Dash) - .id("decrement_button") - .rounded_tl_sm() - .rounded_bl_sm() - .tab_index( - tab_index - .as_mut() - .map(|tab_index| { - *tab_index += 1; - *tab_index - 1 - }) - .unwrap_or(0), - ) - .on_click(decrement_handler), - ) - }) - .child( - h_flex() - .min_w_16() - .size_full() - .border_y_1() - .border_color(border_color) - .bg(bg_color) - .in_focus(|this| this.border_color(focus_border_color)) - .child(match *self.mode.read(cx) { - NumberFieldMode::Read => h_flex() - .px_1() - .flex_1() - .justify_center() - .child(Label::new((self.format)(&self.value))) - .into_any_element(), - // Edit mode is disabled until we implement center text alignment for editor - // mode.write(cx, NumberFieldMode::Edit); - // - // When we get to making Edit mode work, we shouldn't even focus the decrement/increment buttons. - // Focus should go instead straight to the editor, avoiding any double-step focus. - // In this world, the buttons become a mouse-only interaction, given users should be able - // to do everything they'd do with the buttons straight in the editor anyway. - NumberFieldMode::Edit => h_flex() - .flex_1() - .child(window.use_state(cx, { - |window, cx| { - let previous_focus_handle = window.focused(cx); - let mut editor = Editor::single_line(window, cx); - let mut style = EditorStyle::default(); - style.text.text_align = gpui::TextAlign::Right; - editor.set_style(style, window, cx); - - editor.set_text(format!("{}", self.value), window, cx); - cx.on_focus_out(&editor.focus_handle(cx), window, { - let mode = self.mode.clone(); - let min = self.min_value; - let max = self.max_value; - let on_change = self.on_change.clone(); - move |this, _, window, cx| { - if let Ok(new_value) = - this.text(cx).parse::() - { - let new_value = if new_value < min { - min - } else if new_value > max { - max - } else { - new_value - }; - - if let Some(previous) = - previous_focus_handle.as_ref() - { - window.focus(previous); - } - on_change(&new_value, window, cx); - }; - mode.write(cx, NumberFieldMode::Read); - } - }) - .detach(); - - window.focus(&editor.focus_handle(cx)); - - editor - } - })) - .on_action::({ - move |_, window, _| { - window.blur(); - } - }) - .into_any_element(), - }), - ) - .map(|increment| { - let increment_handler = { - let value = self.value; - let on_change = self.on_change.clone(); - let max = self.max_value; - move |click: &ClickEvent, window: &mut Window, cx: &mut App| { - let step = get_step(click.modifiers()); - let new_value = value.saturating_add(step); - let new_value = if new_value > max { max } else { new_value }; - on_change(&new_value, window, cx); - } - }; - - increment.child( - base_button(IconName::Plus) - .id("increment_button") - .rounded_tr_sm() - .rounded_br_sm() - .tab_index( - tab_index - .as_mut() - .map(|tab_index| { - *tab_index += 1; - *tab_index - 1 - }) - .unwrap_or(0), - ) - .on_click(increment_handler), - ) - }), - ) - } -} - -impl Component for NumberField { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn name() -> &'static str { - "Number Field" - } - - fn sort_name() -> &'static str { - Self::name() - } - - fn description() -> Option<&'static str> { - Some("A numeric input element with increment and decrement buttons.") - } - - fn preview(window: &mut Window, cx: &mut App) -> Option { - let stepper_example = window.use_state(cx, |_, _| 100.0); - - Some( - v_flex() - .gap_6() - .children(vec![single_example( - "Default Numeric Stepper", - NumberField::new( - "numeric-stepper-component-preview", - *stepper_example.read(cx), - window, - cx, - ) - .on_change({ - let stepper_example = stepper_example.clone(); - move |value, _, cx| stepper_example.write(cx, *value) - }) - .min(1.0) - .max(100.0) - .into_any_element(), - )]) - .into_any_element(), - ) - } -} diff --git a/crates/ui_input/src/ui_input.rs b/crates/ui_input/src/ui_input.rs deleted file mode 100644 index ddc0e659a2..0000000000 --- a/crates/ui_input/src/ui_input.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! This crate provides UI components that can be used for form-like scenarios, such as a input and number field. -//! -//! It can't be located in the `ui` crate because it depends on `editor`. -//! -mod input_field; -mod number_field; - -pub use input_field::*; -pub use number_field::*; diff --git a/crates/ui_macros/Cargo.toml b/crates/ui_macros/Cargo.toml deleted file mode 100644 index 74bd2186a7..0000000000 --- a/crates/ui_macros/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "ui_macros" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/ui_macros.rs" -proc-macro = true - -[dependencies] -quote.workspace = true -syn.workspace = true - -[dev-dependencies] -component.workspace = true -ui.workspace = true diff --git a/crates/ui_macros/LICENSE-GPL b/crates/ui_macros/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/ui_macros/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/ui_macros/src/derive_register_component.rs b/crates/ui_macros/src/derive_register_component.rs deleted file mode 100644 index 64ab132cc0..0000000000 --- a/crates/ui_macros/src/derive_register_component.rs +++ /dev/null @@ -1,29 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{DeriveInput, parse_macro_input}; - -pub fn derive_register_component(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - - let name = input.ident; - let register_fn_name = syn::Ident::new( - &format!("__component_registry_internal_register_{}", name), - name.span(), - ); - let expanded = quote! { - const _: () = { - struct AssertComponent(::std::marker::PhantomData); - let _ = AssertComponent::<#name>(::std::marker::PhantomData); - }; - - #[allow(non_snake_case)] - fn #register_fn_name() { - component::register_component::<#name>(); - } - - component::__private::inventory::submit! { - component::ComponentFn::new(#register_fn_name) - } - }; - expanded.into() -} diff --git a/crates/ui_macros/src/dynamic_spacing.rs b/crates/ui_macros/src/dynamic_spacing.rs deleted file mode 100644 index 15ba3e241e..0000000000 --- a/crates/ui_macros/src/dynamic_spacing.rs +++ /dev/null @@ -1,167 +0,0 @@ -use proc_macro::TokenStream; -use quote::{format_ident, quote}; -use syn::{ - LitInt, Token, parse::Parse, parse::ParseStream, parse_macro_input, punctuated::Punctuated, -}; - -struct DynamicSpacingInput { - values: Punctuated, -} - -// The input for the derive macro is a list of values. -// -// When a single value is provided, the standard spacing formula is -// used to derive the of spacing values. -// -// When a tuple of three values is provided, the values are used as -// the spacing values directly. -enum DynamicSpacingValue { - Single(LitInt), - Tuple(LitInt, LitInt, LitInt), -} - -impl Parse for DynamicSpacingInput { - fn parse(input: ParseStream) -> syn::Result { - Ok(DynamicSpacingInput { - values: input.parse_terminated(DynamicSpacingValue::parse, Token![,])?, - }) - } -} - -impl Parse for DynamicSpacingValue { - fn parse(input: ParseStream) -> syn::Result { - if input.peek(syn::token::Paren) { - let content; - syn::parenthesized!(content in input); - let a: LitInt = content.parse()?; - content.parse::()?; - let b: LitInt = content.parse()?; - content.parse::()?; - let c: LitInt = content.parse()?; - Ok(DynamicSpacingValue::Tuple(a, b, c)) - } else { - Ok(DynamicSpacingValue::Single(input.parse()?)) - } - } -} - -/// Derives the spacing method for the `DynamicSpacing` enum. -pub fn derive_spacing(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DynamicSpacingInput); - - let spacing_ratios: Vec<_> = input - .values - .iter() - .map(|v| { - let variant = match v { - DynamicSpacingValue::Single(n) => { - format_ident!("Base{:02}", n.base10_parse::().unwrap()) - } - DynamicSpacingValue::Tuple(_, b, _) => { - format_ident!("Base{:02}", b.base10_parse::().unwrap()) - } - }; - match v { - DynamicSpacingValue::Single(n) => { - let n = n.base10_parse::().unwrap(); - quote! { - DynamicSpacing::#variant => match ThemeSettings::get_global(cx).ui_density { - ::theme::UiDensity::Compact => (#n - 4.0).max(0.0) / BASE_REM_SIZE_IN_PX, - ::theme::UiDensity::Default => #n / BASE_REM_SIZE_IN_PX, - ::theme::UiDensity::Comfortable => (#n + 4.0) / BASE_REM_SIZE_IN_PX, - } - } - } - DynamicSpacingValue::Tuple(a, b, c) => { - let a = a.base10_parse::().unwrap(); - let b = b.base10_parse::().unwrap(); - let c = c.base10_parse::().unwrap(); - quote! { - DynamicSpacing::#variant => match ThemeSettings::get_global(cx).ui_density { - ::theme::UiDensity::Compact => #a / BASE_REM_SIZE_IN_PX, - ::theme::UiDensity::Default => #b / BASE_REM_SIZE_IN_PX, - ::theme::UiDensity::Comfortable => #c / BASE_REM_SIZE_IN_PX, - } - } - } - } - }) - .collect(); - - let (variant_names, doc_strings): (Vec<_>, Vec<_>) = input - .values - .iter() - .map(|v| { - let variant = match v { - DynamicSpacingValue::Single(n) => { - format_ident!("Base{:02}", n.base10_parse::().unwrap()) - } - DynamicSpacingValue::Tuple(_, b, _) => { - format_ident!("Base{:02}", b.base10_parse::().unwrap()) - } - }; - let doc_string = match v { - DynamicSpacingValue::Single(n) => { - let n = n.base10_parse::().unwrap(); - let compact = (n - 4.0).max(0.0); - let comfortable = n + 4.0; - format!( - "`{}px`|`{}px`|`{}px (@16px/rem)` - Scales with the user's rem size.", - compact, n, comfortable - ) - } - DynamicSpacingValue::Tuple(a, b, c) => { - let a = a.base10_parse::().unwrap(); - let b = b.base10_parse::().unwrap(); - let c = c.base10_parse::().unwrap(); - format!( - "`{}px`|`{}px`|`{}px (@16px/rem)` - Scales with the user's rem size.", - a, b, c - ) - } - }; - (quote!(#variant), quote!(#doc_string)) - }) - .unzip(); - - let expanded = quote! { - /// A dynamic spacing system that adjusts spacing based on - /// [UiDensity]. - /// - /// The number following "Base" refers to the base pixel size - /// at the default rem size and spacing settings. - /// - /// When possible, [DynamicSpacing] should be used over manual - /// or built-in spacing values in places dynamic spacing is needed. - #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] - pub enum DynamicSpacing { - #( - #[doc = #doc_strings] - #variant_names, - )* - } - - impl DynamicSpacing { - /// Returns the spacing ratio, should only be used internally. - fn spacing_ratio(&self, cx: &App) -> f32 { - const BASE_REM_SIZE_IN_PX: f32 = 16.0; - match self { - #(#spacing_ratios,)* - } - } - - /// Returns the spacing value in rems. - pub fn rems(&self, cx: &App) -> Rems { - rems(self.spacing_ratio(cx)) - } - - /// Returns the spacing value in pixels. - pub fn px(&self, cx: &App) -> Pixels { - let ui_font_size_f32: f32 = ThemeSettings::get_global(cx).ui_font_size(cx).into(); - px(ui_font_size_f32 * self.spacing_ratio(cx)) - } - } - }; - - TokenStream::from(expanded) -} diff --git a/crates/ui_macros/src/ui_macros.rs b/crates/ui_macros/src/ui_macros.rs deleted file mode 100644 index ce48a21c11..0000000000 --- a/crates/ui_macros/src/ui_macros.rs +++ /dev/null @@ -1,37 +0,0 @@ -mod derive_register_component; -mod dynamic_spacing; - -use proc_macro::TokenStream; - -/// Generates the DynamicSpacing enum used for density-aware spacing in the UI. -#[proc_macro] -pub fn derive_dynamic_spacing(input: TokenStream) -> TokenStream { - dynamic_spacing::derive_spacing(input) -} - -/// Registers components that implement the `Component` trait. -/// -/// This proc macro is used to automatically register structs that implement -/// the `Component` trait with the [`component::ComponentRegistry`]. -/// -/// If the component trait is not implemented, it will generate a compile-time error. -/// -/// # Example -/// -/// ``` -/// use ui::Component; -/// use ui_macros::RegisterComponent; -/// -/// #[derive(RegisterComponent)] -/// struct MyComponent; -/// -/// impl Component for MyComponent { -/// // Component implementation -/// } -/// ``` -/// -/// This example will add MyComponent to the ComponentRegistry. -#[proc_macro_derive(RegisterComponent)] -pub fn derive_register_component(input: TokenStream) -> TokenStream { - derive_register_component::derive_register_component(input) -} diff --git a/crates/ui_prompt/Cargo.toml b/crates/ui_prompt/Cargo.toml deleted file mode 100644 index 55a9828843..0000000000 --- a/crates/ui_prompt/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "ui_prompt" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/ui_prompt.rs" - -[features] -default = [] - -[dependencies] -gpui.workspace = true -markdown.workspace = true -menu.workspace = true -settings.workspace = true -theme.workspace = true -ui.workspace = true -workspace.workspace = true diff --git a/crates/ui_prompt/LICENSE-GPL b/crates/ui_prompt/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/ui_prompt/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/ui_prompt/src/ui_prompt.rs b/crates/ui_prompt/src/ui_prompt.rs deleted file mode 100644 index 3b2716fd92..0000000000 --- a/crates/ui_prompt/src/ui_prompt.rs +++ /dev/null @@ -1,205 +0,0 @@ -use gpui::{ - App, Entity, EventEmitter, FocusHandle, Focusable, PromptButton, PromptHandle, PromptLevel, - PromptResponse, RenderablePromptHandle, SharedString, TextStyleRefinement, Window, div, - prelude::*, -}; -use markdown::{Markdown, MarkdownElement, MarkdownStyle}; -use settings::{Settings, SettingsStore}; -use theme::ThemeSettings; -use ui::{FluentBuilder, TintColor, prelude::*}; -use workspace::WorkspaceSettings; - -pub fn init(cx: &mut App) { - process_settings(cx); - - cx.observe_global::(process_settings) - .detach(); -} - -fn process_settings(cx: &mut App) { - let settings = WorkspaceSettings::get_global(cx); - if settings.use_system_prompts && cfg!(not(any(target_os = "linux", target_os = "freebsd"))) { - cx.reset_prompt_builder(); - } else { - cx.set_prompt_builder(zed_prompt_renderer); - } -} - -/// Use this function in conjunction with [App::set_prompt_builder] to force -/// GPUI to use the internal prompt system. -fn zed_prompt_renderer( - level: PromptLevel, - message: &str, - detail: Option<&str>, - actions: &[PromptButton], - handle: PromptHandle, - window: &mut Window, - cx: &mut App, -) -> RenderablePromptHandle { - let renderer = cx.new({ - |cx| ZedPromptRenderer { - _level: level, - message: cx.new(|cx| Markdown::new(SharedString::new(message), None, None, cx)), - actions: actions.iter().map(|a| a.label().to_string()).collect(), - focus: cx.focus_handle(), - active_action_id: 0, - detail: detail - .filter(|text| !text.is_empty()) - .map(|text| cx.new(|cx| Markdown::new(SharedString::new(text), None, None, cx))), - } - }); - - handle.with_view(renderer, window, cx) -} - -pub struct ZedPromptRenderer { - _level: PromptLevel, - message: Entity, - actions: Vec, - focus: FocusHandle, - active_action_id: usize, - detail: Option>, -} - -impl ZedPromptRenderer { - fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context) { - cx.emit(PromptResponse(self.active_action_id)); - } - - fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context) { - if let Some(ix) = self.actions.iter().position(|a| a == "Cancel") { - cx.emit(PromptResponse(ix)); - } - } - - fn select_first( - &mut self, - _: &menu::SelectFirst, - _window: &mut Window, - cx: &mut Context, - ) { - self.active_action_id = self.actions.len().saturating_sub(1); - cx.notify(); - } - - fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context) { - self.active_action_id = 0; - cx.notify(); - } - - fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context) { - self.active_action_id = (self.active_action_id + 1) % self.actions.len(); - cx.notify(); - } - - fn select_previous( - &mut self, - _: &menu::SelectPrevious, - _window: &mut Window, - cx: &mut Context, - ) { - if self.active_action_id > 0 { - self.active_action_id -= 1; - } else { - self.active_action_id = self.actions.len().saturating_sub(1); - } - cx.notify(); - } -} - -impl Render for ZedPromptRenderer { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let settings = ThemeSettings::get_global(cx); - - let dialog = v_flex() - .key_context("Prompt") - .cursor_default() - .track_focus(&self.focus) - .on_action(cx.listener(Self::confirm)) - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(Self::select_next)) - .on_action(cx.listener(Self::select_previous)) - .on_action(cx.listener(Self::select_first)) - .on_action(cx.listener(Self::select_last)) - .w_80() - .p_4() - .gap_4() - .elevation_3(cx) - .overflow_hidden() - .font_family(settings.ui_font.family.clone()) - .child(div().w_full().child(MarkdownElement::new( - self.message.clone(), - markdown_style(true, window, cx), - ))) - .children(self.detail.clone().map(|detail| { - div().w_full().text_xs().child(MarkdownElement::new( - detail, - markdown_style(false, window, cx), - )) - })) - .child( - v_flex() - .gap_1() - .children(self.actions.iter().enumerate().map(|(ix, action)| { - Button::new(ix, action.clone()) - .full_width() - .style(ButtonStyle::Outlined) - .when(ix == self.active_action_id, |s| { - s.style(ButtonStyle::Tinted(TintColor::Accent)) - }) - .tab_index(ix as isize) - .on_click(cx.listener(move |_, _, _window, cx| { - cx.emit(PromptResponse(ix)); - })) - })), - ); - - div() - .size_full() - .occlude() - .bg(gpui::black().opacity(0.2)) - .child( - v_flex() - .size_full() - .absolute() - .top_0() - .left_0() - .items_center() - .justify_center() - .child(dialog), - ) - } -} - -fn markdown_style(main_message: bool, window: &Window, cx: &App) -> MarkdownStyle { - let mut base_text_style = window.text_style(); - let settings = ThemeSettings::get_global(cx); - let font_size = settings.ui_font_size(cx).into(); - - let color = if main_message { - Color::Default.color(cx) - } else { - Color::Muted.color(cx) - }; - - base_text_style.refine(&TextStyleRefinement { - font_family: Some(settings.ui_font.family.clone()), - font_size: Some(font_size), - color: Some(color), - ..Default::default() - }); - - MarkdownStyle { - base_text_style, - selection_background_color: cx.theme().colors().element_selection_background, - ..Default::default() - } -} - -impl EventEmitter for ZedPromptRenderer {} - -impl Focusable for ZedPromptRenderer { - fn focus_handle(&self, _: &crate::App) -> FocusHandle { - self.focus.clone() - } -} diff --git a/crates/util/src/shell_env.rs b/crates/util/src/shell_env.rs deleted file mode 100644 index c41a28b469..0000000000 --- a/crates/util/src/shell_env.rs +++ /dev/null @@ -1,221 +0,0 @@ -use std::path::Path; - -use anyhow::{Context as _, Result}; -use collections::HashMap; - -use crate::shell::ShellKind; - -pub fn print_env() { - let env_vars: HashMap = std::env::vars().collect(); - let json = serde_json::to_string_pretty(&env_vars).unwrap_or_else(|err| { - eprintln!("Error serializing environment variables: {}", err); - std::process::exit(1); - }); - println!("{}", json); -} - -/// Capture all environment variables from the login shell in the given directory. -pub async fn capture( - shell_path: impl AsRef, - args: &[String], - directory: impl AsRef, -) -> Result> { - #[cfg(windows)] - return capture_windows(shell_path.as_ref(), args, directory.as_ref()).await; - #[cfg(unix)] - return capture_unix(shell_path.as_ref(), args, directory.as_ref()).await; -} - -#[cfg(unix)] -async fn capture_unix( - shell_path: &Path, - args: &[String], - directory: &Path, -) -> Result> { - use std::os::unix::process::CommandExt; - - use crate::command::new_std_command; - - let shell_kind = ShellKind::new(shell_path, false); - let zed_path = super::get_shell_safe_zed_path(shell_kind)?; - - let mut command_string = String::new(); - let mut command = new_std_command(shell_path); - command.args(args); - // In some shells, file descriptors greater than 2 cannot be used in interactive mode, - // so file descriptor 0 (stdin) is used instead. This impacts zsh, old bash; perhaps others. - // See: https://github.com/zed-industries/zed/pull/32136#issuecomment-2999645482 - const FD_STDIN: std::os::fd::RawFd = 0; - const FD_STDOUT: std::os::fd::RawFd = 1; - const FD_STDERR: std::os::fd::RawFd = 2; - - let (fd_num, redir) = match shell_kind { - ShellKind::Rc => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]` - ShellKind::Nushell | ShellKind::Tcsh => (FD_STDOUT, "".to_string()), - // xonsh doesn't support redirecting to stdin, and control sequences are printed to - // stdout on startup - ShellKind::Xonsh => (FD_STDERR, "o>e".to_string()), - ShellKind::PowerShell => (FD_STDIN, format!(">{}", FD_STDIN)), - _ => (FD_STDIN, format!(">&{}", FD_STDIN)), // `>&0` - }; - - match shell_kind { - ShellKind::Csh | ShellKind::Tcsh => { - // For csh/tcsh, login shell requires passing `-` as 0th argument (instead of `-l`) - command.arg0("-"); - } - ShellKind::Fish => { - // in fish, asdf, direnv attach to the `fish_prompt` event - command_string.push_str("emit fish_prompt;"); - command.arg("-l"); - } - _ => { - command.arg("-l"); - } - } - // cd into the directory, triggering directory specific side-effects (asdf, direnv, etc) - command_string.push_str(&format!("cd '{}';", directory.display())); - if let Some(prefix) = shell_kind.command_prefix() { - command_string.push(prefix); - } - command_string.push_str(&format!("{} --printenv {}", zed_path, redir)); - command.args(["-i", "-c", &command_string]); - - super::set_pre_exec_to_start_new_session(&mut command); - - let (env_output, process_output) = spawn_and_read_fd(command, fd_num).await?; - let env_output = String::from_utf8_lossy(&env_output); - - anyhow::ensure!( - process_output.status.success(), - "login shell exited with {}. stdout: {:?}, stderr: {:?}", - process_output.status, - String::from_utf8_lossy(&process_output.stdout), - String::from_utf8_lossy(&process_output.stderr), - ); - - // Parse the JSON output from zed --printenv - let env_map: collections::HashMap = serde_json::from_str(&env_output) - .with_context(|| { - format!("Failed to deserialize environment variables from json: {env_output}") - })?; - Ok(env_map) -} - -#[cfg(unix)] -async fn spawn_and_read_fd( - mut command: std::process::Command, - child_fd: std::os::fd::RawFd, -) -> anyhow::Result<(Vec, std::process::Output)> { - use command_fds::{CommandFdExt, FdMapping}; - use std::{io::Read, process::Stdio}; - - let (mut reader, writer) = std::io::pipe()?; - - command.fd_mappings(vec![FdMapping { - parent_fd: writer.into(), - child_fd, - }])?; - - let process = smol::process::Command::from(command) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn()?; - - let mut buffer = Vec::new(); - reader.read_to_end(&mut buffer)?; - - Ok((buffer, process.output().await?)) -} - -#[cfg(windows)] -async fn capture_windows( - shell_path: &Path, - args: &[String], - directory: &Path, -) -> Result> { - use std::process::Stdio; - - let zed_path = - std::env::current_exe().context("Failed to determine current zed executable path.")?; - - let shell_kind = ShellKind::new(shell_path, true); - let mut cmd = crate::command::new_smol_command(shell_path); - cmd.args(args); - let cmd = match shell_kind { - ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Fish - | ShellKind::Xonsh - | ShellKind::Posix => cmd.args([ - "-l", - "-i", - "-c", - &format!( - "cd '{}'; '{}' --printenv", - directory.display(), - zed_path.display() - ), - ]), - ShellKind::PowerShell | ShellKind::Pwsh => cmd.args([ - "-NonInteractive", - "-NoProfile", - "-Command", - &format!( - "Set-Location '{}'; & '{}' --printenv", - directory.display(), - zed_path.display() - ), - ]), - ShellKind::Elvish => cmd.args([ - "-c", - &format!( - "cd '{}'; '{}' --printenv", - directory.display(), - zed_path.display() - ), - ]), - ShellKind::Nushell => cmd.args([ - "-c", - &format!( - "cd '{}'; {}'{}' --printenv", - directory.display(), - shell_kind - .command_prefix() - .map(|prefix| prefix.to_string()) - .unwrap_or_default(), - zed_path.display() - ), - ]), - ShellKind::Cmd => cmd.args([ - "/c", - "cd", - &directory.display().to_string(), - "&&", - &zed_path.display().to_string(), - "--printenv", - ]), - } - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - let output = cmd - .output() - .await - .with_context(|| format!("command {cmd:?}"))?; - anyhow::ensure!( - output.status.success(), - "Command {cmd:?} failed with {}. stdout: {:?}, stderr: {:?}", - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ); - let env_output = String::from_utf8_lossy(&output.stdout); - - // Parse the JSON output from zed --printenv - serde_json::from_str(&env_output).with_context(|| { - format!("Failed to deserialize environment variables from json: {env_output}") - }) -} diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 4ea3590196..d7383a4975 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -10,18 +10,17 @@ pub mod schemars; pub mod serde; pub mod shell; pub mod shell_builder; -pub mod shell_env; pub mod size; #[cfg(any(test, feature = "test-support"))] pub mod test; pub mod time; -use anyhow::{Context as _, Result}; +use anyhow::Result; use futures::Future; use itertools::Either; -use paths::PathExt; + use regex::Regex; -use std::path::PathBuf; + use std::sync::{LazyLock, OnceLock}; use std::{ borrow::Cow, @@ -224,157 +223,7 @@ where items.sort_by(compare); } -/// Prevents execution of the application with root privileges on Unix systems. -/// -/// This function checks if the current process is running with root privileges -/// and terminates the program with an error message unless explicitly allowed via the -/// `ZED_ALLOW_ROOT` environment variable. -#[cfg(unix)] -pub fn prevent_root_execution() { - let is_root = nix::unistd::geteuid().is_root(); - let allow_root = std::env::var("ZED_ALLOW_ROOT").is_ok_and(|val| val == "true"); - if is_root && !allow_root { - eprintln!( - "\ -Error: Running Zed as root or via sudo is unsupported. - Doing so (even once) may subtly break things for all subsequent non-root usage of Zed. - It is untested and not recommended, don't complain when things break. - If you wish to proceed anyways, set `ZED_ALLOW_ROOT=true` in your environment." - ); - std::process::exit(1); - } -} - -#[cfg(unix)] -fn load_shell_from_passwd() -> Result<()> { - let buflen = match unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) } { - n if n < 0 => 1024, - n => n as usize, - }; - let mut buffer = Vec::with_capacity(buflen); - - let mut pwd: std::mem::MaybeUninit = std::mem::MaybeUninit::uninit(); - let mut result: *mut libc::passwd = std::ptr::null_mut(); - - let uid = unsafe { libc::getuid() }; - let status = unsafe { - libc::getpwuid_r( - uid, - pwd.as_mut_ptr(), - buffer.as_mut_ptr() as *mut libc::c_char, - buflen, - &mut result, - ) - }; - anyhow::ensure!(!result.is_null(), "passwd entry for uid {} not found", uid); - - // SAFETY: If `getpwuid_r` doesn't error, we have the entry here. - let entry = unsafe { pwd.assume_init() }; - - anyhow::ensure!( - status == 0, - "call to getpwuid_r failed. uid: {}, status: {}", - uid, - status - ); - anyhow::ensure!( - entry.pw_uid == uid, - "passwd entry has different uid ({}) than getuid ({}) returned", - entry.pw_uid, - uid, - ); - - let shell = unsafe { std::ffi::CStr::from_ptr(entry.pw_shell).to_str().unwrap() }; - let should_set_shell = env::var("SHELL").map_or(true, |shell_env| { - shell_env != shell && !std::path::Path::new(&shell_env).exists() - }); - - if should_set_shell { - log::info!( - "updating SHELL environment variable to value from passwd entry: {:?}", - shell, - ); - unsafe { env::set_var("SHELL", shell) }; - } - - Ok(()) -} - -/// Returns a shell escaped path for the current zed executable -pub fn get_shell_safe_zed_path(shell_kind: shell::ShellKind) -> anyhow::Result { - let zed_path = - std::env::current_exe().context("Failed to determine current zed executable path.")?; - - zed_path - .try_shell_safe(shell_kind) - .context("Failed to shell-escape Zed executable path.") -} - -/// Returns a path for the zed cli executable, this function -/// should be called from the zed executable, not zed-cli. -pub fn get_zed_cli_path() -> Result { - let zed_path = - std::env::current_exe().context("Failed to determine current zed executable path.")?; - let parent = zed_path - .parent() - .context("Failed to determine parent directory of zed executable path.")?; - - let possible_locations: &[&str] = if cfg!(target_os = "macos") { - // On macOS, the zed executable and zed-cli are inside the app bundle, - // so here ./cli is for both installed and development builds. - &["./cli"] - } else if cfg!(target_os = "windows") { - // bin/zed.exe is for installed builds, ./cli.exe is for development builds. - &["bin/zed.exe", "./cli.exe"] - } else if cfg!(target_os = "linux") || cfg!(target_os = "freebsd") { - // bin is the standard, ./cli is for the target directory in development builds. - &["../bin/zed", "./cli"] - } else { - anyhow::bail!("unsupported platform for determining zed-cli path"); - }; - - possible_locations - .iter() - .find_map(|p| { - parent - .join(p) - .canonicalize() - .ok() - .filter(|p| p != &zed_path) - }) - .with_context(|| { - format!( - "could not find zed-cli from any of: {}", - possible_locations.join(", ") - ) - }) -} - -#[cfg(unix)] -pub async fn load_login_shell_environment() -> Result<()> { - load_shell_from_passwd().log_err(); - - // If possible, we want to `cd` in the user's `$HOME` to trigger programs - // such as direnv, asdf, mise, ... to adjust the PATH. These tools often hook - // into shell's `cd` command (and hooks) to manipulate env. - // We do this so that we get the env a user would have when spawning a shell - // in home directory. - for (name, value) in shell_env::capture(get_system_shell(), &[], paths::home_dir()) - .await - .with_context(|| format!("capturing environment with {:?}", get_system_shell()))? - { - unsafe { env::set_var(&name, &value) }; - } - - log::info!( - "set environment variables from shell:{}, path:{}", - std::env::var("SHELL").unwrap_or_default(), - std::env::var("PATH").unwrap_or_default(), - ); - - Ok(()) -} /// Configures the process to start a new session, to prevent interactive shells from taking control /// of the terminal. diff --git a/crates/vercel/Cargo.toml b/crates/vercel/Cargo.toml deleted file mode 100644 index 98b26c9104..0000000000 --- a/crates/vercel/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "vercel" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/vercel.rs" - -[features] -default = [] -schemars = ["dep:schemars"] - -[dependencies] -anyhow.workspace = true -schemars = { workspace = true, optional = true } -serde.workspace = true -strum.workspace = true diff --git a/crates/vercel/LICENSE-GPL b/crates/vercel/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/vercel/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/vercel/src/vercel.rs b/crates/vercel/src/vercel.rs deleted file mode 100644 index 8686fda53f..0000000000 --- a/crates/vercel/src/vercel.rs +++ /dev/null @@ -1,78 +0,0 @@ -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use strum::EnumIter; - -pub const VERCEL_API_URL: &str = "https://api.v0.dev/v1"; - -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)] -pub enum Model { - #[default] - #[serde(rename = "v0-1.5-md")] - VZeroOnePointFiveMedium, - #[serde(rename = "custom")] - Custom { - name: String, - /// The name displayed in the UI, such as in the assistant panel model dropdown menu. - display_name: Option, - max_tokens: u64, - max_output_tokens: Option, - max_completion_tokens: Option, - }, -} - -impl Model { - pub fn default_fast() -> Self { - Self::VZeroOnePointFiveMedium - } - - pub fn from_id(id: &str) -> Result { - match id { - "v0-1.5-md" => Ok(Self::VZeroOnePointFiveMedium), - invalid_id => anyhow::bail!("invalid model id '{invalid_id}'"), - } - } - - pub fn id(&self) -> &str { - match self { - Self::VZeroOnePointFiveMedium => "v0-1.5-md", - Self::Custom { name, .. } => name, - } - } - - pub fn display_name(&self) -> &str { - match self { - Self::VZeroOnePointFiveMedium => "v0-1.5-md", - Self::Custom { - name, display_name, .. - } => display_name.as_ref().unwrap_or(name), - } - } - - pub fn max_token_count(&self) -> u64 { - match self { - Self::VZeroOnePointFiveMedium => 128_000, - Self::Custom { max_tokens, .. } => *max_tokens, - } - } - - pub fn max_output_tokens(&self) -> Option { - match self { - Self::VZeroOnePointFiveMedium => Some(32_000), - Self::Custom { - max_output_tokens, .. - } => *max_output_tokens, - } - } - - pub fn supports_parallel_tool_calls(&self) -> bool { - match self { - Self::VZeroOnePointFiveMedium => true, - Model::Custom { .. } => false, - } - } - - pub fn supports_prompt_cache_key(&self) -> bool { - false - } -} diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml deleted file mode 100644 index 74409a6c25..0000000000 --- a/crates/vim/Cargo.toml +++ /dev/null @@ -1,75 +0,0 @@ -[package] -name = "vim" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/vim.rs" -doctest = false - -[features] -neovim = ["nvim-rs", "async-compat", "async-trait", "tokio"] - -[dependencies] -anyhow.workspace = true -async-compat = { workspace = true, "optional" = true } -async-trait = { workspace = true, "optional" = true } -collections.workspace = true -command_palette.workspace = true -command_palette_hooks.workspace = true -db.workspace = true -editor.workspace = true -env_logger.workspace = true -futures.workspace = true -fuzzy.workspace = true -gpui.workspace = true -itertools.workspace = true -language.workspace = true -log.workspace = true -multi_buffer.workspace = true -nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", rev = "764dd270c642f77f10f3e19d05cc178a6cbe69f3", features = ["use_tokio"], optional = true } -picker.workspace = true -project.workspace = true -regex.workspace = true -schemars.workspace = true -search.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -task.workspace = true -text.workspace = true -theme.workspace = true -menu.workspace = true -tokio = { version = "1.15", features = ["full"], optional = true } -ui.workspace = true -util.workspace = true -util_macros.workspace = true -vim_mode_setting.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -assets.workspace = true -command_palette = { workspace = true, features = ["test-support"] } -editor = { workspace = true, features = ["test-support"] } -git_ui.workspace = true -gpui = { workspace = true, features = ["test-support"] } -indoc.workspace = true -language = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } -lsp = { workspace = true, features = ["test-support"] } -markdown_preview.workspace = true -parking_lot.workspace = true -project_panel.workspace = true -release_channel.workspace = true -semver.workspace = true -settings_ui.workspace = true -settings.workspace = true -perf.workspace = true -util = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/vim/LICENSE-GPL b/crates/vim/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/vim/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/vim/README.md b/crates/vim/README.md deleted file mode 100644 index 28f1375c62..0000000000 --- a/crates/vim/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This contains the code for Zed's Vim emulation mode. - -Vim mode in Zed is supposed to primarily "do what you expect": it mostly tries to copy vim exactly, but will use Zed-specific functionality when available to make things smoother. This means Zed will never be 100% vim compatible, but should be 100% vim familiar! - -The backlog is maintained in the `#vim` channel notes. - -## Testing against Neovim - -If you are making a change to make Zed's behavior more closely match vim/nvim, you can create a test using the `NeovimBackedTestContext`. - -For example, the following test checks that Zed and Neovim have the same behavior when running `*` in visual mode: - -```rust -#[gpui::test] -async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇa.c. abcd a.c. abcd").await; - cx.simulate_shared_keystrokes(["v", "3", "l", "*"]).await; - cx.assert_shared_state("a.c. abcd ˇa.c. abcd").await; -} -``` - -To keep CI runs fast, by default the neovim tests use a cached JSON file that records what neovim did (see crates/vim/test_data), -but while developing this test you'll need to run it with the neovim flag enabled: - -```sh -cargo test -p vim --features neovim test_visual_star_hash -``` - -This will run your keystrokes against a headless neovim and cache the results in the test_data directory. Note that neovim must be installed and reachable on your $PATH in order to run the feature. - - -## Testing zed-only behavior - -Zed does more than vim/neovim in their default modes. The `VimTestContext` can be used instead. This lets you test integration with the language server and other parts of zed's UI that don't have a NeoVim equivalent. diff --git a/crates/vim/src/change_list.rs b/crates/vim/src/change_list.rs deleted file mode 100644 index 21cd332800..0000000000 --- a/crates/vim/src/change_list.rs +++ /dev/null @@ -1,220 +0,0 @@ -use editor::{Bias, Direction, Editor, display_map::ToDisplayPoint, movement}; -use gpui::{Context, Window, actions}; - -use crate::{Vim, state::Mode}; - -actions!( - vim, - [ - /// Navigates to an older position in the change list. - ChangeListOlder, - /// Navigates to a newer position in the change list. - ChangeListNewer - ] -); - -pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, |vim, _: &ChangeListOlder, window, cx| { - vim.move_to_change(Direction::Prev, window, cx); - }); - Vim::action(editor, cx, |vim, _: &ChangeListNewer, window, cx| { - vim.move_to_change(Direction::Next, window, cx); - }); -} - -impl Vim { - fn move_to_change( - &mut self, - direction: Direction, - window: &mut Window, - cx: &mut Context, - ) { - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - self.update_editor(cx, |_, editor, cx| { - if let Some(selections) = editor - .change_list - .next_change(count, direction) - .map(|s| s.to_vec()) - { - editor.change_selections(Default::default(), window, cx, |s| { - let map = s.display_snapshot(); - s.select_display_ranges(selections.iter().map(|a| { - let point = a.to_display_point(&map); - point..point - })) - }) - }; - }); - } - - pub(crate) fn push_to_change_list(&mut self, window: &mut Window, cx: &mut Context) { - let Some((new_positions, buffer)) = self.update_editor(cx, |vim, editor, cx| { - let display_map = editor.display_snapshot(cx); - let selections = editor.selections.all_adjusted_display(&display_map); - let buffer = editor.buffer().clone(); - - let pop_state = editor - .change_list - .last() - .map(|previous| { - previous.len() == selections.len() - && previous.iter().enumerate().all(|(ix, p)| { - p.to_display_point(&display_map).row() == selections[ix].head().row() - }) - }) - .unwrap_or(false); - - let new_positions = selections - .into_iter() - .map(|s| { - let point = if vim.mode == Mode::Insert { - movement::saturating_left(&display_map, s.head()) - } else { - s.head() - }; - display_map.display_point_to_anchor(point, Bias::Left) - }) - .collect::>(); - - editor - .change_list - .push_to_change_list(pop_state, new_positions.clone()); - - (new_positions, buffer) - }) else { - return; - }; - - self.set_mark(".".to_string(), new_positions, &buffer, window, cx) - } -} - -#[cfg(test)] -mod test { - use indoc::indoc; - - use crate::{state::Mode, test::NeovimBackedTestContext}; - - #[gpui::test] - async fn test_change_list_insert(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇ").await; - - cx.simulate_shared_keystrokes("i 1 1 escape shift-o 2 2 escape shift-g o 3 3 escape") - .await; - - cx.shared_state().await.assert_eq(indoc! { - "22 - 11 - 3ˇ3" - }); - - cx.simulate_shared_keystrokes("g ;").await; - // NOTE: this matches nvim when I type it into it - // but in tests, nvim always reports the column as 0... - cx.assert_state( - indoc! { - "22 - 11 - 3ˇ3" - }, - Mode::Normal, - ); - cx.simulate_shared_keystrokes("g ;").await; - cx.assert_state( - indoc! { - "2ˇ2 - 11 - 33" - }, - Mode::Normal, - ); - cx.simulate_shared_keystrokes("g ;").await; - cx.assert_state( - indoc! { - "22 - 1ˇ1 - 33" - }, - Mode::Normal, - ); - cx.simulate_shared_keystrokes("g ,").await; - cx.assert_state( - indoc! { - "2ˇ2 - 11 - 33" - }, - Mode::Normal, - ); - cx.simulate_shared_keystrokes("shift-g i 4 4 escape").await; - cx.simulate_shared_keystrokes("g ;").await; - cx.assert_state( - indoc! { - "22 - 11 - 34ˇ43" - }, - Mode::Normal, - ); - cx.simulate_shared_keystrokes("g ;").await; - cx.assert_state( - indoc! { - "2ˇ2 - 11 - 3443" - }, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_change_list_delete(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - "one two - three fˇour"}) - .await; - cx.simulate_shared_keystrokes("x k d i w ^ x").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇne• - three fur"}); - cx.simulate_shared_keystrokes("2 g ;").await; - cx.shared_state().await.assert_eq(indoc! { - "ne• - three fˇur"}); - cx.simulate_shared_keystrokes("g ,").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇne• - three fur"}); - } - - #[gpui::test] - async fn test_gi(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - "one two - three fˇr"}) - .await; - cx.simulate_shared_keystrokes("i o escape k g i").await; - cx.simulate_shared_keystrokes("u escape").await; - cx.shared_state().await.assert_eq(indoc! { - "one two - three foˇur"}); - } - - #[gpui::test] - async fn test_dot_mark(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - "one two - three fˇr"}) - .await; - cx.simulate_shared_keystrokes("i o escape k ` .").await; - cx.shared_state().await.assert_eq(indoc! { - "one two - three fˇor"}); - } -} diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs deleted file mode 100644 index 5bf0fca041..0000000000 --- a/crates/vim/src/command.rs +++ /dev/null @@ -1,3211 +0,0 @@ -use anyhow::{Result, anyhow}; -use collections::{HashMap, HashSet}; -use command_palette_hooks::{CommandInterceptItem, CommandInterceptResult}; -use editor::{ - Bias, Editor, EditorSettings, SelectionEffects, ToPoint, - actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive}, - display_map::ToDisplayPoint, -}; -use futures::AsyncWriteExt as _; -use gpui::{ - Action, App, AppContext as _, Context, Global, Keystroke, Task, WeakEntity, Window, actions, -}; -use itertools::Itertools; -use language::Point; -use multi_buffer::MultiBufferRow; -use project::ProjectPath; -use regex::Regex; -use schemars::JsonSchema; -use search::{BufferSearchBar, SearchOptions}; -use serde::Deserialize; -use settings::{Settings, SettingsStore}; -use std::{ - iter::Peekable, - ops::{Deref, Range}, - path::{Path, PathBuf}, - process::Stdio, - str::Chars, - sync::OnceLock, - time::Instant, -}; -use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId}; -use ui::ActiveTheme; -use util::{ - ResultExt, - paths::PathStyle, - rel_path::{RelPath, RelPathBuf}, -}; -use workspace::{Item, SaveIntent, Workspace, notifications::NotifyResultExt}; -use workspace::{SplitDirection, notifications::DetachAndPromptErr}; -use zed_actions::{OpenDocs, RevealTarget}; - -use crate::{ - ToggleMarksView, ToggleRegistersView, Vim, - motion::{EndOfDocument, Motion, MotionKind, StartOfDocument}, - normal::{ - JoinLines, - search::{FindCommand, ReplaceCommand, Replacement}, - }, - object::Object, - state::{Mark, Mode}, - visual::VisualDeleteLine, -}; - -/// Goes to the specified line number in the editor. -#[derive(Clone, Debug, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -pub struct GoToLine { - range: CommandRange, -} - -/// Yanks (copies) text based on the specified range. -#[derive(Clone, Debug, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -pub struct YankCommand { - range: CommandRange, -} - -/// Executes a command with the specified range. -#[derive(Clone, Debug, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -pub struct WithRange { - restore_selection: bool, - range: CommandRange, - action: WrappedAction, -} - -/// Executes a command with the specified count. -#[derive(Clone, Debug, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -pub struct WithCount { - count: u32, - action: WrappedAction, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq)] -pub enum VimOption { - Wrap(bool), - Number(bool), - RelativeNumber(bool), - IgnoreCase(bool), -} - -impl VimOption { - fn possible_commands(query: &str) -> Vec { - let mut prefix_of_options = Vec::new(); - let mut options = query.split(" ").collect::>(); - let prefix = options.pop().unwrap_or_default(); - for option in options { - if let Some(opt) = Self::from(option) { - prefix_of_options.push(opt) - } else { - return vec![]; - } - } - - Self::possibilities(prefix) - .map(|possible| { - let mut options = prefix_of_options.clone(); - options.push(possible); - - CommandInterceptItem { - string: format!( - ":set {}", - options.iter().map(|opt| opt.to_string()).join(" ") - ), - action: VimSet { options }.boxed_clone(), - positions: vec![], - } - }) - .collect() - } - - fn possibilities(query: &str) -> impl Iterator + '_ { - [ - (None, VimOption::Wrap(true)), - (None, VimOption::Wrap(false)), - (None, VimOption::Number(true)), - (None, VimOption::Number(false)), - (None, VimOption::RelativeNumber(true)), - (None, VimOption::RelativeNumber(false)), - (Some("rnu"), VimOption::RelativeNumber(true)), - (Some("nornu"), VimOption::RelativeNumber(false)), - (None, VimOption::IgnoreCase(true)), - (None, VimOption::IgnoreCase(false)), - (Some("ic"), VimOption::IgnoreCase(true)), - (Some("noic"), VimOption::IgnoreCase(false)), - ] - .into_iter() - .filter(move |(prefix, option)| prefix.unwrap_or(option.to_string()).starts_with(query)) - .map(|(_, option)| option) - } - - fn from(option: &str) -> Option { - match option { - "wrap" => Some(Self::Wrap(true)), - "nowrap" => Some(Self::Wrap(false)), - - "number" => Some(Self::Number(true)), - "nu" => Some(Self::Number(true)), - "nonumber" => Some(Self::Number(false)), - "nonu" => Some(Self::Number(false)), - - "relativenumber" => Some(Self::RelativeNumber(true)), - "rnu" => Some(Self::RelativeNumber(true)), - "norelativenumber" => Some(Self::RelativeNumber(false)), - "nornu" => Some(Self::RelativeNumber(false)), - - "ignorecase" => Some(Self::IgnoreCase(true)), - "ic" => Some(Self::IgnoreCase(true)), - "noignorecase" => Some(Self::IgnoreCase(false)), - "noic" => Some(Self::IgnoreCase(false)), - - _ => None, - } - } - - fn to_string(&self) -> &'static str { - match self { - VimOption::Wrap(true) => "wrap", - VimOption::Wrap(false) => "nowrap", - VimOption::Number(true) => "number", - VimOption::Number(false) => "nonumber", - VimOption::RelativeNumber(true) => "relativenumber", - VimOption::RelativeNumber(false) => "norelativenumber", - VimOption::IgnoreCase(true) => "ignorecase", - VimOption::IgnoreCase(false) => "noignorecase", - } - } -} - -/// Sets vim options and configuration values. -#[derive(Clone, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -pub struct VimSet { - options: Vec, -} - -/// Saves the current file with optional save intent. -#[derive(Clone, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -struct VimSave { - pub range: Option, - pub save_intent: Option, - pub filename: String, -} - -/// Deletes the specified marks from the editor. -#[derive(Clone, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -struct VimSplit { - pub vertical: bool, - pub filename: String, -} - -#[derive(Clone, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -enum DeleteMarks { - Marks(String), - AllLocal, -} - -actions!( - vim, - [ - /// Executes a command in visual mode. - VisualCommand, - /// Executes a command with a count prefix. - CountCommand, - /// Executes a shell command. - ShellCommand, - /// Indicates that an argument is required for the command. - ArgumentRequired - ] -); - -/// Opens the specified file for editing. -#[derive(Clone, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -struct VimEdit { - pub filename: String, -} - -#[derive(Clone, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -struct VimNorm { - pub range: Option, - pub command: String, -} - -#[derive(Debug)] -struct WrappedAction(Box); - -impl PartialEq for WrappedAction { - fn eq(&self, other: &Self) -> bool { - self.0.partial_eq(&*other.0) - } -} - -impl Clone for WrappedAction { - fn clone(&self) -> Self { - Self(self.0.boxed_clone()) - } -} - -impl Deref for WrappedAction { - type Target = dyn Action; - fn deref(&self) -> &dyn Action { - &*self.0 - } -} - -pub fn register(editor: &mut Editor, cx: &mut Context) { - // Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| { - Vim::action(editor, cx, |vim, action: &VimSet, _, cx| { - for option in action.options.iter() { - vim.update_editor(cx, |_, editor, cx| match option { - VimOption::Wrap(true) => { - editor - .set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); - } - VimOption::Wrap(false) => { - editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx); - } - VimOption::Number(enabled) => { - editor.set_show_line_numbers(*enabled, cx); - } - VimOption::RelativeNumber(enabled) => { - editor.set_relative_line_number(Some(*enabled), cx); - } - VimOption::IgnoreCase(enabled) => { - let mut settings = EditorSettings::get_global(cx).clone(); - settings.search.case_sensitive = !*enabled; - SettingsStore::update(cx, |store, _| { - store.override_global(settings); - }); - } - }); - } - }); - Vim::action(editor, cx, |vim, _: &VisualCommand, window, cx| { - let Some(workspace) = vim.workspace(window) else { - return; - }; - workspace.update(cx, |workspace, cx| { - command_palette::CommandPalette::toggle(workspace, "'<,'>", window, cx); - }) - }); - - Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| { - let Some(workspace) = vim.workspace(window) else { - return; - }; - workspace.update(cx, |workspace, cx| { - command_palette::CommandPalette::toggle(workspace, "'<,'>!", window, cx); - }) - }); - - Vim::action(editor, cx, |_, _: &ArgumentRequired, window, cx| { - let _ = window.prompt( - gpui::PromptLevel::Critical, - "Argument required", - None, - &["Cancel"], - cx, - ); - }); - - Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| { - let Some(workspace) = vim.workspace(window) else { - return; - }; - workspace.update(cx, |workspace, cx| { - command_palette::CommandPalette::toggle(workspace, "'<,'>!", window, cx); - }) - }); - - Vim::action(editor, cx, |vim, action: &VimSave, window, cx| { - if let Some(range) = &action.range { - vim.update_editor(cx, |vim, editor, cx| { - let Some(range) = range.buffer_range(vim, editor, window, cx).ok() else { - return; - }; - let Some((line_ending, text, whole_buffer)) = editor.buffer().update(cx, |multi, cx| { - Some(multi.as_singleton()?.update(cx, |buffer, _| { - ( - buffer.line_ending(), - buffer.as_rope().slice_rows(range.start.0..range.end.0 + 1), - range.start.0 == 0 && range.end.0 + 1 >= buffer.row_count(), - ) - })) - }) else { - return; - }; - - let filename = action.filename.clone(); - let filename = if filename.is_empty() { - let Some(file) = editor - .buffer() - .read(cx) - .as_singleton() - .and_then(|buffer| buffer.read(cx).file()) - else { - let _ = window.prompt( - gpui::PromptLevel::Warning, - "No file name", - Some("Partial buffer write requires file name."), - &["Cancel"], - cx, - ); - return; - }; - file.path().display(file.path_style(cx)).to_string() - } else { - filename - }; - - if action.filename.is_empty() { - if whole_buffer { - if let Some(workspace) = vim.workspace(window) { - workspace.update(cx, |workspace, cx| { - workspace - .save_active_item( - action.save_intent.unwrap_or(SaveIntent::Save), - window, - cx, - ) - .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None); - }); - } - return; - } - if Some(SaveIntent::Overwrite) != action.save_intent { - let _ = window.prompt( - gpui::PromptLevel::Warning, - "Use ! to write partial buffer", - Some("Overwriting the current file with selected buffer content requires '!'."), - &["Cancel"], - cx, - ); - return; - } - editor.buffer().update(cx, |multi, cx| { - if let Some(buffer) = multi.as_singleton() { - buffer.update(cx, |buffer, _| buffer.set_conflict()); - } - }); - }; - - editor.project().unwrap().update(cx, |project, cx| { - let worktree = project.visible_worktrees(cx).next().unwrap(); - - worktree.update(cx, |worktree, cx| { - let path_style = worktree.path_style(); - let Some(path) = RelPath::new(Path::new(&filename), path_style).ok() else { - return; - }; - - let rx = (worktree.entry_for_path(&path).is_some() && Some(SaveIntent::Overwrite) != action.save_intent).then(|| { - window.prompt( - gpui::PromptLevel::Warning, - &format!("{path:?} already exists. Do you want to replace it?"), - Some( - "A file or folder with the same name already exists. Replacing it will overwrite its current contents.", - ), - &["Replace", "Cancel"], - cx - ) - }); - let filename = filename.clone(); - cx.spawn_in(window, async move |this, cx| { - if let Some(rx) = rx - && Ok(0) != rx.await - { - return; - } - - let _ = this.update_in(cx, |worktree, window, cx| { - let Some(path) = RelPath::new(Path::new(&filename), path_style).ok() else { - return; - }; - worktree - .write_file(path.into_arc(), text.clone(), line_ending, cx) - .detach_and_prompt_err("Failed to write lines", window, cx, |_, _, _| None); - }); - }) - .detach(); - }); - }); - }); - return; - } - if action.filename.is_empty() { - if let Some(workspace) = vim.workspace(window) { - workspace.update(cx, |workspace, cx| { - workspace - .save_active_item( - action.save_intent.unwrap_or(SaveIntent::Save), - window, - cx, - ) - .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None); - }); - } - return; - } - vim.update_editor(cx, |_, editor, cx| { - let Some(project) = editor.project().cloned() else { - return; - }; - let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else { - return; - }; - let path_style = worktree.read(cx).path_style(); - let Ok(project_path) = - RelPath::new(Path::new(&action.filename), path_style).map(|path| ProjectPath { - worktree_id: worktree.read(cx).id(), - path: path.into_arc(), - }) - else { - // TODO implement save_as with absolute path - Task::ready(Err::<(), _>(anyhow!( - "Cannot save buffer with absolute path" - ))) - .detach_and_prompt_err( - "Failed to save", - window, - cx, - |_, _, _| None, - ); - return; - }; - - if project.read(cx).entry_for_path(&project_path, cx).is_some() - && action.save_intent != Some(SaveIntent::Overwrite) - { - let answer = window.prompt( - gpui::PromptLevel::Critical, - &format!( - "{} already exists. Do you want to replace it?", - project_path.path.display(path_style) - ), - Some( - "A file or folder with the same name already exists. \ - Replacing it will overwrite its current contents.", - ), - &["Replace", "Cancel"], - cx, - ); - cx.spawn_in(window, async move |editor, cx| { - if answer.await.ok() != Some(0) { - return; - } - - let _ = editor.update_in(cx, |editor, window, cx| { - editor - .save_as(project, project_path, window, cx) - .detach_and_prompt_err("Failed to :w", window, cx, |_, _, _| None); - }); - }) - .detach(); - } else { - editor - .save_as(project, project_path, window, cx) - .detach_and_prompt_err("Failed to :w", window, cx, |_, _, _| None); - } - }); - }); - - Vim::action(editor, cx, |vim, action: &VimSplit, window, cx| { - let Some(workspace) = vim.workspace(window) else { - return; - }; - - workspace.update(cx, |workspace, cx| { - let project = workspace.project().clone(); - let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else { - return; - }; - let path_style = worktree.read(cx).path_style(); - let Some(path) = RelPath::new(Path::new(&action.filename), path_style).log_err() else { - return; - }; - let project_path = ProjectPath { - worktree_id: worktree.read(cx).id(), - path: path.into_arc(), - }; - - let direction = if action.vertical { - SplitDirection::vertical(cx) - } else { - SplitDirection::horizontal(cx) - }; - - workspace - .split_path_preview(project_path, false, Some(direction), window, cx) - .detach_and_log_err(cx); - }) - }); - - Vim::action(editor, cx, |vim, action: &DeleteMarks, window, cx| { - fn err(s: String, window: &mut Window, cx: &mut Context) { - let _ = window.prompt( - gpui::PromptLevel::Critical, - &format!("Invalid argument: {}", s), - None, - &["Cancel"], - cx, - ); - } - vim.update_editor(cx, |vim, editor, cx| match action { - DeleteMarks::Marks(s) => { - if s.starts_with('-') || s.ends_with('-') || s.contains(['\'', '`']) { - err(s.clone(), window, cx); - return; - } - - let to_delete = if s.len() < 3 { - Some(s.clone()) - } else { - s.chars() - .tuple_windows::<(_, _, _)>() - .map(|(a, b, c)| { - if b == '-' { - if match a { - 'a'..='z' => a <= c && c <= 'z', - 'A'..='Z' => a <= c && c <= 'Z', - '0'..='9' => a <= c && c <= '9', - _ => false, - } { - Some((a..=c).collect_vec()) - } else { - None - } - } else if a == '-' { - if c == '-' { None } else { Some(vec![c]) } - } else if c == '-' { - if a == '-' { None } else { Some(vec![a]) } - } else { - Some(vec![a, b, c]) - } - }) - .fold_options(HashSet::::default(), |mut set, chars| { - set.extend(chars.iter().copied()); - set - }) - .map(|set| set.iter().collect::()) - }; - - let Some(to_delete) = to_delete else { - err(s.clone(), window, cx); - return; - }; - - for c in to_delete.chars().filter(|c| !c.is_whitespace()) { - vim.delete_mark(c.to_string(), editor, window, cx); - } - } - DeleteMarks::AllLocal => { - for s in 'a'..='z' { - vim.delete_mark(s.to_string(), editor, window, cx); - } - } - }); - }); - - Vim::action(editor, cx, |vim, action: &VimEdit, window, cx| { - vim.update_editor(cx, |vim, editor, cx| { - let Some(workspace) = vim.workspace(window) else { - return; - }; - let Some(project) = editor.project().cloned() else { - return; - }; - let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else { - return; - }; - let path_style = worktree.read(cx).path_style(); - let Some(path) = RelPath::new(Path::new(&action.filename), path_style).log_err() else { - return; - }; - let project_path = ProjectPath { - worktree_id: worktree.read(cx).id(), - path: path.into_arc(), - }; - - let _ = workspace.update(cx, |workspace, cx| { - workspace - .open_path(project_path, None, true, window, cx) - .detach_and_log_err(cx); - }); - }); - }); - - Vim::action(editor, cx, |vim, action: &VimNorm, window, cx| { - let keystrokes = action - .command - .chars() - .map(|c| Keystroke::parse(&c.to_string()).unwrap()) - .collect(); - vim.switch_mode(Mode::Normal, true, window, cx); - let initial_selections = - vim.update_editor(cx, |_, editor, _| editor.selections.disjoint_anchors_arc()); - if let Some(range) = &action.range { - let result = vim.update_editor(cx, |vim, editor, cx| { - let range = range.buffer_range(vim, editor, window, cx)?; - editor.change_selections( - SelectionEffects::no_scroll().nav_history(false), - window, - cx, - |s| { - s.select_ranges( - (range.start.0..=range.end.0) - .map(|line| Point::new(line, 0)..Point::new(line, 0)), - ); - }, - ); - anyhow::Ok(()) - }); - if let Some(Err(err)) = result { - log::error!("Error selecting range: {}", err); - return; - } - }; - - let Some(workspace) = vim.workspace(window) else { - return; - }; - let task = workspace.update(cx, |workspace, cx| { - workspace.send_keystrokes_impl(keystrokes, window, cx) - }); - let had_range = action.range.is_some(); - - cx.spawn_in(window, async move |vim, cx| { - task.await; - vim.update_in(cx, |vim, window, cx| { - vim.update_editor(cx, |_, editor, cx| { - if had_range { - editor.change_selections(SelectionEffects::default(), window, cx, |s| { - s.select_anchor_ranges([s.newest_anchor().range()]); - }) - } - }); - if matches!(vim.mode, Mode::Insert | Mode::Replace) { - vim.normal_before(&Default::default(), window, cx); - } else { - vim.switch_mode(Mode::Normal, true, window, cx); - } - vim.update_editor(cx, |_, editor, cx| { - if let Some(first_sel) = initial_selections - && let Some(tx_id) = editor - .buffer() - .update(cx, |multi, cx| multi.last_transaction_id(cx)) - { - let last_sel = editor.selections.disjoint_anchors_arc(); - editor.modify_transaction_selection_history(tx_id, |old| { - old.0 = first_sel; - old.1 = Some(last_sel); - }); - } - }); - }) - .ok(); - }) - .detach(); - }); - - Vim::action(editor, cx, |vim, _: &CountCommand, window, cx| { - let Some(workspace) = vim.workspace(window) else { - return; - }; - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - let n = if count > 1 { - format!(".,.+{}", count.saturating_sub(1)) - } else { - ".".to_string() - }; - workspace.update(cx, |workspace, cx| { - command_palette::CommandPalette::toggle(workspace, &n, window, cx); - }) - }); - - Vim::action(editor, cx, |vim, action: &GoToLine, window, cx| { - vim.switch_mode(Mode::Normal, false, window, cx); - let result = vim.update_editor(cx, |vim, editor, cx| { - let snapshot = editor.snapshot(window, cx); - let buffer_row = action.range.head().buffer_row(vim, editor, window, cx)?; - let current = editor - .selections - .newest::(&editor.display_snapshot(cx)); - let target = snapshot - .buffer_snapshot() - .clip_point(Point::new(buffer_row.0, current.head().column), Bias::Left); - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges([target..target]); - }); - - anyhow::Ok(()) - }); - if let Some(e @ Err(_)) = result { - let Some(workspace) = vim.workspace(window) else { - return; - }; - workspace.update(cx, |workspace, cx| { - e.notify_err(workspace, cx); - }); - } - }); - - Vim::action(editor, cx, |vim, action: &YankCommand, window, cx| { - vim.update_editor(cx, |vim, editor, cx| { - let snapshot = editor.snapshot(window, cx); - if let Ok(range) = action.range.buffer_range(vim, editor, window, cx) { - let end = if range.end < snapshot.buffer_snapshot().max_row() { - Point::new(range.end.0 + 1, 0) - } else { - snapshot.buffer_snapshot().max_point() - }; - vim.copy_ranges( - editor, - MotionKind::Linewise, - true, - vec![Point::new(range.start.0, 0)..end], - window, - cx, - ) - } - }); - }); - - Vim::action(editor, cx, |_, action: &WithCount, window, cx| { - for _ in 0..action.count { - window.dispatch_action(action.action.boxed_clone(), cx) - } - }); - - Vim::action(editor, cx, |vim, action: &WithRange, window, cx| { - let result = vim.update_editor(cx, |vim, editor, cx| { - action.range.buffer_range(vim, editor, window, cx) - }); - - let range = match result { - None => return, - Some(e @ Err(_)) => { - let Some(workspace) = vim.workspace(window) else { - return; - }; - workspace.update(cx, |workspace, cx| { - e.notify_err(workspace, cx); - }); - return; - } - Some(Ok(result)) => result, - }; - - let previous_selections = vim - .update_editor(cx, |_, editor, cx| { - let selections = action.restore_selection.then(|| { - editor - .selections - .disjoint_anchor_ranges() - .collect::>() - }); - let snapshot = editor.buffer().read(cx).snapshot(cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - let end = Point::new(range.end.0, snapshot.line_len(range.end)); - s.select_ranges([end..Point::new(range.start.0, 0)]); - }); - selections - }) - .flatten(); - window.dispatch_action(action.action.boxed_clone(), cx); - cx.defer_in(window, move |vim, window, cx| { - vim.update_editor(cx, |_, editor, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - if let Some(previous_selections) = previous_selections { - s.select_ranges(previous_selections); - } else { - s.select_ranges([ - Point::new(range.start.0, 0)..Point::new(range.start.0, 0) - ]); - } - }) - }); - }); - }); - - Vim::action(editor, cx, |vim, action: &OnMatchingLines, window, cx| { - action.run(vim, window, cx) - }); - - Vim::action(editor, cx, |vim, action: &ShellExec, window, cx| { - action.run(vim, window, cx) - }) -} - -#[derive(Default)] -struct VimCommand { - prefix: &'static str, - suffix: &'static str, - action: Option>, - action_name: Option<&'static str>, - bang_action: Option>, - args: Option< - Box, String) -> Option> + Send + Sync + 'static>, - >, - /// Optional range Range to use if no range is specified. - default_range: Option, - range: Option< - Box< - dyn Fn(Box, &CommandRange) -> Option> - + Send - + Sync - + 'static, - >, - >, - has_count: bool, - has_filename: bool, -} - -struct ParsedQuery { - args: String, - has_bang: bool, - has_space: bool, -} - -impl VimCommand { - fn new(pattern: (&'static str, &'static str), action: impl Action) -> Self { - Self { - prefix: pattern.0, - suffix: pattern.1, - action: Some(action.boxed_clone()), - ..Default::default() - } - } - - // from_str is used for actions in other crates. - fn str(pattern: (&'static str, &'static str), action_name: &'static str) -> Self { - Self { - prefix: pattern.0, - suffix: pattern.1, - action_name: Some(action_name), - ..Default::default() - } - } - - fn bang(mut self, bang_action: impl Action) -> Self { - self.bang_action = Some(bang_action.boxed_clone()); - self - } - - fn args( - mut self, - f: impl Fn(Box, String) -> Option> + Send + Sync + 'static, - ) -> Self { - self.args = Some(Box::new(f)); - self - } - - fn filename( - mut self, - f: impl Fn(Box, String) -> Option> + Send + Sync + 'static, - ) -> Self { - self.args = Some(Box::new(f)); - self.has_filename = true; - self - } - - fn range( - mut self, - f: impl Fn(Box, &CommandRange) -> Option> + Send + Sync + 'static, - ) -> Self { - self.range = Some(Box::new(f)); - self - } - - fn default_range(mut self, range: CommandRange) -> Self { - self.default_range = Some(range); - self - } - - fn count(mut self) -> Self { - self.has_count = true; - self - } - - fn generate_filename_completions( - parsed_query: &ParsedQuery, - workspace: WeakEntity, - cx: &mut App, - ) -> Task> { - let ParsedQuery { - args, - has_bang: _, - has_space: _, - } = parsed_query; - let Some(workspace) = workspace.upgrade() else { - return Task::ready(Vec::new()); - }; - - let (task, args_path) = workspace.update(cx, |workspace, cx| { - let prefix = workspace - .project() - .read(cx) - .visible_worktrees(cx) - .map(|worktree| worktree.read(cx).abs_path().to_path_buf()) - .next() - .or_else(std::env::home_dir) - .unwrap_or_else(|| PathBuf::from("")); - - let rel_path = match RelPath::new(Path::new(&args), PathStyle::local()) { - Ok(path) => path.to_rel_path_buf(), - Err(_) => { - return (Task::ready(Ok(Vec::new())), RelPathBuf::new()); - } - }; - - let rel_path = if args.ends_with(PathStyle::local().primary_separator()) { - rel_path - } else { - rel_path - .parent() - .map(|rel_path| rel_path.to_rel_path_buf()) - .unwrap_or(RelPathBuf::new()) - }; - - let task = workspace.project().update(cx, |project, cx| { - let path = prefix - .join(rel_path.as_std_path()) - .to_string_lossy() - .to_string(); - project.list_directory(path, cx) - }); - - (task, rel_path) - }); - - cx.background_spawn(async move { - let directories = task.await.unwrap_or_default(); - directories - .iter() - .map(|dir| { - let path = RelPath::new(dir.path.as_path(), PathStyle::local()) - .map(|cow| cow.into_owned()) - .unwrap_or(RelPathBuf::new()); - let mut path_string = args_path - .join(&path) - .display(PathStyle::local()) - .to_string(); - if dir.is_dir { - path_string.push_str(PathStyle::local().primary_separator()); - } - path_string - }) - .collect() - }) - } - - fn get_parsed_query(&self, query: String) -> Option { - let rest = query - .strip_prefix(self.prefix)? - .to_string() - .chars() - .zip_longest(self.suffix.to_string().chars()) - .skip_while(|e| e.clone().both().map(|(s, q)| s == q).unwrap_or(false)) - .filter_map(|e| e.left()) - .collect::(); - let has_bang = rest.starts_with('!'); - let has_space = rest.starts_with("! ") || rest.starts_with(' '); - let args = if has_bang { - rest.strip_prefix('!')?.trim().to_string() - } else if rest.is_empty() { - "".into() - } else { - rest.strip_prefix(' ')?.trim().to_string() - }; - Some(ParsedQuery { - args, - has_bang, - has_space, - }) - } - - fn parse( - &self, - query: &str, - range: &Option, - cx: &App, - ) -> Option> { - let ParsedQuery { - args, - has_bang, - has_space: _, - } = self.get_parsed_query(query.to_string())?; - let action = if has_bang && self.bang_action.is_some() { - self.bang_action.as_ref().unwrap().boxed_clone() - } else if let Some(action) = self.action.as_ref() { - action.boxed_clone() - } else if let Some(action_name) = self.action_name { - cx.build_action(action_name, None).log_err()? - } else { - return None; - }; - - let action = if args.is_empty() { - action - } else { - // if command does not accept args and we have args then we should do no action - self.args.as_ref()?(action, args)? - }; - - let range = range.as_ref().or(self.default_range.as_ref()); - if let Some(range) = range { - self.range.as_ref().and_then(|f| f(action, range)) - } else { - Some(action) - } - } - - // TODO: ranges with search queries - fn parse_range(query: &str) -> (Option, String) { - let mut chars = query.chars().peekable(); - - match chars.peek() { - Some('%') => { - chars.next(); - return ( - Some(CommandRange { - start: Position::Line { row: 1, offset: 0 }, - end: Some(Position::LastLine { offset: 0 }), - }), - chars.collect(), - ); - } - Some('*') => { - chars.next(); - return ( - Some(CommandRange { - start: Position::Mark { - name: '<', - offset: 0, - }, - end: Some(Position::Mark { - name: '>', - offset: 0, - }), - }), - chars.collect(), - ); - } - _ => {} - } - - let start = Self::parse_position(&mut chars); - - match chars.peek() { - Some(',' | ';') => { - chars.next(); - ( - Some(CommandRange { - start: start.unwrap_or(Position::CurrentLine { offset: 0 }), - end: Self::parse_position(&mut chars), - }), - chars.collect(), - ) - } - _ => ( - start.map(|start| CommandRange { start, end: None }), - chars.collect(), - ), - } - } - - fn parse_position(chars: &mut Peekable) -> Option { - match chars.peek()? { - '0'..='9' => { - let row = Self::parse_u32(chars); - Some(Position::Line { - row, - offset: Self::parse_offset(chars), - }) - } - '\'' => { - chars.next(); - let name = chars.next()?; - Some(Position::Mark { - name, - offset: Self::parse_offset(chars), - }) - } - '.' => { - chars.next(); - Some(Position::CurrentLine { - offset: Self::parse_offset(chars), - }) - } - '+' | '-' => Some(Position::CurrentLine { - offset: Self::parse_offset(chars), - }), - '$' => { - chars.next(); - Some(Position::LastLine { - offset: Self::parse_offset(chars), - }) - } - _ => None, - } - } - - fn parse_offset(chars: &mut Peekable) -> i32 { - let mut res: i32 = 0; - while matches!(chars.peek(), Some('+' | '-')) { - let sign = if chars.next().unwrap() == '+' { 1 } else { -1 }; - let amount = if matches!(chars.peek(), Some('0'..='9')) { - (Self::parse_u32(chars) as i32).saturating_mul(sign) - } else { - sign - }; - res = res.saturating_add(amount) - } - res - } - - fn parse_u32(chars: &mut Peekable) -> u32 { - let mut res: u32 = 0; - while matches!(chars.peek(), Some('0'..='9')) { - res = res - .saturating_mul(10) - .saturating_add(chars.next().unwrap() as u32 - '0' as u32); - } - res - } -} - -#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq)] -enum Position { - Line { row: u32, offset: i32 }, - Mark { name: char, offset: i32 }, - LastLine { offset: i32 }, - CurrentLine { offset: i32 }, -} - -impl Position { - fn buffer_row( - &self, - vim: &Vim, - editor: &mut Editor, - window: &mut Window, - cx: &mut App, - ) -> Result { - let snapshot = editor.snapshot(window, cx); - let target = match self { - Position::Line { row, offset } => { - if let Some(anchor) = editor.active_excerpt(cx).and_then(|(_, buffer, _)| { - editor.buffer().read(cx).buffer_point_to_anchor( - &buffer, - Point::new(row.saturating_sub(1), 0), - cx, - ) - }) { - anchor - .to_point(&snapshot.buffer_snapshot()) - .row - .saturating_add_signed(*offset) - } else { - row.saturating_add_signed(offset.saturating_sub(1)) - } - } - Position::Mark { name, offset } => { - let Some(Mark::Local(anchors)) = - vim.get_mark(&name.to_string(), editor, window, cx) - else { - anyhow::bail!("mark {name} not set"); - }; - let Some(mark) = anchors.last() else { - anyhow::bail!("mark {name} contains empty anchors"); - }; - mark.to_point(&snapshot.buffer_snapshot()) - .row - .saturating_add_signed(*offset) - } - Position::LastLine { offset } => snapshot - .buffer_snapshot() - .max_row() - .0 - .saturating_add_signed(*offset), - Position::CurrentLine { offset } => editor - .selections - .newest_anchor() - .head() - .to_point(&snapshot.buffer_snapshot()) - .row - .saturating_add_signed(*offset), - }; - - Ok(MultiBufferRow(target).min(snapshot.buffer_snapshot().max_row())) - } -} - -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct CommandRange { - start: Position, - end: Option, -} - -impl CommandRange { - fn head(&self) -> &Position { - self.end.as_ref().unwrap_or(&self.start) - } - - /// Convert the `CommandRange` into a `Range`. - pub(crate) fn buffer_range( - &self, - vim: &Vim, - editor: &mut Editor, - window: &mut Window, - cx: &mut App, - ) -> Result> { - let start = self.start.buffer_row(vim, editor, window, cx)?; - let end = if let Some(end) = self.end.as_ref() { - end.buffer_row(vim, editor, window, cx)? - } else { - start - }; - if end < start { - anyhow::Ok(end..start) - } else { - anyhow::Ok(start..end) - } - } - - pub fn as_count(&self) -> Option { - if let CommandRange { - start: Position::Line { row, offset: 0 }, - end: None, - } = &self - { - Some(*row) - } else { - None - } - } - - /// The `CommandRange` representing the entire buffer. - fn buffer() -> Self { - Self { - start: Position::Line { row: 1, offset: 0 }, - end: Some(Position::LastLine { offset: 0 }), - } - } -} - -fn generate_commands(_: &App) -> Vec { - vec![ - VimCommand::new( - ("w", "rite"), - VimSave { - save_intent: Some(SaveIntent::Save), - filename: "".into(), - range: None, - }, - ) - .bang(VimSave { - save_intent: Some(SaveIntent::Overwrite), - filename: "".into(), - range: None, - }) - .filename(|action, filename| { - Some( - VimSave { - save_intent: action - .as_any() - .downcast_ref::() - .and_then(|action| action.save_intent), - filename, - range: None, - } - .boxed_clone(), - ) - }) - .range(|action, range| { - let mut action: VimSave = action.as_any().downcast_ref::().unwrap().clone(); - action.range.replace(range.clone()); - Some(Box::new(action)) - }), - VimCommand::new(("e", "dit"), editor::actions::ReloadFile) - .bang(editor::actions::ReloadFile) - .filename(|_, filename| Some(VimEdit { filename }.boxed_clone())), - VimCommand::new(("sp", "lit"), workspace::SplitHorizontal).filename(|_, filename| { - Some( - VimSplit { - vertical: false, - filename, - } - .boxed_clone(), - ) - }), - VimCommand::new(("vs", "plit"), workspace::SplitVertical).filename(|_, filename| { - Some( - VimSplit { - vertical: true, - filename, - } - .boxed_clone(), - ) - }), - VimCommand::new(("tabe", "dit"), workspace::NewFile) - .filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())), - VimCommand::new(("tabnew", ""), workspace::NewFile) - .filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())), - VimCommand::new( - ("q", "uit"), - workspace::CloseActiveItem { - save_intent: Some(SaveIntent::Close), - close_pinned: false, - }, - ) - .bang(workspace::CloseActiveItem { - save_intent: Some(SaveIntent::Skip), - close_pinned: true, - }), - VimCommand::new( - ("wq", ""), - workspace::CloseActiveItem { - save_intent: Some(SaveIntent::Save), - close_pinned: false, - }, - ) - .bang(workspace::CloseActiveItem { - save_intent: Some(SaveIntent::Overwrite), - close_pinned: true, - }), - VimCommand::new( - ("x", "it"), - workspace::CloseActiveItem { - save_intent: Some(SaveIntent::SaveAll), - close_pinned: false, - }, - ) - .bang(workspace::CloseActiveItem { - save_intent: Some(SaveIntent::Overwrite), - close_pinned: true, - }), - VimCommand::new( - ("exi", "t"), - workspace::CloseActiveItem { - save_intent: Some(SaveIntent::SaveAll), - close_pinned: false, - }, - ) - .bang(workspace::CloseActiveItem { - save_intent: Some(SaveIntent::Overwrite), - close_pinned: true, - }), - VimCommand::new( - ("up", "date"), - workspace::Save { - save_intent: Some(SaveIntent::SaveAll), - }, - ), - VimCommand::new( - ("wa", "ll"), - workspace::SaveAll { - save_intent: Some(SaveIntent::SaveAll), - }, - ) - .bang(workspace::SaveAll { - save_intent: Some(SaveIntent::Overwrite), - }), - VimCommand::new( - ("qa", "ll"), - workspace::CloseAllItemsAndPanes { - save_intent: Some(SaveIntent::Close), - }, - ) - .bang(workspace::CloseAllItemsAndPanes { - save_intent: Some(SaveIntent::Skip), - }), - VimCommand::new( - ("quita", "ll"), - workspace::CloseAllItemsAndPanes { - save_intent: Some(SaveIntent::Close), - }, - ) - .bang(workspace::CloseAllItemsAndPanes { - save_intent: Some(SaveIntent::Skip), - }), - VimCommand::new( - ("xa", "ll"), - workspace::CloseAllItemsAndPanes { - save_intent: Some(SaveIntent::SaveAll), - }, - ) - .bang(workspace::CloseAllItemsAndPanes { - save_intent: Some(SaveIntent::Overwrite), - }), - VimCommand::new( - ("wqa", "ll"), - workspace::CloseAllItemsAndPanes { - save_intent: Some(SaveIntent::SaveAll), - }, - ) - .bang(workspace::CloseAllItemsAndPanes { - save_intent: Some(SaveIntent::Overwrite), - }), - VimCommand::new(("cq", "uit"), zed_actions::Quit), - VimCommand::new( - ("bd", "elete"), - workspace::CloseActiveItem { - save_intent: Some(SaveIntent::Close), - close_pinned: false, - }, - ) - .bang(workspace::CloseActiveItem { - save_intent: Some(SaveIntent::Skip), - close_pinned: true, - }), - VimCommand::new( - ("norm", "al"), - VimNorm { - command: "".into(), - range: None, - }, - ) - .args(|_, args| { - Some( - VimNorm { - command: args, - range: None, - } - .boxed_clone(), - ) - }) - .range(|action, range| { - let mut action: VimNorm = action.as_any().downcast_ref::().unwrap().clone(); - action.range.replace(range.clone()); - Some(Box::new(action)) - }), - VimCommand::new(("bn", "ext"), workspace::ActivateNextItem).count(), - VimCommand::new(("bN", "ext"), workspace::ActivatePreviousItem).count(), - VimCommand::new(("bp", "revious"), workspace::ActivatePreviousItem).count(), - VimCommand::new(("bf", "irst"), workspace::ActivateItem(0)), - VimCommand::new(("br", "ewind"), workspace::ActivateItem(0)), - VimCommand::new(("bl", "ast"), workspace::ActivateLastItem), - VimCommand::str(("buffers", ""), "tab_switcher::ToggleAll"), - VimCommand::str(("ls", ""), "tab_switcher::ToggleAll"), - VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal), - VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical), - VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(), - VimCommand::new(("tabp", "revious"), workspace::ActivatePreviousItem).count(), - VimCommand::new(("tabN", "ext"), workspace::ActivatePreviousItem).count(), - VimCommand::new( - ("tabc", "lose"), - workspace::CloseActiveItem { - save_intent: Some(SaveIntent::Close), - close_pinned: false, - }, - ), - VimCommand::new( - ("tabo", "nly"), - workspace::CloseOtherItems { - save_intent: Some(SaveIntent::Close), - close_pinned: false, - }, - ) - .bang(workspace::CloseOtherItems { - save_intent: Some(SaveIntent::Skip), - close_pinned: false, - }), - VimCommand::new( - ("on", "ly"), - workspace::CloseInactiveTabsAndPanes { - save_intent: Some(SaveIntent::Close), - }, - ) - .bang(workspace::CloseInactiveTabsAndPanes { - save_intent: Some(SaveIntent::Skip), - }), - VimCommand::str(("cl", "ist"), "diagnostics::Deploy"), - VimCommand::new(("cc", ""), editor::actions::Hover), - VimCommand::new(("ll", ""), editor::actions::Hover), - VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic::default()) - .range(wrap_count), - VimCommand::new( - ("cp", "revious"), - editor::actions::GoToPreviousDiagnostic::default(), - ) - .range(wrap_count), - VimCommand::new( - ("cN", "ext"), - editor::actions::GoToPreviousDiagnostic::default(), - ) - .range(wrap_count), - VimCommand::new( - ("lp", "revious"), - editor::actions::GoToPreviousDiagnostic::default(), - ) - .range(wrap_count), - VimCommand::new( - ("lN", "ext"), - editor::actions::GoToPreviousDiagnostic::default(), - ) - .range(wrap_count), - VimCommand::new(("j", "oin"), JoinLines).range(select_range), - VimCommand::new(("fo", "ld"), editor::actions::FoldSelectedRanges).range(act_on_range), - VimCommand::new(("foldo", "pen"), editor::actions::UnfoldLines) - .bang(editor::actions::UnfoldRecursive) - .range(act_on_range), - VimCommand::new(("foldc", "lose"), editor::actions::Fold) - .bang(editor::actions::FoldRecursive) - .range(act_on_range), - VimCommand::new(("dif", "fupdate"), editor::actions::ToggleSelectedDiffHunks) - .range(act_on_range), - VimCommand::str(("rev", "ert"), "git::Restore").range(act_on_range), - VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range), - VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| { - Some( - YankCommand { - range: range.clone(), - } - .boxed_clone(), - ) - }), - VimCommand::new(("reg", "isters"), ToggleRegistersView).bang(ToggleRegistersView), - VimCommand::new(("di", "splay"), ToggleRegistersView).bang(ToggleRegistersView), - VimCommand::new(("marks", ""), ToggleMarksView).bang(ToggleMarksView), - VimCommand::new(("delm", "arks"), ArgumentRequired) - .bang(DeleteMarks::AllLocal) - .args(|_, args| Some(DeleteMarks::Marks(args).boxed_clone())), - VimCommand::new(("sor", "t"), SortLinesCaseSensitive) - .range(select_range) - .default_range(CommandRange::buffer()), - VimCommand::new(("sort i", ""), SortLinesCaseInsensitive) - .range(select_range) - .default_range(CommandRange::buffer()), - VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"), - VimCommand::str(("H", "explore"), "project_panel::ToggleFocus"), - VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"), - VimCommand::str(("S", "explore"), "project_panel::ToggleFocus"), - VimCommand::str(("Ve", "xplore"), "project_panel::ToggleFocus"), - VimCommand::str(("te", "rm"), "terminal_panel::Toggle"), - VimCommand::str(("T", "erm"), "terminal_panel::Toggle"), - VimCommand::str(("C", "ollab"), "collab_panel::ToggleFocus"), - VimCommand::str(("No", "tifications"), "notification_panel::ToggleFocus"), - VimCommand::str(("A", "I"), "agent::ToggleFocus"), - VimCommand::str(("G", "it"), "git_panel::ToggleFocus"), - VimCommand::str(("D", "ebug"), "debug_panel::ToggleFocus"), - VimCommand::new(("noh", "lsearch"), search::buffer_search::Dismiss), - VimCommand::new(("$", ""), EndOfDocument), - VimCommand::new(("%", ""), EndOfDocument), - VimCommand::new(("0", ""), StartOfDocument), - VimCommand::new(("ex", ""), editor::actions::ReloadFile).bang(editor::actions::ReloadFile), - VimCommand::new(("cpp", "link"), editor::actions::CopyPermalinkToLine).range(act_on_range), - VimCommand::str(("opt", "ions"), "zed::OpenDefaultSettings"), - VimCommand::str(("map", ""), "vim::OpenDefaultKeymap"), - VimCommand::new(("h", "elp"), OpenDocs), - ] -} - -struct VimCommands(Vec); -// safety: we only ever access this from the main thread (as ensured by the cx argument) -// actions are not Sync so we can't otherwise use a OnceLock. -unsafe impl Sync for VimCommands {} -impl Global for VimCommands {} - -fn commands(cx: &App) -> &Vec { - static COMMANDS: OnceLock = OnceLock::new(); - &COMMANDS - .get_or_init(|| VimCommands(generate_commands(cx))) - .0 -} - -fn act_on_range(action: Box, range: &CommandRange) -> Option> { - Some( - WithRange { - restore_selection: true, - range: range.clone(), - action: WrappedAction(action), - } - .boxed_clone(), - ) -} - -fn select_range(action: Box, range: &CommandRange) -> Option> { - Some( - WithRange { - restore_selection: false, - range: range.clone(), - action: WrappedAction(action), - } - .boxed_clone(), - ) -} - -fn wrap_count(action: Box, range: &CommandRange) -> Option> { - range.as_count().map(|count| { - WithCount { - count, - action: WrappedAction(action), - } - .boxed_clone() - }) -} - -pub fn command_interceptor( - mut input: &str, - workspace: WeakEntity, - cx: &mut App, -) -> Task { - while input.starts_with(':') { - input = &input[1..]; - } - - let (range, query) = VimCommand::parse_range(input); - let range_prefix = input[0..(input.len() - query.len())].to_string(); - let has_trailing_space = query.ends_with(" "); - let mut query = query.as_str().trim(); - - let on_matching_lines = (query.starts_with('g') || query.starts_with('v')) - .then(|| { - let (pattern, range, search, invert) = OnMatchingLines::parse(query, &range)?; - let start_idx = query.len() - pattern.len(); - query = query[start_idx..].trim(); - Some((range, search, invert)) - }) - .flatten(); - - let mut action = if range.is_some() && query.is_empty() { - Some( - GoToLine { - range: range.clone().unwrap(), - } - .boxed_clone(), - ) - } else if query.starts_with('/') || query.starts_with('?') { - Some( - FindCommand { - query: query[1..].to_string(), - backwards: query.starts_with('?'), - } - .boxed_clone(), - ) - } else if query.starts_with("se ") || query.starts_with("set ") { - let (prefix, option) = query.split_once(' ').unwrap(); - let mut commands = VimOption::possible_commands(option); - if !commands.is_empty() { - let query = prefix.to_string() + " " + option; - for command in &mut commands { - command.positions = generate_positions(&command.string, &query); - } - } - return Task::ready(CommandInterceptResult { - results: commands, - exclusive: false, - }); - } else if query.starts_with('s') { - let mut substitute = "substitute".chars().peekable(); - let mut query = query.chars().peekable(); - while substitute - .peek() - .is_some_and(|char| Some(char) == query.peek()) - { - substitute.next(); - query.next(); - } - if let Some(replacement) = Replacement::parse(query) { - let range = range.clone().unwrap_or(CommandRange { - start: Position::CurrentLine { offset: 0 }, - end: None, - }); - Some(ReplaceCommand { replacement, range }.boxed_clone()) - } else { - None - } - } else if query.contains('!') { - ShellExec::parse(query, range.clone()) - } else if on_matching_lines.is_some() { - commands(cx) - .iter() - .find_map(|command| command.parse(query, &range, cx)) - } else { - None - }; - - if let Some((range, search, invert)) = on_matching_lines - && let Some(ref inner) = action - { - action = Some(Box::new(OnMatchingLines { - range, - search, - action: WrappedAction(inner.boxed_clone()), - invert, - })); - }; - - if let Some(action) = action { - let string = input.to_string(); - let positions = generate_positions(&string, &(range_prefix + query)); - return Task::ready(CommandInterceptResult { - results: vec![CommandInterceptItem { - action, - string, - positions, - }], - exclusive: false, - }); - } - - let Some((mut results, filenames)) = - commands(cx).iter().enumerate().find_map(|(idx, command)| { - let action = command.parse(query, &range, cx)?; - let parsed_query = command.get_parsed_query(query.into())?; - let display_string = ":".to_owned() - + &range_prefix - + command.prefix - + command.suffix - + if parsed_query.has_bang { "!" } else { "" }; - let space = if parsed_query.has_space { " " } else { "" }; - - let string = format!("{}{}{}", &display_string, &space, &parsed_query.args); - let positions = generate_positions(&string, &(range_prefix.clone() + query)); - - let results = vec![CommandInterceptItem { - action, - string, - positions, - }]; - - let no_args_positions = - generate_positions(&display_string, &(range_prefix.clone() + query)); - - // The following are valid autocomplete scenarios: - // :w!filename.txt - // :w filename.txt - // :w[space] - if !command.has_filename - || (!has_trailing_space && !parsed_query.has_bang && parsed_query.args.is_empty()) - { - return Some((results, None)); - } - - Some(( - results, - Some((idx, parsed_query, display_string, no_args_positions)), - )) - }) - else { - return Task::ready(CommandInterceptResult::default()); - }; - - if let Some((cmd_idx, parsed_query, display_string, no_args_positions)) = filenames { - let filenames = VimCommand::generate_filename_completions(&parsed_query, workspace, cx); - cx.spawn(async move |cx| { - let filenames = filenames.await; - const MAX_RESULTS: usize = 100; - let executor = cx.background_executor().clone(); - let mut candidates = Vec::with_capacity(filenames.len()); - - for (idx, filename) in filenames.iter().enumerate() { - candidates.push(fuzzy::StringMatchCandidate::new(idx, &filename)); - } - let filenames = fuzzy::match_strings( - &candidates, - &parsed_query.args, - false, - true, - MAX_RESULTS, - &Default::default(), - executor, - ) - .await; - - for fuzzy::StringMatch { - candidate_id: _, - score: _, - positions, - string, - } in filenames - { - let offset = display_string.len() + 1; - let mut positions: Vec<_> = positions.iter().map(|&pos| pos + offset).collect(); - positions.splice(0..0, no_args_positions.clone()); - let string = format!("{display_string} {string}"); - let (range, query) = VimCommand::parse_range(&string[1..]); - let action = - match cx.update(|cx| commands(cx).get(cmd_idx)?.parse(&query, &range, cx)) { - Ok(Some(action)) => action, - _ => continue, - }; - results.push(CommandInterceptItem { - action, - string, - positions, - }); - } - CommandInterceptResult { - results, - exclusive: true, - } - }) - } else { - Task::ready(CommandInterceptResult { - results, - exclusive: false, - }) - } -} - -fn generate_positions(string: &str, query: &str) -> Vec { - let mut positions = Vec::new(); - let mut chars = query.chars(); - - let Some(mut current) = chars.next() else { - return positions; - }; - - for (i, c) in string.char_indices() { - if c == current { - positions.push(i); - if let Some(c) = chars.next() { - current = c; - } else { - break; - } - } - } - - positions -} - -/// Applies a command to all lines matching a pattern. -#[derive(Debug, PartialEq, Clone, Action)] -#[action(namespace = vim, no_json, no_register)] -pub(crate) struct OnMatchingLines { - range: CommandRange, - search: String, - action: WrappedAction, - invert: bool, -} - -impl OnMatchingLines { - // convert a vim query into something more usable by zed. - // we don't attempt to fully convert between the two regex syntaxes, - // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern, - // and convert \0..\9 to $0..$9 in the replacement so that common idioms work. - pub(crate) fn parse( - query: &str, - range: &Option, - ) -> Option<(String, CommandRange, String, bool)> { - let mut global = "global".chars().peekable(); - let mut query_chars = query.chars().peekable(); - let mut invert = false; - if query_chars.peek() == Some(&'v') { - invert = true; - query_chars.next(); - } - while global - .peek() - .is_some_and(|char| Some(char) == query_chars.peek()) - { - global.next(); - query_chars.next(); - } - if !invert && query_chars.peek() == Some(&'!') { - invert = true; - query_chars.next(); - } - let range = range.clone().unwrap_or(CommandRange { - start: Position::Line { row: 0, offset: 0 }, - end: Some(Position::LastLine { offset: 0 }), - }); - - let delimiter = query_chars.next().filter(|c| { - !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'' && *c != '!' - })?; - - let mut search = String::new(); - let mut escaped = false; - - for c in query_chars.by_ref() { - if escaped { - escaped = false; - // unescape escaped parens - if c != '(' && c != ')' && c != delimiter { - search.push('\\') - } - search.push(c) - } else if c == '\\' { - escaped = true; - } else if c == delimiter { - break; - } else { - // escape unescaped parens - if c == '(' || c == ')' { - search.push('\\') - } - search.push(c) - } - } - - Some((query_chars.collect::(), range, search, invert)) - } - - pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context) { - let result = vim.update_editor(cx, |vim, editor, cx| { - self.range.buffer_range(vim, editor, window, cx) - }); - - let range = match result { - None => return, - Some(e @ Err(_)) => { - let Some(workspace) = vim.workspace(window) else { - return; - }; - workspace.update(cx, |workspace, cx| { - e.notify_err(workspace, cx); - }); - return; - } - Some(Ok(result)) => result, - }; - - let mut action = self.action.boxed_clone(); - let mut last_pattern = self.search.clone(); - - let mut regexes = match Regex::new(&self.search) { - Ok(regex) => vec![(regex, !self.invert)], - e @ Err(_) => { - let Some(workspace) = vim.workspace(window) else { - return; - }; - workspace.update(cx, |workspace, cx| { - e.notify_err(workspace, cx); - }); - return; - } - }; - while let Some(inner) = action - .boxed_clone() - .as_any() - .downcast_ref::() - { - let Some(regex) = Regex::new(&inner.search).ok() else { - break; - }; - last_pattern = inner.search.clone(); - action = inner.action.boxed_clone(); - regexes.push((regex, !inner.invert)) - } - - if let Some(pane) = vim.pane(window, cx) { - pane.update(cx, |pane, cx| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() - { - search_bar.update(cx, |search_bar, cx| { - if search_bar.show(window, cx) { - let _ = search_bar.search( - &last_pattern, - Some(SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE), - false, - window, - cx, - ); - } - }); - } - }); - }; - - vim.update_editor(cx, |_, editor, cx| { - let snapshot = editor.snapshot(window, cx); - let mut row = range.start.0; - - let point_range = Point::new(range.start.0, 0) - ..snapshot - .buffer_snapshot() - .clip_point(Point::new(range.end.0 + 1, 0), Bias::Left); - cx.spawn_in(window, async move |editor, cx| { - let new_selections = cx - .background_spawn(async move { - let mut line = String::new(); - let mut new_selections = Vec::new(); - let chunks = snapshot - .buffer_snapshot() - .text_for_range(point_range) - .chain(["\n"]); - - for chunk in chunks { - for (newline_ix, text) in chunk.split('\n').enumerate() { - if newline_ix > 0 { - if regexes.iter().all(|(regex, should_match)| { - regex.is_match(&line) == *should_match - }) { - new_selections - .push(Point::new(row, 0).to_display_point(&snapshot)) - } - row += 1; - line.clear(); - } - line.push_str(text) - } - } - - new_selections - }) - .await; - - if new_selections.is_empty() { - return; - } - editor - .update_in(cx, |editor, window, cx| { - editor.start_transaction_at(Instant::now(), window, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.replace_cursors_with(|_| new_selections); - }); - window.dispatch_action(action, cx); - cx.defer_in(window, move |editor, window, cx| { - let newest = editor - .selections - .newest::(&editor.display_snapshot(cx)); - editor.change_selections( - SelectionEffects::no_scroll(), - window, - cx, - |s| { - s.select(vec![newest]); - }, - ); - editor.end_transaction_at(Instant::now(), cx); - }) - }) - .ok(); - }) - .detach(); - }); - } -} - -/// Executes a shell command and returns the output. -#[derive(Clone, Debug, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -pub struct ShellExec { - command: String, - range: Option, - is_read: bool, -} - -impl Vim { - pub fn cancel_running_command(&mut self, window: &mut Window, cx: &mut Context) { - if self.running_command.take().is_some() { - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, _window, _cx| { - editor.clear_row_highlights::(); - }) - }); - } - } - - fn prepare_shell_command( - &mut self, - command: &str, - _: &mut Window, - cx: &mut Context, - ) -> String { - let mut ret = String::new(); - // N.B. non-standard escaping rules: - // * !echo % => "echo README.md" - // * !echo \% => "echo %" - // * !echo \\% => echo \% - // * !echo \\\% => echo \\% - for c in command.chars() { - if c != '%' && c != '!' { - ret.push(c); - continue; - } else if ret.chars().last() == Some('\\') { - ret.pop(); - ret.push(c); - continue; - } - match c { - '%' => { - self.update_editor(cx, |_, editor, cx| { - if let Some((_, buffer, _)) = editor.active_excerpt(cx) - && let Some(file) = buffer.read(cx).file() - && let Some(local) = file.as_local() - { - ret.push_str(&local.path().display(local.path_style(cx))); - } - }); - } - '!' => { - if let Some(command) = &self.last_command { - ret.push_str(command) - } - } - _ => {} - } - } - self.last_command = Some(ret.clone()); - ret - } - - pub fn shell_command_motion( - &mut self, - motion: Motion, - times: Option, - forced_motion: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - let Some(workspace) = self.workspace(window) else { - return; - }; - let command = self.update_editor(cx, |_, editor, cx| { - let snapshot = editor.snapshot(window, cx); - let start = editor - .selections - .newest_display(&editor.display_snapshot(cx)); - let text_layout_details = editor.text_layout_details(window); - let (mut range, _) = motion - .range( - &snapshot, - start.clone(), - times, - &text_layout_details, - forced_motion, - ) - .unwrap_or((start.range(), MotionKind::Exclusive)); - if range.start != start.start { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges([ - range.start.to_point(&snapshot)..range.start.to_point(&snapshot) - ]); - }) - } - if range.end.row() > range.start.row() && range.end.column() != 0 { - *range.end.row_mut() -= 1 - } - if range.end.row() == range.start.row() { - ".!".to_string() - } else { - format!(".,.+{}!", (range.end.row() - range.start.row()).0) - } - }); - if let Some(command) = command { - workspace.update(cx, |workspace, cx| { - command_palette::CommandPalette::toggle(workspace, &command, window, cx); - }); - } - } - - pub fn shell_command_object( - &mut self, - object: Object, - around: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - let Some(workspace) = self.workspace(window) else { - return; - }; - let command = self.update_editor(cx, |_, editor, cx| { - let snapshot = editor.snapshot(window, cx); - let start = editor - .selections - .newest_display(&editor.display_snapshot(cx)); - let range = object - .range(&snapshot, start.clone(), around, None) - .unwrap_or(start.range()); - if range.start != start.start { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges([ - range.start.to_point(&snapshot)..range.start.to_point(&snapshot) - ]); - }) - } - if range.end.row() == range.start.row() { - ".!".to_string() - } else { - format!(".,.+{}!", (range.end.row() - range.start.row()).0) - } - }); - if let Some(command) = command { - workspace.update(cx, |workspace, cx| { - command_palette::CommandPalette::toggle(workspace, &command, window, cx); - }); - } - } -} - -impl ShellExec { - pub fn parse(query: &str, range: Option) -> Option> { - let (before, after) = query.split_once('!')?; - let before = before.trim(); - - if !"read".starts_with(before) { - return None; - } - - Some( - ShellExec { - command: after.trim().to_string(), - range, - is_read: !before.is_empty(), - } - .boxed_clone(), - ) - } - - pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context) { - let Some(workspace) = vim.workspace(window) else { - return; - }; - - let project = workspace.read(cx).project().clone(); - let command = vim.prepare_shell_command(&self.command, window, cx); - - if self.range.is_none() && !self.is_read { - 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 = SpawnInTerminal { - id: TaskId("vim".to_string()), - full_label: command.clone(), - label: command.clone(), - command: Some(command.clone()), - args: Vec::new(), - command_label: command.clone(), - cwd, - env: HashMap::default(), - use_new_terminal: true, - allow_concurrent_runs: true, - reveal: RevealStrategy::NoFocus, - reveal_target: RevealTarget::Dock, - hide: HideStrategy::Never, - shell, - show_summary: false, - show_command: false, - show_rerun: false, - }; - - let task_status = workspace.spawn_in_terminal(spawn_in_terminal, window, cx); - cx.background_spawn(async move { - match task_status.await { - Some(Ok(status)) => { - if status.success() { - log::debug!("Vim shell exec succeeded"); - } else { - log::debug!("Vim shell exec failed, code: {:?}", status.code()); - } - } - Some(Err(e)) => log::error!("Vim shell exec failed: {e}"), - None => log::debug!("Vim shell exec got cancelled"), - } - }) - .detach(); - }); - return; - }; - - let mut input_snapshot = None; - let mut input_range = None; - let mut needs_newline_prefix = false; - vim.update_editor(cx, |vim, editor, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let range = if let Some(range) = self.range.clone() { - let Some(range) = range.buffer_range(vim, editor, window, cx).log_err() else { - return; - }; - Point::new(range.start.0, 0) - ..snapshot.clip_point(Point::new(range.end.0 + 1, 0), Bias::Right) - } else { - let mut end = editor - .selections - .newest::(&editor.display_snapshot(cx)) - .range() - .end; - end = snapshot.clip_point(Point::new(end.row + 1, 0), Bias::Right); - needs_newline_prefix = end == snapshot.max_point(); - end..end - }; - if self.is_read { - input_range = - Some(snapshot.anchor_after(range.end)..snapshot.anchor_after(range.end)); - } else { - input_range = - Some(snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end)); - } - editor.highlight_rows::( - input_range.clone().unwrap(), - cx.theme().status().unreachable_background, - Default::default(), - cx, - ); - - if !self.is_read { - input_snapshot = Some(snapshot) - } - }); - - let Some(range) = input_range else { return }; - - let process_task = project.update(cx, |project, cx| project.exec_in_shell(command, cx)); - - let is_read = self.is_read; - - let task = cx.spawn_in(window, async move |vim, cx| { - let Some(mut process) = process_task.await.log_err() else { - return; - }; - process.stdout(Stdio::piped()); - process.stderr(Stdio::piped()); - - if input_snapshot.is_some() { - process.stdin(Stdio::piped()); - } else { - process.stdin(Stdio::null()); - }; - - let Some(mut running) = process.spawn().log_err() else { - vim.update_in(cx, |vim, window, cx| { - vim.cancel_running_command(window, cx); - }) - .log_err(); - return; - }; - - if let Some(mut stdin) = running.stdin.take() - && let Some(snapshot) = input_snapshot - { - let range = range.clone(); - cx.background_spawn(async move { - for chunk in snapshot.text_for_range(range) { - if stdin.write_all(chunk.as_bytes()).await.log_err().is_none() { - return; - } - } - stdin.flush().await.log_err(); - }) - .detach(); - }; - - let output = cx.background_spawn(running.output()).await; - - let Some(output) = output.log_err() else { - vim.update_in(cx, |vim, window, cx| { - vim.cancel_running_command(window, cx); - }) - .log_err(); - return; - }; - let mut text = String::new(); - if needs_newline_prefix { - text.push('\n'); - } - text.push_str(&String::from_utf8_lossy(&output.stdout)); - text.push_str(&String::from_utf8_lossy(&output.stderr)); - if !text.is_empty() && text.chars().last() != Some('\n') { - text.push('\n'); - } - - vim.update_in(cx, |vim, window, cx| { - vim.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.edit([(range.clone(), text)], cx); - let snapshot = editor.buffer().read(cx).snapshot(cx); - editor.change_selections(Default::default(), window, cx, |s| { - let point = if is_read { - let point = range.end.to_point(&snapshot); - Point::new(point.row.saturating_sub(1), 0) - } else { - let point = range.start.to_point(&snapshot); - Point::new(point.row, 0) - }; - s.select_ranges([point..point]); - }) - }) - }); - vim.cancel_running_command(window, cx); - }) - .log_err(); - }); - vim.running_command.replace(task); - } -} - -#[cfg(test)] -mod test { - use std::path::{Path, PathBuf}; - - use crate::{ - VimAddon, - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - use editor::{Editor, EditorSettings}; - use gpui::{Context, TestAppContext}; - use indoc::indoc; - use settings::Settings; - use util::path; - use workspace::{OpenOptions, Workspace}; - - #[gpui::test] - async fn test_command_basics(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇa - b - c"}) - .await; - - cx.simulate_shared_keystrokes(": j enter").await; - - // hack: our cursor positioning after a join command is wrong - cx.simulate_shared_keystrokes("^").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇa b - c" - }); - } - - #[gpui::test] - async fn test_command_goto(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇa - b - c"}) - .await; - cx.simulate_shared_keystrokes(": 3 enter").await; - cx.shared_state().await.assert_eq(indoc! {" - a - b - ˇc"}); - } - - #[gpui::test] - async fn test_command_replace(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇa - b - b - c"}) - .await; - cx.simulate_shared_keystrokes(": % s / b / d enter").await; - cx.shared_state().await.assert_eq(indoc! {" - a - d - ˇd - c"}); - cx.simulate_shared_keystrokes(": % s : . : \\ 0 \\ 0 enter") - .await; - cx.shared_state().await.assert_eq(indoc! {" - aa - dd - dd - ˇcc"}); - cx.simulate_shared_keystrokes("k : s / d d / e e enter") - .await; - cx.shared_state().await.assert_eq(indoc! {" - aa - dd - ˇee - cc"}); - } - - #[gpui::test] - async fn test_command_search(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇa - b - a - c"}) - .await; - cx.simulate_shared_keystrokes(": / b enter").await; - cx.shared_state().await.assert_eq(indoc! {" - a - ˇb - a - c"}); - cx.simulate_shared_keystrokes(": ? a enter").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇa - b - a - c"}); - } - - #[gpui::test] - async fn test_command_write(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - let path = Path::new(path!("/root/dir/file.rs")); - let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone()); - - cx.simulate_keystrokes("i @ escape"); - cx.simulate_keystrokes(": w enter"); - - assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@\n"); - - fs.as_fake().insert_file(path, b"oops\n".to_vec()).await; - - // conflict! - cx.simulate_keystrokes("i @ escape"); - cx.simulate_keystrokes(": w enter"); - cx.simulate_prompt_answer("Cancel"); - - assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "oops\n"); - assert!(!cx.has_pending_prompt()); - cx.simulate_keystrokes(": w !"); - cx.simulate_keystrokes("enter"); - assert!(!cx.has_pending_prompt()); - assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@@\n"); - } - - #[gpui::test] - async fn test_command_quit(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.simulate_keystrokes(": n e w enter"); - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2)); - cx.simulate_keystrokes(": q enter"); - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 1)); - cx.simulate_keystrokes(": n e w enter"); - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2)); - cx.simulate_keystrokes(": q a enter"); - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 0)); - } - - #[gpui::test] - async fn test_offsets(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n") - .await; - - cx.simulate_shared_keystrokes(": + enter").await; - cx.shared_state() - .await - .assert_eq("1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n"); - - cx.simulate_shared_keystrokes(": 1 0 - enter").await; - cx.shared_state() - .await - .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n"); - - cx.simulate_shared_keystrokes(": . - 2 enter").await; - cx.shared_state() - .await - .assert_eq("1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n"); - - cx.simulate_shared_keystrokes(": % enter").await; - cx.shared_state() - .await - .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ"); - } - - #[gpui::test] - async fn test_command_ranges(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await; - - cx.simulate_shared_keystrokes(": 2 , 4 d enter").await; - cx.shared_state().await.assert_eq("1\nˇ4\n3\n2\n1"); - - cx.simulate_shared_keystrokes(": 2 , 4 s o r t enter").await; - cx.shared_state().await.assert_eq("1\nˇ2\n3\n4\n1"); - - cx.simulate_shared_keystrokes(": 2 , 4 j o i n enter").await; - cx.shared_state().await.assert_eq("1\nˇ2 3 4\n1"); - } - - #[gpui::test] - async fn test_command_visual_replace(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await; - - cx.simulate_shared_keystrokes("v 2 j : s / . / k enter") - .await; - cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1"); - } - - #[track_caller] - fn assert_active_item( - workspace: &mut Workspace, - expected_path: &str, - expected_text: &str, - cx: &mut Context, - ) { - let active_editor = workspace.active_item_as::(cx).unwrap(); - - let buffer = active_editor - .read(cx) - .buffer() - .read(cx) - .as_singleton() - .unwrap(); - - let text = buffer.read(cx).text(); - let file = buffer.read(cx).file().unwrap(); - let file_path = file.as_local().unwrap().abs_path(cx); - - assert_eq!(text, expected_text); - assert_eq!(file_path, Path::new(expected_path)); - } - - #[gpui::test] - async fn test_command_gf(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Assert base state, that we're in /root/dir/file.rs - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx); - }); - - // Insert a new file - let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone()); - fs.as_fake() - .insert_file( - path!("/root/dir/file2.rs"), - "This is file2.rs".as_bytes().to_vec(), - ) - .await; - fs.as_fake() - .insert_file( - path!("/root/dir/file3.rs"), - "go to file3".as_bytes().to_vec(), - ) - .await; - - // Put the path to the second file into the currently open buffer - cx.set_state(indoc! {"go to fiˇle2.rs"}, Mode::Normal); - - // Go to file2.rs - cx.simulate_keystrokes("g f"); - - // We now have two items - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2)); - cx.workspace(|workspace, _, cx| { - assert_active_item( - workspace, - path!("/root/dir/file2.rs"), - "This is file2.rs", - cx, - ); - }); - - // Update editor to point to `file2.rs` - cx.editor = - cx.workspace(|workspace, _, cx| workspace.active_item_as::(cx).unwrap()); - - // Put the path to the third file into the currently open buffer, - // but remove its suffix, because we want that lookup to happen automatically. - cx.set_state(indoc! {"go to fiˇle3"}, Mode::Normal); - - // Go to file3.rs - cx.simulate_keystrokes("g f"); - - // We now have three items - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3)); - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/dir/file3.rs"), "go to file3", cx); - }); - } - - #[gpui::test] - async fn test_command_write_filename(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx); - }); - - cx.simulate_keystrokes(": w space other.rs"); - cx.simulate_keystrokes("enter"); - - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/other.rs"), "", cx); - }); - - cx.simulate_keystrokes(": w space dir/file.rs"); - cx.simulate_keystrokes("enter"); - - cx.simulate_prompt_answer("Replace"); - cx.run_until_parked(); - - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx); - }); - - cx.simulate_keystrokes(": w ! space other.rs"); - cx.simulate_keystrokes("enter"); - - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/other.rs"), "", cx); - }); - } - - #[gpui::test] - async fn test_command_write_range(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx); - }); - - cx.set_state( - indoc! {" - The quick - brown« fox - jumpsˇ» over - the lazy dog - "}, - Mode::Visual, - ); - - cx.simulate_keystrokes(": w space dir/other.rs"); - cx.simulate_keystrokes("enter"); - - let other = path!("/root/dir/other.rs"); - - let _ = cx - .workspace(|workspace, window, cx| { - workspace.open_abs_path(PathBuf::from(other), OpenOptions::default(), window, cx) - }) - .await; - - cx.workspace(|workspace, _, cx| { - assert_active_item( - workspace, - other, - indoc! {" - brown fox - jumps over - "}, - cx, - ); - }); - } - - #[gpui::test] - async fn test_command_matching_lines(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇa - b - a - b - a - "}) - .await; - - cx.simulate_shared_keystrokes(":").await; - cx.simulate_shared_keystrokes("g / a / d").await; - cx.simulate_shared_keystrokes("enter").await; - - cx.shared_state().await.assert_eq(indoc! {" - b - b - ˇ"}); - - cx.simulate_shared_keystrokes("u").await; - - cx.shared_state().await.assert_eq(indoc! {" - ˇa - b - a - b - a - "}); - - cx.simulate_shared_keystrokes(":").await; - cx.simulate_shared_keystrokes("v / a / d").await; - cx.simulate_shared_keystrokes("enter").await; - - cx.shared_state().await.assert_eq(indoc! {" - a - a - ˇa"}); - } - - #[gpui::test] - async fn test_del_marks(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇa - b - a - b - a - "}) - .await; - - cx.simulate_shared_keystrokes("m a").await; - - let mark = cx.update_editor(|editor, window, cx| { - let vim = editor.addon::().unwrap().entity.clone(); - vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx)) - }); - assert!(mark.is_some()); - - cx.simulate_shared_keystrokes(": d e l m space a").await; - cx.simulate_shared_keystrokes("enter").await; - - let mark = cx.update_editor(|editor, window, cx| { - let vim = editor.addon::().unwrap().entity.clone(); - vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx)) - }); - assert!(mark.is_none()) - } - - #[gpui::test] - async fn test_normal_command(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - The quick - brown« fox - jumpsˇ» over - the lazy dog - "}) - .await; - - cx.simulate_shared_keystrokes(": n o r m space w C w o r d") - .await; - cx.simulate_shared_keystrokes("enter").await; - - cx.shared_state().await.assert_eq(indoc! {" - The quick - brown word - jumps worˇd - the lazy dog - "}); - - cx.simulate_shared_keystrokes(": n o r m space _ w c i w t e s t") - .await; - cx.simulate_shared_keystrokes("enter").await; - - cx.shared_state().await.assert_eq(indoc! {" - The quick - brown word - jumps tesˇt - the lazy dog - "}); - - cx.simulate_shared_keystrokes("_ l v l : n o r m space s l a") - .await; - cx.simulate_shared_keystrokes("enter").await; - - cx.shared_state().await.assert_eq(indoc! {" - The quick - brown word - lˇaumps test - the lazy dog - "}); - - cx.set_shared_state(indoc! {" - ˇThe quick - brown fox - jumps over - the lazy dog - "}) - .await; - - cx.simulate_shared_keystrokes("c i w M y escape").await; - - cx.shared_state().await.assert_eq(indoc! {" - Mˇy quick - brown fox - jumps over - the lazy dog - "}); - - cx.simulate_shared_keystrokes(": n o r m space u").await; - cx.simulate_shared_keystrokes("enter").await; - - cx.shared_state().await.assert_eq(indoc! {" - ˇThe quick - brown fox - jumps over - the lazy dog - "}); - // Once ctrl-v to input character literals is added there should be a test for redo - } - - #[gpui::test] - async fn test_command_tabnew(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Create a new file to ensure that, when the filename is used with - // `:tabnew`, it opens the existing file in a new tab. - let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone()); - fs.as_fake() - .insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec()) - .await; - - cx.simulate_keystrokes(": tabnew"); - cx.simulate_keystrokes("enter"); - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2)); - - // Assert that the new tab is empty and not associated with any file, as - // no file path was provided to the `:tabnew` command. - cx.workspace(|workspace, _window, cx| { - let active_editor = workspace.active_item_as::(cx).unwrap(); - let buffer = active_editor - .read(cx) - .buffer() - .read(cx) - .as_singleton() - .unwrap(); - - assert!(&buffer.read(cx).file().is_none()); - }); - - // Leverage the filename as an argument to the `:tabnew` command, - // ensuring that the file, instead of an empty buffer, is opened in a - // new tab. - cx.simulate_keystrokes(": tabnew space dir/file_2.rs"); - cx.simulate_keystrokes("enter"); - - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3)); - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx); - }); - - // If the `filename` argument provided to the `:tabnew` command is for a - // file that doesn't yet exist, it should still associate the buffer - // with that file path, so that when the buffer contents are saved, the - // file is created. - cx.simulate_keystrokes(": tabnew space dir/file_3.rs"); - cx.simulate_keystrokes("enter"); - - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4)); - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx); - }); - } - - #[gpui::test] - async fn test_command_tabedit(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Create a new file to ensure that, when the filename is used with - // `:tabedit`, it opens the existing file in a new tab. - let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone()); - fs.as_fake() - .insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec()) - .await; - - cx.simulate_keystrokes(": tabedit"); - cx.simulate_keystrokes("enter"); - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2)); - - // Assert that the new tab is empty and not associated with any file, as - // no file path was provided to the `:tabedit` command. - cx.workspace(|workspace, _window, cx| { - let active_editor = workspace.active_item_as::(cx).unwrap(); - let buffer = active_editor - .read(cx) - .buffer() - .read(cx) - .as_singleton() - .unwrap(); - - assert!(&buffer.read(cx).file().is_none()); - }); - - // Leverage the filename as an argument to the `:tabedit` command, - // ensuring that the file, instead of an empty buffer, is opened in a - // new tab. - cx.simulate_keystrokes(": tabedit space dir/file_2.rs"); - cx.simulate_keystrokes("enter"); - - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3)); - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx); - }); - - // If the `filename` argument provided to the `:tabedit` command is for a - // file that doesn't yet exist, it should still associate the buffer - // with that file path, so that when the buffer contents are saved, the - // file is created. - cx.simulate_keystrokes(": tabedit space dir/file_3.rs"); - cx.simulate_keystrokes("enter"); - - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4)); - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx); - }); - } - - #[gpui::test] - async fn test_ignorecase_command(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.read(|cx| { - assert_eq!( - EditorSettings::get_global(cx).search.case_sensitive, - false, - "The `case_sensitive` setting should be `false` by default." - ); - }); - cx.simulate_keystrokes(": set space noignorecase"); - cx.simulate_keystrokes("enter"); - cx.read(|cx| { - assert_eq!( - EditorSettings::get_global(cx).search.case_sensitive, - true, - "The `case_sensitive` setting should have been enabled with `:set noignorecase`." - ); - }); - cx.simulate_keystrokes(": set space ignorecase"); - cx.simulate_keystrokes("enter"); - cx.read(|cx| { - assert_eq!( - EditorSettings::get_global(cx).search.case_sensitive, - false, - "The `case_sensitive` setting should have been disabled with `:set ignorecase`." - ); - }); - cx.simulate_keystrokes(": set space noic"); - cx.simulate_keystrokes("enter"); - cx.read(|cx| { - assert_eq!( - EditorSettings::get_global(cx).search.case_sensitive, - true, - "The `case_sensitive` setting should have been enabled with `:set noic`." - ); - }); - cx.simulate_keystrokes(": set space ic"); - cx.simulate_keystrokes("enter"); - cx.read(|cx| { - assert_eq!( - EditorSettings::get_global(cx).search.case_sensitive, - false, - "The `case_sensitive` setting should have been disabled with `:set ic`." - ); - }); - } - - #[gpui::test] - async fn test_sort_commands(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - «hornet - quirrel - elderbug - cornifer - idaˇ» - "}, - Mode::Visual, - ); - - cx.simulate_keystrokes(": sort"); - cx.simulate_keystrokes("enter"); - - cx.assert_state( - indoc! {" - ˇcornifer - elderbug - hornet - ida - quirrel - "}, - Mode::Normal, - ); - - // Assert that, by default, `:sort` takes case into consideration. - cx.set_state( - indoc! {" - «hornet - quirrel - Elderbug - cornifer - idaˇ» - "}, - Mode::Visual, - ); - - cx.simulate_keystrokes(": sort"); - cx.simulate_keystrokes("enter"); - - cx.assert_state( - indoc! {" - ˇElderbug - cornifer - hornet - ida - quirrel - "}, - Mode::Normal, - ); - - // Assert that, if the `i` option is passed, `:sort` ignores case. - cx.set_state( - indoc! {" - «hornet - quirrel - Elderbug - cornifer - idaˇ» - "}, - Mode::Visual, - ); - - cx.simulate_keystrokes(": sort space i"); - cx.simulate_keystrokes("enter"); - - cx.assert_state( - indoc! {" - ˇcornifer - Elderbug - hornet - ida - quirrel - "}, - Mode::Normal, - ); - - // When no range is provided, sorts the whole buffer. - cx.set_state( - indoc! {" - ˇhornet - quirrel - elderbug - cornifer - ida - "}, - Mode::Normal, - ); - - cx.simulate_keystrokes(": sort"); - cx.simulate_keystrokes("enter"); - - cx.assert_state( - indoc! {" - ˇcornifer - elderbug - hornet - ida - quirrel - "}, - Mode::Normal, - ); - } -} diff --git a/crates/vim/src/digraph.rs b/crates/vim/src/digraph.rs deleted file mode 100644 index 39014fea5b..0000000000 --- a/crates/vim/src/digraph.rs +++ /dev/null @@ -1,365 +0,0 @@ -use std::sync::Arc; - -use collections::HashMap; -use editor::Editor; -use gpui::{Action, App, Context, Keystroke, KeystrokeEvent, Window}; -use schemars::JsonSchema; -use serde::Deserialize; -use settings::Settings; -use std::sync::LazyLock; - -use crate::{Vim, VimSettings, state::Operator}; - -mod default; - -#[derive(Debug, Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -struct Literal(String, char); - -pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, Vim::literal) -} - -static DEFAULT_DIGRAPHS_MAP: LazyLock>> = LazyLock::new(|| { - let mut map = HashMap::default(); - for &(a, b, c) in default::DEFAULT_DIGRAPHS { - let key = format!("{a}{b}"); - let value = char::from_u32(c).unwrap().to_string().into(); - map.insert(key, value); - } - map -}); - -fn lookup_digraph(a: char, b: char, cx: &App) -> Arc { - let custom_digraphs = &VimSettings::get_global(cx).custom_digraphs; - let input = format!("{a}{b}"); - let reversed = format!("{b}{a}"); - - custom_digraphs - .get(&input) - .or_else(|| DEFAULT_DIGRAPHS_MAP.get(&input)) - .or_else(|| custom_digraphs.get(&reversed)) - .or_else(|| DEFAULT_DIGRAPHS_MAP.get(&reversed)) - .cloned() - .unwrap_or_else(|| b.to_string().into()) -} - -impl Vim { - pub fn insert_digraph( - &mut self, - first_char: char, - second_char: char, - window: &mut Window, - cx: &mut Context, - ) { - let text = lookup_digraph(first_char, second_char, cx); - - self.pop_operator(window, cx); - if self.editor_input_enabled() { - self.update_editor(cx, |_, editor, cx| editor.insert(&text, window, cx)); - } else { - self.input_ignored(text, window, cx); - } - } - - fn literal(&mut self, action: &Literal, window: &mut Window, cx: &mut Context) { - match self.active_operator() { - Some(Operator::Literal { - prefix: Some(prefix), - }) => { - if let Some(keystroke) = Keystroke::parse(&action.0).ok() { - window.defer(cx, |window, cx| { - window.dispatch_keystroke(keystroke, cx); - }); - } - return self.handle_literal_input(prefix, "", window, cx); - } - Some(_) => self.insert_literal(Some(action.1), "", window, cx), - None => log::error!( - "Literal called when no operator was on the stack. This likely means there is an invalid keymap config" - ), - } - } - - pub fn handle_literal_keystroke( - &mut self, - keystroke_event: &KeystrokeEvent, - prefix: String, - window: &mut Window, - cx: &mut Context, - ) { - // handled by handle_literal_input - if keystroke_event.keystroke.key_char.is_some() { - return; - }; - - if !prefix.is_empty() { - self.handle_literal_input(prefix, "", window, cx); - } else { - self.pop_operator(window, cx); - } - - // give another chance to handle the binding outside - // of waiting mode. - if keystroke_event.action.is_none() { - let keystroke = keystroke_event.keystroke.clone(); - window.defer(cx, |window, cx| { - window.dispatch_keystroke(keystroke, cx); - }); - } - } - - pub fn handle_literal_input( - &mut self, - mut prefix: String, - text: &str, - window: &mut Window, - cx: &mut Context, - ) { - let first = prefix.chars().next(); - let next = text.chars().next().unwrap_or(' '); - match first { - Some('o' | 'O') => { - if next.is_digit(8) { - prefix.push(next); - if prefix.len() == 4 { - let ch: char = u8::from_str_radix(&prefix[1..], 8).unwrap_or(255).into(); - return self.insert_literal(Some(ch), "", window, cx); - } - } else { - let ch = if prefix.len() > 1 { - Some(u8::from_str_radix(&prefix[1..], 8).unwrap_or(255).into()) - } else { - None - }; - return self.insert_literal(ch, text, window, cx); - } - } - Some('x' | 'X' | 'u' | 'U') => { - let max_len = match first.unwrap() { - 'x' => 3, - 'X' => 3, - 'u' => 5, - 'U' => 9, - _ => unreachable!(), - }; - if next.is_ascii_hexdigit() { - prefix.push(next); - if prefix.len() == max_len { - let ch: char = u32::from_str_radix(&prefix[1..], 16) - .ok() - .and_then(|n| n.try_into().ok()) - .unwrap_or('\u{FFFD}'); - return self.insert_literal(Some(ch), "", window, cx); - } - } else { - let ch = if prefix.len() > 1 { - Some( - u32::from_str_radix(&prefix[1..], 16) - .ok() - .and_then(|n| n.try_into().ok()) - .unwrap_or('\u{FFFD}'), - ) - } else { - None - }; - return self.insert_literal(ch, text, window, cx); - } - } - Some('0'..='9') => { - if next.is_ascii_hexdigit() { - prefix.push(next); - if prefix.len() == 3 { - let ch: char = u8::from_str_radix(&prefix, 10).unwrap_or(255).into(); - return self.insert_literal(Some(ch), "", window, cx); - } - } else { - let ch: char = u8::from_str_radix(&prefix, 10).unwrap_or(255).into(); - return self.insert_literal(Some(ch), "", window, cx); - } - } - None if matches!(next, 'o' | 'O' | 'x' | 'X' | 'u' | 'U' | '0'..='9') => { - prefix.push(next) - } - _ => { - return self.insert_literal(None, text, window, cx); - } - }; - - self.pop_operator(window, cx); - self.push_operator( - Operator::Literal { - prefix: Some(prefix), - }, - window, - cx, - ); - } - - fn insert_literal( - &mut self, - ch: Option, - suffix: &str, - window: &mut Window, - cx: &mut Context, - ) { - self.pop_operator(window, cx); - let mut text = String::new(); - if let Some(c) = ch { - if c == '\n' { - text.push('\x00') - } else { - text.push(c) - } - } - text.push_str(suffix); - - if self.editor_input_enabled() { - self.update_editor(cx, |_, editor, cx| editor.insert(&text, window, cx)); - } else { - self.input_ignored(text.into(), window, cx); - } - } -} - -#[cfg(test)] -mod test { - use collections::HashMap; - use settings::SettingsStore; - - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - - #[gpui::test] - async fn test_digraph_insert_mode(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("Hellˇo").await; - cx.simulate_shared_keystrokes("a ctrl-k o : escape").await; - cx.shared_state().await.assert_eq("Helloˇö"); - - cx.set_shared_state("Hellˇo").await; - cx.simulate_shared_keystrokes("a ctrl-k : o escape").await; - cx.shared_state().await.assert_eq("Helloˇö"); - - cx.set_shared_state("Hellˇo").await; - cx.simulate_shared_keystrokes("i ctrl-k o : escape").await; - cx.shared_state().await.assert_eq("Hellˇöo"); - } - - #[gpui::test] - async fn test_digraph_insert_multicursor(cx: &mut gpui::TestAppContext) { - let mut cx: VimTestContext = VimTestContext::new(cx, true).await; - - cx.set_state("Hellˇo wˇorld", Mode::Normal); - cx.simulate_keystrokes("a ctrl-k o : escape"); - cx.assert_state("Helloˇö woˇörld", Mode::Normal); - } - - #[gpui::test] - async fn test_digraph_replace(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("Hellˇo").await; - cx.simulate_shared_keystrokes("r ctrl-k o :").await; - cx.shared_state().await.assert_eq("Hellˇö"); - } - - #[gpui::test] - async fn test_digraph_find(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇHellö world").await; - cx.simulate_shared_keystrokes("f ctrl-k o :").await; - cx.shared_state().await.assert_eq("Hellˇö world"); - - cx.set_shared_state("ˇHellö world").await; - cx.simulate_shared_keystrokes("t ctrl-k o :").await; - cx.shared_state().await.assert_eq("Helˇlö world"); - } - - #[gpui::test] - async fn test_digraph_replace_mode(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇHello").await; - cx.simulate_shared_keystrokes( - "shift-r ctrl-k a ' ctrl-k e ` ctrl-k i : ctrl-k o ~ ctrl-k u - escape", - ) - .await; - cx.shared_state().await.assert_eq("áèïõˇū"); - } - - #[gpui::test] - async fn test_digraph_custom(cx: &mut gpui::TestAppContext) { - let mut cx: VimTestContext = VimTestContext::new(cx, true).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - let mut custom_digraphs = HashMap::default(); - custom_digraphs.insert("|-".into(), "⊢".into()); - custom_digraphs.insert(":)".into(), "👨‍💻".into()); - s.vim.get_or_insert_default().custom_digraphs = Some(custom_digraphs); - }); - }); - - cx.set_state("ˇ", Mode::Normal); - cx.simulate_keystrokes("a ctrl-k | - escape"); - cx.assert_state("ˇ⊢", Mode::Normal); - - // Test support for multi-codepoint mappings - cx.set_state("ˇ", Mode::Normal); - cx.simulate_keystrokes("a ctrl-k : ) escape"); - cx.assert_state("ˇ👨‍💻", Mode::Normal); - } - - #[gpui::test] - async fn test_digraph_keymap_conflict(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("Hellˇo").await; - cx.simulate_shared_keystrokes("a ctrl-k s , escape").await; - cx.shared_state().await.assert_eq("Helloˇş"); - } - - #[gpui::test] - async fn test_ctrl_v(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇ").await; - cx.simulate_shared_keystrokes("i ctrl-v 0 0 0").await; - cx.shared_state().await.assert_eq("\x00ˇ"); - - cx.simulate_shared_keystrokes("ctrl-v j").await; - cx.shared_state().await.assert_eq("\x00jˇ"); - cx.simulate_shared_keystrokes("ctrl-v x 6 5").await; - cx.shared_state().await.assert_eq("\x00jeˇ"); - cx.simulate_shared_keystrokes("ctrl-v U 1 F 6 4 0 space") - .await; - cx.shared_state().await.assert_eq("\x00je🙀 ˇ"); - } - - #[gpui::test] - async fn test_ctrl_v_escape(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("ˇ").await; - cx.simulate_shared_keystrokes("i ctrl-v 9 escape").await; - cx.shared_state().await.assert_eq("ˇ\t"); - cx.simulate_shared_keystrokes("i ctrl-v escape").await; - cx.shared_state().await.assert_eq("\x1bˇ\t"); - } - - #[gpui::test] - async fn test_ctrl_v_control(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("ˇ").await; - cx.simulate_shared_keystrokes("i ctrl-v ctrl-d").await; - cx.shared_state().await.assert_eq("\x04ˇ"); - cx.simulate_shared_keystrokes("ctrl-v ctrl-j").await; - cx.shared_state().await.assert_eq("\x04\x00ˇ"); - cx.simulate_shared_keystrokes("ctrl-v tab").await; - cx.shared_state().await.assert_eq("\x04\x00\x09ˇ"); - } -} diff --git a/crates/vim/src/digraph/default.rs b/crates/vim/src/digraph/default.rs deleted file mode 100644 index 717fed3816..0000000000 --- a/crates/vim/src/digraph/default.rs +++ /dev/null @@ -1,1366 +0,0 @@ -/// Copied from https://github.com/neovim/neovim/blob/5fc25ecc7a383a4bed2199774ed2e26022456ca3/src/nvim/digraph.c#L65 -pub const DEFAULT_DIGRAPHS: &[(char, char, u32)] = &[ - ('N', 'U', 0x00), - ('S', 'H', 0x01), - ('S', 'X', 0x02), - ('E', 'X', 0x03), - ('E', 'T', 0x04), - ('E', 'Q', 0x05), - ('A', 'K', 0x06), - ('B', 'L', 0x07), - ('B', 'S', 0x08), - ('H', 'T', 0x09), - ('L', 'F', 0x0a), - ('V', 'T', 0x0b), - ('F', 'F', 0x0c), - ('C', 'R', 0x0d), - ('S', 'O', 0x0e), - ('S', 'I', 0x0f), - ('D', 'L', 0x10), - ('D', '1', 0x11), - ('D', '2', 0x12), - ('D', '3', 0x13), - ('D', '4', 0x14), - ('N', 'K', 0x15), - ('S', 'Y', 0x16), - ('E', 'B', 0x17), - ('C', 'N', 0x18), - ('E', 'M', 0x19), - ('S', 'B', 0x1a), - ('E', 'C', 0x1b), - ('F', 'S', 0x1c), - ('G', 'S', 0x1d), - ('R', 'S', 0x1e), - ('U', 'S', 0x1f), - ('S', 'P', 0x20), - ('N', 'b', 0x23), - ('D', 'O', 0x24), - ('A', 't', 0x40), - ('<', '(', 0x5b), - ('/', '/', 0x5c), - (')', '>', 0x5d), - ('\'', '>', 0x5e), - ('\'', '!', 0x60), - ('(', '!', 0x7b), - ('!', '!', 0x7c), - ('!', ')', 0x7d), - ('\'', '?', 0x7e), - ('D', 'T', 0x7f), - ('P', 'A', 0x80), - ('H', 'O', 0x81), - ('B', 'H', 0x82), - ('N', 'H', 0x83), - ('I', 'N', 0x84), - ('N', 'L', 0x85), - ('S', 'A', 0x86), - ('E', 'S', 0x87), - ('H', 'S', 0x88), - ('H', 'J', 0x89), - ('V', 'S', 0x8a), - ('P', 'D', 0x8b), - ('P', 'U', 0x8c), - ('R', 'I', 0x8d), - ('S', '2', 0x8e), - ('S', '3', 0x8f), - ('D', 'C', 0x90), - ('P', '1', 0x91), - ('P', '2', 0x92), - ('T', 'S', 0x93), - ('C', 'C', 0x94), - ('M', 'W', 0x95), - ('S', 'G', 0x96), - ('E', 'G', 0x97), - ('S', 'S', 0x98), - ('G', 'C', 0x99), - ('S', 'C', 0x9a), - ('C', 'I', 0x9b), - ('S', 'T', 0x9c), - ('O', 'C', 0x9d), - ('P', 'M', 0x9e), - ('A', 'C', 0x9f), - ('N', 'S', 0xa0), - ('!', 'I', 0xa1), - ('~', '!', 0xa1), - ('C', 't', 0xa2), - ('c', '|', 0xa2), - ('P', 'd', 0xa3), - ('$', '$', 0xa3), - ('C', 'u', 0xa4), - ('o', 'x', 0xa4), - ('Y', 'e', 0xa5), - ('Y', '-', 0xa5), - ('B', 'B', 0xa6), - ('|', '|', 0xa6), - ('S', 'E', 0xa7), - ('\'', ':', 0xa8), - ('C', 'o', 0xa9), - ('c', 'O', 0xa9), - ('-', 'a', 0xaa), - ('<', '<', 0xab), - ('N', 'O', 0xac), - ('-', ',', 0xac), - ('-', '-', 0xad), - ('R', 'g', 0xae), - ('\'', 'm', 0xaf), - ('-', '=', 0xaf), - ('D', 'G', 0xb0), - ('~', 'o', 0xb0), - ('+', '-', 0xb1), - ('2', 'S', 0xb2), - ('2', '2', 0xb2), - ('3', 'S', 0xb3), - ('3', '3', 0xb3), - ('\'', '\'', 0xb4), - ('M', 'y', 0xb5), - ('P', 'I', 0xb6), - ('p', 'p', 0xb6), - ('.', 'M', 0xb7), - ('~', '.', 0xb7), - ('\'', ',', 0xb8), - ('1', 'S', 0xb9), - ('1', '1', 0xb9), - ('-', 'o', 0xba), - ('>', '>', 0xbb), - ('1', '4', 0xbc), - ('1', '2', 0xbd), - ('3', '4', 0xbe), - ('?', 'I', 0xbf), - ('~', '?', 0xbf), - ('A', '!', 0xc0), - ('A', '`', 0xc0), - ('A', '\'', 0xc1), - ('A', '>', 0xc2), - ('A', '^', 0xc2), - ('A', '?', 0xc3), - ('A', '~', 0xc3), - ('A', ':', 0xc4), - ('A', '"', 0xc4), - ('A', 'A', 0xc5), - ('A', '@', 0xc5), - ('A', 'E', 0xc6), - ('C', ',', 0xc7), - ('E', '!', 0xc8), - ('E', '`', 0xc8), - ('E', '\'', 0xc9), - ('E', '>', 0xca), - ('E', '^', 0xca), - ('E', ':', 0xcb), - ('E', '"', 0xcb), - ('I', '!', 0xcc), - ('I', '`', 0xcc), - ('I', '\'', 0xcd), - ('I', '>', 0xce), - ('I', '^', 0xce), - ('I', ':', 0xcf), - ('I', '"', 0xcf), - ('D', '-', 0xd0), - ('N', '?', 0xd1), - ('N', '~', 0xd1), - ('O', '!', 0xd2), - ('O', '`', 0xd2), - ('O', '\'', 0xd3), - ('O', '>', 0xd4), - ('O', '^', 0xd4), - ('O', '?', 0xd5), - ('O', '~', 0xd5), - ('O', ':', 0xd6), - ('*', 'X', 0xd7), - ('/', '\\', 0xd7), - ('O', '/', 0xd8), - ('U', '!', 0xd9), - ('U', '`', 0xd9), - ('U', '\'', 0xda), - ('U', '>', 0xdb), - ('U', '^', 0xdb), - ('U', ':', 0xdc), - ('Y', '\'', 0xdd), - ('T', 'H', 0xde), - ('I', 'p', 0xde), - ('s', 's', 0xdf), - ('a', '!', 0xe0), - ('a', '`', 0xe0), - ('a', '\'', 0xe1), - ('a', '>', 0xe2), - ('a', '^', 0xe2), - ('a', '?', 0xe3), - ('a', '~', 0xe3), - ('a', ':', 0xe4), - ('a', '"', 0xe4), - ('a', 'a', 0xe5), - ('a', '@', 0xe5), - ('a', 'e', 0xe6), - ('c', ',', 0xe7), - ('e', '!', 0xe8), - ('e', '`', 0xe8), - ('e', '\'', 0xe9), - ('e', '>', 0xea), - ('e', '^', 0xea), - ('e', ':', 0xeb), - ('e', '"', 0xeb), - ('i', '!', 0xec), - ('i', '`', 0xec), - ('i', '\'', 0xed), - ('i', '>', 0xee), - ('i', '^', 0xee), - ('i', ':', 0xef), - ('d', '-', 0xf0), - ('n', '?', 0xf1), - ('n', '~', 0xf1), - ('o', '!', 0xf2), - ('o', '`', 0xf2), - ('o', '\'', 0xf3), - ('o', '>', 0xf4), - ('o', '^', 0xf4), - ('o', '?', 0xf5), - ('o', '~', 0xf5), - ('o', ':', 0xf6), - ('-', ':', 0xf7), - ('o', '/', 0xf8), - ('u', '!', 0xf9), - ('u', '`', 0xf9), - ('u', '\'', 0xfa), - ('u', '>', 0xfb), - ('u', '^', 0xfb), - ('u', ':', 0xfc), - ('y', '\'', 0xfd), - ('t', 'h', 0xfe), - ('y', ':', 0xff), - ('y', '"', 0xff), - ('A', '-', 0x0100), - ('a', '-', 0x0101), - ('A', '(', 0x0102), - ('a', '(', 0x0103), - ('A', ';', 0x0104), - ('a', ';', 0x0105), - ('C', '\'', 0x0106), - ('c', '\'', 0x0107), - ('C', '>', 0x0108), - ('c', '>', 0x0109), - ('C', '.', 0x010a), - ('c', '.', 0x010b), - ('C', '<', 0x010c), - ('c', '<', 0x010d), - ('D', '<', 0x010e), - ('d', '<', 0x010f), - ('D', '/', 0x0110), - ('d', '/', 0x0111), - ('E', '-', 0x0112), - ('e', '-', 0x0113), - ('E', '(', 0x0114), - ('e', '(', 0x0115), - ('E', '.', 0x0116), - ('e', '.', 0x0117), - ('E', ';', 0x0118), - ('e', ';', 0x0119), - ('E', '<', 0x011a), - ('e', '<', 0x011b), - ('G', '>', 0x011c), - ('g', '>', 0x011d), - ('G', '(', 0x011e), - ('g', '(', 0x011f), - ('G', '.', 0x0120), - ('g', '.', 0x0121), - ('G', ',', 0x0122), - ('g', ',', 0x0123), - ('H', '>', 0x0124), - ('h', '>', 0x0125), - ('H', '/', 0x0126), - ('h', '/', 0x0127), - ('I', '?', 0x0128), - ('i', '?', 0x0129), - ('I', '-', 0x012a), - ('i', '-', 0x012b), - ('I', '(', 0x012c), - ('i', '(', 0x012d), - ('I', ';', 0x012e), - ('i', ';', 0x012f), - ('I', '.', 0x0130), - ('i', '.', 0x0131), - ('I', 'J', 0x0132), - ('i', 'j', 0x0133), - ('J', '>', 0x0134), - ('j', '>', 0x0135), - ('K', ',', 0x0136), - ('k', ',', 0x0137), - ('k', 'k', 0x0138), - ('L', '\'', 0x0139), - ('l', '\'', 0x013a), - ('L', ',', 0x013b), - ('l', ',', 0x013c), - ('L', '<', 0x013d), - ('l', '<', 0x013e), - ('L', '.', 0x013f), - ('l', '.', 0x0140), - ('L', '/', 0x0141), - ('l', '/', 0x0142), - ('N', '\'', 0x0143), - ('n', '\'', 0x0144), - ('N', ',', 0x0145), - ('n', ',', 0x0146), - ('N', '<', 0x0147), - ('n', '<', 0x0148), - ('\'', 'n', 0x0149), - ('N', 'G', 0x014a), - ('n', 'g', 0x014b), - ('O', '-', 0x014c), - ('o', '-', 0x014d), - ('O', '(', 0x014e), - ('o', '(', 0x014f), - ('O', '"', 0x0150), - ('o', '"', 0x0151), - ('O', 'E', 0x0152), - ('o', 'e', 0x0153), - ('R', '\'', 0x0154), - ('r', '\'', 0x0155), - ('R', ',', 0x0156), - ('r', ',', 0x0157), - ('R', '<', 0x0158), - ('r', '<', 0x0159), - ('S', '\'', 0x015a), - ('s', '\'', 0x015b), - ('S', '>', 0x015c), - ('s', '>', 0x015d), - ('S', ',', 0x015e), - ('s', ',', 0x015f), - ('S', '<', 0x0160), - ('s', '<', 0x0161), - ('T', ',', 0x0162), - ('t', ',', 0x0163), - ('T', '<', 0x0164), - ('t', '<', 0x0165), - ('T', '/', 0x0166), - ('t', '/', 0x0167), - ('U', '?', 0x0168), - ('u', '?', 0x0169), - ('U', '-', 0x016a), - ('u', '-', 0x016b), - ('U', '(', 0x016c), - ('u', '(', 0x016d), - ('U', '0', 0x016e), - ('u', '0', 0x016f), - ('U', '"', 0x0170), - ('u', '"', 0x0171), - ('U', ';', 0x0172), - ('u', ';', 0x0173), - ('W', '>', 0x0174), - ('w', '>', 0x0175), - ('Y', '>', 0x0176), - ('y', '>', 0x0177), - ('Y', ':', 0x0178), - ('Z', '\'', 0x0179), - ('z', '\'', 0x017a), - ('Z', '.', 0x017b), - ('z', '.', 0x017c), - ('Z', '<', 0x017d), - ('z', '<', 0x017e), - ('O', '9', 0x01a0), - ('o', '9', 0x01a1), - ('O', 'I', 0x01a2), - ('o', 'i', 0x01a3), - ('y', 'r', 0x01a6), - ('U', '9', 0x01af), - ('u', '9', 0x01b0), - ('Z', '/', 0x01b5), - ('z', '/', 0x01b6), - ('E', 'D', 0x01b7), - ('A', '<', 0x01cd), - ('a', '<', 0x01ce), - ('I', '<', 0x01cf), - ('i', '<', 0x01d0), - ('O', '<', 0x01d1), - ('o', '<', 0x01d2), - ('U', '<', 0x01d3), - ('u', '<', 0x01d4), - ('A', '1', 0x01de), - ('a', '1', 0x01df), - ('A', '7', 0x01e0), - ('a', '7', 0x01e1), - ('A', '3', 0x01e2), - ('a', '3', 0x01e3), - ('G', '/', 0x01e4), - ('g', '/', 0x01e5), - ('G', '<', 0x01e6), - ('g', '<', 0x01e7), - ('K', '<', 0x01e8), - ('k', '<', 0x01e9), - ('O', ';', 0x01ea), - ('o', ';', 0x01eb), - ('O', '1', 0x01ec), - ('o', '1', 0x01ed), - ('E', 'Z', 0x01ee), - ('e', 'z', 0x01ef), - ('j', '<', 0x01f0), - ('G', '\'', 0x01f4), - ('g', '\'', 0x01f5), - (';', 'S', 0x02bf), - ('\'', '<', 0x02c7), - ('\'', '(', 0x02d8), - ('\'', '.', 0x02d9), - ('\'', '0', 0x02da), - ('\'', ';', 0x02db), - ('\'', '"', 0x02dd), - ('A', '%', 0x0386), - ('E', '%', 0x0388), - ('Y', '%', 0x0389), - ('I', '%', 0x038a), - ('O', '%', 0x038c), - ('U', '%', 0x038e), - ('W', '%', 0x038f), - ('i', '3', 0x0390), - ('A', '*', 0x0391), - ('B', '*', 0x0392), - ('G', '*', 0x0393), - ('D', '*', 0x0394), - ('E', '*', 0x0395), - ('Z', '*', 0x0396), - ('Y', '*', 0x0397), - ('H', '*', 0x0398), - ('I', '*', 0x0399), - ('K', '*', 0x039a), - ('L', '*', 0x039b), - ('M', '*', 0x039c), - ('N', '*', 0x039d), - ('C', '*', 0x039e), - ('O', '*', 0x039f), - ('P', '*', 0x03a0), - ('R', '*', 0x03a1), - ('S', '*', 0x03a3), - ('T', '*', 0x03a4), - ('U', '*', 0x03a5), - ('F', '*', 0x03a6), - ('X', '*', 0x03a7), - ('Q', '*', 0x03a8), - ('W', '*', 0x03a9), - ('J', '*', 0x03aa), - ('V', '*', 0x03ab), - ('a', '%', 0x03ac), - ('e', '%', 0x03ad), - ('y', '%', 0x03ae), - ('i', '%', 0x03af), - ('u', '3', 0x03b0), - ('a', '*', 0x03b1), - ('b', '*', 0x03b2), - ('g', '*', 0x03b3), - ('d', '*', 0x03b4), - ('e', '*', 0x03b5), - ('z', '*', 0x03b6), - ('y', '*', 0x03b7), - ('h', '*', 0x03b8), - ('i', '*', 0x03b9), - ('k', '*', 0x03ba), - ('l', '*', 0x03bb), - ('m', '*', 0x03bc), - ('n', '*', 0x03bd), - ('c', '*', 0x03be), - ('o', '*', 0x03bf), - ('p', '*', 0x03c0), - ('r', '*', 0x03c1), - ('*', 's', 0x03c2), - ('s', '*', 0x03c3), - ('t', '*', 0x03c4), - ('u', '*', 0x03c5), - ('f', '*', 0x03c6), - ('x', '*', 0x03c7), - ('q', '*', 0x03c8), - ('w', '*', 0x03c9), - ('j', '*', 0x03ca), - ('v', '*', 0x03cb), - ('o', '%', 0x03cc), - ('u', '%', 0x03cd), - ('w', '%', 0x03ce), - ('\'', 'G', 0x03d8), - (',', 'G', 0x03d9), - ('T', '3', 0x03da), - ('t', '3', 0x03db), - ('M', '3', 0x03dc), - ('m', '3', 0x03dd), - ('K', '3', 0x03de), - ('k', '3', 0x03df), - ('P', '3', 0x03e0), - ('p', '3', 0x03e1), - ('\'', '%', 0x03f4), - ('j', '3', 0x03f5), - ('I', 'O', 0x0401), - ('D', '%', 0x0402), - ('G', '%', 0x0403), - ('I', 'E', 0x0404), - ('D', 'S', 0x0405), - ('I', 'I', 0x0406), - ('Y', 'I', 0x0407), - ('J', '%', 0x0408), - ('L', 'J', 0x0409), - ('N', 'J', 0x040a), - ('T', 's', 0x040b), - ('K', 'J', 0x040c), - ('V', '%', 0x040e), - ('D', 'Z', 0x040f), - ('A', '=', 0x0410), - ('B', '=', 0x0411), - ('V', '=', 0x0412), - ('G', '=', 0x0413), - ('D', '=', 0x0414), - ('E', '=', 0x0415), - ('Z', '%', 0x0416), - ('Z', '=', 0x0417), - ('I', '=', 0x0418), - ('J', '=', 0x0419), - ('K', '=', 0x041a), - ('L', '=', 0x041b), - ('M', '=', 0x041c), - ('N', '=', 0x041d), - ('O', '=', 0x041e), - ('P', '=', 0x041f), - ('R', '=', 0x0420), - ('S', '=', 0x0421), - ('T', '=', 0x0422), - ('U', '=', 0x0423), - ('F', '=', 0x0424), - ('H', '=', 0x0425), - ('C', '=', 0x0426), - ('C', '%', 0x0427), - ('S', '%', 0x0428), - ('S', 'c', 0x0429), - ('=', '"', 0x042a), - ('Y', '=', 0x042b), - ('%', '"', 0x042c), - ('J', 'E', 0x042d), - ('J', 'U', 0x042e), - ('J', 'A', 0x042f), - ('a', '=', 0x0430), - ('b', '=', 0x0431), - ('v', '=', 0x0432), - ('g', '=', 0x0433), - ('d', '=', 0x0434), - ('e', '=', 0x0435), - ('z', '%', 0x0436), - ('z', '=', 0x0437), - ('i', '=', 0x0438), - ('j', '=', 0x0439), - ('k', '=', 0x043a), - ('l', '=', 0x043b), - ('m', '=', 0x043c), - ('n', '=', 0x043d), - ('o', '=', 0x043e), - ('p', '=', 0x043f), - ('r', '=', 0x0440), - ('s', '=', 0x0441), - ('t', '=', 0x0442), - ('u', '=', 0x0443), - ('f', '=', 0x0444), - ('h', '=', 0x0445), - ('c', '=', 0x0446), - ('c', '%', 0x0447), - ('s', '%', 0x0448), - ('s', 'c', 0x0449), - ('=', '\'', 0x044a), - ('y', '=', 0x044b), - ('%', '\'', 0x044c), - ('j', 'e', 0x044d), - ('j', 'u', 0x044e), - ('j', 'a', 0x044f), - ('i', 'o', 0x0451), - ('d', '%', 0x0452), - ('g', '%', 0x0453), - ('i', 'e', 0x0454), - ('d', 's', 0x0455), - ('i', 'i', 0x0456), - ('y', 'i', 0x0457), - ('j', '%', 0x0458), - ('l', 'j', 0x0459), - ('n', 'j', 0x045a), - ('t', 's', 0x045b), - ('k', 'j', 0x045c), - ('v', '%', 0x045e), - ('d', 'z', 0x045f), - ('Y', '3', 0x0462), - ('y', '3', 0x0463), - ('O', '3', 0x046a), - ('o', '3', 0x046b), - ('F', '3', 0x0472), - ('f', '3', 0x0473), - ('V', '3', 0x0474), - ('v', '3', 0x0475), - ('C', '3', 0x0480), - ('c', '3', 0x0481), - ('G', '3', 0x0490), - ('g', '3', 0x0491), - ('A', '+', 0x05d0), - ('B', '+', 0x05d1), - ('G', '+', 0x05d2), - ('D', '+', 0x05d3), - ('H', '+', 0x05d4), - ('W', '+', 0x05d5), - ('Z', '+', 0x05d6), - ('X', '+', 0x05d7), - ('T', 'j', 0x05d8), - ('J', '+', 0x05d9), - ('K', '%', 0x05da), - ('K', '+', 0x05db), - ('L', '+', 0x05dc), - ('M', '%', 0x05dd), - ('M', '+', 0x05de), - ('N', '%', 0x05df), - ('N', '+', 0x05e0), - ('S', '+', 0x05e1), - ('E', '+', 0x05e2), - ('P', '%', 0x05e3), - ('P', '+', 0x05e4), - ('Z', 'j', 0x05e5), - ('Z', 'J', 0x05e6), - ('Q', '+', 0x05e7), - ('R', '+', 0x05e8), - ('S', 'h', 0x05e9), - ('T', '+', 0x05ea), - (',', '+', 0x060c), - (';', '+', 0x061b), - ('?', '+', 0x061f), - ('H', '\'', 0x0621), - ('a', 'M', 0x0622), - ('a', 'H', 0x0623), - ('w', 'H', 0x0624), - ('a', 'h', 0x0625), - ('y', 'H', 0x0626), - ('a', '+', 0x0627), - ('b', '+', 0x0628), - ('t', 'm', 0x0629), - ('t', '+', 0x062a), - ('t', 'k', 0x062b), - ('g', '+', 0x062c), - ('h', 'k', 0x062d), - ('x', '+', 0x062e), - ('d', '+', 0x062f), - ('d', 'k', 0x0630), - ('r', '+', 0x0631), - ('z', '+', 0x0632), - ('s', '+', 0x0633), - ('s', 'n', 0x0634), - ('c', '+', 0x0635), - ('d', 'd', 0x0636), - ('t', 'j', 0x0637), - ('z', 'H', 0x0638), - ('e', '+', 0x0639), - ('i', '+', 0x063a), - ('+', '+', 0x0640), - ('f', '+', 0x0641), - ('q', '+', 0x0642), - ('k', '+', 0x0643), - ('l', '+', 0x0644), - ('m', '+', 0x0645), - ('n', '+', 0x0646), - ('h', '+', 0x0647), - ('w', '+', 0x0648), - ('j', '+', 0x0649), - ('y', '+', 0x064a), - (':', '+', 0x064b), - ('"', '+', 0x064c), - ('=', '+', 0x064d), - ('/', '+', 0x064e), - ('\'', '+', 0x064f), - ('1', '+', 0x0650), - ('3', '+', 0x0651), - ('0', '+', 0x0652), - ('a', 'S', 0x0670), - ('p', '+', 0x067e), - ('v', '+', 0x06a4), - ('g', 'f', 0x06af), - ('0', 'a', 0x06f0), - ('1', 'a', 0x06f1), - ('2', 'a', 0x06f2), - ('3', 'a', 0x06f3), - ('4', 'a', 0x06f4), - ('5', 'a', 0x06f5), - ('6', 'a', 0x06f6), - ('7', 'a', 0x06f7), - ('8', 'a', 0x06f8), - ('9', 'a', 0x06f9), - ('B', '.', 0x1e02), - ('b', '.', 0x1e03), - ('B', '_', 0x1e06), - ('b', '_', 0x1e07), - ('D', '.', 0x1e0a), - ('d', '.', 0x1e0b), - ('D', '_', 0x1e0e), - ('d', '_', 0x1e0f), - ('D', ',', 0x1e10), - ('d', ',', 0x1e11), - ('F', '.', 0x1e1e), - ('f', '.', 0x1e1f), - ('G', '-', 0x1e20), - ('g', '-', 0x1e21), - ('H', '.', 0x1e22), - ('h', '.', 0x1e23), - ('H', ':', 0x1e26), - ('h', ':', 0x1e27), - ('H', ',', 0x1e28), - ('h', ',', 0x1e29), - ('K', '\'', 0x1e30), - ('k', '\'', 0x1e31), - ('K', '_', 0x1e34), - ('k', '_', 0x1e35), - ('L', '_', 0x1e3a), - ('l', '_', 0x1e3b), - ('M', '\'', 0x1e3e), - ('m', '\'', 0x1e3f), - ('M', '.', 0x1e40), - ('m', '.', 0x1e41), - ('N', '.', 0x1e44), - ('n', '.', 0x1e45), - ('N', '_', 0x1e48), - ('n', '_', 0x1e49), - ('P', '\'', 0x1e54), - ('p', '\'', 0x1e55), - ('P', '.', 0x1e56), - ('p', '.', 0x1e57), - ('R', '.', 0x1e58), - ('r', '.', 0x1e59), - ('R', '_', 0x1e5e), - ('r', '_', 0x1e5f), - ('S', '.', 0x1e60), - ('s', '.', 0x1e61), - ('T', '.', 0x1e6a), - ('t', '.', 0x1e6b), - ('T', '_', 0x1e6e), - ('t', '_', 0x1e6f), - ('V', '?', 0x1e7c), - ('v', '?', 0x1e7d), - ('W', '!', 0x1e80), - ('W', '`', 0x1e80), - ('w', '!', 0x1e81), - ('w', '`', 0x1e81), - ('W', '\'', 0x1e82), - ('w', '\'', 0x1e83), - ('W', ':', 0x1e84), - ('w', ':', 0x1e85), - ('W', '.', 0x1e86), - ('w', '.', 0x1e87), - ('X', '.', 0x1e8a), - ('x', '.', 0x1e8b), - ('X', ':', 0x1e8c), - ('x', ':', 0x1e8d), - ('Y', '.', 0x1e8e), - ('y', '.', 0x1e8f), - ('Z', '>', 0x1e90), - ('z', '>', 0x1e91), - ('Z', '_', 0x1e94), - ('z', '_', 0x1e95), - ('h', '_', 0x1e96), - ('t', ':', 0x1e97), - ('w', '0', 0x1e98), - ('y', '0', 0x1e99), - ('A', '2', 0x1ea2), - ('a', '2', 0x1ea3), - ('E', '2', 0x1eba), - ('e', '2', 0x1ebb), - ('E', '?', 0x1ebc), - ('e', '?', 0x1ebd), - ('I', '2', 0x1ec8), - ('i', '2', 0x1ec9), - ('O', '2', 0x1ece), - ('o', '2', 0x1ecf), - ('U', '2', 0x1ee6), - ('u', '2', 0x1ee7), - ('Y', '!', 0x1ef2), - ('Y', '`', 0x1ef2), - ('y', '!', 0x1ef3), - ('y', '`', 0x1ef3), - ('Y', '2', 0x1ef6), - ('y', '2', 0x1ef7), - ('Y', '?', 0x1ef8), - ('y', '?', 0x1ef9), - (';', '\'', 0x1f00), - (',', '\'', 0x1f01), - (';', '!', 0x1f02), - (',', '!', 0x1f03), - ('?', ';', 0x1f04), - ('?', ',', 0x1f05), - ('!', ':', 0x1f06), - ('?', ':', 0x1f07), - ('1', 'N', 0x2002), - ('1', 'M', 0x2003), - ('3', 'M', 0x2004), - ('4', 'M', 0x2005), - ('6', 'M', 0x2006), - ('1', 'T', 0x2009), - ('1', 'H', 0x200a), - ('-', '1', 0x2010), - ('-', 'N', 0x2013), - ('-', 'M', 0x2014), - ('-', '3', 0x2015), - ('!', '2', 0x2016), - ('=', '2', 0x2017), - ('\'', '6', 0x2018), - ('\'', '9', 0x2019), - ('.', '9', 0x201a), - ('9', '\'', 0x201b), - ('"', '6', 0x201c), - ('"', '9', 0x201d), - (':', '9', 0x201e), - ('9', '"', 0x201f), - ('/', '-', 0x2020), - ('/', '=', 0x2021), - ('o', 'o', 0x2022), - ('.', '.', 0x2025), - (',', '.', 0x2026), - ('%', '0', 0x2030), - ('1', '\'', 0x2032), - ('2', '\'', 0x2033), - ('3', '\'', 0x2034), - ('4', '\'', 0x2057), - ('1', '"', 0x2035), - ('2', '"', 0x2036), - ('3', '"', 0x2037), - ('C', 'a', 0x2038), - ('<', '1', 0x2039), - ('>', '1', 0x203a), - (':', 'X', 0x203b), - ('\'', '-', 0x203e), - ('/', 'f', 0x2044), - ('0', 'S', 0x2070), - ('4', 'S', 0x2074), - ('5', 'S', 0x2075), - ('6', 'S', 0x2076), - ('7', 'S', 0x2077), - ('8', 'S', 0x2078), - ('9', 'S', 0x2079), - ('+', 'S', 0x207a), - ('-', 'S', 0x207b), - ('=', 'S', 0x207c), - ('(', 'S', 0x207d), - (')', 'S', 0x207e), - ('n', 'S', 0x207f), - ('0', 's', 0x2080), - ('1', 's', 0x2081), - ('2', 's', 0x2082), - ('3', 's', 0x2083), - ('4', 's', 0x2084), - ('5', 's', 0x2085), - ('6', 's', 0x2086), - ('7', 's', 0x2087), - ('8', 's', 0x2088), - ('9', 's', 0x2089), - ('+', 's', 0x208a), - ('-', 's', 0x208b), - ('=', 's', 0x208c), - ('(', 's', 0x208d), - (')', 's', 0x208e), - ('L', 'i', 0x20a4), - ('P', 't', 0x20a7), - ('W', '=', 0x20a9), - ('=', 'e', 0x20ac), - ('E', 'u', 0x20ac), - ('=', 'R', 0x20bd), - ('=', 'P', 0x20bd), - ('o', 'C', 0x2103), - ('c', 'o', 0x2105), - ('o', 'F', 0x2109), - ('N', '0', 0x2116), - ('P', 'O', 0x2117), - ('R', 'x', 0x211e), - ('S', 'M', 0x2120), - ('T', 'M', 0x2122), - ('O', 'm', 0x2126), - ('A', 'O', 0x212b), - ('1', '3', 0x2153), - ('2', '3', 0x2154), - ('1', '5', 0x2155), - ('2', '5', 0x2156), - ('3', '5', 0x2157), - ('4', '5', 0x2158), - ('1', '6', 0x2159), - ('5', '6', 0x215a), - ('1', '8', 0x215b), - ('3', '8', 0x215c), - ('5', '8', 0x215d), - ('7', '8', 0x215e), - ('1', 'R', 0x2160), - ('2', 'R', 0x2161), - ('3', 'R', 0x2162), - ('4', 'R', 0x2163), - ('5', 'R', 0x2164), - ('6', 'R', 0x2165), - ('7', 'R', 0x2166), - ('8', 'R', 0x2167), - ('9', 'R', 0x2168), - ('a', 'R', 0x2169), - ('b', 'R', 0x216a), - ('c', 'R', 0x216b), - ('1', 'r', 0x2170), - ('2', 'r', 0x2171), - ('3', 'r', 0x2172), - ('4', 'r', 0x2173), - ('5', 'r', 0x2174), - ('6', 'r', 0x2175), - ('7', 'r', 0x2176), - ('8', 'r', 0x2177), - ('9', 'r', 0x2178), - ('a', 'r', 0x2179), - ('b', 'r', 0x217a), - ('c', 'r', 0x217b), - ('<', '-', 0x2190), - ('-', '!', 0x2191), - ('-', '>', 0x2192), - ('-', 'v', 0x2193), - ('<', '>', 0x2194), - ('U', 'D', 0x2195), - ('<', '=', 0x21d0), - ('=', '>', 0x21d2), - ('=', '=', 0x21d4), - ('F', 'A', 0x2200), - ('d', 'P', 0x2202), - ('T', 'E', 0x2203), - ('/', '0', 0x2205), - ('D', 'E', 0x2206), - ('N', 'B', 0x2207), - ('(', '-', 0x2208), - ('-', ')', 0x220b), - ('*', 'P', 0x220f), - ('+', 'Z', 0x2211), - ('-', '2', 0x2212), - ('-', '+', 0x2213), - ('*', '-', 0x2217), - ('O', 'b', 0x2218), - ('S', 'b', 0x2219), - ('R', 'T', 0x221a), - ('0', '(', 0x221d), - ('0', '0', 0x221e), - ('-', 'L', 0x221f), - ('-', 'V', 0x2220), - ('P', 'P', 0x2225), - ('A', 'N', 0x2227), - ('O', 'R', 0x2228), - ('(', 'U', 0x2229), - (')', 'U', 0x222a), - ('I', 'n', 0x222b), - ('D', 'I', 0x222c), - ('I', 'o', 0x222e), - ('.', ':', 0x2234), - (':', '.', 0x2235), - (':', 'R', 0x2236), - (':', ':', 0x2237), - ('?', '1', 0x223c), - ('C', 'G', 0x223e), - ('?', '-', 0x2243), - ('?', '=', 0x2245), - ('?', '2', 0x2248), - ('=', '?', 0x224c), - ('H', 'I', 0x2253), - ('!', '=', 0x2260), - ('=', '3', 0x2261), - ('=', '<', 0x2264), - ('>', '=', 0x2265), - ('<', '*', 0x226a), - ('*', '>', 0x226b), - ('!', '<', 0x226e), - ('!', '>', 0x226f), - ('(', 'C', 0x2282), - (')', 'C', 0x2283), - ('(', '_', 0x2286), - (')', '_', 0x2287), - ('0', '.', 0x2299), - ('0', '2', 0x229a), - ('-', 'T', 0x22a5), - ('.', 'P', 0x22c5), - (':', '3', 0x22ee), - ('.', '3', 0x22ef), - ('E', 'h', 0x2302), - ('<', '7', 0x2308), - ('>', '7', 0x2309), - ('7', '<', 0x230a), - ('7', '>', 0x230b), - ('N', 'I', 0x2310), - ('(', 'A', 0x2312), - ('T', 'R', 0x2315), - ('I', 'u', 0x2320), - ('I', 'l', 0x2321), - ('<', '/', 0x2329), - ('/', '>', 0x232a), - ('V', 's', 0x2423), - ('1', 'h', 0x2440), - ('3', 'h', 0x2441), - ('2', 'h', 0x2442), - ('4', 'h', 0x2443), - ('1', 'j', 0x2446), - ('2', 'j', 0x2447), - ('3', 'j', 0x2448), - ('4', 'j', 0x2449), - ('1', '.', 0x2488), - ('2', '.', 0x2489), - ('3', '.', 0x248a), - ('4', '.', 0x248b), - ('5', '.', 0x248c), - ('6', '.', 0x248d), - ('7', '.', 0x248e), - ('8', '.', 0x248f), - ('9', '.', 0x2490), - ('h', 'h', 0x2500), - ('H', 'H', 0x2501), - ('v', 'v', 0x2502), - ('V', 'V', 0x2503), - ('3', '-', 0x2504), - ('3', '_', 0x2505), - ('3', '!', 0x2506), - ('3', '/', 0x2507), - ('4', '-', 0x2508), - ('4', '_', 0x2509), - ('4', '!', 0x250a), - ('4', '/', 0x250b), - ('d', 'r', 0x250c), - ('d', 'R', 0x250d), - ('D', 'r', 0x250e), - ('D', 'R', 0x250f), - ('d', 'l', 0x2510), - ('d', 'L', 0x2511), - ('D', 'l', 0x2512), - ('L', 'D', 0x2513), - ('u', 'r', 0x2514), - ('u', 'R', 0x2515), - ('U', 'r', 0x2516), - ('U', 'R', 0x2517), - ('u', 'l', 0x2518), - ('u', 'L', 0x2519), - ('U', 'l', 0x251a), - ('U', 'L', 0x251b), - ('v', 'r', 0x251c), - ('v', 'R', 0x251d), - ('V', 'r', 0x2520), - ('V', 'R', 0x2523), - ('v', 'l', 0x2524), - ('v', 'L', 0x2525), - ('V', 'l', 0x2528), - ('V', 'L', 0x252b), - ('d', 'h', 0x252c), - ('d', 'H', 0x252f), - ('D', 'h', 0x2530), - ('D', 'H', 0x2533), - ('u', 'h', 0x2534), - ('u', 'H', 0x2537), - ('U', 'h', 0x2538), - ('U', 'H', 0x253b), - ('v', 'h', 0x253c), - ('v', 'H', 0x253f), - ('V', 'h', 0x2542), - ('V', 'H', 0x254b), - ('F', 'D', 0x2571), - ('B', 'D', 0x2572), - ('T', 'B', 0x2580), - ('L', 'B', 0x2584), - ('F', 'B', 0x2588), - ('l', 'B', 0x258c), - ('R', 'B', 0x2590), - ('.', 'S', 0x2591), - (':', 'S', 0x2592), - ('?', 'S', 0x2593), - ('f', 'S', 0x25a0), - ('O', 'S', 0x25a1), - ('R', 'O', 0x25a2), - ('R', 'r', 0x25a3), - ('R', 'F', 0x25a4), - ('R', 'Y', 0x25a5), - ('R', 'H', 0x25a6), - ('R', 'Z', 0x25a7), - ('R', 'K', 0x25a8), - ('R', 'X', 0x25a9), - ('s', 'B', 0x25aa), - ('S', 'R', 0x25ac), - ('O', 'r', 0x25ad), - ('U', 'T', 0x25b2), - ('u', 'T', 0x25b3), - ('P', 'R', 0x25b6), - ('T', 'r', 0x25b7), - ('D', 't', 0x25bc), - ('d', 'T', 0x25bd), - ('P', 'L', 0x25c0), - ('T', 'l', 0x25c1), - ('D', 'b', 0x25c6), - ('D', 'w', 0x25c7), - ('L', 'Z', 0x25ca), - ('0', 'm', 0x25cb), - ('0', 'o', 0x25ce), - ('0', 'M', 0x25cf), - ('0', 'L', 0x25d0), - ('0', 'R', 0x25d1), - ('S', 'n', 0x25d8), - ('I', 'c', 0x25d9), - ('F', 'd', 0x25e2), - ('B', 'd', 0x25e3), - ('*', '2', 0x2605), - ('*', '1', 0x2606), - ('<', 'H', 0x261c), - ('>', 'H', 0x261e), - ('0', 'u', 0x263a), - ('0', 'U', 0x263b), - ('S', 'U', 0x263c), - ('F', 'm', 0x2640), - ('M', 'l', 0x2642), - ('c', 'S', 0x2660), - ('c', 'H', 0x2661), - ('c', 'D', 0x2662), - ('c', 'C', 0x2663), - ('M', 'd', 0x2669), - ('M', '8', 0x266a), - ('M', '2', 0x266b), - ('M', 'b', 0x266d), - ('M', 'x', 0x266e), - ('M', 'X', 0x266f), - ('O', 'K', 0x2713), - ('X', 'X', 0x2717), - ('-', 'X', 0x2720), - ('I', 'S', 0x3000), - (',', '_', 0x3001), - ('.', '_', 0x3002), - ('+', '"', 0x3003), - ('+', '_', 0x3004), - ('*', '_', 0x3005), - (';', '_', 0x3006), - ('0', '_', 0x3007), - ('<', '+', 0x300a), - ('>', '+', 0x300b), - ('<', '\'', 0x300c), - ('>', '\'', 0x300d), - ('<', '"', 0x300e), - ('>', '"', 0x300f), - ('(', '"', 0x3010), - (')', '"', 0x3011), - ('=', 'T', 0x3012), - ('=', '_', 0x3013), - ('(', '\'', 0x3014), - (')', '\'', 0x3015), - ('(', 'I', 0x3016), - (')', 'I', 0x3017), - ('-', '?', 0x301c), - ('A', '5', 0x3041), - ('a', '5', 0x3042), - ('I', '5', 0x3043), - ('i', '5', 0x3044), - ('U', '5', 0x3045), - ('u', '5', 0x3046), - ('E', '5', 0x3047), - ('e', '5', 0x3048), - ('O', '5', 0x3049), - ('o', '5', 0x304a), - ('k', 'a', 0x304b), - ('g', 'a', 0x304c), - ('k', 'i', 0x304d), - ('g', 'i', 0x304e), - ('k', 'u', 0x304f), - ('g', 'u', 0x3050), - ('k', 'e', 0x3051), - ('g', 'e', 0x3052), - ('k', 'o', 0x3053), - ('g', 'o', 0x3054), - ('s', 'a', 0x3055), - ('z', 'a', 0x3056), - ('s', 'i', 0x3057), - ('z', 'i', 0x3058), - ('s', 'u', 0x3059), - ('z', 'u', 0x305a), - ('s', 'e', 0x305b), - ('z', 'e', 0x305c), - ('s', 'o', 0x305d), - ('z', 'o', 0x305e), - ('t', 'a', 0x305f), - ('d', 'a', 0x3060), - ('t', 'i', 0x3061), - ('d', 'i', 0x3062), - ('t', 'U', 0x3063), - ('t', 'u', 0x3064), - ('d', 'u', 0x3065), - ('t', 'e', 0x3066), - ('d', 'e', 0x3067), - ('t', 'o', 0x3068), - ('d', 'o', 0x3069), - ('n', 'a', 0x306a), - ('n', 'i', 0x306b), - ('n', 'u', 0x306c), - ('n', 'e', 0x306d), - ('n', 'o', 0x306e), - ('h', 'a', 0x306f), - ('b', 'a', 0x3070), - ('p', 'a', 0x3071), - ('h', 'i', 0x3072), - ('b', 'i', 0x3073), - ('p', 'i', 0x3074), - ('h', 'u', 0x3075), - ('b', 'u', 0x3076), - ('p', 'u', 0x3077), - ('h', 'e', 0x3078), - ('b', 'e', 0x3079), - ('p', 'e', 0x307a), - ('h', 'o', 0x307b), - ('b', 'o', 0x307c), - ('p', 'o', 0x307d), - ('m', 'a', 0x307e), - ('m', 'i', 0x307f), - ('m', 'u', 0x3080), - ('m', 'e', 0x3081), - ('m', 'o', 0x3082), - ('y', 'A', 0x3083), - ('y', 'a', 0x3084), - ('y', 'U', 0x3085), - ('y', 'u', 0x3086), - ('y', 'O', 0x3087), - ('y', 'o', 0x3088), - ('r', 'a', 0x3089), - ('r', 'i', 0x308a), - ('r', 'u', 0x308b), - ('r', 'e', 0x308c), - ('r', 'o', 0x308d), - ('w', 'A', 0x308e), - ('w', 'a', 0x308f), - ('w', 'i', 0x3090), - ('w', 'e', 0x3091), - ('w', 'o', 0x3092), - ('n', '5', 0x3093), - ('v', 'u', 0x3094), - ('"', '5', 0x309b), - ('0', '5', 0x309c), - ('*', '5', 0x309d), - ('+', '5', 0x309e), - ('a', '6', 0x30a1), - ('A', '6', 0x30a2), - ('i', '6', 0x30a3), - ('I', '6', 0x30a4), - ('u', '6', 0x30a5), - ('U', '6', 0x30a6), - ('e', '6', 0x30a7), - ('E', '6', 0x30a8), - ('o', '6', 0x30a9), - ('O', '6', 0x30aa), - ('K', 'a', 0x30ab), - ('G', 'a', 0x30ac), - ('K', 'i', 0x30ad), - ('G', 'i', 0x30ae), - ('K', 'u', 0x30af), - ('G', 'u', 0x30b0), - ('K', 'e', 0x30b1), - ('G', 'e', 0x30b2), - ('K', 'o', 0x30b3), - ('G', 'o', 0x30b4), - ('S', 'a', 0x30b5), - ('Z', 'a', 0x30b6), - ('S', 'i', 0x30b7), - ('Z', 'i', 0x30b8), - ('S', 'u', 0x30b9), - ('Z', 'u', 0x30ba), - ('S', 'e', 0x30bb), - ('Z', 'e', 0x30bc), - ('S', 'o', 0x30bd), - ('Z', 'o', 0x30be), - ('T', 'a', 0x30bf), - ('D', 'a', 0x30c0), - ('T', 'i', 0x30c1), - ('D', 'i', 0x30c2), - ('T', 'U', 0x30c3), - ('T', 'u', 0x30c4), - ('D', 'u', 0x30c5), - ('T', 'e', 0x30c6), - ('D', 'e', 0x30c7), - ('T', 'o', 0x30c8), - ('D', 'o', 0x30c9), - ('N', 'a', 0x30ca), - ('N', 'i', 0x30cb), - ('N', 'u', 0x30cc), - ('N', 'e', 0x30cd), - ('N', 'o', 0x30ce), - ('H', 'a', 0x30cf), - ('B', 'a', 0x30d0), - ('P', 'a', 0x30d1), - ('H', 'i', 0x30d2), - ('B', 'i', 0x30d3), - ('P', 'i', 0x30d4), - ('H', 'u', 0x30d5), - ('B', 'u', 0x30d6), - ('P', 'u', 0x30d7), - ('H', 'e', 0x30d8), - ('B', 'e', 0x30d9), - ('P', 'e', 0x30da), - ('H', 'o', 0x30db), - ('B', 'o', 0x30dc), - ('P', 'o', 0x30dd), - ('M', 'a', 0x30de), - ('M', 'i', 0x30df), - ('M', 'u', 0x30e0), - ('M', 'e', 0x30e1), - ('M', 'o', 0x30e2), - ('Y', 'A', 0x30e3), - ('Y', 'a', 0x30e4), - ('Y', 'U', 0x30e5), - ('Y', 'u', 0x30e6), - ('Y', 'O', 0x30e7), - ('Y', 'o', 0x30e8), - ('R', 'a', 0x30e9), - ('R', 'i', 0x30ea), - ('R', 'u', 0x30eb), - ('R', 'e', 0x30ec), - ('R', 'o', 0x30ed), - ('W', 'A', 0x30ee), - ('W', 'a', 0x30ef), - ('W', 'i', 0x30f0), - ('W', 'e', 0x30f1), - ('W', 'o', 0x30f2), - ('N', '6', 0x30f3), - ('V', 'u', 0x30f4), - ('K', 'A', 0x30f5), - ('K', 'E', 0x30f6), - ('V', 'a', 0x30f7), - ('V', 'i', 0x30f8), - ('V', 'e', 0x30f9), - ('V', 'o', 0x30fa), - ('.', '6', 0x30fb), - ('-', '6', 0x30fc), - ('*', '6', 0x30fd), - ('+', '6', 0x30fe), - ('b', '4', 0x3105), - ('p', '4', 0x3106), - ('m', '4', 0x3107), - ('f', '4', 0x3108), - ('d', '4', 0x3109), - ('t', '4', 0x310a), - ('n', '4', 0x310b), - ('l', '4', 0x310c), - ('g', '4', 0x310d), - ('k', '4', 0x310e), - ('h', '4', 0x310f), - ('j', '4', 0x3110), - ('q', '4', 0x3111), - ('x', '4', 0x3112), - ('z', 'h', 0x3113), - ('c', 'h', 0x3114), - ('s', 'h', 0x3115), - ('r', '4', 0x3116), - ('z', '4', 0x3117), - ('c', '4', 0x3118), - ('s', '4', 0x3119), - ('a', '4', 0x311a), - ('o', '4', 0x311b), - ('e', '4', 0x311c), - ('a', 'i', 0x311e), - ('e', 'i', 0x311f), - ('a', 'u', 0x3120), - ('o', 'u', 0x3121), - ('a', 'n', 0x3122), - ('e', 'n', 0x3123), - ('a', 'N', 0x3124), - ('e', 'N', 0x3125), - ('e', 'r', 0x3126), - ('i', '4', 0x3127), - ('u', '4', 0x3128), - ('i', 'u', 0x3129), - ('v', '4', 0x312a), - ('n', 'G', 0x312b), - ('g', 'n', 0x312c), - ('1', 'c', 0x3220), - ('2', 'c', 0x3221), - ('3', 'c', 0x3222), - ('4', 'c', 0x3223), - ('5', 'c', 0x3224), - ('6', 'c', 0x3225), - ('7', 'c', 0x3226), - ('8', 'c', 0x3227), - ('9', 'c', 0x3228), - ('f', 'f', 0xfb00), - ('f', 'i', 0xfb01), - ('f', 'l', 0xfb02), - ('f', 't', 0xfb05), - ('s', 't', 0xfb06), -]; diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs deleted file mode 100644 index fae2bda578..0000000000 --- a/crates/vim/src/helix.rs +++ /dev/null @@ -1,1669 +0,0 @@ -mod boundary; -mod duplicate; -mod object; -mod paste; -mod select; - -use editor::display_map::DisplaySnapshot; -use editor::{ - DisplayPoint, Editor, EditorSettings, HideMouseCursorOrigin, MultiBufferOffset, - SelectionEffects, ToOffset, ToPoint, movement, -}; -use gpui::actions; -use gpui::{Context, Window}; -use language::{CharClassifier, CharKind, Point}; -use search::{BufferSearchBar, SearchOptions}; -use settings::Settings; -use text::{Bias, SelectionGoal}; -use workspace::searchable::FilteredSearchRange; -use workspace::searchable::{self, Direction}; - -use crate::motion::{self, MotionKind}; -use crate::state::SearchState; -use crate::{ - Vim, - motion::{Motion, right}, - state::Mode, -}; - -actions!( - vim, - [ - /// Yanks the current selection or character if no selection. - HelixYank, - /// Inserts at the beginning of the selection. - HelixInsert, - /// Appends at the end of the selection. - HelixAppend, - /// Goes to the location of the last modification. - HelixGotoLastModification, - /// Select entire line or multiple lines, extending downwards. - HelixSelectLine, - /// Select all matches of a given pattern within the current selection. - HelixSelectRegex, - /// Removes all but the one selection that was created last. - /// `Newest` can eventually be `Primary`. - HelixKeepNewestSelection, - /// Copies all selections below. - HelixDuplicateBelow, - /// Copies all selections above. - HelixDuplicateAbove, - /// Delete the selection and enter edit mode. - HelixSubstitute, - /// Delete the selection and enter edit mode, without yanking the selection. - HelixSubstituteNoYank, - /// Delete the selection and enter edit mode. - HelixSelectNext, - /// Delete the selection and enter edit mode, without yanking the selection. - HelixSelectPrevious, - ] -); - -pub fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, Vim::helix_select_lines); - Vim::action(editor, cx, Vim::helix_insert); - Vim::action(editor, cx, Vim::helix_append); - Vim::action(editor, cx, Vim::helix_yank); - Vim::action(editor, cx, Vim::helix_goto_last_modification); - Vim::action(editor, cx, Vim::helix_paste); - Vim::action(editor, cx, Vim::helix_select_regex); - Vim::action(editor, cx, Vim::helix_keep_newest_selection); - Vim::action(editor, cx, |vim, _: &HelixDuplicateBelow, window, cx| { - let times = Vim::take_count(cx); - vim.helix_duplicate_selections_below(times, window, cx); - }); - Vim::action(editor, cx, |vim, _: &HelixDuplicateAbove, window, cx| { - let times = Vim::take_count(cx); - vim.helix_duplicate_selections_above(times, window, cx); - }); - Vim::action(editor, cx, Vim::helix_substitute); - Vim::action(editor, cx, Vim::helix_substitute_no_yank); - Vim::action(editor, cx, Vim::helix_select_next); - Vim::action(editor, cx, Vim::helix_select_previous); -} - -impl Vim { - pub fn helix_normal_motion( - &mut self, - motion: Motion, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.helix_move_cursor(motion, times, window, cx); - } - - pub fn helix_select_motion( - &mut self, - motion: Motion, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.change_selections(Default::default(), window, cx, |s| { - if let Motion::ZedSearchResult { new_selections, .. } = &motion { - s.select_anchor_ranges(new_selections.clone()); - return; - }; - - s.move_with(|map, selection| { - let was_reversed = selection.reversed; - let mut current_head = selection.head(); - - // our motions assume the current character is after the cursor, - // but in (forward) visual mode the current character is just - // before the end of the selection. - - // If the file ends with a newline (which is common) we don't do this. - // so that if you go to the end of such a file you can use "up" to go - // to the previous line and have it work somewhat as expected. - if !selection.reversed - && !selection.is_empty() - && !(selection.end.column() == 0 && selection.end == map.max_point()) - { - current_head = movement::left(map, selection.end) - } - - let (new_head, goal) = match motion { - // Going to next word start is special cased - // since Vim differs from Helix in that motion - // Vim: `w` goes to the first character of a word - // Helix: `w` goes to the character before a word - Motion::NextWordStart { ignore_punctuation } => { - let mut head = movement::right(map, current_head); - let classifier = - map.buffer_snapshot().char_classifier_at(head.to_point(map)); - for _ in 0..times.unwrap_or(1) { - let (_, new_head) = - movement::find_boundary_trail(map, head, |left, right| { - Self::is_boundary_right(ignore_punctuation)( - left, - right, - &classifier, - ) - }); - head = new_head; - } - head = movement::left(map, head); - (head, SelectionGoal::None) - } - _ => motion - .move_point( - map, - current_head, - selection.goal, - times, - &text_layout_details, - ) - .unwrap_or((current_head, selection.goal)), - }; - - selection.set_head(new_head, goal); - - // ensure the current character is included in the selection. - if !selection.reversed { - let next_point = movement::right(map, selection.end); - - if !(next_point.column() == 0 && next_point == map.max_point()) { - selection.end = next_point; - } - } - - // vim always ensures the anchor character stays selected. - // if our selection has reversed, we need to move the opposite end - // to ensure the anchor is still selected. - if was_reversed && !selection.reversed { - selection.start = movement::left(map, selection.start); - } else if !was_reversed && selection.reversed { - selection.end = movement::right(map, selection.end); - } - }) - }); - }); - } - - /// Updates all selections based on where the cursors are. - fn helix_new_selections( - &mut self, - window: &mut Window, - cx: &mut Context, - mut change: impl FnMut( - // the start of the cursor - DisplayPoint, - &DisplaySnapshot, - ) -> Option<(DisplayPoint, DisplayPoint)>, - ) { - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let cursor_start = if selection.reversed || selection.is_empty() { - selection.head() - } else { - movement::left(map, selection.head()) - }; - let Some((head, tail)) = change(cursor_start, map) else { - return; - }; - - selection.set_head_tail(head, tail, SelectionGoal::None); - }); - }); - }); - } - - fn helix_find_range_forward( - &mut self, - times: Option, - window: &mut Window, - cx: &mut Context, - mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool, - ) { - let times = times.unwrap_or(1); - self.helix_new_selections(window, cx, |cursor, map| { - let mut head = movement::right(map, cursor); - let mut tail = cursor; - let classifier = map.buffer_snapshot().char_classifier_at(head.to_point(map)); - if head == map.max_point() { - return None; - } - for _ in 0..times { - let (maybe_next_tail, next_head) = - movement::find_boundary_trail(map, head, |left, right| { - is_boundary(left, right, &classifier) - }); - - if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail { - break; - } - - head = next_head; - if let Some(next_tail) = maybe_next_tail { - tail = next_tail; - } - } - Some((head, tail)) - }); - } - - fn helix_find_range_backward( - &mut self, - times: Option, - window: &mut Window, - cx: &mut Context, - mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool, - ) { - let times = times.unwrap_or(1); - self.helix_new_selections(window, cx, |cursor, map| { - let mut head = cursor; - // The original cursor was one character wide, - // but the search starts from the left side of it, - // so to include that space the selection must end one character to the right. - let mut tail = movement::right(map, cursor); - let classifier = map.buffer_snapshot().char_classifier_at(head.to_point(map)); - if head == DisplayPoint::zero() { - return None; - } - for _ in 0..times { - let (maybe_next_tail, next_head) = - movement::find_preceding_boundary_trail(map, head, |left, right| { - is_boundary(left, right, &classifier) - }); - - if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail { - break; - } - - head = next_head; - if let Some(next_tail) = maybe_next_tail { - tail = next_tail; - } - } - Some((head, tail)) - }); - } - - pub fn helix_move_and_collapse( - &mut self, - motion: Motion, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let goal = selection.goal; - let cursor = if selection.is_empty() || selection.reversed { - selection.head() - } else { - movement::left(map, selection.head()) - }; - - let (point, goal) = motion - .move_point(map, cursor, selection.goal, times, &text_layout_details) - .unwrap_or((cursor, goal)); - - selection.collapse_to(point, goal) - }) - }); - }); - } - - fn is_boundary_right( - ignore_punctuation: bool, - ) -> impl FnMut(char, char, &CharClassifier) -> bool { - move |left, right, classifier| { - let left_kind = classifier.kind_with(left, ignore_punctuation); - let right_kind = classifier.kind_with(right, ignore_punctuation); - let at_newline = (left == '\n') ^ (right == '\n'); - - (left_kind != right_kind && right_kind != CharKind::Whitespace) || at_newline - } - } - - fn is_boundary_left( - ignore_punctuation: bool, - ) -> impl FnMut(char, char, &CharClassifier) -> bool { - move |left, right, classifier| { - let left_kind = classifier.kind_with(left, ignore_punctuation); - let right_kind = classifier.kind_with(right, ignore_punctuation); - let at_newline = (left == '\n') ^ (right == '\n'); - - (left_kind != right_kind && left_kind != CharKind::Whitespace) || at_newline - } - } - - pub fn helix_move_cursor( - &mut self, - motion: Motion, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - match motion { - Motion::NextWordStart { ignore_punctuation } => self.helix_find_range_forward( - times, - window, - cx, - Self::is_boundary_right(ignore_punctuation), - ), - Motion::NextWordEnd { ignore_punctuation } => self.helix_find_range_forward( - times, - window, - cx, - Self::is_boundary_left(ignore_punctuation), - ), - Motion::PreviousWordStart { ignore_punctuation } => self.helix_find_range_backward( - times, - window, - cx, - Self::is_boundary_left(ignore_punctuation), - ), - Motion::PreviousWordEnd { ignore_punctuation } => self.helix_find_range_backward( - times, - window, - cx, - Self::is_boundary_right(ignore_punctuation), - ), - Motion::EndOfLine { .. } => { - // In Helix mode, EndOfLine should position cursor ON the last character, - // not after it. We therefore need special handling for it. - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let goal = selection.goal; - let cursor = if selection.is_empty() || selection.reversed { - selection.head() - } else { - movement::left(map, selection.head()) - }; - - let (point, _goal) = motion - .move_point(map, cursor, goal, times, &text_layout_details) - .unwrap_or((cursor, goal)); - - // Move left by one character to position on the last character - let adjusted_point = movement::saturating_left(map, point); - selection.collapse_to(adjusted_point, SelectionGoal::None) - }) - }); - }); - } - Motion::FindForward { - before, - char, - mode, - smartcase, - } => { - self.helix_new_selections(window, cx, |cursor, map| { - let start = cursor; - let mut last_boundary = start; - for _ in 0..times.unwrap_or(1) { - last_boundary = movement::find_boundary( - map, - movement::right(map, last_boundary), - mode, - |left, right| { - let current_char = if before { right } else { left }; - motion::is_character_match(char, current_char, smartcase) - }, - ); - } - Some((last_boundary, start)) - }); - } - Motion::FindBackward { - after, - char, - mode, - smartcase, - } => { - self.helix_new_selections(window, cx, |cursor, map| { - let start = cursor; - let mut last_boundary = start; - for _ in 0..times.unwrap_or(1) { - last_boundary = movement::find_preceding_boundary_display_point( - map, - last_boundary, - mode, - |left, right| { - let current_char = if after { left } else { right }; - motion::is_character_match(char, current_char, smartcase) - }, - ); - } - // The original cursor was one character wide, - // but the search started from the left side of it, - // so to include that space the selection must end one character to the right. - Some((last_boundary, movement::right(map, start))) - }); - } - _ => self.helix_move_and_collapse(motion, times, window, cx), - } - } - - pub fn helix_yank(&mut self, _: &HelixYank, window: &mut Window, cx: &mut Context) { - self.update_editor(cx, |vim, editor, cx| { - let has_selection = editor - .selections - .all_adjusted(&editor.display_snapshot(cx)) - .iter() - .any(|selection| !selection.is_empty()); - - if !has_selection { - // If no selection, expand to current character (like 'v' does) - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let head = selection.head(); - let new_head = movement::saturating_right(map, head); - selection.set_tail(head, SelectionGoal::None); - selection.set_head(new_head, SelectionGoal::None); - }); - }); - vim.yank_selections_content( - editor, - crate::motion::MotionKind::Exclusive, - window, - cx, - ); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|_map, selection| { - selection.collapse_to(selection.start, SelectionGoal::None); - }); - }); - } else { - // Yank the selection(s) - vim.yank_selections_content( - editor, - crate::motion::MotionKind::Exclusive, - window, - cx, - ); - } - }); - - // Drop back to normal mode after yanking - self.switch_mode(Mode::HelixNormal, true, window, cx); - } - - fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context) { - self.start_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|_map, selection| { - // In helix normal mode, move cursor to start of selection and collapse - if !selection.is_empty() { - selection.collapse_to(selection.start, SelectionGoal::None); - } - }); - }); - }); - self.switch_mode(Mode::Insert, false, window, cx); - } - - fn helix_select_regex( - &mut self, - _: &HelixSelectRegex, - window: &mut Window, - cx: &mut Context, - ) { - Vim::take_forced_motion(cx); - let Some(pane) = self.pane(window, cx) else { - return; - }; - let prior_selections = self.editor_selections(window, cx); - pane.update(cx, |pane, cx| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |search_bar, cx| { - if !search_bar.show(window, cx) { - return; - } - - search_bar.select_query(window, cx); - cx.focus_self(window); - - search_bar.set_replacement(None, cx); - let mut options = SearchOptions::NONE; - options |= SearchOptions::REGEX; - if EditorSettings::get_global(cx).search.case_sensitive { - options |= SearchOptions::CASE_SENSITIVE; - } - search_bar.set_search_options(options, cx); - if let Some(search) = search_bar.set_search_within_selection( - Some(FilteredSearchRange::Selection), - window, - cx, - ) { - cx.spawn_in(window, async move |search_bar, cx| { - if search.await.is_ok() { - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.activate_current_match(window, cx) - }) - } else { - Ok(()) - } - }) - .detach_and_log_err(cx); - } - self.search = SearchState { - direction: searchable::Direction::Next, - count: 1, - prior_selections, - prior_operator: self.operator_stack.last().cloned(), - prior_mode: self.mode, - helix_select: true, - } - }); - } - }); - self.start_recording(cx); - } - - fn helix_append(&mut self, _: &HelixAppend, window: &mut Window, cx: &mut Context) { - self.start_recording(cx); - self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let point = if selection.is_empty() { - right(map, selection.head(), 1) - } else { - selection.end - }; - selection.collapse_to(point, SelectionGoal::None); - }); - }); - }); - } - - pub fn helix_replace(&mut self, text: &str, window: &mut Window, cx: &mut Context) { - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let display_map = editor.display_snapshot(cx); - let selections = editor.selections.all_display(&display_map); - - // Store selection info for positioning after edit - let selection_info: Vec<_> = selections - .iter() - .map(|selection| { - let range = selection.range(); - let start_offset = range.start.to_offset(&display_map, Bias::Left); - let end_offset = range.end.to_offset(&display_map, Bias::Left); - let was_empty = range.is_empty(); - let was_reversed = selection.reversed; - ( - display_map.buffer_snapshot().anchor_before(start_offset), - end_offset - start_offset, - was_empty, - was_reversed, - ) - }) - .collect(); - - let mut edits = Vec::new(); - for selection in &selections { - let mut range = selection.range(); - - // For empty selections, extend to replace one character - if range.is_empty() { - range.end = movement::saturating_right(&display_map, range.start); - } - - let byte_range = range.start.to_offset(&display_map, Bias::Left) - ..range.end.to_offset(&display_map, Bias::Left); - - if !byte_range.is_empty() { - let replacement_text = text.repeat(byte_range.end - byte_range.start); - edits.push((byte_range, replacement_text)); - } - } - - editor.edit(edits, cx); - - // Restore selections based on original info - let snapshot = editor.buffer().read(cx).snapshot(cx); - let ranges: Vec<_> = selection_info - .into_iter() - .map(|(start_anchor, original_len, was_empty, was_reversed)| { - let start_point = start_anchor.to_point(&snapshot); - if was_empty { - // For cursor-only, collapse to start - start_point..start_point - } else { - // For selections, span the replaced text - let replacement_len = text.len() * original_len; - let end_offset = start_anchor.to_offset(&snapshot) + replacement_len; - let end_point = snapshot.offset_to_point(end_offset); - if was_reversed { - end_point..start_point - } else { - start_point..end_point - } - } - }) - .collect(); - - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges(ranges); - }); - }); - }); - self.switch_mode(Mode::HelixNormal, true, window, cx); - } - - pub fn helix_goto_last_modification( - &mut self, - _: &HelixGotoLastModification, - window: &mut Window, - cx: &mut Context, - ) { - self.jump(".".into(), false, false, window, cx); - } - - pub fn helix_select_lines( - &mut self, - _: &HelixSelectLine, - window: &mut Window, - cx: &mut Context, - ) { - let count = Vim::take_count(cx).unwrap_or(1); - self.update_editor(cx, |_, editor, cx| { - editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = editor.selections.all::(&display_map); - let max_point = display_map.buffer_snapshot().max_point(); - let buffer_snapshot = &display_map.buffer_snapshot(); - - for selection in &mut selections { - // Start always goes to column 0 of the first selected line - let start_row = selection.start.row; - let current_end_row = selection.end.row; - - // Check if cursor is on empty line by checking first character - let line_start_offset = buffer_snapshot.point_to_offset(Point::new(start_row, 0)); - let first_char = buffer_snapshot.chars_at(line_start_offset).next(); - let extra_line = if first_char == Some('\n') { 1 } else { 0 }; - - let end_row = current_end_row + count as u32 + extra_line; - - selection.start = Point::new(start_row, 0); - selection.end = if end_row > max_point.row { - max_point - } else { - Point::new(end_row, 0) - }; - selection.reversed = false; - } - - editor.change_selections(Default::default(), window, cx, |s| { - s.select(selections); - }); - }); - } - - fn helix_keep_newest_selection( - &mut self, - _: &HelixKeepNewestSelection, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |_, editor, cx| { - let newest = editor - .selections - .newest::(&editor.display_snapshot(cx)); - editor.change_selections(Default::default(), window, cx, |s| s.select(vec![newest])); - }); - } - - fn do_helix_substitute(&mut self, yank: bool, window: &mut Window, cx: &mut Context) { - self.update_editor(cx, |vim, editor, cx| { - editor.set_clip_at_line_ends(false, cx); - editor.transact(window, cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - if selection.start == selection.end { - selection.end = movement::right(map, selection.end); - } - - // If the selection starts and ends on a newline, we exclude the last one. - if !selection.is_empty() - && selection.start.column() == 0 - && selection.end.column() == 0 - { - selection.end = movement::left(map, selection.end); - } - }) - }); - if yank { - vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx); - } - let selections = editor - .selections - .all::(&editor.display_snapshot(cx)) - .into_iter(); - let edits = selections.map(|selection| (selection.start..selection.end, "")); - editor.edit(edits, cx); - }); - }); - self.switch_mode(Mode::Insert, true, window, cx); - } - - fn helix_substitute( - &mut self, - _: &HelixSubstitute, - window: &mut Window, - cx: &mut Context, - ) { - self.do_helix_substitute(true, window, cx); - } - - fn helix_substitute_no_yank( - &mut self, - _: &HelixSubstituteNoYank, - window: &mut Window, - cx: &mut Context, - ) { - self.do_helix_substitute(false, window, cx); - } - - fn helix_select_next( - &mut self, - _: &HelixSelectNext, - window: &mut Window, - cx: &mut Context, - ) { - self.do_helix_select(Direction::Next, window, cx); - } - - fn helix_select_previous( - &mut self, - _: &HelixSelectPrevious, - window: &mut Window, - cx: &mut Context, - ) { - self.do_helix_select(Direction::Prev, window, cx); - } - - fn do_helix_select( - &mut self, - direction: searchable::Direction, - window: &mut Window, - cx: &mut Context, - ) { - let Some(pane) = self.pane(window, cx) else { - return; - }; - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - let prior_selections = self.editor_selections(window, cx); - - let success = pane.update(cx, |pane, cx| { - let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() else { - return false; - }; - search_bar.update(cx, |search_bar, cx| { - if !search_bar.has_active_match() || !search_bar.show(window, cx) { - return false; - } - search_bar.select_match(direction, count, window, cx); - true - }) - }); - - if !success { - return; - } - if self.mode == Mode::HelixSelect { - self.update_editor(cx, |_vim, editor, cx| { - let snapshot = editor.snapshot(window, cx); - editor.change_selections(SelectionEffects::default(), window, cx, |s| { - s.select_anchor_ranges( - prior_selections - .iter() - .cloned() - .chain(s.all_anchors(&snapshot).iter().map(|s| s.range())), - ); - }) - }); - } - } -} - -#[cfg(test)] -mod test { - use indoc::indoc; - - use crate::{state::Mode, test::VimTestContext}; - - #[gpui::test] - async fn test_word_motions(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - // « - // ˇ - // » - cx.set_state( - indoc! {" - Th«e quiˇ»ck brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("w"); - - cx.assert_state( - indoc! {" - The qu«ick ˇ»brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("w"); - - cx.assert_state( - indoc! {" - The quick «brownˇ» - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("2 b"); - - cx.assert_state( - indoc! {" - The «ˇquick »brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("down e up"); - - cx.assert_state( - indoc! {" - The quicˇk brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.set_state("aa\n «ˇbb»", Mode::HelixNormal); - - cx.simulate_keystroke("b"); - - cx.assert_state("aa\n«ˇ »bb", Mode::HelixNormal); - } - - #[gpui::test] - async fn test_delete(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // test delete a selection - cx.set_state( - indoc! {" - The qu«ick ˇ»brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("d"); - - cx.assert_state( - indoc! {" - The quˇbrown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - // test deleting a single character - cx.simulate_keystrokes("d"); - - cx.assert_state( - indoc! {" - The quˇrown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - } - - #[gpui::test] - async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - The quick brownˇ - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("d"); - - cx.assert_state( - indoc! {" - The quick brownˇfox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - } - - // #[gpui::test] - // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) { - // let mut cx = VimTestContext::new(cx, true).await; - - // cx.set_state( - // indoc! {" - // The quick brown - // fox jumps over - // the lazy dog.ˇ"}, - // Mode::HelixNormal, - // ); - - // cx.simulate_keystrokes("d"); - - // cx.assert_state( - // indoc! {" - // The quick brown - // fox jumps over - // the lazy dog.ˇ"}, - // Mode::HelixNormal, - // ); - // } - - #[gpui::test] - async fn test_f_and_t(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("f z"); - - cx.assert_state( - indoc! {" - The qu«ick brown - fox jumps over - the lazˇ»y dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("F e F e"); - - cx.assert_state( - indoc! {" - The quick brown - fox jumps ov«ˇer - the» lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("e 2 F e"); - - cx.assert_state( - indoc! {" - Th«ˇe quick brown - fox jumps over» - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("t r t r"); - - cx.assert_state( - indoc! {" - The quick «brown - fox jumps oveˇ»r - the lazy dog."}, - Mode::HelixNormal, - ); - } - - #[gpui::test] - async fn test_newline_char(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal); - - cx.simulate_keystroke("w"); - - cx.assert_state("aa\n«bb ˇ»cc", Mode::HelixNormal); - - cx.set_state("aa«\nˇ»", Mode::HelixNormal); - - cx.simulate_keystroke("b"); - - cx.assert_state("«ˇaa»\n", Mode::HelixNormal); - } - - #[gpui::test] - async fn test_insert_selected(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state( - indoc! {" - «The ˇ»quick brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("i"); - - cx.assert_state( - indoc! {" - ˇThe quick brown - fox jumps over - the lazy dog."}, - Mode::Insert, - ); - } - - #[gpui::test] - async fn test_append(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - // test from the end of the selection - cx.set_state( - indoc! {" - «Theˇ» quick brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("a"); - - cx.assert_state( - indoc! {" - Theˇ quick brown - fox jumps over - the lazy dog."}, - Mode::Insert, - ); - - // test from the beginning of the selection - cx.set_state( - indoc! {" - «ˇThe» quick brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("a"); - - cx.assert_state( - indoc! {" - Theˇ quick brown - fox jumps over - the lazy dog."}, - Mode::Insert, - ); - } - - #[gpui::test] - async fn test_replace(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // No selection (single character) - cx.set_state("ˇaa", Mode::HelixNormal); - - cx.simulate_keystrokes("r x"); - - cx.assert_state("ˇxa", Mode::HelixNormal); - - // Cursor at the beginning - cx.set_state("«ˇaa»", Mode::HelixNormal); - - cx.simulate_keystrokes("r x"); - - cx.assert_state("«ˇxx»", Mode::HelixNormal); - - // Cursor at the end - cx.set_state("«aaˇ»", Mode::HelixNormal); - - cx.simulate_keystrokes("r x"); - - cx.assert_state("«xxˇ»", Mode::HelixNormal); - } - - #[gpui::test] - async fn test_helix_yank(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // Test yanking current character with no selection - cx.set_state("hello ˇworld", Mode::HelixNormal); - cx.simulate_keystrokes("y"); - - // Test cursor remains at the same position after yanking single character - cx.assert_state("hello ˇworld", Mode::HelixNormal); - cx.shared_clipboard().assert_eq("w"); - - // Move cursor and yank another character - cx.simulate_keystrokes("l"); - cx.simulate_keystrokes("y"); - cx.shared_clipboard().assert_eq("o"); - - // Test yanking with existing selection - cx.set_state("hello «worlˇ»d", Mode::HelixNormal); - cx.simulate_keystrokes("y"); - cx.shared_clipboard().assert_eq("worl"); - cx.assert_state("hello «worlˇ»d", Mode::HelixNormal); - - // Test yanking in select mode character by character - cx.set_state("hello ˇworld", Mode::HelixNormal); - cx.simulate_keystroke("v"); - cx.assert_state("hello «wˇ»orld", Mode::HelixSelect); - cx.simulate_keystroke("y"); - cx.assert_state("hello «wˇ»orld", Mode::HelixNormal); - cx.shared_clipboard().assert_eq("w"); - } - - #[gpui::test] - async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // First copy some text to clipboard - cx.set_state("«hello worldˇ»", Mode::HelixNormal); - cx.simulate_keystrokes("y"); - - // Test paste with shift-r on single cursor - cx.set_state("foo ˇbar", Mode::HelixNormal); - cx.simulate_keystrokes("shift-r"); - - cx.assert_state("foo hello worldˇbar", Mode::HelixNormal); - - // Test paste with shift-r on selection - cx.set_state("foo «barˇ» baz", Mode::HelixNormal); - cx.simulate_keystrokes("shift-r"); - - cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal); - } - - #[gpui::test] - async fn test_helix_select_mode(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - assert_eq!(cx.mode(), Mode::Normal); - cx.enable_helix(); - - cx.simulate_keystrokes("v"); - assert_eq!(cx.mode(), Mode::HelixSelect); - cx.simulate_keystrokes("escape"); - assert_eq!(cx.mode(), Mode::HelixNormal); - } - - #[gpui::test] - async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // Make a modification at a specific location - cx.set_state("ˇhello", Mode::HelixNormal); - assert_eq!(cx.mode(), Mode::HelixNormal); - cx.simulate_keystrokes("i"); - assert_eq!(cx.mode(), Mode::Insert); - cx.simulate_keystrokes("escape"); - assert_eq!(cx.mode(), Mode::HelixNormal); - } - - #[gpui::test] - async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // Make a modification at a specific location - cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal); - cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal); - cx.simulate_keystrokes("i"); - cx.simulate_keystrokes("escape"); - cx.simulate_keystrokes("i"); - cx.simulate_keystrokes("m o d i f i e d space"); - cx.simulate_keystrokes("escape"); - - // TODO: this fails, because state is no longer helix - cx.assert_state( - "line one\nline modified ˇtwo\nline three", - Mode::HelixNormal, - ); - - // Move cursor away from the modification - cx.simulate_keystrokes("up"); - - // Use "g ." to go back to last modification - cx.simulate_keystrokes("g ."); - - // Verify we're back at the modification location and still in HelixNormal mode - cx.assert_state( - "line one\nline modifiedˇ two\nline three", - Mode::HelixNormal, - ); - } - - #[gpui::test] - async fn test_helix_select_lines(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.set_state( - "line one\nline ˇtwo\nline three\nline four", - Mode::HelixNormal, - ); - cx.simulate_keystrokes("2 x"); - cx.assert_state( - "line one\n«line two\nline three\nˇ»line four", - Mode::HelixNormal, - ); - - // Test extending existing line selection - cx.set_state( - indoc! {" - li«ˇne one - li»ne two - line three - line four"}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("x"); - cx.assert_state( - indoc! {" - «line one - line two - ˇ»line three - line four"}, - Mode::HelixNormal, - ); - - // Pressing x in empty line, select next line (because helix considers cursor a selection) - cx.set_state( - indoc! {" - line one - ˇ - line three - line four"}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("x"); - cx.assert_state( - indoc! {" - line one - « - line three - ˇ»line four"}, - Mode::HelixNormal, - ); - - // Empty line with count selects extra + count lines - cx.set_state( - indoc! {" - line one - ˇ - line three - line four - line five"}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("2 x"); - cx.assert_state( - indoc! {" - line one - « - line three - line four - ˇ»line five"}, - Mode::HelixNormal, - ); - - // Compare empty vs non-empty line behavior - cx.set_state( - indoc! {" - ˇnon-empty line - line two - line three"}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("x"); - cx.assert_state( - indoc! {" - «non-empty line - ˇ»line two - line three"}, - Mode::HelixNormal, - ); - - // Same test but with empty line - should select one extra - cx.set_state( - indoc! {" - ˇ - line two - line three"}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("x"); - cx.assert_state( - indoc! {" - « - line two - ˇ»line three"}, - Mode::HelixNormal, - ); - - // Test selecting multiple lines with count - cx.set_state( - indoc! {" - ˇline one - line two - line threeˇ - line four - line five"}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("x"); - cx.assert_state( - indoc! {" - «line one - ˇ»line two - «line three - ˇ»line four - line five"}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("x"); - cx.assert_state( - indoc! {" - «line one - line two - line three - line four - ˇ»line five"}, - Mode::HelixNormal, - ); - } - - #[gpui::test] - async fn test_helix_select_mode_motion(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - assert_eq!(cx.mode(), Mode::Normal); - cx.enable_helix(); - - cx.set_state("ˇhello", Mode::HelixNormal); - cx.simulate_keystrokes("l v l l"); - cx.assert_state("h«ellˇ»o", Mode::HelixSelect); - } - - #[gpui::test] - async fn test_helix_select_mode_motion_multiple_cursors(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - assert_eq!(cx.mode(), Mode::Normal); - cx.enable_helix(); - - // Start with multiple cursors (no selections) - cx.set_state("ˇhello\nˇworld", Mode::HelixNormal); - - // Enter select mode and move right twice - cx.simulate_keystrokes("v l l"); - - // Each cursor should independently create and extend its own selection - cx.assert_state("«helˇ»lo\n«worˇ»ld", Mode::HelixSelect); - } - - #[gpui::test] - async fn test_helix_select_word_motions(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇone two", Mode::Normal); - cx.simulate_keystrokes("v w"); - cx.assert_state("«one tˇ»wo", Mode::Visual); - - // In Vim, this selects "t". In helix selections stops just before "t" - - cx.enable_helix(); - cx.set_state("ˇone two", Mode::HelixNormal); - cx.simulate_keystrokes("v w"); - cx.assert_state("«one ˇ»two", Mode::HelixSelect); - } - - #[gpui::test] - async fn test_exit_visual_mode(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇone two", Mode::Normal); - cx.simulate_keystrokes("v w"); - cx.assert_state("«one tˇ»wo", Mode::Visual); - cx.simulate_keystrokes("escape"); - cx.assert_state("one ˇtwo", Mode::Normal); - - cx.enable_helix(); - cx.set_state("ˇone two", Mode::HelixNormal); - cx.simulate_keystrokes("v w"); - cx.assert_state("«one ˇ»two", Mode::HelixSelect); - cx.simulate_keystrokes("escape"); - cx.assert_state("«one ˇ»two", Mode::HelixNormal); - } - - #[gpui::test] - async fn test_helix_select_motion(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - cx.set_state("«ˇ»one two three", Mode::HelixSelect); - cx.simulate_keystrokes("w"); - cx.assert_state("«one ˇ»two three", Mode::HelixSelect); - - cx.set_state("«ˇ»one two three", Mode::HelixSelect); - cx.simulate_keystrokes("e"); - cx.assert_state("«oneˇ» two three", Mode::HelixSelect); - } - - #[gpui::test] - async fn test_helix_full_cursor_selection(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - cx.set_state("ˇone two three", Mode::HelixNormal); - cx.simulate_keystrokes("l l v h h h"); - cx.assert_state("«ˇone» two three", Mode::HelixSelect); - } - - #[gpui::test] - async fn test_helix_select_regex(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - cx.set_state("ˇone two one", Mode::HelixNormal); - cx.simulate_keystrokes("x"); - cx.assert_state("«one two oneˇ»", Mode::HelixNormal); - cx.simulate_keystrokes("s o n e"); - cx.run_until_parked(); - cx.simulate_keystrokes("enter"); - cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal); - - cx.simulate_keystrokes("x"); - cx.simulate_keystrokes("s"); - cx.run_until_parked(); - cx.simulate_keystrokes("enter"); - cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal); - - // TODO: change "search_in_selection" to not perform any search when in helix select mode with no selection - // cx.set_state("ˇstuff one two one", Mode::HelixNormal); - // cx.simulate_keystrokes("s o n e enter"); - // cx.assert_state("ˇstuff one two one", Mode::HelixNormal); - } - - #[gpui::test] - async fn test_helix_select_next_match(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇhello two one two one two one", Mode::Visual); - cx.simulate_keystrokes("/ o n e"); - cx.simulate_keystrokes("enter"); - cx.simulate_keystrokes("n n"); - cx.assert_state("«hello two one two one two oˇ»ne", Mode::Visual); - - cx.set_state("ˇhello two one two one two one", Mode::Normal); - cx.simulate_keystrokes("/ o n e"); - cx.simulate_keystrokes("enter"); - cx.simulate_keystrokes("n n"); - cx.assert_state("hello two one two one two ˇone", Mode::Normal); - - cx.set_state("ˇhello two one two one two one", Mode::Normal); - cx.simulate_keystrokes("/ o n e"); - cx.simulate_keystrokes("enter"); - cx.simulate_keystrokes("n g n g n"); - cx.assert_state("hello two one two «one two oneˇ»", Mode::Visual); - - cx.enable_helix(); - - cx.set_state("ˇhello two one two one two one", Mode::HelixNormal); - cx.simulate_keystrokes("/ o n e"); - cx.simulate_keystrokes("enter"); - cx.simulate_keystrokes("n n"); - cx.assert_state("hello two one two one two «oneˇ»", Mode::HelixNormal); - - cx.set_state("ˇhello two one two one two one", Mode::HelixSelect); - cx.simulate_keystrokes("/ o n e"); - cx.simulate_keystrokes("enter"); - cx.simulate_keystrokes("n n"); - cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect); - } - - #[gpui::test] - async fn test_helix_substitute(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇone two", Mode::HelixNormal); - cx.simulate_keystrokes("c"); - cx.assert_state("ˇne two", Mode::Insert); - - cx.set_state("«oneˇ» two", Mode::HelixNormal); - cx.simulate_keystrokes("c"); - cx.assert_state("ˇ two", Mode::Insert); - - cx.set_state( - indoc! {" - oneˇ two - three - "}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("x c"); - cx.assert_state( - indoc! {" - ˇ - three - "}, - Mode::Insert, - ); - - cx.set_state( - indoc! {" - one twoˇ - three - "}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("c"); - cx.assert_state( - indoc! {" - one twoˇthree - "}, - Mode::Insert, - ); - - // Helix doesn't set the cursor to the first non-blank one when - // replacing lines: it uses language-dependent indent queries instead. - cx.set_state( - indoc! {" - one two - « indented - three not indentedˇ» - "}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("c"); - cx.set_state( - indoc! {" - one two - ˇ - "}, - Mode::Insert, - ); - } - - #[gpui::test] - async fn test_g_l_end_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // Test g l moves to last character, not after it - cx.set_state("hello ˇworld!", Mode::HelixNormal); - cx.simulate_keystrokes("g l"); - cx.assert_state("hello worldˇ!", Mode::HelixNormal); - - // Test with Chinese characters, test if work with UTF-8? - cx.set_state("ˇ你好世界", Mode::HelixNormal); - cx.simulate_keystrokes("g l"); - cx.assert_state("你好世ˇ界", Mode::HelixNormal); - - // Test with end of line - cx.set_state("endˇ", Mode::HelixNormal); - cx.simulate_keystrokes("g l"); - cx.assert_state("enˇd", Mode::HelixNormal); - - // Test with empty line - cx.set_state( - indoc! {" - hello - ˇ - world"}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("g l"); - cx.assert_state( - indoc! {" - hello - ˇ - world"}, - Mode::HelixNormal, - ); - - // Test with multiple lines - cx.set_state( - indoc! {" - ˇfirst line - second line - third line"}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("g l"); - cx.assert_state( - indoc! {" - first linˇe - second line - third line"}, - Mode::HelixNormal, - ); - } -} diff --git a/crates/vim/src/helix/boundary.rs b/crates/vim/src/helix/boundary.rs deleted file mode 100644 index 0c2ebbeef0..0000000000 --- a/crates/vim/src/helix/boundary.rs +++ /dev/null @@ -1,739 +0,0 @@ -use std::{cmp::Ordering, ops::Range}; - -use editor::{ - DisplayPoint, MultiBufferOffset, - display_map::{DisplaySnapshot, ToDisplayPoint}, - movement, -}; -use language::{CharClassifier, CharKind}; -use text::Bias; - -use crate::helix::object::HelixTextObject; - -/// Text objects (after helix definition) that can easily be -/// found by reading a buffer and comparing two neighboring chars -/// until a start / end is found -trait BoundedObject { - /// The next start since `from` (inclusive). - /// If outer is true it is the start of "a" object (m a) rather than "inner" object (m i). - fn next_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option; - /// The next end since `from` (inclusive). - /// If outer is true it is the end of "a" object (m a) rather than "inner" object (m i). - fn next_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option; - /// The previous start since `from` (inclusive). - /// If outer is true it is the start of "a" object (m a) rather than "inner" object (m i). - fn previous_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option; - /// The previous end since `from` (inclusive). - /// If outer is true it is the end of "a" object (m a) rather than "inner" object (m i). - fn previous_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option; - - /// Whether the range inside the object can be zero characters wide. - /// If so, the trait assumes that these ranges can't be directly adjacent to each other. - fn inner_range_can_be_zero_width(&self) -> bool; - /// Whether the "ma" can exceed the "mi" range on both sides at the same time - fn surround_on_both_sides(&self) -> bool; - /// Whether the outer range of an object could overlap with the outer range of the neighboring - /// object. If so, they can't be nested. - fn ambiguous_outer(&self) -> bool; - - fn can_be_zero_width(&self, around: bool) -> bool { - if around { - false - } else { - self.inner_range_can_be_zero_width() - } - } - - /// Switches from an "mi" range to an "ma" one. - /// Assumes the inner range is valid. - fn around(&self, map: &DisplaySnapshot, inner_range: Range) -> Range { - if self.surround_on_both_sides() { - let start = self - .previous_start(map, inner_range.start, true) - .unwrap_or(inner_range.start); - let end = self - .next_end(map, inner_range.end, true) - .unwrap_or(inner_range.end); - - return start..end; - } - - let mut start = inner_range.start; - let end = self - .next_end(map, inner_range.end, true) - .unwrap_or(inner_range.end); - if end == inner_range.end { - start = self - .previous_start(map, inner_range.start, true) - .unwrap_or(inner_range.start) - } - - start..end - } - /// Switches from an "ma" range to an "mi" one. - /// Assumes the inner range is valid. - fn inside(&self, map: &DisplaySnapshot, outer_range: Range) -> Range { - let inner_start = self - .next_start(map, outer_range.start, false) - .unwrap_or_else(|| { - log::warn!("The motion might not have found the text object correctly"); - outer_range.start - }); - let inner_end = self - .previous_end(map, outer_range.end, false) - .unwrap_or_else(|| { - log::warn!("The motion might not have found the text object correctly"); - outer_range.end - }); - inner_start..inner_end - } - - /// The next end since `start` (inclusive) on the same nesting level. - fn close_at_end(&self, start: Offset, map: &DisplaySnapshot, outer: bool) -> Option { - let mut end_search_start = if self.can_be_zero_width(outer) { - start - } else { - start.next(map)? - }; - let mut start_search_start = start.next(map)?; - - loop { - let next_end = self.next_end(map, end_search_start, outer)?; - let maybe_next_start = self.next_start(map, start_search_start, outer); - if let Some(next_start) = maybe_next_start - && (next_start.0 < next_end.0 - || next_start.0 == next_end.0 && self.can_be_zero_width(outer)) - && !self.ambiguous_outer() - { - let closing = self.close_at_end(next_start, map, outer)?; - end_search_start = closing.next(map)?; - start_search_start = if self.can_be_zero_width(outer) { - closing.next(map)? - } else { - closing - }; - } else { - return Some(next_end); - } - } - } - /// The previous start since `end` (inclusive) on the same nesting level. - fn close_at_start(&self, end: Offset, map: &DisplaySnapshot, outer: bool) -> Option { - let mut start_search_end = if self.can_be_zero_width(outer) { - end - } else { - end.previous(map)? - }; - let mut end_search_end = end.previous(map)?; - - loop { - let previous_start = self.previous_start(map, start_search_end, outer)?; - let maybe_previous_end = self.previous_end(map, end_search_end, outer); - if let Some(previous_end) = maybe_previous_end - && (previous_end.0 > previous_start.0 - || previous_end.0 == previous_start.0 && self.can_be_zero_width(outer)) - && !self.ambiguous_outer() - { - let closing = self.close_at_start(previous_end, map, outer)?; - start_search_end = closing.previous(map)?; - end_search_end = if self.can_be_zero_width(outer) { - closing.previous(map)? - } else { - closing - }; - } else { - return Some(previous_start); - } - } - } -} - -#[derive(Clone, Copy, PartialEq, Debug, PartialOrd, Ord, Eq)] -struct Offset(MultiBufferOffset); -impl Offset { - fn next(self, map: &DisplaySnapshot) -> Option { - let next = Self( - map.buffer_snapshot() - .clip_offset(self.0 + 1usize, Bias::Right), - ); - (next.0 > self.0).then(|| next) - } - fn previous(self, map: &DisplaySnapshot) -> Option { - if self.0 == MultiBufferOffset(0) { - return None; - } - Some(Self( - map.buffer_snapshot().clip_offset(self.0 - 1, Bias::Left), - )) - } - fn range( - start: (DisplayPoint, Bias), - end: (DisplayPoint, Bias), - map: &DisplaySnapshot, - ) -> Range { - Self(start.0.to_offset(map, start.1))..Self(end.0.to_offset(map, end.1)) - } -} - -impl HelixTextObject for B { - fn range( - &self, - map: &DisplaySnapshot, - relative_to: Range, - around: bool, - ) -> Option> { - let relative_to = Offset::range( - (relative_to.start, Bias::Left), - (relative_to.end, Bias::Left), - map, - ); - - relative_range(self, around, map, |find_outer| { - let search_start = if self.can_be_zero_width(find_outer) { - relative_to.end - } else { - // If the objects can be directly next to each other an object end the - // cursor (relative_to) end would not count for close_at_end, so the search - // needs to start one character to the left. - relative_to.end.previous(map)? - }; - let max_end = self.close_at_end(search_start, map, find_outer)?; - let min_start = self.close_at_start(max_end, map, find_outer)?; - - (min_start <= relative_to.start).then(|| min_start..max_end) - }) - } - - fn next_range( - &self, - map: &DisplaySnapshot, - relative_to: Range, - around: bool, - ) -> Option> { - let relative_to = Offset::range( - (relative_to.start, Bias::Left), - (relative_to.end, Bias::Left), - map, - ); - - relative_range(self, around, map, |find_outer| { - let min_start = self.next_start(map, relative_to.end, find_outer)?; - let max_end = self.close_at_end(min_start, map, find_outer)?; - - Some(min_start..max_end) - }) - } - - fn previous_range( - &self, - map: &DisplaySnapshot, - relative_to: Range, - around: bool, - ) -> Option> { - let relative_to = Offset::range( - (relative_to.start, Bias::Left), - (relative_to.end, Bias::Left), - map, - ); - - relative_range(self, around, map, |find_outer| { - let max_end = self.previous_end(map, relative_to.start, find_outer)?; - let min_start = self.close_at_start(max_end, map, find_outer)?; - - Some(min_start..max_end) - }) - } -} - -fn relative_range( - object: &B, - outer: bool, - map: &DisplaySnapshot, - find_range: impl Fn(bool) -> Option>, -) -> Option> { - // The cursor could be inside the outer range, but not the inner range. - // Whether that should count as found. - let find_outer = object.surround_on_both_sides() && !object.ambiguous_outer(); - let range = find_range(find_outer)?; - let min_start = range.start; - let max_end = range.end; - - let wanted_range = if outer && !find_outer { - // max_end is not yet the outer end - object.around(map, min_start..max_end) - } else if !outer && find_outer { - // max_end is the outer end, but the final result should have the inner end - object.inside(map, min_start..max_end) - } else { - min_start..max_end - }; - - let start = wanted_range.start.0.to_display_point(map); - let end = wanted_range.end.0.to_display_point(map); - - Some(start..end) -} - -/// A textobject whose boundaries can easily be found between two chars -pub enum ImmediateBoundary { - Word { ignore_punctuation: bool }, - Subword { ignore_punctuation: bool }, - AngleBrackets, - BackQuotes, - CurlyBrackets, - DoubleQuotes, - Parentheses, - SingleQuotes, - SquareBrackets, - VerticalBars, -} - -/// A textobject whose start and end can be found from an easy-to-find -/// boundary between two chars by following a simple path from there -pub enum FuzzyBoundary { - Sentence, - Paragraph, -} - -impl ImmediateBoundary { - fn is_inner_start(&self, left: char, right: char, classifier: CharClassifier) -> bool { - match self { - Self::Word { ignore_punctuation } => { - let classifier = classifier.ignore_punctuation(*ignore_punctuation); - is_word_start(left, right, &classifier) - || (is_buffer_start(left) && classifier.kind(right) != CharKind::Whitespace) - } - Self::Subword { ignore_punctuation } => { - let classifier = classifier.ignore_punctuation(*ignore_punctuation); - movement::is_subword_start(left, right, &classifier) - || (is_buffer_start(left) && classifier.kind(right) != CharKind::Whitespace) - } - Self::AngleBrackets => left == '<', - Self::BackQuotes => left == '`', - Self::CurlyBrackets => left == '{', - Self::DoubleQuotes => left == '"', - Self::Parentheses => left == '(', - Self::SingleQuotes => left == '\'', - Self::SquareBrackets => left == '[', - Self::VerticalBars => left == '|', - } - } - fn is_inner_end(&self, left: char, right: char, classifier: CharClassifier) -> bool { - match self { - Self::Word { ignore_punctuation } => { - let classifier = classifier.ignore_punctuation(*ignore_punctuation); - is_word_end(left, right, &classifier) - || (is_buffer_end(right) && classifier.kind(left) != CharKind::Whitespace) - } - Self::Subword { ignore_punctuation } => { - let classifier = classifier.ignore_punctuation(*ignore_punctuation); - movement::is_subword_start(left, right, &classifier) - || (is_buffer_end(right) && classifier.kind(left) != CharKind::Whitespace) - } - Self::AngleBrackets => right == '>', - Self::BackQuotes => right == '`', - Self::CurlyBrackets => right == '}', - Self::DoubleQuotes => right == '"', - Self::Parentheses => right == ')', - Self::SingleQuotes => right == '\'', - Self::SquareBrackets => right == ']', - Self::VerticalBars => right == '|', - } - } - fn is_outer_start(&self, left: char, right: char, classifier: CharClassifier) -> bool { - match self { - word @ Self::Word { .. } => word.is_inner_end(left, right, classifier) || left == '\n', - subword @ Self::Subword { .. } => { - subword.is_inner_end(left, right, classifier) || left == '\n' - } - Self::AngleBrackets => right == '<', - Self::BackQuotes => right == '`', - Self::CurlyBrackets => right == '{', - Self::DoubleQuotes => right == '"', - Self::Parentheses => right == '(', - Self::SingleQuotes => right == '\'', - Self::SquareBrackets => right == '[', - Self::VerticalBars => right == '|', - } - } - fn is_outer_end(&self, left: char, right: char, classifier: CharClassifier) -> bool { - match self { - word @ Self::Word { .. } => { - word.is_inner_start(left, right, classifier) || right == '\n' - } - subword @ Self::Subword { .. } => { - subword.is_inner_start(left, right, classifier) || right == '\n' - } - Self::AngleBrackets => left == '>', - Self::BackQuotes => left == '`', - Self::CurlyBrackets => left == '}', - Self::DoubleQuotes => left == '"', - Self::Parentheses => left == ')', - Self::SingleQuotes => left == '\'', - Self::SquareBrackets => left == ']', - Self::VerticalBars => left == '|', - } - } -} - -impl BoundedObject for ImmediateBoundary { - fn next_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option { - try_find_boundary(map, from, |left, right| { - let classifier = map.buffer_snapshot().char_classifier_at(from.0); - if outer { - self.is_outer_start(left, right, classifier) - } else { - self.is_inner_start(left, right, classifier) - } - }) - } - fn next_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option { - try_find_boundary(map, from, |left, right| { - let classifier = map.buffer_snapshot().char_classifier_at(from.0); - if outer { - self.is_outer_end(left, right, classifier) - } else { - self.is_inner_end(left, right, classifier) - } - }) - } - fn previous_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option { - try_find_preceding_boundary(map, from, |left, right| { - let classifier = map.buffer_snapshot().char_classifier_at(from.0); - if outer { - self.is_outer_start(left, right, classifier) - } else { - self.is_inner_start(left, right, classifier) - } - }) - } - fn previous_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option { - try_find_preceding_boundary(map, from, |left, right| { - let classifier = map.buffer_snapshot().char_classifier_at(from.0); - if outer { - self.is_outer_end(left, right, classifier) - } else { - self.is_inner_end(left, right, classifier) - } - }) - } - fn inner_range_can_be_zero_width(&self) -> bool { - match self { - Self::Subword { .. } | Self::Word { .. } => false, - _ => true, - } - } - fn surround_on_both_sides(&self) -> bool { - match self { - Self::Subword { .. } | Self::Word { .. } => false, - _ => true, - } - } - fn ambiguous_outer(&self) -> bool { - match self { - Self::BackQuotes - | Self::DoubleQuotes - | Self::SingleQuotes - | Self::VerticalBars - | Self::Subword { .. } - | Self::Word { .. } => true, - _ => false, - } - } -} - -impl FuzzyBoundary { - /// When between two chars that form an easy-to-find identifier boundary, - /// what's the way to get to the actual start of the object, if any - fn is_near_potential_inner_start<'a>( - &self, - left: char, - right: char, - classifier: &CharClassifier, - ) -> Option Option>> { - if is_buffer_start(left) { - return Some(Box::new(|identifier, _| Some(identifier))); - } - match self { - Self::Paragraph => { - if left != '\n' || right != '\n' { - return None; - } - Some(Box::new(|identifier, map| { - try_find_boundary(map, identifier, |left, right| left == '\n' && right != '\n') - })) - } - Self::Sentence => { - if let Some(find_paragraph_start) = - Self::Paragraph.is_near_potential_inner_start(left, right, classifier) - { - return Some(find_paragraph_start); - } else if !is_sentence_end(left, right, classifier) { - return None; - } - Some(Box::new(|identifier, map| { - let word = ImmediateBoundary::Word { - ignore_punctuation: false, - }; - word.next_start(map, identifier, false) - })) - } - } - } - /// When between two chars that form an easy-to-find identifier boundary, - /// what's the way to get to the actual end of the object, if any - fn is_near_potential_inner_end<'a>( - &self, - left: char, - right: char, - classifier: &CharClassifier, - ) -> Option Option>> { - if is_buffer_end(right) { - return Some(Box::new(|identifier, _| Some(identifier))); - } - match self { - Self::Paragraph => { - if left != '\n' || right != '\n' { - return None; - } - Some(Box::new(|identifier, map| { - try_find_preceding_boundary(map, identifier, |left, right| { - left != '\n' && right == '\n' - }) - })) - } - Self::Sentence => { - if let Some(find_paragraph_end) = - Self::Paragraph.is_near_potential_inner_end(left, right, classifier) - { - return Some(find_paragraph_end); - } else if !is_sentence_end(left, right, classifier) { - return None; - } - Some(Box::new(|identifier, _| Some(identifier))) - } - } - } - /// When between two chars that form an easy-to-find identifier boundary, - /// what's the way to get to the actual end of the object, if any - fn is_near_potential_outer_start<'a>( - &self, - left: char, - right: char, - classifier: &CharClassifier, - ) -> Option Option>> { - match self { - paragraph @ Self::Paragraph => { - paragraph.is_near_potential_inner_end(left, right, classifier) - } - sentence @ Self::Sentence => { - sentence.is_near_potential_inner_end(left, right, classifier) - } - } - } - /// When between two chars that form an easy-to-find identifier boundary, - /// what's the way to get to the actual end of the object, if any - fn is_near_potential_outer_end<'a>( - &self, - left: char, - right: char, - classifier: &CharClassifier, - ) -> Option Option>> { - match self { - paragraph @ Self::Paragraph => { - paragraph.is_near_potential_inner_start(left, right, classifier) - } - sentence @ Self::Sentence => { - sentence.is_near_potential_inner_start(left, right, classifier) - } - } - } - - // The boundary can be on the other side of `from` than the identifier, so the search needs to go both ways. - // Also, the distance (and direction) between identifier and boundary could vary, so a few ones need to be - // compared, even if one boundary was already found on the right side of `from`. - fn to_boundary( - &self, - map: &DisplaySnapshot, - from: Offset, - outer: bool, - backward: bool, - boundary_kind: Boundary, - ) -> Option { - let generate_boundary_data = |left, right, point: Offset| { - let classifier = map.buffer_snapshot().char_classifier_at(from.0); - let reach_boundary = if outer && boundary_kind == Boundary::Start { - self.is_near_potential_outer_start(left, right, &classifier) - } else if !outer && boundary_kind == Boundary::Start { - self.is_near_potential_inner_start(left, right, &classifier) - } else if outer && boundary_kind == Boundary::End { - self.is_near_potential_outer_end(left, right, &classifier) - } else { - self.is_near_potential_inner_end(left, right, &classifier) - }; - - reach_boundary.map(|reach_start| (point, reach_start)) - }; - - let forwards = try_find_boundary_data(map, from, generate_boundary_data); - let backwards = try_find_preceding_boundary_data(map, from, generate_boundary_data); - let boundaries = [forwards, backwards] - .into_iter() - .flatten() - .filter_map(|(identifier, reach_boundary)| reach_boundary(identifier, map)) - .filter(|boundary| match boundary.cmp(&from) { - Ordering::Equal => true, - Ordering::Less => backward, - Ordering::Greater => !backward, - }); - if backward { - boundaries.max_by_key(|boundary| *boundary) - } else { - boundaries.min_by_key(|boundary| *boundary) - } - } -} - -#[derive(PartialEq)] -enum Boundary { - Start, - End, -} - -impl BoundedObject for FuzzyBoundary { - fn next_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option { - self.to_boundary(map, from, outer, false, Boundary::Start) - } - fn next_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option { - self.to_boundary(map, from, outer, false, Boundary::End) - } - fn previous_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option { - self.to_boundary(map, from, outer, true, Boundary::Start) - } - fn previous_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option { - self.to_boundary(map, from, outer, true, Boundary::End) - } - fn inner_range_can_be_zero_width(&self) -> bool { - false - } - fn surround_on_both_sides(&self) -> bool { - false - } - fn ambiguous_outer(&self) -> bool { - false - } -} - -/// Returns the first boundary after or at `from` in text direction. -/// The start and end of the file are the chars `'\0'`. -fn try_find_boundary( - map: &DisplaySnapshot, - from: Offset, - is_boundary: impl Fn(char, char) -> bool, -) -> Option { - let boundary = try_find_boundary_data(map, from, |left, right, point| { - if is_boundary(left, right) { - Some(point) - } else { - None - } - })?; - Some(boundary) -} - -/// Returns some information about it (of type `T`) as soon as -/// there is a boundary after or at `from` in text direction -/// The start and end of the file are the chars `'\0'`. -fn try_find_boundary_data( - map: &DisplaySnapshot, - mut from: Offset, - boundary_information: impl Fn(char, char, Offset) -> Option, -) -> Option { - let mut prev_ch = map - .buffer_snapshot() - .reversed_chars_at(from.0) - .next() - .unwrap_or('\0'); - - for ch in map.buffer_snapshot().chars_at(from.0).chain(['\0']) { - if let Some(boundary_information) = boundary_information(prev_ch, ch, from) { - return Some(boundary_information); - } - from.0 += ch.len_utf8(); - prev_ch = ch; - } - - None -} - -/// Returns the first boundary after or at `from` in text direction. -/// The start and end of the file are the chars `'\0'`. -fn try_find_preceding_boundary( - map: &DisplaySnapshot, - from: Offset, - is_boundary: impl Fn(char, char) -> bool, -) -> Option { - let boundary = try_find_preceding_boundary_data(map, from, |left, right, point| { - if is_boundary(left, right) { - Some(point) - } else { - None - } - })?; - Some(boundary) -} - -/// Returns some information about it (of type `T`) as soon as -/// there is a boundary before or at `from` in opposite text direction -/// The start and end of the file are the chars `'\0'`. -fn try_find_preceding_boundary_data( - map: &DisplaySnapshot, - mut from: Offset, - is_boundary: impl Fn(char, char, Offset) -> Option, -) -> Option { - let mut prev_ch = map - .buffer_snapshot() - .chars_at(from.0) - .next() - .unwrap_or('\0'); - - for ch in map - .buffer_snapshot() - .reversed_chars_at(from.0) - .chain(['\0']) - { - if let Some(boundary_information) = is_boundary(ch, prev_ch, from) { - return Some(boundary_information); - } - from.0.0 = from.0.0.saturating_sub(ch.len_utf8()); - prev_ch = ch; - } - - None -} - -fn is_buffer_start(left: char) -> bool { - left == '\0' -} - -fn is_buffer_end(right: char) -> bool { - right == '\0' -} - -fn is_word_start(left: char, right: char, classifier: &CharClassifier) -> bool { - classifier.kind(left) != classifier.kind(right) - && classifier.kind(right) != CharKind::Whitespace -} - -fn is_word_end(left: char, right: char, classifier: &CharClassifier) -> bool { - classifier.kind(left) != classifier.kind(right) && classifier.kind(left) != CharKind::Whitespace -} - -fn is_sentence_end(left: char, right: char, classifier: &CharClassifier) -> bool { - const ENDS: [char; 1] = ['.']; - - if classifier.kind(right) != CharKind::Whitespace { - return false; - } - ENDS.into_iter().any(|end| left == end) -} diff --git a/crates/vim/src/helix/duplicate.rs b/crates/vim/src/helix/duplicate.rs deleted file mode 100644 index 37796c57aa..0000000000 --- a/crates/vim/src/helix/duplicate.rs +++ /dev/null @@ -1,234 +0,0 @@ -use std::ops::Range; - -use editor::{DisplayPoint, MultiBufferOffset, display_map::DisplaySnapshot}; -use gpui::Context; -use text::Bias; -use ui::Window; - -use crate::Vim; - -impl Vim { - /// Creates a duplicate of every selection below it in the first place that has both its start - /// and end - pub(super) fn helix_duplicate_selections_below( - &mut self, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.duplicate_selections( - times, - window, - cx, - |prev_point| *prev_point.row_mut() += 1, - |prev_range, map| prev_range.end.row() >= map.max_point().row(), - false, - ); - } - - /// Creates a duplicate of every selection above it in the first place that has both its start - /// and end - pub(super) fn helix_duplicate_selections_above( - &mut self, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.duplicate_selections( - times, - window, - cx, - |prev_point| *prev_point.row_mut() = prev_point.row().0.saturating_sub(1), - |prev_range, _| prev_range.start.row() == DisplayPoint::zero().row(), - true, - ); - } - - fn duplicate_selections( - &mut self, - times: Option, - window: &mut Window, - cx: &mut Context, - advance_search: impl Fn(&mut DisplayPoint), - end_search: impl Fn(&Range, &DisplaySnapshot) -> bool, - above: bool, - ) { - let times = times.unwrap_or(1); - self.update_editor(cx, |_, editor, cx| { - let mut selections = Vec::new(); - let map = editor.display_snapshot(cx); - let mut original_selections = editor.selections.all_display(&map); - // The order matters, because it is recorded when the selections are added. - if above { - original_selections.reverse(); - } - - for origin in original_selections { - let origin = origin.tail()..origin.head(); - selections.push(display_point_range_to_offset_range(&origin, &map)); - let mut last_origin = origin; - for _ in 1..=times { - if let Some(duplicate) = find_next_valid_duplicate_space( - last_origin.clone(), - &map, - &advance_search, - &end_search, - ) { - selections.push(display_point_range_to_offset_range(&duplicate, &map)); - last_origin = duplicate; - } else { - break; - } - } - } - - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges(selections); - }); - }); - } -} - -fn find_next_valid_duplicate_space( - mut origin: Range, - map: &DisplaySnapshot, - advance_search: &impl Fn(&mut DisplayPoint), - end_search: &impl Fn(&Range, &DisplaySnapshot) -> bool, -) -> Option> { - while !end_search(&origin, map) { - advance_search(&mut origin.start); - advance_search(&mut origin.end); - - if map.clip_point(origin.start, Bias::Left) == origin.start - && map.clip_point(origin.end, Bias::Right) == origin.end - { - return Some(origin); - } - } - None -} - -fn display_point_range_to_offset_range( - range: &Range, - map: &DisplaySnapshot, -) -> Range { - range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right) -} - -#[cfg(test)] -mod tests { - use db::indoc; - - use crate::{state::Mode, test::VimTestContext}; - - #[gpui::test] - async fn test_selection_duplication(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - cx.set_state( - indoc! {" - The quick brown - fox «jumpsˇ» - over the - lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("C"); - - cx.assert_state( - indoc! {" - The quick brown - fox «jumpsˇ» - over the - lazy« dog.ˇ»"}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("C"); - - cx.assert_state( - indoc! {" - The quick brown - fox «jumpsˇ» - over the - lazy« dog.ˇ»"}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("alt-C"); - - cx.assert_state( - indoc! {" - The «quickˇ» brown - fox «jumpsˇ» - over the - lazy« dog.ˇ»"}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes(","); - - cx.assert_state( - indoc! {" - The «quickˇ» brown - fox jumps - over the - lazy dog."}, - Mode::HelixNormal, - ); - } - - #[gpui::test] - async fn test_selection_duplication_backwards(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - cx.set_state( - indoc! {" - The quick brown - «ˇfox» jumps - over the - lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("C C alt-C"); - - cx.assert_state( - indoc! {" - «ˇThe» quick brown - «ˇfox» jumps - «ˇove»r the - «ˇlaz»y dog."}, - Mode::HelixNormal, - ); - } - - #[gpui::test] - async fn test_selection_duplication_count(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - cx.set_state( - indoc! {" - The «qˇ»uick brown - fox jumps - over the - lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("9 C"); - - cx.assert_state( - indoc! {" - The «qˇ»uick brown - fox «jˇ»umps - over« ˇ»the - lazy« ˇ»dog."}, - Mode::HelixNormal, - ); - } -} diff --git a/crates/vim/src/helix/object.rs b/crates/vim/src/helix/object.rs deleted file mode 100644 index 798cd7162e..0000000000 --- a/crates/vim/src/helix/object.rs +++ /dev/null @@ -1,182 +0,0 @@ -use std::{ - error::Error, - fmt::{self, Display}, - ops::Range, -}; - -use editor::{DisplayPoint, display_map::DisplaySnapshot, movement}; -use text::Selection; - -use crate::{ - helix::boundary::{FuzzyBoundary, ImmediateBoundary}, - object::Object as VimObject, -}; - -/// A text object from helix or an extra one -pub trait HelixTextObject { - fn range( - &self, - map: &DisplaySnapshot, - relative_to: Range, - around: bool, - ) -> Option>; - - fn next_range( - &self, - map: &DisplaySnapshot, - relative_to: Range, - around: bool, - ) -> Option>; - - fn previous_range( - &self, - map: &DisplaySnapshot, - relative_to: Range, - around: bool, - ) -> Option>; -} - -impl VimObject { - /// Returns the range of the object the cursor is over. - /// Follows helix convention. - pub fn helix_range( - self, - map: &DisplaySnapshot, - selection: Selection, - around: bool, - ) -> Result>, VimToHelixError> { - let cursor = cursor_range(&selection, map); - if let Some(helix_object) = self.to_helix_object() { - Ok(helix_object.range(map, cursor, around)) - } else { - Err(VimToHelixError) - } - } - /// Returns the range of the next object the cursor is not over. - /// Follows helix convention. - pub fn helix_next_range( - self, - map: &DisplaySnapshot, - selection: Selection, - around: bool, - ) -> Result>, VimToHelixError> { - let cursor = cursor_range(&selection, map); - if let Some(helix_object) = self.to_helix_object() { - Ok(helix_object.next_range(map, cursor, around)) - } else { - Err(VimToHelixError) - } - } - /// Returns the range of the previous object the cursor is not over. - /// Follows helix convention. - pub fn helix_previous_range( - self, - map: &DisplaySnapshot, - selection: Selection, - around: bool, - ) -> Result>, VimToHelixError> { - let cursor = cursor_range(&selection, map); - if let Some(helix_object) = self.to_helix_object() { - Ok(helix_object.previous_range(map, cursor, around)) - } else { - Err(VimToHelixError) - } - } -} - -#[derive(Debug)] -pub struct VimToHelixError; -impl Display for VimToHelixError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "Not all vim text objects have an implemented helix equivalent" - ) - } -} -impl Error for VimToHelixError {} - -impl VimObject { - fn to_helix_object(self) -> Option> { - Some(match self { - Self::AngleBrackets => Box::new(ImmediateBoundary::AngleBrackets), - Self::BackQuotes => Box::new(ImmediateBoundary::BackQuotes), - Self::CurlyBrackets => Box::new(ImmediateBoundary::CurlyBrackets), - Self::DoubleQuotes => Box::new(ImmediateBoundary::DoubleQuotes), - Self::Paragraph => Box::new(FuzzyBoundary::Paragraph), - Self::Parentheses => Box::new(ImmediateBoundary::Parentheses), - Self::Quotes => Box::new(ImmediateBoundary::SingleQuotes), - Self::Sentence => Box::new(FuzzyBoundary::Sentence), - Self::SquareBrackets => Box::new(ImmediateBoundary::SquareBrackets), - Self::Subword { ignore_punctuation } => { - Box::new(ImmediateBoundary::Subword { ignore_punctuation }) - } - Self::VerticalBars => Box::new(ImmediateBoundary::VerticalBars), - Self::Word { ignore_punctuation } => { - Box::new(ImmediateBoundary::Word { ignore_punctuation }) - } - _ => return None, - }) - } -} - -/// Returns the start of the cursor of a selection, whether that is collapsed or not. -pub(crate) fn cursor_range( - selection: &Selection, - map: &DisplaySnapshot, -) -> Range { - if selection.is_empty() | selection.reversed { - selection.head()..movement::right(map, selection.head()) - } else { - movement::left(map, selection.head())..selection.head() - } -} - -#[cfg(test)] -mod test { - use db::indoc; - - use crate::{state::Mode, test::VimTestContext}; - - #[gpui::test] - async fn test_select_word_object(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - let start = indoc! {" - The quick brˇowˇnˇ - fox «ˇjumps» ov«er - the laˇ»zy dogˇ - - " - }; - - cx.set_state(start, Mode::HelixNormal); - - cx.simulate_keystrokes("m i w"); - - cx.assert_state( - indoc! {" - The quick «brownˇ» - fox «jumpsˇ» over - the «lazyˇ» dogˇ - - " - }, - Mode::HelixNormal, - ); - - cx.set_state(start, Mode::HelixNormal); - - cx.simulate_keystrokes("m a w"); - - cx.assert_state( - indoc! {" - The quick« brownˇ» - fox «jumps ˇ»over - the «lazy ˇ»dogˇ - - " - }, - Mode::HelixNormal, - ); - } -} diff --git a/crates/vim/src/helix/paste.rs b/crates/vim/src/helix/paste.rs deleted file mode 100644 index d91b138853..0000000000 --- a/crates/vim/src/helix/paste.rs +++ /dev/null @@ -1,455 +0,0 @@ -use editor::{ToOffset, movement}; -use gpui::{Action, Context, Window}; -use schemars::JsonSchema; -use serde::Deserialize; - -use crate::{Vim, state::Mode}; - -/// Pastes text from the specified register at the cursor position. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub struct HelixPaste { - #[serde(default)] - before: bool, -} - -impl Vim { - pub fn helix_paste( - &mut self, - action: &HelixPaste, - window: &mut Window, - cx: &mut Context, - ) { - self.record_current_action(cx); - self.store_visual_marks(window, cx); - let count = Vim::take_count(cx).unwrap_or(1); - // TODO: vim paste calls take_forced_motion here, but I don't know what that does - // (none of the other helix_ methods call it) - - self.update_editor(cx, |vim, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - - let selected_register = vim.selected_register.take(); - - let Some((text, clipboard_selections)) = Vim::update_globals(cx, |globals, cx| { - globals.read_register(selected_register, Some(editor), cx) - }) - .and_then(|reg| { - (!reg.text.is_empty()) - .then_some(reg.text) - .zip(reg.clipboard_selections) - }) else { - return; - }; - - let display_map = editor.display_snapshot(cx); - let current_selections = editor.selections.all_adjusted_display(&display_map); - - // The clipboard can have multiple selections, and there can - // be multiple selections. Helix zips them together, so the first - // clipboard entry gets pasted at the first selection, the second - // entry gets pasted at the second selection, and so on. If there - // are more clipboard selections than selections, the extra ones - // don't get pasted anywhere. If there are more selections than - // clipboard selections, the last clipboard selection gets - // pasted at all remaining selections. - - let mut edits = Vec::new(); - let mut new_selections = Vec::new(); - let mut start_offset = 0; - - let mut replacement_texts: Vec = Vec::new(); - - for ix in 0..current_selections.len() { - let to_insert = if let Some(clip_sel) = clipboard_selections.get(ix) { - let end_offset = start_offset + clip_sel.len; - let text = text[start_offset..end_offset].to_string(); - start_offset = end_offset + 1; - text - } else if let Some(last_text) = replacement_texts.last() { - // We have more current selections than clipboard selections: repeat the last one. - last_text.to_owned() - } else { - text.to_string() - }; - replacement_texts.push(to_insert); - } - - let line_mode = replacement_texts.iter().any(|text| text.ends_with('\n')); - - for (to_insert, sel) in replacement_texts.into_iter().zip(current_selections) { - // Helix doesn't care about the head/tail of the selection. - // Pasting before means pasting before the whole selection. - let display_point = if line_mode { - if action.before { - movement::line_beginning(&display_map, sel.start, false) - } else { - if sel.start == sel.end { - movement::right( - &display_map, - movement::line_end(&display_map, sel.end, false), - ) - } else { - sel.end - } - } - } else if action.before { - sel.start - } else if sel.start == sel.end { - // Helix and Zed differ in how they understand - // single-point cursors. In Helix, a single-point cursor - // is "on top" of some character, and pasting after that - // cursor means that the pasted content should go after - // that character. (If the cursor is at the end of a - // line, the pasted content goes on the next line.) - movement::right(&display_map, sel.end) - } else { - sel.end - }; - let point = display_point.to_point(&display_map); - let anchor = if action.before { - display_map.buffer_snapshot().anchor_after(point) - } else { - display_map.buffer_snapshot().anchor_before(point) - }; - edits.push((point..point, to_insert.repeat(count))); - new_selections.push((anchor, to_insert.len() * count)); - } - - editor.edit(edits, cx); - - let snapshot = editor.buffer().read(cx).snapshot(cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges(new_selections.into_iter().map(|(anchor, len)| { - let offset = anchor.to_offset(&snapshot); - if action.before { - offset.saturating_sub_usize(len)..offset - } else { - offset..(offset + len) - } - })); - }) - }); - }); - - self.switch_mode(Mode::HelixNormal, true, window, cx); - } -} - -#[cfg(test)] -mod test { - use indoc::indoc; - - use crate::{state::Mode, test::VimTestContext}; - - #[gpui::test] - async fn test_paste(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state( - indoc! {" - The «quiˇ»ck brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("y w p"); - - cx.assert_state( - indoc! {" - The quick «quiˇ»brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - // Pasting before the selection: - cx.set_state( - indoc! {" - The quick brown - fox «jumpsˇ» over - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("shift-p"); - cx.assert_state( - indoc! {" - The quick brown - fox «quiˇ»jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - } - - #[gpui::test] - async fn test_point_selection_paste(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state( - indoc! {" - The quiˇck brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("y"); - - // Pasting before the selection: - cx.set_state( - indoc! {" - The quick brown - fox jumpsˇ over - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("shift-p"); - cx.assert_state( - indoc! {" - The quick brown - fox jumps«cˇ» over - the lazy dog."}, - Mode::HelixNormal, - ); - - // Pasting after the selection: - cx.set_state( - indoc! {" - The quick brown - fox jumpsˇ over - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("p"); - cx.assert_state( - indoc! {" - The quick brown - fox jumps «cˇ»over - the lazy dog."}, - Mode::HelixNormal, - ); - - // Pasting after the selection at the end of a line: - cx.set_state( - indoc! {" - The quick brown - fox jumps overˇ - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("p"); - cx.assert_state( - indoc! {" - The quick brown - fox jumps over - «cˇ»the lazy dog."}, - Mode::HelixNormal, - ); - } - - #[gpui::test] - async fn test_multi_cursor_paste(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - // Select two blocks of text. - cx.set_state( - indoc! {" - The «quiˇ»ck brown - fox ju«mpsˇ» over - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("y"); - - // Only one cursor: only the first block gets pasted. - cx.set_state( - indoc! {" - ˇThe quick brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("shift-p"); - cx.assert_state( - indoc! {" - «quiˇ»The quick brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - // Two cursors: both get pasted. - cx.set_state( - indoc! {" - ˇThe ˇquick brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("shift-p"); - cx.assert_state( - indoc! {" - «quiˇ»The «mpsˇ»quick brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - // Three cursors: the second yanked block is duplicated. - cx.set_state( - indoc! {" - ˇThe ˇquick brown - fox jumpsˇ over - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("shift-p"); - cx.assert_state( - indoc! {" - «quiˇ»The «mpsˇ»quick brown - fox jumps«mpsˇ» over - the lazy dog."}, - Mode::HelixNormal, - ); - - // Again with three cursors. All three should be pasted twice. - cx.set_state( - indoc! {" - ˇThe ˇquick brown - fox jumpsˇ over - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("2 shift-p"); - cx.assert_state( - indoc! {" - «quiquiˇ»The «mpsmpsˇ»quick brown - fox jumps«mpsmpsˇ» over - the lazy dog."}, - Mode::HelixNormal, - ); - } - - #[gpui::test] - async fn test_line_mode_paste(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state( - indoc! {" - The quick brow«n - ˇ»fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("y shift-p"); - - cx.assert_state( - indoc! {" - «n - ˇ»The quick brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - // In line mode, if we're in the middle of a line then pasting before pastes on - // the line before. - cx.set_state( - indoc! {" - The quick brown - fox jumpsˇ over - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("shift-p"); - cx.assert_state( - indoc! {" - The quick brown - «n - ˇ»fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - // In line mode, if we're in the middle of a line then pasting after pastes on - // the line after. - cx.set_state( - indoc! {" - The quick brown - fox jumpsˇ over - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("p"); - cx.assert_state( - indoc! {" - The quick brown - fox jumps over - «n - ˇ»the lazy dog."}, - Mode::HelixNormal, - ); - - // If we're currently at the end of a line, "the line after" - // means right after the cursor. - cx.set_state( - indoc! {" - The quick brown - fox jumps overˇ - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("p"); - cx.assert_state( - indoc! {" - The quick brown - fox jumps over - «n - ˇ»the lazy dog."}, - Mode::HelixNormal, - ); - - cx.set_state( - indoc! {" - - The quick brown - fox jumps overˇ - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("x y up up p"); - cx.assert_state( - indoc! {" - - «fox jumps over - ˇ»The quick brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.set_state( - indoc! {" - «The quick brown - fox jumps over - ˇ»the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("y p p"); - cx.assert_state( - indoc! {" - The quick brown - fox jumps over - The quick brown - fox jumps over - «The quick brown - fox jumps over - ˇ»the lazy dog."}, - Mode::HelixNormal, - ); - } -} diff --git a/crates/vim/src/helix/select.rs b/crates/vim/src/helix/select.rs deleted file mode 100644 index d782e8b450..0000000000 --- a/crates/vim/src/helix/select.rs +++ /dev/null @@ -1,84 +0,0 @@ -use text::SelectionGoal; -use ui::{Context, Window}; - -use crate::{Vim, helix::object::cursor_range, object::Object}; - -impl Vim { - /// Selects the object each cursor is over. - /// Follows helix convention. - pub fn select_current_object( - &mut self, - object: Object, - around: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let Some(range) = object - .helix_range(map, selection.clone(), around) - .unwrap_or({ - let vim_range = object.range(map, selection.clone(), around, None); - vim_range.filter(|r| r.start <= cursor_range(selection, map).start) - }) - else { - return; - }; - - selection.set_head_tail(range.end, range.start, SelectionGoal::None); - }); - }); - }); - } - - /// Selects the next object from each cursor which the cursor is not over. - /// Follows helix convention. - pub fn select_next_object( - &mut self, - object: Object, - around: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let Ok(Some(range)) = object.helix_next_range(map, selection.clone(), around) - else { - return; - }; - - selection.set_head_tail(range.end, range.start, SelectionGoal::None); - }); - }); - }); - } - - /// Selects the previous object from each cursor which the cursor is not over. - /// Follows helix convention. - pub fn select_previous_object( - &mut self, - object: Object, - around: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let Ok(Some(range)) = - object.helix_previous_range(map, selection.clone(), around) - else { - return; - }; - - selection.set_head_tail(range.start, range.end, SelectionGoal::None); - }); - }); - }); - } -} diff --git a/crates/vim/src/indent.rs b/crates/vim/src/indent.rs deleted file mode 100644 index 927edf4d9a..0000000000 --- a/crates/vim/src/indent.rs +++ /dev/null @@ -1,247 +0,0 @@ -use crate::{Vim, motion::Motion, object::Object, state::Mode}; -use collections::HashMap; -use editor::SelectionEffects; -use editor::{Bias, Editor, display_map::ToDisplayPoint}; -use gpui::actions; -use gpui::{Context, Window}; -use language::SelectionGoal; -use settings::Settings; -use vim_mode_setting::HelixModeSetting; - -#[derive(PartialEq, Eq)] -pub(crate) enum IndentDirection { - In, - Out, - Auto, -} - -actions!( - vim, - [ - /// Increases indentation of selected lines. - Indent, - /// Decreases indentation of selected lines. - Outdent, - /// Automatically adjusts indentation based on syntax. - AutoIndent - ] -); - -pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, |vim, _: &Indent, window, cx| { - vim.record_current_action(cx); - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - vim.store_visual_marks(window, cx); - vim.update_editor(cx, |vim, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let original_positions = vim.save_selection_starts(editor, cx); - for _ in 0..count { - editor.indent(&Default::default(), window, cx); - } - if !HelixModeSetting::get_global(cx).0 { - vim.restore_selection_cursors(editor, window, cx, original_positions); - } - }); - }); - if vim.mode.is_visual() { - vim.switch_mode(Mode::Normal, true, window, cx) - } - }); - - Vim::action(editor, cx, |vim, _: &Outdent, window, cx| { - vim.record_current_action(cx); - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - vim.store_visual_marks(window, cx); - vim.update_editor(cx, |vim, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let original_positions = vim.save_selection_starts(editor, cx); - for _ in 0..count { - editor.outdent(&Default::default(), window, cx); - } - if !HelixModeSetting::get_global(cx).0 { - vim.restore_selection_cursors(editor, window, cx, original_positions); - } - }); - }); - if vim.mode.is_visual() { - vim.switch_mode(Mode::Normal, true, window, cx) - } - }); - - Vim::action(editor, cx, |vim, _: &AutoIndent, window, cx| { - vim.record_current_action(cx); - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - vim.store_visual_marks(window, cx); - vim.update_editor(cx, |vim, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let original_positions = vim.save_selection_starts(editor, cx); - for _ in 0..count { - editor.autoindent(&Default::default(), window, cx); - } - vim.restore_selection_cursors(editor, window, cx, original_positions); - }); - }); - if vim.mode.is_visual() { - vim.switch_mode(Mode::Normal, true, window, cx) - } - }); -} - -impl Vim { - pub(crate) fn indent_motion( - &mut self, - motion: Motion, - times: Option, - forced_motion: bool, - dir: IndentDirection, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - let mut selection_starts: HashMap<_, _> = Default::default(); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); - selection_starts.insert(selection.id, anchor); - motion.expand_selection( - map, - selection, - times, - &text_layout_details, - forced_motion, - ); - }); - }); - match dir { - IndentDirection::In => editor.indent(&Default::default(), window, cx), - IndentDirection::Out => editor.outdent(&Default::default(), window, cx), - IndentDirection::Auto => editor.autoindent(&Default::default(), window, cx), - } - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = selection_starts.remove(&selection.id).unwrap(); - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); - }); - }); - }); - }); - } - - pub(crate) fn indent_object( - &mut self, - object: Object, - around: bool, - dir: IndentDirection, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let mut original_positions: HashMap<_, _> = Default::default(); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); - original_positions.insert(selection.id, anchor); - object.expand_selection(map, selection, around, times); - }); - }); - match dir { - IndentDirection::In => editor.indent(&Default::default(), window, cx), - IndentDirection::Out => editor.outdent(&Default::default(), window, cx), - IndentDirection::Auto => editor.autoindent(&Default::default(), window, cx), - } - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = original_positions.remove(&selection.id).unwrap(); - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); - }); - }); - }); - }); - } -} - -#[cfg(test)] -mod test { - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - use indoc::indoc; - - #[gpui::test] - async fn test_indent_gv(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_neovim_option("shiftwidth=4").await; - - cx.set_shared_state("ˇhello\nworld\n").await; - cx.simulate_shared_keystrokes("v j > g v").await; - cx.shared_state() - .await - .assert_eq("« hello\n ˇ» world\n"); - } - - #[gpui::test] - async fn test_indent_hx(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - cx.set_state("«Hello\nWorldˇ»\n", Mode::HelixNormal); - - cx.simulate_keystrokes(">"); - cx.assert_state(" «Hello\n Worldˇ»\n", Mode::HelixNormal); - - cx.simulate_keystrokes("<"); - cx.assert_state("«Hello\nWorldˇ»\n", Mode::HelixNormal); - } - - #[gpui::test] - async fn test_autoindent_op(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc!( - " - fn a() { - b(); - c(); - - d(); - ˇe(); - f(); - - g(); - } - " - ), - Mode::Normal, - ); - - cx.simulate_keystrokes("= a p"); - cx.assert_state( - indoc!( - " - fn a() { - b(); - c(); - - d(); - ˇe(); - f(); - - g(); - } - " - ), - Mode::Normal, - ); - } -} diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs deleted file mode 100644 index d5323f31dc..0000000000 --- a/crates/vim/src/insert.rs +++ /dev/null @@ -1,205 +0,0 @@ -use crate::{Vim, state::Mode}; -use editor::{Bias, Editor}; -use gpui::{Action, Context, Window, actions}; -use language::SelectionGoal; -use settings::Settings; -use text::Point; -use vim_mode_setting::HelixModeSetting; -use workspace::searchable::Direction; - -actions!( - vim, - [ - /// Switches to normal mode with cursor positioned before the current character. - NormalBefore, - /// Temporarily switches to normal mode for one command. - TemporaryNormal, - /// Inserts the next character from the line above into the current line. - InsertFromAbove, - /// Inserts the next character from the line below into the current line. - InsertFromBelow - ] -); - -pub fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, Vim::normal_before); - Vim::action(editor, cx, Vim::temporary_normal); - Vim::action(editor, cx, |vim, _: &InsertFromAbove, window, cx| { - vim.insert_around(Direction::Prev, window, cx) - }); - Vim::action(editor, cx, |vim, _: &InsertFromBelow, window, cx| { - vim.insert_around(Direction::Next, window, cx) - }) -} - -impl Vim { - pub(crate) fn normal_before( - &mut self, - action: &NormalBefore, - window: &mut Window, - cx: &mut Context, - ) { - if self.active_operator().is_some() { - self.operator_stack.clear(); - self.sync_vim_settings(window, cx); - return; - } - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - self.stop_recording_immediately(action.boxed_clone(), cx); - if count <= 1 || Vim::globals(cx).dot_replaying { - self.create_mark("^".into(), window, cx); - - if HelixModeSetting::get_global(cx).0 { - self.update_editor(cx, |_, editor, cx| { - editor.dismiss_menus_and_popups(false, window, cx); - }); - self.switch_mode(Mode::HelixNormal, false, window, cx); - return; - } - - self.update_editor(cx, |_, editor, cx| { - editor.dismiss_menus_and_popups(false, window, cx); - - editor.change_selections(Default::default(), window, cx, |s| { - s.move_cursors_with(|map, mut cursor, _| { - *cursor.column_mut() = cursor.column().saturating_sub(1); - (map.clip_point(cursor, Bias::Left), SelectionGoal::None) - }); - }); - }); - - self.switch_mode(Mode::Normal, false, window, cx); - return; - } - - self.repeat(true, window, cx) - } - - fn temporary_normal( - &mut self, - _: &TemporaryNormal, - window: &mut Window, - cx: &mut Context, - ) { - self.switch_mode(Mode::Normal, true, window, cx); - self.temp_mode = true; - } - - fn insert_around(&mut self, direction: Direction, _: &mut Window, cx: &mut Context) { - self.update_editor(cx, |_, editor, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let mut edits = Vec::new(); - for selection in editor.selections.all::(&editor.display_snapshot(cx)) { - let point = selection.head(); - let new_row = match direction { - Direction::Next => point.row + 1, - Direction::Prev if point.row > 0 => point.row - 1, - _ => continue, - }; - let source = snapshot.clip_point(Point::new(new_row, point.column), Bias::Left); - if let Some(c) = snapshot.chars_at(source).next() - && c != '\n' - { - edits.push((point..point, c.to_string())) - } - } - - editor.edit(edits, cx); - }); - } -} - -#[cfg(test)] -mod test { - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - - #[gpui::test] - async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.simulate_keystrokes("i"); - assert_eq!(cx.mode(), Mode::Insert); - cx.simulate_keystrokes("T e s t"); - cx.assert_editor_state("Testˇ"); - cx.simulate_keystrokes("escape"); - assert_eq!(cx.mode(), Mode::Normal); - cx.assert_editor_state("Tesˇt"); - } - - #[gpui::test] - async fn test_insert_with_counts(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("5 i - escape").await; - cx.shared_state().await.assert_eq("----ˇ-hello\n"); - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("5 a - escape").await; - cx.shared_state().await.assert_eq("h----ˇ-ello\n"); - - cx.simulate_shared_keystrokes("4 shift-i - escape").await; - cx.shared_state().await.assert_eq("---ˇ-h-----ello\n"); - - cx.simulate_shared_keystrokes("3 shift-a - escape").await; - cx.shared_state().await.assert_eq("----h-----ello--ˇ-\n"); - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("3 o o i escape").await; - cx.shared_state().await.assert_eq("hello\noi\noi\noˇi\n"); - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("3 shift-o o i escape").await; - cx.shared_state().await.assert_eq("oi\noi\noˇi\nhello\n"); - } - - #[gpui::test] - async fn test_insert_with_repeat(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("3 i - escape").await; - cx.shared_state().await.assert_eq("--ˇ-hello\n"); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("----ˇ--hello\n"); - cx.simulate_shared_keystrokes("2 .").await; - cx.shared_state().await.assert_eq("-----ˇ---hello\n"); - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("2 o k k escape").await; - cx.shared_state().await.assert_eq("hello\nkk\nkˇk\n"); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state() - .await - .assert_eq("hello\nkk\nkk\nkk\nkˇk\n"); - cx.simulate_shared_keystrokes("1 .").await; - cx.shared_state() - .await - .assert_eq("hello\nkk\nkk\nkk\nkk\nkˇk\n"); - } - - #[gpui::test] - async fn test_insert_ctrl_r(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("heˇllo\n").await; - cx.simulate_shared_keystrokes("y y i ctrl-r \"").await; - cx.shared_state().await.assert_eq("hehello\nˇllo\n"); - - cx.simulate_shared_keystrokes("ctrl-r x ctrl-r escape") - .await; - cx.shared_state().await.assert_eq("hehello\nˇllo\n"); - } - - #[gpui::test] - async fn test_insert_ctrl_y(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("hello\nˇ\nworld").await; - cx.simulate_shared_keystrokes("i ctrl-y ctrl-e").await; - cx.shared_state().await.assert_eq("hello\nhoˇ\nworld"); - } -} diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs deleted file mode 100644 index 42d4915fc5..0000000000 --- a/crates/vim/src/mode_indicator.rs +++ /dev/null @@ -1,180 +0,0 @@ -use gpui::{Context, Element, Entity, FontWeight, Render, Subscription, WeakEntity, Window, div}; -use ui::text_for_keystrokes; -use workspace::{StatusItemView, item::ItemHandle, ui::prelude::*}; - -use crate::{Vim, VimEvent, VimGlobals}; - -/// The ModeIndicator displays the current mode in the status bar. -pub struct ModeIndicator { - vim: Option>, - pending_keys: Option, - vim_subscription: Option, -} - -impl ModeIndicator { - /// Construct a new mode indicator in this window. - pub fn new(window: &mut Window, cx: &mut Context) -> Self { - cx.observe_pending_input(window, |this: &mut Self, window, cx| { - this.update_pending_keys(window, cx); - cx.notify(); - }) - .detach(); - - let handle = cx.entity(); - let window_handle = window.window_handle(); - cx.observe_new::(move |_, window, cx| { - let Some(window) = window else { - return; - }; - if window.window_handle() != window_handle { - return; - } - let vim = cx.entity(); - handle.update(cx, |_, cx| { - cx.subscribe(&vim, |mode_indicator, vim, event, cx| match event { - VimEvent::Focused => { - mode_indicator.vim_subscription = - Some(cx.observe(&vim, |_, _, cx| cx.notify())); - mode_indicator.vim = Some(vim.downgrade()); - } - }) - .detach() - }) - }) - .detach(); - - Self { - vim: None, - pending_keys: None, - vim_subscription: None, - } - } - - fn update_pending_keys(&mut self, window: &mut Window, cx: &App) { - self.pending_keys = window - .pending_input_keystrokes() - .map(|keystrokes| text_for_keystrokes(keystrokes, cx)); - } - - fn vim(&self) -> Option> { - self.vim.as_ref().and_then(|vim| vim.upgrade()) - } - - fn current_operators_description(&self, vim: Entity, cx: &mut Context) -> String { - let recording = Vim::globals(cx) - .recording_register - .map(|reg| format!("recording @{reg} ")) - .into_iter(); - - let vim = vim.read(cx); - recording - .chain( - cx.global::() - .pre_count - .map(|count| format!("{}", count)), - ) - .chain(vim.selected_register.map(|reg| format!("\"{reg}"))) - .chain(vim.operator_stack.iter().map(|item| item.status())) - .chain( - cx.global::() - .post_count - .map(|count| format!("{}", count)), - ) - .collect::>() - .join("") - } -} - -impl Render for ModeIndicator { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let vim = self.vim(); - let Some(vim) = vim else { - return div().hidden().into_any_element(); - }; - - let vim_readable = vim.read(cx); - let status_label = vim_readable.status_label.clone(); - let temp_mode = vim_readable.temp_mode; - let mode = vim_readable.mode; - - let theme = cx.theme(); - let colors = theme.colors(); - let system_transparent = gpui::hsla(0.0, 0.0, 0.0, 0.0); - let vim_mode_text = colors.vim_mode_text; - let bg_color = match mode { - crate::state::Mode::Normal => colors.vim_normal_background, - crate::state::Mode::Insert => colors.vim_insert_background, - crate::state::Mode::Replace => colors.vim_replace_background, - crate::state::Mode::Visual => colors.vim_visual_background, - crate::state::Mode::VisualLine => colors.vim_visual_line_background, - crate::state::Mode::VisualBlock => colors.vim_visual_block_background, - crate::state::Mode::HelixNormal => colors.vim_helix_normal_background, - crate::state::Mode::HelixSelect => colors.vim_helix_select_background, - }; - - let (label, mode): (SharedString, Option) = if let Some(label) = status_label - { - (label, None) - } else { - let mode_str = if temp_mode { - format!("(insert) {}", mode) - } else { - mode.to_string() - }; - - let current_operators_description = self.current_operators_description(vim.clone(), cx); - let pending = self - .pending_keys - .as_ref() - .unwrap_or(¤t_operators_description); - let mode = if bg_color != system_transparent { - mode_str.into() - } else { - format!("-- {} --", mode_str).into() - }; - (pending.into(), Some(mode)) - }; - h_flex() - .gap_1() - .when(!label.is_empty(), |el| { - el.child( - Label::new(label) - .line_height_style(LineHeightStyle::UiLabel) - .weight(FontWeight::MEDIUM), - ) - }) - .when_some(mode, |el, mode| { - el.child( - v_flex() - .when(bg_color != system_transparent, |el| el.px_2()) - // match with other icons at the bottom that use default buttons - .h(ButtonSize::Default.rems()) - .justify_center() - .rounded_sm() - .bg(bg_color) - .child( - Label::new(mode) - .size(LabelSize::Small) - .line_height_style(LineHeightStyle::UiLabel) - .weight(FontWeight::MEDIUM) - .when( - bg_color != system_transparent - && vim_mode_text != system_transparent, - |el| el.color(Color::Custom(vim_mode_text)), - ), - ), - ) - }) - .into_any() - } -} - -impl StatusItemView for ModeIndicator { - fn set_active_pane_item( - &mut self, - _active_pane_item: Option<&dyn ItemHandle>, - _window: &mut Window, - _cx: &mut Context, - ) { - } -} diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs deleted file mode 100644 index 6ba28a1c23..0000000000 --- a/crates/vim/src/motion.rs +++ /dev/null @@ -1,4484 +0,0 @@ -use editor::{ - Anchor, Bias, BufferOffset, DisplayPoint, Editor, MultiBufferOffset, RowExt, ToOffset, ToPoint, - display_map::{DisplayRow, DisplaySnapshot, FoldPoint, ToDisplayPoint}, - movement::{ - self, FindRange, TextLayoutDetails, find_boundary, find_preceding_boundary_display_point, - }, -}; -use gpui::{Action, Context, Window, actions, px}; -use language::{CharKind, Point, Selection, SelectionGoal}; -use multi_buffer::MultiBufferRow; -use schemars::JsonSchema; -use serde::Deserialize; -use std::ops::Range; -use workspace::searchable::Direction; - -use crate::{ - Vim, - normal::mark, - state::{Mode, Operator}, - surrounds::SurroundsType, -}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum MotionKind { - Linewise, - Exclusive, - Inclusive, -} - -impl MotionKind { - pub(crate) fn for_mode(mode: Mode) -> Self { - match mode { - Mode::VisualLine => MotionKind::Linewise, - _ => MotionKind::Exclusive, - } - } - - pub(crate) fn linewise(&self) -> bool { - matches!(self, MotionKind::Linewise) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Motion { - Left, - WrappingLeft, - Down { - display_lines: bool, - }, - Up { - display_lines: bool, - }, - Right, - WrappingRight, - NextWordStart { - ignore_punctuation: bool, - }, - NextWordEnd { - ignore_punctuation: bool, - }, - PreviousWordStart { - ignore_punctuation: bool, - }, - PreviousWordEnd { - ignore_punctuation: bool, - }, - NextSubwordStart { - ignore_punctuation: bool, - }, - NextSubwordEnd { - ignore_punctuation: bool, - }, - PreviousSubwordStart { - ignore_punctuation: bool, - }, - PreviousSubwordEnd { - ignore_punctuation: bool, - }, - FirstNonWhitespace { - display_lines: bool, - }, - CurrentLine, - StartOfLine { - display_lines: bool, - }, - MiddleOfLine { - display_lines: bool, - }, - EndOfLine { - display_lines: bool, - }, - SentenceBackward, - SentenceForward, - StartOfParagraph, - EndOfParagraph, - StartOfDocument, - EndOfDocument, - Matching, - GoToPercentage, - UnmatchedForward { - char: char, - }, - UnmatchedBackward { - char: char, - }, - FindForward { - before: bool, - char: char, - mode: FindRange, - smartcase: bool, - }, - FindBackward { - after: bool, - char: char, - mode: FindRange, - smartcase: bool, - }, - Sneak { - first_char: char, - second_char: char, - smartcase: bool, - }, - SneakBackward { - first_char: char, - second_char: char, - smartcase: bool, - }, - RepeatFind { - last_find: Box, - }, - RepeatFindReversed { - last_find: Box, - }, - NextLineStart, - PreviousLineStart, - StartOfLineDownward, - EndOfLineDownward, - GoToColumn, - WindowTop, - WindowMiddle, - WindowBottom, - NextSectionStart, - NextSectionEnd, - PreviousSectionStart, - PreviousSectionEnd, - NextMethodStart, - NextMethodEnd, - PreviousMethodStart, - PreviousMethodEnd, - NextComment, - PreviousComment, - PreviousLesserIndent, - PreviousGreaterIndent, - PreviousSameIndent, - NextLesserIndent, - NextGreaterIndent, - NextSameIndent, - - // we don't have a good way to run a search synchronously, so - // we handle search motions by running the search async and then - // calling back into motion with this - ZedSearchResult { - prior_selections: Vec>, - new_selections: Vec>, - }, - Jump { - anchor: Anchor, - line: bool, - }, -} - -#[derive(Clone, Copy)] -enum IndentType { - Lesser, - Greater, - Same, -} - -/// Moves to the start of the next word. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct NextWordStart { - #[serde(default)] - ignore_punctuation: bool, -} - -/// Moves to the end of the next word. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct NextWordEnd { - #[serde(default)] - ignore_punctuation: bool, -} - -/// Moves to the start of the previous word. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PreviousWordStart { - #[serde(default)] - ignore_punctuation: bool, -} - -/// Moves to the end of the previous word. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PreviousWordEnd { - #[serde(default)] - ignore_punctuation: bool, -} - -/// Moves to the start of the next subword. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub(crate) struct NextSubwordStart { - #[serde(default)] - pub(crate) ignore_punctuation: bool, -} - -/// Moves to the end of the next subword. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub(crate) struct NextSubwordEnd { - #[serde(default)] - pub(crate) ignore_punctuation: bool, -} - -/// Moves to the start of the previous subword. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub(crate) struct PreviousSubwordStart { - #[serde(default)] - pub(crate) ignore_punctuation: bool, -} - -/// Moves to the end of the previous subword. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub(crate) struct PreviousSubwordEnd { - #[serde(default)] - pub(crate) ignore_punctuation: bool, -} - -/// Moves cursor up by the specified number of lines. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub(crate) struct Up { - #[serde(default)] - pub(crate) display_lines: bool, -} - -/// Moves cursor down by the specified number of lines. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub(crate) struct Down { - #[serde(default)] - pub(crate) display_lines: bool, -} - -/// Moves to the first non-whitespace character on the current line. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct FirstNonWhitespace { - #[serde(default)] - display_lines: bool, -} - -/// Moves to the end of the current line. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct EndOfLine { - #[serde(default)] - display_lines: bool, -} - -/// Moves to the start of the current line. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub struct StartOfLine { - #[serde(default)] - pub(crate) display_lines: bool, -} - -/// Moves to the middle of the current line. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct MiddleOfLine { - #[serde(default)] - display_lines: bool, -} - -/// Finds the next unmatched bracket or delimiter. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct UnmatchedForward { - #[serde(default)] - char: char, -} - -/// Finds the previous unmatched bracket or delimiter. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct UnmatchedBackward { - #[serde(default)] - char: char, -} - -actions!( - vim, - [ - /// Moves cursor left one character. - Left, - /// Moves cursor left one character, wrapping to previous line. - #[action(deprecated_aliases = ["vim::Backspace"])] - WrappingLeft, - /// Moves cursor right one character. - Right, - /// Moves cursor right one character, wrapping to next line. - #[action(deprecated_aliases = ["vim::Space"])] - WrappingRight, - /// Selects the current line. - CurrentLine, - /// Moves to the start of the next sentence. - SentenceForward, - /// Moves to the start of the previous sentence. - SentenceBackward, - /// Moves to the start of the paragraph. - StartOfParagraph, - /// Moves to the end of the paragraph. - EndOfParagraph, - /// Moves to the start of the document. - StartOfDocument, - /// Moves to the end of the document. - EndOfDocument, - /// Moves to the matching bracket or delimiter. - Matching, - /// Goes to a percentage position in the file. - GoToPercentage, - /// Moves to the start of the next line. - NextLineStart, - /// Moves to the start of the previous line. - PreviousLineStart, - /// Moves to the start of a line downward. - StartOfLineDownward, - /// Moves to the end of a line downward. - EndOfLineDownward, - /// Goes to a specific column number. - GoToColumn, - /// Repeats the last character find. - RepeatFind, - /// Repeats the last character find in reverse. - RepeatFindReversed, - /// Moves to the top of the window. - WindowTop, - /// Moves to the middle of the window. - WindowMiddle, - /// Moves to the bottom of the window. - WindowBottom, - /// Moves to the start of the next section. - NextSectionStart, - /// Moves to the end of the next section. - NextSectionEnd, - /// Moves to the start of the previous section. - PreviousSectionStart, - /// Moves to the end of the previous section. - PreviousSectionEnd, - /// Moves to the start of the next method. - NextMethodStart, - /// Moves to the end of the next method. - NextMethodEnd, - /// Moves to the start of the previous method. - PreviousMethodStart, - /// Moves to the end of the previous method. - PreviousMethodEnd, - /// Moves to the next comment. - NextComment, - /// Moves to the previous comment. - PreviousComment, - /// Moves to the previous line with lesser indentation. - PreviousLesserIndent, - /// Moves to the previous line with greater indentation. - PreviousGreaterIndent, - /// Moves to the previous line with the same indentation. - PreviousSameIndent, - /// Moves to the next line with lesser indentation. - NextLesserIndent, - /// Moves to the next line with greater indentation. - NextGreaterIndent, - /// Moves to the next line with the same indentation. - NextSameIndent, - ] -); - -pub fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, |vim, _: &Left, window, cx| { - vim.motion(Motion::Left, window, cx) - }); - Vim::action(editor, cx, |vim, _: &WrappingLeft, window, cx| { - vim.motion(Motion::WrappingLeft, window, cx) - }); - Vim::action(editor, cx, |vim, action: &Down, window, cx| { - vim.motion( - Motion::Down { - display_lines: action.display_lines, - }, - window, - cx, - ) - }); - Vim::action(editor, cx, |vim, action: &Up, window, cx| { - vim.motion( - Motion::Up { - display_lines: action.display_lines, - }, - window, - cx, - ) - }); - Vim::action(editor, cx, |vim, _: &Right, window, cx| { - vim.motion(Motion::Right, window, cx) - }); - Vim::action(editor, cx, |vim, _: &WrappingRight, window, cx| { - vim.motion(Motion::WrappingRight, window, cx) - }); - Vim::action( - editor, - cx, - |vim, action: &FirstNonWhitespace, window, cx| { - vim.motion( - Motion::FirstNonWhitespace { - display_lines: action.display_lines, - }, - window, - cx, - ) - }, - ); - Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| { - vim.motion( - Motion::StartOfLine { - display_lines: action.display_lines, - }, - window, - cx, - ) - }); - Vim::action(editor, cx, |vim, action: &MiddleOfLine, window, cx| { - vim.motion( - Motion::MiddleOfLine { - display_lines: action.display_lines, - }, - window, - cx, - ) - }); - Vim::action(editor, cx, |vim, action: &EndOfLine, window, cx| { - vim.motion( - Motion::EndOfLine { - display_lines: action.display_lines, - }, - window, - cx, - ) - }); - Vim::action(editor, cx, |vim, _: &CurrentLine, window, cx| { - vim.motion(Motion::CurrentLine, window, cx) - }); - Vim::action(editor, cx, |vim, _: &StartOfParagraph, window, cx| { - vim.motion(Motion::StartOfParagraph, window, cx) - }); - Vim::action(editor, cx, |vim, _: &EndOfParagraph, window, cx| { - vim.motion(Motion::EndOfParagraph, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &SentenceForward, window, cx| { - vim.motion(Motion::SentenceForward, window, cx) - }); - Vim::action(editor, cx, |vim, _: &SentenceBackward, window, cx| { - vim.motion(Motion::SentenceBackward, window, cx) - }); - Vim::action(editor, cx, |vim, _: &StartOfDocument, window, cx| { - vim.motion(Motion::StartOfDocument, window, cx) - }); - Vim::action(editor, cx, |vim, _: &EndOfDocument, window, cx| { - vim.motion(Motion::EndOfDocument, window, cx) - }); - Vim::action(editor, cx, |vim, _: &Matching, window, cx| { - vim.motion(Motion::Matching, window, cx) - }); - Vim::action(editor, cx, |vim, _: &GoToPercentage, window, cx| { - vim.motion(Motion::GoToPercentage, window, cx) - }); - Vim::action( - editor, - cx, - |vim, &UnmatchedForward { char }: &UnmatchedForward, window, cx| { - vim.motion(Motion::UnmatchedForward { char }, window, cx) - }, - ); - Vim::action( - editor, - cx, - |vim, &UnmatchedBackward { char }: &UnmatchedBackward, window, cx| { - vim.motion(Motion::UnmatchedBackward { char }, window, cx) - }, - ); - Vim::action( - editor, - cx, - |vim, &NextWordStart { ignore_punctuation }: &NextWordStart, window, cx| { - vim.motion(Motion::NextWordStart { ignore_punctuation }, window, cx) - }, - ); - Vim::action( - editor, - cx, - |vim, &NextWordEnd { ignore_punctuation }: &NextWordEnd, window, cx| { - vim.motion(Motion::NextWordEnd { ignore_punctuation }, window, cx) - }, - ); - Vim::action( - editor, - cx, - |vim, &PreviousWordStart { ignore_punctuation }: &PreviousWordStart, window, cx| { - vim.motion(Motion::PreviousWordStart { ignore_punctuation }, window, cx) - }, - ); - Vim::action( - editor, - cx, - |vim, &PreviousWordEnd { ignore_punctuation }, window, cx| { - vim.motion(Motion::PreviousWordEnd { ignore_punctuation }, window, cx) - }, - ); - Vim::action( - editor, - cx, - |vim, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, window, cx| { - vim.motion(Motion::NextSubwordStart { ignore_punctuation }, window, cx) - }, - ); - Vim::action( - editor, - cx, - |vim, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, window, cx| { - vim.motion(Motion::NextSubwordEnd { ignore_punctuation }, window, cx) - }, - ); - Vim::action( - editor, - cx, - |vim, &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart, window, cx| { - vim.motion( - Motion::PreviousSubwordStart { ignore_punctuation }, - window, - cx, - ) - }, - ); - Vim::action( - editor, - cx, - |vim, &PreviousSubwordEnd { ignore_punctuation }, window, cx| { - vim.motion( - Motion::PreviousSubwordEnd { ignore_punctuation }, - window, - cx, - ) - }, - ); - Vim::action(editor, cx, |vim, &NextLineStart, window, cx| { - vim.motion(Motion::NextLineStart, window, cx) - }); - Vim::action(editor, cx, |vim, &PreviousLineStart, window, cx| { - vim.motion(Motion::PreviousLineStart, window, cx) - }); - Vim::action(editor, cx, |vim, &StartOfLineDownward, window, cx| { - vim.motion(Motion::StartOfLineDownward, window, cx) - }); - Vim::action(editor, cx, |vim, &EndOfLineDownward, window, cx| { - vim.motion(Motion::EndOfLineDownward, window, cx) - }); - Vim::action(editor, cx, |vim, &GoToColumn, window, cx| { - vim.motion(Motion::GoToColumn, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &RepeatFind, window, cx| { - if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) { - vim.motion(Motion::RepeatFind { last_find }, window, cx); - } - }); - - Vim::action(editor, cx, |vim, _: &RepeatFindReversed, window, cx| { - if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) { - vim.motion(Motion::RepeatFindReversed { last_find }, window, cx); - } - }); - Vim::action(editor, cx, |vim, &WindowTop, window, cx| { - vim.motion(Motion::WindowTop, window, cx) - }); - Vim::action(editor, cx, |vim, &WindowMiddle, window, cx| { - vim.motion(Motion::WindowMiddle, window, cx) - }); - Vim::action(editor, cx, |vim, &WindowBottom, window, cx| { - vim.motion(Motion::WindowBottom, window, cx) - }); - - Vim::action(editor, cx, |vim, &PreviousSectionStart, window, cx| { - vim.motion(Motion::PreviousSectionStart, window, cx) - }); - Vim::action(editor, cx, |vim, &NextSectionStart, window, cx| { - vim.motion(Motion::NextSectionStart, window, cx) - }); - Vim::action(editor, cx, |vim, &PreviousSectionEnd, window, cx| { - vim.motion(Motion::PreviousSectionEnd, window, cx) - }); - Vim::action(editor, cx, |vim, &NextSectionEnd, window, cx| { - vim.motion(Motion::NextSectionEnd, window, cx) - }); - Vim::action(editor, cx, |vim, &PreviousMethodStart, window, cx| { - vim.motion(Motion::PreviousMethodStart, window, cx) - }); - Vim::action(editor, cx, |vim, &NextMethodStart, window, cx| { - vim.motion(Motion::NextMethodStart, window, cx) - }); - Vim::action(editor, cx, |vim, &PreviousMethodEnd, window, cx| { - vim.motion(Motion::PreviousMethodEnd, window, cx) - }); - Vim::action(editor, cx, |vim, &NextMethodEnd, window, cx| { - vim.motion(Motion::NextMethodEnd, window, cx) - }); - Vim::action(editor, cx, |vim, &NextComment, window, cx| { - vim.motion(Motion::NextComment, window, cx) - }); - Vim::action(editor, cx, |vim, &PreviousComment, window, cx| { - vim.motion(Motion::PreviousComment, window, cx) - }); - Vim::action(editor, cx, |vim, &PreviousLesserIndent, window, cx| { - vim.motion(Motion::PreviousLesserIndent, window, cx) - }); - Vim::action(editor, cx, |vim, &PreviousGreaterIndent, window, cx| { - vim.motion(Motion::PreviousGreaterIndent, window, cx) - }); - Vim::action(editor, cx, |vim, &PreviousSameIndent, window, cx| { - vim.motion(Motion::PreviousSameIndent, window, cx) - }); - Vim::action(editor, cx, |vim, &NextLesserIndent, window, cx| { - vim.motion(Motion::NextLesserIndent, window, cx) - }); - Vim::action(editor, cx, |vim, &NextGreaterIndent, window, cx| { - vim.motion(Motion::NextGreaterIndent, window, cx) - }); - Vim::action(editor, cx, |vim, &NextSameIndent, window, cx| { - vim.motion(Motion::NextSameIndent, window, cx) - }); -} - -impl Vim { - pub(crate) fn search_motion(&mut self, m: Motion, window: &mut Window, cx: &mut Context) { - if let Motion::ZedSearchResult { - prior_selections, .. - } = &m - { - match self.mode { - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { - if !prior_selections.is_empty() { - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges(prior_selections.iter().cloned()) - }) - }); - } - } - Mode::Normal | Mode::Replace | Mode::Insert => { - if self.active_operator().is_none() { - return; - } - } - Mode::HelixNormal | Mode::HelixSelect => {} - } - } - - self.motion(m, window, cx) - } - - pub(crate) fn motion(&mut self, motion: Motion, window: &mut Window, cx: &mut Context) { - if let Some(Operator::FindForward { .. }) - | Some(Operator::Sneak { .. }) - | Some(Operator::SneakBackward { .. }) - | Some(Operator::FindBackward { .. }) = self.active_operator() - { - self.pop_operator(window, cx); - } - - let count = Vim::take_count(cx); - let forced_motion = Vim::take_forced_motion(cx); - let active_operator = self.active_operator(); - let mut waiting_operator: Option = None; - match self.mode { - Mode::Normal | Mode::Replace | Mode::Insert => { - if active_operator == Some(Operator::AddSurrounds { target: None }) { - waiting_operator = Some(Operator::AddSurrounds { - target: Some(SurroundsType::Motion(motion)), - }); - } else { - self.normal_motion(motion, active_operator, count, forced_motion, window, cx) - } - } - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { - self.visual_motion(motion, count, window, cx) - } - - Mode::HelixNormal => self.helix_normal_motion(motion, count, window, cx), - Mode::HelixSelect => self.helix_select_motion(motion, count, window, cx), - } - self.clear_operator(window, cx); - if let Some(operator) = waiting_operator { - self.push_operator(operator, window, cx); - Vim::globals(cx).pre_count = count - } - } -} - -// Motion handling is specified here: -// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt -impl Motion { - fn default_kind(&self) -> MotionKind { - use Motion::*; - match self { - Down { .. } - | Up { .. } - | StartOfDocument - | EndOfDocument - | CurrentLine - | NextLineStart - | PreviousLineStart - | StartOfLineDownward - | WindowTop - | WindowMiddle - | WindowBottom - | NextSectionStart - | NextSectionEnd - | PreviousSectionStart - | PreviousSectionEnd - | NextMethodStart - | NextMethodEnd - | PreviousMethodStart - | PreviousMethodEnd - | NextComment - | PreviousComment - | PreviousLesserIndent - | PreviousGreaterIndent - | PreviousSameIndent - | NextLesserIndent - | NextGreaterIndent - | NextSameIndent - | GoToPercentage - | Jump { line: true, .. } => MotionKind::Linewise, - EndOfLine { .. } - | EndOfLineDownward - | Matching - | FindForward { .. } - | NextWordEnd { .. } - | PreviousWordEnd { .. } - | NextSubwordEnd { .. } - | PreviousSubwordEnd { .. } => MotionKind::Inclusive, - Left - | WrappingLeft - | Right - | WrappingRight - | StartOfLine { .. } - | StartOfParagraph - | EndOfParagraph - | SentenceBackward - | SentenceForward - | GoToColumn - | MiddleOfLine { .. } - | UnmatchedForward { .. } - | UnmatchedBackward { .. } - | NextWordStart { .. } - | PreviousWordStart { .. } - | NextSubwordStart { .. } - | PreviousSubwordStart { .. } - | FirstNonWhitespace { .. } - | FindBackward { .. } - | Sneak { .. } - | SneakBackward { .. } - | Jump { .. } - | ZedSearchResult { .. } => MotionKind::Exclusive, - RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => { - motion.default_kind() - } - } - } - - fn skip_exclusive_special_case(&self) -> bool { - matches!(self, Motion::WrappingLeft | Motion::WrappingRight) - } - - pub(crate) fn push_to_jump_list(&self) -> bool { - use Motion::*; - match self { - CurrentLine - | Down { .. } - | EndOfLine { .. } - | EndOfLineDownward - | FindBackward { .. } - | FindForward { .. } - | FirstNonWhitespace { .. } - | GoToColumn - | Left - | MiddleOfLine { .. } - | NextLineStart - | NextSubwordEnd { .. } - | NextSubwordStart { .. } - | NextWordEnd { .. } - | NextWordStart { .. } - | PreviousLineStart - | PreviousSubwordEnd { .. } - | PreviousSubwordStart { .. } - | PreviousWordEnd { .. } - | PreviousWordStart { .. } - | RepeatFind { .. } - | RepeatFindReversed { .. } - | Right - | StartOfLine { .. } - | StartOfLineDownward - | Up { .. } - | WrappingLeft - | WrappingRight => false, - EndOfDocument - | EndOfParagraph - | GoToPercentage - | Jump { .. } - | Matching - | NextComment - | NextGreaterIndent - | NextLesserIndent - | NextMethodEnd - | NextMethodStart - | NextSameIndent - | NextSectionEnd - | NextSectionStart - | PreviousComment - | PreviousGreaterIndent - | PreviousLesserIndent - | PreviousMethodEnd - | PreviousMethodStart - | PreviousSameIndent - | PreviousSectionEnd - | PreviousSectionStart - | SentenceBackward - | SentenceForward - | Sneak { .. } - | SneakBackward { .. } - | StartOfDocument - | StartOfParagraph - | UnmatchedBackward { .. } - | UnmatchedForward { .. } - | WindowBottom - | WindowMiddle - | WindowTop - | ZedSearchResult { .. } => true, - } - } - - pub fn infallible(&self) -> bool { - use Motion::*; - match self { - StartOfDocument | EndOfDocument | CurrentLine => true, - Down { .. } - | Up { .. } - | EndOfLine { .. } - | MiddleOfLine { .. } - | Matching - | UnmatchedForward { .. } - | UnmatchedBackward { .. } - | FindForward { .. } - | RepeatFind { .. } - | Left - | WrappingLeft - | Right - | WrappingRight - | StartOfLine { .. } - | StartOfParagraph - | EndOfParagraph - | SentenceBackward - | SentenceForward - | StartOfLineDownward - | EndOfLineDownward - | GoToColumn - | GoToPercentage - | NextWordStart { .. } - | NextWordEnd { .. } - | PreviousWordStart { .. } - | PreviousWordEnd { .. } - | NextSubwordStart { .. } - | NextSubwordEnd { .. } - | PreviousSubwordStart { .. } - | PreviousSubwordEnd { .. } - | FirstNonWhitespace { .. } - | FindBackward { .. } - | Sneak { .. } - | SneakBackward { .. } - | RepeatFindReversed { .. } - | WindowTop - | WindowMiddle - | WindowBottom - | NextLineStart - | PreviousLineStart - | ZedSearchResult { .. } - | NextSectionStart - | NextSectionEnd - | PreviousSectionStart - | PreviousSectionEnd - | NextMethodStart - | NextMethodEnd - | PreviousMethodStart - | PreviousMethodEnd - | NextComment - | PreviousComment - | PreviousLesserIndent - | PreviousGreaterIndent - | PreviousSameIndent - | NextLesserIndent - | NextGreaterIndent - | NextSameIndent - | Jump { .. } => false, - } - } - - pub fn move_point( - &self, - map: &DisplaySnapshot, - point: DisplayPoint, - goal: SelectionGoal, - maybe_times: Option, - text_layout_details: &TextLayoutDetails, - ) -> Option<(DisplayPoint, SelectionGoal)> { - let times = maybe_times.unwrap_or(1); - use Motion::*; - let infallible = self.infallible(); - let (new_point, goal) = match self { - Left => (left(map, point, times), SelectionGoal::None), - WrappingLeft => (wrapping_left(map, point, times), SelectionGoal::None), - Down { - display_lines: false, - } => up_down_buffer_rows(map, point, goal, times as isize, text_layout_details), - Down { - display_lines: true, - } => down_display(map, point, goal, times, text_layout_details), - Up { - display_lines: false, - } => up_down_buffer_rows(map, point, goal, 0 - times as isize, text_layout_details), - Up { - display_lines: true, - } => up_display(map, point, goal, times, text_layout_details), - Right => (right(map, point, times), SelectionGoal::None), - WrappingRight => (wrapping_right(map, point, times), SelectionGoal::None), - NextWordStart { ignore_punctuation } => ( - next_word_start(map, point, *ignore_punctuation, times), - SelectionGoal::None, - ), - NextWordEnd { ignore_punctuation } => ( - next_word_end(map, point, *ignore_punctuation, times, true, true), - SelectionGoal::None, - ), - PreviousWordStart { ignore_punctuation } => ( - previous_word_start(map, point, *ignore_punctuation, times), - SelectionGoal::None, - ), - PreviousWordEnd { ignore_punctuation } => ( - previous_word_end(map, point, *ignore_punctuation, times), - SelectionGoal::None, - ), - NextSubwordStart { ignore_punctuation } => ( - next_subword_start(map, point, *ignore_punctuation, times), - SelectionGoal::None, - ), - NextSubwordEnd { ignore_punctuation } => ( - next_subword_end(map, point, *ignore_punctuation, times, true), - SelectionGoal::None, - ), - PreviousSubwordStart { ignore_punctuation } => ( - previous_subword_start(map, point, *ignore_punctuation, times), - SelectionGoal::None, - ), - PreviousSubwordEnd { ignore_punctuation } => ( - previous_subword_end(map, point, *ignore_punctuation, times), - SelectionGoal::None, - ), - FirstNonWhitespace { display_lines } => ( - first_non_whitespace(map, *display_lines, point), - SelectionGoal::None, - ), - StartOfLine { display_lines } => ( - start_of_line(map, *display_lines, point), - SelectionGoal::None, - ), - MiddleOfLine { display_lines } => ( - middle_of_line(map, *display_lines, point, maybe_times), - SelectionGoal::None, - ), - EndOfLine { display_lines } => ( - end_of_line(map, *display_lines, point, times), - SelectionGoal::None, - ), - SentenceBackward => (sentence_backwards(map, point, times), SelectionGoal::None), - SentenceForward => (sentence_forwards(map, point, times), SelectionGoal::None), - StartOfParagraph => ( - movement::start_of_paragraph(map, point, times), - SelectionGoal::None, - ), - EndOfParagraph => ( - map.clip_at_line_end(movement::end_of_paragraph(map, point, times)), - SelectionGoal::None, - ), - CurrentLine => (next_line_end(map, point, times), SelectionGoal::None), - StartOfDocument => ( - start_of_document(map, point, maybe_times), - SelectionGoal::None, - ), - EndOfDocument => ( - end_of_document(map, point, maybe_times), - SelectionGoal::None, - ), - Matching => (matching(map, point), SelectionGoal::None), - GoToPercentage => (go_to_percentage(map, point, times), SelectionGoal::None), - UnmatchedForward { char } => ( - unmatched_forward(map, point, *char, times), - SelectionGoal::None, - ), - UnmatchedBackward { char } => ( - unmatched_backward(map, point, *char, times), - SelectionGoal::None, - ), - // t f - FindForward { - before, - char, - mode, - smartcase, - } => { - return find_forward(map, point, *before, *char, times, *mode, *smartcase) - .map(|new_point| (new_point, SelectionGoal::None)); - } - // T F - FindBackward { - after, - char, - mode, - smartcase, - } => ( - find_backward(map, point, *after, *char, times, *mode, *smartcase), - SelectionGoal::None, - ), - Sneak { - first_char, - second_char, - smartcase, - } => { - return sneak(map, point, *first_char, *second_char, times, *smartcase) - .map(|new_point| (new_point, SelectionGoal::None)); - } - SneakBackward { - first_char, - second_char, - smartcase, - } => { - return sneak_backward(map, point, *first_char, *second_char, times, *smartcase) - .map(|new_point| (new_point, SelectionGoal::None)); - } - // ; -- repeat the last find done with t, f, T, F - RepeatFind { last_find } => match **last_find { - Motion::FindForward { - before, - char, - mode, - smartcase, - } => { - let mut new_point = - find_forward(map, point, before, char, times, mode, smartcase); - if new_point == Some(point) { - new_point = - find_forward(map, point, before, char, times + 1, mode, smartcase); - } - - return new_point.map(|new_point| (new_point, SelectionGoal::None)); - } - - Motion::FindBackward { - after, - char, - mode, - smartcase, - } => { - let mut new_point = - find_backward(map, point, after, char, times, mode, smartcase); - if new_point == point { - new_point = - find_backward(map, point, after, char, times + 1, mode, smartcase); - } - - (new_point, SelectionGoal::None) - } - Motion::Sneak { - first_char, - second_char, - smartcase, - } => { - let mut new_point = - sneak(map, point, first_char, second_char, times, smartcase); - if new_point == Some(point) { - new_point = - sneak(map, point, first_char, second_char, times + 1, smartcase); - } - - return new_point.map(|new_point| (new_point, SelectionGoal::None)); - } - - Motion::SneakBackward { - first_char, - second_char, - smartcase, - } => { - let mut new_point = - sneak_backward(map, point, first_char, second_char, times, smartcase); - if new_point == Some(point) { - new_point = sneak_backward( - map, - point, - first_char, - second_char, - times + 1, - smartcase, - ); - } - - return new_point.map(|new_point| (new_point, SelectionGoal::None)); - } - _ => return None, - }, - // , -- repeat the last find done with t, f, T, F, s, S, in opposite direction - RepeatFindReversed { last_find } => match **last_find { - Motion::FindForward { - before, - char, - mode, - smartcase, - } => { - let mut new_point = - find_backward(map, point, before, char, times, mode, smartcase); - if new_point == point { - new_point = - find_backward(map, point, before, char, times + 1, mode, smartcase); - } - - (new_point, SelectionGoal::None) - } - - Motion::FindBackward { - after, - char, - mode, - smartcase, - } => { - let mut new_point = - find_forward(map, point, after, char, times, mode, smartcase); - if new_point == Some(point) { - new_point = - find_forward(map, point, after, char, times + 1, mode, smartcase); - } - - return new_point.map(|new_point| (new_point, SelectionGoal::None)); - } - - Motion::Sneak { - first_char, - second_char, - smartcase, - } => { - let mut new_point = - sneak_backward(map, point, first_char, second_char, times, smartcase); - if new_point == Some(point) { - new_point = sneak_backward( - map, - point, - first_char, - second_char, - times + 1, - smartcase, - ); - } - - return new_point.map(|new_point| (new_point, SelectionGoal::None)); - } - - Motion::SneakBackward { - first_char, - second_char, - smartcase, - } => { - let mut new_point = - sneak(map, point, first_char, second_char, times, smartcase); - if new_point == Some(point) { - new_point = - sneak(map, point, first_char, second_char, times + 1, smartcase); - } - - return new_point.map(|new_point| (new_point, SelectionGoal::None)); - } - _ => return None, - }, - NextLineStart => (next_line_start(map, point, times), SelectionGoal::None), - PreviousLineStart => (previous_line_start(map, point, times), SelectionGoal::None), - StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None), - EndOfLineDownward => (last_non_whitespace(map, point, times), SelectionGoal::None), - GoToColumn => (go_to_column(map, point, times), SelectionGoal::None), - WindowTop => window_top(map, point, text_layout_details, times - 1), - WindowMiddle => window_middle(map, point, text_layout_details), - WindowBottom => window_bottom(map, point, text_layout_details, times - 1), - Jump { line, anchor } => mark::jump_motion(map, *anchor, *line), - ZedSearchResult { new_selections, .. } => { - // There will be only one selection, as - // Search::SelectNextMatch selects a single match. - if let Some(new_selection) = new_selections.first() { - ( - new_selection.start.to_display_point(map), - SelectionGoal::None, - ) - } else { - return None; - } - } - NextSectionStart => ( - section_motion(map, point, times, Direction::Next, true), - SelectionGoal::None, - ), - NextSectionEnd => ( - section_motion(map, point, times, Direction::Next, false), - SelectionGoal::None, - ), - PreviousSectionStart => ( - section_motion(map, point, times, Direction::Prev, true), - SelectionGoal::None, - ), - PreviousSectionEnd => ( - section_motion(map, point, times, Direction::Prev, false), - SelectionGoal::None, - ), - - NextMethodStart => ( - method_motion(map, point, times, Direction::Next, true), - SelectionGoal::None, - ), - NextMethodEnd => ( - method_motion(map, point, times, Direction::Next, false), - SelectionGoal::None, - ), - PreviousMethodStart => ( - method_motion(map, point, times, Direction::Prev, true), - SelectionGoal::None, - ), - PreviousMethodEnd => ( - method_motion(map, point, times, Direction::Prev, false), - SelectionGoal::None, - ), - NextComment => ( - comment_motion(map, point, times, Direction::Next), - SelectionGoal::None, - ), - PreviousComment => ( - comment_motion(map, point, times, Direction::Prev), - SelectionGoal::None, - ), - PreviousLesserIndent => ( - indent_motion(map, point, times, Direction::Prev, IndentType::Lesser), - SelectionGoal::None, - ), - PreviousGreaterIndent => ( - indent_motion(map, point, times, Direction::Prev, IndentType::Greater), - SelectionGoal::None, - ), - PreviousSameIndent => ( - indent_motion(map, point, times, Direction::Prev, IndentType::Same), - SelectionGoal::None, - ), - NextLesserIndent => ( - indent_motion(map, point, times, Direction::Next, IndentType::Lesser), - SelectionGoal::None, - ), - NextGreaterIndent => ( - indent_motion(map, point, times, Direction::Next, IndentType::Greater), - SelectionGoal::None, - ), - NextSameIndent => ( - indent_motion(map, point, times, Direction::Next, IndentType::Same), - SelectionGoal::None, - ), - }; - (new_point != point || infallible).then_some((new_point, goal)) - } - - // Get the range value after self is applied to the specified selection. - pub fn range( - &self, - map: &DisplaySnapshot, - mut selection: Selection, - times: Option, - text_layout_details: &TextLayoutDetails, - forced_motion: bool, - ) -> Option<(Range, MotionKind)> { - if let Motion::ZedSearchResult { - prior_selections, - new_selections, - } = self - { - if let Some((prior_selection, new_selection)) = - prior_selections.first().zip(new_selections.first()) - { - let start = prior_selection - .start - .to_display_point(map) - .min(new_selection.start.to_display_point(map)); - let end = new_selection - .end - .to_display_point(map) - .max(prior_selection.end.to_display_point(map)); - - if start < end { - return Some((start..end, MotionKind::Exclusive)); - } else { - return Some((end..start, MotionKind::Exclusive)); - } - } else { - return None; - } - } - let maybe_new_point = self.move_point( - map, - selection.head(), - selection.goal, - times, - text_layout_details, - ); - - let (new_head, goal) = match (maybe_new_point, forced_motion) { - (Some((p, g)), _) => Some((p, g)), - (None, false) => None, - (None, true) => Some((selection.head(), selection.goal)), - }?; - - selection.set_head(new_head, goal); - - let mut kind = match (self.default_kind(), forced_motion) { - (MotionKind::Linewise, true) => MotionKind::Exclusive, - (MotionKind::Exclusive, true) => MotionKind::Inclusive, - (MotionKind::Inclusive, true) => MotionKind::Exclusive, - (kind, false) => kind, - }; - - if let Motion::NextWordStart { - ignore_punctuation: _, - } = self - { - // Another special case: When using the "w" motion in combination with an - // operator and the last word moved over is at the end of a line, the end of - // that word becomes the end of the operated text, not the first word in the - // next line. - let start = selection.start.to_point(map); - let end = selection.end.to_point(map); - let start_row = MultiBufferRow(selection.start.to_point(map).row); - if end.row > start.row { - selection.end = Point::new(start_row.0, map.buffer_snapshot().line_len(start_row)) - .to_display_point(map); - - // a bit of a hack, we need `cw` on a blank line to not delete the newline, - // but dw on a blank line should. The `Linewise` returned from this method - // causes the `d` operator to include the trailing newline. - if selection.start == selection.end { - return Some((selection.start..selection.end, MotionKind::Linewise)); - } - } - } else if kind == MotionKind::Exclusive && !self.skip_exclusive_special_case() { - let start_point = selection.start.to_point(map); - let mut end_point = selection.end.to_point(map); - let mut next_point = selection.end; - *next_point.column_mut() += 1; - next_point = map.clip_point(next_point, Bias::Right); - if next_point.to_point(map) == end_point && forced_motion { - selection.end = movement::saturating_left(map, selection.end); - } - - if end_point.row > start_point.row { - let first_non_blank_of_start_row = map - .line_indent_for_buffer_row(MultiBufferRow(start_point.row)) - .raw_len(); - // https://github.com/neovim/neovim/blob/ee143aaf65a0e662c42c636aa4a959682858b3e7/src/nvim/ops.c#L6178-L6203 - if end_point.column == 0 { - // If the motion is exclusive and the end of the motion is in column 1, the - // end of the motion is moved to the end of the previous line and the motion - // becomes inclusive. Example: "}" moves to the first line after a paragraph, - // but "d}" will not include that line. - // - // If the motion is exclusive, the end of the motion is in column 1 and the - // start of the motion was at or before the first non-blank in the line, the - // motion becomes linewise. Example: If a paragraph begins with some blanks - // and you do "d}" while standing on the first non-blank, all the lines of - // the paragraph are deleted, including the blanks. - if start_point.column <= first_non_blank_of_start_row { - kind = MotionKind::Linewise; - } else { - kind = MotionKind::Inclusive; - } - end_point.row -= 1; - end_point.column = 0; - selection.end = map.clip_point(map.next_line_boundary(end_point).1, Bias::Left); - } else if let Motion::EndOfParagraph = self { - // Special case: When using the "}" motion, it's possible - // that there's no blank lines after the paragraph the - // cursor is currently on. - // In this situation the `end_point.column` value will be - // greater than 0, so the selection doesn't actually end on - // the first character of a blank line. In that case, we'll - // want to move one column to the right, to actually include - // all characters of the last non-blank line. - selection.end = movement::saturating_right(map, selection.end) - } - } - } else if kind == MotionKind::Inclusive { - selection.end = movement::saturating_right(map, selection.end) - } - - if kind == MotionKind::Linewise { - selection.start = map.prev_line_boundary(selection.start.to_point(map)).1; - selection.end = map.next_line_boundary(selection.end.to_point(map)).1; - } - Some((selection.start..selection.end, kind)) - } - - // Expands a selection using self for an operator - pub fn expand_selection( - &self, - map: &DisplaySnapshot, - selection: &mut Selection, - times: Option, - text_layout_details: &TextLayoutDetails, - forced_motion: bool, - ) -> Option { - let (range, kind) = self.range( - map, - selection.clone(), - times, - text_layout_details, - forced_motion, - )?; - selection.start = range.start; - selection.end = range.end; - Some(kind) - } -} - -fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint { - for _ in 0..times { - point = movement::saturating_left(map, point); - if point.column() == 0 { - break; - } - } - point -} - -pub(crate) fn wrapping_left( - map: &DisplaySnapshot, - mut point: DisplayPoint, - times: usize, -) -> DisplayPoint { - for _ in 0..times { - point = movement::left(map, point); - if point.is_zero() { - break; - } - } - point -} - -fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint { - for _ in 0..times { - point = wrapping_right_single(map, point); - if point == map.max_point() { - break; - } - } - point -} - -fn wrapping_right_single(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { - let mut next_point = point; - *next_point.column_mut() += 1; - next_point = map.clip_point(next_point, Bias::Right); - if next_point == point { - if next_point.row() == map.max_point().row() { - next_point - } else { - DisplayPoint::new(next_point.row().next_row(), 0) - } - } else { - next_point - } -} - -fn up_down_buffer_rows( - map: &DisplaySnapshot, - mut point: DisplayPoint, - mut goal: SelectionGoal, - mut times: isize, - text_layout_details: &TextLayoutDetails, -) -> (DisplayPoint, SelectionGoal) { - let bias = if times < 0 { Bias::Left } else { Bias::Right }; - - while map.is_folded_buffer_header(point.row()) { - if times < 0 { - (point, _) = movement::up(map, point, goal, true, text_layout_details); - times += 1; - } else if times > 0 { - (point, _) = movement::down(map, point, goal, true, text_layout_details); - times -= 1; - } else { - break; - } - } - - let start = map.display_point_to_fold_point(point, Bias::Left); - let begin_folded_line = map.fold_point_to_display_point( - map.fold_snapshot() - .clip_point(FoldPoint::new(start.row(), 0), Bias::Left), - ); - let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0; - - let (goal_wrap, goal_x) = match goal { - SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x), - SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end as f32), - SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x as f32), - _ => { - let x = map.x_for_display_point(point, text_layout_details); - goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.into())); - (select_nth_wrapped_row, x.into()) - } - }; - - let target = start.row() as isize + times; - let new_row = (target.max(0) as u32).min(map.fold_snapshot().max_point().row()); - - let mut begin_folded_line = map.fold_point_to_display_point( - map.fold_snapshot() - .clip_point(FoldPoint::new(new_row, 0), bias), - ); - - let mut i = 0; - while i < goal_wrap && begin_folded_line.row() < map.max_point().row() { - let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0); - if map - .display_point_to_fold_point(next_folded_line, bias) - .row() - == new_row - { - i += 1; - begin_folded_line = next_folded_line; - } else { - break; - } - } - - let new_col = if i == goal_wrap { - map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details) - } else { - map.line_len(begin_folded_line.row()) - }; - - let point = DisplayPoint::new(begin_folded_line.row(), new_col); - let mut clipped_point = map.clip_point(point, bias); - - // When navigating vertically in vim mode with inlay hints present, - // we need to handle the case where clipping moves us to a different row. - // This can happen when moving down (Bias::Right) and hitting an inlay hint. - // Re-clip with opposite bias to stay on the intended line. - // - // See: https://github.com/zed-industries/zed/issues/29134 - if clipped_point.row() > point.row() { - clipped_point = map.clip_point(point, Bias::Left); - } - - (clipped_point, goal) -} - -fn down_display( - map: &DisplaySnapshot, - mut point: DisplayPoint, - mut goal: SelectionGoal, - times: usize, - text_layout_details: &TextLayoutDetails, -) -> (DisplayPoint, SelectionGoal) { - for _ in 0..times { - (point, goal) = movement::down(map, point, goal, true, text_layout_details); - } - - (point, goal) -} - -fn up_display( - map: &DisplaySnapshot, - mut point: DisplayPoint, - mut goal: SelectionGoal, - times: usize, - text_layout_details: &TextLayoutDetails, -) -> (DisplayPoint, SelectionGoal) { - for _ in 0..times { - (point, goal) = movement::up(map, point, goal, true, text_layout_details); - } - - (point, goal) -} - -pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint { - for _ in 0..times { - let new_point = movement::saturating_right(map, point); - if point == new_point { - break; - } - point = new_point; - } - point -} - -pub(crate) fn next_char( - map: &DisplaySnapshot, - point: DisplayPoint, - allow_cross_newline: bool, -) -> DisplayPoint { - let mut new_point = point; - let mut max_column = map.line_len(new_point.row()); - if !allow_cross_newline { - max_column -= 1; - } - if new_point.column() < max_column { - *new_point.column_mut() += 1; - } else if new_point < map.max_point() && allow_cross_newline { - *new_point.row_mut() += 1; - *new_point.column_mut() = 0; - } - map.clip_ignoring_line_ends(new_point, Bias::Right) -} - -pub(crate) fn next_word_start( - map: &DisplaySnapshot, - mut point: DisplayPoint, - ignore_punctuation: bool, - times: usize, -) -> DisplayPoint { - let classifier = map - .buffer_snapshot() - .char_classifier_at(point.to_point(map)) - .ignore_punctuation(ignore_punctuation); - for _ in 0..times { - let mut crossed_newline = false; - let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| { - let left_kind = classifier.kind(left); - let right_kind = classifier.kind(right); - let at_newline = right == '\n'; - - let found = (left_kind != right_kind && right_kind != CharKind::Whitespace) - || at_newline && crossed_newline - || at_newline && left == '\n'; // Prevents skipping repeated empty lines - - crossed_newline |= at_newline; - found - }); - if point == new_point { - break; - } - point = new_point; - } - point -} - -pub(crate) fn next_word_end( - map: &DisplaySnapshot, - mut point: DisplayPoint, - ignore_punctuation: bool, - times: usize, - allow_cross_newline: bool, - always_advance: bool, -) -> DisplayPoint { - let classifier = map - .buffer_snapshot() - .char_classifier_at(point.to_point(map)) - .ignore_punctuation(ignore_punctuation); - for _ in 0..times { - let mut need_next_char = false; - let new_point = if always_advance { - next_char(map, point, allow_cross_newline) - } else { - point - }; - let new_point = movement::find_boundary_exclusive( - map, - new_point, - FindRange::MultiLine, - |left, right| { - let left_kind = classifier.kind(left); - let right_kind = classifier.kind(right); - let at_newline = right == '\n'; - - if !allow_cross_newline && at_newline { - need_next_char = true; - return true; - } - - left_kind != right_kind && left_kind != CharKind::Whitespace - }, - ); - let new_point = if need_next_char { - next_char(map, new_point, true) - } else { - new_point - }; - let new_point = map.clip_point(new_point, Bias::Left); - if point == new_point { - break; - } - point = new_point; - } - point -} - -fn previous_word_start( - map: &DisplaySnapshot, - mut point: DisplayPoint, - ignore_punctuation: bool, - times: usize, -) -> DisplayPoint { - let classifier = map - .buffer_snapshot() - .char_classifier_at(point.to_point(map)) - .ignore_punctuation(ignore_punctuation); - for _ in 0..times { - // This works even though find_preceding_boundary is called for every character in the line containing - // cursor because the newline is checked only once. - let new_point = movement::find_preceding_boundary_display_point( - map, - point, - FindRange::MultiLine, - |left, right| { - let left_kind = classifier.kind(left); - let right_kind = classifier.kind(right); - - (left_kind != right_kind && !right.is_whitespace()) || left == '\n' - }, - ); - if point == new_point { - break; - } - point = new_point; - } - point -} - -fn previous_word_end( - map: &DisplaySnapshot, - point: DisplayPoint, - ignore_punctuation: bool, - times: usize, -) -> DisplayPoint { - let classifier = map - .buffer_snapshot() - .char_classifier_at(point.to_point(map)) - .ignore_punctuation(ignore_punctuation); - let mut point = point.to_point(map); - - if point.column < map.buffer_snapshot().line_len(MultiBufferRow(point.row)) - && let Some(ch) = map.buffer_snapshot().chars_at(point).next() - { - point.column += ch.len_utf8() as u32; - } - for _ in 0..times { - let new_point = movement::find_preceding_boundary_point( - &map.buffer_snapshot(), - point, - FindRange::MultiLine, - |left, right| { - let left_kind = classifier.kind(left); - let right_kind = classifier.kind(right); - match (left_kind, right_kind) { - (CharKind::Punctuation, CharKind::Whitespace) - | (CharKind::Punctuation, CharKind::Word) - | (CharKind::Word, CharKind::Whitespace) - | (CharKind::Word, CharKind::Punctuation) => true, - (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n', - _ => false, - } - }, - ); - if new_point == point { - break; - } - point = new_point; - } - movement::saturating_left(map, point.to_display_point(map)) -} - -fn next_subword_start( - map: &DisplaySnapshot, - mut point: DisplayPoint, - ignore_punctuation: bool, - times: usize, -) -> DisplayPoint { - let classifier = map - .buffer_snapshot() - .char_classifier_at(point.to_point(map)) - .ignore_punctuation(ignore_punctuation); - for _ in 0..times { - let mut crossed_newline = false; - let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| { - let left_kind = classifier.kind(left); - let right_kind = classifier.kind(right); - let at_newline = right == '\n'; - - let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric(); - let is_subword_start = - left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase(); - - let found = (!right.is_whitespace() && (is_word_start || is_subword_start)) - || at_newline && crossed_newline - || at_newline && left == '\n'; // Prevents skipping repeated empty lines - - crossed_newline |= at_newline; - found - }); - if point == new_point { - break; - } - point = new_point; - } - point -} - -pub(crate) fn next_subword_end( - map: &DisplaySnapshot, - mut point: DisplayPoint, - ignore_punctuation: bool, - times: usize, - allow_cross_newline: bool, -) -> DisplayPoint { - let classifier = map - .buffer_snapshot() - .char_classifier_at(point.to_point(map)) - .ignore_punctuation(ignore_punctuation); - for _ in 0..times { - let new_point = next_char(map, point, allow_cross_newline); - - let mut crossed_newline = false; - let mut need_backtrack = false; - let new_point = - movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| { - let left_kind = classifier.kind(left); - let right_kind = classifier.kind(right); - let at_newline = right == '\n'; - - if !allow_cross_newline && at_newline { - return true; - } - - let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric(); - let is_subword_end = - left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase(); - - let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end); - - if found && (is_word_end || is_subword_end) { - need_backtrack = true; - } - - crossed_newline |= at_newline; - found - }); - let mut new_point = map.clip_point(new_point, Bias::Left); - if need_backtrack { - *new_point.column_mut() -= 1; - } - let new_point = map.clip_point(new_point, Bias::Left); - if point == new_point { - break; - } - point = new_point; - } - point -} - -fn previous_subword_start( - map: &DisplaySnapshot, - mut point: DisplayPoint, - ignore_punctuation: bool, - times: usize, -) -> DisplayPoint { - let classifier = map - .buffer_snapshot() - .char_classifier_at(point.to_point(map)) - .ignore_punctuation(ignore_punctuation); - for _ in 0..times { - let mut crossed_newline = false; - // This works even though find_preceding_boundary is called for every character in the line containing - // cursor because the newline is checked only once. - let new_point = movement::find_preceding_boundary_display_point( - map, - point, - FindRange::MultiLine, - |left, right| { - let left_kind = classifier.kind(left); - let right_kind = classifier.kind(right); - let at_newline = right == '\n'; - - let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric(); - let is_subword_start = - left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase(); - - let found = (!right.is_whitespace() && (is_word_start || is_subword_start)) - || at_newline && crossed_newline - || at_newline && left == '\n'; // Prevents skipping repeated empty lines - - crossed_newline |= at_newline; - - found - }, - ); - if point == new_point { - break; - } - point = new_point; - } - point -} - -fn previous_subword_end( - map: &DisplaySnapshot, - point: DisplayPoint, - ignore_punctuation: bool, - times: usize, -) -> DisplayPoint { - let classifier = map - .buffer_snapshot() - .char_classifier_at(point.to_point(map)) - .ignore_punctuation(ignore_punctuation); - let mut point = point.to_point(map); - - if point.column < map.buffer_snapshot().line_len(MultiBufferRow(point.row)) - && let Some(ch) = map.buffer_snapshot().chars_at(point).next() - { - point.column += ch.len_utf8() as u32; - } - for _ in 0..times { - let new_point = movement::find_preceding_boundary_point( - &map.buffer_snapshot(), - point, - FindRange::MultiLine, - |left, right| { - let left_kind = classifier.kind(left); - let right_kind = classifier.kind(right); - - let is_subword_end = - left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase(); - - if is_subword_end { - return true; - } - - match (left_kind, right_kind) { - (CharKind::Word, CharKind::Whitespace) - | (CharKind::Word, CharKind::Punctuation) => true, - (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n', - _ => false, - } - }, - ); - if new_point == point { - break; - } - point = new_point; - } - movement::saturating_left(map, point.to_display_point(map)) -} - -pub(crate) fn first_non_whitespace( - map: &DisplaySnapshot, - display_lines: bool, - from: DisplayPoint, -) -> DisplayPoint { - let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left); - let classifier = map.buffer_snapshot().char_classifier_at(from.to_point(map)); - for (ch, offset) in map.buffer_chars_at(start_offset) { - if ch == '\n' { - return from; - } - - start_offset = offset; - - if classifier.kind(ch) != CharKind::Whitespace { - break; - } - } - - start_offset.to_display_point(map) -} - -pub(crate) fn last_non_whitespace( - map: &DisplaySnapshot, - from: DisplayPoint, - count: usize, -) -> DisplayPoint { - let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left); - let classifier = map.buffer_snapshot().char_classifier_at(from.to_point(map)); - - // NOTE: depending on clip_at_line_end we may already be one char back from the end. - if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() - && classifier.kind(ch) != CharKind::Whitespace - { - return end_of_line.to_display_point(map); - } - - for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) { - if ch == '\n' { - break; - } - end_of_line = offset; - if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' { - break; - } - } - - end_of_line.to_display_point(map) -} - -pub(crate) fn start_of_line( - map: &DisplaySnapshot, - display_lines: bool, - point: DisplayPoint, -) -> DisplayPoint { - if display_lines { - map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right) - } else { - map.prev_line_boundary(point.to_point(map)).1 - } -} - -pub(crate) fn middle_of_line( - map: &DisplaySnapshot, - display_lines: bool, - point: DisplayPoint, - times: Option, -) -> DisplayPoint { - let percent = if let Some(times) = times.filter(|&t| t <= 100) { - times as f64 / 100. - } else { - 0.5 - }; - if display_lines { - map.clip_point( - DisplayPoint::new( - point.row(), - (map.line_len(point.row()) as f64 * percent) as u32, - ), - Bias::Left, - ) - } else { - let mut buffer_point = point.to_point(map); - buffer_point.column = (map - .buffer_snapshot() - .line_len(MultiBufferRow(buffer_point.row)) as f64 - * percent) as u32; - - map.clip_point(buffer_point.to_display_point(map), Bias::Left) - } -} - -pub(crate) fn end_of_line( - map: &DisplaySnapshot, - display_lines: bool, - mut point: DisplayPoint, - times: usize, -) -> DisplayPoint { - if times > 1 { - point = map.start_of_relative_buffer_row(point, times as isize - 1); - } - if display_lines { - map.clip_point( - DisplayPoint::new(point.row(), map.line_len(point.row())), - Bias::Left, - ) - } else { - map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left) - } -} - -pub(crate) fn sentence_backwards( - map: &DisplaySnapshot, - point: DisplayPoint, - mut times: usize, -) -> DisplayPoint { - let mut start = point.to_point(map).to_offset(&map.buffer_snapshot()); - let mut chars = map.reverse_buffer_chars_at(start).peekable(); - - let mut was_newline = map - .buffer_chars_at(start) - .next() - .is_some_and(|(c, _)| c == '\n'); - - while let Some((ch, offset)) = chars.next() { - let start_of_next_sentence = if was_newline && ch == '\n' { - Some(offset + ch.len_utf8()) - } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') { - Some(next_non_blank(map, offset + ch.len_utf8())) - } else if ch == '.' || ch == '?' || ch == '!' { - start_of_next_sentence(map, offset + ch.len_utf8()) - } else { - None - }; - - if let Some(start_of_next_sentence) = start_of_next_sentence { - if start_of_next_sentence < start { - times = times.saturating_sub(1); - } - if times == 0 || offset.0 == 0 { - return map.clip_point( - start_of_next_sentence - .to_offset(&map.buffer_snapshot()) - .to_display_point(map), - Bias::Left, - ); - } - } - if was_newline { - start = offset; - } - was_newline = ch == '\n'; - } - - DisplayPoint::zero() -} - -pub(crate) fn sentence_forwards( - map: &DisplaySnapshot, - point: DisplayPoint, - mut times: usize, -) -> DisplayPoint { - let start = point.to_point(map).to_offset(&map.buffer_snapshot()); - let mut chars = map.buffer_chars_at(start).peekable(); - - let mut was_newline = map - .reverse_buffer_chars_at(start) - .next() - .is_some_and(|(c, _)| c == '\n') - && chars.peek().is_some_and(|(c, _)| *c == '\n'); - - while let Some((ch, offset)) = chars.next() { - if was_newline && ch == '\n' { - continue; - } - let start_of_next_sentence = if was_newline { - Some(next_non_blank(map, offset)) - } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') { - Some(next_non_blank(map, offset + ch.len_utf8())) - } else if ch == '.' || ch == '?' || ch == '!' { - start_of_next_sentence(map, offset + ch.len_utf8()) - } else { - None - }; - - if let Some(start_of_next_sentence) = start_of_next_sentence { - times = times.saturating_sub(1); - if times == 0 { - return map.clip_point( - start_of_next_sentence - .to_offset(&map.buffer_snapshot()) - .to_display_point(map), - Bias::Right, - ); - } - } - - was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n'); - } - - map.max_point() -} - -fn next_non_blank(map: &DisplaySnapshot, start: MultiBufferOffset) -> MultiBufferOffset { - for (c, o) in map.buffer_chars_at(start) { - if c == '\n' || !c.is_whitespace() { - return o; - } - } - - map.buffer_snapshot().len() -} - -// given the offset after a ., !, or ? find the start of the next sentence. -// if this is not a sentence boundary, returns None. -fn start_of_next_sentence( - map: &DisplaySnapshot, - end_of_sentence: MultiBufferOffset, -) -> Option { - let chars = map.buffer_chars_at(end_of_sentence); - let mut seen_space = false; - - for (char, offset) in chars { - if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') { - continue; - } - - if char == '\n' && seen_space { - return Some(offset); - } else if char.is_whitespace() { - seen_space = true; - } else if seen_space { - return Some(offset); - } else { - return None; - } - } - - Some(map.buffer_snapshot().len()) -} - -fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint { - let point = map.display_point_to_point(display_point, Bias::Left); - let Some(mut excerpt) = map.buffer_snapshot().excerpt_containing(point..point) else { - return display_point; - }; - let offset = excerpt.buffer().point_to_offset( - excerpt - .buffer() - .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left), - ); - let buffer_range = excerpt.buffer_range(); - if offset >= buffer_range.start.0 && offset <= buffer_range.end.0 { - let point = map - .buffer_snapshot() - .offset_to_point(excerpt.map_offset_from_buffer(BufferOffset(offset))); - return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left); - } - let mut last_position = None; - for (excerpt, buffer, range) in map.buffer_snapshot().excerpts() { - let excerpt_range = language::ToOffset::to_offset(&range.context.start, buffer) - ..language::ToOffset::to_offset(&range.context.end, buffer); - if offset >= excerpt_range.start && offset <= excerpt_range.end { - let text_anchor = buffer.anchor_after(offset); - let anchor = Anchor::in_buffer(excerpt, text_anchor); - return anchor.to_display_point(map); - } else if offset <= excerpt_range.start { - let anchor = Anchor::in_buffer(excerpt, range.context.start); - return anchor.to_display_point(map); - } else { - last_position = Some(Anchor::in_buffer(excerpt, range.context.end)); - } - } - - let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot()); - last_point.column = point.column; - - map.clip_point( - map.point_to_display_point( - map.buffer_snapshot().clip_point(point, Bias::Left), - Bias::Left, - ), - Bias::Left, - ) -} - -fn start_of_document( - map: &DisplaySnapshot, - display_point: DisplayPoint, - maybe_times: Option, -) -> DisplayPoint { - if let Some(times) = maybe_times { - return go_to_line(map, display_point, times); - } - - let point = map.display_point_to_point(display_point, Bias::Left); - let mut first_point = Point::zero(); - first_point.column = point.column; - - map.clip_point( - map.point_to_display_point( - map.buffer_snapshot().clip_point(first_point, Bias::Left), - Bias::Left, - ), - Bias::Left, - ) -} - -fn end_of_document( - map: &DisplaySnapshot, - display_point: DisplayPoint, - maybe_times: Option, -) -> DisplayPoint { - if let Some(times) = maybe_times { - return go_to_line(map, display_point, times); - }; - let point = map.display_point_to_point(display_point, Bias::Left); - let mut last_point = map.buffer_snapshot().max_point(); - last_point.column = point.column; - - map.clip_point( - map.point_to_display_point( - map.buffer_snapshot().clip_point(last_point, Bias::Left), - Bias::Left, - ), - Bias::Left, - ) -} - -fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option { - let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?; - let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?; - - if head > outer.start && head < inner.start { - let mut offset = inner.end.to_offset(map, Bias::Left); - for c in map.buffer_snapshot().chars_at(offset) { - if c == '/' || c == '\n' || c == '>' { - return Some(offset.to_display_point(map)); - } - offset += c.len_utf8(); - } - } else { - let mut offset = outer.start.to_offset(map, Bias::Left); - for c in map.buffer_snapshot().chars_at(offset) { - offset += c.len_utf8(); - if c == '<' || c == '\n' { - return Some(offset.to_display_point(map)); - } - } - } - - None -} - -fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint { - if !map.is_singleton() { - return display_point; - } - // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200 - let display_point = map.clip_at_line_end(display_point); - let point = display_point.to_point(map); - let offset = point.to_offset(&map.buffer_snapshot()); - let snapshot = map.buffer_snapshot(); - - // Ensure the range is contained by the current line. - let mut line_end = map.next_line_boundary(point).0; - if line_end == point { - line_end = map.max_point().to_point(map); - } - - // Attempt to find the smallest enclosing bracket range that also contains - // the offset, which only happens if the cursor is currently in a bracket. - let range_filter = |_buffer: &language::BufferSnapshot, - opening_range: Range, - closing_range: Range| { - opening_range.contains(&BufferOffset(offset.0)) - || closing_range.contains(&BufferOffset(offset.0)) - }; - - let bracket_ranges = snapshot - .innermost_enclosing_bracket_ranges(offset..offset, Some(&range_filter)) - .or_else(|| snapshot.innermost_enclosing_bracket_ranges(offset..offset, None)); - - if let Some((opening_range, closing_range)) = bracket_ranges { - let mut chars = map.buffer_snapshot().chars_at(offset); - match chars.next() { - Some('/') => {} - _ => { - if opening_range.contains(&offset) { - return closing_range.start.to_display_point(map); - } else if closing_range.contains(&offset) { - return opening_range.start.to_display_point(map); - } - } - } - } - - let line_range = map.prev_line_boundary(point).0..line_end; - let visible_line_range = - line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1)); - let ranges = map.buffer_snapshot().bracket_ranges(visible_line_range); - if let Some(ranges) = ranges { - let line_range = line_range.start.to_offset(&map.buffer_snapshot()) - ..line_range.end.to_offset(&map.buffer_snapshot()); - let mut closest_pair_destination = None; - let mut closest_distance = usize::MAX; - - for (open_range, close_range) in ranges { - if map.buffer_snapshot().chars_at(open_range.start).next() == Some('<') { - if offset > open_range.start && offset < close_range.start { - let mut chars = map.buffer_snapshot().chars_at(close_range.start); - if (Some('/'), Some('>')) == (chars.next(), chars.next()) { - return display_point; - } - if let Some(tag) = matching_tag(map, display_point) { - return tag; - } - } else if close_range.contains(&offset) { - return open_range.start.to_display_point(map); - } else if open_range.contains(&offset) { - return (close_range.end - 1).to_display_point(map); - } - } - - if (open_range.contains(&offset) || open_range.start >= offset) - && line_range.contains(&open_range.start) - { - let distance = open_range.start.saturating_sub(offset); - if distance < closest_distance { - closest_pair_destination = Some(close_range.start); - closest_distance = distance; - } - } - - if (close_range.contains(&offset) || close_range.start >= offset) - && line_range.contains(&close_range.start) - { - let distance = close_range.start.saturating_sub(offset); - if distance < closest_distance { - closest_pair_destination = Some(open_range.start); - closest_distance = distance; - } - } - - continue; - } - - closest_pair_destination - .map(|destination| destination.to_display_point(map)) - .unwrap_or(display_point) - } else { - display_point - } -} - -// Go to {count} percentage in the file, on the first -// non-blank in the line linewise. To compute the new -// line number this formula is used: -// ({count} * number-of-lines + 99) / 100 -// -// https://neovim.io/doc/user/motion.html#N%25 -fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint { - let total_lines = map.buffer_snapshot().max_point().row + 1; - let target_line = (count * total_lines as usize).div_ceil(100); - let target_point = DisplayPoint::new( - DisplayRow(target_line.saturating_sub(1) as u32), - point.column(), - ); - map.clip_point(target_point, Bias::Left) -} - -fn unmatched_forward( - map: &DisplaySnapshot, - mut display_point: DisplayPoint, - char: char, - times: usize, -) -> DisplayPoint { - for _ in 0..times { - // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245 - let point = display_point.to_point(map); - let offset = point.to_offset(&map.buffer_snapshot()); - - let ranges = map.buffer_snapshot().enclosing_bracket_ranges(point..point); - let Some(ranges) = ranges else { break }; - let mut closest_closing_destination = None; - let mut closest_distance = usize::MAX; - - for (_, close_range) in ranges { - if close_range.start > offset { - let mut chars = map.buffer_snapshot().chars_at(close_range.start); - if Some(char) == chars.next() { - let distance = close_range.start - offset; - if distance < closest_distance { - closest_closing_destination = Some(close_range.start); - closest_distance = distance; - continue; - } - } - } - } - - let new_point = closest_closing_destination - .map(|destination| destination.to_display_point(map)) - .unwrap_or(display_point); - if new_point == display_point { - break; - } - display_point = new_point; - } - display_point -} - -fn unmatched_backward( - map: &DisplaySnapshot, - mut display_point: DisplayPoint, - char: char, - times: usize, -) -> DisplayPoint { - for _ in 0..times { - // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239 - let point = display_point.to_point(map); - let offset = point.to_offset(&map.buffer_snapshot()); - - let ranges = map.buffer_snapshot().enclosing_bracket_ranges(point..point); - let Some(ranges) = ranges else { - break; - }; - - let mut closest_starting_destination = None; - let mut closest_distance = usize::MAX; - - for (start_range, _) in ranges { - if start_range.start < offset { - let mut chars = map.buffer_snapshot().chars_at(start_range.start); - if Some(char) == chars.next() { - let distance = offset - start_range.start; - if distance < closest_distance { - closest_starting_destination = Some(start_range.start); - closest_distance = distance; - continue; - } - } - } - } - - let new_point = closest_starting_destination - .map(|destination| destination.to_display_point(map)) - .unwrap_or(display_point); - if new_point == display_point { - break; - } else { - display_point = new_point; - } - } - display_point -} - -fn find_forward( - map: &DisplaySnapshot, - from: DisplayPoint, - before: bool, - target: char, - times: usize, - mode: FindRange, - smartcase: bool, -) -> Option { - let mut to = from; - let mut found = false; - - for _ in 0..times { - found = false; - let new_to = find_boundary(map, to, mode, |_, right| { - found = is_character_match(target, right, smartcase); - found - }); - if to == new_to { - break; - } - to = new_to; - } - - if found { - if before && to.column() > 0 { - *to.column_mut() -= 1; - Some(map.clip_point(to, Bias::Left)) - } else if before && to.row().0 > 0 { - *to.row_mut() -= 1; - *to.column_mut() = map.line(to.row()).len() as u32; - Some(map.clip_point(to, Bias::Left)) - } else { - Some(to) - } - } else { - None - } -} - -fn find_backward( - map: &DisplaySnapshot, - from: DisplayPoint, - after: bool, - target: char, - times: usize, - mode: FindRange, - smartcase: bool, -) -> DisplayPoint { - let mut to = from; - - for _ in 0..times { - let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| { - is_character_match(target, right, smartcase) - }); - if to == new_to { - break; - } - to = new_to; - } - - let next = map.buffer_snapshot().chars_at(to.to_point(map)).next(); - if next.is_some() && is_character_match(target, next.unwrap(), smartcase) { - if after { - *to.column_mut() += 1; - map.clip_point(to, Bias::Right) - } else { - to - } - } else { - from - } -} - -/// Returns true if one char is equal to the other or its uppercase variant (if smartcase is true). -pub fn is_character_match(target: char, other: char, smartcase: bool) -> bool { - if smartcase { - if target.is_uppercase() { - target == other - } else { - target == other.to_ascii_lowercase() - } - } else { - target == other - } -} - -fn sneak( - map: &DisplaySnapshot, - from: DisplayPoint, - first_target: char, - second_target: char, - times: usize, - smartcase: bool, -) -> Option { - let mut to = from; - let mut found = false; - - for _ in 0..times { - found = false; - let new_to = find_boundary( - map, - movement::right(map, to), - FindRange::MultiLine, - |left, right| { - found = is_character_match(first_target, left, smartcase) - && is_character_match(second_target, right, smartcase); - found - }, - ); - if to == new_to { - break; - } - to = new_to; - } - - if found { - Some(movement::left(map, to)) - } else { - None - } -} - -fn sneak_backward( - map: &DisplaySnapshot, - from: DisplayPoint, - first_target: char, - second_target: char, - times: usize, - smartcase: bool, -) -> Option { - let mut to = from; - let mut found = false; - - for _ in 0..times { - found = false; - let new_to = - find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| { - found = is_character_match(first_target, left, smartcase) - && is_character_match(second_target, right, smartcase); - found - }); - if to == new_to { - break; - } - to = new_to; - } - - if found { - Some(movement::left(map, to)) - } else { - None - } -} - -fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint { - let correct_line = map.start_of_relative_buffer_row(point, times as isize); - first_non_whitespace(map, false, correct_line) -} - -fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint { - let correct_line = map.start_of_relative_buffer_row(point, -(times as isize)); - first_non_whitespace(map, false, correct_line) -} - -fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint { - let correct_line = map.start_of_relative_buffer_row(point, 0); - right(map, correct_line, times.saturating_sub(1)) -} - -pub(crate) fn next_line_end( - map: &DisplaySnapshot, - mut point: DisplayPoint, - times: usize, -) -> DisplayPoint { - if times > 1 { - point = map.start_of_relative_buffer_row(point, times as isize - 1); - } - end_of_line(map, false, point, 1) -} - -fn window_top( - map: &DisplaySnapshot, - point: DisplayPoint, - text_layout_details: &TextLayoutDetails, - mut times: usize, -) -> (DisplayPoint, SelectionGoal) { - let first_visible_line = text_layout_details - .scroll_anchor - .anchor - .to_display_point(map); - - if first_visible_line.row() != DisplayRow(0) - && text_layout_details.vertical_scroll_margin as usize > times - { - times = text_layout_details.vertical_scroll_margin.ceil() as usize; - } - - if let Some(visible_rows) = text_layout_details.visible_rows { - let bottom_row = first_visible_line.row().0 + visible_rows as u32; - let new_row = (first_visible_line.row().0 + (times as u32)) - .min(bottom_row) - .min(map.max_point().row().0); - let new_col = point.column().min(map.line_len(first_visible_line.row())); - - let new_point = DisplayPoint::new(DisplayRow(new_row), new_col); - (map.clip_point(new_point, Bias::Left), SelectionGoal::None) - } else { - let new_row = - DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0)); - let new_col = point.column().min(map.line_len(first_visible_line.row())); - - let new_point = DisplayPoint::new(new_row, new_col); - (map.clip_point(new_point, Bias::Left), SelectionGoal::None) - } -} - -fn window_middle( - map: &DisplaySnapshot, - point: DisplayPoint, - text_layout_details: &TextLayoutDetails, -) -> (DisplayPoint, SelectionGoal) { - if let Some(visible_rows) = text_layout_details.visible_rows { - let first_visible_line = text_layout_details - .scroll_anchor - .anchor - .to_display_point(map); - - let max_visible_rows = - (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0); - - let new_row = - (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0); - let new_row = DisplayRow(new_row); - let new_col = point.column().min(map.line_len(new_row)); - let new_point = DisplayPoint::new(new_row, new_col); - (map.clip_point(new_point, Bias::Left), SelectionGoal::None) - } else { - (point, SelectionGoal::None) - } -} - -fn window_bottom( - map: &DisplaySnapshot, - point: DisplayPoint, - text_layout_details: &TextLayoutDetails, - mut times: usize, -) -> (DisplayPoint, SelectionGoal) { - if let Some(visible_rows) = text_layout_details.visible_rows { - let first_visible_line = text_layout_details - .scroll_anchor - .anchor - .to_display_point(map); - let bottom_row = first_visible_line.row().0 - + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32; - if bottom_row < map.max_point().row().0 - && text_layout_details.vertical_scroll_margin as usize > times - { - times = text_layout_details.vertical_scroll_margin.ceil() as usize; - } - let bottom_row_capped = bottom_row.min(map.max_point().row().0); - let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0 - { - first_visible_line.row() - } else { - DisplayRow(bottom_row_capped.saturating_sub(times as u32)) - }; - let new_col = point.column().min(map.line_len(new_row)); - let new_point = DisplayPoint::new(new_row, new_col); - (map.clip_point(new_point, Bias::Left), SelectionGoal::None) - } else { - (point, SelectionGoal::None) - } -} - -fn method_motion( - map: &DisplaySnapshot, - mut display_point: DisplayPoint, - times: usize, - direction: Direction, - is_start: bool, -) -> DisplayPoint { - let Some((_, _, buffer)) = map.buffer_snapshot().as_singleton() else { - return display_point; - }; - - for _ in 0..times { - let point = map.display_point_to_point(display_point, Bias::Left); - let offset = point.to_offset(&map.buffer_snapshot()).0; - let range = if direction == Direction::Prev { - 0..offset - } else { - offset..buffer.len() - }; - - let possibilities = buffer - .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4)) - .filter_map(|(range, object)| { - if !matches!(object, language::TextObject::AroundFunction) { - return None; - } - - let relevant = if is_start { range.start } else { range.end }; - if direction == Direction::Prev && relevant < offset { - Some(relevant) - } else if direction == Direction::Next && relevant > offset + 1 { - Some(relevant) - } else { - None - } - }); - - let dest = if direction == Direction::Prev { - possibilities.max().unwrap_or(offset) - } else { - possibilities.min().unwrap_or(offset) - }; - let new_point = map.clip_point(MultiBufferOffset(dest).to_display_point(map), Bias::Left); - if new_point == display_point { - break; - } - display_point = new_point; - } - display_point -} - -fn comment_motion( - map: &DisplaySnapshot, - mut display_point: DisplayPoint, - times: usize, - direction: Direction, -) -> DisplayPoint { - let Some((_, _, buffer)) = map.buffer_snapshot().as_singleton() else { - return display_point; - }; - - for _ in 0..times { - let point = map.display_point_to_point(display_point, Bias::Left); - let offset = point.to_offset(&map.buffer_snapshot()).0; - let range = if direction == Direction::Prev { - 0..offset - } else { - offset..buffer.len() - }; - - let possibilities = buffer - .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6)) - .filter_map(|(range, object)| { - if !matches!(object, language::TextObject::AroundComment) { - return None; - } - - let relevant = if direction == Direction::Prev { - range.start - } else { - range.end - }; - if direction == Direction::Prev && relevant < offset { - Some(relevant) - } else if direction == Direction::Next && relevant > offset + 1 { - Some(relevant) - } else { - None - } - }); - - let dest = if direction == Direction::Prev { - possibilities.max().unwrap_or(offset) - } else { - possibilities.min().unwrap_or(offset) - }; - let new_point = map.clip_point(MultiBufferOffset(dest).to_display_point(map), Bias::Left); - if new_point == display_point { - break; - } - display_point = new_point; - } - - display_point -} - -fn section_motion( - map: &DisplaySnapshot, - mut display_point: DisplayPoint, - times: usize, - direction: Direction, - is_start: bool, -) -> DisplayPoint { - if map.buffer_snapshot().as_singleton().is_some() { - for _ in 0..times { - let offset = map - .display_point_to_point(display_point, Bias::Left) - .to_offset(&map.buffer_snapshot()); - let range = if direction == Direction::Prev { - MultiBufferOffset(0)..offset - } else { - offset..map.buffer_snapshot().len() - }; - - // we set a max start depth here because we want a section to only be "top level" - // similar to vim's default of '{' in the first column. - // (and without it, ]] at the start of editor.rs is -very- slow) - let mut possibilities = map - .buffer_snapshot() - .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3)) - .filter(|(_, object)| { - matches!( - object, - language::TextObject::AroundClass | language::TextObject::AroundFunction - ) - }) - .collect::>(); - possibilities.sort_by_key(|(range_a, _)| range_a.start); - let mut prev_end = None; - let possibilities = possibilities.into_iter().filter_map(|(range, t)| { - if t == language::TextObject::AroundFunction - && prev_end.is_some_and(|prev_end| prev_end > range.start) - { - return None; - } - prev_end = Some(range.end); - - let relevant = if is_start { range.start } else { range.end }; - if direction == Direction::Prev && relevant < offset { - Some(relevant) - } else if direction == Direction::Next && relevant > offset + 1usize { - Some(relevant) - } else { - None - } - }); - - let offset = if direction == Direction::Prev { - possibilities.max().unwrap_or(MultiBufferOffset(0)) - } else { - possibilities.min().unwrap_or(map.buffer_snapshot().len()) - }; - - let new_point = map.clip_point(offset.to_display_point(map), Bias::Left); - if new_point == display_point { - break; - } - display_point = new_point; - } - return display_point; - }; - - for _ in 0..times { - let next_point = if is_start { - movement::start_of_excerpt(map, display_point, direction) - } else { - movement::end_of_excerpt(map, display_point, direction) - }; - if next_point == display_point { - break; - } - display_point = next_point; - } - - display_point -} - -fn matches_indent_type( - target_indent: &text::LineIndent, - current_indent: &text::LineIndent, - indent_type: IndentType, -) -> bool { - match indent_type { - IndentType::Lesser => { - target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs - } - IndentType::Greater => { - target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs - } - IndentType::Same => { - target_indent.spaces == current_indent.spaces - && target_indent.tabs == current_indent.tabs - } - } -} - -fn indent_motion( - map: &DisplaySnapshot, - mut display_point: DisplayPoint, - times: usize, - direction: Direction, - indent_type: IndentType, -) -> DisplayPoint { - let buffer_point = map.display_point_to_point(display_point, Bias::Left); - let current_row = MultiBufferRow(buffer_point.row); - let current_indent = map.line_indent_for_buffer_row(current_row); - if current_indent.is_line_empty() { - return display_point; - } - let max_row = map.max_point().to_point(map).row; - - for _ in 0..times { - let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row; - - let target_row = match direction { - Direction::Next => (current_buffer_row + 1..=max_row).find(|&row| { - let indent = map.line_indent_for_buffer_row(MultiBufferRow(row)); - !indent.is_line_empty() - && matches_indent_type(&indent, ¤t_indent, indent_type) - }), - Direction::Prev => (0..current_buffer_row).rev().find(|&row| { - let indent = map.line_indent_for_buffer_row(MultiBufferRow(row)); - !indent.is_line_empty() - && matches_indent_type(&indent, ¤t_indent, indent_type) - }), - } - .unwrap_or(current_buffer_row); - - let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right); - let new_point = first_non_whitespace(map, false, new_point); - if new_point == display_point { - break; - } - display_point = new_point; - } - display_point -} - -#[cfg(test)] -mod test { - - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - use editor::Inlay; - use indoc::indoc; - use language::Point; - use multi_buffer::MultiBufferRow; - - #[gpui::test] - async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - let initial_state = indoc! {r"ˇabc - def - - paragraph - the second - - - - third and - final"}; - - // goes down once - cx.set_shared_state(initial_state).await; - cx.simulate_shared_keystrokes("}").await; - cx.shared_state().await.assert_eq(indoc! {r"abc - def - ˇ - paragraph - the second - - - - third and - final"}); - - // goes up once - cx.simulate_shared_keystrokes("{").await; - cx.shared_state().await.assert_eq(initial_state); - - // goes down twice - cx.simulate_shared_keystrokes("2 }").await; - cx.shared_state().await.assert_eq(indoc! {r"abc - def - - paragraph - the second - ˇ - - - third and - final"}); - - // goes down over multiple blanks - cx.simulate_shared_keystrokes("}").await; - cx.shared_state().await.assert_eq(indoc! {r"abc - def - - paragraph - the second - - - - third and - finaˇl"}); - - // goes up twice - cx.simulate_shared_keystrokes("2 {").await; - cx.shared_state().await.assert_eq(indoc! {r"abc - def - ˇ - paragraph - the second - - - - third and - final"}); - } - - #[gpui::test] - async fn test_matching(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {r"func ˇ(a string) { - do(something(with.and_arrays[0, 2])) - }"}) - .await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state() - .await - .assert_eq(indoc! {r"func (a stringˇ) { - do(something(with.and_arrays[0, 2])) - }"}); - - // test it works on the last character of the line - cx.set_shared_state(indoc! {r"func (a string) ˇ{ - do(something(with.and_arrays[0, 2])) - }"}) - .await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state() - .await - .assert_eq(indoc! {r"func (a string) { - do(something(with.and_arrays[0, 2])) - ˇ}"}); - - // test it works on immediate nesting - cx.set_shared_state("ˇ{()}").await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state().await.assert_eq("{()ˇ}"); - cx.simulate_shared_keystrokes("%").await; - cx.shared_state().await.assert_eq("ˇ{()}"); - - // test it works on immediate nesting inside braces - cx.set_shared_state("{\n ˇ{()}\n}").await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state().await.assert_eq("{\n {()ˇ}\n}"); - - // test it jumps to the next paren on a line - cx.set_shared_state("func ˇboop() {\n}").await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state().await.assert_eq("func boop(ˇ) {\n}"); - } - - #[gpui::test] - async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // test it works with curly braces - cx.set_shared_state(indoc! {r"func (a string) { - do(something(with.anˇd_arrays[0, 2])) - }"}) - .await; - cx.simulate_shared_keystrokes("] }").await; - cx.shared_state() - .await - .assert_eq(indoc! {r"func (a string) { - do(something(with.and_arrays[0, 2])) - ˇ}"}); - - // test it works with brackets - cx.set_shared_state(indoc! {r"func (a string) { - do(somethiˇng(with.and_arrays[0, 2])) - }"}) - .await; - cx.simulate_shared_keystrokes("] )").await; - cx.shared_state() - .await - .assert_eq(indoc! {r"func (a string) { - do(something(with.and_arrays[0, 2])ˇ) - }"}); - - cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"}) - .await; - cx.simulate_shared_keystrokes("] )").await; - cx.shared_state() - .await - .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"}); - - // test it works on immediate nesting - cx.set_shared_state("{ˇ {}{}}").await; - cx.simulate_shared_keystrokes("] }").await; - cx.shared_state().await.assert_eq("{ {}{}ˇ}"); - cx.set_shared_state("(ˇ ()())").await; - cx.simulate_shared_keystrokes("] )").await; - cx.shared_state().await.assert_eq("( ()()ˇ)"); - - // test it works on immediate nesting inside braces - cx.set_shared_state("{\n ˇ {()}\n}").await; - cx.simulate_shared_keystrokes("] }").await; - cx.shared_state().await.assert_eq("{\n {()}\nˇ}"); - cx.set_shared_state("(\n ˇ {()}\n)").await; - cx.simulate_shared_keystrokes("] )").await; - cx.shared_state().await.assert_eq("(\n {()}\nˇ)"); - } - - #[gpui::test] - async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // test it works with curly braces - cx.set_shared_state(indoc! {r"func (a string) { - do(something(with.anˇd_arrays[0, 2])) - }"}) - .await; - cx.simulate_shared_keystrokes("[ {").await; - cx.shared_state() - .await - .assert_eq(indoc! {r"func (a string) ˇ{ - do(something(with.and_arrays[0, 2])) - }"}); - - // test it works with brackets - cx.set_shared_state(indoc! {r"func (a string) { - do(somethiˇng(with.and_arrays[0, 2])) - }"}) - .await; - cx.simulate_shared_keystrokes("[ (").await; - cx.shared_state() - .await - .assert_eq(indoc! {r"func (a string) { - doˇ(something(with.and_arrays[0, 2])) - }"}); - - // test it works on immediate nesting - cx.set_shared_state("{{}{} ˇ }").await; - cx.simulate_shared_keystrokes("[ {").await; - cx.shared_state().await.assert_eq("ˇ{{}{} }"); - cx.set_shared_state("(()() ˇ )").await; - cx.simulate_shared_keystrokes("[ (").await; - cx.shared_state().await.assert_eq("ˇ(()() )"); - - // test it works on immediate nesting inside braces - cx.set_shared_state("{\n {()} ˇ\n}").await; - cx.simulate_shared_keystrokes("[ {").await; - cx.shared_state().await.assert_eq("ˇ{\n {()} \n}"); - cx.set_shared_state("(\n {()} ˇ\n)").await; - cx.simulate_shared_keystrokes("[ (").await; - cx.shared_state().await.assert_eq("ˇ(\n {()} \n)"); - } - - #[gpui::test] - async fn test_unmatched_forward_markdown(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new_markdown_with_rust(cx).await; - - cx.neovim.exec("set filetype=markdown").await; - - cx.set_shared_state(indoc! {r" - ```rs - impl Worktree { - pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> { - ˇ } - } - ``` - "}) - .await; - cx.simulate_shared_keystrokes("] }").await; - cx.shared_state().await.assert_eq(indoc! {r" - ```rs - impl Worktree { - pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> { - ˇ} - } - ``` - "}); - - cx.set_shared_state(indoc! {r" - ```rs - impl Worktree { - pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> { - } ˇ - } - ``` - "}) - .await; - cx.simulate_shared_keystrokes("] }").await; - cx.shared_state().await.assert_eq(indoc! {r" - ```rs - impl Worktree { - pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> { - } • - ˇ} - ``` - "}); - } - - #[gpui::test] - async fn test_unmatched_backward_markdown(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new_markdown_with_rust(cx).await; - - cx.neovim.exec("set filetype=markdown").await; - - cx.set_shared_state(indoc! {r" - ```rs - impl Worktree { - pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> { - ˇ } - } - ``` - "}) - .await; - cx.simulate_shared_keystrokes("[ {").await; - cx.shared_state().await.assert_eq(indoc! {r" - ```rs - impl Worktree { - pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{ - } - } - ``` - "}); - - cx.set_shared_state(indoc! {r" - ```rs - impl Worktree { - pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> { - } ˇ - } - ``` - "}) - .await; - cx.simulate_shared_keystrokes("[ {").await; - cx.shared_state().await.assert_eq(indoc! {r" - ```rs - impl Worktree ˇ{ - pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> { - } • - } - ``` - "}); - } - - #[gpui::test] - async fn test_matching_tags(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new_html(cx).await; - - cx.neovim.exec("set filetype=html").await; - - cx.set_shared_state(indoc! {r""}).await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state() - .await - .assert_eq(indoc! {r"<ˇ/body>"}); - cx.simulate_shared_keystrokes("%").await; - - // test jumping backwards - cx.shared_state() - .await - .assert_eq(indoc! {r"<ˇbody>"}); - - // test self-closing tags - cx.set_shared_state(indoc! {r""}).await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state().await.assert_eq(indoc! {r""}); - - // test tag with attributes - cx.set_shared_state(indoc! {r"
-
- "}) - .await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state() - .await - .assert_eq(indoc! {r"
- <ˇ/div> - "}); - - // test multi-line self-closing tag - cx.set_shared_state(indoc! {r#" -
-
"#}) - .await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state().await.assert_eq(indoc! {r#" - ˇ
-
"#}); - - // test nested closing tag - cx.set_shared_state(indoc! {r#" - - - "#}) - .await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state().await.assert_eq(indoc! {r#" - - <ˇ/body> - "#}); - cx.simulate_shared_keystrokes("%").await; - cx.shared_state().await.assert_eq(indoc! {r#" - <ˇbody> - - "#}); - } - - #[gpui::test] - async fn test_matching_braces_in_tag(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new_typescript(cx).await; - - // test brackets within tags - cx.set_shared_state(indoc! {r"function f() { - return ( -
-

test

-
- ); - }"}) - .await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state().await.assert_eq(indoc! {r"function f() { - return ( -
-

test

-
- ); - }"}); - } - - #[gpui::test] - async fn test_matching_nested_brackets(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new_tsx(cx).await; - - cx.set_shared_state(indoc! {r""}) - .await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state() - .await - .assert_eq(indoc! {r""}); - cx.simulate_shared_keystrokes("%").await; - cx.shared_state() - .await - .assert_eq(indoc! {r""}); - } - - #[gpui::test] - async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // f and F - cx.set_shared_state("ˇone two three four").await; - cx.simulate_shared_keystrokes("f o").await; - cx.shared_state().await.assert_eq("one twˇo three four"); - cx.simulate_shared_keystrokes(",").await; - cx.shared_state().await.assert_eq("ˇone two three four"); - cx.simulate_shared_keystrokes("2 ;").await; - cx.shared_state().await.assert_eq("one two three fˇour"); - cx.simulate_shared_keystrokes("shift-f e").await; - cx.shared_state().await.assert_eq("one two threˇe four"); - cx.simulate_shared_keystrokes("2 ;").await; - cx.shared_state().await.assert_eq("onˇe two three four"); - cx.simulate_shared_keystrokes(",").await; - cx.shared_state().await.assert_eq("one two thrˇee four"); - - // t and T - cx.set_shared_state("ˇone two three four").await; - cx.simulate_shared_keystrokes("t o").await; - cx.shared_state().await.assert_eq("one tˇwo three four"); - cx.simulate_shared_keystrokes(",").await; - cx.shared_state().await.assert_eq("oˇne two three four"); - cx.simulate_shared_keystrokes("2 ;").await; - cx.shared_state().await.assert_eq("one two three ˇfour"); - cx.simulate_shared_keystrokes("shift-t e").await; - cx.shared_state().await.assert_eq("one two threeˇ four"); - cx.simulate_shared_keystrokes("3 ;").await; - cx.shared_state().await.assert_eq("oneˇ two three four"); - cx.simulate_shared_keystrokes(",").await; - cx.shared_state().await.assert_eq("one two thˇree four"); - } - - #[gpui::test] - async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - let initial_state = indoc! {r"something(ˇfoo)"}; - cx.set_shared_state(initial_state).await; - cx.simulate_shared_keystrokes("}").await; - cx.shared_state().await.assert_eq("something(fooˇ)"); - } - - #[gpui::test] - async fn test_next_line_start(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("ˇone\n two\nthree").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq("one\n ˇtwo\nthree"); - } - - #[gpui::test] - async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("ˇ one\n two \nthree").await; - cx.simulate_shared_keystrokes("g _").await; - cx.shared_state().await.assert_eq(" onˇe\n two \nthree"); - - cx.set_shared_state("ˇ one \n two \nthree").await; - cx.simulate_shared_keystrokes("g _").await; - cx.shared_state().await.assert_eq(" onˇe \n two \nthree"); - cx.simulate_shared_keystrokes("2 g _").await; - cx.shared_state().await.assert_eq(" one \n twˇo \nthree"); - } - - #[gpui::test] - async fn test_window_top(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - let initial_state = indoc! {r"abc - def - paragraph - the second - third ˇand - final"}; - - cx.set_shared_state(initial_state).await; - cx.simulate_shared_keystrokes("shift-h").await; - cx.shared_state().await.assert_eq(indoc! {r"abˇc - def - paragraph - the second - third and - final"}); - - // clip point - cx.set_shared_state(indoc! {r" - 1 2 3 - 4 5 6 - 7 8 ˇ9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-h").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 ˇ3 - 4 5 6 - 7 8 9 - "}); - - cx.set_shared_state(indoc! {r" - 1 2 3 - 4 5 6 - ˇ7 8 9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-h").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇ1 2 3 - 4 5 6 - 7 8 9 - "}); - - cx.set_shared_state(indoc! {r" - 1 2 3 - 4 5 ˇ6 - 7 8 9"}) - .await; - cx.simulate_shared_keystrokes("9 shift-h").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 3 - 4 5 6 - 7 8 ˇ9"}); - } - - #[gpui::test] - async fn test_window_middle(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - let initial_state = indoc! {r"abˇc - def - paragraph - the second - third and - final"}; - - cx.set_shared_state(initial_state).await; - cx.simulate_shared_keystrokes("shift-m").await; - cx.shared_state().await.assert_eq(indoc! {r"abc - def - paˇragraph - the second - third and - final"}); - - cx.set_shared_state(indoc! {r" - 1 2 3 - 4 5 6 - 7 8 ˇ9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-m").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 3 - 4 5 ˇ6 - 7 8 9 - "}); - cx.set_shared_state(indoc! {r" - 1 2 3 - 4 5 6 - ˇ7 8 9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-m").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 3 - ˇ4 5 6 - 7 8 9 - "}); - cx.set_shared_state(indoc! {r" - ˇ1 2 3 - 4 5 6 - 7 8 9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-m").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 3 - ˇ4 5 6 - 7 8 9 - "}); - cx.set_shared_state(indoc! {r" - 1 2 3 - ˇ4 5 6 - 7 8 9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-m").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 3 - ˇ4 5 6 - 7 8 9 - "}); - cx.set_shared_state(indoc! {r" - 1 2 3 - 4 5 ˇ6 - 7 8 9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-m").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 3 - 4 5 ˇ6 - 7 8 9 - "}); - } - - #[gpui::test] - async fn test_window_bottom(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - let initial_state = indoc! {r"abc - deˇf - paragraph - the second - third and - final"}; - - cx.set_shared_state(initial_state).await; - cx.simulate_shared_keystrokes("shift-l").await; - cx.shared_state().await.assert_eq(indoc! {r"abc - def - paragraph - the second - third and - fiˇnal"}); - - cx.set_shared_state(indoc! {r" - 1 2 3 - 4 5 ˇ6 - 7 8 9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-l").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 3 - 4 5 6 - 7 8 9 - ˇ"}); - - cx.set_shared_state(indoc! {r" - 1 2 3 - ˇ4 5 6 - 7 8 9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-l").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 3 - 4 5 6 - 7 8 9 - ˇ"}); - - cx.set_shared_state(indoc! {r" - 1 2 ˇ3 - 4 5 6 - 7 8 9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-l").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 3 - 4 5 6 - 7 8 9 - ˇ"}); - - cx.set_shared_state(indoc! {r" - ˇ1 2 3 - 4 5 6 - 7 8 9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-l").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 3 - 4 5 6 - 7 8 9 - ˇ"}); - - cx.set_shared_state(indoc! {r" - 1 2 3 - 4 5 ˇ6 - 7 8 9 - "}) - .await; - cx.simulate_shared_keystrokes("9 shift-l").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 ˇ3 - 4 5 6 - 7 8 9 - "}); - } - - #[gpui::test] - async fn test_previous_word_end(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {r" - 456 5ˇ67 678 - "}) - .await; - cx.simulate_shared_keystrokes("g e").await; - cx.shared_state().await.assert_eq(indoc! {" - 45ˇ6 567 678 - "}); - - // Test times - cx.set_shared_state(indoc! {r" - 123 234 345 - 456 5ˇ67 678 - "}) - .await; - cx.simulate_shared_keystrokes("4 g e").await; - cx.shared_state().await.assert_eq(indoc! {" - 12ˇ3 234 345 - 456 567 678 - "}); - - // With punctuation - cx.set_shared_state(indoc! {r" - 123 234 345 - 4;5.6 5ˇ67 678 - 789 890 901 - "}) - .await; - cx.simulate_shared_keystrokes("g e").await; - cx.shared_state().await.assert_eq(indoc! {" - 123 234 345 - 4;5.ˇ6 567 678 - 789 890 901 - "}); - - // With punctuation and count - cx.set_shared_state(indoc! {r" - 123 234 345 - 4;5.6 5ˇ67 678 - 789 890 901 - "}) - .await; - cx.simulate_shared_keystrokes("5 g e").await; - cx.shared_state().await.assert_eq(indoc! {" - 123 234 345 - ˇ4;5.6 567 678 - 789 890 901 - "}); - - // newlines - cx.set_shared_state(indoc! {r" - 123 234 345 - - 78ˇ9 890 901 - "}) - .await; - cx.simulate_shared_keystrokes("g e").await; - cx.shared_state().await.assert_eq(indoc! {" - 123 234 345 - ˇ - 789 890 901 - "}); - cx.simulate_shared_keystrokes("g e").await; - cx.shared_state().await.assert_eq(indoc! {" - 123 234 34ˇ5 - - 789 890 901 - "}); - - // With punctuation - cx.set_shared_state(indoc! {r" - 123 234 345 - 4;5.ˇ6 567 678 - 789 890 901 - "}) - .await; - cx.simulate_shared_keystrokes("g shift-e").await; - cx.shared_state().await.assert_eq(indoc! {" - 123 234 34ˇ5 - 4;5.6 567 678 - 789 890 901 - "}); - - // With multi byte char - cx.set_shared_state(indoc! {r" - bar ˇó - "}) - .await; - cx.simulate_shared_keystrokes("g e").await; - cx.shared_state().await.assert_eq(indoc! {" - baˇr ó - "}); - } - - #[gpui::test] - async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - fn aˇ() { - return - } - "}) - .await; - cx.simulate_shared_keystrokes("v $ %").await; - cx.shared_state().await.assert_eq(indoc! {" - fn a«() { - return - }ˇ» - "}); - } - - #[gpui::test] - async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - struct Foo { - ˇ - } - "}, - Mode::Normal, - ); - - cx.update_editor(|editor, _window, cx| { - let range = editor.selections.newest_anchor().range(); - let inlay_text = " field: int,\n field2: string\n field3: float"; - let inlay = Inlay::edit_prediction(1, range.start, inlay_text); - editor.splice_inlays(&[], vec![inlay], cx); - }); - - cx.simulate_keystrokes("j"); - cx.assert_state( - indoc! {" - struct Foo { - - ˇ} - "}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - ˇstruct Foo { - - } - "}, - Mode::Normal, - ); - cx.update_editor(|editor, _window, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let end_of_line = - snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0)))); - let inlay_text = " hint"; - let inlay = Inlay::edit_prediction(1, end_of_line, inlay_text); - editor.splice_inlays(&[], vec![inlay], cx); - }); - cx.simulate_keystrokes("$"); - cx.assert_state( - indoc! {" - struct Foo ˇ{ - - } - "}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_visual_mode_with_inlay_hints_on_empty_line(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Test the exact scenario from issue #29134 - cx.set_state( - indoc! {" - fn main() { - let this_is_a_long_name = Vec::::new(); - let new_oneˇ = this_is_a_long_name - .iter() - .map(|i| i + 1) - .map(|i| i * 2) - .collect::>(); - } - "}, - Mode::Normal, - ); - - // Add type hint inlay on the empty line (line 3, after "this_is_a_long_name") - cx.update_editor(|editor, _window, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - // The empty line is at line 3 (0-indexed) - let line_start = snapshot.anchor_after(Point::new(3, 0)); - let inlay_text = ": Vec"; - let inlay = Inlay::edit_prediction(1, line_start, inlay_text); - editor.splice_inlays(&[], vec![inlay], cx); - }); - - // Enter visual mode - cx.simulate_keystrokes("v"); - cx.assert_state( - indoc! {" - fn main() { - let this_is_a_long_name = Vec::::new(); - let new_one« ˇ»= this_is_a_long_name - .iter() - .map(|i| i + 1) - .map(|i| i * 2) - .collect::>(); - } - "}, - Mode::Visual, - ); - - // Move down - should go to the beginning of line 4, not skip to line 5 - cx.simulate_keystrokes("j"); - cx.assert_state( - indoc! {" - fn main() { - let this_is_a_long_name = Vec::::new(); - let new_one« = this_is_a_long_name - ˇ» .iter() - .map(|i| i + 1) - .map(|i| i * 2) - .collect::>(); - } - "}, - Mode::Visual, - ); - - // Test with multiple movements - cx.set_state("let aˇ = 1;\nlet b = 2;\n\nlet c = 3;", Mode::Normal); - - // Add type hint on the empty line - cx.update_editor(|editor, _window, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let empty_line_start = snapshot.anchor_after(Point::new(2, 0)); - let inlay_text = ": i32"; - let inlay = Inlay::edit_prediction(2, empty_line_start, inlay_text); - editor.splice_inlays(&[], vec![inlay], cx); - }); - - // Enter visual mode and move down twice - cx.simulate_keystrokes("v j j"); - cx.assert_state("let a« = 1;\nlet b = 2;\n\nˇ»let c = 3;", Mode::Visual); - } - - #[gpui::test] - async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - // Normal mode - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("2 0 %").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox ˇjumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog"}); - - cx.simulate_shared_keystrokes("2 5 %").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jumps over - the ˇlazy dog - The quick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog"}); - - cx.simulate_shared_keystrokes("7 5 %").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog - The ˇquick brown - fox jumps over - the lazy dog"}); - - // Visual mode - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("v 5 0 %").await; - cx.shared_state().await.assert_eq(indoc! {" - The «quick brown - fox jumps over - the lazy dog - The quick brown - fox jˇ»umps over - the lazy dog - The quick brown - fox jumps over - the lazy dog"}); - - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("v 1 0 0 %").await; - cx.shared_state().await.assert_eq(indoc! {" - The «quick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lˇ»azy dog"}); - } - - #[gpui::test] - async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇπππππ").await; - cx.simulate_shared_keystrokes("3 space").await; - cx.shared_state().await.assert_eq("πππˇππ"); - } - - #[gpui::test] - async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ππππˇπ - πanotherline"}) - .await; - cx.simulate_shared_keystrokes("4 space").await; - cx.shared_state().await.assert_eq(indoc! {" - πππππ - πanˇotherline"}); - } - - #[gpui::test] - async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ππππ - πanˇotherline"}) - .await; - cx.simulate_shared_keystrokes("4 backspace").await; - cx.shared_state().await.assert_eq(indoc! {" - πππˇπ - πanotherline"}); - } - - #[gpui::test] - async fn test_go_to_indent(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.set_state( - indoc! { - "func empty(a string) bool { - ˇif a == \"\" { - return true - } - return false - }" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("[ -"); - cx.assert_state( - indoc! { - "ˇfunc empty(a string) bool { - if a == \"\" { - return true - } - return false - }" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("] ="); - cx.assert_state( - indoc! { - "func empty(a string) bool { - if a == \"\" { - return true - } - return false - ˇ}" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("[ +"); - cx.assert_state( - indoc! { - "func empty(a string) bool { - if a == \"\" { - return true - } - ˇreturn false - }" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("2 [ ="); - cx.assert_state( - indoc! { - "func empty(a string) bool { - ˇif a == \"\" { - return true - } - return false - }" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("] +"); - cx.assert_state( - indoc! { - "func empty(a string) bool { - if a == \"\" { - ˇreturn true - } - return false - }" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("] -"); - cx.assert_state( - indoc! { - "func empty(a string) bool { - if a == \"\" { - return true - ˇ} - return false - }" - }, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("abˇc").await; - cx.simulate_shared_keystrokes("delete").await; - cx.shared_state().await.assert_eq("aˇb"); - } - - #[gpui::test] - async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇthe quick brown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v 0").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇhe quick brown fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - the quick bˇrown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v 0").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇown fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - the quick brown foˇx - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v 0").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇ - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - } - - #[gpui::test] - async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇthe quick brown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v g shift-m").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇbrown fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - the quick bˇrown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v g shift-m").await; - cx.shared_state().await.assert_eq(indoc! {" - the quickˇown fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - the quick brown foˇx - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v g shift-m").await; - cx.shared_state().await.assert_eq(indoc! {" - the quicˇk - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - ˇthe quick brown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇ fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - ˇthe quick brown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇuick brown fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - } - - #[gpui::test] - async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - the quick brown foˇx - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v $").await; - cx.shared_state().await.assert_eq(indoc! {" - the quick brown foˇx - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - ˇthe quick brown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v $").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇx - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - } - - #[gpui::test] - async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇthe quick brown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("y v j p").await; - cx.shared_state().await.assert_eq(indoc! {" - the quick brown fox - ˇthe quick brown fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - the quick bˇrown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("y v j p").await; - cx.shared_state().await.assert_eq(indoc! {" - the quick brˇrown fox - jumped overown fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - the quick brown foˇx - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("y v j p").await; - cx.shared_state().await.assert_eq(indoc! {" - the quick brown foxˇx - jumped over the la - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - the quick brown fox - jˇumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("y v k p").await; - cx.shared_state().await.assert_eq(indoc! {" - thˇhe quick brown fox - je quick brown fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - } - - #[gpui::test] - async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇthe quick brown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v e").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇe quick brown fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - the quick bˇrown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v e").await; - cx.shared_state().await.assert_eq(indoc! {" - the quick bˇn fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - the quick brown foˇx - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v e").await; - cx.shared_state().await.assert_eq(indoc! {" - the quick brown foˇd over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - } -} diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs deleted file mode 100644 index aee0b424f0..0000000000 --- a/crates/vim/src/normal.rs +++ /dev/null @@ -1,2320 +0,0 @@ -mod change; -mod convert; -mod delete; -mod increment; -pub(crate) mod mark; -pub(crate) mod paste; -pub(crate) mod repeat; -mod scroll; -pub(crate) mod search; -pub mod substitute; -mod toggle_comments; -pub(crate) mod yank; - -use std::collections::HashMap; -use std::sync::Arc; - -use crate::{ - Vim, - indent::IndentDirection, - motion::{self, Motion, first_non_whitespace, next_line_end, right}, - object::Object, - state::{Mark, Mode, Operator}, - surrounds::SurroundsType, -}; -use collections::BTreeSet; -use convert::ConvertTarget; -use editor::Editor; -use editor::{Anchor, SelectionEffects}; -use editor::{Bias, ToPoint}; -use editor::{display_map::ToDisplayPoint, movement}; -use gpui::{Context, Window, actions}; -use language::{Point, SelectionGoal}; -use log::error; -use multi_buffer::MultiBufferRow; - -actions!( - vim, - [ - /// Inserts text after the cursor. - InsertAfter, - /// Inserts text before the cursor. - InsertBefore, - /// Inserts at the first non-whitespace character. - InsertFirstNonWhitespace, - /// Inserts at the end of the line. - InsertEndOfLine, - /// Inserts a new line above the current line. - InsertLineAbove, - /// Inserts a new line below the current line. - InsertLineBelow, - /// Inserts an empty line above without entering insert mode. - InsertEmptyLineAbove, - /// Inserts an empty line below without entering insert mode. - InsertEmptyLineBelow, - /// Inserts at the previous insert position. - InsertAtPrevious, - /// Joins the current line with the next line. - JoinLines, - /// Joins lines without adding whitespace. - JoinLinesNoWhitespace, - /// Deletes character to the left. - DeleteLeft, - /// Deletes character to the right. - DeleteRight, - /// Deletes using Helix-style behavior. - HelixDelete, - /// Collapse the current selection - HelixCollapseSelection, - /// Changes from cursor to end of line. - ChangeToEndOfLine, - /// Deletes from cursor to end of line. - DeleteToEndOfLine, - /// Yanks (copies) the selected text. - Yank, - /// Yanks the entire line. - YankLine, - /// Yanks from cursor to end of line. - YankToEndOfLine, - /// Toggles the case of selected text. - ChangeCase, - /// Converts selected text to uppercase. - ConvertToUpperCase, - /// Converts selected text to lowercase. - ConvertToLowerCase, - /// Applies ROT13 cipher to selected text. - ConvertToRot13, - /// Applies ROT47 cipher to selected text. - ConvertToRot47, - /// Toggles comments for selected lines. - ToggleComments, - /// Shows the current location in the file. - ShowLocation, - /// Undoes the last change. - Undo, - /// Redoes the last undone change. - Redo, - /// Undoes all changes to the most recently changed line. - UndoLastLine, - /// Go to tab page (with count support). - GoToTab, - /// Go to previous tab page (with count support). - GoToPreviousTab, - /// Go to tab page (with count support). - GoToPreviousReference, - /// Go to previous tab page (with count support). - GoToNextReference, - ] -); - -pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, Vim::insert_after); - Vim::action(editor, cx, Vim::insert_before); - Vim::action(editor, cx, Vim::insert_first_non_whitespace); - Vim::action(editor, cx, Vim::insert_end_of_line); - Vim::action(editor, cx, Vim::insert_line_above); - Vim::action(editor, cx, Vim::insert_line_below); - Vim::action(editor, cx, Vim::insert_empty_line_above); - Vim::action(editor, cx, Vim::insert_empty_line_below); - Vim::action(editor, cx, Vim::insert_at_previous); - Vim::action(editor, cx, Vim::change_case); - Vim::action(editor, cx, Vim::convert_to_upper_case); - Vim::action(editor, cx, Vim::convert_to_lower_case); - Vim::action(editor, cx, Vim::convert_to_rot13); - Vim::action(editor, cx, Vim::convert_to_rot47); - Vim::action(editor, cx, Vim::yank_line); - Vim::action(editor, cx, Vim::yank_to_end_of_line); - Vim::action(editor, cx, Vim::toggle_comments); - Vim::action(editor, cx, Vim::paste); - Vim::action(editor, cx, Vim::show_location); - - Vim::action(editor, cx, |vim, _: &DeleteLeft, window, cx| { - vim.record_current_action(cx); - let times = Vim::take_count(cx); - let forced_motion = Vim::take_forced_motion(cx); - vim.delete_motion(Motion::Left, times, forced_motion, window, cx); - }); - Vim::action(editor, cx, |vim, _: &DeleteRight, window, cx| { - vim.record_current_action(cx); - let times = Vim::take_count(cx); - let forced_motion = Vim::take_forced_motion(cx); - vim.delete_motion(Motion::Right, times, forced_motion, window, cx); - }); - - Vim::action(editor, cx, |vim, _: &HelixDelete, window, cx| { - vim.record_current_action(cx); - vim.update_editor(cx, |_, editor, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - if selection.is_empty() { - selection.end = movement::right(map, selection.end) - } - }) - }) - }); - vim.visual_delete(false, window, cx); - vim.switch_mode(Mode::HelixNormal, true, window, cx); - }); - - Vim::action(editor, cx, |vim, _: &HelixCollapseSelection, window, cx| { - vim.update_editor(cx, |_, editor, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let mut point = selection.head(); - if !selection.reversed && !selection.is_empty() { - point = movement::left(map, selection.head()); - } - selection.collapse_to(point, selection.goal) - }); - }); - }); - }); - - Vim::action(editor, cx, |vim, _: &ChangeToEndOfLine, window, cx| { - vim.start_recording(cx); - let times = Vim::take_count(cx); - let forced_motion = Vim::take_forced_motion(cx); - vim.change_motion( - Motion::EndOfLine { - display_lines: false, - }, - times, - forced_motion, - window, - cx, - ); - }); - Vim::action(editor, cx, |vim, _: &DeleteToEndOfLine, window, cx| { - vim.record_current_action(cx); - let times = Vim::take_count(cx); - let forced_motion = Vim::take_forced_motion(cx); - vim.delete_motion( - Motion::EndOfLine { - display_lines: false, - }, - times, - forced_motion, - window, - cx, - ); - }); - Vim::action(editor, cx, |vim, _: &JoinLines, window, cx| { - vim.join_lines_impl(true, window, cx); - }); - - Vim::action(editor, cx, |vim, _: &JoinLinesNoWhitespace, window, cx| { - vim.join_lines_impl(false, window, cx); - }); - - Vim::action(editor, cx, |vim, _: &GoToPreviousReference, window, cx| { - let count = Vim::take_count(cx); - vim.update_editor(cx, |_, editor, cx| { - let task = editor.go_to_reference_before_or_after_position( - editor::Direction::Prev, - count.unwrap_or(1), - window, - cx, - ); - if let Some(task) = task { - task.detach_and_log_err(cx); - }; - }); - }); - - Vim::action(editor, cx, |vim, _: &GoToNextReference, window, cx| { - let count = Vim::take_count(cx); - vim.update_editor(cx, |_, editor, cx| { - let task = editor.go_to_reference_before_or_after_position( - editor::Direction::Next, - count.unwrap_or(1), - window, - cx, - ); - if let Some(task) = task { - task.detach_and_log_err(cx); - }; - }); - }); - - Vim::action(editor, cx, |vim, _: &Undo, window, cx| { - let times = Vim::take_count(cx); - Vim::take_forced_motion(cx); - vim.update_editor(cx, |_, editor, cx| { - for _ in 0..times.unwrap_or(1) { - editor.undo(&editor::actions::Undo, window, cx); - } - }); - }); - Vim::action(editor, cx, |vim, _: &Redo, window, cx| { - let times = Vim::take_count(cx); - Vim::take_forced_motion(cx); - vim.update_editor(cx, |_, editor, cx| { - for _ in 0..times.unwrap_or(1) { - editor.redo(&editor::actions::Redo, window, cx); - } - }); - }); - Vim::action(editor, cx, |vim, _: &UndoLastLine, window, cx| { - Vim::take_forced_motion(cx); - vim.update_editor(cx, |vim, editor, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let Some(last_change) = editor.change_list.last_before_grouping() else { - return; - }; - - let anchors = last_change.to_vec(); - let mut last_row = None; - let ranges: Vec<_> = anchors - .iter() - .filter_map(|anchor| { - let point = anchor.to_point(&snapshot); - if last_row == Some(point.row) { - return None; - } - last_row = Some(point.row); - let line_range = Point::new(point.row, 0) - ..Point::new(point.row, snapshot.line_len(MultiBufferRow(point.row))); - Some(( - snapshot.anchor_before(line_range.start) - ..snapshot.anchor_after(line_range.end), - line_range, - )) - }) - .collect(); - - let edits = editor.buffer().update(cx, |buffer, cx| { - let current_content = ranges - .iter() - .map(|(anchors, _)| { - buffer - .snapshot(cx) - .text_for_range(anchors.clone()) - .collect::() - }) - .collect::>(); - let mut content_before_undo = current_content.clone(); - let mut undo_count = 0; - - loop { - let undone_tx = buffer.undo(cx); - undo_count += 1; - let mut content_after_undo = Vec::new(); - - let mut line_changed = false; - for ((anchors, _), text_before_undo) in - ranges.iter().zip(content_before_undo.iter()) - { - let snapshot = buffer.snapshot(cx); - let text_after_undo = - snapshot.text_for_range(anchors.clone()).collect::(); - - if &text_after_undo != text_before_undo { - line_changed = true; - } - content_after_undo.push(text_after_undo); - } - - content_before_undo = content_after_undo; - if !line_changed { - break; - } - if undone_tx == vim.undo_last_line_tx { - break; - } - } - - let edits = ranges - .into_iter() - .zip(content_before_undo.into_iter().zip(current_content)) - .filter_map(|((_, mut points), (mut old_text, new_text))| { - if new_text == old_text { - return None; - } - let common_suffix_starts_at = old_text - .char_indices() - .rev() - .zip(new_text.chars().rev()) - .find_map( - |((i, a), b)| { - if a != b { Some(i + a.len_utf8()) } else { None } - }, - ) - .unwrap_or(old_text.len()); - points.end.column -= (old_text.len() - common_suffix_starts_at) as u32; - old_text = old_text.split_at(common_suffix_starts_at).0.to_string(); - let common_prefix_len = old_text - .char_indices() - .zip(new_text.chars()) - .find_map(|((i, a), b)| if a != b { Some(i) } else { None }) - .unwrap_or(0); - points.start.column = common_prefix_len as u32; - old_text = old_text.split_at(common_prefix_len).1.to_string(); - - Some((points, old_text)) - }) - .collect::>(); - - for _ in 0..undo_count { - buffer.redo(cx); - } - edits - }); - vim.undo_last_line_tx = editor.transact(window, cx, |editor, window, cx| { - editor.change_list.invert_last_group(); - editor.edit(edits, cx); - editor.change_selections(SelectionEffects::default(), window, cx, |s| { - s.select_anchor_ranges(anchors.into_iter().map(|a| a..a)); - }) - }); - }); - }); - - repeat::register(editor, cx); - scroll::register(editor, cx); - search::register(editor, cx); - substitute::register(editor, cx); - increment::register(editor, cx); -} - -impl Vim { - pub fn normal_motion( - &mut self, - motion: Motion, - operator: Option, - times: Option, - forced_motion: bool, - window: &mut Window, - cx: &mut Context, - ) { - match operator { - None => self.move_cursor(motion, times, window, cx), - Some(Operator::Change) => self.change_motion(motion, times, forced_motion, window, cx), - Some(Operator::Delete) => self.delete_motion(motion, times, forced_motion, window, cx), - Some(Operator::Yank) => self.yank_motion(motion, times, forced_motion, window, cx), - Some(Operator::AddSurrounds { target: None }) => {} - Some(Operator::Indent) => self.indent_motion( - motion, - times, - forced_motion, - IndentDirection::In, - window, - cx, - ), - Some(Operator::Rewrap) => self.rewrap_motion(motion, times, forced_motion, window, cx), - Some(Operator::Outdent) => self.indent_motion( - motion, - times, - forced_motion, - IndentDirection::Out, - window, - cx, - ), - Some(Operator::AutoIndent) => self.indent_motion( - motion, - times, - forced_motion, - IndentDirection::Auto, - window, - cx, - ), - Some(Operator::ShellCommand) => { - self.shell_command_motion(motion, times, forced_motion, window, cx) - } - Some(Operator::Lowercase) => self.convert_motion( - motion, - times, - forced_motion, - ConvertTarget::LowerCase, - window, - cx, - ), - Some(Operator::Uppercase) => self.convert_motion( - motion, - times, - forced_motion, - ConvertTarget::UpperCase, - window, - cx, - ), - Some(Operator::OppositeCase) => self.convert_motion( - motion, - times, - forced_motion, - ConvertTarget::OppositeCase, - window, - cx, - ), - Some(Operator::Rot13) => self.convert_motion( - motion, - times, - forced_motion, - ConvertTarget::Rot13, - window, - cx, - ), - Some(Operator::Rot47) => self.convert_motion( - motion, - times, - forced_motion, - ConvertTarget::Rot47, - window, - cx, - ), - Some(Operator::ToggleComments) => { - self.toggle_comments_motion(motion, times, forced_motion, window, cx) - } - Some(Operator::ReplaceWithRegister) => { - self.replace_with_register_motion(motion, times, forced_motion, window, cx) - } - Some(Operator::Exchange) => { - self.exchange_motion(motion, times, forced_motion, window, cx) - } - Some(operator) => { - // Can't do anything for text objects, Ignoring - error!("Unexpected normal mode motion operator: {:?}", operator) - } - } - // Exit temporary normal mode (if active). - self.exit_temporary_normal(window, cx); - } - - pub fn normal_object( - &mut self, - object: Object, - times: Option, - opening: bool, - window: &mut Window, - cx: &mut Context, - ) { - let mut waiting_operator: Option = None; - match self.maybe_pop_operator() { - Some(Operator::Object { around }) => match self.maybe_pop_operator() { - Some(Operator::Change) => self.change_object(object, around, times, window, cx), - Some(Operator::Delete) => self.delete_object(object, around, times, window, cx), - Some(Operator::Yank) => self.yank_object(object, around, times, window, cx), - Some(Operator::Indent) => { - self.indent_object(object, around, IndentDirection::In, times, window, cx) - } - Some(Operator::Outdent) => { - self.indent_object(object, around, IndentDirection::Out, times, window, cx) - } - Some(Operator::AutoIndent) => { - self.indent_object(object, around, IndentDirection::Auto, times, window, cx) - } - Some(Operator::ShellCommand) => { - self.shell_command_object(object, around, window, cx); - } - Some(Operator::Rewrap) => self.rewrap_object(object, around, times, window, cx), - Some(Operator::Lowercase) => { - self.convert_object(object, around, ConvertTarget::LowerCase, times, window, cx) - } - Some(Operator::Uppercase) => { - self.convert_object(object, around, ConvertTarget::UpperCase, times, window, cx) - } - Some(Operator::OppositeCase) => self.convert_object( - object, - around, - ConvertTarget::OppositeCase, - times, - window, - cx, - ), - Some(Operator::Rot13) => { - self.convert_object(object, around, ConvertTarget::Rot13, times, window, cx) - } - Some(Operator::Rot47) => { - self.convert_object(object, around, ConvertTarget::Rot47, times, window, cx) - } - Some(Operator::AddSurrounds { target: None }) => { - waiting_operator = Some(Operator::AddSurrounds { - target: Some(SurroundsType::Object(object, around)), - }); - } - Some(Operator::ToggleComments) => { - self.toggle_comments_object(object, around, times, window, cx) - } - Some(Operator::ReplaceWithRegister) => { - self.replace_with_register_object(object, around, window, cx) - } - Some(Operator::Exchange) => self.exchange_object(object, around, window, cx), - Some(Operator::HelixMatch) => { - self.select_current_object(object, around, window, cx) - } - _ => { - // Can't do anything for namespace operators. Ignoring - } - }, - Some(Operator::HelixNext { around }) => { - self.select_next_object(object, around, window, cx); - } - Some(Operator::HelixPrevious { around }) => { - self.select_previous_object(object, around, window, cx); - } - Some(Operator::DeleteSurrounds) => { - waiting_operator = Some(Operator::DeleteSurrounds); - } - Some(Operator::ChangeSurrounds { target: None, .. }) => { - if self.check_and_move_to_valid_bracket_pair(object, window, cx) { - waiting_operator = Some(Operator::ChangeSurrounds { - target: Some(object), - opening, - }); - } - } - _ => { - // Can't do anything with change/delete/yank/surrounds and text objects. Ignoring - } - } - self.clear_operator(window, cx); - if let Some(operator) = waiting_operator { - self.push_operator(operator, window, cx); - } - } - - pub(crate) fn move_cursor( - &mut self, - motion: Motion, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |vim, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - - // If vim is in temporary mode and the motion being used is - // `EndOfLine` ($), we'll want to disable clipping at line ends so - // that the newline character can be selected so that, when moving - // back to visual mode, the cursor will be placed after the last - // character and not before it. - let clip_at_line_ends = editor.clip_at_line_ends(cx); - let should_disable_clip = matches!(motion, Motion::EndOfLine { .. }) && vim.temp_mode; - - if should_disable_clip { - editor.set_clip_at_line_ends(false, cx) - }; - - editor.change_selections( - SelectionEffects::default().nav_history(motion.push_to_jump_list()), - window, - cx, - |s| { - s.move_cursors_with(|map, cursor, goal| { - motion - .move_point(map, cursor, goal, times, &text_layout_details) - .unwrap_or((cursor, goal)) - }) - }, - ); - - if should_disable_clip { - editor.set_clip_at_line_ends(clip_at_line_ends, cx); - }; - }); - } - - fn insert_after(&mut self, _: &InsertAfter, window: &mut Window, cx: &mut Context) { - self.start_recording(cx); - self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None)); - }); - }); - } - - fn insert_before(&mut self, _: &InsertBefore, window: &mut Window, cx: &mut Context) { - self.start_recording(cx); - if self.mode.is_visual() { - let current_mode = self.mode; - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - if current_mode == Mode::VisualLine { - let start_of_line = motion::start_of_line(map, false, selection.start); - selection.collapse_to(start_of_line, SelectionGoal::None) - } else { - selection.collapse_to(selection.start, SelectionGoal::None) - } - }); - }); - }); - } - self.switch_mode(Mode::Insert, false, window, cx); - } - - fn insert_first_non_whitespace( - &mut self, - _: &InsertFirstNonWhitespace, - window: &mut Window, - cx: &mut Context, - ) { - self.start_recording(cx); - self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_cursors_with(|map, cursor, _| { - ( - first_non_whitespace(map, false, cursor), - SelectionGoal::None, - ) - }); - }); - }); - } - - fn insert_end_of_line( - &mut self, - _: &InsertEndOfLine, - window: &mut Window, - cx: &mut Context, - ) { - self.start_recording(cx); - self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_cursors_with(|map, cursor, _| { - (next_line_end(map, cursor, 1), SelectionGoal::None) - }); - }); - }); - } - - fn insert_at_previous( - &mut self, - _: &InsertAtPrevious, - window: &mut Window, - cx: &mut Context, - ) { - self.start_recording(cx); - self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(cx, |vim, editor, cx| { - if let Some(Mark::Local(marks)) = vim.get_mark("^", editor, window, cx) - && !marks.is_empty() - { - editor.change_selections(Default::default(), window, cx, |s| { - s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark)) - }); - } - }); - } - - fn insert_line_above( - &mut self, - _: &InsertLineAbove, - window: &mut Window, - cx: &mut Context, - ) { - self.start_recording(cx); - self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let selections = editor.selections.all::(&editor.display_snapshot(cx)); - let snapshot = editor.buffer().read(cx).snapshot(cx); - - let selection_start_rows: BTreeSet = selections - .into_iter() - .map(|selection| selection.start.row) - .collect(); - let edits = selection_start_rows - .into_iter() - .map(|row| { - let indent = snapshot - .indent_and_comment_for_line(MultiBufferRow(row), cx) - .chars() - .collect::(); - - let start_of_line = Point::new(row, 0); - (start_of_line..start_of_line, indent + "\n") - }) - .collect::>(); - editor.edit_with_autoindent(edits, cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_cursors_with(|map, cursor, _| { - let previous_line = map.start_of_relative_buffer_row(cursor, -1); - let insert_point = motion::end_of_line(map, false, previous_line, 1); - (insert_point, SelectionGoal::None) - }); - }); - }); - }); - } - - fn insert_line_below( - &mut self, - _: &InsertLineBelow, - window: &mut Window, - cx: &mut Context, - ) { - self.start_recording(cx); - self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - let selections = editor.selections.all::(&editor.display_snapshot(cx)); - let snapshot = editor.buffer().read(cx).snapshot(cx); - - let selection_end_rows: BTreeSet = selections - .into_iter() - .map(|selection| selection.end.row) - .collect(); - let edits = selection_end_rows - .into_iter() - .map(|row| { - let indent = snapshot - .indent_and_comment_for_line(MultiBufferRow(row), cx) - .chars() - .collect::(); - - let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row))); - (end_of_line..end_of_line, "\n".to_string() + &indent) - }) - .collect::>(); - editor.change_selections(Default::default(), window, cx, |s| { - s.maybe_move_cursors_with(|map, cursor, goal| { - Motion::CurrentLine.move_point( - map, - cursor, - goal, - None, - &text_layout_details, - ) - }); - }); - editor.edit_with_autoindent(edits, cx); - }); - }); - } - - fn insert_empty_line_above( - &mut self, - _: &InsertEmptyLineAbove, - window: &mut Window, - cx: &mut Context, - ) { - self.record_current_action(cx); - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, _, cx| { - let selections = editor.selections.all::(&editor.display_snapshot(cx)); - - let selection_start_rows: BTreeSet = selections - .into_iter() - .map(|selection| selection.start.row) - .collect(); - let edits = selection_start_rows - .into_iter() - .map(|row| { - let start_of_line = Point::new(row, 0); - (start_of_line..start_of_line, "\n".repeat(count)) - }) - .collect::>(); - editor.edit(edits, cx); - }); - }); - } - - fn insert_empty_line_below( - &mut self, - _: &InsertEmptyLineBelow, - window: &mut Window, - cx: &mut Context, - ) { - self.record_current_action(cx); - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let display_map = editor.display_snapshot(cx); - let selections = editor.selections.all::(&display_map); - let snapshot = editor.buffer().read(cx).snapshot(cx); - let display_selections = editor.selections.all_display(&display_map); - let original_positions = display_selections - .iter() - .map(|s| (s.id, s.head())) - .collect::>(); - - let selection_end_rows: BTreeSet = selections - .into_iter() - .map(|selection| selection.end.row) - .collect(); - let edits = selection_end_rows - .into_iter() - .map(|row| { - let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row))); - (end_of_line..end_of_line, "\n".repeat(count)) - }) - .collect::>(); - editor.edit(edits, cx); - - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|_, selection| { - if let Some(position) = original_positions.get(&selection.id) { - selection.collapse_to(*position, SelectionGoal::None); - } - }); - }); - }); - }); - } - - fn join_lines_impl( - &mut self, - insert_whitespace: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.record_current_action(cx); - let mut times = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - if self.mode.is_visual() { - times = 1; - } else if times > 1 { - // 2J joins two lines together (same as J or 1J) - times -= 1; - } - - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - for _ in 0..times { - editor.join_lines_impl(insert_whitespace, window, cx) - } - }) - }); - if self.mode.is_visual() { - self.switch_mode(Mode::Normal, true, window, cx) - } - } - - fn yank_line(&mut self, _: &YankLine, window: &mut Window, cx: &mut Context) { - let count = Vim::take_count(cx); - let forced_motion = Vim::take_forced_motion(cx); - self.yank_motion( - motion::Motion::CurrentLine, - count, - forced_motion, - window, - cx, - ) - } - - fn yank_to_end_of_line( - &mut self, - _: &YankToEndOfLine, - window: &mut Window, - cx: &mut Context, - ) { - let count = Vim::take_count(cx); - let forced_motion = Vim::take_forced_motion(cx); - self.yank_motion( - motion::Motion::EndOfLine { - display_lines: false, - }, - count, - forced_motion, - window, - cx, - ) - } - - fn show_location(&mut self, _: &ShowLocation, _: &mut Window, cx: &mut Context) { - let count = Vim::take_count(cx); - Vim::take_forced_motion(cx); - self.update_editor(cx, |vim, editor, cx| { - let selection = editor.selections.newest_anchor(); - let Some((buffer, point, _)) = editor - .buffer() - .read(cx) - .point_to_buffer_point(selection.head(), cx) - else { - return; - }; - let filename = if let Some(file) = buffer.read(cx).file() { - if count.is_some() { - if let Some(local) = file.as_local() { - local.abs_path(cx).to_string_lossy().into_owned() - } else { - file.full_path(cx).to_string_lossy().into_owned() - } - } else { - file.path().display(file.path_style(cx)).into_owned() - } - } else { - "[No Name]".into() - }; - let buffer = buffer.read(cx); - let lines = buffer.max_point().row + 1; - let current_line = point.row; - let percentage = current_line as f32 / lines as f32; - let modified = if buffer.is_dirty() { " [modified]" } else { "" }; - vim.status_label = Some( - format!( - "{}{} {} lines --{:.0}%--", - filename, - modified, - lines, - percentage * 100.0, - ) - .into(), - ); - cx.notify(); - }); - } - - fn toggle_comments(&mut self, _: &ToggleComments, window: &mut Window, cx: &mut Context) { - self.record_current_action(cx); - self.store_visual_marks(window, cx); - self.update_editor(cx, |vim, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let original_positions = vim.save_selection_starts(editor, cx); - editor.toggle_comments(&Default::default(), window, cx); - vim.restore_selection_cursors(editor, window, cx, original_positions); - }); - }); - if self.mode.is_visual() { - self.switch_mode(Mode::Normal, true, window, cx) - } - } - - pub(crate) fn normal_replace( - &mut self, - text: Arc, - window: &mut Window, - cx: &mut Context, - ) { - // We need to use `text.chars().count()` instead of `text.len()` here as - // `len()` counts bytes, not characters. - let char_count = text.chars().count(); - let count = Vim::take_count(cx).unwrap_or(char_count); - let is_return_char = text == "\n".into() || text == "\r".into(); - let repeat_count = match (is_return_char, char_count) { - (true, _) => 0, - (_, 1) => count, - (_, _) => 1, - }; - - Vim::take_forced_motion(cx); - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - let display_map = editor.display_snapshot(cx); - let display_selections = editor.selections.all_display(&display_map); - - let mut edits = Vec::with_capacity(display_selections.len()); - for selection in &display_selections { - let mut range = selection.range(); - for _ in 0..count { - let new_point = movement::saturating_right(&display_map, range.end); - if range.end == new_point { - return; - } - range.end = new_point; - } - - edits.push(( - range.start.to_offset(&display_map, Bias::Left) - ..range.end.to_offset(&display_map, Bias::Left), - text.repeat(repeat_count), - )); - } - - editor.edit(edits, cx); - if is_return_char { - editor.newline(&editor::actions::Newline, window, cx); - } - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let point = movement::saturating_left(map, selection.head()); - selection.collapse_to(point, SelectionGoal::None) - }); - }); - }); - }); - self.pop_operator(window, cx); - } - - pub fn save_selection_starts( - &self, - editor: &Editor, - cx: &mut Context, - ) -> HashMap { - let display_map = editor.display_snapshot(cx); - let selections = editor.selections.all_display(&display_map); - selections - .iter() - .map(|selection| { - ( - selection.id, - display_map.display_point_to_anchor(selection.start, Bias::Right), - ) - }) - .collect::>() - } - - pub fn restore_selection_cursors( - &self, - editor: &mut Editor, - window: &mut Window, - cx: &mut Context, - mut positions: HashMap, - ) { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - if let Some(anchor) = positions.remove(&selection.id) { - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); - } - }); - }); - } - - fn exit_temporary_normal(&mut self, window: &mut Window, cx: &mut Context) { - if self.temp_mode { - self.switch_mode(Mode::Insert, true, window, cx); - } - } -} - -#[cfg(test)] -mod test { - use gpui::{KeyBinding, TestAppContext, UpdateGlobal}; - use indoc::indoc; - use settings::SettingsStore; - - use crate::{ - motion, - state::Mode::{self}, - test::{NeovimBackedTestContext, VimTestContext}, - }; - - #[gpui::test] - async fn test_h(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "h", - indoc! {" - ˇThe qˇuick - ˇbrown" - }, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_backspace(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "backspace", - indoc! {" - ˇThe qˇuick - ˇbrown" - }, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_j(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - aaˇaa - 😃😃" - }) - .await; - cx.simulate_shared_keystrokes("j").await; - cx.shared_state().await.assert_eq(indoc! {" - aaaa - 😃ˇ😃" - }); - - cx.simulate_at_each_offset( - "j", - indoc! {" - ˇThe qˇuick broˇwn - ˇfox jumps" - }, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_enter(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "enter", - indoc! {" - ˇThe qˇuick broˇwn - ˇfox jumps" - }, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_k(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "k", - indoc! {" - ˇThe qˇuick - ˇbrown fˇox jumˇps" - }, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_l(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "l", - indoc! {" - ˇThe qˇuicˇk - ˇbrowˇn"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "$", - indoc! {" - ˇThe qˇuicˇk - ˇbrowˇn"}, - ) - .await - .assert_matches(); - cx.simulate_at_each_offset( - "0", - indoc! {" - ˇThe qˇuicˇk - ˇbrowˇn"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_jump_to_end(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.simulate_at_each_offset( - "shift-g", - indoc! {" - The ˇquick - - brown fox jumps - overˇ the lazy doˇg"}, - ) - .await - .assert_matches(); - cx.simulate( - "shift-g", - indoc! {" - The quiˇck - - brown"}, - ) - .await - .assert_matches(); - cx.simulate( - "shift-g", - indoc! {" - The quiˇck - - "}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_w(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "w", - indoc! {" - The ˇquickˇ-ˇbrown - ˇ - ˇ - ˇfox_jumps ˇover - ˇthˇe"}, - ) - .await - .assert_matches(); - cx.simulate_at_each_offset( - "shift-w", - indoc! {" - The ˇquickˇ-ˇbrown - ˇ - ˇ - ˇfox_jumps ˇover - ˇthˇe"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_end_of_word(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "e", - indoc! {" - Thˇe quicˇkˇ-browˇn - - - fox_jumpˇs oveˇr - thˇe"}, - ) - .await - .assert_matches(); - cx.simulate_at_each_offset( - "shift-e", - indoc! {" - Thˇe quicˇkˇ-browˇn - - - fox_jumpˇs oveˇr - thˇe"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_b(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "b", - indoc! {" - ˇThe ˇquickˇ-ˇbrown - ˇ - ˇ - ˇfox_jumps ˇover - ˇthe"}, - ) - .await - .assert_matches(); - cx.simulate_at_each_offset( - "shift-b", - indoc! {" - ˇThe ˇquickˇ-ˇbrown - ˇ - ˇ - ˇfox_jumps ˇover - ˇthe"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_gg(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "g g", - indoc! {" - The qˇuick - - brown fox jumps - over ˇthe laˇzy dog"}, - ) - .await - .assert_matches(); - cx.simulate( - "g g", - indoc! {" - - - brown fox jumps - over the laˇzy dog"}, - ) - .await - .assert_matches(); - cx.simulate( - "2 g g", - indoc! {" - ˇ - - brown fox jumps - over the lazydog"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_end_of_document(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "shift-g", - indoc! {" - The qˇuick - - brown fox jumps - over ˇthe laˇzy dog"}, - ) - .await - .assert_matches(); - cx.simulate( - "shift-g", - indoc! {" - - - brown fox jumps - over the laˇzy dog"}, - ) - .await - .assert_matches(); - cx.simulate( - "2 shift-g", - indoc! {" - ˇ - - brown fox jumps - over the lazydog"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_a(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset("a", "The qˇuicˇk") - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "shift-a", - indoc! {" - ˇ - The qˇuick - brown ˇfox "}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("^", "The qˇuick").await.assert_matches(); - cx.simulate("^", " The qˇuick").await.assert_matches(); - cx.simulate("^", "ˇ").await.assert_matches(); - cx.simulate( - "^", - indoc! {" - The qˇuick - brown fox"}, - ) - .await - .assert_matches(); - cx.simulate( - "^", - indoc! {" - ˇ - The quick"}, - ) - .await - .assert_matches(); - // Indoc disallows trailing whitespace. - cx.simulate("^", " ˇ \nThe quick").await.assert_matches(); - } - - #[gpui::test] - async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("shift-i", "The qˇuick").await.assert_matches(); - cx.simulate("shift-i", " The qˇuick").await.assert_matches(); - cx.simulate("shift-i", "ˇ").await.assert_matches(); - cx.simulate( - "shift-i", - indoc! {" - The qˇuick - brown fox"}, - ) - .await - .assert_matches(); - cx.simulate( - "shift-i", - indoc! {" - ˇ - The quick"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "shift-d", - indoc! {" - The qˇuick - brown fox"}, - ) - .await - .assert_matches(); - cx.simulate( - "shift-d", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_x(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset("x", "ˇTeˇsˇt") - .await - .assert_matches(); - cx.simulate( - "x", - indoc! {" - Tesˇt - test"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_left(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt") - .await - .assert_matches(); - cx.simulate( - "shift-x", - indoc! {" - Test - ˇtest"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_o(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("o", "ˇ").await.assert_matches(); - cx.simulate("o", "The ˇquick").await.assert_matches(); - cx.simulate_at_each_offset( - "o", - indoc! {" - The qˇuick - brown ˇfox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate( - "o", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - - cx.assert_binding( - "o", - indoc! {" - fn test() { - println!(ˇ); - }"}, - Mode::Normal, - indoc! {" - fn test() { - println!(); - ˇ - }"}, - Mode::Insert, - ); - - cx.assert_binding( - "o", - indoc! {" - fn test(ˇ) { - println!(); - }"}, - Mode::Normal, - indoc! {" - fn test() { - ˇ - println!(); - }"}, - Mode::Insert, - ); - } - - #[gpui::test] - async fn test_insert_line_above(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("shift-o", "ˇ").await.assert_matches(); - cx.simulate("shift-o", "The ˇquick").await.assert_matches(); - cx.simulate_at_each_offset( - "shift-o", - indoc! {" - The qˇuick - brown ˇfox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate( - "shift-o", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - - // Our indentation is smarter than vims. So we don't match here - cx.assert_binding( - "shift-o", - indoc! {" - fn test() { - println!(ˇ); - }"}, - Mode::Normal, - indoc! {" - fn test() { - ˇ - println!(); - }"}, - Mode::Insert, - ); - cx.assert_binding( - "shift-o", - indoc! {" - fn test(ˇ) { - println!(); - }"}, - Mode::Normal, - indoc! {" - ˇ - fn test() { - println!(); - }"}, - Mode::Insert, - ); - } - - #[gpui::test] - async fn test_insert_empty_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("[ space", "ˇ").await.assert_matches(); - cx.simulate("[ space", "The ˇquick").await.assert_matches(); - cx.simulate_at_each_offset( - "3 [ space", - indoc! {" - The qˇuick - brown ˇfox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate_at_each_offset( - "[ space", - indoc! {" - The qˇuick - brown ˇfox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate( - "[ space", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - - cx.simulate("] space", "ˇ").await.assert_matches(); - cx.simulate("] space", "The ˇquick").await.assert_matches(); - cx.simulate_at_each_offset( - "3 ] space", - indoc! {" - The qˇuick - brown ˇfox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate_at_each_offset( - "] space", - indoc! {" - The qˇuick - brown ˇfox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate( - "] space", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_dd(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("d d", "ˇ").await.assert_matches(); - cx.simulate("d d", "The ˇquick").await.assert_matches(); - cx.simulate_at_each_offset( - "d d", - indoc! {" - The qˇuick - brown ˇfox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate( - "d d", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_cc(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("c c", "ˇ").await.assert_matches(); - cx.simulate("c c", "The ˇquick").await.assert_matches(); - cx.simulate_at_each_offset( - "c c", - indoc! {" - The quˇick - brown ˇfox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate( - "c c", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_repeated_word(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - for count in 1..=5 { - cx.simulate_at_each_offset( - &format!("{count} w"), - indoc! {" - ˇThe quˇickˇ browˇn - ˇ - ˇfox ˇjumpsˇ-ˇoˇver - ˇthe lazy dog - "}, - ) - .await - .assert_matches(); - } - } - - #[gpui::test] - async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest") - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_f_and_t(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - for count in 1..=3 { - let test_case = indoc! {" - ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa - ˇ ˇbˇaaˇa ˇbˇbˇb - ˇ - ˇb - "}; - - cx.simulate_at_each_offset(&format!("{count} f b"), test_case) - .await - .assert_matches(); - - cx.simulate_at_each_offset(&format!("{count} t b"), test_case) - .await - .assert_matches(); - } - } - - #[gpui::test] - async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - let test_case = indoc! {" - ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa - ˇ ˇbˇaaˇa ˇbˇbˇb - ˇ••• - ˇb - " - }; - - for count in 1..=3 { - cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case) - .await - .assert_matches(); - - cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case) - .await - .assert_matches(); - } - } - - #[gpui::test] - async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - s.vim.get_or_insert_default().use_smartcase_find = Some(true); - }); - }); - - cx.assert_binding( - "f p", - indoc! {"ˇfmt.Println(\"Hello, World!\")"}, - Mode::Normal, - indoc! {"fmt.ˇPrintln(\"Hello, World!\")"}, - Mode::Normal, - ); - - cx.assert_binding( - "shift-f p", - indoc! {"fmt.Printlnˇ(\"Hello, World!\")"}, - Mode::Normal, - indoc! {"fmt.ˇPrintln(\"Hello, World!\")"}, - Mode::Normal, - ); - - cx.assert_binding( - "t p", - indoc! {"ˇfmt.Println(\"Hello, World!\")"}, - Mode::Normal, - indoc! {"fmtˇ.Println(\"Hello, World!\")"}, - Mode::Normal, - ); - - cx.assert_binding( - "shift-t p", - indoc! {"fmt.Printlnˇ(\"Hello, World!\")"}, - Mode::Normal, - indoc! {"fmt.Pˇrintln(\"Hello, World!\")"}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_percent(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;") - .await - .assert_matches(); - cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;") - .await - .assert_matches(); - cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;") - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // goes to current line end - cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await; - cx.simulate_shared_keystrokes("$").await; - cx.shared_state().await.assert_eq("aˇa\nbb\ncc"); - - // goes to next line end - cx.simulate_shared_keystrokes("2 $").await; - cx.shared_state().await.assert_eq("aa\nbˇb\ncc"); - - // try to exceed the final line. - cx.simulate_shared_keystrokes("4 $").await; - cx.shared_state().await.assert_eq("aa\nbb\ncˇc"); - } - - #[gpui::test] - async fn test_subword_motions(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.update(|_, cx| { - cx.bind_keys(vec![ - KeyBinding::new( - "w", - motion::NextSubwordStart { - ignore_punctuation: false, - }, - Some("Editor && VimControl && !VimWaiting && !menu"), - ), - KeyBinding::new( - "b", - motion::PreviousSubwordStart { - ignore_punctuation: false, - }, - Some("Editor && VimControl && !VimWaiting && !menu"), - ), - KeyBinding::new( - "e", - motion::NextSubwordEnd { - ignore_punctuation: false, - }, - Some("Editor && VimControl && !VimWaiting && !menu"), - ), - KeyBinding::new( - "g e", - motion::PreviousSubwordEnd { - ignore_punctuation: false, - }, - Some("Editor && VimControl && !VimWaiting && !menu"), - ), - ]); - }); - - cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"}); - // Special case: In 'cw', 'w' acts like 'e' - cx.assert_binding( - "c w", - indoc! {"ˇassert_binding"}, - Mode::Normal, - indoc! {"ˇ_binding"}, - Mode::Insert, - ); - - cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"}); - - cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"}); - - cx.assert_binding_normal( - "g e", - indoc! {"assert_bindinˇg"}, - indoc! {"asserˇt_binding"}, - ); - } - - #[gpui::test] - async fn test_r(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("r -").await; - cx.shared_state().await.assert_eq("ˇ-ello\n"); - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("3 r -").await; - cx.shared_state().await.assert_eq("--ˇ-lo\n"); - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("r - 2 l .").await; - cx.shared_state().await.assert_eq("-eˇ-lo\n"); - - cx.set_shared_state("ˇhello world\n").await; - cx.simulate_shared_keystrokes("2 r - f w .").await; - cx.shared_state().await.assert_eq("--llo -ˇ-rld\n"); - - cx.set_shared_state("ˇhello world\n").await; - cx.simulate_shared_keystrokes("2 0 r - ").await; - cx.shared_state().await.assert_eq("ˇhello world\n"); - - cx.set_shared_state(" helloˇ world\n").await; - cx.simulate_shared_keystrokes("r enter").await; - cx.shared_state().await.assert_eq(" hello\n ˇ world\n"); - - cx.set_shared_state(" helloˇ world\n").await; - cx.simulate_shared_keystrokes("2 r enter").await; - cx.shared_state().await.assert_eq(" hello\n ˇ orld\n"); - } - - #[gpui::test] - async fn test_gq(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_neovim_option("textwidth=5").await; - - cx.update(|_, cx| { - SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings(cx, |settings| { - settings - .project - .all_languages - .defaults - .preferred_line_length = Some(5); - }); - }) - }); - - cx.set_shared_state("ˇth th th th th th\n").await; - cx.simulate_shared_keystrokes("g q q").await; - cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n"); - - cx.set_shared_state("ˇth th th th th th\nth th th th th th\n") - .await; - cx.simulate_shared_keystrokes("v j g q").await; - cx.shared_state() - .await - .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n"); - } - - #[gpui::test] - async fn test_o_comment(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_neovim_option("filetype=rust").await; - - cx.set_shared_state("// helloˇ\n").await; - cx.simulate_shared_keystrokes("o").await; - cx.shared_state().await.assert_eq("// hello\n// ˇ\n"); - cx.simulate_shared_keystrokes("x escape shift-o").await; - cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n"); - } - - #[gpui::test] - async fn test_yank_line_with_trailing_newline(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("heˇllo\n").await; - cx.simulate_shared_keystrokes("y y p").await; - cx.shared_state().await.assert_eq("hello\nˇhello\n"); - } - - #[gpui::test] - async fn test_yank_line_without_trailing_newline(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("heˇllo").await; - cx.simulate_shared_keystrokes("y y p").await; - cx.shared_state().await.assert_eq("hello\nˇhello"); - } - - #[gpui::test] - async fn test_yank_multiline_without_trailing_newline(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("heˇllo\nhello").await; - cx.simulate_shared_keystrokes("2 y y p").await; - cx.shared_state() - .await - .assert_eq("hello\nˇhello\nhello\nhello"); - } - - #[gpui::test] - async fn test_dd_then_paste_without_trailing_newline(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("heˇllo").await; - cx.simulate_shared_keystrokes("d d").await; - cx.shared_state().await.assert_eq("ˇ"); - cx.simulate_shared_keystrokes("p p").await; - cx.shared_state().await.assert_eq("\nhello\nˇhello"); - } - - #[gpui::test] - async fn test_visual_mode_insert_before_after(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("heˇllo").await; - cx.simulate_shared_keystrokes("v i w shift-i").await; - cx.shared_state().await.assert_eq("ˇhello"); - - cx.set_shared_state(indoc! {" - The quick brown - fox ˇjumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("shift-v shift-i").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - ˇfox jumps over - the lazy dog"}); - - cx.set_shared_state(indoc! {" - The quick brown - fox ˇjumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("shift-v shift-a").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jˇumps over - the lazy dog"}); - } - - #[gpui::test] - async fn test_jump_list(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇfn a() { } - - - - - - fn b() { } - - - - - - fn b() { }"}) - .await; - cx.simulate_shared_keystrokes("3 }").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("ctrl-o").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("ctrl-i").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("1 1 k").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("ctrl-o").await; - cx.shared_state().await.assert_matches(); - } - - #[gpui::test] - async fn test_undo_last_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇfn a() { } - fn a() { } - fn a() { } - "}) - .await; - // do a jump to reset vim's undo grouping - cx.simulate_shared_keystrokes("shift-g").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("r a").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("shift-u").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("shift-u").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("g g shift-u").await; - cx.shared_state().await.assert_matches(); - } - - #[gpui::test] - async fn test_undo_last_line_newline(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇfn a() { } - fn a() { } - fn a() { } - "}) - .await; - // do a jump to reset vim's undo grouping - cx.simulate_shared_keystrokes("shift-g k").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("o h e l l o escape").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("shift-u").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("shift-u").await; - } - - #[gpui::test] - async fn test_undo_last_line_newline_many_changes(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇfn a() { } - fn a() { } - fn a() { } - "}) - .await; - // do a jump to reset vim's undo grouping - cx.simulate_shared_keystrokes("x shift-g k").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("x f a x f { x").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("shift-u").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("shift-u").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("shift-u").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("shift-u").await; - cx.shared_state().await.assert_matches(); - } - - #[gpui::test] - async fn test_undo_last_line_multicursor(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - ˇone two ˇone - two ˇone two - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("3 r a"); - cx.assert_state( - indoc! {" - aaˇa two aaˇa - two aaˇa two - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("escape escape"); - cx.simulate_keystrokes("shift-u"); - cx.set_state( - indoc! {" - onˇe two onˇe - two onˇe two - "}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_go_to_tab_with_count(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Open 4 tabs. - cx.simulate_keystrokes(": tabnew"); - cx.simulate_keystrokes("enter"); - cx.simulate_keystrokes(": tabnew"); - cx.simulate_keystrokes("enter"); - cx.simulate_keystrokes(": tabnew"); - cx.simulate_keystrokes("enter"); - cx.workspace(|workspace, _, cx| { - assert_eq!(workspace.items(cx).count(), 4); - assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3); - }); - - cx.simulate_keystrokes("1 g t"); - cx.workspace(|workspace, _, cx| { - assert_eq!(workspace.active_pane().read(cx).active_item_index(), 0); - }); - - cx.simulate_keystrokes("3 g t"); - cx.workspace(|workspace, _, cx| { - assert_eq!(workspace.active_pane().read(cx).active_item_index(), 2); - }); - - cx.simulate_keystrokes("4 g t"); - cx.workspace(|workspace, _, cx| { - assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3); - }); - - cx.simulate_keystrokes("1 g t"); - cx.simulate_keystrokes("g t"); - cx.workspace(|workspace, _, cx| { - assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1); - }); - } - - #[gpui::test] - async fn test_go_to_previous_tab_with_count(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Open 4 tabs. - cx.simulate_keystrokes(": tabnew"); - cx.simulate_keystrokes("enter"); - cx.simulate_keystrokes(": tabnew"); - cx.simulate_keystrokes("enter"); - cx.simulate_keystrokes(": tabnew"); - cx.simulate_keystrokes("enter"); - cx.workspace(|workspace, _, cx| { - assert_eq!(workspace.items(cx).count(), 4); - assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3); - }); - - cx.simulate_keystrokes("2 g shift-t"); - cx.workspace(|workspace, _, cx| { - assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1); - }); - - cx.simulate_keystrokes("g shift-t"); - cx.workspace(|workspace, _, cx| { - assert_eq!(workspace.active_pane().read(cx).active_item_index(), 0); - }); - - // Wraparound: gT from first tab should go to last. - cx.simulate_keystrokes("g shift-t"); - cx.workspace(|workspace, _, cx| { - assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3); - }); - - cx.simulate_keystrokes("6 g shift-t"); - cx.workspace(|workspace, _, cx| { - assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1); - }); - } - - #[gpui::test] - async fn test_temporary_mode(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // Test jumping to the end of the line ($). - cx.set_shared_state(indoc! {"lorem ˇipsum"}).await; - cx.simulate_shared_keystrokes("i").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("ctrl-o $").await; - cx.shared_state().await.assert_eq(indoc! {"lorem ipsumˇ"}); - - // Test jumping to the next word. - cx.set_shared_state(indoc! {"loremˇ ipsum dolor"}).await; - cx.simulate_shared_keystrokes("a").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("a n d space ctrl-o w").await; - cx.shared_state() - .await - .assert_eq(indoc! {"lorem and ipsum ˇdolor"}); - - // Test yanking to end of line ($). - cx.set_shared_state(indoc! {"lorem ˇipsum dolor"}).await; - cx.simulate_shared_keystrokes("i").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("a n d space ctrl-o y $") - .await; - cx.shared_state() - .await - .assert_eq(indoc! {"lorem and ˇipsum dolor"}); - } -} diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs deleted file mode 100644 index b0b0bddae1..0000000000 --- a/crates/vim/src/normal/change.rs +++ /dev/null @@ -1,706 +0,0 @@ -use crate::{ - Vim, - motion::{self, Motion, MotionKind}, - object::Object, - state::Mode, -}; -use editor::{ - Bias, DisplayPoint, - display_map::{DisplaySnapshot, ToDisplayPoint}, - movement::TextLayoutDetails, -}; -use gpui::{Context, Window}; -use language::Selection; - -impl Vim { - pub fn change_motion( - &mut self, - motion: Motion, - times: Option, - forced_motion: bool, - window: &mut Window, - cx: &mut Context, - ) { - // Some motions ignore failure when switching to normal mode - let mut motion_kind = if matches!( - motion, - Motion::Left - | Motion::Right - | Motion::EndOfLine { .. } - | Motion::WrappingLeft - | Motion::StartOfLine { .. } - ) { - Some(MotionKind::Exclusive) - } else { - None - }; - self.update_editor(cx, |vim, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - // We are swapping to insert mode anyway. Just set the line end clipping behavior now - editor.set_clip_at_line_ends(false, cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let kind = match motion { - Motion::NextWordStart { ignore_punctuation } - | Motion::NextSubwordStart { ignore_punctuation } => { - expand_changed_word_selection( - map, - selection, - times, - ignore_punctuation, - &text_layout_details, - motion == Motion::NextSubwordStart { ignore_punctuation }, - !matches!(motion, Motion::NextWordStart { .. }), - ) - } - _ => { - let kind = motion.expand_selection( - map, - selection, - times, - &text_layout_details, - forced_motion, - ); - if matches!( - motion, - Motion::CurrentLine | Motion::Down { .. } | Motion::Up { .. } - ) { - let mut start_offset = - selection.start.to_offset(map, Bias::Left); - let classifier = map - .buffer_snapshot() - .char_classifier_at(selection.start.to_point(map)); - for (ch, offset) in map.buffer_chars_at(start_offset) { - if ch == '\n' || !classifier.is_whitespace(ch) { - break; - } - start_offset = offset + ch.len_utf8(); - } - selection.start = start_offset.to_display_point(map); - } - kind - } - }; - if let Some(kind) = kind { - motion_kind.get_or_insert(kind); - } - }); - }); - if let Some(kind) = motion_kind { - vim.copy_selections_content(editor, kind, window, cx); - editor.insert("", window, cx); - editor.refresh_edit_prediction(true, false, window, cx); - } - }); - }); - - if motion_kind.is_some() { - self.switch_mode(Mode::Insert, false, window, cx) - } else { - self.switch_mode(Mode::Normal, false, window, cx) - } - } - - pub fn change_object( - &mut self, - object: Object, - around: bool, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - let mut objects_found = false; - self.update_editor(cx, |vim, editor, cx| { - // We are swapping to insert mode anyway. Just set the line end clipping behavior now - editor.set_clip_at_line_ends(false, cx); - editor.transact(window, cx, |editor, window, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - objects_found |= object.expand_selection(map, selection, around, times); - }); - }); - if objects_found { - let kind = match object.target_visual_mode(vim.mode, around) { - Mode::VisualLine => MotionKind::Linewise, - _ => MotionKind::Exclusive, - }; - vim.copy_selections_content(editor, kind, window, cx); - editor.insert("", window, cx); - editor.refresh_edit_prediction(true, false, window, cx); - } - }); - }); - - if objects_found { - self.switch_mode(Mode::Insert, false, window, cx); - } else { - self.switch_mode(Mode::Normal, false, window, cx); - } - } -} - -// From the docs https://vimdoc.sourceforge.net/htmldoc/motion.html -// Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is -// on a non-blank. This is because "cw" is interpreted as change-word, and a -// word does not include the following white space. {Vi: "cw" when on a blank -// followed by other blanks changes only the first blank; this is probably a -// bug, because "dw" deletes all the blanks} -fn expand_changed_word_selection( - map: &DisplaySnapshot, - selection: &mut Selection, - times: Option, - ignore_punctuation: bool, - text_layout_details: &TextLayoutDetails, - use_subword: bool, - always_advance: bool, -) -> Option { - let is_in_word = || { - let classifier = map - .buffer_snapshot() - .char_classifier_at(selection.start.to_point(map)); - - map.buffer_chars_at(selection.head().to_offset(map, Bias::Left)) - .next() - .map(|(c, _)| !classifier.is_whitespace(c)) - .unwrap_or_default() - }; - if (times.is_none() || times.unwrap() == 1) && is_in_word() { - let next_char = map - .buffer_chars_at( - motion::next_char(map, selection.end, false).to_offset(map, Bias::Left), - ) - .next(); - match next_char { - Some((' ', _)) => selection.end = motion::next_char(map, selection.end, false), - _ => { - if use_subword { - selection.end = - motion::next_subword_end(map, selection.end, ignore_punctuation, 1, false); - } else { - selection.end = motion::next_word_end( - map, - selection.end, - ignore_punctuation, - 1, - false, - always_advance, - ); - } - selection.end = motion::next_char(map, selection.end, false); - } - } - Some(MotionKind::Inclusive) - } else { - let motion = if use_subword { - Motion::NextSubwordStart { ignore_punctuation } - } else { - Motion::NextWordStart { ignore_punctuation } - }; - motion.expand_selection(map, selection, times, text_layout_details, false) - } -} - -#[cfg(test)] -mod test { - use indoc::indoc; - - use crate::test::NeovimBackedTestContext; - - #[gpui::test] - async fn test_change_h(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("c h", "Teˇst").await.assert_matches(); - cx.simulate("c h", "Tˇest").await.assert_matches(); - cx.simulate("c h", "ˇTest").await.assert_matches(); - cx.simulate( - "c h", - indoc! {" - Test - ˇtest"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_change_backspace(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("c backspace", "Teˇst").await.assert_matches(); - cx.simulate("c backspace", "Tˇest").await.assert_matches(); - cx.simulate("c backspace", "ˇTest").await.assert_matches(); - cx.simulate( - "c backspace", - indoc! {" - Test - ˇtest"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_change_l(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("c l", "Teˇst").await.assert_matches(); - cx.simulate("c l", "Tesˇt").await.assert_matches(); - } - - #[gpui::test] - async fn test_change_w(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("c w", "Teˇst").await.assert_matches(); - cx.simulate("c w", "Tˇest test").await.assert_matches(); - cx.simulate("c w", "Testˇ test").await.assert_matches(); - cx.simulate("c w", "Tesˇt test").await.assert_matches(); - cx.simulate( - "c w", - indoc! {" - Test teˇst - test"}, - ) - .await - .assert_matches(); - cx.simulate( - "c w", - indoc! {" - Test tesˇt - test"}, - ) - .await - .assert_matches(); - cx.simulate( - "c w", - indoc! {" - Test test - ˇ - test"}, - ) - .await - .assert_matches(); - - cx.simulate("c shift-w", "Test teˇst-test test") - .await - .assert_matches(); - - // on last character of word, `cw` doesn't eat subsequent punctuation - // see https://github.com/zed-industries/zed/issues/35269 - cx.simulate("c w", "tesˇt-test").await.assert_matches(); - } - - #[gpui::test] - async fn test_change_e(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("c e", "Teˇst Test").await.assert_matches(); - cx.simulate("c e", "Tˇest test").await.assert_matches(); - cx.simulate( - "c e", - indoc! {" - Test teˇst - test"}, - ) - .await - .assert_matches(); - cx.simulate( - "c e", - indoc! {" - Test tesˇt - test"}, - ) - .await - .assert_matches(); - cx.simulate( - "c e", - indoc! {" - Test test - ˇ - test"}, - ) - .await - .assert_matches(); - - cx.simulate("c shift-e", "Test teˇst-test test") - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_change_b(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("c b", "Teˇst Test").await.assert_matches(); - cx.simulate("c b", "Test ˇtest").await.assert_matches(); - cx.simulate("c b", "Test1 test2 ˇtest3") - .await - .assert_matches(); - cx.simulate( - "c b", - indoc! {" - Test test - ˇtest"}, - ) - .await - .assert_matches(); - cx.simulate( - "c b", - indoc! {" - Test test - ˇ - test"}, - ) - .await - .assert_matches(); - - cx.simulate("c shift-b", "Test test-test ˇtest") - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "c $", - indoc! {" - The qˇuick - brown fox"}, - ) - .await - .assert_matches(); - cx.simulate( - "c $", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_change_0(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.simulate( - "c 0", - indoc! {" - The qˇuick - brown fox"}, - ) - .await - .assert_matches(); - cx.simulate( - "c 0", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_change_k(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.simulate( - "c k", - indoc! {" - The quick - brown ˇfox - jumps over"}, - ) - .await - .assert_matches(); - cx.simulate( - "c k", - indoc! {" - The quick - brown fox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate( - "c k", - indoc! {" - The qˇuick - brown fox - jumps over"}, - ) - .await - .assert_matches(); - cx.simulate( - "c k", - indoc! {" - ˇ - brown fox - jumps over"}, - ) - .await - .assert_matches(); - cx.simulate( - "c k", - indoc! {" - The quick - brown fox - ˇjumps over"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_change_j(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "c j", - indoc! {" - The quick - brown ˇfox - jumps over"}, - ) - .await - .assert_matches(); - cx.simulate( - "c j", - indoc! {" - The quick - brown fox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate( - "c j", - indoc! {" - The qˇuick - brown fox - jumps over"}, - ) - .await - .assert_matches(); - cx.simulate( - "c j", - indoc! {" - The quick - brown fox - ˇ"}, - ) - .await - .assert_matches(); - cx.simulate( - "c j", - indoc! {" - The quick - ˇbrown fox - jumps over"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "c shift-g", - indoc! {" - The quick - brownˇ fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "c shift-g", - indoc! {" - The quick - brownˇ fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "c shift-g", - indoc! {" - The quick - brown fox - jumps over - the lˇazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "c shift-g", - indoc! {" - The quick - brown fox - jumps over - ˇ"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_change_cc(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "c c", - indoc! {" - The quick - brownˇ fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - - cx.simulate( - "c c", - indoc! {" - ˇThe quick - brown fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - - cx.simulate( - "c c", - indoc! {" - The quick - broˇwn fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_change_gg(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "c g g", - indoc! {" - The quick - brownˇ fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "c g g", - indoc! {" - The quick - brown fox - jumps over - the lˇazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "c g g", - indoc! {" - The qˇuick - brown fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "c g g", - indoc! {" - ˇ - brown fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_repeated_cj(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - for count in 1..=5 { - cx.simulate_at_each_offset( - &format!("c {count} j"), - indoc! {" - ˇThe quˇickˇ browˇn - ˇ - ˇfox ˇjumpsˇ-ˇoˇver - ˇthe lazy dog - "}, - ) - .await - .assert_matches(); - } - } - - #[gpui::test] - async fn test_repeated_cl(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - for count in 1..=5 { - cx.simulate_at_each_offset( - &format!("c {count} l"), - indoc! {" - ˇThe quˇickˇ browˇn - ˇ - ˇfox ˇjumpsˇ-ˇoˇver - ˇthe lazy dog - "}, - ) - .await - .assert_matches(); - } - } - - #[gpui::test] - async fn test_repeated_cb(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - for count in 1..=5 { - cx.simulate_at_each_offset( - &format!("c {count} b"), - indoc! {" - ˇThe quˇickˇ browˇn - ˇ - ˇfox ˇjumpsˇ-ˇoˇver - ˇthe lazy dog - "}, - ) - .await - .assert_matches() - } - } - - #[gpui::test] - async fn test_repeated_ce(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - for count in 1..=5 { - cx.simulate_at_each_offset( - &format!("c {count} e"), - indoc! {" - ˇThe quˇickˇ browˇn - ˇ - ˇfox ˇjumpsˇ-ˇoˇver - ˇthe lazy dog - "}, - ) - .await - .assert_matches(); - } - } -} diff --git a/crates/vim/src/normal/convert.rs b/crates/vim/src/normal/convert.rs deleted file mode 100644 index 0ee132a44d..0000000000 --- a/crates/vim/src/normal/convert.rs +++ /dev/null @@ -1,469 +0,0 @@ -use collections::HashMap; -use editor::{SelectionEffects, display_map::ToDisplayPoint}; -use gpui::{Context, Window}; -use language::{Bias, Point, SelectionGoal}; -use multi_buffer::MultiBufferRow; - -use crate::{ - Vim, - motion::Motion, - normal::{ChangeCase, ConvertToLowerCase, ConvertToRot13, ConvertToRot47, ConvertToUpperCase}, - object::Object, - state::Mode, -}; - -pub enum ConvertTarget { - LowerCase, - UpperCase, - OppositeCase, - Rot13, - Rot47, -} - -impl Vim { - pub fn convert_motion( - &mut self, - motion: Motion, - times: Option, - forced_motion: bool, - mode: ConvertTarget, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.set_clip_at_line_ends(false, cx); - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - let mut selection_starts: HashMap<_, _> = Default::default(); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = map.display_point_to_anchor(selection.head(), Bias::Left); - selection_starts.insert(selection.id, anchor); - motion.expand_selection( - map, - selection, - times, - &text_layout_details, - forced_motion, - ); - }); - }); - match mode { - ConvertTarget::LowerCase => { - editor.convert_to_lower_case(&Default::default(), window, cx) - } - ConvertTarget::UpperCase => { - editor.convert_to_upper_case(&Default::default(), window, cx) - } - ConvertTarget::OppositeCase => { - editor.convert_to_opposite_case(&Default::default(), window, cx) - } - ConvertTarget::Rot13 => { - editor.convert_to_rot13(&Default::default(), window, cx) - } - ConvertTarget::Rot47 => { - editor.convert_to_rot47(&Default::default(), window, cx) - } - } - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = selection_starts.remove(&selection.id).unwrap(); - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); - }); - }); - }); - editor.set_clip_at_line_ends(true, cx); - }); - } - - pub fn convert_object( - &mut self, - object: Object, - around: bool, - mode: ConvertTarget, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - let mut original_positions: HashMap<_, _> = Default::default(); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - object.expand_selection(map, selection, around, times); - original_positions.insert( - selection.id, - map.display_point_to_anchor(selection.start, Bias::Left), - ); - }); - }); - match mode { - ConvertTarget::LowerCase => { - editor.convert_to_lower_case(&Default::default(), window, cx) - } - ConvertTarget::UpperCase => { - editor.convert_to_upper_case(&Default::default(), window, cx) - } - ConvertTarget::OppositeCase => { - editor.convert_to_opposite_case(&Default::default(), window, cx) - } - ConvertTarget::Rot13 => { - editor.convert_to_rot13(&Default::default(), window, cx) - } - ConvertTarget::Rot47 => { - editor.convert_to_rot47(&Default::default(), window, cx) - } - } - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = original_positions.remove(&selection.id).unwrap(); - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); - }); - }); - editor.set_clip_at_line_ends(true, cx); - }); - }); - } - - pub fn change_case(&mut self, _: &ChangeCase, window: &mut Window, cx: &mut Context) { - self.manipulate_text(window, cx, |c| { - if c.is_lowercase() { - c.to_uppercase().collect::>() - } else { - c.to_lowercase().collect::>() - } - }) - } - - pub fn convert_to_upper_case( - &mut self, - _: &ConvertToUpperCase, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |c| c.to_uppercase().collect::>()) - } - - pub fn convert_to_lower_case( - &mut self, - _: &ConvertToLowerCase, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |c| c.to_lowercase().collect::>()) - } - - pub fn convert_to_rot13( - &mut self, - _: &ConvertToRot13, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |c| { - vec![match c { - 'A'..='M' | 'a'..='m' => ((c as u8) + 13) as char, - 'N'..='Z' | 'n'..='z' => ((c as u8) - 13) as char, - _ => c, - }] - }) - } - - pub fn convert_to_rot47( - &mut self, - _: &ConvertToRot47, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |c| { - let code_point = c as u32; - if code_point >= 33 && code_point <= 126 { - return vec![char::from_u32(33 + ((code_point + 14) % 94)).unwrap()]; - } - vec![c] - }) - } - - fn manipulate_text(&mut self, window: &mut Window, cx: &mut Context, transform: F) - where - F: Fn(char) -> Vec + Copy, - { - self.record_current_action(cx); - self.store_visual_marks(window, cx); - let count = Vim::take_count(cx).unwrap_or(1) as u32; - Vim::take_forced_motion(cx); - - self.update_editor(cx, |vim, editor, cx| { - let mut ranges = Vec::new(); - let mut cursor_positions = Vec::new(); - let snapshot = editor.buffer().read(cx).snapshot(cx); - for selection in editor.selections.all_adjusted(&editor.display_snapshot(cx)) { - match vim.mode { - Mode::Visual | Mode::VisualLine => { - ranges.push(selection.start..selection.end); - cursor_positions.push(selection.start..selection.start); - } - Mode::VisualBlock => { - ranges.push(selection.start..selection.end); - if cursor_positions.is_empty() { - cursor_positions.push(selection.start..selection.start); - } - } - - Mode::HelixNormal | Mode::HelixSelect => { - if selection.is_empty() { - // Handle empty selection by operating on single character - let start = selection.start; - let end = snapshot.clip_point(start + Point::new(0, 1), Bias::Right); - ranges.push(start..end); - cursor_positions.push(selection.start..selection.start); - } else { - ranges.push(selection.start..selection.end); - cursor_positions.push(selection.start..selection.end); - } - } - Mode::Insert | Mode::Normal | Mode::Replace => { - let start = selection.start; - let mut end = start; - for _ in 0..count { - end = snapshot.clip_point(end + Point::new(0, 1), Bias::Right); - } - ranges.push(start..end); - - if end.column == snapshot.line_len(MultiBufferRow(end.row)) - && end.column > 0 - { - end = snapshot.clip_point(end - Point::new(0, 1), Bias::Left); - } - cursor_positions.push(end..end) - } - } - } - editor.transact(window, cx, |editor, window, cx| { - for range in ranges.into_iter().rev() { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let text = snapshot - .text_for_range(range.start..range.end) - .flat_map(|s| s.chars()) - .flat_map(transform) - .collect::(); - editor.edit([(range, text)], cx) - } - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges(cursor_positions) - }) - }); - }); - if self.mode != Mode::HelixNormal { - self.switch_mode(Mode::Normal, true, window, cx) - } - } -} - -#[cfg(test)] -mod test { - use crate::test::VimTestContext; - - use crate::{state::Mode, test::NeovimBackedTestContext}; - - #[gpui::test] - async fn test_change_case(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("ˇabC\n").await; - cx.simulate_shared_keystrokes("~").await; - cx.shared_state().await.assert_eq("AˇbC\n"); - cx.simulate_shared_keystrokes("2 ~").await; - cx.shared_state().await.assert_eq("ABˇc\n"); - - // works in visual mode - cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await; - cx.simulate_shared_keystrokes("~").await; - cx.shared_state().await.assert_eq("a😀CˇDé1*F\n"); - - // works with multibyte characters - cx.simulate_shared_keystrokes("~").await; - cx.set_shared_state("aˇC😀é1*F\n").await; - cx.simulate_shared_keystrokes("4 ~").await; - cx.shared_state().await.assert_eq("ac😀É1ˇ*F\n"); - - // works with line selections - cx.set_shared_state("abˇC\n").await; - cx.simulate_shared_keystrokes("shift-v ~").await; - cx.shared_state().await.assert_eq("ˇABc\n"); - - // works in visual block mode - cx.set_shared_state("ˇaa\nbb\ncc").await; - cx.simulate_shared_keystrokes("ctrl-v j ~").await; - cx.shared_state().await.assert_eq("ˇAa\nBb\ncc"); - - // works with multiple cursors (zed only) - cx.set_state("aˇßcdˇe\n", Mode::Normal); - cx.simulate_keystrokes("~"); - cx.assert_state("aSSˇcdˇE\n", Mode::Normal); - } - - #[gpui::test] - async fn test_convert_to_upper_case(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - // works in visual mode - cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await; - cx.simulate_shared_keystrokes("shift-u").await; - cx.shared_state().await.assert_eq("a😀CˇDÉ1*F\n"); - - // works with line selections - cx.set_shared_state("abˇC\n").await; - cx.simulate_shared_keystrokes("shift-v shift-u").await; - cx.shared_state().await.assert_eq("ˇABC\n"); - - // works in visual block mode - cx.set_shared_state("ˇaa\nbb\ncc").await; - cx.simulate_shared_keystrokes("ctrl-v j shift-u").await; - cx.shared_state().await.assert_eq("ˇAa\nBb\ncc"); - } - - #[gpui::test] - async fn test_convert_to_lower_case(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - // works in visual mode - cx.set_shared_state("A😀c«DÉ1*fˇ»\n").await; - cx.simulate_shared_keystrokes("u").await; - cx.shared_state().await.assert_eq("A😀cˇdé1*f\n"); - - // works with line selections - cx.set_shared_state("ABˇc\n").await; - cx.simulate_shared_keystrokes("shift-v u").await; - cx.shared_state().await.assert_eq("ˇabc\n"); - - // works in visual block mode - cx.set_shared_state("ˇAa\nBb\nCc").await; - cx.simulate_shared_keystrokes("ctrl-v j u").await; - cx.shared_state().await.assert_eq("ˇaa\nbb\nCc"); - } - - #[gpui::test] - async fn test_change_case_motion(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇabc def").await; - cx.simulate_shared_keystrokes("g shift-u w").await; - cx.shared_state().await.assert_eq("ˇABC def"); - - cx.simulate_shared_keystrokes("g u w").await; - cx.shared_state().await.assert_eq("ˇabc def"); - - cx.simulate_shared_keystrokes("g ~ w").await; - cx.shared_state().await.assert_eq("ˇABC def"); - - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("ˇabc def"); - - cx.set_shared_state("abˇc def").await; - cx.simulate_shared_keystrokes("g ~ i w").await; - cx.shared_state().await.assert_eq("ˇABC def"); - - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("ˇabc def"); - - cx.simulate_shared_keystrokes("g shift-u $").await; - cx.shared_state().await.assert_eq("ˇABC DEF"); - } - - #[gpui::test] - async fn test_change_case_motion_object(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("abc dˇef\n").await; - cx.simulate_shared_keystrokes("g shift-u i w").await; - cx.shared_state().await.assert_eq("abc ˇDEF\n"); - } - - #[gpui::test] - async fn test_convert_to_rot13(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - // works in visual mode - cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await; - cx.simulate_shared_keystrokes("g ?").await; - cx.shared_state().await.assert_eq("a😀CˇqÉ1*s\n"); - - // works with line selections - cx.set_shared_state("abˇC\n").await; - cx.simulate_shared_keystrokes("shift-v g ?").await; - cx.shared_state().await.assert_eq("ˇnoP\n"); - - // works in visual block mode - cx.set_shared_state("ˇaa\nbb\ncc").await; - cx.simulate_shared_keystrokes("ctrl-v j g ?").await; - cx.shared_state().await.assert_eq("ˇna\nob\ncc"); - } - - #[gpui::test] - async fn test_change_rot13_motion(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇabc def").await; - cx.simulate_shared_keystrokes("g ? w").await; - cx.shared_state().await.assert_eq("ˇnop def"); - - cx.simulate_shared_keystrokes("g ? w").await; - cx.shared_state().await.assert_eq("ˇabc def"); - - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("ˇnop def"); - - cx.set_shared_state("abˇc def").await; - cx.simulate_shared_keystrokes("g ? i w").await; - cx.shared_state().await.assert_eq("ˇnop def"); - - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("ˇabc def"); - - cx.simulate_shared_keystrokes("g ? $").await; - cx.shared_state().await.assert_eq("ˇnop qrs"); - } - - #[gpui::test] - async fn test_change_rot13_object(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - .await; - cx.simulate_shared_keystrokes("g ? i w").await; - cx.shared_state() - .await - .assert_eq("ˇnopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM"); - } - - #[gpui::test] - async fn test_change_case_helix_mode(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Explicit selection - cx.set_state("«hello worldˇ»", Mode::HelixNormal); - cx.simulate_keystrokes("~"); - cx.assert_state("«HELLO WORLDˇ»", Mode::HelixNormal); - - // Cursor-only (empty) selection - switch case - cx.set_state("The ˇquick brown", Mode::HelixNormal); - cx.simulate_keystrokes("~"); - cx.assert_state("The ˇQuick brown", Mode::HelixNormal); - cx.simulate_keystrokes("~"); - cx.assert_state("The ˇquick brown", Mode::HelixNormal); - - // Cursor-only (empty) selection - switch to uppercase and lowercase explicitly - cx.set_state("The ˇquick brown", Mode::HelixNormal); - cx.simulate_keystrokes("alt-`"); - cx.assert_state("The ˇQuick brown", Mode::HelixNormal); - cx.simulate_keystrokes("`"); - cx.assert_state("The ˇquick brown", Mode::HelixNormal); - - // With `e` motion (which extends selection to end of word in Helix) - cx.set_state("The ˇquick brown fox", Mode::HelixNormal); - cx.simulate_keystrokes("e"); - cx.simulate_keystrokes("~"); - cx.assert_state("The «QUICKˇ» brown fox", Mode::HelixNormal); - - // Cursor-only - } -} diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs deleted file mode 100644 index b1c41315a8..0000000000 --- a/crates/vim/src/normal/delete.rs +++ /dev/null @@ -1,763 +0,0 @@ -use crate::{ - Vim, - motion::{Motion, MotionKind}, - object::Object, - state::Mode, -}; -use collections::{HashMap, HashSet}; -use editor::{ - Bias, DisplayPoint, - display_map::{DisplaySnapshot, ToDisplayPoint}, -}; -use gpui::{Context, Window}; -use language::{Point, Selection}; -use multi_buffer::MultiBufferRow; - -impl Vim { - pub fn delete_motion( - &mut self, - motion: Motion, - times: Option, - forced_motion: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |vim, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - let mut original_columns: HashMap<_, _> = Default::default(); - let mut motion_kind = None; - let mut ranges_to_copy = Vec::new(); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let original_head = selection.head(); - original_columns.insert(selection.id, original_head.column()); - let kind = motion.expand_selection( - map, - selection, - times, - &text_layout_details, - forced_motion, - ); - ranges_to_copy - .push(selection.start.to_point(map)..selection.end.to_point(map)); - - // When deleting line-wise, we always want to delete a newline. - // If there is one after the current line, it goes; otherwise we - // pick the one before. - if kind == Some(MotionKind::Linewise) { - let start = selection.start.to_point(map); - let end = selection.end.to_point(map); - if end.row < map.buffer_snapshot().max_point().row { - selection.end = Point::new(end.row + 1, 0).to_display_point(map) - } else if start.row > 0 { - selection.start = Point::new( - start.row - 1, - map.buffer_snapshot() - .line_len(MultiBufferRow(start.row - 1)), - ) - .to_display_point(map) - } - } - if let Some(kind) = kind { - motion_kind.get_or_insert(kind); - } - }); - }); - let Some(kind) = motion_kind else { return }; - vim.copy_ranges(editor, kind, false, ranges_to_copy, window, cx); - editor.insert("", window, cx); - - // Fixup cursor position after the deletion - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let mut cursor = selection.head(); - if kind.linewise() - && let Some(column) = original_columns.get(&selection.id) - { - *cursor.column_mut() = *column - } - cursor = map.clip_point(cursor, Bias::Left); - selection.collapse_to(cursor, selection.goal) - }); - }); - editor.refresh_edit_prediction(true, false, window, cx); - }); - }); - } - - pub fn delete_object( - &mut self, - object: Object, - around: bool, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |vim, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - // Emulates behavior in vim where if we expanded backwards to include a newline - // the cursor gets set back to the start of the line - let mut should_move_to_start: HashSet<_> = Default::default(); - - // Emulates behavior in vim where after deletion the cursor should try to move - // to the same column it was before deletion if the line is not empty or only - // contains whitespace - let mut column_before_move: HashMap<_, _> = Default::default(); - let target_mode = object.target_visual_mode(vim.mode, around); - - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let cursor_point = selection.head().to_point(map); - if target_mode == Mode::VisualLine { - column_before_move.insert(selection.id, cursor_point.column); - } - - object.expand_selection(map, selection, around, times); - let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range(); - let mut move_selection_start_to_previous_line = - |map: &DisplaySnapshot, selection: &mut Selection| { - let start = selection.start.to_offset(map, Bias::Left); - if selection.start.row().0 > 0 { - should_move_to_start.insert(selection.id); - selection.start = - (start - '\n'.len_utf8()).to_display_point(map); - } - }; - let range = selection.start.to_offset(map, Bias::Left) - ..selection.end.to_offset(map, Bias::Right); - let contains_only_newlines = map - .buffer_chars_at(range.start) - .take_while(|(_, p)| p < &range.end) - .all(|(char, _)| char == '\n') - && !offset_range.is_empty(); - let end_at_newline = map - .buffer_chars_at(range.end) - .next() - .map(|(c, _)| c == '\n') - .unwrap_or(false); - - // If expanded range contains only newlines and - // the object is around or sentence, expand to include a newline - // at the end or start - if (around || object == Object::Sentence) && contains_only_newlines { - if end_at_newline { - move_selection_end_to_next_line(map, selection); - } else { - move_selection_start_to_previous_line(map, selection); - } - } - - // Does post-processing for the trailing newline and EOF - // when not cancelled. - let cancelled = around && selection.start == selection.end; - if object == Object::Paragraph && !cancelled { - // EOF check should be done before including a trailing newline. - if ends_at_eof(map, selection) { - move_selection_start_to_previous_line(map, selection); - } - - if end_at_newline { - move_selection_end_to_next_line(map, selection); - } - } - }); - }); - vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx); - editor.insert("", window, cx); - - // Fixup cursor position after the deletion - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let mut cursor = selection.head(); - if should_move_to_start.contains(&selection.id) { - *cursor.column_mut() = 0; - } else if let Some(column) = column_before_move.get(&selection.id) - && *column > 0 - { - let mut cursor_point = cursor.to_point(map); - cursor_point.column = *column; - cursor = map - .buffer_snapshot() - .clip_point(cursor_point, Bias::Left) - .to_display_point(map); - } - cursor = map.clip_point(cursor, Bias::Left); - selection.collapse_to(cursor, selection.goal) - }); - }); - editor.refresh_edit_prediction(true, false, window, cx); - }); - }); - } -} - -fn move_selection_end_to_next_line(map: &DisplaySnapshot, selection: &mut Selection) { - let end = selection.end.to_offset(map, Bias::Left); - selection.end = (end + '\n'.len_utf8()).to_display_point(map); -} - -fn ends_at_eof(map: &DisplaySnapshot, selection: &mut Selection) -> bool { - selection.end.to_point(map) == map.buffer_snapshot().max_point() -} - -#[cfg(test)] -mod test { - use indoc::indoc; - - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - - #[gpui::test] - async fn test_delete_h(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("d h", "Teˇst").await.assert_matches(); - cx.simulate("d h", "Tˇest").await.assert_matches(); - cx.simulate("d h", "ˇTest").await.assert_matches(); - cx.simulate( - "d h", - indoc! {" - Test - ˇtest"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_l(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("d l", "ˇTest").await.assert_matches(); - cx.simulate("d l", "Teˇst").await.assert_matches(); - cx.simulate("d l", "Tesˇt").await.assert_matches(); - cx.simulate( - "d l", - indoc! {" - Tesˇt - test"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_w(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "d w", - indoc! {" - Test tesˇt - test"}, - ) - .await - .assert_matches(); - - cx.simulate("d w", "Teˇst").await.assert_matches(); - cx.simulate("d w", "Tˇest test").await.assert_matches(); - cx.simulate( - "d w", - indoc! {" - Test teˇst - test"}, - ) - .await - .assert_matches(); - cx.simulate( - "d w", - indoc! {" - Test tesˇt - test"}, - ) - .await - .assert_matches(); - - cx.simulate( - "d w", - indoc! {" - Test test - ˇ - test"}, - ) - .await - .assert_matches(); - - cx.simulate("d shift-w", "Test teˇst-test test") - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_next_word_end(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("d e", "Teˇst Test\n").await.assert_matches(); - cx.simulate("d e", "Tˇest test\n").await.assert_matches(); - cx.simulate( - "d e", - indoc! {" - Test teˇst - test"}, - ) - .await - .assert_matches(); - cx.simulate( - "d e", - indoc! {" - Test tesˇt - test"}, - ) - .await - .assert_matches(); - - cx.simulate("d e", "Test teˇst-test test") - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_b(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("d b", "Teˇst Test").await.assert_matches(); - cx.simulate("d b", "Test ˇtest").await.assert_matches(); - cx.simulate("d b", "Test1 test2 ˇtest3") - .await - .assert_matches(); - cx.simulate( - "d b", - indoc! {" - Test test - ˇtest"}, - ) - .await - .assert_matches(); - cx.simulate( - "d b", - indoc! {" - Test test - ˇ - test"}, - ) - .await - .assert_matches(); - - cx.simulate("d shift-b", "Test test-test ˇtest") - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_end_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "d $", - indoc! {" - The qˇuick - brown fox"}, - ) - .await - .assert_matches(); - cx.simulate( - "d $", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_end_of_paragraph(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "d }", - indoc! {" - ˇhello world. - - hello world."}, - ) - .await - .assert_matches(); - - cx.simulate( - "d }", - indoc! {" - ˇhello world. - hello world."}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_0(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "d 0", - indoc! {" - The qˇuick - brown fox"}, - ) - .await - .assert_matches(); - cx.simulate( - "d 0", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_k(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "d k", - indoc! {" - The quick - brown ˇfox - jumps over"}, - ) - .await - .assert_matches(); - cx.simulate( - "d k", - indoc! {" - The quick - brown fox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate( - "d k", - indoc! {" - The qˇuick - brown fox - jumps over"}, - ) - .await - .assert_matches(); - cx.simulate( - "d k", - indoc! {" - ˇbrown fox - jumps over"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_j(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "d j", - indoc! {" - The quick - brown ˇfox - jumps over"}, - ) - .await - .assert_matches(); - cx.simulate( - "d j", - indoc! {" - The quick - brown fox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate( - "d j", - indoc! {" - The qˇuick - brown fox - jumps over"}, - ) - .await - .assert_matches(); - cx.simulate( - "d j", - indoc! {" - The quick - brown fox - ˇ"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "d shift-g", - indoc! {" - The quick - brownˇ fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "d shift-g", - indoc! {" - The quick - brownˇ fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "d shift-g", - indoc! {" - The quick - brown fox - jumps over - the lˇazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "d shift-g", - indoc! {" - The quick - brown fox - jumps over - ˇ"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_to_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "d 3 shift-g", - indoc! {" - The quick - brownˇ fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "d 3 shift-g", - indoc! {" - The quick - brown fox - jumps over - the lˇazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "d 2 shift-g", - indoc! {" - The quick - brown fox - jumps over - ˇ"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_gg(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "d g g", - indoc! {" - The quick - brownˇ fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "d g g", - indoc! {" - The quick - brown fox - jumps over - the lˇazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "d g g", - indoc! {" - The qˇuick - brown fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "d g g", - indoc! {" - ˇ - brown fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_cancel_delete_operator(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.set_state( - indoc! {" - The quick brown - fox juˇmps over - the lazy dog"}, - Mode::Normal, - ); - - // Canceling operator twice reverts to normal mode with no active operator - cx.simulate_keystrokes("d escape k"); - assert_eq!(cx.active_operator(), None); - assert_eq!(cx.mode(), Mode::Normal); - cx.assert_editor_state(indoc! {" - The quˇick brown - fox jumps over - the lazy dog"}); - } - - #[gpui::test] - async fn test_unbound_command_cancels_pending_operator(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.set_state( - indoc! {" - The quick brown - fox juˇmps over - the lazy dog"}, - Mode::Normal, - ); - - // Canceling operator twice reverts to normal mode with no active operator - cx.simulate_keystrokes("d y"); - assert_eq!(cx.active_operator(), None); - assert_eq!(cx.mode(), Mode::Normal); - } - - #[gpui::test] - async fn test_delete_with_counts(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d 2 d").await; - cx.shared_state().await.assert_eq(indoc! {" - the ˇlazy dog"}); - - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("2 d d").await; - cx.shared_state().await.assert_eq(indoc! {" - the ˇlazy dog"}); - - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the moon, - a star, and - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("2 d 2 d").await; - cx.shared_state().await.assert_eq(indoc! {" - the ˇlazy dog"}); - } - - #[gpui::test] - async fn test_delete_to_adjacent_character(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("d t x", "ˇax").await.assert_matches(); - cx.simulate("d t x", "aˇx").await.assert_matches(); - } - - #[gpui::test] - async fn test_delete_sentence(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - // cx.simulate( - // "d )", - // indoc! {" - // Fiˇrst. Second. Third. - // Fourth. - // "}, - // ) - // .await - // .assert_matches(); - - // cx.simulate( - // "d )", - // indoc! {" - // First. Secˇond. Third. - // Fourth. - // "}, - // ) - // .await - // .assert_matches(); - - // // Two deletes - // cx.simulate( - // "d ) d )", - // indoc! {" - // First. Second. Thirˇd. - // Fourth. - // "}, - // ) - // .await - // .assert_matches(); - - // Should delete whole line if done on first column - cx.simulate( - "d )", - indoc! {" - ˇFirst. - Fourth. - "}, - ) - .await - .assert_matches(); - - // Backwards it should also delete the whole first line - cx.simulate( - "d (", - indoc! {" - First. - ˇSecond. - Fourth. - "}, - ) - .await - .assert_matches(); - } -} diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs deleted file mode 100644 index d9ef32deba..0000000000 --- a/crates/vim/src/normal/increment.rs +++ /dev/null @@ -1,849 +0,0 @@ -use editor::{Editor, MultiBufferSnapshot, ToOffset, ToPoint}; -use gpui::{Action, Context, Window}; -use language::{Bias, Point}; -use schemars::JsonSchema; -use serde::Deserialize; -use std::ops::Range; - -use crate::{Vim, state::Mode}; - -const BOOLEAN_PAIRS: &[(&str, &str)] = &[("true", "false"), ("yes", "no"), ("on", "off")]; - -/// Increments the number under the cursor or toggles boolean values. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct Increment { - #[serde(default)] - step: bool, -} - -/// Decrements the number under the cursor or toggles boolean values. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct Decrement { - #[serde(default)] - step: bool, -} - -pub fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, |vim, action: &Increment, window, cx| { - vim.record_current_action(cx); - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - let step = if action.step { count as i32 } else { 0 }; - vim.increment(count as i64, step, window, cx) - }); - Vim::action(editor, cx, |vim, action: &Decrement, window, cx| { - vim.record_current_action(cx); - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - let step = if action.step { -1 * (count as i32) } else { 0 }; - vim.increment(-(count as i64), step, window, cx) - }); -} - -impl Vim { - fn increment( - &mut self, - mut delta: i64, - step: i32, - window: &mut Window, - cx: &mut Context, - ) { - self.store_visual_marks(window, cx); - self.update_editor(cx, |vim, editor, cx| { - let mut edits = Vec::new(); - let mut new_anchors = Vec::new(); - - let snapshot = editor.buffer().read(cx).snapshot(cx); - for selection in editor.selections.all_adjusted(&editor.display_snapshot(cx)) { - if !selection.is_empty() - && (vim.mode != Mode::VisualBlock || new_anchors.is_empty()) - { - new_anchors.push((true, snapshot.anchor_before(selection.start))) - } - for row in selection.start.row..=selection.end.row { - let start = if row == selection.start.row { - selection.start - } else { - Point::new(row, 0) - }; - let end = if row == selection.end.row { - selection.end - } else { - Point::new(row, snapshot.line_len(multi_buffer::MultiBufferRow(row))) - }; - - let find_result = if !selection.is_empty() { - find_target(&snapshot, start, end, true) - } else { - find_target(&snapshot, start, end, false) - }; - - if let Some((range, target, radix)) = find_result { - let replace = match radix { - 10 => increment_decimal_string(&target, delta), - 16 => increment_hex_string(&target, delta), - 2 => increment_binary_string(&target, delta), - 0 => increment_toggle_string(&target), - _ => unreachable!(), - }; - delta += step as i64; - edits.push((range.clone(), replace)); - if selection.is_empty() { - new_anchors.push((false, snapshot.anchor_after(range.end))) - } - } else if selection.is_empty() { - new_anchors.push((true, snapshot.anchor_after(start))) - } - } - } - editor.transact(window, cx, |editor, window, cx| { - editor.edit(edits, cx); - - let snapshot = editor.buffer().read(cx).snapshot(cx); - editor.change_selections(Default::default(), window, cx, |s| { - let mut new_ranges = Vec::new(); - for (visual, anchor) in new_anchors.iter() { - let mut point = anchor.to_point(&snapshot); - if !*visual && point.column > 0 { - point.column -= 1; - point = snapshot.clip_point(point, Bias::Left) - } - new_ranges.push(point..point); - } - s.select_ranges(new_ranges) - }) - }); - }); - self.switch_mode(Mode::Normal, true, window, cx) - } -} - -fn increment_decimal_string(num: &str, delta: i64) -> String { - let (negative, delta, num_str) = match num.strip_prefix('-') { - Some(n) => (true, -delta, n), - None => (false, delta, num), - }; - let num_length = num_str.len(); - let leading_zero = num_str.starts_with('0'); - - let (result, new_negative) = match u64::from_str_radix(num_str, 10) { - Ok(value) => { - let wrapped = value.wrapping_add_signed(delta); - if delta < 0 && wrapped > value { - ((u64::MAX - wrapped).wrapping_add(1), !negative) - } else if delta > 0 && wrapped < value { - (u64::MAX - wrapped, !negative) - } else { - (wrapped, negative) - } - } - Err(_) => (u64::MAX, negative), - }; - - let formatted = format!("{}", result); - let new_significant_digits = formatted.len(); - let padding = if leading_zero { - num_length.saturating_sub(new_significant_digits) - } else { - 0 - }; - - if new_negative && result != 0 { - format!("-{}{}", "0".repeat(padding), formatted) - } else { - format!("{}{}", "0".repeat(padding), formatted) - } -} - -fn increment_hex_string(num: &str, delta: i64) -> String { - let result = if let Ok(val) = u64::from_str_radix(num, 16) { - val.wrapping_add_signed(delta) - } else { - u64::MAX - }; - if should_use_lowercase(num) { - format!("{:0width$x}", result, width = num.len()) - } else { - format!("{:0width$X}", result, width = num.len()) - } -} - -fn should_use_lowercase(num: &str) -> bool { - let mut use_uppercase = false; - for ch in num.chars() { - if ch.is_ascii_lowercase() { - return true; - } - if ch.is_ascii_uppercase() { - use_uppercase = true; - } - } - !use_uppercase -} - -fn increment_binary_string(num: &str, delta: i64) -> String { - let result = if let Ok(val) = u64::from_str_radix(num, 2) { - val.wrapping_add_signed(delta) - } else { - u64::MAX - }; - format!("{:0width$b}", result, width = num.len()) -} - -fn find_target( - snapshot: &MultiBufferSnapshot, - start: Point, - end: Point, - need_range: bool, -) -> Option<(Range, String, u32)> { - let start_offset = start.to_offset(snapshot); - let end_offset = end.to_offset(snapshot); - - let mut offset = start_offset; - let mut first_char_is_num = snapshot - .chars_at(offset) - .next() - .map_or(false, |ch| ch.is_ascii_hexdigit()); - let mut pre_char = String::new(); - - let next_offset = offset - + snapshot - .chars_at(start_offset) - .next() - .map_or(0, |ch| ch.len_utf8()); - // Backward scan to find the start of the number, but stop at start_offset - for ch in snapshot.reversed_chars_at(next_offset) { - // Search boundaries - if offset.0 == 0 || ch.is_whitespace() || (need_range && offset <= start_offset) { - break; - } - - // Avoid the influence of hexadecimal letters - if first_char_is_num - && !ch.is_ascii_hexdigit() - && (ch != 'b' && ch != 'B') - && (ch != 'x' && ch != 'X') - && ch != '-' - { - // Used to determine if the initial character is a number. - if is_numeric_string(&pre_char) { - break; - } else { - first_char_is_num = false; - } - } - - pre_char.insert(0, ch); - offset -= ch.len_utf8(); - } - - let mut begin = None; - let mut end = None; - let mut target = String::new(); - let mut radix = 10; - let mut is_num = false; - - let mut chars = snapshot.chars_at(offset).peekable(); - - while let Some(ch) = chars.next() { - if need_range && offset >= end_offset { - break; // stop at end of selection - } - - if target == "0" - && (ch == 'b' || ch == 'B') - && chars.peek().is_some() - && chars.peek().unwrap().is_digit(2) - { - radix = 2; - begin = None; - target = String::new(); - } else if target == "0" - && (ch == 'x' || ch == 'X') - && chars.peek().is_some() - && chars.peek().unwrap().is_ascii_hexdigit() - { - radix = 16; - begin = None; - target = String::new(); - } else if ch == '.' { - is_num = false; - begin = None; - target = String::new(); - } else if ch.is_digit(radix) - || ((begin.is_none() || !is_num) - && ch == '-' - && chars.peek().is_some() - && chars.peek().unwrap().is_digit(radix)) - { - if !is_num { - is_num = true; - begin = Some(offset); - target = String::new(); - } else if begin.is_none() { - begin = Some(offset); - } - target.push(ch); - } else if ch.is_ascii_alphabetic() && !is_num { - if begin.is_none() { - begin = Some(offset); - } - target.push(ch); - } else if begin.is_some() && (is_num || !is_num && is_toggle_word(&target)) { - // End of matching - end = Some(offset); - break; - } else if ch == '\n' { - break; - } else { - // To match the next word - is_num = false; - begin = None; - target = String::new(); - } - - offset += ch.len_utf8(); - } - - if let Some(begin) = begin - && (is_num || !is_num && is_toggle_word(&target)) - { - if !is_num { - radix = 0; - } - - let end = end.unwrap_or(offset); - Some(( - begin.to_point(snapshot)..end.to_point(snapshot), - target, - radix, - )) - } else { - None - } -} - -fn is_numeric_string(s: &str) -> bool { - if s.is_empty() { - return false; - } - - let (_, rest) = if let Some(r) = s.strip_prefix('-') { - (true, r) - } else { - (false, s) - }; - - if rest.is_empty() { - return false; - } - - if let Some(digits) = rest.strip_prefix("0b").or_else(|| rest.strip_prefix("0B")) { - digits.is_empty() || digits.chars().all(|c| c == '0' || c == '1') - } else if let Some(digits) = rest.strip_prefix("0x").or_else(|| rest.strip_prefix("0X")) { - digits.is_empty() || digits.chars().all(|c| c.is_ascii_hexdigit()) - } else { - !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit()) - } -} - -fn is_toggle_word(word: &str) -> bool { - let lower = word.to_lowercase(); - BOOLEAN_PAIRS - .iter() - .any(|(a, b)| lower == *a || lower == *b) -} - -fn increment_toggle_string(boolean: &str) -> String { - let lower = boolean.to_lowercase(); - - let target = BOOLEAN_PAIRS - .iter() - .find_map(|(a, b)| { - if lower == *a { - Some(b) - } else if lower == *b { - Some(a) - } else { - None - } - }) - .unwrap_or(&boolean); - - if boolean.chars().all(|c| c.is_uppercase()) { - // Upper case - target.to_uppercase() - } else if boolean.chars().next().unwrap_or(' ').is_uppercase() { - // Title case - let mut chars = target.chars(); - match chars.next() { - None => String::new(), - Some(c) => c.to_uppercase().collect::() + chars.as_str(), - } - } else { - target.to_string() - } -} - -#[cfg(test)] -mod test { - use indoc::indoc; - - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - - #[gpui::test] - async fn test_increment(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - 1ˇ2 - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 1ˇ3 - "}); - cx.simulate_shared_keystrokes("ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - 1ˇ2 - "}); - - cx.simulate_shared_keystrokes("9 9 ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 11ˇ1 - "}); - cx.simulate_shared_keystrokes("1 1 1 ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇ0 - "}); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq(indoc! {" - -11ˇ1 - "}); - } - - #[gpui::test] - async fn test_increment_with_dot(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - 1ˇ.2 - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 1.ˇ3 - "}); - cx.simulate_shared_keystrokes("ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - 1.ˇ2 - "}); - } - - #[gpui::test] - async fn test_increment_with_leading_zeros(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - 000ˇ9 - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 001ˇ0 - "}); - cx.simulate_shared_keystrokes("2 ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - 000ˇ8 - "}); - } - - #[gpui::test] - async fn test_increment_with_leading_zeros_and_zero(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - 01ˇ1 - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 01ˇ2 - "}); - cx.simulate_shared_keystrokes("1 2 ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - 00ˇ0 - "}); - } - - #[gpui::test] - async fn test_increment_with_changing_leading_zeros(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - 099ˇ9 - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 100ˇ0 - "}); - cx.simulate_shared_keystrokes("2 ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - 99ˇ8 - "}); - } - - #[gpui::test] - async fn test_increment_with_two_dots(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - 111.ˇ.2 - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 111..ˇ3 - "}); - cx.simulate_shared_keystrokes("ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - 111..ˇ2 - "}); - } - - #[gpui::test] - async fn test_increment_sign_change(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - ˇ0 - "}) - .await; - cx.simulate_shared_keystrokes("ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - -ˇ1 - "}); - cx.simulate_shared_keystrokes("2 ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇ1 - "}); - } - - #[gpui::test] - async fn test_increment_sign_change_with_leading_zeros(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - 00ˇ1 - "}) - .await; - cx.simulate_shared_keystrokes("ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - 00ˇ0 - "}); - cx.simulate_shared_keystrokes("ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - -00ˇ1 - "}); - cx.simulate_shared_keystrokes("2 ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 00ˇ1 - "}); - } - - #[gpui::test] - async fn test_increment_bin_wrapping_and_padding(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - 0b111111111111111111111111111111111111111111111111111111111111111111111ˇ1 - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1 - "}); - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 0b000000000000000000000000000000000000000000000000000000000000000000000ˇ0 - "}); - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 0b000000000000000000000000000000000000000000000000000000000000000000000ˇ1 - "}); - cx.simulate_shared_keystrokes("2 ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - 0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1 - "}); - } - - #[gpui::test] - async fn test_increment_hex_wrapping_and_padding(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - 0xfffffffffffffffffffˇf - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 0x0000fffffffffffffffˇf - "}); - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 0x0000000000000000000ˇ0 - "}); - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 0x0000000000000000000ˇ1 - "}); - cx.simulate_shared_keystrokes("2 ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - 0x0000fffffffffffffffˇf - "}); - } - - #[gpui::test] - async fn test_increment_wrapping(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - 1844674407370955161ˇ9 - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 1844674407370955161ˇ5 - "}); - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - -1844674407370955161ˇ5 - "}); - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - -1844674407370955161ˇ4 - "}); - cx.simulate_shared_keystrokes("3 ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - 1844674407370955161ˇ4 - "}); - cx.simulate_shared_keystrokes("2 ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - -1844674407370955161ˇ5 - "}); - } - - #[gpui::test] - async fn test_increment_inline(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - inline0x3ˇ9u32 - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - inline0x3ˇau32 - "}); - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - inline0x3ˇbu32 - "}); - cx.simulate_shared_keystrokes("l l l ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - inline0x3bu3ˇ3 - "}); - } - - #[gpui::test] - async fn test_increment_hex_casing(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - 0xFˇa - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 0xfˇb - "}); - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 0xfˇc - "}); - } - - #[gpui::test] - async fn test_increment_radix(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.simulate("ctrl-a", "ˇ total: 0xff") - .await - .assert_matches(); - cx.simulate("ctrl-x", "ˇ total: 0xff") - .await - .assert_matches(); - cx.simulate("ctrl-x", "ˇ total: 0xFF") - .await - .assert_matches(); - cx.simulate("ctrl-a", "(ˇ0b10f)").await.assert_matches(); - cx.simulate("ctrl-a", "ˇ-1").await.assert_matches(); - cx.simulate("ctrl-a", "banˇana").await.assert_matches(); - } - - #[gpui::test] - async fn test_increment_steps(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇ1 - 1 - 1 2 - 1 - 1"}) - .await; - - cx.simulate_shared_keystrokes("j v shift-g g ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 - ˇ2 - 3 2 - 4 - 5"}); - - cx.simulate_shared_keystrokes("shift-g ctrl-v g g").await; - cx.shared_state().await.assert_eq(indoc! {" - «1ˇ» - «2ˇ» - «3ˇ» 2 - «4ˇ» - «5ˇ»"}); - - cx.simulate_shared_keystrokes("g ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇ0 - 0 - 0 2 - 0 - 0"}); - cx.simulate_shared_keystrokes("v shift-g g ctrl-a").await; - cx.simulate_shared_keystrokes("v shift-g 5 g ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇ6 - 12 - 18 2 - 24 - 30"}); - } - - #[gpui::test] - async fn test_increment_toggle(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("let enabled = trˇue;", Mode::Normal); - cx.simulate_keystrokes("ctrl-a"); - cx.assert_state("let enabled = falsˇe;", Mode::Normal); - - cx.simulate_keystrokes("0 ctrl-a"); - cx.assert_state("let enabled = truˇe;", Mode::Normal); - - cx.set_state( - indoc! {" - ˇlet enabled = TRUE; - let enabled = TRUE; - let enabled = TRUE; - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("shift-v j j ctrl-x"); - cx.assert_state( - indoc! {" - ˇlet enabled = FALSE; - let enabled = FALSE; - let enabled = FALSE; - "}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - let enabled = ˇYes; - let enabled = Yes; - let enabled = Yes; - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("ctrl-v j j e ctrl-x"); - cx.assert_state( - indoc! {" - let enabled = ˇNo; - let enabled = No; - let enabled = No; - "}, - Mode::Normal, - ); - - cx.set_state("ˇlet enabled = True;", Mode::Normal); - cx.simulate_keystrokes("ctrl-a"); - cx.assert_state("let enabled = Falsˇe;", Mode::Normal); - - cx.simulate_keystrokes("ctrl-a"); - cx.assert_state("let enabled = Truˇe;", Mode::Normal); - - cx.set_state("let enabled = Onˇ;", Mode::Normal); - cx.simulate_keystrokes("v b ctrl-a"); - cx.assert_state("let enabled = ˇOff;", Mode::Normal); - } - - #[gpui::test] - async fn test_increment_order(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("aaˇa false 1 2 3", Mode::Normal); - cx.simulate_keystrokes("ctrl-a"); - cx.assert_state("aaa truˇe 1 2 3", Mode::Normal); - - cx.set_state("aaˇa 1 false 2 3", Mode::Normal); - cx.simulate_keystrokes("ctrl-a"); - cx.assert_state("aaa ˇ2 false 2 3", Mode::Normal); - - cx.set_state("trueˇ 1 2 3", Mode::Normal); - cx.simulate_keystrokes("ctrl-a"); - cx.assert_state("true ˇ2 2 3", Mode::Normal); - - cx.set_state("falseˇ", Mode::Normal); - cx.simulate_keystrokes("ctrl-a"); - cx.assert_state("truˇe", Mode::Normal); - - cx.set_state("⚡️ˇ⚡️", Mode::Normal); - cx.simulate_keystrokes("ctrl-a"); - cx.assert_state("⚡️ˇ⚡️", Mode::Normal); - } - - #[gpui::test] - async fn test_increment_visual_partial_number(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇ123").await; - cx.simulate_shared_keystrokes("v l ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {"ˇ133"}); - cx.simulate_shared_keystrokes("l v l ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {"1ˇ34"}); - cx.simulate_shared_keystrokes("shift-v y p p ctrl-v k k l ctrl-a") - .await; - cx.shared_state().await.assert_eq(indoc! {"ˇ144\n144\n144"}); - } -} diff --git a/crates/vim/src/normal/mark.rs b/crates/vim/src/normal/mark.rs deleted file mode 100644 index 3bb040511f..0000000000 --- a/crates/vim/src/normal/mark.rs +++ /dev/null @@ -1,397 +0,0 @@ -use std::{ops::Range, path::Path, sync::Arc}; - -use editor::{ - Anchor, Bias, DisplayPoint, Editor, MultiBuffer, - display_map::{DisplaySnapshot, ToDisplayPoint}, - movement, -}; -use gpui::{Context, Entity, EntityId, UpdateGlobal, Window}; -use language::SelectionGoal; -use text::Point; -use ui::App; -use workspace::OpenOptions; - -use crate::{ - Vim, - motion::{self, Motion}, - state::{Mark, Mode, VimGlobals}, -}; - -impl Vim { - pub fn create_mark(&mut self, text: Arc, window: &mut Window, cx: &mut Context) { - self.update_editor(cx, |vim, editor, cx| { - let anchors = editor - .selections - .disjoint_anchors_arc() - .iter() - .map(|s| s.head()) - .collect::>(); - vim.set_mark(text.to_string(), anchors, editor.buffer(), window, cx); - }); - self.clear_operator(window, cx); - } - - // When handling an action, you must create visual marks if you will switch to normal - // mode without the default selection behavior. - pub(crate) fn store_visual_marks(&mut self, window: &mut Window, cx: &mut Context) { - if self.mode.is_visual() { - self.create_visual_marks(self.mode, window, cx); - } - } - - pub(crate) fn create_visual_marks( - &mut self, - mode: Mode, - window: &mut Window, - cx: &mut Context, - ) { - let mut starts = vec![]; - let mut ends = vec![]; - let mut reversed = vec![]; - - self.update_editor(cx, |vim, editor, cx| { - let display_map = editor.display_snapshot(cx); - let selections = editor.selections.all_display(&display_map); - for selection in selections { - let end = movement::saturating_left(&display_map, selection.end); - ends.push( - display_map - .buffer_snapshot() - .anchor_before(end.to_offset(&display_map, Bias::Left)), - ); - starts.push( - display_map - .buffer_snapshot() - .anchor_before(selection.start.to_offset(&display_map, Bias::Left)), - ); - reversed.push(selection.reversed) - } - vim.set_mark("<".to_string(), starts, editor.buffer(), window, cx); - vim.set_mark(">".to_string(), ends, editor.buffer(), window, cx); - }); - - self.stored_visual_mode.replace((mode, reversed)); - } - - fn open_buffer_mark( - &mut self, - line: bool, - entity_id: EntityId, - anchors: Vec, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = self.workspace(window) else { - return; - }; - workspace.update(cx, |workspace, cx| { - let item = workspace.items(cx).find(|item| { - item.act_as::(cx) - .is_some_and(|editor| editor.read(cx).buffer().entity_id() == entity_id) - }); - let Some(item) = item.cloned() else { - return; - }; - if let Some(pane) = workspace.pane_for(item.as_ref()) { - pane.update(cx, |pane, cx| { - if let Some(index) = pane.index_for_item(item.as_ref()) { - pane.activate_item(index, true, true, window, cx); - } - }); - }; - - item.act_as::(cx).unwrap().update(cx, |editor, cx| { - let map = editor.snapshot(window, cx); - let mut ranges: Vec> = Vec::new(); - for mut anchor in anchors { - if line { - let mut point = anchor.to_display_point(&map.display_snapshot); - point = motion::first_non_whitespace(&map.display_snapshot, false, point); - anchor = map - .display_snapshot - .buffer_snapshot() - .anchor_before(point.to_point(&map.display_snapshot)); - } - - if ranges.last() != Some(&(anchor..anchor)) { - ranges.push(anchor..anchor); - } - } - - editor.change_selections(Default::default(), window, cx, |s| { - s.select_anchor_ranges(ranges) - }); - }) - }); - } - - fn open_path_mark( - &mut self, - line: bool, - path: Arc, - points: Vec, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = self.workspace(window) else { - return; - }; - let task = workspace.update(cx, |workspace, cx| { - workspace.open_abs_path( - path.to_path_buf(), - OpenOptions { - visible: Some(workspace::OpenVisible::All), - focus: Some(true), - ..Default::default() - }, - window, - cx, - ) - }); - cx.spawn_in(window, async move |this, cx| { - let editor = task.await?; - this.update_in(cx, |_, window, cx| { - if let Some(editor) = editor.act_as::(cx) { - editor.update(cx, |editor, cx| { - let map = editor.snapshot(window, cx); - let points: Vec<_> = points - .into_iter() - .map(|p| { - if line { - let point = p.to_display_point(&map.display_snapshot); - motion::first_non_whitespace( - &map.display_snapshot, - false, - point, - ) - .to_point(&map.display_snapshot) - } else { - p - } - }) - .collect(); - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges(points.into_iter().map(|p| p..p)) - }) - }) - } - }) - }) - .detach_and_log_err(cx); - } - - pub fn jump( - &mut self, - text: Arc, - line: bool, - should_pop_operator: bool, - window: &mut Window, - cx: &mut Context, - ) { - if should_pop_operator { - self.pop_operator(window, cx); - } - let mark = self - .update_editor(cx, |vim, editor, cx| { - vim.get_mark(&text, editor, window, cx) - }) - .flatten(); - let anchors = match mark { - None => None, - Some(Mark::Local(anchors)) => Some(anchors), - Some(Mark::Buffer(entity_id, anchors)) => { - self.open_buffer_mark(line, entity_id, anchors, window, cx); - return; - } - Some(Mark::Path(path, points)) => { - self.open_path_mark(line, path, points, window, cx); - return; - } - }; - - let Some(mut anchors) = anchors else { return }; - - self.update_editor(cx, |_, editor, cx| { - editor.create_nav_history_entry(cx); - }); - let is_active_operator = self.active_operator().is_some(); - if is_active_operator { - if let Some(anchor) = anchors.last() { - self.motion( - Motion::Jump { - anchor: *anchor, - line, - }, - window, - cx, - ) - } - } else { - // Save the last anchor so as to jump to it later. - let anchor: Option = anchors.last_mut().map(|anchor| *anchor); - let should_jump = self.mode == Mode::Visual - || self.mode == Mode::VisualLine - || self.mode == Mode::VisualBlock; - - self.update_editor(cx, |_, editor, cx| { - let map = editor.snapshot(window, cx); - let mut ranges: Vec> = Vec::new(); - for mut anchor in anchors { - if line { - let mut point = anchor.to_display_point(&map.display_snapshot); - point = motion::first_non_whitespace(&map.display_snapshot, false, point); - anchor = map - .display_snapshot - .buffer_snapshot() - .anchor_before(point.to_point(&map.display_snapshot)); - } - - if ranges.last() != Some(&(anchor..anchor)) { - ranges.push(anchor..anchor); - } - } - - if !should_jump && !ranges.is_empty() { - editor.change_selections(Default::default(), window, cx, |s| { - s.select_anchor_ranges(ranges) - }); - } - }); - - if should_jump && let Some(anchor) = anchor { - self.motion(Motion::Jump { anchor, line }, window, cx) - } - } - } - - pub fn set_mark( - &mut self, - mut name: String, - anchors: Vec, - buffer_entity: &Entity, - window: &mut Window, - cx: &mut App, - ) { - let Some(workspace) = self.workspace(window) else { - return; - }; - if name == "`" { - name = "'".to_string(); - } - if matches!(&name[..], "-" | " ") { - // Not allowed marks - return; - } - let entity_id = workspace.entity_id(); - Vim::update_globals(cx, |vim_globals, cx| { - let Some(marks_state) = vim_globals.marks.get(&entity_id) else { - return; - }; - marks_state.update(cx, |ms, cx| { - ms.set_mark(name.clone(), buffer_entity, anchors, cx); - }); - }); - } - - pub fn get_mark( - &self, - mut name: &str, - editor: &mut Editor, - window: &mut Window, - cx: &mut App, - ) -> Option { - if name == "`" { - name = "'"; - } - if matches!(name, "{" | "}" | "(" | ")") { - let display_map = editor.display_snapshot(cx); - let selections = editor.selections.all_display(&display_map); - let anchors = selections - .into_iter() - .map(|selection| { - let point = match name { - "{" => movement::start_of_paragraph(&display_map, selection.head(), 1), - "}" => movement::end_of_paragraph(&display_map, selection.head(), 1), - "(" => motion::sentence_backwards(&display_map, selection.head(), 1), - ")" => motion::sentence_forwards(&display_map, selection.head(), 1), - _ => unreachable!(), - }; - display_map - .buffer_snapshot() - .anchor_before(point.to_offset(&display_map, Bias::Left)) - }) - .collect::>(); - return Some(Mark::Local(anchors)); - } - VimGlobals::update_global(cx, |globals, cx| { - let workspace_id = self.workspace(window)?.entity_id(); - globals - .marks - .get_mut(&workspace_id)? - .update(cx, |ms, cx| ms.get_mark(name, editor.buffer(), cx)) - }) - } - - pub fn delete_mark( - &self, - name: String, - editor: &mut Editor, - window: &mut Window, - cx: &mut App, - ) { - let Some(workspace) = self.workspace(window) else { - return; - }; - if name == "`" || name == "'" { - return; - } - let entity_id = workspace.entity_id(); - Vim::update_globals(cx, |vim_globals, cx| { - let Some(marks_state) = vim_globals.marks.get(&entity_id) else { - return; - }; - marks_state.update(cx, |ms, cx| { - ms.delete_mark(name.clone(), editor.buffer(), cx); - }); - }); - } -} - -pub fn jump_motion( - map: &DisplaySnapshot, - anchor: Anchor, - line: bool, -) -> (DisplayPoint, SelectionGoal) { - let mut point = anchor.to_display_point(map); - if line { - point = motion::first_non_whitespace(map, false, point) - } - - (point, SelectionGoal::None) -} - -#[cfg(test)] -mod test { - use gpui::TestAppContext; - - use crate::test::NeovimBackedTestContext; - - #[gpui::test] - async fn test_quote_mark(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇHello, world!").await; - cx.simulate_shared_keystrokes("w m o").await; - cx.shared_state().await.assert_eq("Helloˇ, world!"); - cx.simulate_shared_keystrokes("$ ` o").await; - cx.shared_state().await.assert_eq("Helloˇ, world!"); - cx.simulate_shared_keystrokes("` `").await; - cx.shared_state().await.assert_eq("Hello, worldˇ!"); - cx.simulate_shared_keystrokes("` `").await; - cx.shared_state().await.assert_eq("Helloˇ, world!"); - cx.simulate_shared_keystrokes("$ m '").await; - cx.shared_state().await.assert_eq("Hello, worldˇ!"); - cx.simulate_shared_keystrokes("^ ` `").await; - cx.shared_state().await.assert_eq("Hello, worldˇ!"); - } -} diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs deleted file mode 100644 index 82af828deb..0000000000 --- a/crates/vim/src/normal/paste.rs +++ /dev/null @@ -1,1086 +0,0 @@ -use editor::{ - DisplayPoint, MultiBufferOffset, RowExt, SelectionEffects, display_map::ToDisplayPoint, - movement, -}; -use gpui::{Action, Context, Window}; -use language::{Bias, SelectionGoal}; -use schemars::JsonSchema; -use serde::Deserialize; -use settings::Settings; -use std::cmp; -use vim_mode_setting::HelixModeSetting; - -use crate::{ - Vim, - motion::{Motion, MotionKind}, - object::Object, - state::{Mode, Register}, -}; - -/// Pastes text from the specified register at the cursor position. -#[derive(Clone, Default, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub struct Paste { - #[serde(default)] - before: bool, - #[serde(default)] - preserve_clipboard: bool, -} - -impl Vim { - pub fn paste(&mut self, action: &Paste, window: &mut Window, cx: &mut Context) { - self.record_current_action(cx); - self.store_visual_marks(window, cx); - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - - self.update_editor(cx, |vim, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - - let selected_register = vim.selected_register.take(); - - let Some(Register { - text, - clipboard_selections, - }) = Vim::update_globals(cx, |globals, cx| { - globals.read_register(selected_register, Some(editor), cx) - }) - .filter(|reg| !reg.text.is_empty()) - else { - return; - }; - let clipboard_selections = clipboard_selections - .filter(|sel| sel.len() > 1 && vim.mode != Mode::VisualLine); - - if !action.preserve_clipboard && vim.mode.is_visual() { - vim.copy_selections_content(editor, MotionKind::for_mode(vim.mode), window, cx); - } - - let display_map = editor.display_snapshot(cx); - let current_selections = editor.selections.all_adjusted_display(&display_map); - - // unlike zed, if you have a multi-cursor selection from vim block mode, - // pasting it will paste it on subsequent lines, even if you don't yet - // have a cursor there. - let mut selections_to_process = Vec::new(); - let mut i = 0; - while i < current_selections.len() { - selections_to_process - .push((current_selections[i].start..current_selections[i].end, true)); - i += 1; - } - if let Some(clipboard_selections) = clipboard_selections.as_ref() { - let left = current_selections - .iter() - .map(|selection| cmp::min(selection.start.column(), selection.end.column())) - .min() - .unwrap(); - let mut row = current_selections.last().unwrap().end.row().next_row(); - while i < clipboard_selections.len() { - let cursor = - display_map.clip_point(DisplayPoint::new(row, left), Bias::Left); - selections_to_process.push((cursor..cursor, false)); - i += 1; - row.0 += 1; - } - } - - let first_selection_indent_column = - clipboard_selections.as_ref().and_then(|zed_selections| { - zed_selections - .first() - .map(|selection| selection.first_line_indent) - }); - let before = action.before || vim.mode == Mode::VisualLine; - - let mut edits = Vec::new(); - let mut new_selections = Vec::new(); - let mut original_indent_columns = Vec::new(); - let mut start_offset = 0; - - for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() { - let (mut to_insert, original_indent_column) = - if let Some(clipboard_selections) = &clipboard_selections { - if let Some(clipboard_selection) = clipboard_selections.get(ix) { - let end_offset = start_offset + clipboard_selection.len; - let text = text[start_offset..end_offset].to_string(); - start_offset = end_offset + 1; - (text, Some(clipboard_selection.first_line_indent)) - } else { - ("".to_string(), first_selection_indent_column) - } - } else { - (text.to_string(), first_selection_indent_column) - }; - let line_mode = to_insert.ends_with('\n'); - let is_multiline = to_insert.contains('\n'); - - if line_mode && !before { - if selection.is_empty() { - to_insert = - "\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()]; - } else { - to_insert = "\n".to_owned() + &to_insert; - } - } else if line_mode && vim.mode == Mode::VisualLine { - to_insert.pop(); - } - - let display_range = if !selection.is_empty() { - // If vim is in VISUAL LINE mode and the column for the - // selection's end point is 0, that means that the - // cursor is at the newline character (\n) at the end of - // the line. In this situation we'll want to move one - // position to the left, ensuring we don't join the last - // line of the selection with the line directly below. - let end_point = - if vim.mode == Mode::VisualLine && selection.end.column() == 0 { - movement::left(&display_map, selection.end) - } else { - selection.end - }; - - selection.start..end_point - } else if line_mode { - let point = if before { - movement::line_beginning(&display_map, selection.start, false) - } else { - movement::line_end(&display_map, selection.start, false) - }; - point..point - } else { - let point = if before { - selection.start - } else { - movement::saturating_right(&display_map, selection.start) - }; - point..point - }; - - let point_range = display_range.start.to_point(&display_map) - ..display_range.end.to_point(&display_map); - let anchor = if is_multiline || vim.mode == Mode::VisualLine { - display_map - .buffer_snapshot() - .anchor_before(point_range.start) - } else { - display_map.buffer_snapshot().anchor_after(point_range.end) - }; - - if *preserve { - new_selections.push((anchor, line_mode, is_multiline)); - } - edits.push((point_range, to_insert.repeat(count))); - original_indent_columns.push(original_indent_column); - } - - let cursor_offset = editor - .selections - .last::(&display_map) - .head(); - if editor - .buffer() - .read(cx) - .snapshot(cx) - .language_settings_at(cursor_offset, cx) - .auto_indent_on_paste - { - editor.edit_with_block_indent(edits, original_indent_columns, cx); - } else { - editor.edit(edits, cx); - } - - // in line_mode vim will insert the new text on the next (or previous if before) line - // and put the cursor on the first non-blank character of the first inserted line (or at the end if the first line is blank). - // otherwise vim will insert the next text at (or before) the current cursor position, - // the cursor will go to the last (or first, if is_multiline) inserted character. - editor.change_selections(Default::default(), window, cx, |s| { - s.replace_cursors_with(|map| { - let mut cursors = Vec::new(); - for (anchor, line_mode, is_multiline) in &new_selections { - let mut cursor = anchor.to_display_point(map); - if *line_mode { - if !before { - cursor = movement::down( - map, - cursor, - SelectionGoal::None, - false, - &text_layout_details, - ) - .0; - } - cursor = movement::indented_line_beginning(map, cursor, true, true); - } else if !is_multiline && !vim.temp_mode { - cursor = movement::saturating_left(map, cursor) - } - cursors.push(cursor); - if vim.mode == Mode::VisualBlock { - break; - } - } - - cursors - }); - }) - }); - }); - - if HelixModeSetting::get_global(cx).0 { - self.switch_mode(Mode::HelixNormal, true, window, cx); - } else { - self.switch_mode(Mode::Normal, true, window, cx); - } - } - - pub fn replace_with_register_object( - &mut self, - object: Object, - around: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - let selected_register = self.selected_register.take(); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - object.expand_selection(map, selection, around, None); - }); - }); - - let Some(Register { text, .. }) = Vim::update_globals(cx, |globals, cx| { - globals.read_register(selected_register, Some(editor), cx) - }) - .filter(|reg| !reg.text.is_empty()) else { - return; - }; - editor.insert(&text, window, cx); - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - selection.start = map.clip_point(selection.start, Bias::Left); - selection.end = selection.start - }) - }) - }); - }); - } - - pub fn replace_with_register_motion( - &mut self, - motion: Motion, - times: Option, - forced_motion: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - let selected_register = self.selected_register.take(); - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - motion.expand_selection( - map, - selection, - times, - &text_layout_details, - forced_motion, - ); - }); - }); - - let Some(Register { text, .. }) = Vim::update_globals(cx, |globals, cx| { - globals.read_register(selected_register, Some(editor), cx) - }) - .filter(|reg| !reg.text.is_empty()) else { - return; - }; - editor.insert(&text, window, cx); - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - selection.start = map.clip_point(selection.start, Bias::Left); - selection.end = selection.start - }) - }) - }); - }); - } -} - -#[cfg(test)] -mod test { - use crate::{ - state::{Mode, Register}, - test::{NeovimBackedTestContext, VimTestContext}, - }; - use gpui::ClipboardItem; - use indoc::indoc; - use language::{LanguageName, language_settings::LanguageSettingsContent}; - use settings::{SettingsStore, UseSystemClipboard}; - - #[gpui::test] - async fn test_paste(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // single line - cx.set_shared_state(indoc! {" - The quick brown - fox ˇjumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("v w y").await; - cx.shared_clipboard().await.assert_eq("jumps o"); - cx.set_shared_state(indoc! {" - The quick brown - fox jumps oveˇr - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("p").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jumps overjumps ˇo - the lazy dog"}); - - cx.set_shared_state(indoc! {" - The quick brown - fox jumps oveˇr - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("shift-p").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jumps ovejumps ˇor - the lazy dog"}); - - // line mode - cx.set_shared_state(indoc! {" - The quick brown - fox juˇmps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d d").await; - cx.shared_clipboard().await.assert_eq("fox jumps over\n"); - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - the laˇzy dog"}); - cx.simulate_shared_keystrokes("p").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - the lazy dog - ˇfox jumps over"}); - cx.simulate_shared_keystrokes("k shift-p").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - ˇfox jumps over - the lazy dog - fox jumps over"}); - - // multiline, cursor to first character of pasted text. - cx.set_shared_state(indoc! {" - The quick brown - fox jumps ˇover - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("v j y").await; - cx.shared_clipboard().await.assert_eq("over\nthe lazy do"); - - cx.simulate_shared_keystrokes("p").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jumps oˇover - the lazy dover - the lazy dog"}); - cx.simulate_shared_keystrokes("u shift-p").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jumps ˇover - the lazy doover - the lazy dog"}); - } - - #[gpui::test] - async fn test_yank_system_clipboard_never(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never) - }); - }); - - cx.set_state( - indoc! {" - The quick brown - fox jˇumps over - the lazy dog"}, - Mode::Normal, - ); - cx.simulate_keystrokes("v i w y"); - cx.assert_state( - indoc! {" - The quick brown - fox ˇjumps over - the lazy dog"}, - Mode::Normal, - ); - cx.simulate_keystrokes("p"); - cx.assert_state( - indoc! {" - The quick brown - fox jjumpˇsumps over - the lazy dog"}, - Mode::Normal, - ); - assert_eq!(cx.read_from_clipboard(), None); - } - - #[gpui::test] - async fn test_yank_system_clipboard_on_yank(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - s.vim.get_or_insert_default().use_system_clipboard = - Some(UseSystemClipboard::OnYank) - }); - }); - - // copy in visual mode - cx.set_state( - indoc! {" - The quick brown - fox jˇumps over - the lazy dog"}, - Mode::Normal, - ); - cx.simulate_keystrokes("v i w y"); - cx.assert_state( - indoc! {" - The quick brown - fox ˇjumps over - the lazy dog"}, - Mode::Normal, - ); - cx.simulate_keystrokes("p"); - cx.assert_state( - indoc! {" - The quick brown - fox jjumpˇsumps over - the lazy dog"}, - Mode::Normal, - ); - assert_eq!( - cx.read_from_clipboard().map(|item| item.text().unwrap()), - Some("jumps".into()) - ); - cx.simulate_keystrokes("d d p"); - cx.assert_state( - indoc! {" - The quick brown - the lazy dog - ˇfox jjumpsumps over"}, - Mode::Normal, - ); - assert_eq!( - cx.read_from_clipboard().map(|item| item.text().unwrap()), - Some("jumps".into()) - ); - cx.write_to_clipboard(ClipboardItem::new_string("test-copy".to_string())); - cx.simulate_keystrokes("shift-p"); - cx.assert_state( - indoc! {" - The quick brown - the lazy dog - test-copˇyfox jjumpsumps over"}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_paste_visual(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // copy in visual mode - cx.set_shared_state(indoc! {" - The quick brown - fox jˇumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("v i w y").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox ˇjumps over - the lazy dog"}); - // paste in visual mode - cx.simulate_shared_keystrokes("w v i w p").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jumps jumpˇs - the lazy dog"}); - cx.shared_clipboard().await.assert_eq("over"); - // paste in visual line mode - cx.simulate_shared_keystrokes("up shift-v shift-p").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇover - fox jumps jumps - the lazy dog"}); - cx.shared_clipboard().await.assert_eq("over"); - // paste in visual block mode - cx.simulate_shared_keystrokes("ctrl-v down down p").await; - cx.shared_state().await.assert_eq(indoc! {" - oveˇrver - overox jumps jumps - overhe lazy dog"}); - - // copy in visual line mode - cx.set_shared_state(indoc! {" - The quick brown - fox juˇmps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("shift-v d").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - the laˇzy dog"}); - // paste in visual mode - cx.simulate_shared_keystrokes("v i w p").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - the• - ˇfox jumps over - dog"}); - cx.shared_clipboard().await.assert_eq("lazy"); - cx.set_shared_state(indoc! {" - The quick brown - fox juˇmps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("shift-v d").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - the laˇzy dog"}); - cx.shared_clipboard().await.assert_eq("fox jumps over\n"); - // paste in visual line mode - cx.simulate_shared_keystrokes("k shift-v p").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇfox jumps over - the lazy dog"}); - cx.shared_clipboard().await.assert_eq("The quick brown\n"); - - // Copy line and paste in visual mode, with cursor on newline character. - cx.set_shared_state(indoc! {" - ˇThe quick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("y y shift-v j $ p").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇThe quick brown - the lazy dog"}); - } - - #[gpui::test] - async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - // copy in visual block mode - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("ctrl-v 2 j y").await; - cx.shared_clipboard().await.assert_eq("q\nj\nl"); - cx.simulate_shared_keystrokes("p").await; - cx.shared_state().await.assert_eq(indoc! {" - The qˇquick brown - fox jjumps over - the llazy dog"}); - cx.simulate_shared_keystrokes("v i w shift-p").await; - cx.shared_state().await.assert_eq(indoc! {" - The ˇq brown - fox jjjumps over - the lllazy dog"}); - cx.simulate_shared_keystrokes("v i w shift-p").await; - - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("ctrl-v j y").await; - cx.shared_clipboard().await.assert_eq("q\nj"); - cx.simulate_shared_keystrokes("l ctrl-v 2 j shift-p").await; - cx.shared_state().await.assert_eq(indoc! {" - The qˇqick brown - fox jjmps over - the lzy dog"}); - - cx.simulate_shared_keystrokes("shift-v p").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇq - j - fox jjmps over - the lzy dog"}); - } - - #[gpui::test] - async fn test_paste_indent(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new_typescript(cx).await; - - cx.set_state( - indoc! {" - class A {ˇ - } - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("o a ( ) { escape"); - cx.assert_state( - indoc! {" - class A { - a()ˇ{} - } - "}, - Mode::Normal, - ); - // cursor goes to the first non-blank character in the line; - cx.simulate_keystrokes("y y p"); - cx.assert_state( - indoc! {" - class A { - a(){} - ˇa(){} - } - "}, - Mode::Normal, - ); - // indentation is preserved when pasting - cx.simulate_keystrokes("u shift-v up y shift-p"); - cx.assert_state( - indoc! {" - ˇclass A { - a(){} - class A { - a(){} - } - "}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_paste_auto_indent(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - mod some_module { - ˇfn main() { - } - } - "}, - Mode::Normal, - ); - // default auto indentation - cx.simulate_keystrokes("y y p"); - cx.assert_state( - indoc! {" - mod some_module { - fn main() { - ˇfn main() { - } - } - "}, - Mode::Normal, - ); - // back to previous state - cx.simulate_keystrokes("u u"); - cx.assert_state( - indoc! {" - mod some_module { - ˇfn main() { - } - } - "}, - Mode::Normal, - ); - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| { - settings.project.all_languages.languages.0.insert( - LanguageName::new_static("Rust").0, - LanguageSettingsContent { - auto_indent_on_paste: Some(false), - ..Default::default() - }, - ); - }); - }); - // auto indentation turned off - cx.simulate_keystrokes("y y p"); - cx.assert_state( - indoc! {" - mod some_module { - fn main() { - ˇfn main() { - } - } - "}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_paste_count(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - onˇe - two - three - "}) - .await; - cx.simulate_shared_keystrokes("y y 3 p").await; - cx.shared_state().await.assert_eq(indoc! {" - one - ˇone - one - one - two - three - "}); - - cx.set_shared_state(indoc! {" - one - ˇtwo - three - "}) - .await; - cx.simulate_shared_keystrokes("y $ $ 3 p").await; - cx.shared_state().await.assert_eq(indoc! {" - one - twotwotwotwˇo - three - "}); - } - - #[gpui::test] - async fn test_paste_system_clipboard_never(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never) - }); - }); - - cx.set_state( - indoc! {" - ˇThe quick brown - fox jumps over - the lazy dog"}, - Mode::Normal, - ); - - cx.write_to_clipboard(ClipboardItem::new_string("something else".to_string())); - - cx.simulate_keystrokes("d d"); - cx.assert_state( - indoc! {" - ˇfox jumps over - the lazy dog"}, - Mode::Normal, - ); - - cx.simulate_keystrokes("shift-v p"); - cx.assert_state( - indoc! {" - ˇThe quick brown - the lazy dog"}, - Mode::Normal, - ); - - cx.simulate_keystrokes("shift-v"); - cx.dispatch_action(editor::actions::Paste); - cx.assert_state( - indoc! {" - ˇsomething else - the lazy dog"}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_numbered_registers(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never) - }); - }); - - cx.set_shared_state(indoc! {" - The quick brown - fox jˇumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("y y \" 0 p").await; - cx.shared_register('0').await.assert_eq("fox jumps over\n"); - cx.shared_register('"').await.assert_eq("fox jumps over\n"); - - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jumps over - ˇfox jumps over - the lazy dog"}); - cx.simulate_shared_keystrokes("k k d d").await; - cx.shared_register('0').await.assert_eq("fox jumps over\n"); - cx.shared_register('1').await.assert_eq("The quick brown\n"); - cx.shared_register('"').await.assert_eq("The quick brown\n"); - - cx.simulate_shared_keystrokes("d d shift-g d d").await; - cx.shared_register('0').await.assert_eq("fox jumps over\n"); - cx.shared_register('3').await.assert_eq("The quick brown\n"); - cx.shared_register('2').await.assert_eq("fox jumps over\n"); - cx.shared_register('1').await.assert_eq("the lazy dog\n"); - - cx.shared_state().await.assert_eq(indoc! {" - ˇfox jumps over"}); - - cx.simulate_shared_keystrokes("d d \" 3 p p \" 1 p").await; - cx.set_shared_state(indoc! {" - The quick brown - fox jumps over - ˇthe lazy dog"}) - .await; - } - - #[gpui::test] - async fn test_named_registers(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never) - }); - }); - - cx.set_shared_state(indoc! {" - The quick brown - fox jˇumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("\" a d a w").await; - cx.shared_register('a').await.assert_eq("jumps "); - cx.simulate_shared_keystrokes("\" shift-a d i w").await; - cx.shared_register('a').await.assert_eq("jumps over"); - cx.shared_register('"').await.assert_eq("jumps over"); - cx.simulate_shared_keystrokes("\" a p").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jumps oveˇr - the lazy dog"}); - cx.simulate_shared_keystrokes("\" a d a w").await; - cx.shared_register('a').await.assert_eq(" over"); - } - - #[gpui::test] - async fn test_special_registers(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never) - }); - }); - - cx.set_shared_state(indoc! {" - The quick brown - fox jˇumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d i w").await; - cx.shared_register('-').await.assert_eq("jumps"); - cx.simulate_shared_keystrokes("\" _ d d").await; - cx.shared_register('_').await.assert_eq(""); - - cx.simulate_shared_keystrokes("shift-v \" _ y w").await; - cx.shared_register('"').await.assert_eq("jumps"); - - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - the ˇlazy dog"}); - cx.simulate_shared_keystrokes("\" \" d ^").await; - cx.shared_register('0').await.assert_eq("the "); - cx.shared_register('"').await.assert_eq("the "); - - cx.simulate_shared_keystrokes("^ \" + d $").await; - cx.shared_clipboard().await.assert_eq("lazy dog"); - cx.shared_register('"').await.assert_eq("lazy dog"); - - cx.simulate_shared_keystrokes("/ d o g enter").await; - cx.shared_register('/').await.assert_eq("dog"); - cx.simulate_shared_keystrokes("\" / shift-p").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - doˇg"}); - - // not testing nvim as it doesn't have a filename - cx.simulate_keystrokes("\" % p"); - #[cfg(not(target_os = "windows"))] - cx.assert_state( - indoc! {" - The quick brown - dogdir/file.rˇs"}, - Mode::Normal, - ); - #[cfg(target_os = "windows")] - cx.assert_state( - indoc! {" - The quick brown - dogdir\\file.rˇs"}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_multicursor_paste(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never) - }); - }); - - cx.set_state( - indoc! {" - ˇfish one - fish two - fish red - fish blue - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("4 g l w escape d i w 0 shift-p"); - cx.assert_state( - indoc! {" - onˇefish• - twˇofish• - reˇdfish• - bluˇefish• - "}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_replace_with_register(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - ˇfish one - two three - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("y i w"); - cx.simulate_keystrokes("w"); - cx.simulate_keystrokes("g shift-r i w"); - cx.assert_state( - indoc! {" - fish fisˇh - two three - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("j b g shift-r e"); - cx.assert_state( - indoc! {" - fish fish - two fisˇh - "}, - Mode::Normal, - ); - let clipboard: Register = cx.read_from_clipboard().unwrap().into(); - assert_eq!(clipboard.text, "fish"); - - cx.set_state( - indoc! {" - ˇfish one - two three - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("y i w"); - cx.simulate_keystrokes("w"); - cx.simulate_keystrokes("v i w g shift-r"); - cx.assert_state( - indoc! {" - fish fisˇh - two three - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("g shift-r r"); - cx.assert_state( - indoc! {" - fisˇh - two three - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("j w g shift-r $"); - cx.assert_state( - indoc! {" - fish - two fisˇh - "}, - Mode::Normal, - ); - let clipboard: Register = cx.read_from_clipboard().unwrap().into(); - assert_eq!(clipboard.text, "fish"); - } - - #[gpui::test] - async fn test_replace_with_register_dot_repeat(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - ˇfish one - two three - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("y i w"); - cx.simulate_keystrokes("w"); - cx.simulate_keystrokes("g shift-r i w"); - cx.assert_state( - indoc! {" - fish fisˇh - two three - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("j ."); - cx.assert_state( - indoc! {" - fish fish - two fisˇh - "}, - Mode::Normal, - ); - } -} diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs deleted file mode 100644 index e47b2b350f..0000000000 --- a/crates/vim/src/normal/repeat.rs +++ /dev/null @@ -1,911 +0,0 @@ -use std::{cell::RefCell, rc::Rc}; - -use crate::{ - Vim, - insert::NormalBefore, - motion::Motion, - normal::InsertBefore, - state::{Mode, Operator, RecordedSelection, ReplayableAction, VimGlobals}, -}; -use editor::Editor; -use gpui::{Action, App, Context, Window, actions}; -use workspace::Workspace; - -actions!( - vim, - [ - /// Repeats the last change. - Repeat, - /// Ends the repeat recording. - EndRepeat, - /// Toggles macro recording. - ToggleRecord, - /// Replays the last recorded macro. - ReplayLastRecording - ] -); - -fn should_replay(action: &dyn Action) -> bool { - // skip so that we don't leave the character palette open - if editor::actions::ShowCharacterPalette.partial_eq(action) { - return false; - } - true -} - -fn repeatable_insert(action: &ReplayableAction) -> Option> { - match action { - ReplayableAction::Action(action) => { - if super::InsertBefore.partial_eq(&**action) - || super::InsertAfter.partial_eq(&**action) - || super::InsertFirstNonWhitespace.partial_eq(&**action) - || super::InsertEndOfLine.partial_eq(&**action) - { - Some(super::InsertBefore.boxed_clone()) - } else if super::InsertLineAbove.partial_eq(&**action) - || super::InsertLineBelow.partial_eq(&**action) - { - Some(super::InsertLineBelow.boxed_clone()) - } else if crate::replace::ToggleReplace.partial_eq(&**action) { - Some(crate::replace::ToggleReplace.boxed_clone()) - } else { - None - } - } - ReplayableAction::Insertion { .. } => None, - } -} - -pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, |vim, _: &EndRepeat, window, cx| { - Vim::globals(cx).dot_replaying = false; - vim.switch_mode(Mode::Normal, false, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &Repeat, window, cx| { - vim.repeat(false, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &ToggleRecord, window, cx| { - let globals = Vim::globals(cx); - if let Some(char) = globals.recording_register.take() { - globals.last_recorded_register = Some(char) - } else { - vim.push_operator(Operator::RecordRegister, window, cx); - } - }); - - Vim::action(editor, cx, |vim, _: &ReplayLastRecording, window, cx| { - let Some(register) = Vim::globals(cx).last_recorded_register else { - return; - }; - vim.replay_register(register, window, cx) - }); -} - -pub struct ReplayerState { - actions: Vec, - running: bool, - ix: usize, -} - -#[derive(Clone)] -pub struct Replayer(Rc>); - -impl Replayer { - pub fn new() -> Self { - Self(Rc::new(RefCell::new(ReplayerState { - actions: vec![], - running: false, - ix: 0, - }))) - } - - pub fn replay(&mut self, actions: Vec, window: &mut Window, cx: &mut App) { - let mut lock = self.0.borrow_mut(); - let range = lock.ix..lock.ix; - lock.actions.splice(range, actions); - if lock.running { - return; - } - lock.running = true; - let this = self.clone(); - window.defer(cx, move |window, cx| { - this.next(window, cx); - let Some(Some(workspace)) = window.root::() else { - return; - }; - let Some(editor) = workspace - .read(cx) - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - else { - return; - }; - editor.update(cx, |editor, cx| { - editor - .buffer() - .update(cx, |multi, cx| multi.finalize_last_transaction(cx)) - }); - }) - } - - pub fn stop(self) { - self.0.borrow_mut().actions.clear() - } - - pub fn next(self, window: &mut Window, cx: &mut App) { - let mut lock = self.0.borrow_mut(); - let action = if lock.ix < 10000 { - lock.actions.get(lock.ix).cloned() - } else { - log::error!("Aborting replay after 10000 actions"); - None - }; - lock.ix += 1; - drop(lock); - let Some(action) = action else { - Vim::globals(cx).replayer.take(); - return; - }; - match action { - ReplayableAction::Action(action) => { - if should_replay(&*action) { - window.dispatch_action(action.boxed_clone(), cx); - cx.defer(move |cx| Vim::globals(cx).observe_action(action.boxed_clone())); - } - } - ReplayableAction::Insertion { - text, - utf16_range_to_replace, - } => { - let Some(Some(workspace)) = window.root::() else { - return; - }; - let Some(editor) = workspace - .read(cx) - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - else { - return; - }; - editor.update(cx, |editor, cx| { - editor.replay_insert_event(&text, utf16_range_to_replace.clone(), window, cx) - }) - } - } - window.defer(cx, move |window, cx| self.next(window, cx)); - } -} - -impl Vim { - pub(crate) fn record_register( - &mut self, - register: char, - window: &mut Window, - cx: &mut Context, - ) { - let globals = Vim::globals(cx); - globals.recording_register = Some(register); - globals.recordings.remove(®ister); - globals.ignore_current_insertion = true; - self.clear_operator(window, cx) - } - - pub(crate) fn replay_register( - &mut self, - mut register: char, - window: &mut Window, - cx: &mut Context, - ) { - let mut count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - self.clear_operator(window, cx); - - let globals = Vim::globals(cx); - if register == '@' { - let Some(last) = globals.last_replayed_register else { - return; - }; - register = last; - } - let Some(actions) = globals.recordings.get(®ister) else { - return; - }; - - let mut repeated_actions = vec![]; - while count > 0 { - repeated_actions.extend(actions.iter().cloned()); - count -= 1 - } - - globals.last_replayed_register = Some(register); - let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone(); - replayer.replay(repeated_actions, window, cx); - } - - pub(crate) fn repeat( - &mut self, - from_insert_mode: bool, - window: &mut Window, - cx: &mut Context, - ) { - if self.active_operator().is_some() { - Vim::update_globals(cx, |globals, _| { - globals.recording_actions.clear(); - globals.recording_count = None; - globals.dot_recording = false; - globals.stop_recording_after_next_action = false; - }); - self.clear_operator(window, cx); - return; - } - - Vim::take_forced_motion(cx); - let count = Vim::take_count(cx); - - let Some((mut actions, selection, mode)) = Vim::update_globals(cx, |globals, _| { - let actions = globals.recorded_actions.clone(); - if actions.is_empty() { - return None; - } - if globals.replayer.is_none() - && let Some(recording_register) = globals.recording_register - { - globals - .recordings - .entry(recording_register) - .or_default() - .push(ReplayableAction::Action(Repeat.boxed_clone())); - } - - let mut mode = None; - let selection = globals.recorded_selection.clone(); - match selection { - RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => { - globals.recorded_count = None; - mode = Some(Mode::Visual); - } - RecordedSelection::VisualLine { .. } => { - globals.recorded_count = None; - mode = Some(Mode::VisualLine) - } - RecordedSelection::VisualBlock { .. } => { - globals.recorded_count = None; - mode = Some(Mode::VisualBlock) - } - RecordedSelection::None => { - if let Some(count) = count { - globals.recorded_count = Some(count); - } - } - } - - Some((actions, selection, mode)) - }) else { - return; - }; - if mode != Some(self.mode) { - if let Some(mode) = mode { - self.switch_mode(mode, false, window, cx) - } - - match selection { - RecordedSelection::SingleLine { cols } => { - if cols > 1 { - self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx) - } - } - RecordedSelection::Visual { rows, cols } => { - self.visual_motion( - Motion::Down { - display_lines: false, - }, - Some(rows as usize), - window, - cx, - ); - self.visual_motion( - Motion::StartOfLine { - display_lines: false, - }, - None, - window, - cx, - ); - if cols > 1 { - self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx) - } - } - RecordedSelection::VisualBlock { rows, cols } => { - self.visual_motion( - Motion::Down { - display_lines: false, - }, - Some(rows as usize), - window, - cx, - ); - if cols > 1 { - self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx); - } - } - RecordedSelection::VisualLine { rows } => { - self.visual_motion( - Motion::Down { - display_lines: false, - }, - Some(rows as usize), - window, - cx, - ); - } - RecordedSelection::None => {} - } - } - - // insert internally uses repeat to handle counts - // vim doesn't treat 3a1 as though you literally repeated a1 - // 3 times, instead it inserts the content thrice at the insert position. - if let Some(to_repeat) = repeatable_insert(&actions[0]) { - if let Some(ReplayableAction::Action(action)) = actions.last() - && NormalBefore.partial_eq(&**action) - { - actions.pop(); - } - - let mut new_actions = actions.clone(); - actions[0] = ReplayableAction::Action(to_repeat.boxed_clone()); - - let mut count = cx.global::().recorded_count.unwrap_or(1); - - // if we came from insert mode we're just doing repetitions 2 onwards. - if from_insert_mode { - count -= 1; - new_actions[0] = actions[0].clone(); - } - - for _ in 1..count { - new_actions.append(actions.clone().as_mut()); - } - new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone())); - actions = new_actions; - } - - actions.push(ReplayableAction::Action(EndRepeat.boxed_clone())); - - if self.temp_mode { - self.temp_mode = false; - actions.push(ReplayableAction::Action(InsertBefore.boxed_clone())); - } - - let globals = Vim::globals(cx); - globals.dot_replaying = true; - let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone(); - - replayer.replay(actions, window, cx); - } -} - -#[cfg(test)] -mod test { - use editor::test::editor_lsp_test_context::EditorLspTestContext; - use futures::StreamExt; - use indoc::indoc; - - use gpui::EntityInputHandler; - - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - - #[gpui::test] - async fn test_dot_repeat(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // "o" - cx.set_shared_state("ˇhello").await; - cx.simulate_shared_keystrokes("o w o r l d escape").await; - cx.shared_state().await.assert_eq("hello\nworlˇd"); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("hello\nworld\nworlˇd"); - - // "d" - cx.simulate_shared_keystrokes("^ d f o").await; - cx.simulate_shared_keystrokes("g g .").await; - cx.shared_state().await.assert_eq("ˇ\nworld\nrld"); - - // "p" (note that it pastes the current clipboard) - cx.simulate_shared_keystrokes("j y y p").await; - cx.simulate_shared_keystrokes("shift-g y y .").await; - cx.shared_state() - .await - .assert_eq("\nworld\nworld\nrld\nˇrld"); - - // "~" (note that counts apply to the action taken, not . itself) - cx.set_shared_state("ˇthe quick brown fox").await; - cx.simulate_shared_keystrokes("2 ~ .").await; - cx.set_shared_state("THE ˇquick brown fox").await; - cx.simulate_shared_keystrokes("3 .").await; - cx.set_shared_state("THE QUIˇck brown fox").await; - cx.run_until_parked(); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("THE QUICK ˇbrown fox"); - } - - #[gpui::test] - async fn test_repeat_ime(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("hˇllo", Mode::Normal); - cx.simulate_keystrokes("i"); - - // simulate brazilian input for ä. - cx.update_editor(|editor, window, cx| { - editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), window, cx); - editor.replace_text_in_range(None, "ä", window, cx); - }); - cx.simulate_keystrokes("escape"); - cx.assert_state("hˇällo", Mode::Normal); - cx.simulate_keystrokes("."); - cx.assert_state("hˇäällo", Mode::Normal); - } - - #[gpui::test] - async fn test_repeat_completion(cx: &mut gpui::TestAppContext) { - VimTestContext::init(cx); - let cx = EditorLspTestContext::new_rust( - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), ":".to_string()]), - resolve_provider: Some(true), - ..Default::default() - }), - ..Default::default() - }, - cx, - ) - .await; - let mut cx = VimTestContext::new_with_lsp(cx, true); - - cx.set_state( - indoc! {" - onˇe - two - three - "}, - Mode::Normal, - ); - - let mut request = cx.set_request_handler::( - move |_, params, _| async move { - let position = params.text_document_position.position; - Ok(Some(lsp::CompletionResponse::Array(vec![ - lsp::CompletionItem { - label: "first".to_string(), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range::new(position, position), - new_text: "first".to_string(), - })), - ..Default::default() - }, - lsp::CompletionItem { - label: "second".to_string(), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range::new(position, position), - new_text: "second".to_string(), - })), - ..Default::default() - }, - ]))) - }, - ); - cx.simulate_keystrokes("a ."); - request.next().await; - cx.condition(|editor, _| editor.context_menu_visible()) - .await; - cx.simulate_keystrokes("down enter ! escape"); - - cx.assert_state( - indoc! {" - one.secondˇ! - two - three - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("j ."); - cx.assert_state( - indoc! {" - one.second! - two.secondˇ! - three - "}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_repeat_completion_unicode_bug(cx: &mut gpui::TestAppContext) { - VimTestContext::init(cx); - let cx = EditorLspTestContext::new_rust( - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), ":".to_string()]), - resolve_provider: Some(true), - ..Default::default() - }), - ..Default::default() - }, - cx, - ) - .await; - let mut cx = VimTestContext::new_with_lsp(cx, true); - - cx.set_state( - indoc! {" - ĩлˇк - ĩлк - "}, - Mode::Normal, - ); - - let mut request = cx.set_request_handler::( - move |_, params, _| async move { - let position = params.text_document_position.position; - let mut to_the_left = position; - to_the_left.character -= 2; - Ok(Some(lsp::CompletionResponse::Array(vec![ - lsp::CompletionItem { - label: "oops".to_string(), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range::new(to_the_left, position), - new_text: "к!".to_string(), - })), - ..Default::default() - }, - ]))) - }, - ); - cx.simulate_keystrokes("i ."); - request.next().await; - cx.condition(|editor, _| editor.context_menu_visible()) - .await; - cx.simulate_keystrokes("enter escape"); - cx.assert_state( - indoc! {" - ĩкˇ!к - ĩлк - "}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_repeat_visual(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // single-line (3 columns) - cx.set_shared_state(indoc! { - "ˇthe quick brown - fox jumps over - the lazy dog" - }) - .await; - cx.simulate_shared_keystrokes("v i w s o escape").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇo quick brown - fox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes("j w .").await; - cx.shared_state().await.assert_eq(indoc! { - "o quick brown - fox ˇops over - the lazy dog" - }); - cx.simulate_shared_keystrokes("f r .").await; - cx.shared_state().await.assert_eq(indoc! { - "o quick brown - fox ops oveˇothe lazy dog" - }); - - // visual - cx.set_shared_state(indoc! { - "the ˇquick brown - fox jumps over - fox jumps over - fox jumps over - the lazy dog" - }) - .await; - cx.simulate_shared_keystrokes("v j x").await; - cx.shared_state().await.assert_eq(indoc! { - "the ˇumps over - fox jumps over - fox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq(indoc! { - "the ˇumps over - fox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes("w .").await; - cx.shared_state().await.assert_eq(indoc! { - "the umps ˇumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes("j .").await; - cx.shared_state().await.assert_eq(indoc! { - "the umps umps over - the ˇog" - }); - - // block mode (3 rows) - cx.set_shared_state(indoc! { - "ˇthe quick brown - fox jumps over - the lazy dog" - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v j j shift-i o escape") - .await; - cx.shared_state().await.assert_eq(indoc! { - "ˇothe quick brown - ofox jumps over - othe lazy dog" - }); - cx.simulate_shared_keystrokes("j 4 l .").await; - cx.shared_state().await.assert_eq(indoc! { - "othe quick brown - ofoxˇo jumps over - otheo lazy dog" - }); - - // line mode - cx.set_shared_state(indoc! { - "ˇthe quick brown - fox jumps over - the lazy dog" - }) - .await; - cx.simulate_shared_keystrokes("shift-v shift-r o escape") - .await; - cx.shared_state().await.assert_eq(indoc! { - "ˇo - fox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes("j .").await; - cx.shared_state().await.assert_eq(indoc! { - "o - ˇo - the lazy dog" - }); - } - - #[gpui::test] - async fn test_repeat_motion_counts(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "ˇthe quick brown - fox jumps over - the lazy dog" - }) - .await; - cx.simulate_shared_keystrokes("3 d 3 l").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇ brown - fox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes("j .").await; - cx.shared_state().await.assert_eq(indoc! { - " brown - ˇ over - the lazy dog" - }); - cx.simulate_shared_keystrokes("j 2 .").await; - cx.shared_state().await.assert_eq(indoc! { - " brown - over - ˇe lazy dog" - }); - } - - #[gpui::test] - async fn test_record_interrupted(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇhello\n", Mode::Normal); - cx.simulate_keystrokes("4 i j cmd-shift-p escape"); - cx.simulate_keystrokes("escape"); - cx.assert_state("ˇjhello\n", Mode::Normal); - } - - #[gpui::test] - async fn test_repeat_over_blur(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello hello hello\n").await; - cx.simulate_shared_keystrokes("c f o x escape").await; - cx.shared_state().await.assert_eq("ˇx hello hello\n"); - cx.simulate_shared_keystrokes(": escape").await; - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("ˇx hello\n"); - } - - #[gpui::test] - async fn test_undo_repeated_insert(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("hellˇo").await; - cx.simulate_shared_keystrokes("3 a . escape").await; - cx.shared_state().await.assert_eq("hello..ˇ."); - cx.simulate_shared_keystrokes("u").await; - cx.shared_state().await.assert_eq("hellˇo"); - } - - #[gpui::test] - async fn test_record_replay(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello world").await; - cx.simulate_shared_keystrokes("q w c w j escape q").await; - cx.shared_state().await.assert_eq("ˇj world"); - cx.simulate_shared_keystrokes("2 l @ w").await; - cx.shared_state().await.assert_eq("j ˇj"); - } - - #[gpui::test] - async fn test_record_replay_count(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello world!!").await; - cx.simulate_shared_keystrokes("q a v 3 l s 0 escape l q") - .await; - cx.shared_state().await.assert_eq("0ˇo world!!"); - cx.simulate_shared_keystrokes("2 @ a").await; - cx.shared_state().await.assert_eq("000ˇ!"); - } - - #[gpui::test] - async fn test_record_replay_dot(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello world").await; - cx.simulate_shared_keystrokes("q a r a l r b l q").await; - cx.shared_state().await.assert_eq("abˇllo world"); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("abˇblo world"); - cx.simulate_shared_keystrokes("shift-q").await; - cx.shared_state().await.assert_eq("ababˇo world"); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("ababˇb world"); - } - - #[gpui::test] - async fn test_record_replay_of_dot(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello world").await; - cx.simulate_shared_keystrokes("r o q w . q").await; - cx.shared_state().await.assert_eq("ˇoello world"); - cx.simulate_shared_keystrokes("d l").await; - cx.shared_state().await.assert_eq("ˇello world"); - cx.simulate_shared_keystrokes("@ w").await; - cx.shared_state().await.assert_eq("ˇllo world"); - } - - #[gpui::test] - async fn test_record_replay_interleaved(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello world").await; - cx.simulate_shared_keystrokes("q z r a l q").await; - cx.shared_state().await.assert_eq("aˇello world"); - cx.simulate_shared_keystrokes("q b @ z @ z q").await; - cx.shared_state().await.assert_eq("aaaˇlo world"); - cx.simulate_shared_keystrokes("@ @").await; - cx.shared_state().await.assert_eq("aaaaˇo world"); - cx.simulate_shared_keystrokes("@ b").await; - cx.shared_state().await.assert_eq("aaaaaaˇworld"); - cx.simulate_shared_keystrokes("@ @").await; - cx.shared_state().await.assert_eq("aaaaaaaˇorld"); - cx.simulate_shared_keystrokes("q z r b l q").await; - cx.shared_state().await.assert_eq("aaaaaaabˇrld"); - cx.simulate_shared_keystrokes("@ b").await; - cx.shared_state().await.assert_eq("aaaaaaabbbˇd"); - } - - #[gpui::test] - async fn test_repeat_clear(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Check that, when repeat is preceded by something other than a number, - // the current operator is cleared, in order to prevent infinite loops. - cx.set_state("ˇhello world", Mode::Normal); - cx.simulate_keystrokes("d ."); - assert_eq!(cx.active_operator(), None); - } - - #[gpui::test] - async fn test_repeat_clear_repeat(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "ˇthe quick brown - fox jumps over - the lazy dog" - }) - .await; - cx.simulate_shared_keystrokes("d d").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇfox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes("d . .").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇthe lazy dog" - }); - } - - #[gpui::test] - async fn test_repeat_clear_count(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "ˇthe quick brown - fox jumps over - the lazy dog" - }) - .await; - cx.simulate_shared_keystrokes("d d").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇfox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes("2 d .").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇfox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇthe lazy dog" - }); - - cx.set_shared_state(indoc! { - "ˇthe quick brown - fox jumps over - the lazy dog - the quick brown - fox jumps over - the lazy dog" - }) - .await; - cx.simulate_shared_keystrokes("2 d d").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇthe lazy dog - the quick brown - fox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes("5 d .").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇthe lazy dog - the quick brown - fox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇfox jumps over - the lazy dog" - }); - } -} diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs deleted file mode 100644 index 73209c8873..0000000000 --- a/crates/vim/src/normal/scroll.rs +++ /dev/null @@ -1,568 +0,0 @@ -use crate::Vim; -use editor::{ - DisplayPoint, Editor, EditorSettings, SelectionEffects, - display_map::{DisplayRow, ToDisplayPoint}, - scroll::ScrollAmount, -}; -use gpui::{Context, Window, actions}; -use language::Bias; -use settings::Settings; -use text::SelectionGoal; - -actions!( - vim, - [ - /// Scrolls up by one line. - LineUp, - /// Scrolls down by one line. - LineDown, - /// Scrolls right by one column. - ColumnRight, - /// Scrolls left by one column. - ColumnLeft, - /// Scrolls up by half a page. - ScrollUp, - /// Scrolls down by half a page. - ScrollDown, - /// Scrolls up by one page. - PageUp, - /// Scrolls down by one page. - PageDown, - /// Scrolls right by half a page's width. - HalfPageRight, - /// Scrolls left by half a page's width. - HalfPageLeft, - ] -); - -pub fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, |vim, _: &LineDown, window, cx| { - vim.scroll(false, window, cx, |c| ScrollAmount::Line(c.unwrap_or(1.))) - }); - Vim::action(editor, cx, |vim, _: &LineUp, window, cx| { - vim.scroll(false, window, cx, |c| ScrollAmount::Line(-c.unwrap_or(1.))) - }); - Vim::action(editor, cx, |vim, _: &ColumnRight, window, cx| { - vim.scroll(false, window, cx, |c| ScrollAmount::Column(c.unwrap_or(1.))) - }); - Vim::action(editor, cx, |vim, _: &ColumnLeft, window, cx| { - vim.scroll(false, window, cx, |c| { - ScrollAmount::Column(-c.unwrap_or(1.)) - }) - }); - Vim::action(editor, cx, |vim, _: &PageDown, window, cx| { - vim.scroll(false, window, cx, |c| ScrollAmount::Page(c.unwrap_or(1.))) - }); - Vim::action(editor, cx, |vim, _: &PageUp, window, cx| { - vim.scroll(false, window, cx, |c| ScrollAmount::Page(-c.unwrap_or(1.))) - }); - Vim::action(editor, cx, |vim, _: &HalfPageRight, window, cx| { - vim.scroll(false, window, cx, |c| { - ScrollAmount::PageWidth(c.unwrap_or(0.5)) - }) - }); - Vim::action(editor, cx, |vim, _: &HalfPageLeft, window, cx| { - vim.scroll(false, window, cx, |c| { - ScrollAmount::PageWidth(-c.unwrap_or(0.5)) - }) - }); - Vim::action(editor, cx, |vim, _: &ScrollDown, window, cx| { - vim.scroll(true, window, cx, |c| { - if let Some(c) = c { - ScrollAmount::Line(c) - } else { - ScrollAmount::Page(0.5) - } - }) - }); - Vim::action(editor, cx, |vim, _: &ScrollUp, window, cx| { - vim.scroll(true, window, cx, |c| { - if let Some(c) = c { - ScrollAmount::Line(-c) - } else { - ScrollAmount::Page(-0.5) - } - }) - }); -} - -impl Vim { - fn scroll( - &mut self, - move_cursor: bool, - window: &mut Window, - cx: &mut Context, - by: fn(c: Option) -> ScrollAmount, - ) { - let amount = by(Vim::take_count(cx).map(|c| c as f32)); - Vim::take_forced_motion(cx); - self.exit_temporary_normal(window, cx); - self.update_editor(cx, |_, editor, cx| { - scroll_editor(editor, move_cursor, amount, window, cx) - }); - } -} - -fn scroll_editor( - editor: &mut Editor, - preserve_cursor_position: bool, - amount: ScrollAmount, - window: &mut Window, - cx: &mut Context, -) { - let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq(); - let old_top_anchor = editor.scroll_manager.anchor().anchor; - - if editor.scroll_hover(amount, window, cx) { - return; - } - - let full_page_up = amount.is_full_page() && amount.direction().is_upwards(); - let amount = match (amount.is_full_page(), editor.visible_line_count()) { - (true, Some(visible_line_count)) => { - if amount.direction().is_upwards() { - ScrollAmount::Line((amount.lines(visible_line_count) + 1.0) as f32) - } else { - ScrollAmount::Line((amount.lines(visible_line_count) - 1.0) as f32) - } - } - _ => amount, - }; - - editor.scroll_screen(&amount, window, cx); - if !should_move_cursor { - return; - } - - let Some(visible_line_count) = editor.visible_line_count() else { - return; - }; - - let Some(visible_column_count) = editor.visible_column_count() else { - return; - }; - - let top_anchor = editor.scroll_manager.anchor().anchor; - let vertical_scroll_margin = EditorSettings::get_global(cx).vertical_scroll_margin; - - editor.change_selections( - SelectionEffects::no_scroll().nav_history(false), - window, - cx, - |s| { - s.move_with(|map, selection| { - // TODO: Improve the logic and function calls below to be dependent on - // the `amount`. If the amount is vertical, we don't care about - // columns, while if it's horizontal, we don't care about rows, - // so we don't need to calculate both and deal with logic for - // both. - let mut head = selection.head(); - let top = top_anchor.to_display_point(map); - let max_point = map.max_point(); - let starting_column = head.column(); - - let vertical_scroll_margin = - (vertical_scroll_margin as u32).min(visible_line_count as u32 / 2); - - if preserve_cursor_position { - let old_top = old_top_anchor.to_display_point(map); - let new_row = if old_top.row() == top.row() { - DisplayRow( - head.row() - .0 - .saturating_add_signed(amount.lines(visible_line_count) as i32), - ) - } else { - DisplayRow(top.row().0 + selection.head().row().0 - old_top.row().0) - }; - head = map.clip_point(DisplayPoint::new(new_row, head.column()), Bias::Left) - } - - let min_row = if top.row().0 == 0 { - DisplayRow(0) - } else { - DisplayRow(top.row().0 + vertical_scroll_margin) - }; - - let max_visible_row = top.row().0.saturating_add( - (visible_line_count as u32).saturating_sub(1 + vertical_scroll_margin), - ); - // scroll off the end. - let max_row = if top.row().0 + visible_line_count as u32 >= max_point.row().0 { - max_point.row() - } else { - DisplayRow( - (top.row().0 + visible_line_count as u32) - .saturating_sub(1 + vertical_scroll_margin), - ) - }; - - let new_row = if full_page_up { - // Special-casing ctrl-b/page-up, which is special-cased by Vim, it seems - // to always put the cursor on the last line of the page, even if the cursor - // was before that. - DisplayRow(max_visible_row) - } else if head.row() < min_row { - min_row - } else if head.row() > max_row { - max_row - } else { - head.row() - }; - - // The minimum column position that the cursor position can be - // at is either the scroll manager's anchor column, which is the - // left-most column in the visible area, or the scroll manager's - // old anchor column, in case the cursor position is being - // preserved. This is necessary for motions like `ctrl-d` in - // case there's not enough content to scroll half page down, in - // which case the scroll manager's anchor column will be the - // maximum column for the current line, so the minimum column - // would end up being the same as the maximum column. - let min_column = match preserve_cursor_position { - true => old_top_anchor.to_display_point(map).column(), - false => top.column(), - }; - - // As for the maximum column position, that should be either the - // right-most column in the visible area, which we can easily - // calculate by adding the visible column count to the minimum - // column position, or the right-most column in the current - // line, seeing as the cursor might be in a short line, in which - // case we don't want to go past its last column. - let max_row_column = if new_row <= map.max_point().row() { - map.line_len(new_row) - } else { - 0 - }; - let max_column = match min_column + visible_column_count as u32 { - max_column if max_column >= max_row_column => max_row_column, - max_column => max_column, - }; - - // Ensure that the cursor's column stays within the visible - // area, otherwise clip it at either the left or right edge of - // the visible area. - let new_column = match (min_column, max_column) { - (min_column, _) if starting_column < min_column => min_column, - (_, max_column) if starting_column > max_column => max_column, - _ => starting_column, - }; - - let new_head = map.clip_point(DisplayPoint::new(new_row, new_column), Bias::Left); - let goal = match amount { - ScrollAmount::Column(_) | ScrollAmount::PageWidth(_) => SelectionGoal::None, - _ => selection.goal, - }; - - if selection.is_empty() { - selection.collapse_to(new_head, goal) - } else { - selection.set_head(new_head, goal) - }; - }) - }, - ); -} - -#[cfg(test)] -mod test { - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - use editor::ScrollBeyondLastLine; - use gpui::{AppContext as _, point, px, size}; - use indoc::indoc; - use language::Point; - use settings::SettingsStore; - - pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String { - let mut text = String::new(); - for row in 0..rows { - let c: char = (start_char as u32 + row as u32) as u8 as char; - let mut line = c.to_string().repeat(cols); - if row < rows - 1 { - line.push('\n'); - } - text += &line; - } - text - } - - #[gpui::test] - async fn test_scroll(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - let (line_height, visible_line_count) = cx.update_editor(|editor, window, cx| { - ( - editor - .style(cx) - .text - .line_height_in_pixels(window.rem_size()), - editor.visible_line_count().unwrap(), - ) - }); - - let window = cx.window; - let margin = cx - .update_window(window, |_, window, _cx| { - window.viewport_size().height - line_height * visible_line_count as f32 - }) - .unwrap(); - cx.simulate_window_resize( - cx.window, - size(px(1000.), margin + 8. * line_height - px(1.0)), - ); - - cx.set_state( - indoc!( - "ˇone - two - three - four - five - six - seven - eight - nine - ten - eleven - twelve - " - ), - Mode::Normal, - ); - - cx.update_editor(|editor, window, cx| { - assert_eq!(editor.snapshot(window, cx).scroll_position(), point(0., 0.)) - }); - cx.simulate_keystrokes("ctrl-e"); - cx.update_editor(|editor, window, cx| { - assert_eq!(editor.snapshot(window, cx).scroll_position(), point(0., 1.)) - }); - cx.simulate_keystrokes("2 ctrl-e"); - cx.update_editor(|editor, window, cx| { - assert_eq!(editor.snapshot(window, cx).scroll_position(), point(0., 3.)) - }); - cx.simulate_keystrokes("ctrl-y"); - cx.update_editor(|editor, window, cx| { - assert_eq!(editor.snapshot(window, cx).scroll_position(), point(0., 2.)) - }); - - // does not select in normal mode - cx.simulate_keystrokes("g g"); - cx.update_editor(|editor, window, cx| { - assert_eq!(editor.snapshot(window, cx).scroll_position(), point(0., 0.)) - }); - cx.simulate_keystrokes("ctrl-d"); - cx.update_editor(|editor, window, cx| { - assert_eq!( - editor.snapshot(window, cx).scroll_position(), - point(0., 3.0) - ); - assert_eq!( - editor - .selections - .newest(&editor.display_snapshot(cx)) - .range(), - Point::new(6, 0)..Point::new(6, 0) - ) - }); - - // does select in visual mode - cx.simulate_keystrokes("g g"); - cx.update_editor(|editor, window, cx| { - assert_eq!(editor.snapshot(window, cx).scroll_position(), point(0., 0.)) - }); - cx.simulate_keystrokes("v ctrl-d"); - cx.update_editor(|editor, window, cx| { - assert_eq!( - editor.snapshot(window, cx).scroll_position(), - point(0., 3.0) - ); - assert_eq!( - editor - .selections - .newest(&editor.display_snapshot(cx)) - .range(), - Point::new(0, 0)..Point::new(6, 1) - ) - }); - } - - #[gpui::test] - async fn test_ctrl_d_u(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_scroll_height(10).await; - - let content = "ˇ".to_owned() + &sample_text(26, 2, 'a'); - cx.set_shared_state(&content).await; - - // skip over the scrolloff at the top - // test ctrl-d - cx.simulate_shared_keystrokes("4 j ctrl-d").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("ctrl-d").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("g g ctrl-d").await; - cx.shared_state().await.assert_matches(); - - // test ctrl-u - cx.simulate_shared_keystrokes("ctrl-u").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("ctrl-d ctrl-d 4 j ctrl-u ctrl-u") - .await; - cx.shared_state().await.assert_matches(); - - // test returning to top - cx.simulate_shared_keystrokes("g g ctrl-d ctrl-u ctrl-u") - .await; - cx.shared_state().await.assert_matches(); - } - - #[gpui::test] - async fn test_ctrl_f_b(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - let visible_lines = 10; - cx.set_scroll_height(visible_lines).await; - - // First test without vertical scroll margin - cx.neovim.set_option(&format!("scrolloff={}", 0)).await; - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| s.editor.vertical_scroll_margin = Some(0.0)); - }); - - let content = "ˇ".to_owned() + &sample_text(26, 2, 'a'); - cx.set_shared_state(&content).await; - - // scroll down: ctrl-f - cx.simulate_shared_keystrokes("ctrl-f").await; - cx.shared_state().await.assert_matches(); - - cx.simulate_shared_keystrokes("ctrl-f").await; - cx.shared_state().await.assert_matches(); - - // scroll up: ctrl-b - cx.simulate_shared_keystrokes("ctrl-b").await; - cx.shared_state().await.assert_matches(); - - cx.simulate_shared_keystrokes("ctrl-b").await; - cx.shared_state().await.assert_matches(); - - // Now go back to start of file, and test with vertical scroll margin - cx.simulate_shared_keystrokes("g g").await; - cx.shared_state().await.assert_matches(); - - cx.neovim.set_option(&format!("scrolloff={}", 3)).await; - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| s.editor.vertical_scroll_margin = Some(3.0)); - }); - - // scroll down: ctrl-f - cx.simulate_shared_keystrokes("ctrl-f").await; - cx.shared_state().await.assert_matches(); - - cx.simulate_shared_keystrokes("ctrl-f").await; - cx.shared_state().await.assert_matches(); - - // scroll up: ctrl-b - cx.simulate_shared_keystrokes("ctrl-b").await; - cx.shared_state().await.assert_matches(); - - cx.simulate_shared_keystrokes("ctrl-b").await; - cx.shared_state().await.assert_matches(); - } - - #[gpui::test] - async fn test_scroll_beyond_last_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_scroll_height(10).await; - - let content = "ˇ".to_owned() + &sample_text(26, 2, 'a'); - cx.set_shared_state(&content).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - s.editor.scroll_beyond_last_line = Some(ScrollBeyondLastLine::Off); - }); - }); - - // ctrl-d can reach the end and the cursor stays in the first column - cx.simulate_shared_keystrokes("shift-g k").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("ctrl-d").await; - cx.shared_state().await.assert_matches(); - - // ctrl-u from the last line - cx.simulate_shared_keystrokes("shift-g").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("ctrl-u").await; - cx.shared_state().await.assert_matches(); - } - - #[gpui::test] - async fn test_ctrl_y_e(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_scroll_height(10).await; - - let content = "ˇ".to_owned() + &sample_text(26, 2, 'a'); - cx.set_shared_state(&content).await; - - for _ in 0..8 { - cx.simulate_shared_keystrokes("ctrl-e").await; - cx.shared_state().await.assert_matches(); - } - - for _ in 0..8 { - cx.simulate_shared_keystrokes("ctrl-y").await; - cx.shared_state().await.assert_matches(); - } - } - - #[gpui::test] - async fn test_scroll_jumps(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_scroll_height(20).await; - - let content = "ˇ".to_owned() + &sample_text(52, 2, 'a'); - cx.set_shared_state(&content).await; - - cx.simulate_shared_keystrokes("shift-g g g").await; - cx.simulate_shared_keystrokes("ctrl-d ctrl-d ctrl-o").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("ctrl-o").await; - cx.shared_state().await.assert_matches(); - } - - #[gpui::test] - async fn test_horizontal_scroll(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_scroll_height(20).await; - cx.set_shared_wrap(12).await; - cx.set_neovim_option("nowrap").await; - - let content = "ˇ01234567890123456789"; - cx.set_shared_state(content).await; - - cx.simulate_shared_keystrokes("z shift-l").await; - cx.shared_state().await.assert_eq("012345ˇ67890123456789"); - - // At this point, `z h` should not move the cursor as it should still be - // visible within the 12 column width. - cx.simulate_shared_keystrokes("z h").await; - cx.shared_state().await.assert_eq("012345ˇ67890123456789"); - - let content = "ˇ01234567890123456789"; - cx.set_shared_state(content).await; - - cx.simulate_shared_keystrokes("z l").await; - cx.shared_state().await.assert_eq("0ˇ1234567890123456789"); - } -} diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs deleted file mode 100644 index 36a529da5d..0000000000 --- a/crates/vim/src/normal/search.rs +++ /dev/null @@ -1,1176 +0,0 @@ -use editor::{Editor, EditorSettings}; -use gpui::{Action, Context, Window, actions}; -use language::Point; -use schemars::JsonSchema; -use search::{BufferSearchBar, SearchOptions, buffer_search}; -use serde::Deserialize; -use settings::Settings; -use std::{iter::Peekable, str::Chars}; -use util::serde::default_true; -use workspace::{notifications::NotifyResultExt, searchable::Direction}; - -use crate::{ - Vim, - command::CommandRange, - motion::Motion, - state::{Mode, SearchState}, -}; - -/// Moves to the next search match. -#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub(crate) struct MoveToNext { - #[serde(default = "default_true")] - case_sensitive: bool, - #[serde(default)] - partial_word: bool, - #[serde(default = "default_true")] - regex: bool, -} - -/// Moves to the previous search match. -#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub(crate) struct MoveToPrevious { - #[serde(default = "default_true")] - case_sensitive: bool, - #[serde(default)] - partial_word: bool, - #[serde(default = "default_true")] - regex: bool, -} - -/// Initiates a search operation with the specified parameters. -#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub(crate) struct Search { - #[serde(default)] - backwards: bool, - #[serde(default = "default_true")] - regex: bool, -} - -/// Executes a find command to search for patterns in the buffer. -#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub struct FindCommand { - pub query: String, - pub backwards: bool, -} - -/// Executes a search and replace command within the specified range. -#[derive(Clone, Debug, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -pub struct ReplaceCommand { - pub(crate) range: CommandRange, - pub(crate) replacement: Replacement, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct Replacement { - search: String, - replacement: String, - case_sensitive: Option, - flag_n: bool, - flag_g: bool, - flag_c: bool, -} - -actions!( - vim, - [ - /// Submits the current search query. - SearchSubmit, - /// Moves to the next search match. - MoveToNextMatch, - /// Moves to the previous search match. - MoveToPreviousMatch - ] -); - -pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, Vim::move_to_next); - Vim::action(editor, cx, Vim::move_to_previous); - Vim::action(editor, cx, Vim::move_to_next_match); - Vim::action(editor, cx, Vim::move_to_previous_match); - Vim::action(editor, cx, Vim::search); - Vim::action(editor, cx, Vim::search_deploy); - Vim::action(editor, cx, Vim::find_command); - Vim::action(editor, cx, Vim::replace_command); -} - -impl Vim { - fn move_to_next(&mut self, action: &MoveToNext, window: &mut Window, cx: &mut Context) { - self.move_to_internal( - Direction::Next, - action.case_sensitive, - !action.partial_word, - action.regex, - window, - cx, - ) - } - - fn move_to_previous( - &mut self, - action: &MoveToPrevious, - window: &mut Window, - cx: &mut Context, - ) { - self.move_to_internal( - Direction::Prev, - action.case_sensitive, - !action.partial_word, - action.regex, - window, - cx, - ) - } - - fn move_to_next_match( - &mut self, - _: &MoveToNextMatch, - window: &mut Window, - cx: &mut Context, - ) { - self.move_to_match_internal(self.search.direction, window, cx) - } - - fn move_to_previous_match( - &mut self, - _: &MoveToPreviousMatch, - window: &mut Window, - cx: &mut Context, - ) { - self.move_to_match_internal(self.search.direction.opposite(), window, cx) - } - - fn search(&mut self, action: &Search, window: &mut Window, cx: &mut Context) { - let Some(pane) = self.pane(window, cx) else { - return; - }; - let direction = if action.backwards { - Direction::Prev - } else { - Direction::Next - }; - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - let prior_selections = self.editor_selections(window, cx); - pane.update(cx, |pane, cx| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |search_bar, cx| { - if !search_bar.show(window, cx) { - return; - } - - search_bar.select_query(window, cx); - cx.focus_self(window); - - search_bar.set_replacement(None, cx); - let mut options = SearchOptions::NONE; - if action.regex { - options |= SearchOptions::REGEX; - } - if action.backwards { - options |= SearchOptions::BACKWARDS; - } - if EditorSettings::get_global(cx).search.case_sensitive { - options |= SearchOptions::CASE_SENSITIVE; - } - search_bar.set_search_options(options, cx); - let prior_mode = if self.temp_mode { - Mode::Insert - } else { - self.mode - }; - - self.search = SearchState { - direction, - count, - prior_selections, - prior_operator: self.operator_stack.last().cloned(), - prior_mode, - helix_select: false, - } - }); - } - }) - } - - // hook into the existing to clear out any vim search state on cmd+f or edit -> find. - fn search_deploy(&mut self, _: &buffer_search::Deploy, _: &mut Window, cx: &mut Context) { - // Preserve the current mode when resetting search state - let current_mode = self.mode; - self.search = Default::default(); - self.search.prior_mode = current_mode; - cx.propagate(); - } - - pub fn search_submit(&mut self, window: &mut Window, cx: &mut Context) { - self.store_visual_marks(window, cx); - let Some(pane) = self.pane(window, cx) else { - return; - }; - let new_selections = self.editor_selections(window, cx); - let result = pane.update(cx, |pane, cx| { - let search_bar = pane.toolbar().read(cx).item_of_type::()?; - if self.search.helix_select { - search_bar.update(cx, |search_bar, cx| { - search_bar.select_all_matches(&Default::default(), window, cx) - }); - return None; - } - search_bar.update(cx, |search_bar, cx| { - let mut count = self.search.count; - let direction = self.search.direction; - search_bar.has_active_match(); - let new_head = new_selections.last()?.start; - let is_different_head = self - .search - .prior_selections - .last() - .is_none_or(|range| range.start != new_head); - - if is_different_head { - count = count.saturating_sub(1) - } - self.search.count = 1; - search_bar.select_match(direction, count, window, cx); - search_bar.focus_editor(&Default::default(), window, cx); - - let prior_selections: Vec<_> = self.search.prior_selections.drain(..).collect(); - let prior_mode = self.search.prior_mode; - let prior_operator = self.search.prior_operator.take(); - - let query = search_bar.query(cx).into(); - Vim::globals(cx).registers.insert('/', query); - Some((prior_selections, prior_mode, prior_operator)) - }) - }); - - let Some((mut prior_selections, prior_mode, prior_operator)) = result else { - return; - }; - - let new_selections = self.editor_selections(window, cx); - - // If the active editor has changed during a search, don't panic. - if prior_selections.iter().any(|s| { - self.update_editor(cx, |_, editor, cx| { - !s.start - .is_valid(&editor.snapshot(window, cx).buffer_snapshot()) - }) - .unwrap_or(true) - }) { - prior_selections.clear(); - } - - if prior_mode != self.mode { - self.switch_mode(prior_mode, true, window, cx); - } - if let Some(operator) = prior_operator { - self.push_operator(operator, window, cx); - }; - self.search_motion( - Motion::ZedSearchResult { - prior_selections, - new_selections, - }, - window, - cx, - ); - } - - pub fn move_to_match_internal( - &mut self, - direction: Direction, - window: &mut Window, - cx: &mut Context, - ) { - let Some(pane) = self.pane(window, cx) else { - return; - }; - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - let prior_selections = self.editor_selections(window, cx); - - let success = pane.update(cx, |pane, cx| { - let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() else { - return false; - }; - search_bar.update(cx, |search_bar, cx| { - if !search_bar.has_active_match() || !search_bar.show(window, cx) { - return false; - } - search_bar.select_match(direction, count, window, cx); - true - }) - }); - if !success { - return; - } - - let new_selections = self.editor_selections(window, cx); - self.search_motion( - Motion::ZedSearchResult { - prior_selections, - new_selections, - }, - window, - cx, - ); - } - - pub fn move_to_internal( - &mut self, - direction: Direction, - case_sensitive: bool, - whole_word: bool, - regex: bool, - window: &mut Window, - cx: &mut Context, - ) { - let Some(pane) = self.pane(window, cx) else { - return; - }; - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - let prior_selections = self.editor_selections(window, cx); - let cursor_word = self.editor_cursor_word(window, cx); - let vim = cx.entity(); - - let searched = pane.update(cx, |pane, cx| { - self.search.direction = direction; - let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() else { - return false; - }; - let search = search_bar.update(cx, |search_bar, cx| { - let mut options = SearchOptions::NONE; - if case_sensitive { - options |= SearchOptions::CASE_SENSITIVE; - } - if regex { - options |= SearchOptions::REGEX; - } - if whole_word { - options |= SearchOptions::WHOLE_WORD; - } - if !search_bar.show(window, cx) { - return None; - } - let Some(query) = search_bar - .query_suggestion(window, cx) - .or_else(|| cursor_word) - else { - drop(search_bar.search("", None, false, window, cx)); - return None; - }; - - let query = regex::escape(&query); - Some(search_bar.search(&query, Some(options), true, window, cx)) - }); - - let Some(search) = search else { return false }; - - let search_bar = search_bar.downgrade(); - cx.spawn_in(window, async move |_, cx| { - search.await?; - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_match(direction, count, window, cx); - - vim.update(cx, |vim, cx| { - let new_selections = vim.editor_selections(window, cx); - vim.search_motion( - Motion::ZedSearchResult { - prior_selections, - new_selections, - }, - window, - cx, - ) - }); - })?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - true - }); - if !searched { - self.clear_operator(window, cx) - } - - if self.mode.is_visual() { - self.switch_mode(Mode::Normal, false, window, cx) - } - } - - fn find_command(&mut self, action: &FindCommand, window: &mut Window, cx: &mut Context) { - let Some(pane) = self.pane(window, cx) else { - return; - }; - pane.update(cx, |pane, cx| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - let search = search_bar.update(cx, |search_bar, cx| { - if !search_bar.show(window, cx) { - return None; - } - let mut query = action.query.clone(); - if query.is_empty() { - query = search_bar.query(cx); - }; - - let mut options = SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE; - if search_bar.should_use_smartcase_search(cx) { - options.set( - SearchOptions::CASE_SENSITIVE, - search_bar.is_contains_uppercase(&query), - ); - } - - Some(search_bar.search(&query, Some(options), true, window, cx)) - }); - let Some(search) = search else { return }; - let search_bar = search_bar.downgrade(); - let direction = if action.backwards { - Direction::Prev - } else { - Direction::Next - }; - cx.spawn_in(window, async move |_, cx| { - search.await?; - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_match(direction, 1, window, cx) - })?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - }) - } - - fn replace_command( - &mut self, - action: &ReplaceCommand, - window: &mut Window, - cx: &mut Context, - ) { - let replacement = action.replacement.clone(); - let Some(((pane, workspace), editor)) = self - .pane(window, cx) - .zip(self.workspace(window)) - .zip(self.editor()) - else { - return; - }; - if let Some(result) = self.update_editor(cx, |vim, editor, cx| { - let range = action.range.buffer_range(vim, editor, window, cx)?; - let snapshot = editor.snapshot(window, cx); - let snapshot = snapshot.buffer_snapshot(); - let end_point = Point::new(range.end.0, snapshot.line_len(range.end)); - let range = snapshot.anchor_before(Point::new(range.start.0, 0)) - ..snapshot.anchor_after(end_point); - editor.set_search_within_ranges(&[range], cx); - anyhow::Ok(()) - }) { - workspace.update(cx, |workspace, cx| { - result.notify_err(workspace, cx); - }) - } - let Some(search_bar) = pane.update(cx, |pane, cx| { - pane.toolbar().read(cx).item_of_type::() - }) else { - return; - }; - let mut options = SearchOptions::REGEX; - let search = search_bar.update(cx, |search_bar, cx| { - if !search_bar.show(window, cx) { - return None; - } - - let search = if replacement.search.is_empty() { - search_bar.query(cx) - } else { - replacement.search - }; - - if let Some(case) = replacement.case_sensitive { - options.set(SearchOptions::CASE_SENSITIVE, case) - } else if search_bar.should_use_smartcase_search(cx) { - options.set( - SearchOptions::CASE_SENSITIVE, - search_bar.is_contains_uppercase(&search), - ); - } else { - // Fallback: no explicit i/I flags and smartcase disabled; - // use global editor.search.case_sensitive. - options.set( - SearchOptions::CASE_SENSITIVE, - EditorSettings::get_global(cx).search.case_sensitive, - ) - } - - if !replacement.flag_g { - options.set(SearchOptions::ONE_MATCH_PER_LINE, true); - } - - search_bar.set_replacement(Some(&replacement.replacement), cx); - if replacement.flag_c { - search_bar.focus_replace(window, cx); - } - Some(search_bar.search(&search, Some(options), true, window, cx)) - }); - if replacement.flag_n { - self.move_cursor( - Motion::StartOfLine { - display_lines: false, - }, - None, - window, - cx, - ); - return; - } - let Some(search) = search else { return }; - let search_bar = search_bar.downgrade(); - cx.spawn_in(window, async move |vim, cx| { - search.await?; - search_bar.update_in(cx, |search_bar, window, cx| { - if replacement.flag_c { - search_bar.select_first_match(window, cx); - return; - } - search_bar.select_last_match(window, cx); - search_bar.replace_all(&Default::default(), window, cx); - editor.update(cx, |editor, cx| editor.clear_search_within_ranges(cx)); - let _ = search_bar.search(&search_bar.query(cx), None, false, window, cx); - vim.update(cx, |vim, cx| { - vim.move_cursor( - Motion::StartOfLine { - display_lines: false, - }, - None, - window, - cx, - ) - }) - .ok(); - - // Disable the `ONE_MATCH_PER_LINE` search option when finished, as - // this is not properly supported outside of vim mode, and - // not disabling it makes the "Replace All Matches" button - // actually replace only the first match on each line. - options.set(SearchOptions::ONE_MATCH_PER_LINE, false); - search_bar.set_search_options(options, cx); - }) - }) - .detach_and_log_err(cx); - } -} - -impl Replacement { - // convert a vim query into something more usable by zed. - // we don't attempt to fully convert between the two regex syntaxes, - // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern, - // and convert \0..\9 to $0..$9 in the replacement so that common idioms work. - pub(crate) fn parse(mut chars: Peekable) -> Option { - let delimiter = chars - .next() - .filter(|c| !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'')?; - - let mut search = String::new(); - let mut replacement = String::new(); - let mut flags = String::new(); - - let mut buffer = &mut search; - - let mut escaped = false; - // 0 - parsing search - // 1 - parsing replacement - // 2 - parsing flags - let mut phase = 0; - - for c in chars { - if escaped { - escaped = false; - if phase == 1 && c.is_ascii_digit() { - buffer.push('$') - // unescape escaped parens - } else if phase == 0 && (c == '(' || c == ')') { - } else if c != delimiter { - buffer.push('\\') - } - buffer.push(c) - } else if c == '\\' { - escaped = true; - } else if c == delimiter { - if phase == 0 { - buffer = &mut replacement; - phase = 1; - } else if phase == 1 { - buffer = &mut flags; - phase = 2; - } else { - break; - } - } else { - // escape unescaped parens - if phase == 0 && (c == '(' || c == ')') { - buffer.push('\\') - } - buffer.push(c) - } - } - - let mut replacement = Replacement { - search, - replacement, - case_sensitive: None, - flag_g: false, - flag_n: false, - flag_c: false, - }; - - for c in flags.chars() { - match c { - 'g' => replacement.flag_g = true, - 'n' => replacement.flag_n = true, - 'c' => replacement.flag_c = true, - 'i' => replacement.case_sensitive = Some(false), - 'I' => replacement.case_sensitive = Some(true), - _ => {} - } - } - - Some(replacement) - } -} - -#[cfg(test)] -mod test { - use std::time::Duration; - - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - use editor::{DisplayPoint, display_map::DisplayRow}; - - use indoc::indoc; - use search::BufferSearchBar; - use settings::SettingsStore; - - #[gpui::test] - async fn test_move_to_next(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal); - - cx.simulate_keystrokes("*"); - cx.run_until_parked(); - cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); - - cx.simulate_keystrokes("*"); - cx.run_until_parked(); - cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); - - cx.simulate_keystrokes("#"); - cx.run_until_parked(); - cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); - - cx.simulate_keystrokes("#"); - cx.run_until_parked(); - cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); - - cx.simulate_keystrokes("2 *"); - cx.run_until_parked(); - cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); - - cx.simulate_keystrokes("g *"); - cx.run_until_parked(); - cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); - - cx.simulate_keystrokes("n"); - cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); - - cx.simulate_keystrokes("g #"); - cx.run_until_parked(); - cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); - } - - #[gpui::test] - async fn test_move_to_next_with_no_search_wrap(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false)); - }); - - cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal); - - cx.simulate_keystrokes("*"); - cx.run_until_parked(); - cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); - - cx.simulate_keystrokes("*"); - cx.run_until_parked(); - cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); - - cx.simulate_keystrokes("#"); - cx.run_until_parked(); - cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); - - cx.simulate_keystrokes("3 *"); - cx.run_until_parked(); - cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); - - cx.simulate_keystrokes("g *"); - cx.run_until_parked(); - cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); - - cx.simulate_keystrokes("n"); - cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); - - cx.simulate_keystrokes("g #"); - cx.run_until_parked(); - cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); - } - - #[gpui::test] - async fn test_search(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal); - cx.simulate_keystrokes("/ c c"); - - let search_bar = cx.workspace(|workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .toolbar() - .read(cx) - .item_of_type::() - .expect("Buffer search bar should be deployed") - }); - - cx.update_entity(search_bar, |bar, _window, cx| { - assert_eq!(bar.query(cx), "cc"); - }); - - cx.run_until_parked(); - - cx.update_editor(|editor, window, cx| { - let highlights = editor.all_text_background_highlights(window, cx); - assert_eq!(3, highlights.len()); - assert_eq!( - DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 2), - highlights[0].0 - ) - }); - - cx.simulate_keystrokes("enter"); - cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); - - // n to go to next/N to go to previous - cx.simulate_keystrokes("n"); - cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal); - cx.simulate_keystrokes("shift-n"); - cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); - - // ? to go to previous - cx.simulate_keystrokes("? enter"); - cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal); - cx.simulate_keystrokes("? enter"); - cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal); - - // / to go to next - cx.simulate_keystrokes("/ enter"); - cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal); - - // ?{search} to search backwards - cx.simulate_keystrokes("? b enter"); - cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal); - - // works with counts - cx.simulate_keystrokes("4 / c"); - cx.simulate_keystrokes("enter"); - cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal); - - // check that searching resumes from cursor, not previous match - cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal); - cx.simulate_keystrokes("/ d"); - cx.simulate_keystrokes("enter"); - cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal); - cx.update_editor(|editor, window, cx| { - editor.move_to_beginning(&Default::default(), window, cx) - }); - cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal); - cx.simulate_keystrokes("/ b"); - cx.simulate_keystrokes("enter"); - cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal); - - // check that searching switches to normal mode if in visual mode - cx.set_state("ˇone two one", Mode::Normal); - cx.simulate_keystrokes("v l l"); - cx.assert_editor_state("«oneˇ» two one"); - cx.simulate_keystrokes("*"); - cx.assert_state("one two ˇone", Mode::Normal); - - // check that a backward search after last match works correctly - cx.set_state("aa\naa\nbbˇ", Mode::Normal); - cx.simulate_keystrokes("? a a"); - cx.simulate_keystrokes("enter"); - cx.assert_state("aa\nˇaa\nbb", Mode::Normal); - - // check that searching with unable search wrap - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false)); - }); - cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal); - cx.simulate_keystrokes("/ c c enter"); - - cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); - - // n to go to next/N to go to previous - cx.simulate_keystrokes("n"); - cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal); - cx.simulate_keystrokes("shift-n"); - cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); - - // ? to go to previous - cx.simulate_keystrokes("? enter"); - cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); - cx.simulate_keystrokes("? enter"); - cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); - } - - #[gpui::test] - async fn test_non_vim_search(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, false).await; - cx.cx.set_state("ˇone one one one"); - cx.run_until_parked(); - cx.simulate_keystrokes("cmd-f"); - cx.run_until_parked(); - - cx.assert_editor_state("«oneˇ» one one one"); - cx.simulate_keystrokes("enter"); - cx.assert_editor_state("one «oneˇ» one one"); - cx.simulate_keystrokes("shift-enter"); - cx.assert_editor_state("«oneˇ» one one one"); - } - - #[gpui::test] - async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇa.c. abcd a.c. abcd").await; - cx.simulate_shared_keystrokes("v 3 l *").await; - cx.shared_state().await.assert_eq("a.c. abcd ˇa.c. abcd"); - } - - #[gpui::test] - async fn test_d_search(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇa.c. abcd a.c. abcd").await; - cx.simulate_shared_keystrokes("d / c d").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq("ˇcd a.c. abcd"); - } - - #[gpui::test] - async fn test_backwards_n(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇa b a b a b a").await; - cx.simulate_shared_keystrokes("*").await; - cx.simulate_shared_keystrokes("n").await; - cx.shared_state().await.assert_eq("a b a b ˇa b a"); - cx.simulate_shared_keystrokes("#").await; - cx.shared_state().await.assert_eq("a b ˇa b a b a"); - cx.simulate_shared_keystrokes("n").await; - cx.shared_state().await.assert_eq("ˇa b a b a b a"); - } - - #[gpui::test] - async fn test_v_search(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇa.c. abcd a.c. abcd").await; - cx.simulate_shared_keystrokes("v / c d").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq("«a.c. abcˇ»d a.c. abcd"); - - cx.set_shared_state("a a aˇ a a a").await; - cx.simulate_shared_keystrokes("v / a").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq("a a a« aˇ» a a"); - cx.simulate_shared_keystrokes("/ enter").await; - cx.shared_state().await.assert_eq("a a a« a aˇ» a"); - cx.simulate_shared_keystrokes("? enter").await; - cx.shared_state().await.assert_eq("a a a« aˇ» a a"); - cx.simulate_shared_keystrokes("? enter").await; - cx.shared_state().await.assert_eq("a a «ˇa »a a a"); - cx.simulate_shared_keystrokes("/ enter").await; - cx.shared_state().await.assert_eq("a a a« aˇ» a a"); - cx.simulate_shared_keystrokes("/ enter").await; - cx.shared_state().await.assert_eq("a a a« a aˇ» a"); - } - - #[gpui::test] - async fn test_v_search_aa(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇaa aa").await; - cx.simulate_shared_keystrokes("v / a a").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq("«aa aˇ»a"); - } - - #[gpui::test] - async fn test_visual_block_search(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "ˇone two - three four - five six - " - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v j / f").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq(indoc! { - "«one twoˇ» - «three fˇ»our - five six - " - }); - } - - #[gpui::test] - async fn test_replace_with_range_at_start(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "ˇa - a - a - a - a - a - a - " - }) - .await; - cx.simulate_shared_keystrokes(": 2 , 5 s / ^ / b").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq(indoc! { - "a - ba - ba - ba - ˇba - a - a - " - }); - - cx.simulate_shared_keystrokes("/ a").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq(indoc! { - "a - ba - ba - ba - bˇa - a - a - " - }); - } - - #[gpui::test] - async fn test_search_skipping(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - "ˇaa aa aa" - }) - .await; - - cx.simulate_shared_keystrokes("/ a a").await; - cx.simulate_shared_keystrokes("enter").await; - - cx.shared_state().await.assert_eq(indoc! { - "aa ˇaa aa" - }); - - cx.simulate_shared_keystrokes("left / a a").await; - cx.simulate_shared_keystrokes("enter").await; - - cx.shared_state().await.assert_eq(indoc! { - "aa ˇaa aa" - }); - } - - #[gpui::test] - async fn test_replace_n(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - "ˇaa - bb - aa" - }) - .await; - - cx.simulate_shared_keystrokes(": s / b b / d d / n").await; - cx.simulate_shared_keystrokes("enter").await; - - cx.shared_state().await.assert_eq(indoc! { - "ˇaa - bb - aa" - }); - - let search_bar = cx.update_workspace(|workspace, _, cx| { - workspace.active_pane().update(cx, |pane, cx| { - pane.toolbar() - .read(cx) - .item_of_type::() - .unwrap() - }) - }); - cx.update_entity(search_bar, |search_bar, _, cx| { - assert!(!search_bar.is_dismissed()); - assert_eq!(search_bar.query(cx), "bb".to_string()); - assert_eq!(search_bar.replacement(cx), "dd".to_string()); - }) - } - - #[gpui::test] - async fn test_replace_g(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - "ˇaa aa aa aa - aa - aa" - }) - .await; - - cx.simulate_shared_keystrokes(": s / a a / b b").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇbb aa aa aa - aa - aa" - }); - cx.simulate_shared_keystrokes(": s / a a / b b / g").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇbb bb bb bb - aa - aa" - }); - } - - #[gpui::test] - async fn test_replace_c(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.set_state( - indoc! { - "ˇaa - aa - aa" - }, - Mode::Normal, - ); - - cx.simulate_keystrokes("v j : s / a a / d d / c"); - cx.simulate_keystrokes("enter"); - - cx.assert_state( - indoc! { - "ˇaa - aa - aa" - }, - Mode::Normal, - ); - - cx.simulate_keystrokes("enter"); - - cx.assert_state( - indoc! { - "dd - ˇaa - aa" - }, - Mode::Normal, - ); - - cx.simulate_keystrokes("enter"); - cx.assert_state( - indoc! { - "dd - ddˇ - aa" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("enter"); - cx.assert_state( - indoc! { - "dd - ddˇ - aa" - }, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_replace_with_range(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "ˇa - a - a - a - a - a - a - " - }) - .await; - cx.simulate_shared_keystrokes(": 2 , 5 s / a / b").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq(indoc! { - "a - b - b - b - ˇb - a - a - " - }); - cx.executor().advance_clock(Duration::from_millis(250)); - cx.run_until_parked(); - - cx.simulate_shared_keystrokes("/ a enter").await; - cx.shared_state().await.assert_eq(indoc! { - "a - b - b - b - b - ˇa - a - " - }); - } -} diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs deleted file mode 100644 index df8d7b4879..0000000000 --- a/crates/vim/src/normal/substitute.rs +++ /dev/null @@ -1,312 +0,0 @@ -use editor::{Editor, SelectionEffects, movement}; -use gpui::{Context, Window, actions}; -use language::Point; - -use crate::{ - Mode, Vim, - motion::{Motion, MotionKind}, -}; - -actions!( - vim, - [ - /// Substitutes characters in the current selection. - Substitute, - /// Substitutes the entire line. - SubstituteLine - ] -); - -pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, |vim, _: &Substitute, window, cx| { - vim.start_recording(cx); - let count = Vim::take_count(cx); - Vim::take_forced_motion(cx); - vim.substitute(count, vim.mode == Mode::VisualLine, window, cx); - }); - - Vim::action(editor, cx, |vim, _: &SubstituteLine, window, cx| { - vim.start_recording(cx); - if matches!(vim.mode, Mode::VisualBlock | Mode::Visual) { - vim.switch_mode(Mode::VisualLine, false, window, cx) - } - let count = Vim::take_count(cx); - Vim::take_forced_motion(cx); - vim.substitute(count, true, window, cx) - }); -} - -impl Vim { - pub fn substitute( - &mut self, - count: Option, - line_mode: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.store_visual_marks(window, cx); - self.update_editor(cx, |vim, editor, cx| { - editor.set_clip_at_line_ends(false, cx); - editor.transact(window, cx, |editor, window, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - if selection.start == selection.end { - Motion::Right.expand_selection( - map, - selection, - count, - &text_layout_details, - false, - ); - } - if line_mode { - // in Visual mode when the selection contains the newline at the end - // of the line, we should exclude it. - if !selection.is_empty() && selection.end.column() == 0 { - selection.end = movement::left(map, selection.end); - } - Motion::CurrentLine.expand_selection( - map, - selection, - None, - &text_layout_details, - false, - ); - if let Some((point, _)) = (Motion::FirstNonWhitespace { - display_lines: false, - }) - .move_point( - map, - selection.start, - selection.goal, - None, - &text_layout_details, - ) { - selection.start = point; - } - } - }) - }); - let kind = if line_mode { - MotionKind::Linewise - } else { - MotionKind::Exclusive - }; - vim.copy_selections_content(editor, kind, window, cx); - let selections = editor - .selections - .all::(&editor.display_snapshot(cx)) - .into_iter(); - let edits = selections.map(|selection| (selection.start..selection.end, "")); - editor.edit(edits, cx); - }); - }); - self.switch_mode(Mode::Insert, true, window, cx); - } -} - -#[cfg(test)] -mod test { - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - use indoc::indoc; - - #[gpui::test] - async fn test_substitute(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // supports a single cursor - cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal); - cx.simulate_keystrokes("s x"); - cx.assert_editor_state("xˇbc\n"); - - // supports a selection - cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual); - cx.assert_editor_state("a«bcˇ»\n"); - cx.simulate_keystrokes("s x"); - cx.assert_editor_state("axˇ\n"); - - // supports counts - cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal); - cx.simulate_keystrokes("2 s x"); - cx.assert_editor_state("xˇc\n"); - - // supports multiple cursors - cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal); - cx.simulate_keystrokes("2 s x"); - cx.assert_editor_state("axˇdexˇg\n"); - - // does not read beyond end of line - cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal); - cx.simulate_keystrokes("5 s x"); - cx.assert_editor_state("xˇ\n"); - - // it handles multibyte characters - cx.set_state(indoc! {"ˇcàfé\n"}, Mode::Normal); - cx.simulate_keystrokes("4 s"); - cx.assert_editor_state("ˇ\n"); - - // should transactionally undo selection changes - cx.simulate_keystrokes("escape u"); - cx.assert_editor_state("ˇcàfé\n"); - - // it handles visual line mode - cx.set_state( - indoc! {" - alpha - beˇta - gamma"}, - Mode::Normal, - ); - cx.simulate_keystrokes("shift-v s"); - cx.assert_editor_state(indoc! {" - alpha - ˇ - gamma"}); - } - - #[gpui::test] - async fn test_visual_change(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("The quick ˇbrown").await; - cx.simulate_shared_keystrokes("v w c").await; - cx.shared_state().await.assert_eq("The quick ˇ"); - - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("v w j c").await; - cx.shared_state().await.assert_eq(indoc! {" - The ˇver - the lazy dog"}); - - cx.simulate_at_each_offset( - "v w j c", - indoc! {" - The ˇquick brown - fox jumps ˇover - the ˇlazy dog"}, - ) - .await - .assert_matches(); - cx.simulate_at_each_offset( - "v w k c", - indoc! {" - The ˇquick brown - fox jumps ˇover - the ˇlazy dog"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_visual_line_change(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "shift-v c", - indoc! {" - The quˇick brown - fox jumps over - the lazy dog"}, - ) - .await - .assert_matches(); - // Test pasting code copied on change - cx.simulate_shared_keystrokes("escape j p").await; - cx.shared_state().await.assert_matches(); - - cx.simulate_at_each_offset( - "shift-v c", - indoc! {" - The quick brown - fox juˇmps over - the laˇzy dog"}, - ) - .await - .assert_matches(); - cx.simulate( - "shift-v j c", - indoc! {" - The quˇick brown - fox jumps over - the lazy dog"}, - ) - .await - .assert_matches(); - // Test pasting code copied on delete - cx.simulate_shared_keystrokes("escape j p").await; - cx.shared_state().await.assert_matches(); - - cx.simulate_at_each_offset( - "shift-v j c", - indoc! {" - The quick brown - fox juˇmps over - the laˇzy dog"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_substitute_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - let initial_state = indoc! {" - The quick brown - fox juˇmps over - the lazy dog - "}; - - // normal mode - cx.set_shared_state(initial_state).await; - cx.simulate_shared_keystrokes("shift-s o").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - oˇ - the lazy dog - "}); - - // visual mode - cx.set_shared_state(initial_state).await; - cx.simulate_shared_keystrokes("v k shift-s o").await; - cx.shared_state().await.assert_eq(indoc! {" - oˇ - the lazy dog - "}); - - // visual block mode - cx.set_shared_state(initial_state).await; - cx.simulate_shared_keystrokes("ctrl-v j shift-s o").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - oˇ - "}); - - // visual mode including newline - cx.set_shared_state(initial_state).await; - cx.simulate_shared_keystrokes("v $ shift-s o").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - oˇ - the lazy dog - "}); - - // indentation - cx.set_neovim_option("shiftwidth=4").await; - cx.set_shared_state(initial_state).await; - cx.simulate_shared_keystrokes("> > shift-s o").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - oˇ - the lazy dog - "}); - } -} diff --git a/crates/vim/src/normal/toggle_comments.rs b/crates/vim/src/normal/toggle_comments.rs deleted file mode 100644 index 17c3b2d363..0000000000 --- a/crates/vim/src/normal/toggle_comments.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::{Vim, motion::Motion, object::Object}; -use collections::HashMap; -use editor::{Bias, SelectionEffects, display_map::ToDisplayPoint}; -use gpui::{Context, Window}; -use language::SelectionGoal; - -impl Vim { - pub fn toggle_comments_motion( - &mut self, - motion: Motion, - times: Option, - forced_motion: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - let mut selection_starts: HashMap<_, _> = Default::default(); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); - selection_starts.insert(selection.id, anchor); - motion.expand_selection( - map, - selection, - times, - &text_layout_details, - forced_motion, - ); - }); - }); - editor.toggle_comments(&Default::default(), window, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = selection_starts.remove(&selection.id).unwrap(); - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); - }); - }); - }); - }); - } - - pub fn toggle_comments_object( - &mut self, - object: Object, - around: bool, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let mut original_positions: HashMap<_, _> = Default::default(); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); - original_positions.insert(selection.id, anchor); - object.expand_selection(map, selection, around, times); - }); - }); - editor.toggle_comments(&Default::default(), window, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = original_positions.remove(&selection.id).unwrap(); - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); - }); - }); - }); - }); - } -} diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs deleted file mode 100644 index 9920b8fc88..0000000000 --- a/crates/vim/src/normal/yank.rs +++ /dev/null @@ -1,246 +0,0 @@ -use std::{ops::Range, time::Duration}; - -use crate::{ - Vim, VimSettings, - motion::{Motion, MotionKind}, - object::Object, - state::{Mode, Register}, -}; -use collections::HashMap; -use editor::{ClipboardSelection, Editor, SelectionEffects}; -use gpui::Context; -use gpui::Window; -use language::Point; -use settings::Settings; - -struct HighlightOnYank; - -impl Vim { - pub fn yank_motion( - &mut self, - motion: Motion, - times: Option, - forced_motion: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |vim, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - let mut original_positions: HashMap<_, _> = Default::default(); - let mut kind = None; - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let original_position = (selection.head(), selection.goal); - kind = motion.expand_selection( - map, - selection, - times, - &text_layout_details, - forced_motion, - ); - if kind == Some(MotionKind::Exclusive) { - original_positions - .insert(selection.id, (selection.start, selection.goal)); - } else { - original_positions.insert(selection.id, original_position); - } - }) - }); - let Some(kind) = kind else { return }; - vim.yank_selections_content(editor, kind, window, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|_, selection| { - let (head, goal) = original_positions.remove(&selection.id).unwrap(); - selection.collapse_to(head, goal); - }); - }); - }); - }); - self.exit_temporary_normal(window, cx); - } - - pub fn yank_object( - &mut self, - object: Object, - around: bool, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |vim, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - let mut start_positions: HashMap<_, _> = Default::default(); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - object.expand_selection(map, selection, around, times); - let start_position = (selection.start, selection.goal); - start_positions.insert(selection.id, start_position); - }); - }); - let kind = match object.target_visual_mode(vim.mode, around) { - Mode::VisualLine => MotionKind::Linewise, - _ => MotionKind::Exclusive, - }; - vim.yank_selections_content(editor, kind, window, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|_, selection| { - let (head, goal) = start_positions.remove(&selection.id).unwrap(); - selection.collapse_to(head, goal); - }); - }); - }); - }); - self.exit_temporary_normal(window, cx); - } - - pub fn yank_selections_content( - &mut self, - editor: &mut Editor, - kind: MotionKind, - window: &mut Window, - cx: &mut Context, - ) { - self.copy_ranges( - editor, - kind, - true, - editor - .selections - .all_adjusted(&editor.display_snapshot(cx)) - .iter() - .map(|s| s.range()) - .collect(), - window, - cx, - ) - } - - pub fn copy_selections_content( - &mut self, - editor: &mut Editor, - kind: MotionKind, - window: &mut Window, - cx: &mut Context, - ) { - self.copy_ranges( - editor, - kind, - false, - editor - .selections - .all_adjusted(&editor.display_snapshot(cx)) - .iter() - .map(|s| s.range()) - .collect(), - window, - cx, - ) - } - - pub(crate) fn copy_ranges( - &mut self, - editor: &mut Editor, - kind: MotionKind, - is_yank: bool, - selections: Vec>, - window: &mut Window, - cx: &mut Context, - ) { - let buffer = editor.buffer().read(cx).snapshot(cx); - self.set_mark( - "[".to_string(), - selections - .iter() - .map(|s| buffer.anchor_before(s.start)) - .collect(), - editor.buffer(), - window, - cx, - ); - self.set_mark( - "]".to_string(), - selections - .iter() - .map(|s| buffer.anchor_after(s.end)) - .collect(), - editor.buffer(), - window, - cx, - ); - - let mut text = String::new(); - let mut clipboard_selections = Vec::with_capacity(selections.len()); - let mut ranges_to_highlight = Vec::new(); - - { - let mut is_first = true; - for selection in selections.iter() { - let start = selection.start; - let end = selection.end; - if is_first { - is_first = false; - } else { - text.push('\n'); - } - let initial_len = text.len(); - - let start_anchor = buffer.anchor_after(start); - let end_anchor = buffer.anchor_before(end); - ranges_to_highlight.push(start_anchor..end_anchor); - - for chunk in buffer.text_for_range(start..end) { - text.push_str(chunk); - } - if kind.linewise() { - text.push('\n'); - } - clipboard_selections.push(ClipboardSelection::for_buffer( - text.len() - initial_len, - false, - start..end, - &buffer, - editor.project(), - cx, - )); - } - } - - let selected_register = self.selected_register.take(); - Vim::update_globals(cx, |globals, cx| { - globals.write_registers( - Register { - text: text.into(), - clipboard_selections: Some(clipboard_selections), - }, - selected_register, - is_yank, - kind, - cx, - ) - }); - - let highlight_duration = VimSettings::get_global(cx).highlight_on_yank_duration; - if !is_yank || self.mode == Mode::Visual || highlight_duration == 0 { - return; - } - - editor.highlight_background::( - &ranges_to_highlight, - |_, colors| colors.colors().editor_document_highlight_read_background, - cx, - ); - cx.spawn(async move |this, cx| { - cx.background_executor() - .timer(Duration::from_millis(highlight_duration)) - .await; - this.update(cx, |editor, cx| { - editor.clear_background_highlights::(cx) - }) - .ok(); - }) - .detach(); - } -} diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs deleted file mode 100644 index f11386d02d..0000000000 --- a/crates/vim/src/object.rs +++ /dev/null @@ -1,3414 +0,0 @@ -use std::ops::Range; - -use crate::{ - Vim, - motion::right, - state::{Mode, Operator}, -}; -use editor::{ - Bias, BufferOffset, DisplayPoint, Editor, MultiBufferOffset, ToOffset, - display_map::{DisplaySnapshot, ToDisplayPoint}, - movement::{self, FindRange}, -}; -use gpui::{Action, Window, actions}; -use itertools::Itertools; -use language::{BufferSnapshot, CharKind, Point, Selection, TextObject, TreeSitterOptions}; -use multi_buffer::MultiBufferRow; -use schemars::JsonSchema; -use serde::Deserialize; -use ui::Context; - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum Object { - Word { ignore_punctuation: bool }, - Subword { ignore_punctuation: bool }, - Sentence, - Paragraph, - Quotes, - BackQuotes, - AnyQuotes, - MiniQuotes, - DoubleQuotes, - VerticalBars, - AnyBrackets, - MiniBrackets, - Parentheses, - SquareBrackets, - CurlyBrackets, - AngleBrackets, - Argument, - IndentObj { include_below: bool }, - Tag, - Method, - Class, - Comment, - EntireFile, -} - -/// Selects a word text object. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct Word { - #[serde(default)] - ignore_punctuation: bool, -} - -/// Selects a subword text object. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct Subword { - #[serde(default)] - ignore_punctuation: bool, -} -/// Selects text at the same indentation level. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct IndentObj { - #[serde(default)] - include_below: bool, -} - -#[derive(Debug, Clone)] -pub struct CandidateRange { - pub start: DisplayPoint, - pub end: DisplayPoint, -} - -#[derive(Debug, Clone)] -pub struct CandidateWithRanges { - candidate: CandidateRange, - open_range: Range, - close_range: Range, -} - -/// Selects text at the same indentation level. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct Parentheses { - #[serde(default)] - opening: bool, -} - -/// Selects text at the same indentation level. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct SquareBrackets { - #[serde(default)] - opening: bool, -} - -/// Selects text at the same indentation level. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct AngleBrackets { - #[serde(default)] - opening: bool, -} -/// Selects text at the same indentation level. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct CurlyBrackets { - #[serde(default)] - opening: bool, -} - -fn cover_or_next, Range)>>( - candidates: Option, - caret: DisplayPoint, - map: &DisplaySnapshot, -) -> Option { - let caret_offset = caret.to_offset(map, Bias::Left); - let mut covering = vec![]; - let mut next_ones = vec![]; - let snapshot = map.buffer_snapshot(); - - if let Some(ranges) = candidates { - for (open_range, close_range) in ranges { - let start_off = open_range.start; - let end_off = close_range.end; - let candidate = CandidateWithRanges { - candidate: CandidateRange { - start: start_off.to_display_point(map), - end: end_off.to_display_point(map), - }, - open_range: open_range.clone(), - close_range: close_range.clone(), - }; - - if open_range - .start - .to_offset(snapshot) - .to_display_point(map) - .row() - == caret_offset.to_display_point(map).row() - { - if start_off <= caret_offset && caret_offset < end_off { - covering.push(candidate); - } else if start_off >= caret_offset { - next_ones.push(candidate); - } - } - } - } - - // 1) covering -> smallest width - if !covering.is_empty() { - return covering.into_iter().min_by_key(|r| { - r.candidate.end.to_offset(map, Bias::Right) - - r.candidate.start.to_offset(map, Bias::Left) - }); - } - - // 2) next -> closest by start - if !next_ones.is_empty() { - return next_ones.into_iter().min_by_key(|r| { - let start = r.candidate.start.to_offset(map, Bias::Left); - (start.0 as isize - caret_offset.0 as isize).abs() - }); - } - - None -} - -type DelimiterPredicate = dyn Fn(&BufferSnapshot, usize, usize) -> bool; - -struct DelimiterRange { - open: Range, - close: Range, -} - -impl DelimiterRange { - fn to_display_range(&self, map: &DisplaySnapshot, around: bool) -> Range { - if around { - self.open.start.to_display_point(map)..self.close.end.to_display_point(map) - } else { - self.open.end.to_display_point(map)..self.close.start.to_display_point(map) - } - } -} - -fn find_mini_delimiters( - map: &DisplaySnapshot, - display_point: DisplayPoint, - around: bool, - is_valid_delimiter: &DelimiterPredicate, -) -> Option> { - let point = map.clip_at_line_end(display_point).to_point(map); - let offset = point.to_offset(&map.buffer_snapshot()); - - let line_range = get_line_range(map, point); - let visible_line_range = get_visible_line_range(&line_range); - - let snapshot = &map.buffer_snapshot(); - let mut excerpt = snapshot.excerpt_containing(offset..offset)?; - let buffer = excerpt.buffer(); - let buffer_offset = excerpt.map_offset_to_buffer(offset); - - let bracket_filter = |open: Range, close: Range| { - is_valid_delimiter(buffer, open.start, close.start) - }; - - // Try to find delimiters in visible range first - let ranges = map - .buffer_snapshot() - .bracket_ranges(visible_line_range) - .map(|ranges| { - ranges.filter_map(|(open, close)| { - // Convert the ranges from multibuffer space to buffer space as - // that is what `is_valid_delimiter` expects, otherwise it might - // panic as the values might be out of bounds. - let buffer_open = excerpt.map_range_to_buffer(open.clone()); - let buffer_close = excerpt.map_range_to_buffer(close.clone()); - - if is_valid_delimiter(buffer, buffer_open.start.0, buffer_close.start.0) { - Some((open, close)) - } else { - None - } - }) - }); - - if let Some(candidate) = cover_or_next(ranges, display_point, map) { - return Some( - DelimiterRange { - open: candidate.open_range, - close: candidate.close_range, - } - .to_display_range(map, around), - ); - } - - // Fall back to innermost enclosing brackets - let (open_bracket, close_bracket) = buffer - .innermost_enclosing_bracket_ranges(buffer_offset..buffer_offset, Some(&bracket_filter))?; - - Some( - DelimiterRange { - open: excerpt.map_range_from_buffer( - BufferOffset(open_bracket.start)..BufferOffset(open_bracket.end), - ), - close: excerpt.map_range_from_buffer( - BufferOffset(close_bracket.start)..BufferOffset(close_bracket.end), - ), - } - .to_display_range(map, around), - ) -} - -fn get_line_range(map: &DisplaySnapshot, point: Point) -> Range { - let (start, mut end) = ( - map.prev_line_boundary(point).0, - map.next_line_boundary(point).0, - ); - - if end == point { - end = map.max_point().to_point(map); - } - - start..end -} - -fn get_visible_line_range(line_range: &Range) -> Range { - let end_column = line_range.end.column.saturating_sub(1); - line_range.start..Point::new(line_range.end.row, end_column) -} - -fn is_quote_delimiter(buffer: &BufferSnapshot, _start: usize, end: usize) -> bool { - matches!(buffer.chars_at(end).next(), Some('\'' | '"' | '`')) -} - -fn is_bracket_delimiter(buffer: &BufferSnapshot, start: usize, _end: usize) -> bool { - matches!( - buffer.chars_at(start).next(), - Some('(' | '[' | '{' | '<' | '|') - ) -} - -fn find_mini_quotes( - map: &DisplaySnapshot, - display_point: DisplayPoint, - around: bool, -) -> Option> { - find_mini_delimiters(map, display_point, around, &is_quote_delimiter) -} - -fn find_mini_brackets( - map: &DisplaySnapshot, - display_point: DisplayPoint, - around: bool, -) -> Option> { - find_mini_delimiters(map, display_point, around, &is_bracket_delimiter) -} - -actions!( - vim, - [ - /// Selects a sentence text object. - Sentence, - /// Selects a paragraph text object. - Paragraph, - /// Selects text within single quotes. - Quotes, - /// Selects text within backticks. - BackQuotes, - /// Selects text within the nearest quotes (single or double). - MiniQuotes, - /// Selects text within any type of quotes. - AnyQuotes, - /// Selects text within double quotes. - DoubleQuotes, - /// Selects text within vertical bars (pipes). - VerticalBars, - /// Selects text within the nearest brackets. - MiniBrackets, - /// Selects text within any type of brackets. - AnyBrackets, - /// Selects a function argument. - Argument, - /// Selects an HTML/XML tag. - Tag, - /// Selects a method or function. - Method, - /// Selects a class definition. - Class, - /// Selects a comment block. - Comment, - /// Selects the entire file. - EntireFile - ] -); - -pub fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action( - editor, - cx, - |vim, &Word { ignore_punctuation }: &Word, window, cx| { - vim.object(Object::Word { ignore_punctuation }, window, cx) - }, - ); - Vim::action( - editor, - cx, - |vim, &Subword { ignore_punctuation }: &Subword, window, cx| { - vim.object(Object::Subword { ignore_punctuation }, window, cx) - }, - ); - Vim::action(editor, cx, |vim, _: &Tag, window, cx| { - vim.object(Object::Tag, window, cx) - }); - Vim::action(editor, cx, |vim, _: &Sentence, window, cx| { - vim.object(Object::Sentence, window, cx) - }); - Vim::action(editor, cx, |vim, _: &Paragraph, window, cx| { - vim.object(Object::Paragraph, window, cx) - }); - Vim::action(editor, cx, |vim, _: &Quotes, window, cx| { - vim.object(Object::Quotes, window, cx) - }); - Vim::action(editor, cx, |vim, _: &BackQuotes, window, cx| { - vim.object(Object::BackQuotes, window, cx) - }); - Vim::action(editor, cx, |vim, _: &MiniQuotes, window, cx| { - vim.object(Object::MiniQuotes, window, cx) - }); - Vim::action(editor, cx, |vim, _: &MiniBrackets, window, cx| { - vim.object(Object::MiniBrackets, window, cx) - }); - Vim::action(editor, cx, |vim, _: &AnyQuotes, window, cx| { - vim.object(Object::AnyQuotes, window, cx) - }); - Vim::action(editor, cx, |vim, _: &AnyBrackets, window, cx| { - vim.object(Object::AnyBrackets, window, cx) - }); - Vim::action(editor, cx, |vim, _: &BackQuotes, window, cx| { - vim.object(Object::BackQuotes, window, cx) - }); - Vim::action(editor, cx, |vim, _: &DoubleQuotes, window, cx| { - vim.object(Object::DoubleQuotes, window, cx) - }); - Vim::action(editor, cx, |vim, action: &Parentheses, window, cx| { - vim.object_impl(Object::Parentheses, action.opening, window, cx) - }); - Vim::action(editor, cx, |vim, action: &SquareBrackets, window, cx| { - vim.object_impl(Object::SquareBrackets, action.opening, window, cx) - }); - Vim::action(editor, cx, |vim, action: &CurlyBrackets, window, cx| { - vim.object_impl(Object::CurlyBrackets, action.opening, window, cx) - }); - Vim::action(editor, cx, |vim, action: &AngleBrackets, window, cx| { - vim.object_impl(Object::AngleBrackets, action.opening, window, cx) - }); - Vim::action(editor, cx, |vim, _: &VerticalBars, window, cx| { - vim.object(Object::VerticalBars, window, cx) - }); - Vim::action(editor, cx, |vim, _: &Argument, window, cx| { - vim.object(Object::Argument, window, cx) - }); - Vim::action(editor, cx, |vim, _: &Method, window, cx| { - vim.object(Object::Method, window, cx) - }); - Vim::action(editor, cx, |vim, _: &Class, window, cx| { - vim.object(Object::Class, window, cx) - }); - Vim::action(editor, cx, |vim, _: &EntireFile, window, cx| { - vim.object(Object::EntireFile, window, cx) - }); - Vim::action(editor, cx, |vim, _: &Comment, window, cx| { - if !matches!(vim.active_operator(), Some(Operator::Object { .. })) { - vim.push_operator(Operator::Object { around: true }, window, cx); - } - vim.object(Object::Comment, window, cx) - }); - Vim::action( - editor, - cx, - |vim, &IndentObj { include_below }: &IndentObj, window, cx| { - vim.object(Object::IndentObj { include_below }, window, cx) - }, - ); -} - -impl Vim { - fn object(&mut self, object: Object, window: &mut Window, cx: &mut Context) { - self.object_impl(object, false, window, cx); - } - - fn object_impl( - &mut self, - object: Object, - opening: bool, - window: &mut Window, - cx: &mut Context, - ) { - let count = Self::take_count(cx); - - match self.mode { - Mode::Normal | Mode::HelixNormal => { - self.normal_object(object, count, opening, window, cx) - } - Mode::Visual | Mode::VisualLine | Mode::VisualBlock | Mode::HelixSelect => { - self.visual_object(object, count, window, cx) - } - Mode::Insert | Mode::Replace => { - // Shouldn't execute a text object in insert mode. Ignoring - } - } - } -} - -impl Object { - pub fn is_multiline(self) -> bool { - match self { - Object::Word { .. } - | Object::Subword { .. } - | Object::Quotes - | Object::BackQuotes - | Object::AnyQuotes - | Object::MiniQuotes - | Object::VerticalBars - | Object::DoubleQuotes => false, - Object::Sentence - | Object::Paragraph - | Object::AnyBrackets - | Object::MiniBrackets - | Object::Parentheses - | Object::Tag - | Object::AngleBrackets - | Object::CurlyBrackets - | Object::SquareBrackets - | Object::Argument - | Object::Method - | Object::Class - | Object::EntireFile - | Object::Comment - | Object::IndentObj { .. } => true, - } - } - - pub fn always_expands_both_ways(self) -> bool { - match self { - Object::Word { .. } - | Object::Subword { .. } - | Object::Sentence - | Object::Paragraph - | Object::Argument - | Object::IndentObj { .. } => false, - Object::Quotes - | Object::BackQuotes - | Object::AnyQuotes - | Object::MiniQuotes - | Object::DoubleQuotes - | Object::VerticalBars - | Object::AnyBrackets - | Object::MiniBrackets - | Object::Parentheses - | Object::SquareBrackets - | Object::Tag - | Object::Method - | Object::Class - | Object::Comment - | Object::EntireFile - | Object::CurlyBrackets - | Object::AngleBrackets => true, - } - } - - pub fn target_visual_mode(self, current_mode: Mode, around: bool) -> Mode { - match self { - Object::Word { .. } - | Object::Subword { .. } - | Object::Sentence - | Object::Quotes - | Object::AnyQuotes - | Object::MiniQuotes - | Object::BackQuotes - | Object::DoubleQuotes => { - if current_mode == Mode::VisualBlock { - Mode::VisualBlock - } else { - Mode::Visual - } - } - Object::Parentheses - | Object::AnyBrackets - | Object::MiniBrackets - | Object::SquareBrackets - | Object::CurlyBrackets - | Object::AngleBrackets - | Object::VerticalBars - | Object::Tag - | Object::Comment - | Object::Argument - | Object::IndentObj { .. } => Mode::Visual, - Object::Method | Object::Class => { - if around { - Mode::VisualLine - } else { - Mode::Visual - } - } - Object::Paragraph | Object::EntireFile => Mode::VisualLine, - } - } - - pub fn range( - self, - map: &DisplaySnapshot, - selection: Selection, - around: bool, - times: Option, - ) -> Option> { - let relative_to = selection.head(); - match self { - Object::Word { ignore_punctuation } => { - if around { - around_word(map, relative_to, ignore_punctuation) - } else { - in_word(map, relative_to, ignore_punctuation) - } - } - Object::Subword { ignore_punctuation } => { - if around { - around_subword(map, relative_to, ignore_punctuation) - } else { - in_subword(map, relative_to, ignore_punctuation) - } - } - Object::Sentence => sentence(map, relative_to, around), - //change others later - Object::Paragraph => paragraph(map, relative_to, around, times.unwrap_or(1)), - Object::Quotes => { - surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'') - } - Object::BackQuotes => { - surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`') - } - Object::AnyQuotes => { - let quote_types = ['\'', '"', '`']; - let cursor_offset = relative_to.to_offset(map, Bias::Left); - - // Find innermost range directly without collecting all ranges - let mut innermost = None; - let mut min_size = usize::MAX; - - // First pass: find innermost enclosing range - for quote in quote_types { - if let Some(range) = surrounding_markers( - map, - relative_to, - around, - self.is_multiline(), - quote, - quote, - ) { - let start_offset = range.start.to_offset(map, Bias::Left); - let end_offset = range.end.to_offset(map, Bias::Right); - - if cursor_offset >= start_offset && cursor_offset <= end_offset { - let size = end_offset - start_offset; - if size < min_size { - min_size = size; - innermost = Some(range); - } - } - } - } - - if let Some(range) = innermost { - return Some(range); - } - - // Fallback: find nearest pair if not inside any quotes - quote_types - .iter() - .flat_map(|"e| { - surrounding_markers( - map, - relative_to, - around, - self.is_multiline(), - quote, - quote, - ) - }) - .min_by_key(|range| { - let start_offset = range.start.to_offset(map, Bias::Left); - let end_offset = range.end.to_offset(map, Bias::Right); - if cursor_offset < start_offset { - (start_offset - cursor_offset) as isize - } else if cursor_offset > end_offset { - (cursor_offset - end_offset) as isize - } else { - 0 - } - }) - } - Object::MiniQuotes => find_mini_quotes(map, relative_to, around), - Object::DoubleQuotes => { - surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"') - } - Object::VerticalBars => { - surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|') - } - Object::Parentheses => { - surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')') - } - Object::Tag => { - let head = selection.head(); - let range = selection.range(); - surrounding_html_tag(map, head, range, around) - } - Object::AnyBrackets => { - let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')]; - let cursor_offset = relative_to.to_offset(map, Bias::Left); - - // Find innermost enclosing bracket range - let mut innermost = None; - let mut min_size = usize::MAX; - - for &(open, close) in bracket_pairs.iter() { - if let Some(range) = surrounding_markers( - map, - relative_to, - around, - self.is_multiline(), - open, - close, - ) { - let start_offset = range.start.to_offset(map, Bias::Left); - let end_offset = range.end.to_offset(map, Bias::Right); - - if cursor_offset >= start_offset && cursor_offset <= end_offset { - let size = end_offset - start_offset; - if size < min_size { - min_size = size; - innermost = Some(range); - } - } - } - } - - if let Some(range) = innermost { - return Some(range); - } - - // Fallback: find nearest bracket pair if not inside any - bracket_pairs - .iter() - .flat_map(|&(open, close)| { - surrounding_markers( - map, - relative_to, - around, - self.is_multiline(), - open, - close, - ) - }) - .min_by_key(|range| { - let start_offset = range.start.to_offset(map, Bias::Left); - let end_offset = range.end.to_offset(map, Bias::Right); - if cursor_offset < start_offset { - (start_offset - cursor_offset) as isize - } else if cursor_offset > end_offset { - (cursor_offset - end_offset) as isize - } else { - 0 - } - }) - } - Object::MiniBrackets => find_mini_brackets(map, relative_to, around), - Object::SquareBrackets => { - surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']') - } - Object::CurlyBrackets => { - surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}') - } - Object::AngleBrackets => { - surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>') - } - Object::Method => text_object( - map, - relative_to, - if around { - TextObject::AroundFunction - } else { - TextObject::InsideFunction - }, - ), - Object::Comment => text_object( - map, - relative_to, - if around { - TextObject::AroundComment - } else { - TextObject::InsideComment - }, - ), - Object::Class => text_object( - map, - relative_to, - if around { - TextObject::AroundClass - } else { - TextObject::InsideClass - }, - ), - Object::Argument => argument(map, relative_to, around), - Object::IndentObj { include_below } => indent(map, relative_to, around, include_below), - Object::EntireFile => entire_file(map), - } - } - - pub fn expand_selection( - self, - map: &DisplaySnapshot, - selection: &mut Selection, - around: bool, - times: Option, - ) -> bool { - if let Some(range) = self.range(map, selection.clone(), around, times) { - selection.start = range.start; - selection.end = range.end; - true - } else { - false - } - } -} - -/// Returns a range that surrounds the word `relative_to` is in. -/// -/// If `relative_to` is at the start of a word, return the word. -/// If `relative_to` is between words, return the space between. -fn in_word( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - ignore_punctuation: bool, -) -> Option> { - // Use motion::right so that we consider the character under the cursor when looking for the start - let classifier = map - .buffer_snapshot() - .char_classifier_at(relative_to.to_point(map)) - .ignore_punctuation(ignore_punctuation); - let start = movement::find_preceding_boundary_display_point( - map, - right(map, relative_to, 1), - movement::FindRange::SingleLine, - |left, right| classifier.kind(left) != classifier.kind(right), - ); - - let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| { - classifier.kind(left) != classifier.kind(right) - }); - - Some(start..end) -} - -fn in_subword( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - ignore_punctuation: bool, -) -> Option> { - let offset = relative_to.to_offset(map, Bias::Left); - // Use motion::right so that we consider the character under the cursor when looking for the start - let classifier = map - .buffer_snapshot() - .char_classifier_at(relative_to.to_point(map)) - .ignore_punctuation(ignore_punctuation); - let in_subword = map - .buffer_chars_at(offset) - .next() - .map(|(c, _)| { - if classifier.is_word('-') { - !classifier.is_whitespace(c) && c != '_' && c != '-' - } else { - !classifier.is_whitespace(c) && c != '_' - } - }) - .unwrap_or(false); - - let start = if in_subword { - movement::find_preceding_boundary_display_point( - map, - right(map, relative_to, 1), - movement::FindRange::SingleLine, - |left, right| { - let is_word_start = classifier.kind(left) != classifier.kind(right); - let is_subword_start = classifier.is_word('-') && left == '-' && right != '-' - || left == '_' && right != '_' - || left.is_lowercase() && right.is_uppercase(); - is_word_start || is_subword_start - }, - ) - } else { - movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| { - let is_word_start = classifier.kind(left) != classifier.kind(right); - let is_subword_start = classifier.is_word('-') && left == '-' && right != '-' - || left == '_' && right != '_' - || left.is_lowercase() && right.is_uppercase(); - is_word_start || is_subword_start - }) - }; - - let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| { - let is_word_end = classifier.kind(left) != classifier.kind(right); - let is_subword_end = classifier.is_word('-') && left != '-' && right == '-' - || left != '_' && right == '_' - || left.is_lowercase() && right.is_uppercase(); - is_word_end || is_subword_end - }); - - Some(start..end) -} - -pub fn surrounding_html_tag( - map: &DisplaySnapshot, - head: DisplayPoint, - range: Range, - around: bool, -) -> Option> { - fn read_tag(chars: impl Iterator) -> String { - chars - .take_while(|c| c.is_alphanumeric() || *c == ':' || *c == '-' || *c == '_' || *c == '.') - .collect() - } - fn open_tag(mut chars: impl Iterator) -> Option { - if Some('<') != chars.next() { - return None; - } - Some(read_tag(chars)) - } - fn close_tag(mut chars: impl Iterator) -> Option { - if (Some('<'), Some('/')) != (chars.next(), chars.next()) { - return None; - } - Some(read_tag(chars)) - } - - let snapshot = &map.buffer_snapshot(); - let offset = head.to_offset(map, Bias::Left); - let mut excerpt = snapshot.excerpt_containing(offset..offset)?; - let buffer = excerpt.buffer(); - let offset = excerpt.map_offset_to_buffer(offset); - - // Find the most closest to current offset - let mut cursor = buffer.syntax_layer_at(offset)?.node().walk(); - let mut last_child_node = cursor.node(); - while cursor.goto_first_child_for_byte(offset.0).is_some() { - last_child_node = cursor.node(); - } - - let mut last_child_node = Some(last_child_node); - while let Some(cur_node) = last_child_node { - if cur_node.child_count() >= 2 { - let first_child = cur_node.child(0); - let last_child = cur_node.child(cur_node.child_count() - 1); - if let (Some(first_child), Some(last_child)) = (first_child, last_child) { - let open_tag = open_tag(buffer.chars_for_range(first_child.byte_range())); - let close_tag = close_tag(buffer.chars_for_range(last_child.byte_range())); - // It needs to be handled differently according to the selection length - let is_valid = if range.end.to_offset(map, Bias::Left) - - range.start.to_offset(map, Bias::Left) - <= 1 - { - offset.0 <= last_child.end_byte() - } else { - excerpt - .map_offset_to_buffer(range.start.to_offset(map, Bias::Left)) - .0 - >= first_child.start_byte() - && excerpt - .map_offset_to_buffer(range.end.to_offset(map, Bias::Left)) - .0 - <= last_child.start_byte() + 1 - }; - if open_tag.is_some() && open_tag == close_tag && is_valid { - let range = if around { - first_child.byte_range().start..last_child.byte_range().end - } else { - first_child.byte_range().end..last_child.byte_range().start - }; - let range = BufferOffset(range.start)..BufferOffset(range.end); - if excerpt.contains_buffer_range(range.clone()) { - let result = excerpt.map_range_from_buffer(range); - return Some( - result.start.to_display_point(map)..result.end.to_display_point(map), - ); - } - } - } - } - last_child_node = cur_node.parent(); - } - None -} - -/// Returns a range that surrounds the word and following whitespace -/// relative_to is in. -/// -/// If `relative_to` is at the start of a word, return the word and following whitespace. -/// If `relative_to` is between words, return the whitespace back and the following word. -/// -/// if in word -/// delete that word -/// if there is whitespace following the word, delete that as well -/// otherwise, delete any preceding whitespace -/// otherwise -/// delete whitespace around cursor -/// delete word following the cursor -fn around_word( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - ignore_punctuation: bool, -) -> Option> { - let offset = relative_to.to_offset(map, Bias::Left); - let classifier = map - .buffer_snapshot() - .char_classifier_at(offset) - .ignore_punctuation(ignore_punctuation); - let in_word = map - .buffer_chars_at(offset) - .next() - .map(|(c, _)| !classifier.is_whitespace(c)) - .unwrap_or(false); - - if in_word { - around_containing_word(map, relative_to, ignore_punctuation) - } else { - around_next_word(map, relative_to, ignore_punctuation) - } -} - -fn around_subword( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - ignore_punctuation: bool, -) -> Option> { - // Use motion::right so that we consider the character under the cursor when looking for the start - let classifier = map - .buffer_snapshot() - .char_classifier_at(relative_to.to_point(map)) - .ignore_punctuation(ignore_punctuation); - let start = movement::find_preceding_boundary_display_point( - map, - right(map, relative_to, 1), - movement::FindRange::SingleLine, - |left, right| { - let is_word_start = classifier.kind(left) != classifier.kind(right); - let is_subword_start = classifier.is_word('-') && left != '-' && right == '-' - || left != '_' && right == '_' - || left.is_lowercase() && right.is_uppercase(); - is_word_start || is_subword_start - }, - ); - - let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| { - let is_word_end = classifier.kind(left) != classifier.kind(right); - let is_subword_end = classifier.is_word('-') && left != '-' && right == '-' - || left != '_' && right == '_' - || left.is_lowercase() && right.is_uppercase(); - is_word_end || is_subword_end - }); - - Some(start..end).map(|range| expand_to_include_whitespace(map, range, true)) -} - -fn around_containing_word( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - ignore_punctuation: bool, -) -> Option> { - in_word(map, relative_to, ignore_punctuation).map(|range| { - let line_start = DisplayPoint::new(range.start.row(), 0); - let is_first_word = map - .buffer_chars_at(line_start.to_offset(map, Bias::Left)) - .take_while(|(ch, offset)| { - offset < &range.start.to_offset(map, Bias::Left) && ch.is_whitespace() - }) - .count() - > 0; - - if is_first_word { - // For first word on line, trim indentation - let mut expanded = expand_to_include_whitespace(map, range.clone(), true); - expanded.start = range.start; - expanded - } else { - expand_to_include_whitespace(map, range, true) - } - }) -} - -fn around_next_word( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - ignore_punctuation: bool, -) -> Option> { - let classifier = map - .buffer_snapshot() - .char_classifier_at(relative_to.to_point(map)) - .ignore_punctuation(ignore_punctuation); - // Get the start of the word - let start = movement::find_preceding_boundary_display_point( - map, - right(map, relative_to, 1), - FindRange::SingleLine, - |left, right| classifier.kind(left) != classifier.kind(right), - ); - - let mut word_found = false; - let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| { - let left_kind = classifier.kind(left); - let right_kind = classifier.kind(right); - - let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n'; - - if right_kind != CharKind::Whitespace { - word_found = true; - } - - found - }); - - Some(start..end) -} - -fn entire_file(map: &DisplaySnapshot) -> Option> { - Some(DisplayPoint::zero()..map.max_point()) -} - -fn text_object( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - target: TextObject, -) -> Option> { - let snapshot = &map.buffer_snapshot(); - let offset = relative_to.to_offset(map, Bias::Left); - - let mut excerpt = snapshot.excerpt_containing(offset..offset)?; - let buffer = excerpt.buffer(); - let offset = excerpt.map_offset_to_buffer(offset); - - let mut matches: Vec> = buffer - .text_object_ranges(offset..offset, TreeSitterOptions::default()) - .filter_map(|(r, m)| if m == target { Some(r) } else { None }) - .collect(); - matches.sort_by_key(|r| r.end - r.start); - if let Some(buffer_range) = matches.first() { - let buffer_range = BufferOffset(buffer_range.start)..BufferOffset(buffer_range.end); - let range = excerpt.map_range_from_buffer(buffer_range); - return Some(range.start.to_display_point(map)..range.end.to_display_point(map)); - } - - let around = target.around()?; - let mut matches: Vec> = buffer - .text_object_ranges(offset..offset, TreeSitterOptions::default()) - .filter_map(|(r, m)| if m == around { Some(r) } else { None }) - .collect(); - matches.sort_by_key(|r| r.end - r.start); - let around_range = matches.first()?; - - let mut matches: Vec> = buffer - .text_object_ranges(around_range.clone(), TreeSitterOptions::default()) - .filter_map(|(r, m)| if m == target { Some(r) } else { None }) - .collect(); - matches.sort_by_key(|r| r.start); - if let Some(buffer_range) = matches.first() - && !buffer_range.is_empty() - { - let buffer_range = BufferOffset(buffer_range.start)..BufferOffset(buffer_range.end); - let range = excerpt.map_range_from_buffer(buffer_range); - return Some(range.start.to_display_point(map)..range.end.to_display_point(map)); - } - let around_range = BufferOffset(around_range.start)..BufferOffset(around_range.end); - let buffer_range = excerpt.map_range_from_buffer(around_range); - return Some(buffer_range.start.to_display_point(map)..buffer_range.end.to_display_point(map)); -} - -fn argument( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - around: bool, -) -> Option> { - let snapshot = &map.buffer_snapshot(); - let offset = relative_to.to_offset(map, Bias::Left); - - // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level - let mut excerpt = snapshot.excerpt_containing(offset..offset)?; - let buffer = excerpt.buffer(); - - fn comma_delimited_range_at( - buffer: &BufferSnapshot, - mut offset: BufferOffset, - include_comma: bool, - ) -> Option> { - // Seek to the first non-whitespace character - offset += buffer - .chars_at(offset) - .take_while(|c| c.is_whitespace()) - .map(char::len_utf8) - .sum::(); - - let bracket_filter = |open: Range, close: Range| { - // Filter out empty ranges - if open.end == close.start { - return false; - } - - // If the cursor is outside the brackets, ignore them - if open.start == offset.0 || close.end == offset.0 { - return false; - } - - // TODO: Is there any better way to filter out string brackets? - // Used to filter out string brackets - matches!( - buffer.chars_at(open.start).next(), - Some('(' | '[' | '{' | '<' | '|') - ) - }; - - // Find the brackets containing the cursor - let (open_bracket, close_bracket) = - buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?; - - let inner_bracket_range = BufferOffset(open_bracket.end)..BufferOffset(close_bracket.start); - - let layer = buffer.syntax_layer_at(offset)?; - let node = layer.node(); - let mut cursor = node.walk(); - - // Loop until we find the smallest node whose parent covers the bracket range. This node is the argument in the parent argument list - let mut parent_covers_bracket_range = false; - loop { - let node = cursor.node(); - let range = node.byte_range(); - let covers_bracket_range = - range.start == open_bracket.start && range.end == close_bracket.end; - if parent_covers_bracket_range && !covers_bracket_range { - break; - } - parent_covers_bracket_range = covers_bracket_range; - - // Unable to find a child node with a parent that covers the bracket range, so no argument to select - cursor.goto_first_child_for_byte(offset.0)?; - } - - let mut argument_node = cursor.node(); - - // If the child node is the open bracket, move to the next sibling. - if argument_node.byte_range() == open_bracket { - if !cursor.goto_next_sibling() { - return Some(inner_bracket_range); - } - argument_node = cursor.node(); - } - // While the child node is the close bracket or a comma, move to the previous sibling - while argument_node.byte_range() == close_bracket || argument_node.kind() == "," { - if !cursor.goto_previous_sibling() { - return Some(inner_bracket_range); - } - argument_node = cursor.node(); - if argument_node.byte_range() == open_bracket { - return Some(inner_bracket_range); - } - } - - // The start and end of the argument range, defaulting to the start and end of the argument node - let mut start = argument_node.start_byte(); - let mut end = argument_node.end_byte(); - - let mut needs_surrounding_comma = include_comma; - - // Seek backwards to find the start of the argument - either the previous comma or the opening bracket. - // We do this because multiple nodes can represent a single argument, such as with rust `vec![a.b.c, d.e.f]` - while cursor.goto_previous_sibling() { - let prev = cursor.node(); - - if prev.start_byte() < open_bracket.end { - start = open_bracket.end; - break; - } else if prev.kind() == "," { - if needs_surrounding_comma { - start = prev.start_byte(); - needs_surrounding_comma = false; - } - break; - } else if prev.start_byte() < start { - start = prev.start_byte(); - } - } - - // Do the same for the end of the argument, extending to next comma or the end of the argument list - while cursor.goto_next_sibling() { - let next = cursor.node(); - - if next.end_byte() > close_bracket.start { - end = close_bracket.start; - break; - } else if next.kind() == "," { - if needs_surrounding_comma { - // Select up to the beginning of the next argument if there is one, otherwise to the end of the comma - if let Some(next_arg) = next.next_sibling() { - end = next_arg.start_byte(); - } else { - end = next.end_byte(); - } - } - break; - } else if next.end_byte() > end { - end = next.end_byte(); - } - } - - Some(BufferOffset(start)..BufferOffset(end)) - } - - let result = comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), around)?; - - if excerpt.contains_buffer_range(result.clone()) { - let result = excerpt.map_range_from_buffer(result); - Some(result.start.to_display_point(map)..result.end.to_display_point(map)) - } else { - None - } -} - -fn indent( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - around: bool, - include_below: bool, -) -> Option> { - let point = relative_to.to_point(map); - let row = point.row; - - let desired_indent = map.line_indent_for_buffer_row(MultiBufferRow(row)); - - // Loop backwards until we find a non-blank line with less indent - let mut start_row = row; - for prev_row in (0..row).rev() { - let indent = map.line_indent_for_buffer_row(MultiBufferRow(prev_row)); - if indent.is_line_empty() { - continue; - } - if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs { - if around { - // When around is true, include the first line with less indent - start_row = prev_row; - } - break; - } - start_row = prev_row; - } - - // Loop forwards until we find a non-blank line with less indent - let mut end_row = row; - let max_rows = map.buffer_snapshot().max_row().0; - for next_row in (row + 1)..=max_rows { - let indent = map.line_indent_for_buffer_row(MultiBufferRow(next_row)); - if indent.is_line_empty() { - continue; - } - if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs { - if around && include_below { - // When around is true and including below, include this line - end_row = next_row; - } - break; - } - end_row = next_row; - } - - let end_len = map.buffer_snapshot().line_len(MultiBufferRow(end_row)); - let start = map.point_to_display_point(Point::new(start_row, 0), Bias::Right); - let end = map.point_to_display_point(Point::new(end_row, end_len), Bias::Left); - Some(start..end) -} - -fn sentence( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - around: bool, -) -> Option> { - let mut start = None; - let relative_offset = relative_to.to_offset(map, Bias::Left); - let mut previous_end = relative_offset; - - let mut chars = map.buffer_chars_at(previous_end).peekable(); - - // Search backwards for the previous sentence end or current sentence start. Include the character under relative_to - for (char, offset) in chars - .peek() - .cloned() - .into_iter() - .chain(map.reverse_buffer_chars_at(previous_end)) - { - if is_sentence_end(map, offset) { - break; - } - - if is_possible_sentence_start(char) { - start = Some(offset); - } - - previous_end = offset; - } - - // Search forward for the end of the current sentence or if we are between sentences, the start of the next one - let mut end = relative_offset; - for (char, offset) in chars { - if start.is_none() && is_possible_sentence_start(char) { - if around { - start = Some(offset); - continue; - } else { - end = offset; - break; - } - } - - if char != '\n' { - end = offset + char.len_utf8(); - } - - if is_sentence_end(map, end) { - break; - } - } - - let mut range = start.unwrap_or(previous_end).to_display_point(map)..end.to_display_point(map); - if around { - range = expand_to_include_whitespace(map, range, false); - } - - Some(range) -} - -fn is_possible_sentence_start(character: char) -> bool { - !character.is_whitespace() && character != '.' -} - -const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?']; -const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\'']; -const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n']; -fn is_sentence_end(map: &DisplaySnapshot, offset: MultiBufferOffset) -> bool { - let mut next_chars = map.buffer_chars_at(offset).peekable(); - if let Some((char, _)) = next_chars.next() { - // We are at a double newline. This position is a sentence end. - if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) { - return true; - } - - // The next text is not a valid whitespace. This is not a sentence end - if !SENTENCE_END_WHITESPACE.contains(&char) { - return false; - } - } - - for (char, _) in map.reverse_buffer_chars_at(offset) { - if SENTENCE_END_PUNCTUATION.contains(&char) { - return true; - } - - if !SENTENCE_END_FILLERS.contains(&char) { - return false; - } - } - - false -} - -/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the -/// whitespace to the end first and falls back to the start if there was none. -pub fn expand_to_include_whitespace( - map: &DisplaySnapshot, - range: Range, - stop_at_newline: bool, -) -> Range { - let mut range = range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right); - let mut whitespace_included = false; - - let chars = map.buffer_chars_at(range.end).peekable(); - for (char, offset) in chars { - if char == '\n' && stop_at_newline { - break; - } - - if char.is_whitespace() { - if char != '\n' { - range.end = offset + char.len_utf8(); - whitespace_included = true; - } - } else { - // Found non whitespace. Quit out. - break; - } - } - - if !whitespace_included { - for (char, point) in map.reverse_buffer_chars_at(range.start) { - if char == '\n' && stop_at_newline { - break; - } - - if !char.is_whitespace() { - break; - } - - range.start = point; - } - } - - range.start.to_display_point(map)..range.end.to_display_point(map) -} - -/// If not `around` (i.e. inner), returns a range that surrounds the paragraph -/// where `relative_to` is in. If `around`, principally returns the range ending -/// at the end of the next paragraph. -/// -/// Here, the "paragraph" is defined as a block of non-blank lines or a block of -/// blank lines. If the paragraph ends with a trailing newline (i.e. not with -/// EOF), the returned range ends at the trailing newline of the paragraph (i.e. -/// the trailing newline is not subject to subsequent operations). -/// -/// Edge cases: -/// - If `around` and if the current paragraph is the last paragraph of the -/// file and is blank, then the selection results in an error. -/// - If `around` and if the current paragraph is the last paragraph of the -/// file and is not blank, then the returned range starts at the start of the -/// previous paragraph, if it exists. -fn paragraph( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - around: bool, - times: usize, -) -> Option> { - let mut paragraph_start = start_of_paragraph(map, relative_to); - let mut paragraph_end = end_of_paragraph(map, relative_to); - - for i in 0..times { - let paragraph_end_row = paragraph_end.row(); - let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row(); - let point = relative_to.to_point(map); - let current_line_is_empty = map - .buffer_snapshot() - .is_line_blank(MultiBufferRow(point.row)); - - if around { - if paragraph_ends_with_eof { - if current_line_is_empty { - return None; - } - - let paragraph_start_buffer_point = paragraph_start.to_point(map); - if paragraph_start_buffer_point.row != 0 { - let previous_paragraph_last_line_start = - Point::new(paragraph_start_buffer_point.row - 1, 0).to_display_point(map); - paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start); - } - } else { - let paragraph_end_buffer_point = paragraph_end.to_point(map); - let mut start_row = paragraph_end_buffer_point.row + 1; - if i > 0 { - start_row += 1; - } - let next_paragraph_start = Point::new(start_row, 0).to_display_point(map); - paragraph_end = end_of_paragraph(map, next_paragraph_start); - } - } - } - - let range = paragraph_start..paragraph_end; - Some(range) -} - -/// Returns a position of the start of the current paragraph, where a paragraph -/// is defined as a run of non-blank lines or a run of blank lines. -pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint { - let point = display_point.to_point(map); - if point.row == 0 { - return DisplayPoint::zero(); - } - - let is_current_line_blank = map - .buffer_snapshot() - .is_line_blank(MultiBufferRow(point.row)); - - for row in (0..point.row).rev() { - let blank = map.buffer_snapshot().is_line_blank(MultiBufferRow(row)); - if blank != is_current_line_blank { - return Point::new(row + 1, 0).to_display_point(map); - } - } - - DisplayPoint::zero() -} - -/// Returns a position of the end of the current paragraph, where a paragraph -/// is defined as a run of non-blank lines or a run of blank lines. -/// The trailing newline is excluded from the paragraph. -pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint { - let point = display_point.to_point(map); - if point.row == map.buffer_snapshot().max_row().0 { - return map.max_point(); - } - - let is_current_line_blank = map - .buffer_snapshot() - .is_line_blank(MultiBufferRow(point.row)); - - for row in point.row + 1..map.buffer_snapshot().max_row().0 + 1 { - let blank = map.buffer_snapshot().is_line_blank(MultiBufferRow(row)); - if blank != is_current_line_blank { - let previous_row = row - 1; - return Point::new( - previous_row, - map.buffer_snapshot().line_len(MultiBufferRow(previous_row)), - ) - .to_display_point(map); - } - } - - map.max_point() -} - -pub fn surrounding_markers( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - around: bool, - search_across_lines: bool, - open_marker: char, - close_marker: char, -) -> Option> { - let point = relative_to.to_offset(map, Bias::Left); - - let mut matched_closes = 0; - let mut opening = None; - - let mut before_ch = match movement::chars_before(map, point).next() { - Some((ch, _)) => ch, - _ => '\0', - }; - if let Some((ch, range)) = movement::chars_after(map, point).next() - && ch == open_marker - && before_ch != '\\' - { - if open_marker == close_marker { - let mut total = 0; - for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows() { - if ch == '\n' { - break; - } - if ch == open_marker && before_ch != '\\' { - total += 1; - } - } - if total % 2 == 0 { - opening = Some(range) - } - } else { - opening = Some(range) - } - } - - if opening.is_none() { - let mut chars_before = movement::chars_before(map, point).peekable(); - while let Some((ch, range)) = chars_before.next() { - if ch == '\n' && !search_across_lines { - break; - } - - if let Some((before_ch, _)) = chars_before.peek() - && *before_ch == '\\' - { - continue; - } - - if ch == open_marker { - if matched_closes == 0 { - opening = Some(range); - break; - } - matched_closes -= 1; - } else if ch == close_marker { - matched_closes += 1 - } - } - } - if opening.is_none() { - for (ch, range) in movement::chars_after(map, point) { - if before_ch != '\\' { - if ch == open_marker { - opening = Some(range); - break; - } else if ch == close_marker { - break; - } - } - - before_ch = ch; - } - } - - let mut opening = opening?; - - let mut matched_opens = 0; - let mut closing = None; - before_ch = match movement::chars_before(map, opening.end).next() { - Some((ch, _)) => ch, - _ => '\0', - }; - for (ch, range) in movement::chars_after(map, opening.end) { - if ch == '\n' && !search_across_lines { - break; - } - - if before_ch != '\\' { - if ch == close_marker { - if matched_opens == 0 { - closing = Some(range); - break; - } - matched_opens -= 1; - } else if ch == open_marker { - matched_opens += 1; - } - } - - before_ch = ch; - } - - let mut closing = closing?; - - if around && !search_across_lines { - let mut found = false; - - for (ch, range) in movement::chars_after(map, closing.end) { - if ch.is_whitespace() && ch != '\n' { - found = true; - closing.end = range.end; - } else { - break; - } - } - - if !found { - for (ch, range) in movement::chars_before(map, opening.start) { - if ch.is_whitespace() && ch != '\n' { - opening.start = range.start - } else { - break; - } - } - } - } - - // Adjust selection to remove leading and trailing whitespace for multiline inner brackets - if !around && open_marker != close_marker { - let start_point = opening.end.to_display_point(map); - let end_point = closing.start.to_display_point(map); - let start_offset = start_point.to_offset(map, Bias::Left); - let end_offset = end_point.to_offset(map, Bias::Left); - - if start_point.row() != end_point.row() - && map - .buffer_chars_at(start_offset) - .take_while(|(_, offset)| offset < &end_offset) - .any(|(ch, _)| !ch.is_whitespace()) - { - let mut first_non_ws = None; - let mut last_non_ws = None; - for (ch, offset) in map.buffer_chars_at(start_offset) { - if !ch.is_whitespace() { - first_non_ws = Some(offset); - break; - } - } - for (ch, offset) in map.reverse_buffer_chars_at(end_offset) { - if !ch.is_whitespace() { - last_non_ws = Some(offset + ch.len_utf8()); - break; - } - } - if let Some(start) = first_non_ws { - opening.end = start; - } - if let Some(end) = last_non_ws { - closing.start = end; - } - } - } - - let result = if around { - opening.start..closing.end - } else { - opening.end..closing.start - }; - - Some( - map.clip_point(result.start.to_display_point(map), Bias::Left) - ..map.clip_point(result.end.to_display_point(map), Bias::Right), - ) -} - -#[cfg(test)] -mod test { - use editor::{Editor, EditorMode, MultiBuffer, test::editor_test_context::EditorTestContext}; - use gpui::KeyBinding; - use indoc::indoc; - use text::Point; - - use crate::{ - object::{AnyBrackets, AnyQuotes, MiniBrackets}, - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - - const WORD_LOCATIONS: &str = indoc! {" - The quick ˇbrowˇnˇ••• - fox ˇjuˇmpsˇ over - the lazy dogˇ•• - ˇ - ˇ - ˇ - Thˇeˇ-ˇquˇickˇ ˇbrownˇ• - ˇ•• - ˇ•• - ˇ fox-jumpˇs over - the lazy dogˇ• - ˇ - " - }; - - #[gpui::test] - async fn test_change_word_object(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.simulate_at_each_offset("c i w", WORD_LOCATIONS) - .await - .assert_matches(); - cx.simulate_at_each_offset("c i shift-w", WORD_LOCATIONS) - .await - .assert_matches(); - cx.simulate_at_each_offset("c a w", WORD_LOCATIONS) - .await - .assert_matches(); - cx.simulate_at_each_offset("c a shift-w", WORD_LOCATIONS) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_word_object(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.simulate_at_each_offset("d i w", WORD_LOCATIONS) - .await - .assert_matches(); - cx.simulate_at_each_offset("d i shift-w", WORD_LOCATIONS) - .await - .assert_matches(); - cx.simulate_at_each_offset("d a w", WORD_LOCATIONS) - .await - .assert_matches(); - cx.simulate_at_each_offset("d a shift-w", WORD_LOCATIONS) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_visual_word_object(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - /* - cx.set_shared_state("The quick ˇbrown\nfox").await; - cx.simulate_shared_keystrokes(["v"]).await; - cx.assert_shared_state("The quick «bˇ»rown\nfox").await; - cx.simulate_shared_keystrokes(["i", "w"]).await; - cx.assert_shared_state("The quick «brownˇ»\nfox").await; - */ - cx.set_shared_state("The quick brown\nˇ\nfox").await; - cx.simulate_shared_keystrokes("v").await; - cx.shared_state() - .await - .assert_eq("The quick brown\n«\nˇ»fox"); - cx.simulate_shared_keystrokes("i w").await; - cx.shared_state() - .await - .assert_eq("The quick brown\n«\nˇ»fox"); - - cx.simulate_at_each_offset("v i w", WORD_LOCATIONS) - .await - .assert_matches(); - cx.simulate_at_each_offset("v i shift-w", WORD_LOCATIONS) - .await - .assert_matches(); - } - - const PARAGRAPH_EXAMPLES: &[&str] = &[ - // Single line - "ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ", - // Multiple lines without empty lines - indoc! {" - ˇThe quick brownˇ - ˇfox jumps overˇ - the lazy dog.ˇ - "}, - // Heading blank paragraph and trailing normal paragraph - indoc! {" - ˇ - ˇ - ˇThe quick brown fox jumps - ˇover the lazy dog. - ˇ - ˇ - ˇThe quick brown fox jumpsˇ - ˇover the lazy dog.ˇ - "}, - // Inserted blank paragraph and trailing blank paragraph - indoc! {" - ˇThe quick brown fox jumps - ˇover the lazy dog. - ˇ - ˇ - ˇ - ˇThe quick brown fox jumpsˇ - ˇover the lazy dog.ˇ - ˇ - ˇ - ˇ - "}, - // "Blank" paragraph with whitespace characters - indoc! {" - ˇThe quick brown fox jumps - over the lazy dog. - - ˇ \t - - ˇThe quick brown fox jumps - over the lazy dog.ˇ - ˇ - ˇ \t - \t \t - "}, - // Single line "paragraphs", where selection size might be zero. - indoc! {" - ˇThe quick brown fox jumps over the lazy dog. - ˇ - ˇThe quick brown fox jumpˇs over the lazy dog.ˇ - ˇ - "}, - ]; - - #[gpui::test] - async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - for paragraph_example in PARAGRAPH_EXAMPLES { - cx.simulate_at_each_offset("c i p", paragraph_example) - .await - .assert_matches(); - cx.simulate_at_each_offset("c a p", paragraph_example) - .await - .assert_matches(); - } - } - - #[gpui::test] - async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - for paragraph_example in PARAGRAPH_EXAMPLES { - cx.simulate_at_each_offset("d i p", paragraph_example) - .await - .assert_matches(); - cx.simulate_at_each_offset("d a p", paragraph_example) - .await - .assert_matches(); - } - } - - #[gpui::test] - async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - const EXAMPLES: &[&str] = &[ - indoc! {" - ˇThe quick brown - fox jumps over - the lazy dog. - "}, - indoc! {" - ˇ - - ˇThe quick brown fox jumps - over the lazy dog. - ˇ - - ˇThe quick brown fox jumps - over the lazy dog. - "}, - indoc! {" - ˇThe quick brown fox jumps over the lazy dog. - ˇ - ˇThe quick brown fox jumps over the lazy dog. - - "}, - ]; - - for paragraph_example in EXAMPLES { - cx.simulate_at_each_offset("v i p", paragraph_example) - .await - .assert_matches(); - cx.simulate_at_each_offset("v a p", paragraph_example) - .await - .assert_matches(); - } - } - - #[gpui::test] - async fn test_change_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - const WRAPPING_EXAMPLE: &str = indoc! {" - ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines. - - ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly. - - ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ - "}; - - cx.set_shared_wrap(20).await; - - cx.simulate_at_each_offset("c i p", WRAPPING_EXAMPLE) - .await - .assert_matches(); - cx.simulate_at_each_offset("c a p", WRAPPING_EXAMPLE) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - const WRAPPING_EXAMPLE: &str = indoc! {" - ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines. - - ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly. - - ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ - "}; - - cx.set_shared_wrap(20).await; - - cx.simulate_at_each_offset("d i p", WRAPPING_EXAMPLE) - .await - .assert_matches(); - cx.simulate_at_each_offset("d a p", WRAPPING_EXAMPLE) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_paragraph_whitespace(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - a - ˇ• - aaaaaaaaaaaaa - "}) - .await; - - cx.simulate_shared_keystrokes("d i p").await; - cx.shared_state().await.assert_eq(indoc! {" - a - aaaaaaaˇaaaaaa - "}); - } - - #[gpui::test] - async fn test_visual_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - const WRAPPING_EXAMPLE: &str = indoc! {" - ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines. - - ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly. - - ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ - "}; - - cx.set_shared_wrap(20).await; - - cx.simulate_at_each_offset("v i p", WRAPPING_EXAMPLE) - .await - .assert_matches(); - cx.simulate_at_each_offset("v a p", WRAPPING_EXAMPLE) - .await - .assert_matches(); - } - - // Test string with "`" for opening surrounders and "'" for closing surrounders - const SURROUNDING_MARKER_STRING: &str = indoc! {" - ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn` - 'ˇfox juˇmps ov`ˇer - the ˇlazy d'o`ˇg"}; - - const SURROUNDING_OBJECTS: &[(char, char)] = &[ - ('"', '"'), // Double Quote - ('(', ')'), // Parentheses - ]; - - #[gpui::test] - async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - for (start, end) in SURROUNDING_OBJECTS { - let marked_string = SURROUNDING_MARKER_STRING - .replace('`', &start.to_string()) - .replace('\'', &end.to_string()); - - cx.simulate_at_each_offset(&format!("c i {start}"), &marked_string) - .await - .assert_matches(); - cx.simulate_at_each_offset(&format!("c i {end}"), &marked_string) - .await - .assert_matches(); - cx.simulate_at_each_offset(&format!("c a {start}"), &marked_string) - .await - .assert_matches(); - cx.simulate_at_each_offset(&format!("c a {end}"), &marked_string) - .await - .assert_matches(); - } - } - #[gpui::test] - async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_wrap(12).await; - - cx.set_shared_state(indoc! { - "\"ˇhello world\"!" - }) - .await; - cx.simulate_shared_keystrokes("v i \"").await; - cx.shared_state().await.assert_eq(indoc! { - "\"«hello worldˇ»\"!" - }); - - cx.set_shared_state(indoc! { - "\"hˇello world\"!" - }) - .await; - cx.simulate_shared_keystrokes("v i \"").await; - cx.shared_state().await.assert_eq(indoc! { - "\"«hello worldˇ»\"!" - }); - - cx.set_shared_state(indoc! { - "helˇlo \"world\"!" - }) - .await; - cx.simulate_shared_keystrokes("v i \"").await; - cx.shared_state().await.assert_eq(indoc! { - "hello \"«worldˇ»\"!" - }); - - cx.set_shared_state(indoc! { - "hello \"wˇorld\"!" - }) - .await; - cx.simulate_shared_keystrokes("v i \"").await; - cx.shared_state().await.assert_eq(indoc! { - "hello \"«worldˇ»\"!" - }); - - cx.set_shared_state(indoc! { - "hello \"wˇorld\"!" - }) - .await; - cx.simulate_shared_keystrokes("v a \"").await; - cx.shared_state().await.assert_eq(indoc! { - "hello« \"world\"ˇ»!" - }); - - cx.set_shared_state(indoc! { - "hello \"wˇorld\" !" - }) - .await; - cx.simulate_shared_keystrokes("v a \"").await; - cx.shared_state().await.assert_eq(indoc! { - "hello «\"world\" ˇ»!" - }); - - cx.set_shared_state(indoc! { - "hello \"wˇorld\"• - goodbye" - }) - .await; - cx.simulate_shared_keystrokes("v a \"").await; - cx.shared_state().await.assert_eq(indoc! { - "hello «\"world\" ˇ» - goodbye" - }); - } - - #[gpui::test] - async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! { - "func empty(a string) bool { - if a == \"\" { - return true - } - ˇreturn false - }" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("v i {"); - cx.assert_state( - indoc! { - "func empty(a string) bool { - «if a == \"\" { - return true - } - return falseˇ» - }" - }, - Mode::Visual, - ); - - cx.set_state( - indoc! { - "func empty(a string) bool { - if a == \"\" { - ˇreturn true - } - return false - }" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("v i {"); - cx.assert_state( - indoc! { - "func empty(a string) bool { - if a == \"\" { - «return trueˇ» - } - return false - }" - }, - Mode::Visual, - ); - - cx.set_state( - indoc! { - "func empty(a string) bool { - if a == \"\" ˇ{ - return true - } - return false - }" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("v i {"); - cx.assert_state( - indoc! { - "func empty(a string) bool { - if a == \"\" { - «return trueˇ» - } - return false - }" - }, - Mode::Visual, - ); - - cx.set_state( - indoc! { - "func empty(a string) bool { - if a == \"\" { - return true - } - return false - ˇ}" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("v i {"); - cx.assert_state( - indoc! { - "func empty(a string) bool { - «if a == \"\" { - return true - } - return falseˇ» - }" - }, - Mode::Visual, - ); - - cx.set_state( - indoc! { - "func empty(a string) bool { - if a == \"\" { - ˇ - - }" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("c i {"); - cx.assert_state( - indoc! { - "func empty(a string) bool { - if a == \"\" {ˇ}" - }, - Mode::Insert, - ); - } - - #[gpui::test] - async fn test_singleline_surrounding_character_objects_with_escape( - cx: &mut gpui::TestAppContext, - ) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - "h\"e\\\"lˇlo \\\"world\"!" - }) - .await; - cx.simulate_shared_keystrokes("v i \"").await; - cx.shared_state().await.assert_eq(indoc! { - "h\"«e\\\"llo \\\"worldˇ»\"!" - }); - - cx.set_shared_state(indoc! { - "hello \"teˇst \\\"inside\\\" world\"" - }) - .await; - cx.simulate_shared_keystrokes("v i \"").await; - cx.shared_state().await.assert_eq(indoc! { - "hello \"«test \\\"inside\\\" worldˇ»\"" - }); - } - - #[gpui::test] - async fn test_vertical_bars(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.set_state( - indoc! {" - fn boop() { - baz(ˇ|a, b| { bar(|j, k| { })}) - }" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("c i |"); - cx.assert_state( - indoc! {" - fn boop() { - baz(|ˇ| { bar(|j, k| { })}) - }" - }, - Mode::Insert, - ); - cx.simulate_keystrokes("escape 1 8 |"); - cx.assert_state( - indoc! {" - fn boop() { - baz(|| { bar(ˇ|j, k| { })}) - }" - }, - Mode::Normal, - ); - - cx.simulate_keystrokes("v a |"); - cx.assert_state( - indoc! {" - fn boop() { - baz(|| { bar(«|j, k| ˇ»{ })}) - }" - }, - Mode::Visual, - ); - } - - #[gpui::test] - async fn test_argument_object(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Generic arguments - cx.set_state("fn boop() {}", Mode::Normal); - cx.simulate_keystrokes("v i a"); - cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual); - - // Function arguments - cx.set_state( - "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}", - Mode::Normal, - ); - cx.simulate_keystrokes("d a a"); - cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal); - - cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal); - cx.simulate_keystrokes("v a a"); - cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual); - - // Tuple, vec, and array arguments - cx.set_state( - "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}", - Mode::Normal, - ); - cx.simulate_keystrokes("c i a"); - cx.assert_state( - "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}", - Mode::Insert, - ); - - // TODO regressed with the up-to-date Rust grammar. - // cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal); - // cx.simulate_keystrokes("c a a"); - // cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert); - - cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal); - cx.simulate_keystrokes("c i a"); - cx.assert_state("let a = [ˇ, 300];", Mode::Insert); - - cx.set_state( - "let a = vec![Vec::new(), vecˇ![test::call(), 300]];", - Mode::Normal, - ); - cx.simulate_keystrokes("c a a"); - cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert); - - // Cursor immediately before / after brackets - cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal); - cx.simulate_keystrokes("v i a"); - cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual); - - cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal); - cx.simulate_keystrokes("v i a"); - cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual); - } - - #[gpui::test] - async fn test_indent_object(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Base use case - cx.set_state( - indoc! {" - fn boop() { - // Comment - baz();ˇ - - loop { - bar(1); - bar(2); - } - - result - } - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("v i i"); - cx.assert_state( - indoc! {" - fn boop() { - « // Comment - baz(); - - loop { - bar(1); - bar(2); - } - - resultˇ» - } - "}, - Mode::Visual, - ); - - // Around indent (include line above) - cx.set_state( - indoc! {" - const ABOVE: str = true; - fn boop() { - - hello(); - worˇld() - } - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("v a i"); - cx.assert_state( - indoc! {" - const ABOVE: str = true; - «fn boop() { - - hello(); - world()ˇ» - } - "}, - Mode::Visual, - ); - - // Around indent (include line above & below) - cx.set_state( - indoc! {" - const ABOVE: str = true; - fn boop() { - hellˇo(); - world() - - } - const BELOW: str = true; - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("c a shift-i"); - cx.assert_state( - indoc! {" - const ABOVE: str = true; - ˇ - const BELOW: str = true; - "}, - Mode::Insert, - ); - } - - #[gpui::test] - async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - for (start, end) in SURROUNDING_OBJECTS { - let marked_string = SURROUNDING_MARKER_STRING - .replace('`', &start.to_string()) - .replace('\'', &end.to_string()); - - cx.simulate_at_each_offset(&format!("d i {start}"), &marked_string) - .await - .assert_matches(); - cx.simulate_at_each_offset(&format!("d i {end}"), &marked_string) - .await - .assert_matches(); - cx.simulate_at_each_offset(&format!("d a {start}"), &marked_string) - .await - .assert_matches(); - cx.simulate_at_each_offset(&format!("d a {end}"), &marked_string) - .await - .assert_matches(); - } - } - - #[gpui::test] - async fn test_anyquotes_object(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "q", - AnyQuotes, - Some("vim_operator == a || vim_operator == i || vim_operator == cs"), - )]); - }); - - const TEST_CASES: &[(&str, &str, &str, Mode)] = &[ - // the false string in the middle should be considered - ( - "c i q", - "'first' false ˇstring 'second'", - "'first'ˇ'second'", - Mode::Insert, - ), - // Single quotes - ( - "c i q", - "Thisˇ is a 'quote' example.", - "This is a 'ˇ' example.", - Mode::Insert, - ), - ( - "c a q", - "Thisˇ is a 'quote' example.", - "This is a ˇexample.", - Mode::Insert, - ), - ( - "c i q", - "This is a \"simple 'qˇuote'\" example.", - "This is a \"simple 'ˇ'\" example.", - Mode::Insert, - ), - ( - "c a q", - "This is a \"simple 'qˇuote'\" example.", - "This is a \"simpleˇ\" example.", - Mode::Insert, - ), - ( - "c i q", - "This is a 'qˇuote' example.", - "This is a 'ˇ' example.", - Mode::Insert, - ), - ( - "c a q", - "This is a 'qˇuote' example.", - "This is a ˇexample.", - Mode::Insert, - ), - ( - "d i q", - "This is a 'qˇuote' example.", - "This is a 'ˇ' example.", - Mode::Normal, - ), - ( - "d a q", - "This is a 'qˇuote' example.", - "This is a ˇexample.", - Mode::Normal, - ), - // Double quotes - ( - "c i q", - "This is a \"qˇuote\" example.", - "This is a \"ˇ\" example.", - Mode::Insert, - ), - ( - "c a q", - "This is a \"qˇuote\" example.", - "This is a ˇexample.", - Mode::Insert, - ), - ( - "d i q", - "This is a \"qˇuote\" example.", - "This is a \"ˇ\" example.", - Mode::Normal, - ), - ( - "d a q", - "This is a \"qˇuote\" example.", - "This is a ˇexample.", - Mode::Normal, - ), - // Back quotes - ( - "c i q", - "This is a `qˇuote` example.", - "This is a `ˇ` example.", - Mode::Insert, - ), - ( - "c a q", - "This is a `qˇuote` example.", - "This is a ˇexample.", - Mode::Insert, - ), - ( - "d i q", - "This is a `qˇuote` example.", - "This is a `ˇ` example.", - Mode::Normal, - ), - ( - "d a q", - "This is a `qˇuote` example.", - "This is a ˇexample.", - Mode::Normal, - ), - ]; - - for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES { - cx.set_state(initial_state, Mode::Normal); - - cx.simulate_keystrokes(keystrokes); - - cx.assert_state(expected_state, *expected_mode); - } - - const INVALID_CASES: &[(&str, &str, Mode)] = &[ - ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote - ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote - ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote - ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote - ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote - ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote - ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote - ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote - ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote - ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote - ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote - ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote - ]; - - for (keystrokes, initial_state, mode) in INVALID_CASES { - cx.set_state(initial_state, Mode::Normal); - - cx.simulate_keystrokes(keystrokes); - - cx.assert_state(initial_state, *mode); - } - } - - #[gpui::test] - async fn test_miniquotes_object(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new_typescript(cx).await; - - const TEST_CASES: &[(&str, &str, &str, Mode)] = &[ - // Special cases from mini.ai plugin - // the false string in the middle should not be considered - ( - "c i q", - "'first' false ˇstring 'second'", - "'first' false string 'ˇ'", - Mode::Insert, - ), - // Multiline support :)! Same behavior as mini.ai plugin - ( - "c i q", - indoc! {" - ` - first - middle ˇstring - second - ` - "}, - indoc! {" - `ˇ` - "}, - Mode::Insert, - ), - // If you are in the close quote and it is the only quote in the buffer, it should replace inside the quote - // This is not working with the core motion ci' for this special edge case, so I am happy to fix it in MiniQuotes :) - // Bug reference: https://github.com/zed-industries/zed/issues/23889 - ("c i q", "'quote«'ˇ»", "'ˇ'", Mode::Insert), - // Single quotes - ( - "c i q", - "Thisˇ is a 'quote' example.", - "This is a 'ˇ' example.", - Mode::Insert, - ), - ( - "c a q", - "Thisˇ is a 'quote' example.", - "This is a ˇ example.", // same mini.ai plugin behavior - Mode::Insert, - ), - ( - "c i q", - "This is a \"simple 'qˇuote'\" example.", - "This is a \"ˇ\" example.", // Not supported by Tree-sitter queries for now - Mode::Insert, - ), - ( - "c a q", - "This is a \"simple 'qˇuote'\" example.", - "This is a ˇ example.", // Not supported by Tree-sitter queries for now - Mode::Insert, - ), - ( - "c i q", - "This is a 'qˇuote' example.", - "This is a 'ˇ' example.", - Mode::Insert, - ), - ( - "c a q", - "This is a 'qˇuote' example.", - "This is a ˇ example.", // same mini.ai plugin behavior - Mode::Insert, - ), - ( - "d i q", - "This is a 'qˇuote' example.", - "This is a 'ˇ' example.", - Mode::Normal, - ), - ( - "d a q", - "This is a 'qˇuote' example.", - "This is a ˇ example.", // same mini.ai plugin behavior - Mode::Normal, - ), - // Double quotes - ( - "c i q", - "This is a \"qˇuote\" example.", - "This is a \"ˇ\" example.", - Mode::Insert, - ), - ( - "c a q", - "This is a \"qˇuote\" example.", - "This is a ˇ example.", // same mini.ai plugin behavior - Mode::Insert, - ), - ( - "d i q", - "This is a \"qˇuote\" example.", - "This is a \"ˇ\" example.", - Mode::Normal, - ), - ( - "d a q", - "This is a \"qˇuote\" example.", - "This is a ˇ example.", // same mini.ai plugin behavior - Mode::Normal, - ), - // Back quotes - ( - "c i q", - "This is a `qˇuote` example.", - "This is a `ˇ` example.", - Mode::Insert, - ), - ( - "c a q", - "This is a `qˇuote` example.", - "This is a ˇ example.", // same mini.ai plugin behavior - Mode::Insert, - ), - ( - "d i q", - "This is a `qˇuote` example.", - "This is a `ˇ` example.", - Mode::Normal, - ), - ( - "d a q", - "This is a `qˇuote` example.", - "This is a ˇ example.", // same mini.ai plugin behavior - Mode::Normal, - ), - ]; - - for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES { - cx.set_state(initial_state, Mode::Normal); - - cx.simulate_keystrokes(keystrokes); - - cx.assert_state(expected_state, *expected_mode); - } - - const INVALID_CASES: &[(&str, &str, Mode)] = &[ - ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote - ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote - ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote - ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote - ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote - ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote - ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote - ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote - ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote - ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote - ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote - ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote - ]; - - for (keystrokes, initial_state, mode) in INVALID_CASES { - cx.set_state(initial_state, Mode::Normal); - - cx.simulate_keystrokes(keystrokes); - - cx.assert_state(initial_state, *mode); - } - } - - #[gpui::test] - async fn test_anybrackets_object(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "b", - AnyBrackets, - Some("vim_operator == a || vim_operator == i || vim_operator == cs"), - )]); - }); - - const TEST_CASES: &[(&str, &str, &str, Mode)] = &[ - ( - "c i b", - indoc! {" - { - { - ˇprint('hello') - } - } - "}, - indoc! {" - { - { - ˇ - } - } - "}, - Mode::Insert, - ), - // Bracket (Parentheses) - ( - "c i b", - "Thisˇ is a (simple [quote]) example.", - "This is a (ˇ) example.", - Mode::Insert, - ), - ( - "c i b", - "This is a [simple (qˇuote)] example.", - "This is a [simple (ˇ)] example.", - Mode::Insert, - ), - ( - "c a b", - "This is a [simple (qˇuote)] example.", - "This is a [simple ˇ] example.", - Mode::Insert, - ), - ( - "c a b", - "Thisˇ is a (simple [quote]) example.", - "This is a ˇ example.", - Mode::Insert, - ), - ( - "c i b", - "This is a (qˇuote) example.", - "This is a (ˇ) example.", - Mode::Insert, - ), - ( - "c a b", - "This is a (qˇuote) example.", - "This is a ˇ example.", - Mode::Insert, - ), - ( - "d i b", - "This is a (qˇuote) example.", - "This is a (ˇ) example.", - Mode::Normal, - ), - ( - "d a b", - "This is a (qˇuote) example.", - "This is a ˇ example.", - Mode::Normal, - ), - // Square brackets - ( - "c i b", - "This is a [qˇuote] example.", - "This is a [ˇ] example.", - Mode::Insert, - ), - ( - "c a b", - "This is a [qˇuote] example.", - "This is a ˇ example.", - Mode::Insert, - ), - ( - "d i b", - "This is a [qˇuote] example.", - "This is a [ˇ] example.", - Mode::Normal, - ), - ( - "d a b", - "This is a [qˇuote] example.", - "This is a ˇ example.", - Mode::Normal, - ), - // Curly brackets - ( - "c i b", - "This is a {qˇuote} example.", - "This is a {ˇ} example.", - Mode::Insert, - ), - ( - "c a b", - "This is a {qˇuote} example.", - "This is a ˇ example.", - Mode::Insert, - ), - ( - "d i b", - "This is a {qˇuote} example.", - "This is a {ˇ} example.", - Mode::Normal, - ), - ( - "d a b", - "This is a {qˇuote} example.", - "This is a ˇ example.", - Mode::Normal, - ), - ]; - - for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES { - cx.set_state(initial_state, Mode::Normal); - - cx.simulate_keystrokes(keystrokes); - - cx.assert_state(expected_state, *expected_mode); - } - - const INVALID_CASES: &[(&str, &str, Mode)] = &[ - ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket - ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket - ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket - ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket - ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket - ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket - ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket - ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket - ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket - ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket - ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket - ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket - ]; - - for (keystrokes, initial_state, mode) in INVALID_CASES { - cx.set_state(initial_state, Mode::Normal); - - cx.simulate_keystrokes(keystrokes); - - cx.assert_state(initial_state, *mode); - } - } - - #[gpui::test] - async fn test_minibrackets_object(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "b", - MiniBrackets, - Some("vim_operator == a || vim_operator == i || vim_operator == cs"), - )]); - }); - - const TEST_CASES: &[(&str, &str, &str, Mode)] = &[ - // Special cases from mini.ai plugin - // Current line has more priority for the cover or next algorithm, to avoid changing curly brackets which is supper anoying - // Same behavior as mini.ai plugin - ( - "c i b", - indoc! {" - { - { - ˇprint('hello') - } - } - "}, - indoc! {" - { - { - print(ˇ) - } - } - "}, - Mode::Insert, - ), - // If the current line doesn't have brackets then it should consider if the caret is inside an external bracket - // Same behavior as mini.ai plugin - ( - "c i b", - indoc! {" - { - { - ˇ - print('hello') - } - } - "}, - indoc! {" - { - {ˇ} - } - "}, - Mode::Insert, - ), - // If you are in the open bracket then it has higher priority - ( - "c i b", - indoc! {" - «{ˇ» - { - print('hello') - } - } - "}, - indoc! {" - {ˇ} - "}, - Mode::Insert, - ), - // If you are in the close bracket then it has higher priority - ( - "c i b", - indoc! {" - { - { - print('hello') - } - «}ˇ» - "}, - indoc! {" - {ˇ} - "}, - Mode::Insert, - ), - // Bracket (Parentheses) - ( - "c i b", - "Thisˇ is a (simple [quote]) example.", - "This is a (ˇ) example.", - Mode::Insert, - ), - ( - "c i b", - "This is a [simple (qˇuote)] example.", - "This is a [simple (ˇ)] example.", - Mode::Insert, - ), - ( - "c a b", - "This is a [simple (qˇuote)] example.", - "This is a [simple ˇ] example.", - Mode::Insert, - ), - ( - "c a b", - "Thisˇ is a (simple [quote]) example.", - "This is a ˇ example.", - Mode::Insert, - ), - ( - "c i b", - "This is a (qˇuote) example.", - "This is a (ˇ) example.", - Mode::Insert, - ), - ( - "c a b", - "This is a (qˇuote) example.", - "This is a ˇ example.", - Mode::Insert, - ), - ( - "d i b", - "This is a (qˇuote) example.", - "This is a (ˇ) example.", - Mode::Normal, - ), - ( - "d a b", - "This is a (qˇuote) example.", - "This is a ˇ example.", - Mode::Normal, - ), - // Square brackets - ( - "c i b", - "This is a [qˇuote] example.", - "This is a [ˇ] example.", - Mode::Insert, - ), - ( - "c a b", - "This is a [qˇuote] example.", - "This is a ˇ example.", - Mode::Insert, - ), - ( - "d i b", - "This is a [qˇuote] example.", - "This is a [ˇ] example.", - Mode::Normal, - ), - ( - "d a b", - "This is a [qˇuote] example.", - "This is a ˇ example.", - Mode::Normal, - ), - // Curly brackets - ( - "c i b", - "This is a {qˇuote} example.", - "This is a {ˇ} example.", - Mode::Insert, - ), - ( - "c a b", - "This is a {qˇuote} example.", - "This is a ˇ example.", - Mode::Insert, - ), - ( - "d i b", - "This is a {qˇuote} example.", - "This is a {ˇ} example.", - Mode::Normal, - ), - ( - "d a b", - "This is a {qˇuote} example.", - "This is a ˇ example.", - Mode::Normal, - ), - ]; - - for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES { - cx.set_state(initial_state, Mode::Normal); - - cx.simulate_keystrokes(keystrokes); - - cx.assert_state(expected_state, *expected_mode); - } - - const INVALID_CASES: &[(&str, &str, Mode)] = &[ - ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket - ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket - ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket - ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket - ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket - ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket - ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket - ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket - ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket - ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket - ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket - ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket - ]; - - for (keystrokes, initial_state, mode) in INVALID_CASES { - cx.set_state(initial_state, Mode::Normal); - - cx.simulate_keystrokes(keystrokes); - - cx.assert_state(initial_state, *mode); - } - } - - #[gpui::test] - async fn test_minibrackets_multibuffer(cx: &mut gpui::TestAppContext) { - // Initialize test context with the TypeScript language loaded, so we - // can actually get brackets definition. - let mut cx = VimTestContext::new(cx, true).await; - - // Update `b` to `MiniBrackets` so we can later use it when simulating - // keystrokes. - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new("b", MiniBrackets, None)]); - }); - - let (editor, cx) = cx.add_window_view(|window, cx| { - let multi_buffer = MultiBuffer::build_multi( - [ - ("111\n222\n333\n444\n", vec![Point::row_range(0..2)]), - ("111\na {bracket} example\n", vec![Point::row_range(0..2)]), - ], - cx, - ); - - // In order for the brackets to actually be found, we need to update - // the language used for the second buffer. This is something that - // is handled automatically when simply using `VimTestContext::new` - // but, since this is being set manually, the language isn't - // automatically set. - let editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx); - let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids(); - if let Some(buffer) = multi_buffer.read(cx).buffer(buffer_ids[1]) { - buffer.update(cx, |buffer, cx| { - buffer.set_language(Some(language::rust_lang()), cx); - }) - }; - - editor - }); - - let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await; - - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - ˇ111 - 222 - [EXCERPT] - 111 - a {bracket} example - " - }); - - cx.simulate_keystrokes("j j j j f r"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - 111 - 222 - [EXCERPT] - 111 - a {bˇracket} example - " - }); - - cx.simulate_keystrokes("d i b"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - 111 - 222 - [EXCERPT] - 111 - a {ˇ} example - " - }); - } - - #[gpui::test] - async fn test_minibrackets_trailing_space(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("(trailingˇ whitespace )") - .await; - cx.simulate_shared_keystrokes("v i b").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("escape y i b").await; - cx.shared_clipboard() - .await - .assert_eq("trailing whitespace "); - } - - #[gpui::test] - async fn test_tags(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new_html(cx).await; - - cx.set_state("hˇi!", Mode::Normal); - cx.simulate_keystrokes("v i t"); - cx.assert_state( - "«hi!ˇ»", - Mode::Visual, - ); - cx.simulate_keystrokes("a t"); - cx.assert_state( - "«hi!ˇ»", - Mode::Visual, - ); - cx.simulate_keystrokes("a t"); - cx.assert_state( - "«hi!ˇ»", - Mode::Visual, - ); - - // The cursor is before the tag - cx.set_state( - " ˇ hi!", - Mode::Normal, - ); - cx.simulate_keystrokes("v i t"); - cx.assert_state( - " «hi!ˇ»", - Mode::Visual, - ); - cx.simulate_keystrokes("a t"); - cx.assert_state( - " «hi!ˇ»", - Mode::Visual, - ); - - // The cursor is in the open tag - cx.set_state( - "hi!hello!", - Mode::Normal, - ); - cx.simulate_keystrokes("v a t"); - cx.assert_state( - "«hi!ˇ»hello!", - Mode::Visual, - ); - cx.simulate_keystrokes("i t"); - cx.assert_state( - "«hi!hello!ˇ»", - Mode::Visual, - ); - - // current selection length greater than 1 - cx.set_state( - "<«b>hi!ˇ»", - Mode::Visual, - ); - cx.simulate_keystrokes("i t"); - cx.assert_state( - "«hi!ˇ»", - Mode::Visual, - ); - cx.simulate_keystrokes("a t"); - cx.assert_state( - "«hi!ˇ»", - Mode::Visual, - ); - - cx.set_state( - "<«b>hi!", - Mode::Visual, - ); - cx.simulate_keystrokes("a t"); - cx.assert_state( - "«hi!ˇ»", - Mode::Visual, - ); - } - #[gpui::test] - async fn test_around_containing_word_indent(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(" ˇconst f = (x: unknown) => {") - .await; - cx.simulate_shared_keystrokes("v a w").await; - cx.shared_state() - .await - .assert_eq(" «const ˇ»f = (x: unknown) => {"); - - cx.set_shared_state(" ˇconst f = (x: unknown) => {") - .await; - cx.simulate_shared_keystrokes("y a w").await; - cx.shared_clipboard().await.assert_eq("const "); - - cx.set_shared_state(" ˇconst f = (x: unknown) => {") - .await; - cx.simulate_shared_keystrokes("d a w").await; - cx.shared_state() - .await - .assert_eq(" ˇf = (x: unknown) => {"); - cx.shared_clipboard().await.assert_eq("const "); - - cx.set_shared_state(" ˇconst f = (x: unknown) => {") - .await; - cx.simulate_shared_keystrokes("c a w").await; - cx.shared_state() - .await - .assert_eq(" ˇf = (x: unknown) => {"); - cx.shared_clipboard().await.assert_eq("const "); - } -} diff --git a/crates/vim/src/replace.rs b/crates/vim/src/replace.rs deleted file mode 100644 index 63d452f84b..0000000000 --- a/crates/vim/src/replace.rs +++ /dev/null @@ -1,559 +0,0 @@ -use crate::{ - Operator, Vim, - motion::{self, Motion}, - object::Object, - state::Mode, -}; -use editor::{ - Anchor, Bias, Editor, EditorSnapshot, SelectionEffects, ToOffset, ToPoint, - display_map::ToDisplayPoint, -}; -use gpui::{ClipboardEntry, Context, Window, actions}; -use language::{Point, SelectionGoal}; -use std::ops::Range; -use std::sync::Arc; - -actions!( - vim, - [ - /// Toggles replace mode. - ToggleReplace, - /// Undoes the last replacement. - UndoReplace - ] -); - -pub fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, |vim, _: &ToggleReplace, window, cx| { - vim.replacements = vec![]; - vim.start_recording(cx); - vim.switch_mode(Mode::Replace, false, window, cx); - }); - - Vim::action(editor, cx, |vim, _: &UndoReplace, window, cx| { - if vim.mode != Mode::Replace { - return; - } - let count = Vim::take_count(cx); - Vim::take_forced_motion(cx); - vim.undo_replace(count, window, cx) - }); -} - -struct VimExchange; - -impl Vim { - pub(crate) fn multi_replace( - &mut self, - text: Arc, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |vim, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - let map = editor.snapshot(window, cx); - let display_selections = editor.selections.all::(&map.display_snapshot); - - // Handles all string that require manipulation, including inserts and replaces - let edits = display_selections - .into_iter() - .map(|selection| { - let is_new_line = text.as_ref() == "\n"; - let mut range = selection.range(); - // "\n" need to be handled separately, because when a "\n" is typing, - // we don't do a replace, we need insert a "\n" - if !is_new_line { - range.end.column += 1; - range.end = map.buffer_snapshot().clip_point(range.end, Bias::Right); - } - let replace_range = map.buffer_snapshot().anchor_before(range.start) - ..map.buffer_snapshot().anchor_after(range.end); - let current_text = map - .buffer_snapshot() - .text_for_range(replace_range.clone()) - .collect(); - vim.replacements.push((replace_range.clone(), current_text)); - (replace_range, text.clone()) - }) - .collect::>(); - - editor.edit_with_block_indent(edits.clone(), Vec::new(), cx); - - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_anchor_ranges(edits.iter().map(|(range, _)| range.end..range.end)); - }); - editor.set_clip_at_line_ends(true, cx); - }); - }); - } - - fn undo_replace( - &mut self, - maybe_times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |vim, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - let map = editor.snapshot(window, cx); - let selections = editor.selections.all::(&map.display_snapshot); - let mut new_selections = vec![]; - let edits: Vec<(Range, String)> = selections - .into_iter() - .filter_map(|selection| { - let end = selection.head(); - let start = motion::wrapping_left( - &map, - end.to_display_point(&map), - maybe_times.unwrap_or(1), - ) - .to_point(&map); - new_selections.push( - map.buffer_snapshot().anchor_before(start) - ..map.buffer_snapshot().anchor_before(start), - ); - - let mut undo = None; - let edit_range = start..end; - for (i, (range, inverse)) in vim.replacements.iter().rev().enumerate() { - if range.start.to_point(&map.buffer_snapshot()) <= edit_range.start - && range.end.to_point(&map.buffer_snapshot()) >= edit_range.end - { - undo = Some(inverse.clone()); - vim.replacements.remove(vim.replacements.len() - i - 1); - break; - } - } - Some((edit_range, undo?)) - }) - .collect::>(); - - editor.edit(edits, cx); - - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges(new_selections); - }); - editor.set_clip_at_line_ends(true, cx); - }); - }); - } - - pub fn exchange_object( - &mut self, - object: Object, - around: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |vim, editor, cx| { - editor.set_clip_at_line_ends(false, cx); - let mut selection = editor - .selections - .newest_display(&editor.display_snapshot(cx)); - let snapshot = editor.snapshot(window, cx); - object.expand_selection(&snapshot, &mut selection, around, None); - let start = snapshot - .buffer_snapshot() - .anchor_before(selection.start.to_point(&snapshot)); - let end = snapshot - .buffer_snapshot() - .anchor_before(selection.end.to_point(&snapshot)); - let new_range = start..end; - vim.exchange_impl(new_range, editor, &snapshot, window, cx); - editor.set_clip_at_line_ends(true, cx); - }); - } - - pub fn exchange_visual(&mut self, window: &mut Window, cx: &mut Context) { - self.stop_recording(cx); - self.update_editor(cx, |vim, editor, cx| { - let selection = editor.selections.newest_anchor(); - let new_range = selection.start..selection.end; - let snapshot = editor.snapshot(window, cx); - vim.exchange_impl(new_range, editor, &snapshot, window, cx); - }); - self.switch_mode(Mode::Normal, false, window, cx); - } - - pub fn clear_exchange(&mut self, window: &mut Window, cx: &mut Context) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.clear_background_highlights::(cx); - }); - self.clear_operator(window, cx); - } - - pub fn exchange_motion( - &mut self, - motion: Motion, - times: Option, - forced_motion: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |vim, editor, cx| { - editor.set_clip_at_line_ends(false, cx); - let text_layout_details = editor.text_layout_details(window); - let mut selection = editor - .selections - .newest_display(&editor.display_snapshot(cx)); - let snapshot = editor.snapshot(window, cx); - motion.expand_selection( - &snapshot, - &mut selection, - times, - &text_layout_details, - forced_motion, - ); - let start = snapshot - .buffer_snapshot() - .anchor_before(selection.start.to_point(&snapshot)); - let end = snapshot - .buffer_snapshot() - .anchor_before(selection.end.to_point(&snapshot)); - let new_range = start..end; - vim.exchange_impl(new_range, editor, &snapshot, window, cx); - editor.set_clip_at_line_ends(true, cx); - }); - } - - pub fn exchange_impl( - &self, - new_range: Range, - editor: &mut Editor, - snapshot: &EditorSnapshot, - window: &mut Window, - cx: &mut Context, - ) { - if let Some((_, ranges)) = editor.clear_background_highlights::(cx) { - let previous_range = ranges[0].clone(); - - let new_range_start = new_range.start.to_offset(&snapshot.buffer_snapshot()); - let new_range_end = new_range.end.to_offset(&snapshot.buffer_snapshot()); - let previous_range_end = previous_range.end.to_offset(&snapshot.buffer_snapshot()); - let previous_range_start = previous_range.start.to_offset(&snapshot.buffer_snapshot()); - - let text_for = |range: Range| { - snapshot - .buffer_snapshot() - .text_for_range(range) - .collect::() - }; - - let mut final_cursor_position = None; - - if previous_range_end < new_range_start || new_range_end < previous_range_start { - let previous_text = text_for(previous_range.clone()); - let new_text = text_for(new_range.clone()); - final_cursor_position = Some(new_range.start.to_display_point(snapshot)); - - editor.edit([(previous_range, new_text), (new_range, previous_text)], cx); - } else if new_range_start <= previous_range_start && new_range_end >= previous_range_end - { - final_cursor_position = Some(new_range.start.to_display_point(snapshot)); - editor.edit([(new_range, text_for(previous_range))], cx); - } else if previous_range_start <= new_range_start && previous_range_end >= new_range_end - { - final_cursor_position = Some(previous_range.start.to_display_point(snapshot)); - editor.edit([(previous_range, text_for(new_range))], cx); - } - - if let Some(position) = final_cursor_position { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|_map, selection| { - selection.collapse_to(position, SelectionGoal::None); - }); - }) - } - } else { - let ranges = [new_range]; - editor.highlight_background::( - &ranges, - |_, theme| theme.colors().editor_document_highlight_read_background, - cx, - ); - } - } - - /// Pastes the clipboard contents, replacing the same number of characters - /// as the clipboard's contents. - pub fn paste_replace(&mut self, window: &mut Window, cx: &mut Context) { - let clipboard_text = - cx.read_from_clipboard() - .and_then(|item| match item.entries().first() { - Some(ClipboardEntry::String(text)) => Some(text.text().to_string()), - _ => None, - }); - - if let Some(text) = clipboard_text { - self.push_operator(Operator::Replace, window, cx); - self.normal_replace(Arc::from(text), window, cx); - } - } -} - -#[cfg(test)] -mod test { - use gpui::ClipboardItem; - use indoc::indoc; - - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - - #[gpui::test] - async fn test_enter_and_exit_replace_mode(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.simulate_keystrokes("shift-r"); - assert_eq!(cx.mode(), Mode::Replace); - cx.simulate_keystrokes("escape"); - assert_eq!(cx.mode(), Mode::Normal); - } - - #[gpui::test] - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - async fn test_replace_mode(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - - // test normal replace - cx.set_shared_state(indoc! {" - ˇThe quick brown - fox jumps over - the lazy dog."}) - .await; - cx.simulate_shared_keystrokes("shift-r O n e").await; - cx.shared_state().await.assert_eq(indoc! {" - Oneˇ quick brown - fox jumps over - the lazy dog."}); - - // test replace with line ending - cx.set_shared_state(indoc! {" - The quick browˇn - fox jumps over - the lazy dog."}) - .await; - cx.simulate_shared_keystrokes("shift-r O n e").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick browOneˇ - fox jumps over - the lazy dog."}); - - // test replace with blank line - cx.set_shared_state(indoc! {" - The quick brown - ˇ - fox jumps over - the lazy dog."}) - .await; - cx.simulate_shared_keystrokes("shift-r O n e").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - Oneˇ - fox jumps over - the lazy dog."}); - - // test replace with newline - cx.set_shared_state(indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}) - .await; - cx.simulate_shared_keystrokes("shift-r enter O n e").await; - cx.shared_state().await.assert_eq(indoc! {" - The qu - Oneˇ brown - fox jumps over - the lazy dog."}); - - // test replace with multi cursor and newline - cx.set_state( - indoc! {" - ˇThe quick brown - fox jumps over - the lazy ˇdog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("shift-r O n e"); - cx.assert_state( - indoc! {" - Oneˇ quick brown - fox jumps over - the lazy Oneˇ."}, - Mode::Replace, - ); - cx.simulate_keystrokes("enter T w o"); - cx.assert_state( - indoc! {" - One - Twoˇck brown - fox jumps over - the lazy One - Twoˇ"}, - Mode::Replace, - ); - } - - #[gpui::test] - async fn test_replace_mode_with_counts(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("3 shift-r - escape").await; - cx.shared_state().await.assert_eq("--ˇ-lo\n"); - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("3 shift-r a b c escape") - .await; - cx.shared_state().await.assert_eq("abcabcabˇc\n"); - } - - #[gpui::test] - async fn test_replace_mode_repeat(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello world\n").await; - cx.simulate_shared_keystrokes("shift-r - - - escape 4 l .") - .await; - cx.shared_state().await.assert_eq("---lo --ˇ-ld\n"); - } - - #[gpui::test] - async fn test_replace_mode_undo(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - - const UNDO_REPLACE_EXAMPLES: &[&str] = &[ - // replace undo with single line - "ˇThe quick brown fox jumps over the lazy dog.", - // replace undo with ending line - indoc! {" - The quick browˇn - fox jumps over - the lazy dog." - }, - // replace undo with empty line - indoc! {" - The quick brown - ˇ - fox jumps over - the lazy dog." - }, - ]; - - for example in UNDO_REPLACE_EXAMPLES { - // normal undo - cx.simulate("shift-r O n e backspace backspace backspace", example) - .await - .assert_matches(); - // undo with new line - cx.simulate("shift-r O enter e backspace backspace backspace", example) - .await - .assert_matches(); - cx.simulate( - "shift-r O enter n enter e backspace backspace backspace backspace backspace", - example, - ) - .await - .assert_matches(); - } - } - - #[gpui::test] - async fn test_replace_multicursor(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.set_state("ˇabcˇabcabc", Mode::Normal); - cx.simulate_keystrokes("shift-r 1 2 3 4"); - cx.assert_state("1234ˇ234ˇbc", Mode::Replace); - assert_eq!(cx.mode(), Mode::Replace); - cx.simulate_keystrokes("backspace backspace backspace backspace backspace"); - cx.assert_state("ˇabˇcabcabc", Mode::Replace); - } - - #[gpui::test] - async fn test_replace_undo(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇaaaa", Mode::Normal); - cx.simulate_keystrokes("0 shift-r b b b escape u"); - cx.assert_state("ˇaaaa", Mode::Normal); - } - - #[gpui::test] - async fn test_exchange_separate_range(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇhello world", Mode::Normal); - cx.simulate_keystrokes("c x i w w c x i w"); - cx.assert_state("world ˇhello", Mode::Normal); - } - - #[gpui::test] - async fn test_exchange_complete_overlap(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇhello world", Mode::Normal); - cx.simulate_keystrokes("c x x w c x i w"); - cx.assert_state("ˇworld", Mode::Normal); - - // the focus should still be at the start of the word if we reverse the - // order of selections (smaller -> larger) - cx.set_state("ˇhello world", Mode::Normal); - cx.simulate_keystrokes("c x i w c x x"); - cx.assert_state("ˇhello", Mode::Normal); - } - - #[gpui::test] - async fn test_exchange_partial_overlap(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇhello world", Mode::Normal); - cx.simulate_keystrokes("c x t r w c x i w"); - cx.assert_state("hello ˇworld", Mode::Normal); - } - - #[gpui::test] - async fn test_clear_exchange_clears_operator(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇirrelevant", Mode::Normal); - cx.simulate_keystrokes("c x c"); - - assert_eq!(cx.active_operator(), None); - } - - #[gpui::test] - async fn test_clear_exchange(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇhello world", Mode::Normal); - cx.simulate_keystrokes("c x i w c x c"); - - cx.update_editor(|editor, window, cx| { - let highlights = editor.all_text_background_highlights(window, cx); - assert_eq!(0, highlights.len()); - }); - } - - #[gpui::test] - async fn test_paste_replace(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state(indoc! {"ˇ123"}, Mode::Replace); - cx.write_to_clipboard(ClipboardItem::new_string("456".to_string())); - cx.dispatch_action(editor::actions::Paste); - cx.assert_state(indoc! {"45ˇ6"}, Mode::Replace); - - // If the clipboard's contents length is greater than the remaining text - // length, nothing sould be replace and cursor should remain in the same - // position. - cx.set_state(indoc! {"ˇ123"}, Mode::Replace); - cx.write_to_clipboard(ClipboardItem::new_string("4567".to_string())); - cx.dispatch_action(editor::actions::Paste); - cx.assert_state(indoc! {"ˇ123"}, Mode::Replace); - } -} diff --git a/crates/vim/src/rewrap.rs b/crates/vim/src/rewrap.rs deleted file mode 100644 index 85e1967af0..0000000000 --- a/crates/vim/src/rewrap.rs +++ /dev/null @@ -1,148 +0,0 @@ -use crate::{Vim, motion::Motion, object::Object, state::Mode}; -use collections::HashMap; -use editor::{Bias, Editor, RewrapOptions, SelectionEffects, display_map::ToDisplayPoint}; -use gpui::{Context, Window, actions}; -use language::SelectionGoal; - -actions!( - vim, - [ - /// Rewraps the selected text to fit within the line width. - Rewrap - ] -); - -pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, |vim, _: &Rewrap, window, cx| { - vim.record_current_action(cx); - Vim::take_count(cx); - Vim::take_forced_motion(cx); - vim.store_visual_marks(window, cx); - vim.update_editor(cx, |vim, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let mut positions = vim.save_selection_starts(editor, cx); - editor.rewrap_impl( - RewrapOptions { - override_language_settings: true, - ..Default::default() - }, - cx, - ); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - if let Some(anchor) = positions.remove(&selection.id) { - let mut point = anchor.to_display_point(map); - *point.column_mut() = 0; - selection.collapse_to(point, SelectionGoal::None); - } - }); - }); - }); - }); - if vim.mode.is_visual() { - vim.switch_mode(Mode::Normal, true, window, cx) - } - }); -} - -impl Vim { - pub(crate) fn rewrap_motion( - &mut self, - motion: Motion, - times: Option, - forced_motion: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - let mut selection_starts: HashMap<_, _> = Default::default(); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); - selection_starts.insert(selection.id, anchor); - motion.expand_selection( - map, - selection, - times, - &text_layout_details, - forced_motion, - ); - }); - }); - editor.rewrap_impl( - RewrapOptions { - override_language_settings: true, - ..Default::default() - }, - cx, - ); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = selection_starts.remove(&selection.id).unwrap(); - let mut point = anchor.to_display_point(map); - *point.column_mut() = 0; - selection.collapse_to(point, SelectionGoal::None); - }); - }); - }); - }); - } - - pub(crate) fn rewrap_object( - &mut self, - object: Object, - around: bool, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let mut original_positions: HashMap<_, _> = Default::default(); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); - original_positions.insert(selection.id, anchor); - object.expand_selection(map, selection, around, times); - }); - }); - editor.rewrap_impl( - RewrapOptions { - override_language_settings: true, - ..Default::default() - }, - cx, - ); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = original_positions.remove(&selection.id).unwrap(); - let mut point = anchor.to_display_point(map); - *point.column_mut() = 0; - selection.collapse_to(point, SelectionGoal::None); - }); - }); - }); - }); - } -} - -#[cfg(test)] -mod test { - use crate::test::NeovimBackedTestContext; - - #[gpui::test] - async fn test_indent_gv(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_neovim_option("shiftwidth=4").await; - - cx.set_shared_state("ˇhello\nworld\n").await; - cx.simulate_shared_keystrokes("v j > g v").await; - cx.shared_state() - .await - .assert_eq("« hello\n ˇ» world\n"); - } -} diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs deleted file mode 100644 index e96fd3a329..0000000000 --- a/crates/vim/src/state.rs +++ /dev/null @@ -1,1835 +0,0 @@ -use crate::command::command_interceptor; -use crate::motion::MotionKind; -use crate::normal::repeat::Replayer; -use crate::surrounds::SurroundsType; -use crate::{ToggleMarksView, ToggleRegistersView, UseSystemClipboard, Vim, VimAddon, VimSettings}; -use crate::{motion::Motion, object::Object}; -use anyhow::Result; -use collections::HashMap; -use command_palette_hooks::{CommandPaletteFilter, GlobalCommandPaletteInterceptor}; -use db::{ - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, -}; -use editor::display_map::{is_invisible, replacement}; -use editor::{Anchor, ClipboardSelection, Editor, MultiBuffer, ToPoint as EditorToPoint}; -use gpui::{ - Action, App, AppContext, BorrowAppContext, ClipboardEntry, ClipboardItem, DismissEvent, Entity, - EntityId, Global, HighlightStyle, StyledText, Subscription, Task, TextStyle, WeakEntity, -}; -use language::{Buffer, BufferEvent, BufferId, Chunk, Point}; -use multi_buffer::MultiBufferRow; -use picker::{Picker, PickerDelegate}; -use project::{Project, ProjectItem, ProjectPath}; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore}; -use std::borrow::BorrowMut; -use std::collections::HashSet; -use std::path::Path; -use std::{fmt::Display, ops::Range, sync::Arc}; -use text::{Bias, ToPoint}; -use theme::ThemeSettings; -use ui::{ - ActiveTheme, Context, Div, FluentBuilder, KeyBinding, ParentElement, SharedString, Styled, - StyledTypography, Window, h_flex, rems, -}; -use util::ResultExt; -use util::rel_path::RelPath; -use workspace::searchable::Direction; -use workspace::{Workspace, WorkspaceDb, WorkspaceId}; - -#[derive(Clone, Copy, Default, Debug, PartialEq, Serialize, Deserialize)] -pub enum Mode { - #[default] - Normal, - Insert, - Replace, - Visual, - VisualLine, - VisualBlock, - HelixNormal, - HelixSelect, -} - -impl Display for Mode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Mode::Normal => write!(f, "NORMAL"), - Mode::Insert => write!(f, "INSERT"), - Mode::Replace => write!(f, "REPLACE"), - Mode::Visual => write!(f, "VISUAL"), - Mode::VisualLine => write!(f, "VISUAL LINE"), - Mode::VisualBlock => write!(f, "VISUAL BLOCK"), - Mode::HelixNormal => write!(f, "NORMAL"), - Mode::HelixSelect => write!(f, "SELECT"), - } - } -} - -impl Mode { - pub fn is_visual(&self) -> bool { - match self { - Self::Visual | Self::VisualLine | Self::VisualBlock | Self::HelixSelect => true, - Self::Normal | Self::Insert | Self::Replace | Self::HelixNormal => false, - } - } -} - -#[derive(Clone, Debug, PartialEq)] -pub enum Operator { - Change, - Delete, - Yank, - Replace, - Object { - around: bool, - }, - FindForward { - before: bool, - multiline: bool, - }, - FindBackward { - after: bool, - multiline: bool, - }, - Sneak { - first_char: Option, - }, - SneakBackward { - first_char: Option, - }, - AddSurrounds { - // Typically no need to configure this as `SendKeystrokes` can be used - see #23088. - target: Option, - }, - ChangeSurrounds { - target: Option, - /// Represents whether the opening bracket was used for the target - /// object. - opening: bool, - }, - DeleteSurrounds, - Mark, - Jump { - line: bool, - }, - Indent, - Outdent, - AutoIndent, - Rewrap, - ShellCommand, - Lowercase, - Uppercase, - OppositeCase, - Rot13, - Rot47, - Digraph { - first_char: Option, - }, - Literal { - prefix: Option, - }, - Register, - RecordRegister, - ReplayRegister, - ToggleComments, - ReplaceWithRegister, - Exchange, - HelixMatch, - HelixNext { - around: bool, - }, - HelixPrevious { - around: bool, - }, -} - -#[derive(Default, Clone, Debug)] -pub enum RecordedSelection { - #[default] - None, - Visual { - rows: u32, - cols: u32, - }, - SingleLine { - cols: u32, - }, - VisualBlock { - rows: u32, - cols: u32, - }, - VisualLine { - rows: u32, - }, -} - -#[derive(Default, Clone, Debug)] -pub struct Register { - pub(crate) text: SharedString, - pub(crate) clipboard_selections: Option>, -} - -impl From for ClipboardItem { - fn from(register: Register) -> Self { - if let Some(clipboard_selections) = register.clipboard_selections { - ClipboardItem::new_string_with_json_metadata(register.text.into(), clipboard_selections) - } else { - ClipboardItem::new_string(register.text.into()) - } - } -} - -impl From for Register { - fn from(item: ClipboardItem) -> Self { - // For now, we don't store metadata for multiple entries. - match item.entries().first() { - Some(ClipboardEntry::String(value)) if item.entries().len() == 1 => Register { - text: value.text().to_owned().into(), - clipboard_selections: value.metadata_json::>(), - }, - // For now, registers can't store images. This could change in the future. - _ => Register::default(), - } - } -} - -impl From for Register { - fn from(text: String) -> Self { - Register { - text: text.into(), - clipboard_selections: None, - } - } -} - -#[derive(Default)] -pub struct VimGlobals { - pub last_find: Option, - - pub dot_recording: bool, - pub dot_replaying: bool, - - /// pre_count is the number before an operator is specified (3 in 3d2d) - pub pre_count: Option, - /// post_count is the number after an operator is specified (2 in 3d2d) - pub post_count: Option, - pub forced_motion: bool, - pub stop_recording_after_next_action: bool, - pub ignore_current_insertion: bool, - pub recording_count: Option, - pub recorded_count: Option, - pub recording_actions: Vec, - pub recorded_actions: Vec, - pub recorded_selection: RecordedSelection, - - pub recording_register: Option, - pub last_recorded_register: Option, - pub last_replayed_register: Option, - pub replayer: Option, - - pub last_yank: Option, - pub registers: HashMap, - pub recordings: HashMap>, - - pub focused_vim: Option>, - - pub marks: HashMap>, -} - -pub struct MarksState { - workspace: WeakEntity, - - multibuffer_marks: HashMap>>, - buffer_marks: HashMap>>, - watched_buffers: HashMap, - - serialized_marks: HashMap, HashMap>>, - global_marks: HashMap, - - _subscription: Subscription, -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum MarkLocation { - Buffer(EntityId), - Path(Arc), -} - -pub enum Mark { - Local(Vec), - Buffer(EntityId, Vec), - Path(Arc, Vec), -} - -impl MarksState { - pub fn new(workspace: &Workspace, cx: &mut App) -> Entity { - cx.new(|cx| { - let buffer_store = workspace.project().read(cx).buffer_store().clone(); - let subscription = cx.subscribe(&buffer_store, move |this: &mut Self, _, event, cx| { - if let project::buffer_store::BufferStoreEvent::BufferAdded(buffer) = event { - this.on_buffer_loaded(buffer, cx); - } - }); - - let mut this = Self { - workspace: workspace.weak_handle(), - multibuffer_marks: HashMap::default(), - buffer_marks: HashMap::default(), - watched_buffers: HashMap::default(), - serialized_marks: HashMap::default(), - global_marks: HashMap::default(), - _subscription: subscription, - }; - - this.load(cx); - this - }) - } - - fn workspace_id(&self, cx: &App) -> Option { - self.workspace - .read_with(cx, |workspace, _| workspace.database_id()) - .ok() - .flatten() - } - - fn project(&self, cx: &App) -> Option> { - self.workspace - .read_with(cx, |workspace, _| workspace.project().clone()) - .ok() - } - - fn load(&mut self, cx: &mut Context) { - cx.spawn(async move |this, cx| { - let Some(workspace_id) = this.update(cx, |this, cx| this.workspace_id(cx)).ok()? else { - return None; - }; - let (marks, paths) = cx - .background_spawn(async move { - let marks = DB.get_marks(workspace_id)?; - let paths = DB.get_global_marks_paths(workspace_id)?; - anyhow::Ok((marks, paths)) - }) - .await - .log_err()?; - this.update(cx, |this, cx| this.loaded(marks, paths, cx)) - .ok() - }) - .detach(); - } - - fn loaded( - &mut self, - marks: Vec, - global_mark_paths: Vec<(String, Arc)>, - cx: &mut Context, - ) { - let Some(project) = self.project(cx) else { - return; - }; - - for mark in marks { - self.serialized_marks - .entry(mark.path) - .or_default() - .insert(mark.name, mark.points); - } - - for (name, path) in global_mark_paths { - self.global_marks - .insert(name, MarkLocation::Path(path.clone())); - - let project_path = project - .read(cx) - .worktrees(cx) - .filter_map(|worktree| { - let relative = path.strip_prefix(worktree.read(cx).abs_path()).ok()?; - let path = RelPath::new(relative, worktree.read(cx).path_style()).log_err()?; - Some(ProjectPath { - worktree_id: worktree.read(cx).id(), - path: path.into_arc(), - }) - }) - .next(); - if let Some(buffer) = project_path - .and_then(|project_path| project.read(cx).get_open_buffer(&project_path, cx)) - { - self.on_buffer_loaded(&buffer, cx) - } - } - } - - pub fn on_buffer_loaded(&mut self, buffer_handle: &Entity, cx: &mut Context) { - let Some(project) = self.project(cx) else { - return; - }; - let Some(project_path) = buffer_handle.read(cx).project_path(cx) else { - return; - }; - let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) else { - return; - }; - let abs_path: Arc = abs_path.into(); - - let Some(serialized_marks) = self.serialized_marks.get(&abs_path) else { - return; - }; - - let mut loaded_marks = HashMap::default(); - let buffer = buffer_handle.read(cx); - for (name, points) in serialized_marks.iter() { - loaded_marks.insert( - name.clone(), - points - .iter() - .map(|point| buffer.anchor_before(buffer.clip_point(*point, Bias::Left))) - .collect(), - ); - } - self.buffer_marks.insert(buffer.remote_id(), loaded_marks); - self.watch_buffer(MarkLocation::Path(abs_path), buffer_handle, cx) - } - - fn serialize_buffer_marks( - &mut self, - path: Arc, - buffer: &Entity, - cx: &mut Context, - ) { - let new_points: HashMap> = - if let Some(anchors) = self.buffer_marks.get(&buffer.read(cx).remote_id()) { - anchors - .iter() - .map(|(name, anchors)| { - ( - name.clone(), - buffer - .read(cx) - .summaries_for_anchors::(anchors) - .collect(), - ) - }) - .collect() - } else { - HashMap::default() - }; - let old_points = self.serialized_marks.get(&path); - if old_points == Some(&new_points) { - return; - } - let mut to_write = HashMap::default(); - - for (key, value) in &new_points { - if self.is_global_mark(key) - && self.global_marks.get(key) != Some(&MarkLocation::Path(path.clone())) - { - if let Some(workspace_id) = self.workspace_id(cx) { - let path = path.clone(); - let key = key.clone(); - cx.background_spawn(async move { - DB.set_global_mark_path(workspace_id, key, path).await - }) - .detach_and_log_err(cx); - } - - self.global_marks - .insert(key.clone(), MarkLocation::Path(path.clone())); - } - if old_points.and_then(|o| o.get(key)) != Some(value) { - to_write.insert(key.clone(), value.clone()); - } - } - - self.serialized_marks.insert(path.clone(), new_points); - - if let Some(workspace_id) = self.workspace_id(cx) { - cx.background_spawn(async move { - DB.set_marks(workspace_id, path.clone(), to_write).await?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - } - - fn is_global_mark(&self, key: &str) -> bool { - key.chars() - .next() - .is_some_and(|c| c.is_uppercase() || c.is_digit(10)) - } - - fn rename_buffer( - &mut self, - old_path: MarkLocation, - new_path: Arc, - buffer: &Entity, - cx: &mut Context, - ) { - if let MarkLocation::Buffer(entity_id) = old_path - && let Some(old_marks) = self.multibuffer_marks.remove(&entity_id) - { - let buffer_marks = old_marks - .into_iter() - .map(|(k, v)| (k, v.into_iter().map(|anchor| anchor.text_anchor).collect())) - .collect(); - self.buffer_marks - .insert(buffer.read(cx).remote_id(), buffer_marks); - } - self.watch_buffer(MarkLocation::Path(new_path.clone()), buffer, cx); - self.serialize_buffer_marks(new_path, buffer, cx); - } - - fn path_for_buffer(&self, buffer: &Entity, cx: &App) -> Option> { - let project_path = buffer.read(cx).project_path(cx)?; - let project = self.project(cx)?; - let abs_path = project.read(cx).absolute_path(&project_path, cx)?; - Some(abs_path.into()) - } - - fn points_at( - &self, - location: &MarkLocation, - multi_buffer: &Entity, - cx: &App, - ) -> bool { - match location { - MarkLocation::Buffer(entity_id) => entity_id == &multi_buffer.entity_id(), - MarkLocation::Path(path) => { - let Some(singleton) = multi_buffer.read(cx).as_singleton() else { - return false; - }; - self.path_for_buffer(&singleton, cx).as_ref() == Some(path) - } - } - } - - pub fn watch_buffer( - &mut self, - mark_location: MarkLocation, - buffer_handle: &Entity, - cx: &mut Context, - ) { - let on_change = cx.subscribe(buffer_handle, move |this, buffer, event, cx| match event { - BufferEvent::Edited => { - if let Some(path) = this.path_for_buffer(&buffer, cx) { - this.serialize_buffer_marks(path, &buffer, cx); - } - } - BufferEvent::FileHandleChanged => { - let buffer_id = buffer.read(cx).remote_id(); - if let Some(old_path) = this - .watched_buffers - .get(&buffer_id.clone()) - .map(|(path, _, _)| path.clone()) - && let Some(new_path) = this.path_for_buffer(&buffer, cx) - { - this.rename_buffer(old_path, new_path, &buffer, cx) - } - } - _ => {} - }); - - let on_release = cx.observe_release(buffer_handle, |this, buffer, _| { - this.watched_buffers.remove(&buffer.remote_id()); - this.buffer_marks.remove(&buffer.remote_id()); - }); - - self.watched_buffers.insert( - buffer_handle.read(cx).remote_id(), - (mark_location, on_change, on_release), - ); - } - - pub fn set_mark( - &mut self, - name: String, - multibuffer: &Entity, - anchors: Vec, - cx: &mut Context, - ) { - let buffer = multibuffer.read(cx).as_singleton(); - let abs_path = buffer.as_ref().and_then(|b| self.path_for_buffer(b, cx)); - - let Some(abs_path) = abs_path else { - self.multibuffer_marks - .entry(multibuffer.entity_id()) - .or_default() - .insert(name.clone(), anchors); - if self.is_global_mark(&name) { - self.global_marks - .insert(name, MarkLocation::Buffer(multibuffer.entity_id())); - } - if let Some(buffer) = buffer { - let buffer_id = buffer.read(cx).remote_id(); - if !self.watched_buffers.contains_key(&buffer_id) { - self.watch_buffer(MarkLocation::Buffer(multibuffer.entity_id()), &buffer, cx) - } - } - return; - }; - let Some(buffer) = buffer else { - return; - }; - - let buffer_id = buffer.read(cx).remote_id(); - self.buffer_marks.entry(buffer_id).or_default().insert( - name, - anchors - .into_iter() - .map(|anchor| anchor.text_anchor) - .collect(), - ); - if !self.watched_buffers.contains_key(&buffer_id) { - self.watch_buffer(MarkLocation::Path(abs_path.clone()), &buffer, cx) - } - self.serialize_buffer_marks(abs_path, &buffer, cx) - } - - pub fn get_mark( - &self, - name: &str, - multi_buffer: &Entity, - cx: &App, - ) -> Option { - let target = self.global_marks.get(name); - - if !self.is_global_mark(name) || target.is_some_and(|t| self.points_at(t, multi_buffer, cx)) - { - if let Some(anchors) = self.multibuffer_marks.get(&multi_buffer.entity_id()) { - return Some(Mark::Local(anchors.get(name)?.clone())); - } - - let singleton = multi_buffer.read(cx).as_singleton()?; - let excerpt_id = *multi_buffer.read(cx).excerpt_ids().first()?; - let buffer_id = singleton.read(cx).remote_id(); - if let Some(anchors) = self.buffer_marks.get(&buffer_id) { - let text_anchors = anchors.get(name)?; - let anchors = text_anchors - .iter() - .map(|anchor| Anchor::in_buffer(excerpt_id, *anchor)) - .collect(); - return Some(Mark::Local(anchors)); - } - } - - match target? { - MarkLocation::Buffer(entity_id) => { - let anchors = self.multibuffer_marks.get(entity_id)?; - Some(Mark::Buffer(*entity_id, anchors.get(name)?.clone())) - } - MarkLocation::Path(path) => { - let points = self.serialized_marks.get(path)?; - Some(Mark::Path(path.clone(), points.get(name)?.clone())) - } - } - } - pub fn delete_mark( - &mut self, - mark_name: String, - multi_buffer: &Entity, - cx: &mut Context, - ) { - let path = if let Some(target) = self.global_marks.get(&mark_name.clone()) { - let name = mark_name.clone(); - if let Some(workspace_id) = self.workspace_id(cx) { - cx.background_spawn(async move { - DB.delete_global_marks_path(workspace_id, name).await - }) - .detach_and_log_err(cx); - } - self.buffer_marks.iter_mut().for_each(|(_, m)| { - m.remove(&mark_name.clone()); - }); - - match target { - MarkLocation::Buffer(entity_id) => { - self.multibuffer_marks - .get_mut(entity_id) - .map(|m| m.remove(&mark_name.clone())); - return; - } - MarkLocation::Path(path) => path.clone(), - } - } else { - self.multibuffer_marks - .get_mut(&multi_buffer.entity_id()) - .map(|m| m.remove(&mark_name.clone())); - - if let Some(singleton) = multi_buffer.read(cx).as_singleton() { - let buffer_id = singleton.read(cx).remote_id(); - self.buffer_marks - .get_mut(&buffer_id) - .map(|m| m.remove(&mark_name.clone())); - let Some(path) = self.path_for_buffer(&singleton, cx) else { - return; - }; - path - } else { - return; - } - }; - self.global_marks.remove(&mark_name); - self.serialized_marks - .get_mut(&path) - .map(|m| m.remove(&mark_name.clone())); - if let Some(workspace_id) = self.workspace_id(cx) { - cx.background_spawn(async move { DB.delete_mark(workspace_id, path, mark_name).await }) - .detach_and_log_err(cx); - } - } -} - -impl Global for VimGlobals {} - -impl VimGlobals { - pub(crate) fn register(cx: &mut App) { - cx.set_global(VimGlobals::default()); - - cx.observe_keystrokes(|event, _, cx| { - let Some(action) = event.action.as_ref().map(|action| action.boxed_clone()) else { - return; - }; - Vim::globals(cx).observe_action(action.boxed_clone()) - }) - .detach(); - - cx.observe_new(|workspace: &mut Workspace, window, _| { - RegistersView::register(workspace, window); - }) - .detach(); - - cx.observe_new(move |workspace: &mut Workspace, window, _| { - MarksView::register(workspace, window); - }) - .detach(); - - let mut was_enabled = None; - - cx.observe_global::(move |cx| { - let is_enabled = Vim::enabled(cx); - if was_enabled == Some(is_enabled) { - return; - } - was_enabled = Some(is_enabled); - if is_enabled { - KeyBinding::set_vim_mode(cx, true); - CommandPaletteFilter::update_global(cx, |filter, _| { - filter.show_namespace(Vim::NAMESPACE); - }); - GlobalCommandPaletteInterceptor::set(cx, command_interceptor); - for window in cx.windows() { - if let Some(workspace) = window.downcast::() { - workspace - .update(cx, |workspace, _, cx| { - Vim::update_globals(cx, |globals, cx| { - globals.register_workspace(workspace, cx) - }); - }) - .ok(); - } - } - } else { - KeyBinding::set_vim_mode(cx, false); - *Vim::globals(cx) = VimGlobals::default(); - GlobalCommandPaletteInterceptor::clear(cx); - CommandPaletteFilter::update_global(cx, |filter, _| { - filter.hide_namespace(Vim::NAMESPACE); - }); - } - }) - .detach(); - cx.observe_new(|workspace: &mut Workspace, _, cx| { - Vim::update_globals(cx, |globals, cx| globals.register_workspace(workspace, cx)); - }) - .detach() - } - - fn register_workspace(&mut self, workspace: &Workspace, cx: &mut Context) { - let entity_id = cx.entity_id(); - self.marks.insert(entity_id, MarksState::new(workspace, cx)); - cx.observe_release(&cx.entity(), move |_, _, cx| { - Vim::update_globals(cx, |globals, _| { - globals.marks.remove(&entity_id); - }) - }) - .detach(); - } - - pub(crate) fn write_registers( - &mut self, - content: Register, - register: Option, - is_yank: bool, - kind: MotionKind, - cx: &mut Context, - ) { - if let Some(register) = register { - let lower = register.to_lowercase().next().unwrap_or(register); - if lower != register { - let current = self.registers.entry(lower).or_default(); - current.text = (current.text.to_string() + &content.text).into(); - // not clear how to support appending to registers with multiple cursors - current.clipboard_selections.take(); - let yanked = current.clone(); - self.registers.insert('"', yanked); - } else { - match lower { - '_' | ':' | '.' | '%' | '#' | '=' | '/' => {} - '+' => { - self.registers.insert('"', content.clone()); - cx.write_to_clipboard(content.into()); - } - '*' => { - self.registers.insert('"', content.clone()); - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - cx.write_to_primary(content.into()); - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - cx.write_to_clipboard(content.into()); - } - '"' => { - self.registers.insert('"', content.clone()); - self.registers.insert('0', content); - } - _ => { - self.registers.insert('"', content.clone()); - self.registers.insert(lower, content); - } - } - } - } else { - let setting = VimSettings::get_global(cx).use_system_clipboard; - if setting == UseSystemClipboard::Always - || setting == UseSystemClipboard::OnYank && is_yank - { - self.last_yank.replace(content.text.clone()); - cx.write_to_clipboard(content.clone().into()); - } else { - if let Some(text) = cx.read_from_clipboard().and_then(|i| i.text()) { - self.last_yank.replace(text.into()); - } - } - self.registers.insert('"', content.clone()); - if is_yank { - self.registers.insert('0', content); - } else { - let contains_newline = content.text.contains('\n'); - if !contains_newline { - self.registers.insert('-', content.clone()); - } - if kind.linewise() || contains_newline { - let mut content = content; - for i in '1'..='9' { - if let Some(moved) = self.registers.insert(i, content) { - content = moved; - } else { - break; - } - } - } - } - } - } - - pub(crate) fn read_register( - &self, - register: Option, - editor: Option<&mut Editor>, - cx: &mut App, - ) -> Option { - let Some(register) = register.filter(|reg| *reg != '"') else { - let setting = VimSettings::get_global(cx).use_system_clipboard; - return match setting { - UseSystemClipboard::Always => cx.read_from_clipboard().map(|item| item.into()), - UseSystemClipboard::OnYank if self.system_clipboard_is_newer(cx) => { - cx.read_from_clipboard().map(|item| item.into()) - } - _ => self.registers.get(&'"').cloned(), - }; - }; - let lower = register.to_lowercase().next().unwrap_or(register); - match lower { - '_' | ':' | '.' | '#' | '=' => None, - '+' => cx.read_from_clipboard().map(|item| item.into()), - '*' => { - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - { - cx.read_from_primary().map(|item| item.into()) - } - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - { - cx.read_from_clipboard().map(|item| item.into()) - } - } - '%' => editor.and_then(|editor| { - let selection = editor - .selections - .newest::(&editor.display_snapshot(cx)); - if let Some((_, buffer, _)) = editor - .buffer() - .read(cx) - .excerpt_containing(selection.head(), cx) - { - buffer - .read(cx) - .file() - .map(|file| file.path().display(file.path_style(cx)).into_owned().into()) - } else { - None - } - }), - _ => self.registers.get(&lower).cloned(), - } - } - - fn system_clipboard_is_newer(&self, cx: &App) -> bool { - cx.read_from_clipboard().is_some_and(|item| { - match (item.text().as_deref(), &self.last_yank) { - (Some(new), Some(last)) => last.as_ref() != new, - (Some(_), None) => true, - (None, _) => false, - } - }) - } - - pub fn observe_action(&mut self, action: Box) { - if self.dot_recording { - self.recording_actions - .push(ReplayableAction::Action(action.boxed_clone())); - - if self.stop_recording_after_next_action { - self.dot_recording = false; - self.recorded_actions = std::mem::take(&mut self.recording_actions); - self.recorded_count = self.recording_count.take(); - self.stop_recording_after_next_action = false; - } - } - if self.replayer.is_none() - && let Some(recording_register) = self.recording_register - { - self.recordings - .entry(recording_register) - .or_default() - .push(ReplayableAction::Action(action)); - } - } - - pub fn observe_insertion(&mut self, text: &Arc, range_to_replace: Option>) { - if self.ignore_current_insertion { - self.ignore_current_insertion = false; - return; - } - if self.dot_recording { - self.recording_actions.push(ReplayableAction::Insertion { - text: text.clone(), - utf16_range_to_replace: range_to_replace.clone(), - }); - if self.stop_recording_after_next_action { - self.dot_recording = false; - self.recorded_actions = std::mem::take(&mut self.recording_actions); - self.recorded_count = self.recording_count.take(); - self.stop_recording_after_next_action = false; - } - } - if let Some(recording_register) = self.recording_register { - self.recordings.entry(recording_register).or_default().push( - ReplayableAction::Insertion { - text: text.clone(), - utf16_range_to_replace: range_to_replace, - }, - ); - } - } - - pub fn focused_vim(&self) -> Option> { - self.focused_vim.as_ref().and_then(|vim| vim.upgrade()) - } -} - -impl Vim { - pub fn globals(cx: &mut App) -> &mut VimGlobals { - cx.global_mut::() - } - - pub fn update_globals(cx: &mut C, f: impl FnOnce(&mut VimGlobals, &mut C) -> R) -> R - where - C: BorrowMut, - { - cx.update_global(f) - } -} - -#[derive(Debug)] -pub enum ReplayableAction { - Action(Box), - Insertion { - text: Arc, - utf16_range_to_replace: Option>, - }, -} - -impl Clone for ReplayableAction { - fn clone(&self) -> Self { - match self { - Self::Action(action) => Self::Action(action.boxed_clone()), - Self::Insertion { - text, - utf16_range_to_replace, - } => Self::Insertion { - text: text.clone(), - utf16_range_to_replace: utf16_range_to_replace.clone(), - }, - } - } -} - -#[derive(Clone, Default, Debug)] -pub struct SearchState { - pub direction: Direction, - pub count: usize, - - pub prior_selections: Vec>, - pub prior_operator: Option, - pub prior_mode: Mode, - pub helix_select: bool, -} - -impl Operator { - pub fn id(&self) -> &'static str { - match self { - Operator::Object { around: false } => "i", - Operator::Object { around: true } => "a", - Operator::Change => "c", - Operator::Delete => "d", - Operator::Yank => "y", - Operator::Replace => "r", - Operator::Digraph { .. } => "^K", - Operator::Literal { .. } => "^V", - Operator::FindForward { before: false, .. } => "f", - Operator::FindForward { before: true, .. } => "t", - Operator::Sneak { .. } => "s", - Operator::SneakBackward { .. } => "S", - Operator::FindBackward { after: false, .. } => "F", - Operator::FindBackward { after: true, .. } => "T", - Operator::AddSurrounds { .. } => "ys", - Operator::ChangeSurrounds { .. } => "cs", - Operator::DeleteSurrounds => "ds", - Operator::Mark => "m", - Operator::Jump { line: true } => "'", - Operator::Jump { line: false } => "`", - Operator::Indent => ">", - Operator::AutoIndent => "eq", - Operator::ShellCommand => "sh", - Operator::Rewrap => "gq", - Operator::ReplaceWithRegister => "gR", - Operator::Exchange => "cx", - Operator::Outdent => "<", - Operator::Uppercase => "gU", - Operator::Lowercase => "gu", - Operator::OppositeCase => "g~", - Operator::Rot13 => "g?", - Operator::Rot47 => "g?", - Operator::Register => "\"", - Operator::RecordRegister => "q", - Operator::ReplayRegister => "@", - Operator::ToggleComments => "gc", - Operator::HelixMatch => "helix_m", - Operator::HelixNext { .. } => "helix_next", - Operator::HelixPrevious { .. } => "helix_previous", - } - } - - pub fn status(&self) -> String { - fn make_visible(c: &str) -> &str { - match c { - "\n" => "enter", - "\t" => "tab", - " " => "space", - c => c, - } - } - match self { - Operator::Digraph { - first_char: Some(first_char), - } => format!("^K{}", make_visible(&first_char.to_string())), - Operator::Literal { - prefix: Some(prefix), - } => format!("^V{}", make_visible(prefix)), - Operator::AutoIndent => "=".to_string(), - Operator::ShellCommand => "=".to_string(), - Operator::HelixMatch => "m".to_string(), - Operator::HelixNext { .. } => "]".to_string(), - Operator::HelixPrevious { .. } => "[".to_string(), - _ => self.id().to_string(), - } - } - - pub fn is_waiting(&self, mode: Mode) -> bool { - match self { - Operator::AddSurrounds { target } => target.is_some() || mode.is_visual(), - Operator::FindForward { .. } - | Operator::Mark - | Operator::Jump { .. } - | Operator::FindBackward { .. } - | Operator::Sneak { .. } - | Operator::SneakBackward { .. } - | Operator::Register - | Operator::RecordRegister - | Operator::ReplayRegister - | Operator::Replace - | Operator::Digraph { .. } - | Operator::Literal { .. } - | Operator::ChangeSurrounds { - target: Some(_), .. - } - | Operator::DeleteSurrounds => true, - Operator::Change - | Operator::Delete - | Operator::Yank - | Operator::Rewrap - | Operator::Indent - | Operator::Outdent - | Operator::AutoIndent - | Operator::ShellCommand - | Operator::Lowercase - | Operator::Uppercase - | Operator::Rot13 - | Operator::Rot47 - | Operator::ReplaceWithRegister - | Operator::Exchange - | Operator::Object { .. } - | Operator::ChangeSurrounds { target: None, .. } - | Operator::OppositeCase - | Operator::ToggleComments - | Operator::HelixMatch - | Operator::HelixNext { .. } - | Operator::HelixPrevious { .. } => false, - } - } - - pub fn starts_dot_recording(&self) -> bool { - match self { - Operator::Change - | Operator::Delete - | Operator::Replace - | Operator::Indent - | Operator::Outdent - | Operator::AutoIndent - | Operator::Lowercase - | Operator::Uppercase - | Operator::OppositeCase - | Operator::Rot13 - | Operator::Rot47 - | Operator::ToggleComments - | Operator::ReplaceWithRegister - | Operator::Rewrap - | Operator::ShellCommand - | Operator::AddSurrounds { target: None } - | Operator::ChangeSurrounds { target: None, .. } - | Operator::DeleteSurrounds - | Operator::Exchange - | Operator::HelixNext { .. } - | Operator::HelixPrevious { .. } => true, - Operator::Yank - | Operator::Object { .. } - | Operator::FindForward { .. } - | Operator::FindBackward { .. } - | Operator::Sneak { .. } - | Operator::SneakBackward { .. } - | Operator::Mark - | Operator::Digraph { .. } - | Operator::Literal { .. } - | Operator::AddSurrounds { .. } - | Operator::ChangeSurrounds { .. } - | Operator::Jump { .. } - | Operator::Register - | Operator::RecordRegister - | Operator::ReplayRegister - | Operator::HelixMatch => false, - } - } -} - -struct RegisterMatch { - name: char, - contents: SharedString, -} - -pub struct RegistersViewDelegate { - selected_index: usize, - matches: Vec, -} - -impl PickerDelegate for RegistersViewDelegate { - type ListItem = Div; - - fn match_count(&self) -> usize { - self.matches.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; - cx.notify(); - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - Arc::default() - } - - fn update_matches( - &mut self, - _: String, - _: &mut Window, - _: &mut Context>, - ) -> gpui::Task<()> { - Task::ready(()) - } - - fn confirm(&mut self, _: bool, _: &mut Window, _: &mut Context>) {} - - fn dismissed(&mut self, _: &mut Window, _: &mut Context>) {} - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - let register_match = self.matches.get(ix)?; - - let mut output = String::new(); - let mut runs = Vec::new(); - output.push('"'); - output.push(register_match.name); - runs.push(( - 0..output.len(), - HighlightStyle::color(cx.theme().colors().text_accent), - )); - output.push(' '); - output.push(' '); - let mut base = output.len(); - for (ix, c) in register_match.contents.char_indices() { - if ix > 100 { - break; - } - let replace = match c { - '\t' => Some("\\t".to_string()), - '\n' => Some("\\n".to_string()), - '\r' => Some("\\r".to_string()), - c if is_invisible(c) => { - if c <= '\x1f' { - replacement(c).map(|s| s.to_string()) - } else { - Some(format!("\\u{:04X}", c as u32)) - } - } - _ => None, - }; - let Some(replace) = replace else { - output.push(c); - continue; - }; - output.push_str(&replace); - runs.push(( - base + ix..base + ix + replace.len(), - HighlightStyle::color(cx.theme().colors().text_muted), - )); - base += replace.len() - c.len_utf8(); - } - - let theme = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color: cx.theme().colors().editor_foreground, - font_family: theme.buffer_font.family.clone(), - font_features: theme.buffer_font.features.clone(), - font_fallbacks: theme.buffer_font.fallbacks.clone(), - font_size: theme.buffer_font_size(cx).into(), - line_height: (theme.line_height() * theme.buffer_font_size(cx)).into(), - font_weight: theme.buffer_font.weight, - font_style: theme.buffer_font.style, - ..Default::default() - }; - - Some( - h_flex() - .when(selected, |el| el.bg(cx.theme().colors().element_selected)) - .font_buffer(cx) - .text_buffer(cx) - .h(theme.buffer_font_size(cx) * theme.line_height()) - .px_2() - .gap_1() - .child(StyledText::new(output).with_default_highlights(&text_style, runs)), - ) - } -} - -pub struct RegistersView {} - -impl RegistersView { - fn register(workspace: &mut Workspace, _window: Option<&mut Window>) { - workspace.register_action(|workspace, _: &ToggleRegistersView, window, cx| { - Self::toggle(workspace, window, cx); - }); - } - - pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { - let editor = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)); - workspace.toggle_modal(window, cx, move |window, cx| { - RegistersView::new(editor, window, cx) - }); - } - - fn new( - editor: Option>, - window: &mut Window, - cx: &mut Context>, - ) -> Picker { - let mut matches = Vec::default(); - cx.update_global(|globals: &mut VimGlobals, cx| { - for name in ['"', '+', '*'] { - if let Some(register) = globals.read_register(Some(name), None, cx) { - matches.push(RegisterMatch { - name, - contents: register.text.clone(), - }) - } - } - if let Some(editor) = editor { - let register = editor.update(cx, |editor, cx| { - globals.read_register(Some('%'), Some(editor), cx) - }); - if let Some(register) = register { - matches.push(RegisterMatch { - name: '%', - contents: register.text, - }) - } - } - for (name, register) in globals.registers.iter() { - if ['"', '+', '*', '%'].contains(name) { - continue; - }; - matches.push(RegisterMatch { - name: *name, - contents: register.text.clone(), - }) - } - }); - matches.sort_by(|a, b| a.name.cmp(&b.name)); - let delegate = RegistersViewDelegate { - selected_index: 0, - matches, - }; - - Picker::nonsearchable_uniform_list(delegate, window, cx) - .width(rems(36.)) - .modal(true) - } -} - -enum MarksMatchInfo { - Path(Arc), - Title(String), - Content { - line: String, - highlights: Vec<(Range, HighlightStyle)>, - }, -} - -impl MarksMatchInfo { - fn from_chunks<'a>(chunks: impl Iterator>, cx: &App) -> Self { - let mut line = String::new(); - let mut highlights = Vec::new(); - let mut offset = 0; - for chunk in chunks { - line.push_str(chunk.text); - if let Some(highlight_style) = chunk.syntax_highlight_id - && let Some(highlight) = highlight_style.style(cx.theme().syntax()) - { - highlights.push((offset..offset + chunk.text.len(), highlight)) - } - offset += chunk.text.len(); - } - MarksMatchInfo::Content { line, highlights } - } -} - -struct MarksMatch { - name: String, - position: Point, - info: MarksMatchInfo, -} - -pub struct MarksViewDelegate { - selected_index: usize, - matches: Vec, - point_column_width: usize, - workspace: WeakEntity, -} - -impl PickerDelegate for MarksViewDelegate { - type ListItem = Div; - - fn match_count(&self) -> usize { - self.matches.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; - cx.notify(); - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - Arc::default() - } - - fn update_matches( - &mut self, - _: String, - _: &mut Window, - cx: &mut Context>, - ) -> gpui::Task<()> { - let Some(workspace) = self.workspace.upgrade() else { - return Task::ready(()); - }; - cx.spawn(async move |picker, cx| { - let mut matches = Vec::new(); - let _ = workspace.update(cx, |workspace, cx| { - let entity_id = cx.entity_id(); - let Some(editor) = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - else { - return; - }; - let editor = editor.read(cx); - let mut has_seen = HashSet::new(); - let Some(marks_state) = cx.global::().marks.get(&entity_id) else { - return; - }; - let marks_state = marks_state.read(cx); - - if let Some(map) = marks_state - .multibuffer_marks - .get(&editor.buffer().entity_id()) - { - for (name, anchors) in map { - if has_seen.contains(name) { - continue; - } - has_seen.insert(name.clone()); - let Some(anchor) = anchors.first() else { - continue; - }; - - let snapshot = editor.buffer().read(cx).snapshot(cx); - let position = anchor.to_point(&snapshot); - - let chunks = snapshot.chunks( - Point::new(position.row, 0) - ..Point::new( - position.row, - snapshot.line_len(MultiBufferRow(position.row)), - ), - true, - ); - matches.push(MarksMatch { - name: name.clone(), - position, - info: MarksMatchInfo::from_chunks(chunks, cx), - }) - } - } - - if let Some(buffer) = editor.buffer().read(cx).as_singleton() { - let buffer = buffer.read(cx); - if let Some(map) = marks_state.buffer_marks.get(&buffer.remote_id()) { - for (name, anchors) in map { - if has_seen.contains(name) { - continue; - } - has_seen.insert(name.clone()); - let Some(anchor) = anchors.first() else { - continue; - }; - let snapshot = buffer.snapshot(); - let position = anchor.to_point(&snapshot); - let chunks = snapshot.chunks( - Point::new(position.row, 0) - ..Point::new(position.row, snapshot.line_len(position.row)), - true, - ); - - matches.push(MarksMatch { - name: name.clone(), - position, - info: MarksMatchInfo::from_chunks(chunks, cx), - }) - } - } - } - - for (name, mark_location) in marks_state.global_marks.iter() { - if has_seen.contains(name) { - continue; - } - has_seen.insert(name.clone()); - - match mark_location { - MarkLocation::Buffer(entity_id) => { - if let Some(&anchor) = marks_state - .multibuffer_marks - .get(entity_id) - .and_then(|map| map.get(name)) - .and_then(|anchors| anchors.first()) - { - let Some((info, snapshot)) = workspace - .items(cx) - .filter_map(|item| item.act_as::(cx)) - .map(|entity| entity.read(cx).buffer()) - .find(|buffer| buffer.entity_id().eq(entity_id)) - .map(|buffer| { - ( - MarksMatchInfo::Title( - buffer.read(cx).title(cx).to_string(), - ), - buffer.read(cx).snapshot(cx), - ) - }) - else { - continue; - }; - matches.push(MarksMatch { - name: name.clone(), - position: anchor.to_point(&snapshot), - info, - }); - } - } - MarkLocation::Path(path) => { - if let Some(&position) = marks_state - .serialized_marks - .get(path.as_ref()) - .and_then(|map| map.get(name)) - .and_then(|points| points.first()) - { - let info = MarksMatchInfo::Path(path.clone()); - matches.push(MarksMatch { - name: name.clone(), - position, - info, - }); - } - } - } - } - }); - let _ = picker.update(cx, |picker, cx| { - matches.sort_by_key(|a| { - ( - a.name.chars().next().map(|c| c.is_ascii_uppercase()), - a.name.clone(), - ) - }); - let digits = matches - .iter() - .map(|m| (m.position.row + 1).ilog10() + (m.position.column + 1).ilog10()) - .max() - .unwrap_or_default(); - picker.delegate.matches = matches; - picker.delegate.point_column_width = (digits + 4) as usize; - cx.notify(); - }); - }) - } - - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { - let Some(vim) = self - .workspace - .upgrade() - .map(|w| w.read(cx)) - .and_then(|w| w.focused_pane(window, cx).read(cx).active_item()) - .and_then(|item| item.act_as::(cx)) - .and_then(|editor| editor.read(cx).addon::().cloned()) - .map(|addon| addon.entity) - else { - return; - }; - let Some(text): Option> = self - .matches - .get(self.selected_index) - .map(|m| Arc::from(m.name.to_string().into_boxed_str())) - else { - return; - }; - vim.update(cx, |vim, cx| { - vim.jump(text, false, false, window, cx); - }); - - cx.emit(DismissEvent); - } - - fn dismissed(&mut self, _: &mut Window, _: &mut Context>) {} - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - let mark_match = self.matches.get(ix)?; - - let mut left_output = String::new(); - let mut left_runs = Vec::new(); - left_output.push('`'); - left_output.push_str(&mark_match.name); - left_runs.push(( - 0..left_output.len(), - HighlightStyle::color(cx.theme().colors().text_accent), - )); - left_output.push(' '); - left_output.push(' '); - let point_column = format!( - "{},{}", - mark_match.position.row + 1, - mark_match.position.column + 1 - ); - left_output.push_str(&point_column); - if let Some(padding) = self.point_column_width.checked_sub(point_column.len()) { - left_output.push_str(&" ".repeat(padding)); - } - - let (right_output, right_runs): (String, Vec<_>) = match &mark_match.info { - MarksMatchInfo::Path(path) => { - let s = path.to_string_lossy().into_owned(); - ( - s.clone(), - vec![(0..s.len(), HighlightStyle::color(cx.theme().colors().text))], - ) - } - MarksMatchInfo::Title(title) => ( - title.clone(), - vec![( - 0..title.len(), - HighlightStyle::color(cx.theme().colors().text), - )], - ), - MarksMatchInfo::Content { line, highlights } => (line.clone(), highlights.clone()), - }; - - let theme = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color: cx.theme().colors().editor_foreground, - font_family: theme.buffer_font.family.clone(), - font_features: theme.buffer_font.features.clone(), - font_fallbacks: theme.buffer_font.fallbacks.clone(), - font_size: theme.buffer_font_size(cx).into(), - line_height: (theme.line_height() * theme.buffer_font_size(cx)).into(), - font_weight: theme.buffer_font.weight, - font_style: theme.buffer_font.style, - ..Default::default() - }; - - Some( - h_flex() - .when(selected, |el| el.bg(cx.theme().colors().element_selected)) - .font_buffer(cx) - .text_buffer(cx) - .h(theme.buffer_font_size(cx) * theme.line_height()) - .px_2() - .child(StyledText::new(left_output).with_default_highlights(&text_style, left_runs)) - .child( - StyledText::new(right_output).with_default_highlights(&text_style, right_runs), - ), - ) - } -} - -pub struct MarksView {} - -impl MarksView { - fn register(workspace: &mut Workspace, _window: Option<&mut Window>) { - workspace.register_action(|workspace, _: &ToggleMarksView, window, cx| { - Self::toggle(workspace, window, cx); - }); - } - - pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { - let handle = cx.weak_entity(); - workspace.toggle_modal(window, cx, move |window, cx| { - MarksView::new(handle, window, cx) - }); - } - - fn new( - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context>, - ) -> Picker { - let matches = Vec::default(); - let delegate = MarksViewDelegate { - selected_index: 0, - point_column_width: 0, - matches, - workspace, - }; - Picker::nonsearchable_uniform_list(delegate, window, cx) - .width(rems(36.)) - .modal(true) - } -} - -pub struct VimDb(ThreadSafeConnection); - -impl Domain for VimDb { - const NAME: &str = stringify!(VimDb); - - const MIGRATIONS: &[&str] = &[ - sql! ( - CREATE TABLE vim_marks ( - workspace_id INTEGER, - mark_name TEXT, - path BLOB, - value TEXT - ); - CREATE UNIQUE INDEX idx_vim_marks ON vim_marks (workspace_id, mark_name, path); - ), - sql! ( - CREATE TABLE vim_global_marks_paths( - workspace_id INTEGER, - mark_name TEXT, - path BLOB - ); - CREATE UNIQUE INDEX idx_vim_global_marks_paths - ON vim_global_marks_paths(workspace_id, mark_name); - ), - ]; -} - -db::static_connection!(DB, VimDb, [WorkspaceDb]); - -struct SerializedMark { - path: Arc, - name: String, - points: Vec, -} - -impl VimDb { - pub(crate) async fn set_marks( - &self, - workspace_id: WorkspaceId, - path: Arc, - marks: HashMap>, - ) -> Result<()> { - log::debug!("Setting path {path:?} for {} marks", marks.len()); - - self.write(move |conn| { - let mut query = conn.exec_bound(sql!( - INSERT OR REPLACE INTO vim_marks - (workspace_id, mark_name, path, value) - VALUES - (?, ?, ?, ?) - ))?; - for (mark_name, value) in marks { - let pairs: Vec<(u32, u32)> = value - .into_iter() - .map(|point| (point.row, point.column)) - .collect(); - let serialized = serde_json::to_string(&pairs)?; - query((workspace_id, mark_name, path.clone(), serialized))?; - } - Ok(()) - }) - .await - } - - fn get_marks(&self, workspace_id: WorkspaceId) -> Result> { - let result: Vec<(Arc, String, String)> = self.select_bound(sql!( - SELECT path, mark_name, value FROM vim_marks - WHERE workspace_id = ? - ))?(workspace_id)?; - - Ok(result - .into_iter() - .filter_map(|(path, name, value)| { - let pairs: Vec<(u32, u32)> = serde_json::from_str(&value).log_err()?; - Some(SerializedMark { - path, - name, - points: pairs - .into_iter() - .map(|(row, column)| Point { row, column }) - .collect(), - }) - }) - .collect()) - } - - pub(crate) async fn delete_mark( - &self, - workspace_id: WorkspaceId, - path: Arc, - mark_name: String, - ) -> Result<()> { - self.write(move |conn| { - conn.exec_bound(sql!( - DELETE FROM vim_marks - WHERE workspace_id = ? AND mark_name = ? AND path = ? - ))?((workspace_id, mark_name, path)) - }) - .await - } - - pub(crate) async fn set_global_mark_path( - &self, - workspace_id: WorkspaceId, - mark_name: String, - path: Arc, - ) -> Result<()> { - log::debug!("Setting global mark path {path:?} for {mark_name}"); - self.write(move |conn| { - conn.exec_bound(sql!( - INSERT OR REPLACE INTO vim_global_marks_paths - (workspace_id, mark_name, path) - VALUES - (?, ?, ?) - ))?((workspace_id, mark_name, path)) - }) - .await - } - - pub fn get_global_marks_paths( - &self, - workspace_id: WorkspaceId, - ) -> Result)>> { - self.select_bound(sql!( - SELECT mark_name, path FROM vim_global_marks_paths - WHERE workspace_id = ? - ))?(workspace_id) - } - - pub(crate) async fn delete_global_marks_path( - &self, - workspace_id: WorkspaceId, - mark_name: String, - ) -> Result<()> { - self.write(move |conn| { - conn.exec_bound(sql!( - DELETE FROM vim_global_marks_paths - WHERE workspace_id = ? AND mark_name = ? - ))?((workspace_id, mark_name)) - }) - .await - } -} diff --git a/crates/vim/src/surrounds.rs b/crates/vim/src/surrounds.rs deleted file mode 100644 index b3f9307aac..0000000000 --- a/crates/vim/src/surrounds.rs +++ /dev/null @@ -1,1705 +0,0 @@ -use crate::{ - Vim, - motion::{self, Motion}, - object::{Object, surrounding_markers}, - state::Mode, -}; -use editor::{Bias, MultiBufferOffset, movement}; -use gpui::{Context, Window}; -use language::BracketPair; - -use std::sync::Arc; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum SurroundsType { - Motion(Motion), - Object(Object, bool), - Selection, -} - -impl Vim { - pub fn add_surrounds( - &mut self, - text: Arc, - target: SurroundsType, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - let count = Vim::take_count(cx); - let forced_motion = Vim::take_forced_motion(cx); - let mode = self.mode; - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - - let pair = match find_surround_pair(&all_support_surround_pair(), &text) { - Some(pair) => pair.clone(), - None => BracketPair { - start: text.to_string(), - end: text.to_string(), - close: true, - surround: true, - newline: false, - }, - }; - let surround = pair.end != surround_alias((*text).as_ref()); - let display_map = editor.display_snapshot(cx); - let display_selections = editor.selections.all_adjusted_display(&display_map); - let mut edits = Vec::new(); - let mut anchors = Vec::new(); - - for selection in &display_selections { - let range = match &target { - SurroundsType::Object(object, around) => { - object.range(&display_map, selection.clone(), *around, None) - } - SurroundsType::Motion(motion) => { - motion - .range( - &display_map, - selection.clone(), - count, - &text_layout_details, - forced_motion, - ) - .map(|(mut range, _)| { - // The Motion::CurrentLine operation will contain the newline of the current line and leading/trailing whitespace - if let Motion::CurrentLine = motion { - range.start = motion::first_non_whitespace( - &display_map, - false, - range.start, - ); - range.end = movement::saturating_right( - &display_map, - motion::last_non_whitespace(&display_map, range.end, 1), - ); - } - range - }) - } - SurroundsType::Selection => Some(selection.range()), - }; - - if let Some(range) = range { - let start = range.start.to_offset(&display_map, Bias::Right); - let end = range.end.to_offset(&display_map, Bias::Left); - let (start_cursor_str, end_cursor_str) = if mode == Mode::VisualLine { - (format!("{}\n", pair.start), format!("\n{}", pair.end)) - } else { - let maybe_space = if surround { " " } else { "" }; - ( - format!("{}{}", pair.start, maybe_space), - format!("{}{}", maybe_space, pair.end), - ) - }; - let start_anchor = display_map.buffer_snapshot().anchor_before(start); - - edits.push((start..start, start_cursor_str)); - edits.push((end..end, end_cursor_str)); - anchors.push(start_anchor..start_anchor); - } else { - let start_anchor = display_map - .buffer_snapshot() - .anchor_before(selection.head().to_offset(&display_map, Bias::Left)); - anchors.push(start_anchor..start_anchor); - } - } - - editor.edit(edits, cx); - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(Default::default(), window, cx, |s| { - if mode == Mode::VisualBlock { - s.select_anchor_ranges(anchors.into_iter().take(1)) - } else { - s.select_anchor_ranges(anchors) - } - }); - }); - }); - self.switch_mode(Mode::Normal, false, window, cx); - } - - pub fn delete_surrounds( - &mut self, - text: Arc, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - - // only legitimate surrounds can be removed - let pair = match find_surround_pair(&all_support_surround_pair(), &text) { - Some(pair) => pair.clone(), - None => return, - }; - let pair_object = match pair_to_object(&pair) { - Some(pair_object) => pair_object, - None => return, - }; - let surround = pair.end != *text; - - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - - let display_map = editor.display_snapshot(cx); - let display_selections = editor.selections.all_display(&display_map); - let mut edits = Vec::new(); - let mut anchors = Vec::new(); - - for selection in &display_selections { - let start = selection.start.to_offset(&display_map, Bias::Left); - if let Some(range) = - pair_object.range(&display_map, selection.clone(), true, None) - { - // If the current parenthesis object is single-line, - // then we need to filter whether it is the current line or not - if !pair_object.is_multiline() { - let is_same_row = selection.start.row() == range.start.row() - && selection.end.row() == range.end.row(); - if !is_same_row { - anchors.push(start..start); - continue; - } - } - // This is a bit cumbersome, and it is written to deal with some special cases, as shown below - // hello«ˇ "hello in a word" »again. - // Sometimes the expand_selection will not be matched at both ends, and there will be extra spaces - // In order to be able to accurately match and replace in this case, some cumbersome methods are used - let mut chars_and_offset = display_map - .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left)) - .peekable(); - while let Some((ch, offset)) = chars_and_offset.next() { - if ch.to_string() == pair.start { - let start = offset; - let mut end = start + 1usize; - if surround - && let Some((next_ch, _)) = chars_and_offset.peek() - && next_ch.eq(&' ') - { - end += 1; - } - edits.push((start..end, "")); - anchors.push(start..start); - break; - } - } - let mut reverse_chars_and_offsets = display_map - .reverse_buffer_chars_at(range.end.to_offset(&display_map, Bias::Left)) - .peekable(); - while let Some((ch, offset)) = reverse_chars_and_offsets.next() { - if ch.to_string() == pair.end { - let mut start = offset; - let end = start + 1usize; - if surround - && let Some((next_ch, _)) = reverse_chars_and_offsets.peek() - && next_ch.eq(&' ') - { - start -= 1; - } - edits.push((start..end, "")); - break; - } - } - } else { - anchors.push(start..start); - } - } - - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges(anchors); - }); - edits.sort_by_key(|(range, _)| range.start); - editor.edit(edits, cx); - editor.set_clip_at_line_ends(true, cx); - }); - }); - } - - pub fn change_surrounds( - &mut self, - text: Arc, - target: Object, - opening: bool, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(will_replace_pair) = self.object_to_bracket_pair(target, cx) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - - let pair = match find_surround_pair(&all_support_surround_pair(), &text) { - Some(pair) => pair.clone(), - None => BracketPair { - start: text.to_string(), - end: text.to_string(), - close: true, - surround: true, - newline: false, - }, - }; - - // A single space should be added if the new surround is a - // bracket and not a quote (pair.start != pair.end) and if - // the bracket used is the opening bracket. - let add_space = - !(pair.start == pair.end) && (pair.end != surround_alias((*text).as_ref())); - - // Space should be preserved if either the surrounding - // characters being updated are quotes - // (will_replace_pair.start == will_replace_pair.end) or if - // the bracket used in the command is not an opening - // bracket. - let preserve_space = - will_replace_pair.start == will_replace_pair.end || !opening; - - let display_map = editor.display_snapshot(cx); - let selections = editor.selections.all_adjusted_display(&display_map); - let mut edits = Vec::new(); - let mut anchors = Vec::new(); - - for selection in &selections { - let start = selection.start.to_offset(&display_map, Bias::Left); - if let Some(range) = - target.range(&display_map, selection.clone(), true, None) - { - if !target.is_multiline() { - let is_same_row = selection.start.row() == range.start.row() - && selection.end.row() == range.end.row(); - if !is_same_row { - anchors.push(start..start); - continue; - } - } - - // Keeps track of the length of the string that is - // going to be edited on the start so we can ensure - // that the end replacement string does not exceed - // this value. Helpful when dealing with newlines. - let mut edit_len = 0; - let mut open_range_end = MultiBufferOffset(0); - let mut chars_and_offset = display_map - .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left)) - .peekable(); - - while let Some((ch, offset)) = chars_and_offset.next() { - if ch.to_string() == will_replace_pair.start { - let mut open_str = pair.start.clone(); - let start = offset; - open_range_end = start + 1usize; - while let Some((next_ch, _)) = chars_and_offset.next() - && next_ch == ' ' - { - open_range_end += 1; - - if preserve_space { - open_str.push(next_ch); - } - } - - if add_space { - open_str.push(' '); - }; - - edit_len = open_range_end - start; - edits.push((start..open_range_end, open_str)); - anchors.push(start..start); - break; - } - } - - let mut reverse_chars_and_offsets = display_map - .reverse_buffer_chars_at( - range.end.to_offset(&display_map, Bias::Left), - ) - .peekable(); - while let Some((ch, offset)) = reverse_chars_and_offsets.next() { - if ch.to_string() == will_replace_pair.end { - let mut close_str = String::new(); - let mut start = offset; - let end = start + 1usize; - while let Some((next_ch, _)) = reverse_chars_and_offsets.next() - && next_ch == ' ' - && close_str.len() < edit_len - 1 - && start > open_range_end - { - start -= 1; - - if preserve_space { - close_str.push(next_ch); - } - } - - if add_space { - close_str.push(' '); - }; - - close_str.push_str(&pair.end); - edits.push((start..end, close_str)); - break; - } - } - } else { - anchors.push(start..start); - } - } - - let stable_anchors = editor - .selections - .disjoint_anchors_arc() - .iter() - .map(|selection| { - let start = selection.start.bias_left(&display_map.buffer_snapshot()); - start..start - }) - .collect::>(); - edits.sort_by_key(|(range, _)| range.start); - editor.edit(edits, cx); - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.select_anchor_ranges(stable_anchors); - }); - }); - }); - } - } - - /// Checks if any of the current cursors are surrounded by a valid pair of brackets. - /// - /// This method supports multiple cursors and checks each cursor for a valid pair of brackets. - /// A pair of brackets is considered valid if it is well-formed and properly closed. - /// - /// If a valid pair of brackets is found, the method returns `true` and the cursor is automatically moved to the start of the bracket pair. - /// If no valid pair of brackets is found for any cursor, the method returns `false`. - pub fn check_and_move_to_valid_bracket_pair( - &mut self, - object: Object, - window: &mut Window, - cx: &mut Context, - ) -> bool { - let mut valid = false; - if let Some(pair) = self.object_to_bracket_pair(object, cx) { - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - let display_map = editor.display_snapshot(cx); - let selections = editor.selections.all_adjusted_display(&display_map); - let mut anchors = Vec::new(); - - for selection in &selections { - let start = selection.start.to_offset(&display_map, Bias::Left); - if let Some(range) = - object.range(&display_map, selection.clone(), true, None) - { - // If the current parenthesis object is single-line, - // then we need to filter whether it is the current line or not - if object.is_multiline() - || (!object.is_multiline() - && selection.start.row() == range.start.row() - && selection.end.row() == range.end.row()) - { - valid = true; - let chars_and_offset = display_map - .buffer_chars_at( - range.start.to_offset(&display_map, Bias::Left), - ) - .peekable(); - for (ch, offset) in chars_and_offset { - if ch.to_string() == pair.start { - anchors.push(offset..offset); - break; - } - } - } else { - anchors.push(start..start) - } - } else { - anchors.push(start..start) - } - } - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges(anchors); - }); - editor.set_clip_at_line_ends(true, cx); - }); - }); - } - valid - } - - fn object_to_bracket_pair( - &self, - object: Object, - cx: &mut Context, - ) -> Option { - match object { - Object::Quotes => Some(BracketPair { - start: "'".to_string(), - end: "'".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::BackQuotes => Some(BracketPair { - start: "`".to_string(), - end: "`".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::DoubleQuotes => Some(BracketPair { - start: "\"".to_string(), - end: "\"".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::VerticalBars => Some(BracketPair { - start: "|".to_string(), - end: "|".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::Parentheses => Some(BracketPair { - start: "(".to_string(), - end: ")".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::SquareBrackets => Some(BracketPair { - start: "[".to_string(), - end: "]".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::CurlyBrackets { .. } => Some(BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::AngleBrackets => Some(BracketPair { - start: "<".to_string(), - end: ">".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::AnyBrackets => { - // If we're dealing with `AnyBrackets`, which can map to multiple - // bracket pairs, we'll need to first determine which `BracketPair` to - // target. - // As such, we keep track of the smallest range size, so - // that in cases like `({ name: "John" })` if the cursor is - // inside the curly brackets, we target the curly brackets - // instead of the parentheses. - let mut bracket_pair = None; - let mut min_range_size = usize::MAX; - - let _ = self.editor.update(cx, |editor, cx| { - let display_map = editor.display_snapshot(cx); - let selections = editor.selections.all_adjusted_display(&display_map); - // Even if there's multiple cursors, we'll simply rely on - // the first one to understand what bracket pair to map to. - // I believe we could, if worth it, go one step above and - // have a `BracketPair` per selection, so that `AnyBracket` - // could work in situations where the transformation below - // could be done. - // - // ``` - // (< name:ˇ'Zed' >) - // <[ name:ˇ'DeltaDB' ]> - // ``` - // - // After using `csb{`: - // - // ``` - // (ˇ{ name:'Zed' }) - // <ˇ{ name:'DeltaDB' }> - // ``` - if let Some(selection) = selections.first() { - let relative_to = selection.head(); - let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')]; - let cursor_offset = relative_to.to_offset(&display_map, Bias::Left); - - for &(open, close) in bracket_pairs.iter() { - if let Some(range) = surrounding_markers( - &display_map, - relative_to, - true, - false, - open, - close, - ) { - let start_offset = range.start.to_offset(&display_map, Bias::Left); - let end_offset = range.end.to_offset(&display_map, Bias::Right); - - if cursor_offset >= start_offset && cursor_offset <= end_offset { - let size = end_offset - start_offset; - if size < min_range_size { - min_range_size = size; - bracket_pair = Some(BracketPair { - start: open.to_string(), - end: close.to_string(), - close: true, - surround: true, - newline: false, - }) - } - } - } - } - } - }); - - bracket_pair - } - _ => None, - } - } -} - -fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> { - pairs - .iter() - .find(|pair| pair.start == surround_alias(ch) || pair.end == surround_alias(ch)) -} - -fn surround_alias(ch: &str) -> &str { - match ch { - "b" => ")", - "B" => "}", - "a" => ">", - "r" => "]", - _ => ch, - } -} - -fn all_support_surround_pair() -> Vec { - vec![ - BracketPair { - start: "{".into(), - end: "}".into(), - close: true, - surround: true, - newline: false, - }, - BracketPair { - start: "'".into(), - end: "'".into(), - close: true, - surround: true, - newline: false, - }, - BracketPair { - start: "`".into(), - end: "`".into(), - close: true, - surround: true, - newline: false, - }, - BracketPair { - start: "\"".into(), - end: "\"".into(), - close: true, - surround: true, - newline: false, - }, - BracketPair { - start: "(".into(), - end: ")".into(), - close: true, - surround: true, - newline: false, - }, - BracketPair { - start: "|".into(), - end: "|".into(), - close: true, - surround: true, - newline: false, - }, - BracketPair { - start: "[".into(), - end: "]".into(), - close: true, - surround: true, - newline: false, - }, - BracketPair { - start: "<".into(), - end: ">".into(), - close: true, - surround: true, - newline: false, - }, - ] -} - -fn pair_to_object(pair: &BracketPair) -> Option { - match pair.start.as_str() { - "'" => Some(Object::Quotes), - "`" => Some(Object::BackQuotes), - "\"" => Some(Object::DoubleQuotes), - "|" => Some(Object::VerticalBars), - "(" => Some(Object::Parentheses), - "[" => Some(Object::SquareBrackets), - "{" => Some(Object::CurlyBrackets), - "<" => Some(Object::AngleBrackets), - _ => None, - } -} - -#[cfg(test)] -mod test { - use gpui::KeyBinding; - use indoc::indoc; - - use crate::{PushAddSurrounds, object::AnyBrackets, state::Mode, test::VimTestContext}; - - #[gpui::test] - async fn test_add_surrounds(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // test add surrounds with around - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s i w {"); - cx.assert_state( - indoc! {" - The ˇ{ quick } brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test add surrounds not with around - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s i w }"); - cx.assert_state( - indoc! {" - The ˇ{quick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test add surrounds with motion - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s $ }"); - cx.assert_state( - indoc! {" - The quˇ{ick brown} - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test add surrounds with multi cursor - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the laˇzy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s i w '"); - cx.assert_state( - indoc! {" - The ˇ'quick' brown - fox jumps over - the ˇ'lazy' dog."}, - Mode::Normal, - ); - - // test multi cursor add surrounds with motion - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the laˇzy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s $ '"); - cx.assert_state( - indoc! {" - The quˇ'ick brown' - fox jumps over - the laˇ'zy dog.'"}, - Mode::Normal, - ); - - // test multi cursor add surrounds with motion and custom string - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the laˇzy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s $ 1"); - cx.assert_state( - indoc! {" - The quˇ1ick brown1 - fox jumps over - the laˇ1zy dog.1"}, - Mode::Normal, - ); - - // test add surrounds with motion current line - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s s {"); - cx.assert_state( - indoc! {" - ˇ{ The quick brown } - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The quˇick brown• - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s s {"); - cx.assert_state( - indoc! {" - ˇ{ The quick brown }• - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("2 y s s )"); - cx.assert_state( - indoc! {" - ˇ({ The quick brown }• - fox jumps over) - the lazy dog."}, - Mode::Normal, - ); - - // test add surrounds around object - cx.set_state( - indoc! {" - The [quˇick] brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s a ] )"); - cx.assert_state( - indoc! {" - The ˇ([quick]) brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test add surrounds inside object - cx.set_state( - indoc! {" - The [quˇick] brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s i ] )"); - cx.assert_state( - indoc! {" - The [ˇ(quick)] brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_add_surrounds_visual(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "shift-s", - PushAddSurrounds {}, - Some("vim_mode == visual"), - )]) - }); - - // test add surrounds with around - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("v i w shift-s {"); - cx.assert_state( - indoc! {" - The ˇ{ quick } brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test add surrounds not with around - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("v i w shift-s }"); - cx.assert_state( - indoc! {" - The ˇ{quick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test add surrounds with motion - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("v e shift-s }"); - cx.assert_state( - indoc! {" - The quˇ{ick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test add surrounds with multi cursor - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the laˇzy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("v i w shift-s '"); - cx.assert_state( - indoc! {" - The ˇ'quick' brown - fox jumps over - the ˇ'lazy' dog."}, - Mode::Normal, - ); - - // test add surrounds with visual block - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("ctrl-v i w j j shift-s '"); - cx.assert_state( - indoc! {" - The ˇ'quick' brown - fox 'jumps' over - the 'lazy 'dog."}, - Mode::Normal, - ); - - // test add surrounds with visual line - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("j shift-v shift-s '"); - cx.assert_state( - indoc! {" - The quick brown - ˇ' - fox jumps over - ' - the lazy dog."}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_delete_surrounds(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // test delete surround - cx.set_state( - indoc! {" - The {quˇick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s {"); - cx.assert_state( - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test delete not exist surrounds - cx.set_state( - indoc! {" - The {quˇick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s ["); - cx.assert_state( - indoc! {" - The {quˇick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test delete surround forward exist, in the surrounds plugin of other editors, - // the bracket pair in front of the current line will be deleted here, which is not implemented at the moment - cx.set_state( - indoc! {" - The {quick} brˇown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s {"); - cx.assert_state( - indoc! {" - The {quick} brˇown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test cursor delete inner surrounds - cx.set_state( - indoc! {" - The { quick brown - fox jumˇps over } - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s {"); - cx.assert_state( - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test multi cursor delete surrounds - cx.set_state( - indoc! {" - The [quˇick] brown - fox jumps over - the [laˇzy] dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s ]"); - cx.assert_state( - indoc! {" - The ˇquick brown - fox jumps over - the ˇlazy dog."}, - Mode::Normal, - ); - - // test multi cursor delete surrounds with around - cx.set_state( - indoc! {" - Tˇhe [ quick ] brown - fox jumps over - the [laˇzy] dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s ["); - cx.assert_state( - indoc! {" - The ˇquick brown - fox jumps over - the ˇlazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - Tˇhe [ quick ] brown - fox jumps over - the [laˇzy ] dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s ["); - cx.assert_state( - indoc! {" - The ˇquick brown - fox jumps over - the ˇlazy dog."}, - Mode::Normal, - ); - - // test multi cursor delete different surrounds - // the pair corresponding to the two cursors is the same, - // so they are combined into one cursor - cx.set_state( - indoc! {" - The [quˇick] brown - fox jumps over - the {laˇzy} dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s {"); - cx.assert_state( - indoc! {" - The [quick] brown - fox jumps over - the ˇlazy dog."}, - Mode::Normal, - ); - - // test delete surround with multi cursor and nest surrounds - cx.set_state( - indoc! {" - fn test_surround() { - ifˇ 2 > 1 { - ˇprintln!(\"it is fine\"); - }; - }"}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s }"); - cx.assert_state( - indoc! {" - fn test_surround() ˇ - if 2 > 1 ˇ - println!(\"it is fine\"); - ; - "}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_change_surrounds(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - The {quˇick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s { ["); - cx.assert_state( - indoc! {" - The ˇ[ quick ] brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test multi cursor change surrounds - cx.set_state( - indoc! {" - The {quˇick} brown - fox jumps over - the {laˇzy} dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s { ["); - cx.assert_state( - indoc! {" - The ˇ[ quick ] brown - fox jumps over - the ˇ[ lazy ] dog."}, - Mode::Normal, - ); - - // test multi cursor delete different surrounds with after cursor - cx.set_state( - indoc! {" - Thˇe {quick} brown - fox jumps over - the {laˇzy} dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s { ["); - cx.assert_state( - indoc! {" - The ˇ[ quick ] brown - fox jumps over - the ˇ[ lazy ] dog."}, - Mode::Normal, - ); - - // test multi cursor change surrount with not around - cx.set_state( - indoc! {" - Thˇe { quick } brown - fox jumps over - the {laˇzy} dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s { ]"); - cx.assert_state( - indoc! {" - The ˇ[quick] brown - fox jumps over - the ˇ[lazy] dog."}, - Mode::Normal, - ); - - // test multi cursor change with not exist surround - cx.set_state( - indoc! {" - The {quˇick} brown - fox jumps over - the [laˇzy] dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s [ '"); - cx.assert_state( - indoc! {" - The {quick} brown - fox jumps over - the ˇ'lazy' dog."}, - Mode::Normal, - ); - - // test change nesting surrounds - cx.set_state( - indoc! {" - fn test_surround() { - ifˇ 2 > 1 { - ˇprintln!(\"it is fine\"); - } - };"}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s } ]"); - cx.assert_state( - indoc! {" - fn test_surround() ˇ[ - if 2 > 1 ˇ[ - println!(\"it is fine\"); - ] - ];"}, - Mode::Normal, - ); - - // test spaces with quote change surrounds - cx.set_state( - indoc! {" - fn test_surround() { - \"ˇ \" - };"}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s \" '"); - cx.assert_state( - indoc! {" - fn test_surround() { - ˇ' ' - };"}, - Mode::Normal, - ); - - // Currently, the same test case but using the closing bracket `]` - // actually removes a whitespace before the closing bracket, something - // that might need to be fixed? - cx.set_state( - indoc! {" - fn test_surround() { - ifˇ 2 > 1 { - ˇprintln!(\"it is fine\"); - } - };"}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s { ]"); - cx.assert_state( - indoc! {" - fn test_surround() ˇ[ - if 2 > 1 ˇ[ - println!(\"it is fine\"); - ] - ];"}, - Mode::Normal, - ); - - // test change quotes. - cx.set_state(indoc! {"' ˇstr '"}, Mode::Normal); - cx.simulate_keystrokes("c s ' \""); - cx.assert_state(indoc! {"ˇ\" str \""}, Mode::Normal); - - // test multi cursor change quotes - cx.set_state( - indoc! {" - ' ˇstr ' - some example text here - ˇ' str ' - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s ' \""); - cx.assert_state( - indoc! {" - ˇ\" str \" - some example text here - ˇ\" str \" - "}, - Mode::Normal, - ); - - // test quote to bracket spacing. - cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal); - cx.simulate_keystrokes("c s ' {"); - cx.assert_state(indoc! {"ˇ{ foobar }"}, Mode::Normal); - - cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal); - cx.simulate_keystrokes("c s ' }"); - cx.assert_state(indoc! {"ˇ{foobar}"}, Mode::Normal); - } - - #[gpui::test] - async fn test_change_surrounds_any_brackets(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Update keybindings so that using `csb` triggers Vim's `AnyBrackets` - // action. - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "b", - AnyBrackets, - Some("vim_operator == a || vim_operator == i || vim_operator == cs"), - )]); - }); - - cx.set_state(indoc! {"{braˇcketed}"}, Mode::Normal); - cx.simulate_keystrokes("c s b ["); - cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal); - - cx.set_state(indoc! {"[braˇcketed]"}, Mode::Normal); - cx.simulate_keystrokes("c s b {"); - cx.assert_state(indoc! {"ˇ{ bracketed }"}, Mode::Normal); - - cx.set_state(indoc! {""}, Mode::Normal); - cx.simulate_keystrokes("c s b ["); - cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal); - - cx.set_state(indoc! {"(braˇcketed)"}, Mode::Normal); - cx.simulate_keystrokes("c s b ["); - cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal); - - cx.set_state(indoc! {"(< name: ˇ'Zed' >)"}, Mode::Normal); - cx.simulate_keystrokes("c s b }"); - cx.assert_state(indoc! {"(ˇ{ name: 'Zed' })"}, Mode::Normal); - - cx.set_state( - indoc! {" - (< name: ˇ'Zed' >) - (< nˇame: 'DeltaDB' >) - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s b {"); - cx.set_state( - indoc! {" - (ˇ{ name: 'Zed' }) - (ˇ{ name: 'DeltaDB' }) - "}, - Mode::Normal, - ); - } - - // The following test cases all follow tpope/vim-surround's behaviour - // and are more focused on how whitespace is handled. - #[gpui::test] - async fn test_change_surrounds_vim(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Changing quote to quote should never change the surrounding - // whitespace. - cx.set_state(indoc! {"' ˇa '"}, Mode::Normal); - cx.simulate_keystrokes("c s ' \""); - cx.assert_state(indoc! {"ˇ\" a \""}, Mode::Normal); - - cx.set_state(indoc! {"\" ˇa \""}, Mode::Normal); - cx.simulate_keystrokes("c s \" '"); - cx.assert_state(indoc! {"ˇ' a '"}, Mode::Normal); - - // Changing quote to bracket adds one more space when the opening - // bracket is used, does not affect whitespace when the closing bracket - // is used. - cx.set_state(indoc! {"' ˇa '"}, Mode::Normal); - cx.simulate_keystrokes("c s ' {"); - cx.assert_state(indoc! {"ˇ{ a }"}, Mode::Normal); - - cx.set_state(indoc! {"' ˇa '"}, Mode::Normal); - cx.simulate_keystrokes("c s ' }"); - cx.assert_state(indoc! {"ˇ{ a }"}, Mode::Normal); - - // Changing bracket to quote should remove all space when the - // opening bracket is used and preserve all space when the - // closing one is used. - cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal); - cx.simulate_keystrokes("c s { '"); - cx.assert_state(indoc! {"ˇ'a'"}, Mode::Normal); - - cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal); - cx.simulate_keystrokes("c s } '"); - cx.assert_state(indoc! {"ˇ' a '"}, Mode::Normal); - - // Changing bracket to bracket follows these rules: - // * opening → opening – keeps only one space. - // * opening → closing – removes all space. - // * closing → opening – adds one space. - // * closing → closing – does not change space. - cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal); - cx.simulate_keystrokes("c s { ["); - cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal); - - cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal); - cx.simulate_keystrokes("c s { ]"); - cx.assert_state(indoc! {"ˇ[a]"}, Mode::Normal); - - cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal); - cx.simulate_keystrokes("c s } ["); - cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal); - - cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal); - cx.simulate_keystrokes("c s } ]"); - cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal); - } - - #[gpui::test] - async fn test_surrounds(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s i w ["); - cx.assert_state( - indoc! {" - The ˇ[ quick ] brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.simulate_keystrokes("c s [ }"); - cx.assert_state( - indoc! {" - The ˇ{quick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.simulate_keystrokes("d s {"); - cx.assert_state( - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.simulate_keystrokes("u"); - cx.assert_state( - indoc! {" - The ˇ{quick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_surround_aliases(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // add aliases - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s i w b"); - cx.assert_state( - indoc! {" - The ˇ(quick) brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s i w B"); - cx.assert_state( - indoc! {" - The ˇ{quick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s i w a"); - cx.assert_state( - indoc! {" - The ˇ brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s i w r"); - cx.assert_state( - indoc! {" - The ˇ[quick] brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // change aliases - cx.set_state( - indoc! {" - The {quˇick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s { b"); - cx.assert_state( - indoc! {" - The ˇ(quick) brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The (quˇick) brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s ( B"); - cx.assert_state( - indoc! {" - The ˇ{quick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The (quˇick) brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s ( a"); - cx.assert_state( - indoc! {" - The ˇ brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s < b"); - cx.assert_state( - indoc! {" - The ˇ(quick) brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The (quˇick) brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s ( r"); - cx.assert_state( - indoc! {" - The ˇ[quick] brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The [quˇick] brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s [ b"); - cx.assert_state( - indoc! {" - The ˇ(quick) brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // delete alias - cx.set_state( - indoc! {" - The {quˇick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s B"); - cx.assert_state( - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The (quˇick) brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s b"); - cx.assert_state( - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The [quˇick] brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s r"); - cx.assert_state( - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s a"); - cx.assert_state( - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - } -} diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs deleted file mode 100644 index 4c61479157..0000000000 --- a/crates/vim/src/test.rs +++ /dev/null @@ -1,2503 +0,0 @@ -mod neovim_backed_test_context; -mod neovim_connection; -mod vim_test_context; - -use std::{sync::Arc, time::Duration}; - -use collections::HashMap; -use command_palette::CommandPalette; -use editor::{ - AnchorRangeExt, DisplayPoint, Editor, EditorMode, MultiBuffer, MultiBufferOffset, - actions::{DeleteLine, WrapSelectionsInTag}, - code_context_menus::CodeContextMenu, - display_map::DisplayRow, - test::editor_test_context::EditorTestContext, -}; -use futures::StreamExt; -use gpui::{KeyBinding, Modifiers, MouseButton, TestAppContext, px}; -use itertools::Itertools; -use language::{CursorShape, Language, LanguageConfig, Point}; -pub use neovim_backed_test_context::*; -use settings::SettingsStore; -use ui::Pixels; -use util::test::marked_text_ranges; -pub use vim_test_context::*; - -use indoc::indoc; -use search::BufferSearchBar; - -use crate::{PushSneak, PushSneakBackward, insert::NormalBefore, motion, state::Mode}; - -use util_macros::perf; - -#[perf] -#[gpui::test] -async fn test_initially_disabled(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, false).await; - cx.simulate_keystrokes("h j k l"); - cx.assert_editor_state("hjklˇ"); -} - -#[perf] -#[gpui::test] -async fn test_neovim(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.simulate_shared_keystrokes("i").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("shift-t e s t space t e s t escape 0 d w") - .await; - cx.shared_state().await.assert_matches(); - cx.assert_editor_state("ˇtest"); -} - -#[perf] -#[gpui::test] -async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.simulate_keystrokes("i"); - assert_eq!(cx.mode(), Mode::Insert); - - // Editor acts as though vim is disabled - cx.disable_vim(); - cx.simulate_keystrokes("h j k l"); - cx.assert_editor_state("hjklˇ"); - - // Selections aren't changed if editor is blurred but vim-mode is still disabled. - cx.cx.set_state("«hjklˇ»"); - cx.assert_editor_state("«hjklˇ»"); - cx.update_editor(|_, window, _cx| window.blur()); - cx.assert_editor_state("«hjklˇ»"); - cx.update_editor(|_, window, cx| cx.focus_self(window)); - cx.assert_editor_state("«hjklˇ»"); - - // Enabling dynamically sets vim mode again and restores normal mode - cx.enable_vim(); - assert_eq!(cx.mode(), Mode::Normal); - cx.simulate_keystrokes("h h h l"); - assert_eq!(cx.buffer_text(), "hjkl".to_owned()); - cx.assert_editor_state("hˇjkl"); - cx.simulate_keystrokes("i T e s t"); - cx.assert_editor_state("hTestˇjkl"); - - // Disabling and enabling resets to normal mode - assert_eq!(cx.mode(), Mode::Insert); - cx.disable_vim(); - cx.enable_vim(); - assert_eq!(cx.mode(), Mode::Normal); -} - -#[perf] -#[gpui::test] -async fn test_cancel_selection(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {"The quick brown fox juˇmps over the lazy dog"}, - Mode::Normal, - ); - // jumps - cx.simulate_keystrokes("v l l"); - cx.assert_editor_state("The quick brown fox ju«mpsˇ» over the lazy dog"); - - cx.simulate_keystrokes("escape"); - cx.assert_editor_state("The quick brown fox jumpˇs over the lazy dog"); - - // go back to the same selection state - cx.simulate_keystrokes("v h h"); - cx.assert_editor_state("The quick brown fox ju«ˇmps» over the lazy dog"); - - // Ctrl-[ should behave like Esc - cx.simulate_keystrokes("ctrl-["); - cx.assert_editor_state("The quick brown fox juˇmps over the lazy dog"); -} - -#[perf] -#[gpui::test] -async fn test_buffer_search(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - The quick brown - fox juˇmps over - the lazy dog"}, - Mode::Normal, - ); - cx.simulate_keystrokes("/"); - - let search_bar = cx.workspace(|workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .toolbar() - .read(cx) - .item_of_type::() - .expect("Buffer search bar should be deployed") - }); - - cx.update_entity(search_bar, |bar, _, cx| { - assert_eq!(bar.query(cx), ""); - }) -} - -#[perf] -#[gpui::test] -async fn test_count_down(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state(indoc! {"aˇa\nbb\ncc\ndd\nee"}, Mode::Normal); - cx.simulate_keystrokes("2 down"); - cx.assert_editor_state("aa\nbb\ncˇc\ndd\nee"); - cx.simulate_keystrokes("9 down"); - cx.assert_editor_state("aa\nbb\ncc\ndd\neˇe"); -} - -#[perf] -#[gpui::test] -async fn test_end_of_document_710(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // goes to end by default - cx.set_state(indoc! {"aˇa\nbb\ncc"}, Mode::Normal); - cx.simulate_keystrokes("shift-g"); - cx.assert_editor_state("aa\nbb\ncˇc"); - - // can go to line 1 (https://github.com/zed-industries/zed/issues/5812) - cx.simulate_keystrokes("1 shift-g"); - cx.assert_editor_state("aˇa\nbb\ncc"); -} - -#[perf] -#[gpui::test] -async fn test_end_of_line_with_times(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // goes to current line end - cx.set_state(indoc! {"ˇaa\nbb\ncc"}, Mode::Normal); - cx.simulate_keystrokes("$"); - cx.assert_editor_state("aˇa\nbb\ncc"); - - // goes to next line end - cx.simulate_keystrokes("2 $"); - cx.assert_editor_state("aa\nbˇb\ncc"); - - // try to exceed the final line. - cx.simulate_keystrokes("4 $"); - cx.assert_editor_state("aa\nbb\ncˇc"); -} - -#[perf] -#[gpui::test] -async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // works in normal mode - cx.set_state(indoc! {"aa\nbˇb\ncc"}, Mode::Normal); - cx.simulate_keystrokes("> >"); - cx.assert_editor_state("aa\n bˇb\ncc"); - cx.simulate_keystrokes("< <"); - cx.assert_editor_state("aa\nbˇb\ncc"); - - // works in visual mode - cx.simulate_keystrokes("shift-v down >"); - cx.assert_editor_state("aa\n bˇb\n cc"); - - // works as operator - cx.set_state("aa\nbˇb\ncc\n", Mode::Normal); - cx.simulate_keystrokes("> j"); - cx.assert_editor_state("aa\n bˇb\n cc\n"); - cx.simulate_keystrokes("< k"); - cx.assert_editor_state("aa\nbˇb\n cc\n"); - cx.simulate_keystrokes("> i p"); - cx.assert_editor_state(" aa\n bˇb\n cc\n"); - cx.simulate_keystrokes("< i p"); - cx.assert_editor_state("aa\nbˇb\n cc\n"); - cx.simulate_keystrokes("< i p"); - cx.assert_editor_state("aa\nbˇb\ncc\n"); - - cx.set_state("ˇaa\nbb\ncc\n", Mode::Normal); - cx.simulate_keystrokes("> 2 j"); - cx.assert_editor_state(" ˇaa\n bb\n cc\n"); - - cx.set_state("aa\nbb\nˇcc\n", Mode::Normal); - cx.simulate_keystrokes("> 2 k"); - cx.assert_editor_state(" aa\n bb\n ˇcc\n"); - - // works with repeat - cx.set_state("a\nb\nccˇc\n", Mode::Normal); - cx.simulate_keystrokes("> 2 k"); - cx.assert_editor_state(" a\n b\n ccˇc\n"); - cx.simulate_keystrokes("."); - cx.assert_editor_state(" a\n b\n ccˇc\n"); - cx.simulate_keystrokes("v k <"); - cx.assert_editor_state(" a\n bˇ\n ccc\n"); - cx.simulate_keystrokes("."); - cx.assert_editor_state(" a\nbˇ\nccc\n"); -} - -#[perf] -#[gpui::test] -async fn test_escape_command_palette(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("aˇbc\n", Mode::Normal); - cx.simulate_keystrokes("i cmd-shift-p"); - - assert!( - cx.workspace(|workspace, _, cx| workspace.active_modal::(cx).is_some()) - ); - cx.simulate_keystrokes("escape"); - cx.run_until_parked(); - assert!( - !cx.workspace(|workspace, _, cx| workspace.active_modal::(cx).is_some()) - ); - cx.assert_state("aˇbc\n", Mode::Insert); -} - -#[perf] -#[gpui::test] -async fn test_escape_cancels(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("aˇbˇc", Mode::Normal); - cx.simulate_keystrokes("escape"); - - cx.assert_state("aˇbc", Mode::Normal); -} - -#[perf] -#[gpui::test] -async fn test_selection_on_search(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state(indoc! {"aa\nbˇb\ncc\ncc\ncc\n"}, Mode::Normal); - cx.simulate_keystrokes("/ c c"); - - let search_bar = cx.workspace(|workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .toolbar() - .read(cx) - .item_of_type::() - .expect("Buffer search bar should be deployed") - }); - - cx.update_entity(search_bar, |bar, _, cx| { - assert_eq!(bar.query(cx), "cc"); - }); - - cx.update_editor(|editor, window, cx| { - let highlights = editor.all_text_background_highlights(window, cx); - assert_eq!(3, highlights.len()); - assert_eq!( - DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 2), - highlights[0].0 - ) - }); - cx.simulate_keystrokes("enter"); - - cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal); - cx.simulate_keystrokes("n"); - cx.assert_state(indoc! {"aa\nbb\ncc\nˇcc\ncc\n"}, Mode::Normal); - cx.simulate_keystrokes("shift-n"); - cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal); -} - -#[perf] -#[gpui::test] -async fn test_word_characters(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new_typescript(cx).await; - cx.set_state( - indoc! { " - class A { - #ˇgoop = 99; - $ˇgoop () { return this.#gˇoop }; - }; - console.log(new A().$gooˇp()) - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("v i w"); - cx.assert_state( - indoc! {" - class A { - «#goopˇ» = 99; - «$goopˇ» () { return this.«#goopˇ» }; - }; - console.log(new A().«$goopˇ»()) - "}, - Mode::Visual, - ) -} - -#[perf] -#[gpui::test] -async fn test_kebab_case(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new_html(cx).await; - cx.set_state( - indoc! { r#" -
- "#}, - Mode::Normal, - ); - cx.simulate_keystrokes("v i w"); - cx.assert_state( - indoc! { r#" -
- "# - }, - Mode::Visual, - ) -} - -#[perf] -#[gpui::test] -async fn test_join_lines(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇone - two - three - four - five - six - "}) - .await; - cx.simulate_shared_keystrokes("shift-j").await; - cx.shared_state().await.assert_eq(indoc! {" - oneˇ two - three - four - five - six - "}); - cx.simulate_shared_keystrokes("3 shift-j").await; - cx.shared_state().await.assert_eq(indoc! {" - one two threeˇ four - five - six - "}); - - cx.set_shared_state(indoc! {" - ˇone - two - three - four - five - six - "}) - .await; - cx.simulate_shared_keystrokes("j v 3 j shift-j").await; - cx.shared_state().await.assert_eq(indoc! {" - one - two three fourˇ five - six - "}); - - cx.set_shared_state(indoc! {" - ˇone - two - three - four - five - six - "}) - .await; - cx.simulate_shared_keystrokes("g shift-j").await; - cx.shared_state().await.assert_eq(indoc! {" - oneˇtwo - three - four - five - six - "}); - cx.simulate_shared_keystrokes("3 g shift-j").await; - cx.shared_state().await.assert_eq(indoc! {" - onetwothreeˇfour - five - six - "}); - - cx.set_shared_state(indoc! {" - ˇone - two - three - four - five - six - "}) - .await; - cx.simulate_shared_keystrokes("j v 3 j g shift-j").await; - cx.shared_state().await.assert_eq(indoc! {" - one - twothreefourˇfive - six - "}); -} - -#[cfg(target_os = "macos")] -#[perf] -#[gpui::test] -async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_wrap(12).await; - // tests line wrap as follows: - // 1: twelve char - // twelve char - // 2: twelve char - cx.set_shared_state(indoc! { " - tˇwelve char twelve char - twelve char - "}) - .await; - cx.simulate_shared_keystrokes("j").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char twelve char - tˇwelve char - "}); - cx.simulate_shared_keystrokes("k").await; - cx.shared_state().await.assert_eq(indoc! {" - tˇwelve char twelve char - twelve char - "}); - cx.simulate_shared_keystrokes("g j").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char tˇwelve char - twelve char - "}); - cx.simulate_shared_keystrokes("g j").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char twelve char - tˇwelve char - "}); - - cx.simulate_shared_keystrokes("g k").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char tˇwelve char - twelve char - "}); - - cx.simulate_shared_keystrokes("g ^").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char ˇtwelve char - twelve char - "}); - - cx.simulate_shared_keystrokes("^").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇtwelve char twelve char - twelve char - "}); - - cx.simulate_shared_keystrokes("g $").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve charˇ twelve char - twelve char - "}); - cx.simulate_shared_keystrokes("$").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char twelve chaˇr - twelve char - "}); - - cx.set_shared_state(indoc! { " - tˇwelve char twelve char - twelve char - "}) - .await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char twelve char - ˇtwelve char - "}); - - cx.set_shared_state(indoc! { " - twelve char - tˇwelve char twelve char - twelve char - "}) - .await; - cx.simulate_shared_keystrokes("o o escape").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char - twelve char twelve char - ˇo - twelve char - "}); - - cx.set_shared_state(indoc! { " - twelve char - tˇwelve char twelve char - twelve char - "}) - .await; - cx.simulate_shared_keystrokes("shift-a a escape").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char - twelve char twelve charˇa - twelve char - "}); - cx.simulate_shared_keystrokes("shift-i i escape").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char - ˇitwelve char twelve chara - twelve char - "}); - cx.simulate_shared_keystrokes("shift-d").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char - ˇ - twelve char - "}); - - cx.set_shared_state(indoc! { " - twelve char - twelve char tˇwelve char - twelve char - "}) - .await; - cx.simulate_shared_keystrokes("shift-o o escape").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char - ˇo - twelve char twelve char - twelve char - "}); - - // line wraps as: - // fourteen ch - // ar - // fourteen ch - // ar - cx.set_shared_state(indoc! { " - fourteen chaˇr - fourteen char - "}) - .await; - - cx.simulate_shared_keystrokes("d i w").await; - cx.shared_state().await.assert_eq(indoc! {" - fourteenˇ• - fourteen char - "}); - cx.simulate_shared_keystrokes("j shift-f e f r").await; - cx.shared_state().await.assert_eq(indoc! {" - fourteen• - fourteen chaˇr - "}); -} - -#[perf] -#[gpui::test] -async fn test_folds(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_neovim_option("foldmethod=manual").await; - - cx.set_shared_state(indoc! { " - fn boop() { - ˇbarp() - bazp() - } - "}) - .await; - cx.simulate_shared_keystrokes("shift-v j z f").await; - - // visual display is now: - // fn boop () { - // [FOLDED] - // } - - // TODO: this should not be needed but currently zf does not - // return to normal mode. - cx.simulate_shared_keystrokes("escape").await; - - // skip over fold downward - cx.simulate_shared_keystrokes("g g").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇfn boop() { - barp() - bazp() - } - "}); - - cx.simulate_shared_keystrokes("j j").await; - cx.shared_state().await.assert_eq(indoc! {" - fn boop() { - barp() - bazp() - ˇ} - "}); - - // skip over fold upward - cx.simulate_shared_keystrokes("2 k").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇfn boop() { - barp() - bazp() - } - "}); - - // yank the fold - cx.simulate_shared_keystrokes("down y y").await; - cx.shared_clipboard() - .await - .assert_eq(" barp()\n bazp()\n"); - - // re-open - cx.simulate_shared_keystrokes("z o").await; - cx.shared_state().await.assert_eq(indoc! {" - fn boop() { - ˇ barp() - bazp() - } - "}); -} - -#[perf] -#[gpui::test] -async fn test_folds_panic(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_neovim_option("foldmethod=manual").await; - - cx.set_shared_state(indoc! { " - fn boop() { - ˇbarp() - bazp() - } - "}) - .await; - cx.simulate_shared_keystrokes("shift-v j z f").await; - cx.simulate_shared_keystrokes("escape").await; - cx.simulate_shared_keystrokes("g g").await; - cx.simulate_shared_keystrokes("5 d j").await; - cx.shared_state().await.assert_eq("ˇ"); - cx.set_shared_state(indoc! {" - fn boop() { - ˇbarp() - bazp() - } - "}) - .await; - cx.simulate_shared_keystrokes("shift-v j j z f").await; - cx.simulate_shared_keystrokes("escape").await; - cx.simulate_shared_keystrokes("shift-g shift-v").await; - cx.shared_state().await.assert_eq(indoc! {" - fn boop() { - barp() - bazp() - } - ˇ"}); -} - -#[perf] -#[gpui::test] -async fn test_clear_counts(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - The quick brown - fox juˇmps over - the lazy dog"}) - .await; - - cx.simulate_shared_keystrokes("4 escape 3 d l").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox juˇ over - the lazy dog"}); -} - -#[perf] -#[gpui::test] -async fn test_zero(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - The quˇick brown - fox jumps over - the lazy dog"}) - .await; - - cx.simulate_shared_keystrokes("0").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇThe quick brown - fox jumps over - the lazy dog"}); - - cx.simulate_shared_keystrokes("1 0 l").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick ˇbrown - fox jumps over - the lazy dog"}); -} - -#[perf] -#[gpui::test] -async fn test_selection_goal(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ;;ˇ; - Lorem Ipsum"}) - .await; - - cx.simulate_shared_keystrokes("a down up ; down up").await; - cx.shared_state().await.assert_eq(indoc! {" - ;;;;ˇ - Lorem Ipsum"}); -} - -#[cfg(target_os = "macos")] -#[perf] -#[gpui::test] -async fn test_wrapped_motions(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_wrap(12).await; - - cx.set_shared_state(indoc! {" - aaˇaa - 😃😃" - }) - .await; - cx.simulate_shared_keystrokes("j").await; - cx.shared_state().await.assert_eq(indoc! {" - aaaa - 😃ˇ😃" - }); - - cx.set_shared_state(indoc! {" - 123456789012aaˇaa - 123456789012😃😃" - }) - .await; - cx.simulate_shared_keystrokes("j").await; - cx.shared_state().await.assert_eq(indoc! {" - 123456789012aaaa - 123456789012😃ˇ😃" - }); - - cx.set_shared_state(indoc! {" - 123456789012aaˇaa - 123456789012😃😃" - }) - .await; - cx.simulate_shared_keystrokes("j").await; - cx.shared_state().await.assert_eq(indoc! {" - 123456789012aaaa - 123456789012😃ˇ😃" - }); - - cx.set_shared_state(indoc! {" - 123456789012aaaaˇaaaaaaaa123456789012 - wow - 123456789012😃😃😃😃😃😃123456789012" - }) - .await; - cx.simulate_shared_keystrokes("j j").await; - cx.shared_state().await.assert_eq(indoc! {" - 123456789012aaaaaaaaaaaa123456789012 - wow - 123456789012😃😃ˇ😃😃😃😃123456789012" - }); -} - -#[perf] -#[gpui::test] -async fn test_wrapped_delete_end_document(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_wrap(12).await; - - cx.set_shared_state(indoc! {" - aaˇaaaaaaaaaaaaaaaaaa - bbbbbbbbbbbbbbbbbbbb - cccccccccccccccccccc" - }) - .await; - cx.simulate_shared_keystrokes("d shift-g i z z z").await; - cx.shared_state().await.assert_eq(indoc! {" - zzzˇ" - }); -} - -#[perf] -#[gpui::test] -async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - one - ˇ - two"}) - .await; - - cx.simulate_shared_keystrokes("} }").await; - cx.shared_state().await.assert_eq(indoc! {" - one - - twˇo"}); - - cx.simulate_shared_keystrokes("{ { {").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇone - - two"}); -} - -#[perf] -#[gpui::test] -async fn test_select_all_issue_2170(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - defmodule Test do - def test(a, ˇ[_, _] = b), do: IO.puts('hi') - end - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("g a"); - cx.assert_state( - indoc! {" - defmodule Test do - def test(a, «[ˇ»_, _] = b), do: IO.puts('hi') - end - "}, - Mode::Visual, - ); -} - -#[perf] -#[gpui::test] -async fn test_jk(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "j k", - NormalBefore, - Some("vim_mode == insert"), - )]) - }); - cx.neovim.exec("imap jk ").await; - - cx.set_shared_state("ˇhello").await; - cx.simulate_shared_keystrokes("i j o j k").await; - cx.shared_state().await.assert_eq("jˇohello"); -} - -fn assert_pending_input(cx: &mut VimTestContext, expected: &str) { - cx.update_editor(|editor, window, cx| { - let snapshot = editor.snapshot(window, cx); - let highlights = editor - .text_highlights::(cx) - .unwrap() - .1; - let (_, ranges) = marked_text_ranges(expected, false); - - assert_eq!( - highlights - .iter() - .map(|highlight| highlight.to_offset(&snapshot.buffer_snapshot())) - .collect::>(), - ranges - .iter() - .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)) - .collect::>() - ) - }); -} - -#[perf] -#[gpui::test] -async fn test_jk_multi(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "j k l", - NormalBefore, - Some("vim_mode == insert"), - )]) - }); - - cx.set_state("ˇone ˇone ˇone", Mode::Normal); - cx.simulate_keystrokes("i j"); - cx.simulate_keystrokes("k"); - cx.assert_state("ˇjkone ˇjkone ˇjkone", Mode::Insert); - assert_pending_input(&mut cx, "«jk»one «jk»one «jk»one"); - cx.simulate_keystrokes("o j k"); - cx.assert_state("jkoˇjkone jkoˇjkone jkoˇjkone", Mode::Insert); - assert_pending_input(&mut cx, "jko«jk»one jko«jk»one jko«jk»one"); - cx.simulate_keystrokes("l"); - cx.assert_state("jkˇoone jkˇoone jkˇoone", Mode::Normal); -} - -#[perf] -#[gpui::test] -async fn test_jk_delay(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "j k", - NormalBefore, - Some("vim_mode == insert"), - )]) - }); - - cx.set_state("ˇhello", Mode::Normal); - cx.simulate_keystrokes("i j"); - cx.executor().advance_clock(Duration::from_millis(500)); - cx.run_until_parked(); - cx.assert_state("ˇjhello", Mode::Insert); - cx.update_editor(|editor, window, cx| { - let snapshot = editor.snapshot(window, cx); - let highlights = editor - .text_highlights::(cx) - .unwrap() - .1; - - assert_eq!( - highlights - .iter() - .map(|highlight| highlight.to_offset(&snapshot.buffer_snapshot())) - .collect::>(), - vec![MultiBufferOffset(0)..MultiBufferOffset(1)] - ) - }); - cx.executor().advance_clock(Duration::from_millis(500)); - cx.run_until_parked(); - cx.assert_state("jˇhello", Mode::Insert); - cx.simulate_keystrokes("k j k"); - cx.assert_state("jˇkhello", Mode::Normal); -} - -#[perf] -#[gpui::test] -async fn test_jk_max_count(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("1\nˇ2\n3").await; - cx.simulate_shared_keystrokes("9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 j") - .await; - cx.shared_state().await.assert_eq("1\n2\nˇ3"); - - let number: String = usize::MAX.to_string().split("").join(" "); - cx.simulate_shared_keystrokes(&format!("{number} k")).await; - cx.shared_state().await.assert_eq("ˇ1\n2\n3"); -} - -#[perf] -#[gpui::test] -async fn test_comma_w(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - ", w", - motion::Down { - display_lines: false, - }, - Some("vim_mode == normal"), - )]) - }); - cx.neovim.exec("map ,w j").await; - - cx.set_shared_state("ˇhello hello\nhello hello").await; - cx.simulate_shared_keystrokes("f o ; , w").await; - cx.shared_state() - .await - .assert_eq("hello hello\nhello hellˇo"); - - cx.set_shared_state("ˇhello hello\nhello hello").await; - cx.simulate_shared_keystrokes("f o ; , i").await; - cx.shared_state() - .await - .assert_eq("hellˇo hello\nhello hello"); -} - -#[perf] -#[gpui::test] -async fn test_completion_menu_scroll_aside(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new_typescript(cx).await; - - cx.lsp - .set_request_handler::(move |_, _| async move { - Ok(Some(lsp::CompletionResponse::Array(vec![ - lsp::CompletionItem { - label: "Test Item".to_string(), - documentation: Some(lsp::Documentation::String( - "This is some very long documentation content that will be displayed in the aside panel for scrolling.\n".repeat(50) - )), - ..Default::default() - }, - ]))) - }); - - cx.set_state("variableˇ", Mode::Insert); - cx.simulate_keystroke("."); - cx.executor().run_until_parked(); - - let mut initial_offset: Pixels = px(0.0); - - cx.update_editor(|editor, _, _| { - let binding = editor.context_menu().borrow(); - let Some(CodeContextMenu::Completions(menu)) = binding.as_ref() else { - panic!("Should have completions menu open"); - }; - - initial_offset = menu.scroll_handle_aside.offset().y; - }); - - // The `ctrl-e` shortcut should scroll the completion menu's aside content - // down, so the updated offset should be lower than the initial offset. - cx.simulate_keystroke("ctrl-e"); - cx.update_editor(|editor, _, _| { - let binding = editor.context_menu().borrow(); - let Some(CodeContextMenu::Completions(menu)) = binding.as_ref() else { - panic!("Should have completions menu open"); - }; - - assert!(menu.scroll_handle_aside.offset().y < initial_offset); - }); - - // The `ctrl-y` shortcut should do the inverse scrolling as `ctrl-e`, so the - // offset should now be the same as the initial offset. - cx.simulate_keystroke("ctrl-y"); - cx.update_editor(|editor, _, _| { - let binding = editor.context_menu().borrow(); - let Some(CodeContextMenu::Completions(menu)) = binding.as_ref() else { - panic!("Should have completions menu open"); - }; - - assert_eq!(menu.scroll_handle_aside.offset().y, initial_offset); - }); - - // The `ctrl-d` shortcut should scroll the completion menu's aside content - // down, so the updated offset should be lower than the initial offset. - cx.simulate_keystroke("ctrl-d"); - cx.update_editor(|editor, _, _| { - let binding = editor.context_menu().borrow(); - let Some(CodeContextMenu::Completions(menu)) = binding.as_ref() else { - panic!("Should have completions menu open"); - }; - - assert!(menu.scroll_handle_aside.offset().y < initial_offset); - }); - - // The `ctrl-u` shortcut should do the inverse scrolling as `ctrl-u`, so the - // offset should now be the same as the initial offset. - cx.simulate_keystroke("ctrl-u"); - cx.update_editor(|editor, _, _| { - let binding = editor.context_menu().borrow(); - let Some(CodeContextMenu::Completions(menu)) = binding.as_ref() else { - panic!("Should have completions menu open"); - }; - - assert_eq!(menu.scroll_handle_aside.offset().y, initial_offset); - }); -} - -#[perf] -#[gpui::test] -async fn test_rename(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new_typescript(cx).await; - - cx.set_state("const beˇfore = 2; console.log(before)", Mode::Normal); - let def_range = cx.lsp_range("const «beforeˇ» = 2; console.log(before)"); - let tgt_range = cx.lsp_range("const before = 2; console.log(«beforeˇ»)"); - let mut prepare_request = cx.set_request_handler::( - move |_, _, _| async move { Ok(Some(lsp::PrepareRenameResponse::Range(def_range))) }, - ); - let mut rename_request = - cx.set_request_handler::(move |url, params, _| async move { - Ok(Some(lsp::WorkspaceEdit { - changes: Some( - [( - url.clone(), - vec![ - lsp::TextEdit::new(def_range, params.new_name.clone()), - lsp::TextEdit::new(tgt_range, params.new_name), - ], - )] - .into(), - ), - ..Default::default() - })) - }); - - cx.simulate_keystrokes("c d"); - prepare_request.next().await.unwrap(); - cx.simulate_input("after"); - cx.simulate_keystrokes("enter"); - rename_request.next().await.unwrap(); - cx.assert_state("const afterˇ = 2; console.log(after)", Mode::Normal) -} - -#[gpui::test] -async fn test_go_to_definition(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new_typescript(cx).await; - - cx.set_state("const before = 2; console.log(beforˇe)", Mode::Normal); - let def_range = cx.lsp_range("const «beforeˇ» = 2; console.log(before)"); - let mut go_to_request = - cx.set_request_handler::(move |url, _, _| async move { - Ok(Some(lsp::GotoDefinitionResponse::Scalar( - lsp::Location::new(url.clone(), def_range), - ))) - }); - - cx.simulate_keystrokes("g d"); - go_to_request.next().await.unwrap(); - cx.run_until_parked(); - - cx.assert_state("const ˇbefore = 2; console.log(before)", Mode::Normal); -} - -#[perf] -#[gpui::test] -async fn test_remap(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // test moving the cursor - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "g z", - workspace::SendKeystrokes("l l l l".to_string()), - None, - )]) - }); - cx.set_state("ˇ123456789", Mode::Normal); - cx.simulate_keystrokes("g z"); - cx.assert_state("1234ˇ56789", Mode::Normal); - - // test switching modes - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "g y", - workspace::SendKeystrokes("i f o o escape l".to_string()), - None, - )]) - }); - cx.set_state("ˇ123456789", Mode::Normal); - cx.simulate_keystrokes("g y"); - cx.assert_state("fooˇ123456789", Mode::Normal); - - // test recursion - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "g x", - workspace::SendKeystrokes("g z g y".to_string()), - None, - )]) - }); - cx.set_state("ˇ123456789", Mode::Normal); - cx.simulate_keystrokes("g x"); - cx.assert_state("1234fooˇ56789", Mode::Normal); - - // test command - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "g w", - workspace::SendKeystrokes(": j enter".to_string()), - None, - )]) - }); - cx.set_state("ˇ1234\n56789", Mode::Normal); - cx.simulate_keystrokes("g w"); - cx.assert_state("1234ˇ 56789", Mode::Normal); - - // test leaving command - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "g u", - workspace::SendKeystrokes("g w g z".to_string()), - None, - )]) - }); - cx.set_state("ˇ1234\n56789", Mode::Normal); - cx.simulate_keystrokes("g u"); - cx.assert_state("1234 567ˇ89", Mode::Normal); - - // test leaving command - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "g t", - workspace::SendKeystrokes("i space escape".to_string()), - None, - )]) - }); - cx.set_state("12ˇ34", Mode::Normal); - cx.simulate_keystrokes("g t"); - cx.assert_state("12ˇ 34", Mode::Normal); -} - -#[perf] -#[gpui::test] -async fn test_undo(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("hello quˇoel world").await; - cx.simulate_shared_keystrokes("v i w s c o escape u").await; - cx.shared_state().await.assert_eq("hello ˇquoel world"); - cx.simulate_shared_keystrokes("ctrl-r").await; - cx.shared_state().await.assert_eq("hello ˇco world"); - cx.simulate_shared_keystrokes("a o right l escape").await; - cx.shared_state().await.assert_eq("hello cooˇl world"); - cx.simulate_shared_keystrokes("u").await; - cx.shared_state().await.assert_eq("hello cooˇ world"); - cx.simulate_shared_keystrokes("u").await; - cx.shared_state().await.assert_eq("hello cˇo world"); - cx.simulate_shared_keystrokes("u").await; - cx.shared_state().await.assert_eq("hello ˇquoel world"); - - cx.set_shared_state("hello quˇoel world").await; - cx.simulate_shared_keystrokes("v i w ~ u").await; - cx.shared_state().await.assert_eq("hello ˇquoel world"); - - cx.set_shared_state("\nhello quˇoel world\n").await; - cx.simulate_shared_keystrokes("shift-v s c escape u").await; - cx.shared_state().await.assert_eq("\nˇhello quoel world\n"); - - cx.set_shared_state(indoc! {" - ˇ1 - 2 - 3"}) - .await; - - cx.simulate_shared_keystrokes("ctrl-v shift-g ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇ2 - 3 - 4"}); - - cx.simulate_shared_keystrokes("u").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇ1 - 2 - 3"}); -} - -#[perf] -#[gpui::test] -async fn test_mouse_selection(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇone two three", Mode::Normal); - - let start_point = cx.pixel_position("one twˇo three"); - let end_point = cx.pixel_position("one ˇtwo three"); - - cx.simulate_mouse_down(start_point, MouseButton::Left, Modifiers::none()); - cx.simulate_mouse_move(end_point, MouseButton::Left, Modifiers::none()); - cx.simulate_mouse_up(end_point, MouseButton::Left, Modifiers::none()); - - cx.assert_state("one «ˇtwo» three", Mode::Visual) -} - -#[perf] -#[gpui::test] -async fn test_lowercase_marks(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("line one\nline ˇtwo\nline three").await; - cx.simulate_shared_keystrokes("m a l ' a").await; - cx.shared_state() - .await - .assert_eq("line one\nˇline two\nline three"); - cx.simulate_shared_keystrokes("` a").await; - cx.shared_state() - .await - .assert_eq("line one\nline ˇtwo\nline three"); - - cx.simulate_shared_keystrokes("^ d ` a").await; - cx.shared_state() - .await - .assert_eq("line one\nˇtwo\nline three"); -} - -#[perf] -#[gpui::test] -async fn test_lt_gt_marks(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc!( - " - Line one - Line two - Line ˇthree - Line four - Line five - " - )) - .await; - - cx.simulate_shared_keystrokes("v j escape k k").await; - - cx.simulate_shared_keystrokes("' <").await; - cx.shared_state().await.assert_eq(indoc! {" - Line one - Line two - ˇLine three - Line four - Line five - "}); - - cx.simulate_shared_keystrokes("` <").await; - cx.shared_state().await.assert_eq(indoc! {" - Line one - Line two - Line ˇthree - Line four - Line five - "}); - - cx.simulate_shared_keystrokes("' >").await; - cx.shared_state().await.assert_eq(indoc! {" - Line one - Line two - Line three - ˇLine four - Line five - " - }); - - cx.simulate_shared_keystrokes("` >").await; - cx.shared_state().await.assert_eq(indoc! {" - Line one - Line two - Line three - Line ˇfour - Line five - " - }); - - cx.simulate_shared_keystrokes("v i w o escape").await; - cx.simulate_shared_keystrokes("` >").await; - cx.shared_state().await.assert_eq(indoc! {" - Line one - Line two - Line three - Line fouˇr - Line five - " - }); - cx.simulate_shared_keystrokes("` <").await; - cx.shared_state().await.assert_eq(indoc! {" - Line one - Line two - Line three - Line ˇfour - Line five - " - }); -} - -#[perf] -#[gpui::test] -async fn test_caret_mark(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc!( - " - Line one - Line two - Line three - ˇLine four - Line five - " - )) - .await; - - cx.simulate_shared_keystrokes("c w shift-s t r a i g h t space t h i n g escape j j") - .await; - - cx.simulate_shared_keystrokes("' ^").await; - cx.shared_state().await.assert_eq(indoc! {" - Line one - Line two - Line three - ˇStraight thing four - Line five - " - }); - - cx.simulate_shared_keystrokes("` ^").await; - cx.shared_state().await.assert_eq(indoc! {" - Line one - Line two - Line three - Straight thingˇ four - Line five - " - }); - - cx.simulate_shared_keystrokes("k a ! escape k g i ?").await; - cx.shared_state().await.assert_eq(indoc! {" - Line one - Line two - Line three!?ˇ - Straight thing four - Line five - " - }); -} - -#[cfg(target_os = "macos")] -#[perf] -#[gpui::test] -async fn test_dw_eol(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_wrap(12).await; - cx.set_shared_state("twelve ˇchar twelve char\ntwelve char") - .await; - cx.simulate_shared_keystrokes("d w").await; - cx.shared_state() - .await - .assert_eq("twelve ˇtwelve char\ntwelve char"); -} - -#[perf] -#[gpui::test] -async fn test_toggle_comments(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - let language = std::sync::Arc::new(language::Language::new( - language::LanguageConfig { - line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()], - ..Default::default() - }, - Some(language::tree_sitter_rust::LANGUAGE.into()), - )); - cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - - // works in normal model - cx.set_state( - indoc! {" - ˇone - two - three - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("g c c"); - cx.assert_state( - indoc! {" - // ˇone - two - three - "}, - Mode::Normal, - ); - - // works in visual mode - cx.simulate_keystrokes("v j g c"); - cx.assert_state( - indoc! {" - // // ˇone - // two - three - "}, - Mode::Normal, - ); - - // works in visual line mode - cx.simulate_keystrokes("shift-v j g c"); - cx.assert_state( - indoc! {" - // ˇone - two - three - "}, - Mode::Normal, - ); - - // works with count - cx.simulate_keystrokes("g c 2 j"); - cx.assert_state( - indoc! {" - // // ˇone - // two - // three - "}, - Mode::Normal, - ); - - // works with motion object - cx.simulate_keystrokes("shift-g"); - cx.simulate_keystrokes("g c g g"); - cx.assert_state( - indoc! {" - // one - two - three - ˇ"}, - Mode::Normal, - ); -} - -#[perf] -#[gpui::test] -async fn test_find_multibyte(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(r#""#) - .await; - - cx.simulate_shared_keystrokes("c t < o escape").await; - cx.shared_state() - .await - .assert_eq(r#""#); -} - -#[perf] -#[gpui::test] -async fn test_sneak(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.update(|_window, cx| { - cx.bind_keys([ - KeyBinding::new( - "s", - PushSneak { first_char: None }, - Some("vim_mode == normal"), - ), - KeyBinding::new( - "shift-s", - PushSneakBackward { first_char: None }, - Some("vim_mode == normal"), - ), - KeyBinding::new( - "shift-s", - PushSneakBackward { first_char: None }, - Some("vim_mode == visual"), - ), - ]) - }); - - // Sneak forwards multibyte & multiline - cx.set_state( - indoc! { - r#" - Počet hostů - "# - }, - Mode::Normal, - ); - cx.simulate_keystrokes("s t ů"); - cx.assert_state( - indoc! { - r#""# - }, - Mode::Normal, - ); - - // Visual sneak backwards multibyte & multiline - cx.simulate_keystrokes("v S < l"); - cx.assert_state( - indoc! { - r#"«ˇ"# - }, - Mode::Visual, - ); - - // Sneak backwards repeated - cx.set_state(r#"11 12 13 ˇ14"#, Mode::Normal); - cx.simulate_keystrokes("S space 1"); - cx.assert_state(r#"11 12ˇ 13 14"#, Mode::Normal); - cx.simulate_keystrokes(";"); - cx.assert_state(r#"11ˇ 12 13 14"#, Mode::Normal); -} - -#[perf] -#[gpui::test] -async fn test_plus_minus(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "one - two - thrˇee - "}) - .await; - - cx.simulate_shared_keystrokes("-").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("-").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("+").await; - cx.shared_state().await.assert_matches(); -} - -#[perf] -#[gpui::test] -async fn test_command_alias(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - let mut aliases = HashMap::default(); - aliases.insert("Q".to_string(), "upper".to_string()); - s.workspace.command_aliases = aliases - }); - }); - - cx.set_state("ˇhello world", Mode::Normal); - cx.simulate_keystrokes(": Q"); - cx.set_state("ˇHello world", Mode::Normal); -} - -#[perf] -#[gpui::test] -async fn test_remap_adjacent_dog_cat(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.update(|_, cx| { - cx.bind_keys([ - KeyBinding::new( - "d o g", - workspace::SendKeystrokes("🐶".to_string()), - Some("vim_mode == insert"), - ), - KeyBinding::new( - "c a t", - workspace::SendKeystrokes("🐱".to_string()), - Some("vim_mode == insert"), - ), - ]) - }); - cx.neovim.exec("imap dog 🐶").await; - cx.neovim.exec("imap cat 🐱").await; - - cx.set_shared_state("ˇ").await; - cx.simulate_shared_keystrokes("i d o g").await; - cx.shared_state().await.assert_eq("🐶ˇ"); - - cx.set_shared_state("ˇ").await; - cx.simulate_shared_keystrokes("i d o d o g").await; - cx.shared_state().await.assert_eq("do🐶ˇ"); - - cx.set_shared_state("ˇ").await; - cx.simulate_shared_keystrokes("i d o c a t").await; - cx.shared_state().await.assert_eq("do🐱ˇ"); -} - -#[perf] -#[gpui::test] -async fn test_remap_nested_pineapple(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.update(|_, cx| { - cx.bind_keys([ - KeyBinding::new( - "p i n", - workspace::SendKeystrokes("📌".to_string()), - Some("vim_mode == insert"), - ), - KeyBinding::new( - "p i n e", - workspace::SendKeystrokes("🌲".to_string()), - Some("vim_mode == insert"), - ), - KeyBinding::new( - "p i n e a p p l e", - workspace::SendKeystrokes("🍍".to_string()), - Some("vim_mode == insert"), - ), - ]) - }); - cx.neovim.exec("imap pin 📌").await; - cx.neovim.exec("imap pine 🌲").await; - cx.neovim.exec("imap pineapple 🍍").await; - - cx.set_shared_state("ˇ").await; - cx.simulate_shared_keystrokes("i p i n").await; - cx.executor().advance_clock(Duration::from_millis(1000)); - cx.run_until_parked(); - cx.shared_state().await.assert_eq("📌ˇ"); - - cx.set_shared_state("ˇ").await; - cx.simulate_shared_keystrokes("i p i n e").await; - cx.executor().advance_clock(Duration::from_millis(1000)); - cx.run_until_parked(); - cx.shared_state().await.assert_eq("🌲ˇ"); - - cx.set_shared_state("ˇ").await; - cx.simulate_shared_keystrokes("i p i n e a p p l e").await; - cx.shared_state().await.assert_eq("🍍ˇ"); -} - -#[perf] -#[gpui::test] -async fn test_remap_recursion(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "x", - workspace::SendKeystrokes("\" _ x".to_string()), - Some("VimControl"), - )]); - cx.bind_keys([KeyBinding::new( - "y", - workspace::SendKeystrokes("2 x".to_string()), - Some("VimControl"), - )]) - }); - cx.neovim.exec("noremap x \"_x").await; - cx.neovim.exec("map y 2x").await; - - cx.set_shared_state("ˇhello").await; - cx.simulate_shared_keystrokes("d l").await; - cx.shared_clipboard().await.assert_eq("h"); - cx.simulate_shared_keystrokes("y").await; - cx.shared_clipboard().await.assert_eq("h"); - cx.shared_state().await.assert_eq("ˇlo"); -} - -#[perf] -#[gpui::test] -async fn test_escape_while_waiting(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("ˇhi").await; - cx.simulate_shared_keystrokes("\" + escape x").await; - cx.shared_state().await.assert_eq("ˇi"); -} - -#[perf] -#[gpui::test] -async fn test_ctrl_w_override(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new("ctrl-w", DeleteLine, None)]); - }); - cx.neovim.exec("map D").await; - cx.set_shared_state("ˇhi").await; - cx.simulate_shared_keystrokes("ctrl-w").await; - cx.shared_state().await.assert_eq("ˇ"); -} - -#[perf] -#[gpui::test] -async fn test_visual_indent_count(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.set_state("ˇhi", Mode::Normal); - cx.simulate_keystrokes("shift-v 3 >"); - cx.assert_state(" ˇhi", Mode::Normal); - cx.simulate_keystrokes("shift-v 2 <"); - cx.assert_state(" ˇhi", Mode::Normal); -} - -#[perf] -#[gpui::test] -async fn test_record_replay_recursion(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello world").await; - cx.simulate_shared_keystrokes(">").await; - cx.simulate_shared_keystrokes(".").await; - cx.simulate_shared_keystrokes(".").await; - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("ˇhello world"); -} - -#[perf] -#[gpui::test] -async fn test_blackhole_register(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello world").await; - cx.simulate_shared_keystrokes("d i w \" _ d a w").await; - cx.simulate_shared_keystrokes("p").await; - cx.shared_state().await.assert_eq("hellˇo"); -} - -#[perf] -#[gpui::test] -async fn test_sentence_backwards(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("one\n\ntwo\nthree\nˇ\nfour").await; - cx.simulate_shared_keystrokes("(").await; - cx.shared_state() - .await - .assert_eq("one\n\nˇtwo\nthree\n\nfour"); - - cx.set_shared_state("hello.\n\n\nworˇld.").await; - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq("hello.\n\n\nˇworld."); - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq("hello.\n\nˇ\nworld."); - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq("ˇhello.\n\n\nworld."); - - cx.set_shared_state("hello. worlˇd.").await; - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq("hello. ˇworld."); - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq("ˇhello. world."); - - cx.set_shared_state(". helˇlo.").await; - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq(". ˇhello."); - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq(". ˇhello."); - - cx.set_shared_state(indoc! { - "{ - hello_world(); - ˇ}" - }) - .await; - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇ{ - hello_world(); - }" - }); - - cx.set_shared_state(indoc! { - "Hello! World..? - - \tHello! World... ˇ" - }) - .await; - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq(indoc! { - "Hello! World..? - - \tHello! ˇWorld... " - }); - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq(indoc! { - "Hello! World..? - - \tˇHello! World... " - }); - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq(indoc! { - "Hello! World..? - ˇ - \tHello! World... " - }); - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq(indoc! { - "Hello! ˇWorld..? - - \tHello! World... " - }); -} - -#[perf] -#[gpui::test] -async fn test_sentence_forwards(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("helˇlo.\n\n\nworld.").await; - cx.simulate_shared_keystrokes(")").await; - cx.shared_state().await.assert_eq("hello.\nˇ\n\nworld."); - cx.simulate_shared_keystrokes(")").await; - cx.shared_state().await.assert_eq("hello.\n\n\nˇworld."); - cx.simulate_shared_keystrokes(")").await; - cx.shared_state().await.assert_eq("hello.\n\n\nworldˇ."); - - cx.set_shared_state("helˇlo.\n\n\nworld.").await; -} - -#[perf] -#[gpui::test] -async fn test_ctrl_o_visual(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("helloˇ world.").await; - cx.simulate_shared_keystrokes("i ctrl-o v b r l").await; - cx.shared_state().await.assert_eq("ˇllllllworld."); - cx.simulate_shared_keystrokes("ctrl-o v f w d").await; - cx.shared_state().await.assert_eq("ˇorld."); -} - -#[perf] -#[gpui::test] -async fn test_ctrl_o_position(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("helˇlo world.").await; - cx.simulate_shared_keystrokes("i ctrl-o d i w").await; - cx.shared_state().await.assert_eq("ˇ world."); - cx.simulate_shared_keystrokes("ctrl-o p").await; - cx.shared_state().await.assert_eq(" helloˇworld."); -} - -#[perf] -#[gpui::test] -async fn test_ctrl_o_dot(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("heˇllo world.").await; - cx.simulate_shared_keystrokes("x i ctrl-o .").await; - cx.shared_state().await.assert_eq("heˇo world."); - cx.simulate_shared_keystrokes("l l escape .").await; - cx.shared_state().await.assert_eq("hellˇllo world."); -} - -#[perf(iterations = 1)] -#[gpui::test] -async fn test_folded_multibuffer_excerpts(cx: &mut gpui::TestAppContext) { - VimTestContext::init(cx); - cx.update(|cx| { - VimTestContext::init_keybindings(true, cx); - }); - let (editor, cx) = cx.add_window_view(|window, cx| { - let multi_buffer = MultiBuffer::build_multi( - [ - ("111\n222\n333\n444\n", vec![Point::row_range(0..2)]), - ("aaa\nbbb\nccc\nddd\n", vec![Point::row_range(0..2)]), - ("AAA\nBBB\nCCC\nDDD\n", vec![Point::row_range(0..2)]), - ("one\ntwo\nthr\nfou\n", vec![Point::row_range(0..2)]), - ], - cx, - ); - let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx); - - let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids(); - // fold all but the second buffer, so that we test navigating between two - // adjacent folded buffers, as well as folded buffers at the start and - // end the multibuffer - editor.fold_buffer(buffer_ids[0], cx); - editor.fold_buffer(buffer_ids[2], cx); - editor.fold_buffer(buffer_ids[3], cx); - - editor - }); - let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await; - - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - ˇ[FOLDED] - [EXCERPT] - aaa - bbb - [EXCERPT] - [FOLDED] - [EXCERPT] - [FOLDED] - " - }); - cx.simulate_keystroke("j"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - [FOLDED] - [EXCERPT] - ˇaaa - bbb - [EXCERPT] - [FOLDED] - [EXCERPT] - [FOLDED] - " - }); - cx.simulate_keystroke("j"); - cx.simulate_keystroke("j"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - [FOLDED] - [EXCERPT] - aaa - bbb - ˇ[EXCERPT] - [FOLDED] - [EXCERPT] - [FOLDED] - " - }); - cx.simulate_keystroke("j"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - [FOLDED] - [EXCERPT] - aaa - bbb - [EXCERPT] - ˇ[FOLDED] - [EXCERPT] - [FOLDED] - " - }); - cx.simulate_keystroke("j"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - [FOLDED] - [EXCERPT] - aaa - bbb - [EXCERPT] - [FOLDED] - [EXCERPT] - ˇ[FOLDED] - " - }); - cx.simulate_keystroke("k"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - [FOLDED] - [EXCERPT] - aaa - bbb - [EXCERPT] - ˇ[FOLDED] - [EXCERPT] - [FOLDED] - " - }); - cx.simulate_keystroke("k"); - cx.simulate_keystroke("k"); - cx.simulate_keystroke("k"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - [FOLDED] - [EXCERPT] - ˇaaa - bbb - [EXCERPT] - [FOLDED] - [EXCERPT] - [FOLDED] - " - }); - cx.simulate_keystroke("k"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - ˇ[FOLDED] - [EXCERPT] - aaa - bbb - [EXCERPT] - [FOLDED] - [EXCERPT] - [FOLDED] - " - }); - cx.simulate_keystroke("shift-g"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - [FOLDED] - [EXCERPT] - aaa - bbb - [EXCERPT] - [FOLDED] - [EXCERPT] - ˇ[FOLDED] - " - }); - cx.simulate_keystrokes("g g"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - ˇ[FOLDED] - [EXCERPT] - aaa - bbb - [EXCERPT] - [FOLDED] - [EXCERPT] - [FOLDED] - " - }); - cx.update_editor(|editor, _, cx| { - let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids(); - editor.fold_buffer(buffer_ids[1], cx); - }); - - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - ˇ[FOLDED] - [EXCERPT] - [FOLDED] - [EXCERPT] - [FOLDED] - [EXCERPT] - [FOLDED] - " - }); - cx.simulate_keystrokes("2 j"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - [FOLDED] - [EXCERPT] - [FOLDED] - [EXCERPT] - ˇ[FOLDED] - [EXCERPT] - [FOLDED] - " - }); -} - -#[perf] -#[gpui::test] -async fn test_delete_paragraph_motion(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - "ˇhello world. - - hello world. - " - }) - .await; - cx.simulate_shared_keystrokes("y }").await; - cx.shared_clipboard().await.assert_eq("hello world.\n"); - cx.simulate_shared_keystrokes("d }").await; - cx.shared_state().await.assert_eq("ˇ\nhello world.\n"); - cx.shared_clipboard().await.assert_eq("hello world.\n"); - - cx.set_shared_state(indoc! { - "helˇlo world. - - hello world. - " - }) - .await; - cx.simulate_shared_keystrokes("y }").await; - cx.shared_clipboard().await.assert_eq("lo world."); - cx.simulate_shared_keystrokes("d }").await; - cx.shared_state().await.assert_eq("heˇl\n\nhello world.\n"); - cx.shared_clipboard().await.assert_eq("lo world."); -} - -#[perf] -#[gpui::test] -async fn test_delete_unmatched_brace(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - "fn o(wow: i32) { - othˇ(wow) - oth(wow) - } - " - }) - .await; - cx.simulate_shared_keystrokes("d ] }").await; - cx.shared_state().await.assert_eq(indoc! { - "fn o(wow: i32) { - otˇh - } - " - }); - cx.shared_clipboard().await.assert_eq("(wow)\n oth(wow)"); - cx.set_shared_state(indoc! { - "fn o(wow: i32) { - ˇoth(wow) - oth(wow) - } - " - }) - .await; - cx.simulate_shared_keystrokes("d ] }").await; - cx.shared_state().await.assert_eq(indoc! { - "fn o(wow: i32) { - ˇ} - " - }); - cx.shared_clipboard() - .await - .assert_eq(" oth(wow)\n oth(wow)\n"); -} - -#[perf] -#[gpui::test] -async fn test_paragraph_multi_delete(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - " - Emacs is - ˇa great - - operating system - - all it lacks - is a - - decent text editor - " - }) - .await; - - cx.simulate_shared_keystrokes("2 d a p").await; - cx.shared_state().await.assert_eq(indoc! { - " - ˇall it lacks - is a - - decent text editor - " - }); - - cx.simulate_shared_keystrokes("d a p").await; - cx.shared_clipboard() - .await - .assert_eq("all it lacks\nis a\n\n"); - - //reset to initial state - cx.simulate_shared_keystrokes("2 u").await; - - cx.simulate_shared_keystrokes("4 d a p").await; - cx.shared_state().await.assert_eq(indoc! {"ˇ"}); -} - -#[perf] -#[gpui::test] -async fn test_yank_paragraph_with_paste(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - " - first paragraph - ˇstill first - - second paragraph - still second - - third paragraph - " - }) - .await; - - cx.simulate_shared_keystrokes("y a p").await; - cx.shared_clipboard() - .await - .assert_eq("first paragraph\nstill first\n\n"); - - cx.simulate_shared_keystrokes("j j p").await; - cx.shared_state().await.assert_eq(indoc! { - " - first paragraph - still first - - ˇfirst paragraph - still first - - second paragraph - still second - - third paragraph - " - }); -} - -#[perf] -#[gpui::test] -async fn test_change_paragraph(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - " - first paragraph - ˇstill first - - second paragraph - still second - - third paragraph - " - }) - .await; - - cx.simulate_shared_keystrokes("c a p").await; - cx.shared_clipboard() - .await - .assert_eq("first paragraph\nstill first\n\n"); - - cx.simulate_shared_keystrokes("escape").await; - cx.shared_state().await.assert_eq(indoc! { - " - ˇ - second paragraph - still second - - third paragraph - " - }); -} - -#[perf] -#[gpui::test] -async fn test_multi_cursor_replay(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.set_state( - indoc! { - " - oˇne one one - - two two two - " - }, - Mode::Normal, - ); - - cx.simulate_keystrokes("3 g l s wow escape escape"); - cx.assert_state( - indoc! { - " - woˇw wow wow - - two two two - " - }, - Mode::Normal, - ); - - cx.simulate_keystrokes("2 j 3 g l ."); - cx.assert_state( - indoc! { - " - wow wow wow - - woˇw woˇw woˇw - " - }, - Mode::Normal, - ); -} - -#[gpui::test] -async fn test_clipping_on_mode_change(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! { - " - ˇverylongline - andsomelinebelow - " - }, - Mode::Normal, - ); - - cx.simulate_keystrokes("v e"); - cx.assert_state( - indoc! { - " - «verylonglineˇ» - andsomelinebelow - " - }, - Mode::Visual, - ); - - let mut pixel_position = cx.update_editor(|editor, window, cx| { - let snapshot = editor.snapshot(window, cx); - let current_head = editor - .selections - .newest_display(&snapshot.display_snapshot) - .end; - editor.last_bounds().unwrap().origin - + editor - .display_to_pixel_point(current_head, &snapshot, window, cx) - .unwrap() - }); - pixel_position.x += px(100.); - // click beyond end of the line - cx.simulate_click(pixel_position, Modifiers::default()); - cx.run_until_parked(); - - cx.assert_state( - indoc! { - " - verylonglinˇe - andsomelinebelow - " - }, - Mode::Normal, - ); -} - -#[gpui::test] -async fn test_wrap_selections_in_tag_line_mode(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - let js_language = Arc::new(Language::new( - LanguageConfig { - name: "JavaScript".into(), - wrap_characters: Some(language::WrapCharactersConfig { - start_prefix: "<".into(), - start_suffix: ">".into(), - end_prefix: "".into(), - }), - ..LanguageConfig::default() - }, - None, - )); - - cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx)); - - cx.set_state( - indoc! { - " - ˇaaaaa - bbbbb - " - }, - Mode::Normal, - ); - - cx.simulate_keystrokes("shift-v j"); - cx.dispatch_action(WrapSelectionsInTag); - - cx.assert_state( - indoc! { - " - <ˇ>aaaaa - bbbbb - " - }, - Mode::VisualLine, - ); -} - -#[gpui::test] -async fn test_repeat_grouping_41735(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // typically transaction gropuing is disabled in tests, but here we need to test it. - cx.update_buffer(|buffer, _cx| buffer.set_group_interval(Duration::from_millis(300))); - - cx.set_shared_state("ˇ").await; - - cx.simulate_shared_keystrokes("i a escape").await; - cx.simulate_shared_keystrokes(". . .").await; - cx.shared_state().await.assert_eq("ˇaaaa"); - cx.simulate_shared_keystrokes("u").await; - cx.shared_state().await.assert_eq("ˇaaa"); -} - -#[gpui::test] -async fn test_deactivate(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| { - settings.editor.cursor_shape = Some(settings::CursorShape::Underline); - }); - }); - - // Assert that, while in `Normal` mode, the cursor shape is `Block` but, - // after deactivating vim mode, it should revert to the one specified in the - // user's settings, if set. - cx.update_editor(|editor, _window, _cx| { - assert_eq!(editor.cursor_shape(), CursorShape::Block); - }); - - cx.disable_vim(); - - cx.update_editor(|editor, _window, _cx| { - assert_eq!(editor.cursor_shape(), CursorShape::Underline); - }); -} diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs deleted file mode 100644 index d20464ccc4..0000000000 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ /dev/null @@ -1,427 +0,0 @@ -use gpui::{AppContext as _, UpdateGlobal, px, size}; -use indoc::indoc; -use settings::SettingsStore; -use std::{ - ops::{Deref, DerefMut}, - panic, thread, -}; - -use language::language_settings::SoftWrap; -use util::test::marked_text_offsets; - -use super::{VimTestContext, neovim_connection::NeovimConnection}; -use crate::state::{Mode, VimGlobals}; - -pub struct NeovimBackedTestContext { - pub(crate) cx: VimTestContext, - pub(crate) neovim: NeovimConnection, - - last_set_state: Option, - recent_keystrokes: Vec, -} - -#[derive(Default)] -pub struct SharedState { - neovim: String, - editor: String, - initial: String, - neovim_mode: Mode, - editor_mode: Mode, - recent_keystrokes: String, -} - -impl SharedState { - /// Assert that both Zed and NeoVim have the same content and mode. - #[track_caller] - pub fn assert_matches(&self) { - if self.neovim != self.editor || self.neovim_mode != self.editor_mode { - panic!( - indoc! {"Test failed (zed does not match nvim behavior) - # initial state: - {} - # keystrokes: - {} - # neovim ({}): - {} - # zed ({}): - {}"}, - self.initial, - self.recent_keystrokes, - self.neovim_mode, - self.neovim, - self.editor_mode, - self.editor, - ) - } - } - - #[track_caller] - pub fn assert_eq(&mut self, marked_text: &str) { - let marked_text = marked_text.replace('•', " "); - if self.neovim == marked_text - && self.neovim == self.editor - && self.neovim_mode == self.editor_mode - { - return; - } - - let message = if self.neovim != marked_text { - "Test is incorrect (currently expected != neovim_state)" - } else { - "Editor does not match nvim behavior" - }; - panic!( - indoc! {"{} - # initial state: - {} - # keystrokes: - {} - # currently expected: - {} - # neovim ({}): - {} - # zed ({}): - {}"}, - message, - self.initial, - self.recent_keystrokes, - marked_text.replace(" \n", "•\n"), - self.neovim_mode, - self.neovim.replace(" \n", "•\n"), - self.editor_mode, - self.editor.replace(" \n", "•\n"), - ) - } -} - -pub struct SharedClipboard { - register: char, - neovim: String, - editor: String, - state: SharedState, -} - -impl SharedClipboard { - #[track_caller] - pub fn assert_eq(&self, expected: &str) { - if expected == self.neovim && self.neovim == self.editor { - return; - } - - let message = if expected != self.neovim { - "Test is incorrect (currently expected != neovim_state)" - } else { - "Editor does not match nvim behavior" - }; - - panic!( - indoc! {"{} - # initial state: - {} - # keystrokes: - {} - # currently expected: {:?} - # neovim register \"{}: {:?} - # zed register \"{}: {:?}"}, - message, - self.state.initial, - self.state.recent_keystrokes, - expected, - self.register, - self.neovim, - self.register, - self.editor - ) - } -} - -impl NeovimBackedTestContext { - pub async fn new(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext { - #[cfg(feature = "neovim")] - cx.executor().allow_parking(); - // rust stores the name of the test on the current thread. - // We use this to automatically name a file that will store - // the neovim connection's requests/responses so that we can - // run without neovim on CI. - let thread = thread::current(); - let test_name = thread - .name() - .expect("thread is not named") - .split(':') - .next_back() - .unwrap() - .to_string(); - Self { - cx: VimTestContext::new(cx, true).await, - neovim: NeovimConnection::new(test_name).await, - - last_set_state: None, - recent_keystrokes: Default::default(), - } - } - - pub async fn new_html(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext { - #[cfg(feature = "neovim")] - cx.executor().allow_parking(); - // rust stores the name of the test on the current thread. - // We use this to automatically name a file that will store - // the neovim connection's requests/responses so that we can - // run without neovim on CI. - let thread = thread::current(); - let test_name = thread - .name() - .expect("thread is not named") - .split(':') - .next_back() - .unwrap() - .to_string(); - Self { - cx: VimTestContext::new_html(cx).await, - neovim: NeovimConnection::new(test_name).await, - - last_set_state: None, - recent_keystrokes: Default::default(), - } - } - - pub async fn new_markdown_with_rust(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext { - #[cfg(feature = "neovim")] - cx.executor().allow_parking(); - let thread = thread::current(); - let test_name = thread - .name() - .expect("thread is not named") - .split(':') - .next_back() - .unwrap() - .to_string(); - Self { - cx: VimTestContext::new_markdown_with_rust(cx).await, - neovim: NeovimConnection::new(test_name).await, - - last_set_state: None, - recent_keystrokes: Default::default(), - } - } - - pub async fn new_typescript(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext { - #[cfg(feature = "neovim")] - cx.executor().allow_parking(); - // rust stores the name of the test on the current thread. - // We use this to automatically name a file that will store - // the neovim connection's requests/responses so that we can - // run without neovim on CI. - let thread = thread::current(); - let test_name = thread - .name() - .expect("thread is not named") - .split(':') - .next_back() - .unwrap() - .to_string(); - Self { - cx: VimTestContext::new_typescript(cx).await, - neovim: NeovimConnection::new(test_name).await, - - last_set_state: None, - recent_keystrokes: Default::default(), - } - } - - pub async fn new_tsx(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext { - #[cfg(feature = "neovim")] - cx.executor().allow_parking(); - let thread = thread::current(); - let test_name = thread - .name() - .expect("thread is not named") - .split(':') - .next_back() - .unwrap() - .to_string(); - Self { - cx: VimTestContext::new_tsx(cx).await, - neovim: NeovimConnection::new(test_name).await, - - last_set_state: None, - recent_keystrokes: Default::default(), - } - } - - pub async fn set_shared_state(&mut self, marked_text: &str) { - let mode = if marked_text.contains('»') { - Mode::Visual - } else { - Mode::Normal - }; - self.set_state(marked_text, mode); - self.last_set_state = Some(marked_text.to_string()); - self.recent_keystrokes = Vec::new(); - self.neovim.set_state(marked_text).await; - } - - pub async fn simulate_shared_keystrokes(&mut self, keystroke_texts: &str) { - for keystroke_text in keystroke_texts.split(' ') { - self.recent_keystrokes.push(keystroke_text.to_string()); - self.neovim.send_keystroke(keystroke_text).await; - } - self.simulate_keystrokes(keystroke_texts); - } - - #[must_use] - pub async fn simulate(&mut self, keystrokes: &str, initial_state: &str) -> SharedState { - self.set_shared_state(initial_state).await; - self.simulate_shared_keystrokes(keystrokes).await; - self.shared_state().await - } - - pub async fn set_shared_wrap(&mut self, columns: u32) { - if columns < 12 { - panic!("nvim doesn't support columns < 12") - } - self.neovim.set_option("wrap").await; - self.neovim - .set_option(&format!("columns={}", columns)) - .await; - - self.update(|_, cx| { - SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings(cx, |settings| { - settings.project.all_languages.defaults.soft_wrap = - Some(SoftWrap::PreferredLineLength); - settings - .project - .all_languages - .defaults - .preferred_line_length = Some(columns); - }); - }) - }) - } - - pub async fn set_scroll_height(&mut self, rows: u32) { - // match Zed's scrolling behavior - self.neovim.set_option(&format!("scrolloff={}", 3)).await; - // +2 to account for the vim command UI at the bottom. - self.neovim.set_option(&format!("lines={}", rows + 2)).await; - let (line_height, visible_line_count) = self.update_editor(|editor, window, cx| { - ( - editor - .style(cx) - .text - .line_height_in_pixels(window.rem_size()), - editor.visible_line_count().unwrap(), - ) - }); - - let window = self.window; - let margin = self - .update_window(window, |_, window, _cx| { - window.viewport_size().height - line_height * (visible_line_count as f32) - }) - .unwrap(); - - self.simulate_window_resize( - self.window, - size(px(1000.), margin + (rows as f32) * line_height), - ); - } - - pub async fn set_neovim_option(&mut self, option: &str) { - self.neovim.set_option(option).await; - } - - #[must_use] - pub async fn shared_clipboard(&mut self) -> SharedClipboard { - SharedClipboard { - register: '"', - state: self.shared_state().await, - neovim: self.neovim.read_register('"').await, - editor: self.read_from_clipboard().unwrap().text().unwrap(), - } - } - - #[must_use] - pub async fn shared_register(&mut self, register: char) -> SharedClipboard { - SharedClipboard { - register, - state: self.shared_state().await, - neovim: self.neovim.read_register(register).await, - editor: self.update(|_, cx| { - cx.global::() - .registers - .get(®ister) - .cloned() - .unwrap_or_default() - .text - .into() - }), - } - } - - #[must_use] - pub async fn shared_state(&mut self) -> SharedState { - let (mode, marked_text) = self.neovim.state().await; - SharedState { - neovim: marked_text, - neovim_mode: mode, - editor: self.editor_state(), - editor_mode: self.mode(), - initial: self - .last_set_state - .as_ref() - .cloned() - .unwrap_or("N/A".to_string()), - recent_keystrokes: self.recent_keystrokes.join(" "), - } - } - - #[must_use] - pub async fn simulate_at_each_offset( - &mut self, - keystrokes: &str, - marked_positions: &str, - ) -> SharedState { - let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions); - - for cursor_offset in cursor_offsets.iter() { - let mut marked_text = unmarked_text.clone(); - marked_text.insert(*cursor_offset, 'ˇ'); - - let state = self.simulate(keystrokes, &marked_text).await; - if state.neovim != state.editor || state.neovim_mode != state.editor_mode { - return state; - } - } - - SharedState::default() - } -} - -impl Deref for NeovimBackedTestContext { - type Target = VimTestContext; - - fn deref(&self) -> &Self::Target { - &self.cx - } -} - -impl DerefMut for NeovimBackedTestContext { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.cx - } -} - -#[cfg(test)] -mod test { - use crate::test::NeovimBackedTestContext; - use gpui::TestAppContext; - - #[gpui::test] - async fn neovim_backed_test_context_works(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.shared_state().await.assert_matches(); - cx.set_shared_state("This is a tesˇt").await; - cx.shared_state().await.assert_matches(); - } -} diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs deleted file mode 100644 index dbc4068507..0000000000 --- a/crates/vim/src/test/neovim_connection.rs +++ /dev/null @@ -1,624 +0,0 @@ -use std::path::PathBuf; -#[cfg(feature = "neovim")] -use std::{ - cmp, - ops::{Deref, DerefMut, Range}, -}; - -#[cfg(feature = "neovim")] -use async_compat::Compat; -#[cfg(feature = "neovim")] -use async_trait::async_trait; -#[cfg(feature = "neovim")] -use gpui::Keystroke; - -#[cfg(feature = "neovim")] -use language::Point; - -#[cfg(feature = "neovim")] -use nvim_rs::{ - Handler, Neovim, UiAttachOptions, Value, create::tokio::new_child_cmd, error::LoopError, -}; -#[cfg(feature = "neovim")] -use parking_lot::ReentrantMutex; -use serde::{Deserialize, Serialize}; -#[cfg(feature = "neovim")] -use tokio::{ - process::{Child, ChildStdin, Command}, - task::JoinHandle, -}; - -use crate::state::Mode; -use collections::VecDeque; - -// Neovim doesn't like to be started simultaneously from multiple threads. We use this lock -// to ensure we are only constructing one neovim connection at a time. -#[cfg(feature = "neovim")] -static NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(()); - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub enum NeovimData { - Put { state: String }, - Key(String), - Get { state: String, mode: Mode }, - ReadRegister { name: char, value: String }, - Exec { command: String }, - SetOption { value: String }, -} - -pub struct NeovimConnection { - data: VecDeque, - #[cfg(feature = "neovim")] - test_case_id: String, - #[cfg(feature = "neovim")] - nvim: Neovim>, - #[cfg(feature = "neovim")] - _join_handle: JoinHandle>>, - #[cfg(feature = "neovim")] - _child: Child, -} - -impl NeovimConnection { - pub async fn new(mut test_case_id: String) -> Self { - // When running under perf, don't create duplicate files. - if cfg!(perf_enabled) { - if test_case_id.ends_with(perf::consts::SUF_NORMAL) { - test_case_id.truncate(test_case_id.len() - perf::consts::SUF_NORMAL.len()); - } - } - #[cfg(feature = "neovim")] - let handler = NvimHandler {}; - #[cfg(feature = "neovim")] - let (nvim, join_handle, child) = Compat::new(async { - // Ensure we don't create neovim connections in parallel - let _lock = NEOVIM_LOCK.lock(); - let (nvim, join_handle, child) = new_child_cmd( - Command::new("nvim") - .arg("--embed") - .arg("--clean") - // disable swap (otherwise after about 1000 test runs you run out of swap file names) - .arg("-n") - // disable writing files (just in case) - .arg("-m"), - handler, - ) - .await - .expect("Could not connect to neovim process"); - - nvim.ui_attach(100, 100, &UiAttachOptions::default()) - .await - .expect("Could not attach to ui"); - - // Makes system act a little more like zed in terms of indentation - nvim.set_option("smartindent", nvim_rs::Value::Boolean(true)) - .await - .expect("Could not set smartindent on startup"); - - (nvim, join_handle, child) - }) - .await; - - Self { - #[cfg(feature = "neovim")] - data: Default::default(), - #[cfg(not(feature = "neovim"))] - data: Self::read_test_data(&test_case_id), - #[cfg(feature = "neovim")] - test_case_id, - #[cfg(feature = "neovim")] - nvim, - #[cfg(feature = "neovim")] - _join_handle: join_handle, - #[cfg(feature = "neovim")] - _child: child, - } - } - - // Sends a keystroke to the neovim process. - #[cfg(feature = "neovim")] - pub async fn send_keystroke(&mut self, keystroke_text: &str) { - let mut keystroke = Keystroke::parse(keystroke_text).unwrap(); - - if keystroke.key == "<" { - keystroke.key = "lt".to_string() - } - - let special = keystroke.modifiers.shift - || keystroke.modifiers.control - || keystroke.modifiers.alt - || keystroke.modifiers.platform - || keystroke.key.len() > 1; - let start = if special { "<" } else { "" }; - let shift = if keystroke.modifiers.shift { "S-" } else { "" }; - let ctrl = if keystroke.modifiers.control { - "C-" - } else { - "" - }; - let alt = if keystroke.modifiers.alt { "M-" } else { "" }; - let cmd = if keystroke.modifiers.platform { - "D-" - } else { - "" - }; - let end = if special { ">" } else { "" }; - - let key = format!("{start}{shift}{ctrl}{alt}{cmd}{}{end}", keystroke.key); - - self.data - .push_back(NeovimData::Key(keystroke_text.to_string())); - self.nvim - .input(&key) - .await - .expect("Could not input keystroke"); - } - - #[cfg(not(feature = "neovim"))] - pub async fn send_keystroke(&mut self, keystroke_text: &str) { - if matches!(self.data.front(), Some(NeovimData::Get { .. })) { - self.data.pop_front(); - } - assert_eq!( - self.data.pop_front(), - Some(NeovimData::Key(keystroke_text.to_string())), - "operation does not match recorded script. re-record with --features=neovim" - ); - } - - #[cfg(feature = "neovim")] - pub async fn set_state(&mut self, marked_text: &str) { - let (text, selections) = parse_state(marked_text); - - let nvim_buffer = self - .nvim - .get_current_buf() - .await - .expect("Could not get neovim buffer"); - let lines = text - .split('\n') - .map(|line| line.to_string()) - .collect::>(); - - nvim_buffer - .set_lines(0, -1, false, lines) - .await - .expect("Could not set nvim buffer text"); - - self.nvim - .input("") - .await - .expect("Could not send escape to nvim"); - self.nvim - .input("") - .await - .expect("Could not send escape to nvim"); - - let nvim_window = self - .nvim - .get_current_win() - .await - .expect("Could not get neovim window"); - - if selections.len() != 1 { - panic!("must have one selection"); - } - let selection = &selections[0]; - - let cursor = selection.start; - nvim_window - .set_cursor((cursor.row as i64 + 1, cursor.column as i64)) - .await - .expect("Could not set nvim cursor position"); - - if !selection.is_empty() { - self.nvim - .input("v") - .await - .expect("could not enter visual mode"); - - let cursor = selection.end; - nvim_window - .set_cursor((cursor.row as i64 + 1, cursor.column as i64)) - .await - .expect("Could not set nvim cursor position"); - } - - if let Some(NeovimData::Get { mode, state }) = self.data.back() - && *mode == Mode::Normal - && *state == marked_text - { - return; - } - self.data.push_back(NeovimData::Put { - state: marked_text.to_string(), - }) - } - - #[cfg(not(feature = "neovim"))] - pub async fn set_state(&mut self, marked_text: &str) { - if let Some(NeovimData::Get { mode, state: text }) = self.data.front() { - if *mode == Mode::Normal && *text == marked_text { - return; - } - self.data.pop_front(); - } - assert_eq!( - self.data.pop_front(), - Some(NeovimData::Put { - state: marked_text.to_string() - }), - "operation does not match recorded script. re-record with --features=neovim" - ); - } - - #[cfg(feature = "neovim")] - pub async fn set_option(&mut self, value: &str) { - self.nvim - .command_output(format!("set {}", value).as_str()) - .await - .unwrap(); - - self.data.push_back(NeovimData::SetOption { - value: value.to_string(), - }) - } - - #[cfg(not(feature = "neovim"))] - pub async fn set_option(&mut self, value: &str) { - if let Some(NeovimData::Get { .. }) = self.data.front() { - self.data.pop_front(); - }; - assert_eq!( - self.data.pop_front(), - Some(NeovimData::SetOption { - value: value.to_string(), - }), - "operation does not match recorded script. re-record with --features=neovim" - ); - } - - #[cfg(feature = "neovim")] - pub async fn exec(&mut self, value: &str) { - self.nvim.command_output(value).await.unwrap(); - - self.data.push_back(NeovimData::Exec { - command: value.to_string(), - }) - } - - #[cfg(not(feature = "neovim"))] - pub async fn exec(&mut self, value: &str) { - if let Some(NeovimData::Get { .. }) = self.data.front() { - self.data.pop_front(); - }; - assert_eq!( - self.data.pop_front(), - Some(NeovimData::Exec { - command: value.to_string(), - }), - "operation does not match recorded script. re-record with --features=neovim" - ); - } - - #[cfg(not(feature = "neovim"))] - pub async fn read_register(&mut self, register: char) -> String { - if let Some(NeovimData::Get { .. }) = self.data.front() { - self.data.pop_front(); - }; - if let Some(NeovimData::ReadRegister { name, value }) = self.data.pop_front() - && name == register - { - return value; - } - - panic!("operation does not match recorded script. re-record with --features=neovim") - } - - #[cfg(feature = "neovim")] - pub async fn read_register(&mut self, name: char) -> String { - let value = self - .nvim - .command_output(format!("echo getreg('{}')", name).as_str()) - .await - .unwrap(); - - self.data.push_back(NeovimData::ReadRegister { - name, - value: value.clone(), - }); - - value - } - - #[cfg(feature = "neovim")] - async fn read_position(&mut self, cmd: &str) -> u32 { - self.nvim - .command_output(cmd) - .await - .unwrap() - .parse::() - .unwrap() - } - - #[cfg(feature = "neovim")] - pub async fn state(&mut self) -> (Mode, String) { - let nvim_buffer = self - .nvim - .get_current_buf() - .await - .expect("Could not get neovim buffer"); - let text = nvim_buffer - .get_lines(0, -1, false) - .await - .expect("Could not get buffer text") - .join("\n"); - - // nvim columns are 1-based, so -1. - let mut cursor_row = self.read_position("echo line('.')").await - 1; - let mut cursor_col = self.read_position("echo col('.')").await - 1; - let mut selection_row = self.read_position("echo line('v')").await - 1; - let mut selection_col = self.read_position("echo col('v')").await - 1; - let total_rows = self.read_position("echo line('$')").await - 1; - - let nvim_mode_text = self - .nvim - .get_mode() - .await - .expect("Could not get mode") - .into_iter() - .find_map(|(key, value)| { - if key.as_str() == Some("mode") { - Some(value.as_str().unwrap().to_owned()) - } else { - None - } - }) - .expect("Could not find mode value"); - - let mode = match nvim_mode_text.as_ref() { - "i" => Mode::Insert, - "n" => Mode::Normal, - "v" => Mode::Visual, - "V" => Mode::VisualLine, - "R" => Mode::Replace, - "\x16" => Mode::VisualBlock, - _ => panic!("unexpected vim mode: {nvim_mode_text}"), - }; - - let mut selections = Vec::new(); - // Vim uses the index of the first and last character in the selection - // Zed uses the index of the positions between the characters, so we need - // to add one to the end in visual mode. - match mode { - Mode::VisualBlock if selection_row != cursor_row => { - // in zed we fake a block selection by using multiple cursors (one per line) - // this code emulates that. - // to deal with casees where the selection is not perfectly rectangular we extract - // the content of the selection via the "a register to get the shape correctly. - self.nvim.input("\"aygv").await.unwrap(); - let content = self.nvim.command_output("echo getreg('a')").await.unwrap(); - let lines = content.split('\n').collect::>(); - let top = cmp::min(selection_row, cursor_row); - let left = cmp::min(selection_col, cursor_col); - for row in top..=cmp::max(selection_row, cursor_row) { - let content = if row - top >= lines.len() as u32 { - "" - } else { - lines[(row - top) as usize] - }; - let line_len = self - .read_position(format!("echo strlen(getline({}))", row + 1).as_str()) - .await; - - if left > line_len { - continue; - } - - let start = Point::new(row, left); - let end = Point::new(row, left + content.len() as u32); - if cursor_col >= selection_col { - selections.push(start..end) - } else { - selections.push(end..start) - } - } - } - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { - if (selection_row, selection_col) > (cursor_row, cursor_col) { - let selection_line_length = - self.read_position("echo strlen(getline(line('v')))").await; - if selection_line_length > selection_col { - selection_col += 1; - } else if selection_row < total_rows { - selection_col = 0; - selection_row += 1; - } - } else { - let cursor_line_length = - self.read_position("echo strlen(getline(line('.')))").await; - if cursor_line_length > cursor_col { - cursor_col += 1; - } else if cursor_row < total_rows { - cursor_col = 0; - cursor_row += 1; - } - } - selections.push( - Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col), - ) - } - Mode::Insert | Mode::Normal | Mode::Replace => selections - .push(Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col)), - Mode::HelixNormal | Mode::HelixSelect => unreachable!(), - } - - let ranges = encode_ranges(&text, &selections); - let state = NeovimData::Get { - mode, - state: ranges.clone(), - }; - - if self.data.back() != Some(&state) { - self.data.push_back(state); - } - - (mode, ranges) - } - - #[cfg(not(feature = "neovim"))] - pub async fn state(&mut self) -> (Mode, String) { - if let Some(NeovimData::Get { state: raw, mode }) = self.data.front() { - (*mode, raw.to_string()) - } else { - panic!("operation does not match recorded script. re-record with --features=neovim"); - } - } - - fn test_data_path(test_case_id: &str) -> PathBuf { - let mut data_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - data_path.push("test_data"); - data_path.push(format!("{}.json", test_case_id)); - data_path - } - - #[cfg(not(feature = "neovim"))] - fn read_test_data(test_case_id: &str) -> VecDeque { - let path = Self::test_data_path(test_case_id); - let json = std::fs::read_to_string(path).expect( - "Could not read test data. Is it generated? Try running test with '--features neovim'", - ); - - let mut result = VecDeque::new(); - for line in json.lines() { - result.push_back( - serde_json::from_str(line) - .expect("invalid test data. regenerate it with '--features neovim'"), - ); - } - result - } - - #[cfg(feature = "neovim")] - fn write_test_data(test_case_id: &str, data: &VecDeque) { - let path = Self::test_data_path(test_case_id); - let mut json = Vec::new(); - for entry in data { - serde_json::to_writer(&mut json, entry).unwrap(); - json.push(b'\n'); - } - std::fs::create_dir_all(path.parent().unwrap()) - .expect("could not create test data directory"); - std::fs::write(path, json).expect("could not write out test data"); - } -} - -#[cfg(feature = "neovim")] -impl Deref for NeovimConnection { - type Target = Neovim>; - - fn deref(&self) -> &Self::Target { - &self.nvim - } -} - -#[cfg(feature = "neovim")] -impl DerefMut for NeovimConnection { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.nvim - } -} - -#[cfg(feature = "neovim")] -impl Drop for NeovimConnection { - fn drop(&mut self) { - Self::write_test_data(&self.test_case_id, &self.data); - } -} - -#[cfg(feature = "neovim")] -#[derive(Clone)] -struct NvimHandler {} - -#[cfg(feature = "neovim")] -#[async_trait] -impl Handler for NvimHandler { - type Writer = nvim_rs::compat::tokio::Compat; - - async fn handle_request( - &self, - _event_name: String, - _arguments: Vec, - _neovim: Neovim, - ) -> Result { - unimplemented!(); - } - - async fn handle_notify( - &self, - _event_name: String, - _arguments: Vec, - _neovim: Neovim, - ) { - } -} - -#[cfg(feature = "neovim")] -fn parse_state(marked_text: &str) -> (String, Vec>) { - let (text, ranges) = util::test::marked_text_ranges(marked_text, true); - let point_ranges = ranges - .into_iter() - .map(|byte_range| { - let mut point_range = Point::zero()..Point::zero(); - let mut ix = 0; - let mut position = Point::zero(); - for c in text.chars().chain(['\0']) { - if ix == byte_range.start { - point_range.start = position; - } - if ix == byte_range.end { - point_range.end = position; - } - let len_utf8 = c.len_utf8(); - ix += len_utf8; - if c == '\n' { - position.row += 1; - position.column = 0; - } else { - position.column += len_utf8 as u32; - } - } - point_range - }) - .collect::>(); - (text, point_ranges) -} - -#[cfg(feature = "neovim")] -fn encode_ranges(text: &str, point_ranges: &Vec>) -> String { - let byte_ranges = point_ranges - .iter() - .map(|range| { - let mut byte_range = 0..0; - let mut ix = 0; - let mut position = Point::zero(); - for c in text.chars().chain(['\0']) { - if position == range.start { - byte_range.start = ix; - } - if position == range.end { - byte_range.end = ix; - } - let len_utf8 = c.len_utf8(); - ix += len_utf8; - if c == '\n' { - position.row += 1; - position.column = 0; - } else { - position.column += len_utf8 as u32; - } - } - byte_range - }) - .collect::>(); - util::test::generate_marked_text(text, &byte_ranges[..], true) -} diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs deleted file mode 100644 index acd77839f2..0000000000 --- a/crates/vim/src/test/vim_test_context.rs +++ /dev/null @@ -1,289 +0,0 @@ -use std::ops::{Deref, DerefMut}; - -use editor::test::editor_lsp_test_context::EditorLspTestContext; -use gpui::{Context, Entity, UpdateGlobal}; -use search::{BufferSearchBar, project_search::ProjectSearchBar}; -use semver::Version; - -use crate::{state::Operator, *}; - -pub struct VimTestContext { - cx: EditorLspTestContext, -} - -impl VimTestContext { - pub fn init(cx: &mut gpui::TestAppContext) { - if cx.has_global::() { - return; - } - env_logger::try_init().ok(); - cx.update(|cx| { - let settings = SettingsStore::test(cx); - cx.set_global(settings); - release_channel::init(Version::new(0, 0, 0), cx); - command_palette::init(cx); - project_panel::init(cx); - git_ui::init(cx); - crate::init(cx); - search::init(cx); - theme::init(theme::LoadThemes::JustBase, cx); - settings_ui::init(cx); - markdown_preview::init(cx); - }); - } - - pub async fn new(cx: &mut gpui::TestAppContext, enabled: bool) -> VimTestContext { - Self::init(cx); - let lsp = EditorLspTestContext::new_rust(Default::default(), cx).await; - Self::new_with_lsp(lsp, enabled) - } - - pub async fn new_html(cx: &mut gpui::TestAppContext) -> VimTestContext { - Self::init(cx); - Self::new_with_lsp(EditorLspTestContext::new_html(cx).await, true) - } - - pub async fn new_markdown_with_rust(cx: &mut gpui::TestAppContext) -> VimTestContext { - Self::init(cx); - Self::new_with_lsp(EditorLspTestContext::new_markdown_with_rust(cx).await, true) - } - - pub async fn new_typescript(cx: &mut gpui::TestAppContext) -> VimTestContext { - Self::init(cx); - Self::new_with_lsp( - EditorLspTestContext::new_typescript( - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string()]), - ..Default::default() - }), - rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { - prepare_provider: Some(true), - work_done_progress_options: Default::default(), - })), - definition_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - cx, - ) - .await, - true, - ) - } - - pub async fn new_tsx(cx: &mut gpui::TestAppContext) -> VimTestContext { - Self::init(cx); - Self::new_with_lsp( - EditorLspTestContext::new_tsx( - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string()]), - ..Default::default() - }), - rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { - prepare_provider: Some(true), - work_done_progress_options: Default::default(), - })), - ..Default::default() - }, - cx, - ) - .await, - true, - ) - } - - pub fn init_keybindings(enabled: bool, cx: &mut App) { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |s| s.vim_mode = Some(enabled)); - }); - let mut default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure( - "keymaps/default-macos.json", - cx, - ) - .unwrap(); - for key_binding in &mut default_key_bindings { - key_binding.set_meta(settings::KeybindSource::Default.meta()); - } - cx.bind_keys(default_key_bindings); - if enabled { - let vim_key_bindings = settings::KeymapFile::load_asset( - "keymaps/vim.json", - Some(settings::KeybindSource::Vim), - cx, - ) - .unwrap(); - cx.bind_keys(vim_key_bindings); - } - } - - pub fn new_with_lsp(mut cx: EditorLspTestContext, enabled: bool) -> VimTestContext { - cx.update(|_, cx| { - Self::init_keybindings(enabled, cx); - }); - - // Setup search toolbars and keypress hook - cx.update_workspace(|workspace, window, cx| { - workspace.active_pane().update(cx, |pane, cx| { - pane.toolbar().update(cx, |toolbar, cx| { - let buffer_search_bar = cx.new(|cx| BufferSearchBar::new(None, window, cx)); - toolbar.add_item(buffer_search_bar, window, cx); - - let project_search_bar = cx.new(|_| ProjectSearchBar::new()); - toolbar.add_item(project_search_bar, window, cx); - }) - }); - workspace.status_bar().update(cx, |status_bar, cx| { - let vim_mode_indicator = cx.new(|cx| ModeIndicator::new(window, cx)); - status_bar.add_right_item(vim_mode_indicator, window, cx); - }); - }); - - Self { cx } - } - - pub fn update_entity(&mut self, entity: Entity, update: F) -> R - where - T: 'static, - F: FnOnce(&mut T, &mut Window, &mut Context) -> R + 'static, - { - let window = self.window; - self.update_window(window, move |_, window, cx| { - entity.update(cx, |t, cx| update(t, window, cx)) - }) - .unwrap() - } - - pub fn workspace(&mut self, update: F) -> T - where - F: FnOnce(&mut Workspace, &mut Window, &mut Context) -> T, - { - self.cx.update_workspace(update) - } - - pub fn enable_vim(&mut self) { - self.cx.update(|_, cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |s| s.vim_mode = Some(true)); - }); - }) - } - - pub fn disable_vim(&mut self) { - self.cx.update(|_, cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |s| s.vim_mode = Some(false)); - }); - }) - } - - pub fn enable_helix(&mut self) { - self.cx.update(|_, cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |s| s.helix_mode = Some(true)); - }); - }) - } - - pub fn mode(&mut self) -> Mode { - self.update_editor(|editor, _, cx| editor.addon::().unwrap().entity.read(cx).mode) - } - - pub fn forced_motion(&mut self) -> bool { - self.update_editor(|_, _, cx| cx.global::().forced_motion) - } - - pub fn active_operator(&mut self) -> Option { - self.update_editor(|editor, _, cx| { - editor - .addon::() - .unwrap() - .entity - .read(cx) - .operator_stack - .last() - .cloned() - }) - } - - pub fn set_state(&mut self, text: &str, mode: Mode) { - self.cx.set_state(text); - let vim = - self.update_editor(|editor, _window, _cx| editor.addon::().cloned().unwrap()); - - self.update(|window, cx| { - vim.entity.update(cx, |vim, cx| { - vim.switch_mode(mode, true, window, cx); - }); - }); - self.cx.cx.cx.run_until_parked(); - } - - #[track_caller] - pub fn assert_state(&mut self, text: &str, mode: Mode) { - self.assert_editor_state(text); - assert_eq!(self.mode(), mode, "{}", self.assertion_context()); - } - - pub fn assert_binding( - &mut self, - keystrokes: &str, - initial_state: &str, - initial_mode: Mode, - state_after: &str, - mode_after: Mode, - ) { - self.set_state(initial_state, initial_mode); - self.cx.simulate_keystrokes(keystrokes); - self.cx.assert_editor_state(state_after); - assert_eq!(self.mode(), mode_after, "{}", self.assertion_context()); - assert_eq!(self.active_operator(), None, "{}", self.assertion_context()); - } - - pub fn assert_binding_normal( - &mut self, - keystrokes: &str, - initial_state: &str, - state_after: &str, - ) { - self.set_state(initial_state, Mode::Normal); - self.cx.simulate_keystrokes(keystrokes); - self.cx.assert_editor_state(state_after); - assert_eq!(self.mode(), Mode::Normal, "{}", self.assertion_context()); - assert_eq!(self.active_operator(), None, "{}", self.assertion_context()); - } - - pub fn shared_clipboard(&mut self) -> VimClipboard { - VimClipboard { - editor: self - .read_from_clipboard() - .map(|item| item.text().unwrap()) - .unwrap_or_default(), - } - } -} - -pub struct VimClipboard { - editor: String, -} - -impl VimClipboard { - #[track_caller] - pub fn assert_eq(&self, expected: &str) { - assert_eq!(self.editor, expected); - } -} - -impl Deref for VimTestContext { - type Target = EditorLspTestContext; - - fn deref(&self) -> &Self::Target { - &self.cx - } -} - -impl DerefMut for VimTestContext { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.cx - } -} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs deleted file mode 100644 index 9a9a1a001c..0000000000 --- a/crates/vim/src/vim.rs +++ /dev/null @@ -1,2022 +0,0 @@ -//! Vim support for Zed. - -#[cfg(test)] -mod test; - -mod change_list; -mod command; -mod digraph; -mod helix; -mod indent; -mod insert; -mod mode_indicator; -mod motion; -mod normal; -mod object; -mod replace; -mod rewrap; -mod state; -mod surrounds; -mod visual; - -use crate::normal::paste::Paste as VimPaste; -use collections::HashMap; -use editor::{ - Anchor, Bias, Editor, EditorEvent, EditorSettings, HideMouseCursorOrigin, MultiBufferOffset, - SelectionEffects, ToPoint, - actions::Paste, - movement::{self, FindRange}, -}; -use gpui::{ - Action, App, AppContext, Axis, Context, Entity, EventEmitter, KeyContext, KeystrokeEvent, - Render, Subscription, Task, WeakEntity, Window, actions, -}; -use insert::{NormalBefore, TemporaryNormal}; -use language::{ - CharKind, CharScopeContext, CursorShape, Point, Selection, SelectionGoal, TransactionId, -}; -pub use mode_indicator::ModeIndicator; -use motion::Motion; -use normal::search::SearchSubmit; -use object::Object; -use schemars::JsonSchema; -use serde::Deserialize; -use settings::RegisterSetting; -pub use settings::{ - ModeContent, Settings, SettingsStore, UseSystemClipboard, update_settings_file, -}; -use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals}; -use std::{mem, ops::Range, sync::Arc}; -use surrounds::SurroundsType; -use theme::ThemeSettings; -use ui::{IntoElement, SharedString, px}; -use vim_mode_setting::HelixModeSetting; -use vim_mode_setting::VimModeSetting; -use workspace::{self, Pane, Workspace}; - -use crate::{ - normal::{GoToPreviousTab, GoToTab}, - state::ReplayableAction, -}; - -/// Number is used to manage vim's count. Pushing a digit -/// multiplies the current value by 10 and adds the digit. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -struct Number(usize); - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -struct SelectRegister(String); - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PushObject { - around: bool, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PushFindForward { - before: bool, - multiline: bool, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PushFindBackward { - after: bool, - multiline: bool, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -/// Selects the next object. -struct PushHelixNext { - around: bool, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -/// Selects the previous object. -struct PushHelixPrevious { - around: bool, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PushSneak { - first_char: Option, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PushSneakBackward { - first_char: Option, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PushAddSurrounds; - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PushChangeSurrounds { - target: Option, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PushJump { - line: bool, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PushDigraph { - first_char: Option, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PushLiteral { - prefix: Option, -} - -actions!( - vim, - [ - /// Switches to normal mode. - SwitchToNormalMode, - /// Switches to insert mode. - SwitchToInsertMode, - /// Switches to replace mode. - SwitchToReplaceMode, - /// Switches to visual mode. - SwitchToVisualMode, - /// Switches to visual line mode. - SwitchToVisualLineMode, - /// Switches to visual block mode. - SwitchToVisualBlockMode, - /// Switches to Helix-style normal mode. - SwitchToHelixNormalMode, - /// Clears any pending operators. - ClearOperators, - /// Clears the exchange register. - ClearExchange, - /// Inserts a tab character. - Tab, - /// Inserts a newline. - Enter, - /// Selects inner text object. - InnerObject, - /// Maximizes the current pane. - MaximizePane, - /// Resets all pane sizes to default. - ResetPaneSizes, - /// Resizes the pane to the right. - ResizePaneRight, - /// Resizes the pane to the left. - ResizePaneLeft, - /// Resizes the pane upward. - ResizePaneUp, - /// Resizes the pane downward. - ResizePaneDown, - /// Starts a change operation. - PushChange, - /// Starts a delete operation. - PushDelete, - /// Exchanges text regions. - Exchange, - /// Starts a yank operation. - PushYank, - /// Starts a replace operation. - PushReplace, - /// Deletes surrounding characters. - PushDeleteSurrounds, - /// Sets a mark at the current position. - PushMark, - /// Toggles the marks view. - ToggleMarksView, - /// Starts a forced motion. - PushForcedMotion, - /// Starts an indent operation. - PushIndent, - /// Starts an outdent operation. - PushOutdent, - /// Starts an auto-indent operation. - PushAutoIndent, - /// Starts a rewrap operation. - PushRewrap, - /// Starts a shell command operation. - PushShellCommand, - /// Converts to lowercase. - PushLowercase, - /// Converts to uppercase. - PushUppercase, - /// Toggles case. - PushOppositeCase, - /// Applies ROT13 encoding. - PushRot13, - /// Applies ROT47 encoding. - PushRot47, - /// Toggles the registers view. - ToggleRegistersView, - /// Selects a register. - PushRegister, - /// Starts recording to a register. - PushRecordRegister, - /// Replays a register. - PushReplayRegister, - /// Replaces with register contents. - PushReplaceWithRegister, - /// Toggles comments. - PushToggleComments, - /// Selects (count) next menu item - MenuSelectNext, - /// Selects (count) previous menu item - MenuSelectPrevious, - /// Clears count or toggles project panel focus - ToggleProjectPanelFocus, - /// Starts a match operation. - PushHelixMatch, - ] -); - -// in the workspace namespace so it's not filtered out when vim is disabled. -actions!( - workspace, - [ - /// Toggles Vim mode on or off. - ToggleVimMode, - /// Toggles Helix mode on or off. - ToggleHelixMode, - ] -); - -/// Initializes the `vim` crate. -pub fn init(cx: &mut App) { - VimGlobals::register(cx); - - cx.observe_new(Vim::register).detach(); - - cx.observe_new(|workspace: &mut Workspace, _, _| { - workspace.register_action(|workspace, _: &ToggleVimMode, _, cx| { - let fs = workspace.app_state().fs.clone(); - let currently_enabled = VimModeSetting::get_global(cx).0; - update_settings_file(fs, cx, move |setting, _| { - setting.vim_mode = Some(!currently_enabled); - if let Some(helix_mode) = &mut setting.helix_mode { - *helix_mode = false; - } - }) - }); - - workspace.register_action(|workspace, _: &ToggleHelixMode, _, cx| { - let fs = workspace.app_state().fs.clone(); - let currently_enabled = HelixModeSetting::get_global(cx).0; - update_settings_file(fs, cx, move |setting, _| { - setting.helix_mode = Some(!currently_enabled); - if let Some(vim_mode) = &mut setting.vim_mode { - *vim_mode = false; - } - }) - }); - - workspace.register_action(|_, _: &MenuSelectNext, window, cx| { - let count = Vim::take_count(cx).unwrap_or(1); - - for _ in 0..count { - window.dispatch_action(menu::SelectNext.boxed_clone(), cx); - } - }); - - workspace.register_action(|_, _: &MenuSelectPrevious, window, cx| { - let count = Vim::take_count(cx).unwrap_or(1); - - for _ in 0..count { - window.dispatch_action(menu::SelectPrevious.boxed_clone(), cx); - } - }); - - workspace.register_action(|_, _: &ToggleProjectPanelFocus, window, cx| { - if Vim::take_count(cx).is_none() { - window.dispatch_action(zed_actions::project_panel::ToggleFocus.boxed_clone(), cx); - } - }); - - workspace.register_action(|workspace, n: &Number, window, cx| { - let vim = workspace - .focused_pane(window, cx) - .read(cx) - .active_item() - .and_then(|item| item.act_as::(cx)) - .and_then(|editor| editor.read(cx).addon::().cloned()); - if let Some(vim) = vim { - let digit = n.0; - vim.entity.update(cx, |_, cx| { - cx.defer_in(window, move |vim, window, cx| { - vim.push_count_digit(digit, window, cx) - }) - }); - } else { - let count = Vim::globals(cx).pre_count.unwrap_or(0); - Vim::globals(cx).pre_count = Some( - count - .checked_mul(10) - .and_then(|c| c.checked_add(n.0)) - .unwrap_or(count), - ); - }; - }); - - workspace.register_action(|_, _: &zed_actions::vim::OpenDefaultKeymap, _, cx| { - cx.emit(workspace::Event::OpenBundledFile { - text: settings::vim_keymap(), - title: "Default Vim Bindings", - language: "JSON", - }); - }); - - workspace.register_action(|workspace, _: &ResetPaneSizes, _, cx| { - workspace.reset_pane_sizes(cx); - }); - - workspace.register_action(|workspace, _: &MaximizePane, window, cx| { - let pane = workspace.active_pane(); - let Some(size) = workspace.bounding_box_for_pane(pane) else { - return; - }; - - let theme = ThemeSettings::get_global(cx); - let height = theme.buffer_font_size(cx) * theme.buffer_line_height.value(); - - let desired_size = if let Some(count) = Vim::take_count(cx) { - height * count - } else { - px(10000.) - }; - workspace.resize_pane(Axis::Vertical, desired_size - size.size.height, window, cx) - }); - - workspace.register_action(|workspace, _: &ResizePaneRight, window, cx| { - let count = Vim::take_count(cx).unwrap_or(1) as f32; - Vim::take_forced_motion(cx); - let theme = ThemeSettings::get_global(cx); - let font_id = window.text_system().resolve_font(&theme.buffer_font); - let Ok(width) = window - .text_system() - .advance(font_id, theme.buffer_font_size(cx), 'm') - else { - return; - }; - workspace.resize_pane(Axis::Horizontal, width.width * count, window, cx); - }); - - workspace.register_action(|workspace, _: &ResizePaneLeft, window, cx| { - let count = Vim::take_count(cx).unwrap_or(1) as f32; - Vim::take_forced_motion(cx); - let theme = ThemeSettings::get_global(cx); - let font_id = window.text_system().resolve_font(&theme.buffer_font); - let Ok(width) = window - .text_system() - .advance(font_id, theme.buffer_font_size(cx), 'm') - else { - return; - }; - workspace.resize_pane(Axis::Horizontal, -width.width * count, window, cx); - }); - - workspace.register_action(|workspace, _: &ResizePaneUp, window, cx| { - let count = Vim::take_count(cx).unwrap_or(1) as f32; - Vim::take_forced_motion(cx); - let theme = ThemeSettings::get_global(cx); - let height = theme.buffer_font_size(cx) * theme.buffer_line_height.value(); - workspace.resize_pane(Axis::Vertical, height * count, window, cx); - }); - - workspace.register_action(|workspace, _: &ResizePaneDown, window, cx| { - let count = Vim::take_count(cx).unwrap_or(1) as f32; - Vim::take_forced_motion(cx); - let theme = ThemeSettings::get_global(cx); - let height = theme.buffer_font_size(cx) * theme.buffer_line_height.value(); - workspace.resize_pane(Axis::Vertical, -height * count, window, cx); - }); - - workspace.register_action(|workspace, _: &SearchSubmit, window, cx| { - let vim = workspace - .focused_pane(window, cx) - .read(cx) - .active_item() - .and_then(|item| item.act_as::(cx)) - .and_then(|editor| editor.read(cx).addon::().cloned()); - let Some(vim) = vim else { return }; - vim.entity.update(cx, |_, cx| { - cx.defer_in(window, |vim, window, cx| vim.search_submit(window, cx)) - }) - }); - workspace.register_action(|_, _: &GoToTab, window, cx| { - let count = Vim::take_count(cx); - Vim::take_forced_motion(cx); - - if let Some(tab_index) = count { - // gt goes to tab (1-based). - let zero_based_index = tab_index.saturating_sub(1); - window.dispatch_action( - workspace::pane::ActivateItem(zero_based_index).boxed_clone(), - cx, - ); - } else { - // If no count is provided, go to the next tab. - window.dispatch_action(workspace::pane::ActivateNextItem.boxed_clone(), cx); - } - }); - - workspace.register_action(|workspace, _: &GoToPreviousTab, window, cx| { - let count = Vim::take_count(cx); - Vim::take_forced_motion(cx); - - if let Some(count) = count { - // gT with count goes back that many tabs with wraparound (not the same as gt!). - let pane = workspace.active_pane().read(cx); - let item_count = pane.items().count(); - if item_count > 0 { - let current_index = pane.active_item_index(); - let target_index = (current_index as isize - count as isize) - .rem_euclid(item_count as isize) - as usize; - window.dispatch_action( - workspace::pane::ActivateItem(target_index).boxed_clone(), - cx, - ); - } - } else { - // No count provided, go to the previous tab. - window.dispatch_action(workspace::pane::ActivatePreviousItem.boxed_clone(), cx); - } - }); - }) - .detach(); -} - -#[derive(Clone)] -pub(crate) struct VimAddon { - pub(crate) entity: Entity, -} - -impl editor::Addon for VimAddon { - fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) { - self.entity.read(cx).extend_key_context(key_context, cx) - } - - fn to_any(&self) -> &dyn std::any::Any { - self - } -} - -/// The state pertaining to Vim mode. -pub(crate) struct Vim { - pub(crate) mode: Mode, - pub last_mode: Mode, - pub temp_mode: bool, - pub status_label: Option, - pub exit_temporary_mode: bool, - - operator_stack: Vec, - pub(crate) replacements: Vec<(Range, String)>, - - pub(crate) stored_visual_mode: Option<(Mode, Vec)>, - - pub(crate) current_tx: Option, - pub(crate) current_anchor: Option>, - pub(crate) undo_modes: HashMap, - pub(crate) undo_last_line_tx: Option, - - selected_register: Option, - pub search: SearchState, - - editor: WeakEntity, - - last_command: Option, - running_command: Option>, - _subscriptions: Vec, -} - -// Hack: Vim intercepts events dispatched to a window and updates the view in response. -// This means it needs a VisualContext. The easiest way to satisfy that constraint is -// to make Vim a "View" that is just never actually rendered. -impl Render for Vim { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - gpui::Empty - } -} - -enum VimEvent { - Focused, -} -impl EventEmitter for Vim {} - -impl Vim { - /// The namespace for Vim actions. - const NAMESPACE: &'static str = "vim"; - - pub fn new(window: &mut Window, cx: &mut Context) -> Entity { - let editor = cx.entity(); - - let initial_vim_mode = VimSettings::get_global(cx).default_mode; - let (mode, last_mode) = if HelixModeSetting::get_global(cx).0 { - let initial_helix_mode = match initial_vim_mode { - Mode::Normal => Mode::HelixNormal, - Mode::Insert => Mode::Insert, - // Otherwise, we panic with a note that we should never get there due to the - // possible values of VimSettings::get_global(cx).default_mode being either Mode::Normal or Mode::Insert. - _ => unreachable!("Invalid default mode"), - }; - (initial_helix_mode, Mode::HelixNormal) - } else { - (initial_vim_mode, Mode::Normal) - }; - - cx.new(|cx| Vim { - mode, - last_mode, - temp_mode: false, - exit_temporary_mode: false, - operator_stack: Vec::new(), - replacements: Vec::new(), - - stored_visual_mode: None, - current_tx: None, - undo_last_line_tx: None, - current_anchor: None, - undo_modes: HashMap::default(), - - status_label: None, - selected_register: None, - search: SearchState::default(), - - last_command: None, - running_command: None, - - editor: editor.downgrade(), - _subscriptions: vec![ - cx.observe_keystrokes(Self::observe_keystrokes), - cx.subscribe_in(&editor, window, |this, _, event, window, cx| { - this.handle_editor_event(event, window, cx) - }), - ], - }) - } - - fn register(editor: &mut Editor, window: Option<&mut Window>, cx: &mut Context) { - let Some(window) = window else { - return; - }; - - if !editor.use_modal_editing() { - return; - } - - let mut was_enabled = Vim::enabled(cx); - let mut was_toggle = VimSettings::get_global(cx).toggle_relative_line_numbers; - cx.observe_global_in::(window, move |editor, window, cx| { - let enabled = Vim::enabled(cx); - let toggle = VimSettings::get_global(cx).toggle_relative_line_numbers; - if enabled && was_enabled && (toggle != was_toggle) { - if toggle { - let is_relative = editor - .addon::() - .map(|vim| vim.entity.read(cx).mode != Mode::Insert); - editor.set_relative_line_number(is_relative, cx) - } else { - editor.set_relative_line_number(None, cx) - } - } - was_toggle = VimSettings::get_global(cx).toggle_relative_line_numbers; - if was_enabled == enabled { - return; - } - was_enabled = enabled; - if enabled { - Self::activate(editor, window, cx) - } else { - Self::deactivate(editor, cx) - } - }) - .detach(); - if was_enabled { - Self::activate(editor, window, cx) - } - } - - fn activate(editor: &mut Editor, window: &mut Window, cx: &mut Context) { - let vim = Vim::new(window, cx); - - if !editor.mode().is_full() { - vim.update(cx, |vim, _| { - vim.mode = Mode::Insert; - }); - } - - editor.register_addon(VimAddon { - entity: vim.clone(), - }); - - vim.update(cx, |_, cx| { - Vim::action(editor, cx, |vim, _: &SwitchToNormalMode, window, cx| { - vim.switch_mode(Mode::Normal, false, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &SwitchToInsertMode, window, cx| { - vim.switch_mode(Mode::Insert, false, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &SwitchToReplaceMode, window, cx| { - vim.switch_mode(Mode::Replace, false, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &SwitchToVisualMode, window, cx| { - vim.switch_mode(Mode::Visual, false, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &SwitchToVisualLineMode, window, cx| { - vim.switch_mode(Mode::VisualLine, false, window, cx) - }); - - Vim::action( - editor, - cx, - |vim, _: &SwitchToVisualBlockMode, window, cx| { - vim.switch_mode(Mode::VisualBlock, false, window, cx) - }, - ); - - Vim::action( - editor, - cx, - |vim, _: &SwitchToHelixNormalMode, window, cx| { - vim.switch_mode(Mode::HelixNormal, true, window, cx) - }, - ); - Vim::action(editor, cx, |_, _: &PushForcedMotion, _, cx| { - Vim::globals(cx).forced_motion = true; - }); - Vim::action(editor, cx, |vim, action: &PushObject, window, cx| { - vim.push_operator( - Operator::Object { - around: action.around, - }, - window, - cx, - ) - }); - - Vim::action(editor, cx, |vim, action: &PushFindForward, window, cx| { - vim.push_operator( - Operator::FindForward { - before: action.before, - multiline: action.multiline, - }, - window, - cx, - ) - }); - - Vim::action(editor, cx, |vim, action: &PushFindBackward, window, cx| { - vim.push_operator( - Operator::FindBackward { - after: action.after, - multiline: action.multiline, - }, - window, - cx, - ) - }); - - Vim::action(editor, cx, |vim, action: &PushSneak, window, cx| { - vim.push_operator( - Operator::Sneak { - first_char: action.first_char, - }, - window, - cx, - ) - }); - - Vim::action(editor, cx, |vim, action: &PushSneakBackward, window, cx| { - vim.push_operator( - Operator::SneakBackward { - first_char: action.first_char, - }, - window, - cx, - ) - }); - - Vim::action(editor, cx, |vim, _: &PushAddSurrounds, window, cx| { - vim.push_operator(Operator::AddSurrounds { target: None }, window, cx) - }); - - Vim::action( - editor, - cx, - |vim, action: &PushChangeSurrounds, window, cx| { - vim.push_operator( - Operator::ChangeSurrounds { - target: action.target, - opening: false, - }, - window, - cx, - ) - }, - ); - - Vim::action(editor, cx, |vim, action: &PushJump, window, cx| { - vim.push_operator(Operator::Jump { line: action.line }, window, cx) - }); - - Vim::action(editor, cx, |vim, action: &PushDigraph, window, cx| { - vim.push_operator( - Operator::Digraph { - first_char: action.first_char, - }, - window, - cx, - ) - }); - - Vim::action(editor, cx, |vim, action: &PushLiteral, window, cx| { - vim.push_operator( - Operator::Literal { - prefix: action.prefix.clone(), - }, - window, - cx, - ) - }); - - Vim::action(editor, cx, |vim, _: &PushChange, window, cx| { - vim.push_operator(Operator::Change, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushDelete, window, cx| { - vim.push_operator(Operator::Delete, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushYank, window, cx| { - vim.push_operator(Operator::Yank, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushReplace, window, cx| { - vim.push_operator(Operator::Replace, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushDeleteSurrounds, window, cx| { - vim.push_operator(Operator::DeleteSurrounds, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushMark, window, cx| { - vim.push_operator(Operator::Mark, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushIndent, window, cx| { - vim.push_operator(Operator::Indent, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushOutdent, window, cx| { - vim.push_operator(Operator::Outdent, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushAutoIndent, window, cx| { - vim.push_operator(Operator::AutoIndent, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushRewrap, window, cx| { - vim.push_operator(Operator::Rewrap, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushShellCommand, window, cx| { - vim.push_operator(Operator::ShellCommand, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushLowercase, window, cx| { - vim.push_operator(Operator::Lowercase, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushUppercase, window, cx| { - vim.push_operator(Operator::Uppercase, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushOppositeCase, window, cx| { - vim.push_operator(Operator::OppositeCase, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushRot13, window, cx| { - vim.push_operator(Operator::Rot13, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushRot47, window, cx| { - vim.push_operator(Operator::Rot47, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushRegister, window, cx| { - vim.push_operator(Operator::Register, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushRecordRegister, window, cx| { - vim.push_operator(Operator::RecordRegister, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushReplayRegister, window, cx| { - vim.push_operator(Operator::ReplayRegister, window, cx) - }); - - Vim::action( - editor, - cx, - |vim, _: &PushReplaceWithRegister, window, cx| { - vim.push_operator(Operator::ReplaceWithRegister, window, cx) - }, - ); - - Vim::action(editor, cx, |vim, _: &Exchange, window, cx| { - if vim.mode.is_visual() { - vim.exchange_visual(window, cx) - } else { - vim.push_operator(Operator::Exchange, window, cx) - } - }); - - Vim::action(editor, cx, |vim, _: &ClearExchange, window, cx| { - vim.clear_exchange(window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushToggleComments, window, cx| { - vim.push_operator(Operator::ToggleComments, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &ClearOperators, window, cx| { - vim.clear_operator(window, cx) - }); - Vim::action(editor, cx, |vim, n: &Number, window, cx| { - vim.push_count_digit(n.0, window, cx); - }); - Vim::action(editor, cx, |vim, _: &Tab, window, cx| { - vim.input_ignored(" ".into(), window, cx) - }); - Vim::action( - editor, - cx, - |vim, action: &editor::actions::AcceptEditPrediction, window, cx| { - vim.update_editor(cx, |_, editor, cx| { - editor.accept_edit_prediction(action, window, cx); - }); - // In non-insertion modes, predictions will be hidden and instead a jump will be - // displayed (and performed by `accept_edit_prediction`). This switches to - // insert mode so that the prediction is displayed after the jump. - match vim.mode { - Mode::Replace => {} - _ => vim.switch_mode(Mode::Insert, true, window, cx), - }; - }, - ); - Vim::action(editor, cx, |vim, _: &Enter, window, cx| { - vim.input_ignored("\n".into(), window, cx) - }); - Vim::action(editor, cx, |vim, _: &PushHelixMatch, window, cx| { - vim.push_operator(Operator::HelixMatch, window, cx) - }); - Vim::action(editor, cx, |vim, action: &PushHelixNext, window, cx| { - vim.push_operator( - Operator::HelixNext { - around: action.around, - }, - window, - cx, - ); - }); - Vim::action(editor, cx, |vim, action: &PushHelixPrevious, window, cx| { - vim.push_operator( - Operator::HelixPrevious { - around: action.around, - }, - window, - cx, - ); - }); - - Vim::action( - editor, - cx, - |vim, _: &editor::actions::Paste, window, cx| match vim.mode { - Mode::Replace => vim.paste_replace(window, cx), - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { - vim.selected_register.replace('+'); - vim.paste(&VimPaste::default(), window, cx); - } - _ => { - vim.update_editor(cx, |_, editor, cx| editor.paste(&Paste, window, cx)); - } - }, - ); - - normal::register(editor, cx); - insert::register(editor, cx); - helix::register(editor, cx); - motion::register(editor, cx); - command::register(editor, cx); - replace::register(editor, cx); - indent::register(editor, cx); - rewrap::register(editor, cx); - object::register(editor, cx); - visual::register(editor, cx); - change_list::register(editor, cx); - digraph::register(editor, cx); - - if editor.is_focused(window) { - cx.defer_in(window, |vim, window, cx| { - vim.focused(false, window, cx); - }) - } - }) - } - - fn deactivate(editor: &mut Editor, cx: &mut Context) { - editor.set_cursor_shape( - EditorSettings::get_global(cx) - .cursor_shape - .unwrap_or_default(), - cx, - ); - editor.set_clip_at_line_ends(false, cx); - editor.set_collapse_matches(false); - editor.set_input_enabled(true); - editor.set_autoindent(true); - editor.selections.set_line_mode(false); - editor.unregister_addon::(); - editor.set_relative_line_number(None, cx); - if let Some(vim) = Vim::globals(cx).focused_vim() - && vim.entity_id() == cx.entity().entity_id() - { - Vim::globals(cx).focused_vim = None; - } - } - - /// Register an action on the editor. - pub fn action( - editor: &mut Editor, - cx: &mut Context, - f: impl Fn(&mut Vim, &A, &mut Window, &mut Context) + 'static, - ) { - let subscription = editor.register_action(cx.listener(f)); - cx.on_release(|_, _| drop(subscription)).detach(); - } - - pub fn editor(&self) -> Option> { - self.editor.upgrade() - } - - pub fn workspace(&self, window: &mut Window) -> Option> { - window.root::().flatten() - } - - pub fn pane(&self, window: &mut Window, cx: &mut Context) -> Option> { - self.workspace(window) - .map(|workspace| workspace.read(cx).focused_pane(window, cx)) - } - - pub fn enabled(cx: &mut App) -> bool { - VimModeSetting::get_global(cx).0 || HelixModeSetting::get_global(cx).0 - } - - /// Called whenever an keystroke is typed so vim can observe all actions - /// and keystrokes accordingly. - fn observe_keystrokes( - &mut self, - keystroke_event: &KeystrokeEvent, - window: &mut Window, - cx: &mut Context, - ) { - if self.exit_temporary_mode { - self.exit_temporary_mode = false; - // Don't switch to insert mode if the action is temporary_normal. - if let Some(action) = keystroke_event.action.as_ref() - && action.as_any().downcast_ref::().is_some() - { - return; - } - self.switch_mode(Mode::Insert, false, window, cx) - } - if let Some(action) = keystroke_event.action.as_ref() { - // Keystroke is handled by the vim system, so continue forward - if action.name().starts_with("vim::") { - self.update_editor(cx, |_, editor, cx| { - editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx) - }); - - return; - } - } else if window.has_pending_keystrokes() || keystroke_event.keystroke.is_ime_in_progress() - { - return; - } - - if let Some(operator) = self.active_operator() { - match operator { - Operator::Literal { prefix } => { - self.handle_literal_keystroke( - keystroke_event, - prefix.unwrap_or_default(), - window, - cx, - ); - } - _ if !operator.is_waiting(self.mode) => { - self.clear_operator(window, cx); - self.stop_recording_immediately(Box::new(ClearOperators), cx) - } - _ => {} - } - } - } - - fn handle_editor_event( - &mut self, - event: &EditorEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - EditorEvent::Focused => self.focused(true, window, cx), - EditorEvent::Blurred => self.blurred(window, cx), - EditorEvent::SelectionsChanged { local: true } => { - self.local_selections_changed(window, cx); - } - EditorEvent::InputIgnored { text } => { - self.input_ignored(text.clone(), window, cx); - Vim::globals(cx).observe_insertion(text, None) - } - EditorEvent::InputHandled { - text, - utf16_range_to_replace: range_to_replace, - } => Vim::globals(cx).observe_insertion(text, range_to_replace.clone()), - EditorEvent::TransactionBegun { transaction_id } => { - self.transaction_begun(*transaction_id, window, cx) - } - EditorEvent::TransactionUndone { transaction_id } => { - self.transaction_undone(transaction_id, window, cx) - } - EditorEvent::Edited { .. } => self.push_to_change_list(window, cx), - EditorEvent::FocusedIn => self.sync_vim_settings(window, cx), - EditorEvent::CursorShapeChanged => self.cursor_shape_changed(window, cx), - EditorEvent::PushedToNavHistory { - anchor, - is_deactivate, - } => { - self.update_editor(cx, |vim, editor, cx| { - let mark = if *is_deactivate { - "\"".to_string() - } else { - "'".to_string() - }; - vim.set_mark(mark, vec![*anchor], editor.buffer(), window, cx); - }); - } - _ => {} - } - } - - fn push_operator(&mut self, operator: Operator, window: &mut Window, cx: &mut Context) { - if operator.starts_dot_recording() { - self.start_recording(cx); - } - // Since these operations can only be entered with pre-operators, - // we need to clear the previous operators when pushing, - // so that the current stack is the most correct - if matches!( - operator, - Operator::AddSurrounds { .. } - | Operator::ChangeSurrounds { .. } - | Operator::DeleteSurrounds - | Operator::Exchange - ) { - self.operator_stack.clear(); - }; - self.operator_stack.push(operator); - self.sync_vim_settings(window, cx); - } - - pub fn switch_mode( - &mut self, - mode: Mode, - leave_selections: bool, - window: &mut Window, - cx: &mut Context, - ) { - if self.temp_mode && mode == Mode::Normal { - self.temp_mode = false; - self.switch_mode(Mode::Normal, leave_selections, window, cx); - self.switch_mode(Mode::Insert, false, window, cx); - return; - } else if self.temp_mode - && !matches!(mode, Mode::Visual | Mode::VisualLine | Mode::VisualBlock) - { - self.temp_mode = false; - } - - let last_mode = self.mode; - let prior_mode = self.last_mode; - let prior_tx = self.current_tx; - self.status_label.take(); - self.last_mode = last_mode; - self.mode = mode; - self.operator_stack.clear(); - self.selected_register.take(); - self.cancel_running_command(window, cx); - if mode == Mode::Normal || mode != last_mode { - self.current_tx.take(); - self.current_anchor.take(); - self.update_editor(cx, |_, editor, _| { - editor.clear_selection_drag_state(); - }); - } - Vim::take_forced_motion(cx); - if mode != Mode::Insert && mode != Mode::Replace { - Vim::take_count(cx); - } - - // Sync editor settings like clip mode - self.sync_vim_settings(window, cx); - - if VimSettings::get_global(cx).toggle_relative_line_numbers - && self.mode != self.last_mode - && (self.mode == Mode::Insert || self.last_mode == Mode::Insert) - { - self.update_editor(cx, |vim, editor, cx| { - let is_relative = vim.mode != Mode::Insert; - editor.set_relative_line_number(Some(is_relative), cx) - }); - } - if HelixModeSetting::get_global(cx).0 { - if self.mode == Mode::Normal { - self.mode = Mode::HelixNormal - } else if self.mode == Mode::Visual { - self.mode = Mode::HelixSelect - } - } - - if leave_selections { - return; - } - - if !mode.is_visual() && last_mode.is_visual() { - self.create_visual_marks(last_mode, window, cx); - } - - // Adjust selections - self.update_editor(cx, |vim, editor, cx| { - if last_mode != Mode::VisualBlock && last_mode.is_visual() && mode == Mode::VisualBlock - { - vim.visual_block_motion(true, editor, window, cx, |_, point, goal| { - Some((point, goal)) - }) - } - if (last_mode == Mode::Insert || last_mode == Mode::Replace) - && let Some(prior_tx) = prior_tx - { - editor.group_until_transaction(prior_tx, cx) - } - - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - // we cheat with visual block mode and use multiple cursors. - // the cost of this cheat is we need to convert back to a single - // cursor whenever vim would. - if last_mode == Mode::VisualBlock - && (mode != Mode::VisualBlock && mode != Mode::Insert) - { - let tail = s.oldest_anchor().tail(); - let head = s.newest_anchor().head(); - s.select_anchor_ranges(vec![tail..head]); - } else if last_mode == Mode::Insert - && prior_mode == Mode::VisualBlock - && mode != Mode::VisualBlock - { - let pos = s.first_anchor().head(); - s.select_anchor_ranges(vec![pos..pos]) - } - - let snapshot = s.display_snapshot(); - if let Some(pending) = s.pending_anchor_mut() - && pending.reversed - && mode.is_visual() - && !last_mode.is_visual() - { - let mut end = pending.end.to_point(&snapshot.buffer_snapshot()); - end = snapshot - .buffer_snapshot() - .clip_point(end + Point::new(0, 1), Bias::Right); - pending.end = snapshot.buffer_snapshot().anchor_before(end); - } - - s.move_with(|map, selection| { - if last_mode.is_visual() && !mode.is_visual() { - let mut point = selection.head(); - if !selection.reversed && !selection.is_empty() { - point = movement::left(map, selection.head()); - } else if selection.is_empty() { - point = map.clip_point(point, Bias::Left); - } - selection.collapse_to(point, selection.goal) - } else if !last_mode.is_visual() && mode.is_visual() && selection.is_empty() { - selection.end = movement::right(map, selection.start); - } - }); - }) - }); - } - - pub fn take_count(cx: &mut App) -> Option { - let global_state = cx.global_mut::(); - if global_state.dot_replaying { - return global_state.recorded_count; - } - - let count = if global_state.post_count.is_none() && global_state.pre_count.is_none() { - return None; - } else { - Some( - global_state.post_count.take().unwrap_or(1) - * global_state.pre_count.take().unwrap_or(1), - ) - }; - - if global_state.dot_recording { - global_state.recording_count = count; - } - count - } - - pub fn take_forced_motion(cx: &mut App) -> bool { - let global_state = cx.global_mut::(); - let forced_motion = global_state.forced_motion; - global_state.forced_motion = false; - forced_motion - } - - pub fn cursor_shape(&self, cx: &mut App) -> CursorShape { - let cursor_shape = VimSettings::get_global(cx).cursor_shape; - match self.mode { - Mode::Normal => { - if let Some(operator) = self.operator_stack.last() { - match operator { - // Navigation operators -> Block cursor - Operator::FindForward { .. } - | Operator::FindBackward { .. } - | Operator::Mark - | Operator::Jump { .. } - | Operator::Register - | Operator::RecordRegister - | Operator::ReplayRegister => CursorShape::Block, - - // All other operators -> Underline cursor - _ => CursorShape::Underline, - } - } else { - cursor_shape.normal.unwrap_or(CursorShape::Block) - } - } - Mode::HelixNormal => cursor_shape.normal.unwrap_or(CursorShape::Block), - Mode::Replace => cursor_shape.replace.unwrap_or(CursorShape::Underline), - Mode::Visual | Mode::VisualLine | Mode::VisualBlock | Mode::HelixSelect => { - cursor_shape.visual.unwrap_or(CursorShape::Block) - } - Mode::Insert => cursor_shape.insert.unwrap_or({ - let editor_settings = EditorSettings::get_global(cx); - editor_settings.cursor_shape.unwrap_or_default() - }), - } - } - - pub fn editor_input_enabled(&self) -> bool { - match self.mode { - Mode::Insert => { - if let Some(operator) = self.operator_stack.last() { - !operator.is_waiting(self.mode) - } else { - true - } - } - Mode::Normal - | Mode::HelixNormal - | Mode::Replace - | Mode::Visual - | Mode::VisualLine - | Mode::VisualBlock - | Mode::HelixSelect => false, - } - } - - pub fn should_autoindent(&self) -> bool { - !(self.mode == Mode::Insert && self.last_mode == Mode::VisualBlock) - } - - pub fn clip_at_line_ends(&self) -> bool { - match self.mode { - Mode::Insert - | Mode::Visual - | Mode::VisualLine - | Mode::VisualBlock - | Mode::Replace - | Mode::HelixNormal - | Mode::HelixSelect => false, - Mode::Normal => true, - } - } - - pub fn extend_key_context(&self, context: &mut KeyContext, cx: &App) { - let mut mode = match self.mode { - Mode::Normal => "normal", - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => "visual", - Mode::Insert => "insert", - Mode::Replace => "replace", - Mode::HelixNormal => "helix_normal", - Mode::HelixSelect => "helix_select", - } - .to_string(); - - let mut operator_id = "none"; - - let active_operator = self.active_operator(); - if active_operator.is_none() && cx.global::().pre_count.is_some() - || active_operator.is_some() && cx.global::().post_count.is_some() - { - context.add("VimCount"); - } - - if let Some(active_operator) = active_operator { - if active_operator.is_waiting(self.mode) { - if matches!(active_operator, Operator::Literal { .. }) { - mode = "literal".to_string(); - } else { - mode = "waiting".to_string(); - } - } else { - operator_id = active_operator.id(); - mode = "operator".to_string(); - } - } - - if mode == "normal" - || mode == "visual" - || mode == "operator" - || mode == "helix_normal" - || mode == "helix_select" - { - context.add("VimControl"); - } - context.set("vim_mode", mode); - context.set("vim_operator", operator_id); - } - - fn focused(&mut self, preserve_selection: bool, window: &mut Window, cx: &mut Context) { - let Some(editor) = self.editor() else { - return; - }; - let newest_selection_empty = editor.update(cx, |editor, cx| { - editor - .selections - .newest::(&editor.display_snapshot(cx)) - .is_empty() - }); - let editor = editor.read(cx); - let editor_mode = editor.mode(); - - if editor_mode.is_full() - && !newest_selection_empty - && self.mode == Mode::Normal - // When following someone, don't switch vim mode. - && editor.leader_id().is_none() - { - if preserve_selection { - self.switch_mode(Mode::Visual, true, window, cx); - } else { - self.update_editor(cx, |_, editor, cx| { - editor.set_clip_at_line_ends(false, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|_, selection| { - selection.collapse_to(selection.start, selection.goal) - }) - }); - }); - } - } - - cx.emit(VimEvent::Focused); - self.sync_vim_settings(window, cx); - - if VimSettings::get_global(cx).toggle_relative_line_numbers { - if let Some(old_vim) = Vim::globals(cx).focused_vim() { - if old_vim.entity_id() != cx.entity().entity_id() { - old_vim.update(cx, |vim, cx| { - vim.update_editor(cx, |_, editor, cx| { - editor.set_relative_line_number(None, cx) - }); - }); - - self.update_editor(cx, |vim, editor, cx| { - let is_relative = vim.mode != Mode::Insert; - editor.set_relative_line_number(Some(is_relative), cx) - }); - } - } else { - self.update_editor(cx, |vim, editor, cx| { - let is_relative = vim.mode != Mode::Insert; - editor.set_relative_line_number(Some(is_relative), cx) - }); - } - } - Vim::globals(cx).focused_vim = Some(cx.entity().downgrade()); - } - - fn blurred(&mut self, window: &mut Window, cx: &mut Context) { - self.stop_recording_immediately(NormalBefore.boxed_clone(), cx); - self.store_visual_marks(window, cx); - self.clear_operator(window, cx); - self.update_editor(cx, |vim, editor, cx| { - if vim.cursor_shape(cx) == CursorShape::Block { - editor.set_cursor_shape(CursorShape::Hollow, cx); - } - }); - } - - fn cursor_shape_changed(&mut self, _: &mut Window, cx: &mut Context) { - self.update_editor(cx, |vim, editor, cx| { - editor.set_cursor_shape(vim.cursor_shape(cx), cx); - }); - } - - fn update_editor( - &mut self, - cx: &mut Context, - update: impl FnOnce(&mut Self, &mut Editor, &mut Context) -> S, - ) -> Option { - let editor = self.editor.upgrade()?; - Some(editor.update(cx, |editor, cx| update(self, editor, cx))) - } - - fn editor_selections(&mut self, _: &mut Window, cx: &mut Context) -> Vec> { - self.update_editor(cx, |_, editor, _| { - editor - .selections - .disjoint_anchors_arc() - .iter() - .map(|selection| selection.tail()..selection.head()) - .collect() - }) - .unwrap_or_default() - } - - fn editor_cursor_word( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Option { - self.update_editor(cx, |_, editor, cx| { - let snapshot = &editor.snapshot(window, cx); - let selection = editor - .selections - .newest::(&snapshot.display_snapshot); - - let snapshot = snapshot.buffer_snapshot(); - let (range, kind) = - snapshot.surrounding_word(selection.start, Some(CharScopeContext::Completion)); - if kind == Some(CharKind::Word) { - let text: String = snapshot.text_for_range(range).collect(); - if !text.trim().is_empty() { - return Some(text); - } - } - - None - }) - .unwrap_or_default() - } - - /// When doing an action that modifies the buffer, we start recording so that `.` - /// will replay the action. - pub fn start_recording(&mut self, cx: &mut Context) { - Vim::update_globals(cx, |globals, cx| { - if !globals.dot_replaying { - globals.dot_recording = true; - globals.recording_actions = Default::default(); - globals.recording_count = None; - - let selections = self.editor().map(|editor| { - editor.update(cx, |editor, cx| { - let snapshot = editor.display_snapshot(cx); - - ( - editor.selections.oldest::(&snapshot), - editor.selections.newest::(&snapshot), - ) - }) - }); - - if let Some((oldest, newest)) = selections { - globals.recorded_selection = match self.mode { - Mode::Visual if newest.end.row == newest.start.row => { - RecordedSelection::SingleLine { - cols: newest.end.column - newest.start.column, - } - } - Mode::Visual => RecordedSelection::Visual { - rows: newest.end.row - newest.start.row, - cols: newest.end.column, - }, - Mode::VisualLine => RecordedSelection::VisualLine { - rows: newest.end.row - newest.start.row, - }, - Mode::VisualBlock => RecordedSelection::VisualBlock { - rows: newest.end.row.abs_diff(oldest.start.row), - cols: newest.end.column.abs_diff(oldest.start.column), - }, - _ => RecordedSelection::None, - } - } else { - globals.recorded_selection = RecordedSelection::None; - } - } - }) - } - - pub fn stop_replaying(&mut self, cx: &mut Context) { - let globals = Vim::globals(cx); - globals.dot_replaying = false; - if let Some(replayer) = globals.replayer.take() { - replayer.stop(); - } - } - - /// When finishing an action that modifies the buffer, stop recording. - /// as you usually call this within a keystroke handler we also ensure that - /// the current action is recorded. - pub fn stop_recording(&mut self, cx: &mut Context) { - let globals = Vim::globals(cx); - if globals.dot_recording { - globals.stop_recording_after_next_action = true; - } - self.exit_temporary_mode = self.temp_mode; - } - - /// Stops recording actions immediately rather than waiting until after the - /// next action to stop recording. - /// - /// This doesn't include the current action. - pub fn stop_recording_immediately(&mut self, action: Box, cx: &mut Context) { - let globals = Vim::globals(cx); - if globals.dot_recording { - globals - .recording_actions - .push(ReplayableAction::Action(action.boxed_clone())); - globals.recorded_actions = mem::take(&mut globals.recording_actions); - globals.recorded_count = globals.recording_count.take(); - globals.dot_recording = false; - globals.stop_recording_after_next_action = false; - } - self.exit_temporary_mode = self.temp_mode; - } - - /// Explicitly record one action (equivalents to start_recording and stop_recording) - pub fn record_current_action(&mut self, cx: &mut Context) { - self.start_recording(cx); - self.stop_recording(cx); - } - - fn push_count_digit(&mut self, number: usize, window: &mut Window, cx: &mut Context) { - if self.active_operator().is_some() { - let post_count = Vim::globals(cx).post_count.unwrap_or(0); - - Vim::globals(cx).post_count = Some( - post_count - .checked_mul(10) - .and_then(|post_count| post_count.checked_add(number)) - .filter(|post_count| *post_count < isize::MAX as usize) - .unwrap_or(post_count), - ) - } else { - let pre_count = Vim::globals(cx).pre_count.unwrap_or(0); - - Vim::globals(cx).pre_count = Some( - pre_count - .checked_mul(10) - .and_then(|pre_count| pre_count.checked_add(number)) - .filter(|pre_count| *pre_count < isize::MAX as usize) - .unwrap_or(pre_count), - ) - } - // update the keymap so that 0 works - self.sync_vim_settings(window, cx) - } - - fn select_register(&mut self, register: Arc, window: &mut Window, cx: &mut Context) { - if register.chars().count() == 1 { - self.selected_register - .replace(register.chars().next().unwrap()); - } - self.operator_stack.clear(); - self.sync_vim_settings(window, cx); - } - - fn maybe_pop_operator(&mut self) -> Option { - self.operator_stack.pop() - } - - fn pop_operator(&mut self, window: &mut Window, cx: &mut Context) -> Operator { - let popped_operator = self.operator_stack.pop() - .expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config"); - self.sync_vim_settings(window, cx); - popped_operator - } - - fn clear_operator(&mut self, window: &mut Window, cx: &mut Context) { - Vim::take_count(cx); - Vim::take_forced_motion(cx); - self.selected_register.take(); - self.operator_stack.clear(); - self.sync_vim_settings(window, cx); - } - - fn active_operator(&self) -> Option { - self.operator_stack.last().cloned() - } - - fn transaction_begun( - &mut self, - transaction_id: TransactionId, - _window: &mut Window, - _: &mut Context, - ) { - let mode = if (self.mode == Mode::Insert - || self.mode == Mode::Replace - || self.mode == Mode::Normal) - && self.current_tx.is_none() - { - self.current_tx = Some(transaction_id); - self.last_mode - } else { - self.mode - }; - if mode == Mode::VisualLine || mode == Mode::VisualBlock { - self.undo_modes.insert(transaction_id, mode); - } - } - - fn transaction_undone( - &mut self, - transaction_id: &TransactionId, - window: &mut Window, - cx: &mut Context, - ) { - match self.mode { - Mode::VisualLine | Mode::VisualBlock | Mode::Visual | Mode::HelixSelect => { - self.update_editor(cx, |vim, editor, cx| { - let original_mode = vim.undo_modes.get(transaction_id); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - match original_mode { - Some(Mode::VisualLine) => { - s.move_with(|map, selection| { - selection.collapse_to( - map.prev_line_boundary(selection.start.to_point(map)).1, - SelectionGoal::None, - ) - }); - } - Some(Mode::VisualBlock) => { - let mut first = s.first_anchor(); - first.collapse_to(first.start, first.goal); - s.select_anchors(vec![first]); - } - _ => { - s.move_with(|map, selection| { - selection.collapse_to( - map.clip_at_line_end(selection.start), - selection.goal, - ); - }); - } - } - }); - }); - self.switch_mode(Mode::Normal, true, window, cx) - } - Mode::Normal => { - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - selection - .collapse_to(map.clip_at_line_end(selection.end), selection.goal) - }) - }) - }); - } - Mode::Insert | Mode::Replace | Mode::HelixNormal => {} - } - } - - fn local_selections_changed(&mut self, window: &mut Window, cx: &mut Context) { - let Some(editor) = self.editor() else { return }; - - if editor.read(cx).leader_id().is_some() { - return; - } - - let newest = editor.read(cx).selections.newest_anchor().clone(); - let is_multicursor = editor.read(cx).selections.count() > 1; - if self.mode == Mode::Insert && self.current_tx.is_some() { - if self.current_anchor.is_none() { - self.current_anchor = Some(newest); - } else if self.current_anchor.as_ref().unwrap() != &newest - && let Some(tx_id) = self.current_tx.take() - { - self.update_editor(cx, |_, editor, cx| { - editor.group_until_transaction(tx_id, cx) - }); - } - } else if self.mode == Mode::Normal && newest.start != newest.end { - if matches!(newest.goal, SelectionGoal::HorizontalRange { .. }) { - self.switch_mode(Mode::VisualBlock, false, window, cx); - } else { - self.switch_mode(Mode::Visual, false, window, cx) - } - } else if newest.start == newest.end - && !is_multicursor - && [Mode::Visual, Mode::VisualLine, Mode::VisualBlock].contains(&self.mode) - { - self.switch_mode(Mode::Normal, false, window, cx); - } - } - - fn input_ignored(&mut self, text: Arc, window: &mut Window, cx: &mut Context) { - if text.is_empty() { - return; - } - - match self.active_operator() { - Some(Operator::FindForward { before, multiline }) => { - let find = Motion::FindForward { - before, - char: text.chars().next().unwrap(), - mode: if multiline { - FindRange::MultiLine - } else { - FindRange::SingleLine - }, - smartcase: VimSettings::get_global(cx).use_smartcase_find, - }; - Vim::globals(cx).last_find = Some(find.clone()); - self.motion(find, window, cx) - } - Some(Operator::FindBackward { after, multiline }) => { - let find = Motion::FindBackward { - after, - char: text.chars().next().unwrap(), - mode: if multiline { - FindRange::MultiLine - } else { - FindRange::SingleLine - }, - smartcase: VimSettings::get_global(cx).use_smartcase_find, - }; - Vim::globals(cx).last_find = Some(find.clone()); - self.motion(find, window, cx) - } - Some(Operator::Sneak { first_char }) => { - if let Some(first_char) = first_char { - if let Some(second_char) = text.chars().next() { - let sneak = Motion::Sneak { - first_char, - second_char, - smartcase: VimSettings::get_global(cx).use_smartcase_find, - }; - Vim::globals(cx).last_find = Some(sneak.clone()); - self.motion(sneak, window, cx) - } - } else { - let first_char = text.chars().next(); - self.pop_operator(window, cx); - self.push_operator(Operator::Sneak { first_char }, window, cx); - } - } - Some(Operator::SneakBackward { first_char }) => { - if let Some(first_char) = first_char { - if let Some(second_char) = text.chars().next() { - let sneak = Motion::SneakBackward { - first_char, - second_char, - smartcase: VimSettings::get_global(cx).use_smartcase_find, - }; - Vim::globals(cx).last_find = Some(sneak.clone()); - self.motion(sneak, window, cx) - } - } else { - let first_char = text.chars().next(); - self.pop_operator(window, cx); - self.push_operator(Operator::SneakBackward { first_char }, window, cx); - } - } - Some(Operator::Replace) => match self.mode { - Mode::Normal => self.normal_replace(text, window, cx), - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { - self.visual_replace(text, window, cx) - } - Mode::HelixNormal => self.helix_replace(&text, window, cx), - _ => self.clear_operator(window, cx), - }, - Some(Operator::Digraph { first_char }) => { - if let Some(first_char) = first_char { - if let Some(second_char) = text.chars().next() { - self.insert_digraph(first_char, second_char, window, cx); - } - } else { - let first_char = text.chars().next(); - self.pop_operator(window, cx); - self.push_operator(Operator::Digraph { first_char }, window, cx); - } - } - Some(Operator::Literal { prefix }) => { - self.handle_literal_input(prefix.unwrap_or_default(), &text, window, cx) - } - Some(Operator::AddSurrounds { target }) => match self.mode { - Mode::Normal => { - if let Some(target) = target { - self.add_surrounds(text, target, window, cx); - self.clear_operator(window, cx); - } - } - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { - self.add_surrounds(text, SurroundsType::Selection, window, cx); - self.clear_operator(window, cx); - } - _ => self.clear_operator(window, cx), - }, - Some(Operator::ChangeSurrounds { target, opening }) => match self.mode { - Mode::Normal => { - if let Some(target) = target { - self.change_surrounds(text, target, opening, window, cx); - self.clear_operator(window, cx); - } - } - _ => self.clear_operator(window, cx), - }, - Some(Operator::DeleteSurrounds) => match self.mode { - Mode::Normal => { - self.delete_surrounds(text, window, cx); - self.clear_operator(window, cx); - } - _ => self.clear_operator(window, cx), - }, - Some(Operator::Mark) => self.create_mark(text, window, cx), - Some(Operator::RecordRegister) => { - self.record_register(text.chars().next().unwrap(), window, cx) - } - Some(Operator::ReplayRegister) => { - self.replay_register(text.chars().next().unwrap(), window, cx) - } - Some(Operator::Register) => match self.mode { - Mode::Insert => { - self.update_editor(cx, |_, editor, cx| { - if let Some(register) = Vim::update_globals(cx, |globals, cx| { - globals.read_register(text.chars().next(), Some(editor), cx) - }) { - editor.do_paste( - ®ister.text.to_string(), - register.clipboard_selections, - false, - window, - cx, - ) - } - }); - self.clear_operator(window, cx); - } - _ => { - self.select_register(text, window, cx); - } - }, - Some(Operator::Jump { line }) => self.jump(text, line, true, window, cx), - _ => { - if self.mode == Mode::Replace { - self.multi_replace(text, window, cx) - } - - if self.mode == Mode::Normal { - self.update_editor(cx, |_, editor, cx| { - editor.accept_edit_prediction( - &editor::actions::AcceptEditPrediction {}, - window, - cx, - ); - }); - } - } - } - } - - fn sync_vim_settings(&mut self, window: &mut Window, cx: &mut Context) { - self.update_editor(cx, |vim, editor, cx| { - editor.set_cursor_shape(vim.cursor_shape(cx), cx); - editor.set_clip_at_line_ends(vim.clip_at_line_ends(), cx); - let collapse_matches = !HelixModeSetting::get_global(cx).0; - editor.set_collapse_matches(collapse_matches); - editor.set_input_enabled(vim.editor_input_enabled()); - editor.set_autoindent(vim.should_autoindent()); - editor - .selections - .set_line_mode(matches!(vim.mode, Mode::VisualLine)); - - let hide_edit_predictions = !matches!(vim.mode, Mode::Insert | Mode::Replace); - editor.set_edit_predictions_hidden_for_vim_mode(hide_edit_predictions, window, cx); - }); - cx.notify() - } -} - -#[derive(RegisterSetting)] -struct VimSettings { - pub default_mode: Mode, - pub toggle_relative_line_numbers: bool, - pub use_system_clipboard: settings::UseSystemClipboard, - pub use_smartcase_find: bool, - pub custom_digraphs: HashMap>, - pub highlight_on_yank_duration: u64, - pub cursor_shape: CursorShapeSettings, -} - -/// The settings for cursor shape. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct CursorShapeSettings { - /// Cursor shape for the normal mode. - /// - /// Default: block - pub normal: Option, - /// Cursor shape for the replace mode. - /// - /// Default: underline - pub replace: Option, - /// Cursor shape for the visual mode. - /// - /// Default: block - pub visual: Option, - /// Cursor shape for the insert mode. - /// - /// The default value follows the primary cursor_shape. - pub insert: Option, -} - -impl From for CursorShapeSettings { - fn from(settings: settings::CursorShapeSettings) -> Self { - Self { - normal: settings.normal.map(Into::into), - replace: settings.replace.map(Into::into), - visual: settings.visual.map(Into::into), - insert: settings.insert.map(Into::into), - } - } -} - -impl From for Mode { - fn from(mode: ModeContent) -> Self { - match mode { - ModeContent::Normal => Self::Normal, - ModeContent::Insert => Self::Insert, - } - } -} - -impl Settings for VimSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let vim = content.vim.clone().unwrap(); - Self { - default_mode: vim.default_mode.unwrap().into(), - toggle_relative_line_numbers: vim.toggle_relative_line_numbers.unwrap(), - use_system_clipboard: vim.use_system_clipboard.unwrap(), - use_smartcase_find: vim.use_smartcase_find.unwrap(), - custom_digraphs: vim.custom_digraphs.unwrap(), - highlight_on_yank_duration: vim.highlight_on_yank_duration.unwrap(), - cursor_shape: vim.cursor_shape.unwrap().into(), - } - } -} diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs deleted file mode 100644 index 3c6f237435..0000000000 --- a/crates/vim/src/visual.rs +++ /dev/null @@ -1,1978 +0,0 @@ -use std::sync::Arc; - -use collections::HashMap; -use editor::{ - Bias, DisplayPoint, Editor, MultiBufferOffset, SelectionEffects, - display_map::{DisplaySnapshot, ToDisplayPoint}, - movement, -}; -use gpui::{Context, Window, actions}; -use language::{Point, Selection, SelectionGoal}; -use multi_buffer::MultiBufferRow; -use search::BufferSearchBar; -use util::ResultExt; -use workspace::searchable::Direction; - -use crate::{ - Vim, - motion::{Motion, MotionKind, first_non_whitespace, next_line_end, start_of_line}, - object::Object, - state::{Mark, Mode, Operator}, -}; - -actions!( - vim, - [ - /// Toggles visual mode. - ToggleVisual, - /// Toggles visual line mode. - ToggleVisualLine, - /// Toggles visual block mode. - ToggleVisualBlock, - /// Deletes the visual selection. - VisualDelete, - /// Deletes entire lines in visual selection. - VisualDeleteLine, - /// Yanks (copies) the visual selection. - VisualYank, - /// Yanks entire lines in visual selection. - VisualYankLine, - /// Moves cursor to the other end of the selection. - OtherEnd, - /// Moves cursor to the other end of the selection (row-aware). - OtherEndRowAware, - /// Selects the next occurrence of the current selection. - SelectNext, - /// Selects the previous occurrence of the current selection. - SelectPrevious, - /// Selects the next match of the current selection. - SelectNextMatch, - /// Selects the previous match of the current selection. - SelectPreviousMatch, - /// Selects the next smaller syntax node. - SelectSmallerSyntaxNode, - /// Selects the next larger syntax node. - SelectLargerSyntaxNode, - /// Selects the next syntax node sibling. - SelectNextSyntaxNode, - /// Selects the previous syntax node sibling. - SelectPreviousSyntaxNode, - /// Restores the previous visual selection. - RestoreVisualSelection, - /// Inserts at the end of each line in visual selection. - VisualInsertEndOfLine, - /// Inserts at the first non-whitespace character of each line. - VisualInsertFirstNonWhiteSpace, - ] -); - -pub fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, |vim, _: &ToggleVisual, window, cx| { - vim.toggle_mode(Mode::Visual, window, cx) - }); - Vim::action(editor, cx, |vim, _: &ToggleVisualLine, window, cx| { - vim.toggle_mode(Mode::VisualLine, window, cx) - }); - Vim::action(editor, cx, |vim, _: &ToggleVisualBlock, window, cx| { - vim.toggle_mode(Mode::VisualBlock, window, cx) - }); - Vim::action(editor, cx, Vim::other_end); - Vim::action(editor, cx, Vim::other_end_row_aware); - Vim::action(editor, cx, Vim::visual_insert_end_of_line); - Vim::action(editor, cx, Vim::visual_insert_first_non_white_space); - Vim::action(editor, cx, |vim, _: &VisualDelete, window, cx| { - vim.record_current_action(cx); - vim.visual_delete(false, window, cx); - }); - Vim::action(editor, cx, |vim, _: &VisualDeleteLine, window, cx| { - vim.record_current_action(cx); - vim.visual_delete(true, window, cx); - }); - Vim::action(editor, cx, |vim, _: &VisualYank, window, cx| { - vim.visual_yank(false, window, cx) - }); - Vim::action(editor, cx, |vim, _: &VisualYankLine, window, cx| { - vim.visual_yank(true, window, cx) - }); - - Vim::action(editor, cx, Vim::select_next); - Vim::action(editor, cx, Vim::select_previous); - Vim::action(editor, cx, |vim, _: &SelectNextMatch, window, cx| { - vim.select_match(Direction::Next, window, cx); - }); - Vim::action(editor, cx, |vim, _: &SelectPreviousMatch, window, cx| { - vim.select_match(Direction::Prev, window, cx); - }); - - Vim::action(editor, cx, |vim, _: &SelectLargerSyntaxNode, window, cx| { - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - for _ in 0..count { - vim.update_editor(cx, |_, editor, cx| { - editor.select_larger_syntax_node(&Default::default(), window, cx); - }); - } - }); - - Vim::action(editor, cx, |vim, _: &SelectNextSyntaxNode, window, cx| { - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - for _ in 0..count { - vim.update_editor(cx, |_, editor, cx| { - editor.select_next_syntax_node(&Default::default(), window, cx); - }); - } - }); - - Vim::action( - editor, - cx, - |vim, _: &SelectPreviousSyntaxNode, window, cx| { - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - for _ in 0..count { - vim.update_editor(cx, |_, editor, cx| { - editor.select_prev_syntax_node(&Default::default(), window, cx); - }); - } - }, - ); - - Vim::action( - editor, - cx, - |vim, _: &SelectSmallerSyntaxNode, window, cx| { - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - for _ in 0..count { - vim.update_editor(cx, |_, editor, cx| { - editor.select_smaller_syntax_node(&Default::default(), window, cx); - }); - } - }, - ); - - Vim::action(editor, cx, |vim, _: &RestoreVisualSelection, window, cx| { - let Some((stored_mode, reversed)) = vim.stored_visual_mode.take() else { - return; - }; - let marks = vim - .update_editor(cx, |vim, editor, cx| { - vim.get_mark("<", editor, window, cx) - .zip(vim.get_mark(">", editor, window, cx)) - }) - .flatten(); - let Some((Mark::Local(start), Mark::Local(end))) = marks else { - return; - }; - let ranges = start - .iter() - .zip(end) - .zip(reversed) - .map(|((start, end), reversed)| (*start, end, reversed)) - .collect::>(); - - if vim.mode.is_visual() { - vim.create_visual_marks(vim.mode, window, cx); - } - - vim.update_editor(cx, |_, editor, cx| { - editor.set_clip_at_line_ends(false, cx); - editor.change_selections(Default::default(), window, cx, |s| { - let map = s.display_snapshot(); - let ranges = ranges - .into_iter() - .map(|(start, end, reversed)| { - let mut new_end = - movement::saturating_right(&map, end.to_display_point(&map)); - let mut new_start = start.to_display_point(&map); - if new_start >= new_end { - if new_end.column() == 0 { - new_end = movement::right(&map, new_end) - } else { - new_start = movement::saturating_left(&map, new_end); - } - } - Selection { - id: s.new_selection_id(), - start: new_start.to_point(&map), - end: new_end.to_point(&map), - reversed, - goal: SelectionGoal::None, - } - }) - .collect(); - s.select(ranges); - }) - }); - vim.switch_mode(stored_mode, true, window, cx) - }); -} - -impl Vim { - pub fn visual_motion( - &mut self, - motion: Motion, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |vim, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - if vim.mode == Mode::VisualBlock - && !matches!( - motion, - Motion::EndOfLine { - display_lines: false - } - ) - { - let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. }); - vim.visual_block_motion(is_up_or_down, editor, window, cx, |map, point, goal| { - motion.move_point(map, point, goal, times, &text_layout_details) - }) - } else { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let was_reversed = selection.reversed; - let mut current_head = selection.head(); - - // our motions assume the current character is after the cursor, - // but in (forward) visual mode the current character is just - // before the end of the selection. - - // If the file ends with a newline (which is common) we don't do this. - // so that if you go to the end of such a file you can use "up" to go - // to the previous line and have it work somewhat as expected. - if !selection.reversed - && !selection.is_empty() - && !(selection.end.column() == 0 && selection.end == map.max_point()) - { - current_head = movement::left(map, selection.end) - } - - let Some((new_head, goal)) = motion.move_point( - map, - current_head, - selection.goal, - times, - &text_layout_details, - ) else { - return; - }; - - selection.set_head(new_head, goal); - - // ensure the current character is included in the selection. - if !selection.reversed { - let next_point = if vim.mode == Mode::VisualBlock { - movement::saturating_right(map, selection.end) - } else { - movement::right(map, selection.end) - }; - - if !(next_point.column() == 0 && next_point == map.max_point()) { - selection.end = next_point; - } - } - - // vim always ensures the anchor character stays selected. - // if our selection has reversed, we need to move the opposite end - // to ensure the anchor is still selected. - if was_reversed && !selection.reversed { - selection.start = movement::left(map, selection.start); - } else if !was_reversed && selection.reversed { - selection.end = movement::right(map, selection.end); - } - }) - }); - } - }); - } - - pub fn visual_block_motion( - &mut self, - preserve_goal: bool, - editor: &mut Editor, - window: &mut Window, - cx: &mut Context, - mut move_selection: impl FnMut( - &DisplaySnapshot, - DisplayPoint, - SelectionGoal, - ) -> Option<(DisplayPoint, SelectionGoal)>, - ) { - let text_layout_details = editor.text_layout_details(window); - editor.change_selections(Default::default(), window, cx, |s| { - let map = &s.display_snapshot(); - let mut head = s.newest_anchor().head().to_display_point(map); - let mut tail = s.oldest_anchor().tail().to_display_point(map); - - let mut head_x = map.x_for_display_point(head, &text_layout_details); - let mut tail_x = map.x_for_display_point(tail, &text_layout_details); - - let (start, end) = match s.newest_anchor().goal { - SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end), - SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start), - _ => (tail_x.into(), head_x.into()), - }; - let mut goal = SelectionGoal::HorizontalRange { start, end }; - - let was_reversed = tail_x > head_x; - if !was_reversed && !preserve_goal { - head = movement::saturating_left(map, head); - } - - let reverse_aware_goal = if was_reversed { - SelectionGoal::HorizontalRange { - start: end, - end: start, - } - } else { - goal - }; - - let Some((new_head, _)) = move_selection(map, head, reverse_aware_goal) else { - return; - }; - head = new_head; - head_x = map.x_for_display_point(head, &text_layout_details); - - let is_reversed = tail_x > head_x; - if was_reversed && !is_reversed { - tail = movement::saturating_left(map, tail); - tail_x = map.x_for_display_point(tail, &text_layout_details); - } else if !was_reversed && is_reversed { - tail = movement::saturating_right(map, tail); - tail_x = map.x_for_display_point(tail, &text_layout_details); - } - if !is_reversed && !preserve_goal { - head = movement::saturating_right(map, head); - head_x = map.x_for_display_point(head, &text_layout_details); - } - - let positions = if is_reversed { - head_x..tail_x - } else { - tail_x..head_x - }; - - if !preserve_goal { - goal = SelectionGoal::HorizontalRange { - start: f64::from(positions.start), - end: f64::from(positions.end), - }; - } - - let mut selections = Vec::new(); - let mut row = tail.row(); - let going_up = tail.row() > head.row(); - let direction = if going_up { -1 } else { 1 }; - - loop { - let laid_out_line = map.layout_row(row, &text_layout_details); - let start = DisplayPoint::new( - row, - laid_out_line.closest_index_for_x(positions.start) as u32, - ); - let mut end = - DisplayPoint::new(row, laid_out_line.closest_index_for_x(positions.end) as u32); - if end <= start { - if start.column() == map.line_len(start.row()) { - end = start; - } else { - end = movement::saturating_right(map, start); - } - } - - if positions.start <= laid_out_line.width { - let selection = Selection { - id: s.new_selection_id(), - start: start.to_point(map), - end: end.to_point(map), - reversed: is_reversed && - // For neovim parity: cursor is not reversed when column is a single character - end.column() - start.column() > 1, - goal, - }; - - selections.push(selection); - } - - // When dealing with soft wrapped lines, it's possible that - // `row` ends up being set to a value other than `head.row()` as - // `head.row()` might be a `DisplayPoint` mapped to a soft - // wrapped line, hence the need for `<=` and `>=` instead of - // `==`. - if going_up && row <= head.row() || !going_up && row >= head.row() { - break; - } - - // Find the next or previous buffer row where the `row` should - // be moved to, so that wrapped lines are skipped. - row = map - .start_of_relative_buffer_row(DisplayPoint::new(row, 0), direction) - .row(); - } - - s.select(selections); - }) - } - - pub fn visual_object( - &mut self, - object: Object, - count: Option, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(Operator::Object { around }) = self.active_operator() { - self.pop_operator(window, cx); - let current_mode = self.mode; - let target_mode = object.target_visual_mode(current_mode, around); - if target_mode != current_mode { - self.switch_mode(target_mode, true, window, cx); - } - - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let mut mut_selection = selection.clone(); - - // all our motions assume that the current character is - // after the cursor; however in the case of a visual selection - // the current character is before the cursor. - // But this will affect the judgment of the html tag - // so the html tag needs to skip this logic. - if !selection.reversed && object != Object::Tag { - mut_selection.set_head( - movement::left(map, mut_selection.head()), - mut_selection.goal, - ); - } - - let original_point = selection.tail().to_point(map); - - if let Some(range) = object.range(map, mut_selection, around, count) { - if !range.is_empty() { - let expand_both_ways = object.always_expands_both_ways() - || selection.is_empty() - || movement::right(map, selection.start) == selection.end; - - if expand_both_ways { - if selection.start == range.start - && selection.end == range.end - && object.always_expands_both_ways() - { - if let Some(range) = - object.range(map, selection.clone(), around, count) - { - selection.start = range.start; - selection.end = range.end; - } - } else { - selection.start = range.start; - selection.end = range.end; - } - } else if selection.reversed { - selection.start = range.start; - } else { - selection.end = range.end; - } - } - - // In the visual selection result of a paragraph object, the cursor is - // placed at the start of the last line. And in the visual mode, the - // selection end is located after the end character. So, adjustment of - // selection end is needed. - // - // We don't do this adjustment for a one-line blank paragraph since the - // trailing newline is included in its selection from the beginning. - if object == Object::Paragraph && range.start != range.end { - let row_of_selection_end_line = selection.end.to_point(map).row; - let new_selection_end = if map - .buffer_snapshot() - .line_len(MultiBufferRow(row_of_selection_end_line)) - == 0 - { - Point::new(row_of_selection_end_line + 1, 0) - } else { - Point::new(row_of_selection_end_line, 1) - }; - selection.end = new_selection_end.to_display_point(map); - } - - // To match vim, if the range starts of the same line as it originally - // did, we keep the tail of the selection in the same place instead of - // snapping it to the start of the line - if target_mode == Mode::VisualLine { - let new_start_point = selection.start.to_point(map); - if new_start_point.row == original_point.row { - if selection.end.to_point(map).row > new_start_point.row { - if original_point.column - == map - .buffer_snapshot() - .line_len(MultiBufferRow(original_point.row)) - { - selection.start = movement::saturating_left( - map, - original_point.to_display_point(map), - ) - } else { - selection.start = original_point.to_display_point(map) - } - } else { - selection.end = movement::saturating_right( - map, - original_point.to_display_point(map), - ); - if original_point.column > 0 { - selection.reversed = true - } - } - } - } - } - }); - }); - }); - } - } - - fn visual_insert_end_of_line( - &mut self, - _: &VisualInsertEndOfLine, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |_, editor, cx| { - editor.split_selection_into_lines(&Default::default(), window, cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_cursors_with(|map, cursor, _| { - (next_line_end(map, cursor, 1), SelectionGoal::None) - }); - }); - }); - - self.switch_mode(Mode::Insert, false, window, cx); - } - - fn visual_insert_first_non_white_space( - &mut self, - _: &VisualInsertFirstNonWhiteSpace, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |_, editor, cx| { - editor.split_selection_into_lines(&Default::default(), window, cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_cursors_with(|map, cursor, _| { - ( - first_non_whitespace(map, false, cursor), - SelectionGoal::None, - ) - }); - }); - }); - - self.switch_mode(Mode::Insert, false, window, cx); - } - - fn toggle_mode(&mut self, mode: Mode, window: &mut Window, cx: &mut Context) { - if self.mode == mode { - self.switch_mode(Mode::Normal, false, window, cx); - } else { - self.switch_mode(mode, false, window, cx); - } - } - - pub fn other_end(&mut self, _: &OtherEnd, window: &mut Window, cx: &mut Context) { - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|_, selection| { - selection.reversed = !selection.reversed; - }); - }) - }); - } - - pub fn other_end_row_aware( - &mut self, - _: &OtherEndRowAware, - window: &mut Window, - cx: &mut Context, - ) { - let mode = self.mode; - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|_, selection| { - selection.reversed = !selection.reversed; - }); - if mode == Mode::VisualBlock { - s.reverse_selections(); - } - }) - }); - } - - pub fn visual_delete(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context) { - self.store_visual_marks(window, cx); - self.update_editor(cx, |vim, editor, cx| { - let mut original_columns: HashMap<_, _> = Default::default(); - let line_mode = line_mode || editor.selections.line_mode(); - editor.selections.set_line_mode(false); - - editor.transact(window, cx, |editor, window, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - if line_mode { - let mut position = selection.head(); - if !selection.reversed { - position = movement::left(map, position); - } - original_columns.insert(selection.id, position.to_point(map).column); - if vim.mode == Mode::VisualBlock { - *selection.end.column_mut() = map.line_len(selection.end.row()) - } else { - let start = selection.start.to_point(map); - let end = selection.end.to_point(map); - selection.start = map.prev_line_boundary(start).1; - if end.column == 0 && end > start { - let row = end.row.saturating_sub(1); - selection.end = Point::new( - row, - map.buffer_snapshot().line_len(MultiBufferRow(row)), - ) - .to_display_point(map) - } else { - selection.end = map.next_line_boundary(end).1; - } - } - } - selection.goal = SelectionGoal::None; - }); - }); - let kind = if line_mode { - MotionKind::Linewise - } else { - MotionKind::Exclusive - }; - vim.copy_selections_content(editor, kind, window, cx); - - if line_mode && vim.mode != Mode::VisualBlock { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let end = selection.end.to_point(map); - let start = selection.start.to_point(map); - if end.row < map.buffer_snapshot().max_point().row { - selection.end = Point::new(end.row + 1, 0).to_display_point(map) - } else if start.row > 0 { - selection.start = Point::new( - start.row - 1, - map.buffer_snapshot() - .line_len(MultiBufferRow(start.row - 1)), - ) - .to_display_point(map) - } - }); - }); - } - editor.insert("", window, cx); - - // Fixup cursor position after the deletion - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let mut cursor = selection.head().to_point(map); - - if let Some(column) = original_columns.get(&selection.id) { - cursor.column = *column - } - let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left); - selection.collapse_to(cursor, selection.goal) - }); - if vim.mode == Mode::VisualBlock { - s.select_anchors(vec![s.first_anchor()]) - } - }); - }) - }); - self.switch_mode(Mode::Normal, true, window, cx); - } - - pub fn visual_yank(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context) { - self.store_visual_marks(window, cx); - self.update_editor(cx, |vim, editor, cx| { - let line_mode = line_mode || editor.selections.line_mode(); - - // For visual line mode, adjust selections to avoid yanking the next line when on \n - if line_mode && vim.mode != Mode::VisualBlock { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let start = selection.start.to_point(map); - let end = selection.end.to_point(map); - if end.column == 0 && end > start { - let row = end.row.saturating_sub(1); - selection.end = Point::new( - row, - map.buffer_snapshot().line_len(MultiBufferRow(row)), - ) - .to_display_point(map); - } - }); - }); - } - - editor.selections.set_line_mode(line_mode); - let kind = if line_mode { - MotionKind::Linewise - } else { - MotionKind::Exclusive - }; - vim.yank_selections_content(editor, kind, window, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - if line_mode { - selection.start = start_of_line(map, false, selection.start); - }; - selection.collapse_to(selection.start, SelectionGoal::None) - }); - if vim.mode == Mode::VisualBlock { - s.select_anchors(vec![s.first_anchor()]) - } - }); - }); - self.switch_mode(Mode::Normal, true, window, cx); - } - - pub(crate) fn visual_replace( - &mut self, - text: Arc, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let display_map = editor.display_snapshot(cx); - let selections = editor.selections.all_adjusted_display(&display_map); - - // Selections are biased right at the start. So we need to store - // anchors that are biased left so that we can restore the selections - // after the change - let stable_anchors = editor - .selections - .disjoint_anchors_arc() - .iter() - .map(|selection| { - let start = selection.start.bias_left(&display_map.buffer_snapshot()); - start..start - }) - .collect::>(); - - let mut edits = Vec::new(); - for selection in selections.iter() { - let selection = selection.clone(); - for row_range in - movement::split_display_range_by_lines(&display_map, selection.range()) - { - let range = row_range.start.to_offset(&display_map, Bias::Right) - ..row_range.end.to_offset(&display_map, Bias::Right); - let text = text.repeat(range.end - range.start); - edits.push((range, text)); - } - } - - editor.edit(edits, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges(stable_anchors) - }); - }); - }); - self.switch_mode(Mode::Normal, false, window, cx); - } - - pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context) { - Vim::take_forced_motion(cx); - let count = - Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 }); - self.update_editor(cx, |_, editor, cx| { - editor.set_clip_at_line_ends(false, cx); - for _ in 0..count { - if editor - .select_next(&Default::default(), window, cx) - .log_err() - .is_none() - { - break; - } - } - }); - } - - pub fn select_previous( - &mut self, - _: &SelectPrevious, - window: &mut Window, - cx: &mut Context, - ) { - Vim::take_forced_motion(cx); - let count = - Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 }); - self.update_editor(cx, |_, editor, cx| { - for _ in 0..count { - if editor - .select_previous(&Default::default(), window, cx) - .log_err() - .is_none() - { - break; - } - } - }); - } - - pub fn select_match( - &mut self, - direction: Direction, - window: &mut Window, - cx: &mut Context, - ) { - Vim::take_forced_motion(cx); - let count = Vim::take_count(cx).unwrap_or(1); - let Some(pane) = self.pane(window, cx) else { - return; - }; - let vim_is_normal = self.mode == Mode::Normal; - let mut start_selection = MultiBufferOffset(0); - let mut end_selection = MultiBufferOffset(0); - - self.update_editor(cx, |_, editor, _| { - editor.set_collapse_matches(false); - }); - if vim_is_normal { - pane.update(cx, |pane, cx| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() - { - search_bar.update(cx, |search_bar, cx| { - if !search_bar.has_active_match() || !search_bar.show(window, cx) { - return; - } - // without update_match_index there is a bug when the cursor is before the first match - search_bar.update_match_index(window, cx); - search_bar.select_match(direction.opposite(), 1, window, cx); - }); - } - }); - } - self.update_editor(cx, |_, editor, cx| { - let latest = editor - .selections - .newest::(&editor.display_snapshot(cx)); - start_selection = latest.start; - end_selection = latest.end; - }); - - let mut match_exists = false; - pane.update(cx, |pane, cx| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |search_bar, cx| { - search_bar.update_match_index(window, cx); - search_bar.select_match(direction, count, window, cx); - match_exists = search_bar.match_exists(window, cx); - }); - } - }); - if !match_exists { - self.clear_operator(window, cx); - self.stop_replaying(cx); - return; - } - self.update_editor(cx, |_, editor, cx| { - let latest = editor - .selections - .newest::(&editor.display_snapshot(cx)); - if vim_is_normal { - start_selection = latest.start; - end_selection = latest.end; - } else { - start_selection = start_selection.min(latest.start); - end_selection = end_selection.max(latest.end); - } - if direction == Direction::Prev { - std::mem::swap(&mut start_selection, &mut end_selection); - } - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges([start_selection..end_selection]); - }); - editor.set_collapse_matches(true); - }); - - match self.maybe_pop_operator() { - Some(Operator::Change) => self.substitute(None, false, window, cx), - Some(Operator::Delete) => { - self.stop_recording(cx); - self.visual_delete(false, window, cx) - } - Some(Operator::Yank) => self.visual_yank(false, window, cx), - _ => {} // Ignoring other operators - } - } -} -#[cfg(test)] -mod test { - use indoc::indoc; - use workspace::item::Item; - - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - - #[gpui::test] - async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "The ˇquick brown - fox jumps over - the lazy dog" - }) - .await; - let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx)); - - // entering visual mode should select the character - // under cursor - cx.simulate_shared_keystrokes("v").await; - cx.shared_state() - .await - .assert_eq(indoc! { "The «qˇ»uick brown - fox jumps over - the lazy dog"}); - cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx))); - - // forwards motions should extend the selection - cx.simulate_shared_keystrokes("w j").await; - cx.shared_state().await.assert_eq(indoc! { "The «quick brown - fox jumps oˇ»ver - the lazy dog"}); - - cx.simulate_shared_keystrokes("escape").await; - cx.shared_state().await.assert_eq(indoc! { "The quick brown - fox jumps ˇover - the lazy dog"}); - - // motions work backwards - cx.simulate_shared_keystrokes("v k b").await; - cx.shared_state() - .await - .assert_eq(indoc! { "The «ˇquick brown - fox jumps o»ver - the lazy dog"}); - - // works on empty lines - cx.set_shared_state(indoc! {" - a - ˇ - b - "}) - .await; - let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx)); - cx.simulate_shared_keystrokes("v").await; - cx.shared_state().await.assert_eq(indoc! {" - a - « - ˇ»b - "}); - cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx))); - - // toggles off again - cx.simulate_shared_keystrokes("v").await; - cx.shared_state().await.assert_eq(indoc! {" - a - ˇ - b - "}); - - // works at the end of a document - cx.set_shared_state(indoc! {" - a - b - ˇ"}) - .await; - - cx.simulate_shared_keystrokes("v").await; - cx.shared_state().await.assert_eq(indoc! {" - a - b - ˇ"}); - } - - #[gpui::test] - async fn test_visual_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! { - "«The quick brown - fox jumps over - the lazy dogˇ»" - }, - Mode::Visual, - ); - cx.simulate_keystrokes("g shift-i"); - cx.assert_state( - indoc! { - "ˇThe quick brown - ˇfox jumps over - ˇthe lazy dog" - }, - Mode::Insert, - ); - } - - #[gpui::test] - async fn test_visual_insert_end_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! { - "«The quick brown - fox jumps over - the lazy dogˇ»" - }, - Mode::Visual, - ); - cx.simulate_keystrokes("g shift-a"); - cx.assert_state( - indoc! { - "The quick brownˇ - fox jumps overˇ - the lazy dogˇ" - }, - Mode::Insert, - ); - } - - #[gpui::test] - async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "The ˇquick brown - fox jumps over - the lazy dog" - }) - .await; - cx.simulate_shared_keystrokes("shift-v").await; - cx.shared_state() - .await - .assert_eq(indoc! { "The «qˇ»uick brown - fox jumps over - the lazy dog"}); - cx.simulate_shared_keystrokes("x").await; - cx.shared_state().await.assert_eq(indoc! { "fox ˇjumps over - the lazy dog"}); - - // it should work on empty lines - cx.set_shared_state(indoc! {" - a - ˇ - b"}) - .await; - cx.simulate_shared_keystrokes("shift-v").await; - cx.shared_state().await.assert_eq(indoc! {" - a - « - ˇ»b"}); - cx.simulate_shared_keystrokes("x").await; - cx.shared_state().await.assert_eq(indoc! {" - a - ˇb"}); - - // it should work at the end of the document - cx.set_shared_state(indoc! {" - a - b - ˇ"}) - .await; - let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx)); - cx.simulate_shared_keystrokes("shift-v").await; - cx.shared_state().await.assert_eq(indoc! {" - a - b - ˇ"}); - cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx))); - cx.simulate_shared_keystrokes("x").await; - cx.shared_state().await.assert_eq(indoc! {" - a - ˇb"}); - } - - #[gpui::test] - async fn test_visual_delete(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.simulate("v w", "The quick ˇbrown") - .await - .assert_matches(); - - cx.simulate("v w x", "The quick ˇbrown") - .await - .assert_matches(); - cx.simulate( - "v w j x", - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}, - ) - .await - .assert_matches(); - // Test pasting code copied on delete - cx.simulate_shared_keystrokes("j p").await; - cx.shared_state().await.assert_matches(); - - cx.simulate_at_each_offset( - "v w j x", - indoc! {" - The ˇquick brown - fox jumps over - the ˇlazy dog"}, - ) - .await - .assert_matches(); - cx.simulate_at_each_offset( - "v b k x", - indoc! {" - The ˇquick brown - fox jumps ˇover - the ˇlazy dog"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - The quˇick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("shift-v x").await; - cx.shared_state().await.assert_matches(); - - // Test pasting code copied on delete - cx.simulate_shared_keystrokes("p").await; - cx.shared_state().await.assert_matches(); - - cx.set_shared_state(indoc! {" - The quick brown - fox jumps over - the laˇzy dog"}) - .await; - cx.simulate_shared_keystrokes("shift-v x").await; - cx.shared_state().await.assert_matches(); - cx.shared_clipboard().await.assert_eq("the lazy dog\n"); - - cx.set_shared_state(indoc! {" - The quˇick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("shift-v j x").await; - cx.shared_state().await.assert_matches(); - // Test pasting code copied on delete - cx.simulate_shared_keystrokes("p").await; - cx.shared_state().await.assert_matches(); - - cx.set_shared_state(indoc! {" - The ˇlong line - should not - crash - "}) - .await; - cx.simulate_shared_keystrokes("shift-v $ x").await; - cx.shared_state().await.assert_matches(); - } - - #[gpui::test] - async fn test_visual_yank(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("The quick ˇbrown").await; - cx.simulate_shared_keystrokes("v w y").await; - cx.shared_state().await.assert_eq("The quick ˇbrown"); - cx.shared_clipboard().await.assert_eq("brown"); - - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("v w j y").await; - cx.shared_state().await.assert_eq(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}); - cx.shared_clipboard().await.assert_eq(indoc! {" - quick brown - fox jumps o"}); - - cx.set_shared_state(indoc! {" - The quick brown - fox jumps over - the ˇlazy dog"}) - .await; - cx.simulate_shared_keystrokes("v w j y").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jumps over - the ˇlazy dog"}); - cx.shared_clipboard().await.assert_eq("lazy d"); - cx.simulate_shared_keystrokes("shift-v y").await; - cx.shared_clipboard().await.assert_eq("the lazy dog\n"); - - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("v b k y").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇThe quick brown - fox jumps over - the lazy dog"}); - assert_eq!( - cx.read_from_clipboard() - .map(|item| item.text().unwrap()) - .unwrap(), - "The q" - ); - - cx.set_shared_state(indoc! {" - The quick brown - fox ˇjumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("shift-v shift-g shift-y") - .await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - ˇfox jumps over - the lazy dog"}); - cx.shared_clipboard() - .await - .assert_eq("fox jumps over\nthe lazy dog\n"); - - cx.set_shared_state(indoc! {" - The quick brown - fox ˇjumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("shift-v $ shift-y").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - ˇfox jumps over - the lazy dog"}); - cx.shared_clipboard().await.assert_eq("fox jumps over\n"); - } - - #[gpui::test] - async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "The ˇquick brown - fox jumps over - the lazy dog" - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v").await; - cx.shared_state().await.assert_eq(indoc! { - "The «qˇ»uick brown - fox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes("2 down").await; - cx.shared_state().await.assert_eq(indoc! { - "The «qˇ»uick brown - fox «jˇ»umps over - the «lˇ»azy dog" - }); - cx.simulate_shared_keystrokes("e").await; - cx.shared_state().await.assert_eq(indoc! { - "The «quicˇ»k brown - fox «jumpˇ»s over - the «lazyˇ» dog" - }); - cx.simulate_shared_keystrokes("^").await; - cx.shared_state().await.assert_eq(indoc! { - "«ˇThe q»uick brown - «ˇfox j»umps over - «ˇthe l»azy dog" - }); - cx.simulate_shared_keystrokes("$").await; - cx.shared_state().await.assert_eq(indoc! { - "The «quick brownˇ» - fox «jumps overˇ» - the «lazy dogˇ»" - }); - cx.simulate_shared_keystrokes("shift-f space").await; - cx.shared_state().await.assert_eq(indoc! { - "The «quickˇ» brown - fox «jumpsˇ» over - the «lazy ˇ»dog" - }); - - // toggling through visual mode works as expected - cx.simulate_shared_keystrokes("v").await; - cx.shared_state().await.assert_eq(indoc! { - "The «quick brown - fox jumps over - the lazy ˇ»dog" - }); - cx.simulate_shared_keystrokes("ctrl-v").await; - cx.shared_state().await.assert_eq(indoc! { - "The «quickˇ» brown - fox «jumpsˇ» over - the «lazy ˇ»dog" - }); - - cx.set_shared_state(indoc! { - "The ˇquick - brown - fox - jumps over the - - lazy dog - " - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v down down").await; - cx.shared_state().await.assert_eq(indoc! { - "The«ˇ q»uick - bro«ˇwn» - foxˇ - jumps over the - - lazy dog - " - }); - cx.simulate_shared_keystrokes("down").await; - cx.shared_state().await.assert_eq(indoc! { - "The «qˇ»uick - brow«nˇ» - fox - jump«sˇ» over the - - lazy dog - " - }); - cx.simulate_shared_keystrokes("left").await; - cx.shared_state().await.assert_eq(indoc! { - "The«ˇ q»uick - bro«ˇwn» - foxˇ - jum«ˇps» over the - - lazy dog - " - }); - cx.simulate_shared_keystrokes("s o escape").await; - cx.shared_state().await.assert_eq(indoc! { - "Theˇouick - broo - foxo - jumo over the - - lazy dog - " - }); - - // https://github.com/zed-industries/zed/issues/6274 - cx.set_shared_state(indoc! { - "Theˇ quick brown - - fox jumps over - the lazy dog - " - }) - .await; - cx.simulate_shared_keystrokes("l ctrl-v j j").await; - cx.shared_state().await.assert_eq(indoc! { - "The «qˇ»uick brown - - fox «jˇ»umps over - the lazy dog - " - }); - } - - #[gpui::test] - async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "The ˇquick brown - fox jumps over - the lazy dog - " - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v right down").await; - cx.shared_state().await.assert_eq(indoc! { - "The «quˇ»ick brown - fox «juˇ»mps over - the lazy dog - " - }); - } - #[gpui::test] - async fn test_visual_block_mode_down_right(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("ctrl-v l l l l l j").await; - cx.shared_state().await.assert_eq(indoc! {" - The «quick ˇ»brown - fox «jumps ˇ»over - the lazy dog"}); - } - - #[gpui::test] - async fn test_visual_block_mode_up_left(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - The quick brown - fox jumpsˇ over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("ctrl-v h h h h h k").await; - cx.shared_state().await.assert_eq(indoc! {" - The «ˇquick »brown - fox «ˇjumps »over - the lazy dog"}); - } - - #[gpui::test] - async fn test_visual_block_mode_other_end(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - The quick brown - fox jˇumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("ctrl-v l l l l j").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox j«umps ˇ»over - the l«azy dˇ»og"}); - cx.simulate_shared_keystrokes("o k").await; - cx.shared_state().await.assert_eq(indoc! {" - The q«ˇuick »brown - fox j«ˇumps »over - the l«ˇazy d»og"}); - } - - #[gpui::test] - async fn test_visual_block_mode_shift_other_end(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - The quick brown - fox jˇumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("ctrl-v l l l l j").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox j«umps ˇ»over - the l«azy dˇ»og"}); - cx.simulate_shared_keystrokes("shift-o k").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox j«ˇumps »over - the lazy dog"}); - } - - #[gpui::test] - async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "ˇThe quick brown - fox jumps over - the lazy dog - " - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v 9 down").await; - cx.shared_state().await.assert_eq(indoc! { - "«Tˇ»he quick brown - «fˇ»ox jumps over - «tˇ»he lazy dog - ˇ" - }); - - cx.simulate_shared_keystrokes("shift-i k escape").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇkThe quick brown - kfox jumps over - kthe lazy dog - k" - }); - - cx.set_shared_state(indoc! { - "ˇThe quick brown - fox jumps over - the lazy dog - " - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v 9 down").await; - cx.shared_state().await.assert_eq(indoc! { - "«Tˇ»he quick brown - «fˇ»ox jumps over - «tˇ»he lazy dog - ˇ" - }); - cx.simulate_shared_keystrokes("c k escape").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇkhe quick brown - kox jumps over - khe lazy dog - k" - }); - } - - #[gpui::test] - async fn test_visual_block_wrapping_selection(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // Ensure that the editor is wrapping lines at 12 columns so that each - // of the lines ends up being wrapped. - cx.set_shared_wrap(12).await; - cx.set_shared_state(indoc! { - "ˇ12345678901234567890 - 12345678901234567890 - 12345678901234567890 - " - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v j").await; - cx.shared_state().await.assert_eq(indoc! { - "«1ˇ»2345678901234567890 - «1ˇ»2345678901234567890 - 12345678901234567890 - " - }); - - // Test with lines taking up different amounts of display rows to ensure - // that, even in that case, only the buffer rows are taken into account. - cx.set_shared_state(indoc! { - "ˇ123456789012345678901234567890123456789012345678901234567890 - 1234567890123456789012345678901234567890 - 12345678901234567890 - " - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v 2 j").await; - cx.shared_state().await.assert_eq(indoc! { - "«1ˇ»23456789012345678901234567890123456789012345678901234567890 - «1ˇ»234567890123456789012345678901234567890 - «1ˇ»2345678901234567890 - " - }); - - // Same scenario as above, but using the up motion to ensure that the - // result is the same. - cx.set_shared_state(indoc! { - "123456789012345678901234567890123456789012345678901234567890 - 1234567890123456789012345678901234567890 - ˇ12345678901234567890 - " - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v 2 k").await; - cx.shared_state().await.assert_eq(indoc! { - "«1ˇ»23456789012345678901234567890123456789012345678901234567890 - «1ˇ»234567890123456789012345678901234567890 - «1ˇ»2345678901234567890 - " - }); - } - - #[gpui::test] - async fn test_visual_object(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("hello (in [parˇens] o)").await; - cx.simulate_shared_keystrokes("ctrl-v l").await; - cx.simulate_shared_keystrokes("a ]").await; - cx.shared_state() - .await - .assert_eq("hello (in «[parens]ˇ» o)"); - cx.simulate_shared_keystrokes("i (").await; - cx.shared_state() - .await - .assert_eq("hello («in [parens] oˇ»)"); - - cx.set_shared_state("hello in a wˇord again.").await; - cx.simulate_shared_keystrokes("ctrl-v l i w").await; - cx.shared_state() - .await - .assert_eq("hello in a w«ordˇ» again."); - assert_eq!(cx.mode(), Mode::VisualBlock); - cx.simulate_shared_keystrokes("o a s").await; - cx.shared_state() - .await - .assert_eq("«ˇhello in a word» again."); - } - - #[gpui::test] - async fn test_visual_object_expands(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "{ - { - ˇ } - } - { - } - " - }) - .await; - cx.simulate_shared_keystrokes("v l").await; - cx.shared_state().await.assert_eq(indoc! { - "{ - { - « }ˇ» - } - { - } - " - }); - cx.simulate_shared_keystrokes("a {").await; - cx.shared_state().await.assert_eq(indoc! { - "{ - «{ - }ˇ» - } - { - } - " - }); - cx.simulate_shared_keystrokes("a {").await; - cx.shared_state().await.assert_eq(indoc! { - "«{ - { - } - }ˇ» - { - } - " - }); - // cx.simulate_shared_keystrokes("a {").await; - // cx.shared_state().await.assert_eq(indoc! { - // "{ - // «{ - // }ˇ» - // } - // { - // } - // " - // }); - } - - #[gpui::test] - async fn test_mode_across_command(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("aˇbc", Mode::Normal); - cx.simulate_keystrokes("ctrl-v"); - assert_eq!(cx.mode(), Mode::VisualBlock); - cx.simulate_keystrokes("cmd-shift-p escape"); - assert_eq!(cx.mode(), Mode::VisualBlock); - } - - #[gpui::test] - async fn test_gn(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("aaˇ aa aa aa aa").await; - cx.simulate_shared_keystrokes("/ a a enter").await; - cx.shared_state().await.assert_eq("aa ˇaa aa aa aa"); - cx.simulate_shared_keystrokes("g n").await; - cx.shared_state().await.assert_eq("aa «aaˇ» aa aa aa"); - cx.simulate_shared_keystrokes("g n").await; - cx.shared_state().await.assert_eq("aa «aa aaˇ» aa aa"); - cx.simulate_shared_keystrokes("escape d g n").await; - cx.shared_state().await.assert_eq("aa aa ˇ aa aa"); - - cx.set_shared_state("aaˇ aa aa aa aa").await; - cx.simulate_shared_keystrokes("/ a a enter").await; - cx.shared_state().await.assert_eq("aa ˇaa aa aa aa"); - cx.simulate_shared_keystrokes("3 g n").await; - cx.shared_state().await.assert_eq("aa aa aa «aaˇ» aa"); - - cx.set_shared_state("aaˇ aa aa aa aa").await; - cx.simulate_shared_keystrokes("/ a a enter").await; - cx.shared_state().await.assert_eq("aa ˇaa aa aa aa"); - cx.simulate_shared_keystrokes("g shift-n").await; - cx.shared_state().await.assert_eq("aa «ˇaa» aa aa aa"); - cx.simulate_shared_keystrokes("g shift-n").await; - cx.shared_state().await.assert_eq("«ˇaa aa» aa aa aa"); - } - - #[gpui::test] - async fn test_gl(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("aaˇ aa\naa", Mode::Normal); - cx.simulate_keystrokes("g l"); - cx.assert_state("«aaˇ» «aaˇ»\naa", Mode::Visual); - cx.simulate_keystrokes("g >"); - cx.assert_state("«aaˇ» aa\n«aaˇ»", Mode::Visual); - } - - #[gpui::test] - async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("aaˇ aa aa aa aa").await; - cx.simulate_shared_keystrokes("/ a a enter").await; - cx.shared_state().await.assert_eq("aa ˇaa aa aa aa"); - cx.simulate_shared_keystrokes("d g n").await; - - cx.shared_state().await.assert_eq("aa ˇ aa aa aa"); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("aa ˇ aa aa"); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("aa ˇ aa"); - } - - #[gpui::test] - async fn test_cgn_repeat(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("aaˇ aa aa aa aa").await; - cx.simulate_shared_keystrokes("/ a a enter").await; - cx.shared_state().await.assert_eq("aa ˇaa aa aa aa"); - cx.simulate_shared_keystrokes("c g n x escape").await; - cx.shared_state().await.assert_eq("aa ˇx aa aa aa"); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("aa x ˇx aa aa"); - } - - #[gpui::test] - async fn test_cgn_nomatch(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("aaˇ aa aa aa aa").await; - cx.simulate_shared_keystrokes("/ b b enter").await; - cx.shared_state().await.assert_eq("aaˇ aa aa aa aa"); - cx.simulate_shared_keystrokes("c g n x escape").await; - cx.shared_state().await.assert_eq("aaˇaa aa aa aa"); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("aaˇa aa aa aa"); - - cx.set_shared_state("aaˇ bb aa aa aa").await; - cx.simulate_shared_keystrokes("/ b b enter").await; - cx.shared_state().await.assert_eq("aa ˇbb aa aa aa"); - cx.simulate_shared_keystrokes("c g n x escape").await; - cx.shared_state().await.assert_eq("aa ˇx aa aa aa"); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("aa ˇx aa aa aa"); - } - - #[gpui::test] - async fn test_visual_shift_d(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "The ˇquick brown - fox jumps over - the lazy dog - " - }) - .await; - cx.simulate_shared_keystrokes("v down shift-d").await; - cx.shared_state().await.assert_eq(indoc! { - "the ˇlazy dog\n" - }); - - cx.set_shared_state(indoc! { - "The ˇquick brown - fox jumps over - the lazy dog - " - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v down shift-d").await; - cx.shared_state().await.assert_eq(indoc! { - "Theˇ• - fox• - the lazy dog - " - }); - } - - #[gpui::test] - async fn test_shift_y(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "The ˇquick brown\n" - }) - .await; - cx.simulate_shared_keystrokes("v i w shift-y").await; - cx.shared_clipboard().await.assert_eq(indoc! { - "The quick brown\n" - }); - } - - #[gpui::test] - async fn test_gv(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "The ˇquick brown" - }) - .await; - cx.simulate_shared_keystrokes("v i w escape g v").await; - cx.shared_state().await.assert_eq(indoc! { - "The «quickˇ» brown" - }); - - cx.simulate_shared_keystrokes("o escape g v").await; - cx.shared_state().await.assert_eq(indoc! { - "The «ˇquick» brown" - }); - - cx.simulate_shared_keystrokes("escape ^ ctrl-v l").await; - cx.shared_state().await.assert_eq(indoc! { - "«Thˇ»e quick brown" - }); - cx.simulate_shared_keystrokes("g v").await; - cx.shared_state().await.assert_eq(indoc! { - "The «ˇquick» brown" - }); - cx.simulate_shared_keystrokes("g v").await; - cx.shared_state().await.assert_eq(indoc! { - "«Thˇ»e quick brown" - }); - - cx.set_state( - indoc! {" - fiˇsh one - fish two - fish red - fish blue - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("4 g l escape escape g v"); - cx.assert_state( - indoc! {" - «fishˇ» one - «fishˇ» two - «fishˇ» red - «fishˇ» blue - "}, - Mode::Visual, - ); - cx.simulate_keystrokes("y g v"); - cx.assert_state( - indoc! {" - «fishˇ» one - «fishˇ» two - «fishˇ» red - «fishˇ» blue - "}, - Mode::Visual, - ); - } - - #[gpui::test] - async fn test_p_g_v_y(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "The - quicˇk - brown - fox" - }) - .await; - cx.simulate_shared_keystrokes("y y j shift-v p g v y").await; - cx.shared_state().await.assert_eq(indoc! { - "The - quick - ˇquick - fox" - }); - cx.shared_clipboard().await.assert_eq("quick\n"); - } - - #[gpui::test] - async fn test_v2ap(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "The - quicˇk - - brown - fox" - }) - .await; - cx.simulate_shared_keystrokes("v 2 a p").await; - cx.shared_state().await.assert_eq(indoc! { - "«The - quick - - brown - fˇ»ox" - }); - } - - #[gpui::test] - async fn test_visual_syntax_sibling_selection(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - fn test() { - let ˇa = 1; - let b = 2; - let c = 3; - } - "}, - Mode::Normal, - ); - - // Enter visual mode and select the statement - cx.simulate_keystrokes("v w w w"); - cx.assert_state( - indoc! {" - fn test() { - let «a = 1;ˇ» - let b = 2; - let c = 3; - } - "}, - Mode::Visual, - ); - - // The specific behavior of syntax sibling selection in vim mode - // would depend on the key bindings configured, but the actions - // are now available for use - } -} diff --git a/crates/vim/test_data/neovim_backed_test_context_works.json b/crates/vim/test_data/neovim_backed_test_context_works.json deleted file mode 100644 index 3ed9f40f88..0000000000 --- a/crates/vim/test_data/neovim_backed_test_context_works.json +++ /dev/null @@ -1,3 +0,0 @@ -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"This is a tesˇt"}} -{"Get":{"state":"This is a tesˇt","mode":"Normal"}} diff --git a/crates/vim/test_data/test_a.json b/crates/vim/test_data/test_a.json deleted file mode 100644 index 8094974f98..0000000000 --- a/crates/vim/test_data/test_a.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"The qˇuick"}} -{"Key":"a"} -{"Get":{"state":"The quˇick","mode":"Insert"}} -{"Put":{"state":"The quicˇk"}} -{"Key":"a"} -{"Get":{"state":"The quickˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_around_containing_word_indent.json b/crates/vim/test_data/test_around_containing_word_indent.json deleted file mode 100644 index 6707ff6804..0000000000 --- a/crates/vim/test_data/test_around_containing_word_indent.json +++ /dev/null @@ -1,23 +0,0 @@ -{"Put":{"state":" ˇconst f = (x: unknown) => {"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":" «const ˇ»f = (x: unknown) => {","mode":"Visual"}} -{"Put":{"state":" ˇconst f = (x: unknown) => {"}} -{"Key":"y"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":" ˇconst f = (x: unknown) => {","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"const "}} -{"Put":{"state":" ˇconst f = (x: unknown) => {"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":" ˇf = (x: unknown) => {","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"const "}} -{"Put":{"state":" ˇconst f = (x: unknown) => {"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":" ˇf = (x: unknown) => {","mode":"Insert"}} -{"ReadRegister":{"name":"\"","value":"const "}} diff --git a/crates/vim/test_data/test_b.json b/crates/vim/test_data/test_b.json deleted file mode 100644 index 4324f9610d..0000000000 --- a/crates/vim/test_data/test_b.json +++ /dev/null @@ -1,54 +0,0 @@ -{"Put":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"b"} -{"Get":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"b"} -{"Get":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"b"} -{"Get":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-ˇbrown\n\n\nfox_jumps over\nthe"}} -{"Key":"b"} -{"Get":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe"}} -{"Key":"b"} -{"Get":{"state":"The quick-ˇbrown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-brown\n\nˇ\nfox_jumps over\nthe"}} -{"Key":"b"} -{"Get":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-brown\n\n\nˇfox_jumps over\nthe"}} -{"Key":"b"} -{"Get":{"state":"The quick-brown\n\nˇ\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-brown\n\n\nfox_jumps ˇover\nthe"}} -{"Key":"b"} -{"Get":{"state":"The quick-brown\n\n\nˇfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-brown\n\n\nfox_jumps over\nˇthe"}} -{"Key":"b"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps ˇover\nthe","mode":"Normal"}} -{"Put":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-b"} -{"Get":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-b"} -{"Get":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-b"} -{"Get":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-ˇbrown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-b"} -{"Get":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe"}} -{"Key":"shift-b"} -{"Get":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-brown\n\nˇ\nfox_jumps over\nthe"}} -{"Key":"shift-b"} -{"Get":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-brown\n\n\nˇfox_jumps over\nthe"}} -{"Key":"shift-b"} -{"Get":{"state":"The quick-brown\n\nˇ\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-brown\n\n\nfox_jumps ˇover\nthe"}} -{"Key":"shift-b"} -{"Get":{"state":"The quick-brown\n\n\nˇfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-brown\n\n\nfox_jumps over\nˇthe"}} -{"Key":"shift-b"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps ˇover\nthe","mode":"Normal"}} diff --git a/crates/vim/test_data/test_backspace.json b/crates/vim/test_data/test_backspace.json deleted file mode 100644 index b11a2562db..0000000000 --- a/crates/vim/test_data/test_backspace.json +++ /dev/null @@ -1,9 +0,0 @@ -{"Put":{"state":"ˇThe quick\nbrown"}} -{"Key":"backspace"} -{"Get":{"state":"ˇThe quick\nbrown","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown"}} -{"Key":"backspace"} -{"Get":{"state":"The ˇquick\nbrown","mode":"Normal"}} -{"Put":{"state":"The quick\nˇbrown"}} -{"Key":"backspace"} -{"Get":{"state":"The quicˇk\nbrown","mode":"Normal"}} diff --git a/crates/vim/test_data/test_backspace_non_ascii_bol.json b/crates/vim/test_data/test_backspace_non_ascii_bol.json deleted file mode 100644 index 88536bc50d..0000000000 --- a/crates/vim/test_data/test_backspace_non_ascii_bol.json +++ /dev/null @@ -1,4 +0,0 @@ -{"Put":{"state":"ππππ\nπanˇotherline"}} -{"Key":"4"} -{"Key":"backspace"} -{"Get":{"state":"πππˇπ\nπanotherline","mode":"Normal"}} diff --git a/crates/vim/test_data/test_backwards_n.json b/crates/vim/test_data/test_backwards_n.json deleted file mode 100644 index a08cc84d21..0000000000 --- a/crates/vim/test_data/test_backwards_n.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"ˇa b a b a b a"}} -{"Key":"*"} -{"Key":"n"} -{"Get":{"state":"a b a b ˇa b a","mode":"Normal"}} -{"Key":"#"} -{"Get":{"state":"a b ˇa b a b a","mode":"Normal"}} -{"Key":"n"} -{"Get":{"state":"ˇa b a b a b a","mode":"Normal"}} diff --git a/crates/vim/test_data/test_blackhole_register.json b/crates/vim/test_data/test_blackhole_register.json deleted file mode 100644 index e16bb3abe3..0000000000 --- a/crates/vim/test_data/test_blackhole_register.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"ˇhello world"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Key":"\""} -{"Key":"_"} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Key":"p"} -{"Get":{"state":"hellˇo","mode":"Normal"}} diff --git a/crates/vim/test_data/test_builtin_marks.json b/crates/vim/test_data/test_builtin_marks.json deleted file mode 100644 index 0d05385960..0000000000 --- a/crates/vim/test_data/test_builtin_marks.json +++ /dev/null @@ -1,36 +0,0 @@ -{"Put":{"state":"Line one\nLine two\nLine ˇthree\nLine four\nLine five\n"}} -{"Key":"v"} -{"Key":"j"} -{"Key":"escape"} -{"Key":"k"} -{"Key":"k"} -{"Key":"'"} -{"Key":"<"} -{"Get":{"state":"Line one\nLine two\nˇLine three\nLine four\nLine five\n","mode":"Normal"}} -{"Key":"`"} -{"Key":"<"} -{"Get":{"state":"Line one\nLine two\nLine ˇthree\nLine four\nLine five\n","mode":"Normal"}} -{"Key":"'"} -{"Key":">"} -{"Get":{"state":"Line one\nLine two\nLine three\nˇLine four\nLine five\n","mode":"Normal"}} -{"Key":"`"} -{"Key":">"} -{"Get":{"state":"Line one\nLine two\nLine three\nLine ˇfour\nLine five\n","mode":"Normal"}} -{"Key":"g"} -{"Key":"g"} -{"Key":"^"} -{"Key":"j"} -{"Key":"j"} -{"Key":"l"} -{"Key":"l"} -{"Key":"c"} -{"Key":"e"} -{"Key":"k"} -{"Key":"e"} -{"Key":"escape"} -{"Key":"'"} -{"Key":"."} -{"Get":{"state":"Line one\nLine two\nˇLike three\nLine four\nLine five\n","mode":"Normal"}} -{"Key":"`"} -{"Key":"."} -{"Get":{"state":"Line one\nLine two\nLiˇke three\nLine four\nLine five\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_capital_f_and_capital_t.json b/crates/vim/test_data/test_capital_f_and_capital_t.json deleted file mode 100644 index 8ef45ec623..0000000000 --- a/crates/vim/test_data/test_capital_f_and_capital_t.json +++ /dev/null @@ -1,570 +0,0 @@ -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}} -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}} -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}} -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}} -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}} -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_caret_mark.json b/crates/vim/test_data/test_caret_mark.json deleted file mode 100644 index 6a117e9968..0000000000 --- a/crates/vim/test_data/test_caret_mark.json +++ /dev/null @@ -1,35 +0,0 @@ -{"Put":{"state":"Line one\nLine two\nLine three\nˇLine four\nLine five\n"}} -{"Key":"c"} -{"Key":"w"} -{"Key":"shift-s"} -{"Key":"t"} -{"Key":"r"} -{"Key":"a"} -{"Key":"i"} -{"Key":"g"} -{"Key":"h"} -{"Key":"t"} -{"Key":"space"} -{"Key":"t"} -{"Key":"h"} -{"Key":"i"} -{"Key":"n"} -{"Key":"g"} -{"Key":"escape"} -{"Key":"j"} -{"Key":"j"} -{"Key":"'"} -{"Key":"^"} -{"Get":{"state":"Line one\nLine two\nLine three\nˇStraight thing four\nLine five\n","mode":"Normal"}} -{"Key":"`"} -{"Key":"^"} -{"Get":{"state":"Line one\nLine two\nLine three\nStraight thingˇ four\nLine five\n","mode":"Normal"}} -{"Key":"k"} -{"Key":"a"} -{"Key":"!"} -{"Key":"escape"} -{"Key":"k"} -{"Key":"g"} -{"Key":"i"} -{"Key":"?"} -{"Get":{"state":"Line one\nLine two\nLine three!?ˇ\nStraight thing four\nLine five\n","mode":"Insert"}} diff --git a/crates/vim/test_data/test_cc.json b/crates/vim/test_data/test_cc.json deleted file mode 100644 index d4b4a499bb..0000000000 --- a/crates/vim/test_data/test_cc.json +++ /dev/null @@ -1,24 +0,0 @@ -{"Put":{"state":"ˇ"}} -{"Key":"c"} -{"Key":"c"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The ˇquick"}} -{"Key":"c"} -{"Key":"c"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quˇick\nbrown fox\njumps over"}} -{"Key":"c"} -{"Key":"c"} -{"Get":{"state":"ˇ\nbrown fox\njumps over","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"c"} -{"Key":"c"} -{"Get":{"state":"The quick\nˇ\njumps over","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"c"} -{"Key":"c"} -{"Get":{"state":"The quick\nbrown fox\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"c"} -{"Key":"c"} -{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}} diff --git a/crates/vim/test_data/test_cgn_nomatch.json b/crates/vim/test_data/test_cgn_nomatch.json deleted file mode 100644 index 9c2f02bb85..0000000000 --- a/crates/vim/test_data/test_cgn_nomatch.json +++ /dev/null @@ -1,28 +0,0 @@ -{"Put":{"state":"aaˇ aa aa aa aa"}} -{"Key":"/"} -{"Key":"b"} -{"Key":"b"} -{"Key":"enter"} -{"Get":{"state":"aaˇ aa aa aa aa","mode":"Normal"}} -{"Key":"c"} -{"Key":"g"} -{"Key":"n"} -{"Key":"x"} -{"Key":"escape"} -{"Get":{"state":"aaˇaa aa aa aa","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"aaˇa aa aa aa","mode":"Normal"}} -{"Put":{"state":"aaˇ bb aa aa aa"}} -{"Key":"/"} -{"Key":"b"} -{"Key":"b"} -{"Key":"enter"} -{"Get":{"state":"aa ˇbb aa aa aa","mode":"Normal"}} -{"Key":"c"} -{"Key":"g"} -{"Key":"n"} -{"Key":"x"} -{"Key":"escape"} -{"Get":{"state":"aa ˇx aa aa aa","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"aa ˇx aa aa aa","mode":"Normal"}} diff --git a/crates/vim/test_data/test_cgn_repeat.json b/crates/vim/test_data/test_cgn_repeat.json deleted file mode 100644 index 4683a83d07..0000000000 --- a/crates/vim/test_data/test_cgn_repeat.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"aaˇ aa aa aa aa"}} -{"Key":"/"} -{"Key":"a"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}} -{"Key":"c"} -{"Key":"g"} -{"Key":"n"} -{"Key":"x"} -{"Key":"escape"} -{"Get":{"state":"aa ˇx aa aa aa","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"aa x ˇx aa aa","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_0.json b/crates/vim/test_data/test_change_0.json deleted file mode 100644 index 90668f4a17..0000000000 --- a/crates/vim/test_data/test_change_0.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"The qˇuick\nbrown fox"}} -{"Key":"c"} -{"Key":"0"} -{"Get":{"state":"ˇuick\nbrown fox","mode":"Insert"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"c"} -{"Key":"0"} -{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_b.json b/crates/vim/test_data/test_change_b.json deleted file mode 100644 index d43cc04c45..0000000000 --- a/crates/vim/test_data/test_change_b.json +++ /dev/null @@ -1,24 +0,0 @@ -{"Put":{"state":"Teˇst Test"}} -{"Key":"c"} -{"Key":"b"} -{"Get":{"state":"ˇst Test","mode":"Insert"}} -{"Put":{"state":"Test ˇtest"}} -{"Key":"c"} -{"Key":"b"} -{"Get":{"state":"ˇtest","mode":"Insert"}} -{"Put":{"state":"Test1 test2 ˇtest3"}} -{"Key":"c"} -{"Key":"b"} -{"Get":{"state":"Test1 ˇtest3","mode":"Insert"}} -{"Put":{"state":"Test test\nˇtest"}} -{"Key":"c"} -{"Key":"b"} -{"Get":{"state":"Test ˇ\ntest","mode":"Insert"}} -{"Put":{"state":"Test test\nˇ\ntest"}} -{"Key":"c"} -{"Key":"b"} -{"Get":{"state":"Test ˇ\n\ntest","mode":"Insert"}} -{"Put":{"state":"Test test-test ˇtest"}} -{"Key":"c"} -{"Key":"shift-b"} -{"Get":{"state":"Test ˇtest","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_backspace.json b/crates/vim/test_data/test_change_backspace.json deleted file mode 100644 index 508500163b..0000000000 --- a/crates/vim/test_data/test_change_backspace.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"Teˇst"}} -{"Key":"c"} -{"Key":"backspace"} -{"Get":{"state":"Tˇst","mode":"Insert"}} -{"Put":{"state":"Tˇest"}} -{"Key":"c"} -{"Key":"backspace"} -{"Get":{"state":"ˇest","mode":"Insert"}} -{"Put":{"state":"ˇTest"}} -{"Key":"c"} -{"Key":"backspace"} -{"Get":{"state":"ˇTest","mode":"Insert"}} -{"Put":{"state":"Test\nˇtest"}} -{"Key":"c"} -{"Key":"backspace"} -{"Get":{"state":"Testˇtest","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_case.json b/crates/vim/test_data/test_change_case.json deleted file mode 100644 index 10eb93b227..0000000000 --- a/crates/vim/test_data/test_change_case.json +++ /dev/null @@ -1,23 +0,0 @@ -{"Put":{"state":"ˇabC\n"}} -{"Key":"~"} -{"Get":{"state":"AˇbC\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"~"} -{"Get":{"state":"ABˇc\n","mode":"Normal"}} -{"Put":{"state":"a😀C«dÉ1*fˇ»\n"}} -{"Key":"~"} -{"Get":{"state":"a😀CˇDé1*F\n","mode":"Normal"}} -{"Key":"~"} -{"Put":{"state":"aˇC😀é1*F\n"}} -{"Key":"4"} -{"Key":"~"} -{"Get":{"state":"ac😀É1ˇ*F\n","mode":"Normal"}} -{"Put":{"state":"abˇC\n"}} -{"Key":"shift-v"} -{"Key":"~"} -{"Get":{"state":"ˇABc\n","mode":"Normal"}} -{"Put":{"state":"ˇaa\nbb\ncc"}} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Key":"~"} -{"Get":{"state":"ˇAa\nBb\ncc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_case_motion.json b/crates/vim/test_data/test_change_case_motion.json deleted file mode 100644 index 18921f08a2..0000000000 --- a/crates/vim/test_data/test_change_case_motion.json +++ /dev/null @@ -1,27 +0,0 @@ -{"Put":{"state":"ˇabc def"}} -{"Key":"g"} -{"Key":"shift-u"} -{"Key":"w"} -{"Get":{"state":"ˇABC def","mode":"Normal"}} -{"Key":"g"} -{"Key":"u"} -{"Key":"w"} -{"Get":{"state":"ˇabc def","mode":"Normal"}} -{"Key":"g"} -{"Key":"~"} -{"Key":"w"} -{"Get":{"state":"ˇABC def","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"ˇabc def","mode":"Normal"}} -{"Put":{"state":"abˇc def"}} -{"Key":"g"} -{"Key":"~"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"ˇABC def","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"ˇabc def","mode":"Normal"}} -{"Key":"g"} -{"Key":"shift-u"} -{"Key":"$"} -{"Get":{"state":"ˇABC DEF","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_case_motion_object.json b/crates/vim/test_data/test_change_case_motion_object.json deleted file mode 100644 index b2157975bd..0000000000 --- a/crates/vim/test_data/test_change_case_motion_object.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"abc dˇef\n"}} -{"Key":"g"} -{"Key":"shift-u"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"abc ˇDEF\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_cc.json b/crates/vim/test_data/test_change_cc.json deleted file mode 100644 index e2d1cb8d21..0000000000 --- a/crates/vim/test_data/test_change_cc.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"The quick\n brownˇ fox\njumps over\nthe lazy"}} -{"Key":"c"} -{"Key":"c"} -{"Get":{"state":"The quick\n ˇ\njumps over\nthe lazy","mode":"Insert"}} -{"Put":{"state":"ˇThe quick\nbrown fox\njumps over\nthe lazy"}} -{"Key":"c"} -{"Key":"c"} -{"Get":{"state":"ˇ\nbrown fox\njumps over\nthe lazy","mode":"Insert"}} -{"Put":{"state":"The quick\n broˇwn fox\njumps over\nthe lazy"}} -{"Key":"c"} -{"Key":"c"} -{"Get":{"state":"The quick\n ˇ\njumps over\nthe lazy","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_e.json b/crates/vim/test_data/test_change_e.json deleted file mode 100644 index fc9f91fe02..0000000000 --- a/crates/vim/test_data/test_change_e.json +++ /dev/null @@ -1,24 +0,0 @@ -{"Put":{"state":"Teˇst Test"}} -{"Key":"c"} -{"Key":"e"} -{"Get":{"state":"Teˇ Test","mode":"Insert"}} -{"Put":{"state":"Tˇest test"}} -{"Key":"c"} -{"Key":"e"} -{"Get":{"state":"Tˇ test","mode":"Insert"}} -{"Put":{"state":"Test teˇst\ntest"}} -{"Key":"c"} -{"Key":"e"} -{"Get":{"state":"Test teˇ\ntest","mode":"Insert"}} -{"Put":{"state":"Test tesˇt\ntest"}} -{"Key":"c"} -{"Key":"e"} -{"Get":{"state":"Test tesˇ","mode":"Insert"}} -{"Put":{"state":"Test test\nˇ\ntest"}} -{"Key":"c"} -{"Key":"e"} -{"Get":{"state":"Test test\nˇ","mode":"Insert"}} -{"Put":{"state":"Test teˇst-test test"}} -{"Key":"c"} -{"Key":"shift-e"} -{"Get":{"state":"Test teˇ test","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_end_of_document.json b/crates/vim/test_data/test_change_end_of_document.json deleted file mode 100644 index a3f09e4453..0000000000 --- a/crates/vim/test_data/test_change_end_of_document.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}} -{"Key":"c"} -{"Key":"shift-g"} -{"Get":{"state":"The quick\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}} -{"Key":"c"} -{"Key":"shift-g"} -{"Get":{"state":"The quick\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}} -{"Key":"c"} -{"Key":"shift-g"} -{"Get":{"state":"The quick\nbrown fox\njumps over\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown fox\njumps over\nˇ"}} -{"Key":"c"} -{"Key":"shift-g"} -{"Get":{"state":"The quick\nbrown fox\njumps over\nˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_end_of_line.json b/crates/vim/test_data/test_change_end_of_line.json deleted file mode 100644 index 44766df85e..0000000000 --- a/crates/vim/test_data/test_change_end_of_line.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"The qˇuick\nbrown fox"}} -{"Key":"c"} -{"Key":"$"} -{"Get":{"state":"The qˇ\nbrown fox","mode":"Insert"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"c"} -{"Key":"$"} -{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_gg.json b/crates/vim/test_data/test_change_gg.json deleted file mode 100644 index f4271f7120..0000000000 --- a/crates/vim/test_data/test_change_gg.json +++ /dev/null @@ -1,20 +0,0 @@ -{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}} -{"Key":"c"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"ˇ\njumps over\nthe lazy","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}} -{"Key":"c"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over\nthe lazy"}} -{"Key":"c"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"ˇ\nbrown fox\njumps over\nthe lazy","mode":"Insert"}} -{"Put":{"state":"ˇ\nbrown fox\njumps over\nthe lazy"}} -{"Key":"c"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"ˇ\nbrown fox\njumps over\nthe lazy","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_h.json b/crates/vim/test_data/test_change_h.json deleted file mode 100644 index 6acfb5d080..0000000000 --- a/crates/vim/test_data/test_change_h.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"Teˇst"}} -{"Key":"c"} -{"Key":"h"} -{"Get":{"state":"Tˇst","mode":"Insert"}} -{"Put":{"state":"Tˇest"}} -{"Key":"c"} -{"Key":"h"} -{"Get":{"state":"ˇest","mode":"Insert"}} -{"Put":{"state":"ˇTest"}} -{"Key":"c"} -{"Key":"h"} -{"Get":{"state":"ˇTest","mode":"Insert"}} -{"Put":{"state":"Test\nˇtest"}} -{"Key":"c"} -{"Key":"h"} -{"Get":{"state":"Test\nˇtest","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_j.json b/crates/vim/test_data/test_change_j.json deleted file mode 100644 index 3808bb21e2..0000000000 --- a/crates/vim/test_data/test_change_j.json +++ /dev/null @@ -1,20 +0,0 @@ -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"c"} -{"Key":"j"} -{"Get":{"state":"The quick\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"c"} -{"Key":"j"} -{"Get":{"state":"The quick\nbrown fox\njumps ˇover","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"c"} -{"Key":"j"} -{"Get":{"state":"ˇ\njumps over","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown fox\nˇ"}} -{"Key":"c"} -{"Key":"j"} -{"Get":{"state":"The quick\nbrown fox\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick\n ˇbrown fox\n jumps over"}} -{"Key":"c"} -{"Key":"j"} -{"Get":{"state":"The quick\n ˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_k.json b/crates/vim/test_data/test_change_k.json deleted file mode 100644 index 5ac92744d1..0000000000 --- a/crates/vim/test_data/test_change_k.json +++ /dev/null @@ -1,20 +0,0 @@ -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"c"} -{"Key":"k"} -{"Get":{"state":"ˇ\njumps over","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"c"} -{"Key":"k"} -{"Get":{"state":"The quick\nˇ","mode":"Insert"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"c"} -{"Key":"k"} -{"Get":{"state":"The qˇuick\nbrown fox\njumps over","mode":"Normal"}} -{"Put":{"state":"ˇ\nbrown fox\njumps over"}} -{"Key":"c"} -{"Key":"k"} -{"Get":{"state":"ˇ\nbrown fox\njumps over","mode":"Normal"}} -{"Put":{"state":"The quick\n brown fox\n ˇjumps over"}} -{"Key":"c"} -{"Key":"k"} -{"Get":{"state":"The quick\n ˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_l.json b/crates/vim/test_data/test_change_l.json deleted file mode 100644 index 378c5dc7ca..0000000000 --- a/crates/vim/test_data/test_change_l.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"Teˇst"}} -{"Key":"c"} -{"Key":"l"} -{"Get":{"state":"Teˇt","mode":"Insert"}} -{"Put":{"state":"Tesˇt"}} -{"Key":"c"} -{"Key":"l"} -{"Get":{"state":"Tesˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_list_delete.json b/crates/vim/test_data/test_change_list_delete.json deleted file mode 100644 index ad8b1cbd9e..0000000000 --- a/crates/vim/test_data/test_change_list_delete.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"one two\nthree fˇour"}} -{"Key":"x"} -{"Key":"k"} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Key":"^"} -{"Key":"x"} -{"Get":{"state":"ˇne \nthree fur","mode":"Normal"}} -{"Key":"2"} -{"Key":"g"} -{"Key":";"} -{"Get":{"state":"ne \nthree fˇur","mode":"Normal"}} -{"Key":"g"} -{"Key":","} -{"Get":{"state":"ˇne \nthree fur","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_list_insert.json b/crates/vim/test_data/test_change_list_insert.json deleted file mode 100644 index d72878d255..0000000000 --- a/crates/vim/test_data/test_change_list_insert.json +++ /dev/null @@ -1,32 +0,0 @@ -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"1"} -{"Key":"1"} -{"Key":"escape"} -{"Key":"shift-o"} -{"Key":"2"} -{"Key":"2"} -{"Key":"escape"} -{"Key":"shift-g"} -{"Key":"o"} -{"Key":"3"} -{"Key":"3"} -{"Key":"escape"} -{"Get":{"state":"22\n11\n3ˇ3","mode":"Normal"}} -{"Key":"g"} -{"Key":";"} -{"Key":"g"} -{"Key":";"} -{"Key":"g"} -{"Key":";"} -{"Key":"g"} -{"Key":","} -{"Key":"shift-g"} -{"Key":"i"} -{"Key":"4"} -{"Key":"4"} -{"Key":"escape"} -{"Key":"g"} -{"Key":";"} -{"Key":"g"} -{"Key":";"} diff --git a/crates/vim/test_data/test_change_paragraph.json b/crates/vim/test_data/test_change_paragraph.json deleted file mode 100644 index 6d235d9f36..0000000000 --- a/crates/vim/test_data/test_change_paragraph.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"first paragraph\nˇstill first\n\nsecond paragraph\nstill second\n\nthird paragraph\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\nsecond paragraph\nstill second\n\nthird paragraph\n","mode":"Insert"}} -{"ReadRegister":{"name":"\"","value":"first paragraph\nstill first\n\n"}} -{"Key":"escape"} -{"Get":{"state":"ˇ\nsecond paragraph\nstill second\n\nthird paragraph\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_paragraph_object.json b/crates/vim/test_data/test_change_paragraph_object.json deleted file mode 100644 index 7de16dac5b..0000000000 --- a/crates/vim/test_data/test_change_paragraph_object.json +++ /dev/null @@ -1,430 +0,0 @@ -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumpˇs over the lazy dog."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dogˇ."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.ˇ"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumpˇs over the lazy dog."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dogˇ."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.ˇ"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps overˇ\nthe lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps overˇ\nthe lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} -{"Put":{"state":"\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} -{"Put":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} -{"Put":{"state":"\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} -{"Put":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nˇover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nˇover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇ\n\n \t\n\t \t\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇ\n\n \t\n\t \t\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n \t\n\t \t\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\t \t\n","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\nThe quick brown fox jumps over the lazy dog.\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumpˇs over the lazy dog.\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.ˇ\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\nThe quick brown fox jumps over the lazy dog.\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumpˇs over the lazy dog.\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.ˇ\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_paragraph_object_with_soft_wrap.json b/crates/vim/test_data/test_change_paragraph_object_with_soft_wrap.json deleted file mode 100644 index 47d68e13a6..0000000000 --- a/crates/vim/test_data/test_change_paragraph_object_with_soft_wrap.json +++ /dev/null @@ -1,72 +0,0 @@ -{"SetOption":{"value":"wrap"}} -{"SetOption":{"value":"columns=20"}} -{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_rot13_motion.json b/crates/vim/test_data/test_change_rot13_motion.json deleted file mode 100644 index 62c39887ff..0000000000 --- a/crates/vim/test_data/test_change_rot13_motion.json +++ /dev/null @@ -1,23 +0,0 @@ -{"Put":{"state":"ˇabc def"}} -{"Key":"g"} -{"Key":"?"} -{"Key":"w"} -{"Get":{"state":"ˇnop def","mode":"Normal"}} -{"Key":"g"} -{"Key":"?"} -{"Key":"w"} -{"Get":{"state":"ˇabc def","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"ˇnop def","mode":"Normal"}} -{"Put":{"state":"abˇc def"}} -{"Key":"g"} -{"Key":"?"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"ˇnop def","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"ˇabc def","mode":"Normal"}} -{"Key":"g"} -{"Key":"?"} -{"Key":"$"} -{"Get":{"state":"ˇnop qrs","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_rot13_object.json b/crates/vim/test_data/test_change_rot13_object.json deleted file mode 100644 index 19db51b946..0000000000 --- a/crates/vim/test_data/test_change_rot13_object.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"ˇabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"}} -{"Key":"g"} -{"Key":"?"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"ˇnopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_sentence_object.json b/crates/vim/test_data/test_change_sentence_object.json deleted file mode 100644 index 4afbae2713..0000000000 --- a/crates/vim/test_data/test_change_sentence_object.json +++ /dev/null @@ -1,270 +0,0 @@ -{"Put":{"state":"ˇThe quick brown? Fox Jumps! Over the lazy."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Fox Jumps! Over the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick ˇbrown? Fox Jumps! Over the lazy."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Fox Jumps! Over the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ? Fox Jumps! Over the lazy."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Fox Jumps! Over the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brown?ˇ Fox Jumps! Over the lazy."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown?ˇFox Jumps! Over the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brown? ˇFox Jumps! Over the lazy."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇ Over the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jˇumps! Over the lazy."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇ Over the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jumpsˇ! Over the lazy."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇ Over the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jumps!ˇ Over the lazy."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps!ˇOver the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jumps! Ovˇer the lazy."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps! ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jumps! Over theˇ lazy."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps! ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jumps! Over the lazyˇ."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps! ˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick ˇbrown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy doˇg. The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dogˇ. The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. ˇThe quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog. ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. The quick ˇ\nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog. ˇ\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown.)]'\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The ˇquick brown.)]'\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ.)]'\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)ˇ]'\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)]ˇ'\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)]'ˇ\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)]'\" Brown ˇfox jumps. "}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown.)]'\" ˇ ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)]'\" Brown fox jumpsˇ. "}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown.)]'\" ˇ ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)]'\" Brown fox jumps.ˇ "}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown.)]'\" Brown fox jumps.ˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown? Fox Jumps! Over the lazy."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇFox Jumps! Over the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick ˇbrown? Fox Jumps! Over the lazy."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇFox Jumps! Over the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ? Fox Jumps! Over the lazy."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇFox Jumps! Over the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brown? ˇFox Jumps! Over the lazy."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇOver the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jˇumps! Over the lazy."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇOver the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jumpsˇ! Over the lazy."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇOver the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jumps!ˇ Over the lazy."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps!ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jumps! Ovˇer the lazy."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps!ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jumps! Over theˇ lazy."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps!ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jumps! Over the lazyˇ."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps!ˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick ˇbrown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy doˇg. The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dogˇ. The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. ˇThe quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. The quick ˇ\nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown.)]'\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The ˇquick brown.)]'\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ.)]'\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)ˇ]'\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)]ˇ'\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)]'ˇ\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)]'\" Brown ˇfox jumps. "}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown.)]'\" ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)]'\" Brown fox jumpsˇ. "}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown.)]'\" ˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_surrounding_character_objects.json b/crates/vim/test_data/test_change_surrounding_character_objects.json deleted file mode 100644 index 4710e228a6..0000000000 --- a/crates/vim/test_data/test_change_surrounding_character_objects.json +++ /dev/null @@ -1,520 +0,0 @@ -{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck broˇ\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck broˇ\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)ˇe ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ˇ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()quˇi(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ˇck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck broˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)ˇfox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox juˇmps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇer\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe ˇlazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)ˇe ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ˇ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()quˇi(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ˇck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck broˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)ˇfox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox juˇmps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇer\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe ˇlazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)ˇe ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ˇ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()quˇi(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ˇck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck broˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)ˇfox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox juˇmps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇer\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe ˇlazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)ˇe ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ˇ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()quˇi(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ˇck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck broˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)ˇfox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox juˇmps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇer\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe ˇlazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_w.json b/crates/vim/test_data/test_change_w.json deleted file mode 100644 index 149dac8420..0000000000 --- a/crates/vim/test_data/test_change_w.json +++ /dev/null @@ -1,36 +0,0 @@ -{"Put":{"state":"Teˇst"}} -{"Key":"c"} -{"Key":"w"} -{"Get":{"state":"Teˇ","mode":"Insert"}} -{"Put":{"state":"Tˇest test"}} -{"Key":"c"} -{"Key":"w"} -{"Get":{"state":"Tˇ test","mode":"Insert"}} -{"Put":{"state":"Testˇ test"}} -{"Key":"c"} -{"Key":"w"} -{"Get":{"state":"Testˇtest","mode":"Insert"}} -{"Put":{"state":"Tesˇt test"}} -{"Key":"c"} -{"Key":"w"} -{"Get":{"state":"Tesˇ test","mode":"Insert"}} -{"Put":{"state":"Test teˇst\ntest"}} -{"Key":"c"} -{"Key":"w"} -{"Get":{"state":"Test teˇ\ntest","mode":"Insert"}} -{"Put":{"state":"Test tesˇt\ntest"}} -{"Key":"c"} -{"Key":"w"} -{"Get":{"state":"Test tesˇ\ntest","mode":"Insert"}} -{"Put":{"state":"Test test\nˇ\ntest"}} -{"Key":"c"} -{"Key":"w"} -{"Get":{"state":"Test test\nˇ\ntest","mode":"Insert"}} -{"Put":{"state":"Test teˇst-test test"}} -{"Key":"c"} -{"Key":"shift-w"} -{"Get":{"state":"Test teˇ test","mode":"Insert"}} -{"Put":{"state":"tesˇt-test"}} -{"Key":"c"} -{"Key":"w"} -{"Get":{"state":"tesˇ-test","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_word_object.json b/crates/vim/test_data/test_change_word_object.json deleted file mode 100644 index 18baf78c6e..0000000000 --- a/crates/vim/test_data/test_change_word_object.json +++ /dev/null @@ -1,460 +0,0 @@ -{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumpsˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ\n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ\n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇfox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-ˇ over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumpsˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ\n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ\n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇfox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n ˇ over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick ˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick ˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brownˇ jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumpsˇ\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇ\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-ˇover\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ","mode":"Insert"}} -{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick ˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick ˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brownˇ jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumpsˇ\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇ\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n ˇover\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_clear_counts.json b/crates/vim/test_data/test_clear_counts.json deleted file mode 100644 index 6ef6b36017..0000000000 --- a/crates/vim/test_data/test_clear_counts.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"4"} -{"Key":"escape"} -{"Key":"3"} -{"Key":"d"} -{"Key":"l"} -{"Get":{"state":"The quick brown\nfox juˇ over\nthe lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_comma_semicolon.json b/crates/vim/test_data/test_comma_semicolon.json deleted file mode 100644 index e08b6963fa..0000000000 --- a/crates/vim/test_data/test_comma_semicolon.json +++ /dev/null @@ -1,34 +0,0 @@ -{"Put":{"state":"ˇone two three four"}} -{"Key":"f"} -{"Key":"o"} -{"Get":{"state":"one twˇo three four","mode":"Normal"}} -{"Key":","} -{"Get":{"state":"ˇone two three four","mode":"Normal"}} -{"Key":"2"} -{"Key":";"} -{"Get":{"state":"one two three fˇour","mode":"Normal"}} -{"Key":"shift-f"} -{"Key":"e"} -{"Get":{"state":"one two threˇe four","mode":"Normal"}} -{"Key":"2"} -{"Key":";"} -{"Get":{"state":"onˇe two three four","mode":"Normal"}} -{"Key":","} -{"Get":{"state":"one two thrˇee four","mode":"Normal"}} -{"Put":{"state":"ˇone two three four"}} -{"Key":"t"} -{"Key":"o"} -{"Get":{"state":"one tˇwo three four","mode":"Normal"}} -{"Key":","} -{"Get":{"state":"oˇne two three four","mode":"Normal"}} -{"Key":"2"} -{"Key":";"} -{"Get":{"state":"one two three ˇfour","mode":"Normal"}} -{"Key":"shift-t"} -{"Key":"e"} -{"Get":{"state":"one two threeˇ four","mode":"Normal"}} -{"Key":"3"} -{"Key":";"} -{"Get":{"state":"oneˇ two three four","mode":"Normal"}} -{"Key":","} -{"Get":{"state":"one two thˇree four","mode":"Normal"}} diff --git a/crates/vim/test_data/test_comma_w.json b/crates/vim/test_data/test_comma_w.json deleted file mode 100644 index ac7a91c80c..0000000000 --- a/crates/vim/test_data/test_comma_w.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Exec":{"command":"map ,w j"}} -{"Put":{"state":"ˇhello hello\nhello hello"}} -{"Key":"f"} -{"Key":"o"} -{"Key":";"} -{"Key":","} -{"Key":"w"} -{"Get":{"state":"hello hello\nhello hellˇo","mode":"Normal"}} -{"Put":{"state":"ˇhello hello\nhello hello"}} -{"Key":"f"} -{"Key":"o"} -{"Key":";"} -{"Key":","} -{"Key":"i"} -{"Get":{"state":"hellˇo hello\nhello hello","mode":"Insert"}} diff --git a/crates/vim/test_data/test_command_basics.json b/crates/vim/test_data/test_command_basics.json deleted file mode 100644 index 669d34409f..0000000000 --- a/crates/vim/test_data/test_command_basics.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"ˇa\nb\nc"}} -{"Key":":"} -{"Key":"j"} -{"Key":"enter"} -{"Key":"^"} -{"Get":{"state":"ˇa b\nc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_command_goto.json b/crates/vim/test_data/test_command_goto.json deleted file mode 100644 index 2f7ed10eeb..0000000000 --- a/crates/vim/test_data/test_command_goto.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":"ˇa\nb\nc"}} -{"Key":":"} -{"Key":"3"} -{"Key":"enter"} -{"Get":{"state":"a\nb\nˇc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_command_matching_lines.json b/crates/vim/test_data/test_command_matching_lines.json deleted file mode 100644 index 450aae0de0..0000000000 --- a/crates/vim/test_data/test_command_matching_lines.json +++ /dev/null @@ -1,19 +0,0 @@ -{"Put":{"state":"ˇa\nb\na\nb\na\n"}} -{"Key":":"} -{"Key":"g"} -{"Key":"/"} -{"Key":"a"} -{"Key":"/"} -{"Key":"d"} -{"Key":"enter"} -{"Get":{"state":"b\nb\nˇ","mode":"Normal"}} -{"Key":"u"} -{"Get":{"state":"ˇa\nb\na\nb\na\n","mode":"Normal"}} -{"Key":":"} -{"Key":"v"} -{"Key":"/"} -{"Key":"a"} -{"Key":"/"} -{"Key":"d"} -{"Key":"enter"} -{"Get":{"state":"a\na\nˇa","mode":"Normal"}} diff --git a/crates/vim/test_data/test_command_ranges.json b/crates/vim/test_data/test_command_ranges.json deleted file mode 100644 index d0e4928c6a..0000000000 --- a/crates/vim/test_data/test_command_ranges.json +++ /dev/null @@ -1,28 +0,0 @@ -{"Put":{"state":"ˇ1\n2\n3\n4\n4\n3\n2\n1"}} -{"Key":":"} -{"Key":"2"} -{"Key":","} -{"Key":"4"} -{"Key":"d"} -{"Key":"enter"} -{"Get":{"state":"1\nˇ4\n3\n2\n1","mode":"Normal"}} -{"Key":":"} -{"Key":"2"} -{"Key":","} -{"Key":"4"} -{"Key":"s"} -{"Key":"o"} -{"Key":"r"} -{"Key":"t"} -{"Key":"enter"} -{"Get":{"state":"1\nˇ2\n3\n4\n1","mode":"Normal"}} -{"Key":":"} -{"Key":"2"} -{"Key":","} -{"Key":"4"} -{"Key":"j"} -{"Key":"o"} -{"Key":"i"} -{"Key":"n"} -{"Key":"enter"} -{"Get":{"state":"1\nˇ2 3 4\n1","mode":"Normal"}} diff --git a/crates/vim/test_data/test_command_replace.json b/crates/vim/test_data/test_command_replace.json deleted file mode 100644 index d14a8a78ce..0000000000 --- a/crates/vim/test_data/test_command_replace.json +++ /dev/null @@ -1,33 +0,0 @@ -{"Put":{"state":"ˇa\nb\nb\nc"}} -{"Key":":"} -{"Key":"%"} -{"Key":"s"} -{"Key":"/"} -{"Key":"b"} -{"Key":"/"} -{"Key":"d"} -{"Key":"enter"} -{"Get":{"state":"a\nd\nˇd\nc","mode":"Normal"}} -{"Key":":"} -{"Key":"%"} -{"Key":"s"} -{"Key":":"} -{"Key":"."} -{"Key":":"} -{"Key":"\\"} -{"Key":"0"} -{"Key":"\\"} -{"Key":"0"} -{"Key":"enter"} -{"Get":{"state":"aa\ndd\ndd\nˇcc","mode":"Normal"}} -{"Key":"k"} -{"Key":":"} -{"Key":"s"} -{"Key":"/"} -{"Key":"d"} -{"Key":"d"} -{"Key":"/"} -{"Key":"e"} -{"Key":"e"} -{"Key":"enter"} -{"Get":{"state":"aa\ndd\nˇee\ncc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_command_search.json b/crates/vim/test_data/test_command_search.json deleted file mode 100644 index 705ce51fb7..0000000000 --- a/crates/vim/test_data/test_command_search.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"ˇa\nb\na\nc"}} -{"Key":":"} -{"Key":"/"} -{"Key":"b"} -{"Key":"enter"} -{"Get":{"state":"a\nˇb\na\nc","mode":"Normal"}} -{"Key":":"} -{"Key":"?"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"ˇa\nb\na\nc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_command_visual_replace.json b/crates/vim/test_data/test_command_visual_replace.json deleted file mode 100644 index 69713b19ee..0000000000 --- a/crates/vim/test_data/test_command_visual_replace.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"ˇ1\n2\n3\n4\n4\n3\n2\n1"}} -{"Key":"v"} -{"Key":"2"} -{"Key":"j"} -{"Key":":"} -{"Key":"s"} -{"Key":"/"} -{"Key":"."} -{"Key":"/"} -{"Key":"k"} -{"Key":"enter"} -{"Get":{"state":"k\nk\nˇk\n4\n4\n3\n2\n1","mode":"Normal"}} diff --git a/crates/vim/test_data/test_convert_to_lower_case.json b/crates/vim/test_data/test_convert_to_lower_case.json deleted file mode 100644 index 83d7435c89..0000000000 --- a/crates/vim/test_data/test_convert_to_lower_case.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"A😀c«DÉ1*fˇ»\n"}} -{"Key":"u"} -{"Get":{"state":"A😀cˇdé1*f\n","mode":"Normal"}} -{"Put":{"state":"ABˇc\n"}} -{"Key":"shift-v"} -{"Key":"u"} -{"Get":{"state":"ˇabc\n","mode":"Normal"}} -{"Put":{"state":"ˇAa\nBb\nCc"}} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Key":"u"} -{"Get":{"state":"ˇaa\nbb\nCc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_convert_to_rot13.json b/crates/vim/test_data/test_convert_to_rot13.json deleted file mode 100644 index 7ac67c885c..0000000000 --- a/crates/vim/test_data/test_convert_to_rot13.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"a😀C«dÉ1*fˇ»\n"}} -{"Key":"g"} -{"Key":"?"} -{"Get":{"state":"a😀CˇqÉ1*s\n","mode":"Normal"}} -{"Put":{"state":"abˇC\n"}} -{"Key":"shift-v"} -{"Key":"g"} -{"Key":"?"} -{"Get":{"state":"ˇnoP\n","mode":"Normal"}} -{"Put":{"state":"ˇaa\nbb\ncc"}} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Key":"g"} -{"Key":"?"} -{"Get":{"state":"ˇna\nob\ncc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_convert_to_upper_case.json b/crates/vim/test_data/test_convert_to_upper_case.json deleted file mode 100644 index ffc9771073..0000000000 --- a/crates/vim/test_data/test_convert_to_upper_case.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"a😀C«dÉ1*fˇ»\n"}} -{"Key":"shift-u"} -{"Get":{"state":"a😀CˇDÉ1*F\n","mode":"Normal"}} -{"Put":{"state":"abˇC\n"}} -{"Key":"shift-v"} -{"Key":"shift-u"} -{"Get":{"state":"ˇABC\n","mode":"Normal"}} -{"Put":{"state":"ˇaa\nbb\ncc"}} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Key":"shift-u"} -{"Get":{"state":"ˇAa\nBb\ncc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_ctrl_d_u.json b/crates/vim/test_data/test_ctrl_d_u.json deleted file mode 100644 index 77c35f24b9..0000000000 --- a/crates/vim/test_data/test_ctrl_d_u.json +++ /dev/null @@ -1,28 +0,0 @@ -{"SetOption":{"value":"scrolloff=3"}} -{"SetOption":{"value":"lines=12"}} -{"Put":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz"}} -{"Key":"4"} -{"Key":"j"} -{"Key":"ctrl-d"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\nˇjj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-d"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\nˇoo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"g"} -{"Key":"g"} -{"Key":"ctrl-d"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nˇii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-u"} -{"Get":{"state":"aa\nbb\ncc\nˇdd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-d"} -{"Key":"ctrl-d"} -{"Key":"4"} -{"Key":"j"} -{"Key":"ctrl-u"} -{"Key":"ctrl-u"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nˇhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"g"} -{"Key":"g"} -{"Key":"ctrl-d"} -{"Key":"ctrl-u"} -{"Key":"ctrl-u"} -{"Get":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} diff --git a/crates/vim/test_data/test_ctrl_f_b.json b/crates/vim/test_data/test_ctrl_f_b.json deleted file mode 100644 index 19c94d8b6e..0000000000 --- a/crates/vim/test_data/test_ctrl_f_b.json +++ /dev/null @@ -1,24 +0,0 @@ -{"SetOption":{"value":"scrolloff=3"}} -{"SetOption":{"value":"lines=12"}} -{"SetOption":{"value":"scrolloff=0"}} -{"Put":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz"}} -{"Key":"ctrl-f"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nˇii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-f"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nˇqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-b"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nˇrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-b"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\nˇjj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"SetOption":{"value":"scrolloff=3"}} -{"Key":"ctrl-f"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nˇll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-f"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\nˇtt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-b"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\nˇoo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-b"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\nˇgg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} diff --git a/crates/vim/test_data/test_ctrl_o_dot.json b/crates/vim/test_data/test_ctrl_o_dot.json deleted file mode 100644 index e414d785bf..0000000000 --- a/crates/vim/test_data/test_ctrl_o_dot.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"heˇllo world."}} -{"Key":"x"} -{"Key":"i"} -{"Key":"ctrl-o"} -{"Key":"."} -{"Get":{"state":"heˇo world.","mode":"Insert"}} -{"Key":"l"} -{"Key":"l"} -{"Key":"escape"} -{"Key":"."} -{"Get":{"state":"hellˇllo world.","mode":"Normal"}} diff --git a/crates/vim/test_data/test_ctrl_o_position.json b/crates/vim/test_data/test_ctrl_o_position.json deleted file mode 100644 index d8d76ac188..0000000000 --- a/crates/vim/test_data/test_ctrl_o_position.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"helˇlo world."}} -{"Key":"i"} -{"Key":"ctrl-o"} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"ˇ world.","mode":"Insert"}} -{"Key":"ctrl-o"} -{"Key":"p"} -{"Get":{"state":" helloˇworld.","mode":"Insert"}} diff --git a/crates/vim/test_data/test_ctrl_o_visual.json b/crates/vim/test_data/test_ctrl_o_visual.json deleted file mode 100644 index 23ec11d766..0000000000 --- a/crates/vim/test_data/test_ctrl_o_visual.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"helloˇ world."}} -{"Key":"i"} -{"Key":"ctrl-o"} -{"Key":"v"} -{"Key":"b"} -{"Key":"r"} -{"Key":"l"} -{"Get":{"state":"ˇllllllworld.","mode":"Insert"}} -{"Key":"ctrl-o"} -{"Key":"v"} -{"Key":"f"} -{"Key":"w"} -{"Key":"d"} -{"Get":{"state":"ˇorld.","mode":"Insert"}} diff --git a/crates/vim/test_data/test_ctrl_v.json b/crates/vim/test_data/test_ctrl_v.json deleted file mode 100644 index dfc090ab18..0000000000 --- a/crates/vim/test_data/test_ctrl_v.json +++ /dev/null @@ -1,24 +0,0 @@ -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"ctrl-v"} -{"Key":"0"} -{"Key":"0"} -{"Key":"0"} -{"Get":{"state":"\u0000ˇ","mode":"Insert"}} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Get":{"state":"\u0000jˇ","mode":"Insert"}} -{"Key":"ctrl-v"} -{"Key":"x"} -{"Key":"6"} -{"Key":"5"} -{"Get":{"state":"\u0000jeˇ","mode":"Insert"}} -{"Key":"ctrl-v"} -{"Key":"U"} -{"Key":"1"} -{"Key":"F"} -{"Key":"6"} -{"Key":"4"} -{"Key":"0"} -{"Key":"space"} -{"Get":{"state":"\u0000je🙀 ˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_ctrl_v_control.json b/crates/vim/test_data/test_ctrl_v_control.json deleted file mode 100644 index a5a55cf6d5..0000000000 --- a/crates/vim/test_data/test_ctrl_v_control.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"ctrl-v"} -{"Key":"ctrl-d"} -{"Get":{"state":"\u0004ˇ","mode":"Insert"}} -{"Key":"ctrl-v"} -{"Key":"ctrl-j"} -{"Get":{"state":"\u0004\u0000ˇ","mode":"Insert"}} -{"Key":"ctrl-v"} -{"Key":"tab"} -{"Get":{"state":"\u0004\u0000\tˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_ctrl_v_escape.json b/crates/vim/test_data/test_ctrl_v_escape.json deleted file mode 100644 index 8c0397ef0a..0000000000 --- a/crates/vim/test_data/test_ctrl_v_escape.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"ctrl-v"} -{"Key":"9"} -{"Key":"escape"} -{"Get":{"state":"ˇ\t","mode":"Normal"}} -{"Key":"i"} -{"Key":"ctrl-v"} -{"Key":"escape"} -{"Get":{"state":"\u001bˇ\t","mode":"Insert"}} diff --git a/crates/vim/test_data/test_ctrl_w_override.json b/crates/vim/test_data/test_ctrl_w_override.json deleted file mode 100644 index fe8ae94a77..0000000000 --- a/crates/vim/test_data/test_ctrl_w_override.json +++ /dev/null @@ -1,4 +0,0 @@ -{"Exec":{"command":"map D"}} -{"Put":{"state":"ˇhi"}} -{"Key":"ctrl-w"} -{"Get":{"state":"ˇ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_ctrl_y_e.json b/crates/vim/test_data/test_ctrl_y_e.json deleted file mode 100644 index c43977a6ad..0000000000 --- a/crates/vim/test_data/test_ctrl_y_e.json +++ /dev/null @@ -1,35 +0,0 @@ -{"SetOption":{"value":"scrolloff=3"}} -{"SetOption":{"value":"lines=12"}} -{"Put":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz"}} -{"Key":"ctrl-e"} -{"Get":{"state":"aa\nbb\ncc\ndd\nˇee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-e"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nˇff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-e"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\nˇgg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-e"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nˇhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-e"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nˇii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-e"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\nˇjj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-e"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nˇkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-e"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nˇll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-y"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nˇll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-y"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nˇll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-y"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nˇll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-y"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nˇkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-y"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\nˇjj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-y"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nˇii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-y"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nˇhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-y"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\nˇgg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} diff --git a/crates/vim/test_data/test_d_search.json b/crates/vim/test_data/test_d_search.json deleted file mode 100644 index 9cdc855dbf..0000000000 --- a/crates/vim/test_data/test_d_search.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":"ˇa.c. abcd a.c. abcd"}} -{"Key":"d"} -{"Key":"/"} -{"Key":"c"} -{"Key":"d"} -{"Key":"enter"} -{"Get":{"state":"ˇcd a.c. abcd","mode":"Normal"}} diff --git a/crates/vim/test_data/test_dd.json b/crates/vim/test_data/test_dd.json deleted file mode 100644 index c6dc30882e..0000000000 --- a/crates/vim/test_data/test_dd.json +++ /dev/null @@ -1,24 +0,0 @@ -{"Put":{"state":"ˇ"}} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The ˇquick"}} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"brownˇ fox\njumps over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"The quick\njumps ˇover","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"The quick\nbrown ˇfox","mode":"Normal"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"The quick\nˇbrown fox","mode":"Normal"}} diff --git a/crates/vim/test_data/test_dd_then_paste_without_trailing_newline.json b/crates/vim/test_data/test_dd_then_paste_without_trailing_newline.json deleted file mode 100644 index 5b10a2fe28..0000000000 --- a/crates/vim/test_data/test_dd_then_paste_without_trailing_newline.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":"heˇllo"}} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Key":"p"} -{"Key":"p"} -{"Get":{"state":"\nhello\nˇhello","mode":"Normal"}} diff --git a/crates/vim/test_data/test_del_marks.json b/crates/vim/test_data/test_del_marks.json deleted file mode 100644 index c326c6d61e..0000000000 --- a/crates/vim/test_data/test_del_marks.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"ˇa\nb\na\nb\na\n"}} -{"Key":"m"} -{"Key":"a"} -{"Key":":"} -{"Key":"d"} -{"Key":"e"} -{"Key":"l"} -{"Key":"m"} -{"Key":"space"} -{"Key":"a"} -{"Key":"enter"} diff --git a/crates/vim/test_data/test_delete_0.json b/crates/vim/test_data/test_delete_0.json deleted file mode 100644 index 10095cefbb..0000000000 --- a/crates/vim/test_data/test_delete_0.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"The qˇuick\nbrown fox"}} -{"Key":"d"} -{"Key":"0"} -{"Get":{"state":"ˇuick\nbrown fox","mode":"Normal"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"d"} -{"Key":"0"} -{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_b.json b/crates/vim/test_data/test_delete_b.json deleted file mode 100644 index 932a4c1967..0000000000 --- a/crates/vim/test_data/test_delete_b.json +++ /dev/null @@ -1,24 +0,0 @@ -{"Put":{"state":"Teˇst Test"}} -{"Key":"d"} -{"Key":"b"} -{"Get":{"state":"ˇst Test","mode":"Normal"}} -{"Put":{"state":"Test ˇtest"}} -{"Key":"d"} -{"Key":"b"} -{"Get":{"state":"ˇtest","mode":"Normal"}} -{"Put":{"state":"Test1 test2 ˇtest3"}} -{"Key":"d"} -{"Key":"b"} -{"Get":{"state":"Test1 ˇtest3","mode":"Normal"}} -{"Put":{"state":"Test test\nˇtest"}} -{"Key":"d"} -{"Key":"b"} -{"Get":{"state":"Testˇ \ntest","mode":"Normal"}} -{"Put":{"state":"Test test\nˇ\ntest"}} -{"Key":"d"} -{"Key":"b"} -{"Get":{"state":"Testˇ \n\ntest","mode":"Normal"}} -{"Put":{"state":"Test test-test ˇtest"}} -{"Key":"d"} -{"Key":"shift-b"} -{"Get":{"state":"Test ˇtest","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_end_of_document.json b/crates/vim/test_data/test_delete_end_of_document.json deleted file mode 100644 index 863d15aca1..0000000000 --- a/crates/vim/test_data/test_delete_end_of_document.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}} -{"Key":"d"} -{"Key":"shift-g"} -{"Get":{"state":"The qˇuick","mode":"Normal"}} -{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}} -{"Key":"d"} -{"Key":"shift-g"} -{"Get":{"state":"The qˇuick","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}} -{"Key":"d"} -{"Key":"shift-g"} -{"Get":{"state":"The quick\nbrown fox\njumpsˇ over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps over\nˇ"}} -{"Key":"d"} -{"Key":"shift-g"} -{"Get":{"state":"The quick\nbrown fox\nˇjumps over","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_end_of_line.json b/crates/vim/test_data/test_delete_end_of_line.json deleted file mode 100644 index 93f0cc2459..0000000000 --- a/crates/vim/test_data/test_delete_end_of_line.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"The qˇuick\nbrown fox"}} -{"Key":"d"} -{"Key":"$"} -{"Get":{"state":"The ˇq\nbrown fox","mode":"Normal"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"d"} -{"Key":"$"} -{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_end_of_paragraph.json b/crates/vim/test_data/test_delete_end_of_paragraph.json deleted file mode 100644 index 860fed7a68..0000000000 --- a/crates/vim/test_data/test_delete_end_of_paragraph.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"ˇhello world.\n\nhello world."}} -{"Key":"d"} -{"Key":"}"} -{"Get":{"state":"ˇ\nhello world.","mode":"Normal"}} -{"Put":{"state":"ˇhello world.\nhello world."}} -{"Key":"d"} -{"Key":"}"} -{"Get":{"state":"ˇ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_gg.json b/crates/vim/test_data/test_delete_gg.json deleted file mode 100644 index de6aca9665..0000000000 --- a/crates/vim/test_data/test_delete_gg.json +++ /dev/null @@ -1,20 +0,0 @@ -{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}} -{"Key":"d"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"jumpsˇ over\nthe lazy","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}} -{"Key":"d"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over\nthe lazy"}} -{"Key":"d"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"brownˇ fox\njumps over\nthe lazy","mode":"Normal"}} -{"Put":{"state":"ˇ\nbrown fox\njumps over\nthe lazy"}} -{"Key":"d"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"ˇbrown fox\njumps over\nthe lazy","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_h.json b/crates/vim/test_data/test_delete_h.json deleted file mode 100644 index cf842a386e..0000000000 --- a/crates/vim/test_data/test_delete_h.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"Teˇst"}} -{"Key":"d"} -{"Key":"h"} -{"Get":{"state":"Tˇst","mode":"Normal"}} -{"Put":{"state":"Tˇest"}} -{"Key":"d"} -{"Key":"h"} -{"Get":{"state":"ˇest","mode":"Normal"}} -{"Put":{"state":"ˇTest"}} -{"Key":"d"} -{"Key":"h"} -{"Get":{"state":"ˇTest","mode":"Normal"}} -{"Put":{"state":"Test\nˇtest"}} -{"Key":"d"} -{"Key":"h"} -{"Get":{"state":"Test\nˇtest","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_j.json b/crates/vim/test_data/test_delete_j.json deleted file mode 100644 index 76c2f098d3..0000000000 --- a/crates/vim/test_data/test_delete_j.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"d"} -{"Key":"j"} -{"Get":{"state":"The quˇick","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"d"} -{"Key":"j"} -{"Get":{"state":"The quick\nbrown fox\njumps ˇover","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"d"} -{"Key":"j"} -{"Get":{"state":"jumpsˇ over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\nˇ"}} -{"Key":"d"} -{"Key":"j"} -{"Get":{"state":"The quick\nbrown fox\nˇ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_k.json b/crates/vim/test_data/test_delete_k.json deleted file mode 100644 index 75c032430c..0000000000 --- a/crates/vim/test_data/test_delete_k.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"d"} -{"Key":"k"} -{"Get":{"state":"jumps ˇover","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"d"} -{"Key":"k"} -{"Get":{"state":"The quˇick","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"d"} -{"Key":"k"} -{"Get":{"state":"The qˇuick\nbrown fox\njumps over","mode":"Normal"}} -{"Put":{"state":"ˇbrown fox\njumps over"}} -{"Key":"d"} -{"Key":"k"} -{"Get":{"state":"ˇbrown fox\njumps over","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_key_can_remove_last_character.json b/crates/vim/test_data/test_delete_key_can_remove_last_character.json deleted file mode 100644 index ea4cd71e9f..0000000000 --- a/crates/vim/test_data/test_delete_key_can_remove_last_character.json +++ /dev/null @@ -1,3 +0,0 @@ -{"Put":{"state":"abˇc"}} -{"Key":"delete"} -{"Get":{"state":"aˇb","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_l.json b/crates/vim/test_data/test_delete_l.json deleted file mode 100644 index 60b3a6c9b1..0000000000 --- a/crates/vim/test_data/test_delete_l.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"ˇTest"}} -{"Key":"d"} -{"Key":"l"} -{"Get":{"state":"ˇest","mode":"Normal"}} -{"Put":{"state":"Teˇst"}} -{"Key":"d"} -{"Key":"l"} -{"Get":{"state":"Teˇt","mode":"Normal"}} -{"Put":{"state":"Tesˇt"}} -{"Key":"d"} -{"Key":"l"} -{"Get":{"state":"Teˇs","mode":"Normal"}} -{"Put":{"state":"Tesˇt\ntest"}} -{"Key":"d"} -{"Key":"l"} -{"Get":{"state":"Teˇs\ntest","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_left.json b/crates/vim/test_data/test_delete_left.json deleted file mode 100644 index a8a242f1f6..0000000000 --- a/crates/vim/test_data/test_delete_left.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"ˇTest"}} -{"Key":"shift-x"} -{"Get":{"state":"ˇTest","mode":"Normal"}} -{"Put":{"state":"Tˇest"}} -{"Key":"shift-x"} -{"Get":{"state":"ˇest","mode":"Normal"}} -{"Put":{"state":"Teˇst"}} -{"Key":"shift-x"} -{"Get":{"state":"Tˇst","mode":"Normal"}} -{"Put":{"state":"Tesˇt"}} -{"Key":"shift-x"} -{"Get":{"state":"Teˇt","mode":"Normal"}} -{"Put":{"state":"Test\nˇtest"}} -{"Key":"shift-x"} -{"Get":{"state":"Test\nˇtest","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_next_word_end.json b/crates/vim/test_data/test_delete_next_word_end.json deleted file mode 100644 index 2467813b49..0000000000 --- a/crates/vim/test_data/test_delete_next_word_end.json +++ /dev/null @@ -1,20 +0,0 @@ -{"Put":{"state":"Teˇst Test\n"}} -{"Key":"d"} -{"Key":"e"} -{"Get":{"state":"Teˇ Test\n","mode":"Normal"}} -{"Put":{"state":"Tˇest test\n"}} -{"Key":"d"} -{"Key":"e"} -{"Get":{"state":"Tˇ test\n","mode":"Normal"}} -{"Put":{"state":"Test teˇst\ntest"}} -{"Key":"d"} -{"Key":"e"} -{"Get":{"state":"Test tˇe\ntest","mode":"Normal"}} -{"Put":{"state":"Test tesˇt\ntest"}} -{"Key":"d"} -{"Key":"e"} -{"Get":{"state":"Test teˇs","mode":"Normal"}} -{"Put":{"state":"Test teˇst-test test"}} -{"Key":"d"} -{"Key":"e"} -{"Get":{"state":"Test teˇ-test test","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_paragraph.json b/crates/vim/test_data/test_delete_paragraph.json deleted file mode 100644 index 3b09749bab..0000000000 --- a/crates/vim/test_data/test_delete_paragraph.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"helˇlo world.\n\nhello world.\n"}} -{"Key":"y"} -{"Key":"}"} -{"Key":"d"} -{"Key":"}"} -{"Get":{"state":"heˇl\n\nhello world.\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"lo world."}} -{"Get":{"state":"heˇl\n\nhello world.\n","mode":"Normal"}} -{"Put":{"state":"ˇhello world.\n\nhello world.\n"}} -{"Key":"y"} -{"Key":"}"} -{"Key":"d"} -{"Key":"}"} -{"Get":{"state":"ˇ\nhello world.\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_paragraph_motion.json b/crates/vim/test_data/test_delete_paragraph_motion.json deleted file mode 100644 index d4086a8ca5..0000000000 --- a/crates/vim/test_data/test_delete_paragraph_motion.json +++ /dev/null @@ -1,18 +0,0 @@ -{"Put":{"state":"ˇhello world.\n\nhello world.\n"}} -{"Key":"y"} -{"Key":"}"} -{"Get":{"state":"ˇhello world.\n\nhello world.\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"hello world.\n"}} -{"Key":"d"} -{"Key":"}"} -{"Get":{"state":"ˇ\nhello world.\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"hello world.\n"}} -{"Put":{"state":"helˇlo world.\n\nhello world.\n"}} -{"Key":"y"} -{"Key":"}"} -{"Get":{"state":"helˇlo world.\n\nhello world.\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"lo world."}} -{"Key":"d"} -{"Key":"}"} -{"Get":{"state":"heˇl\n\nhello world.\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"lo world."}} diff --git a/crates/vim/test_data/test_delete_paragraph_object.json b/crates/vim/test_data/test_delete_paragraph_object.json deleted file mode 100644 index 2cf1402ae3..0000000000 --- a/crates/vim/test_data/test_delete_paragraph_object.json +++ /dev/null @@ -1,430 +0,0 @@ -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumpˇs over the lazy dog."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dogˇ."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.ˇ"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumpˇs over the lazy dog."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dogˇ."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.ˇ"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps overˇ\nthe lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps overˇ\nthe lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} -{"Put":{"state":"\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} -{"Put":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} -{"Put":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} -{"Put":{"state":"\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} -{"Put":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nˇover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nˇover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇ\n \t\n\t \t\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇ\n \t\n\t \t\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nˇover the lazy dog.","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nˇover the lazy dog.","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\t \t\n","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\nThe quick brown fox jumps over the lazy dog.\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇThe quick brown fox jumps over the lazy dog.\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumpˇs over the lazy dog.\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.ˇ\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumpˇs over the lazy dog.\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.ˇ\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_paragraph_object_with_soft_wrap.json b/crates/vim/test_data/test_delete_paragraph_object_with_soft_wrap.json deleted file mode 100644 index 19dcd175b3..0000000000 --- a/crates/vim/test_data/test_delete_paragraph_object_with_soft_wrap.json +++ /dev/null @@ -1,72 +0,0 @@ -{"SetOption":{"value":"wrap"}} -{"SetOption":{"value":"columns=20"}} -{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Normal"}} -{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"Second paragraph that is also quite long and will definitely wrap under soft wrap conditions andˇ should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nThird paragraph with additional long text content that will also wrap when line length is constraˇined by the wrapping settings.\n","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\nˇ","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\nˇ","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\nˇ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_paragraph_whitespace.json b/crates/vim/test_data/test_delete_paragraph_whitespace.json deleted file mode 100644 index e07b18eaa3..0000000000 --- a/crates/vim/test_data/test_delete_paragraph_whitespace.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":"a\n ˇ•\naaaaaaaaaaaaa\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"a\naaaaaaaˇaaaaaa\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_sentence.json b/crates/vim/test_data/test_delete_sentence.json deleted file mode 100644 index 6056b207e4..0000000000 --- a/crates/vim/test_data/test_delete_sentence.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"ˇFirst.\nFourth.\n"}} -{"Key":"d"} -{"Key":")"} -{"Get":{"state":"ˇFourth.\n","mode":"Normal"}} -{"Put":{"state":"First.\nˇSecond.\nFourth.\n"}} -{"Key":"d"} -{"Key":"("} -{"Get":{"state":"ˇSecond.\nFourth.\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_sentence_object.json b/crates/vim/test_data/test_delete_sentence_object.json deleted file mode 100644 index e45ebd5c4e..0000000000 --- a/crates/vim/test_data/test_delete_sentence_object.json +++ /dev/null @@ -1,270 +0,0 @@ -{"Put":{"state":"ˇThe quick brown? Fox Jumps! Over the lazy."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Fox Jumps! Over the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick ˇbrown? Fox Jumps! Over the lazy."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Fox Jumps! Over the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ? Fox Jumps! Over the lazy."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Fox Jumps! Over the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brown?ˇ Fox Jumps! Over the lazy."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown?ˇFox Jumps! Over the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brown? ˇFox Jumps! Over the lazy."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇ Over the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jˇumps! Over the lazy."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇ Over the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jumpsˇ! Over the lazy."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇ Over the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jumps!ˇ Over the lazy."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps!ˇOver the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jumps! Ovˇer the lazy."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps!ˇ ","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jumps! Over theˇ lazy."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps!ˇ ","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jumps! Over the lazyˇ."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps!ˇ ","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick ˇbrown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy doˇg. The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dogˇ. The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇThe quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. ˇThe quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ \n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. The quick ˇ\nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ \n","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown.)]'\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The ˇquick brown.)]'\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ.)]'\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)ˇ]'\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)]ˇ'\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)]'ˇ\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)]'\" Brown ˇfox jumps. "}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown.)]'\" ˇ ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)]'\" Brown fox jumpsˇ. "}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown.)]'\" ˇ ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)]'\" Brown fox jumps.ˇ "}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown.)]'\" Brown fox jumpsˇ.","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown? Fox Jumps! Over the lazy."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇFox Jumps! Over the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick ˇbrown? Fox Jumps! Over the lazy."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇFox Jumps! Over the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ? Fox Jumps! Over the lazy."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇFox Jumps! Over the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brown? ˇFox Jumps! Over the lazy."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇOver the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jˇumps! Over the lazy."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇOver the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jumpsˇ! Over the lazy."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇOver the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jumps!ˇ Over the lazy."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumpsˇ!","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jumps! Ovˇer the lazy."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumpsˇ!","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jumps! Over theˇ lazy."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumpsˇ!","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jumps! Over the lazyˇ."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumpsˇ!","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick ˇbrown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy doˇg. The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dogˇ. The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dogˇ.\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. ˇThe quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dogˇ.\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. The quick ˇ\nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dogˇ.\n","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown.)]'\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The ˇquick brown.)]'\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ.)]'\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)ˇ]'\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)]ˇ'\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)]'ˇ\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)]'\" Brown ˇfox jumps. "}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown.)]'\"ˇ ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)]'\" Brown fox jumpsˇ. "}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown.)]'\"ˇ ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_surrounding_character_objects.json b/crates/vim/test_data/test_delete_surrounding_character_objects.json deleted file mode 100644 index 3d4e6aeec0..0000000000 --- a/crates/vim/test_data/test_delete_surrounding_character_objects.json +++ /dev/null @@ -1,518 +0,0 @@ -{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck brˇo\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck brˇo\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)ˇe ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ˇ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()quˇi(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ˇck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck broˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)ˇfox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox juˇmps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇer\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe ˇlazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)ˇe ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ˇ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()quˇi(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ˇck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck broˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)ˇfox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox juˇmps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇer\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe ˇlazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)ˇe ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ˇ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()quˇi(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ˇck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck broˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)ˇfox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox juˇmps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇer\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe ˇlazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)ˇe ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ˇ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()quˇi(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ˇck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck broˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)ˇfox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox juˇmps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇer\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe ˇlazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_to_adjacent_character.json b/crates/vim/test_data/test_delete_to_adjacent_character.json deleted file mode 100644 index 130719c890..0000000000 --- a/crates/vim/test_data/test_delete_to_adjacent_character.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"ˇax"}} -{"Key":"d"} -{"Key":"t"} -{"Key":"x"} -{"Get":{"state":"ˇx","mode":"Normal"}} -{"Put":{"state":"aˇx"}} -{"Key":"d"} -{"Key":"t"} -{"Key":"x"} -{"Get":{"state":"aˇx","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_to_end_of_line.json b/crates/vim/test_data/test_delete_to_end_of_line.json deleted file mode 100644 index e15d5d0c4a..0000000000 --- a/crates/vim/test_data/test_delete_to_end_of_line.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"The qˇuick\nbrown fox"}} -{"Key":"shift-d"} -{"Get":{"state":"The ˇq\nbrown fox","mode":"Normal"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"shift-d"} -{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_to_line.json b/crates/vim/test_data/test_delete_to_line.json deleted file mode 100644 index eae919b039..0000000000 --- a/crates/vim/test_data/test_delete_to_line.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}} -{"Key":"d"} -{"Key":"3"} -{"Key":"shift-g"} -{"Get":{"state":"The quick\nthe lˇazy","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}} -{"Key":"d"} -{"Key":"3"} -{"Key":"shift-g"} -{"Get":{"state":"The quick\nbrownˇ fox","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps over\nˇ"}} -{"Key":"d"} -{"Key":"2"} -{"Key":"shift-g"} -{"Get":{"state":"ˇThe quick","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_unmatched_brace.json b/crates/vim/test_data/test_delete_unmatched_brace.json deleted file mode 100644 index e9308edc8b..0000000000 --- a/crates/vim/test_data/test_delete_unmatched_brace.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"fn o(wow: i32) {\n othˇ(wow)\n oth(wow)\n}\n"}} -{"Key":"d"} -{"Key":"]"} -{"Key":"}"} -{"Get":{"state":"fn o(wow: i32) {\n otˇh\n}\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"(wow)\n oth(wow)"}} -{"Put":{"state":"fn o(wow: i32) {\n ˇoth(wow)\n oth(wow)\n}\n"}} -{"Key":"d"} -{"Key":"]"} -{"Key":"}"} -{"Get":{"state":"fn o(wow: i32) {\nˇ}\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":" oth(wow)\n oth(wow)\n"}} diff --git a/crates/vim/test_data/test_delete_w.json b/crates/vim/test_data/test_delete_w.json deleted file mode 100644 index e5e48ca444..0000000000 --- a/crates/vim/test_data/test_delete_w.json +++ /dev/null @@ -1,28 +0,0 @@ -{"Put":{"state":"Test tesˇt\n test"}} -{"Key":"d"} -{"Key":"w"} -{"Get":{"state":"Test teˇs\n test","mode":"Normal"}} -{"Put":{"state":"Teˇst"}} -{"Key":"d"} -{"Key":"w"} -{"Get":{"state":"Tˇe","mode":"Normal"}} -{"Put":{"state":"Tˇest test"}} -{"Key":"d"} -{"Key":"w"} -{"Get":{"state":"Tˇtest","mode":"Normal"}} -{"Put":{"state":"Test teˇst\ntest"}} -{"Key":"d"} -{"Key":"w"} -{"Get":{"state":"Test tˇe\ntest","mode":"Normal"}} -{"Put":{"state":"Test tesˇt\ntest"}} -{"Key":"d"} -{"Key":"w"} -{"Get":{"state":"Test teˇs\ntest","mode":"Normal"}} -{"Put":{"state":"Test test\nˇ\ntest"}} -{"Key":"d"} -{"Key":"w"} -{"Get":{"state":"Test test\nˇtest","mode":"Normal"}} -{"Put":{"state":"Test teˇst-test test"}} -{"Key":"d"} -{"Key":"shift-w"} -{"Get":{"state":"Test teˇtest","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_with_counts.json b/crates/vim/test_data/test_delete_with_counts.json deleted file mode 100644 index de19c5d29d..0000000000 --- a/crates/vim/test_data/test_delete_with_counts.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"d"} -{"Key":"2"} -{"Key":"d"} -{"Get":{"state":"the ˇlazy dog","mode":"Normal"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"2"} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"the ˇlazy dog","mode":"Normal"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe moon,\na star, and\nthe lazy dog"}} -{"Key":"2"} -{"Key":"d"} -{"Key":"2"} -{"Key":"d"} -{"Get":{"state":"the ˇlazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_word_object.json b/crates/vim/test_data/test_delete_word_object.json deleted file mode 100644 index 9c11d89a12..0000000000 --- a/crates/vim/test_data/test_delete_word_object.json +++ /dev/null @@ -1,460 +0,0 @@ -{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick browˇn\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumpsˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy doˇg\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick browˇn\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ\n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ\n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇfox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-ˇ over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy doˇg\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n","mode":"Normal"}} -{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick browˇn\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumpsˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy doˇg\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick browˇn\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ\n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ\n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇfox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n ˇ over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy doˇg\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n","mode":"Normal"}} -{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quickˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quickˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brownˇ jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumpˇs\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy doˇg\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-ˇover\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy doˇg\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nˇthe lazy dog ","mode":"Normal"}} -{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quickˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quickˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brownˇ jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumpˇs\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy doˇg\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n ˇover\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy doˇg\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nˇthe lazy dog ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_dgn_repeat.json b/crates/vim/test_data/test_dgn_repeat.json deleted file mode 100644 index fc1db9e778..0000000000 --- a/crates/vim/test_data/test_dgn_repeat.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"aaˇ aa aa aa aa"}} -{"Key":"/"} -{"Key":"a"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}} -{"Key":"d"} -{"Key":"g"} -{"Key":"n"} -{"Get":{"state":"aa ˇ aa aa aa","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"aa ˇ aa aa","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"aa ˇ aa","mode":"Normal"}} diff --git a/crates/vim/test_data/test_digraph_find.json b/crates/vim/test_data/test_digraph_find.json deleted file mode 100644 index 7d5811bd4d..0000000000 --- a/crates/vim/test_data/test_digraph_find.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"ˇHellö world"}} -{"Key":"f"} -{"Key":"ctrl-k"} -{"Key":"o"} -{"Key":":"} -{"Get":{"state":"Hellˇö world","mode":"Normal"}} -{"Put":{"state":"ˇHellö world"}} -{"Key":"t"} -{"Key":"ctrl-k"} -{"Key":"o"} -{"Key":":"} -{"Get":{"state":"Helˇlö world","mode":"Normal"}} diff --git a/crates/vim/test_data/test_digraph_insert_mode.json b/crates/vim/test_data/test_digraph_insert_mode.json deleted file mode 100644 index e7cebb262e..0000000000 --- a/crates/vim/test_data/test_digraph_insert_mode.json +++ /dev/null @@ -1,21 +0,0 @@ -{"Put":{"state":"Hellˇo"}} -{"Key":"a"} -{"Key":"ctrl-k"} -{"Key":"o"} -{"Key":":"} -{"Key":"escape"} -{"Get":{"state":"Helloˇö","mode":"Normal"}} -{"Put":{"state":"Hellˇo"}} -{"Key":"a"} -{"Key":"ctrl-k"} -{"Key":":"} -{"Key":"o"} -{"Key":"escape"} -{"Get":{"state":"Helloˇö","mode":"Normal"}} -{"Put":{"state":"Hellˇo"}} -{"Key":"i"} -{"Key":"ctrl-k"} -{"Key":"o"} -{"Key":":"} -{"Key":"escape"} -{"Get":{"state":"Hellˇöo","mode":"Normal"}} diff --git a/crates/vim/test_data/test_digraph_insert_multicursor.json b/crates/vim/test_data/test_digraph_insert_multicursor.json deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/vim/test_data/test_digraph_keymap_conflict.json b/crates/vim/test_data/test_digraph_keymap_conflict.json deleted file mode 100644 index 0481fbb724..0000000000 --- a/crates/vim/test_data/test_digraph_keymap_conflict.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":"Hellˇo"}} -{"Key":"a"} -{"Key":"ctrl-k"} -{"Key":"s"} -{"Key":","} -{"Key":"escape"} -{"Get":{"state":"Helloˇş","mode":"Normal"}} diff --git a/crates/vim/test_data/test_digraph_replace.json b/crates/vim/test_data/test_digraph_replace.json deleted file mode 100644 index 7615263339..0000000000 --- a/crates/vim/test_data/test_digraph_replace.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"Hellˇo"}} -{"Key":"r"} -{"Key":"ctrl-k"} -{"Key":"o"} -{"Key":":"} -{"Get":{"state":"Hellˇö","mode":"Normal"}} diff --git a/crates/vim/test_data/test_digraph_replace_mode.json b/crates/vim/test_data/test_digraph_replace_mode.json deleted file mode 100644 index 0030fde850..0000000000 --- a/crates/vim/test_data/test_digraph_replace_mode.json +++ /dev/null @@ -1,19 +0,0 @@ -{"Put":{"state":"ˇHello"}} -{"Key":"shift-r"} -{"Key":"ctrl-k"} -{"Key":"a"} -{"Key":"'"} -{"Key":"ctrl-k"} -{"Key":"e"} -{"Key":"`"} -{"Key":"ctrl-k"} -{"Key":"i"} -{"Key":":"} -{"Key":"ctrl-k"} -{"Key":"o"} -{"Key":"~"} -{"Key":"ctrl-k"} -{"Key":"u"} -{"Key":"-"} -{"Key":"escape"} -{"Get":{"state":"áèïõˇū","mode":"Normal"}} diff --git a/crates/vim/test_data/test_dot_mark.json b/crates/vim/test_data/test_dot_mark.json deleted file mode 100644 index 27edd9d2ab..0000000000 --- a/crates/vim/test_data/test_dot_mark.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"one two\nthree fˇr"}} -{"Key":"i"} -{"Key":"o"} -{"Key":"escape"} -{"Key":"k"} -{"Key":"`"} -{"Key":"."} -{"Get":{"state":"one two\nthree fˇor","mode":"Normal"}} diff --git a/crates/vim/test_data/test_dot_repeat.json b/crates/vim/test_data/test_dot_repeat.json deleted file mode 100644 index 331ef52ecb..0000000000 --- a/crates/vim/test_data/test_dot_repeat.json +++ /dev/null @@ -1,38 +0,0 @@ -{"Put":{"state":"ˇhello"}} -{"Key":"o"} -{"Key":"w"} -{"Key":"o"} -{"Key":"r"} -{"Key":"l"} -{"Key":"d"} -{"Key":"escape"} -{"Get":{"state":"hello\nworlˇd","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"hello\nworld\nworlˇd","mode":"Normal"}} -{"Key":"^"} -{"Key":"d"} -{"Key":"f"} -{"Key":"o"} -{"Key":"g"} -{"Key":"g"} -{"Key":"."} -{"Get":{"state":"ˇ\nworld\nrld","mode":"Normal"}} -{"Key":"j"} -{"Key":"y"} -{"Key":"y"} -{"Key":"p"} -{"Key":"shift-g"} -{"Key":"y"} -{"Key":"y"} -{"Key":"."} -{"Get":{"state":"\nworld\nworld\nrld\nˇrld","mode":"Normal"}} -{"Put":{"state":"ˇthe quick brown fox"}} -{"Key":"2"} -{"Key":"~"} -{"Key":"."} -{"Put":{"state":"THE ˇquick brown fox"}} -{"Key":"3"} -{"Key":"."} -{"Put":{"state":"THE QUIˇck brown fox"}} -{"Key":"."} -{"Get":{"state":"THE QUICK ˇbrown fox","mode":"Normal"}} diff --git a/crates/vim/test_data/test_dw_eol.json b/crates/vim/test_data/test_dw_eol.json deleted file mode 100644 index 93ec9b1066..0000000000 --- a/crates/vim/test_data/test_dw_eol.json +++ /dev/null @@ -1,6 +0,0 @@ -{"SetOption":{"value":"wrap"}} -{"SetOption":{"value":"columns=12"}} -{"Put":{"state":"twelve ˇchar twelve char\ntwelve char"}} -{"Key":"d"} -{"Key":"w"} -{"Get":{"state":"twelve ˇtwelve char\ntwelve char","mode":"Normal"}} diff --git a/crates/vim/test_data/test_end_of_document.json b/crates/vim/test_data/test_end_of_document.json deleted file mode 100644 index 2ad056170c..0000000000 --- a/crates/vim/test_data/test_end_of_document.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"The qˇuick\n\nbrown fox jumps\nover the lazy dog"}} -{"Key":"shift-g"} -{"Get":{"state":"The quick\n\nbrown fox jumps\nover ˇthe lazy dog","mode":"Normal"}} -{"Key":"shift-g"} -{"Get":{"state":"The quick\n\nbrown fox jumps\nover ˇthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick\n\nbrown fox jumps\nover the laˇzy dog"}} -{"Key":"shift-g"} -{"Get":{"state":"The quick\n\nbrown fox jumps\nover the laˇzy dog","mode":"Normal"}} -{"Put":{"state":"\n\nbrown fox jumps\nover the laˇzy dog"}} -{"Key":"shift-g"} -{"Get":{"state":"\n\nbrown fox jumps\nover the laˇzy dog","mode":"Normal"}} -{"Put":{"state":"ˇ\n\nbrown fox jumps\nover the lazydog"}} -{"Key":"2"} -{"Key":"shift-g"} -{"Get":{"state":"\nˇ\nbrown fox jumps\nover the lazydog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_end_of_line_downward.json b/crates/vim/test_data/test_end_of_line_downward.json deleted file mode 100644 index e3563f6b3f..0000000000 --- a/crates/vim/test_data/test_end_of_line_downward.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"ˇ one\n two \nthree"}} -{"Key":"g"} -{"Key":"_"} -{"Get":{"state":" onˇe\n two \nthree","mode":"Normal"}} -{"Put":{"state":"ˇ one \n two \nthree"}} -{"Key":"g"} -{"Key":"_"} -{"Get":{"state":" onˇe \n two \nthree","mode":"Normal"}} -{"Key":"2"} -{"Key":"g"} -{"Key":"_"} -{"Get":{"state":" one \n twˇo \nthree","mode":"Normal"}} diff --git a/crates/vim/test_data/test_end_of_line_with_neovim.json b/crates/vim/test_data/test_end_of_line_with_neovim.json deleted file mode 100644 index 58ac05134a..0000000000 --- a/crates/vim/test_data/test_end_of_line_with_neovim.json +++ /dev/null @@ -1,9 +0,0 @@ -{"Put":{"state":"ˇaa\nbb\ncc"}} -{"Key":"$"} -{"Get":{"state":"aˇa\nbb\ncc","mode":"Normal"}} -{"Key":"2"} -{"Key":"$"} -{"Get":{"state":"aa\nbˇb\ncc","mode":"Normal"}} -{"Key":"4"} -{"Key":"$"} -{"Get":{"state":"aa\nbb\ncˇc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_end_of_word.json b/crates/vim/test_data/test_end_of_word.json deleted file mode 100644 index 06f80dc245..0000000000 --- a/crates/vim/test_data/test_end_of_word.json +++ /dev/null @@ -1,32 +0,0 @@ -{"Put":{"state":"Thˇe quick-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"e"} -{"Get":{"state":"The quicˇk-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"e"} -{"Get":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"e"} -{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumpˇs over\nthe","mode":"Normal"}} -{"Key":"e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps oveˇr\nthe","mode":"Normal"}} -{"Key":"e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} -{"Key":"e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} -{"Put":{"state":"Thˇe quick-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quicˇk-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumpˇs over\nthe","mode":"Normal"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps oveˇr\nthe","mode":"Normal"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} diff --git a/crates/vim/test_data/test_enter.json b/crates/vim/test_data/test_enter.json deleted file mode 100644 index c010a1ffd2..0000000000 --- a/crates/vim/test_data/test_enter.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"ˇThe quick brown\nfox jumps"}} -{"Key":"enter"} -{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}} -{"Put":{"state":"The qˇuick brown\nfox jumps"}} -{"Key":"enter"} -{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}} -{"Put":{"state":"The quick broˇwn\nfox jumps"}} -{"Key":"enter"} -{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}} -{"Key":"enter"} -{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}} diff --git a/crates/vim/test_data/test_enter_visual_line_mode.json b/crates/vim/test_data/test_enter_visual_line_mode.json deleted file mode 100644 index bf14ae2495..0000000000 --- a/crates/vim/test_data/test_enter_visual_line_mode.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":"VisualLine"}} -{"Key":"x"} -{"Get":{"state":"fox ˇjumps over\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"a\nˇ\nb"}} -{"Key":"shift-v"} -{"Get":{"state":"a\n«\nˇ»b","mode":"VisualLine"}} -{"Key":"x"} -{"Get":{"state":"a\nˇb","mode":"Normal"}} -{"Put":{"state":"a\nb\nˇ"}} -{"Key":"shift-v"} -{"Get":{"state":"a\nb\nˇ","mode":"VisualLine"}} -{"Key":"x"} -{"Get":{"state":"a\nˇb","mode":"Normal"}} diff --git a/crates/vim/test_data/test_enter_visual_mode.json b/crates/vim/test_data/test_enter_visual_mode.json deleted file mode 100644 index 090e35cc5d..0000000000 --- a/crates/vim/test_data/test_enter_visual_mode.json +++ /dev/null @@ -1,20 +0,0 @@ -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":"Visual"}} -{"Key":"w"} -{"Key":"j"} -{"Get":{"state":"The «quick brown\nfox jumps oˇ»ver\nthe lazy dog","mode":"Visual"}} -{"Key":"escape"} -{"Get":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog","mode":"Normal"}} -{"Key":"v"} -{"Key":"k"} -{"Key":"b"} -{"Get":{"state":"The «ˇquick brown\nfox jumps o»ver\nthe lazy dog","mode":"Visual"}} -{"Put":{"state":"a\nˇ\nb\n"}} -{"Key":"v"} -{"Get":{"state":"a\n«\nˇ»b\n","mode":"Visual"}} -{"Key":"v"} -{"Get":{"state":"a\nˇ\nb\n","mode":"Normal"}} -{"Put":{"state":"a\nb\nˇ"}} -{"Key":"v"} -{"Get":{"state":"a\nb\nˇ","mode":"Visual"}} diff --git a/crates/vim/test_data/test_escape_while_waiting.json b/crates/vim/test_data/test_escape_while_waiting.json deleted file mode 100644 index d81822cf79..0000000000 --- a/crates/vim/test_data/test_escape_while_waiting.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"ˇhi"}} -{"Key":"\""} -{"Key":"+"} -{"Key":"escape"} -{"Key":"x"} -{"Get":{"state":"ˇi","mode":"Normal"}} diff --git a/crates/vim/test_data/test_f_and_t.json b/crates/vim/test_data/test_f_and_t.json deleted file mode 100644 index 2d2a358452..0000000000 --- a/crates/vim/test_data/test_f_and_t.json +++ /dev/null @@ -1,557 +0,0 @@ -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n\nb\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n","mode":"Normal"}} -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaˇab b bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇ bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇ bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaˇabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaˇabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaˇabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇ baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaaˇ bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaaˇ bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaaˇ bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n","mode":"Normal"}} -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n","mode":"Normal"}} -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇ bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇ bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaˇabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaaˇ bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n","mode":"Normal"}} -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n","mode":"Normal"}} -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇ bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaˇabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaˇabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_find_multibyte.json b/crates/vim/test_data/test_find_multibyte.json deleted file mode 100644 index 710a89aede..0000000000 --- a/crates/vim/test_data/test_find_multibyte.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":""}} -{"Key":"c"} -{"Key":"t"} -{"Key":"<"} -{"Key":"o"} -{"Key":"escape"} -{"Get":{"state":"","mode":"Normal"}} diff --git a/crates/vim/test_data/test_folds.json b/crates/vim/test_data/test_folds.json deleted file mode 100644 index c1972480d1..0000000000 --- a/crates/vim/test_data/test_folds.json +++ /dev/null @@ -1,24 +0,0 @@ -{"SetOption":{"value":"foldmethod=manual"}} -{"Put":{"state":"fn boop() {\n ˇbarp()\n bazp()\n}\n"}} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"z"} -{"Key":"f"} -{"Key":"escape"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"ˇfn boop() {\n barp()\n bazp()\n}\n","mode":"Normal"}} -{"Key":"j"} -{"Key":"j"} -{"Get":{"state":"fn boop() {\n barp()\n bazp()\nˇ}\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"k"} -{"Get":{"state":"ˇfn boop() {\n barp()\n bazp()\n}\n","mode":"Normal"}} -{"Key":"down"} -{"Key":"y"} -{"Key":"y"} -{"Get":{"state":"fn boop() {\nˇ barp()\n bazp()\n}\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":" barp()\n bazp()\n"}} -{"Key":"z"} -{"Key":"o"} -{"Get":{"state":"fn boop() {\nˇ barp()\n bazp()\n}\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_folds_panic.json b/crates/vim/test_data/test_folds_panic.json deleted file mode 100644 index a215765b04..0000000000 --- a/crates/vim/test_data/test_folds_panic.json +++ /dev/null @@ -1,23 +0,0 @@ -{"SetOption":{"value":"foldmethod=manual"}} -{"Put":{"state":"fn boop() {\n ˇbarp()\n bazp()\n}\n"}} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"z"} -{"Key":"f"} -{"Key":"escape"} -{"Key":"g"} -{"Key":"g"} -{"Key":"5"} -{"Key":"d"} -{"Key":"j"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"fn boop() {\n ˇbarp()\n bazp()\n}\n"}} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"j"} -{"Key":"z"} -{"Key":"f"} -{"Key":"escape"} -{"Key":"shift-g"} -{"Key":"shift-v"} -{"Get":{"state":"fn boop() {\n barp()\n bazp()\n}\nˇ","mode":"VisualLine"}} diff --git a/crates/vim/test_data/test_forced_motion_delete_to_end_of_line.json b/crates/vim/test_data/test_forced_motion_delete_to_end_of_line.json deleted file mode 100644 index 4df916befb..0000000000 --- a/crates/vim/test_data/test_forced_motion_delete_to_end_of_line.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"the quick brown foˇx\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"$"} -{"Get":{"state":"the quick brown foˇx\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"$"} -{"Get":{"state":"ˇx\njumped over the lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_forced_motion_delete_to_middle_of_line.json b/crates/vim/test_data/test_forced_motion_delete_to_middle_of_line.json deleted file mode 100644 index ca6aa52804..0000000000 --- a/crates/vim/test_data/test_forced_motion_delete_to_middle_of_line.json +++ /dev/null @@ -1,34 +0,0 @@ -{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"g"} -{"Key":"shift-m"} -{"Get":{"state":"ˇbrown fox\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"the quick bˇrown fox\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"g"} -{"Key":"shift-m"} -{"Get":{"state":"the quickˇown fox\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"the quick brown foˇx\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"g"} -{"Key":"shift-m"} -{"Get":{"state":"the quicˇk\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"7"} -{"Key":"5"} -{"Key":"g"} -{"Key":"shift-m"} -{"Get":{"state":"ˇ fox\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"2"} -{"Key":"3"} -{"Key":"g"} -{"Key":"shift-m"} -{"Get":{"state":"ˇuick brown fox\njumped over the lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_forced_motion_delete_to_start_of_line.json b/crates/vim/test_data/test_forced_motion_delete_to_start_of_line.json deleted file mode 100644 index 8aae77c8de..0000000000 --- a/crates/vim/test_data/test_forced_motion_delete_to_start_of_line.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"0"} -{"Get":{"state":"ˇhe quick brown fox\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"the quick bˇrown fox\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"0"} -{"Get":{"state":"ˇown fox\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"the quick brown foˇx\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"0"} -{"Get":{"state":"ˇ\njumped over the lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_forced_motion_yank.json b/crates/vim/test_data/test_forced_motion_yank.json deleted file mode 100644 index 208c22d689..0000000000 --- a/crates/vim/test_data/test_forced_motion_yank.json +++ /dev/null @@ -1,24 +0,0 @@ -{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} -{"Key":"y"} -{"Key":"v"} -{"Key":"j"} -{"Key":"p"} -{"Get":{"state":"the quick brown fox\nˇthe quick brown fox\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"the quick bˇrown fox\njumped over the lazy dog"}} -{"Key":"y"} -{"Key":"v"} -{"Key":"j"} -{"Key":"p"} -{"Get":{"state":"the quick brˇrown fox\njumped overown fox\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"the quick brown foˇx\njumped over the lazy dog"}} -{"Key":"y"} -{"Key":"v"} -{"Key":"j"} -{"Key":"p"} -{"Get":{"state":"the quick brown foxˇx\njumped over the la\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"the quick brown fox\njˇumped over the lazy dog"}} -{"Key":"y"} -{"Key":"v"} -{"Key":"k"} -{"Key":"p"} -{"Get":{"state":"thˇhe quick brown fox\nje quick brown fox\njumped over the lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_gg.json b/crates/vim/test_data/test_gg.json deleted file mode 100644 index 7cc8291b13..0000000000 --- a/crates/vim/test_data/test_gg.json +++ /dev/null @@ -1,21 +0,0 @@ -{"Put":{"state":"The qˇuick\n\nbrown fox jumps\nover the lazy dog"}} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"The qˇuick\n\nbrown fox jumps\nover the lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick\n\nbrown fox jumps\nover ˇthe lazy dog"}} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"The qˇuick\n\nbrown fox jumps\nover the lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick\n\nbrown fox jumps\nover the laˇzy dog"}} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"The quicˇk\n\nbrown fox jumps\nover the lazy dog","mode":"Normal"}} -{"Put":{"state":"\n\nbrown fox jumps\nover the laˇzy dog"}} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"ˇ\n\nbrown fox jumps\nover the lazy dog","mode":"Normal"}} -{"Put":{"state":"ˇ\n\nbrown fox jumps\nover the lazydog"}} -{"Key":"2"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"\nˇ\nbrown fox jumps\nover the lazydog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_gi.json b/crates/vim/test_data/test_gi.json deleted file mode 100644 index a36a919751..0000000000 --- a/crates/vim/test_data/test_gi.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"one two\nthree fˇr"}} -{"Key":"i"} -{"Key":"o"} -{"Key":"escape"} -{"Key":"k"} -{"Key":"g"} -{"Key":"i"} -{"Key":"u"} -{"Key":"escape"} -{"Get":{"state":"one two\nthree foˇur","mode":"Normal"}} diff --git a/crates/vim/test_data/test_gn.json b/crates/vim/test_data/test_gn.json deleted file mode 100644 index b9e0558fca..0000000000 --- a/crates/vim/test_data/test_gn.json +++ /dev/null @@ -1,39 +0,0 @@ -{"Put":{"state":"aaˇ aa aa aa aa"}} -{"Key":"/"} -{"Key":"a"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}} -{"Key":"g"} -{"Key":"n"} -{"Get":{"state":"aa «aaˇ» aa aa aa","mode":"Visual"}} -{"Key":"g"} -{"Key":"n"} -{"Get":{"state":"aa «aa aaˇ» aa aa","mode":"Visual"}} -{"Key":"escape"} -{"Key":"d"} -{"Key":"g"} -{"Key":"n"} -{"Get":{"state":"aa aa ˇ aa aa","mode":"Normal"}} -{"Put":{"state":"aaˇ aa aa aa aa"}} -{"Key":"/"} -{"Key":"a"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}} -{"Key":"3"} -{"Key":"g"} -{"Key":"n"} -{"Get":{"state":"aa aa aa «aaˇ» aa","mode":"Visual"}} -{"Put":{"state":"aaˇ aa aa aa aa"}} -{"Key":"/"} -{"Key":"a"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}} -{"Key":"g"} -{"Key":"shift-n"} -{"Get":{"state":"aa «ˇaa» aa aa aa","mode":"Visual"}} -{"Key":"g"} -{"Key":"shift-n"} -{"Get":{"state":"«ˇaa aa» aa aa aa","mode":"Visual"}} diff --git a/crates/vim/test_data/test_go_to_percentage.json b/crates/vim/test_data/test_go_to_percentage.json deleted file mode 100644 index d7efd170f2..0000000000 --- a/crates/vim/test_data/test_go_to_percentage.json +++ /dev/null @@ -1,26 +0,0 @@ -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"2"} -{"Key":"0"} -{"Key":"%"} -{"Get":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"2"} -{"Key":"5"} -{"Key":"%"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"7"} -{"Key":"5"} -{"Key":"%"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe ˇquick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"5"} -{"Key":"0"} -{"Key":"%"} -{"Get":{"state":"The «quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jˇ»umps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog","mode":"Visual"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"1"} -{"Key":"0"} -{"Key":"0"} -{"Key":"%"} -{"Get":{"state":"The «quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lˇ»azy dog","mode":"Visual"}} diff --git a/crates/vim/test_data/test_gq.json b/crates/vim/test_data/test_gq.json deleted file mode 100644 index 08cdb12315..0000000000 --- a/crates/vim/test_data/test_gq.json +++ /dev/null @@ -1,12 +0,0 @@ -{"SetOption":{"value":"textwidth=5"}} -{"Put":{"state":"ˇth th th th th th\n"}} -{"Key":"g"} -{"Key":"q"} -{"Key":"q"} -{"Get":{"state":"th th\nth th\nˇth th\n","mode":"Normal"}} -{"Put":{"state":"ˇth th th th th th\nth th th th th th\n"}} -{"Key":"v"} -{"Key":"j"} -{"Key":"g"} -{"Key":"q"} -{"Get":{"state":"th th\nth th\nth th\nth th\nth th\nˇth th\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_gv.json b/crates/vim/test_data/test_gv.json deleted file mode 100644 index 7d33535098..0000000000 --- a/crates/vim/test_data/test_gv.json +++ /dev/null @@ -1,24 +0,0 @@ -{"Put":{"state":"The ˇquick brown"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"escape"} -{"Key":"g"} -{"Key":"v"} -{"Get":{"state":"The «quickˇ» brown","mode":"Visual"}} -{"Key":"o"} -{"Key":"escape"} -{"Key":"g"} -{"Key":"v"} -{"Get":{"state":"The «ˇquick» brown","mode":"Visual"}} -{"Key":"escape"} -{"Key":"^"} -{"Key":"ctrl-v"} -{"Key":"l"} -{"Get":{"state":"«Thˇ»e quick brown","mode":"VisualBlock"}} -{"Key":"g"} -{"Key":"v"} -{"Get":{"state":"The «ˇquick» brown","mode":"Visual"}} -{"Key":"g"} -{"Key":"v"} -{"Get":{"state":"«Thˇ»e quick brown","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_h.json b/crates/vim/test_data/test_h.json deleted file mode 100644 index 4cdf210326..0000000000 --- a/crates/vim/test_data/test_h.json +++ /dev/null @@ -1,9 +0,0 @@ -{"Put":{"state":"ˇThe quick\nbrown"}} -{"Key":"h"} -{"Get":{"state":"ˇThe quick\nbrown","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown"}} -{"Key":"h"} -{"Get":{"state":"The ˇquick\nbrown","mode":"Normal"}} -{"Put":{"state":"The quick\nˇbrown"}} -{"Key":"h"} -{"Get":{"state":"The quick\nˇbrown","mode":"Normal"}} diff --git a/crates/vim/test_data/test_h_through_unicode.json b/crates/vim/test_data/test_h_through_unicode.json deleted file mode 100644 index 95b18396d0..0000000000 --- a/crates/vim/test_data/test_h_through_unicode.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"Testˇ├──┐Test"}} -{"Key":"h"} -{"Get":{"state":"Tesˇt├──┐Test","mode":"Normal"}} -{"Put":{"state":"Test├ˇ──┐Test"}} -{"Key":"h"} -{"Get":{"state":"Testˇ├──┐Test","mode":"Normal"}} -{"Put":{"state":"Test├──ˇ┐Test"}} -{"Key":"h"} -{"Get":{"state":"Test├─ˇ─┐Test","mode":"Normal"}} -{"Put":{"state":"Test├──┐ˇTest"}} -{"Key":"h"} -{"Get":{"state":"Test├──ˇ┐Test","mode":"Normal"}} diff --git a/crates/vim/test_data/test_horizontal_scroll.json b/crates/vim/test_data/test_horizontal_scroll.json deleted file mode 100644 index c6cbac8be5..0000000000 --- a/crates/vim/test_data/test_horizontal_scroll.json +++ /dev/null @@ -1,16 +0,0 @@ -{"SetOption":{"value":"scrolloff=3"}} -{"SetOption":{"value":"lines=22"}} -{"SetOption":{"value":"wrap"}} -{"SetOption":{"value":"columns=12"}} -{"SetOption":{"value":"nowrap"}} -{"Put":{"state":"ˇ01234567890123456789"}} -{"Key":"z"} -{"Key":"shift-l"} -{"Get":{"state":"012345ˇ67890123456789","mode":"Normal"}} -{"Key":"z"} -{"Key":"h"} -{"Get":{"state":"012345ˇ67890123456789","mode":"Normal"}} -{"Put":{"state":"ˇ01234567890123456789"}} -{"Key":"z"} -{"Key":"l"} -{"Get":{"state":"0ˇ1234567890123456789","mode":"Normal"}} diff --git a/crates/vim/test_data/test_inclusive_to_exclusive_delete.json b/crates/vim/test_data/test_inclusive_to_exclusive_delete.json deleted file mode 100644 index 3d25b9fc67..0000000000 --- a/crates/vim/test_data/test_inclusive_to_exclusive_delete.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"e"} -{"Get":{"state":"ˇe quick brown fox\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"the quick bˇrown fox\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"e"} -{"Get":{"state":"the quick bˇn fox\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"the quick brown foˇx\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"e"} -{"Get":{"state":"the quick brown foˇd over the lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment.json b/crates/vim/test_data/test_increment.json deleted file mode 100644 index fe893bca97..0000000000 --- a/crates/vim/test_data/test_increment.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"1ˇ2\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"1ˇ3\n","mode":"Normal"}} -{"Key":"ctrl-x"} -{"Get":{"state":"1ˇ2\n","mode":"Normal"}} -{"Key":"9"} -{"Key":"9"} -{"Key":"ctrl-a"} -{"Get":{"state":"11ˇ1\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"1"} -{"Key":"1"} -{"Key":"ctrl-x"} -{"Get":{"state":"ˇ0\n","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"-11ˇ1\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_bin_wrapping_and_padding.json b/crates/vim/test_data/test_increment_bin_wrapping_and_padding.json deleted file mode 100644 index 69c118c0ad..0000000000 --- a/crates/vim/test_data/test_increment_bin_wrapping_and_padding.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"0b111111111111111111111111111111111111111111111111111111111111111111111ˇ1\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1\n","mode":"Normal"}} -{"Key":"ctrl-a"} -{"Get":{"state":"0b000000000000000000000000000000000000000000000000000000000000000000000ˇ0\n","mode":"Normal"}} -{"Key":"ctrl-a"} -{"Get":{"state":"0b000000000000000000000000000000000000000000000000000000000000000000000ˇ1\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"ctrl-x"} -{"Get":{"state":"0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_hex_casing.json b/crates/vim/test_data/test_increment_hex_casing.json deleted file mode 100644 index 951906fa25..0000000000 --- a/crates/vim/test_data/test_increment_hex_casing.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":"0xFˇa\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"0xfˇb\n","mode":"Normal"}} -{"Key":"ctrl-a"} -{"Get":{"state":"0xfˇc\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_hex_wrapping_and_padding.json b/crates/vim/test_data/test_increment_hex_wrapping_and_padding.json deleted file mode 100644 index 562b368812..0000000000 --- a/crates/vim/test_data/test_increment_hex_wrapping_and_padding.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"0xfffffffffffffffffffˇf\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"0x0000fffffffffffffffˇf\n","mode":"Normal"}} -{"Key":"ctrl-a"} -{"Get":{"state":"0x0000000000000000000ˇ0\n","mode":"Normal"}} -{"Key":"ctrl-a"} -{"Get":{"state":"0x0000000000000000000ˇ1\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"ctrl-x"} -{"Get":{"state":"0x0000fffffffffffffffˇf\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_inline.json b/crates/vim/test_data/test_increment_inline.json deleted file mode 100644 index 1e3d8fbd90..0000000000 --- a/crates/vim/test_data/test_increment_inline.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"inline0x3ˇ9u32\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"inline0x3ˇau32\n","mode":"Normal"}} -{"Key":"ctrl-a"} -{"Get":{"state":"inline0x3ˇbu32\n","mode":"Normal"}} -{"Key":"l"} -{"Key":"l"} -{"Key":"l"} -{"Key":"ctrl-a"} -{"Get":{"state":"inline0x3bu3ˇ3\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_radix.json b/crates/vim/test_data/test_increment_radix.json deleted file mode 100644 index 0f41c01599..0000000000 --- a/crates/vim/test_data/test_increment_radix.json +++ /dev/null @@ -1,18 +0,0 @@ -{"Put":{"state":"ˇ total: 0xff"}} -{"Key":"ctrl-a"} -{"Get":{"state":" total: 0x10ˇ0","mode":"Normal"}} -{"Put":{"state":"ˇ total: 0xff"}} -{"Key":"ctrl-x"} -{"Get":{"state":" total: 0xfˇe","mode":"Normal"}} -{"Put":{"state":"ˇ total: 0xFF"}} -{"Key":"ctrl-x"} -{"Get":{"state":" total: 0xFˇE","mode":"Normal"}} -{"Put":{"state":"(ˇ0b10f)"}} -{"Key":"ctrl-a"} -{"Get":{"state":"(0b1ˇ1f)","mode":"Normal"}} -{"Put":{"state":"ˇ-1"}} -{"Key":"ctrl-a"} -{"Get":{"state":"ˇ0","mode":"Normal"}} -{"Put":{"state":"banˇana"}} -{"Key":"ctrl-a"} -{"Get":{"state":"banˇana","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_sign_change.json b/crates/vim/test_data/test_increment_sign_change.json deleted file mode 100644 index 8f2ee7f2f3..0000000000 --- a/crates/vim/test_data/test_increment_sign_change.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"ˇ0\n"}} -{"Key":"ctrl-x"} -{"Get":{"state":"-ˇ1\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"ctrl-a"} -{"Get":{"state":"ˇ1\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_sign_change_with_leading_zeros.json b/crates/vim/test_data/test_increment_sign_change_with_leading_zeros.json deleted file mode 100644 index c1257012b5..0000000000 --- a/crates/vim/test_data/test_increment_sign_change_with_leading_zeros.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"00ˇ1\n"}} -{"Key":"ctrl-x"} -{"Get":{"state":"00ˇ0\n","mode":"Normal"}} -{"Key":"ctrl-x"} -{"Get":{"state":"-00ˇ1\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"ctrl-a"} -{"Get":{"state":"00ˇ1\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_steps.json b/crates/vim/test_data/test_increment_steps.json deleted file mode 100644 index 5e16efed53..0000000000 --- a/crates/vim/test_data/test_increment_steps.json +++ /dev/null @@ -1,25 +0,0 @@ -{"Put":{"state":"ˇ1\n1\n1 2\n1\n1"}} -{"Key":"j"} -{"Key":"v"} -{"Key":"shift-g"} -{"Key":"g"} -{"Key":"ctrl-a"} -{"Get":{"state":"1\nˇ2\n3 2\n4\n5","mode":"Normal"}} -{"Key":"shift-g"} -{"Key":"ctrl-v"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"«1ˇ»\n«2ˇ»\n«3ˇ» 2\n«4ˇ»\n«5ˇ»","mode":"VisualBlock"}} -{"Key":"g"} -{"Key":"ctrl-x"} -{"Get":{"state":"ˇ0\n0\n0 2\n0\n0","mode":"Normal"}} -{"Key":"v"} -{"Key":"shift-g"} -{"Key":"g"} -{"Key":"ctrl-a"} -{"Key":"v"} -{"Key":"shift-g"} -{"Key":"5"} -{"Key":"g"} -{"Key":"ctrl-a"} -{"Get":{"state":"ˇ6\n12\n18 2\n24\n30","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_visual_partial_number.json b/crates/vim/test_data/test_increment_visual_partial_number.json deleted file mode 100644 index ebb4eece78..0000000000 --- a/crates/vim/test_data/test_increment_visual_partial_number.json +++ /dev/null @@ -1,20 +0,0 @@ -{"Put":{"state":"ˇ123"}} -{"Key":"v"} -{"Key":"l"} -{"Key":"ctrl-a"} -{"Get":{"state":"ˇ133","mode":"Normal"}} -{"Key":"l"} -{"Key":"v"} -{"Key":"l"} -{"Key":"ctrl-a"} -{"Get":{"state":"1ˇ34","mode":"Normal"}} -{"Key":"shift-v"} -{"Key":"y"} -{"Key":"p"} -{"Key":"p"} -{"Key":"ctrl-v"} -{"Key":"k"} -{"Key":"k"} -{"Key":"l"} -{"Key":"ctrl-a"} -{"Get":{"state":"ˇ144\n144\n144","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_with_changing_leading_zeros.json b/crates/vim/test_data/test_increment_with_changing_leading_zeros.json deleted file mode 100644 index dce392e25f..0000000000 --- a/crates/vim/test_data/test_increment_with_changing_leading_zeros.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"099ˇ9\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"100ˇ0\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"ctrl-x"} -{"Get":{"state":"99ˇ8\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_with_dot.json b/crates/vim/test_data/test_increment_with_dot.json deleted file mode 100644 index b5c5b0914e..0000000000 --- a/crates/vim/test_data/test_increment_with_dot.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":"1ˇ.2\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"1.ˇ3\n","mode":"Normal"}} -{"Key":"ctrl-x"} -{"Get":{"state":"1.ˇ2\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_with_leading_zeros.json b/crates/vim/test_data/test_increment_with_leading_zeros.json deleted file mode 100644 index bab262463f..0000000000 --- a/crates/vim/test_data/test_increment_with_leading_zeros.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"000ˇ9\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"001ˇ0\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"ctrl-x"} -{"Get":{"state":"000ˇ8\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_with_leading_zeros_and_zero.json b/crates/vim/test_data/test_increment_with_leading_zeros_and_zero.json deleted file mode 100644 index 94a1a1a715..0000000000 --- a/crates/vim/test_data/test_increment_with_leading_zeros_and_zero.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":"01ˇ1\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"01ˇ2\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"2"} -{"Key":"ctrl-x"} -{"Get":{"state":"00ˇ0\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_with_two_dots.json b/crates/vim/test_data/test_increment_with_two_dots.json deleted file mode 100644 index 38b38f1005..0000000000 --- a/crates/vim/test_data/test_increment_with_two_dots.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":"111.ˇ.2\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"111..ˇ3\n","mode":"Normal"}} -{"Key":"ctrl-x"} -{"Get":{"state":"111..ˇ2\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_wrapping.json b/crates/vim/test_data/test_increment_wrapping.json deleted file mode 100644 index 9f189991a6..0000000000 --- a/crates/vim/test_data/test_increment_wrapping.json +++ /dev/null @@ -1,13 +0,0 @@ -{"Put":{"state":"1844674407370955161ˇ9\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"1844674407370955161ˇ5\n","mode":"Normal"}} -{"Key":"ctrl-a"} -{"Get":{"state":"-1844674407370955161ˇ5\n","mode":"Normal"}} -{"Key":"ctrl-a"} -{"Get":{"state":"-1844674407370955161ˇ4\n","mode":"Normal"}} -{"Key":"3"} -{"Key":"ctrl-x"} -{"Get":{"state":"1844674407370955161ˇ4\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"ctrl-a"} -{"Get":{"state":"-1844674407370955161ˇ5\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_zero_leading_zeros.json b/crates/vim/test_data/test_increment_zero_leading_zeros.json deleted file mode 100644 index 5361fcbc7f..0000000000 --- a/crates/vim/test_data/test_increment_zero_leading_zeros.json +++ /dev/null @@ -1,4 +0,0 @@ -{"Put":{"state":"001ˇ0\n"}} -{"Key":"10"} -{"Key":"ctrl-x"} -{"Get":{"state":"000ˇ9\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_indent_gv.json b/crates/vim/test_data/test_indent_gv.json deleted file mode 100644 index 2c24406aee..0000000000 --- a/crates/vim/test_data/test_indent_gv.json +++ /dev/null @@ -1,8 +0,0 @@ -{"SetOption":{"value":"shiftwidth=4"}} -{"Put":{"state":"ˇhello\nworld\n"}} -{"Key":"v"} -{"Key":"j"} -{"Key":">"} -{"Key":"g"} -{"Key":"v"} -{"Get":{"state":"« hello\n ˇ» world\n","mode":"Visual"}} diff --git a/crates/vim/test_data/test_insert_ctrl_r.json b/crates/vim/test_data/test_insert_ctrl_r.json deleted file mode 100644 index 6167bc3f33..0000000000 --- a/crates/vim/test_data/test_insert_ctrl_r.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"heˇllo\n"}} -{"Key":"y"} -{"Key":"y"} -{"Key":"i"} -{"Key":"ctrl-r"} -{"Key":"\""} -{"Get":{"state":"hehello\nˇllo\n","mode":"Insert"}} -{"Key":"ctrl-r"} -{"Key":"x"} -{"Key":"ctrl-r"} -{"Key":"escape"} -{"Get":{"state":"hehello\nˇllo\n","mode":"Insert"}} diff --git a/crates/vim/test_data/test_insert_ctrl_y.json b/crates/vim/test_data/test_insert_ctrl_y.json deleted file mode 100644 index 09b707a198..0000000000 --- a/crates/vim/test_data/test_insert_ctrl_y.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":"hello\nˇ\nworld"}} -{"Key":"i"} -{"Key":"ctrl-y"} -{"Key":"ctrl-e"} -{"Get":{"state":"hello\nhoˇ\nworld","mode":"Insert"}} diff --git a/crates/vim/test_data/test_insert_empty_line.json b/crates/vim/test_data/test_insert_empty_line.json deleted file mode 100644 index 534db2c457..0000000000 --- a/crates/vim/test_data/test_insert_empty_line.json +++ /dev/null @@ -1,78 +0,0 @@ -{"Put":{"state":"ˇ"}} -{"Key":"["} -{"Key":"space"} -{"Get":{"state":"\nˇ","mode":"Normal"}} -{"Put":{"state":"The ˇquick"}} -{"Key":"["} -{"Key":"space"} -{"Get":{"state":"\nThe ˇquick","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"3"} -{"Key":"["} -{"Key":"space"} -{"Get":{"state":"\n\n\nThe qˇuick\nbrown fox\njumps over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"3"} -{"Key":"["} -{"Key":"space"} -{"Get":{"state":"The quick\n\n\n\nbrown ˇfox\njumps over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"3"} -{"Key":"["} -{"Key":"space"} -{"Get":{"state":"The quick\nbrown fox\n\n\n\njumps ˇover","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"["} -{"Key":"space"} -{"Get":{"state":"\nThe qˇuick\nbrown fox\njumps over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"["} -{"Key":"space"} -{"Get":{"state":"The quick\n\nbrown ˇfox\njumps over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"["} -{"Key":"space"} -{"Get":{"state":"The quick\nbrown fox\n\njumps ˇover","mode":"Normal"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"["} -{"Key":"space"} -{"Get":{"state":"The quick\n\nˇ\nbrown fox","mode":"Normal"}} -{"Put":{"state":"ˇ"}} -{"Key":"]"} -{"Key":"space"} -{"Get":{"state":"ˇ\n","mode":"Normal"}} -{"Put":{"state":"The ˇquick"}} -{"Key":"]"} -{"Key":"space"} -{"Get":{"state":"The ˇquick\n","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"3"} -{"Key":"]"} -{"Key":"space"} -{"Get":{"state":"The qˇuick\n\n\n\nbrown fox\njumps over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"3"} -{"Key":"]"} -{"Key":"space"} -{"Get":{"state":"The quick\nbrown ˇfox\n\n\n\njumps over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"3"} -{"Key":"]"} -{"Key":"space"} -{"Get":{"state":"The quick\nbrown fox\njumps ˇover\n\n\n","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"]"} -{"Key":"space"} -{"Get":{"state":"The qˇuick\n\nbrown fox\njumps over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"]"} -{"Key":"space"} -{"Get":{"state":"The quick\nbrown ˇfox\n\njumps over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"]"} -{"Key":"space"} -{"Get":{"state":"The quick\nbrown fox\njumps ˇover\n","mode":"Normal"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"]"} -{"Key":"space"} -{"Get":{"state":"The quick\nˇ\n\nbrown fox","mode":"Normal"}} diff --git a/crates/vim/test_data/test_insert_end_of_line.json b/crates/vim/test_data/test_insert_end_of_line.json deleted file mode 100644 index a37ad24d39..0000000000 --- a/crates/vim/test_data/test_insert_end_of_line.json +++ /dev/null @@ -1,9 +0,0 @@ -{"Put":{"state":"ˇ\nThe quick\nbrown fox "}} -{"Key":"shift-a"} -{"Get":{"state":"ˇ\nThe quick\nbrown fox ","mode":"Insert"}} -{"Put":{"state":"\nThe qˇuick\nbrown fox "}} -{"Key":"shift-a"} -{"Get":{"state":"\nThe quickˇ\nbrown fox ","mode":"Insert"}} -{"Put":{"state":"\nThe quick\nbrown ˇfox "}} -{"Key":"shift-a"} -{"Get":{"state":"\nThe quick\nbrown fox ˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_insert_first_non_whitespace.json b/crates/vim/test_data/test_insert_first_non_whitespace.json deleted file mode 100644 index 4d13cdb81c..0000000000 --- a/crates/vim/test_data/test_insert_first_non_whitespace.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"The qˇuick"}} -{"Key":"shift-i"} -{"Get":{"state":"ˇThe quick","mode":"Insert"}} -{"Put":{"state":" The qˇuick"}} -{"Key":"shift-i"} -{"Get":{"state":" ˇThe quick","mode":"Insert"}} -{"Put":{"state":"ˇ"}} -{"Key":"shift-i"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The qˇuick\nbrown fox"}} -{"Key":"shift-i"} -{"Get":{"state":"ˇThe quick\nbrown fox","mode":"Insert"}} -{"Put":{"state":"ˇ\nThe quick"}} -{"Key":"shift-i"} -{"Get":{"state":"ˇ\nThe quick","mode":"Insert"}} diff --git a/crates/vim/test_data/test_insert_line_above.json b/crates/vim/test_data/test_insert_line_above.json deleted file mode 100644 index ce5d0d7ac1..0000000000 --- a/crates/vim/test_data/test_insert_line_above.json +++ /dev/null @@ -1,18 +0,0 @@ -{"Put":{"state":"ˇ"}} -{"Key":"shift-o"} -{"Get":{"state":"ˇ\n","mode":"Insert"}} -{"Put":{"state":"The ˇquick"}} -{"Key":"shift-o"} -{"Get":{"state":"ˇ\nThe quick","mode":"Insert"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"shift-o"} -{"Get":{"state":"ˇ\nThe quick\nbrown fox\njumps over","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"shift-o"} -{"Get":{"state":"The quick\nˇ\nbrown fox\njumps over","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"shift-o"} -{"Get":{"state":"The quick\nbrown fox\nˇ\njumps over","mode":"Insert"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"shift-o"} -{"Get":{"state":"The quick\nˇ\n\nbrown fox","mode":"Insert"}} diff --git a/crates/vim/test_data/test_insert_with_counts.json b/crates/vim/test_data/test_insert_with_counts.json deleted file mode 100644 index 470888cf6e..0000000000 --- a/crates/vim/test_data/test_insert_with_counts.json +++ /dev/null @@ -1,36 +0,0 @@ -{"Put":{"state":"ˇhello\n"}} -{"Key":"5"} -{"Key":"i"} -{"Key":"-"} -{"Key":"escape"} -{"Get":{"state":"----ˇ-hello\n","mode":"Normal"}} -{"Put":{"state":"ˇhello\n"}} -{"Key":"5"} -{"Key":"a"} -{"Key":"-"} -{"Key":"escape"} -{"Get":{"state":"h----ˇ-ello\n","mode":"Normal"}} -{"Key":"4"} -{"Key":"shift-i"} -{"Key":"-"} -{"Key":"escape"} -{"Get":{"state":"---ˇ-h-----ello\n","mode":"Normal"}} -{"Key":"3"} -{"Key":"shift-a"} -{"Key":"-"} -{"Key":"escape"} -{"Get":{"state":"----h-----ello--ˇ-\n","mode":"Normal"}} -{"Put":{"state":"ˇhello\n"}} -{"Key":"3"} -{"Key":"o"} -{"Key":"o"} -{"Key":"i"} -{"Key":"escape"} -{"Get":{"state":"hello\noi\noi\noˇi\n","mode":"Normal"}} -{"Put":{"state":"ˇhello\n"}} -{"Key":"3"} -{"Key":"shift-o"} -{"Key":"o"} -{"Key":"i"} -{"Key":"escape"} -{"Get":{"state":"oi\noi\noˇi\nhello\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_insert_with_repeat.json b/crates/vim/test_data/test_insert_with_repeat.json deleted file mode 100644 index ac6637633c..0000000000 --- a/crates/vim/test_data/test_insert_with_repeat.json +++ /dev/null @@ -1,23 +0,0 @@ -{"Put":{"state":"ˇhello\n"}} -{"Key":"3"} -{"Key":"i"} -{"Key":"-"} -{"Key":"escape"} -{"Get":{"state":"--ˇ-hello\n","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"----ˇ--hello\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"."} -{"Get":{"state":"-----ˇ---hello\n","mode":"Normal"}} -{"Put":{"state":"ˇhello\n"}} -{"Key":"2"} -{"Key":"o"} -{"Key":"k"} -{"Key":"k"} -{"Key":"escape"} -{"Get":{"state":"hello\nkk\nkˇk\n","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"hello\nkk\nkk\nkk\nkˇk\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"."} -{"Get":{"state":"hello\nkk\nkk\nkk\nkk\nkˇk\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_j.json b/crates/vim/test_data/test_j.json deleted file mode 100644 index 703f69d22c..0000000000 --- a/crates/vim/test_data/test_j.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"aaˇaa\n😃😃"}} -{"Key":"j"} -{"Get":{"state":"aaaa\n😃ˇ😃","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps"}} -{"Key":"j"} -{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}} -{"Put":{"state":"The qˇuick brown\nfox jumps"}} -{"Key":"j"} -{"Get":{"state":"The quick brown\nfox jˇumps","mode":"Normal"}} -{"Put":{"state":"The quick broˇwn\nfox jumps"}} -{"Key":"j"} -{"Get":{"state":"The quick brown\nfox jumpˇs","mode":"Normal"}} -{"Put":{"state":"The quick brown\nˇfox jumps"}} -{"Key":"j"} -{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}} diff --git a/crates/vim/test_data/test_jk.json b/crates/vim/test_data/test_jk.json deleted file mode 100644 index bc1a6a4ba5..0000000000 --- a/crates/vim/test_data/test_jk.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Exec":{"command":"imap jk "}} -{"Put":{"state":"ˇhello"}} -{"Key":"i"} -{"Key":"j"} -{"Key":"o"} -{"Key":"j"} -{"Key":"k"} -{"Get":{"state":"jˇohello","mode":"Normal"}} diff --git a/crates/vim/test_data/test_jk_max_count.json b/crates/vim/test_data/test_jk_max_count.json deleted file mode 100644 index 83eab46a18..0000000000 --- a/crates/vim/test_data/test_jk_max_count.json +++ /dev/null @@ -1,47 +0,0 @@ -{"Put":{"state":"1\nˇ2\n3"}} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"j"} -{"Get":{"state":"1\n2\nˇ3","mode":"Normal"}} -{"Key":""} -{"Key":"1"} -{"Key":"8"} -{"Key":"4"} -{"Key":"4"} -{"Key":"6"} -{"Key":"7"} -{"Key":"4"} -{"Key":"4"} -{"Key":"0"} -{"Key":"7"} -{"Key":"3"} -{"Key":"7"} -{"Key":"0"} -{"Key":"9"} -{"Key":"5"} -{"Key":"5"} -{"Key":"1"} -{"Key":"6"} -{"Key":"1"} -{"Key":"5"} -{"Key":""} -{"Key":"k"} -{"Get":{"state":"ˇ1\n2\n3","mode":"Normal"}} diff --git a/crates/vim/test_data/test_join_lines.json b/crates/vim/test_data/test_join_lines.json deleted file mode 100644 index 55aa8b1dcb..0000000000 --- a/crates/vim/test_data/test_join_lines.json +++ /dev/null @@ -1,29 +0,0 @@ -{"Put":{"state":"ˇone\ntwo\nthree\nfour\nfive\nsix\n"}} -{"Key":"shift-j"} -{"Get":{"state":"oneˇ two\nthree\nfour\nfive\nsix\n","mode":"Normal"}} -{"Key":"3"} -{"Key":"shift-j"} -{"Get":{"state":"one two threeˇ four\nfive\nsix\n","mode":"Normal"}} -{"Put":{"state":"ˇone\ntwo\nthree\nfour\nfive\nsix\n"}} -{"Key":"j"} -{"Key":"v"} -{"Key":"3"} -{"Key":"j"} -{"Key":"shift-j"} -{"Get":{"state":"one\ntwo three fourˇ five\nsix\n","mode":"Normal"}} -{"Put":{"state":"ˇone\ntwo\nthree\nfour\nfive\nsix\n"}} -{"Key":"g"} -{"Key":"shift-j"} -{"Get":{"state":"oneˇtwo\nthree\nfour\nfive\nsix\n","mode":"Normal"}} -{"Key":"3"} -{"Key":"g"} -{"Key":"shift-j"} -{"Get":{"state":"onetwothreeˇfour\nfive\nsix\n","mode":"Normal"}} -{"Put":{"state":"ˇone\ntwo\nthree\nfour\nfive\nsix\n"}} -{"Key":"j"} -{"Key":"v"} -{"Key":"3"} -{"Key":"j"} -{"Key":"g"} -{"Key":"shift-j"} -{"Get":{"state":"one\ntwothreefourˇfive\nsix\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_jump_list.json b/crates/vim/test_data/test_jump_list.json deleted file mode 100644 index 833d1adadb..0000000000 --- a/crates/vim/test_data/test_jump_list.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"ˇfn a() { }\n\n\n\n\n\nfn b() { }\n\n\n\n\n\nfn b() { }"}} -{"Key":"3"} -{"Key":"}"} -{"Get":{"state":"fn a() { }\n\n\n\n\n\nfn b() { }\n\n\n\n\n\nfn b() { ˇ}","mode":"Normal"}} -{"Key":"ctrl-o"} -{"Get":{"state":"ˇfn a() { }\n\n\n\n\n\nfn b() { }\n\n\n\n\n\nfn b() { }","mode":"Normal"}} -{"Key":"ctrl-i"} -{"Get":{"state":"fn a() { }\n\n\n\n\n\nfn b() { }\n\n\n\n\n\nfn b() { ˇ}","mode":"Normal"}} -{"Key":"1"} -{"Key":"1"} -{"Key":"k"} -{"Get":{"state":"fn a() { }\nˇ\n\n\n\n\nfn b() { }\n\n\n\n\n\nfn b() { }","mode":"Normal"}} -{"Key":"ctrl-o"} -{"Get":{"state":"ˇfn a() { }\n\n\n\n\n\nfn b() { }\n\n\n\n\n\nfn b() { }","mode":"Normal"}} diff --git a/crates/vim/test_data/test_jump_to_end.json b/crates/vim/test_data/test_jump_to_end.json deleted file mode 100644 index fe9a948d43..0000000000 --- a/crates/vim/test_data/test_jump_to_end.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"The ˇquick\n\nbrown fox jumps\nover the lazy dog"}} -{"Key":"shift-g"} -{"Get":{"state":"The quick\n\nbrown fox jumps\noverˇ the lazy dog","mode":"Normal"}} -{"Key":"shift-g"} -{"Get":{"state":"The quick\n\nbrown fox jumps\noverˇ the lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick\n\nbrown fox jumps\nover the lazy doˇg"}} -{"Key":"shift-g"} -{"Get":{"state":"The quick\n\nbrown fox jumps\nover the lazy doˇg","mode":"Normal"}} -{"Put":{"state":"The quiˇck\n\nbrown"}} -{"Key":"shift-g"} -{"Get":{"state":"The quick\n\nbrowˇn","mode":"Normal"}} -{"Put":{"state":"The quiˇck\n\n"}} -{"Key":"shift-g"} -{"Get":{"state":"The quick\n\nˇ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_jump_to_first_non_whitespace.json b/crates/vim/test_data/test_jump_to_first_non_whitespace.json deleted file mode 100644 index c992662195..0000000000 --- a/crates/vim/test_data/test_jump_to_first_non_whitespace.json +++ /dev/null @@ -1,18 +0,0 @@ -{"Put":{"state":"The qˇuick"}} -{"Key":"^"} -{"Get":{"state":"ˇThe quick","mode":"Normal"}} -{"Put":{"state":" The qˇuick"}} -{"Key":"^"} -{"Get":{"state":" ˇThe quick","mode":"Normal"}} -{"Put":{"state":"ˇ"}} -{"Key":"^"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox"}} -{"Key":"^"} -{"Get":{"state":"ˇThe quick\nbrown fox","mode":"Normal"}} -{"Put":{"state":"ˇ\nThe quick"}} -{"Key":"^"} -{"Get":{"state":"ˇ\nThe quick","mode":"Normal"}} -{"Put":{"state":" ˇ \nThe quick"}} -{"Key":"^"} -{"Get":{"state":" ˇ \nThe quick","mode":"Normal"}} diff --git a/crates/vim/test_data/test_jump_to_line_boundaries.json b/crates/vim/test_data/test_jump_to_line_boundaries.json deleted file mode 100644 index 6d4a76ffd6..0000000000 --- a/crates/vim/test_data/test_jump_to_line_boundaries.json +++ /dev/null @@ -1,28 +0,0 @@ -{"Put":{"state":"ˇThe quick\nbrown"}} -{"Key":"$"} -{"Get":{"state":"The quicˇk\nbrown","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown"}} -{"Key":"$"} -{"Get":{"state":"The quicˇk\nbrown","mode":"Normal"}} -{"Key":"$"} -{"Get":{"state":"The quicˇk\nbrown","mode":"Normal"}} -{"Put":{"state":"The quick\nˇbrown"}} -{"Key":"$"} -{"Get":{"state":"The quick\nbrowˇn","mode":"Normal"}} -{"Key":"$"} -{"Get":{"state":"The quick\nbrowˇn","mode":"Normal"}} -{"Put":{"state":"ˇThe quick\nbrown"}} -{"Key":"0"} -{"Get":{"state":"ˇThe quick\nbrown","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown"}} -{"Key":"0"} -{"Get":{"state":"ˇThe quick\nbrown","mode":"Normal"}} -{"Put":{"state":"The quicˇk\nbrown"}} -{"Key":"0"} -{"Get":{"state":"ˇThe quick\nbrown","mode":"Normal"}} -{"Put":{"state":"The quick\nˇbrown"}} -{"Key":"0"} -{"Get":{"state":"The quick\nˇbrown","mode":"Normal"}} -{"Put":{"state":"The quick\nbrowˇn"}} -{"Key":"0"} -{"Get":{"state":"The quick\nˇbrown","mode":"Normal"}} diff --git a/crates/vim/test_data/test_k.json b/crates/vim/test_data/test_k.json deleted file mode 100644 index f064fa9fd9..0000000000 --- a/crates/vim/test_data/test_k.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"ˇThe quick\nbrown fox jumps"}} -{"Key":"k"} -{"Get":{"state":"ˇThe quick\nbrown fox jumps","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox jumps"}} -{"Key":"k"} -{"Get":{"state":"The qˇuick\nbrown fox jumps","mode":"Normal"}} -{"Put":{"state":"The quick\nˇbrown fox jumps"}} -{"Key":"k"} -{"Get":{"state":"ˇThe quick\nbrown fox jumps","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fˇox jumps"}} -{"Key":"k"} -{"Get":{"state":"The quiˇck\nbrown fox jumps","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox jumˇps"}} -{"Key":"k"} -{"Get":{"state":"The quicˇk\nbrown fox jumps","mode":"Normal"}} diff --git a/crates/vim/test_data/test_l.json b/crates/vim/test_data/test_l.json deleted file mode 100644 index b0db891bce..0000000000 --- a/crates/vim/test_data/test_l.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"ˇThe quick\nbrown"}} -{"Key":"l"} -{"Get":{"state":"Tˇhe quick\nbrown","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown"}} -{"Key":"l"} -{"Get":{"state":"The quˇick\nbrown","mode":"Normal"}} -{"Put":{"state":"The quicˇk\nbrown"}} -{"Key":"l"} -{"Get":{"state":"The quicˇk\nbrown","mode":"Normal"}} -{"Put":{"state":"The quick\nˇbrown"}} -{"Key":"l"} -{"Get":{"state":"The quick\nbˇrown","mode":"Normal"}} -{"Put":{"state":"The quick\nbrowˇn"}} -{"Key":"l"} -{"Get":{"state":"The quick\nbrowˇn","mode":"Normal"}} diff --git a/crates/vim/test_data/test_lowercase_marks.json b/crates/vim/test_data/test_lowercase_marks.json deleted file mode 100644 index ff02c58728..0000000000 --- a/crates/vim/test_data/test_lowercase_marks.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"line one\nline ˇtwo\nline three"}} -{"Key":"m"} -{"Key":"a"} -{"Key":"l"} -{"Key":"'"} -{"Key":"a"} -{"Get":{"state":"line one\nˇline two\nline three","mode":"Normal"}} -{"Key":"`"} -{"Key":"a"} -{"Get":{"state":"line one\nline ˇtwo\nline three","mode":"Normal"}} -{"Key":"^"} -{"Key":"d"} -{"Key":"`"} -{"Key":"a"} -{"Get":{"state":"line one\nˇtwo\nline three","mode":"Normal"}} diff --git a/crates/vim/test_data/test_lt_gt_marks.json b/crates/vim/test_data/test_lt_gt_marks.json deleted file mode 100644 index 142ceb9b95..0000000000 --- a/crates/vim/test_data/test_lt_gt_marks.json +++ /dev/null @@ -1,29 +0,0 @@ -{"Put":{"state":"Line one\nLine two\nLine ˇthree\nLine four\nLine five\n"}} -{"Key":"v"} -{"Key":"j"} -{"Key":"escape"} -{"Key":"k"} -{"Key":"k"} -{"Key":"'"} -{"Key":"<"} -{"Get":{"state":"Line one\nLine two\nˇLine three\nLine four\nLine five\n","mode":"Normal"}} -{"Key":"`"} -{"Key":"<"} -{"Get":{"state":"Line one\nLine two\nLine ˇthree\nLine four\nLine five\n","mode":"Normal"}} -{"Key":"'"} -{"Key":">"} -{"Get":{"state":"Line one\nLine two\nLine three\nˇLine four\nLine five\n","mode":"Normal"}} -{"Key":"`"} -{"Key":">"} -{"Get":{"state":"Line one\nLine two\nLine three\nLine ˇfour\nLine five\n","mode":"Normal"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"o"} -{"Key":"escape"} -{"Key":"`"} -{"Key":">"} -{"Get":{"state":"Line one\nLine two\nLine three\nLine fouˇr\nLine five\n","mode":"Normal"}} -{"Key":"`"} -{"Key":"<"} -{"Get":{"state":"Line one\nLine two\nLine three\nLine ˇfour\nLine five\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_marks.json b/crates/vim/test_data/test_marks.json deleted file mode 100644 index ff02c58728..0000000000 --- a/crates/vim/test_data/test_marks.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"line one\nline ˇtwo\nline three"}} -{"Key":"m"} -{"Key":"a"} -{"Key":"l"} -{"Key":"'"} -{"Key":"a"} -{"Get":{"state":"line one\nˇline two\nline three","mode":"Normal"}} -{"Key":"`"} -{"Key":"a"} -{"Get":{"state":"line one\nline ˇtwo\nline three","mode":"Normal"}} -{"Key":"^"} -{"Key":"d"} -{"Key":"`"} -{"Key":"a"} -{"Get":{"state":"line one\nˇtwo\nline three","mode":"Normal"}} diff --git a/crates/vim/test_data/test_matching.json b/crates/vim/test_data/test_matching.json deleted file mode 100644 index 5c8d7529b9..0000000000 --- a/crates/vim/test_data/test_matching.json +++ /dev/null @@ -1,17 +0,0 @@ -{"Put":{"state":"func ˇ(a string) {\n do(something(with.and_arrays[0, 2]))\n}"}} -{"Key":"%"} -{"Get":{"state":"func (a stringˇ) {\n do(something(with.and_arrays[0, 2]))\n}","mode":"Normal"}} -{"Put":{"state":"func (a string) ˇ{\ndo(something(with.and_arrays[0, 2]))\n}"}} -{"Key":"%"} -{"Get":{"state":"func (a string) {\ndo(something(with.and_arrays[0, 2]))\nˇ}","mode":"Normal"}} -{"Put":{"state":"ˇ{()}"}} -{"Key":"%"} -{"Get":{"state":"{()ˇ}","mode":"Normal"}} -{"Key":"%"} -{"Get":{"state":"ˇ{()}","mode":"Normal"}} -{"Put":{"state":"{\n ˇ{()}\n}"}} -{"Key":"%"} -{"Get":{"state":"{\n {()ˇ}\n}","mode":"Normal"}} -{"Put":{"state":"func ˇboop() {\n}"}} -{"Key":"%"} -{"Get":{"state":"func boop(ˇ) {\n}","mode":"Normal"}} diff --git a/crates/vim/test_data/test_matching_braces_in_tag.json b/crates/vim/test_data/test_matching_braces_in_tag.json deleted file mode 100644 index 44201548a7..0000000000 --- a/crates/vim/test_data/test_matching_braces_in_tag.json +++ /dev/null @@ -1,3 +0,0 @@ -{"Put":{"state":"function f() {\n return (\n
\n

test

\n
\n );\n}"}} -{"Key":"%"} -{"Get":{"state":"function f() {\n return (\n
\n

test

\n
\n );\n}","mode":"Normal"}} diff --git a/crates/vim/test_data/test_matching_nested_brackets.json b/crates/vim/test_data/test_matching_nested_brackets.json deleted file mode 100644 index d90b38416e..0000000000 --- a/crates/vim/test_data/test_matching_nested_brackets.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":""}} -{"Key":"%"} -{"Get":{"state":"","mode":"Normal"}} -{"Key":"%"} -{"Get":{"state":"","mode":"Normal"}} diff --git a/crates/vim/test_data/test_matching_tags.json b/crates/vim/test_data/test_matching_tags.json deleted file mode 100644 index b401033a94..0000000000 --- a/crates/vim/test_data/test_matching_tags.json +++ /dev/null @@ -1,20 +0,0 @@ -{"Exec":{"command":"set filetype=html"}} -{"Put":{"state":""}} -{"Key":"%"} -{"Get":{"state":"<ˇ/body>","mode":"Normal"}} -{"Key":"%"} -{"Get":{"state":"<ˇbody>","mode":"Normal"}} -{"Put":{"state":""}} -{"Key":"%"} -{"Get":{"state":"","mode":"Normal"}} -{"Put":{"state":"
\n
\n"}} -{"Key":"%"} -{"Get":{"state":"
\n<ˇ/div>\n","mode":"Normal"}} -{"Put":{"state":"\n \n"}} -{"Key":"%"} -{"Get":{"state":"\n ˇ\n","mode":"Normal"}} -{"Put":{"state":"\n \n \n"}} -{"Key":"%"} -{"Get":{"state":"\n \n <ˇ/body>\n","mode":"Normal"}} -{"Key":"%"} -{"Get":{"state":"\n <ˇbody>\n \n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_minibrackets_trailing_space.json b/crates/vim/test_data/test_minibrackets_trailing_space.json deleted file mode 100644 index ed3f47df6c..0000000000 --- a/crates/vim/test_data/test_minibrackets_trailing_space.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"(trailingˇ whitespace )"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"b"} -{"Get":{"state":"(«trailing whitespace ˇ»)","mode":"Visual"}} -{"Key":"escape"} -{"Key":"y"} -{"Key":"i"} -{"Key":"b"} -{"Get":{"state":"(ˇtrailing whitespace )","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"trailing whitespace "}} diff --git a/crates/vim/test_data/test_named_registers.json b/crates/vim/test_data/test_named_registers.json deleted file mode 100644 index 789f17f93f..0000000000 --- a/crates/vim/test_data/test_named_registers.json +++ /dev/null @@ -1,28 +0,0 @@ -{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}} -{"Key":"\""} -{"Key":"a"} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown\nfox ˇover\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"a","value":"jumps "}} -{"Key":"\""} -{"Key":"shift-a"} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown\nfoxˇ \nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"a","value":"jumps over"}} -{"Get":{"state":"The quick brown\nfoxˇ \nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"jumps over"}} -{"Key":"\""} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog","mode":"Normal"}} -{"Key":"\""} -{"Key":"a"} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown\nfox jumpˇs\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"a","value":" over"}} diff --git a/crates/vim/test_data/test_neovim.json b/crates/vim/test_data/test_neovim.json deleted file mode 100644 index 3fb5d6b3f4..0000000000 --- a/crates/vim/test_data/test_neovim.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Key":"i"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Key":"shift-t"} -{"Key":"e"} -{"Key":"s"} -{"Key":"t"} -{"Key":"space"} -{"Key":"t"} -{"Key":"e"} -{"Key":"s"} -{"Key":"t"} -{"Key":"escape"} -{"Key":"0"} -{"Key":"d"} -{"Key":"w"} -{"Get":{"state":"ˇtest","mode":"Normal"}} diff --git a/crates/vim/test_data/test_next_line_start.json b/crates/vim/test_data/test_next_line_start.json deleted file mode 100644 index 90ed4a4f03..0000000000 --- a/crates/vim/test_data/test_next_line_start.json +++ /dev/null @@ -1,3 +0,0 @@ -{"Put":{"state":"ˇone\n two\nthree"}} -{"Key":"enter"} -{"Get":{"state":"one\n ˇtwo\nthree","mode":"Normal"}} diff --git a/crates/vim/test_data/test_next_word_end_newline_last_char.json b/crates/vim/test_data/test_next_word_end_newline_last_char.json deleted file mode 100644 index 9dac2979f5..0000000000 --- a/crates/vim/test_data/test_next_word_end_newline_last_char.json +++ /dev/null @@ -1,3 +0,0 @@ -{"Put":{"state":"something(ˇfoo)"}} -{"Key":"}"} -{"Get":{"state":"something(fooˇ)","mode":"Normal"}} diff --git a/crates/vim/test_data/test_normal_command.json b/crates/vim/test_data/test_normal_command.json deleted file mode 100644 index efd1d532c4..0000000000 --- a/crates/vim/test_data/test_normal_command.json +++ /dev/null @@ -1,64 +0,0 @@ -{"Put":{"state":"The quick\nbrown« fox\njumpsˇ» over\nthe lazy dog\n"}} -{"Key":":"} -{"Key":"n"} -{"Key":"o"} -{"Key":"r"} -{"Key":"m"} -{"Key":"space"} -{"Key":"w"} -{"Key":"C"} -{"Key":"w"} -{"Key":"o"} -{"Key":"r"} -{"Key":"d"} -{"Key":"enter"} -{"Get":{"state":"The quick\nbrown word\njumps worˇd\nthe lazy dog\n","mode":"Normal"}} -{"Key":":"} -{"Key":"n"} -{"Key":"o"} -{"Key":"r"} -{"Key":"m"} -{"Key":"space"} -{"Key":"_"} -{"Key":"w"} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Key":"t"} -{"Key":"e"} -{"Key":"s"} -{"Key":"t"} -{"Key":"enter"} -{"Get":{"state":"The quick\nbrown word\njumps tesˇt\nthe lazy dog\n","mode":"Normal"}} -{"Key":"_"} -{"Key":"l"} -{"Key":"v"} -{"Key":"l"} -{"Key":":"} -{"Key":"n"} -{"Key":"o"} -{"Key":"r"} -{"Key":"m"} -{"Key":"space"} -{"Key":"s"} -{"Key":"l"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"The quick\nbrown word\nlˇaumps test\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"ˇThe quick\nbrown fox\njumps over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Key":"M"} -{"Key":"y"} -{"Key":"escape"} -{"Get":{"state":"Mˇy quick\nbrown fox\njumps over\nthe lazy dog\n","mode":"Normal"}} -{"Key":":"} -{"Key":"n"} -{"Key":"o"} -{"Key":"r"} -{"Key":"m"} -{"Key":"space"} -{"Key":"u"} -{"Key":"enter"} -{"Get":{"state":"ˇThe quick\nbrown fox\njumps over\nthe lazy dog\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_numbered_registers.json b/crates/vim/test_data/test_numbered_registers.json deleted file mode 100644 index 191a58e01f..0000000000 --- a/crates/vim/test_data/test_numbered_registers.json +++ /dev/null @@ -1,45 +0,0 @@ -{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}} -{"Key":"y"} -{"Key":"y"} -{"Key":"\""} -{"Key":"0"} -{"Key":"p"} -{"Get":{"state":"The quick brown\nfox jumps over\nˇfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"0","value":"fox jumps over\n"}} -{"Get":{"state":"The quick brown\nfox jumps over\nˇfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"fox jumps over\n"}} -{"Get":{"state":"The quick brown\nfox jumps over\nˇfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"k"} -{"Key":"k"} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"ˇfox jumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"0","value":"fox jumps over\n"}} -{"Get":{"state":"ˇfox jumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"1","value":"The quick brown\n"}} -{"Get":{"state":"ˇfox jumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"The quick brown\n"}} -{"Key":"d"} -{"Key":"d"} -{"Key":"shift-g"} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"ˇfox jumps over","mode":"Normal"}} -{"ReadRegister":{"name":"0","value":"fox jumps over\n"}} -{"Get":{"state":"ˇfox jumps over","mode":"Normal"}} -{"ReadRegister":{"name":"3","value":"The quick brown\n"}} -{"Get":{"state":"ˇfox jumps over","mode":"Normal"}} -{"ReadRegister":{"name":"2","value":"fox jumps over\n"}} -{"Get":{"state":"ˇfox jumps over","mode":"Normal"}} -{"ReadRegister":{"name":"1","value":"the lazy dog\n"}} -{"Get":{"state":"ˇfox jumps over","mode":"Normal"}} -{"Key":"d"} -{"Key":"d"} -{"Key":"\""} -{"Key":"3"} -{"Key":"p"} -{"Key":"p"} -{"Key":"\""} -{"Key":"1"} -{"Key":"p"} -{"Put":{"state":"The quick brown\nfox jumps over\nˇthe lazy dog"}} diff --git a/crates/vim/test_data/test_o.json b/crates/vim/test_data/test_o.json deleted file mode 100644 index 015890ac36..0000000000 --- a/crates/vim/test_data/test_o.json +++ /dev/null @@ -1,18 +0,0 @@ -{"Put":{"state":"ˇ"}} -{"Key":"o"} -{"Get":{"state":"\nˇ","mode":"Insert"}} -{"Put":{"state":"The ˇquick"}} -{"Key":"o"} -{"Get":{"state":"The quick\nˇ","mode":"Insert"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"o"} -{"Get":{"state":"The quick\nˇ\nbrown fox\njumps over","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"o"} -{"Get":{"state":"The quick\nbrown fox\nˇ\njumps over","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"o"} -{"Get":{"state":"The quick\nbrown fox\njumps over\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"o"} -{"Get":{"state":"The quick\n\nˇ\nbrown fox","mode":"Insert"}} diff --git a/crates/vim/test_data/test_o_comment.json b/crates/vim/test_data/test_o_comment.json deleted file mode 100644 index b0b84da0e6..0000000000 --- a/crates/vim/test_data/test_o_comment.json +++ /dev/null @@ -1,8 +0,0 @@ -{"SetOption":{"value":"filetype=rust"}} -{"Put":{"state":"// helloˇ\n"}} -{"Key":"o"} -{"Get":{"state":"// hello\n// ˇ\n","mode":"Insert"}} -{"Key":"x"} -{"Key":"escape"} -{"Key":"shift-o"} -{"Get":{"state":"// hello\n// ˇ\n// x\n","mode":"Insert"}} diff --git a/crates/vim/test_data/test_offsets.json b/crates/vim/test_data/test_offsets.json deleted file mode 100644 index 065a234817..0000000000 --- a/crates/vim/test_data/test_offsets.json +++ /dev/null @@ -1,21 +0,0 @@ -{"Put":{"state":"ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n"}} -{"Key":":"} -{"Key":"+"} -{"Key":"enter"} -{"Get":{"state":"1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n","mode":"Normal"}} -{"Key":":"} -{"Key":"1"} -{"Key":"0"} -{"Key":"-"} -{"Key":"enter"} -{"Get":{"state":"1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n","mode":"Normal"}} -{"Key":":"} -{"Key":"."} -{"Key":"-"} -{"Key":"2"} -{"Key":"enter"} -{"Get":{"state":"1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n","mode":"Normal"}} -{"Key":":"} -{"Key":"%"} -{"Key":"enter"} -{"Get":{"state":"1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_p_g_v_y.json b/crates/vim/test_data/test_p_g_v_y.json deleted file mode 100644 index a275c333a3..0000000000 --- a/crates/vim/test_data/test_p_g_v_y.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"The\nquicˇk\nbrown\nfox"}} -{"Key":"y"} -{"Key":"y"} -{"Key":"j"} -{"Key":"shift-v"} -{"Key":"p"} -{"Key":"g"} -{"Key":"v"} -{"Key":"y"} -{"Get":{"state":"The\nquick\nˇquick\nfox","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"quick\n"}} diff --git a/crates/vim/test_data/test_paragraph_multi_delete.json b/crates/vim/test_data/test_paragraph_multi_delete.json deleted file mode 100644 index f706827a24..0000000000 --- a/crates/vim/test_data/test_paragraph_multi_delete.json +++ /dev/null @@ -1,18 +0,0 @@ -{"Put":{"state":"Emacs is\nˇa great\n\noperating system\n\nall it lacks\nis a\n\ndecent text editor\n"}} -{"Key":"2"} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇall it lacks\nis a\n\ndecent text editor\n","mode":"Normal"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇdecent text editor\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"all it lacks\nis a\n\n"}} -{"Key":"2"} -{"Key":"u"} -{"Key":"4"} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_paragraph_object_with_landing_positions_not_at_beginning_of_line.json b/crates/vim/test_data/test_paragraph_object_with_landing_positions_not_at_beginning_of_line.json deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/vim/test_data/test_paragraphs_dont_wrap.json b/crates/vim/test_data/test_paragraphs_dont_wrap.json deleted file mode 100644 index 9e729651be..0000000000 --- a/crates/vim/test_data/test_paragraphs_dont_wrap.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"one\nˇ\ntwo"}} -{"Key":"}"} -{"Key":"}"} -{"Get":{"state":"one\n\ntwˇo","mode":"Normal"}} -{"Key":"{"} -{"Key":"{"} -{"Key":"{"} -{"Get":{"state":"ˇone\n\ntwo","mode":"Normal"}} diff --git a/crates/vim/test_data/test_paste.json b/crates/vim/test_data/test_paste.json deleted file mode 100644 index 21b7e9346a..0000000000 --- a/crates/vim/test_data/test_paste.json +++ /dev/null @@ -1,34 +0,0 @@ -{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"y"} -{"Get":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"jumps o"}} -{"Put":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog"}} -{"Key":"p"} -{"Get":{"state":"The quick brown\nfox jumps overjumps ˇo\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog"}} -{"Key":"shift-p"} -{"Get":{"state":"The quick brown\nfox jumps ovejumps ˇor\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"fox jumps over\n"}} -{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}} -{"Key":"p"} -{"Get":{"state":"The quick brown\nthe lazy dog\nˇfox jumps over","mode":"Normal"}} -{"Key":"k"} -{"Key":"shift-p"} -{"Get":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog\nfox jumps over","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}} -{"Key":"v"} -{"Key":"j"} -{"Key":"y"} -{"Get":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"over\nthe lazy do"}} -{"Key":"p"} -{"Get":{"state":"The quick brown\nfox jumps oˇover\nthe lazy dover\nthe lazy dog","mode":"Normal"}} -{"Key":"u"} -{"Key":"shift-p"} -{"Get":{"state":"The quick brown\nfox jumps ˇover\nthe lazy doover\nthe lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_paste_count.json b/crates/vim/test_data/test_paste_count.json deleted file mode 100644 index 7dd4dc4d91..0000000000 --- a/crates/vim/test_data/test_paste_count.json +++ /dev/null @@ -1,13 +0,0 @@ -{"Put":{"state":"onˇe\ntwo\nthree\n"}} -{"Key":"y"} -{"Key":"y"} -{"Key":"3"} -{"Key":"p"} -{"Get":{"state":"one\nˇone\none\none\ntwo\nthree\n","mode":"Normal"}} -{"Put":{"state":"one\nˇtwo\nthree\n"}} -{"Key":"y"} -{"Key":"$"} -{"Key":"$"} -{"Key":"3"} -{"Key":"p"} -{"Get":{"state":"one\ntwotwotwotwˇo\nthree\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_paste_visual.json b/crates/vim/test_data/test_paste_visual.json deleted file mode 100644 index fb10f94782..0000000000 --- a/crates/vim/test_data/test_paste_visual.json +++ /dev/null @@ -1,51 +0,0 @@ -{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"y"} -{"Get":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"w"} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"p"} -{"Get":{"state":"The quick brown\nfox jumps jumpˇs\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"over"}} -{"Key":"up"} -{"Key":"shift-v"} -{"Key":"shift-p"} -{"Get":{"state":"ˇover\nfox jumps jumps\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"over"}} -{"Key":"ctrl-v"} -{"Key":"down"} -{"Key":"down"} -{"Key":"p"} -{"Get":{"state":"oveˇrver\noverox jumps jumps\noverhe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"d"} -{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"p"} -{"Get":{"state":"The quick brown\nthe \nˇfox jumps over\n dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"lazy"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"d"} -{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"fox jumps over\n"}} -{"Key":"k"} -{"Key":"shift-v"} -{"Key":"p"} -{"Get":{"state":"ˇfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"The quick brown\n"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"y"} -{"Key":"y"} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"$"} -{"Key":"p"} -{"Get":{"state":"ˇThe quick brown\nthe lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_paste_visual_block.json b/crates/vim/test_data/test_paste_visual_block.json deleted file mode 100644 index 5560b7529d..0000000000 --- a/crates/vim/test_data/test_paste_visual_block.json +++ /dev/null @@ -1,33 +0,0 @@ -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"ctrl-v"} -{"Key":"2"} -{"Key":"j"} -{"Key":"y"} -{"Get":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"q\nj\nl"}} -{"Key":"p"} -{"Get":{"state":"The qˇquick brown\nfox jjumps over\nthe llazy dog","mode":"Normal"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"shift-p"} -{"Get":{"state":"The ˇq brown\nfox jjjumps over\nthe lllazy dog","mode":"Normal"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"shift-p"} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Key":"y"} -{"Get":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"q\nj"}} -{"Key":"l"} -{"Key":"ctrl-v"} -{"Key":"2"} -{"Key":"j"} -{"Key":"shift-p"} -{"Get":{"state":"The qˇqick brown\nfox jjmps over\nthe lzy dog","mode":"Normal"}} -{"Key":"shift-v"} -{"Key":"p"} -{"Get":{"state":"ˇq\nj\nfox jjmps over\nthe lzy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_percent.json b/crates/vim/test_data/test_percent.json deleted file mode 100644 index 2382cc9e49..0000000000 --- a/crates/vim/test_data/test_percent.json +++ /dev/null @@ -1,58 +0,0 @@ -{"Put":{"state":"ˇconsole.log(var);"}} -{"Key":"%"} -{"Get":{"state":"console.log(varˇ);","mode":"Normal"}} -{"Put":{"state":"console.logˇ(var);"}} -{"Key":"%"} -{"Get":{"state":"console.log(varˇ);","mode":"Normal"}} -{"Put":{"state":"console.log(ˇvar);"}} -{"Key":"%"} -{"Get":{"state":"console.logˇ(var);","mode":"Normal"}} -{"Put":{"state":"console.log(vaˇr);"}} -{"Key":"%"} -{"Get":{"state":"console.logˇ(var);","mode":"Normal"}} -{"Put":{"state":"console.log(varˇ);"}} -{"Key":"%"} -{"Get":{"state":"console.logˇ(var);","mode":"Normal"}} -{"Put":{"state":"console.log(var)ˇ;"}} -{"Key":"%"} -{"Get":{"state":"console.log(var)ˇ;","mode":"Normal"}} -{"Put":{"state":"ˇconsole.log('var', [1, 2, 3]);"}} -{"Key":"%"} -{"Get":{"state":"console.log('var', [1, 2, 3]ˇ);","mode":"Normal"}} -{"Put":{"state":"console.logˇ('var', [1, 2, 3]);"}} -{"Key":"%"} -{"Get":{"state":"console.log('var', [1, 2, 3]ˇ);","mode":"Normal"}} -{"Put":{"state":"console.log(ˇ'var', [1, 2, 3]);"}} -{"Key":"%"} -{"Get":{"state":"console.log('var', [1, 2, 3ˇ]);","mode":"Normal"}} -{"Put":{"state":"console.log('var', ˇ[1, 2, 3]);"}} -{"Key":"%"} -{"Get":{"state":"console.log('var', [1, 2, 3ˇ]);","mode":"Normal"}} -{"Put":{"state":"console.log('var', [ˇ1, 2, 3]);"}} -{"Key":"%"} -{"Get":{"state":"console.log('var', ˇ[1, 2, 3]);","mode":"Normal"}} -{"Put":{"state":"console.log('var', [1, ˇ2, 3]);"}} -{"Key":"%"} -{"Get":{"state":"console.log('var', ˇ[1, 2, 3]);","mode":"Normal"}} -{"Put":{"state":"console.log('var', [1, 2, 3ˇ]);"}} -{"Key":"%"} -{"Get":{"state":"console.log('var', ˇ[1, 2, 3]);","mode":"Normal"}} -{"Put":{"state":"console.log('var', [1, 2, 3]ˇ);"}} -{"Key":"%"} -{"Get":{"state":"console.logˇ('var', [1, 2, 3]);","mode":"Normal"}} -{"Put":{"state":"console.log('var', [1, 2, 3])ˇ;"}} -{"Key":"%"} -{"Get":{"state":"console.log('var', [1, 2, 3])ˇ;","mode":"Normal"}} -{"Put":{"state":"let result = curried_funˇ()();"}} -{"Key":"%"} -{"Get":{"state":"let result = curried_fun(ˇ)();","mode":"Normal"}} -{"Key":"%"} -{"Get":{"state":"let result = curried_funˇ()();","mode":"Normal"}} -{"Put":{"state":"let result = curried_fun()ˇ();"}} -{"Key":"%"} -{"Get":{"state":"let result = curried_fun()(ˇ);","mode":"Normal"}} -{"Key":"%"} -{"Get":{"state":"let result = curried_fun()ˇ();","mode":"Normal"}} -{"Put":{"state":"let result = curried_fun()()ˇ;"}} -{"Key":"%"} -{"Get":{"state":"let result = curried_fun()()ˇ;","mode":"Normal"}} diff --git a/crates/vim/test_data/test_period_mark.json b/crates/vim/test_data/test_period_mark.json deleted file mode 100644 index 6d3acea83c..0000000000 --- a/crates/vim/test_data/test_period_mark.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"Line one\nLine two\nLiˇne three\nLine four\nLine five\n"}} -{"Key":"c"} -{"Key":"e"} -{"Key":"k"} -{"Key":"e"} -{"Key":"escape"} -{"Key":"j"} -{"Key":"j"} -{"Key":"'"} -{"Key":"."} -{"Get":{"state":"Line one\nLine two\nˇLike three\nLine four\nLine five\n","mode":"Normal"}} -{"Key":"`"} -{"Key":"."} -{"Get":{"state":"Line one\nLine two\nLiˇke three\nLine four\nLine five\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_plus_minus.json b/crates/vim/test_data/test_plus_minus.json deleted file mode 100644 index 277d92ef6a..0000000000 --- a/crates/vim/test_data/test_plus_minus.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":"one\n two\nthrˇee\n"}} -{"Key":"-"} -{"Get":{"state":"one\n ˇtwo\nthree\n","mode":"Normal"}} -{"Key":"-"} -{"Get":{"state":"ˇone\n two\nthree\n","mode":"Normal"}} -{"Key":"+"} -{"Get":{"state":"one\n ˇtwo\nthree\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_previous_word_end.json b/crates/vim/test_data/test_previous_word_end.json deleted file mode 100644 index f1e3540240..0000000000 --- a/crates/vim/test_data/test_previous_word_end.json +++ /dev/null @@ -1,33 +0,0 @@ -{"Put":{"state":"456 5ˇ67 678\n"}} -{"Key":"g"} -{"Key":"e"} -{"Get":{"state":"45ˇ6 567 678\n","mode":"Normal"}} -{"Put":{"state":"123 234 345\n456 5ˇ67 678\n"}} -{"Key":"4"} -{"Key":"g"} -{"Key":"e"} -{"Get":{"state":"12ˇ3 234 345\n456 567 678\n","mode":"Normal"}} -{"Put":{"state":"123 234 345\n4;5.6 5ˇ67 678\n789 890 901\n"}} -{"Key":"g"} -{"Key":"e"} -{"Get":{"state":"123 234 345\n4;5.ˇ6 567 678\n789 890 901\n","mode":"Normal"}} -{"Put":{"state":"123 234 345\n4;5.6 5ˇ67 678\n789 890 901\n"}} -{"Key":"5"} -{"Key":"g"} -{"Key":"e"} -{"Get":{"state":"123 234 345\nˇ4;5.6 567 678\n789 890 901\n","mode":"Normal"}} -{"Put":{"state":"123 234 345\n\n78ˇ9 890 901\n"}} -{"Key":"g"} -{"Key":"e"} -{"Get":{"state":"123 234 345\nˇ\n789 890 901\n","mode":"Normal"}} -{"Key":"g"} -{"Key":"e"} -{"Get":{"state":"123 234 34ˇ5\n\n789 890 901\n","mode":"Normal"}} -{"Put":{"state":"123 234 345\n4;5.ˇ6 567 678\n789 890 901\n"}} -{"Key":"g"} -{"Key":"shift-e"} -{"Get":{"state":"123 234 34ˇ5\n4;5.6 567 678\n789 890 901\n","mode":"Normal"}} -{"Put":{"state":"bar ˇó\n"}} -{"Key":"g"} -{"Key":"e"} -{"Get":{"state":"baˇr ó\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_quote_mark.json b/crates/vim/test_data/test_quote_mark.json deleted file mode 100644 index 6eb85580e8..0000000000 --- a/crates/vim/test_data/test_quote_mark.json +++ /dev/null @@ -1,23 +0,0 @@ -{"Put":{"state":"ˇHello, world!"}} -{"Key":"w"} -{"Key":"m"} -{"Key":"o"} -{"Get":{"state":"Helloˇ, world!","mode":"Normal"}} -{"Key":"$"} -{"Key":"`"} -{"Key":"o"} -{"Get":{"state":"Helloˇ, world!","mode":"Normal"}} -{"Key":"`"} -{"Key":"`"} -{"Get":{"state":"Hello, worldˇ!","mode":"Normal"}} -{"Key":"`"} -{"Key":"`"} -{"Get":{"state":"Helloˇ, world!","mode":"Normal"}} -{"Key":"$"} -{"Key":"m"} -{"Key":"'"} -{"Get":{"state":"Hello, worldˇ!","mode":"Normal"}} -{"Key":"^"} -{"Key":"`"} -{"Key":"`"} -{"Get":{"state":"Hello, worldˇ!","mode":"Normal"}} diff --git a/crates/vim/test_data/test_r.json b/crates/vim/test_data/test_r.json deleted file mode 100644 index 6cb0aec3f5..0000000000 --- a/crates/vim/test_data/test_r.json +++ /dev/null @@ -1,40 +0,0 @@ -{"Put":{"state":"ˇhello\n"}} -{"Key":"r"} -{"Key":"-"} -{"Get":{"state":"ˇ-ello\n","mode":"Normal"}} -{"Put":{"state":"ˇhello\n"}} -{"Key":"3"} -{"Key":"r"} -{"Key":"-"} -{"Get":{"state":"--ˇ-lo\n","mode":"Normal"}} -{"Put":{"state":"ˇhello\n"}} -{"Key":"r"} -{"Key":"-"} -{"Key":"2"} -{"Key":"l"} -{"Key":"."} -{"Get":{"state":"-eˇ-lo\n","mode":"Normal"}} -{"Put":{"state":"ˇhello world\n"}} -{"Key":"2"} -{"Key":"r"} -{"Key":"-"} -{"Key":"f"} -{"Key":"w"} -{"Key":"."} -{"Get":{"state":"--llo -ˇ-rld\n","mode":"Normal"}} -{"Put":{"state":"ˇhello world\n"}} -{"Key":"2"} -{"Key":"0"} -{"Key":"r"} -{"Key":"-"} -{"Key":""} -{"Get":{"state":"ˇhello world\n","mode":"Normal"}} -{"Put":{"state":" helloˇ world\n"}} -{"Key":"r"} -{"Key":"enter"} -{"Get":{"state":" hello\n ˇ world\n","mode":"Normal"}} -{"Put":{"state":" helloˇ world\n"}} -{"Key":"2"} -{"Key":"r"} -{"Key":"enter"} -{"Get":{"state":" hello\n ˇ orld\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_record_replay.json b/crates/vim/test_data/test_record_replay.json deleted file mode 100644 index 8346d9ad8b..0000000000 --- a/crates/vim/test_data/test_record_replay.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"ˇhello world"}} -{"Key":"q"} -{"Key":"w"} -{"Key":"c"} -{"Key":"w"} -{"Key":"j"} -{"Key":"escape"} -{"Key":"q"} -{"Get":{"state":"ˇj world","mode":"Normal"}} -{"Key":"2"} -{"Key":"l"} -{"Key":"@"} -{"Key":"w"} -{"Get":{"state":"j ˇj","mode":"Normal"}} diff --git a/crates/vim/test_data/test_record_replay_count.json b/crates/vim/test_data/test_record_replay_count.json deleted file mode 100644 index 78023ef350..0000000000 --- a/crates/vim/test_data/test_record_replay_count.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"ˇhello world!!"}} -{"Key":"q"} -{"Key":"a"} -{"Key":"v"} -{"Key":"3"} -{"Key":"l"} -{"Key":"s"} -{"Key":"0"} -{"Key":"escape"} -{"Key":"l"} -{"Key":"q"} -{"Get":{"state":"0ˇo world!!","mode":"Normal"}} -{"Key":"2"} -{"Key":"@"} -{"Key":"a"} -{"Get":{"state":"000ˇ!","mode":"Normal"}} diff --git a/crates/vim/test_data/test_record_replay_dot.json b/crates/vim/test_data/test_record_replay_dot.json deleted file mode 100644 index 9cc565f160..0000000000 --- a/crates/vim/test_data/test_record_replay_dot.json +++ /dev/null @@ -1,17 +0,0 @@ -{"Put":{"state":"ˇhello world"}} -{"Key":"q"} -{"Key":"a"} -{"Key":"r"} -{"Key":"a"} -{"Key":"l"} -{"Key":"r"} -{"Key":"b"} -{"Key":"l"} -{"Key":"q"} -{"Get":{"state":"abˇllo world","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"abˇblo world","mode":"Normal"}} -{"Key":"shift-q"} -{"Get":{"state":"ababˇo world","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"ababˇb world","mode":"Normal"}} diff --git a/crates/vim/test_data/test_record_replay_interleaved.json b/crates/vim/test_data/test_record_replay_interleaved.json deleted file mode 100644 index aefb5eac2a..0000000000 --- a/crates/vim/test_data/test_record_replay_interleaved.json +++ /dev/null @@ -1,35 +0,0 @@ -{"Put":{"state":"ˇhello world"}} -{"Key":"q"} -{"Key":"z"} -{"Key":"r"} -{"Key":"a"} -{"Key":"l"} -{"Key":"q"} -{"Get":{"state":"aˇello world","mode":"Normal"}} -{"Key":"q"} -{"Key":"b"} -{"Key":"@"} -{"Key":"z"} -{"Key":"@"} -{"Key":"z"} -{"Key":"q"} -{"Get":{"state":"aaaˇlo world","mode":"Normal"}} -{"Key":"@"} -{"Key":"@"} -{"Get":{"state":"aaaaˇo world","mode":"Normal"}} -{"Key":"@"} -{"Key":"b"} -{"Get":{"state":"aaaaaaˇworld","mode":"Normal"}} -{"Key":"@"} -{"Key":"@"} -{"Get":{"state":"aaaaaaaˇorld","mode":"Normal"}} -{"Key":"q"} -{"Key":"z"} -{"Key":"r"} -{"Key":"b"} -{"Key":"l"} -{"Key":"q"} -{"Get":{"state":"aaaaaaabˇrld","mode":"Normal"}} -{"Key":"@"} -{"Key":"b"} -{"Get":{"state":"aaaaaaabbbˇd","mode":"Normal"}} diff --git a/crates/vim/test_data/test_record_replay_of_dot.json b/crates/vim/test_data/test_record_replay_of_dot.json deleted file mode 100644 index f4cce4bb3d..0000000000 --- a/crates/vim/test_data/test_record_replay_of_dot.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"ˇhello world"}} -{"Key":"r"} -{"Key":"o"} -{"Key":"q"} -{"Key":"w"} -{"Key":"."} -{"Key":"q"} -{"Get":{"state":"ˇoello world","mode":"Normal"}} -{"Key":"d"} -{"Key":"l"} -{"Get":{"state":"ˇello world","mode":"Normal"}} -{"Key":"@"} -{"Key":"w"} -{"Get":{"state":"ˇllo world","mode":"Normal"}} diff --git a/crates/vim/test_data/test_record_replay_recursion.json b/crates/vim/test_data/test_record_replay_recursion.json deleted file mode 100644 index d02817db57..0000000000 --- a/crates/vim/test_data/test_record_replay_recursion.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"ˇhello world"}} -{"Key":">"} -{"Key":"."} -{"Key":"."} -{"Key":"."} -{"Get":{"state":"ˇhello world","mode":"Normal"}} diff --git a/crates/vim/test_data/test_remap_adjacent_dog_cat.json b/crates/vim/test_data/test_remap_adjacent_dog_cat.json deleted file mode 100644 index 91af9ccac6..0000000000 --- a/crates/vim/test_data/test_remap_adjacent_dog_cat.json +++ /dev/null @@ -1,24 +0,0 @@ -{"Exec":{"command":"imap dog 🐶"}} -{"Exec":{"command":"imap cat 🐱"}} -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"d"} -{"Key":"o"} -{"Key":"g"} -{"Get":{"state":"🐶ˇ","mode":"Insert"}} -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"d"} -{"Key":"o"} -{"Key":"d"} -{"Key":"o"} -{"Key":"g"} -{"Get":{"state":"do🐶ˇ","mode":"Insert"}} -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"d"} -{"Key":"o"} -{"Key":"c"} -{"Key":"a"} -{"Key":"t"} -{"Get":{"state":"do🐱ˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_remap_nested_pineapple.json b/crates/vim/test_data/test_remap_nested_pineapple.json deleted file mode 100644 index b4a4acdd2b..0000000000 --- a/crates/vim/test_data/test_remap_nested_pineapple.json +++ /dev/null @@ -1,28 +0,0 @@ -{"Exec":{"command":"imap pin 📌"}} -{"Exec":{"command":"imap pine 🌲"}} -{"Exec":{"command":"imap pineapple 🍍"}} -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"p"} -{"Key":"i"} -{"Key":"n"} -{"Get":{"state":"📌ˇ","mode":"Insert"}} -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"p"} -{"Key":"i"} -{"Key":"n"} -{"Key":"e"} -{"Get":{"state":"🌲ˇ","mode":"Insert"}} -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"p"} -{"Key":"i"} -{"Key":"n"} -{"Key":"e"} -{"Key":"a"} -{"Key":"p"} -{"Key":"p"} -{"Key":"l"} -{"Key":"e"} -{"Get":{"state":"🍍ˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_remap_recursion.json b/crates/vim/test_data/test_remap_recursion.json deleted file mode 100644 index 27e3c64b16..0000000000 --- a/crates/vim/test_data/test_remap_recursion.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Exec":{"command":"noremap x \"_x"}} -{"Exec":{"command":"map y 2x"}} -{"Put":{"state":"ˇhello"}} -{"Key":"d"} -{"Key":"l"} -{"Get":{"state":"ˇello","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"h"}} -{"Key":"y"} -{"Get":{"state":"ˇlo","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"h"}} -{"Get":{"state":"ˇlo","mode":"Normal"}} diff --git a/crates/vim/test_data/test_repeat_clear_count.json b/crates/vim/test_data/test_repeat_clear_count.json deleted file mode 100644 index 352c6ca4a8..0000000000 --- a/crates/vim/test_data/test_repeat_clear_count.json +++ /dev/null @@ -1,21 +0,0 @@ -{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"ˇfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"2"} -{"Key":"d"} -{"Key":"."} -{"Get":{"state":"ˇfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"ˇthe lazy dog","mode":"Normal"}} -{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog\nthe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"2"} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"ˇthe lazy dog\nthe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"5"} -{"Key":"d"} -{"Key":"."} -{"Get":{"state":"ˇthe lazy dog\nthe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"ˇfox jumps over\nthe lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_repeat_clear_repeat.json b/crates/vim/test_data/test_repeat_clear_repeat.json deleted file mode 100644 index 39d96e2a37..0000000000 --- a/crates/vim/test_data/test_repeat_clear_repeat.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"ˇfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"d"} -{"Key":"."} -{"Key":"."} -{"Get":{"state":"ˇthe lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_repeat_grouping_41735.json b/crates/vim/test_data/test_repeat_grouping_41735.json deleted file mode 100644 index 6523be6e4b..0000000000 --- a/crates/vim/test_data/test_repeat_grouping_41735.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"a"} -{"Key":"escape"} -{"Key":"."} -{"Key":"."} -{"Key":"."} -{"Get":{"state":"ˇaaaa","mode":"Normal"}} -{"Key":"u"} -{"Get":{"state":"ˇaaa","mode":"Normal"}} diff --git a/crates/vim/test_data/test_repeat_motion_counts.json b/crates/vim/test_data/test_repeat_motion_counts.json deleted file mode 100644 index c39b8b09c0..0000000000 --- a/crates/vim/test_data/test_repeat_motion_counts.json +++ /dev/null @@ -1,13 +0,0 @@ -{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"3"} -{"Key":"d"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"ˇ brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"j"} -{"Key":"."} -{"Get":{"state":" brown\nˇ over\nthe lazy dog","mode":"Normal"}} -{"Key":"j"} -{"Key":"2"} -{"Key":"."} -{"Get":{"state":" brown\n over\nˇe lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_repeat_over_blur.json b/crates/vim/test_data/test_repeat_over_blur.json deleted file mode 100644 index 3929711e49..0000000000 --- a/crates/vim/test_data/test_repeat_over_blur.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"ˇhello hello hello\n"}} -{"Key":"c"} -{"Key":"f"} -{"Key":"o"} -{"Key":"x"} -{"Key":"escape"} -{"Get":{"state":"ˇx hello hello\n","mode":"Normal"}} -{"Key":":"} -{"Key":"escape"} -{"Key":"."} -{"Get":{"state":"ˇx hello\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_repeat_visual.json b/crates/vim/test_data/test_repeat_visual.json deleted file mode 100644 index cb83addcfb..0000000000 --- a/crates/vim/test_data/test_repeat_visual.json +++ /dev/null @@ -1,51 +0,0 @@ -{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"s"} -{"Key":"o"} -{"Key":"escape"} -{"Get":{"state":"ˇo quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"j"} -{"Key":"w"} -{"Key":"."} -{"Get":{"state":"o quick brown\nfox ˇops over\nthe lazy dog","mode":"Normal"}} -{"Key":"f"} -{"Key":"r"} -{"Key":"."} -{"Get":{"state":"o quick brown\nfox ops oveˇothe lazy dog","mode":"Normal"}} -{"Put":{"state":"the ˇquick brown\nfox jumps over\nfox jumps over\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"j"} -{"Key":"x"} -{"Get":{"state":"the ˇumps over\nfox jumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"the ˇumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"w"} -{"Key":"."} -{"Get":{"state":"the umps ˇumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"j"} -{"Key":"."} -{"Get":{"state":"the umps umps over\nthe ˇog","mode":"Normal"}} -{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Key":"j"} -{"Key":"shift-i"} -{"Key":"o"} -{"Key":"escape"} -{"Get":{"state":"ˇothe quick brown\nofox jumps over\nothe lazy dog","mode":"Normal"}} -{"Key":"j"} -{"Key":"4"} -{"Key":"l"} -{"Key":"."} -{"Get":{"state":"othe quick brown\nofoxˇo jumps over\notheo lazy dog","mode":"Normal"}} -{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"shift-r"} -{"Key":"o"} -{"Key":"escape"} -{"Get":{"state":"ˇo\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"j"} -{"Key":"."} -{"Get":{"state":"o\nˇo\nthe lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_repeated_cb.json b/crates/vim/test_data/test_repeated_cb.json deleted file mode 100644 index 437b55ef29..0000000000 --- a/crates/vim/test_data/test_repeated_cb.json +++ /dev/null @@ -1,275 +0,0 @@ -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"The ˇick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"The ˇ brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"The quick ˇn\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"The quick ˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nˇjumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nfox ˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇver\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"ˇick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"ˇ brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"The ˇn\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"The ˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"The quick ˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"The quick brown\nˇjumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nfox ˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇver\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"ˇick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"ˇ brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"ˇn\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"ˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"The ˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"The quick ˇjumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"The quick brown\nˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nfox ˇver\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nfox ˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"ˇick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"ˇ brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"ˇn\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"ˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"ˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"The ˇjumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"The quick ˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"The quick brown\nˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nˇver\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"ˇick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"ˇ brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"ˇn\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"ˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"ˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"ˇjumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"The ˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"The quick ˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"The quick brown\nˇver\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"The quick brown\nˇ\nthe lazy dog\n","mode":"Insert"}} diff --git a/crates/vim/test_data/test_repeated_ce.json b/crates/vim/test_data/test_repeated_ce.json deleted file mode 100644 index 3032185d64..0000000000 --- a/crates/vim/test_data/test_repeated_ce.json +++ /dev/null @@ -1,275 +0,0 @@ -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"ˇ quick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"The quˇ brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"The quickˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"The quick browˇ jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"The quick brown\nˇ jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nˇ jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox ˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-oˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ lazy dog\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"ˇ brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"The quˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"The quickˇ jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"The quick browˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"The quick brown\nˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox ˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇ lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-oˇ lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ dog\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"ˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"The quˇ jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"The quickˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"The quick browˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"The quick brown\nˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox ˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇ dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-oˇ dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"ˇ jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"The quˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"The quickˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"The quick browˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"The quick brown\nˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox ˇ lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-oˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"ˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"The quˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"The quickˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"The quick browˇ lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"The quick brown\nˇ lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nˇ lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox ˇ dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-oˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_repeated_cj.json b/crates/vim/test_data/test_repeated_cj.json deleted file mode 100644 index e20270f97c..0000000000 --- a/crates/vim/test_data/test_repeated_cj.json +++ /dev/null @@ -1,275 +0,0 @@ -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"ˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"ˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"ˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"ˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"The quick brown\nˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"ˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"ˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"ˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"ˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"The quick brown\nˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"The quick brown\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"The quick brown\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"The quick brown\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_repeated_cl.json b/crates/vim/test_data/test_repeated_cl.json deleted file mode 100644 index 397a364e5a..0000000000 --- a/crates/vim/test_data/test_repeated_cl.json +++ /dev/null @@ -1,275 +0,0 @@ -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"ˇhe quick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"The quˇck brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"The quickˇbrown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"The quick browˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nˇox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox ˇumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇver\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-oˇer\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇhe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"ˇe quick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"The quˇk brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"The quickˇrown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"The quick browˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nˇx jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox ˇmps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇver\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇer\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-oˇr\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"ˇ quick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"The quˇ brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"The quickˇown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"The quick browˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nˇ jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox ˇps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇer\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇr\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-oˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ lazy dog\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"ˇquick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"The quˇbrown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"The quickˇwn\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"The quick browˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nˇjumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox ˇs-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇr\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-oˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇlazy dog\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"ˇuick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"The quˇrown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"The quickˇn\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"The quick browˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nˇumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox ˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-oˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇazy dog\n","mode":"Insert"}} diff --git a/crates/vim/test_data/test_repeated_word.json b/crates/vim/test_data/test_repeated_word.json deleted file mode 100644 index caa7ed7367..0000000000 --- a/crates/vim/test_data/test_repeated_word.json +++ /dev/null @@ -1,214 +0,0 @@ -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The ˇquick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The quick ˇbrown\n\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The quick ˇbrown\n\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe ˇlazy dog\n","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick ˇbrown\n\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe ˇlazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe ˇlazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy ˇdog\n","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe ˇlazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy ˇdog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy ˇdog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy dog\nˇ","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe ˇlazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy ˇdog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy dog\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy dog\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy dog\nˇ","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe ˇlazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy ˇdog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy dog\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy dog\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy dog\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy dog\nˇ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_replace_g.json b/crates/vim/test_data/test_replace_g.json deleted file mode 100644 index 583d1f89bc..0000000000 --- a/crates/vim/test_data/test_replace_g.json +++ /dev/null @@ -1,23 +0,0 @@ -{"Put":{"state":"ˇaa aa aa aa\naa\naa"}} -{"Key":":"} -{"Key":"s"} -{"Key":"/"} -{"Key":"a"} -{"Key":"a"} -{"Key":"/"} -{"Key":"b"} -{"Key":"b"} -{"Key":"enter"} -{"Get":{"state":"ˇbb aa aa aa\naa\naa","mode":"Normal"}} -{"Key":":"} -{"Key":"s"} -{"Key":"/"} -{"Key":"a"} -{"Key":"a"} -{"Key":"/"} -{"Key":"b"} -{"Key":"b"} -{"Key":"/"} -{"Key":"g"} -{"Key":"enter"} -{"Get":{"state":"ˇbb bb bb bb\naa\naa","mode":"Normal"}} diff --git a/crates/vim/test_data/test_replace_mode.json b/crates/vim/test_data/test_replace_mode.json deleted file mode 100644 index 32b2cc64d3..0000000000 --- a/crates/vim/test_data/test_replace_mode.json +++ /dev/null @@ -1,25 +0,0 @@ -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"n"} -{"Key":"e"} -{"Get":{"state":"Oneˇ quick brown\nfox jumps over\nthe lazy dog.","mode":"Replace"}} -{"Put":{"state":"The quick browˇn\nfox jumps over\nthe lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"n"} -{"Key":"e"} -{"Get":{"state":"The quick browOneˇ\nfox jumps over\nthe lazy dog.","mode":"Replace"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps over\nthe lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"n"} -{"Key":"e"} -{"Get":{"state":"The quick brown\nOneˇ\nfox jumps over\nthe lazy dog.","mode":"Replace"}} -{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog."}} -{"Key":"shift-r"} -{"Key":"enter"} -{"Key":"O"} -{"Key":"n"} -{"Key":"e"} -{"Get":{"state":"The qu\nOneˇ brown\nfox jumps over\nthe lazy dog.","mode":"Replace"}} diff --git a/crates/vim/test_data/test_replace_mode_repeat.json b/crates/vim/test_data/test_replace_mode_repeat.json deleted file mode 100644 index ab7cace51c..0000000000 --- a/crates/vim/test_data/test_replace_mode_repeat.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"ˇhello world\n"}} -{"Key":"shift-r"} -{"Key":"-"} -{"Key":"-"} -{"Key":"-"} -{"Key":"escape"} -{"Key":"4"} -{"Key":"l"} -{"Key":"."} -{"Get":{"state":"---lo --ˇ-ld\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_replace_mode_undo.json b/crates/vim/test_data/test_replace_mode_undo.json deleted file mode 100644 index fbad2f5042..0000000000 --- a/crates/vim/test_data/test_replace_mode_undo.json +++ /dev/null @@ -1,93 +0,0 @@ -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"n"} -{"Key":"e"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Get":{"state":"ˇThe quick brown fox jumps over the lazy dog.","mode":"Replace"}} -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"enter"} -{"Key":"e"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Get":{"state":"ˇThe quick brown fox jumps over the lazy dog.","mode":"Replace"}} -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"enter"} -{"Key":"n"} -{"Key":"enter"} -{"Key":"e"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Get":{"state":"ˇThe quick brown fox jumps over the lazy dog.","mode":"Replace"}} -{"Put":{"state":"The quick browˇn\nfox jumps over\nthe lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"n"} -{"Key":"e"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Get":{"state":"The quick browˇn\nfox jumps over\nthe lazy dog.","mode":"Replace"}} -{"Put":{"state":"The quick browˇn\nfox jumps over\nthe lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"enter"} -{"Key":"e"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Get":{"state":"The quick browˇn\nfox jumps over\nthe lazy dog.","mode":"Replace"}} -{"Put":{"state":"The quick browˇn\nfox jumps over\nthe lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"enter"} -{"Key":"n"} -{"Key":"enter"} -{"Key":"e"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Get":{"state":"The quick browˇn\nfox jumps over\nthe lazy dog.","mode":"Replace"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps over\nthe lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"n"} -{"Key":"e"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps over\nthe lazy dog.","mode":"Replace"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps over\nthe lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"enter"} -{"Key":"e"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps over\nthe lazy dog.","mode":"Replace"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps over\nthe lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"enter"} -{"Key":"n"} -{"Key":"enter"} -{"Key":"e"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps over\nthe lazy dog.","mode":"Replace"}} diff --git a/crates/vim/test_data/test_replace_mode_with_counts.json b/crates/vim/test_data/test_replace_mode_with_counts.json deleted file mode 100644 index d88c856fe3..0000000000 --- a/crates/vim/test_data/test_replace_mode_with_counts.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"ˇhello\n"}} -{"Key":"3"} -{"Key":"shift-r"} -{"Key":"-"} -{"Key":"escape"} -{"Get":{"state":"--ˇ-lo\n","mode":"Normal"}} -{"Put":{"state":"ˇhello\n"}} -{"Key":"3"} -{"Key":"shift-r"} -{"Key":"a"} -{"Key":"b"} -{"Key":"c"} -{"Key":"escape"} -{"Get":{"state":"abcabcabˇc\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_replace_n.json b/crates/vim/test_data/test_replace_n.json deleted file mode 100644 index a03c69e9b2..0000000000 --- a/crates/vim/test_data/test_replace_n.json +++ /dev/null @@ -1,13 +0,0 @@ -{"Put":{"state":"ˇaa\nbb\naa"}} -{"Key":":"} -{"Key":"s"} -{"Key":"/"} -{"Key":"b"} -{"Key":"b"} -{"Key":"/"} -{"Key":"d"} -{"Key":"d"} -{"Key":"/"} -{"Key":"n"} -{"Key":"enter"} -{"Get":{"state":"ˇaa\nbb\naa","mode":"Normal"}} diff --git a/crates/vim/test_data/test_replace_with_range.json b/crates/vim/test_data/test_replace_with_range.json deleted file mode 100644 index 8385af9a7f..0000000000 --- a/crates/vim/test_data/test_replace_with_range.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"ˇa\na\na\na\na\na\na\n "}} -{"Key":":"} -{"Key":"2"} -{"Key":","} -{"Key":"5"} -{"Key":"s"} -{"Key":"/"} -{"Key":"a"} -{"Key":"/"} -{"Key":"b"} -{"Key":"enter"} -{"Get":{"state":"a\nb\nb\nb\nˇb\na\na\n ","mode":"Normal"}} -{"Key":"/"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"a\nb\nb\nb\nb\nˇa\na\n ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_replace_with_range_at_start.json b/crates/vim/test_data/test_replace_with_range_at_start.json deleted file mode 100644 index e3810a7ba2..0000000000 --- a/crates/vim/test_data/test_replace_with_range_at_start.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"ˇa\na\na\na\na\na\na\n "}} -{"Key":":"} -{"Key":"2"} -{"Key":","} -{"Key":"5"} -{"Key":"s"} -{"Key":"/"} -{"Key":"^"} -{"Key":"/"} -{"Key":"b"} -{"Key":"enter"} -{"Get":{"state":"a\nba\nba\nba\nˇba\na\na\n ","mode":"Normal"}} -{"Key":"/"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"a\nba\nba\nba\nbˇa\na\na\n ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_scroll_beyond_last_line.json b/crates/vim/test_data/test_scroll_beyond_last_line.json deleted file mode 100644 index e983bda3f2..0000000000 --- a/crates/vim/test_data/test_scroll_beyond_last_line.json +++ /dev/null @@ -1,12 +0,0 @@ -{"SetOption":{"value":"scrolloff=3"}} -{"SetOption":{"value":"lines=12"}} -{"Put":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz"}} -{"Key":"shift-g"} -{"Key":"k"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nˇyy\nzz","mode":"Normal"}} -{"Key":"ctrl-d"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nˇzz","mode":"Normal"}} -{"Key":"shift-g"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nˇzz","mode":"Normal"}} -{"Key":"ctrl-u"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nˇrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} diff --git a/crates/vim/test_data/test_scroll_jumps.json b/crates/vim/test_data/test_scroll_jumps.json deleted file mode 100644 index 8d44457bf9..0000000000 --- a/crates/vim/test_data/test_scroll_jumps.json +++ /dev/null @@ -1,12 +0,0 @@ -{"SetOption":{"value":"scrolloff=3"}} -{"SetOption":{"value":"lines=22"}} -{"Put":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz\n{{\n||\n}}\n~~\n\n€€\n\n‚‚\nƒƒ\n„„\n……\n††\n‡‡\nˆˆ\n‰‰\nŠŠ\n‹‹\nŒŒ\n\nŽŽ\n\n\n‘‘\n’’\n““\n””"}} -{"Key":"shift-g"} -{"Key":"g"} -{"Key":"g"} -{"Key":"ctrl-d"} -{"Key":"ctrl-d"} -{"Key":"ctrl-o"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz\n{{\n||\n}}\n~~\n\n€€\n\n‚‚\nƒƒ\n„„\n……\n††\n‡‡\nˆˆ\n‰‰\nŠŠ\n‹‹\nŒŒ\n\nŽŽ\n\n\n‘‘\n’’\n““\nˇ””","mode":"Normal"}} -{"Key":"ctrl-o"} -{"Get":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz\n{{\n||\n}}\n~~\n\n€€\n\n‚‚\nƒƒ\n„„\n……\n††\n‡‡\nˆˆ\n‰‰\nŠŠ\n‹‹\nŒŒ\n\nŽŽ\n\n\n‘‘\n’’\n““\n””","mode":"Normal"}} diff --git a/crates/vim/test_data/test_search_skipping.json b/crates/vim/test_data/test_search_skipping.json deleted file mode 100644 index 6dae5e909f..0000000000 --- a/crates/vim/test_data/test_search_skipping.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"ˇaa aa aa"}} -{"Key":"/"} -{"Key":"a"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"aa ˇaa aa","mode":"Normal"}} -{"Key":"left"} -{"Key":"/"} -{"Key":"a"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"aa ˇaa aa","mode":"Normal"}} diff --git a/crates/vim/test_data/test_selection_goal.json b/crates/vim/test_data/test_selection_goal.json deleted file mode 100644 index ada2ab4c8e..0000000000 --- a/crates/vim/test_data/test_selection_goal.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":";;ˇ;\nLorem Ipsum"}} -{"Key":"a"} -{"Key":"down"} -{"Key":"up"} -{"Key":";"} -{"Key":"down"} -{"Key":"up"} -{"Get":{"state":";;;;ˇ\nLorem Ipsum","mode":"Insert"}} diff --git a/crates/vim/test_data/test_sentence_backwards.json b/crates/vim/test_data/test_sentence_backwards.json deleted file mode 100644 index 3126c3d39c..0000000000 --- a/crates/vim/test_data/test_sentence_backwards.json +++ /dev/null @@ -1,32 +0,0 @@ -{"Put":{"state":"one\n\ntwo\nthree\nˇ\nfour"}} -{"Key":"("} -{"Get":{"state":"one\n\nˇtwo\nthree\n\nfour","mode":"Normal"}} -{"Put":{"state":"hello.\n\n\nworˇld."}} -{"Key":"("} -{"Get":{"state":"hello.\n\n\nˇworld.","mode":"Normal"}} -{"Key":"("} -{"Get":{"state":"hello.\n\nˇ\nworld.","mode":"Normal"}} -{"Key":"("} -{"Get":{"state":"ˇhello.\n\n\nworld.","mode":"Normal"}} -{"Put":{"state":"hello. worlˇd."}} -{"Key":"("} -{"Get":{"state":"hello. ˇworld.","mode":"Normal"}} -{"Key":"("} -{"Get":{"state":"ˇhello. world.","mode":"Normal"}} -{"Put":{"state":". helˇlo."}} -{"Key":"("} -{"Get":{"state":". ˇhello.","mode":"Normal"}} -{"Key":"("} -{"Get":{"state":". ˇhello.","mode":"Normal"}} -{"Put":{"state":"{\n hello_world();\nˇ}"}} -{"Key":"("} -{"Get":{"state":"ˇ{\n hello_world();\n}","mode":"Normal"}} -{"Put":{"state":"Hello! World..?\n\n\tHello! World... ˇ"}} -{"Key":"("} -{"Get":{"state":"Hello! World..?\n\n\tHello! ˇWorld... ","mode":"Normal"}} -{"Key":"("} -{"Get":{"state":"Hello! World..?\n\n\tˇHello! World... ","mode":"Normal"}} -{"Key":"("} -{"Get":{"state":"Hello! World..?\nˇ\n\tHello! World... ","mode":"Normal"}} -{"Key":"("} -{"Get":{"state":"Hello! ˇWorld..?\n\n\tHello! World... ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_sentence_forwards.json b/crates/vim/test_data/test_sentence_forwards.json deleted file mode 100644 index 47939aae37..0000000000 --- a/crates/vim/test_data/test_sentence_forwards.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"helˇlo.\n\n\nworld."}} -{"Key":")"} -{"Get":{"state":"hello.\nˇ\n\nworld.","mode":"Normal"}} -{"Key":")"} -{"Get":{"state":"hello.\n\n\nˇworld.","mode":"Normal"}} -{"Key":")"} -{"Get":{"state":"hello.\n\n\nworldˇ.","mode":"Normal"}} -{"Put":{"state":"helˇlo.\n\n\nworld."}} diff --git a/crates/vim/test_data/test_shift_y.json b/crates/vim/test_data/test_shift_y.json deleted file mode 100644 index f68f1df18d..0000000000 --- a/crates/vim/test_data/test_shift_y.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":"The ˇquick brown\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"shift-y"} -{"Get":{"state":"ˇThe quick brown\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"The quick brown\n"}} diff --git a/crates/vim/test_data/test_singleline_surrounding_character_objects.json b/crates/vim/test_data/test_singleline_surrounding_character_objects.json deleted file mode 100644 index 2499786c72..0000000000 --- a/crates/vim/test_data/test_singleline_surrounding_character_objects.json +++ /dev/null @@ -1,37 +0,0 @@ -{"SetOption":{"value":"wrap"}} -{"SetOption":{"value":"columns=12"}} -{"Put":{"state":"\"ˇhello world\"!"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"\"«hello worldˇ»\"!","mode":"Visual"}} -{"Put":{"state":"\"hˇello world\"!"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"\"«hello worldˇ»\"!","mode":"Visual"}} -{"Put":{"state":"helˇlo \"world\"!"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"hello \"«worldˇ»\"!","mode":"Visual"}} -{"Put":{"state":"hello \"wˇorld\"!"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"hello \"«worldˇ»\"!","mode":"Visual"}} -{"Put":{"state":"hello \"wˇorld\"!"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"hello« \"world\"ˇ»!","mode":"Visual"}} -{"Put":{"state":"hello \"wˇorld\" !"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"hello «\"world\" ˇ»!","mode":"Visual"}} -{"Put":{"state":"hello \"wˇorld\"•\ngoodbye"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"hello «\"world\" ˇ»\ngoodbye","mode":"Visual"}} diff --git a/crates/vim/test_data/test_singleline_surrounding_character_objects_with_escape.json b/crates/vim/test_data/test_singleline_surrounding_character_objects_with_escape.json deleted file mode 100644 index 0de952ac91..0000000000 --- a/crates/vim/test_data/test_singleline_surrounding_character_objects_with_escape.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"h\"e\\\"lˇlo \\\"world\"!"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"h\"«e\\\"llo \\\"worldˇ»\"!","mode":"Visual"}} -{"Put":{"state":"hello \"teˇst \\\"inside\\\" world\""}} -{"Key":"v"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"hello \"«test \\\"inside\\\" worldˇ»\"","mode":"Visual"}} diff --git a/crates/vim/test_data/test_space_non_ascii.json b/crates/vim/test_data/test_space_non_ascii.json deleted file mode 100644 index 9caed6cb59..0000000000 --- a/crates/vim/test_data/test_space_non_ascii.json +++ /dev/null @@ -1,4 +0,0 @@ -{"Put":{"state":"ˇπππππ"}} -{"Key":"3"} -{"Key":"space"} -{"Get":{"state":"πππˇππ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_space_non_ascii_eol.json b/crates/vim/test_data/test_space_non_ascii_eol.json deleted file mode 100644 index 6c1bb73fa5..0000000000 --- a/crates/vim/test_data/test_space_non_ascii_eol.json +++ /dev/null @@ -1,4 +0,0 @@ -{"Put":{"state":"ππππˇπ\nπanotherline"}} -{"Key":"4"} -{"Key":"space"} -{"Get":{"state":"πππππ\nπanˇotherline","mode":"Normal"}} diff --git a/crates/vim/test_data/test_space_only_ascii_eol.json b/crates/vim/test_data/test_space_only_ascii_eol.json deleted file mode 100644 index 43a3af7945..0000000000 --- a/crates/vim/test_data/test_space_only_ascii_eol.json +++ /dev/null @@ -1,4 +0,0 @@ -{"Put":{"state":"aaaaˇaa\nanotherline"}} -{"Key":"4"} -{"Key":"space"} -{"Get":{"state":"aaaaaa\nanˇotherline","mode":"Normal"}} diff --git a/crates/vim/test_data/test_special_registers.json b/crates/vim/test_data/test_special_registers.json deleted file mode 100644 index 35f181a05c..0000000000 --- a/crates/vim/test_data/test_special_registers.json +++ /dev/null @@ -1,48 +0,0 @@ -{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown\nfox ˇ over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"-","value":"jumps"}} -{"Key":"\""} -{"Key":"_"} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"The quick brown\nthe ˇlazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"_","value":""}} -{"Key":"shift-v"} -{"Key":"\""} -{"Key":"_"} -{"Key":"y"} -{"Key":"w"} -{"Get":{"state":"The quick brown\nthe ˇlazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"jumps"}} -{"Get":{"state":"The quick brown\nthe ˇlazy dog","mode":"Normal"}} -{"Key":"\""} -{"Key":"\""} -{"Key":"d"} -{"Key":"^"} -{"Get":{"state":"The quick brown\nˇlazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"0","value":"the "}} -{"Get":{"state":"The quick brown\nˇlazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"the "}} -{"Key":"^"} -{"Key":"\""} -{"Key":"+"} -{"Key":"d"} -{"Key":"$"} -{"Get":{"state":"The quick brown\nˇ","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"lazy dog"}} -{"Get":{"state":"The quick brown\nˇ","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"lazy dog"}} -{"Key":"/"} -{"Key":"d"} -{"Key":"o"} -{"Key":"g"} -{"Key":"enter"} -{"Get":{"state":"The quick brown\nˇ","mode":"Normal"}} -{"ReadRegister":{"name":"/","value":"dog"}} -{"Key":"\""} -{"Key":"/"} -{"Key":"shift-p"} -{"Get":{"state":"The quick brown\ndoˇg","mode":"Normal"}} diff --git a/crates/vim/test_data/test_start_end_of_paragraph.json b/crates/vim/test_data/test_start_end_of_paragraph.json deleted file mode 100644 index 0de4d84f50..0000000000 --- a/crates/vim/test_data/test_start_end_of_paragraph.json +++ /dev/null @@ -1,13 +0,0 @@ -{"Put":{"state":"ˇabc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinal"}} -{"Key":"}"} -{"Get":{"state":"abc\ndef\nˇ\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}} -{"Key":"{"} -{"Get":{"state":"ˇabc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}} -{"Key":"2"} -{"Key":"}"} -{"Get":{"state":"abc\ndef\n\nparagraph\nthe second\nˇ\n\n\nthird and\nfinal","mode":"Normal"}} -{"Key":"}"} -{"Get":{"state":"abc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinaˇl","mode":"Normal"}} -{"Key":"2"} -{"Key":"{"} -{"Get":{"state":"abc\ndef\nˇ\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}} diff --git a/crates/vim/test_data/test_substitute_line.json b/crates/vim/test_data/test_substitute_line.json deleted file mode 100644 index eb0a9825f8..0000000000 --- a/crates/vim/test_data/test_substitute_line.json +++ /dev/null @@ -1,29 +0,0 @@ -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}} -{"Key":"shift-s"} -{"Key":"o"} -{"Get":{"state":"The quick brown\noˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}} -{"Key":"v"} -{"Key":"k"} -{"Key":"shift-s"} -{"Key":"o"} -{"Get":{"state":"oˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Key":"shift-s"} -{"Key":"o"} -{"Get":{"state":"The quick brown\noˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}} -{"Key":"v"} -{"Key":"$"} -{"Key":"shift-s"} -{"Key":"o"} -{"Get":{"state":"The quick brown\noˇ\nthe lazy dog\n","mode":"Insert"}} -{"SetOption":{"value":"shiftwidth=4"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}} -{"Key":">"} -{"Key":">"} -{"Key":"shift-s"} -{"Key":"o"} -{"Get":{"state":"The quick brown\n oˇ\nthe lazy dog\n","mode":"Insert"}} diff --git a/crates/vim/test_data/test_temporary_mode.json b/crates/vim/test_data/test_temporary_mode.json deleted file mode 100644 index be370cf744..0000000000 --- a/crates/vim/test_data/test_temporary_mode.json +++ /dev/null @@ -1,27 +0,0 @@ -{"Put":{"state":"lorem ˇipsum"}} -{"Key":"i"} -{"Get":{"state":"lorem ˇipsum","mode":"Insert"}} -{"Key":"ctrl-o"} -{"Key":"$"} -{"Get":{"state":"lorem ipsumˇ","mode":"Insert"}} -{"Put":{"state":"loremˇ ipsum dolor"}} -{"Key":"a"} -{"Get":{"state":"lorem ˇipsum dolor","mode":"Insert"}} -{"Key":"a"} -{"Key":"n"} -{"Key":"d"} -{"Key":"space"} -{"Key":"ctrl-o"} -{"Key":"w"} -{"Get":{"state":"lorem and ipsum ˇdolor","mode":"Insert"}} -{"Put":{"state":"lorem ˇipsum dolor"}} -{"Key":"i"} -{"Get":{"state":"lorem ˇipsum dolor","mode":"Insert"}} -{"Key":"a"} -{"Key":"n"} -{"Key":"d"} -{"Key":"space"} -{"Key":"ctrl-o"} -{"Key":"y"} -{"Key":"$"} -{"Get":{"state":"lorem and ˇipsum dolor","mode":"Insert"}} diff --git a/crates/vim/test_data/test_undo.json b/crates/vim/test_data/test_undo.json deleted file mode 100644 index e7f275743e..0000000000 --- a/crates/vim/test_data/test_undo.json +++ /dev/null @@ -1,45 +0,0 @@ -{"Put":{"state":"hello quˇoel world"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"s"} -{"Key":"c"} -{"Key":"o"} -{"Key":"escape"} -{"Key":"u"} -{"Get":{"state":"hello ˇquoel world","mode":"Normal"}} -{"Key":"ctrl-r"} -{"Get":{"state":"hello ˇco world","mode":"Normal"}} -{"Key":"a"} -{"Key":"o"} -{"Key":"right"} -{"Key":"l"} -{"Key":"escape"} -{"Get":{"state":"hello cooˇl world","mode":"Normal"}} -{"Key":"u"} -{"Get":{"state":"hello cooˇ world","mode":"Normal"}} -{"Key":"u"} -{"Get":{"state":"hello cˇo world","mode":"Normal"}} -{"Key":"u"} -{"Get":{"state":"hello ˇquoel world","mode":"Normal"}} -{"Put":{"state":"hello quˇoel world"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"~"} -{"Key":"u"} -{"Get":{"state":"hello ˇquoel world","mode":"Normal"}} -{"Put":{"state":"\nhello quˇoel world\n"}} -{"Key":"shift-v"} -{"Key":"s"} -{"Key":"c"} -{"Key":"escape"} -{"Key":"u"} -{"Get":{"state":"\nˇhello quoel world\n","mode":"Normal"}} -{"Put":{"state":"ˇ1\n2\n3"}} -{"Key":"ctrl-v"} -{"Key":"shift-g"} -{"Key":"ctrl-a"} -{"Get":{"state":"ˇ2\n3\n4","mode":"Normal"}} -{"Key":"u"} -{"Get":{"state":"ˇ1\n2\n3","mode":"Normal"}} diff --git a/crates/vim/test_data/test_undo_last_line.json b/crates/vim/test_data/test_undo_last_line.json deleted file mode 100644 index a2f6fc0995..0000000000 --- a/crates/vim/test_data/test_undo_last_line.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"ˇfn a() { }\nfn a() { }\nfn a() { }\n"}} -{"Key":"shift-g"} -{"Get":{"state":"fn a() { }\nfn a() { }\nfn a() { }\nˇ","mode":"Normal"}} -{"Key":"r"} -{"Key":"a"} -{"Get":{"state":"fn a() { }\nfn a() { }\nfn a() { }\nˇ","mode":"Normal"}} -{"Key":"shift-u"} -{"Get":{"state":"ˇ\nfn a() { }\nfn a() { }\n","mode":"Normal"}} -{"Key":"shift-u"} -{"Get":{"state":"ˇfn a() { }\nfn a() { }\nfn a() { }\n","mode":"Normal"}} -{"Key":"g"} -{"Key":"g"} -{"Key":"shift-u"} -{"Get":{"state":"ˇ\nfn a() { }\nfn a() { }\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_undo_last_line_newline.json b/crates/vim/test_data/test_undo_last_line_newline.json deleted file mode 100644 index 2b21ccef09..0000000000 --- a/crates/vim/test_data/test_undo_last_line_newline.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"ˇfn a() { }\nfn a() { }\nfn a() { }\n"}} -{"Key":"shift-g"} -{"Key":"k"} -{"Get":{"state":"fn a() { }\nfn a() { }\nˇfn a() { }\n","mode":"Normal"}} -{"Key":"o"} -{"Key":"h"} -{"Key":"e"} -{"Key":"l"} -{"Key":"l"} -{"Key":"o"} -{"Key":"escape"} -{"Get":{"state":"fn a() { }\nfn a() { }\nfn a() { }\nhellˇo\n","mode":"Normal"}} -{"Key":"shift-u"} -{"Get":{"state":"fn a() { }\nfn a() { }\nfn a() { }\nˇ\n","mode":"Normal"}} -{"Key":"shift-u"} diff --git a/crates/vim/test_data/test_undo_last_line_newline_many_changes.json b/crates/vim/test_data/test_undo_last_line_newline_many_changes.json deleted file mode 100644 index 6615e8d79a..0000000000 --- a/crates/vim/test_data/test_undo_last_line_newline_many_changes.json +++ /dev/null @@ -1,21 +0,0 @@ -{"Put":{"state":"ˇfn a() { }\nfn a() { }\nfn a() { }\n"}} -{"Key":"x"} -{"Key":"shift-g"} -{"Key":"k"} -{"Get":{"state":"n a() { }\nfn a() { }\nˇfn a() { }\n","mode":"Normal"}} -{"Key":"x"} -{"Key":"f"} -{"Key":"a"} -{"Key":"x"} -{"Key":"f"} -{"Key":"{"} -{"Key":"x"} -{"Get":{"state":"n a() { }\nfn a() { }\nn () ˇ }\n","mode":"Normal"}} -{"Key":"shift-u"} -{"Get":{"state":"n a() { }\nfn a() { }\nˇfn a() { }\n","mode":"Normal"}} -{"Key":"shift-u"} -{"Get":{"state":"n a() { }\nfn a() { }\nn () ˇ }\n","mode":"Normal"}} -{"Key":"shift-u"} -{"Get":{"state":"n a() { }\nfn a() { }\nˇfn a() { }\n","mode":"Normal"}} -{"Key":"shift-u"} -{"Get":{"state":"n a() { }\nfn a() { }\nn () ˇ }\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_undo_repeated_insert.json b/crates/vim/test_data/test_undo_repeated_insert.json deleted file mode 100644 index 98a00968c1..0000000000 --- a/crates/vim/test_data/test_undo_repeated_insert.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"hellˇo"}} -{"Key":"3"} -{"Key":"a"} -{"Key":"."} -{"Key":"escape"} -{"Get":{"state":"hello..ˇ.","mode":"Normal"}} -{"Key":"u"} -{"Get":{"state":"hellˇo","mode":"Normal"}} diff --git a/crates/vim/test_data/test_unmatched_backward.json b/crates/vim/test_data/test_unmatched_backward.json deleted file mode 100644 index bb3825dcd2..0000000000 --- a/crates/vim/test_data/test_unmatched_backward.json +++ /dev/null @@ -1,24 +0,0 @@ -{"Put":{"state":"func (a string) {\n do(something(with.anˇd_arrays[0, 2]))\n}"}} -{"Key":"["} -{"Key":"{"} -{"Get":{"state":"func (a string) ˇ{\n do(something(with.and_arrays[0, 2]))\n}","mode":"Normal"}} -{"Put":{"state":"func (a string) {\n do(somethiˇng(with.and_arrays[0, 2]))\n}"}} -{"Key":"["} -{"Key":"("} -{"Get":{"state":"func (a string) {\n doˇ(something(with.and_arrays[0, 2]))\n}","mode":"Normal"}} -{"Put":{"state":"{{}{} ˇ }"}} -{"Key":"["} -{"Key":"{"} -{"Get":{"state":"ˇ{{}{} }","mode":"Normal"}} -{"Put":{"state":"(()() ˇ )"}} -{"Key":"["} -{"Key":"("} -{"Get":{"state":"ˇ(()() )","mode":"Normal"}} -{"Put":{"state":"{\n {()} ˇ\n}"}} -{"Key":"["} -{"Key":"{"} -{"Get":{"state":"ˇ{\n {()} \n}","mode":"Normal"}} -{"Put":{"state":"(\n {()} ˇ\n)"}} -{"Key":"["} -{"Key":"("} -{"Get":{"state":"ˇ(\n {()} \n)","mode":"Normal"}} diff --git a/crates/vim/test_data/test_unmatched_backward_markdown.json b/crates/vim/test_data/test_unmatched_backward_markdown.json deleted file mode 100644 index c2df848b81..0000000000 --- a/crates/vim/test_data/test_unmatched_backward_markdown.json +++ /dev/null @@ -1,9 +0,0 @@ -{"Exec":{"command":"set filetype=markdown"}} -{"Put":{"state":"```rs\nimpl Worktree {\n pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {\nˇ }\n}\n```\n"}} -{"Key":"["} -{"Key":"{"} -{"Get":{"state":"```rs\nimpl Worktree {\n pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{\n }\n}\n```\n","mode":"Normal"}} -{"Put":{"state":"```rs\nimpl Worktree {\n pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {\n } ˇ\n}\n```\n"}} -{"Key":"["} -{"Key":"{"} -{"Get":{"state":"```rs\nimpl Worktree ˇ{\n pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {\n } \n}\n```\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_unmatched_forward.json b/crates/vim/test_data/test_unmatched_forward.json deleted file mode 100644 index a6b4a38f29..0000000000 --- a/crates/vim/test_data/test_unmatched_forward.json +++ /dev/null @@ -1,28 +0,0 @@ -{"Put":{"state":"func (a string) {\n do(something(with.anˇd_arrays[0, 2]))\n}"}} -{"Key":"]"} -{"Key":"}"} -{"Get":{"state":"func (a string) {\n do(something(with.and_arrays[0, 2]))\nˇ}","mode":"Normal"}} -{"Put":{"state":"func (a string) {\n do(somethiˇng(with.and_arrays[0, 2]))\n}"}} -{"Key":"]"} -{"Key":")"} -{"Get":{"state":"func (a string) {\n do(something(with.and_arrays[0, 2])ˇ)\n}","mode":"Normal"}} -{"Put":{"state":"func (a string) { a((b, cˇ))}"}} -{"Key":"]"} -{"Key":")"} -{"Get":{"state":"func (a string) { a((b, c)ˇ)}","mode":"Normal"}} -{"Put":{"state":"{ˇ {}{}}"}} -{"Key":"]"} -{"Key":"}"} -{"Get":{"state":"{ {}{}ˇ}","mode":"Normal"}} -{"Put":{"state":"(ˇ ()())"}} -{"Key":"]"} -{"Key":")"} -{"Get":{"state":"( ()()ˇ)","mode":"Normal"}} -{"Put":{"state":"{\n ˇ {()}\n}"}} -{"Key":"]"} -{"Key":"}"} -{"Get":{"state":"{\n {()}\nˇ}","mode":"Normal"}} -{"Put":{"state":"(\n ˇ {()}\n)"}} -{"Key":"]"} -{"Key":")"} -{"Get":{"state":"(\n {()}\nˇ)","mode":"Normal"}} diff --git a/crates/vim/test_data/test_unmatched_forward_markdown.json b/crates/vim/test_data/test_unmatched_forward_markdown.json deleted file mode 100644 index 753f68d04f..0000000000 --- a/crates/vim/test_data/test_unmatched_forward_markdown.json +++ /dev/null @@ -1,9 +0,0 @@ -{"Exec":{"command":"set filetype=markdown"}} -{"Put":{"state":"```rs\nimpl Worktree {\n pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {\nˇ }\n}\n```\n"}} -{"Key":"]"} -{"Key":"}"} -{"Get":{"state":"```rs\nimpl Worktree {\n pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {\n ˇ}\n}\n```\n","mode":"Normal"}} -{"Put":{"state":"```rs\nimpl Worktree {\n pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {\n } ˇ\n}\n```\n"}} -{"Key":"]"} -{"Key":"}"} -{"Get":{"state":"```rs\nimpl Worktree {\n pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {\n } \nˇ}\n```\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_v2ap.json b/crates/vim/test_data/test_v2ap.json deleted file mode 100644 index 7b4d31a5dc..0000000000 --- a/crates/vim/test_data/test_v2ap.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"The\nquicˇk\n\nbrown\nfox"}} -{"Key":"v"} -{"Key":"2"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"«The\nquick\n\nbrown\nfˇ»ox","mode":"VisualLine"}} diff --git a/crates/vim/test_data/test_v_search.json b/crates/vim/test_data/test_v_search.json deleted file mode 100644 index b8af915519..0000000000 --- a/crates/vim/test_data/test_v_search.json +++ /dev/null @@ -1,28 +0,0 @@ -{"Put":{"state":"ˇa.c. abcd a.c. abcd"}} -{"Key":"v"} -{"Key":"/"} -{"Key":"c"} -{"Key":"d"} -{"Key":"enter"} -{"Get":{"state":"«a.c. abcˇ»d a.c. abcd","mode":"Visual"}} -{"Put":{"state":"a a aˇ a a a"}} -{"Key":"v"} -{"Key":"/"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"a a a« aˇ» a a","mode":"Visual"}} -{"Key":"/"} -{"Key":"enter"} -{"Get":{"state":"a a a« a aˇ» a","mode":"Visual"}} -{"Key":"?"} -{"Key":"enter"} -{"Get":{"state":"a a a« aˇ» a a","mode":"Visual"}} -{"Key":"?"} -{"Key":"enter"} -{"Get":{"state":"a a «ˇa »a a a","mode":"Visual"}} -{"Key":"/"} -{"Key":"enter"} -{"Get":{"state":"a a a« aˇ» a a","mode":"Visual"}} -{"Key":"/"} -{"Key":"enter"} -{"Get":{"state":"a a a« a aˇ» a","mode":"Visual"}} diff --git a/crates/vim/test_data/test_v_search_aa.json b/crates/vim/test_data/test_v_search_aa.json deleted file mode 100644 index 12d4f51601..0000000000 --- a/crates/vim/test_data/test_v_search_aa.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":"ˇaa aa"}} -{"Key":"v"} -{"Key":"/"} -{"Key":"a"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"«aa aˇ»a","mode":"Visual"}} diff --git a/crates/vim/test_data/test_visual_block_insert.json b/crates/vim/test_data/test_visual_block_insert.json deleted file mode 100644 index d3d2689bd3..0000000000 --- a/crates/vim/test_data/test_visual_block_insert.json +++ /dev/null @@ -1,18 +0,0 @@ -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog\n"}} -{"Key":"ctrl-v"} -{"Key":"9"} -{"Key":"down"} -{"Get":{"state":"«Tˇ»he quick brown\n«fˇ»ox jumps over\n«tˇ»he lazy dog\nˇ","mode":"VisualBlock"}} -{"Key":"shift-i"} -{"Key":"k"} -{"Key":"escape"} -{"Get":{"state":"ˇkThe quick brown\nkfox jumps over\nkthe lazy dog\nk","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog\n"}} -{"Key":"ctrl-v"} -{"Key":"9"} -{"Key":"down"} -{"Get":{"state":"«Tˇ»he quick brown\n«fˇ»ox jumps over\n«tˇ»he lazy dog\nˇ","mode":"VisualBlock"}} -{"Key":"c"} -{"Key":"k"} -{"Key":"escape"} -{"Get":{"state":"ˇkhe quick brown\nkox jumps over\nkhe lazy dog\nk","mode":"Normal"}} diff --git a/crates/vim/test_data/test_visual_block_issue_2123.json b/crates/vim/test_data/test_visual_block_issue_2123.json deleted file mode 100644 index 0f48bcc890..0000000000 --- a/crates/vim/test_data/test_visual_block_issue_2123.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog\n"}} -{"Key":"ctrl-v"} -{"Key":"right"} -{"Key":"down"} -{"Get":{"state":"The «quˇ»ick brown\nfox «juˇ»mps over\nthe lazy dog\n","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_block_mode.json b/crates/vim/test_data/test_visual_block_mode.json deleted file mode 100644 index 2c5ad576c4..0000000000 --- a/crates/vim/test_data/test_visual_block_mode.json +++ /dev/null @@ -1,38 +0,0 @@ -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"ctrl-v"} -{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":"VisualBlock"}} -{"Key":"2"} -{"Key":"down"} -{"Get":{"state":"The «qˇ»uick brown\nfox «jˇ»umps over\nthe «lˇ»azy dog","mode":"VisualBlock"}} -{"Key":"e"} -{"Get":{"state":"The «quicˇ»k brown\nfox «jumpˇ»s over\nthe «lazyˇ» dog","mode":"VisualBlock"}} -{"Key":"^"} -{"Get":{"state":"«ˇThe q»uick brown\n«ˇfox j»umps over\n«ˇthe l»azy dog","mode":"VisualBlock"}} -{"Key":"$"} -{"Get":{"state":"The «quick brownˇ»\nfox «jumps overˇ»\nthe «lazy dogˇ»","mode":"VisualBlock"}} -{"Key":"shift-f"} -{"Key":"space"} -{"Get":{"state":"The «quickˇ» brown\nfox «jumpsˇ» over\nthe «lazy ˇ»dog","mode":"VisualBlock"}} -{"Key":"v"} -{"Get":{"state":"The «quick brown\nfox jumps over\nthe lazy ˇ»dog","mode":"Visual"}} -{"Key":"ctrl-v"} -{"Get":{"state":"The «quickˇ» brown\nfox «jumpsˇ» over\nthe «lazy ˇ»dog","mode":"VisualBlock"}} -{"Put":{"state":"The ˇquick\nbrown\nfox\njumps over the\n\nlazy dog\n"}} -{"Key":"ctrl-v"} -{"Key":"down"} -{"Key":"down"} -{"Get":{"state":"The«ˇ q»uick\nbro«ˇwn»\nfoxˇ\njumps over the\n\nlazy dog\n","mode":"VisualBlock"}} -{"Key":"down"} -{"Get":{"state":"The «qˇ»uick\nbrow«nˇ»\nfox\njump«sˇ» over the\n\nlazy dog\n","mode":"VisualBlock"}} -{"Key":"left"} -{"Get":{"state":"The«ˇ q»uick\nbro«ˇwn»\nfoxˇ\njum«ˇps» over the\n\nlazy dog\n","mode":"VisualBlock"}} -{"Key":"s"} -{"Key":"o"} -{"Key":"escape"} -{"Get":{"state":"Theˇouick\nbroo\nfoxo\njumo over the\n\nlazy dog\n","mode":"Normal"}} -{"Put":{"state":"Theˇ quick brown\n\nfox jumps over\nthe lazy dog\n"}} -{"Key":"l"} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Key":"j"} -{"Get":{"state":"The «qˇ»uick brown\n\nfox «jˇ»umps over\nthe lazy dog\n","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_block_mode_down_right.json b/crates/vim/test_data/test_visual_block_mode_down_right.json deleted file mode 100644 index fbb0558a89..0000000000 --- a/crates/vim/test_data/test_visual_block_mode_down_right.json +++ /dev/null @@ -1,9 +0,0 @@ -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"ctrl-v"} -{"Key":"l"} -{"Key":"l"} -{"Key":"l"} -{"Key":"l"} -{"Key":"l"} -{"Key":"j"} -{"Get":{"state":"The «quick ˇ»brown\nfox «jumps ˇ»over\nthe lazy dog","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_block_mode_other_end.json b/crates/vim/test_data/test_visual_block_mode_other_end.json deleted file mode 100644 index bf7956f0e8..0000000000 --- a/crates/vim/test_data/test_visual_block_mode_other_end.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}} -{"Key":"ctrl-v"} -{"Key":"l"} -{"Key":"l"} -{"Key":"l"} -{"Key":"l"} -{"Key":"j"} -{"Get":{"state":"The quick brown\nfox j«umps ˇ»over\nthe l«azy dˇ»og","mode":"VisualBlock"}} -{"Key":"o"} -{"Key":"k"} -{"Get":{"state":"The q«ˇuick »brown\nfox j«ˇumps »over\nthe l«ˇazy d»og","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_block_mode_shift_other_end.json b/crates/vim/test_data/test_visual_block_mode_shift_other_end.json deleted file mode 100644 index 03882167b4..0000000000 --- a/crates/vim/test_data/test_visual_block_mode_shift_other_end.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}} -{"Key":"ctrl-v"} -{"Key":"l"} -{"Key":"l"} -{"Key":"l"} -{"Key":"l"} -{"Key":"j"} -{"Get":{"state":"The quick brown\nfox j«umps ˇ»over\nthe l«azy dˇ»og","mode":"VisualBlock"}} -{"Key":"shift-o"} -{"Key":"k"} -{"Get":{"state":"The quick brown\nfox j«ˇumps »over\nthe lazy dog","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_block_mode_up_left.json b/crates/vim/test_data/test_visual_block_mode_up_left.json deleted file mode 100644 index 00bbd577c3..0000000000 --- a/crates/vim/test_data/test_visual_block_mode_up_left.json +++ /dev/null @@ -1,9 +0,0 @@ -{"Put":{"state":"The quick brown\nfox jumpsˇ over\nthe lazy dog"}} -{"Key":"ctrl-v"} -{"Key":"h"} -{"Key":"h"} -{"Key":"h"} -{"Key":"h"} -{"Key":"h"} -{"Key":"k"} -{"Get":{"state":"The «ˇquick »brown\nfox «ˇjumps »over\nthe lazy dog","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_block_search.json b/crates/vim/test_data/test_visual_block_search.json deleted file mode 100644 index 1943ce906f..0000000000 --- a/crates/vim/test_data/test_visual_block_search.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":"ˇone two\nthree four\nfive six\n"}} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Key":"/"} -{"Key":"f"} -{"Key":"enter"} -{"Get":{"state":"«one twoˇ»\n«three fˇ»our\nfive six\n","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_block_wrapping_selection.json b/crates/vim/test_data/test_visual_block_wrapping_selection.json deleted file mode 100644 index bbe945cfef..0000000000 --- a/crates/vim/test_data/test_visual_block_wrapping_selection.json +++ /dev/null @@ -1,16 +0,0 @@ -{"SetOption":{"value":"wrap"}} -{"SetOption":{"value":"columns=12"}} -{"Put":{"state":"ˇ12345678901234567890\n12345678901234567890\n12345678901234567890\n"}} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Get":{"state":"«1ˇ»2345678901234567890\n«1ˇ»2345678901234567890\n12345678901234567890\n","mode":"VisualBlock"}} -{"Put":{"state":"ˇ123456789012345678901234567890123456789012345678901234567890\n1234567890123456789012345678901234567890\n12345678901234567890\n"}} -{"Key":"ctrl-v"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"«1ˇ»23456789012345678901234567890123456789012345678901234567890\n«1ˇ»234567890123456789012345678901234567890\n«1ˇ»2345678901234567890\n","mode":"VisualBlock"}} -{"Put":{"state":"123456789012345678901234567890123456789012345678901234567890\n1234567890123456789012345678901234567890\nˇ12345678901234567890\n"}} -{"Key":"ctrl-v"} -{"Key":"2"} -{"Key":"k"} -{"Get":{"state":"«1ˇ»23456789012345678901234567890123456789012345678901234567890\n«1ˇ»234567890123456789012345678901234567890\n«1ˇ»2345678901234567890\n","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_change.json b/crates/vim/test_data/test_visual_change.json deleted file mode 100644 index 297ad0d0d6..0000000000 --- a/crates/vim/test_data/test_visual_change.json +++ /dev/null @@ -1,47 +0,0 @@ -{"Put":{"state":"The quick ˇbrown"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"c"} -{"Get":{"state":"The quick ˇ","mode":"Insert"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Key":"c"} -{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Insert"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Key":"c"} -{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Key":"c"} -{"Get":{"state":"The quick brown\nfox jumps ˇhe lazy dog","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Key":"c"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇog","mode":"Insert"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"k"} -{"Key":"c"} -{"Get":{"state":"The ˇrown\nfox jumps over\nthe lazy dog","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"k"} -{"Key":"c"} -{"Get":{"state":"The quick brown\nˇver\nthe lazy dog","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"k"} -{"Key":"c"} -{"Get":{"state":"The quick brown\nfox jumpsˇazy dog","mode":"Insert"}} diff --git a/crates/vim/test_data/test_visual_delete.json b/crates/vim/test_data/test_visual_delete.json deleted file mode 100644 index d9f8055600..0000000000 --- a/crates/vim/test_data/test_visual_delete.json +++ /dev/null @@ -1,48 +0,0 @@ -{"Put":{"state":"The quick ˇbrown"}} -{"Key":"v"} -{"Key":"w"} -{"Get":{"state":"The quick «brownˇ»","mode":"Visual"}} -{"Put":{"state":"The quick ˇbrown"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"x"} -{"Get":{"state":"The quickˇ ","mode":"Normal"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Key":"x"} -{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Normal"}} -{"Key":"j"} -{"Key":"p"} -{"Get":{"state":"The ver\nthe lˇquick brown\nfox jumps oazy dog","mode":"Normal"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Key":"x"} -{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Key":"x"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇog","mode":"Normal"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"b"} -{"Key":"k"} -{"Key":"x"} -{"Get":{"state":"ˇuick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}} -{"Key":"v"} -{"Key":"b"} -{"Key":"k"} -{"Key":"x"} -{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} -{"Key":"v"} -{"Key":"b"} -{"Key":"k"} -{"Key":"x"} -{"Get":{"state":"The quick brown\nˇazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_visual_line_change.json b/crates/vim/test_data/test_visual_line_change.json deleted file mode 100644 index 336108c6cb..0000000000 --- a/crates/vim/test_data/test_visual_line_change.json +++ /dev/null @@ -1,35 +0,0 @@ -{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"c"} -{"Get":{"state":"ˇ\nfox jumps over\nthe lazy dog","mode":"Insert"}} -{"Key":"escape"} -{"Key":"j"} -{"Key":"p"} -{"Get":{"state":"\nfox jumps over\nˇThe quick brown\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"c"} -{"Get":{"state":"The quick brown\nˇ\nthe lazy dog","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}} -{"Key":"shift-v"} -{"Key":"c"} -{"Get":{"state":"The quick brown\nfox jumps over\nˇ","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"c"} -{"Get":{"state":"ˇ\nthe lazy dog","mode":"Insert"}} -{"Key":"escape"} -{"Key":"j"} -{"Key":"p"} -{"Get":{"state":"\nthe lazy dog\nˇThe quick brown\nfox jumps over","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"c"} -{"Get":{"state":"The quick brown\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"c"} -{"Get":{"state":"The quick brown\nfox jumps over\nˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_visual_line_delete.json b/crates/vim/test_data/test_visual_line_delete.json deleted file mode 100644 index e221a4ad5f..0000000000 --- a/crates/vim/test_data/test_visual_line_delete.json +++ /dev/null @@ -1,23 +0,0 @@ -{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"x"} -{"Get":{"state":"fox juˇmps over\nthe lazy dog","mode":"Normal"}} -{"Key":"p"} -{"Get":{"state":"fox jumps over\nˇThe quick brown\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}} -{"Key":"shift-v"} -{"Key":"x"} -{"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"the lazy dog\n"}} -{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"x"} -{"Get":{"state":"the laˇzy dog","mode":"Normal"}} -{"Key":"p"} -{"Get":{"state":"the lazy dog\nˇThe quick brown\nfox jumps over","mode":"Normal"}} -{"Put":{"state":"The ˇlong line\nshould not\ncrash\n"}} -{"Key":"shift-v"} -{"Key":"$"} -{"Key":"x"} -{"Get":{"state":"should noˇt\ncrash\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_visual_match_eol.json b/crates/vim/test_data/test_visual_match_eol.json deleted file mode 100644 index 9909638e3f..0000000000 --- a/crates/vim/test_data/test_visual_match_eol.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":"fn aˇ() {\n return\n}\n"}} -{"Key":"v"} -{"Key":"$"} -{"Key":"%"} -{"Get":{"state":"fn a«() {\n return\n}ˇ»\n","mode":"Visual"}} diff --git a/crates/vim/test_data/test_visual_mode_insert_before_after.json b/crates/vim/test_data/test_visual_mode_insert_before_after.json deleted file mode 100644 index 065f05dea4..0000000000 --- a/crates/vim/test_data/test_visual_mode_insert_before_after.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"heˇllo"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"shift-i"} -{"Get":{"state":"ˇhello","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"shift-i"} -{"Get":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"shift-a"} -{"Get":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog","mode":"Insert"}} diff --git a/crates/vim/test_data/test_visual_object.json b/crates/vim/test_data/test_visual_object.json deleted file mode 100644 index 7c95a8dc73..0000000000 --- a/crates/vim/test_data/test_visual_object.json +++ /dev/null @@ -1,19 +0,0 @@ -{"Put":{"state":"hello (in [parˇens] o)"}} -{"Key":"ctrl-v"} -{"Key":"l"} -{"Key":"a"} -{"Key":"]"} -{"Get":{"state":"hello (in «[parens]ˇ» o)","mode":"Visual"}} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"hello («in [parens] oˇ»)","mode":"Visual"}} -{"Put":{"state":"hello in a wˇord again."}} -{"Key":"ctrl-v"} -{"Key":"l"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"hello in a w«ordˇ» again.","mode":"VisualBlock"}} -{"Key":"o"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"«ˇhello in a word» again.","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_object_expands.json b/crates/vim/test_data/test_visual_object_expands.json deleted file mode 100644 index ea285ea5be..0000000000 --- a/crates/vim/test_data/test_visual_object_expands.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"{\n {\n ˇ }\n}\n{\n}\n"}} -{"Key":"v"} -{"Key":"l"} -{"Get":{"state":"{\n {\n « }ˇ»\n}\n{\n}\n","mode":"Visual"}} -{"Key":"a"} -{"Key":"{"} -{"Get":{"state":"{\n «{\n }ˇ»\n}\n{\n}\n","mode":"Visual"}} -{"Key":"a"} -{"Key":"{"} -{"Get":{"state":"«{\n {\n }\n}ˇ»\n{\n}\n","mode":"Visual"}} diff --git a/crates/vim/test_data/test_visual_paragraph_object.json b/crates/vim/test_data/test_visual_paragraph_object.json deleted file mode 100644 index 604d6dc93f..0000000000 --- a/crates/vim/test_data/test_visual_paragraph_object.json +++ /dev/null @@ -1,80 +0,0 @@ -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"«The quick brown\nfox jumps over\ntˇ»he lazy dog.\n","mode":"VisualLine"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"«The quick brown\nfox jumps over\nthe lazy dog.\nˇ»","mode":"VisualLine"}} -{"Put":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"«\n\nˇ»The quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"VisualLine"}} -{"Put":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\n«The quick brown fox jumps\noˇ»ver the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"VisualLine"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n«\n\nˇ»The quick brown fox jumps\nover the lazy dog.\n","mode":"VisualLine"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n«The quick brown fox jumps\noˇ»ver the lazy dog.\n","mode":"VisualLine"}} -{"Put":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"«\n\nThe quick brown fox jumps\noˇ»ver the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"VisualLine"}} -{"Put":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\n«The quick brown fox jumps\nover the lazy dog.\n\n\nˇ»The quick brown fox jumps\nover the lazy dog.\n","mode":"VisualLine"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n«\n\nThe quick brown fox jumps\noˇ»ver the lazy dog.\n","mode":"VisualLine"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n«The quick brown fox jumps\nover the lazy dog.\nˇ»","mode":"VisualLine"}} -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"«Tˇ»he quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n","mode":"VisualLine"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n«\nˇ»The quick brown fox jumps over the lazy dog.\n\n","mode":"VisualLine"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\n«Tˇ»he quick brown fox jumps over the lazy dog.\n\n","mode":"VisualLine"}} -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"«The quick brown fox jumps over the lazy dog.\n\nˇ»The quick brown fox jumps over the lazy dog.\n\n","mode":"VisualLine"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n«\nTˇ»he quick brown fox jumps over the lazy dog.\n\n","mode":"VisualLine"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\n«The quick brown fox jumps over the lazy dog.\n\nˇ»","mode":"VisualLine"}} diff --git a/crates/vim/test_data/test_visual_paragraph_object_with_soft_wrap.json b/crates/vim/test_data/test_visual_paragraph_object_with_soft_wrap.json deleted file mode 100644 index 6bfce2f955..0000000000 --- a/crates/vim/test_data/test_visual_paragraph_object_with_soft_wrap.json +++ /dev/null @@ -1,72 +0,0 @@ -{"SetOption":{"value":"wrap"}} -{"SetOption":{"value":"columns=20"}} -{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"«Fˇ»irst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"«ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is l»imited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\n«Sˇ»econd paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\n«ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and s»hould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\n«Tˇ»hird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\n«ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping s»ettings.\n","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\n«ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.»\n","mode":"VisualLine"}} -{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"«First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ»Second paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is «limited making it span multiple display lines.\n\nˇ»Second paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\n«Second paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ»Third paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and «should be handled correctly.\n\nˇ»Third paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\n«Third paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\nˇ»","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping «settings.\nˇ»","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings«.\nˇ»","mode":"VisualLine"}} diff --git a/crates/vim/test_data/test_visual_sentence_object.json b/crates/vim/test_data/test_visual_sentence_object.json deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/vim/test_data/test_visual_shift_d.json b/crates/vim/test_data/test_visual_shift_d.json deleted file mode 100644 index de037d788c..0000000000 --- a/crates/vim/test_data/test_visual_shift_d.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog\n"}} -{"Key":"v"} -{"Key":"down"} -{"Key":"shift-d"} -{"Get":{"state":"the ˇlazy dog\n","mode":"Normal"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog\n"}} -{"Key":"ctrl-v"} -{"Key":"down"} -{"Key":"shift-d"} -{"Get":{"state":"Theˇ \nfox \nthe lazy dog\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_visual_star_hash.json b/crates/vim/test_data/test_visual_star_hash.json deleted file mode 100644 index d6523c4a45..0000000000 --- a/crates/vim/test_data/test_visual_star_hash.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"ˇa.c. abcd a.c. abcd"}} -{"Key":"v"} -{"Key":"3"} -{"Key":"l"} -{"Key":"*"} -{"Get":{"state":"a.c. abcd ˇa.c. abcd","mode":"Normal"}} diff --git a/crates/vim/test_data/test_visual_word_object.json b/crates/vim/test_data/test_visual_word_object.json deleted file mode 100644 index 5e1a9839e9..0000000000 --- a/crates/vim/test_data/test_visual_word_object.json +++ /dev/null @@ -1,236 +0,0 @@ -{"Put":{"state":"The quick brown\nˇ\nfox"}} -{"Key":"v"} -{"Get":{"state":"The quick brown\n«\nˇ»fox","mode":"Visual"}} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n«\nˇ»fox","mode":"Visual"}} -{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown« ˇ»\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps« ˇ»over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog« ˇ»\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n«\nˇ»\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n«\nˇ»\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n«\nˇ»The-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«Theˇ»-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe«-ˇ»quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-«quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-«quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick« ˇ»brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick «brownˇ» \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown« ˇ»\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n« ˇ»\n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n« ˇ»\n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n« ˇ»fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-«jumpsˇ» over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n«\nˇ»","mode":"Visual"}} -{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown« ˇ»\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps« ˇ»over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog« ˇ»\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n«\nˇ»\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n«\nˇ»\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n«\nˇ»The-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick« ˇ»brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick «brownˇ» \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown« ˇ»\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n« ˇ»\n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n« ˇ»\n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n« ˇ»fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n «fox-jumpsˇ» over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n«\nˇ»","mode":"Visual"}} diff --git a/crates/vim/test_data/test_visual_yank.json b/crates/vim/test_data/test_visual_yank.json deleted file mode 100644 index ed1ff2eb3f..0000000000 --- a/crates/vim/test_data/test_visual_yank.json +++ /dev/null @@ -1,42 +0,0 @@ -{"Put":{"state":"The quick ˇbrown"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"y"} -{"Get":{"state":"The quick ˇbrown","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"brown"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Key":"y"} -{"Get":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"quick brown\nfox jumps o"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Key":"y"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"lazy d"}} -{"Key":"shift-v"} -{"Key":"y"} -{"Get":{"state":"The quick brown\nfox jumps over\nˇthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"the lazy dog\n"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"b"} -{"Key":"k"} -{"Key":"y"} -{"Get":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"shift-g"} -{"Key":"shift-y"} -{"Get":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"fox jumps over\nthe lazy dog\n"}} -{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"$"} -{"Key":"shift-y"} -{"Get":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"fox jumps over\n"}} diff --git a/crates/vim/test_data/test_w.json b/crates/vim/test_data/test_w.json deleted file mode 100644 index b7b3bd0e63..0000000000 --- a/crates/vim/test_data/test_w.json +++ /dev/null @@ -1,40 +0,0 @@ -{"Put":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"w"} -{"Get":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"w"} -{"Get":{"state":"The quick-ˇbrown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"w"} -{"Get":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"w"} -{"Get":{"state":"The quick-brown\n\nˇ\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"w"} -{"Get":{"state":"The quick-brown\n\n\nˇfox_jumps over\nthe","mode":"Normal"}} -{"Key":"w"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps ˇover\nthe","mode":"Normal"}} -{"Key":"w"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nˇthe","mode":"Normal"}} -{"Key":"w"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} -{"Key":"w"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} -{"Put":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-w"} -{"Get":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-w"} -{"Get":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-ˇbrown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-w"} -{"Get":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"shift-w"} -{"Get":{"state":"The quick-brown\n\nˇ\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"shift-w"} -{"Get":{"state":"The quick-brown\n\n\nˇfox_jumps over\nthe","mode":"Normal"}} -{"Key":"shift-w"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps ˇover\nthe","mode":"Normal"}} -{"Key":"shift-w"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nˇthe","mode":"Normal"}} -{"Key":"shift-w"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} -{"Key":"shift-w"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} diff --git a/crates/vim/test_data/test_window_bottom.json b/crates/vim/test_data/test_window_bottom.json deleted file mode 100644 index a4855ccb96..0000000000 --- a/crates/vim/test_data/test_window_bottom.json +++ /dev/null @@ -1,19 +0,0 @@ -{"Put":{"state":"abc\ndeˇf\nparagraph\nthe second\nthird and\nfinal"}} -{"Key":"shift-l"} -{"Get":{"state":"abc\ndef\nparagraph\nthe second\nthird and\nfiˇnal","mode":"Normal"}} -{"Put":{"state":"1 2 3\n4 5 ˇ6\n7 8 9\n"}} -{"Key":"shift-l"} -{"Get":{"state":"1 2 3\n4 5 6\n7 8 9\nˇ","mode":"Normal"}} -{"Put":{"state":"1 2 3\nˇ4 5 6\n7 8 9\n"}} -{"Key":"shift-l"} -{"Get":{"state":"1 2 3\n4 5 6\n7 8 9\nˇ","mode":"Normal"}} -{"Put":{"state":"1 2 ˇ3\n4 5 6\n7 8 9\n"}} -{"Key":"shift-l"} -{"Get":{"state":"1 2 3\n4 5 6\n7 8 9\nˇ","mode":"Normal"}} -{"Put":{"state":"ˇ1 2 3\n4 5 6\n7 8 9\n"}} -{"Key":"shift-l"} -{"Get":{"state":"1 2 3\n4 5 6\n7 8 9\nˇ","mode":"Normal"}} -{"Put":{"state":"1 2 3\n4 5 ˇ6\n7 8 9\n"}} -{"Key":"9"} -{"Key":"shift-l"} -{"Get":{"state":"1 2 ˇ3\n4 5 6\n7 8 9\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_window_middle.json b/crates/vim/test_data/test_window_middle.json deleted file mode 100644 index 91154923e4..0000000000 --- a/crates/vim/test_data/test_window_middle.json +++ /dev/null @@ -1,17 +0,0 @@ -{"Put":{"state":"abˇc\ndef\nparagraph\nthe second\nthird and\nfinal"}} -{"Key":"shift-m"} -{"Get":{"state":"abc\ndef\npaˇragraph\nthe second\nthird and\nfinal","mode":"Normal"}} -{"Put":{"state":"1 2 3\n4 5 6\n7 8 ˇ9\n"}} -{"Key":"shift-m"} -{"Get":{"state":"1 2 3\n4 5 ˇ6\n7 8 9\n","mode":"Normal"}} -{"Put":{"state":"1 2 3\n4 5 6\nˇ7 8 9\n"}} -{"Key":"shift-m"} -{"Get":{"state":"1 2 3\nˇ4 5 6\n7 8 9\n","mode":"Normal"}} -{"Put":{"state":"ˇ1 2 3\n4 5 6\n7 8 9\n"}} -{"Key":"shift-m"} -{"Get":{"state":"1 2 3\nˇ4 5 6\n7 8 9\n","mode":"Normal"}} -{"Key":"shift-m"} -{"Get":{"state":"1 2 3\nˇ4 5 6\n7 8 9\n","mode":"Normal"}} -{"Put":{"state":"1 2 3\n4 5 ˇ6\n7 8 9\n"}} -{"Key":"shift-m"} -{"Get":{"state":"1 2 3\n4 5 ˇ6\n7 8 9\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_window_top.json b/crates/vim/test_data/test_window_top.json deleted file mode 100644 index aab6bd6280..0000000000 --- a/crates/vim/test_data/test_window_top.json +++ /dev/null @@ -1,13 +0,0 @@ -{"Put":{"state":"abc\ndef\nparagraph\nthe second\nthird ˇand\nfinal"}} -{"Key":"shift-h"} -{"Get":{"state":"abˇc\ndef\nparagraph\nthe second\nthird and\nfinal","mode":"Normal"}} -{"Put":{"state":"1 2 3\n4 5 6\n7 8 ˇ9\n"}} -{"Key":"shift-h"} -{"Get":{"state":"1 2 ˇ3\n4 5 6\n7 8 9\n","mode":"Normal"}} -{"Put":{"state":"1 2 3\n4 5 6\nˇ7 8 9\n"}} -{"Key":"shift-h"} -{"Get":{"state":"ˇ1 2 3\n4 5 6\n7 8 9\n","mode":"Normal"}} -{"Put":{"state":"1 2 3\n4 5 ˇ6\n7 8 9"}} -{"Key":"9"} -{"Key":"shift-h"} -{"Get":{"state":"1 2 3\n4 5 6\n7 8 ˇ9","mode":"Normal"}} diff --git a/crates/vim/test_data/test_wrapped_delete_end_document.json b/crates/vim/test_data/test_wrapped_delete_end_document.json deleted file mode 100644 index f1dc0cb238..0000000000 --- a/crates/vim/test_data/test_wrapped_delete_end_document.json +++ /dev/null @@ -1,10 +0,0 @@ -{"SetOption":{"value":"wrap"}} -{"SetOption":{"value":"columns=12"}} -{"Put":{"state":"aaˇaaaaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbbbbbb\ncccccccccccccccccccc"}} -{"Key":"d"} -{"Key":"shift-g"} -{"Key":"i"} -{"Key":"z"} -{"Key":"z"} -{"Key":"z"} -{"Get":{"state":"zzzˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_wrapped_lines.json b/crates/vim/test_data/test_wrapped_lines.json deleted file mode 100644 index e5b5d0eac0..0000000000 --- a/crates/vim/test_data/test_wrapped_lines.json +++ /dev/null @@ -1,61 +0,0 @@ -{"SetOption":{"value":"wrap"}} -{"SetOption":{"value":"columns=12"}} -{"Put":{"state":"tˇwelve char twelve char\ntwelve char\n"}} -{"Key":"j"} -{"Get":{"state":"twelve char twelve char\ntˇwelve char\n","mode":"Normal"}} -{"Key":"k"} -{"Get":{"state":"tˇwelve char twelve char\ntwelve char\n","mode":"Normal"}} -{"Key":"g"} -{"Key":"j"} -{"Get":{"state":"twelve char tˇwelve char\ntwelve char\n","mode":"Normal"}} -{"Key":"g"} -{"Key":"j"} -{"Get":{"state":"twelve char twelve char\ntˇwelve char\n","mode":"Normal"}} -{"Key":"g"} -{"Key":"k"} -{"Get":{"state":"twelve char tˇwelve char\ntwelve char\n","mode":"Normal"}} -{"Key":"g"} -{"Key":"^"} -{"Get":{"state":"twelve char ˇtwelve char\ntwelve char\n","mode":"Normal"}} -{"Key":"^"} -{"Get":{"state":"ˇtwelve char twelve char\ntwelve char\n","mode":"Normal"}} -{"Key":"g"} -{"Key":"$"} -{"Get":{"state":"twelve charˇ twelve char\ntwelve char\n","mode":"Normal"}} -{"Key":"$"} -{"Get":{"state":"twelve char twelve chaˇr\ntwelve char\n","mode":"Normal"}} -{"Put":{"state":"tˇwelve char twelve char\ntwelve char\n"}} -{"Key":"enter"} -{"Get":{"state":"twelve char twelve char\nˇtwelve char\n","mode":"Normal"}} -{"Put":{"state":"twelve char\ntˇwelve char twelve char\ntwelve char\n"}} -{"Key":"o"} -{"Key":"o"} -{"Key":"escape"} -{"Get":{"state":"twelve char\ntwelve char twelve char\nˇo\ntwelve char\n","mode":"Normal"}} -{"Put":{"state":"twelve char\ntˇwelve char twelve char\ntwelve char\n"}} -{"Key":"shift-a"} -{"Key":"a"} -{"Key":"escape"} -{"Get":{"state":"twelve char\ntwelve char twelve charˇa\ntwelve char\n","mode":"Normal"}} -{"Key":"shift-i"} -{"Key":"i"} -{"Key":"escape"} -{"Get":{"state":"twelve char\nˇitwelve char twelve chara\ntwelve char\n","mode":"Normal"}} -{"Key":"shift-d"} -{"Get":{"state":"twelve char\nˇ\ntwelve char\n","mode":"Normal"}} -{"Put":{"state":"twelve char\ntwelve char tˇwelve char\ntwelve char\n"}} -{"Key":"shift-o"} -{"Key":"o"} -{"Key":"escape"} -{"Get":{"state":"twelve char\nˇo\ntwelve char twelve char\ntwelve char\n","mode":"Normal"}} -{"Put":{"state":"fourteen chaˇr\nfourteen char\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"fourteenˇ \nfourteen char\n","mode":"Normal"}} -{"Key":"j"} -{"Key":"shift-f"} -{"Key":"e"} -{"Key":"f"} -{"Key":"r"} -{"Get":{"state":"fourteen \nfourteen chaˇr\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_wrapped_motions.json b/crates/vim/test_data/test_wrapped_motions.json deleted file mode 100644 index 195a58f6b5..0000000000 --- a/crates/vim/test_data/test_wrapped_motions.json +++ /dev/null @@ -1,15 +0,0 @@ -{"SetOption":{"value":"wrap"}} -{"SetOption":{"value":"columns=12"}} -{"Put":{"state":"aaˇaa\n😃😃"}} -{"Key":"j"} -{"Get":{"state":"aaaa\n😃ˇ😃","mode":"Normal"}} -{"Put":{"state":"123456789012aaˇaa\n123456789012😃😃"}} -{"Key":"j"} -{"Get":{"state":"123456789012aaaa\n123456789012😃ˇ😃","mode":"Normal"}} -{"Put":{"state":"123456789012aaˇaa\n123456789012😃😃"}} -{"Key":"j"} -{"Get":{"state":"123456789012aaaa\n123456789012😃ˇ😃","mode":"Normal"}} -{"Put":{"state":"123456789012aaaaˇaaaaaaaa123456789012\nwow\n123456789012😃😃😃😃😃😃123456789012"}} -{"Key":"j"} -{"Key":"j"} -{"Get":{"state":"123456789012aaaaaaaaaaaa123456789012\nwow\n123456789012😃😃ˇ😃😃😃😃123456789012","mode":"Normal"}} diff --git a/crates/vim/test_data/test_x.json b/crates/vim/test_data/test_x.json deleted file mode 100644 index cb7eb53472..0000000000 --- a/crates/vim/test_data/test_x.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"ˇTest"}} -{"Key":"x"} -{"Get":{"state":"ˇest","mode":"Normal"}} -{"Put":{"state":"Teˇst"}} -{"Key":"x"} -{"Get":{"state":"Teˇt","mode":"Normal"}} -{"Put":{"state":"Tesˇt"}} -{"Key":"x"} -{"Get":{"state":"Teˇs","mode":"Normal"}} -{"Put":{"state":"Tesˇt\ntest"}} -{"Key":"x"} -{"Get":{"state":"Teˇs\ntest","mode":"Normal"}} diff --git a/crates/vim/test_data/test_yank_line_with_trailing_newline.json b/crates/vim/test_data/test_yank_line_with_trailing_newline.json deleted file mode 100644 index 8b4438737a..0000000000 --- a/crates/vim/test_data/test_yank_line_with_trailing_newline.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":"heˇllo\n"}} -{"Key":"y"} -{"Key":"y"} -{"Key":"p"} -{"Get":{"state":"hello\nˇhello\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_yank_line_without_trailing_newline.json b/crates/vim/test_data/test_yank_line_without_trailing_newline.json deleted file mode 100644 index a1158ff2d5..0000000000 --- a/crates/vim/test_data/test_yank_line_without_trailing_newline.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":"heˇllo"}} -{"Key":"y"} -{"Key":"y"} -{"Key":"p"} -{"Get":{"state":"hello\nˇhello","mode":"Normal"}} diff --git a/crates/vim/test_data/test_yank_multiline_without_trailing_newline.json b/crates/vim/test_data/test_yank_multiline_without_trailing_newline.json deleted file mode 100644 index ec38e81f2e..0000000000 --- a/crates/vim/test_data/test_yank_multiline_without_trailing_newline.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"heˇllo\nhello"}} -{"Key":"2"} -{"Key":"y"} -{"Key":"y"} -{"Key":"p"} -{"Get":{"state":"hello\nˇhello\nhello\nhello","mode":"Normal"}} diff --git a/crates/vim/test_data/test_yank_paragraph_with_paste.json b/crates/vim/test_data/test_yank_paragraph_with_paste.json deleted file mode 100644 index d73d1f6d3b..0000000000 --- a/crates/vim/test_data/test_yank_paragraph_with_paste.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"first paragraph\nˇstill first\n\nsecond paragraph\nstill second\n\nthird paragraph\n"}} -{"Key":"y"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇfirst paragraph\nstill first\n\nsecond paragraph\nstill second\n\nthird paragraph\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"first paragraph\nstill first\n\n"}} -{"Key":"j"} -{"Key":"j"} -{"Key":"p"} -{"Get":{"state":"first paragraph\nstill first\n\nˇfirst paragraph\nstill first\n\nsecond paragraph\nstill second\n\nthird paragraph\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_zero.json b/crates/vim/test_data/test_zero.json deleted file mode 100644 index bc1253deb5..0000000000 --- a/crates/vim/test_data/test_zero.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"0"} -{"Get":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"1"} -{"Key":"0"} -{"Key":"l"} -{"Get":{"state":"The quick ˇbrown\nfox jumps over\nthe lazy dog","mode":"Normal"}} diff --git a/crates/vim_mode_setting/Cargo.toml b/crates/vim_mode_setting/Cargo.toml deleted file mode 100644 index 0ae75d9d55..0000000000 --- a/crates/vim_mode_setting/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "vim_mode_setting" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/vim_mode_setting.rs" - -[dependencies] -settings.workspace = true diff --git a/crates/vim_mode_setting/LICENSE-GPL b/crates/vim_mode_setting/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/vim_mode_setting/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/vim_mode_setting/src/vim_mode_setting.rs b/crates/vim_mode_setting/src/vim_mode_setting.rs deleted file mode 100644 index e229913a80..0000000000 --- a/crates/vim_mode_setting/src/vim_mode_setting.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Contains the [`VimModeSetting`] and [`HelixModeSetting`] used to enable/disable Vim and Helix modes. -//! -//! This is in its own crate as we want other crates to be able to enable or -//! disable Vim/Helix modes without having to depend on the `vim` crate in its -//! entirety. - -use settings::{RegisterSetting, Settings, SettingsContent}; - -#[derive(RegisterSetting)] -pub struct VimModeSetting(pub bool); - -impl Settings for VimModeSetting { - fn from_settings(content: &SettingsContent) -> Self { - Self(content.vim_mode.unwrap()) - } -} - -#[derive(RegisterSetting)] -pub struct HelixModeSetting(pub bool); - -impl Settings for HelixModeSetting { - fn from_settings(content: &SettingsContent) -> Self { - Self(content.helix_mode.unwrap()) - } -} diff --git a/crates/watch/Cargo.toml b/crates/watch/Cargo.toml index 9d77eaedde..8f61d1f031 100644 --- a/crates/watch/Cargo.toml +++ b/crates/watch/Cargo.toml @@ -17,7 +17,8 @@ parking_lot.workspace = true [dev-dependencies] ctor.workspace = true +env_logger.workspace = true futures.workspace = true gpui = { workspace = true, features = ["test-support"] } +log.workspace = true rand.workspace = true -zlog.workspace = true diff --git a/crates/watch/src/watch.rs b/crates/watch/src/watch.rs index 71dab74820..aee602db86 100644 --- a/crates/watch/src/watch.rs +++ b/crates/watch/src/watch.rs @@ -238,7 +238,7 @@ mod tests { for _ in 0..16 { executor.simulate_random_delay().await; let id = next_id.fetch_add(1, SeqCst); - zlog::info!("sending {}", id); + log::info!("sending {}", id); tx.send(id).ok(); } closed.store(true, SeqCst); @@ -255,23 +255,23 @@ mod tests { for _ in 0..16 { executor.simulate_random_delay().await; - zlog::info!("{}: receiving", receiver_id); + log::info!("{}: receiving", receiver_id); let mut timeout = executor.simulate_random_delay().fuse(); let mut recv = pin!(rx.recv().fuse()); select_biased! { _ = timeout => { - zlog::info!("{}: dropping recv future", receiver_id); + log::info!("{}: dropping recv future", receiver_id); } result = recv => { match result { Ok(value) => { - zlog::info!("{}: received {}", receiver_id, value); + log::info!("{}: received {}", receiver_id, value); assert_eq!(value, next_id.load(SeqCst) - 1); assert_ne!(value, prev_observed_value); prev_observed_value = value; } Err(NoSenderError) => { - zlog::info!("{}: closed", receiver_id); + log::info!("{}: closed", receiver_id); assert!(closed.load(SeqCst)); break; } @@ -287,6 +287,6 @@ mod tests { #[ctor::ctor] fn init_logger() { - zlog::init_test(); + let _ = env_logger::try_init(); } } diff --git a/crates/web_search/Cargo.toml b/crates/web_search/Cargo.toml deleted file mode 100644 index d0e32e71f0..0000000000 --- a/crates/web_search/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "web_search" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/web_search.rs" - -[dependencies] -anyhow.workspace = true -cloud_llm_client.workspace = true -collections.workspace = true -gpui.workspace = true -serde.workspace = true diff --git a/crates/web_search/LICENSE-GPL b/crates/web_search/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/web_search/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/web_search/src/web_search.rs b/crates/web_search/src/web_search.rs deleted file mode 100644 index c381b91f39..0000000000 --- a/crates/web_search/src/web_search.rs +++ /dev/null @@ -1,72 +0,0 @@ -use std::sync::Arc; - -use anyhow::Result; -use cloud_llm_client::WebSearchResponse; -use collections::HashMap; -use gpui::{App, AppContext as _, Context, Entity, Global, SharedString, Task}; - -pub fn init(cx: &mut App) { - let registry = cx.new(|_cx| WebSearchRegistry::default()); - cx.set_global(GlobalWebSearchRegistry(registry)); -} - -#[derive(Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)] -pub struct WebSearchProviderId(pub SharedString); - -pub trait WebSearchProvider { - fn id(&self) -> WebSearchProviderId; - fn search(&self, query: String, cx: &mut App) -> Task>; -} - -struct GlobalWebSearchRegistry(Entity); - -impl Global for GlobalWebSearchRegistry {} - -#[derive(Default)] -pub struct WebSearchRegistry { - providers: HashMap>, - active_provider: Option>, -} - -impl WebSearchRegistry { - pub fn global(cx: &App) -> Entity { - cx.global::().0.clone() - } - - pub fn read_global(cx: &App) -> &Self { - cx.global::().0.read(cx) - } - - pub fn providers(&self) -> impl Iterator> { - self.providers.values() - } - - pub fn active_provider(&self) -> Option> { - self.active_provider.clone() - } - - pub fn set_active_provider(&mut self, provider: Arc) { - self.active_provider = Some(provider.clone()); - self.providers.insert(provider.id(), provider); - } - - pub fn register_provider( - &mut self, - provider: T, - _cx: &mut Context, - ) { - let id = provider.id(); - let provider = Arc::new(provider); - self.providers.insert(id, provider.clone()); - if self.active_provider.is_none() { - self.active_provider = Some(provider); - } - } - - pub fn unregister_provider(&mut self, id: WebSearchProviderId) { - self.providers.remove(&id); - if self.active_provider.as_ref().map(|provider| provider.id()) == Some(id) { - self.active_provider = None; - } - } -} diff --git a/crates/web_search_providers/Cargo.toml b/crates/web_search_providers/Cargo.toml deleted file mode 100644 index ecdca5883f..0000000000 --- a/crates/web_search_providers/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "web_search_providers" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/web_search_providers.rs" - -[dependencies] -anyhow.workspace = true -client.workspace = true -cloud_llm_client.workspace = true -futures.workspace = true -gpui.workspace = true -http_client.workspace = true -language_model.workspace = true -serde.workspace = true -serde_json.workspace = true -web_search.workspace = true diff --git a/crates/web_search_providers/LICENSE-GPL b/crates/web_search_providers/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/web_search_providers/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/web_search_providers/src/cloud.rs b/crates/web_search_providers/src/cloud.rs deleted file mode 100644 index 75ffb1da63..0000000000 --- a/crates/web_search_providers/src/cloud.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::sync::Arc; - -use anyhow::{Context as _, Result}; -use client::Client; -use cloud_llm_client::{EXPIRED_LLM_TOKEN_HEADER_NAME, WebSearchBody, WebSearchResponse}; -use futures::AsyncReadExt as _; -use gpui::{App, AppContext, Context, Entity, Subscription, Task}; -use http_client::{HttpClient, Method}; -use language_model::{LlmApiToken, RefreshLlmTokenListener}; -use web_search::{WebSearchProvider, WebSearchProviderId}; - -pub struct CloudWebSearchProvider { - state: Entity, -} - -impl CloudWebSearchProvider { - pub fn new(client: Arc, cx: &mut App) -> Self { - let state = cx.new(|cx| State::new(client, cx)); - - Self { state } - } -} - -pub struct State { - client: Arc, - llm_api_token: LlmApiToken, - _llm_token_subscription: Subscription, -} - -impl State { - pub fn new(client: Arc, cx: &mut Context) -> Self { - let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx); - - Self { - client, - llm_api_token: LlmApiToken::default(), - _llm_token_subscription: cx.subscribe( - &refresh_llm_token_listener, - |this, _, _event, cx| { - let client = this.client.clone(); - let llm_api_token = this.llm_api_token.clone(); - cx.spawn(async move |_this, _cx| { - llm_api_token.refresh(&client).await?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - }, - ), - } - } -} - -pub const ZED_WEB_SEARCH_PROVIDER_ID: &str = "zed.dev"; - -impl WebSearchProvider for CloudWebSearchProvider { - fn id(&self) -> WebSearchProviderId { - WebSearchProviderId(ZED_WEB_SEARCH_PROVIDER_ID.into()) - } - - fn search(&self, query: String, cx: &mut App) -> Task> { - let state = self.state.read(cx); - let client = state.client.clone(); - let llm_api_token = state.llm_api_token.clone(); - let body = WebSearchBody { query }; - cx.background_spawn(async move { perform_web_search(client, llm_api_token, body).await }) - } -} - -async fn perform_web_search( - client: Arc, - llm_api_token: LlmApiToken, - body: WebSearchBody, -) -> Result { - const MAX_RETRIES: usize = 3; - - let http_client = &client.http_client(); - let mut retries_remaining = MAX_RETRIES; - let mut token = llm_api_token.acquire(&client).await?; - - loop { - if retries_remaining == 0 { - return Err(anyhow::anyhow!( - "error performing web search, max retries exceeded" - )); - } - - let request = http_client::Request::builder() - .method(Method::POST) - .uri(http_client.build_zed_llm_url("/web_search", &[])?.as_ref()) - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {token}")) - .body(serde_json::to_string(&body)?.into())?; - let mut response = http_client - .send(request) - .await - .context("failed to send web search request")?; - - if response.status().is_success() { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - return Ok(serde_json::from_str(&body)?); - } else if response - .headers() - .get(EXPIRED_LLM_TOKEN_HEADER_NAME) - .is_some() - { - token = llm_api_token.refresh(&client).await?; - retries_remaining -= 1; - } else { - // For now we will only retry if the LLM token is expired, - // not if the request failed for any other reason. - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - anyhow::bail!( - "error performing web search.\nStatus: {:?}\nBody: {body}", - response.status(), - ); - } - } -} diff --git a/crates/web_search_providers/src/web_search_providers.rs b/crates/web_search_providers/src/web_search_providers.rs deleted file mode 100644 index 8ab0aee47a..0000000000 --- a/crates/web_search_providers/src/web_search_providers.rs +++ /dev/null @@ -1,56 +0,0 @@ -mod cloud; - -use client::Client; -use gpui::{App, Context, Entity}; -use language_model::LanguageModelRegistry; -use std::sync::Arc; -use web_search::{WebSearchProviderId, WebSearchRegistry}; - -pub fn init(client: Arc, cx: &mut App) { - let registry = WebSearchRegistry::global(cx); - registry.update(cx, |registry, cx| { - register_web_search_providers(registry, client, cx); - }); -} - -fn register_web_search_providers( - registry: &mut WebSearchRegistry, - client: Arc, - cx: &mut Context, -) { - register_zed_web_search_provider( - registry, - client.clone(), - &LanguageModelRegistry::global(cx), - cx, - ); - - cx.subscribe( - &LanguageModelRegistry::global(cx), - move |this, registry, event, cx| { - if let language_model::Event::DefaultModelChanged = event { - register_zed_web_search_provider(this, client.clone(), ®istry, cx) - } - }, - ) - .detach(); -} - -fn register_zed_web_search_provider( - registry: &mut WebSearchRegistry, - client: Arc, - language_model_registry: &Entity, - cx: &mut Context, -) { - let using_zed_provider = language_model_registry - .read(cx) - .default_model() - .is_some_and(|default| default.is_provided_by_zed()); - if using_zed_provider { - registry.register_provider(cloud::CloudWebSearchProvider::new(client, cx), cx) - } else { - registry.unregister_provider(WebSearchProviderId( - cloud::ZED_WEB_SEARCH_PROVIDER_ID.into(), - )); - } -} diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml deleted file mode 100644 index d5d3016ab2..0000000000 --- a/crates/workspace/Cargo.toml +++ /dev/null @@ -1,83 +0,0 @@ -[package] -name = "workspace" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/workspace.rs" -doctest = false - -[features] -test-support = [ - "call/test-support", - "client/test-support", - "http_client/test-support", - "db/test-support", - "project/test-support", - "session/test-support", - "settings/test-support", - "gpui/test-support", - "fs/test-support", -] - -[dependencies] -any_vec.workspace = true -anyhow.workspace = true -async-recursion.workspace = true -call.workspace = true -client.workspace = true -clock.workspace = true -collections.workspace = true -component.workspace = true -db.workspace = true -fs.workspace = true -futures.workspace = true -gpui.workspace = true -http_client.workspace = true -itertools.workspace = true -language.workspace = true -log.workspace = true -menu.workspace = true -node_runtime.workspace = true -parking_lot.workspace = true -postage.workspace = true -project.workspace = true -remote.workspace = true -schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -session.workspace = true -settings.workspace = true -smallvec.workspace = true -sqlez.workspace = true -strum.workspace = true -task.workspace = true -telemetry.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -uuid.workspace = true -zed_actions.workspace = true - -[target.'cfg(target_os = "windows")'.dependencies] -windows.workspace = true - -[dev-dependencies] -call = { workspace = true, features = ["test-support"] } -client = { workspace = true, features = ["test-support"] } -dap = { workspace = true, features = ["test-support"] } -db = { workspace = true, features = ["test-support"] } -fs = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } -session = { workspace = true, features = ["test-support"] } -settings = { workspace = true, features = ["test-support"] } -http_client = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true -tempfile.workspace = true -zlog.workspace = true diff --git a/crates/workspace/LICENSE-GPL b/crates/workspace/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/workspace/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs deleted file mode 100644 index dfc341db9c..0000000000 --- a/crates/workspace/src/dock.rs +++ /dev/null @@ -1,1095 +0,0 @@ -use crate::persistence::model::DockData; -use crate::{DraggedDock, Event, ModalLayer, Pane}; -use crate::{Workspace, status_bar::StatusItemView}; -use anyhow::Context as _; -use client::proto; -use gpui::{ - Action, AnyView, App, Axis, Context, Corner, Entity, EntityId, EventEmitter, FocusHandle, - Focusable, IntoElement, KeyContext, MouseButton, MouseDownEvent, MouseUpEvent, ParentElement, - Render, SharedString, StyleRefinement, Styled, Subscription, WeakEntity, Window, deferred, div, - px, -}; -use settings::SettingsStore; -use std::sync::Arc; -use ui::{ContextMenu, Divider, DividerColor, IconButton, Tooltip, h_flex}; -use ui::{prelude::*, right_click_menu}; - -pub(crate) const RESIZE_HANDLE_SIZE: Pixels = px(6.); - -pub enum PanelEvent { - ZoomIn, - ZoomOut, - Activate, - Close, -} - -pub use proto::PanelId; - -pub trait Panel: Focusable + EventEmitter + Render + Sized { - fn persistent_name() -> &'static str; - fn panel_key() -> &'static str; - fn position(&self, window: &Window, cx: &App) -> DockPosition; - fn position_is_valid(&self, position: DockPosition) -> bool; - fn set_position(&mut self, position: DockPosition, window: &mut Window, cx: &mut Context); - fn size(&self, window: &Window, cx: &App) -> Pixels; - fn set_size(&mut self, size: Option, window: &mut Window, cx: &mut Context); - fn icon(&self, window: &Window, cx: &App) -> Option; - fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str>; - fn toggle_action(&self) -> Box; - fn icon_label(&self, _window: &Window, _: &App) -> Option { - None - } - fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool { - false - } - fn starts_open(&self, _window: &Window, _cx: &App) -> bool { - false - } - fn set_zoomed(&mut self, _zoomed: bool, _window: &mut Window, _cx: &mut Context) {} - fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context) {} - fn pane(&self) -> Option> { - None - } - fn remote_id() -> Option { - None - } - fn activation_priority(&self) -> u32; - fn enabled(&self, _cx: &App) -> bool { - true - } -} - -pub trait PanelHandle: Send + Sync { - fn panel_id(&self) -> EntityId; - fn persistent_name(&self) -> &'static str; - fn panel_key(&self) -> &'static str; - fn position(&self, window: &Window, cx: &App) -> DockPosition; - fn position_is_valid(&self, position: DockPosition, cx: &App) -> bool; - fn set_position(&self, position: DockPosition, window: &mut Window, cx: &mut App); - fn is_zoomed(&self, window: &Window, cx: &App) -> bool; - fn set_zoomed(&self, zoomed: bool, window: &mut Window, cx: &mut App); - fn set_active(&self, active: bool, window: &mut Window, cx: &mut App); - fn remote_id(&self) -> Option; - fn pane(&self, cx: &App) -> Option>; - fn size(&self, window: &Window, cx: &App) -> Pixels; - fn set_size(&self, size: Option, window: &mut Window, cx: &mut App); - fn icon(&self, window: &Window, cx: &App) -> Option; - fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str>; - fn toggle_action(&self, window: &Window, cx: &App) -> Box; - fn icon_label(&self, window: &Window, cx: &App) -> Option; - fn panel_focus_handle(&self, cx: &App) -> FocusHandle; - fn to_any(&self) -> AnyView; - fn activation_priority(&self, cx: &App) -> u32; - fn enabled(&self, cx: &App) -> bool; - fn move_to_next_position(&self, window: &mut Window, cx: &mut App) { - let current_position = self.position(window, cx); - let next_position = [ - DockPosition::Left, - DockPosition::Bottom, - DockPosition::Right, - ] - .into_iter() - .filter(|position| self.position_is_valid(*position, cx)) - .skip_while(|valid_position| *valid_position != current_position) - .nth(1) - .unwrap_or(DockPosition::Left); - - self.set_position(next_position, window, cx); - } -} - -impl PanelHandle for Entity -where - T: Panel, -{ - fn panel_id(&self) -> EntityId { - Entity::entity_id(self) - } - - fn persistent_name(&self) -> &'static str { - T::persistent_name() - } - - fn panel_key(&self) -> &'static str { - T::panel_key() - } - - fn position(&self, window: &Window, cx: &App) -> DockPosition { - self.read(cx).position(window, cx) - } - - fn position_is_valid(&self, position: DockPosition, cx: &App) -> bool { - self.read(cx).position_is_valid(position) - } - - fn set_position(&self, position: DockPosition, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| this.set_position(position, window, cx)) - } - - fn is_zoomed(&self, window: &Window, cx: &App) -> bool { - self.read(cx).is_zoomed(window, cx) - } - - fn set_zoomed(&self, zoomed: bool, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| this.set_zoomed(zoomed, window, cx)) - } - - fn set_active(&self, active: bool, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| this.set_active(active, window, cx)) - } - - fn pane(&self, cx: &App) -> Option> { - self.read(cx).pane() - } - - fn remote_id(&self) -> Option { - T::remote_id() - } - - fn size(&self, window: &Window, cx: &App) -> Pixels { - self.read(cx).size(window, cx) - } - - fn set_size(&self, size: Option, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| this.set_size(size, window, cx)) - } - - fn icon(&self, window: &Window, cx: &App) -> Option { - self.read(cx).icon(window, cx) - } - - fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str> { - self.read(cx).icon_tooltip(window, cx) - } - - fn toggle_action(&self, _: &Window, cx: &App) -> Box { - self.read(cx).toggle_action() - } - - fn icon_label(&self, window: &Window, cx: &App) -> Option { - self.read(cx).icon_label(window, cx) - } - - fn to_any(&self) -> AnyView { - self.clone().into() - } - - fn panel_focus_handle(&self, cx: &App) -> FocusHandle { - self.read(cx).focus_handle(cx) - } - - fn activation_priority(&self, cx: &App) -> u32 { - self.read(cx).activation_priority() - } - - fn enabled(&self, cx: &App) -> bool { - self.read(cx).enabled(cx) - } -} - -impl From<&dyn PanelHandle> for AnyView { - fn from(val: &dyn PanelHandle) -> Self { - val.to_any() - } -} - -/// A container with a fixed [`DockPosition`] adjacent to a certain widown edge. -/// Can contain multiple panels and show/hide itself with all contents. -pub struct Dock { - position: DockPosition, - panel_entries: Vec, - workspace: WeakEntity, - is_open: bool, - active_panel_index: Option, - focus_handle: FocusHandle, - pub(crate) serialized_dock: Option, - zoom_layer_open: bool, - modal_layer: Entity, - _subscriptions: [Subscription; 2], -} - -impl Focusable for Dock { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum DockPosition { - Left, - Bottom, - Right, -} - -impl From for DockPosition { - fn from(value: settings::DockPosition) -> Self { - match value { - settings::DockPosition::Left => Self::Left, - settings::DockPosition::Bottom => Self::Bottom, - settings::DockPosition::Right => Self::Right, - } - } -} - -impl Into for DockPosition { - fn into(self) -> settings::DockPosition { - match self { - Self::Left => settings::DockPosition::Left, - Self::Bottom => settings::DockPosition::Bottom, - Self::Right => settings::DockPosition::Right, - } - } -} - -impl DockPosition { - fn label(&self) -> &'static str { - match self { - Self::Left => "Left", - Self::Bottom => "Bottom", - Self::Right => "Right", - } - } - - pub fn axis(&self) -> Axis { - match self { - Self::Left | Self::Right => Axis::Horizontal, - Self::Bottom => Axis::Vertical, - } - } -} - -struct PanelEntry { - panel: Arc, - _subscriptions: [Subscription; 3], -} - -pub struct PanelButtons { - dock: Entity, - _settings_subscription: Subscription, -} - -impl Dock { - pub fn new( - position: DockPosition, - modal_layer: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let focus_handle = cx.focus_handle(); - let workspace = cx.entity(); - let dock = cx.new(|cx| { - let focus_subscription = - cx.on_focus(&focus_handle, window, |dock: &mut Dock, window, cx| { - if let Some(active_entry) = dock.active_panel_entry() { - active_entry.panel.panel_focus_handle(cx).focus(window) - } - }); - let zoom_subscription = cx.subscribe(&workspace, |dock, workspace, e: &Event, cx| { - if matches!(e, Event::ZoomChanged) { - let is_zoomed = workspace.read(cx).zoomed.is_some(); - dock.zoom_layer_open = is_zoomed; - } - }); - Self { - position, - workspace: workspace.downgrade(), - panel_entries: Default::default(), - active_panel_index: None, - is_open: false, - focus_handle: focus_handle.clone(), - _subscriptions: [focus_subscription, zoom_subscription], - serialized_dock: None, - zoom_layer_open: false, - modal_layer, - } - }); - - cx.on_focus_in(&focus_handle, window, { - let dock = dock.downgrade(); - move |workspace, window, cx| { - let Some(dock) = dock.upgrade() else { - return; - }; - let Some(panel) = dock.read(cx).active_panel() else { - return; - }; - if panel.is_zoomed(window, cx) { - workspace.zoomed = Some(panel.to_any().downgrade()); - workspace.zoomed_position = Some(position); - } else { - workspace.zoomed = None; - workspace.zoomed_position = None; - } - cx.emit(Event::ZoomChanged); - workspace.dismiss_zoomed_items_to_reveal(Some(position), window, cx); - workspace.update_active_view_for_followers(window, cx) - } - }) - .detach(); - - cx.observe_in(&dock, window, move |workspace, dock, window, cx| { - if dock.read(cx).is_open() - && let Some(panel) = dock.read(cx).active_panel() - && panel.is_zoomed(window, cx) - { - workspace.zoomed = Some(panel.to_any().downgrade()); - workspace.zoomed_position = Some(position); - cx.emit(Event::ZoomChanged); - return; - } - if workspace.zoomed_position == Some(position) { - workspace.zoomed = None; - workspace.zoomed_position = None; - cx.emit(Event::ZoomChanged); - } - }) - .detach(); - - dock - } - - pub fn position(&self) -> DockPosition { - self.position - } - - pub fn is_open(&self) -> bool { - self.is_open - } - - fn resizable(&self, cx: &App) -> bool { - !(self.zoom_layer_open || self.modal_layer.read(cx).has_active_modal()) - } - - pub fn panel(&self) -> Option> { - self.panel_entries - .iter() - .find_map(|entry| entry.panel.to_any().downcast().ok()) - } - - pub fn panel_index_for_type(&self) -> Option { - self.panel_entries - .iter() - .position(|entry| entry.panel.to_any().downcast::().is_ok()) - } - - pub fn panel_index_for_persistent_name(&self, ui_name: &str, _cx: &App) -> Option { - self.panel_entries - .iter() - .position(|entry| entry.panel.persistent_name() == ui_name) - } - - pub fn panel_index_for_proto_id(&self, panel_id: PanelId) -> Option { - self.panel_entries - .iter() - .position(|entry| entry.panel.remote_id() == Some(panel_id)) - } - - pub fn first_enabled_panel_idx(&mut self, cx: &mut Context) -> anyhow::Result { - self.panel_entries - .iter() - .position(|entry| entry.panel.enabled(cx)) - .with_context(|| { - format!( - "Couldn't find any enabled panel for the {} dock.", - self.position.label() - ) - }) - } - - fn active_panel_entry(&self) -> Option<&PanelEntry> { - self.active_panel_index - .and_then(|index| self.panel_entries.get(index)) - } - - pub fn active_panel_index(&self) -> Option { - self.active_panel_index - } - - pub fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context) { - if open != self.is_open { - self.is_open = open; - if let Some(active_panel) = self.active_panel_entry() { - active_panel.panel.set_active(open, window, cx); - } - - cx.notify(); - } - } - - pub fn set_panel_zoomed( - &mut self, - panel: &AnyView, - zoomed: bool, - window: &mut Window, - cx: &mut Context, - ) { - for entry in &mut self.panel_entries { - if entry.panel.panel_id() == panel.entity_id() { - if zoomed != entry.panel.is_zoomed(window, cx) { - entry.panel.set_zoomed(zoomed, window, cx); - } - } else if entry.panel.is_zoomed(window, cx) { - entry.panel.set_zoomed(false, window, cx); - } - } - - self.workspace - .update(cx, |workspace, cx| { - workspace.serialize_workspace(window, cx); - }) - .ok(); - cx.notify(); - } - - pub fn zoom_out(&mut self, window: &mut Window, cx: &mut Context) { - for entry in &mut self.panel_entries { - if entry.panel.is_zoomed(window, cx) { - entry.panel.set_zoomed(false, window, cx); - } - } - } - - pub(crate) fn add_panel( - &mut self, - panel: Entity, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> usize { - let subscriptions = [ - cx.observe(&panel, |_, _, cx| cx.notify()), - cx.observe_global_in::(window, { - let workspace = workspace.clone(); - let panel = panel.clone(); - - move |this, window, cx| { - let new_position = panel.read(cx).position(window, cx); - if new_position == this.position { - return; - } - - let Ok(new_dock) = workspace.update(cx, |workspace, cx| { - if panel.is_zoomed(window, cx) { - workspace.zoomed_position = Some(new_position); - } - match new_position { - DockPosition::Left => &workspace.left_dock, - DockPosition::Bottom => &workspace.bottom_dock, - DockPosition::Right => &workspace.right_dock, - } - .clone() - }) else { - return; - }; - - let was_visible = this.is_open() - && this.visible_panel().is_some_and(|active_panel| { - active_panel.panel_id() == Entity::entity_id(&panel) - }); - - this.remove_panel(&panel, window, cx); - - new_dock.update(cx, |new_dock, cx| { - new_dock.remove_panel(&panel, window, cx); - let index = - new_dock.add_panel(panel.clone(), workspace.clone(), window, cx); - if was_visible { - new_dock.set_open(true, window, cx); - new_dock.activate_panel(index, window, cx); - } - }); - } - }), - cx.subscribe_in( - &panel, - window, - move |this, panel, event, window, cx| match event { - PanelEvent::ZoomIn => { - this.set_panel_zoomed(&panel.to_any(), true, window, cx); - if !PanelHandle::panel_focus_handle(panel, cx).contains_focused(window, cx) - { - window.focus(&panel.focus_handle(cx)); - } - workspace - .update(cx, |workspace, cx| { - workspace.zoomed = Some(panel.downgrade().into()); - workspace.zoomed_position = - Some(panel.read(cx).position(window, cx)); - cx.emit(Event::ZoomChanged); - }) - .ok(); - } - PanelEvent::ZoomOut => { - this.set_panel_zoomed(&panel.to_any(), false, window, cx); - workspace - .update(cx, |workspace, cx| { - if workspace.zoomed_position == Some(this.position) { - workspace.zoomed = None; - workspace.zoomed_position = None; - cx.emit(Event::ZoomChanged); - } - cx.notify(); - }) - .ok(); - } - PanelEvent::Activate => { - if let Some(ix) = this - .panel_entries - .iter() - .position(|entry| entry.panel.panel_id() == Entity::entity_id(panel)) - { - this.set_open(true, window, cx); - this.activate_panel(ix, window, cx); - window.focus(&panel.read(cx).focus_handle(cx)); - } - } - PanelEvent::Close => { - if this - .visible_panel() - .is_some_and(|p| p.panel_id() == Entity::entity_id(panel)) - { - this.set_open(false, window, cx); - } - } - }, - ), - ]; - - let index = match self - .panel_entries - .binary_search_by_key(&panel.read(cx).activation_priority(), |entry| { - entry.panel.activation_priority(cx) - }) { - Ok(ix) => { - if cfg!(debug_assertions) { - panic!( - "Panels `{}` and `{}` have the same activation priority. Each panel must have a unique priority so the status bar order is deterministic.", - T::panel_key(), - self.panel_entries[ix].panel.panel_key() - ); - } - ix - } - Err(ix) => ix, - }; - if let Some(active_index) = self.active_panel_index.as_mut() - && *active_index >= index - { - *active_index += 1; - } - self.panel_entries.insert( - index, - PanelEntry { - panel: Arc::new(panel.clone()), - _subscriptions: subscriptions, - }, - ); - - self.restore_state(window, cx); - if panel.read(cx).starts_open(window, cx) { - self.activate_panel(index, window, cx); - self.set_open(true, window, cx); - } - - cx.notify(); - index - } - - pub fn restore_state(&mut self, window: &mut Window, cx: &mut Context) -> bool { - if let Some(serialized) = self.serialized_dock.clone() { - if let Some(active_panel) = serialized.active_panel.filter(|_| serialized.visible) - && let Some(idx) = self.panel_index_for_persistent_name(active_panel.as_str(), cx) - { - self.activate_panel(idx, window, cx); - } - - if serialized.zoom - && let Some(panel) = self.active_panel() - { - panel.set_zoomed(true, window, cx) - } - self.set_open(serialized.visible, window, cx); - return true; - } - false - } - - pub fn remove_panel( - &mut self, - panel: &Entity, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(panel_ix) = self - .panel_entries - .iter() - .position(|entry| entry.panel.panel_id() == Entity::entity_id(panel)) - { - if let Some(active_panel_index) = self.active_panel_index.as_mut() { - match panel_ix.cmp(active_panel_index) { - std::cmp::Ordering::Less => { - *active_panel_index -= 1; - } - std::cmp::Ordering::Equal => { - self.active_panel_index = None; - self.set_open(false, window, cx); - } - std::cmp::Ordering::Greater => {} - } - } - self.panel_entries.remove(panel_ix); - cx.notify(); - } - } - - pub fn panels_len(&self) -> usize { - self.panel_entries.len() - } - - pub fn activate_panel(&mut self, panel_ix: usize, window: &mut Window, cx: &mut Context) { - if Some(panel_ix) != self.active_panel_index { - if let Some(active_panel) = self.active_panel_entry() { - active_panel.panel.set_active(false, window, cx); - } - - self.active_panel_index = Some(panel_ix); - if let Some(active_panel) = self.active_panel_entry() { - active_panel.panel.set_active(true, window, cx); - } - - cx.notify(); - } - } - - pub fn visible_panel(&self) -> Option<&Arc> { - let entry = self.visible_entry()?; - Some(&entry.panel) - } - - pub fn active_panel(&self) -> Option<&Arc> { - let panel_entry = self.active_panel_entry()?; - Some(&panel_entry.panel) - } - - fn visible_entry(&self) -> Option<&PanelEntry> { - if self.is_open { - self.active_panel_entry() - } else { - None - } - } - - pub fn zoomed_panel(&self, window: &Window, cx: &App) -> Option> { - let entry = self.visible_entry()?; - if entry.panel.is_zoomed(window, cx) { - Some(entry.panel.clone()) - } else { - None - } - } - - pub fn panel_size(&self, panel: &dyn PanelHandle, window: &Window, cx: &App) -> Option { - self.panel_entries - .iter() - .find(|entry| entry.panel.panel_id() == panel.panel_id()) - .map(|entry| entry.panel.size(window, cx)) - } - - pub fn active_panel_size(&self, window: &Window, cx: &App) -> Option { - if self.is_open { - self.active_panel_entry() - .map(|entry| entry.panel.size(window, cx)) - } else { - None - } - } - - pub fn resize_active_panel( - &mut self, - size: Option, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(entry) = self.active_panel_entry() { - let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round()); - - entry.panel.set_size(size, window, cx); - cx.notify(); - } - } - - pub fn resize_all_panels( - &mut self, - size: Option, - window: &mut Window, - cx: &mut Context, - ) { - for entry in &mut self.panel_entries { - let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round()); - entry.panel.set_size(size, window, cx); - } - cx.notify(); - } - - pub fn toggle_action(&self) -> Box { - match self.position { - DockPosition::Left => crate::ToggleLeftDock.boxed_clone(), - DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(), - DockPosition::Right => crate::ToggleRightDock.boxed_clone(), - } - } - - fn dispatch_context() -> KeyContext { - let mut dispatch_context = KeyContext::new_with_defaults(); - dispatch_context.add("Dock"); - - dispatch_context - } - - pub fn clamp_panel_size(&mut self, max_size: Pixels, window: &mut Window, cx: &mut App) { - let max_size = (max_size - RESIZE_HANDLE_SIZE).abs(); - for panel in self.panel_entries.iter().map(|entry| &entry.panel) { - if panel.size(window, cx) > max_size { - panel.set_size(Some(max_size.max(RESIZE_HANDLE_SIZE)), window, cx); - } - } - } -} - -impl Render for Dock { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let dispatch_context = Self::dispatch_context(); - if let Some(entry) = self.visible_entry() { - let size = entry.panel.size(window, cx); - - let position = self.position; - let create_resize_handle = || { - let handle = div() - .id("resize-handle") - .on_drag(DraggedDock(position), |dock, _, _, cx| { - cx.stop_propagation(); - cx.new(|_| dock.clone()) - }) - .on_mouse_down( - MouseButton::Left, - cx.listener(|_, _: &MouseDownEvent, _, cx| { - cx.stop_propagation(); - }), - ) - .on_mouse_up( - MouseButton::Left, - cx.listener(|dock, e: &MouseUpEvent, window, cx| { - if e.click_count == 2 { - dock.resize_active_panel(None, window, cx); - dock.workspace - .update(cx, |workspace, cx| { - workspace.serialize_workspace(window, cx); - }) - .ok(); - cx.stop_propagation(); - } - }), - ) - .occlude(); - match self.position() { - DockPosition::Left => deferred( - handle - .absolute() - .right(-RESIZE_HANDLE_SIZE / 2.) - .top(px(0.)) - .h_full() - .w(RESIZE_HANDLE_SIZE) - .cursor_col_resize(), - ), - DockPosition::Bottom => deferred( - handle - .absolute() - .top(-RESIZE_HANDLE_SIZE / 2.) - .left(px(0.)) - .w_full() - .h(RESIZE_HANDLE_SIZE) - .cursor_row_resize(), - ), - DockPosition::Right => deferred( - handle - .absolute() - .top(px(0.)) - .left(-RESIZE_HANDLE_SIZE / 2.) - .h_full() - .w(RESIZE_HANDLE_SIZE) - .cursor_col_resize(), - ), - } - }; - - div() - .key_context(dispatch_context) - .track_focus(&self.focus_handle(cx)) - .flex() - .bg(cx.theme().colors().panel_background) - .border_color(cx.theme().colors().border) - .overflow_hidden() - .map(|this| match self.position().axis() { - Axis::Horizontal => this.w(size).h_full().flex_row(), - Axis::Vertical => this.h(size).w_full().flex_col(), - }) - .map(|this| match self.position() { - DockPosition::Left => this.border_r_1(), - DockPosition::Right => this.border_l_1(), - DockPosition::Bottom => this.border_t_1(), - }) - .child( - div() - .map(|this| match self.position().axis() { - Axis::Horizontal => this.min_w(size).h_full(), - Axis::Vertical => this.min_h(size).w_full(), - }) - .child( - entry - .panel - .to_any() - .cached(StyleRefinement::default().v_flex().size_full()), - ), - ) - .when(self.resizable(cx), |this| { - this.child(create_resize_handle()) - }) - } else { - div() - .key_context(dispatch_context) - .track_focus(&self.focus_handle(cx)) - } - } -} - -impl PanelButtons { - pub fn new(dock: Entity, cx: &mut Context) -> Self { - cx.observe(&dock, |_, _, cx| cx.notify()).detach(); - let settings_subscription = cx.observe_global::(|_, cx| cx.notify()); - Self { - dock, - _settings_subscription: settings_subscription, - } - } -} - -impl Render for PanelButtons { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let dock = self.dock.read(cx); - let active_index = dock.active_panel_index; - let is_open = dock.is_open; - let dock_position = dock.position; - - let (menu_anchor, menu_attach) = match dock.position { - DockPosition::Left => (Corner::BottomLeft, Corner::TopLeft), - DockPosition::Bottom | DockPosition::Right => (Corner::BottomRight, Corner::TopRight), - }; - - let buttons: Vec<_> = dock - .panel_entries - .iter() - .enumerate() - .filter_map(|(i, entry)| { - let icon = entry.panel.icon(window, cx)?; - let icon_tooltip = entry.panel.icon_tooltip(window, cx)?; - let name = entry.panel.persistent_name(); - let panel = entry.panel.clone(); - - let is_active_button = Some(i) == active_index && is_open; - let (action, tooltip) = if is_active_button { - let action = dock.toggle_action(); - - let tooltip: SharedString = - format!("Close {} Dock", dock.position.label()).into(); - - (action, tooltip) - } else { - let action = entry.panel.toggle_action(window, cx); - - (action, icon_tooltip.into()) - }; - - let focus_handle = dock.focus_handle(cx); - - Some( - right_click_menu(name) - .menu(move |window, cx| { - const POSITIONS: [DockPosition; 3] = [ - DockPosition::Left, - DockPosition::Right, - DockPosition::Bottom, - ]; - - ContextMenu::build(window, cx, |mut menu, _, cx| { - for position in POSITIONS { - if position != dock_position - && panel.position_is_valid(position, cx) - { - let panel = panel.clone(); - menu = menu.entry( - format!("Dock {}", position.label()), - None, - move |window, cx| { - panel.set_position(position, window, cx); - }, - ) - } - } - menu - }) - }) - .anchor(menu_anchor) - .attach(menu_attach) - .trigger(move |is_active, _window, _cx| { - IconButton::new(name, icon) - .icon_size(IconSize::Small) - .toggle_state(is_active_button) - .on_click({ - let action = action.boxed_clone(); - move |_, window, cx| { - telemetry::event!( - "Panel Button Clicked", - name = name, - toggle_state = !is_open - ); - window.focus(&focus_handle); - window.dispatch_action(action.boxed_clone(), cx) - } - }) - .when(!is_active, |this| { - this.tooltip(move |_window, cx| { - Tooltip::for_action(tooltip.clone(), &*action, cx) - }) - }) - }), - ) - }) - .collect(); - - let has_buttons = !buttons.is_empty(); - - h_flex() - .gap_1() - .when( - has_buttons && dock.position == DockPosition::Bottom, - |this| this.child(Divider::vertical().color(DividerColor::Border)), - ) - .children(buttons) - .when(has_buttons && dock.position == DockPosition::Left, |this| { - this.child(Divider::vertical().color(DividerColor::Border)) - }) - } -} - -impl StatusItemView for PanelButtons { - fn set_active_pane_item( - &mut self, - _active_pane_item: Option<&dyn crate::ItemHandle>, - _window: &mut Window, - _cx: &mut Context, - ) { - // Nothing to do, panel buttons don't depend on the active center item - } -} - -#[cfg(any(test, feature = "test-support"))] -pub mod test { - use super::*; - use gpui::{App, Context, Window, actions, div}; - - pub struct TestPanel { - pub position: DockPosition, - pub zoomed: bool, - pub active: bool, - pub focus_handle: FocusHandle, - pub size: Pixels, - pub activation_priority: u32, - } - actions!(test_only, [ToggleTestPanel]); - - impl EventEmitter for TestPanel {} - - impl TestPanel { - pub fn new(position: DockPosition, activation_priority: u32, cx: &mut App) -> Self { - Self { - position, - zoomed: false, - active: false, - focus_handle: cx.focus_handle(), - size: px(300.), - activation_priority, - } - } - } - - impl Render for TestPanel { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - div().id("test").track_focus(&self.focus_handle(cx)) - } - } - - impl Panel for TestPanel { - fn persistent_name() -> &'static str { - "TestPanel" - } - - fn panel_key() -> &'static str { - "TestPanel" - } - - fn position(&self, _window: &Window, _: &App) -> super::DockPosition { - self.position - } - - fn position_is_valid(&self, _: super::DockPosition) -> bool { - true - } - - fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { - self.position = position; - cx.update_global::(|_, _| {}); - } - - fn size(&self, _window: &Window, _: &App) -> Pixels { - self.size - } - - fn set_size(&mut self, size: Option, _window: &mut Window, _: &mut Context) { - self.size = size.unwrap_or(px(300.)); - } - - fn icon(&self, _window: &Window, _: &App) -> Option { - None - } - - fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> { - None - } - - fn toggle_action(&self) -> Box { - ToggleTestPanel.boxed_clone() - } - - fn is_zoomed(&self, _window: &Window, _: &App) -> bool { - self.zoomed - } - - fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, _cx: &mut Context) { - self.zoomed = zoomed; - } - - fn set_active(&mut self, active: bool, _window: &mut Window, _cx: &mut Context) { - self.active = active; - } - - fn activation_priority(&self) -> u32 { - self.activation_priority - } - } - - impl Focusable for TestPanel { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } - } -} diff --git a/crates/workspace/src/history_manager.rs b/crates/workspace/src/history_manager.rs deleted file mode 100644 index 1b80e7c012..0000000000 --- a/crates/workspace/src/history_manager.rs +++ /dev/null @@ -1,136 +0,0 @@ -use std::path::PathBuf; - -use gpui::{AppContext, Entity, Global, MenuItem}; -use smallvec::SmallVec; -use ui::App; -use util::{ResultExt, paths::PathExt}; - -use crate::{ - NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId, path_list::PathList, -}; - -pub fn init(cx: &mut App) { - let manager = cx.new(|_| HistoryManager::new()); - HistoryManager::set_global(manager.clone(), cx); - HistoryManager::init(manager, cx); -} - -pub struct HistoryManager { - /// The history of workspaces that have been opened in the past, in reverse order. - /// The most recent workspace is at the end of the vector. - history: Vec, -} - -#[derive(Debug)] -pub struct HistoryManagerEntry { - pub id: WorkspaceId, - pub path: SmallVec<[PathBuf; 2]>, -} - -struct GlobalHistoryManager(Entity); - -impl Global for GlobalHistoryManager {} - -impl HistoryManager { - fn new() -> Self { - Self { - history: Vec::new(), - } - } - - fn init(this: Entity, cx: &App) { - cx.spawn(async move |cx| { - let recent_folders = WORKSPACE_DB - .recent_workspaces_on_disk() - .await - .unwrap_or_default() - .into_iter() - .rev() - .filter_map(|(id, location, paths)| { - if matches!(location, SerializedWorkspaceLocation::Local) { - Some(HistoryManagerEntry::new(id, &paths)) - } else { - None - } - }) - .collect::>(); - this.update(cx, |this, cx| { - this.history = recent_folders; - this.update_jump_list(cx); - }) - }) - .detach(); - } - - pub fn global(cx: &App) -> Option> { - cx.try_global::() - .map(|model| model.0.clone()) - } - - fn set_global(history_manager: Entity, cx: &mut App) { - cx.set_global(GlobalHistoryManager(history_manager)); - } - - pub fn update_history(&mut self, id: WorkspaceId, entry: HistoryManagerEntry, cx: &App) { - if let Some(pos) = self.history.iter().position(|e| e.id == id) { - self.history.remove(pos); - } - self.history.push(entry); - self.update_jump_list(cx); - } - - pub fn delete_history(&mut self, id: WorkspaceId, cx: &App) { - let Some(pos) = self.history.iter().position(|e| e.id == id) else { - return; - }; - self.history.remove(pos); - self.update_jump_list(cx); - } - - fn update_jump_list(&mut self, cx: &App) { - let menus = vec![MenuItem::action("New Window", NewWindow)]; - let entries = self - .history - .iter() - .rev() - .map(|entry| entry.path.clone()) - .collect::>(); - let user_removed = cx.update_jump_list(menus, entries); - self.remove_user_removed_workspaces(user_removed, cx); - } - - pub fn remove_user_removed_workspaces( - &mut self, - user_removed: Vec>, - cx: &App, - ) { - if user_removed.is_empty() { - return; - } - let mut deleted_ids = Vec::new(); - for idx in (0..self.history.len()).rev() { - if let Some(entry) = self.history.get(idx) - && user_removed.contains(&entry.path) - { - deleted_ids.push(entry.id); - self.history.remove(idx); - } - } - cx.spawn(async move |_| { - for id in deleted_ids.iter() { - WORKSPACE_DB.delete_workspace_by_id(*id).await.log_err(); - } - }) - .detach(); - } -} - -impl HistoryManagerEntry { - pub fn new(id: WorkspaceId, paths: &PathList) -> Self { - let path = paths - .ordered_paths() - .map(|path| path.compact()) - .collect::>(); - Self { id, path } - } -} diff --git a/crates/workspace/src/invalid_item_view.rs b/crates/workspace/src/invalid_item_view.rs deleted file mode 100644 index 08242a1ed0..0000000000 --- a/crates/workspace/src/invalid_item_view.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::{path::Path, sync::Arc}; - -use gpui::{EventEmitter, FocusHandle, Focusable}; -use ui::{ - App, Button, ButtonCommon, ButtonStyle, Clickable, Context, FluentBuilder, InteractiveElement, - KeyBinding, Label, LabelCommon, LabelSize, ParentElement, Render, SharedString, Styled as _, - Window, h_flex, v_flex, -}; -use zed_actions::workspace::OpenWithSystem; - -use crate::Item; - -/// A view to display when a certain buffer/image/other item fails to open. -#[derive(Debug)] -pub struct InvalidItemView { - /// Which path was attempted to open. - pub abs_path: Arc, - /// An error message, happened when opening the item. - pub error: SharedString, - is_local: bool, - focus_handle: FocusHandle, -} - -impl InvalidItemView { - pub fn new( - abs_path: &Path, - is_local: bool, - e: &anyhow::Error, - _: &mut Window, - cx: &mut App, - ) -> Self { - Self { - is_local, - abs_path: Arc::from(abs_path), - error: format!("{}", e.root_cause()).into(), - focus_handle: cx.focus_handle(), - } - } -} - -impl Item for InvalidItemView { - type Event = (); - - fn tab_content_text(&self, mut detail: usize, _: &App) -> SharedString { - // Ensure we always render at least the filename. - detail += 1; - - let path = self.abs_path.as_ref(); - - let mut prefix = path; - while detail > 0 { - if let Some(parent) = prefix.parent() { - prefix = parent; - detail -= 1; - } else { - break; - } - } - - let path = if detail > 0 { - path - } else { - path.strip_prefix(prefix).unwrap_or(path) - }; - - SharedString::new(path.to_string_lossy()) - } -} - -impl EventEmitter<()> for InvalidItemView {} - -impl Focusable for InvalidItemView { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for InvalidItemView { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { - let abs_path = self.abs_path.clone(); - v_flex() - .size_full() - .track_focus(&self.focus_handle(cx)) - .flex_none() - .justify_center() - .overflow_hidden() - .key_context("InvalidItem") - .child( - h_flex().size_full().justify_center().child( - v_flex() - .justify_center() - .gap_2() - .child(h_flex().justify_center().child("Could not open file")) - .child( - h_flex() - .justify_center() - .child(Label::new(self.error.clone()).size(LabelSize::Small)), - ) - .when(self.is_local, |contents| { - contents.child( - h_flex().justify_center().child( - Button::new("open-with-system", "Open in Default App") - .on_click(move |_, _, cx| { - cx.open_with_system(&abs_path); - }) - .style(ButtonStyle::Outlined) - .key_binding(KeyBinding::for_action(&OpenWithSystem, cx)), - ), - ) - }), - ), - ) - } -} diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs deleted file mode 100644 index 42eb754c21..0000000000 --- a/crates/workspace/src/item.rs +++ /dev/null @@ -1,1694 +0,0 @@ -use crate::{ - CollaboratorId, DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory, - SerializableItemRegistry, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, - invalid_item_view::InvalidItemView, - pane::{self, Pane}, - persistence::model::ItemId, - searchable::SearchableItemHandle, - workspace_settings::{AutosaveSetting, WorkspaceSettings}, -}; -use anyhow::Result; -use client::{Client, proto}; -use futures::{StreamExt, channel::mpsc}; -use gpui::{ - Action, AnyElement, AnyEntity, AnyView, App, AppContext, Context, Entity, EntityId, - EventEmitter, FocusHandle, Focusable, Font, HighlightStyle, Pixels, Point, Render, - SharedString, Task, WeakEntity, Window, -}; -use project::{Project, ProjectEntryId, ProjectPath}; -pub use settings::{ - ActivateOnClose, ClosePosition, RegisterSetting, Settings, SettingsLocation, ShowCloseButton, - ShowDiagnostics, -}; -use smallvec::SmallVec; -use std::{ - any::{Any, TypeId}, - cell::RefCell, - ops::Range, - path::Path, - rc::Rc, - sync::Arc, - time::Duration, -}; -use theme::Theme; -use ui::{Color, Icon, IntoElement, Label, LabelCommon}; -use util::ResultExt; - -pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200); - -#[derive(Clone, Copy, Debug)] -pub struct SaveOptions { - pub format: bool, - pub autosave: bool, -} - -impl Default for SaveOptions { - fn default() -> Self { - Self { - format: true, - autosave: false, - } - } -} - -#[derive(RegisterSetting)] -pub struct ItemSettings { - pub git_status: bool, - pub close_position: ClosePosition, - pub activate_on_close: ActivateOnClose, - pub file_icons: bool, - pub show_diagnostics: ShowDiagnostics, - pub show_close_button: ShowCloseButton, -} - -#[derive(RegisterSetting)] -pub struct PreviewTabsSettings { - pub enabled: bool, - pub enable_preview_from_project_panel: bool, - pub enable_preview_from_file_finder: bool, - pub enable_preview_from_multibuffer: bool, - pub enable_preview_multibuffer_from_code_navigation: bool, - pub enable_preview_file_from_code_navigation: bool, - pub enable_keep_preview_on_code_navigation: bool, -} - -impl Settings for ItemSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let tabs = content.tabs.as_ref().unwrap(); - Self { - git_status: tabs.git_status.unwrap(), - close_position: tabs.close_position.unwrap(), - activate_on_close: tabs.activate_on_close.unwrap(), - file_icons: tabs.file_icons.unwrap(), - show_diagnostics: tabs.show_diagnostics.unwrap(), - show_close_button: tabs.show_close_button.unwrap(), - } - } -} - -impl Settings for PreviewTabsSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let preview_tabs = content.preview_tabs.as_ref().unwrap(); - Self { - enabled: preview_tabs.enabled.unwrap(), - enable_preview_from_project_panel: preview_tabs - .enable_preview_from_project_panel - .unwrap(), - enable_preview_from_file_finder: preview_tabs.enable_preview_from_file_finder.unwrap(), - enable_preview_from_multibuffer: preview_tabs.enable_preview_from_multibuffer.unwrap(), - enable_preview_multibuffer_from_code_navigation: preview_tabs - .enable_preview_multibuffer_from_code_navigation - .unwrap(), - enable_preview_file_from_code_navigation: preview_tabs - .enable_preview_file_from_code_navigation - .unwrap(), - enable_keep_preview_on_code_navigation: preview_tabs - .enable_keep_preview_on_code_navigation - .unwrap(), - } - } -} - -#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] -pub enum ItemEvent { - CloseItem, - UpdateTab, - UpdateBreadcrumbs, - Edit, -} - -// TODO: Combine this with existing HighlightedText struct? -pub struct BreadcrumbText { - pub text: String, - pub highlights: Option, HighlightStyle)>>, - pub font: Option, -} - -#[derive(Clone, Copy, Default, Debug)] -pub struct TabContentParams { - pub detail: Option, - pub selected: bool, - pub preview: bool, - /// Tab content should be deemphasized when active pane does not have focus. - pub deemphasized: bool, -} - -impl TabContentParams { - /// Returns the text color to be used for the tab content. - pub fn text_color(&self) -> Color { - if self.deemphasized { - if self.selected { - Color::Muted - } else { - Color::Hidden - } - } else if self.selected { - Color::Default - } else { - Color::Muted - } - } -} - -pub enum TabTooltipContent { - Text(SharedString), - Custom(Box AnyView>), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ItemBufferKind { - Multibuffer, - Singleton, - None, -} - -pub trait Item: Focusable + EventEmitter + Render + Sized { - type Event; - - /// Returns the tab contents. - /// - /// By default this returns a [`Label`] that displays that text from - /// `tab_content_text`. - fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement { - let text = self.tab_content_text(params.detail.unwrap_or_default(), cx); - - Label::new(text) - .color(params.text_color()) - .into_any_element() - } - - /// Returns the textual contents of the tab. - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString; - - /// Returns the suggested filename for saving this item. - /// By default, returns the tab content text. - fn suggested_filename(&self, cx: &App) -> SharedString { - self.tab_content_text(0, cx) - } - - fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { - None - } - - /// Returns the tab tooltip text. - /// - /// Use this if you don't need to customize the tab tooltip content. - fn tab_tooltip_text(&self, _: &App) -> Option { - None - } - - /// Returns the tab tooltip content. - /// - /// By default this returns a Tooltip text from - /// `tab_tooltip_text`. - fn tab_tooltip_content(&self, cx: &App) -> Option { - self.tab_tooltip_text(cx).map(TabTooltipContent::Text) - } - - fn to_item_events(_event: &Self::Event, _f: impl FnMut(ItemEvent)) {} - - fn deactivated(&mut self, _window: &mut Window, _: &mut Context) {} - fn discarded(&self, _project: Entity, _window: &mut Window, _cx: &mut Context) {} - fn on_removed(&self, _cx: &App) {} - fn workspace_deactivated(&mut self, _window: &mut Window, _: &mut Context) {} - fn navigate(&mut self, _: Box, _window: &mut Window, _: &mut Context) -> bool { - false - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - None - } - - /// (model id, Item) - fn for_each_project_item( - &self, - _: &App, - _: &mut dyn FnMut(EntityId, &dyn project::ProjectItem), - ) { - } - fn buffer_kind(&self, _cx: &App) -> ItemBufferKind { - ItemBufferKind::None - } - fn set_nav_history(&mut self, _: ItemNavHistory, _window: &mut Window, _: &mut Context) {} - - fn can_split(&self) -> bool { - false - } - fn clone_on_split( - &self, - workspace_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> Task>> - where - Self: Sized, - { - _ = (workspace_id, window, cx); - unimplemented!("clone_on_split() must be implemented if can_split() returns true") - } - fn is_dirty(&self, _: &App) -> bool { - false - } - fn has_deleted_file(&self, _: &App) -> bool { - false - } - fn has_conflict(&self, _: &App) -> bool { - false - } - fn can_save(&self, _cx: &App) -> bool { - false - } - fn can_save_as(&self, _: &App) -> bool { - false - } - fn save( - &mut self, - _options: SaveOptions, - _project: Entity, - _window: &mut Window, - _cx: &mut Context, - ) -> Task> { - unimplemented!("save() must be implemented if can_save() returns true") - } - fn save_as( - &mut self, - _project: Entity, - _path: ProjectPath, - _window: &mut Window, - _cx: &mut Context, - ) -> Task> { - unimplemented!("save_as() must be implemented if can_save() returns true") - } - fn reload( - &mut self, - _project: Entity, - _window: &mut Window, - _cx: &mut Context, - ) -> Task> { - unimplemented!("reload() must be implemented if can_save() returns true") - } - - fn act_as_type<'a>( - &'a self, - type_id: TypeId, - self_handle: &'a Entity, - _: &'a App, - ) -> Option { - if TypeId::of::() == type_id { - Some(self_handle.clone().into()) - } else { - None - } - } - - fn as_searchable(&self, _: &Entity, _: &App) -> Option> { - None - } - - fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { - ToolbarItemLocation::Hidden - } - - fn breadcrumbs(&self, _theme: &Theme, _cx: &App) -> Option> { - None - } - - /// Returns optional elements to render to the left of the breadcrumb. - fn breadcrumb_prefix( - &self, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - None - } - - fn added_to_workspace( - &mut self, - _workspace: &mut Workspace, - _window: &mut Window, - _cx: &mut Context, - ) { - } - - fn show_toolbar(&self) -> bool { - true - } - - fn pixel_position_of_cursor(&self, _: &App) -> Option> { - None - } - - fn preserve_preview(&self, _cx: &App) -> bool { - false - } - - fn include_in_nav_history() -> bool { - true - } -} - -pub trait SerializableItem: Item { - fn serialized_item_kind() -> &'static str; - - fn cleanup( - workspace_id: WorkspaceId, - alive_items: Vec, - window: &mut Window, - cx: &mut App, - ) -> Task>; - - fn deserialize( - _project: Entity, - _workspace: WeakEntity, - _workspace_id: WorkspaceId, - _item_id: ItemId, - _window: &mut Window, - _cx: &mut App, - ) -> Task>>; - - fn serialize( - &mut self, - workspace: &mut Workspace, - item_id: ItemId, - closing: bool, - window: &mut Window, - cx: &mut Context, - ) -> Option>>; - - fn should_serialize(&self, event: &Self::Event) -> bool; -} - -pub trait SerializableItemHandle: ItemHandle { - fn serialized_item_kind(&self) -> &'static str; - fn serialize( - &self, - workspace: &mut Workspace, - closing: bool, - window: &mut Window, - cx: &mut App, - ) -> Option>>; - fn should_serialize(&self, event: &dyn Any, cx: &App) -> bool; -} - -impl SerializableItemHandle for Entity -where - T: SerializableItem, -{ - fn serialized_item_kind(&self) -> &'static str { - T::serialized_item_kind() - } - - fn serialize( - &self, - workspace: &mut Workspace, - closing: bool, - window: &mut Window, - cx: &mut App, - ) -> Option>> { - self.update(cx, |this, cx| { - this.serialize(workspace, cx.entity_id().as_u64(), closing, window, cx) - }) - } - - fn should_serialize(&self, event: &dyn Any, cx: &App) -> bool { - event - .downcast_ref::() - .is_some_and(|event| self.read(cx).should_serialize(event)) - } -} - -pub trait ItemHandle: 'static + Send { - fn item_focus_handle(&self, cx: &App) -> FocusHandle; - fn subscribe_to_item_events( - &self, - window: &mut Window, - cx: &mut App, - handler: Box, - ) -> gpui::Subscription; - fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement; - fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString; - fn suggested_filename(&self, cx: &App) -> SharedString; - fn tab_icon(&self, window: &Window, cx: &App) -> Option; - fn tab_tooltip_text(&self, cx: &App) -> Option; - fn tab_tooltip_content(&self, cx: &App) -> Option; - fn telemetry_event_text(&self, cx: &App) -> Option<&'static str>; - fn dragged_tab_content( - &self, - params: TabContentParams, - window: &Window, - cx: &App, - ) -> AnyElement; - fn project_path(&self, cx: &App) -> Option; - fn project_entry_ids(&self, cx: &App) -> SmallVec<[ProjectEntryId; 3]>; - fn project_paths(&self, cx: &App) -> SmallVec<[ProjectPath; 3]>; - fn project_item_model_ids(&self, cx: &App) -> SmallVec<[EntityId; 3]>; - fn for_each_project_item( - &self, - _: &App, - _: &mut dyn FnMut(EntityId, &dyn project::ProjectItem), - ); - fn buffer_kind(&self, cx: &App) -> ItemBufferKind; - fn boxed_clone(&self) -> Box; - fn can_split(&self, cx: &App) -> bool; - fn clone_on_split( - &self, - workspace_id: Option, - window: &mut Window, - cx: &mut App, - ) -> Task>>; - fn added_to_pane( - &self, - workspace: &mut Workspace, - pane: Entity, - window: &mut Window, - cx: &mut Context, - ); - fn deactivated(&self, window: &mut Window, cx: &mut App); - fn on_removed(&self, cx: &App); - fn workspace_deactivated(&self, window: &mut Window, cx: &mut App); - fn navigate(&self, data: Box, window: &mut Window, cx: &mut App) -> bool; - fn item_id(&self) -> EntityId; - fn to_any_view(&self) -> AnyView; - fn is_dirty(&self, cx: &App) -> bool; - fn has_deleted_file(&self, cx: &App) -> bool; - fn has_conflict(&self, cx: &App) -> bool; - fn can_save(&self, cx: &App) -> bool; - fn can_save_as(&self, cx: &App) -> bool; - fn save( - &self, - options: SaveOptions, - project: Entity, - window: &mut Window, - cx: &mut App, - ) -> Task>; - fn save_as( - &self, - project: Entity, - path: ProjectPath, - window: &mut Window, - cx: &mut App, - ) -> Task>; - fn reload( - &self, - project: Entity, - window: &mut Window, - cx: &mut App, - ) -> Task>; - fn act_as_type(&self, type_id: TypeId, cx: &App) -> Option; - fn to_followable_item_handle(&self, cx: &App) -> Option>; - fn to_serializable_item_handle(&self, cx: &App) -> Option>; - fn on_release( - &self, - cx: &mut App, - callback: Box, - ) -> gpui::Subscription; - fn to_searchable_item_handle(&self, cx: &App) -> Option>; - fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation; - fn breadcrumbs(&self, theme: &Theme, cx: &App) -> Option>; - fn breadcrumb_prefix(&self, window: &mut Window, cx: &mut App) -> Option; - fn show_toolbar(&self, cx: &App) -> bool; - fn pixel_position_of_cursor(&self, cx: &App) -> Option>; - fn downgrade_item(&self) -> Box; - fn workspace_settings<'a>(&self, cx: &'a App) -> &'a WorkspaceSettings; - fn preserve_preview(&self, cx: &App) -> bool; - fn include_in_nav_history(&self) -> bool; - fn relay_action(&self, action: Box, window: &mut Window, cx: &mut App); - fn can_autosave(&self, cx: &App) -> bool { - let is_deleted = self.project_entry_ids(cx).is_empty(); - self.is_dirty(cx) && !self.has_conflict(cx) && self.can_save(cx) && !is_deleted - } -} - -pub trait WeakItemHandle: Send + Sync { - fn id(&self) -> EntityId; - fn boxed_clone(&self) -> Box; - fn upgrade(&self) -> Option>; -} - -impl dyn ItemHandle { - pub fn downcast(&self) -> Option> { - self.to_any_view().downcast().ok() - } - - pub fn act_as(&self, cx: &App) -> Option> { - self.act_as_type(TypeId::of::(), cx) - .and_then(|t| t.downcast().ok()) - } -} - -impl ItemHandle for Entity { - fn subscribe_to_item_events( - &self, - window: &mut Window, - cx: &mut App, - handler: Box, - ) -> gpui::Subscription { - window.subscribe(self, cx, move |_, event, window, cx| { - T::to_item_events(event, |item_event| handler(item_event, window, cx)); - }) - } - - fn item_focus_handle(&self, cx: &App) -> FocusHandle { - self.read(cx).focus_handle(cx) - } - - fn telemetry_event_text(&self, cx: &App) -> Option<&'static str> { - self.read(cx).telemetry_event_text() - } - - fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement { - self.read(cx).tab_content(params, window, cx) - } - fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString { - self.read(cx).tab_content_text(detail, cx) - } - - fn suggested_filename(&self, cx: &App) -> SharedString { - self.read(cx).suggested_filename(cx) - } - - fn tab_icon(&self, window: &Window, cx: &App) -> Option { - self.read(cx).tab_icon(window, cx) - } - - fn tab_tooltip_content(&self, cx: &App) -> Option { - self.read(cx).tab_tooltip_content(cx) - } - - fn tab_tooltip_text(&self, cx: &App) -> Option { - self.read(cx).tab_tooltip_text(cx) - } - - fn dragged_tab_content( - &self, - params: TabContentParams, - window: &Window, - cx: &App, - ) -> AnyElement { - self.read(cx).tab_content( - TabContentParams { - selected: true, - ..params - }, - window, - cx, - ) - } - - fn project_path(&self, cx: &App) -> Option { - let this = self.read(cx); - let mut result = None; - if this.buffer_kind(cx) == ItemBufferKind::Singleton { - this.for_each_project_item(cx, &mut |_, item| { - result = item.project_path(cx); - }); - } - result - } - - fn workspace_settings<'a>(&self, cx: &'a App) -> &'a WorkspaceSettings { - if let Some(project_path) = self.project_path(cx) { - WorkspaceSettings::get( - Some(SettingsLocation { - worktree_id: project_path.worktree_id, - path: &project_path.path, - }), - cx, - ) - } else { - WorkspaceSettings::get_global(cx) - } - } - - fn project_entry_ids(&self, cx: &App) -> SmallVec<[ProjectEntryId; 3]> { - let mut result = SmallVec::new(); - self.read(cx).for_each_project_item(cx, &mut |_, item| { - if let Some(id) = item.entry_id(cx) { - result.push(id); - } - }); - result - } - - fn project_paths(&self, cx: &App) -> SmallVec<[ProjectPath; 3]> { - let mut result = SmallVec::new(); - self.read(cx).for_each_project_item(cx, &mut |_, item| { - if let Some(id) = item.project_path(cx) { - result.push(id); - } - }); - result - } - - fn project_item_model_ids(&self, cx: &App) -> SmallVec<[EntityId; 3]> { - let mut result = SmallVec::new(); - self.read(cx).for_each_project_item(cx, &mut |id, _| { - result.push(id); - }); - result - } - - fn for_each_project_item( - &self, - cx: &App, - f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem), - ) { - self.read(cx).for_each_project_item(cx, f) - } - - fn buffer_kind(&self, cx: &App) -> ItemBufferKind { - self.read(cx).buffer_kind(cx) - } - - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn can_split(&self, cx: &App) -> bool { - self.read(cx).can_split() - } - - fn clone_on_split( - &self, - workspace_id: Option, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - let task = self.update(cx, |item, cx| item.clone_on_split(workspace_id, window, cx)); - cx.background_spawn(async move { - task.await - .map(|handle| Box::new(handle) as Box) - }) - } - - fn added_to_pane( - &self, - workspace: &mut Workspace, - pane: Entity, - window: &mut Window, - cx: &mut Context, - ) { - let weak_item = self.downgrade(); - let history = pane.read(cx).nav_history_for_item(self); - self.update(cx, |this, cx| { - this.set_nav_history(history, window, cx); - this.added_to_workspace(workspace, window, cx); - }); - - if let Some(serializable_item) = self.to_serializable_item_handle(cx) { - workspace - .enqueue_item_serialization(serializable_item) - .log_err(); - } - - if workspace - .panes_by_item - .insert(self.item_id(), pane.downgrade()) - .is_none() - { - let mut pending_autosave = DelayedDebouncedEditAction::new(); - let (pending_update_tx, mut pending_update_rx) = mpsc::unbounded(); - let pending_update = Rc::new(RefCell::new(None)); - - let mut send_follower_updates = None; - if let Some(item) = self.to_followable_item_handle(cx) { - let is_project_item = item.is_project_item(window, cx); - let item = item.downgrade(); - - send_follower_updates = Some(cx.spawn_in(window, { - let pending_update = pending_update.clone(); - async move |workspace, cx| { - while let Some(mut leader_id) = pending_update_rx.next().await { - while let Ok(Some(id)) = pending_update_rx.try_next() { - leader_id = id; - } - - workspace.update_in(cx, |workspace, window, cx| { - let Some(item) = item.upgrade() else { return }; - workspace.update_followers( - is_project_item, - proto::update_followers::Variant::UpdateView( - proto::UpdateView { - id: item - .remote_id(workspace.client(), window, cx) - .and_then(|id| id.to_proto()), - variant: pending_update.borrow_mut().take(), - leader_id, - }, - ), - window, - cx, - ); - })?; - cx.background_executor().timer(LEADER_UPDATE_THROTTLE).await; - } - anyhow::Ok(()) - } - })); - } - - let mut event_subscription = Some(cx.subscribe_in( - self, - window, - move |workspace, item: &Entity, event, window, cx| { - let pane = if let Some(pane) = workspace - .panes_by_item - .get(&item.item_id()) - .and_then(|pane| pane.upgrade()) - { - pane - } else { - return; - }; - - if let Some(item) = item.to_followable_item_handle(cx) { - let leader_id = workspace.leader_for_pane(&pane); - - if let Some(leader_id) = leader_id - && let Some(FollowEvent::Unfollow) = item.to_follow_event(event) - { - workspace.unfollow(leader_id, window, cx); - } - - if item.item_focus_handle(cx).contains_focused(window, cx) { - match leader_id { - Some(CollaboratorId::Agent) => {} - Some(CollaboratorId::PeerId(leader_peer_id)) => { - item.add_event_to_update_proto( - event, - &mut pending_update.borrow_mut(), - window, - cx, - ); - pending_update_tx.unbounded_send(Some(leader_peer_id)).ok(); - } - None => { - item.add_event_to_update_proto( - event, - &mut pending_update.borrow_mut(), - window, - cx, - ); - pending_update_tx.unbounded_send(None).ok(); - } - } - } - } - - if let Some(item) = item.to_serializable_item_handle(cx) - && item.should_serialize(event, cx) - { - workspace.enqueue_item_serialization(item).ok(); - } - - T::to_item_events(event, |event| match event { - ItemEvent::CloseItem => { - pane.update(cx, |pane, cx| { - pane.close_item_by_id( - item.item_id(), - crate::SaveIntent::Close, - window, - cx, - ) - }) - .detach_and_log_err(cx); - } - - ItemEvent::UpdateTab => { - workspace.update_item_dirty_state(item, window, cx); - - if item.has_deleted_file(cx) - && !item.is_dirty(cx) - && item.workspace_settings(cx).close_on_file_delete - { - let item_id = item.item_id(); - let close_item_task = pane.update(cx, |pane, cx| { - pane.close_item_by_id( - item_id, - crate::SaveIntent::Close, - window, - cx, - ) - }); - cx.spawn_in(window, { - let pane = pane.clone(); - async move |_workspace, cx| { - close_item_task.await?; - pane.update(cx, |pane, _cx| { - pane.nav_history_mut().remove_item(item_id); - }) - } - }) - .detach_and_log_err(cx); - } else { - pane.update(cx, |_, cx| { - cx.emit(pane::Event::ChangeItemTitle); - cx.notify(); - }); - } - } - - ItemEvent::Edit => { - let autosave = item.workspace_settings(cx).autosave; - - if let AutosaveSetting::AfterDelay { milliseconds } = autosave { - let delay = Duration::from_millis(milliseconds.0); - let item = item.clone(); - pending_autosave.fire_new( - delay, - window, - cx, - move |workspace, window, cx| { - Pane::autosave_item( - &item, - workspace.project().clone(), - window, - cx, - ) - }, - ); - } - pane.update(cx, |pane, cx| pane.handle_item_edit(item.item_id(), cx)); - } - - _ => {} - }); - }, - )); - - cx.on_blur( - &self.read(cx).focus_handle(cx), - window, - move |workspace, window, cx| { - if let Some(item) = weak_item.upgrade() - && item.workspace_settings(cx).autosave == AutosaveSetting::OnFocusChange - { - Pane::autosave_item(&item, workspace.project.clone(), window, cx) - .detach_and_log_err(cx); - } - }, - ) - .detach(); - - let item_id = self.item_id(); - workspace.update_item_dirty_state(self, window, cx); - cx.observe_release_in(self, window, move |workspace, _, _, _| { - workspace.panes_by_item.remove(&item_id); - event_subscription.take(); - send_follower_updates.take(); - }) - .detach(); - } - - cx.defer_in(window, |workspace, window, cx| { - workspace.serialize_workspace(window, cx); - }); - } - - fn deactivated(&self, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| this.deactivated(window, cx)); - } - - fn on_removed(&self, cx: &App) { - self.read(cx).on_removed(cx); - } - - fn workspace_deactivated(&self, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| this.workspace_deactivated(window, cx)); - } - - fn navigate(&self, data: Box, window: &mut Window, cx: &mut App) -> bool { - self.update(cx, |this, cx| this.navigate(data, window, cx)) - } - - fn item_id(&self) -> EntityId { - self.entity_id() - } - - fn to_any_view(&self) -> AnyView { - self.clone().into() - } - - fn is_dirty(&self, cx: &App) -> bool { - self.read(cx).is_dirty(cx) - } - - fn has_deleted_file(&self, cx: &App) -> bool { - self.read(cx).has_deleted_file(cx) - } - - fn has_conflict(&self, cx: &App) -> bool { - self.read(cx).has_conflict(cx) - } - - fn can_save(&self, cx: &App) -> bool { - self.read(cx).can_save(cx) - } - - fn can_save_as(&self, cx: &App) -> bool { - self.read(cx).can_save_as(cx) - } - - fn save( - &self, - options: SaveOptions, - project: Entity, - window: &mut Window, - cx: &mut App, - ) -> Task> { - self.update(cx, |item, cx| item.save(options, project, window, cx)) - } - - fn save_as( - &self, - project: Entity, - path: ProjectPath, - window: &mut Window, - cx: &mut App, - ) -> Task> { - self.update(cx, |item, cx| item.save_as(project, path, window, cx)) - } - - fn reload( - &self, - project: Entity, - window: &mut Window, - cx: &mut App, - ) -> Task> { - self.update(cx, |item, cx| item.reload(project, window, cx)) - } - - fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a App) -> Option { - self.read(cx).act_as_type(type_id, self, cx) - } - - fn to_followable_item_handle(&self, cx: &App) -> Option> { - FollowableViewRegistry::to_followable_view(self.clone(), cx) - } - - fn on_release( - &self, - cx: &mut App, - callback: Box, - ) -> gpui::Subscription { - cx.observe_release(self, move |_, cx| callback(cx)) - } - - fn to_searchable_item_handle(&self, cx: &App) -> Option> { - self.read(cx).as_searchable(self, cx) - } - - fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation { - self.read(cx).breadcrumb_location(cx) - } - - fn breadcrumbs(&self, theme: &Theme, cx: &App) -> Option> { - self.read(cx).breadcrumbs(theme, cx) - } - - fn breadcrumb_prefix(&self, window: &mut Window, cx: &mut App) -> Option { - self.update(cx, |item, cx| item.breadcrumb_prefix(window, cx)) - } - - fn show_toolbar(&self, cx: &App) -> bool { - self.read(cx).show_toolbar() - } - - fn pixel_position_of_cursor(&self, cx: &App) -> Option> { - self.read(cx).pixel_position_of_cursor(cx) - } - - fn downgrade_item(&self) -> Box { - Box::new(self.downgrade()) - } - - fn to_serializable_item_handle(&self, cx: &App) -> Option> { - SerializableItemRegistry::view_to_serializable_item_handle(self.to_any_view(), cx) - } - - fn preserve_preview(&self, cx: &App) -> bool { - self.read(cx).preserve_preview(cx) - } - - fn include_in_nav_history(&self) -> bool { - T::include_in_nav_history() - } - - fn relay_action(&self, action: Box, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| { - this.focus_handle(cx).focus(window); - window.dispatch_action(action, cx); - }) - } -} - -impl From> for AnyView { - fn from(val: Box) -> Self { - val.to_any_view() - } -} - -impl From<&Box> for AnyView { - fn from(val: &Box) -> Self { - val.to_any_view() - } -} - -impl Clone for Box { - fn clone(&self) -> Box { - self.boxed_clone() - } -} - -impl WeakItemHandle for WeakEntity { - fn id(&self) -> EntityId { - self.entity_id() - } - - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn upgrade(&self) -> Option> { - self.upgrade().map(|v| Box::new(v) as Box) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ProjectItemKind(pub &'static str); - -pub trait ProjectItem: Item { - type Item: project::ProjectItem; - - fn project_item_kind() -> Option { - None - } - - fn for_project_item( - project: Entity, - pane: Option<&Pane>, - item: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Self - where - Self: Sized; - - /// A fallback handler, which will be called after [`project::ProjectItem::try_open`] fails, - /// with the error from that failure as an argument. - /// Allows to open an item that can gracefully display and handle errors. - fn for_broken_project_item( - _abs_path: &Path, - _is_local: bool, - _e: &anyhow::Error, - _window: &mut Window, - _cx: &mut App, - ) -> Option - where - Self: Sized, - { - None - } -} - -#[derive(Debug)] -pub enum FollowEvent { - Unfollow, -} - -pub enum Dedup { - KeepExisting, - ReplaceExisting, -} - -pub trait FollowableItem: Item { - fn remote_id(&self) -> Option; - fn to_state_proto(&self, window: &Window, cx: &App) -> Option; - fn from_state_proto( - project: Entity, - id: ViewId, - state: &mut Option, - window: &mut Window, - cx: &mut App, - ) -> Option>>>; - fn to_follow_event(event: &Self::Event) -> Option; - fn add_event_to_update_proto( - &self, - event: &Self::Event, - update: &mut Option, - window: &Window, - cx: &App, - ) -> bool; - fn apply_update_proto( - &mut self, - project: &Entity, - message: proto::update_view::Variant, - window: &mut Window, - cx: &mut Context, - ) -> Task>; - fn is_project_item(&self, window: &Window, cx: &App) -> bool; - fn set_leader_id( - &mut self, - leader_peer_id: Option, - window: &mut Window, - cx: &mut Context, - ); - fn dedup(&self, existing: &Self, window: &Window, cx: &App) -> Option; - fn update_agent_location( - &mut self, - _location: language::Anchor, - _window: &mut Window, - _cx: &mut Context, - ) { - } -} - -pub trait FollowableItemHandle: ItemHandle { - fn remote_id(&self, client: &Arc, window: &mut Window, cx: &mut App) -> Option; - fn downgrade(&self) -> Box; - fn set_leader_id( - &self, - leader_peer_id: Option, - window: &mut Window, - cx: &mut App, - ); - fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option; - fn add_event_to_update_proto( - &self, - event: &dyn Any, - update: &mut Option, - window: &mut Window, - cx: &mut App, - ) -> bool; - fn to_follow_event(&self, event: &dyn Any) -> Option; - fn apply_update_proto( - &self, - project: &Entity, - message: proto::update_view::Variant, - window: &mut Window, - cx: &mut App, - ) -> Task>; - fn is_project_item(&self, window: &mut Window, cx: &mut App) -> bool; - fn dedup( - &self, - existing: &dyn FollowableItemHandle, - window: &mut Window, - cx: &mut App, - ) -> Option; - fn update_agent_location(&self, location: language::Anchor, window: &mut Window, cx: &mut App); -} - -impl FollowableItemHandle for Entity { - fn remote_id(&self, client: &Arc, _: &mut Window, cx: &mut App) -> Option { - self.read(cx).remote_id().or_else(|| { - client.peer_id().map(|creator| ViewId { - creator: CollaboratorId::PeerId(creator), - id: self.item_id().as_u64(), - }) - }) - } - - fn downgrade(&self) -> Box { - Box::new(self.downgrade()) - } - - fn set_leader_id(&self, leader_id: Option, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| this.set_leader_id(leader_id, window, cx)) - } - - fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option { - self.read(cx).to_state_proto(window, cx) - } - - fn add_event_to_update_proto( - &self, - event: &dyn Any, - update: &mut Option, - window: &mut Window, - cx: &mut App, - ) -> bool { - if let Some(event) = event.downcast_ref() { - self.read(cx) - .add_event_to_update_proto(event, update, window, cx) - } else { - false - } - } - - fn to_follow_event(&self, event: &dyn Any) -> Option { - T::to_follow_event(event.downcast_ref()?) - } - - fn apply_update_proto( - &self, - project: &Entity, - message: proto::update_view::Variant, - window: &mut Window, - cx: &mut App, - ) -> Task> { - self.update(cx, |this, cx| { - this.apply_update_proto(project, message, window, cx) - }) - } - - fn is_project_item(&self, window: &mut Window, cx: &mut App) -> bool { - self.read(cx).is_project_item(window, cx) - } - - fn dedup( - &self, - existing: &dyn FollowableItemHandle, - window: &mut Window, - cx: &mut App, - ) -> Option { - let existing = existing.to_any_view().downcast::().ok()?; - self.read(cx).dedup(existing.read(cx), window, cx) - } - - fn update_agent_location(&self, location: language::Anchor, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| { - this.update_agent_location(location, window, cx) - }) - } -} - -pub trait WeakFollowableItemHandle: Send + Sync { - fn upgrade(&self) -> Option>; -} - -impl WeakFollowableItemHandle for WeakEntity { - fn upgrade(&self) -> Option> { - Some(Box::new(self.upgrade()?)) - } -} - -#[cfg(any(test, feature = "test-support"))] -pub mod test { - use super::{Item, ItemEvent, SerializableItem, TabContentParams}; - use crate::{ - ItemId, ItemNavHistory, Workspace, WorkspaceId, - item::{ItemBufferKind, SaveOptions}, - }; - use gpui::{ - AnyElement, App, AppContext as _, Context, Entity, EntityId, EventEmitter, Focusable, - InteractiveElement, IntoElement, Render, SharedString, Task, WeakEntity, Window, - }; - use project::{Project, ProjectEntryId, ProjectPath, WorktreeId}; - use std::{any::Any, cell::Cell}; - use util::rel_path::rel_path; - - pub struct TestProjectItem { - pub entry_id: Option, - pub project_path: Option, - pub is_dirty: bool, - } - - pub struct TestItem { - pub workspace_id: Option, - pub state: String, - pub label: String, - pub save_count: usize, - pub save_as_count: usize, - pub reload_count: usize, - pub is_dirty: bool, - pub buffer_kind: ItemBufferKind, - pub has_conflict: bool, - pub has_deleted_file: bool, - pub project_items: Vec>, - pub nav_history: Option, - pub tab_descriptions: Option>, - pub tab_detail: Cell>, - serialize: Option Option>>>>, - focus_handle: gpui::FocusHandle, - } - - impl project::ProjectItem for TestProjectItem { - fn try_open( - _project: &Entity, - _path: &ProjectPath, - _cx: &mut App, - ) -> Option>>> { - None - } - fn entry_id(&self, _: &App) -> Option { - self.entry_id - } - - fn project_path(&self, _: &App) -> Option { - self.project_path.clone() - } - - fn is_dirty(&self) -> bool { - self.is_dirty - } - } - - pub enum TestItemEvent { - Edit, - } - - impl TestProjectItem { - pub fn new(id: u64, path: &str, cx: &mut App) -> Entity { - let entry_id = Some(ProjectEntryId::from_proto(id)); - let project_path = Some(ProjectPath { - worktree_id: WorktreeId::from_usize(0), - path: rel_path(path).into(), - }); - cx.new(|_| Self { - entry_id, - project_path, - is_dirty: false, - }) - } - - pub fn new_untitled(cx: &mut App) -> Entity { - cx.new(|_| Self { - project_path: None, - entry_id: None, - is_dirty: false, - }) - } - - pub fn new_dirty(id: u64, path: &str, cx: &mut App) -> Entity { - let entry_id = Some(ProjectEntryId::from_proto(id)); - let project_path = Some(ProjectPath { - worktree_id: WorktreeId::from_usize(0), - path: rel_path(path).into(), - }); - cx.new(|_| Self { - entry_id, - project_path, - is_dirty: true, - }) - } - } - - impl TestItem { - pub fn new(cx: &mut Context) -> Self { - Self { - state: String::new(), - label: String::new(), - save_count: 0, - save_as_count: 0, - reload_count: 0, - is_dirty: false, - has_conflict: false, - has_deleted_file: false, - project_items: Vec::new(), - buffer_kind: ItemBufferKind::Singleton, - nav_history: None, - tab_descriptions: None, - tab_detail: Default::default(), - workspace_id: Default::default(), - focus_handle: cx.focus_handle(), - serialize: None, - } - } - - pub fn new_deserialized(id: WorkspaceId, cx: &mut Context) -> Self { - let mut this = Self::new(cx); - this.workspace_id = Some(id); - this - } - - pub fn with_label(mut self, state: &str) -> Self { - self.label = state.to_string(); - self - } - - pub fn with_buffer_kind(mut self, buffer_kind: ItemBufferKind) -> Self { - self.buffer_kind = buffer_kind; - self - } - - pub fn set_has_deleted_file(&mut self, deleted: bool) { - self.has_deleted_file = deleted; - } - - pub fn with_dirty(mut self, dirty: bool) -> Self { - self.is_dirty = dirty; - self - } - - pub fn with_conflict(mut self, has_conflict: bool) -> Self { - self.has_conflict = has_conflict; - self - } - - pub fn with_project_items(mut self, items: &[Entity]) -> Self { - self.project_items.clear(); - self.project_items.extend(items.iter().cloned()); - self - } - - pub fn with_serialize( - mut self, - serialize: impl Fn() -> Option>> + 'static, - ) -> Self { - self.serialize = Some(Box::new(serialize)); - self - } - - pub fn set_state(&mut self, state: String, cx: &mut Context) { - self.push_to_nav_history(cx); - self.state = state; - } - - fn push_to_nav_history(&mut self, cx: &mut Context) { - if let Some(history) = &mut self.nav_history { - history.push(Some(Box::new(self.state.clone())), cx); - } - } - } - - impl Render for TestItem { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - gpui::div().track_focus(&self.focus_handle(cx)) - } - } - - impl EventEmitter for TestItem {} - - impl Focusable for TestItem { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } - } - - impl Item for TestItem { - type Event = ItemEvent; - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { - f(*event) - } - - fn tab_content_text(&self, detail: usize, _cx: &App) -> SharedString { - self.tab_descriptions - .as_ref() - .and_then(|descriptions| { - let description = *descriptions.get(detail).or_else(|| descriptions.last())?; - description.into() - }) - .unwrap_or_default() - .into() - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - None - } - - fn tab_content(&self, params: TabContentParams, _window: &Window, _cx: &App) -> AnyElement { - self.tab_detail.set(params.detail); - gpui::div().into_any_element() - } - - fn for_each_project_item( - &self, - cx: &App, - f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem), - ) { - self.project_items - .iter() - .for_each(|item| f(item.entity_id(), item.read(cx))) - } - - fn buffer_kind(&self, _: &App) -> ItemBufferKind { - self.buffer_kind - } - - fn set_nav_history( - &mut self, - history: ItemNavHistory, - _window: &mut Window, - _: &mut Context, - ) { - self.nav_history = Some(history); - } - - fn navigate( - &mut self, - state: Box, - _window: &mut Window, - _: &mut Context, - ) -> bool { - let state = *state.downcast::().unwrap_or_default(); - if state != self.state { - self.state = state; - true - } else { - false - } - } - - fn deactivated(&mut self, _window: &mut Window, cx: &mut Context) { - self.push_to_nav_history(cx); - } - - fn can_split(&self) -> bool { - true - } - - fn clone_on_split( - &self, - _workspace_id: Option, - _: &mut Window, - cx: &mut Context, - ) -> Task>> - where - Self: Sized, - { - Task::ready(Some(cx.new(|cx| Self { - state: self.state.clone(), - label: self.label.clone(), - save_count: self.save_count, - save_as_count: self.save_as_count, - reload_count: self.reload_count, - is_dirty: self.is_dirty, - buffer_kind: self.buffer_kind, - has_conflict: self.has_conflict, - has_deleted_file: self.has_deleted_file, - project_items: self.project_items.clone(), - nav_history: None, - tab_descriptions: None, - tab_detail: Default::default(), - workspace_id: self.workspace_id, - focus_handle: cx.focus_handle(), - serialize: None, - }))) - } - - fn is_dirty(&self, _: &App) -> bool { - self.is_dirty - } - - fn has_conflict(&self, _: &App) -> bool { - self.has_conflict - } - - fn has_deleted_file(&self, _: &App) -> bool { - self.has_deleted_file - } - - fn can_save(&self, cx: &App) -> bool { - !self.project_items.is_empty() - && self - .project_items - .iter() - .all(|item| item.read(cx).entry_id.is_some()) - } - - fn can_save_as(&self, _cx: &App) -> bool { - self.buffer_kind == ItemBufferKind::Singleton - } - - fn save( - &mut self, - _: SaveOptions, - _: Entity, - _window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.save_count += 1; - self.is_dirty = false; - for item in &self.project_items { - item.update(cx, |item, _| { - if item.is_dirty { - item.is_dirty = false; - } - }) - } - Task::ready(Ok(())) - } - - fn save_as( - &mut self, - _: Entity, - _: ProjectPath, - _window: &mut Window, - _: &mut Context, - ) -> Task> { - self.save_as_count += 1; - self.is_dirty = false; - Task::ready(Ok(())) - } - - fn reload( - &mut self, - _: Entity, - _window: &mut Window, - _: &mut Context, - ) -> Task> { - self.reload_count += 1; - self.is_dirty = false; - Task::ready(Ok(())) - } - } - - impl SerializableItem for TestItem { - fn serialized_item_kind() -> &'static str { - "TestItem" - } - - fn deserialize( - _project: Entity, - _workspace: WeakEntity, - workspace_id: WorkspaceId, - _item_id: ItemId, - _window: &mut Window, - cx: &mut App, - ) -> Task>> { - let entity = cx.new(|cx| Self::new_deserialized(workspace_id, cx)); - Task::ready(Ok(entity)) - } - - fn cleanup( - _workspace_id: WorkspaceId, - _alive_items: Vec, - _window: &mut Window, - _cx: &mut App, - ) -> Task> { - Task::ready(Ok(())) - } - - fn serialize( - &mut self, - _workspace: &mut Workspace, - _item_id: ItemId, - _closing: bool, - _window: &mut Window, - _cx: &mut Context, - ) -> Option>> { - if let Some(serialize) = self.serialize.take() { - let result = serialize(); - self.serialize = Some(serialize); - result - } else { - None - } - } - - fn should_serialize(&self, _event: &Self::Event) -> bool { - false - } - } -} diff --git a/crates/workspace/src/modal_layer.rs b/crates/workspace/src/modal_layer.rs deleted file mode 100644 index bcd7db3a82..0000000000 --- a/crates/workspace/src/modal_layer.rs +++ /dev/null @@ -1,208 +0,0 @@ -use gpui::{ - AnyView, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable as _, ManagedView, - MouseButton, Subscription, -}; -use ui::prelude::*; - -#[derive(Debug)] -pub enum DismissDecision { - Dismiss(bool), - Pending, -} - -pub trait ModalView: ManagedView { - fn on_before_dismiss( - &mut self, - _window: &mut Window, - _: &mut Context, - ) -> DismissDecision { - DismissDecision::Dismiss(true) - } - - fn fade_out_background(&self) -> bool { - false - } -} - -trait ModalViewHandle { - fn on_before_dismiss(&mut self, window: &mut Window, cx: &mut App) -> DismissDecision; - fn view(&self) -> AnyView; - fn fade_out_background(&self, cx: &mut App) -> bool; -} - -impl ModalViewHandle for Entity { - fn on_before_dismiss(&mut self, window: &mut Window, cx: &mut App) -> DismissDecision { - self.update(cx, |this, cx| this.on_before_dismiss(window, cx)) - } - - fn view(&self) -> AnyView { - self.clone().into() - } - - fn fade_out_background(&self, cx: &mut App) -> bool { - self.read(cx).fade_out_background() - } -} - -pub struct ActiveModal { - modal: Box, - _subscriptions: [Subscription; 2], - previous_focus_handle: Option, - focus_handle: FocusHandle, -} - -pub struct ModalLayer { - active_modal: Option, - dismiss_on_focus_lost: bool, -} - -pub(crate) struct ModalOpenedEvent; - -impl EventEmitter for ModalLayer {} - -impl Default for ModalLayer { - fn default() -> Self { - Self::new() - } -} - -impl ModalLayer { - pub fn new() -> Self { - Self { - active_modal: None, - dismiss_on_focus_lost: false, - } - } - - pub fn toggle_modal(&mut self, window: &mut Window, cx: &mut Context, build_view: B) - where - V: ModalView, - B: FnOnce(&mut Window, &mut Context) -> V, - { - if let Some(active_modal) = &self.active_modal { - let is_close = active_modal.modal.view().downcast::().is_ok(); - let did_close = self.hide_modal(window, cx); - if is_close || !did_close { - return; - } - } - let new_modal = cx.new(|cx| build_view(window, cx)); - self.show_modal(new_modal, window, cx); - cx.emit(ModalOpenedEvent); - } - - fn show_modal(&mut self, new_modal: Entity, window: &mut Window, cx: &mut Context) - where - V: ModalView, - { - let focus_handle = cx.focus_handle(); - self.active_modal = Some(ActiveModal { - modal: Box::new(new_modal.clone()), - _subscriptions: [ - cx.subscribe_in( - &new_modal, - window, - |this, _, _: &DismissEvent, window, cx| { - this.hide_modal(window, cx); - }, - ), - cx.on_focus_out(&focus_handle, window, |this, _event, window, cx| { - if this.dismiss_on_focus_lost { - this.hide_modal(window, cx); - } - }), - ], - previous_focus_handle: window.focused(cx), - focus_handle, - }); - cx.defer_in(window, move |_, window, cx| { - window.focus(&new_modal.focus_handle(cx)); - }); - cx.notify(); - } - - pub fn hide_modal(&mut self, window: &mut Window, cx: &mut Context) -> bool { - let Some(active_modal) = self.active_modal.as_mut() else { - self.dismiss_on_focus_lost = false; - return false; - }; - - match active_modal.modal.on_before_dismiss(window, cx) { - DismissDecision::Dismiss(dismiss) => { - self.dismiss_on_focus_lost = !dismiss; - if !dismiss { - return false; - } - } - DismissDecision::Pending => { - self.dismiss_on_focus_lost = false; - return false; - } - } - - if let Some(active_modal) = self.active_modal.take() { - if let Some(previous_focus) = active_modal.previous_focus_handle - && active_modal.focus_handle.contains_focused(window, cx) - { - previous_focus.focus(window); - } - cx.notify(); - } - true - } - - pub fn active_modal(&self) -> Option> - where - V: 'static, - { - let active_modal = self.active_modal.as_ref()?; - active_modal.modal.view().downcast::().ok() - } - - pub fn has_active_modal(&self) -> bool { - self.active_modal.is_some() - } -} - -impl Render for ModalLayer { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let Some(active_modal) = &self.active_modal else { - return div(); - }; - - div() - .occlude() - .absolute() - .size_full() - .top_0() - .left_0() - .when(active_modal.modal.fade_out_background(cx), |el| { - let mut background = cx.theme().colors().elevated_surface_background; - background.fade_out(0.2); - el.bg(background) - }) - .on_mouse_down( - MouseButton::Left, - cx.listener(|this, _, window, cx| { - this.hide_modal(window, cx); - }), - ) - .child( - v_flex() - .h(px(0.0)) - .top_20() - .flex() - .flex_col() - .items_center() - .track_focus(&active_modal.focus_handle) - .child( - h_flex() - .occlude() - .child(active_modal.modal.view()) - .on_mouse_down(MouseButton::Left, |_, _, cx| { - cx.stop_propagation(); - }), - ), - ) - } -} diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs deleted file mode 100644 index cfdc730b4d..0000000000 --- a/crates/workspace/src/notifications.rs +++ /dev/null @@ -1,1115 +0,0 @@ -use crate::{SuppressNotification, Toast, Workspace}; -use anyhow::Context as _; -use gpui::{ - AnyView, App, AppContext as _, AsyncWindowContext, ClickEvent, ClipboardItem, Context, - DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, PromptLevel, Render, ScrollHandle, - Task, svg, -}; -use parking_lot::Mutex; - -use std::ops::Deref; -use std::sync::{Arc, LazyLock}; -use std::{any::TypeId, time::Duration}; -use ui::{Tooltip, prelude::*}; -use util::ResultExt; - -#[derive(Default)] -pub struct Notifications { - notifications: Vec<(NotificationId, AnyView)>, -} - -impl Deref for Notifications { - type Target = Vec<(NotificationId, AnyView)>; - - fn deref(&self) -> &Self::Target { - &self.notifications - } -} - -impl std::ops::DerefMut for Notifications { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.notifications - } -} - -#[derive(Debug, Eq, PartialEq, Clone, Hash)] -pub enum NotificationId { - Unique(TypeId), - Composite(TypeId, ElementId), - Named(SharedString), -} - -impl NotificationId { - /// Returns a unique [`NotificationId`] for the given type. - pub fn unique() -> Self { - Self::Unique(TypeId::of::()) - } - - /// Returns a [`NotificationId`] for the given type that is also identified - /// by the provided ID. - pub fn composite(id: impl Into) -> Self { - Self::Composite(TypeId::of::(), id.into()) - } - - /// Builds a `NotificationId` out of the given string. - pub fn named(id: SharedString) -> Self { - Self::Named(id) - } -} - -pub trait Notification: - EventEmitter + EventEmitter + Focusable + Render -{ -} - -pub struct SuppressEvent; - -impl Workspace { - #[cfg(any(test, feature = "test-support"))] - pub fn notification_ids(&self) -> Vec { - self.notifications - .iter() - .map(|(id, _)| id) - .cloned() - .collect() - } - - pub fn show_notification( - &mut self, - id: NotificationId, - cx: &mut Context, - build_notification: impl FnOnce(&mut Context) -> Entity, - ) { - self.show_notification_without_handling_dismiss_events(&id, cx, |cx| { - let notification = build_notification(cx); - cx.subscribe(¬ification, { - let id = id.clone(); - move |this, _, _: &DismissEvent, cx| { - this.dismiss_notification(&id, cx); - } - }) - .detach(); - cx.subscribe(¬ification, { - let id = id.clone(); - move |workspace: &mut Workspace, _, _: &SuppressEvent, cx| { - workspace.suppress_notification(&id, cx); - } - }) - .detach(); - notification.into() - }); - } - - /// Shows a notification in this workspace's window. Caller must handle dismiss. - /// - /// This exists so that the `build_notification` closures stored for app notifications can - /// return `AnyView`. Subscribing to events from an `AnyView` is not supported, so instead that - /// responsibility is pushed to the caller where the `V` type is known. - pub(crate) fn show_notification_without_handling_dismiss_events( - &mut self, - id: &NotificationId, - cx: &mut Context, - build_notification: impl FnOnce(&mut Context) -> AnyView, - ) { - if self.suppressed_notifications.contains(id) { - return; - } - self.dismiss_notification(id, cx); - self.notifications - .push((id.clone(), build_notification(cx))); - cx.notify(); - } - - pub fn show_error(&mut self, err: &E, cx: &mut Context) - where - E: std::fmt::Debug + std::fmt::Display, - { - self.show_notification(workspace_error_notification_id(), cx, |cx| { - cx.new(|cx| ErrorMessagePrompt::new(format!("Error: {err}"), cx)) - }); - } - - pub fn show_portal_error(&mut self, err: String, cx: &mut Context) { - struct PortalError; - - self.show_notification(NotificationId::unique::(), cx, |cx| { - cx.new(|cx| { - ErrorMessagePrompt::new(err.to_string(), cx).with_link_button( - "See docs", - "https://zed.dev/docs/linux#i-cant-open-any-files", - ) - }) - }); - } - - pub fn dismiss_notification(&mut self, id: &NotificationId, cx: &mut Context) { - self.notifications.retain(|(existing_id, _)| { - if existing_id == id { - cx.notify(); - false - } else { - true - } - }); - } - - pub fn show_toast(&mut self, toast: Toast, cx: &mut Context) { - self.dismiss_notification(&toast.id, cx); - self.show_notification(toast.id.clone(), cx, |cx| { - cx.new(|cx| match toast.on_click.as_ref() { - Some((click_msg, on_click)) => { - let on_click = on_click.clone(); - simple_message_notification::MessageNotification::new(toast.msg.clone(), cx) - .primary_message(click_msg.clone()) - .primary_on_click(move |window, cx| on_click(window, cx)) - } - None => { - simple_message_notification::MessageNotification::new(toast.msg.clone(), cx) - } - }) - }); - if toast.autohide { - cx.spawn(async move |workspace, cx| { - cx.background_executor() - .timer(Duration::from_millis(5000)) - .await; - workspace - .update(cx, |workspace, cx| workspace.dismiss_toast(&toast.id, cx)) - .ok(); - }) - .detach(); - } - } - - pub fn dismiss_toast(&mut self, id: &NotificationId, cx: &mut Context) { - self.dismiss_notification(id, cx); - } - - pub fn clear_all_notifications(&mut self, cx: &mut Context) { - self.notifications.clear(); - cx.notify(); - } - - /// Hide all notifications matching the given ID - pub fn suppress_notification(&mut self, id: &NotificationId, cx: &mut Context) { - self.dismiss_notification(id, cx); - self.suppressed_notifications.insert(id.clone()); - } - - pub fn show_initial_notifications(&mut self, cx: &mut Context) { - // Allow absence of the global so that tests don't need to initialize it. - let app_notifications = GLOBAL_APP_NOTIFICATIONS - .lock() - .app_notifications - .iter() - .cloned() - .collect::>(); - for (id, build_notification) in app_notifications { - self.show_notification_without_handling_dismiss_events(&id, cx, |cx| { - build_notification(cx) - }); - } - } -} - -pub struct LanguageServerPrompt { - focus_handle: FocusHandle, - request: Option, - scroll_handle: ScrollHandle, -} - -impl Focusable for LanguageServerPrompt { - fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl Notification for LanguageServerPrompt {} - -impl LanguageServerPrompt { - pub fn new(request: project::LanguageServerPromptRequest, cx: &mut App) -> Self { - Self { - focus_handle: cx.focus_handle(), - request: Some(request), - scroll_handle: ScrollHandle::new(), - } - } - - async fn select_option(this: Entity, ix: usize, cx: &mut AsyncWindowContext) { - util::maybe!(async move { - let potential_future = this.update(cx, |this, _| { - this.request.take().map(|request| request.respond(ix)) - }); - - potential_future? // App Closed - .context("Response already sent")? - .await - .context("Stream already closed")?; - - this.update(cx, |_, cx| cx.emit(DismissEvent))?; - - anyhow::Ok(()) - }) - .await - .log_err(); - } -} - -impl Render for LanguageServerPrompt { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let Some(request) = &self.request else { - return div().id("language_server_prompt_notification"); - }; - - let (icon, color) = match request.level { - PromptLevel::Info => (IconName::Info, Color::Accent), - PromptLevel::Warning => (IconName::Warning, Color::Warning), - PromptLevel::Critical => (IconName::XCircle, Color::Error), - }; - - let suppress = window.modifiers().shift; - let (close_id, close_icon) = if suppress { - ("suppress", IconName::Minimize) - } else { - ("close", IconName::Close) - }; - - div() - .id("language_server_prompt_notification") - .group("language_server_prompt_notification") - .occlude() - .w_full() - .max_h(vh(0.8, window)) - .elevation_3(cx) - .overflow_y_scroll() - .track_scroll(&self.scroll_handle) - .on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify())) - .child( - v_flex() - .p_3() - .overflow_hidden() - .child( - h_flex() - .justify_between() - .items_start() - .child( - h_flex() - .gap_2() - .child(Icon::new(icon).color(color)) - .child(Label::new(request.lsp_name.clone())), - ) - .child( - h_flex() - .gap_2() - .child( - IconButton::new("copy", IconName::Copy) - .on_click({ - let message = request.message.clone(); - move |_, _, cx| { - cx.write_to_clipboard( - ClipboardItem::new_string(message.clone()), - ) - } - }) - .tooltip(Tooltip::text("Copy Description")), - ) - .child( - IconButton::new(close_id, close_icon) - .tooltip(move |_window, cx| { - if suppress { - Tooltip::for_action( - "Suppress.\nClose with click.", - &SuppressNotification, - cx, - ) - } else { - Tooltip::for_action( - "Close.\nSuppress with shift-click.", - &menu::Cancel, - cx, - ) - } - }) - .on_click(cx.listener( - move |_, _: &ClickEvent, _, cx| { - if suppress { - cx.emit(SuppressEvent); - } else { - cx.emit(DismissEvent); - } - }, - )), - ), - ), - ) - .child(Label::new(request.message.to_string()).size(LabelSize::Small)) - .children(request.actions.iter().enumerate().map(|(ix, action)| { - let this_handle = cx.entity(); - Button::new(ix, action.title.clone()) - .size(ButtonSize::Large) - .on_click(move |_, window, cx| { - let this_handle = this_handle.clone(); - window - .spawn(cx, async move |cx| { - LanguageServerPrompt::select_option(this_handle, ix, cx) - .await - }) - .detach() - }) - })), - ) - } -} - -impl EventEmitter for LanguageServerPrompt {} -impl EventEmitter for LanguageServerPrompt {} - -fn workspace_error_notification_id() -> NotificationId { - struct WorkspaceErrorNotification; - NotificationId::unique::() -} - -#[derive(Debug, Clone)] -pub struct ErrorMessagePrompt { - message: SharedString, - focus_handle: gpui::FocusHandle, - label_and_url_button: Option<(SharedString, SharedString)>, -} - -impl ErrorMessagePrompt { - pub fn new(message: S, cx: &mut App) -> Self - where - S: Into, - { - Self { - message: message.into(), - focus_handle: cx.focus_handle(), - label_and_url_button: None, - } - } - - pub fn with_link_button(mut self, label: S, url: S) -> Self - where - S: Into, - { - self.label_and_url_button = Some((label.into(), url.into())); - self - } -} - -impl Render for ErrorMessagePrompt { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - h_flex() - .id("error_message_prompt_notification") - .occlude() - .elevation_3(cx) - .items_start() - .justify_between() - .p_2() - .gap_2() - .w_full() - .child( - v_flex() - .w_full() - .child( - h_flex() - .w_full() - .justify_between() - .child( - svg() - .size(window.text_style().font_size) - .flex_none() - .mr_2() - .mt(px(-2.0)) - .map(|icon| { - icon.path(IconName::Warning.path()) - .text_color(Color::Error.color(cx)) - }), - ) - .child( - ui::IconButton::new("close", ui::IconName::Close) - .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), - ), - ) - .child( - div() - .id("error_message") - .max_w_96() - .max_h_40() - .overflow_y_scroll() - .child(Label::new(self.message.clone()).size(LabelSize::Small)), - ) - .when_some(self.label_and_url_button.clone(), |elm, (label, url)| { - elm.child( - div().mt_2().child( - ui::Button::new("error_message_prompt_notification_button", label) - .on_click(move |_, _, cx| cx.open_url(&url)), - ), - ) - }), - ) - } -} - -impl Focusable for ErrorMessagePrompt { - fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl EventEmitter for ErrorMessagePrompt {} -impl EventEmitter for ErrorMessagePrompt {} - -impl Notification for ErrorMessagePrompt {} - -#[derive(IntoElement, RegisterComponent)] -pub struct NotificationFrame { - title: Option, - show_suppress_button: bool, - show_close_button: bool, - close: Option>, - contents: Option, - suffix: Option, -} - -impl NotificationFrame { - pub fn new() -> Self { - Self { - title: None, - contents: None, - suffix: None, - show_suppress_button: true, - show_close_button: true, - close: None, - } - } - - pub fn with_title(mut self, title: Option>) -> Self { - self.title = title.map(Into::into); - self - } - - pub fn with_content(self, content: impl IntoElement) -> Self { - Self { - contents: Some(content.into_any_element()), - ..self - } - } - - /// Determines whether the given notification ID should be suppressible - /// Suppressed notifications will not be shown anymore - pub fn show_suppress_button(mut self, show: bool) -> Self { - self.show_suppress_button = show; - self - } - - pub fn show_close_button(mut self, show: bool) -> Self { - self.show_close_button = show; - self - } - - pub fn on_close(self, on_close: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { - Self { - close: Some(Box::new(on_close)), - ..self - } - } - - pub fn with_suffix(mut self, suffix: impl IntoElement) -> Self { - self.suffix = Some(suffix.into_any_element()); - self - } -} - -impl RenderOnce for NotificationFrame { - fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let entity = window.current_view(); - let show_suppress_button = self.show_suppress_button; - let suppress = show_suppress_button && window.modifiers().shift; - let (close_id, close_icon) = if suppress { - ("suppress", IconName::Minimize) - } else { - ("close", IconName::Close) - }; - - v_flex() - .occlude() - .p_3() - .gap_2() - .elevation_3(cx) - .child( - h_flex() - .gap_4() - .justify_between() - .items_start() - .child( - v_flex() - .gap_0p5() - .when_some(self.title.clone(), |div, title| { - div.child(Label::new(title)) - }) - .child(div().max_w_96().children(self.contents)), - ) - .when(self.show_close_button, |this| { - this.on_modifiers_changed(move |_, _, cx| cx.notify(entity)) - .child( - IconButton::new(close_id, close_icon) - .tooltip(move |_window, cx| { - if suppress { - Tooltip::for_action( - "Suppress.\nClose with click.", - &SuppressNotification, - cx, - ) - } else if show_suppress_button { - Tooltip::for_action( - "Close.\nSuppress with shift-click.", - &menu::Cancel, - cx, - ) - } else { - Tooltip::for_action("Close", &menu::Cancel, cx) - } - }) - .on_click({ - let close = self.close.take(); - move |_, window, cx| { - if let Some(close) = &close { - close(&suppress, window, cx) - } - } - }), - ) - }), - ) - .children(self.suffix) - } -} - -impl Component for NotificationFrame {} - -pub mod simple_message_notification { - use std::sync::Arc; - - use gpui::{ - AnyElement, DismissEvent, EventEmitter, FocusHandle, Focusable, ParentElement, Render, - ScrollHandle, SharedString, Styled, - }; - use ui::{WithScrollbar, prelude::*}; - - use crate::notifications::NotificationFrame; - - use super::{Notification, SuppressEvent}; - - pub struct MessageNotification { - focus_handle: FocusHandle, - build_content: Box) -> AnyElement>, - primary_message: Option, - primary_icon: Option, - primary_icon_color: Option, - primary_on_click: Option)>>, - secondary_message: Option, - secondary_icon: Option, - secondary_icon_color: Option, - secondary_on_click: Option)>>, - more_info_message: Option, - more_info_url: Option>, - show_close_button: bool, - show_suppress_button: bool, - title: Option, - scroll_handle: ScrollHandle, - } - - impl Focusable for MessageNotification { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } - } - - impl EventEmitter for MessageNotification {} - impl EventEmitter for MessageNotification {} - - impl Notification for MessageNotification {} - - impl MessageNotification { - pub fn new(message: S, cx: &mut App) -> MessageNotification - where - S: Into, - { - let message = message.into(); - Self::new_from_builder(cx, move |_, _| { - Label::new(message.clone()).into_any_element() - }) - } - - pub fn new_from_builder(cx: &mut App, content: F) -> MessageNotification - where - F: 'static + Fn(&mut Window, &mut Context) -> AnyElement, - { - Self { - build_content: Box::new(content), - primary_message: None, - primary_icon: None, - primary_icon_color: None, - primary_on_click: None, - secondary_message: None, - secondary_icon: None, - secondary_icon_color: None, - secondary_on_click: None, - more_info_message: None, - more_info_url: None, - show_close_button: true, - show_suppress_button: true, - title: None, - focus_handle: cx.focus_handle(), - scroll_handle: ScrollHandle::new(), - } - } - - pub fn primary_message(mut self, message: S) -> Self - where - S: Into, - { - self.primary_message = Some(message.into()); - self - } - - pub fn primary_icon(mut self, icon: IconName) -> Self { - self.primary_icon = Some(icon); - self - } - - pub fn primary_icon_color(mut self, color: Color) -> Self { - self.primary_icon_color = Some(color); - self - } - - pub fn primary_on_click(mut self, on_click: F) -> Self - where - F: 'static + Fn(&mut Window, &mut Context), - { - self.primary_on_click = Some(Arc::new(on_click)); - self - } - - pub fn primary_on_click_arc(mut self, on_click: Arc) -> Self - where - F: 'static + Fn(&mut Window, &mut Context), - { - self.primary_on_click = Some(on_click); - self - } - - pub fn secondary_message(mut self, message: S) -> Self - where - S: Into, - { - self.secondary_message = Some(message.into()); - self - } - - pub fn secondary_icon(mut self, icon: IconName) -> Self { - self.secondary_icon = Some(icon); - self - } - - pub fn secondary_icon_color(mut self, color: Color) -> Self { - self.secondary_icon_color = Some(color); - self - } - - pub fn secondary_on_click(mut self, on_click: F) -> Self - where - F: 'static + Fn(&mut Window, &mut Context), - { - self.secondary_on_click = Some(Arc::new(on_click)); - self - } - - pub fn secondary_on_click_arc(mut self, on_click: Arc) -> Self - where - F: 'static + Fn(&mut Window, &mut Context), - { - self.secondary_on_click = Some(on_click); - self - } - - pub fn more_info_message(mut self, message: S) -> Self - where - S: Into, - { - self.more_info_message = Some(message.into()); - self - } - - pub fn more_info_url(mut self, url: S) -> Self - where - S: Into>, - { - self.more_info_url = Some(url.into()); - self - } - - pub fn dismiss(&mut self, cx: &mut Context) { - cx.emit(DismissEvent); - } - - pub fn show_close_button(mut self, show: bool) -> Self { - self.show_close_button = show; - self - } - - /// Determines whether the given notification ID should be suppressible - /// Suppressed notifications will not be shown anymor - pub fn show_suppress_button(mut self, show: bool) -> Self { - self.show_suppress_button = show; - self - } - - pub fn with_title(mut self, title: S) -> Self - where - S: Into, - { - self.title = Some(title.into()); - self - } - } - - impl Render for MessageNotification { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - NotificationFrame::new() - .with_title(self.title.clone()) - .with_content( - div() - .child( - div() - .id("message-notification-content") - .max_h(vh(0.6, window)) - .overflow_y_scroll() - .track_scroll(&self.scroll_handle.clone()) - .child((self.build_content)(window, cx)), - ) - .vertical_scrollbar_for(&self.scroll_handle, window, cx), - ) - .show_close_button(self.show_close_button) - .show_suppress_button(self.show_suppress_button) - .on_close(cx.listener(|_, suppress, _, cx| { - if *suppress { - cx.emit(SuppressEvent); - } else { - cx.emit(DismissEvent); - } - })) - .with_suffix( - h_flex() - .gap_1() - .children(self.primary_message.iter().map(|message| { - let mut button = Button::new(message.clone(), message.clone()) - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - if let Some(on_click) = this.primary_on_click.as_ref() { - (on_click)(window, cx) - }; - this.dismiss(cx) - })); - - if let Some(icon) = self.primary_icon { - button = button - .icon(icon) - .icon_color(self.primary_icon_color.unwrap_or(Color::Muted)) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small); - } - - button - })) - .children(self.secondary_message.iter().map(|message| { - let mut button = Button::new(message.clone(), message.clone()) - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - if let Some(on_click) = this.secondary_on_click.as_ref() { - (on_click)(window, cx) - }; - this.dismiss(cx) - })); - - if let Some(icon) = self.secondary_icon { - button = button - .icon(icon) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(self.secondary_icon_color.unwrap_or(Color::Muted)); - } - - button - })) - .child( - h_flex().w_full().justify_end().children( - self.more_info_message - .iter() - .zip(self.more_info_url.iter()) - .map(|(message, url)| { - let url = url.clone(); - Button::new(message.clone(), message.clone()) - .label_size(LabelSize::Small) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Indicator) - .icon_color(Color::Muted) - .on_click(cx.listener(move |_, _, _, cx| { - cx.open_url(&url); - })) - }), - ), - ), - ) - } - } -} - -static GLOBAL_APP_NOTIFICATIONS: LazyLock> = LazyLock::new(|| { - Mutex::new(AppNotifications { - app_notifications: Vec::new(), - }) -}); - -/// Stores app notifications so that they can be shown in new workspaces. -struct AppNotifications { - app_notifications: Vec<( - NotificationId, - Arc) -> AnyView + Send + Sync>, - )>, -} - -impl AppNotifications { - pub fn insert( - &mut self, - id: NotificationId, - build_notification: Arc) -> AnyView + Send + Sync>, - ) { - self.remove(&id); - self.app_notifications.push((id, build_notification)) - } - - pub fn remove(&mut self, id: &NotificationId) { - self.app_notifications - .retain(|(existing_id, _)| existing_id != id); - } -} - -/// Shows a notification in all workspaces. New workspaces will also receive the notification - this -/// is particularly to handle notifications that occur on initialization before any workspaces -/// exist. If the notification is dismissed within any workspace, it will be removed from all. -pub fn show_app_notification( - id: NotificationId, - cx: &mut App, - build_notification: impl Fn(&mut Context) -> Entity + 'static + Send + Sync, -) { - // Defer notification creation so that windows on the stack can be returned to GPUI - cx.defer(move |cx| { - // Handle dismiss events by removing the notification from all workspaces. - let build_notification: Arc) -> AnyView + Send + Sync> = - Arc::new({ - let id = id.clone(); - move |cx| { - let notification = build_notification(cx); - cx.subscribe(¬ification, { - let id = id.clone(); - move |_, _, _: &DismissEvent, cx| { - dismiss_app_notification(&id, cx); - } - }) - .detach(); - cx.subscribe(¬ification, { - let id = id.clone(); - move |workspace: &mut Workspace, _, _: &SuppressEvent, cx| { - workspace.suppress_notification(&id, cx); - } - }) - .detach(); - notification.into() - } - }); - - // Store the notification so that new workspaces also receive it. - GLOBAL_APP_NOTIFICATIONS - .lock() - .insert(id.clone(), build_notification.clone()); - - for window in cx.windows() { - if let Some(workspace_window) = window.downcast::() { - workspace_window - .update(cx, |workspace, _window, cx| { - workspace.show_notification_without_handling_dismiss_events( - &id, - cx, - |cx| build_notification(cx), - ); - }) - .ok(); // Doesn't matter if the windows are dropped - } - } - }); -} - -pub fn dismiss_app_notification(id: &NotificationId, cx: &mut App) { - let id = id.clone(); - // Defer notification dismissal so that windows on the stack can be returned to GPUI - cx.defer(move |cx| { - GLOBAL_APP_NOTIFICATIONS.lock().remove(&id); - for window in cx.windows() { - if let Some(workspace_window) = window.downcast::() { - let id = id.clone(); - workspace_window - .update(cx, |workspace, _window, cx| { - workspace.dismiss_notification(&id, cx) - }) - .ok(); - } - } - }); -} - -pub trait NotifyResultExt { - type Ok; - - fn notify_err(self, workspace: &mut Workspace, cx: &mut Context) - -> Option; - - fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option; - - /// Notifies the active workspace if there is one, otherwise notifies all workspaces. - fn notify_app_err(self, cx: &mut App) -> Option; -} - -impl NotifyResultExt for std::result::Result -where - E: std::fmt::Debug + std::fmt::Display, -{ - type Ok = T; - - fn notify_err(self, workspace: &mut Workspace, cx: &mut Context) -> Option { - match self { - Ok(value) => Some(value), - Err(err) => { - log::error!("Showing error notification in workspace: {err:?}"); - workspace.show_error(&err, cx); - None - } - } - } - - fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option { - match self { - Ok(value) => Some(value), - Err(err) => { - log::error!("{err:?}"); - cx.update_root(|view, _, cx| { - if let Ok(workspace) = view.downcast::() { - workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx)) - } - }) - .ok(); - None - } - } - } - - fn notify_app_err(self, cx: &mut App) -> Option { - match self { - Ok(value) => Some(value), - Err(err) => { - let message: SharedString = format!("Error: {err}").into(); - log::error!("Showing error notification in app: {message}"); - show_app_notification(workspace_error_notification_id(), cx, { - move |cx| { - cx.new({ - let message = message.clone(); - move |cx| ErrorMessagePrompt::new(message, cx) - }) - } - }); - - None - } - } - } -} - -pub trait NotifyTaskExt { - fn detach_and_notify_err(self, window: &mut Window, cx: &mut App); -} - -impl NotifyTaskExt for Task> -where - E: std::fmt::Debug + std::fmt::Display + Sized + 'static, - R: 'static, -{ - fn detach_and_notify_err(self, window: &mut Window, cx: &mut App) { - window - .spawn(cx, async move |cx| self.await.notify_async_err(cx)) - .detach(); - } -} - -pub trait DetachAndPromptErr { - fn prompt_err( - self, - msg: &str, - window: &Window, - cx: &App, - f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option + 'static, - ) -> Task>; - - fn detach_and_prompt_err( - self, - msg: &str, - window: &Window, - cx: &App, - f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option + 'static, - ); -} - -impl DetachAndPromptErr for Task> -where - R: 'static, -{ - fn prompt_err( - self, - msg: &str, - window: &Window, - cx: &App, - f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option + 'static, - ) -> Task> { - let msg = msg.to_owned(); - window.spawn(cx, async move |cx| { - let result = self.await; - if let Err(err) = result.as_ref() { - log::error!("{err:#}"); - if let Ok(prompt) = cx.update(|window, cx| { - let mut display = format!("{err:#}"); - if !display.ends_with('\n') { - display.push('.'); - display.push(' ') - } - let detail = - f(err, window, cx).unwrap_or_else(|| format!("{display}Please try again.")); - window.prompt(PromptLevel::Critical, &msg, Some(&detail), &["Ok"], cx) - }) { - prompt.await.ok(); - } - return None; - } - Some(result.unwrap()) - }) - } - - fn detach_and_prompt_err( - self, - msg: &str, - window: &Window, - cx: &App, - f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option + 'static, - ) { - self.prompt_err(msg, window, cx, f).detach(); - } -} diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs deleted file mode 100644 index e99f8d1dc9..0000000000 --- a/crates/workspace/src/pane.rs +++ /dev/null @@ -1,7010 +0,0 @@ -use crate::{ - CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible, - SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace, - WorkspaceItemBuilder, - invalid_item_view::InvalidItemView, - item::{ - ActivateOnClose, ClosePosition, Item, ItemBufferKind, ItemHandle, ItemSettings, - PreviewTabsSettings, ProjectItemKind, SaveOptions, ShowCloseButton, ShowDiagnostics, - TabContentParams, TabTooltipContent, WeakItemHandle, - }, - move_item, - notifications::NotifyResultExt, - toolbar::Toolbar, - workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings}, -}; -use anyhow::Result; -use collections::{BTreeSet, HashMap, HashSet, VecDeque}; -use futures::{StreamExt, stream::FuturesUnordered}; -use gpui::{ - Action, AnyElement, App, AsyncWindowContext, ClickEvent, ClipboardItem, Context, Corner, Div, - DragMoveEvent, Entity, EntityId, EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent, - Focusable, KeyContext, MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point, - PromptLevel, Render, ScrollHandle, Subscription, Task, WeakEntity, WeakFocusHandle, Window, - actions, anchored, deferred, prelude::*, -}; -use itertools::Itertools; -use language::DiagnosticSeverity; -use parking_lot::Mutex; -use project::{DirectoryLister, Project, ProjectEntryId, ProjectPath, WorktreeId}; -use schemars::JsonSchema; -use serde::Deserialize; -use settings::{Settings, SettingsStore}; -use std::{ - any::Any, - cmp, fmt, mem, - num::NonZeroUsize, - ops::ControlFlow, - path::PathBuf, - rc::Rc, - sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, - }, - time::Duration, -}; -use theme::ThemeSettings; -use ui::{ - ButtonSize, Color, ContextMenu, ContextMenuEntry, ContextMenuItem, DecoratedIcon, IconButton, - IconButtonShape, IconDecoration, IconDecorationKind, IconName, IconSize, Indicator, Label, - PopoverMenu, PopoverMenuHandle, Tab, TabBar, TabPosition, Tooltip, prelude::*, - right_click_menu, -}; -use util::{ResultExt, debug_panic, maybe, paths::PathStyle, truncate_and_remove_front}; - -/// A selected entry in e.g. project panel. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct SelectedEntry { - pub worktree_id: WorktreeId, - pub entry_id: ProjectEntryId, -} - -/// A group of selected entries from project panel. -#[derive(Debug)] -pub struct DraggedSelection { - pub active_selection: SelectedEntry, - pub marked_selections: Arc<[SelectedEntry]>, -} - -impl DraggedSelection { - pub fn items<'a>(&'a self) -> Box + 'a> { - if self.marked_selections.contains(&self.active_selection) { - Box::new(self.marked_selections.iter()) - } else { - Box::new(std::iter::once(&self.active_selection)) - } - } -} - -#[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum SaveIntent { - /// write all files (even if unchanged) - /// prompt before overwriting on-disk changes - Save, - /// same as Save, but without auto formatting - SaveWithoutFormat, - /// write any files that have local changes - /// prompt before overwriting on-disk changes - SaveAll, - /// always prompt for a new path - SaveAs, - /// prompt "you have unsaved changes" before writing - Close, - /// write all dirty files, don't prompt on conflict - Overwrite, - /// skip all save-related behavior - Skip, -} - -/// Activates a specific item in the pane by its index. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = pane)] -pub struct ActivateItem(pub usize); - -/// Closes the currently active item in the pane. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = pane)] -#[serde(deny_unknown_fields)] -pub struct CloseActiveItem { - #[serde(default)] - pub save_intent: Option, - #[serde(default)] - pub close_pinned: bool, -} - -/// Closes all inactive items in the pane. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = pane)] -#[serde(deny_unknown_fields)] -#[action(deprecated_aliases = ["pane::CloseInactiveItems"])] -pub struct CloseOtherItems { - #[serde(default)] - pub save_intent: Option, - #[serde(default)] - pub close_pinned: bool, -} - -/// Closes all multibuffers in the pane. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = pane)] -#[serde(deny_unknown_fields)] -pub struct CloseMultibufferItems { - #[serde(default)] - pub save_intent: Option, - #[serde(default)] - pub close_pinned: bool, -} - -/// Closes all items in the pane. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = pane)] -#[serde(deny_unknown_fields)] -pub struct CloseAllItems { - #[serde(default)] - pub save_intent: Option, - #[serde(default)] - pub close_pinned: bool, -} - -/// Closes all items that have no unsaved changes. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = pane)] -#[serde(deny_unknown_fields)] -pub struct CloseCleanItems { - #[serde(default)] - pub close_pinned: bool, -} - -/// Closes all items to the right of the current item. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = pane)] -#[serde(deny_unknown_fields)] -pub struct CloseItemsToTheRight { - #[serde(default)] - pub close_pinned: bool, -} - -/// Closes all items to the left of the current item. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = pane)] -#[serde(deny_unknown_fields)] -pub struct CloseItemsToTheLeft { - #[serde(default)] - pub close_pinned: bool, -} - -/// Reveals the current item in the project panel. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = pane)] -#[serde(deny_unknown_fields)] -pub struct RevealInProjectPanel { - #[serde(skip)] - pub entry_id: Option, -} - -/// Opens the search interface with the specified configuration. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = pane)] -#[serde(deny_unknown_fields)] -pub struct DeploySearch { - #[serde(default)] - pub replace_enabled: bool, - #[serde(default)] - pub included_files: Option, - #[serde(default)] - pub excluded_files: Option, -} - -actions!( - pane, - [ - /// Activates the previous item in the pane. - ActivatePreviousItem, - /// Activates the next item in the pane. - ActivateNextItem, - /// Activates the last item in the pane. - ActivateLastItem, - /// Switches to the alternate file. - AlternateFile, - /// Navigates back in history. - GoBack, - /// Navigates forward in history. - GoForward, - /// Joins this pane into the next pane. - JoinIntoNext, - /// Joins all panes into one. - JoinAll, - /// Reopens the most recently closed item. - ReopenClosedItem, - /// Splits the pane to the left, cloning the current item. - SplitLeft, - /// Splits the pane upward, cloning the current item. - SplitUp, - /// Splits the pane to the right, cloning the current item. - SplitRight, - /// Splits the pane downward, cloning the current item. - SplitDown, - /// Splits the pane to the left, moving the current item. - SplitAndMoveLeft, - /// Splits the pane upward, moving the current item. - SplitAndMoveUp, - /// Splits the pane to the right, moving the current item. - SplitAndMoveRight, - /// Splits the pane downward, moving the current item. - SplitAndMoveDown, - /// Splits the pane horizontally. - SplitHorizontal, - /// Splits the pane vertically. - SplitVertical, - /// Swaps the current item with the one to the left. - SwapItemLeft, - /// Swaps the current item with the one to the right. - SwapItemRight, - /// Toggles preview mode for the current tab. - TogglePreviewTab, - /// Toggles pin status for the current tab. - TogglePinTab, - /// Unpins all tabs in the pane. - UnpinAllTabs, - ] -); - -impl DeploySearch { - pub fn find() -> Self { - Self { - replace_enabled: false, - included_files: None, - excluded_files: None, - } - } -} - -const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; - -pub enum Event { - AddItem { - item: Box, - }, - ActivateItem { - local: bool, - focus_changed: bool, - }, - Remove { - focus_on_pane: Option>, - }, - RemovedItem { - item: Box, - }, - Split { - direction: SplitDirection, - clone_active_item: bool, - }, - ItemPinned, - ItemUnpinned, - JoinAll, - JoinIntoNext, - ChangeItemTitle, - Focus, - ZoomIn, - ZoomOut, - UserSavedItem { - item: Box, - save_intent: SaveIntent, - }, -} - -impl fmt::Debug for Event { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Event::AddItem { item } => f - .debug_struct("AddItem") - .field("item", &item.item_id()) - .finish(), - Event::ActivateItem { local, .. } => f - .debug_struct("ActivateItem") - .field("local", local) - .finish(), - Event::Remove { .. } => f.write_str("Remove"), - Event::RemovedItem { item } => f - .debug_struct("RemovedItem") - .field("item", &item.item_id()) - .finish(), - Event::Split { - direction, - clone_active_item, - } => f - .debug_struct("Split") - .field("direction", direction) - .field("clone_active_item", clone_active_item) - .finish(), - Event::JoinAll => f.write_str("JoinAll"), - Event::JoinIntoNext => f.write_str("JoinIntoNext"), - Event::ChangeItemTitle => f.write_str("ChangeItemTitle"), - Event::Focus => f.write_str("Focus"), - Event::ZoomIn => f.write_str("ZoomIn"), - Event::ZoomOut => f.write_str("ZoomOut"), - Event::UserSavedItem { item, save_intent } => f - .debug_struct("UserSavedItem") - .field("item", &item.id()) - .field("save_intent", save_intent) - .finish(), - Event::ItemPinned => f.write_str("ItemPinned"), - Event::ItemUnpinned => f.write_str("ItemUnpinned"), - } - } -} - -/// A container for 0 to many items that are open in the workspace. -/// Treats all items uniformly via the [`ItemHandle`] trait, whether it's an editor, search results multibuffer, terminal or something else, -/// responsible for managing item tabs, focus and zoom states and drag and drop features. -/// Can be split, see `PaneGroup` for more details. -pub struct Pane { - alternate_file_items: ( - Option>, - Option>, - ), - focus_handle: FocusHandle, - items: Vec>, - activation_history: Vec, - next_activation_timestamp: Arc, - zoomed: bool, - was_focused: bool, - active_item_index: usize, - preview_item_id: Option, - last_focus_handle_by_item: HashMap, - nav_history: NavHistory, - toolbar: Entity, - pub(crate) workspace: WeakEntity, - project: WeakEntity, - pub drag_split_direction: Option, - can_drop_predicate: Option bool>>, - custom_drop_handle: Option< - Arc) -> ControlFlow<(), ()>>, - >, - can_split_predicate: - Option) -> bool>>, - can_toggle_zoom: bool, - should_display_tab_bar: Rc) -> bool>, - render_tab_bar_buttons: Rc< - dyn Fn( - &mut Pane, - &mut Window, - &mut Context, - ) -> (Option, Option), - >, - render_tab_bar: Rc) -> AnyElement>, - show_tab_bar_buttons: bool, - max_tabs: Option, - use_max_tabs: bool, - _subscriptions: Vec, - tab_bar_scroll_handle: ScrollHandle, - /// This is set to true if a user scroll has occurred more recently than a system scroll - /// We want to suppress certain system scrolls when the user has intentionally scrolled - suppress_scroll: bool, - /// Is None if navigation buttons are permanently turned off (and should not react to setting changes). - /// Otherwise, when `display_nav_history_buttons` is Some, it determines whether nav buttons should be displayed. - display_nav_history_buttons: Option, - double_click_dispatch_action: Box, - save_modals_spawned: HashSet, - close_pane_if_empty: bool, - pub new_item_context_menu_handle: PopoverMenuHandle, - pub split_item_context_menu_handle: PopoverMenuHandle, - pinned_tab_count: usize, - diagnostics: HashMap, - zoom_out_on_close: bool, - diagnostic_summary_update: Task<()>, - /// If a certain project item wants to get recreated with specific data, it can persist its data before the recreation here. - pub project_item_restoration_data: HashMap>, -} - -pub struct ActivationHistoryEntry { - pub entity_id: EntityId, - pub timestamp: usize, -} - -pub struct ItemNavHistory { - history: NavHistory, - item: Arc, - is_preview: bool, -} - -#[derive(Clone)] -pub struct NavHistory(Arc>); - -struct NavHistoryState { - mode: NavigationMode, - backward_stack: VecDeque, - forward_stack: VecDeque, - closed_stack: VecDeque, - paths_by_item: HashMap)>, - pane: WeakEntity, - next_timestamp: Arc, -} - -#[derive(Debug, Default, Copy, Clone)] -pub enum NavigationMode { - #[default] - Normal, - GoingBack, - GoingForward, - ClosingItem, - ReopeningClosedItem, - Disabled, -} - -pub struct NavigationEntry { - pub item: Arc, - pub data: Option>, - pub timestamp: usize, - pub is_preview: bool, -} - -#[derive(Clone)] -pub struct DraggedTab { - pub pane: Entity, - pub item: Box, - pub ix: usize, - pub detail: usize, - pub is_active: bool, -} - -impl EventEmitter for Pane {} - -pub enum Side { - Left, - Right, -} - -#[derive(Copy, Clone)] -enum PinOperation { - Pin, - Unpin, -} - -impl Pane { - pub fn new( - workspace: WeakEntity, - project: Entity, - next_timestamp: Arc, - can_drop_predicate: Option bool + 'static>>, - double_click_dispatch_action: Box, - use_max_tabs: bool, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let focus_handle = cx.focus_handle(); - let max_tabs = if use_max_tabs { - WorkspaceSettings::get_global(cx).max_tabs - } else { - None - }; - - let subscriptions = vec![ - cx.on_focus(&focus_handle, window, Pane::focus_in), - cx.on_focus_in(&focus_handle, window, Pane::focus_in), - cx.on_focus_out(&focus_handle, window, Pane::focus_out), - cx.observe_global_in::(window, Self::settings_changed), - cx.subscribe(&project, Self::project_events), - ]; - - let handle = cx.entity().downgrade(); - - Self { - alternate_file_items: (None, None), - focus_handle, - items: Vec::new(), - activation_history: Vec::new(), - next_activation_timestamp: next_timestamp.clone(), - was_focused: false, - zoomed: false, - active_item_index: 0, - preview_item_id: None, - max_tabs, - use_max_tabs, - last_focus_handle_by_item: Default::default(), - nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState { - mode: NavigationMode::Normal, - backward_stack: Default::default(), - forward_stack: Default::default(), - closed_stack: Default::default(), - paths_by_item: Default::default(), - pane: handle, - next_timestamp, - }))), - toolbar: cx.new(|_| Toolbar::new()), - tab_bar_scroll_handle: ScrollHandle::new(), - suppress_scroll: false, - drag_split_direction: None, - workspace, - project: project.downgrade(), - can_drop_predicate, - custom_drop_handle: None, - can_split_predicate: None, - can_toggle_zoom: true, - should_display_tab_bar: Rc::new(|_, cx| TabBarSettings::get_global(cx).show), - render_tab_bar_buttons: Rc::new(default_render_tab_bar_buttons), - render_tab_bar: Rc::new(Self::render_tab_bar), - show_tab_bar_buttons: TabBarSettings::get_global(cx).show_tab_bar_buttons, - display_nav_history_buttons: Some( - TabBarSettings::get_global(cx).show_nav_history_buttons, - ), - _subscriptions: subscriptions, - double_click_dispatch_action, - save_modals_spawned: HashSet::default(), - close_pane_if_empty: true, - split_item_context_menu_handle: Default::default(), - new_item_context_menu_handle: Default::default(), - pinned_tab_count: 0, - diagnostics: Default::default(), - zoom_out_on_close: true, - diagnostic_summary_update: Task::ready(()), - project_item_restoration_data: HashMap::default(), - } - } - - fn alternate_file(&mut self, _: &AlternateFile, window: &mut Window, cx: &mut Context) { - let (_, alternative) = &self.alternate_file_items; - if let Some(alternative) = alternative { - let existing = self - .items() - .find_position(|item| item.item_id() == alternative.id()); - if let Some((ix, _)) = existing { - self.activate_item(ix, true, true, window, cx); - } else if let Some(upgraded) = alternative.upgrade() { - self.add_item(upgraded, true, true, None, window, cx); - } - } - } - - pub fn track_alternate_file_items(&mut self) { - if let Some(item) = self.active_item().map(|item| item.downgrade_item()) { - let (current, _) = &self.alternate_file_items; - match current { - Some(current) => { - if current.id() != item.id() { - self.alternate_file_items = - (Some(item), self.alternate_file_items.0.take()); - } - } - None => { - self.alternate_file_items = (Some(item), None); - } - } - } - } - - pub fn has_focus(&self, window: &Window, cx: &App) -> bool { - // We not only check whether our focus handle contains focus, but also - // whether the active item might have focus, because we might have just activated an item - // that hasn't rendered yet. - // Before the next render, we might transfer focus - // to the item, and `focus_handle.contains_focus` returns false because the `active_item` - // is not hooked up to us in the dispatch tree. - self.focus_handle.contains_focused(window, cx) - || self - .active_item() - .is_some_and(|item| item.item_focus_handle(cx).contains_focused(window, cx)) - } - - fn focus_in(&mut self, window: &mut Window, cx: &mut Context) { - if !self.was_focused { - self.was_focused = true; - self.update_history(self.active_item_index); - if !self.suppress_scroll && self.items.get(self.active_item_index).is_some() { - self.update_active_tab(self.active_item_index); - } - cx.emit(Event::Focus); - cx.notify(); - } - - self.toolbar.update(cx, |toolbar, cx| { - toolbar.focus_changed(true, window, cx); - }); - - if let Some(active_item) = self.active_item() { - if self.focus_handle.is_focused(window) { - // Schedule a redraw next frame, so that the focus changes below take effect - cx.on_next_frame(window, |_, _, cx| { - cx.notify(); - }); - - // Pane was focused directly. We need to either focus a view inside the active item, - // or focus the active item itself - if let Some(weak_last_focus_handle) = - self.last_focus_handle_by_item.get(&active_item.item_id()) - && let Some(focus_handle) = weak_last_focus_handle.upgrade() - { - focus_handle.focus(window); - return; - } - - active_item.item_focus_handle(cx).focus(window); - } else if let Some(focused) = window.focused(cx) - && !self.context_menu_focused(window, cx) - { - self.last_focus_handle_by_item - .insert(active_item.item_id(), focused.downgrade()); - } - } - } - - pub fn context_menu_focused(&self, window: &mut Window, cx: &mut Context) -> bool { - self.new_item_context_menu_handle.is_focused(window, cx) - || self.split_item_context_menu_handle.is_focused(window, cx) - } - - fn focus_out(&mut self, _event: FocusOutEvent, window: &mut Window, cx: &mut Context) { - self.was_focused = false; - self.toolbar.update(cx, |toolbar, cx| { - toolbar.focus_changed(false, window, cx); - }); - - cx.notify(); - } - - fn project_events( - &mut self, - _project: Entity, - event: &project::Event, - cx: &mut Context, - ) { - match event { - project::Event::DiskBasedDiagnosticsFinished { .. } - | project::Event::DiagnosticsUpdated { .. } => { - if ItemSettings::get_global(cx).show_diagnostics != ShowDiagnostics::Off { - self.diagnostic_summary_update = cx.spawn(async move |this, cx| { - cx.background_executor() - .timer(Duration::from_millis(30)) - .await; - this.update(cx, |this, cx| { - this.update_diagnostics(cx); - cx.notify(); - }) - .log_err(); - }); - } - } - _ => {} - } - } - - fn update_diagnostics(&mut self, cx: &mut Context) { - let Some(project) = self.project.upgrade() else { - return; - }; - let show_diagnostics = ItemSettings::get_global(cx).show_diagnostics; - self.diagnostics = if show_diagnostics != ShowDiagnostics::Off { - project - .read(cx) - .diagnostic_summaries(false, cx) - .filter_map(|(project_path, _, diagnostic_summary)| { - if diagnostic_summary.error_count > 0 { - Some((project_path, DiagnosticSeverity::ERROR)) - } else if diagnostic_summary.warning_count > 0 - && show_diagnostics != ShowDiagnostics::Errors - { - Some((project_path, DiagnosticSeverity::WARNING)) - } else { - None - } - }) - .collect() - } else { - HashMap::default() - } - } - - fn settings_changed(&mut self, window: &mut Window, cx: &mut Context) { - let tab_bar_settings = TabBarSettings::get_global(cx); - let new_max_tabs = WorkspaceSettings::get_global(cx).max_tabs; - - if let Some(display_nav_history_buttons) = self.display_nav_history_buttons.as_mut() { - *display_nav_history_buttons = tab_bar_settings.show_nav_history_buttons; - } - - self.show_tab_bar_buttons = tab_bar_settings.show_tab_bar_buttons; - - if !PreviewTabsSettings::get_global(cx).enabled { - self.preview_item_id = None; - } - - if self.use_max_tabs && new_max_tabs != self.max_tabs { - self.max_tabs = new_max_tabs; - self.close_items_on_settings_change(window, cx); - } - - self.update_diagnostics(cx); - cx.notify(); - } - - pub fn active_item_index(&self) -> usize { - self.active_item_index - } - - pub fn activation_history(&self) -> &[ActivationHistoryEntry] { - &self.activation_history - } - - pub fn set_should_display_tab_bar(&mut self, should_display_tab_bar: F) - where - F: 'static + Fn(&Window, &mut Context) -> bool, - { - self.should_display_tab_bar = Rc::new(should_display_tab_bar); - } - - pub fn set_can_split( - &mut self, - can_split_predicate: Option< - Arc) -> bool + 'static>, - >, - ) { - self.can_split_predicate = can_split_predicate; - } - - pub fn set_can_toggle_zoom(&mut self, can_toggle_zoom: bool, cx: &mut Context) { - self.can_toggle_zoom = can_toggle_zoom; - cx.notify(); - } - - pub fn set_close_pane_if_empty(&mut self, close_pane_if_empty: bool, cx: &mut Context) { - self.close_pane_if_empty = close_pane_if_empty; - cx.notify(); - } - - pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut Context) { - self.toolbar.update(cx, |toolbar, cx| { - toolbar.set_can_navigate(can_navigate, cx); - }); - cx.notify(); - } - - pub fn set_render_tab_bar(&mut self, cx: &mut Context, render: F) - where - F: 'static + Fn(&mut Pane, &mut Window, &mut Context) -> AnyElement, - { - self.render_tab_bar = Rc::new(render); - cx.notify(); - } - - pub fn set_render_tab_bar_buttons(&mut self, cx: &mut Context, render: F) - where - F: 'static - + Fn( - &mut Pane, - &mut Window, - &mut Context, - ) -> (Option, Option), - { - self.render_tab_bar_buttons = Rc::new(render); - cx.notify(); - } - - pub fn set_custom_drop_handle(&mut self, cx: &mut Context, handle: F) - where - F: 'static - + Fn(&mut Pane, &dyn Any, &mut Window, &mut Context) -> ControlFlow<(), ()>, - { - self.custom_drop_handle = Some(Arc::new(handle)); - cx.notify(); - } - - pub fn nav_history_for_item(&self, item: &Entity) -> ItemNavHistory { - ItemNavHistory { - history: self.nav_history.clone(), - item: Arc::new(item.downgrade()), - is_preview: self.preview_item_id == Some(item.item_id()), - } - } - - pub fn nav_history(&self) -> &NavHistory { - &self.nav_history - } - - pub fn nav_history_mut(&mut self) -> &mut NavHistory { - &mut self.nav_history - } - - pub fn disable_history(&mut self) { - self.nav_history.disable(); - } - - pub fn enable_history(&mut self) { - self.nav_history.enable(); - } - - pub fn can_navigate_backward(&self) -> bool { - !self.nav_history.0.lock().backward_stack.is_empty() - } - - pub fn can_navigate_forward(&self) -> bool { - !self.nav_history.0.lock().forward_stack.is_empty() - } - - pub fn navigate_backward(&mut self, _: &GoBack, window: &mut Window, cx: &mut Context) { - if let Some(workspace) = self.workspace.upgrade() { - let pane = cx.entity().downgrade(); - window.defer(cx, move |window, cx| { - workspace.update(cx, |workspace, cx| { - workspace.go_back(pane, window, cx).detach_and_log_err(cx) - }) - }) - } - } - - fn navigate_forward(&mut self, _: &GoForward, window: &mut Window, cx: &mut Context) { - if let Some(workspace) = self.workspace.upgrade() { - let pane = cx.entity().downgrade(); - window.defer(cx, move |window, cx| { - workspace.update(cx, |workspace, cx| { - workspace - .go_forward(pane, window, cx) - .detach_and_log_err(cx) - }) - }) - } - } - - fn history_updated(&mut self, cx: &mut Context) { - self.toolbar.update(cx, |_, cx| cx.notify()); - } - - pub fn preview_item_id(&self) -> Option { - self.preview_item_id - } - - pub fn preview_item(&self) -> Option> { - self.preview_item_id - .and_then(|id| self.items.iter().find(|item| item.item_id() == id)) - .cloned() - } - - pub fn preview_item_idx(&self) -> Option { - if let Some(preview_item_id) = self.preview_item_id { - self.items - .iter() - .position(|item| item.item_id() == preview_item_id) - } else { - None - } - } - - pub fn is_active_preview_item(&self, item_id: EntityId) -> bool { - self.preview_item_id == Some(item_id) - } - - /// Promotes the item with the given ID to not be a preview item. - /// This does nothing if it wasn't already a preview item. - pub fn unpreview_item_if_preview(&mut self, item_id: EntityId) { - if self.is_active_preview_item(item_id) { - self.preview_item_id = None; - } - } - - /// Marks the item with the given ID as the preview item. - /// This will be ignored if the global setting `preview_tabs` is disabled. - /// - /// The old preview item (if there was one) is closed and its index is returned. - pub fn replace_preview_item_id( - &mut self, - item_id: EntityId, - window: &mut Window, - cx: &mut Context, - ) -> Option { - let idx = self.close_current_preview_item(window, cx); - self.set_preview_item_id(Some(item_id), cx); - idx - } - - /// Marks the item with the given ID as the preview item. - /// This will be ignored if the global setting `preview_tabs` is disabled. - /// - /// This is a low-level method. Prefer `unpreview_item_if_preview()` or `set_new_preview_item()`. - pub(crate) fn set_preview_item_id(&mut self, item_id: Option, cx: &App) { - if item_id.is_none() || PreviewTabsSettings::get_global(cx).enabled { - self.preview_item_id = item_id; - } - } - - /// Should only be used when deserializing a pane. - pub fn set_pinned_count(&mut self, count: usize) { - self.pinned_tab_count = count; - } - - pub fn pinned_count(&self) -> usize { - self.pinned_tab_count - } - - pub fn handle_item_edit(&mut self, item_id: EntityId, cx: &App) { - if let Some(preview_item) = self.preview_item() - && preview_item.item_id() == item_id - && !preview_item.preserve_preview(cx) - { - self.unpreview_item_if_preview(item_id); - } - } - - pub(crate) fn open_item( - &mut self, - project_entry_id: Option, - project_path: ProjectPath, - focus_item: bool, - allow_preview: bool, - activate: bool, - suggested_position: Option, - window: &mut Window, - cx: &mut Context, - build_item: WorkspaceItemBuilder, - ) -> Box { - let mut existing_item = None; - if let Some(project_entry_id) = project_entry_id { - for (index, item) in self.items.iter().enumerate() { - if item.buffer_kind(cx) == ItemBufferKind::Singleton - && item.project_entry_ids(cx).as_slice() == [project_entry_id] - { - let item = item.boxed_clone(); - existing_item = Some((index, item)); - break; - } - } - } else { - for (index, item) in self.items.iter().enumerate() { - if item.buffer_kind(cx) == ItemBufferKind::Singleton - && item.project_path(cx).as_ref() == Some(&project_path) - { - let item = item.boxed_clone(); - existing_item = Some((index, item)); - break; - } - } - } - - let set_up_existing_item = - |index: usize, pane: &mut Self, window: &mut Window, cx: &mut Context| { - if !allow_preview && let Some(item) = pane.items.get(index) { - pane.unpreview_item_if_preview(item.item_id()); - } - if activate { - pane.activate_item(index, focus_item, focus_item, window, cx); - } - }; - let set_up_new_item = |new_item: Box, - destination_index: Option, - pane: &mut Self, - window: &mut Window, - cx: &mut Context| { - if allow_preview { - pane.replace_preview_item_id(new_item.item_id(), window, cx); - } - - if let Some(text) = new_item.telemetry_event_text(cx) { - telemetry::event!(text); - } - - pane.add_item_inner( - new_item, - true, - focus_item, - activate, - destination_index, - window, - cx, - ); - }; - - if let Some((index, existing_item)) = existing_item { - set_up_existing_item(index, self, window, cx); - existing_item - } else { - // If the item is being opened as preview and we have an existing preview tab, - // open the new item in the position of the existing preview tab. - let destination_index = if allow_preview { - self.close_current_preview_item(window, cx) - } else { - suggested_position - }; - - let new_item = build_item(self, window, cx); - // A special case that won't ever get a `project_entry_id` but has to be deduplicated nonetheless. - if let Some(invalid_buffer_view) = new_item.downcast::() { - let mut already_open_view = None; - let mut views_to_close = HashSet::default(); - for existing_error_view in self - .items_of_type::() - .filter(|item| item.read(cx).abs_path == invalid_buffer_view.read(cx).abs_path) - { - if already_open_view.is_none() - && existing_error_view.read(cx).error == invalid_buffer_view.read(cx).error - { - already_open_view = Some(existing_error_view); - } else { - views_to_close.insert(existing_error_view.item_id()); - } - } - - let resulting_item = match already_open_view { - Some(already_open_view) => { - if let Some(index) = self.index_for_item_id(already_open_view.item_id()) { - set_up_existing_item(index, self, window, cx); - } - Box::new(already_open_view) as Box<_> - } - None => { - set_up_new_item(new_item.clone(), destination_index, self, window, cx); - new_item - } - }; - - self.close_items(window, cx, SaveIntent::Skip, |existing_item| { - views_to_close.contains(&existing_item) - }) - .detach(); - - resulting_item - } else { - set_up_new_item(new_item.clone(), destination_index, self, window, cx); - new_item - } - } - } - - pub fn close_current_preview_item( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Option { - let item_idx = self.preview_item_idx()?; - let id = self.preview_item_id()?; - self.set_preview_item_id(None, cx); - - let prev_active_item_index = self.active_item_index; - self.remove_item(id, false, false, window, cx); - self.active_item_index = prev_active_item_index; - - if item_idx < self.items.len() { - Some(item_idx) - } else { - None - } - } - - pub fn add_item_inner( - &mut self, - item: Box, - activate_pane: bool, - focus_item: bool, - activate: bool, - destination_index: Option, - window: &mut Window, - cx: &mut Context, - ) { - let item_already_exists = self - .items - .iter() - .any(|existing_item| existing_item.item_id() == item.item_id()); - - if !item_already_exists { - self.close_items_on_item_open(window, cx); - } - - if item.buffer_kind(cx) == ItemBufferKind::Singleton - && let Some(&entry_id) = item.project_entry_ids(cx).first() - { - let Some(project) = self.project.upgrade() else { - return; - }; - - let project = project.read(cx); - if let Some(project_path) = project.path_for_entry(entry_id, cx) { - let abs_path = project.absolute_path(&project_path, cx); - self.nav_history - .0 - .lock() - .paths_by_item - .insert(item.item_id(), (project_path, abs_path)); - } - } - // If no destination index is specified, add or move the item after the - // active item (or at the start of tab bar, if the active item is pinned) - let mut insertion_index = { - cmp::min( - if let Some(destination_index) = destination_index { - destination_index - } else { - cmp::max(self.active_item_index + 1, self.pinned_count()) - }, - self.items.len(), - ) - }; - - // Does the item already exist? - let project_entry_id = if item.buffer_kind(cx) == ItemBufferKind::Singleton { - item.project_entry_ids(cx).first().copied() - } else { - None - }; - - let existing_item_index = self.items.iter().position(|existing_item| { - if existing_item.item_id() == item.item_id() { - true - } else if existing_item.buffer_kind(cx) == ItemBufferKind::Singleton { - existing_item - .project_entry_ids(cx) - .first() - .is_some_and(|existing_entry_id| { - Some(existing_entry_id) == project_entry_id.as_ref() - }) - } else { - false - } - }); - if let Some(existing_item_index) = existing_item_index { - // If the item already exists, move it to the desired destination and activate it - - if existing_item_index != insertion_index { - let existing_item_is_active = existing_item_index == self.active_item_index; - - // If the caller didn't specify a destination and the added item is already - // the active one, don't move it - if existing_item_is_active && destination_index.is_none() { - insertion_index = existing_item_index; - } else { - self.items.remove(existing_item_index); - if existing_item_index < self.active_item_index { - self.active_item_index -= 1; - } - insertion_index = insertion_index.min(self.items.len()); - - self.items.insert(insertion_index, item.clone()); - - if existing_item_is_active { - self.active_item_index = insertion_index; - } else if insertion_index <= self.active_item_index { - self.active_item_index += 1; - } - } - - cx.notify(); - } - - if activate { - self.activate_item(insertion_index, activate_pane, focus_item, window, cx); - } - } else { - self.items.insert(insertion_index, item.clone()); - cx.notify(); - - if activate { - if insertion_index <= self.active_item_index - && self.preview_item_idx() != Some(self.active_item_index) - { - self.active_item_index += 1; - } - - self.activate_item(insertion_index, activate_pane, focus_item, window, cx); - } - } - - cx.emit(Event::AddItem { item }); - } - - pub fn add_item( - &mut self, - item: Box, - activate_pane: bool, - focus_item: bool, - destination_index: Option, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(text) = item.telemetry_event_text(cx) { - telemetry::event!(text); - } - - self.add_item_inner( - item, - activate_pane, - focus_item, - true, - destination_index, - window, - cx, - ) - } - - pub fn items_len(&self) -> usize { - self.items.len() - } - - pub fn items(&self) -> impl DoubleEndedIterator> { - self.items.iter() - } - - pub fn items_of_type(&self) -> impl '_ + Iterator> { - self.items - .iter() - .filter_map(|item| item.to_any_view().downcast().ok()) - } - - pub fn active_item(&self) -> Option> { - self.items.get(self.active_item_index).cloned() - } - - fn active_item_id(&self) -> EntityId { - self.items[self.active_item_index].item_id() - } - - pub fn pixel_position_of_cursor(&self, cx: &App) -> Option> { - self.items - .get(self.active_item_index)? - .pixel_position_of_cursor(cx) - } - - pub fn item_for_entry( - &self, - entry_id: ProjectEntryId, - cx: &App, - ) -> Option> { - self.items.iter().find_map(|item| { - if item.buffer_kind(cx) == ItemBufferKind::Singleton - && (item.project_entry_ids(cx).as_slice() == [entry_id]) - { - Some(item.boxed_clone()) - } else { - None - } - }) - } - - pub fn item_for_path( - &self, - project_path: ProjectPath, - cx: &App, - ) -> Option> { - self.items.iter().find_map(move |item| { - if item.buffer_kind(cx) == ItemBufferKind::Singleton - && (item.project_path(cx).as_slice() == [project_path.clone()]) - { - Some(item.boxed_clone()) - } else { - None - } - }) - } - - pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option { - self.index_for_item_id(item.item_id()) - } - - fn index_for_item_id(&self, item_id: EntityId) -> Option { - self.items.iter().position(|i| i.item_id() == item_id) - } - - pub fn item_for_index(&self, ix: usize) -> Option<&dyn ItemHandle> { - self.items.get(ix).map(|i| i.as_ref()) - } - - pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context) { - if !self.can_toggle_zoom { - cx.propagate(); - } else if self.zoomed { - cx.emit(Event::ZoomOut); - } else if !self.items.is_empty() { - if !self.focus_handle.contains_focused(window, cx) { - cx.focus_self(window); - } - cx.emit(Event::ZoomIn); - } - } - - pub fn activate_item( - &mut self, - index: usize, - activate_pane: bool, - focus_item: bool, - window: &mut Window, - cx: &mut Context, - ) { - use NavigationMode::{GoingBack, GoingForward}; - if index < self.items.len() { - let prev_active_item_ix = mem::replace(&mut self.active_item_index, index); - if (prev_active_item_ix != self.active_item_index - || matches!(self.nav_history.mode(), GoingBack | GoingForward)) - && let Some(prev_item) = self.items.get(prev_active_item_ix) - { - prev_item.deactivated(window, cx); - } - self.update_history(index); - self.update_toolbar(window, cx); - self.update_status_bar(window, cx); - - if focus_item { - self.focus_active_item(window, cx); - } - - cx.emit(Event::ActivateItem { - local: activate_pane, - focus_changed: focus_item, - }); - - self.update_active_tab(index); - cx.notify(); - } - } - - fn update_active_tab(&mut self, index: usize) { - if !self.is_tab_pinned(index) { - self.suppress_scroll = false; - self.tab_bar_scroll_handle.scroll_to_item(index); - } - } - - fn update_history(&mut self, index: usize) { - if let Some(newly_active_item) = self.items.get(index) { - self.activation_history - .retain(|entry| entry.entity_id != newly_active_item.item_id()); - self.activation_history.push(ActivationHistoryEntry { - entity_id: newly_active_item.item_id(), - timestamp: self - .next_activation_timestamp - .fetch_add(1, Ordering::SeqCst), - }); - } - } - - pub fn activate_previous_item( - &mut self, - _: &ActivatePreviousItem, - window: &mut Window, - cx: &mut Context, - ) { - let mut index = self.active_item_index; - if index > 0 { - index -= 1; - } else if !self.items.is_empty() { - index = self.items.len() - 1; - } - self.activate_item(index, true, true, window, cx); - } - - pub fn activate_next_item( - &mut self, - _: &ActivateNextItem, - window: &mut Window, - cx: &mut Context, - ) { - let mut index = self.active_item_index; - if index + 1 < self.items.len() { - index += 1; - } else { - index = 0; - } - self.activate_item(index, true, true, window, cx); - } - - pub fn swap_item_left( - &mut self, - _: &SwapItemLeft, - window: &mut Window, - cx: &mut Context, - ) { - let index = self.active_item_index; - if index == 0 { - return; - } - - self.items.swap(index, index - 1); - self.activate_item(index - 1, true, true, window, cx); - } - - pub fn swap_item_right( - &mut self, - _: &SwapItemRight, - window: &mut Window, - cx: &mut Context, - ) { - let index = self.active_item_index; - if index + 1 >= self.items.len() { - return; - } - - self.items.swap(index, index + 1); - self.activate_item(index + 1, true, true, window, cx); - } - - pub fn activate_last_item( - &mut self, - _: &ActivateLastItem, - window: &mut Window, - cx: &mut Context, - ) { - let index = self.items.len().saturating_sub(1); - self.activate_item(index, true, true, window, cx); - } - - pub fn close_active_item( - &mut self, - action: &CloseActiveItem, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - if self.items.is_empty() { - // Close the window when there's no active items to close, if configured - if WorkspaceSettings::get_global(cx) - .when_closing_with_no_tabs - .should_close() - { - window.dispatch_action(Box::new(CloseWindow), cx); - } - - return Task::ready(Ok(())); - } - if self.is_tab_pinned(self.active_item_index) && !action.close_pinned { - // Activate any non-pinned tab in same pane - let non_pinned_tab_index = self - .items() - .enumerate() - .find(|(index, _item)| !self.is_tab_pinned(*index)) - .map(|(index, _item)| index); - if let Some(index) = non_pinned_tab_index { - self.activate_item(index, false, false, window, cx); - return Task::ready(Ok(())); - } - - // Activate any non-pinned tab in different pane - let current_pane = cx.entity(); - self.workspace - .update(cx, |workspace, cx| { - let panes = workspace.center.panes(); - let pane_with_unpinned_tab = panes.iter().find(|pane| { - if **pane == ¤t_pane { - return false; - } - pane.read(cx).has_unpinned_tabs() - }); - if let Some(pane) = pane_with_unpinned_tab { - pane.update(cx, |pane, cx| pane.activate_unpinned_tab(window, cx)); - } - }) - .ok(); - - return Task::ready(Ok(())); - }; - - let active_item_id = self.active_item_id(); - - self.close_item_by_id( - active_item_id, - action.save_intent.unwrap_or(SaveIntent::Close), - window, - cx, - ) - } - - pub fn close_item_by_id( - &mut self, - item_id_to_close: EntityId, - save_intent: SaveIntent, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.close_items(window, cx, save_intent, move |view_id| { - view_id == item_id_to_close - }) - } - - pub fn close_other_items( - &mut self, - action: &CloseOtherItems, - target_item_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - if self.items.is_empty() { - return Task::ready(Ok(())); - } - - let active_item_id = match target_item_id { - Some(result) => result, - None => self.active_item_id(), - }; - - let pinned_item_ids = self.pinned_item_ids(); - - self.close_items( - window, - cx, - action.save_intent.unwrap_or(SaveIntent::Close), - move |item_id| { - item_id != active_item_id - && (action.close_pinned || !pinned_item_ids.contains(&item_id)) - }, - ) - } - - pub fn close_multibuffer_items( - &mut self, - action: &CloseMultibufferItems, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - if self.items.is_empty() { - return Task::ready(Ok(())); - } - - let pinned_item_ids = self.pinned_item_ids(); - let multibuffer_items = self.multibuffer_item_ids(cx); - - self.close_items( - window, - cx, - action.save_intent.unwrap_or(SaveIntent::Close), - move |item_id| { - (action.close_pinned || !pinned_item_ids.contains(&item_id)) - && multibuffer_items.contains(&item_id) - }, - ) - } - - pub fn close_clean_items( - &mut self, - action: &CloseCleanItems, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - if self.items.is_empty() { - return Task::ready(Ok(())); - } - - let clean_item_ids = self.clean_item_ids(cx); - let pinned_item_ids = self.pinned_item_ids(); - - self.close_items(window, cx, SaveIntent::Close, move |item_id| { - clean_item_ids.contains(&item_id) - && (action.close_pinned || !pinned_item_ids.contains(&item_id)) - }) - } - - pub fn close_items_to_the_left_by_id( - &mut self, - item_id: Option, - action: &CloseItemsToTheLeft, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.close_items_to_the_side_by_id(item_id, Side::Left, action.close_pinned, window, cx) - } - - pub fn close_items_to_the_right_by_id( - &mut self, - item_id: Option, - action: &CloseItemsToTheRight, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.close_items_to_the_side_by_id(item_id, Side::Right, action.close_pinned, window, cx) - } - - pub fn close_items_to_the_side_by_id( - &mut self, - item_id: Option, - side: Side, - close_pinned: bool, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - if self.items.is_empty() { - return Task::ready(Ok(())); - } - - let item_id = item_id.unwrap_or_else(|| self.active_item_id()); - let to_the_side_item_ids = self.to_the_side_item_ids(item_id, side); - let pinned_item_ids = self.pinned_item_ids(); - - self.close_items(window, cx, SaveIntent::Close, move |item_id| { - to_the_side_item_ids.contains(&item_id) - && (close_pinned || !pinned_item_ids.contains(&item_id)) - }) - } - - pub fn close_all_items( - &mut self, - action: &CloseAllItems, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - if self.items.is_empty() { - return Task::ready(Ok(())); - } - - let pinned_item_ids = self.pinned_item_ids(); - - self.close_items( - window, - cx, - action.save_intent.unwrap_or(SaveIntent::Close), - |item_id| action.close_pinned || !pinned_item_ids.contains(&item_id), - ) - } - - fn close_items_on_item_open(&mut self, window: &mut Window, cx: &mut Context) { - let target = self.max_tabs.map(|m| m.get()); - let protect_active_item = false; - self.close_items_to_target_count(target, protect_active_item, window, cx); - } - - fn close_items_on_settings_change(&mut self, window: &mut Window, cx: &mut Context) { - let target = self.max_tabs.map(|m| m.get() + 1); - // The active item in this case is the settings.json file, which should be protected from being closed - let protect_active_item = true; - self.close_items_to_target_count(target, protect_active_item, window, cx); - } - - fn close_items_to_target_count( - &mut self, - target_count: Option, - protect_active_item: bool, - window: &mut Window, - cx: &mut Context, - ) { - let Some(target_count) = target_count else { - return; - }; - - let mut index_list = Vec::new(); - let mut items_len = self.items_len(); - let mut indexes: HashMap = HashMap::default(); - let active_ix = self.active_item_index(); - - for (index, item) in self.items.iter().enumerate() { - indexes.insert(item.item_id(), index); - } - - // Close least recently used items to reach target count. - // The target count is allowed to be exceeded, as we protect pinned - // items, dirty items, and sometimes, the active item. - for entry in self.activation_history.iter() { - if items_len < target_count { - break; - } - - let Some(&index) = indexes.get(&entry.entity_id) else { - continue; - }; - - if protect_active_item && index == active_ix { - continue; - } - - if let Some(true) = self.items.get(index).map(|item| item.is_dirty(cx)) { - continue; - } - - if self.is_tab_pinned(index) { - continue; - } - - index_list.push(index); - items_len -= 1; - } - // The sort and reverse is necessary since we remove items - // using their index position, hence removing from the end - // of the list first to avoid changing indexes. - index_list.sort_unstable(); - index_list - .iter() - .rev() - .for_each(|&index| self._remove_item(index, false, false, None, window, cx)); - } - - // Usually when you close an item that has unsaved changes, we prompt you to - // save it. That said, if you still have the buffer open in a different pane - // we can close this one without fear of losing data. - pub fn skip_save_on_close(item: &dyn ItemHandle, workspace: &Workspace, cx: &App) -> bool { - let mut dirty_project_item_ids = Vec::new(); - item.for_each_project_item(cx, &mut |project_item_id, project_item| { - if project_item.is_dirty() { - dirty_project_item_ids.push(project_item_id); - } - }); - if dirty_project_item_ids.is_empty() { - return !(item.buffer_kind(cx) == ItemBufferKind::Singleton && item.is_dirty(cx)); - } - - for open_item in workspace.items(cx) { - if open_item.item_id() == item.item_id() { - continue; - } - if open_item.buffer_kind(cx) != ItemBufferKind::Singleton { - continue; - } - let other_project_item_ids = open_item.project_item_model_ids(cx); - dirty_project_item_ids.retain(|id| !other_project_item_ids.contains(id)); - } - dirty_project_item_ids.is_empty() - } - - pub(super) fn file_names_for_prompt( - items: &mut dyn Iterator>, - cx: &App, - ) -> String { - let mut file_names = BTreeSet::default(); - for item in items { - item.for_each_project_item(cx, &mut |_, project_item| { - if !project_item.is_dirty() { - return; - } - let filename = project_item - .project_path(cx) - .and_then(|path| path.path.file_name().map(ToOwned::to_owned)); - file_names.insert(filename.unwrap_or("untitled".to_string())); - }); - } - if file_names.len() > 6 { - format!( - "{}\n.. and {} more", - file_names.iter().take(5).join("\n"), - file_names.len() - 5 - ) - } else { - file_names.into_iter().join("\n") - } - } - - pub fn close_items( - &self, - window: &mut Window, - cx: &mut Context, - mut save_intent: SaveIntent, - should_close: impl Fn(EntityId) -> bool, - ) -> Task> { - // Find the items to close. - let mut items_to_close = Vec::new(); - for item in &self.items { - if should_close(item.item_id()) { - items_to_close.push(item.boxed_clone()); - } - } - - let active_item_id = self.active_item().map(|item| item.item_id()); - - items_to_close.sort_by_key(|item| { - let path = item.project_path(cx); - // Put the currently active item at the end, because if the currently active item is not closed last - // closing the currently active item will cause the focus to switch to another item - // This will cause Zed to expand the content of the currently active item - // - // Beyond that sort in order of project path, with untitled files and multibuffers coming last. - (active_item_id == Some(item.item_id()), path.is_none(), path) - }); - - let workspace = self.workspace.clone(); - let Some(project) = self.project.upgrade() else { - return Task::ready(Ok(())); - }; - cx.spawn_in(window, async move |pane, cx| { - let dirty_items = workspace.update(cx, |workspace, cx| { - items_to_close - .iter() - .filter(|item| { - item.is_dirty(cx) && !Self::skip_save_on_close(item.as_ref(), workspace, cx) - }) - .map(|item| item.boxed_clone()) - .collect::>() - })?; - - if save_intent == SaveIntent::Close && dirty_items.len() > 1 { - let answer = pane.update_in(cx, |_, window, cx| { - let detail = Self::file_names_for_prompt(&mut dirty_items.iter(), cx); - window.prompt( - PromptLevel::Warning, - "Do you want to save changes to the following files?", - Some(&detail), - &["Save all", "Discard all", "Cancel"], - cx, - ) - })?; - match answer.await { - Ok(0) => save_intent = SaveIntent::SaveAll, - Ok(1) => save_intent = SaveIntent::Skip, - Ok(2) => return Ok(()), - _ => {} - } - } - - for item_to_close in items_to_close { - let mut should_save = true; - if save_intent == SaveIntent::Close { - workspace.update(cx, |workspace, cx| { - if Self::skip_save_on_close(item_to_close.as_ref(), workspace, cx) { - should_save = false; - } - })?; - } - - if should_save { - match Self::save_item(project.clone(), &pane, &*item_to_close, save_intent, cx) - .await - { - Ok(success) => { - if !success { - break; - } - } - Err(err) => { - let answer = pane.update_in(cx, |_, window, cx| { - let detail = Self::file_names_for_prompt( - &mut [&item_to_close].into_iter(), - cx, - ); - window.prompt( - PromptLevel::Warning, - &format!("Unable to save file: {}", &err), - Some(&detail), - &["Close Without Saving", "Cancel"], - cx, - ) - })?; - match answer.await { - Ok(0) => {} - Ok(1..) | Err(_) => break, - } - } - } - } - - // Remove the item from the pane. - pane.update_in(cx, |pane, window, cx| { - pane.remove_item( - item_to_close.item_id(), - false, - pane.close_pane_if_empty, - window, - cx, - ); - }) - .ok(); - } - - pane.update(cx, |_, cx| cx.notify()).ok(); - Ok(()) - }) - } - - pub fn take_active_item( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Option> { - let item = self.active_item()?; - self.remove_item(item.item_id(), false, false, window, cx); - Some(item) - } - - pub fn remove_item( - &mut self, - item_id: EntityId, - activate_pane: bool, - close_pane_if_empty: bool, - window: &mut Window, - cx: &mut Context, - ) { - let Some(item_index) = self.index_for_item_id(item_id) else { - return; - }; - self._remove_item( - item_index, - activate_pane, - close_pane_if_empty, - None, - window, - cx, - ) - } - - pub fn remove_item_and_focus_on_pane( - &mut self, - item_index: usize, - activate_pane: bool, - focus_on_pane_if_closed: Entity, - window: &mut Window, - cx: &mut Context, - ) { - self._remove_item( - item_index, - activate_pane, - true, - Some(focus_on_pane_if_closed), - window, - cx, - ) - } - - fn _remove_item( - &mut self, - item_index: usize, - activate_pane: bool, - close_pane_if_empty: bool, - focus_on_pane_if_closed: Option>, - window: &mut Window, - cx: &mut Context, - ) { - let activate_on_close = &ItemSettings::get_global(cx).activate_on_close; - self.activation_history - .retain(|entry| entry.entity_id != self.items[item_index].item_id()); - - if self.is_tab_pinned(item_index) { - self.pinned_tab_count -= 1; - } - if item_index == self.active_item_index { - let left_neighbour_index = || item_index.min(self.items.len()).saturating_sub(1); - let index_to_activate = match activate_on_close { - ActivateOnClose::History => self - .activation_history - .pop() - .and_then(|last_activated_item| { - self.items.iter().enumerate().find_map(|(index, item)| { - (item.item_id() == last_activated_item.entity_id).then_some(index) - }) - }) - // We didn't have a valid activation history entry, so fallback - // to activating the item to the left - .unwrap_or_else(left_neighbour_index), - ActivateOnClose::Neighbour => { - self.activation_history.pop(); - if item_index + 1 < self.items.len() { - item_index + 1 - } else { - item_index.saturating_sub(1) - } - } - ActivateOnClose::LeftNeighbour => { - self.activation_history.pop(); - left_neighbour_index() - } - }; - - let should_activate = activate_pane || self.has_focus(window, cx); - if self.items.len() == 1 && should_activate { - self.focus_handle.focus(window); - } else { - self.activate_item( - index_to_activate, - should_activate, - should_activate, - window, - cx, - ); - } - } - - let item = self.items.remove(item_index); - - cx.emit(Event::RemovedItem { item: item.clone() }); - if self.items.is_empty() { - item.deactivated(window, cx); - if close_pane_if_empty { - self.update_toolbar(window, cx); - cx.emit(Event::Remove { - focus_on_pane: focus_on_pane_if_closed, - }); - } - } - - if item_index < self.active_item_index { - self.active_item_index -= 1; - } - - let mode = self.nav_history.mode(); - self.nav_history.set_mode(NavigationMode::ClosingItem); - item.deactivated(window, cx); - item.on_removed(cx); - self.nav_history.set_mode(mode); - - self.unpreview_item_if_preview(item.item_id()); - - if let Some(path) = item.project_path(cx) { - let abs_path = self - .nav_history - .0 - .lock() - .paths_by_item - .get(&item.item_id()) - .and_then(|(_, abs_path)| abs_path.clone()); - - self.nav_history - .0 - .lock() - .paths_by_item - .insert(item.item_id(), (path, abs_path)); - } else { - self.nav_history - .0 - .lock() - .paths_by_item - .remove(&item.item_id()); - } - - if self.zoom_out_on_close && self.items.is_empty() && close_pane_if_empty && self.zoomed { - cx.emit(Event::ZoomOut); - } - - cx.notify(); - } - - pub async fn save_item( - project: Entity, - pane: &WeakEntity, - item: &dyn ItemHandle, - save_intent: SaveIntent, - cx: &mut AsyncWindowContext, - ) -> Result { - const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?"; - - const DELETED_MESSAGE: &str = "This file has been deleted on disk since you started editing it. Do you want to recreate it?"; - - let path_style = project.read_with(cx, |project, cx| project.path_style(cx))?; - if save_intent == SaveIntent::Skip { - return Ok(true); - }; - let Some(item_ix) = pane - .read_with(cx, |pane, _| pane.index_for_item(item)) - .ok() - .flatten() - else { - return Ok(true); - }; - - let ( - mut has_conflict, - mut is_dirty, - mut can_save, - can_save_as, - is_singleton, - has_deleted_file, - ) = cx.update(|_window, cx| { - ( - item.has_conflict(cx), - item.is_dirty(cx), - item.can_save(cx), - item.can_save_as(cx), - item.buffer_kind(cx) == ItemBufferKind::Singleton, - item.has_deleted_file(cx), - ) - })?; - - // when saving a single buffer, we ignore whether or not it's dirty. - if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat { - is_dirty = true; - } - - if save_intent == SaveIntent::SaveAs { - is_dirty = true; - has_conflict = false; - can_save = false; - } - - if save_intent == SaveIntent::Overwrite { - has_conflict = false; - } - - let should_format = save_intent != SaveIntent::SaveWithoutFormat; - - if has_conflict && can_save { - if has_deleted_file && is_singleton { - let answer = pane.update_in(cx, |pane, window, cx| { - pane.activate_item(item_ix, true, true, window, cx); - window.prompt( - PromptLevel::Warning, - DELETED_MESSAGE, - None, - &["Save", "Close", "Cancel"], - cx, - ) - })?; - match answer.await { - Ok(0) => { - pane.update_in(cx, |_, window, cx| { - item.save( - SaveOptions { - format: should_format, - autosave: false, - }, - project, - window, - cx, - ) - })? - .await? - } - Ok(1) => { - pane.update_in(cx, |pane, window, cx| { - pane.remove_item(item.item_id(), false, true, window, cx) - })?; - } - _ => return Ok(false), - } - return Ok(true); - } else { - let answer = pane.update_in(cx, |pane, window, cx| { - pane.activate_item(item_ix, true, true, window, cx); - window.prompt( - PromptLevel::Warning, - CONFLICT_MESSAGE, - None, - &["Overwrite", "Discard", "Cancel"], - cx, - ) - })?; - match answer.await { - Ok(0) => { - pane.update_in(cx, |_, window, cx| { - item.save( - SaveOptions { - format: should_format, - autosave: false, - }, - project, - window, - cx, - ) - })? - .await? - } - Ok(1) => { - pane.update_in(cx, |_, window, cx| item.reload(project, window, cx))? - .await? - } - _ => return Ok(false), - } - } - } else if is_dirty && (can_save || can_save_as) { - if save_intent == SaveIntent::Close { - let will_autosave = cx.update(|_window, cx| { - item.can_autosave(cx) - && item.workspace_settings(cx).autosave.should_save_on_close() - })?; - if !will_autosave { - let item_id = item.item_id(); - let answer_task = pane.update_in(cx, |pane, window, cx| { - if pane.save_modals_spawned.insert(item_id) { - pane.activate_item(item_ix, true, true, window, cx); - let prompt = dirty_message_for(item.project_path(cx), path_style); - Some(window.prompt( - PromptLevel::Warning, - &prompt, - None, - &["Save", "Don't Save", "Cancel"], - cx, - )) - } else { - None - } - })?; - if let Some(answer_task) = answer_task { - let answer = answer_task.await; - pane.update(cx, |pane, _| { - if !pane.save_modals_spawned.remove(&item_id) { - debug_panic!( - "save modal was not present in spawned modals after awaiting for its answer" - ) - } - })?; - match answer { - Ok(0) => {} - Ok(1) => { - // Don't save this file - pane.update_in(cx, |pane, _, cx| { - if pane.is_tab_pinned(item_ix) && !item.can_save(cx) { - pane.pinned_tab_count -= 1; - } - }) - .log_err(); - return Ok(true); - } - _ => return Ok(false), // Cancel - } - } else { - return Ok(false); - } - } - } - - if can_save { - pane.update_in(cx, |pane, window, cx| { - pane.unpreview_item_if_preview(item.item_id()); - item.save( - SaveOptions { - format: should_format, - autosave: false, - }, - project, - window, - cx, - ) - })? - .await?; - } else if can_save_as && is_singleton { - let suggested_name = - cx.update(|_window, cx| item.suggested_filename(cx).to_string())?; - let new_path = pane.update_in(cx, |pane, window, cx| { - pane.activate_item(item_ix, true, true, window, cx); - pane.workspace.update(cx, |workspace, cx| { - let lister = if workspace.project().read(cx).is_local() { - DirectoryLister::Local( - workspace.project().clone(), - workspace.app_state().fs.clone(), - ) - } else { - DirectoryLister::Project(workspace.project().clone()) - }; - workspace.prompt_for_new_path(lister, Some(suggested_name), window, cx) - }) - })??; - let Some(new_path) = new_path.await.ok().flatten().into_iter().flatten().next() - else { - return Ok(false); - }; - - let project_path = pane - .update(cx, |pane, cx| { - pane.project - .update(cx, |project, cx| { - project.find_or_create_worktree(new_path, true, cx) - }) - .ok() - }) - .ok() - .flatten(); - let save_task = if let Some(project_path) = project_path { - let (worktree, path) = project_path.await?; - let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?; - let new_path = ProjectPath { - worktree_id, - path: path, - }; - - pane.update_in(cx, |pane, window, cx| { - if let Some(item) = pane.item_for_path(new_path.clone(), cx) { - pane.remove_item(item.item_id(), false, false, window, cx); - } - - item.save_as(project, new_path, window, cx) - })? - } else { - return Ok(false); - }; - - save_task.await?; - return Ok(true); - } - } - - pane.update(cx, |_, cx| { - cx.emit(Event::UserSavedItem { - item: item.downgrade_item(), - save_intent, - }); - true - }) - } - - pub fn autosave_item( - item: &dyn ItemHandle, - project: Entity, - window: &mut Window, - cx: &mut App, - ) -> Task> { - let format = !matches!( - item.workspace_settings(cx).autosave, - AutosaveSetting::AfterDelay { .. } - ); - if item.can_autosave(cx) { - item.save( - SaveOptions { - format, - autosave: true, - }, - project, - window, - cx, - ) - } else { - Task::ready(Ok(())) - } - } - - pub fn focus_active_item(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(active_item) = self.active_item() { - let focus_handle = active_item.item_focus_handle(cx); - window.focus(&focus_handle); - } - } - - pub fn split(&mut self, direction: SplitDirection, cx: &mut Context) { - cx.emit(Event::Split { - direction, - clone_active_item: true, - }); - } - - pub fn split_and_move(&mut self, direction: SplitDirection, cx: &mut Context) { - if self.items.len() > 1 { - cx.emit(Event::Split { - direction, - clone_active_item: false, - }); - } - } - - pub fn toolbar(&self) -> &Entity { - &self.toolbar - } - - pub fn handle_deleted_project_item( - &mut self, - entry_id: ProjectEntryId, - window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - let item_id = self.items().find_map(|item| { - if item.buffer_kind(cx) == ItemBufferKind::Singleton - && item.project_entry_ids(cx).as_slice() == [entry_id] - { - Some(item.item_id()) - } else { - None - } - })?; - - self.remove_item(item_id, false, true, window, cx); - self.nav_history.remove_item(item_id); - - Some(()) - } - - fn update_toolbar(&mut self, window: &mut Window, cx: &mut Context) { - let active_item = self - .items - .get(self.active_item_index) - .map(|item| item.as_ref()); - self.toolbar.update(cx, |toolbar, cx| { - toolbar.set_active_item(active_item, window, cx); - }); - } - - fn update_status_bar(&mut self, window: &mut Window, cx: &mut Context) { - let workspace = self.workspace.clone(); - let pane = cx.entity(); - - window.defer(cx, move |window, cx| { - let Ok(status_bar) = - workspace.read_with(cx, |workspace, _| workspace.status_bar.clone()) - else { - return; - }; - - status_bar.update(cx, move |status_bar, cx| { - status_bar.set_active_pane(&pane, window, cx); - }); - }); - } - - fn entry_abs_path(&self, entry: ProjectEntryId, cx: &App) -> Option { - let worktree = self - .workspace - .upgrade()? - .read(cx) - .project() - .read(cx) - .worktree_for_entry(entry, cx)? - .read(cx); - let entry = worktree.entry_for_id(entry)?; - Some(match &entry.canonical_path { - Some(canonical_path) => canonical_path.to_path_buf(), - None => worktree.absolutize(&entry.path), - }) - } - - pub fn icon_color(selected: bool) -> Color { - if selected { - Color::Default - } else { - Color::Muted - } - } - - fn toggle_pin_tab(&mut self, _: &TogglePinTab, window: &mut Window, cx: &mut Context) { - if self.items.is_empty() { - return; - } - let active_tab_ix = self.active_item_index(); - if self.is_tab_pinned(active_tab_ix) { - self.unpin_tab_at(active_tab_ix, window, cx); - } else { - self.pin_tab_at(active_tab_ix, window, cx); - } - } - - fn unpin_all_tabs(&mut self, _: &UnpinAllTabs, window: &mut Window, cx: &mut Context) { - if self.items.is_empty() { - return; - } - - let pinned_item_ids = self.pinned_item_ids().into_iter().rev(); - - for pinned_item_id in pinned_item_ids { - if let Some(ix) = self.index_for_item_id(pinned_item_id) { - self.unpin_tab_at(ix, window, cx); - } - } - } - - fn pin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context) { - self.change_tab_pin_state(ix, PinOperation::Pin, window, cx); - } - - fn unpin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context) { - self.change_tab_pin_state(ix, PinOperation::Unpin, window, cx); - } - - fn change_tab_pin_state( - &mut self, - ix: usize, - operation: PinOperation, - window: &mut Window, - cx: &mut Context, - ) { - maybe!({ - let pane = cx.entity(); - - let destination_index = match operation { - PinOperation::Pin => self.pinned_tab_count.min(ix), - PinOperation::Unpin => self.pinned_tab_count.checked_sub(1)?, - }; - - let id = self.item_for_index(ix)?.item_id(); - let should_activate = ix == self.active_item_index; - - if matches!(operation, PinOperation::Pin) { - self.unpreview_item_if_preview(id); - } - - match operation { - PinOperation::Pin => self.pinned_tab_count += 1, - PinOperation::Unpin => self.pinned_tab_count -= 1, - } - - if ix == destination_index { - cx.notify(); - } else { - self.workspace - .update(cx, |_, cx| { - cx.defer_in(window, move |_, window, cx| { - move_item( - &pane, - &pane, - id, - destination_index, - should_activate, - window, - cx, - ); - }); - }) - .ok()?; - } - - let event = match operation { - PinOperation::Pin => Event::ItemPinned, - PinOperation::Unpin => Event::ItemUnpinned, - }; - - cx.emit(event); - - Some(()) - }); - } - - fn is_tab_pinned(&self, ix: usize) -> bool { - self.pinned_tab_count > ix - } - - fn has_unpinned_tabs(&self) -> bool { - self.pinned_tab_count < self.items.len() - } - - fn activate_unpinned_tab(&mut self, window: &mut Window, cx: &mut Context) { - if self.items.is_empty() { - return; - } - let Some(index) = self - .items() - .enumerate() - .find_map(|(index, _item)| (!self.is_tab_pinned(index)).then_some(index)) - else { - return; - }; - self.activate_item(index, true, true, window, cx); - } - - fn render_tab( - &self, - ix: usize, - item: &dyn ItemHandle, - detail: usize, - focus_handle: &FocusHandle, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement + use<> { - let is_active = ix == self.active_item_index; - let is_preview = self - .preview_item_id - .map(|id| id == item.item_id()) - .unwrap_or(false); - - let label = item.tab_content( - TabContentParams { - detail: Some(detail), - selected: is_active, - preview: is_preview, - deemphasized: !self.has_focus(window, cx), - }, - window, - cx, - ); - - let item_diagnostic = item - .project_path(cx) - .map_or(None, |project_path| self.diagnostics.get(&project_path)); - - let decorated_icon = item_diagnostic.map_or(None, |diagnostic| { - let icon = match item.tab_icon(window, cx) { - Some(icon) => icon, - None => return None, - }; - - let knockout_item_color = if is_active { - cx.theme().colors().tab_active_background - } else { - cx.theme().colors().tab_bar_background - }; - - let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR) - { - (IconDecorationKind::X, Color::Error) - } else { - (IconDecorationKind::Triangle, Color::Warning) - }; - - Some(DecoratedIcon::new( - icon.size(IconSize::Small).color(Color::Muted), - Some( - IconDecoration::new(icon_decoration, knockout_item_color, cx) - .color(icon_color.color(cx)) - .position(Point { - x: px(-2.), - y: px(-2.), - }), - ), - )) - }); - - let icon = if decorated_icon.is_none() { - match item_diagnostic { - Some(&DiagnosticSeverity::ERROR) => None, - Some(&DiagnosticSeverity::WARNING) => None, - _ => item - .tab_icon(window, cx) - .map(|icon| icon.color(Color::Muted)), - } - .map(|icon| icon.size(IconSize::Small)) - } else { - None - }; - - let settings = ItemSettings::get_global(cx); - let close_side = &settings.close_position; - let show_close_button = &settings.show_close_button; - let indicator = render_item_indicator(item.boxed_clone(), cx); - let tab_tooltip_content = item.tab_tooltip_content(cx); - let item_id = item.item_id(); - let is_first_item = ix == 0; - let is_last_item = ix == self.items.len() - 1; - let is_pinned = self.is_tab_pinned(ix); - let position_relative_to_active_item = ix.cmp(&self.active_item_index); - - let tab = Tab::new(ix) - .position(if is_first_item { - TabPosition::First - } else if is_last_item { - TabPosition::Last - } else { - TabPosition::Middle(position_relative_to_active_item) - }) - .close_side(match close_side { - ClosePosition::Left => ui::TabCloseSide::Start, - ClosePosition::Right => ui::TabCloseSide::End, - }) - .toggle_state(is_active) - .on_click(cx.listener(move |pane: &mut Self, _, window, cx| { - pane.activate_item(ix, true, true, window, cx) - })) - // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener. - .on_mouse_down( - MouseButton::Middle, - cx.listener(move |pane, _event, window, cx| { - pane.close_item_by_id(item_id, SaveIntent::Close, window, cx) - .detach_and_log_err(cx); - }), - ) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |pane, event: &MouseDownEvent, _, _| { - if event.click_count > 1 { - pane.unpreview_item_if_preview(item_id); - } - }), - ) - .on_drag( - DraggedTab { - item: item.boxed_clone(), - pane: cx.entity(), - detail, - is_active, - ix, - }, - |tab, _, _, cx| cx.new(|_| tab.clone()), - ) - .drag_over::(move |tab, dragged_tab: &DraggedTab, _, cx| { - let mut styled_tab = tab - .bg(cx.theme().colors().drop_target_background) - .border_color(cx.theme().colors().drop_target_border) - .border_0(); - - if ix < dragged_tab.ix { - styled_tab = styled_tab.border_l_2(); - } else if ix > dragged_tab.ix { - styled_tab = styled_tab.border_r_2(); - } - - styled_tab - }) - .drag_over::(|tab, _, _, cx| { - tab.bg(cx.theme().colors().drop_target_background) - }) - .when_some(self.can_drop_predicate.clone(), |this, p| { - this.can_drop(move |a, window, cx| p(a, window, cx)) - }) - .on_drop( - cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| { - this.drag_split_direction = None; - this.handle_tab_drop(dragged_tab, ix, window, cx) - }), - ) - .on_drop( - cx.listener(move |this, selection: &DraggedSelection, window, cx| { - this.drag_split_direction = None; - this.handle_dragged_selection_drop(selection, Some(ix), window, cx) - }), - ) - .on_drop(cx.listener(move |this, paths, window, cx| { - this.drag_split_direction = None; - this.handle_external_paths_drop(paths, window, cx) - })) - .start_slot::(indicator) - .map(|this| { - let end_slot_action: &'static dyn Action; - let end_slot_tooltip_text: &'static str; - let end_slot = if is_pinned { - end_slot_action = &TogglePinTab; - end_slot_tooltip_text = "Unpin Tab"; - IconButton::new("unpin tab", IconName::Pin) - .shape(IconButtonShape::Square) - .icon_color(Color::Muted) - .size(ButtonSize::None) - .icon_size(IconSize::Small) - .on_click(cx.listener(move |pane, _, window, cx| { - pane.unpin_tab_at(ix, window, cx); - })) - } else { - end_slot_action = &CloseActiveItem { - save_intent: None, - close_pinned: false, - }; - end_slot_tooltip_text = "Close Tab"; - match show_close_button { - ShowCloseButton::Always => IconButton::new("close tab", IconName::Close), - ShowCloseButton::Hover => { - IconButton::new("close tab", IconName::Close).visible_on_hover("") - } - ShowCloseButton::Hidden => return this, - } - .shape(IconButtonShape::Square) - .icon_color(Color::Muted) - .size(ButtonSize::None) - .icon_size(IconSize::Small) - .on_click(cx.listener(move |pane, _, window, cx| { - pane.close_item_by_id(item_id, SaveIntent::Close, window, cx) - .detach_and_log_err(cx); - })) - } - .map(|this| { - if is_active { - let focus_handle = focus_handle.clone(); - this.tooltip(move |window, cx| { - Tooltip::for_action_in( - end_slot_tooltip_text, - end_slot_action, - &window.focused(cx).unwrap_or_else(|| focus_handle.clone()), - cx, - ) - }) - } else { - this.tooltip(Tooltip::text(end_slot_tooltip_text)) - } - }); - this.end_slot(end_slot) - }) - .child( - h_flex() - .gap_1() - .items_center() - .children( - std::iter::once(if let Some(decorated_icon) = decorated_icon { - Some(div().child(decorated_icon.into_any_element())) - } else { - icon.map(|icon| div().child(icon.into_any_element())) - }) - .flatten(), - ) - .child(label) - .id(("pane-tab-content", ix)) - .map(|this| match tab_tooltip_content { - Some(TabTooltipContent::Text(text)) => this.tooltip(Tooltip::text(text)), - Some(TabTooltipContent::Custom(element_fn)) => { - this.tooltip(move |window, cx| element_fn(window, cx)) - } - None => this, - }), - ); - - let single_entry_to_resolve = (self.items[ix].buffer_kind(cx) == ItemBufferKind::Singleton) - .then(|| self.items[ix].project_entry_ids(cx).get(0).copied()) - .flatten(); - - let total_items = self.items.len(); - let has_multibuffer_items = self - .items - .iter() - .any(|item| item.buffer_kind(cx) == ItemBufferKind::Multibuffer); - let has_items_to_left = ix > 0; - let has_items_to_right = ix < total_items - 1; - let has_clean_items = self.items.iter().any(|item| !item.is_dirty(cx)); - let is_pinned = self.is_tab_pinned(ix); - let pane = cx.entity().downgrade(); - let menu_context = item.item_focus_handle(cx); - right_click_menu(ix) - .trigger(|_, _, _| tab) - .menu(move |window, cx| { - let pane = pane.clone(); - let menu_context = menu_context.clone(); - ContextMenu::build(window, cx, move |mut menu, window, cx| { - let close_active_item_action = CloseActiveItem { - save_intent: None, - close_pinned: true, - }; - let close_inactive_items_action = CloseOtherItems { - save_intent: None, - close_pinned: false, - }; - let close_multibuffers_action = CloseMultibufferItems { - save_intent: None, - close_pinned: false, - }; - let close_items_to_the_left_action = CloseItemsToTheLeft { - close_pinned: false, - }; - let close_items_to_the_right_action = CloseItemsToTheRight { - close_pinned: false, - }; - let close_clean_items_action = CloseCleanItems { - close_pinned: false, - }; - let close_all_items_action = CloseAllItems { - save_intent: None, - close_pinned: false, - }; - if let Some(pane) = pane.upgrade() { - menu = menu - .entry( - "Close", - Some(Box::new(close_active_item_action)), - window.handler_for(&pane, move |pane, window, cx| { - pane.close_item_by_id(item_id, SaveIntent::Close, window, cx) - .detach_and_log_err(cx); - }), - ) - .item(ContextMenuItem::Entry( - ContextMenuEntry::new("Close Others") - .action(Box::new(close_inactive_items_action.clone())) - .disabled(total_items == 1) - .handler(window.handler_for(&pane, move |pane, window, cx| { - pane.close_other_items( - &close_inactive_items_action, - Some(item_id), - window, - cx, - ) - .detach_and_log_err(cx); - })), - )) - // We make this optional, instead of using disabled as to not overwhelm the context menu unnecessarily - .extend(has_multibuffer_items.then(|| { - ContextMenuItem::Entry( - ContextMenuEntry::new("Close Multibuffers") - .action(Box::new(close_multibuffers_action.clone())) - .handler(window.handler_for( - &pane, - move |pane, window, cx| { - pane.close_multibuffer_items( - &close_multibuffers_action, - window, - cx, - ) - .detach_and_log_err(cx); - }, - )), - ) - })) - .separator() - .item(ContextMenuItem::Entry( - ContextMenuEntry::new("Close Left") - .action(Box::new(close_items_to_the_left_action.clone())) - .disabled(!has_items_to_left) - .handler(window.handler_for(&pane, move |pane, window, cx| { - pane.close_items_to_the_left_by_id( - Some(item_id), - &close_items_to_the_left_action, - window, - cx, - ) - .detach_and_log_err(cx); - })), - )) - .item(ContextMenuItem::Entry( - ContextMenuEntry::new("Close Right") - .action(Box::new(close_items_to_the_right_action.clone())) - .disabled(!has_items_to_right) - .handler(window.handler_for(&pane, move |pane, window, cx| { - pane.close_items_to_the_right_by_id( - Some(item_id), - &close_items_to_the_right_action, - window, - cx, - ) - .detach_and_log_err(cx); - })), - )) - .separator() - .item(ContextMenuItem::Entry( - ContextMenuEntry::new("Close Clean") - .action(Box::new(close_clean_items_action.clone())) - .disabled(!has_clean_items) - .handler(window.handler_for(&pane, move |pane, window, cx| { - pane.close_clean_items( - &close_clean_items_action, - window, - cx, - ) - .detach_and_log_err(cx) - })), - )) - .entry( - "Close All", - Some(Box::new(close_all_items_action.clone())), - window.handler_for(&pane, move |pane, window, cx| { - pane.close_all_items(&close_all_items_action, window, cx) - .detach_and_log_err(cx) - }), - ); - - let pin_tab_entries = |menu: ContextMenu| { - menu.separator().map(|this| { - if is_pinned { - this.entry( - "Unpin Tab", - Some(TogglePinTab.boxed_clone()), - window.handler_for(&pane, move |pane, window, cx| { - pane.unpin_tab_at(ix, window, cx); - }), - ) - } else { - this.entry( - "Pin Tab", - Some(TogglePinTab.boxed_clone()), - window.handler_for(&pane, move |pane, window, cx| { - pane.pin_tab_at(ix, window, cx); - }), - ) - } - }) - }; - if let Some(entry) = single_entry_to_resolve { - let project_path = pane - .read(cx) - .item_for_entry(entry, cx) - .and_then(|item| item.project_path(cx)); - let worktree = project_path.as_ref().and_then(|project_path| { - pane.read(cx) - .project - .upgrade()? - .read(cx) - .worktree_for_id(project_path.worktree_id, cx) - }); - let has_relative_path = worktree.as_ref().is_some_and(|worktree| { - worktree - .read(cx) - .root_entry() - .is_some_and(|entry| entry.is_dir()) - }); - - let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx); - let parent_abs_path = entry_abs_path - .as_deref() - .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf())); - let relative_path = project_path - .map(|project_path| project_path.path) - .filter(|_| has_relative_path); - - let visible_in_project_panel = relative_path.is_some() - && worktree.is_some_and(|worktree| worktree.read(cx).is_visible()); - - let entry_id = entry.to_proto(); - menu = menu - .separator() - .when_some(entry_abs_path, |menu, abs_path| { - menu.entry( - "Copy Path", - Some(Box::new(zed_actions::workspace::CopyPath)), - window.handler_for(&pane, move |_, _, cx| { - cx.write_to_clipboard(ClipboardItem::new_string( - abs_path.to_string_lossy().into_owned(), - )); - }), - ) - }) - .when_some(relative_path, |menu, relative_path| { - menu.entry( - "Copy Relative Path", - Some(Box::new(zed_actions::workspace::CopyRelativePath)), - window.handler_for(&pane, move |this, _, cx| { - let Some(project) = this.project.upgrade() else { - return; - }; - let path_style = project - .update(cx, |project, cx| project.path_style(cx)); - cx.write_to_clipboard(ClipboardItem::new_string( - relative_path.display(path_style).to_string(), - )); - }), - ) - }) - .map(pin_tab_entries) - .separator() - .when(visible_in_project_panel, |menu| { - menu.entry( - "Reveal In Project Panel", - Some(Box::new(RevealInProjectPanel::default())), - window.handler_for(&pane, move |pane, _, cx| { - pane.project - .update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel( - ProjectEntryId::from_proto(entry_id), - )) - }) - .ok(); - }), - ) - }) - .when_some(parent_abs_path, |menu, parent_abs_path| { - menu.entry( - "Open in Terminal", - Some(Box::new(OpenInTerminal)), - window.handler_for(&pane, move |_, window, cx| { - window.dispatch_action( - OpenTerminal { - working_directory: parent_abs_path.clone(), - } - .boxed_clone(), - cx, - ); - }), - ) - }); - } else { - menu = menu.map(pin_tab_entries); - } - } - - menu.context(menu_context) - }) - }) - } - - fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { - let focus_handle = self.focus_handle.clone(); - let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft) - .icon_size(IconSize::Small) - .on_click({ - let entity = cx.entity(); - move |_, window, cx| { - entity.update(cx, |pane, cx| { - pane.navigate_backward(&Default::default(), window, cx) - }) - } - }) - .disabled(!self.can_navigate_backward()) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Go Back", - &GoBack, - &window.focused(cx).unwrap_or_else(|| focus_handle.clone()), - cx, - ) - } - }); - - let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight) - .icon_size(IconSize::Small) - .on_click({ - let entity = cx.entity(); - move |_, window, cx| { - entity.update(cx, |pane, cx| { - pane.navigate_forward(&Default::default(), window, cx) - }) - } - }) - .disabled(!self.can_navigate_forward()) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Go Forward", - &GoForward, - &window.focused(cx).unwrap_or_else(|| focus_handle.clone()), - cx, - ) - } - }); - - let mut tab_items = self - .items - .iter() - .enumerate() - .zip(tab_details(&self.items, window, cx)) - .map(|((ix, item), detail)| { - self.render_tab(ix, &**item, detail, &focus_handle, window, cx) - }) - .collect::>(); - let tab_count = tab_items.len(); - if self.is_tab_pinned(tab_count) { - log::warn!( - "Pinned tab count ({}) exceeds actual tab count ({}). \ - This should not happen. If possible, add reproduction steps, \ - in a comment, to https://github.com/zed-industries/zed/issues/33342", - self.pinned_tab_count, - tab_count - ); - self.pinned_tab_count = tab_count; - } - let unpinned_tabs = tab_items.split_off(self.pinned_tab_count); - let pinned_tabs = tab_items; - - TabBar::new("tab_bar") - .when( - self.display_nav_history_buttons.unwrap_or_default(), - |tab_bar| { - tab_bar - .start_child(navigate_backward) - .start_child(navigate_forward) - }, - ) - .map(|tab_bar| { - if self.show_tab_bar_buttons { - let render_tab_buttons = self.render_tab_bar_buttons.clone(); - let (left_children, right_children) = render_tab_buttons(self, window, cx); - tab_bar - .start_children(left_children) - .end_children(right_children) - } else { - tab_bar - } - }) - .children(pinned_tabs.len().ne(&0).then(|| { - let max_scroll = self.tab_bar_scroll_handle.max_offset().width; - // We need to check both because offset returns delta values even when the scroll handle is not scrollable - let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.); - // Avoid flickering when max_offset is very small (< 2px). - // The border adds 1-2px which can push max_offset back to 0, creating a loop. - let is_scrollable = max_scroll > px(2.0); - let has_active_unpinned_tab = self.active_item_index >= self.pinned_tab_count; - h_flex() - .children(pinned_tabs) - .when(is_scrollable && is_scrolled, |this| { - this.when(has_active_unpinned_tab, |this| this.border_r_2()) - .when(!has_active_unpinned_tab, |this| this.border_r_1()) - .border_color(cx.theme().colors().border) - }) - })) - .child( - h_flex() - .id("unpinned tabs") - .overflow_x_scroll() - .w_full() - .track_scroll(&self.tab_bar_scroll_handle) - .on_scroll_wheel(cx.listener(|this, _, _, _| { - this.suppress_scroll = true; - })) - .children(unpinned_tabs) - .child( - div() - .id("tab_bar_drop_target") - .min_w_6() - // HACK: This empty child is currently necessary to force the drop target to appear - // despite us setting a min width above. - .child("") - // HACK: h_full doesn't occupy the complete height, using fixed height instead - .h(Tab::container_height(cx)) - .flex_grow() - .drag_over::(|bar, _, _, cx| { - bar.bg(cx.theme().colors().drop_target_background) - }) - .drag_over::(|bar, _, _, cx| { - bar.bg(cx.theme().colors().drop_target_background) - }) - .on_drop(cx.listener( - move |this, dragged_tab: &DraggedTab, window, cx| { - this.drag_split_direction = None; - this.handle_tab_drop(dragged_tab, this.items.len(), window, cx) - }, - )) - .on_drop(cx.listener( - move |this, selection: &DraggedSelection, window, cx| { - this.drag_split_direction = None; - this.handle_project_entry_drop( - &selection.active_selection.entry_id, - Some(tab_count), - window, - cx, - ) - }, - )) - .on_drop(cx.listener(move |this, paths, window, cx| { - this.drag_split_direction = None; - this.handle_external_paths_drop(paths, window, cx) - })) - .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| { - if event.click_count() == 2 { - window.dispatch_action( - this.double_click_dispatch_action.boxed_clone(), - cx, - ); - } - })), - ), - ) - .into_any_element() - } - - pub fn render_menu_overlay(menu: &Entity) -> Div { - div().absolute().bottom_0().right_0().size_0().child( - deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1), - ) - } - - pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context) { - self.zoomed = zoomed; - cx.notify(); - } - - pub fn is_zoomed(&self) -> bool { - self.zoomed - } - - fn handle_drag_move( - &mut self, - event: &DragMoveEvent, - window: &mut Window, - cx: &mut Context, - ) { - let can_split_predicate = self.can_split_predicate.take(); - let can_split = match &can_split_predicate { - Some(can_split_predicate) => { - can_split_predicate(self, event.dragged_item(), window, cx) - } - None => false, - }; - self.can_split_predicate = can_split_predicate; - if !can_split { - return; - } - - let rect = event.bounds.size; - - let size = event.bounds.size.width.min(event.bounds.size.height) - * WorkspaceSettings::get_global(cx).drop_target_size; - - let relative_cursor = Point::new( - event.event.position.x - event.bounds.left(), - event.event.position.y - event.bounds.top(), - ); - - let direction = if relative_cursor.x < size - || relative_cursor.x > rect.width - size - || relative_cursor.y < size - || relative_cursor.y > rect.height - size - { - [ - SplitDirection::Up, - SplitDirection::Right, - SplitDirection::Down, - SplitDirection::Left, - ] - .iter() - .min_by_key(|side| match side { - SplitDirection::Up => relative_cursor.y, - SplitDirection::Right => rect.width - relative_cursor.x, - SplitDirection::Down => rect.height - relative_cursor.y, - SplitDirection::Left => relative_cursor.x, - }) - .cloned() - } else { - None - }; - - if direction != self.drag_split_direction { - self.drag_split_direction = direction; - } - } - - pub fn handle_tab_drop( - &mut self, - dragged_tab: &DraggedTab, - ix: usize, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(custom_drop_handle) = self.custom_drop_handle.clone() - && let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) - { - return; - } - let mut to_pane = cx.entity(); - let split_direction = self.drag_split_direction; - let item_id = dragged_tab.item.item_id(); - self.unpreview_item_if_preview(item_id); - - let is_clone = cfg!(target_os = "macos") && window.modifiers().alt - || cfg!(not(target_os = "macos")) && window.modifiers().control; - - let from_pane = dragged_tab.pane.clone(); - - self.workspace - .update(cx, |_, cx| { - cx.defer_in(window, move |workspace, window, cx| { - if let Some(split_direction) = split_direction { - to_pane = workspace.split_pane(to_pane, split_direction, window, cx); - } - let database_id = workspace.database_id(); - let was_pinned_in_from_pane = from_pane.read_with(cx, |pane, _| { - pane.index_for_item_id(item_id) - .is_some_and(|ix| pane.is_tab_pinned(ix)) - }); - let to_pane_old_length = to_pane.read(cx).items.len(); - if is_clone { - let Some(item) = from_pane - .read(cx) - .items() - .find(|item| item.item_id() == item_id) - .cloned() - else { - return; - }; - if item.can_split(cx) { - let task = item.clone_on_split(database_id, window, cx); - let to_pane = to_pane.downgrade(); - cx.spawn_in(window, async move |_, cx| { - if let Some(item) = task.await { - to_pane - .update_in(cx, |pane, window, cx| { - pane.add_item(item, true, true, None, window, cx) - }) - .ok(); - } - }) - .detach(); - } else { - move_item(&from_pane, &to_pane, item_id, ix, true, window, cx); - } - } else { - move_item(&from_pane, &to_pane, item_id, ix, true, window, cx); - } - to_pane.update(cx, |this, _| { - if to_pane == from_pane { - let actual_ix = this - .items - .iter() - .position(|item| item.item_id() == item_id) - .unwrap_or(0); - - let is_pinned_in_to_pane = this.is_tab_pinned(actual_ix); - - if !was_pinned_in_from_pane && is_pinned_in_to_pane { - this.pinned_tab_count += 1; - } else if was_pinned_in_from_pane && !is_pinned_in_to_pane { - this.pinned_tab_count -= 1; - } - } else if this.items.len() >= to_pane_old_length { - let is_pinned_in_to_pane = this.is_tab_pinned(ix); - let item_created_pane = to_pane_old_length == 0; - let is_first_position = ix == 0; - let was_dropped_at_beginning = item_created_pane || is_first_position; - let should_remain_pinned = is_pinned_in_to_pane - || (was_pinned_in_from_pane && was_dropped_at_beginning); - - if should_remain_pinned { - this.pinned_tab_count += 1; - } - } - }); - }); - }) - .log_err(); - } - - fn handle_dragged_selection_drop( - &mut self, - dragged_selection: &DraggedSelection, - dragged_onto: Option, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(custom_drop_handle) = self.custom_drop_handle.clone() - && let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx) - { - return; - } - self.handle_project_entry_drop( - &dragged_selection.active_selection.entry_id, - dragged_onto, - window, - cx, - ); - } - - fn handle_project_entry_drop( - &mut self, - project_entry_id: &ProjectEntryId, - target: Option, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(custom_drop_handle) = self.custom_drop_handle.clone() - && let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) - { - return; - } - let mut to_pane = cx.entity(); - let split_direction = self.drag_split_direction; - let project_entry_id = *project_entry_id; - self.workspace - .update(cx, |_, cx| { - cx.defer_in(window, move |workspace, window, cx| { - if let Some(project_path) = workspace - .project() - .read(cx) - .path_for_entry(project_entry_id, cx) - { - let load_path_task = workspace.load_path(project_path.clone(), window, cx); - cx.spawn_in(window, async move |workspace, cx| { - if let Some((project_entry_id, build_item)) = - load_path_task.await.notify_async_err(cx) - { - let (to_pane, new_item_handle) = workspace - .update_in(cx, |workspace, window, cx| { - if let Some(split_direction) = split_direction { - to_pane = workspace.split_pane( - to_pane, - split_direction, - window, - cx, - ); - } - let new_item_handle = to_pane.update(cx, |pane, cx| { - pane.open_item( - project_entry_id, - project_path, - true, - false, - true, - target, - window, - cx, - build_item, - ) - }); - (to_pane, new_item_handle) - }) - .log_err()?; - to_pane - .update_in(cx, |this, window, cx| { - let Some(index) = this.index_for_item(&*new_item_handle) - else { - return; - }; - - if target.is_some_and(|target| this.is_tab_pinned(target)) { - this.pin_tab_at(index, window, cx); - } - }) - .ok()? - } - Some(()) - }) - .detach(); - }; - }); - }) - .log_err(); - } - - fn handle_external_paths_drop( - &mut self, - paths: &ExternalPaths, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(custom_drop_handle) = self.custom_drop_handle.clone() - && let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) - { - return; - } - let mut to_pane = cx.entity(); - let mut split_direction = self.drag_split_direction; - let paths = paths.paths().to_vec(); - let is_remote = self - .workspace - .update(cx, |workspace, cx| { - if workspace.project().read(cx).is_via_collab() { - workspace.show_error( - &anyhow::anyhow!("Cannot drop files on a remote project"), - cx, - ); - true - } else { - false - } - }) - .unwrap_or(true); - if is_remote { - return; - } - - self.workspace - .update(cx, |workspace, cx| { - let fs = Arc::clone(workspace.project().read(cx).fs()); - cx.spawn_in(window, async move |workspace, cx| { - let mut is_file_checks = FuturesUnordered::new(); - for path in &paths { - is_file_checks.push(fs.is_file(path)) - } - let mut has_files_to_open = false; - while let Some(is_file) = is_file_checks.next().await { - if is_file { - has_files_to_open = true; - break; - } - } - drop(is_file_checks); - if !has_files_to_open { - split_direction = None; - } - - if let Ok((open_task, to_pane)) = - workspace.update_in(cx, |workspace, window, cx| { - if let Some(split_direction) = split_direction { - to_pane = - workspace.split_pane(to_pane, split_direction, window, cx); - } - ( - workspace.open_paths( - paths, - OpenOptions { - visible: Some(OpenVisible::OnlyDirectories), - ..Default::default() - }, - Some(to_pane.downgrade()), - window, - cx, - ), - to_pane, - ) - }) - { - let opened_items: Vec<_> = open_task.await; - _ = workspace.update_in(cx, |workspace, window, cx| { - for item in opened_items.into_iter().flatten() { - if let Err(e) = item { - workspace.show_error(&e, cx); - } - } - if to_pane.read(cx).items_len() == 0 { - workspace.remove_pane(to_pane, None, window, cx); - } - }); - } - }) - .detach(); - }) - .log_err(); - } - - pub fn display_nav_history_buttons(&mut self, display: Option) { - self.display_nav_history_buttons = display; - } - - fn pinned_item_ids(&self) -> Vec { - self.items - .iter() - .enumerate() - .filter_map(|(index, item)| { - if self.is_tab_pinned(index) { - return Some(item.item_id()); - } - - None - }) - .collect() - } - - fn clean_item_ids(&self, cx: &mut Context) -> Vec { - self.items() - .filter_map(|item| { - if !item.is_dirty(cx) { - return Some(item.item_id()); - } - - None - }) - .collect() - } - - fn to_the_side_item_ids(&self, item_id: EntityId, side: Side) -> Vec { - match side { - Side::Left => self - .items() - .take_while(|item| item.item_id() != item_id) - .map(|item| item.item_id()) - .collect(), - Side::Right => self - .items() - .rev() - .take_while(|item| item.item_id() != item_id) - .map(|item| item.item_id()) - .collect(), - } - } - - fn multibuffer_item_ids(&self, cx: &mut Context) -> Vec { - self.items() - .filter(|item| item.buffer_kind(cx) == ItemBufferKind::Multibuffer) - .map(|item| item.item_id()) - .collect() - } - - pub fn drag_split_direction(&self) -> Option { - self.drag_split_direction - } - - pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) { - self.zoom_out_on_close = zoom_out_on_close; - } -} - -fn default_render_tab_bar_buttons( - pane: &mut Pane, - window: &mut Window, - cx: &mut Context, -) -> (Option, Option) { - if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) { - return (None, None); - } - let (can_clone, can_split_move) = match pane.active_item() { - Some(active_item) if active_item.can_split(cx) => (true, false), - Some(_) => (false, pane.items_len() > 1), - None => (false, false), - }; - // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s - // `end_slot`, but due to needing a view here that isn't possible. - let right_children = h_flex() - // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here. - .gap(DynamicSpacing::Base04.rems(cx)) - .child( - PopoverMenu::new("pane-tab-bar-popover-menu") - .trigger_with_tooltip( - IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small), - Tooltip::text("New..."), - ) - .anchor(Corner::TopRight) - .with_handle(pane.new_item_context_menu_handle.clone()) - .menu(move |window, cx| { - Some(ContextMenu::build(window, cx, |menu, _, _| { - menu.action("New File", NewFile.boxed_clone()) - .action("Open File", ToggleFileFinder::default().boxed_clone()) - .separator() - .action( - "Search Project", - DeploySearch { - replace_enabled: false, - included_files: None, - excluded_files: None, - } - .boxed_clone(), - ) - .action("Search Symbols", ToggleProjectSymbols.boxed_clone()) - .separator() - .action("New Terminal", NewTerminal.boxed_clone()) - })) - }), - ) - .child( - PopoverMenu::new("pane-tab-bar-split") - .trigger_with_tooltip( - IconButton::new("split", IconName::Split) - .icon_size(IconSize::Small) - .disabled(!can_clone && !can_split_move), - Tooltip::text("Split Pane"), - ) - .anchor(Corner::TopRight) - .with_handle(pane.split_item_context_menu_handle.clone()) - .menu(move |window, cx| { - ContextMenu::build(window, cx, |menu, _, _| { - if can_split_move { - menu.action("Split Right", SplitAndMoveRight.boxed_clone()) - .action("Split Left", SplitAndMoveLeft.boxed_clone()) - .action("Split Up", SplitAndMoveUp.boxed_clone()) - .action("Split Down", SplitAndMoveDown.boxed_clone()) - } else { - menu.action("Split Right", SplitRight.boxed_clone()) - .action("Split Left", SplitLeft.boxed_clone()) - .action("Split Up", SplitUp.boxed_clone()) - .action("Split Down", SplitDown.boxed_clone()) - } - }) - .into() - }), - ) - .child({ - let zoomed = pane.is_zoomed(); - IconButton::new("toggle_zoom", IconName::Maximize) - .icon_size(IconSize::Small) - .toggle_state(zoomed) - .selected_icon(IconName::Minimize) - .on_click(cx.listener(|pane, _, window, cx| { - pane.toggle_zoom(&crate::ToggleZoom, window, cx); - })) - .tooltip(move |_window, cx| { - Tooltip::for_action( - if zoomed { "Zoom Out" } else { "Zoom In" }, - &ToggleZoom, - cx, - ) - }) - }) - .into_any_element() - .into(); - (None, right_children) -} - -impl Focusable for Pane { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for Pane { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let mut key_context = KeyContext::new_with_defaults(); - key_context.add("Pane"); - if self.active_item().is_none() { - key_context.add("EmptyPane"); - } - - self.toolbar - .read(cx) - .contribute_context(&mut key_context, cx); - - let should_display_tab_bar = self.should_display_tab_bar.clone(); - let display_tab_bar = should_display_tab_bar(window, cx); - let Some(project) = self.project.upgrade() else { - return div().track_focus(&self.focus_handle(cx)); - }; - let is_local = project.read(cx).is_local(); - - v_flex() - .key_context(key_context) - .track_focus(&self.focus_handle(cx)) - .size_full() - .flex_none() - .overflow_hidden() - .on_action( - cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)), - ) - .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx))) - .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| { - pane.split(SplitDirection::horizontal(cx), cx) - })) - .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| { - pane.split(SplitDirection::vertical(cx), cx) - })) - .on_action( - cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)), - ) - .on_action( - cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)), - ) - .on_action(cx.listener(|pane, _: &SplitAndMoveUp, _, cx| { - pane.split_and_move(SplitDirection::Up, cx) - })) - .on_action(cx.listener(|pane, _: &SplitAndMoveDown, _, cx| { - pane.split_and_move(SplitDirection::Down, cx) - })) - .on_action(cx.listener(|pane, _: &SplitAndMoveLeft, _, cx| { - pane.split_and_move(SplitDirection::Left, cx) - })) - .on_action(cx.listener(|pane, _: &SplitAndMoveRight, _, cx| { - pane.split_and_move(SplitDirection::Right, cx) - })) - .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| { - cx.emit(Event::JoinIntoNext); - })) - .on_action(cx.listener(|_, _: &JoinAll, _, cx| { - cx.emit(Event::JoinAll); - })) - .on_action(cx.listener(Pane::toggle_zoom)) - .on_action(cx.listener(Self::navigate_backward)) - .on_action(cx.listener(Self::navigate_forward)) - .on_action( - cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| { - pane.activate_item( - action.0.min(pane.items.len().saturating_sub(1)), - true, - true, - window, - cx, - ); - }), - ) - .on_action(cx.listener(Self::alternate_file)) - .on_action(cx.listener(Self::activate_last_item)) - .on_action(cx.listener(Self::activate_previous_item)) - .on_action(cx.listener(Self::activate_next_item)) - .on_action(cx.listener(Self::swap_item_left)) - .on_action(cx.listener(Self::swap_item_right)) - .on_action(cx.listener(Self::toggle_pin_tab)) - .on_action(cx.listener(Self::unpin_all_tabs)) - .when(PreviewTabsSettings::get_global(cx).enabled, |this| { - this.on_action( - cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, window, cx| { - if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) { - if pane.is_active_preview_item(active_item_id) { - pane.unpreview_item_if_preview(active_item_id); - } else { - pane.replace_preview_item_id(active_item_id, window, cx); - } - } - }), - ) - }) - .on_action( - cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| { - pane.close_active_item(action, window, cx) - .detach_and_log_err(cx) - }), - ) - .on_action( - cx.listener(|pane: &mut Self, action: &CloseOtherItems, window, cx| { - pane.close_other_items(action, None, window, cx) - .detach_and_log_err(cx); - }), - ) - .on_action( - cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| { - pane.close_clean_items(action, window, cx) - .detach_and_log_err(cx) - }), - ) - .on_action(cx.listener( - |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| { - pane.close_items_to_the_left_by_id(None, action, window, cx) - .detach_and_log_err(cx) - }, - )) - .on_action(cx.listener( - |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| { - pane.close_items_to_the_right_by_id(None, action, window, cx) - .detach_and_log_err(cx) - }, - )) - .on_action( - cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| { - pane.close_all_items(action, window, cx) - .detach_and_log_err(cx) - }), - ) - .on_action(cx.listener( - |pane: &mut Self, action: &CloseMultibufferItems, window, cx| { - pane.close_multibuffer_items(action, window, cx) - .detach_and_log_err(cx) - }, - )) - .on_action( - cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| { - let entry_id = action - .entry_id - .map(ProjectEntryId::from_proto) - .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied()); - if let Some(entry_id) = entry_id { - pane.project - .update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(entry_id)) - }) - .ok(); - } - }), - ) - .on_action(cx.listener(|_, _: &menu::Cancel, window, cx| { - if cx.stop_active_drag(window) { - } else { - cx.propagate(); - } - })) - .when(self.active_item().is_some() && display_tab_bar, |pane| { - pane.child((self.render_tab_bar.clone())(self, window, cx)) - }) - .child({ - let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some(); - // main content - div() - .flex_1() - .relative() - .group("") - .overflow_hidden() - .on_drag_move::(cx.listener(Self::handle_drag_move)) - .on_drag_move::(cx.listener(Self::handle_drag_move)) - .when(is_local, |div| { - div.on_drag_move::(cx.listener(Self::handle_drag_move)) - }) - .map(|div| { - if let Some(item) = self.active_item() { - div.id("pane_placeholder") - .v_flex() - .size_full() - .overflow_hidden() - .child(self.toolbar.clone()) - .child(item.to_any_view()) - } else { - let placeholder = div - .id("pane_placeholder") - .h_flex() - .size_full() - .justify_center() - .on_click(cx.listener( - move |this, event: &ClickEvent, window, cx| { - if event.click_count() == 2 { - window.dispatch_action( - this.double_click_dispatch_action.boxed_clone(), - cx, - ); - } - }, - )); - if has_worktrees { - placeholder - } else { - placeholder.child( - Label::new("Open a file or project to get started.") - .color(Color::Muted), - ) - } - } - }) - .child( - // drag target - div() - .invisible() - .absolute() - .bg(cx.theme().colors().drop_target_background) - .group_drag_over::("", |style| style.visible()) - .group_drag_over::("", |style| style.visible()) - .when(is_local, |div| { - div.group_drag_over::("", |style| style.visible()) - }) - .when_some(self.can_drop_predicate.clone(), |this, p| { - this.can_drop(move |a, window, cx| p(a, window, cx)) - }) - .on_drop(cx.listener(move |this, dragged_tab, window, cx| { - this.handle_tab_drop( - dragged_tab, - this.active_item_index(), - window, - cx, - ) - })) - .on_drop(cx.listener( - move |this, selection: &DraggedSelection, window, cx| { - this.handle_dragged_selection_drop(selection, None, window, cx) - }, - )) - .on_drop(cx.listener(move |this, paths, window, cx| { - this.handle_external_paths_drop(paths, window, cx) - })) - .map(|div| { - let size = DefiniteLength::Fraction(0.5); - match self.drag_split_direction { - None => div.top_0().right_0().bottom_0().left_0(), - Some(SplitDirection::Up) => { - div.top_0().left_0().right_0().h(size) - } - Some(SplitDirection::Down) => { - div.left_0().bottom_0().right_0().h(size) - } - Some(SplitDirection::Left) => { - div.top_0().left_0().bottom_0().w(size) - } - Some(SplitDirection::Right) => { - div.top_0().bottom_0().right_0().w(size) - } - } - }), - ) - }) - .on_mouse_down( - MouseButton::Navigate(NavigationDirection::Back), - cx.listener(|pane, _, window, cx| { - if let Some(workspace) = pane.workspace.upgrade() { - let pane = cx.entity().downgrade(); - window.defer(cx, move |window, cx| { - workspace.update(cx, |workspace, cx| { - workspace.go_back(pane, window, cx).detach_and_log_err(cx) - }) - }) - } - }), - ) - .on_mouse_down( - MouseButton::Navigate(NavigationDirection::Forward), - cx.listener(|pane, _, window, cx| { - if let Some(workspace) = pane.workspace.upgrade() { - let pane = cx.entity().downgrade(); - window.defer(cx, move |window, cx| { - workspace.update(cx, |workspace, cx| { - workspace - .go_forward(pane, window, cx) - .detach_and_log_err(cx) - }) - }) - } - }), - ) - } -} - -impl ItemNavHistory { - pub fn push(&mut self, data: Option, cx: &mut App) { - if self - .item - .upgrade() - .is_some_and(|item| item.include_in_nav_history()) - { - self.history - .push(data, self.item.clone(), self.is_preview, cx); - } - } - - pub fn pop_backward(&mut self, cx: &mut App) -> Option { - self.history.pop(NavigationMode::GoingBack, cx) - } - - pub fn pop_forward(&mut self, cx: &mut App) -> Option { - self.history.pop(NavigationMode::GoingForward, cx) - } -} - -impl NavHistory { - pub fn for_each_entry( - &self, - cx: &App, - mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option)), - ) { - let borrowed_history = self.0.lock(); - borrowed_history - .forward_stack - .iter() - .chain(borrowed_history.backward_stack.iter()) - .chain(borrowed_history.closed_stack.iter()) - .for_each(|entry| { - if let Some(project_and_abs_path) = - borrowed_history.paths_by_item.get(&entry.item.id()) - { - f(entry, project_and_abs_path.clone()); - } else if let Some(item) = entry.item.upgrade() - && let Some(path) = item.project_path(cx) - { - f(entry, (path, None)); - } - }) - } - - pub fn set_mode(&mut self, mode: NavigationMode) { - self.0.lock().mode = mode; - } - - pub fn mode(&self) -> NavigationMode { - self.0.lock().mode - } - - pub fn disable(&mut self) { - self.0.lock().mode = NavigationMode::Disabled; - } - - pub fn enable(&mut self) { - self.0.lock().mode = NavigationMode::Normal; - } - - pub fn clear(&mut self, cx: &mut App) { - let mut state = self.0.lock(); - - if state.backward_stack.is_empty() - && state.forward_stack.is_empty() - && state.closed_stack.is_empty() - && state.paths_by_item.is_empty() - { - return; - } - - state.mode = NavigationMode::Normal; - state.backward_stack.clear(); - state.forward_stack.clear(); - state.closed_stack.clear(); - state.paths_by_item.clear(); - state.did_update(cx); - } - - pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option { - let mut state = self.0.lock(); - let entry = match mode { - NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => { - return None; - } - NavigationMode::GoingBack => &mut state.backward_stack, - NavigationMode::GoingForward => &mut state.forward_stack, - NavigationMode::ReopeningClosedItem => &mut state.closed_stack, - } - .pop_back(); - if entry.is_some() { - state.did_update(cx); - } - entry - } - - pub fn push( - &mut self, - data: Option, - item: Arc, - is_preview: bool, - cx: &mut App, - ) { - let state = &mut *self.0.lock(); - match state.mode { - NavigationMode::Disabled => {} - NavigationMode::Normal | NavigationMode::ReopeningClosedItem => { - if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { - state.backward_stack.pop_front(); - } - state.backward_stack.push_back(NavigationEntry { - item, - data: data.map(|data| Box::new(data) as Box), - timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), - is_preview, - }); - state.forward_stack.clear(); - } - NavigationMode::GoingBack => { - if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { - state.forward_stack.pop_front(); - } - state.forward_stack.push_back(NavigationEntry { - item, - data: data.map(|data| Box::new(data) as Box), - timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), - is_preview, - }); - } - NavigationMode::GoingForward => { - if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { - state.backward_stack.pop_front(); - } - state.backward_stack.push_back(NavigationEntry { - item, - data: data.map(|data| Box::new(data) as Box), - timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), - is_preview, - }); - } - NavigationMode::ClosingItem if is_preview => return, - NavigationMode::ClosingItem => { - if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { - state.closed_stack.pop_front(); - } - state.closed_stack.push_back(NavigationEntry { - item, - data: data.map(|data| Box::new(data) as Box), - timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), - is_preview, - }); - } - } - state.did_update(cx); - } - - pub fn remove_item(&mut self, item_id: EntityId) { - let mut state = self.0.lock(); - state.paths_by_item.remove(&item_id); - state - .backward_stack - .retain(|entry| entry.item.id() != item_id); - state - .forward_stack - .retain(|entry| entry.item.id() != item_id); - state - .closed_stack - .retain(|entry| entry.item.id() != item_id); - } - - pub fn rename_item( - &mut self, - item_id: EntityId, - project_path: ProjectPath, - abs_path: Option, - ) { - let mut state = self.0.lock(); - let path_for_item = state.paths_by_item.get_mut(&item_id); - if let Some(path_for_item) = path_for_item { - path_for_item.0 = project_path; - path_for_item.1 = abs_path; - } - } - - pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option)> { - self.0.lock().paths_by_item.get(&item_id).cloned() - } -} - -impl NavHistoryState { - pub fn did_update(&self, cx: &mut App) { - if let Some(pane) = self.pane.upgrade() { - cx.defer(move |cx| { - pane.update(cx, |pane, cx| pane.history_updated(cx)); - }); - } - } -} - -fn dirty_message_for(buffer_path: Option, path_style: PathStyle) -> String { - let path = buffer_path - .as_ref() - .and_then(|p| { - let path = p.path.display(path_style); - if path.is_empty() { None } else { Some(path) } - }) - .unwrap_or("This buffer".into()); - let path = truncate_and_remove_front(&path, 80); - format!("{path} contains unsaved edits. Do you want to save it?") -} - -pub fn tab_details(items: &[Box], _window: &Window, cx: &App) -> Vec { - let mut tab_details = items.iter().map(|_| 0).collect::>(); - let mut tab_descriptions = HashMap::default(); - let mut done = false; - while !done { - done = true; - - // Store item indices by their tab description. - for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() { - let description = item.tab_content_text(*detail, cx); - if *detail == 0 || description != item.tab_content_text(detail - 1, cx) { - tab_descriptions - .entry(description) - .or_insert(Vec::new()) - .push(ix); - } - } - - // If two or more items have the same tab description, increase their level - // of detail and try again. - for (_, item_ixs) in tab_descriptions.drain() { - if item_ixs.len() > 1 { - done = false; - for ix in item_ixs { - tab_details[ix] += 1; - } - } - } - } - - tab_details -} - -pub fn render_item_indicator(item: Box, cx: &App) -> Option { - maybe!({ - let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) { - (true, _) => Color::Warning, - (_, true) => Color::Accent, - (false, false) => return None, - }; - - Some(Indicator::dot().color(indicator_color)) - }) -} - -impl Render for DraggedTab { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let ui_font = ThemeSettings::get_global(cx).ui_font.clone(); - let label = self.item.tab_content( - TabContentParams { - detail: Some(self.detail), - selected: false, - preview: false, - deemphasized: false, - }, - window, - cx, - ); - Tab::new("") - .toggle_state(self.is_active) - .child(label) - .render(window, cx) - .font(ui_font) - } -} - -#[cfg(test)] -mod tests { - use std::num::NonZero; - - use super::*; - use crate::item::test::{TestItem, TestProjectItem}; - use gpui::{TestAppContext, VisualTestContext, size}; - use project::FakeFs; - use settings::SettingsStore; - use theme::LoadThemes; - use util::TryFutureExt; - - #[gpui::test] - async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - for i in 0..7 { - add_labeled_item(&pane, format!("{}", i).as_str(), false, cx); - } - - set_max_tabs(cx, Some(5)); - add_labeled_item(&pane, "7", false, cx); - // Remove items to respect the max tab cap. - assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(0, false, false, window, cx); - }); - add_labeled_item(&pane, "X", false, cx); - // Respect activation order. - assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx); - - for i in 0..7 { - add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx); - } - // Keeps dirty items, even over max tab cap. - assert_item_labels( - &pane, - ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"], - cx, - ); - - set_max_tabs(cx, None); - for i in 0..7 { - add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx); - } - // No cap when max tabs is None. - assert_item_labels( - &pane, - [ - "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4", - "N5", "N6*", - ], - cx, - ); - } - - #[gpui::test] - async fn test_reduce_max_tabs_closes_existing_items(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - add_labeled_item(&pane, "A", false, cx); - add_labeled_item(&pane, "B", false, cx); - let item_c = add_labeled_item(&pane, "C", false, cx); - let item_d = add_labeled_item(&pane, "D", false, cx); - add_labeled_item(&pane, "E", false, cx); - add_labeled_item(&pane, "Settings", false, cx); - assert_item_labels(&pane, ["A", "B", "C", "D", "E", "Settings*"], cx); - - set_max_tabs(cx, Some(5)); - assert_item_labels(&pane, ["B", "C", "D", "E", "Settings*"], cx); - - set_max_tabs(cx, Some(4)); - assert_item_labels(&pane, ["C", "D", "E", "Settings*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_d.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["C!", "D!", "E", "Settings*"], cx); - - set_max_tabs(cx, Some(2)); - assert_item_labels(&pane, ["C!", "D!", "Settings*"], cx); - } - - #[gpui::test] - async fn test_allow_pinning_dirty_item_at_max_tabs(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - set_max_tabs(cx, Some(1)); - let item_a = add_labeled_item(&pane, "A", true, cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A*^!"], cx); - } - - #[gpui::test] - async fn test_allow_pinning_non_dirty_item_at_max_tabs(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - set_max_tabs(cx, Some(1)); - let item_a = add_labeled_item(&pane, "A", false, cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A*!"], cx); - } - - #[gpui::test] - async fn test_pin_tabs_incrementally_at_max_capacity(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - set_max_tabs(cx, Some(3)); - - let item_a = add_labeled_item(&pane, "A", false, cx); - assert_item_labels(&pane, ["A*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A*!"], cx); - - let item_b = add_labeled_item(&pane, "B", false, cx); - assert_item_labels(&pane, ["A!", "B*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A!", "B*!"], cx); - - let item_c = add_labeled_item(&pane, "C", false, cx); - assert_item_labels(&pane, ["A!", "B!", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A!", "B!", "C*!"], cx); - } - - #[gpui::test] - async fn test_pin_tabs_left_to_right_after_opening_at_max_capacity(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - set_max_tabs(cx, Some(3)); - - let item_a = add_labeled_item(&pane, "A", false, cx); - assert_item_labels(&pane, ["A*"], cx); - - let item_b = add_labeled_item(&pane, "B", false, cx); - assert_item_labels(&pane, ["A", "B*"], cx); - - let item_c = add_labeled_item(&pane, "C", false, cx); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A!", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A!", "B!", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A!", "B!", "C*!"], cx); - } - - #[gpui::test] - async fn test_pin_tabs_right_to_left_after_opening_at_max_capacity(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - set_max_tabs(cx, Some(3)); - - let item_a = add_labeled_item(&pane, "A", false, cx); - assert_item_labels(&pane, ["A*"], cx); - - let item_b = add_labeled_item(&pane, "B", false, cx); - assert_item_labels(&pane, ["A", "B*"], cx); - - let item_c = add_labeled_item(&pane, "C", false, cx); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["C*!", "A", "B"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["C*!", "B!", "A"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["C*!", "B!", "A!"], cx); - } - - #[gpui::test] - async fn test_pinned_tabs_never_closed_at_max_tabs(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - let item_a = add_labeled_item(&pane, "A", false, cx); - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - - let item_b = add_labeled_item(&pane, "B", false, cx); - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - - add_labeled_item(&pane, "C", false, cx); - add_labeled_item(&pane, "D", false, cx); - add_labeled_item(&pane, "E", false, cx); - assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx); - - set_max_tabs(cx, Some(3)); - add_labeled_item(&pane, "F", false, cx); - assert_item_labels(&pane, ["A!", "B!", "F*"], cx); - - add_labeled_item(&pane, "G", false, cx); - assert_item_labels(&pane, ["A!", "B!", "G*"], cx); - - add_labeled_item(&pane, "H", false, cx); - assert_item_labels(&pane, ["A!", "B!", "H*"], cx); - } - - #[gpui::test] - async fn test_always_allows_one_unpinned_item_over_max_tabs_regardless_of_pinned_count( - cx: &mut TestAppContext, - ) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - set_max_tabs(cx, Some(3)); - - let item_a = add_labeled_item(&pane, "A", false, cx); - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - - let item_b = add_labeled_item(&pane, "B", false, cx); - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - - let item_c = add_labeled_item(&pane, "C", false, cx); - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - - assert_item_labels(&pane, ["A!", "B!", "C*!"], cx); - - let item_d = add_labeled_item(&pane, "D", false, cx); - assert_item_labels(&pane, ["A!", "B!", "C!", "D*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_d.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A!", "B!", "C!", "D*!"], cx); - - add_labeled_item(&pane, "E", false, cx); - assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "E*"], cx); - - add_labeled_item(&pane, "F", false, cx); - assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "F*"], cx); - } - - #[gpui::test] - async fn test_can_open_one_item_when_all_tabs_are_dirty_at_max(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - set_max_tabs(cx, Some(3)); - - add_labeled_item(&pane, "A", true, cx); - assert_item_labels(&pane, ["A*^"], cx); - - add_labeled_item(&pane, "B", true, cx); - assert_item_labels(&pane, ["A^", "B*^"], cx); - - add_labeled_item(&pane, "C", true, cx); - assert_item_labels(&pane, ["A^", "B^", "C*^"], cx); - - add_labeled_item(&pane, "D", false, cx); - assert_item_labels(&pane, ["A^", "B^", "C^", "D*"], cx); - - add_labeled_item(&pane, "E", false, cx); - assert_item_labels(&pane, ["A^", "B^", "C^", "E*"], cx); - - add_labeled_item(&pane, "F", false, cx); - assert_item_labels(&pane, ["A^", "B^", "C^", "F*"], cx); - - add_labeled_item(&pane, "G", true, cx); - assert_item_labels(&pane, ["A^", "B^", "C^", "G*^"], cx); - } - - #[gpui::test] - async fn test_toggle_pin_tab(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - set_labeled_items(&pane, ["A", "B*", "C"], cx); - assert_item_labels(&pane, ["A", "B*", "C"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.toggle_pin_tab(&TogglePinTab, window, cx); - }); - assert_item_labels(&pane, ["B*!", "A", "C"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.toggle_pin_tab(&TogglePinTab, window, cx); - }); - assert_item_labels(&pane, ["B*", "A", "C"], cx); - } - - #[gpui::test] - async fn test_unpin_all_tabs(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Unpin all, in an empty pane - pane.update_in(cx, |pane, window, cx| { - pane.unpin_all_tabs(&UnpinAllTabs, window, cx); - }); - - assert_item_labels(&pane, [], cx); - - let item_a = add_labeled_item(&pane, "A", false, cx); - let item_b = add_labeled_item(&pane, "B", false, cx); - let item_c = add_labeled_item(&pane, "C", false, cx); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - // Unpin all, when no tabs are pinned - pane.update_in(cx, |pane, window, cx| { - pane.unpin_all_tabs(&UnpinAllTabs, window, cx); - }); - - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - // Pin inactive tabs only - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A!", "B!", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.unpin_all_tabs(&UnpinAllTabs, window, cx); - }); - - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - // Pin all tabs - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A!", "B!", "C*!"], cx); - - // Activate middle tab - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(1, false, false, window, cx); - }); - assert_item_labels(&pane, ["A!", "B*!", "C!"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.unpin_all_tabs(&UnpinAllTabs, window, cx); - }); - - // Order has not changed - assert_item_labels(&pane, ["A", "B*", "C"], cx); - } - - #[gpui::test] - async fn test_pinning_active_tab_without_position_change_maintains_focus( - cx: &mut TestAppContext, - ) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A - let item_a = add_labeled_item(&pane, "A", false, cx); - assert_item_labels(&pane, ["A*"], cx); - - // Add B - add_labeled_item(&pane, "B", false, cx); - assert_item_labels(&pane, ["A", "B*"], cx); - - // Activate A again - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.activate_item(ix, true, true, window, cx); - }); - assert_item_labels(&pane, ["A*", "B"], cx); - - // Pin A - remains active - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A*!", "B"], cx); - - // Unpin A - remain active - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.unpin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A*", "B"], cx); - } - - #[gpui::test] - async fn test_pinning_active_tab_with_position_change_maintains_focus(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B, C - add_labeled_item(&pane, "A", false, cx); - add_labeled_item(&pane, "B", false, cx); - let item_c = add_labeled_item(&pane, "C", false, cx); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - // Pin C - moves to pinned area, remains active - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["C*!", "A", "B"], cx); - - // Unpin C - moves after pinned area, remains active - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.unpin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["C*", "A", "B"], cx); - } - - #[gpui::test] - async fn test_pinning_inactive_tab_without_position_change_preserves_existing_focus( - cx: &mut TestAppContext, - ) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B - let item_a = add_labeled_item(&pane, "A", false, cx); - add_labeled_item(&pane, "B", false, cx); - assert_item_labels(&pane, ["A", "B*"], cx); - - // Pin A - already in pinned area, B remains active - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A!", "B*"], cx); - - // Unpin A - stays in place, B remains active - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.unpin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A", "B*"], cx); - } - - #[gpui::test] - async fn test_pinning_inactive_tab_with_position_change_preserves_existing_focus( - cx: &mut TestAppContext, - ) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B, C - add_labeled_item(&pane, "A", false, cx); - let item_b = add_labeled_item(&pane, "B", false, cx); - let item_c = add_labeled_item(&pane, "C", false, cx); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - // Activate B - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.activate_item(ix, true, true, window, cx); - }); - assert_item_labels(&pane, ["A", "B*", "C"], cx); - - // Pin C - moves to pinned area, B remains active - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["C!", "A", "B*"], cx); - - // Unpin C - moves after pinned area, B remains active - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.unpin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["C", "A", "B*"], cx); - } - - #[gpui::test] - async fn test_drag_unpinned_tab_to_split_creates_pane_with_unpinned_tab( - cx: &mut TestAppContext, - ) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B. Pin B. Activate A - let item_a = add_labeled_item(&pane_a, "A", false, cx); - let item_b = add_labeled_item(&pane_a, "B", false, cx); - - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.activate_item(ix, true, true, window, cx); - }); - - // Drag A to create new split - pane_a.update_in(cx, |pane, window, cx| { - pane.drag_split_direction = Some(SplitDirection::Right); - - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 0, window, cx); - }); - - // A should be moved to new pane. B should remain pinned, A should not be pinned - let (pane_a, pane_b) = workspace.read_with(cx, |workspace, _| { - let panes = workspace.panes(); - (panes[0].clone(), panes[1].clone()) - }); - assert_item_labels(&pane_a, ["B*!"], cx); - assert_item_labels(&pane_b, ["A*"], cx); - } - - #[gpui::test] - async fn test_drag_pinned_tab_to_split_creates_pane_with_pinned_tab(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B. Pin both. Activate A - let item_a = add_labeled_item(&pane_a, "A", false, cx); - let item_b = add_labeled_item(&pane_a, "B", false, cx); - - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.activate_item(ix, true, true, window, cx); - }); - assert_item_labels(&pane_a, ["A*!", "B!"], cx); - - // Drag A to create new split - pane_a.update_in(cx, |pane, window, cx| { - pane.drag_split_direction = Some(SplitDirection::Right); - - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 0, window, cx); - }); - - // A should be moved to new pane. Both A and B should still be pinned - let (pane_a, pane_b) = workspace.read_with(cx, |workspace, _| { - let panes = workspace.panes(); - (panes[0].clone(), panes[1].clone()) - }); - assert_item_labels(&pane_a, ["B*!"], cx); - assert_item_labels(&pane_b, ["A*!"], cx); - } - - #[gpui::test] - async fn test_drag_pinned_tab_into_existing_panes_pinned_region(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A to pane A and pin - let item_a = add_labeled_item(&pane_a, "A", false, cx); - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A*!"], cx); - - // Add B to pane B and pin - let pane_b = workspace.update_in(cx, |workspace, window, cx| { - workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx) - }); - let item_b = add_labeled_item(&pane_b, "B", false, cx); - pane_b.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_b, ["B*!"], cx); - - // Move A from pane A to pane B's pinned region - pane_b.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 0, window, cx); - }); - - // A should stay pinned - assert_item_labels(&pane_a, [], cx); - assert_item_labels(&pane_b, ["A*!", "B!"], cx); - } - - #[gpui::test] - async fn test_drag_pinned_tab_into_existing_panes_unpinned_region(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A to pane A and pin - let item_a = add_labeled_item(&pane_a, "A", false, cx); - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A*!"], cx); - - // Create pane B with pinned item B - let pane_b = workspace.update_in(cx, |workspace, window, cx| { - workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx) - }); - let item_b = add_labeled_item(&pane_b, "B", false, cx); - assert_item_labels(&pane_b, ["B*"], cx); - - pane_b.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_b, ["B*!"], cx); - - // Move A from pane A to pane B's unpinned region - pane_b.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 1, window, cx); - }); - - // A should become pinned - assert_item_labels(&pane_a, [], cx); - assert_item_labels(&pane_b, ["B!", "A*"], cx); - } - - #[gpui::test] - async fn test_drag_pinned_tab_into_existing_panes_first_position_with_no_pinned_tabs( - cx: &mut TestAppContext, - ) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A to pane A and pin - let item_a = add_labeled_item(&pane_a, "A", false, cx); - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A*!"], cx); - - // Add B to pane B - let pane_b = workspace.update_in(cx, |workspace, window, cx| { - workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx) - }); - add_labeled_item(&pane_b, "B", false, cx); - assert_item_labels(&pane_b, ["B*"], cx); - - // Move A from pane A to position 0 in pane B, indicating it should stay pinned - pane_b.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 0, window, cx); - }); - - // A should stay pinned - assert_item_labels(&pane_a, [], cx); - assert_item_labels(&pane_b, ["A*!", "B"], cx); - } - - #[gpui::test] - async fn test_drag_pinned_tab_into_existing_pane_at_max_capacity_closes_unpinned_tabs( - cx: &mut TestAppContext, - ) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - set_max_tabs(cx, Some(2)); - - // Add A, B to pane A. Pin both - let item_a = add_labeled_item(&pane_a, "A", false, cx); - let item_b = add_labeled_item(&pane_a, "B", false, cx); - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A!", "B*!"], cx); - - // Add C, D to pane B. Pin both - let pane_b = workspace.update_in(cx, |workspace, window, cx| { - workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx) - }); - let item_c = add_labeled_item(&pane_b, "C", false, cx); - let item_d = add_labeled_item(&pane_b, "D", false, cx); - pane_b.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_d.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_b, ["C!", "D*!"], cx); - - // Add a third unpinned item to pane B (exceeds max tabs), but is allowed, - // as we allow 1 tab over max if the others are pinned or dirty - add_labeled_item(&pane_b, "E", false, cx); - assert_item_labels(&pane_b, ["C!", "D!", "E*"], cx); - - // Drag pinned A from pane A to position 0 in pane B - pane_b.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 0, window, cx); - }); - - // E (unpinned) should be closed, leaving 3 pinned items - assert_item_labels(&pane_a, ["B*!"], cx); - assert_item_labels(&pane_b, ["A*!", "C!", "D!"], cx); - } - - #[gpui::test] - async fn test_drag_last_pinned_tab_to_same_position_stays_pinned(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A to pane A and pin it - let item_a = add_labeled_item(&pane_a, "A", false, cx); - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A*!"], cx); - - // Drag pinned A to position 1 (directly to the right) in the same pane - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 1, window, cx); - }); - - // A should still be pinned and active - assert_item_labels(&pane_a, ["A*!"], cx); - } - - #[gpui::test] - async fn test_drag_pinned_tab_beyond_last_pinned_tab_in_same_pane_stays_pinned( - cx: &mut TestAppContext, - ) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B to pane A and pin both - let item_a = add_labeled_item(&pane_a, "A", false, cx); - let item_b = add_labeled_item(&pane_a, "B", false, cx); - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A!", "B*!"], cx); - - // Drag pinned A right of B in the same pane - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 2, window, cx); - }); - - // A stays pinned - assert_item_labels(&pane_a, ["B!", "A*!"], cx); - } - - #[gpui::test] - async fn test_dragging_pinned_tab_onto_unpinned_tab_reduces_unpinned_tab_count( - cx: &mut TestAppContext, - ) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B to pane A and pin A - let item_a = add_labeled_item(&pane_a, "A", false, cx); - add_labeled_item(&pane_a, "B", false, cx); - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A!", "B*"], cx); - - // Drag pinned A on top of B in the same pane, which changes tab order to B, A - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 1, window, cx); - }); - - // Neither are pinned - assert_item_labels(&pane_a, ["B", "A*"], cx); - } - - #[gpui::test] - async fn test_drag_pinned_tab_beyond_unpinned_tab_in_same_pane_becomes_unpinned( - cx: &mut TestAppContext, - ) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B to pane A and pin A - let item_a = add_labeled_item(&pane_a, "A", false, cx); - add_labeled_item(&pane_a, "B", false, cx); - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A!", "B*"], cx); - - // Drag pinned A right of B in the same pane - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 2, window, cx); - }); - - // A becomes unpinned - assert_item_labels(&pane_a, ["B", "A*"], cx); - } - - #[gpui::test] - async fn test_drag_unpinned_tab_in_front_of_pinned_tab_in_same_pane_becomes_pinned( - cx: &mut TestAppContext, - ) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B to pane A and pin A - let item_a = add_labeled_item(&pane_a, "A", false, cx); - let item_b = add_labeled_item(&pane_a, "B", false, cx); - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A!", "B*"], cx); - - // Drag pinned B left of A in the same pane - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_b.boxed_clone(), - ix: 1, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 0, window, cx); - }); - - // A becomes unpinned - assert_item_labels(&pane_a, ["B*!", "A!"], cx); - } - - #[gpui::test] - async fn test_drag_unpinned_tab_to_the_pinned_region_stays_pinned(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B, C to pane A and pin A - let item_a = add_labeled_item(&pane_a, "A", false, cx); - add_labeled_item(&pane_a, "B", false, cx); - let item_c = add_labeled_item(&pane_a, "C", false, cx); - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A!", "B", "C*"], cx); - - // Drag pinned C left of B in the same pane - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_c.boxed_clone(), - ix: 2, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 1, window, cx); - }); - - // A stays pinned, B and C remain unpinned - assert_item_labels(&pane_a, ["A!", "C*", "B"], cx); - } - - #[gpui::test] - async fn test_drag_unpinned_tab_into_existing_panes_pinned_region(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add unpinned item A to pane A - let item_a = add_labeled_item(&pane_a, "A", false, cx); - assert_item_labels(&pane_a, ["A*"], cx); - - // Create pane B with pinned item B - let pane_b = workspace.update_in(cx, |workspace, window, cx| { - workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx) - }); - let item_b = add_labeled_item(&pane_b, "B", false, cx); - pane_b.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_b, ["B*!"], cx); - - // Move A from pane A to pane B's pinned region - pane_b.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 0, window, cx); - }); - - // A should become pinned since it was dropped in the pinned region - assert_item_labels(&pane_a, [], cx); - assert_item_labels(&pane_b, ["A*!", "B!"], cx); - } - - #[gpui::test] - async fn test_drag_unpinned_tab_into_existing_panes_unpinned_region(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add unpinned item A to pane A - let item_a = add_labeled_item(&pane_a, "A", false, cx); - assert_item_labels(&pane_a, ["A*"], cx); - - // Create pane B with one pinned item B - let pane_b = workspace.update_in(cx, |workspace, window, cx| { - workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx) - }); - let item_b = add_labeled_item(&pane_b, "B", false, cx); - pane_b.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_b, ["B*!"], cx); - - // Move A from pane A to pane B's unpinned region - pane_b.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 1, window, cx); - }); - - // A should remain unpinned since it was dropped outside the pinned region - assert_item_labels(&pane_a, [], cx); - assert_item_labels(&pane_b, ["B!", "A*"], cx); - } - - #[gpui::test] - async fn test_drag_pinned_tab_throughout_entire_range_of_pinned_tabs_both_directions( - cx: &mut TestAppContext, - ) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B, C and pin all - let item_a = add_labeled_item(&pane_a, "A", false, cx); - let item_b = add_labeled_item(&pane_a, "B", false, cx); - let item_c = add_labeled_item(&pane_a, "C", false, cx); - assert_item_labels(&pane_a, ["A", "B", "C*"], cx); - - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A!", "B!", "C*!"], cx); - - // Move A to right of B - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 1, window, cx); - }); - - // A should be after B and all are pinned - assert_item_labels(&pane_a, ["B!", "A*!", "C!"], cx); - - // Move A to right of C - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 1, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 2, window, cx); - }); - - // A should be after C and all are pinned - assert_item_labels(&pane_a, ["B!", "C!", "A*!"], cx); - - // Move A to left of C - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 2, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 1, window, cx); - }); - - // A should be before C and all are pinned - assert_item_labels(&pane_a, ["B!", "A*!", "C!"], cx); - - // Move A to left of B - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 1, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 0, window, cx); - }); - - // A should be before B and all are pinned - assert_item_labels(&pane_a, ["A*!", "B!", "C!"], cx); - } - - #[gpui::test] - async fn test_drag_first_tab_to_last_position(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B, C - let item_a = add_labeled_item(&pane_a, "A", false, cx); - add_labeled_item(&pane_a, "B", false, cx); - add_labeled_item(&pane_a, "C", false, cx); - assert_item_labels(&pane_a, ["A", "B", "C*"], cx); - - // Move A to the end - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 2, window, cx); - }); - - // A should be at the end - assert_item_labels(&pane_a, ["B", "C", "A*"], cx); - } - - #[gpui::test] - async fn test_drag_last_tab_to_first_position(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B, C - add_labeled_item(&pane_a, "A", false, cx); - add_labeled_item(&pane_a, "B", false, cx); - let item_c = add_labeled_item(&pane_a, "C", false, cx); - assert_item_labels(&pane_a, ["A", "B", "C*"], cx); - - // Move C to the beginning - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_c.boxed_clone(), - ix: 2, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 0, window, cx); - }); - - // C should be at the beginning - assert_item_labels(&pane_a, ["C*", "A", "B"], cx); - } - - #[gpui::test] - async fn test_add_item_with_new_item(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // 1. Add with a destination index - // a. Add before the active item - set_labeled_items(&pane, ["A", "B*", "C"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item( - Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))), - false, - false, - Some(0), - window, - cx, - ); - }); - assert_item_labels(&pane, ["D*", "A", "B", "C"], cx); - - // b. Add after the active item - set_labeled_items(&pane, ["A", "B*", "C"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item( - Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))), - false, - false, - Some(2), - window, - cx, - ); - }); - assert_item_labels(&pane, ["A", "B", "D*", "C"], cx); - - // c. Add at the end of the item list (including off the length) - set_labeled_items(&pane, ["A", "B*", "C"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item( - Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))), - false, - false, - Some(5), - window, - cx, - ); - }); - assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); - - // 2. Add without a destination index - // a. Add with active item at the start of the item list - set_labeled_items(&pane, ["A*", "B", "C"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item( - Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))), - false, - false, - None, - window, - cx, - ); - }); - set_labeled_items(&pane, ["A", "D*", "B", "C"], cx); - - // b. Add with active item at the end of the item list - set_labeled_items(&pane, ["A", "B", "C*"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item( - Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))), - false, - false, - None, - window, - cx, - ); - }); - assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); - } - - #[gpui::test] - async fn test_add_item_with_existing_item(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // 1. Add with a destination index - // 1a. Add before the active item - let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item(d, false, false, Some(0), window, cx); - }); - assert_item_labels(&pane, ["D*", "A", "B", "C"], cx); - - // 1b. Add after the active item - let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item(d, false, false, Some(2), window, cx); - }); - assert_item_labels(&pane, ["A", "B", "D*", "C"], cx); - - // 1c. Add at the end of the item list (including off the length) - let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item(a, false, false, Some(5), window, cx); - }); - assert_item_labels(&pane, ["B", "C", "D", "A*"], cx); - - // 1d. Add same item to active index - let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item(b, false, false, Some(1), window, cx); - }); - assert_item_labels(&pane, ["A", "B*", "C"], cx); - - // 1e. Add item to index after same item in last position - let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item(c, false, false, Some(2), window, cx); - }); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - // 2. Add without a destination index - // 2a. Add with active item at the start of the item list - let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item(d, false, false, None, window, cx); - }); - assert_item_labels(&pane, ["A", "D*", "B", "C"], cx); - - // 2b. Add with active item at the end of the item list - let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item(a, false, false, None, window, cx); - }); - assert_item_labels(&pane, ["B", "C", "D", "A*"], cx); - - // 2c. Add active item to active item at end of list - let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item(c, false, false, None, window, cx); - }); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - // 2d. Add active item to active item at start of list - let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item(a, false, false, None, window, cx); - }); - assert_item_labels(&pane, ["A*", "B", "C"], cx); - } - - #[gpui::test] - async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // singleton view - pane.update_in(cx, |pane, window, cx| { - pane.add_item( - Box::new(cx.new(|cx| { - TestItem::new(cx) - .with_buffer_kind(ItemBufferKind::Singleton) - .with_label("buffer 1") - .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]) - })), - false, - false, - None, - window, - cx, - ); - }); - assert_item_labels(&pane, ["buffer 1*"], cx); - - // new singleton view with the same project entry - pane.update_in(cx, |pane, window, cx| { - pane.add_item( - Box::new(cx.new(|cx| { - TestItem::new(cx) - .with_buffer_kind(ItemBufferKind::Singleton) - .with_label("buffer 1") - .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) - })), - false, - false, - None, - window, - cx, - ); - }); - assert_item_labels(&pane, ["buffer 1*"], cx); - - // new singleton view with different project entry - pane.update_in(cx, |pane, window, cx| { - pane.add_item( - Box::new(cx.new(|cx| { - TestItem::new(cx) - .with_buffer_kind(ItemBufferKind::Singleton) - .with_label("buffer 2") - .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)]) - })), - false, - false, - None, - window, - cx, - ); - }); - assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx); - - // new multibuffer view with the same project entry - pane.update_in(cx, |pane, window, cx| { - pane.add_item( - Box::new(cx.new(|cx| { - TestItem::new(cx) - .with_buffer_kind(ItemBufferKind::Multibuffer) - .with_label("multibuffer 1") - .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) - })), - false, - false, - None, - window, - cx, - ); - }); - assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx); - - // another multibuffer view with the same project entry - pane.update_in(cx, |pane, window, cx| { - pane.add_item( - Box::new(cx.new(|cx| { - TestItem::new(cx) - .with_buffer_kind(ItemBufferKind::Multibuffer) - .with_label("multibuffer 1b") - .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) - })), - false, - false, - None, - window, - cx, - ); - }); - assert_item_labels( - &pane, - ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"], - cx, - ); - } - - #[gpui::test] - async fn test_remove_item_ordering_history(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - add_labeled_item(&pane, "A", false, cx); - add_labeled_item(&pane, "B", false, cx); - add_labeled_item(&pane, "C", false, cx); - add_labeled_item(&pane, "D", false, cx); - assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(1, false, false, window, cx) - }); - add_labeled_item(&pane, "1", false, cx); - assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A", "B*", "C", "D"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(3, false, false, window, cx) - }); - assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A", "B*", "C"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A*"], cx); - } - - #[gpui::test] - async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) { - init_test(cx); - cx.update_global::(|s, cx| { - s.update_user_settings(cx, |s| { - s.tabs.get_or_insert_default().activate_on_close = Some(ActivateOnClose::Neighbour); - }); - }); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - add_labeled_item(&pane, "A", false, cx); - add_labeled_item(&pane, "B", false, cx); - add_labeled_item(&pane, "C", false, cx); - add_labeled_item(&pane, "D", false, cx); - assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(1, false, false, window, cx) - }); - add_labeled_item(&pane, "1", false, cx); - assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A", "B", "C*", "D"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(3, false, false, window, cx) - }); - assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A", "B*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A*"], cx); - } - - #[gpui::test] - async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) { - init_test(cx); - cx.update_global::(|s, cx| { - s.update_user_settings(cx, |s| { - s.tabs.get_or_insert_default().activate_on_close = - Some(ActivateOnClose::LeftNeighbour); - }); - }); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - add_labeled_item(&pane, "A", false, cx); - add_labeled_item(&pane, "B", false, cx); - add_labeled_item(&pane, "C", false, cx); - add_labeled_item(&pane, "D", false, cx); - assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(1, false, false, window, cx) - }); - add_labeled_item(&pane, "1", false, cx); - assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A", "B*", "C", "D"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(3, false, false, window, cx) - }); - assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(0, false, false, window, cx) - }); - assert_item_labels(&pane, ["A*", "B", "C"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["B*", "C"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["C*"], cx); - } - - #[gpui::test] - async fn test_close_inactive_items(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - let item_a = add_labeled_item(&pane, "A", false, cx); - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A*!"], cx); - - let item_b = add_labeled_item(&pane, "B", false, cx); - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A!", "B*!"], cx); - - add_labeled_item(&pane, "C", false, cx); - assert_item_labels(&pane, ["A!", "B!", "C*"], cx); - - add_labeled_item(&pane, "D", false, cx); - add_labeled_item(&pane, "E", false, cx); - assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_other_items( - &CloseOtherItems { - save_intent: None, - close_pinned: false, - }, - None, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A!", "B!", "E*"], cx); - } - - #[gpui::test] - async fn test_running_close_inactive_items_via_an_inactive_item(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - add_labeled_item(&pane, "A", false, cx); - assert_item_labels(&pane, ["A*"], cx); - - let item_b = add_labeled_item(&pane, "B", false, cx); - assert_item_labels(&pane, ["A", "B*"], cx); - - add_labeled_item(&pane, "C", false, cx); - add_labeled_item(&pane, "D", false, cx); - add_labeled_item(&pane, "E", false, cx); - assert_item_labels(&pane, ["A", "B", "C", "D", "E*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_other_items( - &CloseOtherItems { - save_intent: None, - close_pinned: false, - }, - Some(item_b.item_id()), - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["B*"], cx); - } - - #[gpui::test] - async fn test_close_clean_items(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - add_labeled_item(&pane, "A", true, cx); - add_labeled_item(&pane, "B", false, cx); - add_labeled_item(&pane, "C", true, cx); - add_labeled_item(&pane, "D", false, cx); - add_labeled_item(&pane, "E", false, cx); - assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_clean_items( - &CloseCleanItems { - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A^", "C*^"], cx); - } - - #[gpui::test] - async fn test_close_items_to_the_left(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_items_to_the_left_by_id( - None, - &CloseItemsToTheLeft { - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["C*", "D", "E"], cx); - } - - #[gpui::test] - async fn test_close_items_to_the_right(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_items_to_the_right_by_id( - None, - &CloseItemsToTheRight { - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - } - - #[gpui::test] - async fn test_close_all_items(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - let item_a = add_labeled_item(&pane, "A", false, cx); - add_labeled_item(&pane, "B", false, cx); - add_labeled_item(&pane, "C", false, cx); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - pane.close_all_items( - &CloseAllItems { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A*!"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.unpin_tab_at(ix, window, cx); - pane.close_all_items( - &CloseAllItems { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - - assert_item_labels(&pane, [], cx); - - add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| { - item.project_items - .push(TestProjectItem::new_dirty(1, "A.txt", cx)) - }); - add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| { - item.project_items - .push(TestProjectItem::new_dirty(2, "B.txt", cx)) - }); - add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| { - item.project_items - .push(TestProjectItem::new_dirty(3, "C.txt", cx)) - }); - assert_item_labels(&pane, ["A^", "B^", "C*^"], cx); - - let save = pane.update_in(cx, |pane, window, cx| { - pane.close_all_items( - &CloseAllItems { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }); - - cx.executor().run_until_parked(); - cx.simulate_prompt_answer("Save all"); - save.await.unwrap(); - assert_item_labels(&pane, [], cx); - - add_labeled_item(&pane, "A", true, cx); - add_labeled_item(&pane, "B", true, cx); - add_labeled_item(&pane, "C", true, cx); - assert_item_labels(&pane, ["A^", "B^", "C*^"], cx); - let save = pane.update_in(cx, |pane, window, cx| { - pane.close_all_items( - &CloseAllItems { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }); - - cx.executor().run_until_parked(); - cx.simulate_prompt_answer("Discard all"); - save.await.unwrap(); - assert_item_labels(&pane, [], cx); - } - - #[gpui::test] - async fn test_close_multibuffer_items(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - let add_labeled_item = |pane: &Entity, - label, - is_dirty, - kind: ItemBufferKind, - cx: &mut VisualTestContext| { - pane.update_in(cx, |pane, window, cx| { - let labeled_item = Box::new(cx.new(|cx| { - TestItem::new(cx) - .with_label(label) - .with_dirty(is_dirty) - .with_buffer_kind(kind) - })); - pane.add_item(labeled_item.clone(), false, false, None, window, cx); - labeled_item - }) - }; - - let item_a = add_labeled_item(&pane, "A", false, ItemBufferKind::Multibuffer, cx); - add_labeled_item(&pane, "B", false, ItemBufferKind::Multibuffer, cx); - add_labeled_item(&pane, "C", false, ItemBufferKind::Singleton, cx); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - pane.close_multibuffer_items( - &CloseMultibufferItems { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A!", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.unpin_tab_at(ix, window, cx); - pane.close_multibuffer_items( - &CloseMultibufferItems { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - - assert_item_labels(&pane, ["C*"], cx); - - add_labeled_item(&pane, "A", true, ItemBufferKind::Singleton, cx).update(cx, |item, cx| { - item.project_items - .push(TestProjectItem::new_dirty(1, "A.txt", cx)) - }); - add_labeled_item(&pane, "B", true, ItemBufferKind::Multibuffer, cx).update( - cx, - |item, cx| { - item.project_items - .push(TestProjectItem::new_dirty(2, "B.txt", cx)) - }, - ); - add_labeled_item(&pane, "D", true, ItemBufferKind::Multibuffer, cx).update( - cx, - |item, cx| { - item.project_items - .push(TestProjectItem::new_dirty(3, "D.txt", cx)) - }, - ); - assert_item_labels(&pane, ["C", "A^", "B^", "D*^"], cx); - - let save = pane.update_in(cx, |pane, window, cx| { - pane.close_multibuffer_items( - &CloseMultibufferItems { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }); - - cx.executor().run_until_parked(); - cx.simulate_prompt_answer("Save all"); - save.await.unwrap(); - assert_item_labels(&pane, ["C", "A*^"], cx); - - add_labeled_item(&pane, "B", true, ItemBufferKind::Multibuffer, cx).update( - cx, - |item, cx| { - item.project_items - .push(TestProjectItem::new_dirty(2, "B.txt", cx)) - }, - ); - add_labeled_item(&pane, "D", true, ItemBufferKind::Multibuffer, cx).update( - cx, - |item, cx| { - item.project_items - .push(TestProjectItem::new_dirty(3, "D.txt", cx)) - }, - ); - assert_item_labels(&pane, ["C", "A^", "B^", "D*^"], cx); - let save = pane.update_in(cx, |pane, window, cx| { - pane.close_multibuffer_items( - &CloseMultibufferItems { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }); - - cx.executor().run_until_parked(); - cx.simulate_prompt_answer("Discard all"); - save.await.unwrap(); - assert_item_labels(&pane, ["C", "A*^"], cx); - } - - #[gpui::test] - async fn test_close_with_save_intent(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx)); - let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx)); - let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx)); - - add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| { - item.project_items.push(a.clone()); - item.project_items.push(b.clone()); - }); - add_labeled_item(&pane, "C", true, cx) - .update(cx, |item, _| item.project_items.push(c.clone())); - assert_item_labels(&pane, ["AB^", "C*^"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_all_items( - &CloseAllItems { - save_intent: Some(SaveIntent::Save), - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - - assert_item_labels(&pane, [], cx); - cx.update(|_, cx| { - assert!(!a.read(cx).is_dirty); - assert!(!b.read(cx).is_dirty); - assert!(!c.read(cx).is_dirty); - }); - } - - #[gpui::test] - async fn test_new_tab_scrolls_into_view_completely(cx: &mut TestAppContext) { - // Arrange - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - cx.simulate_resize(size(px(300.), px(300.))); - - add_labeled_item(&pane, "untitled", false, cx); - add_labeled_item(&pane, "untitled", false, cx); - add_labeled_item(&pane, "untitled", false, cx); - add_labeled_item(&pane, "untitled", false, cx); - // Act: this should trigger a scroll - add_labeled_item(&pane, "untitled", false, cx); - // Assert - let tab_bar_scroll_handle = - pane.update_in(cx, |pane, _window, _cx| pane.tab_bar_scroll_handle.clone()); - assert_eq!(tab_bar_scroll_handle.children_count(), 6); - let tab_bounds = cx.debug_bounds("TAB-3").unwrap(); - let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap(); - let scroll_bounds = tab_bar_scroll_handle.bounds(); - let scroll_offset = tab_bar_scroll_handle.offset(); - assert!(tab_bounds.right() <= scroll_bounds.right() + scroll_offset.x); - // -39.5 is the magic number for this setup - assert_eq!(scroll_offset.x, px(-39.5)); - assert!( - !tab_bounds.intersects(&new_tab_button_bounds), - "Tab should not overlap with the new tab button, if this is failing check if there's been a redesign!" - ); - } - - #[gpui::test] - async fn test_close_all_items_including_pinned(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - let item_a = add_labeled_item(&pane, "A", false, cx); - add_labeled_item(&pane, "B", false, cx); - add_labeled_item(&pane, "C", false, cx); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - pane.close_all_items( - &CloseAllItems { - save_intent: None, - close_pinned: true, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, [], cx); - } - - #[gpui::test] - async fn test_close_pinned_tab_with_non_pinned_in_same_pane(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - // Non-pinned tabs in same pane - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - add_labeled_item(&pane, "A", false, cx); - add_labeled_item(&pane, "B", false, cx); - add_labeled_item(&pane, "C", false, cx); - pane.update_in(cx, |pane, window, cx| { - pane.pin_tab_at(0, window, cx); - }); - set_labeled_items(&pane, ["A*", "B", "C"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - .unwrap(); - }); - // Non-pinned tab should be active - assert_item_labels(&pane, ["A!", "B*", "C"], cx); - } - - #[gpui::test] - async fn test_close_pinned_tab_with_non_pinned_in_different_pane(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - // No non-pinned tabs in same pane, non-pinned tabs in another pane - let pane1 = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - let pane2 = workspace.update_in(cx, |workspace, window, cx| { - workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx) - }); - add_labeled_item(&pane1, "A", false, cx); - pane1.update_in(cx, |pane, window, cx| { - pane.pin_tab_at(0, window, cx); - }); - set_labeled_items(&pane1, ["A*"], cx); - add_labeled_item(&pane2, "B", false, cx); - set_labeled_items(&pane2, ["B"], cx); - pane1.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - .unwrap(); - }); - // Non-pinned tab of other pane should be active - assert_item_labels(&pane2, ["B*"], cx); - } - - #[gpui::test] - async fn ensure_item_closing_actions_do_not_panic_when_no_items_exist(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - assert_item_labels(&pane, [], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - - pane.update_in(cx, |pane, window, cx| { - pane.close_other_items( - &CloseOtherItems { - save_intent: None, - close_pinned: false, - }, - None, - window, - cx, - ) - }) - .await - .unwrap(); - - pane.update_in(cx, |pane, window, cx| { - pane.close_all_items( - &CloseAllItems { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - - pane.update_in(cx, |pane, window, cx| { - pane.close_clean_items( - &CloseCleanItems { - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - - pane.update_in(cx, |pane, window, cx| { - pane.close_items_to_the_right_by_id( - None, - &CloseItemsToTheRight { - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - - pane.update_in(cx, |pane, window, cx| { - pane.close_items_to_the_left_by_id( - None, - &CloseItemsToTheLeft { - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - } - - #[gpui::test] - async fn test_item_swapping_actions(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - assert_item_labels(&pane, [], cx); - - // Test that these actions do not panic - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_right(&Default::default(), window, cx); - }); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_left(&Default::default(), window, cx); - }); - - add_labeled_item(&pane, "A", false, cx); - add_labeled_item(&pane, "B", false, cx); - add_labeled_item(&pane, "C", false, cx); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_right(&Default::default(), window, cx); - }); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_left(&Default::default(), window, cx); - }); - assert_item_labels(&pane, ["A", "C*", "B"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_left(&Default::default(), window, cx); - }); - assert_item_labels(&pane, ["C*", "A", "B"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_left(&Default::default(), window, cx); - }); - assert_item_labels(&pane, ["C*", "A", "B"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_right(&Default::default(), window, cx); - }); - assert_item_labels(&pane, ["A", "C*", "B"], cx); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - theme::init(LoadThemes::JustBase, cx); - }); - } - - fn set_max_tabs(cx: &mut TestAppContext, value: Option) { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| { - settings.workspace.max_tabs = value.map(|v| NonZero::new(v).unwrap()) - }); - }); - } - - fn add_labeled_item( - pane: &Entity, - label: &str, - is_dirty: bool, - cx: &mut VisualTestContext, - ) -> Box> { - pane.update_in(cx, |pane, window, cx| { - let labeled_item = - Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty))); - pane.add_item(labeled_item.clone(), false, false, None, window, cx); - labeled_item - }) - } - - fn set_labeled_items( - pane: &Entity, - labels: [&str; COUNT], - cx: &mut VisualTestContext, - ) -> [Box>; COUNT] { - pane.update_in(cx, |pane, window, cx| { - pane.items.clear(); - let mut active_item_index = 0; - - let mut index = 0; - let items = labels.map(|mut label| { - if label.ends_with('*') { - label = label.trim_end_matches('*'); - active_item_index = index; - } - - let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label))); - pane.add_item(labeled_item.clone(), false, false, None, window, cx); - index += 1; - labeled_item - }); - - pane.activate_item(active_item_index, false, false, window, cx); - - items - }) - } - - // Assert the item label, with the active item label suffixed with a '*' - #[track_caller] - fn assert_item_labels( - pane: &Entity, - expected_states: [&str; COUNT], - cx: &mut VisualTestContext, - ) { - let actual_states = pane.update(cx, |pane, cx| { - pane.items - .iter() - .enumerate() - .map(|(ix, item)| { - let mut state = item - .to_any_view() - .downcast::() - .unwrap() - .read(cx) - .label - .clone(); - if ix == pane.active_item_index { - state.push('*'); - } - if item.is_dirty(cx) { - state.push('^'); - } - if pane.is_tab_pinned(ix) { - state.push('!'); - } - state - }) - .collect::>() - }); - assert_eq!( - actual_states, expected_states, - "pane items do not match expectation" - ); - } -} diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs deleted file mode 100644 index c9d9897713..0000000000 --- a/crates/workspace/src/pane_group.rs +++ /dev/null @@ -1,1456 +0,0 @@ -use crate::{ - AppState, CollaboratorId, FollowerState, Pane, Workspace, WorkspaceSettings, - pane_group::element::pane_axis, - workspace_settings::{PaneSplitDirectionHorizontal, PaneSplitDirectionVertical}, -}; -use anyhow::Result; -use call::{ActiveCall, ParticipantLocation}; -use collections::HashMap; -use gpui::{ - Along, AnyView, AnyWeakView, Axis, Bounds, Entity, Hsla, IntoElement, MouseButton, Pixels, - Point, StyleRefinement, WeakEntity, Window, point, size, -}; -use parking_lot::Mutex; -use project::Project; -use schemars::JsonSchema; -use serde::Deserialize; -use settings::Settings; -use std::sync::Arc; -use ui::prelude::*; - -pub const HANDLE_HITBOX_SIZE: f32 = 4.0; -const HORIZONTAL_MIN_SIZE: f32 = 80.; -const VERTICAL_MIN_SIZE: f32 = 100.; - -/// One or many panes, arranged in a horizontal or vertical axis due to a split. -/// Panes have all their tabs and capabilities preserved, and can be split again or resized. -/// Single-pane group is a regular pane. -#[derive(Clone)] -pub struct PaneGroup { - pub root: Member, -} - -pub struct PaneRenderResult { - pub element: gpui::AnyElement, - pub contains_active_pane: bool, -} - -impl PaneGroup { - pub fn with_root(root: Member) -> Self { - Self { root } - } - - pub fn new(pane: Entity) -> Self { - Self { - root: Member::Pane(pane), - } - } - - pub fn split( - &mut self, - old_pane: &Entity, - new_pane: &Entity, - direction: SplitDirection, - ) -> Result<()> { - match &mut self.root { - Member::Pane(pane) => { - if pane == old_pane { - self.root = Member::new_axis(old_pane.clone(), new_pane.clone(), direction); - Ok(()) - } else { - anyhow::bail!("Pane not found"); - } - } - Member::Axis(axis) => axis.split(old_pane, new_pane, direction), - } - } - - pub fn bounding_box_for_pane(&self, pane: &Entity) -> Option> { - match &self.root { - Member::Pane(_) => None, - Member::Axis(axis) => axis.bounding_box_for_pane(pane), - } - } - - pub fn pane_at_pixel_position(&self, coordinate: Point) -> Option<&Entity> { - match &self.root { - Member::Pane(pane) => Some(pane), - Member::Axis(axis) => axis.pane_at_pixel_position(coordinate), - } - } - - /// Moves active pane to span the entire border in the given direction, - /// similar to Vim ctrl+w shift-[hjkl] motion. - /// - /// Returns: - /// - Ok(true) if it found and moved a pane - /// - Ok(false) if it found but did not move the pane - /// - Err(_) if it did not find the pane - pub fn move_to_border( - &mut self, - active_pane: &Entity, - direction: SplitDirection, - ) -> Result { - if let Some(pane) = self.find_pane_at_border(direction) - && pane == active_pane - { - return Ok(false); - } - - if !self.remove(active_pane)? { - return Ok(false); - } - - if let Member::Axis(root) = &mut self.root - && direction.axis() == root.axis - { - let idx = if direction.increasing() { - root.members.len() - } else { - 0 - }; - root.insert_pane(idx, active_pane); - return Ok(true); - } - - let members = if direction.increasing() { - vec![self.root.clone(), Member::Pane(active_pane.clone())] - } else { - vec![Member::Pane(active_pane.clone()), self.root.clone()] - }; - self.root = Member::Axis(PaneAxis::new(direction.axis(), members)); - Ok(true) - } - - fn find_pane_at_border(&self, direction: SplitDirection) -> Option<&Entity> { - match &self.root { - Member::Pane(pane) => Some(pane), - Member::Axis(axis) => axis.find_pane_at_border(direction), - } - } - - /// Returns: - /// - Ok(true) if it found and removed a pane - /// - Ok(false) if it found but did not remove the pane - /// - Err(_) if it did not find the pane - pub fn remove(&mut self, pane: &Entity) -> Result { - match &mut self.root { - Member::Pane(_) => Ok(false), - Member::Axis(axis) => { - if let Some(last_pane) = axis.remove(pane)? { - self.root = last_pane; - } - Ok(true) - } - } - } - - pub fn resize( - &mut self, - pane: &Entity, - direction: Axis, - amount: Pixels, - bounds: &Bounds, - ) { - match &mut self.root { - Member::Pane(_) => {} - Member::Axis(axis) => { - let _ = axis.resize(pane, direction, amount, bounds); - } - }; - } - - pub fn reset_pane_sizes(&mut self) { - match &mut self.root { - Member::Pane(_) => {} - Member::Axis(axis) => { - let _ = axis.reset_pane_sizes(); - } - }; - } - - pub fn swap(&mut self, from: &Entity, to: &Entity) { - match &mut self.root { - Member::Pane(_) => {} - Member::Axis(axis) => axis.swap(from, to), - }; - } - - pub fn render( - &self, - zoomed: Option<&AnyWeakView>, - render_cx: &dyn PaneLeaderDecorator, - window: &mut Window, - cx: &mut App, - ) -> impl IntoElement { - self.root.render(0, zoomed, render_cx, window, cx).element - } - - pub fn panes(&self) -> Vec<&Entity> { - let mut panes = Vec::new(); - self.root.collect_panes(&mut panes); - panes - } - - pub fn first_pane(&self) -> Entity { - self.root.first_pane() - } - - pub fn last_pane(&self) -> Entity { - self.root.last_pane() - } - - pub fn find_pane_in_direction( - &mut self, - active_pane: &Entity, - direction: SplitDirection, - cx: &App, - ) -> Option<&Entity> { - let bounding_box = self.bounding_box_for_pane(active_pane)?; - let cursor = active_pane.read(cx).pixel_position_of_cursor(cx); - let center = match cursor { - Some(cursor) if bounding_box.contains(&cursor) => cursor, - _ => bounding_box.center(), - }; - - let distance_to_next = crate::HANDLE_HITBOX_SIZE; - - let target = match direction { - SplitDirection::Left => { - Point::new(bounding_box.left() - distance_to_next.into(), center.y) - } - SplitDirection::Right => { - Point::new(bounding_box.right() + distance_to_next.into(), center.y) - } - SplitDirection::Up => { - Point::new(center.x, bounding_box.top() - distance_to_next.into()) - } - SplitDirection::Down => { - Point::new(center.x, bounding_box.bottom() + distance_to_next.into()) - } - }; - self.pane_at_pixel_position(target) - } - - pub fn invert_axies(&mut self) { - self.root.invert_pane_axies(); - } -} - -#[derive(Debug, Clone)] -pub enum Member { - Axis(PaneAxis), - Pane(Entity), -} - -#[derive(Clone, Copy)] -pub struct PaneRenderContext<'a> { - pub project: &'a Entity, - pub follower_states: &'a HashMap, - pub active_call: Option<&'a Entity>, - pub active_pane: &'a Entity, - pub app_state: &'a Arc, - pub workspace: &'a WeakEntity, -} - -#[derive(Default)] -pub struct LeaderDecoration { - border: Option, - status_box: Option, -} - -pub trait PaneLeaderDecorator { - fn decorate(&self, pane: &Entity, cx: &App) -> LeaderDecoration; - fn active_pane(&self) -> &Entity; - fn workspace(&self) -> &WeakEntity; -} - -pub struct ActivePaneDecorator<'a> { - active_pane: &'a Entity, - workspace: &'a WeakEntity, -} - -impl<'a> ActivePaneDecorator<'a> { - pub fn new(active_pane: &'a Entity, workspace: &'a WeakEntity) -> Self { - Self { - active_pane, - workspace, - } - } -} - -impl PaneLeaderDecorator for ActivePaneDecorator<'_> { - fn decorate(&self, _: &Entity, _: &App) -> LeaderDecoration { - LeaderDecoration::default() - } - fn active_pane(&self) -> &Entity { - self.active_pane - } - - fn workspace(&self) -> &WeakEntity { - self.workspace - } -} - -impl PaneLeaderDecorator for PaneRenderContext<'_> { - fn decorate(&self, pane: &Entity, cx: &App) -> LeaderDecoration { - let follower_state = self.follower_states.iter().find_map(|(leader_id, state)| { - if state.center_pane == *pane { - Some((*leader_id, state)) - } else { - None - } - }); - let Some((leader_id, follower_state)) = follower_state else { - return LeaderDecoration::default(); - }; - - let mut leader_color; - let status_box; - match leader_id { - CollaboratorId::PeerId(peer_id) => { - let Some(leader) = self.active_call.as_ref().and_then(|call| { - let room = call.read(cx).room()?.read(cx); - room.remote_participant_for_peer_id(peer_id) - }) else { - return LeaderDecoration::default(); - }; - - let is_in_unshared_view = follower_state.active_view_id.is_some_and(|view_id| { - !follower_state - .items_by_leader_view_id - .contains_key(&view_id) - }); - - let mut leader_join_data = None; - let leader_status_box = match leader.location { - ParticipantLocation::SharedProject { - project_id: leader_project_id, - } => { - if Some(leader_project_id) == self.project.read(cx).remote_id() { - is_in_unshared_view.then(|| { - Label::new(format!( - "{} is in an unshared pane", - leader.user.github_login - )) - }) - } else { - leader_join_data = Some((leader_project_id, leader.user.id)); - Some(Label::new(format!( - "Follow {} to their active project", - leader.user.github_login, - ))) - } - } - ParticipantLocation::UnsharedProject => Some(Label::new(format!( - "{} is viewing an unshared Zed project", - leader.user.github_login - ))), - ParticipantLocation::External => Some(Label::new(format!( - "{} is viewing a window outside of Zed", - leader.user.github_login - ))), - }; - status_box = leader_status_box.map(|status| { - div() - .absolute() - .w_96() - .bottom_3() - .right_3() - .elevation_2(cx) - .p_1() - .child(status) - .when_some( - leader_join_data, - |this, (leader_project_id, leader_user_id)| { - let app_state = self.app_state.clone(); - this.cursor_pointer().on_mouse_down( - MouseButton::Left, - move |_, _, cx| { - crate::join_in_room_project( - leader_project_id, - leader_user_id, - app_state.clone(), - cx, - ) - .detach_and_log_err(cx); - }, - ) - }, - ) - .into_any_element() - }); - leader_color = cx - .theme() - .players() - .color_for_participant(leader.participant_index.0) - .cursor; - } - CollaboratorId::Agent => { - status_box = None; - leader_color = cx.theme().players().agent().cursor; - } - } - - let is_in_panel = follower_state.dock_pane.is_some(); - if is_in_panel { - leader_color.fade_out(0.75); - } else { - leader_color.fade_out(0.3); - } - - LeaderDecoration { - status_box, - border: Some(leader_color), - } - } - - fn active_pane(&self) -> &Entity { - self.active_pane - } - - fn workspace(&self) -> &WeakEntity { - self.workspace - } -} - -impl Member { - fn new_axis(old_pane: Entity, new_pane: Entity, direction: SplitDirection) -> Self { - use Axis::*; - use SplitDirection::*; - - let axis = match direction { - Up | Down => Vertical, - Left | Right => Horizontal, - }; - - let members = match direction { - Up | Left => vec![Member::Pane(new_pane), Member::Pane(old_pane)], - Down | Right => vec![Member::Pane(old_pane), Member::Pane(new_pane)], - }; - - Member::Axis(PaneAxis::new(axis, members)) - } - - fn first_pane(&self) -> Entity { - match self { - Member::Axis(axis) => axis.members[0].first_pane(), - Member::Pane(pane) => pane.clone(), - } - } - - fn last_pane(&self) -> Entity { - match self { - Member::Axis(axis) => axis.members.last().unwrap().last_pane(), - Member::Pane(pane) => pane.clone(), - } - } - - pub fn render( - &self, - basis: usize, - zoomed: Option<&AnyWeakView>, - render_cx: &dyn PaneLeaderDecorator, - window: &mut Window, - cx: &mut App, - ) -> PaneRenderResult { - match self { - Member::Pane(pane) => { - if zoomed == Some(&pane.downgrade().into()) { - return PaneRenderResult { - element: div().into_any(), - contains_active_pane: false, - }; - } - - let decoration = render_cx.decorate(pane, cx); - let is_active = pane == render_cx.active_pane(); - - PaneRenderResult { - element: div() - .relative() - .flex_1() - .size_full() - .child( - AnyView::from(pane.clone()) - .cached(StyleRefinement::default().v_flex().size_full()), - ) - .when_some(decoration.border, |this, color| { - this.child( - div() - .absolute() - .size_full() - .left_0() - .top_0() - .border_2() - .border_color(color), - ) - }) - .children(decoration.status_box) - .into_any(), - contains_active_pane: is_active, - } - } - Member::Axis(axis) => axis.render(basis + 1, zoomed, render_cx, window, cx), - } - } - - fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a Entity>) { - match self { - Member::Axis(axis) => { - for member in &axis.members { - member.collect_panes(panes); - } - } - Member::Pane(pane) => panes.push(pane), - } - } - - fn invert_pane_axies(&mut self) { - match self { - Self::Axis(axis) => { - axis.axis = axis.axis.invert(); - for member in axis.members.iter_mut() { - member.invert_pane_axies(); - } - } - Self::Pane(_) => {} - } - } -} - -#[derive(Debug, Clone)] -pub struct PaneAxis { - pub axis: Axis, - pub members: Vec, - pub flexes: Arc>>, - pub bounding_boxes: Arc>>>>, -} - -impl PaneAxis { - pub fn new(axis: Axis, members: Vec) -> Self { - let flexes = Arc::new(Mutex::new(vec![1.; members.len()])); - let bounding_boxes = Arc::new(Mutex::new(vec![None; members.len()])); - Self { - axis, - members, - flexes, - bounding_boxes, - } - } - - pub fn load(axis: Axis, members: Vec, flexes: Option>) -> Self { - let mut flexes = flexes.unwrap_or_else(|| vec![1.; members.len()]); - if flexes.len() != members.len() - || (flexes.iter().copied().sum::() - flexes.len() as f32).abs() >= 0.001 - { - flexes = vec![1.; members.len()]; - } - - let flexes = Arc::new(Mutex::new(flexes)); - let bounding_boxes = Arc::new(Mutex::new(vec![None; members.len()])); - Self { - axis, - members, - flexes, - bounding_boxes, - } - } - - fn split( - &mut self, - old_pane: &Entity, - new_pane: &Entity, - direction: SplitDirection, - ) -> Result<()> { - for (mut idx, member) in self.members.iter_mut().enumerate() { - match member { - Member::Axis(axis) => { - if axis.split(old_pane, new_pane, direction).is_ok() { - return Ok(()); - } - } - Member::Pane(pane) => { - if pane == old_pane { - if direction.axis() == self.axis { - if direction.increasing() { - idx += 1; - } - self.insert_pane(idx, new_pane); - } else { - *member = - Member::new_axis(old_pane.clone(), new_pane.clone(), direction); - } - return Ok(()); - } - } - } - } - anyhow::bail!("Pane not found"); - } - - fn insert_pane(&mut self, idx: usize, new_pane: &Entity) { - self.members.insert(idx, Member::Pane(new_pane.clone())); - *self.flexes.lock() = vec![1.; self.members.len()]; - } - - fn find_pane_at_border(&self, direction: SplitDirection) -> Option<&Entity> { - if self.axis != direction.axis() { - return None; - } - let member = if direction.increasing() { - self.members.last() - } else { - self.members.first() - }; - member.and_then(|e| match e { - Member::Pane(pane) => Some(pane), - Member::Axis(_) => None, - }) - } - - fn remove(&mut self, pane_to_remove: &Entity) -> Result> { - let mut found_pane = false; - let mut remove_member = None; - for (idx, member) in self.members.iter_mut().enumerate() { - match member { - Member::Axis(axis) => { - if let Ok(last_pane) = axis.remove(pane_to_remove) { - if let Some(last_pane) = last_pane { - *member = last_pane; - } - found_pane = true; - break; - } - } - Member::Pane(pane) => { - if pane == pane_to_remove { - found_pane = true; - remove_member = Some(idx); - break; - } - } - } - } - - if found_pane { - if let Some(idx) = remove_member { - self.members.remove(idx); - *self.flexes.lock() = vec![1.; self.members.len()]; - } - - if self.members.len() == 1 { - let result = self.members.pop(); - *self.flexes.lock() = vec![1.; self.members.len()]; - Ok(result) - } else { - Ok(None) - } - } else { - anyhow::bail!("Pane not found"); - } - } - - fn reset_pane_sizes(&self) { - *self.flexes.lock() = vec![1.; self.members.len()]; - for member in self.members.iter() { - if let Member::Axis(axis) = member { - axis.reset_pane_sizes(); - } - } - } - - fn resize( - &mut self, - pane: &Entity, - axis: Axis, - amount: Pixels, - bounds: &Bounds, - ) -> Option { - let container_size = self - .bounding_boxes - .lock() - .iter() - .filter_map(|e| *e) - .reduce(|acc, e| acc.union(&e)) - .unwrap_or(*bounds) - .size; - - let found_pane = self - .members - .iter() - .any(|member| matches!(member, Member::Pane(p) if p == pane)); - - if found_pane && self.axis != axis { - return Some(false); // pane found but this is not the correct axis direction - } - let mut found_axis_index: Option = None; - if !found_pane { - for (i, pa) in self.members.iter_mut().enumerate() { - if let Member::Axis(pa) = pa - && let Some(done) = pa.resize(pane, axis, amount, bounds) - { - if done { - return Some(true); // pane found and operations already done - } else if self.axis != axis { - return Some(false); // pane found but this is not the correct axis direction - } else { - found_axis_index = Some(i); // pane found and this is correct direction - } - } - } - found_axis_index?; // no pane found - } - - let min_size = match axis { - Axis::Horizontal => px(HORIZONTAL_MIN_SIZE), - Axis::Vertical => px(VERTICAL_MIN_SIZE), - }; - let mut flexes = self.flexes.lock(); - - let ix = if found_pane { - self.members.iter().position(|m| { - if let Member::Pane(p) = m { - p == pane - } else { - false - } - }) - } else { - found_axis_index - }; - - if ix.is_none() { - return Some(true); - } - - let ix = ix.unwrap_or(0); - - let size = move |ix, flexes: &[f32]| { - container_size.along(axis) * (flexes[ix] / flexes.len() as f32) - }; - - // Don't allow resizing to less than the minimum size, if elements are already too small - if min_size - px(1.) > size(ix, flexes.as_slice()) { - return Some(true); - } - - let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| { - let flex_change = flexes.len() as f32 * pixel_dx / container_size.along(axis); - let current_target_flex = flexes[target_ix] + flex_change; - let next_target_flex = flexes[(target_ix as isize + next) as usize] - flex_change; - (current_target_flex, next_target_flex) - }; - - let apply_changes = - |current_ix: usize, proposed_current_pixel_change: Pixels, flexes: &mut [f32]| { - let next_target_size = Pixels::max( - size(current_ix + 1, flexes) - proposed_current_pixel_change, - min_size, - ); - let current_target_size = Pixels::max( - size(current_ix, flexes) + size(current_ix + 1, flexes) - next_target_size, - min_size, - ); - - let current_pixel_change = current_target_size - size(current_ix, flexes); - - let (current_target_flex, next_target_flex) = - flex_changes(current_pixel_change, current_ix, 1, flexes); - - flexes[current_ix] = current_target_flex; - flexes[current_ix + 1] = next_target_flex; - }; - - if ix + 1 == flexes.len() { - apply_changes(ix - 1, -1.0 * amount, flexes.as_mut_slice()); - } else { - apply_changes(ix, amount, flexes.as_mut_slice()); - } - Some(true) - } - - fn swap(&mut self, from: &Entity, to: &Entity) { - for member in self.members.iter_mut() { - match member { - Member::Axis(axis) => axis.swap(from, to), - Member::Pane(pane) => { - if pane == from { - *member = Member::Pane(to.clone()); - } else if pane == to { - *member = Member::Pane(from.clone()) - } - } - } - } - } - - fn bounding_box_for_pane(&self, pane: &Entity) -> Option> { - debug_assert!(self.members.len() == self.bounding_boxes.lock().len()); - - for (idx, member) in self.members.iter().enumerate() { - match member { - Member::Pane(found) => { - if pane == found { - return self.bounding_boxes.lock()[idx]; - } - } - Member::Axis(axis) => { - if let Some(rect) = axis.bounding_box_for_pane(pane) { - return Some(rect); - } - } - } - } - None - } - - fn pane_at_pixel_position(&self, coordinate: Point) -> Option<&Entity> { - debug_assert!(self.members.len() == self.bounding_boxes.lock().len()); - - let bounding_boxes = self.bounding_boxes.lock(); - - for (idx, member) in self.members.iter().enumerate() { - if let Some(coordinates) = bounding_boxes[idx] - && coordinates.contains(&coordinate) - { - return match member { - Member::Pane(found) => Some(found), - Member::Axis(axis) => axis.pane_at_pixel_position(coordinate), - }; - } - } - None - } - - fn render( - &self, - basis: usize, - zoomed: Option<&AnyWeakView>, - render_cx: &dyn PaneLeaderDecorator, - window: &mut Window, - cx: &mut App, - ) -> PaneRenderResult { - debug_assert!(self.members.len() == self.flexes.lock().len()); - let mut active_pane_ix = None; - let mut contains_active_pane = false; - let mut is_leaf_pane = vec![false; self.members.len()]; - - let rendered_children = self - .members - .iter() - .enumerate() - .map(|(ix, member)| { - match member { - Member::Pane(pane) => { - is_leaf_pane[ix] = true; - if pane == render_cx.active_pane() { - active_pane_ix = Some(ix); - contains_active_pane = true; - } - } - Member::Axis(_) => { - is_leaf_pane[ix] = false; - } - } - - let result = member.render((basis + ix) * 10, zoomed, render_cx, window, cx); - if result.contains_active_pane { - contains_active_pane = true; - } - result.element.into_any_element() - }) - .collect::>(); - - let element = pane_axis( - self.axis, - basis, - self.flexes.clone(), - self.bounding_boxes.clone(), - render_cx.workspace().clone(), - ) - .with_is_leaf_pane_mask(is_leaf_pane) - .children(rendered_children) - .with_active_pane(active_pane_ix) - .into_any_element(); - - PaneRenderResult { - element, - contains_active_pane, - } - } -} - -#[derive(Clone, Copy, Debug, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum SplitDirection { - Up, - Down, - Left, - Right, -} - -impl std::fmt::Display for SplitDirection { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SplitDirection::Up => write!(f, "up"), - SplitDirection::Down => write!(f, "down"), - SplitDirection::Left => write!(f, "left"), - SplitDirection::Right => write!(f, "right"), - } - } -} - -impl SplitDirection { - pub fn all() -> [Self; 4] { - [Self::Up, Self::Down, Self::Left, Self::Right] - } - - pub fn vertical(cx: &mut App) -> Self { - match WorkspaceSettings::get_global(cx).pane_split_direction_vertical { - PaneSplitDirectionVertical::Left => SplitDirection::Left, - PaneSplitDirectionVertical::Right => SplitDirection::Right, - } - } - - pub fn horizontal(cx: &mut App) -> Self { - match WorkspaceSettings::get_global(cx).pane_split_direction_horizontal { - PaneSplitDirectionHorizontal::Down => SplitDirection::Down, - PaneSplitDirectionHorizontal::Up => SplitDirection::Up, - } - } - - pub fn edge(&self, rect: Bounds) -> Pixels { - match self { - Self::Up => rect.origin.y, - Self::Down => rect.bottom_left().y, - Self::Left => rect.bottom_left().x, - Self::Right => rect.bottom_right().x, - } - } - - pub fn along_edge(&self, bounds: Bounds, length: Pixels) -> Bounds { - match self { - Self::Up => Bounds { - origin: bounds.origin, - size: size(bounds.size.width, length), - }, - Self::Down => Bounds { - origin: point(bounds.bottom_left().x, bounds.bottom_left().y - length), - size: size(bounds.size.width, length), - }, - Self::Left => Bounds { - origin: bounds.origin, - size: size(length, bounds.size.height), - }, - Self::Right => Bounds { - origin: point(bounds.bottom_right().x - length, bounds.bottom_left().y), - size: size(length, bounds.size.height), - }, - } - } - - pub fn axis(&self) -> Axis { - match self { - Self::Up | Self::Down => Axis::Vertical, - Self::Left | Self::Right => Axis::Horizontal, - } - } - - pub fn increasing(&self) -> bool { - match self { - Self::Left | Self::Up => false, - Self::Down | Self::Right => true, - } - } - - pub fn opposite(&self) -> SplitDirection { - match self { - Self::Down => Self::Up, - Self::Up => Self::Down, - Self::Left => Self::Right, - Self::Right => Self::Left, - } - } -} - -mod element { - use std::mem; - use std::{cell::RefCell, iter, rc::Rc, sync::Arc}; - - use gpui::{ - Along, AnyElement, App, Axis, BorderStyle, Bounds, Element, GlobalElementId, - HitboxBehavior, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, - Pixels, Point, Size, Style, WeakEntity, Window, px, relative, size, - }; - use gpui::{CursorStyle, Hitbox}; - use parking_lot::Mutex; - use settings::Settings; - use smallvec::SmallVec; - use ui::prelude::*; - use util::ResultExt; - - use crate::Workspace; - - use crate::WorkspaceSettings; - - use super::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE}; - - const DIVIDER_SIZE: f32 = 1.0; - - pub(super) fn pane_axis( - axis: Axis, - basis: usize, - flexes: Arc>>, - bounding_boxes: Arc>>>>, - workspace: WeakEntity, - ) -> PaneAxisElement { - PaneAxisElement { - axis, - basis, - flexes, - bounding_boxes, - children: SmallVec::new(), - active_pane_ix: None, - workspace, - is_leaf_pane_mask: Vec::new(), - } - } - - pub struct PaneAxisElement { - axis: Axis, - basis: usize, - /// Equivalent to ColumnWidths (but in terms of flexes instead of percentages) - /// For example, flexes "1.33, 1, 1", instead of "40%, 30%, 30%" - flexes: Arc>>, - bounding_boxes: Arc>>>>, - children: SmallVec<[AnyElement; 2]>, - active_pane_ix: Option, - workspace: WeakEntity, - // Track which children are leaf panes (Member::Pane) vs axes (Member::Axis) - is_leaf_pane_mask: Vec, - } - - pub struct PaneAxisLayout { - dragged_handle: Rc>>, - children: Vec, - } - - struct PaneAxisChildLayout { - bounds: Bounds, - element: AnyElement, - handle: Option, - is_leaf_pane: bool, - } - - struct PaneAxisHandleLayout { - hitbox: Hitbox, - divider_bounds: Bounds, - } - - impl PaneAxisElement { - pub fn with_active_pane(mut self, active_pane_ix: Option) -> Self { - self.active_pane_ix = active_pane_ix; - self - } - - pub fn with_is_leaf_pane_mask(mut self, mask: Vec) -> Self { - self.is_leaf_pane_mask = mask; - self - } - - fn compute_resize( - flexes: &Arc>>, - e: &MouseMoveEvent, - ix: usize, - axis: Axis, - child_start: Point, - container_size: Size, - workspace: WeakEntity, - window: &mut Window, - cx: &mut App, - ) { - let min_size = match axis { - Axis::Horizontal => px(HORIZONTAL_MIN_SIZE), - Axis::Vertical => px(VERTICAL_MIN_SIZE), - }; - let mut flexes = flexes.lock(); - debug_assert!(flex_values_in_bounds(flexes.as_slice())); - - // Math to convert a flex value to a pixel value - let size = move |ix, flexes: &[f32]| { - container_size.along(axis) * (flexes[ix] / flexes.len() as f32) - }; - - // Don't allow resizing to less than the minimum size, if elements are already too small - if min_size - px(1.) > size(ix, flexes.as_slice()) { - return; - } - - // This is basically a "bucket" of pixel changes that need to be applied in response to this - // mouse event. Probably a small, fractional number like 0.5 or 1.5 pixels - let mut proposed_current_pixel_change = - (e.position - child_start).along(axis) - size(ix, flexes.as_slice()); - - // This takes a pixel change, and computes the flex changes that correspond to this pixel change - // as well as the next one, for some reason - let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| { - let flex_change = pixel_dx / container_size.along(axis); - let current_target_flex = flexes[target_ix] + flex_change; - let next_target_flex = flexes[(target_ix as isize + next) as usize] - flex_change; - (current_target_flex, next_target_flex) - }; - - // Generate the list of flex successors, from the current index. - // If you're dragging column 3 forward, out of 6 columns, then this code will produce [4, 5, 6] - // If you're dragging column 3 backward, out of 6 columns, then this code will produce [2, 1, 0] - let mut successors = iter::from_fn({ - let forward = proposed_current_pixel_change > px(0.); - let mut ix_offset = 0; - let len = flexes.len(); - move || { - let result = if forward { - (ix + 1 + ix_offset < len).then(|| ix + ix_offset) - } else { - (ix as isize - ix_offset as isize >= 0).then(|| ix - ix_offset) - }; - - ix_offset += 1; - - result - } - }); - - // Now actually loop over these, and empty our bucket of pixel changes - while proposed_current_pixel_change.abs() > px(0.) { - let Some(current_ix) = successors.next() else { - break; - }; - - let next_target_size = Pixels::max( - size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change, - min_size, - ); - - let current_target_size = Pixels::max( - size(current_ix, flexes.as_slice()) + size(current_ix + 1, flexes.as_slice()) - - next_target_size, - min_size, - ); - - let current_pixel_change = - current_target_size - size(current_ix, flexes.as_slice()); - - let (current_target_flex, next_target_flex) = - flex_changes(current_pixel_change, current_ix, 1, flexes.as_slice()); - - flexes[current_ix] = current_target_flex; - flexes[current_ix + 1] = next_target_flex; - - proposed_current_pixel_change -= current_pixel_change; - } - - workspace - .update(cx, |this, cx| this.serialize_workspace(window, cx)) - .log_err(); - cx.stop_propagation(); - window.refresh(); - } - - fn layout_handle( - axis: Axis, - pane_bounds: Bounds, - window: &mut Window, - _cx: &mut App, - ) -> PaneAxisHandleLayout { - let handle_bounds = Bounds { - origin: pane_bounds.origin.apply_along(axis, |origin| { - origin + pane_bounds.size.along(axis) - px(HANDLE_HITBOX_SIZE / 2.) - }), - size: pane_bounds - .size - .apply_along(axis, |_| px(HANDLE_HITBOX_SIZE)), - }; - let divider_bounds = Bounds { - origin: pane_bounds - .origin - .apply_along(axis, |origin| origin + pane_bounds.size.along(axis)), - size: pane_bounds.size.apply_along(axis, |_| px(DIVIDER_SIZE)), - }; - - PaneAxisHandleLayout { - hitbox: window.insert_hitbox(handle_bounds, HitboxBehavior::BlockMouse), - divider_bounds, - } - } - } - - impl IntoElement for PaneAxisElement { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } - } - - impl Element for PaneAxisElement { - type RequestLayoutState = (); - type PrepaintState = PaneAxisLayout; - - fn id(&self) -> Option { - Some(self.basis.into()) - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _global_id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (gpui::LayoutId, Self::RequestLayoutState) { - let style = Style { - flex_grow: 1., - flex_shrink: 1., - flex_basis: relative(0.).into(), - size: size(relative(1.).into(), relative(1.).into()), - ..Style::default() - }; - (window.request_layout(style, None, cx), ()) - } - - fn prepaint( - &mut self, - global_id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - _state: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> PaneAxisLayout { - let dragged_handle = window.with_element_state::>>, _>( - global_id.unwrap(), - |state, _cx| { - let state = state.unwrap_or_else(|| Rc::new(RefCell::new(None))); - (state.clone(), state) - }, - ); - let flexes = self.flexes.lock().clone(); - let len = self.children.len(); - debug_assert!(flexes.len() == len); - debug_assert!(flex_values_in_bounds(flexes.as_slice())); - - let total_flex = len as f32; - - let mut origin = bounds.origin; - let space_per_flex = bounds.size.along(self.axis) / total_flex; - - let mut bounding_boxes = self.bounding_boxes.lock(); - bounding_boxes.clear(); - - let mut layout = PaneAxisLayout { - dragged_handle, - children: Vec::new(), - }; - for (ix, mut child) in mem::take(&mut self.children).into_iter().enumerate() { - let child_flex = flexes[ix]; - - let child_size = bounds - .size - .apply_along(self.axis, |_| space_per_flex * child_flex) - .map(|d| d.round()); - - let child_bounds = Bounds { - origin, - size: child_size, - }; - - bounding_boxes.push(Some(child_bounds)); - child.layout_as_root(child_size.into(), window, cx); - child.prepaint_at(origin, window, cx); - - origin = origin.apply_along(self.axis, |val| val + child_size.along(self.axis)); - - let is_leaf_pane = self.is_leaf_pane_mask.get(ix).copied().unwrap_or(true); - - layout.children.push(PaneAxisChildLayout { - bounds: child_bounds, - element: child, - handle: None, - is_leaf_pane, - }) - } - - for (ix, child_layout) in layout.children.iter_mut().enumerate() { - if ix < len - 1 { - child_layout.handle = Some(Self::layout_handle( - self.axis, - child_layout.bounds, - window, - cx, - )); - } - } - - layout - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - bounds: gpui::Bounds, - _: &mut Self::RequestLayoutState, - layout: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - for child in &mut layout.children { - child.element.paint(window, cx); - } - - let overlay_opacity = WorkspaceSettings::get(None, cx) - .active_pane_modifiers - .inactive_opacity - .map(|val| val.0.clamp(0.0, 1.0)) - .and_then(|val| (val <= 1.).then_some(val)); - - let mut overlay_background = cx.theme().colors().editor_background; - if let Some(opacity) = overlay_opacity { - overlay_background.fade_out(opacity); - } - - let overlay_border = WorkspaceSettings::get(None, cx) - .active_pane_modifiers - .border_size - .and_then(|val| (val >= 0.).then_some(val)); - - for (ix, child) in &mut layout.children.iter_mut().enumerate() { - if overlay_opacity.is_some() || overlay_border.is_some() { - // the overlay has to be painted in origin+1px with size width-1px - // in order to accommodate the divider between panels - let overlay_bounds = Bounds { - origin: child - .bounds - .origin - .apply_along(Axis::Horizontal, |val| val + px(1.)), - size: child - .bounds - .size - .apply_along(Axis::Horizontal, |val| val - px(1.)), - }; - - if overlay_opacity.is_some() - && child.is_leaf_pane - && self.active_pane_ix != Some(ix) - { - window.paint_quad(gpui::fill(overlay_bounds, overlay_background)); - } - - if let Some(border) = overlay_border - && self.active_pane_ix == Some(ix) - && child.is_leaf_pane - { - window.paint_quad(gpui::quad( - overlay_bounds, - 0., - gpui::transparent_black(), - border, - cx.theme().colors().border_selected, - BorderStyle::Solid, - )); - } - } - - if let Some(handle) = child.handle.as_mut() { - let cursor_style = match self.axis { - Axis::Vertical => CursorStyle::ResizeRow, - Axis::Horizontal => CursorStyle::ResizeColumn, - }; - - if layout - .dragged_handle - .borrow() - .is_some_and(|dragged_ix| dragged_ix == ix) - { - window.set_window_cursor_style(cursor_style); - } else { - window.set_cursor_style(cursor_style, &handle.hitbox); - } - - window.paint_quad(gpui::fill( - handle.divider_bounds, - cx.theme().colors().pane_group_border, - )); - - window.on_mouse_event({ - let dragged_handle = layout.dragged_handle.clone(); - let flexes = self.flexes.clone(); - let workspace = self.workspace.clone(); - let handle_hitbox = handle.hitbox.clone(); - move |e: &MouseDownEvent, phase, window, cx| { - if phase.bubble() && handle_hitbox.is_hovered(window) { - dragged_handle.replace(Some(ix)); - if e.click_count >= 2 { - let mut borrow = flexes.lock(); - *borrow = vec![1.; borrow.len()]; - workspace - .update(cx, |this, cx| this.serialize_workspace(window, cx)) - .log_err(); - - window.refresh(); - } - cx.stop_propagation(); - } - } - }); - window.on_mouse_event({ - let workspace = self.workspace.clone(); - let dragged_handle = layout.dragged_handle.clone(); - let flexes = self.flexes.clone(); - let child_bounds = child.bounds; - let axis = self.axis; - move |e: &MouseMoveEvent, phase, window, cx| { - let dragged_handle = dragged_handle.borrow(); - if phase.bubble() && *dragged_handle == Some(ix) { - Self::compute_resize( - &flexes, - e, - ix, - axis, - child_bounds.origin, - bounds.size, - workspace.clone(), - window, - cx, - ) - } - } - }); - } - } - - window.on_mouse_event({ - let dragged_handle = layout.dragged_handle.clone(); - move |_: &MouseUpEvent, phase, _window, _cx| { - if phase.bubble() { - dragged_handle.replace(None); - } - } - }); - } - } - - impl ParentElement for PaneAxisElement { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } - } - - fn flex_values_in_bounds(flexes: &[f32]) -> bool { - (flexes.iter().copied().sum::() - flexes.len() as f32).abs() < 0.001 - } -} diff --git a/crates/workspace/src/path_list.rs b/crates/workspace/src/path_list.rs deleted file mode 100644 index 035f9e44fc..0000000000 --- a/crates/workspace/src/path_list.rs +++ /dev/null @@ -1,196 +0,0 @@ -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; - -use itertools::Itertools; -use util::paths::SanitizedPath; - -/// A list of absolute paths, in a specific order. -/// -/// The paths are stored in lexicographic order, so that they can be compared to -/// other path lists without regard to the order of the paths. -/// -/// The paths can be retrieved in the original order using `ordered_paths()`. -#[derive(Default, PartialEq, Eq, Debug, Clone)] -pub struct PathList { - /// The paths, in lexicographic order. - paths: Arc<[PathBuf]>, - /// The order in which the paths were provided. - /// - /// See `ordered_paths()` for a way to get the paths in the original order. - order: Arc<[usize]>, -} - -#[derive(Debug)] -pub struct SerializedPathList { - pub paths: String, - pub order: String, -} - -impl PathList { - pub fn new>(paths: &[P]) -> Self { - let mut indexed_paths: Vec<(usize, PathBuf)> = paths - .iter() - .enumerate() - .map(|(ix, path)| (ix, SanitizedPath::new(path).into())) - .collect(); - indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b)); - let order = indexed_paths.iter().map(|e| e.0).collect::>().into(); - let paths = indexed_paths - .into_iter() - .map(|e| e.1) - .collect::>() - .into(); - Self { order, paths } - } - - pub fn is_empty(&self) -> bool { - self.paths.is_empty() - } - - /// Get the paths in lexicographic order. - pub fn paths(&self) -> &[PathBuf] { - self.paths.as_ref() - } - - /// Get the order in which the paths were provided. - pub fn order(&self) -> &[usize] { - self.order.as_ref() - } - - /// Get the paths in the original order. - pub fn ordered_paths(&self) -> impl Iterator { - self.order - .iter() - .zip(self.paths.iter()) - .sorted_by_key(|(i, _)| **i) - .map(|(_, path)| path) - } - - pub fn is_lexicographically_ordered(&self) -> bool { - self.order.iter().enumerate().all(|(i, &j)| i == j) - } - - pub fn deserialize(serialized: &SerializedPathList) -> Self { - let mut paths: Vec = if serialized.paths.is_empty() { - Vec::new() - } else { - serialized.paths.split('\n').map(PathBuf::from).collect() - }; - - let mut order: Vec = serialized - .order - .split(',') - .filter_map(|s| s.parse().ok()) - .collect(); - - if !paths.is_sorted() || order.len() != paths.len() { - order = (0..paths.len()).collect(); - paths.sort(); - } - - Self { - paths: paths.into(), - order: order.into(), - } - } - - pub fn serialize(&self) -> SerializedPathList { - use std::fmt::Write as _; - - let mut paths = String::new(); - for path in self.paths.iter() { - if !paths.is_empty() { - paths.push('\n'); - } - paths.push_str(&path.to_string_lossy()); - } - - let mut order = String::new(); - for ix in self.order.iter() { - if !order.is_empty() { - order.push(','); - } - write!(&mut order, "{}", *ix).unwrap(); - } - SerializedPathList { paths, order } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_path_list() { - let list1 = PathList::new(&["a/d", "a/c"]); - let list2 = PathList::new(&["a/c", "a/d"]); - - assert_eq!(list1.paths(), list2.paths(), "paths differ"); - assert_eq!(list1.order(), &[1, 0], "list1 order incorrect"); - assert_eq!(list2.order(), &[0, 1], "list2 order incorrect"); - - let list1_deserialized = PathList::deserialize(&list1.serialize()); - assert_eq!(list1_deserialized, list1, "list1 deserialization failed"); - - let list2_deserialized = PathList::deserialize(&list2.serialize()); - assert_eq!(list2_deserialized, list2, "list2 deserialization failed"); - - assert_eq!( - list1.ordered_paths().collect_array().unwrap(), - [&PathBuf::from("a/d"), &PathBuf::from("a/c")], - "list1 ordered paths incorrect" - ); - assert_eq!( - list2.ordered_paths().collect_array().unwrap(), - [&PathBuf::from("a/c"), &PathBuf::from("a/d")], - "list2 ordered paths incorrect" - ); - } - - #[test] - fn test_path_list_ordering() { - let list = PathList::new(&["b", "a", "c"]); - assert_eq!( - list.paths(), - &[PathBuf::from("a"), PathBuf::from("b"), PathBuf::from("c")] - ); - assert_eq!(list.order(), &[1, 0, 2]); - assert!(!list.is_lexicographically_ordered()); - - let serialized = list.serialize(); - let deserialized = PathList::deserialize(&serialized); - assert_eq!(deserialized, list); - - assert_eq!( - deserialized.ordered_paths().collect_array().unwrap(), - [ - &PathBuf::from("b"), - &PathBuf::from("a"), - &PathBuf::from("c") - ] - ); - - let list = PathList::new(&["b", "c", "a"]); - assert_eq!( - list.paths(), - &[PathBuf::from("a"), PathBuf::from("b"), PathBuf::from("c")] - ); - assert_eq!(list.order(), &[2, 0, 1]); - assert!(!list.is_lexicographically_ordered()); - - let serialized = list.serialize(); - let deserialized = PathList::deserialize(&serialized); - assert_eq!(deserialized, list); - - assert_eq!( - deserialized.ordered_paths().collect_array().unwrap(), - [ - &PathBuf::from("b"), - &PathBuf::from("c"), - &PathBuf::from("a"), - ] - ); - } -} diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs deleted file mode 100644 index f1835caf8d..0000000000 --- a/crates/workspace/src/persistence.rs +++ /dev/null @@ -1,3051 +0,0 @@ -pub mod model; - -use std::{ - borrow::Cow, - collections::BTreeMap, - path::{Path, PathBuf}, - str::FromStr, - sync::Arc, -}; - -use anyhow::{Context as _, Result, bail}; -use collections::{HashMap, IndexSet}; -use db::{ - query, - sqlez::{connection::Connection, domain::Domain}, - sqlez_macros::sql, -}; -use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size}; -use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}; - -use language::{LanguageName, Toolchain, ToolchainScope}; -use project::WorktreeId; -use remote::{ - DockerConnectionOptions, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions, -}; -use sqlez::{ - bindable::{Bind, Column, StaticColumnCount}, - statement::Statement, - thread_safe_connection::ThreadSafeConnection, -}; - -use ui::{App, SharedString, px}; -use util::{ResultExt, maybe, rel_path::RelPath}; -use uuid::Uuid; - -use crate::{ - WorkspaceId, - path_list::{PathList, SerializedPathList}, - persistence::model::RemoteConnectionKind, -}; - -use model::{ - GroupId, ItemId, PaneId, RemoteConnectionId, SerializedItem, SerializedPane, - SerializedPaneGroup, SerializedWorkspace, -}; - -use self::model::{DockStructure, SerializedWorkspaceLocation}; - -#[derive(Copy, Clone, Debug, PartialEq)] -pub(crate) struct SerializedAxis(pub(crate) gpui::Axis); -impl sqlez::bindable::StaticColumnCount for SerializedAxis {} -impl sqlez::bindable::Bind for SerializedAxis { - fn bind( - &self, - statement: &sqlez::statement::Statement, - start_index: i32, - ) -> anyhow::Result { - match self.0 { - gpui::Axis::Horizontal => "Horizontal", - gpui::Axis::Vertical => "Vertical", - } - .bind(statement, start_index) - } -} - -impl sqlez::bindable::Column for SerializedAxis { - fn column( - statement: &mut sqlez::statement::Statement, - start_index: i32, - ) -> anyhow::Result<(Self, i32)> { - String::column(statement, start_index).and_then(|(axis_text, next_index)| { - Ok(( - match axis_text.as_str() { - "Horizontal" => Self(Axis::Horizontal), - "Vertical" => Self(Axis::Vertical), - _ => anyhow::bail!("Stored serialized item kind is incorrect"), - }, - next_index, - )) - }) - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Default)] -pub(crate) struct SerializedWindowBounds(pub(crate) WindowBounds); - -impl StaticColumnCount for SerializedWindowBounds { - fn column_count() -> usize { - 5 - } -} - -impl Bind for SerializedWindowBounds { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - match self.0 { - WindowBounds::Windowed(bounds) => { - let next_index = statement.bind(&"Windowed", start_index)?; - statement.bind( - &( - SerializedPixels(bounds.origin.x), - SerializedPixels(bounds.origin.y), - SerializedPixels(bounds.size.width), - SerializedPixels(bounds.size.height), - ), - next_index, - ) - } - WindowBounds::Maximized(bounds) => { - let next_index = statement.bind(&"Maximized", start_index)?; - statement.bind( - &( - SerializedPixels(bounds.origin.x), - SerializedPixels(bounds.origin.y), - SerializedPixels(bounds.size.width), - SerializedPixels(bounds.size.height), - ), - next_index, - ) - } - WindowBounds::Fullscreen(bounds) => { - let next_index = statement.bind(&"FullScreen", start_index)?; - statement.bind( - &( - SerializedPixels(bounds.origin.x), - SerializedPixels(bounds.origin.y), - SerializedPixels(bounds.size.width), - SerializedPixels(bounds.size.height), - ), - next_index, - ) - } - } - } -} - -impl Column for SerializedWindowBounds { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let (window_state, next_index) = String::column(statement, start_index)?; - let ((x, y, width, height), _): ((i32, i32, i32, i32), _) = - Column::column(statement, next_index)?; - let bounds = Bounds { - origin: point(px(x as f32), px(y as f32)), - size: size(px(width as f32), px(height as f32)), - }; - - let status = match window_state.as_str() { - "Windowed" | "Fixed" => SerializedWindowBounds(WindowBounds::Windowed(bounds)), - "Maximized" => SerializedWindowBounds(WindowBounds::Maximized(bounds)), - "FullScreen" => SerializedWindowBounds(WindowBounds::Fullscreen(bounds)), - _ => bail!("Window State did not have a valid string"), - }; - - Ok((status, next_index + 4)) - } -} - -#[derive(Debug)] -pub struct Breakpoint { - pub position: u32, - pub message: Option>, - pub condition: Option>, - pub hit_condition: Option>, - pub state: BreakpointState, -} - -/// Wrapper for DB type of a breakpoint -struct BreakpointStateWrapper<'a>(Cow<'a, BreakpointState>); - -impl From for BreakpointStateWrapper<'static> { - fn from(kind: BreakpointState) -> Self { - BreakpointStateWrapper(Cow::Owned(kind)) - } -} - -impl StaticColumnCount for BreakpointStateWrapper<'_> { - fn column_count() -> usize { - 1 - } -} - -impl Bind for BreakpointStateWrapper<'_> { - fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { - statement.bind(&self.0.to_int(), start_index) - } -} - -impl Column for BreakpointStateWrapper<'_> { - fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { - let state = statement.column_int(start_index)?; - - match state { - 0 => Ok((BreakpointState::Enabled.into(), start_index + 1)), - 1 => Ok((BreakpointState::Disabled.into(), start_index + 1)), - _ => anyhow::bail!("Invalid BreakpointState discriminant {state}"), - } - } -} - -impl sqlez::bindable::StaticColumnCount for Breakpoint { - fn column_count() -> usize { - // Position, log message, condition message, and hit condition message - 4 + BreakpointStateWrapper::column_count() - } -} - -impl sqlez::bindable::Bind for Breakpoint { - fn bind( - &self, - statement: &sqlez::statement::Statement, - start_index: i32, - ) -> anyhow::Result { - let next_index = statement.bind(&self.position, start_index)?; - let next_index = statement.bind(&self.message, next_index)?; - let next_index = statement.bind(&self.condition, next_index)?; - let next_index = statement.bind(&self.hit_condition, next_index)?; - statement.bind( - &BreakpointStateWrapper(Cow::Borrowed(&self.state)), - next_index, - ) - } -} - -impl Column for Breakpoint { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let position = statement - .column_int(start_index) - .with_context(|| format!("Failed to read BreakPoint at index {start_index}"))? - as u32; - let (message, next_index) = Option::::column(statement, start_index + 1)?; - let (condition, next_index) = Option::::column(statement, next_index)?; - let (hit_condition, next_index) = Option::::column(statement, next_index)?; - let (state, next_index) = BreakpointStateWrapper::column(statement, next_index)?; - - Ok(( - Breakpoint { - position, - message: message.map(Arc::from), - condition: condition.map(Arc::from), - hit_condition: hit_condition.map(Arc::from), - state: state.0.into_owned(), - }, - next_index, - )) - } -} - -#[derive(Clone, Debug, PartialEq)] -struct SerializedPixels(gpui::Pixels); -impl sqlez::bindable::StaticColumnCount for SerializedPixels {} - -impl sqlez::bindable::Bind for SerializedPixels { - fn bind( - &self, - statement: &sqlez::statement::Statement, - start_index: i32, - ) -> anyhow::Result { - let this: i32 = u32::from(self.0) as _; - this.bind(statement, start_index) - } -} - -pub struct WorkspaceDb(ThreadSafeConnection); - -impl Domain for WorkspaceDb { - const NAME: &str = stringify!(WorkspaceDb); - - const MIGRATIONS: &[&str] = &[ - sql!( - CREATE TABLE workspaces( - workspace_id INTEGER PRIMARY KEY, - workspace_location BLOB UNIQUE, - dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. - dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. - dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. - left_sidebar_open INTEGER, // Boolean - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) - ) STRICT; - - CREATE TABLE pane_groups( - group_id INTEGER PRIMARY KEY, - workspace_id INTEGER NOT NULL, - parent_group_id INTEGER, // NULL indicates that this is a root node - position INTEGER, // NULL indicates that this is a root node - axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - ) STRICT; - - CREATE TABLE panes( - pane_id INTEGER PRIMARY KEY, - workspace_id INTEGER NOT NULL, - active INTEGER NOT NULL, // Boolean - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE - ) STRICT; - - CREATE TABLE center_panes( - pane_id INTEGER PRIMARY KEY, - parent_group_id INTEGER, // NULL means that this is a root pane - position INTEGER, // NULL means that this is a root pane - FOREIGN KEY(pane_id) REFERENCES panes(pane_id) - ON DELETE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - ) STRICT; - - CREATE TABLE items( - item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique - workspace_id INTEGER NOT NULL, - pane_id INTEGER NOT NULL, - kind TEXT NOT NULL, - position INTEGER NOT NULL, - active INTEGER NOT NULL, - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE, - FOREIGN KEY(pane_id) REFERENCES panes(pane_id) - ON DELETE CASCADE, - PRIMARY KEY(item_id, workspace_id) - ) STRICT; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN window_state TEXT; - ALTER TABLE workspaces ADD COLUMN window_x REAL; - ALTER TABLE workspaces ADD COLUMN window_y REAL; - ALTER TABLE workspaces ADD COLUMN window_width REAL; - ALTER TABLE workspaces ADD COLUMN window_height REAL; - ALTER TABLE workspaces ADD COLUMN display BLOB; - ), - // Drop foreign key constraint from workspaces.dock_pane to panes table. - sql!( - CREATE TABLE workspaces_2( - workspace_id INTEGER PRIMARY KEY, - workspace_location BLOB UNIQUE, - dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. - dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. - dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. - left_sidebar_open INTEGER, // Boolean - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - window_state TEXT, - window_x REAL, - window_y REAL, - window_width REAL, - window_height REAL, - display BLOB - ) STRICT; - INSERT INTO workspaces_2 SELECT * FROM workspaces; - DROP TABLE workspaces; - ALTER TABLE workspaces_2 RENAME TO workspaces; - ), - // Add panels related information - sql!( - ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT; - ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT; - ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT; - ), - // Add panel zoom persistence - sql!( - ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool - ), - // Add pane group flex data - sql!( - ALTER TABLE pane_groups ADD COLUMN flexes TEXT; - ), - // Add fullscreen field to workspace - // Deprecated, `WindowBounds` holds the fullscreen state now. - // Preserving so users can downgrade Zed. - sql!( - ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool - ), - // Add preview field to items - sql!( - ALTER TABLE items ADD COLUMN preview INTEGER; //bool - ), - // Add centered_layout field to workspace - sql!( - ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool - ), - sql!( - CREATE TABLE remote_projects ( - remote_project_id INTEGER NOT NULL UNIQUE, - path TEXT, - dev_server_name TEXT - ); - ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER; - ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths; - ), - sql!( - DROP TABLE remote_projects; - CREATE TABLE dev_server_projects ( - id INTEGER NOT NULL UNIQUE, - path TEXT, - dev_server_name TEXT - ); - ALTER TABLE workspaces DROP COLUMN remote_project_id; - ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL; - ), - sql!( - ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0; - ), - sql!( - CREATE TABLE ssh_projects ( - id INTEGER PRIMARY KEY, - host TEXT NOT NULL, - port INTEGER, - path TEXT NOT NULL, - user TEXT - ); - ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE; - ), - sql!( - ALTER TABLE ssh_projects RENAME COLUMN path TO paths; - ), - sql!( - CREATE TABLE toolchains ( - workspace_id INTEGER, - worktree_id INTEGER, - language_name TEXT NOT NULL, - name TEXT NOT NULL, - path TEXT NOT NULL, - PRIMARY KEY (workspace_id, worktree_id, language_name) - ); - ), - sql!( - ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}"; - ), - sql!( - CREATE TABLE breakpoints ( - workspace_id INTEGER NOT NULL, - path TEXT NOT NULL, - breakpoint_location INTEGER NOT NULL, - kind INTEGER NOT NULL, - log_message TEXT, - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE - ); - ), - sql!( - ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT; - CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array); - ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT; - ), - sql!( - ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL - ), - sql!( - ALTER TABLE breakpoints DROP COLUMN kind - ), - sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL), - sql!( - ALTER TABLE breakpoints ADD COLUMN condition TEXT; - ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT; - ), - sql!(CREATE TABLE toolchains2 ( - workspace_id INTEGER, - worktree_id INTEGER, - language_name TEXT NOT NULL, - name TEXT NOT NULL, - path TEXT NOT NULL, - raw_json TEXT NOT NULL, - relative_worktree_path TEXT NOT NULL, - PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT; - INSERT INTO toolchains2 - SELECT * FROM toolchains; - DROP TABLE toolchains; - ALTER TABLE toolchains2 RENAME TO toolchains; - ), - sql!( - CREATE TABLE ssh_connections ( - id INTEGER PRIMARY KEY, - host TEXT NOT NULL, - port INTEGER, - user TEXT - ); - - INSERT INTO ssh_connections (host, port, user) - SELECT DISTINCT host, port, user - FROM ssh_projects; - - CREATE TABLE workspaces_2( - workspace_id INTEGER PRIMARY KEY, - paths TEXT, - paths_order TEXT, - ssh_connection_id INTEGER REFERENCES ssh_connections(id), - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - window_state TEXT, - window_x REAL, - window_y REAL, - window_width REAL, - window_height REAL, - display BLOB, - left_dock_visible INTEGER, - left_dock_active_panel TEXT, - right_dock_visible INTEGER, - right_dock_active_panel TEXT, - bottom_dock_visible INTEGER, - bottom_dock_active_panel TEXT, - left_dock_zoom INTEGER, - right_dock_zoom INTEGER, - bottom_dock_zoom INTEGER, - fullscreen INTEGER, - centered_layout INTEGER, - session_id TEXT, - window_id INTEGER - ) STRICT; - - INSERT - INTO workspaces_2 - SELECT - workspaces.workspace_id, - CASE - WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths - ELSE - CASE - WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN - NULL - ELSE - replace(workspaces.local_paths_array, ',', CHAR(10)) - END - END as paths, - - CASE - WHEN ssh_projects.id IS NOT NULL THEN "" - ELSE workspaces.local_paths_order_array - END as paths_order, - - CASE - WHEN ssh_projects.id IS NOT NULL THEN ( - SELECT ssh_connections.id - FROM ssh_connections - WHERE - ssh_connections.host IS ssh_projects.host AND - ssh_connections.port IS ssh_projects.port AND - ssh_connections.user IS ssh_projects.user - ) - ELSE NULL - END as ssh_connection_id, - - workspaces.timestamp, - workspaces.window_state, - workspaces.window_x, - workspaces.window_y, - workspaces.window_width, - workspaces.window_height, - workspaces.display, - workspaces.left_dock_visible, - workspaces.left_dock_active_panel, - workspaces.right_dock_visible, - workspaces.right_dock_active_panel, - workspaces.bottom_dock_visible, - workspaces.bottom_dock_active_panel, - workspaces.left_dock_zoom, - workspaces.right_dock_zoom, - workspaces.bottom_dock_zoom, - workspaces.fullscreen, - workspaces.centered_layout, - workspaces.session_id, - workspaces.window_id - FROM - workspaces LEFT JOIN - ssh_projects ON - workspaces.ssh_project_id = ssh_projects.id; - - DELETE FROM workspaces_2 - WHERE workspace_id NOT IN ( - SELECT MAX(workspace_id) - FROM workspaces_2 - GROUP BY ssh_connection_id, paths - ); - - DROP TABLE ssh_projects; - DROP TABLE workspaces; - ALTER TABLE workspaces_2 RENAME TO workspaces; - - CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths); - ), - // Fix any data from when workspaces.paths were briefly encoded as JSON arrays - sql!( - UPDATE workspaces - SET paths = CASE - WHEN substr(paths, 1, 2) = '[' || '"' AND substr(paths, -2, 2) = '"' || ']' THEN - replace( - substr(paths, 3, length(paths) - 4), - '"' || ',' || '"', - CHAR(10) - ) - ELSE - replace(paths, ',', CHAR(10)) - END - WHERE paths IS NOT NULL - ), - sql!( - CREATE TABLE remote_connections( - id INTEGER PRIMARY KEY, - kind TEXT NOT NULL, - host TEXT, - port INTEGER, - user TEXT, - distro TEXT - ); - - CREATE TABLE workspaces_2( - workspace_id INTEGER PRIMARY KEY, - paths TEXT, - paths_order TEXT, - remote_connection_id INTEGER REFERENCES remote_connections(id), - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - window_state TEXT, - window_x REAL, - window_y REAL, - window_width REAL, - window_height REAL, - display BLOB, - left_dock_visible INTEGER, - left_dock_active_panel TEXT, - right_dock_visible INTEGER, - right_dock_active_panel TEXT, - bottom_dock_visible INTEGER, - bottom_dock_active_panel TEXT, - left_dock_zoom INTEGER, - right_dock_zoom INTEGER, - bottom_dock_zoom INTEGER, - fullscreen INTEGER, - centered_layout INTEGER, - session_id TEXT, - window_id INTEGER - ) STRICT; - - INSERT INTO remote_connections - SELECT - id, - "ssh" as kind, - host, - port, - user, - NULL as distro - FROM ssh_connections; - - INSERT - INTO workspaces_2 - SELECT - workspace_id, - paths, - paths_order, - ssh_connection_id as remote_connection_id, - timestamp, - window_state, - window_x, - window_y, - window_width, - window_height, - display, - left_dock_visible, - left_dock_active_panel, - right_dock_visible, - right_dock_active_panel, - bottom_dock_visible, - bottom_dock_active_panel, - left_dock_zoom, - right_dock_zoom, - bottom_dock_zoom, - fullscreen, - centered_layout, - session_id, - window_id - FROM - workspaces; - - DROP TABLE workspaces; - ALTER TABLE workspaces_2 RENAME TO workspaces; - - CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(remote_connection_id, paths); - ), - sql!(CREATE TABLE user_toolchains ( - remote_connection_id INTEGER, - workspace_id INTEGER NOT NULL, - worktree_id INTEGER NOT NULL, - relative_worktree_path TEXT NOT NULL, - language_name TEXT NOT NULL, - name TEXT NOT NULL, - path TEXT NOT NULL, - raw_json TEXT NOT NULL, - - PRIMARY KEY (workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) - ) STRICT;), - sql!( - DROP TABLE ssh_connections; - ), - sql!( - ALTER TABLE remote_connections ADD COLUMN name TEXT; - ALTER TABLE remote_connections ADD COLUMN container_id TEXT; - ), - ]; - - // Allow recovering from bad migration that was initially shipped to nightly - // when introducing the ssh_connections table. - fn should_allow_migration_change(_index: usize, old: &str, new: &str) -> bool { - old.starts_with("CREATE TABLE ssh_connections") - && new.starts_with("CREATE TABLE ssh_connections") - } -} - -db::static_connection!(DB, WorkspaceDb, []); - -impl WorkspaceDb { - /// Returns a serialized workspace for the given worktree_roots. If the passed array - /// is empty, the most recent workspace is returned instead. If no workspace for the - /// passed roots is stored, returns none. - pub(crate) fn workspace_for_roots>( - &self, - worktree_roots: &[P], - ) -> Option { - self.workspace_for_roots_internal(worktree_roots, None) - } - - pub(crate) fn remote_workspace_for_roots>( - &self, - worktree_roots: &[P], - remote_project_id: RemoteConnectionId, - ) -> Option { - self.workspace_for_roots_internal(worktree_roots, Some(remote_project_id)) - } - - pub(crate) fn workspace_for_roots_internal>( - &self, - worktree_roots: &[P], - remote_connection_id: Option, - ) -> Option { - // paths are sorted before db interactions to ensure that the order of the paths - // doesn't affect the workspace selection for existing workspaces - let root_paths = PathList::new(worktree_roots); - - // Note that we re-assign the workspace_id here in case it's empty - // and we've grabbed the most recent workspace - let ( - workspace_id, - paths, - paths_order, - window_bounds, - display, - centered_layout, - docks, - window_id, - ): ( - WorkspaceId, - String, - String, - Option, - Option, - Option, - DockStructure, - Option, - ) = self - .select_row_bound(sql! { - SELECT - workspace_id, - paths, - paths_order, - window_state, - window_x, - window_y, - window_width, - window_height, - display, - centered_layout, - left_dock_visible, - left_dock_active_panel, - left_dock_zoom, - right_dock_visible, - right_dock_active_panel, - right_dock_zoom, - bottom_dock_visible, - bottom_dock_active_panel, - bottom_dock_zoom, - window_id - FROM workspaces - WHERE - paths IS ? AND - remote_connection_id IS ? - LIMIT 1 - }) - .and_then(|mut prepared_statement| { - (prepared_statement)(( - root_paths.serialize().paths, - remote_connection_id.map(|id| id.0 as i32), - )) - }) - .context("No workspaces found") - .warn_on_err() - .flatten()?; - - let paths = PathList::deserialize(&SerializedPathList { - paths, - order: paths_order, - }); - - let remote_connection_options = if let Some(remote_connection_id) = remote_connection_id { - self.remote_connection(remote_connection_id) - .context("Get remote connection") - .log_err() - } else { - None - }; - - Some(SerializedWorkspace { - id: workspace_id, - location: match remote_connection_options { - Some(options) => SerializedWorkspaceLocation::Remote(options), - None => SerializedWorkspaceLocation::Local, - }, - paths, - center_group: self - .get_center_pane_group(workspace_id) - .context("Getting center group") - .log_err()?, - window_bounds, - centered_layout: centered_layout.unwrap_or(false), - display, - docks, - session_id: None, - breakpoints: self.breakpoints(workspace_id), - window_id, - user_toolchains: self.user_toolchains(workspace_id, remote_connection_id), - }) - } - - fn breakpoints(&self, workspace_id: WorkspaceId) -> BTreeMap, Vec> { - let breakpoints: Result> = self - .select_bound(sql! { - SELECT path, breakpoint_location, log_message, condition, hit_condition, state - FROM breakpoints - WHERE workspace_id = ? - }) - .and_then(|mut prepared_statement| (prepared_statement)(workspace_id)); - - match breakpoints { - Ok(bp) => { - if bp.is_empty() { - log::debug!("Breakpoints are empty after querying database for them"); - } - - let mut map: BTreeMap, Vec> = Default::default(); - - for (path, breakpoint) in bp { - let path: Arc = path.into(); - map.entry(path.clone()).or_default().push(SourceBreakpoint { - row: breakpoint.position, - path, - message: breakpoint.message, - condition: breakpoint.condition, - hit_condition: breakpoint.hit_condition, - state: breakpoint.state, - }); - } - - for (path, bps) in map.iter() { - log::info!( - "Got {} breakpoints from database at path: {}", - bps.len(), - path.to_string_lossy() - ); - } - - map - } - Err(msg) => { - log::error!("Breakpoints query failed with msg: {msg}"); - Default::default() - } - } - } - - fn user_toolchains( - &self, - workspace_id: WorkspaceId, - remote_connection_id: Option, - ) -> BTreeMap> { - type RowKind = (WorkspaceId, u64, String, String, String, String, String); - - let toolchains: Vec = self - .select_bound(sql! { - SELECT workspace_id, worktree_id, relative_worktree_path, - language_name, name, path, raw_json - FROM user_toolchains WHERE remote_connection_id IS ?1 AND ( - workspace_id IN (0, ?2) - ) - }) - .and_then(|mut statement| { - (statement)((remote_connection_id.map(|id| id.0), workspace_id)) - }) - .unwrap_or_default(); - let mut ret = BTreeMap::<_, IndexSet<_>>::default(); - - for ( - _workspace_id, - worktree_id, - relative_worktree_path, - language_name, - name, - path, - raw_json, - ) in toolchains - { - // INTEGER's that are primary keys (like workspace ids, remote connection ids and such) start at 1, so we're safe to - let scope = if _workspace_id == WorkspaceId(0) { - debug_assert_eq!(worktree_id, u64::MAX); - debug_assert_eq!(relative_worktree_path, String::default()); - ToolchainScope::Global - } else { - debug_assert_eq!(workspace_id, _workspace_id); - debug_assert_eq!( - worktree_id == u64::MAX, - relative_worktree_path == String::default() - ); - - let Some(relative_path) = RelPath::unix(&relative_worktree_path).log_err() else { - continue; - }; - if worktree_id != u64::MAX && relative_worktree_path != String::default() { - ToolchainScope::Subproject( - WorktreeId::from_usize(worktree_id as usize), - relative_path.into(), - ) - } else { - ToolchainScope::Project - } - }; - let Ok(as_json) = serde_json::from_str(&raw_json) else { - continue; - }; - let toolchain = Toolchain { - name: SharedString::from(name), - path: SharedString::from(path), - language_name: LanguageName::from_proto(language_name), - as_json, - }; - ret.entry(scope).or_default().insert(toolchain); - } - - ret - } - - /// Saves a workspace using the worktree roots. Will garbage collect any workspaces - /// that used this workspace previously - pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) { - let paths = workspace.paths.serialize(); - log::debug!("Saving workspace at location: {:?}", workspace.location); - self.write(move |conn| { - conn.with_savepoint("update_worktrees", || { - let remote_connection_id = match workspace.location.clone() { - SerializedWorkspaceLocation::Local => None, - SerializedWorkspaceLocation::Remote(connection_options) => { - Some(Self::get_or_create_remote_connection_internal( - conn, - connection_options - )?.0) - } - }; - - // Clear out panes and pane_groups - conn.exec_bound(sql!( - DELETE FROM pane_groups WHERE workspace_id = ?1; - DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id) - .context("Clearing old panes")?; - - conn.exec_bound( - sql!( - DELETE FROM breakpoints WHERE workspace_id = ?1; - ) - )?(workspace.id).context("Clearing old breakpoints")?; - - for (path, breakpoints) in workspace.breakpoints { - for bp in breakpoints { - let state = BreakpointStateWrapper::from(bp.state); - match conn.exec_bound(sql!( - INSERT INTO breakpoints (workspace_id, path, breakpoint_location, log_message, condition, hit_condition, state) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);))? - - (( - workspace.id, - path.as_ref(), - bp.row, - bp.message, - bp.condition, - bp.hit_condition, - state, - )) { - Ok(_) => { - log::debug!("Stored breakpoint at row: {} in path: {}", bp.row, path.to_string_lossy()) - } - Err(err) => { - log::error!("{err}"); - continue; - } - } - } - } - - conn.exec_bound( - sql!( - DELETE FROM user_toolchains WHERE workspace_id = ?1; - ) - )?(workspace.id).context("Clearing old user toolchains")?; - - for (scope, toolchains) in workspace.user_toolchains { - for toolchain in toolchains { - let query = sql!(INSERT OR REPLACE INTO user_toolchains(remote_connection_id, workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)); - let (workspace_id, worktree_id, relative_worktree_path) = match scope { - ToolchainScope::Subproject(worktree_id, ref path) => (Some(workspace.id), Some(worktree_id), Some(path.as_unix_str().to_owned())), - ToolchainScope::Project => (Some(workspace.id), None, None), - ToolchainScope::Global => (None, None, None), - }; - let args = (remote_connection_id, workspace_id.unwrap_or(WorkspaceId(0)), worktree_id.map_or(usize::MAX,|id| id.to_usize()), relative_worktree_path.unwrap_or_default(), - toolchain.language_name.as_ref().to_owned(), toolchain.name.to_string(), toolchain.path.to_string(), toolchain.as_json.to_string()); - if let Err(err) = conn.exec_bound(query)?(args) { - log::error!("{err}"); - continue; - } - } - } - - conn.exec_bound(sql!( - DELETE - FROM workspaces - WHERE - workspace_id != ?1 AND - paths IS ?2 AND - remote_connection_id IS ?3 - ))?(( - workspace.id, - paths.paths.clone(), - remote_connection_id, - )) - .context("clearing out old locations")?; - - // Upsert - let query = sql!( - INSERT INTO workspaces( - workspace_id, - paths, - paths_order, - remote_connection_id, - left_dock_visible, - left_dock_active_panel, - left_dock_zoom, - right_dock_visible, - right_dock_active_panel, - right_dock_zoom, - bottom_dock_visible, - bottom_dock_active_panel, - bottom_dock_zoom, - session_id, - window_id, - timestamp - ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, CURRENT_TIMESTAMP) - ON CONFLICT DO - UPDATE SET - paths = ?2, - paths_order = ?3, - remote_connection_id = ?4, - left_dock_visible = ?5, - left_dock_active_panel = ?6, - left_dock_zoom = ?7, - right_dock_visible = ?8, - right_dock_active_panel = ?9, - right_dock_zoom = ?10, - bottom_dock_visible = ?11, - bottom_dock_active_panel = ?12, - bottom_dock_zoom = ?13, - session_id = ?14, - window_id = ?15, - timestamp = CURRENT_TIMESTAMP - ); - let mut prepared_query = conn.exec_bound(query)?; - let args = ( - workspace.id, - paths.paths.clone(), - paths.order.clone(), - remote_connection_id, - workspace.docks, - workspace.session_id, - workspace.window_id, - ); - - prepared_query(args).context("Updating workspace")?; - - // Save center pane group - Self::save_pane_group(conn, workspace.id, &workspace.center_group, None) - .context("save pane group in save workspace")?; - - Ok(()) - }) - .log_err(); - }) - .await; - } - - pub(crate) async fn get_or_create_remote_connection( - &self, - options: RemoteConnectionOptions, - ) -> Result { - self.write(move |conn| Self::get_or_create_remote_connection_internal(conn, options)) - .await - } - - fn get_or_create_remote_connection_internal( - this: &Connection, - options: RemoteConnectionOptions, - ) -> Result { - let kind; - let mut user = None; - let mut host = None; - let mut port = None; - let mut distro = None; - let mut name = None; - let mut container_id = None; - match options { - RemoteConnectionOptions::Ssh(options) => { - kind = RemoteConnectionKind::Ssh; - host = Some(options.host); - port = options.port; - user = options.username; - } - RemoteConnectionOptions::Wsl(options) => { - kind = RemoteConnectionKind::Wsl; - distro = Some(options.distro_name); - user = options.user; - } - RemoteConnectionOptions::Docker(options) => { - kind = RemoteConnectionKind::Docker; - container_id = Some(options.container_id); - name = Some(options.name); - } - } - Self::get_or_create_remote_connection_query( - this, - kind, - host, - port, - user, - distro, - name, - container_id, - ) - } - - fn get_or_create_remote_connection_query( - this: &Connection, - kind: RemoteConnectionKind, - host: Option, - port: Option, - user: Option, - distro: Option, - name: Option, - container_id: Option, - ) -> Result { - if let Some(id) = this.select_row_bound(sql!( - SELECT id - FROM remote_connections - WHERE - kind IS ? AND - host IS ? AND - port IS ? AND - user IS ? AND - distro IS ? AND - name IS ? AND - container_id IS ? - LIMIT 1 - ))?(( - kind.serialize(), - host.clone(), - port, - user.clone(), - distro.clone(), - name.clone(), - container_id.clone(), - ))? { - Ok(RemoteConnectionId(id)) - } else { - let id = this.select_row_bound(sql!( - INSERT INTO remote_connections ( - kind, - host, - port, - user, - distro, - name, - container_id - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) - RETURNING id - ))?(( - kind.serialize(), - host, - port, - user, - distro, - name, - container_id, - ))? - .context("failed to insert remote project")?; - Ok(RemoteConnectionId(id)) - } - } - - query! { - pub async fn next_id() -> Result { - INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id - } - } - - fn recent_workspaces( - &self, - ) -> Result)>> { - Ok(self - .recent_workspaces_query()? - .into_iter() - .map(|(id, paths, order, remote_connection_id)| { - ( - id, - PathList::deserialize(&SerializedPathList { paths, order }), - remote_connection_id.map(RemoteConnectionId), - ) - }) - .collect()) - } - - query! { - fn recent_workspaces_query() -> Result)>> { - SELECT workspace_id, paths, paths_order, remote_connection_id - FROM workspaces - WHERE - paths IS NOT NULL OR - remote_connection_id IS NOT NULL - ORDER BY timestamp DESC - } - } - - fn session_workspaces( - &self, - session_id: String, - ) -> Result, Option)>> { - Ok(self - .session_workspaces_query(session_id)? - .into_iter() - .map(|(paths, order, window_id, remote_connection_id)| { - ( - PathList::deserialize(&SerializedPathList { paths, order }), - window_id, - remote_connection_id.map(RemoteConnectionId), - ) - }) - .collect()) - } - - query! { - fn session_workspaces_query(session_id: String) -> Result, Option)>> { - SELECT paths, paths_order, window_id, remote_connection_id - FROM workspaces - WHERE session_id = ?1 - ORDER BY timestamp DESC - } - } - - query! { - pub fn breakpoints_for_file(workspace_id: WorkspaceId, file_path: &Path) -> Result> { - SELECT breakpoint_location - FROM breakpoints - WHERE workspace_id= ?1 AND path = ?2 - } - } - - query! { - pub fn clear_breakpoints(file_path: &Path) -> Result<()> { - DELETE FROM breakpoints - WHERE file_path = ?2 - } - } - - fn remote_connections(&self) -> Result> { - Ok(self.select(sql!( - SELECT - id, kind, host, port, user, distro, container_id, name - FROM - remote_connections - ))?()? - .into_iter() - .filter_map(|(id, kind, host, port, user, distro, container_id, name)| { - Some(( - RemoteConnectionId(id), - Self::remote_connection_from_row( - kind, - host, - port, - user, - distro, - container_id, - name, - )?, - )) - }) - .collect()) - } - - pub(crate) fn remote_connection( - &self, - id: RemoteConnectionId, - ) -> Result { - let (kind, host, port, user, distro, container_id, name) = self.select_row_bound(sql!( - SELECT kind, host, port, user, distro, container_id, name - FROM remote_connections - WHERE id = ? - ))?(id.0)? - .context("no such remote connection")?; - Self::remote_connection_from_row(kind, host, port, user, distro, container_id, name) - .context("invalid remote_connection row") - } - - fn remote_connection_from_row( - kind: String, - host: Option, - port: Option, - user: Option, - distro: Option, - container_id: Option, - name: Option, - ) -> Option { - match RemoteConnectionKind::deserialize(&kind)? { - RemoteConnectionKind::Wsl => Some(RemoteConnectionOptions::Wsl(WslConnectionOptions { - distro_name: distro?, - user: user, - })), - RemoteConnectionKind::Ssh => Some(RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: host?, - port, - username: user, - ..Default::default() - })), - RemoteConnectionKind::Docker => { - Some(RemoteConnectionOptions::Docker(DockerConnectionOptions { - container_id: container_id?, - name: name?, - upload_binary_over_docker_exec: false, - })) - } - } - } - - pub(crate) fn last_window( - &self, - ) -> anyhow::Result<(Option, Option)> { - let mut prepared_query = - self.select::<(Option, Option)>(sql!( - SELECT - display, - window_state, window_x, window_y, window_width, window_height - FROM workspaces - WHERE paths - IS NOT NULL - ORDER BY timestamp DESC - LIMIT 1 - ))?; - let result = prepared_query()?; - Ok(result.into_iter().next().unwrap_or((None, None))) - } - - query! { - pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> { - DELETE FROM workspaces - WHERE workspace_id IS ? - } - } - - // Returns the recent locations which are still valid on disk and deletes ones which no longer - // exist. - pub async fn recent_workspaces_on_disk( - &self, - ) -> Result> { - let mut result = Vec::new(); - let mut delete_tasks = Vec::new(); - let remote_connections = self.remote_connections()?; - - for (id, paths, remote_connection_id) in self.recent_workspaces()? { - if let Some(remote_connection_id) = remote_connection_id { - if let Some(connection_options) = remote_connections.get(&remote_connection_id) { - result.push(( - id, - SerializedWorkspaceLocation::Remote(connection_options.clone()), - paths, - )); - } else { - delete_tasks.push(self.delete_workspace_by_id(id)); - } - continue; - } - - let has_wsl_path = if cfg!(windows) { - paths - .paths() - .iter() - .any(|path| util::paths::WslPath::from_path(path).is_some()) - } else { - false - }; - - // Delete the workspace if any of the paths are WSL paths. - // If a local workspace points to WSL, this check will cause us to wait for the - // WSL VM and file server to boot up. This can block for many seconds. - // Supported scenarios use remote workspaces. - if !has_wsl_path && paths.paths().iter().all(|path| path.exists()) { - // Only show directories in recent projects - if paths.paths().iter().any(|path| path.is_dir()) { - result.push((id, SerializedWorkspaceLocation::Local, paths)); - } - } else { - delete_tasks.push(self.delete_workspace_by_id(id)); - } - } - - futures::future::join_all(delete_tasks).await; - Ok(result) - } - - pub async fn last_workspace(&self) -> Result> { - Ok(self - .recent_workspaces_on_disk() - .await? - .into_iter() - .next() - .map(|(_, location, paths)| (location, paths))) - } - - // Returns the locations of the workspaces that were still opened when the last - // session was closed (i.e. when Zed was quit). - // If `last_session_window_order` is provided, the returned locations are ordered - // according to that. - pub fn last_session_workspace_locations( - &self, - last_session_id: &str, - last_session_window_stack: Option>, - ) -> Result> { - let mut workspaces = Vec::new(); - - for (paths, window_id, remote_connection_id) in - self.session_workspaces(last_session_id.to_owned())? - { - if let Some(remote_connection_id) = remote_connection_id { - workspaces.push(( - SerializedWorkspaceLocation::Remote( - self.remote_connection(remote_connection_id)?, - ), - paths, - window_id.map(WindowId::from), - )); - } else if paths.paths().iter().all(|path| path.exists()) - && paths.paths().iter().any(|path| path.is_dir()) - { - workspaces.push(( - SerializedWorkspaceLocation::Local, - paths, - window_id.map(WindowId::from), - )); - } - } - - if let Some(stack) = last_session_window_stack { - workspaces.sort_by_key(|(_, _, window_id)| { - window_id - .and_then(|id| stack.iter().position(|&order_id| order_id == id)) - .unwrap_or(usize::MAX) - }); - } - - Ok(workspaces - .into_iter() - .map(|(location, paths, _)| (location, paths)) - .collect::>()) - } - - fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result { - Ok(self - .get_pane_group(workspace_id, None)? - .into_iter() - .next() - .unwrap_or_else(|| { - SerializedPaneGroup::Pane(SerializedPane { - active: true, - children: vec![], - pinned_count: 0, - }) - })) - } - - fn get_pane_group( - &self, - workspace_id: WorkspaceId, - group_id: Option, - ) -> Result> { - type GroupKey = (Option, WorkspaceId); - type GroupOrPane = ( - Option, - Option, - Option, - Option, - Option, - Option, - ); - self.select_bound::(sql!( - SELECT group_id, axis, pane_id, active, pinned_count, flexes - FROM (SELECT - group_id, - axis, - NULL as pane_id, - NULL as active, - NULL as pinned_count, - position, - parent_group_id, - workspace_id, - flexes - FROM pane_groups - UNION - SELECT - NULL, - NULL, - center_panes.pane_id, - panes.active as active, - pinned_count, - position, - parent_group_id, - panes.workspace_id as workspace_id, - NULL - FROM center_panes - JOIN panes ON center_panes.pane_id = panes.pane_id) - WHERE parent_group_id IS ? AND workspace_id = ? - ORDER BY position - ))?((group_id, workspace_id))? - .into_iter() - .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| { - let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) }); - if let Some((group_id, axis)) = group_id.zip(axis) { - let flexes = flexes - .map(|flexes: String| serde_json::from_str::>(&flexes)) - .transpose()?; - - Ok(SerializedPaneGroup::Group { - axis, - children: self.get_pane_group(workspace_id, Some(group_id))?, - flexes, - }) - } else if let Some((pane_id, active, pinned_count)) = maybe_pane { - Ok(SerializedPaneGroup::Pane(SerializedPane::new( - self.get_items(pane_id)?, - active, - pinned_count, - ))) - } else { - bail!("Pane Group Child was neither a pane group or a pane"); - } - }) - // Filter out panes and pane groups which don't have any children or items - .filter(|pane_group| match pane_group { - Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(), - Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(), - _ => true, - }) - .collect::>() - } - - fn save_pane_group( - conn: &Connection, - workspace_id: WorkspaceId, - pane_group: &SerializedPaneGroup, - parent: Option<(GroupId, usize)>, - ) -> Result<()> { - if parent.is_none() { - log::debug!("Saving a pane group for workspace {workspace_id:?}"); - } - match pane_group { - SerializedPaneGroup::Group { - axis, - children, - flexes, - } => { - let (parent_id, position) = parent.unzip(); - - let flex_string = flexes - .as_ref() - .map(|flexes| serde_json::json!(flexes).to_string()); - - let group_id = conn.select_row_bound::<_, i64>(sql!( - INSERT INTO pane_groups( - workspace_id, - parent_group_id, - position, - axis, - flexes - ) - VALUES (?, ?, ?, ?, ?) - RETURNING group_id - ))?(( - workspace_id, - parent_id, - position, - *axis, - flex_string, - ))? - .context("Couldn't retrieve group_id from inserted pane_group")?; - - for (position, group) in children.iter().enumerate() { - Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))? - } - - Ok(()) - } - SerializedPaneGroup::Pane(pane) => { - Self::save_pane(conn, workspace_id, pane, parent)?; - Ok(()) - } - } - } - - fn save_pane( - conn: &Connection, - workspace_id: WorkspaceId, - pane: &SerializedPane, - parent: Option<(GroupId, usize)>, - ) -> Result { - let pane_id = conn.select_row_bound::<_, i64>(sql!( - INSERT INTO panes(workspace_id, active, pinned_count) - VALUES (?, ?, ?) - RETURNING pane_id - ))?((workspace_id, pane.active, pane.pinned_count))? - .context("Could not retrieve inserted pane_id")?; - - let (parent_id, order) = parent.unzip(); - conn.exec_bound(sql!( - INSERT INTO center_panes(pane_id, parent_group_id, position) - VALUES (?, ?, ?) - ))?((pane_id, parent_id, order))?; - - Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?; - - Ok(pane_id) - } - - fn get_items(&self, pane_id: PaneId) -> Result> { - self.select_bound(sql!( - SELECT kind, item_id, active, preview FROM items - WHERE pane_id = ? - ORDER BY position - ))?(pane_id) - } - - fn save_items( - conn: &Connection, - workspace_id: WorkspaceId, - pane_id: PaneId, - items: &[SerializedItem], - ) -> Result<()> { - let mut insert = conn.exec_bound(sql!( - INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?) - )).context("Preparing insertion")?; - for (position, item) in items.iter().enumerate() { - insert((workspace_id, pane_id, position, item))?; - } - - Ok(()) - } - - query! { - pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> { - UPDATE workspaces - SET timestamp = CURRENT_TIMESTAMP - WHERE workspace_id = ? - } - } - - query! { - pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> { - UPDATE workspaces - SET window_state = ?2, - window_x = ?3, - window_y = ?4, - window_width = ?5, - window_height = ?6, - display = ?7 - WHERE workspace_id = ?1 - } - } - - query! { - pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> { - UPDATE workspaces - SET centered_layout = ?2 - WHERE workspace_id = ?1 - } - } - - query! { - pub(crate) async fn set_session_id(workspace_id: WorkspaceId, session_id: Option) -> Result<()> { - UPDATE workspaces - SET session_id = ?2 - WHERE workspace_id = ?1 - } - } - - pub(crate) async fn toolchains( - &self, - workspace_id: WorkspaceId, - ) -> Result)>> { - self.write(move |this| { - let mut select = this - .select_bound(sql!( - SELECT - name, path, worktree_id, relative_worktree_path, language_name, raw_json - FROM toolchains - WHERE workspace_id = ? - )) - .context("select toolchains")?; - - let toolchain: Vec<(String, String, u64, String, String, String)> = - select(workspace_id)?; - - Ok(toolchain - .into_iter() - .filter_map( - |(name, path, worktree_id, relative_worktree_path, language, json)| { - Some(( - Toolchain { - name: name.into(), - path: path.into(), - language_name: LanguageName::new(&language), - as_json: serde_json::Value::from_str(&json).ok()?, - }, - WorktreeId::from_proto(worktree_id), - RelPath::from_proto(&relative_worktree_path).log_err()?, - )) - }, - ) - .collect()) - }) - .await - } - - pub async fn set_toolchain( - &self, - workspace_id: WorkspaceId, - worktree_id: WorktreeId, - relative_worktree_path: Arc, - toolchain: Toolchain, - ) -> Result<()> { - log::debug!( - "Setting toolchain for workspace, worktree: {worktree_id:?}, relative path: {relative_worktree_path:?}, toolchain: {}", - toolchain.name - ); - self.write(move |conn| { - let mut insert = conn - .exec_bound(sql!( - INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?, ?, ?) - ON CONFLICT DO - UPDATE SET - name = ?5, - path = ?6, - raw_json = ?7 - )) - .context("Preparing insertion")?; - - insert(( - workspace_id, - worktree_id.to_usize(), - relative_worktree_path.as_unix_str(), - toolchain.language_name.as_ref(), - toolchain.name.as_ref(), - toolchain.path.as_ref(), - toolchain.as_json.to_string(), - ))?; - - Ok(()) - }).await - } -} - -pub fn delete_unloaded_items( - alive_items: Vec, - workspace_id: WorkspaceId, - table: &'static str, - db: &ThreadSafeConnection, - cx: &mut App, -) -> Task> { - let db = db.clone(); - cx.spawn(async move |_| { - let placeholders = alive_items - .iter() - .map(|_| "?") - .collect::>() - .join(", "); - - let query = format!( - "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})" - ); - - db.write(move |conn| { - let mut statement = Statement::prepare(conn, query)?; - let mut next_index = statement.bind(&workspace_id, 1)?; - for id in alive_items { - next_index = statement.bind(&id, next_index)?; - } - statement.exec() - }) - .await - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::persistence::model::{ - SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, - }; - use gpui; - use pretty_assertions::assert_eq; - use remote::SshConnectionOptions; - use std::{thread, time::Duration}; - - #[gpui::test] - async fn test_breakpoints() { - zlog::init_test(); - - let db = WorkspaceDb::open_test_db("test_breakpoints").await; - let id = db.next_id().await.unwrap(); - - let path = Path::new("/tmp/test.rs"); - - let breakpoint = Breakpoint { - position: 123, - message: None, - state: BreakpointState::Enabled, - condition: None, - hit_condition: None, - }; - - let log_breakpoint = Breakpoint { - position: 456, - message: Some("Test log message".into()), - state: BreakpointState::Enabled, - condition: None, - hit_condition: None, - }; - - let disable_breakpoint = Breakpoint { - position: 578, - message: None, - state: BreakpointState::Disabled, - condition: None, - hit_condition: None, - }; - - let condition_breakpoint = Breakpoint { - position: 789, - message: None, - state: BreakpointState::Enabled, - condition: Some("x > 5".into()), - hit_condition: None, - }; - - let hit_condition_breakpoint = Breakpoint { - position: 999, - message: None, - state: BreakpointState::Enabled, - condition: None, - hit_condition: Some(">= 3".into()), - }; - - let workspace = SerializedWorkspace { - id, - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: { - let mut map = collections::BTreeMap::default(); - map.insert( - Arc::from(path), - vec![ - SourceBreakpoint { - row: breakpoint.position, - path: Arc::from(path), - message: breakpoint.message.clone(), - state: breakpoint.state, - condition: breakpoint.condition.clone(), - hit_condition: breakpoint.hit_condition.clone(), - }, - SourceBreakpoint { - row: log_breakpoint.position, - path: Arc::from(path), - message: log_breakpoint.message.clone(), - state: log_breakpoint.state, - condition: log_breakpoint.condition.clone(), - hit_condition: log_breakpoint.hit_condition.clone(), - }, - SourceBreakpoint { - row: disable_breakpoint.position, - path: Arc::from(path), - message: disable_breakpoint.message.clone(), - state: disable_breakpoint.state, - condition: disable_breakpoint.condition.clone(), - hit_condition: disable_breakpoint.hit_condition.clone(), - }, - SourceBreakpoint { - row: condition_breakpoint.position, - path: Arc::from(path), - message: condition_breakpoint.message.clone(), - state: condition_breakpoint.state, - condition: condition_breakpoint.condition.clone(), - hit_condition: condition_breakpoint.hit_condition.clone(), - }, - SourceBreakpoint { - row: hit_condition_breakpoint.position, - path: Arc::from(path), - message: hit_condition_breakpoint.message.clone(), - state: hit_condition_breakpoint.state, - condition: hit_condition_breakpoint.condition.clone(), - hit_condition: hit_condition_breakpoint.hit_condition.clone(), - }, - ], - ); - map - }, - session_id: None, - window_id: None, - user_toolchains: Default::default(), - }; - - db.save_workspace(workspace.clone()).await; - - let loaded = db.workspace_for_roots(&["/tmp"]).unwrap(); - let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap(); - - assert_eq!(loaded_breakpoints.len(), 5); - - // normal breakpoint - assert_eq!(loaded_breakpoints[0].row, breakpoint.position); - assert_eq!(loaded_breakpoints[0].message, breakpoint.message); - assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition); - assert_eq!( - loaded_breakpoints[0].hit_condition, - breakpoint.hit_condition - ); - assert_eq!(loaded_breakpoints[0].state, breakpoint.state); - assert_eq!(loaded_breakpoints[0].path, Arc::from(path)); - - // enabled breakpoint - assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position); - assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message); - assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition); - assert_eq!( - loaded_breakpoints[1].hit_condition, - log_breakpoint.hit_condition - ); - assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state); - assert_eq!(loaded_breakpoints[1].path, Arc::from(path)); - - // disable breakpoint - assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position); - assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message); - assert_eq!( - loaded_breakpoints[2].condition, - disable_breakpoint.condition - ); - assert_eq!( - loaded_breakpoints[2].hit_condition, - disable_breakpoint.hit_condition - ); - assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state); - assert_eq!(loaded_breakpoints[2].path, Arc::from(path)); - - // condition breakpoint - assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position); - assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message); - assert_eq!( - loaded_breakpoints[3].condition, - condition_breakpoint.condition - ); - assert_eq!( - loaded_breakpoints[3].hit_condition, - condition_breakpoint.hit_condition - ); - assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state); - assert_eq!(loaded_breakpoints[3].path, Arc::from(path)); - - // hit condition breakpoint - assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position); - assert_eq!( - loaded_breakpoints[4].message, - hit_condition_breakpoint.message - ); - assert_eq!( - loaded_breakpoints[4].condition, - hit_condition_breakpoint.condition - ); - assert_eq!( - loaded_breakpoints[4].hit_condition, - hit_condition_breakpoint.hit_condition - ); - assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state); - assert_eq!(loaded_breakpoints[4].path, Arc::from(path)); - } - - #[gpui::test] - async fn test_remove_last_breakpoint() { - zlog::init_test(); - - let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await; - let id = db.next_id().await.unwrap(); - - let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs"); - - let breakpoint_to_remove = Breakpoint { - position: 100, - message: None, - state: BreakpointState::Enabled, - condition: None, - hit_condition: None, - }; - - let workspace = SerializedWorkspace { - id, - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: { - let mut map = collections::BTreeMap::default(); - map.insert( - Arc::from(singular_path), - vec![SourceBreakpoint { - row: breakpoint_to_remove.position, - path: Arc::from(singular_path), - message: None, - state: BreakpointState::Enabled, - condition: None, - hit_condition: None, - }], - ); - map - }, - session_id: None, - window_id: None, - user_toolchains: Default::default(), - }; - - db.save_workspace(workspace.clone()).await; - - let loaded = db.workspace_for_roots(&["/tmp"]).unwrap(); - let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap(); - - assert_eq!(loaded_breakpoints.len(), 1); - assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position); - assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message); - assert_eq!( - loaded_breakpoints[0].condition, - breakpoint_to_remove.condition - ); - assert_eq!( - loaded_breakpoints[0].hit_condition, - breakpoint_to_remove.hit_condition - ); - assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state); - assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path)); - - let workspace_without_breakpoint = SerializedWorkspace { - id, - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: collections::BTreeMap::default(), - session_id: None, - window_id: None, - user_toolchains: Default::default(), - }; - - db.save_workspace(workspace_without_breakpoint.clone()) - .await; - - let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap(); - let empty_breakpoints = loaded_after_remove - .breakpoints - .get(&Arc::from(singular_path)); - - assert!(empty_breakpoints.is_none()); - } - - #[gpui::test] - async fn test_next_id_stability() { - zlog::init_test(); - - let db = WorkspaceDb::open_test_db("test_next_id_stability").await; - - db.write(|conn| { - conn.migrate( - "test_table", - &[sql!( - CREATE TABLE test_table( - text TEXT, - workspace_id INTEGER, - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ) STRICT; - )], - |_, _, _| false, - ) - .unwrap(); - }) - .await; - - let id = db.next_id().await.unwrap(); - // Assert the empty row got inserted - assert_eq!( - Some(id), - db.select_row_bound::(sql!( - SELECT workspace_id FROM workspaces WHERE workspace_id = ? - )) - .unwrap()(id) - .unwrap() - ); - - db.write(move |conn| { - conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?))) - .unwrap()(("test-text-1", id)) - .unwrap() - }) - .await; - - let test_text_1 = db - .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?)) - .unwrap()(1) - .unwrap() - .unwrap(); - assert_eq!(test_text_1, "test-text-1"); - } - - #[gpui::test] - async fn test_workspace_id_stability() { - zlog::init_test(); - - let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await; - - db.write(|conn| { - conn.migrate( - "test_table", - &[sql!( - CREATE TABLE test_table( - text TEXT, - workspace_id INTEGER, - FOREIGN KEY(workspace_id) - REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ) STRICT;)], - |_, _, _| false, - ) - }) - .await - .unwrap(); - - let mut workspace_1 = SerializedWorkspace { - id: WorkspaceId(1), - paths: PathList::new(&["/tmp", "/tmp2"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: Default::default(), - session_id: None, - window_id: None, - user_toolchains: Default::default(), - }; - - let workspace_2 = SerializedWorkspace { - id: WorkspaceId(2), - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: Default::default(), - session_id: None, - window_id: None, - user_toolchains: Default::default(), - }; - - db.save_workspace(workspace_1.clone()).await; - - db.write(|conn| { - conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?))) - .unwrap()(("test-text-1", 1)) - .unwrap(); - }) - .await; - - db.save_workspace(workspace_2.clone()).await; - - db.write(|conn| { - conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?))) - .unwrap()(("test-text-2", 2)) - .unwrap(); - }) - .await; - - workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]); - db.save_workspace(workspace_1.clone()).await; - db.save_workspace(workspace_1).await; - db.save_workspace(workspace_2).await; - - let test_text_2 = db - .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?)) - .unwrap()(2) - .unwrap() - .unwrap(); - assert_eq!(test_text_2, "test-text-2"); - - let test_text_1 = db - .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?)) - .unwrap()(1) - .unwrap() - .unwrap(); - assert_eq!(test_text_1, "test-text-1"); - } - - fn group(axis: Axis, children: Vec) -> SerializedPaneGroup { - SerializedPaneGroup::Group { - axis: SerializedAxis(axis), - flexes: None, - children, - } - } - - #[gpui::test] - async fn test_full_workspace_serialization() { - zlog::init_test(); - - let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await; - - // ----------------- - // | 1,2 | 5,6 | - // | - - - | | - // | 3,4 | | - // ----------------- - let center_group = group( - Axis::Horizontal, - vec![ - group( - Axis::Vertical, - vec![ - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 5, false, false), - SerializedItem::new("Terminal", 6, true, false), - ], - false, - 0, - )), - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 7, true, false), - SerializedItem::new("Terminal", 8, false, false), - ], - false, - 0, - )), - ], - ), - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 9, false, false), - SerializedItem::new("Terminal", 10, true, false), - ], - false, - 0, - )), - ], - ); - - let workspace = SerializedWorkspace { - id: WorkspaceId(5), - paths: PathList::new(&["/tmp", "/tmp2"]), - location: SerializedWorkspaceLocation::Local, - center_group, - window_bounds: Default::default(), - breakpoints: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - session_id: None, - window_id: Some(999), - user_toolchains: Default::default(), - }; - - db.save_workspace(workspace.clone()).await; - - let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]); - assert_eq!(workspace, round_trip_workspace.unwrap()); - - // Test guaranteed duplicate IDs - db.save_workspace(workspace.clone()).await; - db.save_workspace(workspace.clone()).await; - - let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]); - assert_eq!(workspace, round_trip_workspace.unwrap()); - } - - #[gpui::test] - async fn test_workspace_assignment() { - zlog::init_test(); - - let db = WorkspaceDb::open_test_db("test_basic_functionality").await; - - let workspace_1 = SerializedWorkspace { - id: WorkspaceId(1), - paths: PathList::new(&["/tmp", "/tmp2"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - breakpoints: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - session_id: None, - window_id: Some(1), - user_toolchains: Default::default(), - }; - - let mut workspace_2 = SerializedWorkspace { - id: WorkspaceId(2), - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: Default::default(), - session_id: None, - window_id: Some(2), - user_toolchains: Default::default(), - }; - - db.save_workspace(workspace_1.clone()).await; - db.save_workspace(workspace_2.clone()).await; - - // Test that paths are treated as a set - assert_eq!( - db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), - workspace_1 - ); - assert_eq!( - db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(), - workspace_1 - ); - - // Make sure that other keys work - assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2); - assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None); - - // Test 'mutate' case of updating a pre-existing id - workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]); - - db.save_workspace(workspace_2.clone()).await; - assert_eq!( - db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), - workspace_2 - ); - - // Test other mechanism for mutating - let mut workspace_3 = SerializedWorkspace { - id: WorkspaceId(3), - paths: PathList::new(&["/tmp2", "/tmp"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - breakpoints: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - session_id: None, - window_id: Some(3), - user_toolchains: Default::default(), - }; - - db.save_workspace(workspace_3.clone()).await; - assert_eq!( - db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), - workspace_3 - ); - - // Make sure that updating paths differently also works - workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]); - db.save_workspace(workspace_3.clone()).await; - assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None); - assert_eq!( - db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"]) - .unwrap(), - workspace_3 - ); - } - - #[gpui::test] - async fn test_session_workspaces() { - zlog::init_test(); - - let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await; - - let workspace_1 = SerializedWorkspace { - id: WorkspaceId(1), - paths: PathList::new(&["/tmp1"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: Default::default(), - session_id: Some("session-id-1".to_owned()), - window_id: Some(10), - user_toolchains: Default::default(), - }; - - let workspace_2 = SerializedWorkspace { - id: WorkspaceId(2), - paths: PathList::new(&["/tmp2"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: Default::default(), - session_id: Some("session-id-1".to_owned()), - window_id: Some(20), - user_toolchains: Default::default(), - }; - - let workspace_3 = SerializedWorkspace { - id: WorkspaceId(3), - paths: PathList::new(&["/tmp3"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: Default::default(), - session_id: Some("session-id-2".to_owned()), - window_id: Some(30), - user_toolchains: Default::default(), - }; - - let workspace_4 = SerializedWorkspace { - id: WorkspaceId(4), - paths: PathList::new(&["/tmp4"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: Default::default(), - session_id: None, - window_id: None, - user_toolchains: Default::default(), - }; - - let connection_id = db - .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: "my-host".to_string(), - port: Some(1234), - ..Default::default() - })) - .await - .unwrap(); - - let workspace_5 = SerializedWorkspace { - id: WorkspaceId(5), - paths: PathList::default(), - location: SerializedWorkspaceLocation::Remote( - db.remote_connection(connection_id).unwrap(), - ), - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: Default::default(), - session_id: Some("session-id-2".to_owned()), - window_id: Some(50), - user_toolchains: Default::default(), - }; - - let workspace_6 = SerializedWorkspace { - id: WorkspaceId(6), - paths: PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - breakpoints: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - session_id: Some("session-id-3".to_owned()), - window_id: Some(60), - user_toolchains: Default::default(), - }; - - db.save_workspace(workspace_1.clone()).await; - thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment - db.save_workspace(workspace_2.clone()).await; - db.save_workspace(workspace_3.clone()).await; - thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment - db.save_workspace(workspace_4.clone()).await; - db.save_workspace(workspace_5.clone()).await; - db.save_workspace(workspace_6.clone()).await; - - let locations = db.session_workspaces("session-id-1".to_owned()).unwrap(); - assert_eq!(locations.len(), 2); - assert_eq!(locations[0].0, PathList::new(&["/tmp2"])); - assert_eq!(locations[0].1, Some(20)); - assert_eq!(locations[1].0, PathList::new(&["/tmp1"])); - assert_eq!(locations[1].1, Some(10)); - - let locations = db.session_workspaces("session-id-2".to_owned()).unwrap(); - assert_eq!(locations.len(), 2); - assert_eq!(locations[0].0, PathList::default()); - assert_eq!(locations[0].1, Some(50)); - assert_eq!(locations[0].2, Some(connection_id)); - assert_eq!(locations[1].0, PathList::new(&["/tmp3"])); - assert_eq!(locations[1].1, Some(30)); - - let locations = db.session_workspaces("session-id-3".to_owned()).unwrap(); - assert_eq!(locations.len(), 1); - assert_eq!( - locations[0].0, - PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]), - ); - assert_eq!(locations[0].1, Some(60)); - } - - fn default_workspace>( - paths: &[P], - center_group: &SerializedPaneGroup, - ) -> SerializedWorkspace { - SerializedWorkspace { - id: WorkspaceId(4), - paths: PathList::new(paths), - location: SerializedWorkspaceLocation::Local, - center_group: center_group.clone(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - breakpoints: Default::default(), - centered_layout: false, - session_id: None, - window_id: None, - user_toolchains: Default::default(), - } - } - - #[gpui::test] - async fn test_last_session_workspace_locations() { - let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap(); - let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap(); - let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap(); - let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap(); - - let db = - WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await; - - let workspaces = [ - (1, vec![dir1.path()], 9), - (2, vec![dir2.path()], 5), - (3, vec![dir3.path()], 8), - (4, vec![dir4.path()], 2), - (5, vec![dir1.path(), dir2.path(), dir3.path()], 3), - (6, vec![dir4.path(), dir3.path(), dir2.path()], 4), - ] - .into_iter() - .map(|(id, paths, window_id)| SerializedWorkspace { - id: WorkspaceId(id), - paths: PathList::new(paths.as_slice()), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - session_id: Some("one-session".to_owned()), - breakpoints: Default::default(), - window_id: Some(window_id), - user_toolchains: Default::default(), - }) - .collect::>(); - - for workspace in workspaces.iter() { - db.save_workspace(workspace.clone()).await; - } - - let stack = Some(Vec::from([ - WindowId::from(2), // Top - WindowId::from(8), - WindowId::from(5), - WindowId::from(9), - WindowId::from(3), - WindowId::from(4), // Bottom - ])); - - let locations = db - .last_session_workspace_locations("one-session", stack) - .unwrap(); - assert_eq!( - locations, - [ - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir4.path()]) - ), - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir3.path()]) - ), - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir2.path()]) - ), - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir1.path()]) - ), - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir1.path(), dir2.path(), dir3.path()]) - ), - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir4.path(), dir3.path(), dir2.path()]) - ), - ] - ); - } - - #[gpui::test] - async fn test_last_session_workspace_locations_remote() { - let db = - WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote") - .await; - - let remote_connections = [ - ("host-1", "my-user-1"), - ("host-2", "my-user-2"), - ("host-3", "my-user-3"), - ("host-4", "my-user-4"), - ] - .into_iter() - .map(|(host, user)| async { - let options = RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: host.to_string(), - username: Some(user.to_string()), - ..Default::default() - }); - db.get_or_create_remote_connection(options.clone()) - .await - .unwrap(); - options - }) - .collect::>(); - - let remote_connections = futures::future::join_all(remote_connections).await; - - let workspaces = [ - (1, remote_connections[0].clone(), 9), - (2, remote_connections[1].clone(), 5), - (3, remote_connections[2].clone(), 8), - (4, remote_connections[3].clone(), 2), - ] - .into_iter() - .map(|(id, remote_connection, window_id)| SerializedWorkspace { - id: WorkspaceId(id), - paths: PathList::default(), - location: SerializedWorkspaceLocation::Remote(remote_connection), - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - session_id: Some("one-session".to_owned()), - breakpoints: Default::default(), - window_id: Some(window_id), - user_toolchains: Default::default(), - }) - .collect::>(); - - for workspace in workspaces.iter() { - db.save_workspace(workspace.clone()).await; - } - - let stack = Some(Vec::from([ - WindowId::from(2), // Top - WindowId::from(8), - WindowId::from(5), - WindowId::from(9), // Bottom - ])); - - let have = db - .last_session_workspace_locations("one-session", stack) - .unwrap(); - assert_eq!(have.len(), 4); - assert_eq!( - have[0], - ( - SerializedWorkspaceLocation::Remote(remote_connections[3].clone()), - PathList::default() - ) - ); - assert_eq!( - have[1], - ( - SerializedWorkspaceLocation::Remote(remote_connections[2].clone()), - PathList::default() - ) - ); - assert_eq!( - have[2], - ( - SerializedWorkspaceLocation::Remote(remote_connections[1].clone()), - PathList::default() - ) - ); - assert_eq!( - have[3], - ( - SerializedWorkspaceLocation::Remote(remote_connections[0].clone()), - PathList::default() - ) - ); - } - - #[gpui::test] - async fn test_get_or_create_ssh_project() { - let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await; - - let host = "example.com".to_string(); - let port = Some(22_u16); - let user = Some("user".to_string()); - - let connection_id = db - .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: host.clone(), - port, - username: user.clone(), - ..Default::default() - })) - .await - .unwrap(); - - // Test that calling the function again with the same parameters returns the same project - let same_connection = db - .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: host.clone(), - port, - username: user.clone(), - ..Default::default() - })) - .await - .unwrap(); - - assert_eq!(connection_id, same_connection); - - // Test with different parameters - let host2 = "otherexample.com".to_string(); - let port2 = None; - let user2 = Some("otheruser".to_string()); - - let different_connection = db - .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: host2.clone(), - port: port2, - username: user2.clone(), - ..Default::default() - })) - .await - .unwrap(); - - assert_ne!(connection_id, different_connection); - } - - #[gpui::test] - async fn test_get_or_create_ssh_project_with_null_user() { - let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await; - - let (host, port, user) = ("example.com".to_string(), None, None); - - let connection_id = db - .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: host.clone(), - port, - username: None, - ..Default::default() - })) - .await - .unwrap(); - - let same_connection_id = db - .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: host.clone(), - port, - username: user.clone(), - ..Default::default() - })) - .await - .unwrap(); - - assert_eq!(connection_id, same_connection_id); - } - - #[gpui::test] - async fn test_get_remote_connections() { - let db = WorkspaceDb::open_test_db("test_get_remote_connections").await; - - let connections = [ - ("example.com".to_string(), None, None), - ( - "anotherexample.com".to_string(), - Some(123_u16), - Some("user2".to_string()), - ), - ("yetanother.com".to_string(), Some(345_u16), None), - ]; - - let mut ids = Vec::new(); - for (host, port, user) in connections.iter() { - ids.push( - db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh( - SshConnectionOptions { - host: host.clone(), - port: *port, - username: user.clone(), - ..Default::default() - }, - )) - .await - .unwrap(), - ); - } - - let stored_connections = db.remote_connections().unwrap(); - assert_eq!( - stored_connections, - [ - ( - ids[0], - RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: "example.com".into(), - port: None, - username: None, - ..Default::default() - }), - ), - ( - ids[1], - RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: "anotherexample.com".into(), - port: Some(123), - username: Some("user2".into()), - ..Default::default() - }), - ), - ( - ids[2], - RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: "yetanother.com".into(), - port: Some(345), - username: None, - ..Default::default() - }), - ), - ] - .into_iter() - .collect::>(), - ); - } - - #[gpui::test] - async fn test_simple_split() { - zlog::init_test(); - - let db = WorkspaceDb::open_test_db("simple_split").await; - - // ----------------- - // | 1,2 | 5,6 | - // | - - - | | - // | 3,4 | | - // ----------------- - let center_pane = group( - Axis::Horizontal, - vec![ - group( - Axis::Vertical, - vec![ - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 1, false, false), - SerializedItem::new("Terminal", 2, true, false), - ], - false, - 0, - )), - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 4, false, false), - SerializedItem::new("Terminal", 3, true, false), - ], - true, - 0, - )), - ], - ), - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 5, true, false), - SerializedItem::new("Terminal", 6, false, false), - ], - false, - 0, - )), - ], - ); - - let workspace = default_workspace(&["/tmp"], ¢er_pane); - - db.save_workspace(workspace.clone()).await; - - let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap(); - - assert_eq!(workspace.center_group, new_workspace.center_group); - } - - #[gpui::test] - async fn test_cleanup_panes() { - zlog::init_test(); - - let db = WorkspaceDb::open_test_db("test_cleanup_panes").await; - - let center_pane = group( - Axis::Horizontal, - vec![ - group( - Axis::Vertical, - vec![ - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 1, false, false), - SerializedItem::new("Terminal", 2, true, false), - ], - false, - 0, - )), - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 4, false, false), - SerializedItem::new("Terminal", 3, true, false), - ], - true, - 0, - )), - ], - ), - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 5, false, false), - SerializedItem::new("Terminal", 6, true, false), - ], - false, - 0, - )), - ], - ); - - let id = &["/tmp"]; - - let mut workspace = default_workspace(id, ¢er_pane); - - db.save_workspace(workspace.clone()).await; - - workspace.center_group = group( - Axis::Vertical, - vec![ - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 1, false, false), - SerializedItem::new("Terminal", 2, true, false), - ], - false, - 0, - )), - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 4, true, false), - SerializedItem::new("Terminal", 3, false, false), - ], - true, - 0, - )), - ], - ); - - db.save_workspace(workspace.clone()).await; - - let new_workspace = db.workspace_for_roots(id).unwrap(); - - assert_eq!(workspace.center_group, new_workspace.center_group); - } -} diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs deleted file mode 100644 index 08a3adf9eb..0000000000 --- a/crates/workspace/src/persistence/model.rs +++ /dev/null @@ -1,399 +0,0 @@ -use super::{SerializedAxis, SerializedWindowBounds}; -use crate::{ - Member, Pane, PaneAxis, SerializableItemRegistry, Workspace, WorkspaceId, item::ItemHandle, - path_list::PathList, -}; -use anyhow::{Context, Result}; -use async_recursion::async_recursion; -use collections::IndexSet; -use db::sqlez::{ - bindable::{Bind, Column, StaticColumnCount}, - statement::Statement, -}; -use gpui::{AsyncWindowContext, Entity, WeakEntity}; - -use language::{Toolchain, ToolchainScope}; -use project::{Project, debugger::breakpoint_store::SourceBreakpoint}; -use remote::RemoteConnectionOptions; -use std::{ - collections::BTreeMap, - path::{Path, PathBuf}, - sync::Arc, -}; -use util::ResultExt; -use uuid::Uuid; - -#[derive( - Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, -)] -pub(crate) struct RemoteConnectionId(pub u64); - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub(crate) enum RemoteConnectionKind { - Ssh, - Wsl, - Docker, -} - -#[derive(Debug, PartialEq, Clone)] -pub enum SerializedWorkspaceLocation { - Local, - Remote(RemoteConnectionOptions), -} - -impl SerializedWorkspaceLocation { - /// Get sorted paths - pub fn sorted_paths(&self) -> Arc> { - unimplemented!() - } -} - -#[derive(Debug, PartialEq, Clone)] -pub(crate) struct SerializedWorkspace { - pub(crate) id: WorkspaceId, - pub(crate) location: SerializedWorkspaceLocation, - pub(crate) paths: PathList, - pub(crate) center_group: SerializedPaneGroup, - pub(crate) window_bounds: Option, - pub(crate) centered_layout: bool, - pub(crate) display: Option, - pub(crate) docks: DockStructure, - pub(crate) session_id: Option, - pub(crate) breakpoints: BTreeMap, Vec>, - pub(crate) user_toolchains: BTreeMap>, - pub(crate) window_id: Option, -} - -#[derive(Debug, PartialEq, Clone, Default)] -pub struct DockStructure { - pub(crate) left: DockData, - pub(crate) right: DockData, - pub(crate) bottom: DockData, -} - -impl RemoteConnectionKind { - pub(crate) fn serialize(&self) -> &'static str { - match self { - RemoteConnectionKind::Ssh => "ssh", - RemoteConnectionKind::Wsl => "wsl", - RemoteConnectionKind::Docker => "docker", - } - } - - pub(crate) fn deserialize(text: &str) -> Option { - match text { - "ssh" => Some(Self::Ssh), - "wsl" => Some(Self::Wsl), - "docker" => Some(Self::Docker), - _ => None, - } - } -} - -impl Column for DockStructure { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let (left, next_index) = DockData::column(statement, start_index)?; - let (right, next_index) = DockData::column(statement, next_index)?; - let (bottom, next_index) = DockData::column(statement, next_index)?; - Ok(( - DockStructure { - left, - right, - bottom, - }, - next_index, - )) - } -} - -impl Bind for DockStructure { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let next_index = statement.bind(&self.left, start_index)?; - let next_index = statement.bind(&self.right, next_index)?; - statement.bind(&self.bottom, next_index) - } -} - -#[derive(Debug, PartialEq, Clone, Default)] -pub struct DockData { - pub(crate) visible: bool, - pub(crate) active_panel: Option, - pub(crate) zoom: bool, -} - -impl Column for DockData { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let (visible, next_index) = Option::::column(statement, start_index)?; - let (active_panel, next_index) = Option::::column(statement, next_index)?; - let (zoom, next_index) = Option::::column(statement, next_index)?; - Ok(( - DockData { - visible: visible.unwrap_or(false), - active_panel, - zoom: zoom.unwrap_or(false), - }, - next_index, - )) - } -} - -impl Bind for DockData { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let next_index = statement.bind(&self.visible, start_index)?; - let next_index = statement.bind(&self.active_panel, next_index)?; - statement.bind(&self.zoom, next_index) - } -} - -#[derive(Debug, PartialEq, Clone)] -pub(crate) enum SerializedPaneGroup { - Group { - axis: SerializedAxis, - flexes: Option>, - children: Vec, - }, - Pane(SerializedPane), -} - -#[cfg(test)] -impl Default for SerializedPaneGroup { - fn default() -> Self { - Self::Pane(SerializedPane { - children: vec![SerializedItem::default()], - active: false, - pinned_count: 0, - }) - } -} - -impl SerializedPaneGroup { - #[async_recursion(?Send)] - pub(crate) async fn deserialize( - self, - project: &Entity, - workspace_id: WorkspaceId, - workspace: WeakEntity, - cx: &mut AsyncWindowContext, - ) -> Option<( - Member, - Option>, - Vec>>, - )> { - match self { - SerializedPaneGroup::Group { - axis, - children, - flexes, - } => { - let mut current_active_pane = None; - let mut members = Vec::new(); - let mut items = Vec::new(); - for child in children { - if let Some((new_member, active_pane, new_items)) = child - .deserialize(project, workspace_id, workspace.clone(), cx) - .await - { - members.push(new_member); - items.extend(new_items); - current_active_pane = current_active_pane.or(active_pane); - } - } - - if members.is_empty() { - return None; - } - - if members.len() == 1 { - return Some((members.remove(0), current_active_pane, items)); - } - - Some(( - Member::Axis(PaneAxis::load(axis.0, members, flexes)), - current_active_pane, - items, - )) - } - SerializedPaneGroup::Pane(serialized_pane) => { - let pane = workspace - .update_in(cx, |workspace, window, cx| { - workspace.add_pane(window, cx).downgrade() - }) - .log_err()?; - let active = serialized_pane.active; - let new_items = serialized_pane - .deserialize_to(project, &pane, workspace_id, workspace.clone(), cx) - .await - .context("Could not deserialize pane)") - .log_err()?; - - if pane - .read_with(cx, |pane, _| pane.items_len() != 0) - .log_err()? - { - let pane = pane.upgrade()?; - Some(( - Member::Pane(pane.clone()), - active.then_some(pane), - new_items, - )) - } else { - let pane = pane.upgrade()?; - workspace - .update_in(cx, |workspace, window, cx| { - workspace.force_remove_pane(&pane, &None, window, cx) - }) - .log_err()?; - None - } - } - } - } -} - -#[derive(Debug, PartialEq, Eq, Default, Clone)] -pub struct SerializedPane { - pub(crate) active: bool, - pub(crate) children: Vec, - pub(crate) pinned_count: usize, -} - -impl SerializedPane { - pub fn new(children: Vec, active: bool, pinned_count: usize) -> Self { - SerializedPane { - children, - active, - pinned_count, - } - } - - pub async fn deserialize_to( - &self, - project: &Entity, - pane: &WeakEntity, - workspace_id: WorkspaceId, - workspace: WeakEntity, - cx: &mut AsyncWindowContext, - ) -> Result>>> { - let mut item_tasks = Vec::new(); - let mut active_item_index = None; - let mut preview_item_index = None; - for (index, item) in self.children.iter().enumerate() { - let project = project.clone(); - item_tasks.push(pane.update_in(cx, |_, window, cx| { - SerializableItemRegistry::deserialize( - &item.kind, - project, - workspace.clone(), - workspace_id, - item.item_id, - window, - cx, - ) - })?); - if item.active { - active_item_index = Some(index); - } - if item.preview { - preview_item_index = Some(index); - } - } - - let mut items = Vec::new(); - for item_handle in futures::future::join_all(item_tasks).await { - let item_handle = item_handle.log_err(); - items.push(item_handle.clone()); - - if let Some(item_handle) = item_handle { - pane.update_in(cx, |pane, window, cx| { - pane.add_item(item_handle.clone(), true, true, None, window, cx); - })?; - } - } - - if let Some(active_item_index) = active_item_index { - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(active_item_index, false, false, window, cx); - })?; - } - - if let Some(preview_item_index) = preview_item_index { - pane.update(cx, |pane, cx| { - if let Some(item) = pane.item_for_index(preview_item_index) { - pane.set_preview_item_id(Some(item.item_id()), cx); - } - })?; - } - pane.update(cx, |pane, _| { - pane.set_pinned_count(self.pinned_count.min(items.len())); - })?; - - anyhow::Ok(items) - } -} - -pub type GroupId = i64; -pub type PaneId = i64; -pub type ItemId = u64; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct SerializedItem { - pub kind: Arc, - pub item_id: ItemId, - pub active: bool, - pub preview: bool, -} - -impl SerializedItem { - pub fn new(kind: impl AsRef, item_id: ItemId, active: bool, preview: bool) -> Self { - Self { - kind: Arc::from(kind.as_ref()), - item_id, - active, - preview, - } - } -} - -#[cfg(test)] -impl Default for SerializedItem { - fn default() -> Self { - SerializedItem { - kind: Arc::from("Terminal"), - item_id: 100000, - active: false, - preview: false, - } - } -} - -impl StaticColumnCount for SerializedItem { - fn column_count() -> usize { - 4 - } -} -impl Bind for &SerializedItem { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let next_index = statement.bind(&self.kind, start_index)?; - let next_index = statement.bind(&self.item_id, next_index)?; - let next_index = statement.bind(&self.active, next_index)?; - statement.bind(&self.preview, next_index) - } -} - -impl Column for SerializedItem { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let (kind, next_index) = Arc::::column(statement, start_index)?; - let (item_id, next_index) = ItemId::column(statement, next_index)?; - let (active, next_index) = bool::column(statement, next_index)?; - let (preview, next_index) = bool::column(statement, next_index)?; - Ok(( - SerializedItem { - kind, - item_id, - active, - preview, - }, - next_index, - )) - } -} diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs deleted file mode 100644 index badfe7d243..0000000000 --- a/crates/workspace/src/searchable.rs +++ /dev/null @@ -1,461 +0,0 @@ -use std::{any::Any, sync::Arc}; - -use any_vec::AnyVec; -use gpui::{ - AnyView, AnyWeakEntity, App, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, - Window, -}; -use project::search::SearchQuery; - -use crate::{ - ItemHandle, - item::{Item, WeakItemHandle}, -}; - -#[derive(Clone, Debug)] -pub enum SearchEvent { - MatchesInvalidated, - ActiveMatchChanged, -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] -pub enum Direction { - Prev, - #[default] - Next, -} - -impl Direction { - pub fn opposite(&self) -> Self { - match self { - Direction::Prev => Direction::Next, - Direction::Next => Direction::Prev, - } - } -} - -#[derive(Clone, Copy, Debug, Default)] -pub struct SearchOptions { - pub case: bool, - pub word: bool, - pub regex: bool, - /// Specifies whether the supports search & replace. - pub replacement: bool, - pub selection: bool, - pub find_in_results: bool, -} - -// Whether to always select the current selection (even if empty) -// or to use the default (restoring the previous search ranges if some, -// otherwise using the whole file). -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub enum FilteredSearchRange { - Selection, - #[default] - Default, -} - -pub trait SearchableItem: Item + EventEmitter { - type Match: Any + Sync + Send + Clone; - - fn supported_options(&self) -> SearchOptions { - SearchOptions { - case: true, - word: true, - regex: true, - replacement: true, - selection: true, - find_in_results: false, - } - } - - fn search_bar_visibility_changed( - &mut self, - _visible: bool, - _window: &mut Window, - _cx: &mut Context, - ) { - } - - fn has_filtered_search_ranges(&mut self) -> bool { - self.supported_options().selection - } - - fn toggle_filtered_search_ranges( - &mut self, - _enabled: Option, - _window: &mut Window, - _cx: &mut Context, - ) { - } - - fn get_matches(&self, _window: &mut Window, _: &mut App) -> Vec { - Vec::new() - } - fn clear_matches(&mut self, window: &mut Window, cx: &mut Context); - fn update_matches( - &mut self, - matches: &[Self::Match], - active_match_index: Option, - window: &mut Window, - cx: &mut Context, - ); - fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context) -> String; - fn activate_match( - &mut self, - index: usize, - matches: &[Self::Match], - window: &mut Window, - cx: &mut Context, - ); - fn select_matches( - &mut self, - matches: &[Self::Match], - window: &mut Window, - cx: &mut Context, - ); - fn replace( - &mut self, - _: &Self::Match, - _: &SearchQuery, - _window: &mut Window, - _: &mut Context, - ); - fn replace_all( - &mut self, - matches: &mut dyn Iterator, - query: &SearchQuery, - window: &mut Window, - cx: &mut Context, - ) { - for item in matches { - self.replace(item, query, window, cx); - } - } - fn match_index_for_direction( - &mut self, - matches: &[Self::Match], - current_index: usize, - direction: Direction, - count: usize, - _window: &mut Window, - _: &mut Context, - ) -> usize { - match direction { - Direction::Prev => { - let count = count % matches.len(); - if current_index >= count { - current_index - count - } else { - matches.len() - (count - current_index) - } - } - Direction::Next => (current_index + count) % matches.len(), - } - } - fn find_matches( - &mut self, - query: Arc, - window: &mut Window, - cx: &mut Context, - ) -> Task>; - fn active_match_index( - &mut self, - direction: Direction, - matches: &[Self::Match], - window: &mut Window, - cx: &mut Context, - ) -> Option; - fn set_search_is_case_sensitive(&mut self, _: Option, _: &mut Context) {} -} - -pub trait SearchableItemHandle: ItemHandle { - fn downgrade(&self) -> Box; - fn boxed_clone(&self) -> Box; - fn supported_options(&self, cx: &App) -> SearchOptions; - fn subscribe_to_search_events( - &self, - window: &mut Window, - cx: &mut App, - handler: Box, - ) -> Subscription; - fn clear_matches(&self, window: &mut Window, cx: &mut App); - fn update_matches( - &self, - matches: &AnyVec, - active_match_index: Option, - window: &mut Window, - cx: &mut App, - ); - fn query_suggestion(&self, window: &mut Window, cx: &mut App) -> String; - fn activate_match( - &self, - index: usize, - matches: &AnyVec, - window: &mut Window, - cx: &mut App, - ); - fn select_matches(&self, matches: &AnyVec, window: &mut Window, cx: &mut App); - fn replace( - &self, - _: any_vec::element::ElementRef<'_, dyn Send>, - _: &SearchQuery, - _window: &mut Window, - _: &mut App, - ); - fn replace_all( - &self, - matches: &mut dyn Iterator>, - query: &SearchQuery, - window: &mut Window, - cx: &mut App, - ); - fn match_index_for_direction( - &self, - matches: &AnyVec, - current_index: usize, - direction: Direction, - count: usize, - window: &mut Window, - cx: &mut App, - ) -> usize; - fn find_matches( - &self, - query: Arc, - window: &mut Window, - cx: &mut App, - ) -> Task>; - fn active_match_index( - &self, - direction: Direction, - matches: &AnyVec, - window: &mut Window, - cx: &mut App, - ) -> Option; - fn search_bar_visibility_changed(&self, visible: bool, window: &mut Window, cx: &mut App); - - fn toggle_filtered_search_ranges( - &mut self, - enabled: Option, - window: &mut Window, - cx: &mut App, - ); - - fn set_search_is_case_sensitive(&self, is_case_sensitive: Option, cx: &mut App); -} - -impl SearchableItemHandle for Entity { - fn downgrade(&self) -> Box { - Box::new(self.downgrade()) - } - - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn supported_options(&self, cx: &App) -> SearchOptions { - self.read(cx).supported_options() - } - - fn subscribe_to_search_events( - &self, - window: &mut Window, - cx: &mut App, - handler: Box, - ) -> Subscription { - window.subscribe(self, cx, move |_, event: &SearchEvent, window, cx| { - handler(event, window, cx) - }) - } - - fn clear_matches(&self, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| this.clear_matches(window, cx)); - } - fn update_matches( - &self, - matches: &AnyVec, - active_match_index: Option, - window: &mut Window, - cx: &mut App, - ) { - let matches = matches.downcast_ref().unwrap(); - self.update(cx, |this, cx| { - this.update_matches(matches.as_slice(), active_match_index, window, cx) - }); - } - fn query_suggestion(&self, window: &mut Window, cx: &mut App) -> String { - self.update(cx, |this, cx| this.query_suggestion(window, cx)) - } - fn activate_match( - &self, - index: usize, - matches: &AnyVec, - window: &mut Window, - cx: &mut App, - ) { - let matches = matches.downcast_ref().unwrap(); - self.update(cx, |this, cx| { - this.activate_match(index, matches.as_slice(), window, cx) - }); - } - - fn select_matches(&self, matches: &AnyVec, window: &mut Window, cx: &mut App) { - let matches = matches.downcast_ref().unwrap(); - self.update(cx, |this, cx| { - this.select_matches(matches.as_slice(), window, cx) - }); - } - - fn match_index_for_direction( - &self, - matches: &AnyVec, - current_index: usize, - direction: Direction, - count: usize, - window: &mut Window, - cx: &mut App, - ) -> usize { - let matches = matches.downcast_ref().unwrap(); - self.update(cx, |this, cx| { - this.match_index_for_direction( - matches.as_slice(), - current_index, - direction, - count, - window, - cx, - ) - }) - } - fn find_matches( - &self, - query: Arc, - window: &mut Window, - cx: &mut App, - ) -> Task> { - let matches = self.update(cx, |this, cx| this.find_matches(query, window, cx)); - window.spawn(cx, async |_| { - let matches = matches.await; - let mut any_matches = AnyVec::with_capacity::(matches.len()); - { - let mut any_matches = any_matches.downcast_mut::().unwrap(); - for mat in matches { - any_matches.push(mat); - } - } - any_matches - }) - } - fn active_match_index( - &self, - direction: Direction, - matches: &AnyVec, - window: &mut Window, - cx: &mut App, - ) -> Option { - let matches = matches.downcast_ref()?; - self.update(cx, |this, cx| { - this.active_match_index(direction, matches.as_slice(), window, cx) - }) - } - - fn replace( - &self, - mat: any_vec::element::ElementRef<'_, dyn Send>, - query: &SearchQuery, - window: &mut Window, - cx: &mut App, - ) { - let mat = mat.downcast_ref().unwrap(); - self.update(cx, |this, cx| this.replace(mat, query, window, cx)) - } - - fn replace_all( - &self, - matches: &mut dyn Iterator>, - query: &SearchQuery, - window: &mut Window, - cx: &mut App, - ) { - self.update(cx, |this, cx| { - this.replace_all( - &mut matches.map(|m| m.downcast_ref().unwrap()), - query, - window, - cx, - ); - }) - } - - fn search_bar_visibility_changed(&self, visible: bool, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| { - this.search_bar_visibility_changed(visible, window, cx) - }); - } - - fn toggle_filtered_search_ranges( - &mut self, - enabled: Option, - window: &mut Window, - cx: &mut App, - ) { - self.update(cx, |this, cx| { - this.toggle_filtered_search_ranges(enabled, window, cx) - }); - } - fn set_search_is_case_sensitive(&self, enabled: Option, cx: &mut App) { - self.update(cx, |this, cx| { - this.set_search_is_case_sensitive(enabled, cx) - }); - } -} - -impl From> for AnyView { - fn from(this: Box) -> Self { - this.to_any_view() - } -} - -impl From<&Box> for AnyView { - fn from(this: &Box) -> Self { - this.to_any_view() - } -} - -impl PartialEq for Box { - fn eq(&self, other: &Self) -> bool { - self.item_id() == other.item_id() - } -} - -impl Eq for Box {} - -pub trait WeakSearchableItemHandle: WeakItemHandle { - fn upgrade(&self, cx: &App) -> Option>; - - fn into_any(self) -> AnyWeakEntity; -} - -impl WeakSearchableItemHandle for WeakEntity { - fn upgrade(&self, _cx: &App) -> Option> { - Some(Box::new(self.upgrade()?)) - } - - fn into_any(self) -> AnyWeakEntity { - self.into() - } -} - -impl PartialEq for Box { - fn eq(&self, other: &Self) -> bool { - self.id() == other.id() - } -} - -impl Eq for Box {} - -impl std::hash::Hash for Box { - fn hash(&self, state: &mut H) { - self.id().hash(state) - } -} diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs deleted file mode 100644 index 3c009f613e..0000000000 --- a/crates/workspace/src/shared_screen.rs +++ /dev/null @@ -1,136 +0,0 @@ -use crate::{ - ItemNavHistory, WorkspaceId, - item::{Item, ItemEvent}, -}; -use call::{RemoteVideoTrack, RemoteVideoTrackView, Room}; -use client::{User, proto::PeerId}; -use gpui::{ - AppContext as _, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, - ParentElement, Render, SharedString, Styled, Task, div, -}; -use std::sync::Arc; -use ui::{Icon, IconName, prelude::*}; - -pub enum Event { - Close, -} - -pub struct SharedScreen { - pub peer_id: PeerId, - user: Arc, - nav_history: Option, - view: Entity, - focus: FocusHandle, -} - -impl SharedScreen { - pub fn new( - track: RemoteVideoTrack, - peer_id: PeerId, - user: Arc, - room: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let my_sid = track.sid(); - cx.subscribe(&room, move |_, _, ev, cx| { - if let call::room::Event::RemoteVideoTrackUnsubscribed { sid } = ev - && sid == &my_sid - { - cx.emit(Event::Close) - } - }) - .detach(); - - let view = cx.new(|cx| RemoteVideoTrackView::new(track.clone(), window, cx)); - cx.subscribe(&view, |_, _, ev, cx| match ev { - call::RemoteVideoTrackViewEvent::Close => cx.emit(Event::Close), - }) - .detach(); - Self { - view, - peer_id, - user, - nav_history: Default::default(), - focus: cx.focus_handle(), - } - } -} - -impl EventEmitter for SharedScreen {} - -impl Focusable for SharedScreen { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus.clone() - } -} -impl Render for SharedScreen { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .bg(cx.theme().colors().editor_background) - .track_focus(&self.focus) - .key_context("SharedScreen") - .size_full() - .child(self.view.clone()) - } -} - -impl Item for SharedScreen { - type Event = Event; - - fn tab_tooltip_text(&self, _: &App) -> Option { - Some(format!("{}'s screen", self.user.github_login).into()) - } - - fn deactivated(&mut self, _window: &mut Window, cx: &mut Context) { - if let Some(nav_history) = self.nav_history.as_mut() { - nav_history.push::<()>(None, cx); - } - } - - fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { - Some(Icon::new(IconName::Screen)) - } - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - format!("{}'s screen", self.user.github_login).into() - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - None - } - - fn set_nav_history( - &mut self, - history: ItemNavHistory, - _window: &mut Window, - _cx: &mut Context, - ) { - self.nav_history = Some(history); - } - - fn can_split(&self) -> bool { - true - } - - fn clone_on_split( - &self, - _workspace_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - Task::ready(Some(cx.new(|cx| Self { - view: self.view.update(cx, |view, cx| view.clone(window, cx)), - peer_id: self.peer_id, - user: self.user.clone(), - nav_history: Default::default(), - focus: cx.focus_handle(), - }))) - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { - match event { - Event::Close => f(ItemEvent::CloseItem), - } - } -} diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs deleted file mode 100644 index 9087cbba42..0000000000 --- a/crates/workspace/src/status_bar.rs +++ /dev/null @@ -1,223 +0,0 @@ -use crate::{ItemHandle, Pane}; -use gpui::{ - AnyView, App, Context, Decorations, Entity, IntoElement, ParentElement, Render, Styled, - Subscription, Window, -}; -use std::any::TypeId; -use theme::CLIENT_SIDE_DECORATION_ROUNDING; -use ui::{h_flex, prelude::*}; -use util::ResultExt; - -pub trait StatusItemView: Render { - /// Event callback that is triggered when the active pane item changes. - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn crate::ItemHandle>, - window: &mut Window, - cx: &mut Context, - ); -} - -trait StatusItemViewHandle: Send { - fn to_any(&self) -> AnyView; - fn set_active_pane_item( - &self, - active_pane_item: Option<&dyn ItemHandle>, - window: &mut Window, - cx: &mut App, - ); - fn item_type(&self) -> TypeId; -} - -pub struct StatusBar { - left_items: Vec>, - right_items: Vec>, - active_pane: Entity, - _observe_active_pane: Subscription, -} - -impl Render for StatusBar { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - h_flex() - .w_full() - .justify_between() - .gap(DynamicSpacing::Base08.rems(cx)) - .py(DynamicSpacing::Base04.rems(cx)) - .px(DynamicSpacing::Base06.rems(cx)) - .bg(cx.theme().colors().status_bar_background) - .map(|el| match window.window_decorations() { - Decorations::Server => el, - Decorations::Client { tiling, .. } => el - .when(!(tiling.bottom || tiling.right), |el| { - el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.left), |el| { - el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING) - }) - // This border is to avoid a transparent gap in the rounded corners - .mb(px(-1.)) - .border_b(px(1.0)) - .border_color(cx.theme().colors().status_bar_background), - }) - .child(self.render_left_tools()) - .child(self.render_right_tools()) - } -} - -impl StatusBar { - fn render_left_tools(&self) -> impl IntoElement { - h_flex() - .gap_1() - .overflow_x_hidden() - .children(self.left_items.iter().map(|item| item.to_any())) - } - - fn render_right_tools(&self) -> impl IntoElement { - h_flex() - .gap_1() - .overflow_x_hidden() - .children(self.right_items.iter().rev().map(|item| item.to_any())) - } -} - -impl StatusBar { - pub fn new(active_pane: &Entity, window: &mut Window, cx: &mut Context) -> Self { - let mut this = Self { - left_items: Default::default(), - right_items: Default::default(), - active_pane: active_pane.clone(), - _observe_active_pane: cx.observe_in(active_pane, window, |this, _, window, cx| { - this.update_active_pane_item(window, cx) - }), - }; - this.update_active_pane_item(window, cx); - this - } - - pub fn add_left_item(&mut self, item: Entity, window: &mut Window, cx: &mut Context) - where - T: 'static + StatusItemView, - { - let active_pane_item = self.active_pane.read(cx).active_item(); - item.set_active_pane_item(active_pane_item.as_deref(), window, cx); - - self.left_items.push(Box::new(item)); - cx.notify(); - } - - pub fn item_of_type(&self) -> Option> { - self.left_items - .iter() - .chain(self.right_items.iter()) - .find_map(|item| item.to_any().downcast().log_err()) - } - - pub fn position_of_item(&self) -> Option - where - T: StatusItemView, - { - for (index, item) in self.left_items.iter().enumerate() { - if item.item_type() == TypeId::of::() { - return Some(index); - } - } - for (index, item) in self.right_items.iter().enumerate() { - if item.item_type() == TypeId::of::() { - return Some(index + self.left_items.len()); - } - } - None - } - - pub fn insert_item_after( - &mut self, - position: usize, - item: Entity, - window: &mut Window, - cx: &mut Context, - ) where - T: 'static + StatusItemView, - { - let active_pane_item = self.active_pane.read(cx).active_item(); - item.set_active_pane_item(active_pane_item.as_deref(), window, cx); - - if position < self.left_items.len() { - self.left_items.insert(position + 1, Box::new(item)) - } else { - self.right_items - .insert(position + 1 - self.left_items.len(), Box::new(item)) - } - cx.notify() - } - - pub fn remove_item_at(&mut self, position: usize, cx: &mut Context) { - if position < self.left_items.len() { - self.left_items.remove(position); - } else { - self.right_items.remove(position - self.left_items.len()); - } - cx.notify(); - } - - pub fn add_right_item( - &mut self, - item: Entity, - window: &mut Window, - cx: &mut Context, - ) where - T: 'static + StatusItemView, - { - let active_pane_item = self.active_pane.read(cx).active_item(); - item.set_active_pane_item(active_pane_item.as_deref(), window, cx); - - self.right_items.push(Box::new(item)); - cx.notify(); - } - - pub fn set_active_pane( - &mut self, - active_pane: &Entity, - window: &mut Window, - cx: &mut Context, - ) { - self.active_pane = active_pane.clone(); - self._observe_active_pane = cx.observe_in(active_pane, window, |this, _, window, cx| { - this.update_active_pane_item(window, cx) - }); - self.update_active_pane_item(window, cx); - } - - fn update_active_pane_item(&mut self, window: &mut Window, cx: &mut Context) { - let active_pane_item = self.active_pane.read(cx).active_item(); - for item in self.left_items.iter().chain(&self.right_items) { - item.set_active_pane_item(active_pane_item.as_deref(), window, cx); - } - } -} - -impl StatusItemViewHandle for Entity { - fn to_any(&self) -> AnyView { - self.clone().into() - } - - fn set_active_pane_item( - &self, - active_pane_item: Option<&dyn ItemHandle>, - window: &mut Window, - cx: &mut App, - ) { - self.update(cx, |this, cx| { - this.set_active_pane_item(active_pane_item, window, cx) - }); - } - - fn item_type(&self) -> TypeId { - TypeId::of::() - } -} - -impl From<&dyn StatusItemViewHandle> for AnyView { - fn from(val: &dyn StatusItemViewHandle) -> Self { - val.to_any() - } -} diff --git a/crates/workspace/src/tasks.rs b/crates/workspace/src/tasks.rs deleted file mode 100644 index 5f52cb49e7..0000000000 --- a/crates/workspace/src/tasks.rs +++ /dev/null @@ -1,134 +0,0 @@ -use std::process::ExitStatus; - -use anyhow::Result; -use gpui::{AppContext, Context, Entity, Task}; -use language::Buffer; -use project::{TaskSourceKind, WorktreeId}; -use remote::ConnectionState; -use task::{DebugScenario, ResolvedTask, SpawnInTerminal, TaskContext, TaskTemplate}; -use ui::Window; - -use crate::{Toast, Workspace, notifications::NotificationId}; - -impl Workspace { - pub fn schedule_task( - self: &mut Workspace, - task_source_kind: TaskSourceKind, - task_to_resolve: &TaskTemplate, - task_cx: &TaskContext, - omit_history: bool, - window: &mut Window, - cx: &mut Context, - ) { - match self.project.read(cx).remote_connection_state(cx) { - None | Some(ConnectionState::Connected) => {} - Some( - ConnectionState::Connecting - | ConnectionState::Disconnected - | ConnectionState::HeartbeatMissed - | ConnectionState::Reconnecting, - ) => { - log::warn!("Cannot schedule tasks when disconnected from a remote host"); - return; - } - } - - if let Some(spawn_in_terminal) = - task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_cx) - { - self.schedule_resolved_task( - task_source_kind, - spawn_in_terminal, - omit_history, - window, - cx, - ); - } - } - - pub fn schedule_resolved_task( - self: &mut Workspace, - task_source_kind: TaskSourceKind, - resolved_task: ResolvedTask, - omit_history: bool, - window: &mut Window, - cx: &mut Context, - ) { - let spawn_in_terminal = resolved_task.resolved.clone(); - if !omit_history { - if let Some(debugger_provider) = self.debugger_provider.as_ref() { - debugger_provider.task_scheduled(cx); - } - - self.project().update(cx, |project, cx| { - if let Some(task_inventory) = - project.task_store().read(cx).task_inventory().cloned() - { - task_inventory.update(cx, |inventory, _| { - inventory.task_scheduled(task_source_kind, resolved_task); - }) - } - }); - } - - if let Some(terminal_provider) = self.terminal_provider.as_ref() { - let task_status = terminal_provider.spawn(spawn_in_terminal, window, cx); - - let task = cx.spawn(async |w, cx| { - let res = cx.background_spawn(task_status).await; - match res { - Some(Ok(status)) => { - if status.success() { - log::debug!("Task spawn succeeded"); - } else { - log::debug!("Task spawn failed, code: {:?}", status.code()); - } - } - Some(Err(e)) => { - log::error!("Task spawn failed: {e:#}"); - _ = w.update(cx, |w, cx| { - let id = NotificationId::unique::(); - w.show_toast(Toast::new(id, format!("Task spawn failed: {e}")), cx); - }) - } - None => log::debug!("Task spawn got cancelled"), - }; - }); - self.scheduled_tasks.push(task); - } - } - - pub fn start_debug_session( - &mut self, - scenario: DebugScenario, - task_context: TaskContext, - active_buffer: Option>, - worktree_id: Option, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(provider) = self.debugger_provider.as_mut() { - provider.start_session( - scenario, - task_context, - active_buffer, - worktree_id, - window, - cx, - ) - } - } - - pub fn spawn_in_terminal( - self: &mut Workspace, - spawn_in_terminal: SpawnInTerminal, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - if let Some(terminal_provider) = self.terminal_provider.as_ref() { - terminal_provider.spawn(spawn_in_terminal, window, cx) - } else { - Task::ready(None) - } - } -} diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs deleted file mode 100644 index f978da706b..0000000000 --- a/crates/workspace/src/theme_preview.rs +++ /dev/null @@ -1,429 +0,0 @@ -#![allow(unused, dead_code)] -use gpui::{ - AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Hsla, Task, actions, hsla, -}; -use strum::IntoEnumIterator; -use theme::all_theme_colors; -use ui::{ - AudioStatus, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike, - Checkbox, CollaboratorAvailability, ContentGroup, DecoratedIcon, ElevationIndex, Facepile, - IconDecoration, Indicator, KeybindingHint, Switch, TintColor, Tooltip, prelude::*, - utils::calculate_contrast_ratio, -}; - -use crate::{Item, Workspace}; - -actions!( - dev, - [ - /// Opens the theme preview window. - OpenThemePreview - ] -); - -pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, _, _| { - workspace.register_action(|workspace, _: &OpenThemePreview, window, cx| { - let theme_preview = cx.new(|cx| ThemePreview::new(window, cx)); - workspace.add_item_to_active_pane(Box::new(theme_preview), None, true, window, cx) - }); - }) - .detach(); -} - -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, strum::EnumIter)] -enum ThemePreviewPage { - Overview, - Typography, -} - -impl ThemePreviewPage { - pub fn name(&self) -> &'static str { - match self { - Self::Overview => "Overview", - Self::Typography => "Typography", - } - } -} - -struct ThemePreview { - current_page: ThemePreviewPage, - focus_handle: FocusHandle, -} - -impl ThemePreview { - pub fn new(window: &mut Window, cx: &mut Context) -> Self { - Self { - current_page: ThemePreviewPage::Overview, - focus_handle: cx.focus_handle(), - } - } - - pub fn view( - &self, - page: ThemePreviewPage, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - match page { - ThemePreviewPage::Overview => self.render_overview_page(window, cx).into_any_element(), - ThemePreviewPage::Typography => { - self.render_typography_page(window, cx).into_any_element() - } - } - } -} - -impl EventEmitter<()> for ThemePreview {} - -impl Focusable for ThemePreview { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} -impl ThemePreview {} - -impl Item for ThemePreview { - type Event = (); - - fn to_item_events(_: &Self::Event, _: impl FnMut(crate::item::ItemEvent)) {} - - fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString { - let name = cx.theme().name.clone(); - format!("{} Preview", name).into() - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - None - } - - fn can_split(&self) -> bool { - true - } - - fn clone_on_split( - &self, - _workspace_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> Task>> - where - Self: Sized, - { - Task::ready(Some(cx.new(|cx| Self::new(window, cx)))) - } -} - -const AVATAR_URL: &str = "https://avatars.githubusercontent.com/u/1714999?v=4"; - -impl ThemePreview { - fn preview_bg(window: &mut Window, cx: &mut App) -> Hsla { - cx.theme().colors().editor_background - } - - fn render_text( - &self, - layer: ElevationIndex, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let bg = layer.bg(cx); - - let label_with_contrast = |label: &str, fg: Hsla| { - let contrast = calculate_contrast_ratio(fg, bg); - format!("{} ({:.2})", label, contrast) - }; - - v_flex() - .gap_1() - .child(Headline::new("Text").size(HeadlineSize::Small).color(Color::Muted)) - .child( - h_flex() - .items_start() - .gap_4() - .child( - v_flex() - .gap_1() - .child(Headline::new("Headline Sizes").size(HeadlineSize::Small).color(Color::Muted)) - .child(Headline::new("XLarge Headline").size(HeadlineSize::XLarge)) - .child(Headline::new("Large Headline").size(HeadlineSize::Large)) - .child(Headline::new("Medium Headline").size(HeadlineSize::Medium)) - .child(Headline::new("Small Headline").size(HeadlineSize::Small)) - .child(Headline::new("XSmall Headline").size(HeadlineSize::XSmall)), - ) - .child( - v_flex() - .gap_1() - .child(Headline::new("Text Colors").size(HeadlineSize::Small).color(Color::Muted)) - .child( - Label::new(label_with_contrast( - "Default Text", - Color::Default.color(cx), - )) - .color(Color::Default), - ) - .child( - Label::new(label_with_contrast( - "Accent Text", - Color::Accent.color(cx), - )) - .color(Color::Accent), - ) - .child( - Label::new(label_with_contrast( - "Conflict Text", - Color::Conflict.color(cx), - )) - .color(Color::Conflict), - ) - .child( - Label::new(label_with_contrast( - "Created Text", - Color::Created.color(cx), - )) - .color(Color::Created), - ) - .child( - Label::new(label_with_contrast( - "Deleted Text", - Color::Deleted.color(cx), - )) - .color(Color::Deleted), - ) - .child( - Label::new(label_with_contrast( - "Disabled Text", - Color::Disabled.color(cx), - )) - .color(Color::Disabled), - ) - .child( - Label::new(label_with_contrast( - "Error Text", - Color::Error.color(cx), - )) - .color(Color::Error), - ) - .child( - Label::new(label_with_contrast( - "Hidden Text", - Color::Hidden.color(cx), - )) - .color(Color::Hidden), - ) - .child( - Label::new(label_with_contrast( - "Hint Text", - Color::Hint.color(cx), - )) - .color(Color::Hint), - ) - .child( - Label::new(label_with_contrast( - "Ignored Text", - Color::Ignored.color(cx), - )) - .color(Color::Ignored), - ) - .child( - Label::new(label_with_contrast( - "Info Text", - Color::Info.color(cx), - )) - .color(Color::Info), - ) - .child( - Label::new(label_with_contrast( - "Modified Text", - Color::Modified.color(cx), - )) - .color(Color::Modified), - ) - .child( - Label::new(label_with_contrast( - "Muted Text", - Color::Muted.color(cx), - )) - .color(Color::Muted), - ) - .child( - Label::new(label_with_contrast( - "Placeholder Text", - Color::Placeholder.color(cx), - )) - .color(Color::Placeholder), - ) - .child( - Label::new(label_with_contrast( - "Selected Text", - Color::Selected.color(cx), - )) - .color(Color::Selected), - ) - .child( - Label::new(label_with_contrast( - "Success Text", - Color::Success.color(cx), - )) - .color(Color::Success), - ) - .child( - Label::new(label_with_contrast( - "Warning Text", - Color::Warning.color(cx), - )) - .color(Color::Warning), - ) - ) - .child( - v_flex() - .gap_1() - .child(Headline::new("Wrapping Text").size(HeadlineSize::Small).color(Color::Muted)) - .child( - div().max_w(px(200.)).child( - "This is a longer piece of text that should wrap to multiple lines. It demonstrates how text behaves when it exceeds the width of its container." - )) - ) - ) - } - - fn render_colors( - &self, - layer: ElevationIndex, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let bg = layer.bg(cx); - let all_colors = all_theme_colors(cx); - - v_flex() - .gap_1() - .child( - Headline::new("Colors") - .size(HeadlineSize::Small) - .color(Color::Muted), - ) - .child( - h_flex() - .flex_wrap() - .gap_1() - .children(all_colors.into_iter().map(|(color, name)| { - let id = ElementId::Name(format!("{:?}-preview", color).into()); - div().size_8().flex_none().child( - ButtonLike::new(id) - .child( - div() - .size_8() - .bg(color) - .border_1() - .border_color(cx.theme().colors().border) - .overflow_hidden(), - ) - .size(ButtonSize::None) - .style(ButtonStyle::Transparent) - .tooltip(move |window, cx| { - let name = name.clone(); - Tooltip::with_meta(name, None, format!("{:?}", color), cx) - }), - ) - })), - ) - } - - fn render_theme_layer( - &self, - layer: ElevationIndex, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - v_flex() - .p_4() - .bg(layer.bg(cx)) - .text_color(cx.theme().colors().text) - .gap_2() - .child(Headline::new(layer.clone().to_string()).size(HeadlineSize::Medium)) - .child(self.render_text(layer, window, cx)) - .child(self.render_colors(layer, window, cx)) - } - - fn render_overview_page( - &self, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - v_flex() - .id("theme-preview-overview") - .overflow_scroll() - .size_full() - .child( - v_flex() - .child(Headline::new("Theme Preview").size(HeadlineSize::Large)) - .child(div().w_full().text_color(cx.theme().colors().text_muted).child("This view lets you preview a range of UI elements across a theme. Use it for testing out changes to the theme.")) - ) - .child(self.render_theme_layer(ElevationIndex::Background, window, cx)) - .child(self.render_theme_layer(ElevationIndex::Surface, window, cx)) - .child(self.render_theme_layer(ElevationIndex::EditorSurface, window, cx)) - .child(self.render_theme_layer(ElevationIndex::ElevatedSurface, window, cx)) - } - - fn render_typography_page( - &self, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - v_flex() - .id("theme-preview-typography") - .overflow_scroll() - .size_full() - .child(v_flex() - .gap_4() - .child(Headline::new("Headline 1").size(HeadlineSize::XLarge)) - .child(Label::new("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")) - .child(Headline::new("Headline 2").size(HeadlineSize::Large)) - .child(Label::new("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.")) - .child(Headline::new("Headline 3").size(HeadlineSize::Medium)) - .child(Label::new("Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.")) - .child(Headline::new("Headline 4").size(HeadlineSize::Small)) - .child(Label::new("Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")) - .child(Headline::new("Headline 5").size(HeadlineSize::XSmall)) - .child(Label::new("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.")) - .child(Headline::new("Body Text").size(HeadlineSize::Small)) - .child(Label::new("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")) - ) - } - - fn render_page_nav(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - h_flex() - .id("theme-preview-nav") - .items_center() - .gap_4() - .py_2() - .bg(Self::preview_bg(window, cx)) - .children(ThemePreviewPage::iter().map(|p| { - Button::new(ElementId::Name(p.name().into()), p.name()) - .on_click(cx.listener(move |this, _, window, cx| { - this.current_page = p; - cx.notify(); - })) - .toggle_state(p == self.current_page) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - })) - } -} - -impl Render for ThemePreview { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { - v_flex() - .id("theme-preview") - .key_context("ThemePreview") - .items_start() - .overflow_hidden() - .size_full() - .max_h_full() - .track_focus(&self.focus_handle) - .px_2() - .bg(Self::preview_bg(window, cx)) - .child(self.render_page_nav(window, cx)) - .child(self.view(self.current_page, window, cx)) - } -} diff --git a/crates/workspace/src/toast_layer.rs b/crates/workspace/src/toast_layer.rs deleted file mode 100644 index 5157945548..0000000000 --- a/crates/workspace/src/toast_layer.rs +++ /dev/null @@ -1,252 +0,0 @@ -use std::{ - rc::Rc, - time::{Duration, Instant}, -}; - -use gpui::{AnyView, DismissEvent, Entity, EntityId, FocusHandle, ManagedView, Subscription, Task}; -use ui::{animation::DefaultAnimations, prelude::*}; -use zed_actions::toast; - -use crate::Workspace; - -const DEFAULT_TOAST_DURATION: Duration = Duration::from_secs(10); -const MINIMUM_RESUME_DURATION: Duration = Duration::from_millis(800); - -pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, _window, _cx| { - workspace.register_action(|_workspace, _: &toast::RunAction, window, cx| { - let workspace = cx.entity(); - let window = window.window_handle(); - cx.defer(move |cx| { - let action = workspace - .read(cx) - .toast_layer - .read(cx) - .active_toast - .as_ref() - .and_then(|active_toast| active_toast.action.clone()); - - if let Some(on_click) = action.and_then(|action| action.on_click) { - window - .update(cx, |_, window, cx| { - on_click(window, cx); - }) - .ok(); - } - }); - }); - }) - .detach(); -} - -pub trait ToastView: ManagedView { - fn action(&self) -> Option; -} - -#[derive(Clone)] -pub struct ToastAction { - pub id: ElementId, - pub label: SharedString, - pub on_click: Option>, -} - -impl ToastAction { - pub fn new( - label: SharedString, - on_click: Option>, - ) -> Self { - let id = ElementId::Name(label.clone()); - - Self { - id, - label, - on_click, - } - } -} - -trait ToastViewHandle { - fn view(&self) -> AnyView; -} - -impl ToastViewHandle for Entity { - fn view(&self) -> AnyView { - self.clone().into() - } -} - -pub struct ActiveToast { - id: EntityId, - toast: Box, - action: Option, - _subscriptions: [Subscription; 1], - focus_handle: FocusHandle, -} - -struct DismissTimer { - instant_started: Instant, - _task: Task<()>, -} - -pub struct ToastLayer { - active_toast: Option, - duration_remaining: Option, - dismiss_timer: Option, -} - -impl Default for ToastLayer { - fn default() -> Self { - Self::new() - } -} - -impl ToastLayer { - pub fn new() -> Self { - Self { - active_toast: None, - duration_remaining: None, - dismiss_timer: None, - } - } - - pub fn toggle_toast(&mut self, cx: &mut Context, new_toast: Entity) - where - V: ToastView, - { - if let Some(active_toast) = &self.active_toast { - let show_new = active_toast.id != new_toast.entity_id(); - self.hide_toast(cx); - if !show_new { - return; - } - } - self.show_toast(new_toast, cx); - } - - pub fn show_toast(&mut self, new_toast: Entity, cx: &mut Context) - where - V: ToastView, - { - let action = new_toast.read(cx).action(); - let focus_handle = cx.focus_handle(); - - self.active_toast = Some(ActiveToast { - _subscriptions: [cx.subscribe(&new_toast, |this, _, _: &DismissEvent, cx| { - this.hide_toast(cx); - })], - id: new_toast.entity_id(), - toast: Box::new(new_toast), - action, - focus_handle, - }); - - self.start_dismiss_timer(DEFAULT_TOAST_DURATION, cx); - - cx.notify(); - } - - pub fn hide_toast(&mut self, cx: &mut Context) { - self.active_toast.take(); - cx.notify(); - } - - pub fn active_toast(&self) -> Option> - where - V: 'static, - { - let active_toast = self.active_toast.as_ref()?; - active_toast.toast.view().downcast::().ok() - } - - pub fn has_active_toast(&self) -> bool { - self.active_toast.is_some() - } - - fn pause_dismiss_timer(&mut self) { - let Some(dismiss_timer) = self.dismiss_timer.take() else { - return; - }; - let Some(duration_remaining) = self.duration_remaining.as_mut() else { - return; - }; - *duration_remaining = - duration_remaining.saturating_sub(dismiss_timer.instant_started.elapsed()); - if *duration_remaining < MINIMUM_RESUME_DURATION { - *duration_remaining = MINIMUM_RESUME_DURATION; - } - } - - /// Starts a timer to automatically dismiss the toast after the specified duration - pub fn start_dismiss_timer(&mut self, duration: Duration, cx: &mut Context) { - self.clear_dismiss_timer(cx); - - let instant_started = std::time::Instant::now(); - let task = cx.spawn(async move |this, cx| { - cx.background_executor().timer(duration).await; - - if let Some(this) = this.upgrade() { - this.update(cx, |this, cx| this.hide_toast(cx)).ok(); - } - }); - - self.duration_remaining = Some(duration); - self.dismiss_timer = Some(DismissTimer { - instant_started, - _task: task, - }); - cx.notify(); - } - - /// Restarts the dismiss timer with a new duration - pub fn restart_dismiss_timer(&mut self, cx: &mut Context) { - let Some(duration) = self.duration_remaining else { - return; - }; - self.start_dismiss_timer(duration, cx); - cx.notify(); - } - - /// Clears the dismiss timer if one exists - pub fn clear_dismiss_timer(&mut self, cx: &mut Context) { - self.dismiss_timer.take(); - cx.notify(); - } -} - -impl Render for ToastLayer { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let Some(active_toast) = &self.active_toast else { - return div(); - }; - - div().absolute().size_full().bottom_0().left_0().child( - v_flex() - .id(("toast-layer-container", active_toast.id)) - .absolute() - .w_full() - .bottom(px(0.)) - .flex() - .flex_col() - .items_center() - .track_focus(&active_toast.focus_handle) - .child( - h_flex() - .id("active-toast-container") - .occlude() - .on_hover(cx.listener(|this, hover_start, _window, cx| { - if *hover_start { - this.pause_dismiss_timer(); - } else { - this.restart_dismiss_timer(cx); - } - cx.stop_propagation(); - })) - .on_click(|_, _, cx| { - cx.stop_propagation(); - }) - .child(active_toast.toast.view()), - ) - .animate_in(AnimationDirection::FromBottom, true), - ) - } -} diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs deleted file mode 100644 index 6e26be6dc7..0000000000 --- a/crates/workspace/src/toolbar.rs +++ /dev/null @@ -1,282 +0,0 @@ -use crate::ItemHandle; -use gpui::{ - AnyView, App, Context, Entity, EntityId, EventEmitter, KeyContext, ParentElement as _, Render, - Styled, Window, -}; -use ui::prelude::*; -use ui::{h_flex, v_flex}; - -pub enum ToolbarItemEvent { - ChangeLocation(ToolbarItemLocation), -} - -pub trait ToolbarItemView: Render + EventEmitter { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn crate::ItemHandle>, - window: &mut Window, - cx: &mut Context, - ) -> ToolbarItemLocation; - - fn pane_focus_update( - &mut self, - _pane_focused: bool, - _window: &mut Window, - _cx: &mut Context, - ) { - } - - fn contribute_context(&self, _context: &mut KeyContext, _cx: &App) {} -} - -trait ToolbarItemViewHandle: Send { - fn id(&self) -> EntityId; - fn to_any(&self) -> AnyView; - fn set_active_pane_item( - &self, - active_pane_item: Option<&dyn ItemHandle>, - window: &mut Window, - cx: &mut App, - ) -> ToolbarItemLocation; - fn focus_changed(&mut self, pane_focused: bool, window: &mut Window, cx: &mut App); - fn contribute_context(&self, context: &mut KeyContext, cx: &App); -} - -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum ToolbarItemLocation { - Hidden, - PrimaryLeft, - PrimaryRight, - Secondary, -} - -pub struct Toolbar { - active_item: Option>, - hidden: bool, - can_navigate: bool, - items: Vec<(Box, ToolbarItemLocation)>, -} - -impl Toolbar { - fn has_any_visible_items(&self) -> bool { - self.items - .iter() - .any(|(_item, location)| *location != ToolbarItemLocation::Hidden) - } - - fn left_items(&self) -> impl Iterator { - self.items.iter().filter_map(|(item, location)| { - if *location == ToolbarItemLocation::PrimaryLeft { - Some(item.as_ref()) - } else { - None - } - }) - } - - fn right_items(&self) -> impl Iterator { - self.items.iter().filter_map(|(item, location)| { - if *location == ToolbarItemLocation::PrimaryRight { - Some(item.as_ref()) - } else { - None - } - }) - } - - fn secondary_items(&self) -> impl Iterator { - self.items.iter().rev().filter_map(|(item, location)| { - if *location == ToolbarItemLocation::Secondary { - Some(item.as_ref()) - } else { - None - } - }) - } -} - -impl Render for Toolbar { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - if !self.has_any_visible_items() { - return div(); - } - - let secondary_items = self.secondary_items().map(|item| item.to_any()); - - let has_left_items = self.left_items().count() > 0; - let has_right_items = self.right_items().count() > 0; - - v_flex() - .group("toolbar") - .relative() - .p(DynamicSpacing::Base08.rems(cx)) - .when(has_left_items || has_right_items, |this| { - this.gap(DynamicSpacing::Base08.rems(cx)) - }) - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().toolbar_background) - .when(has_left_items || has_right_items, |this| { - this.child( - h_flex() - .min_h_6() - .justify_between() - .gap(DynamicSpacing::Base08.rems(cx)) - .when(has_left_items, |this| { - this.child( - h_flex() - .flex_auto() - .justify_start() - .overflow_x_hidden() - .children(self.left_items().map(|item| item.to_any())), - ) - }) - .when(has_right_items, |this| { - this.child( - h_flex() - .h_full() - .flex_row_reverse() - .map(|el| { - if has_left_items { - // We're using `flex_none` here to prevent some flickering that can occur when the - // size of the left items container changes. - el.flex_none() - } else { - el.flex_auto() - } - }) - .justify_end() - .children(self.right_items().map(|item| item.to_any())), - ) - }), - ) - }) - .children(secondary_items) - } -} - -impl Default for Toolbar { - fn default() -> Self { - Self::new() - } -} - -impl Toolbar { - pub fn new() -> Self { - Self { - active_item: None, - items: Default::default(), - hidden: false, - can_navigate: true, - } - } - - pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut Context) { - self.can_navigate = can_navigate; - cx.notify(); - } - - pub fn add_item(&mut self, item: Entity, window: &mut Window, cx: &mut Context) - where - T: 'static + ToolbarItemView, - { - let location = item.set_active_pane_item(self.active_item.as_deref(), window, cx); - cx.subscribe(&item, |this, item, event, cx| { - if let Some((_, current_location)) = this - .items - .iter_mut() - .find(|(i, _)| i.id() == item.entity_id()) - { - match event { - ToolbarItemEvent::ChangeLocation(new_location) => { - if new_location != current_location { - *current_location = *new_location; - cx.notify(); - } - } - } - } - }) - .detach(); - self.items.push((Box::new(item), location)); - cx.notify(); - } - - pub fn set_active_item( - &mut self, - item: Option<&dyn ItemHandle>, - window: &mut Window, - cx: &mut Context, - ) { - self.active_item = item.map(|item| item.boxed_clone()); - self.hidden = self - .active_item - .as_ref() - .map(|item| !item.show_toolbar(cx)) - .unwrap_or(false); - - for (toolbar_item, current_location) in self.items.iter_mut() { - let new_location = toolbar_item.set_active_pane_item(item, window, cx); - if new_location != *current_location { - *current_location = new_location; - cx.notify(); - } - } - } - - pub fn focus_changed(&mut self, focused: bool, window: &mut Window, cx: &mut Context) { - for (toolbar_item, _) in self.items.iter_mut() { - toolbar_item.focus_changed(focused, window, cx); - } - } - - pub fn item_of_type(&self) -> Option> { - self.items - .iter() - .find_map(|(item, _)| item.to_any().downcast().ok()) - } - - pub fn hidden(&self) -> bool { - self.hidden - } - - pub fn contribute_context(&self, context: &mut KeyContext, cx: &App) { - for (item, location) in &self.items { - if *location != ToolbarItemLocation::Hidden { - item.contribute_context(context, cx); - } - } - } -} - -impl ToolbarItemViewHandle for Entity { - fn id(&self) -> EntityId { - self.entity_id() - } - - fn to_any(&self) -> AnyView { - self.clone().into() - } - - fn set_active_pane_item( - &self, - active_pane_item: Option<&dyn ItemHandle>, - window: &mut Window, - cx: &mut App, - ) -> ToolbarItemLocation { - self.update(cx, |this, cx| { - this.set_active_pane_item(active_pane_item, window, cx) - }) - } - - fn focus_changed(&mut self, pane_focused: bool, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| { - this.pane_focus_update(pane_focused, window, cx); - cx.notify(); - }); - } - - fn contribute_context(&self, context: &mut KeyContext, cx: &App) { - self.read(cx).contribute_context(context, cx) - } -} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs deleted file mode 100644 index d2a9ef71fc..0000000000 --- a/crates/workspace/src/workspace.rs +++ /dev/null @@ -1,11487 +0,0 @@ -pub mod dock; -pub mod history_manager; -pub mod invalid_item_view; -pub mod item; -mod modal_layer; -pub mod notifications; -pub mod pane; -pub mod pane_group; -mod path_list; -mod persistence; -pub mod searchable; -pub mod shared_screen; -mod status_bar; -pub mod tasks; -mod theme_preview; -mod toast_layer; -mod toolbar; -mod workspace_settings; - -pub use crate::notifications::NotificationFrame; -pub use dock::Panel; -pub use path_list::PathList; -pub use toast_layer::{ToastAction, ToastLayer, ToastView}; - -use anyhow::{Context as _, Result, anyhow}; -use call::{ActiveCall, call_settings::CallSettings}; -use client::{ - ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore, - proto::{self, ErrorCode, PanelId, PeerId}, -}; -use collections::{HashMap, HashSet, hash_map}; -use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE}; -use futures::{ - Future, FutureExt, StreamExt, - channel::{ - mpsc::{self, UnboundedReceiver, UnboundedSender}, - oneshot, - }, - future::{Shared, try_join_all}, -}; -use gpui::{ - Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context, - CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, - Focusable, Global, HitboxBehavior, Hsla, KeyContext, Keystroke, ManagedView, MouseButton, - PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, - SystemWindowTabController, Task, Tiling, WeakEntity, WindowBounds, WindowHandle, WindowId, - WindowOptions, actions, canvas, point, relative, size, transparent_black, -}; -pub use history_manager::*; -pub use item::{ - FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings, - ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle, -}; -use itertools::Itertools; -use language::{Buffer, LanguageRegistry, Rope, language_settings::all_language_settings}; -pub use modal_layer::*; -use node_runtime::NodeRuntime; -use notifications::{ - DetachAndPromptErr, Notifications, dismiss_app_notification, - simple_message_notification::MessageNotification, -}; -pub use pane::*; -pub use pane_group::{ - ActivePaneDecorator, HANDLE_HITBOX_SIZE, Member, PaneAxis, PaneGroup, PaneRenderContext, - SplitDirection, -}; -use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace}; -pub use persistence::{ - DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items, - model::{ItemId, SerializedWorkspaceLocation}, -}; -use postage::stream::Stream; -use project::{ - DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId, - WorktreeSettings, - debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus}, - toolchain_store::ToolchainStoreEvent, -}; -use remote::{ - RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions, - remote_client::ConnectionIdentifier, -}; -use schemars::JsonSchema; -use serde::Deserialize; -use session::AppSession; -use settings::{CenteredPaddingSettings, Settings, SettingsLocation, update_settings_file}; -use shared_screen::SharedScreen; -use sqlez::{ - bindable::{Bind, Column, StaticColumnCount}, - statement::Statement, -}; -use status_bar::StatusBar; -pub use status_bar::StatusItemView; -use std::{ - any::TypeId, - borrow::Cow, - cell::RefCell, - cmp, - collections::{VecDeque, hash_map::DefaultHasher}, - env, - hash::{Hash, Hasher}, - path::{Path, PathBuf}, - process::ExitStatus, - rc::Rc, - sync::{ - Arc, LazyLock, Weak, - atomic::{AtomicBool, AtomicUsize}, - }, - time::Duration, -}; -use task::{DebugScenario, SpawnInTerminal, TaskContext}; -use theme::{ActiveTheme, GlobalTheme, SystemAppearance, ThemeSettings}; -pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; -pub use ui; -use ui::{Window, prelude::*}; -use util::{ - ResultExt, TryFutureExt, - paths::{PathStyle, SanitizedPath}, - rel_path::RelPath, - serde::default_true, -}; -use uuid::Uuid; -pub use workspace_settings::{ - AutosaveSetting, BottomDockLayout, RestoreOnStartupBehavior, StatusBarSettings, TabBarSettings, - WorkspaceSettings, -}; -use zed_actions::{Spawn, feedback::FileBugReport}; - -use crate::persistence::{ - SerializedAxis, - model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup}, -}; -use crate::{item::ItemBufferKind, notifications::NotificationId}; - -pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200); - -static ZED_WINDOW_SIZE: LazyLock>> = LazyLock::new(|| { - env::var("ZED_WINDOW_SIZE") - .ok() - .as_deref() - .and_then(parse_pixel_size_env_var) -}); - -static ZED_WINDOW_POSITION: LazyLock>> = LazyLock::new(|| { - env::var("ZED_WINDOW_POSITION") - .ok() - .as_deref() - .and_then(parse_pixel_position_env_var) -}); - -pub trait TerminalProvider { - fn spawn( - &self, - task: SpawnInTerminal, - window: &mut Window, - cx: &mut App, - ) -> Task>>; -} - -pub trait DebuggerProvider { - // `active_buffer` is used to resolve build task's name against language-specific tasks. - fn start_session( - &self, - definition: DebugScenario, - task_context: TaskContext, - active_buffer: Option>, - worktree_id: Option, - window: &mut Window, - cx: &mut App, - ); - - fn spawn_task_or_modal( - &self, - workspace: &mut Workspace, - action: &Spawn, - window: &mut Window, - cx: &mut Context, - ); - - fn task_scheduled(&self, cx: &mut App); - fn debug_scenario_scheduled(&self, cx: &mut App); - fn debug_scenario_scheduled_last(&self, cx: &App) -> bool; - - fn active_thread_state(&self, cx: &App) -> Option; -} - -actions!( - workspace, - [ - /// Activates the next pane in the workspace. - ActivateNextPane, - /// Activates the previous pane in the workspace. - ActivatePreviousPane, - /// Switches to the next window. - ActivateNextWindow, - /// Switches to the previous window. - ActivatePreviousWindow, - /// Adds a folder to the current project. - AddFolderToProject, - /// Clears all notifications. - ClearAllNotifications, - /// Clears all navigation history, including forward/backward navigation, recently opened files, and recently closed tabs. **This action is irreversible**. - ClearNavigationHistory, - /// Closes the active dock. - CloseActiveDock, - /// Closes all docks. - CloseAllDocks, - /// Toggles all docks. - ToggleAllDocks, - /// Closes the current window. - CloseWindow, - /// Opens the feedback dialog. - Feedback, - /// Follows the next collaborator in the session. - FollowNextCollaborator, - /// Moves the focused panel to the next position. - MoveFocusedPanelToNextPosition, - /// Opens a new terminal in the center. - NewCenterTerminal, - /// Creates a new file. - NewFile, - /// Creates a new file in a vertical split. - NewFileSplitVertical, - /// Creates a new file in a horizontal split. - NewFileSplitHorizontal, - /// Opens a new search. - NewSearch, - /// Opens a new terminal. - NewTerminal, - /// Opens a new window. - NewWindow, - /// Opens a file or directory. - Open, - /// Opens multiple files. - OpenFiles, - /// Opens the current location in terminal. - OpenInTerminal, - /// Opens the component preview. - OpenComponentPreview, - /// Reloads the active item. - ReloadActiveItem, - /// Resets the active dock to its default size. - ResetActiveDockSize, - /// Resets all open docks to their default sizes. - ResetOpenDocksSize, - /// Reloads the application - Reload, - /// Saves the current file with a new name. - SaveAs, - /// Saves without formatting. - SaveWithoutFormat, - /// Shuts down all debug adapters. - ShutdownDebugAdapters, - /// Suppresses the current notification. - SuppressNotification, - /// Toggles the bottom dock. - ToggleBottomDock, - /// Toggles centered layout mode. - ToggleCenteredLayout, - /// Toggles edit prediction feature globally for all files. - ToggleEditPrediction, - /// Toggles the left dock. - ToggleLeftDock, - /// Toggles the right dock. - ToggleRightDock, - /// Toggles zoom on the active pane. - ToggleZoom, - /// Stops following a collaborator. - Unfollow, - /// Restores the banner. - RestoreBanner, - /// Toggles expansion of the selected item. - ToggleExpandItem, - ] -); - -/// Activates a specific pane by its index. -#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)] -#[action(namespace = workspace)] -pub struct ActivatePane(pub usize); - -/// Moves an item to a specific pane by index. -#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct MoveItemToPane { - #[serde(default = "default_1")] - pub destination: usize, - #[serde(default = "default_true")] - pub focus: bool, - #[serde(default)] - pub clone: bool, -} - -fn default_1() -> usize { - 1 -} - -/// Moves an item to a pane in the specified direction. -#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct MoveItemToPaneInDirection { - #[serde(default = "default_right")] - pub direction: SplitDirection, - #[serde(default = "default_true")] - pub focus: bool, - #[serde(default)] - pub clone: bool, -} - -/// Creates a new file in a split of the desired direction. -#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct NewFileSplit(pub SplitDirection); - -fn default_right() -> SplitDirection { - SplitDirection::Right -} - -/// Saves all open files in the workspace. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct SaveAll { - #[serde(default)] - pub save_intent: Option, -} - -/// Saves the current file with the specified options. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct Save { - #[serde(default)] - pub save_intent: Option, -} - -/// Closes all items and panes in the workspace. -#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct CloseAllItemsAndPanes { - #[serde(default)] - pub save_intent: Option, -} - -/// Closes all inactive tabs and panes in the workspace. -#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct CloseInactiveTabsAndPanes { - #[serde(default)] - pub save_intent: Option, -} - -/// Sends a sequence of keystrokes to the active element. -#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)] -#[action(namespace = workspace)] -pub struct SendKeystrokes(pub String); - -actions!( - project_symbols, - [ - /// Toggles the project symbols search. - #[action(name = "Toggle")] - ToggleProjectSymbols - ] -); - -/// Toggles the file finder interface. -#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)] -#[action(namespace = file_finder, name = "Toggle")] -#[serde(deny_unknown_fields)] -pub struct ToggleFileFinder { - #[serde(default)] - pub separate_history: bool, -} - -/// Increases size of a currently focused dock by a given amount of pixels. -#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct IncreaseActiveDockSize { - /// For 0px parameter, uses UI font size value. - #[serde(default)] - pub px: u32, -} - -/// Decreases size of a currently focused dock by a given amount of pixels. -#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct DecreaseActiveDockSize { - /// For 0px parameter, uses UI font size value. - #[serde(default)] - pub px: u32, -} - -/// Increases size of all currently visible docks uniformly, by a given amount of pixels. -#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct IncreaseOpenDocksSize { - /// For 0px parameter, uses UI font size value. - #[serde(default)] - pub px: u32, -} - -/// Decreases size of all currently visible docks uniformly, by a given amount of pixels. -#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct DecreaseOpenDocksSize { - /// For 0px parameter, uses UI font size value. - #[serde(default)] - pub px: u32, -} - -actions!( - workspace, - [ - /// Activates the pane to the left. - ActivatePaneLeft, - /// Activates the pane to the right. - ActivatePaneRight, - /// Activates the pane above. - ActivatePaneUp, - /// Activates the pane below. - ActivatePaneDown, - /// Swaps the current pane with the one to the left. - SwapPaneLeft, - /// Swaps the current pane with the one to the right. - SwapPaneRight, - /// Swaps the current pane with the one above. - SwapPaneUp, - /// Swaps the current pane with the one below. - SwapPaneDown, - // Swaps the current pane with the first available adjacent pane (searching in order: below, above, right, left) and activates that pane. - SwapPaneAdjacent, - /// Move the current pane to be at the far left. - MovePaneLeft, - /// Move the current pane to be at the far right. - MovePaneRight, - /// Move the current pane to be at the very top. - MovePaneUp, - /// Move the current pane to be at the very bottom. - MovePaneDown, - ] -); - -#[derive(PartialEq, Eq, Debug)] -pub enum CloseIntent { - /// Quit the program entirely. - Quit, - /// Close a window. - CloseWindow, - /// Replace the workspace in an existing window. - ReplaceWindow, -} - -#[derive(Clone)] -pub struct Toast { - id: NotificationId, - msg: Cow<'static, str>, - autohide: bool, - on_click: Option<(Cow<'static, str>, Arc)>, -} - -impl Toast { - pub fn new>>(id: NotificationId, msg: I) -> Self { - Toast { - id, - msg: msg.into(), - on_click: None, - autohide: false, - } - } - - pub fn on_click(mut self, message: M, on_click: F) -> Self - where - M: Into>, - F: Fn(&mut Window, &mut App) + 'static, - { - self.on_click = Some((message.into(), Arc::new(on_click))); - self - } - - pub fn autohide(mut self) -> Self { - self.autohide = true; - self - } -} - -impl PartialEq for Toast { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - && self.msg == other.msg - && self.on_click.is_some() == other.on_click.is_some() - } -} - -/// Opens a new terminal with the specified working directory. -#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct OpenTerminal { - pub working_directory: PathBuf, -} - -#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub struct WorkspaceId(i64); - -impl StaticColumnCount for WorkspaceId {} -impl Bind for WorkspaceId { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - self.0.bind(statement, start_index) - } -} -impl Column for WorkspaceId { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - i64::column(statement, start_index) - .map(|(i, next_index)| (Self(i), next_index)) - .with_context(|| format!("Failed to read WorkspaceId at index {start_index}")) - } -} -impl From for i64 { - fn from(val: WorkspaceId) -> Self { - val.0 - } -} - -fn prompt_and_open_paths(app_state: Arc, options: PathPromptOptions, cx: &mut App) { - let paths = cx.prompt_for_paths(options); - cx.spawn( - async move |cx| match paths.await.anyhow().and_then(|res| res) { - Ok(Some(paths)) => { - cx.update(|cx| { - open_paths(&paths, app_state, OpenOptions::default(), cx).detach_and_log_err(cx) - }) - .ok(); - } - Ok(None) => {} - Err(err) => { - util::log_err(&err); - cx.update(|cx| { - if let Some(workspace_window) = cx - .active_window() - .and_then(|window| window.downcast::()) - { - workspace_window - .update(cx, |workspace, _, cx| { - workspace.show_portal_error(err.to_string(), cx); - }) - .ok(); - } - }) - .ok(); - } - }, - ) - .detach(); -} - -pub fn init(app_state: Arc, cx: &mut App) { - component::init(); - theme_preview::init(cx); - toast_layer::init(cx); - history_manager::init(cx); - - cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx)); - cx.on_action(|_: &Reload, cx| reload(cx)); - - cx.on_action({ - let app_state = Arc::downgrade(&app_state); - move |_: &Open, cx: &mut App| { - if let Some(app_state) = app_state.upgrade() { - prompt_and_open_paths( - app_state, - PathPromptOptions { - files: true, - directories: true, - multiple: true, - prompt: None, - }, - cx, - ); - } - } - }); - cx.on_action({ - let app_state = Arc::downgrade(&app_state); - move |_: &OpenFiles, cx: &mut App| { - let directories = cx.can_select_mixed_files_and_dirs(); - if let Some(app_state) = app_state.upgrade() { - prompt_and_open_paths( - app_state, - PathPromptOptions { - files: true, - directories, - multiple: true, - prompt: None, - }, - cx, - ); - } - } - }); -} - -type BuildProjectItemFn = - fn(AnyEntity, Entity, Option<&Pane>, &mut Window, &mut App) -> Box; - -type BuildProjectItemForPathFn = - fn( - &Entity, - &ProjectPath, - &mut Window, - &mut App, - ) -> Option, WorkspaceItemBuilder)>>>; - -#[derive(Clone, Default)] -struct ProjectItemRegistry { - build_project_item_fns_by_type: HashMap, - build_project_item_for_path_fns: Vec, -} - -impl ProjectItemRegistry { - fn register(&mut self) { - self.build_project_item_fns_by_type.insert( - TypeId::of::(), - |item, project, pane, window, cx| { - let item = item.downcast().unwrap(); - Box::new(cx.new(|cx| T::for_project_item(project, pane, item, window, cx))) - as Box - }, - ); - self.build_project_item_for_path_fns - .push(|project, project_path, window, cx| { - let project_path = project_path.clone(); - let is_file = project - .read(cx) - .entry_for_path(&project_path, cx) - .is_some_and(|entry| entry.is_file()); - let entry_abs_path = project.read(cx).absolute_path(&project_path, cx); - let is_local = project.read(cx).is_local(); - let project_item = - ::try_open(project, &project_path, cx)?; - let project = project.clone(); - Some(window.spawn(cx, async move |cx| { - match project_item.await.with_context(|| { - format!( - "opening project path {:?}", - entry_abs_path.as_deref().unwrap_or(&project_path.path.as_std_path()) - ) - }) { - Ok(project_item) => { - let project_item = project_item; - let project_entry_id: Option = - project_item.read_with(cx, project::ProjectItem::entry_id)?; - let build_workspace_item = Box::new( - |pane: &mut Pane, window: &mut Window, cx: &mut Context| { - Box::new(cx.new(|cx| { - T::for_project_item( - project, - Some(pane), - project_item, - window, - cx, - ) - })) as Box - }, - ) as Box<_>; - Ok((project_entry_id, build_workspace_item)) - } - Err(e) => { - log::warn!("Failed to open a project item: {e:#}"); - if e.error_code() == ErrorCode::Internal { - if let Some(abs_path) = - entry_abs_path.as_deref().filter(|_| is_file) - { - if let Some(broken_project_item_view) = - cx.update(|window, cx| { - T::for_broken_project_item( - abs_path, is_local, &e, window, cx, - ) - })? - { - let build_workspace_item = Box::new( - move |_: &mut Pane, _: &mut Window, cx: &mut Context| { - cx.new(|_| broken_project_item_view).boxed_clone() - }, - ) - as Box<_>; - return Ok((None, build_workspace_item)); - } - } - } - Err(e) - } - } - })) - }); - } - - fn open_path( - &self, - project: &Entity, - path: &ProjectPath, - window: &mut Window, - cx: &mut App, - ) -> Task, WorkspaceItemBuilder)>> { - let Some(open_project_item) = self - .build_project_item_for_path_fns - .iter() - .rev() - .find_map(|open_project_item| open_project_item(project, path, window, cx)) - else { - return Task::ready(Err(anyhow!("cannot open file {:?}", path.path))); - }; - open_project_item - } - - fn build_item( - &self, - item: Entity, - project: Entity, - pane: Option<&Pane>, - window: &mut Window, - cx: &mut App, - ) -> Option> { - let build = self - .build_project_item_fns_by_type - .get(&TypeId::of::())?; - Some(build(item.into_any(), project, pane, window, cx)) - } -} - -type WorkspaceItemBuilder = - Box) -> Box>; - -impl Global for ProjectItemRegistry {} - -/// Registers a [ProjectItem] for the app. When opening a file, all the registered -/// items will get a chance to open the file, starting from the project item that -/// was added last. -pub fn register_project_item(cx: &mut App) { - cx.default_global::().register::(); -} - -#[derive(Default)] -pub struct FollowableViewRegistry(HashMap); - -struct FollowableViewDescriptor { - from_state_proto: fn( - Entity, - ViewId, - &mut Option, - &mut Window, - &mut App, - ) -> Option>>>, - to_followable_view: fn(&AnyView) -> Box, -} - -impl Global for FollowableViewRegistry {} - -impl FollowableViewRegistry { - pub fn register(cx: &mut App) { - cx.default_global::().0.insert( - TypeId::of::(), - FollowableViewDescriptor { - from_state_proto: |workspace, id, state, window, cx| { - I::from_state_proto(workspace, id, state, window, cx).map(|task| { - cx.foreground_executor() - .spawn(async move { Ok(Box::new(task.await?) as Box<_>) }) - }) - }, - to_followable_view: |view| Box::new(view.clone().downcast::().unwrap()), - }, - ); - } - - pub fn from_state_proto( - workspace: Entity, - view_id: ViewId, - mut state: Option, - window: &mut Window, - cx: &mut App, - ) -> Option>>> { - cx.update_default_global(|this: &mut Self, cx| { - this.0.values().find_map(|descriptor| { - (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, window, cx) - }) - }) - } - - pub fn to_followable_view( - view: impl Into, - cx: &App, - ) -> Option> { - let this = cx.try_global::()?; - let view = view.into(); - let descriptor = this.0.get(&view.entity_type())?; - Some((descriptor.to_followable_view)(&view)) - } -} - -#[derive(Copy, Clone)] -struct SerializableItemDescriptor { - deserialize: fn( - Entity, - WeakEntity, - WorkspaceId, - ItemId, - &mut Window, - &mut Context, - ) -> Task>>, - cleanup: fn(WorkspaceId, Vec, &mut Window, &mut App) -> Task>, - view_to_serializable_item: fn(AnyView) -> Box, -} - -#[derive(Default)] -struct SerializableItemRegistry { - descriptors_by_kind: HashMap, SerializableItemDescriptor>, - descriptors_by_type: HashMap, -} - -impl Global for SerializableItemRegistry {} - -impl SerializableItemRegistry { - fn deserialize( - item_kind: &str, - project: Entity, - workspace: WeakEntity, - workspace_id: WorkspaceId, - item_item: ItemId, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let Some(descriptor) = Self::descriptor(item_kind, cx) else { - return Task::ready(Err(anyhow!( - "cannot deserialize {}, descriptor not found", - item_kind - ))); - }; - - (descriptor.deserialize)(project, workspace, workspace_id, item_item, window, cx) - } - - fn cleanup( - item_kind: &str, - workspace_id: WorkspaceId, - loaded_items: Vec, - window: &mut Window, - cx: &mut App, - ) -> Task> { - let Some(descriptor) = Self::descriptor(item_kind, cx) else { - return Task::ready(Err(anyhow!( - "cannot cleanup {}, descriptor not found", - item_kind - ))); - }; - - (descriptor.cleanup)(workspace_id, loaded_items, window, cx) - } - - fn view_to_serializable_item_handle( - view: AnyView, - cx: &App, - ) -> Option> { - let this = cx.try_global::()?; - let descriptor = this.descriptors_by_type.get(&view.entity_type())?; - Some((descriptor.view_to_serializable_item)(view)) - } - - fn descriptor(item_kind: &str, cx: &App) -> Option { - let this = cx.try_global::()?; - this.descriptors_by_kind.get(item_kind).copied() - } -} - -pub fn register_serializable_item(cx: &mut App) { - let serialized_item_kind = I::serialized_item_kind(); - - let registry = cx.default_global::(); - let descriptor = SerializableItemDescriptor { - deserialize: |project, workspace, workspace_id, item_id, window, cx| { - let task = I::deserialize(project, workspace, workspace_id, item_id, window, cx); - cx.foreground_executor() - .spawn(async { Ok(Box::new(task.await?) as Box<_>) }) - }, - cleanup: |workspace_id, loaded_items, window, cx| { - I::cleanup(workspace_id, loaded_items, window, cx) - }, - view_to_serializable_item: |view| Box::new(view.downcast::().unwrap()), - }; - registry - .descriptors_by_kind - .insert(Arc::from(serialized_item_kind), descriptor); - registry - .descriptors_by_type - .insert(TypeId::of::(), descriptor); -} - -pub struct AppState { - pub languages: Arc, - pub client: Arc, - pub user_store: Entity, - pub workspace_store: Entity, - pub fs: Arc, - pub build_window_options: fn(Option, &mut App) -> WindowOptions, - pub node_runtime: NodeRuntime, - pub session: Entity, -} - -struct GlobalAppState(Weak); - -impl Global for GlobalAppState {} - -pub struct WorkspaceStore { - workspaces: HashSet>, - client: Arc, - _subscriptions: Vec, -} - -#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)] -pub enum CollaboratorId { - PeerId(PeerId), - Agent, -} - -impl From for CollaboratorId { - fn from(peer_id: PeerId) -> Self { - CollaboratorId::PeerId(peer_id) - } -} - -impl From<&PeerId> for CollaboratorId { - fn from(peer_id: &PeerId) -> Self { - CollaboratorId::PeerId(*peer_id) - } -} - -#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] -struct Follower { - project_id: Option, - peer_id: PeerId, -} - -impl AppState { - #[track_caller] - pub fn global(cx: &App) -> Weak { - cx.global::().0.clone() - } - pub fn try_global(cx: &App) -> Option> { - cx.try_global::() - .map(|state| state.0.clone()) - } - pub fn set_global(state: Weak, cx: &mut App) { - cx.set_global(GlobalAppState(state)); - } - - #[cfg(any(test, feature = "test-support"))] - pub fn test(cx: &mut App) -> Arc { - use node_runtime::NodeRuntime; - use session::Session; - use settings::SettingsStore; - - if !cx.has_global::() { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - } - - let fs = fs::FakeFs::new(cx.background_executor().clone()); - let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); - let clock = Arc::new(clock::FakeSystemClock::new()); - let http_client = http_client::FakeHttpClient::with_404_response(); - let client = Client::new(clock, http_client, cx); - let session = cx.new(|cx| AppSession::new(Session::test(), cx)); - let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx)); - - theme::init(theme::LoadThemes::JustBase, cx); - client::init(&client, cx); - - Arc::new(Self { - client, - fs, - languages, - user_store, - workspace_store, - node_runtime: NodeRuntime::unavailable(), - build_window_options: |_, _| Default::default(), - session, - }) - } -} - -struct DelayedDebouncedEditAction { - task: Option>, - cancel_channel: Option>, -} - -impl DelayedDebouncedEditAction { - fn new() -> DelayedDebouncedEditAction { - DelayedDebouncedEditAction { - task: None, - cancel_channel: None, - } - } - - fn fire_new( - &mut self, - delay: Duration, - window: &mut Window, - cx: &mut Context, - func: F, - ) where - F: 'static - + Send - + FnOnce(&mut Workspace, &mut Window, &mut Context) -> Task>, - { - if let Some(channel) = self.cancel_channel.take() { - _ = channel.send(()); - } - - let (sender, mut receiver) = oneshot::channel::<()>(); - self.cancel_channel = Some(sender); - - let previous_task = self.task.take(); - self.task = Some(cx.spawn_in(window, async move |workspace, cx| { - let mut timer = cx.background_executor().timer(delay).fuse(); - if let Some(previous_task) = previous_task { - previous_task.await; - } - - futures::select_biased! { - _ = receiver => return, - _ = timer => {} - } - - if let Some(result) = workspace - .update_in(cx, |workspace, window, cx| (func)(workspace, window, cx)) - .log_err() - { - result.await.log_err(); - } - })); - } -} - -pub enum Event { - PaneAdded(Entity), - PaneRemoved, - ItemAdded { - item: Box, - }, - ActiveItemChanged, - ItemRemoved { - item_id: EntityId, - }, - UserSavedItem { - pane: WeakEntity, - item: Box, - save_intent: SaveIntent, - }, - ContactRequestedJoin(u64), - WorkspaceCreated(WeakEntity), - OpenBundledFile { - text: Cow<'static, str>, - title: &'static str, - language: &'static str, - }, - ZoomChanged, - ModalOpened, -} - -#[derive(Debug)] -pub enum OpenVisible { - All, - None, - OnlyFiles, - OnlyDirectories, -} - -enum WorkspaceLocation { - // Valid local paths or SSH project to serialize - Location(SerializedWorkspaceLocation, PathList), - // No valid location found hence clear session id - DetachFromSession, - // No valid location found to serialize - None, -} - -type PromptForNewPath = Box< - dyn Fn( - &mut Workspace, - DirectoryLister, - &mut Window, - &mut Context, - ) -> oneshot::Receiver>>, ->; - -type PromptForOpenPath = Box< - dyn Fn( - &mut Workspace, - DirectoryLister, - &mut Window, - &mut Context, - ) -> oneshot::Receiver>>, ->; - -#[derive(Default)] -struct DispatchingKeystrokes { - dispatched: HashSet>, - queue: VecDeque, - task: Option>>, -} - -/// Collects everything project-related for a certain window opened. -/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`. -/// -/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar. -/// The `Workspace` owns everybody's state and serves as a default, "global context", -/// that can be used to register a global action to be triggered from any place in the window. -pub struct Workspace { - weak_self: WeakEntity, - workspace_actions: Vec) -> Div>>, - zoomed: Option, - previous_dock_drag_coordinates: Option>, - zoomed_position: Option, - center: PaneGroup, - left_dock: Entity, - bottom_dock: Entity, - right_dock: Entity, - panes: Vec>, - panes_by_item: HashMap>, - active_pane: Entity, - last_active_center_pane: Option>, - last_active_view_id: Option, - status_bar: Entity, - modal_layer: Entity, - toast_layer: Entity, - titlebar_item: Option, - notifications: Notifications, - suppressed_notifications: HashSet, - project: Entity, - follower_states: HashMap, - last_leaders_by_pane: HashMap, CollaboratorId>, - window_edited: bool, - last_window_title: Option, - dirty_items: HashMap, - active_call: Option<(Entity, Vec)>, - leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, - database_id: Option, - app_state: Arc, - dispatching_keystrokes: Rc>, - _subscriptions: Vec, - _apply_leader_updates: Task>, - _observe_current_user: Task>, - _schedule_serialize_workspace: Option>, - _schedule_serialize_ssh_paths: Option>, - pane_history_timestamp: Arc, - bounds: Bounds, - pub centered_layout: bool, - bounds_save_task_queued: Option>, - on_prompt_for_new_path: Option, - on_prompt_for_open_path: Option, - terminal_provider: Option>, - debugger_provider: Option>, - serializable_items_tx: UnboundedSender>, - _items_serializer: Task>, - session_id: Option, - scheduled_tasks: Vec>, - last_open_dock_positions: Vec, - removing: bool, -} - -impl EventEmitter for Workspace {} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -pub struct ViewId { - pub creator: CollaboratorId, - pub id: u64, -} - -pub struct FollowerState { - center_pane: Entity, - dock_pane: Option>, - active_view_id: Option, - items_by_leader_view_id: HashMap, -} - -struct FollowerView { - view: Box, - location: Option, -} - -impl Workspace { - pub fn new( - workspace_id: Option, - project: Entity, - app_state: Arc, - window: &mut Window, - cx: &mut Context, - ) -> Self { - cx.subscribe_in(&project, window, move |this, _, event, window, cx| { - match event { - project::Event::RemoteIdChanged(_) => { - this.update_window_title(window, cx); - } - - project::Event::CollaboratorLeft(peer_id) => { - this.collaborator_left(*peer_id, window, cx); - } - - project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => { - this.update_window_title(window, cx); - this.serialize_workspace(window, cx); - // This event could be triggered by `AddFolderToProject` or `RemoveFromProject`. - this.update_history(cx); - } - - project::Event::DisconnectedFromHost => { - this.update_window_edited(window, cx); - let leaders_to_unfollow = - this.follower_states.keys().copied().collect::>(); - for leader_id in leaders_to_unfollow { - this.unfollow(leader_id, window, cx); - } - } - - project::Event::DisconnectedFromSshRemote => { - this.update_window_edited(window, cx); - } - - project::Event::Closed => { - window.remove_window(); - } - - project::Event::DeletedEntry(_, entry_id) => { - for pane in this.panes.iter() { - pane.update(cx, |pane, cx| { - pane.handle_deleted_project_item(*entry_id, window, cx) - }); - } - } - - project::Event::Toast { - notification_id, - message, - } => this.show_notification( - NotificationId::named(notification_id.clone()), - cx, - |cx| cx.new(|cx| MessageNotification::new(message.clone(), cx)), - ), - - project::Event::HideToast { notification_id } => { - this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx) - } - - project::Event::LanguageServerPrompt(request) => { - struct LanguageServerPrompt; - - let mut hasher = DefaultHasher::new(); - request.lsp_name.as_str().hash(&mut hasher); - let id = hasher.finish(); - - this.show_notification( - NotificationId::composite::(id as usize), - cx, - |cx| { - cx.new(|cx| { - notifications::LanguageServerPrompt::new(request.clone(), cx) - }) - }, - ); - } - - project::Event::AgentLocationChanged => { - this.handle_agent_location_changed(window, cx) - } - - _ => {} - } - cx.notify() - }) - .detach(); - - cx.subscribe_in( - &project.read(cx).breakpoint_store(), - window, - |workspace, _, event, window, cx| match event { - BreakpointStoreEvent::BreakpointsUpdated(_, _) - | BreakpointStoreEvent::BreakpointsCleared(_) => { - workspace.serialize_workspace(window, cx); - } - BreakpointStoreEvent::SetDebugLine | BreakpointStoreEvent::ClearDebugLines => {} - }, - ) - .detach(); - if let Some(toolchain_store) = project.read(cx).toolchain_store() { - cx.subscribe_in( - &toolchain_store, - window, - |workspace, _, event, window, cx| match event { - ToolchainStoreEvent::CustomToolchainsModified => { - workspace.serialize_workspace(window, cx); - } - _ => {} - }, - ) - .detach(); - } - - cx.on_focus_lost(window, |this, window, cx| { - let focus_handle = this.focus_handle(cx); - window.focus(&focus_handle); - }) - .detach(); - - let weak_handle = cx.entity().downgrade(); - let pane_history_timestamp = Arc::new(AtomicUsize::new(0)); - - let center_pane = cx.new(|cx| { - let mut center_pane = Pane::new( - weak_handle.clone(), - project.clone(), - pane_history_timestamp.clone(), - None, - NewFile.boxed_clone(), - true, - window, - cx, - ); - center_pane.set_can_split(Some(Arc::new(|_, _, _, _| true))); - center_pane - }); - cx.subscribe_in(¢er_pane, window, Self::handle_pane_event) - .detach(); - - window.focus(¢er_pane.focus_handle(cx)); - - cx.emit(Event::PaneAdded(center_pane.clone())); - - let window_handle = window.window_handle().downcast::().unwrap(); - app_state.workspace_store.update(cx, |store, _| { - store.workspaces.insert(window_handle); - }); - - let mut current_user = app_state.user_store.read(cx).watch_current_user(); - let mut connection_status = app_state.client.status(); - let _observe_current_user = cx.spawn_in(window, async move |this, cx| { - current_user.next().await; - connection_status.next().await; - let mut stream = - Stream::map(current_user, drop).merge(Stream::map(connection_status, drop)); - - while stream.recv().await.is_some() { - this.update(cx, |_, cx| cx.notify())?; - } - anyhow::Ok(()) - }); - - // All leader updates are enqueued and then processed in a single task, so - // that each asynchronous operation can be run in order. - let (leader_updates_tx, mut leader_updates_rx) = - mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>(); - let _apply_leader_updates = cx.spawn_in(window, async move |this, cx| { - while let Some((leader_id, update)) = leader_updates_rx.next().await { - Self::process_leader_update(&this, leader_id, update, cx) - .await - .log_err(); - } - - Ok(()) - }); - - cx.emit(Event::WorkspaceCreated(weak_handle.clone())); - let modal_layer = cx.new(|_| ModalLayer::new()); - let toast_layer = cx.new(|_| ToastLayer::new()); - cx.subscribe( - &modal_layer, - |_, _, _: &modal_layer::ModalOpenedEvent, cx| { - cx.emit(Event::ModalOpened); - }, - ) - .detach(); - - let left_dock = Dock::new(DockPosition::Left, modal_layer.clone(), window, cx); - let bottom_dock = Dock::new(DockPosition::Bottom, modal_layer.clone(), window, cx); - let right_dock = Dock::new(DockPosition::Right, modal_layer.clone(), window, cx); - let left_dock_buttons = cx.new(|cx| PanelButtons::new(left_dock.clone(), cx)); - let bottom_dock_buttons = cx.new(|cx| PanelButtons::new(bottom_dock.clone(), cx)); - let right_dock_buttons = cx.new(|cx| PanelButtons::new(right_dock.clone(), cx)); - let status_bar = cx.new(|cx| { - let mut status_bar = StatusBar::new(¢er_pane.clone(), window, cx); - status_bar.add_left_item(left_dock_buttons, window, cx); - status_bar.add_right_item(right_dock_buttons, window, cx); - status_bar.add_right_item(bottom_dock_buttons, window, cx); - status_bar - }); - - let session_id = app_state.session.read(cx).id().to_owned(); - - let mut active_call = None; - if let Some(call) = ActiveCall::try_global(cx) { - let subscriptions = vec![cx.subscribe_in(&call, window, Self::on_active_call_event)]; - active_call = Some((call, subscriptions)); - } - - let (serializable_items_tx, serializable_items_rx) = - mpsc::unbounded::>(); - let _items_serializer = cx.spawn_in(window, async move |this, cx| { - Self::serialize_items(&this, serializable_items_rx, cx).await - }); - - let subscriptions = vec![ - cx.observe_window_activation(window, Self::on_window_activation_changed), - cx.observe_window_bounds(window, move |this, window, cx| { - if this.bounds_save_task_queued.is_some() { - return; - } - this.bounds_save_task_queued = Some(cx.spawn_in(window, async move |this, cx| { - cx.background_executor() - .timer(Duration::from_millis(100)) - .await; - this.update_in(cx, |this, window, cx| { - if let Some(display) = window.display(cx) - && let Ok(display_uuid) = display.uuid() - { - let window_bounds = window.inner_window_bounds(); - if let Some(database_id) = workspace_id { - cx.background_executor() - .spawn(DB.set_window_open_status( - database_id, - SerializedWindowBounds(window_bounds), - display_uuid, - )) - .detach_and_log_err(cx); - } - } - this.bounds_save_task_queued.take(); - }) - .ok(); - })); - cx.notify(); - }), - cx.observe_window_appearance(window, |_, window, cx| { - let window_appearance = window.appearance(); - - *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into()); - - GlobalTheme::reload_theme(cx); - GlobalTheme::reload_icon_theme(cx); - }), - cx.on_release(move |this, cx| { - this.app_state.workspace_store.update(cx, move |store, _| { - store.workspaces.remove(&window_handle); - }) - }), - ]; - - cx.defer_in(window, |this, window, cx| { - this.update_window_title(window, cx); - this.show_initial_notifications(cx); - }); - Workspace { - weak_self: weak_handle.clone(), - zoomed: None, - zoomed_position: None, - previous_dock_drag_coordinates: None, - center: PaneGroup::new(center_pane.clone()), - panes: vec![center_pane.clone()], - panes_by_item: Default::default(), - active_pane: center_pane.clone(), - last_active_center_pane: Some(center_pane.downgrade()), - last_active_view_id: None, - status_bar, - modal_layer, - toast_layer, - titlebar_item: None, - notifications: Notifications::default(), - suppressed_notifications: HashSet::default(), - left_dock, - bottom_dock, - right_dock, - project: project.clone(), - follower_states: Default::default(), - last_leaders_by_pane: Default::default(), - dispatching_keystrokes: Default::default(), - window_edited: false, - last_window_title: None, - dirty_items: Default::default(), - active_call, - database_id: workspace_id, - app_state, - _observe_current_user, - _apply_leader_updates, - _schedule_serialize_workspace: None, - _schedule_serialize_ssh_paths: None, - leader_updates_tx, - _subscriptions: subscriptions, - pane_history_timestamp, - workspace_actions: Default::default(), - // This data will be incorrect, but it will be overwritten by the time it needs to be used. - bounds: Default::default(), - centered_layout: false, - bounds_save_task_queued: None, - on_prompt_for_new_path: None, - on_prompt_for_open_path: None, - terminal_provider: None, - debugger_provider: None, - serializable_items_tx, - _items_serializer, - session_id: Some(session_id), - - scheduled_tasks: Vec::new(), - last_open_dock_positions: Vec::new(), - removing: false, - } - } - - pub fn new_local( - abs_paths: Vec, - app_state: Arc, - requesting_window: Option>, - env: Option>, - cx: &mut App, - ) -> Task< - anyhow::Result<( - WindowHandle, - Vec>>>, - )>, - > { - let project_handle = Project::local( - app_state.client.clone(), - app_state.node_runtime.clone(), - app_state.user_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - env, - cx, - ); - - cx.spawn(async move |cx| { - let mut paths_to_open = Vec::with_capacity(abs_paths.len()); - for path in abs_paths.into_iter() { - if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() { - paths_to_open.push(canonical) - } else { - paths_to_open.push(path) - } - } - - let serialized_workspace = - persistence::DB.workspace_for_roots(paths_to_open.as_slice()); - - if let Some(paths) = serialized_workspace.as_ref().map(|ws| &ws.paths) { - paths_to_open = paths.ordered_paths().cloned().collect(); - if !paths.is_lexicographically_ordered() { - project_handle - .update(cx, |project, cx| { - project.set_worktrees_reordered(true, cx); - }) - .log_err(); - } - } - - // Get project paths for all of the abs_paths - let mut project_paths: Vec<(PathBuf, Option)> = - Vec::with_capacity(paths_to_open.len()); - - for path in paths_to_open.into_iter() { - if let Some((_, project_entry)) = cx - .update(|cx| { - Workspace::project_path_for_path(project_handle.clone(), &path, true, cx) - })? - .await - .log_err() - { - project_paths.push((path, Some(project_entry))); - } else { - project_paths.push((path, None)); - } - } - - let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() { - serialized_workspace.id - } else { - DB.next_id().await.unwrap_or_else(|_| Default::default()) - }; - - let toolchains = DB.toolchains(workspace_id).await?; - - for (toolchain, worktree_id, path) in toolchains { - let toolchain_path = PathBuf::from(toolchain.path.clone().to_string()); - if !app_state.fs.is_file(toolchain_path.as_path()).await { - continue; - } - - project_handle - .update(cx, |this, cx| { - this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx) - })? - .await; - } - if let Some(workspace) = serialized_workspace.as_ref() { - project_handle.update(cx, |this, cx| { - for (scope, toolchains) in &workspace.user_toolchains { - for toolchain in toolchains { - this.add_toolchain(toolchain.clone(), scope.clone(), cx); - } - } - })?; - } - - let window = if let Some(window) = requesting_window { - let centered_layout = serialized_workspace - .as_ref() - .map(|w| w.centered_layout) - .unwrap_or(false); - - cx.update_window(window.into(), |_, window, cx| { - window.replace_root(cx, |window, cx| { - let mut workspace = Workspace::new( - Some(workspace_id), - project_handle.clone(), - app_state.clone(), - window, - cx, - ); - - workspace.centered_layout = centered_layout; - workspace - }); - })?; - window - } else { - let window_bounds_override = window_bounds_env_override(); - - let (window_bounds, display) = if let Some(bounds) = window_bounds_override { - (Some(WindowBounds::Windowed(bounds)), None) - } else if let Some(workspace) = serialized_workspace.as_ref() { - // Reopening an existing workspace - restore its saved bounds - if let (Some(display), Some(bounds)) = - (workspace.display, workspace.window_bounds.as_ref()) - { - (Some(bounds.0), Some(display)) - } else { - (None, None) - } - } else { - // New window - let GPUI's default_bounds() handle cascading - (None, None) - }; - - // Use the serialized workspace to construct the new window - let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx))?; - options.window_bounds = window_bounds; - let centered_layout = serialized_workspace - .as_ref() - .map(|w| w.centered_layout) - .unwrap_or(false); - cx.open_window(options, { - let app_state = app_state.clone(); - let project_handle = project_handle.clone(); - move |window, cx| { - cx.new(|cx| { - let mut workspace = Workspace::new( - Some(workspace_id), - project_handle, - app_state, - window, - cx, - ); - workspace.centered_layout = centered_layout; - workspace - }) - } - })? - }; - - notify_if_database_failed(window, cx); - let opened_items = window - .update(cx, |_workspace, window, cx| { - open_items(serialized_workspace, project_paths, window, cx) - })? - .await - .unwrap_or_default(); - - window - .update(cx, |workspace, window, cx| { - window.activate_window(); - workspace.update_history(cx); - }) - .log_err(); - Ok((window, opened_items)) - }) - } - - pub fn weak_handle(&self) -> WeakEntity { - self.weak_self.clone() - } - - pub fn left_dock(&self) -> &Entity { - &self.left_dock - } - - pub fn bottom_dock(&self) -> &Entity { - &self.bottom_dock - } - - pub fn set_bottom_dock_layout( - &mut self, - layout: BottomDockLayout, - window: &mut Window, - cx: &mut Context, - ) { - let fs = self.project().read(cx).fs(); - settings::update_settings_file(fs.clone(), cx, move |content, _cx| { - content.workspace.bottom_dock_layout = Some(layout); - }); - - cx.notify(); - self.serialize_workspace(window, cx); - } - - pub fn right_dock(&self) -> &Entity { - &self.right_dock - } - - pub fn all_docks(&self) -> [&Entity; 3] { - [&self.left_dock, &self.bottom_dock, &self.right_dock] - } - - pub fn dock_at_position(&self, position: DockPosition) -> &Entity { - match position { - DockPosition::Left => &self.left_dock, - DockPosition::Bottom => &self.bottom_dock, - DockPosition::Right => &self.right_dock, - } - } - - pub fn is_edited(&self) -> bool { - self.window_edited - } - - pub fn add_panel( - &mut self, - panel: Entity, - window: &mut Window, - cx: &mut Context, - ) { - let focus_handle = panel.panel_focus_handle(cx); - cx.on_focus_in(&focus_handle, window, Self::handle_panel_focused) - .detach(); - - let dock_position = panel.position(window, cx); - let dock = self.dock_at_position(dock_position); - - dock.update(cx, |dock, cx| { - dock.add_panel(panel, self.weak_self.clone(), window, cx) - }); - } - - pub fn remove_panel( - &mut self, - panel: &Entity, - window: &mut Window, - cx: &mut Context, - ) { - for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { - dock.update(cx, |dock, cx| { - dock.remove_panel(panel, window, cx); - }) - } - } - - pub fn status_bar(&self) -> &Entity { - &self.status_bar - } - - pub fn status_bar_visible(&self, cx: &App) -> bool { - StatusBarSettings::get_global(cx).show - } - - pub fn app_state(&self) -> &Arc { - &self.app_state - } - - pub fn user_store(&self) -> &Entity { - &self.app_state.user_store - } - - pub fn project(&self) -> &Entity { - &self.project - } - - pub fn path_style(&self, cx: &App) -> PathStyle { - self.project.read(cx).path_style(cx) - } - - pub fn recently_activated_items(&self, cx: &App) -> HashMap { - let mut history: HashMap = HashMap::default(); - - for pane_handle in &self.panes { - let pane = pane_handle.read(cx); - - for entry in pane.activation_history() { - history.insert( - entry.entity_id, - history - .get(&entry.entity_id) - .cloned() - .unwrap_or(0) - .max(entry.timestamp), - ); - } - } - - history - } - - pub fn recent_active_item_by_type(&self, cx: &App) -> Option> { - let mut recent_item: Option> = None; - let mut recent_timestamp = 0; - for pane_handle in &self.panes { - let pane = pane_handle.read(cx); - let item_map: HashMap> = - pane.items().map(|item| (item.item_id(), item)).collect(); - for entry in pane.activation_history() { - if entry.timestamp > recent_timestamp - && let Some(&item) = item_map.get(&entry.entity_id) - && let Some(typed_item) = item.act_as::(cx) - { - recent_timestamp = entry.timestamp; - recent_item = Some(typed_item); - } - } - } - recent_item - } - - pub fn recent_navigation_history_iter( - &self, - cx: &App, - ) -> impl Iterator)> + use<> { - let mut abs_paths_opened: HashMap> = HashMap::default(); - let mut history: HashMap, usize)> = HashMap::default(); - - for pane in &self.panes { - let pane = pane.read(cx); - - pane.nav_history() - .for_each_entry(cx, |entry, (project_path, fs_path)| { - if let Some(fs_path) = &fs_path { - abs_paths_opened - .entry(fs_path.clone()) - .or_default() - .insert(project_path.clone()); - } - let timestamp = entry.timestamp; - match history.entry(project_path) { - hash_map::Entry::Occupied(mut entry) => { - let (_, old_timestamp) = entry.get(); - if ×tamp > old_timestamp { - entry.insert((fs_path, timestamp)); - } - } - hash_map::Entry::Vacant(entry) => { - entry.insert((fs_path, timestamp)); - } - } - }); - - if let Some(item) = pane.active_item() - && let Some(project_path) = item.project_path(cx) - { - let fs_path = self.project.read(cx).absolute_path(&project_path, cx); - - if let Some(fs_path) = &fs_path { - abs_paths_opened - .entry(fs_path.clone()) - .or_default() - .insert(project_path.clone()); - } - - history.insert(project_path, (fs_path, std::usize::MAX)); - } - } - - history - .into_iter() - .sorted_by_key(|(_, (_, order))| *order) - .map(|(project_path, (fs_path, _))| (project_path, fs_path)) - .rev() - .filter(move |(history_path, abs_path)| { - let latest_project_path_opened = abs_path - .as_ref() - .and_then(|abs_path| abs_paths_opened.get(abs_path)) - .and_then(|project_paths| { - project_paths - .iter() - .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id)) - }); - - latest_project_path_opened.is_none_or(|path| path == history_path) - }) - } - - pub fn recent_navigation_history( - &self, - limit: Option, - cx: &App, - ) -> Vec<(ProjectPath, Option)> { - self.recent_navigation_history_iter(cx) - .take(limit.unwrap_or(usize::MAX)) - .collect() - } - - pub fn clear_navigation_history(&mut self, _window: &mut Window, cx: &mut Context) { - for pane in &self.panes { - pane.update(cx, |pane, cx| pane.nav_history_mut().clear(cx)); - } - } - - fn navigate_history( - &mut self, - pane: WeakEntity, - mode: NavigationMode, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let to_load = if let Some(pane) = pane.upgrade() { - pane.update(cx, |pane, cx| { - window.focus(&pane.focus_handle(cx)); - loop { - // Retrieve the weak item handle from the history. - let entry = pane.nav_history_mut().pop(mode, cx)?; - - // If the item is still present in this pane, then activate it. - if let Some(index) = entry - .item - .upgrade() - .and_then(|v| pane.index_for_item(v.as_ref())) - { - let prev_active_item_index = pane.active_item_index(); - pane.nav_history_mut().set_mode(mode); - pane.activate_item(index, true, true, window, cx); - pane.nav_history_mut().set_mode(NavigationMode::Normal); - - let mut navigated = prev_active_item_index != pane.active_item_index(); - if let Some(data) = entry.data { - navigated |= pane.active_item()?.navigate(data, window, cx); - } - - if navigated { - break None; - } - } else { - // If the item is no longer present in this pane, then retrieve its - // path info in order to reopen it. - break pane - .nav_history() - .path_for_item(entry.item.id()) - .map(|(project_path, abs_path)| (project_path, abs_path, entry)); - } - } - }) - } else { - None - }; - - if let Some((project_path, abs_path, entry)) = to_load { - // If the item was no longer present, then load it again from its previous path, first try the local path - let open_by_project_path = self.load_path(project_path.clone(), window, cx); - - cx.spawn_in(window, async move |workspace, cx| { - let open_by_project_path = open_by_project_path.await; - let mut navigated = false; - match open_by_project_path - .with_context(|| format!("Navigating to {project_path:?}")) - { - Ok((project_entry_id, build_item)) => { - let prev_active_item_id = pane.update(cx, |pane, _| { - pane.nav_history_mut().set_mode(mode); - pane.active_item().map(|p| p.item_id()) - })?; - - pane.update_in(cx, |pane, window, cx| { - let item = pane.open_item( - project_entry_id, - project_path, - true, - entry.is_preview, - true, - None, - window, cx, - build_item, - ); - navigated |= Some(item.item_id()) != prev_active_item_id; - pane.nav_history_mut().set_mode(NavigationMode::Normal); - if let Some(data) = entry.data { - navigated |= item.navigate(data, window, cx); - } - })?; - } - Err(open_by_project_path_e) => { - // Fall back to opening by abs path, in case an external file was opened and closed, - // and its worktree is now dropped - if let Some(abs_path) = abs_path { - let prev_active_item_id = pane.update(cx, |pane, _| { - pane.nav_history_mut().set_mode(mode); - pane.active_item().map(|p| p.item_id()) - })?; - let open_by_abs_path = workspace.update_in(cx, |workspace, window, cx| { - workspace.open_abs_path(abs_path.clone(), OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx) - })?; - match open_by_abs_path - .await - .with_context(|| format!("Navigating to {abs_path:?}")) - { - Ok(item) => { - pane.update_in(cx, |pane, window, cx| { - navigated |= Some(item.item_id()) != prev_active_item_id; - pane.nav_history_mut().set_mode(NavigationMode::Normal); - if let Some(data) = entry.data { - navigated |= item.navigate(data, window, cx); - } - })?; - } - Err(open_by_abs_path_e) => { - log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}"); - } - } - } - } - } - - if !navigated { - workspace - .update_in(cx, |workspace, window, cx| { - Self::navigate_history(workspace, pane, mode, window, cx) - })? - .await?; - } - - Ok(()) - }) - } else { - Task::ready(Ok(())) - } - } - - pub fn go_back( - &mut self, - pane: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.navigate_history(pane, NavigationMode::GoingBack, window, cx) - } - - pub fn go_forward( - &mut self, - pane: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.navigate_history(pane, NavigationMode::GoingForward, window, cx) - } - - pub fn reopen_closed_item( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.navigate_history( - self.active_pane().downgrade(), - NavigationMode::ReopeningClosedItem, - window, - cx, - ) - } - - pub fn client(&self) -> &Arc { - &self.app_state.client - } - - pub fn set_titlebar_item(&mut self, item: AnyView, _: &mut Window, cx: &mut Context) { - self.titlebar_item = Some(item); - cx.notify(); - } - - pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) { - self.on_prompt_for_new_path = Some(prompt) - } - - pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) { - self.on_prompt_for_open_path = Some(prompt) - } - - pub fn set_terminal_provider(&mut self, provider: impl TerminalProvider + 'static) { - self.terminal_provider = Some(Box::new(provider)); - } - - pub fn set_debugger_provider(&mut self, provider: impl DebuggerProvider + 'static) { - self.debugger_provider = Some(Arc::new(provider)); - } - - pub fn debugger_provider(&self) -> Option> { - self.debugger_provider.clone() - } - - pub fn prompt_for_open_path( - &mut self, - path_prompt_options: PathPromptOptions, - lister: DirectoryLister, - window: &mut Window, - cx: &mut Context, - ) -> oneshot::Receiver>> { - if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts { - let prompt = self.on_prompt_for_open_path.take().unwrap(); - let rx = prompt(self, lister, window, cx); - self.on_prompt_for_open_path = Some(prompt); - rx - } else { - let (tx, rx) = oneshot::channel(); - let abs_path = cx.prompt_for_paths(path_prompt_options); - - cx.spawn_in(window, async move |workspace, cx| { - let Ok(result) = abs_path.await else { - return Ok(()); - }; - - match result { - Ok(result) => { - tx.send(result).ok(); - } - Err(err) => { - let rx = workspace.update_in(cx, |workspace, window, cx| { - workspace.show_portal_error(err.to_string(), cx); - let prompt = workspace.on_prompt_for_open_path.take().unwrap(); - let rx = prompt(workspace, lister, window, cx); - workspace.on_prompt_for_open_path = Some(prompt); - rx - })?; - if let Ok(path) = rx.await { - tx.send(path).ok(); - } - } - }; - anyhow::Ok(()) - }) - .detach(); - - rx - } - } - - pub fn prompt_for_new_path( - &mut self, - lister: DirectoryLister, - suggested_name: Option, - window: &mut Window, - cx: &mut Context, - ) -> oneshot::Receiver>> { - if self.project.read(cx).is_via_collab() - || self.project.read(cx).is_via_remote_server() - || !WorkspaceSettings::get_global(cx).use_system_path_prompts - { - let prompt = self.on_prompt_for_new_path.take().unwrap(); - let rx = prompt(self, lister, window, cx); - self.on_prompt_for_new_path = Some(prompt); - return rx; - } - - let (tx, rx) = oneshot::channel(); - cx.spawn_in(window, async move |workspace, cx| { - let abs_path = workspace.update(cx, |workspace, cx| { - let relative_to = workspace - .most_recent_active_path(cx) - .and_then(|p| p.parent().map(|p| p.to_path_buf())) - .or_else(|| { - let project = workspace.project.read(cx); - project.visible_worktrees(cx).find_map(|worktree| { - Some(worktree.read(cx).as_local()?.abs_path().to_path_buf()) - }) - }) - .or_else(std::env::home_dir) - .unwrap_or_else(|| PathBuf::from("")); - cx.prompt_for_new_path(&relative_to, suggested_name.as_deref()) - })?; - let abs_path = match abs_path.await? { - Ok(path) => path, - Err(err) => { - let rx = workspace.update_in(cx, |workspace, window, cx| { - workspace.show_portal_error(err.to_string(), cx); - - let prompt = workspace.on_prompt_for_new_path.take().unwrap(); - let rx = prompt(workspace, lister, window, cx); - workspace.on_prompt_for_new_path = Some(prompt); - rx - })?; - if let Ok(path) = rx.await { - tx.send(path).ok(); - } - return anyhow::Ok(()); - } - }; - - tx.send(abs_path.map(|path| vec![path])).ok(); - anyhow::Ok(()) - }) - .detach(); - - rx - } - - pub fn titlebar_item(&self) -> Option { - self.titlebar_item.clone() - } - - /// Call the given callback with a workspace whose project is local. - /// - /// If the given workspace has a local project, then it will be passed - /// to the callback. Otherwise, a new empty window will be created. - pub fn with_local_workspace( - &mut self, - window: &mut Window, - cx: &mut Context, - callback: F, - ) -> Task> - where - T: 'static, - F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context) -> T, - { - if self.project.read(cx).is_local() { - Task::ready(Ok(callback(self, window, cx))) - } else { - let env = self.project.read(cx).cli_environment(cx); - let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, cx); - cx.spawn_in(window, async move |_vh, cx| { - let (workspace, _) = task.await?; - workspace.update(cx, callback) - }) - } - } - - pub fn worktrees<'a>(&self, cx: &'a App) -> impl 'a + Iterator> { - self.project.read(cx).worktrees(cx) - } - - pub fn visible_worktrees<'a>( - &self, - cx: &'a App, - ) -> impl 'a + Iterator> { - self.project.read(cx).visible_worktrees(cx) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn worktree_scans_complete(&self, cx: &App) -> impl Future + 'static + use<> { - let futures = self - .worktrees(cx) - .filter_map(|worktree| worktree.read(cx).as_local()) - .map(|worktree| worktree.scan_complete()) - .collect::>(); - async move { - for future in futures { - future.await; - } - } - } - - pub fn close_global(cx: &mut App) { - cx.defer(|cx| { - cx.windows().iter().find(|window| { - window - .update(cx, |_, window, _| { - if window.is_window_active() { - //This can only get called when the window's project connection has been lost - //so we don't need to prompt the user for anything and instead just close the window - window.remove_window(); - true - } else { - false - } - }) - .unwrap_or(false) - }); - }); - } - - pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context) { - let prepare = self.prepare_to_close(CloseIntent::CloseWindow, window, cx); - cx.spawn_in(window, async move |_, cx| { - if prepare.await? { - cx.update(|window, _cx| window.remove_window())?; - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx) - } - - pub fn move_focused_panel_to_next_position( - &mut self, - _: &MoveFocusedPanelToNextPosition, - window: &mut Window, - cx: &mut Context, - ) { - let docks = self.all_docks(); - let active_dock = docks - .into_iter() - .find(|dock| dock.focus_handle(cx).contains_focused(window, cx)); - - if let Some(dock) = active_dock { - dock.update(cx, |dock, cx| { - let active_panel = dock - .active_panel() - .filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx)); - - if let Some(panel) = active_panel { - panel.move_to_next_position(window, cx); - } - }) - } - } - - pub fn prepare_to_close( - &mut self, - close_intent: CloseIntent, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let active_call = self.active_call().cloned(); - - cx.spawn_in(window, async move |this, cx| { - this.update(cx, |this, _| { - if close_intent == CloseIntent::CloseWindow { - this.removing = true; - } - })?; - - let workspace_count = cx.update(|_window, cx| { - cx.windows() - .iter() - .filter(|window| window.downcast::().is_some()) - .count() - })?; - - #[cfg(target_os = "macos")] - let save_last_workspace = false; - - // On Linux and Windows, closing the last window should restore the last workspace. - #[cfg(not(target_os = "macos"))] - let save_last_workspace = { - let remaining_workspaces = cx.update(|_window, cx| { - cx.windows() - .iter() - .filter_map(|window| window.downcast::()) - .filter_map(|workspace| { - workspace - .update(cx, |workspace, _, _| workspace.removing) - .ok() - }) - .filter(|removing| !removing) - .count() - })?; - - close_intent != CloseIntent::ReplaceWindow && remaining_workspaces == 0 - }; - - if let Some(active_call) = active_call - && workspace_count == 1 - && active_call.read_with(cx, |call, _| call.room().is_some())? - { - if close_intent == CloseIntent::CloseWindow { - let answer = cx.update(|window, cx| { - window.prompt( - PromptLevel::Warning, - "Do you want to leave the current call?", - None, - &["Close window and hang up", "Cancel"], - cx, - ) - })?; - - if answer.await.log_err() == Some(1) { - return anyhow::Ok(false); - } else { - active_call - .update(cx, |call, cx| call.hang_up(cx))? - .await - .log_err(); - } - } - if close_intent == CloseIntent::ReplaceWindow { - _ = active_call.update(cx, |this, cx| { - let workspace = cx - .windows() - .iter() - .filter_map(|window| window.downcast::()) - .next() - .unwrap(); - let project = workspace.read(cx)?.project.clone(); - if project.read(cx).is_shared() { - this.unshare_project(project, cx)?; - } - Ok::<_, anyhow::Error>(()) - })?; - } - } - - let save_result = this - .update_in(cx, |this, window, cx| { - this.save_all_internal(SaveIntent::Close, window, cx) - })? - .await; - - // If we're not quitting, but closing, we remove the workspace from - // the current session. - if close_intent != CloseIntent::Quit - && !save_last_workspace - && save_result.as_ref().is_ok_and(|&res| res) - { - this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx))? - .await; - } - - save_result - }) - } - - fn save_all(&mut self, action: &SaveAll, window: &mut Window, cx: &mut Context) { - self.save_all_internal( - action.save_intent.unwrap_or(SaveIntent::SaveAll), - window, - cx, - ) - .detach_and_log_err(cx); - } - - fn send_keystrokes( - &mut self, - action: &SendKeystrokes, - window: &mut Window, - cx: &mut Context, - ) { - let keystrokes: Vec = action - .0 - .split(' ') - .flat_map(|k| Keystroke::parse(k).log_err()) - .map(|k| { - cx.keyboard_mapper() - .map_key_equivalent(k, true) - .inner() - .clone() - }) - .collect(); - let _ = self.send_keystrokes_impl(keystrokes, window, cx); - } - - pub fn send_keystrokes_impl( - &mut self, - keystrokes: Vec, - window: &mut Window, - cx: &mut Context, - ) -> Shared> { - let mut state = self.dispatching_keystrokes.borrow_mut(); - if !state.dispatched.insert(keystrokes.clone()) { - cx.propagate(); - return state.task.clone().unwrap(); - } - - state.queue.extend(keystrokes); - - let keystrokes = self.dispatching_keystrokes.clone(); - if state.task.is_none() { - state.task = Some( - window - .spawn(cx, async move |cx| { - // limit to 100 keystrokes to avoid infinite recursion. - for _ in 0..100 { - let mut state = keystrokes.borrow_mut(); - let Some(keystroke) = state.queue.pop_front() else { - state.dispatched.clear(); - state.task.take(); - return; - }; - drop(state); - cx.update(|window, cx| { - let focused = window.focused(cx); - window.dispatch_keystroke(keystroke.clone(), cx); - if window.focused(cx) != focused { - // dispatch_keystroke may cause the focus to change. - // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle - // And we need that to happen before the next keystroke to keep vim mode happy... - // (Note that the tests always do this implicitly, so you must manually test with something like: - // "bindings": { "g z": ["workspace::SendKeystrokes", ": j u"]} - // ) - window.draw(cx).clear(); - } - }) - .ok(); - } - - *keystrokes.borrow_mut() = Default::default(); - log::error!("over 100 keystrokes passed to send_keystrokes"); - }) - .shared(), - ); - } - state.task.clone().unwrap() - } - - fn save_all_internal( - &mut self, - mut save_intent: SaveIntent, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - if self.project.read(cx).is_disconnected(cx) { - return Task::ready(Ok(true)); - } - let dirty_items = self - .panes - .iter() - .flat_map(|pane| { - pane.read(cx).items().filter_map(|item| { - if item.is_dirty(cx) { - item.tab_content_text(0, cx); - Some((pane.downgrade(), item.boxed_clone())) - } else { - None - } - }) - }) - .collect::>(); - - let project = self.project.clone(); - cx.spawn_in(window, async move |workspace, cx| { - let dirty_items = if save_intent == SaveIntent::Close && !dirty_items.is_empty() { - let (serialize_tasks, remaining_dirty_items) = - workspace.update_in(cx, |workspace, window, cx| { - let mut remaining_dirty_items = Vec::new(); - let mut serialize_tasks = Vec::new(); - for (pane, item) in dirty_items { - if let Some(task) = item - .to_serializable_item_handle(cx) - .and_then(|handle| handle.serialize(workspace, true, window, cx)) - { - serialize_tasks.push(task); - } else { - remaining_dirty_items.push((pane, item)); - } - } - (serialize_tasks, remaining_dirty_items) - })?; - - futures::future::try_join_all(serialize_tasks).await?; - - if remaining_dirty_items.len() > 1 { - let answer = workspace.update_in(cx, |_, window, cx| { - let detail = Pane::file_names_for_prompt( - &mut remaining_dirty_items.iter().map(|(_, handle)| handle), - cx, - ); - window.prompt( - PromptLevel::Warning, - "Do you want to save all changes in the following files?", - Some(&detail), - &["Save all", "Discard all", "Cancel"], - cx, - ) - })?; - match answer.await.log_err() { - Some(0) => save_intent = SaveIntent::SaveAll, - Some(1) => save_intent = SaveIntent::Skip, - Some(2) => return Ok(false), - _ => {} - } - } - - remaining_dirty_items - } else { - dirty_items - }; - - for (pane, item) in dirty_items { - let (singleton, project_entry_ids) = cx.update(|_, cx| { - ( - item.buffer_kind(cx) == ItemBufferKind::Singleton, - item.project_entry_ids(cx), - ) - })?; - if (singleton || !project_entry_ids.is_empty()) - && !Pane::save_item(project.clone(), &pane, &*item, save_intent, cx).await? - { - return Ok(false); - } - } - Ok(true) - }) - } - - pub fn open_workspace_for_paths( - &mut self, - replace_current_window: bool, - paths: Vec, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let window_handle = window.window_handle().downcast::(); - let is_remote = self.project.read(cx).is_via_collab(); - let has_worktree = self.project.read(cx).worktrees(cx).next().is_some(); - let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx)); - - let window_to_replace = if replace_current_window { - window_handle - } else if is_remote || has_worktree || has_dirty_items { - None - } else { - window_handle - }; - let app_state = self.app_state.clone(); - - cx.spawn(async move |_, cx| { - cx.update(|cx| { - open_paths( - &paths, - app_state, - OpenOptions { - replace_window: window_to_replace, - ..Default::default() - }, - cx, - ) - })? - .await?; - Ok(()) - }) - } - - #[allow(clippy::type_complexity)] - pub fn open_paths( - &mut self, - mut abs_paths: Vec, - options: OpenOptions, - pane: Option>, - window: &mut Window, - cx: &mut Context, - ) -> Task>>>> { - let fs = self.app_state.fs.clone(); - - // Sort the paths to ensure we add worktrees for parents before their children. - abs_paths.sort_unstable(); - cx.spawn_in(window, async move |this, cx| { - let mut tasks = Vec::with_capacity(abs_paths.len()); - - for abs_path in &abs_paths { - let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) { - OpenVisible::All => Some(true), - OpenVisible::None => Some(false), - OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() { - Some(Some(metadata)) => Some(!metadata.is_dir), - Some(None) => Some(true), - None => None, - }, - OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() { - Some(Some(metadata)) => Some(metadata.is_dir), - Some(None) => Some(false), - None => None, - }, - }; - let project_path = match visible { - Some(visible) => match this - .update(cx, |this, cx| { - Workspace::project_path_for_path( - this.project.clone(), - abs_path, - visible, - cx, - ) - }) - .log_err() - { - Some(project_path) => project_path.await.log_err(), - None => None, - }, - None => None, - }; - - let this = this.clone(); - let abs_path: Arc = SanitizedPath::new(&abs_path).as_path().into(); - let fs = fs.clone(); - let pane = pane.clone(); - let task = cx.spawn(async move |cx| { - let (worktree, project_path) = project_path?; - if fs.is_dir(&abs_path).await { - this.update(cx, |workspace, cx| { - let worktree = worktree.read(cx); - let worktree_abs_path = worktree.abs_path(); - let entry_id = if abs_path.as_ref() == worktree_abs_path.as_ref() { - worktree.root_entry() - } else { - abs_path - .strip_prefix(worktree_abs_path.as_ref()) - .ok() - .and_then(|relative_path| { - let relative_path = - RelPath::new(relative_path, PathStyle::local()) - .log_err()?; - worktree.entry_for_path(&relative_path) - }) - } - .map(|entry| entry.id); - if let Some(entry_id) = entry_id { - workspace.project.update(cx, |_, cx| { - cx.emit(project::Event::ActiveEntryChanged(Some(entry_id))); - }) - } - }) - .ok()?; - None - } else { - Some( - this.update_in(cx, |this, window, cx| { - this.open_path( - project_path, - pane, - options.focus.unwrap_or(true), - window, - cx, - ) - }) - .ok()? - .await, - ) - } - }); - tasks.push(task); - } - - futures::future::join_all(tasks).await - }) - } - - pub fn open_resolved_path( - &mut self, - path: ResolvedPath, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - match path { - ResolvedPath::ProjectPath { project_path, .. } => { - self.open_path(project_path, None, true, window, cx) - } - ResolvedPath::AbsPath { path, .. } => self.open_abs_path( - PathBuf::from(path), - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - window, - cx, - ), - } - } - - pub fn absolute_path_of_worktree( - &self, - worktree_id: WorktreeId, - cx: &mut Context, - ) -> Option { - self.project - .read(cx) - .worktree_for_id(worktree_id, cx) - // TODO: use `abs_path` or `root_dir` - .map(|wt| wt.read(cx).abs_path().as_ref().to_path_buf()) - } - - fn add_folder_to_project( - &mut self, - _: &AddFolderToProject, - window: &mut Window, - cx: &mut Context, - ) { - let project = self.project.read(cx); - if project.is_via_collab() { - self.show_error( - &anyhow!("You cannot add folders to someone else's project"), - cx, - ); - return; - } - let paths = self.prompt_for_open_path( - PathPromptOptions { - files: false, - directories: true, - multiple: true, - prompt: None, - }, - DirectoryLister::Project(self.project.clone()), - window, - cx, - ); - cx.spawn_in(window, async move |this, cx| { - if let Some(paths) = paths.await.log_err().flatten() { - let results = this - .update_in(cx, |this, window, cx| { - this.open_paths( - paths, - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - None, - window, - cx, - ) - })? - .await; - for result in results.into_iter().flatten() { - result.log_err(); - } - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - - pub fn project_path_for_path( - project: Entity, - abs_path: &Path, - visible: bool, - cx: &mut App, - ) -> Task, ProjectPath)>> { - let entry = project.update(cx, |project, cx| { - project.find_or_create_worktree(abs_path, visible, cx) - }); - cx.spawn(async move |cx| { - let (worktree, path) = entry.await?; - let worktree_id = worktree.read_with(cx, |t, _| t.id())?; - Ok(( - worktree, - ProjectPath { - worktree_id, - path: path, - }, - )) - }) - } - - pub fn items<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator> { - self.panes.iter().flat_map(|pane| pane.read(cx).items()) - } - - pub fn item_of_type(&self, cx: &App) -> Option> { - self.items_of_type(cx).max_by_key(|item| item.item_id()) - } - - pub fn items_of_type<'a, T: Item>( - &'a self, - cx: &'a App, - ) -> impl 'a + Iterator> { - self.panes - .iter() - .flat_map(|pane| pane.read(cx).items_of_type()) - } - - pub fn active_item(&self, cx: &App) -> Option> { - self.active_pane().read(cx).active_item() - } - - pub fn active_item_as(&self, cx: &App) -> Option> { - let item = self.active_item(cx)?; - item.to_any_view().downcast::().ok() - } - - fn active_project_path(&self, cx: &App) -> Option { - self.active_item(cx).and_then(|item| item.project_path(cx)) - } - - pub fn most_recent_active_path(&self, cx: &App) -> Option { - self.recent_navigation_history_iter(cx) - .filter_map(|(path, abs_path)| { - let worktree = self - .project - .read(cx) - .worktree_for_id(path.worktree_id, cx)?; - if worktree.read(cx).is_visible() { - abs_path - } else { - None - } - }) - .next() - } - - pub fn save_active_item( - &mut self, - save_intent: SaveIntent, - window: &mut Window, - cx: &mut App, - ) -> Task> { - let project = self.project.clone(); - let pane = self.active_pane(); - let item = pane.read(cx).active_item(); - let pane = pane.downgrade(); - - window.spawn(cx, async move |cx| { - if let Some(item) = item { - Pane::save_item(project, &pane, item.as_ref(), save_intent, cx) - .await - .map(|_| ()) - } else { - Ok(()) - } - }) - } - - pub fn close_inactive_items_and_panes( - &mut self, - action: &CloseInactiveTabsAndPanes, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(task) = self.close_all_internal( - true, - action.save_intent.unwrap_or(SaveIntent::Close), - window, - cx, - ) { - task.detach_and_log_err(cx) - } - } - - pub fn close_all_items_and_panes( - &mut self, - action: &CloseAllItemsAndPanes, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(task) = self.close_all_internal( - false, - action.save_intent.unwrap_or(SaveIntent::Close), - window, - cx, - ) { - task.detach_and_log_err(cx) - } - } - - fn close_all_internal( - &mut self, - retain_active_pane: bool, - save_intent: SaveIntent, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - let current_pane = self.active_pane(); - - let mut tasks = Vec::new(); - - if retain_active_pane { - let current_pane_close = current_pane.update(cx, |pane, cx| { - pane.close_other_items( - &CloseOtherItems { - save_intent: None, - close_pinned: false, - }, - None, - window, - cx, - ) - }); - - tasks.push(current_pane_close); - } - - for pane in self.panes() { - if retain_active_pane && pane.entity_id() == current_pane.entity_id() { - continue; - } - - let close_pane_items = pane.update(cx, |pane: &mut Pane, cx| { - pane.close_all_items( - &CloseAllItems { - save_intent: Some(save_intent), - close_pinned: false, - }, - window, - cx, - ) - }); - - tasks.push(close_pane_items) - } - - if tasks.is_empty() { - None - } else { - Some(cx.spawn_in(window, async move |_, _| { - for task in tasks { - task.await? - } - Ok(()) - })) - } - } - - pub fn is_dock_at_position_open(&self, position: DockPosition, cx: &mut Context) -> bool { - self.dock_at_position(position).read(cx).is_open() - } - - pub fn toggle_dock( - &mut self, - dock_side: DockPosition, - window: &mut Window, - cx: &mut Context, - ) { - let mut focus_center = false; - let mut reveal_dock = false; - - let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side); - let was_visible = self.is_dock_at_position_open(dock_side, cx) && !other_is_zoomed; - if was_visible { - self.save_open_dock_positions(cx); - } - - let dock = self.dock_at_position(dock_side); - dock.update(cx, |dock, cx| { - dock.set_open(!was_visible, window, cx); - - if dock.active_panel().is_none() { - let Some(panel_ix) = dock - .first_enabled_panel_idx(cx) - .log_with_level(log::Level::Info) - else { - return; - }; - dock.activate_panel(panel_ix, window, cx); - } - - if let Some(active_panel) = dock.active_panel() { - if was_visible { - if active_panel - .panel_focus_handle(cx) - .contains_focused(window, cx) - { - focus_center = true; - } - } else { - let focus_handle = &active_panel.panel_focus_handle(cx); - window.focus(focus_handle); - reveal_dock = true; - } - } - }); - - if reveal_dock { - self.dismiss_zoomed_items_to_reveal(Some(dock_side), window, cx); - } - - if focus_center { - self.active_pane - .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))) - } - - cx.notify(); - self.serialize_workspace(window, cx); - } - - fn active_dock(&self, window: &Window, cx: &Context) -> Option<&Entity> { - self.all_docks().into_iter().find(|&dock| { - dock.read(cx).is_open() && dock.focus_handle(cx).contains_focused(window, cx) - }) - } - - fn close_active_dock(&mut self, window: &mut Window, cx: &mut Context) -> bool { - if let Some(dock) = self.active_dock(window, cx).cloned() { - self.save_open_dock_positions(cx); - dock.update(cx, |dock, cx| { - dock.set_open(false, window, cx); - }); - return true; - } - false - } - - pub fn close_all_docks(&mut self, window: &mut Window, cx: &mut Context) { - self.save_open_dock_positions(cx); - for dock in self.all_docks() { - dock.update(cx, |dock, cx| { - dock.set_open(false, window, cx); - }); - } - - cx.focus_self(window); - cx.notify(); - self.serialize_workspace(window, cx); - } - - fn get_open_dock_positions(&self, cx: &Context) -> Vec { - self.all_docks() - .into_iter() - .filter_map(|dock| { - let dock_ref = dock.read(cx); - if dock_ref.is_open() { - Some(dock_ref.position()) - } else { - None - } - }) - .collect() - } - - /// Saves the positions of currently open docks. - /// - /// Updates `last_open_dock_positions` with positions of all currently open - /// docks, to later be restored by the 'Toggle All Docks' action. - fn save_open_dock_positions(&mut self, cx: &mut Context) { - let open_dock_positions = self.get_open_dock_positions(cx); - if !open_dock_positions.is_empty() { - self.last_open_dock_positions = open_dock_positions; - } - } - - /// Toggles all docks between open and closed states. - /// - /// If any docks are open, closes all and remembers their positions. If all - /// docks are closed, restores the last remembered dock configuration. - fn toggle_all_docks( - &mut self, - _: &ToggleAllDocks, - window: &mut Window, - cx: &mut Context, - ) { - let open_dock_positions = self.get_open_dock_positions(cx); - - if !open_dock_positions.is_empty() { - self.close_all_docks(window, cx); - } else if !self.last_open_dock_positions.is_empty() { - self.restore_last_open_docks(window, cx); - } - } - - /// Reopens docks from the most recently remembered configuration. - /// - /// Opens all docks whose positions are stored in `last_open_dock_positions` - /// and clears the stored positions. - fn restore_last_open_docks(&mut self, window: &mut Window, cx: &mut Context) { - let positions_to_open = std::mem::take(&mut self.last_open_dock_positions); - - for position in positions_to_open { - let dock = self.dock_at_position(position); - dock.update(cx, |dock, cx| dock.set_open(true, window, cx)); - } - - cx.focus_self(window); - cx.notify(); - self.serialize_workspace(window, cx); - } - - /// Transfer focus to the panel of the given type. - pub fn focus_panel( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Option> { - let panel = self.focus_or_unfocus_panel::(window, cx, |_, _, _| true)?; - panel.to_any().downcast().ok() - } - - /// Focus the panel of the given type if it isn't already focused. If it is - /// already focused, then transfer focus back to the workspace center. - pub fn toggle_panel_focus( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> bool { - let mut did_focus_panel = false; - self.focus_or_unfocus_panel::(window, cx, |panel, window, cx| { - did_focus_panel = !panel.panel_focus_handle(cx).contains_focused(window, cx); - did_focus_panel - }); - did_focus_panel - } - - pub fn activate_panel_for_proto_id( - &mut self, - panel_id: PanelId, - window: &mut Window, - cx: &mut Context, - ) -> Option> { - let mut panel = None; - for dock in self.all_docks() { - if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) { - panel = dock.update(cx, |dock, cx| { - dock.activate_panel(panel_index, window, cx); - dock.set_open(true, window, cx); - dock.active_panel().cloned() - }); - break; - } - } - - if panel.is_some() { - cx.notify(); - self.serialize_workspace(window, cx); - } - - panel - } - - /// Focus or unfocus the given panel type, depending on the given callback. - fn focus_or_unfocus_panel( - &mut self, - window: &mut Window, - cx: &mut Context, - mut should_focus: impl FnMut(&dyn PanelHandle, &mut Window, &mut Context) -> bool, - ) -> Option> { - let mut result_panel = None; - let mut serialize = false; - for dock in self.all_docks() { - if let Some(panel_index) = dock.read(cx).panel_index_for_type::() { - let mut focus_center = false; - let panel = dock.update(cx, |dock, cx| { - dock.activate_panel(panel_index, window, cx); - - let panel = dock.active_panel().cloned(); - if let Some(panel) = panel.as_ref() { - if should_focus(&**panel, window, cx) { - dock.set_open(true, window, cx); - panel.panel_focus_handle(cx).focus(window); - } else { - focus_center = true; - } - } - panel - }); - - if focus_center { - self.active_pane - .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))) - } - - result_panel = panel; - serialize = true; - break; - } - } - - if serialize { - self.serialize_workspace(window, cx); - } - - cx.notify(); - result_panel - } - - /// Open the panel of the given type - pub fn open_panel(&mut self, window: &mut Window, cx: &mut Context) { - for dock in self.all_docks() { - if let Some(panel_index) = dock.read(cx).panel_index_for_type::() { - dock.update(cx, |dock, cx| { - dock.activate_panel(panel_index, window, cx); - dock.set_open(true, window, cx); - }); - } - } - } - - pub fn close_panel(&self, window: &mut Window, cx: &mut Context) { - for dock in self.all_docks().iter() { - dock.update(cx, |dock, cx| { - if dock.panel::().is_some() { - dock.set_open(false, window, cx) - } - }) - } - } - - pub fn panel(&self, cx: &App) -> Option> { - self.all_docks() - .iter() - .find_map(|dock| dock.read(cx).panel::()) - } - - fn dismiss_zoomed_items_to_reveal( - &mut self, - dock_to_reveal: Option, - window: &mut Window, - cx: &mut Context, - ) { - // If a center pane is zoomed, unzoom it. - for pane in &self.panes { - if pane != &self.active_pane || dock_to_reveal.is_some() { - pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); - } - } - - // If another dock is zoomed, hide it. - let mut focus_center = false; - for dock in self.all_docks() { - dock.update(cx, |dock, cx| { - if Some(dock.position()) != dock_to_reveal - && let Some(panel) = dock.active_panel() - && panel.is_zoomed(window, cx) - { - focus_center |= panel.panel_focus_handle(cx).contains_focused(window, cx); - dock.set_open(false, window, cx); - } - }); - } - - if focus_center { - self.active_pane - .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))) - } - - if self.zoomed_position != dock_to_reveal { - self.zoomed = None; - self.zoomed_position = None; - cx.emit(Event::ZoomChanged); - } - - cx.notify(); - } - - fn add_pane(&mut self, window: &mut Window, cx: &mut Context) -> Entity { - let pane = cx.new(|cx| { - let mut pane = Pane::new( - self.weak_handle(), - self.project.clone(), - self.pane_history_timestamp.clone(), - None, - NewFile.boxed_clone(), - true, - window, - cx, - ); - pane.set_can_split(Some(Arc::new(|_, _, _, _| true))); - pane - }); - cx.subscribe_in(&pane, window, Self::handle_pane_event) - .detach(); - self.panes.push(pane.clone()); - - window.focus(&pane.focus_handle(cx)); - - cx.emit(Event::PaneAdded(pane.clone())); - pane - } - - pub fn add_item_to_center( - &mut self, - item: Box, - window: &mut Window, - cx: &mut Context, - ) -> bool { - if let Some(center_pane) = self.last_active_center_pane.clone() { - if let Some(center_pane) = center_pane.upgrade() { - center_pane.update(cx, |pane, cx| { - pane.add_item(item, true, true, None, window, cx) - }); - true - } else { - false - } - } else { - false - } - } - - pub fn add_item_to_active_pane( - &mut self, - item: Box, - destination_index: Option, - focus_item: bool, - window: &mut Window, - cx: &mut App, - ) { - self.add_item( - self.active_pane.clone(), - item, - destination_index, - false, - focus_item, - window, - cx, - ) - } - - pub fn add_item( - &mut self, - pane: Entity, - item: Box, - destination_index: Option, - activate_pane: bool, - focus_item: bool, - window: &mut Window, - cx: &mut App, - ) { - pane.update(cx, |pane, cx| { - pane.add_item( - item, - activate_pane, - focus_item, - destination_index, - window, - cx, - ) - }); - } - - pub fn split_item( - &mut self, - split_direction: SplitDirection, - item: Box, - window: &mut Window, - cx: &mut Context, - ) { - let new_pane = self.split_pane(self.active_pane.clone(), split_direction, window, cx); - self.add_item(new_pane, item, None, true, true, window, cx); - } - - pub fn open_abs_path( - &mut self, - abs_path: PathBuf, - options: OpenOptions, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - cx.spawn_in(window, async move |workspace, cx| { - let open_paths_task_result = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_paths(vec![abs_path.clone()], options, None, window, cx) - }) - .with_context(|| format!("open abs path {abs_path:?} task spawn"))? - .await; - anyhow::ensure!( - open_paths_task_result.len() == 1, - "open abs path {abs_path:?} task returned incorrect number of results" - ); - match open_paths_task_result - .into_iter() - .next() - .expect("ensured single task result") - { - Some(open_result) => { - open_result.with_context(|| format!("open abs path {abs_path:?} task join")) - } - None => anyhow::bail!("open abs path {abs_path:?} task returned None"), - } - }) - } - - pub fn split_abs_path( - &mut self, - abs_path: PathBuf, - visible: bool, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let project_path_task = - Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx); - cx.spawn_in(window, async move |this, cx| { - let (_, path) = project_path_task.await?; - this.update_in(cx, |this, window, cx| this.split_path(path, window, cx))? - .await - }) - } - - pub fn open_path( - &mut self, - path: impl Into, - pane: Option>, - focus_item: bool, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - self.open_path_preview(path, pane, focus_item, false, true, window, cx) - } - - pub fn open_path_preview( - &mut self, - path: impl Into, - pane: Option>, - focus_item: bool, - allow_preview: bool, - activate: bool, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - let pane = pane.unwrap_or_else(|| { - self.last_active_center_pane.clone().unwrap_or_else(|| { - self.panes - .first() - .expect("There must be an active pane") - .downgrade() - }) - }); - - let project_path = path.into(); - let task = self.load_path(project_path.clone(), window, cx); - window.spawn(cx, async move |cx| { - let (project_entry_id, build_item) = task.await?; - - pane.update_in(cx, |pane, window, cx| { - pane.open_item( - project_entry_id, - project_path, - focus_item, - allow_preview, - activate, - None, - window, - cx, - build_item, - ) - }) - }) - } - - pub fn split_path( - &mut self, - path: impl Into, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - self.split_path_preview(path, false, None, window, cx) - } - - pub fn split_path_preview( - &mut self, - path: impl Into, - allow_preview: bool, - split_direction: Option, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let pane = self.last_active_center_pane.clone().unwrap_or_else(|| { - self.panes - .first() - .expect("There must be an active pane") - .downgrade() - }); - - if let Member::Pane(center_pane) = &self.center.root - && center_pane.read(cx).items_len() == 0 - { - return self.open_path(path, Some(pane), true, window, cx); - } - - let project_path = path.into(); - let task = self.load_path(project_path.clone(), window, cx); - cx.spawn_in(window, async move |this, cx| { - let (project_entry_id, build_item) = task.await?; - this.update_in(cx, move |this, window, cx| -> Option<_> { - let pane = pane.upgrade()?; - let new_pane = this.split_pane( - pane, - split_direction.unwrap_or(SplitDirection::Right), - window, - cx, - ); - new_pane.update(cx, |new_pane, cx| { - Some(new_pane.open_item( - project_entry_id, - project_path, - true, - allow_preview, - true, - None, - window, - cx, - build_item, - )) - }) - }) - .map(|option| option.context("pane was dropped"))? - }) - } - - fn load_path( - &mut self, - path: ProjectPath, - window: &mut Window, - cx: &mut App, - ) -> Task, WorkspaceItemBuilder)>> { - let registry = cx.default_global::().clone(); - registry.open_path(self.project(), &path, window, cx) - } - - pub fn find_project_item( - &self, - pane: &Entity, - project_item: &Entity, - cx: &App, - ) -> Option> - where - T: ProjectItem, - { - use project::ProjectItem as _; - let project_item = project_item.read(cx); - let entry_id = project_item.entry_id(cx); - let project_path = project_item.project_path(cx); - - let mut item = None; - if let Some(entry_id) = entry_id { - item = pane.read(cx).item_for_entry(entry_id, cx); - } - if item.is_none() - && let Some(project_path) = project_path - { - item = pane.read(cx).item_for_path(project_path, cx); - } - - item.and_then(|item| item.downcast::()) - } - - pub fn is_project_item_open( - &self, - pane: &Entity, - project_item: &Entity, - cx: &App, - ) -> bool - where - T: ProjectItem, - { - self.find_project_item::(pane, project_item, cx) - .is_some() - } - - pub fn open_project_item( - &mut self, - pane: Entity, - project_item: Entity, - activate_pane: bool, - focus_item: bool, - keep_old_preview: bool, - allow_new_preview: bool, - window: &mut Window, - cx: &mut Context, - ) -> Entity - where - T: ProjectItem, - { - let old_item_id = pane.read(cx).active_item().map(|item| item.item_id()); - - if let Some(item) = self.find_project_item(&pane, &project_item, cx) { - if !keep_old_preview - && let Some(old_id) = old_item_id - && old_id != item.item_id() - { - // switching to a different item, so unpreview old active item - pane.update(cx, |pane, _| { - pane.unpreview_item_if_preview(old_id); - }); - } - - self.activate_item(&item, activate_pane, focus_item, window, cx); - if !allow_new_preview { - pane.update(cx, |pane, _| { - pane.unpreview_item_if_preview(item.item_id()); - }); - } - return item; - } - - let item = pane.update(cx, |pane, cx| { - cx.new(|cx| { - T::for_project_item(self.project().clone(), Some(pane), project_item, window, cx) - }) - }); - let mut destination_index = None; - pane.update(cx, |pane, cx| { - if !keep_old_preview && let Some(old_id) = old_item_id { - pane.unpreview_item_if_preview(old_id); - } - if allow_new_preview { - destination_index = pane.replace_preview_item_id(item.item_id(), window, cx); - } - }); - - self.add_item( - pane, - Box::new(item.clone()), - destination_index, - activate_pane, - focus_item, - window, - cx, - ); - item - } - - pub fn open_shared_screen( - &mut self, - peer_id: PeerId, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(shared_screen) = - self.shared_screen_for_peer(peer_id, &self.active_pane, window, cx) - { - self.active_pane.update(cx, |pane, cx| { - pane.add_item(Box::new(shared_screen), false, true, None, window, cx) - }); - } - } - - pub fn activate_item( - &mut self, - item: &dyn ItemHandle, - activate_pane: bool, - focus_item: bool, - window: &mut Window, - cx: &mut App, - ) -> bool { - let result = self.panes.iter().find_map(|pane| { - pane.read(cx) - .index_for_item(item) - .map(|ix| (pane.clone(), ix)) - }); - if let Some((pane, ix)) = result { - pane.update(cx, |pane, cx| { - pane.activate_item(ix, activate_pane, focus_item, window, cx) - }); - true - } else { - false - } - } - - fn activate_pane_at_index( - &mut self, - action: &ActivatePane, - window: &mut Window, - cx: &mut Context, - ) { - let panes = self.center.panes(); - if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) { - window.focus(&pane.focus_handle(cx)); - } else { - self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx) - .detach(); - } - } - - fn move_item_to_pane_at_index( - &mut self, - action: &MoveItemToPane, - window: &mut Window, - cx: &mut Context, - ) { - let panes = self.center.panes(); - let destination = match panes.get(action.destination) { - Some(&destination) => destination.clone(), - None => { - if !action.clone && self.active_pane.read(cx).items_len() < 2 { - return; - } - let direction = SplitDirection::Right; - let split_off_pane = self - .find_pane_in_direction(direction, cx) - .unwrap_or_else(|| self.active_pane.clone()); - let new_pane = self.add_pane(window, cx); - if self - .center - .split(&split_off_pane, &new_pane, direction) - .log_err() - .is_none() - { - return; - }; - new_pane - } - }; - - if action.clone { - if self - .active_pane - .read(cx) - .active_item() - .is_some_and(|item| item.can_split(cx)) - { - clone_active_item( - self.database_id(), - &self.active_pane, - &destination, - action.focus, - window, - cx, - ); - return; - } - } - move_active_item( - &self.active_pane, - &destination, - action.focus, - true, - window, - cx, - ) - } - - pub fn activate_next_pane(&mut self, window: &mut Window, cx: &mut App) { - let panes = self.center.panes(); - if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) { - let next_ix = (ix + 1) % panes.len(); - let next_pane = panes[next_ix].clone(); - window.focus(&next_pane.focus_handle(cx)); - } - } - - pub fn activate_previous_pane(&mut self, window: &mut Window, cx: &mut App) { - let panes = self.center.panes(); - if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) { - let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1); - let prev_pane = panes[prev_ix].clone(); - window.focus(&prev_pane.focus_handle(cx)); - } - } - - pub fn activate_pane_in_direction( - &mut self, - direction: SplitDirection, - window: &mut Window, - cx: &mut App, - ) { - use ActivateInDirectionTarget as Target; - enum Origin { - LeftDock, - RightDock, - BottomDock, - Center, - } - - let origin: Origin = [ - (&self.left_dock, Origin::LeftDock), - (&self.right_dock, Origin::RightDock), - (&self.bottom_dock, Origin::BottomDock), - ] - .into_iter() - .find_map(|(dock, origin)| { - if dock.focus_handle(cx).contains_focused(window, cx) && dock.read(cx).is_open() { - Some(origin) - } else { - None - } - }) - .unwrap_or(Origin::Center); - - let get_last_active_pane = || { - let pane = self - .last_active_center_pane - .clone() - .unwrap_or_else(|| { - self.panes - .first() - .expect("There must be an active pane") - .downgrade() - }) - .upgrade()?; - (pane.read(cx).items_len() != 0).then_some(pane) - }; - - let try_dock = - |dock: &Entity| dock.read(cx).is_open().then(|| Target::Dock(dock.clone())); - - let target = match (origin, direction) { - // We're in the center, so we first try to go to a different pane, - // otherwise try to go to a dock. - (Origin::Center, direction) => { - if let Some(pane) = self.find_pane_in_direction(direction, cx) { - Some(Target::Pane(pane)) - } else { - match direction { - SplitDirection::Up => None, - SplitDirection::Down => try_dock(&self.bottom_dock), - SplitDirection::Left => try_dock(&self.left_dock), - SplitDirection::Right => try_dock(&self.right_dock), - } - } - } - - (Origin::LeftDock, SplitDirection::Right) => { - if let Some(last_active_pane) = get_last_active_pane() { - Some(Target::Pane(last_active_pane)) - } else { - try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock)) - } - } - - (Origin::LeftDock, SplitDirection::Down) - | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock), - - (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane), - (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock), - (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock), - - (Origin::RightDock, SplitDirection::Left) => { - if let Some(last_active_pane) = get_last_active_pane() { - Some(Target::Pane(last_active_pane)) - } else { - try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock)) - } - } - - _ => None, - }; - - match target { - Some(ActivateInDirectionTarget::Pane(pane)) => { - let pane = pane.read(cx); - if let Some(item) = pane.active_item() { - item.item_focus_handle(cx).focus(window); - } else { - log::error!( - "Could not find a focus target when in switching focus in {direction} direction for a pane", - ); - } - } - Some(ActivateInDirectionTarget::Dock(dock)) => { - // Defer this to avoid a panic when the dock's active panel is already on the stack. - window.defer(cx, move |window, cx| { - let dock = dock.read(cx); - if let Some(panel) = dock.active_panel() { - panel.panel_focus_handle(cx).focus(window); - } else { - log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position()); - } - }) - } - None => {} - } - } - - pub fn move_item_to_pane_in_direction( - &mut self, - action: &MoveItemToPaneInDirection, - window: &mut Window, - cx: &mut Context, - ) { - let destination = match self.find_pane_in_direction(action.direction, cx) { - Some(destination) => destination, - None => { - if !action.clone && self.active_pane.read(cx).items_len() < 2 { - return; - } - let new_pane = self.add_pane(window, cx); - if self - .center - .split(&self.active_pane, &new_pane, action.direction) - .log_err() - .is_none() - { - return; - }; - new_pane - } - }; - - if action.clone { - if self - .active_pane - .read(cx) - .active_item() - .is_some_and(|item| item.can_split(cx)) - { - clone_active_item( - self.database_id(), - &self.active_pane, - &destination, - action.focus, - window, - cx, - ); - return; - } - } - move_active_item( - &self.active_pane, - &destination, - action.focus, - true, - window, - cx, - ); - } - - pub fn bounding_box_for_pane(&self, pane: &Entity) -> Option> { - self.center.bounding_box_for_pane(pane) - } - - pub fn find_pane_in_direction( - &mut self, - direction: SplitDirection, - cx: &App, - ) -> Option> { - self.center - .find_pane_in_direction(&self.active_pane, direction, cx) - .cloned() - } - - pub fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context) { - if let Some(to) = self.find_pane_in_direction(direction, cx) { - self.center.swap(&self.active_pane, &to); - cx.notify(); - } - } - - pub fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context) { - if self - .center - .move_to_border(&self.active_pane, direction) - .unwrap() - { - cx.notify(); - } - } - - pub fn resize_pane( - &mut self, - axis: gpui::Axis, - amount: Pixels, - window: &mut Window, - cx: &mut Context, - ) { - let docks = self.all_docks(); - let active_dock = docks - .into_iter() - .find(|dock| dock.focus_handle(cx).contains_focused(window, cx)); - - if let Some(dock) = active_dock { - let Some(panel_size) = dock.read(cx).active_panel_size(window, cx) else { - return; - }; - match dock.read(cx).position() { - DockPosition::Left => self.resize_left_dock(panel_size + amount, window, cx), - DockPosition::Bottom => self.resize_bottom_dock(panel_size + amount, window, cx), - DockPosition::Right => self.resize_right_dock(panel_size + amount, window, cx), - } - } else { - self.center - .resize(&self.active_pane, axis, amount, &self.bounds); - } - cx.notify(); - } - - pub fn reset_pane_sizes(&mut self, cx: &mut Context) { - self.center.reset_pane_sizes(); - cx.notify(); - } - - fn handle_pane_focused( - &mut self, - pane: Entity, - window: &mut Window, - cx: &mut Context, - ) { - // This is explicitly hoisted out of the following check for pane identity as - // terminal panel panes are not registered as a center panes. - self.status_bar.update(cx, |status_bar, cx| { - status_bar.set_active_pane(&pane, window, cx); - }); - if self.active_pane != pane { - self.set_active_pane(&pane, window, cx); - } - - if self.last_active_center_pane.is_none() { - self.last_active_center_pane = Some(pane.downgrade()); - } - - self.dismiss_zoomed_items_to_reveal(None, window, cx); - if pane.read(cx).is_zoomed() { - self.zoomed = Some(pane.downgrade().into()); - } else { - self.zoomed = None; - } - self.zoomed_position = None; - cx.emit(Event::ZoomChanged); - self.update_active_view_for_followers(window, cx); - pane.update(cx, |pane, _| { - pane.track_alternate_file_items(); - }); - - cx.notify(); - } - - fn set_active_pane( - &mut self, - pane: &Entity, - window: &mut Window, - cx: &mut Context, - ) { - self.active_pane = pane.clone(); - self.active_item_path_changed(window, cx); - self.last_active_center_pane = Some(pane.downgrade()); - } - - fn handle_panel_focused(&mut self, window: &mut Window, cx: &mut Context) { - self.update_active_view_for_followers(window, cx); - } - - fn handle_pane_event( - &mut self, - pane: &Entity, - event: &pane::Event, - window: &mut Window, - cx: &mut Context, - ) { - let mut serialize_workspace = true; - match event { - pane::Event::AddItem { item } => { - item.added_to_pane(self, pane.clone(), window, cx); - cx.emit(Event::ItemAdded { - item: item.boxed_clone(), - }); - } - pane::Event::Split { - direction, - clone_active_item, - } => { - if *clone_active_item { - self.split_and_clone(pane.clone(), *direction, window, cx) - .detach(); - } else { - self.split_and_move(pane.clone(), *direction, window, cx); - } - } - pane::Event::JoinIntoNext => { - self.join_pane_into_next(pane.clone(), window, cx); - } - pane::Event::JoinAll => { - self.join_all_panes(window, cx); - } - pane::Event::Remove { focus_on_pane } => { - self.remove_pane(pane.clone(), focus_on_pane.clone(), window, cx); - } - pane::Event::ActivateItem { - local, - focus_changed, - } => { - window.invalidate_character_coordinates(); - - pane.update(cx, |pane, _| { - pane.track_alternate_file_items(); - }); - if *local { - self.unfollow_in_pane(pane, window, cx); - } - serialize_workspace = *focus_changed || pane != self.active_pane(); - if pane == self.active_pane() { - self.active_item_path_changed(window, cx); - self.update_active_view_for_followers(window, cx); - } else if *local { - self.set_active_pane(pane, window, cx); - } - } - pane::Event::UserSavedItem { item, save_intent } => { - cx.emit(Event::UserSavedItem { - pane: pane.downgrade(), - item: item.boxed_clone(), - save_intent: *save_intent, - }); - serialize_workspace = false; - } - pane::Event::ChangeItemTitle => { - if *pane == self.active_pane { - self.active_item_path_changed(window, cx); - } - serialize_workspace = false; - } - pane::Event::RemovedItem { item } => { - cx.emit(Event::ActiveItemChanged); - self.update_window_edited(window, cx); - if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id()) - && entry.get().entity_id() == pane.entity_id() - { - entry.remove(); - } - cx.emit(Event::ItemRemoved { - item_id: item.item_id(), - }); - } - pane::Event::Focus => { - window.invalidate_character_coordinates(); - self.handle_pane_focused(pane.clone(), window, cx); - } - pane::Event::ZoomIn => { - if *pane == self.active_pane { - pane.update(cx, |pane, cx| pane.set_zoomed(true, cx)); - if pane.read(cx).has_focus(window, cx) { - self.zoomed = Some(pane.downgrade().into()); - self.zoomed_position = None; - cx.emit(Event::ZoomChanged); - } - cx.notify(); - } - } - pane::Event::ZoomOut => { - pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); - if self.zoomed_position.is_none() { - self.zoomed = None; - cx.emit(Event::ZoomChanged); - } - cx.notify(); - } - pane::Event::ItemPinned | pane::Event::ItemUnpinned => {} - } - - if serialize_workspace { - self.serialize_workspace(window, cx); - } - } - - pub fn unfollow_in_pane( - &mut self, - pane: &Entity, - window: &mut Window, - cx: &mut Context, - ) -> Option { - let leader_id = self.leader_for_pane(pane)?; - self.unfollow(leader_id, window, cx); - Some(leader_id) - } - - pub fn split_pane( - &mut self, - pane_to_split: Entity, - split_direction: SplitDirection, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let new_pane = self.add_pane(window, cx); - self.center - .split(&pane_to_split, &new_pane, split_direction) - .unwrap(); - cx.notify(); - new_pane - } - - pub fn split_and_move( - &mut self, - pane: Entity, - direction: SplitDirection, - window: &mut Window, - cx: &mut Context, - ) { - let Some(item) = pane.update(cx, |pane, cx| pane.take_active_item(window, cx)) else { - return; - }; - let new_pane = self.add_pane(window, cx); - new_pane.update(cx, |pane, cx| { - pane.add_item(item, true, true, None, window, cx) - }); - self.center.split(&pane, &new_pane, direction).unwrap(); - cx.notify(); - } - - pub fn split_and_clone( - &mut self, - pane: Entity, - direction: SplitDirection, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let Some(item) = pane.read(cx).active_item() else { - return Task::ready(None); - }; - if !item.can_split(cx) { - return Task::ready(None); - } - let task = item.clone_on_split(self.database_id(), window, cx); - cx.spawn_in(window, async move |this, cx| { - if let Some(clone) = task.await { - this.update_in(cx, |this, window, cx| { - let new_pane = this.add_pane(window, cx); - new_pane.update(cx, |pane, cx| { - pane.add_item(clone, true, true, None, window, cx) - }); - this.center.split(&pane, &new_pane, direction).unwrap(); - cx.notify(); - new_pane - }) - .ok() - } else { - None - } - }) - } - - pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context) { - let active_item = self.active_pane.read(cx).active_item(); - for pane in &self.panes { - join_pane_into_active(&self.active_pane, pane, window, cx); - } - if let Some(active_item) = active_item { - self.activate_item(active_item.as_ref(), true, true, window, cx); - } - cx.notify(); - } - - pub fn join_pane_into_next( - &mut self, - pane: Entity, - window: &mut Window, - cx: &mut Context, - ) { - let next_pane = self - .find_pane_in_direction(SplitDirection::Right, cx) - .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx)) - .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx)) - .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx)); - let Some(next_pane) = next_pane else { - return; - }; - move_all_items(&pane, &next_pane, window, cx); - cx.notify(); - } - - fn remove_pane( - &mut self, - pane: Entity, - focus_on: Option>, - window: &mut Window, - cx: &mut Context, - ) { - if self.center.remove(&pane).unwrap() { - self.force_remove_pane(&pane, &focus_on, window, cx); - self.unfollow_in_pane(&pane, window, cx); - self.last_leaders_by_pane.remove(&pane.downgrade()); - for removed_item in pane.read(cx).items() { - self.panes_by_item.remove(&removed_item.item_id()); - } - - cx.notify(); - } else { - self.active_item_path_changed(window, cx); - } - cx.emit(Event::PaneRemoved); - } - - pub fn panes_mut(&mut self) -> &mut [Entity] { - &mut self.panes - } - - pub fn panes(&self) -> &[Entity] { - &self.panes - } - - pub fn active_pane(&self) -> &Entity { - &self.active_pane - } - - pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity { - for dock in self.all_docks() { - if dock.focus_handle(cx).contains_focused(window, cx) - && let Some(pane) = dock - .read(cx) - .active_panel() - .and_then(|panel| panel.pane(cx)) - { - return pane; - } - } - self.active_pane().clone() - } - - pub fn adjacent_pane(&mut self, window: &mut Window, cx: &mut Context) -> Entity { - self.find_pane_in_direction(SplitDirection::Right, cx) - .unwrap_or_else(|| { - self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx) - }) - } - - pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option> { - let weak_pane = self.panes_by_item.get(&handle.item_id())?; - weak_pane.upgrade() - } - - fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context) { - self.follower_states.retain(|leader_id, state| { - if *leader_id == CollaboratorId::PeerId(peer_id) { - for item in state.items_by_leader_view_id.values() { - item.view.set_leader_id(None, window, cx); - } - false - } else { - true - } - }); - cx.notify(); - } - - pub fn start_following( - &mut self, - leader_id: impl Into, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - let leader_id = leader_id.into(); - let pane = self.active_pane().clone(); - - self.last_leaders_by_pane - .insert(pane.downgrade(), leader_id); - self.unfollow(leader_id, window, cx); - self.unfollow_in_pane(&pane, window, cx); - self.follower_states.insert( - leader_id, - FollowerState { - center_pane: pane.clone(), - dock_pane: None, - active_view_id: None, - items_by_leader_view_id: Default::default(), - }, - ); - cx.notify(); - - match leader_id { - CollaboratorId::PeerId(leader_peer_id) => { - let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); - let project_id = self.project.read(cx).remote_id(); - let request = self.app_state.client.request(proto::Follow { - room_id, - project_id, - leader_id: Some(leader_peer_id), - }); - - Some(cx.spawn_in(window, async move |this, cx| { - let response = request.await?; - this.update(cx, |this, _| { - let state = this - .follower_states - .get_mut(&leader_id) - .context("following interrupted")?; - state.active_view_id = response - .active_view - .as_ref() - .and_then(|view| ViewId::from_proto(view.id.clone()?).ok()); - anyhow::Ok(()) - })??; - if let Some(view) = response.active_view { - Self::add_view_from_leader(this.clone(), leader_peer_id, &view, cx).await?; - } - this.update_in(cx, |this, window, cx| { - this.leader_updated(leader_id, window, cx) - })?; - Ok(()) - })) - } - CollaboratorId::Agent => { - self.leader_updated(leader_id, window, cx)?; - Some(Task::ready(Ok(()))) - } - } - } - - pub fn follow_next_collaborator( - &mut self, - _: &FollowNextCollaborator, - window: &mut Window, - cx: &mut Context, - ) { - let collaborators = self.project.read(cx).collaborators(); - let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) { - let mut collaborators = collaborators.keys().copied(); - for peer_id in collaborators.by_ref() { - if CollaboratorId::PeerId(peer_id) == leader_id { - break; - } - } - collaborators.next().map(CollaboratorId::PeerId) - } else if let Some(last_leader_id) = - self.last_leaders_by_pane.get(&self.active_pane.downgrade()) - { - match last_leader_id { - CollaboratorId::PeerId(peer_id) => { - if collaborators.contains_key(peer_id) { - Some(*last_leader_id) - } else { - None - } - } - CollaboratorId::Agent => Some(CollaboratorId::Agent), - } - } else { - None - }; - - let pane = self.active_pane.clone(); - let Some(leader_id) = next_leader_id.or_else(|| { - Some(CollaboratorId::PeerId( - collaborators.keys().copied().next()?, - )) - }) else { - return; - }; - if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) { - return; - } - if let Some(task) = self.start_following(leader_id, window, cx) { - task.detach_and_log_err(cx) - } - } - - pub fn follow( - &mut self, - leader_id: impl Into, - window: &mut Window, - cx: &mut Context, - ) { - let leader_id = leader_id.into(); - - if let CollaboratorId::PeerId(peer_id) = leader_id { - let Some(room) = ActiveCall::global(cx).read(cx).room() else { - return; - }; - let room = room.read(cx); - let Some(remote_participant) = room.remote_participant_for_peer_id(peer_id) else { - return; - }; - - let project = self.project.read(cx); - - let other_project_id = match remote_participant.location { - call::ParticipantLocation::External => None, - call::ParticipantLocation::UnsharedProject => None, - call::ParticipantLocation::SharedProject { project_id } => { - if Some(project_id) == project.remote_id() { - None - } else { - Some(project_id) - } - } - }; - - // if they are active in another project, follow there. - if let Some(project_id) = other_project_id { - let app_state = self.app_state.clone(); - crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx) - .detach_and_log_err(cx); - } - } - - // if you're already following, find the right pane and focus it. - if let Some(follower_state) = self.follower_states.get(&leader_id) { - window.focus(&follower_state.pane().focus_handle(cx)); - - return; - } - - // Otherwise, follow. - if let Some(task) = self.start_following(leader_id, window, cx) { - task.detach_and_log_err(cx) - } - } - - pub fn unfollow( - &mut self, - leader_id: impl Into, - window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - cx.notify(); - - let leader_id = leader_id.into(); - let state = self.follower_states.remove(&leader_id)?; - for (_, item) in state.items_by_leader_view_id { - item.view.set_leader_id(None, window, cx); - } - - if let CollaboratorId::PeerId(leader_peer_id) = leader_id { - let project_id = self.project.read(cx).remote_id(); - let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); - self.app_state - .client - .send(proto::Unfollow { - room_id, - project_id, - leader_id: Some(leader_peer_id), - }) - .log_err(); - } - - Some(()) - } - - pub fn is_being_followed(&self, id: impl Into) -> bool { - self.follower_states.contains_key(&id.into()) - } - - fn active_item_path_changed(&mut self, window: &mut Window, cx: &mut Context) { - cx.emit(Event::ActiveItemChanged); - let active_entry = self.active_project_path(cx); - self.project.update(cx, |project, cx| { - project.set_active_path(active_entry.clone(), cx) - }); - - if let Some(project_path) = &active_entry { - let git_store_entity = self.project.read(cx).git_store().clone(); - git_store_entity.update(cx, |git_store, cx| { - git_store.set_active_repo_for_path(project_path, cx); - }); - } - - self.update_window_title(window, cx); - } - - fn update_window_title(&mut self, window: &mut Window, cx: &mut App) { - let project = self.project().read(cx); - let mut title = String::new(); - - for (i, worktree) in project.visible_worktrees(cx).enumerate() { - let name = { - let settings_location = SettingsLocation { - worktree_id: worktree.read(cx).id(), - path: RelPath::empty(), - }; - - let settings = WorktreeSettings::get(Some(settings_location), cx); - match &settings.project_name { - Some(name) => name.as_str(), - None => worktree.read(cx).root_name_str(), - } - }; - if i > 0 { - title.push_str(", "); - } - title.push_str(name); - } - - if title.is_empty() { - title = "empty project".to_string(); - } - - if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) { - let filename = path.path.file_name().or_else(|| { - Some( - project - .worktree_for_id(path.worktree_id, cx)? - .read(cx) - .root_name_str(), - ) - }); - - if let Some(filename) = filename { - title.push_str(" — "); - title.push_str(filename.as_ref()); - } - } - - if project.is_via_collab() { - title.push_str(" ↙"); - } else if project.is_shared() { - title.push_str(" ↗"); - } - - if let Some(last_title) = self.last_window_title.as_ref() - && &title == last_title - { - return; - } - window.set_window_title(&title); - SystemWindowTabController::update_tab_title( - cx, - window.window_handle().window_id(), - SharedString::from(&title), - ); - self.last_window_title = Some(title); - } - - fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) { - let is_edited = !self.project.read(cx).is_disconnected(cx) && !self.dirty_items.is_empty(); - if is_edited != self.window_edited { - self.window_edited = is_edited; - window.set_window_edited(self.window_edited) - } - } - - fn update_item_dirty_state( - &mut self, - item: &dyn ItemHandle, - window: &mut Window, - cx: &mut App, - ) { - let is_dirty = item.is_dirty(cx); - let item_id = item.item_id(); - let was_dirty = self.dirty_items.contains_key(&item_id); - if is_dirty == was_dirty { - return; - } - if was_dirty { - self.dirty_items.remove(&item_id); - self.update_window_edited(window, cx); - return; - } - if let Some(window_handle) = window.window_handle().downcast::() { - let s = item.on_release( - cx, - Box::new(move |cx| { - window_handle - .update(cx, |this, window, cx| { - this.dirty_items.remove(&item_id); - this.update_window_edited(window, cx) - }) - .ok(); - }), - ); - self.dirty_items.insert(item_id, s); - self.update_window_edited(window, cx); - } - } - - fn render_notifications(&self, _window: &mut Window, _cx: &mut Context) -> Option
{ - if self.notifications.is_empty() { - None - } else { - Some( - div() - .absolute() - .right_3() - .bottom_3() - .w_112() - .h_full() - .flex() - .flex_col() - .justify_end() - .gap_2() - .children( - self.notifications - .iter() - .map(|(_, notification)| notification.clone().into_any()), - ), - ) - } - } - - // RPC handlers - - fn active_view_for_follower( - &self, - follower_project_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> Option { - let (item, panel_id) = self.active_item_for_followers(window, cx); - let item = item?; - let leader_id = self - .pane_for(&*item) - .and_then(|pane| self.leader_for_pane(&pane)); - let leader_peer_id = match leader_id { - Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id), - Some(CollaboratorId::Agent) | None => None, - }; - - let item_handle = item.to_followable_item_handle(cx)?; - let id = item_handle.remote_id(&self.app_state.client, window, cx)?; - let variant = item_handle.to_state_proto(window, cx)?; - - if item_handle.is_project_item(window, cx) - && (follower_project_id.is_none() - || follower_project_id != self.project.read(cx).remote_id()) - { - return None; - } - - Some(proto::View { - id: id.to_proto(), - leader_id: leader_peer_id, - variant: Some(variant), - panel_id: panel_id.map(|id| id as i32), - }) - } - - fn handle_follow( - &mut self, - follower_project_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> proto::FollowResponse { - let active_view = self.active_view_for_follower(follower_project_id, window, cx); - - cx.notify(); - proto::FollowResponse { - // TODO: Remove after version 0.145.x stabilizes. - active_view_id: active_view.as_ref().and_then(|view| view.id.clone()), - views: active_view.iter().cloned().collect(), - active_view, - } - } - - fn handle_update_followers( - &mut self, - leader_id: PeerId, - message: proto::UpdateFollowers, - _window: &mut Window, - _cx: &mut Context, - ) { - self.leader_updates_tx - .unbounded_send((leader_id, message)) - .ok(); - } - - async fn process_leader_update( - this: &WeakEntity, - leader_id: PeerId, - update: proto::UpdateFollowers, - cx: &mut AsyncWindowContext, - ) -> Result<()> { - match update.variant.context("invalid update")? { - proto::update_followers::Variant::CreateView(view) => { - let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?; - let should_add_view = this.update(cx, |this, _| { - if let Some(state) = this.follower_states.get_mut(&leader_id.into()) { - anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id)) - } else { - anyhow::Ok(false) - } - })??; - - if should_add_view { - Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await? - } - } - proto::update_followers::Variant::UpdateActiveView(update_active_view) => { - let should_add_view = this.update(cx, |this, _| { - if let Some(state) = this.follower_states.get_mut(&leader_id.into()) { - state.active_view_id = update_active_view - .view - .as_ref() - .and_then(|view| ViewId::from_proto(view.id.clone()?).ok()); - - if state.active_view_id.is_some_and(|view_id| { - !state.items_by_leader_view_id.contains_key(&view_id) - }) { - anyhow::Ok(true) - } else { - anyhow::Ok(false) - } - } else { - anyhow::Ok(false) - } - })??; - - if should_add_view && let Some(view) = update_active_view.view { - Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await? - } - } - proto::update_followers::Variant::UpdateView(update_view) => { - let variant = update_view.variant.context("missing update view variant")?; - let id = update_view.id.context("missing update view id")?; - let mut tasks = Vec::new(); - this.update_in(cx, |this, window, cx| { - let project = this.project.clone(); - if let Some(state) = this.follower_states.get(&leader_id.into()) { - let view_id = ViewId::from_proto(id.clone())?; - if let Some(item) = state.items_by_leader_view_id.get(&view_id) { - tasks.push(item.view.apply_update_proto( - &project, - variant.clone(), - window, - cx, - )); - } - } - anyhow::Ok(()) - })??; - try_join_all(tasks).await.log_err(); - } - } - this.update_in(cx, |this, window, cx| { - this.leader_updated(leader_id, window, cx) - })?; - Ok(()) - } - - async fn add_view_from_leader( - this: WeakEntity, - leader_id: PeerId, - view: &proto::View, - cx: &mut AsyncWindowContext, - ) -> Result<()> { - let this = this.upgrade().context("workspace dropped")?; - - let Some(id) = view.id.clone() else { - anyhow::bail!("no id for view"); - }; - let id = ViewId::from_proto(id)?; - let panel_id = view.panel_id.and_then(proto::PanelId::from_i32); - - let pane = this.update(cx, |this, _cx| { - let state = this - .follower_states - .get(&leader_id.into()) - .context("stopped following")?; - anyhow::Ok(state.pane().clone()) - })??; - let existing_item = pane.update_in(cx, |pane, window, cx| { - let client = this.read(cx).client().clone(); - pane.items().find_map(|item| { - let item = item.to_followable_item_handle(cx)?; - if item.remote_id(&client, window, cx) == Some(id) { - Some(item) - } else { - None - } - }) - })?; - let item = if let Some(existing_item) = existing_item { - existing_item - } else { - let variant = view.variant.clone(); - anyhow::ensure!(variant.is_some(), "missing view variant"); - - let task = cx.update(|window, cx| { - FollowableViewRegistry::from_state_proto(this.clone(), id, variant, window, cx) - })?; - - let Some(task) = task else { - anyhow::bail!( - "failed to construct view from leader (maybe from a different version of zed?)" - ); - }; - - let mut new_item = task.await?; - pane.update_in(cx, |pane, window, cx| { - let mut item_to_remove = None; - for (ix, item) in pane.items().enumerate() { - if let Some(item) = item.to_followable_item_handle(cx) { - match new_item.dedup(item.as_ref(), window, cx) { - Some(item::Dedup::KeepExisting) => { - new_item = - item.boxed_clone().to_followable_item_handle(cx).unwrap(); - break; - } - Some(item::Dedup::ReplaceExisting) => { - item_to_remove = Some((ix, item.item_id())); - break; - } - None => {} - } - } - } - - if let Some((ix, id)) = item_to_remove { - pane.remove_item(id, false, false, window, cx); - pane.add_item(new_item.boxed_clone(), false, false, Some(ix), window, cx); - } - })?; - - new_item - }; - - this.update_in(cx, |this, window, cx| { - let state = this.follower_states.get_mut(&leader_id.into())?; - item.set_leader_id(Some(leader_id.into()), window, cx); - state.items_by_leader_view_id.insert( - id, - FollowerView { - view: item, - location: panel_id, - }, - ); - - Some(()) - })?; - - Ok(()) - } - - fn handle_agent_location_changed(&mut self, window: &mut Window, cx: &mut Context) { - let Some(follower_state) = self.follower_states.get_mut(&CollaboratorId::Agent) else { - return; - }; - - if let Some(agent_location) = self.project.read(cx).agent_location() { - let buffer_entity_id = agent_location.buffer.entity_id(); - let view_id = ViewId { - creator: CollaboratorId::Agent, - id: buffer_entity_id.as_u64(), - }; - follower_state.active_view_id = Some(view_id); - - let item = match follower_state.items_by_leader_view_id.entry(view_id) { - hash_map::Entry::Occupied(entry) => Some(entry.into_mut()), - hash_map::Entry::Vacant(entry) => { - let existing_view = - follower_state - .center_pane - .read(cx) - .items() - .find_map(|item| { - let item = item.to_followable_item_handle(cx)?; - if item.buffer_kind(cx) == ItemBufferKind::Singleton - && item.project_item_model_ids(cx).as_slice() - == [buffer_entity_id] - { - Some(item) - } else { - None - } - }); - let view = existing_view.or_else(|| { - agent_location.buffer.upgrade().and_then(|buffer| { - cx.update_default_global(|registry: &mut ProjectItemRegistry, cx| { - registry.build_item(buffer, self.project.clone(), None, window, cx) - })? - .to_followable_item_handle(cx) - }) - }); - - view.map(|view| { - entry.insert(FollowerView { - view, - location: None, - }) - }) - } - }; - - if let Some(item) = item { - item.view - .set_leader_id(Some(CollaboratorId::Agent), window, cx); - item.view - .update_agent_location(agent_location.position, window, cx); - } - } else { - follower_state.active_view_id = None; - } - - self.leader_updated(CollaboratorId::Agent, window, cx); - } - - pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) { - let mut is_project_item = true; - let mut update = proto::UpdateActiveView::default(); - if window.is_window_active() { - let (active_item, panel_id) = self.active_item_for_followers(window, cx); - - if let Some(item) = active_item - && item.item_focus_handle(cx).contains_focused(window, cx) - { - let leader_id = self - .pane_for(&*item) - .and_then(|pane| self.leader_for_pane(&pane)); - let leader_peer_id = match leader_id { - Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id), - Some(CollaboratorId::Agent) | None => None, - }; - - if let Some(item) = item.to_followable_item_handle(cx) { - let id = item - .remote_id(&self.app_state.client, window, cx) - .map(|id| id.to_proto()); - - if let Some(id) = id - && let Some(variant) = item.to_state_proto(window, cx) - { - let view = Some(proto::View { - id: id.clone(), - leader_id: leader_peer_id, - variant: Some(variant), - panel_id: panel_id.map(|id| id as i32), - }); - - is_project_item = item.is_project_item(window, cx); - update = proto::UpdateActiveView { - view, - // TODO: Remove after version 0.145.x stabilizes. - id, - leader_id: leader_peer_id, - }; - }; - } - } - } - - let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref()); - if active_view_id != self.last_active_view_id.as_ref() { - self.last_active_view_id = active_view_id.cloned(); - self.update_followers( - is_project_item, - proto::update_followers::Variant::UpdateActiveView(update), - window, - cx, - ); - } - } - - fn active_item_for_followers( - &self, - window: &mut Window, - cx: &mut App, - ) -> (Option>, Option) { - let mut active_item = None; - let mut panel_id = None; - for dock in self.all_docks() { - if dock.focus_handle(cx).contains_focused(window, cx) - && let Some(panel) = dock.read(cx).active_panel() - && let Some(pane) = panel.pane(cx) - && let Some(item) = pane.read(cx).active_item() - { - active_item = Some(item); - panel_id = panel.remote_id(); - break; - } - } - - if active_item.is_none() { - active_item = self.active_pane().read(cx).active_item(); - } - (active_item, panel_id) - } - - fn update_followers( - &self, - project_only: bool, - update: proto::update_followers::Variant, - _: &mut Window, - cx: &mut App, - ) -> Option<()> { - // If this update only applies to for followers in the current project, - // then skip it unless this project is shared. If it applies to all - // followers, regardless of project, then set `project_id` to none, - // indicating that it goes to all followers. - let project_id = if project_only { - Some(self.project.read(cx).remote_id()?) - } else { - None - }; - self.app_state().workspace_store.update(cx, |store, cx| { - store.update_followers(project_id, update, cx) - }) - } - - pub fn leader_for_pane(&self, pane: &Entity) -> Option { - self.follower_states.iter().find_map(|(leader_id, state)| { - if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) { - Some(*leader_id) - } else { - None - } - }) - } - - fn leader_updated( - &mut self, - leader_id: impl Into, - window: &mut Window, - cx: &mut Context, - ) -> Option> { - cx.notify(); - - let leader_id = leader_id.into(); - let (panel_id, item) = match leader_id { - CollaboratorId::PeerId(peer_id) => self.active_item_for_peer(peer_id, window, cx)?, - CollaboratorId::Agent => (None, self.active_item_for_agent()?), - }; - - let state = self.follower_states.get(&leader_id)?; - let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx); - let pane; - if let Some(panel_id) = panel_id { - pane = self - .activate_panel_for_proto_id(panel_id, window, cx)? - .pane(cx)?; - let state = self.follower_states.get_mut(&leader_id)?; - state.dock_pane = Some(pane.clone()); - } else { - pane = state.center_pane.clone(); - let state = self.follower_states.get_mut(&leader_id)?; - if let Some(dock_pane) = state.dock_pane.take() { - transfer_focus |= dock_pane.focus_handle(cx).contains_focused(window, cx); - } - } - - pane.update(cx, |pane, cx| { - let focus_active_item = pane.has_focus(window, cx) || transfer_focus; - if let Some(index) = pane.index_for_item(item.as_ref()) { - pane.activate_item(index, false, false, window, cx); - } else { - pane.add_item(item.boxed_clone(), false, false, None, window, cx) - } - - if focus_active_item { - pane.focus_active_item(window, cx) - } - }); - - Some(item) - } - - fn active_item_for_agent(&self) -> Option> { - let state = self.follower_states.get(&CollaboratorId::Agent)?; - let active_view_id = state.active_view_id?; - Some( - state - .items_by_leader_view_id - .get(&active_view_id)? - .view - .boxed_clone(), - ) - } - - fn active_item_for_peer( - &self, - peer_id: PeerId, - window: &mut Window, - cx: &mut Context, - ) -> Option<(Option, Box)> { - let call = self.active_call()?; - let room = call.read(cx).room()?.read(cx); - let participant = room.remote_participant_for_peer_id(peer_id)?; - let leader_in_this_app; - let leader_in_this_project; - match participant.location { - call::ParticipantLocation::SharedProject { project_id } => { - leader_in_this_app = true; - leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id(); - } - call::ParticipantLocation::UnsharedProject => { - leader_in_this_app = true; - leader_in_this_project = false; - } - call::ParticipantLocation::External => { - leader_in_this_app = false; - leader_in_this_project = false; - } - }; - let state = self.follower_states.get(&peer_id.into())?; - let mut item_to_activate = None; - if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) { - if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) - && (leader_in_this_project || !item.view.is_project_item(window, cx)) - { - item_to_activate = Some((item.location, item.view.boxed_clone())); - } - } else if let Some(shared_screen) = - self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx) - { - item_to_activate = Some((None, Box::new(shared_screen))); - } - item_to_activate - } - - fn shared_screen_for_peer( - &self, - peer_id: PeerId, - pane: &Entity, - window: &mut Window, - cx: &mut App, - ) -> Option> { - let call = self.active_call()?; - let room = call.read(cx).room()?.clone(); - let participant = room.read(cx).remote_participant_for_peer_id(peer_id)?; - let track = participant.video_tracks.values().next()?.clone(); - let user = participant.user.clone(); - - for item in pane.read(cx).items_of_type::() { - if item.read(cx).peer_id == peer_id { - return Some(item); - } - } - - Some(cx.new(|cx| SharedScreen::new(track, peer_id, user.clone(), room.clone(), window, cx))) - } - - pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context) { - if window.is_window_active() { - self.update_active_view_for_followers(window, cx); - - if let Some(database_id) = self.database_id { - cx.background_spawn(persistence::DB.update_timestamp(database_id)) - .detach(); - } - } else { - for pane in &self.panes { - pane.update(cx, |pane, cx| { - if let Some(item) = pane.active_item() { - item.workspace_deactivated(window, cx); - } - for item in pane.items() { - if matches!( - item.workspace_settings(cx).autosave, - AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange - ) { - Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx) - .detach_and_log_err(cx); - } - } - }); - } - } - } - - pub fn active_call(&self) -> Option<&Entity> { - self.active_call.as_ref().map(|(call, _)| call) - } - - fn on_active_call_event( - &mut self, - _: &Entity, - event: &call::room::Event, - window: &mut Window, - cx: &mut Context, - ) { - match event { - call::room::Event::ParticipantLocationChanged { participant_id } - | call::room::Event::RemoteVideoTracksChanged { participant_id } => { - self.leader_updated(participant_id, window, cx); - } - _ => {} - } - } - - pub fn database_id(&self) -> Option { - self.database_id - } - - pub fn session_id(&self) -> Option { - self.session_id.clone() - } - - pub fn root_paths(&self, cx: &App) -> Vec> { - let project = self.project().read(cx); - project - .visible_worktrees(cx) - .map(|worktree| worktree.read(cx).abs_path()) - .collect::>() - } - - fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context) { - match member { - Member::Axis(PaneAxis { members, .. }) => { - for child in members.iter() { - self.remove_panes(child.clone(), window, cx) - } - } - Member::Pane(pane) => { - self.force_remove_pane(&pane, &None, window, cx); - } - } - } - - fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> { - self.session_id.take(); - self.serialize_workspace_internal(window, cx) - } - - fn force_remove_pane( - &mut self, - pane: &Entity, - focus_on: &Option>, - window: &mut Window, - cx: &mut Context, - ) { - self.panes.retain(|p| p != pane); - if let Some(focus_on) = focus_on { - focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))); - } else if self.active_pane() == pane { - self.panes - .last() - .unwrap() - .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))); - } - if self.last_active_center_pane == Some(pane.downgrade()) { - self.last_active_center_pane = None; - } - cx.notify(); - } - - fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context) { - if self._schedule_serialize_workspace.is_none() { - self._schedule_serialize_workspace = - Some(cx.spawn_in(window, async move |this, cx| { - cx.background_executor() - .timer(SERIALIZATION_THROTTLE_TIME) - .await; - this.update_in(cx, |this, window, cx| { - this.serialize_workspace_internal(window, cx).detach(); - this._schedule_serialize_workspace.take(); - }) - .log_err(); - })); - } - } - - fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> { - let Some(database_id) = self.database_id() else { - return Task::ready(()); - }; - - fn serialize_pane_handle( - pane_handle: &Entity, - window: &mut Window, - cx: &mut App, - ) -> SerializedPane { - let (items, active, pinned_count) = { - let pane = pane_handle.read(cx); - let active_item_id = pane.active_item().map(|item| item.item_id()); - ( - pane.items() - .filter_map(|handle| { - let handle = handle.to_serializable_item_handle(cx)?; - - Some(SerializedItem { - kind: Arc::from(handle.serialized_item_kind()), - item_id: handle.item_id().as_u64(), - active: Some(handle.item_id()) == active_item_id, - preview: pane.is_active_preview_item(handle.item_id()), - }) - }) - .collect::>(), - pane.has_focus(window, cx), - pane.pinned_count(), - ) - }; - - SerializedPane::new(items, active, pinned_count) - } - - fn build_serialized_pane_group( - pane_group: &Member, - window: &mut Window, - cx: &mut App, - ) -> SerializedPaneGroup { - match pane_group { - Member::Axis(PaneAxis { - axis, - members, - flexes, - bounding_boxes: _, - }) => SerializedPaneGroup::Group { - axis: SerializedAxis(*axis), - children: members - .iter() - .map(|member| build_serialized_pane_group(member, window, cx)) - .collect::>(), - flexes: Some(flexes.lock().clone()), - }, - Member::Pane(pane_handle) => { - SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx)) - } - } - } - - fn build_serialized_docks( - this: &Workspace, - window: &mut Window, - cx: &mut App, - ) -> DockStructure { - let left_dock = this.left_dock.read(cx); - let left_visible = left_dock.is_open(); - let left_active_panel = left_dock - .active_panel() - .map(|panel| panel.persistent_name().to_string()); - let left_dock_zoom = left_dock - .active_panel() - .map(|panel| panel.is_zoomed(window, cx)) - .unwrap_or(false); - - let right_dock = this.right_dock.read(cx); - let right_visible = right_dock.is_open(); - let right_active_panel = right_dock - .active_panel() - .map(|panel| panel.persistent_name().to_string()); - let right_dock_zoom = right_dock - .active_panel() - .map(|panel| panel.is_zoomed(window, cx)) - .unwrap_or(false); - - let bottom_dock = this.bottom_dock.read(cx); - let bottom_visible = bottom_dock.is_open(); - let bottom_active_panel = bottom_dock - .active_panel() - .map(|panel| panel.persistent_name().to_string()); - let bottom_dock_zoom = bottom_dock - .active_panel() - .map(|panel| panel.is_zoomed(window, cx)) - .unwrap_or(false); - - DockStructure { - left: DockData { - visible: left_visible, - active_panel: left_active_panel, - zoom: left_dock_zoom, - }, - right: DockData { - visible: right_visible, - active_panel: right_active_panel, - zoom: right_dock_zoom, - }, - bottom: DockData { - visible: bottom_visible, - active_panel: bottom_active_panel, - zoom: bottom_dock_zoom, - }, - } - } - - match self.serialize_workspace_location(cx) { - WorkspaceLocation::Location(location, paths) => { - let breakpoints = self.project.update(cx, |project, cx| { - project - .breakpoint_store() - .read(cx) - .all_source_breakpoints(cx) - }); - let user_toolchains = self - .project - .read(cx) - .user_toolchains(cx) - .unwrap_or_default(); - - let center_group = build_serialized_pane_group(&self.center.root, window, cx); - let docks = build_serialized_docks(self, window, cx); - let window_bounds = Some(SerializedWindowBounds(window.window_bounds())); - - let serialized_workspace = SerializedWorkspace { - id: database_id, - location, - paths, - center_group, - window_bounds, - display: Default::default(), - docks, - centered_layout: self.centered_layout, - session_id: self.session_id.clone(), - breakpoints, - window_id: Some(window.window_handle().window_id().as_u64()), - user_toolchains, - }; - - window.spawn(cx, async move |_| { - persistence::DB.save_workspace(serialized_workspace).await; - }) - } - WorkspaceLocation::DetachFromSession => window.spawn(cx, async move |_| { - persistence::DB - .set_session_id(database_id, None) - .await - .log_err(); - }), - WorkspaceLocation::None => Task::ready(()), - } - } - - fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation { - let paths = PathList::new(&self.root_paths(cx)); - if let Some(connection) = self.project.read(cx).remote_connection_options(cx) { - WorkspaceLocation::Location(SerializedWorkspaceLocation::Remote(connection), paths) - } else if self.project.read(cx).is_local() { - if !paths.is_empty() { - WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths) - } else { - WorkspaceLocation::DetachFromSession - } - } else { - WorkspaceLocation::None - } - } - - fn update_history(&self, cx: &mut App) { - let Some(id) = self.database_id() else { - return; - }; - if !self.project.read(cx).is_local() { - return; - } - if let Some(manager) = HistoryManager::global(cx) { - let paths = PathList::new(&self.root_paths(cx)); - manager.update(cx, |this, cx| { - this.update_history(id, HistoryManagerEntry::new(id, &paths), cx); - }); - } - } - - async fn serialize_items( - this: &WeakEntity, - items_rx: UnboundedReceiver>, - cx: &mut AsyncWindowContext, - ) -> Result<()> { - const CHUNK_SIZE: usize = 200; - - let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE); - - while let Some(items_received) = serializable_items.next().await { - let unique_items = - items_received - .into_iter() - .fold(HashMap::default(), |mut acc, item| { - acc.entry(item.item_id()).or_insert(item); - acc - }); - - // We use into_iter() here so that the references to the items are moved into - // the tasks and not kept alive while we're sleeping. - for (_, item) in unique_items.into_iter() { - if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| { - item.serialize(workspace, false, window, cx) - }) { - cx.background_spawn(async move { task.await.log_err() }) - .detach(); - } - } - - cx.background_executor() - .timer(SERIALIZATION_THROTTLE_TIME) - .await; - } - - Ok(()) - } - - pub(crate) fn enqueue_item_serialization( - &mut self, - item: Box, - ) -> Result<()> { - self.serializable_items_tx - .unbounded_send(item) - .map_err(|err| anyhow!("failed to send serializable item over channel: {err}")) - } - - pub(crate) fn load_workspace( - serialized_workspace: SerializedWorkspace, - paths_to_open: Vec>, - window: &mut Window, - cx: &mut Context, - ) -> Task>>>> { - cx.spawn_in(window, async move |workspace, cx| { - let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?; - - let mut center_group = None; - let mut center_items = None; - - // Traverse the splits tree and add to things - if let Some((group, active_pane, items)) = serialized_workspace - .center_group - .deserialize(&project, serialized_workspace.id, workspace.clone(), cx) - .await - { - center_items = Some(items); - center_group = Some((group, active_pane)) - } - - let mut items_by_project_path = HashMap::default(); - let mut item_ids_by_kind = HashMap::default(); - let mut all_deserialized_items = Vec::default(); - cx.update(|_, cx| { - for item in center_items.unwrap_or_default().into_iter().flatten() { - if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) { - item_ids_by_kind - .entry(serializable_item_handle.serialized_item_kind()) - .or_insert(Vec::new()) - .push(item.item_id().as_u64() as ItemId); - } - - if let Some(project_path) = item.project_path(cx) { - items_by_project_path.insert(project_path, item.clone()); - } - all_deserialized_items.push(item); - } - })?; - - let opened_items = paths_to_open - .into_iter() - .map(|path_to_open| { - path_to_open - .and_then(|path_to_open| items_by_project_path.remove(&path_to_open)) - }) - .collect::>(); - - // Remove old panes from workspace panes list - workspace.update_in(cx, |workspace, window, cx| { - if let Some((center_group, active_pane)) = center_group { - workspace.remove_panes(workspace.center.root.clone(), window, cx); - - // Swap workspace center group - workspace.center = PaneGroup::with_root(center_group); - if let Some(active_pane) = active_pane { - workspace.set_active_pane(&active_pane, window, cx); - cx.focus_self(window); - } else { - workspace.set_active_pane(&workspace.center.first_pane(), window, cx); - } - } - - let docks = serialized_workspace.docks; - - for (dock, serialized_dock) in [ - (&mut workspace.right_dock, docks.right), - (&mut workspace.left_dock, docks.left), - (&mut workspace.bottom_dock, docks.bottom), - ] - .iter_mut() - { - dock.update(cx, |dock, cx| { - dock.serialized_dock = Some(serialized_dock.clone()); - dock.restore_state(window, cx); - }); - } - - cx.notify(); - })?; - - let _ = project - .update(cx, |project, cx| { - project - .breakpoint_store() - .update(cx, |breakpoint_store, cx| { - breakpoint_store - .with_serialized_breakpoints(serialized_workspace.breakpoints, cx) - }) - })? - .await; - - // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means - // after loading the items, we might have different items and in order to avoid - // the database filling up, we delete items that haven't been loaded now. - // - // The items that have been loaded, have been saved after they've been added to the workspace. - let clean_up_tasks = workspace.update_in(cx, |_, window, cx| { - item_ids_by_kind - .into_iter() - .map(|(item_kind, loaded_items)| { - SerializableItemRegistry::cleanup( - item_kind, - serialized_workspace.id, - loaded_items, - window, - cx, - ) - .log_err() - }) - .collect::>() - })?; - - futures::future::join_all(clean_up_tasks).await; - - workspace - .update_in(cx, |workspace, window, cx| { - // Serialize ourself to make sure our timestamps and any pane / item changes are replicated - workspace.serialize_workspace_internal(window, cx).detach(); - - // Ensure that we mark the window as edited if we did load dirty items - workspace.update_window_edited(window, cx); - }) - .ok(); - - Ok(opened_items) - }) - } - - fn actions(&self, div: Div, window: &mut Window, cx: &mut Context) -> Div { - self.add_workspace_actions_listeners(div, window, cx) - .on_action(cx.listener( - |_workspace, action_sequence: &settings::ActionSequence, window, cx| { - for action in &action_sequence.0 { - window.dispatch_action(action.boxed_clone(), cx); - } - }, - )) - .on_action(cx.listener(Self::close_inactive_items_and_panes)) - .on_action(cx.listener(Self::close_all_items_and_panes)) - .on_action(cx.listener(Self::save_all)) - .on_action(cx.listener(Self::send_keystrokes)) - .on_action(cx.listener(Self::add_folder_to_project)) - .on_action(cx.listener(Self::follow_next_collaborator)) - .on_action(cx.listener(Self::close_window)) - .on_action(cx.listener(Self::activate_pane_at_index)) - .on_action(cx.listener(Self::move_item_to_pane_at_index)) - .on_action(cx.listener(Self::move_focused_panel_to_next_position)) - .on_action(cx.listener(Self::toggle_edit_predictions_all_files)) - .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| { - let pane = workspace.active_pane().clone(); - workspace.unfollow_in_pane(&pane, window, cx); - })) - .on_action(cx.listener(|workspace, action: &Save, window, cx| { - workspace - .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx) - .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None); - })) - .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| { - workspace - .save_active_item(SaveIntent::SaveWithoutFormat, window, cx) - .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None); - })) - .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| { - workspace - .save_active_item(SaveIntent::SaveAs, window, cx) - .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None); - })) - .on_action( - cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| { - workspace.activate_previous_pane(window, cx) - }), - ) - .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| { - workspace.activate_next_pane(window, cx) - })) - .on_action( - cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| { - workspace.activate_next_window(cx) - }), - ) - .on_action( - cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| { - workspace.activate_previous_window(cx) - }), - ) - .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| { - workspace.activate_pane_in_direction(SplitDirection::Left, window, cx) - })) - .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| { - workspace.activate_pane_in_direction(SplitDirection::Right, window, cx) - })) - .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| { - workspace.activate_pane_in_direction(SplitDirection::Up, window, cx) - })) - .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| { - workspace.activate_pane_in_direction(SplitDirection::Down, window, cx) - })) - .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| { - workspace.activate_next_pane(window, cx) - })) - .on_action(cx.listener( - |workspace, action: &MoveItemToPaneInDirection, window, cx| { - workspace.move_item_to_pane_in_direction(action, window, cx) - }, - )) - .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| { - workspace.swap_pane_in_direction(SplitDirection::Left, cx) - })) - .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| { - workspace.swap_pane_in_direction(SplitDirection::Right, cx) - })) - .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| { - workspace.swap_pane_in_direction(SplitDirection::Up, cx) - })) - .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| { - workspace.swap_pane_in_direction(SplitDirection::Down, cx) - })) - .on_action(cx.listener(|workspace, _: &SwapPaneAdjacent, window, cx| { - const DIRECTION_PRIORITY: [SplitDirection; 4] = [ - SplitDirection::Down, - SplitDirection::Up, - SplitDirection::Right, - SplitDirection::Left, - ]; - for dir in DIRECTION_PRIORITY { - if workspace.find_pane_in_direction(dir, cx).is_some() { - workspace.swap_pane_in_direction(dir, cx); - workspace.activate_pane_in_direction(dir.opposite(), window, cx); - break; - } - } - })) - .on_action(cx.listener(|workspace, _: &MovePaneLeft, _, cx| { - workspace.move_pane_to_border(SplitDirection::Left, cx) - })) - .on_action(cx.listener(|workspace, _: &MovePaneRight, _, cx| { - workspace.move_pane_to_border(SplitDirection::Right, cx) - })) - .on_action(cx.listener(|workspace, _: &MovePaneUp, _, cx| { - workspace.move_pane_to_border(SplitDirection::Up, cx) - })) - .on_action(cx.listener(|workspace, _: &MovePaneDown, _, cx| { - workspace.move_pane_to_border(SplitDirection::Down, cx) - })) - .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| { - this.toggle_dock(DockPosition::Left, window, cx); - })) - .on_action(cx.listener( - |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| { - workspace.toggle_dock(DockPosition::Right, window, cx); - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| { - workspace.toggle_dock(DockPosition::Bottom, window, cx); - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, _: &CloseActiveDock, window, cx| { - if !workspace.close_active_dock(window, cx) { - cx.propagate(); - } - }, - )) - .on_action( - cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| { - workspace.close_all_docks(window, cx); - }), - ) - .on_action(cx.listener(Self::toggle_all_docks)) - .on_action(cx.listener( - |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| { - workspace.clear_all_notifications(cx); - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, _: &ClearNavigationHistory, window, cx| { - workspace.clear_navigation_history(window, cx); - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, _: &SuppressNotification, _, cx| { - if let Some((notification_id, _)) = workspace.notifications.pop() { - workspace.suppress_notification(¬ification_id, cx); - } - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| { - workspace.reopen_closed_item(window, cx).detach(); - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, _: &ResetActiveDockSize, window, cx| { - for dock in workspace.all_docks() { - if dock.focus_handle(cx).contains_focused(window, cx) { - let Some(panel) = dock.read(cx).active_panel() else { - return; - }; - - // Set to `None`, then the size will fall back to the default. - panel.clone().set_size(None, window, cx); - - return; - } - } - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, _: &ResetOpenDocksSize, window, cx| { - for dock in workspace.all_docks() { - if let Some(panel) = dock.read(cx).visible_panel() { - // Set to `None`, then the size will fall back to the default. - panel.clone().set_size(None, window, cx); - } - } - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, act: &IncreaseActiveDockSize, window, cx| { - adjust_active_dock_size_by_px( - px_with_ui_font_fallback(act.px, cx), - workspace, - window, - cx, - ); - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, act: &DecreaseActiveDockSize, window, cx| { - adjust_active_dock_size_by_px( - px_with_ui_font_fallback(act.px, cx) * -1., - workspace, - window, - cx, - ); - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, act: &IncreaseOpenDocksSize, window, cx| { - adjust_open_docks_size_by_px( - px_with_ui_font_fallback(act.px, cx), - workspace, - window, - cx, - ); - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, act: &DecreaseOpenDocksSize, window, cx| { - adjust_open_docks_size_by_px( - px_with_ui_font_fallback(act.px, cx) * -1., - workspace, - window, - cx, - ); - }, - )) - .on_action(cx.listener(Workspace::toggle_centered_layout)) - .on_action(cx.listener( - |workspace: &mut Workspace, _action: &pane::ActivateNextItem, window, cx| { - if let Some(active_dock) = workspace.active_dock(window, cx) { - let dock = active_dock.read(cx); - if let Some(active_panel) = dock.active_panel() { - if active_panel.pane(cx).is_none() { - let mut recent_pane: Option> = None; - let mut recent_timestamp = 0; - for pane_handle in workspace.panes() { - let pane = pane_handle.read(cx); - for entry in pane.activation_history() { - if entry.timestamp > recent_timestamp { - recent_timestamp = entry.timestamp; - recent_pane = Some(pane_handle.clone()); - } - } - } - - if let Some(pane) = recent_pane { - pane.update(cx, |pane, cx| { - let current_index = pane.active_item_index(); - let items_len = pane.items_len(); - if items_len > 0 { - let next_index = if current_index + 1 < items_len { - current_index + 1 - } else { - 0 - }; - pane.activate_item( - next_index, false, false, window, cx, - ); - } - }); - return; - } - } - } - } - cx.propagate(); - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, _action: &pane::ActivatePreviousItem, window, cx| { - if let Some(active_dock) = workspace.active_dock(window, cx) { - let dock = active_dock.read(cx); - if let Some(active_panel) = dock.active_panel() { - if active_panel.pane(cx).is_none() { - let mut recent_pane: Option> = None; - let mut recent_timestamp = 0; - for pane_handle in workspace.panes() { - let pane = pane_handle.read(cx); - for entry in pane.activation_history() { - if entry.timestamp > recent_timestamp { - recent_timestamp = entry.timestamp; - recent_pane = Some(pane_handle.clone()); - } - } - } - - if let Some(pane) = recent_pane { - pane.update(cx, |pane, cx| { - let current_index = pane.active_item_index(); - let items_len = pane.items_len(); - if items_len > 0 { - let prev_index = if current_index > 0 { - current_index - 1 - } else { - items_len.saturating_sub(1) - }; - pane.activate_item( - prev_index, false, false, window, cx, - ); - } - }); - return; - } - } - } - } - cx.propagate(); - }, - )) - .on_action(cx.listener(Workspace::cancel)) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn set_random_database_id(&mut self) { - self.database_id = Some(WorkspaceId(Uuid::new_v4().as_u64_pair().0 as i64)); - } - - #[cfg(any(test, feature = "test-support"))] - pub fn test_new(project: Entity, window: &mut Window, cx: &mut Context) -> Self { - use node_runtime::NodeRuntime; - use session::Session; - - let client = project.read(cx).client(); - let user_store = project.read(cx).user_store(); - let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx)); - let session = cx.new(|cx| AppSession::new(Session::test(), cx)); - window.activate_window(); - let app_state = Arc::new(AppState { - languages: project.read(cx).languages().clone(), - workspace_store, - client, - user_store, - fs: project.read(cx).fs().clone(), - build_window_options: |_, _| Default::default(), - node_runtime: NodeRuntime::unavailable(), - session, - }); - let workspace = Self::new(Default::default(), project, app_state, window, cx); - workspace - .active_pane - .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))); - workspace - } - - pub fn register_action( - &mut self, - callback: impl Fn(&mut Self, &A, &mut Window, &mut Context) + 'static, - ) -> &mut Self { - let callback = Arc::new(callback); - - self.workspace_actions.push(Box::new(move |div, _, _, cx| { - let callback = callback.clone(); - div.on_action(cx.listener(move |workspace, event, window, cx| { - (callback)(workspace, event, window, cx) - })) - })); - self - } - pub fn register_action_renderer( - &mut self, - callback: impl Fn(Div, &Workspace, &mut Window, &mut Context) -> Div + 'static, - ) -> &mut Self { - self.workspace_actions.push(Box::new(callback)); - self - } - - fn add_workspace_actions_listeners( - &self, - mut div: Div, - window: &mut Window, - cx: &mut Context, - ) -> Div { - for action in self.workspace_actions.iter() { - div = (action)(div, self, window, cx) - } - div - } - - pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool { - self.modal_layer.read(cx).has_active_modal() - } - - pub fn active_modal(&self, cx: &App) -> Option> { - self.modal_layer.read(cx).active_modal() - } - - pub fn toggle_modal(&mut self, window: &mut Window, cx: &mut App, build: B) - where - B: FnOnce(&mut Window, &mut Context) -> V, - { - self.modal_layer.update(cx, |modal_layer, cx| { - modal_layer.toggle_modal(window, cx, build) - }) - } - - pub fn hide_modal(&mut self, window: &mut Window, cx: &mut App) -> bool { - self.modal_layer - .update(cx, |modal_layer, cx| modal_layer.hide_modal(window, cx)) - } - - pub fn toggle_status_toast(&mut self, entity: Entity, cx: &mut App) { - self.toast_layer - .update(cx, |toast_layer, cx| toast_layer.toggle_toast(cx, entity)) - } - - pub fn toggle_centered_layout( - &mut self, - _: &ToggleCenteredLayout, - _: &mut Window, - cx: &mut Context, - ) { - self.centered_layout = !self.centered_layout; - if let Some(database_id) = self.database_id() { - cx.background_spawn(DB.set_centered_layout(database_id, self.centered_layout)) - .detach_and_log_err(cx); - } - cx.notify(); - } - - fn adjust_padding(padding: Option) -> f32 { - padding - .unwrap_or(CenteredPaddingSettings::default().0) - .clamp( - CenteredPaddingSettings::MIN_PADDING, - CenteredPaddingSettings::MAX_PADDING, - ) - } - - fn render_dock( - &self, - position: DockPosition, - dock: &Entity, - window: &mut Window, - cx: &mut App, - ) -> Option
{ - if self.zoomed_position == Some(position) { - return None; - } - - let leader_border = dock.read(cx).active_panel().and_then(|panel| { - let pane = panel.pane(cx)?; - let follower_states = &self.follower_states; - leader_border_for_pane(follower_states, &pane, window, cx) - }); - - Some( - div() - .flex() - .flex_none() - .overflow_hidden() - .child(dock.clone()) - .children(leader_border), - ) - } - - pub fn for_window(window: &mut Window, _: &mut App) -> Option> { - window.root().flatten() - } - - pub fn zoomed_item(&self) -> Option<&AnyWeakView> { - self.zoomed.as_ref() - } - - pub fn activate_next_window(&mut self, cx: &mut Context) { - let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else { - return; - }; - let windows = cx.windows(); - let next_window = - SystemWindowTabController::get_next_tab_group_window(cx, current_window_id).or_else( - || { - windows - .iter() - .cycle() - .skip_while(|window| window.window_id() != current_window_id) - .nth(1) - }, - ); - - if let Some(window) = next_window { - window - .update(cx, |_, window, _| window.activate_window()) - .ok(); - } - } - - pub fn activate_previous_window(&mut self, cx: &mut Context) { - let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else { - return; - }; - let windows = cx.windows(); - let prev_window = - SystemWindowTabController::get_prev_tab_group_window(cx, current_window_id).or_else( - || { - windows - .iter() - .rev() - .cycle() - .skip_while(|window| window.window_id() != current_window_id) - .nth(1) - }, - ); - - if let Some(window) = prev_window { - window - .update(cx, |_, window, _| window.activate_window()) - .ok(); - } - } - - pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context) { - if cx.stop_active_drag(window) { - } else if let Some((notification_id, _)) = self.notifications.pop() { - dismiss_app_notification(¬ification_id, cx); - } else { - cx.propagate(); - } - } - - fn adjust_dock_size_by_px( - &mut self, - panel_size: Pixels, - dock_pos: DockPosition, - px: Pixels, - window: &mut Window, - cx: &mut Context, - ) { - match dock_pos { - DockPosition::Left => self.resize_left_dock(panel_size + px, window, cx), - DockPosition::Right => self.resize_right_dock(panel_size + px, window, cx), - DockPosition::Bottom => self.resize_bottom_dock(panel_size + px, window, cx), - } - } - - fn resize_left_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) { - let size = new_size.min(self.bounds.right() - RESIZE_HANDLE_SIZE); - - self.left_dock.update(cx, |left_dock, cx| { - if WorkspaceSettings::get_global(cx) - .resize_all_panels_in_dock - .contains(&DockPosition::Left) - { - left_dock.resize_all_panels(Some(size), window, cx); - } else { - left_dock.resize_active_panel(Some(size), window, cx); - } - }); - } - - fn resize_right_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) { - let mut size = new_size.max(self.bounds.left() - RESIZE_HANDLE_SIZE); - self.left_dock.read_with(cx, |left_dock, cx| { - let left_dock_size = left_dock - .active_panel_size(window, cx) - .unwrap_or(Pixels::ZERO); - if left_dock_size + size > self.bounds.right() { - size = self.bounds.right() - left_dock_size - } - }); - self.right_dock.update(cx, |right_dock, cx| { - if WorkspaceSettings::get_global(cx) - .resize_all_panels_in_dock - .contains(&DockPosition::Right) - { - right_dock.resize_all_panels(Some(size), window, cx); - } else { - right_dock.resize_active_panel(Some(size), window, cx); - } - }); - } - - fn resize_bottom_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) { - let size = new_size.min(self.bounds.bottom() - RESIZE_HANDLE_SIZE - self.bounds.top()); - self.bottom_dock.update(cx, |bottom_dock, cx| { - if WorkspaceSettings::get_global(cx) - .resize_all_panels_in_dock - .contains(&DockPosition::Bottom) - { - bottom_dock.resize_all_panels(Some(size), window, cx); - } else { - bottom_dock.resize_active_panel(Some(size), window, cx); - } - }); - } - - fn toggle_edit_predictions_all_files( - &mut self, - _: &ToggleEditPrediction, - _window: &mut Window, - cx: &mut Context, - ) { - let fs = self.project().read(cx).fs().clone(); - let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx); - update_settings_file(fs, cx, move |file, _| { - file.project.all_languages.defaults.show_edit_predictions = Some(!show_edit_predictions) - }); - } -} - -fn leader_border_for_pane( - follower_states: &HashMap, - pane: &Entity, - _: &Window, - cx: &App, -) -> Option
{ - let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| { - if state.pane() == pane { - Some((*leader_id, state)) - } else { - None - } - })?; - - let mut leader_color = match leader_id { - CollaboratorId::PeerId(leader_peer_id) => { - let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx); - let leader = room.remote_participant_for_peer_id(leader_peer_id)?; - - cx.theme() - .players() - .color_for_participant(leader.participant_index.0) - .cursor - } - CollaboratorId::Agent => cx.theme().players().agent().cursor, - }; - leader_color.fade_out(0.3); - Some( - div() - .absolute() - .size_full() - .left_0() - .top_0() - .border_2() - .border_color(leader_color), - ) -} - -fn window_bounds_env_override() -> Option> { - ZED_WINDOW_POSITION - .zip(*ZED_WINDOW_SIZE) - .map(|(position, size)| Bounds { - origin: position, - size, - }) -} - -fn open_items( - serialized_workspace: Option, - mut project_paths_to_open: Vec<(PathBuf, Option)>, - window: &mut Window, - cx: &mut Context, -) -> impl 'static + Future>>>>> + use<> { - let restored_items = serialized_workspace.map(|serialized_workspace| { - Workspace::load_workspace( - serialized_workspace, - project_paths_to_open - .iter() - .map(|(_, project_path)| project_path) - .cloned() - .collect(), - window, - cx, - ) - }); - - cx.spawn_in(window, async move |workspace, cx| { - let mut opened_items = Vec::with_capacity(project_paths_to_open.len()); - - if let Some(restored_items) = restored_items { - let restored_items = restored_items.await?; - - let restored_project_paths = restored_items - .iter() - .filter_map(|item| { - cx.update(|_, cx| item.as_ref()?.project_path(cx)) - .ok() - .flatten() - }) - .collect::>(); - - for restored_item in restored_items { - opened_items.push(restored_item.map(Ok)); - } - - project_paths_to_open - .iter_mut() - .for_each(|(_, project_path)| { - if let Some(project_path_to_open) = project_path - && restored_project_paths.contains(project_path_to_open) - { - *project_path = None; - } - }); - } else { - for _ in 0..project_paths_to_open.len() { - opened_items.push(None); - } - } - assert!(opened_items.len() == project_paths_to_open.len()); - - let tasks = - project_paths_to_open - .into_iter() - .enumerate() - .map(|(ix, (abs_path, project_path))| { - let workspace = workspace.clone(); - cx.spawn(async move |cx| { - let file_project_path = project_path?; - let abs_path_task = workspace.update(cx, |workspace, cx| { - workspace.project().update(cx, |project, cx| { - project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx) - }) - }); - - // We only want to open file paths here. If one of the items - // here is a directory, it was already opened further above - // with a `find_or_create_worktree`. - if let Ok(task) = abs_path_task - && task.await.is_none_or(|p| p.is_file()) - { - return Some(( - ix, - workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_path( - file_project_path, - None, - true, - window, - cx, - ) - }) - .log_err()? - .await, - )); - } - None - }) - }); - - let tasks = tasks.collect::>(); - - let tasks = futures::future::join_all(tasks); - for (ix, path_open_result) in tasks.await.into_iter().flatten() { - opened_items[ix] = Some(path_open_result); - } - - Ok(opened_items) - }) -} - -enum ActivateInDirectionTarget { - Pane(Entity), - Dock(Entity), -} - -fn notify_if_database_failed(workspace: WindowHandle, cx: &mut AsyncApp) { - workspace - .update(cx, |workspace, _, cx| { - if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) { - struct DatabaseFailedNotification; - - workspace.show_notification( - NotificationId::unique::(), - cx, - |cx| { - cx.new(|cx| { - MessageNotification::new("Failed to load the database file.", cx) - .primary_message("File an Issue") - .primary_icon(IconName::Plus) - .primary_on_click(|window, cx| { - window.dispatch_action(Box::new(FileBugReport), cx) - }) - }) - }, - ); - } - }) - .log_err(); -} - -fn px_with_ui_font_fallback(val: u32, cx: &Context) -> Pixels { - if val == 0 { - ThemeSettings::get_global(cx).ui_font_size(cx) - } else { - px(val as f32) - } -} - -fn adjust_active_dock_size_by_px( - px: Pixels, - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, -) { - let Some(active_dock) = workspace - .all_docks() - .into_iter() - .find(|dock| dock.focus_handle(cx).contains_focused(window, cx)) - else { - return; - }; - let dock = active_dock.read(cx); - let Some(panel_size) = dock.active_panel_size(window, cx) else { - return; - }; - let dock_pos = dock.position(); - workspace.adjust_dock_size_by_px(panel_size, dock_pos, px, window, cx); -} - -fn adjust_open_docks_size_by_px( - px: Pixels, - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, -) { - let docks = workspace - .all_docks() - .into_iter() - .filter_map(|dock| { - if dock.read(cx).is_open() { - let dock = dock.read(cx); - let panel_size = dock.active_panel_size(window, cx)?; - let dock_pos = dock.position(); - Some((panel_size, dock_pos, px)) - } else { - None - } - }) - .collect::>(); - - docks - .into_iter() - .for_each(|(panel_size, dock_pos, offset)| { - workspace.adjust_dock_size_by_px(panel_size, dock_pos, offset, window, cx); - }); -} - -impl Focusable for Workspace { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.active_pane.focus_handle(cx) - } -} - -#[derive(Clone)] -struct DraggedDock(DockPosition); - -impl Render for DraggedDock { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - gpui::Empty - } -} - -impl Render for Workspace { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - static FIRST_PAINT: AtomicBool = AtomicBool::new(true); - if FIRST_PAINT.swap(false, std::sync::atomic::Ordering::Relaxed) { - log::info!("Rendered first frame"); - } - let mut context = KeyContext::new_with_defaults(); - context.add("Workspace"); - context.set("keyboard_layout", cx.keyboard_layout().name().to_string()); - if let Some(status) = self - .debugger_provider - .as_ref() - .and_then(|provider| provider.active_thread_state(cx)) - { - match status { - ThreadStatus::Running | ThreadStatus::Stepping => { - context.add("debugger_running"); - } - ThreadStatus::Stopped => context.add("debugger_stopped"), - ThreadStatus::Exited | ThreadStatus::Ended => {} - } - } - - if self.left_dock.read(cx).is_open() { - if let Some(active_panel) = self.left_dock.read(cx).active_panel() { - context.set("left_dock", active_panel.panel_key()); - } - } - - if self.right_dock.read(cx).is_open() { - if let Some(active_panel) = self.right_dock.read(cx).active_panel() { - context.set("right_dock", active_panel.panel_key()); - } - } - - if self.bottom_dock.read(cx).is_open() { - if let Some(active_panel) = self.bottom_dock.read(cx).active_panel() { - context.set("bottom_dock", active_panel.panel_key()); - } - } - - let centered_layout = self.centered_layout - && self.center.panes().len() == 1 - && self.active_item(cx).is_some(); - let render_padding = |size| { - (size > 0.0).then(|| { - div() - .h_full() - .w(relative(size)) - .bg(cx.theme().colors().editor_background) - .border_color(cx.theme().colors().pane_group_border) - }) - }; - let paddings = if centered_layout { - let settings = WorkspaceSettings::get_global(cx).centered_layout; - ( - render_padding(Self::adjust_padding( - settings.left_padding.map(|padding| padding.0), - )), - render_padding(Self::adjust_padding( - settings.right_padding.map(|padding| padding.0), - )), - ) - } else { - (None, None) - }; - let ui_font = theme::setup_ui_font(window, cx); - - let theme = cx.theme().clone(); - let colors = theme.colors(); - let notification_entities = self - .notifications - .iter() - .map(|(_, notification)| notification.entity_id()) - .collect::>(); - let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout; - - client_side_decorations( - self.actions(div(), window, cx) - .key_context(context) - .relative() - .size_full() - .flex() - .flex_col() - .font(ui_font) - .gap_0() - .justify_start() - .items_start() - .text_color(colors.text) - .overflow_hidden() - .children(self.titlebar_item.clone()) - .on_modifiers_changed(move |_, _, cx| { - for &id in ¬ification_entities { - cx.notify(id); - } - }) - .child( - div() - .size_full() - .relative() - .flex_1() - .flex() - .flex_col() - .child( - div() - .id("workspace") - .bg(colors.background) - .relative() - .flex_1() - .w_full() - .flex() - .flex_col() - .overflow_hidden() - .border_t_1() - .border_b_1() - .border_color(colors.border) - .child({ - let this = cx.entity(); - canvas( - move |bounds, window, cx| { - this.update(cx, |this, cx| { - let bounds_changed = this.bounds != bounds; - this.bounds = bounds; - - if bounds_changed { - this.left_dock.update(cx, |dock, cx| { - dock.clamp_panel_size( - bounds.size.width, - window, - cx, - ) - }); - - this.right_dock.update(cx, |dock, cx| { - dock.clamp_panel_size( - bounds.size.width, - window, - cx, - ) - }); - - this.bottom_dock.update(cx, |dock, cx| { - dock.clamp_panel_size( - bounds.size.height, - window, - cx, - ) - }); - } - }) - }, - |_, _, _, _| {}, - ) - .absolute() - .size_full() - }) - .when(self.zoomed.is_none(), |this| { - this.on_drag_move(cx.listener( - move |workspace, - e: &DragMoveEvent, - window, - cx| { - if workspace.previous_dock_drag_coordinates - != Some(e.event.position) - { - workspace.previous_dock_drag_coordinates = - Some(e.event.position); - match e.drag(cx).0 { - DockPosition::Left => { - workspace.resize_left_dock( - e.event.position.x - - workspace.bounds.left(), - window, - cx, - ); - } - DockPosition::Right => { - workspace.resize_right_dock( - workspace.bounds.right() - - e.event.position.x, - window, - cx, - ); - } - DockPosition::Bottom => { - workspace.resize_bottom_dock( - workspace.bounds.bottom() - - e.event.position.y, - window, - cx, - ); - } - }; - workspace.serialize_workspace(window, cx); - } - }, - )) - }) - .child({ - match bottom_dock_layout { - BottomDockLayout::Full => div() - .flex() - .flex_col() - .h_full() - .child( - div() - .flex() - .flex_row() - .flex_1() - .overflow_hidden() - .children(self.render_dock( - DockPosition::Left, - &self.left_dock, - window, - cx, - )) - .child( - div() - .flex() - .flex_col() - .flex_1() - .overflow_hidden() - .child( - h_flex() - .flex_1() - .when_some( - paddings.0, - |this, p| { - this.child( - p.border_r_1(), - ) - }, - ) - .child(self.center.render( - self.zoomed.as_ref(), - &PaneRenderContext { - follower_states: - &self.follower_states, - active_call: self.active_call(), - active_pane: &self.active_pane, - app_state: &self.app_state, - project: &self.project, - workspace: &self.weak_self, - }, - window, - cx, - )) - .when_some( - paddings.1, - |this, p| { - this.child( - p.border_l_1(), - ) - }, - ), - ), - ) - .children(self.render_dock( - DockPosition::Right, - &self.right_dock, - window, - cx, - )), - ) - .child(div().w_full().children(self.render_dock( - DockPosition::Bottom, - &self.bottom_dock, - window, - cx - ))), - - BottomDockLayout::LeftAligned => div() - .flex() - .flex_row() - .h_full() - .child( - div() - .flex() - .flex_col() - .flex_1() - .h_full() - .child( - div() - .flex() - .flex_row() - .flex_1() - .children(self.render_dock(DockPosition::Left, &self.left_dock, window, cx)) - .child( - div() - .flex() - .flex_col() - .flex_1() - .overflow_hidden() - .child( - h_flex() - .flex_1() - .when_some(paddings.0, |this, p| this.child(p.border_r_1())) - .child(self.center.render( - self.zoomed.as_ref(), - &PaneRenderContext { - follower_states: - &self.follower_states, - active_call: self.active_call(), - active_pane: &self.active_pane, - app_state: &self.app_state, - project: &self.project, - workspace: &self.weak_self, - }, - window, - cx, - )) - .when_some(paddings.1, |this, p| this.child(p.border_l_1())), - ) - ) - ) - .child( - div() - .w_full() - .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx)) - ), - ) - .children(self.render_dock( - DockPosition::Right, - &self.right_dock, - window, - cx, - )), - - BottomDockLayout::RightAligned => div() - .flex() - .flex_row() - .h_full() - .children(self.render_dock( - DockPosition::Left, - &self.left_dock, - window, - cx, - )) - .child( - div() - .flex() - .flex_col() - .flex_1() - .h_full() - .child( - div() - .flex() - .flex_row() - .flex_1() - .child( - div() - .flex() - .flex_col() - .flex_1() - .overflow_hidden() - .child( - h_flex() - .flex_1() - .when_some(paddings.0, |this, p| this.child(p.border_r_1())) - .child(self.center.render( - self.zoomed.as_ref(), - &PaneRenderContext { - follower_states: - &self.follower_states, - active_call: self.active_call(), - active_pane: &self.active_pane, - app_state: &self.app_state, - project: &self.project, - workspace: &self.weak_self, - }, - window, - cx, - )) - .when_some(paddings.1, |this, p| this.child(p.border_l_1())), - ) - ) - .children(self.render_dock(DockPosition::Right, &self.right_dock, window, cx)) - ) - .child( - div() - .w_full() - .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx)) - ), - ), - - BottomDockLayout::Contained => div() - .flex() - .flex_row() - .h_full() - .children(self.render_dock( - DockPosition::Left, - &self.left_dock, - window, - cx, - )) - .child( - div() - .flex() - .flex_col() - .flex_1() - .overflow_hidden() - .child( - h_flex() - .flex_1() - .when_some(paddings.0, |this, p| { - this.child(p.border_r_1()) - }) - .child(self.center.render( - self.zoomed.as_ref(), - &PaneRenderContext { - follower_states: - &self.follower_states, - active_call: self.active_call(), - active_pane: &self.active_pane, - app_state: &self.app_state, - project: &self.project, - workspace: &self.weak_self, - }, - window, - cx, - )) - .when_some(paddings.1, |this, p| { - this.child(p.border_l_1()) - }), - ) - .children(self.render_dock( - DockPosition::Bottom, - &self.bottom_dock, - window, - cx, - )), - ) - .children(self.render_dock( - DockPosition::Right, - &self.right_dock, - window, - cx, - )), - } - }) - .children(self.zoomed.as_ref().and_then(|view| { - let zoomed_view = view.upgrade()?; - let div = div() - .occlude() - .absolute() - .overflow_hidden() - .border_color(colors.border) - .bg(colors.background) - .child(zoomed_view) - .inset_0() - .shadow_lg(); - - if !WorkspaceSettings::get_global(cx).zoomed_padding { - return Some(div); - } - - Some(match self.zoomed_position { - Some(DockPosition::Left) => div.right_2().border_r_1(), - Some(DockPosition::Right) => div.left_2().border_l_1(), - Some(DockPosition::Bottom) => div.top_2().border_t_1(), - None => { - div.top_2().bottom_2().left_2().right_2().border_1() - } - }) - })) - .children(self.render_notifications(window, cx)), - ) - .when(self.status_bar_visible(cx), |parent| { - parent.child(self.status_bar.clone()) - }) - .child(self.modal_layer.clone()) - .child(self.toast_layer.clone()), - ), - window, - cx, - ) - } -} - -impl WorkspaceStore { - pub fn new(client: Arc, cx: &mut Context) -> Self { - Self { - workspaces: Default::default(), - _subscriptions: vec![ - client.add_request_handler(cx.weak_entity(), Self::handle_follow), - client.add_message_handler(cx.weak_entity(), Self::handle_update_followers), - ], - client, - } - } - - pub fn update_followers( - &self, - project_id: Option, - update: proto::update_followers::Variant, - cx: &App, - ) -> Option<()> { - let active_call = ActiveCall::try_global(cx)?; - let room_id = active_call.read(cx).room()?.read(cx).id(); - self.client - .send(proto::UpdateFollowers { - room_id, - project_id, - variant: Some(update), - }) - .log_err() - } - - pub async fn handle_follow( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - this.update(&mut cx, |this, cx| { - let follower = Follower { - project_id: envelope.payload.project_id, - peer_id: envelope.original_sender_id()?, - }; - - let mut response = proto::FollowResponse::default(); - this.workspaces.retain(|workspace| { - workspace - .update(cx, |workspace, window, cx| { - let handler_response = - workspace.handle_follow(follower.project_id, window, cx); - if let Some(active_view) = handler_response.active_view - && workspace.project.read(cx).remote_id() == follower.project_id - { - response.active_view = Some(active_view) - } - }) - .is_ok() - }); - - Ok(response) - })? - } - - async fn handle_update_followers( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let leader_id = envelope.original_sender_id()?; - let update = envelope.payload; - - this.update(&mut cx, |this, cx| { - this.workspaces.retain(|workspace| { - workspace - .update(cx, |workspace, window, cx| { - let project_id = workspace.project.read(cx).remote_id(); - if update.project_id != project_id && update.project_id.is_some() { - return; - } - workspace.handle_update_followers(leader_id, update.clone(), window, cx); - }) - .is_ok() - }); - Ok(()) - })? - } - - pub fn workspaces(&self) -> &HashSet> { - &self.workspaces - } -} - -impl ViewId { - pub(crate) fn from_proto(message: proto::ViewId) -> Result { - Ok(Self { - creator: message - .creator - .map(CollaboratorId::PeerId) - .context("creator is missing")?, - id: message.id, - }) - } - - pub(crate) fn to_proto(self) -> Option { - if let CollaboratorId::PeerId(peer_id) = self.creator { - Some(proto::ViewId { - creator: Some(peer_id), - id: self.id, - }) - } else { - None - } - } -} - -impl FollowerState { - fn pane(&self) -> &Entity { - self.dock_pane.as_ref().unwrap_or(&self.center_pane) - } -} - -pub trait WorkspaceHandle { - fn file_project_paths(&self, cx: &App) -> Vec; -} - -impl WorkspaceHandle for Entity { - fn file_project_paths(&self, cx: &App) -> Vec { - self.read(cx) - .worktrees(cx) - .flat_map(|worktree| { - let worktree_id = worktree.read(cx).id(); - worktree.read(cx).files(true, 0).map(move |f| ProjectPath { - worktree_id, - path: f.path.clone(), - }) - }) - .collect::>() - } -} - -pub async fn last_opened_workspace_location() -> Option<(SerializedWorkspaceLocation, PathList)> { - DB.last_workspace().await.log_err().flatten() -} - -pub fn last_session_workspace_locations( - last_session_id: &str, - last_session_window_stack: Option>, -) -> Option> { - DB.last_session_workspace_locations(last_session_id, last_session_window_stack) - .log_err() -} - -actions!( - collab, - [ - /// Opens the channel notes for the current call. - /// - /// Use `collab_panel::OpenSelectedChannelNotes` to open the channel notes for the selected - /// channel in the collab panel. - /// - /// If you want to open a specific channel, use `zed::OpenZedUrl` with a channel notes URL - - /// can be copied via "Copy link to section" in the context menu of the channel notes - /// buffer. These URLs look like `https://zed.dev/channel/channel-name-CHANNEL_ID/notes`. - OpenChannelNotes, - /// Mutes your microphone. - Mute, - /// Deafens yourself (mute both microphone and speakers). - Deafen, - /// Leaves the current call. - LeaveCall, - /// Shares the current project with collaborators. - ShareProject, - /// Shares your screen with collaborators. - ScreenShare, - /// Copies the current room name and session id for debugging purposes. - CopyRoomId, - ] -); -actions!( - zed, - [ - /// Opens the Zed log file. - OpenLog, - /// Reveals the Zed log file in the system file manager. - RevealLogInFileManager - ] -); - -async fn join_channel_internal( - channel_id: ChannelId, - app_state: &Arc, - requesting_window: Option>, - active_call: &Entity, - cx: &mut AsyncApp, -) -> Result { - let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| { - let Some(room) = active_call.room().map(|room| room.read(cx)) else { - return (false, None); - }; - - let already_in_channel = room.channel_id() == Some(channel_id); - let should_prompt = room.is_sharing_project() - && !room.remote_participants().is_empty() - && !already_in_channel; - let open_room = if already_in_channel { - active_call.room().cloned() - } else { - None - }; - (should_prompt, open_room) - })?; - - if let Some(room) = open_room { - let task = room.update(cx, |room, cx| { - if let Some((project, host)) = room.most_active_project(cx) { - return Some(join_in_room_project(project, host, app_state.clone(), cx)); - } - - None - })?; - if let Some(task) = task { - task.await?; - } - return anyhow::Ok(true); - } - - if should_prompt { - if let Some(workspace) = requesting_window { - let answer = workspace - .update(cx, |_, window, cx| { - window.prompt( - PromptLevel::Warning, - "Do you want to switch channels?", - Some("Leaving this call will unshare your current project."), - &["Yes, Join Channel", "Cancel"], - cx, - ) - })? - .await; - - if answer == Ok(1) { - return Ok(false); - } - } else { - return Ok(false); // unreachable!() hopefully - } - } - - let client = cx.update(|cx| active_call.read(cx).client())?; - - let mut client_status = client.status(); - - // this loop will terminate within client::CONNECTION_TIMEOUT seconds. - 'outer: loop { - let Some(status) = client_status.recv().await else { - anyhow::bail!("error connecting"); - }; - - match status { - Status::Connecting - | Status::Authenticating - | Status::Authenticated - | Status::Reconnecting - | Status::Reauthenticating - | Status::Reauthenticated => continue, - Status::Connected { .. } => break 'outer, - Status::SignedOut | Status::AuthenticationError => { - return Err(ErrorCode::SignedOut.into()); - } - Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()), - Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => { - return Err(ErrorCode::Disconnected.into()); - } - } - } - - let room = active_call - .update(cx, |active_call, cx| { - active_call.join_channel(channel_id, cx) - })? - .await?; - - let Some(room) = room else { - return anyhow::Ok(true); - }; - - room.update(cx, |room, _| room.room_update_completed())? - .await; - - let task = room.update(cx, |room, cx| { - if let Some((project, host)) = room.most_active_project(cx) { - return Some(join_in_room_project(project, host, app_state.clone(), cx)); - } - - // If you are the first to join a channel, see if you should share your project. - if room.remote_participants().is_empty() - && !room.local_participant_is_guest() - && let Some(workspace) = requesting_window - { - let project = workspace.update(cx, |workspace, _, cx| { - let project = workspace.project.read(cx); - - if !CallSettings::get_global(cx).share_on_join { - return None; - } - - if (project.is_local() || project.is_via_remote_server()) - && project.visible_worktrees(cx).any(|tree| { - tree.read(cx) - .root_entry() - .is_some_and(|entry| entry.is_dir()) - }) - { - Some(workspace.project.clone()) - } else { - None - } - }); - if let Ok(Some(project)) = project { - return Some(cx.spawn(async move |room, cx| { - room.update(cx, |room, cx| room.share_project(project, cx))? - .await?; - Ok(()) - })); - } - } - - None - })?; - if let Some(task) = task { - task.await?; - return anyhow::Ok(true); - } - anyhow::Ok(false) -} - -pub fn join_channel( - channel_id: ChannelId, - app_state: Arc, - requesting_window: Option>, - cx: &mut App, -) -> Task> { - let active_call = ActiveCall::global(cx); - cx.spawn(async move |cx| { - let result = - join_channel_internal(channel_id, &app_state, requesting_window, &active_call, cx) - .await; - - // join channel succeeded, and opened a window - if matches!(result, Ok(true)) { - return anyhow::Ok(()); - } - - // find an existing workspace to focus and show call controls - let mut active_window = requesting_window.or_else(|| activate_any_workspace_window(cx)); - if active_window.is_none() { - // no open workspaces, make one to show the error in (blergh) - let (window_handle, _) = cx - .update(|cx| { - Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx) - })? - .await?; - - if result.is_ok() { - cx.update(|cx| { - cx.dispatch_action(&OpenChannelNotes); - }) - .log_err(); - } - - active_window = Some(window_handle); - } - - if let Err(err) = result { - log::error!("failed to join channel: {}", err); - if let Some(active_window) = active_window { - active_window - .update(cx, |_, window, cx| { - let detail: SharedString = match err.error_code() { - ErrorCode::SignedOut => "Please sign in to continue.".into(), - ErrorCode::UpgradeRequired => concat!( - "Your are running an unsupported version of Zed. ", - "Please update to continue." - ) - .into(), - ErrorCode::NoSuchChannel => concat!( - "No matching channel was found. ", - "Please check the link and try again." - ) - .into(), - ErrorCode::Forbidden => concat!( - "This channel is private, and you do not have access. ", - "Please ask someone to add you and try again." - ) - .into(), - ErrorCode::Disconnected => { - "Please check your internet connection and try again.".into() - } - _ => format!("{}\n\nPlease try again.", err).into(), - }; - window.prompt( - PromptLevel::Critical, - "Failed to join channel", - Some(&detail), - &["Ok"], - cx, - ) - })? - .await - .ok(); - } - } - - // return ok, we showed the error to the user. - anyhow::Ok(()) - }) -} - -pub async fn get_any_active_workspace( - app_state: Arc, - mut cx: AsyncApp, -) -> anyhow::Result> { - // find an existing workspace to focus and show call controls - let active_window = activate_any_workspace_window(&mut cx); - if active_window.is_none() { - cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))? - .await?; - } - activate_any_workspace_window(&mut cx).context("could not open zed") -} - -fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option> { - cx.update(|cx| { - if let Some(workspace_window) = cx - .active_window() - .and_then(|window| window.downcast::()) - { - return Some(workspace_window); - } - - for window in cx.windows() { - if let Some(workspace_window) = window.downcast::() { - workspace_window - .update(cx, |_, window, _| window.activate_window()) - .ok(); - return Some(workspace_window); - } - } - None - }) - .ok() - .flatten() -} - -pub fn local_workspace_windows(cx: &App) -> Vec> { - cx.windows() - .into_iter() - .filter_map(|window| window.downcast::()) - .filter(|workspace| { - workspace - .read(cx) - .is_ok_and(|workspace| workspace.project.read(cx).is_local()) - }) - .collect() -} - -#[derive(Default)] -pub struct OpenOptions { - pub visible: Option, - pub focus: Option, - pub open_new_workspace: Option, - pub prefer_focused_window: bool, - pub replace_window: Option>, - pub env: Option>, -} - -#[allow(clippy::type_complexity)] -pub fn open_paths( - abs_paths: &[PathBuf], - app_state: Arc, - open_options: OpenOptions, - cx: &mut App, -) -> Task< - anyhow::Result<( - WindowHandle, - Vec>>>, - )>, -> { - let abs_paths = abs_paths.to_vec(); - let mut existing = None; - let mut best_match = None; - let mut open_visible = OpenVisible::All; - #[cfg(target_os = "windows")] - let wsl_path = abs_paths - .iter() - .find_map(|p| util::paths::WslPath::from_path(p)); - - cx.spawn(async move |cx| { - if open_options.open_new_workspace != Some(true) { - let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path)); - let all_metadatas = futures::future::join_all(all_paths) - .await - .into_iter() - .filter_map(|result| result.ok().flatten()) - .collect::>(); - - cx.update(|cx| { - for window in local_workspace_windows(cx) { - if let Ok(workspace) = window.read(cx) { - let m = workspace.project.read(cx).visibility_for_paths( - &abs_paths, - &all_metadatas, - open_options.open_new_workspace == None, - cx, - ); - if m > best_match { - existing = Some(window); - best_match = m; - } else if best_match.is_none() - && open_options.open_new_workspace == Some(false) - { - existing = Some(window) - } - } - } - })?; - - if open_options.open_new_workspace.is_none() - && (existing.is_none() || open_options.prefer_focused_window) - && all_metadatas.iter().all(|file| !file.is_dir) - { - cx.update(|cx| { - if let Some(window) = cx - .active_window() - .and_then(|window| window.downcast::()) - && let Ok(workspace) = window.read(cx) - { - let project = workspace.project().read(cx); - if project.is_local() && !project.is_via_collab() { - existing = Some(window); - open_visible = OpenVisible::None; - return; - } - } - for window in local_workspace_windows(cx) { - if let Ok(workspace) = window.read(cx) { - let project = workspace.project().read(cx); - if project.is_via_collab() { - continue; - } - existing = Some(window); - open_visible = OpenVisible::None; - break; - } - } - })?; - } - } - - let result = if let Some(existing) = existing { - let open_task = existing - .update(cx, |workspace, window, cx| { - window.activate_window(); - workspace.open_paths( - abs_paths, - OpenOptions { - visible: Some(open_visible), - ..Default::default() - }, - None, - window, - cx, - ) - })? - .await; - - _ = existing.update(cx, |workspace, _, cx| { - for item in open_task.iter().flatten() { - if let Err(e) = item { - workspace.show_error(&e, cx); - } - } - }); - - Ok((existing, open_task)) - } else { - cx.update(move |cx| { - Workspace::new_local( - abs_paths, - app_state.clone(), - open_options.replace_window, - open_options.env, - cx, - ) - })? - .await - }; - - #[cfg(target_os = "windows")] - if let Some(util::paths::WslPath{distro, path}) = wsl_path - && let Ok((workspace, _)) = &result - { - workspace - .update(cx, move |workspace, _window, cx| { - struct OpenInWsl; - workspace.show_notification(NotificationId::unique::(), cx, move |cx| { - let display_path = util::markdown::MarkdownInlineCode(&path.to_string_lossy()); - let msg = format!("{display_path} is inside a WSL filesystem, some features may not work unless you open it with WSL remote"); - cx.new(move |cx| { - MessageNotification::new(msg, cx) - .primary_message("Open in WSL") - .primary_icon(IconName::FolderOpen) - .primary_on_click(move |window, cx| { - window.dispatch_action(Box::new(remote::OpenWslPath { - distro: remote::WslConnectionOptions { - distro_name: distro.clone(), - user: None, - }, - paths: vec![path.clone().into()], - }), cx) - }) - }) - }); - }) - .unwrap(); - }; - result - }) -} - -pub fn open_new( - open_options: OpenOptions, - app_state: Arc, - cx: &mut App, - init: impl FnOnce(&mut Workspace, &mut Window, &mut Context) + 'static + Send, -) -> Task> { - let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx); - cx.spawn(async move |cx| { - let (workspace, opened_paths) = task.await?; - workspace.update(cx, |workspace, window, cx| { - if opened_paths.is_empty() { - init(workspace, window, cx) - } - })?; - Ok(()) - }) -} - -pub fn create_and_open_local_file( - path: &'static Path, - window: &mut Window, - cx: &mut Context, - default_content: impl 'static + Send + FnOnce() -> Rope, -) -> Task>> { - cx.spawn_in(window, async move |workspace, cx| { - let fs = workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?; - if !fs.is_file(path).await { - fs.create_file(path, Default::default()).await?; - fs.save(path, &default_content(), Default::default()) - .await?; - } - - let mut items = workspace - .update_in(cx, |workspace, window, cx| { - workspace.with_local_workspace(window, cx, |workspace, window, cx| { - workspace.open_paths( - vec![path.to_path_buf()], - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - None, - window, - cx, - ) - }) - })? - .await? - .await; - - let item = items.pop().flatten(); - item.with_context(|| format!("path {path:?} is not a file"))? - }) -} - -pub fn open_remote_project_with_new_connection( - window: WindowHandle, - remote_connection: Arc, - cancel_rx: oneshot::Receiver<()>, - delegate: Arc, - app_state: Arc, - paths: Vec, - cx: &mut App, -) -> Task>>>> { - cx.spawn(async move |cx| { - let (workspace_id, serialized_workspace) = - deserialize_remote_project(remote_connection.connection_options(), paths.clone(), cx) - .await?; - - let session = match cx - .update(|cx| { - remote::RemoteClient::new( - ConnectionIdentifier::Workspace(workspace_id.0), - remote_connection, - cancel_rx, - delegate, - cx, - ) - })? - .await? - { - Some(result) => result, - None => return Ok(Vec::new()), - }; - - let project = cx.update(|cx| { - project::Project::remote( - session, - app_state.client.clone(), - app_state.node_runtime.clone(), - app_state.user_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - cx, - ) - })?; - - open_remote_project_inner( - project, - paths, - workspace_id, - serialized_workspace, - app_state, - window, - cx, - ) - .await - }) -} - -pub fn open_remote_project_with_existing_connection( - connection_options: RemoteConnectionOptions, - project: Entity, - paths: Vec, - app_state: Arc, - window: WindowHandle, - cx: &mut AsyncApp, -) -> Task>>>> { - cx.spawn(async move |cx| { - let (workspace_id, serialized_workspace) = - deserialize_remote_project(connection_options.clone(), paths.clone(), cx).await?; - - open_remote_project_inner( - project, - paths, - workspace_id, - serialized_workspace, - app_state, - window, - cx, - ) - .await - }) -} - -async fn open_remote_project_inner( - project: Entity, - paths: Vec, - workspace_id: WorkspaceId, - serialized_workspace: Option, - app_state: Arc, - window: WindowHandle, - cx: &mut AsyncApp, -) -> Result>>> { - let toolchains = DB.toolchains(workspace_id).await?; - for (toolchain, worktree_id, path) in toolchains { - project - .update(cx, |this, cx| { - this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx) - })? - .await; - } - let mut project_paths_to_open = vec![]; - let mut project_path_errors = vec![]; - - for path in paths { - let result = cx - .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))? - .await; - match result { - Ok((_, project_path)) => { - project_paths_to_open.push((path.clone(), Some(project_path))); - } - Err(error) => { - project_path_errors.push(error); - } - }; - } - - if project_paths_to_open.is_empty() { - return Err(project_path_errors.pop().context("no paths given")?); - } - - if let Some(detach_session_task) = window - .update(cx, |_workspace, window, cx| { - cx.spawn_in(window, async move |this, cx| { - this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx)) - }) - }) - .ok() - { - detach_session_task.await.ok(); - } - - cx.update_window(window.into(), |_, window, cx| { - window.replace_root(cx, |window, cx| { - telemetry::event!("SSH Project Opened"); - - let mut workspace = - Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx); - workspace.update_history(cx); - - if let Some(ref serialized) = serialized_workspace { - workspace.centered_layout = serialized.centered_layout; - } - - workspace - }); - })?; - - let items = window - .update(cx, |_, window, cx| { - window.activate_window(); - open_items(serialized_workspace, project_paths_to_open, window, cx) - })? - .await?; - - window.update(cx, |workspace, _, cx| { - for error in project_path_errors { - if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist { - if let Some(path) = error.error_tag("path") { - workspace.show_error(&anyhow!("'{path}' does not exist"), cx) - } - } else { - workspace.show_error(&error, cx) - } - } - })?; - - Ok(items.into_iter().map(|item| item?.ok()).collect()) -} - -fn deserialize_remote_project( - connection_options: RemoteConnectionOptions, - paths: Vec, - cx: &AsyncApp, -) -> Task)>> { - cx.background_spawn(async move { - let remote_connection_id = persistence::DB - .get_or_create_remote_connection(connection_options) - .await?; - - let serialized_workspace = - persistence::DB.remote_workspace_for_roots(&paths, remote_connection_id); - - let workspace_id = if let Some(workspace_id) = - serialized_workspace.as_ref().map(|workspace| workspace.id) - { - workspace_id - } else { - persistence::DB.next_id().await? - }; - - Ok((workspace_id, serialized_workspace)) - }) -} - -pub fn join_in_room_project( - project_id: u64, - follow_user_id: u64, - app_state: Arc, - cx: &mut App, -) -> Task> { - let windows = cx.windows(); - cx.spawn(async move |cx| { - let existing_workspace = windows.into_iter().find_map(|window_handle| { - window_handle - .downcast::() - .and_then(|window_handle| { - window_handle - .update(cx, |workspace, _window, cx| { - if workspace.project().read(cx).remote_id() == Some(project_id) { - Some(window_handle) - } else { - None - } - }) - .unwrap_or(None) - }) - }); - - let workspace = if let Some(existing_workspace) = existing_workspace { - existing_workspace - } else { - let active_call = cx.update(|cx| ActiveCall::global(cx))?; - let room = active_call - .read_with(cx, |call, _| call.room().cloned())? - .context("not in a call")?; - let project = room - .update(cx, |room, cx| { - room.join_project( - project_id, - app_state.languages.clone(), - app_state.fs.clone(), - cx, - ) - })? - .await?; - - let window_bounds_override = window_bounds_env_override(); - cx.update(|cx| { - let mut options = (app_state.build_window_options)(None, cx); - options.window_bounds = window_bounds_override.map(WindowBounds::Windowed); - cx.open_window(options, |window, cx| { - cx.new(|cx| { - Workspace::new(Default::default(), project, app_state.clone(), window, cx) - }) - }) - })?? - }; - - workspace.update(cx, |workspace, window, cx| { - cx.activate(true); - window.activate_window(); - - if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - let follow_peer_id = room - .read(cx) - .remote_participants() - .iter() - .find(|(_, participant)| participant.user.id == follow_user_id) - .map(|(_, p)| p.peer_id) - .or_else(|| { - // If we couldn't follow the given user, follow the host instead. - let collaborator = workspace - .project() - .read(cx) - .collaborators() - .values() - .find(|collaborator| collaborator.is_host)?; - Some(collaborator.peer_id) - }); - - if let Some(follow_peer_id) = follow_peer_id { - workspace.follow(follow_peer_id, window, cx); - } - } - })?; - - anyhow::Ok(()) - }) -} - -pub fn reload(cx: &mut App) { - let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit; - let mut workspace_windows = cx - .windows() - .into_iter() - .filter_map(|window| window.downcast::()) - .collect::>(); - - // If multiple windows have unsaved changes, and need a save prompt, - // prompt in the active window before switching to a different window. - workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false)); - - let mut prompt = None; - if let (true, Some(window)) = (should_confirm, workspace_windows.first()) { - prompt = window - .update(cx, |_, window, cx| { - window.prompt( - PromptLevel::Info, - "Are you sure you want to restart?", - None, - &["Restart", "Cancel"], - cx, - ) - }) - .ok(); - } - - cx.spawn(async move |cx| { - if let Some(prompt) = prompt { - let answer = prompt.await?; - if answer != 0 { - return Ok(()); - } - } - - // If the user cancels any save prompt, then keep the app open. - for window in workspace_windows { - if let Ok(should_close) = window.update(cx, |workspace, window, cx| { - workspace.prepare_to_close(CloseIntent::Quit, window, cx) - }) && !should_close.await? - { - return Ok(()); - } - } - cx.update(|cx| cx.restart()) - }) - .detach_and_log_err(cx); -} - -fn parse_pixel_position_env_var(value: &str) -> Option> { - let mut parts = value.split(','); - let x: usize = parts.next()?.parse().ok()?; - let y: usize = parts.next()?.parse().ok()?; - Some(point(px(x as f32), px(y as f32))) -} - -fn parse_pixel_size_env_var(value: &str) -> Option> { - let mut parts = value.split(','); - let width: usize = parts.next()?.parse().ok()?; - let height: usize = parts.next()?.parse().ok()?; - Some(size(px(width as f32), px(height as f32))) -} - -/// Add client-side decorations (rounded corners, shadows, resize handling) when appropriate. -pub fn client_side_decorations( - element: impl IntoElement, - window: &mut Window, - cx: &mut App, -) -> Stateful
{ - const BORDER_SIZE: Pixels = px(1.0); - let decorations = window.window_decorations(); - - match decorations { - Decorations::Client { .. } => window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW), - Decorations::Server => window.set_client_inset(px(0.0)), - } - - struct GlobalResizeEdge(ResizeEdge); - impl Global for GlobalResizeEdge {} - - div() - .id("window-backdrop") - .bg(transparent_black()) - .map(|div| match decorations { - Decorations::Server => div, - Decorations::Client { tiling, .. } => div - .when(!(tiling.top || tiling.right), |div| { - div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.top || tiling.left), |div| { - div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.right), |div| { - div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.left), |div| { - div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!tiling.top, |div| { - div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW) - }) - .when(!tiling.bottom, |div| { - div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW) - }) - .when(!tiling.left, |div| { - div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW) - }) - .when(!tiling.right, |div| { - div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW) - }) - .on_mouse_move(move |e, window, cx| { - let size = window.window_bounds().get_bounds().size; - let pos = e.position; - - let new_edge = - resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling); - - let edge = cx.try_global::(); - if new_edge != edge.map(|edge| edge.0) { - window - .window_handle() - .update(cx, |workspace, _, cx| { - cx.notify(workspace.entity_id()); - }) - .ok(); - } - }) - .on_mouse_down(MouseButton::Left, move |e, window, _| { - let size = window.window_bounds().get_bounds().size; - let pos = e.position; - - let edge = match resize_edge( - pos, - theme::CLIENT_SIDE_DECORATION_SHADOW, - size, - tiling, - ) { - Some(value) => value, - None => return, - }; - - window.start_window_resize(edge); - }), - }) - .size_full() - .child( - div() - .cursor(CursorStyle::Arrow) - .map(|div| match decorations { - Decorations::Server => div, - Decorations::Client { tiling } => div - .border_color(cx.theme().colors().border) - .when(!(tiling.top || tiling.right), |div| { - div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.top || tiling.left), |div| { - div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.right), |div| { - div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.left), |div| { - div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!tiling.top, |div| div.border_t(BORDER_SIZE)) - .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE)) - .when(!tiling.left, |div| div.border_l(BORDER_SIZE)) - .when(!tiling.right, |div| div.border_r(BORDER_SIZE)) - .when(!tiling.is_tiled(), |div| { - div.shadow(vec![gpui::BoxShadow { - color: Hsla { - h: 0., - s: 0., - l: 0., - a: 0.4, - }, - blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2., - spread_radius: px(0.), - offset: point(px(0.0), px(0.0)), - }]) - }), - }) - .on_mouse_move(|_e, _, cx| { - cx.stop_propagation(); - }) - .size_full() - .child(element), - ) - .map(|div| match decorations { - Decorations::Server => div, - Decorations::Client { tiling, .. } => div.child( - canvas( - |_bounds, window, _| { - window.insert_hitbox( - Bounds::new( - point(px(0.0), px(0.0)), - window.window_bounds().get_bounds().size, - ), - HitboxBehavior::Normal, - ) - }, - move |_bounds, hitbox, window, cx| { - let mouse = window.mouse_position(); - let size = window.window_bounds().get_bounds().size; - let Some(edge) = - resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling) - else { - return; - }; - cx.set_global(GlobalResizeEdge(edge)); - window.set_cursor_style( - match edge { - ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown, - ResizeEdge::Left | ResizeEdge::Right => { - CursorStyle::ResizeLeftRight - } - ResizeEdge::TopLeft | ResizeEdge::BottomRight => { - CursorStyle::ResizeUpLeftDownRight - } - ResizeEdge::TopRight | ResizeEdge::BottomLeft => { - CursorStyle::ResizeUpRightDownLeft - } - }, - &hitbox, - ); - }, - ) - .size_full() - .absolute(), - ), - }) -} - -fn resize_edge( - pos: Point, - shadow_size: Pixels, - window_size: Size, - tiling: Tiling, -) -> Option { - let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5); - if bounds.contains(&pos) { - return None; - } - - let corner_size = size(shadow_size * 1.5, shadow_size * 1.5); - let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size); - if !tiling.top && top_left_bounds.contains(&pos) { - return Some(ResizeEdge::TopLeft); - } - - let top_right_bounds = Bounds::new( - Point::new(window_size.width - corner_size.width, px(0.)), - corner_size, - ); - if !tiling.top && top_right_bounds.contains(&pos) { - return Some(ResizeEdge::TopRight); - } - - let bottom_left_bounds = Bounds::new( - Point::new(px(0.), window_size.height - corner_size.height), - corner_size, - ); - if !tiling.bottom && bottom_left_bounds.contains(&pos) { - return Some(ResizeEdge::BottomLeft); - } - - let bottom_right_bounds = Bounds::new( - Point::new( - window_size.width - corner_size.width, - window_size.height - corner_size.height, - ), - corner_size, - ); - if !tiling.bottom && bottom_right_bounds.contains(&pos) { - return Some(ResizeEdge::BottomRight); - } - - if !tiling.top && pos.y < shadow_size { - Some(ResizeEdge::Top) - } else if !tiling.bottom && pos.y > window_size.height - shadow_size { - Some(ResizeEdge::Bottom) - } else if !tiling.left && pos.x < shadow_size { - Some(ResizeEdge::Left) - } else if !tiling.right && pos.x > window_size.width - shadow_size { - Some(ResizeEdge::Right) - } else { - None - } -} - -fn join_pane_into_active( - active_pane: &Entity, - pane: &Entity, - window: &mut Window, - cx: &mut App, -) { - if pane == active_pane { - } else if pane.read(cx).items_len() == 0 { - pane.update(cx, |_, cx| { - cx.emit(pane::Event::Remove { - focus_on_pane: None, - }); - }) - } else { - move_all_items(pane, active_pane, window, cx); - } -} - -fn move_all_items( - from_pane: &Entity, - to_pane: &Entity, - window: &mut Window, - cx: &mut App, -) { - let destination_is_different = from_pane != to_pane; - let mut moved_items = 0; - for (item_ix, item_handle) in from_pane - .read(cx) - .items() - .enumerate() - .map(|(ix, item)| (ix, item.clone())) - .collect::>() - { - let ix = item_ix - moved_items; - if destination_is_different { - // Close item from previous pane - from_pane.update(cx, |source, cx| { - source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx); - }); - moved_items += 1; - } - - // This automatically removes duplicate items in the pane - to_pane.update(cx, |destination, cx| { - destination.add_item(item_handle, true, true, None, window, cx); - window.focus(&destination.focus_handle(cx)) - }); - } -} - -pub fn move_item( - source: &Entity, - destination: &Entity, - item_id_to_move: EntityId, - destination_index: usize, - activate: bool, - window: &mut Window, - cx: &mut App, -) { - let Some((item_ix, item_handle)) = source - .read(cx) - .items() - .enumerate() - .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move) - .map(|(ix, item)| (ix, item.clone())) - else { - // Tab was closed during drag - return; - }; - - if source != destination { - // Close item from previous pane - source.update(cx, |source, cx| { - source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx); - }); - } - - // This automatically removes duplicate items in the pane - destination.update(cx, |destination, cx| { - destination.add_item_inner( - item_handle, - activate, - activate, - activate, - Some(destination_index), - window, - cx, - ); - if activate { - window.focus(&destination.focus_handle(cx)) - } - }); -} - -pub fn move_active_item( - source: &Entity, - destination: &Entity, - focus_destination: bool, - close_if_empty: bool, - window: &mut Window, - cx: &mut App, -) { - if source == destination { - return; - } - let Some(active_item) = source.read(cx).active_item() else { - return; - }; - source.update(cx, |source_pane, cx| { - let item_id = active_item.item_id(); - source_pane.remove_item(item_id, false, close_if_empty, window, cx); - destination.update(cx, |target_pane, cx| { - target_pane.add_item( - active_item, - focus_destination, - focus_destination, - Some(target_pane.items_len()), - window, - cx, - ); - }); - }); -} - -pub fn clone_active_item( - workspace_id: Option, - source: &Entity, - destination: &Entity, - focus_destination: bool, - window: &mut Window, - cx: &mut App, -) { - if source == destination { - return; - } - let Some(active_item) = source.read(cx).active_item() else { - return; - }; - if !active_item.can_split(cx) { - return; - } - let destination = destination.downgrade(); - let task = active_item.clone_on_split(workspace_id, window, cx); - window - .spawn(cx, async move |cx| { - let Some(clone) = task.await else { - return; - }; - destination - .update_in(cx, |target_pane, window, cx| { - target_pane.add_item( - clone, - focus_destination, - focus_destination, - Some(target_pane.items_len()), - window, - cx, - ); - }) - .log_err(); - }) - .detach(); -} - -#[derive(Debug)] -pub struct WorkspacePosition { - pub window_bounds: Option, - pub display: Option, - pub centered_layout: bool, -} - -pub fn remote_workspace_position_from_db( - connection_options: RemoteConnectionOptions, - paths_to_open: &[PathBuf], - cx: &App, -) -> Task> { - let paths = paths_to_open.to_vec(); - - cx.background_spawn(async move { - let remote_connection_id = persistence::DB - .get_or_create_remote_connection(connection_options) - .await - .context("fetching serialized ssh project")?; - let serialized_workspace = - persistence::DB.remote_workspace_for_roots(&paths, remote_connection_id); - - let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() { - (Some(WindowBounds::Windowed(bounds)), None) - } else { - let restorable_bounds = serialized_workspace - .as_ref() - .and_then(|workspace| Some((workspace.display?, workspace.window_bounds?))) - .or_else(|| { - let (display, window_bounds) = DB.last_window().log_err()?; - Some((display?, window_bounds?)) - }); - - if let Some((serialized_display, serialized_status)) = restorable_bounds { - (Some(serialized_status.0), Some(serialized_display)) - } else { - (None, None) - } - }; - - let centered_layout = serialized_workspace - .as_ref() - .map(|w| w.centered_layout) - .unwrap_or(false); - - Ok(WorkspacePosition { - window_bounds, - display, - centered_layout, - }) - }) -} - -pub fn with_active_or_new_workspace( - cx: &mut App, - f: impl FnOnce(&mut Workspace, &mut Window, &mut Context) + Send + 'static, -) { - match cx.active_window().and_then(|w| w.downcast::()) { - Some(workspace) => { - cx.defer(move |cx| { - workspace - .update(cx, |workspace, window, cx| f(workspace, window, cx)) - .log_err(); - }); - } - None => { - let app_state = AppState::global(cx); - if let Some(app_state) = app_state.upgrade() { - open_new( - OpenOptions::default(), - app_state, - cx, - move |workspace, window, cx| f(workspace, window, cx), - ) - .detach_and_log_err(cx); - } - } - } -} - -#[cfg(test)] -mod tests { - use std::{cell::RefCell, rc::Rc}; - - use super::*; - use crate::{ - dock::{PanelEvent, test::TestPanel}, - item::{ - ItemBufferKind, ItemEvent, - test::{TestItem, TestProjectItem}, - }, - }; - use fs::FakeFs; - use gpui::{ - DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext, - UpdateGlobal, VisualTestContext, px, - }; - use project::{Project, ProjectEntryId}; - use serde_json::json; - use settings::SettingsStore; - use util::rel_path::rel_path; - - #[gpui::test] - async fn test_tab_disambiguation(cx: &mut TestAppContext) { - init_test(cx); - - 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)); - - // Adding an item with no ambiguity renders the tab without detail. - let item1 = cx.new(|cx| { - let mut item = TestItem::new(cx); - item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]); - item - }); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx); - }); - item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0))); - - // Adding an item that creates ambiguity increases the level of detail on - // both tabs. - let item2 = cx.new_window_entity(|_window, cx| { - let mut item = TestItem::new(cx); - item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]); - item - }); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx); - }); - item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); - item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); - - // Adding an item that creates ambiguity increases the level of detail only - // on the ambiguous tabs. In this case, the ambiguity can't be resolved so - // we stop at the highest detail available. - let item3 = cx.new(|cx| { - let mut item = TestItem::new(cx); - item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]); - item - }); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx); - }); - item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); - item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3))); - item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3))); - } - - #[gpui::test] - async fn test_tracking_active_path(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "one.txt": "", - "two.txt": "", - }), - ) - .await; - fs.insert_tree( - "/root2", - json!({ - "three.txt": "", - }), - ) - .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 pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - let worktree_id = project.update(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }); - - let item1 = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]) - }); - let item2 = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)]) - }); - - // Add an item to an empty pane - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx) - }); - project.update(cx, |project, cx| { - assert_eq!( - project.active_entry(), - project - .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx) - .map(|e| e.id) - ); - }); - assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt")); - - // Add a second item to a non-empty pane - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx) - }); - assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt")); - project.update(cx, |project, cx| { - assert_eq!( - project.active_entry(), - project - .entry_for_path(&(worktree_id, rel_path("two.txt")).into(), cx) - .map(|e| e.id) - ); - }); - - // Close the active item - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item(&Default::default(), window, cx) - }) - .await - .unwrap(); - assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt")); - project.update(cx, |project, cx| { - assert_eq!( - project.active_entry(), - project - .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx) - .map(|e| e.id) - ); - }); - - // Add a project folder - project - .update(cx, |project, cx| { - project.find_or_create_worktree("root2", true, cx) - }) - .await - .unwrap(); - assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt")); - - // Remove a project folder - project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); - assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt")); - } - - #[gpui::test] - async fn test_close_window(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/root", json!({ "one": "" })).await; - - let project = Project::test(fs, ["root".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - // When there are no dirty items, there's nothing to do. - let item1 = cx.new(TestItem::new); - workspace.update_in(cx, |w, window, cx| { - w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx) - }); - let task = workspace.update_in(cx, |w, window, cx| { - w.prepare_to_close(CloseIntent::CloseWindow, window, cx) - }); - assert!(task.await.unwrap()); - - // When there are dirty untitled items, prompt to save each one. If the user - // cancels any prompt, then abort. - let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true)); - let item3 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) - }); - workspace.update_in(cx, |w, window, cx| { - w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx); - w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx); - }); - let task = workspace.update_in(cx, |w, window, cx| { - w.prepare_to_close(CloseIntent::CloseWindow, window, cx) - }); - cx.executor().run_until_parked(); - cx.simulate_prompt_answer("Cancel"); // cancel save all - cx.executor().run_until_parked(); - assert!(!cx.has_pending_prompt()); - assert!(!task.await.unwrap()); - } - - #[gpui::test] - async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) { - init_test(cx); - - // Register TestItem as a serializable item - cx.update(|cx| { - register_serializable_item::(cx); - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/root", json!({ "one": "" })).await; - - let project = Project::test(fs, ["root".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - // When there are dirty untitled items, but they can serialize, then there is no prompt. - let item1 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_serialize(|| Some(Task::ready(Ok(())))) - }); - let item2 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) - .with_serialize(|| Some(Task::ready(Ok(())))) - }); - workspace.update_in(cx, |w, window, cx| { - w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx); - w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx); - }); - let task = workspace.update_in(cx, |w, window, cx| { - w.prepare_to_close(CloseIntent::CloseWindow, window, cx) - }); - assert!(task.await.unwrap()); - } - - #[gpui::test] - async fn test_close_pane_items(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - let item1 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_project_items(&[dirty_project_item(1, "1.txt", cx)]) - }); - let item2 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_conflict(true) - .with_project_items(&[dirty_project_item(2, "2.txt", cx)]) - }); - let item3 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_conflict(true) - .with_project_items(&[dirty_project_item(3, "3.txt", cx)]) - }); - let item4 = cx.new(|cx| { - TestItem::new(cx).with_dirty(true).with_project_items(&[{ - let project_item = TestProjectItem::new_untitled(cx); - project_item.update(cx, |project_item, _| project_item.is_dirty = true); - project_item - }]) - }); - let pane = workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx); - workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx); - workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx); - workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx); - workspace.active_pane().clone() - }); - - let close_items = pane.update_in(cx, |pane, window, cx| { - pane.activate_item(1, true, true, window, cx); - assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id()); - let item1_id = item1.item_id(); - let item3_id = item3.item_id(); - let item4_id = item4.item_id(); - pane.close_items(window, cx, SaveIntent::Close, move |id| { - [item1_id, item3_id, item4_id].contains(&id) - }) - }); - cx.executor().run_until_parked(); - - assert!(cx.has_pending_prompt()); - cx.simulate_prompt_answer("Save all"); - - cx.executor().run_until_parked(); - - // Item 1 is saved. There's a prompt to save item 3. - pane.update(cx, |pane, cx| { - assert_eq!(item1.read(cx).save_count, 1); - assert_eq!(item1.read(cx).save_as_count, 0); - assert_eq!(item1.read(cx).reload_count, 0); - assert_eq!(pane.items_len(), 3); - assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id()); - }); - assert!(cx.has_pending_prompt()); - - // Cancel saving item 3. - cx.simulate_prompt_answer("Discard"); - cx.executor().run_until_parked(); - - // Item 3 is reloaded. There's a prompt to save item 4. - pane.update(cx, |pane, cx| { - assert_eq!(item3.read(cx).save_count, 0); - assert_eq!(item3.read(cx).save_as_count, 0); - assert_eq!(item3.read(cx).reload_count, 1); - assert_eq!(pane.items_len(), 2); - assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id()); - }); - - // There's a prompt for a path for item 4. - cx.simulate_new_path_selection(|_| Some(Default::default())); - close_items.await.unwrap(); - - // The requested items are closed. - pane.update(cx, |pane, cx| { - assert_eq!(item4.read(cx).save_count, 0); - assert_eq!(item4.read(cx).save_as_count, 1); - assert_eq!(item4.read(cx).reload_count, 0); - assert_eq!(pane.items_len(), 1); - assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id()); - }); - } - - #[gpui::test] - async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) { - init_test(cx); - - 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, window, cx)); - - // Create several workspace items with single project entries, and two - // workspace items with multiple project entries. - let single_entry_items = (0..=4) - .map(|project_entry_id| { - cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_project_items(&[dirty_project_item( - project_entry_id, - &format!("{project_entry_id}.txt"), - cx, - )]) - }) - }) - .collect::>(); - let item_2_3 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_buffer_kind(ItemBufferKind::Multibuffer) - .with_project_items(&[ - single_entry_items[2].read(cx).project_items[0].clone(), - single_entry_items[3].read(cx).project_items[0].clone(), - ]) - }); - let item_3_4 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_buffer_kind(ItemBufferKind::Multibuffer) - .with_project_items(&[ - single_entry_items[3].read(cx).project_items[0].clone(), - single_entry_items[4].read(cx).project_items[0].clone(), - ]) - }); - - // Create two panes that contain the following project entries: - // left pane: - // multi-entry items: (2, 3) - // single-entry items: 0, 2, 3, 4 - // right pane: - // single-entry items: 4, 1 - // multi-entry items: (3, 4) - let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| { - let left_pane = workspace.active_pane().clone(); - workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx); - workspace.add_item_to_active_pane( - single_entry_items[0].boxed_clone(), - None, - true, - window, - cx, - ); - workspace.add_item_to_active_pane( - single_entry_items[2].boxed_clone(), - None, - true, - window, - cx, - ); - workspace.add_item_to_active_pane( - single_entry_items[3].boxed_clone(), - None, - true, - window, - cx, - ); - workspace.add_item_to_active_pane( - single_entry_items[4].boxed_clone(), - None, - true, - window, - cx, - ); - - let right_pane = - workspace.split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx); - - let boxed_clone = single_entry_items[1].boxed_clone(); - let right_pane = window.spawn(cx, async move |cx| { - right_pane.await.inspect(|right_pane| { - right_pane - .update_in(cx, |pane, window, cx| { - pane.add_item(boxed_clone, true, true, None, window, cx); - pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx); - }) - .unwrap(); - }) - }); - - (left_pane, right_pane) - }); - let right_pane = right_pane.await.unwrap(); - cx.focus(&right_pane); - - let mut close = right_pane.update_in(cx, |pane, window, cx| { - pane.close_all_items(&CloseAllItems::default(), window, cx) - .unwrap() - }); - cx.executor().run_until_parked(); - - let msg = cx.pending_prompt().unwrap().0; - assert!(msg.contains("1.txt")); - assert!(!msg.contains("2.txt")); - assert!(!msg.contains("3.txt")); - assert!(!msg.contains("4.txt")); - - cx.simulate_prompt_answer("Cancel"); - close.await; - - left_pane - .update_in(cx, |left_pane, window, cx| { - left_pane.close_item_by_id( - single_entry_items[3].entity_id(), - SaveIntent::Skip, - window, - cx, - ) - }) - .await - .unwrap(); - - close = right_pane.update_in(cx, |pane, window, cx| { - pane.close_all_items(&CloseAllItems::default(), window, cx) - .unwrap() - }); - cx.executor().run_until_parked(); - - let details = cx.pending_prompt().unwrap().1; - assert!(details.contains("1.txt")); - assert!(!details.contains("2.txt")); - assert!(details.contains("3.txt")); - // ideally this assertion could be made, but today we can only - // save whole items not project items, so the orphaned item 3 causes - // 4 to be saved too. - // assert!(!details.contains("4.txt")); - - cx.simulate_prompt_answer("Save all"); - - cx.executor().run_until_parked(); - close.await; - right_pane.read_with(cx, |pane, _| { - assert_eq!(pane.items_len(), 0); - }); - } - - #[gpui::test] - async fn test_autosave(cx: &mut gpui::TestAppContext) { - init_test(cx); - - 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, window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - let item = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) - }); - let item_id = item.entity_id(); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx); - }); - - // Autosave on window change. - item.update(cx, |item, cx| { - SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings(cx, |settings| { - settings.workspace.autosave = Some(AutosaveSetting::OnWindowChange); - }) - }); - item.is_dirty = true; - }); - - // Deactivating the window saves the file. - cx.deactivate_window(); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 1)); - - // Re-activating the window doesn't save the file. - cx.update(|window, _| window.activate_window()); - cx.executor().run_until_parked(); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 1)); - - // Autosave on focus change. - item.update_in(cx, |item, window, cx| { - cx.focus_self(window); - SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings(cx, |settings| { - settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange); - }) - }); - item.is_dirty = true; - }); - // Blurring the item saves the file. - item.update_in(cx, |_, window, _| window.blur()); - cx.executor().run_until_parked(); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 2)); - - // Deactivating the window still saves the file. - item.update_in(cx, |item, window, cx| { - cx.focus_self(window); - item.is_dirty = true; - }); - cx.deactivate_window(); - item.update(cx, |item, _| assert_eq!(item.save_count, 3)); - - // Autosave after delay. - item.update(cx, |item, cx| { - SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings(cx, |settings| { - settings.workspace.autosave = Some(AutosaveSetting::AfterDelay { - milliseconds: 500.into(), - }); - }) - }); - item.is_dirty = true; - cx.emit(ItemEvent::Edit); - }); - - // Delay hasn't fully expired, so the file is still dirty and unsaved. - cx.executor().advance_clock(Duration::from_millis(250)); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 3)); - - // After delay expires, the file is saved. - cx.executor().advance_clock(Duration::from_millis(250)); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 4)); - - // Autosave after delay, should save earlier than delay if tab is closed - item.update(cx, |item, cx| { - item.is_dirty = true; - cx.emit(ItemEvent::Edit); - }); - cx.executor().advance_clock(Duration::from_millis(250)); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 4)); - - // // Ensure auto save with delay saves the item on close, even if the timer hasn't yet run out. - pane.update_in(cx, |pane, window, cx| { - pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id) - }) - .await - .unwrap(); - assert!(!cx.has_pending_prompt()); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); - - // Add the item again, ensuring autosave is prevented if the underlying file has been deleted. - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx); - }); - item.update_in(cx, |item, _window, cx| { - item.is_dirty = true; - for project_item in &mut item.project_items { - project_item.update(cx, |project_item, _| project_item.is_dirty = true); - } - }); - cx.run_until_parked(); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); - - // Autosave on focus change, ensuring closing the tab counts as such. - item.update(cx, |item, cx| { - SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings(cx, |settings| { - settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange); - }) - }); - item.is_dirty = true; - for project_item in &mut item.project_items { - project_item.update(cx, |project_item, _| project_item.is_dirty = true); - } - }); - - pane.update_in(cx, |pane, window, cx| { - pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id) - }) - .await - .unwrap(); - assert!(!cx.has_pending_prompt()); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 6)); - - // Add the item again, ensuring autosave is prevented if the underlying file has been deleted. - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx); - }); - item.update_in(cx, |item, window, cx| { - item.project_items[0].update(cx, |item, _| { - item.entry_id = None; - }); - item.is_dirty = true; - window.blur(); - }); - cx.run_until_parked(); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 6)); - - // Ensure autosave is prevented for deleted files also when closing the buffer. - let _close_items = pane.update_in(cx, |pane, window, cx| { - pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id) - }); - cx.run_until_parked(); - assert!(cx.has_pending_prompt()); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 6)); - } - - #[gpui::test] - async fn test_pane_navigation(cx: &mut gpui::TestAppContext) { - init_test(cx); - - 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, window, cx)); - - let item = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) - }); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone()); - let toolbar_notify_count = Rc::new(RefCell::new(0)); - - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx); - let toolbar_notification_count = toolbar_notify_count.clone(); - cx.observe_in(&toolbar, window, move |_, _, _, _| { - *toolbar_notification_count.borrow_mut() += 1 - }) - .detach(); - }); - - pane.read_with(cx, |pane, _| { - assert!(!pane.can_navigate_backward()); - assert!(!pane.can_navigate_forward()); - }); - - item.update_in(cx, |item, _, cx| { - item.set_state("one".to_string(), cx); - }); - - // Toolbar must be notified to re-render the navigation buttons - assert_eq!(*toolbar_notify_count.borrow(), 1); - - pane.read_with(cx, |pane, _| { - assert!(pane.can_navigate_backward()); - assert!(!pane.can_navigate_forward()); - }); - - workspace - .update_in(cx, |workspace, window, cx| { - workspace.go_back(pane.downgrade(), window, cx) - }) - .await - .unwrap(); - - assert_eq!(*toolbar_notify_count.borrow(), 2); - pane.read_with(cx, |pane, _| { - assert!(!pane.can_navigate_backward()); - assert!(pane.can_navigate_forward()); - }); - } - - #[gpui::test] - async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) { - init_test(cx); - 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, window, cx)); - - let panel = workspace.update_in(cx, |workspace, window, cx| { - let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx)); - workspace.add_panel(panel.clone(), window, cx); - - workspace - .right_dock() - .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx)); - - panel - }); - - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - pane.update_in(cx, |pane, window, cx| { - let item = cx.new(TestItem::new); - pane.add_item(Box::new(item), true, true, None, window, cx); - }); - - // Transfer focus from center to panel - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_panel_focus::(window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert!(!panel.is_zoomed(window, cx)); - assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx)); - }); - - // Transfer focus from panel to center - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_panel_focus::(window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert!(!panel.is_zoomed(window, cx)); - assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx)); - }); - - // Close the dock - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_dock(DockPosition::Right, window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - assert!(!workspace.right_dock().read(cx).is_open()); - assert!(!panel.is_zoomed(window, cx)); - assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx)); - }); - - // Open the dock - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_dock(DockPosition::Right, window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert!(!panel.is_zoomed(window, cx)); - assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx)); - }); - - // Focus and zoom panel - panel.update_in(cx, |panel, window, cx| { - cx.focus_self(window); - panel.set_zoomed(true, window, cx) - }); - - workspace.update_in(cx, |workspace, window, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert!(panel.is_zoomed(window, cx)); - assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx)); - }); - - // Transfer focus to the center closes the dock - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_panel_focus::(window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - assert!(!workspace.right_dock().read(cx).is_open()); - assert!(panel.is_zoomed(window, cx)); - assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx)); - }); - - // Transferring focus back to the panel keeps it zoomed - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_panel_focus::(window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert!(panel.is_zoomed(window, cx)); - assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx)); - }); - - // Close the dock while it is zoomed - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_dock(DockPosition::Right, window, cx) - }); - - workspace.update_in(cx, |workspace, window, cx| { - assert!(!workspace.right_dock().read(cx).is_open()); - assert!(panel.is_zoomed(window, cx)); - assert!(workspace.zoomed.is_none()); - assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx)); - }); - - // Opening the dock, when it's zoomed, retains focus - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_dock(DockPosition::Right, window, cx) - }); - - workspace.update_in(cx, |workspace, window, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert!(panel.is_zoomed(window, cx)); - assert!(workspace.zoomed.is_some()); - assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx)); - }); - - // Unzoom and close the panel, zoom the active pane. - panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx)); - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_dock(DockPosition::Right, window, cx) - }); - pane.update_in(cx, |pane, window, cx| { - pane.toggle_zoom(&Default::default(), window, cx) - }); - - // Opening a dock unzooms the pane. - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_dock(DockPosition::Right, window, cx) - }); - workspace.update_in(cx, |workspace, window, cx| { - let pane = pane.read(cx); - assert!(!pane.is_zoomed()); - assert!(!pane.focus_handle(cx).is_focused(window)); - assert!(workspace.right_dock().read(cx).is_open()); - assert!(workspace.zoomed.is_none()); - }); - } - - #[gpui::test] - async fn test_toggle_all_docks(cx: &mut gpui::TestAppContext) { - init_test(cx); - 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, window, cx)); - workspace.update_in(cx, |workspace, window, cx| { - // Open two docks - let left_dock = workspace.dock_at_position(DockPosition::Left); - let right_dock = workspace.dock_at_position(DockPosition::Right); - - left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx)); - right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx)); - - assert!(left_dock.read(cx).is_open()); - assert!(right_dock.read(cx).is_open()); - }); - - workspace.update_in(cx, |workspace, window, cx| { - // Toggle all docks - should close both - workspace.toggle_all_docks(&ToggleAllDocks, window, cx); - - let left_dock = workspace.dock_at_position(DockPosition::Left); - let right_dock = workspace.dock_at_position(DockPosition::Right); - assert!(!left_dock.read(cx).is_open()); - assert!(!right_dock.read(cx).is_open()); - }); - - workspace.update_in(cx, |workspace, window, cx| { - // Toggle again - should reopen both - workspace.toggle_all_docks(&ToggleAllDocks, window, cx); - - let left_dock = workspace.dock_at_position(DockPosition::Left); - let right_dock = workspace.dock_at_position(DockPosition::Right); - assert!(left_dock.read(cx).is_open()); - assert!(right_dock.read(cx).is_open()); - }); - } - - #[gpui::test] - async fn test_toggle_all_with_manual_close(cx: &mut gpui::TestAppContext) { - init_test(cx); - 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, window, cx)); - workspace.update_in(cx, |workspace, window, cx| { - // Open two docks - let left_dock = workspace.dock_at_position(DockPosition::Left); - let right_dock = workspace.dock_at_position(DockPosition::Right); - - left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx)); - right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx)); - - assert!(left_dock.read(cx).is_open()); - assert!(right_dock.read(cx).is_open()); - }); - - workspace.update_in(cx, |workspace, window, cx| { - // Close them manually - workspace.toggle_dock(DockPosition::Left, window, cx); - workspace.toggle_dock(DockPosition::Right, window, cx); - - let left_dock = workspace.dock_at_position(DockPosition::Left); - let right_dock = workspace.dock_at_position(DockPosition::Right); - assert!(!left_dock.read(cx).is_open()); - assert!(!right_dock.read(cx).is_open()); - }); - - workspace.update_in(cx, |workspace, window, cx| { - // Toggle all docks - only last closed (right dock) should reopen - workspace.toggle_all_docks(&ToggleAllDocks, window, cx); - - let left_dock = workspace.dock_at_position(DockPosition::Left); - let right_dock = workspace.dock_at_position(DockPosition::Right); - assert!(!left_dock.read(cx).is_open()); - assert!(right_dock.read(cx).is_open()); - }); - } - - #[gpui::test] - async fn test_toggle_all_docks_after_dock_move(cx: &mut gpui::TestAppContext) { - init_test(cx); - 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, window, cx)); - - // Open two docks (left and right) with one panel each - let (left_panel, right_panel) = workspace.update_in(cx, |workspace, window, cx| { - let left_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx)); - workspace.add_panel(left_panel.clone(), window, cx); - - let right_panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx)); - workspace.add_panel(right_panel.clone(), window, cx); - - workspace.toggle_dock(DockPosition::Left, window, cx); - workspace.toggle_dock(DockPosition::Right, window, cx); - - // Verify initial state - assert!( - workspace.left_dock().read(cx).is_open(), - "Left dock should be open" - ); - assert_eq!( - workspace - .left_dock() - .read(cx) - .visible_panel() - .unwrap() - .panel_id(), - left_panel.panel_id(), - "Left panel should be visible in left dock" - ); - assert!( - workspace.right_dock().read(cx).is_open(), - "Right dock should be open" - ); - assert_eq!( - workspace - .right_dock() - .read(cx) - .visible_panel() - .unwrap() - .panel_id(), - right_panel.panel_id(), - "Right panel should be visible in right dock" - ); - assert!( - !workspace.bottom_dock().read(cx).is_open(), - "Bottom dock should be closed" - ); - - (left_panel, right_panel) - }); - - // Focus the left panel and move it to the next position (bottom dock) - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_panel_focus::(window, cx); // Focus left panel - assert!( - left_panel.read(cx).focus_handle(cx).is_focused(window), - "Left panel should be focused" - ); - }); - - cx.dispatch_action(MoveFocusedPanelToNextPosition); - - // Verify the left panel has moved to the bottom dock, and the bottom dock is now open - workspace.update(cx, |workspace, cx| { - assert!( - !workspace.left_dock().read(cx).is_open(), - "Left dock should be closed" - ); - assert!( - workspace.bottom_dock().read(cx).is_open(), - "Bottom dock should now be open" - ); - assert_eq!( - left_panel.read(cx).position, - DockPosition::Bottom, - "Left panel should now be in the bottom dock" - ); - assert_eq!( - workspace - .bottom_dock() - .read(cx) - .visible_panel() - .unwrap() - .panel_id(), - left_panel.panel_id(), - "Left panel should be the visible panel in the bottom dock" - ); - }); - - // Toggle all docks off - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_all_docks(&ToggleAllDocks, window, cx); - assert!( - !workspace.left_dock().read(cx).is_open(), - "Left dock should be closed" - ); - assert!( - !workspace.right_dock().read(cx).is_open(), - "Right dock should be closed" - ); - assert!( - !workspace.bottom_dock().read(cx).is_open(), - "Bottom dock should be closed" - ); - }); - - // Toggle all docks back on and verify positions are restored - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_all_docks(&ToggleAllDocks, window, cx); - assert!( - !workspace.left_dock().read(cx).is_open(), - "Left dock should remain closed" - ); - assert!( - workspace.right_dock().read(cx).is_open(), - "Right dock should remain open" - ); - assert!( - workspace.bottom_dock().read(cx).is_open(), - "Bottom dock should remain open" - ); - assert_eq!( - left_panel.read(cx).position, - DockPosition::Bottom, - "Left panel should remain in the bottom dock" - ); - assert_eq!( - right_panel.read(cx).position, - DockPosition::Right, - "Right panel should remain in the right dock" - ); - assert_eq!( - workspace - .bottom_dock() - .read(cx) - .visible_panel() - .unwrap() - .panel_id(), - left_panel.panel_id(), - "Left panel should be the visible panel in the right dock" - ); - }); - } - - #[gpui::test] - async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - // Let's arrange the panes like this: - // - // +-----------------------+ - // | top | - // +------+--------+-------+ - // | left | center | right | - // +------+--------+-------+ - // | bottom | - // +-----------------------+ - - let top_item = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)]) - }); - let bottom_item = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)]) - }); - let left_item = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)]) - }); - let right_item = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)]) - }); - let center_item = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)]) - }); - - let top_pane_id = workspace.update_in(cx, |workspace, window, cx| { - let top_pane_id = workspace.active_pane().entity_id(); - workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx); - workspace.split_pane( - workspace.active_pane().clone(), - SplitDirection::Down, - window, - cx, - ); - top_pane_id - }); - let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| { - let bottom_pane_id = workspace.active_pane().entity_id(); - workspace.add_item_to_active_pane( - Box::new(bottom_item.clone()), - None, - false, - window, - cx, - ); - workspace.split_pane( - workspace.active_pane().clone(), - SplitDirection::Up, - window, - cx, - ); - bottom_pane_id - }); - let left_pane_id = workspace.update_in(cx, |workspace, window, cx| { - let left_pane_id = workspace.active_pane().entity_id(); - workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx); - workspace.split_pane( - workspace.active_pane().clone(), - SplitDirection::Right, - window, - cx, - ); - left_pane_id - }); - let right_pane_id = workspace.update_in(cx, |workspace, window, cx| { - let right_pane_id = workspace.active_pane().entity_id(); - workspace.add_item_to_active_pane( - Box::new(right_item.clone()), - None, - false, - window, - cx, - ); - workspace.split_pane( - workspace.active_pane().clone(), - SplitDirection::Left, - window, - cx, - ); - right_pane_id - }); - let center_pane_id = workspace.update_in(cx, |workspace, window, cx| { - let center_pane_id = workspace.active_pane().entity_id(); - workspace.add_item_to_active_pane( - Box::new(center_item.clone()), - None, - false, - window, - cx, - ); - center_pane_id - }); - cx.executor().run_until_parked(); - - workspace.update_in(cx, |workspace, window, cx| { - assert_eq!(center_pane_id, workspace.active_pane().entity_id()); - - // Join into next from center pane into right - workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - let active_pane = workspace.active_pane(); - assert_eq!(right_pane_id, active_pane.entity_id()); - assert_eq!(2, active_pane.read(cx).items_len()); - let item_ids_in_pane = - HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id())); - assert!(item_ids_in_pane.contains(¢er_item.item_id())); - assert!(item_ids_in_pane.contains(&right_item.item_id())); - - // Join into next from right pane into bottom - workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - let active_pane = workspace.active_pane(); - assert_eq!(bottom_pane_id, active_pane.entity_id()); - assert_eq!(3, active_pane.read(cx).items_len()); - let item_ids_in_pane = - HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id())); - assert!(item_ids_in_pane.contains(¢er_item.item_id())); - assert!(item_ids_in_pane.contains(&right_item.item_id())); - assert!(item_ids_in_pane.contains(&bottom_item.item_id())); - - // Join into next from bottom pane into left - workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - let active_pane = workspace.active_pane(); - assert_eq!(left_pane_id, active_pane.entity_id()); - assert_eq!(4, active_pane.read(cx).items_len()); - let item_ids_in_pane = - HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id())); - assert!(item_ids_in_pane.contains(¢er_item.item_id())); - assert!(item_ids_in_pane.contains(&right_item.item_id())); - assert!(item_ids_in_pane.contains(&bottom_item.item_id())); - assert!(item_ids_in_pane.contains(&left_item.item_id())); - - // Join into next from left pane into top - workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - let active_pane = workspace.active_pane(); - assert_eq!(top_pane_id, active_pane.entity_id()); - assert_eq!(5, active_pane.read(cx).items_len()); - let item_ids_in_pane = - HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id())); - assert!(item_ids_in_pane.contains(¢er_item.item_id())); - assert!(item_ids_in_pane.contains(&right_item.item_id())); - assert!(item_ids_in_pane.contains(&bottom_item.item_id())); - assert!(item_ids_in_pane.contains(&left_item.item_id())); - assert!(item_ids_in_pane.contains(&top_item.item_id())); - - // Single pane left: no-op - workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx) - }); - - workspace.update(cx, |workspace, _cx| { - let active_pane = workspace.active_pane(); - assert_eq!(top_pane_id, active_pane.entity_id()); - }); - } - - fn add_an_item_to_active_pane( - cx: &mut VisualTestContext, - workspace: &Entity, - item_id: u64, - ) -> Entity { - let item = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new( - item_id, - "item{item_id}.txt", - cx, - )]) - }); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx); - }); - item - } - - fn split_pane(cx: &mut VisualTestContext, workspace: &Entity) -> Entity { - workspace.update_in(cx, |workspace, window, cx| { - workspace.split_pane( - workspace.active_pane().clone(), - SplitDirection::Right, - window, - cx, - ) - }) - } - - #[gpui::test] - async fn test_join_all_panes(cx: &mut gpui::TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - add_an_item_to_active_pane(cx, &workspace, 1); - split_pane(cx, &workspace); - add_an_item_to_active_pane(cx, &workspace, 2); - split_pane(cx, &workspace); // empty pane - split_pane(cx, &workspace); - let last_item = add_an_item_to_active_pane(cx, &workspace, 3); - - cx.executor().run_until_parked(); - - workspace.update(cx, |workspace, cx| { - let num_panes = workspace.panes().len(); - let num_items_in_current_pane = workspace.active_pane().read(cx).items().count(); - let active_item = workspace - .active_pane() - .read(cx) - .active_item() - .expect("item is in focus"); - - assert_eq!(num_panes, 4); - assert_eq!(num_items_in_current_pane, 1); - assert_eq!(active_item.item_id(), last_item.item_id()); - }); - - workspace.update_in(cx, |workspace, window, cx| { - workspace.join_all_panes(window, cx); - }); - - workspace.update(cx, |workspace, cx| { - let num_panes = workspace.panes().len(); - let num_items_in_current_pane = workspace.active_pane().read(cx).items().count(); - let active_item = workspace - .active_pane() - .read(cx) - .active_item() - .expect("item is in focus"); - - assert_eq!(num_panes, 1); - assert_eq!(num_items_in_current_pane, 3); - assert_eq!(active_item.item_id(), last_item.item_id()); - }); - } - struct TestModal(FocusHandle); - - impl TestModal { - fn new(_: &mut Window, cx: &mut Context) -> Self { - Self(cx.focus_handle()) - } - } - - impl EventEmitter for TestModal {} - - impl Focusable for TestModal { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.0.clone() - } - } - - impl ModalView for TestModal {} - - impl Render for TestModal { - fn render( - &mut self, - _window: &mut Window, - _cx: &mut Context, - ) -> impl IntoElement { - div().track_focus(&self.0) - } - } - - #[gpui::test] - async fn test_panels(cx: &mut gpui::TestAppContext) { - init_test(cx); - 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, window, cx)); - - let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| { - let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx)); - workspace.add_panel(panel_1.clone(), window, cx); - workspace.toggle_dock(DockPosition::Left, window, cx); - let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx)); - workspace.add_panel(panel_2.clone(), window, cx); - workspace.toggle_dock(DockPosition::Right, window, cx); - - let left_dock = workspace.left_dock(); - assert_eq!( - left_dock.read(cx).visible_panel().unwrap().panel_id(), - panel_1.panel_id() - ); - assert_eq!( - left_dock.read(cx).active_panel_size(window, cx).unwrap(), - panel_1.size(window, cx) - ); - - left_dock.update(cx, |left_dock, cx| { - left_dock.resize_active_panel(Some(px(1337.)), window, cx) - }); - assert_eq!( - workspace - .right_dock() - .read(cx) - .visible_panel() - .unwrap() - .panel_id(), - panel_2.panel_id(), - ); - - (panel_1, panel_2) - }); - - // Move panel_1 to the right - panel_1.update_in(cx, |panel_1, window, cx| { - panel_1.set_position(DockPosition::Right, window, cx) - }); - - workspace.update_in(cx, |workspace, window, cx| { - // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right. - // Since it was the only panel on the left, the left dock should now be closed. - assert!(!workspace.left_dock().read(cx).is_open()); - assert!(workspace.left_dock().read(cx).visible_panel().is_none()); - let right_dock = workspace.right_dock(); - assert_eq!( - right_dock.read(cx).visible_panel().unwrap().panel_id(), - panel_1.panel_id() - ); - assert_eq!( - right_dock.read(cx).active_panel_size(window, cx).unwrap(), - px(1337.) - ); - - // Now we move panel_2 to the left - panel_2.set_position(DockPosition::Left, window, cx); - }); - - workspace.update(cx, |workspace, cx| { - // Since panel_2 was not visible on the right, we don't open the left dock. - assert!(!workspace.left_dock().read(cx).is_open()); - // And the right dock is unaffected in its displaying of panel_1 - assert!(workspace.right_dock().read(cx).is_open()); - assert_eq!( - workspace - .right_dock() - .read(cx) - .visible_panel() - .unwrap() - .panel_id(), - panel_1.panel_id(), - ); - }); - - // Move panel_1 back to the left - panel_1.update_in(cx, |panel_1, window, cx| { - panel_1.set_position(DockPosition::Left, window, cx) - }); - - workspace.update_in(cx, |workspace, window, cx| { - // Since panel_1 was visible on the right, we open the left dock and make panel_1 active. - let left_dock = workspace.left_dock(); - assert!(left_dock.read(cx).is_open()); - assert_eq!( - left_dock.read(cx).visible_panel().unwrap().panel_id(), - panel_1.panel_id() - ); - assert_eq!( - left_dock.read(cx).active_panel_size(window, cx).unwrap(), - px(1337.) - ); - // And the right dock should be closed as it no longer has any panels. - assert!(!workspace.right_dock().read(cx).is_open()); - - // Now we move panel_1 to the bottom - panel_1.set_position(DockPosition::Bottom, window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - // Since panel_1 was visible on the left, we close the left dock. - assert!(!workspace.left_dock().read(cx).is_open()); - // The bottom dock is sized based on the panel's default size, - // since the panel orientation changed from vertical to horizontal. - let bottom_dock = workspace.bottom_dock(); - assert_eq!( - bottom_dock.read(cx).active_panel_size(window, cx).unwrap(), - panel_1.size(window, cx), - ); - // Close bottom dock and move panel_1 back to the left. - bottom_dock.update(cx, |bottom_dock, cx| { - bottom_dock.set_open(false, window, cx) - }); - panel_1.set_position(DockPosition::Left, window, cx); - }); - - // Emit activated event on panel 1 - panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate)); - - // Now the left dock is open and panel_1 is active and focused. - workspace.update_in(cx, |workspace, window, cx| { - let left_dock = workspace.left_dock(); - assert!(left_dock.read(cx).is_open()); - assert_eq!( - left_dock.read(cx).visible_panel().unwrap().panel_id(), - panel_1.panel_id(), - ); - assert!(panel_1.focus_handle(cx).is_focused(window)); - }); - - // Emit closed event on panel 2, which is not active - panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close)); - - // Wo don't close the left dock, because panel_2 wasn't the active panel - workspace.update(cx, |workspace, cx| { - let left_dock = workspace.left_dock(); - assert!(left_dock.read(cx).is_open()); - assert_eq!( - left_dock.read(cx).visible_panel().unwrap().panel_id(), - panel_1.panel_id(), - ); - }); - - // Emitting a ZoomIn event shows the panel as zoomed. - panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn)); - workspace.read_with(cx, |workspace, _| { - assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade())); - assert_eq!(workspace.zoomed_position, Some(DockPosition::Left)); - }); - - // Move panel to another dock while it is zoomed - panel_1.update_in(cx, |panel, window, cx| { - panel.set_position(DockPosition::Right, window, cx) - }); - workspace.read_with(cx, |workspace, _| { - assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade())); - - assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); - }); - - // This is a helper for getting a: - // - valid focus on an element, - // - that isn't a part of the panes and panels system of the Workspace, - // - and doesn't trigger the 'on_focus_lost' API. - let focus_other_view = { - let workspace = workspace.clone(); - move |cx: &mut VisualTestContext| { - workspace.update_in(cx, |workspace, window, cx| { - if workspace.active_modal::(cx).is_some() { - workspace.toggle_modal(window, cx, TestModal::new); - workspace.toggle_modal(window, cx, TestModal::new); - } else { - workspace.toggle_modal(window, cx, TestModal::new); - } - }) - } - }; - - // If focus is transferred to another view that's not a panel or another pane, we still show - // the panel as zoomed. - focus_other_view(cx); - workspace.read_with(cx, |workspace, _| { - assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade())); - assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); - }); - - // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed. - workspace.update_in(cx, |_workspace, window, cx| { - cx.focus_self(window); - }); - workspace.read_with(cx, |workspace, _| { - assert_eq!(workspace.zoomed, None); - assert_eq!(workspace.zoomed_position, None); - }); - - // If focus is transferred again to another view that's not a panel or a pane, we won't - // show the panel as zoomed because it wasn't zoomed before. - focus_other_view(cx); - workspace.read_with(cx, |workspace, _| { - assert_eq!(workspace.zoomed, None); - assert_eq!(workspace.zoomed_position, None); - }); - - // When the panel is activated, it is zoomed again. - cx.dispatch_action(ToggleRightDock); - workspace.read_with(cx, |workspace, _| { - assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade())); - assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); - }); - - // Emitting a ZoomOut event unzooms the panel. - panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut)); - workspace.read_with(cx, |workspace, _| { - assert_eq!(workspace.zoomed, None); - assert_eq!(workspace.zoomed_position, None); - }); - - // Emit closed event on panel 1, which is active - panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close)); - - // Now the left dock is closed, because panel_1 was the active panel - workspace.update(cx, |workspace, cx| { - let right_dock = workspace.right_dock(); - assert!(!right_dock.read(cx).is_open()); - }); - } - - #[gpui::test] - async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - let dirty_regular_buffer = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_label("1.txt") - .with_project_items(&[dirty_project_item(1, "1.txt", cx)]) - }); - let dirty_regular_buffer_2 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_label("2.txt") - .with_project_items(&[dirty_project_item(2, "2.txt", cx)]) - }); - let dirty_multi_buffer_with_both = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_buffer_kind(ItemBufferKind::Multibuffer) - .with_label("Fake Project Search") - .with_project_items(&[ - dirty_regular_buffer.read(cx).project_items[0].clone(), - dirty_regular_buffer_2.read(cx).project_items[0].clone(), - ]) - }); - let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id(); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item( - pane.clone(), - Box::new(dirty_regular_buffer.clone()), - None, - false, - false, - window, - cx, - ); - workspace.add_item( - pane.clone(), - Box::new(dirty_regular_buffer_2.clone()), - None, - false, - false, - window, - cx, - ); - workspace.add_item( - pane.clone(), - Box::new(dirty_multi_buffer_with_both.clone()), - None, - false, - false, - window, - cx, - ); - }); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(2, true, true, window, cx); - assert_eq!( - pane.active_item().unwrap().item_id(), - multi_buffer_with_both_files_id, - "Should select the multi buffer in the pane" - ); - }); - let close_all_but_multi_buffer_task = pane.update_in(cx, |pane, window, cx| { - pane.close_other_items( - &CloseOtherItems { - save_intent: Some(SaveIntent::Save), - close_pinned: true, - }, - None, - window, - cx, - ) - }); - cx.background_executor.run_until_parked(); - assert!(!cx.has_pending_prompt()); - close_all_but_multi_buffer_task - .await - .expect("Closing all buffers but the multi buffer failed"); - pane.update(cx, |pane, cx| { - assert_eq!(dirty_regular_buffer.read(cx).save_count, 1); - assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0); - assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1); - assert_eq!(pane.items_len(), 1); - assert_eq!( - pane.active_item().unwrap().item_id(), - multi_buffer_with_both_files_id, - "Should have only the multi buffer left in the pane" - ); - assert!( - dirty_multi_buffer_with_both.read(cx).is_dirty, - "The multi buffer containing the unsaved buffer should still be dirty" - ); - }); - - dirty_regular_buffer.update(cx, |buffer, cx| { - buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true) - }); - - let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: Some(SaveIntent::Close), - close_pinned: false, - }, - window, - cx, - ) - }); - cx.background_executor.run_until_parked(); - assert!( - cx.has_pending_prompt(), - "Dirty multi buffer should prompt a save dialog" - ); - cx.simulate_prompt_answer("Save"); - cx.background_executor.run_until_parked(); - close_multi_buffer_task - .await - .expect("Closing the multi buffer failed"); - pane.update(cx, |pane, cx| { - assert_eq!( - dirty_multi_buffer_with_both.read(cx).save_count, - 1, - "Multi buffer item should get be saved" - ); - // Test impl does not save inner items, so we do not assert them - assert_eq!( - pane.items_len(), - 0, - "No more items should be left in the pane" - ); - assert!(pane.active_item().is_none()); - }); - } - - #[gpui::test] - async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane( - cx: &mut TestAppContext, - ) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - let dirty_regular_buffer = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_label("1.txt") - .with_project_items(&[dirty_project_item(1, "1.txt", cx)]) - }); - let dirty_regular_buffer_2 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_label("2.txt") - .with_project_items(&[dirty_project_item(2, "2.txt", cx)]) - }); - let clear_regular_buffer = cx.new(|cx| { - TestItem::new(cx) - .with_label("3.txt") - .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)]) - }); - - let dirty_multi_buffer_with_both = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_buffer_kind(ItemBufferKind::Multibuffer) - .with_label("Fake Project Search") - .with_project_items(&[ - dirty_regular_buffer.read(cx).project_items[0].clone(), - dirty_regular_buffer_2.read(cx).project_items[0].clone(), - clear_regular_buffer.read(cx).project_items[0].clone(), - ]) - }); - let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id(); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item( - pane.clone(), - Box::new(dirty_regular_buffer.clone()), - None, - false, - false, - window, - cx, - ); - workspace.add_item( - pane.clone(), - Box::new(dirty_multi_buffer_with_both.clone()), - None, - false, - false, - window, - cx, - ); - }); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(1, true, true, window, cx); - assert_eq!( - pane.active_item().unwrap().item_id(), - multi_buffer_with_both_files_id, - "Should select the multi buffer in the pane" - ); - }); - let _close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }); - cx.background_executor.run_until_parked(); - assert!( - cx.has_pending_prompt(), - "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown" - ); - } - - /// Tests that when `close_on_file_delete` is enabled, files are automatically - /// closed when they are deleted from disk. - #[gpui::test] - async fn test_close_on_disk_deletion_enabled(cx: &mut TestAppContext) { - init_test(cx); - - // Enable the close_on_disk_deletion setting - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| { - settings.workspace.close_on_file_delete = Some(true); - }); - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Create a test item that simulates a file - let item = cx.new(|cx| { - TestItem::new(cx) - .with_label("test.txt") - .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)]) - }); - - // Add item to workspace - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item( - pane.clone(), - Box::new(item.clone()), - None, - false, - false, - window, - cx, - ); - }); - - // Verify the item is in the pane - pane.read_with(cx, |pane, _| { - assert_eq!(pane.items().count(), 1); - }); - - // Simulate file deletion by setting the item's deleted state - item.update(cx, |item, _| { - item.set_has_deleted_file(true); - }); - - // Emit UpdateTab event to trigger the close behavior - cx.run_until_parked(); - item.update(cx, |_, cx| { - cx.emit(ItemEvent::UpdateTab); - }); - - // Allow the close operation to complete - cx.run_until_parked(); - - // Verify the item was automatically closed - pane.read_with(cx, |pane, _| { - assert_eq!( - pane.items().count(), - 0, - "Item should be automatically closed when file is deleted" - ); - }); - } - - /// Tests that when `close_on_file_delete` is disabled (default), files remain - /// open with a strikethrough when they are deleted from disk. - #[gpui::test] - async fn test_close_on_disk_deletion_disabled(cx: &mut TestAppContext) { - init_test(cx); - - // Ensure close_on_disk_deletion is disabled (default) - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| { - settings.workspace.close_on_file_delete = Some(false); - }); - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Create a test item that simulates a file - let item = cx.new(|cx| { - TestItem::new(cx) - .with_label("test.txt") - .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)]) - }); - - // Add item to workspace - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item( - pane.clone(), - Box::new(item.clone()), - None, - false, - false, - window, - cx, - ); - }); - - // Verify the item is in the pane - pane.read_with(cx, |pane, _| { - assert_eq!(pane.items().count(), 1); - }); - - // Simulate file deletion - item.update(cx, |item, _| { - item.set_has_deleted_file(true); - }); - - // Emit UpdateTab event - cx.run_until_parked(); - item.update(cx, |_, cx| { - cx.emit(ItemEvent::UpdateTab); - }); - - // Allow any potential close operation to complete - cx.run_until_parked(); - - // Verify the item remains open (with strikethrough) - pane.read_with(cx, |pane, _| { - assert_eq!( - pane.items().count(), - 1, - "Item should remain open when close_on_disk_deletion is disabled" - ); - }); - - // Verify the item shows as deleted - item.read_with(cx, |item, _| { - assert!( - item.has_deleted_file, - "Item should be marked as having deleted file" - ); - }); - } - - /// Tests that dirty files are not automatically closed when deleted from disk, - /// even when `close_on_file_delete` is enabled. This ensures users don't lose - /// unsaved changes without being prompted. - #[gpui::test] - async fn test_close_on_disk_deletion_with_dirty_file(cx: &mut TestAppContext) { - init_test(cx); - - // Enable the close_on_file_delete setting - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| { - settings.workspace.close_on_file_delete = Some(true); - }); - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Create a dirty test item - let item = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_label("test.txt") - .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)]) - }); - - // Add item to workspace - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item( - pane.clone(), - Box::new(item.clone()), - None, - false, - false, - window, - cx, - ); - }); - - // Simulate file deletion - item.update(cx, |item, _| { - item.set_has_deleted_file(true); - }); - - // Emit UpdateTab event to trigger the close behavior - cx.run_until_parked(); - item.update(cx, |_, cx| { - cx.emit(ItemEvent::UpdateTab); - }); - - // Allow any potential close operation to complete - cx.run_until_parked(); - - // Verify the item remains open (dirty files are not auto-closed) - pane.read_with(cx, |pane, _| { - assert_eq!( - pane.items().count(), - 1, - "Dirty items should not be automatically closed even when file is deleted" - ); - }); - - // Verify the item is marked as deleted and still dirty - item.read_with(cx, |item, _| { - assert!( - item.has_deleted_file, - "Item should be marked as having deleted file" - ); - assert!(item.is_dirty, "Item should still be dirty"); - }); - } - - /// Tests that navigation history is cleaned up when files are auto-closed - /// due to deletion from disk. - #[gpui::test] - async fn test_close_on_disk_deletion_cleans_navigation_history(cx: &mut TestAppContext) { - init_test(cx); - - // Enable the close_on_file_delete setting - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| { - settings.workspace.close_on_file_delete = Some(true); - }); - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Create test items - let item1 = cx.new(|cx| { - TestItem::new(cx) - .with_label("test1.txt") - .with_project_items(&[TestProjectItem::new(1, "test1.txt", cx)]) - }); - let item1_id = item1.item_id(); - - let item2 = cx.new(|cx| { - TestItem::new(cx) - .with_label("test2.txt") - .with_project_items(&[TestProjectItem::new(2, "test2.txt", cx)]) - }); - - // Add items to workspace - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item( - pane.clone(), - Box::new(item1.clone()), - None, - false, - false, - window, - cx, - ); - workspace.add_item( - pane.clone(), - Box::new(item2.clone()), - None, - false, - false, - window, - cx, - ); - }); - - // Activate item1 to ensure it gets navigation entries - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(0, true, true, window, cx); - }); - - // Switch to item2 and back to create navigation history - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(1, true, true, window, cx); - }); - cx.run_until_parked(); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(0, true, true, window, cx); - }); - cx.run_until_parked(); - - // Simulate file deletion for item1 - item1.update(cx, |item, _| { - item.set_has_deleted_file(true); - }); - - // Emit UpdateTab event to trigger the close behavior - item1.update(cx, |_, cx| { - cx.emit(ItemEvent::UpdateTab); - }); - cx.run_until_parked(); - - // Verify item1 was closed - pane.read_with(cx, |pane, _| { - assert_eq!( - pane.items().count(), - 1, - "Should have 1 item remaining after auto-close" - ); - }); - - // Check navigation history after close - let has_item = pane.read_with(cx, |pane, cx| { - let mut has_item = false; - pane.nav_history().for_each_entry(cx, |entry, _| { - if entry.item.id() == item1_id { - has_item = true; - } - }); - has_item - }); - - assert!( - !has_item, - "Navigation history should not contain closed item entries" - ); - } - - #[gpui::test] - async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane( - cx: &mut TestAppContext, - ) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - let dirty_regular_buffer = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_label("1.txt") - .with_project_items(&[dirty_project_item(1, "1.txt", cx)]) - }); - let dirty_regular_buffer_2 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_label("2.txt") - .with_project_items(&[dirty_project_item(2, "2.txt", cx)]) - }); - let clear_regular_buffer = cx.new(|cx| { - TestItem::new(cx) - .with_label("3.txt") - .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)]) - }); - - let dirty_multi_buffer = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_buffer_kind(ItemBufferKind::Multibuffer) - .with_label("Fake Project Search") - .with_project_items(&[ - dirty_regular_buffer.read(cx).project_items[0].clone(), - dirty_regular_buffer_2.read(cx).project_items[0].clone(), - clear_regular_buffer.read(cx).project_items[0].clone(), - ]) - }); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item( - pane.clone(), - Box::new(dirty_regular_buffer.clone()), - None, - false, - false, - window, - cx, - ); - workspace.add_item( - pane.clone(), - Box::new(dirty_regular_buffer_2.clone()), - None, - false, - false, - window, - cx, - ); - workspace.add_item( - pane.clone(), - Box::new(dirty_multi_buffer.clone()), - None, - false, - false, - window, - cx, - ); - }); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(2, true, true, window, cx); - assert_eq!( - pane.active_item().unwrap().item_id(), - dirty_multi_buffer.item_id(), - "Should select the multi buffer in the pane" - ); - }); - let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }); - cx.background_executor.run_until_parked(); - assert!( - !cx.has_pending_prompt(), - "All dirty items from the multi buffer are in the pane still, no save prompts should be shown" - ); - close_multi_buffer_task - .await - .expect("Closing multi buffer failed"); - pane.update(cx, |pane, cx| { - assert_eq!(dirty_regular_buffer.read(cx).save_count, 0); - assert_eq!(dirty_multi_buffer.read(cx).save_count, 0); - assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0); - assert_eq!( - pane.items() - .map(|item| item.item_id()) - .sorted() - .collect::>(), - vec![ - dirty_regular_buffer.item_id(), - dirty_regular_buffer_2.item_id(), - ], - "Should have no multi buffer left in the pane" - ); - assert!(dirty_regular_buffer.read(cx).is_dirty); - assert!(dirty_regular_buffer_2.read(cx).is_dirty); - }); - } - - #[gpui::test] - async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) { - init_test(cx); - 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, window, cx)); - - // Add a new panel to the right dock, opening the dock and setting the - // focus to the new panel. - let panel = workspace.update_in(cx, |workspace, window, cx| { - let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx)); - workspace.add_panel(panel.clone(), window, cx); - - workspace - .right_dock() - .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx)); - - workspace.toggle_panel_focus::(window, cx); - - panel - }); - - // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the - // panel to the next valid position which, in this case, is the left - // dock. - cx.dispatch_action(MoveFocusedPanelToNextPosition); - workspace.update(cx, |workspace, cx| { - assert!(workspace.left_dock().read(cx).is_open()); - assert_eq!(panel.read(cx).position, DockPosition::Left); - }); - - // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the - // panel to the next valid position which, in this case, is the bottom - // dock. - cx.dispatch_action(MoveFocusedPanelToNextPosition); - workspace.update(cx, |workspace, cx| { - assert!(workspace.bottom_dock().read(cx).is_open()); - assert_eq!(panel.read(cx).position, DockPosition::Bottom); - }); - - // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time - // around moving the panel to its initial position, the right dock. - cx.dispatch_action(MoveFocusedPanelToNextPosition); - workspace.update(cx, |workspace, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert_eq!(panel.read(cx).position, DockPosition::Right); - }); - - // Remove focus from the panel, ensuring that, if the panel is not - // focused, the `MoveFocusedPanelToNextPosition` action does not update - // the panel's position, so the panel is still in the right dock. - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_panel_focus::(window, cx); - }); - - cx.dispatch_action(MoveFocusedPanelToNextPosition); - workspace.update(cx, |workspace, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert_eq!(panel.read(cx).position, DockPosition::Right); - }); - } - - #[gpui::test] - async fn test_moving_items_create_panes(cx: &mut TestAppContext) { - init_test(cx); - - 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 item_1 = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)]) - }); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx); - workspace.move_item_to_pane_in_direction( - &MoveItemToPaneInDirection { - direction: SplitDirection::Right, - focus: true, - clone: false, - }, - window, - cx, - ); - workspace.move_item_to_pane_at_index( - &MoveItemToPane { - destination: 3, - focus: true, - clone: false, - }, - window, - cx, - ); - - assert_eq!(workspace.panes.len(), 1, "No new panes were created"); - assert_eq!( - pane_items_paths(&workspace.active_pane, cx), - vec!["first.txt".to_string()], - "Single item was not moved anywhere" - ); - }); - - let item_2 = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "second.txt", cx)]) - }); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item_2), None, true, window, cx); - assert_eq!( - pane_items_paths(&workspace.panes[0], cx), - vec!["first.txt".to_string(), "second.txt".to_string()], - ); - workspace.move_item_to_pane_in_direction( - &MoveItemToPaneInDirection { - direction: SplitDirection::Right, - focus: true, - clone: false, - }, - window, - cx, - ); - - assert_eq!(workspace.panes.len(), 2, "A new pane should be created"); - assert_eq!( - pane_items_paths(&workspace.panes[0], cx), - vec!["first.txt".to_string()], - "After moving, one item should be left in the original pane" - ); - assert_eq!( - pane_items_paths(&workspace.panes[1], cx), - vec!["second.txt".to_string()], - "New item should have been moved to the new pane" - ); - }); - - let item_3 = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "third.txt", cx)]) - }); - workspace.update_in(cx, |workspace, window, cx| { - let original_pane = workspace.panes[0].clone(); - workspace.set_active_pane(&original_pane, window, cx); - workspace.add_item_to_active_pane(Box::new(item_3), None, true, window, cx); - assert_eq!(workspace.panes.len(), 2, "No new panes were created"); - assert_eq!( - pane_items_paths(&workspace.active_pane, cx), - vec!["first.txt".to_string(), "third.txt".to_string()], - "New pane should be ready to move one item out" - ); - - workspace.move_item_to_pane_at_index( - &MoveItemToPane { - destination: 3, - focus: true, - clone: false, - }, - window, - cx, - ); - assert_eq!(workspace.panes.len(), 3, "A new pane should be created"); - assert_eq!( - pane_items_paths(&workspace.active_pane, cx), - vec!["first.txt".to_string()], - "After moving, one item should be left in the original pane" - ); - assert_eq!( - pane_items_paths(&workspace.panes[1], cx), - vec!["second.txt".to_string()], - "Previously created pane should be unchanged" - ); - assert_eq!( - pane_items_paths(&workspace.panes[2], cx), - vec!["third.txt".to_string()], - "New item should have been moved to the new pane" - ); - }); - } - - #[gpui::test] - async fn test_moving_items_can_clone_panes(cx: &mut TestAppContext) { - init_test(cx); - - 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 item_1 = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)]) - }); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx); - workspace.move_item_to_pane_in_direction( - &MoveItemToPaneInDirection { - direction: SplitDirection::Right, - focus: true, - clone: true, - }, - window, - cx, - ); - workspace.move_item_to_pane_at_index( - &MoveItemToPane { - destination: 3, - focus: true, - clone: true, - }, - window, - cx, - ); - }); - cx.run_until_parked(); - - workspace.update(cx, |workspace, cx| { - assert_eq!(workspace.panes.len(), 3, "Two new panes were created"); - for pane in workspace.panes() { - assert_eq!( - pane_items_paths(pane, cx), - vec!["first.txt".to_string()], - "Single item exists in all panes" - ); - } - }); - - // verify that the active pane has been updated after waiting for the - // pane focus event to fire and resolve - workspace.read_with(cx, |workspace, _app| { - assert_eq!( - workspace.active_pane(), - &workspace.panes[2], - "The third pane should be the active one: {:?}", - workspace.panes - ); - }) - } - - mod register_project_item_tests { - - use super::*; - - // View - struct TestPngItemView { - focus_handle: FocusHandle, - } - // Model - struct TestPngItem {} - - impl project::ProjectItem for TestPngItem { - fn try_open( - _project: &Entity, - path: &ProjectPath, - cx: &mut App, - ) -> Option>>> { - if path.path.extension().unwrap() == "png" { - Some(cx.spawn(async move |cx| cx.new(|_| TestPngItem {}))) - } else { - None - } - } - - fn entry_id(&self, _: &App) -> Option { - None - } - - fn project_path(&self, _: &App) -> Option { - None - } - - fn is_dirty(&self) -> bool { - false - } - } - - impl Item for TestPngItemView { - type Event = (); - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "".into() - } - } - impl EventEmitter<()> for TestPngItemView {} - impl Focusable for TestPngItemView { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } - } - - impl Render for TestPngItemView { - fn render( - &mut self, - _window: &mut Window, - _cx: &mut Context, - ) -> impl IntoElement { - Empty - } - } - - impl ProjectItem for TestPngItemView { - type Item = TestPngItem; - - fn for_project_item( - _project: Entity, - _pane: Option<&Pane>, - _item: Entity, - _: &mut Window, - cx: &mut Context, - ) -> Self - where - Self: Sized, - { - Self { - focus_handle: cx.focus_handle(), - } - } - } - - // View - struct TestIpynbItemView { - focus_handle: FocusHandle, - } - // Model - struct TestIpynbItem {} - - impl project::ProjectItem for TestIpynbItem { - fn try_open( - _project: &Entity, - path: &ProjectPath, - cx: &mut App, - ) -> Option>>> { - if path.path.extension().unwrap() == "ipynb" { - Some(cx.spawn(async move |cx| cx.new(|_| TestIpynbItem {}))) - } else { - None - } - } - - fn entry_id(&self, _: &App) -> Option { - None - } - - fn project_path(&self, _: &App) -> Option { - None - } - - fn is_dirty(&self) -> bool { - false - } - } - - impl Item for TestIpynbItemView { - type Event = (); - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "".into() - } - } - impl EventEmitter<()> for TestIpynbItemView {} - impl Focusable for TestIpynbItemView { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } - } - - impl Render for TestIpynbItemView { - fn render( - &mut self, - _window: &mut Window, - _cx: &mut Context, - ) -> impl IntoElement { - Empty - } - } - - impl ProjectItem for TestIpynbItemView { - type Item = TestIpynbItem; - - fn for_project_item( - _project: Entity, - _pane: Option<&Pane>, - _item: Entity, - _: &mut Window, - cx: &mut Context, - ) -> Self - where - Self: Sized, - { - Self { - focus_handle: cx.focus_handle(), - } - } - } - - struct TestAlternatePngItemView { - focus_handle: FocusHandle, - } - - impl Item for TestAlternatePngItemView { - type Event = (); - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "".into() - } - } - - impl EventEmitter<()> for TestAlternatePngItemView {} - impl Focusable for TestAlternatePngItemView { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } - } - - impl Render for TestAlternatePngItemView { - fn render( - &mut self, - _window: &mut Window, - _cx: &mut Context, - ) -> impl IntoElement { - Empty - } - } - - impl ProjectItem for TestAlternatePngItemView { - type Item = TestPngItem; - - fn for_project_item( - _project: Entity, - _pane: Option<&Pane>, - _item: Entity, - _: &mut Window, - cx: &mut Context, - ) -> Self - where - Self: Sized, - { - Self { - focus_handle: cx.focus_handle(), - } - } - } - - #[gpui::test] - async fn test_register_project_item(cx: &mut TestAppContext) { - init_test(cx); - - cx.update(|cx| { - register_project_item::(cx); - register_project_item::(cx); - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "one.png": "BINARYDATAHERE", - "two.ipynb": "{ totally a notebook }", - "three.txt": "editing text, sure why not?" - }), - ) - .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, rel_path("one.png")); - workspace.open_path(project_path, None, true, window, cx) - }) - .await - .unwrap(); - - // Now we can check if the handle we got back errored or not - assert_eq!( - handle.to_any_view().entity_type(), - TypeId::of::() - ); - - let handle = workspace - .update_in(cx, |workspace, window, cx| { - let project_path = (worktree_id, rel_path("two.ipynb")); - workspace.open_path(project_path, None, true, window, cx) - }) - .await - .unwrap(); - - assert_eq!( - handle.to_any_view().entity_type(), - TypeId::of::() - ); - - let handle = workspace - .update_in(cx, |workspace, window, cx| { - let project_path = (worktree_id, rel_path("three.txt")); - workspace.open_path(project_path, None, true, window, cx) - }) - .await; - assert!(handle.is_err()); - } - - #[gpui::test] - async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) { - init_test(cx); - - cx.update(|cx| { - register_project_item::(cx); - register_project_item::(cx); - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "one.png": "BINARYDATAHERE", - "two.ipynb": "{ totally a notebook }", - "three.txt": "editing text, sure why not?" - }), - ) - .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, rel_path("one.png")); - workspace.open_path(project_path, None, true, window, cx) - }) - .await - .unwrap(); - - // This _must_ be the second item registered - assert_eq!( - handle.to_any_view().entity_type(), - TypeId::of::() - ); - - let handle = workspace - .update_in(cx, |workspace, window, cx| { - let project_path = (worktree_id, rel_path("three.txt")); - workspace.open_path(project_path, None, true, window, cx) - }) - .await; - assert!(handle.is_err()); - } - } - - #[gpui::test] - async fn test_status_bar_visibility(cx: &mut TestAppContext) { - init_test(cx); - - 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)); - - // Test with status bar shown (default) - workspace.read_with(cx, |workspace, cx| { - let visible = workspace.status_bar_visible(cx); - assert!(visible, "Status bar should be visible by default"); - }); - - // Test with status bar hidden - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| { - settings.status_bar.get_or_insert_default().show = Some(false); - }); - }); - - workspace.read_with(cx, |workspace, cx| { - let visible = workspace.status_bar_visible(cx); - assert!(!visible, "Status bar should be hidden when show is false"); - }); - - // Test with status bar shown explicitly - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| { - settings.status_bar.get_or_insert_default().show = Some(true); - }); - }); - - workspace.read_with(cx, |workspace, cx| { - let visible = workspace.status_bar_visible(cx); - assert!(visible, "Status bar should be visible when show is true"); - }); - } - - fn pane_items_paths(pane: &Entity, cx: &App) -> Vec { - pane.read(cx) - .items() - .flat_map(|item| { - item.project_paths(cx) - .into_iter() - .map(|path| path.path.display(PathStyle::local()).into_owned()) - }) - .collect() - } - - pub fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - theme::init(theme::LoadThemes::JustBase, cx); - }); - } - - fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity { - let item = TestProjectItem::new(id, path, cx); - item.update(cx, |item, _| { - item.is_dirty = true; - }); - item - } -} diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs deleted file mode 100644 index 4ce0394fe5..0000000000 --- a/crates/workspace/src/workspace_settings.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::num::NonZeroUsize; - -use crate::DockPosition; -use collections::HashMap; -use serde::Deserialize; -pub use settings::{ - AutosaveSetting, BottomDockLayout, InactiveOpacity, PaneSplitDirectionHorizontal, - PaneSplitDirectionVertical, RegisterSetting, RestoreOnStartupBehavior, Settings, -}; - -#[derive(RegisterSetting)] -pub struct WorkspaceSettings { - pub active_pane_modifiers: ActivePanelModifiers, - pub bottom_dock_layout: settings::BottomDockLayout, - pub pane_split_direction_horizontal: settings::PaneSplitDirectionHorizontal, - pub pane_split_direction_vertical: settings::PaneSplitDirectionVertical, - pub centered_layout: settings::CenteredLayoutSettings, - pub confirm_quit: bool, - pub show_call_status_icon: bool, - pub autosave: AutosaveSetting, - pub restore_on_startup: settings::RestoreOnStartupBehavior, - pub restore_on_file_reopen: bool, - pub drop_target_size: f32, - pub use_system_path_prompts: bool, - pub use_system_prompts: bool, - pub command_aliases: HashMap, - pub max_tabs: Option, - pub when_closing_with_no_tabs: settings::CloseWindowWhenNoItems, - pub on_last_window_closed: settings::OnLastWindowClosed, - pub resize_all_panels_in_dock: Vec, - pub close_on_file_delete: bool, - pub use_system_window_tabs: bool, - pub zoomed_padding: bool, - pub window_decorations: settings::WindowDecorations, -} - -#[derive(Copy, Clone, PartialEq, Debug, Default)] -pub struct ActivePanelModifiers { - /// Size of the border surrounding the active pane. - /// When set to 0, the active pane doesn't have any border. - /// The border is drawn inset. - /// - /// Default: `0.0` - // TODO: make this not an option, it is never None - pub border_size: Option, - /// Opacity of inactive panels. - /// When set to 1.0, the inactive panes have the same opacity as the active one. - /// If set to 0, the inactive panes content will not be visible at all. - /// Values are clamped to the [0.0, 1.0] range. - /// - /// Default: `1.0` - // TODO: make this not an option, it is never None - pub inactive_opacity: Option, -} - -#[derive(Deserialize, RegisterSetting)] -pub struct TabBarSettings { - pub show: bool, - pub show_nav_history_buttons: bool, - pub show_tab_bar_buttons: bool, -} - -impl Settings for WorkspaceSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let workspace = &content.workspace; - Self { - active_pane_modifiers: ActivePanelModifiers { - border_size: Some( - workspace - .active_pane_modifiers - .unwrap() - .border_size - .unwrap(), - ), - inactive_opacity: Some( - workspace - .active_pane_modifiers - .unwrap() - .inactive_opacity - .unwrap(), - ), - }, - bottom_dock_layout: workspace.bottom_dock_layout.unwrap(), - pane_split_direction_horizontal: workspace.pane_split_direction_horizontal.unwrap(), - pane_split_direction_vertical: workspace.pane_split_direction_vertical.unwrap(), - centered_layout: workspace.centered_layout.unwrap(), - confirm_quit: workspace.confirm_quit.unwrap(), - show_call_status_icon: workspace.show_call_status_icon.unwrap(), - autosave: workspace.autosave.unwrap(), - restore_on_startup: workspace.restore_on_startup.unwrap(), - restore_on_file_reopen: workspace.restore_on_file_reopen.unwrap(), - drop_target_size: workspace.drop_target_size.unwrap(), - use_system_path_prompts: workspace.use_system_path_prompts.unwrap(), - use_system_prompts: workspace.use_system_prompts.unwrap(), - command_aliases: workspace.command_aliases.clone(), - max_tabs: workspace.max_tabs, - when_closing_with_no_tabs: workspace.when_closing_with_no_tabs.unwrap(), - on_last_window_closed: workspace.on_last_window_closed.unwrap(), - resize_all_panels_in_dock: workspace - .resize_all_panels_in_dock - .clone() - .unwrap() - .into_iter() - .map(Into::into) - .collect(), - close_on_file_delete: workspace.close_on_file_delete.unwrap(), - use_system_window_tabs: workspace.use_system_window_tabs.unwrap(), - zoomed_padding: workspace.zoomed_padding.unwrap(), - window_decorations: workspace.window_decorations.unwrap(), - } - } -} - -impl Settings for TabBarSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let tab_bar = content.tab_bar.clone().unwrap(); - TabBarSettings { - show: tab_bar.show.unwrap(), - show_nav_history_buttons: tab_bar.show_nav_history_buttons.unwrap(), - show_tab_bar_buttons: tab_bar.show_tab_bar_buttons.unwrap(), - } - } -} - -#[derive(Deserialize, RegisterSetting)] -pub struct StatusBarSettings { - pub show: bool, - pub active_language_button: bool, - pub cursor_position_button: bool, - pub line_endings_button: bool, -} - -impl Settings for StatusBarSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let status_bar = content.status_bar.clone().unwrap(); - StatusBarSettings { - show: status_bar.show.unwrap(), - active_language_button: status_bar.active_language_button.unwrap(), - cursor_position_button: status_bar.cursor_position_button.unwrap(), - line_endings_button: status_bar.line_endings_button.unwrap(), - } - } -} diff --git a/crates/worktree/Cargo.toml b/crates/worktree/Cargo.toml deleted file mode 100644 index 6d132fbd2c..0000000000 --- a/crates/worktree/Cargo.toml +++ /dev/null @@ -1,63 +0,0 @@ -[package] -name = "worktree" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lib] -path = "src/worktree.rs" -doctest = false - -[lints] -workspace = true - -[features] -test-support = [ - "gpui/test-support", - "http_client/test-support", - "language/test-support", - "settings/test-support", - "text/test-support", - "util/test-support", -] - -[dependencies] -anyhow.workspace = true -async-lock.workspace = true -clock.workspace = true -collections.workspace = true -fs.workspace = true -futures.workspace = true -fuzzy.workspace = true -git.workspace = true -gpui.workspace = true -ignore.workspace = true -language.workspace = true -log.workspace = true -parking_lot.workspace = true -paths.workspace = true -postage.workspace = true -rpc = { workspace = true, features = ["gpui"] } -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smallvec.workspace = true -smol.workspace = true -sum_tree.workspace = true -text.workspace = true -util.workspace = true - -[dev-dependencies] -clock = { workspace = true, features = ["test-support"] } -collections = { workspace = true, features = ["test-support"] } -git2.workspace = true -gpui = { workspace = true, features = ["test-support"] } -http_client.workspace = true -paths = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true -rand.workspace = true -rpc = { workspace = true, features = ["test-support"] } -settings = { workspace = true, features = ["test-support"] } -util = { workspace = true, features = ["test-support"] } -zlog.workspace = true diff --git a/crates/worktree/LICENSE-GPL b/crates/worktree/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/worktree/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/worktree/src/ignore.rs b/crates/worktree/src/ignore.rs deleted file mode 100644 index 17c362e2d7..0000000000 --- a/crates/worktree/src/ignore.rs +++ /dev/null @@ -1,102 +0,0 @@ -use ignore::gitignore::Gitignore; -use std::{ffi::OsStr, path::Path, sync::Arc}; - -#[derive(Clone, Debug)] -pub struct IgnoreStack { - pub repo_root: Option>, - pub top: Arc, -} - -#[derive(Debug)] -pub enum IgnoreStackEntry { - None, - Global { - ignore: Arc, - }, - Some { - abs_base_path: Arc, - ignore: Arc, - parent: Arc, - }, - All, -} - -impl IgnoreStack { - pub fn none() -> Self { - Self { - repo_root: None, - top: Arc::new(IgnoreStackEntry::None), - } - } - - pub fn all() -> Self { - Self { - repo_root: None, - top: Arc::new(IgnoreStackEntry::All), - } - } - - pub fn global(ignore: Arc) -> Self { - Self { - repo_root: None, - top: Arc::new(IgnoreStackEntry::Global { ignore }), - } - } - - pub fn append(self, abs_base_path: Arc, ignore: Arc) -> Self { - let top = match self.top.as_ref() { - IgnoreStackEntry::All => self.top.clone(), - _ => Arc::new(IgnoreStackEntry::Some { - abs_base_path, - ignore, - parent: self.top.clone(), - }), - }; - Self { - repo_root: self.repo_root, - top, - } - } - - pub fn is_abs_path_ignored(&self, abs_path: &Path, is_dir: bool) -> bool { - if is_dir && abs_path.file_name() == Some(OsStr::new(".git")) { - return true; - } - - match self.top.as_ref() { - IgnoreStackEntry::None => false, - IgnoreStackEntry::All => true, - IgnoreStackEntry::Global { ignore } => { - let combined_path; - let abs_path = if let Some(repo_root) = self.repo_root.as_ref() { - combined_path = ignore.path().join( - abs_path - .strip_prefix(repo_root) - .expect("repo root should be a parent of matched path"), - ); - &combined_path - } else { - abs_path - }; - match ignore.matched(abs_path, is_dir) { - ignore::Match::None => false, - ignore::Match::Ignore(_) => true, - ignore::Match::Whitelist(_) => false, - } - } - IgnoreStackEntry::Some { - abs_base_path, - ignore, - parent: prev, - } => match ignore.matched(abs_path.strip_prefix(abs_base_path).unwrap(), is_dir) { - ignore::Match::None => IgnoreStack { - repo_root: self.repo_root.clone(), - top: prev.clone(), - } - .is_abs_path_ignored(abs_path, is_dir), - ignore::Match::Ignore(_) => true, - ignore::Match::Whitelist(_) => false, - }, - } - } -} diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs deleted file mode 100644 index e1ce31c038..0000000000 --- a/crates/worktree/src/worktree.rs +++ /dev/null @@ -1,5677 +0,0 @@ -mod ignore; -mod worktree_settings; -#[cfg(test)] -mod worktree_tests; - -use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; -use anyhow::{Context as _, Result, anyhow}; -use clock::ReplicaId; -use collections::{HashMap, HashSet, VecDeque}; -use fs::{Fs, MTime, PathEvent, RemoveOptions, Watcher, copy_recursive, read_dir_items}; -use futures::{ - FutureExt as _, Stream, StreamExt, - channel::{ - mpsc::{self, UnboundedSender}, - oneshot, - }, - select_biased, stream, - task::Poll, -}; -use fuzzy::CharBag; -use git::{ - COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, INDEX_LOCK, LFS_DIR, status::GitSummary, -}; -use gpui::{ - App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, Priority, - Task, -}; -use ignore::IgnoreStack; -use language::DiskState; - -use parking_lot::Mutex; -use paths::{local_settings_folder_name, local_vscode_folder_name}; -use postage::{ - barrier, - prelude::{Sink as _, Stream as _}, - watch, -}; -use rpc::{ - AnyProtoClient, - proto::{self, split_worktree_update}, -}; -pub use settings::WorktreeId; -use settings::{Settings, SettingsLocation, SettingsStore}; -use smallvec::{SmallVec, smallvec}; -use smol::channel::{self, Sender}; -use std::{ - any::Any, - borrow::Borrow as _, - cmp::Ordering, - collections::hash_map, - convert::TryFrom, - ffi::OsStr, - fmt, - future::Future, - mem::{self}, - ops::{Deref, DerefMut, Range}, - path::{Path, PathBuf}, - pin::Pin, - sync::{ - Arc, - atomic::{AtomicUsize, Ordering::SeqCst}, - }, - time::{Duration, Instant}, -}; -use sum_tree::{Bias, Dimensions, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet}; -use text::{LineEnding, Rope}; -use util::{ - ResultExt, debug_panic, maybe, - paths::{PathMatcher, PathStyle, SanitizedPath, home_dir}, - rel_path::RelPath, -}; -pub use worktree_settings::WorktreeSettings; - -pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100); - -/// A set of local or remote files that are being opened as part of a project. -/// Responsible for tracking related FS (for local)/collab (for remote) events and corresponding updates. -/// Stores git repositories data and the diagnostics for the file(s). -/// -/// Has an absolute path, and may be set to be visible in Zed UI or not. -/// May correspond to a directory or a single file. -/// Possible examples: -/// * a drag and dropped file — may be added as an invisible, "ephemeral" entry to the current worktree -/// * a directory opened in Zed — may be added as a visible entry to the current worktree -/// -/// Uses [`Entry`] to track the state of each file/directory, can look up absolute paths for entries. -pub enum Worktree { - Local(LocalWorktree), - Remote(RemoteWorktree), -} - -/// An entry, created in the worktree. -#[derive(Debug)] -pub enum CreatedEntry { - /// Got created and indexed by the worktree, receiving a corresponding entry. - Included(Entry), - /// Got created, but not indexed due to falling under exclusion filters. - Excluded { abs_path: PathBuf }, -} - -#[derive(Debug)] -pub struct LoadedFile { - pub file: Arc, - pub text: String, -} - -pub struct LoadedBinaryFile { - pub file: Arc, - pub content: Vec, -} - -impl fmt::Debug for LoadedBinaryFile { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("LoadedBinaryFile") - .field("file", &self.file) - .field("content_bytes", &self.content.len()) - .finish() - } -} - -pub struct LocalWorktree { - snapshot: LocalSnapshot, - scan_requests_tx: channel::Sender, - path_prefixes_to_scan_tx: channel::Sender, - is_scanning: (watch::Sender, watch::Receiver), - _background_scanner_tasks: Vec>, - update_observer: Option, - fs: Arc, - fs_case_sensitive: bool, - visible: bool, - next_entry_id: Arc, - settings: WorktreeSettings, - share_private_files: bool, - scanning_enabled: bool, -} - -pub struct PathPrefixScanRequest { - path: Arc, - done: SmallVec<[barrier::Sender; 1]>, -} - -struct ScanRequest { - relative_paths: Vec>, - done: SmallVec<[barrier::Sender; 1]>, -} - -pub struct RemoteWorktree { - snapshot: Snapshot, - background_snapshot: Arc)>>, - project_id: u64, - client: AnyProtoClient, - file_scan_inclusions: PathMatcher, - updates_tx: Option>, - update_observer: Option>, - snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>, - replica_id: ReplicaId, - visible: bool, - disconnected: bool, -} - -#[derive(Clone)] -pub struct Snapshot { - id: WorktreeId, - /// The absolute path of the worktree root. - abs_path: Arc, - path_style: PathStyle, - root_name: Arc, - root_char_bag: CharBag, - entries_by_path: SumTree, - entries_by_id: SumTree, - always_included_entries: Vec>, - - /// A number that increases every time the worktree begins scanning - /// a set of paths from the filesystem. This scanning could be caused - /// by some operation performed on the worktree, such as reading or - /// writing a file, or by an event reported by the filesystem. - scan_id: usize, - - /// The latest scan id that has completed, and whose preceding scans - /// have all completed. The current `scan_id` could be more than one - /// greater than the `completed_scan_id` if operations are performed - /// on the worktree while it is processing a file-system event. - completed_scan_id: usize, -} - -/// This path corresponds to the 'content path' of a repository in relation -/// to Zed's project root. -/// In the majority of the cases, this is the folder that contains the .git folder. -/// But if a sub-folder of a git repository is opened, this corresponds to the -/// project root and the .git folder is located in a parent directory. -#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)] -pub enum WorkDirectory { - InProject { - relative_path: Arc, - }, - AboveProject { - absolute_path: Arc, - location_in_repo: Arc, - }, -} - -impl WorkDirectory { - fn path_key(&self) -> PathKey { - match self { - WorkDirectory::InProject { relative_path } => PathKey(relative_path.clone()), - WorkDirectory::AboveProject { .. } => PathKey(RelPath::empty().into()), - } - } - - /// Returns true if the given path is a child of the work directory. - /// - /// Note that the path may not be a member of this repository, if there - /// is a repository in a directory between these two paths - /// external .git folder in a parent folder of the project root. - #[track_caller] - pub fn directory_contains(&self, path: &RelPath) -> bool { - match self { - WorkDirectory::InProject { relative_path } => path.starts_with(relative_path), - WorkDirectory::AboveProject { .. } => true, - } - } -} - -impl Default for WorkDirectory { - fn default() -> Self { - Self::InProject { - relative_path: Arc::from(RelPath::empty()), - } - } -} - -#[derive(Clone)] -pub struct LocalSnapshot { - snapshot: Snapshot, - global_gitignore: Option>, - /// All of the gitignore files in the worktree, indexed by their absolute path. - /// The boolean indicates whether the gitignore needs to be updated. - ignores_by_parent_abs_path: HashMap, (Arc, bool)>, - /// All of the git repositories in the worktree, indexed by the project entry - /// id of their parent directory. - git_repositories: TreeMap, - /// The file handle of the worktree root - /// (so we can find it after it's been moved) - root_file_handle: Option>, - executor: BackgroundExecutor, -} - -struct BackgroundScannerState { - snapshot: LocalSnapshot, - scanned_dirs: HashSet, - path_prefixes_to_scan: HashSet>, - paths_to_scan: HashSet>, - /// The ids of all of the entries that were removed from the snapshot - /// as part of the current update. These entry ids may be re-used - /// if the same inode is discovered at a new path, or if the given - /// path is re-created after being deleted. - removed_entries: HashMap, - changed_paths: Vec>, - prev_snapshot: Snapshot, -} - -#[derive(Debug, Clone)] -struct LocalRepositoryEntry { - work_directory_id: ProjectEntryId, - work_directory: WorkDirectory, - work_directory_abs_path: Arc, - git_dir_scan_id: usize, - /// Absolute path to the original .git entry that caused us to create this repository. - /// - /// This is normally a directory, but may be a "gitfile" that points to a directory elsewhere - /// (whose path we then store in `repository_dir_abs_path`). - dot_git_abs_path: Arc, - /// Absolute path to the "commondir" for this repository. - /// - /// This is always a directory. For a normal repository, this is the same as dot_git_abs_path, - /// but in the case of a submodule or a worktree it is the path to the "parent" .git directory - /// from which the submodule/worktree was derived. - common_dir_abs_path: Arc, - /// Absolute path to the directory holding the repository's state. - /// - /// For a normal repository, this is a directory and coincides with `dot_git_abs_path` and - /// `common_dir_abs_path`. For a submodule or worktree, this is some subdirectory of the - /// commondir like `/project/.git/modules/foo`. - repository_dir_abs_path: Arc, -} - -impl sum_tree::Item for LocalRepositoryEntry { - type Summary = PathSummary; - - fn summary(&self, _: ::Context<'_>) -> Self::Summary { - PathSummary { - max_path: self.work_directory.path_key().0, - item_summary: sum_tree::NoSummary, - } - } -} - -impl KeyedItem for LocalRepositoryEntry { - type Key = PathKey; - - fn key(&self) -> Self::Key { - self.work_directory.path_key() - } -} - -impl Deref for LocalRepositoryEntry { - type Target = WorkDirectory; - - fn deref(&self) -> &Self::Target { - &self.work_directory - } -} - -impl Deref for LocalSnapshot { - type Target = Snapshot; - - fn deref(&self) -> &Self::Target { - &self.snapshot - } -} - -impl DerefMut for LocalSnapshot { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.snapshot - } -} - -enum ScanState { - Started, - Updated { - snapshot: LocalSnapshot, - changes: UpdatedEntriesSet, - barrier: SmallVec<[barrier::Sender; 1]>, - scanning: bool, - }, - RootUpdated { - new_path: Arc, - }, -} - -struct UpdateObservationState { - snapshots_tx: mpsc::UnboundedSender<(LocalSnapshot, UpdatedEntriesSet)>, - resume_updates: watch::Sender<()>, - _maintain_remote_snapshot: Task>, -} - -#[derive(Clone)] -pub enum Event { - UpdatedEntries(UpdatedEntriesSet), - UpdatedGitRepositories(UpdatedGitRepositoriesSet), - DeletedEntry(ProjectEntryId), -} - -impl EventEmitter for Worktree {} - -impl Worktree { - pub async fn local( - path: impl Into>, - visible: bool, - fs: Arc, - next_entry_id: Arc, - scanning_enabled: bool, - cx: &mut AsyncApp, - ) -> Result> { - let abs_path = path.into(); - let metadata = fs - .metadata(&abs_path) - .await - .context("failed to stat worktree path")?; - - let fs_case_sensitive = fs.is_case_sensitive().await.unwrap_or_else(|e| { - log::error!( - "Failed to determine whether filesystem is case sensitive (falling back to true) due to error: {e:#}" - ); - true - }); - - let root_file_handle = if metadata.as_ref().is_some() { - fs.open_handle(&abs_path) - .await - .with_context(|| { - format!( - "failed to open local worktree root at {}", - abs_path.display() - ) - }) - .log_err() - } else { - None - }; - - cx.new(move |cx: &mut Context| { - let mut snapshot = LocalSnapshot { - ignores_by_parent_abs_path: Default::default(), - global_gitignore: Default::default(), - git_repositories: Default::default(), - snapshot: Snapshot::new( - cx.entity_id().as_u64(), - abs_path - .file_name() - .and_then(|f| f.to_str()) - .map_or(RelPath::empty().into(), |f| { - RelPath::unix(f).unwrap().into() - }), - abs_path.clone(), - PathStyle::local(), - ), - root_file_handle, - executor: cx.background_executor().clone(), - }; - - let worktree_id = snapshot.id(); - let settings_location = Some(SettingsLocation { - worktree_id, - path: RelPath::empty(), - }); - - let settings = WorktreeSettings::get(settings_location, cx).clone(); - cx.observe_global::(move |this, cx| { - if let Self::Local(this) = this { - let settings = WorktreeSettings::get(settings_location, cx).clone(); - if this.settings != settings { - this.settings = settings; - this.restart_background_scanners(cx); - } - } - }) - .detach(); - - let share_private_files = false; - if let Some(metadata) = metadata { - let mut entry = Entry::new( - RelPath::empty().into(), - &metadata, - ProjectEntryId::new(&next_entry_id), - snapshot.root_char_bag, - None, - ); - if !metadata.is_dir { - if let Some(file_name) = abs_path.file_name() - && let Some(file_name) = file_name.to_str() - && let Ok(path) = RelPath::unix(file_name) - { - entry.is_private = !share_private_files && settings.is_path_private(path); - entry.is_hidden = settings.is_path_hidden(path); - } - } - snapshot.insert_entry(entry, fs.as_ref()); - } - - let (scan_requests_tx, scan_requests_rx) = channel::unbounded(); - let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded(); - let mut worktree = LocalWorktree { - share_private_files, - next_entry_id, - snapshot, - is_scanning: watch::channel_with(true), - update_observer: None, - scan_requests_tx, - path_prefixes_to_scan_tx, - _background_scanner_tasks: Vec::new(), - fs, - fs_case_sensitive, - visible, - settings, - scanning_enabled, - }; - worktree.start_background_scanner(scan_requests_rx, path_prefixes_to_scan_rx, cx); - Worktree::Local(worktree) - }) - } - - pub fn remote( - project_id: u64, - replica_id: ReplicaId, - worktree: proto::WorktreeMetadata, - client: AnyProtoClient, - path_style: PathStyle, - cx: &mut App, - ) -> Entity { - cx.new(|cx: &mut Context| { - let snapshot = Snapshot::new( - worktree.id, - RelPath::from_proto(&worktree.root_name) - .unwrap_or_else(|_| RelPath::empty().into()), - Path::new(&worktree.abs_path).into(), - path_style, - ); - - let background_snapshot = Arc::new(Mutex::new(( - snapshot.clone(), - Vec::::new(), - ))); - let (background_updates_tx, mut background_updates_rx) = - mpsc::unbounded::(); - let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel(); - - let worktree_id = snapshot.id(); - let settings_location = Some(SettingsLocation { - worktree_id, - path: RelPath::empty(), - }); - - let settings = WorktreeSettings::get(settings_location, cx).clone(); - let worktree = RemoteWorktree { - client, - project_id, - replica_id, - snapshot, - file_scan_inclusions: settings.parent_dir_scan_inclusions.clone(), - background_snapshot: background_snapshot.clone(), - updates_tx: Some(background_updates_tx), - update_observer: None, - snapshot_subscriptions: Default::default(), - visible: worktree.visible, - disconnected: false, - }; - - // Apply updates to a separate snapshot in a background task, then - // send them to a foreground task which updates the model. - cx.background_spawn(async move { - while let Some(update) = background_updates_rx.next().await { - { - let mut lock = background_snapshot.lock(); - lock.0.apply_remote_update( - update.clone(), - &settings.parent_dir_scan_inclusions, - ); - lock.1.push(update); - } - snapshot_updated_tx.send(()).await.ok(); - } - }) - .detach(); - - // On the foreground task, update to the latest snapshot and notify - // any update observer of all updates that led to that snapshot. - cx.spawn(async move |this, cx| { - while (snapshot_updated_rx.recv().await).is_some() { - this.update(cx, |this, cx| { - let mut entries_changed = false; - let this = this.as_remote_mut().unwrap(); - { - let mut lock = this.background_snapshot.lock(); - this.snapshot = lock.0.clone(); - for update in lock.1.drain(..) { - entries_changed |= !update.updated_entries.is_empty() - || !update.removed_entries.is_empty(); - if let Some(tx) = &this.update_observer { - tx.unbounded_send(update).ok(); - } - } - }; - - if entries_changed { - cx.emit(Event::UpdatedEntries(Arc::default())); - } - cx.notify(); - while let Some((scan_id, _)) = this.snapshot_subscriptions.front() { - if this.observed_snapshot(*scan_id) { - let (_, tx) = this.snapshot_subscriptions.pop_front().unwrap(); - let _ = tx.send(()); - } else { - break; - } - } - })?; - } - anyhow::Ok(()) - }) - .detach(); - - Worktree::Remote(worktree) - }) - } - - pub fn as_local(&self) -> Option<&LocalWorktree> { - if let Worktree::Local(worktree) = self { - Some(worktree) - } else { - None - } - } - - pub fn as_remote(&self) -> Option<&RemoteWorktree> { - if let Worktree::Remote(worktree) = self { - Some(worktree) - } else { - None - } - } - - pub fn as_local_mut(&mut self) -> Option<&mut LocalWorktree> { - if let Worktree::Local(worktree) = self { - Some(worktree) - } else { - None - } - } - - pub fn as_remote_mut(&mut self) -> Option<&mut RemoteWorktree> { - if let Worktree::Remote(worktree) = self { - Some(worktree) - } else { - None - } - } - - pub fn is_local(&self) -> bool { - matches!(self, Worktree::Local(_)) - } - - pub fn is_remote(&self) -> bool { - !self.is_local() - } - - pub fn settings_location(&self, _: &Context) -> SettingsLocation<'static> { - SettingsLocation { - worktree_id: self.id(), - path: RelPath::empty(), - } - } - - pub fn snapshot(&self) -> Snapshot { - match self { - Worktree::Local(worktree) => worktree.snapshot.snapshot.clone(), - Worktree::Remote(worktree) => worktree.snapshot.clone(), - } - } - - pub fn scan_id(&self) -> usize { - match self { - Worktree::Local(worktree) => worktree.snapshot.scan_id, - Worktree::Remote(worktree) => worktree.snapshot.scan_id, - } - } - - pub fn metadata_proto(&self) -> proto::WorktreeMetadata { - proto::WorktreeMetadata { - id: self.id().to_proto(), - root_name: self.root_name().to_proto(), - visible: self.is_visible(), - abs_path: self.abs_path().to_string_lossy().into_owned(), - } - } - - pub fn completed_scan_id(&self) -> usize { - match self { - Worktree::Local(worktree) => worktree.snapshot.completed_scan_id, - Worktree::Remote(worktree) => worktree.snapshot.completed_scan_id, - } - } - - pub fn is_visible(&self) -> bool { - match self { - Worktree::Local(worktree) => worktree.visible, - Worktree::Remote(worktree) => worktree.visible, - } - } - - pub fn replica_id(&self) -> ReplicaId { - match self { - Worktree::Local(_) => ReplicaId::LOCAL, - Worktree::Remote(worktree) => worktree.replica_id, - } - } - - pub fn abs_path(&self) -> Arc { - match self { - Worktree::Local(worktree) => SanitizedPath::cast_arc(worktree.abs_path.clone()), - Worktree::Remote(worktree) => SanitizedPath::cast_arc(worktree.abs_path.clone()), - } - } - - pub fn root_file(&self, cx: &Context) -> Option> { - let entry = self.root_entry()?; - Some(File::for_entry(entry.clone(), cx.entity())) - } - - pub fn observe_updates(&mut self, project_id: u64, cx: &Context, callback: F) - where - F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut, - Fut: 'static + Send + Future, - { - match self { - Worktree::Local(this) => this.observe_updates(project_id, cx, callback), - Worktree::Remote(this) => this.observe_updates(project_id, cx, callback), - } - } - - pub fn stop_observing_updates(&mut self) { - match self { - Worktree::Local(this) => { - this.update_observer.take(); - } - Worktree::Remote(this) => { - this.update_observer.take(); - } - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn has_update_observer(&self) -> bool { - match self { - Worktree::Local(this) => this.update_observer.is_some(), - Worktree::Remote(this) => this.update_observer.is_some(), - } - } - - pub fn load_file(&self, path: &RelPath, cx: &Context) -> Task> { - match self { - Worktree::Local(this) => this.load_file(path, cx), - Worktree::Remote(_) => { - Task::ready(Err(anyhow!("remote worktrees can't yet load files"))) - } - } - } - - pub fn load_binary_file( - &self, - path: &RelPath, - cx: &Context, - ) -> Task> { - match self { - Worktree::Local(this) => this.load_binary_file(path, cx), - Worktree::Remote(_) => { - Task::ready(Err(anyhow!("remote worktrees can't yet load binary files"))) - } - } - } - - pub fn write_file( - &self, - path: Arc, - text: Rope, - line_ending: LineEnding, - cx: &Context, - ) -> Task>> { - match self { - Worktree::Local(this) => this.write_file(path, text, line_ending, cx), - Worktree::Remote(_) => { - Task::ready(Err(anyhow!("remote worktree can't yet write files"))) - } - } - } - - pub fn create_entry( - &mut self, - path: Arc, - is_directory: bool, - content: Option>, - cx: &Context, - ) -> Task> { - let worktree_id = self.id(); - match self { - Worktree::Local(this) => this.create_entry(path, is_directory, content, cx), - Worktree::Remote(this) => { - let project_id = this.project_id; - let request = this.client.request(proto::CreateProjectEntry { - worktree_id: worktree_id.to_proto(), - project_id, - path: path.as_ref().to_proto(), - content, - is_directory, - }); - cx.spawn(async move |this, cx| { - let response = request.await?; - match response.entry { - Some(entry) => this - .update(cx, |worktree, cx| { - worktree.as_remote_mut().unwrap().insert_entry( - entry, - response.worktree_scan_id as usize, - cx, - ) - })? - .await - .map(CreatedEntry::Included), - None => { - let abs_path = - this.read_with(cx, |worktree, _| worktree.absolutize(&path))?; - Ok(CreatedEntry::Excluded { abs_path }) - } - } - }) - } - } - } - - pub fn delete_entry( - &mut self, - entry_id: ProjectEntryId, - trash: bool, - cx: &mut Context, - ) -> Option>> { - let task = match self { - Worktree::Local(this) => this.delete_entry(entry_id, trash, cx), - Worktree::Remote(this) => this.delete_entry(entry_id, trash, cx), - }?; - - let entry = match &*self { - Worktree::Local(this) => this.entry_for_id(entry_id), - Worktree::Remote(this) => this.entry_for_id(entry_id), - }?; - - let mut ids = vec![entry_id]; - let path = &*entry.path; - - self.get_children_ids_recursive(path, &mut ids); - - for id in ids { - cx.emit(Event::DeletedEntry(id)); - } - Some(task) - } - - fn get_children_ids_recursive(&self, path: &RelPath, ids: &mut Vec) { - let children_iter = self.child_entries(path); - for child in children_iter { - ids.push(child.id); - self.get_children_ids_recursive(&child.path, ids); - } - } - - // pub fn rename_entry( - // &mut self, - // entry_id: ProjectEntryId, - // new_path: Arc, - // cx: &Context, - // ) -> Task> { - // match self { - // Worktree::Local(this) => this.rename_entry(entry_id, new_path, cx), - // Worktree::Remote(this) => this.rename_entry(entry_id, new_path, cx), - // } - // } - - pub fn copy_external_entries( - &mut self, - target_directory: Arc, - paths: Vec>, - fs: Arc, - cx: &Context, - ) -> Task>> { - match self { - Worktree::Local(this) => this.copy_external_entries(target_directory, paths, cx), - Worktree::Remote(this) => this.copy_external_entries(target_directory, paths, fs, cx), - } - } - - pub fn expand_entry( - &mut self, - entry_id: ProjectEntryId, - cx: &Context, - ) -> Option>> { - match self { - Worktree::Local(this) => this.expand_entry(entry_id, cx), - Worktree::Remote(this) => { - let response = this.client.request(proto::ExpandProjectEntry { - project_id: this.project_id, - entry_id: entry_id.to_proto(), - }); - Some(cx.spawn(async move |this, cx| { - let response = response.await?; - this.update(cx, |this, _| { - this.as_remote_mut() - .unwrap() - .wait_for_snapshot(response.worktree_scan_id as usize) - })? - .await?; - Ok(()) - })) - } - } - } - - pub fn expand_all_for_entry( - &mut self, - entry_id: ProjectEntryId, - cx: &Context, - ) -> Option>> { - match self { - Worktree::Local(this) => this.expand_all_for_entry(entry_id, cx), - Worktree::Remote(this) => { - let response = this.client.request(proto::ExpandAllForProjectEntry { - project_id: this.project_id, - entry_id: entry_id.to_proto(), - }); - Some(cx.spawn(async move |this, cx| { - let response = response.await?; - this.update(cx, |this, _| { - this.as_remote_mut() - .unwrap() - .wait_for_snapshot(response.worktree_scan_id as usize) - })? - .await?; - Ok(()) - })) - } - } - } - - pub async fn handle_create_entry( - this: Entity, - request: proto::CreateProjectEntry, - mut cx: AsyncApp, - ) -> Result { - let (scan_id, entry) = this.update(&mut cx, |this, cx| { - anyhow::Ok(( - this.scan_id(), - this.create_entry( - RelPath::from_proto(&request.path).with_context(|| { - format!("received invalid relative path {:?}", request.path) - })?, - request.is_directory, - request.content, - cx, - ), - )) - })??; - Ok(proto::ProjectEntryResponse { - entry: match &entry.await? { - CreatedEntry::Included(entry) => Some(entry.into()), - CreatedEntry::Excluded { .. } => None, - }, - worktree_scan_id: scan_id as u64, - }) - } - - pub async fn handle_delete_entry( - this: Entity, - request: proto::DeleteProjectEntry, - mut cx: AsyncApp, - ) -> Result { - let (scan_id, task) = this.update(&mut cx, |this, cx| { - ( - this.scan_id(), - this.delete_entry( - ProjectEntryId::from_proto(request.entry_id), - request.use_trash, - cx, - ), - ) - })?; - task.context("invalid entry")?.await?; - Ok(proto::ProjectEntryResponse { - entry: None, - worktree_scan_id: scan_id as u64, - }) - } - - pub async fn handle_expand_entry( - this: Entity, - request: proto::ExpandProjectEntry, - mut cx: AsyncApp, - ) -> Result { - let task = this.update(&mut cx, |this, cx| { - this.expand_entry(ProjectEntryId::from_proto(request.entry_id), cx) - })?; - task.context("no such entry")?.await?; - let scan_id = this.read_with(&cx, |this, _| this.scan_id())?; - Ok(proto::ExpandProjectEntryResponse { - worktree_scan_id: scan_id as u64, - }) - } - - pub async fn handle_expand_all_for_entry( - this: Entity, - request: proto::ExpandAllForProjectEntry, - mut cx: AsyncApp, - ) -> Result { - let task = this.update(&mut cx, |this, cx| { - this.expand_all_for_entry(ProjectEntryId::from_proto(request.entry_id), cx) - })?; - task.context("no such entry")?.await?; - let scan_id = this.read_with(&cx, |this, _| this.scan_id())?; - Ok(proto::ExpandAllForProjectEntryResponse { - worktree_scan_id: scan_id as u64, - }) - } - - pub fn is_single_file(&self) -> bool { - self.root_dir().is_none() - } - - /// For visible worktrees, returns the path with the worktree name as the first component. - /// Otherwise, returns an absolute path. - pub fn full_path(&self, worktree_relative_path: &RelPath) -> PathBuf { - if self.is_visible() { - self.root_name() - .join(worktree_relative_path) - .display(self.path_style) - .to_string() - .into() - } else { - let full_path = self.abs_path(); - let mut full_path_string = if self.is_local() - && let Ok(stripped) = full_path.strip_prefix(home_dir()) - { - self.path_style - .join("~", &*stripped.to_string_lossy()) - .unwrap() - } else { - full_path.to_string_lossy().into_owned() - }; - - if worktree_relative_path.components().next().is_some() { - full_path_string.push_str(self.path_style.primary_separator()); - full_path_string.push_str(&worktree_relative_path.display(self.path_style)); - } - - full_path_string.into() - } - } -} - -impl LocalWorktree { - pub fn fs(&self) -> &Arc { - &self.fs - } - - pub fn is_path_private(&self, path: &RelPath) -> bool { - !self.share_private_files && self.settings.is_path_private(path) - } - - pub fn fs_is_case_sensitive(&self) -> bool { - self.fs_case_sensitive - } - - fn restart_background_scanners(&mut self, cx: &Context) { - let (scan_requests_tx, scan_requests_rx) = channel::unbounded(); - let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded(); - self.scan_requests_tx = scan_requests_tx; - self.path_prefixes_to_scan_tx = path_prefixes_to_scan_tx; - - self.start_background_scanner(scan_requests_rx, path_prefixes_to_scan_rx, cx); - let always_included_entries = mem::take(&mut self.snapshot.always_included_entries); - log::debug!( - "refreshing entries for the following always included paths: {:?}", - always_included_entries - ); - - // Cleans up old always included entries to ensure they get updated properly. Otherwise, - // nested always included entries may not get updated and will result in out-of-date info. - self.refresh_entries_for_paths(always_included_entries); - } - - fn start_background_scanner( - &mut self, - scan_requests_rx: channel::Receiver, - path_prefixes_to_scan_rx: channel::Receiver, - cx: &Context, - ) { - let snapshot = self.snapshot(); - let share_private_files = self.share_private_files; - let next_entry_id = self.next_entry_id.clone(); - let fs = self.fs.clone(); - let scanning_enabled = self.scanning_enabled; - let settings = self.settings.clone(); - let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded(); - let background_scanner = cx.background_spawn({ - let abs_path = snapshot.abs_path.as_path().to_path_buf(); - let background = cx.background_executor().clone(); - async move { - let (events, watcher) = if scanning_enabled { - fs.watch(&abs_path, FS_WATCH_LATENCY).await - } else { - (Box::pin(stream::pending()) as _, Arc::new(NullWatcher) as _) - }; - let fs_case_sensitive = fs.is_case_sensitive().await.unwrap_or_else(|e| { - log::error!("Failed to determine whether filesystem is case sensitive: {e:#}"); - true - }); - - let mut scanner = BackgroundScanner { - fs, - fs_case_sensitive, - status_updates_tx: scan_states_tx, - executor: background, - scan_requests_rx, - path_prefixes_to_scan_rx, - next_entry_id, - state: async_lock::Mutex::new(BackgroundScannerState { - prev_snapshot: snapshot.snapshot.clone(), - snapshot, - scanned_dirs: Default::default(), - path_prefixes_to_scan: Default::default(), - paths_to_scan: Default::default(), - removed_entries: Default::default(), - changed_paths: Default::default(), - }), - phase: BackgroundScannerPhase::InitialScan, - share_private_files, - scanning_enabled, - settings, - watcher, - }; - - scanner - .run(Box::pin(events.map(|events| events.into_iter().collect()))) - .await; - } - }); - let scan_state_updater = cx.spawn(async move |this, cx| { - while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade()) { - this.update(cx, |this, cx| { - let this = this.as_local_mut().unwrap(); - match state { - ScanState::Started => { - *this.is_scanning.0.borrow_mut() = true; - } - ScanState::Updated { - snapshot, - changes, - barrier, - scanning, - } => { - *this.is_scanning.0.borrow_mut() = scanning; - this.set_snapshot(snapshot, changes, cx); - drop(barrier); - } - ScanState::RootUpdated { new_path } => { - this.update_abs_path_and_refresh(new_path, cx); - } - } - }) - .ok(); - } - }); - self._background_scanner_tasks = vec![background_scanner, scan_state_updater]; - *self.is_scanning.0.borrow_mut() = true; - } - - fn set_snapshot( - &mut self, - mut new_snapshot: LocalSnapshot, - entry_changes: UpdatedEntriesSet, - cx: &mut Context, - ) { - let repo_changes = self.changed_repos(&self.snapshot, &mut new_snapshot); - self.snapshot = new_snapshot; - - if let Some(share) = self.update_observer.as_mut() { - share - .snapshots_tx - .unbounded_send((self.snapshot.clone(), entry_changes.clone())) - .ok(); - } - - if !entry_changes.is_empty() { - cx.emit(Event::UpdatedEntries(entry_changes)); - } - if !repo_changes.is_empty() { - cx.emit(Event::UpdatedGitRepositories(repo_changes)); - } - } - - fn changed_repos( - &self, - old_snapshot: &LocalSnapshot, - new_snapshot: &mut LocalSnapshot, - ) -> UpdatedGitRepositoriesSet { - let mut changes = Vec::new(); - let mut old_repos = old_snapshot.git_repositories.iter().peekable(); - let new_repos = new_snapshot.git_repositories.clone(); - let mut new_repos = new_repos.iter().peekable(); - - loop { - match (new_repos.peek().map(clone), old_repos.peek().map(clone)) { - (Some((new_entry_id, new_repo)), Some((old_entry_id, old_repo))) => { - match Ord::cmp(&new_entry_id, &old_entry_id) { - Ordering::Less => { - changes.push(UpdatedGitRepository { - work_directory_id: new_entry_id, - old_work_directory_abs_path: None, - new_work_directory_abs_path: Some( - new_repo.work_directory_abs_path.clone(), - ), - dot_git_abs_path: Some(new_repo.dot_git_abs_path.clone()), - repository_dir_abs_path: Some( - new_repo.repository_dir_abs_path.clone(), - ), - common_dir_abs_path: Some(new_repo.common_dir_abs_path.clone()), - }); - new_repos.next(); - } - Ordering::Equal => { - if new_repo.git_dir_scan_id != old_repo.git_dir_scan_id - || new_repo.work_directory_abs_path - != old_repo.work_directory_abs_path - { - changes.push(UpdatedGitRepository { - work_directory_id: new_entry_id, - old_work_directory_abs_path: Some( - old_repo.work_directory_abs_path.clone(), - ), - new_work_directory_abs_path: Some( - new_repo.work_directory_abs_path.clone(), - ), - dot_git_abs_path: Some(new_repo.dot_git_abs_path.clone()), - repository_dir_abs_path: Some( - new_repo.repository_dir_abs_path.clone(), - ), - common_dir_abs_path: Some(new_repo.common_dir_abs_path.clone()), - }); - } - new_repos.next(); - old_repos.next(); - } - Ordering::Greater => { - changes.push(UpdatedGitRepository { - work_directory_id: old_entry_id, - old_work_directory_abs_path: Some( - old_repo.work_directory_abs_path.clone(), - ), - new_work_directory_abs_path: None, - dot_git_abs_path: None, - repository_dir_abs_path: None, - common_dir_abs_path: None, - }); - old_repos.next(); - } - } - } - (Some((entry_id, repo)), None) => { - changes.push(UpdatedGitRepository { - work_directory_id: entry_id, - old_work_directory_abs_path: None, - new_work_directory_abs_path: Some(repo.work_directory_abs_path.clone()), - dot_git_abs_path: Some(repo.dot_git_abs_path.clone()), - repository_dir_abs_path: Some(repo.repository_dir_abs_path.clone()), - common_dir_abs_path: Some(repo.common_dir_abs_path.clone()), - }); - new_repos.next(); - } - (None, Some((entry_id, repo))) => { - changes.push(UpdatedGitRepository { - work_directory_id: entry_id, - old_work_directory_abs_path: Some(repo.work_directory_abs_path.clone()), - new_work_directory_abs_path: None, - dot_git_abs_path: Some(repo.dot_git_abs_path.clone()), - repository_dir_abs_path: Some(repo.repository_dir_abs_path.clone()), - common_dir_abs_path: Some(repo.common_dir_abs_path.clone()), - }); - old_repos.next(); - } - (None, None) => break, - } - } - - fn clone(value: &(&T, &U)) -> (T, U) { - (value.0.clone(), value.1.clone()) - } - - changes.into() - } - - pub fn scan_complete(&self) -> impl Future + use<> { - let mut is_scanning_rx = self.is_scanning.1.clone(); - async move { - let mut is_scanning = *is_scanning_rx.borrow(); - while is_scanning { - if let Some(value) = is_scanning_rx.recv().await { - is_scanning = value; - } else { - break; - } - } - } - } - - pub fn snapshot(&self) -> LocalSnapshot { - self.snapshot.clone() - } - - pub fn settings(&self) -> WorktreeSettings { - self.settings.clone() - } - - fn load_binary_file( - &self, - path: &RelPath, - cx: &Context, - ) -> Task> { - let path = Arc::from(path); - let abs_path = self.absolutize(&path); - let fs = self.fs.clone(); - let entry = self.refresh_entry(path.clone(), None, cx); - let is_private = self.is_path_private(&path); - - let worktree = cx.weak_entity(); - cx.background_spawn(async move { - let content = fs.load_bytes(&abs_path).await?; - - let worktree = worktree.upgrade().context("worktree was dropped")?; - let file = match entry.await? { - Some(entry) => File::for_entry(entry, worktree), - None => { - let metadata = fs - .metadata(&abs_path) - .await - .with_context(|| { - format!("Loading metadata for excluded file {abs_path:?}") - })? - .with_context(|| { - format!("Excluded file {abs_path:?} got removed during loading") - })?; - Arc::new(File { - entry_id: None, - worktree, - path, - disk_state: DiskState::Present { - mtime: metadata.mtime, - }, - is_local: true, - is_private, - }) - } - }; - - Ok(LoadedBinaryFile { file, content }) - }) - } - - fn load_file(&self, path: &RelPath, cx: &Context) -> Task> { - let path = Arc::from(path); - let abs_path = self.absolutize(&path); - let fs = self.fs.clone(); - let entry = self.refresh_entry(path.clone(), None, cx); - let is_private = self.is_path_private(path.as_ref()); - - let this = cx.weak_entity(); - cx.background_spawn(async move { - // WARN: Temporary workaround for #27283. - // We are not efficient with our memory usage per file, and use in excess of 64GB for a 10GB file - // Therefore, as a temporary workaround to prevent system freezes, we just bail before opening a file - // if it is too large - // 5GB seems to be more reasonable, peaking at ~16GB, while 6GB jumps up to >24GB which seems like a - // reasonable limit - { - const FILE_SIZE_MAX: u64 = 6 * 1024 * 1024 * 1024; // 6GB - if let Ok(Some(metadata)) = fs.metadata(&abs_path).await - && metadata.len >= FILE_SIZE_MAX - { - anyhow::bail!("File is too large to load"); - } - } - let text = fs.load(&abs_path).await?; - - let worktree = this.upgrade().context("worktree was dropped")?; - let file = match entry.await? { - Some(entry) => File::for_entry(entry, worktree), - None => { - let metadata = fs - .metadata(&abs_path) - .await - .with_context(|| { - format!("Loading metadata for excluded file {abs_path:?}") - })? - .with_context(|| { - format!("Excluded file {abs_path:?} got removed during loading") - })?; - Arc::new(File { - entry_id: None, - worktree, - path, - disk_state: DiskState::Present { - mtime: metadata.mtime, - }, - is_local: true, - is_private, - }) - } - }; - - Ok(LoadedFile { file, text }) - }) - } - - /// Find the lowest path in the worktree's datastructures that is an ancestor - fn lowest_ancestor(&self, path: &RelPath) -> Arc { - let mut lowest_ancestor = None; - for path in path.ancestors() { - if self.entry_for_path(path).is_some() { - lowest_ancestor = Some(path.into()); - break; - } - } - - lowest_ancestor.unwrap_or_else(|| RelPath::empty().into()) - } - - fn create_entry( - &self, - path: Arc, - is_dir: bool, - content: Option>, - cx: &Context, - ) -> Task> { - let abs_path = self.absolutize(&path); - let path_excluded = self.settings.is_path_excluded(&path); - let fs = self.fs.clone(); - let task_abs_path = abs_path.clone(); - let write = cx.background_spawn(async move { - if is_dir { - fs.create_dir(&task_abs_path) - .await - .with_context(|| format!("creating directory {task_abs_path:?}")) - } else { - fs.write(&task_abs_path, content.as_deref().unwrap_or(&[])) - .await - .with_context(|| format!("creating file {task_abs_path:?}")) - } - }); - - let lowest_ancestor = self.lowest_ancestor(&path); - cx.spawn(async move |this, cx| { - write.await?; - if path_excluded { - return Ok(CreatedEntry::Excluded { abs_path }); - } - - let (result, refreshes) = this.update(cx, |this, cx| { - let mut refreshes = Vec::new(); - let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap(); - for refresh_path in refresh_paths.ancestors() { - if refresh_path == RelPath::empty() { - continue; - } - let refresh_full_path = lowest_ancestor.join(refresh_path); - - refreshes.push(this.as_local_mut().unwrap().refresh_entry( - refresh_full_path, - None, - cx, - )); - } - ( - this.as_local_mut().unwrap().refresh_entry(path, None, cx), - refreshes, - ) - })?; - for refresh in refreshes { - refresh.await.log_err(); - } - - Ok(result - .await? - .map(CreatedEntry::Included) - .unwrap_or_else(|| CreatedEntry::Excluded { abs_path })) - }) - } - - fn write_file( - &self, - path: Arc, - text: Rope, - line_ending: LineEnding, - cx: &Context, - ) -> Task>> { - let fs = self.fs.clone(); - let is_private = self.is_path_private(&path); - let abs_path = self.absolutize(&path); - - let write = cx.background_spawn({ - let fs = fs.clone(); - let abs_path = abs_path.clone(); - async move { fs.save(&abs_path, &text, line_ending).await } - }); - - cx.spawn(async move |this, cx| { - write.await?; - let entry = this - .update(cx, |this, cx| { - this.as_local_mut() - .unwrap() - .refresh_entry(path.clone(), None, cx) - })? - .await?; - let worktree = this.upgrade().context("worktree dropped")?; - if let Some(entry) = entry { - Ok(File::for_entry(entry, worktree)) - } else { - let metadata = fs - .metadata(&abs_path) - .await - .with_context(|| { - format!("Fetching metadata after saving the excluded buffer {abs_path:?}") - })? - .with_context(|| { - format!("Excluded buffer {path:?} got removed during saving") - })?; - Ok(Arc::new(File { - worktree, - path, - disk_state: DiskState::Present { - mtime: metadata.mtime, - }, - entry_id: None, - is_local: true, - is_private, - })) - } - }) - } - - fn delete_entry( - &self, - entry_id: ProjectEntryId, - trash: bool, - cx: &Context, - ) -> Option>> { - let entry = self.entry_for_id(entry_id)?.clone(); - let abs_path = self.absolutize(&entry.path); - let fs = self.fs.clone(); - - let delete = cx.background_spawn(async move { - if entry.is_file() { - if trash { - fs.trash_file(&abs_path, Default::default()).await?; - } else { - fs.remove_file(&abs_path, Default::default()).await?; - } - } else if trash { - fs.trash_dir( - &abs_path, - RemoveOptions { - recursive: true, - ignore_if_not_exists: false, - }, - ) - .await?; - } else { - fs.remove_dir( - &abs_path, - RemoveOptions { - recursive: true, - ignore_if_not_exists: false, - }, - ) - .await?; - } - anyhow::Ok(entry.path) - }); - - Some(cx.spawn(async move |this, cx| { - let path = delete.await?; - this.update(cx, |this, _| { - this.as_local_mut() - .unwrap() - .refresh_entries_for_paths(vec![path]) - })? - .recv() - .await; - Ok(()) - })) - } - - pub fn copy_external_entries( - &self, - target_directory: Arc, - paths: Vec>, - cx: &Context, - ) -> Task>> { - let target_directory = self.absolutize(&target_directory); - let worktree_path = self.abs_path().clone(); - let fs = self.fs.clone(); - let paths = paths - .into_iter() - .filter_map(|source| { - let file_name = source.file_name()?; - let mut target = target_directory.clone(); - target.push(file_name); - - // Do not allow copying the same file to itself. - if source.as_ref() != target.as_path() { - Some((source, target)) - } else { - None - } - }) - .collect::>(); - - let paths_to_refresh = paths - .iter() - .filter_map(|(_, target)| { - RelPath::new( - target.strip_prefix(&worktree_path).ok()?, - PathStyle::local(), - ) - .ok() - .map(|path| path.into_arc()) - }) - .collect::>(); - - cx.spawn(async move |this, cx| { - cx.background_spawn(async move { - for (source, target) in paths { - copy_recursive( - fs.as_ref(), - &source, - &target, - fs::CopyOptions { - overwrite: true, - ..Default::default() - }, - ) - .await - .with_context(|| { - format!("Failed to copy file from {source:?} to {target:?}") - })?; - } - anyhow::Ok(()) - }) - .await - .log_err(); - let mut refresh = cx.read_entity( - &this.upgrade().with_context(|| "Dropped worktree")?, - |this, _| { - anyhow::Ok::( - this.as_local() - .with_context(|| "Worktree is not local")? - .refresh_entries_for_paths(paths_to_refresh.clone()), - ) - }, - )??; - - cx.background_spawn(async move { - refresh.next().await; - anyhow::Ok(()) - }) - .await - .log_err(); - - let this = this.upgrade().with_context(|| "Dropped worktree")?; - cx.read_entity(&this, |this, _| { - paths_to_refresh - .iter() - .filter_map(|path| Some(this.entry_for_path(path)?.id)) - .collect() - }) - }) - } - - fn expand_entry( - &self, - entry_id: ProjectEntryId, - cx: &Context, - ) -> Option>> { - let path = self.entry_for_id(entry_id)?.path.clone(); - let mut refresh = self.refresh_entries_for_paths(vec![path]); - Some(cx.background_spawn(async move { - refresh.next().await; - Ok(()) - })) - } - - fn expand_all_for_entry( - &self, - entry_id: ProjectEntryId, - cx: &Context, - ) -> Option>> { - let path = self.entry_for_id(entry_id).unwrap().path.clone(); - let mut rx = self.add_path_prefix_to_scan(path); - Some(cx.background_spawn(async move { - rx.next().await; - Ok(()) - })) - } - - pub fn refresh_entries_for_paths(&self, paths: Vec>) -> barrier::Receiver { - let (tx, rx) = barrier::channel(); - self.scan_requests_tx - .try_send(ScanRequest { - relative_paths: paths, - done: smallvec![tx], - }) - .ok(); - rx - } - - #[cfg(feature = "test-support")] - pub fn manually_refresh_entries_for_paths( - &self, - paths: Vec>, - ) -> barrier::Receiver { - self.refresh_entries_for_paths(paths) - } - - pub fn add_path_prefix_to_scan(&self, path_prefix: Arc) -> barrier::Receiver { - let (tx, rx) = barrier::channel(); - self.path_prefixes_to_scan_tx - .try_send(PathPrefixScanRequest { - path: path_prefix, - done: smallvec![tx], - }) - .ok(); - rx - } - - pub fn refresh_entry( - &self, - path: Arc, - old_path: Option>, - cx: &Context, - ) -> Task>> { - if self.settings.is_path_excluded(&path) { - return Task::ready(Ok(None)); - } - let paths = if let Some(old_path) = old_path.as_ref() { - vec![old_path.clone(), path.clone()] - } else { - vec![path.clone()] - }; - let t0 = Instant::now(); - let mut refresh = self.refresh_entries_for_paths(paths); - // todo(lw): Hot foreground spawn - cx.spawn(async move |this, cx| { - refresh.recv().await; - log::trace!("refreshed entry {path:?} in {:?}", t0.elapsed()); - let new_entry = this.read_with(cx, |this, _| { - this.entry_for_path(&path).cloned().with_context(|| { - format!("Could not find entry in worktree for {path:?} after refresh") - }) - })??; - Ok(Some(new_entry)) - }) - } - - fn observe_updates(&mut self, project_id: u64, cx: &Context, callback: F) - where - F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut, - Fut: 'static + Send + Future, - { - if let Some(observer) = self.update_observer.as_mut() { - *observer.resume_updates.borrow_mut() = (); - return; - } - - let (resume_updates_tx, mut resume_updates_rx) = watch::channel::<()>(); - let (snapshots_tx, mut snapshots_rx) = - mpsc::unbounded::<(LocalSnapshot, UpdatedEntriesSet)>(); - snapshots_tx - .unbounded_send((self.snapshot(), Arc::default())) - .ok(); - - let worktree_id = cx.entity_id().as_u64(); - let _maintain_remote_snapshot = cx.background_spawn(async move { - let mut is_first = true; - while let Some((snapshot, entry_changes)) = snapshots_rx.next().await { - let update = if is_first { - is_first = false; - snapshot.build_initial_update(project_id, worktree_id) - } else { - snapshot.build_update(project_id, worktree_id, entry_changes) - }; - - for update in proto::split_worktree_update(update) { - let _ = resume_updates_rx.try_recv(); - loop { - let result = callback(update.clone()); - if result.await { - break; - } else { - log::info!("waiting to resume updates"); - if resume_updates_rx.next().await.is_none() { - return Some(()); - } - } - } - } - } - Some(()) - }); - - self.update_observer = Some(UpdateObservationState { - snapshots_tx, - resume_updates: resume_updates_tx, - _maintain_remote_snapshot, - }); - } - - pub fn share_private_files(&mut self, cx: &Context) { - self.share_private_files = true; - self.restart_background_scanners(cx); - } - - pub fn update_abs_path_and_refresh( - &mut self, - new_path: Arc, - cx: &Context, - ) { - self.snapshot.git_repositories = Default::default(); - self.snapshot.ignores_by_parent_abs_path = Default::default(); - let root_name = new_path - .as_path() - .file_name() - .and_then(|f| f.to_str()) - .map_or(RelPath::empty().into(), |f| { - RelPath::unix(f).unwrap().into() - }); - self.snapshot.update_abs_path(new_path, root_name); - self.restart_background_scanners(cx); - } -} - -impl RemoteWorktree { - pub fn project_id(&self) -> u64 { - self.project_id - } - - pub fn client(&self) -> AnyProtoClient { - self.client.clone() - } - - pub fn disconnected_from_host(&mut self) { - self.updates_tx.take(); - self.snapshot_subscriptions.clear(); - self.disconnected = true; - } - - pub fn update_from_remote(&self, update: proto::UpdateWorktree) { - if let Some(updates_tx) = &self.updates_tx { - updates_tx - .unbounded_send(update) - .expect("consumer runs to completion"); - } - } - - fn observe_updates(&mut self, project_id: u64, cx: &Context, callback: F) - where - F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut, - Fut: 'static + Send + Future, - { - let (tx, mut rx) = mpsc::unbounded(); - let initial_update = self - .snapshot - .build_initial_update(project_id, self.id().to_proto()); - self.update_observer = Some(tx); - cx.spawn(async move |this, cx| { - let mut update = initial_update; - 'outer: loop { - // SSH projects use a special project ID of 0, and we need to - // remap it to the correct one here. - update.project_id = project_id; - - for chunk in split_worktree_update(update) { - if !callback(chunk).await { - break 'outer; - } - } - - if let Some(next_update) = rx.next().await { - update = next_update; - } else { - break; - } - } - this.update(cx, |this, _| { - let this = this.as_remote_mut().unwrap(); - this.update_observer.take(); - }) - }) - .detach(); - } - - fn observed_snapshot(&self, scan_id: usize) -> bool { - self.completed_scan_id >= scan_id - } - - pub fn wait_for_snapshot( - &mut self, - scan_id: usize, - ) -> impl Future> + use<> { - let (tx, rx) = oneshot::channel(); - if self.observed_snapshot(scan_id) { - let _ = tx.send(()); - } else if self.disconnected { - drop(tx); - } else { - match self - .snapshot_subscriptions - .binary_search_by_key(&scan_id, |probe| probe.0) - { - Ok(ix) | Err(ix) => self.snapshot_subscriptions.insert(ix, (scan_id, tx)), - } - } - - async move { - rx.await?; - Ok(()) - } - } - - pub fn insert_entry( - &mut self, - entry: proto::Entry, - scan_id: usize, - cx: &Context, - ) -> Task> { - let wait_for_snapshot = self.wait_for_snapshot(scan_id); - cx.spawn(async move |this, cx| { - wait_for_snapshot.await?; - this.update(cx, |worktree, _| { - let worktree = worktree.as_remote_mut().unwrap(); - let snapshot = &mut worktree.background_snapshot.lock().0; - let entry = snapshot.insert_entry(entry, &worktree.file_scan_inclusions); - worktree.snapshot = snapshot.clone(); - entry - })? - }) - } - - fn delete_entry( - &self, - entry_id: ProjectEntryId, - trash: bool, - cx: &Context, - ) -> Option>> { - let response = self.client.request(proto::DeleteProjectEntry { - project_id: self.project_id, - entry_id: entry_id.to_proto(), - use_trash: trash, - }); - Some(cx.spawn(async move |this, cx| { - let response = response.await?; - let scan_id = response.worktree_scan_id as usize; - - this.update(cx, move |this, _| { - this.as_remote_mut().unwrap().wait_for_snapshot(scan_id) - })? - .await?; - - this.update(cx, |this, _| { - let this = this.as_remote_mut().unwrap(); - let snapshot = &mut this.background_snapshot.lock().0; - snapshot.delete_entry(entry_id); - this.snapshot = snapshot.clone(); - }) - })) - } - - // fn rename_entry( - // &self, - // entry_id: ProjectEntryId, - // new_path: impl Into>, - // cx: &Context, - // ) -> Task> { - // let new_path: Arc = new_path.into(); - // let response = self.client.request(proto::RenameProjectEntry { - // project_id: self.project_id, - // entry_id: entry_id.to_proto(), - // new_worktree_id: new_path.worktree_id, - // new_path: new_path.as_ref().to_proto(), - // }); - // cx.spawn(async move |this, cx| { - // let response = response.await?; - // match response.entry { - // Some(entry) => this - // .update(cx, |this, cx| { - // this.as_remote_mut().unwrap().insert_entry( - // entry, - // response.worktree_scan_id as usize, - // cx, - // ) - // })? - // .await - // .map(CreatedEntry::Included), - // None => { - // let abs_path = - // this.read_with(cx, |worktree, _| worktree.absolutize(&new_path))?; - // Ok(CreatedEntry::Excluded { abs_path }) - // } - // } - // }) - // } - - fn copy_external_entries( - &self, - target_directory: Arc, - paths_to_copy: Vec>, - local_fs: Arc, - cx: &Context, - ) -> Task>> { - let client = self.client.clone(); - let worktree_id = self.id().to_proto(); - let project_id = self.project_id; - - cx.background_spawn(async move { - let mut requests = Vec::new(); - for root_path_to_copy in paths_to_copy { - let Some(filename) = root_path_to_copy - .file_name() - .and_then(|name| name.to_str()) - .and_then(|filename| RelPath::unix(filename).ok()) - else { - continue; - }; - for (abs_path, is_directory) in - read_dir_items(local_fs.as_ref(), &root_path_to_copy).await? - { - let Some(relative_path) = abs_path - .strip_prefix(&root_path_to_copy) - .map_err(|e| anyhow::Error::from(e)) - .and_then(|relative_path| RelPath::new(relative_path, PathStyle::local())) - .log_err() - else { - continue; - }; - let content = if is_directory { - None - } else { - Some(local_fs.load_bytes(&abs_path).await?) - }; - - let mut target_path = target_directory.join(filename); - if relative_path.file_name().is_some() { - target_path = target_path.join(&relative_path); - } - - requests.push(proto::CreateProjectEntry { - project_id, - worktree_id, - path: target_path.to_proto(), - is_directory, - content, - }); - } - } - requests.sort_unstable_by(|a, b| a.path.cmp(&b.path)); - requests.dedup(); - - let mut copied_entry_ids = Vec::new(); - for request in requests { - let response = client.request(request).await?; - copied_entry_ids.extend(response.entry.map(|e| ProjectEntryId::from_proto(e.id))); - } - - Ok(copied_entry_ids) - }) - } -} - -impl Snapshot { - pub fn new( - id: u64, - root_name: Arc, - abs_path: Arc, - path_style: PathStyle, - ) -> Self { - Snapshot { - id: WorktreeId::from_usize(id as usize), - abs_path: SanitizedPath::from_arc(abs_path), - path_style, - root_char_bag: root_name - .as_unix_str() - .chars() - .map(|c| c.to_ascii_lowercase()) - .collect(), - root_name, - always_included_entries: Default::default(), - entries_by_path: Default::default(), - entries_by_id: Default::default(), - scan_id: 1, - completed_scan_id: 0, - } - } - - pub fn id(&self) -> WorktreeId { - self.id - } - - // TODO: - // Consider the following: - // - // ```rust - // let abs_path: Arc = snapshot.abs_path(); // e.g. "C:\Users\user\Desktop\project" - // let some_non_trimmed_path = Path::new("\\\\?\\C:\\Users\\user\\Desktop\\project\\main.rs"); - // // The caller perform some actions here: - // some_non_trimmed_path.strip_prefix(abs_path); // This fails - // some_non_trimmed_path.starts_with(abs_path); // This fails too - // ``` - // - // This is definitely a bug, but it's not clear if we should handle it here or not. - pub fn abs_path(&self) -> &Arc { - SanitizedPath::cast_arc_ref(&self.abs_path) - } - - fn build_initial_update(&self, project_id: u64, worktree_id: u64) -> proto::UpdateWorktree { - let mut updated_entries = self - .entries_by_path - .iter() - .map(proto::Entry::from) - .collect::>(); - updated_entries.sort_unstable_by_key(|e| e.id); - - proto::UpdateWorktree { - project_id, - worktree_id, - abs_path: self.abs_path().to_string_lossy().into_owned(), - root_name: self.root_name().to_proto(), - updated_entries, - removed_entries: Vec::new(), - scan_id: self.scan_id as u64, - is_last_update: self.completed_scan_id == self.scan_id, - // Sent in separate messages. - updated_repositories: Vec::new(), - removed_repositories: Vec::new(), - } - } - - pub fn work_directory_abs_path(&self, work_directory: &WorkDirectory) -> PathBuf { - match work_directory { - WorkDirectory::InProject { relative_path } => self.absolutize(relative_path), - WorkDirectory::AboveProject { absolute_path, .. } => absolute_path.as_ref().to_owned(), - } - } - - pub fn absolutize(&self, path: &RelPath) -> PathBuf { - if path.file_name().is_some() { - let mut abs_path = self.abs_path.to_string(); - for component in path.components() { - if !abs_path.ends_with(self.path_style.primary_separator()) { - abs_path.push_str(self.path_style.primary_separator()); - } - abs_path.push_str(component); - } - PathBuf::from(abs_path) - } else { - self.abs_path.as_path().to_path_buf() - } - } - - pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool { - self.entries_by_id.get(&entry_id, ()).is_some() - } - - fn insert_entry( - &mut self, - entry: proto::Entry, - always_included_paths: &PathMatcher, - ) -> Result { - let entry = Entry::try_from((&self.root_char_bag, always_included_paths, entry))?; - let old_entry = self.entries_by_id.insert_or_replace( - PathEntry { - id: entry.id, - path: entry.path.clone(), - is_ignored: entry.is_ignored, - scan_id: 0, - }, - (), - ); - if let Some(old_entry) = old_entry { - self.entries_by_path.remove(&PathKey(old_entry.path), ()); - } - self.entries_by_path.insert_or_replace(entry.clone(), ()); - Ok(entry) - } - - fn delete_entry(&mut self, entry_id: ProjectEntryId) -> Option> { - let removed_entry = self.entries_by_id.remove(&entry_id, ())?; - self.entries_by_path = { - let mut cursor = self.entries_by_path.cursor::(()); - let mut new_entries_by_path = - cursor.slice(&TraversalTarget::path(&removed_entry.path), Bias::Left); - while let Some(entry) = cursor.item() { - if entry.path.starts_with(&removed_entry.path) { - self.entries_by_id.remove(&entry.id, ()); - cursor.next(); - } else { - break; - } - } - new_entries_by_path.append(cursor.suffix(), ()); - new_entries_by_path - }; - - Some(removed_entry.path) - } - - fn update_abs_path(&mut self, abs_path: Arc, root_name: Arc) { - self.abs_path = abs_path; - if root_name != self.root_name { - self.root_char_bag = root_name - .as_unix_str() - .chars() - .map(|c| c.to_ascii_lowercase()) - .collect(); - self.root_name = root_name; - } - } - - fn apply_remote_update( - &mut self, - update: proto::UpdateWorktree, - always_included_paths: &PathMatcher, - ) { - log::debug!( - "applying remote worktree update. {} entries updated, {} removed", - update.updated_entries.len(), - update.removed_entries.len() - ); - if let Some(root_name) = RelPath::from_proto(&update.root_name).log_err() { - self.update_abs_path( - SanitizedPath::new_arc(&Path::new(&update.abs_path)), - root_name, - ); - } - - let mut entries_by_path_edits = Vec::new(); - let mut entries_by_id_edits = Vec::new(); - - for entry_id in update.removed_entries { - let entry_id = ProjectEntryId::from_proto(entry_id); - entries_by_id_edits.push(Edit::Remove(entry_id)); - if let Some(entry) = self.entry_for_id(entry_id) { - entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone()))); - } - } - - for entry in update.updated_entries { - let Some(entry) = - Entry::try_from((&self.root_char_bag, always_included_paths, entry)).log_err() - else { - continue; - }; - if let Some(PathEntry { path, .. }) = self.entries_by_id.get(&entry.id, ()) { - entries_by_path_edits.push(Edit::Remove(PathKey(path.clone()))); - } - if let Some(old_entry) = self.entries_by_path.get(&PathKey(entry.path.clone()), ()) - && old_entry.id != entry.id - { - entries_by_id_edits.push(Edit::Remove(old_entry.id)); - } - entries_by_id_edits.push(Edit::Insert(PathEntry { - id: entry.id, - path: entry.path.clone(), - is_ignored: entry.is_ignored, - scan_id: 0, - })); - entries_by_path_edits.push(Edit::Insert(entry)); - } - - self.entries_by_path.edit(entries_by_path_edits, ()); - self.entries_by_id.edit(entries_by_id_edits, ()); - - self.scan_id = update.scan_id as usize; - if update.is_last_update { - self.completed_scan_id = update.scan_id as usize; - } - } - - pub fn entry_count(&self) -> usize { - self.entries_by_path.summary().count - } - - pub fn visible_entry_count(&self) -> usize { - self.entries_by_path.summary().non_ignored_count - } - - pub fn dir_count(&self) -> usize { - let summary = self.entries_by_path.summary(); - summary.count - summary.file_count - } - - pub fn visible_dir_count(&self) -> usize { - let summary = self.entries_by_path.summary(); - summary.non_ignored_count - summary.non_ignored_file_count - } - - pub fn file_count(&self) -> usize { - self.entries_by_path.summary().file_count - } - - pub fn visible_file_count(&self) -> usize { - self.entries_by_path.summary().non_ignored_file_count - } - - fn traverse_from_offset( - &self, - include_files: bool, - include_dirs: bool, - include_ignored: bool, - start_offset: usize, - ) -> Traversal<'_> { - let mut cursor = self.entries_by_path.cursor(()); - cursor.seek( - &TraversalTarget::Count { - count: start_offset, - include_files, - include_dirs, - include_ignored, - }, - Bias::Right, - ); - Traversal { - snapshot: self, - cursor, - include_files, - include_dirs, - include_ignored, - } - } - - pub fn traverse_from_path( - &self, - include_files: bool, - include_dirs: bool, - include_ignored: bool, - path: &RelPath, - ) -> Traversal<'_> { - Traversal::new(self, include_files, include_dirs, include_ignored, path) - } - - pub fn files(&self, include_ignored: bool, start: usize) -> Traversal<'_> { - self.traverse_from_offset(true, false, include_ignored, start) - } - - pub fn directories(&self, include_ignored: bool, start: usize) -> Traversal<'_> { - self.traverse_from_offset(false, true, include_ignored, start) - } - - pub fn entries(&self, include_ignored: bool, start: usize) -> Traversal<'_> { - self.traverse_from_offset(true, true, include_ignored, start) - } - - pub fn paths(&self) -> impl Iterator { - self.entries_by_path - .cursor::<()>(()) - .filter(move |entry| !entry.path.is_empty()) - .map(|entry| entry.path.as_ref()) - } - - pub fn child_entries<'a>(&'a self, parent_path: &'a RelPath) -> ChildEntriesIter<'a> { - let options = ChildEntriesOptions { - include_files: true, - include_dirs: true, - include_ignored: true, - }; - self.child_entries_with_options(parent_path, options) - } - - pub fn child_entries_with_options<'a>( - &'a self, - parent_path: &'a RelPath, - options: ChildEntriesOptions, - ) -> ChildEntriesIter<'a> { - let mut cursor = self.entries_by_path.cursor(()); - cursor.seek(&TraversalTarget::path(parent_path), Bias::Right); - let traversal = Traversal { - snapshot: self, - cursor, - include_files: options.include_files, - include_dirs: options.include_dirs, - include_ignored: options.include_ignored, - }; - ChildEntriesIter { - traversal, - parent_path, - } - } - - pub fn root_entry(&self) -> Option<&Entry> { - self.entries_by_path.first() - } - - /// Returns `None` for a single file worktree, or `Some(self.abs_path())` if - /// it is a directory. - pub fn root_dir(&self) -> Option> { - self.root_entry() - .filter(|entry| entry.is_dir()) - .map(|_| self.abs_path().clone()) - } - - pub fn root_name(&self) -> &RelPath { - &self.root_name - } - - pub fn root_name_str(&self) -> &str { - self.root_name.as_unix_str() - } - - pub fn scan_id(&self) -> usize { - self.scan_id - } - - pub fn entry_for_path(&self, path: &RelPath) -> Option<&Entry> { - self.traverse_from_path(true, true, true, path) - .entry() - .and_then(|entry| { - if entry.path.as_ref() == path { - Some(entry) - } else { - None - } - }) - } - - /// Resolves a path to an executable using the following heuristics: - /// - /// 1. If the path starts with `~`, it is expanded to the user's home directory. - /// 2. If the path is relative and contains more than one component, - /// it is joined to the worktree root path. - /// 3. If the path is relative and exists in the worktree - /// (even if falls under an exclusion filter), - /// it is joined to the worktree root path. - /// 4. Otherwise the path is returned unmodified. - /// - /// Relative paths that do not exist in the worktree may - /// still be found using the `PATH` environment variable. - pub fn resolve_executable_path(&self, path: PathBuf) -> PathBuf { - if let Some(path_str) = path.to_str() { - if let Some(remaining_path) = path_str.strip_prefix("~/") { - return home_dir().join(remaining_path); - } else if path_str == "~" { - return home_dir().to_path_buf(); - } - } - - if let Ok(rel_path) = RelPath::new(&path, self.path_style) - && (path.components().count() > 1 || self.entry_for_path(&rel_path).is_some()) - { - self.abs_path().join(path) - } else { - path - } - } - - pub fn entry_for_id(&self, id: ProjectEntryId) -> Option<&Entry> { - let entry = self.entries_by_id.get(&id, ())?; - self.entry_for_path(&entry.path) - } - - pub fn path_style(&self) -> PathStyle { - self.path_style - } -} - -impl LocalSnapshot { - fn local_repo_for_work_directory_path(&self, path: &RelPath) -> Option<&LocalRepositoryEntry> { - self.git_repositories - .iter() - .map(|(_, entry)| entry) - .find(|entry| entry.work_directory.path_key() == PathKey(path.into())) - } - - fn build_update( - &self, - project_id: u64, - worktree_id: u64, - entry_changes: UpdatedEntriesSet, - ) -> proto::UpdateWorktree { - let mut updated_entries = Vec::new(); - let mut removed_entries = Vec::new(); - - for (_, entry_id, path_change) in entry_changes.iter() { - if let PathChange::Removed = path_change { - removed_entries.push(entry_id.0 as u64); - } else if let Some(entry) = self.entry_for_id(*entry_id) { - updated_entries.push(proto::Entry::from(entry)); - } - } - - removed_entries.sort_unstable(); - updated_entries.sort_unstable_by_key(|e| e.id); - - // TODO - optimize, knowing that removed_entries are sorted. - removed_entries.retain(|id| updated_entries.binary_search_by_key(id, |e| e.id).is_err()); - - proto::UpdateWorktree { - project_id, - worktree_id, - abs_path: self.abs_path().to_string_lossy().into_owned(), - root_name: self.root_name().to_proto(), - updated_entries, - removed_entries, - scan_id: self.scan_id as u64, - is_last_update: self.completed_scan_id == self.scan_id, - // Sent in separate messages. - updated_repositories: Vec::new(), - removed_repositories: Vec::new(), - } - } - - fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry { - log::trace!("insert entry {:?}", entry.path); - if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) { - let abs_path = self.absolutize(&entry.path); - match self.executor.block(build_gitignore(&abs_path, fs)) { - Ok(ignore) => { - self.ignores_by_parent_abs_path - .insert(abs_path.parent().unwrap().into(), (Arc::new(ignore), true)); - } - Err(error) => { - log::error!( - "error loading .gitignore file {:?} - {:?}", - &entry.path, - error - ); - } - } - } - - if entry.kind == EntryKind::PendingDir - && let Some(existing_entry) = self.entries_by_path.get(&PathKey(entry.path.clone()), ()) - { - entry.kind = existing_entry.kind; - } - - let scan_id = self.scan_id; - let removed = self.entries_by_path.insert_or_replace(entry.clone(), ()); - if let Some(removed) = removed - && removed.id != entry.id - { - self.entries_by_id.remove(&removed.id, ()); - } - self.entries_by_id.insert_or_replace( - PathEntry { - id: entry.id, - path: entry.path.clone(), - is_ignored: entry.is_ignored, - scan_id, - }, - (), - ); - - entry - } - - fn ancestor_inodes_for_path(&self, path: &RelPath) -> TreeSet { - let mut inodes = TreeSet::default(); - for ancestor in path.ancestors().skip(1) { - if let Some(entry) = self.entry_for_path(ancestor) { - inodes.insert(entry.inode); - } - } - inodes - } - - async fn ignore_stack_for_abs_path( - &self, - abs_path: &Path, - is_dir: bool, - fs: &dyn Fs, - ) -> IgnoreStack { - let mut new_ignores = Vec::new(); - let mut repo_root = None; - for (index, ancestor) in abs_path.ancestors().enumerate() { - if index > 0 { - if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) { - new_ignores.push((ancestor, Some(ignore.clone()))); - } else { - new_ignores.push((ancestor, None)); - } - } - - let metadata = fs.metadata(&ancestor.join(DOT_GIT)).await.ok().flatten(); - if metadata.is_some() { - repo_root = Some(Arc::from(ancestor)); - break; - } - } - - let mut ignore_stack = if let Some(global_gitignore) = self.global_gitignore.clone() { - IgnoreStack::global(global_gitignore) - } else { - IgnoreStack::none() - }; - ignore_stack.repo_root = repo_root; - for (parent_abs_path, ignore) in new_ignores.into_iter().rev() { - if ignore_stack.is_abs_path_ignored(parent_abs_path, true) { - ignore_stack = IgnoreStack::all(); - break; - } else if let Some(ignore) = ignore { - ignore_stack = ignore_stack.append(parent_abs_path.into(), ignore); - } - } - - if ignore_stack.is_abs_path_ignored(abs_path, is_dir) { - ignore_stack = IgnoreStack::all(); - } - - ignore_stack - } - - #[cfg(test)] - fn expanded_entries(&self) -> impl Iterator { - self.entries_by_path - .cursor::<()>(()) - .filter(|entry| entry.kind == EntryKind::Dir && (entry.is_external || entry.is_ignored)) - } - - #[cfg(test)] - pub fn check_invariants(&self, git_state: bool) { - use pretty_assertions::assert_eq; - - assert_eq!( - self.entries_by_path - .cursor::<()>(()) - .map(|e| (&e.path, e.id)) - .collect::>(), - self.entries_by_id - .cursor::<()>(()) - .map(|e| (&e.path, e.id)) - .collect::>() - .into_iter() - .collect::>(), - "entries_by_path and entries_by_id are inconsistent" - ); - - let mut files = self.files(true, 0); - let mut visible_files = self.files(false, 0); - for entry in self.entries_by_path.cursor::<()>(()) { - if entry.is_file() { - assert_eq!(files.next().unwrap().inode, entry.inode); - if (!entry.is_ignored && !entry.is_external) || entry.is_always_included { - assert_eq!(visible_files.next().unwrap().inode, entry.inode); - } - } - } - - assert!(files.next().is_none()); - assert!(visible_files.next().is_none()); - - let mut bfs_paths = Vec::new(); - let mut stack = self - .root_entry() - .map(|e| e.path.as_ref()) - .into_iter() - .collect::>(); - while let Some(path) = stack.pop() { - bfs_paths.push(path); - let ix = stack.len(); - for child_entry in self.child_entries(path) { - stack.insert(ix, &child_entry.path); - } - } - - let dfs_paths_via_iter = self - .entries_by_path - .cursor::<()>(()) - .map(|e| e.path.as_ref()) - .collect::>(); - assert_eq!(bfs_paths, dfs_paths_via_iter); - - let dfs_paths_via_traversal = self - .entries(true, 0) - .map(|e| e.path.as_ref()) - .collect::>(); - - assert_eq!(dfs_paths_via_traversal, dfs_paths_via_iter); - - if git_state { - for ignore_parent_abs_path in self.ignores_by_parent_abs_path.keys() { - let ignore_parent_path = &RelPath::new( - ignore_parent_abs_path - .strip_prefix(self.abs_path.as_path()) - .unwrap(), - PathStyle::local(), - ) - .unwrap(); - assert!(self.entry_for_path(ignore_parent_path).is_some()); - assert!( - self.entry_for_path( - &ignore_parent_path.join(RelPath::unix(GITIGNORE).unwrap()) - ) - .is_some() - ); - } - } - } - - #[cfg(test)] - pub fn entries_without_ids(&self, include_ignored: bool) -> Vec<(&RelPath, u64, bool)> { - let mut paths = Vec::new(); - for entry in self.entries_by_path.cursor::<()>(()) { - if include_ignored || !entry.is_ignored { - paths.push((entry.path.as_ref(), entry.inode, entry.is_ignored)); - } - } - paths.sort_by(|a, b| a.0.cmp(b.0)); - paths - } -} - -impl BackgroundScannerState { - fn should_scan_directory(&self, entry: &Entry) -> bool { - (!entry.is_external && (!entry.is_ignored || entry.is_always_included)) - || entry.path.file_name() == Some(DOT_GIT) - || entry.path.file_name() == Some(local_settings_folder_name()) - || entry.path.file_name() == Some(local_vscode_folder_name()) - || self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning - || self - .paths_to_scan - .iter() - .any(|p| p.starts_with(&entry.path)) - || self - .path_prefixes_to_scan - .iter() - .any(|p| entry.path.starts_with(p)) - } - - async fn enqueue_scan_dir( - &self, - abs_path: Arc, - entry: &Entry, - scan_job_tx: &Sender, - fs: &dyn Fs, - ) { - let path = entry.path.clone(); - let ignore_stack = self - .snapshot - .ignore_stack_for_abs_path(&abs_path, true, fs) - .await; - let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path); - - if !ancestor_inodes.contains(&entry.inode) { - ancestor_inodes.insert(entry.inode); - scan_job_tx - .try_send(ScanJob { - abs_path, - path, - ignore_stack, - scan_queue: scan_job_tx.clone(), - ancestor_inodes, - is_external: entry.is_external, - }) - .unwrap(); - } - } - - fn reuse_entry_id(&mut self, entry: &mut Entry) { - if let Some(mtime) = entry.mtime { - // If an entry with the same inode was removed from the worktree during this scan, - // then it *might* represent the same file or directory. But the OS might also have - // re-used the inode for a completely different file or directory. - // - // Conditionally reuse the old entry's id: - // * if the mtime is the same, the file was probably been renamed. - // * if the path is the same, the file may just have been updated - if let Some(removed_entry) = self.removed_entries.remove(&entry.inode) { - if removed_entry.mtime == Some(mtime) || removed_entry.path == entry.path { - entry.id = removed_entry.id; - } - } else if let Some(existing_entry) = self.snapshot.entry_for_path(&entry.path) { - entry.id = existing_entry.id; - } - } - } - - fn entry_id_for( - &mut self, - next_entry_id: &AtomicUsize, - path: &RelPath, - metadata: &fs::Metadata, - ) -> ProjectEntryId { - // If an entry with the same inode was removed from the worktree during this scan, - // then it *might* represent the same file or directory. But the OS might also have - // re-used the inode for a completely different file or directory. - // - // Conditionally reuse the old entry's id: - // * if the mtime is the same, the file was probably been renamed. - // * if the path is the same, the file may just have been updated - if let Some(removed_entry) = self.removed_entries.remove(&metadata.inode) { - if removed_entry.mtime == Some(metadata.mtime) || *removed_entry.path == *path { - return removed_entry.id; - } - } else if let Some(existing_entry) = self.snapshot.entry_for_path(path) { - return existing_entry.id; - } - ProjectEntryId::new(next_entry_id) - } - - async fn insert_entry(&mut self, entry: Entry, fs: &dyn Fs, watcher: &dyn Watcher) -> Entry { - let entry = self.snapshot.insert_entry(entry, fs); - if entry.path.file_name() == Some(&DOT_GIT) { - self.insert_git_repository(entry.path.clone(), fs, watcher) - .await; - } - - #[cfg(test)] - self.snapshot.check_invariants(false); - - entry - } - - fn populate_dir( - &mut self, - parent_path: Arc, - entries: impl IntoIterator, - ignore: Option>, - ) { - let mut parent_entry = if let Some(parent_entry) = self - .snapshot - .entries_by_path - .get(&PathKey(parent_path.clone()), ()) - { - parent_entry.clone() - } else { - log::warn!( - "populating a directory {:?} that has been removed", - parent_path - ); - return; - }; - - match parent_entry.kind { - EntryKind::PendingDir | EntryKind::UnloadedDir => parent_entry.kind = EntryKind::Dir, - EntryKind::Dir => {} - _ => return, - } - - if let Some(ignore) = ignore { - let abs_parent_path = self - .snapshot - .abs_path - .as_path() - .join(parent_path.as_std_path()) - .into(); - self.snapshot - .ignores_by_parent_abs_path - .insert(abs_parent_path, (ignore, false)); - } - - let parent_entry_id = parent_entry.id; - self.scanned_dirs.insert(parent_entry_id); - let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)]; - let mut entries_by_id_edits = Vec::new(); - - for entry in entries { - entries_by_id_edits.push(Edit::Insert(PathEntry { - id: entry.id, - path: entry.path.clone(), - is_ignored: entry.is_ignored, - scan_id: self.snapshot.scan_id, - })); - entries_by_path_edits.push(Edit::Insert(entry)); - } - - self.snapshot - .entries_by_path - .edit(entries_by_path_edits, ()); - self.snapshot.entries_by_id.edit(entries_by_id_edits, ()); - - if let Err(ix) = self.changed_paths.binary_search(&parent_path) { - self.changed_paths.insert(ix, parent_path.clone()); - } - - #[cfg(test)] - self.snapshot.check_invariants(false); - } - - fn remove_path(&mut self, path: &RelPath) { - log::trace!("background scanner removing path {path:?}"); - let mut new_entries; - let removed_entries; - { - let mut cursor = self - .snapshot - .entries_by_path - .cursor::(()); - new_entries = cursor.slice(&TraversalTarget::path(path), Bias::Left); - removed_entries = cursor.slice(&TraversalTarget::successor(path), Bias::Left); - new_entries.append(cursor.suffix(), ()); - } - self.snapshot.entries_by_path = new_entries; - - let mut removed_ids = Vec::with_capacity(removed_entries.summary().count); - for entry in removed_entries.cursor::<()>(()) { - match self.removed_entries.entry(entry.inode) { - hash_map::Entry::Occupied(mut e) => { - let prev_removed_entry = e.get_mut(); - if entry.id > prev_removed_entry.id { - *prev_removed_entry = entry.clone(); - } - } - hash_map::Entry::Vacant(e) => { - e.insert(entry.clone()); - } - } - - if entry.path.file_name() == Some(GITIGNORE) { - let abs_parent_path = self.snapshot.absolutize(&entry.path.parent().unwrap()); - if let Some((_, needs_update)) = self - .snapshot - .ignores_by_parent_abs_path - .get_mut(abs_parent_path.as_path()) - { - *needs_update = true; - } - } - - if let Err(ix) = removed_ids.binary_search(&entry.id) { - removed_ids.insert(ix, entry.id); - } - } - - self.snapshot - .entries_by_id - .edit(removed_ids.iter().map(|&id| Edit::Remove(id)).collect(), ()); - self.snapshot - .git_repositories - .retain(|id, _| removed_ids.binary_search(id).is_err()); - - #[cfg(test)] - self.snapshot.check_invariants(false); - } - - async fn insert_git_repository( - &mut self, - dot_git_path: Arc, - fs: &dyn Fs, - watcher: &dyn Watcher, - ) { - let work_dir_path: Arc = match dot_git_path.parent() { - Some(parent_dir) => { - // Guard against repositories inside the repository metadata - if parent_dir - .components() - .any(|component| component == DOT_GIT) - { - log::debug!( - "not building git repository for nested `.git` directory, `.git` path in the worktree: {dot_git_path:?}" - ); - return; - }; - - parent_dir.into() - } - None => { - // `dot_git_path.parent().is_none()` means `.git` directory is the opened worktree itself, - // no files inside that directory are tracked by git, so no need to build the repo around it - log::debug!( - "not building git repository for the worktree itself, `.git` path in the worktree: {dot_git_path:?}" - ); - return; - } - }; - - let dot_git_abs_path = Arc::from(self.snapshot.absolutize(&dot_git_path).as_ref()); - - self.insert_git_repository_for_path( - WorkDirectory::InProject { - relative_path: work_dir_path, - }, - dot_git_abs_path, - fs, - watcher, - ) - .await - .log_err(); - } - - async fn insert_git_repository_for_path( - &mut self, - work_directory: WorkDirectory, - dot_git_abs_path: Arc, - fs: &dyn Fs, - watcher: &dyn Watcher, - ) -> Result { - let work_dir_entry = self - .snapshot - .entry_for_path(&work_directory.path_key().0) - .with_context(|| { - format!( - "working directory `{}` not indexed", - work_directory - .path_key() - .0 - .display(self.snapshot.path_style) - ) - })?; - let work_directory_abs_path = self.snapshot.work_directory_abs_path(&work_directory); - - let (repository_dir_abs_path, common_dir_abs_path) = - discover_git_paths(&dot_git_abs_path, fs).await; - watcher - .add(&common_dir_abs_path) - .context("failed to add common directory to watcher") - .log_err(); - if !repository_dir_abs_path.starts_with(&common_dir_abs_path) { - watcher - .add(&repository_dir_abs_path) - .context("failed to add repository directory to watcher") - .log_err(); - } - - let work_directory_id = work_dir_entry.id; - - let local_repository = LocalRepositoryEntry { - work_directory_id, - work_directory, - work_directory_abs_path: work_directory_abs_path.as_path().into(), - git_dir_scan_id: 0, - dot_git_abs_path, - common_dir_abs_path, - repository_dir_abs_path, - }; - - self.snapshot - .git_repositories - .insert(work_directory_id, local_repository.clone()); - - log::trace!("inserting new local git repository"); - Ok(local_repository) - } -} - -async fn is_git_dir(path: &Path, fs: &dyn Fs) -> bool { - if let Some(file_name) = path.file_name() - && file_name == DOT_GIT - { - return true; - } - - // If we're in a bare repository, we are not inside a `.git` folder. In a - // bare repository, the root folder contains what would normally be in the - // `.git` folder. - let head_metadata = fs.metadata(&path.join("HEAD")).await; - if !matches!(head_metadata, Ok(Some(_))) { - return false; - } - let config_metadata = fs.metadata(&path.join("config")).await; - matches!(config_metadata, Ok(Some(_))) -} - -async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result { - let contents = fs - .load(abs_path) - .await - .with_context(|| format!("failed to load gitignore file at {}", abs_path.display()))?; - let parent = abs_path.parent().unwrap_or_else(|| Path::new("/")); - let mut builder = GitignoreBuilder::new(parent); - for line in contents.lines() { - builder.add_line(Some(abs_path.into()), line)?; - } - Ok(builder.build()?) -} - -impl Deref for Worktree { - type Target = Snapshot; - - fn deref(&self) -> &Self::Target { - match self { - Worktree::Local(worktree) => &worktree.snapshot, - Worktree::Remote(worktree) => &worktree.snapshot, - } - } -} - -impl Deref for LocalWorktree { - type Target = LocalSnapshot; - - fn deref(&self) -> &Self::Target { - &self.snapshot - } -} - -impl Deref for RemoteWorktree { - type Target = Snapshot; - - fn deref(&self) -> &Self::Target { - &self.snapshot - } -} - -impl fmt::Debug for LocalWorktree { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.snapshot.fmt(f) - } -} - -impl fmt::Debug for Snapshot { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - struct EntriesById<'a>(&'a SumTree); - struct EntriesByPath<'a>(&'a SumTree); - - impl fmt::Debug for EntriesByPath<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_map() - .entries(self.0.iter().map(|entry| (&entry.path, entry.id))) - .finish() - } - } - - impl fmt::Debug for EntriesById<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_list().entries(self.0.iter()).finish() - } - } - - f.debug_struct("Snapshot") - .field("id", &self.id) - .field("root_name", &self.root_name) - .field("entries_by_path", &EntriesByPath(&self.entries_by_path)) - .field("entries_by_id", &EntriesById(&self.entries_by_id)) - .finish() - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct File { - pub worktree: Entity, - pub path: Arc, - pub disk_state: DiskState, - pub entry_id: Option, - pub is_local: bool, - pub is_private: bool, -} - -impl language::File for File { - fn as_local(&self) -> Option<&dyn language::LocalFile> { - if self.is_local { Some(self) } else { None } - } - - fn disk_state(&self) -> DiskState { - self.disk_state - } - - fn path(&self) -> &Arc { - &self.path - } - - fn full_path(&self, cx: &App) -> PathBuf { - self.worktree.read(cx).full_path(&self.path) - } - - /// Returns the last component of this handle's absolute path. If this handle refers to the root - /// of its worktree, then this method will return the name of the worktree itself. - fn file_name<'a>(&'a self, cx: &'a App) -> &'a str { - self.path - .file_name() - .unwrap_or_else(|| self.worktree.read(cx).root_name_str()) - } - - fn worktree_id(&self, cx: &App) -> WorktreeId { - self.worktree.read(cx).id() - } - - fn to_proto(&self, cx: &App) -> rpc::proto::File { - rpc::proto::File { - worktree_id: self.worktree.read(cx).id().to_proto(), - entry_id: self.entry_id.map(|id| id.to_proto()), - path: self.path.as_ref().to_proto(), - mtime: self.disk_state.mtime().map(|time| time.into()), - is_deleted: self.disk_state == DiskState::Deleted, - } - } - - fn is_private(&self) -> bool { - self.is_private - } - - fn path_style(&self, cx: &App) -> PathStyle { - self.worktree.read(cx).path_style() - } -} - -impl language::LocalFile for File { - fn abs_path(&self, cx: &App) -> PathBuf { - self.worktree.read(cx).absolutize(&self.path) - } - - fn load(&self, cx: &App) -> Task> { - let worktree = self.worktree.read(cx).as_local().unwrap(); - let abs_path = worktree.absolutize(&self.path); - let fs = worktree.fs.clone(); - cx.background_spawn(async move { fs.load(&abs_path).await }) - } - - fn load_bytes(&self, cx: &App) -> Task>> { - let worktree = self.worktree.read(cx).as_local().unwrap(); - let abs_path = worktree.absolutize(&self.path); - let fs = worktree.fs.clone(); - cx.background_spawn(async move { fs.load_bytes(&abs_path).await }) - } -} - -impl File { - pub fn for_entry(entry: Entry, worktree: Entity) -> Arc { - Arc::new(Self { - worktree, - path: entry.path.clone(), - disk_state: if let Some(mtime) = entry.mtime { - DiskState::Present { mtime } - } else { - DiskState::New - }, - entry_id: Some(entry.id), - is_local: true, - is_private: entry.is_private, - }) - } - - pub fn from_proto( - proto: rpc::proto::File, - worktree: Entity, - cx: &App, - ) -> Result { - let worktree_id = worktree.read(cx).as_remote().context("not remote")?.id(); - - anyhow::ensure!( - worktree_id.to_proto() == proto.worktree_id, - "worktree id does not match file" - ); - - let disk_state = if proto.is_deleted { - DiskState::Deleted - } else if let Some(mtime) = proto.mtime.map(&Into::into) { - DiskState::Present { mtime } - } else { - DiskState::New - }; - - Ok(Self { - worktree, - path: RelPath::from_proto(&proto.path).context("invalid path in file protobuf")?, - disk_state, - entry_id: proto.entry_id.map(ProjectEntryId::from_proto), - is_local: false, - is_private: false, - }) - } - - pub fn from_dyn(file: Option<&Arc>) -> Option<&Self> { - file.and_then(|f| { - let f: &dyn language::File = f.borrow(); - let f: &dyn Any = f; - f.downcast_ref() - }) - } - - pub fn worktree_id(&self, cx: &App) -> WorktreeId { - self.worktree.read(cx).id() - } - - pub fn project_entry_id(&self) -> Option { - match self.disk_state { - DiskState::Deleted => None, - _ => self.entry_id, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Entry { - pub id: ProjectEntryId, - pub kind: EntryKind, - pub path: Arc, - pub inode: u64, - pub mtime: Option, - - pub canonical_path: Option>, - /// Whether this entry is ignored by Git. - /// - /// We only scan ignored entries once the directory is expanded and - /// exclude them from searches. - pub is_ignored: bool, - - /// Whether this entry is hidden or inside hidden directory. - /// - /// We only scan hidden entries once the directory is expanded. - pub is_hidden: bool, - - /// Whether this entry is always included in searches. - /// - /// This is used for entries that are always included in searches, even - /// if they are ignored by git. Overridden by file_scan_exclusions. - pub is_always_included: bool, - - /// Whether this entry's canonical path is outside of the worktree. - /// This means the entry is only accessible from the worktree root via a - /// symlink. - /// - /// We only scan entries outside of the worktree once the symlinked - /// directory is expanded. External entries are treated like gitignored - /// entries in that they are not included in searches. - pub is_external: bool, - - /// Whether this entry is considered to be a `.env` file. - pub is_private: bool, - /// The entry's size on disk, in bytes. - pub size: u64, - pub char_bag: CharBag, - pub is_fifo: bool, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum EntryKind { - UnloadedDir, - PendingDir, - Dir, - File, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum PathChange { - /// A filesystem entry was was created. - Added, - /// A filesystem entry was removed. - Removed, - /// A filesystem entry was updated. - Updated, - /// A filesystem entry was either updated or added. We don't know - /// whether or not it already existed, because the path had not - /// been loaded before the event. - AddedOrUpdated, - /// A filesystem entry was found during the initial scan of the worktree. - Loaded, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct UpdatedGitRepository { - /// ID of the repository's working directory. - /// - /// For a repo that's above the worktree root, this is the ID of the worktree root, and hence not unique. - /// It's included here to aid the GitStore in detecting when a repository's working directory is renamed. - pub work_directory_id: ProjectEntryId, - pub old_work_directory_abs_path: Option>, - pub new_work_directory_abs_path: Option>, - /// For a normal git repository checkout, the absolute path to the .git directory. - /// For a worktree, the absolute path to the worktree's subdirectory inside the .git directory. - pub dot_git_abs_path: Option>, - pub repository_dir_abs_path: Option>, - pub common_dir_abs_path: Option>, -} - -pub type UpdatedEntriesSet = Arc<[(Arc, ProjectEntryId, PathChange)]>; -pub type UpdatedGitRepositoriesSet = Arc<[UpdatedGitRepository]>; - -#[derive(Clone, Debug)] -pub struct PathProgress<'a> { - pub max_path: &'a RelPath, -} - -#[derive(Clone, Debug)] -pub struct PathSummary { - pub max_path: Arc, - pub item_summary: S, -} - -impl Summary for PathSummary { - type Context<'a> = S::Context<'a>; - - fn zero(cx: Self::Context<'_>) -> Self { - Self { - max_path: RelPath::empty().into(), - item_summary: S::zero(cx), - } - } - - fn add_summary(&mut self, rhs: &Self, cx: Self::Context<'_>) { - self.max_path = rhs.max_path.clone(); - self.item_summary.add_summary(&rhs.item_summary, cx); - } -} - -impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary> for PathProgress<'a> { - fn zero(_: as Summary>::Context<'_>) -> Self { - Self { - max_path: RelPath::empty(), - } - } - - fn add_summary( - &mut self, - summary: &'a PathSummary, - _: as Summary>::Context<'_>, - ) { - self.max_path = summary.max_path.as_ref() - } -} - -impl<'a> sum_tree::Dimension<'a, PathSummary> for GitSummary { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a PathSummary, _: ()) { - *self += summary.item_summary - } -} - -impl<'a> - sum_tree::SeekTarget<'a, PathSummary, Dimensions, GitSummary>> - for PathTarget<'_> -{ - fn cmp( - &self, - cursor_location: &Dimensions, GitSummary>, - _: (), - ) -> Ordering { - self.cmp_path(cursor_location.0.max_path) - } -} - -impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary> for PathKey { - fn zero(_: S::Context<'_>) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a PathSummary, _: S::Context<'_>) { - self.0 = summary.max_path.clone(); - } -} - -impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary> for TraversalProgress<'a> { - fn zero(_cx: S::Context<'_>) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a PathSummary, _: S::Context<'_>) { - self.max_path = summary.max_path.as_ref(); - } -} - -impl Entry { - fn new( - path: Arc, - metadata: &fs::Metadata, - id: ProjectEntryId, - root_char_bag: CharBag, - canonical_path: Option>, - ) -> Self { - let char_bag = char_bag_for_path(root_char_bag, &path); - Self { - id, - kind: if metadata.is_dir { - EntryKind::PendingDir - } else { - EntryKind::File - }, - path, - inode: metadata.inode, - mtime: Some(metadata.mtime), - size: metadata.len, - canonical_path, - is_ignored: false, - is_hidden: false, - is_always_included: false, - is_external: false, - is_private: false, - char_bag, - is_fifo: metadata.is_fifo, - } - } - - pub fn is_created(&self) -> bool { - self.mtime.is_some() - } - - pub fn is_dir(&self) -> bool { - self.kind.is_dir() - } - - pub fn is_file(&self) -> bool { - self.kind.is_file() - } -} - -impl EntryKind { - pub fn is_dir(&self) -> bool { - matches!( - self, - EntryKind::Dir | EntryKind::PendingDir | EntryKind::UnloadedDir - ) - } - - pub fn is_unloaded(&self) -> bool { - matches!(self, EntryKind::UnloadedDir) - } - - pub fn is_file(&self) -> bool { - matches!(self, EntryKind::File) - } -} - -impl sum_tree::Item for Entry { - type Summary = EntrySummary; - - fn summary(&self, _cx: ()) -> Self::Summary { - let non_ignored_count = if (self.is_ignored || self.is_external) && !self.is_always_included - { - 0 - } else { - 1 - }; - let file_count; - let non_ignored_file_count; - if self.is_file() { - file_count = 1; - non_ignored_file_count = non_ignored_count; - } else { - file_count = 0; - non_ignored_file_count = 0; - } - - EntrySummary { - max_path: self.path.clone(), - count: 1, - non_ignored_count, - file_count, - non_ignored_file_count, - } - } -} - -impl sum_tree::KeyedItem for Entry { - type Key = PathKey; - - fn key(&self) -> Self::Key { - PathKey(self.path.clone()) - } -} - -#[derive(Clone, Debug)] -pub struct EntrySummary { - max_path: Arc, - count: usize, - non_ignored_count: usize, - file_count: usize, - non_ignored_file_count: usize, -} - -impl Default for EntrySummary { - fn default() -> Self { - Self { - max_path: Arc::from(RelPath::empty()), - count: 0, - non_ignored_count: 0, - file_count: 0, - non_ignored_file_count: 0, - } - } -} - -impl sum_tree::ContextLessSummary for EntrySummary { - fn zero() -> Self { - Default::default() - } - - fn add_summary(&mut self, rhs: &Self) { - self.max_path = rhs.max_path.clone(); - self.count += rhs.count; - self.non_ignored_count += rhs.non_ignored_count; - self.file_count += rhs.file_count; - self.non_ignored_file_count += rhs.non_ignored_file_count; - } -} - -#[derive(Clone, Debug)] -struct PathEntry { - id: ProjectEntryId, - path: Arc, - is_ignored: bool, - scan_id: usize, -} - -impl sum_tree::Item for PathEntry { - type Summary = PathEntrySummary; - - fn summary(&self, _cx: ()) -> Self::Summary { - PathEntrySummary { max_id: self.id } - } -} - -impl sum_tree::KeyedItem for PathEntry { - type Key = ProjectEntryId; - - fn key(&self) -> Self::Key { - self.id - } -} - -#[derive(Clone, Debug, Default)] -struct PathEntrySummary { - max_id: ProjectEntryId, -} - -impl sum_tree::ContextLessSummary for PathEntrySummary { - fn zero() -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &Self) { - self.max_id = summary.max_id; - } -} - -impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for ProjectEntryId { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a PathEntrySummary, _: ()) { - *self = summary.max_id; - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct PathKey(pub Arc); - -impl Default for PathKey { - fn default() -> Self { - Self(RelPath::empty().into()) - } -} - -impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a EntrySummary, _: ()) { - self.0 = summary.max_path.clone(); - } -} - -struct BackgroundScanner { - state: async_lock::Mutex, - fs: Arc, - fs_case_sensitive: bool, - status_updates_tx: UnboundedSender, - executor: BackgroundExecutor, - scan_requests_rx: channel::Receiver, - path_prefixes_to_scan_rx: channel::Receiver, - next_entry_id: Arc, - phase: BackgroundScannerPhase, - watcher: Arc, - settings: WorktreeSettings, - share_private_files: bool, - scanning_enabled: bool, -} - -#[derive(Copy, Clone, PartialEq)] -enum BackgroundScannerPhase { - InitialScan, - EventsReceivedDuringInitialScan, - Events, -} - -impl BackgroundScanner { - async fn run(&mut self, mut fs_events_rx: Pin>>>) { - // If the worktree root does not contain a git repository, then find - // the git repository in an ancestor directory. Find any gitignore files - // in ancestor directories. - let root_abs_path = self.state.lock().await.snapshot.abs_path.clone(); - - let repo = if self.scanning_enabled { - let (ignores, repo) = discover_ancestor_git_repo(self.fs.clone(), &root_abs_path).await; - self.state - .lock() - .await - .snapshot - .ignores_by_parent_abs_path - .extend(ignores); - repo - } else { - None - }; - - let containing_git_repository = if let Some((ancestor_dot_git, work_directory)) = repo - && self.scanning_enabled - { - maybe!(async { - self.state - .lock() - .await - .insert_git_repository_for_path( - work_directory, - ancestor_dot_git.clone().into(), - self.fs.as_ref(), - self.watcher.as_ref(), - ) - .await - .log_err()?; - Some(ancestor_dot_git) - }) - .await - } else { - None - }; - - log::trace!("containing git repository: {containing_git_repository:?}"); - - let mut global_gitignore_events = if let Some(global_gitignore_path) = - &paths::global_gitignore_path() - && self.scanning_enabled - { - let is_file = self.fs.is_file(&global_gitignore_path).await; - self.state.lock().await.snapshot.global_gitignore = if is_file { - build_gitignore(global_gitignore_path, self.fs.as_ref()) - .await - .ok() - .map(Arc::new) - } else { - None - }; - if is_file - || matches!(global_gitignore_path.parent(), Some(path) if self.fs.is_dir(path).await) - { - self.fs - .watch(global_gitignore_path, FS_WATCH_LATENCY) - .await - .0 - } else { - Box::pin(futures::stream::pending()) - } - } else { - self.state.lock().await.snapshot.global_gitignore = None; - Box::pin(futures::stream::pending()) - }; - - let (scan_job_tx, scan_job_rx) = channel::unbounded(); - { - let mut state = self.state.lock().await; - state.snapshot.scan_id += 1; - if let Some(mut root_entry) = state.snapshot.root_entry().cloned() { - let ignore_stack = state - .snapshot - .ignore_stack_for_abs_path(root_abs_path.as_path(), true, self.fs.as_ref()) - .await; - if ignore_stack.is_abs_path_ignored(root_abs_path.as_path(), true) { - root_entry.is_ignored = true; - let mut root_entry = root_entry.clone(); - state.reuse_entry_id(&mut root_entry); - state - .insert_entry(root_entry, self.fs.as_ref(), self.watcher.as_ref()) - .await; - } - if root_entry.is_dir() && self.scanning_enabled { - state - .enqueue_scan_dir( - root_abs_path.as_path().into(), - &root_entry, - &scan_job_tx, - self.fs.as_ref(), - ) - .await; - } - } - }; - - // Perform an initial scan of the directory. - drop(scan_job_tx); - self.scan_dirs(true, scan_job_rx).await; - { - let mut state = self.state.lock().await; - state.snapshot.completed_scan_id = state.snapshot.scan_id; - } - - self.send_status_update(false, SmallVec::new()).await; - - // Process any any FS events that occurred while performing the initial scan. - // For these events, update events cannot be as precise, because we didn't - // have the previous state loaded yet. - self.phase = BackgroundScannerPhase::EventsReceivedDuringInitialScan; - if let Poll::Ready(Some(mut paths)) = futures::poll!(fs_events_rx.next()) { - while let Poll::Ready(Some(more_paths)) = futures::poll!(fs_events_rx.next()) { - paths.extend(more_paths); - } - self.process_events( - paths - .into_iter() - .filter(|e| e.kind.is_some()) - .map(Into::into) - .collect(), - ) - .await; - } - if let Some(abs_path) = containing_git_repository { - self.process_events(vec![abs_path]).await; - } - - // Continue processing events until the worktree is dropped. - self.phase = BackgroundScannerPhase::Events; - - loop { - select_biased! { - // Process any path refresh requests from the worktree. Prioritize - // these before handling changes reported by the filesystem. - request = self.next_scan_request().fuse() => { - let Ok(request) = request else { break }; - if !self.process_scan_request(request, false).await { - return; - } - } - - path_prefix_request = self.path_prefixes_to_scan_rx.recv().fuse() => { - let Ok(request) = path_prefix_request else { break }; - log::trace!("adding path prefix {:?}", request.path); - - let did_scan = self.forcibly_load_paths(std::slice::from_ref(&request.path)).await; - if did_scan { - let abs_path = - { - let mut state = self.state.lock().await; - state.path_prefixes_to_scan.insert(request.path.clone()); - state.snapshot.absolutize(&request.path) - }; - - if let Some(abs_path) = self.fs.canonicalize(&abs_path).await.log_err() { - self.process_events(vec![abs_path]).await; - } - } - self.send_status_update(false, request.done).await; - } - - paths = fs_events_rx.next().fuse() => { - let Some(mut paths) = paths else { break }; - while let Poll::Ready(Some(more_paths)) = futures::poll!(fs_events_rx.next()) { - paths.extend(more_paths); - } - self.process_events(paths.into_iter().filter(|e| e.kind.is_some()).map(Into::into).collect()).await; - } - - paths = global_gitignore_events.next().fuse() => { - match paths.as_deref() { - Some([event, ..]) => { - self.update_global_gitignore(&event.path).await; - } - _ => (), - } - } - } - } - } - - async fn process_scan_request(&self, mut request: ScanRequest, scanning: bool) -> bool { - log::debug!("rescanning paths {:?}", request.relative_paths); - - request.relative_paths.sort_unstable(); - self.forcibly_load_paths(&request.relative_paths).await; - - let root_path = self.state.lock().await.snapshot.abs_path.clone(); - let root_canonical_path = self.fs.canonicalize(root_path.as_path()).await; - let root_canonical_path = match &root_canonical_path { - Ok(path) => SanitizedPath::new(path), - Err(err) => { - log::error!("failed to canonicalize root path {root_path:?}: {err:#}"); - return true; - } - }; - let abs_paths = request - .relative_paths - .iter() - .map(|path| { - if path.file_name().is_some() { - root_canonical_path.as_path().join(path.as_std_path()) - } else { - root_canonical_path.as_path().to_path_buf() - } - }) - .collect::>(); - - { - let mut state = self.state.lock().await; - let is_idle = state.snapshot.completed_scan_id == state.snapshot.scan_id; - state.snapshot.scan_id += 1; - if is_idle { - state.snapshot.completed_scan_id = state.snapshot.scan_id; - } - } - - self.reload_entries_for_paths( - &root_path, - &root_canonical_path, - &request.relative_paths, - abs_paths, - None, - ) - .await; - - self.send_status_update(scanning, request.done).await - } - - async fn process_events(&self, mut abs_paths: Vec) { - log::trace!("process events: {abs_paths:?}"); - let root_path = self.state.lock().await.snapshot.abs_path.clone(); - let root_canonical_path = self.fs.canonicalize(root_path.as_path()).await; - let root_canonical_path = match &root_canonical_path { - Ok(path) => SanitizedPath::new(path), - Err(err) => { - let new_path = self - .state - .lock() - .await - .snapshot - .root_file_handle - .clone() - .and_then(|handle| handle.current_path(&self.fs).log_err()) - .map(|path| SanitizedPath::new_arc(&path)) - .filter(|new_path| *new_path != root_path); - - if let Some(new_path) = new_path { - log::info!( - "root renamed from {} to {}", - root_path.as_path().display(), - new_path.as_path().display() - ); - self.status_updates_tx - .unbounded_send(ScanState::RootUpdated { new_path }) - .ok(); - } else { - log::warn!("root path could not be canonicalized: {:#}", err); - } - return; - } - }; - - // Certain directories may have FS changes, but do not lead to git data changes that Zed cares about. - // Ignore these, to avoid Zed unnecessarily rescanning git metadata. - let skipped_files_in_dot_git = [COMMIT_MESSAGE, INDEX_LOCK]; - let skipped_dirs_in_dot_git = [FSMONITOR_DAEMON, LFS_DIR]; - - let mut relative_paths = Vec::with_capacity(abs_paths.len()); - let mut dot_git_abs_paths = Vec::new(); - abs_paths.sort_unstable(); - abs_paths.dedup_by(|a, b| a.starts_with(b)); - { - let snapshot = &self.state.lock().await.snapshot; - - let mut ranges_to_drop = SmallVec::<[Range; 4]>::new(); - - fn skip_ix(ranges: &mut SmallVec<[Range; 4]>, ix: usize) { - if let Some(last_range) = ranges.last_mut() - && last_range.end == ix - { - last_range.end += 1; - } else { - ranges.push(ix..ix + 1); - } - } - - for (ix, abs_path) in abs_paths.iter().enumerate() { - let abs_path = &SanitizedPath::new(&abs_path); - - let mut is_git_related = false; - let mut dot_git_paths = None; - - for ancestor in abs_path.as_path().ancestors() { - if is_git_dir(ancestor, self.fs.as_ref()).await { - let path_in_git_dir = abs_path - .as_path() - .strip_prefix(ancestor) - .expect("stripping off the ancestor"); - dot_git_paths = Some((ancestor.to_owned(), path_in_git_dir.to_owned())); - break; - } - } - - if let Some((dot_git_abs_path, path_in_git_dir)) = dot_git_paths { - if skipped_files_in_dot_git - .iter() - .any(|skipped| OsStr::new(skipped) == path_in_git_dir.as_path().as_os_str()) - || skipped_dirs_in_dot_git.iter().any(|skipped_git_subdir| { - path_in_git_dir.starts_with(skipped_git_subdir) - }) - { - log::debug!( - "ignoring event {abs_path:?} as it's in the .git directory among skipped files or directories" - ); - skip_ix(&mut ranges_to_drop, ix); - continue; - } - - is_git_related = true; - if !dot_git_abs_paths.contains(&dot_git_abs_path) { - dot_git_abs_paths.push(dot_git_abs_path); - } - } - - let relative_path = if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) - && let Ok(path) = RelPath::new(path, PathStyle::local()) - { - path - } else { - if is_git_related { - log::debug!( - "ignoring event {abs_path:?}, since it's in git dir outside of root path {root_canonical_path:?}", - ); - } else { - log::error!( - "ignoring event {abs_path:?} outside of root path {root_canonical_path:?}", - ); - } - skip_ix(&mut ranges_to_drop, ix); - continue; - }; - - if abs_path.file_name() == Some(OsStr::new(GITIGNORE)) { - for (_, repo) in snapshot - .git_repositories - .iter() - .filter(|(_, repo)| repo.directory_contains(&relative_path)) - { - if !dot_git_abs_paths.iter().any(|dot_git_abs_path| { - dot_git_abs_path == repo.common_dir_abs_path.as_ref() - }) { - dot_git_abs_paths.push(repo.common_dir_abs_path.to_path_buf()); - } - } - } - - let parent_dir_is_loaded = relative_path.parent().is_none_or(|parent| { - snapshot - .entry_for_path(parent) - .is_some_and(|entry| entry.kind == EntryKind::Dir) - }); - if !parent_dir_is_loaded { - log::debug!("ignoring event {relative_path:?} within unloaded directory"); - skip_ix(&mut ranges_to_drop, ix); - continue; - } - - if self.settings.is_path_excluded(&relative_path) { - if !is_git_related { - log::debug!("ignoring FS event for excluded path {relative_path:?}"); - } - skip_ix(&mut ranges_to_drop, ix); - continue; - } - - relative_paths.push(relative_path.into_arc()); - } - - for range_to_drop in ranges_to_drop.into_iter().rev() { - abs_paths.drain(range_to_drop); - } - } - - if relative_paths.is_empty() && dot_git_abs_paths.is_empty() { - return; - } - - self.state.lock().await.snapshot.scan_id += 1; - - let (scan_job_tx, scan_job_rx) = channel::unbounded(); - log::debug!("received fs events {:?}", relative_paths); - self.reload_entries_for_paths( - &root_path, - &root_canonical_path, - &relative_paths, - abs_paths, - Some(scan_job_tx.clone()), - ) - .await; - - let affected_repo_roots = if !dot_git_abs_paths.is_empty() { - self.update_git_repositories(dot_git_abs_paths).await - } else { - Vec::new() - }; - - { - let mut ignores_to_update = self.ignores_needing_update().await; - ignores_to_update.extend(affected_repo_roots); - let ignores_to_update = self.order_ignores(ignores_to_update).await; - let snapshot = self.state.lock().await.snapshot.clone(); - self.update_ignore_statuses_for_paths(scan_job_tx, snapshot, ignores_to_update) - .await; - self.scan_dirs(false, scan_job_rx).await; - } - - { - let mut state = self.state.lock().await; - state.snapshot.completed_scan_id = state.snapshot.scan_id; - for (_, entry) in mem::take(&mut state.removed_entries) { - state.scanned_dirs.remove(&entry.id); - } - } - self.send_status_update(false, SmallVec::new()).await; - } - - async fn update_global_gitignore(&self, abs_path: &Path) { - let ignore = build_gitignore(abs_path, self.fs.as_ref()) - .await - .log_err() - .map(Arc::new); - let (prev_snapshot, ignore_stack, abs_path) = { - let mut state = self.state.lock().await; - state.snapshot.global_gitignore = ignore; - let abs_path = state.snapshot.abs_path().clone(); - let ignore_stack = state - .snapshot - .ignore_stack_for_abs_path(&abs_path, true, self.fs.as_ref()) - .await; - (state.snapshot.clone(), ignore_stack, abs_path) - }; - let (scan_job_tx, scan_job_rx) = channel::unbounded(); - self.update_ignore_statuses_for_paths( - scan_job_tx, - prev_snapshot, - vec![(abs_path, ignore_stack)], - ) - .await; - self.scan_dirs(false, scan_job_rx).await; - self.send_status_update(false, SmallVec::new()).await; - } - - async fn forcibly_load_paths(&self, paths: &[Arc]) -> bool { - let (scan_job_tx, scan_job_rx) = channel::unbounded(); - { - let mut state = self.state.lock().await; - let root_path = state.snapshot.abs_path.clone(); - for path in paths { - for ancestor in path.ancestors() { - if let Some(entry) = state.snapshot.entry_for_path(ancestor) - && entry.kind == EntryKind::UnloadedDir - { - let abs_path = root_path.join(ancestor.as_std_path()); - state - .enqueue_scan_dir( - abs_path.into(), - entry, - &scan_job_tx, - self.fs.as_ref(), - ) - .await; - state.paths_to_scan.insert(path.clone()); - break; - } - } - } - drop(scan_job_tx); - } - while let Ok(job) = scan_job_rx.recv().await { - self.scan_dir(&job).await.log_err(); - } - - !mem::take(&mut self.state.lock().await.paths_to_scan).is_empty() - } - - async fn scan_dirs( - &self, - enable_progress_updates: bool, - scan_jobs_rx: channel::Receiver, - ) { - if self - .status_updates_tx - .unbounded_send(ScanState::Started) - .is_err() - { - return; - } - - let progress_update_count = AtomicUsize::new(0); - self.executor - .scoped_priority(Priority::Low, |scope| { - for _ in 0..self.executor.num_cpus() { - scope.spawn(async { - let mut last_progress_update_count = 0; - let progress_update_timer = self.progress_timer(enable_progress_updates).fuse(); - futures::pin_mut!(progress_update_timer); - - loop { - select_biased! { - // Process any path refresh requests before moving on to process - // the scan queue, so that user operations are prioritized. - request = self.next_scan_request().fuse() => { - let Ok(request) = request else { break }; - if !self.process_scan_request(request, true).await { - return; - } - } - - // Send periodic progress updates to the worktree. Use an atomic counter - // to ensure that only one of the workers sends a progress update after - // the update interval elapses. - _ = progress_update_timer => { - match progress_update_count.compare_exchange( - last_progress_update_count, - last_progress_update_count + 1, - SeqCst, - SeqCst - ) { - Ok(_) => { - last_progress_update_count += 1; - self.send_status_update(true, SmallVec::new()).await; - } - Err(count) => { - last_progress_update_count = count; - } - } - progress_update_timer.set(self.progress_timer(enable_progress_updates).fuse()); - } - - // Recursively load directories from the file system. - job = scan_jobs_rx.recv().fuse() => { - let Ok(job) = job else { break }; - if let Err(err) = self.scan_dir(&job).await - && job.path.is_empty() { - log::error!("error scanning directory {:?}: {}", job.abs_path, err); - } - } - } - } - }); - } - }) - .await; - } - - async fn send_status_update( - &self, - scanning: bool, - barrier: SmallVec<[barrier::Sender; 1]>, - ) -> bool { - let mut state = self.state.lock().await; - if state.changed_paths.is_empty() && scanning { - return true; - } - - let new_snapshot = state.snapshot.clone(); - let old_snapshot = mem::replace(&mut state.prev_snapshot, new_snapshot.snapshot.clone()); - let changes = build_diff( - self.phase, - &old_snapshot, - &new_snapshot, - &state.changed_paths, - ); - state.changed_paths.clear(); - - self.status_updates_tx - .unbounded_send(ScanState::Updated { - snapshot: new_snapshot, - changes, - scanning, - barrier, - }) - .is_ok() - } - - async fn scan_dir(&self, job: &ScanJob) -> Result<()> { - let root_abs_path; - let root_char_bag; - { - let snapshot = &self.state.lock().await.snapshot; - if self.settings.is_path_excluded(&job.path) { - log::error!("skipping excluded directory {:?}", job.path); - return Ok(()); - } - log::trace!("scanning directory {:?}", job.path); - root_abs_path = snapshot.abs_path().clone(); - root_char_bag = snapshot.root_char_bag; - } - - let next_entry_id = self.next_entry_id.clone(); - let mut ignore_stack = job.ignore_stack.clone(); - let mut new_ignore = None; - let mut root_canonical_path = None; - let mut new_entries: Vec = Vec::new(); - let mut new_jobs: Vec> = Vec::new(); - let mut child_paths = self - .fs - .read_dir(&job.abs_path) - .await? - .filter_map(|entry| async { - match entry { - Ok(entry) => Some(entry), - Err(error) => { - log::error!("error processing entry {:?}", error); - None - } - } - }) - .collect::>() - .await; - - // Ensure that .git and .gitignore are processed first. - swap_to_front(&mut child_paths, GITIGNORE); - swap_to_front(&mut child_paths, DOT_GIT); - - if let Some(path) = child_paths.first() - && path.ends_with(DOT_GIT) - { - ignore_stack.repo_root = Some(job.abs_path.clone()); - } - - for child_abs_path in child_paths { - let child_abs_path: Arc = child_abs_path.into(); - let child_name = child_abs_path.file_name().unwrap(); - let Some(child_path) = child_name - .to_str() - .and_then(|name| Some(job.path.join(RelPath::unix(name).ok()?))) - else { - continue; - }; - - if child_name == DOT_GIT { - let mut state = self.state.lock().await; - state - .insert_git_repository( - child_path.clone(), - self.fs.as_ref(), - self.watcher.as_ref(), - ) - .await; - } else if child_name == GITIGNORE { - match build_gitignore(&child_abs_path, self.fs.as_ref()).await { - Ok(ignore) => { - let ignore = Arc::new(ignore); - ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone()); - new_ignore = Some(ignore); - } - Err(error) => { - log::error!( - "error loading .gitignore file {:?} - {:?}", - child_name, - error - ); - } - } - } - - if self.settings.is_path_excluded(&child_path) { - log::debug!("skipping excluded child entry {child_path:?}"); - self.state.lock().await.remove_path(&child_path); - continue; - } - - let child_metadata = match self.fs.metadata(&child_abs_path).await { - Ok(Some(metadata)) => metadata, - Ok(None) => continue, - Err(err) => { - log::error!("error processing {child_abs_path:?}: {err:?}"); - continue; - } - }; - - let mut child_entry = Entry::new( - child_path.clone(), - &child_metadata, - ProjectEntryId::new(&next_entry_id), - root_char_bag, - None, - ); - - if job.is_external { - child_entry.is_external = true; - } else if child_metadata.is_symlink { - let canonical_path = match self.fs.canonicalize(&child_abs_path).await { - Ok(path) => path, - Err(err) => { - log::error!("error reading target of symlink {child_abs_path:?}: {err:#}",); - continue; - } - }; - - // lazily canonicalize the root path in order to determine if - // symlinks point outside of the worktree. - let root_canonical_path = match &root_canonical_path { - Some(path) => path, - None => match self.fs.canonicalize(&root_abs_path).await { - Ok(path) => root_canonical_path.insert(path), - Err(err) => { - log::error!("error canonicalizing root {:?}: {:?}", root_abs_path, err); - continue; - } - }, - }; - - if !canonical_path.starts_with(root_canonical_path) { - child_entry.is_external = true; - } - - child_entry.canonical_path = Some(canonical_path.into()); - } - - if child_entry.is_dir() { - child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true); - child_entry.is_always_included = - self.settings.is_path_always_included(&child_path, true); - - // Avoid recursing until crash in the case of a recursive symlink - if job.ancestor_inodes.contains(&child_entry.inode) { - new_jobs.push(None); - } else { - let mut ancestor_inodes = job.ancestor_inodes.clone(); - ancestor_inodes.insert(child_entry.inode); - - new_jobs.push(Some(ScanJob { - abs_path: child_abs_path.clone(), - path: child_path, - is_external: child_entry.is_external, - ignore_stack: if child_entry.is_ignored { - IgnoreStack::all() - } else { - ignore_stack.clone() - }, - ancestor_inodes, - scan_queue: job.scan_queue.clone(), - })); - } - } else { - child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false); - child_entry.is_always_included = - self.settings.is_path_always_included(&child_path, false); - } - - { - let relative_path = job - .path - .join(RelPath::unix(child_name.to_str().unwrap()).unwrap()); - if self.is_path_private(&relative_path) { - log::debug!("detected private file: {relative_path:?}"); - child_entry.is_private = true; - } - if self.settings.is_path_hidden(&relative_path) { - log::debug!("detected hidden file: {relative_path:?}"); - child_entry.is_hidden = true; - } - } - - new_entries.push(child_entry); - } - - let mut state = self.state.lock().await; - - // Identify any subdirectories that should not be scanned. - let mut job_ix = 0; - for entry in &mut new_entries { - state.reuse_entry_id(entry); - if entry.is_dir() { - if state.should_scan_directory(entry) { - job_ix += 1; - } else { - log::debug!("defer scanning directory {:?}", entry.path); - entry.kind = EntryKind::UnloadedDir; - new_jobs.remove(job_ix); - } - } - if entry.is_always_included { - state - .snapshot - .always_included_entries - .push(entry.path.clone()); - } - } - - state.populate_dir(job.path.clone(), new_entries, new_ignore); - self.watcher.add(job.abs_path.as_ref()).log_err(); - - for new_job in new_jobs.into_iter().flatten() { - job.scan_queue - .try_send(new_job) - .expect("channel is unbounded"); - } - - Ok(()) - } - - /// All list arguments should be sorted before calling this function - async fn reload_entries_for_paths( - &self, - root_abs_path: &SanitizedPath, - root_canonical_path: &SanitizedPath, - relative_paths: &[Arc], - abs_paths: Vec, - scan_queue_tx: Option>, - ) { - // grab metadata for all requested paths - let metadata = futures::future::join_all( - abs_paths - .iter() - .map(|abs_path| async move { - let metadata = self.fs.metadata(abs_path).await?; - if let Some(metadata) = metadata { - let canonical_path = self.fs.canonicalize(abs_path).await?; - - // If we're on a case-insensitive filesystem (default on macOS), we want - // to only ignore metadata for non-symlink files if their absolute-path matches - // the canonical-path. - // Because if not, this might be a case-only-renaming (`mv test.txt TEST.TXT`) - // and we want to ignore the metadata for the old path (`test.txt`) so it's - // treated as removed. - if !self.fs_case_sensitive && !metadata.is_symlink { - let canonical_file_name = canonical_path.file_name(); - let file_name = abs_path.file_name(); - if canonical_file_name != file_name { - return Ok(None); - } - } - - anyhow::Ok(Some((metadata, SanitizedPath::new_arc(&canonical_path)))) - } else { - Ok(None) - } - }) - .collect::>(), - ) - .await; - - let mut new_ancestor_repo = if relative_paths.iter().any(|path| path.is_empty()) { - Some(discover_ancestor_git_repo(self.fs.clone(), &root_abs_path).await) - } else { - None - }; - - let mut state = self.state.lock().await; - let doing_recursive_update = scan_queue_tx.is_some(); - - // Remove any entries for paths that no longer exist or are being recursively - // refreshed. Do this before adding any new entries, so that renames can be - // detected regardless of the order of the paths. - for (path, metadata) in relative_paths.iter().zip(metadata.iter()) { - if matches!(metadata, Ok(None)) || doing_recursive_update { - state.remove_path(path); - } - } - - for (path, metadata) in relative_paths.iter().zip(metadata.into_iter()) { - let abs_path: Arc = root_abs_path.join(path.as_std_path()).into(); - match metadata { - Ok(Some((metadata, canonical_path))) => { - let ignore_stack = state - .snapshot - .ignore_stack_for_abs_path(&abs_path, metadata.is_dir, self.fs.as_ref()) - .await; - let is_external = !canonical_path.starts_with(&root_canonical_path); - let entry_id = state.entry_id_for(self.next_entry_id.as_ref(), path, &metadata); - let mut fs_entry = Entry::new( - path.clone(), - &metadata, - entry_id, - state.snapshot.root_char_bag, - if metadata.is_symlink { - Some(canonical_path.as_path().to_path_buf().into()) - } else { - None - }, - ); - - let is_dir = fs_entry.is_dir(); - fs_entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, is_dir); - fs_entry.is_external = is_external; - fs_entry.is_private = self.is_path_private(path); - fs_entry.is_always_included = - self.settings.is_path_always_included(path, is_dir); - fs_entry.is_hidden = self.settings.is_path_hidden(path); - - if let (Some(scan_queue_tx), true) = (&scan_queue_tx, is_dir) { - if state.should_scan_directory(&fs_entry) - || (fs_entry.path.is_empty() - && abs_path.file_name() == Some(OsStr::new(DOT_GIT))) - { - state - .enqueue_scan_dir( - abs_path, - &fs_entry, - scan_queue_tx, - self.fs.as_ref(), - ) - .await; - } else { - fs_entry.kind = EntryKind::UnloadedDir; - } - } - - state - .insert_entry(fs_entry.clone(), self.fs.as_ref(), self.watcher.as_ref()) - .await; - - if path.is_empty() - && let Some((ignores, repo)) = new_ancestor_repo.take() - { - log::trace!("updating ancestor git repository"); - state.snapshot.ignores_by_parent_abs_path.extend(ignores); - if let Some((ancestor_dot_git, work_directory)) = repo { - state - .insert_git_repository_for_path( - work_directory, - ancestor_dot_git.into(), - self.fs.as_ref(), - self.watcher.as_ref(), - ) - .await - .log_err(); - } - } - } - Ok(None) => { - self.remove_repo_path(path.clone(), &mut state.snapshot); - } - Err(err) => { - log::error!("error reading file {abs_path:?} on event: {err:#}"); - } - } - } - - util::extend_sorted( - &mut state.changed_paths, - relative_paths.iter().cloned(), - usize::MAX, - Ord::cmp, - ); - } - - fn remove_repo_path(&self, path: Arc, snapshot: &mut LocalSnapshot) -> Option<()> { - if !path.components().any(|component| component == DOT_GIT) - && let Some(local_repo) = snapshot.local_repo_for_work_directory_path(&path) - { - let id = local_repo.work_directory_id; - log::debug!("remove repo path: {:?}", path); - snapshot.git_repositories.remove(&id); - return Some(()); - } - - Some(()) - } - - async fn update_ignore_statuses_for_paths( - &self, - scan_job_tx: Sender, - prev_snapshot: LocalSnapshot, - ignores_to_update: Vec<(Arc, IgnoreStack)>, - ) { - let (ignore_queue_tx, ignore_queue_rx) = channel::unbounded(); - { - for (parent_abs_path, ignore_stack) in ignores_to_update { - ignore_queue_tx - .send_blocking(UpdateIgnoreStatusJob { - abs_path: parent_abs_path, - ignore_stack, - ignore_queue: ignore_queue_tx.clone(), - scan_queue: scan_job_tx.clone(), - }) - .unwrap(); - } - } - drop(ignore_queue_tx); - - self.executor - .scoped(|scope| { - for _ in 0..self.executor.num_cpus() { - scope.spawn(async { - loop { - select_biased! { - // Process any path refresh requests before moving on to process - // the queue of ignore statuses. - request = self.next_scan_request().fuse() => { - let Ok(request) = request else { break }; - if !self.process_scan_request(request, true).await { - return; - } - } - - // Recursively process directories whose ignores have changed. - job = ignore_queue_rx.recv().fuse() => { - let Ok(job) = job else { break }; - self.update_ignore_status(job, &prev_snapshot).await; - } - } - } - }); - } - }) - .await; - } - - async fn ignores_needing_update(&self) -> Vec> { - let mut ignores_to_update = Vec::new(); - - { - let snapshot = &mut self.state.lock().await.snapshot; - let abs_path = snapshot.abs_path.clone(); - snapshot - .ignores_by_parent_abs_path - .retain(|parent_abs_path, (_, needs_update)| { - if let Ok(parent_path) = parent_abs_path.strip_prefix(abs_path.as_path()) - && let Some(parent_path) = - RelPath::new(&parent_path, PathStyle::local()).log_err() - { - if *needs_update { - *needs_update = false; - if snapshot.snapshot.entry_for_path(&parent_path).is_some() { - ignores_to_update.push(parent_abs_path.clone()); - } - } - - let ignore_path = parent_path.join(RelPath::unix(GITIGNORE).unwrap()); - if snapshot.snapshot.entry_for_path(&ignore_path).is_none() { - return false; - } - } - true - }); - } - - ignores_to_update - } - - async fn order_ignores(&self, mut ignores: Vec>) -> Vec<(Arc, IgnoreStack)> { - let fs = self.fs.clone(); - let snapshot = self.state.lock().await.snapshot.clone(); - ignores.sort_unstable(); - let mut ignores_to_update = ignores.into_iter().peekable(); - - let mut result = vec![]; - while let Some(parent_abs_path) = ignores_to_update.next() { - while ignores_to_update - .peek() - .map_or(false, |p| p.starts_with(&parent_abs_path)) - { - ignores_to_update.next().unwrap(); - } - let ignore_stack = snapshot - .ignore_stack_for_abs_path(&parent_abs_path, true, fs.as_ref()) - .await; - result.push((parent_abs_path, ignore_stack)); - } - - result - } - - async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) { - log::trace!("update ignore status {:?}", job.abs_path); - - let mut ignore_stack = job.ignore_stack; - if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) { - ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone()); - } - - let mut entries_by_id_edits = Vec::new(); - let mut entries_by_path_edits = Vec::new(); - let Some(path) = job - .abs_path - .strip_prefix(snapshot.abs_path.as_path()) - .map_err(|_| { - anyhow::anyhow!( - "Failed to strip prefix '{}' from path '{}'", - snapshot.abs_path.as_path().display(), - job.abs_path.display() - ) - }) - .log_err() - else { - return; - }; - - let Some(path) = RelPath::new(&path, PathStyle::local()).log_err() else { - return; - }; - - if let Ok(Some(metadata)) = self.fs.metadata(&job.abs_path.join(DOT_GIT)).await - && metadata.is_dir - { - ignore_stack.repo_root = Some(job.abs_path.clone()); - } - - for mut entry in snapshot.child_entries(&path).cloned() { - let was_ignored = entry.is_ignored; - let abs_path: Arc = snapshot.absolutize(&entry.path).into(); - entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, entry.is_dir()); - - if entry.is_dir() { - let child_ignore_stack = if entry.is_ignored { - IgnoreStack::all() - } else { - ignore_stack.clone() - }; - - // Scan any directories that were previously ignored and weren't previously scanned. - if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() { - let state = self.state.lock().await; - if state.should_scan_directory(&entry) { - state - .enqueue_scan_dir( - abs_path.clone(), - &entry, - &job.scan_queue, - self.fs.as_ref(), - ) - .await; - } - } - - job.ignore_queue - .send(UpdateIgnoreStatusJob { - abs_path: abs_path.clone(), - ignore_stack: child_ignore_stack, - ignore_queue: job.ignore_queue.clone(), - scan_queue: job.scan_queue.clone(), - }) - .await - .unwrap(); - } - - if entry.is_ignored != was_ignored { - let mut path_entry = snapshot.entries_by_id.get(&entry.id, ()).unwrap().clone(); - path_entry.scan_id = snapshot.scan_id; - path_entry.is_ignored = entry.is_ignored; - entries_by_id_edits.push(Edit::Insert(path_entry)); - entries_by_path_edits.push(Edit::Insert(entry)); - } - } - - let state = &mut self.state.lock().await; - for edit in &entries_by_path_edits { - if let Edit::Insert(entry) = edit - && let Err(ix) = state.changed_paths.binary_search(&entry.path) - { - state.changed_paths.insert(ix, entry.path.clone()); - } - } - - state - .snapshot - .entries_by_path - .edit(entries_by_path_edits, ()); - state.snapshot.entries_by_id.edit(entries_by_id_edits, ()); - } - - async fn update_git_repositories(&self, dot_git_paths: Vec) -> Vec> { - log::trace!("reloading repositories: {dot_git_paths:?}"); - let mut state = self.state.lock().await; - let scan_id = state.snapshot.scan_id; - let mut affected_repo_roots = Vec::new(); - for dot_git_dir in dot_git_paths { - let existing_repository_entry = - state - .snapshot - .git_repositories - .iter() - .find_map(|(_, repo)| { - let dot_git_dir = SanitizedPath::new(&dot_git_dir); - if SanitizedPath::new(repo.common_dir_abs_path.as_ref()) == dot_git_dir - || SanitizedPath::new(repo.repository_dir_abs_path.as_ref()) - == dot_git_dir - { - Some(repo.clone()) - } else { - None - } - }); - - match existing_repository_entry { - None => { - let Ok(relative) = dot_git_dir.strip_prefix(state.snapshot.abs_path()) else { - debug_panic!( - "update_git_repositories called with .git directory outside the worktree root" - ); - return Vec::new(); - }; - affected_repo_roots.push(dot_git_dir.parent().unwrap().into()); - state - .insert_git_repository( - RelPath::new(relative, PathStyle::local()) - .unwrap() - .into_arc(), - self.fs.as_ref(), - self.watcher.as_ref(), - ) - .await; - } - Some(local_repository) => { - state.snapshot.git_repositories.update( - &local_repository.work_directory_id, - |entry| { - entry.git_dir_scan_id = scan_id; - }, - ); - } - }; - } - - // Remove any git repositories whose .git entry no longer exists. - let snapshot = &mut state.snapshot; - let mut ids_to_preserve = HashSet::default(); - for (&work_directory_id, entry) in snapshot.git_repositories.iter() { - let exists_in_snapshot = - snapshot - .entry_for_id(work_directory_id) - .is_some_and(|entry| { - snapshot - .entry_for_path(&entry.path.join(RelPath::unix(DOT_GIT).unwrap())) - .is_some() - }); - - if exists_in_snapshot - || matches!( - self.fs.metadata(&entry.common_dir_abs_path).await, - Ok(Some(_)) - ) - { - ids_to_preserve.insert(work_directory_id); - } - } - - snapshot - .git_repositories - .retain(|work_directory_id, entry| { - let preserve = ids_to_preserve.contains(work_directory_id); - if !preserve { - affected_repo_roots.push(entry.dot_git_abs_path.parent().unwrap().into()); - } - preserve - }); - - affected_repo_roots - } - - async fn progress_timer(&self, running: bool) { - if !running { - return futures::future::pending().await; - } - - #[cfg(any(test, feature = "test-support"))] - if self.fs.is_fake() { - return self.executor.simulate_random_delay().await; - } - - smol::Timer::after(FS_WATCH_LATENCY).await; - } - - fn is_path_private(&self, path: &RelPath) -> bool { - !self.share_private_files && self.settings.is_path_private(path) - } - - async fn next_scan_request(&self) -> Result { - let mut request = self.scan_requests_rx.recv().await?; - while let Ok(next_request) = self.scan_requests_rx.try_recv() { - request.relative_paths.extend(next_request.relative_paths); - request.done.extend(next_request.done); - } - Ok(request) - } -} - -async fn discover_ancestor_git_repo( - fs: Arc, - root_abs_path: &SanitizedPath, -) -> ( - HashMap, (Arc, bool)>, - Option<(PathBuf, WorkDirectory)>, -) { - let mut ignores = HashMap::default(); - for (index, ancestor) in root_abs_path.as_path().ancestors().enumerate() { - if index != 0 { - if ancestor == paths::home_dir() { - // Unless $HOME is itself the worktree root, don't consider it as a - // containing git repository---expensive and likely unwanted. - break; - } else if let Ok(ignore) = build_gitignore(&ancestor.join(GITIGNORE), fs.as_ref()).await - { - ignores.insert(ancestor.into(), (ignore.into(), false)); - } - } - - let ancestor_dot_git = ancestor.join(DOT_GIT); - log::trace!("considering ancestor: {ancestor_dot_git:?}"); - // Check whether the directory or file called `.git` exists (in the - // case of worktrees it's a file.) - if fs - .metadata(&ancestor_dot_git) - .await - .is_ok_and(|metadata| metadata.is_some()) - { - if index != 0 { - // We canonicalize, since the FS events use the canonicalized path. - if let Some(ancestor_dot_git) = fs.canonicalize(&ancestor_dot_git).await.log_err() { - let location_in_repo = root_abs_path - .as_path() - .strip_prefix(ancestor) - .unwrap() - .into(); - log::info!("inserting parent git repo for this worktree: {location_in_repo:?}"); - // We associate the external git repo with our root folder and - // also mark where in the git repo the root folder is located. - return ( - ignores, - Some(( - ancestor_dot_git, - WorkDirectory::AboveProject { - absolute_path: ancestor.into(), - location_in_repo, - }, - )), - ); - }; - } - - // Reached root of git repository. - break; - } - } - - (ignores, None) -} - -fn build_diff( - phase: BackgroundScannerPhase, - old_snapshot: &Snapshot, - new_snapshot: &Snapshot, - event_paths: &[Arc], -) -> UpdatedEntriesSet { - use BackgroundScannerPhase::*; - use PathChange::{Added, AddedOrUpdated, Loaded, Removed, Updated}; - - // Identify which paths have changed. Use the known set of changed - // parent paths to optimize the search. - let mut changes = Vec::new(); - let mut old_paths = old_snapshot.entries_by_path.cursor::(()); - let mut new_paths = new_snapshot.entries_by_path.cursor::(()); - let mut last_newly_loaded_dir_path = None; - old_paths.next(); - new_paths.next(); - for path in event_paths { - let path = PathKey(path.clone()); - if old_paths.item().is_some_and(|e| e.path < path.0) { - old_paths.seek_forward(&path, Bias::Left); - } - if new_paths.item().is_some_and(|e| e.path < path.0) { - new_paths.seek_forward(&path, Bias::Left); - } - loop { - match (old_paths.item(), new_paths.item()) { - (Some(old_entry), Some(new_entry)) => { - if old_entry.path > path.0 - && new_entry.path > path.0 - && !old_entry.path.starts_with(&path.0) - && !new_entry.path.starts_with(&path.0) - { - break; - } - - match Ord::cmp(&old_entry.path, &new_entry.path) { - Ordering::Less => { - changes.push((old_entry.path.clone(), old_entry.id, Removed)); - old_paths.next(); - } - Ordering::Equal => { - if phase == EventsReceivedDuringInitialScan { - if old_entry.id != new_entry.id { - changes.push((old_entry.path.clone(), old_entry.id, Removed)); - } - // If the worktree was not fully initialized when this event was generated, - // we can't know whether this entry was added during the scan or whether - // it was merely updated. - changes.push(( - new_entry.path.clone(), - new_entry.id, - AddedOrUpdated, - )); - } else if old_entry.id != new_entry.id { - changes.push((old_entry.path.clone(), old_entry.id, Removed)); - changes.push((new_entry.path.clone(), new_entry.id, Added)); - } else if old_entry != new_entry { - if old_entry.kind.is_unloaded() { - last_newly_loaded_dir_path = Some(&new_entry.path); - changes.push((new_entry.path.clone(), new_entry.id, Loaded)); - } else { - changes.push((new_entry.path.clone(), new_entry.id, Updated)); - } - } - old_paths.next(); - new_paths.next(); - } - Ordering::Greater => { - let is_newly_loaded = phase == InitialScan - || last_newly_loaded_dir_path - .as_ref() - .is_some_and(|dir| new_entry.path.starts_with(dir)); - changes.push(( - new_entry.path.clone(), - new_entry.id, - if is_newly_loaded { Loaded } else { Added }, - )); - new_paths.next(); - } - } - } - (Some(old_entry), None) => { - changes.push((old_entry.path.clone(), old_entry.id, Removed)); - old_paths.next(); - } - (None, Some(new_entry)) => { - let is_newly_loaded = phase == InitialScan - || last_newly_loaded_dir_path - .as_ref() - .is_some_and(|dir| new_entry.path.starts_with(dir)); - changes.push(( - new_entry.path.clone(), - new_entry.id, - if is_newly_loaded { Loaded } else { Added }, - )); - new_paths.next(); - } - (None, None) => break, - } - } - } - - changes.into() -} - -fn swap_to_front(child_paths: &mut Vec, file: &str) { - let position = child_paths - .iter() - .position(|path| path.file_name().unwrap() == file); - if let Some(position) = position { - let temp = child_paths.remove(position); - child_paths.insert(0, temp); - } -} - -fn char_bag_for_path(root_char_bag: CharBag, path: &RelPath) -> CharBag { - let mut result = root_char_bag; - result.extend(path.as_unix_str().chars().map(|c| c.to_ascii_lowercase())); - result -} - -#[derive(Debug)] -struct ScanJob { - abs_path: Arc, - path: Arc, - ignore_stack: IgnoreStack, - scan_queue: Sender, - ancestor_inodes: TreeSet, - is_external: bool, -} - -struct UpdateIgnoreStatusJob { - abs_path: Arc, - ignore_stack: IgnoreStack, - ignore_queue: Sender, - scan_queue: Sender, -} - -pub trait WorktreeModelHandle { - #[cfg(any(test, feature = "test-support"))] - fn flush_fs_events<'a>( - &self, - cx: &'a mut gpui::TestAppContext, - ) -> futures::future::LocalBoxFuture<'a, ()>; - - #[cfg(any(test, feature = "test-support"))] - fn flush_fs_events_in_root_git_repository<'a>( - &self, - cx: &'a mut gpui::TestAppContext, - ) -> futures::future::LocalBoxFuture<'a, ()>; -} - -impl WorktreeModelHandle for Entity { - // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that - // occurred before the worktree was constructed. These events can cause the worktree to perform - // extra directory scans, and emit extra scan-state notifications. - // - // This function mutates the worktree's directory and waits for those mutations to be picked up, - // to ensure that all redundant FS events have already been processed. - #[cfg(any(test, feature = "test-support"))] - fn flush_fs_events<'a>( - &self, - cx: &'a mut gpui::TestAppContext, - ) -> futures::future::LocalBoxFuture<'a, ()> { - let file_name = "fs-event-sentinel"; - - let tree = self.clone(); - let (fs, root_path) = self.read_with(cx, |tree, _| { - let tree = tree.as_local().unwrap(); - (tree.fs.clone(), tree.abs_path.clone()) - }); - - async move { - fs.create_file(&root_path.join(file_name), Default::default()) - .await - .unwrap(); - - let mut events = cx.events(&tree); - while events.next().await.is_some() { - if tree.read_with(cx, |tree, _| { - tree.entry_for_path(RelPath::unix(file_name).unwrap()) - .is_some() - }) { - break; - } - } - - fs.remove_file(&root_path.join(file_name), Default::default()) - .await - .unwrap(); - while events.next().await.is_some() { - if tree.read_with(cx, |tree, _| { - tree.entry_for_path(RelPath::unix(file_name).unwrap()) - .is_none() - }) { - break; - } - } - - cx.update(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - } - .boxed_local() - } - - // This function is similar to flush_fs_events, except that it waits for events to be flushed in - // the .git folder of the root repository. - // The reason for its existence is that a repository's .git folder might live *outside* of the - // worktree and thus its FS events might go through a different path. - // In order to flush those, we need to create artificial events in the .git folder and wait - // for the repository to be reloaded. - #[cfg(any(test, feature = "test-support"))] - fn flush_fs_events_in_root_git_repository<'a>( - &self, - cx: &'a mut gpui::TestAppContext, - ) -> futures::future::LocalBoxFuture<'a, ()> { - let file_name = "fs-event-sentinel"; - - let tree = self.clone(); - let (fs, root_path, mut git_dir_scan_id) = self.read_with(cx, |tree, _| { - let tree = tree.as_local().unwrap(); - let local_repo_entry = tree - .git_repositories - .values() - .min_by_key(|local_repo_entry| local_repo_entry.work_directory.clone()) - .unwrap(); - ( - tree.fs.clone(), - local_repo_entry.common_dir_abs_path.clone(), - local_repo_entry.git_dir_scan_id, - ) - }); - - let scan_id_increased = |tree: &mut Worktree, git_dir_scan_id: &mut usize| { - let tree = tree.as_local().unwrap(); - // let repository = tree.repositories.first().unwrap(); - let local_repo_entry = tree - .git_repositories - .values() - .min_by_key(|local_repo_entry| local_repo_entry.work_directory.clone()) - .unwrap(); - - if local_repo_entry.git_dir_scan_id > *git_dir_scan_id { - *git_dir_scan_id = local_repo_entry.git_dir_scan_id; - true - } else { - false - } - }; - - async move { - fs.create_file(&root_path.join(file_name), Default::default()) - .await - .unwrap(); - - let mut events = cx.events(&tree); - while events.next().await.is_some() { - if tree.update(cx, |tree, _| scan_id_increased(tree, &mut git_dir_scan_id)) { - break; - } - } - - fs.remove_file(&root_path.join(file_name), Default::default()) - .await - .unwrap(); - - while events.next().await.is_some() { - if tree.update(cx, |tree, _| scan_id_increased(tree, &mut git_dir_scan_id)) { - break; - } - } - - cx.update(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - } - .boxed_local() - } -} - -#[derive(Clone, Debug)] -struct TraversalProgress<'a> { - max_path: &'a RelPath, - count: usize, - non_ignored_count: usize, - file_count: usize, - non_ignored_file_count: usize, -} - -impl TraversalProgress<'_> { - fn count(&self, include_files: bool, include_dirs: bool, include_ignored: bool) -> usize { - match (include_files, include_dirs, include_ignored) { - (true, true, true) => self.count, - (true, true, false) => self.non_ignored_count, - (true, false, true) => self.file_count, - (true, false, false) => self.non_ignored_file_count, - (false, true, true) => self.count - self.file_count, - (false, true, false) => self.non_ignored_count - self.non_ignored_file_count, - (false, false, _) => 0, - } - } -} - -impl<'a> sum_tree::Dimension<'a, EntrySummary> for TraversalProgress<'a> { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a EntrySummary, _: ()) { - self.max_path = summary.max_path.as_ref(); - self.count += summary.count; - self.non_ignored_count += summary.non_ignored_count; - self.file_count += summary.file_count; - self.non_ignored_file_count += summary.non_ignored_file_count; - } -} - -impl Default for TraversalProgress<'_> { - fn default() -> Self { - Self { - max_path: RelPath::empty(), - count: 0, - non_ignored_count: 0, - file_count: 0, - non_ignored_file_count: 0, - } - } -} - -#[derive(Debug)] -pub struct Traversal<'a> { - snapshot: &'a Snapshot, - cursor: sum_tree::Cursor<'a, 'static, Entry, TraversalProgress<'a>>, - include_ignored: bool, - include_files: bool, - include_dirs: bool, -} - -impl<'a> Traversal<'a> { - fn new( - snapshot: &'a Snapshot, - include_files: bool, - include_dirs: bool, - include_ignored: bool, - start_path: &RelPath, - ) -> Self { - let mut cursor = snapshot.entries_by_path.cursor(()); - cursor.seek(&TraversalTarget::path(start_path), Bias::Left); - let mut traversal = Self { - snapshot, - cursor, - include_files, - include_dirs, - include_ignored, - }; - if traversal.end_offset() == traversal.start_offset() { - traversal.next(); - } - traversal - } - - pub fn advance(&mut self) -> bool { - self.advance_by(1) - } - - pub fn advance_by(&mut self, count: usize) -> bool { - self.cursor.seek_forward( - &TraversalTarget::Count { - count: self.end_offset() + count, - include_dirs: self.include_dirs, - include_files: self.include_files, - include_ignored: self.include_ignored, - }, - Bias::Left, - ) - } - - pub fn advance_to_sibling(&mut self) -> bool { - while let Some(entry) = self.cursor.item() { - self.cursor - .seek_forward(&TraversalTarget::successor(&entry.path), Bias::Left); - if let Some(entry) = self.cursor.item() - && (self.include_files || !entry.is_file()) - && (self.include_dirs || !entry.is_dir()) - && (self.include_ignored || !entry.is_ignored || entry.is_always_included) - { - return true; - } - } - false - } - - pub fn back_to_parent(&mut self) -> bool { - let Some(parent_path) = self.cursor.item().and_then(|entry| entry.path.parent()) else { - return false; - }; - self.cursor - .seek(&TraversalTarget::path(parent_path), Bias::Left) - } - - pub fn entry(&self) -> Option<&'a Entry> { - self.cursor.item() - } - - pub fn snapshot(&self) -> &'a Snapshot { - self.snapshot - } - - pub fn start_offset(&self) -> usize { - self.cursor - .start() - .count(self.include_files, self.include_dirs, self.include_ignored) - } - - pub fn end_offset(&self) -> usize { - self.cursor - .end() - .count(self.include_files, self.include_dirs, self.include_ignored) - } -} - -impl<'a> Iterator for Traversal<'a> { - type Item = &'a Entry; - - fn next(&mut self) -> Option { - if let Some(item) = self.entry() { - self.advance(); - Some(item) - } else { - None - } - } -} - -#[derive(Debug, Clone, Copy)] -pub enum PathTarget<'a> { - Path(&'a RelPath), - Successor(&'a RelPath), -} - -impl PathTarget<'_> { - fn cmp_path(&self, other: &RelPath) -> Ordering { - match self { - PathTarget::Path(path) => path.cmp(&other), - PathTarget::Successor(path) => { - if other.starts_with(path) { - Ordering::Greater - } else { - Ordering::Equal - } - } - } - } -} - -impl<'a, S: Summary> SeekTarget<'a, PathSummary, PathProgress<'a>> for PathTarget<'_> { - fn cmp(&self, cursor_location: &PathProgress<'a>, _: S::Context<'_>) -> Ordering { - self.cmp_path(cursor_location.max_path) - } -} - -impl<'a, S: Summary> SeekTarget<'a, PathSummary, TraversalProgress<'a>> for PathTarget<'_> { - fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: S::Context<'_>) -> Ordering { - self.cmp_path(cursor_location.max_path) - } -} - -#[derive(Debug)] -enum TraversalTarget<'a> { - Path(PathTarget<'a>), - Count { - count: usize, - include_files: bool, - include_ignored: bool, - include_dirs: bool, - }, -} - -impl<'a> TraversalTarget<'a> { - fn path(path: &'a RelPath) -> Self { - Self::Path(PathTarget::Path(path)) - } - - fn successor(path: &'a RelPath) -> Self { - Self::Path(PathTarget::Successor(path)) - } - - fn cmp_progress(&self, progress: &TraversalProgress) -> Ordering { - match self { - TraversalTarget::Path(path) => path.cmp_path(progress.max_path), - TraversalTarget::Count { - count, - include_files, - include_dirs, - include_ignored, - } => Ord::cmp( - count, - &progress.count(*include_files, *include_dirs, *include_ignored), - ), - } - } -} - -impl<'a> SeekTarget<'a, EntrySummary, TraversalProgress<'a>> for TraversalTarget<'_> { - fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: ()) -> Ordering { - self.cmp_progress(cursor_location) - } -} - -impl<'a> SeekTarget<'a, PathSummary, TraversalProgress<'a>> - for TraversalTarget<'_> -{ - fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: ()) -> Ordering { - self.cmp_progress(cursor_location) - } -} - -pub struct ChildEntriesOptions { - pub include_files: bool, - pub include_dirs: bool, - pub include_ignored: bool, -} - -pub struct ChildEntriesIter<'a> { - parent_path: &'a RelPath, - traversal: Traversal<'a>, -} - -impl<'a> Iterator for ChildEntriesIter<'a> { - type Item = &'a Entry; - - 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); - } - None - } -} - -impl<'a> From<&'a Entry> for proto::Entry { - fn from(entry: &'a Entry) -> Self { - Self { - id: entry.id.to_proto(), - is_dir: entry.is_dir(), - path: entry.path.as_ref().to_proto(), - inode: entry.inode, - mtime: entry.mtime.map(|time| time.into()), - is_ignored: entry.is_ignored, - is_hidden: entry.is_hidden, - is_external: entry.is_external, - is_fifo: entry.is_fifo, - size: Some(entry.size), - canonical_path: entry - .canonical_path - .as_ref() - .map(|path| path.to_string_lossy().into_owned()), - } - } -} - -impl TryFrom<(&CharBag, &PathMatcher, proto::Entry)> for Entry { - type Error = anyhow::Error; - - fn try_from( - (root_char_bag, always_included, entry): (&CharBag, &PathMatcher, proto::Entry), - ) -> Result { - let kind = if entry.is_dir { - EntryKind::Dir - } else { - EntryKind::File - }; - - let path = - RelPath::from_proto(&entry.path).context("invalid relative path in proto message")?; - let char_bag = char_bag_for_path(*root_char_bag, &path); - let is_always_included = always_included.is_match(&path); - Ok(Entry { - id: ProjectEntryId::from_proto(entry.id), - kind, - path, - inode: entry.inode, - mtime: entry.mtime.map(|time| time.into()), - size: entry.size.unwrap_or(0), - canonical_path: entry - .canonical_path - .map(|path_string| Arc::from(PathBuf::from(path_string))), - is_ignored: entry.is_ignored, - is_hidden: entry.is_hidden, - is_always_included, - is_external: entry.is_external, - is_private: false, - char_bag, - is_fifo: entry.is_fifo, - }) - } -} - -#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub struct ProjectEntryId(usize); - -impl ProjectEntryId { - pub const MAX: Self = Self(usize::MAX); - pub const MIN: Self = Self(usize::MIN); - - pub fn new(counter: &AtomicUsize) -> Self { - Self(counter.fetch_add(1, SeqCst)) - } - - pub fn from_proto(id: u64) -> Self { - Self(id as usize) - } - - pub fn to_proto(self) -> u64 { - self.0 as u64 - } - - pub fn from_usize(id: usize) -> Self { - ProjectEntryId(id) - } - - pub fn to_usize(self) -> usize { - self.0 - } -} - -#[cfg(any(test, feature = "test-support"))] -impl CreatedEntry { - pub fn into_included(self) -> Option { - match self { - CreatedEntry::Included(entry) => Some(entry), - CreatedEntry::Excluded { .. } => None, - } - } -} - -fn parse_gitfile(content: &str) -> anyhow::Result<&Path> { - let path = content - .strip_prefix("gitdir:") - .with_context(|| format!("parsing gitfile content {content:?}"))?; - Ok(Path::new(path.trim())) -} - -async fn discover_git_paths(dot_git_abs_path: &Arc, fs: &dyn Fs) -> (Arc, Arc) { - let mut repository_dir_abs_path = dot_git_abs_path.clone(); - let mut common_dir_abs_path = dot_git_abs_path.clone(); - - if let Some(path) = fs - .load(dot_git_abs_path) - .await - .ok() - .as_ref() - .and_then(|contents| parse_gitfile(contents).log_err()) - { - let path = dot_git_abs_path - .parent() - .unwrap_or(Path::new("")) - .join(path); - if let Some(path) = fs.canonicalize(&path).await.log_err() { - repository_dir_abs_path = Path::new(&path).into(); - common_dir_abs_path = repository_dir_abs_path.clone(); - - if let Some(commondir_contents) = fs.load(&path.join("commondir")).await.ok() - && let Some(commondir_path) = fs - .canonicalize(&path.join(commondir_contents.trim())) - .await - .log_err() - { - common_dir_abs_path = commondir_path.as_path().into(); - } - } - }; - (repository_dir_abs_path, common_dir_abs_path) -} - -struct NullWatcher; - -impl fs::Watcher for NullWatcher { - fn add(&self, _path: &Path) -> Result<()> { - Ok(()) - } - - fn remove(&self, _path: &Path) -> Result<()> { - Ok(()) - } -} diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs deleted file mode 100644 index a86720184e..0000000000 --- a/crates/worktree/src/worktree_settings.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::path::Path; - -use anyhow::Context as _; -use settings::{RegisterSetting, Settings}; -use util::{ - ResultExt, - paths::{PathMatcher, PathStyle}, - rel_path::RelPath, -}; - -#[derive(Clone, PartialEq, Eq, RegisterSetting)] -pub struct WorktreeSettings { - pub project_name: Option, - /// Whether to prevent this project from being shared in public channels. - pub prevent_sharing_in_public_channels: bool, - pub file_scan_exclusions: PathMatcher, - pub file_scan_inclusions: PathMatcher, - /// This field contains all ancestors of the `file_scan_inclusions`. It's used to - /// determine whether to terminate worktree scanning for a given dir. - pub parent_dir_scan_inclusions: PathMatcher, - pub private_files: PathMatcher, - pub hidden_files: PathMatcher, -} - -impl WorktreeSettings { - pub fn is_path_private(&self, path: &RelPath) -> bool { - path.ancestors() - .any(|ancestor| self.private_files.is_match(ancestor)) - } - - pub fn is_path_excluded(&self, path: &RelPath) -> bool { - path.ancestors() - .any(|ancestor| self.file_scan_exclusions.is_match(ancestor)) - } - - pub fn is_path_always_included(&self, path: &RelPath, is_dir: bool) -> bool { - if is_dir { - self.parent_dir_scan_inclusions.is_match(path) - } else { - self.file_scan_inclusions.is_match(path) - } - } - - pub fn is_path_hidden(&self, path: &RelPath) -> bool { - path.ancestors() - .any(|ancestor| self.hidden_files.is_match(ancestor)) - } -} - -impl Settings for WorktreeSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let worktree = content.project.worktree.clone(); - let file_scan_exclusions = worktree.file_scan_exclusions.unwrap(); - let file_scan_inclusions = worktree.file_scan_inclusions.unwrap(); - let private_files = worktree.private_files.unwrap().0; - let hidden_files = worktree.hidden_files.unwrap(); - let parsed_file_scan_inclusions: Vec = file_scan_inclusions - .iter() - .flat_map(|glob| { - Path::new(glob) - .ancestors() - .skip(1) - .map(|a| a.to_string_lossy().into()) - }) - .filter(|p: &String| !p.is_empty()) - .collect(); - - Self { - project_name: worktree.project_name, - prevent_sharing_in_public_channels: worktree.prevent_sharing_in_public_channels, - file_scan_exclusions: path_matchers(file_scan_exclusions, "file_scan_exclusions") - .log_err() - .unwrap_or_default(), - parent_dir_scan_inclusions: path_matchers( - parsed_file_scan_inclusions, - "file_scan_inclusions", - ) - .unwrap(), - file_scan_inclusions: path_matchers(file_scan_inclusions, "file_scan_inclusions") - .unwrap(), - private_files: path_matchers(private_files, "private_files") - .log_err() - .unwrap_or_default(), - hidden_files: path_matchers(hidden_files, "hidden_files") - .log_err() - .unwrap_or_default(), - } - } -} - -fn path_matchers(mut values: Vec, context: &'static str) -> anyhow::Result { - values.sort(); - PathMatcher::new(values, PathStyle::local()) - .with_context(|| format!("Failed to parse globs from {}", context)) -} diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs deleted file mode 100644 index e58e99ea68..0000000000 --- a/crates/worktree/src/worktree_tests.rs +++ /dev/null @@ -1,2466 +0,0 @@ -use crate::{Entry, EntryKind, Event, PathChange, Worktree, WorktreeModelHandle}; -use anyhow::Result; -use fs::{FakeFs, Fs, RealFs, RemoveOptions}; -use git::GITIGNORE; -use gpui::{AppContext as _, BackgroundExecutor, BorrowAppContext, Context, Task, TestAppContext}; -use parking_lot::Mutex; -use postage::stream::Stream; -use pretty_assertions::assert_eq; -use rand::prelude::*; - -use serde_json::json; -use settings::SettingsStore; -use std::{ - env, - fmt::Write, - mem, - path::{Path, PathBuf}, - sync::Arc, -}; -use util::{ - ResultExt, path, - rel_path::{RelPath, rel_path}, - test::TempTree, -}; - -#[gpui::test] -async fn test_traversal(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/root", - json!({ - ".gitignore": "a/b\n", - "a": { - "b": "", - "c": "", - } - }), - ) - .await; - - let tree = Worktree::local( - Path::new("/root"), - true, - fs, - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(false, 0) - .map(|entry| entry.path.as_ref()) - .collect::>(), - vec![ - rel_path(""), - rel_path(".gitignore"), - rel_path("a"), - rel_path("a/c"), - ] - ); - assert_eq!( - tree.entries(true, 0) - .map(|entry| entry.path.as_ref()) - .collect::>(), - vec![ - rel_path(""), - rel_path(".gitignore"), - rel_path("a"), - rel_path("a/b"), - rel_path("a/c"), - ] - ); - }) -} - -#[gpui::test(iterations = 10)] -async fn test_circular_symlinks(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/root", - json!({ - "lib": { - "a": { - "a.txt": "" - }, - "b": { - "b.txt": "" - } - } - }), - ) - .await; - fs.create_symlink("/root/lib/a/lib".as_ref(), "..".into()) - .await - .unwrap(); - fs.create_symlink("/root/lib/b/lib".as_ref(), "..".into()) - .await - .unwrap(); - - let tree = Worktree::local( - Path::new("/root"), - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(false, 0) - .map(|entry| entry.path.as_ref()) - .collect::>(), - vec![ - rel_path(""), - rel_path("lib"), - rel_path("lib/a"), - rel_path("lib/a/a.txt"), - rel_path("lib/a/lib"), - rel_path("lib/b"), - rel_path("lib/b/b.txt"), - rel_path("lib/b/lib"), - ] - ); - }); - - fs.rename( - Path::new("/root/lib/a/lib"), - Path::new("/root/lib/a/lib-2"), - Default::default(), - ) - .await - .unwrap(); - cx.executor().run_until_parked(); - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(false, 0) - .map(|entry| entry.path.as_ref()) - .collect::>(), - vec![ - rel_path(""), - rel_path("lib"), - rel_path("lib/a"), - rel_path("lib/a/a.txt"), - rel_path("lib/a/lib-2"), - rel_path("lib/b"), - rel_path("lib/b/b.txt"), - rel_path("lib/b/lib"), - ] - ); - }); -} - -#[gpui::test] -async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/root", - json!({ - "dir1": { - "deps": { - // symlinks here - }, - "src": { - "a.rs": "", - "b.rs": "", - }, - }, - "dir2": { - "src": { - "c.rs": "", - "d.rs": "", - } - }, - "dir3": { - "deps": {}, - "src": { - "e.rs": "", - "f.rs": "", - }, - } - }), - ) - .await; - - // These symlinks point to directories outside of the worktree's root, dir1. - fs.create_symlink("/root/dir1/deps/dep-dir2".as_ref(), "../../dir2".into()) - .await - .unwrap(); - fs.create_symlink("/root/dir1/deps/dep-dir3".as_ref(), "../../dir3".into()) - .await - .unwrap(); - - let tree = Worktree::local( - Path::new("/root/dir1"), - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - - let tree_updates = Arc::new(Mutex::new(Vec::new())); - tree.update(cx, |_, cx| { - let tree_updates = tree_updates.clone(); - cx.subscribe(&tree, move |_, _, event, _| { - if let Event::UpdatedEntries(update) = event { - tree_updates.lock().extend( - update - .iter() - .map(|(path, _, change)| (path.clone(), *change)), - ); - } - }) - .detach(); - }); - - // The symlinked directories are not scanned by default. - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|entry| (entry.path.as_ref(), entry.is_external)) - .collect::>(), - vec![ - (rel_path(""), false), - (rel_path("deps"), false), - (rel_path("deps/dep-dir2"), true), - (rel_path("deps/dep-dir3"), true), - (rel_path("src"), false), - (rel_path("src/a.rs"), false), - (rel_path("src/b.rs"), false), - ] - ); - - assert_eq!( - tree.entry_for_path(rel_path("deps/dep-dir2")).unwrap().kind, - EntryKind::UnloadedDir - ); - }); - - // Expand one of the symlinked directories. - tree.read_with(cx, |tree, _| { - tree.as_local() - .unwrap() - .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3").into()]) - }) - .recv() - .await; - - // The expanded directory's contents are loaded. Subdirectories are - // not scanned yet. - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|entry| (entry.path.as_ref(), entry.is_external)) - .collect::>(), - vec![ - (rel_path(""), false), - (rel_path("deps"), false), - (rel_path("deps/dep-dir2"), true), - (rel_path("deps/dep-dir3"), true), - (rel_path("deps/dep-dir3/deps"), true), - (rel_path("deps/dep-dir3/src"), true), - (rel_path("src"), false), - (rel_path("src/a.rs"), false), - (rel_path("src/b.rs"), false), - ] - ); - }); - assert_eq!( - mem::take(&mut *tree_updates.lock()), - &[ - (rel_path("deps/dep-dir3").into(), PathChange::Loaded), - (rel_path("deps/dep-dir3/deps").into(), PathChange::Loaded), - (rel_path("deps/dep-dir3/src").into(), PathChange::Loaded) - ] - ); - - // Expand a subdirectory of one of the symlinked directories. - tree.read_with(cx, |tree, _| { - tree.as_local() - .unwrap() - .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3/src").into()]) - }) - .recv() - .await; - - // The expanded subdirectory's contents are loaded. - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|entry| (entry.path.as_ref(), entry.is_external)) - .collect::>(), - vec![ - (rel_path(""), false), - (rel_path("deps"), false), - (rel_path("deps/dep-dir2"), true), - (rel_path("deps/dep-dir3"), true), - (rel_path("deps/dep-dir3/deps"), true), - (rel_path("deps/dep-dir3/src"), true), - (rel_path("deps/dep-dir3/src/e.rs"), true), - (rel_path("deps/dep-dir3/src/f.rs"), true), - (rel_path("src"), false), - (rel_path("src/a.rs"), false), - (rel_path("src/b.rs"), false), - ] - ); - }); - - assert_eq!( - mem::take(&mut *tree_updates.lock()), - &[ - (rel_path("deps/dep-dir3/src").into(), PathChange::Loaded), - ( - rel_path("deps/dep-dir3/src/e.rs").into(), - PathChange::Loaded - ), - ( - rel_path("deps/dep-dir3/src/f.rs").into(), - PathChange::Loaded - ) - ] - ); -} - -#[cfg(target_os = "macos")] -#[gpui::test] -async fn test_renaming_case_only(cx: &mut TestAppContext) { - cx.executor().allow_parking(); - init_test(cx); - - const OLD_NAME: &str = "aaa.rs"; - const NEW_NAME: &str = "AAA.rs"; - - let fs = Arc::new(RealFs::new(None, cx.executor())); - let temp_root = TempTree::new(json!({ - OLD_NAME: "", - })); - - let tree = Worktree::local( - temp_root.path(), - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|entry| entry.path.as_ref()) - .collect::>(), - vec![rel_path(""), rel_path(OLD_NAME)] - ); - }); - - fs.rename( - &temp_root.path().join(OLD_NAME), - &temp_root.path().join(NEW_NAME), - fs::RenameOptions { - overwrite: true, - ignore_if_exists: true, - create_parents: false, - }, - ) - .await - .unwrap(); - - tree.flush_fs_events(cx).await; - - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|entry| entry.path.as_ref()) - .collect::>(), - vec![rel_path(""), rel_path(NEW_NAME)] - ); - }); -} - -#[gpui::test] -async fn test_open_gitignored_files(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/root", - json!({ - ".gitignore": "node_modules\n", - "one": { - "node_modules": { - "a": { - "a1.js": "a1", - "a2.js": "a2", - }, - "b": { - "b1.js": "b1", - "b2.js": "b2", - }, - "c": { - "c1.js": "c1", - "c2.js": "c2", - } - }, - }, - "two": { - "x.js": "", - "y.js": "", - }, - }), - ) - .await; - - let tree = Worktree::local( - Path::new("/root"), - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|entry| (entry.path.as_ref(), entry.is_ignored)) - .collect::>(), - vec![ - (rel_path(""), false), - (rel_path(".gitignore"), false), - (rel_path("one"), false), - (rel_path("one/node_modules"), true), - (rel_path("two"), false), - (rel_path("two/x.js"), false), - (rel_path("two/y.js"), false), - ] - ); - }); - - // Open a file that is nested inside of a gitignored directory that - // has not yet been expanded. - let prev_read_dir_count = fs.read_dir_call_count(); - let loaded = tree - .update(cx, |tree, cx| { - tree.load_file(rel_path("one/node_modules/b/b1.js"), cx) - }) - .await - .unwrap(); - - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|entry| (entry.path.as_ref(), entry.is_ignored)) - .collect::>(), - vec![ - (rel_path(""), false), - (rel_path(".gitignore"), false), - (rel_path("one"), false), - (rel_path("one/node_modules"), true), - (rel_path("one/node_modules/a"), true), - (rel_path("one/node_modules/b"), true), - (rel_path("one/node_modules/b/b1.js"), true), - (rel_path("one/node_modules/b/b2.js"), true), - (rel_path("one/node_modules/c"), true), - (rel_path("two"), false), - (rel_path("two/x.js"), false), - (rel_path("two/y.js"), false), - ] - ); - - assert_eq!( - loaded.file.path.as_ref(), - rel_path("one/node_modules/b/b1.js") - ); - - // Only the newly-expanded directories are scanned. - assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2); - }); - - // Open another file in a different subdirectory of the same - // gitignored directory. - let prev_read_dir_count = fs.read_dir_call_count(); - let loaded = tree - .update(cx, |tree, cx| { - tree.load_file(rel_path("one/node_modules/a/a2.js"), cx) - }) - .await - .unwrap(); - - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|entry| (entry.path.as_ref(), entry.is_ignored)) - .collect::>(), - vec![ - (rel_path(""), false), - (rel_path(".gitignore"), false), - (rel_path("one"), false), - (rel_path("one/node_modules"), true), - (rel_path("one/node_modules/a"), true), - (rel_path("one/node_modules/a/a1.js"), true), - (rel_path("one/node_modules/a/a2.js"), true), - (rel_path("one/node_modules/b"), true), - (rel_path("one/node_modules/b/b1.js"), true), - (rel_path("one/node_modules/b/b2.js"), true), - (rel_path("one/node_modules/c"), true), - (rel_path("two"), false), - (rel_path("two/x.js"), false), - (rel_path("two/y.js"), false), - ] - ); - - assert_eq!( - loaded.file.path.as_ref(), - rel_path("one/node_modules/a/a2.js") - ); - - // Only the newly-expanded directory is scanned. - assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1); - }); - - let path = PathBuf::from("/root/one/node_modules/c/lib"); - - // No work happens when files and directories change within an unloaded directory. - let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count(); - // When we open a directory, we check each ancestor whether it's a git - // repository. That means we have an fs.metadata call per ancestor that we - // need to subtract here. - let ancestors = path.ancestors().count(); - - fs.create_dir(path.as_ref()).await.unwrap(); - cx.executor().run_until_parked(); - - assert_eq!( - fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count - ancestors, - 0 - ); -} - -#[gpui::test] -async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/root", - json!({ - ".gitignore": "node_modules\n", - "a": { - "a.js": "", - }, - "b": { - "b.js": "", - }, - "node_modules": { - "c": { - "c.js": "", - }, - "d": { - "d.js": "", - "e": { - "e1.js": "", - "e2.js": "", - }, - "f": { - "f1.js": "", - "f2.js": "", - } - }, - }, - }), - ) - .await; - - let tree = Worktree::local( - Path::new("/root"), - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - - // Open a file within the gitignored directory, forcing some of its - // subdirectories to be read, but not all. - let read_dir_count_1 = fs.read_dir_call_count(); - tree.read_with(cx, |tree, _| { - tree.as_local() - .unwrap() - .refresh_entries_for_paths(vec![rel_path("node_modules/d/d.js").into()]) - }) - .recv() - .await; - - // Those subdirectories are now loaded. - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|e| (e.path.as_ref(), e.is_ignored)) - .collect::>(), - &[ - (rel_path(""), false), - (rel_path(".gitignore"), false), - (rel_path("a"), false), - (rel_path("a/a.js"), false), - (rel_path("b"), false), - (rel_path("b/b.js"), false), - (rel_path("node_modules"), true), - (rel_path("node_modules/c"), true), - (rel_path("node_modules/d"), true), - (rel_path("node_modules/d/d.js"), true), - (rel_path("node_modules/d/e"), true), - (rel_path("node_modules/d/f"), true), - ] - ); - }); - let read_dir_count_2 = fs.read_dir_call_count(); - assert_eq!(read_dir_count_2 - read_dir_count_1, 2); - - // Update the gitignore so that node_modules is no longer ignored, - // but a subdirectory is ignored - fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default()) - .await - .unwrap(); - cx.executor().run_until_parked(); - - // All of the directories that are no longer ignored are now loaded. - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|e| (e.path.as_ref(), e.is_ignored)) - .collect::>(), - &[ - (rel_path(""), false), - (rel_path(".gitignore"), false), - (rel_path("a"), false), - (rel_path("a/a.js"), false), - (rel_path("b"), false), - (rel_path("b/b.js"), false), - // This directory is no longer ignored - (rel_path("node_modules"), false), - (rel_path("node_modules/c"), false), - (rel_path("node_modules/c/c.js"), false), - (rel_path("node_modules/d"), false), - (rel_path("node_modules/d/d.js"), false), - // This subdirectory is now ignored - (rel_path("node_modules/d/e"), true), - (rel_path("node_modules/d/f"), false), - (rel_path("node_modules/d/f/f1.js"), false), - (rel_path("node_modules/d/f/f2.js"), false), - ] - ); - }); - - // Each of the newly-loaded directories is scanned only once. - let read_dir_count_3 = fs.read_dir_call_count(); - assert_eq!(read_dir_count_3 - read_dir_count_2, 2); -} - -#[gpui::test] -async fn test_write_file(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - let dir = TempTree::new(json!({ - ".git": {}, - ".gitignore": "ignored-dir\n", - "tracked-dir": {}, - "ignored-dir": {} - })); - - let worktree = Worktree::local( - dir.path(), - true, - Arc::new(RealFs::new(None, cx.executor())), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - #[cfg(not(target_os = "macos"))] - fs::fs_watcher::global(|_| {}).unwrap(); - - cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete()) - .await; - worktree.flush_fs_events(cx).await; - - worktree - .update(cx, |tree, cx| { - tree.write_file( - rel_path("tracked-dir/file.txt").into(), - "hello".into(), - Default::default(), - cx, - ) - }) - .await - .unwrap(); - worktree - .update(cx, |tree, cx| { - tree.write_file( - rel_path("ignored-dir/file.txt").into(), - "world".into(), - Default::default(), - cx, - ) - }) - .await - .unwrap(); - worktree.read_with(cx, |tree, _| { - let tracked = tree - .entry_for_path(rel_path("tracked-dir/file.txt")) - .unwrap(); - let ignored = tree - .entry_for_path(rel_path("ignored-dir/file.txt")) - .unwrap(); - assert!(!tracked.is_ignored); - assert!(ignored.is_ignored); - }); -} - -#[gpui::test] -async fn test_file_scan_inclusions(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - let dir = TempTree::new(json!({ - ".gitignore": "**/target\n/node_modules\ntop_level.txt\n", - "target": { - "index": "blah2" - }, - "node_modules": { - ".DS_Store": "", - "prettier": { - "package.json": "{}", - }, - "package.json": "//package.json" - }, - "src": { - ".DS_Store": "", - "foo": { - "foo.rs": "mod another;\n", - "another.rs": "// another", - }, - "bar": { - "bar.rs": "// bar", - }, - "lib.rs": "mod foo;\nmod bar;\n", - }, - "top_level.txt": "top level file", - ".DS_Store": "", - })); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![]); - settings.project.worktree.file_scan_inclusions = Some(vec![ - "node_modules/**/package.json".to_string(), - "**/.DS_Store".to_string(), - ]); - }); - }); - }); - - let tree = Worktree::local( - dir.path(), - true, - Arc::new(RealFs::new(None, cx.executor())), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.flush_fs_events(cx).await; - tree.read_with(cx, |tree, _| { - // Assert that file_scan_inclusions overrides file_scan_exclusions. - check_worktree_entries( - tree, - &[], - &["target", "node_modules"], - &["src/lib.rs", "src/bar/bar.rs", ".gitignore"], - &[ - "node_modules/prettier/package.json", - ".DS_Store", - "node_modules/.DS_Store", - "src/.DS_Store", - ], - ) - }); -} - -#[gpui::test] -async fn test_file_scan_exclusions_overrules_inclusions(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - let dir = TempTree::new(json!({ - ".gitignore": "**/target\n/node_modules\n", - "target": { - "index": "blah2" - }, - "node_modules": { - ".DS_Store": "", - "prettier": { - "package.json": "{}", - }, - }, - "src": { - ".DS_Store": "", - "foo": { - "foo.rs": "mod another;\n", - "another.rs": "// another", - }, - }, - ".DS_Store": "", - })); - - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["**/.DS_Store".to_string()]); - settings.project.worktree.file_scan_inclusions = - Some(vec!["**/.DS_Store".to_string()]); - }); - }); - }); - - let tree = Worktree::local( - dir.path(), - true, - Arc::new(RealFs::new(None, cx.executor())), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.flush_fs_events(cx).await; - tree.read_with(cx, |tree, _| { - // Assert that file_scan_inclusions overrides file_scan_exclusions. - check_worktree_entries( - tree, - &[".DS_Store, src/.DS_Store"], - &["target", "node_modules"], - &["src/foo/another.rs", "src/foo/foo.rs", ".gitignore"], - &[], - ) - }); -} - -#[gpui::test] -async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - let dir = TempTree::new(json!({ - ".gitignore": "**/target\n/node_modules/\n", - "target": { - "index": "blah2" - }, - "node_modules": { - ".DS_Store": "", - "prettier": { - "package.json": "{}", - }, - }, - "src": { - ".DS_Store": "", - "foo": { - "foo.rs": "mod another;\n", - "another.rs": "// another", - }, - }, - ".DS_Store": "", - })); - - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![]); - settings.project.worktree.file_scan_inclusions = - Some(vec!["node_modules/**".to_string()]); - }); - }); - }); - let tree = Worktree::local( - dir.path(), - true, - Arc::new(RealFs::new(None, cx.executor())), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.flush_fs_events(cx).await; - - tree.read_with(cx, |tree, _| { - assert!( - tree.entry_for_path(rel_path("node_modules")) - .is_some_and(|f| f.is_always_included) - ); - assert!( - tree.entry_for_path(rel_path("node_modules/prettier/package.json")) - .is_some_and(|f| f.is_always_included) - ); - }); - - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![]); - settings.project.worktree.file_scan_inclusions = Some(vec![]); - }); - }); - }); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.flush_fs_events(cx).await; - - tree.read_with(cx, |tree, _| { - assert!( - tree.entry_for_path(rel_path("node_modules")) - .is_some_and(|f| !f.is_always_included) - ); - assert!( - tree.entry_for_path(rel_path("node_modules/prettier/package.json")) - .is_some_and(|f| !f.is_always_included) - ); - }); -} - -#[gpui::test] -async fn test_file_scan_exclusions(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - let dir = TempTree::new(json!({ - ".gitignore": "**/target\n/node_modules\n", - "target": { - "index": "blah2" - }, - "node_modules": { - ".DS_Store": "", - "prettier": { - "package.json": "{}", - }, - }, - "src": { - ".DS_Store": "", - "foo": { - "foo.rs": "mod another;\n", - "another.rs": "// another", - }, - "bar": { - "bar.rs": "// bar", - }, - "lib.rs": "mod foo;\nmod bar;\n", - }, - ".DS_Store": "", - })); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]); - }); - }); - }); - - let tree = Worktree::local( - dir.path(), - true, - Arc::new(RealFs::new(None, cx.executor())), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.flush_fs_events(cx).await; - tree.read_with(cx, |tree, _| { - check_worktree_entries( - tree, - &[ - "src/foo/foo.rs", - "src/foo/another.rs", - "node_modules/.DS_Store", - "src/.DS_Store", - ".DS_Store", - ], - &["target", "node_modules"], - &["src/lib.rs", "src/bar/bar.rs", ".gitignore"], - &[], - ) - }); - - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["**/node_modules/**".to_string()]); - }); - }); - }); - tree.flush_fs_events(cx).await; - cx.executor().run_until_parked(); - tree.read_with(cx, |tree, _| { - check_worktree_entries( - tree, - &[ - "node_modules/prettier/package.json", - "node_modules/.DS_Store", - "node_modules", - ], - &["target"], - &[ - ".gitignore", - "src/lib.rs", - "src/bar/bar.rs", - "src/foo/foo.rs", - "src/foo/another.rs", - "src/.DS_Store", - ".DS_Store", - ], - &[], - ) - }); -} - -#[gpui::test] -async fn test_hidden_files(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - let dir = TempTree::new(json!({ - ".gitignore": "**/target\n", - ".hidden_file": "content", - ".hidden_dir": { - "nested.rs": "code", - }, - "src": { - "visible.rs": "code", - }, - "logs": { - "app.log": "logs", - "debug.log": "logs", - }, - "visible.txt": "content", - })); - - let tree = Worktree::local( - dir.path(), - true, - Arc::new(RealFs::new(None, cx.executor())), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.flush_fs_events(cx).await; - - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|entry| (entry.path.as_ref(), entry.is_hidden)) - .collect::>(), - vec![ - (rel_path(""), false), - (rel_path(".gitignore"), true), - (rel_path(".hidden_dir"), true), - (rel_path(".hidden_dir/nested.rs"), true), - (rel_path(".hidden_file"), true), - (rel_path("logs"), false), - (rel_path("logs/app.log"), false), - (rel_path("logs/debug.log"), false), - (rel_path("src"), false), - (rel_path("src/visible.rs"), false), - (rel_path("visible.txt"), false), - ] - ); - }); - - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.hidden_files = Some(vec!["**/*.log".to_string()]); - }); - }); - }); - tree.flush_fs_events(cx).await; - cx.executor().run_until_parked(); - - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|entry| (entry.path.as_ref(), entry.is_hidden)) - .collect::>(), - vec![ - (rel_path(""), false), - (rel_path(".gitignore"), false), - (rel_path(".hidden_dir"), false), - (rel_path(".hidden_dir/nested.rs"), false), - (rel_path(".hidden_file"), false), - (rel_path("logs"), false), - (rel_path("logs/app.log"), true), - (rel_path("logs/debug.log"), true), - (rel_path("src"), false), - (rel_path("src/visible.rs"), false), - (rel_path("visible.txt"), false), - ] - ); - }); -} - -#[gpui::test] -async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - let dir = TempTree::new(json!({ - ".git": { - "HEAD": "ref: refs/heads/main\n", - "foo": "bar", - }, - ".gitignore": "**/target\n/node_modules\ntest_output\n", - "target": { - "index": "blah2" - }, - "node_modules": { - ".DS_Store": "", - "prettier": { - "package.json": "{}", - }, - }, - "src": { - ".DS_Store": "", - "foo": { - "foo.rs": "mod another;\n", - "another.rs": "// another", - }, - "bar": { - "bar.rs": "// bar", - }, - "lib.rs": "mod foo;\nmod bar;\n", - }, - ".DS_Store": "", - })); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![ - "**/.git".to_string(), - "node_modules/".to_string(), - "build_output".to_string(), - ]); - }); - }); - }); - - let tree = Worktree::local( - dir.path(), - true, - Arc::new(RealFs::new(None, cx.executor())), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.flush_fs_events(cx).await; - tree.read_with(cx, |tree, _| { - check_worktree_entries( - tree, - &[ - ".git/HEAD", - ".git/foo", - "node_modules", - "node_modules/.DS_Store", - "node_modules/prettier", - "node_modules/prettier/package.json", - ], - &["target"], - &[ - ".DS_Store", - "src/.DS_Store", - "src/lib.rs", - "src/foo/foo.rs", - "src/foo/another.rs", - "src/bar/bar.rs", - ".gitignore", - ], - &[], - ) - }); - - let new_excluded_dir = dir.path().join("build_output"); - let new_ignored_dir = dir.path().join("test_output"); - std::fs::create_dir_all(&new_excluded_dir) - .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}")); - std::fs::create_dir_all(&new_ignored_dir) - .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}")); - let node_modules_dir = dir.path().join("node_modules"); - let dot_git_dir = dir.path().join(".git"); - let src_dir = dir.path().join("src"); - for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] { - assert!( - existing_dir.is_dir(), - "Expect {existing_dir:?} to be present in the FS already" - ); - } - - for directory_for_new_file in [ - new_excluded_dir, - new_ignored_dir, - node_modules_dir, - dot_git_dir, - src_dir, - ] { - std::fs::write(directory_for_new_file.join("new_file"), "new file contents") - .unwrap_or_else(|e| { - panic!("Failed to create in {directory_for_new_file:?} a new file: {e}") - }); - } - tree.flush_fs_events(cx).await; - - tree.read_with(cx, |tree, _| { - check_worktree_entries( - tree, - &[ - ".git/HEAD", - ".git/foo", - ".git/new_file", - "node_modules", - "node_modules/.DS_Store", - "node_modules/prettier", - "node_modules/prettier/package.json", - "node_modules/new_file", - "build_output", - "build_output/new_file", - "test_output/new_file", - ], - &["target", "test_output"], - &[ - ".DS_Store", - "src/.DS_Store", - "src/lib.rs", - "src/foo/foo.rs", - "src/foo/another.rs", - "src/bar/bar.rs", - "src/new_file", - ".gitignore", - ], - &[], - ) - }); -} - -#[gpui::test] -async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - let dir = TempTree::new(json!({ - ".git": { - "HEAD": "ref: refs/heads/main\n", - "foo": "foo contents", - }, - })); - let dot_git_worktree_dir = dir.path().join(".git"); - - let tree = Worktree::local( - dot_git_worktree_dir.clone(), - true, - Arc::new(RealFs::new(None, cx.executor())), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.flush_fs_events(cx).await; - tree.read_with(cx, |tree, _| { - check_worktree_entries(tree, &[], &["HEAD", "foo"], &[], &[]) - }); - - std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents") - .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}")); - tree.flush_fs_events(cx).await; - tree.read_with(cx, |tree, _| { - check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[], &[]) - }); -} - -#[gpui::test(iterations = 30)] -async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/root", - json!({ - "b": {}, - "c": {}, - "d": {}, - }), - ) - .await; - - let tree = Worktree::local( - "/root".as_ref(), - true, - fs, - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - let snapshot1 = tree.update(cx, |tree, cx| { - let tree = tree.as_local_mut().unwrap(); - let snapshot = Arc::new(Mutex::new(tree.snapshot())); - tree.observe_updates(0, cx, { - let snapshot = snapshot.clone(); - let settings = tree.settings(); - move |update| { - snapshot - .lock() - .apply_remote_update(update, &settings.file_scan_inclusions); - async { true } - } - }); - snapshot - }); - - let entry = tree - .update(cx, |tree, cx| { - tree.as_local_mut() - .unwrap() - .create_entry(rel_path("a/e").into(), true, None, cx) - }) - .await - .unwrap() - .into_included() - .unwrap(); - assert!(entry.is_dir()); - - cx.executor().run_until_parked(); - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entry_for_path(rel_path("a/e")).unwrap().kind, - EntryKind::Dir - ); - }); - - let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot()); - assert_eq!( - snapshot1.lock().entries(true, 0).collect::>(), - snapshot2.entries(true, 0).collect::>() - ); -} - -#[gpui::test] -async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - - let fs_fake = FakeFs::new(cx.background_executor.clone()); - fs_fake - .insert_tree( - "/root", - json!({ - "a": {}, - }), - ) - .await; - - let tree_fake = Worktree::local( - "/root".as_ref(), - true, - fs_fake, - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - let entry = tree_fake - .update(cx, |tree, cx| { - tree.as_local_mut().unwrap().create_entry( - rel_path("a/b/c/d.txt").into(), - false, - None, - cx, - ) - }) - .await - .unwrap() - .into_included() - .unwrap(); - assert!(entry.is_file()); - - cx.executor().run_until_parked(); - tree_fake.read_with(cx, |tree, _| { - assert!( - tree.entry_for_path(rel_path("a/b/c/d.txt")) - .unwrap() - .is_file() - ); - assert!(tree.entry_for_path(rel_path("a/b/c")).unwrap().is_dir()); - assert!(tree.entry_for_path(rel_path("a/b")).unwrap().is_dir()); - }); - - let fs_real = Arc::new(RealFs::new(None, cx.executor())); - let temp_root = TempTree::new(json!({ - "a": {} - })); - - let tree_real = Worktree::local( - temp_root.path(), - true, - fs_real, - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - let entry = tree_real - .update(cx, |tree, cx| { - tree.as_local_mut().unwrap().create_entry( - rel_path("a/b/c/d.txt").into(), - false, - None, - cx, - ) - }) - .await - .unwrap() - .into_included() - .unwrap(); - assert!(entry.is_file()); - - cx.executor().run_until_parked(); - tree_real.read_with(cx, |tree, _| { - assert!( - tree.entry_for_path(rel_path("a/b/c/d.txt")) - .unwrap() - .is_file() - ); - assert!(tree.entry_for_path(rel_path("a/b/c")).unwrap().is_dir()); - assert!(tree.entry_for_path(rel_path("a/b")).unwrap().is_dir()); - }); - - // Test smallest change - let entry = tree_real - .update(cx, |tree, cx| { - tree.as_local_mut().unwrap().create_entry( - rel_path("a/b/c/e.txt").into(), - false, - None, - cx, - ) - }) - .await - .unwrap() - .into_included() - .unwrap(); - assert!(entry.is_file()); - - cx.executor().run_until_parked(); - tree_real.read_with(cx, |tree, _| { - assert!( - tree.entry_for_path(rel_path("a/b/c/e.txt")) - .unwrap() - .is_file() - ); - }); - - // Test largest change - let entry = tree_real - .update(cx, |tree, cx| { - tree.as_local_mut().unwrap().create_entry( - rel_path("d/e/f/g.txt").into(), - false, - None, - cx, - ) - }) - .await - .unwrap() - .into_included() - .unwrap(); - assert!(entry.is_file()); - - cx.executor().run_until_parked(); - tree_real.read_with(cx, |tree, _| { - assert!( - tree.entry_for_path(rel_path("d/e/f/g.txt")) - .unwrap() - .is_file() - ); - assert!(tree.entry_for_path(rel_path("d/e/f")).unwrap().is_dir()); - assert!(tree.entry_for_path(rel_path("d/e")).unwrap().is_dir()); - assert!(tree.entry_for_path(rel_path("d")).unwrap().is_dir()); - }); -} - -#[gpui::test] -async fn test_create_file_in_expanded_gitignored_dir(cx: &mut TestAppContext) { - // Tests the behavior of our worktree refresh when a file in a gitignored directory - // is created. - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/root", - json!({ - ".gitignore": "ignored_dir\n", - "ignored_dir": { - "existing_file.txt": "existing content", - "another_file.txt": "another content", - }, - }), - ) - .await; - - let tree = Worktree::local( - Path::new("/root"), - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - - tree.read_with(cx, |tree, _| { - let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap(); - assert!(ignored_dir.is_ignored); - assert_eq!(ignored_dir.kind, EntryKind::UnloadedDir); - }); - - tree.update(cx, |tree, cx| { - tree.load_file(rel_path("ignored_dir/existing_file.txt"), cx) - }) - .await - .unwrap(); - - tree.read_with(cx, |tree, _| { - let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap(); - assert!(ignored_dir.is_ignored); - assert_eq!(ignored_dir.kind, EntryKind::Dir); - - assert!( - tree.entry_for_path(rel_path("ignored_dir/existing_file.txt")) - .is_some() - ); - assert!( - tree.entry_for_path(rel_path("ignored_dir/another_file.txt")) - .is_some() - ); - }); - - let entry = tree - .update(cx, |tree, cx| { - tree.create_entry(rel_path("ignored_dir/new_file.txt").into(), false, None, cx) - }) - .await - .unwrap(); - assert!(entry.into_included().is_some()); - - cx.executor().run_until_parked(); - - tree.read_with(cx, |tree, _| { - let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap(); - assert!(ignored_dir.is_ignored); - assert_eq!( - ignored_dir.kind, - EntryKind::Dir, - "ignored_dir should still be loaded, not UnloadedDir" - ); - - assert!( - tree.entry_for_path(rel_path("ignored_dir/existing_file.txt")) - .is_some(), - "existing_file.txt should still be visible" - ); - assert!( - tree.entry_for_path(rel_path("ignored_dir/another_file.txt")) - .is_some(), - "another_file.txt should still be visible" - ); - assert!( - tree.entry_for_path(rel_path("ignored_dir/new_file.txt")) - .is_some(), - "new_file.txt should be visible" - ); - }); -} - -#[gpui::test] -async fn test_fs_event_for_gitignored_dir_does_not_lose_contents(cx: &mut TestAppContext) { - // Tests the behavior of our worktree refresh when a directory modification for a gitignored directory - // is triggered. - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/root", - json!({ - ".gitignore": "ignored_dir\n", - "ignored_dir": { - "file1.txt": "content1", - "file2.txt": "content2", - }, - }), - ) - .await; - - let tree = Worktree::local( - Path::new("/root"), - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - - // Load a file to expand the ignored directory - tree.update(cx, |tree, cx| { - tree.load_file(rel_path("ignored_dir/file1.txt"), cx) - }) - .await - .unwrap(); - - tree.read_with(cx, |tree, _| { - let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap(); - assert_eq!(ignored_dir.kind, EntryKind::Dir); - assert!( - tree.entry_for_path(rel_path("ignored_dir/file1.txt")) - .is_some() - ); - assert!( - tree.entry_for_path(rel_path("ignored_dir/file2.txt")) - .is_some() - ); - }); - - fs.emit_fs_event("/root/ignored_dir", Some(fs::PathEventKind::Changed)); - tree.flush_fs_events(cx).await; - - tree.read_with(cx, |tree, _| { - let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap(); - assert_eq!( - ignored_dir.kind, - EntryKind::Dir, - "ignored_dir should still be loaded (Dir), not UnloadedDir" - ); - assert!( - tree.entry_for_path(rel_path("ignored_dir/file1.txt")) - .is_some(), - "file1.txt should still be visible after directory fs event" - ); - assert!( - tree.entry_for_path(rel_path("ignored_dir/file2.txt")) - .is_some(), - "file2.txt should still be visible after directory fs event" - ); - }); -} - -#[gpui::test(iterations = 100)] -async fn test_random_worktree_operations_during_initial_scan( - cx: &mut TestAppContext, - mut rng: StdRng, -) { - init_test(cx); - let operations = env::var("OPERATIONS") - .map(|o| o.parse().unwrap()) - .unwrap_or(5); - let initial_entries = env::var("INITIAL_ENTRIES") - .map(|o| o.parse().unwrap()) - .unwrap_or(20); - - let root_dir = Path::new(path!("/test")); - let fs = FakeFs::new(cx.background_executor.clone()) as Arc; - fs.as_fake().insert_tree(root_dir, json!({})).await; - for _ in 0..initial_entries { - randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await; - } - log::info!("generated initial tree"); - - let worktree = Worktree::local( - root_dir, - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())]; - let updates = Arc::new(Mutex::new(Vec::new())); - worktree.update(cx, |tree, cx| { - check_worktree_change_events(tree, cx); - - tree.as_local_mut().unwrap().observe_updates(0, cx, { - let updates = updates.clone(); - move |update| { - updates.lock().push(update); - async { true } - } - }); - }); - - for _ in 0..operations { - worktree - .update(cx, |worktree, cx| { - randomly_mutate_worktree(worktree, &mut rng, cx) - }) - .await - .log_err(); - worktree.read_with(cx, |tree, _| { - tree.as_local().unwrap().snapshot().check_invariants(true) - }); - - if rng.random_bool(0.6) { - snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())); - } - } - - worktree - .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete()) - .await; - - cx.executor().run_until_parked(); - - let final_snapshot = worktree.read_with(cx, |tree, _| { - let tree = tree.as_local().unwrap(); - let snapshot = tree.snapshot(); - snapshot.check_invariants(true); - snapshot - }); - - let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings()); - - for (i, snapshot) in snapshots.into_iter().enumerate().rev() { - let mut updated_snapshot = snapshot.clone(); - for update in updates.lock().iter() { - if update.scan_id >= updated_snapshot.scan_id() as u64 { - updated_snapshot - .apply_remote_update(update.clone(), &settings.file_scan_inclusions); - } - } - - assert_eq!( - updated_snapshot.entries(true, 0).collect::>(), - final_snapshot.entries(true, 0).collect::>(), - "wrong updates after snapshot {i}: {updates:#?}", - ); - } -} - -#[gpui::test(iterations = 100)] -async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) { - init_test(cx); - let operations = env::var("OPERATIONS") - .map(|o| o.parse().unwrap()) - .unwrap_or(40); - let initial_entries = env::var("INITIAL_ENTRIES") - .map(|o| o.parse().unwrap()) - .unwrap_or(20); - - let root_dir = Path::new(path!("/test")); - let fs = FakeFs::new(cx.background_executor.clone()) as Arc; - fs.as_fake().insert_tree(root_dir, json!({})).await; - for _ in 0..initial_entries { - randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await; - } - log::info!("generated initial tree"); - - let worktree = Worktree::local( - root_dir, - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - let updates = Arc::new(Mutex::new(Vec::new())); - worktree.update(cx, |tree, cx| { - check_worktree_change_events(tree, cx); - - tree.as_local_mut().unwrap().observe_updates(0, cx, { - let updates = updates.clone(); - move |update| { - updates.lock().push(update); - async { true } - } - }); - }); - - worktree - .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete()) - .await; - - fs.as_fake().pause_events(); - let mut snapshots = Vec::new(); - let mut mutations_len = operations; - while mutations_len > 1 { - if rng.random_bool(0.2) { - worktree - .update(cx, |worktree, cx| { - randomly_mutate_worktree(worktree, &mut rng, cx) - }) - .await - .log_err(); - } else { - randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await; - } - - let buffered_event_count = fs.as_fake().buffered_event_count(); - if buffered_event_count > 0 && rng.random_bool(0.3) { - let len = rng.random_range(0..=buffered_event_count); - log::info!("flushing {} events", len); - fs.as_fake().flush_events(len); - } else { - randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await; - mutations_len -= 1; - } - - cx.executor().run_until_parked(); - if rng.random_bool(0.2) { - log::info!("storing snapshot {}", snapshots.len()); - let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); - snapshots.push(snapshot); - } - } - - log::info!("quiescing"); - fs.as_fake().flush_events(usize::MAX); - cx.executor().run_until_parked(); - - let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); - snapshot.check_invariants(true); - let expanded_paths = snapshot - .expanded_entries() - .map(|e| e.path.clone()) - .collect::>(); - - { - let new_worktree = Worktree::local( - root_dir, - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - new_worktree - .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete()) - .await; - new_worktree - .update(cx, |tree, _| { - tree.as_local_mut() - .unwrap() - .refresh_entries_for_paths(expanded_paths) - }) - .recv() - .await; - let new_snapshot = - new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); - assert_eq!( - snapshot.entries_without_ids(true), - new_snapshot.entries_without_ids(true) - ); - } - - let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings()); - - for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() { - for update in updates.lock().iter() { - if update.scan_id >= prev_snapshot.scan_id() as u64 { - prev_snapshot.apply_remote_update(update.clone(), &settings.file_scan_inclusions); - } - } - - assert_eq!( - prev_snapshot - .entries(true, 0) - .map(ignore_pending_dir) - .collect::>(), - snapshot - .entries(true, 0) - .map(ignore_pending_dir) - .collect::>(), - "wrong updates after snapshot {i}: {updates:#?}", - ); - } - - fn ignore_pending_dir(entry: &Entry) -> Entry { - let mut entry = entry.clone(); - if entry.kind.is_dir() { - entry.kind = EntryKind::Dir - } - entry - } -} - -// The worktree's `UpdatedEntries` event can be used to follow along with -// all changes to the worktree's snapshot. -fn check_worktree_change_events(tree: &mut Worktree, cx: &mut Context) { - let mut entries = tree.entries(true, 0).cloned().collect::>(); - cx.subscribe(&cx.entity(), move |tree, _, event, _| { - if let Event::UpdatedEntries(changes) = event { - for (path, _, change_type) in changes.iter() { - let entry = tree.entry_for_path(path).cloned(); - let ix = match entries.binary_search_by_key(&path, |e| &e.path) { - Ok(ix) | Err(ix) => ix, - }; - match change_type { - PathChange::Added => entries.insert(ix, entry.unwrap()), - PathChange::Removed => drop(entries.remove(ix)), - PathChange::Updated => { - let entry = entry.unwrap(); - let existing_entry = entries.get_mut(ix).unwrap(); - assert_eq!(existing_entry.path, entry.path); - *existing_entry = entry; - } - PathChange::AddedOrUpdated | PathChange::Loaded => { - let entry = entry.unwrap(); - if entries.get(ix).map(|e| &e.path) == Some(&entry.path) { - *entries.get_mut(ix).unwrap() = entry; - } else { - entries.insert(ix, entry); - } - } - } - } - - let new_entries = tree.entries(true, 0).cloned().collect::>(); - assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes); - } - }) - .detach(); -} - -fn randomly_mutate_worktree( - worktree: &mut Worktree, - rng: &mut impl Rng, - cx: &mut Context, -) -> Task> { - log::info!("mutating worktree"); - let worktree = worktree.as_local_mut().unwrap(); - let snapshot = worktree.snapshot(); - let entry = snapshot.entries(false, 0).choose(rng).unwrap(); - - match rng.random_range(0_u32..100) { - 0..=33 if entry.path.as_ref() != RelPath::empty() => { - log::info!("deleting entry {:?} ({})", entry.path, entry.id.0); - worktree.delete_entry(entry.id, false, cx).unwrap() - } - _ => { - if entry.is_dir() { - let child_path = entry.path.join(rel_path(&random_filename(rng))); - let is_dir = rng.random_bool(0.3); - log::info!( - "creating {} at {:?}", - if is_dir { "dir" } else { "file" }, - child_path, - ); - let task = worktree.create_entry(child_path, is_dir, None, cx); - cx.background_spawn(async move { - task.await?; - Ok(()) - }) - } else { - log::info!("overwriting file {:?} ({})", &entry.path, entry.id.0); - let task = - worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx); - cx.background_spawn(async move { - task.await?; - Ok(()) - }) - } - } - } -} - -async fn randomly_mutate_fs( - fs: &Arc, - root_path: &Path, - insertion_probability: f64, - rng: &mut impl Rng, -) { - log::info!("mutating fs"); - let mut files = Vec::new(); - let mut dirs = Vec::new(); - for path in fs.as_fake().paths(false) { - if path.starts_with(root_path) { - if fs.is_file(&path).await { - files.push(path); - } else { - dirs.push(path); - } - } - } - - if (files.is_empty() && dirs.len() == 1) || rng.random_bool(insertion_probability) { - let path = dirs.choose(rng).unwrap(); - let new_path = path.join(random_filename(rng)); - - if rng.random() { - log::info!( - "creating dir {:?}", - new_path.strip_prefix(root_path).unwrap() - ); - fs.create_dir(&new_path).await.unwrap(); - } else { - log::info!( - "creating file {:?}", - new_path.strip_prefix(root_path).unwrap() - ); - fs.create_file(&new_path, Default::default()).await.unwrap(); - } - } else if rng.random_bool(0.05) { - let ignore_dir_path = dirs.choose(rng).unwrap(); - let ignore_path = ignore_dir_path.join(GITIGNORE); - - let subdirs = dirs - .iter() - .filter(|d| d.starts_with(ignore_dir_path)) - .cloned() - .collect::>(); - let subfiles = files - .iter() - .filter(|d| d.starts_with(ignore_dir_path)) - .cloned() - .collect::>(); - let files_to_ignore = { - let len = rng.random_range(0..=subfiles.len()); - subfiles.choose_multiple(rng, len) - }; - let dirs_to_ignore = { - let len = rng.random_range(0..subdirs.len()); - subdirs.choose_multiple(rng, len) - }; - - let mut ignore_contents = String::new(); - for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) { - writeln!( - ignore_contents, - "{}", - path_to_ignore - .strip_prefix(ignore_dir_path) - .unwrap() - .to_str() - .unwrap() - ) - .unwrap(); - } - log::info!( - "creating gitignore {:?} with contents:\n{}", - ignore_path.strip_prefix(root_path).unwrap(), - ignore_contents - ); - fs.save( - &ignore_path, - &ignore_contents.as_str().into(), - Default::default(), - ) - .await - .unwrap(); - } else { - let old_path = { - let file_path = files.choose(rng); - let dir_path = dirs[1..].choose(rng); - file_path.into_iter().chain(dir_path).choose(rng).unwrap() - }; - - let is_rename = rng.random(); - if is_rename { - let new_path_parent = dirs - .iter() - .filter(|d| !d.starts_with(old_path)) - .choose(rng) - .unwrap(); - - let overwrite_existing_dir = - !old_path.starts_with(new_path_parent) && rng.random_bool(0.3); - let new_path = if overwrite_existing_dir { - fs.remove_dir( - new_path_parent, - RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await - .unwrap(); - new_path_parent.to_path_buf() - } else { - new_path_parent.join(random_filename(rng)) - }; - - log::info!( - "renaming {:?} to {}{:?}", - old_path.strip_prefix(root_path).unwrap(), - if overwrite_existing_dir { - "overwrite " - } else { - "" - }, - new_path.strip_prefix(root_path).unwrap() - ); - fs.rename( - old_path, - &new_path, - fs::RenameOptions { - overwrite: true, - ignore_if_exists: true, - create_parents: false, - }, - ) - .await - .unwrap(); - } else if fs.is_file(old_path).await { - log::info!( - "deleting file {:?}", - old_path.strip_prefix(root_path).unwrap() - ); - fs.remove_file(old_path, Default::default()).await.unwrap(); - } else { - log::info!( - "deleting dir {:?}", - old_path.strip_prefix(root_path).unwrap() - ); - fs.remove_dir( - old_path, - RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await - .unwrap(); - } - } -} - -fn random_filename(rng: &mut impl Rng) -> String { - (0..6) - .map(|_| rng.sample(rand::distr::Alphanumeric)) - .map(char::from) - .collect() -} - -#[gpui::test] -async fn test_private_single_file_worktree(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree("/", json!({".env": "PRIVATE=secret\n"})) - .await; - let tree = Worktree::local( - Path::new("/.env"), - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.read_with(cx, |tree, _| { - let entry = tree.entry_for_path(rel_path("")).unwrap(); - assert!(entry.is_private); - }); -} - -#[gpui::test] -async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(executor); - fs.insert_tree( - path!("/root"), - json!({ - ".git": {}, - "subproject": { - "a.txt": "A" - } - }), - ) - .await; - let worktree = Worktree::local( - path!("/root/subproject").as_ref(), - true, - fs.clone(), - Arc::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - worktree - .update(cx, |worktree, _| { - worktree.as_local().unwrap().scan_complete() - }) - .await; - cx.run_until_parked(); - let repos = worktree.update(cx, |worktree, _| { - worktree - .as_local() - .unwrap() - .git_repositories - .values() - .map(|entry| entry.work_directory_abs_path.clone()) - .collect::>() - }); - pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]); - - fs.touch_path(path!("/root/subproject")).await; - worktree - .update(cx, |worktree, _| { - worktree.as_local().unwrap().scan_complete() - }) - .await; - cx.run_until_parked(); - - let repos = worktree.update(cx, |worktree, _| { - worktree - .as_local() - .unwrap() - .git_repositories - .values() - .map(|entry| entry.work_directory_abs_path.clone()) - .collect::>() - }); - pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]); -} - -#[gpui::test] -async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppContext) { - init_test(cx); - - let home = paths::home_dir(); - let fs = FakeFs::new(executor); - fs.insert_tree( - home, - json!({ - ".config": { - "git": { - "ignore": "foo\n/bar\nbaz\n" - } - }, - "project": { - ".git": {}, - ".gitignore": "!baz", - "foo": "", - "bar": "", - "sub": { - "bar": "", - }, - "subrepo": { - ".git": {}, - "bar": "" - }, - "baz": "" - } - }), - ) - .await; - let worktree = Worktree::local( - home.join("project"), - true, - fs.clone(), - Arc::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - worktree - .update(cx, |worktree, _| { - worktree.as_local().unwrap().scan_complete() - }) - .await; - cx.run_until_parked(); - - // .gitignore overrides excludesFile, and anchored paths in excludesFile are resolved - // relative to the nearest containing repository - worktree.update(cx, |worktree, _cx| { - check_worktree_entries( - worktree, - &[], - &["foo", "bar", "subrepo/bar"], - &["sub/bar", "baz"], - &[], - ); - }); - - // Ignore statuses are updated when excludesFile changes - fs.write( - &home.join(".config").join("git").join("ignore"), - "/bar\nbaz\n".as_bytes(), - ) - .await - .unwrap(); - worktree - .update(cx, |worktree, _| { - worktree.as_local().unwrap().scan_complete() - }) - .await; - cx.run_until_parked(); - - worktree.update(cx, |worktree, _cx| { - check_worktree_entries( - worktree, - &[], - &["bar", "subrepo/bar"], - &["foo", "sub/bar", "baz"], - &[], - ); - }); - - // Statuses are updated when .git added/removed - fs.remove_dir( - &home.join("project").join("subrepo").join(".git"), - RemoveOptions { - recursive: true, - ..Default::default() - }, - ) - .await - .unwrap(); - worktree - .update(cx, |worktree, _| { - worktree.as_local().unwrap().scan_complete() - }) - .await; - cx.run_until_parked(); - - worktree.update(cx, |worktree, _cx| { - check_worktree_entries( - worktree, - &[], - &["bar"], - &["foo", "sub/bar", "baz", "subrepo/bar"], - &[], - ); - }); -} - -#[track_caller] -fn check_worktree_entries( - tree: &Worktree, - expected_excluded_paths: &[&str], - expected_ignored_paths: &[&str], - expected_tracked_paths: &[&str], - expected_included_paths: &[&str], -) { - for path in expected_excluded_paths { - let entry = tree.entry_for_path(rel_path(path)); - assert!( - entry.is_none(), - "expected path '{path}' to be excluded, but got entry: {entry:?}", - ); - } - for path in expected_ignored_paths { - let entry = tree - .entry_for_path(rel_path(path)) - .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'")); - assert!( - entry.is_ignored, - "expected path '{path}' to be ignored, but got entry: {entry:?}", - ); - } - for path in expected_tracked_paths { - let entry = tree - .entry_for_path(rel_path(path)) - .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'")); - assert!( - !entry.is_ignored || entry.is_always_included, - "expected path '{path}' to be tracked, but got entry: {entry:?}", - ); - } - for path in expected_included_paths { - let entry = tree - .entry_for_path(rel_path(path)) - .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'")); - assert!( - entry.is_always_included, - "expected path '{path}' to always be included, but got entry: {entry:?}", - ); - } -} - -fn init_test(cx: &mut gpui::TestAppContext) { - zlog::init_test(); - - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - }); -} diff --git a/crates/worktree_benchmarks/Cargo.toml b/crates/worktree_benchmarks/Cargo.toml deleted file mode 100644 index 29681573ad..0000000000 --- a/crates/worktree_benchmarks/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "worktree_benchmarks" -version = "0.1.0" -publish.workspace = true -edition.workspace = true - -[dependencies] -fs.workspace = true -gpui = { workspace = true, features = ["windows-manifest"] } -settings.workspace = true -worktree.workspace = true - -[lints] -workspace = true diff --git a/crates/worktree_benchmarks/LICENSE-GPL b/crates/worktree_benchmarks/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/worktree_benchmarks/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/worktree_benchmarks/src/main.rs b/crates/worktree_benchmarks/src/main.rs deleted file mode 100644 index 00f268b75f..0000000000 --- a/crates/worktree_benchmarks/src/main.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::{ - path::Path, - sync::{Arc, atomic::AtomicUsize}, -}; - -use fs::RealFs; -use gpui::Application; -use settings::Settings; -use worktree::{Worktree, WorktreeSettings}; - -fn main() { - let Some(worktree_root_path) = std::env::args().nth(1) else { - println!( - "Missing path to worktree root\nUsage: bench_background_scan PATH_TO_WORKTREE_ROOT" - ); - return; - }; - let app = Application::headless(); - - app.run(|cx| { - settings::init(cx); - let fs = Arc::new(RealFs::new(None, cx.background_executor().clone())); - - cx.spawn(async move |cx| { - let worktree = Worktree::local( - Path::new(&worktree_root_path), - true, - fs, - Arc::new(AtomicUsize::new(0)), - cx, - ) - .await - .expect("Worktree initialization to succeed"); - let did_finish_scan = worktree - .update(cx, |this, _| this.as_local().unwrap().scan_complete()) - .unwrap(); - let start = std::time::Instant::now(); - did_finish_scan.await; - let elapsed = start.elapsed(); - let (files, directories) = worktree - .read_with(cx, |this, _| (this.file_count(), this.dir_count())) - .unwrap(); - println!( - "{:?} for {directories} directories and {files} files", - elapsed - ); - cx.update(|cx| { - cx.quit(); - }) - }) - .detach(); - }) -} diff --git a/crates/x_ai/Cargo.toml b/crates/x_ai/Cargo.toml deleted file mode 100644 index 8ff020df8c..0000000000 --- a/crates/x_ai/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "x_ai" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/x_ai.rs" - -[features] -default = [] -schemars = ["dep:schemars"] - -[dependencies] -anyhow.workspace = true -schemars = { workspace = true, optional = true } -serde.workspace = true -strum.workspace = true diff --git a/crates/x_ai/LICENSE-GPL b/crates/x_ai/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/x_ai/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/x_ai/src/x_ai.rs b/crates/x_ai/src/x_ai.rs deleted file mode 100644 index 072a893a6a..0000000000 --- a/crates/x_ai/src/x_ai.rs +++ /dev/null @@ -1,208 +0,0 @@ -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use strum::EnumIter; - -pub const XAI_API_URL: &str = "https://api.x.ai/v1"; - -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)] -pub enum Model { - #[serde(rename = "grok-2-vision-latest")] - Grok2Vision, - #[default] - #[serde(rename = "grok-3-latest")] - Grok3, - #[serde(rename = "grok-3-mini-latest")] - Grok3Mini, - #[serde(rename = "grok-3-fast-latest")] - Grok3Fast, - #[serde(rename = "grok-3-mini-fast-latest")] - Grok3MiniFast, - #[serde(rename = "grok-4", alias = "grok-4-latest")] - Grok4, - #[serde( - rename = "grok-4-fast-reasoning", - alias = "grok-4-fast-reasoning-latest" - )] - Grok4FastReasoning, - #[serde( - rename = "grok-4-fast-non-reasoning", - alias = "grok-4-fast-non-reasoning-latest" - )] - Grok4FastNonReasoning, - #[serde( - rename = "grok-4-1-fast-non-reasoning", - alias = "grok-4-1-fast-non-reasoning-latest" - )] - Grok41FastNonReasoning, - #[serde( - rename = "grok-4-1-fast-reasoning", - alias = "grok-4-1-fast-reasoning-latest", - alias = "grok-4-1-fast" - )] - Grok41FastReasoning, - #[serde(rename = "grok-code-fast-1", alias = "grok-code-fast-1-0825")] - GrokCodeFast1, - #[serde(rename = "custom")] - Custom { - name: String, - /// The name displayed in the UI, such as in the assistant panel model dropdown menu. - display_name: Option, - max_tokens: u64, - max_output_tokens: Option, - max_completion_tokens: Option, - supports_images: Option, - supports_tools: Option, - parallel_tool_calls: Option, - }, -} - -impl Model { - pub fn default_fast() -> Self { - Self::Grok3Fast - } - - pub fn from_id(id: &str) -> Result { - match id { - "grok-4" => Ok(Self::Grok4), - "grok-4-fast-reasoning" => Ok(Self::Grok4FastReasoning), - "grok-4-fast-non-reasoning" => Ok(Self::Grok4FastNonReasoning), - "grok-4-1-fast-non-reasoning" => Ok(Self::Grok41FastNonReasoning), - "grok-4-1-fast-reasoning" => Ok(Self::Grok41FastReasoning), - "grok-4-1-fast" => Ok(Self::Grok41FastReasoning), - "grok-2-vision" => Ok(Self::Grok2Vision), - "grok-3" => Ok(Self::Grok3), - "grok-3-mini" => Ok(Self::Grok3Mini), - "grok-3-fast" => Ok(Self::Grok3Fast), - "grok-3-mini-fast" => Ok(Self::Grok3MiniFast), - "grok-code-fast-1" => Ok(Self::GrokCodeFast1), - _ => anyhow::bail!("invalid model id '{id}'"), - } - } - - pub fn id(&self) -> &str { - match self { - Self::Grok2Vision => "grok-2-vision", - Self::Grok3 => "grok-3", - Self::Grok3Mini => "grok-3-mini", - Self::Grok3Fast => "grok-3-fast", - Self::Grok3MiniFast => "grok-3-mini-fast", - Self::Grok4 => "grok-4", - Self::Grok4FastReasoning => "grok-4-fast-reasoning", - Self::Grok4FastNonReasoning => "grok-4-fast-non-reasoning", - Self::Grok41FastNonReasoning => "grok-4-1-fast-non-reasoning", - Self::Grok41FastReasoning => "grok-4-1-fast-reasoning", - Self::GrokCodeFast1 => "grok-code-fast-1", - Self::Custom { name, .. } => name, - } - } - - pub fn display_name(&self) -> &str { - match self { - Self::Grok2Vision => "Grok 2 Vision", - Self::Grok3 => "Grok 3", - Self::Grok3Mini => "Grok 3 Mini", - Self::Grok3Fast => "Grok 3 Fast", - Self::Grok3MiniFast => "Grok 3 Mini Fast", - Self::Grok4 => "Grok 4", - Self::Grok4FastReasoning => "Grok 4 Fast", - Self::Grok4FastNonReasoning => "Grok 4 Fast (Non-Reasoning)", - Self::Grok41FastNonReasoning => "Grok 4.1 Fast (Non-Reasoning)", - Self::Grok41FastReasoning => "Grok 4.1 Fast", - Self::GrokCodeFast1 => "Grok Code Fast 1", - Self::Custom { - name, display_name, .. - } => display_name.as_ref().unwrap_or(name), - } - } - - pub fn max_token_count(&self) -> u64 { - match self { - Self::Grok3 | Self::Grok3Mini | Self::Grok3Fast | Self::Grok3MiniFast => 131_072, - Self::Grok4 | Self::GrokCodeFast1 => 256_000, - Self::Grok4FastReasoning - | Self::Grok4FastNonReasoning - | Self::Grok41FastNonReasoning - | Self::Grok41FastReasoning => 2_000_000, - Self::Grok2Vision => 8_192, - Self::Custom { max_tokens, .. } => *max_tokens, - } - } - - pub fn max_output_tokens(&self) -> Option { - match self { - Self::Grok3 | Self::Grok3Mini | Self::Grok3Fast | Self::Grok3MiniFast => Some(8_192), - Self::Grok4 - | Self::Grok4FastReasoning - | Self::Grok4FastNonReasoning - | Self::Grok41FastNonReasoning - | Self::Grok41FastReasoning - | Self::GrokCodeFast1 => Some(64_000), - Self::Grok2Vision => Some(4_096), - Self::Custom { - max_output_tokens, .. - } => *max_output_tokens, - } - } - - pub fn supports_parallel_tool_calls(&self) -> bool { - match self { - Self::Grok2Vision - | Self::Grok3 - | Self::Grok3Mini - | Self::Grok3Fast - | Self::Grok3MiniFast - | Self::Grok4 - | Self::Grok4FastReasoning - | Self::Grok4FastNonReasoning - | Self::Grok41FastNonReasoning - | Self::Grok41FastReasoning => true, - Self::Custom { - parallel_tool_calls: Some(support), - .. - } => *support, - Self::GrokCodeFast1 | Model::Custom { .. } => false, - } - } - - pub fn supports_prompt_cache_key(&self) -> bool { - false - } - - pub fn supports_tool(&self) -> bool { - match self { - Self::Grok2Vision - | Self::Grok3 - | Self::Grok3Mini - | Self::Grok3Fast - | Self::Grok3MiniFast - | Self::Grok4 - | Self::Grok4FastReasoning - | Self::Grok4FastNonReasoning - | Self::Grok41FastNonReasoning - | Self::Grok41FastReasoning - | Self::GrokCodeFast1 => true, - Self::Custom { - supports_tools: Some(support), - .. - } => *support, - Model::Custom { .. } => false, - } - } - - pub fn supports_images(&self) -> bool { - match self { - Self::Grok2Vision - | Self::Grok4 - | Self::Grok4FastReasoning - | Self::Grok4FastNonReasoning - | Self::Grok41FastNonReasoning - | Self::Grok41FastReasoning => true, - Self::Custom { - supports_images: Some(support), - .. - } => *support, - _ => false, - } - } -} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml deleted file mode 100644 index 92a274da96..0000000000 --- a/crates/zed/Cargo.toml +++ /dev/null @@ -1,231 +0,0 @@ -[package] -description = "The fast, collaborative code editor." -edition.workspace = true -name = "zed" -version = "0.218.0" -publish.workspace = true -license = "GPL-3.0-or-later" -authors = ["Zed Team "] - -[lints] -workspace = true - -[features] -tracy = ["ztracing/tracy"] - -[[bin]] -name = "zed" -path = "src/zed-main.rs" - -[lib] -name = "zed" -path = "src/main.rs" - -[dependencies] -acp_tools.workspace = true -activity_indicator.workspace = true -agent_settings.workspace = true -agent_ui.workspace = true -anyhow.workspace = true -askpass.workspace = true -assets.workspace = true -audio.workspace = true -auto_update.workspace = true -auto_update_ui.workspace = true -bincode.workspace = true -breadcrumbs.workspace = true -call.workspace = true -channel.workspace = true -clap.workspace = true -cli.workspace = true -client.workspace = true -codestral.workspace = true -collab_ui.workspace = true -collections.workspace = true -command_palette.workspace = true -component.workspace = true -copilot.workspace = true -crashes.workspace = true -dap_adapters.workspace = true -db.workspace = true -debug_adapter_extension.workspace = true -debugger_tools.workspace = true -debugger_ui.workspace = true -diagnostics.workspace = true -editor.workspace = true -env_logger.workspace = true -extension.workspace = true -extension_host.workspace = true -extensions_ui.workspace = true -feature_flags.workspace = true -feedback.workspace = true -file_finder.workspace = true -fs.workspace = true -futures.workspace = true -git.workspace = true -git_hosting_providers.workspace = true -git_ui.workspace = true -go_to_line.workspace = true -system_specs.workspace = true -gpui = { workspace = true, features = [ - "wayland", - "x11", - "font-kit", - "windows-manifest", -] } -gpui_tokio.workspace = true -rayon.workspace = true - -edit_prediction.workspace = true -edit_prediction_ui.workspace = true -http_client.workspace = true -image_viewer.workspace = true -inspector_ui.workspace = true -install_cli.workspace = true -journal.workspace = true -json_schema_store.workspace = true -keymap_editor.workspace = true -language.workspace = true -language_extension.workspace = true -language_model.workspace = true -language_models.workspace = true -language_onboarding.workspace = true -language_selector.workspace = true -language_tools.workspace = true -languages = { workspace = true, features = ["load-grammars"] } -line_ending_selector.workspace = true -log.workspace = true -markdown.workspace = true -markdown_preview.workspace = true -menu.workspace = true -migrator.workspace = true -miniprofiler_ui.workspace = true -mimalloc = { version = "0.1", optional = true } -nc.workspace = true -node_runtime.workspace = true -notifications.workspace = true -onboarding.workspace = true -outline.workspace = true -outline_panel.workspace = true -parking_lot.workspace = true -paths.workspace = true -picker.workspace = true -profiling.workspace = true -project.workspace = true -project_panel.workspace = true -project_symbols.workspace = true -prompt_store.workspace = true -proto.workspace = true -recent_projects.workspace = true -release_channel.workspace = true -remote.workspace = true -repl.workspace = true -reqwest.workspace = true -reqwest_client.workspace = true -rope.workspace = true -search.workspace = true -serde.workspace = true -serde_json.workspace = true -session.workspace = true -settings.workspace = true -settings_profile_selector.workspace = true -settings_ui.workspace = true -shellexpand.workspace = true -smol.workspace = true -snippet_provider.workspace = true -snippets_ui.workspace = true -supermaven.workspace = true -svg_preview.workspace = true -sysinfo.workspace = true -tab_switcher.workspace = true -task.workspace = true -tasks_ui.workspace = true -telemetry.workspace = true -terminal_view.workspace = true -theme.workspace = true -theme_extension.workspace = true -theme_selector.workspace = true -time.workspace = true -title_bar.workspace = true -ztracing.workspace = true -tracing.workspace = true -toolchain_selector.workspace = true -ui.workspace = true -ui_input.workspace = true -ui_prompt.workspace = true -url.workspace = true -urlencoding.workspace = true -util.workspace = true -uuid.workspace = true -vim.workspace = true -vim_mode_setting.workspace = true -watch.workspace = true -web_search.workspace = true -web_search_providers.workspace = true -workspace.workspace = true -zed_actions.workspace = true -zed_env_vars.workspace = true -zlog.workspace = true -zlog_settings.workspace = true -chrono.workspace = true - -[target.'cfg(target_os = "windows")'.dependencies] -windows.workspace = true -chrono.workspace = true - -[target.'cfg(target_os = "windows")'.build-dependencies] -winresource = "0.1" - -[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies] -ashpd.workspace = true - -[dev-dependencies] -call = { workspace = true, features = ["test-support"] } -dap = { workspace = true, features = ["test-support"] } -editor = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -image_viewer = { workspace = true, features = ["test-support"] } -itertools.workspace = true -language = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true -project = { workspace = true, features = ["test-support"] } -semver.workspace = true -terminal_view = { workspace = true, features = ["test-support"] } -tree-sitter-md.workspace = true -tree-sitter-rust.workspace = true -workspace = { workspace = true, features = ["test-support"] } - -[package.metadata.bundle-dev] -icon = ["resources/app-icon-dev@2x.png", "resources/app-icon-dev.png"] -identifier = "dev.zed.Zed-Dev" -name = "Zed Dev" -osx_minimum_system_version = "10.15.7" -osx_info_plist_exts = ["resources/info/*"] -osx_url_schemes = ["zed"] - -[package.metadata.bundle-nightly] -icon = ["resources/app-icon-nightly@2x.png", "resources/app-icon-nightly.png"] -identifier = "dev.zed.Zed-Nightly" -name = "Zed Nightly" -osx_minimum_system_version = "10.15.7" -osx_info_plist_exts = ["resources/info/*"] -osx_url_schemes = ["zed"] - -[package.metadata.bundle-preview] -icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"] -identifier = "dev.zed.Zed-Preview" -name = "Zed Preview" -osx_minimum_system_version = "10.15.7" -osx_info_plist_exts = ["resources/info/*"] -osx_url_schemes = ["zed"] - -[package.metadata.bundle-stable] -icon = ["resources/app-icon@2x.png", "resources/app-icon.png"] -identifier = "dev.zed.Zed" -name = "Zed" -osx_minimum_system_version = "10.15.7" -osx_info_plist_exts = ["resources/info/*"] -osx_url_schemes = ["zed"] - -[package.metadata.cargo-machete] -ignored = ["profiling", "zstd", "tracing"] diff --git a/crates/zed/LICENSE-GPL b/crates/zed/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/zed/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/zed/RELEASE_CHANNEL b/crates/zed/RELEASE_CHANNEL deleted file mode 100644 index 38f8e886e1..0000000000 --- a/crates/zed/RELEASE_CHANNEL +++ /dev/null @@ -1 +0,0 @@ -dev diff --git a/crates/zed/build.rs b/crates/zed/build.rs deleted file mode 100644 index dd26a1152e..0000000000 --- a/crates/zed/build.rs +++ /dev/null @@ -1,105 +0,0 @@ -#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")] -use std::process::Command; - -fn main() { - if cfg!(target_os = "macos") { - println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7"); - - // Weakly link ReplayKit to ensure Zed can be used on macOS 10.15+. - println!("cargo:rustc-link-arg=-Wl,-weak_framework,ReplayKit"); - - // Seems to be required to enable Swift concurrency - println!("cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/swift"); - - // Register exported Objective-C selectors, protocols, etc - println!("cargo:rustc-link-arg=-Wl,-ObjC"); - - // weak link to support Catalina - println!("cargo:rustc-link-arg=-Wl,-weak_framework,ScreenCaptureKit"); - } - - // Populate git sha environment variable if git is available - println!("cargo:rerun-if-changed=../../.git/logs/HEAD"); - println!( - "cargo:rustc-env=TARGET={}", - std::env::var("TARGET").unwrap() - ); - if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() - && output.status.success() - { - let git_sha = String::from_utf8_lossy(&output.stdout); - let git_sha = git_sha.trim(); - - println!("cargo:rustc-env=ZED_COMMIT_SHA={git_sha}"); - - if let Some(build_identifier) = option_env!("GITHUB_RUN_NUMBER") { - println!("cargo:rustc-env=ZED_BUILD_ID={build_identifier}"); - } - - if let Ok(build_profile) = std::env::var("PROFILE") - && build_profile == "release" - { - // This is currently the best way to make `cargo build ...`'s build script - // to print something to stdout without extra verbosity. - println!("cargo::warning=Info: using '{git_sha}' hash for ZED_COMMIT_SHA env var"); - } - } - - #[cfg(target_os = "windows")] - { - #[cfg(target_env = "msvc")] - { - // todo(windows): This is to avoid stack overflow. Remove it when solved. - println!("cargo:rustc-link-arg=/stack:{}", 8 * 1024 * 1024); - } - - if cfg!(target_arch = "x86_64") { - println!("cargo::rerun-if-changed=resources\\windows\\bin\\x64\\conpty.dll"); - println!("cargo::rerun-if-changed=resources\\windows\\bin\\x64\\OpenConsole.exe"); - let conpty_target = std::env::var("OUT_DIR").unwrap() + "\\..\\..\\..\\conpty.dll"; - match std::fs::copy("resources/windows/bin/x64/conpty.dll", &conpty_target) { - Ok(_) => println!("Copied conpty.dll to {conpty_target}"), - Err(e) => println!("cargo::warning=Failed to copy conpty.dll: {}", e), - } - let open_console_target = - std::env::var("OUT_DIR").unwrap() + "\\..\\..\\..\\OpenConsole.exe"; - match std::fs::copy( - "resources/windows/bin/x64/OpenConsole.exe", - &open_console_target, - ) { - Ok(_) => println!("Copied OpenConsole.exe to {open_console_target}"), - Err(e) => println!("cargo::warning=Failed to copy OpenConsole.exe: {}", e), - } - } - - let release_channel = option_env!("RELEASE_CHANNEL").unwrap_or("dev"); - let icon = match release_channel { - "stable" => "resources/windows/app-icon.ico", - "preview" => "resources/windows/app-icon-preview.ico", - "nightly" => "resources/windows/app-icon-nightly.ico", - "dev" => "resources/windows/app-icon-dev.ico", - _ => "resources/windows/app-icon-dev.ico", - }; - let icon = std::path::Path::new(icon); - - println!("cargo:rerun-if-env-changed=RELEASE_CHANNEL"); - println!("cargo:rerun-if-changed={}", icon.display()); - - let mut res = winresource::WindowsResource::new(); - - // Depending on the security applied to the computer, winresource might fail - // fetching the RC path. Therefore, we add a way to explicitly specify the - // toolkit path, allowing winresource to use a valid RC path. - if let Some(explicit_rc_toolkit_path) = std::env::var("ZED_RC_TOOLKIT_PATH").ok() { - res.set_toolkit_path(explicit_rc_toolkit_path.as_str()); - } - res.set_icon(icon.to_str().unwrap()); - res.set("FileDescription", "Zed"); - res.set("ProductName", "Zed"); - - if let Err(e) = res.compile() { - eprintln!("{}", e); - std::process::exit(1); - } - } -} diff --git a/crates/zed/contents/dev/embedded.provisionprofile b/crates/zed/contents/dev/embedded.provisionprofile deleted file mode 100644 index 8979e1fb9f..0000000000 Binary files a/crates/zed/contents/dev/embedded.provisionprofile and /dev/null differ diff --git a/crates/zed/contents/nightly/embedded.provisionprofile b/crates/zed/contents/nightly/embedded.provisionprofile deleted file mode 100644 index 8979e1fb9f..0000000000 Binary files a/crates/zed/contents/nightly/embedded.provisionprofile and /dev/null differ diff --git a/crates/zed/contents/preview/embedded.provisionprofile b/crates/zed/contents/preview/embedded.provisionprofile deleted file mode 100644 index 6eea317c37..0000000000 Binary files a/crates/zed/contents/preview/embedded.provisionprofile and /dev/null differ diff --git a/crates/zed/contents/stable/embedded.provisionprofile b/crates/zed/contents/stable/embedded.provisionprofile deleted file mode 100644 index 0b2abe1838..0000000000 Binary files a/crates/zed/contents/stable/embedded.provisionprofile and /dev/null differ diff --git a/crates/zed/resources/app-icon-dev.png b/crates/zed/resources/app-icon-dev.png deleted file mode 100644 index cbe2ac3ef7..0000000000 Binary files a/crates/zed/resources/app-icon-dev.png and /dev/null differ diff --git a/crates/zed/resources/app-icon-dev@2x.png b/crates/zed/resources/app-icon-dev@2x.png deleted file mode 100644 index 8ab9979698..0000000000 Binary files a/crates/zed/resources/app-icon-dev@2x.png and /dev/null differ diff --git a/crates/zed/resources/app-icon-nightly.png b/crates/zed/resources/app-icon-nightly.png deleted file mode 100644 index 776cd06b1b..0000000000 Binary files a/crates/zed/resources/app-icon-nightly.png and /dev/null differ diff --git a/crates/zed/resources/app-icon-nightly@2x.png b/crates/zed/resources/app-icon-nightly@2x.png deleted file mode 100644 index 6d781594ac..0000000000 Binary files a/crates/zed/resources/app-icon-nightly@2x.png and /dev/null differ diff --git a/crates/zed/resources/app-icon-preview.png b/crates/zed/resources/app-icon-preview.png deleted file mode 100644 index b76e578858..0000000000 Binary files a/crates/zed/resources/app-icon-preview.png and /dev/null differ diff --git a/crates/zed/resources/app-icon-preview@2x.png b/crates/zed/resources/app-icon-preview@2x.png deleted file mode 100644 index 6e08503927..0000000000 Binary files a/crates/zed/resources/app-icon-preview@2x.png and /dev/null differ diff --git a/crates/zed/resources/app-icon.png b/crates/zed/resources/app-icon.png deleted file mode 100644 index 08b6d8afa0..0000000000 Binary files a/crates/zed/resources/app-icon.png and /dev/null differ diff --git a/crates/zed/resources/app-icon@2x.png b/crates/zed/resources/app-icon@2x.png deleted file mode 100644 index 5bb5754bc1..0000000000 Binary files a/crates/zed/resources/app-icon@2x.png and /dev/null differ diff --git a/crates/zed/resources/flatpak/manifest-template.json b/crates/zed/resources/flatpak/manifest-template.json deleted file mode 100644 index 0a14a1c2b0..0000000000 --- a/crates/zed/resources/flatpak/manifest-template.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "id": "$APP_ID", - "runtime": "org.freedesktop.Platform", - "runtime-version": "23.08", - "sdk": "org.freedesktop.Sdk", - "sdk-extensions": [ - "org.freedesktop.Sdk.Extension.rust-stable" - ], - "command": "zed", - "finish-args": [ - "--talk-name=org.freedesktop.Flatpak", - "--device=dri", - "--share=ipc", - "--share=network", - "--socket=wayland", - "--socket=fallback-x11", - "--socket=pulseaudio", - "--filesystem=host" - ], - "build-options": { - "append-path": "/usr/lib/sdk/rust-stable/bin" - }, - "modules": [ - { - "name": "zed", - "buildsystem": "simple", - "build-options": { - "env": { - "APP_ID": "$APP_ID", - "APP_ICON": "$APP_ID", - "APP_NAME": "$APP_NAME", - "BRANDING_LIGHT": "$BRANDING_LIGHT", - "BRANDING_DARK": "$BRANDING_DARK", - "APP_CLI": "zed", - "APP_ARGS": "--foreground %U", - "DO_STARTUP_NOTIFY": "false" - } - }, - "build-commands": [ - "install -Dm644 $ICON_FILE.png /app/share/icons/hicolor/512x512/apps/$APP_ID.png", - "envsubst < zed.desktop.in > zed.desktop && install -Dm755 zed.desktop /app/share/applications/$APP_ID.desktop", - "envsubst < flatpak/zed.metainfo.xml.in > zed.metainfo.xml && install -Dm644 zed.metainfo.xml /app/share/metainfo/$APP_ID.metainfo.xml", - "sed -i -e '/@release_info@/{r flatpak/release-info/$CHANNEL' -e 'd}' /app/share/metainfo/$APP_ID.metainfo.xml", - "install -Dm755 bin/zed /app/bin/zed", - "install -Dm755 libexec/zed-editor /app/libexec/zed-editor", - "install -Dm755 lib/* -t /app/lib" - ], - "sources": [ - { - "type": "archive", - "path": "./target/release/$ARCHIVE" - }, - { - "type": "dir", - "path": "./crates/zed/resources" - } - ] - } - ] -} diff --git a/crates/zed/resources/flatpak/release-info/dev b/crates/zed/resources/flatpak/release-info/dev deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/zed/resources/flatpak/release-info/nightly b/crates/zed/resources/flatpak/release-info/nightly deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/zed/resources/flatpak/release-info/preview b/crates/zed/resources/flatpak/release-info/preview deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/zed/resources/flatpak/release-info/stable b/crates/zed/resources/flatpak/release-info/stable deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/zed/resources/flatpak/zed.metainfo.xml.in b/crates/zed/resources/flatpak/zed.metainfo.xml.in deleted file mode 100644 index b8a88d9221..0000000000 --- a/crates/zed/resources/flatpak/zed.metainfo.xml.in +++ /dev/null @@ -1,85 +0,0 @@ - - - $APP_ID - MIT - AGPL-3.0-or-later and Apache-2.0 and GPL-3.0-or-later - - $APP_NAME - High-performance, multiplayer code editor - - Zed Industries, Inc. - - -

- Productive coding starts with a tool that stays out of your way. Zed blends the power of an IDE with the speed of a lightweight editor for productivity you can feel under your fingertips. -

-

Features:

-
    -
  • Performance: Efficiently uses every CPU core and your GPU for instant startup, quick file loading, and responsive keystrokes.
  • -
  • Language-aware: Maintains a syntax tree for precise highlighting, and auto-indent, with LSP support for autocompletion and refactoring.
  • -
  • Collaboration: Real-time editing and navigation for multiple developers in a shared workspace.
  • -
  • AI Integration: Integrates GitHub Copilot and GPT-4 for natural language code generation.
  • -
-
- - $APP_ID.desktop - - - $BRANDING_LIGHT - $BRANDING_DARK - - - - intense - intense - - - https://zed.dev - https://github.com/zed-industries/zed/issues - https://zed.dev/faq - https://zed.dev/docs/getting-started - https://zed.dev/community-links - https://github.com/zed-industries/zed - https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md - - - offline-only - - - pointing - keyboard - 768 - - - - - Zed with a large project open, showing language server and gitblame support - https://zed.dev/img/flatpak/flatpak-1.png - - - Zed with a file open and a channel message thread in the right sidebar - https://zed.dev/img/flatpak/flatpak-2.png - - - Example of a channel's shared document - https://zed.dev/img/flatpak/flatpak-3.png - - - Zed's extension list - https://zed.dev/img/flatpak/flatpak-4.png - - - Theme switcher UI and example theme - https://zed.dev/img/flatpak/flatpak-5.png - - - - - @release_info@ - - -

Dummy release to keep flatpak-builder AppStream metadata validation from complaining

-
-
-
-
diff --git a/crates/zed/resources/info/DocumentTypes.plist b/crates/zed/resources/info/DocumentTypes.plist deleted file mode 100644 index 0a509d9ddc..0000000000 --- a/crates/zed/resources/info/DocumentTypes.plist +++ /dev/null @@ -1,63 +0,0 @@ -CFBundleDocumentTypes - - - CFBundleTypeIconFile - Document - CFBundleTypeRole - Editor - LSHandlerRank - Alternate - LSItemContentTypes - - public.folder - public.plain-text - public.text - public.utf8-plain-text - - - - CFBundleTypeIconFile - Document - CFBundleTypeName - Zed Text Document - CFBundleTypeRole - Editor - CFBundleTypeOSTypes - - **** - - LSHandlerRank - Default - CFBundleTypeExtensions - - Gemfile - c - c++ - cc - cpp - css - erb - ex - exs - go - h - h++ - hh - hpp - html - js - json - jsx - md - py - rb - rkt - rs - scm - toml - ts - tsx - txt - - - diff --git a/crates/zed/resources/info/Permissions.plist b/crates/zed/resources/info/Permissions.plist deleted file mode 100644 index bded5a82e2..0000000000 --- a/crates/zed/resources/info/Permissions.plist +++ /dev/null @@ -1,24 +0,0 @@ -NSSystemAdministrationUsageDescription -The operation being performed by a program in Zed requires elevated permission. -NSAppleEventsUsageDescription -An application in Zed wants to use AppleScript. -NSBluetoothAlwaysUsageDescription -An application in Zed wants to use Bluetooth. -NSCalendarsUsageDescription -An application in Zed wants to use Calendar data. -NSCameraUsageDescription -An application in Zed wants to use the camera. -NSContactsUsageDescription -An application in Zed wants to use your contacts. -NSLocationAlwaysUsageDescription -An application in Zed wants to use your location information, even in the background. -NSLocationUsageDescription -An application in Zed wants to use your location information. -NSLocationWhenInUseUsageDescription -An application in Zed wants to use your location information while active. -NSMicrophoneUsageDescription -An application in Zed wants to use your microphone. -NSSpeechRecognitionUsageDescription -An application in Zed wants to use speech recognition. -NSRemindersUsageDescription -An application in Zed wants to use your reminders. diff --git a/crates/zed/resources/info/SupportedPlatforms.plist b/crates/zed/resources/info/SupportedPlatforms.plist deleted file mode 100644 index fd2a4101d8..0000000000 --- a/crates/zed/resources/info/SupportedPlatforms.plist +++ /dev/null @@ -1,4 +0,0 @@ -CFBundleSupportedPlatforms - - MacOSX - diff --git a/crates/zed/resources/snap/snapcraft.yaml.in b/crates/zed/resources/snap/snapcraft.yaml.in deleted file mode 100644 index 4c94a9fd03..0000000000 --- a/crates/zed/resources/snap/snapcraft.yaml.in +++ /dev/null @@ -1,59 +0,0 @@ -name: zed -title: Zed -base: core24 -version: "$RELEASE_VERSION" -summary: The editor for what's next -description: | - Zed is a modern open-source code editor, built from the ground up in Rust with - a GPU-accelerated renderer. We help you build software faster than ever before. -grade: stable -confinement: classic -compression: lzo -website: https://zed.dev/ -source-code: https://github.com/zed-industries/zed -issues: https://github.com/zed-industries/zed/issues -contact: https://zed.dev/community-links#support-and-feedback - -parts: - zed: - plugin: dump - source: "https://github.com/zed-industries/zed/releases/download/v$RELEASE_VERSION/zed-linux-x86_64.tar.gz" - - organize: - # These renames seem to not be necessary, but it's tidier. - bin: usr/bin - libexec: usr/libexec - - stage-packages: - - libasound2t64 - # snapcraft has a lint that this is unused, but without it Zed exits with - # "Missing Vulkan entry points: LibraryLoadFailure" in blade_graphics. - - libvulkan1 - # snapcraft has a lint that this is unused, but without it Zed exits with - # "NoWaylandLib" when run with Wayland. - - libwayland-client0 - - libxcb1 - - libxkbcommon-x11-0 - - libxkbcommon0 - - build-attributes: - - enable-patchelf - - prime: - # Omit unneeded files from the tarball - - -lib - - -licenses.md - - -share - - # Omit unneeded files from stage-packages - - -etc - - -usr/share/doc - - -usr/share/lintian - - -usr/share/man - -apps: - zed: - command: usr/bin/zed - common-id: dev.zed.Zed - environment: - ZED_BUNDLE_TYPE: snap diff --git a/crates/zed/resources/windows/app-icon-dev.ico b/crates/zed/resources/windows/app-icon-dev.ico deleted file mode 100644 index 1d6367b788..0000000000 Binary files a/crates/zed/resources/windows/app-icon-dev.ico and /dev/null differ diff --git a/crates/zed/resources/windows/app-icon-nightly.ico b/crates/zed/resources/windows/app-icon-nightly.ico deleted file mode 100644 index 875c0d7b35..0000000000 Binary files a/crates/zed/resources/windows/app-icon-nightly.ico and /dev/null differ diff --git a/crates/zed/resources/windows/app-icon-preview.ico b/crates/zed/resources/windows/app-icon-preview.ico deleted file mode 100644 index 5c8601d314..0000000000 Binary files a/crates/zed/resources/windows/app-icon-preview.ico and /dev/null differ diff --git a/crates/zed/resources/windows/app-icon.ico b/crates/zed/resources/windows/app-icon.ico deleted file mode 100644 index 9c5761b9e9..0000000000 Binary files a/crates/zed/resources/windows/app-icon.ico and /dev/null differ diff --git a/crates/zed/resources/windows/bin/x64/OpenConsole.exe b/crates/zed/resources/windows/bin/x64/OpenConsole.exe deleted file mode 100644 index 8bb6ab2188..0000000000 Binary files a/crates/zed/resources/windows/bin/x64/OpenConsole.exe and /dev/null differ diff --git a/crates/zed/resources/windows/bin/x64/conpty.dll b/crates/zed/resources/windows/bin/x64/conpty.dll deleted file mode 100644 index 555d6bf655..0000000000 Binary files a/crates/zed/resources/windows/bin/x64/conpty.dll and /dev/null differ diff --git a/crates/zed/resources/windows/messages/Default.zh-cn.isl b/crates/zed/resources/windows/messages/Default.zh-cn.isl deleted file mode 100644 index d900c7d448..0000000000 --- a/crates/zed/resources/windows/messages/Default.zh-cn.isl +++ /dev/null @@ -1,403 +0,0 @@ -; *** Inno Setup version 6.4.0+ Chinese Simplified messages *** -; -; To download user-contributed translations of this file, go to: -; https://jrsoftware.org/files/istrans/ -; -; Note: When translating this text, do not add periods (.) to the end of -; messages that didn't have them already, because on those messages Inno -; Setup adds the periods automatically (appending a period would result in -; two periods being displayed). -; -; Maintained by Zhenghan Yang -; Email: 847320916@QQ.com -; Translation based on network resource -; The latest Translation is on https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation -; - -[LangOptions] -; The following three entries are very important. Be sure to read and -; understand the '[LangOptions] section' topic in the help file. -LanguageName=简体中文 -; If Language Name display incorrect, uncomment next line -; LanguageName=<7B80><4F53><4E2D><6587> -; About LanguageID, to reference link: -; https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c -LanguageID=$0804 -; About CodePage, to reference link: -; https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers -LanguageCodePage=936 -; If the language you are translating to requires special font faces or -; sizes, uncomment any of the following entries and change them accordingly. -;DialogFontName= -;DialogFontSize=8 -;WelcomeFontName=Verdana -;WelcomeFontSize=12 -;TitleFontName=Arial -;TitleFontSize=29 -;CopyrightFontName=Arial -;CopyrightFontSize=8 - -[Messages] - -; *** 应用程序标题 -SetupAppTitle=安装 -SetupWindowTitle=安装 - %1 -UninstallAppTitle=卸载 -UninstallAppFullTitle=%1 卸载 - -; *** Misc. common -InformationTitle=信息 -ConfirmTitle=确认 -ErrorTitle=错误 - -; *** SetupLdr messages -SetupLdrStartupMessage=现在将安装 %1。您想要继续吗? -LdrCannotCreateTemp=无法创建临时文件。安装程序已中止 -LdrCannotExecTemp=无法执行临时目录中的文件。安装程序已中止 -HelpTextNote= - -; *** 启动错误消息 -LastErrorMessage=%1。%n%n错误 %2: %3 -SetupFileMissing=安装目录中缺少文件 %1。请修正这个问题或者获取程序的新副本。 -SetupFileCorrupt=安装文件已损坏。请获取程序的新副本。 -SetupFileCorruptOrWrongVer=安装文件已损坏,或是与这个安装程序的版本不兼容。请修正这个问题或获取新的程序副本。 -InvalidParameter=无效的命令行参数:%n%n%1 -SetupAlreadyRunning=安装程序正在运行。 -WindowsVersionNotSupported=此程序不支持当前计算机运行的 Windows 版本。 -WindowsServicePackRequired=此程序需要 %1 服务包 %2 或更高版本。 -NotOnThisPlatform=此程序不能在 %1 上运行。 -OnlyOnThisPlatform=此程序只能在 %1 上运行。 -OnlyOnTheseArchitectures=此程序只能安装到为下列处理器架构设计的 Windows 版本中:%n%n%1 -WinVersionTooLowError=此程序需要 %1 版本 %2 或更高。 -WinVersionTooHighError=此程序不能安装于 %1 版本 %2 或更高。 -AdminPrivilegesRequired=在安装此程序时您必须以管理员身份登录。 -PowerUserPrivilegesRequired=在安装此程序时您必须以管理员身份或有权限的用户组身份登录。 -SetupAppRunningError=安装程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。 -UninstallAppRunningError=卸载程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。 - -; *** 启动问题 -PrivilegesRequiredOverrideTitle=选择安装程序模式 -PrivilegesRequiredOverrideInstruction=选择安装模式 -PrivilegesRequiredOverrideText1=%1 可以为所有用户安装(需要管理员权限),或仅为您安装。 -PrivilegesRequiredOverrideText2=%1 只能为您安装,或为所有用户安装(需要管理员权限)。 -PrivilegesRequiredOverrideAllUsers=为所有用户安装(&A) -PrivilegesRequiredOverrideAllUsersRecommended=为所有用户安装(&A) (建议选项) -PrivilegesRequiredOverrideCurrentUser=只为我安装(&M) -PrivilegesRequiredOverrideCurrentUserRecommended=只为我安装(&M) (建议选项) - -; *** 其他错误 -ErrorCreatingDir=安装程序无法创建目录“%1” -ErrorTooManyFilesInDir=无法在目录“%1”中创建文件,因为里面包含太多文件 - -; *** 安装程序公共消息 -ExitSetupTitle=退出安装程序 -ExitSetupMessage=安装程序尚未完成。如果现在退出,将不会安装该程序。%n%n您之后可以再次运行安装程序完成安装。%n%n现在退出安装程序吗? -AboutSetupMenuItem=关于安装程序(&A)... -AboutSetupTitle=关于安装程序 -AboutSetupMessage=%1 版本 %2%n%3%n%n%1 主页:%n%4 -AboutSetupNote= -TranslatorNote=简体中文翻译由Kira(847320916@qq.com)维护。项目地址:https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation - -; *** 按钮 -ButtonBack=< 上一步(&B) -ButtonNext=下一步(&N) > -ButtonInstall=安装(&I) -ButtonOK=确定 -ButtonCancel=取消 -ButtonYes=是(&Y) -ButtonYesToAll=全是(&A) -ButtonNo=否(&N) -ButtonNoToAll=全否(&O) -ButtonFinish=完成(&F) -ButtonBrowse=浏览(&B)... -ButtonWizardBrowse=浏览(&R)... -ButtonNewFolder=新建文件夹(&M) - -; *** “选择语言”对话框消息 -SelectLanguageTitle=选择安装语言 -SelectLanguageLabel=选择安装时使用的语言。 - -; *** 公共向导文字 -ClickNext=点击“下一步”继续,或点击“取消”退出安装程序。 -BeveledLabel= -BrowseDialogTitle=浏览文件夹 -BrowseDialogLabel=在下面的列表中选择一个文件夹,然后点击“确定”。 -NewFolderName=新建文件夹 - -; *** “欢迎”向导页 -WelcomeLabel1=欢迎使用 [name] 安装向导 -WelcomeLabel2=现在将安装 [name/ver] 到您的电脑中。%n%n建议您在继续安装前关闭所有其他应用程序。 - -; *** “密码”向导页 -WizardPassword=密码 -PasswordLabel1=这个安装程序有密码保护。 -PasswordLabel3=请输入密码,然后点击“下一步”继续。密码区分大小写。 -PasswordEditLabel=密码(&P): -IncorrectPassword=您输入的密码不正确,请重新输入。 - -; *** “许可协议”向导页 -WizardLicense=许可协议 -LicenseLabel=请在继续安装前阅读以下重要信息。 -LicenseLabel3=请仔细阅读下列许可协议。在继续安装前您必须同意这些协议条款。 -LicenseAccepted=我同意此协议(&A) -LicenseNotAccepted=我不同意此协议(&D) - -; *** “信息”向导页 -WizardInfoBefore=信息 -InfoBeforeLabel=请在继续安装前阅读以下重要信息。 -InfoBeforeClickLabel=准备好继续安装后,点击“下一步”。 -WizardInfoAfter=信息 -InfoAfterLabel=请在继续安装前阅读以下重要信息。 -InfoAfterClickLabel=准备好继续安装后,点击“下一步”。 - -; *** “用户信息”向导页 -WizardUserInfo=用户信息 -UserInfoDesc=请输入您的信息。 -UserInfoName=用户名(&U): -UserInfoOrg=组织(&O): -UserInfoSerial=序列号(&S): -UserInfoNameRequired=您必须输入用户名。 - -; *** “选择目标目录”向导页 -WizardSelectDir=选择目标位置 -SelectDirDesc=您想将 [name] 安装在哪里? -SelectDirLabel3=安装程序将安装 [name] 到下面的文件夹中。 -SelectDirBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。 -DiskSpaceGBLabel=至少需要有 [gb] GB 的可用磁盘空间。 -DiskSpaceMBLabel=至少需要有 [mb] MB 的可用磁盘空间。 -CannotInstallToNetworkDrive=安装程序无法安装到一个网络驱动器。 -CannotInstallToUNCPath=安装程序无法安装到一个 UNC 路径。 -InvalidPath=您必须输入一个带驱动器卷标的完整路径,例如:%n%nC:\APP%n%n或UNC路径:%n%n\\server\share -InvalidDrive=您选定的驱动器或 UNC 共享不存在或不能访问。请选择其他位置。 -DiskSpaceWarningTitle=磁盘空间不足 -DiskSpaceWarning=安装程序至少需要 %1 KB 的可用空间才能安装,但选定驱动器只有 %2 KB 的可用空间。%n%n您一定要继续吗? -DirNameTooLong=文件夹名称或路径太长。 -InvalidDirName=文件夹名称无效。 -BadDirName32=文件夹名称不能包含下列任何字符:%n%n%1 -DirExistsTitle=文件夹已存在 -DirExists=文件夹:%n%n%1%n%n已经存在。您一定要安装到这个文件夹中吗? -DirDoesntExistTitle=文件夹不存在 -DirDoesntExist=文件夹:%n%n%1%n%n不存在。您想要创建此文件夹吗? - -; *** “选择组件”向导页 -WizardSelectComponents=选择组件 -SelectComponentsDesc=您想安装哪些程序组件? -SelectComponentsLabel2=选中您想安装的组件;取消您不想安装的组件。然后点击“下一步”继续。 -FullInstallation=完全安装 -; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language) -CompactInstallation=简洁安装 -CustomInstallation=自定义安装 -NoUninstallWarningTitle=组件已存在 -NoUninstallWarning=安装程序检测到下列组件已安装在您的电脑中:%n%n%1%n%n取消选中这些组件不会卸载它们。%n%n确定要继续吗? -ComponentSize1=%1 KB -ComponentSize2=%1 MB -ComponentsDiskSpaceGBLabel=当前选择的组件需要至少 [gb] GB 的磁盘空间。 -ComponentsDiskSpaceMBLabel=当前选择的组件需要至少 [mb] MB 的磁盘空间。 - -; *** “选择附加任务”向导页 -WizardSelectTasks=选择附加任务 -SelectTasksDesc=您想要安装程序执行哪些附加任务? -SelectTasksLabel2=选择您想要安装程序在安装 [name] 时执行的附加任务,然后点击“下一步”。 - -; *** “选择开始菜单文件夹”向导页 -WizardSelectProgramGroup=选择开始菜单文件夹 -SelectStartMenuFolderDesc=安装程序应该在哪里放置程序的快捷方式? -SelectStartMenuFolderLabel3=安装程序将在下列“开始”菜单文件夹中创建程序的快捷方式。 -SelectStartMenuFolderBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。 -MustEnterGroupName=您必须输入一个文件夹名。 -GroupNameTooLong=文件夹名或路径太长。 -InvalidGroupName=无效的文件夹名字。 -BadGroupName=文件夹名不能包含下列任何字符:%n%n%1 -NoProgramGroupCheck2=不创建开始菜单文件夹(&D) - -; *** “准备安装”向导页 -WizardReady=准备安装 -ReadyLabel1=安装程序准备就绪,现在可以开始安装 [name] 到您的电脑。 -ReadyLabel2a=点击“安装”继续此安装程序。如果您想重新考虑或修改任何设置,点击“上一步”。 -ReadyLabel2b=点击“安装”继续此安装程序。 -ReadyMemoUserInfo=用户信息: -ReadyMemoDir=目标位置: -ReadyMemoType=安装类型: -ReadyMemoComponents=已选择组件: -ReadyMemoGroup=开始菜单文件夹: -ReadyMemoTasks=附加任务: - -; *** TExtractionWizardPage wizard page and Extract7ZipArchive -ExtractionLabel=正在提取附加文件... -ButtonStopExtraction=停止提取(&S) -StopExtraction=您确定要停止提取吗? -ErrorExtractionAborted=提取已中止 -ErrorExtractionFailed=提取失败:%1 - -; *** TDownloadWizardPage wizard page and DownloadTemporaryFile -DownloadingLabel=正在下载附加文件... -ButtonStopDownload=停止下载(&S) -StopDownload=您确定要停止下载吗? -ErrorDownloadAborted=下载已中止 -ErrorDownloadFailed=下载失败:%1 %2 -ErrorDownloadSizeFailed=获取下载大小失败:%1 %2 -ErrorFileHash1=校验文件哈希失败:%1 -ErrorFileHash2=无效的文件哈希:预期 %1,实际 %2 -ErrorProgress=无效的进度:%1 / %2 -ErrorFileSize=文件大小错误:预期 %1,实际 %2 - -; *** “正在准备安装”向导页 -WizardPreparing=正在准备安装 -PreparingDesc=安装程序正在准备安装 [name] 到您的电脑。 -PreviousInstallNotCompleted=先前的程序安装或卸载未完成,您需要重启您的电脑以完成。%n%n在重启电脑后,再次运行安装程序以完成 [name] 的安装。 -CannotContinue=安装程序不能继续。请点击“取消”退出。 -ApplicationsFound=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。 -ApplicationsFound2=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。安装完成后,安装程序将尝试重新启动这些应用程序。 -CloseApplications=自动关闭应用程序(&A) -DontCloseApplications=不要关闭应用程序(&D) -ErrorCloseApplications=安装程序无法自动关闭所有应用程序。建议您在继续之前,关闭所有在使用需要由安装程序更新的文件的应用程序。 -PrepareToInstallNeedsRestart=安装程序必须重启您的计算机。计算机重启后,请再次运行安装程序以完成 [name] 的安装。%n%n是否立即重新启动? - -; *** “正在安装”向导页 -WizardInstalling=正在安装 -InstallingLabel=安装程序正在安装 [name] 到您的电脑,请稍候。 - -; *** “安装完成”向导页 -FinishedHeadingLabel=[name] 安装完成 -FinishedLabelNoIcons=安装程序已在您的电脑中安装了 [name]。 -FinishedLabel=安装程序已在您的电脑中安装了 [name]。您可以通过已安装的快捷方式运行此应用程序。 -ClickFinish=点击“完成”退出安装程序。 -FinishedRestartLabel=为完成 [name] 的安装,安装程序必须重新启动您的电脑。要立即重启吗? -FinishedRestartMessage=为完成 [name] 的安装,安装程序必须重新启动您的电脑。%n%n要立即重启吗? -ShowReadmeCheck=是,我想查阅自述文件 -YesRadio=是,立即重启电脑(&Y) -NoRadio=否,稍后重启电脑(&N) -; used for example as 'Run MyProg.exe' -RunEntryExec=运行 %1 -; used for example as 'View Readme.txt' -RunEntryShellExec=查阅 %1 - -; *** “安装程序需要下一张磁盘”提示 -ChangeDiskTitle=安装程序需要下一张磁盘 -SelectDiskLabel2=请插入磁盘 %1 并点击“确定”。%n%n如果这个磁盘中的文件可以在下列文件夹之外的文件夹中找到,请输入正确的路径或点击“浏览”。 -PathLabel=路径(&P): -FileNotInDir2=“%2”中找不到文件“%1”。请插入正确的磁盘或选择其他文件夹。 -SelectDirectoryLabel=请指定下一张磁盘的位置。 - -; *** 安装状态消息 -SetupAborted=安装程序未完成安装。%n%n请修正这个问题并重新运行安装程序。 -AbortRetryIgnoreSelectAction=选择操作 -AbortRetryIgnoreRetry=重试(&T) -AbortRetryIgnoreIgnore=忽略错误并继续(&I) -AbortRetryIgnoreCancel=关闭安装程序 - -; *** 安装状态消息 -StatusClosingApplications=正在关闭应用程序... -StatusCreateDirs=正在创建目录... -StatusExtractFiles=正在解压缩文件... -StatusCreateIcons=正在创建快捷方式... -StatusCreateIniEntries=正在创建 INI 条目... -StatusCreateRegistryEntries=正在创建注册表条目... -StatusRegisterFiles=正在注册文件... -StatusSavingUninstall=正在保存卸载信息... -StatusRunProgram=正在完成安装... -StatusRestartingApplications=正在重启应用程序... -StatusRollback=正在撤销更改... - -; *** 其他错误 -ErrorInternal2=内部错误:%1 -ErrorFunctionFailedNoCode=%1 失败 -ErrorFunctionFailed=%1 失败;错误代码 %2 -ErrorFunctionFailedWithMessage=%1 失败;错误代码 %2.%n%3 -ErrorExecutingProgram=无法执行文件:%n%1 - -; *** 注册表错误 -ErrorRegOpenKey=打开注册表项时出错:%n%1\%2 -ErrorRegCreateKey=创建注册表项时出错:%n%1\%2 -ErrorRegWriteKey=写入注册表项时出错:%n%1\%2 - -; *** INI 错误 -ErrorIniEntry=在文件“%1”中创建 INI 条目时出错。 - -; *** 文件复制错误 -FileAbortRetryIgnoreSkipNotRecommended=跳过此文件(&S) (不推荐) -FileAbortRetryIgnoreIgnoreNotRecommended=忽略错误并继续(&I) (不推荐) -SourceIsCorrupted=源文件已损坏 -SourceDoesntExist=源文件“%1”不存在 -ExistingFileReadOnly2=无法替换现有文件,它是只读的。 -ExistingFileReadOnlyRetry=移除只读属性并重试(&R) -ExistingFileReadOnlyKeepExisting=保留现有文件(&K) -ErrorReadingExistingDest=尝试读取现有文件时出错: -FileExistsSelectAction=选择操作 -FileExists2=文件已经存在。 -FileExistsOverwriteExisting=覆盖已存在的文件(&O) -FileExistsKeepExisting=保留现有的文件(&K) -FileExistsOverwriteOrKeepAll=为所有冲突文件执行此操作(&D) -ExistingFileNewerSelectAction=选择操作 -ExistingFileNewer2=现有的文件比安装程序将要安装的文件还要新。 -ExistingFileNewerOverwriteExisting=覆盖已存在的文件(&O) -ExistingFileNewerKeepExisting=保留现有的文件(&K) (推荐) -ExistingFileNewerOverwriteOrKeepAll=为所有冲突文件执行此操作(&D) -ErrorChangingAttr=尝试更改下列现有文件的属性时出错: -ErrorCreatingTemp=尝试在目标目录创建文件时出错: -ErrorReadingSource=尝试读取下列源文件时出错: -ErrorCopying=尝试复制下列文件时出错: -ErrorReplacingExistingFile=尝试替换现有文件时出错: -ErrorRestartReplace=重启并替换失败: -ErrorRenamingTemp=尝试重命名下列目标目录中的一个文件时出错: -ErrorRegisterServer=无法注册 DLL/OCX:%1 -ErrorRegSvr32Failed=RegSvr32 失败;退出代码 %1 -ErrorRegisterTypeLib=无法注册类库:%1 - -; *** 卸载显示名字标记 -; used for example as 'My Program (32-bit)' -UninstallDisplayNameMark=%1 (%2) -; used for example as 'My Program (32-bit, All users)' -UninstallDisplayNameMarks=%1 (%2, %3) -UninstallDisplayNameMark32Bit=32 位 -UninstallDisplayNameMark64Bit=64 位 -UninstallDisplayNameMarkAllUsers=所有用户 -UninstallDisplayNameMarkCurrentUser=当前用户 - -; *** 安装后错误 -ErrorOpeningReadme=尝试打开自述文件时出错。 -ErrorRestartingComputer=安装程序无法重启电脑,请手动重启。 - -; *** 卸载消息 -UninstallNotFound=文件“%1”不存在。无法卸载。 -UninstallOpenError=文件“%1”不能被打开。无法卸载。 -UninstallUnsupportedVer=此版本的卸载程序无法识别卸载日志文件“%1”的格式。无法卸载 -UninstallUnknownEntry=卸载日志中遇到一个未知条目 (%1) -ConfirmUninstall=您确认要完全移除 %1 及其所有组件吗? -UninstallOnlyOnWin64=仅允许在 64 位 Windows 中卸载此程序。 -OnlyAdminCanUninstall=仅使用管理员权限的用户能完成此卸载。 -UninstallStatusLabel=正在从您的电脑中移除 %1,请稍候。 -UninstalledAll=已顺利从您的电脑中移除 %1。 -UninstalledMost=%1 卸载完成。%n%n有部分内容未能被删除,但您可以手动删除它们。 -UninstalledAndNeedsRestart=为完成 %1 的卸载,需要重启您的电脑。%n%n立即重启电脑吗? -UninstallDataCorrupted=文件“%1”已损坏。无法卸载 - -; *** 卸载状态消息 -ConfirmDeleteSharedFileTitle=删除共享的文件吗? -ConfirmDeleteSharedFile2=系统表示下列共享的文件已不有其他程序使用。您希望卸载程序删除这些共享的文件吗?%n%n如果删除这些文件,但仍有程序在使用这些文件,则这些程序可能出现异常。如果您不能确定,请选择“否”,在系统中保留这些文件以免引发问题。 -SharedFileNameLabel=文件名: -SharedFileLocationLabel=位置: -WizardUninstalling=卸载状态 -StatusUninstalling=正在卸载 %1... - -; *** Shutdown block reasons -ShutdownBlockReasonInstallingApp=正在安装 %1。 -ShutdownBlockReasonUninstallingApp=正在卸载 %1。 - -; The custom messages below aren't used by Setup itself, but if you make -; use of them in your scripts, you'll want to translate them. - -[CustomMessages] - -NameAndVersion=%1 版本 %2 -AdditionalIcons=附加快捷方式: -CreateDesktopIcon=创建桌面快捷方式(&D) -CreateQuickLaunchIcon=创建快速启动栏快捷方式(&Q) -ProgramOnTheWeb=%1 网站 -UninstallProgram=卸载 %1 -LaunchProgram=运行 %1 -AssocFileExtension=将 %2 文件扩展名与 %1 建立关联(&A) -AssocingFileExtension=正在将 %2 文件扩展名与 %1 建立关联... -AutoStartProgramGroupDescription=启动: -AutoStartProgram=自动启动 %1 -AddonHostProgramNotFound=您选择的文件夹中无法找到 %1。%n%n您要继续吗? diff --git a/crates/zed/resources/windows/messages/en.isl b/crates/zed/resources/windows/messages/en.isl deleted file mode 100644 index 2e82bea4ff..0000000000 --- a/crates/zed/resources/windows/messages/en.isl +++ /dev/null @@ -1,15 +0,0 @@ -[Messages] -FinishedLabel=Setup has finished installing [name] on your computer. The application may be launched by selecting the installed shortcuts. -ConfirmUninstall=Are you sure you want to completely remove %1 and all of its components? - -[CustomMessages] -AdditionalIcons=Additional icons: -CreateDesktopIcon=Create a &desktop icon -AddContextMenuFiles=Add "Open with %1" action to Windows Explorer file context menu -AddContextMenuFolders=Add "Open with %1" action to Windows Explorer directory context menu -AssociateWithFiles=Register %1 as an editor for supported file types -AddToPath=Add to PATH (requires shell restart) -RunAfter=Run %1 after installation -Other=Other: -SourceFile=%1 Source File -OpenWithContextMenu=Open w&ith %1 diff --git a/crates/zed/resources/windows/messages/zh-cn.isl b/crates/zed/resources/windows/messages/zh-cn.isl deleted file mode 100644 index 50c03ccaaf..0000000000 --- a/crates/zed/resources/windows/messages/zh-cn.isl +++ /dev/null @@ -1,9 +0,0 @@ -[CustomMessages] -AddContextMenuFiles=将“通过 %1 打开”操作添加到 Windows 资源管理器文件上下文菜单 -AddContextMenuFolders=将“通过 %1 打开”操作添加到 Windows 资源管理器目录上下文菜单 -AssociateWithFiles=将 %1 注册为受支持的文件类型的编辑器 -AddToPath=添加到 PATH (重启后生效) -RunAfter=安装后运行 %1 -Other=其他: -SourceFile=%1 源文件 -OpenWithContextMenu=通过 %1 打开 diff --git a/crates/zed/resources/windows/sign.ps1 b/crates/zed/resources/windows/sign.ps1 deleted file mode 100644 index d00b33c0fc..0000000000 --- a/crates/zed/resources/windows/sign.ps1 +++ /dev/null @@ -1,53 +0,0 @@ -param ( - [Parameter(Mandatory = $true)] - [string]$filePath -) - -$params = @{} - -$endpoint = $ENV:ENDPOINT -if ([string]::IsNullOrWhiteSpace($endpoint)) { - throw "The 'ENDPOINT' env is required." -} -$params["Endpoint"] = $endpoint - -$trustedSigningAccountName = $ENV:ACCOUNT_NAME -if ([string]::IsNullOrWhiteSpace($trustedSigningAccountName)) { - throw "The 'ACCOUNT_NAME' env is required." -} -$params["CodeSigningAccountName"] = $trustedSigningAccountName - -$certificateProfileName = $ENV:CERT_PROFILE_NAME -if ([string]::IsNullOrWhiteSpace($certificateProfileName)) { - throw "The 'CERT_PROFILE_NAME' env is required." -} -$params["CertificateProfileName"] = $certificateProfileName - -$fileDigest = $ENV:FILE_DIGEST -if ([string]::IsNullOrWhiteSpace($fileDigest)) { - throw "The 'FILE_DIGEST' env is required." -} -$params["FileDigest"] = $fileDigest - -$timeStampDigest = $ENV:TIMESTAMP_DIGEST -if ([string]::IsNullOrWhiteSpace($timeStampDigest)) { - throw "The 'TIMESTAMP_DIGEST' env is required." -} -$params["TimestampDigest"] = $timeStampDigest - -$timeStampServer = $ENV:TIMESTAMP_SERVER -if ([string]::IsNullOrWhiteSpace($timeStampServer)) { - throw "The 'TIMESTAMP_SERVER' env is required." -} -$params["TimestampRfc3161"] = $timeStampServer - -$params["Files"] = $filePath - -$trace = $ENV:TRACE -if (-Not [string]::IsNullOrWhiteSpace($trace)) { - if ([System.Convert]::ToBoolean($trace)) { - Set-PSDebug -Trace 2 - } -} - -Invoke-TrustedSigning @params diff --git a/crates/zed/resources/windows/zed.iss b/crates/zed/resources/windows/zed.iss deleted file mode 100644 index 9df6d3b228..0000000000 --- a/crates/zed/resources/windows/zed.iss +++ /dev/null @@ -1,1421 +0,0 @@ -[Setup] -AppId={#AppId} -AppName={#AppName} -AppVerName={#AppDisplayName} -AppPublisher=Zed Industries -AppPublisherURL=https://www.zed.dev/ -AppSupportURL=https://www.zed.dev/ -AppUpdatesURL=https://www.zed.dev/ -DefaultGroupName={#AppName} -DisableProgramGroupPage=yes -DisableReadyPage=yes -AllowNoIcons=yes -OutputDir={#OutputDir} -OutputBaseFilename={#AppSetupName} -Compression=lzma -SolidCompression=yes -AppMutex={code:GetAppMutex} -SetupMutex={#AppMutex}Setup -; WizardImageFile="{#ResourcesDir}\inno-100.bmp,{#ResourcesDir}\inno-125.bmp,{#ResourcesDir}\inno-150.bmp,{#ResourcesDir}\inno-175.bmp,{#ResourcesDir}\inno-200.bmp,{#ResourcesDir}\inno-225.bmp,{#ResourcesDir}\inno-250.bmp" -; WizardSmallImageFile="{#ResourcesDir}\inno-small-100.bmp,{#ResourcesDir}\inno-small-125.bmp,{#ResourcesDir}\inno-small-150.bmp,{#ResourcesDir}\inno-small-175.bmp,{#ResourcesDir}\inno-small-200.bmp,{#ResourcesDir}\inno-small-225.bmp,{#ResourcesDir}\inno-small-250.bmp" -SetupIconFile={#ResourcesDir}\{#AppIconName}.ico -UninstallDisplayIcon={app}\{#AppExeName}.exe -ChangesEnvironment=true -ChangesAssociations=true -MinVersion=10.0.16299 -SourceDir={#SourceDir} -AppVersion={#Version} -VersionInfoVersion={#Version} -ShowLanguageDialog=auto -WizardStyle=modern - -CloseApplications=force - -#if GetEnv("CI") != "" -SignTool=Defaultsign -#endif - -DefaultDirName={autopf}\{#AppName} -PrivilegesRequired=lowest - -ArchitecturesAllowed=x64compatible -ArchitecturesInstallIn64BitMode=x64compatible - -[Languages] -Name: "english"; MessagesFile: "compiler:Default.isl,{#ResourcesDir}\messages\en.isl"; LicenseFile: "script\terms\terms.rtf" -Name: "simplifiedChinese"; MessagesFile: "{#ResourcesDir}\messages\Default.zh-cn.isl,{#ResourcesDir}\messages\zh-cn.isl"; LicenseFile: "script\terms\terms.rtf" - -[UninstallDelete] -; Delete logs -Type: filesandordirs; Name: "{app}\tools" -Type: filesandordirs; Name: "{app}\updates" -; Delete newer files which may not have been added by the initial installation -Type: filesandordirs; Name: "{app}\x64" -Type: filesandordirs; Name: "{app}\arm64" - - -[Tasks] -Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked -Name: "addcontextmenufiles"; Description: "{cm:AddContextMenuFiles,{#AppDisplayName}}"; GroupDescription: "{cm:Other}" -Name: "addcontextmenufolders"; Description: "{cm:AddContextMenuFolders,{#AppDisplayName}}"; GroupDescription: "{cm:Other}"; Flags: unchecked; Check: not IsWindows11OrLater -Name: "associatewithfiles"; Description: "{cm:AssociateWithFiles,{#AppDisplayName}}"; GroupDescription: "{cm:Other}" -Name: "addtopath"; Description: "{cm:AddToPath}"; GroupDescription: "{cm:Other}" - -[Dirs] -Name: "{app}"; AfterInstall: DisableAppDirInheritance - -[Files] -Source: "{#ResourcesDir}\Zed.exe"; DestDir: "{code:GetInstallDir}"; Flags: ignoreversion -Source: "{#ResourcesDir}\bin\*"; DestDir: "{code:GetInstallDir}\bin"; Flags: ignoreversion -Source: "{#ResourcesDir}\tools\*"; DestDir: "{app}\tools"; Flags: ignoreversion -Source: "{#ResourcesDir}\appx\*"; DestDir: "{app}\appx"; BeforeInstall: RemoveAppxPackage; AfterInstall: AddAppxPackage; Flags: ignoreversion; Check: IsWindows11OrLater -#ifexist ResourcesDir + "\amd_ags_x64.dll" -Source: "{#ResourcesDir}\amd_ags_x64.dll"; DestDir: "{app}"; Flags: ignoreversion -#endif -#ifexist ResourcesDir + "\x64\OpenConsole.exe" -Source: "{#ResourcesDir}\x64\OpenConsole.exe"; DestDir: "{code:GetInstallDir}\x64"; Flags: ignoreversion -#endif -#ifexist ResourcesDir + "\arm64\OpenConsole.exe" -Source: "{#ResourcesDir}\arm64\OpenConsole.exe"; DestDir: "{code:GetInstallDir}\arm64"; Flags: ignoreversion -#endif -Source: "{#ResourcesDir}\conpty.dll"; DestDir: "{code:GetInstallDir}"; Flags: ignoreversion - -[Icons] -Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}.exe"; AppUserModelID: "{#AppUserId}" -Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}.exe"; Tasks: desktopicon; AppUserModelID: "{#AppUserId}" - -[Run] -Filename: "{app}\{#AppExeName}.exe"; Description: "{cm:LaunchProgram,{#AppName}}"; Flags: nowait postinstall; Check: WizardNotSilent - -[UninstallRun] -Filename: "powershell.exe"; Parameters: "Invoke-Command -ScriptBlock {{Remove-AppxPackage -Package ""{#AppxFullName}""}"; Check: IsWindows11OrLater; Flags: shellexec waituntilterminated runhidden - -[Registry] -Root: HKCU; Subkey: "Software\Classes\.ascx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.ascx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ascx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ascx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASCX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ascx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ascx\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ascx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ascx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.asp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.asp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.asp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.asp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASP}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.asp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.asp\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.asp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.asp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.aspx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.aspx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.aspx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.aspx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASPX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.aspx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.aspx\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.aspx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.aspx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.bash\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.bash\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.bash_login\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.bash_login\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_login"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_login"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Login}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_login"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_login\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_login\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_login\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.bash_logout\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.bash_logout\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_logout"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_logout"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Logout}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_logout"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_logout\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_logout\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_logout\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.bash_profile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.bash_profile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_profile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_profile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_profile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_profile\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_profile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_profile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.bashrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.bashrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bashrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bashrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bashrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bashrc\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bashrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bashrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.bib\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.bib\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bib"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bib"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,BibTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bib"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bib\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bib\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bib\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.bowerrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.bowerrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bowerrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bowerrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bower RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bowerrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bowerrc\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bowerrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bowerrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.c++\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.c++\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.c++"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c++"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c++"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c++\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c++\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.c\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.c\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.c"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cc\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cfg\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cfg\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cfg"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cfg"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Configuration}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cfg"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cfg\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cfg\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cfg\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cjs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cjs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cjs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cjs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cjs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cjs\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cjs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cjs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.clj\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.clj\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.clj"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clj"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Clojure}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clj"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clj\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clj\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clj\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cljs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cljs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cljs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ClojureScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljs\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cljx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cljx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cljx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CLJX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljx\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.clojure\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.clojure\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.clojure"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clojure"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Clojure}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clojure"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clojure\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clojure\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clojure\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cls\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cls\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cls"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cls"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LaTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cls"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cls\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cls\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cls\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.code-workspace\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.code-workspace\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.code-workspace"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.code-workspace"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Code Workspace}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.code"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.code-workspace\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.code-workspace\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.code-workspace\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cmake\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cmake\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cmake"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cmake"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CMake}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cmake"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cmake\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cmake\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cmake\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.coffee\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.coffee\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.coffee"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.coffee"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CoffeeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.coffee"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.coffee\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.coffee\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.coffee\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.config\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.config\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.config"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.config"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Configuration}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.config"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.config\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.config\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.config\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.containerfile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.containerfile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.containerfile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.containerfile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Containerfile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.containerfile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.containerfile\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.containerfile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cpp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cpp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cpp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cpp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cpp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cpp\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cpp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cpp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C#}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cs\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cshtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cshtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cshtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cshtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CSHTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cshtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cshtml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cshtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cshtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.csproj\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.csproj\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csproj"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csproj"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C# Project}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csproj"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csproj\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csproj\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csproj\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.css\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.css\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.css"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.css"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CSS}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.css"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.css\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.css\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.css\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.csv\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.csv\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csv"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csv"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Comma Separated Values}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csv"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csv\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csv\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csv\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.csx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.csx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csx\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.ctp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.ctp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ctp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ctp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CakePHP Template}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ctp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ctp\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ctp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ctp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cxx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cxx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cxx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cxx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cxx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cxx\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cxx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cxx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.dart\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.dart\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dart"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dart"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dart}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dart"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dart\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dart\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dart\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.diff\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.diff\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.diff"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.diff"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Diff}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.diff"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.diff\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.diff\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.diff\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.dockerfile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.dockerfile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dockerfile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dockerfile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dockerfile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dockerfile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dockerfile\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dockerfile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dockerfile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.dot\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.dot\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dot"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dot"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dot}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dot"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dot\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dot\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dot\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.dtd\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.dtd\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dtd"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dtd"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Document Type Definition}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dtd"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dtd\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dtd\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dtd\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.editorconfig\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.editorconfig\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.editorconfig"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.editorconfig"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Editor Config}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.editorconfig"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.editorconfig\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.editorconfig\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.editorconfig\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.edn\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.edn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.edn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.edn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Extensible Data Notation}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.edn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.edn\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.edn\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.edn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.erb\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.erb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.erb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.erb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Ruby}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.erb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.erb\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.erb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.erb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.eyaml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.eyaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.eyaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Hiera Eyaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyaml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyaml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.eyml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.eyml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.eyml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Hiera Eyaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.fs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.fs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F#}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fs\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.fsi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.fsi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Signature}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsi\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.fsscript\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.fsscript\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsscript"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsscript"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsscript"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsscript\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsscript\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsscript\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.fsx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.fsx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsx\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.gemspec\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.gemspec\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gemspec"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gemspec"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Gemspec}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gemspec"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gemspec\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gemspec\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gemspec\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.gitattributes\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.gitattributes\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gitattributes"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Attributes}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitattributes\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitattributes\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitattributes\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.gitconfig\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.gitconfig\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gitconfig"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Config}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitconfig\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitconfig\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitconfig\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.gitignore\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.gitignore\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gitignore"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Ignore}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitignore\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitignore\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitignore\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.go\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.go\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.go"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.go"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Go}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.go"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.go\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.go\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.go\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.gradle\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.gradle\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gradle"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gradle"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Gradle}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gradle"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gradle\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gradle\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gradle\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.groovy\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.groovy\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.groovy"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.groovy"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Groovy}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.groovy"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.groovy\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.groovy\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.groovy\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.h\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.h\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.h"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.handlebars\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.handlebars\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.handlebars"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.handlebars"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Handlebars}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.handlebars"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.handlebars\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.handlebars\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.handlebars\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.hbs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.hbs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hbs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hbs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Handlebars}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hbs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hbs\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hbs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hbs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.h++\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.h++\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.h++"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h++"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h++"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h++\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h++\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.hh\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.hh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hh\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hh\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.hpp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.hpp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hpp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hpp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hpp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hpp\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hpp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hpp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.htm\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.htm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.htm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.htm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.htm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.htm\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.htm\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.htm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.html\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.html\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.html"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.html"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.html"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.html\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.html\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.html\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.hxx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.hxx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hxx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hxx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hxx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hxx\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hxx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hxx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.ini\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.ini\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ini"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ini"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,INI}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ini"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ini\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ini\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ini\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.ipynb\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.ipynb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ipynb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ipynb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Jupyter}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ipynb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ipynb\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ipynb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ipynb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.jade\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.jade\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jade"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jade"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Jade}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jade"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jade\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jade\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jade\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.jav\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.jav\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jav"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jav"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jav"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jav\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jav\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jav\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.java\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.java\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.java"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.java"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.java"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.java\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.java\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.java\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.js\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.js\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.js"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.js"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.js"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.js\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.js\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.js\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.jsx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.jsx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jsx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsx\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.jscsrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.jscsrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jscsrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jscsrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSCS RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jscsrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jscsrc\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jscsrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jscsrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.jshintrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.jshintrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jshintrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshintrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSHint RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshintrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshintrc\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshintrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshintrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.jshtm\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.jshtm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jshtm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshtm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript HTML Template}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshtm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshtm\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshtm\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshtm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.json\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.json\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.json"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.json"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSON}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.json"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.json\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.json\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.json\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.jsp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.jsp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jsp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java Server Pages}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsp\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.less\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.less\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.less"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.less"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LESS}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.less"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.less\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.less\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.less\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.log\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.log\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.log"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.log"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Log file}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.log"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.log\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.log\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.log\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.lua\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.lua\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.lua"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.lua"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Lua}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.lua"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.lua\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.lua\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.lua\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.m\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.m\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.m"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.m"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Objective C}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.m"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.m\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.m\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.m\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.makefile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.makefile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.makefile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.makefile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Makefile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.makefile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.makefile\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.makefile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.makefile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.markdown\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.markdown\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.markdown"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.markdown"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.markdown"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.markdown\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.markdown\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.markdown\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.md\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.md\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.md"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.md"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.md"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.md\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.md\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.md\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.mdoc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.mdoc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdoc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdoc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,MDoc}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdoc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdoc\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdoc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdoc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.mdown\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.mdown\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdown"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdown"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdown"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdown\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdown\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdown\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.mdtext\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.mdtext\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdtext"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtext"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtext"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtext\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtext\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtext\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.mdtxt\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.mdtxt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdtxt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtxt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtxt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtxt\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtxt\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtxt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.mdwn\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.mdwn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdwn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdwn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdwn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdwn\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdwn\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdwn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.mk\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.mk\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mk"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mk"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Makefile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mk"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mk\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mk\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mk\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.mkd\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.mkd\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mkd"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkd"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkd"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkd\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkd\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkd\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.mkdn\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.mkdn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mkdn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkdn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkdn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkdn\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkdn\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkdn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.ml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.ml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,OCaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.mli\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.mli\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mli"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mli"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,OCaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mli"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mli\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mli\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mli\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.mjs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.mjs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mjs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mjs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mjs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mjs\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mjs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mjs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.npmignore\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.npmignore\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.npmignore"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,NPM Ignore}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.npmignore\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.npmignore\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.npmignore\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.php\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.php\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.php"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.php"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PHP}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.php"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.php\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.php\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.php\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.phtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.phtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.phtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.phtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PHP HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.phtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.phtml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.phtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.phtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.pl\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.pl\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pl"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.pl6\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.pl6\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pl6"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl6"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl 6}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl6"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl6\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl6\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl6\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.plist\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.plist\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.plist"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.plist"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Properties file}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.plist"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.plist\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.plist\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.plist\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.pm\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.pm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.pm6\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.pm6\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pm6"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm6"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl 6 Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm6"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm6\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm6\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm6\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.pod\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.pod\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pod"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pod"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl POD}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pod"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pod\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pod\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pod\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.pp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.pp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pp\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.profile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.profile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.profile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.profile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.profile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.profile\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.profile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.profile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.properties\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.properties\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.properties"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.properties"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Properties}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.properties"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.properties\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.properties\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.properties\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.ps1\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.ps1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ps1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ps1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ps1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ps1\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ps1\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ps1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.psd1\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.psd1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psd1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psd1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell Module Manifest}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psd1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psd1\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psd1\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psd1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.psgi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.psgi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psgi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psgi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl CGI}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psgi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psgi\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psgi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psgi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.psm1\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.psm1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psm1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psm1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psm1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psm1\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psm1\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psm1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.py\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.py\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.py"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.py"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Python}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.py"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.py\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.py\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.py\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.pyi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.pyi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pyi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pyi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Python}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pyi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pyi\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pyi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pyi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.r\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.r\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.r"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.r"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.r"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.r\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.r\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.r\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.rb\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.rb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Ruby}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rb\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.rhistory\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.rhistory\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rhistory"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rhistory"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R History}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rhistory"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rhistory\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rhistory\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rhistory\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.rprofile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.rprofile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rprofile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rprofile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rprofile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rprofile\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rprofile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rprofile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.rs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.rs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Rust}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rs\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.rst\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.rst\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rst"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rst"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Restructured Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rst"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rst\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rst\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rst\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.rt\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.rt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Rich Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rt\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rt\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.sass\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.sass\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sass"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sass"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Sass}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sass"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sass\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sass\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sass\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.scss\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.scss\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.scss"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.scss"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Sass}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.scss"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.scss\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.scss\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.scss\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.sh\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.sh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SH}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sh\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sh\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.shtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.shtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.shtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.shtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SHTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.shtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.shtml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.shtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.shtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.sql\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.sql\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sql"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sql"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SQL}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sql"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sql\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sql\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sql\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.svg\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.svg\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.svg"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.svg"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SVG}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.svg"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.svg\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.svg\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.svg\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.t\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.t\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.t"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.t"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.t"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.t\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.t\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.t\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.tex\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.tex\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.tex"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tex"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LaTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tex"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tex\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tex\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tex\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.ts\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.ts\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ts"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ts"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,TypeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ts"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ts\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ts\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ts\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.toml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.toml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.toml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.toml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Toml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.toml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.toml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.toml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.toml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.tsx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.tsx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.tsx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tsx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,TypeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tsx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tsx\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tsx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tsx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.txt\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.txt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.txt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.txt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.txt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.txt\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.txt\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.txt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.vb\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.vb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.vb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Visual Basic}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vb\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.vue\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.vue\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.vue"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vue"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,VUE}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vue"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vue\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vue\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vue\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.wxi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.wxi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX Include}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxi\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.wxl\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.wxl\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxl"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxl"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX Localization}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxl"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxl\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxl\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxl\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.wxs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.wxs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxs\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.xaml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.xaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,XAML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xaml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xaml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.xhtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.xhtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xhtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xhtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xhtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xhtml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xhtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xhtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.xml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.xml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,XML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.yaml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.yaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.yaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Yaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yaml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yaml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.yml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.yml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.yml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Yaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.zsh\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.zsh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.zsh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.zsh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ZSH}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.zsh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.zsh\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.zsh\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.zsh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}SourceFile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,{#AppName}}"; Flags: uninsdeletekey -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}SourceFile\DefaultIcon"; ValueType: none; Flags: deletekey -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}SourceFile\shell\open"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe""" -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}SourceFile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1""" - -Root: HKCU; Subkey: "Software\Classes\Applications\{#AppExeName}.exe"; ValueType: none; ValueName: ""; Flags: uninsdeletekey -Root: HKCU; Subkey: "Software\Classes\Applications\{#AppExeName}.exe\DefaultIcon"; ValueType: none; Flags: deletekey -Root: HKCU; Subkey: "Software\Classes\Applications\{#AppExeName}.exe\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe""" -Root: HKCU; Subkey: "Software\Classes\Applications\{#AppExeName}.exe\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1""" - -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}ContextMenu"; ValueType: expandsz; ValueName: "Title"; ValueData: "{cm:OpenWithContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: not IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#AppExeName}.exe"; Tasks: addcontextmenufiles; Check: not IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\*\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: addcontextmenufiles; Check: not IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#AppExeName}.exe"; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\directory\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#AppExeName}.exe"; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#AppExeName}.exe"; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\Drive\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater - -; Environment -Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "Path"; ValueData: "{code:AddToPath|{app}\bin}"; Tasks: addtopath; Check: NeedsAddToPath(ExpandConstant('{app}\bin')) - -; URI Scheme -Root: HKCU; Subkey: "Software\Classes\zed"; ValueType: "string"; ValueData: "URL:zed Protocol"; Flags: uninsdeletekey -Root: HKCU; Subkey: "Software\Classes\zed"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "" -Root: HKCU; Subkey: "Software\Classes\zed\DefaultIcon"; ValueType: "string"; ValueData: "{app}\Zed.exe,1" -Root: HKCU; Subkey: "Software\Classes\zed\shell\open\command"; ValueType: "string"; ValueData: """{app}\Zed.exe"" ""%1""" - -[Code] -function WizardNotSilent(): Boolean; -begin - Result := not WizardSilent(); -end; - -function IsWindows11OrLater(): Boolean; -begin - Result := (GetWindowsVersion >= $0A0055F0); -end; - -// https://stackoverflow.com/a/23838239/261019 -procedure Explode(var Dest: TArrayOfString; Text: String; Separator: String); -var - i, p: Integer; -begin - i := 0; - repeat - SetArrayLength(Dest, i+1); - p := Pos(Separator,Text); - if p > 0 then begin - Dest[i] := Copy(Text, 1, p-1); - Text := Copy(Text, p + Length(Separator), Length(Text)); - i := i + 1; - end else begin - Dest[i] := Text; - Text := ''; - end; - until Length(Text)=0; -end; - -function NeedsAddToPath(path: string): boolean; -var - OrigPath: string; -begin - if not RegQueryStringValue(HKCU, 'Environment', 'Path', OrigPath) - then begin - Result := True; - exit; - end; - Result := Pos(';' + path + ';', ';' + OrigPath + ';') = 0; -end; - -function AddToPath(path: string): string; -var - OrigPath: string; -begin - RegQueryStringValue(HKCU, 'Environment', 'Path', OrigPath) - - if (Length(OrigPath) > 0) and (OrigPath[Length(OrigPath)] = ';') then - Result := OrigPath + path - else - Result := OrigPath + ';' + path -end; - -procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); -var - Path: string; - InstalledPath: string; - Parts: TArrayOfString; - NewPath: string; - i: Integer; -begin - if not CurUninstallStep = usUninstall then begin - exit; - end; - if not RegQueryStringValue(HKCU, 'Environment', 'Path', Path) - then begin - exit; - end; - NewPath := ''; - InstalledPath := ExpandConstant('{app}\bin') - Explode(Parts, Path, ';'); - for i:=0 to GetArrayLength(Parts)-1 do begin - if CompareText(Parts[i], InstalledPath) <> 0 then begin - NewPath := NewPath + Parts[i]; - - if i < GetArrayLength(Parts) - 1 then begin - NewPath := NewPath + ';'; - end; - end; - end; - RegWriteExpandStringValue(HKCU, 'Environment', 'Path', NewPath); -end; - -// https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/icacls -// https://docs.microsoft.com/en-US/windows/security/identity-protection/access-control/security-identifiers -procedure DisableAppDirInheritance(); -var - ResultCode: Integer; - Permissions: string; -begin - Permissions := '/grant:r "*S-1-5-18:(OI)(CI)F" /grant:r "*S-1-5-32-544:(OI)(CI)F" /grant:r "*S-1-5-11:(OI)(CI)RX" /grant:r "*S-1-5-32-545:(OI)(CI)RX"'; - - Permissions := Permissions + Format(' /grant:r "*S-1-3-0:(OI)(CI)F" /grant:r "%s:(OI)(CI)F"', [GetUserNameString()]); - - Exec(ExpandConstant('{sys}\icacls.exe'), ExpandConstant('"{app}" /inheritancelevel:r ') + Permissions, '', SW_HIDE, ewWaitUntilTerminated, ResultCode); -end; - -procedure AddAppxPackage(); -var - AddAppxPackageResultCode: Integer; -begin - if WizardIsTaskSelected('addcontextmenufiles') then begin - ShellExec('', 'powershell.exe', '-Command ' + AddQuotes('Add-AppxPackage -Path ''' + ExpandConstant('{app}\appx\zed_explorer_command_injector.appx') + ''' -ExternalLocation ''' + ExpandConstant('{app}\appx') + ''''), '', SW_HIDE, ewWaitUntilTerminated, AddAppxPackageResultCode); - RegDeleteKeyIncludingSubkeys(HKCU, 'Software\Classes\*\shell\{#RegValueName}'); - RegDeleteKeyIncludingSubkeys(HKCU, 'Software\Classes\directory\shell\{#RegValueName}'); - RegDeleteKeyIncludingSubkeys(HKCU, 'Software\Classes\directory\background\shell\{#RegValueName}'); - RegDeleteKeyIncludingSubkeys(HKCU, 'Software\Classes\Drive\shell\{#RegValueName}'); - end; -end; - -procedure RemoveAppxPackage(); -var - RemoveAppxPackageResultCode: Integer; -begin - ShellExec('', 'powershell.exe', '-Command ' + AddQuotes('Remove-AppxPackage -Package ''{#AppxFullName}'''), '', SW_HIDE, ewWaitUntilTerminated, RemoveAppxPackageResultCode); - if not WizardIsTaskSelected('addcontextmenufiles') then begin - RegDeleteKeyIncludingSubkeys(HKCU, 'Software\Classes\{#RegValueName}ContextMenu'); - end; -end; - -function SwitchHasValue(Name: string; Value: string): Boolean; -begin - Result := CompareText(ExpandConstant('{param:' + Name + '}'), Value) = 0; -end; - -function IsUpdating(): Boolean; -begin - Result := SwitchHasValue('update', 'true') and WizardSilent(); -end; - -procedure CurStepChanged(CurStep: TSetupStep); -begin - if CurStep = ssPostInstall then - begin - if IsUpdating() then - begin - SaveStringToFile(ExpandConstant('{app}\updates\versions.txt'), '{#Version}' + #13#10, True); - end - end; -end; - -function GetAppMutex(Param: string): string; -begin - if IsUpdating() then - Result := '' - else - Result := '{#AppMutex}'; -end; - -function GetInstallDir(Param: string): string; -begin - if IsUpdating() then - Result := ExpandConstant('{app}\install') - else - Result := ExpandConstant('{app}'); -end; diff --git a/crates/zed/resources/windows/zed.sh b/crates/zed/resources/windows/zed.sh deleted file mode 100644 index 734b1a7eb0..0000000000 --- a/crates/zed/resources/windows/zed.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env sh - -if [ "$ZED_WSL_DEBUG_INFO" = true ]; then - set -x -fi - -ZED_PATH="$(dirname "$(realpath "$0")")" - -IN_WSL=false -if [ -n "$WSL_DISTRO_NAME" ]; then - # $WSL_DISTRO_NAME is available since WSL builds 18362, also for WSL2 - IN_WSL=true -fi - -if [ $IN_WSL = true ]; then - WSL_USER="$USER" - if [ -z "$WSL_USER" ]; then - WSL_USER="$USERNAME" - fi - "$ZED_PATH/zed.exe" --wsl "$WSL_USER@$WSL_DISTRO_NAME" "$@" - exit $? -else - "$ZED_PATH/zed.exe" "$@" - exit $? -fi diff --git a/crates/zed/resources/zed.desktop.in b/crates/zed/resources/zed.desktop.in deleted file mode 100644 index eaace153dd..0000000000 --- a/crates/zed/resources/zed.desktop.in +++ /dev/null @@ -1,21 +0,0 @@ -[Desktop Entry] -Version=1.0 -Type=Application -Name=$APP_NAME -GenericName=Text Editor -Comment=A high-performance, multiplayer code editor. -TryExec=$APP_CLI -StartupNotify=$DO_STARTUP_NOTIFY -Exec=$APP_CLI $APP_ARGS -Icon=$APP_ICON -Categories=Utility;TextEditor;Development;IDE; -Keywords=zed; -# To add Zed to "Open Folder With..." context menu, add `inode/directory` to the MimeType field (semicolon separated) -# Arch linux users have reported this setting Zed as default file browser. See https://github.com/zed-industries/zed/pull/39076 and related issues. -# If this happens to you, an unconfirmed fix may be to install Arch's `gnome-defaults-list` package. -MimeType=text/plain;application/x-zerosize;x-scheme-handler/zed; -Actions=NewWorkspace; - -[Desktop Action NewWorkspace] -Exec=$APP_CLI --new $APP_ARGS -Name=Open a new workspace diff --git a/crates/zed/resources/zed.entitlements b/crates/zed/resources/zed.entitlements deleted file mode 100644 index 2a16afe755..0000000000 --- a/crates/zed/resources/zed.entitlements +++ /dev/null @@ -1,30 +0,0 @@ - - - - - com.apple.security.automation.apple-events - - com.apple.security.cs.allow-jit - - com.apple.security.cs.allow-unsigned-executable-memory - - - com.apple.security.device.audio-input - - com.apple.security.device.camera - - com.apple.security.personal-information.addressbook - - com.apple.security.personal-information.calendars - - com.apple.security.personal-information.location - - com.apple.security.personal-information.photos-library - - com.apple.security.files.user-selected.read-write - - com.apple.security.files.downloads.read-write - - - diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs deleted file mode 100644 index dfbc57d293..0000000000 --- a/crates/zed/src/main.rs +++ /dev/null @@ -1,1532 +0,0 @@ -mod reliability; -mod zed; - -use agent_ui::AgentPanel; -use anyhow::{Context as _, Error, Result}; -use clap::Parser; -use cli::FORCE_CLI_MODE_ENV_VAR_NAME; -use client::{Client, ProxySettings, UserStore, parse_zed_link}; -use collab_ui::channel_view::ChannelView; -use collections::HashMap; -use crashes::InitCrashHandler; -use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE}; -use editor::Editor; -use extension::ExtensionHostProxy; -use fs::{Fs, RealFs}; -use futures::{StreamExt, channel::oneshot, future}; -use git::GitHostingProviderRegistry; -use gpui::{App, AppContext, Application, AsyncApp, Focusable as _, QuitMode, UpdateGlobal as _}; - -use gpui_tokio::Tokio; -use language::LanguageRegistry; -use onboarding::{FIRST_OPEN, show_onboarding_view}; -use prompt_store::PromptBuilder; -use remote::RemoteConnectionOptions; -use reqwest_client::ReqwestClient; - -use assets::Assets; -use node_runtime::{NodeBinaryOptions, NodeRuntime}; -use parking_lot::Mutex; -use project::project_settings::ProjectSettings; -use recent_projects::{SshSettings, open_remote_project}; -use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; -use session::{AppSession, Session}; -use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file}; -use std::{ - env, - io::{self, IsTerminal}, - path::{Path, PathBuf}, - process, - sync::{Arc, OnceLock}, - time::Instant, -}; -use theme::{ActiveTheme, GlobalTheme, ThemeRegistry}; -use util::{ResultExt, TryFutureExt, maybe}; -use uuid::Uuid; -use workspace::{ - AppState, PathList, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, - WorkspaceStore, notifications::NotificationId, -}; -use zed::{ - OpenListener, OpenRequest, RawOpenRequest, app_menus, build_window_options, - derive_paths_with_position, edit_prediction_registry, handle_cli_connection, - handle_keymap_file_changes, handle_settings_file_changes, initialize_workspace, - open_paths_with_positions, -}; - -use crate::zed::{OpenRequestKind, eager_load_active_theme_and_icon_theme}; - -#[cfg(feature = "mimalloc")] -#[global_allocator] -static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; - -fn files_not_created_on_launch(errors: HashMap>) { - let message = "Zed failed to launch"; - let error_details = errors - .into_iter() - .flat_map(|(kind, paths)| { - #[allow(unused_mut)] // for non-unix platforms - let mut error_kind_details = match paths.len() { - 0 => return None, - 1 => format!( - "{kind} when creating directory {:?}", - paths.first().expect("match arm checks for a single entry") - ), - _many => format!("{kind} when creating directories {paths:?}"), - }; - - #[cfg(unix)] - { - if kind == io::ErrorKind::PermissionDenied { - error_kind_details.push_str("\n\nConsider using chown and chmod tools for altering the directories permissions if your user has corresponding rights.\ - \nFor example, `sudo chown $(whoami):staff ~/.config` and `chmod +uwrx ~/.config`"); - } - } - - Some(error_kind_details) - }) - .collect::>().join("\n\n"); - - eprintln!("{message}: {error_details}"); - Application::new() - .with_quit_mode(QuitMode::Explicit) - .run(move |cx| { - if let Ok(window) = cx.open_window(gpui::WindowOptions::default(), |_, cx| { - cx.new(|_| gpui::Empty) - }) { - window - .update(cx, |_, window, cx| { - let response = window.prompt( - gpui::PromptLevel::Critical, - message, - Some(&error_details), - &["Exit"], - cx, - ); - - cx.spawn_in(window, async move |_, cx| { - response.await?; - cx.update(|_, cx| cx.quit()) - }) - .detach_and_log_err(cx); - }) - .log_err(); - } else { - fail_to_open_window(anyhow::anyhow!("{message}: {error_details}"), cx) - } - }) -} - -fn fail_to_open_window_async(e: anyhow::Error, cx: &mut AsyncApp) { - cx.update(|cx| fail_to_open_window(e, cx)).log_err(); -} - -fn fail_to_open_window(e: anyhow::Error, _cx: &mut App) { - eprintln!( - "Zed failed to open a window: {e:?}. See https://zed.dev/docs/linux for troubleshooting steps." - ); - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - { - process::exit(1); - } - - // Maybe unify this with gpui::platform::linux::platform::ResultExt::notify_err(..)? - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - { - use ashpd::desktop::notification::{Notification, NotificationProxy, Priority}; - _cx.spawn(async move |_cx| { - let Ok(proxy) = NotificationProxy::new().await else { - process::exit(1); - }; - - let notification_id = "dev.zed.Oops"; - proxy - .add_notification( - notification_id, - Notification::new("Zed failed to launch") - .body(Some( - format!( - "{e:?}. See https://zed.dev/docs/linux for troubleshooting steps." - ) - .as_str(), - )) - .priority(Priority::High) - .icon(ashpd::desktop::Icon::with_names(&[ - "dialog-question-symbolic", - ])), - ) - .await - .ok(); - - process::exit(1); - }) - .detach(); - } -} -pub static STARTUP_TIME: OnceLock = OnceLock::new(); - -pub fn main() { - STARTUP_TIME.get_or_init(|| Instant::now()); - - #[cfg(unix)] - util::prevent_root_execution(); - - let args = Args::parse(); - - // `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass - #[cfg(not(target_os = "windows"))] - if let Some(socket) = &args.askpass { - askpass::main(socket); - return; - } - - // `zed --crash-handler` Makes zed operate in minidump crash handler mode - if let Some(socket) = &args.crash_handler { - crashes::crash_server(socket.as_path()); - return; - } - - // `zed --nc` Makes zed operate in nc/netcat mode for use with MCP - if let Some(socket) = &args.nc { - match nc::main(socket) { - Ok(()) => return, - Err(err) => { - eprintln!("Error: {}", err); - process::exit(1); - } - } - } - - #[cfg(all(not(debug_assertions), target_os = "windows"))] - unsafe { - use windows::Win32::System::Console::{ATTACH_PARENT_PROCESS, AttachConsole}; - - if args.foreground { - let _ = AttachConsole(ATTACH_PARENT_PROCESS); - } - } - - // `zed --printenv` Outputs environment variables as JSON to stdout - if args.printenv { - util::shell_env::print_env(); - return; - } - - if args.dump_all_actions { - dump_all_gpui_actions(); - return; - } - - // Set custom data directory. - if let Some(dir) = &args.user_data_dir { - paths::set_custom_data_dir(dir); - } - - #[cfg(target_os = "windows")] - match util::get_zed_cli_path() { - Ok(path) => askpass::set_askpass_program(path), - Err(err) => { - eprintln!("Error: {}", err); - if std::option_env!("ZED_BUNDLE").is_some() { - process::exit(1); - } - } - } - - let file_errors = init_paths(); - if !file_errors.is_empty() { - files_not_created_on_launch(file_errors); - return; - } - - zlog::init(); - - if stdout_is_a_pty() { - zlog::init_output_stdout(); - } else { - let result = zlog::init_output_file(paths::log_file(), Some(paths::old_log_file())); - if let Err(err) = result { - eprintln!("Could not open log file: {}... Defaulting to stdout", err); - zlog::init_output_stdout(); - }; - } - ztracing::init(); - - let version = option_env!("ZED_BUILD_ID"); - let app_commit_sha = - option_env!("ZED_COMMIT_SHA").map(|commit_sha| AppCommitSha::new(commit_sha.to_string())); - let app_version = AppVersion::load(env!("CARGO_PKG_VERSION"), version, app_commit_sha.clone()); - - if args.system_specs { - let system_specs = system_specs::SystemSpecs::new_stateless( - app_version, - app_commit_sha, - *release_channel::RELEASE_CHANNEL, - ); - println!("Zed System Specs (from CLI):\n{}", system_specs); - return; - } - - rayon::ThreadPoolBuilder::new() - .num_threads(std::thread::available_parallelism().map_or(1, |n| n.get().div_ceil(2))) - .stack_size(10 * 1024 * 1024) - .thread_name(|ix| format!("RayonWorker{}", ix)) - .build_global() - .unwrap(); - - log::info!( - "========== starting zed version {}, sha {} ==========", - app_version, - app_commit_sha - .as_ref() - .map(|sha| sha.short()) - .as_deref() - .unwrap_or("unknown"), - ); - - #[cfg(windows)] - check_for_conpty_dll(); - - let app = Application::new().with_assets(Assets); - - let system_id = app.background_executor().spawn(system_id()); - let installation_id = app.background_executor().spawn(installation_id()); - let session_id = Uuid::new_v4().to_string(); - let session = app - .background_executor() - .spawn(Session::new(session_id.clone())); - - app.background_executor() - .spawn(crashes::init(InitCrashHandler { - session_id, - zed_version: app_version.to_string(), - binary: "zed".to_string(), - release_channel: release_channel::RELEASE_CHANNEL_NAME.clone(), - commit_sha: app_commit_sha - .as_ref() - .map(|sha| sha.full()) - .unwrap_or_else(|| "no sha".to_owned()), - })) - .detach(); - - let (open_listener, mut open_rx) = OpenListener::new(); - - let failed_single_instance_check = if *zed_env_vars::ZED_STATELESS - || *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev - { - false - } else { - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - { - crate::zed::listen_for_cli_connections(open_listener.clone()).is_err() - } - - #[cfg(target_os = "windows")] - { - !crate::zed::windows_only_instance::handle_single_instance(open_listener.clone(), &args) - } - - #[cfg(target_os = "macos")] - { - use zed::mac_only_instance::*; - ensure_only_instance() != IsOnlyInstance::Yes - } - }; - if failed_single_instance_check { - println!("zed is already running"); - return; - } - - let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new()); - let git_binary_path = - if cfg!(target_os = "macos") && option_env!("ZED_BUNDLE").as_deref() == Some("true") { - app.path_for_auxiliary_executable("git") - .context("could not find git binary path") - .log_err() - } else { - None - }; - if let Some(git_binary_path) = &git_binary_path { - log::info!("Using git binary path: {:?}", git_binary_path); - } - - let fs = Arc::new(RealFs::new(git_binary_path, app.background_executor())); - let user_settings_file_rx = watch_config_file( - &app.background_executor(), - fs.clone(), - paths::settings_file().clone(), - ); - let global_settings_file_rx = watch_config_file( - &app.background_executor(), - fs.clone(), - paths::global_settings_file().clone(), - ); - let user_keymap_file_rx = watch_config_file( - &app.background_executor(), - fs.clone(), - paths::keymap_file().clone(), - ); - - let (shell_env_loaded_tx, shell_env_loaded_rx) = oneshot::channel(); - if !stdout_is_a_pty() { - app.background_executor() - .spawn(async { - #[cfg(unix)] - util::load_login_shell_environment().await.log_err(); - shell_env_loaded_tx.send(()).ok(); - }) - .detach() - } else { - drop(shell_env_loaded_tx) - } - - app.on_open_urls({ - let open_listener = open_listener.clone(); - move |urls| { - open_listener.open(RawOpenRequest { - urls, - diff_paths: Vec::new(), - ..Default::default() - }) - } - }); - app.on_reopen(move |cx| { - if let Some(app_state) = AppState::try_global(cx).and_then(|app_state| app_state.upgrade()) - { - cx.spawn({ - let app_state = app_state; - async move |cx| { - if let Err(e) = restore_or_create_workspace(app_state, cx).await { - fail_to_open_window_async(e, cx) - } - } - }) - .detach(); - } - }); - - app.run(move |cx| { - menu::init(); - zed_actions::init(); - - release_channel::init(app_version, cx); - gpui_tokio::init(cx); - if let Some(app_commit_sha) = app_commit_sha { - AppCommitSha::set_global(app_commit_sha, cx); - } - settings::init(cx); - zlog_settings::init(cx); - handle_settings_file_changes(user_settings_file_rx, global_settings_file_rx, cx); - handle_keymap_file_changes(user_keymap_file_rx, cx); - - let user_agent = format!( - "Zed/{} ({}; {})", - AppVersion::global(cx), - std::env::consts::OS, - std::env::consts::ARCH - ); - let proxy_url = ProxySettings::get_global(cx).proxy_url(); - let http = { - let _guard = Tokio::handle(cx).enter(); - - ReqwestClient::proxy_and_user_agent(proxy_url, &user_agent) - .expect("could not start HTTP client") - }; - cx.set_http_client(Arc::new(http)); - - ::set_global(fs.clone(), cx); - - GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx); - git_hosting_providers::init(cx); - - OpenListener::set_global(cx, open_listener.clone()); - - extension::init(cx); - let extension_host_proxy = ExtensionHostProxy::global(cx); - - let client = Client::production(cx); - cx.set_http_client(client.http_client()); - let mut languages = LanguageRegistry::new(cx.background_executor().clone()); - languages.set_language_server_download_dir(paths::languages_dir().clone()); - let languages = Arc::new(languages); - let (mut tx, rx) = watch::channel(None); - cx.observe_global::(move |cx| { - let settings = &ProjectSettings::get_global(cx).node; - let options = NodeBinaryOptions { - allow_path_lookup: !settings.ignore_system_version, - // TODO: Expose this setting - allow_binary_download: true, - use_paths: settings.path.as_ref().map(|node_path| { - let node_path = PathBuf::from(shellexpand::tilde(node_path).as_ref()); - let npm_path = settings - .npm_path - .as_ref() - .map(|path| PathBuf::from(shellexpand::tilde(&path).as_ref())); - ( - node_path.clone(), - npm_path.unwrap_or_else(|| { - let base_path = PathBuf::new(); - node_path.parent().unwrap_or(&base_path).join("npm") - }), - ) - }), - }; - tx.send(Some(options)).log_err(); - }) - .detach(); - let node_runtime = NodeRuntime::new(client.http_client(), Some(shell_env_loaded_rx), rx); - - debug_adapter_extension::init(extension_host_proxy.clone(), cx); - languages::init(languages.clone(), fs.clone(), node_runtime.clone(), cx); - let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx)); - - language_extension::init( - language_extension::LspAccess::ViaWorkspaces({ - let workspace_store = workspace_store.clone(); - Arc::new(move |cx: &mut App| { - workspace_store.update(cx, |workspace_store, cx| { - workspace_store - .workspaces() - .iter() - .map(|workspace| { - workspace.update(cx, |workspace, _, cx| { - workspace.project().read(cx).lsp_store() - }) - }) - .collect() - }) - }) - }), - extension_host_proxy.clone(), - languages.clone(), - ); - - Client::set_global(client.clone(), cx); - - zed::init(cx); - project::Project::init(&client, cx); - debugger_ui::init(cx); - debugger_tools::init(cx); - client::init(&client, cx); - - let system_id = cx.background_executor().block(system_id).ok(); - let installation_id = cx.background_executor().block(installation_id).ok(); - let session = cx.background_executor().block(session); - - let telemetry = client.telemetry(); - telemetry.start( - system_id.as_ref().map(|id| id.to_string()), - installation_id.as_ref().map(|id| id.to_string()), - session.id().to_owned(), - cx, - ); - - // We should rename these in the future to `first app open`, `first app open for release channel`, and `app open` - if let (Some(system_id), Some(installation_id)) = (&system_id, &installation_id) { - match (&system_id, &installation_id) { - (IdType::New(_), IdType::New(_)) => { - telemetry::event!("App First Opened"); - telemetry::event!("App First Opened For Release Channel"); - } - (IdType::Existing(_), IdType::New(_)) => { - telemetry::event!("App First Opened For Release Channel"); - } - (_, IdType::Existing(_)) => { - telemetry::event!("App Opened"); - } - } - } - let app_session = cx.new(|cx| AppSession::new(session, cx)); - - let app_state = Arc::new(AppState { - languages, - client: client.clone(), - user_store, - fs: fs.clone(), - build_window_options, - workspace_store, - node_runtime, - session: app_session, - }); - AppState::set_global(Arc::downgrade(&app_state), cx); - - auto_update::init(client.clone(), cx); - dap_adapters::init(cx); - auto_update_ui::init(cx); - reliability::init(client.clone(), cx); - extension_host::init( - extension_host_proxy.clone(), - app_state.fs.clone(), - app_state.client.clone(), - app_state.node_runtime.clone(), - cx, - ); - - theme::init(theme::LoadThemes::All(Box::new(Assets)), cx); - eager_load_active_theme_and_icon_theme(fs.clone(), cx); - theme_extension::init( - extension_host_proxy, - ThemeRegistry::global(cx), - cx.background_executor().clone(), - ); - command_palette::init(cx); - let copilot_language_server_id = app_state.languages.next_language_server_id(); - copilot::init( - copilot_language_server_id, - app_state.fs.clone(), - app_state.client.http_client(), - app_state.node_runtime.clone(), - cx, - ); - supermaven::init(app_state.client.clone(), cx); - language_model::init(app_state.client.clone(), cx); - language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx); - acp_tools::init(cx); - edit_prediction_ui::init(cx); - web_search::init(cx); - web_search_providers::init(app_state.client.clone(), cx); - snippet_provider::init(cx); - edit_prediction_registry::init(app_state.client.clone(), app_state.user_store.clone(), cx); - let prompt_builder = PromptBuilder::load(app_state.fs.clone(), stdout_is_a_pty(), cx); - agent_ui::init( - app_state.fs.clone(), - app_state.client.clone(), - prompt_builder.clone(), - app_state.languages.clone(), - false, - cx, - ); - repl::init(app_state.fs.clone(), cx); - recent_projects::init(cx); - - load_embedded_fonts(cx); - - editor::init(cx); - image_viewer::init(cx); - repl::notebook::init(cx); - diagnostics::init(cx); - - audio::init(cx); - workspace::init(app_state.clone(), cx); - ui_prompt::init(cx); - - go_to_line::init(cx); - file_finder::init(cx); - tab_switcher::init(cx); - outline::init(cx); - project_symbols::init(cx); - project_panel::init(cx); - outline_panel::init(cx); - tasks_ui::init(cx); - snippets_ui::init(cx); - channel::init(&app_state.client.clone(), app_state.user_store.clone(), cx); - search::init(cx); - vim::init(cx); - terminal_view::init(cx); - journal::init(app_state.clone(), cx); - language_selector::init(cx); - line_ending_selector::init(cx); - toolchain_selector::init(cx); - theme_selector::init(cx); - settings_profile_selector::init(cx); - language_tools::init(cx); - call::init(app_state.client.clone(), app_state.user_store.clone(), cx); - notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); - collab_ui::init(&app_state, cx); - git_ui::init(cx); - feedback::init(cx); - markdown_preview::init(cx); - svg_preview::init(cx); - onboarding::init(cx); - settings_ui::init(cx); - keymap_editor::init(cx); - extensions_ui::init(cx); - edit_prediction::init(cx); - inspector_ui::init(app_state.clone(), cx); - json_schema_store::init(cx); - miniprofiler_ui::init(*STARTUP_TIME.get().unwrap(), cx); - - cx.observe_global::({ - let http = app_state.client.http_client(); - let client = app_state.client.clone(); - move |cx| { - for &mut window in cx.windows().iter_mut() { - let background_appearance = cx.theme().window_background_appearance(); - window - .update(cx, |_, window, _| { - window.set_background_appearance(background_appearance) - }) - .ok(); - } - - let new_host = &client::ClientSettings::get_global(cx).server_url; - if &http.base_url() != new_host { - http.set_base_url(new_host); - if client.status().borrow().is_connected() { - client.reconnect(&cx.to_async()); - } - } - } - }) - .detach(); - app_state.languages.set_theme(cx.theme().clone()); - cx.observe_global::({ - let languages = app_state.languages.clone(); - move |cx| { - languages.set_theme(cx.theme().clone()); - } - }) - .detach(); - telemetry::event!( - "Settings Changed", - setting = "theme", - value = cx.theme().name.to_string() - ); - telemetry::event!( - "Settings Changed", - setting = "keymap", - value = BaseKeymap::get_global(cx).to_string() - ); - telemetry.flush_events().detach(); - - let fs = app_state.fs.clone(); - load_user_themes_in_background(fs.clone(), cx); - watch_themes(fs.clone(), cx); - watch_languages(fs.clone(), app_state.languages.clone(), cx); - - let menus = app_menus(cx); - cx.set_menus(menus); - initialize_workspace(app_state.clone(), prompt_builder, cx); - - cx.activate(true); - - cx.spawn({ - let client = app_state.client.clone(); - async move |cx| authenticate(client, cx).await - }) - .detach_and_log_err(cx); - - let urls: Vec<_> = args - .paths_or_urls - .iter() - .map(|arg| parse_url_arg(arg, cx)) - .collect(); - - let diff_paths: Vec<[String; 2]> = args - .diff - .chunks(2) - .map(|chunk| [chunk[0].clone(), chunk[1].clone()]) - .collect(); - - #[cfg(target_os = "windows")] - let wsl = args.wsl; - #[cfg(not(target_os = "windows"))] - let wsl = None; - - if !urls.is_empty() || !diff_paths.is_empty() { - open_listener.open(RawOpenRequest { - urls, - diff_paths, - wsl, - }) - } - - match open_rx - .try_next() - .ok() - .flatten() - .and_then(|request| OpenRequest::parse(request, cx).log_err()) - { - Some(request) => { - handle_open_request(request, app_state.clone(), cx); - } - None => { - cx.spawn({ - let app_state = app_state.clone(); - async move |cx| { - if let Err(e) = restore_or_create_workspace(app_state, cx).await { - fail_to_open_window_async(e, cx) - } - } - }) - .detach(); - } - } - - let app_state = app_state.clone(); - - crate::zed::component_preview::init(app_state.clone(), cx); - - cx.spawn(async move |cx| { - while let Some(urls) = open_rx.next().await { - cx.update(|cx| { - if let Some(request) = OpenRequest::parse(urls, cx).log_err() { - handle_open_request(request, app_state.clone(), cx); - } - }) - .ok(); - } - }) - .detach(); - }); -} - -fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut App) { - if let Some(kind) = request.kind { - match kind { - OpenRequestKind::CliConnection(connection) => { - cx.spawn(async move |cx| handle_cli_connection(connection, app_state, cx).await) - .detach(); - } - OpenRequestKind::Extension { extension_id } => { - cx.spawn(async move |cx| { - let workspace = - workspace::get_any_active_workspace(app_state, cx.clone()).await?; - workspace.update(cx, |_, window, cx| { - window.dispatch_action( - Box::new(zed_actions::Extensions { - category_filter: None, - id: Some(extension_id), - }), - cx, - ); - }) - }) - .detach_and_log_err(cx); - } - OpenRequestKind::AgentPanel => { - cx.spawn(async move |cx| { - let workspace = - workspace::get_any_active_workspace(app_state, cx.clone()).await?; - workspace.update(cx, |workspace, window, cx| { - if let Some(panel) = workspace.panel::(cx) { - panel.focus_handle(cx).focus(window); - } - }) - }) - .detach_and_log_err(cx); - } - OpenRequestKind::DockMenuAction { index } => { - cx.perform_dock_menu_action(index); - } - OpenRequestKind::BuiltinJsonSchema { schema_path } => { - workspace::with_active_or_new_workspace(cx, |_workspace, window, cx| { - cx.spawn_in(window, async move |workspace, cx| { - let res = async move { - let json = app_state.languages.language_for_name("JSONC").await.ok(); - let json_schema_content = - json_schema_store::resolve_schema_request_inner( - &app_state.languages, - &schema_path, - cx, - )?; - let json_schema_content = - serde_json::to_string_pretty(&json_schema_content) - .context("Failed to serialize JSON Schema as JSON")?; - let buffer_task = workspace.update(cx, |workspace, cx| { - workspace - .project() - .update(cx, |project, cx| project.create_buffer(false, cx)) - })?; - - let buffer = buffer_task.await?; - - workspace.update_in(cx, |workspace, window, cx| { - buffer.update(cx, |buffer, cx| { - buffer.set_language(json, cx); - buffer.edit([(0..0, json_schema_content)], None, cx); - buffer.edit( - [(0..0, format!("// {} JSON Schema\n", schema_path))], - None, - cx, - ); - }); - - workspace.add_item_to_active_pane( - Box::new(cx.new(|cx| { - let mut editor = - editor::Editor::for_buffer(buffer, None, window, cx); - editor.set_read_only(true); - editor - })), - None, - true, - window, - cx, - ); - }) - } - .await; - res.context("Failed to open builtin JSON Schema").log_err(); - }) - .detach(); - }); - } - OpenRequestKind::Setting { setting_path } => { - // zed://settings/languages/$(language)/tab_size - DONT SUPPORT - // zed://settings/languages/Rust/tab_size - SUPPORT - // languages.$(language).tab_size - // [ languages $(language) tab_size] - cx.spawn(async move |cx| { - let workspace = - workspace::get_any_active_workspace(app_state, cx.clone()).await?; - - workspace.update(cx, |_, window, cx| match setting_path { - None => window.dispatch_action(Box::new(zed_actions::OpenSettings), cx), - Some(setting_path) => window.dispatch_action( - Box::new(zed_actions::OpenSettingsAt { path: setting_path }), - cx, - ), - }) - }) - .detach_and_log_err(cx); - } - } - - return; - } - - if let Some(connection_options) = request.remote_connection { - cx.spawn(async move |cx| { - let paths: Vec = request.open_paths.into_iter().map(PathBuf::from).collect(); - open_remote_project( - connection_options, - paths, - app_state, - workspace::OpenOptions::default(), - cx, - ) - .await - }) - .detach_and_log_err(cx); - return; - } - - let mut task = None; - if !request.open_paths.is_empty() || !request.diff_paths.is_empty() { - let app_state = app_state.clone(); - task = Some(cx.spawn(async move |cx| { - let paths_with_position = - derive_paths_with_position(app_state.fs.as_ref(), request.open_paths).await; - let (_window, results) = open_paths_with_positions( - &paths_with_position, - &request.diff_paths, - app_state, - workspace::OpenOptions::default(), - cx, - ) - .await?; - for result in results.into_iter().flatten() { - if let Err(err) = result { - log::error!("Error opening path: {err}",); - } - } - anyhow::Ok(()) - })); - } - - if !request.open_channel_notes.is_empty() || request.join_channel.is_some() { - cx.spawn(async move |cx| { - let result = maybe!(async { - if let Some(task) = task { - task.await?; - } - let client = app_state.client.clone(); - // we continue even if authentication fails as join_channel/ open channel notes will - // show a visible error message. - authenticate(client, cx).await.log_err(); - - if let Some(channel_id) = request.join_channel { - cx.update(|cx| { - workspace::join_channel( - client::ChannelId(channel_id), - app_state.clone(), - None, - cx, - ) - })? - .await?; - } - - let workspace_window = - workspace::get_any_active_workspace(app_state, cx.clone()).await?; - let workspace = workspace_window.entity(cx)?; - - let mut promises = Vec::new(); - for (channel_id, heading) in request.open_channel_notes { - promises.push(cx.update_window(workspace_window.into(), |_, window, cx| { - ChannelView::open( - client::ChannelId(channel_id), - heading, - workspace.clone(), - window, - cx, - ) - .log_err() - })?) - } - future::join_all(promises).await; - anyhow::Ok(()) - }) - .await; - if let Err(err) = result { - fail_to_open_window_async(err, cx); - } - }) - .detach() - } else if let Some(task) = task { - cx.spawn(async move |cx| { - if let Err(err) = task.await { - fail_to_open_window_async(err, cx); - } - }) - .detach(); - } -} - -async fn authenticate(client: Arc, cx: &AsyncApp) -> Result<()> { - if stdout_is_a_pty() { - if client::IMPERSONATE_LOGIN.is_some() { - client.sign_in_with_optional_connect(false, cx).await?; - } else if client.has_credentials(cx).await { - client.sign_in_with_optional_connect(true, cx).await?; - } - } else if client.has_credentials(cx).await { - client.sign_in_with_optional_connect(true, cx).await?; - } - - Ok(()) -} - -async fn system_id() -> Result { - let key_name = "system_id".to_string(); - - if let Ok(Some(system_id)) = GLOBAL_KEY_VALUE_STORE.read_kvp(&key_name) { - return Ok(IdType::Existing(system_id)); - } - - let system_id = Uuid::new_v4().to_string(); - - GLOBAL_KEY_VALUE_STORE - .write_kvp(key_name, system_id.clone()) - .await?; - - Ok(IdType::New(system_id)) -} - -async fn installation_id() -> Result { - let legacy_key_name = "device_id".to_string(); - let key_name = "installation_id".to_string(); - - // Migrate legacy key to new key - if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp(&legacy_key_name) { - KEY_VALUE_STORE - .write_kvp(key_name, installation_id.clone()) - .await?; - KEY_VALUE_STORE.delete_kvp(legacy_key_name).await?; - return Ok(IdType::Existing(installation_id)); - } - - if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp(&key_name) { - return Ok(IdType::Existing(installation_id)); - } - - let installation_id = Uuid::new_v4().to_string(); - - KEY_VALUE_STORE - .write_kvp(key_name, installation_id.clone()) - .await?; - - Ok(IdType::New(installation_id)) -} - -async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp) -> Result<()> { - if let Some(locations) = restorable_workspace_locations(cx, &app_state).await { - let use_system_window_tabs = cx - .update(|cx| WorkspaceSettings::get_global(cx).use_system_window_tabs) - .unwrap_or(false); - let mut results: Vec> = Vec::new(); - let mut tasks = Vec::new(); - - for (index, (location, paths)) in locations.into_iter().enumerate() { - match location { - SerializedWorkspaceLocation::Local => { - let app_state = app_state.clone(); - let task = cx.spawn(async move |cx| { - let open_task = cx.update(|cx| { - workspace::open_paths( - &paths.paths(), - app_state, - workspace::OpenOptions::default(), - cx, - ) - })?; - open_task.await.map(|_| ()) - }); - - // If we're using system window tabs and this is the first workspace, - // wait for it to finish so that the other windows can be added as tabs. - if use_system_window_tabs && index == 0 { - results.push(task.await); - } else { - tasks.push(task); - } - } - SerializedWorkspaceLocation::Remote(mut connection_options) => { - let app_state = app_state.clone(); - if let RemoteConnectionOptions::Ssh(options) = &mut connection_options { - cx.update(|cx| { - SshSettings::get_global(cx) - .fill_connection_options_from_settings(options) - })?; - } - let task = cx.spawn(async move |cx| { - recent_projects::open_remote_project( - connection_options, - paths.paths().into_iter().map(PathBuf::from).collect(), - app_state, - workspace::OpenOptions::default(), - cx, - ) - .await - .map_err(|e| anyhow::anyhow!(e)) - }); - tasks.push(task); - } - } - } - - // Wait for all workspaces to open concurrently - results.extend(future::join_all(tasks).await); - - // Show notifications for any errors that occurred - let mut error_count = 0; - for result in results { - if let Err(e) = result { - log::error!("Failed to restore workspace: {}", e); - error_count += 1; - } - } - - if error_count > 0 { - let message = if error_count == 1 { - "Failed to restore 1 workspace. Check logs for details.".to_string() - } else { - format!( - "Failed to restore {} workspaces. Check logs for details.", - error_count - ) - }; - - // Try to find an active workspace to show the toast - let toast_shown = cx - .update(|cx| { - if let Some(window) = cx.active_window() - && let Some(workspace) = window.downcast::() - { - workspace - .update(cx, |workspace, _, cx| { - workspace.show_toast( - Toast::new(NotificationId::unique::<()>(), message), - cx, - ) - }) - .ok(); - return true; - } - false - }) - .unwrap_or(false); - - // If we couldn't show a toast (no windows opened successfully), - // we've already logged the errors above, so the user can check logs - if !toast_shown { - log::error!( - "Failed to show notification for window restoration errors, because no workspace windows were available." - ); - } - } - } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { - cx.update(|cx| show_onboarding_view(app_state, cx))?.await?; - } else { - cx.update(|cx| { - workspace::open_new( - Default::default(), - app_state, - cx, - |workspace, window, cx| { - Editor::new_file(workspace, &Default::default(), window, cx) - }, - ) - })? - .await?; - } - - Ok(()) -} - -pub(crate) async fn restorable_workspace_locations( - cx: &mut AsyncApp, - app_state: &Arc, -) -> Option> { - let mut restore_behavior = cx - .update(|cx| WorkspaceSettings::get(None, cx).restore_on_startup) - .ok()?; - - let session_handle = app_state.session.clone(); - let (last_session_id, last_session_window_stack) = cx - .update(|cx| { - let session = session_handle.read(cx); - - ( - session.last_session_id().map(|id| id.to_string()), - session.last_session_window_stack(), - ) - }) - .ok()?; - - if last_session_id.is_none() - && matches!( - restore_behavior, - workspace::RestoreOnStartupBehavior::LastSession - ) - { - restore_behavior = workspace::RestoreOnStartupBehavior::LastWorkspace; - } - - match restore_behavior { - workspace::RestoreOnStartupBehavior::LastWorkspace => { - workspace::last_opened_workspace_location() - .await - .map(|location| vec![location]) - } - workspace::RestoreOnStartupBehavior::LastSession => { - if let Some(last_session_id) = last_session_id { - let ordered = last_session_window_stack.is_some(); - - let mut locations = workspace::last_session_workspace_locations( - &last_session_id, - last_session_window_stack, - ) - .filter(|locations| !locations.is_empty()); - - // Since last_session_window_order returns the windows ordered front-to-back - // we need to open the window that was frontmost last. - if ordered && let Some(locations) = locations.as_mut() { - locations.reverse(); - } - - locations - } else { - None - } - } - _ => None, - } -} - -fn init_paths() -> HashMap> { - [ - paths::config_dir(), - paths::extensions_dir(), - paths::languages_dir(), - paths::debug_adapters_dir(), - paths::database_dir(), - paths::logs_dir(), - paths::temp_dir(), - paths::hang_traces_dir(), - ] - .into_iter() - .fold(HashMap::default(), |mut errors, path| { - if let Err(e) = std::fs::create_dir_all(path) { - errors.entry(e.kind()).or_insert_with(Vec::new).push(path); - } - errors - }) -} - -pub fn stdout_is_a_pty() -> bool { - std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && io::stdout().is_terminal() -} - -#[derive(Parser, Debug)] -#[command(name = "zed", disable_version_flag = true, max_term_width = 100)] -struct Args { - /// A sequence of space-separated paths or urls that you want to open. - /// - /// Use `path:line:row` syntax to open a file at a specific location. - /// Non-existing paths and directories will ignore `:line:row` suffix. - /// - /// URLs can either be `file://` or `zed://` scheme, or relative to . - paths_or_urls: Vec, - - /// Pairs of file paths to diff. Can be specified multiple times. - #[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"])] - diff: Vec, - - /// Sets a custom directory for all user data (e.g., database, extensions, logs). - /// - /// This overrides the default platform-specific data directory location. - /// On macOS, the default is `~/Library/Application Support/Zed`. - /// On Linux/FreeBSD, the default is `$XDG_DATA_HOME/zed`. - /// On Windows, the default is `%LOCALAPPDATA%\Zed`. - #[arg(long, value_name = "DIR", verbatim_doc_comment)] - user_data_dir: Option, - - /// The username and WSL distribution to use when opening paths. If not specified, - /// Zed will attempt to open the paths directly. - /// - /// The username is optional, and if not specified, the default user for the distribution - /// will be used. - /// - /// Example: `me@Ubuntu` or `Ubuntu`. - /// - /// WARN: You should not fill in this field by hand. - #[cfg(target_os = "windows")] - #[arg(long, value_name = "USER@DISTRO")] - wsl: Option, - - /// Instructs zed to run as a dev server on this machine. (not implemented) - #[arg(long)] - dev_server_token: Option, - - /// Prints system specs. - /// - /// Useful for submitting issues on GitHub when encountering a bug that - /// prevents Zed from starting, so you can't run `zed: copy system specs to - /// clipboard` - #[arg(long)] - system_specs: bool, - - /// Used for the MCP Server, to remove the need for netcat as a dependency, - /// by having Zed act like netcat communicating over a Unix socket. - #[arg(long, hide = true)] - nc: Option, - - /// Used for recording minidumps on crashes by having Zed run a separate - /// process communicating over a socket. - #[arg(long, hide = true)] - crash_handler: Option, - - /// Run zed in the foreground, only used on Windows, to match the behavior on macOS. - #[arg(long)] - #[cfg(target_os = "windows")] - #[arg(hide = true)] - foreground: bool, - - /// The dock action to perform. This is used on Windows only. - #[arg(long)] - #[cfg(target_os = "windows")] - #[arg(hide = true)] - dock_action: Option, - - /// Used for SSH/Git password authentication, to remove the need for netcat as a dependency, - /// by having Zed act like netcat communicating over a Unix socket. - #[arg(long)] - #[cfg(not(target_os = "windows"))] - #[arg(hide = true)] - askpass: Option, - - #[arg(long, hide = true)] - dump_all_actions: bool, - - /// Output current environment variables as JSON to stdout - #[arg(long, hide = true)] - printenv: bool, -} - -#[derive(Clone, Debug)] -enum IdType { - New(String), - Existing(String), -} - -impl ToString for IdType { - fn to_string(&self) -> String { - match self { - IdType::New(id) | IdType::Existing(id) => id.clone(), - } - } -} - -fn parse_url_arg(arg: &str, cx: &App) -> String { - match std::fs::canonicalize(Path::new(&arg)) { - Ok(path) => format!("file://{}", path.display()), - Err(_) => { - if arg.starts_with("file://") - || arg.starts_with("zed-cli://") - || arg.starts_with("ssh://") - || parse_zed_link(arg, cx).is_some() - { - arg.into() - } else { - format!("file://{arg}") - } - } - } -} - -fn load_embedded_fonts(cx: &App) { - let asset_source = cx.asset_source(); - let font_paths = asset_source.list("fonts").unwrap(); - let embedded_fonts = Mutex::new(Vec::new()); - let executor = cx.background_executor(); - - executor.block(executor.scoped(|scope| { - for font_path in &font_paths { - if !font_path.ends_with(".ttf") { - continue; - } - - scope.spawn(async { - let font_bytes = asset_source.load(font_path).unwrap().unwrap(); - embedded_fonts.lock().push(font_bytes); - }); - } - })); - - cx.text_system() - .add_fonts(embedded_fonts.into_inner()) - .unwrap(); -} - -/// Spawns a background task to load the user themes from the themes directory. -fn load_user_themes_in_background(fs: Arc, cx: &mut App) { - cx.spawn({ - let fs = fs.clone(); - async move |cx| { - if let Some(theme_registry) = cx.update(|cx| ThemeRegistry::global(cx)).log_err() { - let themes_dir = paths::themes_dir().as_ref(); - match fs - .metadata(themes_dir) - .await - .ok() - .flatten() - .map(|m| m.is_dir) - { - Some(is_dir) => { - anyhow::ensure!(is_dir, "Themes dir path {themes_dir:?} is not a directory") - } - None => { - fs.create_dir(themes_dir).await.with_context(|| { - format!("Failed to create themes dir at path {themes_dir:?}") - })?; - } - } - theme_registry.load_user_themes(themes_dir, fs).await?; - cx.update(GlobalTheme::reload_theme)?; - } - anyhow::Ok(()) - } - }) - .detach_and_log_err(cx); -} - -/// Spawns a background task to watch the themes directory for changes. -fn watch_themes(fs: Arc, cx: &mut App) { - use std::time::Duration; - cx.spawn(async move |cx| { - let (mut events, _) = fs - .watch(paths::themes_dir(), Duration::from_millis(100)) - .await; - - while let Some(paths) = events.next().await { - for event in paths { - if fs.metadata(&event.path).await.ok().flatten().is_some() - && let Some(theme_registry) = - cx.update(|cx| ThemeRegistry::global(cx)).log_err() - && let Some(()) = theme_registry - .load_user_theme(&event.path, fs.clone()) - .await - .log_err() - { - cx.update(GlobalTheme::reload_theme).log_err(); - } - } - } - }) - .detach() -} - -#[cfg(debug_assertions)] -fn watch_languages(fs: Arc, languages: Arc, cx: &mut App) { - use std::time::Duration; - - cx.background_spawn(async move { - let languages_src = Path::new("crates/languages/src"); - let Some(languages_src) = fs.canonicalize(languages_src).await.log_err() else { - return; - }; - - let (mut events, watcher) = fs.watch(&languages_src, Duration::from_millis(100)).await; - - // add subdirectories since fs.watch is not recursive on Linux - if let Some(mut paths) = fs.read_dir(&languages_src).await.log_err() { - while let Some(path) = paths.next().await { - if let Some(path) = path.log_err() - && fs.is_dir(&path).await - { - watcher.add(&path).log_err(); - } - } - } - - while let Some(event) = events.next().await { - let has_language_file = event - .iter() - .any(|event| event.path.extension().is_some_and(|ext| ext == "scm")); - if has_language_file { - languages.reload(); - } - } - }) - .detach(); -} - -#[cfg(not(debug_assertions))] -fn watch_languages(_fs: Arc, _languages: Arc, _cx: &mut App) {} - -fn dump_all_gpui_actions() { - #[derive(Debug, serde::Serialize)] - struct ActionDef { - name: &'static str, - human_name: String, - aliases: &'static [&'static str], - documentation: Option<&'static str>, - } - let mut actions = gpui::generate_list_of_all_registered_actions() - .map(|action| ActionDef { - name: action.name, - human_name: command_palette::humanize_action_name(action.name), - aliases: action.deprecated_aliases, - documentation: action.documentation, - }) - .collect::>(); - - actions.sort_by_key(|a| a.name); - - io::Write::write( - &mut std::io::stdout(), - serde_json::to_string_pretty(&actions).unwrap().as_bytes(), - ) - .unwrap(); -} - -#[cfg(target_os = "windows")] -fn check_for_conpty_dll() { - use windows::{ - Win32::{Foundation::FreeLibrary, System::LibraryLoader::LoadLibraryW}, - core::w, - }; - - if let Ok(hmodule) = unsafe { LoadLibraryW(w!("conpty.dll")) } { - unsafe { - FreeLibrary(hmodule) - .context("Failed to free conpty.dll") - .log_err(); - } - } else { - log::warn!("Failed to load conpty.dll. Terminal will work with reduced functionality."); - } -} diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs deleted file mode 100644 index da8dffa85d..0000000000 --- a/crates/zed/src/reliability.rs +++ /dev/null @@ -1,337 +0,0 @@ -use anyhow::{Context as _, Result}; -use client::{Client, telemetry::MINIDUMP_ENDPOINT}; -use futures::{AsyncReadExt, TryStreamExt}; -use gpui::{App, AppContext as _, SerializedThreadTaskTimings}; -use http_client::{self, AsyncBody, HttpClient, Request}; -use log::info; -use project::Project; -use proto::{CrashReport, GetCrashFilesResponse}; -use reqwest::multipart::{Form, Part}; -use smol::stream::StreamExt; -use std::{ffi::OsStr, fs, sync::Arc, thread::ThreadId, time::Duration}; -use util::ResultExt; - -use crate::STARTUP_TIME; - -pub fn init(client: Arc, cx: &mut App) { - monitor_hangs(cx); - - if client.telemetry().diagnostics_enabled() { - let client = client.clone(); - cx.background_spawn(async move { - upload_previous_minidumps(client).await.warn_on_err(); - }) - .detach() - } - - cx.observe_new(move |project: &mut Project, _, cx| { - let client = client.clone(); - - let Some(remote_client) = project.remote_client() else { - return; - }; - remote_client.update(cx, |remote_client, cx| { - if !client.telemetry().diagnostics_enabled() { - return; - } - let request = remote_client - .proto_client() - .request(proto::GetCrashFiles {}); - cx.background_spawn(async move { - let GetCrashFilesResponse { crashes } = request.await?; - - let Some(endpoint) = MINIDUMP_ENDPOINT.as_ref() else { - return Ok(()); - }; - for CrashReport { - metadata, - minidump_contents, - } in crashes - { - if let Some(metadata) = serde_json::from_str(&metadata).log_err() { - upload_minidump(client.clone(), endpoint, minidump_contents, &metadata) - .await - .log_err(); - } - } - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - }) - }) - .detach(); -} - -fn monitor_hangs(cx: &App) { - let main_thread_id = std::thread::current().id(); - - let foreground_executor = cx.foreground_executor(); - let background_executor = cx.background_executor(); - - // 3 seconds hang - let (mut tx, mut rx) = futures::channel::mpsc::channel(3); - foreground_executor - .spawn(async move { while (rx.next().await).is_some() {} }) - .detach(); - - background_executor - .spawn({ - let background_executor = background_executor.clone(); - async move { - let mut hang_time = None; - - let mut hanging = false; - loop { - background_executor.timer(Duration::from_secs(1)).await; - match tx.try_send(()) { - Ok(_) => { - hang_time = None; - hanging = false; - continue; - } - Err(e) => { - let is_full = e.into_send_error().is_full(); - if is_full && !hanging { - hanging = true; - hang_time = Some(chrono::Local::now()); - } - - if is_full { - save_hang_trace( - main_thread_id, - &background_executor, - hang_time.unwrap(), - ); - } - } - } - } - } - }) - .detach(); -} - -fn save_hang_trace( - main_thread_id: ThreadId, - background_executor: &gpui::BackgroundExecutor, - hang_time: chrono::DateTime, -) { - let thread_timings = background_executor.dispatcher.get_all_timings(); - let thread_timings = thread_timings - .into_iter() - .map(|mut timings| { - if timings.thread_id == main_thread_id { - timings.thread_name = Some("main".to_string()); - } - - SerializedThreadTaskTimings::convert(*STARTUP_TIME.get().unwrap(), timings) - }) - .collect::>(); - - let trace_path = paths::hang_traces_dir().join(&format!( - "hang-{}.miniprof", - hang_time.format("%Y-%m-%d_%H-%M-%S") - )); - - let Some(timings) = serde_json::to_string(&thread_timings) - .context("hang timings serialization") - .log_err() - else { - return; - }; - - std::fs::write(&trace_path, timings) - .context("hang trace file writing") - .log_err(); - - info!( - "hang detected, trace file saved at: {}", - trace_path.display() - ); -} - -pub async fn upload_previous_minidumps(client: Arc) -> anyhow::Result<()> { - let Some(minidump_endpoint) = MINIDUMP_ENDPOINT.as_ref() else { - log::warn!("Minidump endpoint not set"); - return Ok(()); - }; - - let mut children = smol::fs::read_dir(paths::logs_dir()).await?; - while let Some(child) = children.next().await { - let child = child?; - let child_path = child.path(); - if child_path.extension() != Some(OsStr::new("dmp")) { - continue; - } - let mut json_path = child_path.clone(); - json_path.set_extension("json"); - if let Ok(metadata) = serde_json::from_slice(&smol::fs::read(&json_path).await?) - && upload_minidump( - client.clone(), - minidump_endpoint, - smol::fs::read(&child_path) - .await - .context("Failed to read minidump")?, - &metadata, - ) - .await - .log_err() - .is_some() - { - fs::remove_file(child_path).ok(); - fs::remove_file(json_path).ok(); - } - } - Ok(()) -} - -async fn upload_minidump( - client: Arc, - endpoint: &str, - minidump: Vec, - metadata: &crashes::CrashInfo, -) -> Result<()> { - let mut form = Form::new() - .part( - "upload_file_minidump", - Part::bytes(minidump) - .file_name("minidump.dmp") - .mime_str("application/octet-stream")?, - ) - .text( - "sentry[tags][channel]", - metadata.init.release_channel.clone(), - ) - .text("sentry[tags][version]", metadata.init.zed_version.clone()) - .text("sentry[tags][binary]", metadata.init.binary.clone()) - .text("sentry[release]", metadata.init.commit_sha.clone()) - .text("platform", "rust"); - let mut panic_message = "".to_owned(); - if let Some(panic_info) = metadata.panic.as_ref() { - panic_message = panic_info.message.clone(); - form = form - .text("sentry[logentry][formatted]", panic_info.message.clone()) - .text("span", panic_info.span.clone()); - } - if let Some(minidump_error) = metadata.minidump_error.clone() { - form = form.text("minidump_error", minidump_error); - } - - if let Some(id) = client.telemetry().metrics_id() { - form = form.text("sentry[user][id]", id.to_string()); - form = form.text( - "sentry[user][is_staff]", - if client.telemetry().is_staff().unwrap_or_default() { - "true" - } else { - "false" - }, - ); - } else if let Some(id) = client.telemetry().installation_id() { - form = form.text("sentry[user][id]", format!("installation-{}", id)) - } - - ::telemetry::event!( - "Minidump Uploaded", - panic_message = panic_message, - crashed_version = metadata.init.zed_version.clone(), - commit_sha = metadata.init.commit_sha.clone(), - ); - - let gpu_count = metadata.gpus.len(); - for (index, gpu) in metadata.gpus.iter().cloned().enumerate() { - let system_specs::GpuInfo { - device_name, - device_pci_id, - vendor_name, - vendor_pci_id, - driver_version, - driver_name, - } = gpu; - let num = if gpu_count == 1 && metadata.active_gpu.is_none() { - String::new() - } else { - index.to_string() - }; - let name = format!("gpu{num}"); - let root = format!("sentry[contexts][{name}]"); - form = form - .text( - format!("{root}[Description]"), - "A GPU found on the users system. May or may not be the GPU Zed is running on", - ) - .text(format!("{root}[type]"), "gpu") - .text(format!("{root}[name]"), device_name.unwrap_or(name)) - .text(format!("{root}[id]"), format!("{:#06x}", device_pci_id)) - .text( - format!("{root}[vendor_id]"), - format!("{:#06x}", vendor_pci_id), - ) - .text_if_some(format!("{root}[vendor_name]"), vendor_name) - .text_if_some(format!("{root}[driver_version]"), driver_version) - .text_if_some(format!("{root}[driver_name]"), driver_name); - } - if let Some(active_gpu) = metadata.active_gpu.clone() { - form = form - .text( - "sentry[contexts][Active_GPU][Description]", - "The GPU Zed is running on", - ) - .text("sentry[contexts][Active_GPU][type]", "gpu") - .text("sentry[contexts][Active_GPU][name]", active_gpu.device_name) - .text( - "sentry[contexts][Active_GPU][driver_version]", - active_gpu.driver_info, - ) - .text( - "sentry[contexts][Active_GPU][driver_name]", - active_gpu.driver_name, - ) - .text( - "sentry[contexts][Active_GPU][is_software_emulated]", - active_gpu.is_software_emulated.to_string(), - ); - } - - // TODO: feature-flag-context, and more of device-context like screen resolution, available ram, device model, etc - - let stream = form - .into_stream() - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) - .into_async_read(); - let body = AsyncBody::from_reader(stream); - let req = Request::builder().uri(endpoint).body(body)?; - let mut response_text = String::new(); - let mut response = client.http_client().send(req).await?; - response - .body_mut() - .read_to_string(&mut response_text) - .await?; - if !response.status().is_success() { - anyhow::bail!("failed to upload minidump: {response_text}"); - } - log::info!("Uploaded minidump. event id: {response_text}"); - Ok(()) -} - -trait FormExt { - fn text_if_some( - self, - label: impl Into>, - value: Option>>, - ) -> Self; -} - -impl FormExt for Form { - fn text_if_some( - self, - label: impl Into>, - value: Option>>, - ) -> Self { - match value { - Some(value) => self.text(label.into(), value.into()), - None => self, - } - } -} diff --git a/crates/zed/src/zed-main.rs b/crates/zed/src/zed-main.rs deleted file mode 100644 index 6c49c197dd..0000000000 --- a/crates/zed/src/zed-main.rs +++ /dev/null @@ -1,8 +0,0 @@ -// Disable command line from opening on release mode -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -pub fn main() { - // separated out so that the file containing the main function can be imported by other crates, - // while having all gpui resources that are registered in main (primarily actions) initialized - zed::main(); -} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs deleted file mode 100644 index 71653124b1..0000000000 --- a/crates/zed/src/zed.rs +++ /dev/null @@ -1,5163 +0,0 @@ -mod app_menus; -pub mod component_preview; -pub mod edit_prediction_registry; -#[cfg(target_os = "macos")] -pub(crate) mod mac_only_instance; -mod migrate; -mod open_listener; -mod quick_action_bar; -#[cfg(target_os = "windows")] -pub(crate) mod windows_only_instance; - -use agent_ui::{AgentDiffToolbar, AgentPanelDelegate}; -use anyhow::Context as _; -pub use app_menus::*; -use assets::Assets; -use audio::{AudioSettings, REPLAY_DURATION}; -use breadcrumbs::Breadcrumbs; -use client::zed_urls; -use collections::VecDeque; -use debugger_ui::debugger_panel::DebugPanel; -use editor::{Editor, MultiBuffer}; -use extension_host::ExtensionStore; -use feature_flags::{FeatureFlagAppExt, PanicFeatureFlag}; -use fs::Fs; -use futures::FutureExt as _; -use futures::future::Either; -use futures::{StreamExt, channel::mpsc, select_biased}; -use git_ui::commit_view::CommitViewToolbar; -use git_ui::git_panel::GitPanel; -use git_ui::project_diff::ProjectDiffToolbar; -use gpui::{ - Action, App, AppContext as _, AsyncWindowContext, Context, DismissEvent, Element, Entity, - Focusable, KeyBinding, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString, - Styled, Task, TitlebarOptions, UpdateGlobal, WeakEntity, Window, WindowKind, WindowOptions, - actions, image_cache, point, px, retain_all, -}; -use image_viewer::ImageInfo; -use language::Capability; -use language_onboarding::BasedPyrightBanner; -use language_tools::lsp_button::{self, LspButton}; -use language_tools::lsp_log_view::LspLogToolbarItemView; -use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType}; -use migrator::migrate_keymap; -use onboarding::DOCS_URL; -use onboarding::multibuffer_hint::MultibufferHint; -pub use open_listener::*; -use outline_panel::OutlinePanel; -use paths::{ - local_debug_file_relative_path, local_settings_file_relative_path, - local_tasks_file_relative_path, -}; -use project::{DirectoryLister, DisableAiSettings, ProjectItem}; -use project_panel::ProjectPanel; -use prompt_store::PromptBuilder; -use quick_action_bar::QuickActionBar; -use recent_projects::open_remote_project; -use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; -use rope::Rope; -use search::project_search::ProjectSearchBar; -use settings::{ - BaseKeymap, DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeybindSource, KeymapFile, - KeymapFileLoadResult, MigrationStatus, Settings, SettingsStore, VIM_KEYMAP_PATH, - initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content, - update_settings_file, -}; -use std::time::Duration; -use std::{ - borrow::Cow, - path::{Path, PathBuf}, - sync::Arc, - sync::atomic::{self, AtomicBool}, -}; -use terminal_view::terminal_panel::{self, TerminalPanel}; -use theme::{ActiveTheme, GlobalTheme, SystemAppearance, ThemeRegistry, ThemeSettings}; -use ui::{PopoverMenuHandle, prelude::*}; -use util::markdown::MarkdownString; -use util::rel_path::RelPath; -use util::{ResultExt, asset_str}; -use uuid::Uuid; -use vim_mode_setting::VimModeSetting; -use workspace::notifications::{ - NotificationId, SuppressEvent, dismiss_app_notification, show_app_notification, -}; -use workspace::{ - AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings, - create_and_open_local_file, notifications::simple_message_notification::MessageNotification, - open_new, -}; -use workspace::{ - CloseIntent, CloseWindow, NotificationFrame, RestoreBanner, with_active_or_new_workspace, -}; -use workspace::{Pane, notifications::DetachAndPromptErr}; -use zed_actions::{ - OpenAccountSettings, OpenBrowser, OpenDocs, OpenServerSettings, OpenSettingsFile, OpenZedUrl, - Quit, -}; - -actions!( - zed, - [ - /// Opens the element inspector for debugging UI. - DebugElements, - /// Hides the application window. - Hide, - /// Hides all other application windows. - HideOthers, - /// Minimizes the current window. - Minimize, - /// Opens the default settings file. - OpenDefaultSettings, - /// Opens project-specific settings file. - OpenProjectSettingsFile, - /// Opens the project tasks configuration. - OpenProjectTasks, - /// Opens the tasks panel. - OpenTasks, - /// Opens debug tasks configuration. - OpenDebugTasks, - /// Resets the application database. - ResetDatabase, - /// Shows all hidden windows. - ShowAll, - /// Toggles fullscreen mode. - ToggleFullScreen, - /// Zooms the window. - Zoom, - /// Triggers a test panic for debugging. - TestPanic, - /// Triggers a hard crash for debugging. - TestCrash, - ] -); - -actions!( - dev, - [ - /// Stores last 30s of audio from zed staff using the experimental rodio - /// audio system (including yourself) on the current call in a tar file - /// in the current working directory. - CaptureRecentAudio, - ] -); - -pub fn init(cx: &mut App) { - #[cfg(target_os = "macos")] - cx.on_action(|_: &Hide, cx| cx.hide()); - #[cfg(target_os = "macos")] - cx.on_action(|_: &HideOthers, cx| cx.hide_other_apps()); - #[cfg(target_os = "macos")] - cx.on_action(|_: &ShowAll, cx| cx.unhide_other_apps()); - cx.on_action(quit); - - cx.on_action(|_: &RestoreBanner, cx| title_bar::restore_banner(cx)); - let flag = cx.wait_for_flag::(); - cx.spawn(async |cx| { - if cx - .update(|cx| ReleaseChannel::global(cx) == ReleaseChannel::Dev) - .unwrap_or_default() - || flag.await - { - cx.update(|cx| { - cx.on_action(|_: &TestPanic, _| panic!("Ran the TestPanic action")); - cx.on_action(|_: &TestCrash, _| { - unsafe extern "C" { - fn puts(s: *const i8); - } - unsafe { - puts(0xabad1d3a as *const i8); - } - }); - }) - .ok(); - }; - }) - .detach(); - cx.on_action(|_: &OpenLog, cx| { - with_active_or_new_workspace(cx, |workspace, window, cx| { - open_log_file(workspace, window, cx); - }); - }); - cx.on_action(|_: &workspace::RevealLogInFileManager, cx| { - cx.reveal_path(paths::log_file().as_path()); - }); - cx.on_action(|_: &zed_actions::OpenLicenses, cx| { - with_active_or_new_workspace(cx, |workspace, window, cx| { - open_bundled_file( - workspace, - asset_str::("licenses.md"), - "Open Source License Attribution", - "Markdown", - window, - cx, - ); - }); - }); - cx.on_action(|_: &zed_actions::OpenTelemetryLog, cx| { - with_active_or_new_workspace(cx, |workspace, window, cx| { - open_telemetry_log_file(workspace, window, cx); - }); - }); - cx.on_action(|&zed_actions::OpenKeymapFile, cx| { - with_active_or_new_workspace(cx, |_, window, cx| { - open_settings_file( - paths::keymap_file(), - || settings::initial_keymap_content().as_ref().into(), - window, - cx, - ); - }); - }); - cx.on_action(|_: &OpenSettingsFile, cx| { - with_active_or_new_workspace(cx, |_, window, cx| { - open_settings_file( - paths::settings_file(), - || settings::initial_user_settings_content().as_ref().into(), - window, - cx, - ); - }); - }); - cx.on_action(|_: &OpenAccountSettings, cx| { - with_active_or_new_workspace(cx, |_, _, cx| { - cx.open_url(&zed_urls::account_url(cx)); - }); - }); - cx.on_action(|_: &OpenTasks, cx| { - with_active_or_new_workspace(cx, |_, window, cx| { - open_settings_file( - paths::tasks_file(), - || settings::initial_tasks_content().as_ref().into(), - window, - cx, - ); - }); - }); - cx.on_action(|_: &OpenDebugTasks, cx| { - with_active_or_new_workspace(cx, |_, window, cx| { - open_settings_file( - paths::debug_scenarios_file(), - || settings::initial_debug_tasks_content().as_ref().into(), - window, - cx, - ); - }); - }); - cx.on_action(|_: &OpenDefaultSettings, cx| { - with_active_or_new_workspace(cx, |workspace, window, cx| { - open_bundled_file( - workspace, - settings::default_settings(), - "Default Settings", - "JSON", - window, - cx, - ); - }); - }); - cx.on_action(|_: &zed_actions::OpenDefaultKeymap, cx| { - with_active_or_new_workspace(cx, |workspace, window, cx| { - open_bundled_file( - workspace, - settings::default_keymap(), - "Default Key Bindings", - "JSON", - window, - cx, - ); - }); - }); - cx.on_action(|_: &zed_actions::About, cx| { - with_active_or_new_workspace(cx, |workspace, window, cx| { - about(workspace, window, cx); - }); - }); -} - -fn bind_on_window_closed(cx: &mut App) -> Option { - #[cfg(target_os = "macos")] - { - WorkspaceSettings::get_global(cx) - .on_last_window_closed - .is_quit_app() - .then(|| { - cx.on_window_closed(|cx| { - if cx.windows().is_empty() { - cx.quit(); - } - }) - }) - } - #[cfg(not(target_os = "macos"))] - { - Some(cx.on_window_closed(|cx| { - if cx.windows().is_empty() { - cx.quit(); - } - })) - } -} - -pub fn build_window_options(display_uuid: Option, cx: &mut App) -> WindowOptions { - let display = display_uuid.and_then(|uuid| { - cx.displays() - .into_iter() - .find(|display| display.uuid().ok() == Some(uuid)) - }); - let app_id = ReleaseChannel::global(cx).app_id(); - let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") { - Ok(val) if val == "server" => gpui::WindowDecorations::Server, - Ok(val) if val == "client" => gpui::WindowDecorations::Client, - _ => match WorkspaceSettings::get_global(cx).window_decorations { - settings::WindowDecorations::Server => gpui::WindowDecorations::Server, - settings::WindowDecorations::Client => gpui::WindowDecorations::Client, - }, - }; - - let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs; - - WindowOptions { - titlebar: Some(TitlebarOptions { - title: None, - appears_transparent: true, - traffic_light_position: Some(point(px(9.0), px(9.0))), - }), - window_bounds: None, - focus: false, - show: false, - kind: WindowKind::Normal, - is_movable: true, - display_id: display.map(|display| display.id()), - window_background: cx.theme().window_background_appearance(), - app_id: Some(app_id.to_owned()), - window_decorations: Some(window_decorations), - window_min_size: Some(gpui::Size { - width: px(360.0), - height: px(240.0), - }), - tabbing_identifier: if use_system_window_tabs { - Some(String::from("zed")) - } else { - None - }, - ..Default::default() - } -} - -pub fn initialize_workspace( - app_state: Arc, - prompt_builder: Arc, - cx: &mut App, -) { - let mut _on_close_subscription = bind_on_window_closed(cx); - cx.observe_global::(move |cx| { - _on_close_subscription = bind_on_window_closed(cx); - }) - .detach(); - - cx.observe_new(move |workspace: &mut Workspace, window, cx| { - let Some(window) = window else { - return; - }; - - let workspace_handle = cx.entity(); - let center_pane = workspace.active_pane().clone(); - initialize_pane(workspace, ¢er_pane, window, cx); - - cx.subscribe_in(&workspace_handle, window, { - move |workspace, _, event, window, cx| match event { - workspace::Event::PaneAdded(pane) => { - initialize_pane(workspace, pane, window, cx); - } - workspace::Event::OpenBundledFile { - text, - title, - language, - } => open_bundled_file(workspace, text.clone(), title, language, window, cx), - _ => {} - } - }) - .detach(); - - #[cfg(not(target_os = "macos"))] - initialize_file_watcher(window, cx); - - if let Some(specs) = window.gpu_specs() { - log::info!("Using GPU: {:?}", specs); - show_software_emulation_warning_if_needed(specs.clone(), window, cx); - if let Some((crash_server, message)) = crashes::CRASH_HANDLER - .get() - .zip(bincode::serialize(&specs).ok()) - && let Err(err) = crash_server.send_message(3, message) - { - log::warn!( - "Failed to store active gpu info for crash reporting: {}", - err - ); - } - } - - #[cfg(target_os = "windows")] - unstable_version_notification(cx); - - let edit_prediction_menu_handle = PopoverMenuHandle::default(); - let edit_prediction_ui = cx.new(|cx| { - edit_prediction_ui::EditPredictionButton::new( - app_state.fs.clone(), - app_state.user_store.clone(), - edit_prediction_menu_handle.clone(), - app_state.client.clone(), - cx, - ) - }); - workspace.register_action({ - move |_, _: &edit_prediction_ui::ToggleMenu, window, cx| { - edit_prediction_menu_handle.toggle(window, cx); - } - }); - - let search_button = cx.new(|_| search::search_status_button::SearchButton::new()); - let diagnostic_summary = - cx.new(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx)); - let activity_indicator = activity_indicator::ActivityIndicator::new( - workspace, - workspace.project().read(cx).languages().clone(), - window, - cx, - ); - let active_buffer_language = - cx.new(|_| language_selector::ActiveBufferLanguage::new(workspace)); - let active_toolchain_language = - cx.new(|cx| toolchain_selector::ActiveToolchain::new(workspace, window, cx)); - let vim_mode_indicator = cx.new(|cx| vim::ModeIndicator::new(window, cx)); - let image_info = cx.new(|_cx| ImageInfo::new(workspace)); - - let lsp_button_menu_handle = PopoverMenuHandle::default(); - let lsp_button = - cx.new(|cx| LspButton::new(workspace, lsp_button_menu_handle.clone(), window, cx)); - workspace.register_action({ - move |_, _: &lsp_button::ToggleMenu, window, cx| { - lsp_button_menu_handle.toggle(window, cx); - } - }); - - let cursor_position = - cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace)); - let line_ending_indicator = - cx.new(|_| line_ending_selector::LineEndingIndicator::default()); - workspace.status_bar().update(cx, |status_bar, cx| { - status_bar.add_left_item(search_button, window, cx); - status_bar.add_left_item(lsp_button, window, cx); - status_bar.add_left_item(diagnostic_summary, window, cx); - status_bar.add_left_item(activity_indicator, window, cx); - status_bar.add_right_item(edit_prediction_ui, window, cx); - status_bar.add_right_item(active_buffer_language, window, cx); - status_bar.add_right_item(active_toolchain_language, window, cx); - status_bar.add_right_item(line_ending_indicator, window, cx); - status_bar.add_right_item(vim_mode_indicator, window, cx); - status_bar.add_right_item(cursor_position, window, cx); - status_bar.add_right_item(image_info, window, cx); - }); - - let handle = cx.entity().downgrade(); - window.on_window_should_close(cx, move |window, cx| { - handle - .update(cx, |workspace, cx| { - // We'll handle closing asynchronously - workspace.close_window(&CloseWindow, window, cx); - false - }) - .unwrap_or(true) - }); - - initialize_panels(prompt_builder.clone(), window, cx); - register_actions(app_state.clone(), workspace, window, cx); - - workspace.focus_handle(cx).focus(window); - }) - .detach(); -} - -#[cfg(target_os = "windows")] -fn unstable_version_notification(cx: &mut App) { - if !matches!( - ReleaseChannel::try_global(cx), - Some(ReleaseChannel::Nightly) - ) { - return; - } - let db_key = "zed_windows_nightly_notif_shown_at".to_owned(); - let time = chrono::Utc::now(); - if let Some(last_shown) = db::kvp::KEY_VALUE_STORE - .read_kvp(&db_key) - .log_err() - .flatten() - .and_then(|timestamp| chrono::DateTime::parse_from_rfc3339(×tamp).ok()) - { - if time.fixed_offset() - last_shown < chrono::Duration::days(7) { - return; - } - } - cx.spawn(async move |_| { - db::kvp::KEY_VALUE_STORE - .write_kvp(db_key, time.to_rfc3339()) - .await - }) - .detach_and_log_err(cx); - struct WindowsNightly; - show_app_notification(NotificationId::unique::(), cx, |cx| { - cx.new(|cx| { - MessageNotification::new("You're using an unstable version of Zed (Nightly)", cx) - .primary_message("Download Stable") - .primary_icon_color(Color::Accent) - .primary_icon(IconName::Download) - .primary_on_click(|window, cx| { - window.dispatch_action( - zed_actions::OpenBrowser { - url: "https://zed.dev/download".to_string(), - } - .boxed_clone(), - cx, - ); - cx.emit(DismissEvent); - }) - }) - }); -} - -#[cfg(any(target_os = "linux", target_os = "freebsd"))] -fn initialize_file_watcher(window: &mut Window, cx: &mut Context) { - if let Err(e) = fs::fs_watcher::global(|_| {}) { - let message = format!( - db::indoc! {r#" - inotify_init returned {} - - This may be due to system-wide limits on inotify instances. For troubleshooting see: https://zed.dev/docs/linux - "#}, - e - ); - let prompt = window.prompt( - PromptLevel::Critical, - "Could not start inotify", - Some(&message), - &["Troubleshoot and Quit"], - cx, - ); - cx.spawn(async move |_, cx| { - if prompt.await == Ok(0) { - cx.update(|cx| { - cx.open_url("https://zed.dev/docs/linux#could-not-start-inotify"); - cx.quit(); - }) - .ok(); - } - }) - .detach() - } -} - -#[cfg(target_os = "windows")] -fn initialize_file_watcher(window: &mut Window, cx: &mut Context) { - if let Err(e) = fs::fs_watcher::global(|_| {}) { - let message = format!( - db::indoc! {r#" - ReadDirectoryChangesW initialization failed: {} - - This may occur on network filesystems and WSL paths. For troubleshooting see: https://zed.dev/docs/windows - "#}, - e - ); - let prompt = window.prompt( - PromptLevel::Critical, - "Could not start ReadDirectoryChangesW", - Some(&message), - &["Troubleshoot and Quit"], - cx, - ); - cx.spawn(async move |_, cx| { - if prompt.await == Ok(0) { - cx.update(|cx| { - cx.open_url("https://zed.dev/docs/windows"); - cx.quit() - }) - .ok(); - } - }) - .detach() - } -} - -fn show_software_emulation_warning_if_needed( - specs: gpui::GpuSpecs, - window: &mut Window, - cx: &mut Context, -) { - if specs.is_software_emulated && std::env::var("ZED_ALLOW_EMULATED_GPU").is_err() { - let (graphics_api, docs_url, open_url) = if cfg!(target_os = "windows") { - ( - "DirectX", - "https://zed.dev/docs/windows", - "https://zed.dev/docs/windows", - ) - } else { - ( - "Vulkan", - "https://zed.dev/docs/linux", - "https://zed.dev/docs/linux#zed-fails-to-open-windows", - ) - }; - let message = format!( - db::indoc! {r#" - Zed uses {} for rendering and requires a compatible GPU. - - Currently you are using a software emulated GPU ({}) which - will result in awful performance. - - For troubleshooting see: {} - Set ZED_ALLOW_EMULATED_GPU=1 env var to permanently override. - "#}, - graphics_api, specs.device_name, docs_url - ); - let prompt = window.prompt( - PromptLevel::Critical, - "Unsupported GPU", - Some(&message), - &["Skip", "Troubleshoot and Quit"], - cx, - ); - cx.spawn(async move |_, cx| { - if prompt.await == Ok(1) { - cx.update(|cx| { - cx.open_url(open_url); - cx.quit(); - }) - .ok(); - } - }) - .detach() - } -} - -fn initialize_panels( - prompt_builder: Arc, - window: &mut Window, - cx: &mut Context, -) { - cx.spawn_in(window, async move |workspace_handle, cx| { - let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); - let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone()); - let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); - let git_panel = GitPanel::load(workspace_handle.clone(), cx.clone()); - let channels_panel = - collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); - let notification_panel = collab_ui::notification_panel::NotificationPanel::load( - workspace_handle.clone(), - cx.clone(), - ); - let debug_panel = DebugPanel::load(workspace_handle.clone(), cx); - - async fn add_panel_when_ready( - panel_task: impl Future>> + 'static, - workspace_handle: WeakEntity, - mut cx: gpui::AsyncWindowContext, - ) { - if let Some(panel) = panel_task.await.context("failed to load panel").log_err() - { - workspace_handle - .update_in(&mut cx, |workspace, window, cx| { - workspace.add_panel(panel, window, cx); - }) - .log_err(); - } - } - - futures::join!( - add_panel_when_ready(project_panel, workspace_handle.clone(), cx.clone()), - add_panel_when_ready(outline_panel, workspace_handle.clone(), cx.clone()), - add_panel_when_ready(terminal_panel, workspace_handle.clone(), cx.clone()), - add_panel_when_ready(git_panel, workspace_handle.clone(), cx.clone()), - add_panel_when_ready(channels_panel, workspace_handle.clone(), cx.clone()), - add_panel_when_ready(notification_panel, workspace_handle.clone(), cx.clone()), - add_panel_when_ready(debug_panel, workspace_handle.clone(), cx.clone()), - initialize_agent_panel(workspace_handle, prompt_builder, cx.clone()).map(|r| r.log_err()) - ); - - anyhow::Ok(()) - }) - .detach(); -} - -async fn initialize_agent_panel( - workspace_handle: WeakEntity, - prompt_builder: Arc, - mut cx: AsyncWindowContext, -) -> anyhow::Result<()> { - fn setup_or_teardown_agent_panel( - workspace: &mut Workspace, - prompt_builder: Arc, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let disable_ai = SettingsStore::global(cx) - .get::(None) - .disable_ai - || cfg!(test); - let existing_panel = workspace.panel::(cx); - match (disable_ai, existing_panel) { - (false, None) => cx.spawn_in(window, async move |workspace, cx| { - let panel = - agent_ui::AgentPanel::load(workspace.clone(), prompt_builder, cx.clone()) - .await?; - workspace.update_in(cx, |workspace, window, cx| { - let disable_ai = SettingsStore::global(cx) - .get::(None) - .disable_ai; - let have_panel = workspace.panel::(cx).is_some(); - if !disable_ai && !have_panel { - workspace.add_panel(panel, window, cx); - } - }) - }), - (true, Some(existing_panel)) => { - workspace.remove_panel::(&existing_panel, window, cx); - Task::ready(Ok(())) - } - _ => Task::ready(Ok(())), - } - } - - workspace_handle - .update_in(&mut cx, |workspace, window, cx| { - setup_or_teardown_agent_panel(workspace, prompt_builder.clone(), window, cx) - })? - .await?; - - workspace_handle.update_in(&mut cx, |workspace, window, cx| { - cx.observe_global_in::(window, { - let prompt_builder = prompt_builder.clone(); - move |workspace, window, cx| { - setup_or_teardown_agent_panel(workspace, prompt_builder.clone(), window, cx) - .detach_and_log_err(cx); - } - }) - .detach(); - - // Register the actions that are shared between `assistant` and `assistant2`. - // - // We need to do this here instead of within the individual `init` - // functions so that we only register the actions once. - // - // Once we ship `assistant2` we can push this back down into `agent::agent_panel::init`. - if !cfg!(test) { - ::set_global( - Arc::new(agent_ui::ConcreteAssistantPanelDelegate), - cx, - ); - - workspace - .register_action(agent_ui::AgentPanel::toggle_focus) - .register_action(agent_ui::InlineAssistant::inline_assist); - } - })?; - - anyhow::Ok(()) -} - -fn register_actions( - app_state: Arc, - workspace: &mut Workspace, - _: &mut Window, - cx: &mut Context, -) { - workspace - .register_action(|_, _: &OpenDocs, _, cx| cx.open_url(DOCS_URL)) - .register_action(|_, _: &Minimize, window, _| { - window.minimize_window(); - }) - .register_action(|_, _: &Zoom, window, _| { - window.zoom_window(); - }) - .register_action(|_, _: &ToggleFullScreen, window, _| { - window.toggle_fullscreen(); - }) - .register_action(|_, action: &OpenZedUrl, _, cx| { - OpenListener::global(cx).open(RawOpenRequest { - urls: vec![action.url.clone()], - ..Default::default() - }) - }) - .register_action(|workspace, action: &OpenBrowser, _window, cx| { - // Parse and validate the URL to ensure it's properly formatted - match url::Url::parse(&action.url) { - Ok(parsed_url) => { - // Use the parsed URL's string representation which is properly escaped - cx.open_url(parsed_url.as_str()); - } - Err(e) => { - workspace.show_error( - &anyhow::anyhow!( - "Opening this URL in a browser failed because the URL is invalid: {}\n\nError was: {e}", - action.url - ), - cx, - ); - } - } - }) - .register_action(|workspace, _: &workspace::Open, window, cx| { - telemetry::event!("Project Opened"); - let paths = workspace.prompt_for_open_path( - PathPromptOptions { - files: true, - directories: true, - multiple: true, - prompt: None, - }, - DirectoryLister::Local( - workspace.project().clone(), - workspace.app_state().fs.clone(), - ), - window, - cx, - ); - - cx.spawn_in(window, async move |this, cx| { - let Some(paths) = paths.await.log_err().flatten() else { - return; - }; - - if let Some(task) = this - .update_in(cx, |this, window, cx| { - this.open_workspace_for_paths(false, paths, window, cx) - }) - .log_err() - { - task.await.log_err(); - } - }) - .detach() - }) - .register_action(|workspace, action: &zed_actions::OpenRemote, window, cx| { - if !action.from_existing_connection { - cx.propagate(); - return; - } - // You need existing remote connection to open it this way - if workspace.project().read(cx).is_local() { - return; - } - telemetry::event!("Project Opened"); - let paths = workspace.prompt_for_open_path( - PathPromptOptions { - files: true, - directories: true, - multiple: true, - prompt: None, - }, - DirectoryLister::Project(workspace.project().clone()), - window, - cx, - ); - cx.spawn_in(window, async move |this, cx| { - let Some(paths) = paths.await.log_err().flatten() else { - return; - }; - if let Some(task) = this - .update_in(cx, |this, window, cx| { - open_new_ssh_project_from_project(this, paths, window, cx) - }) - .log_err() - { - task.await.log_err(); - } - }) - .detach() - }) - .register_action({ - let fs = app_state.fs.clone(); - move |_, action: &zed_actions::IncreaseUiFontSize, _window, cx| { - if action.persist { - update_settings_file(fs.clone(), cx, move |settings, cx| { - let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) + px(1.0); - let _ = settings - .theme - .ui_font_size - .insert(theme::clamp_font_size(ui_font_size).into()); - }); - } else { - theme::adjust_ui_font_size(cx, |size| size + px(1.0)); - } - } - }) - .register_action({ - let fs = app_state.fs.clone(); - move |_, action: &zed_actions::DecreaseUiFontSize, _window, cx| { - if action.persist { - update_settings_file(fs.clone(), cx, move |settings, cx| { - let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) - px(1.0); - let _ = settings - .theme - .ui_font_size - .insert(theme::clamp_font_size(ui_font_size).into()); - }); - } else { - theme::adjust_ui_font_size(cx, |size| size - px(1.0)); - } - } - }) - .register_action({ - let fs = app_state.fs.clone(); - move |_, action: &zed_actions::ResetUiFontSize, _window, cx| { - if action.persist { - update_settings_file(fs.clone(), cx, move |settings, _| { - settings.theme.ui_font_size = None; - }); - } else { - theme::reset_ui_font_size(cx); - } - } - }) - .register_action({ - let fs = app_state.fs.clone(); - move |_, action: &zed_actions::IncreaseBufferFontSize, _window, cx| { - if action.persist { - update_settings_file(fs.clone(), cx, move |settings, cx| { - let buffer_font_size = - ThemeSettings::get_global(cx).buffer_font_size(cx) + px(1.0); - let _ = settings - .theme - .buffer_font_size - .insert(theme::clamp_font_size(buffer_font_size).into()); - }); - } else { - theme::adjust_buffer_font_size(cx, |size| size + px(1.0)); - } - } - }) - .register_action({ - let fs = app_state.fs.clone(); - move |_, action: &zed_actions::DecreaseBufferFontSize, _window, cx| { - if action.persist { - update_settings_file(fs.clone(), cx, move |settings, cx| { - let buffer_font_size = - ThemeSettings::get_global(cx).buffer_font_size(cx) - px(1.0); - let _ = settings - .theme - .buffer_font_size - .insert(theme::clamp_font_size(buffer_font_size).into()); - }); - } else { - theme::adjust_buffer_font_size(cx, |size| size - px(1.0)); - } - } - }) - .register_action({ - let fs = app_state.fs.clone(); - move |_, action: &zed_actions::ResetBufferFontSize, _window, cx| { - if action.persist { - update_settings_file(fs.clone(), cx, move |settings, _| { - settings.theme.buffer_font_size = None; - }); - } else { - theme::reset_buffer_font_size(cx); - } - } - }) - .register_action({ - let fs = app_state.fs.clone(); - move |_, action: &zed_actions::ResetAllZoom, _window, cx| { - if action.persist { - update_settings_file(fs.clone(), cx, move |settings, _| { - settings.theme.ui_font_size = None; - settings.theme.buffer_font_size = None; - settings.theme.agent_ui_font_size = None; - settings.theme.agent_buffer_font_size = None; - }); - } else { - theme::reset_ui_font_size(cx); - theme::reset_buffer_font_size(cx); - theme::reset_agent_ui_font_size(cx); - theme::reset_agent_buffer_font_size(cx); - } - } - }) - .register_action(|_, _: &install_cli::RegisterZedScheme, window, cx| { - cx.spawn_in(window, async move |workspace, cx| { - install_cli::register_zed_scheme(cx).await?; - workspace.update_in(cx, |workspace, _, cx| { - struct RegisterZedScheme; - - workspace.show_toast( - Toast::new( - NotificationId::unique::(), - format!( - "zed:// links will now open in {}.", - ReleaseChannel::global(cx).display_name() - ), - ), - cx, - ) - })?; - Ok(()) - }) - .detach_and_prompt_err( - "Error registering zed:// scheme", - window, - cx, - |_, _, _| None, - ); - }) - .register_action(open_project_settings_file) - .register_action(open_project_tasks_file) - .register_action(open_project_debug_tasks_file) - .register_action( - |workspace: &mut Workspace, - _: &zed_actions::project_panel::ToggleFocus, - window: &mut Window, - cx: &mut Context| { - workspace.toggle_panel_focus::(window, cx); - }, - ) - .register_action( - |workspace: &mut Workspace, - _: &outline_panel::ToggleFocus, - window: &mut Window, - cx: &mut Context| { - workspace.toggle_panel_focus::(window, cx); - }, - ) - .register_action( - |workspace: &mut Workspace, - _: &collab_ui::collab_panel::ToggleFocus, - window: &mut Window, - cx: &mut Context| { - workspace.toggle_panel_focus::(window, cx); - }, - ) - .register_action( - |workspace: &mut Workspace, - _: &collab_ui::notification_panel::ToggleFocus, - window: &mut Window, - cx: &mut Context| { - workspace.toggle_panel_focus::( - window, cx, - ); - }, - ) - .register_action( - |workspace: &mut Workspace, - _: &terminal_panel::ToggleFocus, - window: &mut Window, - cx: &mut Context| { - workspace.toggle_panel_focus::(window, cx); - }, - ) - .register_action({ - let app_state = Arc::downgrade(&app_state); - move |_, _: &NewWindow, _, cx| { - if let Some(app_state) = app_state.upgrade() { - open_new( - Default::default(), - app_state, - cx, - |workspace, window, cx| { - cx.activate(true); - Editor::new_file(workspace, &Default::default(), window, cx) - }, - ) - .detach(); - } - } - }) - .register_action({ - let app_state = Arc::downgrade(&app_state); - move |_, _: &NewFile, _, cx| { - if let Some(app_state) = app_state.upgrade() { - open_new( - Default::default(), - app_state, - cx, - |workspace, window, cx| { - Editor::new_file(workspace, &Default::default(), window, cx) - }, - ) - .detach(); - } - } - }) - .register_action(|workspace, _: &CaptureRecentAudio, window, cx| { - capture_recent_audio(workspace, window, cx); - }); - - #[cfg(not(target_os = "windows"))] - workspace.register_action(install_cli); - - if workspace.project().read(cx).is_via_remote_server() { - workspace.register_action({ - move |workspace, _: &OpenServerSettings, window, cx| { - let open_server_settings = workspace - .project() - .update(cx, |project, cx| project.open_server_settings(cx)); - - cx.spawn_in(window, async move |workspace, cx| { - let buffer = open_server_settings.await?; - - workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_path( - buffer - .read(cx) - .project_path(cx) - .expect("Settings file must have a location"), - None, - true, - window, - cx, - ) - })? - .await?; - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - }); - } -} - -fn initialize_pane( - workspace: &Workspace, - pane: &Entity, - window: &mut Window, - cx: &mut Context, -) { - pane.update(cx, |pane, cx| { - pane.toolbar().update(cx, |toolbar, cx| { - let multibuffer_hint = cx.new(|_| MultibufferHint::new()); - toolbar.add_item(multibuffer_hint, window, cx); - let breadcrumbs = cx.new(|_| Breadcrumbs::new()); - toolbar.add_item(breadcrumbs, window, cx); - let buffer_search_bar = cx.new(|cx| { - search::BufferSearchBar::new( - Some(workspace.project().read(cx).languages().clone()), - window, - cx, - ) - }); - toolbar.add_item(buffer_search_bar.clone(), window, cx); - let quick_action_bar = - cx.new(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx)); - toolbar.add_item(quick_action_bar, window, cx); - let diagnostic_editor_controls = cx.new(|_| diagnostics::ToolbarControls::new()); - toolbar.add_item(diagnostic_editor_controls, window, cx); - let project_search_bar = cx.new(|_| ProjectSearchBar::new()); - toolbar.add_item(project_search_bar, window, cx); - let lsp_log_item = cx.new(|_| LspLogToolbarItemView::new()); - toolbar.add_item(lsp_log_item, window, cx); - let dap_log_item = cx.new(|_| debugger_tools::DapLogToolbarItemView::new()); - toolbar.add_item(dap_log_item, window, cx); - let acp_tools_item = cx.new(|_| acp_tools::AcpToolsToolbarItemView::new()); - toolbar.add_item(acp_tools_item, window, cx); - let syntax_tree_item = cx.new(|_| language_tools::SyntaxTreeToolbarItemView::new()); - toolbar.add_item(syntax_tree_item, window, cx); - let migration_banner = cx.new(|cx| MigrationBanner::new(workspace, cx)); - toolbar.add_item(migration_banner, window, cx); - let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx)); - toolbar.add_item(project_diff_toolbar, window, cx); - let commit_view_toolbar = cx.new(|_| CommitViewToolbar::new()); - toolbar.add_item(commit_view_toolbar, window, cx); - let agent_diff_toolbar = cx.new(AgentDiffToolbar::new); - toolbar.add_item(agent_diff_toolbar, window, cx); - let basedpyright_banner = cx.new(|cx| BasedPyrightBanner::new(workspace, cx)); - toolbar.add_item(basedpyright_banner, window, cx); - }) - }); -} - -fn about(_: &mut Workspace, window: &mut Window, cx: &mut Context) { - use std::fmt::Write; - let release_channel = ReleaseChannel::global(cx).display_name(); - let full_version = AppVersion::global(cx); - let version = env!("CARGO_PKG_VERSION"); - let debug = if cfg!(debug_assertions) { - "(debug)" - } else { - "" - }; - let message = format!("{release_channel} {version} {debug}"); - - let mut detail = AppCommitSha::try_global(cx) - .map(|sha| sha.full()) - .unwrap_or_default(); - if !detail.is_empty() { - detail.push('\n'); - } - _ = write!(&mut detail, "\n{full_version}"); - - let detail = Some(detail); - - let prompt = window.prompt( - PromptLevel::Info, - &message, - detail.as_deref(), - &["Copy", "OK"], - cx, - ); - cx.spawn(async move |_, cx| { - if let Ok(0) = prompt.await { - let content = format!("{}\n{}", message, detail.as_deref().unwrap_or("")); - cx.update(|cx| { - cx.write_to_clipboard(gpui::ClipboardItem::new_string(content)); - }) - .ok(); - } - }) - .detach(); -} - -#[cfg(not(target_os = "windows"))] -fn install_cli( - _: &mut Workspace, - _: &install_cli::InstallCliBinary, - window: &mut Window, - cx: &mut Context, -) { - install_cli::install_cli_binary(window, cx) -} - -static WAITING_QUIT_CONFIRMATION: AtomicBool = AtomicBool::new(false); -fn quit(_: &Quit, cx: &mut App) { - if WAITING_QUIT_CONFIRMATION.load(atomic::Ordering::Acquire) { - return; - } - - let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit; - cx.spawn(async move |cx| { - let mut workspace_windows = cx.update(|cx| { - cx.windows() - .into_iter() - .filter_map(|window| window.downcast::()) - .collect::>() - })?; - - // If multiple windows have unsaved changes, and need a save prompt, - // prompt in the active window before switching to a different window. - cx.update(|cx| { - workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false)); - }) - .log_err(); - - if should_confirm && let Some(workspace) = workspace_windows.first() { - let answer = workspace - .update(cx, |_, window, cx| { - window.prompt( - PromptLevel::Info, - "Are you sure you want to quit?", - None, - &["Quit", "Cancel"], - cx, - ) - }) - .log_err(); - - if let Some(answer) = answer { - WAITING_QUIT_CONFIRMATION.store(true, atomic::Ordering::Release); - let answer = answer.await.ok(); - WAITING_QUIT_CONFIRMATION.store(false, atomic::Ordering::Release); - if answer != Some(0) { - return Ok(()); - } - } - } - - // If the user cancels any save prompt, then keep the app open. - for window in workspace_windows { - if let Some(should_close) = window - .update(cx, |workspace, window, cx| { - workspace.prepare_to_close(CloseIntent::Quit, window, cx) - }) - .log_err() - && !should_close.await? - { - return Ok(()); - } - } - cx.update(|cx| cx.quit())?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); -} - -fn open_log_file(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { - const MAX_LINES: usize = 1000; - workspace - .with_local_workspace(window, cx, move |workspace, window, cx| { - let app_state = workspace.app_state(); - let languages = app_state.languages.clone(); - let fs = app_state.fs.clone(); - cx.spawn_in(window, async move |workspace, cx| { - let (old_log, new_log, log_language) = futures::join!( - fs.load(paths::old_log_file()), - fs.load(paths::log_file()), - languages.language_for_name("log") - ); - let log = match (old_log, new_log) { - (Err(_), Err(_)) => None, - (old_log, new_log) => { - let mut lines = VecDeque::with_capacity(MAX_LINES); - for line in old_log - .iter() - .flat_map(|log| log.lines()) - .chain(new_log.iter().flat_map(|log| log.lines())) - { - if lines.len() == MAX_LINES { - lines.pop_front(); - } - lines.push_back(line); - } - Some( - lines - .into_iter() - .flat_map(|line| [line, "\n"]) - .collect::(), - ) - } - }; - let log_language = log_language.ok(); - - workspace - .update_in(cx, |workspace, window, cx| { - let Some(log) = log else { - struct OpenLogError; - - workspace.show_notification( - NotificationId::unique::(), - cx, - |cx| { - cx.new(|cx| { - MessageNotification::new( - format!( - "Unable to access/open log file at path {:?}", - paths::log_file().as_path() - ), - cx, - ) - }) - }, - ); - return; - }; - let project = workspace.project().clone(); - let buffer = project.update(cx, |project, cx| { - project.create_local_buffer(&log, log_language, false, cx) - }); - - let buffer = cx - .new(|cx| MultiBuffer::singleton(buffer, cx).with_title("Log".into())); - let editor = cx.new(|cx| { - let mut editor = - Editor::for_multibuffer(buffer, Some(project), window, cx); - editor.set_read_only(true); - editor.set_breadcrumb_header(format!( - "Last {} lines in {}", - MAX_LINES, - paths::log_file().display() - )); - editor - }); - - editor.update(cx, |editor, cx| { - let last_multi_buffer_offset = editor.buffer().read(cx).len(cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges(Some( - last_multi_buffer_offset..last_multi_buffer_offset, - )); - }) - }); - - workspace.add_item_to_active_pane(Box::new(editor), None, true, window, cx); - }) - .log_err(); - }) - .detach(); - }) - .detach(); -} - -fn notify_settings_errors(result: settings::SettingsParseResult, is_user: bool, cx: &mut App) { - if let settings::ParseStatus::Failed { error: err } = &result.parse_status { - let settings_type = if is_user { "user" } else { "global" }; - log::error!("Failed to load {} settings: {err}", settings_type); - } - - let error = match result.parse_status { - settings::ParseStatus::Failed { error } => Some(anyhow::format_err!(error)), - settings::ParseStatus::Success => None, - }; - let id = NotificationId::Named(format!("failed-to-parse-settings-{is_user}").into()); - - let showed_parse_error = match error { - Some(error) => { - if let Some(InvalidSettingsError::LocalSettings { .. }) = - error.downcast_ref::() - { - false - // Local settings errors are displayed by the projects - } else { - show_app_notification(id, cx, move |cx| { - cx.new(|cx| { - MessageNotification::new(format!("Invalid user settings file\n{error}"), cx) - .primary_message("Open Settings File") - .primary_icon(IconName::Settings) - .primary_on_click(|window, cx| { - window.dispatch_action( - zed_actions::OpenSettingsFile.boxed_clone(), - cx, - ); - cx.emit(DismissEvent); - }) - }) - }); - true - } - } - None => { - dismiss_app_notification(&id, cx); - false - } - }; - let id = NotificationId::Named(format!("failed-to-migrate-settings-{is_user}").into()); - - match result.migration_status { - settings::MigrationStatus::Succeeded | settings::MigrationStatus::NotNeeded => { - dismiss_app_notification(&id, cx); - } - settings::MigrationStatus::Failed { error: err } => { - if !showed_parse_error { - show_app_notification(id, cx, move |cx| { - cx.new(|cx| { - MessageNotification::new( - format!( - "Failed to migrate settings\n\ - {err}" - ), - cx, - ) - .primary_message("Open Settings File") - .primary_icon(IconName::Settings) - .primary_on_click(|window, cx| { - window.dispatch_action(zed_actions::OpenSettingsFile.boxed_clone(), cx); - cx.emit(DismissEvent); - }) - }) - }); - } - } - }; -} - -pub fn handle_settings_file_changes( - mut user_settings_file_rx: mpsc::UnboundedReceiver, - mut global_settings_file_rx: mpsc::UnboundedReceiver, - cx: &mut App, -) { - MigrationNotification::set_global(cx.new(|_| MigrationNotification), cx); - - // Initial load of both settings files - let global_content = cx - .background_executor() - .block(global_settings_file_rx.next()) - .unwrap(); - let user_content = cx - .background_executor() - .block(user_settings_file_rx.next()) - .unwrap(); - - SettingsStore::update_global(cx, |store, cx| { - notify_settings_errors(store.set_user_settings(&user_content, cx), true, cx); - notify_settings_errors(store.set_global_settings(&global_content, cx), false, cx); - }); - - // Watch for changes in both files - cx.spawn(async move |cx| { - let mut settings_streams = futures::stream::select( - global_settings_file_rx.map(Either::Left), - user_settings_file_rx.map(Either::Right), - ); - - while let Some(content) = settings_streams.next().await { - let (content, is_user) = match content { - Either::Left(content) => (content, false), - Either::Right(content) => (content, true), - }; - - let result = cx.update_global(|store: &mut SettingsStore, cx| { - let result = if is_user { - store.set_user_settings(&content, cx) - } else { - store.set_global_settings(&content, cx) - }; - let migrating_in_memory = - matches!(&result.migration_status, MigrationStatus::Succeeded); - notify_settings_errors(result, is_user, cx); - if let Some(notifier) = MigrationNotification::try_global(cx) { - notifier.update(cx, |_, cx| { - cx.emit(MigrationEvent::ContentChanged { - migration_type: MigrationType::Settings, - migrating_in_memory, - }); - }); - } - cx.refresh_windows(); - }); - - if result.is_err() { - break; // App dropped - } - } - }) - .detach(); -} - -pub fn handle_keymap_file_changes( - mut user_keymap_file_rx: mpsc::UnboundedReceiver, - cx: &mut App, -) { - let (base_keymap_tx, mut base_keymap_rx) = mpsc::unbounded(); - let (keyboard_layout_tx, mut keyboard_layout_rx) = mpsc::unbounded(); - let mut old_base_keymap = *BaseKeymap::get_global(cx); - let mut old_vim_enabled = VimModeSetting::get_global(cx).0; - let mut old_helix_enabled = vim_mode_setting::HelixModeSetting::get_global(cx).0; - - cx.observe_global::(move |cx| { - let new_base_keymap = *BaseKeymap::get_global(cx); - let new_vim_enabled = VimModeSetting::get_global(cx).0; - let new_helix_enabled = vim_mode_setting::HelixModeSetting::get_global(cx).0; - - if new_base_keymap != old_base_keymap - || new_vim_enabled != old_vim_enabled - || new_helix_enabled != old_helix_enabled - { - old_base_keymap = new_base_keymap; - old_vim_enabled = new_vim_enabled; - old_helix_enabled = new_helix_enabled; - - base_keymap_tx.unbounded_send(()).unwrap(); - } - }) - .detach(); - - #[cfg(target_os = "windows")] - { - let mut current_layout_id = cx.keyboard_layout().id().to_string(); - cx.on_keyboard_layout_change(move |cx| { - let next_layout_id = cx.keyboard_layout().id(); - if next_layout_id != current_layout_id { - current_layout_id = next_layout_id.to_string(); - keyboard_layout_tx.unbounded_send(()).ok(); - } - }) - .detach(); - } - - #[cfg(not(target_os = "windows"))] - { - let mut current_mapping = cx.keyboard_mapper().get_key_equivalents().cloned(); - cx.on_keyboard_layout_change(move |cx| { - let next_mapping = cx.keyboard_mapper().get_key_equivalents(); - if current_mapping.as_ref() != next_mapping { - current_mapping = next_mapping.cloned(); - keyboard_layout_tx.unbounded_send(()).ok(); - } - }) - .detach(); - } - - load_default_keymap(cx); - - struct KeymapParseErrorNotification; - let notification_id = NotificationId::unique::(); - - cx.spawn(async move |cx| { - let mut user_keymap_content = String::new(); - let mut migrating_in_memory = false; - loop { - select_biased! { - _ = base_keymap_rx.next() => {}, - _ = keyboard_layout_rx.next() => {}, - content = user_keymap_file_rx.next() => { - if let Some(content) = content { - if let Ok(Some(migrated_content)) = migrate_keymap(&content) { - user_keymap_content = migrated_content; - migrating_in_memory = true; - } else { - user_keymap_content = content; - migrating_in_memory = false; - } - } - } - }; - cx.update(|cx| { - if let Some(notifier) = MigrationNotification::try_global(cx) { - notifier.update(cx, |_, cx| { - cx.emit(MigrationEvent::ContentChanged { - migration_type: MigrationType::Keymap, - migrating_in_memory, - }); - }); - } - let load_result = KeymapFile::load(&user_keymap_content, cx); - match load_result { - KeymapFileLoadResult::Success { key_bindings } => { - reload_keymaps(cx, key_bindings); - dismiss_app_notification(¬ification_id.clone(), cx); - } - KeymapFileLoadResult::SomeFailedToLoad { - key_bindings, - error_message, - } => { - if !key_bindings.is_empty() { - reload_keymaps(cx, key_bindings); - } - show_keymap_file_load_error(notification_id.clone(), error_message, cx); - } - KeymapFileLoadResult::JsonParseFailure { error } => { - show_keymap_file_json_error(notification_id.clone(), &error, cx) - } - } - }) - .ok(); - } - }) - .detach(); -} - -fn show_keymap_file_json_error( - notification_id: NotificationId, - error: &anyhow::Error, - cx: &mut App, -) { - let message: SharedString = - format!("JSON parse error in keymap file. Bindings not reloaded.\n\n{error}").into(); - show_app_notification(notification_id, cx, move |cx| { - cx.new(|cx| { - MessageNotification::new(message.clone(), cx) - .primary_message("Open Keymap File") - .primary_on_click(|window, cx| { - window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx); - cx.emit(DismissEvent); - }) - }) - }); -} - -fn show_keymap_file_load_error( - notification_id: NotificationId, - error_message: MarkdownString, - cx: &mut App, -) { - show_markdown_app_notification( - notification_id, - error_message, - "Open Keymap File".into(), - |window, cx| { - window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx); - cx.emit(DismissEvent); - }, - cx, - ) -} - -fn show_markdown_app_notification( - notification_id: NotificationId, - message: MarkdownString, - primary_button_message: SharedString, - primary_button_on_click: F, - cx: &mut App, -) where - F: 'static + Send + Sync + Fn(&mut Window, &mut Context), -{ - let parsed_markdown = cx.background_spawn(async move { - let file_location_directory = None; - let language_registry = None; - markdown_preview::markdown_parser::parse_markdown( - &message.0, - file_location_directory, - language_registry, - ) - .await - }); - - cx.spawn(async move |cx| { - let parsed_markdown = Arc::new(parsed_markdown.await); - let primary_button_message = primary_button_message.clone(); - let primary_button_on_click = Arc::new(primary_button_on_click); - cx.update(|cx| { - show_app_notification(notification_id, cx, move |cx| { - let workspace_handle = cx.entity().downgrade(); - let parsed_markdown = parsed_markdown.clone(); - let primary_button_message = primary_button_message.clone(); - let primary_button_on_click = primary_button_on_click.clone(); - cx.new(move |cx| { - MessageNotification::new_from_builder(cx, move |window, cx| { - image_cache(retain_all("notification-cache")) - .text_xs() - .child(markdown_preview::markdown_renderer::render_parsed_markdown( - &parsed_markdown.clone(), - Some(workspace_handle.clone()), - window, - cx, - )) - .into_any() - }) - .primary_message(primary_button_message) - .primary_on_click_arc(primary_button_on_click) - }) - }) - }) - .ok(); - }) - .detach(); -} - -fn reload_keymaps(cx: &mut App, mut user_key_bindings: Vec) { - cx.clear_key_bindings(); - load_default_keymap(cx); - - for key_binding in &mut user_key_bindings { - key_binding.set_meta(KeybindSource::User.meta()); - } - cx.bind_keys(user_key_bindings); - - let menus = app_menus(cx); - cx.set_menus(menus); - // On Windows, this is set in the `update_jump_list` method of the `HistoryManager`. - #[cfg(not(target_os = "windows"))] - cx.set_dock_menu(vec![gpui::MenuItem::action( - "New Window", - workspace::NewWindow, - )]); - // todo: nicer api here? - keymap_editor::KeymapEventChannel::trigger_keymap_changed(cx); -} - -pub fn load_default_keymap(cx: &mut App) { - let base_keymap = *BaseKeymap::get_global(cx); - if base_keymap == BaseKeymap::None { - return; - } - - cx.bind_keys( - KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, Some(KeybindSource::Default), cx).unwrap(), - ); - - if let Some(asset_path) = base_keymap.asset_path() { - cx.bind_keys(KeymapFile::load_asset(asset_path, Some(KeybindSource::Base), cx).unwrap()); - } - - if VimModeSetting::get_global(cx).0 || vim_mode_setting::HelixModeSetting::get_global(cx).0 { - cx.bind_keys( - KeymapFile::load_asset(VIM_KEYMAP_PATH, Some(KeybindSource::Vim), cx).unwrap(), - ); - } -} - -pub fn open_new_ssh_project_from_project( - workspace: &mut Workspace, - paths: Vec, - window: &mut Window, - cx: &mut Context, -) -> Task> { - let app_state = workspace.app_state().clone(); - let Some(ssh_client) = workspace.project().read(cx).remote_client() else { - return Task::ready(Err(anyhow::anyhow!("Not an ssh project"))); - }; - let connection_options = ssh_client.read(cx).connection_options(); - cx.spawn_in(window, async move |_, cx| { - open_remote_project( - connection_options, - paths, - app_state, - workspace::OpenOptions { - open_new_workspace: Some(true), - ..Default::default() - }, - cx, - ) - .await - }) -} - -fn open_project_settings_file( - workspace: &mut Workspace, - _: &OpenProjectSettingsFile, - window: &mut Window, - cx: &mut Context, -) { - open_local_file( - workspace, - local_settings_file_relative_path(), - initial_project_settings_content(), - window, - cx, - ) -} - -fn open_project_tasks_file( - workspace: &mut Workspace, - _: &OpenProjectTasks, - window: &mut Window, - cx: &mut Context, -) { - open_local_file( - workspace, - local_tasks_file_relative_path(), - initial_tasks_content(), - window, - cx, - ) -} - -fn open_project_debug_tasks_file( - workspace: &mut Workspace, - _: &zed_actions::OpenProjectDebugTasks, - window: &mut Window, - cx: &mut Context, -) { - open_local_file( - workspace, - local_debug_file_relative_path(), - initial_local_debug_tasks_content(), - window, - cx, - ) -} - -fn open_local_file( - workspace: &mut Workspace, - settings_relative_path: &'static RelPath, - initial_contents: Cow<'static, str>, - window: &mut Window, - cx: &mut Context, -) { - let project = workspace.project().clone(); - let worktree = project - .read(cx) - .visible_worktrees(cx) - .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree)); - if let Some(worktree) = worktree { - let tree_id = worktree.read(cx).id(); - cx.spawn_in(window, async move |workspace, cx| { - // Check if the file actually exists on disk (even if it's excluded from worktree) - let file_exists = { - let full_path = worktree.read_with(cx, |tree, _| { - tree.abs_path().join(settings_relative_path.as_std_path()) - })?; - - let fs = project.read_with(cx, |project, _| project.fs().clone())?; - - fs.metadata(&full_path) - .await - .ok() - .flatten() - .is_some_and(|metadata| !metadata.is_dir && !metadata.is_fifo) - }; - - if !file_exists { - if let Some(dir_path) = settings_relative_path.parent() - && worktree.read_with(cx, |tree, _| tree.entry_for_path(dir_path).is_none())? - { - project - .update(cx, |project, cx| { - project.create_entry((tree_id, dir_path), true, cx) - })? - .await - .context("worktree was removed")?; - } - - if worktree.read_with(cx, |tree, _| { - tree.entry_for_path(settings_relative_path).is_none() - })? { - project - .update(cx, |project, cx| { - project.create_entry((tree_id, settings_relative_path), false, cx) - })? - .await - .context("worktree was removed")?; - } - } - - let editor = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_path((tree_id, settings_relative_path), None, true, window, cx) - })? - .await? - .downcast::() - .context("unexpected item type: expected editor item")?; - - editor - .downgrade() - .update(cx, |editor, cx| { - if let Some(buffer) = editor.buffer().read(cx).as_singleton() - && buffer.read(cx).is_empty() - { - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, initial_contents)], None, cx) - }); - } - }) - .ok(); - - anyhow::Ok(()) - }) - .detach(); - } else { - struct NoOpenFolders; - - workspace.show_notification(NotificationId::unique::(), cx, |cx| { - cx.new(|cx| MessageNotification::new("This project has no folders open.", cx)) - }) - } -} - -fn open_telemetry_log_file( - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, -) { - const HEADER: &str = concat!( - "// Zed collects anonymous usage data to help us understand how people are using the app.\n", - "// Telemetry can be disabled via the `settings.json` file.\n", - "// Here is the data that has been reported for the current session:\n", - ); - workspace - .with_local_workspace(window, cx, move |workspace, window, cx| { - let app_state = workspace.app_state().clone(); - cx.spawn_in(window, async move |workspace, cx| { - async fn fetch_log_string(app_state: &Arc) -> Option { - let path = client::telemetry::Telemetry::log_file_path(); - app_state.fs.load(&path).await.log_err() - } - - let log = fetch_log_string(&app_state) - .await - .unwrap_or_else(|| "// No data has been collected yet".to_string()); - - const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024; - let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN); - if let Some(newline_offset) = log[start_offset..].find('\n') { - start_offset += newline_offset + 1; - } - let log_suffix = &log[start_offset..]; - let content = format!("{}\n{}", HEADER, log_suffix); - let json = app_state - .languages - .language_for_name("JSON") - .await - .log_err(); - - workspace - .update_in(cx, |workspace, window, cx| { - let project = workspace.project().clone(); - let buffer = project.update(cx, |project, cx| { - project.create_local_buffer(&content, json, false, cx) - }); - let buffer = cx.new(|cx| { - MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into()) - }); - workspace.add_item_to_active_pane( - Box::new(cx.new(|cx| { - let mut editor = - Editor::for_multibuffer(buffer, Some(project), window, cx); - editor.set_read_only(true); - editor.set_breadcrumb_header("Telemetry Log".into()); - editor - })), - None, - true, - window, - cx, - ); - }) - .log_err()?; - - Some(()) - }) - .detach(); - }) - .detach(); -} - -fn open_bundled_file( - workspace: &Workspace, - text: Cow<'static, str>, - title: &'static str, - language: &'static str, - window: &mut Window, - cx: &mut Context, -) { - let language = workspace.app_state().languages.language_for_name(language); - cx.spawn_in(window, async move |workspace, cx| { - let language = language.await.log_err(); - workspace - .update_in(cx, |workspace, window, cx| { - workspace.with_local_workspace(window, cx, |workspace, window, cx| { - let project = workspace.project(); - let buffer = project.update(cx, move |project, cx| { - let buffer = - project.create_local_buffer(text.as_ref(), language, false, cx); - buffer.update(cx, |buffer, cx| { - buffer.set_capability(Capability::ReadOnly, cx); - }); - buffer - }); - let buffer = - cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into())); - workspace.add_item_to_active_pane( - Box::new(cx.new(|cx| { - let mut editor = - Editor::for_multibuffer(buffer, Some(project.clone()), window, cx); - editor.set_read_only(true); - editor.set_should_serialize(false, cx); - editor.set_breadcrumb_header(title.into()); - editor - })), - None, - true, - window, - cx, - ); - }) - })? - .await - }) - .detach_and_log_err(cx); -} - -fn open_settings_file( - abs_path: &'static Path, - default_content: impl FnOnce() -> Rope + Send + 'static, - window: &mut Window, - cx: &mut Context, -) { - cx.spawn_in(window, async move |workspace, cx| { - let (worktree_creation_task, settings_open_task) = workspace - .update_in(cx, |workspace, window, cx| { - workspace.with_local_workspace(window, cx, move |workspace, window, cx| { - let worktree_creation_task = workspace.project().update(cx, |project, cx| { - // Set up a dedicated worktree for settings, since - // otherwise we're dropping and re-starting LSP servers - // for each file inside on every settings file - // close/open - - // TODO: Do note that all other external files (e.g. - // drag and drop from OS) still have their worktrees - // released on file close, causing LSP servers' - // restarts. - project.find_or_create_worktree(paths::config_dir().as_path(), false, cx) - }); - let settings_open_task = - create_and_open_local_file(abs_path, window, cx, default_content); - (worktree_creation_task, settings_open_task) - }) - })? - .await?; - let _ = worktree_creation_task.await?; - let _ = settings_open_task.await?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); -} - -fn capture_recent_audio(workspace: &mut Workspace, _: &mut Window, cx: &mut Context) { - struct CaptureRecentAudioNotification { - focus_handle: gpui::FocusHandle, - save_result: Option>, - _save_task: Task>, - } - - impl gpui::EventEmitter for CaptureRecentAudioNotification {} - impl gpui::EventEmitter for CaptureRecentAudioNotification {} - impl gpui::Focusable for CaptureRecentAudioNotification { - fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } - } - impl workspace::notifications::Notification for CaptureRecentAudioNotification {} - - impl Render for CaptureRecentAudioNotification { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let message = match &self.save_result { - None => format!( - "Saving up to {} seconds of recent audio", - REPLAY_DURATION.as_secs(), - ), - Some(Ok((path, duration))) => format!( - "Saved {} seconds of all audio to {}", - duration.as_secs(), - path.display(), - ), - Some(Err(e)) => format!("Error saving audio replays: {e:?}"), - }; - - NotificationFrame::new() - .with_title(Some("Saved Audio")) - .show_suppress_button(false) - .on_close(cx.listener(|_, _, _, cx| { - cx.emit(DismissEvent); - })) - .with_content(message) - } - } - - impl CaptureRecentAudioNotification { - fn new(cx: &mut Context) -> Self { - if AudioSettings::get_global(cx).rodio_audio { - let executor = cx.background_executor().clone(); - let save_task = cx.default_global::().save_replays(executor); - let _save_task = cx.spawn(async move |this, cx| { - let res = save_task.await; - this.update(cx, |this, cx| { - this.save_result = Some(res); - cx.notify(); - }) - }); - - Self { - focus_handle: cx.focus_handle(), - _save_task, - save_result: None, - } - } else { - Self { - focus_handle: cx.focus_handle(), - _save_task: Task::ready(Ok(())), - save_result: Some(Err(anyhow::anyhow!( - "Capturing recent audio is only supported on the experimental rodio audio pipeline" - ))), - } - } - } - } - - workspace.show_notification( - NotificationId::unique::(), - cx, - |cx| cx.new(CaptureRecentAudioNotification::new), - ); -} - -/// Eagerly loads the active theme and icon theme based on the selections in the -/// theme settings. -/// -/// This fast path exists to load these themes as soon as possible so the user -/// doesn't see the default themes while waiting on extensions to load. -pub(crate) fn eager_load_active_theme_and_icon_theme(fs: Arc, cx: &mut App) { - let extension_store = ExtensionStore::global(cx); - let theme_registry = ThemeRegistry::global(cx); - let theme_settings = ThemeSettings::get_global(cx); - let appearance = SystemAppearance::global(cx).0; - - enum LoadTarget { - Theme(PathBuf), - IconTheme((PathBuf, PathBuf)), - } - - let theme_name = theme_settings.theme.name(appearance); - let icon_theme_name = theme_settings.icon_theme.name(appearance); - let themes_to_load = [ - theme_registry - .get(&theme_name.0) - .is_err() - .then(|| { - extension_store - .read(cx) - .path_to_extension_theme(&theme_name.0) - }) - .flatten() - .map(LoadTarget::Theme), - theme_registry - .get_icon_theme(&icon_theme_name.0) - .is_err() - .then(|| { - extension_store - .read(cx) - .path_to_extension_icon_theme(&icon_theme_name.0) - }) - .flatten() - .map(LoadTarget::IconTheme), - ]; - - enum ReloadTarget { - Theme, - IconTheme, - } - - let executor = cx.background_executor(); - let reload_tasks = parking_lot::Mutex::new(Vec::with_capacity(themes_to_load.len())); - - let mut themes_to_load = themes_to_load.into_iter().flatten().peekable(); - - if themes_to_load.peek().is_none() { - return; - } - - executor.block(executor.scoped(|scope| { - for load_target in themes_to_load { - let theme_registry = &theme_registry; - let reload_tasks = &reload_tasks; - let fs = fs.clone(); - - scope.spawn(async { - match load_target { - LoadTarget::Theme(theme_path) => { - if theme_registry - .load_user_theme(&theme_path, fs) - .await - .log_err() - .is_some() - { - reload_tasks.lock().push(ReloadTarget::Theme); - } - } - LoadTarget::IconTheme((icon_theme_path, icons_root_path)) => { - if theme_registry - .load_icon_theme(&icon_theme_path, &icons_root_path, fs) - .await - .log_err() - .is_some() - { - reload_tasks.lock().push(ReloadTarget::IconTheme); - } - } - } - }); - } - })); - - for reload_target in reload_tasks.into_inner() { - match reload_target { - ReloadTarget::Theme => GlobalTheme::reload_theme(cx), - ReloadTarget::IconTheme => GlobalTheme::reload_icon_theme(cx), - }; - } -} - -#[cfg(test)] -mod tests { - use super::*; - use assets::Assets; - use collections::HashSet; - use editor::{ - DisplayPoint, Editor, MultiBufferOffset, SelectionEffects, display_map::DisplayRow, - }; - use gpui::{ - Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, TestAppContext, UpdateGlobal, - VisualTestContext, WindowHandle, actions, - }; - use language::LanguageRegistry; - use languages::{markdown_lang, rust_lang}; - use pretty_assertions::{assert_eq, assert_ne}; - use project::{Project, ProjectPath}; - use semver::Version; - use serde_json::json; - use settings::{SettingsStore, watch_config_file}; - use std::{ - path::{Path, PathBuf}, - time::Duration, - }; - use theme::ThemeRegistry; - use util::{ - path, - rel_path::{RelPath, rel_path}, - }; - use workspace::{ - NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection, - WorkspaceHandle, - item::SaveOptions, - item::{Item, ItemHandle}, - open_new, open_paths, pane, - }; - - #[gpui::test] - async fn test_open_non_existing_file(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "a": { - }, - }), - ) - .await; - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/a/new"))], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.read(|cx| cx.windows().len()), 1); - - let workspace = cx.windows()[0].downcast::().unwrap(); - workspace - .update(cx, |workspace, _, cx| { - assert!(workspace.active_item_as::(cx).is_some()) - }) - .unwrap(); - } - - #[gpui::test] - async fn test_open_paths_action(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - "/root", - json!({ - "a": { - "aa": null, - "ab": null, - }, - "b": { - "ba": null, - "bb": null, - }, - "c": { - "ca": null, - "cb": null, - }, - "d": { - "da": null, - "db": null, - }, - "e": { - "ea": null, - "eb": null, - } - }), - ) - .await; - - cx.update(|cx| { - open_paths( - &[PathBuf::from("/root/a"), PathBuf::from("/root/b")], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.read(|cx| cx.windows().len()), 1); - - cx.update(|cx| { - open_paths( - &[PathBuf::from("/root/a")], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.read(|cx| cx.windows().len()), 1); - let workspace_1 = cx - .read(|cx| cx.windows()[0].downcast::()) - .unwrap(); - cx.run_until_parked(); - workspace_1 - .update(cx, |workspace, window, cx| { - assert_eq!(workspace.worktrees(cx).count(), 2); - assert!(workspace.left_dock().read(cx).is_open()); - assert!( - workspace - .active_pane() - .read(cx) - .focus_handle(cx) - .is_focused(window) - ); - }) - .unwrap(); - - cx.update(|cx| { - open_paths( - &[PathBuf::from("/root/c"), PathBuf::from("/root/d")], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.read(|cx| cx.windows().len()), 2); - - // Replace existing windows - let window = cx - .update(|cx| cx.windows()[0].downcast::()) - .unwrap(); - cx.update(|cx| { - open_paths( - &[PathBuf::from("/root/e")], - app_state, - workspace::OpenOptions { - replace_window: Some(window), - ..Default::default() - }, - cx, - ) - }) - .await - .unwrap(); - cx.background_executor.run_until_parked(); - assert_eq!(cx.read(|cx| cx.windows().len()), 2); - let workspace_1 = cx - .update(|cx| cx.windows()[0].downcast::()) - .unwrap(); - workspace_1 - .update(cx, |workspace, window, cx| { - assert_eq!( - workspace - .worktrees(cx) - .map(|w| w.read(cx).abs_path()) - .collect::>(), - &[Path::new("/root/e").into()] - ); - assert!(workspace.left_dock().read(cx).is_open()); - assert!(workspace.active_pane().focus_handle(cx).is_focused(window)); - }) - .unwrap(); - } - - #[gpui::test] - async fn test_open_add_new(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({"a": "hey", "b": "", "dir": {"c": "f"}}), - ) - .await; - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/dir"))], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 1); - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/a"))], - app_state.clone(), - workspace::OpenOptions { - open_new_workspace: Some(false), - ..Default::default() - }, - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 1); - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/dir/c"))], - app_state.clone(), - workspace::OpenOptions { - open_new_workspace: Some(true), - ..Default::default() - }, - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 2); - } - - #[gpui::test] - async fn test_open_file_in_many_spaces(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}}), - ) - .await; - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/dir1/a"))], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 1); - let window1 = cx.update(|cx| cx.active_window().unwrap()); - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/dir2/c"))], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 1); - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/dir2"))], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 2); - let window2 = cx.update(|cx| cx.active_window().unwrap()); - assert!(window1 != window2); - cx.update_window(window1, |_, window, _| window.activate_window()) - .unwrap(); - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/dir2/c"))], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 2); - // should have opened in window2 because that has dir2 visibly open (window1 has it open, but not in the project panel) - assert!(cx.update(|cx| cx.active_window().unwrap()) == window2); - } - - #[gpui::test] - async fn test_window_edit_state_restoring_disabled(cx: &mut TestAppContext) { - let executor = cx.executor(); - let app_state = init_test(cx); - - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings - .session - .get_or_insert_default() - .restore_unsaved_buffers = Some(false) - }); - }); - }); - - app_state - .fs - .as_fake() - .insert_tree(path!("/root"), json!({"a": "hey"})) - .await; - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/a"))], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 1); - - // When opening the workspace, the window is not in a edited state. - let window = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); - - let window_is_edited = |window: WindowHandle, cx: &mut TestAppContext| { - cx.update(|cx| window.read(cx).unwrap().is_edited()) - }; - let pane = window - .read_with(cx, |workspace, _| workspace.active_pane().clone()) - .unwrap(); - let editor = window - .read_with(cx, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }) - .unwrap(); - - assert!(!window_is_edited(window, cx)); - - // Editing a buffer marks the window as edited. - window - .update(cx, |_, window, cx| { - editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx)); - }) - .unwrap(); - - assert!(window_is_edited(window, cx)); - - // Undoing the edit restores the window's edited state. - window - .update(cx, |_, window, cx| { - editor.update(cx, |editor, cx| { - editor.undo(&Default::default(), window, cx) - }); - }) - .unwrap(); - assert!(!window_is_edited(window, cx)); - - // Redoing the edit marks the window as edited again. - window - .update(cx, |_, window, cx| { - editor.update(cx, |editor, cx| { - editor.redo(&Default::default(), window, cx) - }); - }) - .unwrap(); - assert!(window_is_edited(window, cx)); - let weak = editor.downgrade(); - - // Closing the item restores the window's edited state. - let close = window - .update(cx, |_, window, cx| { - pane.update(cx, |pane, cx| { - drop(editor); - pane.close_active_item(&Default::default(), window, cx) - }) - }) - .unwrap(); - executor.run_until_parked(); - - cx.simulate_prompt_answer("Don't Save"); - close.await.unwrap(); - - // Advance the clock to ensure that the item has been serialized and dropped from the queue - cx.executor().advance_clock(Duration::from_secs(1)); - - weak.assert_released(); - assert!(!window_is_edited(window, cx)); - // Opening the buffer again doesn't impact the window's edited state. - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/a"))], - app_state, - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - executor.run_until_parked(); - - window - .update(cx, |workspace, _, cx| { - let editor = workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap(); - - editor.update(cx, |editor, cx| { - assert_eq!(editor.text(cx), "hey"); - }); - }) - .unwrap(); - - let editor = window - .read_with(cx, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }) - .unwrap(); - assert!(!window_is_edited(window, cx)); - - // Editing the buffer marks the window as edited. - window - .update(cx, |_, window, cx| { - editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx)); - }) - .unwrap(); - executor.run_until_parked(); - assert!(window_is_edited(window, cx)); - - // Ensure closing the window via the mouse gets preempted due to the - // buffer having unsaved changes. - assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close()); - executor.run_until_parked(); - assert_eq!(cx.update(|cx| cx.windows().len()), 1); - - // The window is successfully closed after the user dismisses the prompt. - cx.simulate_prompt_answer("Don't Save"); - executor.run_until_parked(); - assert_eq!(cx.update(|cx| cx.windows().len()), 0); - } - - #[gpui::test] - async fn test_window_edit_state_restoring_enabled(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree(path!("/root"), json!({"a": "hey"})) - .await; - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/a"))], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - - assert_eq!(cx.update(|cx| cx.windows().len()), 1); - - // When opening the workspace, the window is not in a edited state. - let window = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); - - let window_is_edited = |window: WindowHandle, cx: &mut TestAppContext| { - cx.update(|cx| window.read(cx).unwrap().is_edited()) - }; - - let editor = window - .read_with(cx, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }) - .unwrap(); - - assert!(!window_is_edited(window, cx)); - - // Editing a buffer marks the window as edited. - window - .update(cx, |_, window, cx| { - editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx)); - }) - .unwrap(); - - assert!(window_is_edited(window, cx)); - cx.run_until_parked(); - - // Advance the clock to make sure the workspace is serialized - cx.executor().advance_clock(Duration::from_secs(1)); - - // When closing the window, no prompt shows up and the window is closed. - // buffer having unsaved changes. - assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close()); - cx.run_until_parked(); - assert_eq!(cx.update(|cx| cx.windows().len()), 0); - - // When we now reopen the window, the edited state and the edited buffer are back - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/a"))], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - - assert_eq!(cx.update(|cx| cx.windows().len()), 1); - assert!(cx.update(|cx| cx.active_window().is_some())); - - cx.run_until_parked(); - - // When opening the workspace, the window is not in a edited state. - let window = cx.update(|cx| cx.active_window().unwrap().downcast::().unwrap()); - assert!(window_is_edited(window, cx)); - - window - .update(cx, |workspace, _, cx| { - let editor = workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap(); - editor.update(cx, |editor, cx| { - assert_eq!(editor.text(cx), "EDIThey"); - assert!(editor.is_dirty(cx)); - }); - - editor - }) - .unwrap(); - } - - #[gpui::test] - async fn test_new_empty_workspace(cx: &mut TestAppContext) { - let app_state = init_test(cx); - cx.update(|cx| { - open_new( - Default::default(), - app_state.clone(), - cx, - |workspace, window, cx| { - Editor::new_file(workspace, &Default::default(), window, cx) - }, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - - let workspace = cx - .update(|cx| cx.windows().first().unwrap().downcast::()) - .unwrap(); - - let editor = workspace - .update(cx, |workspace, _, cx| { - let editor = workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap(); - editor.update(cx, |editor, cx| { - assert!(editor.text(cx).is_empty()); - assert!(!editor.is_dirty(cx)); - }); - - editor - }) - .unwrap(); - - let save_task = workspace - .update(cx, |workspace, window, cx| { - workspace.save_active_item(SaveIntent::Save, window, cx) - }) - .unwrap(); - app_state.fs.create_dir(Path::new("/root")).await.unwrap(); - cx.background_executor.run_until_parked(); - cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name"))); - save_task.await.unwrap(); - workspace - .update(cx, |_, _, cx| { - editor.update(cx, |editor, cx| { - assert!(!editor.is_dirty(cx)); - assert_eq!(editor.title(cx), "the-new-name"); - }); - }) - .unwrap(); - } - - #[gpui::test] - async fn test_open_entry(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "a": { - "file1": "contents 1", - "file2": "contents 2", - "file3": "contents 3", - }, - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - project.update(cx, |project, _cx| project.languages().add(markdown_lang())); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); - - let entries = cx.read(|cx| workspace.file_project_paths(cx)); - let file1 = entries[0].clone(); - let file2 = entries[1].clone(); - let file3 = entries[2].clone(); - - // Open the first entry - let entry_1 = window - .update(cx, |w, window, cx| { - w.open_path(file1.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap(); - cx.read(|cx| { - let pane = workspace.read(cx).active_pane().read(cx); - assert_eq!( - pane.active_item().unwrap().project_path(cx), - Some(file1.clone()) - ); - assert_eq!(pane.items_len(), 1); - }); - - // Open the second entry - window - .update(cx, |w, window, cx| { - w.open_path(file2.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap(); - cx.read(|cx| { - let pane = workspace.read(cx).active_pane().read(cx); - assert_eq!( - pane.active_item().unwrap().project_path(cx), - Some(file2.clone()) - ); - assert_eq!(pane.items_len(), 2); - }); - - // Open the first entry again. The existing pane item is activated. - let entry_1b = window - .update(cx, |w, window, cx| { - w.open_path(file1.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(entry_1.item_id(), entry_1b.item_id()); - - cx.read(|cx| { - let pane = workspace.read(cx).active_pane().read(cx); - assert_eq!( - pane.active_item().unwrap().project_path(cx), - Some(file1.clone()) - ); - assert_eq!(pane.items_len(), 2); - }); - - // Split the pane with the first entry, then open the second entry again. - window - .update(cx, |w, window, cx| { - w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx) - }) - .unwrap() - .await - .unwrap(); - window - .update(cx, |w, window, cx| { - w.open_path(file2.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap(); - - window - .read_with(cx, |w, cx| { - assert_eq!( - w.active_pane() - .read(cx) - .active_item() - .unwrap() - .project_path(cx), - Some(file2.clone()) - ); - }) - .unwrap(); - - // Open the third entry twice concurrently. Only one pane item is added. - let (t1, t2) = window - .update(cx, |w, window, cx| { - ( - w.open_path(file3.clone(), None, true, window, cx), - w.open_path(file3.clone(), None, true, window, cx), - ) - }) - .unwrap(); - t1.await.unwrap(); - t2.await.unwrap(); - cx.read(|cx| { - let pane = workspace.read(cx).active_pane().read(cx); - assert_eq!( - pane.active_item().unwrap().project_path(cx), - Some(file3.clone()) - ); - let pane_entries = pane - .items() - .map(|i| i.project_path(cx).unwrap()) - .collect::>(); - assert_eq!(pane_entries, &[file1, file2, file3]); - }); - } - - #[gpui::test] - async fn test_open_paths(cx: &mut TestAppContext) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree( - path!("/"), - json!({ - "dir1": { - "a.txt": "" - }, - "dir2": { - "b.txt": "" - }, - "dir3": { - "c.txt": "" - }, - "d.txt": "" - }), - ) - .await; - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/dir1/"))], - app_state, - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!(cx.update(|cx| cx.windows().len()), 1); - let window = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); - let workspace = window.root(cx).unwrap(); - - #[track_caller] - fn assert_project_panel_selection( - workspace: &Workspace, - expected_worktree_path: &Path, - expected_entry_path: &RelPath, - cx: &App, - ) { - let project_panel = [ - workspace.left_dock().read(cx).panel::(), - workspace.right_dock().read(cx).panel::(), - workspace.bottom_dock().read(cx).panel::(), - ] - .into_iter() - .find_map(std::convert::identity) - .expect("found no project panels") - .read(cx); - let (selected_worktree, selected_entry) = project_panel - .selected_entry(cx) - .expect("project panel should have a selected entry"); - assert_eq!( - selected_worktree.abs_path().as_ref(), - expected_worktree_path, - "Unexpected project panel selected worktree path" - ); - assert_eq!( - selected_entry.path.as_ref(), - expected_entry_path, - "Unexpected project panel selected entry path" - ); - } - - // Open a file within an existing worktree. - window - .update(cx, |workspace, window, cx| { - workspace.open_paths( - vec![path!("/dir1/a.txt").into()], - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - None, - window, - cx, - ) - }) - .unwrap() - .await; - cx.run_until_parked(); - cx.read(|cx| { - let workspace = workspace.read(cx); - assert_project_panel_selection( - workspace, - Path::new(path!("/dir1")), - rel_path("a.txt"), - cx, - ); - assert_eq!( - workspace - .active_pane() - .read(cx) - .active_item() - .unwrap() - .act_as::(cx) - .unwrap() - .read(cx) - .title(cx), - "a.txt" - ); - }); - - // Open a file outside of any existing worktree. - window - .update(cx, |workspace, window, cx| { - workspace.open_paths( - vec![path!("/dir2/b.txt").into()], - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - None, - window, - cx, - ) - }) - .unwrap() - .await; - cx.run_until_parked(); - cx.read(|cx| { - let workspace = workspace.read(cx); - assert_project_panel_selection( - workspace, - Path::new(path!("/dir2/b.txt")), - rel_path(""), - cx, - ); - let worktree_roots = workspace - .worktrees(cx) - .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) - .collect::>(); - assert_eq!( - worktree_roots, - vec![path!("/dir1"), path!("/dir2/b.txt")] - .into_iter() - .map(Path::new) - .collect(), - ); - assert_eq!( - workspace - .active_pane() - .read(cx) - .active_item() - .unwrap() - .act_as::(cx) - .unwrap() - .read(cx) - .title(cx), - "b.txt" - ); - }); - - // Ensure opening a directory and one of its children only adds one worktree. - window - .update(cx, |workspace, window, cx| { - workspace.open_paths( - vec![path!("/dir3").into(), path!("/dir3/c.txt").into()], - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - None, - window, - cx, - ) - }) - .unwrap() - .await; - cx.run_until_parked(); - cx.read(|cx| { - let workspace = workspace.read(cx); - assert_project_panel_selection( - workspace, - Path::new(path!("/dir3")), - rel_path("c.txt"), - cx, - ); - let worktree_roots = workspace - .worktrees(cx) - .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) - .collect::>(); - assert_eq!( - worktree_roots, - vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")] - .into_iter() - .map(Path::new) - .collect(), - ); - assert_eq!( - workspace - .active_pane() - .read(cx) - .active_item() - .unwrap() - .act_as::(cx) - .unwrap() - .read(cx) - .title(cx), - "c.txt" - ); - }); - - // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree. - window - .update(cx, |workspace, window, cx| { - workspace.open_paths( - vec![path!("/d.txt").into()], - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - None, - window, - cx, - ) - }) - .unwrap() - .await; - cx.run_until_parked(); - cx.read(|cx| { - let workspace = workspace.read(cx); - assert_project_panel_selection(workspace, Path::new(path!("/d.txt")), rel_path(""), cx); - let worktree_roots = workspace - .worktrees(cx) - .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) - .collect::>(); - assert_eq!( - worktree_roots, - vec![ - path!("/dir1"), - path!("/dir2/b.txt"), - path!("/dir3"), - path!("/d.txt") - ] - .into_iter() - .map(Path::new) - .collect(), - ); - - let visible_worktree_roots = workspace - .visible_worktrees(cx) - .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) - .collect::>(); - assert_eq!( - visible_worktree_roots, - vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")] - .into_iter() - .map(Path::new) - .collect(), - ); - - assert_eq!( - workspace - .active_pane() - .read(cx) - .active_item() - .unwrap() - .act_as::(cx) - .unwrap() - .read(cx) - .title(cx), - "d.txt" - ); - }); - } - - #[gpui::test] - async fn test_opening_excluded_paths(cx: &mut TestAppContext) { - let app_state = init_test(cx); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |project_settings| { - project_settings.project.worktree.file_scan_exclusions = - Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]); - }); - }); - }); - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - ".gitignore": "ignored_dir\n", - ".git": { - "HEAD": "ref: refs/heads/main", - }, - "regular_dir": { - "file": "regular file contents", - }, - "ignored_dir": { - "ignored_subdir": { - "file": "ignored subfile contents", - }, - "file": "ignored file contents", - }, - "excluded_dir": { - "file": "excluded file contents", - "ignored_subdir": { - "file": "ignored subfile contents", - }, - }, - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - project.update(cx, |project, _cx| project.languages().add(markdown_lang())); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); - - let initial_entries = cx.read(|cx| workspace.file_project_paths(cx)); - let paths_to_open = [ - PathBuf::from(path!("/root/excluded_dir/file")), - PathBuf::from(path!("/root/.git/HEAD")), - PathBuf::from(path!("/root/excluded_dir/ignored_subdir")), - ]; - let (opened_workspace, new_items) = cx - .update(|cx| { - workspace::open_paths( - &paths_to_open, - app_state, - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - - assert_eq!( - opened_workspace.root(cx).unwrap().entity_id(), - workspace.entity_id(), - "Excluded files in subfolders of a workspace root should be opened in the workspace" - ); - let mut opened_paths = cx.read(|cx| { - assert_eq!( - new_items.len(), - paths_to_open.len(), - "Expect to get the same number of opened items as submitted paths to open" - ); - new_items - .iter() - .zip(paths_to_open.iter()) - .map(|(i, path)| { - match i { - Some(Ok(i)) => Some(i.project_path(cx).map(|p| p.path)), - Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"), - None => None, - } - .flatten() - }) - .collect::>() - }); - opened_paths.sort(); - assert_eq!( - opened_paths, - vec![ - None, - Some(rel_path(".git/HEAD").into()), - Some(rel_path("excluded_dir/file").into()), - ], - "Excluded files should get opened, excluded dir should not get opened" - ); - - let entries = cx.read(|cx| workspace.file_project_paths(cx)); - assert_eq!( - initial_entries, entries, - "Workspace entries should not change after opening excluded files and directories paths" - ); - - cx.read(|cx| { - let pane = workspace.read(cx).active_pane().read(cx); - let mut opened_buffer_paths = pane - .items() - .map(|i| { - i.project_path(cx) - .expect("all excluded files that got open should have a path") - .path - }) - .collect::>(); - opened_buffer_paths.sort(); - assert_eq!( - opened_buffer_paths, - vec![rel_path(".git/HEAD").into(), rel_path("excluded_dir/file").into()], - "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane" - ); - }); - } - - #[gpui::test] - async fn test_save_conflicting_item(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree(path!("/root"), json!({ "a.txt": "" })) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - project.update(cx, |project, _cx| project.languages().add(markdown_lang())); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); - - // Open a file within an existing worktree. - window - .update(cx, |workspace, window, cx| { - workspace.open_paths( - vec![PathBuf::from(path!("/root/a.txt"))], - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - None, - window, - cx, - ) - }) - .unwrap() - .await; - let editor = cx.read(|cx| { - let pane = workspace.read(cx).active_pane().read(cx); - let item = pane.active_item().unwrap(); - item.downcast::().unwrap() - }); - - window - .update(cx, |_, window, cx| { - editor.update(cx, |editor, cx| editor.handle_input("x", window, cx)); - }) - .unwrap(); - - app_state - .fs - .as_fake() - .insert_file(path!("/root/a.txt"), b"changed".to_vec()) - .await; - - cx.run_until_parked(); - cx.read(|cx| assert!(editor.is_dirty(cx))); - cx.read(|cx| assert!(editor.has_conflict(cx))); - - let save_task = window - .update(cx, |workspace, window, cx| { - workspace.save_active_item(SaveIntent::Save, window, cx) - }) - .unwrap(); - cx.background_executor.run_until_parked(); - cx.simulate_prompt_answer("Overwrite"); - save_task.await.unwrap(); - window - .update(cx, |_, _, cx| { - editor.update(cx, |editor, cx| { - assert!(!editor.is_dirty(cx)); - assert!(!editor.has_conflict(cx)); - }); - }) - .unwrap(); - } - - #[gpui::test] - async fn test_open_and_save_new_file(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .create_dir(Path::new(path!("/root"))) - .await - .unwrap(); - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - project.update(cx, |project, _| { - project.languages().add(markdown_lang()); - project.languages().add(rust_lang()); - }); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let worktree = cx.update(|cx| window.read(cx).unwrap().worktrees(cx).next().unwrap()); - - // Create a new untitled buffer - cx.dispatch_action(window.into(), NewFile); - let editor = window - .read_with(cx, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }) - .unwrap(); - - window - .update(cx, |_, window, cx| { - editor.update(cx, |editor, cx| { - assert!(!editor.is_dirty(cx)); - assert_eq!(editor.title(cx), "untitled"); - assert!(Arc::ptr_eq( - &editor - .buffer() - .read(cx) - .language_at(MultiBufferOffset(0), cx) - .unwrap(), - &languages::PLAIN_TEXT - )); - editor.handle_input("hi", window, cx); - assert!(editor.is_dirty(cx)); - }); - }) - .unwrap(); - - // Save the buffer. This prompts for a filename. - let save_task = window - .update(cx, |workspace, window, cx| { - workspace.save_active_item(SaveIntent::Save, window, cx) - }) - .unwrap(); - cx.background_executor.run_until_parked(); - cx.simulate_new_path_selection(|parent_dir| { - assert_eq!(parent_dir, Path::new(path!("/root"))); - Some(parent_dir.join("the-new-name.rs")) - }); - cx.read(|cx| { - assert!(editor.is_dirty(cx)); - assert_eq!(editor.read(cx).title(cx), "hi"); - }); - - // When the save completes, the buffer's title is updated and the language is assigned based - // on the path. - save_task.await.unwrap(); - window - .update(cx, |_, _, cx| { - editor.update(cx, |editor, cx| { - assert!(!editor.is_dirty(cx)); - assert_eq!(editor.title(cx), "the-new-name.rs"); - assert_eq!( - editor - .buffer() - .read(cx) - .language_at(MultiBufferOffset(0), cx) - .unwrap() - .name(), - "Rust".into() - ); - }); - }) - .unwrap(); - - // Edit the file and save it again. This time, there is no filename prompt. - window - .update(cx, |_, window, cx| { - editor.update(cx, |editor, cx| { - editor.handle_input(" there", window, cx); - assert!(editor.is_dirty(cx)); - }); - }) - .unwrap(); - - let save_task = window - .update(cx, |workspace, window, cx| { - workspace.save_active_item(SaveIntent::Save, window, cx) - }) - .unwrap(); - save_task.await.unwrap(); - - assert!(!cx.did_prompt_for_new_path()); - window - .update(cx, |_, _, cx| { - editor.update(cx, |editor, cx| { - assert!(!editor.is_dirty(cx)); - assert_eq!(editor.title(cx), "the-new-name.rs") - }); - }) - .unwrap(); - - // Open the same newly-created file in another pane item. The new editor should reuse - // the same buffer. - cx.dispatch_action(window.into(), NewFile); - window - .update(cx, |workspace, window, cx| { - workspace.split_and_clone( - workspace.active_pane().clone(), - SplitDirection::Right, - window, - cx, - ) - }) - .unwrap() - .await - .unwrap(); - window - .update(cx, |workspace, window, cx| { - workspace.open_path( - (worktree.read(cx).id(), rel_path("the-new-name.rs")), - None, - true, - window, - cx, - ) - }) - .unwrap() - .await - .unwrap(); - let editor2 = window - .update(cx, |workspace, _, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }) - .unwrap(); - cx.read(|cx| { - assert_eq!( - editor2.read(cx).buffer().read(cx).as_singleton().unwrap(), - editor.read(cx).buffer().read(cx).as_singleton().unwrap() - ); - }) - } - - #[gpui::test] - async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state.fs.create_dir(Path::new("/root")).await.unwrap(); - - let project = Project::test(app_state.fs.clone(), [], cx).await; - project.update(cx, |project, _| { - project.languages().add(language::rust_lang()); - project.languages().add(language::markdown_lang()); - }); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - - // Create a new untitled buffer - cx.dispatch_action(window.into(), NewFile); - let editor = window - .read_with(cx, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }) - .unwrap(); - window - .update(cx, |_, window, cx| { - editor.update(cx, |editor, cx| { - assert!(Arc::ptr_eq( - &editor - .buffer() - .read(cx) - .language_at(MultiBufferOffset(0), cx) - .unwrap(), - &languages::PLAIN_TEXT - )); - editor.handle_input("hi", window, cx); - assert!(editor.is_dirty(cx)); - }); - }) - .unwrap(); - - // Save the buffer. This prompts for a filename. - let save_task = window - .update(cx, |workspace, window, cx| { - workspace.save_active_item(SaveIntent::Save, window, cx) - }) - .unwrap(); - cx.background_executor.run_until_parked(); - cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs"))); - save_task.await.unwrap(); - // The buffer is not dirty anymore and the language is assigned based on the path. - window - .update(cx, |_, _, cx| { - editor.update(cx, |editor, cx| { - assert!(!editor.is_dirty(cx)); - assert_eq!( - editor - .buffer() - .read(cx) - .language_at(MultiBufferOffset(0), cx) - .unwrap() - .name(), - "Rust".into() - ) - }); - }) - .unwrap(); - } - - #[gpui::test] - async fn test_pane_actions(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "a": { - "file1": "contents 1", - "file2": "contents 2", - "file3": "contents 3", - }, - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - project.update(cx, |project, _cx| project.languages().add(markdown_lang())); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); - - let entries = cx.read(|cx| workspace.file_project_paths(cx)); - let file1 = entries[0].clone(); - - let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone()); - - window - .update(cx, |w, window, cx| { - w.open_path(file1.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap(); - - let (editor_1, buffer) = window - .update(cx, |_, window, cx| { - pane_1.update(cx, |pane_1, cx| { - let editor = pane_1.active_item().unwrap().downcast::().unwrap(); - assert_eq!(editor.project_path(cx), Some(file1.clone())); - let buffer = editor.update(cx, |editor, cx| { - editor.insert("dirt", window, cx); - editor.buffer().downgrade() - }); - (editor.downgrade(), buffer) - }) - }) - .unwrap(); - - cx.dispatch_action(window.into(), pane::SplitRight); - let editor_2 = cx.update(|cx| { - let pane_2 = workspace.read(cx).active_pane().clone(); - assert_ne!(pane_1, pane_2); - - let pane2_item = pane_2.read(cx).active_item().unwrap(); - assert_eq!(pane2_item.project_path(cx), Some(file1.clone())); - - pane2_item.downcast::().unwrap().downgrade() - }); - cx.dispatch_action( - window.into(), - workspace::CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - ); - - cx.background_executor.run_until_parked(); - window - .read_with(cx, |workspace, _| { - assert_eq!(workspace.panes().len(), 1); - assert_eq!(workspace.active_pane(), &pane_1); - }) - .unwrap(); - - cx.dispatch_action( - window.into(), - workspace::CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - ); - cx.background_executor.run_until_parked(); - cx.simulate_prompt_answer("Don't Save"); - cx.background_executor.run_until_parked(); - - window - .update(cx, |workspace, _, cx| { - assert_eq!(workspace.panes().len(), 1); - assert!(workspace.active_item(cx).is_none()); - }) - .unwrap(); - - cx.background_executor - .advance_clock(SERIALIZATION_THROTTLE_TIME); - cx.update(|_| {}); - editor_1.assert_released(); - editor_2.assert_released(); - buffer.assert_released(); - } - - #[gpui::test] - async fn test_navigation(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "a": { - "file1": "contents 1\n".repeat(20), - "file2": "contents 2\n".repeat(20), - "file3": "contents 3\n".repeat(20), - }, - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - project.update(cx, |project, _cx| project.languages().add(markdown_lang())); - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace - .read_with(cx, |workspace, _| workspace.active_pane().clone()) - .unwrap(); - - let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx)); - let file1 = entries[0].clone(); - let file2 = entries[1].clone(); - let file3 = entries[2].clone(); - - let editor1 = workspace - .update(cx, |w, window, cx| { - w.open_path(file1.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap() - .downcast::() - .unwrap(); - workspace - .update(cx, |_, window, cx| { - editor1.update(cx, |editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.select_display_ranges([DisplayPoint::new(DisplayRow(10), 0) - ..DisplayPoint::new(DisplayRow(10), 0)]) - }); - }); - }) - .unwrap(); - - let editor2 = workspace - .update(cx, |w, window, cx| { - w.open_path(file2.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap() - .downcast::() - .unwrap(); - let editor3 = workspace - .update(cx, |w, window, cx| { - w.open_path(file3.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap() - .downcast::() - .unwrap(); - - workspace - .update(cx, |_, window, cx| { - editor3.update(cx, |editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.select_display_ranges([DisplayPoint::new(DisplayRow(12), 0) - ..DisplayPoint::new(DisplayRow(12), 0)]) - }); - editor.newline(&Default::default(), window, cx); - editor.newline(&Default::default(), window, cx); - editor.move_down(&Default::default(), window, cx); - editor.move_down(&Default::default(), window, cx); - editor.save( - SaveOptions { - format: true, - autosave: false, - }, - project.clone(), - window, - cx, - ) - }) - }) - .unwrap() - .await - .unwrap(); - workspace - .update(cx, |_, window, cx| { - editor3.update(cx, |editor, cx| { - editor.set_scroll_position(point(0., 12.5), window, cx) - }); - }) - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5) - ); - - workspace - .update(cx, |w, window, cx| { - w.go_back(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.) - ); - - workspace - .update(cx, |w, window, cx| { - w.go_back(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.) - ); - - workspace - .update(cx, |w, window, cx| { - w.go_back(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.) - ); - - workspace - .update(cx, |w, window, cx| { - w.go_back(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.) - ); - - // Go back one more time and ensure we don't navigate past the first item in the history. - workspace - .update(cx, |w, window, cx| { - w.go_back(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.) - ); - - workspace - .update(cx, |w, window, cx| { - w.go_forward(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.) - ); - - workspace - .update(cx, |w, window, cx| { - w.go_forward(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.) - ); - - // Go forward to an item that has been closed, ensuring it gets re-opened at the same - // location. - workspace - .update(cx, |_, window, cx| { - pane.update(cx, |pane, cx| { - let editor3_id = editor3.entity_id(); - drop(editor3); - pane.close_item_by_id(editor3_id, SaveIntent::Close, window, cx) - }) - }) - .unwrap() - .await - .unwrap(); - workspace - .update(cx, |w, window, cx| { - w.go_forward(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.) - ); - - workspace - .update(cx, |w, window, cx| { - w.go_forward(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5) - ); - - workspace - .update(cx, |w, window, cx| { - w.go_back(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.) - ); - - // Go back to an item that has been closed and removed from disk - workspace - .update(cx, |_, window, cx| { - pane.update(cx, |pane, cx| { - let editor2_id = editor2.entity_id(); - drop(editor2); - pane.close_item_by_id(editor2_id, SaveIntent::Close, window, cx) - }) - }) - .unwrap() - .await - .unwrap(); - app_state - .fs - .remove_file(Path::new(path!("/root/a/file2")), Default::default()) - .await - .unwrap(); - cx.background_executor.run_until_parked(); - - workspace - .update(cx, |w, window, cx| { - w.go_back(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.) - ); - workspace - .update(cx, |w, window, cx| { - w.go_forward(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.) - ); - - // Modify file to collapse multiple nav history entries into the same location. - // Ensure we don't visit the same location twice when navigating. - workspace - .update(cx, |_, window, cx| { - editor1.update(cx, |editor, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([DisplayPoint::new(DisplayRow(15), 0) - ..DisplayPoint::new(DisplayRow(15), 0)]) - }) - }); - }) - .unwrap(); - for _ in 0..5 { - workspace - .update(cx, |_, window, cx| { - editor1.update(cx, |editor, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([DisplayPoint::new(DisplayRow(3), 0) - ..DisplayPoint::new(DisplayRow(3), 0)]) - }); - }); - }) - .unwrap(); - - workspace - .update(cx, |_, window, cx| { - editor1.update(cx, |editor, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([DisplayPoint::new(DisplayRow(13), 0) - ..DisplayPoint::new(DisplayRow(13), 0)]) - }) - }); - }) - .unwrap(); - } - workspace - .update(cx, |_, window, cx| { - editor1.update(cx, |editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([DisplayPoint::new(DisplayRow(2), 0) - ..DisplayPoint::new(DisplayRow(14), 0)]) - }); - editor.insert("", window, cx); - }) - }); - }) - .unwrap(); - - workspace - .update(cx, |_, window, cx| { - editor1.update(cx, |editor, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([DisplayPoint::new(DisplayRow(1), 0) - ..DisplayPoint::new(DisplayRow(1), 0)]) - }) - }); - }) - .unwrap(); - workspace - .update(cx, |w, window, cx| { - w.go_back(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file1.clone(), DisplayPoint::new(DisplayRow(2), 0), 0.) - ); - workspace - .update(cx, |w, window, cx| { - w.go_back(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file1.clone(), DisplayPoint::new(DisplayRow(3), 0), 0.) - ); - - fn active_location( - workspace: &WindowHandle, - cx: &mut TestAppContext, - ) -> (ProjectPath, DisplayPoint, f64) { - workspace - .update(cx, |workspace, _, cx| { - let item = workspace.active_item(cx).unwrap(); - let editor = item.downcast::().unwrap(); - let (selections, scroll_position) = editor.update(cx, |editor, cx| { - ( - editor - .selections - .display_ranges(&editor.display_snapshot(cx)), - editor.scroll_position(cx), - ) - }); - ( - item.project_path(cx).unwrap(), - selections[0].start, - scroll_position.y, - ) - }) - .unwrap() - } - } - - #[gpui::test] - async fn test_reopening_closed_items(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "a": { - "file1": "", - "file2": "", - "file3": "", - "file4": "", - }, - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - project.update(cx, |project, _cx| project.languages().add(markdown_lang())); - let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let pane = workspace - .read_with(cx, |workspace, _| workspace.active_pane().clone()) - .unwrap(); - - let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx)); - let file1 = entries[0].clone(); - let file2 = entries[1].clone(); - let file3 = entries[2].clone(); - let file4 = entries[3].clone(); - - let file1_item_id = workspace - .update(cx, |w, window, cx| { - w.open_path(file1.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap() - .item_id(); - let file2_item_id = workspace - .update(cx, |w, window, cx| { - w.open_path(file2.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap() - .item_id(); - let file3_item_id = workspace - .update(cx, |w, window, cx| { - w.open_path(file3.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap() - .item_id(); - let file4_item_id = workspace - .update(cx, |w, window, cx| { - w.open_path(file4.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap() - .item_id(); - assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - - // Close all the pane items in some arbitrary order. - workspace - .update(cx, |_, window, cx| { - pane.update(cx, |pane, cx| { - pane.close_item_by_id(file1_item_id, SaveIntent::Close, window, cx) - }) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - - workspace - .update(cx, |_, window, cx| { - pane.update(cx, |pane, cx| { - pane.close_item_by_id(file4_item_id, SaveIntent::Close, window, cx) - }) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - - workspace - .update(cx, |_, window, cx| { - pane.update(cx, |pane, cx| { - pane.close_item_by_id(file2_item_id, SaveIntent::Close, window, cx) - }) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - workspace - .update(cx, |_, window, cx| { - pane.update(cx, |pane, cx| { - pane.close_item_by_id(file3_item_id, SaveIntent::Close, window, cx) - }) - }) - .unwrap() - .await - .unwrap(); - - assert_eq!(active_path(&workspace, cx), None); - - // Reopen all the closed items, ensuring they are reopened in the same order - // in which they were closed. - workspace - .update(cx, Workspace::reopen_closed_item) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - - workspace - .update(cx, Workspace::reopen_closed_item) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file2.clone())); - - workspace - .update(cx, Workspace::reopen_closed_item) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - - workspace - .update(cx, Workspace::reopen_closed_item) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file1.clone())); - - // Reopening past the last closed item is a no-op. - workspace - .update(cx, Workspace::reopen_closed_item) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file1.clone())); - - // Reopening closed items doesn't interfere with navigation history. - workspace - .update(cx, |workspace, window, cx| { - workspace.go_back(workspace.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - - workspace - .update(cx, |workspace, window, cx| { - workspace.go_back(workspace.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file2.clone())); - - workspace - .update(cx, |workspace, window, cx| { - workspace.go_back(workspace.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - - workspace - .update(cx, |workspace, window, cx| { - workspace.go_back(workspace.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - - workspace - .update(cx, |workspace, window, cx| { - workspace.go_back(workspace.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - - workspace - .update(cx, |workspace, window, cx| { - workspace.go_back(workspace.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file2.clone())); - - workspace - .update(cx, |workspace, window, cx| { - workspace.go_back(workspace.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file1.clone())); - - workspace - .update(cx, |workspace, window, cx| { - workspace.go_back(workspace.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file1.clone())); - - fn active_path( - workspace: &WindowHandle, - cx: &TestAppContext, - ) -> Option { - workspace - .read_with(cx, |workspace, cx| { - let item = workspace.active_item(cx)?; - item.project_path(cx) - }) - .unwrap() - } - } - - fn init_keymap_test(cx: &mut TestAppContext) -> Arc { - cx.update(|cx| { - let app_state = AppState::test(cx); - - theme::init(theme::LoadThemes::JustBase, cx); - client::init(&app_state.client, cx); - workspace::init(app_state.clone(), cx); - onboarding::init(cx); - app_state - }) - } - - actions!(test_only, [ActionA, ActionB]); - - #[gpui::test] - async fn test_base_keymap(cx: &mut gpui::TestAppContext) { - let executor = cx.executor(); - let app_state = init_keymap_test(cx); - let project = Project::test(app_state.fs.clone(), [], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - // From the Atom keymap - use workspace::ActivatePreviousPane; - // From the JetBrains keymap - use workspace::ActivatePreviousItem; - - app_state - .fs - .save( - "/settings.json".as_ref(), - &r#"{"base_keymap": "Atom"}"#.into(), - Default::default(), - ) - .await - .unwrap(); - - app_state - .fs - .save( - "/keymap.json".as_ref(), - &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(), - Default::default(), - ) - .await - .unwrap(); - executor.run_until_parked(); - cx.update(|cx| { - let settings_rx = watch_config_file( - &executor, - app_state.fs.clone(), - PathBuf::from("/settings.json"), - ); - let keymap_rx = watch_config_file( - &executor, - app_state.fs.clone(), - PathBuf::from("/keymap.json"), - ); - let global_settings_rx = watch_config_file( - &executor, - app_state.fs.clone(), - PathBuf::from("/global_settings.json"), - ); - handle_settings_file_changes(settings_rx, global_settings_rx, cx); - handle_keymap_file_changes(keymap_rx, cx); - }); - workspace - .update(cx, |workspace, _, cx| { - workspace.register_action(|_, _: &ActionA, _window, _cx| {}); - workspace.register_action(|_, _: &ActionB, _window, _cx| {}); - workspace.register_action(|_, _: &ActivatePreviousPane, _window, _cx| {}); - workspace.register_action(|_, _: &ActivatePreviousItem, _window, _cx| {}); - cx.notify(); - }) - .unwrap(); - executor.run_until_parked(); - // Test loading the keymap base at all - assert_key_bindings_for( - workspace.into(), - cx, - vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)], - line!(), - ); - - // Test modifying the users keymap, while retaining the base keymap - app_state - .fs - .save( - "/keymap.json".as_ref(), - &r#"[{"bindings": {"backspace": "test_only::ActionB"}}]"#.into(), - Default::default(), - ) - .await - .unwrap(); - - executor.run_until_parked(); - - assert_key_bindings_for( - workspace.into(), - cx, - vec![("backspace", &ActionB), ("k", &ActivatePreviousPane)], - line!(), - ); - - // Test modifying the base, while retaining the users keymap - app_state - .fs - .save( - "/settings.json".as_ref(), - &r#"{"base_keymap": "JetBrains"}"#.into(), - Default::default(), - ) - .await - .unwrap(); - - executor.run_until_parked(); - - assert_key_bindings_for( - workspace.into(), - cx, - vec![("backspace", &ActionB), ("{", &ActivatePreviousItem)], - line!(), - ); - } - - #[gpui::test] - async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) { - let executor = cx.executor(); - let app_state = init_keymap_test(cx); - let project = Project::test(app_state.fs.clone(), [], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - // From the Atom keymap - use workspace::ActivatePreviousPane; - // From the JetBrains keymap - use diagnostics::Deploy; - - workspace - .update(cx, |workspace, _, _| { - workspace.register_action(|_, _: &ActionA, _window, _cx| {}); - workspace.register_action(|_, _: &ActionB, _window, _cx| {}); - workspace.register_action(|_, _: &Deploy, _window, _cx| {}); - }) - .unwrap(); - app_state - .fs - .save( - "/settings.json".as_ref(), - &r#"{"base_keymap": "Atom"}"#.into(), - Default::default(), - ) - .await - .unwrap(); - app_state - .fs - .save( - "/keymap.json".as_ref(), - &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(), - Default::default(), - ) - .await - .unwrap(); - - cx.update(|cx| { - let settings_rx = watch_config_file( - &executor, - app_state.fs.clone(), - PathBuf::from("/settings.json"), - ); - let keymap_rx = watch_config_file( - &executor, - app_state.fs.clone(), - PathBuf::from("/keymap.json"), - ); - - let global_settings_rx = watch_config_file( - &executor, - app_state.fs.clone(), - PathBuf::from("/global_settings.json"), - ); - handle_settings_file_changes(settings_rx, global_settings_rx, cx); - handle_keymap_file_changes(keymap_rx, cx); - }); - - cx.background_executor.run_until_parked(); - - cx.background_executor.run_until_parked(); - // Test loading the keymap base at all - assert_key_bindings_for( - workspace.into(), - cx, - vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)], - line!(), - ); - - // Test disabling the key binding for the base keymap - app_state - .fs - .save( - "/keymap.json".as_ref(), - &r#"[{"bindings": {"backspace": null}}]"#.into(), - Default::default(), - ) - .await - .unwrap(); - - cx.background_executor.run_until_parked(); - - assert_key_bindings_for( - workspace.into(), - cx, - vec![("k", &ActivatePreviousPane)], - line!(), - ); - - // Test modifying the base, while retaining the users keymap - app_state - .fs - .save( - "/settings.json".as_ref(), - &r#"{"base_keymap": "JetBrains"}"#.into(), - Default::default(), - ) - .await - .unwrap(); - - cx.background_executor.run_until_parked(); - - assert_key_bindings_for(workspace.into(), cx, vec![("6", &Deploy)], line!()); - } - - #[gpui::test] - async fn test_generate_keymap_json_schema_for_registered_actions( - cx: &mut gpui::TestAppContext, - ) { - init_keymap_test(cx); - cx.update(|cx| { - // Make sure it doesn't panic. - KeymapFile::generate_json_schema_for_registered_actions(cx); - }); - } - - /// Checks that action namespaces are the expected set. The purpose of this is to prevent typos - /// and let you know when introducing a new namespace. - #[gpui::test] - async fn test_action_namespaces(cx: &mut gpui::TestAppContext) { - use itertools::Itertools; - - init_keymap_test(cx); - cx.update(|cx| { - let all_actions = cx.all_action_names(); - - let mut actions_without_namespace = Vec::new(); - let all_namespaces = all_actions - .iter() - .filter_map(|action_name| { - let namespace = action_name - .split("::") - .collect::>() - .into_iter() - .rev() - .skip(1) - .rev() - .join("::"); - if namespace.is_empty() { - actions_without_namespace.push(*action_name); - } - if &namespace == "test_only" || &namespace == "stories" { - None - } else { - Some(namespace) - } - }) - .sorted() - .dedup() - .collect::>(); - assert_eq!(actions_without_namespace, Vec::<&str>::new()); - - let expected_namespaces = vec![ - "action", - "activity_indicator", - "agent", - #[cfg(not(target_os = "macos"))] - "app_menu", - "assistant", - "assistant2", - "auto_update", - "branch_picker", - "bedrock", - "branches", - "buffer_search", - "channel_modal", - "cli", - "client", - "collab", - "collab_panel", - "command_palette", - "console", - "context_server", - "copilot", - "debug_panel", - "debugger", - "dev", - "diagnostics", - "edit_prediction", - "editor", - "feedback", - "file_finder", - "git", - "git_onboarding", - "git_panel", - "go_to_line", - "icon_theme_selector", - "inline_assistant", - "journal", - "keymap_editor", - "keystroke_input", - "language_selector", - "line_ending_selector", - "lsp_tool", - "markdown", - "menu", - "notebook", - "notification_panel", - "onboarding", - "outline", - "outline_panel", - "pane", - "panel", - "picker", - "project_panel", - "project_search", - "project_symbols", - "projects", - "repl", - "rules_library", - "search", - "settings_editor", - "settings_profile_selector", - "snippets", - "stash_picker", - "supermaven", - "svg", - "syntax_tree_view", - "tab_switcher", - "task", - "terminal", - "terminal_panel", - "theme_selector", - "toast", - "toolchain", - "variable_list", - "vim", - "window", - "workspace", - "zed", - "zed_actions", - "zed_predict_onboarding", - "zeta", - ]; - assert_eq!( - all_namespaces, - expected_namespaces - .into_iter() - .map(|namespace| namespace.to_string()) - .sorted() - .collect::>() - ); - }); - } - - #[gpui::test] - fn test_bundled_settings_and_themes(cx: &mut App) { - cx.text_system() - .add_fonts(vec![ - Assets - .load("fonts/lilex/Lilex-Regular.ttf") - .unwrap() - .unwrap(), - Assets - .load("fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf") - .unwrap() - .unwrap(), - ]) - .unwrap(); - let themes = ThemeRegistry::default(); - settings::init(cx); - theme::init(theme::LoadThemes::JustBase, cx); - - let mut has_default_theme = false; - for theme_name in themes.list().into_iter().map(|meta| meta.name) { - let theme = themes.get(&theme_name).unwrap(); - assert_eq!(theme.name, theme_name); - if theme.name.as_ref() == "One Dark" { - has_default_theme = true; - } - } - assert!(has_default_theme); - } - - #[gpui::test] - async fn test_bundled_files_editor(cx: &mut TestAppContext) { - let app_state = init_test(cx); - cx.update(init); - - let project = Project::test(app_state.fs.clone(), [], cx).await; - let _window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - - cx.update(|cx| { - cx.dispatch_action(&OpenDefaultSettings); - }); - cx.run_until_parked(); - - assert_eq!(cx.read(|cx| cx.windows().len()), 1); - - let workspace = cx.windows()[0].downcast::().unwrap(); - let active_editor = workspace - .update(cx, |workspace, _, cx| { - workspace.active_item_as::(cx) - }) - .unwrap(); - assert!( - active_editor.is_some(), - "Settings action should have opened an editor with the default file contents" - ); - - let active_editor = active_editor.unwrap(); - assert!( - active_editor.read_with(cx, |editor, cx| editor.read_only(cx)), - "Default settings should be readonly" - ); - assert!( - active_editor.read_with(cx, |editor, cx| editor.buffer().read(cx).read_only()), - "The underlying buffer should also be readonly for the shipped default settings" - ); - } - - #[gpui::test] - async fn test_bundled_languages(cx: &mut TestAppContext) { - let fs = fs::FakeFs::new(cx.background_executor.clone()); - env_logger::builder().is_test(true).try_init().ok(); - let settings = cx.update(SettingsStore::test); - cx.set_global(settings); - let languages = LanguageRegistry::test(cx.executor()); - let languages = Arc::new(languages); - let node_runtime = node_runtime::NodeRuntime::unavailable(); - cx.update(|cx| { - languages::init(languages.clone(), fs, node_runtime, cx); - }); - for name in languages.language_names() { - languages - .language_for_name(name.as_ref()) - .await - .with_context(|| format!("language name {name}")) - .unwrap(); - } - cx.run_until_parked(); - } - - pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc { - init_test_with_state(cx, cx.update(AppState::test)) - } - - fn init_test_with_state( - cx: &mut TestAppContext, - mut app_state: Arc, - ) -> Arc { - cx.update(move |cx| { - env_logger::builder().is_test(true).try_init().ok(); - - let state = Arc::get_mut(&mut app_state).unwrap(); - state.build_window_options = build_window_options; - app_state.languages.add(markdown_lang()); - - gpui_tokio::init(cx); - theme::init(theme::LoadThemes::JustBase, cx); - audio::init(cx); - channel::init(&app_state.client, app_state.user_store.clone(), cx); - call::init(app_state.client.clone(), app_state.user_store.clone(), cx); - notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); - workspace::init(app_state.clone(), cx); - release_channel::init(Version::new(0, 0, 0), cx); - command_palette::init(cx); - editor::init(cx); - collab_ui::init(&app_state, cx); - git_ui::init(cx); - project_panel::init(cx); - outline_panel::init(cx); - terminal_view::init(cx); - copilot::copilot_chat::init( - app_state.fs.clone(), - app_state.client.http_client(), - copilot::copilot_chat::CopilotChatConfiguration::default(), - cx, - ); - image_viewer::init(cx); - language_model::init(app_state.client.clone(), cx); - language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx); - web_search::init(cx); - web_search_providers::init(app_state.client.clone(), cx); - let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx); - agent_ui::init( - app_state.fs.clone(), - app_state.client.clone(), - prompt_builder.clone(), - app_state.languages.clone(), - false, - cx, - ); - repl::init(app_state.fs.clone(), cx); - repl::notebook::init(cx); - tasks_ui::init(cx); - project::debugger::breakpoint_store::BreakpointStore::init( - &app_state.client.clone().into(), - ); - project::debugger::dap_store::DapStore::init(&app_state.client.clone().into(), cx); - debugger_ui::init(cx); - initialize_workspace(app_state.clone(), prompt_builder, cx); - search::init(cx); - app_state - }) - } - - #[track_caller] - fn assert_key_bindings_for( - window: AnyWindowHandle, - cx: &TestAppContext, - actions: Vec<(&'static str, &dyn Action)>, - line: u32, - ) { - let available_actions = cx - .update(|cx| window.update(cx, |_, window, cx| window.available_actions(cx))) - .unwrap(); - for (key, action) in actions { - let bindings = cx - .update(|cx| window.update(cx, |_, window, _| window.bindings_for_action(action))) - .unwrap(); - // assert that... - assert!( - available_actions.iter().any(|bound_action| { - // actions match... - bound_action.partial_eq(action) - }), - "On {} Failed to find {}", - line, - action.name(), - ); - assert!( - // and key strokes contain the given key - bindings - .into_iter() - .any(|binding| binding.keystrokes().iter().any(|k| k.key() == key)), - "On {} Failed to find {} with key binding {}", - line, - action.name(), - key - ); - } - } - - #[gpui::test] - async fn test_opening_project_settings_when_excluded(cx: &mut gpui::TestAppContext) { - // Use the proper initialization for runtime state - let app_state = init_keymap_test(cx); - - eprintln!("Running test_opening_project_settings_when_excluded"); - - // 1. Set up a project with some project settings - let settings_init = - r#"{ "UNIQUEVALUE": true, "git": { "inline_blame": { "enabled": false } } }"#; - app_state - .fs - .as_fake() - .insert_tree( - Path::new("/root"), - json!({ - ".zed": { - "settings.json": settings_init - } - }), - ) - .await; - - eprintln!("Created project with .zed/settings.json containing UNIQUEVALUE"); - - // 2. Create a project with the file system and load it - let project = Project::test(app_state.fs.clone(), [Path::new("/root")], cx).await; - - // Save original settings content for comparison - let original_settings = app_state - .fs - .load(Path::new("/root/.zed/settings.json")) - .await - .unwrap(); - - let original_settings_str = original_settings.clone(); - - // Verify settings exist on disk and have expected content - eprintln!("Original settings content: {}", original_settings_str); - assert!( - original_settings_str.contains("UNIQUEVALUE"), - "Test setup failed - settings file doesn't contain our marker" - ); - - // 3. Add .zed to file scan exclusions in user settings - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |worktree_settings| { - worktree_settings.project.worktree.file_scan_exclusions = - Some(vec![".zed".to_string()]); - }); - }); - - eprintln!("Added .zed to file_scan_exclusions in settings"); - - // 4. Run tasks to apply settings - cx.background_executor.run_until_parked(); - - // 5. Critical: Verify .zed is actually excluded from worktree - let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap()); - - let has_zed_entry = - cx.update(|cx| worktree.read(cx).entry_for_path(rel_path(".zed")).is_some()); - - eprintln!( - "Is .zed directory visible in worktree after exclusion: {}", - has_zed_entry - ); - - // This assertion verifies the test is set up correctly to show the bug - // If .zed is not excluded, the test will fail here - assert!( - !has_zed_entry, - "Test precondition failed: .zed directory should be excluded but was found in worktree" - ); - - // 6. Create workspace and trigger the actual function that causes the bug - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - window - .update(cx, |workspace, window, cx| { - // Call the exact function that contains the bug - eprintln!("About to call open_project_settings_file"); - open_project_settings_file(workspace, &OpenProjectSettingsFile, window, cx); - }) - .unwrap(); - - // 7. Run background tasks until completion - cx.background_executor.run_until_parked(); - - // 8. Verify file contents after calling function - let new_content = app_state - .fs - .load(Path::new("/root/.zed/settings.json")) - .await - .unwrap(); - - let new_content_str = new_content; - eprintln!("New settings content: {}", new_content_str); - - // The bug causes the settings to be overwritten with empty settings - // So if the unique value is no longer present, the bug has been reproduced - let bug_exists = !new_content_str.contains("UNIQUEVALUE"); - eprintln!("Bug reproduced: {}", bug_exists); - - // This assertion should fail if the bug exists - showing the bug is real - assert!( - new_content_str.contains("UNIQUEVALUE"), - "BUG FOUND: Project settings were overwritten when opening via command - original custom content was lost" - ); - } - - #[gpui::test] - async fn test_prefer_focused_window(cx: &mut gpui::TestAppContext) { - let app_state = init_test(cx); - let paths = [PathBuf::from(path!("/dir/document.txt"))]; - - app_state - .fs - .as_fake() - .insert_tree( - path!("/dir"), - json!({ - "document.txt": "Some of the documentation's content." - }), - ) - .await; - - let project_a = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window_a = - cx.add_window(|window, cx| Workspace::test_new(project_a.clone(), window, cx)); - - let project_b = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window_b = - cx.add_window(|window, cx| Workspace::test_new(project_b.clone(), window, cx)); - - let project_c = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window_c = - cx.add_window(|window, cx| Workspace::test_new(project_c.clone(), window, cx)); - - for window in [window_a, window_b, window_c] { - let _ = cx.update_window(*window, |_, window, _| { - window.activate_window(); - }); - - cx.update(|cx| { - let open_options = OpenOptions { - prefer_focused_window: true, - ..Default::default() - }; - - workspace::open_paths(&paths, app_state.clone(), open_options, cx) - }) - .await - .unwrap(); - - cx.update_window(*window, |_, window, _| assert!(window.is_window_active())) - .unwrap(); - - let _ = window.read_with(cx, |workspace, cx| { - let pane = workspace.active_pane().read(cx); - let project_path = pane.active_item().unwrap().project_path(cx).unwrap(); - - assert_eq!( - project_path.path.as_ref().as_std_path().to_str().unwrap(), - path!("document.txt") - ) - }); - } - } -} diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs deleted file mode 100644 index a7961ac6d4..0000000000 --- a/crates/zed/src/zed/app_menus.rs +++ /dev/null @@ -1,331 +0,0 @@ -use collab_ui::collab_panel; -use gpui::{App, Menu, MenuItem, OsAction}; -use release_channel::ReleaseChannel; -use terminal_view::terminal_panel; -use zed_actions::{ToggleFocus as ToggleDebugPanel, dev}; - -pub fn app_menus(cx: &mut App) -> Vec { - use zed_actions::Quit; - - let mut view_items = vec![ - MenuItem::action( - "Zoom In", - zed_actions::IncreaseBufferFontSize { persist: false }, - ), - MenuItem::action( - "Zoom Out", - zed_actions::DecreaseBufferFontSize { persist: false }, - ), - MenuItem::action( - "Reset Zoom", - zed_actions::ResetBufferFontSize { persist: false }, - ), - MenuItem::action( - "Reset All Zoom", - zed_actions::ResetAllZoom { persist: false }, - ), - MenuItem::separator(), - MenuItem::action("Toggle Left Dock", workspace::ToggleLeftDock), - MenuItem::action("Toggle Right Dock", workspace::ToggleRightDock), - MenuItem::action("Toggle Bottom Dock", workspace::ToggleBottomDock), - MenuItem::action("Toggle All Docks", workspace::ToggleAllDocks), - MenuItem::submenu(Menu { - name: "Editor Layout".into(), - items: vec![ - MenuItem::action("Split Up", workspace::SplitUp), - MenuItem::action("Split Down", workspace::SplitDown), - MenuItem::action("Split Left", workspace::SplitLeft), - MenuItem::action("Split Right", workspace::SplitRight), - ], - }), - MenuItem::separator(), - MenuItem::action("Project Panel", zed_actions::project_panel::ToggleFocus), - MenuItem::action("Outline Panel", outline_panel::ToggleFocus), - MenuItem::action("Collab Panel", collab_panel::ToggleFocus), - MenuItem::action("Terminal Panel", terminal_panel::ToggleFocus), - MenuItem::action("Debugger Panel", ToggleDebugPanel), - MenuItem::separator(), - MenuItem::action("Diagnostics", diagnostics::Deploy), - MenuItem::separator(), - ]; - - if ReleaseChannel::try_global(cx) == Some(ReleaseChannel::Dev) { - view_items.push(MenuItem::action( - "Toggle GPUI Inspector", - dev::ToggleInspector, - )); - view_items.push(MenuItem::separator()); - } - - vec![ - Menu { - name: "Zed".into(), - items: vec![ - MenuItem::action("About Zed", zed_actions::About), - MenuItem::action("Check for Updates", auto_update::Check), - MenuItem::separator(), - MenuItem::submenu(Menu { - name: "Settings".into(), - items: vec![ - MenuItem::action("Open Settings", zed_actions::OpenSettings), - MenuItem::action("Open Settings File", super::OpenSettingsFile), - MenuItem::action("Open Project Settings", zed_actions::OpenProjectSettings), - MenuItem::action( - "Open Project Settings File", - super::OpenProjectSettingsFile, - ), - MenuItem::action("Open Default Settings", super::OpenDefaultSettings), - MenuItem::separator(), - MenuItem::action("Open Keymap", zed_actions::OpenKeymap), - MenuItem::action("Open Keymap File", zed_actions::OpenKeymapFile), - MenuItem::action( - "Open Default Key Bindings", - zed_actions::OpenDefaultKeymap, - ), - MenuItem::separator(), - MenuItem::action( - "Select Theme...", - zed_actions::theme_selector::Toggle::default(), - ), - MenuItem::action( - "Select Icon Theme...", - zed_actions::icon_theme_selector::Toggle::default(), - ), - ], - }), - MenuItem::separator(), - #[cfg(target_os = "macos")] - MenuItem::os_submenu("Services", gpui::SystemMenuType::Services), - MenuItem::separator(), - MenuItem::action("Extensions", zed_actions::Extensions::default()), - #[cfg(not(target_os = "windows"))] - MenuItem::action("Install CLI", install_cli::InstallCliBinary), - MenuItem::separator(), - #[cfg(target_os = "macos")] - MenuItem::action("Hide Zed", super::Hide), - #[cfg(target_os = "macos")] - MenuItem::action("Hide Others", super::HideOthers), - #[cfg(target_os = "macos")] - MenuItem::action("Show All", super::ShowAll), - MenuItem::separator(), - MenuItem::action("Quit Zed", Quit), - ], - }, - Menu { - name: "File".into(), - items: vec![ - MenuItem::action("New", workspace::NewFile), - MenuItem::action("New Window", workspace::NewWindow), - MenuItem::separator(), - #[cfg(not(target_os = "macos"))] - MenuItem::action("Open File...", workspace::OpenFiles), - MenuItem::action( - if cfg!(not(target_os = "macos")) { - "Open Folder..." - } else { - "Open…" - }, - workspace::Open, - ), - MenuItem::action( - "Open Recent...", - zed_actions::OpenRecent { - create_new_window: false, - }, - ), - MenuItem::action( - "Open Remote...", - zed_actions::OpenRemote { - create_new_window: false, - from_existing_connection: false, - }, - ), - MenuItem::separator(), - MenuItem::action("Add Folder to Project…", workspace::AddFolderToProject), - MenuItem::separator(), - MenuItem::action("Save", workspace::Save { save_intent: None }), - MenuItem::action("Save As…", workspace::SaveAs), - MenuItem::action("Save All", workspace::SaveAll { save_intent: None }), - MenuItem::separator(), - MenuItem::action( - "Close Editor", - workspace::CloseActiveItem { - save_intent: None, - close_pinned: true, - }, - ), - MenuItem::action("Close Window", workspace::CloseWindow), - ], - }, - Menu { - name: "Edit".into(), - items: vec![ - MenuItem::os_action("Undo", editor::actions::Undo, OsAction::Undo), - MenuItem::os_action("Redo", editor::actions::Redo, OsAction::Redo), - MenuItem::separator(), - MenuItem::os_action("Cut", editor::actions::Cut, OsAction::Cut), - MenuItem::os_action("Copy", editor::actions::Copy, OsAction::Copy), - MenuItem::action("Copy and Trim", editor::actions::CopyAndTrim), - MenuItem::os_action("Paste", editor::actions::Paste, OsAction::Paste), - MenuItem::separator(), - MenuItem::action("Find", search::buffer_search::Deploy::find()), - MenuItem::action("Find in Project", workspace::DeploySearch::find()), - MenuItem::separator(), - MenuItem::action( - "Toggle Line Comment", - editor::actions::ToggleComments::default(), - ), - ], - }, - Menu { - name: "Selection".into(), - items: vec![ - MenuItem::os_action( - "Select All", - editor::actions::SelectAll, - OsAction::SelectAll, - ), - MenuItem::action("Expand Selection", editor::actions::SelectLargerSyntaxNode), - MenuItem::action("Shrink Selection", editor::actions::SelectSmallerSyntaxNode), - MenuItem::action("Select Next Sibling", editor::actions::SelectNextSyntaxNode), - MenuItem::action( - "Select Previous Sibling", - editor::actions::SelectPreviousSyntaxNode, - ), - MenuItem::separator(), - MenuItem::action( - "Add Cursor Above", - editor::actions::AddSelectionAbove { - skip_soft_wrap: true, - }, - ), - MenuItem::action( - "Add Cursor Below", - editor::actions::AddSelectionBelow { - skip_soft_wrap: true, - }, - ), - MenuItem::action( - "Select Next Occurrence", - editor::actions::SelectNext { - replace_newest: false, - }, - ), - MenuItem::action( - "Select Previous Occurrence", - editor::actions::SelectPrevious { - replace_newest: false, - }, - ), - MenuItem::action("Select All Occurrences", editor::actions::SelectAllMatches), - MenuItem::separator(), - MenuItem::action("Move Line Up", editor::actions::MoveLineUp), - MenuItem::action("Move Line Down", editor::actions::MoveLineDown), - MenuItem::action("Duplicate Selection", editor::actions::DuplicateLineDown), - ], - }, - Menu { - name: "View".into(), - items: view_items, - }, - Menu { - name: "Go".into(), - items: vec![ - MenuItem::action("Back", workspace::GoBack), - MenuItem::action("Forward", workspace::GoForward), - MenuItem::separator(), - MenuItem::action("Command Palette...", zed_actions::command_palette::Toggle), - MenuItem::separator(), - MenuItem::action("Go to File...", workspace::ToggleFileFinder::default()), - // MenuItem::action("Go to Symbol in Project", project_symbols::Toggle), - MenuItem::action( - "Go to Symbol in Editor...", - zed_actions::outline::ToggleOutline, - ), - MenuItem::action("Go to Line/Column...", editor::actions::ToggleGoToLine), - MenuItem::separator(), - MenuItem::action("Go to Definition", editor::actions::GoToDefinition), - MenuItem::action("Go to Declaration", editor::actions::GoToDeclaration), - MenuItem::action("Go to Type Definition", editor::actions::GoToTypeDefinition), - MenuItem::action( - "Find All References", - editor::actions::FindAllReferences::default(), - ), - MenuItem::separator(), - MenuItem::action("Next Problem", editor::actions::GoToDiagnostic::default()), - MenuItem::action( - "Previous Problem", - editor::actions::GoToPreviousDiagnostic::default(), - ), - ], - }, - Menu { - name: "Run".into(), - items: vec![ - MenuItem::action( - "Spawn Task", - zed_actions::Spawn::ViaModal { - reveal_target: None, - }, - ), - MenuItem::action("Start Debugger", debugger_ui::Start), - MenuItem::separator(), - MenuItem::action("Edit tasks.json...", crate::zed::OpenProjectTasks), - MenuItem::action("Edit debug.json...", zed_actions::OpenProjectDebugTasks), - MenuItem::separator(), - MenuItem::action("Continue", debugger_ui::Continue), - MenuItem::action("Step Over", debugger_ui::StepOver), - MenuItem::action("Step Into", debugger_ui::StepInto), - MenuItem::action("Step Out", debugger_ui::StepOut), - MenuItem::separator(), - MenuItem::action("Toggle Breakpoint", editor::actions::ToggleBreakpoint), - MenuItem::action("Edit Breakpoint", editor::actions::EditLogBreakpoint), - MenuItem::action("Clear All Breakpoints", debugger_ui::ClearAllBreakpoints), - ], - }, - Menu { - name: "Window".into(), - items: vec![ - MenuItem::action("Minimize", super::Minimize), - MenuItem::action("Zoom", super::Zoom), - MenuItem::separator(), - ], - }, - Menu { - name: "Help".into(), - items: vec![ - MenuItem::action( - "View Release Notes Locally", - auto_update_ui::ViewReleaseNotesLocally, - ), - MenuItem::action("View Telemetry", zed_actions::OpenTelemetryLog), - MenuItem::action("View Dependency Licenses", zed_actions::OpenLicenses), - MenuItem::action("Show Welcome", onboarding::ShowWelcome), - MenuItem::separator(), - MenuItem::action("File Bug Report...", zed_actions::feedback::FileBugReport), - MenuItem::action("Request Feature...", zed_actions::feedback::RequestFeature), - MenuItem::action("Email Us...", zed_actions::feedback::EmailZed), - MenuItem::separator(), - MenuItem::action( - "Documentation", - super::OpenBrowser { - url: "https://zed.dev/docs".into(), - }, - ), - MenuItem::action("Zed Repository", feedback::OpenZedRepo), - MenuItem::action( - "Zed Twitter", - super::OpenBrowser { - url: "https://twitter.com/zeddotdev".into(), - }, - ), - MenuItem::action( - "Join the Team", - super::OpenBrowser { - url: "https://zed.dev/jobs".into(), - }, - ), - ], - }, - ] -} diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs deleted file mode 100644 index 14a46d8882..0000000000 --- a/crates/zed/src/zed/component_preview.rs +++ /dev/null @@ -1,1001 +0,0 @@ -//! # Component Preview -//! -//! A view for exploring Zed components. - -mod persistence; - -use client::UserStore; -use collections::HashMap; -use component::{ComponentId, ComponentMetadata, ComponentStatus, components}; -use gpui::{ - App, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, Window, list, prelude::*, -}; -use gpui::{ListState, ScrollHandle, ScrollStrategy, UniformListScrollHandle}; -use languages::LanguageRegistry; -use notifications::status_toast::{StatusToast, ToastIcon}; -use persistence::COMPONENT_PREVIEW_DB; -use project::Project; -use std::{iter::Iterator, ops::Range, sync::Arc}; -use ui::{ButtonLike, Divider, HighlightedLabel, ListItem, ListSubHeader, Tooltip, prelude::*}; -use ui_input::InputField; -use workspace::{ - AppState, Item, ItemId, SerializableItem, Workspace, WorkspaceId, delete_unloaded_items, - item::ItemEvent, -}; - -pub fn init(app_state: Arc, cx: &mut App) { - workspace::register_serializable_item::(cx); - - cx.observe_new(move |workspace: &mut Workspace, _window, cx| { - let app_state = app_state.clone(); - let project = workspace.project().clone(); - let weak_workspace = cx.entity().downgrade(); - - workspace.register_action( - move |workspace, _: &workspace::OpenComponentPreview, window, cx| { - let app_state = app_state.clone(); - - let language_registry = app_state.languages.clone(); - let user_store = app_state.user_store.clone(); - - let component_preview = cx.new(|cx| { - ComponentPreview::new( - weak_workspace.clone(), - project.clone(), - language_registry, - user_store, - None, - None, - window, - cx, - ) - .expect("Failed to create component preview") - }); - - workspace.add_item_to_active_pane( - Box::new(component_preview), - None, - true, - window, - cx, - ) - }, - ); - }) - .detach(); -} - -enum PreviewEntry { - AllComponents, - Separator, - Component(ComponentMetadata, Option>), - SectionHeader(SharedString), -} - -impl From for PreviewEntry { - fn from(component: ComponentMetadata) -> Self { - PreviewEntry::Component(component, None) - } -} - -impl From for PreviewEntry { - fn from(section_header: SharedString) -> Self { - PreviewEntry::SectionHeader(section_header) - } -} - -#[derive(Default, Debug, Clone, PartialEq, Eq)] -enum PreviewPage { - #[default] - AllComponents, - Component(ComponentId), -} - -struct ComponentPreview { - active_page: PreviewPage, - reset_key: usize, - component_list: ListState, - entries: Vec, - component_map: HashMap, - components: Vec, - cursor_index: usize, - filter_editor: Entity, - filter_text: String, - focus_handle: FocusHandle, - language_registry: Arc, - nav_scroll_handle: UniformListScrollHandle, - project: Entity, - user_store: Entity, - workspace: WeakEntity, - workspace_id: Option, - _view_scroll_handle: ScrollHandle, -} - -impl ComponentPreview { - pub fn new( - workspace: WeakEntity, - project: Entity, - language_registry: Arc, - user_store: Entity, - selected_index: impl Into>, - active_page: Option, - window: &mut Window, - cx: &mut Context, - ) -> anyhow::Result { - let component_registry = Arc::new(components()); - let sorted_components = component_registry.sorted_components(); - let selected_index = selected_index.into().unwrap_or(0); - let active_page = active_page.unwrap_or(PreviewPage::AllComponents); - let filter_editor = cx.new(|cx| InputField::new(window, cx, "Find components or usages…")); - - let component_list = ListState::new( - sorted_components.len(), - gpui::ListAlignment::Top, - px(1500.0), - ); - - let mut component_preview = Self { - active_page, - reset_key: 0, - component_list, - entries: Vec::new(), - component_map: component_registry.component_map(), - components: sorted_components, - cursor_index: selected_index, - filter_editor, - filter_text: String::new(), - focus_handle: cx.focus_handle(), - language_registry, - nav_scroll_handle: UniformListScrollHandle::new(), - project, - user_store, - workspace, - workspace_id: None, - _view_scroll_handle: ScrollHandle::new(), - }; - - if component_preview.cursor_index > 0 { - component_preview.scroll_to_preview(component_preview.cursor_index, cx); - } - - component_preview.update_component_list(cx); - - let focus_handle = component_preview.filter_editor.read(cx).focus_handle(cx); - window.focus(&focus_handle); - - Ok(component_preview) - } - - pub fn active_page_id(&self, _cx: &App) -> ActivePageId { - match &self.active_page { - PreviewPage::AllComponents => ActivePageId::default(), - PreviewPage::Component(component_id) => ActivePageId(component_id.0.to_string()), - } - } - - fn scroll_to_preview(&mut self, ix: usize, cx: &mut Context) { - self.component_list.scroll_to_reveal_item(ix); - self.cursor_index = ix; - cx.notify(); - } - - fn set_active_page(&mut self, page: PreviewPage, cx: &mut Context) { - if self.active_page == page { - // Force the current preview page to render again - self.reset_key = self.reset_key.wrapping_add(1); - } else { - self.active_page = page; - cx.emit(ItemEvent::UpdateTab); - } - cx.notify(); - } - - fn filtered_components(&self) -> Vec { - if self.filter_text.is_empty() { - return self.components.clone(); - } - - let filter = self.filter_text.to_lowercase(); - self.components - .iter() - .filter(|component| { - let component_name = component.name().to_lowercase(); - let scope_name = component.scope().to_string().to_lowercase(); - let description = component - .description() - .map(|d| d.to_lowercase()) - .unwrap_or_default(); - - component_name.contains(&filter) - || scope_name.contains(&filter) - || description.contains(&filter) - }) - .cloned() - .collect() - } - - fn scope_ordered_entries(&self) -> Vec { - use collections::HashMap; - - let mut scope_groups: HashMap< - ComponentScope, - Vec<(ComponentMetadata, Option>)>, - > = HashMap::default(); - let lowercase_filter = self.filter_text.to_lowercase(); - - for component in &self.components { - if self.filter_text.is_empty() { - scope_groups - .entry(component.scope()) - .or_insert_with(Vec::new) - .push((component.clone(), None)); - continue; - } - - // let full_component_name = component.name(); - let scopeless_name = component.scopeless_name(); - let scope_name = component.scope().to_string(); - let description = component.description().unwrap_or_default(); - - let lowercase_scopeless = scopeless_name.to_lowercase(); - let lowercase_scope = scope_name.to_lowercase(); - let lowercase_desc = description.to_lowercase(); - - if lowercase_scopeless.contains(&lowercase_filter) - && let Some(index) = lowercase_scopeless.find(&lowercase_filter) - { - let end = index + lowercase_filter.len(); - - if end <= scopeless_name.len() { - let mut positions = Vec::new(); - for i in index..end { - if scopeless_name.is_char_boundary(i) { - positions.push(i); - } - } - - if !positions.is_empty() { - scope_groups - .entry(component.scope()) - .or_insert_with(Vec::new) - .push((component.clone(), Some(positions))); - continue; - } - } - } - - if lowercase_scopeless.contains(&lowercase_filter) - || lowercase_scope.contains(&lowercase_filter) - || lowercase_desc.contains(&lowercase_filter) - { - scope_groups - .entry(component.scope()) - .or_insert_with(Vec::new) - .push((component.clone(), None)); - } - } - - // Sort the components in each group - for components in scope_groups.values_mut() { - components.sort_by_key(|(c, _)| c.sort_name()); - } - - let mut entries = Vec::new(); - - // Always show all components first - entries.push(PreviewEntry::AllComponents); - - let mut scopes: Vec<_> = scope_groups - .keys() - .filter(|scope| !matches!(**scope, ComponentScope::None)) - .cloned() - .collect(); - - scopes.sort_by_key(|s| s.to_string()); - - for scope in scopes { - if let Some(components) = scope_groups.remove(&scope) - && !components.is_empty() - { - entries.push(PreviewEntry::Separator); - entries.push(PreviewEntry::SectionHeader(scope.to_string().into())); - - let mut sorted_components = components; - sorted_components.sort_by_key(|(component, _)| component.sort_name()); - - for (component, positions) in sorted_components { - entries.push(PreviewEntry::Component(component, positions)); - } - } - } - - // Add uncategorized components last - if let Some(components) = scope_groups.get(&ComponentScope::None) - && !components.is_empty() - { - entries.push(PreviewEntry::Separator); - entries.push(PreviewEntry::SectionHeader("Uncategorized".into())); - let mut sorted_components = components.clone(); - sorted_components.sort_by_key(|(c, _)| c.sort_name()); - - for (component, positions) in sorted_components { - entries.push(PreviewEntry::Component(component, positions)); - } - } - - entries - } - - fn update_component_list(&mut self, cx: &mut Context) { - let entries = self.scope_ordered_entries(); - let new_len = entries.len(); - - if new_len > 0 { - self.nav_scroll_handle - .scroll_to_item(0, ScrollStrategy::Top); - } - - let filtered_components = self.filtered_components(); - - if !self.filter_text.is_empty() - && !matches!(self.active_page, PreviewPage::AllComponents) - && let PreviewPage::Component(ref component_id) = self.active_page - { - let component_still_visible = filtered_components - .iter() - .any(|component| component.id() == *component_id); - - if !component_still_visible { - if !filtered_components.is_empty() { - let first_component = &filtered_components[0]; - self.set_active_page(PreviewPage::Component(first_component.id()), cx); - } else { - self.set_active_page(PreviewPage::AllComponents, cx); - } - } - } - - self.component_list = ListState::new(new_len, gpui::ListAlignment::Top, px(1500.0)); - self.entries = entries; - - cx.emit(ItemEvent::UpdateTab); - } - - fn render_sidebar_entry( - &self, - ix: usize, - entry: &PreviewEntry, - cx: &Context, - ) -> impl IntoElement + use<> { - match entry { - PreviewEntry::Component(component_metadata, highlight_positions) => { - let id = component_metadata.id(); - let selected = self.active_page == PreviewPage::Component(id.clone()); - let name = component_metadata.scopeless_name(); - - ListItem::new(ix) - .child(if let Some(_positions) = highlight_positions { - let name_lower = name.to_lowercase(); - let filter_lower = self.filter_text.to_lowercase(); - let valid_positions = if let Some(start) = name_lower.find(&filter_lower) { - let end = start + filter_lower.len(); - (start..end).collect() - } else { - Vec::new() - }; - if valid_positions.is_empty() { - Label::new(name).into_any_element() - } else { - HighlightedLabel::new(name, valid_positions).into_any_element() - } - } else { - Label::new(name).into_any_element() - }) - .selectable(true) - .toggle_state(selected) - .inset(true) - .on_click(cx.listener(move |this, _, _, cx| { - let id = id.clone(); - this.set_active_page(PreviewPage::Component(id), cx); - })) - .into_any_element() - } - PreviewEntry::SectionHeader(shared_string) => ListSubHeader::new(shared_string) - .inset(true) - .into_any_element(), - PreviewEntry::AllComponents => { - let selected = self.active_page == PreviewPage::AllComponents; - - ListItem::new(ix) - .child(Label::new("All Components")) - .selectable(true) - .toggle_state(selected) - .inset(true) - .on_click(cx.listener(move |this, _, _, cx| { - this.set_active_page(PreviewPage::AllComponents, cx); - })) - .into_any_element() - } - PreviewEntry::Separator => ListItem::new(ix) - .disabled(true) - .child(div().w_full().py_2().child(Divider::horizontal())) - .into_any_element(), - } - } - - fn render_scope_header( - &self, - _ix: usize, - title: SharedString, - _window: &Window, - _cx: &App, - ) -> impl IntoElement { - h_flex() - .w_full() - .h_10() - .child(Headline::new(title).size(HeadlineSize::XSmall)) - .child(Divider::horizontal()) - } - - fn render_preview( - &self, - component: &ComponentMetadata, - window: &mut Window, - cx: &mut App, - ) -> impl IntoElement { - let name = component.scopeless_name(); - let scope = component.scope(); - - let description = component.description(); - - // Build the content container - let mut preview_container = v_flex().py_2().child( - v_flex() - .border_1() - .border_color(cx.theme().colors().border) - .rounded_sm() - .w_full() - .gap_4() - .py_4() - .px_6() - .flex_none() - .child( - v_flex() - .gap_1() - .child( - h_flex() - .gap_1() - .text_xl() - .child(div().child(name)) - .when(!matches!(scope, ComponentScope::None), |this| { - this.child(div().opacity(0.5).child(format!("({})", scope))) - }), - ) - .when_some(description, |this, description| { - this.child( - div() - .text_ui_sm(cx) - .text_color(cx.theme().colors().text_muted) - .max_w(px(600.0)) - .child(description), - ) - }), - ), - ); - - if let Some(preview) = component.preview() { - preview_container = preview_container.children(preview(window, cx)); - } - - preview_container.into_any_element() - } - - fn render_all_components(&self, cx: &Context) -> impl IntoElement { - v_flex() - .id("component-list") - .px_8() - .pt_4() - .size_full() - .child( - if self.filtered_components().is_empty() && !self.filter_text.is_empty() { - div() - .size_full() - .items_center() - .justify_center() - .text_color(cx.theme().colors().text_muted) - .child(format!("No components matching '{}'.", self.filter_text)) - .into_any_element() - } else { - list( - self.component_list.clone(), - cx.processor(|this, ix, window, cx| { - if ix >= this.entries.len() { - return div().w_full().h_0().into_any_element(); - } - - let entry = &this.entries[ix]; - - match entry { - PreviewEntry::Component(component, _) => this - .render_preview(component, window, cx) - .into_any_element(), - PreviewEntry::SectionHeader(shared_string) => this - .render_scope_header(ix, shared_string.clone(), window, cx) - .into_any_element(), - PreviewEntry::AllComponents => { - div().w_full().h_0().into_any_element() - } - PreviewEntry::Separator => div().w_full().h_0().into_any_element(), - } - }), - ) - .flex_grow() - .with_sizing_behavior(gpui::ListSizingBehavior::Auto) - .into_any_element() - }, - ) - } - - fn render_component_page( - &mut self, - component_id: &ComponentId, - _window: &mut Window, - _cx: &mut Context, - ) -> impl IntoElement { - let component = self.component_map.get(component_id); - - if let Some(component) = component { - v_flex() - .id("render-component-page") - .flex_1() - .child(ComponentPreviewPage::new(component.clone(), self.reset_key)) - .into_any_element() - } else { - v_flex() - .size_full() - .items_center() - .justify_center() - .child("Component not found") - .into_any_element() - } - } - - fn test_status_toast(&self, cx: &mut Context) { - if let Some(workspace) = self.workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - let status_toast = - StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| { - this.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)) - .action("Open Pull Request", |_, cx| { - cx.open_url("https://github.com/") - }) - }); - workspace.toggle_status_toast(status_toast, cx) - }); - } - } -} - -impl Render for ComponentPreview { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - // TODO: move this into the struct - let current_filter = self.filter_editor.update(cx, |input, cx| { - if input.is_empty(cx) { - String::new() - } else { - input.editor().read(cx).text(cx) - } - }); - - if current_filter != self.filter_text { - self.filter_text = current_filter; - self.update_component_list(cx); - } - let sidebar_entries = self.scope_ordered_entries(); - let active_page = self.active_page.clone(); - - h_flex() - .id("component-preview") - .key_context("ComponentPreview") - .items_start() - .overflow_hidden() - .size_full() - .track_focus(&self.focus_handle) - .bg(cx.theme().colors().editor_background) - .child( - v_flex() - .h_full() - .border_r_1() - .border_color(cx.theme().colors().border) - .child( - gpui::uniform_list( - "component-nav", - sidebar_entries.len(), - cx.processor(move |this, range: Range, _window, cx| { - range - .filter_map(|ix| { - if ix < sidebar_entries.len() { - Some(this.render_sidebar_entry( - ix, - &sidebar_entries[ix], - cx, - )) - } else { - None - } - }) - .collect() - }), - ) - .track_scroll(&self.nav_scroll_handle) - .p_2p5() - .w(px(231.)) // Matches perfectly with the size of the "Component Preview" tab, if that's the first one in the pane - .h_full() - .flex_1(), - ) - .child( - div() - .w_full() - .p_2p5() - .border_t_1() - .border_color(cx.theme().colors().border) - .child( - Button::new("toast-test", "Launch Toast") - .full_width() - .on_click(cx.listener({ - move |this, _, _window, cx| { - this.test_status_toast(cx); - cx.notify(); - } - })), - ), - ), - ) - .child( - v_flex() - .flex_1() - .size_full() - .child( - div() - .p_2() - .w_full() - .border_b_1() - .border_color(cx.theme().colors().border) - .child(self.filter_editor.clone()), - ) - .child( - div().id("content-area").flex_1().overflow_y_scroll().child( - match active_page { - PreviewPage::AllComponents => { - self.render_all_components(cx).into_any_element() - } - PreviewPage::Component(id) => self - .render_component_page(&id, window, cx) - .into_any_element(), - }, - ), - ), - ) - } -} - -impl EventEmitter for ComponentPreview {} - -impl Focusable for ComponentPreview { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ActivePageId(pub String); - -impl Default for ActivePageId { - fn default() -> Self { - ActivePageId("AllComponents".to_string()) - } -} - -impl From for ActivePageId { - fn from(id: ComponentId) -> Self { - Self(id.0.to_string()) - } -} - -impl Item for ComponentPreview { - type Event = ItemEvent; - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "Component Preview".into() - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - None - } - - fn show_toolbar(&self) -> bool { - false - } - - fn can_split(&self) -> bool { - true - } - - fn clone_on_split( - &self, - _workspace_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> Task>> - where - Self: Sized, - { - let language_registry = self.language_registry.clone(); - let user_store = self.user_store.clone(); - let weak_workspace = self.workspace.clone(); - let project = self.project.clone(); - let selected_index = self.cursor_index; - let active_page = self.active_page.clone(); - - let self_result = Self::new( - weak_workspace, - project, - language_registry, - user_store, - selected_index, - Some(active_page), - window, - cx, - ); - - Task::ready(match self_result { - Ok(preview) => Some(cx.new(|_cx| preview)), - Err(e) => { - log::error!("Failed to clone component preview: {}", e); - None - } - }) - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { - f(*event) - } - - fn added_to_workspace( - &mut self, - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, - ) { - self.workspace_id = workspace.database_id(); - - let focus_handle = self.filter_editor.read(cx).focus_handle(cx); - window.focus(&focus_handle); - } -} - -impl SerializableItem for ComponentPreview { - fn serialized_item_kind() -> &'static str { - "ComponentPreview" - } - - fn deserialize( - project: Entity, - workspace: WeakEntity, - workspace_id: WorkspaceId, - item_id: ItemId, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - let deserialized_active_page = - match COMPONENT_PREVIEW_DB.get_active_page(item_id, workspace_id) { - Ok(page) => { - if let Some(page) = page { - ActivePageId(page) - } else { - ActivePageId::default() - } - } - Err(_) => ActivePageId::default(), - }; - - let user_store = project.read(cx).user_store(); - let language_registry = project.read(cx).languages().clone(); - let preview_page = if deserialized_active_page.0 == ActivePageId::default().0 { - Some(PreviewPage::default()) - } else { - let component_str = deserialized_active_page.0; - let component_registry = components(); - let all_components = component_registry.components(); - let found_component = all_components.iter().find(|c| c.id().0 == component_str); - - if let Some(component) = found_component { - Some(PreviewPage::Component(component.id())) - } else { - Some(PreviewPage::default()) - } - }; - - window.spawn(cx, async move |cx| { - let user_store = user_store.clone(); - let language_registry = language_registry.clone(); - let weak_workspace = workspace.clone(); - let project = project.clone(); - cx.update(move |window, cx| { - Ok(cx.new(|cx| { - ComponentPreview::new( - weak_workspace, - project, - language_registry, - user_store, - None, - preview_page, - window, - cx, - ) - .expect("Failed to create component preview") - })) - })? - }) - } - - fn cleanup( - workspace_id: WorkspaceId, - alive_items: Vec, - _window: &mut Window, - cx: &mut App, - ) -> Task> { - delete_unloaded_items( - alive_items, - workspace_id, - "component_previews", - &COMPONENT_PREVIEW_DB, - cx, - ) - } - - fn serialize( - &mut self, - _workspace: &mut Workspace, - item_id: ItemId, - _closing: bool, - _window: &mut Window, - cx: &mut Context, - ) -> Option>> { - let active_page = self.active_page_id(cx); - let workspace_id = self.workspace_id?; - Some(cx.background_spawn(async move { - COMPONENT_PREVIEW_DB - .save_active_page(item_id, workspace_id, active_page.0) - .await - })) - } - - fn should_serialize(&self, event: &Self::Event) -> bool { - matches!(event, ItemEvent::UpdateTab) - } -} - -// TODO: use language registry to allow rendering markdown -#[derive(IntoElement)] -pub struct ComponentPreviewPage { - // languages: Arc, - component: ComponentMetadata, - reset_key: usize, -} - -impl ComponentPreviewPage { - pub fn new( - component: ComponentMetadata, - reset_key: usize, - // languages: Arc - ) -> Self { - Self { - // languages, - component, - reset_key, - } - } - - /// Renders the component status when it would be useful - /// - /// Doesn't render if the component is `ComponentStatus::Live` - /// as that is the default state - fn render_component_status(&self, cx: &App) -> Option { - let status = self.component.status(); - let status_description = status.description().to_string(); - - let color = match status { - ComponentStatus::Deprecated => Color::Error, - ComponentStatus::EngineeringReady => Color::Info, - ComponentStatus::Live => Color::Success, - ComponentStatus::WorkInProgress => Color::Warning, - }; - - if status != ComponentStatus::Live { - Some( - ButtonLike::new("component_status") - .child( - div() - .px_1p5() - .rounded_sm() - .bg(color.color(cx).alpha(0.12)) - .child( - Label::new(status.to_string()) - .size(LabelSize::Small) - .color(color), - ), - ) - .tooltip(Tooltip::text(status_description)) - .disabled(true), - ) - } else { - None - } - } - - fn render_header(&self, _: &Window, cx: &App) -> impl IntoElement { - v_flex() - .min_w_0() - .w_full() - .p_12() - .gap_6() - .bg(cx.theme().colors().surface_background) - .border_b_1() - .border_color(cx.theme().colors().border) - .child( - v_flex() - .gap_1() - .child( - Label::new(self.component.scope().to_string()) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( - h_flex() - .gap_2() - .child( - Headline::new(self.component.scopeless_name()) - .size(HeadlineSize::XLarge), - ) - .children(self.render_component_status(cx)), - ), - ) - .when_some(self.component.description(), |this, description| { - this.child(Label::new(description).size(LabelSize::Small)) - }) - } - - fn render_preview(&self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let content = if let Some(preview) = self.component.preview() { - // Fall back to component preview - preview(window, cx).unwrap_or_else(|| { - div() - .child("Failed to load preview. This path should be unreachable") - .into_any_element() - }) - } else { - div().child("No preview available").into_any_element() - }; - - v_flex() - .id(("component-preview", self.reset_key)) - .size_full() - .flex_1() - .px_12() - .py_6() - .bg(cx.theme().colors().editor_background) - .child(content) - } -} - -impl RenderOnce for ComponentPreviewPage { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - v_flex() - .size_full() - .flex_1() - .overflow_x_hidden() - .child(self.render_header(window, cx)) - .child(self.render_preview(window, cx)) - } -} diff --git a/crates/zed/src/zed/component_preview/persistence.rs b/crates/zed/src/zed/component_preview/persistence.rs deleted file mode 100644 index c37a4cc389..0000000000 --- a/crates/zed/src/zed/component_preview/persistence.rs +++ /dev/null @@ -1,59 +0,0 @@ -use anyhow::Result; -use db::{ - query, - sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, -}; -use workspace::{ItemId, WorkspaceDb, WorkspaceId}; - -pub struct ComponentPreviewDb(ThreadSafeConnection); - -impl Domain for ComponentPreviewDb { - const NAME: &str = stringify!(ComponentPreviewDb); - - const MIGRATIONS: &[&str] = &[sql!( - CREATE TABLE component_previews ( - workspace_id INTEGER, - item_id INTEGER UNIQUE, - active_page_id TEXT, - PRIMARY KEY(workspace_id, item_id), - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ) STRICT; - )]; -} - -db::static_connection!(COMPONENT_PREVIEW_DB, ComponentPreviewDb, [WorkspaceDb]); - -impl ComponentPreviewDb { - pub async fn save_active_page( - &self, - item_id: ItemId, - workspace_id: WorkspaceId, - active_page_id: String, - ) -> Result<()> { - log::debug!( - "Saving active page: item_id={item_id:?}, workspace_id={workspace_id:?}, active_page_id={active_page_id}" - ); - let query = "INSERT INTO component_previews(item_id, workspace_id, active_page_id) - VALUES (?1, ?2, ?3) - ON CONFLICT DO UPDATE SET - active_page_id = ?3"; - self.write(move |conn| { - let mut statement = Statement::prepare(conn, query)?; - let mut next_index = statement.bind(&item_id, 1)?; - next_index = statement.bind(&workspace_id, next_index)?; - statement.bind(&active_page_id, next_index)?; - statement.exec() - }) - .await - } - - query! { - pub fn get_active_page(item_id: ItemId, workspace_id: WorkspaceId) -> Result> { - SELECT active_page_id - FROM component_previews - WHERE item_id = ? AND workspace_id = ? - } - } -} diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs deleted file mode 100644 index 77a1f71596..0000000000 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ /dev/null @@ -1,256 +0,0 @@ -use client::{Client, UserStore}; -use codestral::CodestralEditPredictionDelegate; -use collections::HashMap; -use copilot::{Copilot, CopilotEditPredictionDelegate}; -use edit_prediction::{SweepFeatureFlag, ZedEditPredictionDelegate, Zeta2FeatureFlag}; -use editor::Editor; -use feature_flags::FeatureFlagAppExt; -use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, WeakEntity}; -use language::language_settings::{EditPredictionProvider, all_language_settings}; -use language_models::MistralLanguageModelProvider; -use settings::{ - EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME, - EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME, - EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME, SettingsStore, -}; -use std::{cell::RefCell, rc::Rc, sync::Arc}; -use supermaven::{Supermaven, SupermavenEditPredictionDelegate}; -use ui::Window; - -pub fn init(client: Arc, user_store: Entity, cx: &mut App) { - let editors: Rc, AnyWindowHandle>>> = Rc::default(); - cx.observe_new({ - let editors = editors.clone(); - let client = client.clone(); - let user_store = user_store.clone(); - move |editor: &mut Editor, window, cx: &mut Context| { - if !editor.mode().is_full() { - return; - } - - register_backward_compatible_actions(editor, cx); - - let Some(window) = window else { - return; - }; - - let editor_handle = cx.entity().downgrade(); - cx.on_release({ - let editor_handle = editor_handle.clone(); - let editors = editors.clone(); - move |_, _| { - editors.borrow_mut().remove(&editor_handle); - } - }) - .detach(); - - editors - .borrow_mut() - .insert(editor_handle, window.window_handle()); - let provider = all_language_settings(None, cx).edit_predictions.provider; - assign_edit_prediction_provider( - editor, - provider, - &client, - user_store.clone(), - window, - cx, - ); - } - }) - .detach(); - - cx.on_action(clear_edit_prediction_store_edit_history); - - let mut provider = all_language_settings(None, cx).edit_predictions.provider; - cx.subscribe(&user_store, { - let editors = editors.clone(); - let client = client.clone(); - - move |user_store, event, cx| { - if let client::user::Event::PrivateUserInfoUpdated = event { - assign_edit_prediction_providers(&editors, provider, &client, user_store, cx); - } - } - }) - .detach(); - - cx.observe_global::({ - let user_store = user_store.clone(); - move |cx| { - let new_provider = all_language_settings(None, cx).edit_predictions.provider; - - if new_provider != provider { - telemetry::event!( - "Edit Prediction Provider Changed", - from = provider, - to = new_provider, - ); - - provider = new_provider; - assign_edit_prediction_providers( - &editors, - provider, - &client, - user_store.clone(), - cx, - ); - } - } - }) - .detach(); -} - -fn clear_edit_prediction_store_edit_history(_: &edit_prediction::ClearHistory, cx: &mut App) { - if let Some(ep_store) = edit_prediction::EditPredictionStore::try_global(cx) { - ep_store.update(cx, |ep_store, _| ep_store.clear_history()); - } -} - -fn assign_edit_prediction_providers( - editors: &Rc, AnyWindowHandle>>>, - provider: EditPredictionProvider, - client: &Arc, - user_store: Entity, - cx: &mut App, -) { - if provider == EditPredictionProvider::Codestral { - let mistral = MistralLanguageModelProvider::global(client.http_client(), cx); - mistral.load_codestral_api_key(cx).detach(); - } - for (editor, window) in editors.borrow().iter() { - _ = window.update(cx, |_window, window, cx| { - _ = editor.update(cx, |editor, cx| { - assign_edit_prediction_provider( - editor, - provider, - client, - user_store.clone(), - window, - cx, - ); - }) - }); - } -} - -fn register_backward_compatible_actions(editor: &mut Editor, cx: &mut Context) { - // We renamed some of these actions to not be copilot-specific, but that - // would have not been backwards-compatible. So here we are re-registering - // the actions with the old names to not break people's keymaps. - editor - .register_action(cx.listener( - |editor, _: &copilot::Suggest, window: &mut Window, cx: &mut Context| { - editor.show_edit_prediction(&Default::default(), window, cx); - }, - )) - .detach(); - editor - .register_action(cx.listener( - |editor, _: &copilot::NextSuggestion, window: &mut Window, cx: &mut Context| { - editor.next_edit_prediction(&Default::default(), window, cx); - }, - )) - .detach(); - editor - .register_action(cx.listener( - |editor, - _: &copilot::PreviousSuggestion, - window: &mut Window, - cx: &mut Context| { - editor.previous_edit_prediction(&Default::default(), window, cx); - }, - )) - .detach(); -} - -fn assign_edit_prediction_provider( - editor: &mut Editor, - provider: EditPredictionProvider, - client: &Arc, - user_store: Entity, - window: &mut Window, - cx: &mut Context, -) { - // TODO: Do we really want to collect data only for singleton buffers? - let singleton_buffer = editor.buffer().read(cx).as_singleton(); - - match provider { - EditPredictionProvider::None => { - editor.set_edit_prediction_provider::(None, window, cx); - } - EditPredictionProvider::Copilot => { - if let Some(copilot) = Copilot::global(cx) { - if let Some(buffer) = singleton_buffer - && buffer.read(cx).file().is_some() - { - copilot.update(cx, |copilot, cx| { - copilot.register_buffer(&buffer, cx); - }); - } - let provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot)); - editor.set_edit_prediction_provider(Some(provider), window, cx); - } - } - EditPredictionProvider::Supermaven => { - if let Some(supermaven) = Supermaven::global(cx) { - let provider = cx.new(|_| SupermavenEditPredictionDelegate::new(supermaven)); - editor.set_edit_prediction_provider(Some(provider), window, cx); - } - } - EditPredictionProvider::Codestral => { - let http_client = client.http_client(); - let provider = cx.new(|_| CodestralEditPredictionDelegate::new(http_client)); - editor.set_edit_prediction_provider(Some(provider), window, cx); - } - value @ (EditPredictionProvider::Experimental(_) | EditPredictionProvider::Zed) => { - let ep_store = edit_prediction::EditPredictionStore::global(client, &user_store, cx); - - if let Some(project) = editor.project() - && let Some(buffer) = &singleton_buffer - && buffer.read(cx).file().is_some() - { - let has_model = ep_store.update(cx, |ep_store, cx| { - let model = if let EditPredictionProvider::Experimental(name) = value { - if name == EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME - && cx.has_flag::() - { - edit_prediction::EditPredictionModel::Sweep - } else if name == EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME - && cx.has_flag::() - { - edit_prediction::EditPredictionModel::Zeta2 - } else if name == EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME - && cx.has_flag::() - { - edit_prediction::EditPredictionModel::Mercury - } else { - return false; - } - } else if user_store.read(cx).current_user().is_some() { - edit_prediction::EditPredictionModel::Zeta1 - } else { - return false; - }; - - ep_store.set_edit_prediction_model(model); - ep_store.register_buffer(buffer, project, cx); - true - }); - - if has_model { - let provider = cx.new(|cx| { - ZedEditPredictionDelegate::new( - project.clone(), - singleton_buffer, - &client, - &user_store, - cx, - ) - }); - editor.set_edit_prediction_provider(Some(provider), window, cx); - } - } - } - } -} diff --git a/crates/zed/src/zed/mac_only_instance.rs b/crates/zed/src/zed/mac_only_instance.rs deleted file mode 100644 index b7898fae17..0000000000 --- a/crates/zed/src/zed/mac_only_instance.rs +++ /dev/null @@ -1,151 +0,0 @@ -use std::{ - io::{Read, Write}, - net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener, TcpStream}, - thread, - time::Duration, -}; - -use sysinfo::System; - -use release_channel::ReleaseChannel; - -const LOCALHOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1); -const CONNECT_TIMEOUT: Duration = Duration::from_millis(10); -const RECEIVE_TIMEOUT: Duration = Duration::from_millis(35); -const SEND_TIMEOUT: Duration = Duration::from_millis(20); -const USER_BLOCK: u16 = 100; - -fn address() -> SocketAddr { - // These port numbers are offset by the user ID to avoid conflicts between - // different users on the same machine. In addition to that the ports for each - // release channel are spaced out by 100 to avoid conflicts between different - // users running different release channels on the same machine. This ends up - // interleaving the ports between different users and different release channels. - // - // On macOS user IDs start at 501 and on Linux they start at 1000. The first user - // on a Mac with ID 501 running a dev channel build will use port 44238, and the - // second user with ID 502 will use port 44239, and so on. User 501 will use ports - // 44338, 44438, and 44538 for the preview, stable, and nightly channels, - // respectively. User 502 will use ports 44339, 44439, and 44539 for the preview, - // stable, and nightly channels, respectively. - let port = match *release_channel::RELEASE_CHANNEL { - ReleaseChannel::Dev => 43737, - ReleaseChannel::Preview => 43737 + USER_BLOCK, - ReleaseChannel::Stable => 43737 + (2 * USER_BLOCK), - ReleaseChannel::Nightly => 43737 + (3 * USER_BLOCK), - }; - let mut user_port = port; - let mut sys = System::new_all(); - sys.refresh_all(); - if let Ok(current_pid) = sysinfo::get_current_pid() - && let Some(uid) = sys - .process(current_pid) - .and_then(|process| process.user_id()) - { - let uid_u32 = get_uid_as_u32(uid); - // Ensure that the user ID is not too large to avoid overflow when - // calculating the port number. This seems unlikely but it doesn't - // hurt to be safe. - let max_port = 65535; - let max_uid: u32 = max_port - port as u32; - let wrapped_uid: u16 = (uid_u32 % max_uid) as u16; - user_port += wrapped_uid; - } - - SocketAddr::V4(SocketAddrV4::new(LOCALHOST, user_port)) -} - -#[cfg(unix)] -fn get_uid_as_u32(uid: &sysinfo::Uid) -> u32 { - *uid.clone() -} - -#[cfg(windows)] -fn get_uid_as_u32(uid: &sysinfo::Uid) -> u32 { - // Extract the RID which is an integer - uid.to_string() - .rsplit('-') - .next() - .and_then(|rid| rid.parse::().ok()) - .unwrap_or(0) -} - -fn instance_handshake() -> &'static str { - match *release_channel::RELEASE_CHANNEL { - ReleaseChannel::Dev => "Zed Editor Dev Instance Running", - ReleaseChannel::Nightly => "Zed Editor Nightly Instance Running", - ReleaseChannel::Preview => "Zed Editor Preview Instance Running", - ReleaseChannel::Stable => "Zed Editor Stable Instance Running", - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum IsOnlyInstance { - Yes, - No, -} - -pub fn ensure_only_instance() -> IsOnlyInstance { - if check_got_handshake() { - return IsOnlyInstance::No; - } - - let listener = match TcpListener::bind(address()) { - Ok(listener) => listener, - - Err(err) => { - log::warn!("Error binding to single instance port: {err}"); - if check_got_handshake() { - return IsOnlyInstance::No; - } - - // Avoid failing to start when some other application by chance already has - // a claim on the port. This is sub-par as any other instance that gets launched - // will be unable to communicate with this instance and will duplicate - log::warn!("Backup handshake request failed, continuing without handshake"); - return IsOnlyInstance::Yes; - } - }; - - thread::Builder::new() - .name("EnsureSingleton".to_string()) - .spawn(move || { - for stream in listener.incoming() { - let mut stream = match stream { - Ok(stream) => stream, - Err(_) => return, - }; - - _ = stream.set_nodelay(true); - _ = stream.set_read_timeout(Some(SEND_TIMEOUT)); - _ = stream.write_all(instance_handshake().as_bytes()); - } - }) - .unwrap(); - - IsOnlyInstance::Yes -} - -fn check_got_handshake() -> bool { - match TcpStream::connect_timeout(&address(), CONNECT_TIMEOUT) { - Ok(mut stream) => { - let mut buf = vec![0u8; instance_handshake().len()]; - - stream.set_read_timeout(Some(RECEIVE_TIMEOUT)).unwrap(); - if let Err(err) = stream.read_exact(&mut buf) { - log::warn!("Connected to single instance port but failed to read: {err}"); - return false; - } - - if buf == instance_handshake().as_bytes() { - log::info!("Got instance handshake"); - return true; - } - - log::warn!("Got wrong instance handshake value"); - false - } - - Err(_) => false, - } -} diff --git a/crates/zed/src/zed/migrate.rs b/crates/zed/src/zed/migrate.rs deleted file mode 100644 index 2452f17d04..0000000000 --- a/crates/zed/src/zed/migrate.rs +++ /dev/null @@ -1,323 +0,0 @@ -use anyhow::{Context as _, Result}; -use editor::Editor; -use fs::Fs; -use migrator::{migrate_keymap, migrate_settings}; -use settings::{KeymapFile, Settings, SettingsStore}; -use util::ResultExt; -use workspace::notifications::NotifyTaskExt; - -use std::sync::Arc; - -use gpui::{Entity, EventEmitter, Global, Task, TextStyle, TextStyleRefinement}; -use markdown::{Markdown, MarkdownElement, MarkdownStyle}; -use theme::ThemeSettings; -use ui::prelude::*; -use workspace::item::ItemHandle; -use workspace::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace}; - -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum MigrationType { - Keymap, - Settings, -} - -pub struct MigrationBanner { - migration_type: Option, - should_migrate_task: Option>, - markdown: Option>, -} - -pub enum MigrationEvent { - ContentChanged { - migration_type: MigrationType, - migrating_in_memory: bool, - }, -} - -pub struct MigrationNotification; - -impl EventEmitter for MigrationNotification {} - -impl MigrationNotification { - pub fn try_global(cx: &App) -> Option> { - cx.try_global::() - .map(|notifier| notifier.0.clone()) - } - - pub fn set_global(notifier: Entity, cx: &mut App) { - cx.set_global(GlobalMigrationNotification(notifier)); - } -} - -struct GlobalMigrationNotification(Entity); - -impl Global for GlobalMigrationNotification {} - -impl MigrationBanner { - pub fn new(_: &Workspace, cx: &mut Context) -> Self { - if let Some(notifier) = MigrationNotification::try_global(cx) { - cx.subscribe( - ¬ifier, - move |migrator_banner, _, event: &MigrationEvent, cx| { - migrator_banner.handle_notification(event, cx); - }, - ) - .detach(); - } - Self { - migration_type: None, - should_migrate_task: None, - markdown: None, - } - } - - fn handle_notification(&mut self, event: &MigrationEvent, cx: &mut Context) { - match event { - MigrationEvent::ContentChanged { - migration_type, - migrating_in_memory, - } => { - if *migrating_in_memory { - self.migration_type = Some(*migration_type); - self.show(cx); - } else { - cx.emit(ToolbarItemEvent::ChangeLocation( - ToolbarItemLocation::Hidden, - )); - self.reset(cx); - }; - } - } - } - - fn show(&mut self, cx: &mut Context) { - let (file_type, backup_file_name) = match self.migration_type { - Some(MigrationType::Keymap) => ( - "keymap", - paths::keymap_backup_file() - .file_name() - .unwrap_or_default() - .to_string_lossy() - .into_owned(), - ), - Some(MigrationType::Settings) => ( - "settings", - paths::settings_backup_file() - .file_name() - .unwrap_or_default() - .to_string_lossy() - .into_owned(), - ), - None => return, - }; - - let migration_text = format!( - "Your {} file uses deprecated settings which can be \ - automatically updated. A backup will be saved to `{}`", - file_type, backup_file_name - ); - - self.markdown = Some(cx.new(|cx| Markdown::new(migration_text.into(), None, None, cx))); - - cx.emit(ToolbarItemEvent::ChangeLocation( - ToolbarItemLocation::Secondary, - )); - cx.notify(); - } - - fn reset(&mut self, cx: &mut Context) { - self.should_migrate_task.take(); - self.migration_type.take(); - self.markdown.take(); - cx.notify(); - } -} - -impl EventEmitter for MigrationBanner {} - -impl ToolbarItemView for MigrationBanner { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - window: &mut Window, - cx: &mut Context, - ) -> ToolbarItemLocation { - self.reset(cx); - - let Some(target) = active_pane_item - .and_then(|item| item.act_as::(cx)) - .and_then(|editor| editor.update(cx, |editor, cx| editor.target_file_abs_path(cx))) - else { - return ToolbarItemLocation::Hidden; - }; - - if &target == paths::keymap_file() { - self.migration_type = Some(MigrationType::Keymap); - let fs = ::global(cx); - let should_migrate = cx.background_spawn(should_migrate_keymap(fs)); - self.should_migrate_task = Some(cx.spawn_in(window, async move |this, cx| { - if let Ok(true) = should_migrate.await { - this.update(cx, |this, cx| { - this.show(cx); - }) - .log_err(); - } - })); - } else if &target == paths::settings_file() { - self.migration_type = Some(MigrationType::Settings); - let fs = ::global(cx); - let should_migrate = cx.background_spawn(should_migrate_settings(fs)); - self.should_migrate_task = Some(cx.spawn_in(window, async move |this, cx| { - if let Ok(true) = should_migrate.await { - this.update(cx, |this, cx| { - this.show(cx); - }) - .log_err(); - } - })); - } - - ToolbarItemLocation::Hidden - } -} - -impl Render for MigrationBanner { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let migration_type = self.migration_type; - let settings = ThemeSettings::get_global(cx); - let ui_font_family = settings.ui_font.family.clone(); - let line_height = settings.ui_font_size(cx) * 1.3; - h_flex() - .py_1() - .pl_2() - .pr_1() - .justify_between() - .bg(cx.theme().status().info_background.opacity(0.6)) - .border_1() - .border_color(cx.theme().colors().border_variant) - .rounded_sm() - .child( - h_flex() - .gap_2() - .overflow_hidden() - .child( - Icon::new(IconName::Warning) - .size(IconSize::XSmall) - .color(Color::Warning), - ) - .child( - div() - .overflow_hidden() - .text_size(TextSize::Default.rems(cx)) - .max_h(2 * line_height) - .when_some(self.markdown.as_ref(), |this, markdown| { - this.child( - MarkdownElement::new( - markdown.clone(), - MarkdownStyle { - base_text_style: TextStyle { - color: cx.theme().colors().text, - font_family: ui_font_family, - ..Default::default() - }, - inline_code: TextStyleRefinement { - background_color: Some( - cx.theme().colors().background, - ), - ..Default::default() - }, - ..Default::default() - }, - ) - .into_any_element(), - ) - }), - ), - ) - .child( - Button::new("backup-and-migrate", "Backup and Update").on_click( - move |_, window, cx| { - let fs = ::global(cx); - match migration_type { - Some(MigrationType::Keymap) => { - cx.background_spawn(write_keymap_migration(fs.clone())) - .detach_and_notify_err(window, cx); - } - Some(MigrationType::Settings) => { - cx.background_spawn(write_settings_migration(fs.clone())) - .detach_and_notify_err(window, cx); - } - None => unreachable!(), - } - }, - ), - ) - .into_any_element() - } -} - -async fn should_migrate_keymap(fs: Arc) -> Result { - let old_text = KeymapFile::load_keymap_file(&fs).await?; - if let Ok(Some(_)) = migrate_keymap(&old_text) { - return Ok(true); - }; - Ok(false) -} - -async fn should_migrate_settings(fs: Arc) -> Result { - let old_text = SettingsStore::load_settings(&fs).await?; - if let Ok(Some(_)) = migrate_settings(&old_text) { - return Ok(true); - }; - Ok(false) -} - -async fn write_keymap_migration(fs: Arc) -> Result<()> { - let old_text = KeymapFile::load_keymap_file(&fs).await?; - let Ok(Some(new_text)) = migrate_keymap(&old_text) else { - return Ok(()); - }; - let keymap_path = paths::keymap_file().as_path(); - if fs.is_file(keymap_path).await { - fs.atomic_write(paths::keymap_backup_file().to_path_buf(), old_text) - .await - .with_context(|| "Failed to create settings backup in home directory".to_string())?; - let resolved_path = fs - .canonicalize(keymap_path) - .await - .with_context(|| format!("Failed to canonicalize keymap path {:?}", keymap_path))?; - fs.atomic_write(resolved_path.clone(), new_text) - .await - .with_context(|| format!("Failed to write keymap to file {:?}", resolved_path))?; - } else { - fs.atomic_write(keymap_path.to_path_buf(), new_text) - .await - .with_context(|| format!("Failed to write keymap to file {:?}", keymap_path))?; - } - Ok(()) -} - -async fn write_settings_migration(fs: Arc) -> Result<()> { - let old_text = SettingsStore::load_settings(&fs).await?; - let Ok(Some(new_text)) = migrate_settings(&old_text) else { - return Ok(()); - }; - let settings_path = paths::settings_file().as_path(); - if fs.is_file(settings_path).await { - fs.atomic_write(paths::settings_backup_file().to_path_buf(), old_text) - .await - .with_context(|| "Failed to create settings backup in home directory".to_string())?; - let resolved_path = fs - .canonicalize(settings_path) - .await - .with_context(|| format!("Failed to canonicalize settings path {:?}", settings_path))?; - fs.atomic_write(resolved_path.clone(), new_text) - .await - .with_context(|| format!("Failed to write settings to file {:?}", resolved_path))?; - } else { - fs.atomic_write(settings_path.to_path_buf(), new_text) - .await - .with_context(|| format!("Failed to write settings to file {:?}", settings_path))?; - } - Ok(()) -} diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs deleted file mode 100644 index 5e855aa5a9..0000000000 --- a/crates/zed/src/zed/open_listener.rs +++ /dev/null @@ -1,932 +0,0 @@ -use crate::handle_open_request; -use crate::restorable_workspace_locations; -use anyhow::{Context as _, Result, anyhow}; -use cli::{CliRequest, CliResponse, ipc::IpcSender}; -use cli::{IpcHandshake, ipc}; -use client::parse_zed_link; -use collections::HashMap; -use db::kvp::KEY_VALUE_STORE; -use editor::Editor; -use fs::Fs; -use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; -use futures::channel::{mpsc, oneshot}; -use futures::future::join_all; -use futures::{FutureExt, SinkExt, StreamExt}; -use git_ui::file_diff_view::FileDiffView; -use gpui::{App, AsyncApp, Global, WindowHandle}; -use language::Point; -use onboarding::FIRST_OPEN; -use onboarding::show_onboarding_view; -use recent_projects::{SshSettings, open_remote_project}; -use remote::{RemoteConnectionOptions, WslConnectionOptions}; -use settings::Settings; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::thread; -use std::time::Duration; -use util::ResultExt; -use util::paths::PathWithPosition; -use workspace::PathList; -use workspace::item::ItemHandle; -use workspace::{AppState, OpenOptions, SerializedWorkspaceLocation, Workspace}; - -#[derive(Default, Debug)] -pub struct OpenRequest { - pub kind: Option, - pub open_paths: Vec, - pub diff_paths: Vec<[String; 2]>, - pub open_channel_notes: Vec<(u64, Option)>, - pub join_channel: Option, - pub remote_connection: Option, -} - -#[derive(Debug)] -pub enum OpenRequestKind { - CliConnection((mpsc::Receiver, IpcSender)), - Extension { - extension_id: String, - }, - AgentPanel, - DockMenuAction { - index: usize, - }, - BuiltinJsonSchema { - schema_path: String, - }, - Setting { - /// `None` opens settings without navigating to a specific path. - setting_path: Option, - }, -} - -impl OpenRequest { - pub fn parse(request: RawOpenRequest, cx: &App) -> Result { - let mut this = Self::default(); - - this.diff_paths = request.diff_paths; - if let Some(wsl) = request.wsl { - let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') { - if user.is_empty() { - anyhow::bail!("user is empty in wsl argument"); - } - (Some(user.to_string()), distro.to_string()) - } else { - (None, wsl) - }; - this.remote_connection = Some(RemoteConnectionOptions::Wsl(WslConnectionOptions { - distro_name, - user, - })); - } - - for url in request.urls { - if let Some(server_name) = url.strip_prefix("zed-cli://") { - this.kind = Some(OpenRequestKind::CliConnection(connect_to_cli(server_name)?)); - } else if let Some(action_index) = url.strip_prefix("zed-dock-action://") { - this.kind = Some(OpenRequestKind::DockMenuAction { - index: action_index.parse()?, - }); - } else if let Some(file) = url.strip_prefix("file://") { - this.parse_file_path(file) - } else if let Some(file) = url.strip_prefix("zed://file") { - this.parse_file_path(file) - } else if let Some(file) = url.strip_prefix("zed://ssh") { - let ssh_url = "ssh:/".to_string() + file; - this.parse_ssh_file_path(&ssh_url, cx)? - } else if let Some(extension_id) = url.strip_prefix("zed://extension/") { - this.kind = Some(OpenRequestKind::Extension { - extension_id: extension_id.to_string(), - }); - } else if url == "zed://agent" { - this.kind = Some(OpenRequestKind::AgentPanel); - } else if let Some(schema_path) = url.strip_prefix("zed://schemas/") { - this.kind = Some(OpenRequestKind::BuiltinJsonSchema { - schema_path: schema_path.to_string(), - }); - } else if url == "zed://settings" || url == "zed://settings/" { - this.kind = Some(OpenRequestKind::Setting { setting_path: None }); - } else if let Some(setting_path) = url.strip_prefix("zed://settings/") { - this.kind = Some(OpenRequestKind::Setting { - setting_path: Some(setting_path.to_string()), - }); - } else if url.starts_with("ssh://") { - this.parse_ssh_file_path(&url, cx)? - } else if let Some(request_path) = parse_zed_link(&url, cx) { - this.parse_request_path(request_path).log_err(); - } else { - log::error!("unhandled url: {}", url); - } - } - - Ok(this) - } - - fn parse_file_path(&mut self, file: &str) { - if let Some(decoded) = urlencoding::decode(file).log_err() { - self.open_paths.push(decoded.into_owned()) - } - } - - fn parse_ssh_file_path(&mut self, file: &str, cx: &App) -> Result<()> { - let url = url::Url::parse(file)?; - let host = url - .host() - .with_context(|| format!("missing host in ssh url: {file}"))? - .to_string(); - let username = Some(url.username().to_string()).filter(|s| !s.is_empty()); - let port = url.port(); - anyhow::ensure!( - self.open_paths.is_empty(), - "cannot open both local and ssh paths" - ); - let mut connection_options = - SshSettings::get_global(cx).connection_options_for(host, port, username); - if let Some(password) = url.password() { - connection_options.password = Some(password.to_string()); - } - - let connection_options = RemoteConnectionOptions::Ssh(connection_options); - if let Some(ssh_connection) = &self.remote_connection { - anyhow::ensure!( - *ssh_connection == connection_options, - "cannot open multiple different remote connections" - ); - } - self.remote_connection = Some(connection_options); - self.parse_file_path(url.path()); - Ok(()) - } - - fn parse_request_path(&mut self, request_path: &str) -> Result<()> { - let mut parts = request_path.split('/'); - if parts.next() == Some("channel") - && let Some(slug) = parts.next() - && let Some(id_str) = slug.split('-').next_back() - && let Ok(channel_id) = id_str.parse::() - { - let Some(next) = parts.next() else { - self.join_channel = Some(channel_id); - return Ok(()); - }; - - if let Some(heading) = next.strip_prefix("notes#") { - self.open_channel_notes - .push((channel_id, Some(heading.to_string()))); - return Ok(()); - } - if next == "notes" { - self.open_channel_notes.push((channel_id, None)); - return Ok(()); - } - } - anyhow::bail!("invalid zed url: {request_path}") - } -} - -#[derive(Clone)] -pub struct OpenListener(UnboundedSender); - -#[derive(Default)] -pub struct RawOpenRequest { - pub urls: Vec, - pub diff_paths: Vec<[String; 2]>, - pub wsl: Option, -} - -impl Global for OpenListener {} - -impl OpenListener { - pub fn new() -> (Self, UnboundedReceiver) { - let (tx, rx) = mpsc::unbounded(); - (OpenListener(tx), rx) - } - - pub fn open(&self, request: RawOpenRequest) { - self.0 - .unbounded_send(request) - .context("no listener for open requests") - .log_err(); - } -} - -#[cfg(any(target_os = "linux", target_os = "freebsd"))] -pub fn listen_for_cli_connections(opener: OpenListener) -> Result<()> { - use release_channel::RELEASE_CHANNEL_NAME; - use std::os::unix::net::UnixDatagram; - - let sock_path = paths::data_dir().join(format!("zed-{}.sock", *RELEASE_CHANNEL_NAME)); - // remove the socket if the process listening on it has died - if let Err(e) = UnixDatagram::unbound()?.connect(&sock_path) - && e.kind() == std::io::ErrorKind::ConnectionRefused - { - std::fs::remove_file(&sock_path)?; - } - let listener = UnixDatagram::bind(&sock_path)?; - thread::spawn(move || { - let mut buf = [0u8; 1024]; - while let Ok(len) = listener.recv(&mut buf) { - opener.open(RawOpenRequest { - urls: vec![String::from_utf8_lossy(&buf[..len]).to_string()], - ..Default::default() - }); - } - }); - Ok(()) -} - -fn connect_to_cli( - server_name: &str, -) -> Result<(mpsc::Receiver, IpcSender)> { - let handshake_tx = cli::ipc::IpcSender::::connect(server_name.to_string()) - .context("error connecting to cli")?; - let (request_tx, request_rx) = ipc::channel::()?; - let (response_tx, response_rx) = ipc::channel::()?; - - handshake_tx - .send(IpcHandshake { - requests: request_tx, - responses: response_rx, - }) - .context("error sending ipc handshake")?; - - let (mut async_request_tx, async_request_rx) = - futures::channel::mpsc::channel::(16); - thread::spawn(move || { - while let Ok(cli_request) = request_rx.recv() { - if smol::block_on(async_request_tx.send(cli_request)).is_err() { - break; - } - } - anyhow::Ok(()) - }); - - Ok((async_request_rx, response_tx)) -} - -pub async fn open_paths_with_positions( - path_positions: &[PathWithPosition], - diff_paths: &[[String; 2]], - app_state: Arc, - open_options: workspace::OpenOptions, - cx: &mut AsyncApp, -) -> Result<( - WindowHandle, - Vec>>>, -)> { - let mut caret_positions = HashMap::default(); - - let paths = path_positions - .iter() - .map(|path_with_position| { - let path = path_with_position.path.clone(); - if let Some(row) = path_with_position.row - && path.is_file() - { - let row = row.saturating_sub(1); - let col = path_with_position.column.unwrap_or(0).saturating_sub(1); - caret_positions.insert(path.clone(), Point::new(row, col)); - } - path - }) - .collect::>(); - - let (workspace, mut items) = cx - .update(|cx| workspace::open_paths(&paths, app_state, open_options, cx))? - .await?; - - for diff_pair in diff_paths { - let old_path = Path::new(&diff_pair[0]).canonicalize()?; - let new_path = Path::new(&diff_pair[1]).canonicalize()?; - if let Ok(diff_view) = workspace.update(cx, |workspace, window, cx| { - FileDiffView::open(old_path, new_path, workspace, window, cx) - }) && let Some(diff_view) = diff_view.await.log_err() - { - items.push(Some(Ok(Box::new(diff_view)))) - } - } - - for (item, path) in items.iter_mut().zip(&paths) { - if let Some(Err(error)) = item { - *error = anyhow!("error opening {path:?}: {error}"); - continue; - } - let Some(Ok(item)) = item else { - continue; - }; - let Some(point) = caret_positions.remove(path) else { - continue; - }; - if let Some(active_editor) = item.downcast::() { - workspace - .update(cx, |_, window, cx| { - active_editor.update(cx, |editor, cx| { - editor.go_to_singleton_buffer_point(point, window, cx); - }); - }) - .log_err(); - } - } - - Ok((workspace, items)) -} - -pub async fn handle_cli_connection( - (mut requests, responses): (mpsc::Receiver, IpcSender), - app_state: Arc, - cx: &mut AsyncApp, -) { - if let Some(request) = requests.next().await { - match request { - CliRequest::Open { - urls, - paths, - diff_paths, - wait, - wsl, - open_new_workspace, - reuse, - env, - user_data_dir: _, - } => { - if !urls.is_empty() { - cx.update(|cx| { - match OpenRequest::parse( - RawOpenRequest { - urls, - diff_paths, - wsl, - }, - cx, - ) { - Ok(open_request) => { - handle_open_request(open_request, app_state.clone(), cx); - responses.send(CliResponse::Exit { status: 0 }).log_err(); - } - Err(e) => { - responses - .send(CliResponse::Stderr { - message: format!("{e}"), - }) - .log_err(); - responses.send(CliResponse::Exit { status: 1 }).log_err(); - } - }; - }) - .log_err(); - return; - } - - let open_workspace_result = open_workspaces( - paths, - diff_paths, - open_new_workspace, - reuse, - &responses, - wait, - app_state.clone(), - env, - cx, - ) - .await; - - let status = if open_workspace_result.is_err() { 1 } else { 0 }; - responses.send(CliResponse::Exit { status }).log_err(); - } - } - } -} - -async fn open_workspaces( - paths: Vec, - diff_paths: Vec<[String; 2]>, - open_new_workspace: Option, - reuse: bool, - responses: &IpcSender, - wait: bool, - app_state: Arc, - env: Option>, - cx: &mut AsyncApp, -) -> Result<()> { - let grouped_locations = if paths.is_empty() && diff_paths.is_empty() { - // If no paths are provided, restore from previous workspaces unless a new workspace is requested with -n - if open_new_workspace == Some(true) { - Vec::new() - } else { - restorable_workspace_locations(cx, &app_state) - .await - .unwrap_or_default() - } - } else { - vec![( - SerializedWorkspaceLocation::Local, - PathList::new(&paths.into_iter().map(PathBuf::from).collect::>()), - )] - }; - - if grouped_locations.is_empty() { - // If we have no paths to open, show the welcome screen if this is the first launch - if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { - cx.update(|cx| show_onboarding_view(app_state, cx).detach()) - .log_err(); - } - // If not the first launch, show an empty window with empty editor - else { - cx.update(|cx| { - let open_options = OpenOptions { - env, - ..Default::default() - }; - workspace::open_new(open_options, app_state, cx, |workspace, window, cx| { - Editor::new_file(workspace, &Default::default(), window, cx) - }) - .detach(); - }) - .log_err(); - } - } else { - // If there are paths to open, open a workspace for each grouping of paths - let mut errored = false; - - for (location, workspace_paths) in grouped_locations { - match location { - SerializedWorkspaceLocation::Local => { - let workspace_paths = workspace_paths - .paths() - .iter() - .map(|path| path.to_string_lossy().into_owned()) - .collect(); - - let workspace_failed_to_open = open_local_workspace( - workspace_paths, - diff_paths.clone(), - open_new_workspace, - reuse, - wait, - responses, - env.as_ref(), - &app_state, - cx, - ) - .await; - - if workspace_failed_to_open { - errored = true - } - } - SerializedWorkspaceLocation::Remote(mut connection) => { - let app_state = app_state.clone(); - if let RemoteConnectionOptions::Ssh(options) = &mut connection { - cx.update(|cx| { - SshSettings::get_global(cx) - .fill_connection_options_from_settings(options) - })?; - } - cx.spawn(async move |cx| { - open_remote_project( - connection, - workspace_paths.paths().to_vec(), - app_state, - OpenOptions::default(), - cx, - ) - .await - .log_err(); - }) - .detach(); - } - } - } - - anyhow::ensure!(!errored, "failed to open a workspace"); - } - - Ok(()) -} - -async fn open_local_workspace( - workspace_paths: Vec, - diff_paths: Vec<[String; 2]>, - open_new_workspace: Option, - reuse: bool, - wait: bool, - responses: &IpcSender, - env: Option<&HashMap>, - app_state: &Arc, - cx: &mut AsyncApp, -) -> bool { - let mut errored = false; - - let paths_with_position = - derive_paths_with_position(app_state.fs.as_ref(), workspace_paths).await; - - // Handle reuse flag by finding existing window to replace - let replace_window = if reuse { - cx.update(|cx| workspace::local_workspace_windows(cx).into_iter().next()) - .ok() - .flatten() - } else { - None - }; - - // For reuse, force new workspace creation but with replace_window set - let effective_open_new_workspace = if reuse { - Some(true) - } else { - open_new_workspace - }; - - match open_paths_with_positions( - &paths_with_position, - &diff_paths, - app_state.clone(), - workspace::OpenOptions { - open_new_workspace: effective_open_new_workspace, - replace_window, - prefer_focused_window: wait, - env: env.cloned(), - ..Default::default() - }, - cx, - ) - .await - { - Ok((workspace, items)) => { - let mut item_release_futures = Vec::new(); - - for item in items { - match item { - Some(Ok(item)) => { - cx.update(|cx| { - let released = oneshot::channel(); - item.on_release( - cx, - Box::new(move |_| { - let _ = released.0.send(()); - }), - ) - .detach(); - item_release_futures.push(released.1); - }) - .log_err(); - } - Some(Err(err)) => { - responses - .send(CliResponse::Stderr { - message: err.to_string(), - }) - .log_err(); - errored = true; - } - None => {} - } - } - - if wait { - let background = cx.background_executor().clone(); - let wait = async move { - if paths_with_position.is_empty() && diff_paths.is_empty() { - let (done_tx, done_rx) = oneshot::channel(); - let _subscription = workspace.update(cx, |_, _, cx| { - cx.on_release(move |_, _| { - let _ = done_tx.send(()); - }) - }); - let _ = done_rx.await; - } else { - let _ = futures::future::try_join_all(item_release_futures).await; - }; - } - .fuse(); - - futures::pin_mut!(wait); - - loop { - // Repeatedly check if CLI is still open to avoid wasting resources - // waiting for files or workspaces to close. - let mut timer = background.timer(Duration::from_secs(1)).fuse(); - futures::select_biased! { - _ = wait => break, - _ = timer => { - if responses.send(CliResponse::Ping).is_err() { - break; - } - } - } - } - } - } - Err(error) => { - errored = true; - responses - .send(CliResponse::Stderr { - message: format!("error opening {paths_with_position:?}: {error}"), - }) - .log_err(); - } - } - errored -} - -pub async fn derive_paths_with_position( - fs: &dyn Fs, - path_strings: impl IntoIterator>, -) -> Vec { - join_all(path_strings.into_iter().map(|path_str| async move { - let canonicalized = fs.canonicalize(Path::new(path_str.as_ref())).await; - (path_str, canonicalized) - })) - .await - .into_iter() - .map(|(original, canonicalized)| match canonicalized { - Ok(canonicalized) => PathWithPosition::from_path(canonicalized), - Err(_) => PathWithPosition::parse_str(original.as_ref()), - }) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::zed::{open_listener::open_local_workspace, tests::init_test}; - use cli::{ - CliResponse, - ipc::{self}, - }; - use editor::Editor; - use gpui::TestAppContext; - use language::LineEnding; - use remote::SshConnectionOptions; - use rope::Rope; - use serde_json::json; - use std::sync::Arc; - use util::path; - use workspace::{AppState, Workspace}; - - #[gpui::test] - fn test_parse_ssh_url(cx: &mut TestAppContext) { - let _app_state = init_test(cx); - let request = cx.update(|cx| { - OpenRequest::parse( - RawOpenRequest { - urls: vec!["ssh://me@localhost:/".into()], - ..Default::default() - }, - cx, - ) - .unwrap() - }); - assert_eq!( - request.remote_connection.unwrap(), - RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: "localhost".into(), - username: Some("me".into()), - port: None, - password: None, - args: None, - port_forwards: None, - nickname: None, - upload_binary_over_ssh: false, - }) - ); - assert_eq!(request.open_paths, vec!["/"]); - } - - #[gpui::test] - async fn test_open_workspace_with_directory(cx: &mut TestAppContext) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "dir1": { - "file1.txt": "content1", - "file2.txt": "content2", - }, - }), - ) - .await; - - assert_eq!(cx.windows().len(), 0); - - // First open the workspace directory - open_workspace_file(path!("/root/dir1"), None, app_state.clone(), cx).await; - - assert_eq!(cx.windows().len(), 1); - let workspace = cx.windows()[0].downcast::().unwrap(); - workspace - .update(cx, |workspace, _, cx| { - assert!(workspace.active_item_as::(cx).is_none()) - }) - .unwrap(); - - // Now open a file inside that workspace - open_workspace_file(path!("/root/dir1/file1.txt"), None, app_state.clone(), cx).await; - - assert_eq!(cx.windows().len(), 1); - workspace - .update(cx, |workspace, _, cx| { - assert!(workspace.active_item_as::(cx).is_some()); - }) - .unwrap(); - - // Now open a file inside that workspace, but tell Zed to open a new window - open_workspace_file( - path!("/root/dir1/file1.txt"), - Some(true), - app_state.clone(), - cx, - ) - .await; - - assert_eq!(cx.windows().len(), 2); - - let workspace_2 = cx.windows()[1].downcast::().unwrap(); - workspace_2 - .update(cx, |workspace, _, cx| { - assert!(workspace.active_item_as::(cx).is_some()); - let items = workspace.items(cx).collect::>(); - assert_eq!(items.len(), 1, "Workspace should have two items"); - }) - .unwrap(); - } - - #[gpui::test] - async fn test_open_workspace_with_nonexistent_files(cx: &mut TestAppContext) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree(path!("/root"), json!({})) - .await; - - assert_eq!(cx.windows().len(), 0); - - // Test case 1: Open a single file that does not exist yet - open_workspace_file(path!("/root/file5.txt"), None, app_state.clone(), cx).await; - - assert_eq!(cx.windows().len(), 1); - let workspace_1 = cx.windows()[0].downcast::().unwrap(); - workspace_1 - .update(cx, |workspace, _, cx| { - assert!(workspace.active_item_as::(cx).is_some()) - }) - .unwrap(); - - // Test case 2: Open a single file that does not exist yet, - // but tell Zed to add it to the current workspace - open_workspace_file(path!("/root/file6.txt"), Some(false), app_state.clone(), cx).await; - - assert_eq!(cx.windows().len(), 1); - workspace_1 - .update(cx, |workspace, _, cx| { - let items = workspace.items(cx).collect::>(); - assert_eq!(items.len(), 2, "Workspace should have two items"); - }) - .unwrap(); - - // Test case 3: Open a single file that does not exist yet, - // but tell Zed to NOT add it to the current workspace - open_workspace_file(path!("/root/file7.txt"), Some(true), app_state.clone(), cx).await; - - assert_eq!(cx.windows().len(), 2); - let workspace_2 = cx.windows()[1].downcast::().unwrap(); - workspace_2 - .update(cx, |workspace, _, cx| { - let items = workspace.items(cx).collect::>(); - assert_eq!(items.len(), 1, "Workspace should have two items"); - }) - .unwrap(); - } - - async fn open_workspace_file( - path: &str, - open_new_workspace: Option, - app_state: Arc, - cx: &TestAppContext, - ) { - let (response_tx, _) = ipc::channel::().unwrap(); - - let workspace_paths = vec![path.to_owned()]; - - let errored = cx - .spawn(|mut cx| async move { - open_local_workspace( - workspace_paths, - vec![], - open_new_workspace, - false, - false, - &response_tx, - None, - &app_state, - &mut cx, - ) - .await - }) - .await; - - assert!(!errored); - } - - #[gpui::test] - async fn test_reuse_flag_functionality(cx: &mut TestAppContext) { - let app_state = init_test(cx); - - let root_dir = if cfg!(windows) { "C:\\root" } else { "/root" }; - let file1_path = if cfg!(windows) { - "C:\\root\\file1.txt" - } else { - "/root/file1.txt" - }; - let file2_path = if cfg!(windows) { - "C:\\root\\file2.txt" - } else { - "/root/file2.txt" - }; - - app_state.fs.create_dir(Path::new(root_dir)).await.unwrap(); - app_state - .fs - .create_file(Path::new(file1_path), Default::default()) - .await - .unwrap(); - app_state - .fs - .save( - Path::new(file1_path), - &Rope::from("content1"), - LineEnding::Unix, - ) - .await - .unwrap(); - app_state - .fs - .create_file(Path::new(file2_path), Default::default()) - .await - .unwrap(); - app_state - .fs - .save( - Path::new(file2_path), - &Rope::from("content2"), - LineEnding::Unix, - ) - .await - .unwrap(); - - // First, open a workspace normally - let (response_tx, _response_rx) = ipc::channel::().unwrap(); - let workspace_paths = vec![file1_path.to_string()]; - - let _errored = cx - .spawn({ - let app_state = app_state.clone(); - let response_tx = response_tx.clone(); - |mut cx| async move { - open_local_workspace( - workspace_paths, - vec![], - None, - false, - false, - &response_tx, - None, - &app_state, - &mut cx, - ) - .await - } - }) - .await; - - // Now test the reuse functionality - should replace the existing workspace - let workspace_paths_reuse = vec![file1_path.to_string()]; - - let errored_reuse = cx - .spawn({ - let app_state = app_state.clone(); - let response_tx = response_tx.clone(); - |mut cx| async move { - open_local_workspace( - workspace_paths_reuse, - vec![], - None, // open_new_workspace will be overridden by reuse logic - true, // reuse = true - false, - &response_tx, - None, - &app_state, - &mut cx, - ) - .await - } - }) - .await; - - assert!(!errored_reuse); - } -} diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs deleted file mode 100644 index 2a52cc6972..0000000000 --- a/crates/zed/src/zed/quick_action_bar.rs +++ /dev/null @@ -1,730 +0,0 @@ -mod preview; -mod repl_menu; - -use agent_settings::AgentSettings; -use editor::actions::{ - AddSelectionAbove, AddSelectionBelow, CodeActionSource, DuplicateLineDown, GoToDiagnostic, - GoToHunk, GoToPreviousDiagnostic, GoToPreviousHunk, MoveLineDown, MoveLineUp, SelectAll, - SelectLargerSyntaxNode, SelectNext, SelectSmallerSyntaxNode, ToggleCodeActions, - ToggleDiagnostics, ToggleGoToLine, ToggleInlineDiagnostics, -}; -use editor::code_context_menus::{CodeContextMenu, ContextMenuOrigin}; -use editor::{Editor, EditorSettings}; -use gpui::{ - Action, AnchoredPositionMode, ClickEvent, Context, Corner, ElementId, Entity, EventEmitter, - FocusHandle, Focusable, InteractiveElement, ParentElement, Render, Styled, Subscription, - WeakEntity, Window, anchored, deferred, point, -}; -use project::{DisableAiSettings, project_settings::DiagnosticSeverity}; -use search::{BufferSearchBar, buffer_search}; -use settings::{Settings, SettingsStore}; -use ui::{ - ButtonStyle, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, IconButton, - IconName, IconSize, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*, -}; -use vim_mode_setting::{HelixModeSetting, VimModeSetting}; -use workspace::item::ItemBufferKind; -use workspace::{ - ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, item::ItemHandle, -}; -use zed_actions::{agent::AddSelectionToThread, assistant::InlineAssist, outline::ToggleOutline}; - -const MAX_CODE_ACTION_MENU_LINES: u32 = 16; - -pub struct QuickActionBar { - _inlay_hints_enabled_subscription: Option, - _ai_settings_subscription: Subscription, - active_item: Option>, - buffer_search_bar: Entity, - show: bool, - toggle_selections_handle: PopoverMenuHandle, - toggle_settings_handle: PopoverMenuHandle, - workspace: WeakEntity, -} - -impl QuickActionBar { - pub fn new( - buffer_search_bar: Entity, - workspace: &Workspace, - cx: &mut Context, - ) -> Self { - let mut was_agent_enabled = AgentSettings::get_global(cx).enabled(cx); - let mut was_agent_button = AgentSettings::get_global(cx).button; - - let ai_settings_subscription = cx.observe_global::(move |_, cx| { - let agent_settings = AgentSettings::get_global(cx); - let is_agent_enabled = agent_settings.enabled(cx); - - if was_agent_enabled != is_agent_enabled || was_agent_button != agent_settings.button { - was_agent_enabled = is_agent_enabled; - was_agent_button = agent_settings.button; - cx.notify(); - } - }); - - let mut this = Self { - _inlay_hints_enabled_subscription: None, - _ai_settings_subscription: ai_settings_subscription, - active_item: None, - buffer_search_bar, - show: true, - toggle_selections_handle: Default::default(), - toggle_settings_handle: Default::default(), - workspace: workspace.weak_handle(), - }; - this.apply_settings(cx); - cx.observe_global::(|this, cx| this.apply_settings(cx)) - .detach(); - this - } - - fn active_editor(&self) -> Option> { - self.active_item - .as_ref() - .and_then(|item| item.downcast::()) - } - - fn apply_settings(&mut self, cx: &mut Context) { - let new_show = EditorSettings::get_global(cx).toolbar.quick_actions; - if new_show != self.show { - self.show = new_show; - cx.emit(ToolbarItemEvent::ChangeLocation( - self.get_toolbar_item_location(), - )); - } - } - - fn get_toolbar_item_location(&self) -> ToolbarItemLocation { - if self.show && self.active_editor().is_some() { - ToolbarItemLocation::PrimaryRight - } else { - ToolbarItemLocation::Hidden - } - } -} - -impl Render for QuickActionBar { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let Some(editor) = self.active_editor() else { - return div().id("empty quick action bar"); - }; - - let supports_inlay_hints = editor.update(cx, |editor, cx| editor.supports_inlay_hints(cx)); - let editor_value = editor.read(cx); - let selection_menu_enabled = editor_value.selection_menu_enabled(cx); - let inlay_hints_enabled = editor_value.inlay_hints_enabled(); - let inline_values_enabled = editor_value.inline_values_enabled(); - let supports_diagnostics = editor_value.mode().is_full(); - let diagnostics_enabled = editor_value.diagnostics_max_severity != DiagnosticSeverity::Off; - let supports_inline_diagnostics = editor_value.inline_diagnostics_enabled(); - let inline_diagnostics_enabled = editor_value.show_inline_diagnostics(); - let git_blame_inline_enabled = editor_value.git_blame_inline_enabled(); - let show_git_blame_gutter = editor_value.show_git_blame_gutter(); - let auto_signature_help_enabled = editor_value.auto_signature_help_enabled(cx); - let show_line_numbers = editor_value.line_numbers_enabled(cx); - let has_edit_prediction_provider = editor_value.edit_prediction_provider().is_some(); - let show_edit_predictions = editor_value.edit_predictions_enabled(); - let edit_predictions_enabled_at_cursor = - editor_value.edit_predictions_enabled_at_cursor(cx); - let supports_minimap = editor_value.supports_minimap(cx); - let minimap_enabled = supports_minimap && editor_value.minimap().is_some(); - let has_available_code_actions = editor_value.has_available_code_actions(); - let code_action_enabled = editor_value.code_actions_enabled_for_toolbar(cx); - let focus_handle = editor_value.focus_handle(cx); - - let search_button = (editor.buffer_kind(cx) == ItemBufferKind::Singleton).then(|| { - QuickActionBarButton::new( - "toggle buffer search", - search::SEARCH_ICON, - !self.buffer_search_bar.read(cx).is_dismissed(), - Box::new(buffer_search::Deploy::find()), - focus_handle.clone(), - "Buffer Search", - { - let buffer_search_bar = self.buffer_search_bar.clone(); - move |_, window, cx| { - buffer_search_bar.update(cx, |search_bar, cx| { - search_bar.toggle(&buffer_search::Deploy::find(), window, cx) - }); - } - }, - ) - }); - - let assistant_button = QuickActionBarButton::new( - "toggle inline assistant", - IconName::ZedAssistant, - false, - Box::new(InlineAssist::default()), - focus_handle, - "Inline Assist", - move |_, window, cx| { - window.dispatch_action(Box::new(InlineAssist::default()), cx); - }, - ); - - let code_actions_dropdown = code_action_enabled.then(|| { - let focus = editor.focus_handle(cx); - let is_deployed = { - let menu_ref = editor.read(cx).context_menu().borrow(); - let code_action_menu = menu_ref - .as_ref() - .filter(|menu| matches!(menu, CodeContextMenu::CodeActions(..))); - code_action_menu - .as_ref() - .is_some_and(|menu| matches!(menu.origin(), ContextMenuOrigin::QuickActionBar)) - }; - let code_action_element = is_deployed - .then(|| { - editor.update(cx, |editor, cx| { - editor.render_context_menu(MAX_CODE_ACTION_MENU_LINES, window, cx) - }) - }) - .flatten(); - v_flex() - .child( - IconButton::new("toggle_code_actions_icon", IconName::BoltOutlined) - .icon_size(IconSize::Small) - .style(ButtonStyle::Subtle) - .disabled(!has_available_code_actions) - .toggle_state(is_deployed) - .when(!is_deployed, |this| { - this.when(has_available_code_actions, |this| { - this.tooltip(Tooltip::for_action_title( - "Code Actions", - &ToggleCodeActions::default(), - )) - }) - .when( - !has_available_code_actions, - |this| { - this.tooltip(Tooltip::for_action_title( - "No Code Actions Available", - &ToggleCodeActions::default(), - )) - }, - ) - }) - .on_click({ - let focus = focus; - move |_, window, cx| { - focus.dispatch_action( - &ToggleCodeActions { - deployed_from: Some(CodeActionSource::QuickActionBar), - quick_launch: false, - }, - window, - cx, - ); - } - }), - ) - .children(code_action_element.map(|menu| { - deferred( - anchored() - .position_mode(AnchoredPositionMode::Local) - .position(point(px(20.), px(20.))) - .anchor(Corner::TopRight) - .child(menu), - ) - })) - }); - - let editor_selections_dropdown = selection_menu_enabled.then(|| { - let has_diff_hunks = editor - .read(cx) - .buffer() - .read(cx) - .snapshot(cx) - .has_diff_hunks(); - let has_selection = editor.update(cx, |editor, cx| { - editor.has_non_empty_selection(&editor.display_snapshot(cx)) - }); - - let focus = editor.focus_handle(cx); - - let disable_ai = DisableAiSettings::get_global(cx).disable_ai; - - PopoverMenu::new("editor-selections-dropdown") - .trigger_with_tooltip( - IconButton::new("toggle_editor_selections_icon", IconName::CursorIBeam) - .icon_size(IconSize::Small) - .style(ButtonStyle::Subtle) - .toggle_state(self.toggle_selections_handle.is_deployed()), - Tooltip::text("Selection Controls"), - ) - .with_handle(self.toggle_selections_handle.clone()) - .anchor(Corner::TopRight) - .menu(move |window, cx| { - let focus = focus.clone(); - let menu = ContextMenu::build(window, cx, move |menu, _, _| { - menu.context(focus.clone()) - .action("Select All", Box::new(SelectAll)) - .action( - "Select Next Occurrence", - Box::new(SelectNext { - replace_newest: false, - }), - ) - .action("Expand Selection", Box::new(SelectLargerSyntaxNode)) - .action("Shrink Selection", Box::new(SelectSmallerSyntaxNode)) - .action( - "Add Cursor Above", - Box::new(AddSelectionAbove { - skip_soft_wrap: true, - }), - ) - .action( - "Add Cursor Below", - Box::new(AddSelectionBelow { - skip_soft_wrap: true, - }), - ) - .when(!disable_ai, |this| { - this.separator().action_disabled_when( - !has_selection, - "Add to Agent Thread", - Box::new(AddSelectionToThread), - ) - }) - .separator() - .action("Go to Symbol", Box::new(ToggleOutline)) - .action("Go to Line/Column", Box::new(ToggleGoToLine)) - .separator() - .action("Next Problem", Box::new(GoToDiagnostic::default())) - .action( - "Previous Problem", - Box::new(GoToPreviousDiagnostic::default()), - ) - .separator() - .action_disabled_when(!has_diff_hunks, "Next Hunk", Box::new(GoToHunk)) - .action_disabled_when( - !has_diff_hunks, - "Previous Hunk", - Box::new(GoToPreviousHunk), - ) - .separator() - .action("Move Line Up", Box::new(MoveLineUp)) - .action("Move Line Down", Box::new(MoveLineDown)) - .action("Duplicate Selection", Box::new(DuplicateLineDown)) - }); - Some(menu) - }) - }); - - let editor_focus_handle = editor.focus_handle(cx); - let editor = editor.downgrade(); - let editor_settings_dropdown = { - let vim_mode_enabled = VimModeSetting::get_global(cx).0; - let helix_mode_enabled = HelixModeSetting::get_global(cx).0; - - PopoverMenu::new("editor-settings") - .trigger_with_tooltip( - IconButton::new("toggle_editor_settings_icon", IconName::Sliders) - .icon_size(IconSize::Small) - .style(ButtonStyle::Subtle) - .toggle_state(self.toggle_settings_handle.is_deployed()), - Tooltip::text("Editor Controls"), - ) - .anchor(Corner::TopRight) - .with_handle(self.toggle_settings_handle.clone()) - .menu(move |window, cx| { - let menu = ContextMenu::build(window, cx, { - let focus_handle = editor_focus_handle.clone(); - |mut menu, _, _| { - menu = menu.context(focus_handle); - - if supports_inlay_hints { - menu = menu.toggleable_entry( - "Inlay Hints", - inlay_hints_enabled, - IconPosition::Start, - Some(editor::actions::ToggleInlayHints.boxed_clone()), - { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_inlay_hints( - &editor::actions::ToggleInlayHints, - window, - cx, - ); - }) - .ok(); - } - }, - ); - - menu = menu.toggleable_entry( - "Inline Values", - inline_values_enabled, - IconPosition::Start, - Some(editor::actions::ToggleInlineValues.boxed_clone()), - { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_inline_values( - &editor::actions::ToggleInlineValues, - window, - cx, - ); - }) - .ok(); - } - } - ); - } - - if supports_minimap { - menu = menu.toggleable_entry("Minimap", minimap_enabled, IconPosition::Start, Some(editor::actions::ToggleMinimap.boxed_clone()), { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_minimap( - &editor::actions::ToggleMinimap, - window, - cx, - ); - }) - .ok(); - } - },) - } - - if has_edit_prediction_provider { - let mut edit_prediction_entry = ContextMenuEntry::new("Edit Predictions") - .toggleable(IconPosition::Start, edit_predictions_enabled_at_cursor && show_edit_predictions) - .disabled(!edit_predictions_enabled_at_cursor) - .action( - editor::actions::ToggleEditPrediction.boxed_clone(), - ).handler({ - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_edit_predictions( - &editor::actions::ToggleEditPrediction, - window, - cx, - ); - }) - .ok(); - } - }); - if !edit_predictions_enabled_at_cursor { - edit_prediction_entry = edit_prediction_entry.documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, |_| { - Label::new("You can't toggle edit predictions for this file as it is within the excluded files list.").into_any_element() - }); - } - - menu = menu.item(edit_prediction_entry); - } - - menu = menu.separator(); - - if supports_diagnostics { - menu = menu.toggleable_entry( - "Diagnostics", - diagnostics_enabled, - IconPosition::Start, - Some(ToggleDiagnostics.boxed_clone()), - { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_diagnostics( - &ToggleDiagnostics, - window, - cx, - ); - }) - .ok(); - } - }, - ); - - if supports_inline_diagnostics { - let mut inline_diagnostics_item = ContextMenuEntry::new("Inline Diagnostics") - .toggleable(IconPosition::Start, diagnostics_enabled && inline_diagnostics_enabled) - .action(ToggleInlineDiagnostics.boxed_clone()) - .handler({ - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_inline_diagnostics( - &ToggleInlineDiagnostics, - window, - cx, - ); - }) - .ok(); - } - }); - if !diagnostics_enabled { - inline_diagnostics_item = inline_diagnostics_item.disabled(true).documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, |_| Label::new("Inline diagnostics are not available until regular diagnostics are enabled.").into_any_element()); - } - menu = menu.item(inline_diagnostics_item) - } - - menu = menu.separator(); - } - - menu = menu.toggleable_entry( - "Line Numbers", - show_line_numbers, - IconPosition::Start, - Some(editor::actions::ToggleLineNumbers.boxed_clone()), - { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_line_numbers( - &editor::actions::ToggleLineNumbers, - window, - cx, - ); - }) - .ok(); - } - }, - ); - - menu = menu.toggleable_entry( - "Selection Menu", - selection_menu_enabled, - IconPosition::Start, - Some(editor::actions::ToggleSelectionMenu.boxed_clone()), - { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_selection_menu( - &editor::actions::ToggleSelectionMenu, - window, - cx, - ) - }) - .ok(); - } - }, - ); - - menu = menu.toggleable_entry( - "Auto Signature Help", - auto_signature_help_enabled, - IconPosition::Start, - Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()), - { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_auto_signature_help_menu( - &editor::actions::ToggleAutoSignatureHelp, - window, - cx, - ); - }) - .ok(); - } - }, - ); - - menu = menu.separator(); - - menu = menu.toggleable_entry( - "Inline Git Blame", - git_blame_inline_enabled, - IconPosition::Start, - Some(editor::actions::ToggleGitBlameInline.boxed_clone()), - { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_git_blame_inline( - &editor::actions::ToggleGitBlameInline, - window, - cx, - ) - }) - .ok(); - } - }, - ); - - menu = menu.toggleable_entry( - "Column Git Blame", - show_git_blame_gutter, - IconPosition::Start, - Some(git::Blame.boxed_clone()), - { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_git_blame( - &git::Blame, - window, - cx, - ) - }) - .ok(); - } - }, - ); - - menu = menu.separator(); - - menu = menu.toggleable_entry( - "Vim Mode", - vim_mode_enabled, - IconPosition::Start, - None, - { - move |window, cx| { - let new_value = !vim_mode_enabled; - VimModeSetting::override_global(VimModeSetting(new_value), cx); - HelixModeSetting::override_global(HelixModeSetting(false), cx); - window.refresh(); - } - }, - ); - menu = menu.toggleable_entry( - "Helix Mode", - helix_mode_enabled, - IconPosition::Start, - None, - { - move |window, cx| { - let new_value = !helix_mode_enabled; - HelixModeSetting::override_global(HelixModeSetting(new_value), cx); - VimModeSetting::override_global(VimModeSetting(false), cx); - window.refresh(); - } - } - ); - - menu - } - }); - Some(menu) - }) - }; - - h_flex() - .id("quick action bar") - .gap(DynamicSpacing::Base01.rems(cx)) - .children(self.render_repl_menu(cx)) - .children(self.render_preview_button(self.workspace.clone(), cx)) - .children(search_button) - .when( - AgentSettings::get_global(cx).enabled(cx) && AgentSettings::get_global(cx).button, - |bar| bar.child(assistant_button), - ) - .children(code_actions_dropdown) - .children(editor_selections_dropdown) - .child(editor_settings_dropdown) - } -} - -impl EventEmitter for QuickActionBar {} - -#[derive(IntoElement)] -struct QuickActionBarButton { - id: ElementId, - icon: IconName, - toggled: bool, - action: Box, - focus_handle: FocusHandle, - tooltip: SharedString, - on_click: Box, -} - -impl QuickActionBarButton { - fn new( - id: impl Into, - icon: IconName, - toggled: bool, - action: Box, - focus_handle: FocusHandle, - tooltip: impl Into, - on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - Self { - id: id.into(), - icon, - toggled, - action, - focus_handle, - tooltip: tooltip.into(), - on_click: Box::new(on_click), - } - } -} - -impl RenderOnce for QuickActionBarButton { - fn render(self, _window: &mut Window, _: &mut App) -> impl IntoElement { - let tooltip = self.tooltip.clone(); - let action = self.action.boxed_clone(); - - IconButton::new(self.id.clone(), self.icon) - .icon_size(IconSize::Small) - .style(ButtonStyle::Subtle) - .toggle_state(self.toggled) - .tooltip(move |_window, cx| { - Tooltip::for_action_in(tooltip.clone(), &*action, &self.focus_handle, cx) - }) - .on_click(move |event, window, cx| (self.on_click)(event, window, cx)) - } -} - -impl ToolbarItemView for QuickActionBar { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - _: &mut Window, - cx: &mut Context, - ) -> ToolbarItemLocation { - self.active_item = active_pane_item.map(ItemHandle::boxed_clone); - if let Some(active_item) = active_pane_item { - self._inlay_hints_enabled_subscription.take(); - - if let Some(editor) = active_item.downcast::() { - let (mut inlay_hints_enabled, mut supports_inlay_hints) = - editor.update(cx, |editor, cx| { - ( - editor.inlay_hints_enabled(), - editor.supports_inlay_hints(cx), - ) - }); - self._inlay_hints_enabled_subscription = - Some(cx.observe(&editor, move |_, editor, cx| { - let (new_inlay_hints_enabled, new_supports_inlay_hints) = - editor.update(cx, |editor, cx| { - ( - editor.inlay_hints_enabled(), - editor.supports_inlay_hints(cx), - ) - }); - let should_notify = inlay_hints_enabled != new_inlay_hints_enabled - || supports_inlay_hints != new_supports_inlay_hints; - inlay_hints_enabled = new_inlay_hints_enabled; - supports_inlay_hints = new_supports_inlay_hints; - if should_notify { - cx.notify() - } - })); - } - } - self.get_toolbar_item_location() - } -} diff --git a/crates/zed/src/zed/quick_action_bar/preview.rs b/crates/zed/src/zed/quick_action_bar/preview.rs deleted file mode 100644 index 5d43e79542..0000000000 --- a/crates/zed/src/zed/quick_action_bar/preview.rs +++ /dev/null @@ -1,96 +0,0 @@ -use gpui::{AnyElement, Modifiers, WeakEntity}; -use markdown_preview::{ - OpenPreview as MarkdownOpenPreview, OpenPreviewToTheSide as MarkdownOpenPreviewToTheSide, - markdown_preview_view::MarkdownPreviewView, -}; -use svg_preview::{ - OpenPreview as SvgOpenPreview, OpenPreviewToTheSide as SvgOpenPreviewToTheSide, - svg_preview_view::SvgPreviewView, -}; -use ui::{Tooltip, prelude::*, text_for_keystroke}; -use workspace::Workspace; - -use super::QuickActionBar; - -#[derive(Clone, Copy)] -enum PreviewType { - Markdown, - Svg, -} - -impl QuickActionBar { - pub fn render_preview_button( - &self, - workspace_handle: WeakEntity, - cx: &mut Context, - ) -> Option { - let mut preview_type = None; - - if let Some(workspace) = self.workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - if MarkdownPreviewView::resolve_active_item_as_markdown_editor(workspace, cx) - .is_some() - { - preview_type = Some(PreviewType::Markdown); - } else if SvgPreviewView::resolve_active_item_as_svg_buffer(workspace, cx).is_some() - { - preview_type = Some(PreviewType::Svg); - } - }); - } - - let preview_type = preview_type?; - - let (button_id, tooltip_text, open_action, open_to_side_action, open_action_for_tooltip) = - match preview_type { - PreviewType::Markdown => ( - "toggle-markdown-preview", - "Preview Markdown", - Box::new(MarkdownOpenPreview) as Box, - Box::new(MarkdownOpenPreviewToTheSide) as Box, - &markdown_preview::OpenPreview as &dyn gpui::Action, - ), - PreviewType::Svg => ( - "toggle-svg-preview", - "Preview SVG", - Box::new(SvgOpenPreview) as Box, - Box::new(SvgOpenPreviewToTheSide) as Box, - &svg_preview::OpenPreview as &dyn gpui::Action, - ), - }; - - let alt_click = gpui::Keystroke { - key: "click".into(), - modifiers: Modifiers::alt(), - ..Default::default() - }; - - let button = IconButton::new(button_id, IconName::Eye) - .icon_size(IconSize::Small) - .style(ButtonStyle::Subtle) - .tooltip(move |_window, cx| { - Tooltip::with_meta( - tooltip_text, - Some(open_action_for_tooltip), - format!( - "{} to open in a split", - text_for_keystroke(&alt_click.modifiers, &alt_click.key, cx) - ), - cx, - ) - }) - .on_click(move |_, window, cx| { - if let Some(workspace) = workspace_handle.upgrade() { - workspace.update(cx, |_, cx| { - if window.modifiers().alt { - window.dispatch_action(open_to_side_action.boxed_clone(), cx); - } else { - window.dispatch_action(open_action.boxed_clone(), cx); - } - }); - } - }); - - Some(button.into_any_element()) - } -} diff --git a/crates/zed/src/zed/quick_action_bar/repl_menu.rs b/crates/zed/src/zed/quick_action_bar/repl_menu.rs deleted file mode 100644 index 1ebdf35bb9..0000000000 --- a/crates/zed/src/zed/quick_action_bar/repl_menu.rs +++ /dev/null @@ -1,474 +0,0 @@ -use gpui::ElementId; -use gpui::{AnyElement, Entity}; -use picker::Picker; -use repl::{ - ExecutionState, JupyterSettings, Kernel, KernelSpecification, KernelStatus, Session, - SessionSupport, - components::{KernelPickerDelegate, KernelSelector}, - worktree_id_for_editor, -}; -use ui::{ - ButtonLike, CommonAnimationExt, ContextMenu, IconWithIndicator, Indicator, IntoElement, - PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*, -}; -use util::ResultExt; - -use super::QuickActionBar; - -const ZED_REPL_DOCUMENTATION: &str = "https://zed.dev/docs/repl"; - -struct ReplMenuState { - tooltip: SharedString, - icon: IconName, - icon_color: Color, - icon_is_animating: bool, - popover_disabled: bool, - indicator: Option, - - status: KernelStatus, - kernel_name: SharedString, - kernel_language: SharedString, -} - -impl QuickActionBar { - pub fn render_repl_menu(&self, cx: &mut Context) -> Option { - if !JupyterSettings::enabled(cx) { - return None; - } - - let editor = self.active_editor()?; - - let is_local_project = editor - .read(cx) - .workspace() - .map(|workspace| workspace.read(cx).project().read(cx).is_local()) - .unwrap_or(false); - - if !is_local_project { - return None; - } - - let has_nonempty_selection = { - editor.update(cx, |this, cx| { - this.selections - .count() - .ne(&0) - .then(|| { - let snapshot = this.display_snapshot(cx); - let latest = this.selections.newest_display(&snapshot); - !latest.is_empty() - }) - .unwrap_or_default() - }) - }; - - let session = repl::session(editor.downgrade(), cx); - let session = match session { - SessionSupport::ActiveSession(session) => session, - SessionSupport::Inactive(spec) => { - return self.render_repl_launch_menu(spec, cx); - } - SessionSupport::RequiresSetup(language) => { - return self.render_repl_setup(language.as_ref(), cx); - } - SessionSupport::Unsupported => return None, - }; - - let menu_state = session_state(session.clone(), cx); - - let id = "repl-menu"; - - let element_id = |suffix| ElementId::Name(format!("{}-{}", id, suffix).into()); - - let editor = editor.downgrade(); - let dropdown_menu = PopoverMenu::new(element_id("menu")) - .menu(move |window, cx| { - let editor = editor.clone(); - let session = session.clone(); - ContextMenu::build(window, cx, move |menu, _, cx| { - let menu_state = session_state(session, cx); - let status = menu_state.status; - let editor = editor.clone(); - - menu.map(|menu| { - if status.is_connected() { - let status = status.clone(); - menu.custom_row(move |_window, _cx| { - h_flex() - .child( - Label::new(format!( - "kernel: {} ({})", - menu_state.kernel_name, menu_state.kernel_language - )) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .into_any_element() - }) - .custom_row(move |_window, _cx| { - h_flex() - .child( - Label::new(status.clone().to_string()) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .into_any_element() - }) - } else { - let status = status.clone(); - menu.custom_row(move |_window, _cx| { - h_flex() - .child( - Label::new(format!("{}...", status.to_string())) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .into_any_element() - }) - } - }) - .separator() - .custom_entry( - move |_window, _cx| { - Label::new(if has_nonempty_selection { - "Run Selection" - } else { - "Run Line" - }) - .into_any_element() - }, - { - let editor = editor.clone(); - move |window, cx| { - repl::run(editor.clone(), true, window, cx).log_err(); - } - }, - ) - .custom_entry( - move |_window, _cx| { - Label::new("Interrupt") - .size(LabelSize::Small) - .color(Color::Error) - .into_any_element() - }, - { - let editor = editor.clone(); - move |_, cx| { - repl::interrupt(editor.clone(), cx); - } - }, - ) - .custom_entry( - move |_window, _cx| { - Label::new("Clear Outputs") - .size(LabelSize::Small) - .color(Color::Muted) - .into_any_element() - }, - { - let editor = editor.clone(); - move |_, cx| { - repl::clear_outputs(editor.clone(), cx); - } - }, - ) - .separator() - .custom_entry( - move |_window, _cx| { - Label::new("Shut Down Kernel") - .size(LabelSize::Small) - .color(Color::Error) - .into_any_element() - }, - { - let editor = editor.clone(); - move |window, cx| { - repl::shutdown(editor.clone(), window, cx); - } - }, - ) - .custom_entry( - move |_window, _cx| { - Label::new("Restart Kernel") - .size(LabelSize::Small) - .color(Color::Error) - .into_any_element() - }, - { - move |window, cx| { - repl::restart(editor.clone(), window, cx); - } - }, - ) - .separator() - .action("View Sessions", Box::new(repl::Sessions)) - // TODO: Add shut down all kernels action - // .action("Shut Down all Kernels", Box::new(gpui::NoAction)) - }) - .into() - }) - .trigger_with_tooltip( - ButtonLike::new_rounded_right(element_id("dropdown")) - .child( - Icon::new(IconName::ChevronDown) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .width(rems(1.)) - .disabled(menu_state.popover_disabled), - Tooltip::text("REPL Menu"), - ); - - let button = ButtonLike::new_rounded_left("toggle_repl_icon") - .child(if menu_state.icon_is_animating { - Icon::new(menu_state.icon) - .color(menu_state.icon_color) - .with_rotate_animation(5) - .into_any_element() - } else { - IconWithIndicator::new( - Icon::new(IconName::ReplNeutral).color(menu_state.icon_color), - menu_state.indicator, - ) - .indicator_border_color(Some(cx.theme().colors().toolbar_background)) - .into_any_element() - }) - .size(ButtonSize::Compact) - .style(ButtonStyle::Subtle) - .tooltip(Tooltip::text(menu_state.tooltip)) - .on_click(|_, window, cx| window.dispatch_action(Box::new(repl::Run {}), cx)) - .into_any_element(); - - Some( - h_flex() - .child(self.render_kernel_selector(cx)) - .child(button) - .child(dropdown_menu) - .into_any_element(), - ) - } - pub fn render_repl_launch_menu( - &self, - kernel_specification: KernelSpecification, - cx: &mut Context, - ) -> Option { - let tooltip: SharedString = - SharedString::from(format!("Start REPL for {}", kernel_specification.name())); - - Some( - h_flex() - .child(self.render_kernel_selector(cx)) - .child( - IconButton::new("toggle_repl_icon", IconName::ReplNeutral) - .size(ButtonSize::Compact) - .icon_color(Color::Muted) - .style(ButtonStyle::Subtle) - .tooltip(Tooltip::text(tooltip)) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(repl::Run {}), cx) - }), - ) - .into_any_element(), - ) - } - - pub fn render_kernel_selector(&self, cx: &mut Context) -> impl IntoElement { - let editor = if let Some(editor) = self.active_editor() { - editor - } else { - return div().into_any_element(); - }; - - let Some(worktree_id) = worktree_id_for_editor(editor.downgrade(), cx) else { - return div().into_any_element(); - }; - - let session = repl::session(editor.downgrade(), cx); - - let current_kernelspec = match session { - SessionSupport::ActiveSession(session) => { - Some(session.read(cx).kernel_specification.clone()) - } - SessionSupport::Inactive(kernel_specification) => Some(kernel_specification), - SessionSupport::RequiresSetup(_language_name) => None, - SessionSupport::Unsupported => None, - }; - - let current_kernel_name = current_kernelspec.as_ref().map(|spec| spec.name()); - - let menu_handle: PopoverMenuHandle> = - PopoverMenuHandle::default(); - KernelSelector::new( - { - Box::new(move |kernelspec, window, cx| { - repl::assign_kernelspec(kernelspec, editor.downgrade(), window, cx).ok(); - }) - }, - worktree_id, - ButtonLike::new("kernel-selector") - .style(ButtonStyle::Subtle) - .size(ButtonSize::Compact) - .child( - h_flex() - .w_full() - .gap_0p5() - .child( - div() - .overflow_x_hidden() - .flex_grow() - .whitespace_nowrap() - .child( - Label::new(if let Some(name) = current_kernel_name { - name - } else { - SharedString::from("Select Kernel") - }) - .size(LabelSize::Small) - .color(if current_kernelspec.is_some() { - Color::Default - } else { - Color::Placeholder - }) - .into_any_element(), - ), - ) - .child( - Icon::new(IconName::ChevronDown) - .color(Color::Muted) - .size(IconSize::XSmall), - ), - ), - Tooltip::text("Select Kernel"), - ) - .with_handle(menu_handle) - .into_any_element() - } - - pub fn render_repl_setup(&self, language: &str, cx: &mut Context) -> Option { - let tooltip: SharedString = SharedString::from(format!("Setup Zed REPL for {}", language)); - Some( - h_flex() - .gap(DynamicSpacing::Base06.rems(cx)) - .child(self.render_kernel_selector(cx)) - .child( - IconButton::new("toggle_repl_icon", IconName::ReplNeutral) - .style(ButtonStyle::Subtle) - .shape(ui::IconButtonShape::Square) - .icon_size(ui::IconSize::Small) - .icon_color(Color::Muted) - .tooltip(Tooltip::text(tooltip)) - .on_click(|_, _window, cx| { - cx.open_url(&format!("{}#installation", ZED_REPL_DOCUMENTATION)) - }), - ) - .into_any_element(), - ) - } -} - -fn session_state(session: Entity, cx: &mut App) -> ReplMenuState { - let session = session.read(cx); - - let kernel_name = session.kernel_specification.name(); - let kernel_language: SharedString = session.kernel_specification.language(); - - let fill_fields = || { - ReplMenuState { - tooltip: "Nothing running".into(), - icon: IconName::ReplNeutral, - icon_color: Color::Default, - icon_is_animating: false, - popover_disabled: false, - indicator: None, - kernel_name: kernel_name.clone(), - kernel_language: kernel_language.clone(), - // TODO: Technically not shutdown, but indeterminate - status: KernelStatus::Shutdown, - // current_delta: Duration::default(), - } - }; - - let transitional = - |tooltip: SharedString, animating: bool, popover_disabled: bool| ReplMenuState { - tooltip, - icon_is_animating: animating, - popover_disabled, - icon_color: Color::Muted, - indicator: Some(Indicator::dot().color(Color::Muted)), - status: session.kernel.status(), - ..fill_fields() - }; - - let starting = || transitional(format!("{} is starting", kernel_name).into(), true, true); - let restarting = || transitional(format!("Restarting {}", kernel_name).into(), true, true); - let shutting_down = || { - transitional( - format!("{} is shutting down", kernel_name).into(), - false, - true, - ) - }; - let auto_restarting = || { - transitional( - format!("Auto-restarting {}", kernel_name).into(), - true, - true, - ) - }; - let unknown = || transitional(format!("{} state unknown", kernel_name).into(), false, true); - let other = |state: &str| { - transitional( - format!("{} state: {}", kernel_name, state).into(), - false, - true, - ) - }; - - let shutdown = || ReplMenuState { - tooltip: "Nothing running".into(), - icon: IconName::ReplNeutral, - icon_color: Color::Default, - icon_is_animating: false, - popover_disabled: false, - indicator: None, - status: KernelStatus::Shutdown, - ..fill_fields() - }; - - match &session.kernel { - Kernel::Restarting => restarting(), - Kernel::RunningKernel(kernel) => match &kernel.execution_state() { - ExecutionState::Idle => ReplMenuState { - tooltip: format!("Run code on {} ({})", kernel_name, kernel_language).into(), - indicator: Some(Indicator::dot().color(Color::Success)), - status: session.kernel.status(), - ..fill_fields() - }, - ExecutionState::Busy => ReplMenuState { - tooltip: format!("Interrupt {} ({})", kernel_name, kernel_language).into(), - icon_is_animating: true, - popover_disabled: false, - indicator: None, - status: session.kernel.status(), - ..fill_fields() - }, - ExecutionState::Unknown => unknown(), - ExecutionState::Starting => starting(), - ExecutionState::Restarting => restarting(), - ExecutionState::Terminating => shutting_down(), - ExecutionState::AutoRestarting => auto_restarting(), - ExecutionState::Dead => shutdown(), - ExecutionState::Other(state) => other(state), - }, - Kernel::StartingKernel(_) => starting(), - Kernel::ErroredLaunch(e) => ReplMenuState { - tooltip: format!("Error with kernel {}: {}", kernel_name, e).into(), - popover_disabled: false, - indicator: Some(Indicator::dot().color(Color::Error)), - status: session.kernel.status(), - ..fill_fields() - }, - Kernel::ShuttingDown => shutting_down(), - Kernel::Shutdown => shutdown(), - } -} diff --git a/crates/zed/src/zed/windows_only_instance.rs b/crates/zed/src/zed/windows_only_instance.rs deleted file mode 100644 index f3eab15441..0000000000 --- a/crates/zed/src/zed/windows_only_instance.rs +++ /dev/null @@ -1,217 +0,0 @@ -use std::{sync::Arc, thread::JoinHandle}; - -use anyhow::Context; -use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer}; -use parking_lot::Mutex; -use release_channel::app_identifier; -use util::ResultExt; -use windows::{ - Win32::{ - Foundation::{CloseHandle, ERROR_ALREADY_EXISTS, GENERIC_WRITE, GetLastError, HANDLE}, - Storage::FileSystem::{ - CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, OPEN_EXISTING, - PIPE_ACCESS_INBOUND, ReadFile, WriteFile, - }, - System::{ - Pipes::{ - ConnectNamedPipe, CreateNamedPipeW, DisconnectNamedPipe, PIPE_READMODE_MESSAGE, - PIPE_TYPE_MESSAGE, PIPE_WAIT, - }, - Threading::CreateMutexW, - }, - }, - core::HSTRING, -}; - -use crate::{Args, OpenListener, RawOpenRequest}; - -#[inline] -fn is_first_instance() -> bool { - unsafe { - CreateMutexW( - None, - false, - &HSTRING::from(format!("{}-Instance-Mutex", app_identifier())), - ) - .expect("Unable to create instance mutex.") - }; - unsafe { GetLastError() != ERROR_ALREADY_EXISTS } -} - -pub fn handle_single_instance(opener: OpenListener, args: &Args) -> bool { - let is_first_instance = is_first_instance(); - if is_first_instance { - // We are the first instance, listen for messages sent from other instances - std::thread::Builder::new() - .name("EnsureSingleton".to_owned()) - .spawn(move || { - with_pipe(|url| { - opener.open(RawOpenRequest { - urls: vec![url], - ..Default::default() - }) - }) - }) - .unwrap(); - } else if !args.foreground { - // We are not the first instance, send args to the first instance - send_args_to_instance(args).log_err(); - } - - is_first_instance -} - -fn with_pipe(f: impl Fn(String)) { - let pipe = unsafe { - CreateNamedPipeW( - &HSTRING::from(format!("\\\\.\\pipe\\{}-Named-Pipe", app_identifier())), - PIPE_ACCESS_INBOUND, - PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, - 1, - 128, - 128, - 0, - None, - ) - }; - if pipe.is_invalid() { - log::error!("Failed to create named pipe: {:?}", unsafe { - GetLastError() - }); - return; - } - - loop { - if let Some(message) = retrieve_message_from_pipe(pipe) - .context("Failed to read from named pipe") - .log_err() - { - f(message); - } - } -} - -fn retrieve_message_from_pipe(pipe: HANDLE) -> anyhow::Result { - unsafe { ConnectNamedPipe(pipe, None)? }; - let message = retrieve_message_from_pipe_inner(pipe); - unsafe { DisconnectNamedPipe(pipe).log_err() }; - message -} - -fn retrieve_message_from_pipe_inner(pipe: HANDLE) -> anyhow::Result { - let mut buffer = [0u8; 128]; - unsafe { - ReadFile(pipe, Some(&mut buffer), None, None)?; - } - let message = std::ffi::CStr::from_bytes_until_nul(&buffer)?; - Ok(message.to_string_lossy().into_owned()) -} - -// This part of code is mostly from crates/cli/src/main.rs -fn send_args_to_instance(args: &Args) -> anyhow::Result<()> { - if let Some(dock_menu_action_idx) = args.dock_action { - let url = format!("zed-dock-action://{}", dock_menu_action_idx); - return write_message_to_instance_pipe(url.as_bytes()); - } - - let (server, server_name) = - IpcOneShotServer::::new().context("Handshake before Zed spawn")?; - let url = format!("zed-cli://{server_name}"); - - let request = { - let mut paths = vec![]; - let mut urls = vec![]; - let mut diff_paths = vec![]; - for path in args.paths_or_urls.iter() { - match std::fs::canonicalize(&path) { - Ok(path) => paths.push(path.to_string_lossy().into_owned()), - Err(error) => { - if path.starts_with("zed://") - || path.starts_with("http://") - || path.starts_with("https://") - || path.starts_with("file://") - || path.starts_with("ssh://") - { - urls.push(path.clone()); - } else { - log::error!("error parsing path argument: {}", error); - } - } - } - } - - for path in args.diff.chunks(2) { - let old = std::fs::canonicalize(&path[0]).log_err(); - let new = std::fs::canonicalize(&path[1]).log_err(); - if let Some((old, new)) = old.zip(new) { - diff_paths.push([ - old.to_string_lossy().into_owned(), - new.to_string_lossy().into_owned(), - ]); - } - } - - CliRequest::Open { - paths, - urls, - diff_paths, - wait: false, - wsl: args.wsl.clone(), - open_new_workspace: None, - reuse: false, - env: None, - user_data_dir: args.user_data_dir.clone(), - } - }; - - let exit_status = Arc::new(Mutex::new(None)); - let sender: JoinHandle> = std::thread::Builder::new() - .name("CliReceiver".to_owned()) - .spawn({ - let exit_status = exit_status.clone(); - move || { - let (_, handshake) = server.accept().context("Handshake after Zed spawn")?; - let (tx, rx) = (handshake.requests, handshake.responses); - - tx.send(request)?; - - while let Ok(response) = rx.recv() { - match response { - CliResponse::Ping => {} - CliResponse::Stdout { message } => log::info!("{message}"), - CliResponse::Stderr { message } => log::error!("{message}"), - CliResponse::Exit { status } => { - exit_status.lock().replace(status); - return Ok(()); - } - } - } - Ok(()) - } - }) - .unwrap(); - - write_message_to_instance_pipe(url.as_bytes())?; - sender.join().unwrap()?; - if let Some(exit_status) = exit_status.lock().take() { - std::process::exit(exit_status); - } - Ok(()) -} - -fn write_message_to_instance_pipe(message: &[u8]) -> anyhow::Result<()> { - unsafe { - let pipe = CreateFileW( - &HSTRING::from(format!("\\\\.\\pipe\\{}-Named-Pipe", app_identifier())), - GENERIC_WRITE.0, - FILE_SHARE_MODE::default(), - None, - OPEN_EXISTING, - FILE_FLAGS_AND_ATTRIBUTES::default(), - None, - )?; - WriteFile(pipe, Some(message), None, None)?; - CloseHandle(pipe)?; - } - Ok(()) -} diff --git a/crates/zed_actions/Cargo.toml b/crates/zed_actions/Cargo.toml deleted file mode 100644 index 1a140c483f..0000000000 --- a/crates/zed_actions/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "zed_actions" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[dependencies] -gpui.workspace = true -schemars.workspace = true -serde.workspace = true -uuid.workspace = true diff --git a/crates/zed_actions/LICENSE-GPL b/crates/zed_actions/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/zed_actions/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs deleted file mode 100644 index a89e943e02..0000000000 --- a/crates/zed_actions/src/lib.rs +++ /dev/null @@ -1,597 +0,0 @@ -use gpui::{Action, actions}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -// If the zed binary doesn't use anything in this crate, it will be optimized away -// and the actions won't initialize. So we just provide an empty initialization function -// to be called from main. -// -// These may provide relevant context: -// https://github.com/rust-lang/rust/issues/47384 -// https://github.com/mmastrac/rust-ctor/issues/280 -pub fn init() {} - -/// Opens a URL in the system's default web browser. -#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct OpenBrowser { - pub url: String, -} - -/// Opens a zed:// URL within the application. -#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct OpenZedUrl { - pub url: String, -} - -/// Opens the keymap to either add a keybinding or change an existing one -#[derive(PartialEq, Clone, Default, Action, JsonSchema, Serialize, Deserialize)] -#[action(namespace = zed, no_json, no_register)] -pub struct ChangeKeybinding { - pub action: String, -} - -actions!( - zed, - [ - /// Opens the settings editor. - #[action(deprecated_aliases = ["zed_actions::OpenSettingsEditor"])] - OpenSettings, - /// Opens the settings JSON file. - #[action(deprecated_aliases = ["zed_actions::OpenSettings"])] - OpenSettingsFile, - /// Opens project-specific settings. - #[action(deprecated_aliases = ["zed_actions::OpenProjectSettings"])] - OpenProjectSettings, - /// Opens the default keymap file. - OpenDefaultKeymap, - /// Opens the user keymap file. - #[action(deprecated_aliases = ["zed_actions::OpenKeymap"])] - OpenKeymapFile, - /// Opens the keymap editor. - #[action(deprecated_aliases = ["zed_actions::OpenKeymapEditor"])] - OpenKeymap, - /// Opens account settings. - OpenAccountSettings, - /// Opens server settings. - OpenServerSettings, - /// Quits the application. - Quit, - /// Shows information about Zed. - About, - /// Opens the documentation website. - OpenDocs, - /// Views open source licenses. - OpenLicenses, - /// Opens the telemetry log. - OpenTelemetryLog, - /// Opens the performance profiler. - OpenPerformanceProfiler, - ] -); - -#[derive(PartialEq, Clone, Copy, Debug, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum ExtensionCategoryFilter { - Themes, - IconThemes, - Languages, - Grammars, - LanguageServers, - ContextServers, - AgentServers, - SlashCommands, - IndexedDocsProviders, - Snippets, - DebugAdapters, -} - -/// Opens the extensions management interface. -#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct Extensions { - /// Filters the extensions page down to extensions that are in the specified category. - #[serde(default)] - pub category_filter: Option, - /// Focuses just the extension with the specified ID. - #[serde(default)] - pub id: Option, -} - -/// Decreases the font size in the editor buffer. -#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct DecreaseBufferFontSize { - #[serde(default)] - pub persist: bool, -} - -/// Increases the font size in the editor buffer. -#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct IncreaseBufferFontSize { - #[serde(default)] - pub persist: bool, -} - -/// Increases the font size in the editor buffer. -#[derive(PartialEq, Clone, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct OpenSettingsAt { - /// A path to a specific setting (e.g. `theme.mode`) - pub path: String, -} - -/// Resets the buffer font size to the default value. -#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct ResetBufferFontSize { - #[serde(default)] - pub persist: bool, -} - -/// Decreases the font size of the user interface. -#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct DecreaseUiFontSize { - #[serde(default)] - pub persist: bool, -} - -/// Increases the font size of the user interface. -#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct IncreaseUiFontSize { - #[serde(default)] - pub persist: bool, -} - -/// Resets the UI font size to the default value. -#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct ResetUiFontSize { - #[serde(default)] - pub persist: bool, -} - -/// Resets all zoom levels (UI and buffer font sizes, including in the agent panel) to their default values. -#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct ResetAllZoom { - #[serde(default)] - pub persist: bool, -} - -pub mod dev { - use gpui::actions; - - actions!( - dev, - [ - /// Toggles the developer inspector for debugging UI elements. - ToggleInspector - ] - ); -} - -pub mod workspace { - use gpui::actions; - - actions!( - workspace, - [ - #[action(deprecated_aliases = ["editor::CopyPath", "outline_panel::CopyPath", "project_panel::CopyPath"])] - CopyPath, - #[action(deprecated_aliases = ["editor::CopyRelativePath", "outline_panel::CopyRelativePath", "project_panel::CopyRelativePath"])] - CopyRelativePath, - /// Opens the selected file with the system's default application. - #[action(deprecated_aliases = ["project_panel::OpenWithSystem"])] - OpenWithSystem, - ] - ); -} - -pub mod git { - use gpui::actions; - - actions!( - git, - [ - /// Checks out a different git branch. - CheckoutBranch, - /// Switches to a different git branch. - Switch, - /// Selects a different repository. - SelectRepo, - /// Filter remotes. - FilterRemotes, - /// Create a git remote. - CreateRemote, - /// Opens the git branch selector. - #[action(deprecated_aliases = ["branches::OpenRecent"])] - Branch, - /// Opens the git stash selector. - ViewStash, - /// Opens the git worktree selector. - Worktree - ] - ); -} - -pub mod toast { - use gpui::actions; - - actions!( - toast, - [ - /// Runs the action associated with a toast notification. - RunAction - ] - ); -} - -pub mod command_palette { - use gpui::actions; - - actions!( - command_palette, - [ - /// Toggles the command palette. - Toggle, - ] - ); -} - -pub mod project_panel { - use gpui::actions; - - actions!( - project_panel, - [ - /// Toggles focus on the project panel. - ToggleFocus - ] - ); -} -pub mod feedback { - use gpui::actions; - - actions!( - feedback, - [ - /// Opens email client to send feedback to Zed support. - EmailZed, - /// Opens the bug report form. - FileBugReport, - /// Opens the feature request form. - RequestFeature - ] - ); -} - -pub mod theme_selector { - use gpui::Action; - use schemars::JsonSchema; - use serde::Deserialize; - - /// Toggles the theme selector interface. - #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] - #[action(namespace = theme_selector)] - #[serde(deny_unknown_fields)] - pub struct Toggle { - /// A list of theme names to filter the theme selector down to. - pub themes_filter: Option>, - } -} - -pub mod icon_theme_selector { - use gpui::Action; - use schemars::JsonSchema; - use serde::Deserialize; - - /// Toggles the icon theme selector interface. - #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] - #[action(namespace = icon_theme_selector)] - #[serde(deny_unknown_fields)] - pub struct Toggle { - /// A list of icon theme names to filter the theme selector down to. - pub themes_filter: Option>, - } -} - -pub mod settings_profile_selector { - use gpui::Action; - use schemars::JsonSchema; - use serde::Deserialize; - - #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] - #[action(namespace = settings_profile_selector)] - pub struct Toggle; -} - -pub mod agent { - use gpui::actions; - - actions!( - agent, - [ - /// Opens the agent settings panel. - #[action(deprecated_aliases = ["agent::OpenConfiguration"])] - OpenSettings, - /// Opens the agent onboarding modal. - OpenOnboardingModal, - /// Opens the ACP onboarding modal. - OpenAcpOnboardingModal, - /// Opens the Claude Code onboarding modal. - OpenClaudeCodeOnboardingModal, - /// Resets the agent onboarding state. - ResetOnboarding, - /// Starts a chat conversation with the agent. - Chat, - /// Toggles the language model selector dropdown. - #[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])] - ToggleModelSelector, - /// Triggers re-authentication on Gemini - ReauthenticateAgent, - /// Add the current selection as context for threads in the agent panel. - #[action(deprecated_aliases = ["assistant::QuoteSelection", "agent::QuoteSelection"])] - AddSelectionToThread, - /// Resets the agent panel zoom levels (agent UI and buffer font sizes). - ResetAgentZoom, - ] - ); -} - -pub mod assistant { - use gpui::{Action, actions}; - use schemars::JsonSchema; - use serde::Deserialize; - use uuid::Uuid; - - actions!( - agent, - [ - #[action(deprecated_aliases = ["assistant::ToggleFocus"])] - ToggleFocus - ] - ); - - actions!( - assistant, - [ - /// Shows the assistant configuration panel. - ShowConfiguration - ] - ); - - /// Opens the rules library for managing agent rules and prompts. - #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] - #[action(namespace = agent, deprecated_aliases = ["assistant::OpenRulesLibrary", "assistant::DeployPromptLibrary"])] - #[serde(deny_unknown_fields)] - pub struct OpenRulesLibrary { - #[serde(skip)] - pub prompt_to_select: Option, - } - - /// Deploys the assistant interface with the specified configuration. - #[derive(Clone, Default, Deserialize, PartialEq, JsonSchema, Action)] - #[action(namespace = assistant)] - #[serde(deny_unknown_fields)] - pub struct InlineAssist { - pub prompt: Option, - } -} - -pub mod debugger { - use gpui::actions; - - actions!( - debugger, - [ - /// Opens the debugger onboarding modal. - OpenOnboardingModal, - /// Resets the debugger onboarding state. - ResetOnboarding - ] - ); -} - -/// Opens the recent projects interface. -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] -#[action(namespace = projects)] -#[serde(deny_unknown_fields)] -pub struct OpenRecent { - #[serde(default)] - pub create_new_window: bool, -} - -/// Creates a project from a selected template. -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] -#[action(namespace = projects)] -#[serde(deny_unknown_fields)] -pub struct OpenRemote { - #[serde(default)] - pub from_existing_connection: bool, - #[serde(default)] - pub create_new_window: bool, -} - -/// Opens the dev container connection modal. -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] -#[action(namespace = projects)] -#[serde(deny_unknown_fields)] -pub struct OpenDevContainer; - -/// Where to spawn the task in the UI. -#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum RevealTarget { - /// In the central pane group, "main" editor area. - Center, - /// In the terminal dock, "regular" terminal items' place. - #[default] - Dock, -} - -/// Spawns a task with name or opens tasks modal. -#[derive(Debug, PartialEq, Clone, Deserialize, JsonSchema, Action)] -#[action(namespace = task)] -#[serde(untagged)] -pub enum Spawn { - /// Spawns a task by the name given. - ByName { - task_name: String, - #[serde(default)] - reveal_target: Option, - }, - /// Spawns a task by the name given. - ByTag { - task_tag: String, - #[serde(default)] - reveal_target: Option, - }, - /// Spawns a task via modal's selection. - ViaModal { - /// Selected task's `reveal_target` property override. - #[serde(default)] - reveal_target: Option, - }, -} - -impl Spawn { - pub fn modal() -> Self { - Self::ViaModal { - reveal_target: None, - } - } -} - -/// Reruns the last task. -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] -#[action(namespace = task)] -#[serde(deny_unknown_fields)] -pub struct Rerun { - /// Controls whether the task context is reevaluated prior to execution of a task. - /// If it is not, environment variables such as ZED_COLUMN, ZED_FILE are gonna be the same as in the last execution of a task - /// If it is, these variables will be updated to reflect current state of editor at the time task::Rerun is executed. - /// default: false - #[serde(default)] - pub reevaluate_context: bool, - /// Overrides `allow_concurrent_runs` property of the task being reran. - /// Default: null - #[serde(default)] - pub allow_concurrent_runs: Option, - /// Overrides `use_new_terminal` property of the task being reran. - /// Default: null - #[serde(default)] - pub use_new_terminal: Option, - - /// If present, rerun the task with this ID, otherwise rerun the last task. - #[serde(skip)] - pub task_id: Option, -} - -pub mod outline { - use std::sync::OnceLock; - - use gpui::{AnyView, App, Window, actions}; - - actions!( - outline, - [ - #[action(name = "Toggle")] - ToggleOutline - ] - ); - /// A pointer to outline::toggle function, exposed here to sewer the breadcrumbs <-> outline dependency. - pub static TOGGLE_OUTLINE: OnceLock = OnceLock::new(); -} - -actions!( - zed_predict_onboarding, - [ - /// Opens the Zed Predict onboarding modal. - OpenZedPredictOnboarding - ] -); -actions!( - git_onboarding, - [ - /// Opens the git integration onboarding modal. - OpenGitIntegrationOnboarding - ] -); - -actions!( - debug_panel, - [ - /// Toggles focus on the debug panel. - ToggleFocus - ] -); -actions!( - debugger, - [ - /// Toggles the enabled state of a breakpoint. - ToggleEnableBreakpoint, - /// Removes a breakpoint. - UnsetBreakpoint, - /// Opens the project debug tasks configuration. - OpenProjectDebugTasks, - ] -); - -pub mod vim { - use gpui::actions; - - actions!( - vim, - [ - /// Opens the default keymap file. - OpenDefaultKeymap - ] - ); -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct WslConnectionOptions { - pub distro_name: String, - pub user: Option, -} - -#[cfg(target_os = "windows")] -pub mod wsl_actions { - use gpui::Action; - use schemars::JsonSchema; - use serde::Deserialize; - - /// Opens a folder inside Wsl. - #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] - #[action(namespace = projects)] - #[serde(deny_unknown_fields)] - pub struct OpenFolderInWsl { - #[serde(default)] - pub create_new_window: bool, - } - - /// Open a wsl distro. - #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] - #[action(namespace = projects)] - #[serde(deny_unknown_fields)] - pub struct OpenWsl { - #[serde(default)] - pub create_new_window: bool, - } -} diff --git a/crates/zed_env_vars/Cargo.toml b/crates/zed_env_vars/Cargo.toml deleted file mode 100644 index 1cf32174c3..0000000000 --- a/crates/zed_env_vars/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "zed_env_vars" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/zed_env_vars.rs" - -[features] -default = [] - -[dependencies] -gpui.workspace = true diff --git a/crates/zed_env_vars/LICENSE-GPL b/crates/zed_env_vars/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/zed_env_vars/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/zed_env_vars/src/zed_env_vars.rs b/crates/zed_env_vars/src/zed_env_vars.rs deleted file mode 100644 index 53b9c22bb2..0000000000 --- a/crates/zed_env_vars/src/zed_env_vars.rs +++ /dev/null @@ -1,44 +0,0 @@ -use gpui::SharedString; -use std::sync::LazyLock; - -/// Whether Zed is running in stateless mode. -/// When true, Zed will use in-memory databases instead of persistent storage. -pub static ZED_STATELESS: LazyLock = bool_env_var!("ZED_STATELESS"); - -pub struct EnvVar { - pub name: SharedString, - /// Value of the environment variable. Also `None` when set to an empty string. - pub value: Option, -} - -impl EnvVar { - pub fn new(name: SharedString) -> Self { - let value = std::env::var(name.as_str()).ok(); - if value.as_ref().is_some_and(|v| v.is_empty()) { - Self { name, value: None } - } else { - Self { name, value } - } - } - - pub fn or(self, other: EnvVar) -> EnvVar { - if self.value.is_some() { self } else { other } - } -} - -/// Creates a `LazyLock` expression for use in a `static` declaration. -#[macro_export] -macro_rules! env_var { - ($name:expr) => { - LazyLock::new(|| $crate::EnvVar::new(($name).into())) - }; -} - -/// Generates a `LazyLock` expression for use in a `static` declaration. Checks if the -/// environment variable exists and is non-empty. -#[macro_export] -macro_rules! bool_env_var { - ($name:expr) => { - LazyLock::new(|| $crate::EnvVar::new(($name).into()).value.is_some()) - }; -} diff --git a/crates/zeta_prompt/Cargo.toml b/crates/zeta_prompt/Cargo.toml deleted file mode 100644 index c9b1e2d784..0000000000 --- a/crates/zeta_prompt/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "zeta_prompt" -version = "0.1.0" -publish.workspace = true -edition.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/zeta_prompt.rs" - -[dependencies] -serde.workspace = true \ No newline at end of file diff --git a/crates/zeta_prompt/LICENSE-GPL b/crates/zeta_prompt/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/zeta_prompt/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/zeta_prompt/src/zeta_prompt.rs b/crates/zeta_prompt/src/zeta_prompt.rs deleted file mode 100644 index 21fbca1ae1..0000000000 --- a/crates/zeta_prompt/src/zeta_prompt.rs +++ /dev/null @@ -1,165 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::fmt::Write; -use std::ops::Range; -use std::path::Path; -use std::sync::Arc; - -pub const CURSOR_MARKER: &str = "<|user_cursor|>"; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ZetaPromptInput { - pub cursor_path: Arc, - pub cursor_excerpt: Arc, - pub editable_range_in_excerpt: Range, - pub cursor_offset_in_excerpt: usize, - pub events: Vec>, - pub related_files: Arc<[RelatedFile]>, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(tag = "event")] -pub enum Event { - BufferChange { - path: Arc, - old_path: Arc, - diff: String, - predicted: bool, - in_open_source_repo: bool, - }, -} - -pub fn write_event(prompt: &mut String, event: &Event) { - fn write_path_as_unix_str(prompt: &mut String, path: &Path) { - for component in path.components() { - prompt.push('/'); - write!(prompt, "{}", component.as_os_str().display()).ok(); - } - } - match event { - Event::BufferChange { - path, - old_path, - diff, - predicted, - in_open_source_repo: _, - } => { - if *predicted { - prompt.push_str("// User accepted prediction:\n"); - } - prompt.push_str("--- a"); - write_path_as_unix_str(prompt, old_path.as_ref()); - prompt.push_str("\n+++ b"); - write_path_as_unix_str(prompt, path.as_ref()); - prompt.push('\n'); - prompt.push_str(diff); - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct RelatedFile { - pub path: Arc, - pub max_row: u32, - pub excerpts: Vec, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct RelatedExcerpt { - pub row_range: Range, - pub text: String, -} - -pub fn format_zeta_prompt(input: &ZetaPromptInput) -> String { - let mut prompt = String::new(); - write_related_files(&mut prompt, &input.related_files); - write_edit_history_section(&mut prompt, input); - write_cursor_excerpt_section(&mut prompt, input); - prompt -} - -pub fn write_related_files(prompt: &mut String, related_files: &[RelatedFile]) { - push_delimited(prompt, "related_files", &[], |prompt| { - for file in related_files { - let path_str = file.path.to_string_lossy(); - push_delimited(prompt, "related_file", &[("path", &path_str)], |prompt| { - for excerpt in &file.excerpts { - push_delimited( - prompt, - "related_excerpt", - &[( - "lines", - &format!( - "{}-{}", - excerpt.row_range.start + 1, - excerpt.row_range.end + 1 - ), - )], - |prompt| { - prompt.push_str(&excerpt.text); - prompt.push('\n'); - }, - ); - } - }); - } - }); -} - -fn write_edit_history_section(prompt: &mut String, input: &ZetaPromptInput) { - push_delimited(prompt, "edit_history", &[], |prompt| { - if input.events.is_empty() { - prompt.push_str("(No edit history)"); - } else { - for event in &input.events { - write_event(prompt, event); - } - } - }); -} - -fn write_cursor_excerpt_section(prompt: &mut String, input: &ZetaPromptInput) { - push_delimited(prompt, "cursor_excerpt", &[], |prompt| { - let path_str = input.cursor_path.to_string_lossy(); - push_delimited(prompt, "file", &[("path", &path_str)], |prompt| { - prompt.push_str(&input.cursor_excerpt[..input.editable_range_in_excerpt.start]); - push_delimited(prompt, "editable_region", &[], |prompt| { - prompt.push_str( - &input.cursor_excerpt - [input.editable_range_in_excerpt.start..input.cursor_offset_in_excerpt], - ); - prompt.push_str(CURSOR_MARKER); - prompt.push_str( - &input.cursor_excerpt - [input.cursor_offset_in_excerpt..input.editable_range_in_excerpt.end], - ); - }); - prompt.push_str(&input.cursor_excerpt[input.editable_range_in_excerpt.end..]); - }); - }); -} - -fn push_delimited( - prompt: &mut String, - tag: &'static str, - arguments: &[(&str, &str)], - cb: impl FnOnce(&mut String), -) { - if !prompt.ends_with("\n") { - prompt.push('\n'); - } - prompt.push('<'); - prompt.push_str(tag); - for (arg_name, arg_value) in arguments { - write!(prompt, " {}=\"{}\"", arg_name, arg_value).ok(); - } - prompt.push_str(">\n"); - - cb(prompt); - - if !prompt.ends_with('\n') { - prompt.push('\n'); - } - prompt.push_str("\n"); -} diff --git a/crates/zlog/Cargo.toml b/crates/zlog/Cargo.toml deleted file mode 100644 index 2799592c8e..0000000000 --- a/crates/zlog/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "zlog" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/zlog.rs" - -[features] -default = [] - -[dependencies] -collections.workspace = true -chrono.workspace = true -log.workspace = true -anyhow.workspace = true - -[dev-dependencies] -tempfile.workspace = true diff --git a/crates/zlog/LICENSE-GPL b/crates/zlog/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/zlog/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/zlog/README.md b/crates/zlog/README.md deleted file mode 100644 index 6d0fef147c..0000000000 --- a/crates/zlog/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Zlog - -Use the `ZED_LOG` environment variable to control logging output for Zed -applications and libraries. The variable accepts a comma-separated list of -directives that specify logging levels for different modules (crates). The -general format is for instance: - -``` -ZED_LOG=info,project=debug,agent=off -``` - -- Levels can be one of: `off`/`none`, `error`, `warn`, `info`, `debug`, or - `trace`. -- You don't need to specify the global level, default is `trace` in the crate - and `info` set by `RUST_LOG` in Zed. diff --git a/crates/zlog/src/env_config.rs b/crates/zlog/src/env_config.rs deleted file mode 100644 index 38d3adc179..0000000000 --- a/crates/zlog/src/env_config.rs +++ /dev/null @@ -1,122 +0,0 @@ -use anyhow::Result; - -pub struct EnvFilter { - pub level_global: Option, - pub directive_names: Vec, - pub directive_levels: Vec, -} - -pub fn parse(filter: &str) -> Result { - let mut max_level = None; - let mut directive_names = Vec::new(); - let mut directive_levels = Vec::new(); - - for directive in filter.split(',') { - match directive.split_once('=') { - Some((name, level)) => { - anyhow::ensure!(!level.contains('='), "Invalid directive: {directive}"); - let level = parse_level(level.trim())?; - directive_names.push(name.trim().trim_end_matches(".rs").to_string()); - directive_levels.push(level); - } - None => { - let Ok(level) = parse_level(directive.trim()) else { - directive_names.push(directive.trim().trim_end_matches(".rs").to_string()); - directive_levels.push(log::LevelFilter::max() /* Enable all levels */); - continue; - }; - anyhow::ensure!(max_level.is_none(), "Cannot set multiple max levels"); - max_level.replace(level); - } - }; - } - - Ok(EnvFilter { - level_global: max_level, - directive_names, - directive_levels, - }) -} - -fn parse_level(level: &str) -> Result { - if level.eq_ignore_ascii_case("TRACE") { - return Ok(log::LevelFilter::Trace); - } - if level.eq_ignore_ascii_case("DEBUG") { - return Ok(log::LevelFilter::Debug); - } - if level.eq_ignore_ascii_case("INFO") { - return Ok(log::LevelFilter::Info); - } - if level.eq_ignore_ascii_case("WARN") { - return Ok(log::LevelFilter::Warn); - } - if level.eq_ignore_ascii_case("ERROR") { - return Ok(log::LevelFilter::Error); - } - if level.eq_ignore_ascii_case("OFF") || level.eq_ignore_ascii_case("NONE") { - return Ok(log::LevelFilter::Off); - } - anyhow::bail!("Invalid level: {level}") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn global_level() { - let input = "info"; - let filter = parse(input).unwrap(); - - assert_eq!(filter.level_global.unwrap(), log::LevelFilter::Info); - assert!(filter.directive_names.is_empty()); - assert!(filter.directive_levels.is_empty()); - } - - #[test] - fn directive_level() { - let input = "my_module=debug"; - let filter = parse(input).unwrap(); - - assert_eq!(filter.level_global, None); - assert_eq!(filter.directive_names, vec!["my_module".to_string()]); - assert_eq!(filter.directive_levels, vec![log::LevelFilter::Debug]); - } - - #[test] - fn global_level_and_directive_level() { - let input = "info,my_module=debug"; - let filter = parse(input).unwrap(); - - assert_eq!(filter.level_global.unwrap(), log::LevelFilter::Info); - assert_eq!(filter.directive_names, vec!["my_module".to_string()]); - assert_eq!(filter.directive_levels, vec![log::LevelFilter::Debug]); - } - - #[test] - fn global_level_and_bare_module() { - let input = "info,my_module"; - let filter = parse(input).unwrap(); - - assert_eq!(filter.level_global.unwrap(), log::LevelFilter::Info); - assert_eq!(filter.directive_names, vec!["my_module".to_string()]); - assert_eq!(filter.directive_levels, vec![log::LevelFilter::max()]); - } - - #[test] - fn err_when_multiple_max_levels() { - let input = "info,warn"; - let result = parse(input); - - assert!(result.is_err()); - } - - #[test] - fn err_when_invalid_level() { - let input = "my_module=foobar"; - let result = parse(input); - - assert!(result.is_err()); - } -} diff --git a/crates/zlog/src/filter.rs b/crates/zlog/src/filter.rs deleted file mode 100644 index 0be6f4ead5..0000000000 --- a/crates/zlog/src/filter.rs +++ /dev/null @@ -1,843 +0,0 @@ -use collections::HashMap; -use std::collections::VecDeque; -use std::sync::{ - OnceLock, RwLock, - atomic::{AtomicU8, Ordering}, -}; - -use crate::{SCOPE_DEPTH_MAX, SCOPE_STRING_SEP_STR, ScopeAlloc, ScopeRef, env_config, private}; - -use log; - -static ENV_FILTER: OnceLock = OnceLock::new(); -static SCOPE_MAP: RwLock = RwLock::new(ScopeMap::empty()); - -pub const LEVEL_ENABLED_MAX_DEFAULT: log::LevelFilter = log::LevelFilter::Info; -/// The maximum log level of verbosity that is enabled by default. -/// All messages more verbose than this level will be discarded -/// by default unless specially configured. -/// -/// This is used instead of the `log::max_level` as we need to tell the `log` -/// crate that the max level is everything, so that we can dynamically enable -/// logs that are more verbose than this level without the `log` crate throwing -/// them away before we see them -static LEVEL_ENABLED_MAX_STATIC: AtomicU8 = AtomicU8::new(LEVEL_ENABLED_MAX_DEFAULT as u8); - -/// A cache of the true maximum log level that _could_ be printed. This is based -/// on the maximally verbose level that is configured by the user, and is used -/// to filter out logs more verbose than any configured level. -/// -/// E.g. if `LEVEL_ENABLED_MAX_STATIC `is 'info' but a user has configured some -/// scope to print at a `debug` level, then this will be `debug`, and all -/// `trace` logs will be discarded. -/// Therefore, it should always be `>= LEVEL_ENABLED_MAX_STATIC` -// PERF: this doesn't need to be an atomic, we don't actually care about race conditions here -pub static LEVEL_ENABLED_MAX_CONFIG: AtomicU8 = AtomicU8::new(LEVEL_ENABLED_MAX_DEFAULT as u8); - -const DEFAULT_FILTERS: &[(&str, log::LevelFilter)] = &[ - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - ("zbus", log::LevelFilter::Warn), - #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))] - ("blade_graphics", log::LevelFilter::Warn), - #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))] - ("naga::back::spv::writer", log::LevelFilter::Warn), - // usvg prints a lot of warnings on rendering an SVG with partial errors, which - // can happen a lot with the SVG preview - ("usvg::parser::style", log::LevelFilter::Error), -]; - -pub fn init_env_filter(filter: env_config::EnvFilter) { - if let Some(level_max) = filter.level_global { - LEVEL_ENABLED_MAX_STATIC.store(level_max as u8, Ordering::Release) - } - if ENV_FILTER.set(filter).is_err() { - panic!("Environment filter cannot be initialized twice"); - } -} - -pub fn is_possibly_enabled_level(level: log::Level) -> bool { - level as u8 <= LEVEL_ENABLED_MAX_CONFIG.load(Ordering::Acquire) -} - -pub fn is_scope_enabled( - scope: &ScopeRef<'_>, - module_path: Option<&str>, - level: log::Level, -) -> bool { - // TODO: is_always_allowed_level that checks against LEVEL_ENABLED_MIN_CONFIG - if !is_possibly_enabled_level(level) { - // [FAST PATH] - // if the message is above the maximum enabled log level - // (where error < warn < info etc) then disable without checking - // scope map - return false; - } - let is_enabled_by_default = level as u8 <= LEVEL_ENABLED_MAX_STATIC.load(Ordering::Acquire); - let global_scope_map = SCOPE_MAP.read().unwrap_or_else(|err| { - SCOPE_MAP.clear_poison(); - err.into_inner() - }); - - if global_scope_map.is_empty() { - // if no scopes are enabled, return false because it's not <= LEVEL_ENABLED_MAX_STATIC - return is_enabled_by_default; - } - let enabled_status = global_scope_map.is_enabled(scope, module_path, level); - match enabled_status { - EnabledStatus::NotConfigured => is_enabled_by_default, - EnabledStatus::Enabled => true, - EnabledStatus::Disabled => false, - } -} - -pub fn refresh_from_settings(settings: &HashMap) { - let env_config = ENV_FILTER.get(); - let map_new = ScopeMap::new_from_settings_and_env(settings, env_config, DEFAULT_FILTERS); - let mut level_enabled_max = LEVEL_ENABLED_MAX_STATIC.load(Ordering::Acquire); - for entry in &map_new.entries { - if let Some(level) = entry.enabled { - level_enabled_max = level_enabled_max.max(level as u8); - } - } - LEVEL_ENABLED_MAX_CONFIG.store(level_enabled_max, Ordering::Release); - - { - let mut global_map = SCOPE_MAP.write().unwrap_or_else(|err| { - SCOPE_MAP.clear_poison(); - err.into_inner() - }); - *global_map = map_new; - } - log::trace!("Log configuration updated"); -} - -fn level_filter_from_str(level_str: &str) -> Option { - use log::LevelFilter::*; - let level = match level_str.to_ascii_lowercase().as_str() { - "" => Trace, - "trace" => Trace, - "debug" => Debug, - "info" => Info, - "warn" => Warn, - "error" => Error, - "off" => Off, - "disable" | "no" | "none" | "disabled" => { - crate::warn!( - "Invalid log level \"{level_str}\", to disable logging set to \"off\". Defaulting to \"off\"." - ); - Off - } - _ => { - crate::warn!("Invalid log level \"{level_str}\", ignoring"); - return None; - } - }; - Some(level) -} - -fn scope_alloc_from_scope_str(scope_str: &str) -> Option { - let mut scope_buf = [""; SCOPE_DEPTH_MAX]; - let mut index = 0; - let mut scope_iter = scope_str.split(SCOPE_STRING_SEP_STR); - while index < SCOPE_DEPTH_MAX { - let Some(scope) = scope_iter.next() else { - break; - }; - if scope.is_empty() { - continue; - } - scope_buf[index] = scope; - index += 1; - } - if index == 0 { - return None; - } - if scope_iter.next().is_some() { - crate::warn!( - "Invalid scope key, too many nested scopes: '{scope_str}'. Max depth is {SCOPE_DEPTH_MAX}", - ); - return None; - } - let scope = scope_buf.map(|s| s.to_string()); - Some(scope) -} - -#[derive(Debug, PartialEq, Eq)] -pub struct ScopeMap { - entries: Vec, - modules: Vec<(String, log::LevelFilter)>, - root_count: usize, -} - -#[derive(Debug, PartialEq, Eq)] -pub struct ScopeMapEntry { - scope: String, - enabled: Option, - descendants: std::ops::Range, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EnabledStatus { - Enabled, - Disabled, - NotConfigured, -} - -impl ScopeMap { - pub fn new_from_settings_and_env( - items_input_map: &HashMap, - env_config: Option<&env_config::EnvFilter>, - default_filters: &[(&str, log::LevelFilter)], - ) -> Self { - let mut items = Vec::<(ScopeAlloc, log::LevelFilter)>::with_capacity( - items_input_map.len() - + env_config.map_or(0, |c| c.directive_names.len()) - + default_filters.len(), - ); - let mut modules = Vec::with_capacity(4); - - let env_filters = env_config.iter().flat_map(|env_filter| { - env_filter - .directive_names - .iter() - .zip(env_filter.directive_levels.iter()) - .map(|(scope_str, level_filter)| (scope_str.as_str(), *level_filter)) - }); - - let new_filters = items_input_map.iter().filter_map(|(scope_str, level_str)| { - let level_filter = level_filter_from_str(level_str)?; - Some((scope_str.as_str(), level_filter)) - }); - - let all_filters = default_filters - .iter() - .cloned() - .chain(env_filters) - .chain(new_filters); - - for (scope_str, level_filter) in all_filters { - if scope_str.contains("::") { - if let Some(idx) = modules.iter().position(|(module, _)| module == scope_str) { - modules[idx].1 = level_filter; - } else { - modules.push((scope_str.to_string(), level_filter)); - } - continue; - } - let Some(scope) = scope_alloc_from_scope_str(scope_str) else { - continue; - }; - if let Some(idx) = items - .iter() - .position(|(scope_existing, _)| scope_existing == &scope) - { - items[idx].1 = level_filter; - } else { - items.push((scope, level_filter)); - } - } - - items.sort_by(|a, b| a.0.cmp(&b.0)); - modules.sort_by(|(a_name, _), (b_name, _)| a_name.cmp(b_name)); - - let mut this = Self { - entries: Vec::with_capacity(items.len() * SCOPE_DEPTH_MAX), - modules, - root_count: 0, - }; - - let items_count = items.len(); - - struct ProcessQueueEntry { - parent_index: usize, - depth: usize, - items_range: std::ops::Range, - } - let mut process_queue = VecDeque::new(); - process_queue.push_back(ProcessQueueEntry { - parent_index: usize::MAX, - depth: 0, - items_range: 0..items_count, - }); - - let empty_range = 0..0; - - while let Some(process_entry) = process_queue.pop_front() { - let ProcessQueueEntry { - items_range, - depth, - parent_index, - } = process_entry; - let mut cursor = items_range.start; - let res_entries_start = this.entries.len(); - while cursor < items_range.end { - let sub_items_start = cursor; - cursor += 1; - let scope_name = &items[sub_items_start].0[depth]; - while cursor < items_range.end && &items[cursor].0[depth] == scope_name { - cursor += 1; - } - let sub_items_end = cursor; - if scope_name.is_empty() { - assert_eq!(sub_items_start + 1, sub_items_end); - assert_ne!(depth, 0); - assert_ne!(parent_index, usize::MAX); - assert!(this.entries[parent_index].enabled.is_none()); - this.entries[parent_index].enabled = Some(items[sub_items_start].1); - continue; - } - let is_valid_scope = !scope_name.is_empty(); - let is_last = depth + 1 == SCOPE_DEPTH_MAX || !is_valid_scope; - let mut enabled = None; - if is_last { - assert_eq!( - sub_items_start + 1, - sub_items_end, - "Expected one item: got: {:?}", - &items[items_range] - ); - enabled = Some(items[sub_items_start].1); - } else { - let entry_index = this.entries.len(); - process_queue.push_back(ProcessQueueEntry { - items_range: sub_items_start..sub_items_end, - parent_index: entry_index, - depth: depth + 1, - }); - } - this.entries.push(ScopeMapEntry { - scope: scope_name.to_owned(), - enabled, - descendants: empty_range.clone(), - }); - } - let res_entries_end = this.entries.len(); - if parent_index != usize::MAX { - this.entries[parent_index].descendants = res_entries_start..res_entries_end; - } else { - this.root_count = res_entries_end; - } - } - - this - } - - pub fn is_empty(&self) -> bool { - self.entries.is_empty() && self.modules.is_empty() - } - - pub fn is_enabled( - &self, - scope: &[S; SCOPE_DEPTH_MAX], - module_path: Option<&str>, - level: log::Level, - ) -> EnabledStatus - where - S: AsRef, - { - fn search(map: &ScopeMap, scope: &[S; SCOPE_DEPTH_MAX]) -> Option - where - S: AsRef, - { - let mut enabled = None; - let mut cur_range = &map.entries[0..map.root_count]; - let mut depth = 0; - 'search: while !cur_range.is_empty() - && depth < SCOPE_DEPTH_MAX - && scope[depth].as_ref() != "" - { - for entry in cur_range { - if entry.scope == scope[depth].as_ref() { - enabled = entry.enabled.or(enabled); - cur_range = &map.entries[entry.descendants.clone()]; - depth += 1; - continue 'search; - } - } - break 'search; - } - enabled - } - - let mut enabled = search(self, scope); - - if let Some(module_path) = module_path { - let scope_is_empty = scope[0].as_ref().is_empty(); - - if enabled.is_none() && scope_is_empty { - let crate_name = private::extract_crate_name_from_module_path(module_path); - let mut crate_name_scope = [""; SCOPE_DEPTH_MAX]; - crate_name_scope[0] = crate_name; - enabled = search(self, &crate_name_scope); - } - - if !self.modules.is_empty() { - let crate_name = private::extract_crate_name_from_module_path(module_path); - let is_scope_just_crate_name = - scope[0].as_ref() == crate_name && scope[1].as_ref() == ""; - if enabled.is_none() || is_scope_just_crate_name { - for (module, filter) in &self.modules { - if module == module_path { - enabled.replace(*filter); - break; - } - } - } - } - } - - if let Some(enabled_filter) = enabled { - if level <= enabled_filter { - return EnabledStatus::Enabled; - } - return EnabledStatus::Disabled; - } - EnabledStatus::NotConfigured - } - - const fn empty() -> ScopeMap { - ScopeMap { - entries: vec![], - modules: vec![], - root_count: 0, - } - } -} - -#[cfg(test)] -mod tests { - use log::LevelFilter; - - use crate::Scope; - use crate::private::scope_new; - - use super::*; - - fn scope_map_from_keys(kv: &[(&str, &str)]) -> ScopeMap { - let hash_map: HashMap = kv - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(); - ScopeMap::new_from_settings_and_env(&hash_map, None, &[]) - } - - #[test] - fn test_initialization() { - let map = scope_map_from_keys(&[("a.b.c.d", "trace")]); - assert_eq!(map.root_count, 1); - assert_eq!(map.entries.len(), 4); - - let map = scope_map_from_keys(&[]); - assert_eq!(map.root_count, 0); - assert_eq!(map.entries.len(), 0); - - let map = scope_map_from_keys(&[("", "trace")]); - assert_eq!(map.root_count, 0); - assert_eq!(map.entries.len(), 0); - - let map = scope_map_from_keys(&[("foo..bar", "trace")]); - assert_eq!(map.root_count, 1); - assert_eq!(map.entries.len(), 2); - - let map = scope_map_from_keys(&[ - ("a.b.c.d", "trace"), - ("e.f.g.h", "debug"), - ("i.j.k.l", "info"), - ("m.n.o.p", "warn"), - ("q.r.s.t", "error"), - ]); - assert_eq!(map.root_count, 5); - assert_eq!(map.entries.len(), 20); - assert_eq!(map.entries[0].scope, "a"); - assert_eq!(map.entries[1].scope, "e"); - assert_eq!(map.entries[2].scope, "i"); - assert_eq!(map.entries[3].scope, "m"); - assert_eq!(map.entries[4].scope, "q"); - } - - fn scope_from_scope_str(scope_str: &'static str) -> Scope { - let mut scope_buf = [""; SCOPE_DEPTH_MAX]; - let mut index = 0; - let mut scope_iter = scope_str.split(SCOPE_STRING_SEP_STR); - while index < SCOPE_DEPTH_MAX { - let Some(scope) = scope_iter.next() else { - break; - }; - if scope.is_empty() { - continue; - } - scope_buf[index] = scope; - index += 1; - } - assert_ne!(index, 0); - assert!(scope_iter.next().is_none()); - scope_buf - } - - #[test] - fn test_is_enabled() { - let map = scope_map_from_keys(&[ - ("a.b.c.d", "trace"), - ("e.f.g.h", "debug"), - ("i.j.k.l", "info"), - ("m.n.o.p", "warn"), - ("q.r.s.t", "error"), - ]); - use log::Level; - assert_eq!( - map.is_enabled(&scope_from_scope_str("a.b.c.d"), None, Level::Trace), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("a.b.c.d"), None, Level::Debug), - EnabledStatus::Enabled - ); - - assert_eq!( - map.is_enabled(&scope_from_scope_str("e.f.g.h"), None, Level::Debug), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("e.f.g.h"), None, Level::Info), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("e.f.g.h"), None, Level::Trace), - EnabledStatus::Disabled - ); - - assert_eq!( - map.is_enabled(&scope_from_scope_str("i.j.k.l"), None, Level::Info), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("i.j.k.l"), None, Level::Warn), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("i.j.k.l"), None, Level::Debug), - EnabledStatus::Disabled - ); - - assert_eq!( - map.is_enabled(&scope_from_scope_str("m.n.o.p"), None, Level::Warn), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("m.n.o.p"), None, Level::Error), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("m.n.o.p"), None, Level::Info), - EnabledStatus::Disabled - ); - - assert_eq!( - map.is_enabled(&scope_from_scope_str("q.r.s.t"), None, Level::Error), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("q.r.s.t"), None, Level::Warn), - EnabledStatus::Disabled - ); - } - - #[test] - fn test_is_enabled_module() { - let mut map = scope_map_from_keys(&[("a", "trace")]); - map.modules = [("a::b::c", "trace"), ("a::b::d", "debug")] - .map(|(k, v)| (k.to_string(), v.parse().unwrap())) - .to_vec(); - use log::Level; - assert_eq!( - map.is_enabled( - &scope_from_scope_str("__unused__"), - Some("a::b::c"), - Level::Trace - ), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled( - &scope_from_scope_str("__unused__"), - Some("a::b::d"), - Level::Debug - ), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled( - &scope_from_scope_str("__unused__"), - Some("a::b::d"), - Level::Trace, - ), - EnabledStatus::Disabled - ); - assert_eq!( - map.is_enabled( - &scope_from_scope_str("__unused__"), - Some("a::e"), - Level::Info - ), - EnabledStatus::NotConfigured - ); - // when scope is just crate name, more specific module path overrides it - assert_eq!( - map.is_enabled(&scope_from_scope_str("a"), Some("a::b::d"), Level::Trace), - EnabledStatus::Disabled, - ); - // but when it is scoped, the scope overrides the module path - assert_eq!( - map.is_enabled( - &scope_from_scope_str("a.scope"), - Some("a::b::d"), - Level::Trace - ), - EnabledStatus::Enabled, - ); - } - - fn scope_map_from_keys_and_env(kv: &[(&str, &str)], env: &env_config::EnvFilter) -> ScopeMap { - let hash_map: HashMap = kv - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(); - ScopeMap::new_from_settings_and_env(&hash_map, Some(env), &[]) - } - - #[test] - fn test_initialization_with_env() { - let env_filter = env_config::parse("a.b=debug,u=error").unwrap(); - let map = scope_map_from_keys_and_env(&[], &env_filter); - assert_eq!(map.root_count, 2); - assert_eq!(map.entries.len(), 3); - assert_eq!( - map.is_enabled(&scope_new(&["a"]), None, log::Level::Debug), - EnabledStatus::NotConfigured - ); - assert_eq!( - map.is_enabled(&scope_new(&["a", "b"]), None, log::Level::Debug), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled(&scope_new(&["a", "b", "c"]), None, log::Level::Trace), - EnabledStatus::Disabled - ); - - let env_filter = env_config::parse("a.b=debug,e.f.g.h=trace,u=error").unwrap(); - let map = scope_map_from_keys_and_env( - &[ - ("a.b.c.d", "trace"), - ("e.f.g.h", "debug"), - ("i.j.k.l", "info"), - ("m.n.o.p", "warn"), - ("q.r.s.t", "error"), - ], - &env_filter, - ); - assert_eq!(map.root_count, 6); - assert_eq!(map.entries.len(), 21); - assert_eq!(map.entries[0].scope, "a"); - assert_eq!(map.entries[1].scope, "e"); - assert_eq!(map.entries[2].scope, "i"); - assert_eq!(map.entries[3].scope, "m"); - assert_eq!(map.entries[4].scope, "q"); - assert_eq!(map.entries[5].scope, "u"); - assert_eq!( - map.is_enabled(&scope_new(&["a", "b", "c", "d"]), None, log::Level::Trace), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled(&scope_new(&["a", "b", "c"]), None, log::Level::Trace), - EnabledStatus::Disabled - ); - assert_eq!( - map.is_enabled(&scope_new(&["u", "v"]), None, log::Level::Warn), - EnabledStatus::Disabled - ); - // settings override env - assert_eq!( - map.is_enabled(&scope_new(&["e", "f", "g", "h"]), None, log::Level::Trace), - EnabledStatus::Disabled, - ); - } - - fn scope_map_from_all( - kv: &[(&str, &str)], - env: &env_config::EnvFilter, - default_filters: &[(&str, log::LevelFilter)], - ) -> ScopeMap { - let hash_map: HashMap = kv - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(); - ScopeMap::new_from_settings_and_env(&hash_map, Some(env), default_filters) - } - - #[test] - fn precedence() { - // Test precedence: kv > env > default - - // Default filters - these should be overridden by env and kv when they overlap - let default_filters = &[ - ("a.b.c", log::LevelFilter::Debug), // Should be overridden by env - ("p.q.r", log::LevelFilter::Info), // Should be overridden by kv - ("x.y.z", log::LevelFilter::Warn), // Not overridden - ("crate::module::default", log::LevelFilter::Error), // Module in default - ("crate::module::user", log::LevelFilter::Off), // Module disabled in default - ]; - - // Environment filters - these should override default but be overridden by kv - let env_filter = - env_config::parse("a.b.c=trace,p.q=debug,m.n.o=error,crate::module::env=debug") - .unwrap(); - - // Key-value filters (highest precedence) - these should override everything - let kv_filters = &[ - ("p.q.r", "trace"), // Overrides default - ("m.n.o", "warn"), // Overrides env - ("j.k.l", "info"), // New filter - ("crate::module::env", "trace"), // Overrides env for module - ("crate::module::kv", "trace"), // New module filter - ]; - - let map = scope_map_from_all(kv_filters, &env_filter, default_filters); - - // Test scope precedence - use log::Level; - - // KV overrides all for scopes - assert_eq!( - map.is_enabled(&scope_from_scope_str("p.q.r"), None, Level::Trace), - EnabledStatus::Enabled, - "KV should override default filters for scopes" - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("m.n.o"), None, Level::Warn), - EnabledStatus::Enabled, - "KV should override env filters for scopes" - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("m.n.o"), None, Level::Debug), - EnabledStatus::Disabled, - "KV correctly limits log level" - ); - - // ENV overrides default but not KV for scopes - assert_eq!( - map.is_enabled(&scope_from_scope_str("a.b.c"), None, Level::Trace), - EnabledStatus::Enabled, - "ENV should override default filters for scopes" - ); - - // Default is used when no override exists for scopes - assert_eq!( - map.is_enabled(&scope_from_scope_str("x.y.z"), None, Level::Warn), - EnabledStatus::Enabled, - "Default filters should work when not overridden" - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("x.y.z"), None, Level::Info), - EnabledStatus::Disabled, - "Default filters correctly limit log level" - ); - - // KV overrides all for modules - assert_eq!( - map.is_enabled(&scope_new(&[""]), Some("crate::module::env"), Level::Trace), - EnabledStatus::Enabled, - "KV should override env filters for modules" - ); - assert_eq!( - map.is_enabled(&scope_new(&[""]), Some("crate::module::kv"), Level::Trace), - EnabledStatus::Enabled, - "KV module filters should work" - ); - - // ENV overrides default for modules - assert_eq!( - map.is_enabled(&scope_new(&[""]), Some("crate::module::env"), Level::Debug), - EnabledStatus::Enabled, - "ENV should override default for modules" - ); - - // Default is used when no override exists for modules - assert_eq!( - map.is_enabled( - &scope_new(&[""]), - Some("crate::module::default"), - Level::Error - ), - EnabledStatus::Enabled, - "Default filters should work for modules" - ); - assert_eq!( - map.is_enabled( - &scope_new(&[""]), - Some("crate::module::default"), - Level::Warn - ), - EnabledStatus::Disabled, - "Default filters correctly limit log level for modules" - ); - - assert_eq!( - map.is_enabled(&scope_new(&[""]), Some("crate::module::user"), Level::Error), - EnabledStatus::Disabled, - "Module turned off in default filters is not enabled" - ); - - assert_eq!( - map.is_enabled( - &scope_new(&["crate"]), - Some("crate::module::user"), - Level::Error - ), - EnabledStatus::Disabled, - "Module turned off in default filters is not enabled, even with crate name as scope" - ); - - // Test non-conflicting but similar paths - - // Test that "a.b" and "a.b.c" don't conflict (different depth) - assert_eq!( - map.is_enabled(&scope_from_scope_str("a.b.c.d"), None, Level::Trace), - EnabledStatus::Enabled, - "Scope a.b.c should inherit from a.b env filter" - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("a.b.c"), None, Level::Trace), - EnabledStatus::Enabled, - "Scope a.b.c.d should use env filter level (trace)" - ); - - // Test that similar module paths don't conflict - assert_eq!( - map.is_enabled(&scope_new(&[""]), Some("crate::module"), Level::Error), - EnabledStatus::NotConfigured, - "Module crate::module should not be affected by crate::module::default filter" - ); - assert_eq!( - map.is_enabled( - &scope_new(&[""]), - Some("crate::module::default::sub"), - Level::Error - ), - EnabledStatus::NotConfigured, - "Module crate::module::default::sub should not be affected by crate::module::default filter" - ); - } - - #[test] - fn default_filter_crate() { - let default_filters = &[("crate", LevelFilter::Off)]; - let map = scope_map_from_all(&[], &env_config::parse("").unwrap(), default_filters); - - use log::Level; - assert_eq!( - map.is_enabled(&scope_new(&[""]), Some("crate::submodule"), Level::Error), - EnabledStatus::Disabled, - "crate::submodule should be disabled by disabling `crate` filter" - ); - } -} diff --git a/crates/zlog/src/sink.rs b/crates/zlog/src/sink.rs deleted file mode 100644 index 07e87be1b0..0000000000 --- a/crates/zlog/src/sink.rs +++ /dev/null @@ -1,324 +0,0 @@ -use std::{ - fs, - io::{self, Write}, - path::PathBuf, - sync::{ - Mutex, OnceLock, - atomic::{AtomicBool, AtomicU64, Ordering}, - }, -}; - -use crate::{SCOPE_STRING_SEP_CHAR, ScopeRef}; - -// ANSI color escape codes for log levels -const ANSI_RESET: &str = "\x1b[0m"; -const ANSI_BOLD: &str = "\x1b[1m"; -const ANSI_RED: &str = "\x1b[31m"; -const ANSI_YELLOW: &str = "\x1b[33m"; -const ANSI_GREEN: &str = "\x1b[32m"; -const ANSI_BLUE: &str = "\x1b[34m"; -const ANSI_MAGENTA: &str = "\x1b[35m"; - -/// Is Some(file) if file output is enabled. -static ENABLED_SINKS_FILE: Mutex> = Mutex::new(None); -static SINK_FILE_PATH: OnceLock<&'static PathBuf> = OnceLock::new(); -static SINK_FILE_PATH_ROTATE: OnceLock<&'static PathBuf> = OnceLock::new(); - -// NB: Since this can be accessed in tests, we probably should stick to atomics here. -/// Whether stdout output is enabled. -static ENABLED_SINKS_STDOUT: AtomicBool = AtomicBool::new(false); -/// Whether stderr output is enabled. -static ENABLED_SINKS_STDERR: AtomicBool = AtomicBool::new(false); -/// Atomic counter for the size of the log file in bytes. -static SINK_FILE_SIZE_BYTES: AtomicU64 = AtomicU64::new(0); -/// Maximum size of the log file before it will be rotated, in bytes. -const SINK_FILE_SIZE_BYTES_MAX: u64 = 1024 * 1024; // 1 MB - -pub struct Record<'a> { - pub scope: ScopeRef<'a>, - pub level: log::Level, - pub message: &'a std::fmt::Arguments<'a>, - pub module_path: Option<&'a str>, - pub line: Option, -} - -pub fn init_output_stdout() { - // Use atomics here instead of just a `static mut`, since in the context - // of tests these accesses can be multi-threaded. - ENABLED_SINKS_STDOUT.store(true, Ordering::Release); -} - -pub fn init_output_stderr() { - ENABLED_SINKS_STDERR.store(true, Ordering::Release); -} - -pub fn init_output_file( - path: &'static PathBuf, - path_rotate: Option<&'static PathBuf>, -) -> io::Result<()> { - let mut file = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open(path)?; - - SINK_FILE_PATH - .set(path) - .expect("Init file output should only be called once"); - if let Some(path_rotate) = path_rotate { - SINK_FILE_PATH_ROTATE - .set(path_rotate) - .expect("Init file output should only be called once"); - } - - let mut enabled_sinks_file = ENABLED_SINKS_FILE - .try_lock() - .expect("Log file lock is available during init"); - - let size_bytes = file.metadata().map_or(0, |metadata| metadata.len()); - if size_bytes >= SINK_FILE_SIZE_BYTES_MAX { - rotate_log_file(&mut file, Some(path), path_rotate, &SINK_FILE_SIZE_BYTES); - } else { - SINK_FILE_SIZE_BYTES.store(size_bytes, Ordering::Release); - } - - *enabled_sinks_file = Some(file); - - Ok(()) -} - -const LEVEL_OUTPUT_STRINGS: [&str; 6] = [ - " ", // nop: ERROR = 1 - "ERROR", // - "WARN ", // - "INFO ", // - "DEBUG", // - "TRACE", // -]; - -// Colors for different log levels -static LEVEL_ANSI_COLORS: [&str; 6] = [ - "", // nop - ANSI_RED, // Error: Red - ANSI_YELLOW, // Warn: Yellow - ANSI_GREEN, // Info: Green - ANSI_BLUE, // Debug: Blue - ANSI_MAGENTA, // Trace: Magenta -]; - -// PERF: batching -pub fn submit(mut record: Record) { - if record.module_path.is_none_or(|p| !p.ends_with(".rs")) { - // Only render line numbers for actual rust files emitted by `log_err` and friends - record.line.take(); - } - if ENABLED_SINKS_STDOUT.load(Ordering::Acquire) { - let mut stdout = std::io::stdout().lock(); - _ = writeln!( - &mut stdout, - "{} {ANSI_BOLD}{}{}{ANSI_RESET} {} {}", - chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%:z"), - LEVEL_ANSI_COLORS[record.level as usize], - LEVEL_OUTPUT_STRINGS[record.level as usize], - SourceFmt { - scope: record.scope, - module_path: record.module_path, - line: record.line, - ansi: true, - }, - record.message - ); - } else if ENABLED_SINKS_STDERR.load(Ordering::Acquire) { - let mut stdout = std::io::stderr().lock(); - _ = writeln!( - &mut stdout, - "{} {ANSI_BOLD}{}{}{ANSI_RESET} {} {}", - chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%:z"), - LEVEL_ANSI_COLORS[record.level as usize], - LEVEL_OUTPUT_STRINGS[record.level as usize], - SourceFmt { - scope: record.scope, - module_path: record.module_path, - line: record.line, - ansi: true, - }, - record.message - ); - } - let mut file = ENABLED_SINKS_FILE.lock().unwrap_or_else(|handle| { - ENABLED_SINKS_FILE.clear_poison(); - handle.into_inner() - }); - if let Some(file) = file.as_mut() { - struct SizedWriter<'a> { - file: &'a mut std::fs::File, - written: u64, - } - impl io::Write for SizedWriter<'_> { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.file.write(buf)?; - self.written += buf.len() as u64; - Ok(buf.len()) - } - - fn flush(&mut self) -> io::Result<()> { - self.file.flush() - } - } - let file_size_bytes = { - let mut writer = SizedWriter { file, written: 0 }; - _ = writeln!( - &mut writer, - "{} {} {} {}", - chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%:z"), - LEVEL_OUTPUT_STRINGS[record.level as usize], - SourceFmt { - scope: record.scope, - module_path: record.module_path, - line: record.line, - ansi: false, - }, - record.message - ); - SINK_FILE_SIZE_BYTES.fetch_add(writer.written, Ordering::AcqRel) + writer.written - }; - if file_size_bytes > SINK_FILE_SIZE_BYTES_MAX { - rotate_log_file( - file, - SINK_FILE_PATH.get(), - SINK_FILE_PATH_ROTATE.get(), - &SINK_FILE_SIZE_BYTES, - ); - } - } -} - -pub fn flush() { - if ENABLED_SINKS_STDOUT.load(Ordering::Acquire) { - _ = std::io::stdout().lock().flush(); - } - let mut file = ENABLED_SINKS_FILE.lock().unwrap_or_else(|handle| { - ENABLED_SINKS_FILE.clear_poison(); - handle.into_inner() - }); - if let Some(file) = file.as_mut() - && let Err(err) = file.flush() - { - eprintln!("Failed to flush log file: {}", err); - } -} - -struct SourceFmt<'a> { - scope: ScopeRef<'a>, - module_path: Option<&'a str>, - line: Option, - ansi: bool, -} - -impl std::fmt::Display for SourceFmt<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use std::fmt::Write; - f.write_char('[')?; - if self.ansi { - f.write_str(ANSI_BOLD)?; - } - // NOTE: if no longer prefixing scopes with their crate name, check if scope[0] is empty - if (self.scope[1].is_empty() && self.module_path.is_some()) || self.scope[0].is_empty() { - f.write_str(self.module_path.unwrap_or("?"))?; - } else { - f.write_str(self.scope[0])?; - for subscope in &self.scope[1..] { - if subscope.is_empty() { - break; - } - f.write_char(SCOPE_STRING_SEP_CHAR)?; - f.write_str(subscope)?; - } - } - if let Some(line) = self.line { - f.write_char(':')?; - line.fmt(f)?; - } - if self.ansi { - f.write_str(ANSI_RESET)?; - } - f.write_char(']')?; - Ok(()) - } -} - -fn rotate_log_file( - file: &mut fs::File, - path: Option, - path_rotate: Option, - atomic_size: &AtomicU64, -) where - PathRef: AsRef, -{ - if let Err(err) = file.flush() { - eprintln!( - "Failed to flush log file before rotating, some logs may be lost: {}", - err - ); - } - let rotation_error = match (path, path_rotate) { - (Some(_), None) => Some(anyhow::anyhow!("No rotation log file path configured")), - (None, _) => Some(anyhow::anyhow!("No log file path configured")), - (Some(path), Some(path_rotate)) => fs::copy(path, path_rotate) - .err() - .map(|err| anyhow::anyhow!(err)), - }; - if let Some(err) = rotation_error { - eprintln!("Log file rotation failed. Truncating log file anyways: {err}",); - } - _ = file.set_len(0); - - // SAFETY: It is safe to set size to 0 even if set_len fails as - // according to the documentation, it only fails if: - // - the file is not writeable: should never happen, - // - the size would cause an overflow (implementation specific): 0 should never cause an overflow - atomic_size.store(0, Ordering::Release); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_rotate_log_file() { - let temp_dir = tempfile::tempdir().unwrap(); - let log_file_path = temp_dir.path().join("log.txt"); - let rotation_log_file_path = temp_dir.path().join("log_rotated.txt"); - - let mut file = fs::File::create(&log_file_path).unwrap(); - let contents = String::from("Hello, world!"); - file.write_all(contents.as_bytes()).unwrap(); - - let size = AtomicU64::new(contents.len() as u64); - - rotate_log_file( - &mut file, - Some(&log_file_path), - Some(&rotation_log_file_path), - &size, - ); - - assert!(log_file_path.exists()); - assert_eq!(log_file_path.metadata().unwrap().len(), 0); - assert!(rotation_log_file_path.exists()); - assert_eq!( - std::fs::read_to_string(&rotation_log_file_path).unwrap(), - contents, - ); - assert_eq!(size.load(Ordering::Acquire), 0); - } - - /// Regression test, ensuring that if log level values change we are made aware - #[test] - fn test_log_level_names() { - assert_eq!(LEVEL_OUTPUT_STRINGS[log::Level::Error as usize], "ERROR"); - assert_eq!(LEVEL_OUTPUT_STRINGS[log::Level::Warn as usize], "WARN "); - assert_eq!(LEVEL_OUTPUT_STRINGS[log::Level::Info as usize], "INFO "); - assert_eq!(LEVEL_OUTPUT_STRINGS[log::Level::Debug as usize], "DEBUG"); - assert_eq!(LEVEL_OUTPUT_STRINGS[log::Level::Trace as usize], "TRACE"); - } -} diff --git a/crates/zlog/src/zlog.rs b/crates/zlog/src/zlog.rs deleted file mode 100644 index 3c154f7908..0000000000 --- a/crates/zlog/src/zlog.rs +++ /dev/null @@ -1,409 +0,0 @@ -//! # logger -pub use log as log_impl; - -mod env_config; -pub mod filter; -pub mod sink; - -pub use sink::{flush, init_output_file, init_output_stderr, init_output_stdout}; - -pub const SCOPE_DEPTH_MAX: usize = 4; - -pub fn init() { - if let Err(err) = try_init(None) { - log::error!("{err}"); - eprintln!("{err}"); - } -} - -pub fn try_init(filter: Option) -> anyhow::Result<()> { - log::set_logger(&ZLOG)?; - log::set_max_level(log::LevelFilter::max()); - process_env(filter); - filter::refresh_from_settings(&std::collections::HashMap::default()); - Ok(()) -} - -pub fn init_test() { - if get_env_config().is_some() && try_init(None).is_ok() { - init_output_stdout(); - } -} - -fn get_env_config() -> Option { - std::env::var("ZED_LOG") - .or_else(|_| std::env::var("RUST_LOG")) - .ok() - .or_else(|| { - if std::env::var("CI").is_ok() { - Some("info".to_owned()) - } else { - None - } - }) -} - -pub fn process_env(filter: Option) { - let Some(env_config) = get_env_config().or(filter) else { - return; - }; - match env_config::parse(&env_config) { - Ok(filter) => { - filter::init_env_filter(filter); - } - Err(err) => { - eprintln!("Failed to parse log filter: {}", err); - } - } -} - -static ZLOG: Zlog = Zlog {}; - -pub struct Zlog {} - -impl log::Log for Zlog { - fn enabled(&self, metadata: &log::Metadata) -> bool { - filter::is_possibly_enabled_level(metadata.level()) - } - - fn log(&self, record: &log::Record) { - if !self.enabled(record.metadata()) { - return; - } - let module_path = record.module_path().or(record.file()); - let (crate_name_scope, module_scope) = match module_path { - Some(module_path) => { - let crate_name = private::extract_crate_name_from_module_path(module_path); - let crate_name_scope = private::scope_ref_new(&[crate_name]); - let module_scope = private::scope_ref_new(&[module_path]); - (crate_name_scope, module_scope) - } - None => { - // TODO: when do we hit this - (private::scope_new(&[]), private::scope_new(&["*unknown*"])) - } - }; - let level = record.metadata().level(); - if !filter::is_scope_enabled(&crate_name_scope, Some(record.target()), level) { - return; - } - sink::submit(sink::Record { - scope: module_scope, - level, - message: record.args(), - // PERF(batching): store non-static paths in a cache + leak them and pass static str here - module_path, - line: record.line(), - }); - } - - fn flush(&self) { - sink::flush(); - } -} - -#[macro_export] -macro_rules! log { - ($logger:expr, $level:expr, $($arg:tt)+) => { - let level = $level; - let logger = $logger; - let enabled = $crate::filter::is_scope_enabled(&logger.scope, Some(module_path!()), level); - if enabled { - $crate::sink::submit($crate::sink::Record { - scope: logger.scope, - level, - message: &format_args!($($arg)+), - module_path: Some(module_path!()), - line: Some(line!()), - }); - } - } -} - -#[macro_export] -macro_rules! trace { - ($logger:expr => $($arg:tt)+) => { - $crate::log!($logger, $crate::log_impl::Level::Trace, $($arg)+); - }; - ($($arg:tt)+) => { - $crate::log!($crate::default_logger!(), $crate::log_impl::Level::Trace, $($arg)+); - }; -} - -#[macro_export] -macro_rules! debug { - ($logger:expr => $($arg:tt)+) => { - $crate::log!($logger, $crate::log_impl::Level::Debug, $($arg)+); - }; - ($($arg:tt)+) => { - $crate::log!($crate::default_logger!(), $crate::log_impl::Level::Debug, $($arg)+); - }; -} - -#[macro_export] -macro_rules! info { - ($logger:expr => $($arg:tt)+) => { - $crate::log!($logger, $crate::log_impl::Level::Info, $($arg)+); - }; - ($($arg:tt)+) => { - $crate::log!($crate::default_logger!(), $crate::log_impl::Level::Info, $($arg)+); - }; -} - -#[macro_export] -macro_rules! warn { - ($logger:expr => $($arg:tt)+) => { - $crate::log!($logger, $crate::log_impl::Level::Warn, $($arg)+); - }; - ($($arg:tt)+) => { - $crate::log!($crate::default_logger!(), $crate::log_impl::Level::Warn, $($arg)+); - }; -} - -#[macro_export] -macro_rules! error { - ($logger:expr => $($arg:tt)+) => { - $crate::log!($logger, $crate::log_impl::Level::Error, $($arg)+); - }; - ($($arg:tt)+) => { - $crate::log!($crate::default_logger!(), $crate::log_impl::Level::Error, $($arg)+); - }; -} - -/// Creates a timer that logs the duration it was active for either when -/// it is dropped, or when explicitly stopped using the `end` method. -/// Logs at the `trace` level. -/// Note that it will include time spent across await points -/// (i.e. should not be used to measure the performance of async code) -/// However, this is a feature not a bug, as it allows for a more accurate -/// understanding of how long the action actually took to complete, including -/// interruptions, which can help explain why something may have timed out, -/// why it took longer to complete than it would have had the await points resolved -/// immediately, etc. -#[macro_export] -macro_rules! time { - ($logger:expr => $name:expr) => { - $crate::Timer::new($logger, $name) - }; - ($name:expr) => { - $crate::time!($crate::default_logger!() => $name) - }; -} - -#[macro_export] -macro_rules! scoped { - ($parent:expr => $name:expr) => {{ - $crate::scoped_logger($parent, $name) - }}; - ($name:expr) => { - $crate::scoped!($crate::default_logger!() => $name) - }; -} - -pub const fn scoped_logger(parent: Logger, name: &'static str) -> Logger { - let mut scope = parent.scope; - let mut index = 1; // always have crate/module name - while index < scope.len() && !scope[index].is_empty() { - index += 1; - } - if index >= scope.len() { - #[cfg(debug_assertions)] - { - panic!("Scope overflow trying to add scope... ignoring scope"); - } - } - scope[index] = name; - Logger { scope } -} - -#[macro_export] -macro_rules! default_logger { - () => { - $crate::Logger { - scope: $crate::private::scope_new(&[$crate::crate_name!()]), - } - }; -} - -#[macro_export] -macro_rules! crate_name { - () => { - $crate::private::extract_crate_name_from_module_path(module_path!()) - }; -} - -/// functions that are used in macros, and therefore must be public, -/// but should not be used directly -pub mod private { - use super::*; - - pub const fn extract_crate_name_from_module_path(module_path: &str) -> &str { - let mut i = 0; - let mod_path_bytes = module_path.as_bytes(); - let mut index = mod_path_bytes.len(); - while i + 1 < mod_path_bytes.len() { - if mod_path_bytes[i] == b':' && mod_path_bytes[i + 1] == b':' { - index = i; - break; - } - i += 1; - } - let Some((crate_name, _)) = module_path.split_at_checked(index) else { - return module_path; - }; - crate_name - } - - pub const fn scope_new(scopes: &[&'static str]) -> Scope { - scope_ref_new(scopes) - } - - pub const fn scope_ref_new<'a>(scopes: &[&'a str]) -> ScopeRef<'a> { - assert!(scopes.len() <= SCOPE_DEPTH_MAX); - let mut scope = [""; SCOPE_DEPTH_MAX]; - let mut i = 0; - while i < scopes.len() { - scope[i] = scopes[i]; - i += 1; - } - scope - } - - pub fn scope_alloc_new(scopes: &[&str]) -> ScopeAlloc { - assert!(scopes.len() <= SCOPE_DEPTH_MAX); - let mut scope = [""; SCOPE_DEPTH_MAX]; - scope[0..scopes.len()].copy_from_slice(scopes); - scope.map(|s| s.to_string()) - } - - pub fn scope_to_alloc(scope: &Scope) -> ScopeAlloc { - scope.map(|s| s.to_string()) - } -} - -pub type Scope = [&'static str; SCOPE_DEPTH_MAX]; -pub type ScopeRef<'a> = [&'a str; SCOPE_DEPTH_MAX]; -pub type ScopeAlloc = [String; SCOPE_DEPTH_MAX]; -const SCOPE_STRING_SEP_STR: &str = "."; -const SCOPE_STRING_SEP_CHAR: char = '.'; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Logger { - pub scope: Scope, -} - -impl log::Log for Logger { - fn enabled(&self, metadata: &log::Metadata) -> bool { - filter::is_possibly_enabled_level(metadata.level()) - } - - fn log(&self, record: &log::Record) { - if !self.enabled(record.metadata()) { - return; - } - let level = record.metadata().level(); - if !filter::is_scope_enabled(&self.scope, Some(record.target()), level) { - return; - } - sink::submit(sink::Record { - scope: self.scope, - level, - message: record.args(), - module_path: record.module_path(), - line: record.line(), - }); - } - - fn flush(&self) { - sink::flush(); - } -} - -pub struct Timer { - pub logger: Logger, - pub start_time: std::time::Instant, - pub name: &'static str, - pub warn_if_longer_than: Option, - pub done: bool, -} - -impl Drop for Timer { - fn drop(&mut self) { - self.finish(); - } -} - -impl Timer { - #[must_use = "Timer will stop when dropped, the result of this function should be saved in a variable prefixed with `_` if it should stop when dropped"] - pub fn new(logger: Logger, name: &'static str) -> Self { - Self { - logger, - name, - start_time: std::time::Instant::now(), - warn_if_longer_than: None, - done: false, - } - } - - pub fn warn_if_gt(mut self, warn_limit: std::time::Duration) -> Self { - self.warn_if_longer_than = Some(warn_limit); - self - } - - pub fn end(mut self) { - self.finish(); - } - - fn finish(&mut self) { - if self.done { - return; - } - let elapsed = self.start_time.elapsed(); - if let Some(warn_limit) = self.warn_if_longer_than - && elapsed > warn_limit - { - crate::warn!( - self.logger => - "Timer '{}' took {:?}. Which was longer than the expected limit of {:?}", - self.name, - elapsed, - warn_limit - ); - self.done = true; - return; - } - crate::trace!( - self.logger => - "Timer '{}' finished in {:?}", - self.name, - elapsed - ); - self.done = true; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_crate_name() { - assert_eq!(crate_name!(), "zlog"); - assert_eq!( - private::extract_crate_name_from_module_path("my_speedy_⚡️_crate::some_module"), - "my_speedy_⚡️_crate" - ); - assert_eq!( - private::extract_crate_name_from_module_path("my_speedy_crate_⚡️::some_module"), - "my_speedy_crate_⚡️" - ); - assert_eq!( - private::extract_crate_name_from_module_path("my_speedy_crate_:⚡️:some_module"), - "my_speedy_crate_:⚡️:some_module" - ); - assert_eq!( - private::extract_crate_name_from_module_path("my_speedy_crate_::⚡️some_module"), - "my_speedy_crate_" - ); - } -} diff --git a/crates/zlog_settings/Cargo.toml b/crates/zlog_settings/Cargo.toml deleted file mode 100644 index 39c3b6a193..0000000000 --- a/crates/zlog_settings/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "zlog_settings" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/zlog_settings.rs" - -[features] -default = [] - -[dependencies] -gpui.workspace = true -collections.workspace = true -settings.workspace = true -zlog.workspace = true diff --git a/crates/zlog_settings/LICENSE-GPL b/crates/zlog_settings/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/zlog_settings/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/zlog_settings/src/zlog_settings.rs b/crates/zlog_settings/src/zlog_settings.rs deleted file mode 100644 index cb09375b9a..0000000000 --- a/crates/zlog_settings/src/zlog_settings.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! # zlog_settings -use collections::HashMap; - -use gpui::App; -use settings::{RegisterSetting, Settings, SettingsStore}; - -pub fn init(cx: &mut App) { - cx.observe_global::(|cx| { - let zlog_settings = ZlogSettings::get_global(cx); - zlog::filter::refresh_from_settings(&zlog_settings.scopes); - }) - .detach(); -} - -#[derive(Clone, Debug, RegisterSetting)] -pub struct ZlogSettings { - /// A map of log scopes to the desired log level. - /// Useful for filtering out noisy logs or enabling more verbose logging. - /// - /// Example: {"log": {"client": "warn"}} - pub scopes: HashMap, -} - -impl Settings for ZlogSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - ZlogSettings { - scopes: content.log.clone().unwrap(), - } - } -} diff --git a/crates/ztracing/Cargo.toml b/crates/ztracing/Cargo.toml deleted file mode 100644 index 0d9f15b9af..0000000000 --- a/crates/ztracing/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "ztracing" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[features] -tracy = ["tracing-tracy"] - -[dependencies] -zlog.workspace = true -tracing.workspace = true - -tracing-subscriber = "0.3.22" -tracing-tracy = { version = "0.11.4", optional = true, features = ["enable", "ondemand"] } - -ztracing_macro.workspace = true diff --git a/crates/ztracing/LICENSE-AGPL b/crates/ztracing/LICENSE-AGPL deleted file mode 120000 index 5f5cf25dc4..0000000000 --- a/crates/ztracing/LICENSE-AGPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-AGPL \ No newline at end of file diff --git a/crates/ztracing/LICENSE-APACHE b/crates/ztracing/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/crates/ztracing/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ztracing/LICENSE-GPL b/crates/ztracing/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/ztracing/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/ztracing/build.rs b/crates/ztracing/build.rs deleted file mode 100644 index dc0d0ad704..0000000000 --- a/crates/ztracing/build.rs +++ /dev/null @@ -1,9 +0,0 @@ -use std::env; - -fn main() { - if env::var_os("ZTRACING").is_some() { - println!(r"cargo::rustc-cfg=ztracing"); - } - println!("cargo::rerun-if-changed=build.rs"); - println!("cargo::rerun-if-env-changed=ZTRACING"); -} diff --git a/crates/ztracing/src/lib.rs b/crates/ztracing/src/lib.rs deleted file mode 100644 index b9b318cc35..0000000000 --- a/crates/ztracing/src/lib.rs +++ /dev/null @@ -1,52 +0,0 @@ -pub use tracing::Level; - -#[cfg(ztracing)] -pub use tracing::{ - debug_span, error_span, event, info_span, instrument, span, trace_span, warn_span, -}; -#[cfg(not(ztracing))] -pub use ztracing_macro::instrument; - -#[cfg(not(ztracing))] -pub use __consume_all_tokens as trace_span; -#[cfg(not(ztracing))] -pub use __consume_all_tokens as info_span; -#[cfg(not(ztracing))] -pub use __consume_all_tokens as debug_span; -#[cfg(not(ztracing))] -pub use __consume_all_tokens as warn_span; -#[cfg(not(ztracing))] -pub use __consume_all_tokens as error_span; -#[cfg(not(ztracing))] -pub use __consume_all_tokens as event; -#[cfg(not(ztracing))] -pub use __consume_all_tokens as span; - -#[cfg(not(ztracing))] -#[macro_export] -macro_rules! __consume_all_tokens { - ($($t:tt)*) => { - $crate::FakeSpan - }; -} - -pub struct FakeSpan; -impl FakeSpan { - pub fn enter(&self) {} -} - -// #[cfg(not(ztracing))] -// pub use span; - -#[cfg(ztracing)] -pub fn init() { - zlog::info!("Starting tracy subscriber, you can now connect the profiler"); - use tracing_subscriber::prelude::*; - tracing::subscriber::set_global_default( - tracing_subscriber::registry().with(tracing_tracy::TracyLayer::default()), - ) - .expect("setup tracy layer"); -} - -#[cfg(not(ztracing))] -pub fn init() {} diff --git a/crates/ztracing_macro/Cargo.toml b/crates/ztracing_macro/Cargo.toml deleted file mode 100644 index dbd7adce5f..0000000000 --- a/crates/ztracing_macro/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "ztracing_macro" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lib] -proc-macro = true - -[dependencies] diff --git a/crates/ztracing_macro/LICENSE-AGPL b/crates/ztracing_macro/LICENSE-AGPL deleted file mode 120000 index 5f5cf25dc4..0000000000 --- a/crates/ztracing_macro/LICENSE-AGPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-AGPL \ No newline at end of file diff --git a/crates/ztracing_macro/LICENSE-APACHE b/crates/ztracing_macro/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/crates/ztracing_macro/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ztracing_macro/LICENSE-GPL b/crates/ztracing_macro/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/ztracing_macro/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/ztracing_macro/src/lib.rs b/crates/ztracing_macro/src/lib.rs deleted file mode 100644 index d9b073ed13..0000000000 --- a/crates/ztracing_macro/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[proc_macro_attribute] -pub fn instrument( - _attr: proc_macro::TokenStream, - item: proc_macro::TokenStream, -) -> proc_macro::TokenStream { - item -} diff --git a/debug.plist b/debug.plist deleted file mode 100644 index e09573c9d1..0000000000 --- a/debug.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.get-task-allow - - - diff --git a/default.nix b/default.nix deleted file mode 100644 index 1d976a3576..0000000000 --- a/default.nix +++ /dev/null @@ -1,11 +0,0 @@ -(import ( - let - lock = builtins.fromJSON (builtins.readFile ./flake.lock); - in - fetchTarball { - url = - lock.nodes.flake-compat.locked.url - or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; - sha256 = lock.nodes.flake-compat.locked.narHash; - } -) { src = ./.; }).defaultNix diff --git a/docker-compose.sql b/docker-compose.sql deleted file mode 100644 index 7de55a1f98..0000000000 --- a/docker-compose.sql +++ /dev/null @@ -1,2 +0,0 @@ -create database zed; -create database zed_llm; diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 3006b271da..0000000000 --- a/docs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -book/ diff --git a/docs/.prettierignore b/docs/.prettierignore deleted file mode 100644 index a52439689a..0000000000 --- a/docs/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -# Handlebars partials are not supported by Prettier. -*.hbs diff --git a/docs/.prettierrc b/docs/.prettierrc deleted file mode 100644 index 1c5e966021..0000000000 --- a/docs/.prettierrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "printWidth": 80 -} diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index e1649f4bc9..0000000000 --- a/docs/README.md +++ /dev/null @@ -1,132 +0,0 @@ -# Zed Docs - -Welcome to Zed's documentation. - -This is built on push to `main` and published automatically to [https://zed.dev/docs](https://zed.dev/docs). - -To preview the docs locally you will need to install [mdBook](https://rust-lang.github.io/mdBook/) (`cargo install mdbook@0.4.40`) and then run: - -```sh -mdbook serve docs -``` - -It's important to note the version number above. For an unknown reason, as of 2025-04-23, running 0.4.48 will cause odd URL behavior that breaks things. - -Before committing, verify that the docs are formatted in the way Prettier expects with: - -``` -cd docs && pnpm dlx prettier@3.5.0 . --write && cd .. -``` - -## Preprocessor - -We have a custom mdBook preprocessor for interfacing with our crates (`crates/docs_preprocessor`). - -If for some reason you need to bypass the docs preprocessor, you can comment out `[preprocessor.zed_docs_preprocessor]` from the `book.toml`. - -## Images and videos - -To add images or videos to the docs, upload them to another location (e.g., zed.dev, GitHub's asset storage) and then link out to them from the docs. - -Putting binary assets such as images in the Git repository will bloat the repository size over time. - -## Internal notes: - -- We have a Cloudflare router called `docs-proxy` that intercepts requests to `zed.dev/docs` and forwards them to the "docs" Cloudflare Pages project. -- The CI uploads a new version to the Cloudflare Pages project from `.github/workflows/deploy_docs.yml` on every push to `main`. - -### Table of Contents - -The table of contents files (`theme/page-toc.js` and `theme/page-doc.css`) were initially generated by [`mdbook-pagetoc`](https://crates.io/crates/mdbook-pagetoc). - -Since all this preprocessor does is generate the static assets, we don't need to keep it around once they have been generated. - -## Referencing Keybindings and Actions - -When referencing keybindings or actions, use the following formats: - -### Keybindings - -`{#kb scope::Action}` - e.g., `{#kb zed::OpenSettings}`. - -This will output a code element like: `Cmd + , | Ctrl + ,`. We then use a client-side plugin to show the actual keybinding based on the user's platform. - -By using the action name, we can ensure that the keybinding is always up-to-date rather than hardcoding the keybinding. - -### Actions - -`{#action scope::Action}` - e.g., `{#action zed::OpenSettings}`. - -This will render a human-readable version of the action name, e.g., "zed: open settings", and will allow us to implement things like additional context on hover, etc. - -### Creating New Templates - -Templates are functions that modify the source of the docs pages (usually with a regex match and replace). -You can see how the actions and keybindings are templated in `crates/docs_preprocessor/src/main.rs` for reference on how to create new templates. - -### References - -- Template Trait: `crates/docs_preprocessor/src/templates.rs` -- Example template: `crates/docs_preprocessor/src/templates/keybinding.rs` -- Client-side plugins: `docs/theme/plugins.js` - -## Postprocessor - -A postprocessor is implemented as a sub-command of `docs_preprocessor` that wraps the built-in HTML renderer and applies post-processing to the HTML files, to add support for page-specific title and `meta` tag description values. - -An example of the syntax can be found in `git.md`, as well as below: - -```md ---- -title: Some more detailed title for this page -description: A page-specific description ---- - -# Editor -``` - -The above code will be transformed into (with non-relevant tags removed): - -```html - - Editor | Some more detailed title for this page - - - -

Editor

- -``` - -If no front matter is provided, or if one or both keys aren't provided, the `title` and `description` will be set based on the `default-title` and `default-description` keys in `book.toml` respectively. - -### Implementation details - -Unfortunately, mdBook does not support post-processing like it does pre-processing, and only supports defining one description to put in the `meta` tag per book rather than per file. -So in order to apply post-processing (necessary to modify the HTML `head` tags) the global book description is set to a marker value `#description#` and the HTML renderer is replaced with a sub-command of `docs_preprocessor` that wraps the built-in HTML renderer and applies post-processing to the HTML files, replacing the marker value and the `(.*)` with the contents of the front matter if there is one. - -### Known limitations - -The front matter parsing is extremely simple, which avoids needing to take on an additional dependency, or implement full YAML parsing. - -- Double quotes and multi-line values are not supported, i.e. Keys and values must be entirely on the same line, with no double quotes around the value. - -The following will not work: - -```md ---- -title: Some - Multi-line - Title ---- -``` - -neither this: - -```md ---- -title: "Some title" ---- -``` - -- The front matter must be at the top of the file, with only white-space preceding it. -- The contents of the `title` and `description` will not be HTML escaped. They should be simple ASCII text with no unicode or emoji characters. diff --git a/docs/book.toml b/docs/book.toml deleted file mode 100644 index 2bb57c5c08..0000000000 --- a/docs/book.toml +++ /dev/null @@ -1,102 +0,0 @@ -[book] -authors = ["The Zed Team"] -language = "en" -multilingual = false -src = "src" -title = "Zed" -site-url = "/docs/" - -[build] -extra-watch-dirs = ["../crates/docs_preprocessor"] - -# zed-html is a "custom" renderer that just wraps the -# builtin mdbook html renderer, and applies post-processing -# as post-processing is not possible with mdbook in the same way -# pre-processing is -# The config is passed directly to the html renderer, so all config -# options that apply to html apply to zed-html -[output.zed-html] -command = "cargo run -p docs_preprocessor -- postprocess" -# Set here instead of above as we only use it replace the `#description#` we set in the template -# when no front-matter is provided value -default-description = "Learn how to use and customize Zed, the fast, collaborative code editor. Official docs on features, configuration, AI tools, and workflows." -default-title = "Zed Code Editor Documentation" -no-section-label = true -preferred-dark-theme = "dark" -additional-css = ["theme/page-toc.css", "theme/plugins.css", "theme/highlight.css"] -additional-js = ["theme/page-toc.js", "theme/plugins.js"] - -[output.zed-html.print] -enable = false - -# Redirects for `/docs` pages. -# -# All of the source URLs are interpreted relative to mdBook, so they must: -# 1. Not start with `/docs` -# 2. End in `.html` -# -# The destination URLs are interpreted relative to `https://zed.dev`. -# - Redirects to other docs pages should end in `.html` -# - You can link to pages on the Zed site by omitting the `/docs` in front of it. -[output.zed-html.redirect] -# AI -"/ai.html" = "/docs/ai/overview.html" -"/assistant-panel.html" = "/docs/ai/agent-panel.html" -"/assistant.html" = "/docs/assistant/assistant.html" -"/assistant/assistant-panel.html" = "/docs/ai/agent-panel.html" -"/assistant/assistant.html" = "/docs/ai/overview.html" -"/assistant/commands.html" = "/docs/ai/text-threads.html" -"/assistant/configuration.html" = "/docs/ai/configuration.html" -"/assistant/context-servers.html" = "/docs/ai/mcp.html" -"/assistant/contexts.html" = "/docs/ai/text-threads.html" -"/assistant/inline-assistant.html" = "/docs/ai/inline-assistant.html" -"/assistant/model-context-protocol.html" = "/docs/ai/mcp.html" -"/assistant/prompting.html" = "/docs/ai/rules.html" -"/language-model-integration.html" = "/docs/assistant/assistant.html" -"/model-improvement.html" = "/docs/ai/ai-improvement.html" -"/ai/temperature.html" = "/docs/ai/agent-settings.html#model-temperature" - -# Collaboration -"/channels.html" = "/docs/collaboration/channels.html" -"/collaboration.html" = "/docs/collaboration/overview.html" - -# Community -"/community/feedback.html" = "/community-links" -"/conversations.html" = "/community-links" - -# Debugger -"/debuggers.html" = "/docs/debugger.html" - -# MCP -"/assistant/model-context-protocolCitedby.html" = "/docs/ai/mcp.html" -"/context-servers.html" = "/docs/ai/mcp.html" -"/extensions/context-servers.html" = "/docs/extensions/mcp-extensions.html" - -# Languages -"/adding-new-languages.html" = "/docs/extensions/languages.html" -"/elixir.html" = "/docs/languages/elixir.html" -"/javascript.html" = "/docs/languages/javascript.html" -"/languages/languages/html.html" = "/docs/languages/html.html" -"/languages/languages/javascript.html" = "/docs/languages/javascript.html" -"/languages/languages/makefile.html" = "/docs/languages/makefile.html" -"/languages/languages/nim.html" = "/docs/languages/nim.html" -"/languages/languages/ruby.html" = "/docs/languages/ruby.html" -"/languages/languages/scala.html" = "/docs/languages/scala.html" -"/python.html" = "/docs/languages/python.html" -"/ruby.html" = "/docs/languages/ruby.html" - -# Zed development -"/contribute-to-zed.html" = "/docs/development.html#contributor-links" -"/contributing.html" = "/docs/development.html#contributor-links" -"/developing-zed.html" = "/docs/development.html" -"/development/development/linux.html" = "/docs/development/linux.html" -"/development/development/macos.html" = "/docs/development/macos.html" -"/development/development/windows.html" = "/docs/development/windows.html" - -# Our custom preprocessor for expanding commands like `{#kb action::ActionName}`, -# and other docs-related functions. -# -# Comment the below section out if you need to bypass the preprocessor for some reason. -[preprocessor.zed_docs_preprocessor] -command = "cargo run -p docs_preprocessor --" -renderer = ["html"] diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md deleted file mode 100644 index 9d1f6f61d4..0000000000 --- a/docs/src/SUMMARY.md +++ /dev/null @@ -1,179 +0,0 @@ -# Summary - -# Welcome - -- [Getting Started](./getting-started.md) -- [Installation](./installation.md) - - [Update](./update.md) - - [Uninstall](./uninstall.md) -- [Authenticate](./authentication.md) -- [Telemetry](./telemetry.md) -- [Troubleshooting](./troubleshooting.md) - -# Configuration - -- [Configuring Zed](./configuring-zed.md) -- [Configuring Languages](./configuring-languages.md) - - [Toolchains](./toolchains.md) -- [Key bindings](./key-bindings.md) - - [All Actions](./all-actions.md) -- [Snippets](./snippets.md) -- [Themes](./themes.md) -- [Icon Themes](./icon-themes.md) -- [Visual Customization](./visual-customization.md) -- [Vim Mode](./vim.md) -- [Helix Mode](./helix.md) - - - - -# Using Zed - -- [Multibuffers](./multibuffers.md) -- [Command Palette](./command-palette.md) -- [Command-line Interface](./command-line-interface.md) -- [Outline Panel](./outline-panel.md) -- [Code Completions](./completions.md) -- [Collaboration](./collaboration/overview.md) - - [Channels](./collaboration/channels.md) - - [Contacts and Private Calls](./collaboration/contacts-and-private-calls.md) -- [Git](./git.md) -- [Debugger](./debugger.md) -- [Diagnostics](./diagnostics.md) -- [Tasks](./tasks.md) -- [Tab Switcher](./tab-switcher.md) -- [Remote Development](./remote-development.md) -- [Environment Variables](./environment.md) -- [REPL](./repl.md) - -# Platform Support - -- [Windows](./windows.md) -- [Linux](./linux.md) - -# AI - -- [Overview](./ai/overview.md) -- [Agent Panel](./ai/agent-panel.md) - - [Tools](./ai/tools.md) - - [External Agents](./ai/external-agents.md) -- [Inline Assistant](./ai/inline-assistant.md) -- [Edit Prediction](./ai/edit-prediction.md) -- [Text Threads](./ai/text-threads.md) -- [Rules](./ai/rules.md) -- [Model Context Protocol](./ai/mcp.md) -- [Configuration](./ai/configuration.md) - - [LLM Providers](./ai/llm-providers.md) - - [Agent Settings](./ai/agent-settings.md) -- [Subscription](./ai/subscription.md) - - [Models](./ai/models.md) - - [Plans and Usage](./ai/plans-and-usage.md) - - [Billing](./ai/billing.md) -- [Privacy and Security](./ai/privacy-and-security.md) - - [AI Improvement](./ai/ai-improvement.md) - -# Extensions - -- [Overview](./extensions.md) -- [Installing Extensions](./extensions/installing-extensions.md) -- [Developing Extensions](./extensions/developing-extensions.md) -- [Extension Capabilities](./extensions/capabilities.md) -- [Language Extensions](./extensions/languages.md) -- [Debugger Extensions](./extensions/debugger-extensions.md) -- [Theme Extensions](./extensions/themes.md) -- [Icon Theme Extensions](./extensions/icon-themes.md) -- [Slash Command Extensions](./extensions/slash-commands.md) -- [Agent Server Extensions](./extensions/agent-servers.md) -- [MCP Server Extensions](./extensions/mcp-extensions.md) - -# Migrate - -- [VS Code](./migrate/vs-code.md) - -# Language Support - -- [All Languages](./languages.md) -- [Ansible](./languages/ansible.md) -- [AsciiDoc](./languages/asciidoc.md) -- [Astro](./languages/astro.md) -- [Bash](./languages/bash.md) -- [Biome](./languages/biome.md) -- [C](./languages/c.md) -- [C++](./languages/cpp.md) -- [C#](./languages/csharp.md) -- [Clojure](./languages/clojure.md) -- [CSS](./languages/css.md) -- [Dart](./languages/dart.md) -- [Deno](./languages/deno.md) -- [Diff](./languages/diff.md) -- [Docker](./languages/docker.md) -- [Elixir](./languages/elixir.md) -- [Elm](./languages/elm.md) -- [Emmet](./languages/emmet.md) -- [Erlang](./languages/erlang.md) -- [Fish](./languages/fish.md) -- [GDScript](./languages/gdscript.md) -- [Gleam](./languages/gleam.md) -- [GLSL](./languages/glsl.md) -- [Go](./languages/go.md) -- [Groovy](./languages/groovy.md) -- [Haskell](./languages/haskell.md) -- [Helm](./languages/helm.md) -- [HTML](./languages/html.md) -- [Java](./languages/java.md) -- [JavaScript](./languages/javascript.md) -- [Julia](./languages/julia.md) -- [JSON](./languages/json.md) -- [Jsonnet](./languages/jsonnet.md) -- [Kotlin](./languages/kotlin.md) -- [Lua](./languages/lua.md) -- [Luau](./languages/luau.md) -- [Makefile](./languages/makefile.md) -- [Markdown](./languages/markdown.md) -- [Nim](./languages/nim.md) -- [OCaml](./languages/ocaml.md) -- [OpenTofu](./languages/opentofu.md) -- [PHP](./languages/php.md) -- [PowerShell](./languages/powershell.md) -- [Prisma](./languages/prisma.md) -- [Proto](./languages/proto.md) -- [PureScript](./languages/purescript.md) -- [Python](./languages/python.md) -- [R](./languages/r.md) -- [Rego](./languages/rego.md) -- [ReStructuredText](./languages/rst.md) -- [Racket](./languages/racket.md) -- [Roc](./languages/roc.md) -- [Ruby](./languages/ruby.md) -- [Rust](./languages/rust.md) -- [Scala](./languages/scala.md) -- [Scheme](./languages/scheme.md) -- [Shell Script](./languages/sh.md) -- [SQL](./languages/sql.md) -- [Svelte](./languages/svelte.md) -- [Swift](./languages/swift.md) -- [Tailwind CSS](./languages/tailwindcss.md) -- [Terraform](./languages/terraform.md) -- [TOML](./languages/toml.md) -- [TypeScript](./languages/typescript.md) -- [Uiua](./languages/uiua.md) -- [Vue](./languages/vue.md) -- [XML](./languages/xml.md) -- [YAML](./languages/yaml.md) -- [Yara](./languages/yara.md) -- [Yarn](./languages/yarn.md) -- [Zig](./languages/zig.md) - -# Developing Zed - -- [Developing Zed](./development.md) - - [macOS](./development/macos.md) - - [Linux](./development/linux.md) - - [Windows](./development/windows.md) - - [FreeBSD](./development/freebsd.md) - - [Local Collaboration](./development/local-collaboration.md) - - [Using Debuggers](./development/debuggers.md) - - [Performance](./performance.md) - - [Glossary](./development/glossary.md) -- [Release Notes](./development/release-notes.md) -- [Debugging Crashes](./development/debugging-crashes.md) diff --git a/docs/src/ai/agent-panel.md b/docs/src/ai/agent-panel.md deleted file mode 100644 index c383862ed6..0000000000 --- a/docs/src/ai/agent-panel.md +++ /dev/null @@ -1,184 +0,0 @@ -# Agent Panel - -The Agent Panel allows you to interact with many LLMs and coding agents that can help with various types of tasks, such as generating code, codebase understanding, and other general inquiries like writing emails, documentation, and more. - -To open it, use the `agent: new thread` action in [the Command Palette](../getting-started.md#command-palette) or click the ✨ (sparkles) icon in the status bar. - -## Getting Started {#getting-started} - -If you're using the Agent Panel for the first time, you need to have at least one LLM provider or external agent configured. -You can do that by: - -1. [subscribing to our Pro plan](https://zed.dev/pricing), so you have access to our hosted models -2. [using your own API keys](./llm-providers.md#use-your-own-keys), either from model providers like Anthropic or model gateways like OpenRouter. -3. using an [external agent](./external-agents.md) like [Gemini CLI](./external-agents.md#gemini-cli) or [Claude Code](./external-agents.md#claude-code) - -## Overview {#overview} - -With an LLM provider or an external agent configured, type at the message editor and hit `enter` to submit your prompt. -If you need extra room to type, you can expand the message editor with {#kb agent::ExpandMessageEditor}. - -You should start to see the responses stream in with indications of [which tools](./tools.md) the model is using to fulfill your prompt. -From this point on, you can interact with the many supported features outlined below. - -> Note that for external agents, like [Gemini CLI](./external-agents.md#gemini-cli) or [Claude Code](./external-agents.md#claude-code), some of the features outlined below may _not_ be supported—for example, _restoring threads from history_, _checkpoints_, _token usage display_, and others. Their availability varies depending on the agent. - -### Creating New Threads {#new-thread} - -By default, the Agent Panel uses Zed's first-party agent. - -To change that, go to the plus button in the top-right of the Agent Panel and choose another option. -You can choose to create a new [Text Thread](./text-threads.md) or, if you have [external agents](./external-agents.md) connected, you can create new threads with them. - -### Editing Messages {#editing-messages} - -Any message that you send to the AI is editable. -You can click on the card that contains your message and re-submit it with an adjusted prompt and/or new pieces of context. - -### Checkpoints {#checkpoints} - -Every time the AI performs an edit, you should see a "Restore Checkpoint" button at the top of your message, allowing you to return your code base to the state it was in prior to that message. - -The checkpoint button appears even if you interrupt the thread midway through an edit attempt, as this is likely a moment when you've identified that the agent is not heading in the right direction and you want to revert back. - -### Navigating History {#navigating-history} - -To quickly navigate through recently opened threads, use the {#kb agent::ToggleNavigationMenu} binding, when focused on the panel's editor, or click the menu icon button at the top right of the panel to open the dropdown that shows you the six most recent threads. - -The items in this menu function similarly to tabs, and closing them doesn’t delete the thread; instead, it simply removes them from the recent list. - -To view all historical conversations, reach for the `View All` option from within the same menu or via the {#kb agent::OpenHistory} binding. - -### Following the Agent {#following-the-agent} - -Zed is built with collaboration natively integrated, and this design pattern extends to collaboration with AI. To follow the agent as it reads and edits in your codebase, click on the "crosshair" icon button at the bottom left of the panel. - -You can also do that with the keyboard by pressing the `cmd`/`ctrl` modifier with `enter` when submitting a message. - -### Get Notified {#get-notified} - -If you send a prompt to the Agent and then move elsewhere, putting Zed in the background, you can be notified when its response is finished via: - -- a visual notification that appears in the top right of your screen -- a sound notification - -These notifications can be used together or individually, according to your preference. - -You can customize their behavior, including turning them off entirely, by using the `agent.notify_when_agent_waiting` and `agent.play_sound_when_agent_done` settings keys. - -### Reviewing Changes {#reviewing-changes} - -Once the agent has made changes to your project, the panel will surface which files, and how many of them, have been edited. - -To see which files specifically have been edited, expand the accordion bar that shows up right above the message editor or click the `Review Changes` button ({#kb agent::OpenAgentDiff}), which opens a multi-buffer tab with all changes. - -You're able to reject or accept each individual change hunk, or the whole set of changes made by the agent. - -Edit diffs also appear in individual buffers. If your active tab had edits made by the AI, you'll see diffs with the same accept/reject controls as in the multi-buffer. - -## Adding Context {#adding-context} - -Although Zed's agent is very efficient at reading through your code base to autonomously pick up relevant context, manually adding whatever would be useful to fulfill your prompt is still very encouraged as a way to not only improve the AI's response quality but also to speed up its response time. - -In Zed's Agent Panel, all pieces of context are added as mentions in the panel's message editor. -You can type `@` to mention files, directories, symbols, previous threads, and rules files. - -Copying images and pasting them in the panel's message editor is also supported. - -### Selection as Context - -Additionally, you can also select text in a buffer and add it as context by using the {#kb agent::AddSelectionToThread} keybinding, running the {#action agent::AddSelectionToThread} action, or choosing the "Selection" item in the `@` menu. - -## Token Usage {#token-usage} - -Zed surfaces how many tokens you are consuming for your currently active thread near the profile selector in the panel's message editor. Depending on how many pieces of context you add, your token consumption can grow rapidly. - -Once you approach the model's context window, a banner appears below the message editor suggesting to start a new thread with the current one summarized and added as context. -You can also do this at any time with an ongoing thread via the "Agent Options" menu on the top right. - -## Changing Models {#changing-models} - -After you've configured your LLM providers—either via [a custom API key](./llm-providers.md) or through [Zed's hosted models](./models.md)—you can switch between them by clicking on the model selector on the message editor or by using the {#kb agent::ToggleModelSelector} keybinding. - -> The same model can be offered via multiple providers - for example, Claude Sonnet 4 is available via Zed Pro, OpenRouter, Anthropic directly, and more. -> Make sure you've selected the correct model **_provider_** for the model you'd like to use, delineated by the logo to the left of the model in the model selector. - -## Using Tools {#using-tools} - -The new Agent Panel supports tool calling, which enables agentic editing. -Zed comes with [several built-in tools](./tools.md) that allow models to perform tasks such as searching through your codebase, editing files, running commands, and others. - -You can also extend the set of available tools via [MCP Servers](./mcp.md). - -### Profiles {#profiles} - -Profiles act as a way to group tools. -Zed offers three built-in profiles and you can create as many custom ones as you want. - -#### Built-in Profiles {#built-in-profiles} - -- `Write`: A profile with tools to allow the LLM to write to your files and run terminal commands. This one essentially has all built-in tools turned on. -- `Ask`: A profile with read-only tools. Best for asking questions about your code base without the concern of the agent making changes. -- `Minimal`: A profile with no tools. Best for general conversations with the LLM where no knowledge of your code base is necessary. - -You can explore the exact tools enabled in each profile by clicking on the profile selector button > `Configure` button > the one you want to check out. - -Alternatively, you can also use either the command palette, by running {#action agent::ManageProfiles}, or the keybinding directly, {#kb agent::ManageProfiles}, to have access to the profile management modal. - -#### Custom Profiles {#custom-profiles} - -You can also create a custom profile through the Agent Profile modal. -From there, you can choose to `Add New Profile` or fork an existing one with a custom name and your preferred set of tools. - -It's also possible to override built-in profiles. -In the Agent Profile modal, select a built-in profile, navigate to `Configure Tools`, and rearrange the tools you'd like to keep or remove. - -Zed will store this profile in your settings using the same profile name as the default you overrode. - -All custom profiles can be edited via the UI or by hand under the `agent.profiles` key in your `settings.json` file. - -### Tool Approval - -Zed's Agent Panel surfaces the `agent.always_allow_tool_actions` setting that, if turned to `false`, will require you to give permission to any editing attempt as well as tool calls coming from MCP servers. - -You can change that by setting this key to `true` in either your `settings.json` or via the Agent Panel's settings view. - -### Model Support {#model-support} - -Tool calling needs to be individually supported by each model and model provider. -Therefore, despite the presence of tools, some models may not have the ability to pick them up yet in Zed. -You should see a "No tools" label if you select a model that falls into this case. - -All [Zed's hosted models](./models.md) support tool calling out-of-the-box. - -### MCP Servers {#mcp-servers} - -Similarly to the built-in tools, some models may not support all tools included in a given MCP Server. -Zed's UI will inform you about this via a warning icon that appears close to the model selector. - -## Text Threads {#text-threads} - -["Text Threads"](./text-threads.md) present your conversation with the LLM in a different format—as raw text. -With text threads, you have full control over the conversation data. -You can remove and edit responses from the LLM, swap roles, and include more context earlier in the conversation. - -For users who have been with us for some time, you'll notice that text threads are our original assistant panel—users love it for the control it offers. -We do not plan to deprecate text threads, but it should be noted that if you want the AI to write to your code base autonomously, that's only available in the newer, and now default, "Threads". - -## Errors and Debugging {#errors-and-debugging} - -In case of any error or strange LLM response behavior, the best way to help the Zed team debug is by reaching for the `agent: open thread as markdown` action and attaching that data as part of your issue on GitHub. - -You can also open threads as Markdown by clicking on the file icon button, to the right of the thumbs down button, when focused on the panel's editor. - -## Feedback {#feedback} - -Zed supports rating responses from the agent for feedback and improvement. - -> Note that rating responses will send your data related to that response to Zed's servers. -> See [AI Improvement](./ai-improvement.md) and [Privacy and Security](./privacy-and-security.md) for more information about Zed's approach to AI improvement, privacy, and security. -> **_If you don't want data persisted on Zed's servers, don't rate_**. We will not collect data for improving our Agentic offering without you explicitly rating responses. - -The best way you can help influence the next change to Zed's system prompt and tools is by rating the LLM's response via the thumbs up/down buttons at the end of every response. In case of a thumbs down, a new text area will show up where you can add more specifics about what happened. - -You can provide feedback on the thread at any point after the agent responds, and multiple times within the same thread. diff --git a/docs/src/ai/agent-settings.md b/docs/src/ai/agent-settings.md deleted file mode 100644 index 21607649ad..0000000000 --- a/docs/src/ai/agent-settings.md +++ /dev/null @@ -1,266 +0,0 @@ -# Agent Settings - -Learn about all the settings you can customize in Zed's Agent Panel. - -## Model Settings {#model-settings} - -### Default Model {#default-model} - -If you're using [Zed's hosted LLM service](./subscription.md), it sets `claude-sonnet-4` as the default model for agentic work (agent panel, inline assistant) and `gpt-5-nano` as the default "fast" model (thread summarization, git commit messages). If you're not subscribed or want to change these defaults, you can manually edit the `default_model` object in your settings: - -```json [settings] -{ - "agent": { - "default_model": { - "provider": "openai", - "model": "gpt-4o" - } - } -} -``` - -### Feature-specific Models {#feature-specific-models} - -You can assign distinct and specific models for the following AI-powered features: - -- Thread summary model: Used for generating thread summaries -- Inline assistant model: Used for the inline assistant feature -- Commit message model: Used for generating Git commit messages - -```json [settings] -{ - "agent": { - "default_model": { - "provider": "zed.dev", - "model": "claude-sonnet-4" - }, - "inline_assistant_model": { - "provider": "anthropic", - "model": "claude-3-5-sonnet" - }, - "commit_message_model": { - "provider": "openai", - "model": "gpt-4o-mini" - }, - "thread_summary_model": { - "provider": "google", - "model": "gemini-2.0-flash" - } - } -} -``` - -> If a custom model isn't set for one of these features, they automatically fall back to using the default model. - -### Alternative Models for Inline Assists {#alternative-assists} - -With the Inline Assistant in particular, you can send the same prompt to multiple models at once. - -Here's how you can customize your `settings.json` to add this functionality: - -```json [settings] -{ - "agent": { - "default_model": { - "provider": "zed.dev", - "model": "claude-sonnet-4" - }, - "inline_alternatives": [ - { - "provider": "zed.dev", - "model": "gpt-4-mini" - } - ] - } -} -``` - -When multiple models are configured, you'll see in the Inline Assistant UI buttons that allow you to cycle between outputs generated by each model. - -The models you specify here are always used in _addition_ to your [default model](#default-model). - -For example, the following configuration will generate three outputs for every assist. -One with Claude Sonnet 4 (the default model), another with GPT-5-mini, and another one with Gemini 2.5 Flash. - -```json [settings] -{ - "agent": { - "default_model": { - "provider": "zed.dev", - "model": "claude-sonnet-4" - }, - "inline_alternatives": [ - { - "provider": "zed.dev", - "model": "gpt-4-mini" - }, - { - "provider": "zed.dev", - "model": "gemini-2.5-flash" - } - ] - } -} -``` - -### Model Temperature - -Specify a custom temperature for a provider and/or model: - -```json [settings] -"model_parameters": [ - // To set parameters for all requests to OpenAI models: - { - "provider": "openai", - "temperature": 0.5 - }, - // To set parameters for all requests in general: - { - "temperature": 0 - }, - // To set parameters for a specific provider and model: - { - "provider": "zed.dev", - "model": "claude-sonnet-4", - "temperature": 1.0 - } -], -``` - -## Agent Panel Settings {#agent-panel-settings} - -Note that some of these settings are also surfaced in the Agent Panel's settings UI, which you can access either via the `agent: open settings` action or by the dropdown menu on the top-right corner of the panel. - -### Default View - -Use the `default_view` setting to change the default view of the Agent Panel. -You can choose between `thread` (the default) and `text_thread`: - -```json [settings] -{ - "agent": { - "default_view": "text_thread" - } -} -``` - -### Font Size - -Use the `agent_font_size` setting to change the font size of rendered agent responses in the panel. - -```json [settings] -{ - "agent": { - "agent_font_size": 18 - } -} -``` - -> Editors in the Agent Panel—whether that is the main message textarea or previous messages—use monospace fonts and therefore, are controlled by the `buffer_font_size` setting, which is defined globally in your `settings.json`. - -### Auto-run Commands - -Control whether to allow the agent to run commands without asking you for permission. -The default value is `false`. - -```json [settings] -{ - "agent": { - "always_allow_tool_actions": true - } -} -``` - -### Single-file Review - -Control whether to display review actions (accept & reject) in single buffers after the agent is done performing edits. -The default value is `false`. - -```json [settings] -{ - "agent": { - "single_file_review": true - } -} -``` - -When set to false, these controls are only available in the multibuffer review tab. - -### Sound Notification - -Control whether to hear a notification sound when the agent is done generating changes or needs your input. -The default value is `false`. - -```json [settings] -{ - "agent": { - "play_sound_when_agent_done": true - } -} -``` - -### Message Editor Size - -Use the `message_editor_min_lines` setting to control the minimum number of lines of height the agent message editor should have. -It is set to `4` by default, and the max number of lines is always double of the minimum. - -```json [settings] -{ - "agent": { - "message_editor_min_lines": 4 - } -} -``` - -### Modifier to Send - -Make a modifier (`cmd` on macOS, `ctrl` on Linux) required to send messages. -This is encouraged for more thoughtful prompt crafting. -The default value is `false`. - -```json [settings] -{ - "agent": { - "use_modifier_to_send": true - } -} -``` - -### Edit Card - -Use the `expand_edit_card` setting to control whether edit cards show the full diff in the Agent Panel. -It is set to `true` by default, but if set to false, the card's height is capped to a certain number of lines, requiring a click to be expanded. - -```json [settings] -{ - "agent": { - "expand_edit_card": false - } -} -``` - -### Terminal Card - -Use the `expand_terminal_card` setting to control whether terminal cards show the command output in the Agent Panel. -It is set to `true` by default, but if set to false, the card will be fully collapsed even while the command is running, requiring a click to be expanded. - -```json [settings] -{ - "agent": { - "expand_terminal_card": false - } -} -``` - -### Feedback Controls - -Control whether to display the thumbs up/down buttons at the bottom of each agent response, allowing you to give Zed feedback about the agent's performance. -The default value is `true`. - -```json [settings] -{ - "agent": { - "enable_feedback": false - } -} -``` diff --git a/docs/src/ai/ai-improvement.md b/docs/src/ai/ai-improvement.md deleted file mode 100644 index 857ca2c0ef..0000000000 --- a/docs/src/ai/ai-improvement.md +++ /dev/null @@ -1,112 +0,0 @@ -# Zed AI Improvement - -## Agent Panel - -### Opt-In - -When you use the Agent Panel through any of these means: - -- [Zed's hosted models](./subscription.md) -- [connecting a non-Zed AI service via API key](./llm-providers.md) -- using an [external agent](./external-agents.md) - -Zed does not persistently store user content or use user content to evaluate and/or improve our AI features, unless it is explicitly shared with Zed. Each share is opt-in, and sharing once will not cause future content or data to be shared again. - -> Note that rating responses will send your data related to that response to Zed's servers. -> **_If you don't want data persisted on Zed's servers, don't rate_**. We will not collect data for improving our Agentic offering without you explicitly rating responses. - -When using upstream services through Zed's hosted models, we require assurances from our service providers that your user content won't be used for training models. - -| Provider | No Training Guarantee | Zero-Data Retention (ZDR) | -| --------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -| Anthropic | [Yes](https://www.anthropic.com/legal/commercial-terms) | [Yes](https://privacy.anthropic.com/en/articles/8956058-i-have-a-zero-data-retention-agreement-with-anthropic-what-products-does-it-apply-to) | -| Google | [Yes](https://cloud.google.com/terms/service-terms) | [Yes](https://cloud.google.com/terms/service-terms), see Service Terms sections 17 and 19h | -| OpenAI | [Yes](https://openai.com/enterprise-privacy/) | [Yes](https://platform.openai.com/docs/guides/your-data) | - -When you use your own API keys or external agents, **Zed does not have control over how your data is used by that service provider.** -You should reference your agreement with each service provider to understand what terms and conditions apply. - -### Data we collect - -For prompts you have explicitly shared with us, Zed may store copies of those prompts and other data about the specific use of the Agent Panel. - -This data includes: - -- The prompt given to the Agent -- Any commentary you include -- Product telemetry about the agentic thread -- Metadata about your Zed installation - -### Data Handling - -Collected data is stored in Snowflake, a private database where we track other metrics. We periodically review this data to improve our overall agentic approach and refine the product via our system prompt, tool use, etc. We ensure any included data is anonymized and contains no sensitive information (access tokens, user IDs, email addresses, etc). - -## Edit Predictions - -By default, when using Zed Edit Predictions, Zed does not persistently store user content or use user content for training of its models. - -### Opt-in - -Users who are working on open source licensed projects may optionally opt-in to providing model improvement feedback. This opt-in occurs on a per-project basis. If you work on multiple open source projects and wish to provide model improvement feedback you will have to opt-in for each individual project. - -When working on other projects where you haven't opted-in, Zed will not persistently store user content or use user content for training of its models. - -You can see exactly how Zed detects open source licenses in: [license_detection.rs](https://github.com/zed-industries/zed/blob/main/crates/zeta/src/license_detection.rs). - -### Exclusions - -Zed will intentionally exclude certain files from Predictive Edits entirely, even when you have opted-in to model improvement feedback. - -You can inspect this exclusion list by opening `zed: open default settings` from the command palette: - -```json [settings] -{ - "edit_predictions": { - // A list of globs representing files that edit predictions should be disabled for. - // There's a sensible default list of globs already included. - // Any addition to this list will be merged with the default list. - "disabled_globs": [ - "**/.env*", - "**/*.pem", - "**/*.key", - "**/*.cert", - "**/*.crt", - "**/secrets.yml" - ] - } -} -``` - -Users may explicitly exclude additional paths and/or file extensions by adding them to [`edit_predictions.disabled_globs`](https://zed.dev/docs/configuring-zed#edit-predictions) in their Zed settings.json: - -```json [settings] -{ - "edit_predictions": { - "disabled_globs": ["secret_dir/*", "**/*.log"] - } -} -``` - -### Data we collect - -For open source projects where you have opted-in, Zed may store copies of requests and responses to the Zed AI Prediction service. - -This data includes: - -- the edit prediction -- a portion of the buffer content around the cursor -- a few recent edits -- the current buffer outline -- diagnostics (errors, warnings, etc) from language servers - -### Data Handling - -Collected data is stored in Snowflake, a private database where we track other metrics. We periodically review this data to select training samples for inclusion in our model training dataset. We ensure any included data is anonymized and contains no sensitive information (access tokens, user IDs, email addresses, etc). This training dataset is publicly available at [huggingface.co/datasets/zed-industries/zeta](https://huggingface.co/datasets/zed-industries/zeta). - -### Model Output - -We then use this training dataset to fine-tune [Qwen2.5-Coder-7B](https://huggingface.co/Qwen/Qwen2.5-Coder-7B) and make the resulting model available at [huggingface.co/zed-industries/zeta](https://huggingface.co/zed-industries/zeta). - -## Applicable terms - -Please see the [Zed Terms of Service](https://zed.dev/terms-of-service) for more. diff --git a/docs/src/ai/billing.md b/docs/src/ai/billing.md deleted file mode 100644 index 64ff871ce1..0000000000 --- a/docs/src/ai/billing.md +++ /dev/null @@ -1,50 +0,0 @@ -# Billing - -We use Stripe as our payments provider, and Orb for invoicing and metering. All Pro plans require payment via credit card or other supported payment method. -For invoice-based billing, a Business plan is required. Contact [sales@zed.dev](mailto:sales@zed.dev) for more information. - -## Billing Information {#settings} - -You can access billing information and settings at [zed.dev/account](https://zed.dev/account). -Most of the page embeds information from our invoicing/metering partner, Orb (we're planning on a more native experience soon!). - -## Billing Cycles {#billing-cycles} - -Zed is billed on a monthly basis based on the date you initially subscribe. You'll receive _at least_ one invoice from Zed each month you're subscribed to Zed Pro, and more than one if you use more than $10 in incremental token spend within the month. - -## Threshold Billing {#threshold-billing} - -Zed utilizes threshold billing to ensure timely collection of owed monies and prevent abuse. Every time your usage of Zed's hosted models crosses a $10 spend threshold, a new invoice is generated, and the threshold resets to $0. - -For example, - -- You subscribe on February 1. Your first invoice is $10. -- You use $12 of incremental tokens in the month of February, with the first $10 spent on February 15. You'll receive an invoice for $10 on February 15 -- On March 1, you receive an invoice for $12: $10 (March Pro subscription) and $2 in leftover token spend, since your usage didn't cross the $10 threshold. - -## Payment Failures {#payment-failures} - -If payment of an invoice fails, Zed will block usage of our hosted models until the payment is complete. Email [billing-support@zed.dev](mailto:billing-support@zed.dev) for assistance. - -## Invoice History {#invoice-history} - -You can access your invoice history by navigating to [zed.dev/account](https://zed.dev/account) and clicking `Invoice history` within the embedded Orb portal. - -If you require historical Stripe invoices, email [billing-support@zed.dev](mailto:billing-support@zed.dev) - -## Updating Billing Information {#updating-billing-info} - -Email [billing-support@zed.dev](mailto:billing-support@zed.dev) for help updating payment methods, names, addresses, and tax information. - -> We'll be updating our account page shortly to allow for self-service updates. Stay tuned! - -Please note that changes to billing information will **only** affect future invoices — **we cannot modify historical invoices**. - -## Sales Tax {#sales-tax} - -Zed partners with [Sphere](https://www.getsphere.com/) to calculate indirect tax rate for invoices, based on customer location and the product being sold. Tax is listed as a separate line item on invoices, based preferentially on your billing address, followed by the card issue country known to Stripe. - -If you have a VAT/GST ID, you can add it at during checkout. Check the box that denotes you as a business. - -Please note that changes to VAT/GST IDs and address will **only** affect future invoices — **we cannot modify historical invoices**. -Questions or issues can be directed to [billing-support@zed.dev](mailto:billing-support@zed.dev). diff --git a/docs/src/ai/configuration.md b/docs/src/ai/configuration.md deleted file mode 100644 index 8877689e46..0000000000 --- a/docs/src/ai/configuration.md +++ /dev/null @@ -1,23 +0,0 @@ -# Configuration - -When using AI in Zed, you can configure multiple dimensions: - -1. Which LLM providers you can use - - Zed's hosted models, which require [authentication](../authentication.md) and [subscription](./subscription.md) - - [Using your own API keys](./llm-providers.md), which do not - - Using [external agents like Claude Code](./external-agents.md), which do not -2. [Model parameters and usage](./agent-settings.md#model-settings) -3. [Interactions with the Agent Panel](./agent-settings.md#agent-panel-settings) - -## Turning AI Off Entirely - -We want to respect users who want to use Zed without interacting with AI whatsoever. -To do that, add the following key to your `settings.json`: - -```json [settings] -{ - "disable_ai": true -} -``` - -Read [the following blog post](https://zed.dev/blog/disable-ai-features) to learn more about our motivation to promote this, as much as we also encourage users to explore AI-assisted programming. diff --git a/docs/src/ai/edit-prediction.md b/docs/src/ai/edit-prediction.md deleted file mode 100644 index feef6d36d2..0000000000 --- a/docs/src/ai/edit-prediction.md +++ /dev/null @@ -1,365 +0,0 @@ -# Edit Prediction - -Edit Prediction is Zed's LLM mechanism for predicting the code you want to write. -Each keystroke sends a new request to the edit prediction provider, which returns individual or multi-line suggestions that can be quickly accepted by pressing `tab`. - -The default provider is [Zeta, a proprietary open source and open dataset model](https://huggingface.co/zed-industries/zeta), but you can also use [other providers](#other-providers) like GitHub Copilot, Supermaven, and Codestral. - -## Configuring Zeta - -To use Zeta, the only thing you need to do is [to sign in](../authentication.md#what-features-require-signing-in). -After doing that, you should already see predictions as you type on your files. - -You can confirm that Zeta is properly configured either by verifying whether you have the following code in your `settings.json`: - -```json [settings] -"features": { - "edit_prediction_provider": "zed" -}, -``` - -Or you can also look for a little Z icon in the right of your status bar at the bottom. - -### Pricing and Plans - -From just signing in, while in Zed's free plan, you get 2,000 Zeta-powered edit predictions per month. -But you can get _**unlimited edit predictions**_ by upgrading to [the Pro plan](../ai/plans-and-usage.md). -More information can be found in [Zed's pricing page](https://zed.dev/pricing). - -### Switching Modes {#switching-modes} - -Zed's Edit Prediction comes with two different display modes: - -1. `eager` (default): predictions are displayed inline as long as it doesn't conflict with language server completions -2. `subtle`: predictions only appear inline when holding a modifier key (`alt` by default) - -Toggle between them via the `mode` key: - -```json [settings] -"edit_predictions": { - "mode": "eager" // or "subtle" -}, -``` - -Or directly via the UI through the status bar menu: - -![Edit Prediction status bar menu, with the modes toggle.](https://zed.dev/img/edit-prediction/status-bar-menu.webp) - -> Note that edit prediction modes work with any prediction provider. - -### Conflict With Other `tab` Actions {#edit-predictions-conflict} - -By default, when `tab` would normally perform a different action, Zed requires a modifier key to accept predictions: - -1. When the language server completions menu is visible. -2. When your cursor isn't at the right indentation level. - -In these cases, `alt-tab` is used instead to accept the prediction. When the language server completions menu is open, holding `alt` first will cause it to temporarily disappear in order to preview the prediction within the buffer. - -On Linux, `alt-tab` is often used by the window manager for switching windows, so `alt-l` is provided as the default binding for accepting predictions. `tab` and `alt-tab` also work, but aren't displayed by default. - -{#action editor::AcceptPartialEditPrediction} ({#kb editor::AcceptPartialEditPrediction}) can be used to accept the current edit prediction up to the next word boundary. - -## Configuring Edit Prediction Keybindings {#edit-predictions-keybinding} - -By default, `tab` is used to accept edit predictions. You can use another keybinding by inserting this in your keymap: - -```json [settings] -{ - "context": "Editor && edit_prediction", - "bindings": { - // Here we also allow `alt-enter` to accept the prediction - "alt-enter": "editor::AcceptEditPrediction" - } -} -``` - -When there's a [conflict with the `tab` key](#edit-predictions-conflict), Zed uses a different key context to accept keybindings (`edit_prediction_conflict`). -If you want to use a different one, you can insert this in your keymap: - -```json [settings] -{ - "context": "Editor && edit_prediction_conflict", - "bindings": { - "ctrl-enter": "editor::AcceptEditPrediction" // Example of a modified keybinding - } -} -``` - -If your keybinding contains a modifier (`ctrl` in the example above), it will also be used to preview the edit prediction and temporarily hide the language server completion menu. - -You can also bind this action to keybind without a modifier. -In that case, Zed will use the default modifier (`alt`) to preview the edit prediction. - -```json [settings] -{ - "context": "Editor && edit_prediction_conflict", - "bindings": { - // Here we bind tab to accept even when there's a language server completion - // or the cursor isn't at the correct indentation level - "tab": "editor::AcceptEditPrediction" - } -} -``` - -To maintain the use of the modifier key for accepting predictions when there is a language server completions menu, but allow `tab` to accept predictions regardless of cursor position, you can specify the context further with `showing_completions`: - -```json [settings] -{ - "context": "Editor && edit_prediction_conflict && !showing_completions", - "bindings": { - // Here we don't require a modifier unless there's a language server completion - "tab": "editor::AcceptEditPrediction" - } -} -``` - -### Keybinding Example: Always Use Tab - -If you want to use `tab` to always accept edit predictions, you can use the following keybinding: - -```json [keymap] -{ - "context": "Editor && edit_prediction_conflict && showing_completions", - "bindings": { - "tab": "editor::AcceptEditPrediction" - } -} -``` - -This will make `tab` work to accept edit predictions _even when_ you're also seeing language server completions. -That means that you need to rely on `enter` for accepting the latter. - -### Keybinding Example: Always Use Alt-Tab - -The keybinding example below causes `alt-tab` to always be used instead of sometimes using `tab`. -You might want this in order to have just one (alternative) keybinding to use for accepting edit predictions, since the behavior of `tab` varies based on context. - -```json [keymap] - { - "context": "Editor && edit_prediction", - "bindings": { - "alt-tab": "editor::AcceptEditPrediction" - } - }, - // Bind `tab` back to its original behavior. - { - "context": "Editor", - "bindings": { - "tab": "editor::Tab" - } - }, - { - "context": "Editor && showing_completions", - "bindings": { - "tab": "editor::ComposeCompletion" - } - }, -``` - -If you are using [Vim mode](../vim.md), then additional bindings are needed after the above to return `tab` to its original behavior: - -```json [keymap] - { - "context": "(VimControl && !menu) || vim_mode == replace || vim_mode == waiting", - "bindings": { - "tab": "vim::Tab" - } - }, - { - "context": "vim_mode == literal", - "bindings": { - "tab": ["vim::Literal", ["tab", "\u0009"]] - } - }, -``` - -### Keybinding Example: Displaying Tab and Alt-Tab on Linux - -While `tab` and `alt-tab` are supported on Linux, `alt-l` is displayed instead. -If your window manager does not reserve `alt-tab`, and you would prefer to use `tab` and `alt-tab`, include these bindings in `keymap.json`: - -```json [keymap] - { - "context": "Editor && edit_prediction", - "bindings": { - "tab": "editor::AcceptEditPrediction", - // Optional: This makes the default `alt-l` binding do nothing. - "alt-l": null - } - }, - { - "context": "Editor && edit_prediction_conflict", - "bindings": { - "alt-tab": "editor::AcceptEditPrediction", - // Optional: This makes the default `alt-l` binding do nothing. - "alt-l": null - } - }, -``` - -### Missing keybind {#edit-predictions-missing-keybinding} - -Zed requires at least one keybinding for the {#action editor::AcceptEditPrediction} action in both the `Editor && edit_prediction` and `Editor && edit_prediction_conflict` contexts ([learn more above](#edit-predictions-keybinding)). - -If you have previously bound the default keybindings to different actions in the global context, you will not be able to preview or accept edit predictions. For example: - -```json [keymap] -[ - // Your keymap - { - "bindings": { - // Binds `alt-tab` to a different action globally - "alt-tab": "menu::SelectNext" - } - } -] -``` - -To fix this, you can specify your own keybinding for accepting edit predictions: - -```json [keymap] -[ - // ... - { - "context": "Editor && edit_prediction_conflict", - "bindings": { - "alt-l": "editor::AcceptEditPrediction" - } - } -] -``` - -If you would like to use the default keybinding, you can free it up by either moving yours to a more specific context or changing it to something else. - -## Disabling Automatic Edit Prediction - -There are different levels in which you can disable edit predictions to be displayed, including not having it turned on at all. - -Alternatively, if you have Zed set as your provider, consider [using Subtle Mode](#switching-modes). - -### On Buffers - -To not have predictions appear automatically as you type, set this within `settings.json`: - -```json [settings] -{ - "show_edit_predictions": false -} -``` - -This hides every indication that there is a prediction available, regardless of [the display mode](#switching-modes) you're in (valid only if you have Zed as your provider). -Still, you can trigger edit predictions manually by executing {#action editor::ShowEditPrediction} or hitting {#kb editor::ShowEditPrediction}. - -### For Specific Languages - -To not have predictions appear automatically as you type when working with a specific language, set this within `settings.json`: - -```json [settings] -{ - "language": { - "python": { - "show_edit_predictions": false - } - } -} -``` - -### In Specific Directories - -To disable edit predictions for specific directories or files, set this within `settings.json`: - -```json [settings] -{ - "edit_predictions": { - "disabled_globs": ["~/.config/zed/settings.json"] - } -} -``` - -### Turning Off Completely - -To completely turn off edit prediction across all providers, explicitly set the settings to `none`, like so: - -```json [settings] -"features": { - "edit_prediction_provider": "none" -}, -``` - -## Configuring Other Providers {#other-providers} - -Zed's Edit Prediction also work with other completion model providers aside from Zeta. -Learn about the available ones below. - -### GitHub Copilot {#github-copilot} - -To use GitHub Copilot as your provider, set this within `settings.json`: - -```json [settings] -{ - "features": { - "edit_prediction_provider": "copilot" - } -} -``` - -You should be able to sign-in to GitHub Copilot by clicking on the Copilot icon in the status bar and following the setup instructions. - -#### Using GitHub Copilot Enterprise - -If your organization uses GitHub Copilot Enterprise, you can configure Zed to use your enterprise instance by specifying the enterprise URI in your `settings.json`: - -```json [settings] -{ - "edit_predictions": { - "copilot": { - "enterprise_uri": "https://your.enterprise.domain" - } - } -} -``` - -Replace `"https://your.enterprise.domain"` with the URL provided by your GitHub Enterprise administrator (e.g., `https://foo.ghe.com`). - -Once set, Zed will route Copilot requests through your enterprise endpoint. -When you sign in by clicking the Copilot icon in the status bar, you will be redirected to your configured enterprise URL to complete authentication. -All other Copilot features and usage remain the same. - -Copilot can provide multiple completion alternatives, and these can be navigated with the following actions: - -- {#action editor::NextEditPrediction} ({#kb editor::NextEditPrediction}): To cycle to the next edit prediction -- {#action editor::PreviousEditPrediction} ({#kb editor::PreviousEditPrediction}): To cycle to the previous edit prediction - -### Supermaven {#supermaven} - -To use Supermaven as your provider, set this within `settings.json`: - -```json [settings] -{ - "features": { - "edit_prediction_provider": "supermaven" - } -} -``` - -You should be able to sign-in to Supermaven by clicking on the Supermaven icon in the status bar and following the setup instructions. - -### Codestral {#codestral} - -To use Mistral's Codestral as your provider, start by going to the Agent Panel settings view by running the {#action agent::OpenSettings} action. -Look for the Mistral item and add a Codestral API key in the corresponding text input. - -After that, you should be able to switch your provider to it in your `settings.json` file: - -```json [settings] -{ - "features": { - "edit_prediction_provider": "codestral" - } -} -``` - -## See also - -To learn about other ways to interact with AI in Zed, you may also want to see more about the [Agent Panel](./agent-panel.md) or the [Inline Assistant](./inline-assistant.md) feature. diff --git a/docs/src/ai/external-agents.md b/docs/src/ai/external-agents.md deleted file mode 100644 index 0467913b07..0000000000 --- a/docs/src/ai/external-agents.md +++ /dev/null @@ -1,248 +0,0 @@ -# External Agents - -Zed supports terminal-based agents through the [Agent Client Protocol (ACP)](https://agentclientprotocol.com). - -Currently, [Gemini CLI](https://github.com/google-gemini/gemini-cli) serves as the reference implementation. -[Claude Code](https://www.anthropic.com/claude-code) and [Codex](https://developers.openai.com/codex) are also included by default, and you can [add custom ACP-compatible agents](#add-more-agents) as well. - -> Note that Zed's affordance for external agents is strictly UI-based; the billing and legal/terms arrangement is directly between you and the agent provider. -> Zed does not charge for use of external agents, and our [zero-data retention agreements/privacy guarantees](./ai-improvement.md) are **_only_** applicable for Zed's hosted models. - -## Gemini CLI {#gemini-cli} - -Zed provides the ability to run [Gemini CLI](https://github.com/google-gemini/gemini-cli) directly in the [agent panel](./agent-panel.md). - -Under the hood we run Gemini CLI in the background, and talk to it over ACP. -This means that you're running the real Gemini CLI, with all of the advantages of that, but you can see and interact with files in your editor. - -### Getting Started - -As of [Zed Stable v0.201.5](https://zed.dev/releases/stable/0.201.5) you should be able to use Gemini CLI directly from Zed. First open the agent panel with {#kb agent::ToggleFocus}, and then use the `+` button in the top right to start a new Gemini CLI thread. - -If you'd like to bind this to a keyboard shortcut, you can do so by editing your `keymap.json` file via the `zed: open keymap` command to include: - -```json [keymap] -[ - { - "bindings": { - "cmd-alt-g": ["agent::NewExternalAgentThread", { "agent": "gemini" }] - } - } -] -``` - -#### Installation - -The first time you create a Gemini CLI thread, Zed will install [@google/gemini-cli](https://github.com/google-gemini/gemini-cli). This installation is only available to Zed and is kept up to date as you use the agent. - -By default, Zed will use this managed version of Gemini CLI even if you have it installed globally. However, you can configure it to use a version in your `PATH` by adding this to your settings: - -```json [settings] -{ - "agent_servers": { - "gemini": { - "ignore_system_version": false - } - } -} -``` - -#### Authentication - -After you have Gemini CLI running, you'll be prompted to choose your authentication method. - -Most users should click the "Log in with Google". This will cause a browser window to pop-up and auth directly with Gemini CLI. Zed does not see your OAuth or access tokens in this case. - -You can also use the "Gemini API Key". If you select this, and have the `GEMINI_API_KEY` set, then we will use that. Otherwise Zed will prompt you for an API key which will be stored securely in your keychain, and used to start Gemini CLI from within Zed. - -The "Vertex AI" option is for those who are using [Vertex AI](https://cloud.google.com/vertex-ai), and have already configured their environment correctly. - -For more information, see the [Gemini CLI docs](https://github.com/google-gemini/gemini-cli/blob/main/docs/index.md). - -### Usage - -Similar to Zed's first-party agent, you can use Gemini CLI to do anything that you need. -And to give it context, you can @-mention files, recent threads, symbols, or fetch the web. - -> Note that some first-party agent features don't yet work with Gemini CLI: editing past messages, resuming threads from history, and checkpointing. -> We hope to add these features in the near future. - -## Claude Code - -Similar to Gemini CLI, you can also run [Claude Code](https://www.anthropic.com/claude-code) directly via Zed's [agent panel](./agent-panel.md). -Under the hood, Zed runs Claude Code and communicate to it over ACP, through [a dedicated adapter](https://github.com/zed-industries/claude-code-acp). - -### Getting Started - -Open the agent panel with {#kb agent::ToggleFocus}, and then use the `+` button in the top right to start a new Claude Code thread. - -If you'd like to bind this to a keyboard shortcut, you can do so by editing your `keymap.json` file via the `zed: open keymap` command to include: - -```json [keymap] -[ - { - "bindings": { - "cmd-alt-c": ["agent::NewExternalAgentThread", { "agent": "claude_code" }] - } - } -] -``` - -### Authentication - -As of version `0.202.7` (stable) and `0.203.2` (preview), authentication to Zed's Claude Code installation is decoupled entirely from Zed's agent. That is to say, an Anthropic API key added via the [Zed Agent's settings](./llm-providers.md#anthropic) will _not_ be utilized by Claude Code for authentication and billing. - -To ensure you're using your billing method of choice, [open a new Claude Code thread](./agent-panel.md#new-thread). Then, run `/login`, and authenticate either via API key, or via `Log in with Claude Code` to use a Claude Pro/Max subscription. - -#### Installation - -The first time you create a Claude Code thread, Zed will install [@zed-industries/claude-code-acp](https://github.com/zed-industries/claude-code-acp). This installation is only available to Zed and is kept up to date as you use the agent. - -Zed will always use this managed version of the Claude Code adapter, which includes a vendored version of the Claude Code CLI, even if you have it installed globally. - -If you want to override the executable used by the adapter, you can set the `CLAUDE_CODE_EXECUTABLE` environment variable in your settings to the path of your preferred executable. - -```json -{ - "agent_servers": { - "claude": { - "env": { - "CLAUDE_CODE_EXECUTABLE": "/path/to/alternate-claude-code-executable" - } - } - } -} -``` - -### Usage - -Similar to Zed's first-party agent, you can use Claude Code to do anything that you need. -And to give it context, you can @-mention files, recent threads, symbols, or fetch the web. - -In complement to talking to it [over ACP](https://agentclientprotocol.com), Zed relies on the [Claude Code SDK](https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-overview) to support some of its specific features. -However, the SDK doesn't yet expose everything needed to fully support all of them: - -- Slash Commands: A subset of [built-in commands](https://docs.anthropic.com/en/docs/claude-code/slash-commands#built-in-slash-commands) are supported, while [custom slash commands](https://docs.anthropic.com/en/docs/claude-code/slash-commands#custom-slash-commands) are fully supported. -- [Subagents](https://docs.anthropic.com/en/docs/claude-code/sub-agents) are supported. -- [Hooks](https://docs.anthropic.com/en/docs/claude-code/hooks-guide) are currently _not_ supported. - -> Also note that some [first-party agent](./agent-panel.md) features don't yet work with Claude Code: editing past messages, resuming threads from history, and checkpointing. -> We hope to add these features in the near future. - -#### CLAUDE.md - -Claude Code in Zed will automatically use any `CLAUDE.md` file found in your project root, project subdirectories, or root `.claude` directory. - -If you don't have a `CLAUDE.md` file, you can ask Claude Code to create one for you through the `init` slash command. - -## Codex CLI - -You can also run [Codex CLI](https://github.com/openai/codex) directly via Zed's [agent panel](./agent-panel.md). -Under the hood, Zed runs Codex CLI and communicates to it over ACP, through [a dedicated adapter](https://github.com/zed-industries/codex-acp). - -### Getting Started - -As of Zed Stable v0.208 you should be able to use Codex directly from Zed. Open the agent panel with {#kb agent::ToggleFocus}, and then use the `+` button in the top right to start a new Codex thread. - -If you'd like to bind this to a keyboard shortcut, you can do so by editing your `keymap.json` file via the `zed: open keymap` command to include: - -```json -[ - { - "bindings": { - "cmd-alt-c": ["agent::NewExternalAgentThread", { "agent": "codex" }] - } - } -] -``` - -### Authentication - -Authentication to Zed's Codex installation is decoupled entirely from Zed's agent. That is to say, an OpenAI API key added via the [Zed Agent's settings](./llm-providers.md#openai) will _not_ be utilized by Codex for authentication and billing. - -To ensure you're using your billing method of choice, [open a new Codex thread](./agent-panel.md#new-thread). The first time you will be prompted to authenticate with one of three methods: - -1. Login with ChatGPT - allows you to use your existing, paid ChatGPT subscription. _Note: This method isn't currently supported in remote projects_ -2. `CODEX_API_KEY` - uses an API key you have set in your environment under the variable `CODEX_API_KEY`. -3. `OPENAI_API_KEY` - uses an API key you have set in your environment under the variable `OPENAI_API_KEY`. - -If you are already logged in and want to change your authentication method, type `/logout` in the thread and authenticate again. - -If you want to use a third-party provider with Codex, you can configure that with your [Codex config.toml](https://github.com/openai/codex/blob/main/docs/config.md#model-selection) or pass extra [args/env variables](https://github.com/openai/codex/blob/main/docs/config.md#model-selection) to your Codex agent servers settings. - -#### Installation - -The first time you create a Codex thread, Zed will install [codex-acp](https://github.com/zed-industries/codex-acp). This installation is only available to Zed and is kept up to date as you use the agent. - -Zed will always use this managed version of Codex even if you have it installed globally. - -### Usage - -Similar to Zed's first-party agent, you can use Codex to do anything that you need. -And to give it context, you can @-mention files, symbols, or fetch the web. - -> Note that some first-party agent features don't yet work with Codex: editing past messages, resuming threads from history, and checkpointing. -> We hope to add these features in the near future. - -## Add More Agents {#add-more-agents} - -Add more external agents to Zed by installing [Agent Server extensions](../extensions/agent-servers.md). - -See what agents are available by filtering for "Agent Servers" in the extensions page, which you can access via the command palette with `zed: extensions`, or the [Zed website](https://zed.dev/extensions?filter=agent-servers). - -You can also add agents through your `settings.json`, by specifying certain fields under `agent_servers`, like so: - -```json [settings] -{ - "agent_servers": { - "My Custom Agent": { - "type": "custom", - "command": "node", - "args": ["~/projects/agent/index.js", "--acp"], - "env": {} - } - } -} -``` - -This can be useful if you're in the middle of developing a new agent that speaks the protocol and you want to debug it. - -It's also possible to specify a custom path, arguments, or environment for the builtin integrations by using the `claude` and `gemini` names. - -### Custom Keybinding For Extension-Based Agents - -To assign a custom keybinding to start a new thread for agents that were added by installing agent server extensions, add the following snippet to your `keymap.json` file: - -```json [keymap] -{ - "bindings": { - "cmd-alt-n": [ // Your custom keybinding - "agent::NewExternalAgentThread", - { - "agent": { - "custom": { - "name": "My Agent", // The agent name as it appears in the UI (e.g., "OpenCode", "Auggie CLI", etc.) - "command": { - "command": "my-agent", // The agent name in lowercase with no spaces - "args": ["acp"] - } - } - } - } - ] - } -}, -``` - -## Debugging Agents - -When using external agents in Zed, you can access the debug view via with `dev: open acp logs` from the Command Palette. This lets you see the messages being sent and received between Zed and the agent. - -![The debug view for ACP logs.](https://zed.dev/img/acp/acp-logs.webp) - -## MCP Servers - -Note that for external agents, access to MCP servers [installed from Zed](./mcp.md) may vary depending on the ACP agent implementation. - -Regarding the built-in ones, Claude Code and Codex both support it, and Gemini CLI does not yet. -In the meantime, learn how to add MCP server support to Gemini CLI through [their documentation](https://github.com/google-gemini/gemini-cli?tab=readme-ov-file#using-mcp-servers). diff --git a/docs/src/ai/inline-assistant.md b/docs/src/ai/inline-assistant.md deleted file mode 100644 index af232a837e..0000000000 --- a/docs/src/ai/inline-assistant.md +++ /dev/null @@ -1,120 +0,0 @@ -# Inline Assistant - -## Usage Overview - -Use {#kb assistant::InlineAssist} to open the Inline Assistant nearly anywhere you can enter text: editors, text threads, the rules library, channel notes, and even within the terminal panel. - -The Inline Assistant allows you to send the current selection (or the current line) to a language model and modify the selection with the language model's response. - -## Getting Started - -If you're using the Inline Assistant for the first time, you need to have at least one LLM provider or external agent configured. -You can do that by: - -1. [subscribing to our Pro plan](https://zed.dev/pricing), so you have access to our hosted models -2. [using your own API keys](./llm-providers.md#use-your-own-keys), either from model providers like Anthropic or model gateways like OpenRouter. - -If you have already set up an LLM provider to interact with [the Agent Panel](./agent-panel.md#getting-started), then that will also work for the Inline Assistant. - -> Unlike the Agent Panel, though, the only exception at the moment is [external agents](./external-agents.md). -> They currently can't be used for generating changes with the Inline Assistant. - -## Adding Context - -You can add context in the Inline Assistant the same way you can in [the Agent Panel](./agent-panel.md#adding-context): - -- @-mention files, directories, past threads, rules, and symbols -- paste images that are copied on your clipboard - -Additionally, a useful pattern is to create a thread in the Agent Panel, and then mention it with `@thread` in the Inline Assistant to include it as context. -That often serves as a way to more quickly iterate over a specific part of a change that happened in the context of a larger thread. - -## Parallel Generations - -There are two ways in which you can generate multiple changes at once with the Inline Assistant: - -### Multiple Cursors - -If you have a multiple cursor selection and hit {#kb assistant::InlineAssist}, you can shoot the same prompt for all cursor positions and get a change in all of them. - -This is particularly useful when working on excerpts in [a multibuffer context](../multibuffers.md). - -### Multiple Models - -You can use the Inline Assistant to send the same prompt to multiple models at once. - -Here's how you can customize your `settings.json` to add this functionality: - -```json [settings] -{ - "agent": { - "default_model": { - "provider": "zed.dev", - "model": "claude-sonnet-4" - }, - "inline_alternatives": [ - { - "provider": "zed.dev", - "model": "gpt-4-mini" - } - ] - } -} -``` - -When multiple models are configured, you'll see in the Inline Assistant UI buttons that allow you to cycle between outputs generated by each model. - -The models you specify here are always used in _addition_ to your [default model](#default-model). - -For example, the following configuration will generate three outputs for every assist. -One with Claude Sonnet 4 (the default model), another with GPT-5-mini, and another one with Gemini 2.5 Flash. - -```json [settings] -{ - "agent": { - "default_model": { - "provider": "zed.dev", - "model": "claude-sonnet-4" - }, - "inline_alternatives": [ - { - "provider": "zed.dev", - "model": "gpt-4-mini" - }, - { - "provider": "zed.dev", - "model": "gemini-2.5-flash" - } - ] - } -} -``` - -## Inline Assistant vs. Edit Prediction - -Users often ask what's the difference between these two AI-powered features in Zed, particularly because both of them involve getting inline LLM code completions. - -Here's how they are different: - -- The Inline Assistant is more similar to the Agent Panel as in you're still writing a prompt yourself and crafting context. It works from within the buffer and is mostly centered around your selections. -- [Edit Predictions](./edit-prediction.md) is an AI-powered completion mechanism that intelligently suggests what you likely want to add next, based on context automatically gathered from your previous edits, recently visited files, and more. - -In summary, the key difference is that in the Inline Assistant, you're still manually prompting, whereas Edit Prediction will _automatically suggest_ edits to you. - -## Prefilling Prompts - -To create a custom keybinding that prefills a prompt, you can add the following format in your keymap: - -```json [keymap] -[ - { - "context": "Editor && mode == full", - "bindings": { - "ctrl-shift-enter": [ - "assistant::InlineAssist", - { "prompt": "Build a snake game" } - ] - } - } -] -``` diff --git a/docs/src/ai/llm-providers.md b/docs/src/ai/llm-providers.md deleted file mode 100644 index f13ece5d3e..0000000000 --- a/docs/src/ai/llm-providers.md +++ /dev/null @@ -1,678 +0,0 @@ -# LLM Providers - -To use AI in Zed, you need to have at least one large language model provider set up. - -You can do that by either subscribing to [one of Zed's plans](./plans-and-usage.md), or by using API keys you already have for the supported providers. - -## Use Your Own Keys {#use-your-own-keys} - -If you already have an API key for an existing LLM provider, like Anthropic or OpenAI, you can add them to Zed and use the full power of the Agent Panel **_for free_**. - -To add an existing API key to a given provider, go to the Agent Panel settings (`agent: open settings`), look for the desired provider, paste the key into the input, and hit enter. - -> Note: API keys are _not_ stored as plain text in your `settings.json`, but rather in your OS's secure credential storage. - -## Supported Providers - -Zed offers an extensive list of "use your own key" LLM providers - -- [Amazon Bedrock](#amazon-bedrock) -- [Anthropic](#anthropic) -- [DeepSeek](#deepseek) -- [GitHub Copilot Chat](#github-copilot-chat) -- [Google AI](#google-ai) -- [LM Studio](#lmstudio) -- [Mistral](#mistral) -- [Ollama](#ollama) -- [OpenAI](#openai) -- [OpenAI API Compatible](#openai-api-compatible) -- [OpenRouter](#openrouter) -- [Vercel](#vercel-v0) -- [xAI](#xai) - -### Amazon Bedrock {#amazon-bedrock} - -> Supports tool use with models that support streaming tool use. -> More details can be found in the [Amazon Bedrock's Tool Use documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-supported-models-features.html). - -To use Amazon Bedrock's models, an AWS authentication is required. -Ensure your credentials have the following permissions set up: - -- `bedrock:InvokeModelWithResponseStream` -- `bedrock:InvokeModel` - -Your IAM policy should look similar to: - -```json [settings] -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "bedrock:InvokeModel", - "bedrock:InvokeModelWithResponseStream" - ], - "Resource": "*" - } - ] -} -``` - -With that done, choose one of the two authentication methods: - -#### Authentication via Named Profile (Recommended) - -1. Ensure you have the AWS CLI installed and configured with a named profile -2. Open your `settings.json` (`zed: open settings file`) and include the `bedrock` key under `language_models` with the following settings: - ```json [settings] - { - "language_models": { - "bedrock": { - "authentication_method": "named_profile", - "region": "your-aws-region", - "profile": "your-profile-name" - } - } - } - ``` - -#### Authentication via Static Credentials - -While it's possible to configure through the Agent Panel settings UI by entering your AWS access key and secret directly, we recommend using named profiles instead for better security practices. -To do this: - -1. Create an IAM User that you can assume in the [IAM Console](https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/users). -2. Create security credentials for that User, save them and keep them secure. -3. Open the Agent Configuration with (`agent: open settings`) and go to the Amazon Bedrock section -4. Copy the credentials from Step 2 into the respective **Access Key ID**, **Secret Access Key**, and **Region** fields. - -#### Cross-Region Inference - -The Zed implementation of Amazon Bedrock uses [Cross-Region inference](https://docs.aws.amazon.com/bedrock/latest/userguide/cross-region-inference.html) to improve availability and throughput. -With Cross-Region inference, you can distribute traffic across multiple AWS Regions, enabling higher throughput. - -##### Regional vs Global Inference Profiles - -Bedrock supports two types of cross-region inference profiles: - -- **Regional profiles** (default): Route requests within a specific geography (US, EU, APAC). For example, `us-east-1` uses the `us.*` profile which routes across `us-east-1`, `us-east-2`, and `us-west-2`. -- **Global profiles**: Route requests across all commercial AWS Regions for maximum availability and performance. - -By default, Zed uses **regional profiles** which keep your data within the same geography. You can opt into global profiles by adding `"allow_global": true` to your Bedrock configuration: - -```json [settings] -{ - "language_models": { - "bedrock": { - "authentication_method": "named_profile", - "region": "your-aws-region", - "profile": "your-profile-name", - "allow_global": true - } - } -} -``` - -**Note:** Only select newer models support global inference profiles. See the [AWS Bedrock supported models documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html#inference-profiles-support-system) for the current list of models that support global inference. If you encounter availability issues with a model in your region, enabling `allow_global` may resolve them. - -Although the data remains stored only in the source Region, your input prompts and output results might move outside of your source Region during cross-Region inference. -All data will be transmitted encrypted across Amazon's secure network. - -We will support Cross-Region inference for each of the models on a best-effort basis, please refer to the [Cross-Region Inference method Code](https://github.com/zed-industries/zed/blob/main/crates/bedrock/src/models.rs#L297). - -For the most up-to-date supported regions and models, refer to the [Supported Models and Regions for Cross Region inference](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html). - -### Anthropic {#anthropic} - -You can use Anthropic models by choosing them via the model dropdown in the Agent Panel. - -1. Sign up for Anthropic and [create an API key](https://console.anthropic.com/settings/keys) -2. Make sure that your Anthropic account has credits -3. Open the settings view (`agent: open settings`) and go to the Anthropic section -4. Enter your Anthropic API key - -Even if you pay for Claude Pro, you will still have to [pay for additional credits](https://console.anthropic.com/settings/plans) to use it via the API. - -Zed will also use the `ANTHROPIC_API_KEY` environment variable if it's defined. - -#### Custom Models {#anthropic-custom-models} - -You can add custom models to the Anthropic provider by adding the following to your Zed `settings.json`: - -```json [settings] -{ - "language_models": { - "anthropic": { - "available_models": [ - { - "name": "claude-3-5-sonnet-20240620", - "display_name": "Sonnet 2024-June", - "max_tokens": 128000, - "max_output_tokens": 2560, - "cache_configuration": { - "max_cache_anchors": 10, - "min_total_token": 10000, - "should_speculate": false - }, - "tool_override": "some-model-that-supports-toolcalling" - } - ] - } - } -} -``` - -Custom models will be listed in the model dropdown in the Agent Panel. - -You can configure a model to use [extended thinking](https://docs.anthropic.com/en/docs/about-claude/models/extended-thinking-models) (if it supports it) by changing the mode in your model's configuration to `thinking`, for example: - -```json [settings] -{ - "name": "claude-sonnet-4-latest", - "display_name": "claude-sonnet-4-thinking", - "max_tokens": 200000, - "mode": { - "type": "thinking", - "budget_tokens": 4096 - } -} -``` - -### DeepSeek {#deepseek} - -1. Visit the DeepSeek platform and [create an API key](https://platform.deepseek.com/api_keys) -2. Open the settings view (`agent: open settings`) and go to the DeepSeek section -3. Enter your DeepSeek API key - -The DeepSeek API key will be saved in your keychain. - -Zed will also use the `DEEPSEEK_API_KEY` environment variable if it's defined. - -#### Custom Models {#deepseek-custom-models} - -The Zed agent comes pre-configured to use the latest version for common models (DeepSeek Chat, DeepSeek Reasoner). -If you wish to use alternate models or customize the API endpoint, you can do so by adding the following to your Zed `settings.json`: - -```json [settings] -{ - "language_models": { - "deepseek": { - "api_url": "https://api.deepseek.com", - "available_models": [ - { - "name": "deepseek-chat", - "display_name": "DeepSeek Chat", - "max_tokens": 64000 - }, - { - "name": "deepseek-reasoner", - "display_name": "DeepSeek Reasoner", - "max_tokens": 64000, - "max_output_tokens": 4096 - } - ] - } - } -} -``` - -Custom models will be listed in the model dropdown in the Agent Panel. -You can also modify the `api_url` to use a custom endpoint if needed. - -### GitHub Copilot Chat {#github-copilot-chat} - -You can use GitHub Copilot Chat with the Zed agent by choosing it via the model dropdown in the Agent Panel. - -1. Open the settings view (`agent: open settings`) and go to the GitHub Copilot Chat section -2. Click on `Sign in to use GitHub Copilot`, follow the steps shown in the modal. - -Alternatively, you can provide an OAuth token via the `GH_COPILOT_TOKEN` environment variable. - -> **Note**: If you don't see specific models in the dropdown, you may need to enable them in your [GitHub Copilot settings](https://github.com/settings/copilot/features). - -To use Copilot Enterprise with Zed (for both agent and completions), you must configure your enterprise endpoint as described in [Configuring GitHub Copilot Enterprise](./edit-prediction.md#github-copilot-enterprise). - -### Google AI {#google-ai} - -You can use Gemini models with the Zed agent by choosing it via the model dropdown in the Agent Panel. - -1. Go to the Google AI Studio site and [create an API key](https://aistudio.google.com/app/apikey). -2. Open the settings view (`agent: open settings`) and go to the Google AI section -3. Enter your Google AI API key and press enter. - -The Google AI API key will be saved in your keychain. - -Zed will also use the `GEMINI_API_KEY` environment variable if it's defined. See [Using Gemini API keys](https://ai.google.dev/gemini-api/docs/api-key) in the Gemini docs for more. - -#### Custom Models {#google-ai-custom-models} - -By default, Zed will use `stable` versions of models, but you can use specific versions of models, including [experimental models](https://ai.google.dev/gemini-api/docs/models/experimental-models). You can configure a model to use [thinking mode](https://ai.google.dev/gemini-api/docs/thinking) (if it supports it) by adding a `mode` configuration to your model. This is useful for controlling reasoning token usage and response speed. If not specified, Gemini will automatically choose the thinking budget. - -Here is an example of a custom Google AI model you could add to your Zed `settings.json`: - -```json [settings] -{ - "language_models": { - "google": { - "available_models": [ - { - "name": "gemini-2.5-flash-preview-05-20", - "display_name": "Gemini 2.5 Flash (Thinking)", - "max_tokens": 1000000, - "mode": { - "type": "thinking", - "budget_tokens": 24000 - } - } - ] - } - } -} -``` - -Custom models will be listed in the model dropdown in the Agent Panel. - -### LM Studio {#lmstudio} - -1. Download and install [the latest version of LM Studio](https://lmstudio.ai/download) -2. In the app press `cmd/ctrl-shift-m` and download at least one model (e.g., qwen2.5-coder-7b). Alternatively, you can get models via the LM Studio CLI: - - ```sh - lms get qwen2.5-coder-7b - ``` - -3. Make sure the LM Studio API server is running by executing: - - ```sh - lms server start - ``` - -Tip: Set [LM Studio as a login item](https://lmstudio.ai/docs/advanced/headless#run-the-llm-service-on-machine-login) to automate running the LM Studio server. - -### Mistral {#mistral} - -1. Visit the Mistral platform and [create an API key](https://console.mistral.ai/api-keys/) -2. Open the configuration view (`agent: open settings`) and navigate to the Mistral section -3. Enter your Mistral API key - -The Mistral API key will be saved in your keychain. - -Zed will also use the `MISTRAL_API_KEY` environment variable if it's defined. - -#### Custom Models {#mistral-custom-models} - -The Zed agent comes pre-configured with several Mistral models (codestral-latest, mistral-large-latest, mistral-medium-latest, mistral-small-latest, open-mistral-nemo, and open-codestral-mamba). -All the default models support tool use. -If you wish to use alternate models or customize their parameters, you can do so by adding the following to your Zed `settings.json`: - -```json [settings] -{ - "language_models": { - "mistral": { - "api_url": "https://api.mistral.ai/v1", - "available_models": [ - { - "name": "mistral-tiny-latest", - "display_name": "Mistral Tiny", - "max_tokens": 32000, - "max_output_tokens": 4096, - "max_completion_tokens": 1024, - "supports_tools": true, - "supports_images": false - } - ] - } - } -} -``` - -Custom models will be listed in the model dropdown in the Agent Panel. - -### Ollama {#ollama} - -Download and install Ollama from [ollama.com/download](https://ollama.com/download) (Linux or macOS) and ensure it's running with `ollama --version`. - -1. Download one of the [available models](https://ollama.com/models), for example, for `mistral`: - - ```sh - ollama pull mistral - ``` - -2. Make sure that the Ollama server is running. You can start it either via running Ollama.app (macOS) or launching: - - ```sh - ollama serve - ``` - -3. In the Agent Panel, select one of the Ollama models using the model dropdown. - -#### Ollama Context Length {#ollama-context} - -Zed has pre-configured maximum context lengths (`max_tokens`) to match the capabilities of common models. -Zed API requests to Ollama include this as the `num_ctx` parameter, but the default values do not exceed `16384` so users with ~16GB of RAM are able to use most models out of the box. - -See [get_max_tokens in ollama.rs](https://github.com/zed-industries/zed/blob/main/crates/ollama/src/ollama.rs) for a complete set of defaults. - -> **Note**: Token counts displayed in the Agent Panel are only estimates and will differ from the model's native tokenizer. - -Depending on your hardware or use-case you may wish to limit or increase the context length for a specific model via settings.json: - -```json [settings] -{ - "language_models": { - "ollama": { - "api_url": "http://localhost:11434", - "available_models": [ - { - "name": "qwen2.5-coder", - "display_name": "qwen 2.5 coder 32K", - "max_tokens": 32768, - "supports_tools": true, - "supports_thinking": true, - "supports_images": true - } - ] - } - } -} -``` - -If you specify a context length that is too large for your hardware, Ollama will log an error. -You can watch these logs by running: `tail -f ~/.ollama/logs/ollama.log` (macOS) or `journalctl -u ollama -f` (Linux). -Depending on the memory available on your machine, you may need to adjust the context length to a smaller value. - -You may also optionally specify a value for `keep_alive` for each available model. -This can be an integer (seconds) or alternatively a string duration like "5m", "10m", "1h", "1d", etc. -For example, `"keep_alive": "120s"` will allow the remote server to unload the model (freeing up GPU VRAM) after 120 seconds. - -The `supports_tools` option controls whether the model will use additional tools. -If the model is tagged with `tools` in the Ollama catalog, this option should be supplied, and the built-in profiles `Ask` and `Write` can be used. -If the model is not tagged with `tools` in the Ollama catalog, this option can still be supplied with the value `true`; however, be aware that only the `Minimal` built-in profile will work. - -The `supports_thinking` option controls whether the model will perform an explicit "thinking" (reasoning) pass before producing its final answer. -If the model is tagged with `thinking` in the Ollama catalog, set this option and you can use it in Zed. - -The `supports_images` option enables the model's vision capabilities, allowing it to process images included in the conversation context. -If the model is tagged with `vision` in the Ollama catalog, set this option and you can use it in Zed. - -#### Ollama Authentication - -In addition to running Ollama on your own hardware, which generally does not require authentication, Zed also supports connecting to remote Ollama instances. API keys are required for authentication. - -One such service is [Ollama Turbo])(https://ollama.com/turbo). To configure Zed to use Ollama turbo: - -1. Sign in to your Ollama account and subscribe to Ollama Turbo -2. Visit [ollama.com/settings/keys](https://ollama.com/settings/keys) and create an API key -3. Open the settings view (`agent: open settings`) and go to the Ollama section -4. Paste your API key and press enter. -5. For the API URL enter `https://ollama.com` - -Zed will also use the `OLLAMA_API_KEY` environment variables if defined. - -### OpenAI {#openai} - -1. Visit the OpenAI platform and [create an API key](https://platform.openai.com/account/api-keys) -2. Make sure that your OpenAI account has credits -3. Open the settings view (`agent: open settings`) and go to the OpenAI section -4. Enter your OpenAI API key - -The OpenAI API key will be saved in your keychain. - -Zed will also use the `OPENAI_API_KEY` environment variable if it's defined. - -#### Custom Models {#openai-custom-models} - -The Zed agent comes pre-configured to use the latest version for common models (GPT-5, GPT-5 mini, o4-mini, GPT-4.1, and others). -To use alternate models, perhaps a preview release, or if you wish to control the request parameters, you can do so by adding the following to your Zed `settings.json`: - -```json [settings] -{ - "language_models": { - "openai": { - "available_models": [ - { - "name": "gpt-5", - "display_name": "gpt-5 high", - "reasoning_effort": "high", - "max_tokens": 272000, - "max_completion_tokens": 20000 - }, - { - "name": "gpt-4o-2024-08-06", - "display_name": "GPT 4o Summer 2024", - "max_tokens": 128000 - } - ] - } - } -} -``` - -You must provide the model's context window in the `max_tokens` parameter; this can be found in the [OpenAI model documentation](https://platform.openai.com/docs/models). - -OpenAI `o1` models should set `max_completion_tokens` as well to avoid incurring high reasoning token costs. -Custom models will be listed in the model dropdown in the Agent Panel. - -### OpenAI API Compatible {#openai-api-compatible} - -Zed supports using [OpenAI compatible APIs](https://platform.openai.com/docs/api-reference/chat) by specifying a custom `api_url` and `available_models` for the OpenAI provider. -This is useful for connecting to other hosted services (like Together AI, Anyscale, etc.) or local models. - -You can add a custom, OpenAI-compatible model either via the UI or by editing your `settings.json`. - -To do it via the UI, go to the Agent Panel settings (`agent: open settings`) and look for the "Add Provider" button to the right of the "LLM Providers" section title. -Then, fill up the input fields available in the modal. - -To do it via your `settings.json`, add the following snippet under `language_models`: - -```json [settings] -{ - "language_models": { - "openai_compatible": { - // Using Together AI as an example - "Together AI": { - "api_url": "https://api.together.xyz/v1", - "available_models": [ - { - "name": "mistralai/Mixtral-8x7B-Instruct-v0.1", - "display_name": "Together Mixtral 8x7B", - "max_tokens": 32768, - "capabilities": { - "tools": true, - "images": false, - "parallel_tool_calls": false, - "prompt_cache_key": false - } - } - ] - } - } - } -} -``` - -By default, OpenAI-compatible models inherit the following capabilities: - -- `tools`: true (supports tool/function calling) -- `images`: false (does not support image inputs) -- `parallel_tool_calls`: false (does not support `parallel_tool_calls` parameter) -- `prompt_cache_key`: false (does not support `prompt_cache_key` parameter) - -Note that LLM API keys aren't stored in your settings file. -So, ensure you have it set in your environment variables (`_API_KEY=`) so your settings can pick it up. In the example above, it would be `TOGETHER_AI_API_KEY=`. - -### OpenRouter {#openrouter} - -OpenRouter provides access to multiple AI models through a single API. It supports tool use for compatible models. - -1. Visit [OpenRouter](https://openrouter.ai) and create an account -2. Generate an API key from your [OpenRouter keys page](https://openrouter.ai/keys) -3. Open the settings view (`agent: open settings`) and go to the OpenRouter section -4. Enter your OpenRouter API key - -The OpenRouter API key will be saved in your keychain. - -Zed will also use the `OPENROUTER_API_KEY` environment variable if it's defined. - -#### Custom Models {#openrouter-custom-models} - -You can add custom models to the OpenRouter provider by adding the following to your Zed `settings.json`: - -```json [settings] -{ - "language_models": { - "open_router": { - "api_url": "https://openrouter.ai/api/v1", - "available_models": [ - { - "name": "google/gemini-2.0-flash-thinking-exp", - "display_name": "Gemini 2.0 Flash (Thinking)", - "max_tokens": 200000, - "max_output_tokens": 8192, - "supports_tools": true, - "supports_images": true, - "mode": { - "type": "thinking", - "budget_tokens": 8000 - } - } - ] - } - } -} -``` - -The available configuration options for each model are: - -- `name` (required): The model identifier used by OpenRouter -- `display_name` (optional): A human-readable name shown in the UI -- `max_tokens` (required): The model's context window size -- `max_output_tokens` (optional): Maximum tokens the model can generate -- `max_completion_tokens` (optional): Maximum completion tokens -- `supports_tools` (optional): Whether the model supports tool/function calling -- `supports_images` (optional): Whether the model supports image inputs -- `mode` (optional): Special mode configuration for thinking models - -You can find available models and their specifications on the [OpenRouter models page](https://openrouter.ai/models). - -Custom models will be listed in the model dropdown in the Agent Panel. - -#### Provider Routing - -You can optionally control how OpenRouter routes a given custom model request among underlying upstream providers via the `provider` object on each model entry. - -Supported fields (all optional): - -- `order`: Array of provider slugs to try first, in order (e.g. `["anthropic", "openai"]`) -- `allow_fallbacks` (default: `true`): Whether fallback providers may be used if preferred ones are unavailable -- `require_parameters` (default: `false`): Only use providers that support every parameter you supplied -- `data_collection` (default: `allow`): `"allow"` or `"disallow"` (controls use of providers that may store data) -- `only`: Whitelist of provider slugs allowed for this request -- `ignore`: Provider slugs to skip -- `quantizations`: Restrict to specific quantization variants (e.g. `["int4","int8"]`) -- `sort`: Sort strategy for candidate providers (e.g. `"price"` or `"throughput"`) - -Example adding routing preferences to a model: - -```json [settings] -{ - "language_models": { - "open_router": { - "api_url": "https://openrouter.ai/api/v1", - "available_models": [ - { - "name": "openrouter/auto", - "display_name": "Auto Router (Tools Preferred)", - "max_tokens": 2000000, - "supports_tools": true, - "provider": { - "order": ["anthropic", "openai"], - "allow_fallbacks": true, - "require_parameters": true, - "only": ["anthropic", "openai", "google"], - "ignore": ["cohere"], - "quantizations": ["int8"], - "sort": "price", - "data_collection": "allow" - } - } - ] - } - } -} -``` - -These routing controls let you fine‑tune cost, capability, and reliability trade‑offs without changing the model name you select in the UI. - -### Vercel v0 {#vercel-v0} - -[Vercel v0](https://v0.app/docs/api/model) is an expert model for generating full-stack apps, with framework-aware completions optimized for modern stacks like Next.js and Vercel. -It supports text and image inputs and provides fast streaming responses. - -The v0 models are [OpenAI-compatible models](/#openai-api-compatible), but Vercel is listed as first-class provider in the panel's settings view. - -To start using it with Zed, ensure you have first created a [v0 API key](https://v0.dev/chat/settings/keys). -Once you have it, paste it directly into the Vercel provider section in the panel's settings view. - -You should then find it as `v0-1.5-md` in the model dropdown in the Agent Panel. - -### xAI {#xai} - -Zed has first-class support for [xAI](https://x.ai/) models. You can use your own API key to access Grok models. - -1. [Create an API key in the xAI Console](https://console.x.ai/team/default/api-keys) -2. Open the settings view (`agent: open settings`) and go to the **xAI** section -3. Enter your xAI API key - -The xAI API key will be saved in your keychain. Zed will also use the `XAI_API_KEY` environment variable if it's defined. - -> **Note:** While the xAI API is OpenAI-compatible, Zed has first-class support for it as a dedicated provider. For the best experience, we recommend using the dedicated `x_ai` provider configuration instead of the [OpenAI API Compatible](#openai-api-compatible) method. - -#### Custom Models {#xai-custom-models} - -The Zed agent comes pre-configured with common Grok models. If you wish to use alternate models or customize their parameters, you can do so by adding the following to your Zed `settings.json`: - -```json [settings] -{ - "language_models": { - "x_ai": { - "api_url": "https://api.x.ai/v1", - "available_models": [ - { - "name": "grok-1.5", - "display_name": "Grok 1.5", - "max_tokens": 131072, - "max_output_tokens": 8192 - }, - { - "name": "grok-1.5v", - "display_name": "Grok 1.5V (Vision)", - "max_tokens": 131072, - "max_output_tokens": 8192, - "supports_images": true - } - ] - } - } -} -``` - -## Custom Provider Endpoints {#custom-provider-endpoint} - -You can use a custom API endpoint for different providers, as long as it's compatible with the provider's API structure. -To do so, add the following to your `settings.json`: - -```json [settings] -{ - "language_models": { - "some-provider": { - "api_url": "http://localhost:11434" - } - } -} -``` - -Currently, `some-provider` can be any of the following values: `anthropic`, `google`, `ollama`, `openai`. - -This is the same infrastructure that powers models that are, for example, [OpenAI-compatible](#openai-api-compatible). diff --git a/docs/src/ai/mcp.md b/docs/src/ai/mcp.md deleted file mode 100644 index 956477a1c2..0000000000 --- a/docs/src/ai/mcp.md +++ /dev/null @@ -1,139 +0,0 @@ -# Model Context Protocol - -Zed uses the [Model Context Protocol](https://modelcontextprotocol.io/) to interact with context servers. - -> The Model Context Protocol (MCP) is an open protocol that enables seamless integration between LLM applications and external data sources and tools. Whether you're building an AI-powered IDE, enhancing a chat interface, or creating custom AI workflows, MCP provides a standardized way to connect LLMs with the context they need. - -Check out the [Anthropic news post](https://www.anthropic.com/news/model-context-protocol) and the [Zed blog post](https://zed.dev/blog/mcp) for a general intro to MCP. - -## Installing MCP Servers - -### As Extensions - -One of the ways you can use MCP servers in Zed is by exposing them as an extension. -Check out the [MCP Server Extensions](../extensions/mcp-extensions.md) page to learn how to create your own. - -Thanks to our awesome community, many MCP servers have already been added as extensions. -You can check which ones are available via any of these routes: - -1. [the Zed website](https://zed.dev/extensions?filter=context-servers) -2. in the app, open the Command Palette and run the `zed: extensions` action -3. in the app, go to the Agent Panel's top-right menu and look for the "View Server Extensions" menu item - -In any case, here are some popular available servers: - -- [Context7](https://zed.dev/extensions/context7-mcp-server) -- [GitHub](https://zed.dev/extensions/github-mcp-server) -- [Puppeteer](https://zed.dev/extensions/puppeteer-mcp-server) -- [Gem](https://zed.dev/extensions/gem) -- [Brave Search](https://zed.dev/extensions/brave-search-mcp-server) -- [Prisma](https://github.com/aqrln/prisma-mcp-zed) -- [Framelink Figma](https://zed.dev/extensions/framelink-figma-mcp-server) -- [Linear](https://zed.dev/extensions/linear-mcp-server) -- [Resend](https://zed.dev/extensions/resend-mcp-server) - -### As Custom Servers - -Creating an extension is not the only way to use MCP servers in Zed. -You can connect them by adding their commands directly to your `settings.json`, like so: - -```json [settings] -{ - "context_servers": { - "local-mcp-server": { - "command": "some-command", - "args": ["arg-1", "arg-2"], - "env": {} - }, - "remote-mcp-server": { - "url": "custom", - "headers": { "Authorization": "Bearer " } - } - } -} -``` - -Alternatively, you can also add a custom server by accessing the Agent Panel's Settings view (also accessible via the `agent: open settings` action). -From there, you can add it through the modal that appears when you click the "Add Custom Server" button. - -## Using MCP Servers - -### Configuration Check - -Regardless of how you've installed MCP servers, whether as an extension or adding them directly, most servers out there still require some sort of configuration as part of the setup process. - -In the case of extensions, after installing it, Zed will pop up a modal displaying what is required for you to properly set it up. -For example, the GitHub MCP extension requires you to add a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). - -In the case of custom servers, make sure you check the provider documentation to determine what type of command, arguments, and environment variables need to be added to the JSON. - -To check if your MCP server is properly configured, go to the Agent Panel's settings view and watch the indicator dot next to its name. -If they're running correctly, the indicator will be green and its tooltip will say "Server is active". -If not, other colors and tooltip messages will indicate what is happening. - -### Agent Panel Usage - -Once installation is complete, you can return to the Agent Panel and start prompting. - -Some models are better than others when it comes to picking up tools from MCP servers. -Mentioning your server by name always helps the model to pick it up. - -However, if you want to _ensure_ a given MCP server will be used, you can create [a custom profile](./agent-panel.md#custom-profiles) where all built-in tools (or the ones that could cause conflicts with the server's tools) are turned off and only the tools coming from the MCP server are turned on. - -As an example, [the Dagger team suggests](https://container-use.com/agent-integrations#zed) doing that with their [Container Use MCP server](https://zed.dev/extensions/mcp-server-container-use): - -```json [settings] -"agent": { - "profiles": { - "container-use": { - "name": "Container Use", - "tools": { - "fetch": true, - "thinking": true, - "copy_path": false, - "find_path": false, - "delete_path": false, - "create_directory": false, - "list_directory": false, - "diagnostics": false, - "read_file": false, - "open": false, - "move_path": false, - "grep": false, - "edit_file": false, - "terminal": false - }, - "enable_all_context_servers": false, - "context_servers": { - "container-use": { - "tools": { - "environment_create": true, - "environment_add_service": true, - "environment_update": true, - "environment_run_cmd": true, - "environment_open": true, - "environment_file_write": true, - "environment_file_read": true, - "environment_file_list": true, - "environment_file_delete": true, - "environment_checkpoint": true - } - } - } - } - } -} -``` - -### Tool Approval - -Zed's Agent Panel includes the `agent.always_allow_tool_actions` setting that, if set to `false`, will require you to give permission for any editing attempt as well as tool calls coming from MCP servers. - -You can change this by setting this key to `true` in either your `settings.json` or through the Agent Panel's settings view. - -### External Agents - -Note that for [external agents](./external-agents.md) connected through the [Agent Client Protocol](https://agentclientprotocol.com/), access to MCP servers installed from Zed may vary depending on the ACP agent implementation. - -Regarding the built-in ones, Claude Code and Codex both support it, and Gemini CLI does not yet. -In the meantime, learn how to add MCP server support to Gemini CLI through [their documentation](https://github.com/google-gemini/gemini-cli?tab=readme-ov-file#using-mcp-servers). diff --git a/docs/src/ai/models.md b/docs/src/ai/models.md deleted file mode 100644 index 6033bf23fa..0000000000 --- a/docs/src/ai/models.md +++ /dev/null @@ -1,93 +0,0 @@ -# Models - -Zed’s plans offer hosted versions of major LLMs, generally with higher rate limits than using your API keys. -We’re working hard to expand the models supported by Zed’s subscription offerings, so please check back often. - -| Model | Provider | Token Type | Provider Price per 1M tokens | Zed Price per 1M tokens | -| ---------------------- | --------- | ------------------- | ---------------------------- | ----------------------- | -| Claude Opus 4.5 | Anthropic | Input | $5.00 | $5.50 | -| | Anthropic | Output | $25.00 | $27.50 | -| | Anthropic | Input - Cache Write | $6.25 | $6.875 | -| | Anthropic | Input - Cache Read | $0.50 | $0.55 | -| Claude Opus 4.1 | Anthropic | Input | $15.00 | $16.50 | -| | Anthropic | Output | $75.00 | $82.50 | -| | Anthropic | Input - Cache Write | $18.75 | $20.625 | -| | Anthropic | Input - Cache Read | $1.50 | $1.65 | -| Claude Sonnet 4.5 | Anthropic | Input | $3.00 | $3.30 | -| | Anthropic | Output | $15.00 | $16.50 | -| | Anthropic | Input - Cache Write | $3.75 | $4.125 | -| | Anthropic | Input - Cache Read | $0.30 | $0.33 | -| Claude Sonnet 4 | Anthropic | Input | $3.00 | $3.30 | -| | Anthropic | Output | $15.00 | $16.50 | -| | Anthropic | Input - Cache Write | $3.75 | $4.125 | -| | Anthropic | Input - Cache Read | $0.30 | $0.33 | -| Claude Sonnet 3.7 | Anthropic | Input | $3.00 | $3.30 | -| | Anthropic | Output | $15.00 | $16.50 | -| | Anthropic | Input - Cache Write | $3.75 | $4.125 | -| | Anthropic | Input - Cache Read | $0.30 | $0.33 | -| Claude Haiku 4.5 | Anthropic | Input | $1.00 | $1.10 | -| | Anthropic | Output | $5.00 | $5.50 | -| | Anthropic | Input - Cache Write | $1.25 | $1.375 | -| | Anthropic | Input - Cache Read | $0.10 | $0.11 | -| GPT-5 | OpenAI | Input | $1.25 | $1.375 | -| | OpenAI | Output | $10.00 | $11.00 | -| | OpenAI | Cached Input | $0.125 | $0.1375 | -| GPT-5 mini | OpenAI | Input | $0.25 | $0.275 | -| | OpenAI | Output | $2.00 | $2.20 | -| | OpenAI | Cached Input | $0.025 | $0.0275 | -| GPT-5 nano | OpenAI | Input | $0.05 | $0.055 | -| | OpenAI | Output | $0.40 | $0.44 | -| | OpenAI | Cached Input | $0.005 | $0.0055 | -| Gemini 3.0 Pro | Google | Input | $2.00 | $2.20 | -| | Google | Output | $12.00 | $13.20 | -| Gemini 2.5 Pro | Google | Input | $1.25 | $1.375 | -| | Google | Output | $10.00 | $11.00 | -| Gemini 2.5 Flash | Google | Input | $0.30 | $0.33 | -| | Google | Output | $2.50 | $2.75 | -| Grok 4 | X.ai | Input | $3.00 | $3.30 | -| | X.ai | Output | $15.00 | $16.5 | -| | X.ai | Cached Input | $0.75 | $0.825 | -| Grok 4 Fast | X.ai | Input | $0.20 | $0.22 | -| | X.ai | Output | $0.50 | $0.55 | -| | X.ai | Cached Input | $0.05 | $0.055 | -| Grok 4 (Non-Reasoning) | X.ai | Input | $0.20 | $0.22 | -| | X.ai | Output | $0.50 | $0.55 | -| | X.ai | Cached Input | $0.05 | $0.055 | -| Grok Code Fast 1 | X.ai | Input | $0.20 | $0.22 | -| | X.ai | Output | $1.50 | $1.65 | -| | X.ai | Cached Input | $0.02 | $0.022 | - -## Usage {#usage} - -Any usage of a Zed-hosted model will be billed at the Zed Price (rightmost column above). See [Plans and Usage](./plans-and-usage.md) for details on Zed's plans and limits for use of hosted models. - -> We encourage you to think through what model is best for your needs before leaving the Agent Panel to work. All LLMs can "spiral" and occasionally enter unending loops that require user intervention. - -## Context Windows {#context-windows} - -A context window is the maximum span of text and code an LLM can consider at once, including both the input prompt and output generated by the model. - -| Model | Provider | Zed-Hosted Context Window | -| ----------------- | --------- | ------------------------- | -| Claude Opus 4.5 | Anthropic | 200k | -| Claude Opus 4.1 | Anthropic | 200k | -| Claude Sonnet 4 | Anthropic | 200k | -| Claude Sonnet 3.7 | Anthropic | 200k | -| Claude Haiku 4.5 | Anthropic | 200k | -| GPT-5 | OpenAI | 400k | -| GPT-5 mini | OpenAI | 400k | -| GPT-5 nano | OpenAI | 400k | -| Gemini 2.5 Pro | Google | 200k | -| Gemini 2.5 Flash | Google | 200k | -| Gemini 3.0 Pro | Google | 200k | - -> We're planning on expanding supported context windows for hosted Sonnet 4 and Gemini 2.5 Pro/Flash in the near future. Stay tuned! - -Each Agent thread and text thread in Zed maintains its own context window. -The more prompts, attached files, and responses included in a session, the larger the context window grows. - -For best results, it’s recommended you take a purpose-based approach to Agent thread management, starting a new thread for each unique task. - -## Tool Calls {#tool-calls} - -Models can use [tools](./tools.md) to interface with your code, search the web, and perform other useful functions. diff --git a/docs/src/ai/overview.md b/docs/src/ai/overview.md deleted file mode 100644 index e1a9cb77a9..0000000000 --- a/docs/src/ai/overview.md +++ /dev/null @@ -1,33 +0,0 @@ -# AI - -Learn how to get started using AI with Zed and all its capabilities. - -## Setting up AI in Zed - -- [Configuration](./configuration.md): Learn how to set up different language model providers like Anthropic, OpenAI, Ollama, Google AI, and more. - -- [External Agents](./external-agents.md): Learn how to plug in your favorite agent into Zed. - -- [Subscription](./subscription.md): Learn about Zed's hosted models and other billing-related information. - -- [Privacy and Security](./privacy-and-security.md): Understand how Zed handles privacy and security with AI features. - -## Agentic Editing - -- [Agent Panel](./agent-panel.md): Create and manage interactions with LLM agents. - -- [Rules](./rules.md): How to define rules for AI interactions. - -- [Tools](./tools.md): Explore the tools that power Zed's built-in agent. - -- [Model Context Protocol](./mcp.md): Learn about how to configure and use MCP servers. - -- [Inline Assistant](./inline-assistant.md): Discover how to use AI to generate inline transformations directly within a file or terminal. - -## Edit Prediction - -- [Edit Prediction](./edit-prediction.md): Learn about Zed's AI prediction feature that helps autocomplete your code. - -## Text Threads - -- [Text Threads](./text-threads.md): Learn about an editor-based interface for interacting with language models. diff --git a/docs/src/ai/plans-and-usage.md b/docs/src/ai/plans-and-usage.md deleted file mode 100644 index fc59a894aa..0000000000 --- a/docs/src/ai/plans-and-usage.md +++ /dev/null @@ -1,31 +0,0 @@ -# Plans and Usage - -## Available Plans {#plans} - -For costs and more information on pricing, visit [Zed’s pricing page](https://zed.dev/pricing). - -Please note that if you’re interested in just using Zed as the world’s fastest editor, with no AI or subscription features, you can always do so for free, without [authentication](../authentication.md). - -## Usage {#usage} - -Usage of Zed's hosted models is measured on a token basis, converted to dollars at the rates lists on [the Models page](./models.md) (list price from the provider, +10%). - -Zed Pro comes with $5 of monthly dollar credit. A trial of Zed Pro includes $20 of credit, usable for 14 days. Monthly included credit resets on your monthly billing date. - -To view your current usage, you can visit your account at [zed.dev/account](https://zed.dev/account). Information from our metering and billing provider, Orb, is embedded on that page. - -## Spend Limits {#usage-spend-limits} - -At the top of [the Account page](https://zed.dev/account), you'll find an input for `Maximum Token Spend`. The dollar amount here specifies your _monthly_ limit for spend on tokens, _not counting_ the $5/month included with your Pro subscription. - -The default value for all Pro users is $10, for a total monthly spend with Zed of $20 ($10 for your Pro subscription, $10 in incremental token spend). This can be set to $0 to limit your spend with Zed to exactly $10/month. If you adjust this limit _higher_ than $10 and consume more than $10 of incremental token spend, you'll be billed via [threshold billing](./billing.md#threshold-billing). - -Once the spend limit is hit, we’ll stop any further usage until your token spend limit resets. - -## Business Usage {#business-usage} - -Email [sales@zed.dev](mailto:sales@zed.dev) with any questions on business plans. - -## Trials {#trials} - -Note that trials will automatically convert to Zed Free plans on termination, and no cancellation is needed to prevent conversion to Zed Pro. diff --git a/docs/src/ai/privacy-and-security.md b/docs/src/ai/privacy-and-security.md deleted file mode 100644 index 6921567b91..0000000000 --- a/docs/src/ai/privacy-and-security.md +++ /dev/null @@ -1,29 +0,0 @@ -# Privacy and Security - -## Philosophy - -Zed aims to collect on the minimum data necessary to serve and improve our product. - -We believe in opt-in data sharing as the default in building AI products, rather than opt-out, like most of our competitors. Privacy Mode is not a setting to be toggled, it's a default stance. - -As an open-source product, we believe in maximal transparency, and invite you to examine our codebase. If you find issues, we encourage you to share them with us. - -It is entirely possible to use Zed, including Zed's AI capabilities, without sharing any data with us and without authenticating into the product. We're happy to always support this desired use pattern. - -## Documentation - -- [Telemetry](../telemetry.md): How Zed collects general telemetry data. - -- [AI Improvement](./ai-improvement.md): Zed's opt-in-only approach to data collection for AI improvement, whether our Agentic offering or Edit Predictions. - -- [Accounts](../authentication.md): When and why you'd need to authenticate into Zed, how to do so, and what scope we need from you. - -- [Collab](https://zed.dev/faq#data-and-privacy): How Zed's live collaboration works, and how data flows to provide the experience (we don't store your code!). - -## Legal Links - -- [Terms of Service](https://zed.dev/terms-of-service) -- [Terms of Use](https://zed.dev/terms) -- [Privacy Policy](https://zed.dev/privacy-policy) -- [Zed's Contributor License and Feedback Agreement](https://zed.dev/cla) -- [Subprocessors](https://zed.dev/subprocessors) diff --git a/docs/src/ai/rules.md b/docs/src/ai/rules.md deleted file mode 100644 index 4169920425..0000000000 --- a/docs/src/ai/rules.md +++ /dev/null @@ -1,75 +0,0 @@ -# Using Rules {#using-rules} - -A rule is essentially a prompt that is inserted at the beginning of each interaction with the Agent. -Currently, Zed supports adding rules through files inserted directly in the worktree or through the Rules Library, which allows you to store multiple rules for constant or on-demand usage. - -## `.rules` files - -Zed supports including `.rules` files at the top level of worktrees, and they act as project-level instructions that are included in all of your interactions with the Agent Panel. -Other names for this file are also supported for compatibility with other agents, but note that the first file which matches in this list will be used: - -- `.rules` -- `.cursorrules` -- `.windsurfrules` -- `.clinerules` -- `.github/copilot-instructions.md` -- `AGENT.md` -- `AGENTS.md` -- `CLAUDE.md` -- `GEMINI.md` - -## Rules Library {#rules-library} - -The Rules Library is an interface for writing and managing rules. Like other text-driven UIs in Zed, it is a full editor with syntax highlighting, keyboard shortcuts, etc. - -You can use the inline assistant right in the rules editor, allowing you to automate and rewrite rules. - -### Opening the Rules Library - -1. Open the Agent Panel. -2. Click on the Agent menu (`...`) in the top right corner. -3. Select `Rules...` from the dropdown. - -You can also reach it by running {#action agent::OpenRulesLibrary} in the command palette or through the {#kb agent::OpenRulesLibrary} keybinding. - -### Managing Rules - -Once a rules file is selected, you can edit it directly in the built-in editor. Its title can be changed from the editor title bar as well. - -Rules can be duplicated, deleted, or added to the default rules using the buttons in the rules editor. - -### Creating Rules {#creating-rules} - -To create a rule file, simply open the `Rules Library` and click the `+` button. Rules files are stored locally and can be accessed from the library at any time. - -Having a series of rules files specifically tailored to prompt engineering can also help you write consistent and effective rules. - -Here are a couple of helpful resources for writing better rules: - -- [Anthropic: Prompt Engineering](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview) -- [OpenAI: Prompt Engineering](https://platform.openai.com/docs/guides/prompt-engineering) - -### Editing the Default Rules {#default-rules} - -Zed allows you to customize the default rules used when interacting with LLMs. -Or to be more precise, it uses a series of rules that are combined to form the default rules. - -Default rules are included in the context of every new thread automatically. -You can also manually add other rules (that are not flagged as default) as context using the `@rule` command. - -## Migrating from Prompt Library - -Previously, the Rules Library was called the "Prompt Library". -The new rules system replaces the Prompt Library except in a few specific cases, which are outlined below. - -### Slash Commands in Rules - -Previously, it was possible to use slash commands (now @-mentions) in custom prompts (now rules). -There is currently no support for using @-mentions in rules files, however, slash commands are supported in rules files when used with text threads. -See the documentation for using [slash commands in rules](./text-threads.md#slash-commands-in-rules) for more information. - -### Prompt templates - -Zed maintains backwards compatibility with its original template system, which allows you to customize prompts used throughout the application, including the inline assistant. -While the Rules Library is now the primary way to manage prompts, you can still use these legacy templates to override default prompts. -For more details, see the [Rules Templates](./text-threads.md#rule-templates) section under [Text Threads](./text-threads.md). diff --git a/docs/src/ai/subscription.md b/docs/src/ai/subscription.md deleted file mode 100644 index 704fdc0ce3..0000000000 --- a/docs/src/ai/subscription.md +++ /dev/null @@ -1,13 +0,0 @@ -# Subscription - -Zed's hosted models are offered via subscription to Zed Pro or Zed Business. - -> Using [your own API keys](./llm-providers.md), and [external agents](./external-agents.md), is _free_ — you do not need to subscribe to a Zed plan to use AI features. - -See the following pages for specific aspects of our subscription offering: - -- [Models](./models.md): Overview of the models offered by Zed's subscriptions. - -- [Plans and Usage](./plans-and-usage.md): Outlines Zed's plans and how usage is measured. - -- [Billing](./billing.md): Billing policies and procedures, and how to update or view various billing settings. diff --git a/docs/src/ai/text-threads.md b/docs/src/ai/text-threads.md deleted file mode 100644 index c82cda7265..0000000000 --- a/docs/src/ai/text-threads.md +++ /dev/null @@ -1,291 +0,0 @@ -# Text Threads - -## Overview {#overview} - -Text threads in the [Agent Panel](./agent-panel.md) function similarly to any other editor. -You can use custom key bindings and work with multiple cursors, allowing for seamless transitions between coding and engaging in discussions with the language models. - -However, the text threads differ in the inclusion of message blocks. -These blocks serve as containers for text that correspond to different roles within the context. -These roles include: - -- `You` -- `Assistant` -- `System` - -To begin, type a message in a `You` block. - -![Asking a question](https://zed.dev/img/assistant/ask-a-question.png) - -As you type, the remaining tokens count for the selected model is updated. - -Inserting text from an editor is as simple as highlighting the text and running `agent: add selection to thread` ({#kb agent::AddSelectionToThread}); Zed will wrap it in a fenced code block if it is code. - -![Quoting a selection](https://zed.dev/img/assistant/quoting-a-selection.png) - -To submit a message, use {#kb assistant::Assist}(`assistant: assist`). -Unlike normal threads, where pressing enter would submit the message, in text threads, our goal is to make it feel as close to a regular editor as possible. -So, pressing {#kb editor::Newline} simply inserts a new line. - -After submitting a message, the response will be streamed below, in an `Assistant` message block. - -![Receiving an answer](https://zed.dev/img/assistant/receiving-an-answer.png) - -The stream can be canceled at any point with escape. -This is useful if you realize early on that the response is not what you were looking for. - -If you want to start a new conversation at any time, you can hit cmd-n|ctrl-n or use the `New Chat` menu option in the hamburger menu at the top left of the panel. - -Simple back-and-forth conversations work well with the text threads. -However, there may come a time when you want to modify the previous text in the conversation and steer it in a different direction. - -## Editing a Text Thread {#edit-text-thread} - -Text threads give you the flexibility to have control over the context. -You can freely edit any previous text, including the responses from the LLM. -If you want to remove a message block entirely, simply place your cursor at the beginning of the block and use the `delete` key. -A typical workflow might involve making edits and adjustments throughout the context to refine your inquiry or provide additional information. -Here's an example: - -1. Write text in a `You` block. -2. Submit the message with {#kb assistant::Assist}. -3. Receive an `Assistant` response that doesn't meet your expectations. -4. Cancel the response with escape. -5. Erase the content of the `Assistant` message block and remove the block entirely. -6. Add additional context to your original message. -7. Submit the message with {#kb assistant::Assist}. - -Being able to edit previous messages gives you control over how tokens are used. -You don't need to start up a new chat to correct a mistake or to add additional information, and you don't have to waste tokens by submitting follow-up corrections. - -> **Note**: The act of editing past messages is often referred to as "Rewriting History" in the context of the language models. - -Some additional points to keep in mind: - -- You can cycle the role of a message block by clicking on the role, which is useful when you receive a response in an `Assistant` block that you want to edit and send back up as a `You` block. - -## Commands Overview {#commands} - -Slash commands enhance the assistant's capabilities. -Begin by typing a `/` at the beginning of the line to see a list of available commands: - -- `/default`: Inserts the default rule -- `/diagnostics`: Injects errors reported by the project's language server -- `/fetch`: Fetches the content of a webpage and inserts it -- `/file`: Inserts a single file or a directory of files -- `/now`: Inserts the current date and time -- `/prompt`: Adds a custom-configured prompt to the context ([see Rules Library](./rules.md#rules-library)) -- `/symbols`: Inserts the current tab's active symbols -- `/tab`: Inserts the content of the active tab or all open tabs -- `/terminal`: Inserts a select number of lines of output from the terminal -- `/selection`: Inserts the selected text - -> **Note:** Remember, commands are only evaluated when the text thread is created or when the command is inserted, so a command like `/now` won't continuously update, or `/file` commands won't keep their contents up to date. - -### `/default` - -Read more about `/default` in the [Rules: Editing the Default Rules](./rules.md#default-rules) section. - -Usage: `/default` - -### `/diagnostics` - -The `/diagnostics` command injects errors reported by the project's language server into the context. -This is useful for getting an overview of current issues in your project. - -Usage: `/diagnostics [--include-warnings] [path]` - -- `--include-warnings`: Optional flag to include warnings in addition to errors. -- `path`: Optional path to limit diagnostics to a specific file or directory. - -### `/file` - -The `/file` command inserts the content of a single file or a directory of files into the context. -This allows you to reference specific parts of your project in your conversation with the assistant. - -Usage: `/file ` - -You can use glob patterns to match multiple files or directories. - -Examples: - -- `/file src/index.js` - Inserts the content of `src/index.js` into the context. -- `/file src/*.js` - Inserts the content of all `.js` files in the `src` directory. -- `/file src` - Inserts the content of all files in the `src` directory. - -### `/now` - -The `/now` command inserts the current date and time into the context. -This can be useful for letting the language model know the current time (and by extension, how old their current knowledge base is). - -Usage: `/now` - -### `/prompt` - -The `/prompt` command inserts a prompt from the prompt library into the context. -It can also be used to nest prompts within prompts. - -Usage: `/prompt ` - -Related: `/default` - -### `/symbols` - -The `/symbols` command inserts the active symbols (functions, classes, etc.) from the current tab into the context. -This is useful for getting an overview of the structure of the current file. - -Usage: `/symbols` - -### `/tab` - -The `/tab` command inserts the content of the active tab or all open tabs into the context. -This allows you to reference the content you're currently working on. - -Usage: `/tab [tab_name|all]` - -- `tab_name`: Optional name of a specific tab to insert. -- `all`: Insert content from all open tabs. - -Examples: - -- `/tab` - Inserts the content of the active tab. -- `/tab "index.js"` - Inserts the content of the tab named "index.js". -- `/tab all` - Inserts the content of all open tabs. - -### `/terminal` - -The `/terminal` command inserts a select number of lines of output from the terminal into the context. -This is useful for referencing recent command outputs or logs. - -Usage: `/terminal []` - -- ``: Optional parameter to specify the number of lines to insert (default is 50). - -### `/selection` - -The `/selection` command inserts the selected text in the editor into the context. -This is useful for referencing specific parts of your code. - -This is equivalent to the `agent: add selection to thread` command ({#kb agent::AddSelectionToThread}). - -Usage: `/selection` - -## Commands in the Rules Library {#slash-commands-in-rules} - -[Commands](#commands) can be used in rules, in the Rules Library (previously known as Prompt Library), to insert dynamic content or perform actions. -For example, if you want to create a rule where it is important for the model to know the date, you can use the `/now` command to insert the current date. - -> **Warn:** Slash commands in rules **only** work when they are used in text threads. Using them in non-text threads is not supported. - -> **Note:** Slash commands in rules **must** be on their own line. - -See the [list of commands](#commands) above for more information on commands, and what slash commands are available. - -### Example - -```plaintext -You are an expert Rust engineer. The user has asked you to review their project and answer some questions. - -Here is some information about their project: - -/file Cargo.toml -``` - -In the above example, the `/file` command is used to insert the contents of the `Cargo.toml` file (or all `Cargo.toml` files present in the project) into the rule. - -## Nesting Rules - -Similar to adding rules to the default rules, you can nest rules within other rules with the `/prompt` command (only supported in Text Threads currently). - -You might want to nest rules to: - -- Create templates on the fly -- Break collections like docs or references into smaller, mix-and-matchable parts -- Create variants of a similar rule (e.g., `Async Rust - Tokio` vs. `Async Rust - Async-std`) - -### Example - -```plaintext -Title: Zed-Flavored Rust - -## About Zed - -/prompt Zed: Zed (a rule about what Zed is) - -## Rust - Zed Style - -/prompt Rust: Async - Async-std (zed doesn't use tokio) -/prompt Rust: Zed-style Crates (we have some unique conventions) -/prompt Rust - Workspace deps (bias towards reusing deps from the workspace) -``` - -_The text in parentheses above are comments and are not part of the rule._ - -> **Note:** While you technically _can_ nest a rule within itself, we wouldn't recommend it (in the strongest of terms.) Use at your own risk! - -By using nested rules, you can create modular and reusable rule components that can be combined in various ways to suit different scenarios. - -> **Note:** When using slash commands to bring in additional context, the injected content can be edited directly inline in the text thread—edits here will not propagate to the saved rules. - -## Extensibility - -Additional slash commands can be provided by extensions. - -See [Extension: Slash Commands](../extensions/slash-commands.md) to learn how to create your own. - -## Text Threads vs. Threads - -For some time, text threads were the only way to interact with AI in Zed. -In May 2025, we introduced a new version of the agent panel, which, as opposed to being editor-based, is optimized for readability. -Visit [the Agent Panel page](./agent-panel.md) to learn more about it. - -More importantly, aside from the many UI differences, the major aspect that sets one apart from the other is that tool calls don't work in Text Threads. -Due to that, it's accurate to say that Text Threads aren't conceptually agentic, as they can't perform any action on your behalf (or any action at all). - -Think of it more like a regular/"traditional" AI chat, where the only thing you can get from the model is simply just text. -Consequently, [MCP servers](./mcp.md) and [external agents](./external-agents.md) are also not available in Text Threads. - -## Advanced Concepts - -### Rule Templates {#rule-templates} - -Zed uses rule templates to power internal assistant features, like the terminal assistant, or the content rules used in the inline assistant. - -Zed has the following internal rule templates: - -- `content_prompt.hbs`: Used for generating content in the editor. -- `terminal_assistant_prompt.hbs`: Used for the terminal assistant feature. - -At this point it is unknown if we will expand templates further to be user-creatable. - -### Overriding Templates - -> **Note:** It is not recommended to override templates unless you know what you are doing. Editing templates will break your assistant if done incorrectly. - -Zed allows you to override the default rules used for various assistant features by placing custom Handlebars (.hbs) templates in your `~/.config/zed/prompt_overrides` directory. - -The following templates can be overridden: - -1. [`content_prompt.hbs`](https://github.com/zed-industries/zed/tree/main/assets/prompts/content_prompt.hbs): Used for generating content in the editor. - -2. [`terminal_assistant_prompt.hbs`](https://github.com/zed-industries/zed/tree/main/assets/prompts/terminal_assistant_prompt.hbs): Used for the terminal assistant feature. - -> **Note:** Be sure you want to override these, as you'll miss out on iteration on our built-in features. -> This should be primarily used when developing Zed. - -You can customize these templates to better suit your needs while maintaining the core structure and variables used by Zed. -Zed will automatically reload your prompt overrides when they change on disk. - -Consult Zed's [assets/prompts](https://github.com/zed-industries/zed/tree/main/assets/prompts) directory for current versions you can play with. - -### History {#history} - -After you submit your first message in a text thread, a name for your context is generated by the language model, and the context is automatically saved to your file system in - -- `~/.config/zed/conversations` (macOS) -- `~/.local/share/zed/conversations` (Linux) -- `%LocalAppData%\Zed\conversations` (Windows) - -You can access and load previous contexts by clicking on the history button in the top-left corner of the agent panel. - -![Viewing assistant history](https://zed.dev/img/assistant/assistant-history.png) diff --git a/docs/src/ai/tools.md b/docs/src/ai/tools.md deleted file mode 100644 index e40cfcec84..0000000000 --- a/docs/src/ai/tools.md +++ /dev/null @@ -1,73 +0,0 @@ -# Tools - -Zed's built-in agent has access to a variety of tools that allow it to interact with your codebase and perform tasks. - -## Read & Search Tools - -### `diagnostics` - -Gets errors and warnings for either a specific file or the entire project, useful after making edits to determine if further changes are needed. -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. - -### `fetch` - -Fetches a URL and returns the content as Markdown. Useful for providing docs as context. - -### `find_path` - -Quickly finds files by matching glob patterns (like "\*_/_.js"), returning matching file paths alphabetically. - -### `grep` - -Searches file contents across the project using regular expressions, preferred for finding symbols in code without knowing exact file paths. - -### `list_directory` - -Lists files and directories in a given path, providing an overview of filesystem contents. - -### `now` - -Returns the current date and time. - -### `open` - -Opens a file or URL with the default application associated with it on the user's operating system. - -### `read_file` - -Reads the content of a specified file in the project, allowing access to file contents. - -### `thinking` - -Allows the Agent to work through problems, brainstorm ideas, or plan without executing actions, useful for complex problem-solving. - -### `web_search` - -Searches the web for information, providing results with snippets and links from relevant web pages, useful for accessing real-time information. - -## Edit Tools - -### `copy_path` - -Copies a file or directory recursively in the project, more efficient than manually reading and writing files when duplicating content. - -### `create_directory` - -Creates a new directory at the specified path within the project, creating all necessary parent directories (similar to `mkdir -p`). - -### `delete_path` - -Deletes a file or directory (including contents recursively) at the specified path and confirms the deletion. - -### `edit_file` - -Edits files by replacing specific text with new content. - -### `move_path` - -Moves or renames a file or directory in the project, performing a rename if only the filename differs. - -### `terminal` - -Executes shell commands and returns the combined output, creating a new shell process for each invocation. diff --git a/docs/src/all-actions.md b/docs/src/all-actions.md deleted file mode 100644 index e5a45a8fd8..0000000000 --- a/docs/src/all-actions.md +++ /dev/null @@ -1,3 +0,0 @@ -# All Actions - -{#ACTIONS_TABLE#} diff --git a/docs/src/authentication.md b/docs/src/authentication.md deleted file mode 100644 index 0ea97040a0..0000000000 --- a/docs/src/authentication.md +++ /dev/null @@ -1,37 +0,0 @@ -# Authenticate with Zed - -Signing in to Zed is not required. You can use most features you'd expect in a code editor without ever doing so. We'll outline the few features that do require signing in, and how to do so, here. - -## What Features Require Signing In? - -1. All real-time [collaboration features](./collaboration/overview.md). -2. [LLM-powered features](./ai/overview.md), if you are using Zed as the provider of your LLM models. To use AI without signing in, you can [bring and configure your own API keys](./ai/llm-providers.md#use-your-own-keys). - -## Signing In - -Zed uses GitHub's OAuth flow to authenticate users, requiring only the `read:user` GitHub scope, which grants read-only access to your GitHub profile information. - -1. Open Zed and click the `Sign In` button in the top-right corner of the window, or run the `client: sign in` command from the command palette (`cmd-shift-p` on macOS or `ctrl-shift-p` on Windows/Linux). -2. Your default web browser will open to the Zed sign-in page. -3. Authenticate with your GitHub account when prompted. -4. After successful authentication, your browser will display a confirmation, and you'll be automatically signed in to Zed. - -**Note**: If you're behind a corporate firewall, ensure that connections to `zed.dev` and `collab.zed.dev` are allowed. - -## Signing Out - -To sign out of Zed, you can use either of these methods: - -- Click on the profile icon in the upper right corner and select `Sign Out` from the dropdown menu. -- Open the command palette and run the `client: sign out` command. - -## Email Addresses {#email} - -Your Zed account's email address is the address provided by GitHub OAuth. If you have a public email address then it will be used, otherwise your primary GitHub email address will be used. Changes to your email address on GitHub can be synced to your Zed account by [signing in to zed.dev](https://zed.dev/sign_in). - -Stripe is used for billing, and will use your Zed account's email address when starting a subscription. Changes to your Zed account email address do not currently update the email address used in Stripe. See [Updating Billing Information](./ai/billing.md#updating-billing-info) for how to change this email address. - -## Hiding Sign In button from the interface - -In case the Sign In feature is not used, it's possible to hide that from the interface by using `show_sign_in` settings property. -Refer to [Visual Customization page](./visual-customization.md) for more details. diff --git a/docs/src/collaboration/channels.md b/docs/src/collaboration/channels.md deleted file mode 100644 index ebc2760275..0000000000 --- a/docs/src/collaboration/channels.md +++ /dev/null @@ -1,122 +0,0 @@ -# Channels - -Channels provide a way to streamline collaborating for software engineers in many ways, but particularly: - -- Pairing – when working on something together, you both have your own screen, mouse, and keyboard. -- Mentoring – it's easy to jump in to someone else's context, and help them get unstuck, without the friction of pushing code up. -- Refactoring – you can have multiple people join in on large refactoring without fear of conflict. -- Ambient awareness – you can see what everyone else is working on with no need for status emails or meetings. - -Each channel corresponds to an ongoing project or work-stream. -You can see who's in a channel as their avatars will show up in the sidebar. -This makes it easy to see what everyone is doing and where to find them if needed. - -Create a channel by clicking the `+` icon next to the `Channels` text in the collab panel. -Create a subchannel by right clicking an existing channel and selecting `New Subchannel`. - -You can mix channels for your day job, as well as side-projects in your collab panel. - -Joining a channel adds you to a shared room where you can work on projects together. - -_Join [our channel tree](https://zed.dev/channel/zed-283) to get an idea of how you can organize yours._ - -## Inviting People - -By default, channels you create can only be accessed by you. -You can invite collaborators by right clicking and selecting `Manage members`. - -When you have subchannels nested under others, permissions are inherited. -For instance, adding people to the top-level channel in your channel tree will automatically give them access to its subchannels. - -Once you have added someone, they can either join your channel by clicking on it in their Zed sidebar, or you can share the link to the channel so that they can join directly. - -## Voice Chat - -You can mute/unmute your microphone via the microphone icon in the upper right-hand side of the window. - -> Note: When joining a channel, Zed will automatically share your microphone with other users in the call, if your OS allows it. -> If you'd prefer your microphone to be off when joining a channel, you can do so via the [`mute_on_join`](../configuring-zed.md#calls) setting. - -## Sharing Projects - -After joining a channel, you can share a project over the channel via the `Share` button in the upper right-hand side of the window. -This will allow channel members to edit the code hosted on your machine as though they had it checked out locally. - -When you are editing someone else's project, you still have the full power of the editor at your fingertips; you can jump to definitions, use the AI assistant, and see any diagnostic errors. -This is extremely powerful for pairing, as one of you can be implementing the current method while the other is reading and researching the correct solution to the next problem. -And, because you have your own config running, it feels like you're using your own machine. - -We aim to eliminate the distinction between local and remote projects as much as possible. -Collaborators can open, edit, and save files, perform searches, interact with the language server, etc. -Guests have a read-only view of the project, including access to language server info. - -### Unsharing a Project - -You can remove a project from a channel by clicking on the `Unshare` button in the title bar. - -Collaborators that are currently in that project will be disconnected from the project and will not be able to rejoin it unless you share it again. - -## Channel Notes - -Each channel has a Markdown notes file associated with it to keep track of current status, new ideas, or to collaborate on building out the design for the feature that you're working on before diving into code. - -This is similar to a Google Doc, except powered by Zed's collaborative software and persisted to our servers. - -Open the channel notes by clicking on the document icon to the right of the channel name in the collaboration panel. - -> Note: You can view a channel's notes without joining the channel, if you'd just like to read up on what has been written. - -## Following Collaborators - -To follow a collaborator, click on their avatar in the top left of the title bar. -You can also cycle through collaborators using {#kb workspace::FollowNextCollaborator} or `workspace: follow next collaborator` in the command palette. - -When you join a project, you'll immediately start following the collaborator that invited you. - -When you are in a pane that is following a collaborator, you will: - -- follow their cursor and scroll position -- follow them to other files in the same project -- instantly swap to viewing their screenshare in that pane, if they are sharing their screen and leave the project - -To stop following, simply move your mouse or make an edit via your keyboard. - -### How Following Works - -Following is confined to a particular pane. -When a pane is following a collaborator, it is outlined in their cursor color. - -Avatars of collaborators in the same project as you are in color, and have a cursor color. -Collaborators in other projects are shown in gray. - -This pane-specific behavior allows you to follow someone in one pane while navigating independently in another and can be an effective layout for some collaboration styles. - -### Following a Terminal - -Following is not currently supported in the terminal in the way it is supported in the editor. -As a workaround, collaborators can share their screen and you can follow that instead. - -## Screen Sharing - -Share your screen with collaborators in the current channel by clicking on the `Share screen` (monitor icon) button in the top right of the title bar. -If you have multiple displays, you can choose which one to share via the chevron to the right of the monitor icon. - -After you've shared your screen, others can click on the `Screen` entry under your name in the collaboration panel to open a tab that always keeps it visible. -If they are following you, Zed will automatically switch between following your cursor in their Zed instance and your screen share, depending on whether you are focused on Zed or another application, like a web browser. - -> Note: Collaborators can see your entire screen when you are screen sharing, so be careful not to share anything you don't want to share. -> Remember to stop screen sharing when you are finished. - -## Livestreaming & Guests - -A Channel can also be made Public. -This allows anyone to join the channel by clicking on the link. - -Guest users in channels can hear and see everything that is happening, and have read only access to projects and channel notes. - -If you'd like to invite a guest to participate in a channel for the duration of a call you can do so by right clicking on them in the Collaboration Panel. -"Allowing Write Access" will allow them to edit any projects shared into the call, and to use their microphone and share their screen if they wish. - -## Leaving a Call - -You can leave a channel by clicking on the `Leave call` button in the upper right-hand side of the window. diff --git a/docs/src/collaboration/contacts-and-private-calls.md b/docs/src/collaboration/contacts-and-private-calls.md deleted file mode 100644 index f011fa2c67..0000000000 --- a/docs/src/collaboration/contacts-and-private-calls.md +++ /dev/null @@ -1,25 +0,0 @@ -# Contacts and Private Calls - -Zed allows you to have private calls / collaboration sessions with those in your contacts. -These calls can be one-on-ones or contain any number of users from your contacts. - -## Adding a Contact - -1. In the collaboration panel, click the `+` button next to the `Contacts` section -1. Search for the contact using their GitHub handle.\ - _Note: Your contact must be an existing Zed user who has completed the GitHub authentication sign-in flow._ -1. Your contact will receive a notification. - Once they accept, you'll both appear in each other's contact list. - -## Private Calls - -To start up a private call... - -1. Click the `...` menu next to an online contact's name in the collaboration panel. -1. Click `Call ` - -Once you've begun a private call, you can add other online contacts by clicking on their name in the collaboration panel. - ---- - -_Aside from a few additional features (channel notes, etc.), collaboration in private calls is largely the same as it is in [channels](./channels.md)._ diff --git a/docs/src/collaboration/overview.md b/docs/src/collaboration/overview.md deleted file mode 100644 index 719aa56ee3..0000000000 --- a/docs/src/collaboration/overview.md +++ /dev/null @@ -1,24 +0,0 @@ -# Collaboration - -At Zed, we believe that great things are built by great people working together. -We have designed Zed to help individuals work faster and help teams of people work together more effectively. - -In Zed, all collaboration happens in the collaboration panel, which can be opened via {#kb collab_panel::ToggleFocus} or `collab panel: toggle focus` from the command palette. - -You will need to [sign in](../authentication.md#signing-in) in order to access features within the collaboration panel. - -## Collaboration panel - -The collaboration panel is broken down into two sections: - -1. [Channels](./channels.md): Ongoing project rooms where team members can share projects, collaborate on code, and maintain ambient awareness of what everyone is working on. -1. [Contacts and Private Calls](./contacts-and-private-calls.md): Your contacts list for ad-hoc private collaboration. - ---- - -> Note: Only collaborate with people that you trust. -> Since sharing a project gives them access to your local file system, you should not share projects with people you do not trust; they could potentially do some nasty things. -> -> In the future, we will do more to prevent this type of access beyond the shared project and add more control over what collaborators can do, but for now, only collaborate with people you trust. - -See our [Data and Privacy FAQs](https://zed.dev/faq#data-and-privacy) for collaboration. diff --git a/docs/src/command-line-interface.md b/docs/src/command-line-interface.md deleted file mode 100644 index 1a7831811d..0000000000 --- a/docs/src/command-line-interface.md +++ /dev/null @@ -1,18 +0,0 @@ -# Command-line Interface - -Zed has a CLI, on Linux this should come with the distribution's Zed package (binary name can vary from distribution to distribution, `zed` will be used later for brevity). -For macOS, the CLI comes in the same package with the editor binary, and could be installed into the system with the `cli: install` Zed command which will create a symlink to the `/usr/local/bin/zed`. -It can also be built from source out of the `cli` crate in this repository. - -Use `zed --help` to see the full list of capabilities. -General highlights: - -- Opening another empty Zed window: `zed` - -- Opening a file or directory in Zed: `zed /path/to/entry` (use `-n` to open in the new window) - -- Reading from stdin: `ps axf | zed -` - -- Starting Zed with logs in the terminal: `zed --foreground` - -- Uninstalling Zed and all its related files: `zed --uninstall` diff --git a/docs/src/command-palette.md b/docs/src/command-palette.md deleted file mode 100644 index b573fc6a5f..0000000000 --- a/docs/src/command-palette.md +++ /dev/null @@ -1,9 +0,0 @@ -# Command Palette - -The Command Palette is the main way to access pretty much any functionality that's available in Zed. Its keybinding is the first one you should make yourself familiar with. To open it, hit: {#kb command_palette::Toggle}. - -![The opened Command Palette](https://zed.dev/img/features/command-palette.jpg) - -Try it! Open the Command Palette and type in `new file`. You should see the list of commands being filtered down to `workspace: new file`. Hit return and you end up with a new buffer. - -Any time you see instructions that include commands of the form `zed: ...` or `editor: ...` and so on that means you need to execute them in the Command Palette. diff --git a/docs/src/completions.md b/docs/src/completions.md deleted file mode 100644 index ff96ede750..0000000000 --- a/docs/src/completions.md +++ /dev/null @@ -1,28 +0,0 @@ -# Completions - -Zed supports two sources for completions: - -1. "Code Completions" provided by Language Servers (LSPs) automatically installed by Zed or via [Zed Language Extensions](languages.md). -2. "Edit Predictions" provided by Zed's own Zeta model or by external providers like [GitHub Copilot](#github-copilot) or [Supermaven](#supermaven). - -## Language Server Code Completions {#code-completions} - -When there is an appropriate language server available, Zed will provide completions of variable names, functions, and other symbols in the current file. You can disable these by adding the following to your Zed `settings.json` file: - -```json [settings] -"show_completions_on_input": false -``` - -You can manually trigger completions with `ctrl-space` or by triggering the `editor::ShowCompletions` action from the command palette. - -For more information, see: - -- [Configuring Supported Languages](./configuring-languages.md) -- [List of Zed Supported Languages](./languages.md) - -## Edit Predictions {#edit-predictions} - -Zed has built-in support for predicting multiple edits at a time [via Zeta](https://huggingface.co/zed-industries/zeta), Zed's open-source and open-data model. -Edit predictions appear as you type, and most of the time, you can accept them by pressing `tab`. - -See the [edit predictions documentation](./ai/edit-prediction.md) for more information on how to setup and configure Zed's edit predictions. diff --git a/docs/src/configuring-languages.md b/docs/src/configuring-languages.md deleted file mode 100644 index 9185b67906..0000000000 --- a/docs/src/configuring-languages.md +++ /dev/null @@ -1,465 +0,0 @@ -# Configuring Supported Languages - -Zed offers powerful customization options for each programming language it supports. This guide will walk you through the various ways you can tailor your coding experience to your preferences and project requirements. - -Zed's language support is built on two main technologies: - -1. Tree-sitter: This handles syntax highlighting and structure-based features like the outline panel. -2. Language Server Protocol (LSP): This provides semantic features such as code completion and diagnostics. - -These components work together to provide Zed's language capabilities. - -In this guide, we'll cover: - -- Language-specific settings -- File associations -- Working with language servers -- Formatting and linting configuration -- Customizing syntax highlighting and themes -- Advanced language features - -By the end of this guide, you should know how to configure and customize supported languages in Zed. - -For a comprehensive list of languages supported by Zed and their specific configurations, see our [Supported Languages](./languages.md) page. To go further, you could explore developing your own extensions to add support for additional languages or enhance existing functionality. For more information on creating language extensions, see our [Language Extensions](./extensions/languages.md) guide. - -## Language-specific Settings - -Zed allows you to override global settings for individual languages. These custom configurations are defined in your `settings.json` file under the `languages` key. - -Here's an example of language-specific settings: - -```json [settings] -"languages": { - "Python": { - "tab_size": 4, - "formatter": "language_server", - "format_on_save": "on" - }, - "JavaScript": { - "tab_size": 2, - "formatter": { - "external": { - "command": "prettier", - "arguments": ["--stdin-filepath", "{buffer_path}"] - } - } - } -} -``` - -You can customize a wide range of settings for each language, including: - -- [`tab_size`](./configuring-zed.md#tab-size): The number of spaces for each indentation level -- [`formatter`](./configuring-zed.md#formatter): The tool used for code formatting -- [`format_on_save`](./configuring-zed.md#format-on-save): Whether to automatically format code when saving -- [`enable_language_server`](./configuring-zed.md#enable-language-server): Toggle language server support -- [`hard_tabs`](./configuring-zed.md#hard-tabs): Use tabs instead of spaces for indentation -- [`preferred_line_length`](./configuring-zed.md#preferred-line-length): The recommended maximum line length -- [`soft_wrap`](./configuring-zed.md#soft-wrap): How to wrap long lines of code -- [`show_completions_on_input`](./configuring-zed.md#show-completions-on-input): Whether or not to show completions as you type -- [`show_completion_documentation`](./configuring-zed.md#show-completion-documentation): Whether to display inline and alongside documentation for items in the completions menu -- [`colorize_brackets`](./configuring-zed.md#colorize-brackets): Whether to use tree-sitter bracket queries to detect and colorize the brackets in the editor (also known as "rainbow brackets") - -These settings allow you to maintain specific coding styles across different languages and projects. - -## File Associations - -Zed automatically detects file types based on their extensions, but you can customize these associations to fit your workflow. - -To set up custom file associations, use the [`file_types`](./configuring-zed.md#file-types) setting in your `settings.json`: - -```json [settings] -"file_types": { - "C++": ["c"], - "TOML": ["MyLockFile"], - "Dockerfile": ["Dockerfile*"] -} -``` - -This configuration tells Zed to: - -- Treat `.c` files as C++ instead of C -- Recognize files named "MyLockFile" as TOML -- Apply Dockerfile syntax to any file starting with "Dockerfile" - -You can use glob patterns for more flexible matching, allowing you to handle complex naming conventions in your projects. - -## Working with Language Servers - -Language servers are a crucial part of Zed's intelligent coding features, providing capabilities like auto-completion, go-to-definition, and real-time error checking. - -### What are Language Servers? - -Language servers implement the Language Server Protocol (LSP), which standardizes communication between the editor and language-specific tools. This allows Zed to support advanced features for multiple programming languages without implementing each feature separately. - -Some key features provided by language servers include: - -- Code completion -- Error checking and diagnostics -- Code navigation (go to definition, find references) -- Code actions (Rename, extract method) -- Hover information -- Workspace symbol search - -### Managing Language Servers - -Zed simplifies language server management for users: - -1. Automatic Download: When you open a file with a matching file type, Zed automatically downloads the appropriate language server. Zed may prompt you to install an extension for known file types. - -2. Storage Location: - - - macOS: `~/Library/Application Support/Zed/languages` - - Linux: `$XDG_DATA_HOME/zed/languages`, `$FLATPAK_XDG_DATA_HOME/zed/languages`, or `$HOME/.local/share/zed/languages` - -3. Automatic Updates: Zed keeps your language servers up-to-date, ensuring you always have the latest features and improvements. - -### Choosing Language Servers - -Some languages in Zed offer multiple language server options. You might have multiple extensions installed that bundle language servers targeting the same language, potentially leading to overlapping capabilities. To ensure you get the functionality you prefer, Zed allows you to prioritize which language servers are used and in what order. - -You can specify your preference using the `language_servers` setting: - -```json [settings] - "languages": { - "PHP": { - "language_servers": ["intelephense", "!phpactor", "!phptools", "..."] - } - } -``` - -In this example: - -- `intelephense` is set as the primary language server -- `phpactor` is disabled (note the `!` prefix) -- `...` expands to the rest of the language servers that are registered for PHP - -This configuration allows you to tailor the language server setup to your specific needs, ensuring that you get the most suitable functionality for your development workflow. - -### Toolchains - -Some language servers need to be configured with a current "toolchain", which is an installation of a specific version of a programming language compiler or/and interpreter, which can possibly include a full set of dependencies of a project. -An example of what Zed considers a toolchain is a virtual environment in Python. -Not all languages in Zed support toolchain discovery and selection, but for those that do, you can specify the toolchain from a toolchain picker (via {#action toolchain::Select}). To learn more about toolchains in Zed, see [`toolchains`](./toolchains.md). - -### Configuring Language Servers - -Many language servers accept custom configuration options. You can set these in the `lsp` section of your `settings.json`: - -```json [settings] - "lsp": { - "rust-analyzer": { - "initialization_options": { - "check": { - "command": "clippy" - } - } - } - } -``` - -This example configures the Rust Analyzer to use Clippy for additional linting when saving files. - -#### Nested objects - -When configuring language server options in Zed, it's important to use nested objects rather than dot-delimited strings. This is particularly relevant when working with more complex configurations. Let's look at a real-world example using the TypeScript language server: - -Suppose you want to configure the following settings for TypeScript: - -- Enable strict null checks -- Set the target ECMAScript version to ES2020 - -Here's how you would structure these settings in Zed's `settings.json`: - -```json [settings] -"lsp": { - "typescript-language-server": { - "initialization_options": { - // These are not supported (VSCode dotted style): - // "preferences.strictNullChecks": true, - // "preferences.target": "ES2020" - // - // These is correct (nested notation): - "preferences": { - "strictNullChecks": true, - "target": "ES2020" - }, - } - } -} -``` - -#### Possible configuration options - -Depending on how a particular language server is implemented, they may depend on different configuration options, both specified in the LSP. - -- [initializationOptions](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#version_3_17_0) - -Sent once during language server startup, requires server's restart to reapply changes. - -For example, rust-analyzer and clangd rely on this way of configuring only. - -```json [settings] - "lsp": { - "rust-analyzer": { - "initialization_options": { - "checkOnSave": false - } - } - } -``` - -- [Configuration Request](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_configuration) - -May be queried by the server multiple times. -Most of the servers would rely on this way of configuring only. - -```json [settings] -"lsp": { - "tailwindcss-language-server": { - "settings": { - "tailwindCSS": { - "emmetCompletions": true, - }, - } - } -} -``` - -Apart of the LSP-related server configuration options, certain servers in Zed allow configuring the way binary is launched by Zed. - -Language servers are automatically downloaded or launched if found in your path, if you wish to specify an explicit alternate binary you can specify that in settings: - -```json [settings] - "lsp": { - "rust-analyzer": { - "binary": { - // Whether to fetch the binary from the internet, or attempt to find locally. - "ignore_system_version": false, - "path": "/path/to/langserver/bin", - "arguments": ["--option", "value"], - "env": { - "FOO": "BAR" - } - } - } - } -``` - -### Enabling or Disabling Language Servers - -You can toggle language server support globally or per-language: - -```json [settings] - "languages": { - "Markdown": { - "enable_language_server": false - } - } -``` - -This disables the language server for Markdown files, which can be useful for performance in large documentation projects. You can configure this globally in your `~/.config/zed/settings.json` or inside a `.zed/settings.json` in your project directory. - -## Formatting and Linting - -Zed provides support for code formatting and linting to maintain consistent code style and catch potential issues early. - -### Configuring Formatters - -Zed supports both built-in and external formatters. See [`formatter`](./configuring-zed.md#formatter) docs for more. You can configure formatters globally or per-language in your `settings.json`: - -```json [settings] -"languages": { - "JavaScript": { - "formatter": { - "external": { - "command": "prettier", - "arguments": ["--stdin-filepath", "{buffer_path}"] - } - }, - "format_on_save": "on" - }, - "Rust": { - "formatter": "language_server", - "format_on_save": "on" - } -} -``` - -This example uses Prettier for JavaScript and the language server's formatter for Rust, both set to format on save. - -To disable formatting for a specific language: - -```json [settings] -"languages": { - "Markdown": { - "format_on_save": "off" - } -} -``` - -### Setting Up Linters - -Linting in Zed is typically handled by language servers. Many language servers allow you to configure linting rules: - -```json [settings] -"lsp": { - "eslint": { - "settings": { - "codeActionOnSave": { - "rules": ["import/order"] - } - } - } -} -``` - -This configuration sets up ESLint to organize imports on save for JavaScript files. - -To run linter fixes automatically on save: - -```json [settings] -"languages": { - "JavaScript": { - "formatter": { - "code_action": "source.fixAll.eslint" - } - } -} -``` - -### Integrating Formatting and Linting - -Zed allows you to run both formatting and linting on save. Here's an example that uses Prettier for formatting and ESLint for linting JavaScript files: - -```json [settings] -"languages": { - "JavaScript": { - "formatter": [ - { - "code_action": "source.fixAll.eslint" - }, - { - "external": { - "command": "prettier", - "arguments": ["--stdin-filepath", "{buffer_path}"] - } - } - ], - "format_on_save": "on" - } -} -``` - -### Troubleshooting - -If you encounter issues with formatting or linting: - -1. Check Zed's log file for error messages (Use the command palette: `zed: open log`) -2. Ensure external tools (formatters, linters) are correctly installed and in your PATH -3. Verify configurations in both Zed settings and language-specific config files (e.g., `.eslintrc`, `.prettierrc`) - -## Syntax Highlighting and Themes - -Zed offers customization options for syntax highlighting and themes, allowing you to tailor the visual appearance of your code. - -### Customizing Syntax Highlighting - -Zed uses Tree-sitter grammars for syntax highlighting. Override the default highlighting using the `theme_overrides` setting. - -This example makes comments italic and changes the color of strings: - -```json [settings] -"theme_overrides": { - "One Dark": { - "syntax": { - "comment": { - "font_style": "italic" - }, - "string": { - "color": "#00AA00" - } - } - } -} -``` - -### Selecting and Customizing Themes - -Change your theme: - -1. Use the theme selector ({#kb theme_selector::Toggle}) -2. Or set it in your `settings.json`: - -```json [settings] -"theme": { - "mode": "dark", - "dark": "One Dark", - "light": "GitHub Light" -} -``` - -Create custom themes by creating a JSON file in `~/.config/zed/themes/`. Zed will automatically detect and make available any themes in this directory. - -### Using Theme Extensions - -Zed supports theme extensions. Browse and install theme extensions from the Extensions panel ({#kb zed::Extensions}). - -To create your own theme extension, refer to the [Developing Theme Extensions](./extensions/themes.md) guide. - -## Using Language Server Features - -### Inlay Hints - -Inlay hints provide additional information inline in your code, such as parameter names or inferred types. Configure inlay hints in your `settings.json`: - -```json [settings] -"inlay_hints": { - "enabled": true, - "show_type_hints": true, - "show_parameter_hints": true, - "show_other_hints": true -} -``` - -For language-specific inlay hint settings, refer to the documentation for each language. - -### Code Actions - -Code actions provide quick fixes and refactoring options. Access code actions using the `editor: Toggle Code Actions` command or by clicking the lightbulb icon that appears next to your cursor when actions are available. - -### Go To Definition and References - -Use these commands to navigate your codebase: - -- `editor: Go to Definition` (f12|f12) -- `editor: Go to Type Definition` (cmd-f12|ctrl-f12) -- `editor: Find All References` (shift-f12|shift-f12) - -### Rename Symbol - -To rename a symbol across your project: - -1. Place your cursor on the symbol -2. Use the `editor: Rename Symbol` command (f2|f2) -3. Enter the new name and press Enter - -These features depend on the capabilities of the language server for each language. - -When renaming a symbol that spans multiple files, Zed will open a preview in a multibuffer. This allows you to review all the changes across your project before applying them. To confirm the rename, simply save the multibuffer. If you decide not to proceed with the rename, you can undo the changes or close the multibuffer without saving. - -### Hover Information - -Use the `editor: Hover` command to display information about the symbol under the cursor. This often includes type information, documentation, and links to relevant resources. - -### Workspace Symbol Search - -The `workspace: Open Symbol` command allows you to search for symbols (functions, classes, variables) across your entire project. This is useful for quickly navigating large codebases. - -### Code Completion - -Zed provides intelligent code completion suggestions as you type. You can manually trigger completion with the `editor: Show Completions` command. Use tab|tab or enter|enter to accept suggestions. - -### Diagnostics - -Language servers provide real-time diagnostics (errors, warnings, hints) as you code. View all diagnostics for your project using the `diagnostics: Toggle` command. diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md deleted file mode 100644 index 477885a453..0000000000 --- a/docs/src/configuring-zed.md +++ /dev/null @@ -1,4996 +0,0 @@ -# Configuring Zed - -Zed is designed to be configured: we want to fit your workflow and preferences exactly. We provide default settings that are designed to be a comfortable starting point for as many people as possible, but we hope you will enjoy tweaking it to make it feel incredible. - -In addition to the settings described here, you may also want to change your [theme](./themes.md), configure your [key bindings](./key-bindings.md), set up [tasks](./tasks.md) or install [extensions](https://github.com/zed-industries/extensions). - -## Settings Editor - -You can browse through many of the supported settings via the Settings Editor, which can be opened with the {#kb zed::OpenSettings} keybinding, or through the `zed: open settings` action in the command palette. Through it, you can customize your local, user settings as well as project settings. - -> Note that not all settings that Zed supports are available through the Settings Editor yet. -> Some more intricate ones, such as language formatters, can only be changed through the JSON settings file {#kb zed::OpenSettingsFile}. - -## User Settings File - - - -Your settings JSON file can be opened with {#kb zed::OpenSettingsFile}. -By default it is located at `~/.config/zed/settings.json`, though if you have `XDG_CONFIG_HOME` in your environment on Linux it will be at `$XDG_CONFIG_HOME/zed/settings.json` instead. - -Whatever you have added to your user settings file gets merged with any local configuration inside your projects. - -### Default Settings - -In the Settings Editor, the values you see set are the default ones. -You can also verify them in JSON by running {#action zed::OpenDefaultSettings} from the command palette. - -Extensions that provide language servers may also provide default settings for those language servers. - -## Project Settings File - -Similarly to user files, you can open your project settings file by running {#action zed::OpenProjectSettings} from the command palette. -This will create a `.zed` directory containing`.zed/settings.json`. - -Although most projects will only need one settings file at the root, you can add more local settings files for subdirectories as needed. -Not all settings can be set in local files, just those that impact the behavior of the editor and language tooling. -For example you can set `tab_size`, `formatter` etc. but not `theme`, `vim_mode` and similar. - -The syntax for configuration files is a super-set of JSON that allows `//` comments. - -## Per-release Channel Overrides - -Zed reads the same `settings.json` across all release channels (Stable, Preview or Nightly). -However, you can scope overrides to a specific channel by adding top-level `stable`, `preview`, `nightly` or `dev` objects. -They are merged into the base configuration with settings from these keys taking precedence upon launching the specified build. For example: - -```json [settings] -{ - "theme": "sunset", - "vim_mode": false, - "nightly": { - "theme": "cave-light", - "vim_mode": true - }, - "preview": { - "theme": "zed-dark" - } -} -``` - -With this configuration, Stable keeps all base preferences, Preview switches to `zed-dark`, and Nightly enables Vim mode with a different theme. - -Changing settings in the Settings Editorwill always apply the change across all channels. - -# Settings - -Find below an extensive run-through of many supported settings by Zed. - -## Active Pane Modifiers - -- Description: Styling settings applied to the active pane. -- Setting: `active_pane_modifiers` -- Default: - -```json [settings] -{ - "active_pane_modifiers": { - "border_size": 0.0, - "inactive_opacity": 1.0 - } -} -``` - -### Border size - -- Description: Size of the border surrounding the active pane. When set to 0, the active pane doesn't have any border. The border is drawn inset. -- Setting: `border_size` -- Default: `0.0` - -**Options** - -Non-negative `float` values - -### Inactive Opacity - -- Description: Opacity of inactive panels. When set to 1.0, the inactive panes have the same opacity as the active one. If set to 0, the inactive panes content will not be visible at all. Values are clamped to the [0.0, 1.0] range. -- Setting: `inactive_opacity` -- Default: `1.0` - -**Options** - -`float` values - -## Bottom Dock Layout - -- Description: Control the layout of the bottom dock, relative to the left and right docks. -- Setting: `bottom_dock_layout` -- Default: `"contained"` - -**Options** - -1. Contain the bottom dock, giving the full height of the window to the left and right docks. - -```json [settings] -{ - "bottom_dock_layout": "contained" -} -``` - -2. Give the bottom dock the full width of the window, truncating the left and right docks. - -```json [settings] -{ - "bottom_dock_layout": "full" -} -``` - -3. Left align the bottom dock, truncating the left dock and giving the right dock the full height of the window. - -```json [settings] -{ - "bottom_dock_layout": "left_aligned" -} -``` - -4. Right align the bottom dock, giving the left dock the full height of the window and truncating the right dock. - -```json [settings] -{ - "bottom_dock_layout": "right_aligned" -} -``` - -## Agent Font Size - -- Description: The font size for text in the agent panel. Inherits the UI font size if unset. -- Setting: `agent_font_size` -- Default: `null` - -**Options** - -`integer` values from `6` to `100` pixels (inclusive) - -## Allow Rewrap - -- Description: Controls where the {#action editor::Rewrap} action is allowed in the current language scope -- Setting: `allow_rewrap` -- Default: `"in_comments"` - -**Options** - -1. Allow rewrap in comments only: - -```json [settings] -{ - "allow_rewrap": "in_comments" -} -``` - -2. Allow rewrap in selections only: - -```json [settings] -{ - "allow_rewrap": "in_selections" -} -``` - -3. Allow rewrap anywhere: - -```json [settings] -{ - "allow_rewrap": "anywhere" -} -``` - -Note: This setting has no effect in Vim mode, as rewrap is already allowed everywhere. - -## Auto Indent - -- Description: Whether indentation should be adjusted based on the context whilst typing. This can be specified on a per-language basis. -- Setting: `auto_indent` -- Default: `true` - -**Options** - -`boolean` values - -## Auto Indent On Paste - -- Description: Whether indentation of pasted content should be adjusted based on the context -- Setting: `auto_indent_on_paste` -- Default: `true` - -**Options** - -`boolean` values - -## Auto Install extensions - -- Description: Define extensions to be autoinstalled or never be installed. -- Setting: `auto_install_extensions` -- Default: `{ "html": true }` - -**Options** - -You can find the names of your currently installed extensions by listing the subfolders under the [extension installation location](./extensions/installing-extensions.md#installation-location): - -On macOS: - -```sh -ls ~/Library/Application\ Support/Zed/extensions/installed/ -``` - -On Linux: - -```sh -ls ~/.local/share/zed/extensions/installed -``` - -Define extensions which should be installed (`true`) or never installed (`false`). - -```json [settings] -{ - "auto_install_extensions": { - "html": true, - "dockerfile": true, - "docker-compose": false - } -} -``` - -## Autosave - -- Description: When to automatically save edited buffers. -- Setting: `autosave` -- Default: `off` - -**Options** - -1. To disable autosave, set it to `off`: - -```json [settings] -{ - "autosave": "off" -} -``` - -2. To autosave when focus changes, use `on_focus_change`: - -```json [settings] -{ - "autosave": "on_focus_change" -} -``` - -3. To autosave when the active window changes, use `on_window_change`: - -```json [settings] -{ - "autosave": "on_window_change" -} -``` - -4. To autosave after an inactivity period, use `after_delay`: - -```json [settings] -{ - "autosave": { - "after_delay": { - "milliseconds": 1000 - } - } -} -``` - -Note that a save will be triggered when an unsaved tab is closed, even if this is earlier than the configured inactivity period. - -## Autoscroll on Clicks - -- Description: Whether to scroll when clicking near the edge of the visible text area. -- Setting: `autoscroll_on_clicks` -- Default: `false` - -**Options** - -`boolean` values - -## Auto Signature Help - -- Description: Show method signatures in the editor, when inside parentheses -- Setting: `auto_signature_help` -- Default: `false` - -**Options** - -`boolean` values - -### Show Signature Help After Edits - -- Description: Whether to show the signature help after completion or a bracket pair inserted. If `auto_signature_help` is enabled, this setting will be treated as enabled also. -- Setting: `show_signature_help_after_edits` -- Default: `false` - -**Options** - -`boolean` values - -## Auto Update - -- Description: Whether or not to automatically check for updates. -- Setting: `auto_update` -- Default: `true` - -**Options** - -`boolean` values - -## Base Keymap - -- Description: Base key bindings scheme. Base keymaps can be overridden with user keymaps. -- Setting: `base_keymap` -- Default: `VSCode` - -**Options** - -1. VS Code - -```json [settings] -{ - "base_keymap": "VSCode" -} -``` - -2. Atom - -```json [settings] -{ - "base_keymap": "Atom" -} -``` - -3. JetBrains - -```json [settings] -{ - "base_keymap": "JetBrains" -} -``` - -4. None - -```json [settings] -{ - "base_keymap": "None" -} -``` - -5. Sublime Text - -```json [settings] -{ - "base_keymap": "SublimeText" -} -``` - -6. TextMate - -```json [settings] -{ - "base_keymap": "TextMate" -} -``` - -## Buffer Font Family - -- Description: The name of a font to use for rendering text in the editor. -- Setting: `buffer_font_family` -- Default: `.ZedMono`. This currently aliases to [Lilex](https://lilex.myrt.co). - -**Options** - -The name of any font family installed on the user's system, or `".ZedMono"`. - -## Buffer Font Features - -- Description: The OpenType features to enable for text in the editor. -- Setting: `buffer_font_features` -- Default: `null` -- Platform: macOS and Windows. - -**Options** - -Zed supports all OpenType features that can be enabled or disabled for a given buffer or terminal font, as well as setting values for font features. - -For example, to disable font ligatures, add the following to your settings: - -```json [settings] -{ - "buffer_font_features": { - "calt": false - } -} -``` - -You can also set other OpenType features, like setting `cv01` to `7`: - -```json [settings] -{ - "buffer_font_features": { - "cv01": 7 - } -} -``` - -## Buffer Font Fallbacks - -- Description: Set the buffer text's font fallbacks, this will be merged with the platform's default fallbacks. -- Setting: `buffer_font_fallbacks` -- Default: `null` -- Platform: macOS and Windows. - -**Options** - -For example, to use `Nerd Font` as a fallback, add the following to your settings: - -```json [settings] -{ - "buffer_font_fallbacks": ["Nerd Font"] -} -``` - -## Buffer Font Size - -- Description: The default font size for text in the editor. -- Setting: `buffer_font_size` -- Default: `15` - -**Options** - -A font size from `6` to `100` pixels (inclusive) - -## Buffer Font Weight - -- Description: The default font weight for text in the editor. -- Setting: `buffer_font_weight` -- Default: `400` - -**Options** - -`integer` values between `100` and `900` - -## Buffer Line Height - -- Description: The default line height for text in the editor. -- Setting: `buffer_line_height` -- Default: `"comfortable"` - -**Options** - -`"standard"`, `"comfortable"` or `{ "custom": float }` (`1` is compact, `2` is loose) - -## Centered Layout - -- Description: Configuration for the centered layout mode. -- Setting: `centered_layout` -- Default: - -```json [settings] -"centered_layout": { - "left_padding": 0.2, - "right_padding": 0.2, -} -``` - -**Options** - -The `left_padding` and `right_padding` options define the relative width of the -left and right padding of the central pane from the workspace when the centered layout mode is activated. Valid values range is from `0` to `0.4`. - -## Close on File Delete - -- Description: Whether to automatically close editor tabs when their corresponding files are deleted from disk. -- Setting: `close_on_file_delete` -- Default: `false` - -**Options** - -`boolean` values - -When enabled, this setting will automatically close tabs for files that have been deleted from the file system. This is particularly useful for workflows involving temporary or scratch files that are frequently created and deleted. When disabled (default), deleted files remain open with a strikethrough through their tab title. - -Note: Dirty files (files with unsaved changes) will not be automatically closed even when this setting is enabled, ensuring you don't lose unsaved work. - -## Confirm Quit - -- Description: Whether or not to prompt the user to confirm before closing the application. -- Setting: `confirm_quit` -- Default: `false` - -**Options** - -`boolean` values - -## Diagnostics Max Severity - -- Description: Which level to use to filter out diagnostics displayed in the editor -- Setting: `diagnostics_max_severity` -- Default: `null` - -**Options** - -1. Allow all diagnostics (default): - -```json [settings] -{ - "diagnostics_max_severity": "all" -} -``` - -2. Show only errors: - -```json [settings] -{ - "diagnostics_max_severity": "error" -} -``` - -3. Show errors and warnings: - -```json [settings] -{ - "diagnostics_max_severity": "warning" -} -``` - -4. Show errors, warnings, and information: - -```json [settings] -{ - "diagnostics_max_severity": "info" -} -``` - -5. Show all including hints: - -```json [settings] -{ - "diagnostics_max_severity": "hint" -} -``` - -## Disable AI - -- Description: Whether to disable all AI features in Zed -- Setting: `disable_ai` -- Default: `false` - -**Options** - -`boolean` values - -## Direnv Integration - -- Description: Settings for [direnv](https://direnv.net/) integration. Requires `direnv` to be installed. - `direnv` integration make it possible to use the environment variables set by a `direnv` configuration to detect some language servers in `$PATH` instead of installing them. - It also allows for those environment variables to be used in tasks. -- Setting: `load_direnv` -- Default: `"direct"` - -**Options** - -There are three options to choose from: - -1. `shell_hook`: Use the shell hook to load direnv. This relies on direnv to activate upon entering the directory. Supports POSIX shells and fish. -2. `direct`: Use `direnv export json` to load direnv. This will load direnv directly without relying on the shell hook and might cause some inconsistencies. This allows direnv to work with any shell. -3. `disabled`: No shell environment will be loaded automatically; direnv must be invoked manually (e.g. with `direnv exec`) to be used. - -## Double Click In Multibuffer - -- Description: What to do when multibuffer is double clicked in some of its excerpts (parts of singleton buffers) -- Setting: `double_click_in_multibuffer` -- Default: `"select"` - -**Options** - -1. Behave as a regular buffer and select the whole word (default): - -```json [settings] -{ - "double_click_in_multibuffer": "select" -} -``` - -2. Open the excerpt clicked as a new buffer in the new tab: - -```json [settings] -{ - "double_click_in_multibuffer": "open" -} -``` - -For the case of "open", regular selection behavior can be achieved by holding `alt` when double clicking. - -## Drop Target Size - -- Description: Relative size of the drop target in the editor that will open dropped file as a split pane (0-0.5). For example, 0.25 means if you drop onto the top/bottom quarter of the pane a new vertical split will be used, if you drop onto the left/right quarter of the pane a new horizontal split will be used. -- Setting: `drop_target_size` -- Default: `0.2` - -**Options** - -`float` values between `0` and `0.5` - -## Edit Predictions - -- Description: Settings for edit predictions. -- Setting: `edit_predictions` -- Default: - -```json [settings] - "edit_predictions": { - "disabled_globs": [ - "**/.env*", - "**/*.pem", - "**/*.key", - "**/*.cert", - "**/*.crt", - "**/.dev.vars", - "**/secrets.yml" - ] - } -``` - -**Options** - -### Disabled Globs - -- Description: A list of globs for which edit predictions should be disabled for. This list adds to a pre-existing, sensible default set of globs. Any additional ones you add are combined with them. -- Setting: `disabled_globs` -- Default: `["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/.dev.vars", "**/secrets.yml"]` - -**Options** - -List of `string` values. - -## Edit Predictions Disabled in - -- Description: A list of language scopes in which edit predictions should be disabled. -- Setting: `edit_predictions_disabled_in` -- Default: `[]` - -**Options** - -List of `string` values - -1. Don't show edit predictions in comments: - -```json [settings] -"disabled_in": ["comment"] -``` - -2. Don't show edit predictions in strings and comments: - -```json [settings] -"disabled_in": ["comment", "string"] -``` - -3. Only in Go, don't show edit predictions in strings and comments: - -```json [settings] -{ - "languages": { - "Go": { - "edit_predictions_disabled_in": ["comment", "string"] - } - } -} -``` - -## Current Line Highlight - -- Description: How to highlight the current line in the editor. -- Setting: `current_line_highlight` -- Default: `all` - -**Options** - -1. Don't highlight the current line: - -```json [settings] -"current_line_highlight": "none" -``` - -2. Highlight the gutter area: - -```json [settings] -"current_line_highlight": "gutter" -``` - -3. Highlight the editor area: - -```json [settings] -"current_line_highlight": "line" -``` - -4. Highlight the full line: - -```json [settings] -"current_line_highlight": "all" -``` - -## Selection Highlight - -- Description: Whether to highlight all occurrences of the selected text in an editor. -- Setting: `selection_highlight` -- Default: `true` - -## Rounded Selection - -- Description: Whether the text selection should have rounded corners. -- Setting: `rounded_selection` -- Default: `true` - -## Cursor Blink - -- Description: Whether or not the cursor blinks. -- Setting: `cursor_blink` -- Default: `true` - -**Options** - -`boolean` values - -## Cursor Shape - -- Description: Cursor shape for the default editor. -- Setting: `cursor_shape` -- Default: `bar` - -**Options** - -1. A vertical bar: - -```json [settings] -"cursor_shape": "bar" -``` - -2. A block that surrounds the following character: - -```json [settings] -"cursor_shape": "block" -``` - -3. An underline / underscore that runs along the following character: - -```json [settings] -"cursor_shape": "underline" -``` - -4. An box drawn around the following character: - -```json [settings] -"cursor_shape": "hollow" -``` - -## Gutter - -- Description: Settings for the editor gutter -- Setting: `gutter` -- Default: - -```json [settings] -{ - "gutter": { - "line_numbers": true, - "runnables": true, - "breakpoints": true, - "folds": true, - "min_line_number_digits": 4 - } -} -``` - -**Options** - -- `line_numbers`: Whether to show line numbers in the gutter -- `runnables`: Whether to show runnable buttons in the gutter -- `breakpoints`: Whether to show breakpoints in the gutter -- `folds`: Whether to show fold buttons in the gutter -- `min_line_number_digits`: Minimum number of characters to reserve space for in the gutter - -## Hide Mouse - -- Description: Determines when the mouse cursor should be hidden in an editor or input box. -- Setting: `hide_mouse` -- Default: `on_typing_and_movement` - -**Options** - -1. Never hide the mouse cursor: - -```json [settings] -"hide_mouse": "never" -``` - -2. Hide only when typing: - -```json [settings] -"hide_mouse": "on_typing" -``` - -3. Hide on both typing and cursor movement: - -```json [settings] -"hide_mouse": "on_typing_and_movement" -``` - -## Snippet Sort Order - -- Description: Determines how snippets are sorted relative to other completion items. -- Setting: `snippet_sort_order` -- Default: `inline` - -**Options** - -1. Place snippets at the top of the completion list: - -```json [settings] -"snippet_sort_order": "top" -``` - -2. Place snippets normally without any preference: - -```json [settings] -"snippet_sort_order": "inline" -``` - -3. Place snippets at the bottom of the completion list: - -```json [settings] -"snippet_sort_order": "bottom" -``` - -4. Do not show snippets in the completion list at all: - -```json [settings] -"snippet_sort_order": "none" -``` - -## Editor Scrollbar - -- Description: Whether or not to show the editor scrollbar and various elements in it. -- Setting: `scrollbar` -- Default: - -```json [settings] -"scrollbar": { - "show": "auto", - "cursors": true, - "git_diff": true, - "search_results": true, - "selected_text": true, - "selected_symbol": true, - "diagnostics": "all", - "axes": { - "horizontal": true, - "vertical": true, - }, -}, -``` - -### Show Mode - -- Description: When to show the editor scrollbar. -- Setting: `show` -- Default: `auto` - -**Options** - -1. Show the scrollbar if there's important information or follow the system's configured behavior: - -```json [settings] -"scrollbar": { - "show": "auto" -} -``` - -2. Match the system's configured behavior: - -```json [settings] -"scrollbar": { - "show": "system" -} -``` - -3. Always show the scrollbar: - -```json [settings] -"scrollbar": { - "show": "always" -} -``` - -4. Never show the scrollbar: - -```json [settings] -"scrollbar": { - "show": "never" -} -``` - -### Cursor Indicators - -- Description: Whether to show cursor positions in the scrollbar. -- Setting: `cursors` -- Default: `true` - -Cursor indicators appear as small marks on the scrollbar showing where other collaborators' cursors are positioned in the file. - -**Options** - -`boolean` values - -### Git Diff Indicators - -- Description: Whether to show git diff indicators in the scrollbar. -- Setting: `git_diff` -- Default: `true` - -Git diff indicators appear as colored marks showing lines that have been added, modified, or deleted compared to the git HEAD. - -**Options** - -`boolean` values - -### Search Results Indicators - -- Description: Whether to show buffer search results in the scrollbar. -- Setting: `search_results` -- Default: `true` - -Search result indicators appear as marks showing all locations in the file where your current search query matches. - -**Options** - -`boolean` values - -### Selected Text Indicators - -- Description: Whether to show selected text occurrences in the scrollbar. -- Setting: `selected_text` -- Default: `true` - -Selected text indicators appear as marks showing all occurrences of the currently selected text throughout the file. - -**Options** - -`boolean` values - -### Selected Symbols Indicators - -- Description: Whether to show selected symbol occurrences in the scrollbar. -- Setting: `selected_symbol` -- Default: `true` - -Selected symbol indicators appear as marks showing all occurrences of the currently selected symbol (like a function or variable name) throughout the file. - -**Options** - -`boolean` values - -### Diagnostics - -- Description: Which diagnostic indicators to show in the scrollbar. -- Setting: `diagnostics` -- Default: `all` - -Diagnostic indicators appear as colored marks showing errors, warnings, and other language server diagnostics at their corresponding line positions in the file. - -**Options** - -1. Show all diagnostics: - -```json [settings] -{ - "show_diagnostics": "all" -} -``` - -2. Do not show any diagnostics: - -```json [settings] -{ - "show_diagnostics": "off" -} -``` - -3. Show only errors: - -```json [settings] -{ - "show_diagnostics": "error" -} -``` - -4. Show only errors and warnings: - -```json [settings] -{ - "show_diagnostics": "warning" -} -``` - -5. Show only errors, warnings, and information: - -```json [settings] -{ - "show_diagnostics": "info" -} -``` - -### Axes - -- Description: Forcefully enable or disable the scrollbar for each axis -- Setting: `axes` -- Default: - -```json [settings] -"scrollbar": { - "axes": { - "horizontal": true, - "vertical": true, - }, -} -``` - -#### Horizontal - -- Description: When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings. -- Setting: `horizontal` -- Default: `true` - -**Options** - -`boolean` values - -#### Vertical - -- Description: When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings. -- Setting: `vertical` -- Default: `true` - -**Options** - -`boolean` values - -## Minimap - -- Description: Settings related to the editor's minimap, which provides an overview of your document. -- Setting: `minimap` -- Default: - -```json [settings] -{ - "minimap": { - "show": "never", - "thumb": "always", - "thumb_border": "left_open", - "current_line_highlight": null - } -} -``` - -### Show Mode - -- Description: When to show the minimap in the editor. -- Setting: `show` -- Default: `never` - -**Options** - -1. Always show the minimap: - -```json [settings] -{ - "show": "always" -} -``` - -2. Show the minimap if the editor's scrollbars are visible: - -```json [settings] -{ - "show": "auto" -} -``` - -3. Never show the minimap: - -```json [settings] -{ - "show": "never" -} -``` - -### Thumb Display - -- Description: When to show the minimap thumb (the visible editor area) in the minimap. -- Setting: `thumb` -- Default: `always` - -**Options** - -1. Show the minimap thumb when hovering over the minimap: - -```json [settings] -{ - "thumb": "hover" -} -``` - -2. Always show the minimap thumb: - -```json [settings] -{ - "thumb": "always" -} -``` - -### Thumb Border - -- Description: How the minimap thumb border should look. -- Setting: `thumb_border` -- Default: `left_open` - -**Options** - -1. Display a border on all sides of the thumb: - -```json [settings] -{ - "thumb_border": "full" -} -``` - -2. Display a border on all sides except the left side: - -```json [settings] -{ - "thumb_border": "left_open" -} -``` - -3. Display a border on all sides except the right side: - -```json [settings] -{ - "thumb_border": "right_open" -} -``` - -4. Display a border only on the left side: - -```json [settings] -{ - "thumb_border": "left_only" -} -``` - -5. Display the thumb without any border: - -```json [settings] -{ - "thumb_border": "none" -} -``` - -### Current Line Highlight - -- Description: How to highlight the current line in the minimap. -- Setting: `current_line_highlight` -- Default: `null` - -**Options** - -1. Inherit the editor's current line highlight setting: - -```json [settings] -{ - "minimap": { - "current_line_highlight": null - } -} -``` - -2. Highlight the current line in the minimap: - -```json [settings] -{ - "minimap": { - "current_line_highlight": "line" - } -} -``` - -or - -```json [settings] -{ - "minimap": { - "current_line_highlight": "all" - } -} -``` - -3. Do not highlight the current line in the minimap: - -```json [settings] -{ - "minimap": { - "current_line_highlight": "gutter" - } -} -``` - -or - -```json [settings] -{ - "minimap": { - "current_line_highlight": "none" - } -} -``` - -## Editor Tab Bar - -- Description: Settings related to the editor's tab bar. -- Settings: `tab_bar` -- Default: - -```json [settings] -"tab_bar": { - "show": true, - "show_nav_history_buttons": true, - "show_tab_bar_buttons": true -} -``` - -### Show - -- Description: Whether or not to show the tab bar in the editor. -- Setting: `show` -- Default: `true` - -**Options** - -`boolean` values - -### Navigation History Buttons - -- Description: Whether or not to show the navigation history buttons. -- Setting: `show_nav_history_buttons` -- Default: `true` - -**Options** - -`boolean` values - -### Tab Bar Buttons - -- Description: Whether or not to show the tab bar buttons. -- Setting: `show_tab_bar_buttons` -- Default: `true` - -**Options** - -`boolean` values - -## Editor Tabs - -- Description: Configuration for the editor tabs. -- Setting: `tabs` -- Default: - -```json [settings] -"tabs": { - "close_position": "right", - "file_icons": false, - "git_status": false, - "activate_on_close": "history", - "show_close_button": "hover", - "show_diagnostics": "off" -}, -``` - -### Close Position - -- Description: Where to display close button within a tab. -- Setting: `close_position` -- Default: `right` - -**Options** - -1. Display the close button on the right: - -```json [settings] -{ - "close_position": "right" -} -``` - -2. Display the close button on the left: - -```json [settings] -{ - "close_position": "left" -} -``` - -### File Icons - -- Description: Whether to show the file icon for a tab. -- Setting: `file_icons` -- Default: `false` - -### Git Status - -- Description: Whether or not to show Git file status in tab. -- Setting: `git_status` -- Default: `false` - -### Activate on close - -- Description: What to do after closing the current tab. -- Setting: `activate_on_close` -- Default: `history` - -**Options** - -1. Activate the tab that was open previously: - -```json [settings] -{ - "activate_on_close": "history" -} -``` - -2. Activate the right neighbour tab if present: - -```json [settings] -{ - "activate_on_close": "neighbour" -} -``` - -3. Activate the left neighbour tab if present: - -```json [settings] -{ - "activate_on_close": "left_neighbour" -} -``` - -### Show close button - -- Description: Controls the appearance behavior of the tab's close button. -- Setting: `show_close_button` -- Default: `hover` - -**Options** - -1. Show it just upon hovering the tab: - -```json [settings] -{ - "show_close_button": "hover" -} -``` - -2. Show it persistently: - -```json [settings] -{ - "show_close_button": "always" -} -``` - -3. Never show it, even if hovering it: - -```json [settings] -{ - "show_close_button": "hidden" -} -``` - -### Show Diagnostics - -- Description: Whether to show diagnostics indicators in tabs. This setting only works when file icons are active and controls which files with diagnostic issues to mark. -- Setting: `show_diagnostics` -- Default: `off` - -**Options** - -1. Do not mark any files: - -```json [settings] -{ - "show_diagnostics": "off" -} -``` - -2. Only mark files with errors: - -```json [settings] -{ - "show_diagnostics": "errors" -} -``` - -3. Mark files with errors and warnings: - -```json [settings] -{ - "show_diagnostics": "all" -} -``` - -### Show Inline Code Actions - -- Description: Whether to show code action button at start of buffer line. -- Setting: `inline_code_actions` -- Default: `true` - -**Options** - -`boolean` values - -### Drag And Drop Selection - -- Description: Whether to allow drag and drop text selection in buffer. `delay` is the milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created. -- Setting: `drag_and_drop_selection` -- Default: - -```json [settings] -"drag_and_drop_selection": { - "enabled": true, - "delay": 300 -} -``` - -## Editor Toolbar - -- Description: Whether or not to show various elements in the editor toolbar. -- Setting: `toolbar` -- Default: - -```json [settings] -"toolbar": { - "breadcrumbs": true, - "quick_actions": true, - "selections_menu": true, - "agent_review": true, - "code_actions": false -}, -``` - -**Options** - -Each option controls displaying of a particular toolbar element. If all elements are hidden, the editor toolbar is not displayed. - -## Use System Tabs - -- Description: Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). -- Setting: `use_system_window_tabs` -- Default: `false` - -**Options** - -This setting enables integration with macOS’s native window tabbing feature. When set to `true`, Zed windows can be grouped together as tabs in a single macOS window, following the system-wide tabbing preferences set by the user (such as "Always", "In Full Screen", or "Never"). This setting is only available on macOS. - -## Enable Language Server - -- Description: Whether or not to use language servers to provide code intelligence. -- Setting: `enable_language_server` -- Default: `true` - -**Options** - -`boolean` values - -## Ensure Final Newline On Save - -- Description: Removes any lines containing only whitespace at the end of the file and ensures just one newline at the end. -- Setting: `ensure_final_newline_on_save` -- Default: `true` - -**Options** - -`boolean` values - -## Expand Excerpt Lines - -- Description: The default number of lines to expand excerpts in the multibuffer by -- Setting: `expand_excerpt_lines` -- Default: `5` - -**Options** - -Positive `integer` values - -## Excerpt Context Lines - -- Description: The number of lines of context to provide when showing excerpts in the multibuffer. -- Setting: `excerpt_context_lines` -- Default: `2` - -**Options** - -Positive `integer` value between 1 and 32. Values outside of this range will be clamped to this range. - -## Extend Comment On Newline - -- Description: Whether to start a new line with a comment when a previous line is a comment as well. -- Setting: `extend_comment_on_newline` -- Default: `true` - -**Options** - -`boolean` values - -## Status Bar - -- Description: Control various elements in the status bar. Note that some items in the status bar have their own settings set elsewhere. -- Setting: `status_bar` -- Default: - -```json [settings] -"status_bar": { - "active_language_button": true, - "cursor_position_button": true, - "line_endings_button": false -}, -``` - -There is an experimental setting that completely hides the status bar. This causes major usability problems (you will be unable to use many of Zed's features), but is provided for those who value screen real-estate above all else. - -```json -"status_bar": { - "experimental.show": false -} -``` - -## LSP - -- Description: Configuration for language servers. -- Setting: `lsp` -- Default: `null` - -**Options** - -The following settings can be overridden for specific language servers: - -- `initialization_options` -- `settings` - -To override configuration for a language server, add an entry for that language server's name to the `lsp` value. - -Some options are passed via `initialization_options` to the language server. These are for options which must be specified at language server startup and when changed will require restarting the language server. - -For example to pass the `check` option to `rust-analyzer`, use the following configuration: - -```json [settings] -"lsp": { - "rust-analyzer": { - "initialization_options": { - "check": { - "command": "clippy" // rust-analyzer.check.command (default: "check") - } - } - } -} -``` - -While other options may be changed at a runtime and should be placed under `settings`: - -```json [settings] -"lsp": { - "yaml-language-server": { - "settings": { - "yaml": { - "keyOrdering": true // Enforces alphabetical ordering of keys in maps - } - } - } -} -``` - -## Global LSP Settings - -- Description: Configuration for global LSP settings that apply to all language servers -- Setting: `global_lsp_settings` -- Default: - -```json [settings] -{ - "global_lsp_settings": { - "button": true - } -} -``` - -**Options** - -- `button`: Whether to show the LSP status button in the status bar - -## LSP Highlight Debounce - -- Description: The debounce delay in milliseconds before querying highlights from the language server based on the current cursor location. -- Setting: `lsp_highlight_debounce` -- Default: `75` - -**Options** - -`integer` values representing milliseconds - -## Features - -- Description: Features that can be globally enabled or disabled -- Setting: `features` -- Default: - -```json [settings] -{ - "features": { - "edit_prediction_provider": "zed" - } -} -``` - -### Edit Prediction Provider - -- Description: Which edit prediction provider to use -- Setting: `edit_prediction_provider` -- Default: `"zed"` - -**Options** - -1. Use Zeta as the edit prediction provider: - -```json [settings] -{ - "features": { - "edit_prediction_provider": "zed" - } -} -``` - -2. Use Copilot as the edit prediction provider: - -```json [settings] -{ - "features": { - "edit_prediction_provider": "copilot" - } -} -``` - -3. Use Supermaven as the edit prediction provider: - -```json [settings] -{ - "features": { - "edit_prediction_provider": "supermaven" - } -} -``` - -4. Turn off edit predictions across all providers - -```json [settings] -{ - "features": { - "edit_prediction_provider": "none" - } -} -``` - -## Format On Save - -- Description: Whether or not to perform a buffer format before saving. -- Setting: `format_on_save` -- Default: `on` - -**Options** - -1. `on`, enables format on save obeying `formatter` setting: - -```json [settings] -{ - "format_on_save": "on" -} -``` - -2. `off`, disables format on save: - -```json [settings] -{ - "format_on_save": "off" -} -``` - -## Formatter - -- Description: How to perform a buffer format. -- Setting: `formatter` -- Default: `auto` - -**Options** - -1. To use the current language server, use `"language_server"`: - -```json [settings] -{ - "formatter": "language_server" -} -``` - -2. Or to use an external command, use `"external"`. Specify the name of the formatting program to run, and an array of arguments to pass to the program. The buffer's text will be passed to the program on stdin, and the formatted output should be written to stdout. For example, the following command would strip trailing spaces using [`sed(1)`](https://linux.die.net/man/1/sed): - -```json [settings] -{ - "formatter": { - "external": { - "command": "sed", - "arguments": ["-e", "s/ *$//"] - } - } -} -``` - -3. External formatters may optionally include a `{buffer_path}` placeholder which at runtime will include the path of the buffer being formatted. Formatters operate by receiving file content via standard input, reformatting it and then outputting it to standard output and so normally don't know the filename of what they are formatting. Tools like Prettier support receiving the file path via a command line argument which can then used to impact formatting decisions. - -WARNING: `{buffer_path}` should not be used to direct your formatter to read from a filename. Your formatter should only read from standard input and should not read or write files directly. - -```json [settings] - "formatter": { - "external": { - "command": "prettier", - "arguments": ["--stdin-filepath", "{buffer_path}"] - } - } -``` - -4. Or to use code actions provided by the connected language servers, use `"code_actions"`: - -```json [settings] -{ - "formatter": [ - // Use ESLint's --fix: - { "code_action": "source.fixAll.eslint" }, - // Organize imports on save: - { "code_action": "source.organizeImports" } - ] -} -``` - -5. Or to use multiple formatters consecutively, use an array of formatters: - -```json [settings] -{ - "formatter": [ - { "language_server": { "name": "rust-analyzer" } }, - { - "external": { - "command": "sed", - "arguments": ["-e", "s/ *$//"] - } - } - ] -} -``` - -Here `rust-analyzer` will be used first to format the code, followed by a call of sed. -If any of the formatters fails, the subsequent ones will still be executed. - -## Auto close - -- Description: Whether to automatically add matching closing characters when typing opening parenthesis, bracket, brace, single or double quote characters. -- Setting: `use_autoclose` -- Default: `true` - -**Options** - -`boolean` values - -## Always Treat Brackets As Autoclosed - -- Description: Controls how the editor handles the autoclosed characters. -- Setting: `always_treat_brackets_as_autoclosed` -- Default: `false` - -**Options** - -`boolean` values - -**Example** - -If the setting is set to `true`: - -1. Enter in the editor: `)))` -2. Move the cursor to the start: `^)))` -3. Enter again: `)))` - -The result is still `)))` and not `))))))`, which is what it would be by default. - -## File Scan Exclusions - -- Setting: `file_scan_exclusions` -- Description: Files or globs of files that will be excluded by Zed entirely. They will be skipped during file scans, file searches, and not be displayed in the project file tree. Overrides `file_scan_inclusions`. -- Default: - -```json [settings] -"file_scan_exclusions": [ - "**/.git", - "**/.svn", - "**/.hg", - "**/.jj", - "**/CVS", - "**/.DS_Store", - "**/Thumbs.db", - "**/.classpath", - "**/.settings" -], -``` - -Note, specifying `file_scan_exclusions` in settings.json will override the defaults (shown above). If you are looking to exclude additional items you will need to include all the default values in your settings. - -## File Scan Inclusions - -- Setting: `file_scan_inclusions` -- Description: Files or globs of files that will be included by Zed, even when ignored by git. This is useful for files that are not tracked by git, but are still important to your project. Note that globs that are overly broad can slow down Zed's file scanning. `file_scan_exclusions` takes precedence over these inclusions. -- Default: - -```json [settings] -"file_scan_inclusions": [".env*"], -``` - -## File Types - -- Setting: `file_types` -- Description: Configure how Zed selects a language for a file based on its filename or extension. Supports glob entries. -- Default: - -```json [settings] -"file_types": { - "JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json"], - "Shell Script": [".env.*"] -} -``` - -**Examples** - -To interpret all `.c` files as C++, files called `MyLockFile` as TOML and files starting with `Dockerfile` as Dockerfile: - -```json [settings] -{ - "file_types": { - "C++": ["c"], - "TOML": ["MyLockFile"], - "Dockerfile": ["Dockerfile*"] - } -} -``` - -## Diagnostics - -- Description: Configuration for diagnostics-related features. -- Setting: `diagnostics` -- Default: - -```json [settings] -{ - "diagnostics": { - "include_warnings": true, - "inline": { - "enabled": false - }, - "update_with_cursor": false, - "primary_only": false, - "use_rendered": false - } -} -``` - -### Inline Diagnostics - -- Description: Whether or not to show diagnostics information inline. -- Setting: `inline` -- Default: - -```json [settings] -{ - "diagnostics": { - "inline": { - "enabled": false, - "update_debounce_ms": 150, - "padding": 4, - "min_column": 0, - "max_severity": null - } - } -} -``` - -**Options** - -1. Enable inline diagnostics. - -```json [settings] -{ - "diagnostics": { - "inline": { - "enabled": true - } - } -} -``` - -2. Delay diagnostic updates until some time after the last diagnostic update. - -```json [settings] -{ - "diagnostics": { - "inline": { - "enabled": true, - "update_debounce_ms": 150 - } - } -} -``` - -3. Set padding between the end of the source line and the start of the diagnostic. - -```json [settings] -{ - "diagnostics": { - "inline": { - "enabled": true, - "padding": 4 - } - } -} -``` - -4. Horizontally align inline diagnostics at the given column. - -```json [settings] -{ - "diagnostics": { - "inline": { - "enabled": true, - "min_column": 80 - } - } -} -``` - -5. Show only warning and error diagnostics. - -```json [settings] -{ - "diagnostics": { - "inline": { - "enabled": true, - "max_severity": "warning" - } - } -} -``` - -## Git - -- Description: Configuration for git-related features. -- Setting: `git` -- Default: - -```json [settings] -{ - "git": { - "git_gutter": "tracked_files", - "inline_blame": { - "enabled": true - }, - "branch_picker": { - "show_author_name": true - }, - "hunk_style": "staged_hollow" - } -} -``` - -### Git Gutter - -- Description: Whether or not to show the git gutter. -- Setting: `git_gutter` -- Default: `tracked_files` - -**Options** - -1. Show git gutter in tracked files - -```json [settings] -{ - "git": { - "git_gutter": "tracked_files" - } -} -``` - -2. Hide git gutter - -```json [settings] -{ - "git": { - "git_gutter": "hide" - } -} -``` - -### Gutter Debounce - -- Description: Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter. -- Setting: `gutter_debounce` -- Default: `null` - -**Options** - -`integer` values representing milliseconds - -Example: - -```json [settings] -{ - "git": { - "gutter_debounce": 100 - } -} -``` - -### Inline Git Blame - -- Description: Whether or not to show git blame information inline, on the currently focused line. -- Setting: `inline_blame` -- Default: - -```json [settings] -{ - "git": { - "inline_blame": { - "enabled": true - } - } -} -``` - -**Options** - -1. Disable inline git blame: - -```json [settings] -{ - "git": { - "inline_blame": { - "enabled": false - } - } -} -``` - -2. Only show inline git blame after a delay (that starts after cursor stops moving): - -```json [settings] -{ - "git": { - "inline_blame": { - "delay_ms": 500 - } - } -} -``` - -3. Show a commit summary next to the commit date and author: - -```json [settings] -{ - "git": { - "inline_blame": { - "show_commit_summary": true - } - } -} -``` - -4. Use this as the minimum column at which to display inline blame information: - -```json [settings] -{ - "git": { - "inline_blame": { - "min_column": 80 - } - } -} -``` - -5. Set the padding between the end of the line and the inline blame hint, in ems: - -```json [settings] -{ - "git": { - "inline_blame": { - "padding": 10 - } - } -} -``` - -### Branch Picker - -- Description: Configuration related to the branch picker. -- Setting: `branch_picker` -- Default: - -```json [settings] -{ - "git": { - "branch_picker": { - "show_author_name": false - } - } -} -``` - -**Options** - -1. Show the author name in the branch picker: - -```json [settings] -{ - "git": { - "branch_picker": { - "show_author_name": true - } - } -} -``` - -### Hunk Style - -- Description: What styling we should use for the diff hunks. -- Setting: `hunk_style` -- Default: - -```json [settings] -{ - "git": { - "hunk_style": "staged_hollow" - } -} -``` - -**Options** - -1. Show the staged hunks faded out and with a border: - -```json [settings] -{ - "git": { - "hunk_style": "staged_hollow" - } -} -``` - -2. Show unstaged hunks faded out and with a border: - -```json [settings] -{ - "git": { - "hunk_style": "unstaged_hollow" - } -} -``` - -## Go to Definition Fallback - -- Description: What to do when the {#action editor::GoToDefinition} action fails to find a definition -- Setting: `go_to_definition_fallback` -- Default: `"find_all_references"` - -**Options** - -1. Do nothing: - -```json [settings] -{ - "go_to_definition_fallback": "none" -} -``` - -2. Find references for the same symbol (default): - -```json [settings] -{ - "go_to_definition_fallback": "find_all_references" -} -``` - -## Hard Tabs - -- Description: Whether to indent lines using tab characters or multiple spaces. -- Setting: `hard_tabs` -- Default: `false` - -**Options** - -`boolean` values - -## Helix Mode - -- Description: Whether or not to enable Helix mode. Enabling `helix_mode` also enables `vim_mode`. See the [Helix documentation](./helix.md) for more details. -- Setting: `helix_mode` -- Default: `false` - -**Options** - -`boolean` values - -## Indent Guides - -- Description: Configuration related to indent guides. Indent guides can be configured separately for each language. -- Setting: `indent_guides` -- Default: - -```json [settings] -{ - "indent_guides": { - "enabled": true, - "line_width": 1, - "active_line_width": 1, - "coloring": "fixed", - "background_coloring": "disabled" - } -} -``` - -**Options** - -1. Disable indent guides - -```json [settings] -{ - "indent_guides": { - "enabled": false - } -} -``` - -2. Enable indent guides for a specific language. - -```json [settings] -{ - "languages": { - "Python": { - "indent_guides": { - "enabled": true - } - } - } -} -``` - -3. Enable indent aware coloring ("rainbow indentation"). - The colors that are used for different indentation levels are defined in the theme (theme key: `accents`). They can be customized by using theme overrides. - -```json [settings] -{ - "indent_guides": { - "enabled": true, - "coloring": "indent_aware" - } -} -``` - -4. Enable indent aware background coloring ("rainbow indentation"). - The colors that are used for different indentation levels are defined in the theme (theme key: `accents`). They can be customized by using theme overrides. - -```json [settings] -{ - "indent_guides": { - "enabled": true, - "coloring": "indent_aware", - "background_coloring": "indent_aware" - } -} -``` - -## Hover Popover Enabled - -- Description: Whether or not to show the informational hover box when moving the mouse over symbols in the editor. -- Setting: `hover_popover_enabled` -- Default: `true` - -**Options** - -`boolean` values - -## Hover Popover Delay - -- Description: Time to wait in milliseconds before showing the informational hover box. -- Setting: `hover_popover_delay` -- Default: `300` - -**Options** - -`integer` values representing milliseconds - -## Icon Theme - -- Description: The icon theme setting can be specified in two forms - either as the name of an icon theme or as an object containing the `mode`, `dark`, and `light` icon themes for files/folders inside Zed. -- Setting: `icon_theme` -- Default: `Zed (Default)` - -### Icon Theme Object - -- Description: Specify the icon theme using an object that includes the `mode`, `dark`, and `light`. -- Setting: `icon_theme` -- Default: - -```json [settings] -"icon_theme": { - "mode": "system", - "dark": "Zed (Default)", - "light": "Zed (Default)" -}, -``` - -### Mode - -- Description: Specify the icon theme mode. -- Setting: `mode` -- Default: `system` - -**Options** - -1. Set the icon theme to dark mode - -```json [settings] -{ - "mode": "dark" -} -``` - -2. Set the icon theme to light mode - -```json [settings] -{ - "mode": "light" -} -``` - -3. Set the icon theme to system mode - -```json [settings] -{ - "mode": "system" -} -``` - -### Dark - -- Description: The name of the dark icon theme. -- Setting: `dark` -- Default: `Zed (Default)` - -**Options** - -Run the {#action icon_theme_selector::Toggle} action in the command palette to see a current list of valid icon themes names. - -### Light - -- Description: The name of the light icon theme. -- Setting: `light` -- Default: `Zed (Default)` - -**Options** - -Run the {#action icon_theme_selector::Toggle} action in the command palette to see a current list of valid icon themes names. - -## Image Viewer - -- Description: Settings for image viewer functionality -- Setting: `image_viewer` -- Default: - -```json [settings] -{ - "image_viewer": { - "unit": "binary" - } -} -``` - -**Options** - -### Unit - -- Description: The unit for image file sizes -- Setting: `unit` -- Default: `"binary"` - -**Options** - -1. Use binary units (KiB, MiB): - -```json [settings] -{ - "image_viewer": { - "unit": "binary" - } -} -``` - -2. Use decimal units (KB, MB): - -```json [settings] -{ - "image_viewer": { - "unit": "decimal" - } -} -``` - -## Inlay hints - -- Description: Configuration for displaying extra text with hints in the editor. -- Setting: `inlay_hints` -- Default: - -```json [settings] -"inlay_hints": { - "enabled": false, - "show_type_hints": true, - "show_parameter_hints": true, - "show_other_hints": true, - "show_background": false, - "edit_debounce_ms": 700, - "scroll_debounce_ms": 50, - "toggle_on_modifiers_press": null -} -``` - -**Options** - -Inlay hints querying consists of two parts: editor (client) and LSP server. -With the inlay settings above are changed to enable the hints, editor will start to query certain types of hints and react on LSP hint refresh request from the server. -At this point, the server may or may not return hints depending on its implementation, further configuration might be needed, refer to the corresponding LSP server documentation. - -The following languages have inlay hints preconfigured by Zed: - -- [Go](https://docs.zed.dev/languages/go) -- [Rust](https://docs.zed.dev/languages/rust) -- [Svelte](https://docs.zed.dev/languages/svelte) -- [TypeScript](https://docs.zed.dev/languages/typescript) - -Use the `lsp` section for the server configuration. Examples are provided in the corresponding language documentation. - -Hints are not instantly queried in Zed, two kinds of debounces are used, either may be set to 0 to be disabled. -Settings-related hint updates are not debounced. - -All possible config values for `toggle_on_modifiers_press` are: - -```json [settings] -"inlay_hints": { - "toggle_on_modifiers_press": { - "control": true, - "shift": true, - "alt": true, - "platform": true, - "function": true - } -} -``` - -Unspecified values have a `false` value, hints won't be toggled if all the modifiers are `false` or not all the modifiers are pressed. - -## Journal - -- Description: Configuration for the journal. -- Setting: `journal` -- Default: - -```json [settings] -"journal": { - "path": "~", - "hour_format": "hour12" -} - -``` - -### Path - -- Description: The path of the directory where journal entries are stored. If an invalid path is specified, the journal will fall back to using `~` (the home directory). -- Setting: `path` -- Default: `~` - -**Options** - -`string` values - -### Hour Format - -- Description: The format to use for displaying hours in the journal. -- Setting: `hour_format` -- Default: `hour12` - -**Options** - -1. 12-hour format: - -```json [settings] -{ - "hour_format": "hour12" -} -``` - -2. 24-hour format: - -```json [settings] -{ - "hour_format": "hour24" -} -``` - -## JSX Tag Auto Close - -- Description: Whether to automatically close JSX tags -- Setting: `jsx_tag_auto_close` -- Default: - -```json [settings] -{ - "jsx_tag_auto_close": { - "enabled": true - } -} -``` - -**Options** - -- `enabled`: Whether to enable automatic JSX tag closing - -## Languages - -- Description: Configuration for specific languages. -- Setting: `languages` -- Default: `null` - -**Options** - -To override settings for a language, add an entry for that languages name to the `languages` value. Example: - -```json [settings] -"languages": { - "C": { - "format_on_save": "off", - "preferred_line_length": 64, - "soft_wrap": "preferred_line_length" - }, - "JSON": { - "tab_size": 4 - } -} -``` - -The following settings can be overridden for each specific language: - -- [`enable_language_server`](#enable-language-server) -- [`ensure_final_newline_on_save`](#ensure-final-newline-on-save) -- [`format_on_save`](#format-on-save) -- [`formatter`](#formatter) -- [`hard_tabs`](#hard-tabs) -- [`preferred_line_length`](#preferred-line-length) -- [`remove_trailing_whitespace_on_save`](#remove-trailing-whitespace-on-save) -- [`show_edit_predictions`](#show-edit-predictions) -- [`show_whitespaces`](#show-whitespaces) -- [`whitespace_map`](#whitespace-map) -- [`soft_wrap`](#soft-wrap) -- [`tab_size`](#tab-size) -- [`use_autoclose`](#use-autoclose) -- [`always_treat_brackets_as_autoclosed`](#always-treat-brackets-as-autoclosed) - -These values take in the same options as the root-level settings with the same name. - -## Language Models - -- Description: Configuration for language model providers -- Setting: `language_models` -- Default: - -```json [settings] -{ - "language_models": { - "anthropic": { - "api_url": "https://api.anthropic.com" - }, - "google": { - "api_url": "https://generativelanguage.googleapis.com" - }, - "ollama": { - "api_url": "http://localhost:11434" - }, - "openai": { - "api_url": "https://api.openai.com/v1" - } - } -} -``` - -**Options** - -Configuration for various AI model providers including API URLs and authentication settings. - -## Line Indicator Format - -- Description: Format for line indicator in the status bar -- Setting: `line_indicator_format` -- Default: `"short"` - -**Options** - -1. Short format: - -```json [settings] -{ - "line_indicator_format": "short" -} -``` - -2. Long format: - -```json [settings] -{ - "line_indicator_format": "long" -} -``` - -## Linked Edits - -- Description: Whether to perform linked edits of associated ranges, if the language server supports it. For example, when editing opening `` tag, the contents of the closing `` tag will be edited as well. -- Setting: `linked_edits` -- Default: `true` - -**Options** - -`boolean` values - -## LSP Document Colors - -- Description: Whether to show document color information from the language server -- Setting: `lsp_document_colors` -- Default: `true` - -**Options** - -`boolean` values - -## Max Tabs - -- Description: Maximum number of tabs to show in the tab bar -- Setting: `max_tabs` -- Default: `null` - -**Options** - -Positive `integer` values or `null` for unlimited tabs - -## Middle Click Paste (Linux only) - -- Description: Enable middle-click paste on Linux -- Setting: `middle_click_paste` -- Default: `true` - -**Options** - -`boolean` values - -## Multi Cursor Modifier - -- Description: Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier. -- Setting: `multi_cursor_modifier` -- Default: `alt` - -**Options** - -1. Maps to `Alt` on Linux and Windows and to `Option` on macOS: - -```json [settings] -{ - "multi_cursor_modifier": "alt" -} -``` - -2. Maps `Control` on Linux and Windows and to `Command` on macOS: - -```json [settings] -{ - "multi_cursor_modifier": "cmd_or_ctrl" // alias: "cmd", "ctrl" -} -``` - -## Node - -- Description: Configuration for Node.js integration -- Setting: `node` -- Default: - -```json [settings] -{ - "node": { - "ignore_system_version": false, - "path": null, - "npm_path": null - } -} -``` - -**Options** - -- `ignore_system_version`: Whether to ignore the system Node.js version -- `path`: Custom path to Node.js binary -- `npm_path`: Custom path to npm binary - -## Network Proxy - -- Description: Configure a network proxy for Zed. -- Setting: `proxy` -- Default: `null` - -**Options** - -The proxy setting must contain a URL to the proxy. - -The following URI schemes are supported: - -- `http` -- `https` -- `socks4` - SOCKS4 proxy with local DNS -- `socks4a` - SOCKS4 proxy with remote DNS -- `socks5` - SOCKS5 proxy with local DNS -- `socks5h` - SOCKS5 proxy with remote DNS - -`http` will be used when no scheme is specified. - -By default no proxy will be used, or Zed will attempt to retrieve proxy settings from environment variables, such as `http_proxy`, `HTTP_PROXY`, `https_proxy`, `HTTPS_PROXY`, `all_proxy`, `ALL_PROXY`, `no_proxy` and `NO_PROXY`. - -For example, to set an `http` proxy, add the following to your settings: - -```json [settings] -{ - "proxy": "http://127.0.0.1:10809" -} -``` - -Or to set a `socks5` proxy: - -```json [settings] -{ - "proxy": "socks5h://localhost:10808" -} -``` - -If you wish to exclude certain hosts from using the proxy, set the `NO_PROXY` environment variable. This accepts a comma-separated list of hostnames, host suffixes, IPv4/IPv6 addresses or blocks that should not use the proxy. For example if your environment included `NO_PROXY="google.com, 192.168.1.0/24"` all hosts in `192.168.1.*`, `google.com` and `*.google.com` would bypass the proxy. See [reqwest NoProxy docs](https://docs.rs/reqwest/latest/reqwest/struct.NoProxy.html#method.from_string) for more. - -## On Last Window Closed - -- Description: What to do when the last window is closed -- Setting: `on_last_window_closed` -- Default: `"platform_default"` - -**Options** - -1. Use platform default behavior: - -```json [settings] -{ - "on_last_window_closed": "platform_default" -} -``` - -2. Always quit the application: - -```json [settings] -{ - "on_last_window_closed": "quit_app" -} -``` - -## Profiles - -- Description: Configuration profiles that can be applied on top of existing settings -- Setting: `profiles` -- Default: `{}` - -**Options** - -Configuration object for defining settings profiles. Example: - -```json [settings] -{ - "profiles": { - "presentation": { - "buffer_font_size": 20, - "ui_font_size": 18, - "theme": "One Light" - } - } -} -``` - -## Preview tabs - -- Description: - Preview tabs allow you to open files in preview mode, where they close automatically when you switch to another file unless you explicitly pin them. This is useful for quickly viewing files without cluttering your workspace. Preview tabs display their file names in italics. \ - There are several ways to convert a preview tab into a regular tab: - - - Double-clicking on the file - - Double-clicking on the tab header - - Using the {#action project_panel::OpenPermanent} action - - Editing the file - - Dragging the file to a different pane - -- Setting: `preview_tabs` -- Default: - -```json [settings] -"preview_tabs": { - "enabled": true, - "enable_preview_from_project_panel": true, - "enable_preview_from_file_finder": false, - "enable_preview_from_multibuffer": true, - "enable_preview_multibuffer_from_code_navigation": false, - "enable_preview_file_from_code_navigation": true, - "enable_keep_preview_on_code_navigation": false, -} -``` - -### Enable preview from project panel - -- Description: Determines whether to open files in preview mode when opened from the project panel with a single click. -- Setting: `enable_preview_from_project_panel` -- Default: `true` - -**Options** - -`boolean` values - -### Enable preview from file finder - -- Description: Determines whether to open files in preview mode when selected from the file finder. -- Setting: `enable_preview_from_file_finder` -- Default: `false` - -**Options** - -`boolean` values - -### Enable preview from multibuffer - -- Description: Determines whether to open files in preview mode when opened from a multibuffer. -- Setting: `enable_preview_from_multibuffer` -- Default: `true` - -**Options** - -`boolean` values - -### Enable preview multibuffer from code navigation - -- Description: Determines whether to open tabs in preview mode when code navigation is used to open a multibuffer. -- Setting: `enable_preview_multibuffer_from_code_navigation` -- Default: `false` - -**Options** - -`boolean` values - -### Enable preview file from code navigation - -- Description: Determines whether to open tabs in preview mode when code navigation is used to open a single file. -- Setting: `enable_preview_file_from_code_navigation` -- Default: `true` - -**Options** - -`boolean` values - -### Enable keep preview on code navigation - -- Description: Determines whether to keep tabs in preview mode when code navigation is used to navigate away from them. If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one. -- Setting: `enable_keep_preview_on_code_navigation` -- Default: `false` - -**Options** - -`boolean` values - -## File Finder - -### File Icons - -- Description: Whether to show file icons in the file finder. -- Setting: `file_icons` -- Default: `true` - -### Modal Max Width - -- Description: Max-width of the file finder modal. It can take one of these values: `small`, `medium`, `large`, `xlarge`, and `full`. -- Setting: `modal_max_width` -- Default: `small` - -### Skip Focus For Active In Search - -- Description: Determines whether the file finder should skip focus for the active file in search results. -- Setting: `skip_focus_for_active_in_search` -- Default: `true` - -## Pane Split Direction Horizontal - -- Description: The direction that you want to split panes horizontally -- Setting: `pane_split_direction_horizontal` -- Default: `"up"` - -**Options** - -1. Split upward: - -```json [settings] -{ - "pane_split_direction_horizontal": "up" -} -``` - -2. Split downward: - -```json [settings] -{ - "pane_split_direction_horizontal": "down" -} -``` - -## Pane Split Direction Vertical - -- Description: The direction that you want to split panes vertically -- Setting: `pane_split_direction_vertical` -- Default: `"left"` - -**Options** - -1. Split to the left: - -```json [settings] -{ - "pane_split_direction_vertical": "left" -} -``` - -2. Split to the right: - -```json [settings] -{ - "pane_split_direction_vertical": "right" -} -``` - -## Preferred Line Length - -- Description: The column at which to soft-wrap lines, for buffers where soft-wrap is enabled. -- Setting: `preferred_line_length` -- Default: `80` - -**Options** - -`integer` values - -## Private Files - -- Description: Globs to match against file paths to determine if a file is private -- Setting: `private_files` -- Default: `["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"]` - -**Options** - -List of `string` glob patterns - -## Projects Online By Default - -- Description: Whether or not to show the online projects view by default. -- Setting: `projects_online_by_default` -- Default: `true` - -**Options** - -`boolean` values - -## Read SSH Config - -- Description: Whether to read SSH configuration files -- Setting: `read_ssh_config` -- Default: `true` - -**Options** - -`boolean` values - -## Redact Private Values - -- Description: Hide the values of variables from visual display in private files -- Setting: `redact_private_values` -- Default: `false` - -**Options** - -`boolean` values - -## Relative Line Numbers - -- Description: Whether to show relative line numbers in the gutter -- Setting: `relative_line_numbers` -- Default: `"disabled"` - -**Options** - -1. Show relative line numbers in the gutter whilst counting wrapped lines as one line: - -```json [settings] -{ - "relative_line_numbers": "enabled" -} -``` - -2. Show relative line numbers in the gutter, including wrapped lines in the counting: - -```json [settings] -{ - "relative_line_numbers": "wrapped" -} -``` - -2. Do not use relative line numbers: - -```json [settings] -{ - "relative_line_numbers": "disabled" -} -``` - -## Remove Trailing Whitespace On Save - -- Description: Whether or not to remove any trailing whitespace from lines of a buffer before saving it. -- Setting: `remove_trailing_whitespace_on_save` -- Default: `true` - -**Options** - -`boolean` values - -## Resize All Panels In Dock - -- Description: Whether to resize all the panels in a dock when resizing the dock. Can be a combination of "left", "right" and "bottom". -- Setting: `resize_all_panels_in_dock` -- Default: `["left"]` - -**Options** - -List of strings containing any combination of: - -- `"left"`: Resize left dock panels together -- `"right"`: Resize right dock panels together -- `"bottom"`: Resize bottom dock panels together - -## Restore on File Reopen - -- Description: Whether to attempt to restore previous file's state when opening it again. The state is stored per pane. -- Setting: `restore_on_file_reopen` -- Default: `true` - -**Options** - -`boolean` values - -## Restore on Startup - -- Description: Controls session restoration on startup. -- Setting: `restore_on_startup` -- Default: `last_session` - -**Options** - -1. Restore all workspaces that were open when quitting Zed: - -```json [settings] -{ - "restore_on_startup": "last_session" -} -``` - -2. Restore the workspace that was closed last: - -```json [settings] -{ - "restore_on_startup": "last_workspace" -} -``` - -3. Always start with an empty editor: - -```json [settings] -{ - "restore_on_startup": "none" -} -``` - -## Scroll Beyond Last Line - -- Description: Whether the editor will scroll beyond the last line -- Setting: `scroll_beyond_last_line` -- Default: `"one_page"` - -**Options** - -1. Scroll one page beyond the last line by one page: - -```json [settings] -{ - "scroll_beyond_last_line": "one_page" -} -``` - -2. The editor will scroll beyond the last line by the same amount of lines as `vertical_scroll_margin`: - -```json [settings] -{ - "scroll_beyond_last_line": "vertical_scroll_margin" -} -``` - -3. The editor will not scroll beyond the last line: - -```json [settings] -{ - "scroll_beyond_last_line": "off" -} -``` - -**Options** - -`boolean` values - -## Scroll Sensitivity - -- Description: Scroll sensitivity multiplier. This multiplier is applied to both the horizontal and vertical delta values while scrolling. -- Setting: `scroll_sensitivity` -- Default: `1.0` - -**Options** - -Positive `float` values - -### Fast Scroll Sensitivity - -- Description: Scroll sensitivity multiplier for fast scrolling. This multiplier is applied to both the horizontal and vertical delta values while scrolling. Fast scrolling happens when a user holds the alt or option key while scrolling. -- Setting: `fast_scroll_sensitivity` -- Default: `4.0` - -**Options** - -Positive `float` values - -### Horizontal Scroll Margin - -- Description: The number of characters to keep on either side when scrolling with the mouse -- Setting: `horizontal_scroll_margin` -- Default: `5` - -**Options** - -Non-negative `integer` values - -### Vertical Scroll Margin - -- Description: The number of lines to keep above/below the cursor when scrolling with the keyboard -- Setting: `vertical_scroll_margin` -- Default: `3` - -**Options** - -Non-negative `integer` values - -## Search - -- Description: Search options to enable by default when opening new project and buffer searches. -- Setting: `search` -- Default: - -```json [settings] -"search": { - "button": true, - "whole_word": false, - "case_sensitive": false, - "include_ignored": false, - "regex": false, - "center_on_match": false -}, -``` - -### Button - -- Description: Whether to show the project search button in the status bar. -- Setting: `button` -- Default: `true` - -### Whole Word - -- Description: Whether to only match on whole words. -- Setting: `whole_word` -- Default: `false` - -### Case Sensitive - -- Description: Whether to match case sensitively. This setting affects both - searches and editor actions like "Select Next Occurrence", "Select Previous - Occurrence", and "Select All Occurrences". -- Setting: `case_sensitive` -- Default: `false` - -### Include Ignore - -- Description: Whether to include gitignored files in search results. -- Setting: `include_ignored` -- Default: `false` - -### Regex - -- Description: Whether to interpret the search query as a regular expression. -- Setting: `regex` -- Default: `false` - -### Center On Match - -- Description: Whether to center the cursor on each search match when navigating. -- Setting: `center_on_match` -- Default: `false` - -## Search Wrap - -- Description: If `search_wrap` is disabled, search result do not wrap around the end of the file -- Setting: `search_wrap` -- Default: `true` - -## Center on Match - -- Description: If `center_on_match` is enabled, the editor will center the cursor on the current match when searching. -- Setting: `center_on_match` -- Default: `false` - -## Seed Search Query From Cursor - -- Description: When to populate a new search's query based on the text under the cursor. -- Setting: `seed_search_query_from_cursor` -- Default: `always` - -**Options** - -1. `always` always populate the search query with the word under the cursor -2. `selection` only populate the search query when there is text selected -3. `never` never populate the search query - -## Use Smartcase Search - -- Description: When enabled, automatically adjusts search case sensitivity based on your query. If your search query contains any uppercase letters, the search becomes case-sensitive; if it contains only lowercase letters, the search becomes case-insensitive. \ - This applies to both in-file searches and project-wide searches. -- Setting: `use_smartcase_search` -- Default: `false` - -**Options** - -`boolean` values - -Examples: - -- Searching for "function" would match "function", "Function", "FUNCTION", etc. -- Searching for "Function" would only match "Function", not "function" or "FUNCTION" - -## Show Call Status Icon - -- Description: Whether or not to show the call status icon in the status bar. -- Setting: `show_call_status_icon` -- Default: `true` - -**Options** - -`boolean` values - -## Completions - -- Description: Controls how completions are processed for this language. -- Setting: `completions` -- Default: - -```json [settings] -{ - "completions": { - "words": "fallback", - "words_min_length": 3, - "lsp": true, - "lsp_fetch_timeout_ms": 0, - "lsp_insert_mode": "replace_suffix" - } -} -``` - -### Words - -- Description: Controls how words are completed. For large documents, not all words may be fetched for completion. -- Setting: `words` -- Default: `fallback` - -**Options** - -1. `enabled` - Always fetch document's words for completions along with LSP completions -2. `fallback` - Only if LSP response errors or times out, use document's words to show completions -3. `disabled` - Never fetch or complete document's words for completions (word-based completions can still be queried via a separate action) - -### Min Words Query Length - -- Description: 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. -- Setting: `words_min_length` -- Default: `3` - -**Options** - -Positive integer values - -### LSP - -- Description: Whether to fetch LSP completions or not. -- Setting: `lsp` -- Default: `true` - -**Options** - -`boolean` values - -### LSP Fetch Timeout (ms) - -- Description: When fetching LSP completions, determines how long to wait for a response of a particular server. When set to 0, waits indefinitely. -- Setting: `lsp_fetch_timeout_ms` -- Default: `0` - -**Options** - -`integer` values representing milliseconds - -### LSP Insert Mode - -- Description: Controls what range to replace when accepting LSP completions. -- Setting: `lsp_insert_mode` -- Default: `replace_suffix` - -**Options** - -1. `insert` - Replaces text before the cursor, using the `insert` range described in the LSP specification -2. `replace` - Replaces text before and after the cursor, using the `replace` range described in the LSP specification -3. `replace_subsequence` - Behaves like `"replace"` if the text that would be replaced is a subsequence of the completion text, and like `"insert"` otherwise -4. `replace_suffix` - Behaves like `"replace"` if the text after the cursor is a suffix of the completion, and like `"insert"` otherwise - -## Show Completions On Input - -- Description: Whether or not to show completions as you type. -- Setting: `show_completions_on_input` -- Default: `true` - -**Options** - -`boolean` values - -## Show Completion Documentation - -- Description: Whether to display inline and alongside documentation for items in the completions menu. -- Setting: `show_completion_documentation` -- Default: `true` - -**Options** - -`boolean` values - -## Show Edit Predictions - -- Description: Whether to show edit predictions as you type or manually by triggering `editor::ShowEditPrediction`. -- Setting: `show_edit_predictions` -- Default: `true` - -**Options** - -`boolean` values - -## Show Whitespaces - -- Description: Whether or not to render whitespace characters in the editor. -- Setting: `show_whitespaces` -- Default: `selection` - -**Options** - -1. `all` -2. `selection` -3. `none` -4. `boundary` - -## Whitespace Map - -- Description: Specify the characters used to render whitespace when show_whitespaces is enabled. -- Setting: `whitespace_map` -- Default: - -```json [settings] -{ - "whitespace_map": { - "space": "•", - "tab": "→" - } -} -``` - -## Soft Wrap - -- Description: Whether or not to automatically wrap lines of text to fit editor / preferred width. -- Setting: `soft_wrap` -- Default: `none` - -**Options** - -1. `none` to avoid wrapping generally, unless the line is too long -2. `prefer_line` (deprecated, same as `none`) -3. `editor_width` to wrap lines that overflow the editor width -4. `preferred_line_length` to wrap lines that overflow `preferred_line_length` config value -5. `bounded` to wrap lines at the minimum of `editor_width` and `preferred_line_length` - -## Show Wrap Guides - -- Description: Whether to show wrap guides (vertical rulers) in the editor. Setting this to true will show a guide at the 'preferred_line_length' value if 'soft_wrap' is set to 'preferred_line_length', and will show any additional guides as specified by the 'wrap_guides' setting. -- Setting: `show_wrap_guides` -- Default: `true` - -**Options** - -`boolean` values - -## Use On Type Format - -- Description: Whether to use additional LSP queries to format (and amend) the code after every "trigger" symbol input, defined by LSP server capabilities -- Setting: `use_on_type_format` -- Default: `true` - -**Options** - -`boolean` values - -## Use Auto Surround - -- Description: Whether to automatically surround selected text when typing opening parenthesis, bracket, brace, single or double quote characters. For example, when you select text and type '(', Zed will surround the text with (). -- Setting: `use_auto_surround` -- Default: `true` - -**Options** - -`boolean` values - -## Use System Path Prompts - -- Description: 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. -- Setting: `use_system_path_prompts` -- Default: `true` - -**Options** - -`boolean` values - -## Use System Prompts - -- Description: Whether to use the system provided dialogs for prompts, such as confirmation prompts. When set to false, Zed will use its built-in prompts. Note that on Linux, this option is ignored and Zed will always use the built-in prompts. -- Setting: `use_system_prompts` -- Default: `true` - -**Options** - -`boolean` values - -## Wrap Guides (Vertical Rulers) - -- Description: Where to display vertical rulers as wrap-guides. Disable by setting `show_wrap_guides` to `false`. -- Setting: `wrap_guides` -- Default: [] - -**Options** - -List of `integer` column numbers - -## Tab Size - -- Description: The number of spaces to use for each tab character. -- Setting: `tab_size` -- Default: `4` - -**Options** - -`integer` values - -## Tasks - -- Description: Configuration for tasks that can be run within Zed -- Setting: `tasks` -- Default: - -```json [settings] -{ - "tasks": { - "variables": {}, - "enabled": true, - "prefer_lsp": false - } -} -``` - -**Options** - -- `variables`: Custom variables for task configuration -- `enabled`: Whether tasks are enabled -- `prefer_lsp`: Whether to prefer LSP-provided tasks over Zed language extension ones - -## Telemetry - -- Description: Control what info is collected by Zed. -- Setting: `telemetry` -- Default: - -```json [settings] -"telemetry": { - "diagnostics": true, - "metrics": true -}, -``` - -**Options** - -### Diagnostics - -- Description: Setting for sending debug-related data, such as crash reports. -- Setting: `diagnostics` -- Default: `true` - -**Options** - -`boolean` values - -### Metrics - -- Description: Setting for sending anonymized usage data, such what languages you're using Zed with. -- Setting: `metrics` -- Default: `true` - -**Options** - -`boolean` values - -## Terminal - -- Description: Configuration for the terminal. -- Setting: `terminal` -- Default: - -```json [settings] -{ - "terminal": { - "alternate_scroll": "off", - "blinking": "terminal_controlled", - "copy_on_select": false, - "keep_selection_on_copy": true, - "dock": "bottom", - "default_width": 640, - "default_height": 320, - "detect_venv": { - "on": { - "directories": [".env", "env", ".venv", "venv"], - "activate_script": "default" - } - }, - "env": {}, - "font_family": null, - "font_features": null, - "font_size": null, - "line_height": "comfortable", - "minimum_contrast": 45, - "option_as_meta": false, - "button": true, - "shell": "system", - "scroll_multiplier": 3.0, - "toolbar": { - "breadcrumbs": false - }, - "working_directory": "current_project_directory", - "scrollbar": { - "show": null - } - } -} -``` - -### Terminal: Dock - -- Description: Control the position of the dock -- Setting: `dock` -- Default: `bottom` - -**Options** - -`"bottom"`, `"left"` or `"right"` - -### Terminal: Alternate Scroll - -- Description: Set whether Alternate Scroll mode (DECSET code: `?1007`) is active by default. Alternate Scroll mode converts mouse scroll events into up / down key presses when in the alternate screen (e.g. when running applications like vim or less). The terminal can still set and unset this mode with ANSI escape codes. -- Setting: `alternate_scroll` -- Default: `off` - -**Options** - -1. Default alternate scroll mode to off - -```json [settings] -{ - "terminal": { - "alternate_scroll": "off" - } -} -``` - -2. Default alternate scroll mode to on - -```json [settings] -{ - "terminal": { - "alternate_scroll": "on" - } -} -``` - -### Terminal: Blinking - -- Description: Set the cursor blinking behavior in the terminal -- Setting: `blinking` -- Default: `terminal_controlled` - -**Options** - -1. Never blink the cursor, ignore the terminal mode - -```json [settings] -{ - "terminal": { - "blinking": "off" - } -} -``` - -2. Default the cursor blink to off, but allow the terminal to turn blinking on - -```json [settings] -{ - "terminal": { - "blinking": "terminal_controlled" - } -} -``` - -3. Always blink the cursor, ignore the terminal mode - -```json [settings] -{ - "terminal": { - "blinking": "on" - } -} -``` - -### Terminal: Copy On Select - -- Description: Whether or not selecting text in the terminal will automatically copy to the system clipboard. -- Setting: `copy_on_select` -- Default: `false` - -**Options** - -`boolean` values - -**Example** - -```json [settings] -{ - "terminal": { - "copy_on_select": true - } -} -``` - -### Terminal: Cursor Shape - -- Description: Controls the visual shape of the cursor in the terminal. When not explicitly set, it defaults to a block shape. -- Setting: `cursor_shape` -- Default: `null` (defaults to block) - -**Options** - -1. A block that surrounds the following character - -```json [settings] -{ - "terminal": { - "cursor_shape": "block" - } -} -``` - -2. A vertical bar - -```json [settings] -{ - "terminal": { - "cursor_shape": "bar" - } -} -``` - -3. An underline / underscore that runs along the following character - -```json [settings] -{ - "terminal": { - "cursor_shape": "underline" - } -} -``` - -4. A box drawn around the following character - -```json [settings] -{ - "terminal": { - "cursor_shape": "hollow" - } -} -``` - -### Terminal: Keep Selection On Copy - -- Description: Whether or not to keep the selection in the terminal after copying text. -- Setting: `keep_selection_on_copy` -- Default: `true` - -**Options** - -`boolean` values - -**Example** - -```json [settings] -{ - "terminal": { - "keep_selection_on_copy": false - } -} -``` - -### Terminal: Env - -- Description: Any key-value pairs added to this object will be added to the terminal's environment. Keys must be unique, use `:` to separate multiple values in a single variable -- Setting: `env` -- Default: `{}` - -**Example** - -```json [settings] -{ - "terminal": { - "env": { - "ZED": "1", - "KEY": "value1:value2" - } - } -} -``` - -### Terminal: Font Size - -- Description: What font size to use for the terminal. When not set defaults to matching the editor's font size -- Setting: `font_size` -- Default: `null` - -**Options** - -`integer` values - -```json [settings] -{ - "terminal": { - "font_size": 15 - } -} -``` - -### Terminal: Font Family - -- Description: What font to use for the terminal. When not set, defaults to matching the editor's font. -- Setting: `font_family` -- Default: `null` - -**Options** - -The name of any font family installed on the user's system - -```json [settings] -{ - "terminal": { - "font_family": "Berkeley Mono" - } -} -``` - -### Terminal: Font Features - -- Description: What font features to use for the terminal. When not set, defaults to matching the editor's font features. -- Setting: `font_features` -- Default: `null` -- Platform: macOS and Windows. - -**Options** - -See Buffer Font Features - -```json [settings] -{ - "terminal": { - "font_features": { - "calt": false - // See Buffer Font Features for more features - } - } -} -``` - -### Terminal: Line Height - -- Description: Set the terminal's line height. -- Setting: `line_height` -- Default: `standard` - -**Options** - -1. Use a line height that's `comfortable` for reading, 1.618. - -```json [settings] -{ - "terminal": { - "line_height": "comfortable" - } -} -``` - -2. Use a `standard` line height, 1.3. This option is useful for TUIs, particularly if they use box characters. (default) - -```json [settings] -{ - "terminal": { - "line_height": "standard" - } -} -``` - -3. Use a custom line height. - -```json [settings] -{ - "terminal": { - "line_height": { - "custom": 2 - } - } -} -``` - -### Terminal: Minimum Contrast - -- Description: Controls the minimum contrast between foreground and background colors in the terminal. Uses the APCA (Accessible Perceptual Contrast Algorithm) for color adjustments. Set this to 0 to disable this feature. -- Setting: `minimum_contrast` -- Default: `45` - -**Options** - -`integer` values from 0 to 106. Common recommended values: - -- `0`: No contrast adjustment -- `45`: Minimum for large fluent text (default) -- `60`: Minimum for other content text -- `75`: Minimum for body text -- `90`: Preferred for body text - -```json [settings] -{ - "terminal": { - "minimum_contrast": 45 - } -} -``` - -### Terminal: Option As Meta - -- Description: Re-interprets the option keys to act like a 'meta' key, like in Emacs. -- Setting: `option_as_meta` -- Default: `false` - -**Options** - -`boolean` values - -```json [settings] -{ - "terminal": { - "option_as_meta": true - } -} -``` - -### Terminal: Shell - -- Description: What shell to use when launching the terminal. -- Setting: `shell` -- Default: `system` - -**Options** - -1. Use the system's default terminal configuration (usually the `/etc/passwd` file). - -```json [settings] -{ - "terminal": { - "shell": "system" - } -} -``` - -2. A program to launch: - -```json [settings] -{ - "terminal": { - "shell": { - "program": "sh" - } - } -} -``` - -3. A program with arguments: - -```json [settings] -{ - "terminal": { - "shell": { - "with_arguments": { - "program": "/bin/bash", - "args": ["--login"] - } - } - } -} -``` - -## Terminal: Detect Virtual Environments {#terminal-detect_venv} - -- Description: Activate the [Python Virtual Environment](https://docs.python.org/3/library/venv.html), if one is found, in the terminal's working directory (as resolved by the working_directory and automatically activating the virtual environment. -- Setting: `detect_venv` -- Default: - -```json [settings] -{ - "terminal": { - "detect_venv": { - "on": { - // Default directories to search for virtual environments, relative - // to the current working directory. We recommend overriding this - // in your project's settings, rather than globally. - "directories": [".env", "env", ".venv", "venv"], - // Can also be `csh`, `fish`, and `nushell` - "activate_script": "default" - } - } - } -} -``` - -Disable with: - -```json [settings] -{ - "terminal": { - "detect_venv": "off" - } -} -``` - -### Terminal: Scroll Multiplier - -- Description: The multiplier for scrolling speed in the terminal when using mouse wheel or trackpad. -- Setting: `scroll_multiplier` -- Default: `1.0` - -**Options** - -Positive floating point values. Values less than or equal to 0 will be clamped to a minimum of 0.01. - -**Example** - -```json -{ - "terminal": { - "scroll_multiplier": 5.0 - } -} -``` - -## Terminal: Toolbar - -- Description: Whether or not to show various elements in the terminal toolbar. -- Setting: `toolbar` -- Default: - -```json [settings] -{ - "terminal": { - "toolbar": { - "breadcrumbs": false - } - } -} -``` - -**Options** - -At the moment, only the `breadcrumbs` option is available, it controls displaying of the terminal title that can be changed via `PROMPT_COMMAND`. - -If the terminal title is empty, the breadcrumbs won't be shown. - -The shell running in the terminal needs to be configured to emit the title. - -Example command to set the title: `echo -e "\e]2;New Title\007";` - -### Terminal: Button - -- Description: Control to show or hide the terminal button in the status bar -- Setting: `button` -- Default: `true` - -**Options** - -`boolean` values - -```json [settings] -{ - "terminal": { - "button": false - } -} -``` - -### Terminal: Working Directory - -- Description: What working directory to use when launching the terminal. -- Setting: `working_directory` -- Default: `"current_project_directory"` - -**Options** - -1. Use the current file's project directory. Fallback to the first project directory strategy if unsuccessful. - -```json [settings] -{ - "terminal": { - "working_directory": "current_project_directory" - } -} -``` - -2. Use the first project in this workspace's directory. Fallback to using this platform's home directory. - -```json [settings] -{ - "terminal": { - "working_directory": "first_project_directory" - } -} -``` - -3. Always use this platform's home directory if it can be found. - -```json [settings] -{ - "terminal": { - "working_directory": "always_home" - } -} -``` - -4. Always use a specific directory. This value will be shell expanded. If this path is not a valid directory the terminal will default to this platform's home directory. - -```json [settings] -{ - "terminal": { - "working_directory": { - "always": { - "directory": "~/zed/projects/" - } - } - } -} -``` - -### Terminal: Path Hyperlink Regexes - -- Description: Regexes used to identify path hyperlinks. The regexes can be specified in two forms - a single regex string, or an array of strings (which will be collected into a single multi-line regex string). -- Setting: `path_hyperlink_regexes` -- Default: - -```json [settings] -{ - "terminal": { - "path_hyperlink_regexes": [ - // Python-style diagnostics - "File \"(?[^\"]+)\", line (?[0-9]+)", - // Common path syntax with optional line, column, description, trailing punctuation, or - // surrounding symbols or quotes - [ - "(?x)", - "# optionally starts with 0-2 opening prefix symbols", - "[({\\[<]{0,2}", - "# which may be followed by an opening quote", - "(?[\"'`])?", - "# `path` is the shortest sequence of any non-space character", - "(?(?[^ ]+?", - " # which may end with a line and optionally a column,", - " (?:+[0-9]+(:[0-9]+)?|:?\\([0-9]+([,:][0-9]+)?\\))?", - "))", - "# which must be followed by a matching quote", - "(?()\\k)", - "# and optionally a single closing symbol", - "[)}\\]>]?", - "# if line/column matched, may be followed by a description", - "(?():[^ 0-9][^ ]*)?", - "# which may be followed by trailing punctuation", - "[.,:)}\\]>]*", - "# and always includes trailing whitespace or end of line", - "([ ]+|$)" - ] - ] - } -} -``` - -### Terminal: Path Hyperlink Timeout (ms) - -- Description: Maximum time to search for a path hyperlink. When set to 0, path hyperlinks are disabled. -- Setting: `path_hyperlink_timeout_ms` -- Default: `1` - -## REPL - -- Description: Repl settings. -- Setting: `repl` -- Default: - -```json [settings] -"repl": { - // Maximum number of columns to keep in REPL's scrollback buffer. - // Clamped with [20, 512] range. - "max_columns": 128, - // Maximum number of lines to keep in REPL's scrollback buffer. - // Clamped with [4, 256] range. - "max_lines": 32 -}, -``` - -## Theme - -- Description: The theme setting can be specified in two forms - either as the name of a theme or as an object containing the `mode`, `dark`, and `light` themes for the Zed UI. -- Setting: `theme` -- Default: `One Dark` - -### Theme Object - -- Description: Specify the theme using an object that includes the `mode`, `dark`, and `light` themes. -- Setting: `theme` -- Default: - -```json [settings] -"theme": { - "mode": "system", - "dark": "One Dark", - "light": "One Light" -}, -``` - -### Mode - -- Description: Specify theme mode. -- Setting: `mode` -- Default: `system` - -**Options** - -1. Set the theme to dark mode - -```json [settings] -{ - "mode": "dark" -} -``` - -2. Set the theme to light mode - -```json [settings] -{ - "mode": "light" -} -``` - -3. Set the theme to system mode - -```json [settings] -{ - "mode": "system" -} -``` - -### Dark - -- Description: The name of the dark Zed theme to use for the UI. -- Setting: `dark` -- Default: `One Dark` - -**Options** - -Run the {#action theme_selector::Toggle} action in the command palette to see a current list of valid themes names. - -### Light - -- Description: The name of the light Zed theme to use for the UI. -- Setting: `light` -- Default: `One Light` - -**Options** - -Run the {#action theme_selector::Toggle} action in the command palette to see a current list of valid themes names. - -## Title Bar - -- Description: Whether or not to show various elements in the title bar -- Setting: `title_bar` -- Default: - -```json [settings] -"title_bar": { - "show_branch_icon": false, - "show_branch_name": true, - "show_project_items": true, - "show_onboarding_banner": true, - "show_user_picture": true, - "show_sign_in": true, - "show_menus": false -} -``` - -**Options** - -- `show_branch_icon`: Whether to show the branch icon beside branch switcher in the titlebar -- `show_branch_name`: Whether to show the branch name button in the titlebar -- `show_project_items`: Whether to show the project host and name in the titlebar -- `show_onboarding_banner`: Whether to show onboarding banners in the titlebar -- `show_user_picture`: Whether to show user picture in the titlebar -- `show_sign_in`: Whether to show the sign in button in the titlebar -- `show_menus`: Whether to show the menus in the titlebar - -## Vim - -- Description: Whether or not to enable vim mode. -- Setting: `vim_mode` -- Default: `false` - -## When Closing With No Tabs - -- Description: Whether the window should be closed when using 'close active item' on a window with no tabs -- Setting: `when_closing_with_no_tabs` -- Default: `"platform_default"` - -**Options** - -1. Use platform default behavior: - -```json [settings] -{ - "when_closing_with_no_tabs": "platform_default" -} -``` - -2. Always close the window: - -```json [settings] -{ - "when_closing_with_no_tabs": "close_window" -} -``` - -3. Never close the window: - -```json [settings] -{ - "when_closing_with_no_tabs": "keep_window_open" -} -``` - -## Project Panel - -- Description: Customize project panel -- Setting: `project_panel` -- Default: - -```json [settings] -{ - "project_panel": { - "button": true, - "default_width": 240, - "dock": "left", - "entry_spacing": "comfortable", - "file_icons": true, - "folder_icons": true, - "git_status": true, - "indent_size": 20, - "auto_reveal_entries": true, - "auto_fold_dirs": true, - "drag_and_drop": true, - "scrollbar": { - "show": null - }, - "sticky_scroll": true, - "show_diagnostics": "all", - "indent_guides": { - "show": "always" - }, - "sort_mode": "directories_first", - "hide_root": false, - "hide_hidden": false, - "starts_open": true, - "auto_open": { - "on_create": true, - "on_paste": true, - "on_drop": true - } - } -} -``` - -### Dock - -- Description: Control the position of the dock -- Setting: `dock` -- Default: `left` - -**Options** - -1. Default dock position to left - -```json [settings] -{ - "dock": "left" -} -``` - -2. Default dock position to right - -```json [settings] -{ - "dock": "right" -} -``` - -### Entry Spacing - -- Description: Spacing between worktree entries -- Setting: `entry_spacing` -- Default: `comfortable` - -**Options** - -1. Comfortable entry spacing - -```json [settings] -{ - "entry_spacing": "comfortable" -} -``` - -2. Standard entry spacing - -```json [settings] -{ - "entry_spacing": "standard" -} -``` - -### Git Status - -- Description: Indicates newly created and updated files -- Setting: `git_status` -- Default: `true` - -**Options** - -1. Default enable git status - -```json [settings] -{ - "git_status": true -} -``` - -2. Default disable git status - -```json [settings] -{ - "git_status": false -} -``` - -### Default Width - -- Description: Customize default width taken by project panel -- Setting: `default_width` -- Default: `240` - -**Options** - -`float` values - -### Auto Reveal Entries - -- Description: Whether to reveal it in the project panel automatically, when a corresponding project entry becomes active. Gitignored entries are never auto revealed. -- Setting: `auto_reveal_entries` -- Default: `true` - -**Options** - -1. Enable auto reveal entries - -```json [settings] -{ - "auto_reveal_entries": true -} -``` - -2. Disable auto reveal entries - -```json [settings] -{ - "auto_reveal_entries": false -} -``` - -### Auto Fold Dirs - -- Description: Whether to fold directories automatically when directory has only one directory inside. -- Setting: `auto_fold_dirs` -- Default: `true` - -**Options** - -1. Enable auto fold dirs - -```json [settings] -{ - "auto_fold_dirs": true -} -``` - -2. Disable auto fold dirs - -```json [settings] -{ - "auto_fold_dirs": false -} -``` - -### Indent Size - -- Description: Amount of indentation (in pixels) for nested items. -- Setting: `indent_size` -- Default: `20` - -### Indent Guides: Show - -- Description: Whether to show indent guides in the project panel. -- Setting: `indent_guides` -- Default: - -```json [settings] -"indent_guides": { - "show": "always" -} -``` - -**Options** - -1. Show indent guides in the project panel - -```json [settings] -{ - "indent_guides": { - "show": "always" - } -} -``` - -2. Hide indent guides in the project panel - -```json [settings] -{ - "indent_guides": { - "show": "never" - } -} -``` - -### Scrollbar: Show - -- Description: Whether to show a scrollbar in the project panel. Possible values: null, "auto", "system", "always", "never". Inherits editor settings when absent, see its description for more details. -- Setting: `scrollbar` -- Default: - -```json [settings] -"scrollbar": { - "show": null -} -``` - -**Options** - -1. Show scrollbar in the project panel - -```json [settings] -{ - "scrollbar": { - "show": "always" - } -} -``` - -2. Hide scrollbar in the project panel - -```json [settings] -{ - "scrollbar": { - "show": "never" - } -} -``` - -### Sort Mode - -- Description: Sort order for entries in the project panel -- Setting: `sort_mode` -- Default: `directories_first` - -**Options** - -1. Show directories first, then files - -```json [settings] -{ - "sort_mode": "directories_first" -} -``` - -2. Mix directories and files together - -```json [settings] -{ - "sort_mode": "mixed" -} -``` - -3. Show files first, then directories - -```json [settings] -{ - "sort_mode": "files_first" -} -``` - -### Auto Open - -- Description: Control whether files are opened automatically after different creation flows in the project panel. -- Setting: `auto_open` -- Default: - -```json [settings] -"auto_open": { - "on_create": true, - "on_paste": true, - "on_drop": true -} -``` - -**Options** - -- `on_create`: Whether to automatically open newly created files in the editor. -- `on_paste`: Whether to automatically open files after pasting or duplicating them. -- `on_drop`: Whether to automatically open files dropped from external sources. - -## Agent - -Visit [the Configuration page](./ai/configuration.md) under the AI section to learn more about all the agent-related settings. - -## Collaboration Panel - -- Description: Customizations for the collaboration panel. -- Setting: `collaboration_panel` -- Default: - -```json [settings] -{ - "collaboration_panel": { - "button": true, - "dock": "left", - "default_width": 240 - } -} -``` - -**Options** - -- `button`: Whether to show the collaboration panel button in the status bar -- `dock`: Where to dock the collaboration panel. Can be `left` or `right` -- `default_width`: Default width of the collaboration panel - -## Debugger - -- Description: Configuration for debugger panel and settings -- Setting: `debugger` -- Default: - -```json [settings] -{ - "debugger": { - "stepping_granularity": "line", - "save_breakpoints": true, - "dock": "bottom", - "button": true - } -} -``` - -See the [debugger page](./debugger.md) for more information about debugging support within Zed. - -## Git Panel - -- Description: Setting to customize the behavior of the git panel. -- Setting: `git_panel` -- Default: - -```json [settings] -{ - "git_panel": { - "button": true, - "dock": "left", - "default_width": 360, - "status_style": "icon", - "fallback_branch_name": "main", - "sort_by_path": false, - "collapse_untracked_diff": false, - "scrollbar": { - "show": null - } - } -} -``` - -**Options** - -- `button`: Whether to show the git panel button in the status bar -- `dock`: Where to dock the git panel. Can be `left` or `right` -- `default_width`: Default width of the git panel -- `status_style`: How to display git status. Can be `label_color` or `icon` -- `fallback_branch_name`: What branch name to use if `init.defaultBranch` is not set -- `sort_by_path`: Whether to sort entries in the panel by path or by status (the default) -- `collapse_untracked_diff`: Whether to collapse untracked files in the diff panel -- `scrollbar`: When to show the scrollbar in the git panel - -## Git Hosting Providers - -- Description: Register self-hosted GitHub, GitLab, or Bitbucket instances so commit hashes, issue references, and permalinks resolve to the right host. -- Setting: `git_hosting_providers` -- Default: `[]` - -**Options** - -Each entry accepts: - -- `provider`: One of `github`, `gitlab`, or `bitbucket` -- `name`: Display name for the instance -- `base_url`: Base URL, e.g. `https://git.example.corp` - -You can define these in user or project settings; project settings are merged on top of user settings. - -```json [settings] -{ - "git_hosting_providers": [ - { - "provider": "github", - "name": "BigCorp GitHub", - "base_url": "https://git.example.corp" - } - ] -} -``` - -## Outline Panel - -- Description: Customize outline Panel -- Setting: `outline_panel` -- Default: - -```json [settings] -"outline_panel": { - "button": true, - "default_width": 300, - "dock": "left", - "file_icons": true, - "folder_icons": true, - "git_status": true, - "indent_size": 20, - "auto_reveal_entries": true, - "auto_fold_dirs": true, - "indent_guides": { - "show": "always" - }, - "scrollbar": { - "show": null - } -} -``` - -## Calls - -- Description: Customize behavior when participating in a call -- Setting: `calls` -- Default: - -```json [settings] -"calls": { - // Join calls with the microphone live by default - "mute_on_join": false, - // Share your project when you are the first to join a channel - "share_on_join": false -}, -``` - -## Colorize Brackets - -- Description: Whether to use tree-sitter bracket queries to detect and colorize the brackets in the editor (also known as "rainbow brackets"). -- Setting: `colorize_brackets` -- Default: `false` - -**Options** - -`boolean` values - -The colors that are used for different indentation levels are defined in the theme (theme key: `accents`). They can be customized by using theme overrides. - -## Unnecessary Code Fade - -- Description: How much to fade out unused code. -- Setting: `unnecessary_code_fade` -- Default: `0.3` - -**Options** - -Float values between `0.0` and `0.9`, where: - -- `0.0` means no fading (unused code looks the same as used code) -- `0.9` means maximum fading (unused code is very faint but still visible) - -**Example** - -```json [settings] -{ - "unnecessary_code_fade": 0.5 -} -``` - -## UI Font Family - -- Description: The name of the font to use for text in the UI. -- Setting: `ui_font_family` -- Default: `.ZedSans`. This currently aliases to [IBM Plex](https://www.ibm.com/plex/). - -**Options** - -The name of any font family installed on the system, `".ZedSans"` to use the Zed-provided default, or `".SystemUIFont"` to use the system's default UI font (on macOS and Windows). - -## UI Font Features - -- Description: The OpenType features to enable for text in the UI. -- Setting: `ui_font_features` -- Default: - -```json [settings] -"ui_font_features": { - "calt": false -} -``` - -- Platform: macOS and Windows. - -**Options** - -Zed supports all OpenType features that can be enabled or disabled for a given UI font, as well as setting values for font features. - -For example, to disable font ligatures, add the following to your settings: - -```json [settings] -{ - "ui_font_features": { - "calt": false - } -} -``` - -You can also set other OpenType features, like setting `cv01` to `7`: - -```json [settings] -{ - "ui_font_features": { - "cv01": 7 - } -} -``` - -## UI Font Fallbacks - -- Description: The font fallbacks to use for text in the UI. -- Setting: `ui_font_fallbacks` -- Default: `null` -- Platform: macOS and Windows. - -**Options** - -For example, to use `Nerd Font` as a fallback, add the following to your settings: - -```json [settings] -{ - "ui_font_fallbacks": ["Nerd Font"] -} -``` - -## UI Font Size - -- Description: The default font size for text in the UI. -- Setting: `ui_font_size` -- Default: `16` - -**Options** - -`integer` values from `6` to `100` pixels (inclusive) - -## UI Font Weight - -- Description: The default font weight for text in the UI. -- Setting: `ui_font_weight` -- Default: `400` - -**Options** - -`integer` values between `100` and `900` - -## Settings Profiles - -- Description: Configure any number of settings profiles that are temporarily applied on top of your existing user settings when selected from `settings profile selector: toggle`. -- Setting: `profiles` -- Default: `{}` - -In your `settings.json` file, add the `profiles` object. -Each key within this object is the name of a settings profile, and each value is an object that can include any of Zed's settings. - -Example: - -```json [settings] -"profiles": { - "Presenting (Dark)": { - "agent_buffer_font_size": 18.0, - "buffer_font_size": 18.0, - "theme": "One Dark", - "ui_font_size": 18.0 - }, - "Presenting (Light)": { - "agent_buffer_font_size": 18.0, - "buffer_font_size": 18.0, - "theme": "One Light", - "ui_font_size": 18.0 - }, - "Writing": { - "agent_buffer_font_size": 15.0, - "buffer_font_size": 15.0, - "theme": "Catppuccin Frappé - No Italics", - "ui_font_size": 15.0, - "tab_bar": { "show": false }, - "toolbar": { "breadcrumbs": false } - } -} -``` - -To preview and enable a settings profile, open the command palette via {#kb command_palette::Toggle} and search for `settings profile selector: toggle`. - -## An example configuration: - -```json [settings] -// ~/.config/zed/settings.json -{ - "theme": "cave-light", - "tab_size": 2, - "preferred_line_length": 80, - "soft_wrap": "none", - - "buffer_font_size": 18, - "buffer_font_family": ".ZedMono", - - "autosave": "on_focus_change", - "format_on_save": "off", - "vim_mode": false, - "projects_online_by_default": true, - "terminal": { - "font_family": "FiraCode Nerd Font Mono", - "blinking": "off" - }, - "languages": { - "C": { - "format_on_save": "on", - "formatter": "language_server", - "preferred_line_length": 64, - "soft_wrap": "preferred_line_length" - } - } -} -``` diff --git a/docs/src/debugger.md b/docs/src/debugger.md deleted file mode 100644 index 15094be360..0000000000 --- a/docs/src/debugger.md +++ /dev/null @@ -1,370 +0,0 @@ -# Debugger - -Zed uses the [Debug Adapter Protocol (DAP)](https://microsoft.github.io/debug-adapter-protocol/) to provide debugging functionality across multiple programming languages. -DAP is a standardized protocol that defines how debuggers, editors, and IDEs communicate with each other. -It allows Zed to support various debuggers without needing to implement language-specific debugging logic. -Zed implements the client side of the protocol, and various _debug adapters_ implement the server side. - -This protocol enables features like setting breakpoints, stepping through code, inspecting variables, -and more, in a consistent manner across different programming languages and runtime environments. - -## Supported Languages - -To debug code written in a specific language, Zed needs to find a debug adapter for that language. Some debug adapters are provided by Zed without additional setup, and some are provided by [language extensions](./extensions/debugger-extensions.md). The following languages currently have debug adapters available: - - - -- [C](./languages/c.md#debugging) (built-in) -- [C++](./languages/cpp.md#debugging) (built-in) -- [Go](./languages/go.md#debugging) (built-in) -- [Java](./languages/java.md#debugging) (provided by extension) -- [JavaScript](./languages/javascript.md#debugging) (built-in) -- [PHP](./languages/php.md#debugging) (built-in) -- [Python](./languages/python.md#debugging) (built-in) -- [Ruby](./languages/ruby.md#debugging) (provided by extension) -- [Rust](./languages/rust.md#debugging) (built-in) -- [Swift](./languages/swift.md#debugging) (provided by extension) -- [TypeScript](./languages/typescript.md#debugging) (built-in) - -> If your language isn't listed, you can contribute by adding a debug adapter for it. Check out our [debugger extensions](./extensions/debugger-extensions.md) documentation for more information. - -Follow those links for language- and adapter-specific information and examples, or read on for more about Zed's general debugging features that apply to all adapters. - -## Getting Started - -For most languages, the fastest way to get started is to run {#action debugger::Start} ({#kb debugger::Start}). This opens the _new process modal_, which shows you a contextual list of preconfigured debug tasks for the current project. Debug tasks are created from tests, entry points (like a `main` function), and from other sources — consult the documentation for your language for full information about what's supported. - -You can open the same modal by clicking the "plus" button at the top right of the debug panel. - -For languages that don't provide preconfigured debug tasks (this includes C, C++, and some extension-supported languages), you can define debug configurations in the `.zed/debug.json` file in your project root. This file should be an array of configuration objects: - -```json [debug] -[ - { - "adapter": "CodeLLDB", - "label": "First configuration" - // ... - }, - { - "adapter": "Debugpy", - "label": "Second configuration" - // ... - } -] -``` - -Check the documentation for your language for example configurations covering typical use-cases. Once you've added configurations to `.zed/debug.json`, they'll appear in the list in the new process modal. - -Zed will also load debug configurations from `.vscode/launch.json`, and show them in the new process modal if no configurations are found in `.zed/debug.json`. - -#### Global debug configurations - -If you run the same launch profiles across multiple projects, you can store them once in your user configuration. Invoke {#action zed::OpenDebugTasks} from the command palette to open the global `debug.json` file; Zed creates it next to your user `settings.json` and keeps it in sync with the debugger UI. The file lives at: - -- **macOS:** `~/Library/Application Support/Zed/debug.json` -- **Linux/BSD:** `$XDG_CONFIG_HOME/zed/debug.json` (falls back to `~/.config/zed/debug.json`) -- **Windows:** `%APPDATA%\Zed\debug.json` - -Populate this file with the same array of objects you would place in `.zed/debug.json`. Any scenarios defined there are merged into every workspace, so your favorite launch presets appear automatically in the "New Debug Session" dialog. - -### Launching & Attaching - -Zed debugger offers two ways to debug your program; you can either _launch_ a new instance of your program or _attach_ to an existing process. -Which one you choose depends on what you are trying to achieve. - -When launching a new instance, Zed (and the underlying debug adapter) can often do a better job at picking up the debug information compared to attaching to an existing process, since it controls the lifetime of a whole program. -Running unit tests or a debug build of your application is a good use case for launching. - -Compared to launching, attaching to an existing process might seem inferior, but that's far from truth; there are cases where you cannot afford to restart your program, because for example, the bug is not reproducible outside of a production environment or some other circumstances. - -## Configuration - -Zed requires the `adapter` and `label` fields for all debug tasks. In addition, Zed will use the `build` field to run any necessary setup steps before the debugger starts [(see below)](#build-tasks), and can accept a `tcp_connection` field to connect to an existing process. - -All other fields are provided by the debug adapter and can contain [task variables](./tasks.md#variables). Most adapters support `request`, `program`, and `cwd`: - -```json [debug] -[ - { - // The label for the debug configuration and used to identify the debug session inside the debug panel & new process modal - "label": "Example Start debugger config", - // The debug adapter that Zed should use to debug the program - "adapter": "Example adapter name", - // Request: - // - launch: Zed will launch the program if specified, or show a debug terminal with the right configuration - // - attach: Zed will attach to a running program to debug it, or when the process_id is not specified, will show a process picker (only supported for node currently) - "request": "launch", - // The program to debug. This field supports path resolution with ~ or . symbols. - "program": "path_to_program", - // cwd: defaults to the current working directory of your project ($ZED_WORKTREE_ROOT) - "cwd": "$ZED_WORKTREE_ROOT" - } -] -``` - -Check your debug adapter's documentation for more information on the fields it supports. - -### Build tasks - -Zed allows embedding a Zed task in the `build` field that is run before the debugger starts. This is useful for setting up the environment or running any necessary setup steps before the debugger starts. - -```json [debug] -[ - { - "label": "Build Binary", - "adapter": "CodeLLDB", - "program": "path_to_program", - "request": "launch", - "build": { - "command": "make", - "args": ["build", "-j8"] - } - } -] -``` - -Build tasks can also refer to the existing tasks by unsubstituted label: - -```json [debug] -[ - { - "label": "Build Binary", - "adapter": "CodeLLDB", - "program": "path_to_program", - "request": "launch", - "build": "my build task" // Or "my build task for $ZED_FILE" - } -] -``` - -### Automatic scenario creation - -Given a Zed task, Zed can automatically create a scenario for you. Automatic scenario creation also powers our scenario creation from gutter. -Automatic scenario creation is currently supported for Rust, Go, Python, JavaScript, and TypeScript. - -## Breakpoints - -To set a breakpoint, simply click next to the line number in the editor gutter. -Breakpoints can be tweaked depending on your needs; to access additional options of a given breakpoint, right-click on the breakpoint icon in the gutter and select the desired option. -At present, you can: - -- Add a log to a breakpoint, which will output a log message whenever that breakpoint is hit. -- Make the breakpoint conditional, which will only stop at the breakpoint when the condition is met. The syntax for conditions is adapter-specific. -- Add a hit count to a breakpoint, which will only stop at the breakpoint after it's hit a certain number of times. -- Disable a breakpoint, which will prevent it from being hit while leaving it visible in the gutter. - -Some debug adapters (e.g. CodeLLDB and JavaScript) will also _verify_ whether your breakpoints can be hit; breakpoints that cannot be hit are surfaced more prominently in the UI. - -All breakpoints enabled for a given project are also listed in "Breakpoints" item in your debugging session UI. From "Breakpoints" item in your UI you can also manage exception breakpoints. -The debug adapter will then stop whenever an exception of a given kind occurs. Which exception types are supported depends on the debug adapter. - -## Settings - -The settings for the debugger are grouped under the `debugger` key in `settings.json`: - -- `dock`: Determines the position of the debug panel in the UI. -- `stepping_granularity`: Determines the stepping granularity. -- `save_breakpoints`: Whether the breakpoints should be reused across Zed sessions. -- `button`: Whether to show the debug button in the status bar. -- `timeout`: Time in milliseconds until timeout error when connecting to a TCP debug adapter. -- `log_dap_communications`: Whether to log messages between active debug adapters and Zed. -- `format_dap_log_messages`: Whether to format DAP messages when adding them to the debug adapter logger. - -### Dock - -- Description: The position of the debug panel in the UI. -- Default: `bottom` -- Setting: debugger.dock - -**Options** - -1. `left` - The debug panel will be docked to the left side of the UI. -2. `right` - The debug panel will be docked to the right side of the UI. -3. `bottom` - The debug panel will be docked to the bottom of the UI. - -```json [settings] -"debugger": { - "dock": "bottom" -}, -``` - -### Stepping granularity - -- Description: The Step granularity that the debugger will use -- Default: `line` -- Setting: `debugger.stepping_granularity` - -**Options** - -1. Statement - The step should allow the program to run until the current statement has finished executing. - The meaning of a statement is determined by the adapter and it may be considered equivalent to a line. - For example 'for(int i = 0; i < 10; i++)' could be considered to have 3 statements 'int i = 0', 'i < 10', and 'i++'. - -```json [settings] -{ - "debugger": { - "stepping_granularity": "statement" - } -} -``` - -2. Line - The step should allow the program to run until the current source line has executed. - -```json [settings] -{ - "debugger": { - "stepping_granularity": "line" - } -} -``` - -3. Instruction - The step should allow one instruction to execute (e.g. one x86 instruction). - -```json [settings] -{ - "debugger": { - "stepping_granularity": "instruction" - } -} -``` - -### Save Breakpoints - -- Description: Whether the breakpoints should be saved across Zed sessions. -- Default: `true` -- Setting: `debugger.save_breakpoints` - -**Options** - -`boolean` values - -```json [settings] -{ - "debugger": { - "save_breakpoints": true - } -} -``` - -### Button - -- Description: Whether the button should be displayed in the debugger toolbar. -- Default: `true` -- Setting: `debugger.show_button` - -**Options** - -`boolean` values - -```json [settings] -{ - "debugger": { - "show_button": true - } -} -``` - -### Timeout - -- Description: Time in milliseconds until timeout error when connecting to a TCP debug adapter. -- Default: `2000` -- Setting: `debugger.timeout` - -**Options** - -`integer` values - -```json [settings] -{ - "debugger": { - "timeout": 3000 - } -} -``` - -### Inline Values - -- Description: Whether to enable editor inlay hints showing the values of variables in your code during debugging sessions. -- Default: `true` -- Setting: `inlay_hints.show_value_hints` - -**Options** - -```json [settings] -{ - "inlay_hints": { - "show_value_hints": false - } -} -``` - -Inline value hints can also be toggled from the Editor Controls menu in the editor toolbar. - -### Log Dap Communications - -- Description: Whether to log messages between active debug adapters and Zed. (Used for DAP development) -- Default: false -- Setting: debugger.log_dap_communications - -**Options** - -`boolean` values - -```json [settings] -{ - "debugger": { - "log_dap_communications": true - } -} -``` - -### Format Dap Log Messages - -- Description: Whether to format DAP messages when adding them to the debug adapter logger. (Used for DAP development) -- Default: false -- Setting: debugger.format_dap_log_messages - -**Options** - -`boolean` values - -```json [settings] -{ - "debugger": { - "format_dap_log_messages": true - } -} -``` - -### Customizing Debug Adapters - -- Description: Custom program path and arguments to override how Zed launches a specific debug adapter. -- Default: Adapter-specific -- Setting: `dap.$ADAPTER.binary` and `dap.$ADAPTER.args` - -You can pass `binary`, `args`, or both. `binary` should be a path to a _debug adapter_ (like `lldb-dap`) not a _debugger_ (like `lldb` itself). The `args` setting overrides any arguments that Zed would otherwise pass to the adapter. - -```json [settings] -{ - "dap": { - "CodeLLDB": { - "binary": "/Users/name/bin/lldb-dap", - "args": ["--wait-for-debugger"] - } - } -} -``` - -## Theme - -The Debugger supports the following theme options: - -- `debugger.accent`: Color used to accent breakpoint & breakpoint-related symbols -- `editor.debugger_active_line.background`: Background color of active debug line - -## Troubleshooting - -If you're running into problems with the debugger, please [open a GitHub issue](https://github.com/zed-industries/zed/issues/new?template=04_bug_debugger.yml), providing as much context as possible. There are also some features you can use to gather more information about the problem: - -- When you have a session running in the debug panel, you can run the {#action dev::CopyDebugAdapterArguments} action to copy a JSON blob to the clipboard that describes how Zed initialized the session. This is especially useful when the session failed to start, and is great context to add if you open a GitHub issue. -- You can also use the {#action dev::OpenDebugAdapterLogs} action to see a trace of all of Zed's communications with debug adapters during the most recent debug sessions. diff --git a/docs/src/development.md b/docs/src/development.md deleted file mode 100644 index 31bb245ac4..0000000000 --- a/docs/src/development.md +++ /dev/null @@ -1,93 +0,0 @@ -# Developing Zed - -See the platform-specific instructions for building Zed from source: - -- [macOS](./development/macos.md) -- [Linux](./development/linux.md) -- [Windows](./development/windows.md) - -If you'd like to develop collaboration features, additionally see: - -- [Local Collaboration](./development/local-collaboration.md) - -## Keychain access - -Zed stores secrets in the system keychain. - -However, when running a development build of Zed on macOS (and perhaps other -platforms) trying to access the keychain results in a lot of keychain prompts -that require entering your password over and over. - -On macOS this is caused by the development build not having a stable identity. -Even if you choose the "Always Allow" option, the OS will still prompt you for -your password again the next time something changes in the binary. - -This quickly becomes annoying and impedes development speed. - -That is why, by default, when running a development build of Zed an alternative -credential provider is used in order to bypass the system keychain. - -> Note: This is **only** the case for development builds. For all non-development -> release channels the system keychain is always used. - -If you need to test something out using the real system keychain in a -development build, run Zed with the following environment variable set: - -``` -ZED_DEVELOPMENT_USE_KEYCHAIN=1 -``` - -## Performance Measurements - -Zed includes a frame time measurement system that can be used to profile how long it takes to render each frame. This is particularly useful when comparing rendering performance between different versions or when optimizing frame rendering code. - -### Using ZED_MEASUREMENTS - -To enable performance measurements, set the `ZED_MEASUREMENTS` environment variable: - -```sh -export ZED_MEASUREMENTS=1 -``` - -When enabled, Zed will print frame rendering timing information to stderr, showing how long each frame takes to render. - -### Performance Comparison Workflow - -Here's a typical workflow for comparing frame rendering performance between different versions: - -1. **Enable measurements:** - - ```sh - export ZED_MEASUREMENTS=1 - ``` - -2. **Test the first version:** - - - Checkout the commit you want to measure - - Run Zed in release mode and use it for 5-10 seconds: `cargo run --release &> version-a` - -3. **Test the second version:** - - - Checkout another commit you want to compare - - Run Zed in release mode and use it for 5-10 seconds: `cargo run --release &> version-b` - -4. **Generate comparison:** - - ```sh - script/histogram version-a version-b - ``` - -The `script/histogram` tool can accept as many measurement files as you like and will generate a histogram visualization comparing the frame rendering performance data between the provided versions. - -### Using `util_macros::perf` - -For benchmarking unit tests, annotate them with the `#[perf]` attribute from the `util_macros` crate. Then run `cargo -perf-test -p $CRATE` to benchmark them. See the rustdoc documentation on `crates/util_macros` and `tooling/perf` for -in-depth examples and explanations. - -## Contributor links - -- [CONTRIBUTING.md](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md) -- [Debugging Crashes](./development/debugging-crashes.md) -- [Code of Conduct](https://zed.dev/code-of-conduct) -- [Zed Contributor License](https://zed.dev/cla) diff --git a/docs/src/development/debuggers.md b/docs/src/development/debuggers.md deleted file mode 100644 index 11f49390d4..0000000000 --- a/docs/src/development/debuggers.md +++ /dev/null @@ -1,113 +0,0 @@ -# Using a debugger - -> **DISCLAIMER**: This is not documentation for [configuring Zed's debugger](../debugger.md). -> Rather, it is intended to provide information on how to use a debugger while developing Zed itself to both Zed employees and external contributors. - -## Using Zed's built-in debugger - -While the Zed project is open you can open the `New Process Modal` and select the `Debug` tab. There you can see two debug configurations to debug Zed with, one for GDB and one for LLDB. Select the configuration you want and Zed will build and launch the binary. - -Please note, GDB isn't supported on arm Macbooks - -## Release build profile considerations - -By default, builds using the release profile (release is the profile used for production builds, i.e. nightly, preview, and stable) include limited debug info. - -This is done by setting the `profile.(release).debug` field in the root `Cargo.toml` field to `"limited"`. - -The official documentation for the `debug` field can be found [here](https://doc.rust-lang.org/cargo/reference/profiles.html#debug). -But the TLDR is that `"limited"` strips type and variable level debug info. - -In release builds, this is done to reduce the binary size, as type and variable level debug info is not required, and does not impact the usability of generated stack traces. - -However, while the type and variable level debug info is not required for good stack traces, it is very important for a good experience using debuggers, -as without the type and variable level debug info, the debugger has no way to resolve local variables, inspect them, format them using pretty-printers, etc. - -Therefore, in order to use a debugger to it's fullest extent when debugging a release build, you must compile a new Zed binary, with full debug info. - -The simplest way to do this, is to use the `--config` flag to override the `debug` field in the root `Cargo.toml` file when running `cargo run` or `cargo build` like so: - -```sh -cargo run --config 'profile.release.debug="full"' -cargo build --config 'profile.release.debug="full"' -``` - -> If you wish to avoid passing the `--config` flag on every invocation of `cargo`. You may also change the section in the [root `Cargo.toml`](https://github.com/zed-industries/zed/blob/main/Cargo.toml) -> -> from -> -> ```toml -> [profile.release] -> debug = "limited" -> ``` -> -> to -> -> ```toml -> [profile.release] -> debug = "full" -> ``` -> -> This will ensure all invocations of `cargo run --release` or `cargo build --release` will compile with full debug info. -> -> **WARNING:** Make sure to avoid committing these changes! - -## Running Zed with a shell debugger GDB/LLDB - -### Background - -When installing rust through rustup, (the recommended way to do so when developing Zed, see the documentation for getting started on your platform [here](../development.md)) -a few additional scripts are installed and put on your path to assist with debugging binaries compiled with rust. - -These are `rust-gdb` and `rust-lldb` respectively. - -You can read more information about these scripts and why they are useful [here](https://michaelwoerister.github.io/2015/03/27/rust-xxdb.html) if you are interested. - -However, the summary is that they are simple shell scripts that wrap the standard `gdb` and `lldb` commands, injecting the relevant commands and flags to enable additional -rust-specific features such as pretty-printers and type information. - -Therefore, in order to use `rust-gdb` or `rust-lldb`, you must have `gdb` or `lldb` installed on your system. If you don't have them installed, you will need to install them in a manner appropriate for your platform. - -According to the [previously linked article](https://michaelwoerister.github.io/2015/03/27/rust-xxdb.html), "The minimum supported debugger versions are GDB 7.7 and LLDB 310. However, the general rule is: the newer the better." Therefore, it is recommended to install the latest version of `gdb` or `lldb` if possible. - -> **Note**: `rust-gdb` is not installed by default on Windows, as `gdb` support for windows is not very stable. It is recommended to use `lldb` with `rust-lldb` instead on Windows. - -If you are unfamiliar with `gdb` or `lldb`, you can learn more about them [here](https://www.gnu.org/software/gdb/) and [here](https://lldb.llvm.org/) respectively. - -### Usage with Zed - -After following the steps above for including full debug info when compiling Zed, -You can either run `rust-gdb` or `rust-lldb` on the compiled Zed binary after building it with `cargo build`, by running one of the following commands: - -``` -rust-gdb target/debug/zed -rust-lldb target/debug/zed -``` - -Alternatively, you can attach to a running instance of Zed (such as an instance of Zed started using `cargo run`) by running one of the following commands: - -``` -rust-gdb -p -rust-lldb -p -``` - -Where `` is the process ID of the Zed instance you want to attach to. - -To get the process ID of a running Zed instance, you can use your systems process management tools such as `Task Manager` on windows or `Activity Monitor` on macOS. - -Alternatively, you can run the `ps aux | grep zed` command on macOS and Linux or `Get-Process | Select-Object Id, ProcessName` in an instance of PowerShell on Windows. - -#### Debugging Panics and Crashes - -Debuggers can be an excellent tool for debugging the cause of panics and crashes in all programs, including Zed. - -By default, when a process that `gdb` or `lldb` is attached to hits an exception such as a panic, the debugger will automatically stop at the point of the panic and allow you to inspect the state of the program. - -Most likely, the point at which the debugger stops will be deep in the rust standard library panic or exception handling code, so you will need to navigate up the stack trace to find the actual cause of the panic. - -This can be accomplished using the `backtrace` command in combination with the `frame select` command in `lldb`, with similar commands available in `gdb`. - -Once the program is stopped, you will not be able to continue execution as you can before an exception is hit. However, you can jump around to different stack frames, and inspect the values of variables and expressions -within each frame, which can be very useful in identifying the root cause of the crash. - -You can find additional information on debugging Zed crashes [here](./debugging-crashes.md). diff --git a/docs/src/development/debugging-crashes.md b/docs/src/development/debugging-crashes.md deleted file mode 100644 index 9da3f88066..0000000000 --- a/docs/src/development/debugging-crashes.md +++ /dev/null @@ -1,20 +0,0 @@ -# Debugging Crashes - -When Zed panics or otherwise crashes, Zed sends a message to a sidecar process which inspects the memory of the crashing editor to create a [minidump](https://chromium.googlesource.com/breakpad/breakpad/+/master/docs/getting_started_with_breakpad.md#the-minidump-file-format) file in `~/Library/Logs/Zed` or `$XDG_DATA_HOME/zed/logs`. This minidump can be used to generate backtraces for the stacks of all threads. - -If you have enabled Zed's telemetry these will be uploaded to us when you restart the app. They end up in a [Slack channel](https://zed-industries.slack.com/archives/C0977J9MA1Y) and in [Sentry](https://zed-dev.sentry.io/issues) (both of which are Zed-staff-only). - -These crash reports contain rich information; but they are hard to read because they don't contain spans or symbol information. You can still work with them locally by downloading sources and an unstripped binary (or separate symbols file) for your Zed release and running: - -```sh -zstd -d ~/.local/share/zed/.dmp -o minidump.dmp -minidump-stackwalk minidump.dmp -``` - -Alongside the minidump file in your logs dir, there should be a `.json` which contains additional metadata like the panic message, span, and system specs. - -## Using a Debugger - -If you can reproduce the crash consistently, a debugger can be used to inspect the state of the program at the time of the crash, often providing useful insights into the cause of the crash. - -You can read more about setting up and using a debugger with Zed, and specifically for debugging crashes [here](./debuggers.md#debugging-panics-and-crashes) diff --git a/docs/src/development/freebsd.md b/docs/src/development/freebsd.md deleted file mode 100644 index 199e653a65..0000000000 --- a/docs/src/development/freebsd.md +++ /dev/null @@ -1,51 +0,0 @@ -# Building Zed for FreeBSD - -Note, FreeBSD is not currently a supported platform, and so this is a work-in-progress. - -## Repository - -Clone the [Zed repository](https://github.com/zed-industries/zed). - -## Dependencies - -- Install the necessary system packages and rustup: - - ```sh - script/freebsd - ``` - - If preferred, you can inspect [`script/freebsd`](https://github.com/zed-industries/zed/blob/main/script/freebsd) and perform the steps manually. - -## Building from source - -Once the dependencies are installed, you can build Zed using [Cargo](https://doc.rust-lang.org/cargo/). - -For a debug build of the editor: - -```sh -cargo run -``` - -And to run the tests: - -```sh -cargo test --workspace -``` - -In release mode, the primary user interface is the `cli` crate. You can run it in development with: - -```sh -cargo run -p cli -``` - -### WebRTC Notice - -Currently, building `webrtc-sys` on FreeBSD fails due to missing upstream support and unavailable prebuilt binaries. As a result, some collaboration features (audio calls and screensharing) that depend on WebRTC are temporarily disabled. - -See [Issue #15309: FreeBSD Support] and [Discussion #29550: Unofficial FreeBSD port for Zed] for more. - -## Troubleshooting - -### Cargo errors claiming that a dependency is using unstable features - -Try `cargo clean` and `cargo build`. diff --git a/docs/src/development/glossary.md b/docs/src/development/glossary.md deleted file mode 100644 index 34172ec9a5..0000000000 --- a/docs/src/development/glossary.md +++ /dev/null @@ -1,118 +0,0 @@ -# Zed Development: Glossary - -These are some terms and structures frequently used throughout the zed codebase. - -This is a best effort list and a work in progress. - - - -## Naming conventions - -These are generally true for the whole codebase. Note that Name can be anything -here. An example would be `AnyElement` and `LspStore`. - -- `AnyName`: A type erased version of _name_. Think `Box`. -- `NameStore`: A wrapper type which abstracts over whether operations are running locally or on a remote. - -## GPUI - -### State management - -- `App`: A singleton which holds the full application state including all the entities. Crucially: `App` is not `Send`, which means that `App` only exists on the thread that created it (which is the main/UI thread, usually). Thus, if you see a `&mut App`, know that you're on UI thread. -- `Context`: A wrapper around the `App` struct with specialized behavior for a specific `Entity`. Think of it as `(&mut App, Entity)`. The specialized behavior is surfaced in the API surface of `Context`. E.g., `App::spawn` takes an `AsyncFnOnce(AsyncApp) -> Ret`, whereas `Context::spawn` takes an `AsyncFnOnce(WeakEntity, AsyncApp) -> Ret`. -- `AsyncApp`: An owned version of `App` for use in async contexts. This type is _still_ not `Send` (so `AsyncApp` = you're on the main thread) and any use of it may be fallible (to account for the fact that the `App` might've been terminated by the time this closure runs). - The convenience of `AsyncApp` lies in the fact that you usually interface with `App` via `&mut App`, which would be inconvenient to use with async closures; `AsyncApp` is owned, so you can use it in async closures with no sweat. -- `AppContext` A trait which abstracts over `App`, `AsyncApp` & `Context` and their Test versions. -- `Task`: A future running or scheduled to run on the background or foreground - executor. In contradiction to regular Futures Tasks do not need `.await` to start running. You do need to await them to get the result of the task. -- `Executor`: Used to spawn tasks that run either on the foreground or background thread. Try to run the tasks on the background thread. - - `BackgroundExecutor`: A threadpool running `Task`s. - - `ForegroundExecutor`: The main thread running `Task`s. -- `Entity`: A strong, well-typed reference to a struct which is managed by gpui. Effectively a pointer/map key into the `App::EntityMap`. -- `WeakEntity`: A runtime checked reference to an `Entity` which may no longer exist. Similar to [`std::rc::Weak`](https://doc.rust-lang.org/std/rc/struct.Weak.html). -- `Global`: A singleton type which has only one value, that is stored in the `App`. -- `Event`: A datatype which can be send by an `Entity` to subscribers -- `Action`: An event that represents a user's keyboard input that can be handled by listeners - Example: `file finder: toggle` -- `Observing`: reacting entities notifying they've changed -- `Subscription`: An event handler that is used to react to the changes of state in the application. - 1. Emitted event handling - 2. Observing `{new,release,on notify}` of an entity - -### UI - -- `View`: An `Entity` which can produce an `Element` through its implementation of `Render`. -- `Element`: A type that can be laid out and painted to the screen. -- `element expression`: An expression that builds an element tree, example: - -```rust -h_flex() - .id(text[i]) - .relative() - .when(selected, |this| { - this.child( - div() - .h_4() - .absolute() - etc etc -``` - -- `Component`: A builder which can be rendered turning it into an `Element`. -- `Dispatch tree`: TODO -- `Focus`: The place where keystrokes are handled first -- `Focus tree`: Path from the place that has the current focus to the UI Root. Example TODO - -## Zed UI - -- `Window`: A struct in zed representing a zed window in your desktop environment (see image below). There can be multiple if you have multiple zed instances open. Mostly passed around for rendering. -- `Modal`: A UI element that floats on top of the rest of the UI -- `Picker`: A struct representing a list of items in floating on top of the UI (Modal). You can select an item and confirm. What happens on select or confirm is determined by the picker's delegate. (The 'Model' in the image below is a picker.) -- `PickerDelegate`: A trait used to specialize behavior for a `Picker`. The `Picker` stores the `PickerDelegate` in the field delegate. -- `Center`: The middle of the zed window, the center is split into multiple `Pane`s. In the codebase this is a field on the `Workspace` struct. (see image below). -- `Pane`: An area in the `Center` where we can place items, such as an editor, multi-buffer or terminal (see image below). -- `Panel`: An `Entity` implementing the `Panel` trait. These can be placed in a `Dock`. In the image below we see the: `ProjectPanel` in the left dock, the `DebugPanel` in the bottom dock, and `AgentPanel` in the right dock. Note `Editor` does not implement `Panel` and hence is not a `Panel`. -- `Dock`: A UI element similar to a `Pane` which can be opened and hidden. There can be up to 3 docks open at a time, left right and below the center. A dock contains one or more `Panel`s not `Pane`s. (see image). - -Screenshot for the Pane and Dock features - -- `Project`: One or more `Worktree`s -- `Worktree`: Represents either local or remote files. - -Screenshot for the Worktree feature - -- [Multibuffer](https://zed.dev/docs/multibuffers): A list of Editors, a multi-buffer allows editing multiple files simultaneously. A multi-buffer opens when an operation in Zed returns multiple locations, examples: _search_ or _go to definition_. See project search in the image below. - -Screenshot for the MultiBuffer feature - -## Editor - -- `Editor`: _The_ text editor, nearly everything in zed is an `Editor`, even single line inputs. Each pane in the image above contains one or more `Editor` instances. -- `Workspace`: The root of the window -- `Entry`: A file, dir, pending dir or unloaded dir. -- `Buffer`: The in-memory representation of a 'file' together with relevant data such as syntax trees, git status and diagnostics. -- `pending selection`: You have mouse down and you're dragging but you have not yet released. - -## Collab - -- `Collab session`: Multiple users working in a shared `Project` -- `Upstream client`: The zed client which has shared their workspace -- `Downstream client`: The zed client joining a shared workspace - -## Debugger - -- `DapStore`: Is an entity that manages debugger sessions -- `debugger::Session`: Is an entity that manages the lifecycle of a debug session and communication with DAPS -- `BreakpointStore`: Is an entity that manages breakpoints states in local and remote instances of Zed -- `DebugSession`: Manages a debug session's UI and running state -- `RunningState`: Directly manages all the views of a debug session -- `VariableList`: The variable and watch list view of a debug session -- `Console`: TODO -- `Terminal`: TODO -- `BreakpointList`: TODO diff --git a/docs/src/development/linux.md b/docs/src/development/linux.md deleted file mode 100644 index df3b840fa1..0000000000 --- a/docs/src/development/linux.md +++ /dev/null @@ -1,224 +0,0 @@ -# Building Zed for Linux - -## Repository - -Clone down the [Zed repository](https://github.com/zed-industries/zed). - -## Dependencies - -- Install [rustup](https://www.rust-lang.org/tools/install) - -- Install the necessary system libraries: - - ```sh - script/linux - ``` - - If you prefer to install the system libraries manually, you can find the list of required packages in the `script/linux` file. - -### Backend Dependencies (optional) {#backend-dependencies} - -If you are looking to develop Zed collaboration features using a local collaboration server, please see: [Local Collaboration](./local-collaboration.md) docs. - -### Linkers {#linker} - -On Linux, Rust's default linker is [LLVM's `lld`](https://blog.rust-lang.org/2025/09/18/Rust-1.90.0/). Alternative linkers, especially [Wild](https://github.com/davidlattimore/wild) and [Mold](https://github.com/rui314/mold) can significantly improve clean and incremental build time. - -At present Zed uses Mold in CI because it's more mature. For local development Wild is recommended because it's 5-20% faster than Mold. - -These linkers can be installed with `script/install-mold` and `script/install-wild`. - -To use Wild as your default, add these lines to your `~/.cargo/config.toml`: - -```toml -[target.x86_64-unknown-linux-gnu] -linker = "clang" -rustflags = ["-C", "link-arg=--ld-path=wild"] - -[target.aarch64-unknown-linux-gnu] -linker = "clang" -rustflags = ["-C", "link-arg=--ld-path=wild"] -``` - -To use Mold as your default: - -```toml -[target.'cfg(target_os = "linux")'] -rustflags = ["-C", "link-arg=-fuse-ld=mold"] -``` - -## Building from source - -Once the dependencies are installed, you can build Zed using [Cargo](https://doc.rust-lang.org/cargo/). - -For a debug build of the editor: - -```sh -cargo run -``` - -And to run the tests: - -```sh -cargo test --workspace -``` - -In release mode, the primary user interface is the `cli` crate. You can run it in development with: - -```sh -cargo run -p cli -``` - -## Installing a development build - -You can install a local build on your machine with: - -```sh -./script/install-linux -``` - -This will build zed and the cli in release mode and make them available at `~/.local/bin/zed`, installing .desktop files to `~/.local/share`. - -> **_Note_**: If you encounter linker errors similar to the following: -> -> ```bash -> error: linking with `cc` failed: exit status: 1 ... -> = note: /usr/bin/ld: /tmp/rustcISMaod/libaws_lc_sys-79f08eb6d32e546e.rlib(f8e4fd781484bd36-bcm.o): in function `aws_lc_0_25_0_handle_cpu_env': -> /aws-lc/crypto/fipsmodule/cpucap/cpu_intel.c:(.text.aws_lc_0_25_0_handle_cpu_env+0x63): undefined reference to `__isoc23_sscanf' -> /usr/bin/ld: /tmp/rustcISMaod/libaws_lc_sys-79f08eb6d32e546e.rlib(f8e4fd781484bd36-bcm.o): in function `pkey_rsa_ctrl_str': -> /aws-lc/crypto/fipsmodule/evp/p_rsa.c:741:(.text.pkey_rsa_ctrl_str+0x20d): undefined reference to `__isoc23_strtol' -> /usr/bin/ld: /aws-lc/crypto/fipsmodule/evp/p_rsa.c:752:(.text.pkey_rsa_ctrl_str+0x258): undefined reference to `__isoc23_strtol' -> collect2: error: ld returned 1 exit status -> = note: some `extern` functions couldn't be found; some native libraries may need to be installed or have their path specified -> = note: use the `-l` flag to specify native libraries to link -> = note: use the `cargo:rustc-link-lib` directive to specify the native libraries to link with Cargo (see https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-link-lib) -> error: could not compile `remote_server` (bin "remote_server") due to 1 previous error -> ``` -> -> **Cause**: -> this is caused by known bugs in aws-lc-rs(doesn't support GCC >= 14): [FIPS fails to build with GCC >= 14](https://github.com/aws/aws-lc-rs/issues/569) -> & [GCC-14 - build failure for FIPS module](https://github.com/aws/aws-lc/issues/2010) -> -> You can refer to [linux: Linker error for remote_server when using script/install-linux](https://github.com/zed-industries/zed/issues/24880) for more information. -> -> **Workarounds**: -> Set the remote server target to `x86_64-unknown-linux-gnu` like so `export REMOTE_SERVER_TARGET=x86_64-unknown-linux-gnu; script/install-linux` - -## Wayland & X11 - -Zed supports both X11 and Wayland. By default, we pick whichever we can find at runtime. If you're on Wayland and want to run in X11 mode, use the environment variable `WAYLAND_DISPLAY=''`. - -## Notes for packaging Zed - -Thank you for taking on the task of packaging Zed! - -### Technical requirements - -Zed has two main binaries: - -- You will need to build `crates/cli` and make its binary available in `$PATH` with the name `zed`. -- You will need to build `crates/zed` and put it at `$PATH/to/cli/../../libexec/zed-editor`. For example, if you are going to put the cli at `~/.local/bin/zed` put zed at `~/.local/libexec/zed-editor`. As some linux distributions (notably Arch) discourage the use of `libexec`, you can also put this binary at `$PATH/to/cli/../../lib/zed/zed-editor` (e.g. `~/.local/lib/zed/zed-editor`) instead. -- If you are going to provide a `.desktop` file you can find a template in `crates/zed/resources/zed.desktop.in`, and use `envsubst` to populate it with the values required. This file should also be renamed to `$APP_ID.desktop` so that the file [follows the FreeDesktop standards](https://github.com/zed-industries/zed/issues/12707#issuecomment-2168742761). You should also make this desktop file executable (`chmod 755`). -- You will need to ensure that the necessary libraries are installed. You can get the current list by [inspecting the built binary](https://github.com/zed-industries/zed/blob/935cf542aebf55122ce6ed1c91d0fe8711970c82/script/bundle-linux#L65-L67) on your system. -- For an example of a complete build script, see [script/bundle-linux](https://github.com/zed-industries/zed/blob/935cf542aebf55122ce6ed1c91d0fe8711970c82/script/bundle-linux). -- You can disable Zed's auto updates and provide instructions for users who try to update Zed manually by building (or running) Zed with the environment variable `ZED_UPDATE_EXPLANATION`. For example: `ZED_UPDATE_EXPLANATION="Please use flatpak to update zed."`. -- Make sure to update the contents of the `crates/zed/RELEASE_CHANNEL` file to 'nightly', 'preview', or 'stable', with no newline. This will cause Zed to use the credentials manager to remember a user's login. - -### Other things to note - -At Zed, our priority has been to move fast and bring the latest technology to our users. We've long been frustrated at having software that is slow, out of date, or hard to configure, and so we've built our editor to those tastes. - -However, we realize that many distros have other priorities. We want to work with everyone to bring Zed to their favorite platforms. But there is a long way to go: - -- Zed is a fast-moving early-phase project. We typically release 2-3 builds per week to fix user-reported issues and release major features. -- There are a couple of other `zed` binaries that may be present on Linux systems ([1](https://openzfs.github.io/openzfs-docs/man/v2.2/8/zed.8.html), [2](https://zed.brimdata.io/docs/commands/zed)). If you want to rename our CLI binary because of these issues, we suggest `zedit`, `zeditor`, or `zed-cli`. -- Zed automatically installs the correct version of common developer tools in the same way as rustup/rbenv/pyenv, etc. We understand this is contentious, [see here](https://github.com/zed-industries/zed/issues/12589). -- We allow users to install extensions locally and from [zed-industries/extensions](https://github.com/zed-industries/extensions). These extensions may install further tooling as needed, such as language servers. In the long run, we would like to make this safer, [see here](https://github.com/zed-industries/zed/issues/12358). -- Zed connects to several online services by default (AI, telemetry, collaboration). AI and our telemetry can be disabled by your users with their zed settings or by patching our [default settings file](https://github.com/zed-industries/zed/blob/main/assets/settings/default.json). -- As a result of the above issues, zed currently does not play nice with sandboxes, [see here](https://github.com/zed-industries/zed/pull/12006#issuecomment-2130421220) - -## Flatpak - -> Zed's current Flatpak integration exits the sandbox on startup. Workflows that rely on Flatpak's sandboxing may not work as expected. - -To build & install the Flatpak package locally follow the steps below: - -1. Install Flatpak for your distribution as outlined [here](https://flathub.org/setup). -2. Run the `script/flatpak/deps` script to install the required dependencies. -3. Run `script/flatpak/bundle-flatpak`. -4. Now the package has been installed and has a bundle available at `target/release/{app-id}.flatpak`. - -## Memory profiling - -[`heaptrack`](https://github.com/KDE/heaptrack) is quite useful for diagnosing memory leaks. To install it: - -```sh -$ sudo apt install heaptrack heaptrack-gui -$ cargo install cargo-heaptrack -``` - -Then, to build and run Zed with the profiler attached: - -```sh -$ cargo heaptrack -b zed -``` - -When this zed instance is exited, terminal output will include a command to run `heaptrack_interpret` to convert the `*.raw.zst` profile to a `*.zst` file which can be passed to `heaptrack_gui` for viewing. - -## Perf recording - -How to get a flamegraph with resolved symbols from a running zed instance. Use -when zed is using a lot of CPU. Not useful for hangs. - -### During the incident - -- Find the PID (process ID) using: - `ps -eo size,pid,comm | grep zed | sort | head -n 1 | cut -d ' ' -f 2` - Or find the pid of the command zed-editor with the most ram usage in something - like htop/btop/top. - -- Install perf: - On Ubuntu (derivatives) run `sudo apt install linux-tools`. - -- Perf Record: - run `sudo perf record -p `, wait a few seconds to gather data then press Ctrl+C. You should now have a perf.data file - -- Make the output file user owned: - run `sudo chown $USER:$USER perf.data` - -- Get build info: - Run zed again and type `zed: about` in the command pallet to get the exact commit. - -The `data.perf` file can be send to zed together with the exact commit. - -### Later - -This can be done by Zed staff. - -- Build Zed with symbols: - Check out the commit found previously and modify `Cargo.toml`. - Apply the following diff then make a release build. - -```diff -[profile.release] --debug = "limited" -+debug = "full" -``` - -- Add the symbols to perf database: - `pref buildid-cache -v -a ` - -- Resolve the symbols from the db: - `perf inject -i perf.data -o perf_with_symbols.data` - -- Install flamegraph: - `cargo install cargo-flamegraph` - -- Render the flamegraph: - `flamegraph --perfdata perf_with_symbols.data` - -## Troubleshooting - -### Cargo errors claiming that a dependency is using unstable features - -Try `cargo clean` and `cargo build`. diff --git a/docs/src/development/local-collaboration.md b/docs/src/development/local-collaboration.md deleted file mode 100644 index 393c6f0bbf..0000000000 --- a/docs/src/development/local-collaboration.md +++ /dev/null @@ -1,207 +0,0 @@ -# Local Collaboration - -1. Ensure you have access to our cloud infrastructure. If you don't have access, you can't collaborate locally at this time. - -2. Make sure you've installed Zed's dependencies for your platform: - -- [macOS](#macos) -- [Linux](#linux) -- [Windows](#backend-windows) - -Note that `collab` can be compiled only with MSVC toolchain on Windows - -3. Clone down our cloud repository and follow the instructions in the cloud README - -4. Setup the local database for your platform: - -- [macOS & Linux](#database-unix) -- [Windows](#database-windows) - -5. Run collab: - -- [macOS & Linux](#run-collab-unix) -- [Windows](#run-collab-windows) - -## Backend Dependencies - -If you are developing collaborative features of Zed, you'll need to install the dependencies of zed's `collab` server: - -- PostgreSQL -- LiveKit -- Foreman - -You can install these dependencies natively or run them under Docker. - -### macOS - -1. Install [Postgres.app](https://postgresapp.com) or [postgresql via homebrew](https://formulae.brew.sh/formula/postgresql@15): - - ```sh - brew install postgresql@15 - ``` - -2. Install [Livekit](https://formulae.brew.sh/formula/livekit) and [Foreman](https://formulae.brew.sh/formula/foreman) - - ```sh - brew install livekit foreman - ``` - -- Follow the steps in the [collab README](https://github.com/zed-industries/zed/blob/main/crates/collab/README.md) to configure the Postgres database for integration tests - -Alternatively, if you have [Docker](https://www.docker.com/) installed you can bring up all the `collab` dependencies using Docker Compose. - -### Linux - -1. Install [Postgres](https://www.postgresql.org/download/linux/) - - ```sh - sudo apt-get install postgresql # Ubuntu/Debian - sudo pacman -S postgresql # Arch Linux - sudo dnf install postgresql postgresql-server # RHEL/Fedora - sudo zypper install postgresql postgresql-server # OpenSUSE - ``` - -2. Install [Livekit](https://github.com/livekit/livekit-cli) - - ```sh - curl -sSL https://get.livekit.io/cli | bash - ``` - -3. Install [Foreman](https://theforeman.org/manuals/3.15/quickstart_guide.html) - -### Windows {#backend-windows} - -> This section is still in development. The instructions are not yet complete. - -- Install [Postgres](https://www.postgresql.org/download/windows/) -- Install [Livekit](https://github.com/livekit/livekit), optionally you can add the `livekit-server` binary to your `PATH`. - -Alternatively, if you have [Docker](https://www.docker.com/) installed you can bring up all the `collab` dependencies using Docker Compose. - -### Docker {#Docker} - -If you have docker or podman available, you can run the backend dependencies inside containers with Docker Compose: - -```sh -docker compose up -d -``` - -## Database setup - -Before you can run the `collab` server locally, you'll need to set up a `zed` Postgres database. - -### On macOS and Linux {#database-unix} - -```sh -script/bootstrap -``` - -This script will set up the `zed` Postgres database, and populate it with some users. It requires internet access, because it fetches some users from the GitHub API. - -The script will seed the database with various content defined by: - -```sh -cat crates/collab/seed.default.json -``` - -To use a different set of admin users, you can create your own version of that json file and export the `SEED_PATH` environment variable. Note that the usernames listed in the admins list currently must correspond to valid GitHub users. - -```json [settings] -{ - "admins": ["admin1", "admin2"], - "channels": ["zed"] -} -``` - -### On Windows {#database-windows} - -```powershell -.\script\bootstrap.ps1 -``` - -## Testing collaborative features locally - -### On macOS and Linux {#run-collab-unix} - -Ensure that Postgres is configured and running, then run Zed's collaboration server and the `livekit` dev server: - -```sh -foreman start -# OR -docker compose up -``` - -Alternatively, if you're not testing voice and screenshare, you can just run `collab` and `cloud`, and not the `livekit` dev server: - -```sh -cargo run -p collab -- serve all -``` - -```sh -cd ../cloud; cargo make dev -``` - -In a new terminal, run two or more instances of Zed. - -```sh -script/zed-local -3 -``` - -This script starts one to four instances of Zed, depending on the `-2`, `-3` or `-4` flags. Each instance will be connected to the local `collab` server, signed in as a different user from `.admins.json` or `.admins.default.json`. - -### On Windows {#run-collab-windows} - -Since `foreman` is not available on Windows, you can run the following commands in separate terminals: - -```powershell -cargo run --package=collab -- serve all -``` - -If you have added the `livekit-server` binary to your `PATH`, you can run: - -```powershell -livekit-server --dev -``` - -Otherwise, - -```powershell -.\path\to\livekit-serve.exe --dev -``` - -You'll also need to start the cloud server: - -```powershell -cd ..\cloud; cargo make dev -``` - -In a new terminal, run two or more instances of Zed. - -```powershell -node .\script\zed-local -2 -``` - -Note that this requires `node.exe` to be in your `PATH`. - -## Running a local collab server - -> [!NOTE] -> Because of recent changes to our authentication system, Zed will not be able to authenticate itself with, and therefore use, a local collab server. - -If you want to run your own version of the zed collaboration service, you can, but note that this is still under development, and there is no support for authentication nor extensions. - -Configuration is done through environment variables. By default it will read the configuration from [`.env.toml`](https://github.com/zed-industries/zed/blob/main/crates/collab/.env.toml) and you should use that as a guide for setting this up. - -By default Zed assumes that the DATABASE_URL is a Postgres database, but you can make it use Sqlite by compiling with `--features sqlite` and using a sqlite DATABASE_URL with `?mode=rwc`. - -To authenticate you must first configure the server by creating a seed.json file that contains at a minimum your github handle. This will be used to create the user on demand. - -```json [settings] -{ - "admins": ["nathansobo"] -} -``` - -By default the collab server will seed the database when first creating it, but if you want to add more users you can explicitly reseed them with `SEED_PATH=./seed.json cargo run -p collab seed` - -Then when running the zed client you must specify two environment variables, `ZED_ADMIN_API_TOKEN` (which should match the value of `API_TOKEN` in .env.toml) and `ZED_IMPERSONATE` (which should match one of the users in your seed.json) diff --git a/docs/src/development/macos.md b/docs/src/development/macos.md deleted file mode 100644 index 9c99e5f8da..0000000000 --- a/docs/src/development/macos.md +++ /dev/null @@ -1,150 +0,0 @@ -# Building Zed for macOS - -## Repository - -Clone down the [Zed repository](https://github.com/zed-industries/zed). - -## Dependencies - -- Install [rustup](https://www.rust-lang.org/tools/install) - -- Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store, or from the [Apple Developer](https://developer.apple.com/download/all/) website. Note this requires a developer account. - -> Ensure you launch Xcode after installing, and install the macOS components, which is the default option. - -- Install [Xcode command line tools](https://developer.apple.com/xcode/resources/) - - ```sh - xcode-select --install - ``` - -- Ensure that the Xcode command line tools are using your newly installed copy of Xcode: - - ```sh - sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer - sudo xcodebuild -license accept - ``` - -- Install `cmake` (required by [a dependency](https://docs.rs/wasmtime-c-api-impl/latest/wasmtime_c_api/)) - - ```sh - brew install cmake - ``` - -### Backend Dependencies (optional) {#backend-dependencies} - -If you are looking to develop Zed collaboration features using a local collaboration server, please see: [Local Collaboration](./local-collaboration.md) docs. - -## Building Zed from Source - -Once you have the dependencies installed, you can build Zed using [Cargo](https://doc.rust-lang.org/cargo/). - -For a debug build: - -```sh -cargo run -``` - -For a release build: - -```sh -cargo run --release -``` - -And to run the tests: - -```sh -cargo test --workspace -``` - -## Troubleshooting - -### Error compiling metal shaders - -```sh -error: failed to run custom build command for gpui v0.1.0 (/Users/path/to/zed)`** - -xcrun: error: unable to find utility "metal", not a developer tool or in PATH -``` - -Try `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer` - -If you're on macOS 26, try `xcodebuild -downloadComponent MetalToolchain` - -### Cargo errors claiming that a dependency is using unstable features - -Try `cargo clean` and `cargo build`. - -### Error: 'dispatch/dispatch.h' file not found - -If you encounter an error similar to: - -```sh -src/platform/mac/dispatch.h:1:10: fatal error: 'dispatch/dispatch.h' file not found - -Caused by: - process didn't exit successfully - - --- stdout - cargo:rustc-link-lib=framework=System - cargo:rerun-if-changed=src/platform/mac/dispatch.h - cargo:rerun-if-env-changed=TARGET - cargo:rerun-if-env-changed=BINDGEN_EXTRA_CLANG_ARGS_aarch64-apple-darwin - cargo:rerun-if-env-changed=BINDGEN_EXTRA_CLANG_ARGS_aarch64_apple_darwin - cargo:rerun-if-env-changed=BINDGEN_EXTRA_CLANG_ARGS -``` - -This file is part of Xcode. Ensure you have installed the Xcode command line tools and set the correct path: - -```sh -xcode-select --install -sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer -``` - -Additionally, set the `BINDGEN_EXTRA_CLANG_ARGS` environment variable: - -```sh -export BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --show-sdk-path)" -``` - -Then clean and rebuild the project: - -```sh -cargo clean -cargo run -``` - -### Tests failing due to `Too many open files (os error 24)` - -This error seems to be caused by OS resource constraints. Installing and running tests with `cargo-nextest` should resolve the issue. - -- `cargo install cargo-nextest --locked` -- `cargo nextest run --workspace --no-fail-fast` - -## Tips & Tricks - -### Avoiding continual rebuilds - -If you are finding that Zed is continually rebuilding root crates, it may be because -you are pointing your development Zed at the codebase itself. - -This causes problems because `cargo run` exports a bunch of environment -variables which are picked up by the `rust-analyzer` that runs in the development -build of Zed. These environment variables are in turn passed to `cargo check`, which -invalidates the build cache of some of the crates we depend on. - -You can easily avoid running the built binary on the checked-out Zed codebase using `cargo run -~/path/to/other/project` to ensure that you don't hit this. - -### Speeding up verification - -If you are building Zed a lot, you may find that macOS continually verifies new -builds which can add a few seconds to your iteration cycles. - -To fix this, you can: - -- Run `sudo spctl developer-mode enable-terminal` to enable the Developer Tools panel in System Settings. -- In System Settings, search for "Developer Tools" and add your terminal (e.g. iTerm or Ghostty) to the list under "Allow applications to use developer tools" -- Restart your terminal. - -Thanks to the nextest developers for publishing [this](https://nexte.st/docs/installation/macos/#gatekeeper). diff --git a/docs/src/development/release-notes.md b/docs/src/development/release-notes.md deleted file mode 100644 index 90e1ad21b1..0000000000 --- a/docs/src/development/release-notes.md +++ /dev/null @@ -1,29 +0,0 @@ -# Release Notes - -Whenever you open a pull request, the body is automatically populated based on this [pull request template](https://github.com/zed-industries/zed/blob/main/.github/pull_request_template.md). - -```md -... - -Release Notes: - -- N/A _or_ Added/Fixed/Improved ... -``` - -On Wednesdays, we run a [`get-preview-channel-changes`](https://github.com/zed-industries/zed/blob/main/script/get-preview-channel-changes) script that scrapes `Release Notes` lines from pull requests landing in preview, as documented in our [Release](https://zed.dev/docs/development/release-notes) docs. - -The script outputs everything below the `Release Notes` line, including additional data such as the pull request author (if not a Zed team member) and a link to the pull request. -If you use `N/A`, the script skips your pull request entirely. - -## Guidelines for crafting your `Release Notes` line(s) - -- A `Release Notes` line should only be written if the user can see or feel the difference in Zed. -- A `Release Notes` line should be written such that a Zed user can understand what the change is. - Don't assume a user knows technical editor developer lingo; phrase your change in language they understand as a user of a text editor. -- If you want to include technical details about your pull request for other team members to see, do so above the `Release Notes` line. -- Changes to docs should be labeled as `N/A`. -- If your pull request adds/changes a setting or a keybinding, always mention that setting or keybinding. - Don't make the user dig into docs or the pull request to find this information (although it should be included in docs as well). -- For pull requests that are reverts: - - If the item being reverted **has already been shipped**, include a `Release Notes` line explaining why we reverted, as this is a breaking change. - - If the item being reverted **hasn't been shipped**, edit the original PR's `Release Notes` line to be `N/A`; otherwise, it will be included and the compiler of the release notes may not know to skip it, leading to a potentially-awkward situation where we are stating we shipped something we actually didn't. diff --git a/docs/src/development/windows.md b/docs/src/development/windows.md deleted file mode 100644 index 17382e0bee..0000000000 --- a/docs/src/development/windows.md +++ /dev/null @@ -1,259 +0,0 @@ -# Building Zed for Windows - -> The following commands may be executed in any shell. - -## Repository - -Clone down the [Zed repository](https://github.com/zed-industries/zed). - -## Dependencies - -- Install [rustup](https://www.rust-lang.org/tools/install) - -- Install either [Visual Studio](https://visualstudio.microsoft.com/downloads/) with the optional components `MSVC v*** - VS YYYY C++ x64/x86 build tools` and `MSVC v*** - VS YYYY C++ x64/x86 Spectre-mitigated libs (latest)` (`v***` is your VS version and `YYYY` is year when your VS was released. Pay attention to the architecture and change it to yours if needed.) -- Or, if you prefer to have a slimmer installer of only the MSVC compiler tools, you can install the [build tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) (+libs as above) and the "Desktop development with C++" workload. - But beware this installation is not automatically picked up by rustup. You must initialize your environment variables by first launching the "developer" shell (cmd/powershell) this installation places in the start menu or in Windows Terminal and then compile. -- Install Windows 11 or 10 SDK depending on your system, but ensure that at least `Windows 10 SDK version 2104 (10.0.20348.0)` is installed on your machine. You can download it from the [Windows SDK Archive](https://developer.microsoft.com/windows/downloads/windows-sdk/) -- Install [CMake](https://cmake.org/download) (required by [a dependency](https://docs.rs/wasmtime-c-api-impl/latest/wasmtime_c_api/)). Or you can install it through Visual Studio Installer, then manually add the `bin` directory to your `PATH`, for example: `C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin`. - -If you can't compile Zed, make sure that you have at least the following components installed in case of a Visual Studio installation: - -```json [settings] -{ - "version": "1.0", - "components": [ - "Microsoft.VisualStudio.Component.CoreEditor", - "Microsoft.VisualStudio.Workload.CoreEditor", - "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", - "Microsoft.VisualStudio.ComponentGroup.WebToolsExtensions.CMake", - "Microsoft.VisualStudio.Component.VC.CMake.Project", - "Microsoft.VisualStudio.Component.Windows11SDK.26100", - "Microsoft.VisualStudio.Component.VC.Runtimes.x86.x64.Spectre" - ], - "extensions": [] -} -``` - -Or if in case of just Build Tools, the following components: - -```json [settings] -{ - "version": "1.0", - "components": [ - "Microsoft.VisualStudio.Component.Roslyn.Compiler", - "Microsoft.Component.MSBuild", - "Microsoft.VisualStudio.Component.CoreBuildTools", - "Microsoft.VisualStudio.Workload.MSBuildTools", - "Microsoft.VisualStudio.Component.Windows10SDK", - "Microsoft.VisualStudio.Component.VC.CoreBuildTools", - "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", - "Microsoft.VisualStudio.Component.VC.Redist.14.Latest", - "Microsoft.VisualStudio.Component.Windows11SDK.26100", - "Microsoft.VisualStudio.Component.VC.CMake.Project", - "Microsoft.VisualStudio.Component.TextTemplating", - "Microsoft.VisualStudio.Component.VC.CoreIde", - "Microsoft.VisualStudio.ComponentGroup.NativeDesktop.Core", - "Microsoft.VisualStudio.Workload.VCTools", - "Microsoft.VisualStudio.Component.VC.Runtimes.x86.x64.Spectre" - ], - "extensions": [] -} -``` - -The list can be obtained as follows: - -- Open the Visual Studio Installer -- Click on `More` in the `Installed` tab -- Click on `Export configuration` - -### Backend Dependencies (optional) {#backend-dependencies} - -If you are looking to develop Zed collaboration features using a local collaboration server, please see: [Local Collaboration](./local-collaboration.md) docs. - -### Notes - -You should modify the `pg_hba.conf` file in the `data` directory to use `trust` instead of `scram-sha-256` for the `host` method. Otherwise, the connection will fail with the error `password authentication failed`. The `pg_hba.conf` file typically locates at `C:\Program Files\PostgreSQL\17\data\pg_hba.conf`. After the modification, the file should look like this: - -```conf -# IPv4 local connections: -host all all 127.0.0.1/32 trust -# IPv6 local connections: -host all all ::1/128 trust -``` - -Also, if you are using a non-latin Windows version, you must modify the`lc_messages` parameter in the `postgresql.conf` file in the `data` directory to `English_United States.1252` (or whatever UTF8-compatible encoding you have). Otherwise, the database will panic. The `postgresql.conf` file should look like this: - -```conf -# lc_messages = 'Chinese (Simplified)_China.936' # locale for system error message strings -lc_messages = 'English_United States.1252' -``` - -After this, you should restart the `postgresql` service. Press the `win` key + `R` to launch the `Run` window. Type the `services.msc` and hit the `OK` button to open the Services Manager. Then, find the `postgresql-x64-XX` service, right-click on it, and select `Restart`. - -## Building from source - -Once you have the dependencies installed, you can build Zed using [Cargo](https://doc.rust-lang.org/cargo/). - -For a debug build: - -```sh -cargo run -``` - -For a release build: - -```sh -cargo run --release -``` - -And to run the tests: - -```sh -cargo test --workspace -``` - -## Installing from msys2 - -Zed does not support unofficial MSYS2 Zed packages built for Mingw-w64. Please report any issues you may have with [mingw-w64-zed](https://packages.msys2.org/base/mingw-w64-zed) to [msys2/MINGW-packages/issues](https://github.com/msys2/MINGW-packages/issues?q=is%3Aissue+is%3Aopen+zed). - -Please refer to [MSYS2 documentation](https://www.msys2.org/docs/ides-editors/#zed) first. - -## Troubleshooting - -### Setting `RUSTFLAGS` env var breaks builds - -If you set the `RUSTFLAGS` env var, it will override the `rustflags` settings in `.cargo/config.toml` which is required to properly build Zed. - -Since these settings can vary from time to time, the build errors you receive may vary from linker errors, to other stranger errors. - -If you'd like to add extra rust flags, you may do 1 of the following in `.cargo/config.toml`: - -Add your flags in the build section - -```toml -[build] -rustflags = ["-C", "symbol-mangling-version=v0", "--cfg", "tokio_unstable"] -``` - -Add your flags in the windows target section - -```toml -[target.'cfg(target_os = "windows")'] -rustflags = [ - "--cfg", - "windows_slim_errors", - "-C", - "target-feature=+crt-static", -] -``` - -Or, you can create a new `.cargo/config.toml` in the same folder as the Zed repo (see below). This is particularly useful if you are doing CI builds since you don't have to edit the original `.cargo/config.toml`. - -``` -upper_dir -├── .cargo // <-- Make this folder -│ └── config.toml // <-- Make this file -└── zed - ├── .cargo - │ └── config.toml - └── crates - ├── assistant - └── ... -``` - -In the new (above) `.cargo/config.toml`, if we wanted to add `--cfg gles` to our rustflags, it would look like this - -```toml -[target.'cfg(all())'] -rustflags = ["--cfg", "gles"] -``` - -### Cargo errors claiming that a dependency is using unstable features - -Try `cargo clean` and `cargo build`. - -### `STATUS_ACCESS_VIOLATION` - -This error can happen if you are using the "rust-lld.exe" linker. Consider trying a different linker. - -If you are using a global config, consider moving the Zed repository to a nested directory and add a `.cargo/config.toml` with a custom linker config in the parent directory. - -See this issue for more information [#12041](https://github.com/zed-industries/zed/issues/12041) - -### Invalid RC path selected - -Sometimes, depending on the security rules applied to your laptop, you may get the following error while compiling Zed: - -``` -error: failed to run custom build command for `zed(C:\Users\USER\src\zed\crates\zed)` - -Caused by: - process didn't exit successfully: `C:\Users\USER\src\zed\target\debug\build\zed-b24f1e9300107efc\build-script-build` (exit code: 1) - --- stdout - cargo:rerun-if-changed=../../.git/logs/HEAD - cargo:rustc-env=ZED_COMMIT_SHA=25e2e9c6727ba9b77415588cfa11fd969612adb7 - cargo:rustc-link-arg=/stack:8388608 - cargo:rerun-if-changed=resources/windows/app-icon.ico - package.metadata.winresource does not exist - Selected RC path: 'bin\x64\rc.exe' - - --- stderr - The system cannot find the path specified. (os error 3) -warning: build failed, waiting for other jobs to finish... -``` - -In order to fix this issue, you can manually set the `ZED_RC_TOOLKIT_PATH` environment variable to the RC toolkit path. Usually, you can set it to: -`C:\Program Files (x86)\Windows Kits\10\bin\\x64`. - -See this [issue](https://github.com/zed-industries/zed/issues/18393) for more information. - -### Build fails: Path too long - -You may receive an error like the following when building - -``` -error: failed to get `pet` as a dependency of package `languages v0.1.0 (D:\a\zed-windows-builds\zed-windows-builds\crates\languages)` - -Caused by: - failed to load source for dependency `pet` - -Caused by: - Unable to update https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f2 - -Caused by: - path too long: 'C:/Users/runneradmin/.cargo/git/checkouts/python-environment-tools-903993894b37a7d2/ffcbf3f/crates/pet-conda/tests/unix/conda_env_without_manager_but_found_in_history/some_other_location/conda_install/conda-meta/python-fastjsonschema-2.16.2-py310hca03da5_0.json'; class=Filesystem (30) -``` - -In order to solve this, you can enable longpath support for git and Windows. - -For git: `git config --system core.longpaths true` - -And for Windows with this PS command: - -```powershell -New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force -``` - -For more information on this, please see [win32 docs](https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=powershell) - -(note that you will need to restart your system after enabling longpath support) - -### Graphics issues - -#### Zed fails to launch - -Currently, Zed uses Vulkan as its graphics API on Windows. However, Vulkan isn't always the most reliable on Windows, so if Zed fails to launch, it's likely a Vulkan-related issue. - -You can check the Zed log at: -`C:\Users\YOU\AppData\Local\Zed\logs\Zed.log` - -If you see messages like: - -- `Zed failed to open a window: NoSupportedDeviceFound` -- `ERROR_INITIALIZATION_FAILED` -- `GPU Crashed` -- `ERROR_SURFACE_LOST_KHR` - -Then Vulkan might not be working properly on your system. In most cases, updating your GPU drivers may help resolve this. - -If there's nothing Vulkan-related in the logs and you happen to have Bandicam installed, try uninstalling it. Zed is currently not compatible with Bandicam. diff --git a/docs/src/diagnostics.md b/docs/src/diagnostics.md deleted file mode 100644 index 47cc586008..0000000000 --- a/docs/src/diagnostics.md +++ /dev/null @@ -1,70 +0,0 @@ -# Diagnostics - -Zed gets its diagnostics from the language servers and supports both push and pull variants of the LSP which makes it compatible with all existing language servers. - -# Regular diagnostics - -By default, Zed displays all diagnostics as underlined text in the editor and the scrollbar. - -Editor diagnostics could be filtered with the - -```json [settings] -"diagnostics_max_severity": null -``` - -editor setting (possible values: `"off"`, `"error"`, `"warning"`, `"info"`, `"hint"`, `null` (default, all diagnostics)). - -The scrollbar ones are configured with the - -```json [settings] -"scrollbar": { - "diagnostics": "all", -} -``` - -configuration (possible values: `"none"`, `"error"`, `"warning"`, `"information"`, `"all"` (default)) - -The diagnostics could be hovered to display a tooltip with full, rendered diagnostic message. -Or, `editor::GoToDiagnostic` and `editor::GoToPreviousDiagnostic` could be used to navigate between diagnostics in the editor, showing a popover for the currently active diagnostic. - -# Inline diagnostics (Error lens) - -Zed supports showing diagnostic as lens to the right of the code. -This is disabled by default, but can either be temporarily turned on (or off) using the editor menu, or permanently, using the - -```json [settings] -"diagnostics": { - "inline": { - "enabled": true, - "max_severity": null, // same values as the `diagnostics_max_severity` from the editor settings - } -} -``` - -# Other UI places - -## Project Panel - -Project panel can have its entries coloured based on the severity of the diagnostics in the file. - -To configure, use - -```json [settings] -"project_panel": { - "show_diagnostics": "all", -} -``` - -configuration (possible values: `"off"`, `"errors"`, `"all"` (default)) - -## Editor tabs - -Similar to the project panel, editor tabs can be colorized with the - -```json [settings] -"tabs": { - "show_diagnostics": "off", -} -``` - -configuration (possible values: `"off"` (default), `"errors"`, `"all"`) diff --git a/docs/src/environment.md b/docs/src/environment.md deleted file mode 100644 index 41980b2d96..0000000000 --- a/docs/src/environment.md +++ /dev/null @@ -1,92 +0,0 @@ -# Environment Variables - -_**Note**: The following only applies to Zed 0.152.0 and later._ - -Multiple features in Zed are affected by environment variables: - -- Tasks -- Built-in terminal -- Look-up of language servers -- Language servers - -In order to make the best use of these features, it's helpful to understand where Zed gets its environment variables from and how they're used. - -## Where does Zed get its environment variables from? - -How Zed was started — whether it's icon was clicked in the macOS Dock or in a Linux window manager, or whether it was started via the CLI `zed` that comes with Zed — influences which environment variables Zed can use. - -### Launched from the CLI - -If Zed is opened via the CLI (`zed`), it will inherit the environment variables from the surrounding shell session. - -That means if you do - -``` -$ export MY_ENV_VAR=hello -$ zed . -``` - -the environment variable `MY_ENV_VAR` is now available inside Zed. For example, in the built-in terminal. - -Starting with Zed 0.152.0, the CLI `zed` will _always_ pass along its environment to Zed, regardless of whether a Zed instance was previously running or not. Prior to Zed 0.152.0 this was not the case and only the first Zed instance would inherit the environment variables. - -### Launched via window manager, Dock, or launcher - -When Zed has been launched via the macOS Dock, or a GNOME or KDE icon on Linux, or an application launcher like Alfred or Raycast, it has no surrounding shell environment from which to inherit its environment variables. - -In order to still have a useful environment, Zed spawns a login shell in the user's home directory and gets its environment. This environment is then set on the Zed _process_. That means all Zed windows and projects will inherit that home directory environment. - -Since that can lead to problems for users that require different environment variables for a project (because they use `direnv`, or `asdf`, or `mise`, ... in that project), when opening project, Zed spawns another login shell. This time in the project's directory. The environment from that login shell is _not_ set on the process (because that would mean opening a new project changes the environment for all Zed windows). Instead, the environment is stored and passed along when running tasks, opening terminals, or spawning language servers. - -## Where and how are environment variables used? - -There are two sets of environment variables: - -1. Environment variables of the Zed process -2. Environment variables stored per project - -The variables from (1) are always used, since they are stored on the process itself and every spawned process (tasks, terminals, language servers, ...) will inherit them by default. - -The variables from (2) are used explicitly, depending on the feature. - -### Tasks - -Tasks are spawned with an combined environment. In order of precedence (low to high, with the last overwriting the first): - -- the Zed process environment -- if the project was opened from the CLI: the CLI environment -- if the project was not opened from the CLI: the project environment variables obtained by running a login shell in the project's root folder -- optional, explicitly configured environment in settings - -### Built-in terminal - -Built-in terminals, like tasks, are spawned with an combined environment. In order of precedence (low to high): - -- the Zed process environment -- if the project was opened from the CLI: the CLI environment -- if the project was not opened from the CLI: the project environment variables obtained by running a login shell in the project's root folder -- optional, explicitly configured environment in settings - -### Look-up of language servers - -For some languages the language server adapters lookup the binary in the user's `$PATH`. Examples: - -- Go -- Zig -- Rust (if [configured to do so](./languages/rust.md#binary)) -- C -- TypeScript - -For this look-up, Zed uses the following the environment: - -- if the project was opened from the CLI: the CLI environment -- if the project was not opened from the CLI: the project environment variables obtained by running a login shell in the project's root folder - -### Language servers - -After looking up a language server, Zed starts them. - -These language server processes always inherit Zed's process environment. But, depending on the language server look-up, additional environment variables might be set or overwrite the process environment. - -- If the language server was found in the project environment's `$PATH`, then the project environment's is passed along to the language server process. Where the project environment comes from depends on how the project was opened, via CLI or not. See previous point on look-up of language servers. -- If the language servers was not found in the project environment, Zed tries to install it globally and start it globally. In that case, the process will inherit Zed's process environment, and — if the project was opened via ClI — from the CLI. diff --git a/docs/src/extensions.md b/docs/src/extensions.md deleted file mode 100644 index 627fe8f4c0..0000000000 --- a/docs/src/extensions.md +++ /dev/null @@ -1,14 +0,0 @@ -# Extensions - -Zed lets you add new functionality using user-defined extensions. - -- [Installing Extensions](./extensions/installing-extensions.md) -- [Extension Capabilities](./extensions/capabilities.md) -- [Developing Extensions](./extensions/developing-extensions.md) - - [Developing Language Extensions](./extensions/languages.md) - - [Developing Debugger Extensions](./extensions/debugger-extensions.md) - - [Developing Themes](./extensions/themes.md) - - [Developing Icon Themes](./extensions/icon-themes.md) - - [Developing Slash Commands](./extensions/slash-commands.md) - - [Developing Agent Servers](./extensions/agent-servers.md) - - [Developing MCP Servers](./extensions/mcp-extensions.md) diff --git a/docs/src/extensions/agent-servers.md b/docs/src/extensions/agent-servers.md deleted file mode 100644 index c8367a8418..0000000000 --- a/docs/src/extensions/agent-servers.md +++ /dev/null @@ -1,173 +0,0 @@ -# Agent Server Extensions - -Agent Servers are programs that provide AI agent implementations through the [Agent Client Protocol (ACP)](https://agentclientprotocol.com). -Agent Server Extensions let you package up an Agent Server so that users can install the extension and have your agent easily available to use in Zed. - -You can see the current Agent Server extensions either by opening the Extensions tab in Zed (execute the `zed: extensions` command) and changing the filter from `All` to `Agent Servers`, or by visiting [the Zed website](https://zed.dev/extensions?filter=agent-servers). - -## Defining Agent Server Extensions - -An extension can register one or more agent servers in the `extension.toml` like so: - -```toml -[agent_servers.my-agent] -name = "My Agent" - -[agent_servers.my-agent.targets.darwin-aarch64] -archive = "https://github.com/owner/repo/releases/download/v1.0.0/agent-darwin-arm64.tar.gz" -cmd = "./agent" -args = ["--serve"] - -[agent_servers.my-agent.targets.linux-x86_64] -archive = "https://github.com/owner/repo/releases/download/v1.0.0/agent-linux-x64.tar.gz" -cmd = "./agent" -args = ["--serve"] - -[agent_servers.my-agent.targets.windows-x86_64] -archive = "https://github.com/owner/repo/releases/download/v1.0.0/agent-windows-x64.zip" -cmd = "./agent.exe" -args = ["--serve"] -``` - -### Required Fields - -- `name`: A human-readable display name for the agent server (shown in menus) -- `targets`: Platform-specific configurations for downloading and running the agent - -### Target Configuration - -Each target key uses the format `{os}-{arch}` where: - -- **os**: `darwin` (macOS), `linux`, or `windows` -- **arch**: `aarch64` (ARM64) or `x86_64` - -Each target must specify: - -- `archive`: URL to download the archive from (supports `.tar.gz`, `.zip`, etc.) -- `cmd`: Command to run the agent server (relative to the extracted archive) -- `args`: Command-line arguments to pass to the agent server (optional) -- `sha256`: SHA-256 hash string of the archive's bytes (optional, but recommended for security) -- `env`: Environment variables specific to this target (optional, overrides agent-level env vars with the same name) - -### Optional Fields - -You can also optionally specify at the agent server level: - -- `env`: Environment variables to set in the agent's spawned process. These apply to all targets by default. -- `icon`: Path to an SVG icon (relative to extension root) for display in menus. - -### Environment Variables - -Environment variables can be configured at two levels: - -1. **Agent-level** (`[agent_servers.my-agent.env]`): Variables that apply to all platforms -2. **Target-level** (`[agent_servers.my-agent.targets.{platform}.env]`): Variables specific to a platform - -When both are specified, target-level environment variables override agent-level variables with the same name. Variables defined only at the agent level are inherited by all targets. - -### Complete Example - -Here's a more complete example with all optional fields: - -```toml -[agent_servers.example-agent] -name = "Example Agent" -icon = "icon/agent.svg" - -[agent_servers.example-agent.env] -AGENT_LOG_LEVEL = "info" -AGENT_MODE = "production" - -[agent_servers.example-agent.targets.darwin-aarch64] -archive = "https://github.com/example/agent/releases/download/v2.0.0/agent-darwin-arm64.tar.gz" -cmd = "./bin/agent" -args = ["serve", "--port", "8080"] -sha256 = "abc123def456..." - -[agent_servers.example-agent.targets.linux-x86_64] -archive = "https://github.com/example/agent/releases/download/v2.0.0/agent-linux-x64.tar.gz" -cmd = "./bin/agent" -args = ["serve", "--port", "8080"] -sha256 = "def456abc123..." - -[agent_servers.example-agent.targets.linux-x86_64.env] -AGENT_MEMORY_LIMIT = "2GB" # Linux-specific override -``` - -## Installation Process - -When a user installs your extension and selects the agent server: - -1. Zed downloads the appropriate archive for the user's platform -2. The archive is extracted to a cache directory -3. Zed launches the agent using the specified command and arguments -4. Environment variables are set as configured -5. The agent server runs in the background, ready to assist the user - -Archives are cached locally, so subsequent launches are fast. - -## Distribution Best Practices - -### Use GitHub Releases - -GitHub Releases are a reliable way to distribute agent server binaries: - -1. Build your agent for each platform (macOS ARM64, macOS x86_64, Linux x86_64, Windows x86_64) -2. Package each build as a compressed archive (`.tar.gz` or `.zip`) -3. Create a GitHub release and upload the archives -4. Use the release URLs in your `extension.toml` - -## SHA-256 Hashes - -It's good for security to include SHA-256 hashes of your archives in `extension.toml`. Here's how to generate it: - -### macOS and Linux - -```bash -shasum -a 256 agent-darwin-arm64.tar.gz -``` - -### Windows - -```bash -certutil -hashfile agent-windows-x64.zip SHA256 -``` - -Then add that string to your target configuration: - -```toml -[agent_servers.my-agent.targets.darwin-aarch64] -archive = "https://github.com/owner/repo/releases/download/v1.0.0/agent-darwin-arm64.tar.gz" -cmd = "./agent" -sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" -``` - -## Testing - -To test your Agent Server Extension: - -1. [Install it as a dev extension](./developing-extensions.md#developing-an-extension-locally) -2. Open the [Agent Panel](../ai/agent-panel.md) -3. Select your Agent Server from the list -4. Verify that it downloads, installs, and launches correctly -5. Test its functionality by conversing with it and watching the [ACP logs](../ai/external-agents.md#debugging-agents) - -## Icon Guideline - -In case your agent server has a logo, we highly recommend adding it as an SVG icon. -For optimal display, follow these guidelines: - -- Make sure you resize your SVG to fit a 16x16 bounding box, with a padding of around one or two pixels -- Ensure you have a clean SVG code by processing it through [SVGOMG](https://jakearchibald.github.io/svgomg/) -- Avoid including icons with gradients as they will often make the SVG more complicated and possibly not render perfectly - -Note that we'll automatically convert your icon to monochrome to preserve Zed's design consistency. -(You can still use opacity in different paths of your SVG to add visual layering.) - ---- - -This is all you need to distribute an agent server through Zed's extension system! - -## Publishing - -Once your extension is ready, see [Publishing your extension](./developing-extensions.md#publishing-your-extension) to learn how to submit it to the Zed extension registry. diff --git a/docs/src/extensions/capabilities.md b/docs/src/extensions/capabilities.md deleted file mode 100644 index 4d935a0725..0000000000 --- a/docs/src/extensions/capabilities.md +++ /dev/null @@ -1,96 +0,0 @@ -# Extension Capabilities - -The operations that Zed extensions are able to perform are governed by a capability system. - -## Restricting capabilities - -As a user, you have the option of restricting the capabilities that are granted to extensions. - -This is controlled via the `granted_extension_capabilities` setting. - -Restricting or removing a capability will cause an error to be returned when an extension attempts to call the corresponding extension API without sufficient capabilities. - -For instance, if you wanted to restrict downloads to just files from GitHub, you could modify `host` for the `download_file` capability: - -```diff -{ - "granted_extension_capabilities": [ - { "kind": "process:exec", "command": "*", "args": ["**"] }, -- { "kind": "download_file", "host": "*", "path": ["**"] }, -+ { "kind": "download_file", "host": "github.com", "path": ["**"] }, - { "kind": "npm:install", "package": "*" } - ] -} -``` - -If you don't want extensions to be able to perform _any_ capabilities, you can remove all granted capabilities: - -```json -{ - "granted_extension_capabilities": [] -} -``` - -> Note that this will likely make many extensions non-functional, at least in their default configuration. - -## Capabilities - -### `process:exec` - -The `process:exec` capability grants extensions the ability to invoke commands using [`zed_extension_api::process::Command`](https://docs.rs/zed_extension_api/latest/zed_extension_api/process/struct.Command.html). - -#### Examples - -To allow any command to be executed with any arguments: - -```toml -{ kind = "process:exec", command = "*", args = ["**"] } -``` - -To allow a specific command (e.g., `gem`) to be executed with any arguments: - -```toml -{ kind = "process:exec", command = "gem", args = ["**"] } -``` - -### `download_file` - -The `download_file` capability grants extensions the ability to download files using [`zed_extension_api::download_file`](https://docs.rs/zed_extension_api/latest/zed_extension_api/fn.download_file.html). - -#### Examples - -To allow any file to be downloaded: - -```toml -{ kind = "download_file", host = "github.com", path = ["**"] } -``` - -To allow any file to be downloaded from `github.com`: - -```toml -{ kind = "download_file", host = "github.com", path = ["**"] } -``` - -To allow any file to be downloaded from a specific GitHub repository: - -```toml -{ kind = "download_file", host = "github.com", path = ["zed-industries", "zed", "**"] } -``` - -### `npm:install` - -The `npm:install` capability grants extensions the ability to install npm packages using [`zed_extension_api::npm_install_package`](https://docs.rs/zed_extension_api/latest/zed_extension_api/fn.npm_install_package.html). - -#### Examples - -To allow any npm package to be installed: - -```toml -{ kind = "npm:install", package = "*" } -``` - -To allow a specific npm package (e.g., `typescript`) to be installed: - -```toml -{ kind = "npm:install", package = "typescript" } -``` diff --git a/docs/src/extensions/debugger-extensions.md b/docs/src/extensions/debugger-extensions.md deleted file mode 100644 index fa33c25732..0000000000 --- a/docs/src/extensions/debugger-extensions.md +++ /dev/null @@ -1,117 +0,0 @@ -# Debugger Extensions - -[Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol) Servers can be exposed as extensions for use in the [debugger](../debugger.md). - -## Defining Debugger Extensions - -A given extension may provide one or more DAP servers. -Each DAP server must be registered in the `extension.toml`: - -```toml -[debug_adapters.my-debug-adapter] -# Optional relative path to the JSON schema for the debug adapter configuration schema. Defaults to `debug_adapter_schemas/$DEBUG_ADAPTER_NAME_ID.json`. -# Note that while this field is optional, a schema is mandatory. -schema_path = "relative/path/to/schema.json" -``` - -Then, in the Rust code for your extension, implement the `get_dap_binary` method on your extension: - -```rust -impl zed::Extension for MyExtension { - fn get_dap_binary( - &mut self, - adapter_name: String, - config: DebugTaskDefinition, - user_provided_debug_adapter_path: Option, - worktree: &Worktree, - ) -> Result; -} -``` - -This method should return the command to start up a debug adapter protocol server, along with any arguments or environment variables necessary for it to function. - -If you need to download the DAP server from an external source—like GitHub Releases or npm—you can also do that in this function. Make sure to check for updates only periodically, as this function is called whenever a user spawns a new debug session with your debug adapter. - -You must also implement `dap_request_kind`. This function is used to determine whether a given debug scenario will _launch_ a new debuggee or _attach_ to an existing one. -We also use it to determine that a given debug scenario requires running a _locator_. - -```rust -impl zed::Extension for MyExtension { - fn dap_request_kind( - &mut self, - _adapter_name: String, - _config: Value, - ) -> Result; -} -``` - -These two functions are sufficient to expose your debug adapter in `debug.json`-based user workflows, but you should strongly consider implementing `dap_config_to_scenario` as well. - -```rust -impl zed::Extension for MyExtension { - fn dap_config_to_scenario( - &mut self, - _adapter_name: DebugConfig, - ) -> Result; -} -``` - -`dap_config_to_scenario` is used when the user spawns a session via new process modal UI. At a high level, it takes a generic debug configuration (that isn't specific to any -debug adapter) and tries to turn it into a concrete debug scenario for your adapter. -Put another way, it is supposed to answer the question: "Given a program, a list of arguments, current working directory and environment variables, what would the configuration for spawning this debug adapter look like?". - -## Defining Debug Locators - -Zed offers an automatic way to create debug scenarios with _debug locators_. -A locator locates the debug target and figures out how to spawn a debug session for it. Thanks to locators, we can automatically convert existing user tasks (e.g. `cargo run`) and convert them into debug scenarios (e.g. `cargo build` followed by spawning a debugger with `target/debug/my_program` as the program to debug). - -> Your extension can define its own debug locators even if it does not expose a debug adapter. We strongly recommend doing so when your extension already exposes language tasks, as it allows users to spawn a debug session without having to manually configure the debug adapter. - -Locators can (but don't have to) be agnostic to the debug adapter they are used with. They are simply responsible for locating the debug target and figuring out how to spawn a debug session for it. This allows for a more flexible and extensible debugging experience. - -Your extension can define one or more debug locators. Each debug locator must be registered in the `extension.toml`: - -```toml -[debug_locators.my-debug-locator] -``` - -Locators have two components. -First, each locator is ran on each available task to figure out if any of the available locators can provide a debug scenario for a given task. This is done by calling `dap_locator_create_scenario`. - -```rust -impl zed::Extension for MyExtension { - fn dap_locator_create_scenario( - &mut self, - _locator_name: String, - _build_task: TaskTemplate, - _resolved_label: String, - _debug_adapter_name: String, - ) -> Option; -} -``` - -This function should return `Some` debug scenario when that scenario defines a debugging counterpart to a given user task. -Note that a `DebugScenario` can include a [build task](../debugger.md#build-tasks). If there is one, we will execute `run_dap_locator` after a build task is finished successfully. - -```rust -impl zed::Extension for MyExtension { - fn run_dap_locator( - &mut self, - _locator_name: String, - _build_task: TaskTemplate, - ) -> Result; -} -``` - -`run_dap_locator` is useful in case you cannot determine a build target deterministically. Some build systems may produce artifacts whose names are not known up-front. -Note however that you do _not_ need to go through a 2-phase resolution; if you can determine the full debug configuration with just `dap_locator_create_scenario`, you can omit `build` property on a returned `DebugScenario`. Please also note that your locator **will be** called with tasks it's unlikely to accept; thus you should take some effort to return `None` early before performing any expensive operations. - -## Available Extensions - -Check out all the DAP servers that have already been exposed as extensions [on Zed's site](https://zed.dev/extensions?filter=debug-adapters). - -We recommend taking a look at their repositories as a way to understand how they are generally created and structured. - -## Testing - -To test your new Debug Adapter Protocol server extension, you can [install it as a dev extension](./developing-extensions.md#developing-an-extension-locally). diff --git a/docs/src/extensions/developing-extensions.md b/docs/src/extensions/developing-extensions.md deleted file mode 100644 index dc8a693291..0000000000 --- a/docs/src/extensions/developing-extensions.md +++ /dev/null @@ -1,182 +0,0 @@ -# Developing Extensions - -## Extension Features - -Extensions are able to provide the following features to Zed: - -- [Languages](./languages.md) -- [Debuggers](./debugger-extensions.md) -- [Themes](./themes.md) -- [Icon Themes](./icon-themes.md) -- [Slash Commands](./slash-commands.md) -- [MCP Servers](./mcp-extensions.md) - -## Developing an Extension Locally - -Before starting to develop an extension for Zed, be sure to [install Rust via rustup](https://www.rust-lang.org/tools/install). - -> Rust must be installed via rustup. If you have Rust installed via homebrew or otherwise, installing dev extensions will not work. - -When developing an extension, you can use it in Zed without needing to publish it by installing it as a _dev extension_. - -From the extensions page, click the `Install Dev Extension` button (or the {#action zed::InstallDevExtension} action) and select the directory containing your extension. - -If you need to troubleshoot, you can check the Zed.log ({#action zed::OpenLog}) for additional output. For debug output, close and relaunch zed with the `zed --foreground` from the command line which show more verbose INFO level logging. - -If you already have the published version of the extension installed, the published version will be uninstalled prior to the installation of the dev extension. After successful installation, the `Extensions` page will indicate that the upstream extension is "Overridden by dev extension". - -## Directory Structure of a Zed Extension - -A Zed extension is a Git repository that contains an `extension.toml`. This file must contain some -basic information about the extension: - -```toml -id = "my-extension" -name = "My extension" -version = "0.0.1" -schema_version = 1 -authors = ["Your Name "] -description = "My cool extension" -repository = "https://github.com/your-name/my-zed-extension" -``` - -In addition to this, there are several other optional files and directories that can be used to add functionality to a Zed extension. An example directory structure of an extension that provides all capabilities is as follows: - -``` -my-extension/ - extension.toml - Cargo.toml - src/ - lib.rs - languages/ - my-language/ - config.toml - highlights.scm - themes/ - my-theme.json -``` - -## WebAssembly - -Procedural parts of extensions are written in Rust and compiled to WebAssembly. To develop an extension that includes custom code, include a `Cargo.toml` like this: - -```toml -[package] -name = "my-extension" -version = "0.0.1" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = "0.1.0" -``` - -Use the latest version of the [`zed_extension_api`](https://crates.io/crates/zed_extension_api) available on crates.io. Make sure it's still [compatible with Zed versions](https://github.com/zed-industries/zed/blob/main/crates/extension_api#compatible-zed-versions) you want to support. - -In the `src/lib.rs` file in your Rust crate you will need to define a struct for your extension and implement the `Extension` trait, as well as use the `register_extension!` macro to register your extension: - -```rs -use zed_extension_api as zed; - -struct MyExtension { - // ... state -} - -impl zed::Extension for MyExtension { - // ... -} - -zed::register_extension!(MyExtension); -``` - -> `stdout`/`stderr` is forwarded directly to the Zed process. In order to see `println!`/`dbg!` output from your extension, you can start Zed in your terminal with a `--foreground` flag. - -## Forking and cloning the repo - -1. Fork the repo - -> Note: It is very helpful if you fork the `zed-industries/extensions` repo to a personal GitHub account instead of a GitHub organization, as this allows Zed staff to push any needed changes to your PR to expedite the publishing process. - -2. Clone the repo to your local machine - -```sh -# Substitute the url of your fork here: -# git clone https://github.com/zed-industries/extensions -cd extensions -git submodule init -git submodule update -``` - -## Extension License Requirements - -As of October 1st, 2025, extension repositories must include a license. -The following licenses are accepted: - -- [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) -- [BSD 3-Clause](https://opensource.org/license/bsd-3-clause) -- [GNU GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html) -- [MIT](https://opensource.org/license/mit) - -This allows us to distribute the resulting binary produced from your extension code to our users. -Without a valid license, the pull request to add or update your extension in the following steps will fail CI. - -Your license file should be at the root of your extension repository. Any filename that has `LICENCE` or `LICENSE` as a prefix (case insensitive) will be inspected to ensure it matches one of the accepted licenses. See the [license validation source code](https://github.com/zed-industries/extensions/blob/main/src/lib/license.js). - -> This license requirement applies only to your extension code itself (the code that gets compiled into the extension binary). -> It does not apply to any tools your extension may download or interact with, such as language servers or other external dependencies. -> If your repository contains both extension code and other projects (like a language server), you are not required to relicense those other projects—only the extension code needs to be one of the aforementioned accepted licenses. - -## Publishing your extension - -To publish an extension, open a PR to [the `zed-industries/extensions` repo](https://github.com/zed-industries/extensions). - -In your PR, do the following: - -1. Add your extension as a Git submodule within the `extensions/` directory - -```sh -git submodule add https://github.com/your-username/foobar-zed.git extensions/foobar -git add extensions/foobar -``` - -> All extension submodules must use HTTPS URLs and not SSH URLS (`git@github.com`). - -2. Add a new entry to the top-level `extensions.toml` file containing your extension: - -```toml -[my-extension] -submodule = "extensions/my-extension" -version = "0.0.1" -``` - -> If your extension is in a subdirectory within the submodule you can use the `path` field to point to where the extension resides. - -3. Run `pnpm sort-extensions` to ensure `extensions.toml` and `.gitmodules` are sorted - -Once your PR is merged, the extension will be packaged and published to the Zed extension registry. - -> Extension IDs and names should not contain `zed` or `Zed`, since they are all Zed extensions. - -## Updating an extension - -To update an extension, open a PR to [the `zed-industries/extensions` repo](https://github.com/zed-industries/extensions). - -In your PR do the following: - -1. Update the extension's submodule to the commit of the new version. For this, you can run - -```sh -# From the root of the repository: -git submodule update --remote extensions/your-extension-name -``` - -to update your extension to the latest commit available in your remote repository. - -2. Update the `version` field for the extension in `extensions.toml` - - Make sure the `version` matches the one set in `extension.toml` at the particular commit. - -If you'd like to automate this process, there is a [community GitHub Action](https://github.com/huacnlee/zed-extension-action) you can use. - -> **Note:** If your extension repository has a different license, you'll need to update it to be one of the [accepted extension licenses](#extension-license-requirements) before publishing your update. diff --git a/docs/src/extensions/icon-themes.md b/docs/src/extensions/icon-themes.md deleted file mode 100644 index 676cae59cd..0000000000 --- a/docs/src/extensions/icon-themes.md +++ /dev/null @@ -1,78 +0,0 @@ -# Icon Themes - -Extensions may provide icon themes in order to change the icons Zed uses for folders and files. - -## Example extension - -The [Material Icon Theme](https://github.com/zed-extensions/material-icon-theme) serves as an example for the structure of an extension containing an icon theme. - -## Directory structure - -There are two important directories for an icon theme extension: - -- `icon_themes`: This directory will contain one or more JSON files containing the icon theme definitions. -- `icons`: This directory contains the icon assets that will be distributed with the extension. You can created subdirectories in this directory, if so desired. - -Each icon theme file should adhere to the JSON schema specified at [`https://zed.dev/schema/icon_themes/v0.3.0.json`](https://zed.dev/schema/icon_themes/v0.3.0.json). - -Here is an example of the structure of an icon theme: - -```json [icon-theme] -{ - "$schema": "https://zed.dev/schema/icon_themes/v0.3.0.json", - "name": "My Icon Theme", - "author": "Your Name", - "themes": [ - { - "name": "My Icon Theme", - "appearance": "dark", - "directory_icons": { - "collapsed": "./icons/folder.svg", - "expanded": "./icons/folder-open.svg" - }, - "named_directory_icons": { - "stylesheets": { - "collapsed": "./icons/folder-stylesheets.svg", - "expanded": "./icons/folder-stylesheets-open.svg" - } - }, - "chevron_icons": { - "collapsed": "./icons/chevron-right.svg", - "expanded": "./icons/chevron-down.svg" - }, - "file_stems": { - "Makefile": "make" - }, - "file_suffixes": { - "mp3": "audio", - "rs": "rust" - }, - "file_icons": { - "audio": { "path": "./icons/audio.svg" }, - "default": { "path": "./icons/file.svg" }, - "make": { "path": "./icons/make.svg" }, - "rust": { "path": "./icons/rust.svg" } - // ... - } - } - ] -} -``` - -Each icon path is resolved relative to the root of the extension directory. - -In this example, the extension would have a structure like so: - -``` -extension.toml -icon_themes/ - my-icon-theme.json -icons/ - audio.svg - chevron-down.svg - chevron-right.svg - file.svg - folder-open.svg - folder.svg - rust.svg -``` diff --git a/docs/src/extensions/installing-extensions.md b/docs/src/extensions/installing-extensions.md deleted file mode 100644 index d9573556f0..0000000000 --- a/docs/src/extensions/installing-extensions.md +++ /dev/null @@ -1,20 +0,0 @@ -# Installing Extensions - -You can search for extensions by launching the Zed Extension Gallery by pressing {#kb zed::Extensions} , opening the command palette and selecting {#action zed::Extensions} or by selecting "Zed > Extensions" from the menu bar. - -Here you can view the extensions that you currently have installed or search and install new ones. - -## Installation Location - -- On macOS, extensions are installed in `~/Library/Application Support/Zed/extensions`. -- On Linux, they are installed in either `$XDG_DATA_HOME/zed/extensions` or `~/.local/share/zed/extensions`. -- On Windows, the directory is `%LOCALAPPDATA%\Zed\extensions`. - -This directory contains two subdirectories: - -- `installed`, which contains the source code for each extension. -- `work` which contains files created by the extension itself, such as downloaded language servers. - -## Auto installing - -To automate extension installation/uninstallation see the docs for [auto_install_extensions](../configuring-zed.md#auto-install-extensions). diff --git a/docs/src/extensions/languages.md b/docs/src/extensions/languages.md deleted file mode 100644 index f3ffcd71ba..0000000000 --- a/docs/src/extensions/languages.md +++ /dev/null @@ -1,420 +0,0 @@ -# Language Extensions - -Language support in Zed has several components: - -- Language metadata and configuration -- Grammar -- Queries -- Language servers - -## Language Metadata - -Each language supported by Zed must be defined in a subdirectory inside the `languages` directory of your extension. - -This subdirectory must contain a file called `config.toml` file with the following structure: - -```toml -name = "My Language" -grammar = "my-language" -path_suffixes = ["myl"] -line_comments = ["# "] -``` - -- `name` (required) is the human readable name that will show up in the Select Language dropdown. -- `grammar` (required) is the name of a grammar. Grammars are registered separately, described below. -- `path_suffixes` is an array of file suffixes that should be associated with this language. Unlike `file_types` in settings, this does not support glob patterns. -- `line_comments` is an array of strings that are used to identify line comments in the language. This is used for the `editor::ToggleComments` keybind: {#kb editor::ToggleComments} for toggling lines of code. -- `tab_size` defines the indentation/tab size used for this language (default is `4`). -- `hard_tabs` whether to indent with tabs (`true`) or spaces (`false`, the default). -- `first_line_pattern` is a regular expression, that in addition to `path_suffixes` (above) or `file_types` in settings can be used to match files which should use this language. For example Zed uses this to identify Shell Scripts by matching the [shebangs lines](https://github.com/zed-industries/zed/blob/main/crates/languages/src/bash/config.toml) in the first line of a script. -- `debuggers` is an array of strings that are used to identify debuggers in the language. When launching a debugger's `New Process Modal`, Zed will order available debuggers by the order of entries in this array. - - - -## Grammar - -Zed uses the [Tree-sitter](https://tree-sitter.github.io) parsing library to provide built-in language-specific features. There are grammars available for many languages, and you can also [develop your own grammar](https://tree-sitter.github.io/tree-sitter/creating-parsers#writing-the-grammar). A growing list of Zed features are built using pattern matching over syntax trees with Tree-sitter queries. As mentioned above, every language that is defined in an extension must specify the name of a Tree-sitter grammar that is used for parsing. These grammars are then registered separately in extensions' `extension.toml` file, like this: - -```toml -[grammars.gleam] -repository = "https://github.com/gleam-lang/tree-sitter-gleam" -rev = "58b7cac8fc14c92b0677c542610d8738c373fa81" -``` - -The `repository` field must specify a repository where the Tree-sitter grammar should be loaded from, and the `rev` field must contain a Git revision to use, such as the SHA of a Git commit. If you're developing an extension locally and want to load a grammar from the local filesystem, you can use a `file://` URL for `repository`. An extension can provide multiple grammars by referencing multiple tree-sitter repositories. - -## Tree-sitter Queries - -Zed uses the syntax tree produced by the [Tree-sitter](https://tree-sitter.github.io) query language to implement -several features: - -- Syntax highlighting -- Bracket matching -- Code outline/structure -- Auto-indentation -- Code injections -- Syntax overrides -- Text redactions -- Runnable code detection -- Selecting classes, functions, etc. - -The following sections elaborate on how [Tree-sitter queries](https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax) enable these -features in Zed, using [JSON syntax](https://www.json.org/json-en.html) as a guiding example. - -### Syntax highlighting - -In Tree-sitter, the `highlights.scm` file defines syntax highlighting rules for a particular syntax. - -Here's an example from a `highlights.scm` for JSON: - -```scheme -(string) @string - -(pair - key: (string) @property.json_key) - -(number) @number -``` - -This query marks strings, object keys, and numbers for highlighting. The following is a comprehensive list of captures supported by themes: - -| Capture | Description | -| ------------------------ | -------------------------------------- | -| @attribute | Captures attributes | -| @boolean | Captures boolean values | -| @comment | Captures comments | -| @comment.doc | Captures documentation comments | -| @constant | Captures constants | -| @constructor | Captures constructors | -| @embedded | Captures embedded content | -| @emphasis | Captures emphasized text | -| @emphasis.strong | Captures strongly emphasized text | -| @enum | Captures enumerations | -| @function | Captures functions | -| @hint | Captures hints | -| @keyword | Captures keywords | -| @label | Captures labels | -| @link_text | Captures link text | -| @link_uri | Captures link URIs | -| @number | Captures numeric values | -| @operator | Captures operators | -| @predictive | Captures predictive text | -| @preproc | Captures preprocessor directives | -| @primary | Captures primary elements | -| @property | Captures properties | -| @punctuation | Captures punctuation | -| @punctuation.bracket | Captures brackets | -| @punctuation.delimiter | Captures delimiters | -| @punctuation.list_marker | Captures list markers | -| @punctuation.special | Captures special punctuation | -| @string | Captures string literals | -| @string.escape | Captures escaped characters in strings | -| @string.regex | Captures regular expressions | -| @string.special | Captures special strings | -| @string.special.symbol | Captures special symbols | -| @tag | Captures tags | -| @tag.doctype | Captures doctypes (e.g., in HTML) | -| @text.literal | Captures literal text | -| @title | Captures titles | -| @type | Captures types | -| @variable | Captures variables | -| @variable.special | Captures special variables | -| @variant | Captures variants | - -### Bracket matching - -The `brackets.scm` file defines matching brackets. - -Here's an example from a `brackets.scm` file for JSON: - -```scheme -("[" @open "]" @close) -("{" @open "}" @close) -("\"" @open "\"" @close) -``` - -This query identifies opening and closing brackets, braces, and quotation marks. - -| Capture | Description | -| ------- | --------------------------------------------- | -| @open | Captures opening brackets, braces, and quotes | -| @close | Captures closing brackets, braces, and quotes | - -Zed uses these to highlight matching brackets: painting each bracket pair with a different color ("rainbow brackets") and highlighting the brackets if the cursor is inside the bracket pair. - -To opt out of rainbow brackets colorization, add the following to the corresponding `brackets.scm` entry: - -```scheme -(("\"" @open "\"" @close) (#set! rainbow.exclude)) -``` - -### Code outline/structure - -The `outline.scm` file defines the structure for the code outline. - -Here's an example from an `outline.scm` file for JSON: - -```scheme -(pair - key: (string (string_content) @name)) @item -``` - -This query captures object keys for the outline structure. - -| Capture | Description | -| -------------- | ------------------------------------------------------------------------------------ | -| @name | Captures the content of object keys | -| @item | Captures the entire key-value pair | -| @context | Captures elements that provide context for the outline item | -| @context.extra | Captures additional contextual information for the outline item | -| @annotation | Captures nodes that annotate outline item (doc comments, attributes, decorators)[^1] | - -[^1]: These annotations are used by Assistant when generating code modification steps. - -### Auto-indentation - -The `indents.scm` file defines indentation rules. - -Here's an example from an `indents.scm` file for JSON: - -```scheme -(array "]" @end) @indent -(object "}" @end) @indent -``` - -This query marks the end of arrays and objects for indentation purposes. - -| Capture | Description | -| ------- | -------------------------------------------------- | -| @end | Captures closing brackets and braces | -| @indent | Captures entire arrays and objects for indentation | - -### Code injections - -The `injections.scm` file defines rules for embedding one language within another, such as code blocks in Markdown or SQL queries in Python strings. - -Here's an example from an `injections.scm` file for Markdown: - -```scheme -(fenced_code_block - (info_string - (language) @injection.language) - (code_fence_content) @injection.content) - -((inline) @content - (#set! injection.language "markdown-inline")) -``` - -This query identifies fenced code blocks, capturing the language specified in the info string and the content within the block. It also captures inline content and sets its language to "markdown-inline". - -| Capture | Description | -| ------------------- | ---------------------------------------------------------- | -| @injection.language | Captures the language identifier for a code block | -| @injection.content | Captures the content to be treated as a different language | - -Note that we couldn't use JSON as an example here because it doesn't support language injections. - -### Syntax overrides - -The `overrides.scm` file defines syntactic _scopes_ that can be used to override certain editor settings within specific language constructs. - -For example, there is a language-specific setting called `word_characters` that controls which non-alphabetic characters are considered part of a word, for example when you double click to select a variable. In JavaScript, "$" and "#" are considered word characters. - -There is also a language-specific setting called `completion_query_characters` that controls which characters trigger autocomplete suggestions. In JavaScript, when your cursor is within a _string_, "-" is should be considered a completion query character. To achieve this, the JavaScript `overrides.scm` file contains the following pattern: - -```scheme -[ - (string) - (template_string) -] @string -``` - -And the JavaScript `config.toml` contains this setting: - -```toml -word_characters = ["#", "$"] - -[overrides.string] -completion_query_characters = ["-"] -``` - -You can also disable certain auto-closing brackets in a specific scope. For example, to prevent auto-closing `'` within strings, you could put the following in the JavaScript `config.toml`: - -```toml -brackets = [ - { start = "'", end = "'", close = true, newline = false, not_in = ["string"] }, - # other pairs... -] -``` - -#### Range inclusivity - -By default, the ranges defined in `overrides.scm` are _exclusive_. So in the case above, if you cursor was _outside_ the quotation marks delimiting the string, the `string` scope would not take effect. Sometimes, you may want to make the range _inclusive_. You can do this by adding the `.inclusive` suffix to the capture name in the query. - -For example, in JavaScript, we also disable auto-closing of single quotes within comments. And the comment scope must extend all the way to the newline after a line comment. To achieve this, the JavaScript `overrides.scm` contains the following pattern: - -```scheme -(comment) @comment.inclusive -``` - -### Text objects - -The `textobjects.scm` file defines rules for navigating by text objects. This was added in Zed v0.165 and is currently used only in Vim mode. - -Vim provides two levels of granularity for navigating around files. Section-by-section with `[]` etc., and method-by-method with `]m` etc. Even languages that don't support functions and classes can work well by defining similar concepts. For example CSS defines a rule-set as a method, and a media-query as a class. - -For languages with closures, these typically should not count as functions in Zed. This is best-effort however, as languages like JavaScript do not syntactically differentiate syntactically between closures and top-level function declarations. - -For languages with declarations like C, provide queries that match `@class.around` or `@function.around`. The `if` and `ic` text objects will default to these if there is no inside. - -If you are not sure what to put in textobjects.scm, both [nvim-treesitter-textobjects](https://github.com/nvim-treesitter/nvim-treesitter-textobjects), and the [Helix editor](https://github.com/helix-editor/helix) have queries for many languages. You can refer to the Zed [built-in languages](https://github.com/zed-industries/zed/tree/main/crates/languages/src) to see how to adapt these. - -| Capture | Description | Vim mode | -| ---------------- | ----------------------------------------------------------------------- | ------------------------------------------------ | -| @function.around | An entire function definition or equivalent small section of a file. | `[m`, `]m`, `[M`,`]M` motions. `af` text object | -| @function.inside | The function body (the stuff within the braces). | `if` text object | -| @class.around | An entire class definition or equivalent large section of a file. | `[[`, `]]`, `[]`, `][` motions. `ac` text object | -| @class.inside | The contents of a class definition. | `ic` text object | -| @comment.around | An entire comment (e.g. all adjacent line comments, or a block comment) | `gc` text object | -| @comment.inside | The contents of a comment | `igc` text object (rarely supported) | - -For example: - -```scheme -; include only the content of the method in the function -(method_definition - body: (_ - "{" - (_)* @function.inside - "}")) @function.around - -; match function.around for declarations with no body -(function_signature_item) @function.around - -; join all adjacent comments into one -(comment)+ @comment.around -``` - -### Text redactions - -The `redactions.scm` file defines text redaction rules. When collaborating and sharing your screen, it makes sure that certain syntax nodes are rendered in a redacted mode to avoid them from leaking. - -Here's an example from a `redactions.scm` file for JSON: - -```scheme -(pair value: (number) @redact) -(pair value: (string) @redact) -(array (number) @redact) -(array (string) @redact) -``` - -This query marks number and string values in key-value pairs and arrays for redaction. - -| Capture | Description | -| ------- | ------------------------------ | -| @redact | Captures values to be redacted | - -### Runnable code detection - -The `runnables.scm` file defines rules for detecting runnable code. - -Here's an example from a `runnables.scm` file for JSON: - -```scheme -( - (document - (object - (pair - key: (string - (string_content) @_name - (#eq? @_name "scripts") - ) - value: (object - (pair - key: (string (string_content) @run @script) - ) - ) - ) - ) - ) - (#set! tag package-script) - (#set! tag composer-script) -) -``` - -This query detects runnable scripts in package.json and composer.json files. - -The `@run` capture specifies where the run button should appear in the editor. Other captures, except those prefixed with an underscore, are exposed as environment variables with a prefix of `ZED_CUSTOM_$(capture_name)` when running the code. - -| Capture | Description | -| ------- | ------------------------------------------------------ | -| @\_name | Captures the "scripts" key | -| @run | Captures the script name | -| @script | Also captures the script name (for different purposes) | - - - -## Language Servers - -Zed uses the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) to provide advanced language support. - -An extension may provide any number of language servers. To provide a language server from your extension, add an entry to your `extension.toml` with the name of your language server and the language(s) it applies to. The entry in the list of `languages` has to match the `name` field from the `config.toml` file for that language: - -```toml -[language_servers.my-language-server] -name = "My Language LSP" -languages = ["My Language"] -``` - -Then, in the Rust code for your extension, implement the `language_server_command` method on your extension: - -```rust -impl zed::Extension for MyExtension { - fn language_server_command( - &mut self, - language_server_id: &LanguageServerId, - worktree: &zed::Worktree, - ) -> Result { - Ok(zed::Command { - command: get_path_to_language_server_executable()?, - args: get_args_for_language_server()?, - env: get_env_for_language_server()?, - }) - } -} -``` - -You can customize the handling of the language server using several optional methods in the `Extension` trait. For example, you can control how completions are styled using the `label_for_completion` method. For a complete list of methods, see the [API docs for the Zed extension API](https://docs.rs/zed_extension_api). - -### Multi-Language Support - -If your language server supports additional languages, you can use `language_ids` to map Zed `languages` to the desired [LSP-specific `languageId`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem) identifiers: - -```toml - -[language-servers.my-language-server] -name = "Whatever LSP" -languages = ["JavaScript", "HTML", "CSS"] - -[language-servers.my-language-server.language_ids] -"JavaScript" = "javascript" -"TSX" = "typescriptreact" -"HTML" = "html" -"CSS" = "css" -``` diff --git a/docs/src/extensions/mcp-extensions.md b/docs/src/extensions/mcp-extensions.md deleted file mode 100644 index cf9c5a611b..0000000000 --- a/docs/src/extensions/mcp-extensions.md +++ /dev/null @@ -1,44 +0,0 @@ -# MCP Server Extensions - -[Model Context Protocol servers](../ai/mcp.md) can be exposed as extensions for use in the Agent Panel. - -## Defining MCP Extensions - -A given extension may provide one or more MCP servers. -Each MCP server must be registered in the `extension.toml`: - -```toml -[context_servers.my-context-server] -``` - -Then, in the Rust code for your extension, implement the `context_server_command` method on your extension: - -```rust -impl zed::Extension for MyExtension { - fn context_server_command( - &mut self, - context_server_id: &ContextServerId, - project: &zed::Project, - ) -> Result { - Ok(zed::Command { - command: get_path_to_context_server_executable()?, - args: get_args_for_context_server()?, - env: get_env_for_context_server()?, - }) - } -} -``` - -This method should return the command to start up an MCP server, along with any arguments or environment variables necessary for it to function. - -If you need to download the MCP server from an external source—like GitHub Releases or npm—you can also do that in this function. - -## Available Extensions - -Check out all the MCP servers that have already been exposed as extensions [on Zed's site](https://zed.dev/extensions?filter=context-servers). - -We recommend taking a look at their repositories as a way to understand how they are generally created and structured. - -## Testing - -To test your new MCP server extension, you can [install it as a dev extension](./developing-extensions.md#developing-an-extension-locally). diff --git a/docs/src/extensions/slash-commands.md b/docs/src/extensions/slash-commands.md deleted file mode 100644 index 898649b327..0000000000 --- a/docs/src/extensions/slash-commands.md +++ /dev/null @@ -1,138 +0,0 @@ -# Slash Commands - -Extensions may provide slash commands for use in the Assistant. - -## Example extension - -To see a working example of an extension that provides slash commands, check out the [`slash-commands-example` extension](https://github.com/zed-industries/zed/tree/main/extensions/slash-commands-example). - -This extension can be [installed as a dev extension](./developing-extensions.md#developing-an-extension-locally) if you want to try it out for yourself. - -## Defining slash commands - -A given extension may provide one or more slash commands. Each slash command must be registered in the `extension.toml`. - -For example, here is an extension that provides two slash commands: `/echo` and `/pick-one`: - -```toml -[slash_commands.echo] -description = "echoes the provided input" -requires_argument = true - -[slash_commands.pick-one] -description = "pick one of three options" -requires_argument = true -``` - -Each slash command may define the following properties: - -- `description`: A description of the slash command that will be shown when completing available commands. -- `requires_argument`: Indicates whether a slash command requires at least one argument to run. - -## Implementing slash command behavior - -To implement behavior for your slash commands, implement `run_slash_command` for your extension. - -This method accepts the slash command that will be run, the list of arguments passed to it, and an optional `Worktree`. - -This method returns `SlashCommandOutput`, which contains the textual output of the command in the `text` field. The output may also define `SlashCommandOutputSection`s that contain ranges into the output. These sections are then rendered as creases in the Assistant's context editor. - -Your extension should `match` on the command name (without the leading `/`) and then execute behavior accordingly: - -```rs -impl zed::Extension for MyExtension { - fn run_slash_command( - &self, - command: SlashCommand, - args: Vec, - _worktree: Option<&Worktree>, - ) -> Result { - match command.name.as_str() { - "echo" => { - if args.is_empty() { - return Err("nothing to echo".to_string()); - } - - let text = args.join(" "); - - Ok(SlashCommandOutput { - sections: vec![SlashCommandOutputSection { - range: (0..text.len()).into(), - label: "Echo".to_string(), - }], - text, - }) - } - "pick-one" => { - let Some(selection) = args.first() else { - return Err("no option selected".to_string()); - }; - - match selection.as_str() { - "option-1" | "option-2" | "option-3" => {} - invalid_option => { - return Err(format!("{invalid_option} is not a valid option")); - } - } - - let text = format!("You chose {selection}."); - - Ok(SlashCommandOutput { - sections: vec![SlashCommandOutputSection { - range: (0..text.len()).into(), - label: format!("Pick One: {selection}"), - }], - text, - }) - } - command => Err(format!("unknown slash command: \"{command}\"")), - } - } -} -``` - -## Auto-completing slash command arguments - -For slash commands that have arguments, you may also choose to implement `complete_slash_command_argument` to provide completions for your slash commands. - -This method accepts the slash command that will be run and the list of arguments passed to it. It returns a list of `SlashCommandArgumentCompletion`s that will be shown in the completion menu. - -A `SlashCommandArgumentCompletion` consists of the following properties: - -- `label`: The label that will be shown in the completion menu. -- `new_text`: The text that will be inserted when the completion is accepted. -- `run_command`: Whether the slash command will be run when the completion is accepted. - -Once again, your extension should `match` on the command name (without the leading `/`) and return the desired argument completions: - -```rs -impl zed::Extension for MyExtension { - fn complete_slash_command_argument( - &self, - command: SlashCommand, - _args: Vec, - ) -> Result, String> { - match command.name.as_str() { - "echo" => Ok(vec![]), - "pick-one" => Ok(vec![ - SlashCommandArgumentCompletion { - label: "Option One".to_string(), - new_text: "option-1".to_string(), - run_command: true, - }, - SlashCommandArgumentCompletion { - label: "Option Two".to_string(), - new_text: "option-2".to_string(), - run_command: true, - }, - SlashCommandArgumentCompletion { - label: "Option Three".to_string(), - new_text: "option-3".to_string(), - run_command: true, - }, - ]), - command => Err(format!("unknown slash command: \"{command}\"")), - } - } -} -``` diff --git a/docs/src/extensions/themes.md b/docs/src/extensions/themes.md deleted file mode 100644 index ecdbdace59..0000000000 --- a/docs/src/extensions/themes.md +++ /dev/null @@ -1,53 +0,0 @@ -# Themes - -The `themes` directory in an extension should contain one or more theme files. - -Each theme file should adhere to the JSON schema specified at [`https://zed.dev/schema/themes/v0.2.0.json`](https://zed.dev/schema/themes/v0.2.0.json). - -See [this blog post](https://zed.dev/blog/user-themes-now-in-preview) for more details about creating themes. - -## Theme JSON Structure - -The structure of a Zed theme is defined in the [Zed Theme JSON Schema](https://zed.dev/schema/themes/v0.2.0.json). - -A Zed theme consists of a Theme Family object including: - -- `name`: The name for the theme family -- `author`: The name of the author of the theme family -- `themes`: An array of Themes belonging to the theme family - -The core components a Theme object include: - -1. Theme Metadata: - - - `name`: The name of the theme - - `appearance`: Either "light" or "dark" - -2. Style Properties under the `style`, such as: - - - `background`: The main background color - - `foreground`: The main text color - - `accent`: The accent color used for highlighting and emphasis - -3. Syntax Highlighting: - - - `syntax`: An object containing color definitions for various syntax elements (e.g., keywords, strings, comments) - -4. UI Elements: - - - Colors for various UI components such as: - - `element.background`: Background color for UI elements - - `border`: Border colors for different states (normal, focused, selected) - - `text`: Text colors for different states (normal, muted, accent) - -5. Editor-specific Colors: - - - Colors for editor-related elements such as: - - `editor.background`: Editor background color - - `editor.gutter`: Gutter colors - - `editor.line_number`: Line number colors - -6. Terminal Colors: - - ANSI color definitions for the integrated terminal - -We recommend looking at our [existing themes](https://github.com/zed-industries/zed/tree/main/assets/themes) to get a more comprehensive idea of what can be styled. diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md deleted file mode 100644 index 77bf9cef30..0000000000 --- a/docs/src/getting-started.md +++ /dev/null @@ -1,19 +0,0 @@ -# Getting Started - -Welcome to Zed! We are excited to have you. Zed is a powerful multiplayer code editor designed to stay out of your way and help you build what's next. - -## Key Features - -- [Smooth Editing](./configuring-zed.md): Built in Rust, Zed is responsive and intuitive, with a minimalistic aesthetic and pixel-level editing controls. -- [Agentic Editing](./ai/overview.md): Use Zed's hosted models to collaborate with agents directly in an IDE. You can also plug into a third-party agent or bring your own keys. -- [Debugger](./debugger.md): Debug your code in seconds, not hours, with minimal setup required. -- [Remote Development](./remote-development.md): Offload the heavy lifting to the cloud, so you can focus on writing code. -- [Extensions](./extensions.md): Leverage Zed's extensions to customize how you work. - -## Join the Zed Community - -Zed is proudly open source, and we get better with every contribution. Join us on GitHub or in Discord to contribute code, report bugs, or suggest features. - -- [Join Discord](https://discord.com/invite/zedindustries) -- [GitHub Discussions](https://github.com/zed-industries/zed/discussions) -- [Zed Reddit](https://www.reddit.com/r/ZedEditor) diff --git a/docs/src/git.md b/docs/src/git.md deleted file mode 100644 index d562eb4d0a..0000000000 --- a/docs/src/git.md +++ /dev/null @@ -1,250 +0,0 @@ ---- -description: Zed is a text editor that supports lots of Git features -title: Zed Editor Git integration documentation ---- - -# Git - -Zed currently offers a set of fundamental Git features, with support coming in the future for more advanced ones, like conflict resolution tools, line by line staging, and more. - -Here's an overview of all currently supported features: - -- Committing -- Staging, pushing, pulling, and fetching -- Project Diff: A multibuffer view of all changes -- Diff indicators in buffers and editor scrollbars -- Inline diff toggle and reverts in the editor for unstaged changes -- Git status in the Project Panel -- Branch creating and switching -- Git blame viewing -- Git stash pop, apply, drop and view - -## Git Panel - -The Git Panel gives you a birds-eye view of the state of your working tree and of Git's staging area. - -You can open the Git Panel using {#action git_panel::ToggleFocus}, or by clicking the Git icon in the status bar. - -In the panel you can see the state of your project at a glance—which repository and branch are active, what files have changed and the current staging state of each file. - -Zed monitors your repository so that changes you make on the command line are instantly reflected. - -### Configuration - -You can configure how Zed hard wraps commit messages with the `preferred-line-length` setting of the "Git Commit" language. The default is `72`, but it can be set to any number of characters `0` or more. - -The Git Panel also allows configuring the `soft_wrap` setting to adjust how commit messages display while you are typing them in the Git Panel. The default setting is `editor_width`, however, `none`, `preferred_line_length`, and `bounded` are also options. - -#### Example - -```json -"languages": { - "Git Commit": { - "soft_wrap": "editor_width", - "preferred_line_length": 72 - }, -} -``` - -## Project Diff - -You can see all of the changes captured by Git in Zed by opening the Project Diff ({#kb git::Diff}), accessible via the {#action git::Diff} action in the Command Palette or the Git Panel. - -All of the changes displayed in the Project Diff behave exactly the same as any other multibuffer: they are all editable excerpts of files. - -You can stage or unstage each hunk as well as a whole file by hitting the buttons on the tab bar or their corresponding keybindings. - - - -## Fetch, push, and pull - -Fetch, push, or pull from your Git repository in Zed via the buttons available on the Git Panel or via the Command Palette by looking at the respective actions: {#action git::Fetch}, {#action git::Push}, and {#action git::Pull}. - -## Staging Workflow - -Zed has two primary staging workflows, using either the Project Diff or the panel directly. - -### Using the Project Diff - -In the Project Diff view, you can focus on each hunk and stage them individually by clicking on the tab bar buttons or via the keybindings {#action git::StageAndNext} ({#kb git::StageAndNext}). - -Similarly, stage all hunks at the same time with the {#action git::StageAll} ({#kb git::StageAll}) keybinding and then immediately commit with {#action git::Commit} ({#kb git::Commit}). - -### Using the Git Panel - -From the panel, you can simply type a commit message and hit the commit button, or {#action git::Commit}. This will automatically stage all tracked files (indicated by a `[·]` in the entry's checkbox) and commit them. - - - -Entries can be staged using each individual entry's checkbox. All changes can be staged using the button at the top of the panel, or {#action git::StageAll}. - - - -## Committing - -Zed offers two commit textareas: - -1. The first one is available right at the bottom of the Git Panel. Hitting {#kb git::Commit} immediately commits all of your staged changes. -2. The second is available via the action {#action git::ExpandCommitEditor} or via hitting the {#kb git::ExpandCommitEditor} while focused in the Git Panel commit textarea. - -### Undoing a Commit - -As soon as you commit in Zed, in the Git Panel, you'll see a bar right under the commit textarea, which will show the recently submitted commit. -In there, you can use the "Uncommit" button, which performs the `git reset HEADˆ--soft` command. - -### Configuring Commit Line Length - -By default, Zed sets the commit line length to `72` but it can be configured in your local `settings.json` file. - -Find more information about setting the `preferred-line-length` in the [Configuration](#configuration) section. - -## Stashing - -Git stash allows you to temporarily save your uncommitted changes and revert your working directory to a clean state. This is particularly useful when you need to quickly switch branches or pull updates without committing incomplete work. - -### Creating Stashes - -To stash all your current changes, use the {#action git::StashAll} action. This will save both staged and unstaged changes to a new stash entry and clean your working directory. - -### Managing Stashes - -Zed provides a comprehensive stash picker accessible via {#action git::ViewStash}. From the stash picker, you can: - -- **View stash list**: Browse all your saved stashes with their descriptions and timestamps -- **Open diffs**: See exactly what changes are stored in each stash -- **Apply stashes**: Apply stash changes to your working directory while keeping the stash entry -- **Pop stashes**: Apply stash changes and remove the stash entry from the list -- **Drop stashes**: Delete unwanted stash entries without applying them - -### Quick Stash Operations - -For faster workflows, Zed provides direct actions to work with the most recent stash: - -- **Apply latest stash**: Use {#action git::StashApply} to apply the most recent stash without removing it -- **Pop latest stash**: Use {#action git::StashPop} to apply and remove the most recent stash - -### Stash Diff View - -When viewing a specific stash in the diff view, you have additional options available through the interface: - -- Apply the current stash to your working directory -- Pop the current stash (apply and remove) -- Remove the stash without applying changes - -To open the stash diff view, select a stash from the stash picker and use the {#action stash_picker::ShowStashItem} ({#kb stash_picker::ShowStashItem}) keybinding. - -## AI Support in Git - -Zed currently supports LLM-powered commit message generation. -You can ask AI to generate a commit message by focusing on the message editor within the Git Panel and either clicking on the pencil icon in the bottom left, or reaching for the {#action git::GenerateCommitMessage} ({#kb git::GenerateCommitMessage}) keybinding. - -> Note that you need to have an LLM provider configured for billing purposes, either via your own API keys or trialing/paying for Zed's hosted AI models. Visit [the AI configuration page](./ai/configuration.md) to learn how to do so. - -You can specify your preferred model to use by providing a `commit_message_model` agent setting. See [Feature-specific models](./ai/agent-settings.md#feature-specific-models) for more information. - -```json [settings] -{ - "agent": { - "version": "2", - "commit_message_model": { - "provider": "anthropic", - "model": "claude-3-5-haiku" - } - } -} -``` - - - -More advanced AI integration with Git features may come in the future. - -## Git Integrations - -Zed integrates with popular Git hosting services to ensure that Git commit hashes and references to Issues, Pull Requests, and Merge Requests become clickable links. - -Zed currently supports links to the hosted versions of -[GitHub](https://github.com), -[GitLab](https://gitlab.com), -[Bitbucket](https://bitbucket.org), -[SourceHut](https://sr.ht) and -[Codeberg](https://codeberg.org). - -For self-hosted GitHub, GitLab, or Bitbucket instances, add them to the `git_hosting_providers` setting so commit hashes and permalinks resolve to your domain: - -```json [settings] -{ - "git_hosting_providers": [ - { - "provider": "gitlab", - "name": "Corp GitLab", - "base_url": "https://git.example.corp" - } - ] -} -``` - -Zed also has a Copy Permalink feature to create a permanent link to a code snippet on your Git hosting service. -These links are useful for sharing a specific line or range of lines in a file at a specific commit. -Trigger this action via the [Command Palette](./getting-started.md#command-palette) (search for `permalink`), -by creating a [custom key bindings](key-bindings.md#custom-key-bindings) to the -`editor::CopyPermalinkToLine` or `editor::OpenPermalinkToLine` actions -or by simply right clicking and selecting `Copy Permalink` with line(s) selected in your editor. - -## Diff Hunk Keyboard Shortcuts - -When viewing files with changes, Zed displays diff hunks that can be expanded or collapsed for detailed review: - -- **Expand all diff hunks**: {#action editor::ExpandAllDiffHunks} ({#kb editor::ExpandAllDiffHunks}) -- **Collapse all diff hunks**: Press `Escape` (bound to {#action editor::Cancel}) -- **Toggle selected diff hunks**: {#action editor::ToggleSelectedDiffHunks} ({#kb editor::ToggleSelectedDiffHunks}) -- **Navigate between hunks**: {#action editor::GoToHunk} and {#action editor::GoToPreviousHunk} - -> **Tip:** The `Escape` key is the quickest way to collapse all expanded diff hunks and return to an overview of your changes. - -## Action Reference - -| Action | Keybinding | -| ----------------------------------------- | ------------------------------------- | -| {#action git::Add} | {#kb git::Add} | -| {#action git::StageAll} | {#kb git::StageAll} | -| {#action git::UnstageAll} | {#kb git::UnstageAll} | -| {#action git::ToggleStaged} | {#kb git::ToggleStaged} | -| {#action git::StageAndNext} | {#kb git::StageAndNext} | -| {#action git::UnstageAndNext} | {#kb git::UnstageAndNext} | -| {#action git::Commit} | {#kb git::Commit} | -| {#action git::ExpandCommitEditor} | {#kb git::ExpandCommitEditor} | -| {#action git::Push} | {#kb git::Push} | -| {#action git::ForcePush} | {#kb git::ForcePush} | -| {#action git::Pull} | {#kb git::Pull} | -| {#action git::PullRebase} | {#kb git::PullRebase} | -| {#action git::Fetch} | {#kb git::Fetch} | -| {#action git::Diff} | {#kb git::Diff} | -| {#action git::Restore} | {#kb git::Restore} | -| {#action git::RestoreFile} | {#kb git::RestoreFile} | -| {#action git::Branch} | {#kb git::Branch} | -| {#action git::Switch} | {#kb git::Switch} | -| {#action git::CheckoutBranch} | {#kb git::CheckoutBranch} | -| {#action git::Blame} | {#kb git::Blame} | -| {#action git::StashAll} | {#kb git::StashAll} | -| {#action git::StashPop} | {#kb git::StashPop} | -| {#action git::StashApply} | {#kb git::StashApply} | -| {#action git::ViewStash} | {#kb git::ViewStash} | -| {#action editor::ToggleGitBlameInline} | {#kb editor::ToggleGitBlameInline} | -| {#action editor::ExpandAllDiffHunks} | {#kb editor::ExpandAllDiffHunks} | -| {#action editor::ToggleSelectedDiffHunks} | {#kb editor::ToggleSelectedDiffHunks} | - -> Not all actions have default keybindings, but can be bound by [customizing your keymap](./key-bindings.md#user-keymaps). - -## Git CLI Configuration - -If you would like to also use Zed for your [git commit message editor](https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration#_core_editor) when committing from the command line you can use `zed --wait`: - -```sh -git config --global core.editor "zed --wait" -``` - -Or add the following to your shell environment (in `~/.zshrc`, `~/.bashrc`, etc): - -```sh -export GIT_EDITOR="zed --wait" -``` diff --git a/docs/src/globs.md b/docs/src/globs.md deleted file mode 100644 index 2f86fb9158..0000000000 --- a/docs/src/globs.md +++ /dev/null @@ -1,80 +0,0 @@ -# Globs - -Zed supports the use of [glob]() patterns that are the formal name for Unix shell-style path matching wildcards like `*.md` or `docs/src/**/*.md` supported by sh, bash, zsh, etc. A glob is similar but distinct from a [regex (regular expression)](https://en.wikipedia.org/wiki/Regular_expression). You may be In Zed these are commonly used when matching filenames. - -## Glob Flavor - -Zed uses two different rust crates for matching glob patterns: - -- [ignore crate](https://docs.rs/ignore/latest/ignore/) for matching glob patterns stored in `.gitignore` files -- [glob crate](https://docs.rs/glob/latest/glob/) for matching file paths in Zed - -While simple expressions are portable across environments (e.g. running `ls *.py` or `*.tmp` in a gitignore) there is significant divergence in the support for and syntax of more advanced features varies (character classes, exclusions, `**`, etc) across implementations. For the rest of this document we will be describing globs as supported in Zed via the `glob` crate implementation. Please see [References](#references) below for documentation links for glob pattern syntax for `.gitignore`, shells and other programming languages. - -The `glob` crate is implemented entirely in rust and does not rely on the `glob` / `fnmatch` interfaces provided by your platforms libc. This means that globs in Zed should behave similarly with across platforms. - -## Introduction - -A glob "pattern" is used to match a file name or complete file path. For example, when using "Search all files" {#kb project_search::ToggleFocus} you can click the funnel shaped Toggle Filters" button or {#kb project_search::ToggleFilters} and it will show additional search fields for "Include" and "Exclude" which support specifying glob patterns for matching file paths and file names. - -When creating a glob pattern you can use one or multiple special characters: - -| Special Character | Meaning | -| ----------------- | ----------------------------------------------------------------- | -| `?` | Matches any single character | -| `*` | Matches any (possibly empty) sequence of characters | -| `**` | Matches the current directory and arbitrary subdirectories | -| `[abc]` | Matches any one character in the brackets | -| `[a-z]` | Matches any of a range of characters (ordered by Unicode) | -| `[!...]` | The negation of `[...]` (matches a character not in the brackets) | - -Notes: - -1. Shell-style brace-expansions like `{a,b,c}` are not supported. -2. To match a literal `-` character inside brackets it must come first `[-abc]` or last `[abc-]`. -3. To match the literal `[` character use `[[]` or put it as the first character in the group `[[abc]`. -4. To match the literal `]` character use `[]]` or put it as the last character in the group `[abc]]`. - -## Examples - -### Matching file extensions - -If you wanted to only search Markdown files add `*.md` to the "Include" search field. - -### Case insensitive matching - -Globs in Zed are case-sensitive, so `*.c` will not match `main.C` (even on case-insensitive filesystems like HFS+/APFS on macOS). Instead use brackets to match characters. So instead of `*.c` use `*.[cC]`. - -### Matching directories - -If you wanted to search the [zed repository](https://github.com/zed-industries/zed) for examples of [Configuring Language Servers](https://zed.dev/docs/configuring-languages#configuring-language-servers) (under `"lsp"` in Zed settings.json) you could search for `"lsp"` and in the "Include" filter specify `docs/**/*.md`. This would only match files whose path was under the `docs` directory or any nested subdirectories `**/` of that folder with a filename that ends in `.md`. - -If instead you wanted to restrict yourself only to [Zed Language-Specific Documentation](https://zed.dev/docs/languages) pages you could define a narrower pattern of: `docs/src/languages/*.md` this would match [`docs/src/languages/rust.md`](https://github.com/zed-industries/zed/blob/main/docs/src/languages/rust.md) and [`docs/src/languages/cpp.md`](https://github.com/zed-industries/zed/blob/main/docs/src/languages/cpp.md) but not [`docs/src/configuring-languages.md`](https://github.com/zed-industries/zed/blob/main/docs/src/configuring-languages.md). - -### Implicit Wildcards - -When using the "Include" / "Exclude" filters on a Project Search each glob is wrapped in implicit wildcards. For example to exclude any files with license in the path or filename from your search just type `license` in the exclude box. Behind the scenes Zed transforms `license` to `**license**`. This means that files named `license.*`, `*.license` or inside a `license` subdirectory will all be filtered out. This enables users to easily filter for `*.ts` without having to remember to type `**/*.ts` every time. - -Alternatively, if in your Zed settings you wanted a [`file_types`](./configuring-zed.md#file-types) override which only applied to a certain directory you must explicitly include the wildcard globs. For example, if you had a directory of template files with the `html` extension that you wanted to recognize as Jinja2 template you could use the following: - -```json [settings] -{ - "file_types": { - "C++": ["[cC]"], - "Jinja2": ["**/templates/*.html"] - } -} -``` - -## References - -While globs in Zed are implemented as described above, when writing code using globs in other languages, please reference your platform's glob documentation: - -- [macOS fnmatch](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/fnmatch.3.html) (BSD C Standard Library) -- [Linux fnmatch](https://www.gnu.org/software/libc/manual/html_node/Wildcard-Matching.html) (GNU C Standard Library) -- [POSIX fnmatch](https://pubs.opengroup.org/onlinepubs/9699919799/functions/fnmatch.html) (POSIX Specification) -- [node-glob](https://github.com/isaacs/node-glob) (Node.js `glob` package) -- [Python glob](https://docs.python.org/3/library/glob.html) (Python Standard Library) -- [Golang glob](https://pkg.go.dev/path/filepath#Match) (Go Standard Library) -- [gitignore patterns](https://git-scm.com/docs/gitignore) (Gitignore Pattern Format) -- [PowerShell: About Wildcards](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_wildcards) (Wildcards in PowerShell) diff --git a/docs/src/helix.md b/docs/src/helix.md deleted file mode 100644 index 467a2fac7c..0000000000 --- a/docs/src/helix.md +++ /dev/null @@ -1,15 +0,0 @@ -# Helix Mode - -_Work in progress! Not all Helix keybindings are implemented yet._ - -Zed's Helix mode is an emulation layer that brings Helix-style keybindings and modal editing to Zed. It builds upon Zed's [Vim mode](./vim.md), so much of the core functionality is shared. Enabling `helix_mode` will also enable `vim_mode`. - -For a guide on Vim-related features that are also available in Helix mode, please refer to our [Vim mode documentation](./vim.md). - -To check the current status of Helix mode, or to request a missing Helix feature, checkout out the ["Are we Helix yet?" discussion](https://github.com/zed-industries/zed/discussions/33580). - -For a detailed list of Helix's default keybindings, please visit the [official Helix documentation](https://docs.helix-editor.com/keymap.html). - -## Core differences - -Any text object that works with `m i` or `m a` also works with `]` and `[`, so for example `] (` selects the next pair of parentheses after the cursor. diff --git a/docs/src/icon-themes.md b/docs/src/icon-themes.md deleted file mode 100644 index 72fc51b834..0000000000 --- a/docs/src/icon-themes.md +++ /dev/null @@ -1,35 +0,0 @@ -# Icon Themes - -Zed comes with a built-in icon theme, with more icon themes available as extensions. - -## Selecting an Icon Theme - -See what icon themes are installed and preview them via the Icon Theme Selector, which you can open from the command palette with `icon theme selector: toggle`. - -Navigating through the icon theme list by moving up and down will change the icon theme in real time and hitting enter will save it to your settings file. - -## Installing more Icon Themes - -More icon themes are available from the Extensions page, which you can access via the command palette with `zed: extensions` or the [Zed website](https://zed.dev/extensions?filter=icon-themes). - -## Configuring Icon Themes - -Your selected icon theme is stored in your settings file. -You can open your settings file from the command palette with {#action zed::OpenSettingsFile} (bound to {#kb zed::OpenSettingsFile}). - -Just like with themes, Zed allows for configuring different icon themes for light and dark mode. -You can set the mode to `"light"` or `"dark"` to ignore the current system mode. - -```json [settings] -{ - "icon_theme": { - "mode": "system", - "light": "Light Icon Theme", - "dark": "Dark Icon Theme" - } -} -``` - -## Icon Theme Development - -See: [Developing Zed Icon Themes](./extensions/icon-themes.md) diff --git a/docs/src/installation.md b/docs/src/installation.md deleted file mode 100644 index 7802ef7776..0000000000 --- a/docs/src/installation.md +++ /dev/null @@ -1,109 +0,0 @@ -# Installing Zed - -## Download Zed - -### macOS - -Get the latest stable builds via [the download page](https://zed.dev/download). If you want to download our preview build, you can find it on its [releases page](https://zed.dev/releases/preview). After the first manual installation, Zed will periodically check for install updates. - -You can also install Zed stable via Homebrew: - -```sh -brew install --cask zed -``` - -As well as Zed preview: - -```sh -brew install --cask zed@preview -``` - -### Windows - -Get the latest stable builds via [the download page](https://zed.dev/download). If you want to download our preview build, you can find it on its [releases page](https://zed.dev/releases/preview). After the first manual installation, Zed will periodically check for install updates. - -### Linux - -For most Linux users, the easiest way to install Zed is through our installation script: - -```sh -curl -f https://zed.dev/install.sh | sh -``` - -If you'd like to help us test our new features, you can also install our preview build: - -```sh -curl -f https://zed.dev/install.sh | ZED_CHANNEL=preview sh -``` - -This script supports `x86_64` and `AArch64`, as well as common Linux distributions: Ubuntu, Arch, Debian, RedHat, CentOS, Fedora, and more. - -If Zed is installed using this installation script, it can be uninstalled at any time by running the shell command `zed --uninstall`. The shell will then prompt you whether you'd like to keep your preferences or delete them. After making a choice, you should see a message that Zed was successfully uninstalled. - -If this script is insufficient for your use case, you run into problems running Zed, or there are errors in uninstalling Zed, please see our [Linux-specific documentation](./linux.md). - -## System Requirements - -### macOS - -Zed supports the follow macOS releases: - -| Version | Codename | Apple Status | Zed Status | -| ------------- | -------- | -------------- | ------------------- | -| macOS 26.x | Tahoe | Supported | Supported | -| macOS 15.x | Sequoia | Supported | Supported | -| macOS 14.x | Sonoma | Supported | Supported | -| macOS 13.x | Ventura | Supported | Supported | -| macOS 12.x | Monterey | EOL 2024-09-16 | Supported | -| macOS 11.x | Big Sur | EOL 2023-09-26 | Partially Supported | -| macOS 10.15.x | Catalina | EOL 2022-09-12 | Partially Supported | - -The macOS releases labelled "Partially Supported" (Big Sur and Catalina) do not support screen sharing via Zed Collaboration. These features use the [LiveKit SDK](https://livekit.io) which relies upon [ScreenCaptureKit.framework](https://developer.apple.com/documentation/screencapturekit/) only available on macOS 12 (Monterey) and newer. - -#### Mac Hardware - -Zed supports machines with Intel (x86_64) or Apple (aarch64) processors that meet the above macOS requirements: - -- MacBook Pro (Early 2015 and newer) -- MacBook Air (Early 2015 and newer) -- MacBook (Early 2016 and newer) -- Mac Mini (Late 2014 and newer) -- Mac Pro (Late 2013 or newer) -- iMac (Late 2015 and newer) -- iMac Pro (all models) -- Mac Studio (all models) - -### Linux - -Zed supports 64-bit Intel/AMD (x86_64) and 64-bit Arm (aarch64) processors. - -Zed requires a Vulkan 1.3 driver and the following desktop portals: - -- `org.freedesktop.portal.FileChooser` -- `org.freedesktop.portal.OpenURI` -- `org.freedesktop.portal.Secret` or `org.freedesktop.Secrets` - -### Windows - -Zed supports the following Windows releases: -| Version | Zed Status | -| ------------------------- | ------------------- | -| Windows 11, version 22H2 and later | Supported | -| Windows 10, version 1903 and later | Supported | - -A 64-bit operating system is required to run Zed. - -#### Windows Hardware - -Zed supports machines with x64 (Intel, AMD) or Arm64 (Qualcomm) processors that meet the following requirements: - -- Graphics: A GPU that supports DirectX 11 (most PCs from 2012+). -- Driver: Current NVIDIA/AMD/Intel/Qualcomm driver (not the Microsoft Basic Display Adapter). - -### FreeBSD - -Not yet available as an official download. Can be built [from source](./development/freebsd.md). - -### Web - -Not supported at this time. See our [Platform Support issue](https://github.com/zed-industries/zed/issues/5391). diff --git a/docs/src/key-bindings.md b/docs/src/key-bindings.md deleted file mode 100644 index f0f1e472c7..0000000000 --- a/docs/src/key-bindings.md +++ /dev/null @@ -1,299 +0,0 @@ -# Key bindings - -Zed has a very customizable key binding system—you can tweak everything to work exactly how your fingers expect! - -## Predefined Keymaps - -If you're used to a specific editor's defaults, you can change your `base_keymap` through the settings window ({#kb zed::OpenSettings}) or directly through your `settings.json` file ({#kb zed::OpenSettingsFile}). -We currently support: - -- VS Code (default) -- Atom -- Emacs (Beta) -- JetBrains -- Sublime Text -- TextMate -- Cursor -- None (disables _all_ key bindings) - -This setting can also be changed via the command palette through the `zed: toggle base keymap selector` action. - -You can also enable `vim_mode` or `helix_mode`, which add modal bindings. -For more information, see the documentation for [Vim mode](./vim.md) and [Helix mode](./helix.md). - -## Keymap Editor - -You can access the keymap editor through the {#kb zed::OpenKeymap} action or by running {#action zed::OpenKeymap} action from the command palette. You can easily add or change a keybind for an action with the `Change Keybinding` or `Add Keybinding` button on the command pallets left bottom corner. - -In there, you can see all of the existing actions in Zed as well as the associated keybindings set to them by default. - -You can also customize them right from there, either by clicking on the pencil icon that appears when you hover over a particular action, by double-clicking on the action row, or by pressing the `enter` key. - -Anything that you end up doing on the keymap editor also gets reflected on the `keymap.json` file. - -## User Keymaps - -The keymap file is stored in the following locations for each platform: - -- macOS/Linux: `~/.config/zed/keymap.json` -- Windows: `~\AppData\Roaming\Zed/keymap.json` - -You can open the keymap with the {#action zed::OpenKeymapFile} action from the command palette. - -This file contains a JSON array of objects with `"bindings"`. -If no `"context"` is set, the bindings are always active. -If it is set, the binding is only active when the [context matches](#contexts). - -Within each binding section, a [key sequence](#keybinding-syntax) is mapped to [an action](#actions). -If conflicts are detected, they are resolved as [described below](#precedence). - -If you are using a non-QWERTY, Latin-character keyboard, you may want to set `use_key_equivalents` to `true`. See [Non-QWERTY keyboards](#non-qwerty-keyboards) for more information. - -For example: - -```json [keymap] -[ - { - "bindings": { - "ctrl-right": "editor::SelectLargerSyntaxNode", - "ctrl-left": "editor::SelectSmallerSyntaxNode" - } - }, - { - "context": "ProjectPanel && not_editing", - "bindings": { - "o": "project_panel::Open" - } - } -] -``` - -You can see all of Zed's default bindings for each platform in the default keymaps files: - -- [macOS](https://github.com/zed-industries/zed/blob/main/assets/keymaps/default-macos.json) -- [Windows](https://github.com/zed-industries/zed/blob/main/assets/keymaps/default-windows.json) -- [Linux](https://github.com/zed-industries/zed/blob/main/assets/keymaps/default-linux.json). - -If you want to debug problems with custom keymaps, you can use `dev: Open Key Context View` from the command palette. -Please file [an issue](https://github.com/zed-industries/zed) if you run into something you think should work but isn't. - -### Keybinding Syntax - -Zed has the ability to match against not just a single keypress, but a sequence of keys typed in order. Each key in the `"bindings"` map is a sequence of keypresses separated with a space. - -Each keypress is a sequence of modifiers followed by a key. The modifiers are: - -- `ctrl-` The control key -- `cmd-`, `win-` or `super-` for the platform modifier (Command on macOS, Windows key on Windows, and the Super key on Linux). -- `alt-` for alt (option on macOS) -- `shift-` The shift key -- `fn-` The function key -- `secondary-` Equivalent to `cmd` when Zed is running on macOS and `ctrl` when on Windows and Linux - -The keys can be any single Unicode codepoint that your keyboard generates (for example `a`, `0`, `£` or `ç`), or any named key (`tab`, `f1`, `shift`, or `cmd`). If you are using a non-Latin layout (e.g. Cyrillic), you can bind either to the Cyrillic character or the Latin character that key generates with `cmd` pressed. - -A few examples: - -```json [settings] - "bindings": { - "cmd-k cmd-s": "zed::OpenKeymap", // matches ⌘-k then ⌘-s - "space e": "editor::Complete", // type space then e - "ç": "editor::Complete", // matches ⌥-c - "shift shift": "file_finder::Toggle", // matches pressing and releasing shift twice - } -``` - -The `shift-` modifier can only be used in combination with a letter to indicate the uppercase version. For example, `shift-g` matches typing `G`. Although on many keyboards shift is used to type punctuation characters like `(`, the keypress is not considered to be modified, and so `shift-(` does not match. - -The `alt-` modifier can be used on many layouts to generate a different key. For example, on a macOS US keyboard, the combination `alt-c` types `ç`. You can match against either in your keymap file, though by convention, Zed spells this combination as `alt-c`. - -It is possible to match against typing a modifier key on its own. For example, `shift shift` can be used to implement JetBrains' 'Search Everywhere' shortcut. In this case, the binding happens on key release instead of on keypress. - -### Contexts - -If a binding group has a `"context"` key, it will be matched against the currently active contexts in Zed. - -Zed's contexts make up a tree, with the root being `Workspace`. Workspaces contain Panes and Panels, and Panes contain Editors, etc. The easiest way to see what contexts are active at a given moment is the key context view, which you can get to with the `dev: open key context view` command in the command palette. - -For example: - -``` -# in an editor, it might look like this: -Workspace os=macos keyboard_layout=com.apple.keylayout.QWERTY - Pane - Editor mode=full extension=md vim_mode=insert - -# in the project panel -Workspace os=macos - Dock - ProjectPanel not_editing -``` - -Context expressions can contain the following syntax: - -- `X && Y`, `X || Y` to and/or two conditions -- `!X` to check that a condition is false -- `(X)` for grouping -- `X > Y` to match if an ancestor in the tree matches X and this layer matches Y. - -For example: - -- `"context": "Editor"` - matches any editor (including inline inputs) -- `"context": "Editor && mode == full"` - matches the main editors used for editing code -- `"context": "!Editor && !Terminal"` - matches anywhere except where an Editor or Terminal is focused -- `"context": "os == macos > Editor"` - matches any editor on macOS. - -It's worth noting that attributes are only available on the node they are defined on. This means that if you want to (for example) only enable a keybinding when the debugger is stopped in vim normal mode, you need to do `debugger_stopped > vim_mode == normal`. - -> Note: Before Zed v0.197.x, the `!` operator only looked at one node at a time, and `>` meant "parent" not "ancestor". This meant that `!Editor` would match the context `Workspace > Pane > Editor`, because (confusingly) the Pane matches `!Editor`, and that `os == macos > Editor` did not match the context `Workspace > Pane > Editor` because of the intermediate `Pane` node. - -If you're using Vim mode, we have information on how [vim modes influence the context](./vim.md#contexts). Helix mode is built on top of Vim mode and uses the same contexts. - -### Actions - -Almost all of Zed's functionality is exposed as actions. -Although there is no explicitly documented list, you can find most of them by searching in the command palette, by looking in the default keymaps for [macOS](https://github.com/zed-industries/zed/blob/main/assets/keymaps/default-macos.json), [Windows](https://github.com/zed-industries/zed/blob/main/assets/keymaps/default-windows.json) or [Linux](https://github.com/zed-industries/zed/blob/main/assets/keymaps/default-linux.json), or by using Zed's autocomplete in your keymap file. - -Most actions do not require any arguments, and so you can bind them as strings: `"ctrl-a": "language_selector::Toggle"`. Some require a single argument and must be bound as an array: `"cmd-1": ["workspace::ActivatePane", 0]`. Some actions require multiple arguments and are bound as an array of a string and an object: `"ctrl-a": ["pane::DeploySearch", { "replace_enabled": true }]`. - -### Precedence - -When multiple keybindings have the same keystroke and are active at the same time, precedence is resolved in two ways: - -- Bindings that match on lower nodes in the context tree win. This means that if you have a binding with a context of `Editor`, it will take precedence over a binding with a context of `Workspace`. Bindings with no context match at the lowest level in the tree. -- If there are multiple bindings that match at the same level in the tree, then the binding defined later takes precedence. As user keybindings are loaded after system keybindings, this allows user bindings to take precedence over built-in keybindings. - -The other kind of conflict that arises is when you have two bindings, one of which is a prefix of the other. For example, if you have `"ctrl-w":"editor::DeleteToNextWordEnd"` and `"ctrl-w left":"editor::DeleteToEndOfLine"`. - -When this happens, and both bindings are active in the current context, Zed will wait for 1 second after you type `ctrl-w` to see if you're about to type `left`. If you don't type anything, or if you type a different key, then `DeleteToNextWordEnd` will be triggered. If you do, then `DeleteToEndOfLine` will be triggered. - -### Non-QWERTY keyboards - -Zed's support for non-QWERTY keyboards is still a work in progress. - -If your keyboard can type the full ASCII range (DVORAK, COLEMAK, etc.), then shortcuts should work as you expect. - -Otherwise, read on... - -#### macOS - -On Cyrillic, Hebrew, Armenian, and other keyboards that are mostly non-ASCII, macOS automatically maps keys to the ASCII range when `cmd` is held. Zed takes this a step further, and it can always match key-presses against either the ASCII layout or the real layout, regardless of modifiers and the `use_key_equivalents` setting. For example, in Thai, pressing `ctrl-ๆ` will match bindings associated with `ctrl-q` or `ctrl-ๆ`. - -On keyboards that support extended Latin alphabets (French AZERTY, German QWERTZ, etc.), it is often not possible to type the entire ASCII range without `option`. This introduces an ambiguity: `option-2` produces `@`. To ensure that all the built-in keyboard shortcuts can still be typed on these keyboards, we move key bindings around. For example, shortcuts bound to `@` on QWERTY are moved to `"` on a Spanish layout. This mapping is based on the macOS system defaults and can be seen by running `dev: open key context view` from the command palette. - -If you are defining shortcuts in your personal keymap, you can opt into the key equivalent mapping by setting `use_key_equivalents` to `true` in your keymap: - -```json [keymap] -[ - { - "use_key_equivalents": true, - "bindings": { - "ctrl->": "editor::Indent" // parsed as ctrl-: when a German QWERTZ keyboard is active - } - } -] -``` - -### Linux - -Since v0.196.0, on Linux, if the key that you type doesn't produce an ASCII character, then we use the QWERTY-layout equivalent key for keyboard shortcuts. This means that many shortcuts can be typed on many layouts. - -We do not yet move shortcuts around to ensure that all the built-in shortcuts can be typed on every layout, so if there are some ASCII characters that cannot be typed, and your keyboard layout has different ASCII characters on the same keys as would be needed to type them, you may need to add custom key bindings to make this work. We do intend to fix this at some point, and help is very much appreciated! - -## Tips and tricks - -### Disabling a binding - -If you'd like a given binding to do nothing in a given context, you can use -`null` as the action. This is useful if you hit the key binding by accident and -want to disable it, or if you want to type the character that would be typed by -the sequence, or if you want to disable multikey bindings starting with that key. - -```json [keymap] -[ - { - "context": "Workspace", - "bindings": { - "cmd-r": null // cmd-r will do nothing when the Workspace context is active - } - } -] -``` - -A `null` binding follows the same precedence rules as normal actions, so it disables all bindings that would match further up in the tree too. If you'd like a binding that matches further up in the tree to take precedence over a lower binding, you need to rebind it to the action you want in the context you want. - -This is useful for preventing Zed from falling back to a default key binding when the action you specified is conditional and propagates. For example, `buffer_search::DeployReplace` only triggers when the search bar is not in view. If the search bar is in view, it would propagate and trigger the default action set for that key binding, such as opening the right dock. To prevent this from happening: - -```json [keymap] -[ - { - "context": "Workspace", - "bindings": { - "cmd-r": null // cmd-r will do nothing when the search bar is in view - } - }, - { - "context": "Workspace", - "bindings": { - "cmd-r": "buffer_search::DeployReplace" // cmd-r will deploy replace when the search bar is not in view - } - } -] -``` - -### Remapping keys - -A common request is to be able to map from a single keystroke to a sequence. You can do this with the `workspace::SendKeystrokes` action. - -```json [keymap] -[ - { - "bindings": { - // Move down four times - "alt-down": ["workspace::SendKeystrokes", "down down down down"], - // Expand the selection (editor::SelectLargerSyntaxNode); - // copy to the clipboard; and then undo the selection expansion. - "cmd-alt-c": [ - "workspace::SendKeystrokes", - "ctrl-shift-right ctrl-shift-right ctrl-shift-right cmd-c ctrl-shift-left ctrl-shift-left ctrl-shift-left" - ] - } - }, - { - "context": "Editor && vim_mode == insert", - "bindings": { - "j k": ["workspace::SendKeystrokes", "escape"] - } - } -] -``` - -There are some limitations to this, notably: - -- Any asynchronous operation will not happen until after all your key bindings have been dispatched. For example, this means that while you can use a binding to open a file (as in the `cmd-alt-r` example), you cannot send further keystrokes and hope to have them interpreted by the new view. -- Other examples of asynchronous things are: opening the command palette, communicating with a language server, changing the language of a buffer, anything that hits the network. -- There is a limit of 100 simulated keys at a time. - -The argument to `SendKeystrokes` is a space-separated list of keystrokes (using the same syntax as above). Due to the way that keystrokes are parsed, any segment that is not recognized as a keypress will be sent verbatim to the currently focused input field. - -If the argument to `SendKeystrokes` contains the binding used to trigger it, it will use the next-highest-precedence definition of that binding. This allows you to extend the default behavior of a key binding. - -### Forward keys to terminal - -If you're on Linux or Windows, you might find yourself wanting to forward key combinations to the built-in terminal instead of them being handled by Zed. - -For example, `ctrl-n` creates a new tab in Zed on Linux. If you want to send `ctrl-n` to the built-in terminal when it's focused, add the following to your keymap: - -```json [settings] -{ - "context": "Terminal", - "bindings": { - "ctrl-n": ["terminal::SendKeystroke", "ctrl-n"] - } -} -``` - -### Task Key bindings - -You can also bind keys to launch Zed Tasks defined in your `tasks.json`. -See the [tasks documentation](tasks.md#custom-keybindings-for-tasks) for more. diff --git a/docs/src/languages.md b/docs/src/languages.md deleted file mode 100644 index faee42176c..0000000000 --- a/docs/src/languages.md +++ /dev/null @@ -1,168 +0,0 @@ -# Language Support in Zed - -Zed supports hundreds of programming languages and text formats. -Some work out-of-the box and others rely on 3rd party extensions. - -> The ones included out-of-the-box, natively built into Zed, are marked with \*. - -## Languages with Documentation - -- [Ansible](./languages/ansible.md) -- [AsciiDoc](./languages/asciidoc.md) -- [Astro](./languages/astro.md) -- [Bash](./languages/bash.md) -- [Biome](./languages/biome.md) -- [C](./languages/c.md) \* -- [C++](./languages/cpp.md) \* -- [C#](./languages/csharp.md) -- [Clojure](./languages/clojure.md) -- [CSS](./languages/css.md) \* -- [Dart](./languages/dart.md) -- [Deno](./languages/deno.md) -- [Diff](./languages/diff.md) \* -- [Docker](./languages/docker.md) -- [Elixir](./languages/elixir.md) -- [Elm](./languages/elm.md) -- [Emmet](./languages/emmet.md) -- [Erlang](./languages/erlang.md) -- [Fish](./languages/fish.md) -- [GDScript](./languages/gdscript.md) -- [Gleam](./languages/gleam.md) -- [GLSL](./languages/glsl.md) -- [Go](./languages/go.md) \* -- [Groovy](./languages/groovy.md) -- [Haskell](./languages/haskell.md) -- [Helm](./languages/helm.md) -- [HTML](./languages/html.md) -- [Java](./languages/java.md) -- [JavaScript](./languages/javascript.md) \* -- [Julia](./languages/julia.md) -- [JSON](./languages/json.md) \* -- [Jsonnet](./languages/jsonnet.md) -- [Kotlin](./languages/kotlin.md) -- [Lua](./languages/lua.md) -- [Luau](./languages/luau.md) -- [Makefile](./languages/makefile.md) -- [Markdown](./languages/markdown.md) \* -- [Nim](./languages/nim.md) -- [OCaml](./languages/ocaml.md) -- [PHP](./languages/php.md) -- [Prisma](./languages/prisma.md) -- [Proto](./languages/proto.md) -- [PureScript](./languages/purescript.md) -- [Python](./languages/python.md) \* -- [R](./languages/r.md) -- [Rego](./languages/rego.md) -- [ReStructuredText](./languages/rst.md) -- [Racket](./languages/racket.md) -- [Roc](./languages/roc.md) -- [Ruby](./languages/ruby.md) -- [Rust](./languages/rust.md) \* (Zed's written in Rust) -- [Scala](./languages/scala.md) -- [Scheme](./languages/scheme.md) -- [Shell Script](./languages/sh.md) -- [Svelte](./languages/svelte.md) -- [Swift](./languages/swift.md) -- [Tailwind CSS](./languages/tailwindcss.md) \* -- [Terraform](./languages/terraform.md) -- [TOML](./languages/toml.md) -- [TypeScript](./languages/typescript.md) \* -- [Uiua](./languages/uiua.md) -- [Vue](./languages/vue.md) -- [XML](./languages/xml.md) -- [YAML](./languages/yaml.md) \* -- [Yara](./languages/yarn.md) -- [Yarn](./languages/yarn.md) -- [Zig](./languages/zig.md) - -## Additional Community Language Extensions - -- [Ada](https://github.com/wisn/zed-ada-language) -- [Aiken](https://github.com/aiken-lang/zed-aiken) -- [Amber](https://github.com/amber-lang/zed-amber-extension) -- [Assembly](https://github.com/DevBlocky/zed-asm) -- [AWK](https://github.com/dangh/zed-awk) -- [Beancount](https://github.com/zed-extensions/beancount) -- [Bend](https://github.com/mrpedrobraga/zed-bend) -- [Blade](https://github.com/bajrangCoder/zed-laravel-blade) -- [Blueprint](https://github.com/tfuxu/zed-blueprint) -- [BQN](https://github.com/DavidZwitser/zed-bqn) -- [Brainfuck](https://github.com/JosephTLyons/zed-brainfuck) -- [Cadence](https://github.com/janezpodhostnik/cadence.zed) -- [Cairo](https://github.com/trbutler4/zed-cairo) -- [Cap'n Proto](https://github.com/cmackenzie1/zed-capnp) -- [Cedar](https://github.com/chrnorm/zed-cedar) -- [CFEngine policy language](https://github.com/olehermanse/zed-cfengine) -- [CSV](https://github.com/huacnlee/zed-csv) -- [Cucumber/Gherkin](https://github.com/thlcodes/zed-extension-cucumber) -- [CUE](https://github.com/jkasky/zed-cue) -- [Curry](https://github.com/fwcd/zed-curry) -- [D](https://github.com/staysail/zed-d) -- [Database Markup Language (DBML)](https://github.com/shuklaayush/zed-dbml) -- [Earthfile](https://github.com/glehmann/earthfile.zed) -- [EJS template](https://github.com/dangh/zed-ejs) -- [Elisp](https://github.com/JosephTLyons/zed-elisp) -- [Ember](https://github.com/jylamont/zed-ember) -- [Env](https://github.com/zarifpour/zed-env) -- [Exograph](https://github.com/exograph/zed-extension) -- [Fortran](https://github.com/Xavier-Maruff/zed-fortran) -- [F#](https://github.com/nathanjcollins/zed-fsharp) -- [Gemini gemtext](https://github.com/clseibold/gemini-zed) -- [Git Firefly](https://github.com/d1y/git_firefly) -- [GraphQL](https://github.com/11bit/zed-extension-graphql) -- [Groq](https://github.com/juice49/zed-groq) -- [INI](https://github.com/bajrangCoder/zed-ini) -- [Java](https://github.com/zed-extensions/java) -- [Justfiles](https://github.com/jackTabsCode/zed-just) -- [LaTeX](https://github.com/rzukic/zed-latex) -- [Ledger](https://github.com/mrkstwrt/zed-ledger) -- [Less](https://github.com/jimliang/zed-less) -- [LilyPond](https://github.com/nwhetsell/lilypond-zed-extension) -- [Liquid](https://github.com/TheBeyondGroup/zed-shopify-liquid) -- [Log](https://github.com/evrensen467/zed-log) -- [Lox](https://github.com/arian81/zed-lox) -- [Markdown Oxide](https://github.com/Feel-ix-343/markdown-oxide-zed) -- [Marksman](https://github.com/vitallium/zed-marksman) -- [Matlab](https://github.com/rzukic/zed-matlab) -- [Meson](https://github.com/hqnna/zed-meson) -- [Navi](https://github.com/navi-language/zed-navi) -- [NeoCMake](https://github.com/k0tran/zed_neocmake) -- [Nginx](https://github.com/d1y/nginx-zed) -- [Nim](https://github.com/foxoman/zed-nim) -- [Nix](https://github.com/zed-extensions/nix) -- [Noir](https://github.com/shuklaayush/zed-noir) -- [Nu](https://github.com/zed-extensions/nu) -- [Odin](https://github.com/rxptr/zed-odin) -- [Pact](https://github.com/kadena-community/pact-zed) -- [Pest](https://github.com/pest-parser/zed-pest) -- [PICA200 assembly](https://github.com/Squareheron942/zed-pica200) -- [Pkl](https://github.com/Moshyfawn/pkl-zed) -- [PlaydateSDK](https://github.com/notpeter/playdate-zed-extension) -- [QML](https://github.com/lkroll/zed-qml) -- [Rainbow CSV](https://github.com/weartist/zed-rainbow-csv) -- [Rego](https://github.com/StyraInc/zed-rego) -- [Rescript](https://github.com/humaans/rescript-zed) -- [Roclang](https://github.com/h2000/zed-roc) -- [Ron](https://github.com/onbjerg/zed-ron) -- [Metals](https://github.com/scalameta/metals-zed) -- [SCSS](https://github.com/bajrangCoder/zed-scss) -- [Slim](https://github.com/calmyournerves/zed-slim) -- [Slint](https://gitlab.com/flukejones/zed-slint) -- [Smithy](https://github.com/joshrutkowski/zed-smithy) -- [Solidity](https://github.com/zarifpour/zed-solidity) -- [SQL](https://github.com/evrensen467/zed-sql) -- [Strace](https://github.com/sigmaSd/zed-strace) -- [Swift](https://github.com/zed-extensions/swift) -- [Templ](https://github.com/makifdb/zed-templ) -- [Tmux](https://github.com/dangh/zed-tmux) -- [Twig](https://github.com/YussufSassi/zed-twig) -- [Typst](https://github.com/WeetHet/typst.zed) -- [Unison](https://github.com/zetashift/unison-zed) -- [UnoCSS](https://github.com/bajrangCoder/zed-unocss) -- [Vlang](https://github.com/lv37/zed-v) -- [Vala](https://github.com/FyraLabs/zed-vala) -- [Vale](https://github.com/koozz/zed-vale) -- [Verilog](https://github.com/someone13574/zed-verilog-extension) -- [VHS](https://github.com/eth0net/zed-vhs) -- [Wgsl](https://github.com/luan/zed-wgsl) -- [WIT](https://github.com/valentinegb/zed-wit) diff --git a/docs/src/languages/ansible.md b/docs/src/languages/ansible.md deleted file mode 100644 index bce25ddc6c..0000000000 --- a/docs/src/languages/ansible.md +++ /dev/null @@ -1,130 +0,0 @@ -# Ansible - -Support for Ansible in Zed is provided via a community-maintained [Ansible extension](https://github.com/kartikvashistha/zed-ansible). - -- Tree-sitter: [zed-industries/tree-sitter-yaml](https://github.com/zed-industries/tree-sitter-yaml) -- Language Server: [ansible/vscode-ansible](https://github.com/ansible/vscode-ansible/tree/main/packages/ansible-language-server) - -## Setup - -### File detection - -To avoid mishandling non-Ansible YAML files, the Ansible Language is not associated with any file extensions by default. To change this behavior you can add a `"file_types"` section to Zed settings inside your project (`.zed/settings.json`) or your Zed user settings (`~/.config/zed/settings.json`) to match your folder/naming conventions. For example: - -```json [settings] -"file_types": { - "Ansible": [ - "**.ansible.yml", - "**.ansible.yaml", - "**/defaults/*.yml", - "**/defaults/*.yaml", - "**/meta/*.yml", - "**/meta/*.yaml", - "**/tasks/*.yml", - "**/tasks/*.yaml", - "**/handlers/*.yml", - "**/handlers/*.yaml", - "**/group_vars/*.yml", - "**/group_vars/*.yaml", - "**/host_vars/*.yml", - "**/host_vars/*.yaml", - "**/playbooks/*.yml", - "**/playbooks/*.yaml", - "**playbook*.yml", - "**playbook*.yaml" - ] - } -``` - -Feel free to modify this list as per your needs. - -#### Inventory - -If your inventory file is in the YAML format, you can either: - -- Append the `ansible-lint` inventory json schema to it via the following comment at the top of your inventory file: - -```yml -# yaml-language-server: $schema=https://raw.githubusercontent.com/ansible/ansible-lint/main/src/ansiblelint/schemas/inventory.json -``` - -- Or configure the yaml language server settings to set this schema for all your inventory files, that match your inventory pattern, under your Zed settings ([ref](https://zed.dev/docs/languages/yaml)): - -```json [settings] -"lsp": { - "yaml-language-server": { - "settings": { - "yaml": { - "schemas": { - "https://raw.githubusercontent.com/ansible/ansible-lint/main/src/ansiblelint/schemas/inventory.json": [ - "./inventory/*.yaml", - "hosts.yml", - ] - } - } - } - } -}, -``` - -### LSP Configuration - -By default, the following default config is passed to the Ansible language server. It conveniently mirrors the defaults set by [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig/blob/03bc581e05e81d33808b42b2d7e76d70adb3b595/lua/lspconfig/configs/ansiblels.lua) for the Ansible language server: - -```json [settings] -{ - "ansible": { - "ansible": { - "path": "ansible" - }, - "executionEnvironment": { - "enabled": false - }, - "python": { - "interpreterPath": "python3" - }, - "validation": { - "enabled": true, - "lint": { - "enabled": true, - "path": "ansible-lint" - } - } - } -} -``` - -> [!NOTE] -> In order for linting to work, ensure that `ansible-lint` is installed and discoverable on your PATH - -When desired, any of the above default settings can be overridden under the `"lsp"` section of your Zed settings file. For example: - -```json [settings] -"lsp": { - // Note, the Zed Ansible extension prefixes all settings with `ansible` - // so instead of using `ansible.ansible.path` use `ansible.path`. - "ansible-language-server": { - "settings": { - "ansible": { - "path": "ansible" - }, - "executionEnvironment": { - "enabled": false - }, - "python": { - "interpreterPath": "python3" - }, - "validation": { - "enabled": false, // disable validation - "lint": { - "enabled": false, // disable ansible-lint - "path": "ansible-lint" - } - } - } - } -} -``` - -A full list of options/settings, that can be passed to the server, can be found at the project's page [here](https://github.com/ansible/vscode-ansible/blob/5a89836d66d470fb9d20e7ea8aa2af96f12f61fb/docs/als/settings.md). -Feel free to modify option values as needed. diff --git a/docs/src/languages/asciidoc.md b/docs/src/languages/asciidoc.md deleted file mode 100644 index 7f5ead7e07..0000000000 --- a/docs/src/languages/asciidoc.md +++ /dev/null @@ -1,6 +0,0 @@ -# AsciiDoc - -AsciiDoc language support in Zed is provided by the community-maintained [AsciiDoc extension](https://github.com/andreicek/zed-asciidoc). -Report issues to: [https://github.com/andreicek/zed-asciidoc/issues](https://github.com/andreicek/zed-asciidoc/issues) - -- Tree-sitter: [cathaysia/tree-sitter-asciidoc](https://github.com/cathaysia/tree-sitter-asciidoc) diff --git a/docs/src/languages/astro.md b/docs/src/languages/astro.md deleted file mode 100644 index cbfe8de74e..0000000000 --- a/docs/src/languages/astro.md +++ /dev/null @@ -1,10 +0,0 @@ -# Astro - -Astro support is available through the [Astro extension](https://github.com/zed-extensions/astro). - -- Tree-sitter: [virchau13/tree-sitter-astro](https://github.com/virchau13/tree-sitter-astro) -- Language Server: [withastro/language-tools](https://github.com/withastro/astro/tree/main/packages/language-tools/language-server) - - diff --git a/docs/src/languages/bash.md b/docs/src/languages/bash.md deleted file mode 100644 index 882a816216..0000000000 --- a/docs/src/languages/bash.md +++ /dev/null @@ -1,36 +0,0 @@ -# Bash - -Bash language support in Zed is provided by the community-maintained [Basher extension](https://github.com/d1y/bash.zed). -Report issues to: [https://github.com/d1y/bash.zed/issues](https://github.com/d1y/bash.zed/issues) - -- Tree-sitter: [tree-sitter/tree-sitter-bash](https://github.com/tree-sitter/tree-sitter-bash) -- Language Server: [bash-lsp/bash-language-server](https://github.com/bash-lsp/bash-language-server) - -## Configuration - -When `shellcheck` is available `bash-language-server` will use it internally to provide diagnostics. - -### Install `shellcheck`: - -```sh -brew install shellcheck # macOS (HomeBrew) -apt-get install shellcheck # Ubuntu/Debian -pacman -S shellcheck # ArchLinux -dnf install shellcheck # Fedora -yum install shellcheck # CentOS/RHEL -zypper install shellcheck # openSUSE -choco install shellcheck # Windows (Chocolatey) -``` - -And verify it is available from your path: - -```sh -which shellcheck -shellcheck --version -``` - -If you wish to customize the warnings/errors reported you just need to create a `.shellcheckrc` file. You can do this in the root of your project or in your home directory (`~/.shellcheckrc`). See: [shellcheck documentation](https://github.com/koalaman/shellcheck/wiki/Ignore#ignoring-one-or-more-types-of-errors-forever) for more. - -### See also: - -- [Zed Docs: Language Support: Shell Scripts](./sh.md) diff --git a/docs/src/languages/biome.md b/docs/src/languages/biome.md deleted file mode 100644 index f0756fe5ba..0000000000 --- a/docs/src/languages/biome.md +++ /dev/null @@ -1,35 +0,0 @@ -# Biome - -[Biome](https://biomejs.dev/) support in Zed is provided by the community-maintained [Biome extension](https://github.com/biomejs/biome-zed). -Report issues to: [https://github.com/biomejs/biome-zed/issues](https://github.com/biomejs/biome-zed/issues) - -- Language Server: [biomejs/biome](https://github.com/biomejs/biome) - -## Biome Language Support - -The Biome extension includes support for the following languages: - -- JavaScript -- TypeScript -- JSX -- TSX -- JSON -- JSONC -- Vue.js -- Astro -- Svelte -- CSS - -## Configuration - -By default, the `biome.json` file is required to be in the root of the workspace. - -```json [settings] -{ - "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json" -} -``` - -For a full list of `biome.json` options see [Biome Configuration](https://biomejs.dev/reference/configuration/) documentation. - -See the [Biome Zed Extension README](https://github.com/biomejs/biome-zed) for a complete list of features and configuration options. diff --git a/docs/src/languages/c.md b/docs/src/languages/c.md deleted file mode 100644 index 2259ad21a4..0000000000 --- a/docs/src/languages/c.md +++ /dev/null @@ -1,91 +0,0 @@ -# C - -C support is available natively in Zed. - -- Tree-sitter: [tree-sitter/tree-sitter-c](https://github.com/tree-sitter/tree-sitter-c) -- Language Server: [clangd/clangd](https://github.com/clangd/clangd) -- Debug Adapter: [CodeLLDB](https://github.com/vadimcn) (primary), [GDB](https://sourceware.org/gdb/) (secondary, not available on Apple silicon) - -## Clangd: Force detect as C - -Clangd out of the box assumes mixed C++/C projects. If you have a C-only project you may wish to instruct clangd to treat all files as C using the `-xc` flag. To do this, create a `.clangd` file in the root of your project with the following: - -```yaml -# yaml-language-server: $schema=https://json.schemastore.org/clangd.json -CompileFlags: - Add: [-xc] -``` - -By default clang and gcc will recognize `*.C` and `*.H` (uppercase extensions) as C++ and not C and so Zed too follows this convention. If you are working with a C-only project (perhaps one with legacy uppercase pathing like `FILENAME.C`) you can override this behavior by adding this to your settings: - -```json [settings] -{ - "file_types": { - "C": ["C", "H"] - } -} -``` - -## Formatting - -By default Zed will use the `clangd` language server for formatting C code like the `clang-format` CLI tool. To configure this you can add a `.clang-format` file. For example: - -```yaml -# yaml-language-server: $schema=https://json.schemastore.org/clang-format-21.x.json ---- -BasedOnStyle: GNU -IndentWidth: 2 ---- -``` - -See [Clang-Format Style Options](https://clang.llvm.org/docs/ClangFormatStyleOptions.html) for a complete list of options. - -You can trigger formatting via {#kb editor::Format} or the `editor: format` action from the command palette or by adding `format_on_save` to your Zed settings: - -```json [settings] - "languages": { - "C": { - "format_on_save": "on", - "tab_size": 2 - } - } -``` - -## Compile Commands - -For some projects Clangd requires a `compile_commands.json` file to properly analyze your project. This file contains the compilation database that tells clangd how your project should be built. - -### CMake Compile Commands - -With CMake, you can generate `compile_commands.json` automatically by adding the following line to your `CMakeLists.txt`: - -```cmake -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -``` - -After building your project, CMake will generate the `compile_commands.json` file in the build directory and clangd will automatically pick it up. - -## Debugging - -You can use CodeLLDB or GDB to debug native binaries. (Make sure that your build process passes `-g` to the C compiler, so that debug information is included in the resulting binary.) See below for examples of debug configurations that you can add to `.zed/debug.json`. - -- [CodeLLDB configuration documentation](https://github.com/vadimcn/codelldb/blob/master/MANUAL.md#starting-a-new-debug-session) -- [GDB configuration documentation](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Debugger-Adapter-Protocol.html) - -### Build and Debug Binary - -```json [debug] -[ - { - "label": "Debug native binary", - "build": { - "command": "make", - "args": ["-j8"], - "cwd": "$ZED_WORKTREE_ROOT" - }, - "program": "$ZED_WORKTREE_ROOT/build/prog", - "request": "launch", - "adapter": "CodeLLDB" - } -] -``` diff --git a/docs/src/languages/clojure.md b/docs/src/languages/clojure.md deleted file mode 100644 index a590cb6b55..0000000000 --- a/docs/src/languages/clojure.md +++ /dev/null @@ -1,10 +0,0 @@ -# Clojure - -Clojure support is available through the [Clojure extension](https://github.com/zed-extensions/clojure). - -- Tree-sitter: [prcastro/tree-sitter-clojure](https://github.com/prcastro/tree-sitter-clojure) -- Language Server: [clojure-lsp/clojure-lsp](https://github.com/clojure-lsp/clojure-lsp) - - diff --git a/docs/src/languages/cpp.md b/docs/src/languages/cpp.md deleted file mode 100644 index c20dd58335..0000000000 --- a/docs/src/languages/cpp.md +++ /dev/null @@ -1,160 +0,0 @@ -# C++ - -C++ support is available natively in Zed. - -- Tree-sitter: [tree-sitter/tree-sitter-cpp](https://github.com/tree-sitter/tree-sitter-cpp) -- Language Server: [clangd/clangd](https://github.com/clangd/clangd) - -## Binary - -You can configure which `clangd` binary Zed should use. - -By default, Zed will try to find a `clangd` in your `$PATH` and try to use that. If that binary successfully executes, it's used. Otherwise, Zed will fall back to installing its own `clangd` version and use that. - -If you want to install a pre-release `clangd` version instead you can instruct Zed to do so by setting `pre_release` to `true` in your `settings.json`: - -```json [settings] -{ - "lsp": { - "clangd": { - "fetch": { - "pre_release": true - } - } - } -} -``` - -If you want to disable Zed looking for a `clangd` binary, you can set `ignore_system_version` to `true` in your `settings.json`: - -```json [settings] -{ - "lsp": { - "clangd": { - "binary": { - "ignore_system_version": true - } - } - } -} -``` - -If you want to use a binary in a custom location, you can specify a `path` and optional `arguments`: - -```json [settings] -{ - "lsp": { - "clangd": { - "binary": { - "path": "/path/to/clangd", - "arguments": [] - } - } - } -} -``` - -This `"path"` has to be an absolute path. - -## Arguments - -You can pass any number of arguments to clangd. To see a full set of available options, run `clangd --help` from the command line. For example with `--function-arg-placeholders=0` completions contain only parentheses for function calls, while the default (`--function-arg-placeholders=1`) completions also contain placeholders for method parameters. - -```json [settings] -{ - "lsp": { - "clangd": { - "binary": { - "path": "/path/to/clangd", - "arguments": ["--function-arg-placeholders=0"] - } - } - } -} -``` - -## Formatting - -By default Zed will use the `clangd` language server for formatting C++ code. The Clangd is the same as the `clang-format` CLI tool. To configure this you can add a `.clang-format` file. For example: - -```yaml -# yaml-language-server: $schema=https://json.schemastore.org/clang-format-21.x.json ---- -BasedOnStyle: LLVM -IndentWidth: 4 ---- -Language: Cpp -# Force pointers to the type for C++. -DerivePointerAlignment: false -PointerAlignment: Left ---- -``` - -See [Clang-Format Style Options](https://clang.llvm.org/docs/ClangFormatStyleOptions.html) for a complete list of options. - -You can trigger formatting via {#kb editor::Format} or the `editor: format` action from the command palette or by adding `format_on_save` to your Zed settings: - -```json [settings] - "languages": { - "C++": { - "format_on_save": "on", - "tab_size": 2 - } - } -``` - -## More server configuration - -In the root of your project, it is generally common to create a `.clangd` file to set extra configuration. - -```yaml -# yaml-language-server: $schema=https://json.schemastore.org/clangd.json -CompileFlags: - Add: - - "--include-directory=/path/to/include" -Diagnostics: - MissingIncludes: Strict - UnusedIncludes: Strict -``` - -For more advanced usage of clangd configuration file, take a look into their [official page](https://clangd.llvm.org/config.html). - -## Compile Commands - -For some projects Clangd requires a `compile_commands.json` file to properly analyze your project. This file contains the compilation database that tells clangd how your project should be built. - -### CMake Compile Commands - -With CMake, you can generate `compile_commands.json` automatically by adding the following line to your `CMakeLists.txt`: - -```cmake -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -``` - -After building your project, CMake will generate the `compile_commands.json` file in the build directory and clangd will automatically pick it up. - -## Debugging - -You can use CodeLLDB or GDB to debug native binaries. (Make sure that your build process passes `-g` to the C++ compiler, so that debug information is included in the resulting binary.) See below for examples of debug configurations that you can add to `.zed/debug.json`. - -- [CodeLLDB configuration documentation](https://github.com/vadimcn/codelldb/blob/master/MANUAL.md#starting-a-new-debug-session) -- [GDB configuration documentation](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Debugger-Adapter-Protocol.html) - - GDB needs to be at least v14.1 - -### Build and Debug Binary - -```json [debug] -[ - { - "label": "Debug native binary", - "build": { - "command": "make", - "args": ["-j8"], - "cwd": "$ZED_WORKTREE_ROOT" - }, - "program": "$ZED_WORKTREE_ROOT/build/prog", - "request": "launch", - "adapter": "CodeLLDB" - } -] -``` diff --git a/docs/src/languages/csharp.md b/docs/src/languages/csharp.md deleted file mode 100644 index e7a702c190..0000000000 --- a/docs/src/languages/csharp.md +++ /dev/null @@ -1,25 +0,0 @@ -# C# - -Note language name is "CSharp" for settings not "C#' - -C# support is available through the [C# extension](https://github.com/zed-extensions/csharp). - -- Tree-sitter: [tree-sitter/tree-sitter-c-sharp](https://github.com/tree-sitter/tree-sitter-c-sharp) -- Language Server: [OmniSharp/omnisharp-roslyn](https://github.com/OmniSharp/omnisharp-roslyn) - -## Configuration - -The `OmniSharp` binary can be configured in a Zed settings file with: - -```json [settings] -{ - "lsp": { - "omnisharp": { - "binary": { - "path": "/path/to/OmniSharp", - "arguments": ["optional", "additional", "args", "-lsp"] - } - } - } -} -``` diff --git a/docs/src/languages/css.md b/docs/src/languages/css.md deleted file mode 100644 index 59fd578aee..0000000000 --- a/docs/src/languages/css.md +++ /dev/null @@ -1,20 +0,0 @@ -# CSS - -Zed has built-in support for CSS. - -- Tree-sitter: [tree-sitter/tree-sitter-css](https://github.com/tree-sitter/tree-sitter-css) -- Language Servers: - - [microsoft/vscode-css-languageservice](https://github.com/microsoft/vscode-css-languageservice) - - [tailwindcss-language-server](https://github.com/tailwindlabs/tailwindcss-intellisense) - -## Tailwind CSS - -Zed also supports [Tailwind CSS](./tailwindcss.md) out-of-the-box for languages and frameworks like JavaScript, Astro, Svelte, and more. - - - -## Recommended Reading - -- [HTML](./html.md) -- [TypeScript](./typescript.md) -- [JavaScript](./javascript.md) diff --git a/docs/src/languages/dart.md b/docs/src/languages/dart.md deleted file mode 100644 index 20f8a1d230..0000000000 --- a/docs/src/languages/dart.md +++ /dev/null @@ -1,54 +0,0 @@ -# Dart - -Dart support is available through the [Dart extension](https://github.com/zed-extensions/dart). - -- Tree-sitter: [UserNobody14/tree-sitter-dart](https://github.com/UserNobody14/tree-sitter-dart) -- Language Server: [dart language-server](https://github.com/dart-lang/sdk) - -## Pre-requisites - -You will need to install the Dart SDK. - -You can install dart from [dart.dev/get-dart](https://dart.dev/get-dart) or via the [Flutter Version Management CLI (fvm)](https://fvm.app/documentation/getting-started/installation) - -## Configuration - -The dart extension requires no configuration if you have `dart` in your path: - -```sh -which dart -dart --version -``` - -If you would like to use a specific dart binary or use dart via FVM you can specify the `dart` binary in your Zed settings.jsons file: - -```json [settings] -{ - "lsp": { - "dart": { - "binary": { - "path": "/opt/homebrew/bin/fvm", - "arguments": ["dart", "language-server", "--protocol=lsp"] - } - } - } -} -``` - -### Formatting - -Dart by-default uses a very conservative maximum line length (80). If you would like the dart LSP to permit a longer line length when auto-formatting, add the following to your Zed settings.json: - -```json [settings] -{ - "lsp": { - "dart": { - "settings": { - "lineLength": 140 - } - } - } -} -``` - -Please see the Dart documentation for more information on [dart language-server capabilities](https://github.com/dart-lang/sdk/blob/main/pkg/analysis_server/tool/lsp_spec/README.md). diff --git a/docs/src/languages/deno.md b/docs/src/languages/deno.md deleted file mode 100644 index 0fa645291e..0000000000 --- a/docs/src/languages/deno.md +++ /dev/null @@ -1,127 +0,0 @@ -# Deno - -Deno support is available through the [Deno extension](https://github.com/zed-extensions/deno). - -- Language server: [Deno Language Server](https://docs.deno.com/runtime/manual/advanced/language_server/overview/) - -## Deno Configuration - -To use the Deno Language Server with TypeScript and TSX files, you will likely wish to disable the default language servers and enable deno by adding the following to your `settings.json`: - -```json [settings] -{ - "lsp": { - "deno": { - "settings": { - "deno": { - "enable": true - } - } - } - }, - "languages": { - "JavaScript": { - "language_servers": [ - "deno", - "!typescript-language-server", - "!vtsls", - "!eslint" - ], - "formatter": "language_server" - }, - "TypeScript": { - "language_servers": [ - "deno", - "!typescript-language-server", - "!vtsls", - "!eslint" - ], - "formatter": "language_server" - }, - "TSX": { - "language_servers": [ - "deno", - "!typescript-language-server", - "!vtsls", - "!eslint" - ], - "formatter": "language_server" - } - } -} -``` - -See [Configuring supported languages](../configuring-languages.md) in the Zed documentation for more information. - - - -## Configuration completion - -To get completions for `deno.json` or `package.json` you can add the following to your `settings.json`: (More info here https://zed.dev/docs/languages/json) - -```json [settings] -"lsp": { - "json-language-server": { - "settings": { - "json": { - "schemas": [ - { - "fileMatch": [ - "deno.json", - "deno.jsonc" - ], - "url": "https://raw.githubusercontent.com/denoland/deno/refs/heads/main/cli/schemas/config-file.v1.json" - }, - { - "fileMatch": [ - "package.json" - ], - "url": "https://www.schemastore.org/package" - } - ] - } - } - } - } -``` - -## DAP support - -To debug deno programs, add this to `.zed/debug.json` - -```json [debug] -[ - { - "adapter": "JavaScript", - "label": "Deno", - "request": "launch", - "type": "pwa-node", - "cwd": "$ZED_WORKTREE_ROOT", - "program": "$ZED_FILE", - "runtimeExecutable": "deno", - "runtimeArgs": ["run", "--allow-all", "--inspect-wait"], - "attachSimplePort": 9229 - } -] -``` - -## Runnable support - -To run deno tasks like tests from the ui, add this to `.zed/tasks.json` - -```json [tasks] -[ - { - "label": "deno test", - "command": "deno test -A --filter '/^$ZED_CUSTOM_DENO_TEST_NAME$/' '$ZED_FILE'", - "tags": ["js-test"] - } -] -``` - -## See also: - -- [TypeScript](./typescript.md) -- [JavaScript](./javascript.md) diff --git a/docs/src/languages/diff.md b/docs/src/languages/diff.md deleted file mode 100644 index a089b975e7..0000000000 --- a/docs/src/languages/diff.md +++ /dev/null @@ -1,17 +0,0 @@ -# Diff - -Diff support is available natively in Zed. - -- Tree-sitter: [zed-industries/the-mikedavis/tree-sitter-diff](https://github.com/the-mikedavis/tree-sitter-diff) - -## Configuration - -Zed will not attempt to format diff files and has [`remove_trailing_whitespace_on_save`](https://zed.dev/docs/configuring-zed#remove-trailing-whitespace-on-save) and [`ensure-final-newline-on-save`](https://zed.dev/docs/configuring-zed#ensure-final-newline-on-save) set to false. - -Zed will automatically recognize files with `patch` and `diff` extensions as Diff files. To recognize other extensions, add them to `file_types` in your Zed settings.json: - -```json [settings] - "file_types": { - "Diff": ["dif"] - }, -``` diff --git a/docs/src/languages/docker.md b/docs/src/languages/docker.md deleted file mode 100644 index f003a575d2..0000000000 --- a/docs/src/languages/docker.md +++ /dev/null @@ -1,16 +0,0 @@ -# Docker - -Support for `Dockerfile` and `docker-compose.yaml` in Zed is provided by community-maintained extensions. - -## Docker Compose - -Docker `compose.yaml` language support in Zed is provided by the [Docker Compose extension](https://github.com/eth0net/zed-docker-compose). Please report issues to: [https://github.com/eth0net/zed-docker-compose/issues](https://github.com/eth0net/zed-docker-compose/issues). - -- Language Server: [microsoft/compose-language-service](https://github.com/microsoft/compose-language-service) - -## Dockerfile - -`Dockerfile` language support in Zed is provided by the [Dockerfile extension](https://github.com/d1y/dockerfile.zed). Please report issues to: [https://github.com/d1y/dockerfile.zed/issues](https://github.com/d1y/dockerfile.zed/issues). - -- Tree-sitter: [camdencheek/tree-sitter-dockerfile](https://github.com/camdencheek/tree-sitter-dockerfile) -- Language Server: [rcjsuen/dockerfile-language-server](https://github.com/rcjsuen/dockerfile-language-server) diff --git a/docs/src/languages/elixir.md b/docs/src/languages/elixir.md deleted file mode 100644 index 3df116492a..0000000000 --- a/docs/src/languages/elixir.md +++ /dev/null @@ -1,125 +0,0 @@ -# Elixir - -Elixir support is available through the [Elixir extension](https://github.com/zed-extensions/elixir). - -- Tree-sitter: - - [elixir-lang/tree-sitter-elixir](https://github.com/elixir-lang/tree-sitter-elixir) - - [phoenixframework/tree-sitter-heex](https://github.com/phoenixframework/tree-sitter-heex) -- Language servers: - - [elixir-lang/expert](https://github.com/elixir-lang/expert) - - [elixir-lsp/elixir-ls](https://github.com/elixir-lsp/elixir-ls) - - [elixir-tools/next-ls](https://github.com/elixir-tools/next-ls) - - [lexical-lsp/lexical](https://github.com/lexical-lsp/lexical) - -## Choosing a language server - -The Elixir extension offers language server support for `expert`, `elixir-ls`, `next-ls`, and `lexical`. - -`elixir-ls` is enabled by default. - -### Expert - -To switch to `expert`, add the following to your `settings.json`: - -```json [settings] - "languages": { - "Elixir": { - "language_servers": ["expert", "!elixir-ls", "!next-ls", "!lexical", "..."] - }, - "HEEX": { - "language_servers": ["expert", "!elixir-ls", "!next-ls", "!lexical", "..."] - } - } -``` - -### Next LS - -To switch to `next-ls`, add the following to your `settings.json`: - -```json [settings] - "languages": { - "Elixir": { - "language_servers": ["next-ls", "!expert", "!elixir-ls", "!lexical", "..."] - }, - "HEEX": { - "language_servers": ["next-ls", "!expert", "!elixir-ls", "!lexical", "..."] - } - } -``` - -### Lexical - -To switch to `lexical`, add the following to your `settings.json`: - -```json [settings] - "languages": { - "Elixir": { - "language_servers": ["lexical", "!expert", "!elixir-ls", "!next-ls", "..."] - }, - "HEEX": { - "language_servers": ["lexical", "!expert", "!elixir-ls", "!next-ls", "..."] - } - } -``` - -## Setting up `elixir-ls` - -1. Install `elixir`: - -```sh -brew install elixir -``` - -2. Install `elixir-ls`: - -```sh -brew install elixir-ls -``` - -3. Restart Zed - -> If `elixir-ls` is not running in an elixir project, check the error log via the command palette action `zed: open log`. If you find an error message mentioning: `invalid LSP message header "Shall I install Hex? (if running non-interactively, use \"mix local.hex --force\") [Yn]`, you might need to install [`Hex`](https://hex.pm). You run `elixir-ls` from the command line and accept the prompt to install `Hex`. - -### Formatting with Mix - -If you prefer to format your code with [Mix](https://hexdocs.pm/mix/Mix.html), use the following snippet in your `settings.json` file to configure it as an external formatter. Formatting will occur on file save. - -```json [settings] -{ - "languages": { - "Elixir": { - "format_on_save": "on", - "formatter": { - "external": { - "command": "mix", - "arguments": ["format", "--stdin-filename", "{buffer_path}", "-"] - } - } - } - } -} -``` - -### Additional workspace configuration options - -You can pass additional elixir-ls workspace configuration options via lsp settings in `settings.json`. - -The following example disables dialyzer: - -```json [settings] - "lsp": { - "elixir-ls": { - "settings": { - "dialyzerEnabled": false - } - } - } -``` - -See [ElixirLS configuration settings](https://github.com/elixir-lsp/elixir-ls#elixirls-configuration-settings) for more options. - -### HEEx - -Zed also supports HEEx templates. HEEx is a mix of [EEx](https://hexdocs.pm/eex/1.12.3/EEx.html) (Embedded Elixir) and HTML, and is used in Phoenix LiveView applications. - -- Tree-sitter: [phoenixframework/tree-sitter-heex](https://github.com/phoenixframework/tree-sitter-heex) diff --git a/docs/src/languages/elm.md b/docs/src/languages/elm.md deleted file mode 100644 index 3a18af05bb..0000000000 --- a/docs/src/languages/elm.md +++ /dev/null @@ -1,40 +0,0 @@ -# Elm - -Elm support is available through the [Elm extension](https://github.com/zed-extensions/elm). - -- Tree-sitter: [elm-tooling/tree-sitter-elm](https://github.com/elm-tooling/tree-sitter-elm) -- Language Server: [elm-tooling/elm-language-server](https://github.com/elm-tooling/elm-language-server) - -## Setup - -Zed support for Elm requires installation of `elm`, `elm-format`, and `elm-review`. - -1. [Install Elm](https://guide.elm-lang.org/install/elm.html) (or run `brew install elm` on macOS). -2. Install `elm-review` to support code linting: - ```sh - npm install elm-review --save-dev - ``` -3. Install `elm-format` to support automatic formatting - ```sh - npm install -g elm-format - ``` - -## Configuring `elm-language-server` - -Elm language server can be configured in your `settings.json`, e.g.: - -```json [settings] -{ - "lsp": { - "elm-language-server": { - "initialization_options": { - "disableElmLSDiagnostics": true, - "onlyUpdateDiagnosticsOnSave": false, - "elmReviewDiagnostics": "warning" - } - } - } -} -``` - -`elm-format`, `elm-review` and `elm` need to be installed and made available in the environment or configured in the settings. See the [full list of server settings here](https://github.com/elm-tooling/elm-language-server?tab=readme-ov-file#server-settings). diff --git a/docs/src/languages/emmet.md b/docs/src/languages/emmet.md deleted file mode 100644 index 73e34c209f..0000000000 --- a/docs/src/languages/emmet.md +++ /dev/null @@ -1,11 +0,0 @@ -# Emmet - -Emmet support is available through the [Emmet extension](https://github.com/zed-extensions/emmet). - -[Emmet](https://emmet.io/) is a web-developer’s toolkit that can greatly improve your HTML & CSS workflow. - -- Language Server: [olrtg/emmet-language-server](https://github.com/olrtg/emmet-language-server) - - diff --git a/docs/src/languages/erlang.md b/docs/src/languages/erlang.md deleted file mode 100644 index b3850fc55e..0000000000 --- a/docs/src/languages/erlang.md +++ /dev/null @@ -1,31 +0,0 @@ -# Erlang - -Erlang support is available through the [Erlang extension](https://github.com/zed-extensions/erlang). - -- Tree-sitter: [WhatsApp/tree-sitter-erlang](https://github.com/WhatsApp/tree-sitter-erlang) -- Language Servers: - - [erlang-ls/erlang_ls](https://github.com/erlang-ls/erlang_ls) - - [WhatsApp/erlang-language-platform](https://github.com/WhatsApp/erlang-language-platform) - -## Choosing a language server - -The Erlang extension offers language server support for `erlang_ls` and `erlang-language-platform`. - -`erlang_ls` is enabled by default. - -To switch to `erlang-language-platform`, add the following to your `settings.json`: - -```json [settings] -{ - "languages": { - "Erlang": { - "language_servers": ["elp", "!erlang-ls", "..."] - } - } -} -``` - -## See also: - -- [Elixir](./elixir.md) -- [Gleam](./gleam.md) diff --git a/docs/src/languages/fish.md b/docs/src/languages/fish.md deleted file mode 100644 index 6c07d444b8..0000000000 --- a/docs/src/languages/fish.md +++ /dev/null @@ -1,31 +0,0 @@ -# Fish - -Fish language support in Zed is provided by the community-maintained [Fish extension](https://github.com/hasit/zed-fish). -Report issues to: [https://github.com/hasit/zed-fish/issues](https://github.com/hasit/zed-fish/issues) - -- Tree-sitter: [ram02z/tree-sitter-fish](https://github.com/ram02z/tree-sitter-fish) - -### Formatting - -Zed supports auto-formatting fish code using external tools like [`fish_indent`](https://fishshell.com/docs/current/cmds/fish_indent.html), which is included with fish. - -1. Ensure `fish_indent` is available in your path and check the version: - -```sh -which fish_indent -fish_indent --version -``` - -2. Configure Zed to automatically format fish code with `fish_indent`: - -```json [settings] - "languages": { - "Fish": { - "formatter": { - "external": { - "command": "fish_indent" - } - } - } - }, -``` diff --git a/docs/src/languages/gdscript.md b/docs/src/languages/gdscript.md deleted file mode 100644 index 53a3dc305d..0000000000 --- a/docs/src/languages/gdscript.md +++ /dev/null @@ -1,26 +0,0 @@ -# GDScript - -Godot [GDScript](https://gdscript.com/) language support in Zed is provided by the community-maintained [GDScript extension](https://github.com/GDQuest/zed-gdscript). -Report issues to: [https://github.com/GDQuest/zed-gdscript/issues](https://github.com/GDQuest/zed-gdscript/issues) - -- Tree-sitter: [PrestonKnopp/tree-sitter-gdscript](https://github.com/PrestonKnopp/tree-sitter-gdscript) and [PrestonKnopp/tree-sitter-godot-resource](https://github.com/PrestonKnopp/tree-sitter-godot-resource) -- Language Server: [gdscript-language-server](https://github.com/godotengine/godot) - -## Pre-requisites - -You will need: - -- [Godot](https://godotengine.org/download/). -- netcat (`nc` or `ncat`) on your system PATH. - -## Setup - -1. Inside your Godot editor, open Editor Settings, look for `Text Editor -> External` and set the following options: - - Exec Path: `/path/to/zed` - - Exec Flags: `{project} {file}:{line}:{col}` - - Use External Editor: "✅ On" -2. Open any \*.gd file through Godot and Zed will launch. - -## Usage - -When Godot is running, the GDScript extension will connect to the language server provided by the Godot runtime and will provide `jump to definition`, hover states when you hold Ctrl/cmd and other language server features. diff --git a/docs/src/languages/gleam.md b/docs/src/languages/gleam.md deleted file mode 100644 index bc7eb010b9..0000000000 --- a/docs/src/languages/gleam.md +++ /dev/null @@ -1,11 +0,0 @@ -# Gleam - -Gleam support is available through the [Gleam extension](https://github.com/gleam-lang/zed-gleam). To learn about Gleam, see the [docs](https://gleam.run/documentation/) or check out the [`stdlib` reference](https://hexdocs.pm/gleam_stdlib/). The Gleam language server has a variety of features, including go-to definition, automatic imports, and [more](https://gleam.run/language-server/). - -- Tree-sitter: [gleam-lang/tree-sitter-gleam](https://github.com/gleam-lang/tree-sitter-gleam) -- Language Server: [gleam lsp](https://github.com/gleam-lang/gleam/tree/main/compiler-core/src/language_server) - -See also: - -- [Elixir](./elixir.md) -- [Erlang](./erlang.md) diff --git a/docs/src/languages/glsl.md b/docs/src/languages/glsl.md deleted file mode 100644 index b257468752..0000000000 --- a/docs/src/languages/glsl.md +++ /dev/null @@ -1,6 +0,0 @@ -# GLSL - -GLSL (OpenGL Shading Language) support is available through the [GLSL Extension](https://github.com/zed-industries/zed/tree/main/extensions/glsl/) - -- Tree-sitter: [theHamsta/tree-sitter-glsl](https://github.com/theHamsta/tree-sitter-glsl) -- Language Server: [nolanderc/glsl_analyzer](https://github.com/nolanderc/glsl_analyzer) diff --git a/docs/src/languages/go.md b/docs/src/languages/go.md deleted file mode 100644 index 3c4e505f8f..0000000000 --- a/docs/src/languages/go.md +++ /dev/null @@ -1,198 +0,0 @@ -# Go - -Go support is available natively in Zed. - -- Tree-sitter: [tree-sitter/tree-sitter-go](https://github.com/tree-sitter/tree-sitter-go) -- Language Server: [golang/tools/tree/master/gopls](https://github.com/golang/tools/tree/master/gopls) -- Debug Adapter: [delve](https://github.com/go-delve/delve) - -## Setup - -We recommend installing gopls via go's package manager and not via Homebrew or your Linux distribution's package manager. - -1. Make sure you have uninstalled any version of gopls you have installed via your package manager: - -```sh -# MacOS homebrew -brew remove gopls -# Ubuntu -sudo apt-get remove gopls -sudo snap remove gopls -# Arch -sudo pacman -R gopls -``` - -2. Install/Update `gopls` to the latest version using the go module tool: - -```sh -go install golang.org/x/tools/gopls@latest -``` - -3. Ensure that `gopls` is in your path: - -```sh -which gopls -gopls version -``` - -If `gopls` is not found you will likely need to add `export PATH="$PATH:$HOME/go/bin"` to your `.zshrc` / `.bash_profile` - -## Inlay Hints - -Zed sets the following initialization options for inlay hints: - -```json [settings] -"hints": { - "assignVariableTypes": true, - "compositeLiteralFields": true, - "compositeLiteralTypes": true, - "constantValues": true, - "functionTypeParameters": true, - "parameterNames": true, - "rangeVariableTypes": true -} -``` - -to make the language server send back inlay hints when Zed has them enabled in the settings. - -Use - -```json [settings] -"lsp": { - "gopls": { - "initialization_options": { - "hints": { - // .... - } - } - } -} -``` - -to override these settings. - -See [gopls inlayHints documentation](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md) for more information. - -## Debugging - -Zed supports zero-configuration debugging of Go tests and entry points (`func main`) using Delve. Run {#action debugger::Start} ({#kb debugger::Start}) to see a contextual list of these preconfigured debug tasks. - -For more control, you can add debug configurations to `.zed/debug.json`. See below for examples. - -- [Delve configuration documentation](https://github.com/go-delve/delve/blob/master/Documentation/api/dap/README.md#launch-and-attach-configurations) - -### Debug Go Packages - -To debug a specific package, you can do so by setting the Delve mode to "debug". In this case "program" should be set to the package name. - -```json [debug] -[ - { - "label": "Go (Delve)", - "adapter": "Delve", - "program": "$ZED_FILE", - "request": "launch", - "mode": "debug" - }, - { - "label": "Run server", - "adapter": "Delve", - "request": "launch", - "mode": "debug", - // For Delve, the program can be a package name - "program": "./cmd/server" - // "args": [], - // "buildFlags": [], - } -] -``` - -### Debug Go Tests - -To debug the tests for a package, set the Delve mode to "test". -The "program" is still the package name, and you can use the "buildFlags" to do things like set tags, and the "args" to set args on the test binary. (See `go help testflags` for more information on doing that). - -```json [debug] -[ - { - "label": "Run integration tests", - "adapter": "Delve", - "request": "launch", - "mode": "test", - "program": ".", - "buildFlags": ["-tags", "integration"] - // To filter down to just the test your cursor is in: - // "args": ["-test.run", "$ZED_SYMBOL"] - } -] -``` - -### Build and debug separately - -If you need to build your application with a specific command, you can use the "exec" mode of Delve. In this case "program" should point to an executable, -and the "build" command should build that. - -```json [debug] -[ - { - "label": "Debug Prebuilt Unit Tests", - "adapter": "Delve", - "request": "launch", - "mode": "exec", - "program": "${ZED_WORKTREE_ROOT}/__debug_unit", - "args": ["-test.v", "-test.run=${ZED_SYMBOL}"], - "build": { - "command": "go", - "args": [ - "test", - "-c", - "-tags", - "unit", - "-gcflags\"all=-N -l\"", - "-o", - "__debug_unit", - "./pkg/..." - ] - } - } -] -``` - -### Attaching to an existing instance of Delve - -You might find yourself needing to connect to an existing instance of Delve that's not necessarily running on your machine; in such case, you can use `tcp_arguments` to instrument Zed's connection to Delve. - -```json [debug] -[ - { - "adapter": "Delve", - "label": "Connect to a running Delve instance", - "program": "/Users/zed/Projects/language_repositories/golang/hello/hello", - "cwd": "/Users/zed/Projects/language_repositories/golang/hello", - "args": [], - "env": {}, - "request": "launch", - "mode": "exec", - "stopOnEntry": false, - "tcp_connection": { "host": "127.0.0.1", "port": 53412 } - } -] -``` - -In such case Zed won't spawn a new instance of Delve, as it opts to use an existing one. The consequence of this is that _there will be no terminal_ in Zed; you have to interact with the Delve instance directly, as it handles stdin/stdout of the debuggee. - -## Go Mod - -- Tree-sitter: [camdencheek/tree-sitter-go-mod](https://github.com/camdencheek/tree-sitter-go-mod) -- Language Server: N/A - -## Go Sum - -- Tree-sitter: [amaanq/tree-sitter-go-sum](https://github.com/amaanq/tree-sitter-go-sum) -- Language Server: N/A - -## Go Work - -- Tree-sitter: - [tree-sitter-go-work](https://github.com/d1y/tree-sitter-go-work) -- Language Server: N/A diff --git a/docs/src/languages/groovy.md b/docs/src/languages/groovy.md deleted file mode 100644 index cbf54a5455..0000000000 --- a/docs/src/languages/groovy.md +++ /dev/null @@ -1,7 +0,0 @@ -# Groovy - -Groovy language support in Zed is provided by the community-maintained [Groovy extension](https://github.com/valentinegb/zed-groovy). -Report issues to: [https://github.com/valentinegb/zed-groovy/issues](https://github.com/valentinegb/zed-groovy/issues) - -- Tree-sitter: [murtaza64/tree-sitter-groovy](https://github.com/murtaza64/tree-sitter-groovy) -- Language Server: [GroovyLanguageServer/groovy-language-server](https://github.com/GroovyLanguageServer/groovy-language-server) diff --git a/docs/src/languages/haskell.md b/docs/src/languages/haskell.md deleted file mode 100644 index 901bd9ded1..0000000000 --- a/docs/src/languages/haskell.md +++ /dev/null @@ -1,51 +0,0 @@ -# Haskell - -Haskell support is available through the [Haskell extension](https://github.com/zed-extensions/haskell). - -- Tree-sitter: [tree-sitter-haskell](https://github.com/tree-sitter/tree-sitter-haskell) -- Language Server: [haskell-language-server](https://github.com/haskell/haskell-language-server) - -## Installing HLS - -Recommended method to [install haskell-language-server](https://haskell-language-server.readthedocs.io/en/latest/installation.html) (HLS) is via [ghcup](https://www.haskell.org/ghcup/install/) (`curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh -`): - -```sh -ghcup install hls -which haskell-language-server-wrapper -``` - -## Configuring HLS - -If you need to configure haskell-language-server (hls) you can add configuration options to your Zed settings.json: - -```json [settings] -{ - "lsp": { - "hls": { - "initialization_options": { - "haskell": { - "formattingProvider": "fourmolu" - } - } - } - } -} -``` - -See the official [configuring haskell-language-server](https://haskell-language-server.readthedocs.io/en/latest/configuration.html) docs for more options. - -If you would like to use a specific hls binary, or perhaps use [static-ls](https://github.com/josephsumabat/static-ls) as a drop-in replacement instead, you can specify the binary path and arguments: - -```json [settings] -{ - "lsp": { - "hls": { - "binary": { - "path": "static-ls", - "arguments": ["--experimentalFeatures"] - } - } - } -} -``` diff --git a/docs/src/languages/helm.md b/docs/src/languages/helm.md deleted file mode 100644 index f8a6f5c5fa..0000000000 --- a/docs/src/languages/helm.md +++ /dev/null @@ -1,25 +0,0 @@ -# Helm - -Support for Helm in Zed is provided by the community-maintained [Helm extension](https://github.com/cabrinha/helm.zed). - -- Tree-sitter: [tree-sitter-go-template](https://github.com/ngalaiko/tree-sitter-go-template/tree/master) -- Language Server: [mrjosh/helm-ls](https://github.com/mrjosh/helm-ls) - -## Setup - -Enable Helm language for Helm files by editing your `.zed/settings.json` and adding: - -```json [settings] - "file_types": { - "Helm": [ - "**/templates/**/*.tpl", - "**/templates/**/*.yaml", - "**/templates/**/*.yml", - "**/helmfile.d/**/*.yaml", - "**/helmfile.d/**/*.yml", - "**/values*.yaml" - ] - } -``` - -This will also mark values.yaml files as the type helm, since helm-ls supports this. diff --git a/docs/src/languages/html.md b/docs/src/languages/html.md deleted file mode 100644 index 274083adee..0000000000 --- a/docs/src/languages/html.md +++ /dev/null @@ -1,71 +0,0 @@ -# HTML - -HTML support is available through the [HTML extension](https://github.com/zed-industries/zed/tree/main/extensions/html). - -- Tree-sitter: [tree-sitter/tree-sitter-html](https://github.com/tree-sitter/tree-sitter-html) -- Language Server: [microsoft/vscode-html-languageservice](https://github.com/microsoft/vscode-html-languageservice) - -This extension is automatically installed, but if you do not want to use it, you can add the following to your settings: - -```json [settings] -{ - "auto_install_extensions": { - "html": false - } -} -``` - -## Formatting - -By default Zed uses [Prettier](https://prettier.io/) for formatting HTML. - -You can disable `format_on_save` by adding the following to your Zed `settings.json`: - -```json [settings] - "languages": { - "HTML": { - "format_on_save": "off", - } - } -``` - -You can still trigger formatting manually with {#kb editor::Format} or by opening [the Command Palette](..//getting-started.md#command-palette) ({#kb command_palette::Toggle}) and selecting "Format Document". - -### LSP Formatting - -To use the `vscode-html-language-server` language server auto-formatting instead of Prettier, add the following to your Zed settings: - -```json [settings] - "languages": { - "HTML": { - "formatter": "language_server", - } - } -``` - -You can customize various [formatting options](https://code.visualstudio.com/docs/languages/html#_formatting) for `vscode-html-language-server` via your Zed `settings.json`: - -```json [settings] - "lsp": { - "vscode-html-language-server": { - "settings": { - "html": { - "format": { - // Indent under and (default: false) - "indentInnerHtml": true, - // Disable formatting inside or
and

- "extraLiners": "div,p" - } - } - } - } - } -``` - -## See also - -- [CSS](./css.md) -- [JavaScript](./javascript.md) -- [TypeScript](./typescript.md) diff --git a/docs/src/languages/java.md b/docs/src/languages/java.md deleted file mode 100644 index 482429aef3..0000000000 --- a/docs/src/languages/java.md +++ /dev/null @@ -1,167 +0,0 @@ -# Java - -Java language support in Zed is provided by: - -- Zed Java: [zed-extensions/java](https://github.com/zed-extensions/java) -- Tree-sitter: [tree-sitter/tree-sitter-java](https://github.com/tree-sitter/tree-sitter-java) -- Language Server: [eclipse-jdtls/eclipse.jdt.ls](https://github.com/eclipse-jdtls/eclipse.jdt.ls) - -## Install OpenJDK - -You will need to install a Java runtime (OpenJDK). - -- macOS: `brew install openjdk` -- Ubuntu: `sudo add-apt-repository ppa:openjdk-23 && sudo apt-get install openjdk-23` -- Windows: `choco install openjdk` -- Arch Linux: `sudo pacman -S jre-openjdk-headless` - -Or manually download and install [OpenJDK 23](https://jdk.java.net/23/). - -## Extension Install - -You can install by opening {#action zed::Extensions}({#kb zed::Extensions}) and searching for `java`. - -## Quick start and configuration - -For the majority of users, Java support should work out of the box. - -- It is generally recommended to open projects with the Zed-project root at the Java project root folder (where you would commonly have your `pom.xml` or `build.gradle` file). - -- By default the extension will download and run the latest official version of JDTLS for you, but this requires Java version 21 to be available on your system via either the `$JAVA_HOME` environment variable or as a `java(.exe)` executable on your `$PATH`. If your project requires a lower Java version in the environment, you can specify a different JDK to use for running JDTLS via the `java_home` configuration option. - -- You can provide a **custom launch script for JDTLS**, by adding an executable named `jdtls` (or `jdtls.bat` on Windows) to your `$PATH` environment variable. If this is present, the extension will skip downloading and launching a managed instance and use the one from the environment. - -- To support [Lombok](https://projectlombok.org/), the lombok-jar must be downloaded and registered as a Java-Agent when launching JDTLS. By default the extension automatically takes care of that, but in case you don't want that you can set the `lombok_support` configuration-option to `false`. - -Here is a common `settings.json` including the above mentioned configurations: - -```jsonc -{ - "lsp": { - "jdtls": { - "settings": { - "java_home": "/path/to/your/JDK21+", - "lombok_support": true, - }, - }, - }, -} -``` - -## Debugging - -Debug support is enabled via our [Fork of Java Debug](https://github.com/zed-industries/java-debug), which the extension will automatically download and start for you. Please refer to the [Debugger Documentation](https://zed.dev/docs/debugger#getting-started) for general information about how debugging works in Zed. - -To get started with Java, click the `edit debug.json` button in the Debug menu, and replace the contents of the file with the following: - -```jsonc -[ - { - "adapter": "Java", - "request": "launch", - "label": "Launch Debugger", - // if your project has multiple entry points, specify the one to use: - // "mainClass": "com.myorganization.myproject.MyMainClass", - // - // this effectively sets a breakpoint at your program entry: - "stopOnEntry": true, - // the working directory for the debug process - "cwd": "$ZED_WORKTREE_ROOT", - }, -] -``` - -You should then be able to start a new Debug Session with the "Launch Debugger" scenario from the debug menu. - -## Launch Scripts (aka Tasks) in Windows - -This extension provides tasks for running your application and tests from within Zed via little play buttons next to tests/entry points. However, due to current limitations of Zed's extension interface, we can not provide scripts that will work across Maven and Gradle on both Windows and Unix-compatible systems, so out of the box the launch scripts only work on Mac and Linux. - -There is a fairly straightforward fix that you can apply to make it work on Windows by supplying your own task scripts. Please see [this Issue](https://github.com/zed-extensions/java/issues/94) for information on how to do that and read the [Tasks section in Zeds documentation](https://zed.dev/docs/tasks) for more information. - -## Advanced Configuration/JDTLS initialization Options - -JDTLS provides many configuration options that can be passed via the `initialize` LSP-request. The extension will pass the JSON-object from `lsp.jdtls.settings.initialization_options` in your settings on to JDTLS. Please refer to the [JDTLS Configuration Wiki Page](https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request) for the available options and values. Below is an example `settings.json` that would pass on the example configuration from the above wiki page to JDTLS: - -```jsonc -{ - "lsp": { - "jdtls": { - "settings": { - // this will be sent to JDTLS as initializationOptions: - "initialization_options": { - "bundles": [], - // use this if your zed project root folder is not the same as the java project root: - "workspaceFolders": ["file:///home/snjeza/Project"], - "settings": { - "java": { - "home": "/usr/local/jdk-9.0.1", - "errors": { - "incompleteClasspath": { - "severity": "warning", - }, - }, - "configuration": { - "updateBuildConfiguration": "interactive", - "maven": { - "userSettings": null, - }, - }, - "import": { - "gradle": { - "enabled": true, - }, - "maven": { - "enabled": true, - }, - "exclusions": [ - "**/node_modules/**", - "**/.metadata/**", - "**/archetype-resources/**", - "**/META-INF/maven/**", - "/**/test/**", - ], - }, - "referencesCodeLens": { - "enabled": false, - }, - "signatureHelp": { - "enabled": false, - }, - "implementationCodeLens": "all", - "format": { - "enabled": true, - }, - "saveActions": { - "organizeImports": false, - }, - "contentProvider": { - "preferred": null, - }, - "autobuild": { - "enabled": false, - }, - "completion": { - "favoriteStaticMembers": [ - "org.junit.Assert.*", - "org.junit.Assume.*", - "org.junit.jupiter.api.Assertions.*", - "org.junit.jupiter.api.Assumptions.*", - "org.junit.jupiter.api.DynamicContainer.*", - "org.junit.jupiter.api.DynamicTest.*", - ], - "importOrder": ["java", "javax", "com", "org"], - }, - }, - }, - }, - }, - }, - }, -} -``` - -## See also - -[Zed Java Repo](https://github.com/zed-extensions/java) -[Eclipse JDTLS Repo](https://github.com/eclipse-jdtls/eclipse.jdt.ls) diff --git a/docs/src/languages/javascript.md b/docs/src/languages/javascript.md deleted file mode 100644 index 1b87dac555..0000000000 --- a/docs/src/languages/javascript.md +++ /dev/null @@ -1,237 +0,0 @@ -# JavaScript - -JavaScript support is available natively in Zed. - -- Tree-sitter: [tree-sitter/tree-sitter-javascript](https://github.com/tree-sitter/tree-sitter-javascript) -- Language Server: [yioneko/vtsls](https://github.com/yioneko/vtsls) -- Alternate Language Server: [typescript-language-server/typescript-language-server](https://github.com/typescript-language-server/typescript-language-server) -- Debug Adapter: [vscode-js-debug](https://github.com/microsoft/vscode-js-debug) - -## Code formatting - -Formatting on save is enabled by default for JavaScript, using TypeScript's built-in code formatting. -But many JavaScript projects use other command-line code-formatting tools, such as [Prettier](https://prettier.io/). -You can use one of these tools by specifying an _external_ code formatter for JavaScript in your settings. -See [the configuration docs](../configuring-zed.md) for more information. - -For example, if you have Prettier installed and on your `PATH`, you can use it to format JavaScript files by adding the following to your `settings.json`: - -```json [settings] -{ - "languages": { - "JavaScript": { - "formatter": { - "external": { - "command": "prettier", - "arguments": ["--stdin-filepath", "{buffer_path}"] - } - } - } - } -} -``` - -## JSX - -Zed supports JSX syntax highlighting out of the box. - -In JSX strings, the [`tailwindcss-language-server`](./tailwindcss.md) is used to provide autocompletion for Tailwind CSS classes. - -## JSDoc - -Zed supports JSDoc syntax in JavaScript and TypeScript comments that match the JSDoc syntax. -Zed uses [tree-sitter/tree-sitter-jsdoc](https://github.com/tree-sitter/tree-sitter-jsdoc) for parsing and highlighting JSDoc. - -## ESLint - -You can configure Zed to format code using `eslint --fix` by running the ESLint code action when formatting: - -```json [settings] -{ - "languages": { - "JavaScript": { - "code_actions_on_format": { - "source.fixAll.eslint": true - } - } - } -} -``` - -You can also only execute a single ESLint rule when using `fixAll`: - -```json [settings] -{ - "languages": { - "JavaScript": { - "code_actions_on_format": { - "source.fixAll.eslint": true - } - } - }, - "lsp": { - "eslint": { - "settings": { - "codeActionOnSave": { - "rules": ["import/order"] - } - } - } - } -} -``` - -> **Note:** the other formatter you have configured will still run, after ESLint. -> So if your language server or Prettier configuration don't format according to -> ESLint's rules, then they will overwrite what ESLint fixed and you end up with -> errors. - -If you **only** want to run ESLint on save, you can configure code actions as -the formatter: - -```json [settings] -{ - "languages": { - "JavaScript": { - "formatter": [], - "code_actions_on_format": { - "source.fixAll.eslint": true - } - } - } -} -``` - -### Configure ESLint's `nodePath`: - -You can configure ESLint's `nodePath` setting: - -```json [settings] -{ - "lsp": { - "eslint": { - "settings": { - "nodePath": ".yarn/sdks" - } - } - } -} -``` - -### Configure ESLint's `problems`: - -You can configure ESLint's `problems` setting. - -For example, here's how to set `problems.shortenToSingleLine`: - -```json [settings] -{ - "lsp": { - "eslint": { - "settings": { - "problems": { - "shortenToSingleLine": true - } - } - } - } -} -``` - -### Configure ESLint's `rulesCustomizations`: - -You can configure ESLint's `rulesCustomizations` setting: - -```json [settings] -{ - "lsp": { - "eslint": { - "settings": { - "rulesCustomizations": [ - // set all eslint errors/warnings to show as warnings - { "rule": "*", "severity": "warn" } - ] - } - } - } -} -``` - -### Configure ESLint's `workingDirectory`: - -You can configure ESLint's `workingDirectory` setting: - -```json [settings] -{ - "lsp": { - "eslint": { - "settings": { - "workingDirectory": { - "mode": "auto" - } - } - } - } -} -``` - -## Debugging - -Zed supports debugging JavaScript code out of the box with `vscode-js-debug`. -The following can be debugged without writing additional configuration: - -- Tasks from `package.json` -- Tests written using several popular frameworks (Jest, Mocha, Vitest, Jasmine, Bun, Node) - -Run {#action debugger::Start} ({#kb debugger::Start}) to see a contextual list of these predefined debug tasks. - -> **Note:** Bun test is automatically detected when `@types/bun` is present in `package.json`. -> -> **Note:** Node test is automatically detected when `@types/node` is present in `package.json` (requires Node.js 20+). - -As for all languages, configurations from `.vscode/launch.json` are also available for debugging in Zed. - -If your use-case isn't covered by any of these, you can take full control by adding debug configurations to `.zed/debug.json`. See below for example configurations. - -### Configuring JavaScript debug tasks - -JavaScript debugging is more complicated than other languages because there are two different environments: Node.js and the browser. `vscode-js-debug` exposes a `type` field, that you can use to specify the environment, either `node` or `chrome`. - -- [vscode-js-debug configuration documentation](https://github.com/microsoft/vscode-js-debug/blob/main/OPTIONS.md) - -### Debug the current file with Node - -```json [debug] -[ - { - "adapter": "JavaScript", - "label": "Debug JS file", - "type": "node", - "request": "launch", - "program": "$ZED_FILE", - "skipFiles": ["/**"] - } -] -``` - -### Launch a web app in Chrome - -```json [debug] -[ - { - "adapter": "JavaScript", - "label": "Debug app in Chrome", - "type": "chrome", - "request": "launch", - "file": "$ZED_WORKTREE_ROOT/index.html", - "webRoot": "$ZED_WORKTREE_ROOT", - "console": "integratedTerminal", - "skipFiles": ["/**"] - } -] -``` - -## See also - -- [Yarn documentation](./yarn.md) for a walkthrough of configuring your project to use Yarn. -- [TypeScript documentation](./typescript.md) diff --git a/docs/src/languages/json.md b/docs/src/languages/json.md deleted file mode 100644 index 33acdb172e..0000000000 --- a/docs/src/languages/json.md +++ /dev/null @@ -1,77 +0,0 @@ -# JSON - -JSON support is available natively in Zed. - -- Tree-sitter: [tree-sitter/tree-sitter-json](https://github.com/tree-sitter/tree-sitter-json) -- Language Server: [zed-industries/json-language-server](https://github.com/zed-industries/json-language-server) - -## JSONC - -Zed also supports a super-set of JSON called JSONC, which allows single line comments (`//`) in JSON files. -While editing these files you can use `cmd-/` (macOS) or `ctrl-/` (Linux) to toggle comments on the current line or selection. - -## JSONC Prettier Formatting - -If you use files with the `*.jsonc` extension when using `Format Document` or have `format_on_save` enabled, Zed invokes Prettier as the formatter. Prettier has an [outstanding issue](https://github.com/prettier/prettier/issues/15956) where it will add trailing commas to files with a `jsonc` extension. JSONC files which have a `.json` extension are unaffected. - -To workaround this behavior you can add the following to your `.prettierrc` configuration file: - -```json [settings] -{ - "overrides": [ - { - "files": ["*.jsonc"], - "options": { - "parser": "json", - "trailingComma": "none" - } - } - ] -} -``` - -## JSON Language Server - -Zed automatically out of the box supports JSON Schema validation of `package.json` and `tsconfig.json` files, but `json-language-server` can use JSON Schema definitions in project files, from the [JSON Schema Store](https://www.schemastore.org) or other publicly available URLs for JSON validation. - -### Inline Schema Specification - -To specify a schema inline with your JSON files, add a `$schema` top level key linking to your json schema file. - -For example to for a `.luarc.json` for use with [lua-language-server](https://github.com/LuaLS/lua-language-server/): - -```json [settings] -{ - "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", - "runtime.version": "Lua 5.4" -} -``` - -### Schema Specification via Settings - -You can alternatively associate JSON Schemas with file paths by via Zed LSP settings. - -To - -```json [settings] -"lsp": { - "json-language-server": { - "settings": { - "json": { - "schemas": [ - { - "fileMatch": ["*/*.luarc.json"], - "url": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json" - } - ] - } - } - } -} -``` - -You can also pass any of the [supported settings](https://github.com/Microsoft/vscode/blob/main/extensions/json-language-features/server/README.md#settings) to json-language-server by specifying them in your Zed settings.json: - - diff --git a/docs/src/languages/jsonnet.md b/docs/src/languages/jsonnet.md deleted file mode 100644 index 405087766b..0000000000 --- a/docs/src/languages/jsonnet.md +++ /dev/null @@ -1,24 +0,0 @@ -# Jsonnet - -Jsonnet language support in Zed is provided by the community-maintained [Jsonnet extension](https://github.com/narqo/zed-jsonnet). - -- Tree-sitter: [sourcegraph/tree-sitter-jsonnet](https://github.com/sourcegraph/tree-sitter-jsonnet) -- Language Server: [grafana/jsonnet-language-server](https://github.com/grafana/jsonnet-language-server) - -## Configuration - -Workspace configuration options can be passed to the language server via the `lsp` settings of the `settings.json`. - -The following example enables support for resolving [tanka](https://tanka.dev) import paths in `jsonnet-language-server`: - -```json [settings] -{ - "lsp": { - "jsonnet-language-server": { - "settings": { - "resolve_paths_with_tanka": true - } - } - } -} -``` diff --git a/docs/src/languages/julia.md b/docs/src/languages/julia.md deleted file mode 100644 index 171f8c816f..0000000000 --- a/docs/src/languages/julia.md +++ /dev/null @@ -1,12 +0,0 @@ -# Julia - -Julia language support in Zed is provided by the community-maintained [Julia extension](https://github.com/JuliaEditorSupport/zed-julia). -Report issues to: [https://github.com/JuliaEditorSupport/zed-julia/issues](https://github.com/JuliaEditorSupport/zed-julia/issues) - -- Tree-sitter: [tree-sitter/tree-sitter-julia](https://github.com/tree-sitter/tree-sitter-julia) -- Language Server: [julia-vscode/LanguageServer.jl](https://github.com/julia-vscode/LanguageServer.jl) - - diff --git a/docs/src/languages/kotlin.md b/docs/src/languages/kotlin.md deleted file mode 100644 index a81643ab7d..0000000000 --- a/docs/src/languages/kotlin.md +++ /dev/null @@ -1,55 +0,0 @@ -# Kotlin - -Kotlin language support in Zed is provided by the community-maintained [Kotlin extension](https://github.com/zed-extensions/kotlin). -Report issues to: [https://github.com/zed-extensions/kotlin/issues](https://github.com/zed-extensions/kotlin/issues) - -- Tree-sitter: [fwcd/tree-sitter-kotlin](https://github.com/fwcd/tree-sitter-kotlin) -- Language Server: [fwcd/kotlin-language-server](https://github.com/fwcd/kotlin-language-server) - -## Configuration - -Workspace configuration options can be passed to the language server via lsp -settings in `settings.json`. - -The full list of lsp `settings` can be found -[here](https://github.com/fwcd/kotlin-language-server/blob/main/server/src/main/kotlin/org/javacs/kt/Configuration.kt) -under `class Configuration` and initialization_options under `class InitializationOptions`. - -### JVM Target - -The following example changes the JVM target from `default` (which is 1.8) to -`17`: - -```json [settings] -{ - "lsp": { - "kotlin-language-server": { - "settings": { - "compiler": { - "jvm": { - "target": "17" - } - } - } - } - } -} -``` - -### JAVA_HOME - -To use a specific java installation, just specify the `JAVA_HOME` environment variable with: - -```json [settings] -{ - "lsp": { - "kotlin-language-server": { - "binary": { - "env": { - "JAVA_HOME": "/Users/whatever/Applications/Work/Android Studio.app/Contents/jbr/Contents/Home" - } - } - } - } -} -``` diff --git a/docs/src/languages/lua.md b/docs/src/languages/lua.md deleted file mode 100644 index 65b709b391..0000000000 --- a/docs/src/languages/lua.md +++ /dev/null @@ -1,181 +0,0 @@ -# Lua - -Lua support is available through the [Lua extension](https://github.com/zed-extensions/lua). - -- Tree-sitter: [tree-sitter-grammars/tree-sitter-lua](https://github.com/tree-sitter-grammars/tree-sitter-lua) -- Language server: [LuaLS/lua-language-server](https://github.com/LuaLS/lua-language-server) - -## luarc.json - -To configure LuaLS you can create a `.luarc.json` file in the root of your workspace. - -```json [settings] -{ - "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", - "runtime.version": "Lua 5.4", - "format.enable": true, - "workspace.library": ["../somedir/library"] -} -``` - -See [LuaLS Settings Documentation](https://luals.github.io/wiki/settings/) for all available configuration options, or when editing this file in Zed available settings options will autocomplete, (e.g `runtime.version` will show `"Lua 5.1"`, `"Lua 5.2"`, `"Lua 5.3"`, `"Lua 5.4"` and `"LuaJIT"` as allowed values). Note when importing settings options from VS Code, remove the `Lua.` prefix. (e.g. `runtime.version` instead of `Lua.runtime.version`). - -### LuaCATS Definitions - -LuaLS can provide enhanced LSP autocompletion suggestions and type validation with the help of LuaCATS (Lua Comment and Type System) definitions. These definitions are available for many common Lua libraries, and local paths containing them can be specified via `workspace.library` in `luarc.json`. You can do this via relative paths if you checkout your definitions into the same partent directory of your project (`../playdate-luacats`, `../love2d`, etc). Alternatively you can create submodule(s) inside your project for each LuaCATS definition repo. - -### LÖVE (Love2D) {#love2d} - -To use [LÖVE (Love2D)](https://love2d.org/) in Zed, checkout [LuaCATS/love2d](https://github.com/LuaCATS/love2d) into a folder called `love2d-luacats` into the parent folder of your project: - -```sh -cd .. && git clone https://github.com/LuaCATS/love2d love2d-luacats -``` - -Then in your `.luarc.json`: - -``` -{ - "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", - "runtime.version": "Lua 5.4", - "workspace.library": ["../love2d-luacats"], - "runtime.special": { - "love.filesystem.load": "loadfile" - } -} -``` - -### PlaydateSDK - -To use [Playdate Lua SDK](https://play.date/dev/) in Zed, checkout [playdate-luacats](https://github.com/notpeter/playdate-luacats) into the parent folder of your project: - -```sh -cd .. && git clone https://github.com/notpeter/playdate-luacats -``` - -Then in your `.luarc.json`: - -```json [settings] -{ - "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", - "runtime.version": "Lua 5.4", - "runtime.nonstandardSymbol": [ - "+=", - "-=", - "*=", - "/=", - "//=", - "%=", - "<<=", - ">>=", - "&=", - "|=", - "^=" - ], - "diagnostics.severity": { "duplicate-set-field": "Hint" }, - "diagnostics.globals": ["import"], - "workspace.library": ["../playdate-luacats"], - "format.defaultConfig": { - "indent_style": "space", - "indent_size": "4" - }, - "format.enable": true, - "runtime.builtin": { "io": "disable", "os": "disable", "package": "disable" } -} -``` - -### Inlay Hints - -To enable [Inlay Hints](../configuring-languages.md#inlay-hints) for LuaLS in Zed - -1. Add the following to your Zed settings.json: - -```json [settings] - "languages": { - "Lua": { - "inlay_hints": { - "enabled": true, - "show_type_hints": true, - "show_parameter_hints": true, - "show_other_hints": true - } - } - } -``` - -2. Add `"hint.enable": true` to your `.luarc.json`. - -## Formatting - -### LuaLS Formatting - -To enable auto-formatting with your LuaLS (provided by [CppCXY/EmmyLuaCodeStyle](https://github.com/CppCXY/EmmyLuaCodeStyle)) make sure you have `"format.enable": true,` in your .luarc.json: - -```json [settings] -{ - "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", - "format.enable": true -} -``` - -Then add the following to your Zed `settings.json`: - -```json [settings] -{ - "languages": { - "Lua": { - "format_on_save": "on", - "formatter": "language_server" - } - } -} -``` - -You can customize various EmmyLuaCodeStyle style options via `.editorconfig`, see [lua.template.editorconfig](https://github.com/CppCXY/EmmyLuaCodeStyle/blob/master/lua.template.editorconfig) for all available options. - -### StyLua Formatting - -Alternatively to use [StyLua](https://github.com/JohnnyMorganz/StyLua) for auto-formatting: - -1. Install [StyLua](https://github.com/JohnnyMorganz/StyLua): `brew install stylua` or `cargo install stylua --features lua52,lua53,lua54,luau,luajit` (feel free to remove any Lua versions you don't need). -2. Add the following to your `settings.json`: - -```json [settings] -{ - "languages": { - "Lua": { - "format_on_save": "on", - "formatter": { - "external": { - "command": "stylua", - "arguments": [ - "--syntax=Lua54", - "--respect-ignores", - "--stdin-filepath", - "{buffer_path}", - "-" - ] - } - } - } - } -} -``` - -You can specify various options to StyLua either on the command line above (like `--syntax=Lua54`) or in a `stylua.toml` in your workspace: - -```toml -syntax = "Lua54" -column_width = 100 -line_endings = "Unix" -indent_type = "Spaces" -indent_width = 4 -quote_style = "AutoPreferDouble" -call_parentheses = "Always" -collapse_simple_statement = "All" - -[sort_requires] -enabled = true -``` - -For a complete list of available options, see: [StyLua Options](https://github.com/JohnnyMorganz/StyLua?tab=readme-ov-file#options). diff --git a/docs/src/languages/luau.md b/docs/src/languages/luau.md deleted file mode 100644 index b99cfc86ac..0000000000 --- a/docs/src/languages/luau.md +++ /dev/null @@ -1,41 +0,0 @@ -# Luau - -[Luau](https://luau.org/) is a fast, small, safe, gradually typed, embeddable scripting language derived from Lua. Luau was developed by Roblox and is available under the MIT license. - -Luau language support in Zed is provided by the community-maintained [Luau extension](https://github.com/4teapo/zed-luau). -Report issues to: [https://github.com/4teapo/zed-luau/issues](https://github.com/4teapo/zed-luau/issues) - -- Tree-sitter: [4teapo/tree-sitter-luau](https://github.com/4teapo/tree-sitter-luau) -- Language Server: [JohnnyMorganz/luau-lsp](https://github.com/JohnnyMorganz/luau-lsp) - -## Configuration - -Configuration instructions are available in the [Luau Zed Extension README](https://github.com/4teapo/zed-luau). - -## Formatting - -To support automatically formatting your code, you can use [JohnnyMorganz/StyLua](https://github.com/JohnnyMorganz/StyLua), a Lua code formatter. - -Install with: - -```sh -# macOS via Homebrew -brew install stylua -# Or via Cargo -cargo install stylua --features lua52,lua53,lua54,luau -``` - -Then add the following to your Zed `settings.json`: - -```json [settings] - "languages": { - "Luau": { - "formatter": { - "external": { - "command": "stylua", - "arguments": ["-"] - } - } - } - } -``` diff --git a/docs/src/languages/makefile.md b/docs/src/languages/makefile.md deleted file mode 100644 index 1d42299145..0000000000 --- a/docs/src/languages/makefile.md +++ /dev/null @@ -1,6 +0,0 @@ -# Makefile - -Makefile language support in Zed is provided by the community-maintained [Make extension](https://github.com/caius/zed-make). -Report issues to: [https://github.com/caius/zed-make/issues](https://github.com/caius/zed-make/issues). - -- Tree-sitter: [caius/tree-sitter-make](https://github.com/caius/tree-sitter-make) diff --git a/docs/src/languages/markdown.md b/docs/src/languages/markdown.md deleted file mode 100644 index 36ce734f7c..0000000000 --- a/docs/src/languages/markdown.md +++ /dev/null @@ -1,46 +0,0 @@ -# Markdown - -Markdown support is available natively in Zed. - -- Tree-sitter: [tree-sitter-markdown](https://github.com/tree-sitter-grammars/tree-sitter-markdown) -- Language Server: N/A - -## Syntax Highlighting Code Blocks - -Zed supports language-specific syntax highlighting of markdown code blocks by leveraging [tree-sitter language grammars](../extensions/languages.md#grammar). All [Zed supported languages](../languages.md), including those provided by official or community extensions, are available for use in markdown code blocks. All you need to do is provide a language name after the opening ``` code fence like so: - -````python -```python -import functools as ft - -@ft.lru_cache(maxsize=500) -def fib(n): - return n if n < 2 else fib(n - 1) + fib(n - 2) -``` -```` - -## Configuration - -### Format - -Zed supports using Prettier to automatically re-format Markdown documents. You can trigger this manually via the {#action editor::Format} action or via the {#kb editor::Format} keyboard shortcut. Alternately, you can automatically format by enabling [`format_on_save`](../configuring-zed.md#format-on-save) in your settings.json: - -```json [settings] - "languages": { - "Markdown": { - "format_on_save": "on" - } - }, -``` - -### Trailing Whitespace - -By default Zed will remove trailing whitespace on save. If you rely on invisible trailing whitespace being converted to `
` in Markdown files you can disable this behavior with: - -```json [settings] - "languages": { - "Markdown": { - "remove_trailing_whitespace_on_save": false - } - }, -``` diff --git a/docs/src/languages/nim.md b/docs/src/languages/nim.md deleted file mode 100644 index 03c2bc0609..0000000000 --- a/docs/src/languages/nim.md +++ /dev/null @@ -1,24 +0,0 @@ -# Nim - -Nim language support in Zed is provided by the community-maintained [Nim extension](https://github.com/foxoman/zed-nim). -Report issues to: [https://github.com/foxoman/zed-nim/issues](https://github.com/foxoman/zed-nim/issues) - -- Tree-sitter: [alaviss/tree-sitter-nim](https://github.com/alaviss/tree-sitter-nim) -- Language Server: [nim-lang/langserver](https://github.com/nim-lang/langserver) - -## Formatting - -To use [arnetheduck/nph](https://github.com/arnetheduck/nph) as a formatter, follow the [nph installation instructions](https://github.com/arnetheduck/nph?tab=readme-ov-file#installation) and add this to your Zed `settings.json`: - -```json [settings] - "languages": { - "Nim": { - "formatter": { - "external": { - "command": "nph", - "arguments": ["-"] - } - } - } - } -``` diff --git a/docs/src/languages/ocaml.md b/docs/src/languages/ocaml.md deleted file mode 100644 index 10c3c1ac09..0000000000 --- a/docs/src/languages/ocaml.md +++ /dev/null @@ -1,36 +0,0 @@ -# OCaml - -OCaml support is available through the [OCaml extension](https://github.com/zed-extensions/ocaml). - -- Tree-sitter: [tree-sitter/tree-sitter-ocaml](https://github.com/tree-sitter/tree-sitter-ocaml) -- Language Server: [ocaml/ocaml-lsp](https://github.com/ocaml/ocaml-lsp) - -## Setup Instructions - -If you have the development environment already setup, you can skip to [Launching Zed](#launching-zed) - -### Using Opam - -Opam is the official package manager for OCaml and is highly recommended for getting started with OCaml. To get started using Opam, please follow the instructions provided [here](https://ocaml.org/install). - -Once you install opam and setup a switch with your development environment as per the instructions, you can proceed. - -### Launching Zed - -By now you should have `ocamllsp` installed, you can verify so by running - -```sh -ocamllsp --help -``` - -in your terminal. If you get a help message, you're good to go. If not, please revisit the installation instructions for `ocamllsp` and ensure it's properly installed. - -With that aside, we can now launch Zed. Given how the OCaml package manager works, we require you to run Zed from the terminal, so please make sure you install the [Zed cli](https://zed.dev/features#cli) if you haven't already. - -Once you have the cli, simply from a terminal, navigate to your project and run - -```sh -zed . -``` - -Voilà! You should have Zed running with OCaml support, no additional setup required. diff --git a/docs/src/languages/opentofu.md b/docs/src/languages/opentofu.md deleted file mode 100644 index dfe8fa7b81..0000000000 --- a/docs/src/languages/opentofu.md +++ /dev/null @@ -1,20 +0,0 @@ -# OpenTofu - -OpenTofu support is available through the [OpenTofu extension](https://github.com/ashpool37/zed-extension-opentofu). - -- Tree-sitter: [MichaHoffmann/tree-sitter-hcl](https://github.com/MichaHoffmann/tree-sitter-hcl) -- Language Server: [opentofu/tofu-ls](https://github.com/opentofu/tofu-ls) - -## Configuration - -In order to automatically use the OpenTofu extension and language server when editing .tf and .tfvars files, -either uninstall the Terraform extension or add this to your settings.json: - -```json -"file_types": { - "OpenTofu": ["tf"], - "OpenTofu Vars": ["tfvars"] -}, -``` - -See the [full list of server settings here](https://github.com/opentofu/tofu-ls/blob/main/docs/SETTINGS.md). diff --git a/docs/src/languages/php.md b/docs/src/languages/php.md deleted file mode 100644 index 1a9f1cdade..0000000000 --- a/docs/src/languages/php.md +++ /dev/null @@ -1,156 +0,0 @@ -# PHP - -PHP support is available through the [PHP extension](https://github.com/zed-extensions/php). - -- Tree-sitter: [tree-sitter/tree-sitter-php](https://github.com/tree-sitter/tree-sitter-php) -- Language Server: [phpactor/phpactor](https://github.com/phpactor/phpactor) -- Alternate Language Server: [bmewburn/vscode-intelephense](https://github.com/bmewburn/vscode-intelephense/) - -## Install PHP - -The PHP extension requires PHP to be installed and available in your `PATH`: - -```sh -# macOS via Homebrew -brew install php - -# Debian/Ubuntu -sudo apt-get install php-cli - -# CentOS 8+/RHEL -sudo dnf install php-cli - -# Arch Linux -sudo pacman -S php - -# check PHP path -## macOS and Linux -which php - -## Windows -where php -``` - -## Choosing a language server - -The PHP extension uses [LSP language servers](https://microsoft.github.io/language-server-protocol) with Phpactor as the default. If you want to use other language servers that support Zed (e.g. Intelephense or PHP Tools), make sure to follow the documentation on how to implement it. - -### Intelephense - -[Intelephense](https://intelephense.com/) is a [proprietary](https://github.com/bmewburn/vscode-intelephense/blob/master/LICENSE.txt#L29) language server for PHP operating under a freemium model. Certain features require purchase of a [premium license](https://intelephense.com/buy). - -To use Intelephense, add the following to your `settings.json`: - -```json [settings] -{ - "languages": { - "PHP": { - "language_servers": ["intelephense", "!phpactor", "!phptools", "..."] - } - } -} -``` - -To use the premium features, you can place your license file inside your home directory at `~/intelephense/licence.txt` for macOS and Linux, or `%USERPROFILE%/intelephense/licence.txt` on Windows. - -Alternatively, you can pass the licence key or a path to a file containing the licence key as an initialization option. To do this, add the following to your `settings.json`: - -```json [settings] -{ - "lsp": { - "intelephense": { - "initialization_options": { - "licenceKey": "/path/to/licence.txt" - } - } - } -} -``` - -### PHP Tools - -[PHP Tools](https://www.devsense.com/) is a proprietary language server that offers free and premium features. You need to [purchase a license](https://www.devsense.com/en/purchase) to activate the premium features. - -To use PHP Tools, add the following to your `settings.json`: - -```json [settings] -{ - "languages": { - "PHP": { - "language_servers": ["phptools", "!intelephense", "!phpactor", "..."] - } - } -} -``` - -To use the premium features, you can add your license in `initialization_options` in your `settings.json`: - -```json [settings] -{ - "lsp": { - "phptools": { - "initialization_options": { - "0": "your_license_key" - } - } - } -} -``` - -or, set environment variable `DEVSENSE_PHP_LS_LICENSE` on `.env` file in your project. - -```env -DEVSENSE_PHP_LS_LICENSE="your_license_key" -``` - -Check out the documentation of [PHP Tools for Zed](https://docs.devsense.com/other/zed/) for more details. - -### Phpactor - -To use Phpactor instead of Intelephense or any other tools, add the following to your `settings.json`: - -```json [settings] -{ - "languages": { - "PHP": { - "language_servers": ["phpactor", "!intelephense", "!phptools", "..."] - } - } -} -``` - -## PHPDoc - -Zed supports syntax highlighting for PHPDoc comments. - -- Tree-sitter: [claytonrcarter/tree-sitter-phpdoc](https://github.com/claytonrcarter/tree-sitter-phpdoc) - -## Debugging - -The PHP extension provides a debug adapter for PHP via Xdebug. There are several ways to use it: - -```json -[ - { - "label": "PHP: Listen to Xdebug", - "adapter": "Xdebug", - "request": "launch", - "port": 9003 - }, - { - "label": "PHP: Debug this test", - "adapter": "Xdebug", - "request": "launch", - "program": "vendor/bin/phpunit", - "args": ["--filter", "$ZED_SYMBOL"] - } -] -``` - -These are common troubleshooting tips, in case you run into issues: - -- Ensure that you have Xdebug installed for the version of PHP you’re running. -- Ensure that Xdebug is configured to run in `debug` mode. -- Ensure that Xdebug is actually starting a debugging session. -- Ensure that the host and port matches between Xdebug and Zed. -- Look at the diagnostics log by using the `xdebug_info()` function in the page you’re trying to debug. diff --git a/docs/src/languages/powershell.md b/docs/src/languages/powershell.md deleted file mode 100644 index 195ce4ad36..0000000000 --- a/docs/src/languages/powershell.md +++ /dev/null @@ -1,35 +0,0 @@ -# PowerShell - -PowerShell language support in Zed is provided by the community-maintained [Zed PowerShell extension](https://github.com/wingyplus/zed-powershell). Please report issues to: [github.com/wingyplus/zed-powershell/issues](https://github.com/wingyplus/zed-powershell/issues) - -- Tree-sitter: [airbus-cert/tree-sitter-powershell](https://github.com/airbus-cert/tree-sitter-powershell) -- Language Server: [PowerShell/PowerShellEditorServices](https://github.com/PowerShell/PowerShellEditorServices) - -## Setup - -### Install PowerShell 7+ {#powershell-install} - -- macOS: `brew install powershell/tap/powershell` -- Alpine: [Installing PowerShell on Alpine Linux](https://learn.microsoft.com/en-us/powershell/scripting/install/install-alpine) -- Debian: [Install PowerShell on Debian Linux](https://learn.microsoft.com/en-us/powershell/scripting/install/install-debian) -- RedHat: [Install PowerShell on RHEL](https://learn.microsoft.com/en-us/powershell/scripting/install/install-rhel) -- Ubuntu: [Install PowerShell on RHEL](https://learn.microsoft.com/en-us/powershell/scripting/install/install-ubuntu) -- Windows: [Install PowerShell on Windows](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows) - -The Zed PowerShell extension will default to the `pwsh` executable found in your path. - -### Install PowerShell Editor Services (Optional) {#powershell-editor-services} - -The Zed PowerShell extensions will attempt to download [PowerShell Editor Services](https://github.com/PowerShell/PowerShellEditorServices) automatically. - -If want to use a specific binary, you can specify in your that in your Zed settings.json: - -```json [settings] - "lsp": { - "powershell-es": { - "binary": { - "path": "/path/to/PowerShellEditorServices" - } - } - } -``` diff --git a/docs/src/languages/prisma.md b/docs/src/languages/prisma.md deleted file mode 100644 index 92924a6043..0000000000 --- a/docs/src/languages/prisma.md +++ /dev/null @@ -1,10 +0,0 @@ -# Prisma - -Prisma support is available through the [Prisma extension](https://github.com/zed-extensions/prisma). - -- Tree-sitter: [victorhqc/tree-sitter-prisma](https://github.com/victorhqc/tree-sitter-prisma) -- Language-Server: [prisma/language-tools](https://github.com/prisma/language-tools) - - diff --git a/docs/src/languages/proto.md b/docs/src/languages/proto.md deleted file mode 100644 index 8d9b8350fa..0000000000 --- a/docs/src/languages/proto.md +++ /dev/null @@ -1,79 +0,0 @@ -# Proto - -Proto/proto3 (Protocol Buffers definition language) support is available through the [Proto extension](https://github.com/zed-industries/zed/tree/main/extensions/proto). - -- Tree-sitter: [coder3101/tree-sitter-proto](https://github.com/coder3101/tree-sitter-proto) -- Language Servers: [protobuf-language-server](https://github.com/lasorda/protobuf-language-server) - - diff --git a/docs/src/languages/purescript.md b/docs/src/languages/purescript.md deleted file mode 100644 index a3e3de3cd3..0000000000 --- a/docs/src/languages/purescript.md +++ /dev/null @@ -1,6 +0,0 @@ -# PureScript - -PureScript support is available through the [PureScript extension](https://github.com/zed-extensions/purescript). - -- Tree-sitter: [postsolar/tree-sitter-purescript](https://github.com/postsolar/tree-sitter-purescript) -- Language-Server: [nwolverson/purescript-language-server](https://github.com/nwolverson/purescript-language-server) diff --git a/docs/src/languages/python.md b/docs/src/languages/python.md deleted file mode 100644 index 5051a72209..0000000000 --- a/docs/src/languages/python.md +++ /dev/null @@ -1,359 +0,0 @@ -# How to Set Up Python in Zed - -Python support is available natively in Zed. - -- Tree-sitter: [tree-sitter-python](https://github.com/zed-industries/tree-sitter-python) -- Language Servers: - - [DetachHead/basedpyright](https://github.com/DetachHead/basedpyright) - - [astral-sh/ruff](https://github.com/astral-sh/ruff) - - [astral-sh/ty](https://github.com/astral-sh/ty) - - [microsoft/pyright](https://github.com/microsoft/pyright) - - [python-lsp/python-lsp-server](https://github.com/python-lsp/python-lsp-server) (PyLSP) -- Debug Adapter: [debugpy](https://github.com/microsoft/debugpy) - -## Install Python - -You'll need both Zed and Python installed before you can begin. - -### Step 1: Install Python - -Zed does not bundle a Python runtime, so you’ll need to install one yourself. -Choose one of the following options: - -- uv (recommended) - -```bash -curl -LsSf https://astral.sh/uv/install.sh | sh -``` - -To learn more, visit [Astral’s installation guide](https://docs.astral.sh/uv/getting-started/installation/). - -- Homebrew: - -```bash -brew install python -``` - -- Python.org installer: Download the latest version from [python.org/downloads](https://python.org/downloads). - -### Step 2: Verify Python Installation - -Confirm Python is installed and available in your shell: - -```bash -python3 --version -``` - -You should see an output like `Python 3.x.x`. - -## Open Your First Python Project in Zed - -Once Zed and Python are installed, open a folder containing Python code to start working. - -### Step 1: Launch Zed with a Python Project - -Open Zed. -From the menu bar, choose File > Open Folder, or launch from the terminal: - -```bash -zed path/to/your/project -``` - -Zed will recognize `.py` files automatically using its native tree-sitter-python parser, with no plugins or manual setup required. - -### Step 2: Use the Integrated Terminal (Optional) - -Zed includes an integrated terminal, accessible from the bottom panel. If Zed detects that your project is using a [virtual environment](#virtual-environments), it will be activated automatically in newly-created terminals. You can configure this behavior with the [`detect_venv`](../configuring-zed.md#terminal-detect_venv) setting. - -## Configure Python Language Servers in Zed - -Zed provides several Python language servers out of the box. By default, [basedpyright](https://github.com/DetachHead/basedpyright) is the primary language server, and [Ruff](https://github.com/astral-sh/ruff) is used for formatting and linting. - -Other built-in language servers are: - -- [Ty](https://docs.astral.sh/ty/)—Up-and-coming language server from Astral, built for speed. -- [Pyright](https://github.com/microsoft/pyright)—The basis for basedpyright. -- [PyLSP](https://github.com/python-lsp/python-lsp-server)—A plugin-based language server that integrates with tools like `pycodestyle`, `autopep8`, and `yapf`. - -These are disabled by default, but can be enabled in your settings. For example: - -```json [settings] -{ - "languages": { - "Python": { - "language_servers": [ - // Disable basedpyright and enable Ty, and otherwise - // use the default configuration. - "ty", - "!basedpyright", - "..." - ] - } - } -} -``` - -See: [Working with Language Servers](https://zed.dev/docs/configuring-languages#working-with-language-servers) for more information about how to enable and disable language servers. - -### Basedpyright - -[basedpyright](https://docs.basedpyright.com/latest/) is the primary Python language server in Zed beginning with Zed v0.204.0. It provides core language server functionality like navigation (go to definition/find all references) and type checking. Compared to Pyright, it adds support for additional language server features (like inlay hints) and checking rules. - -Note that while basedpyright in isolation defaults to the `recommended` [type-checking mode](https://docs.basedpyright.com/latest/benefits-over-pyright/better-defaults/#typecheckingmode), Zed configures it to use the less-strict `standard` mode by default, which matches the behavior of Pyright. You can set the type-checking mode for your project using the `typeCheckingMode` setting in `pyrightconfig.json` or `pyproject.toml`, which will override Zed's default. Read on more for more details about how to configure basedpyright. - -#### Basedpyright Configuration - -basedpyright reads configuration options from two different kinds of sources: - -- Language server settings ("workspace configuration"), which must be configured per-editor (using `settings.json` in Zed's case) but apply to all projects opened in that editor -- Configuration files (`pyrightconfig.json`, `pyproject.toml`), which are editor-independent but specific to the project where they are placed - -As a rule of thumb, options that are only relevant when using basedpyright from an editor must be set in language server settings, and options that are relevant even if you're running it [as a command-line tool](https://docs.basedpyright.com/latest/configuration/command-line/) must be set in configuration files. Settings related to inlay hints are examples of the first category, and the [diagnostic category](https://docs.basedpyright.com/latest/configuration/config-files/#diagnostic-categories) settings are examples of the second category. - -Examples of both kinds of configuration are provided below. Refer to the basedpyright documentation on [language server settings](https://docs.basedpyright.com/latest/configuration/language-server-settings/) and [configuration files](https://docs.basedpyright.com/latest/configuration/config-files/) for comprehensive lists of available options. - -##### Language server settings - -Language server settings for basedpyright in Zed can be set in the `lsp` section of your `settings.json`. - -For example, in order to: - -- diagnose all files in the workspace instead of the only open files default -- disable inlay hints on function arguments - -You can use the following configuration: - -```json [settings] -{ - "lsp": { - "basedpyright": { - "settings": { - "basedpyright.analysis": { - "diagnosticMode": "workspace", - "inlayHints": { - "callArgumentNames": false - } - } - } - } - } -} -``` - -##### Configuration files - -basedpyright reads project-specific configuration from the `pyrightconfig.json` configuration file and from the `[tool.basedpyright]` and `[tool.pyright]` sections of `pyproject.toml` manifests. `pyrightconfig.json` overrides `pyproject.toml` if configuration is present in both places. - -Here's an example `pyrightconfig.json` file that configures basedpyright to use the `strict` type-checking mode and not to issue diagnostics for any files in `__pycache__` directories: - -```json [settings] -{ - "typeCheckingMode": "strict", - "ignore": ["**/__pycache__"] -} -``` - -### PyLSP - -[python-lsp-server](https://github.com/python-lsp/python-lsp-server/), more commonly known as PyLSP, by default integrates with a number of external tools (autopep8, mccabe, pycodestyle, yapf) while others are optional and must be explicitly enabled and configured (flake8, pylint). - -See [Python Language Server Configuration](https://github.com/python-lsp/python-lsp-server/blob/develop/CONFIGURATION.md) for more. - -## Virtual Environments - -[Virtual environments](https://docs.python.org/3/library/venv.html) are a useful tool for fixing a Python version and set of dependencies for a specific project, in a way that's isolated from other projects on the same machine. Zed has built-in support for discovering, configuring, and activating virtual environments, based on the language-agnostic concept of a [toolchain](../toolchains.md). - -Note that if you have a global Python installation, it is also counted as a toolchain for Zed's purposes. - -### Create a Virtual Environment - -If your project doesn't have a virtual environment set up already, you can create one as follows: - -```bash -python3 -m venv .venv -``` - -Alternatively, if you're using `uv`, running `uv sync` will create a virtual environment the first time you run it. - -### How Zed Uses Python Toolchains - -Zed uses the selected Python toolchain for your project in the following ways: - -- Built-in language servers will be automatically configured with the path to the toolchain's Python interpreter and, if applicable, virtual environment. This is important so that they can resolve dependencies. (Note that language servers provided by extensions can't be automatically configured like this currently.) -- Python tasks (such as pytest tests) will be run using the toolchain's Python interpreter. -- If the toolchain is a virtual environment, the environment's activation script will be run automatically when you launch a new shell in Zed's integrated terminal, giving you convenient access to the selected Python interpreter and dependency set. -- If a built-in language server is installed in the active virtual environment, that binary will be used instead of Zed's private automatically-installed binary. This also applies to debugpy. - -### Selecting a Toolchain - -For most projects, Zed will automatically select the right Python toolchain. In complex projects with multiple virtual environments, it might be necessary to override this selection. You can use the [toolchain selector](../toolchains.md#selecting-toolchains) to pick a toolchain from the list discovered by Zed, or [specify the path to a toolchain manually](../toolchains.md#adding-toolchains-manually) if it's not on the list. - -## Code Formatting & Linting - -Zed provides the [Ruff](https://docs.astral.sh/ruff/) formatter and linter for Python code. (Specifically, Zed runs Ruff as an LSP server using the `ruff server` subcommand.) Both formatting and linting are enabled by default, including format-on-save. - -### Configuring formatting - -You can disable format-on-save for Python files in your `settings.json`: - -```json [settings] -{ - "languages": { - "Python": { - "format_on_save": "off" - } - } -} -``` - -Alternatively, you can use the `black` command-line tool for Python formatting, while keeping Ruff enabled for linting: - -```json [settings] -{ - "languages": { - "Python": { - "formatter": { - "external": { - "command": "black", - "arguments": ["--stdin-filename", "{buffer_path}", "-"] - } - } - // Or use `"formatter": null` to disable formatting entirely. - } - } -} -``` - -### Configuring Ruff - -Like basedpyright, Ruff reads options from both Zed's language server settings and configuration files (`ruff.toml`) when used in Zed. Unlike basedpyright, _all_ options can be configured in either of these locations, so the choice of where to put your Ruff configuration comes down to whether you want it to be shared between projects but specific to Zed (in which case you should use language server settings), or specific to one project but common to all Ruff invocations (in which case you should use `ruff.toml`). - -Here's an example of using language server settings in Zed's `settings.json` to disable all Ruff lints in Zed (while still using Ruff as a formatter): - -```json [settings] -{ - "lsp": { - "ruff": { - "initialization_options": { - "settings": { - "exclude": ["*"] - } - } - } - } -} -``` - -And here's an example `ruff.toml` with linting and formatting options, adapted from the Ruff documentation: - -```toml -[lint] -# Avoid enforcing line-length violations (`E501`) -ignore = ["E501"] - -[format] -# Use single quotes when formatting. -quote-style = "single" -``` - -For more details, refer to the Ruff documentation about [configuration files](https://docs.astral.sh/ruff/configuration/) and [language server settings](https://docs.astral.sh/ruff/editors/settings/), and the [list of options](https://docs.astral.sh/ruff/settings/). - -## Debugging - -Zed supports Python debugging through the `debugpy` adapter. You can start with no configuration or define custom launch profiles in `.zed/debug.json`. - -### Start Debugging with No Setup - -Zed can automatically detect debuggable Python entry points. Press F4 (or run debugger: start from the Command Palette) to see available options for your current project. -This works for: - -- Python scripts -- Modules -- pytest tests - -Zed uses `debugpy` under the hood, but no manual adapter configuration is required. - -### Define Custom Debug Configurations - -For reusable setups, create a `.zed/debug.json` file in your project root. This gives you more control over how Zed runs and debugs your code. - -- [debugpy configuration documentation](https://github.com/microsoft/debugpy/wiki/Debug-configuration-settings#launchattach-settings) - -#### Debug Active File - -```json [debug] -[ - { - "label": "Python Active File", - "adapter": "Debugpy", - "program": "$ZED_FILE", - "request": "launch" - } -] -``` - -This runs the file currently open in the editor. - -#### Debug a Flask App - -For projects using Flask, you can define a full launch configuration: - -``` -.venv/ -app/ - init.py - main.py - routes.py -templates/ - index.html -static/ - style.css -requirements.txt -``` - -…the following configuration can be used: - -```json [debug] -[ - { - "label": "Python: Flask", - "adapter": "Debugpy", - "request": "launch", - "module": "app", - "cwd": "$ZED_WORKTREE_ROOT", - "env": { - "FLASK_APP": "app", - "FLASK_DEBUG": "1" - }, - "args": [ - "run", - "--reload", // Enables Flask reloader that watches for file changes - "--debugger" // Enables Flask debugger - ], - "autoReload": { - "enable": true - }, - "jinja": true, - "justMyCode": true - } -] -``` - -These can be combined to tailor the experience for web servers, test runners, or custom scripts. - -## Troubleshoot and Maintain a Productive Python Setup - -Zed is designed to minimize configuration overhead, but occasional issues can still arise—especially around environments, language servers, or tooling. Here's how to keep your Python setup working smoothly. - -### Resolve Language Server Startup Issues - -If a language server isn't responding or features like diagnostics or autocomplete aren't available: - -- Check your Zed log (using the {#action zed::OpenLog} action) for errors related to the language server you're trying to use. This is where you're likely to find useful information if the language server failed to start up at all. -- Use the language server logs view to understand the lifecycle of the affected language server. You can access this view using the {#action dev::OpenLanguageServerLogs} action, or by clicking the lightning bolt icon in the status bar and selecting your language server. The most useful pieces of data in this view are: - - "Server Logs", which shows any errors printed by the language server - - "Server Info", which shows details about how the language server was started -- Verify your `settings.json` or `pyrightconfig.json` is syntactically correct. -- Restart Zed to reinitialize language server connections, or try restarting the language server using the {#action editor::RestartLanguageServer} - -If the language server is failing to resolve imports, and you're using a virtual environment, make sure that the right environment is chosen in the selector. You can use "Server Info" view to confirm which virtual environment Zed is sending to the language server—look for the `* Configuration` section at the end. diff --git a/docs/src/languages/r.md b/docs/src/languages/r.md deleted file mode 100644 index a21afb9976..0000000000 --- a/docs/src/languages/r.md +++ /dev/null @@ -1,165 +0,0 @@ -# R - -R support is available via multiple R Zed extensions: - -- [ocsmit/zed-r](https://github.com/ocsmit/zed-r) - - - Tree-sitter: [r-lib/tree-sitter-r](https://github.com/r-lib/tree-sitter-r) - - Language-Server: [REditorSupport/languageserver](https://github.com/REditorSupport/languageserver) - -- [posit-dev/air](https://github.com/posit-dev/air/tree/main/editors/zed) - - Formatter: [posit-dev/air](https://posit-dev.github.io/air/) - -## Installation - -1. [Download and Install R](https://cloud.r-project.org/). -2. Install the R packages `languageserver` and `lintr`: - -```R -install.packages("languageserver") -install.packages("lintr") -``` - -3. Install the [R](https://github.com/ocsmit/zed-r) extension through Zed's extensions manager for basic R language support (syntax highlighting, tree-sitter support) and for [REditorSupport/languageserver](https://github.com/REditorSupport/languageserver) support. - -4. Install the [Air](https://posit-dev.github.io/air/) extension through Zed's extensions manager for R code formatting via Air. - -## Linting - -`REditorSupport/languageserver` bundles support for [r-lib/lintr](https://github.com/r-lib/lintr) as a linter. This can be configured via the use of a `.lintr` inside your project (or in your home directory for global defaults). - -```r -linters: linters_with_defaults( - line_length_linter(120), - commented_code_linter = NULL - ) -exclusions: list( - "inst/doc/creating_linters.R" = 1, - "inst/example/bad.R", - "tests/testthat/exclusions-test" - ) -``` - -Or exclude it from linting anything, - -```r -exclusions: list(".") -``` - -See [Using lintr](https://lintr.r-lib.org/articles/lintr.html) for a complete list of options, - -## Formatting - -### Air - -[Air](https://posit-dev.github.io/air/) provides code formatting for R, including support for format-on-save. The [Air documentation for Zed](https://posit-dev.github.io/air/editor-zed.html) contains the most up-to-date advice for running Air in Zed. - -Ensure that you have installed both the [ocsmit/zed-r](https://github.com/ocsmit/zed-r) extension (for general R language awareness in Zed) and the [Air](https://posit-dev.github.io/air/) extension. - -Enable Air in your `settings.json`: - -```json [settings] -{ - "languages": { - "R": { - "language_servers": ["air"] - } - } -} -``` - -If you use the `"r_language_server"` from `REditorSupport/languageserver`, but would still like to use Air for formatting, use the following configuration: - -```json [settings] -{ - "languages": { - "R": { - "language_servers": ["air", "r_language_server"], - "use_on_type_format": false - } - } -} -``` - -Note that `"air"` must come first in this list, otherwise [r-lib/styler](https://github.com/r-lib/styler) will be invoked via `"r_language_server"`. - -`"r_language_server"` provides on-type-formatting that differs from Air's formatting rules. To avoid this entirely and let Air be fully in charge of formatting your R files, also set `"use_on_type_format": false` as shown above. - -#### Configuring Air - -Air is minimally configurable via an `air.toml` file placed in the root directory of your project: - -```toml -[format] -line-width = 80 -indent-width = 2 -``` - -For more details, refer to the Air documentation about [configuration](https://posit-dev.github.io/air/configuration.html). - -### Styler - -`REditorSupport/languageserver` bundles support for [r-lib/styler](https://github.com/r-lib/styler) as a formatter. See [Customizing Styler](https://cran.r-project.org/web/packages/styler/vignettes/customizing_styler.html) for more information on how to customize its behavior. - - - - diff --git a/docs/src/languages/racket.md b/docs/src/languages/racket.md deleted file mode 100644 index 48312a7c82..0000000000 --- a/docs/src/languages/racket.md +++ /dev/null @@ -1,7 +0,0 @@ -# Racket - -Racket support is available through the [Racket extension](https://github.com/zed-extensions/racket). - -- Tree-sitter: [zed-industries/tree-sitter-racket](https://github.com/zed-industries/tree-sitter-racket) - -The [racket-language-server](https://docs.racket-lang.org/racket-language-server/index.html) is not yet supported in Zed, please see [Issue #15789](https://github.com/zed-industries/zed/issues/15789) for more information. diff --git a/docs/src/languages/rego.md b/docs/src/languages/rego.md deleted file mode 100644 index c52cccea54..0000000000 --- a/docs/src/languages/rego.md +++ /dev/null @@ -1,38 +0,0 @@ -# Rego - -Rego language support in Zed is provided by the community-maintained [Rego extension](https://github.com/StyraInc/zed-rego). - -- Tree-sitter: [FallenAngel97/tree-sitter-rego](https://github.com/FallenAngel97/tree-sitter-rego) -- Language Server: [open-policy-agent/regal](https://github.com/open-policy-agent/regal) - -## Installation - -The extension is largely based on the [Regal](https://docs.styra.com/regal/language-server) language server which should be installed to make use of the extension. Read the [getting started](https://docs.styra.com/regal#getting-started) instructions for more information. - -## Configuration - -The extension's behavior is configured in the `.regal/config.yaml` file. The following is an example configuration which disables the `todo-comment` rule, customizes the `line-length` rule, and ignores test files for the `opa-fmt` rule: - -```yaml -rules: - style: - todo-comment: - # don't report on todo comments - level: ignore - line-length: - # custom rule configuration - max-line-length: 100 - # warn on too long lines, but don't fail - level: warning - opa-fmt: - # not needed as error is the default, but - # being explicit won't hurt - level: error - # files can be ignored for any individual rule - # in this example, test files are ignored - ignore: - files: - - "*_test.rego" -``` - -Read Regal's [configuration documentation](https://docs.styra.com/regal#configuration) for more information. diff --git a/docs/src/languages/roc.md b/docs/src/languages/roc.md deleted file mode 100644 index 44f0576274..0000000000 --- a/docs/src/languages/roc.md +++ /dev/null @@ -1,14 +0,0 @@ -# Roc - -[Roc](https://www.roc-lang.org/) is a fast, friendly, functional language. - -Roc language support in Zed is provided by the community-maintained [Roc extension](https://github.com/h2000/zed-roc). -Report issues to: [https://github.com/h2000/zed-roc/issues](https://github.com/h2000/zed-roc/issues) - -- Tree-sitter: [faldor20/tree-sitter-roc](https://github.com/faldor20/tree-sitter-roc) -- Language Server: [roc-lang/roc/tree/main/crates/language_server](https://github.com/roc-lang/roc/tree/main/crates/language_server) - -## Setup - -1. Follow instructions to [Install Roc](https://www.roc-lang.org/install) from the Roc-Lang website. -2. Ensure `roc` and `roc_language_server` are in your PATH. diff --git a/docs/src/languages/rst.md b/docs/src/languages/rst.md deleted file mode 100644 index 44830ae60e..0000000000 --- a/docs/src/languages/rst.md +++ /dev/null @@ -1,7 +0,0 @@ -# ReStructuredText (rst) - -ReStructuredText language support in Zed is provided by the community-maintained [reST extension](https://github.com/elmarco/zed-rst). -Report issues to: [https://github.com/elmarco/zed-rst/issues](https://github.com/elmarco/zed-rst/issues) - -- Tree-sitter: [stsewd/tree-sitter-rst.git](https://github.com/stsewd/tree-sitter-rst.git) -- Language Server: [swyddfa/esbonio](https://github.com/swyddfa/esbonio) diff --git a/docs/src/languages/ruby.md b/docs/src/languages/ruby.md deleted file mode 100644 index 7e072ac5d3..0000000000 --- a/docs/src/languages/ruby.md +++ /dev/null @@ -1,416 +0,0 @@ -# Ruby - -Ruby support is available through the [Ruby extension](https://github.com/zed-extensions/ruby). - -- Tree-sitters: - - [tree-sitter-ruby](https://github.com/tree-sitter/tree-sitter-ruby) - - [tree-sitter-embedded-template](https://github.com/tree-sitter/tree-sitter-embedded-template) -- Language Servers: - - [ruby-lsp](https://github.com/Shopify/ruby-lsp) - - [solargraph](https://github.com/castwide/solargraph) - - [rubocop](https://github.com/rubocop/rubocop) - - [Herb](https://herb-tools.dev) -- Debug Adapter: [`rdbg`](https://github.com/ruby/debug) - -The Ruby extension also provides support for ERB files. - -## Language Servers - -There are multiple language servers available for Ruby. Zed supports the two following: - -- [solargraph](https://github.com/castwide/solargraph) -- [ruby-lsp](https://github.com/Shopify/ruby-lsp) - -They both have an overlapping feature set of autocomplete, diagnostics, code actions, etc. and it's up to you to decide which one you want to use. Note that you can't use both at the same time. - -In addition to these two language servers, Zed also supports: - -- [rubocop](https://github.com/rubocop/rubocop) which is a static code analyzer and linter for Ruby. Under the hood, it's also used by Zed as a language server, but its functionality is complimentary to that of solargraph and ruby-lsp. -- [sorbet](https://sorbet.org/) which is a static type checker for Ruby with a custom gradual type system. -- [steep](https://github.com/soutaro/steep) which is a static type checker for Ruby that leverages Ruby Signature (RBS). -- [Herb](https://herb-tools.dev) which is a language server for ERB files. - -When configuring a language server, it helps to open the LSP Logs window using the 'dev: Open Language Server Logs' command. You can then choose the corresponding language instance to see any logged information. - -## Configuring a language server - -The [Ruby extension](https://github.com/zed-extensions/ruby) offers both `solargraph` and `ruby-lsp` language server support. - -### Language Server Activation - -For all supported Ruby language servers (`solargraph`, `ruby-lsp`, `rubocop`, `sorbet`, and `steep`), the Ruby extension follows this activation sequence: - -1. If the language server is found in your project's `Gemfile`, it will be used through `bundle exec`. -2. If not found in the `Gemfile`, the Ruby extension will look for the executable in your system `PATH`. -3. If the language server is not found in either location, the Ruby extension will automatically install it as a global gem (note: this will not install to your current Ruby gemset). - -You can skip step 1 and force using the system executable by setting `use_bundler` to `false` in your settings: - -```json [settings] -{ - "lsp": { - "": { - "settings": { - "use_bundler": false - } - } - } -} -``` - -### Using `solargraph` - -`solargraph` is enabled by default in the Ruby extension. - -### Using `ruby-lsp` - -To switch to `ruby-lsp`, add the following to your `settings.json`: - -```json [settings] -{ - "languages": { - "Ruby": { - "language_servers": ["ruby-lsp", "!solargraph", "!rubocop", "..."] - }, - // Enable herb and ruby-lsp for *.html.erb files - "HTML+ERB": { - "language_servers": ["herb", "ruby-lsp", "..."] - }, - // Enable ruby-lsp for *.js.erb files - "JS+ERB": { - "language_servers": ["ruby-lsp", "..."] - }, - // Enable ruby-lsp for *.yaml.erb files - "YAML+ERB": { - "language_servers": ["ruby-lsp", "..."] - } - } -} -``` - -That disables `solargraph` and `rubocop` and enables `ruby-lsp`. - -### Using `rubocop` - -The Ruby extension also provides support for `rubocop` language server for offense detection and autocorrection. - -To enable it, add the following to your `settings.json`: - -```json [settings] -{ - "languages": { - "Ruby": { - "language_servers": ["ruby-lsp", "rubocop", "!solargraph", "..."] - } - } -} -``` - -Or, conversely, you can disable `ruby-lsp` and enable `solargraph` and `rubocop` by adding the following to your `settings.json`: - -```json [settings] -{ - "languages": { - "Ruby": { - "language_servers": ["solargraph", "rubocop", "!ruby-lsp", "..."] - } - } -} -``` - -## Setting up `solargraph` - -Solargraph has formatting and diagnostics disabled by default. We can tell Zed to enable them by adding the following to your `settings.json`: - -```json [settings] -{ - "lsp": { - "solargraph": { - "initialization_options": { - "diagnostics": true, - "formatting": true - } - } - } -} -``` - -### Configuration - -Solargraph reads its configuration from a file called `.solargraph.yml` in the root of your project. For more information about this file, see the [Solargraph configuration documentation](https://solargraph.org/guides/configuration). - -## Setting up `ruby-lsp` - -You can pass Ruby LSP configuration to `initialization_options`, e.g. - -```json [settings] -{ - "languages": { - "Ruby": { - "language_servers": ["ruby-lsp", "!solargraph", "..."] - } - }, - "lsp": { - "ruby-lsp": { - "initialization_options": { - "enabledFeatures": { - // "someFeature": false - } - } - } - } -} -``` - -LSP `settings` and `initialization_options` can also be project-specific. For example to use [standardrb/standard](https://github.com/standardrb/standard) as a formatter and linter for a particular project, add this to a `.zed/settings.json` inside your project repo: - -```json [settings] -{ - "lsp": { - "ruby-lsp": { - "initialization_options": { - "formatter": "standard", - "linters": ["standard"] - } - } - } -} -``` - -## Setting up `rubocop` LSP - -Rubocop has unsafe autocorrection disabled by default. We can tell Zed to enable it by adding the following to your `settings.json`: - -```json [settings] -{ - "languages": { - "Ruby": { - // Use ruby-lsp as the primary language server and rubocop as the secondary. - "language_servers": ["ruby-lsp", "rubocop", "!solargraph", "..."] - } - }, - "lsp": { - "rubocop": { - "initialization_options": { - "safeAutocorrect": false - } - }, - "ruby-lsp": { - "initialization_options": { - "enabledFeatures": { - "diagnostics": false - } - } - } - } -} -``` - -## Setting up Sorbet - -[Sorbet](https://sorbet.org/) is a popular static type checker for Ruby that includes a language server. - -To enable Sorbet, add `\"sorbet\"` to the `language_servers` list for Ruby in your `settings.json`. You may want to disable other language servers if Sorbet is intended to be your primary LSP, or if you plan to use it alongside another LSP for specific features like type checking. - -```json [settings] -{ - "languages": { - "Ruby": { - "language_servers": [ - "ruby-lsp", - "sorbet", - "!rubocop", - "!solargraph", - "..." - ] - } - } -} -``` - -For all aspects of installing Sorbet, setting it up in your project, and configuring its behavior, please refer to the [official Sorbet documentation](https://sorbet.org/docs/overview). - -## Setting up Steep - -[Steep](https://github.com/soutaro/steep) is a static type checker for Ruby that uses RBS files to define types. - -To enable Steep, add `\"steep\"` to the `language_servers` list for Ruby in your `settings.json`. You may need to adjust the order or disable other LSPs depending on your desired setup. - -```json [settings] -{ - "languages": { - "Ruby": { - "language_servers": [ - "ruby-lsp", - "steep", - "!solargraph", - "!rubocop", - "..." - ] - } - } -} -``` - -## Setting up Herb - -`Herb` is enabled by default for the `HTML+ERB` language. - -## Using the Tailwind CSS Language Server with Ruby - -It's possible to use the [Tailwind CSS Language Server](https://github.com/tailwindlabs/tailwindcss-intellisense/tree/HEAD/packages/tailwindcss-language-server#readme) in Ruby and ERB files. - -In order to do that, you need to configure the language server so that it knows about where to look for CSS classes in Ruby/ERB files by adding the following to your `settings.json`: - -```json [settings] -{ - "languages": { - "Ruby": { - "language_servers": ["tailwindcss-language-server", "..."] - } - }, - "lsp": { - "tailwindcss-language-server": { - "settings": { - "experimental": { - "classRegex": ["\\bclass:\\s*['\"]([^'\"]*)['\"]"] - } - } - } - } -} -``` - -With these settings you will get completions for Tailwind CSS classes in HTML attributes inside ERB files and inside Ruby/ERB strings that are coming after a `class:` key. Examples: - -```rb -# Ruby file: -def method - div(class: "pl-2 ") do - p(class: "mt-2 ") { "Hello World" } - end -end - -# ERB file: -<%= link_to "Hello", "/hello", class: "pl-2 " %> -Hello -``` - -## Running tests - -To run tests in your Ruby project, you can set up custom tasks in your local `.zed/tasks.json` configuration file. These tasks can be defined to work with different test frameworks like Minitest, RSpec, quickdraw, and tldr. Below are some examples of how to set up these tasks to run your tests from within your editor. - -### Minitest with Rails - -```json [tasks] -[ - { - "label": "test $ZED_RELATIVE_FILE -n /$ZED_CUSTOM_RUBY_TEST_NAME/", - "command": "bin/rails", - "args": [ - "test", - "$ZED_RELATIVE_FILE", - "-n", - "\"$ZED_CUSTOM_RUBY_TEST_NAME\"" - ], - "cwd": "$ZED_WORKTREE_ROOT", - "tags": ["ruby-test"] - } -] -``` - -### Minitest - -Plain minitest does not support running tests by line number, only by name, so we need to use `$ZED_CUSTOM_RUBY_TEST_NAME` instead: - -```json [tasks] -[ - { - "label": "-Itest $ZED_RELATIVE_FILE -n /$ZED_CUSTOM_RUBY_TEST_NAME/", - "command": "bundle", - "args": [ - "exec", - "ruby", - "-Itest", - "$ZED_RELATIVE_FILE", - "-n", - "\"$ZED_CUSTOM_RUBY_TEST_NAME\"" - ], - "cwd": "$ZED_WORKTREE_ROOT", - "tags": ["ruby-test"] - } -] -``` - -### RSpec - -```json [tasks] -[ - { - "label": "test $ZED_RELATIVE_FILE:$ZED_ROW", - "command": "bundle", - "args": ["exec", "rspec", "\"$ZED_RELATIVE_FILE:$ZED_ROW\""], - "cwd": "$ZED_WORKTREE_ROOT", - "tags": ["ruby-test"] - } -] -``` - -Similar task syntax can be used for other test frameworks such as `quickdraw` or `tldr`. - -## Debugging - -The Ruby extension provides a debug adapter for debugging Ruby code. Zed's name for the adapter (in the UI and `debug.json`) is `rdbg`, and under the hood, it uses the [`debug`](https://github.com/ruby/debug) gem. The extension uses the [same activation logic](#language-server-activation) as the language servers. - -### Examples - -#### Debug a Ruby script - -```json [debug] -[ - { - "label": "Debug current file", - "adapter": "rdbg", - "request": "launch", - "script": "$ZED_FILE", - "cwd": "$ZED_WORKTREE_ROOT" - } -] -``` - -#### Debug Rails server - -```json [debug] -[ - { - "label": "Debug Rails server", - "adapter": "rdbg", - "request": "launch", - "command": "./bin/rails", - "args": ["server"], - "cwd": "$ZED_WORKTREE_ROOT", - "env": { - "RUBY_DEBUG_OPEN": "true" - } - } -] -``` - -## Formatters - -### `erb-formatter` - -To format ERB templates, you can use the `erb-formatter` formatter. This formatter uses the [`erb-formatter`](https://rubygems.org/gems/erb-formatter) gem to format ERB templates. - -```json [settings] -{ - "HTML+ERB": { - "formatter": { - "external": { - "command": "erb-formatter", - "arguments": ["--stdin-filename", "{buffer_path}"] - } - } - } -} -``` diff --git a/docs/src/languages/rust.md b/docs/src/languages/rust.md deleted file mode 100644 index d696cfe411..0000000000 --- a/docs/src/languages/rust.md +++ /dev/null @@ -1,340 +0,0 @@ -# Rust - -Rust support is available natively in Zed. - -- Tree-sitter: [tree-sitter/tree-sitter-rust](https://github.com/tree-sitter/tree-sitter-rust) -- Language Server: [rust-lang/rust-analyzer](https://github.com/rust-lang/rust-analyzer) -- Debug Adapter: [CodeLLDB](https://github.com/vadimcn/codelldb) (primary), [GDB](https://sourceware.org/gdb/) (secondary, not available on Apple silicon) - - - -## Inlay Hints - -The following configuration can be used to change the inlay hint settings for `rust-analyzer` in Rust: - -```json [settings] -{ - "lsp": { - "rust-analyzer": { - "initialization_options": { - "inlayHints": { - "maxLength": null, - "lifetimeElisionHints": { - "enable": "skip_trivial", - "useParameterNames": true - }, - "closureReturnTypeHints": { - "enable": "always" - } - } - } - } - } -} -``` - -See [Inlay Hints](https://rust-analyzer.github.io/book/features.html#inlay-hints) in the Rust Analyzer Manual for more information. - -## Target directory - -The `rust-analyzer` target directory can be set in `initialization_options`: - -```json [settings] -{ - "lsp": { - "rust-analyzer": { - "initialization_options": { - "rust": { - "analyzerTargetDir": true - } - } - } - } -} -``` - -A `true` setting will set the target directory to `target/rust-analyzer`. You can set a custom directory with a string like `"target/analyzer"` instead of `true`. - -## Binary - -You can configure which `rust-analyzer` binary Zed should use. - -By default, Zed will try to find a `rust-analyzer` in your `$PATH` and try to use that. If that binary successfully executes `rust-analyzer --help`, it's used. Otherwise, Zed will fall back to installing its own stable `rust-analyzer` version and use that. - -If you want to install pre-release `rust-analyzer` version instead you can instruct Zed to do so by setting `pre_release` to `true` in your `settings.json`: - -```json [settings] -{ - "lsp": { - "rust-analyzer": { - "fetch": { - "pre_release": true - } - } - } -} -``` - -If you want to disable Zed looking for a `rust-analyzer` binary, you can set `ignore_system_version` to `true` in your `settings.json`: - -```json [settings] -{ - "lsp": { - "rust-analyzer": { - "binary": { - "ignore_system_version": true - } - } - } -} -``` - -If you want to use a binary in a custom location, you can specify a `path` and optional `arguments`: - -```json [settings] -{ - "lsp": { - "rust-analyzer": { - "binary": { - "path": "/Users/example/bin/rust-analyzer", - "arguments": [] - } - } - } -} -``` - -This `"path"` has to be an absolute path. - -## Alternate Targets - -If you want rust-analyzer to provide diagnostics for a target other than your current platform (e.g. for windows when running on macOS) you can use the following Zed lsp settings: - -```json [settings] -{ - "lsp": { - "rust-analyzer": { - "initialization_options": { - "cargo": { - "target": "x86_64-pc-windows-msvc" - } - } - } - } -} -``` - -If you are using `rustup`, you can find a list of available target triples (`aarch64-apple-darwin`, `x86_64-unknown-linux-gnu`, etc) by running: - -```sh -rustup target list --installed -``` - -## LSP tasks - -Zed provides tasks using tree-sitter, but rust-analyzer has an LSP extension method for querying file-related tasks via LSP. -This is enabled by default and can be configured as - -```json [settings] -"lsp": { - "rust-analyzer": { - "enable_lsp_tasks": true, - } -} -``` - -## Manual Cargo Diagnostics fetch - -By default, rust-analyzer has `checkOnSave: true` enabled, which causes every buffer save to trigger a `cargo check --workspace --all-targets` command. -If disabled with `checkOnSave: false` (see the example of the server configuration json above), it's still possible to fetch the diagnostics manually, with the `editor: run/clear/cancel flycheck` commands in Rust files to refresh cargo diagnostics; the project diagnostics editor will also refresh cargo diagnostics with `editor: run flycheck` command when the setting is enabled. - -## More server configuration - - - -Rust-analyzer [manual](https://rust-analyzer.github.io/book/) describes various features and configuration options for rust-analyzer language server. -Rust-analyzer in Zed runs with the default parameters. - -### Large projects and performance - -One of the main caveats that might cause extensive resource usage on large projects, is the combination of the following features: - -``` -rust-analyzer.checkOnSave (default: true) - Run the check command for diagnostics on save. -``` - -``` -rust-analyzer.check.workspace (default: true) - Whether --workspace should be passed to cargo check. If false, -p will be passed instead. -``` - -``` -rust-analyzer.cargo.allTargets (default: true) - Pass --all-targets to cargo invocation -``` - -Which would mean that every time Zed saves, a `cargo check --workspace --all-targets` command is run, checking the entire project (workspace), lib, doc, test, bin, bench and [other targets](https://doc.rust-lang.org/cargo/reference/cargo-targets.html). - -While that works fine on small projects, it does not scale well. - -The alternatives would be to use [tasks](../tasks.md), as Zed already provides a `cargo check --workspace --all-targets` task and the ability to cmd/ctrl-click on the terminal output to navigate to the error, and limit or turn off the check on save feature entirely. - -Check on save feature is responsible for returning part of the diagnostics based on cargo check output, so turning it off will limit rust-analyzer with its own [diagnostics](https://rust-analyzer.github.io/book/diagnostics.html). - -Consider more `rust-analyzer.cargo.` and `rust-analyzer.check.` and `rust-analyzer.diagnostics.` settings from the manual for more fine-grained configuration. -Here's a snippet for Zed settings.json (the language server will restart automatically after the `lsp.rust-analyzer` section is edited and saved): - -```json [settings] -{ - "lsp": { - "rust-analyzer": { - "initialization_options": { - // get more cargo-less diagnostics from rust-analyzer, - // which might include false-positives (those can be turned off by their names) - "diagnostics": { - "experimental": { - "enable": true - } - }, - // To disable the checking entirely - // (ignores all cargo and check settings below) - "checkOnSave": false, - // To check the `lib` target only. - "cargo": { - "allTargets": false - }, - // Use `-p` instead of `--workspace` for cargo check - "check": { - "workspace": false - } - } - } - } -} -``` - -### Multi-project workspaces - -If you want rust-analyzer to analyze multiple Rust projects in the same folder that are not listed in `[members]` in the Cargo workspace, -you can list them in `linkedProjects` in the local project settings: - -```json [settings] -{ - "lsp": { - "rust-analyzer": { - "initialization_options": { - "linkedProjects": ["./path/to/a/Cargo.toml", "./path/to/b/Cargo.toml"] - } - } - } -} -``` - -### Snippets - -There's a way to get custom completion items from rust-analyzer, that will transform the code according to the snippet body: - -```json [settings] -{ - "lsp": { - "rust-analyzer": { - "initialization_options": { - "completion": { - "snippets": { - "custom": { - "Arc::new": { - "postfix": "arc", - "body": ["Arc::new(${receiver})"], - "requires": "std::sync::Arc", - "scope": "expr" - }, - "Some": { - "postfix": "some", - "body": ["Some(${receiver})"], - "scope": "expr" - }, - "Ok": { - "postfix": "ok", - "body": ["Ok(${receiver})"], - "scope": "expr" - }, - "Rc::new": { - "postfix": "rc", - "body": ["Rc::new(${receiver})"], - "requires": "std::rc::Rc", - "scope": "expr" - }, - "Box::pin": { - "postfix": "boxpin", - "body": ["Box::pin(${receiver})"], - "requires": "std::boxed::Box", - "scope": "expr" - }, - "vec!": { - "postfix": "vec", - "body": ["vec![${receiver}]"], - "description": "vec![]", - "scope": "expr" - } - } - } - } - } - } - } -} -``` - -## Debugging - -Zed supports debugging Rust binaries and tests out of the box with `CodeLLDB` and `GDB`. Run {#action debugger::Start} ({#kb debugger::Start}) to launch one of these preconfigured debug tasks. - -For more control, you can add debug configurations to `.zed/debug.json`. See the examples below. - -- [CodeLLDB configuration documentation](https://github.com/vadimcn/codelldb/blob/master/MANUAL.md#starting-a-new-debug-session) -- [GDB configuration documentation](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Debugger-Adapter-Protocol.html) - -### Build binary then debug - -```json [debug] -[ - { - "label": "Build & Debug native binary", - "build": { - "command": "cargo", - "args": ["build"] - }, - "program": "$ZED_WORKTREE_ROOT/target/debug/binary", - // sourceLanguages is required for CodeLLDB (not GDB) when using Rust - "sourceLanguages": ["rust"], - "request": "launch", - "adapter": "CodeLLDB" - } -] -``` - -### Automatically locate a debug target based on build command - -When you use `cargo build` or `cargo test` as the build command, Zed can infer the path to the output binary. - -```json [debug] -[ - { - "label": "Build & Debug native binary", - "adapter": "CodeLLDB", - "build": { - "command": "cargo", - "args": ["build"] - }, - // sourceLanguages is required for CodeLLDB (not GDB) when using Rust - "sourceLanguages": ["rust"] - } -] -``` diff --git a/docs/src/languages/scala.md b/docs/src/languages/scala.md deleted file mode 100644 index fc169bf229..0000000000 --- a/docs/src/languages/scala.md +++ /dev/null @@ -1,29 +0,0 @@ -# Scala - -Scala language support in Zed is provided by the community-maintained [Scala extension](https://github.com/scalameta/metals-zed). -Report issues to: [https://github.com/scalameta/metals-zed/issues](https://github.com/scalameta/metals-zed/issues) - -- Tree-sitter: [tree-sitter/tree-sitter-scala](https://github.com/tree-sitter/tree-sitter-scala) -- Language Server: [scalameta/metals](https://github.com/scalameta/metals) - -## Setup - -- Install Scala with `cs setup` (Coursier): https://www.scala-lang.org/download/ - - `brew install coursier/formulas/coursier && cs setup` -- REPL (Almond) Setup Instructions https://almond.sh/docs/quick-start-install - - `brew install --cask temurin` (Eclipse foundation official OpenJDK binaries) - - `brew install coursier/formulas/coursier && cs setup` - - `coursier launch --use-bootstrap almond -- --install` - -## Configuration - -Behavior of the Metals language server can be controlled with: - -- `.scalafix.conf` file - See [Scalafix Configuration](https://scalacenter.github.io/scalafix/docs/users/configuration.html) -- `.scalafmt.conf` file - See [Scalafmt Configuration](https://scalameta.org/scalafmt/docs/configuration.html) - -You can place these files in the root of your project or specifying their location in the Metals configuration. See [Metals User Configuration](https://scalameta.org/metals/docs/editors/user-configuration) for more. - - diff --git a/docs/src/languages/scheme.md b/docs/src/languages/scheme.md deleted file mode 100644 index 59e7ae1ddf..0000000000 --- a/docs/src/languages/scheme.md +++ /dev/null @@ -1,5 +0,0 @@ -# Scheme - -Scheme support is available through the [Scheme extension](https://github.com/zed-extensions/scheme). - -- Tree-sitter: [6cdh/tree-sitter-scheme](https://github.com/6cdh/tree-sitter-scheme) diff --git a/docs/src/languages/sh.md b/docs/src/languages/sh.md deleted file mode 100644 index cf88c89bfa..0000000000 --- a/docs/src/languages/sh.md +++ /dev/null @@ -1,62 +0,0 @@ -# Shell Scripts - -Shell Scripts (bash, zsh, dash, sh) are supported natively by Zed. - -- Tree-sitter: [tree-sitter/tree-sitter-bash](https://github.com/tree-sitter/tree-sitter-bash) - -## Settings - -You can configure various settings for Shell Scripts in your Zed User Settings (`~/.config/zed/settings.json`) or Zed Project Settings (`.zed/settings.json`): - -```json [settings] - "languages": { - "Shell Script": { - "tab_size": 2, - "hard_tabs": false - } - } -``` - -### Formatting - -Zed supports auto-formatting Shell Scripts using external tools like [`shfmt`](https://github.com/mvdan/sh). - -1. Install `shfmt`: - -```sh -brew install shfmt # macos (homebrew) -sudo apt-get install shfmt # debian/ubuntu -dnf install shfmt # fedora -yum install shfmt # redhat -pacman -Sy shfmt # archlinux -choco install shfmt # windows (chocolatey) -``` - -2. Ensure `shfmt` is available in your path and check the version: - -```sh -which shfmt -shfmt --version -``` - -3. Configure Zed to automatically format Shell Scripts with `shfmt` on save: - -```json [settings] - "languages": { - "Shell Script": { - "format_on_save": "on", - "formatter": { - "external": { - "command": "shfmt", - // Change `--indent 2` to match your preferred tab_size - "arguments": ["--filename", "{buffer_path}", "--indent", "2"] - } - } - } - } -``` - -## See also: - -- [Zed Docs: Language Support: Bash](./bash.md) -- [Zed Docs: Language Support: Fish](./fish.md) diff --git a/docs/src/languages/sql.md b/docs/src/languages/sql.md deleted file mode 100644 index fd257c9ab0..0000000000 --- a/docs/src/languages/sql.md +++ /dev/null @@ -1,68 +0,0 @@ -# SQL - -SQL files are handled by the [SQL Extension](https://github.com/zed-extensions/sql). - -- Tree-sitter: [nervenes/tree-sitter-sql](https://github.com/nervenes/tree-sitter-sql) - -### Formatting - -Zed supports auto-formatting SQL using external tools like [`sql-formatter`](https://github.com/sql-formatter-org/sql-formatter). - -1. Install `sql-formatter`: - -```sh -npm install -g sql-formatter -``` - -2. Ensure `sql-formatter` is available in your path and check the version: - -```sh -which sql-formatter -sql-formatter --version -``` - -3. Configure Zed to automatically format SQL with `sql-formatter`: - -```json [settings] - "languages": { - "SQL": { - "formatter": { - "external": { - "command": "sql-formatter", - "arguments": ["--language", "mysql"] - } - } - } - }, -``` - -Substitute your preferred [SQL Dialect] for `mysql` above (`duckdb`, `hive`, `mariadb`, `postgresql`, `redshift`, `snowflake`, `sqlite`, `spark`, etc). - -You can add this to Zed project settings (`.zed/settings.json`) or via your Zed user settings (`~/.config/zed/settings.json`). - -### Advanced Formatting - -Sql-formatter also allows more precise control by providing [sql-formatter configuration options](https://github.com/sql-formatter-org/sql-formatter#configuration-options). To provide these, create a `.sql-formatter.json` file in your project: - -```json [settings] -{ - "language": "postgresql", - "tabWidth": 2, - "keywordCase": "upper", - "linesBetweenQueries": 2 -} -``` - -When using a `.sql-formatter.json` file you can use a more simplified set of Zed settings since the language need not be specified inline: - -```json [settings] - "languages": { - "SQL": { - "formatter": { - "external": { - "command": "sql-formatter" - } - } - } - }, -``` diff --git a/docs/src/languages/svelte.md b/docs/src/languages/svelte.md deleted file mode 100644 index 139195987b..0000000000 --- a/docs/src/languages/svelte.md +++ /dev/null @@ -1,73 +0,0 @@ -# Svelte - -Svelte support is available through the [Svelte extension](https://github.com/zed-extensions/svelte). - -- Tree-sitter: [tree-sitter-grammars/tree-sitter-svelte](https://github.com/tree-sitter-grammars/tree-sitter-svelte) -- Language Server: [sveltejs/language-tools](https://github.com/sveltejs/language-tools) - -## Extra theme styling configuration - -You can modify how certain styles, such as directives and modifiers, appear in attributes: - -```json [settings] -"syntax": { - // Styling for directives (e.g., `class:foo` or `on:click`) (the `on` or `class` part of the attribute). - "attribute.function": { - "color": "#ff0000" - }, - // Styling for modifiers at the end of attributes, e.g. `on:` - "attribute.special": { - "color": "#00ff00" - } -} -``` - -## Inlay Hints - -When inlay hints is enabled in Zed, to make the language server send them back, Zed sets the following initialization options: - -```json [settings] -"inlayHints": { - "parameterNames": { - "enabled": "all", - "suppressWhenArgumentMatchesName": false - }, - "parameterTypes": { - "enabled": true - }, - "variableTypes": { - "enabled": true, - "suppressWhenTypeMatchesName": false - }, - "propertyDeclarationTypes": { - "enabled": true - }, - "functionLikeReturnTypes": { - "enabled": true - }, - "enumMemberValues": { - "enabled": true - } -} -``` - -To override these settings, use the following: - -```json [settings] -"lsp": { - "svelte-language-server": { - "initialization_options": { - "configuration": { - "typescript": { - // ...... - }, - "javascript": { - // ...... - } - } - } - } -} -``` - -See [the TypeScript language server `package.json`](https://github.com/microsoft/vscode/blob/main/extensions/typescript-language-features/package.json) for more information. diff --git a/docs/src/languages/swift.md b/docs/src/languages/swift.md deleted file mode 100644 index 1492942fe8..0000000000 --- a/docs/src/languages/swift.md +++ /dev/null @@ -1,40 +0,0 @@ -# Swift - -Swift language support in Zed is provided by the community-maintained [Swift extension](https://github.com/zed-extensions/swift). -Report issues to: [https://github.com/zed-extensions/swift/issues](https://github.com/zed-extensions/swift/issues) - -- Tree-sitter: [alex-pinkus/tree-sitter-swift](https://github.com/alex-pinkus/tree-sitter-swift) -- Language Server: [swiftlang/sourcekit-lsp](https://github.com/swiftlang/sourcekit-lsp) -- Debug Adapter: [`lldb-dap`](https://github.com/swiftlang/llvm-project/blob/next/lldb/tools/lldb-dap/README.md) - -## Language Server Configuration - -You can modify the behavior of SourceKit LSP by creating a `.sourcekit-lsp/config.json` under your home directory or in your project root. See [SourceKit-LSP configuration file](https://github.com/swiftlang/sourcekit-lsp/blob/main/Documentation/Configuration%20File.md) for complete documentation. - -## Debugging - -The Swift extension provides a debug adapter for debugging Swift code. -Zed's name for the adapter (in the UI and `debug.json`) is `Swift`, and under the hood it uses [`lldb-dap`](https://github.com/swiftlang/llvm-project/blob/next/lldb/tools/lldb-dap/README.md), as provided by the Swift toolchain. -The extension tries to find an `lldb-dap` binary using `swiftly`, using `xcrun`, and by searching `$PATH`, in that order of preference. -The extension doesn't attempt to download `lldb-dap` if it's not found. - -- [lldb-dap configuration documentation](https://github.com/llvm/llvm-project/blob/main/lldb/tools/lldb-dap/README.md#configuration-settings-reference) - -### Examples - -#### Build and debug a Swift binary - -```json [debug] -[ - { - "label": "Debug Swift", - "build": { - "command": "swift", - "args": ["build"] - }, - "program": "$ZED_WORKTREE_ROOT/swift-app/.build/arm64-apple-macosx/debug/swift-app", - "request": "launch", - "adapter": "Swift" - } -] -``` diff --git a/docs/src/languages/tailwindcss.md b/docs/src/languages/tailwindcss.md deleted file mode 100644 index be9c9437d1..0000000000 --- a/docs/src/languages/tailwindcss.md +++ /dev/null @@ -1,49 +0,0 @@ -# Tailwind CSS - -Zed has built-in support for Tailwind CSS autocomplete, linting, and hover previews. - -- Language Server: [tailwindlabs/tailwindcss-intellisense](https://github.com/tailwindlabs/tailwindcss-intellisense) - -## Configuration - -To configure the Tailwind CSS language server, refer [to the extension settings](https://github.com/tailwindlabs/tailwindcss-intellisense?tab=readme-ov-file#extension-settings) and add them to the `lsp` section of your `settings.json`: - -```json [settings] -{ - "lsp": { - "tailwindcss-language-server": { - "settings": { - "classFunctions": ["cva", "cx"], - "experimental": { - "classRegex": ["[cls|className]\\s\\:\\=\\s\"([^\"]*)"] - } - } - } - } -} -``` - -Languages which can be used with Tailwind CSS in Zed: - -- [Astro](./astro.md) -- [CSS](./css.md) -- [ERB](./ruby.md) -- [Gleam](./gleam.md) -- [HEEx](./elixir.md#heex) -- [HTML](./html.md) -- [TypeScript](./typescript.md) -- [JavaScript](./javascript.md) -- [PHP](./php.md) -- [Svelte](./svelte.md) -- [Vue](./vue.md) - -### Prettier Plugin - -Zed supports Prettier out of the box, which means that if you have the [Tailwind CSS Prettier plugin](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) installed, adding it to your Prettier configuration will make it work automatically: - -```json [settings] -// .prettierrc -{ - "plugins": ["prettier-plugin-tailwindcss"] -} -``` diff --git a/docs/src/languages/terraform.md b/docs/src/languages/terraform.md deleted file mode 100644 index c1ff03a83a..0000000000 --- a/docs/src/languages/terraform.md +++ /dev/null @@ -1,30 +0,0 @@ -# Terraform - -Terraform support is available through the [Terraform extension](https://github.com/zed-extensions/terraform). - -- Tree-sitter: [MichaHoffmann/tree-sitter-hcl](https://github.com/MichaHoffmann/tree-sitter-hcl) -- Language Server: [hashicorp/terraform-ls](https://github.com/hashicorp/terraform-ls) - -## Configuration - - - -The Terraform language server can be configured in your `settings.json`, e.g.: - -```json [settings] -{ - "lsp": { - "terraform-ls": { - "initialization_options": { - "experimentalFeatures": { - "prefillRequiredFields": true - } - } - } - } -} -``` - -See the [full list of server settings here](https://github.com/hashicorp/terraform-ls/blob/main/docs/SETTINGS.md). diff --git a/docs/src/languages/toml.md b/docs/src/languages/toml.md deleted file mode 100644 index 46b93b67eb..0000000000 --- a/docs/src/languages/toml.md +++ /dev/null @@ -1,7 +0,0 @@ -# TOML - -TOML support is available through the [TOML extension](https://zed.dev/extensions/toml). - -- Tree-sitter: [tree-sitter/tree-sitter-toml](https://github.com/tree-sitter/tree-sitter-toml) - -A TOML language server is available in the [Tombi extension](https://zed.dev/extensions/tombi). diff --git a/docs/src/languages/typescript.md b/docs/src/languages/typescript.md deleted file mode 100644 index a6ec5b71ec..0000000000 --- a/docs/src/languages/typescript.md +++ /dev/null @@ -1,209 +0,0 @@ -# TypeScript - -TypeScript and TSX support are available natively in Zed. - -- Tree-sitter: [tree-sitter/tree-sitter-typescript](https://github.com/tree-sitter/tree-sitter-typescript) -- Language Server: [yioneko/vtsls](https://github.com/yioneko/vtsls) -- Alternate Language Server: [typescript-language-server/typescript-language-server](https://github.com/typescript-language-server/typescript-language-server) -- Debug Adapter: [vscode-js-debug](https://github.com/microsoft/vscode-js-debug) - - - -## Language servers - -By default Zed uses [vtsls](https://github.com/yioneko/vtsls) for TypeScript, TSX, and JavaScript files. -You can configure the use of [typescript-language-server](https://github.com/typescript-language-server/typescript-language-server) per language in your settings file: - -```json [settings] -{ - "languages": { - "TypeScript": { - "language_servers": ["typescript-language-server", "!vtsls", "..."] - }, - "TSX": { - "language_servers": ["typescript-language-server", "!vtsls", "..."] - }, - "JavaScript": { - "language_servers": ["typescript-language-server", "!vtsls", "..."] - } - } -} -``` - -Prettier will also be used for TypeScript files by default. To disable this: - -```json [settings] -{ - "languages": { - "TypeScript": { - "prettier": { "allowed": false } - } - //... - } -} -``` - -## Large projects - -`vtsls` may run out of memory on very large projects. We default the limit to 8092 (8 GiB) vs. the default of 3072 but this may not be sufficient for you: - -```json [settings] -{ - "lsp": { - "vtsls": { - "settings": { - // For TypeScript: - "typescript": { "tsserver": { "maxTsServerMemory": 16184 } }, - // For JavaScript: - "javascript": { "tsserver": { "maxTsServerMemory": 16184 } } - } - } - } -} -``` - -## Inlay Hints - -Zed sets the following initialization options to make the language server send back inlay hints (that is, when Zed has inlay hints enabled in the settings). - -You can override these settings in your Zed `settings.json` when using `typescript-language-server`: - -```json [settings] -{ - "lsp": { - "typescript-language-server": { - "initialization_options": { - "preferences": { - "includeInlayParameterNameHints": "all", - "includeInlayParameterNameHintsWhenArgumentMatchesName": true, - "includeInlayFunctionParameterTypeHints": true, - "includeInlayVariableTypeHints": true, - "includeInlayVariableTypeHintsWhenTypeMatchesName": true, - "includeInlayPropertyDeclarationTypeHints": true, - "includeInlayFunctionLikeReturnTypeHints": true, - "includeInlayEnumMemberValueHints": true - } - } - } - } -} -``` - -See [typescript-language-server inlayhints documentation](https://github.com/typescript-language-server/typescript-language-server?tab=readme-ov-file#inlay-hints-textdocumentinlayhint) for more information. - -When using `vtsls`: - -```json [settings] -{ - "lsp": { - "vtsls": { - "settings": { - // For JavaScript: - "javascript": { - "inlayHints": { - "parameterNames": { - "enabled": "all", - "suppressWhenArgumentMatchesName": false - }, - "parameterTypes": { - "enabled": true - }, - "variableTypes": { - "enabled": true, - "suppressWhenTypeMatchesName": true - }, - "propertyDeclarationTypes": { - "enabled": true - }, - "functionLikeReturnTypes": { - "enabled": true - }, - "enumMemberValues": { - "enabled": true - } - } - }, - // For TypeScript: - "typescript": { - "inlayHints": { - "parameterNames": { - "enabled": "all", - "suppressWhenArgumentMatchesName": false - }, - "parameterTypes": { - "enabled": true - }, - "variableTypes": { - "enabled": true, - "suppressWhenTypeMatchesName": true - }, - "propertyDeclarationTypes": { - "enabled": true - }, - "functionLikeReturnTypes": { - "enabled": true - }, - "enumMemberValues": { - "enabled": true - } - } - } - } - } - } -} -``` - -## Debugging - -Zed supports debugging TypeScript code out of the box with `vscode-js-debug`. -The following can be debugged without writing additional configuration: - -- Tasks from `package.json` -- Tests written using several popular frameworks (Jest, Mocha, Vitest, Jasmine, Bun, Node) - -Run {#action debugger::Start} ({#kb debugger::Start}) to see a contextual list of these predefined debug tasks. - -> **Note:** Bun test is automatically detected when `@types/bun` is present in `package.json`. -> -> **Note:** Node test is automatically detected when `@types/node` is present in `package.json` (requires Node.js 20+). - -As for all languages, configurations from `.vscode/launch.json` are also available for debugging in Zed. - -If your use-case isn't covered by any of these, you can take full control by adding debug configurations to `.zed/debug.json`. See below for example configurations. - -### Configuring JavaScript debug tasks - -JavaScript debugging is more complicated than other languages because there are two different environments: Node.js and the browser. `vscode-js-debug` exposes a `type` field, that you can use to specify the environment, either `node` or `chrome`. - -- [vscode-js-debug configuration documentation](https://github.com/microsoft/vscode-js-debug/blob/main/OPTIONS.md) - -### Attach debugger to a server running in web browser (`npx serve`) - -Given an externally-ran web server (e.g., with `npx serve` or `npx live-server`) one can attach to it and open it with a browser. - -```json [debug] -[ - { - "label": "Launch Chrome (TypeScript)", - "adapter": "JavaScript", - "type": "chrome", - "request": "launch", - "url": "http://localhost:5500", - "program": "$ZED_FILE", - "webRoot": "${ZED_WORKTREE_ROOT}", - "build": { - "command": "npx", - "args": ["tsc"] - }, - "skipFiles": ["/**"] - } -] -``` - -## See also - -- [Zed Yarn documentation](./yarn.md) for a walkthrough of configuring your project to use Yarn. -- [Zed Deno documentation](./deno.md) diff --git a/docs/src/languages/uiua.md b/docs/src/languages/uiua.md deleted file mode 100644 index 622e04fc3d..0000000000 --- a/docs/src/languages/uiua.md +++ /dev/null @@ -1,8 +0,0 @@ -# Uiua - -[Uiua](https://www.uiua.org/) is a general purpose, stack-based, array-oriented programming language with a focus on simplicity, beauty, and tacit code. - -Uiua support is available through the [Uiua extension](https://github.com/zed-extensions/uiua). - -- Tree-sitter: [shnarazk/tree-sitter-uiua](https://github.com/shnarazk/tree-sitter-uiua) -- Language Server: [uiua-lang/uiua](https://github.com/uiua-lang/uiua/) diff --git a/docs/src/languages/vue.md b/docs/src/languages/vue.md deleted file mode 100644 index b8997d2237..0000000000 --- a/docs/src/languages/vue.md +++ /dev/null @@ -1,6 +0,0 @@ -# Vue - -Vue support is available through the [Vue extension](https://github.com/zed-extensions/vue). - -- Tree-sitter: [tree-sitter-grammars/tree-sitter-vue](https://github.com/tree-sitter-grammars/tree-sitter-vue) -- Language Server: [vuejs/language-tools/](https://github.com/vuejs/language-tools/) diff --git a/docs/src/languages/xml.md b/docs/src/languages/xml.md deleted file mode 100644 index df3d845d6d..0000000000 --- a/docs/src/languages/xml.md +++ /dev/null @@ -1,15 +0,0 @@ -# XML - -XML support is available through the [XML extension](https://github.com/sweetppro/zed-xml/). - -- Tree-sitter: [tree-sitter-grammars/tree-sitter-xml](https://github.com/tree-sitter-grammars/tree-sitter-xml) - -## Configuration - -If you have additional file extensions that are not being automatically recognized as XML just add them to [file_types](../configuring-zed.md#file-types) in your Zed settings: - -```json [settings] - "file_types": { - "XML": ["rdf", "gpx", "kml"] - } -``` diff --git a/docs/src/languages/yaml.md b/docs/src/languages/yaml.md deleted file mode 100644 index 33b92df94e..0000000000 --- a/docs/src/languages/yaml.md +++ /dev/null @@ -1,164 +0,0 @@ -# YAML - -YAML support is available natively in Zed. - -- Tree-sitter: [zed-industries/tree-sitter-yaml](https://github.com/zed-industries/tree-sitter-yaml) -- Language Server: [redhat-developer/yaml-language-server](https://github.com/redhat-developer/yaml-language-server) - -## Configuration - -You can configure various [yaml-language-server settings](https://github.com/redhat-developer/yaml-language-server?tab=readme-ov-file#language-server-settings) by adding them to your Zed settings.json in a `yaml-language-server` block under the `lsp` key. For example: - -```json [settings] - "lsp": { - "yaml-language-server": { - "settings": { - "yaml": { - "keyOrdering": true, - "format": { - "singleQuote": true - }, - "schemas": { - "https://getcomposer.org/schema.json": ["/*"], - "../relative/path/schema.json": ["/config*.yaml"] - } - } - } - } - } -``` - -Note, settings keys must be nested, so `yaml.keyOrdering` becomes `{"yaml": { "keyOrdering": true }}`. - -## Formatting - -By default, Zed uses Prettier for formatting YAML files. - -### Prettier Formatting - -You can customize the formatting behavior of Prettier. For example to use single-quotes in yaml files add the following to your `.prettierrc` configuration file: - -```json [settings] -{ - "overrides": [ - { - "files": ["*.yaml", "*.yml"], - "options": { - "singleQuote": false - } - } - ] -} -``` - -### yaml-language-server Formatting - -To use `yaml-language-server` instead of Prettier for YAML formatting, add the following to your Zed `settings.json`: - -```json [settings] - "languages": { - "YAML": { - "formatter": "language_server" - } - } -``` - -## Schemas - -By default yaml-language-server will attempt to determine the correct schema for a given yaml file and retrieve the appropriate JSON Schema from [Json Schema Store](https://schemastore.org/). - -You can override any auto-detected schema via the `schemas` settings key (demonstrated above) or by providing an [inlined schema](https://github.com/redhat-developer/yaml-language-server#using-inlined-schema) reference via a modeline comment at the top of your yaml file: - -```yaml -# yaml-language-server: $schema=https://www.schemastore.org/github-action.json -name: Issue Assignment -on: - issues: - types: [opened] -``` - -You can disable the automatic detection and retrieval of schemas from the JSON Schema if desired: - -```json [settings] - "lsp": { - "yaml-language-server": { - "settings": { - "yaml": { - "schemaStore": { - "enable": false - } - } - } - } - } -``` - -## Custom Tags - -Yaml-language-server supports [custom tags](https://github.com/redhat-developer/yaml-language-server#adding-custom-tags) which can be used to inject custom application functionality at runtime into your yaml files. - -For example Amazon CloudFormation YAML uses a number of custom tags, to support these you can add the following to your settings.json: - -```json [settings] - "lsp": { - "yaml-language-server": { - "settings": { - "yaml": { - "customTags": [ - "!And scalar", - "!And mapping", - "!And sequence", - "!If scalar", - "!If mapping", - "!If sequence", - "!Not scalar", - "!Not mapping", - "!Not sequence", - "!Equals scalar", - "!Equals mapping", - "!Equals sequence", - "!Or scalar", - "!Or mapping", - "!Or sequence", - "!FindInMap scalar", - "!FindInMap mapping", - "!FindInMap sequence", - "!Base64 scalar", - "!Base64 mapping", - "!Base64 sequence", - "!Cidr scalar", - "!Cidr mapping", - "!Cidr sequence", - "!Ref scalar", - "!Ref mapping", - "!Ref sequence", - "!Sub scalar", - "!Sub mapping", - "!Sub sequence", - "!GetAtt scalar", - "!GetAtt mapping", - "!GetAtt sequence", - "!GetAZs scalar", - "!GetAZs mapping", - "!GetAZs sequence", - "!ImportValue scalar", - "!ImportValue mapping", - "!ImportValue sequence", - "!Select scalar", - "!Select mapping", - "!Select sequence", - "!Split scalar", - "!Split mapping", - "!Split sequence", - "!Join scalar", - "!Join mapping", - "!Join sequence", - "!Condition scalar", - "!Condition mapping", - "!Condition sequence" - ] - } - } - } - } -``` diff --git a/docs/src/languages/yara.md b/docs/src/languages/yara.md deleted file mode 100644 index ba31a97ded..0000000000 --- a/docs/src/languages/yara.md +++ /dev/null @@ -1,6 +0,0 @@ -# Yara - -`Yara` language support in Zed is provided by the [Yara language extension](https://github.com/egibs/yara.zed). Please report issues to [https://github.com/egibs/yara.zed/issues](https://github.com/egibs/yara.zed/issues). - -- Tree-sitter: [egibs/tree-sitter-yara](https://github.com/egibs/tree-sitter-yara) -- Language Server: [avast/yls](https://github.com/avast/yls) diff --git a/docs/src/languages/yarn.md b/docs/src/languages/yarn.md deleted file mode 100644 index 3bb7bd6ae9..0000000000 --- a/docs/src/languages/yarn.md +++ /dev/null @@ -1,9 +0,0 @@ -# Yarn - -[Yarn](https://yarnpkg.com/) is a versatile package manager that improves dependency management and workflow efficiency for JavaScript and other languages. It ensures a deterministic dependency tree, offers offline support, and enhances security for reliable builds. - -## Setup - -1. Run `yarn dlx @yarnpkg/sdks base` to generate a `.yarn/sdks` directory. -2. Set your language server (e.g. VTSLS) to use TypeScript SDK from `.yarn/sdks/typescript/lib` directory in [LSP initialization options](../configuring-zed.md#lsp). The actual setting for that depends on language server; for example, for VTSLS you should set [`typescript.tsdk`](https://github.com/yioneko/vtsls/blob/6adfb5d3889ad4b82c5e238446b27ae3ee1e3767/packages/service/configuration.schema.json#L5). -3. Voilla! Language server functionalities such as Go to Definition, Code Completions and On Hover documentation should work. diff --git a/docs/src/languages/zig.md b/docs/src/languages/zig.md deleted file mode 100644 index 4a48405d0c..0000000000 --- a/docs/src/languages/zig.md +++ /dev/null @@ -1,6 +0,0 @@ -# Zig - -Zig support is available through the [Zig extension](https://github.com/zed-extensions/zig). - -- Tree-sitter: [tree-sitter-zig](https://github.com/tree-sitter-grammars/tree-sitter-zig) -- Language Server: [zls](https://github.com/zigtools/zls) diff --git a/docs/src/linux.md b/docs/src/linux.md deleted file mode 100644 index b535a5e78a..0000000000 --- a/docs/src/linux.md +++ /dev/null @@ -1,395 +0,0 @@ -# Zed on Linux - -## Standard Installation - -For most people we recommend using the script on the [download](https://zed.dev/download) page to install Zed: - -```sh -curl -f https://zed.dev/install.sh | sh -``` - -We also offer a preview build of Zed which receives updates about a week ahead of stable. You can install it with: - -```sh -curl -f https://zed.dev/install.sh | ZED_CHANNEL=preview sh -``` - -The Zed installed by the script works best on systems that: - -- have a Vulkan compatible GPU available (for example Linux on an M-series macBook) -- have a system-wide glibc (NixOS and Alpine do not by default) - - x86_64 (Intel/AMD): glibc version >= 2.31 (Ubuntu 20 and newer) - - aarch64 (ARM): glibc version >= 2.35 (Ubuntu 22 and newer) - -Both Nix and Alpine have third-party Zed packages available (though they are currently a few weeks out of date). If you'd like to use our builds they do work if you install a glibc compatibility layer. On NixOS you can try [nix-ld](https://github.com/Mic92/nix-ld), and on Alpine [gcompat](https://wiki.alpinelinux.org/wiki/Running_glibc_programs). - -You will need to build from source for: - -- architectures other than 64-bit Intel or 64-bit ARM (for example a 32-bit or RISC-V machine) -- Redhat Enterprise Linux 8.x, Rocky Linux 8, AlmaLinux 8, Amazon Linux 2 on all architectures -- Redhat Enterprise Linux 9.x, Rocky Linux 9.3, AlmaLinux 8, Amazon Linux 2023 on aarch64 (x86_x64 OK) - -## Other ways to install Zed on Linux - -Zed is open source, and [you can install from source](./development/linux.md). - -### Installing via a package manager - -There are several third-party Zed packages for various Linux distributions and package managers, sometimes under `zed-editor`. You may be able to install Zed using these packages: - -- Flathub: [`dev.zed.Zed`](https://flathub.org/apps/dev.zed.Zed) -- Arch: [`zed`](https://archlinux.org/packages/extra/x86_64/zed/) -- Arch (AUR): [`zed-git`](https://aur.archlinux.org/packages/zed-git), [`zed-preview`](https://aur.archlinux.org/packages/zed-preview), [`zed-preview-bin`](https://aur.archlinux.org/packages/zed-preview-bin) -- Alpine: `zed` ([aarch64](https://pkgs.alpinelinux.org/package/edge/testing/aarch64/zed)) ([x86_64](https://pkgs.alpinelinux.org/package/edge/testing/x86_64/zed)) -- Conda: [`zed`](https://anaconda.org/conda-forge/zed) -- Nix: `zed-editor` ([unstable](https://search.nixos.org/packages?channel=unstable&show=zed-editor)) -- Fedora/Ultramarine (Terra): [`zed`](https://github.com/terrapkg/packages/tree/frawhide/anda/devs/zed/stable), [`zed-preview`](https://github.com/terrapkg/packages/tree/frawhide/anda/devs/zed/preview), [`zed-nightly`](https://github.com/terrapkg/packages/tree/frawhide/anda/devs/zed/nightly) -- Solus: [`zed`](https://github.com/getsolus/packages/tree/main/packages/z/zed) -- Parabola: [`zed`](https://www.parabola.nu/packages/extra/x86_64/zed/) -- Manjaro: [`zed`](https://packages.manjaro.org/?query=zed) -- ALT Linux (Sisyphus): [`zed`](https://packages.altlinux.org/en/sisyphus/srpms/zed/) -- AOSC OS: [`zed`](https://packages.aosc.io/packages/zed) - -See [Repology](https://repology.org/project/zed-editor/versions) for a list of Zed packages in various repositories. - -### Community - -When installing a third-party package please be aware that it may not be completely up to date and may be slightly different from the Zed we package (a common change is to rename the binary to `zedit` or `zeditor` to avoid conflicting with other packages). - -We'd love your help making Zed available for everyone. If Zed is not yet available for your package manager, and you would like to fix that, we have some notes on [how to do it](./development/linux.md#notes-for-packaging-zed). - -The packages in this section provide binary installs for Zed but are not official packages within the associated distributions. These packages are maintained by community members and as such a higher level of caution should be taken when installing them. - -#### Debian - -Zed is available in [this community-maintained repository](https://debian.griffo.io/). - -Instructions for each version are available in the README of the repository where packages are built. -Build, packaging and instructions for each version are available in the README of the [repository](https://github.com/dariogriffo/zed-debian) - -### Downloading manually - -If you'd prefer, you can install Zed by downloading our pre-built .tar.gz. This is the same artifact that our install script uses, but you can customize the location of your installation by modifying the instructions below: - -Download the `.tar.gz` file: - -- [zed-linux-x86_64.tar.gz](https://cloud.zed.dev/releases/stable/latest/download?asset=zed&arch=x86_64&os=linux&source=docs) - ([preview](https://cloud.zed.dev/releases/preview/latest/download?asset=zed&arch=x86_64&os=linux&source=docs)) -- [zed-linux-aarch64.tar.gz](https://cloud.zed.dev/releases/stable/latest/download?asset=zed&arch=aarch64&os=linux&source=docs) - ([preview](https://cloud.zed.dev/releases/preview/latest/download?asset=zed&arch=aarch64&os=linux&source=docs)) - -Then ensure that the `zed` binary in the tarball is on your path. The easiest way is to unpack the tarball and create a symlink: - -```sh -mkdir -p ~/.local -# extract zed to ~/.local/zed.app/ -tar -xvf .tar.gz -C ~/.local -# link the zed binary to ~/.local/bin (or another directory in your $PATH) -ln -sf ~/.local/zed.app/bin/zed ~/.local/bin/zed -``` - -If you'd like integration with an XDG-compatible desktop environment, you will also need to install the `.desktop` file: - -```sh -cp ~/.local/zed.app/share/applications/zed.desktop ~/.local/share/applications/dev.zed.Zed.desktop -sed -i "s|Icon=zed|Icon=$HOME/.local/zed.app/share/icons/hicolor/512x512/apps/zed.png|g" ~/.local/share/applications/dev.zed.Zed.desktop -sed -i "s|Exec=zed|Exec=$HOME/.local/zed.app/libexec/zed-editor|g" ~/.local/share/applications/dev.zed.Zed.desktop -``` - -## Uninstalling Zed - -### Standard Uninstall - -If Zed was installed using the default installation script, it can be uninstalled by supplying the `--uninstall` flag to the `zed` shell command - -```sh -zed --uninstall -``` - -If there are no errors, the shell will then prompt you whether you'd like to keep your preferences or delete them. After making a choice, you should see a message that Zed was successfully uninstalled. - -In the case that the `zed` shell command was not found in your PATH, you can try one of the following commands - -```sh -$HOME/.local/bin/zed --uninstall -``` - -or - -```sh -$HOME/.local/zed.app/bin.zed --uninstall -``` - -The first case might fail if a symlink was not properly established between `$HOME/.local/bin/zed` and `$HOME/.local/zed.app/bin.zed`. But the second case should work as long as Zed was installed to its default location. - -If Zed was installed to a different location, you must invoke the `zed` binary stored in that installation directory and pass the `--uninstall` flag to it in the same format as the previous commands. - -### Package Manager - -If Zed was installed using a package manager, please consult the documentation for that package manager on how to uninstall a package. - -## Troubleshooting - -Linux works on a large variety of systems configured in many different ways. We primarily test Zed on a vanilla Ubuntu setup, as it is the most common distribution our users use, that said we do expect it to work on a wide variety of machines. - -### Zed fails to start - -If you see an error like "/lib64/libc.so.6: version 'GLIBC_2.29' not found" it means that your distribution's version of glibc is too old. You can either upgrade your system, or [install Zed from source](./development/linux.md). - -### Graphics issues - -#### Zed fails to open windows - -Zed requires a GPU to run effectively. Under the hood, we use [Vulkan](https://www.vulkan.org/) to communicate with your GPU. If you are seeing problems with performance, or Zed fails to load, it is possible that Vulkan is the culprit. - -If you see a notification saying `Zed failed to open a window: NoSupportedDeviceFound` this means that Vulkan cannot find a compatible GPU. you can try running [vkcube](https://github.com/krh/vkcube) (usually available as part of the `vulkaninfo` or `vulkan-tools` package on various distributions) to try to troubleshoot where the issue is coming from like so: - -``` -vkcube -``` - -> **_Note_**: Try running in both X11 and wayland modes by running `vkcube -m [x11|wayland]`. Some versions of `vkcube` use `vkcube` to run in X11 and `vkcube-wayland` to run in wayland. - -This should output a line describing your current graphics setup and show a rotating cube. If this does not work, you should be able to fix it by installing Vulkan compatible GPU drivers, however in some cases there is no Vulkan support yet. - -You can find out which graphics card Zed is using by looking in the Zed log (`~/.local/share/zed/logs/Zed.log`) for `Using GPU: ...`. - -If you see errors like `ERROR_INITIALIZATION_FAILED` or `GPU Crashed` or `ERROR_SURFACE_LOST_KHR` then you may be able to work around this by installing different drivers for your GPU, or by selecting a different GPU to run on. (See [#14225](https://github.com/zed-industries/zed/issues/14225)) - -On some systems the file `/etc/prime-discrete` can be used to enforce the use of a discrete GPU using [PRIME](https://wiki.archlinux.org/title/PRIME). Depending on the details of your setup, you may need to change the contents of this file to "on" (to force discrete graphics) or "off" (to force integrated graphics). - -On others, you may be able to the environment variable `DRI_PRIME=1` when running Zed to force the use of the discrete GPU. - -If you're using an AMD GPU and Zed crashes when selecting long lines, try setting the `ZED_PATH_SAMPLE_COUNT=0` environment variable. (See [#26143](https://github.com/zed-industries/zed/issues/26143)) - -If you're using an AMD GPU, you might get a 'Broken Pipe' error. Try using the RADV or Mesa drivers. (See [#13880](https://github.com/zed-industries/zed/issues/13880)) - -If you are using `amdvlk`, the default open-source AMD graphics driver, you may find that Zed consistently fails to launch. This is a known issue for some users, for example on Omarchy (see issue [#28851](https://github.com/zed-industries/zed/issues/28851)). To fix this, you will need to use a different driver. We recommend removing the `amdvlk` and `lib32-amdvlk` packages and installing `vulkan-radeon` instead (see issue [#14141](https://github.com/zed-industries/zed/issues/14141)). - -For more information, the [Arch guide to Vulkan](https://wiki.archlinux.org/title/Vulkan) has some good steps that translate well to most distributions. - -#### Forcing Zed to use a specific GPU - -There are a few different ways to force Zed to use a specific GPU: - -##### Option A - -You can use the `ZED_DEVICE_ID={device_id}` environment variable to specify the device ID of the GPU you wish to have Zed use. - -You can obtain the device ID of your GPU by running `lspci -nn | grep VGA` which will output each GPU on one line like: - -``` -08:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA104 [GeForce RTX 3070] [10de:2484] (rev a1) -``` - -where the device ID here is `2484`. This value is in hexadecimal, so to force Zed to use this specific GPU you would set the environment variable like so: - -``` -ZED_DEVICE_ID=0x2484 zed -``` - -Make sure to export the variable if you choose to define it globally in a `.bashrc` or similar. - -##### Option B - -If you are using Mesa, you can run `MESA_VK_DEVICE_SELECT=list zed --foreground` to get a list of available GPUs and then export `MESA_VK_DEVICE_SELECT=xxxx:yyyy` to choose a specific device. Furthermore, you can fallback to xwayland with an additional export of `WAYLAND_DISPLAY=""`. - -##### Option C - -Using [vkdevicechooser](https://github.com/jiriks74/vkdevicechooser). - -#### Reporting graphics issues - -If Vulkan is configured correctly, and Zed is still not working for you, please [file an issue](https://github.com/zed-industries/zed) with as much information as possible. - -When reporting issues where Zed fails to start due to graphics initialization errors on GitHub, it can be impossible to run the `zed: copy system specs into clipboard` command like we instruct you to in our issue template. We provide an alternative way to collect the system specs specifically for this situation. - -Passing the `--system-specs` flag to Zed like - -```sh -zed --system-specs -``` - -will print the system specs to the terminal like so. It is strongly recommended to copy the output verbatim into the issue on GitHub, as it uses markdown formatting to ensure the output is readable. - -Additionally, it is extremely beneficial to provide the contents of your Zed log when reporting such issues. The log is usually located at `~/.local/share/zed/logs/Zed.log`. The recommended process for producing a helpful log file is as follows: - -```sh -truncate -s 0 ~/.local/share/zed/logs/Zed.log # Clear the log file -ZED_LOG=blade_graphics=info zed . -cat ~/.local/share/zed/logs/Zed.log -# copy the output -``` - -Or, if you have the Zed cli setup, you can do - -```sh -ZED_LOG=blade_graphics=info /path/to/zed/cli --foreground . -# copy the output -``` - -It is also highly recommended when pasting the log into a github issue, to do so with the following template: - -> **_Note_**: The whitespace in the template is important, and will cause incorrect formatting if not preserved. - -```` -

Zed Log - -``` -{zed log contents} -``` - -
-```` - -This will cause the logs to be collapsed by default, making it easier to read the issue. - -### I can't open any files - -### Clicking links isn't working - -These features are provided by XDG desktop portals, specifically: - -- `org.freedesktop.portal.FileChooser` -- `org.freedesktop.portal.OpenURI` - -Some window managers, such as `Hyprland`, don't provide a file picker by default. See [this list](https://wiki.archlinux.org/title/XDG_Desktop_Portal#List_of_backends_and_interfaces) as a starting point for alternatives. - -### Zed isn't remembering my API keys - -### Zed isn't remembering my login - -These feature also requires XDG desktop portals, specifically: - -- `org.freedesktop.portal.Secret` or -- `org.freedesktop.Secrets` - -Zed needs a place to securely store secrets such as your Zed login cookie or your OpenAI API Keys and we use a system provided keychain to do this. Examples of packages that provide this are `gnome-keyring`, `KWallet` and `keepassxc` among others. - -### Could not start inotify - -Zed relies on inotify to watch your filesystem for changes. If you cannot start inotify then Zed will not work reliably. - -If you are seeing "too many open files" then first try `sysctl fs.inotify`. - -- You should see that max_user_instances is 128 or higher (you can change the limit with `sudo sysctl fs.inotify.max_user_instances=1024`). Zed needs only 1 inotify instance. -- You should see that `max_user_watches` is 8000 or higher (you can change the limit with `sudo sysctl fs.inotify.max_user_watches=64000`). Zed needs one watch per directory in all your open projects + one per git repository + a handful more for settings, themes, keymaps, extensions. - -It is also possible that you are running out of file descriptors. You can check the limits with `ulimit` and update them by editing `/etc/security/limits.conf`. - -### No sound or wrong output device - -If you're not hearing any sound in Zed or the audio is routed to the wrong device, it could be due to a mismatch between audio systems. Zed relies on ALSA, while your system may be using PipeWire or PulseAudio. To resolve this, you need to configure ALSA to route audio through PipeWire/PulseAudio. - -If your system uses PipeWire: - -1. **Install the PipeWire ALSA plugin** - - On Debian-based systems, run: - - ```bash - sudo apt install pipewire-alsa - ``` - -2. **Configure ALSA to use PipeWire** - - Add the following configuration to your ALSA settings file. You can use either `~/.asoundrc` (user-level) or `/etc/asound.conf` (system-wide): - - ```bash - pcm.!default { - type pipewire - } - - ctl.!default { - type pipewire - } - ``` - -3. **Restart your system** - -### Forcing X11 scale factor - -On X11 systems, Zed automatically detects the appropriate scale factor for high-DPI displays. The scale factor is determined using the following priority order: - -1. `GPUI_X11_SCALE_FACTOR` environment variable (if set) -2. `Xft.dpi` from X resources database (xrdb) -3. Automatic detection via RandR based on monitor resolution and physical size - -If you want to customize the scale factor beyond what Zed detects automatically, you have several options: - -#### Check your current scale factor - -You can verify if you have `Xft.dpi` set: - -```sh -xrdb -query | grep Xft.dpi -``` - -If this command returns no output, Zed is using RandR (X11's monitor management extension) to automatically calculate the scale factor based on your monitor's reported resolution and physical dimensions. - -#### Option 1: Set Xft.dpi (X Resources Database) - -`Xft.dpi` is a standard X11 setting that many applications use for consistent font and UI scaling. Setting this ensures Zed scales the same way as other X11 applications that respect this setting. - -Edit or create the `~/.Xresources` file: - -```sh -vim ~/.Xresources -``` - -Add this line with your desired DPI: - -```sh -Xft.dpi: 96 -``` - -Common DPI values: - -- `96` for standard 1x scaling -- `144` for 1.5x scaling -- `192` for 2x scaling -- `288` for 3x scaling - -Load the configuration: - -```sh -xrdb -merge ~/.Xresources -``` - -Restart Zed for the changes to take effect. - -#### Option 2: Use the GPUI_X11_SCALE_FACTOR environment variable - -This Zed-specific environment variable directly sets the scale factor, bypassing all automatic detection. - -```sh -GPUI_X11_SCALE_FACTOR=1.5 zed -``` - -You can use decimal values (e.g., `1.25`, `1.5`, `2.0`) or set `GPUI_X11_SCALE_FACTOR=randr` to force RandR-based detection even when `Xft.dpi` is set. - -To make this permanent, add it to your shell profile or desktop entry. - -#### Option 3: Adjust system-wide RandR DPI - -This changes the reported DPI for your entire X11 session, affecting how RandR calculates scaling for all applications that use it. - -Add this to your `.xprofile` or `.xinitrc`: - -```sh -xrandr --dpi 192 -``` - -Replace `192` with your desired DPI value. This affects the system globally and will be used by Zed's automatic RandR detection when `Xft.dpi` is not set. - -### Font rendering parameters - -When using Blade rendering (Linux platforms and self-compiled builds with the Blade renderer enabled), Zed reads `ZED_FONTS_GAMMA` and `ZED_FONTS_GRAYSCALE_ENHANCED_CONTRAST` environment variables for the values to use for font rendering. - -`ZED_FONTS_GAMMA` corresponds to [getgamma](https://learn.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwriterenderingparams-getgamma) values. -Allowed range [1.0, 2.2], other values are clipped. -Default: 1.8 - -`ZED_FONTS_GRAYSCALE_ENHANCED_CONTRAST` corresponds to [getgrayscaleenhancedcontrast](https://learn.microsoft.com/en-us/windows/win32/api/dwrite_1/nf-dwrite_1-idwriterenderingparams1-getgrayscaleenhancedcontrast) values. -Allowed range: [0.0, ..), other values are clipped. -Default: 1.0 diff --git a/docs/src/migrate/vs-code.md b/docs/src/migrate/vs-code.md deleted file mode 100644 index dd7419e3ff..0000000000 --- a/docs/src/migrate/vs-code.md +++ /dev/null @@ -1,373 +0,0 @@ -# How to Migrate from VS Code to Zed - -This guide is for developers who spent serious time in VS Code and want to try Zed without starting from scratch. - -If you’re here, you might be looking for a faster editor. Or something less cluttered. Or you’re curious about built-in collaboration. Whatever brought you here, this guide helps you move over your habits, shortcuts, and settings. - -We’ll cover what to bring, what to change, and what’s different. You can ease in gradually or switch all at once. Either way, you’ll stay productive. - -## Install Zed - -Zed is available on macOS, Windows, and Linux. - -For macOS, you can download it from zed.dev/download, or install via Homebrew: -`brew install zed-editor/zed/zed` - -For most Linux users, the easiest way to install Zed is through our installation script: -`curl -f https://zed.dev/install.sh | sh` - -After installation, you can launch Zed from your Applications folder (macOS) or directly from the terminal (Linux) using: -`zed .` -This opens the current directory in Zed. - -## Import Settings from VS Code - -During setup, you have the option to import key settings from VS Code. Zed imports the following settings: - -### Settings Imported from VS Code - -The following VS Code settings are automatically imported when you use **Import Settings from VS Code**: - -**Editor** - -| VS Code Setting | Zed Setting | -| ------------------------------------------- | ---------------------------------------------- | -| `editor.fontFamily` | `buffer_font_family` | -| `editor.fontSize` | `buffer_font_size` | -| `editor.fontWeight` | `buffer_font_weight` | -| `editor.tabSize` | `tab_size` | -| `editor.insertSpaces` | `hard_tabs` (inverted) | -| `editor.wordWrap` | `soft_wrap` | -| `editor.wordWrapColumn` | `preferred_line_length` | -| `editor.cursorStyle` | `cursor_shape` | -| `editor.cursorBlinking` | `cursor_blink` | -| `editor.renderLineHighlight` | `current_line_highlight` | -| `editor.lineNumbers` | `gutter.line_numbers`, `relative_line_numbers` | -| `editor.showFoldingControls` | `gutter.folds` | -| `editor.minimap.enabled` | `minimap.show` | -| `editor.minimap.autohide` | `minimap.show` | -| `editor.minimap.showSlider` | `minimap.thumb` | -| `editor.minimap.maxColumn` | `minimap.max_width_columns` | -| `editor.stickyScroll.enabled` | `sticky_scroll.enabled` | -| `editor.scrollbar.horizontal` | `scrollbar.axes.horizontal` | -| `editor.scrollbar.vertical` | `scrollbar.axes.vertical` | -| `editor.mouseWheelScrollSensitivity` | `scroll_sensitivity` | -| `editor.fastScrollSensitivity` | `fast_scroll_sensitivity` | -| `editor.cursorSurroundingLines` | `vertical_scroll_margin` | -| `editor.hover.enabled` | `hover_popover_enabled` | -| `editor.hover.delay` | `hover_popover_delay` | -| `editor.parameterHints.enabled` | `auto_signature_help` | -| `editor.multiCursorModifier` | `multi_cursor_modifier` | -| `editor.selectionHighlight` | `selection_highlight` | -| `editor.roundedSelection` | `rounded_selection` | -| `editor.find.seedSearchStringFromSelection` | `seed_search_query_from_cursor` | -| `editor.rulers` | `wrap_guides` | -| `editor.renderWhitespace` | `show_whitespaces` | -| `editor.guides.indentation` | `indent_guides.enabled` | -| `editor.linkedEditing` | `linked_edits` | -| `editor.autoSurround` | `use_auto_surround` | -| `editor.formatOnSave` | `format_on_save` | -| `editor.formatOnPaste` | `auto_indent_on_paste` | -| `editor.formatOnType` | `use_on_type_format` | -| `editor.trimAutoWhitespace` | `remove_trailing_whitespace_on_save` | -| `editor.suggestOnTriggerCharacters` | `show_completions_on_input` | -| `editor.suggest.showWords` | `completions.words` | -| `editor.inlineSuggest.enabled` | `show_edit_predictions` | - -**Files & Workspace** - -| VS Code Setting | Zed Setting | -| --------------------------- | ------------------------------ | -| `files.autoSave` | `autosave` | -| `files.autoSaveDelay` | `autosave.milliseconds` | -| `files.insertFinalNewline` | `ensure_final_newline_on_save` | -| `files.associations` | `file_types` | -| `files.watcherExclude` | `file_scan_exclusions` | -| `files.watcherInclude` | `file_scan_inclusions` | -| `files.simpleDialog.enable` | `use_system_path_prompts` | -| `search.smartCase` | `use_smartcase_search` | -| `search.useIgnoreFiles` | `search.include_ignored` | - -**Terminal** - -| VS Code Setting | Zed Setting | -| ------------------------------------- | ----------------------------------- | -| `terminal.integrated.fontFamily` | `terminal.font_family` | -| `terminal.integrated.fontSize` | `terminal.font_size` | -| `terminal.integrated.lineHeight` | `terminal.line_height` | -| `terminal.integrated.cursorStyle` | `terminal.cursor_shape` | -| `terminal.integrated.cursorBlinking` | `terminal.blinking` | -| `terminal.integrated.copyOnSelection` | `terminal.copy_on_select` | -| `terminal.integrated.scrollback` | `terminal.max_scroll_history_lines` | -| `terminal.integrated.macOptionIsMeta` | `terminal.option_as_meta` | -| `terminal.integrated.{platform}Exec` | `terminal.shell` | -| `terminal.integrated.env.{platform}` | `terminal.env` | - -**Tabs & Panels** - -| VS Code Setting | Zed Setting | -| -------------------------------------------------- | -------------------------------------------------- | -| `workbench.editor.showTabs` | `tab_bar.show` | -| `workbench.editor.showIcons` | `tabs.file_icons` | -| `workbench.editor.tabActionLocation` | `tabs.close_position` | -| `workbench.editor.tabActionCloseVisibility` | `tabs.show_close_button` | -| `workbench.editor.focusRecentEditorAfterClose` | `tabs.activate_on_close` | -| `workbench.editor.enablePreview` | `preview_tabs.enabled` | -| `workbench.editor.enablePreviewFromQuickOpen` | `preview_tabs.enable_preview_from_file_finder` | -| `workbench.editor.enablePreviewFromCodeNavigation` | `preview_tabs.enable_preview_from_code_navigation` | -| `workbench.editor.editorActionsLocation` | `tab_bar.show_tab_bar_buttons` | -| `workbench.editor.limit.enabled` / `value` | `max_tabs` | -| `workbench.editor.restoreViewState` | `restore_on_file_reopen` | -| `workbench.statusBar.visible` | `status_bar.show` | - -**Project Panel (File Explorer)** - -| VS Code Setting | Zed Setting | -| ------------------------------ | ----------------------------------- | -| `explorer.compactFolders` | `project_panel.auto_fold_dirs` | -| `explorer.autoReveal` | `project_panel.auto_reveal_entries` | -| `explorer.excludeGitIgnore` | `project_panel.hide_gitignore` | -| `problems.decorations.enabled` | `project_panel.show_diagnostics` | -| `explorer.decorations.badges` | `project_panel.git_status` | - -**Git** - -| VS Code Setting | Zed Setting | -| ------------------------------------ | ---------------------------------------------- | -| `git.enabled` | `git_panel.button` | -| `git.defaultBranchName` | `git_panel.fallback_branch_name` | -| `git.decorations.enabled` | `git.inline_blame`, `project_panel.git_status` | -| `git.blame.editorDecoration.enabled` | `git.inline_blame.enabled` | - -**Window & Behavior** - -| VS Code Setting | Zed Setting | -| ------------------------------------------------ | ---------------------------------------- | -| `window.confirmBeforeClose` | `confirm_quit` | -| `window.nativeTabs` | `use_system_window_tabs` | -| `window.closeWhenEmpty` | `when_closing_with_no_tabs` | -| `accessibility.dimUnfocused.enabled` / `opacity` | `active_pane_modifiers.inactive_opacity` | - -**Other** - -| VS Code Setting | Zed Setting | -| -------------------------- | -------------------------------------------------------- | -| `http.proxy` | `proxy` | -| `npm.packageManager` | `node.npm_path` | -| `telemetry.telemetryLevel` | `telemetry.metrics`, `telemetry.diagnostics` | -| `outline.icons` | `outline_panel.file_icons`, `outline_panel.folder_icons` | -| `chat.agent.enabled` | `agent.enabled` | -| `mcp` | `context_servers` | - -Zed doesn’t import extensions or keybindings, but this is the fastest way to get a familiar feel while trying something new. If you skip that step during setup, you can still import settings manually later via the command palette: - -`Cmd+Shift+P → Zed: Import VS Code Settings` - -## Set Up Editor Preferences - -You can also configure settings manually in the Settings Editor. - -To edit your settings: - -1. `Cmd+,` to open the Settings Editor. -2. Run `zed: open settings` in the Command Palette. - -Here’s how common VS Code settings translate: -| VS Code | Zed | Notes | -| --- | --- | --- | -| editor.fontFamily | buffer_font_family | Zed uses Zed Mono by default | -| editor.fontSize | buffer_font_size | Set in pixels | -| editor.tabSize | tab_size | Can override per language | -| editor.insertSpaces | insert_spaces | Boolean | -| editor.formatOnSave | format_on_save | Works with formatter enabled | -| editor.wordWrap | soft_wrap | Supports optional wrap column | - -Zed also supports per-project settings. You can find these in the Settings Editor as well. - -## Open or Create a Project - -After setup, press `Cmd+O` (`Ctrl+O` on Linux) to open a folder. This becomes your workspace in Zed. There's no support for multi-root workspaces or `.code-workspace` files like in VS Code. Zed keeps it simple: one folder, one workspace. - -To start a new project, create a directory using your terminal or file manager, then open it in Zed. The editor will treat that folder as the root of your project. - -You can also launch Zed from the terminal inside any folder with: -`zed .` - -Once inside a project, use `Cmd+P` to jump between files quickly. `Cmd+Shift+P` (`Ctrl+Shift+P` on Linux) opens the command palette for running actions / tasks, toggling settings, or starting a collaboration session. - -Open buffers appear as tabs across the top. The sidebar shows your file tree and Git status. Collapse it with `Cmd+B` for a distraction-free view. - -## Differences in Keybindings - -If you chose the VS Code keymap during onboarding, you're likely good to go, and most of your shortcuts should already feel familiar. -Here’s a quick reference guide for how our keybindings compare to what you’re used to coming from VS Code. - -### Common Shared Keybindings (Zed <> VS Code) - -| Action | Shortcut | -| --------------------------- | ---------------------- | -| Find files | `Cmd + P` | -| Run a command | `Cmd + Shift + P` | -| Search text (project-wide) | `Cmd + Shift + F` | -| Find symbols (project-wide) | `Cmd + T` | -| Find symbols (file-wide) | `Cmd + Shift + O` | -| Toggle left dock | `Cmd + B` | -| Toggle bottom dock | `Cmd + J` | -| Open terminal | `Ctrl + ~` | -| Open file tree explorer | `Cmd + Shift + E` | -| Close current buffer | `Cmd + W` | -| Close whole project | `Cmd + Shift + W` | -| Refactor: rename symbol | `F2` | -| Change theme | `Cmd + K, Cmd + T` | -| Wrap text | `Opt + Z` | -| Navigate open tabs | `Cmd + Opt + Arrow` | -| Syntactic fold / unfold | `Cmd + Opt + {` or `}` | - -### Different Keybindings (Zed <> VS Code) - -| Action | VS Code | Zed | -| ------------------- | --------------------- | ---------------------- | -| Open recent project | `Ctrl + R` | `Cmd + Opt + O` | -| Move lines up/down | `Opt + Up/Down` | `Cmd + Ctrl + Up/Down` | -| Split panes | `Cmd + \` | `Cmd + K, Arrow Keys` | -| Expand Selection | `Shift + Alt + Right` | `Opt + Up` | - -### Unique to Zed - -| Action | Shortcut | Notes | -| ------------------- | ---------------------------- | ------------------------------------------------ | -| Toggle right dock | `Cmd + R` or `Cmd + Alt + B` | | -| Syntactic selection | `Opt + Up/Down` | Selects code by structure (e.g., inside braces). | - -### How to Customize Keybindings - -To edit your keybindings: - -- Open the command palette (`Cmd+Shift+P`) -- Run `Zed: Open Keymap Editor` - -This opens a list of all available bindings. You can override individual shortcuts, remove conflicts, or build a layout that works better for your setup. - -Zed also supports chords (multi-key sequences) like `Cmd+K Cmd+C`, like VS Code does. - -## Differences in User Interfaces - -### No Workspace - -VS Code uses a dedicated Workspace concept, with multi-root folders, `.code-workspace` files, and a clear distinction between “a window” and “a workspace.” -Zed simplifies this model. - -In Zed: - -- There is no workspace file format. Opening a folder is your project context. - -- Zed does not support multi-root workspaces. You can only open one folder at a time in a window. - -- Most project-level behavior is scoped to the folder you open. Search, Git integration, tasks, and environment detection all treat the opened directory as the project root. - -- Per-project settings are optional. You can add a `.zed/settings.json` file inside a project to override global settings, but Zed does not use `.code-workspace` files and won’t import them. - -- You can start from a single file or an empty window. Zed doesn’t require you to open a folder to begin editing. - -The result is a simpler model: -Open a folder → work inside that folder → no additional workspace layer. - -### Navigating in a Project - -In VS Code, the standard entry point is opening a folder. From there, the left-hand sidebar is central to your navigation. -Zed takes a different approach: - -- You can still open folders, but you don’t need to. Opening a single file or even starting with an empty workspace is valid. -- The Command Palette (`Cmd+Shift+P`) and File Finder (`Cmd+P`) are your primary navigation tools. The File Finder searches across the entire workspace instantly; files, symbols, commands, even teammates if you're collaborating. -- Instead of a persistent sidebar, Zed encourages you to: - - Fuzzy-find files by name (`Cmd+P`) - - Jump directly to symbols (`Cmd+Shift+O`) - - Use split panes and tabs for context, rather than keeping a large file tree open (though you can do this with the Project Panel if you prefer). - -The UI is intentionally minimal. Panels slide in only when needed, then get out of your way. The focus is on flowing between code instead of managing panes. - -### Extensions vs. Marketplace - -Zed does not offer as many extensions as VS Code. The available extensions are focused on language support, themes, syntax highlighting, and other core editing enhancements. - -However there are several features that typically require extensions in VS Code which we built directly into Zed: - -- Real-time collaboration with voice and cursor sharing (no Live Share required) -- AI coding assistance (no Copilot extension needed) -- Built-in terminal panel -- Project-wide fuzzy search -- Task runner with JSON config -- Inline diagnostics and code actions via LSP - -You won’t find one-to-one replacements for every VS Code extension, especially if you rely on tools for DevOps, containers, or test runners. Zed's extension ecosystem is still growing, and the catalog is smaller by design. - -### Collaboration in Zed vs. VS Code - -Unlike VS Code, Zed doesn’t require an extension to collaborate. It’s built into the core experience. - -- Open the Collab Panel in the left dock. -- Create a channel and [invite your collaborators](https://zed.dev/docs/collaboration#inviting-a-collaborator) to join. -- [Share your screen or your codebase](https://zed.dev/docs/collaboration#share-a-project) directly. - -Once connected, you’ll see each other's cursors, selections, and edits in real time. Voice chat is included, so you can talk as you work. There’s no need for separate tools or third-party logins. Zed’s collaboration is designed for everything from quick pair programming to longer team sessions. - -Learn how [Zed uses Zed](https://zed.dev/blog/zed-is-our-office) to plan work and collaborate. - -### Using AI in Zed - -If you’re used to GitHub Copilot in VS Code, you can do the same in Zed. You can also explore other agents through Zed Pro, or bring your own keys and connect without authentication. Zed is designed to enable many options for using AI, including disabling it entirely. - -#### Configuring GitHub Copilot - -You should be able to sign-in to GitHub Copilot by clicking on the Zeta icon in the status bar and following the setup instructions. -You can also add this to your settings: - -```json -{ - "features": { - "edit_prediction_provider": "copilot" - } -} -``` - -To invoke completions, just start typing. Zed will offer suggestions inline for you to accept. - -#### Additional AI Options - -To use other AI models in Zed, you have several options: - -- Use Zed’s hosted models, with higher rate limits. Requires [authentication](https://zed.dev/docs/accounts.html) and subscription to [Zed Pro](https://zed.dev/docs/ai/subscription.html). -- Bring your own [API keys](https://zed.dev/docs/ai/llm-providers.html), no authentication needed -- Use [external agents like Claude Code](https://zed.dev/docs/ai/external-agents.html). - -### Advanced Config and Productivity Tweaks - -Zed exposes advanced settings for power users who want to fine-tune their environment. - -Here are a few useful tweaks: - -**Format on Save:** - -```json -"format_on_save": "on" -``` - -**Enable direnv support:** - -```json -"load_direnv": "shell_hook" -``` - -**Custom Tasks**: Define build or run commands in your `tasks.json` (accessed via command palette: `zed: open tasks`): - -```json -[ - { - "label": "build", - "command": "cargo build" - } -] -``` - -**Bring over custom snippets** -Copy your VS Code snippet JSON directly into Zed's snippets folder (`zed: configure snippets`). diff --git a/docs/src/multibuffers.md b/docs/src/multibuffers.md deleted file mode 100644 index 7d9f4cafc4..0000000000 --- a/docs/src/multibuffers.md +++ /dev/null @@ -1,40 +0,0 @@ -# Multibuffers - -One of the superpowers Zed gives you is the ability to edit multiple files simultaneously. When combined with multiple cursors, this makes wide-ranging refactors significantly faster. - -## Editing in a multibuffer - -
- -
- -Editing a multibuffer is the same as editing a normal file. Changes you make will be reflected in the open copies of that file in the rest of the editor, and you can save all files with `editor: Save` (bound to `cmd-s` on macOS, `ctrl-s` on Windows/Linux, or `:w` in Vim mode). - -When in a multibuffer, it is often useful to use multiple cursors to edit every file simultaneously. If you want to edit a few instances, you can select them with the mouse (`option-click` on macOS, `alt-click` on Window/Linux) or the keyboard. `cmd-d` on macOS, `ctrl-d` on Windows/Linux, or `gl` in Vim mode will select the next match of the word under the cursor. - -When you want to edit all matches you can select them by running the `editor: Select All Matches` command (`cmd-shift-l` on macOS, `ctrl-shift-l` on Windows/Linux, or `g a` in Vim mode). - -## Navigating to the Source File - -While you can easily edit files in a multibuffer, navigating directly to the source file is often beneficial. You can accomplish this by clicking on any of the divider lines between excerpts or by placing your cursor in an excerpt and executing the `editor: open excerpts` command. It’s key to note that if multiple cursors are being used, the command will open the source file positioned under each cursor within the multibuffer. - -Additionally, if you prefer to use the mouse and would like to double-click on an excerpt to open it, you can enable this functionality with the setting: `"double_click_in_multibuffer": "open"`. - -## Project search - -To start a search run the `pane: Toggle Search` command (`cmd-shift-f` on macOS, `ctrl-shift-f` on Windows/Linux, or `g/` in Vim mode). After the search has completed, the results will be shown in a new multibuffer. There will be one excerpt for each matching line across the whole project. - -## Diagnostics - -If you have a language server installed, the diagnostics pane can show you all errors across your project. You can open it by clicking on the icon in the status bar, or running the `diagnostics: Deploy` command` ('cmd-shift-m` on macOS, `ctrl-shift-m` on Windows/Linux, or `:clist` in Vim mode). - -## Find References - -If you have a language server installed, you can find all references to the symbol under the cursor with the `editor: Find References` command (`cmd-click` on macOS, `ctrl-click` on Windows/Linux, or `g A` in Vim mode. - -Depending on your language server, commands like `editor: Go To Definition` and `editor: Go To Type Definition` will also open a multibuffer if there are multiple possible definitions. diff --git a/docs/src/outline-panel.md b/docs/src/outline-panel.md deleted file mode 100644 index bc743596d6..0000000000 --- a/docs/src/outline-panel.md +++ /dev/null @@ -1,31 +0,0 @@ -# Outline Panel - -In addition to the modal outline (`cmd-shift-o`), Zed offers an outline panel. The outline panel can be deployed via `cmd-shift-b` (`outline panel: toggle focus` via the command palette), or by clicking the `Outline Panel` button in the status bar. - -When viewing a "singleton" buffer (i.e., a single file on a tab), the outline panel works similarly to that of the outline modal-it displays the outline of the current buffer's symbols, as reported by tree-sitter. Clicking on an entry allows you to jump to the associated section in the file. The outline view will also automatically scroll to the section associated with the current cursor position within the file. - -![Using the outline panel in a singleton buffer](https://zed.dev/img/outline-panel/singleton.png) - -## Usage with multibuffers - -The outline panel truly excels when used with multi-buffers. Here are some examples of its versatility: - -### Project Search Results - -Get an overview of search results across your project. - -![Using the outline panel in a project search multi-buffer](https://zed.dev/img/outline-panel/project-search.png) - -### Project Diagnostics - -View a summary of all errors and warnings reported by the language server. - -![Using the outline panel while viewing project diagnostics multi-buffer](https://zed.dev/img/outline-panel/project-diagnostics.png) - -### Find All References - -Quickly navigate through all references when using the `editor: find all references` action. - -![Using the outline panel while viewing `find all references` multi-buffer](https://zed.dev/img/outline-panel/find-all-references.png) - -The outline view provides a great way to quickly navigate to specific parts of your code and helps you maintain context when working with large result sets in multi-buffers. diff --git a/docs/src/performance.md b/docs/src/performance.md deleted file mode 100644 index 544e39e94b..0000000000 --- a/docs/src/performance.md +++ /dev/null @@ -1,93 +0,0 @@ -How to use our internal tools to profile and keep Zed fast. - -# Rough quick CPU profiling (Flamechart) - -See what the CPU spends the most time on. Strongly recommend you use -[samply](https://github.com/mstange/samply). It opens an interactive profile in -the browser (specifically a local instance of [firefox_profiler](https://profiler.firefox.com/)). - -See [samply](https://github.com/mstange/samply)'s README on how to install and run. - -The profile.json does not contain any symbols. Firefox profiler can add the local symbols to the profile for for. To do that hit the upload local profile button in the top right corner. - -image - -# In depth CPU profiling (Tracing) - -See how long each annotated function call took and its arguments (if -configured). - -Annotate any function you need appear in the profile with instrument. For more -details see -[tracing-instrument](https://docs.rs/tracing/latest/tracing/attr.instrument.html): - -```rust -#[instrument(skip_all)] -fn should_appear_in_profile(kitty: Cat) { - sleep(QUITE_LONG) -} -``` - -Then either compile Zed with `ZTRACING=1 cargo r --features tracy --release`. The release build is optional but highly recommended as like every program Zeds performance characteristics change dramatically with optimizations. You do not want to chase slowdowns that do not exist in release. - -## One time Setup/Building the profiler: - -Download the profiler: -[linux x86_64](https://zed-tracy-import-miniprofiler.nyc3.digitaloceanspaces.com/tracy-profiler-linux-x86_64) -[macos aarch64](https://zed-tracy-import-miniprofiler.nyc3.digitaloceanspaces.com/tracy-profiler-0.13.0-macos-aarch64) - -### Alternative: Building it yourself - -- Clone the repo at git@github.com:wolfpld/tracy.git -- `cd profiler && mkdir build && cd build` -- Run cmake to generate build files: `cmake -G Ninja -DCMAKE_BUILD_TYPE=Release ..` -- Build the profiler: `ninja` -- [Optional] move the profiler somewhere nice like ~/.local/bin on linux - -## Usage - -Open the profiler (tracy-profiler), you should see zed in the list of `Discovered clients` click it. -image - -To find functions that take a long time follow this image: -image - -# Task/Async profiling - -Get a profile of the zed foreground executor and background executors. Check if -anything is blocking the foreground too long or taking too much (clock) time in -the background. - -The profiler always runs in the background. You can save a trace from its UI or -look at the results live. - -## Setup/Building the importer: - -Download the importer -[linux x86_64](https://zed-tracy-import-miniprofiler.nyc3.digitaloceanspaces.com/tracy-import-miniprofiler-linux-x86_64) -[mac aarch64](https://zed-tracy-import-miniprofiler.nyc3.digitaloceanspaces.com/tracy-import-miniprofiler-macos-aarch64) - -### Alternative: Building it yourself - -- Clone the repo at git@github.com:zed-industries/tracy.git on v0.12.2 branch -- `cd import && mkdir build && cd build` -- Run cmake to generate build files: `cmake -G Ninja -DCMAKE_BUILD_TYPE=Release ..` -- Build the importer: `ninja` -- Run the importer on the trace file: `./tracy-import-miniprofiler /path/to/trace.miniprof /path/to/output.tracy` -- Open the trace in tracy: - - If you're on windows download the v0.12.2 version from the releases on the upstream repo - - If you're on other platforms open it on the website: https://tracy.nereid.pl/ (the version might mismatch so your luck might vary, we need to host our own ideally..) - -## To Save a Trace: - -- Run the action: `zed open performance profiler` -- Hit the save button. This opens a save dialog or if that fails to open the trace gets saved in your working directory. -- Convert the profile so it can be imported in tracy using the importer: `./tracy-import-miniprofiler output.tracy` -- Go to hit the 'power button' in the top left and then open saved trace. -- Now zoom in to see the tasks and how long they took - -# Warn if function is slow - -```rust -let _timer = zlog::time!("my_function_name").warn_if_gt(std::time::Duration::from_millis(100)); -``` diff --git a/docs/src/quick-start.md b/docs/src/quick-start.md deleted file mode 100644 index 05cf8c1fd0..0000000000 --- a/docs/src/quick-start.md +++ /dev/null @@ -1 +0,0 @@ -# Quick Start diff --git a/docs/src/remote-development.md b/docs/src/remote-development.md deleted file mode 100644 index c25d160a17..0000000000 --- a/docs/src/remote-development.md +++ /dev/null @@ -1,255 +0,0 @@ -# Remote Development - -Remote Development allows you to code at the speed of thought, even when your codebase is not on your local machine. You use Zed locally so the UI is immediately responsive, but offload heavy computation to the development server so that you can work effectively. - -## Overview - -Remote development requires two computers, your local machine that runs the Zed UI and the remote server which runs a Zed headless server. The two communicate over SSH, so you will need to be able to SSH from your local machine into the remote server to use this feature. - -![Architectural overview of Zed Remote Development](https://zed.dev/img/remote-development/diagram.png) - -On your local machine, Zed runs its UI, talks to language models, uses Tree-sitter to parse and syntax-highlight code, and store unsaved changes and recent projects. The source code, language servers, tasks, and the terminal all run on the remote server. - -> **Note:** The original version of remote development sent traffic via Zed's servers. As of Zed v0.157 you can no-longer use that mode. - -## Setup - -1. Download and install the latest [Zed](https://zed.dev/releases). You need at least Zed v0.159. -1. Use {#kb projects::OpenRemote} to open the "Remote Projects" dialog. -1. Click "Connect New Server" and enter the command you use to SSH into the server. See [Supported SSH options](#supported-ssh-options) for options you can pass. -1. Your local machine will attempt to connect to the remote server using the `ssh` binary on your path. Assuming the connection is successful, Zed will download the server on the remote host and start it. -1. Once the Zed server is running, you will be prompted to choose a path to open on the remote server. - > **Note:** Zed does not currently handle opening very large directories (for example, `/` or `~` that may have >100,000 files) very well. We are working on improving this, but suggest in the meantime opening only specific projects, or subfolders of very large mono-repos. - -For simple cases where you don't need any SSH arguments, you can run `zed ssh://[@][:]/` to open a remote folder/file directly. If you'd like to hotlink into an SSH project, use a link of the format: `zed://ssh/[@][:]/`. - -## Supported platforms - -The remote machine must be able to run Zed's server. The following platforms should work, though note that we have not exhaustively tested every Linux distribution: - -- macOS Catalina or later (Intel or Apple Silicon) -- Linux (x86_64 or arm64, we do not yet support 32-bit platforms) -- Windows is not yet supported as a remote server, but Windows can be used as a local machine to connect to remote servers. - -## Configuration - -The list of remote servers is stored in your settings file {#kb zed::OpenSettings}. You can edit this list using the Remote Projects dialog {#kb projects::OpenRemote}, which provides some robustness - for example it checks that the connection can be established before writing it to the settings file. - -```json [settings] -{ - "ssh_connections": [ - { - "host": "192.168.1.10", - "projects": [{ "paths": ["~/code/zed/zed"] }] - } - ] -} -``` - -Zed shells out to the `ssh` on your path, and so it will inherit any configuration you have in `~/.ssh/config` for the given host. That said, if you need to override anything you can configure the following additional options on each connection: - -```json [settings] -{ - "ssh_connections": [ - { - "host": "192.168.1.10", - "projects": [{ "paths": ["~/code/zed/zed"] }], - // any argument to pass to the ssh master process - "args": ["-i", "~/.ssh/work_id_file"], - "port": 22, // defaults to 22 - // defaults to your username on your local machine - "username": "me" - } - ] -} -``` - -There are two additional Zed-specific options per connection, `upload_binary_over_ssh` and `nickname`: - -```json [settings] -{ - "ssh_connections": [ - { - "host": "192.168.1.10", - "projects": [{ "paths": ["~/code/zed/zed"] }], - // by default Zed will download the server binary from the internet on the remote. - // When this is true, it'll be downloaded to your laptop and uploaded over SSH. - // This is useful when your remote server has restricted internet access. - "upload_binary_over_ssh": true, - // Shown in the Zed UI to help distinguish multiple hosts. - "nickname": "lil-linux" - } - ] -} -``` - -If you use the command line to open a connection to a host by doing `zed ssh://192.168.1.10/~/.vimrc`, then extra options are read from your settings file by finding the first connection that matches the host/username/port of the URL on the command line. - -Additionally it's worth noting that while you can pass a password on the command line `zed ssh://user:password@host/~`, we do not support writing a password to your settings file. If you're connecting repeatedly to the same host, you should configure key-based authentication. - -## Remote Development on Windows (SSH) - -Zed on Windows supports SSH remoting and will prompt for credentials when needed. - -If you encounter authentication issues, confirm that your SSH key agent is running (e.g., ssh-agent or your Git client's agent) and that ssh.exe is on PATH. - -### Troubleshooting SSH on Windows - -When prompted for credentials, use the graphical askpass dialog. If it doesn't appear, check for credential manager conflicts and that GUI prompts aren't blocked by your terminal. - -## WSL Support - -Zed supports opening folders inside of WSL natively on Windows. - -### Opening a local folder in WSL - -To open a local folder inside a WSL container, use the `projects: open in wsl` action and select the folder you want to open. You will be presented with a list of available WSL distributions to open the folder in. - -### Opening a folder already in WSL - -To open a folder that's already located inside of a WSL container, use the `projects: open wsl` action and select the WSL distribution. The distribution will be added to the `Remote Projects` window where you will be able to open the folder. - -## Port forwarding - -If you'd like to be able to connect to ports on your remote server from your local machine, you can configure port forwarding in your settings file. This is particularly useful for developing websites so you can load the site in your browser while working. - -```json [settings] -{ - "ssh_connections": [ - { - "host": "192.168.1.10", - "port_forwards": [{ "local_port": 8080, "remote_port": 80 }] - } - ] -} -``` - -This will cause requests from your local machine to `localhost:8080` to be forwarded to the remote machine's port 80. Under the hood this uses the `-L` argument to ssh. - -By default these ports are bound to localhost, so other computers in the same network as your development machine cannot access them. You can set the local_host to bind to a different interface, for example, 0.0.0.0 will bind to all local interfaces. - -```json [settings] -{ - "ssh_connections": [ - { - "host": "192.168.1.10", - "port_forwards": [ - { - "local_port": 8080, - "remote_port": 80, - "local_host": "0.0.0.0" - } - ] - } - ] -} -``` - -These ports also default to the `localhost` interface on the remote host. If you need to change this, you can also set the remote host: - -```json [settings] -{ - "ssh_connections": [ - { - "host": "192.168.1.10", - "port_forwards": [ - { - "local_port": 8080, - "remote_port": 80, - "remote_host": "docker-host" - } - ] - } - ] -} -``` - -## Zed settings - -When opening a remote project there are three relevant settings locations: - -- The local Zed settings (in `~/.zed/settings.json` on macOS or `~/.config/zed/settings.json` on Linux) on your local machine. -- The server Zed settings (in the same place) on the remote server. -- The project settings (in `.zed/settings.json` or `.editorconfig` of your project) - -Both the local Zed and the server Zed read the project settings, but they are not aware of the other's main `settings.json`. - -Which settings file you should use depends on the kind of setting you want to make: - -- Project settings should be used for things that affect the project: indentation settings, which formatter / language server to use, etc. -- Server settings should be used for things that affect the server: paths to language servers, proxy settings, etc. -- Local settings should be used for things that affect the UI: font size, etc. - -In addition any extensions you have installed locally will be propagated to the remote server. This means that language servers, etc. will run correctly. - -## Proxy Configuration - -The remote server will not use your local machine's proxy configuration because they may be under different network policies. If your remote server requires a proxy to access the internet, you must configure it on the remote server itself. - -In most cases, your remote server will already have proxy environment variables configured. Zed will automatically use them when downloading language servers, communicating with LLM models, etc. - -If needed, you can set these environment variables in the server's shell configuration (e.g., `~/.bashrc`): - -```bash -export http_proxy="http://proxy.example.com:8080" -export https_proxy="http://proxy.example.com:8080" -export no_proxy="localhost,127.0.0.1" -``` - -Alternatively, you can configure the proxy in the remote machine's `~/.config/zed/settings.json` (Linux) or `~/.zed/settings.json` (macOS): - -```json -{ - "proxy": "http://proxy.example.com:8080" -} -``` - -See the [proxy documentation](./configuring-zed.md#network-proxy) for supported proxy types and additional configuration options. - -## Initializing the remote server - -Once you provide the SSH options, Zed shells out to `ssh` on your local machine to create a ControlMaster connection with the options you provide. - -Any prompts that SSH needs will be shown in the UI, so you can verify host keys, type key passwords, etc. - -Once the master connection is established, Zed will check to see if the remote server binary is present in `~/.zed_server` on the remote, and that its version matches the current version of Zed that you're using. - -If it is not there or the version mismatches, Zed will try to download the latest version. By default, it will download from `https://zed.dev` directly, but if you set: `{"upload_binary_over_ssh":true}` in your settings for that server, it will download the binary to your local machine and then upload it to the remote server. - -If you'd like to maintain the server binary yourself you can. You can either download our prebuilt versions from [GitHub](https://github.com/zed-industries/zed/releases), or [build your own](https://zed.dev/docs/development) with `cargo build -p remote_server --release`. If you do this, you must upload it to `~/.zed_server/zed-remote-server-{RELEASE_CHANNEL}-{VERSION}` on the server, for example `~/.zed_server/zed-remote-server-stable-0.181.6`. The version must exactly match the version of Zed itself you are using. - -## Maintaining the SSH connection - -Once the server is initialized. Zed will create new SSH connections (reusing the existing ControlMaster) to run the remote development server. - -Each connection tries to run the development server in proxy mode. This mode will start the daemon if it is not running, and reconnect to it if it is. This way when your connection drops and is restarted, you can continue to work without interruption. - -In the case that reconnecting fails, the daemon will not be re-used. That said, unsaved changes are by default persisted locally, so that you do not lose work. You can always reconnect to the project at a later date and Zed will restore unsaved changes. - -If you are struggling with connection issues, you should be able to see more information in the Zed log `cmd-shift-p Open Log`. If you are seeing things that are unexpected, please file a [GitHub issue](https://github.com/zed-industries/zed/issues/new) or reach out in the #remoting-feedback channel in the [Zed Discord](https://zed.dev/community-links). - -## Supported SSH Options - -Under the hood, Zed shells out to the `ssh` binary to connect to the remote server. We create one SSH control master per project, and then use that to multiplex SSH connections for the Zed protocol itself, any terminals you open and tasks you run. We read settings from your SSH config file, but if you want to specify additional options to the SSH control master you can configure Zed to set them. - -When typing in the "Connect New Server" dialog, you can use bash-style quoting to pass options containing a space. Once you have created a server it will be added to the `"ssh_connections": []` array in your settings file. You can edit the settings file directly to make changes to SSH connections. - -Supported options: - -- `-p` / `-l` - these are equivalent to passing the port and the username in the host string. -- `-L` / `-R` for port forwarding -- `-i` - to use a specific key file -- `-o` - to set custom options -- `-J` / `-w` - to proxy the SSH connection -- `-F` for specifying an `ssh_config` -- And also... `-4`, `-6`, `-A`, `-B`, `-C`, `-D`, `-I`, `-K`, `-P`, `-X`, `-Y`, `-a`, `-b`, `-c`, `-i`, `-k`, `-l`, `-m`, `-o`, `-p`, `-w`, `-x`, `-y` - -Note that we deliberately disallow some options (for example `-t` or `-T`) that Zed will set for you. - -## Known Limitations - -- You can't open files from the remote Terminal by typing the `zed` command. - -## Feedback - -Please join the #remoting-feedback channel in the [Zed Discord](https://zed.dev/community-links). diff --git a/docs/src/repl.md b/docs/src/repl.md deleted file mode 100644 index 692093007c..0000000000 --- a/docs/src/repl.md +++ /dev/null @@ -1,183 +0,0 @@ -# REPL - -## Getting started - -Bring the power of [Jupyter kernels](https://docs.jupyter.org/en/latest/projects/kernels.html) to your editor! The built-in REPL for Zed allows you to run code interactively in your editor similarly to a notebook with your own text files. - -
- -
- -## Installation - -Zed supports running code in multiple languages. To get started, you need to install a kernel for the language you want to use. - -**Currently supported languages:** - -- [Python (ipykernel)](#python) -- [TypeScript (Deno)](#typescript-deno) -- [R (Ark)](#r-ark) -- [R (Xeus)](#r-xeus) -- [Julia](#julia) -- [Scala (Almond)](#scala) - -Once installed, you can start using the REPL in the respective language files, or other places those languages are supported, such as Markdown. If you recently added the kernels, run the `repl: refresh kernelspecs` command to make them available in the editor. - -## Using the REPL - -To start the REPL, open a file with the language you want to use and use the `repl: run` command (defaults to `ctrl-shift-enter` on macOS) to run a block, selection, or line. You can also click on the REPL icon in the toolbar. - -The `repl: run` command will be executed on your selection(s), and the result will be displayed below the selection. - -Outputs can be cleared with the `repl: clear outputs` command, or from the REPL menu in the toolbar. - -### Cell mode - -Zed supports [notebooks as scripts](https://jupytext.readthedocs.io/en/latest/formats-scripts.html) using the `# %%` cell separator in Python and `// %%` in TypeScript. This allows you to write code in a single file and run it as if it were a notebook, cell by cell. - -The `repl: run` command will run each block of code between the `# %%` markers as a separate cell. - -```python -# %% Cell 1 -import time -import numpy as np - -# %% Cell 2 -import matplotlib.pyplot as plt -import matplotlib.pyplot as plt -from matplotlib import style -style.use('ggplot') -``` - -## Language specific instructions - -### Python {#python} - -#### Global environment - -
- -On macOS, your system Python will _not_ work. Either set up [pyenv](https://github.com/pyenv/pyenv?tab=readme-ov-file#installation) or use a virtual environment. - -
- -To setup your current Python to have an available kernel, run: - -```sh -pip install ipykernel -python -m ipykernel install --user -``` - -#### Conda Environment - -```sh -source activate myenv -conda install ipykernel -python -m ipykernel install --user --name myenv --display-name "Python (myenv)" -``` - -#### Virtualenv with pip - -```sh -source activate myenv -pip install ipykernel -python -m ipykernel install --user --name myenv --display-name "Python (myenv)" -``` - -### R (Ark Kernel) {#r-ark} - -Install [Ark](https://github.com/posit-dev/ark/releases) by downloading the release for your operating system. For example, for macOS just unpack `ark` binary and put it into `/usr/local/bin`. Then run: - -```sh -ark --install -``` - -### R (Xeus Kernel) {#r-xeus} - -- Install [Xeus-R](https://github.com/jupyter-xeus/xeus-r) -- Install the R Extension for Zed (search for `R` in Zed Extensions) - - - -### TypeScript: Deno {#typescript-deno} - -- [Install Deno](https://docs.deno.com/runtime/manual/getting_started/installation/) and then install the Deno jupyter kernel: - -```sh -deno jupyter --install -``` - - - -### Julia - -- Download and install Julia from the [official website](https://julialang.org/downloads/). -- Install the Julia Extension for Zed (search for `Julia` in Zed Extensions) - - - -### Scala - -- [Install Scala](https://www.scala-lang.org/download/) with `cs setup` (Coursier): - - `brew install coursier/formulas/coursier && cs setup` -- REPL (Almond) [setup instructions](https://almond.sh/docs/quick-start-install): - - `brew install --cask temurin` (Eclipse foundation official OpenJDK binaries) - - `brew install coursier/formulas/coursier && cs setup` - - `coursier launch --use-bootstrap almond -- --install` - -## Changing which kernel is used per language {#changing-kernels} - -Zed automatically detects the available kernels on your system. If you need to configure a different default kernel for a -language, you can assign a kernel for any supported language in your `settings.json`. - -```json [settings] -{ - "jupyter": { - "kernel_selections": { - "python": "conda-env", - "typescript": "deno", - "javascript": "deno", - "r": "ark" - } - } -} -``` - -## Debugging Kernelspecs - -Available kernels are shown via the `repl: sessions` command. To refresh the kernels you can run, use the `repl: refresh kernelspecs` command. - -If you have `jupyter` installed, you can run `jupyter kernelspec list` to see the available kernels. - -```sh -$ jupyter kernelspec list -Available kernels: - ark /Users/z/Library/Jupyter/kernels/ark - conda-base /Users/z/Library/Jupyter/kernels/conda-base - deno /Users/z/Library/Jupyter/kernels/deno - python-chatlab-dev /Users/z/Library/Jupyter/kernels/python-chatlab-dev - python3 /Users/z/Library/Jupyter/kernels/python3 - ruby /Users/z/Library/Jupyter/kernels/ruby - rust /Users/z/Library/Jupyter/kernels/rust -``` - -> Note: Zed makes best effort usage of `sys.prefix` and `CONDA_PREFIX` to find kernels in Python environments. If you want explicitly control run `python -m ipykernel install --user --name myenv --display-name "Python (myenv)"` to install the kernel directly while in the environment. diff --git a/docs/src/snippets.md b/docs/src/snippets.md deleted file mode 100644 index e84210d0fa..0000000000 --- a/docs/src/snippets.md +++ /dev/null @@ -1,59 +0,0 @@ -# Snippets - -Use the {#action snippets::ConfigureSnippets} action to create a new snippets file or edit an existing snippets file for a specified [scope](#scopes). - -The snippets are located in `~/.config/zed/snippets` directory to which you can navigate to with the {#action snippets::OpenFolder} action. - -## Example configuration - -```json [settings] -{ - // Each snippet must have a name and body, but the prefix and description are optional. - // The prefix is used to trigger the snippet, but when omitted then the name is used. - // Use placeholders like $1, $2 or ${1:defaultValue} to define tab stops. - // The $0 determines the final cursor position. - // Placeholders with the same value are linked. - "Log to console": { - "prefix": "log", - "body": ["console.info(\"Hello, ${1:World}!\")", "$0"], - "description": "Logs to console" - } -} -``` - -## Scopes - -The scope is determined by the language name in lowercase e.g. `python.json` for Python, `shell script.json` for Shell Script, but there are some exceptions to this rule: - -| Scope | Filename | -| ---------- | --------------- | -| Global | snippets.json | -| JSX | javascript.json | -| Plain Text | plaintext.json | - -To create JSX snippets you have to use `javascript.json` snippets file, instead of `jsx.json`, but this does not apply to TSX and TypeScript which follow the above rule. - -## Known Limitations - -- Only the first prefix is used when a list of prefixes is passed in. -- Currently only the `json` snippet file format is supported, even though the `simple-completion-language-server` supports both `json` and `toml` file formats. - -## See also - -The `feature_paths` option in `simple-completion-language-server` is disabled by default. - -If you want to enable it you can add the following to your `settings.json`: - -```json [settings] -{ - "lsp": { - "snippet-completion-server": { - "settings": { - "feature_paths": true - } - } - } -} -``` - -For more configuration information, see the [`simple-completion-language-server` instructions](https://github.com/zed-industries/simple-completion-language-server/tree/main). diff --git a/docs/src/tab-switcher.md b/docs/src/tab-switcher.md deleted file mode 100644 index 5cc72be449..0000000000 --- a/docs/src/tab-switcher.md +++ /dev/null @@ -1,46 +0,0 @@ -# Tab Switcher - -The Tab Switcher provides a quick way to navigate between open tabs in Zed. It -displays a list of your open tabs sorted by recent usage, making it easy to jump -back to whatever you were just working on. - -![Tab Switcher with multiple panes](https://zed.dev/img/features/tab-switcher.png) - -## Quick Switching - -When the Tab Switcher is opened using {#kb tab_switcher::Toggle}, instead of -running the {#action tab_switcher::Toggle} from the command palette, it'll stay -active as long as the ctrl key is held down. - -While holding down ctrl, each subsequent tab press cycles to the next item (shift to cycle backwards) and, when ctrl is released, the selected item is confirmed and -the switcher is closed. - -## Opening the Tab Switcher - -The Tab Switcher can also be opened with either {#action tab_switcher::Toggle} -or {#action tab_switcher::ToggleAll}. Using {#kb tab_switcher::Toggle} will show -only the tabs for the current pane, while {#kb tab_switcher::ToggleAll} shows -all tabs for all panes. - -While the Tab Switcher is open, you can: - -- Press {#kb menu::SelectNext} to move to the next tab in the list -- Press {#kb menu::SelectPrevious} to move to the previous tab -- Press enter to confirm the selected tab and close the switcher -- Press escape to close the switcher and return to the original tab from which - the switcher was opened -- Press {#kb tab_switcher::CloseSelectedItem} to close the currently selected tab - -As you navigate through the list, Zed will update the pane's active item to -match the selected tab. - -## Action Reference - -| Action | Description | -| ----------------------------------------- | ------------------------------------------------- | -| {#action tab_switcher::Toggle} | Open the Tab Switcher for the current pane | -| {#action tab_switcher::ToggleAll} | Open the Tab Switcher showing tabs from all panes | -| {#action tab_switcher::CloseSelectedItem} | Close the selected tab in the Tab Switcher | diff --git a/docs/src/tasks.md b/docs/src/tasks.md deleted file mode 100644 index a11988d9a0..0000000000 --- a/docs/src/tasks.md +++ /dev/null @@ -1,243 +0,0 @@ -# Tasks - -Zed supports ways to spawn (and rerun) commands using its integrated terminal to output the results. These commands can read a limited subset of Zed state (such as a path to the file currently being edited or selected text). - -```json [tasks] -[ - { - "label": "Example task", - "command": "for i in {1..5}; do echo \"Hello $i/5\"; sleep 1; done", - //"args": [], - // Env overrides for the command, will be appended to the terminal's environment from the settings. - "env": { "foo": "bar" }, - // Current working directory to spawn the command into, defaults to current project root. - //"cwd": "/path/to/working/directory", - // Whether to use a new terminal tab or reuse the existing one to spawn the process, defaults to `false`. - "use_new_terminal": false, - // Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`. - "allow_concurrent_runs": false, - // What to do with the terminal pane and tab, after the command was started: - // * `always` — always show the task's pane, and focus the corresponding tab in it (default) - // * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it - // * `never` — do not alter focus, but still add/reuse the task's tab in its pane - "reveal": "always", - // What to do with the terminal pane and tab, after the command has finished: - // * `never` — Do nothing when the command finishes (default) - // * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it - // * `on_success` — hide the terminal tab on task success only, otherwise behaves similar to `always` - "hide": "never", - // Which shell to use when running a task inside the terminal. - // May take 3 values: - // 1. (default) Use the system's default terminal configuration in /etc/passwd - // "shell": "system" - // 2. A program: - // "shell": { - // "program": "sh" - // } - // 3. A program with arguments: - // "shell": { - // "with_arguments": { - // "program": "/bin/bash", - // "args": ["--login"] - // } - // } - "shell": "system", - // Whether to show the task line in the output of the spawned task, defaults to `true`. - "show_summary": true, - // Whether to show the command line in the output of the spawned task, defaults to `true`. - "show_command": true - // Represents the tags for inline runnable indicators, or spawning multiple tasks at once. - // "tags": [] - } -] -``` - -There are two actions that drive the workflow of using tasks: `task: spawn` and `task: rerun`. -`task: spawn` opens a modal with all available tasks in the current file. -`task: rerun` reruns the most recently spawned task. You can also rerun tasks from the task modal. - -By default, rerunning tasks reuses the same terminal (due to the `"use_new_terminal": false` default) but waits for the previous task to finish before starting (due to the `"allow_concurrent_runs": false` default). - -Keep `"use_new_terminal": false` and set `"allow_concurrent_runs": true` to allow cancelling previous tasks on rerun. - -## Task templates - -Tasks can be defined: - -- in the global `tasks.json` file; such tasks are available in all Zed projects you work on. This file is usually located in `~/.config/zed/tasks.json`. You can edit them by using the `zed: open tasks` action. -- in the worktree-specific (local) `.zed/tasks.json` file; such tasks are available only when working on a project with that worktree included. You can edit worktree-specific tasks by using the `zed: open project tasks` action. -- on the fly with [oneshot tasks](#oneshot-tasks). These tasks are project-specific and do not persist across sessions. -- by language extension. - -## Variables - -Zed tasks act just like your shell; that also means that you can reference environmental variables via sh-esque `$VAR_NAME` syntax. A couple of additional environmental variables are set for your convenience. -These variables allow you to pull information from the current editor and use it in your tasks. The following variables are available: - -- `ZED_COLUMN`: current line column -- `ZED_ROW`: current line row -- `ZED_FILE`: absolute path of the currently opened file (e.g. `/Users/my-user/path/to/project/src/main.rs`) -- `ZED_FILENAME`: filename of the currently opened file (e.g. `main.rs`) -- `ZED_DIRNAME`: absolute path of the currently opened file with file name stripped (e.g. `/Users/my-user/path/to/project/src`) -- `ZED_RELATIVE_FILE`: path of the currently opened file, relative to `ZED_WORKTREE_ROOT` (e.g. `src/main.rs`) -- `ZED_RELATIVE_DIR`: path of the currently opened file's directory, relative to `ZED_WORKTREE_ROOT` (e.g. `src`) -- `ZED_STEM`: stem (filename without extension) of the currently opened file (e.g. `main`) -- `ZED_SYMBOL`: currently selected symbol; should match the last symbol shown in a symbol breadcrumb (e.g. `mod tests > fn test_task_contexts`) -- `ZED_SELECTED_TEXT`: currently selected text -- `ZED_WORKTREE_ROOT`: absolute path to the root of the current worktree. (e.g. `/Users/my-user/path/to/project`) -- `ZED_CUSTOM_RUST_PACKAGE`: (Rust-specific) name of the parent package of $ZED_FILE source file. - -To use a variable in a task, prefix it with a dollar sign (`$`): - -```json [settings] -{ - "label": "echo current file's path", - "command": "echo $ZED_FILE" -} -``` - -You can also use verbose syntax that allows specifying a default if a given variable is not available: `${ZED_FILE:default_value}` - -These environmental variables can also be used in tasks' `cwd`, `args`, and `label` fields. - -### Variable Quoting - -When working with paths containing spaces or other special characters, please ensure variables are properly escaped. - -For example, instead of this (which will fail if the path has a space): - -```json [settings] -{ - "label": "stat current file", - "command": "stat $ZED_FILE" -} -``` - -Provide the following: - -```json [settings] -{ - "label": "stat current file", - "command": "stat", - "args": ["$ZED_FILE"] -} -``` - -Or explicitly include escaped quotes like so: - -```json [settings] -{ - "label": "stat current file", - "command": "stat \"$ZED_FILE\"" -} -``` - -### Task filtering based on variables - -Task definitions with variables which are not present at the moment the task list is determined are filtered out. -For example, the following task will appear in the spawn modal only if there is a text selection: - -```json [settings] -{ - "label": "selected text", - "command": "echo \"$ZED_SELECTED_TEXT\"" -} -``` - -Set default values to such variables to have such tasks always displayed: - -```json [settings] -{ - "label": "selected text with default", - "command": "echo \"${ZED_SELECTED_TEXT:no text selected}\"" -} -``` - -## Oneshot tasks - -The same task modal opened via `task: spawn` supports arbitrary bash-like command execution: type a command inside the modal text field, and use `opt-enter` to spawn it. - -The task modal persists these ad-hoc commands for the duration of the session, `task: rerun` will also rerun such tasks if they were the last ones spawned. - -You can also adjust the currently selected task in a modal (`tab` is the default key binding). Doing so will put its command into a prompt that can then be edited & spawned as a oneshot task. - -### Ephemeral tasks - -You can use the `cmd` modifier when spawning a task via a modal; tasks spawned this way will not have their usage count increased (thus, they will not be respawned with `task: rerun` and they won't have a high rank in the task modal). -The intended use of ephemeral tasks is to stay in the flow with continuous `task: rerun` usage. - -### More task rerun control - -By default, tasks capture their variables into a context once, and this "resolved task" is being rerun always. - -This can be controlled with the `"reevaluate_context"` argument to the task: setting it to `true` will force the task to be reevaluated before each run. - -```json [keymap] -{ - "context": "Workspace", - "bindings": { - "alt-t": ["task::Rerun", { "reevaluate_context": true }] - } -} -``` - -## Custom keybindings for tasks - -You can define your own keybindings for your tasks via an additional argument to `task::Spawn`. If you wanted to bind the aforementioned `echo current file's path` task to `alt-g`, you would add the following snippet in your [`keymap.json`](./key-bindings.md) file: - -```json [keymap] -{ - "context": "Workspace", - "bindings": { - "alt-g": ["task::Spawn", { "task_name": "echo current file's path" }] - } -} -``` - -Note that these tasks can also have a 'target' specified to control where the spawned task should show up. -This could be useful for launching a terminal application that you want to use in the center area: - -```json [tasks] -// In tasks.json -{ - "label": "start lazygit", - "command": "lazygit -p $ZED_WORKTREE_ROOT" -} -``` - -```json [keymap] -// In keymap.json -{ - "context": "Workspace", - "bindings": { - "alt-g": [ - "task::Spawn", - { "task_name": "start lazygit", "reveal_target": "center" } - ] - } -} -``` - -## Binding runnable tags to task templates - -Zed supports overriding the default action for inline runnable indicators via workspace-local and global `tasks.json` file with the following precedence hierarchy: - -1. Workspace `tasks.json` -2. Global `tasks.json` -3. Language-provided tag bindings (default). - -To tag a task, add the runnable tag name to the `tags` field on the task template: - -```json [settings] -{ - "label": "echo current file's path", - "command": "echo $ZED_FILE", - "tags": ["rust-test"] -} -``` - -In doing so, you can change which task is shown in the runnables indicator. - -## Keybindings to run tasks bound to runnables - -When you have a task definition that is bound to the runnable, you can quickly run it using [Code Actions](https://zed.dev/docs/configuring-languages?#code-actions) that you can trigger either via `editor: Toggle Code Actions` command or by the `cmd-.`/`ctrl-.` shortcut. Your task will be the first in the dropdown. The task will run immediately if there are no additional Code Actions for this line. diff --git a/docs/src/telemetry.md b/docs/src/telemetry.md deleted file mode 100644 index 8dca8c1ee6..0000000000 --- a/docs/src/telemetry.md +++ /dev/null @@ -1,61 +0,0 @@ -# Telemetry in Zed - -Zed collects anonymous telemetry data to help the team understand how people are using the application and to see what sort of issues they are experiencing. - -## Configuring Telemetry Settings - -You have full control over what data is sent out by Zed. -To enable or disable some or all telemetry types, open your `settings.json` file via {#action zed::OpenSettings}({#kb zed::OpenSettings}) from the command palette. - -Insert and tweak the following: - -```json [settings] -"telemetry": { - "diagnostics": false, - "metrics": false -}, -``` - -## Dataflow - -Telemetry is sent from the application to our servers. Data is proxied through our servers to enable us to easily switch analytics services. We currently use: - -- [Sentry](https://sentry.io): Crash-monitoring service - stores diagnostic events -- [Snowflake](https://snowflake.com): Data warehouse - stores both diagnostic and metric events -- [Hex](https://www.hex.tech): Dashboards and data exploration - accesses data stored in Snowflake -- [Amplitude](https://www.amplitude.com): Dashboards and data exploration - accesses data stored in Snowflake - -## Types of Telemetry - -### Diagnostics - -Crash reports consist of a [minidump](https://learn.microsoft.com/en-us/windows/win32/debug/minidump-files) and some extra debug information. Reports are sent on the first application launch after the crash occurred. We've built dashboards that allow us to visualize the frequency and severity of issues experienced by users. Having these reports sent automatically allows us to begin implementing fixes without the user needing to file a report in our issue tracker. The plots in the dashboards also give us an informal measurement of the stability of Zed. - -You can see what extra data is sent alongside the minidump in the `Panic` struct in [crates/telemetry_events/src/telemetry_events.rs](https://github.com/zed-industries/zed/blob/main/crates/telemetry_events/src/telemetry_events.rs) in the Zed repo. You can find additional information in the [Debugging Crashes](./development/debugging-crashes.md) documentation. - -### Client-Side Usage Data {#client-metrics} - -To improve Zed and understand how it is being used in the wild, Zed optionally collects usage data like the following: - -- (a) file extensions of opened files; -- (b) features and tools You use within the Editor; -- (c) project statistics (e.g., number of files); and -- (d) frameworks detected in Your projects - -Usage Data does not include any of Your software code or sensitive project details. Metric events are reported over HTTPS, and requests are rate-limited to avoid using significant network bandwidth. - -Usage Data is associated with a secure random telemetry ID which may be linked to Your email address. This linkage currently serves two purposes: (1) it allows Zed to analyze usage patterns over time while maintaining Your privacy; and (2) it enables Zed to reach out to specific user groups for feedback and improvement suggestions. - -You can audit the metrics data that Zed has reported by running the command {#action zed::OpenTelemetryLog} from the command palette, or clicking `Help > View Telemetry Log` in the application menu. - -You can see the full list of the event types and exactly the data sent for each by inspecting the `Event` enum and the associated structs in [crates/telemetry_events/src/telemetry_events.rs](https://github.com/zed-industries/zed/blob/main/crates/telemetry_events/src/telemetry_events.rs) in the Zed repository. - -### Server-Side Usage Data {#metrics} - -When using Zed's hosted services, we may collect, generate, and Process data to allow us to support users and improve our hosted offering. Examples include metadata around rate limiting and billing metrics/token usage. Zed does not persistently store user content or use user content to evaluate and/or improve our AI features, unless it is explicitly shared with Zed, and we have a zero-data retention agreement with Anthropic. - -You can see more about our stance on data collection (and that any prompt data shared with Zed is explicitly opt-in) at [AI Improvement](./ai/ai-improvement.md). - -## Concerns and Questions - -If you have concerns about telemetry, please feel free to [open an issue](https://github.com/zed-industries/zed/issues/new/choose). diff --git a/docs/src/themes.md b/docs/src/themes.md deleted file mode 100644 index 615cd2c7b3..0000000000 --- a/docs/src/themes.md +++ /dev/null @@ -1,82 +0,0 @@ -# Themes - -Zed comes with a number of built-in themes, with more themes available as extensions. - -## Selecting a Theme - -See what themes are installed and preview them via the Theme Selector, which you can open from the command palette with `theme selector: toggle` (bound to {#kb theme_selector::Toggle}). - -Navigating through the theme list by moving up and down will change the theme in real time and hitting enter will save it to your settings file. - -## Installing more Themes - -More themes are available from the Extensions page, which you can access via the command palette with `zed: extensions` or the [Zed website](https://zed.dev/extensions?filter=themes). - -Many popular themes have been ported to Zed, and if you're struggling to choose one, visit [zed-themes.com](https://zed-themes.com), a third-party gallery with visible previews for many of them. - -## Configuring a Theme - -Your selected theme is stored in your settings file. -You can open your settings file from the command palette with {#action zed::OpenSettingsFile} (bound to {#kb zed::OpenSettingsFile}). - -By default, Zed maintains two themes: one for light mode and one for dark mode. -You can set the mode to `"dark"` or `"light"` to ignore the current system mode. - -```json [settings] -{ - "theme": { - "mode": "system", - "light": "One Light", - "dark": "One Dark" - } -} -``` - -## Theme Overrides - -To override specific attributes of a theme, use the `theme_overrides` setting. -This setting can be used to configure theme-specific overrides. - -For example, add the following to your `settings.json` if you wish to override the background color of the editor and display comments and doc comments as italics: - -```json [settings] -{ - "theme_overrides": { - "One Dark": { - "editor.background": "#333", - "syntax": { - "comment": { - "font_style": "italic" - }, - "comment.doc": { - "font_style": "italic" - } - }, - "accents": [ - "#ff0000", - "#ff7f00", - "#ffff00", - "#00ff00", - "#0000ff", - "#8b00ff" - ] - } - } -} -``` - -To see a comprehensive list of list of captures (like `comment` and `comment.doc`) see [Language Extensions: Syntax highlighting](./extensions/languages.md#syntax-highlighting). - -To see a list of available theme attributes look at the JSON file for your theme. -For example, [assets/themes/one/one.json](https://github.com/zed-industries/zed/blob/main/assets/themes/one/one.json) for the default One Dark and One Light themes. - -## Local Themes - -Store new themes locally by placing them in the `~/.config/zed/themes` directory (macOS and Linux) or `%USERPROFILE%\AppData\Roaming\Zed\themes\` (Windows). - -For example, to create a new theme called `my-cool-theme`, create a file called `my-cool-theme.json` in that directory. -It will be available in the theme selector the next time Zed loads. - -## Theme Development - -See: [Developing Zed Themes](./extensions/themes.md) diff --git a/docs/src/toolchains.md b/docs/src/toolchains.md deleted file mode 100644 index f9f5f3fe0e..0000000000 --- a/docs/src/toolchains.md +++ /dev/null @@ -1,31 +0,0 @@ -# Toolchains - -Zed projects offer a dedicated UI for toolchain selection, which lets you pick a set of tools for working with a given language in a current project. - -Imagine you're working with Python project, which has virtual environments that encapsulate a set of dependencies of your project along with a suitable interpreter to run it with. The language server has to know which virtual environment you're working with, as it uses it to understand your project's code. -With toolchain selector, you don't need to spend time configuring your language server to point it at the right virtual environment directory—you can just select the right virtual environment (toolchain) from a dropdown. - -You can even select different toolchains for different subprojects within your Zed project. A definition of a subproject is language-specific. -In collaborative scenarios, only the project owner can see and modify an active toolchain. - -In [remote projects](./remote-development.md), you can use the toolchain selector to control the active toolchain on the SSH host. When [sharing your project](./collaboration/overview.md), the toolchain selector is not available to guests. - -## Why do we need toolchains? - -The active toolchain is relevant for launching language servers, which may need it to function properly—it may not be able to resolve dependencies, which in turn may make functionalities like "Go to definition" or "Code completions" unavailable. - -The active toolchain is also relevant when launching a shell in the terminal panel: some toolchains provide "activation scripts" for shells, which make those toolchains available in the shell environment for your convenience. Zed will run these activation scripts automatically when you create a new terminal. - -This also applies to [tasks](./tasks.md)—Zed tasks behave "as if" you opened a new terminal tab and ran a given task invocation yourself, which in turn means that Zed task execution is affected by the active toolchain and its activation script. - -## Selecting toolchains - -The active toolchain (if there is one) is displayed in the status bar (on the right hand side). Click on it to access the toolchain selector—you can also use an action from a command palette ({#action toolchain::Select}). - -Zed will automatically infer a set of toolchains to choose from based on the project you're working with. A default will also be selected on your behalf on a best-effort basis when you open a project for the first time. - -The toolchain selection applies to a current subproject, which—depending on the structure of your Zed project—might be your whole project or just a subset of it. For example, if you have a monorepo with multiple subprojects, you might want to select a different toolchain for each subproject. - -## Adding toolchains manually - -If automatic detection does not suffice for you, you can add toolchains manually. To do that, click on the "Add toolchain" button in the toolchain selector. From there you can provide a path to a toolchain and set a name of your liking for it. diff --git a/docs/src/troubleshooting.md b/docs/src/troubleshooting.md deleted file mode 100644 index 4aeeda6e3d..0000000000 --- a/docs/src/troubleshooting.md +++ /dev/null @@ -1,80 +0,0 @@ -# Troubleshooting - -This guide covers common troubleshooting techniques for Zed. -Sometimes you'll be able to identify and resolve issues on your own using this information. -Other times, troubleshooting means gathering the right information—logs, profiles, or reproduction steps—to help us diagnose and fix the problem. - -> **Note**: To open the command palette, use `cmd-shift-p` on macOS or `ctrl-shift-p` on Windows / Linux. - -## Retrieve Zed and System Information - -When reporting issues or seeking help, it's useful to know your Zed version and system specifications. You can retrieve this information using the following actions from the command palette: - -- {#action zed::About}: Find your Zed version number -- {#action zed::CopySystemSpecsIntoClipboard}: Populate your clipboard with Zed version number, operating system version, and hardware specs - -## Zed Log - -Often, a good first place to look when troubleshooting any issue in Zed is the Zed log, which might contain clues about what's going wrong. -You can review the most recent 1000 lines of the log by running the {#action zed::OpenLog} action from the command palette. -If you want to view the full file, you can reveal it in your operating system's native file manager via {#action zed::RevealLogInFileManager} from the command palette. - -You'll find the Zed log in the respective location on each operating system: - -- macOS: `~/Library/Logs/Zed/Zed.log` -- Windows: `C:\Users\YOU\AppData\Local\Zed\logs\Zed.log` -- Linux: `~/.local/share/zed/logs/Zed.log` or `$XDG_DATA_HOME` - -> Note: In some cases, it might be useful to monitor the log live, such as when [developing a Zed extension](https://zed.dev/docs/extensions/developing-extensions). -> Example: `tail -f ~/Library/Logs/Zed/Zed.log` - -The log may contain enough context to help you debug the issue yourself, or you may find specific errors that are useful when filing a [GitHub issue](https://github.com/zed-industries/zed/issues/new/choose) or when talking to Zed staff in our [Discord server](https://zed.dev/community-links#forums-and-discussions). - -## Performance Issues (Profiling) - -If you're running into performance issues in Zed—such as hitches, hangs, or general unresponsiveness—having a performance profile attached to your issue will help us zero in on what is getting stuck, so we can fix it. - -### macOS - -Xcode Instruments (which comes bundled with your [Xcode](https://apps.apple.com/us/app/xcode/id497799835) download) is the standard tool for profiling on macOS. - -1. With Zed running, open Instruments -1. Select `Time Profiler` as the profiling template -1. In the `Time Profiler` configuration, set the target to the running Zed process -1. Start recording -1. If the performance issue occurs when performing a specific action in Zed, perform that action now -1. Stop recording -1. Save the trace file -1. Compress the trace file into a zip archive -1. File a [GitHub issue](https://github.com/zed-industries/zed/issues/new/choose) with the trace zip attached - - - - - -## Startup and Workspace Issues - -Zed creates local SQLite databases to persist data relating to its workspace and your projects. These databases store, for instance, the tabs and panes you have open in a project, the scroll position of each open file, the list of all projects you've opened (for the recent projects modal picker), etc. You can find and explore these databases in the following locations: - -- macOS: `~/Library/Application Support/Zed/db` -- Linux and FreeBSD: `~/.local/share/zed/db` (or within `XDG_DATA_HOME` or `FLATPAK_XDG_DATA_HOME`) -- Windows: `%LOCALAPPDATA%\Zed\db` - -The naming convention of these databases takes on the form of `0-`: - -- Stable: `0-stable` -- Preview: `0-preview` -- Nightly: `0-nightly` -- Dev: `0-dev` - -While rare, we've seen a few cases where workspace databases became corrupted, which prevented Zed from starting. -If you're experiencing startup issues, you can test whether it's workspace-related by temporarily moving the database from its location, then trying to start Zed again. - -> **Note**: Moving the workspace database will cause Zed to create a fresh one. -> Your recent projects, open tabs, etc. will be reset to "factory". - -If your issue persists after regenerating the database, please [file an issue](https://github.com/zed-industries/zed/issues/new/choose). - -## Language Server Issues - -If you're experiencing language-server related issues, such as stale diagnostics or issues jumping to definitions, restarting the language server via {#action editor::RestartLanguageServer} from the command palette will often resolve the issue. diff --git a/docs/src/uninstall.md b/docs/src/uninstall.md deleted file mode 100644 index c1f71a6609..0000000000 --- a/docs/src/uninstall.md +++ /dev/null @@ -1,113 +0,0 @@ -# Uninstall - -This guide covers how to uninstall Zed on different operating systems. - -## macOS - -### Standard Installation - -If you installed Zed by downloading it from the website: - -1. Quit Zed if it's running -2. Open Finder and go to your Applications folder -3. Drag Zed to the Trash (or right-click and select "Move to Trash") -4. Empty the Trash - -### Homebrew Installation - -If you installed Zed using Homebrew, use the following command: - -```sh -brew uninstall --cask zed -``` - -Or for the preview version: - -```sh -brew uninstall --cask zed@preview -``` - -### Removing User Data (Optional) - -To completely remove all Zed configuration files and data: - -1. Open Finder -2. Press `Cmd + Shift + G` to open "Go to Folder" -3. Delete the following directories if they exist: - - `~/Library/Application Support/Zed` - - `~/Library/Saved Application State/dev.zed.Zed.savedState` - - `~/Library/Logs/Zed` - - `~/Library/Caches/dev.zed.Zed` - -## Linux - -### Standard Uninstall - -If Zed was installed using the default installation script, run: - -```sh -zed --uninstall -``` - -You'll be prompted whether to keep or delete your preferences. After making a choice, you should see a message that Zed was successfully uninstalled. - -If the `zed` command is not found in your PATH, try: - -```sh -$HOME/.local/bin/zed --uninstall -``` - -or: - -```sh -$HOME/.local/zed.app/bin/zed --uninstall -``` - -### Package Manager - -If you installed Zed using a package manager (such as Flatpak, Snap, or a distribution-specific package manager), consult that package manager's documentation for uninstallation instructions. - -### Manual Removal - -If the uninstall command fails or Zed was installed to a custom location, you can manually remove: - -- Installation directory: `~/.local/zed.app` (or your custom installation path) -- Binary symlink: `~/.local/bin/zed` -- Configuration and data: `~/.config/zed` - -## Windows - -### Standard Installation - -1. Quit Zed if it's running -2. Open Settings (Windows key + I) -3. Go to "Apps" > "Installed apps" (or "Apps & features" on Windows 10) -4. Search for "Zed" -5. Click the three dots menu next to Zed and select "Uninstall" -6. Follow the prompts to complete the uninstallation - -Alternatively, you can: - -1. Open the Start menu -2. Right-click on Zed -3. Select "Uninstall" - -### Removing User Data (Optional) - -To completely remove all Zed configuration files and data: - -1. Press `Windows key + R` to open Run -2. Type `%APPDATA%` and press Enter -3. Delete the `Zed` folder if it exists -4. Press `Windows key + R` again, type `%LOCALAPPDATA%` and press Enter -5. Delete the `Zed` folder if it exists - -## Troubleshooting - -If you encounter issues during uninstallation: - -- **macOS/Windows**: Ensure Zed is completely quit before attempting to uninstall. Check Activity Manager (macOS) or Task Manager (Windows) for any running Zed processes. -- **Linux**: If the uninstall script fails, check the error message and consider manual removal of the directories listed above. -- **All platforms**: If you want to start fresh while keeping Zed installed, you can delete the configuration directories instead of uninstalling the application entirely. - -For additional help, see our [Linux-specific documentation](./linux.md) or visit the [Zed community](https://zed.dev/community-links). diff --git a/docs/src/update.md b/docs/src/update.md deleted file mode 100644 index d828e5edf0..0000000000 --- a/docs/src/update.md +++ /dev/null @@ -1,21 +0,0 @@ -# Update Zed - -Zed is designed to keep itself up to date automatically. You can always update this behavior in your settings. - -## Auto-updates - -By default, Zed checks for updates and installs them automatically the next time you restart the app. You’ll always be running the latest version with no extra steps. - -If an update is available, Zed will download it in the background and apply it on restart. - -## How to check your current version - -To check which version of Zed you're using: - -Open the Command Palette (Cmd+Shift+P on macOS, Ctrl+Shift+P on Linux/Windows). - -Type and select `zed: about`. A modal will appear with your version information. - -## How to control update behavior - -If you want to turn off auto-updates, open the Settings Editor (Cmd ,) and find `Auto Update` under General Settings. diff --git a/docs/src/vim.md b/docs/src/vim.md deleted file mode 100644 index c9a0cd09f2..0000000000 --- a/docs/src/vim.md +++ /dev/null @@ -1,655 +0,0 @@ -# Vim Mode - -Zed includes a Vim emulation layer known as "vim mode". On this page, you will learn how to turn Zed's vim mode on or off, what tools and commands Zed provides to help you navigate and edit your code, and generally how to make the most of vim mode in Zed. - -You'll learn how to: - -- Understand the core differences between Zed's vim mode and traditional Vim -- Enable or disable vim mode -- Make the most of Zed-specific features within vim mode -- Customize vim mode key bindings -- Configure vim mode settings - -Whether you're new to vim mode or an experienced Vim user looking to optimize your Zed experience, this guide will help you harness the full power of modal editing in Zed. - -## Zed's vim mode design - -Vim mode tries to offer a familiar experience to Vim users: it replicates the behavior of motions and commands precisely when it makes sense and uses Zed-specific functionality to provide an editing experience that "just works" without requiring configuration on your part. - -This includes support for semantic navigation, multiple cursors, or other features usually provided by plugins like surrounding text. - -So, Zed's vim mode does not replicate Vim one-to-one, but it meshes Vim's modal design with Zed's modern features to provide a more fluid experience. It's also configurable, so you can add your own key bindings or override the defaults. - -### Core differences - -There are four types of features in vim mode that use Zed's core functionality, leading to some differences in behavior: - -1. **Motions**: vim mode uses Zed's semantic parsing to tune the behavior of motions per language. For example, in Rust, jumping to matching bracket with `%` works with the pipe character `|`. In JavaScript, `w` considers `$` to be a word character. -2. **Visual block selections**: vim mode uses Zed's multiple cursor to emulate visual block selections, making block selections a lot more flexible. For example, anything you insert after a block selection updates on every line in real-time, and you can add or remove cursors anytime. -3. **Macros**: vim mode uses Zed's recording system for vim macros. So, you can capture and replay more complex actions, like autocompletion. -4. **Search and replace**: vim mode uses Zed's search system, so, the syntax for regular expressions is slightly different compared to Vim. [Head to the Regex differences section](#regex-differences) for details. - -> **Note:** The foundations of Zed's vim mode should already cover many use cases, and we're always looking to improve it. If you find missing features that you rely on in your workflow, please [file an issue on GitHub](https://github.com/zed-industries/zed/issues). - -## Enabling and disabling vim mode - -When you first open Zed, you'll see a checkbox on the welcome screen that allows you to enable vim mode. - -If you missed this, you can toggle vim mode on or off anytime by opening the command palette and using the workspace command `toggle vim mode`. - -> **Note**: This command toggles the following property in your user settings: -> -> ```json [settings] -> { -> "vim_mode": true -> } -> ``` - -## Zed-specific features - -Zed is built on a modern foundation that (among other things) uses Tree-sitter and language servers to understand the content of the file you're editing and supports multiple cursors out of the box. - -Vim mode has several "core Zed" key bindings that will help you make the most of Zed's specific feature set. - -### Language server - -The following commands use the language server to help you navigate and refactor your code. - -| Command | Default Shortcut | -| ---------------------------------------- | ---------------- | -| Go to definition | `g d` | -| Go to declaration | `g D` | -| Go to type definition | `g y` | -| Go to implementation | `g I` | -| Rename (change definition) | `c d` | -| Go to All references to the current word | `g A` | -| Find symbol in current file | `g s` | -| Find symbol in entire project | `g S` | -| Go to next diagnostic | `g ]` or `] d` | -| Go to previous diagnostic | `g [` or `[ d` | -| Show inline error (hover) | `g h` | -| Open the code actions menu | `g .` | - -### Git - -| Command | Default Shortcut | -| ------------------------------- | ---------------- | -| Go to next git change | `] c` | -| Go to previous git change | `[ c` | -| Expand diff hunk | `d o` | -| Toggle staged | `d O` | -| Stage and next (in diff view) | `d u` | -| Unstage and next (in diff view) | `d U` | -| Restore change | `d p` | - -### Tree-sitter - -Tree-sitter is a powerful tool that Zed uses to understand the structure of your code. Zed provides motions that change the current cursor position, and text objects that can be used as the target of actions. - -| Command | Default Shortcut | -| ------------------------------- | --------------------------- | -| Go to next/previous method | `] m` / `[ m` | -| Go to next/previous method end | `] M` / `[ M` | -| Go to next/previous section | `] ]` / `[ [` | -| Go to next/previous section end | `] [` / `[ ]` | -| Go to next/previous comment | `] /`, `] *` / `[ /`, `[ *` | -| Select a larger syntax node | `[ x` | -| Select a smaller syntax node | `] x` | - -| Text Objects | Default Shortcut | -| ---------------------------------------------------------- | ---------------- | -| Around a class, definition, etc. | `a c` | -| Inside a class, definition, etc. | `i c` | -| Around a function, method etc. | `a f` | -| Inside a function, method, etc. | `i f` | -| A comment | `g c` | -| An argument, or list item, etc. | `i a` | -| An argument, or list item, etc. (including trailing comma) | `a a` | -| Around an HTML-like tag | `a t` | -| Inside an HTML-like tag | `i t` | -| The current indent level, and one line before and after | `a I` | -| The current indent level, and one line before | `a i` | -| The current indent level | `i i` | - -Note that the definitions for the targets of the `[m` family of motions are the same as the -boundaries defined by `af`. The targets of the `[[` are the same as those defined by `ac`, though -if there are no classes, then functions are also used. Similarly `gc` is used to find `[ /`. `g c` - -The definition of functions, classes and comments is language dependent, and support can be added -to extensions by adding a [`textobjects.scm`]. The definition of arguments and tags operates at -the Tree-sitter level, but looks for certain patterns in the parse tree and is not currently configurable -per language. - -### Multi cursor - -These commands help you manage multiple cursors in Zed. - -| Command | Default Shortcut | -| ------------------------------------------------------------ | ---------------- | -| Add a cursor selecting the next copy of the current word | `g l` | -| Add a cursor selecting the previous copy of the current word | `g L` | -| Skip latest word selection, and add next | `g >` | -| Skip latest word selection, and add previous | `g <` | -| Add a visual selection for every copy of the current word | `g a` | - -### Pane management - -These commands open new panes or jump to specific panes. - -| Command | Default Shortcut | -| ------------------------------------------ | ------------------ | -| Open a project-wide search | `g /` | -| Open the current search excerpt | `g ` | -| Open the current search excerpt in a split | ` ` | -| Go to definition in a split | ` g d` | -| Go to type definition in a split | ` g D` | - -### In insert mode - -The following commands help you bring up Zed's completion menu, request a suggestion from GitHub Copilot, or open the inline AI assistant without leaving insert mode. - -| Command | Default Shortcut | -| ---------------------------------------------------------------------------- | ---------------- | -| Open the completion menu | `ctrl-x ctrl-o` | -| Request GitHub Copilot suggestion (requires GitHub Copilot to be configured) | `ctrl-x ctrl-c` | -| Open the inline AI assistant (requires a configured assistant) | `ctrl-x ctrl-a` | -| Open the code actions menu | `ctrl-x ctrl-l` | -| Hides all suggestions | `ctrl-x ctrl-z` | - -### Supported plugins - -Zed's vim mode includes some features that are usually provided by very popular plugins in the Vim ecosystem: - -- You can surround text objects with `ys` (yank surround), change surrounding with `cs`, and delete surrounding with `ds`. -- You can comment and uncomment selections with `gc` in visual mode and `gcc` in normal mode. -- The project panel supports many shortcuts modeled after the Vim plugin `netrw`: navigation with `hjkl`, open file with `o`, open file in a new tab with `t`, etc. -- You can add key bindings to your keymap to navigate "camelCase" names. [Head down to the Optional key bindings](#optional-key-bindings) section to learn how. -- You can use `gR` to do [ReplaceWithRegister](https://github.com/vim-scripts/ReplaceWithRegister). -- You can use `cx` for [vim-exchange](https://github.com/tommcdo/vim-exchange) functionality. Note that it does not have a default binding in visual mode, but you can add one to your keymap (refer to the [optional key bindings](#optional-key-bindings) section). -- You can navigate to indent depths relative to your cursor with the [indent wise](https://github.com/jeetsukumaran/vim-indentwise) plugin `[-`, `]-`, `[+`, `]+`, `[=`, `]=`. -- You can select quoted text with AnyQuotes and bracketed text with AnyBrackets text objects. Zed also provides MiniQuotes and MiniBrackets which offer alternative selection behavior based on the [mini.ai](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-ai.md) Neovim plugin. See the [Quote and Bracket text objects](#quote-and-bracket-text-objects) section below for details. -- You can configure AnyQuotes, AnyBrackets, MiniQuotes, and MiniBrackets text objects for selecting quoted and bracketed text using different selection strategies. See the [Any Bracket Functionality](#any-bracket-functionality) section below for details. - -### Any Bracket Functionality - -Zed offers two different strategies for selecting text surrounded by any quote, or any bracket. These text objects are **not enabled by default** and must be configured in your keymap to be used. - -#### Included Characters - -Each text object type works with specific characters: - -| Text Object | Characters | -| ------------------------ | -------------------------------------------------------------------------------------- | -| AnyQuotes/MiniQuotes | Single quote (`'`), Double quote (`"`), Backtick (`` ` ``) | -| AnyBrackets/MiniBrackets | Parentheses (`()`), Square brackets (`[]`), Curly braces (`{}`), Angle brackets (`<>`) | - -Both "Any" and "Mini" variants work with the same character sets, but differ in their selection strategy. - -#### AnyQuotes and AnyBrackets (Traditional Vim behavior) - -These text objects implement traditional Vim behavior: - -- **Selection priority**: Finds the innermost (closest) quotes or brackets first -- **Fallback mechanism**: If none are found, falls back to the current line -- **Character-based matching**: Focuses solely on open and close characters without considering syntax -- **Vanilla Vim similarity**: AnyBrackets matches the behavior of commands like `ci<`, `ci(`, etc., in vanilla Vim, including potential edge cases (like considering `>` in `=>` as a closing delimiter) - -#### MiniQuotes and MiniBrackets (mini.ai behavior) - -These text objects implement the behavior of the [mini.ai](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-ai.md) Neovim plugin: - -- **Selection priority**: Searches the current line first before expanding outward -- **Tree-sitter integration**: Uses Tree-sitter queries for more context-aware selections -- **Syntax-aware matching**: Can distinguish between actual brackets and similar characters in other contexts (like `>` in `=>`) - -#### Choosing Between Approaches - -- Use **AnyQuotes/AnyBrackets** if you: - - - Prefer traditional Vim behavior - - Want consistent character-based selection prioritizing innermost delimiters - - Need behavior that closely matches vanilla Vim's text objects - -- Use **MiniQuotes/MiniBrackets** if you: - - Prefer the mini.ai plugin behavior - - Want more context-aware selections using Tree-sitter - - Prefer current-line priority when searching - -#### Example Configuration - -To use these text objects, you need to add bindings to your keymap. Here's an example configuration that makes them available when using text object operators (`i` and `a`) or change-surrounds (`cs`): - -```json [settings] -{ - "context": "vim_operator == a || vim_operator == i || vim_operator == cs", - "bindings": { - // Traditional Vim behavior - "q": "vim::AnyQuotes", - "b": "vim::AnyBrackets", - - // mini.ai plugin behavior - "Q": "vim::MiniQuotes", - "B": "vim::MiniBrackets" - } -} -``` - -With this configuration, you can use commands like: - -- `cib` - Change inside brackets using AnyBrackets behavior -- `ciB` - Change inside brackets using MiniBrackets behavior -- `ciq` - Change inside quotes using AnyQuotes behavior -- `ciQ` - Change inside quotes using MiniQuotes behavior - -## Command palette - -Vim mode allows you to open Zed's command palette with `:`. You can then type to access any usual Zed command. Additionally, vim mode adds aliases for popular Vim commands to ensure your muscle memory transfers to Zed. For example, you can write `:w` or `:write` to save the file. - -Below, you'll find tables listing the commands you can use in the command palette. We put optional characters in square brackets to indicate that you can omit them. - -> **Note**: We don't emulate the full power of Vim's command line yet. In particular, commands currently do not support arguments. Please [file issues on GitHub](https://github.com/zed-industries/zed) as you find things that are missing from the command palette. - -### File and window management - -This table shows commands for managing windows, tabs, and panes. As commands don't support arguments currently, you cannot specify a filename when saving or creating a new file. - -| Command | Description | -| -------------- | ---------------------------------------------------- | -| `:w[rite][!]` | Save the current file | -| `:wq[!]` | Save the file and close the buffer | -| `:q[uit][!]` | Close the buffer | -| `:wa[ll][!]` | Save all open files | -| `:wqa[ll][!]` | Save all open files and close all buffers | -| `:qa[ll][!]` | Close all buffers | -| `:[e]x[it][!]` | Close the buffer | -| `:up[date]` | Save the current file | -| `:cq` | Quit completely (close all running instances of Zed) | -| `:vs[plit]` | Split the pane vertically | -| `:sp[lit]` | Split the pane horizontally | -| `:new` | Create a new file in a horizontal split | -| `:vne[w]` | Create a new file in a vertical split | -| `:tabedit` | Create a new file in a new tab | -| `:tabnew` | Create a new file in a new tab | -| `:tabn[ext]` | Go to the next tab | -| `:tabp[rev]` | Go to previous tab | -| `:tabc[lose]` | Close the current tab | -| `:ls` | Show all buffers | - -> **Note:** The `!` character is used to force the command to execute without saving changes or prompting before overwriting a file. - -### Ex commands - -These ex commands open Zed's various panels and windows. - -| Command | Default Shortcut | -| ---------------------------- | ---------------- | -| Open the project panel | `:E[xplore]` | -| Open the collaboration panel | `:C[ollab]` | -| Open the chat panel | `:Ch[at]` | -| Open the AI panel | `:A[I]` | -| Open the git panel | `:G[it]` | -| Open the debug panel | `:D[ebug]` | -| Open the notifications panel | `:No[tif]` | -| Open the feedback window | `:fe[edback]` | -| Open the diagnostics window | `:cl[ist]` | -| Open the terminal | `:te[rm]` | -| Open the extensions window | `:Ext[ensions]` | - -### Navigating diagnostics - -These commands navigate diagnostics. - -| Command | Description | -| ------------------------ | ------------------------------ | -| `:cn[ext]` or `:ln[ext]` | Go to the next diagnostic | -| `:cp[rev]` or `:lp[rev]` | Go to the previous diagnostics | -| `:cc` or `:ll` | Open the errors page | - -### Git - -These commands interact with the version control system git. - -| Command | Description | -| --------------- | ------------------------------------------------------- | -| `:dif[fupdate]` | View the diff under the cursor (`d o` in normal mode) | -| `:rev[ert]` | Revert the diff under the cursor (`d p` in normal mode) | - -### Jump - -These commands jump to specific positions in the file. - -| Command | Description | -| ------------------- | ----------------------------------- | -| `:` | Jump to a line number | -| `:$` | Jump to the end of the file | -| `:/foo` and `:?foo` | Jump to next/prev line matching foo | - -### Replacement - -This command replaces text. It emulates the substitute command in vim. The substitute command uses regular expressions, and Zed uses a slightly different syntax than vim. You can learn more about Zed's syntax below, [in the regex differences section](#regex-differences). Zed will replace only the first occurrence of the search pattern in the current line. To replace all matches append the `g` flag. - -| Command | Description | -| ----------------------- | --------------------------------- | -| `:[range]s/foo/bar/[g]` | Replace instances of foo with bar | - -### Editing - -These commands help you edit text. - -| Command | Description | -| ----------------- | ------------------------------------------------------- | -| `:j[oin]` | Join the current line | -| `:d[elete][l][p]` | Delete the current line | -| `:s[ort] [i]` | Sort the current selection (with i, case-insensitively) | -| `:y[ank]` | Yank (copy) the current selection or line | - -### Set - -These commands modify editor options locally for the current buffer. - -| Command | Description | -| ------------------------------- | --------------------------------------------------------------------------------------------- | -| `:se[t] [no]wrap` | Lines longer than the width of the window will wrap and displaying continues on the next line | -| `:se[t] [no]nu[mber]` | Print the line number in front of each line | -| `:se[t] [no]r[elative]nu[mber]` | Changes the displayed number to be relative to the cursor | -| `:se[t] [no]i[gnore]c[ase]` | Controls whether the buffer and project search use case-sensitive matching | - -### Command mnemonics - -As any Zed command is available, you may find that it's helpful to remember mnemonics that run the correct command. For example: - -- `:diffs` for "toggle all hunk diffs" -- `:cpp` for "copy path to file" -- `:crp` for "copy relative path" -- `:reveal` for "reveal in finder" -- `:zlog` for "open zed log" -- `:clank` for "cancel language server work" - -## Customizing key bindings - -In this section, we'll learn how to customize the key bindings of Zed's vim mode. You'll learn: - -- How to select the correct context for your new key bindings. -- Useful contexts for vim mode key bindings. -- Common key bindings to customize for extra productivity. - -### Selecting the correct context - -Zed's key bindings are evaluated only when the `"context"` property matches your location in the editor. For example, if you add key bindings to the `"Editor"` context, they will only work when you're editing a file. If you add key bindings to the `"Workspace"` context, they will work everywhere in Zed. Here's an example of a key binding that saves when you're editing a file: - -```json [settings] -{ - "context": "Editor", - "bindings": { - "ctrl-s": "file::Save" - } -} -``` - -Contexts are nested, so when you're editing a file, the context is the `"Editor"` context, which is inside the `"Pane"` context, which is inside the `"Workspace"` context. That's why any key bindings you add to the `"Workspace"` context will work when you're editing a file. Here's an example: - -```json [keymap] -// This key binding will work when you're editing a file. It comes built into Zed by default as the workspace: save command. -{ - "context": "Workspace", - "bindings": { - "ctrl-s": "workspace::Save" - } -} -``` - -Contexts are expressions. They support boolean operators like `&&` (and) and `||` (or). For example, you can use the context `"Editor && vim_mode == normal"` to create key bindings that only work when you're editing a file _and_ you're in vim's normal mode. - -Vim mode adds several contexts to the `"Editor"` context: - -| Operator | Description | -| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| VimControl | Indicates that vim keybindings should work. Currently an alias for `vim_mode == normal \|\| vim_mode == visual \|\| vim_mode == operator`, but the definition may change over time | -| vim_mode == normal | Normal mode | -| vim_mode == visual | Visual mode | -| vim_mode == insert | Insert mode | -| vim_mode == replace | Replace mode | -| vim_mode == waiting | Waiting for an arbitrary key (e.g., after typing `f` or `t`) | -| vim_mode == operator | Waiting for another binding to trigger (e.g., after typing `c` or `d`) | -| vim_operator | Set to `none` unless `vim_mode == operator`, in which case it is set to the current operator's default keybinding (e.g., after typing `d`, `vim_operator == d`) | - -> **Note**: Contexts are matched only on one level at a time. So it is possible to use the expression `"Editor && vim_mode == normal"`, but `"Workspace && vim_mode == normal"` will never match because we set the vim context at the `"Editor"` level. - -### Useful contexts for vim mode key bindings - -Here's a template with useful vim mode contexts to help you customize your vim mode key bindings. You can copy it and integrate it into your user keymap. - -```json [keymap] -[ - { - "context": "VimControl && !menu", - "bindings": { - // Put key bindings here if you want them to work in normal & visual mode. - } - }, - { - "context": "vim_mode == normal && !menu", - "bindings": { - // "shift-y": ["workspace::SendKeystrokes", "y $"] // Use neovim's yank behavior: yank to end of line. - } - }, - { - "context": "vim_mode == insert", - "bindings": { - // "j k": "vim::NormalBefore" // In insert mode, make jk escape to normal mode. - } - }, - { - "context": "EmptyPane || SharedScreen", - "bindings": { - // Put key bindings here (in addition to the context above) if you want them to - // work when no editor exists. - // "space f": "file_finder::Toggle" - } - } -] -``` - -> **Note**: If you would like to emulate Vim's `map` commands (`nmap`, etc.), you can use the action `workspace::SendKeystrokes` in the correct context. - -### Optional key bindings - -By default, you can navigate between the different files open in the editor with shortcuts like `ctrl+w` followed by one of `hjkl` to move to the left, down, up, or right, respectively. - -But you cannot use the same shortcuts to move between all the editor docks (the terminal, project panel, assistant panel, ...). If you want to use the same shortcuts to navigate to the docks, you can add the following key bindings to your user keymap. - -```json [settings] -{ - "context": "Dock", - "bindings": { - "ctrl-w h": "workspace::ActivatePaneLeft", - "ctrl-w l": "workspace::ActivatePaneRight", - "ctrl-w k": "workspace::ActivatePaneUp", - "ctrl-w j": "workspace::ActivatePaneDown" - // ... or other keybindings - } -} -``` - -Subword motion, which allows you to navigate and select individual words in camelCase or snake_case, is not enabled by default. To enable it, add these bindings to your keymap. - -```json [settings] -{ - "context": "VimControl && !menu && vim_mode != operator", - "bindings": { - "w": "vim::NextSubwordStart", - "b": "vim::PreviousSubwordStart", - "e": "vim::NextSubwordEnd", - "g e": "vim::PreviousSubwordEnd" - } -} -``` - -Vim mode comes with shortcuts to surround the selection in normal mode (`ys`), but it doesn't have a shortcut to add surrounds in visual mode. By default, `shift-s` substitutes the selection (erases the text and enters insert mode). To use `shift-s` to add surrounds in visual mode, you can add the following object to your keymap. - -```json [settings] -{ - "context": "vim_mode == visual", - "bindings": { - "shift-s": "vim::PushAddSurrounds" - } -} -``` - -In non-modal text editors, cursor navigation typically wraps when moving past line ends. Zed, however, handles this behavior exactly like Vim by default: the cursor stops at line boundaries. If you prefer your cursor to wrap between lines, override these keybindings: - -```json [settings] -// In VimScript, this would look like this: -// set whichwrap+=<,>,[,],h,l -{ - "context": "VimControl && !menu", - "bindings": { - "left": "vim::WrappingLeft", - "right": "vim::WrappingRight", - "h": "vim::WrappingLeft", - "l": "vim::WrappingRight" - } -} -``` - -The [Sneak motion](https://github.com/justinmk/vim-sneak) feature allows for quick navigation to any two-character sequence in your text. You can enable it by adding the following keybindings to your keymap. By default, the `s` key is mapped to `vim::Substitute`. Adding these bindings will override that behavior, so ensure this change aligns with your workflow preferences. - -```json [settings] -{ - "context": "vim_mode == normal || vim_mode == visual", - "bindings": { - "s": "vim::PushSneak", - "shift-s": "vim::PushSneakBackward" - } -} -``` - -The [vim-exchange](https://github.com/tommcdo/vim-exchange) feature does not have a default binding for visual mode, as the `shift-x` binding conflicts with the default `shift-x` binding for visual mode (`vim::VisualDeleteLine`). To assign the default vim-exchange binding, add the following keybinding to your keymap: - -```json [settings] -{ - "context": "vim_mode == visual", - "bindings": { - "shift-x": "vim::Exchange" - } -} -``` - -### Restoring common text editing and Zed keybindings - -If you're using vim mode on Linux or Windows, you may find it overrides keybindings you can't live without: `ctrl+v` to paste, `ctrl+f` to search, etc. You can restore them by copying this data into your keymap: - -```json [keymap] -{ - "context": "Editor && !menu", - "bindings": { - "ctrl-c": "editor::Copy", // vim default: return to normal mode - "ctrl-x": "editor::Cut", // vim default: decrement - "ctrl-v": "editor::Paste", // vim default: visual block mode - "ctrl-y": "editor::Undo", // vim default: line up - "ctrl-f": "buffer_search::Deploy", // vim default: page down - "ctrl-o": "workspace::Open", // vim default: go back - "ctrl-s": "workspace::Save", // vim default: show signature - "ctrl-a": "editor::SelectAll", // vim default: increment - "ctrl-b": "workspace::ToggleLeftDock" // vim default: down - } -}, -``` - -## Changing vim mode settings - -You can change the following settings to modify vim mode's behavior: - -| Property | Description | Default Value | -| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| default_mode | The default mode to start in. One of "normal", "insert", "replace", "visual", "visual_line", "visual_block", "helix_normal". | "normal" | -| use_system_clipboard | Determines how system clipboard is used:
  • "always": use for all operations
  • "never": only use when explicitly specified
  • "on_yank": use for yank operations
| "always" | -| use_multiline_find | deprecated | -| use_smartcase_find | If `true`, `f` and `t` motions are case-insensitive when the target letter is lowercase. | false | -| toggle_relative_line_numbers | If `true`, line numbers are relative in normal mode and absolute in insert mode, giving you the best of both options. | false | -| custom_digraphs | An object that allows you to add custom digraphs. Read below for an example. | {} | -| highlight_on_yank_duration | The duration of the highlight animation(in ms). Set to `0` to disable | 200 | - -Here's an example of adding a digraph for the zombie emoji. This allows you to type `ctrl-k f z` to insert a zombie emoji. You can add as many digraphs as you like. - -```json [settings] -{ - "vim": { - "custom_digraphs": { - "fz": "🧟‍♀️" - } - } -} -``` - -Here's an example of these settings changed: - -```json [settings] -{ - "vim": { - "default_mode": "insert", - "use_system_clipboard": "never", - "use_smartcase_find": true, - "toggle_relative_line_numbers": true, - "highlight_on_yank_duration": 50, - "custom_digraphs": { - "fz": "🧟‍♀️" - } - } -} -``` - -## Useful core Zed settings for vim mode - -Here are a few general Zed settings that can help you fine-tune your Vim experience: - -| Property | Description | Default Value | -| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | -| cursor_blink | If `true`, the cursor blinks. | `true` | -| relative_line_numbers | If `"enabled"`, line numbers in the left gutter are relative to the cursor. If `"wrapped"`, they also display for wrapped lines. | `"disabled"` | -| scrollbar | Object that controls the scrollbar display. Set to `{ "show": "never" }` to hide the scroll bar. | `{ "show": "auto" }` | -| scroll_beyond_last_line | If set to `"one_page"`, allows scrolling up to one page beyond the last line. Set to `"off"` to prevent this behavior. | `"one_page"` | -| vertical_scroll_margin | The number of lines to keep above or below the cursor when scrolling. Set to `0` to allow the cursor to go up to the edges of the screen vertically. | `3` | -| gutter.line_numbers | Controls the display of line numbers in the gutter. Set the `"line_numbers"` property to `false` to hide line numbers. | `true` | -| command_aliases | Object that defines aliases for commands in the command palette. You can use it to define shortcut names for commands you use often. Read below for examples. | `{}` | - -Here's an example of these settings changed: - -```json [settings] -{ - // Disable cursor blink - "cursor_blink": false, - // Use relative line numbers - "relative_line_numbers": "enabled", - // Hide the scroll bar - "scrollbar": { "show": "never" }, - // Prevent the buffer from scrolling beyond the last line - "scroll_beyond_last_line": "off", - // Allow the cursor to reach the edges of the screen - "vertical_scroll_margin": 0, - "gutter": { - // Disable line numbers completely - "line_numbers": false - }, - "command_aliases": { - "W": "w", - "Wq": "wq", - "Q": "q" - } -} -``` - -The `command_aliases` property is a single object that maps keys or key sequences to vim mode commands. The example above defines multiple aliases: `W` for `w`, `Wq` for `wq`, and `Q` for `q`. - -## Regex differences - -Zed uses a different regular expression engine from Vim. This means that you will have to use a different syntax in some cases. Here are the most common differences: - -- **Capture groups**: Vim uses `\(` and `\)` to represent capture groups, in Zed these are `(` and `)`. On the flip side, in Vim, `(` and `)` represent literal parentheses, but in Zed these must be escaped to `\(` and `\)`. -- **Matches**: When replacing, Vim uses the backslash character followed by a number to represent a matched capture group. For example, `\1`. Zed uses the dollar sign instead. So, when in Vim you use `\0` to represent the entire match, in Zed the syntax is `$0` instead. Same for numbered capture groups: `\1` in Vim is `$1` in Zed. -- **Global option**: By default, in Vim, regex searches only match the first occurrence on a line, and you append `/g` at the end of your query to find all matches. In Zed, regex searches are global by default. -- **Case sensitivity**: Vim uses `/i` to indicate a case-insensitive search. In Zed you can either write `(?i)` at the start of the pattern or toggle case-sensitivity with the shortcut {#kb search::ToggleCaseSensitive}. - -> **Note**: To help with the transition, the command palette will fix parentheses and replace groups for you when you write a Vim-style substitute command, `:%s//`. So, Zed will convert `%s:/\(a\)(b)/\1/` into a search for "(a)\(b\)" and a replacement of "$1". - -For the full syntax supported by Zed's regex engine [see the regex crate documentation](https://docs.rs/regex/latest/regex/#syntax). diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md deleted file mode 100644 index e518571927..0000000000 --- a/docs/src/visual-customization.md +++ /dev/null @@ -1,592 +0,0 @@ -# Visual Customization - -Various aspects of Zed's visual layout can be configured via either the settings window or the `settings.json` file, which you can access via {#action zed::OpenSettings} ({#kb zed::OpenSettings}) and {#action zed::OpenSettingsFile} ({#kb zed::OpenSettingsFile}) respectively. - -See [Configuring Zed](./configuring-zed.md) for additional information and other non-visual settings. - -## Themes - -You can install many [themes](./themes.md) and [icon themes](./icon-themes.md) in form of extensions by running {#action zed::Extensions} from the command palette. - -You can preview/choose amongst your installed themes and icon themes with {#action theme_selector::Toggle} ({#kb theme_selector::Toggle}) and {#action icon_theme_selector::Toggle} which will modify the following settings: - -```json [settings] -{ - "theme": "One Dark", - "icon_theme": "Zed (Default)" -} -``` - -If you would like to use distinct themes for light mode/dark mode that can be set with: - -```json [settings] -{ - "theme": { - "dark": "One Dark", - "light": "One Light", - // Mode to use (dark, light) or "system" to follow the OS's light/dark mode (default) - "mode": "system" - }, - "icon_theme": { - "dark": "Zed (Default)", - "light": "Zed (Default)", - // Mode to use (dark, light) or "system" to follow the OS's light/dark mode (default) - "mode": "system" - } -} -``` - -## Fonts - -```json [settings] - // UI Font. Use ".SystemUIFont" to use the default system font (SF Pro on macOS), - // or ".ZedSans" for the bundled default (currently IBM Plex) - "ui_font_family": ".SystemUIFont", - "ui_font_weight": 400, // Font weight in standard CSS units from 100 to 900. - "ui_font_size": 16, - - // Buffer Font - Used by editor buffers - // use ".ZedMono" for the bundled default monospace (currently Lilex) - "buffer_font_family": "Berkeley Mono", // Font name for editor buffers - "buffer_font_size": 15, // Font size for editor buffers - "buffer_font_weight": 400, // Font weight in CSS units [100-900] - // Line height "comfortable" (1.618), "standard" (1.3) or custom: `{ "custom": 2 }` - "buffer_line_height": "comfortable", - - // Terminal Font Settings - "terminal": { - "font_family": "", - "font_size": 15, - // Terminal line height: comfortable (1.618), standard(1.3) or `{ "custom": 2 }` - "line_height": "standard", - }, - - // Controls the font size for agent responses in the agent panel. - // If not specified, it falls back to the UI font size. - "agent_ui_font_size": 15, - // Controls the font size for the agent panel's message editor, user message, - // and any other snippet of code. - "agent_buffer_font_size": 12 -``` - -### Font ligatures - -By default Zed enable font ligatures which will visually combines certain adjacent characters. - -For example `=>` will be displayed as `→` and `!=` will be `≠`. -This is purely cosmetic and the individual characters remain unchanged. - -To disable this behavior use: - -```json [settings] -{ - "buffer_font_features": { - "calt": false // Disable ligatures - } -} -``` - -### Status Bar - -```json [settings] -{ - // Whether to show full labels in line indicator or short ones - // - `short`: "2 s, 15 l, 32 c" - // - `long`: "2 selections, 15 lines, 32 characters" - "line_indicator_format": "long" - - // Individual status bar icons can be hidden: - // "project_panel": {"button": false }, - // "outline_panel": {"button": false }, - // "collaboration_panel": {"button": false }, - // "git_panel": {"button": false }, - // "notification_panel": {"button": false }, - // "agent": {"button": false }, - // "debugger": {"button": false }, - // "diagnostics": {"button": false }, - // "search": {"button": false }, -} -``` - -### Titlebar - -```json [settings] - // Control which items are shown/hidden in the title bar - "title_bar": { - "show_branch_icon": false, // Show/hide branch icon beside branch switcher - "show_branch_name": true, // Show/hide branch name - "show_project_items": true, // Show/hide project host and name - "show_onboarding_banner": true, // Show/hide onboarding banners - "show_user_picture": true, // Show/hide user avatar - "show_sign_in": true, // Show/hide sign-in button - "show_menus": false // Show/hide menus - }, -``` - -## Workspace - -```json [settings] -{ - // Force usage of Zed build in path prompts (file and directory pickers) - // instead of OS native pickers (false). - "use_system_path_prompts": true, - // Force usage of Zed built in confirmation prompts ("Do you want to save?") - // instead of OS native prompts (false). On linux this is ignored (always false). - "use_system_prompts": true, - - // Active pane styling settings. - "active_pane_modifiers": { - // Inset border size of the active pane, in pixels. - "border_size": 0.0, - // Opacity of the inactive panes. 0 means transparent, 1 means opaque. - "inactive_opacity": 1.0 - }, - - // Layout mode of the bottom dock: contained, full, left_aligned, right_aligned - "bottom_dock_layout": "contained", - - // Whether to resize all the panels in a dock when resizing the dock. - // Can be a combination of "left", "right" and "bottom". - "resize_all_panels_in_dock": ["left"] -} -``` - - - -## Editor - -```json [settings] - // Whether the cursor blinks in the editor. - "cursor_blink": true, - - // Cursor shape for the default editor: bar, block, underline, hollow - "cursor_shape": null, - - // Highlight the current line in the editor: none, gutter, line, all - "current_line_highlight": "all", - - // When does the mouse cursor hide: never, on_typing, on_typing_and_movement - "hide_mouse": "on_typing_and_movement", - - // Whether to highlight all occurrences of the selected text in an editor. - "selection_highlight": true, - - // Visually show tabs and spaces (none, all, selection, boundary, trailing) - "show_whitespaces": "selection", - "whitespace_map": { // Which characters to show when `show_whitespaces` enabled - "space": "•", - "tab": "⟶" // use "→", for a shorter arrow - }, - - "unnecessary_code_fade": 0.3, // How much to fade out unused code. - - // Hide the values of in variables from visual display in private files - "redact_private_values": false, - - // Soft-wrap and rulers - "soft_wrap": "none", // none, editor_width, preferred_line_length, bounded - "preferred_line_length": 80, // Column to soft-wrap - "show_wrap_guides": true, // Show/hide wrap guides (vertical rulers) - "wrap_guides": [], // Where to position wrap_guides (character counts) - - // Gutter Settings - "gutter": { - "line_numbers": true, // Show/hide line numbers in the gutter. - "runnables": true, // Show/hide runnables buttons in the gutter. - "breakpoints": true, // Show/hide show breakpoints in the gutter. - "folds": true, // Show/hide show fold buttons in the gutter. - "min_line_number_digits": 4 // Reserve space for N digit line numbers - }, - "relative_line_numbers": "enabled", // Show relative line numbers in gutter - - // Indent guides - "indent_guides": { - "enabled": true, - "line_width": 1, // Width of guides in pixels [1-10] - "active_line_width": 1, // Width of active guide in pixels [1-10] - "coloring": "fixed", // disabled, fixed, indent_aware - "background_coloring": "disabled" // disabled, indent_aware - }, - - "sticky_scroll": { - "enabled": false // Whether to stick scopes to the top of the editor. Disabled by default. - } -``` - -### Git Blame {#editor-blame} - -```json [settings] - "git": { - "inline_blame": { - "enabled": true, // Show/hide inline blame - "delay_ms": 0, // Show after delay (ms) - "min_column": 0, // Minimum column to inline display blame - "padding": 7, // Padding between code and inline blame (em) - "show_commit_summary": false // Show/hide commit summary - }, - "hunk_style": "staged_hollow" // staged_hollow, unstaged_hollow - } -``` - -### Editor Toolbar - -```json [settings] - // Editor toolbar related settings - "toolbar": { - "breadcrumbs": true, // Whether to show breadcrumbs. - "quick_actions": true, // Whether to show quick action buttons. - "selections_menu": true, // Whether to show the Selections menu - "agent_review": true, // Whether to show agent review buttons - "code_actions": false // Whether to show code action buttons - } -``` - -### Editor Scrollbar and Minimap {#editor-scrollbar} - -```json [settings] - // Scrollbar related settings - "scrollbar": { - // When to show the scrollbar in the editor (auto, system, always, never) - "show": "auto", - "cursors": true, // Show cursor positions in the scrollbar. - "git_diff": true, // Show git diff indicators in the scrollbar. - "search_results": true, // Show buffer search results in the scrollbar. - "selected_text": true, // Show selected text occurrences in the scrollbar. - "selected_symbol": true, // Show selected symbol occurrences in the scrollbar. - "diagnostics": "all", // Show diagnostics (none, error, warning, information, all) - "axes": { - "horizontal": true, // Show/hide the horizontal scrollbar - "vertical": true // Show/hide the vertical scrollbar - } - }, - - // Minimap related settings - "minimap": { - "show": "never", // When to show (auto, always, never) - "display_in": "active_editor", // Where to show (active_editor, all_editor) - "thumb": "always", // When to show thumb (always, hover) - "thumb_border": "left_open", // Thumb border (left_open, right_open, full, none) - "max_width_columns": 80, // Maximum width of minimap - "current_line_highlight": null // Highlight current line (null, line, gutter) - }, - - // Control Editor scroll beyond the last line: off, one_page, vertical_scroll_margin - "scroll_beyond_last_line": "one_page", - // Lines to keep above/below the cursor when scrolling with the keyboard - "vertical_scroll_margin": 3, - // The number of characters to keep on either side when scrolling with the mouse - "horizontal_scroll_margin": 5, - // Scroll sensitivity multiplier - "scroll_sensitivity": 1.0, - // Scroll sensitivity multiplier for fast scrolling (hold alt while scrolling) - "fast_scroll_sensitivity": 4.0, -``` - -### Editor Tabs - -```json [settings] - // Maximum number of tabs per pane. Unset for unlimited. - "max_tabs": null, - - // Customize the tab bar appearance - "tab_bar": { - "show": true, // Show/hide the tab bar - "show_nav_history_buttons": true, // Show/hide history buttons on tab bar - "show_tab_bar_buttons": true // Show hide buttons (new, split, zoom) - }, - "tabs": { - "git_status": false, // Color to show git status - "close_position": "right", // Close button position (left, right, hidden) - "show_close_button": "hover", // Close button shown (hover, always, hidden) - "file_icons": false, // Icon showing file type - // Show diagnostics in file icon (off, errors, all). Requires file_icons=true - "show_diagnostics": "off" - } -``` - -### Status Bar - -```json [settings] - "status_bar": { - // Show/hide a button that displays the active buffer's language. - // Clicking the button brings up the language selector. - // Defaults to true. - "active_language_button": true, - // Show/hide a button that displays the cursor's position. - // Clicking the button brings up an input for jumping to a line and column. - // Defaults to true. - "cursor_position_button": true, - // Show/hide a button that displays the buffer's line-ending mode. - // Clicking the button brings up the line-ending selector. - // Defaults to false. - "line_endings_button": false - }, - "global_lsp_settings": { - // Show/hide the LSP button in the status bar. - // Activity from the LSP is still shown. - // Button is not shown if "enable_language_server" if false. - "button": true - }, -``` - -### Multibuffer - -```json [settings] -{ - // The default number of lines to expand excerpts in the multibuffer by. - "expand_excerpt_lines": 5, - // The default number of lines of context provided for excerpts in the multibuffer by. - "excerpt_context_lines": 2 -} -``` - -### Editor Completions, Snippets, Actions, Diagnostics {#editor-lsp} - -```json [settings] - "snippet_sort_order": "inline", // Snippets completions: top, inline, bottom, none - "show_completions_on_input": true, // Show completions while typing - "show_completion_documentation": true, // Show documentation in completions - "auto_signature_help": false, // Show method signatures inside parentheses - - // Whether to show the signature help after completion or a bracket pair inserted. - // If `auto_signature_help` is enabled, this setting will be treated as enabled also. - "show_signature_help_after_edits": false, - - // Whether to show code action button at start of buffer line. - "inline_code_actions": true, - - // Which level to use to filter out diagnostics displayed in the editor: - "diagnostics_max_severity": null, // off, error, warning, info, hint, null (all) - - // How to render LSP `textDocument/documentColor` colors in the editor. - "lsp_document_colors": "inlay", // none, inlay, border, background - // When to show the scrollbar in the completion menu. - "completion_menu_scrollbar": "never", // auto, system, always, never - // Turn on colorization of brackets in editors (configurable per language) - "colorize_brackets": true, -``` - -### Edit Predictions {#editor-ai} - -```json [settings] - "edit_predictions": { - "mode": "eager", // Automatically show (eager) or hold-alt (subtle) - "enabled_in_text_threads": true // Show/hide predictions in agent text threads - }, - "show_edit_predictions": true // Show/hide predictions in editor -``` - -### Editor Inlay Hints - -```json [settings] -{ - "inlay_hints": { - "enabled": false, - // Toggle certain types of hints on and off, all switched on by default. - "show_type_hints": true, - "show_parameter_hints": true, - "show_other_hints": true, - - // Whether to show a background for inlay hints (theme `hint.background`) - "show_background": false, // - - // Time to wait after editing before requesting hints (0 to disable debounce) - "edit_debounce_ms": 700, - // Time to wait after scrolling before requesting hints (0 to disable debounce) - "scroll_debounce_ms": 50, - - // A set of modifiers which, when pressed, will toggle the visibility of inlay hints. - "toggle_on_modifiers_press": { - "control": false, - "shift": false, - "alt": false, - "platform": false, - "function": false - } - } -} -``` - -## File Finder - -```json [settings] - // File Finder Settings - "file_finder": { - "file_icons": true, // Show/hide file icons - "modal_max_width": "small", // Horizontal size: small, medium, large, xlarge, full - "git_status": true, // Show the git status for each entry - "include_ignored": null // gitignored files in results: true, false, null - }, -``` - -## Project Panel - -Project panel can be shown/hidden with {#action project_panel::ToggleFocus} ({#kb project_panel::ToggleFocus}) or with {#action pane::RevealInProjectPanel} ({#kb pane::RevealInProjectPanel}). - -```json [settings] - // Project Panel Settings - "project_panel": { - "button": true, // Show/hide button in the status bar - "default_width": 240, // Default panel width - "dock": "left", // Position of the dock (left, right) - "entry_spacing": "comfortable", // Vertical spacing (comfortable, standard) - "file_icons": true, // Show/hide file icons - "folder_icons": true, // Show/hide folder icons - "git_status": true, // Indicate new/updated files - "indent_size": 20, // Pixels for each successive indent - "auto_reveal_entries": true, // Show file in panel when activating its buffer - "auto_fold_dirs": true, // Fold dirs with single subdir - "sticky_scroll": true, // Stick parent directories at top of the project panel. - "drag_and_drop": true, // Whether drag and drop is enabled - "scrollbar": { // Project panel scrollbar settings - "show": null // Show/hide: (auto, system, always, never) - }, - "show_diagnostics": "all", // - // Settings related to indent guides in the project panel. - "indent_guides": { - // When to show indent guides in the project panel. (always, never) - "show": "always" - }, - // Sort order for entries (directories_first, mixed, files_first) - "sort_mode": "directories_first", - // Whether to hide the root entry when only one folder is open in the window. - "hide_root": false, - // Whether to hide the hidden entries in the project panel. - "hide_hidden": false - } -``` - -## Agent Panel - -```json [settings] - "agent": { - "version": "2", - "enabled": true, // Enable/disable the agent - "button": true, // Show/hide the icon in the status bar - "dock": "right", // Where to dock: left, right, bottom - "default_width": 640, // Default width (left/right docked) - "default_height": 320, // Default height (bottom docked) - }, - // Controls the font size for agent responses in the agent panel. - // If not specified, it falls back to the UI font size. - "agent_ui_font_size": 15, - // Controls the font size for the agent panel's message editor, user message, - // and any other snippet of code. - "agent_buffer_font_size": 12 -``` - -See [Zed AI Documentation](./ai/overview.md) for additional non-visual AI settings. - -## Terminal Panel - -```json [settings] - // Terminal Panel Settings - "terminal": { - "dock": "bottom", // Where to dock: left, right, bottom - "button": true, // Show/hide status bar icon - "default_width": 640, // Default width (left/right docked) - "default_height": 320, // Default height (bottom docked) - - // Set the cursor blinking behavior in the terminal (on, off, terminal_controlled) - "blinking": "terminal_controlled", - // Default cursor shape for the terminal cursor (block, bar, underline, hollow) - "cursor_shape": "block", - - // Environment variables to add to terminal's process environment - "env": { - // "KEY": "value" - }, - - // Terminal scrollbar - "scrollbar": { - "show": null // Show/hide: (auto, system, always, never) - }, - // Terminal Font Settings - "font_family": "Fira Code", - "font_size": 15, - "font_weight": 400, - // Terminal line height: comfortable (1.618), standard(1.3) or `{ "custom": 2 }` - "line_height": "comfortable", - - "max_scroll_history_lines": 10000, // Scrollback history (0=disable, max=100000) - } -``` - -See [Terminal settings](./configuring-zed.md#terminal) for additional non-visual customization options. - -### Other Panels - -```json [settings] - // Git Panel - "git_panel": { - "button": true, // Show/hide status bar icon - "dock": "left", // Where to dock: left, right - "default_width": 360, // Default width of the git panel. - "status_style": "icon", // label_color, icon - "sort_by_path": false, // Sort by path (false) or status (true) - "scrollbar": { - "show": null // Show/hide: (auto, system, always, never) - } - }, - - // Debugger Panel - "debugger": { - "dock": "bottom", // Where to dock: left, right, bottom - "button": true // Show/hide status bar icon - }, - - // Outline Panel - "outline_panel": { - "button": true, // Show/hide status bar icon - "default_width": 300, // Default width of the git panel - "dock": "left", // Where to dock: left, right - "file_icons": true, // Show/hide file_icons - "folder_icons": true, // Show file_icons (true), chevrons (false) for dirs - "git_status": true, // Show git status - "indent_size": 20, // Indentation for nested items (pixels) - "indent_guides": { - "show": "always" // Show indent guides (always, never) - }, - "auto_reveal_entries": true, // Show file in panel when activating its buffer - "auto_fold_dirs": true, // Fold dirs with single subdir - "scrollbar": { // Project panel scrollbar settings - "show": null // Show/hide: (auto, system, always, never) - } - } -``` - -## Collaboration Panels - -```json [settings] -{ - // Collaboration Panel - "collaboration_panel": { - "button": true, // Show/hide status bar icon - "dock": "left", // Where to dock: left, right - "default_width": 240 // Default width of the collaboration panel. - }, - "show_call_status_icon": true, // Shown call status in the OS status bar. - - // Notification Panel - "notification_panel": { - // Whether to show the notification panel button in the status bar. - "button": true, - // Where to dock the notification panel. Can be 'left' or 'right'. - "dock": "right", - // Default width of the notification panel. - "default_width": 380 - } -} -``` diff --git a/docs/src/windows.md b/docs/src/windows.md deleted file mode 100644 index 34a553dd5b..0000000000 --- a/docs/src/windows.md +++ /dev/null @@ -1,53 +0,0 @@ -# Zed on Windows - -## Installing Zed - -Get the latest stable builds via [the download page](https://zed.dev/download). If you want to download our preview build, you can find it on its [releases page](https://zed.dev/releases/preview). After the first manual installation, Zed will periodically check for install updates. - -You can also build zed from source, see [these docs](https://zed.dev/docs/development/windows) for instructions. - -## Uninstall - -- Installed via installer: Use `Settings` → `Apps` → `Installed apps`, search for Zed, and click Uninstall. -- Built from source: Remove the build output directory you created (e.g., your target/install folder). - -Your settings and extensions live in your user profile. When uninstalling, you can choose to keep or remove them. - -## Remote Development (SSH) - -Zed supports remote development on Windows through both SSH and WSL. You can connect to remote servers via SSH or work with files inside WSL distributions directly from Zed. - -For detailed instructions on setting up and using remote development features, including SSH configuration, WSL setup, and troubleshooting, see the [Remote Development documentation](./remote-development.md). - -## Troubleshooting - -### Zed fails to start or shows a blank window - -- Check that your hardware and operating system version are compatible with Zed. See our [installation guide](./installation.md) for more information. -- Update your GPU drivers from your GPU vendor (Intel/AMD/NVIDIA/Qualcomm). -- Ensure hardware acceleration is enabled in Windows and not blocked by third‑party software. -- Try launching Zed with no extensions or custom settings to isolate conflicts. - -### Terminal issues - -If activation scripts don’t run, update to the latest version and verify your shell profile files are not exiting early. For Git operations, confirm Git Bash or PowerShell is available and on PATH. - -### SSH remoting problems - -When prompted for credentials, use the graphical askpass dialog. If it doesn’t appear, check for credential manager conflicts and that GUI prompts aren’t blocked by your terminal. - -### Graphics issues - -#### Zed fails to open / degraded performance - -Zed requires a DirectX 11 compatible GPU to run. If Zed fails to open, your GPU may not meet the minimum requirements. - -To check if your GPU supports DirectX 11, run the following command: - -``` -dxdiag -``` - -This will open the DirectX Diagnostic Tool, which shows the DirectX version your GPU supports under `System` → `System Information` → `DirectX Version`. - -If you're running Zed inside a virtual machine, it will use the emulated adapter provided by your VM. While Zed will work in this environment, performance may be degraded. diff --git a/docs/theme/css/chrome.css b/docs/theme/css/chrome.css deleted file mode 100644 index ff0ba71120..0000000000 --- a/docs/theme/css/chrome.css +++ /dev/null @@ -1,811 +0,0 @@ -/* CSS for UI elements (a.k.a. chrome) */ - -@import "variables.css"; - -html { - background-color: var(--bg); - scrollbar-color: var(--scrollbar) var(--bg); -} -#searchresults a, -.content a:link, -a:visited, -a > .hljs { - color: var(--links); -} - -.icon-logo-img { - display: block; -} - -.icon-button { - position: relative; - height: 28px; - width: 28px; - z-index: 10; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: color 0.5s; - border: 0; - background-color: transparent; - border-radius: 4px; - color: var(--icons); -} - -.icon-button:hover { - color: var(--icons-hover); - background-color: var(--icon-btn-bg-hover); -} - -.ib-hidden-desktop { - display: none; -} - -.header-bar { - position: sticky; - top: 0; - z-index: 100; - padding: 12px 24px; - background-color: var(--sidebar-bg); - border-bottom: 1px solid var(--divider); - display: flex; - align-items: center; - justify-content: space-between; - flex-shrink: 0; -} - -.header-bar .left-container { - width: 160px; - display: flex; - align-items: center; - gap: 8px; -} - -.header-bar .right-container { - width: 160px; - display: flex; - align-items: center; - gap: 4px; -} - -.logo-nav { - display: block; - filter: var(--logo-brightness); -} - -.nav-chapters { - font-size: 2.5em; - text-align: center; - text-decoration: none; - - position: fixed; - top: 0; - bottom: 0; - margin: 0; - max-width: 150px; - min-width: 90px; - - display: flex; - justify-content: center; - align-content: center; - flex-direction: column; - - transition: - color 0.5s, - background-color 0.5s; -} - -.nav-chapters:hover { - text-decoration: none; - background-color: var(--theme-hover); - transition: - background-color 0.15s, - color 0.15s; -} - -.nav-wrapper { - margin-block-start: 50px; - display: none; -} - -.footer-buttons { - display: flex; - justify-content: space-between; - align-items: center; - gap: 1rem; - padding: 24px 0; -} - -.footer-button { - width: 100%; - padding: 12px; - display: flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - background-color: var(--footer-btn-bg); - border: 1px solid var(--footer-btn-border); - border-radius: 0.5rem; - font-size: 0.9em; -} - -.footer-button:hover { - background-color: var(--footer-btn-bg-hover); - border-color: var(--footer-btn-border-hover); -} - -.footer-button i { - text-decoration: underline !important; - text-decoration-color: transparent !important; -} - -.mobile-nav-chapters { - font-size: 2.5em; - text-align: center; - text-decoration: none; - width: 90px; - border-radius: 5px; - background-color: var(--sidebar-bg); -} - -/* Only Firefox supports flow-relative values */ -.previous { - float: left; -} -[dir="rtl"] .previous { - float: right; -} - -/* Only Firefox supports flow-relative values */ -.next { - float: right; - right: var(--page-padding); -} -[dir="rtl"] .next { - float: left; - right: unset; - left: var(--page-padding); -} - -/* Use the correct buttons for RTL layouts*/ -[dir="rtl"] .previous i.fa-angle-left:before { - content: "\f105"; -} -[dir="rtl"] .next i.fa-angle-right:before { - content: "\f104"; -} - -@media only screen and (max-width: 1080px) { - .nav-wide-wrapper { - display: none; - } - .nav-wrapper { - display: block; - } -} - -/* sidebar-visible */ -@media only screen and (max-width: 1380px) { - #sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wide-wrapper { - display: none; - } - #sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wrapper { - display: block; - } -} - -/* Inline code */ - -:not(pre) > .hljs { - display: inline; - padding: 0.1em 0.3em; - border-radius: 3px; -} - -:not(pre):not(a) > .hljs { - color: var(--inline-code-color); - overflow-x: initial; -} - -a:hover > .hljs { - text-decoration: underline; -} - -pre { - background-color: var(--pre-bg); - border: 1px solid; - border-color: var(--pre-border); - box-shadow: var(--pre-shadow) 4px 4px 0px 0px; - position: relative; -} -pre > .hljs { - background-color: initial; -} -pre > .buttons { - position: absolute; - z-index: 100; - right: 0px; - top: 2px; - margin: 0px; - padding: 2px 0px; - - color: var(--sidebar-fg); - cursor: pointer; - visibility: hidden; - opacity: 0; - transition: - visibility 0.1s linear, - opacity 0.1s linear; -} -pre:hover > .buttons { - visibility: visible; - opacity: 1; -} -pre > .buttons :hover { - color: var(--sidebar-active); - border-color: var(--border-hover); - background-color: var(--theme-hover); -} -pre > .buttons i { - margin-inline-start: 8px; -} -pre > .buttons button { - cursor: inherit; - margin: 0 4px; - height: 26px; - width: 26px; - font-size: 14px; - border-style: solid; - border-width: 1px; - border-radius: 4px; - border-color: var(--border); - background-color: var(--popover-bg); - transition: 100ms; - transition-property: color, border-color, background-color; - color: var(--icons); -} - -pre > .playground { - border: none; - margin: 0; - box-shadow: none; - /* HACK: This serves to visually hide nested
 elements in "playground" code snippets.
-  A more robust solution would involve modifying the rendered HTML. */
-}
-
-@media (pointer: coarse) {
-  pre > .buttons button {
-    /* On mobile, make it easier to tap buttons. */
-    padding: 0.3rem 1rem;
-  }
-
-  .sidebar-resize-indicator {
-    /* Hide resize indicator on devices with limited accuracy */
-    display: none;
-  }
-}
-pre > code {
-  display: block;
-  padding: 1rem;
-}
-
-/* TODO: ACE editors overlap their buttons because ACE does absolute
-   positioning within the code block which breaks padding. The only solution I
-   can think of is to move the padding to the outer pre tag (or insert a div
-   wrapper), but that would require fixing a whole bunch of CSS rules.
-*/
-.hljs.ace_editor {
-  padding: 0rem 0rem;
-}
-
-pre > .result {
-  margin-block-start: 10px;
-}
-
-/* Search */
-
-#searchresults a {
-  text-decoration: none;
-}
-
-mark {
-  border-radius: 2px;
-  padding-block-start: 0;
-  padding-block-end: 1px;
-  padding-inline-start: 3px;
-  padding-inline-end: 3px;
-  margin-block-start: 0;
-  margin-block-end: -1px;
-  margin-inline-start: -3px;
-  margin-inline-end: -3px;
-  background-color: var(--search-mark-bg);
-  transition: background-color 300ms linear;
-  cursor: pointer;
-}
-
-mark.fade-out {
-  background-color: rgba(0, 0, 0, 0) !important;
-  cursor: auto;
-}
-
-.searchbar-outer {
-  margin-inline-start: auto;
-  margin-inline-end: auto;
-  max-width: var(--content-max-width);
-}
-
-#searchbar {
-  width: 100%;
-  margin-block-start: 5px;
-  margin-block-end: 0;
-  margin-inline-start: auto;
-  margin-inline-end: auto;
-  padding: 10px 16px;
-  transition: box-shadow 300ms ease-in-out;
-  border: 1px solid var(--searchbar-border-color);
-  border-radius: 3px;
-  background-color: var(--searchbar-bg);
-  color: var(--searchbar-fg);
-}
-#searchbar:focus,
-#searchbar.active {
-  box-shadow: 0 0 3px var(--searchbar-shadow-color);
-  outline: none;
-  border-color: var(--search-mark-bg);
-}
-
-.searchresults-header {
-  font-weight: bold;
-  font-size: 1em;
-  padding-block-start: 18px;
-  padding-block-end: 0;
-  padding-inline-start: 5px;
-  padding-inline-end: 0;
-  color: var(--searchresults-header-fg);
-}
-
-ul#searchresults {
-  list-style: none;
-  padding-inline-start: 0;
-}
-ul#searchresults li {
-  margin: 10px 0px;
-  padding: 2px;
-  border-radius: 2px;
-}
-ul#searchresults li.focus {
-  background-color: var(--searchresults-li-bg);
-}
-ul#searchresults span.teaser {
-  display: block;
-  font-size: 0.8em;
-  margin-block-start: 5px;
-  margin-inline-start: 4px;
-  padding-inline-start: 2ch;
-  border-left: 1px solid var(--divider);
-}
-ul#searchresults span.teaser em {
-  font-weight: bold;
-  color: var(--full-contrast);
-  background: var(--code-bg);
-}
-
-/* Sidebar */
-
-.sidebar {
-  position: relative;
-  width: var(--sidebar-width);
-  flex-shrink: 0;
-  display: flex;
-  flex-direction: column;
-  font-size: 0.875em;
-  box-sizing: border-box;
-  -webkit-overflow-scrolling: touch;
-  overscroll-behavior-y: none;
-  overflow: hidden;
-  background-color: var(--sidebar-bg);
-  color: var(--sidebar-fg);
-  border-right: 1px solid var(--divider);
-}
-
-[dir="rtl"] .sidebar {
-  left: unset;
-  right: 0;
-}
-.sidebar-resizing {
-  -moz-user-select: none;
-  -webkit-user-select: none;
-  -ms-user-select: none;
-  user-select: none;
-}
-.no-js .sidebar,
-.js:not(.sidebar-resizing) .sidebar {
-  transition: transform 0.3s; /* Animation: slide away */
-}
-.sidebar code {
-  line-height: 2em;
-}
-.sidebar .sidebar-scrollbox {
-  flex: 1;
-  overflow-y: auto;
-  min-height: 0;
-}
-
-.sidebar .sidebar-resize-handle {
-  position: absolute;
-  cursor: col-resize;
-  width: 0;
-  right: calc(var(--sidebar-resize-indicator-width) * -1);
-  top: 0;
-  bottom: 0;
-  display: flex;
-  align-items: center;
-}
-
-.sidebar-resize-handle .sidebar-resize-indicator {
-  width: 100%;
-  height: 12px;
-  background-color: var(--icons);
-  margin-inline-start: var(--sidebar-resize-indicator-space);
-}
-
-[dir="rtl"] .sidebar .sidebar-resize-handle {
-  left: calc(var(--sidebar-resize-indicator-width) * -1);
-  right: unset;
-}
-.js .sidebar .sidebar-resize-handle {
-  cursor: col-resize;
-  width: calc(
-    var(--sidebar-resize-indicator-width) -
-      var(--sidebar-resize-indicator-space)
-  );
-}
-.sidebar::-webkit-scrollbar {
-  background: var(--sidebar-bg);
-}
-.sidebar::-webkit-scrollbar-thumb {
-  background: var(--scrollbar);
-}
-
-@media only screen and (max-width: 780px) {
-  .sidebar {
-    position: fixed;
-    top: 0;
-    left: 0;
-    height: 100vh;
-    padding-top: 57px; /* Account for header height */
-    transform: translateX(-100%);
-    z-index: 99;
-    transition: transform 0.1s ease;
-  }
-
-  [dir="rtl"] .sidebar {
-    left: unset;
-    right: 0;
-    transform: translateX(100%);
-  }
-
-  body.sidebar-open .sidebar {
-    box-shadow: var(--sidebar-mobile-shadow);
-    transform: translateX(0);
-  }
-}
-
-.chapter {
-  list-style: none outside none;
-  padding: 8px 20px 20px 20px;
-  line-height: 2.2em;
-  margin: 0;
-}
-
-.chapter ol {
-  width: 100%;
-}
-
-.chapter li {
-  display: flex;
-  color: var(--sidebar-non-existant);
-}
-
-.chapter li a {
-  display: block;
-  padding: 0 4px;
-  text-decoration: none;
-  color: var(--sidebar-fg);
-}
-
-.chapter li a:hover {
-  color: var(--sidebar-active);
-}
-
-.chapter li a.active {
-  color: var(--sidebar-active);
-  background-color: var(--sidebar-active-bg);
-}
-
-.chapter li > a.toggle {
-  cursor: pointer;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  margin-inline-start: auto;
-  user-select: none;
-  opacity: 0.5;
-  border-radius: 4px;
-  transition:
-    opacity 0.15s ease,
-    background-color 0.15s ease;
-}
-
-.chapter li > a.toggle:hover {
-  opacity: 1;
-  background-color: var(--theme-hover);
-}
-
-.chapter li.chapter-item {
-  display: flex;
-  flex-wrap: wrap;
-  align-items: center;
-  line-height: 1.5em;
-  margin-block-start: 0.6em;
-}
-
-.chapter li.chapter-item > a:first-child {
-  flex: 1;
-  min-width: 0;
-}
-
-.chapter li.expanded > a.toggle div {
-  transform: rotate(90deg);
-}
-
-.chapter li.part-title {
-  font-size: 1.4rem;
-  padding: 0 8px 0 4px;
-  color: var(--title-color);
-  cursor: pointer;
-  user-select: none;
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  line-height: auto;
-  border-radius: 2px;
-}
-
-.chapter li.part-title.collapsible:hover {
-  background-color: var(--hover-section-title);
-}
-
-.chapter li.part-title.collapsible::after {
-  content: "❯";
-  display: inline-block;
-  font-size: 1.2rem;
-  opacity: 0.6;
-  transition: transform 0.2s ease;
-  flex-shrink: 0;
-}
-
-.chapter li.part-title.collapsible.expanded::after {
-  transform: rotate(90deg);
-}
-
-.chapter li.section-spacer {
-  height: 2rem;
-  list-style: none;
-}
-
-.chapter li.section-hidden {
-  display: none !important;
-}
-
-.section {
-  list-style: none outside none;
-  padding-inline-start: 3ch;
-  line-height: 1.9em;
-}
-
-.theme-popup {
-  position: absolute;
-  right: 155px;
-  top: calc(var(--menu-bar-height) - 18px);
-  z-index: 1000;
-  border-radius: 4px;
-  font-size: 1.4rem;
-  color: var(--fg);
-  background: var(--popover-bg);
-  border: 1px solid var(--popover-border);
-  margin: 0;
-  padding: 0;
-  list-style: none;
-  display: none;
-  overflow: hidden;
-}
-
-[dir="rtl"] .theme-popup {
-  left: unset;
-  right: 10px;
-}
-.theme-popup .default {
-  color: var(--icons);
-}
-.theme-popup .theme {
-  width: 100%;
-  border: 0;
-  margin: 0;
-  padding: 2px 24px;
-  line-height: 25px;
-  white-space: nowrap;
-  text-align: start;
-  cursor: pointer;
-  color: inherit;
-  background: inherit;
-  font-size: inherit;
-  font-family: inherit;
-}
-.theme-popup .theme:hover {
-  background-color: var(--theme-hover);
-}
-
-.theme-selected::before {
-  font-family: Arial, Helvetica, sans-serif;
-  text-align: center;
-  display: inline-block;
-  content: "✓";
-  margin-inline-start: -20px;
-  width: 20px;
-}
-
-.download-button {
-  max-height: 28px;
-  margin-left: 8px;
-  background: var(--download-btn-bg);
-  color: var(--download-btn-color);
-  padding: 4px 8px;
-  border: 1px solid;
-  border-color: var(--download-btn-border);
-  font-size: 1.4rem;
-  border-radius: 4px;
-  box-shadow: var(--download-btn-shadow) 0px -2px 0px 0px inset;
-  text-decoration: none;
-  transition: 100ms;
-  transition-property: box-shadow, border-color, background-color;
-}
-
-.download-button:hover {
-  background: var(--download-btn-bg);
-  border-color: var(--download-btn-border-hover);
-  box-shadow: none;
-}
-
-.search-button {
-  min-width: 100px;
-  max-width: 300px;
-  height: 28px;
-  width: 100%;
-  padding: 4px 4px 4px 8px;
-  display: flex;
-  gap: 8px;
-  background: var(--search-btn-bg);
-  border: 1px solid;
-  border-color: var(--search-btn-border);
-  font-size: 1.4rem;
-  font-family: var(--font);
-  color: var(--icons);
-  border-radius: 4px;
-  transition: 100ms;
-  transition-property: box-shadow, border-color, background-color;
-}
-
-.search-button:hover {
-  background: var(--search-btn-bg-hover);
-}
-
-.search-button .icon {
-  width: 12px;
-  height: 12px;
-  transform: translateY(10%);
-  scale: 0.9;
-}
-
-.search-content-desktop {
-  width: 100%;
-  display: flex;
-  justify-content: space-between;
-}
-
-.search-content-mobile {
-  display: none;
-}
-
-.search-container {
-  box-sizing: border-box;
-  position: fixed;
-  inset: 0;
-  z-index: 1000;
-  padding: 24px;
-  padding-top: 72px;
-  background-color: rgba(0, 0, 0, 0.5);
-  display: none;
-  justify-content: center;
-}
-
-.search-container:has(#search-wrapper:not(.hidden)) {
-  display: flex;
-}
-
-.search-modal {
-  box-sizing: border-box;
-
-  max-width: 600px;
-  min-width: 600px;
-  height: fit-content;
-  max-height: 600px;
-  display: flex;
-  flex-direction: column;
-  padding: 16px;
-  overflow-y: auto;
-
-  border-radius: 8px;
-  background: var(--popover-bg);
-  border: 1px solid var(--popover-border);
-  box-shadow: var(--popover-shadow);
-}
-
-.searchbar-outer {
-  width: 100%;
-}
-
-#searchbar {
-  margin: 0;
-}
-
-@media only screen and (max-width: 780px) {
-  .header-bar {
-    padding: 16px;
-    justify-content: start;
-  }
-
-  .download-button {
-    display: none;
-  }
-
-  .ib-hidden-mobile {
-    display: none;
-  }
-
-  .header-bar .left-container {
-    width: fit-content;
-  }
-
-  .header-bar .right-container {
-    width: fit-content;
-  }
-
-  .search-button {
-    width: 100px;
-    margin-left: auto;
-    margin-right: 8px;
-  }
-
-  .ib-hidden-desktop {
-    display: block;
-  }
-
-  .search-modal {
-    width: 90vw;
-    min-width: auto;
-  }
-
-  .search-content-desktop {
-    display: none;
-  }
-
-  .search-content-mobile {
-    display: flex;
-  }
-
-  .theme-popup {
-    right: 15px;
-  }
-}
diff --git a/docs/theme/css/general.css b/docs/theme/css/general.css
deleted file mode 100644
index 9d4791ea40..0000000000
--- a/docs/theme/css/general.css
+++ /dev/null
@@ -1,452 +0,0 @@
-/* Base styles and content styles */
-
-@import "variables.css";
-
-:root {
-  /* Browser default font-size is 16px, this way 1 rem = 10px */
-  font-size: 62.5%;
-  color-scheme: var(--color-scheme);
-}
-
-html {
-  font-family: var(--font);
-  color: var(--fg);
-  background-color: var(--bg);
-  text-size-adjust: none;
-  -webkit-text-size-adjust: none;
-
-  text-rendering: geometricPrecision !important;
-  -webkit-font-smoothing: antialiased !important;
-  text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.005);
-}
-
-body {
-  margin: 0;
-  font-size: 1.6rem;
-  overflow: hidden;
-  height: 100vh;
-  overscroll-behavior-y: none;
-}
-
-#body-container {
-  display: flex;
-  flex-direction: column;
-  height: 100vh;
-  overflow: hidden;
-}
-
-code {
-  font-family: var(--mono-font) !important;
-  font-size: var(--code-font-size);
-  direction: ltr !important;
-}
-
-/* make long words/inline code not x overflow */
-main {
-  overflow-wrap: break-word;
-}
-
-.noise-pattern {
-  pointer-events: none;
-  user-select: none;
-  z-index: 105;
-  position: absolute;
-  inset: 0;
-  background-size: 180px;
-  background-repeat: repeat;
-  opacity: var(--noise-opacity);
-}
-
-/* make wide tables scroll if they overflow */
-.table-wrapper {
-  overflow-x: auto;
-}
-
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
-  position: relative;
-  font-family: var(--title-font);
-  font-weight: 480;
-  color: var(--title-color);
-}
-
-/* Don't change font size in headers. */
-h1 code,
-h2 code,
-h3 code,
-h4 code,
-h5 code,
-h6 code {
-  font-size: unset;
-}
-
-.left {
-  float: left;
-}
-.right {
-  float: right;
-}
-.boring {
-  opacity: 0.6;
-}
-.hide-boring .boring {
-  display: none;
-}
-.hidden {
-  display: none !important;
-}
-
-h1 {
-  font-size: 3.4rem;
-}
-
-h2 {
-  padding-bottom: 1rem;
-  border-bottom: 1px solid;
-  border-color: var(--border-light);
-}
-
-h3 {
-  font-size: 2rem;
-  padding-bottom: 0.8rem;
-  border-bottom: 1px dashed;
-  border-color: var(--border-light);
-}
-
-h4 {
-  font-size: 1.8rem;
-}
-
-h5 {
-  font-size: 1.6rem;
-}
-
-h2,
-h3,
-h4,
-h5 {
-  margin-block-start: 1.5em;
-  margin-block-end: 0;
-}
-
-code:focus-visible,
-pre:focus-visible,
-li:focus-visible,
-button:focus-visible,
-a:focus-visible {
-  outline: 3px solid #094ece80;
-}
-
-.header + .header h3,
-.header + .header h4,
-.header + .header h5 {
-  margin-block-start: 1em;
-}
-
-h1:target::before,
-h2:target::before,
-h3:target::before,
-h4:target::before,
-h5:target::before,
-h6:target::before {
-  content: "»";
-  position: absolute;
-  left: -1.5ch;
-}
-
-hr {
-  border: 0px solid;
-  color: transparent;
-  width: 100%;
-  height: 1px;
-  background-color: var(--border-light);
-}
-
-/* This is broken on Safari as of version 14, but is fixed
-   in Safari Technology Preview 117 which I think will be Safari 14.2.
-   https://bugs.webkit.org/show_bug.cgi?id=218076
-*/
-:target {
-  /* Safari does not support logical properties */
-  scroll-margin-top: calc(var(--menu-bar-height) + 2rem);
-}
-
-.page-wrapper {
-  box-sizing: border-box;
-  background-color: var(--bg);
-  display: flex;
-  flex: 1;
-  overflow: hidden;
-  min-height: 0;
-}
-
-.page {
-  outline: 0;
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  overflow-x: hidden;
-  overflow-y: auto;
-  overscroll-behavior-y: none;
-  min-width: 0;
-  position: relative;
-}
-
-.no-js .page-wrapper,
-.js:not(.sidebar-resizing) .page-wrapper {
-  transition:
-    margin-left 0.3s ease,
-    transform 0.3s ease; /* Animation: slide away */
-}
-[dir="rtl"] .js:not(.sidebar-resizing) .page-wrapper {
-  transition:
-    margin-right 0.3s ease,
-    transform 0.3s ease; /* Animation: slide away */
-}
-
-.content {
-  padding: 48px 32px 0 32px;
-  display: flex;
-  justify-content: space-between;
-  gap: 36px;
-}
-
-.content main {
-  margin-inline-start: auto;
-  margin-inline-end: auto;
-  max-width: var(--content-max-width);
-}
-
-.content p {
-  line-height: 1.625em;
-}
-.content div.video {
-  z-index: 150;
-  margin-top: 1rem;
-  border: 1px solid;
-  border-color: var(--border);
-  border-radius: 8px;
-  overflow: clip;
-}
-.content div.video iframe {
-  margin: 0;
-}
-.content ol {
-  marker: none;
-  line-height: 1.8;
-  padding-left: 2em;
-  ::marker {
-    font-size: 1.4rem;
-  }
-  li {
-    padding-left: 0;
-  }
-}
-.content ul {
-  line-height: 1.8;
-  padding-left: 1.8em;
-}
-.content a {
-  text-decoration: underline;
-  text-decoration-color: var(--link-line-decoration);
-}
-.content a:hover {
-  text-decoration-color: var(--link-line-decoration-hover);
-}
-.content img,
-.content video {
-  position: relative;
-  z-index: 150;
-  max-width: 100%;
-  background-color: var(--media-bg);
-  border: 1px solid;
-  border-color: var(--border);
-  border-radius: 8px;
-  overflow: clip;
-}
-.content .header:link,
-.content .header:visited {
-  color: var(--title-color);
-}
-.content .header:link,
-.content .header:visited:hover {
-  text-decoration: none;
-}
-
-iframe {
-  margin-top: 1rem;
-  margin-bottom: 10rem;
-}
-
-table {
-  margin-top: 1.4rem;
-  width: 100%;
-  border-collapse: collapse;
-  font-size: 1.4rem;
-}
-table td {
-  padding: 4px 12px;
-  border: 1px var(--table-border-color) solid;
-}
-table thead {
-  background: var(--table-header-bg);
-}
-table thead td {
-  font-weight: 700;
-  border: none;
-}
-table thead th {
-  padding: 6px 12px;
-  color: var(--full-contrast);
-  text-align: left;
-  border: 1px var(--table-border-color) solid;
-}
-table thead tr {
-  border: 1px var(--table-border-color) solid;
-}
-/* Alternate background colors for rows */
-table tbody tr:nth-child(2n) {
-  background: var(--table-alternate-bg);
-}
-
-blockquote {
-  margin: auto;
-  margin-top: 1rem;
-  padding: 1rem 1.25rem;
-  color: var(--full-contrast);
-  background-color: var(--quote-bg);
-  border: 1px solid var(--quote-border);
-}
-
-blockquote > p {
-  margin: 0;
-  padding-left: 2.6rem;
-  font-size: 1.4rem;
-}
-
-blockquote:before {
-  --size: 1.4rem;
-  position: absolute;
-  content: "ⓘ";
-  margin: 0.3rem 0;
-  width: var(--size);
-  height: var(--size);
-  font-size: var(--size);
-  font-weight: bold;
-  color: var(--icons);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  line-height: 1.625em;
-}
-
-blockquote .warning:before {
-  background-color: var(--quote-bg);
-}
-
-.warning {
-  margin: auto;
-  padding: 1rem 1.25rem;
-  color: var(--full-contrast);
-  background-color: var(--warning-bg);
-  border: 1px solid var(--warning-border);
-}
-
-.warning > p {
-  margin: 0;
-  padding-left: 2.6rem;
-  font-size: 1.4rem;
-}
-
-.warning:before {
-  --size: 1.4rem;
-  position: absolute;
-  content: "ⓘ";
-  margin: 0.3rem 0;
-  width: var(--size);
-  height: var(--size);
-  font-size: var(--size);
-  font-weight: bold;
-  color: var(--warning-icon);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  line-height: 1.625em;
-}
-
-kbd {
-  background-color: rgba(8, 76, 207, 0.1);
-  border-radius: 4px;
-  border: solid 1px var(--popover-border);
-  box-shadow: inset 0 -1px 0 var(--theme-hover);
-  display: inline-block;
-  font-size: var(--code-font-size);
-  font-family: var(--mono-font);
-  line-height: 10px;
-  padding: 4px 5px;
-  vertical-align: middle;
-}
-
-:not(.footnote-definition) + .footnote-definition,
-.footnote-definition + :not(.footnote-definition) {
-  margin-block-start: 2em;
-}
-.footnote-definition {
-  font-size: 1.4rem;
-  margin: 0.5em 0;
-  border-bottom: 1px solid;
-  border-color: var(--divider);
-}
-.footnote-definition p {
-  display: inline;
-}
-
-.tooltiptext {
-  position: absolute;
-  visibility: hidden;
-  color: #fff;
-  background-color: #333;
-  transform: translateX(
-    -50%
-  ); /* Center by moving tooltip 50% of its width left */
-  left: -8px; /* Half of the width of the icon */
-  top: -35px;
-  font-size: 0.8em;
-  text-align: center;
-  border-radius: 6px;
-  padding: 5px 8px;
-  margin: 5px;
-  z-index: 1000;
-}
-.tooltipped .tooltiptext {
-  visibility: visible;
-}
-
-.result-no-output {
-  font-style: italic;
-}
-
-code:not(pre code).hljs {
-  color: var(--code-text) !important;
-  background-color: var(--code-bg) !important;
-}
-
-@media only screen and (max-width: 1020px) {
-  .content {
-    padding: 16px 32px 0 32px;
-  }
-
-  .content main {
-    width: 100%;
-  }
-}
-
-@media only screen and (max-width: 400px) {
-  .content {
-    padding: 16px 16px 0 16px;
-  }
-}
diff --git a/docs/theme/css/variables.css b/docs/theme/css/variables.css
deleted file mode 100644
index 285540c6dc..0000000000
--- a/docs/theme/css/variables.css
+++ /dev/null
@@ -1,208 +0,0 @@
-/* Globals */
-
-:root {
-  --color-scheme: light;
-
-  --logo-brightness: brightness(1);
-
-  --sidebar-width: 280px;
-  --sidebar-resize-indicator-width: 0px;
-  --sidebar-resize-indicator-space: 2px;
-  --page-padding: 15px;
-  --content-max-width: 690px;
-  --menu-bar-height: 64px;
-  --font: "IA Writer Quattro S", sans-serif;
-  --title-font: "Lora", "Helvetica Neue", Helvetica, Arial, sans-serif;
-  --mono-font:
-    ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono,
-    Courier New, monospace;
-  --code-font-size: 0.875em
-    /* please adjust the ace font size accordingly in editor.js */;
-
-  --noise-opacity: 0.024;
-  --bg: hsla(50, 25%, 96%);
-  --fg: hsl(220, 13%, 34%);
-  --title-color: hsl(220, 92%, 42%);
-
-  --border: hsl(220, 13%, 80%);
-  --border-light: hsl(220, 13%, 90%);
-  --border-hover: hsl(220, 13%, 70%);
-
-  --media-bg: hsl(50, 25%, 92%);
-
-  --sidebar-bg: hsla(50, 25%, 94%);
-  --sidebar-fg: hsl(0, 0%, 0%);
-  --sidebar-non-existant: #aaaaaa;
-  --sidebar-active: hsl(220, 93%, 42%);
-  --sidebar-active-bg: hsl(220, 93%, 42%, 0.1);
-  --sidebar-mobile-shadow: 0px 16px 16px hsl(0, 0%, 0%, 0.1);
-
-  --divider: hsl(220, 50%, 45%, 0.1);
-  --scrollbar: #8f8f8f;
-
-  --icons: #747474;
-  --icons-hover: #000000;
-  --icon-btn-bg-hover: hsl(220, 93%, 42%, 0.15);
-
-  --links: hsl(220, 92%, 42%);
-  --link-line-decoration: hsl(220, 93%, 42%, 0.2);
-  --link-line-decoration-hover: hsl(220, 93%, 42%, 0.5);
-
-  --full-contrast: #000;
-
-  --inline-code-color: #301900;
-  --code-text: hsl(220, 13%, 10%);
-  --code-bg: hsl(220, 93%, 42%, 0.1);
-  --keybinding-bg: hsl(0, 0%, 94%);
-
-  --pre-bg: #fff;
-  --pre-border: hsla(220, 93%, 42%, 0.3);
-  --pre-shadow: hsla(220, 93%, 42%, 0.07);
-
-  --popover-bg: #fafafa;
-  --popover-border: #cccccc;
-  --popover-shadow:
-    0 10px 15px -3px hsl(0, 0%, 0%, 0.1), 0 4px 6px -4px hsl(0, 0%, 0%, 0.1);
-
-  --theme-hover: #e6e6e6;
-  --hover-section-title: hsl(50, 25%, 88%);
-
-  --quote-bg: hsl(197, 37%, 96%);
-  --quote-border: hsl(197, 37%, 84%);
-
-  --warning-border: hsl(25, 100%, 85%);
-  --warning-bg: hsl(42, 100%, 60%, 0.1);
-  --warning-icon: hsl(42, 100%, 30%);
-
-  --table-header-bg: hsl(220, 50%, 90%, 0.4);
-  --table-border-color: hsl(220, 93%, 42%, 0.15);
-  --table-alternate-bg: hsl(220, 10%, 90%, 0.4);
-
-  --toc-link-underline: hsl(0, 0%, 0%, 0.1);
-  --toc-link-underline-hover: hsl(0, 0%, 0%, 0.5);
-
-  --searchbar-border-color: #aaa;
-  --searchbar-bg: #fafafa;
-  --searchbar-fg: #000;
-  --searchbar-shadow-color: #aaa;
-  --searchresults-header-fg: #666;
-  --searchresults-li-bg: #e4f2fe;
-  --search-mark-bg: #a2cff5;
-
-  --download-btn-bg: hsl(220, 60%, 95%);
-  --download-btn-bg-hover: hsl(220, 60%, 93%);
-  --download-btn-color: hsl(220, 60%, 30%);
-  --download-btn-border: hsla(220, 60%, 40%, 0.2);
-  --download-btn-border-hover: hsla(220, 60%, 50%, 0.2);
-  --download-btn-shadow: hsla(220, 40%, 60%, 0.1);
-
-  --search-btn-bg: hsl(220, 100%, 100%);
-  --search-btn-bg-hover: hsla(50, 25%, 97%);
-  --search-btn-border: hsl(220, 50%, 45%, 0.2);
-
-  --toast-bg: hsla(220, 93%, 98%);
-  --toast-border: hsla(220, 93%, 42%, 0.3);
-  --toast-border-success: hsla(120, 73%, 42%, 0.3);
-  --toast-border-error: hsla(0, 90%, 50%, 0.3);
-
-  --footer-btn-bg: hsl(220, 60%, 98%, 0.4);
-  --footer-btn-bg-hover: hsl(220, 60%, 93%, 0.5);
-  --footer-btn-border: hsla(220, 60%, 40%, 0.15);
-  --footer-btn-border-hover: hsla(220, 60%, 50%, 0.2);
-}
-
-.dark {
-  --color-scheme: dark;
-
-  --logo-brightness: brightness(2);
-
-  --noise-opacity: 0.012;
-  --bg: hsl(220, 13%, 7.5%);
-  --fg: hsl(220, 14%, 70%);
-  --title-color: hsl(220, 92%, 80%);
-
-  --border: hsl(220, 13%, 20%);
-  --border-light: hsl(220, 13%, 15%);
-  --border-hover: hsl(220, 13%, 40%);
-
-  --media-bg: hsl(220, 13%, 8%);
-
-  --sidebar-bg: hsl(220, 13%, 6.5%);
-  --sidebar-fg: hsl(220, 14%, 71%);
-  --sidebar-non-existant: #505254;
-  --sidebar-active: hsl(220, 92%, 75%);
-  --sidebar-active-bg: hsl(220, 93%, 42%, 0.25);
-  --sidebar-mobile-shadow: 0px 16px 16px hsl(0, 0%, 0%, 0.6);
-
-  --divider: hsl(220, 13%, 12%);
-  --scrollbar: hsl(220, 13%, 30%);
-
-  --icons: hsl(220, 14%, 71%);
-  --icons-hover: hsl(220, 14%, 90%);
-  --icon-btn-bg-hover: hsl(220, 93%, 42%, 0.4);
-
-  --links: hsl(220, 93%, 75%);
-  --link-line-decoration: hsl(220, 92%, 80%, 0.4);
-  --link-line-decoration-hover: hsl(220, 92%, 80%, 0.8);
-  --full-contrast: #fff;
-
-  --inline-code-color: hsl(40, 100%, 80%);
-  --code-text: hsl(220, 13%, 95%);
-  --code-bg: hsl(220, 93%, 50%, 0.2);
-  --keybinding-bg: hsl(0, 0%, 12%);
-
-  --pre-bg: hsl(220, 13%, 5%);
-  --pre-border: hsla(220, 93%, 70%, 0.3);
-  --pre-shadow: hsla(220, 93%, 70%, 0.1);
-
-  --popover-bg: hsl(220, 13%, 8%);
-  --popover-border: hsl(220, 13%, 20%);
-  --popover-shadow:
-    0 10px 15px -3px hsl(0, 0%, 0%, 0.1), 0 4px 6px -4px hsl(0, 0%, 0%, 0.1);
-
-  --theme-hover: hsl(220, 13%, 25%);
-  --hover-section-title: hsl(220, 13%, 11%);
-
-  --quote-bg: hsl(220, 13%, 25%, 0.4);
-  --quote-border: hsl(220, 13%, 32%, 0.5);
-
-  --table-border-color: hsl(220, 13%, 30%, 0.5);
-  --table-header-bg: hsl(220, 13%, 25%, 0.5);
-  --table-alternate-bg: hsl(220, 13%, 20%, 0.4);
-
-  --toc-link-underline: hsl(255, 100%, 100%, 0.1);
-  --toc-link-underline-hover: hsl(255, 100%, 100%, 0.4);
-
-  --warning-border: hsl(25, 100%, 85%, 0.2);
-  --warning-bg: hsl(42, 100%, 40%, 0.1);
-  --warning-icon: hsl(42, 100%, 80%);
-
-  --searchbar-border-color: hsl(220, 13%, 30%);
-  --searchbar-bg: hsl(220, 13%, 22%, 0.5);
-  --searchbar-fg: hsl(220, 14%, 71%);
-  --searchbar-shadow-color: hsl(220, 13%, 15%);
-  --searchresults-header-fg: hsl(220, 14%, 60%);
-  --searchresults-li-bg: hsl(220, 13%, 25%);
-  --search-mark-bg: hsl(220, 93%, 60%);
-
-  --download-btn-bg: hsl(220, 90%, 90%, 0.1);
-  --download-btn-bg-hover: hsl(220, 90%, 50%, 0.2);
-  --download-btn-color: hsl(220, 90%, 95%);
-  --download-btn-border: hsla(220, 90%, 80%, 0.2);
-  --download-btn-border-hover: hsla(220, 90%, 80%, 0.4);
-  --download-btn-shadow: hsla(220, 50%, 60%, 0.15);
-
-  --search-btn-bg: hsl(220, 90%, 90%, 0.05);
-  --search-btn-bg-hover: hsl(220, 90%, 90%, 0.1);
-  --search-btn-border: hsla(220, 90%, 80%, 0.1);
-
-  --toast-bg: hsla(220, 20%, 98%, 0.05);
-  --toast-border: hsla(220, 93%, 70%, 0.2);
-  --toast-border-success: hsla(120, 90%, 60%, 0.3);
-  --toast-border-error: hsla(0, 90%, 80%, 0.3);
-
-  --footer-btn-bg: hsl(220, 90%, 95%, 0.01);
-  --footer-btn-bg-hover: hsl(220, 90%, 50%, 0.05);
-  --footer-btn-border: hsla(220, 90%, 90%, 0.05);
-  --footer-btn-border-hover: hsla(220, 90%, 80%, 0.2);
-}
diff --git a/docs/theme/favicon.png b/docs/theme/favicon.png
deleted file mode 100644
index 4621223567..0000000000
Binary files a/docs/theme/favicon.png and /dev/null differ
diff --git a/docs/theme/fonts/Lora.var.woff2 b/docs/theme/fonts/Lora.var.woff2
deleted file mode 100644
index e2d8990a7e..0000000000
Binary files a/docs/theme/fonts/Lora.var.woff2 and /dev/null differ
diff --git a/docs/theme/fonts/fonts.css b/docs/theme/fonts/fonts.css
deleted file mode 100644
index f55cb6ee89..0000000000
--- a/docs/theme/fonts/fonts.css
+++ /dev/null
@@ -1,17 +0,0 @@
-/* Open Sans is licensed under the Apache License, Version 2.0. See http://www.apache.org/licenses/LICENSE-2.0 */
-/* Source Code Pro is under the Open Font License. See https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL */
-/* open-sans-300 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
-
-@font-face {
-  font-family: "IA Writer Quattro S";
-  font-style: normal;
-  font-weight: 400;
-  src: url("iAWriterQuattroS-Regular.woff2") format("woff2");
-}
-
-@font-face {
-  font-family: "Lora";
-  src: url("Lora.var.woff2") format("woff2-variations");
-  font-weight: 100 900;
-  font-style: normal;
-}
diff --git a/docs/theme/fonts/iAWriterQuattroS-Regular.woff2 b/docs/theme/fonts/iAWriterQuattroS-Regular.woff2
deleted file mode 100644
index a25cdbcdd3..0000000000
Binary files a/docs/theme/fonts/iAWriterQuattroS-Regular.woff2 and /dev/null differ
diff --git a/docs/theme/highlight.css b/docs/theme/highlight.css
deleted file mode 100644
index 9bd80f3516..0000000000
--- a/docs/theme/highlight.css
+++ /dev/null
@@ -1,252 +0,0 @@
-/*!
-  Theme: GitHub
-  Description: Light theme as seen on github.com
-  Author: github.com
-  Maintainer: @Hirse
-  Updated: 2021-05-15
-
-  Outdated base version: https://github.com/primer/github-syntax-light
-  Current colors taken from GitHub's CSS
-*/
-
-.hljs {
-  color: #24292e;
-  background: #ffffff;
-  overflow-x: auto;
-}
-
-.hljs-doctag,
-.hljs-keyword,
-.hljs-meta .hljs-keyword,
-.hljs-template-tag,
-.hljs-template-variable,
-.hljs-type,
-.hljs-variable.language_ {
-  /* prettylights-syntax-keyword */
-  color: #d73a49;
-}
-
-.hljs-title,
-.hljs-title.class_,
-.hljs-title.class_.inherited__,
-.hljs-title.function_ {
-  /* prettylights-syntax-entity */
-  color: #6f42c1;
-}
-
-.hljs-attr,
-.hljs-attribute,
-.hljs-literal,
-.hljs-meta,
-.hljs-number,
-.hljs-operator,
-.hljs-variable,
-.hljs-selector-attr,
-.hljs-selector-class,
-.hljs-selector-id {
-  /* prettylights-syntax-constant */
-  color: #005cc5;
-}
-
-.hljs-regexp,
-.hljs-string,
-.hljs-meta .hljs-string {
-  /* prettylights-syntax-string */
-  color: #032f62;
-}
-
-.hljs-built_in,
-.hljs-symbol {
-  /* prettylights-syntax-variable */
-  color: #e36209;
-}
-
-.hljs-comment,
-.hljs-code,
-.hljs-formula {
-  /* prettylights-syntax-comment */
-  color: #6a737d;
-}
-
-.hljs-name,
-.hljs-quote,
-.hljs-selector-tag,
-.hljs-selector-pseudo {
-  /* prettylights-syntax-entity-tag */
-  color: #22863a;
-}
-
-.hljs-subst {
-  /* prettylights-syntax-storage-modifier-import */
-  color: #24292e;
-}
-
-.hljs-section {
-  /* prettylights-syntax-markup-heading */
-  color: #005cc5;
-  font-weight: bold;
-}
-
-.hljs-bullet {
-  /* prettylights-syntax-markup-list */
-  color: #735c0f;
-}
-
-.hljs-emphasis {
-  /* prettylights-syntax-markup-italic */
-  color: #24292e;
-  font-style: italic;
-}
-
-.hljs-strong {
-  /* prettylights-syntax-markup-bold */
-  color: #24292e;
-  font-weight: bold;
-}
-
-.hljs-addition {
-  /* prettylights-syntax-markup-inserted */
-  color: #22863a;
-  background-color: #f0fff4;
-}
-
-.hljs-deletion {
-  /* prettylights-syntax-markup-deleted */
-  color: #b31d28;
-  background-color: #ffeef0;
-}
-
-.hljs-char.escape_,
-.hljs-link,
-.hljs-params,
-.hljs-property,
-.hljs-punctuation,
-.hljs-tag {
-  /* purposely ignored */
-}
-
-/*!
-  Theme: GitHub Dark
-  Description: Dark theme as seen on github.com
-  Author: github.com
-  Maintainer: @Hirse
-  Updated: 2021-05-15
-
-  Outdated base version: https://github.com/primer/github-syntax-dark
-  Current colors taken from GitHub's CSS
-*/
-
-.dark .hljs {
-  color: #c9d1d9;
-  background: #0d1117;
-}
-
-.dark .hljs-doctag,
-.dark .hljs-keyword,
-.dark .hljs-meta .hljs-keyword,
-.dark .hljs-template-tag,
-.dark .hljs-template-variable,
-.dark .hljs-type,
-.dark .hljs-variable.language_ {
-  /* prettylights-syntax-keyword */
-  color: #ff7b72;
-}
-
-.dark .hljs-title,
-.dark .hljs-title.class_,
-.dark .hljs-title.class_.inherited__,
-.dark .hljs-title.function_ {
-  /* prettylights-syntax-entity */
-  color: #d2a8ff;
-}
-
-.dark .hljs-attr,
-.dark .hljs-attribute,
-.dark .hljs-literal,
-.dark .hljs-meta,
-.dark .hljs-number,
-.dark .hljs-operator,
-.dark .hljs-variable,
-.dark .hljs-selector-attr,
-.dark .hljs-selector-class,
-.dark .hljs-selector-id {
-  /* prettylights-syntax-constant */
-  color: #79c0ff;
-}
-
-.dark .hljs-regexp,
-.dark .hljs-string,
-.dark .hljs-meta .hljs-string {
-  /* prettylights-syntax-string */
-  color: #a5d6ff;
-}
-
-.dark .hljs-built_in,
-.dark .hljs-symbol {
-  /* prettylights-syntax-variable */
-  color: #ffa657;
-}
-
-.dark .hljs-comment,
-.dark .hljs-code,
-.dark .hljs-formula {
-  /* prettylights-syntax-comment */
-  color: #8b949e;
-}
-
-.dark .hljs-name,
-.dark .hljs-quote,
-.dark .hljs-selector-tag,
-.dark .hljs-selector-pseudo {
-  /* prettylights-syntax-entity-tag */
-  color: #7ee787;
-}
-
-.dark .hljs-subst {
-  /* prettylights-syntax-storage-modifier-import */
-  color: #c9d1d9;
-}
-
-.dark .hljs-section {
-  /* prettylights-syntax-markup-heading */
-  color: #1f6feb;
-  font-weight: bold;
-}
-
-.dark .hljs-bullet {
-  /* prettylights-syntax-markup-list */
-  color: #f2cc60;
-}
-
-.dark .hljs-emphasis {
-  /* prettylights-syntax-markup-italic */
-  color: #c9d1d9;
-  font-style: italic;
-}
-
-.dark .hljs-strong {
-  /* prettylights-syntax-markup-bold */
-  color: #c9d1d9;
-  font-weight: bold;
-}
-
-.dark .hljs-addition {
-  /* prettylights-syntax-markup-inserted */
-  color: #aff5b4;
-  background-color: #033a16;
-}
-
-.dark .hljs-deletion {
-  /* prettylights-syntax-markup-deleted */
-  color: #ffdcd7;
-  background-color: #67060c;
-}
-
-.dark .hljs-char.escape_,
-.dark .hljs-link,
-.dark .hljs-params,
-.dark .hljs-property,
-.dark .hljs-punctuation,
-.dark .hljs-tag {
-  /* purposely ignored */
-}
diff --git a/docs/theme/index.hbs b/docs/theme/index.hbs
deleted file mode 100644
index 98f64d41c3..0000000000
--- a/docs/theme/index.hbs
+++ /dev/null
@@ -1,429 +0,0 @@
-
-
-    
-        
-        
-        
-        
-        
-        {{ title }}
-        {{#if is_print }}
-        
-        {{/if}}
-        {{#if base_url}}
-        
-        {{/if}}
-
-
-        
-        {{> head}}
-
-        
-        
-        
-
-        
-        
-        
-        
-        {{#if print_enable}}
-        
-        {{/if}}
-
-        
-        
-        {{#if copy_fonts}}
-        
-        {{/if}}
-
-        
-        
-        
-        
-
-        
-        {{#each additional_css}}
-        
-        {{/each}}
-
-        {{#if mathjax_support}}
-        
-        
-        {{/if}}
-    
-    
-    
-
- - - - -
-
- - Zed Industries - - -
- {{#if search_enabled}} - - {{/if}} -
- - - - - - - - Download - - {{#if git_repository_url}} - - - - {{/if}} - {{#if git_repository_edit_url}} - - - - {{/if}} -
-
- -
- - {{#if search_enabled}} -
- -
- {{/if}} - - - - - - - - - - - - - - - -
-
-
- {{{ content }}} - -
-
- -
- - -
-
-
- - {{#if live_reload_endpoint}} - - - {{/if}} - - {{#if playground_line_numbers}} - - {{/if}} - - {{#if playground_copyable}} - - {{/if}} - - {{#if playground_js}} - - - - - - {{/if}} - - {{#if search_js}} - - - - {{/if}} - - - - - - - {{#each additional_js}} - - {{/each}} - - {{#if is_print}} - {{#if mathjax_support}} - - {{else}} - - {{/if}} - {{/if}} - - - -
- - diff --git a/docs/theme/page-toc.css b/docs/theme/page-toc.css deleted file mode 100644 index 6a16265976..0000000000 --- a/docs/theme/page-toc.css +++ /dev/null @@ -1,79 +0,0 @@ -.pagetoc { - box-sizing: border-box; - position: sticky; - top: 50px; - display: flex; - flex-direction: column; - gap: 4px; - padding: 28px 0 120px 0; - width: 200px; - max-height: calc(100svh - 50px); - overflow-x: hidden; -} -.pagetoc > :last-child { - margin-bottom: 16px; -} -.pagetoc a { - width: fit-content; - font-size: 1.4rem; - color: var(--fg) !important; - display: inline-block; - padding: 2px 0; - text-align: left; - text-decoration: underline; - text-decoration-color: var(--toc-link-underline); -} -.pagetoc a:hover { - text-decoration-color: var(--toc-link-underline-hover); -} -.pagetoc a.active { - background-color: var(--sidebar-active-bg); - color: var(--sidebar-active) !important; - text-decoration-color: hsl(219, 93%, 42%, 0.1); -} -.pagetoc a.active:hover { - text-decoration-color: hsl(219, 93%, 42%, 0.8); -} -.pagetoc .active { - background: var(--sidebar-bg); - color: var(--sidebar-fg); -} -.pagetoc .pagetoc-H1 { - display: none; -} -.pagetoc .pagetoc-H3 { - margin-left: 2ch; -} -.pagetoc .pagetoc-H4 { - margin-left: 4ch; -} -.pagetoc .pagetoc-H5 { - display: none; -} -.pagetoc .pagetoc-H6 { - display: none; -} -.toc-title { - margin: 0; - margin-bottom: 6px; - font-size: 1.4rem; - color: var(--full-contrast); -} - -.toc-container { - visibility: hidden; -} - -.toc-container.has-toc { - visibility: visible; -} - -.toc-container.no-toc { - display: none; -} - -@media only screen and (max-width: 1200px) { - .toc-container { - display: none; - } -} diff --git a/docs/theme/page-toc.js b/docs/theme/page-toc.js deleted file mode 100644 index 627416fddf..0000000000 --- a/docs/theme/page-toc.js +++ /dev/null @@ -1,112 +0,0 @@ -let scrollTimeout; - -const listenActive = () => { - const elems = document.querySelector(".pagetoc").children; - [...elems].forEach((el) => { - el.addEventListener("click", (_) => { - clearTimeout(scrollTimeout); - [...elems].forEach((el) => el.classList.remove("active")); - el.classList.add("active"); - - scrollTimeout = setTimeout(() => { - scrollTimeout = null; - }, 100); - }); - }); -}; - -const autoCreatePagetoc = () => { - const main = document.querySelector("#content > main"); - const content = Object.assign(document.createElement("div"), { - className: "content-wrap", - }); - content.append(...main.childNodes); - main.prepend(content); - main.insertAdjacentHTML( - "afterbegin", - '
', - ); - return document.querySelector(".pagetoc"); -}; - -const getPagetoc = () => - document.querySelector(".pagetoc") || autoCreatePagetoc(); - -const updateFunction = () => { - if (scrollTimeout) return; - - const headers = [...document.getElementsByClassName("header")]; - if (headers.length === 0) return; - - const threshold = 100; - let activeHeader = null; - - for (const header of headers) { - const rect = header.getBoundingClientRect(); - - if (rect.top <= threshold) { - activeHeader = header; - } - } - - if (!activeHeader && headers.length > 0) { - activeHeader = headers[0]; - } - - const pagetocLinks = [...document.querySelector(".pagetoc").children]; - pagetocLinks.forEach((link) => link.classList.remove("active")); - - if (activeHeader) { - const activeLink = pagetocLinks.find( - (link) => activeHeader.href === link.href, - ); - if (activeLink) activeLink.classList.add("active"); - } -}; - -document.addEventListener("DOMContentLoaded", () => { - const pagetoc = getPagetoc(); - const headers = [...document.getElementsByClassName("header")]; - - const nonH1Headers = headers.filter( - (header) => !header.parentElement.tagName.toLowerCase().startsWith("h1"), - ); - const tocContainer = document.querySelector(".toc-container"); - - if (nonH1Headers.length === 0) { - if (tocContainer) { - tocContainer.classList.add("no-toc"); - } - return; - } - - if (tocContainer) { - tocContainer.classList.add("has-toc"); - } - - const tocTitle = Object.assign(document.createElement("p"), { - className: "toc-title", - textContent: "On This Page", - }); - - pagetoc.appendChild(tocTitle); - - headers.forEach((header) => { - const link = Object.assign(document.createElement("a"), { - textContent: header.text, - href: header.href, - className: `pagetoc-${header.parentElement.tagName}`, - }); - pagetoc.appendChild(link); - }); - - updateFunction(); - listenActive(); - - const pageElement = document.querySelector(".page"); - if (pageElement) { - pageElement.addEventListener("scroll", updateFunction); - } else { - window.addEventListener("scroll", updateFunction); - } -}); diff --git a/docs/theme/plugins.css b/docs/theme/plugins.css deleted file mode 100644 index 8c9f0c438e..0000000000 --- a/docs/theme/plugins.css +++ /dev/null @@ -1,45 +0,0 @@ -kbd.keybinding { - background-color: var(--keybinding-bg); - padding: 2px 4px; - border-radius: 3px; - font-family: monospace; - display: inline-block; - margin: 0 2px; -} - -#copy-markdown-toggle i { - font-weight: 500 !important; - -webkit-text-stroke: 0.5px currentColor; -} - -.copy-toast { - position: fixed; - top: 72px; - right: 16px; - padding: 12px 16px; - border-radius: 4px; - font-size: 14px; - font-weight: 500; - color: var(--fg); - background: var(--toast-bg); - border: 1px solid var(--toast-border); - z-index: 1000; - opacity: 0; - transform: translateY(-10px); - transition: all 0.1s ease-in-out; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); - max-width: 280px; -} - -.copy-toast.success { - border-color: var(--toast-border-success); -} - -.copy-toast.error { - border-color: var(--toast-border-error); -} - -.copy-toast.show { - opacity: 1; - transform: translateY(0); -} diff --git a/docs/theme/plugins.js b/docs/theme/plugins.js deleted file mode 100644 index 1e20fe65c1..0000000000 --- a/docs/theme/plugins.js +++ /dev/null @@ -1,321 +0,0 @@ -function detectOS() { - var userAgent = navigator.userAgent; - - var platform = navigator.platform; - var macosPlatforms = ["Macintosh", "MacIntel", "MacPPC", "Mac68K"]; - var windowsPlatforms = ["Win32", "Win64", "Windows", "WinCE"]; - var iosPlatforms = ["iPhone", "iPad", "iPod"]; - - if (macosPlatforms.indexOf(platform) !== -1) { - return "Mac"; - } else if (iosPlatforms.indexOf(platform) !== -1) { - return "iOS"; - } else if (windowsPlatforms.indexOf(platform) !== -1) { - return "Windows"; - } else if (/Android/.test(userAgent)) { - return "Android"; - } else if (/Linux/.test(platform)) { - return "Linux"; - } - - return "Unknown"; -} - -var os = detectOS(); -console.log("Operating System:", os); - -// Defer keybinding processing to avoid blocking initial render -function updateKeybindings() { - const os = detectOS(); - const isMac = os === "Mac" || os === "iOS"; - - function processKeybinding(element) { - const [macKeybinding, linuxKeybinding] = element.textContent.split("|"); - element.textContent = isMac ? macKeybinding : linuxKeybinding; - element.classList.add("keybinding"); - } - - // Process all kbd elements at once (more efficient than walking entire DOM) - const kbdElements = document.querySelectorAll("kbd"); - kbdElements.forEach(processKeybinding); -} - -// Use requestIdleCallback if available, otherwise requestAnimationFrame -if (typeof requestIdleCallback === "function") { - requestIdleCallback(updateKeybindings); -} else { - requestAnimationFrame(updateKeybindings); -} - -function darkModeToggle() { - var html = document.documentElement; - - function setTheme(theme) { - html.setAttribute("data-theme", theme); - html.setAttribute("data-color-scheme", theme); - html.className = theme; - localStorage.setItem("mdbook-theme", theme); - } - - // Set initial theme - var currentTheme = localStorage.getItem("mdbook-theme"); - if (currentTheme) { - setTheme(currentTheme); - } else { - // If no theme is set, use the system's preference - var systemPreference = window.matchMedia("(prefers-color-scheme: dark)") - .matches - ? "dark" - : "light"; - setTheme(systemPreference); - } - - // Listen for system's preference changes - const darkModeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); - darkModeMediaQuery.addEventListener("change", function (e) { - if (!localStorage.getItem("mdbook-theme")) { - setTheme(e.matches ? "dark" : "light"); - } - }); -} - -const copyMarkdown = () => { - const copyButton = document.getElementById("copy-markdown-toggle"); - if (!copyButton) return; - - // Store the original icon class, loading state, and timeout reference - const originalIconClass = "fa fa-copy"; - let isLoading = false; - let iconTimeoutId = null; - - const getCurrentPagePath = () => { - const pathname = window.location.pathname; - - // Handle root docs path - if (pathname === "/docs/" || pathname === "/docs") { - return "getting-started.md"; - } - - // Remove /docs/ prefix and .html suffix, then add .md - const cleanPath = pathname - .replace(/^\/docs\//, "") - .replace(/\.html$/, "") - .replace(/\/$/, ""); - - return cleanPath ? cleanPath + ".md" : "getting-started.md"; - }; - - const showToast = (message, isSuccess = true) => { - // Remove existing toast if any - const existingToast = document.getElementById("copy-toast"); - existingToast?.remove(); - - const toast = document.createElement("div"); - toast.id = "copy-toast"; - toast.className = `copy-toast ${isSuccess ? "success" : "error"}`; - toast.textContent = message; - - document.body.appendChild(toast); - - // Show toast with animation - setTimeout(() => { - toast.classList.add("show"); - }, 10); - - // Hide and remove toast after 2 seconds - setTimeout(() => { - toast.classList.remove("show"); - setTimeout(() => { - toast.parentNode?.removeChild(toast); - }, 300); - }, 2000); - }; - - const changeButtonIcon = (iconClass, duration = 1000) => { - const icon = copyButton.querySelector("i"); - if (!icon) return; - - // Clear any existing timeout - if (iconTimeoutId) { - clearTimeout(iconTimeoutId); - iconTimeoutId = null; - } - - icon.className = iconClass; - - if (duration > 0) { - iconTimeoutId = setTimeout(() => { - icon.className = originalIconClass; - iconTimeoutId = null; - }, duration); - } - }; - - const fetchAndCopyMarkdown = async () => { - // Prevent multiple simultaneous requests - if (isLoading) return; - - try { - isLoading = true; - changeButtonIcon("fa fa-spinner fa-spin", 0); // Don't auto-restore spinner - - const pagePath = getCurrentPagePath(); - const rawUrl = `https://raw.githubusercontent.com/zed-industries/zed/main/docs/src/${pagePath}`; - - const response = await fetch(rawUrl); - if (!response.ok) { - throw new Error( - `Failed to fetch markdown: ${response.status} ${response.statusText}`, - ); - } - - const markdownContent = await response.text(); - - // Copy to clipboard using modern API - if (navigator.clipboard?.writeText) { - await navigator.clipboard.writeText(markdownContent); - } else { - // Fallback: throw error if clipboard API isn't available - throw new Error("Clipboard API not supported in this browser"); - } - - changeButtonIcon("fa fa-check", 1000); - showToast("Page content copied to clipboard!"); - } catch (error) { - console.error("Error copying markdown:", error); - changeButtonIcon("fa fa-exclamation-triangle", 2000); - showToast("Failed to copy markdown. Please try again.", false); - } finally { - isLoading = false; - } - }; - - copyButton.addEventListener("click", fetchAndCopyMarkdown); -}; - -// Initialize functionality when DOM is loaded -document.addEventListener("DOMContentLoaded", () => { - darkModeToggle(); - copyMarkdown(); -}); - -// Collapsible sidebar navigation for entire sections -// Note: Initial collapsed state is applied in index.hbs to prevent flicker -function initCollapsibleSidebar() { - var sidebar = document.getElementById("sidebar"); - if (!sidebar) return; - - var chapterList = sidebar.querySelector("ol.chapter"); - if (!chapterList) return; - - var partTitles = Array.from(chapterList.querySelectorAll("li.part-title")); - - partTitles.forEach(function (partTitle) { - // Get all sibling elements that belong to this section - var sectionItems = getSectionItems(partTitle); - - if (sectionItems.length > 0) { - setupCollapsibleSection(partTitle, sectionItems); - } - }); -} - -// Saves the list of collapsed section names to sessionStorage -// This gets reset when the tab is closed and opened again -function saveCollapsedSections() { - var collapsedSections = []; - var partTitles = document.querySelectorAll( - "#sidebar li.part-title.collapsible", - ); - - partTitles.forEach(function (partTitle) { - if (!partTitle.classList.contains("expanded")) { - collapsedSections.push(partTitle._sectionName); - } - }); - - try { - sessionStorage.setItem( - "sidebar-collapsed-sections", - JSON.stringify(collapsedSections), - ); - } catch (e) { - // sessionStorage might not be available - } -} - -function getSectionItems(partTitle) { - var items = []; - var sibling = partTitle.nextElementSibling; - - while (sibling) { - // Stop when we hit another part-title - if (sibling.classList.contains("part-title")) { - break; - } - items.push(sibling); - sibling = sibling.nextElementSibling; - } - - return items; -} - -function setupCollapsibleSection(partTitle, sectionItems) { - partTitle.classList.add("collapsible"); - partTitle.setAttribute("role", "button"); - partTitle.setAttribute("tabindex", "0"); - partTitle._sectionItems = sectionItems; - - var isCurrentlyCollapsed = partTitle._isCollapsed; - if (isCurrentlyCollapsed) { - partTitle.setAttribute("aria-expanded", "false"); - } else { - partTitle.classList.add("expanded"); - partTitle.setAttribute("aria-expanded", "true"); - } - - partTitle.addEventListener("click", function (e) { - e.preventDefault(); - toggleSection(partTitle); - }); - - // a11y: Add keyboard support (Enter and Space) - partTitle.addEventListener("keydown", function (e) { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - toggleSection(partTitle); - } - }); -} - -function toggleSection(partTitle) { - var isExpanded = partTitle.classList.contains("expanded"); - var sectionItems = partTitle._sectionItems; - var spacerAfter = partTitle._spacerAfter; - - if (isExpanded) { - partTitle.classList.remove("expanded"); - partTitle.setAttribute("aria-expanded", "false"); - sectionItems.forEach(function (item) { - item.classList.add("section-hidden"); - }); - if (spacerAfter) { - spacerAfter.classList.add("section-hidden"); - } - } else { - partTitle.classList.add("expanded"); - partTitle.setAttribute("aria-expanded", "true"); - sectionItems.forEach(function (item) { - item.classList.remove("section-hidden"); - }); - if (spacerAfter) { - spacerAfter.classList.remove("section-hidden"); - } - } - - saveCollapsedSections(); -} - -document.addEventListener("DOMContentLoaded", function () { - initCollapsibleSidebar(); -}); diff --git a/extensions/.gitignore b/extensions/.gitignore deleted file mode 100644 index 84cbee6b39..0000000000 --- a/extensions/.gitignore +++ /dev/null @@ -1 +0,0 @@ -grammars diff --git a/extensions/EXTRACTION.md b/extensions/EXTRACTION.md deleted file mode 100644 index 2bcc8e35d5..0000000000 --- a/extensions/EXTRACTION.md +++ /dev/null @@ -1,181 +0,0 @@ -# Extracting an extension to dedicated repo - -These are some notes of how to extract an extension from the main zed repository and generate a new repository which preserves the history as best as possible. In the this example we will be extracting the `ruby` extension, substitute as appropriate. - -## Pre-requisites - -Install [git-filter-repo](https://github.com/newren/git-filter-repo/blob/main/INSTALL.md): - -``` -brew install git-filter-repo -``` - -## Process - -We are going to use a `$LANGNAME` variable for all these steps. Make sure it is set correctly. - -> **Note** -> If you get `zsh: command not found: #` errors, run: -> `setopt interactive_comments && echo "setopt interactive_comments" >> ~/.zshrc` - -1. Create a clean clone the zed repository, delete tags and delete branches. - -```sh -LANGNAME=your_language_name_here - -rm -rf $LANGNAME -git clone --single-branch --no-tags git@github.com:zed-industries/zed.git $LANGNAME -cd $LANGNAME -``` - -2. Create an expressions.txt file somewhere (e.g. `~/projects/$LANGNAME.txt`) - -This file takes the form of `patern==>replacement`, where the replacement is optional. -Note whitespace matters so `ruby: ==>` is removing the `ruby:` prefix from a commit messages and adding a space after `==> ` means the replacement begins with a space. Regex capture groups are numbered `\1`, `\2`, etc. - -See: [Git Filter Repo Docs](https://htmlpreview.github.io/?https://github.com/newren/git-filter-repo/blob/docs/html/git-filter-repo.html) for more. - -```sh -# Create regex mapping for rewriting commit messages (edit as appropriate) -mkdir -p ~/projects -echo "${LANGNAME}: ==> -extension: ==> -chore: ==> -zed_extension_api: ==> -"'regex:(?zed-industries/zed\1' \ - > ~/projects/${LANGNAME}.txt - -# This removes the LICENSE symlink -git filter-repo --invert-paths --path extensions/$LANGNAME/LICENSE-APACHE - -# This does the work -git filter-repo \ - --use-mailmap \ - --subdirectory-filter extensions/$LANGNAME/ \ - --path LICENSE-APACHE \ - --replace-message ~/projects/${LANGNAME}.txt -``` - -3. Review the commits. - -This is your last chance to make any modifications. -If you don't fix it now, it'll be wrong forever. - -For example, a previous commit message was `php/ruby: bump version to 0.0.5` -which was replaced with `php/bump version to 0.0.5` -so I added a new line to expressions.txt with `php/==>` -and next run it became `bump version to 0.0.5`. - -4. [Optional] Generate tags - -You can always add tags later, but it's a nice touch. - -Show you all commits that mention a version number: - -```sh -git log --grep="(\d+\.\d+\.\d+)" --perl-regexp --oneline --reverse -``` - -Then just: - -``` -git tag v0.0.2 abcd1234 -git tag v0.0.3 deadbeef -``` - -Usually the initial extraction didn't mention a version number so you can just do that one manually. - -4. [Optional] Add a README.md and commit. - -5. Push to the new repo - -Create a new empty repo on github under the [zed-extensions](https://github.com/organizations/zed-extensions/repositories/new) organization. - -``` -git remote add origin git@github.com:zed-extensions/$LANGNAME -git push origin main --tags -git branch --set-upstream-to=origin/main main -``` - -6. Setup the new repository: - -- Go to the repository settings: - - Disable Wikis - - Uncheck "Allow Merge Commits" - - Check "Allow Squash Merging" - - Default commit message: "Pull request title and description" - -7. Publish a new version of the extension. - -```sh -OLD_VERSION=$(grep '^version = ' extension.toml | cut -d'"' -f2) -NEW_VERSION=$(echo "$OLD_VERSION" | awk -F. '{$NF = $NF + 1;} 1' OFS=.) -echo $OLD_VERSION $NEW_VERSION -perl -i -pe "s/$OLD_VERSION/$NEW_VERSION/" extension.toml -perl -i -pe "s#https://github.com/zed-industries/zed#https://github.com/zed-extensions/${LANGNAME}#g" extension.toml - -# if there's rust code, update this too. -test -f Cargo.toml && perl -i -pe "s/$OLD_VERSION/$NEW_VERSION/" Cargo.toml -# remove workspace Cargo.toml lines -test -f Cargo.toml && perl -ni -e 'print unless /^.*(workspace\s*=\s*true|\[lints\])\s*$/' Cargo.toml -test -f Cargo.toml && cargo check - -# add a .gitignore -echo "target/ -grammars/ -*.wasm" > .gitignore - -# commit and push -git add -u -git checkout -b "bump_${NEW_VERSION}" -git commit -m "Bump to v${NEW_VERSION}" -git push -gh pr create --title "Bump to v${NEW_VERSION}" --web - -# merge PR in web interface -git checkout main -git pull -git tag v${NEW_VERSION} -git push origin v${NEW_VERSION} -``` - -7. In zed repository, remove the old extension and push a PR. - -```sh -rm -rf extensions/$LANGNAME -sed -i '' "/extensions\/$LANGNAME/d" Cargo.toml -cargo check -git checkout -b remove_$LANGNAME -git add extensions/$LANGNAME -git add Cargo.toml Cargo.lock extensions/$LANGNAME -git commit -m "Migrate to $LANGNAME extension to zed-extensions/$LANGNAME" -git push -gh pr create --web -``` - -8. Update extensions repository: - -```sh -cd ../extensions -git checkout main -git pull -git submodule init -git submodule update -git status - -git checkout -b ${LANGNAME}_v${NEW_VERSION} -git submodule add https://github.com/zed-extensions/${LANGNAME}.git extensions/${LANGNAME} -pnpm sort-extensions - -# edit extensions.toml: -# - bump version -# - change `submodule` from `extensions/zed` to new path -# - remove `path` line all together - -git add extensions.toml .gitmodules extensions/${LANGNAME} -git diff --cached -git commit -m "Bump ${LANGNAME} to v${NEW_VERSION}" -git push -``` - -Create PR and reference the Zed PR with removal from tree. diff --git a/extensions/README.md b/extensions/README.md deleted file mode 100644 index c677e0b909..0000000000 --- a/extensions/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# Zed Extensions - -This directory contains extensions for Zed that are largely maintained by the Zed team. They currently live in the Zed repository for ease of maintenance. - -If you are looking for the Zed extension registry, see the [`zed-industries/extensions`](https://github.com/zed-industries/extensions) repo. - -## Structure - -Currently, Zed includes support for a number of languages without requiring installing an extension. Those languages can be found under [`crates/languages/src`](https://github.com/zed-industries/zed/tree/main/crates/languages/src). - -Support for all other languages is done via extensions. This directory ([extensions/](https://github.com/zed-industries/zed/tree/main/extensions/)) contains a number of officially maintained extensions. These extensions use the same [zed_extension_api](https://docs.rs/zed_extension_api/latest/zed_extension_api/) available to all [Zed Extensions](https://zed.dev/extensions) for providing [language servers](https://zed.dev/docs/extensions/languages#language-servers), [tree-sitter grammars](https://zed.dev/docs/extensions/languages#grammar) and [tree-sitter queries](https://zed.dev/docs/extensions/languages#tree-sitter-queries). - -## Dev Extensions - -See the docs for [Developing an Extension Locally](https://zed.dev/docs/extensions/developing-extensions#developing-an-extension-locally) for how to work with one of these extensions. - -## Updating - -> [!NOTE] -> This update process is usually handled by Zed staff. -> Community contributors should just submit a PR (step 1) and we'll take it from there. - -The process for updating an extension in this directory has three parts. - -1. Create a PR with your changes. (Merge it) -2. Bump the extension version in: - - - extensions/{language_name}/extension.toml - - extensions/{language_name}/Cargo.toml - - Cargo.lock - - You can do this manually, or with a script: - - ```sh - # Output the current version for a given language - ./script/language-extension-version - - # Update the version in `extension.toml` and `Cargo.toml` and trigger a `cargo check` - ./script/language-extension-version - ``` - - Commit your changes to a branch, push a PR and merge it. - -3. Open a PR to [`zed-industries/extensions`](https://github.com/zed-industries/extensions) repo that updates the extension in question - -Edit [`extensions.toml`](https://github.com/zed-industries/extensions/blob/main/extensions.toml) in the extensions repo to reflect the new version you set above and update the submodule latest Zed commit. - -```sh -# Go into your clone of the extensions repo -cd ../extensions - -# Update -git checkout main -git pull -just init-submodule extensions/zed - -# Update the Zed submodule -cd extensions/zed -git checkout main -git pull -cd - -git add extensions.toml extensions/zed -``` diff --git a/extensions/glsl/Cargo.toml b/extensions/glsl/Cargo.toml deleted file mode 100644 index 6dddeea358..0000000000 --- a/extensions/glsl/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "zed_glsl" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "Apache-2.0" - -[lints] -workspace = true - -[lib] -path = "src/glsl.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = "0.1.0" diff --git a/extensions/glsl/LICENSE-APACHE b/extensions/glsl/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/extensions/glsl/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/glsl/extension.toml b/extensions/glsl/extension.toml deleted file mode 100644 index 7cf9e218c8..0000000000 --- a/extensions/glsl/extension.toml +++ /dev/null @@ -1,15 +0,0 @@ -id = "glsl" -name = "GLSL" -description = "GLSL support." -version = "0.1.0" -schema_version = 1 -authors = ["Mikayla Maki "] -repository = "https://github.com/zed-industries/zed" - -[language_servers.glsl_analyzer] -name = "GLSL Analyzer LSP" -language = "GLSL" - -[grammars.glsl] -repository = "https://github.com/theHamsta/tree-sitter-glsl" -commit = "31064ce53385150f894a6c72d61b94076adf640a" diff --git a/extensions/glsl/languages/glsl/config.toml b/extensions/glsl/languages/glsl/config.toml deleted file mode 100644 index 0c71419c91..0000000000 --- a/extensions/glsl/languages/glsl/config.toml +++ /dev/null @@ -1,20 +0,0 @@ -name = "GLSL" -grammar = "glsl" -path_suffixes = [ - # Traditional rasterization pipeline shaders - "vert", "frag", "tesc", "tese", "geom", - # Compute shaders - "comp", - # Ray tracing pipeline shaders - "rgen", "rint", "rahit", "rchit", "rmiss", "rcall", - # Other - "glsl" - ] -first_line_pattern = '^#version \d+' -line_comments = ["// "] -block_comment = { start = "/* ", prefix = "* ", end = "*/", tab_size = 1 } -brackets = [ - { start = "{", end = "}", close = true, newline = true }, - { start = "[", end = "]", close = true, newline = true }, - { start = "(", end = ")", close = true, newline = true }, -] diff --git a/extensions/glsl/languages/glsl/highlights.scm b/extensions/glsl/languages/glsl/highlights.scm deleted file mode 100644 index 09f94d4fb5..0000000000 --- a/extensions/glsl/languages/glsl/highlights.scm +++ /dev/null @@ -1,117 +0,0 @@ -"break" @keyword -"case" @keyword -"const" @keyword -"continue" @keyword -"default" @keyword -"do" @keyword -"else" @keyword -"enum" @keyword -"extern" @keyword -"for" @keyword -"if" @keyword -"inline" @keyword -"return" @keyword -"sizeof" @keyword -"static" @keyword -"struct" @keyword -"switch" @keyword -"typedef" @keyword -"union" @keyword -"volatile" @keyword -"while" @keyword - -"#define" @keyword -"#elif" @keyword -"#else" @keyword -"#endif" @keyword -"#if" @keyword -"#ifdef" @keyword -"#ifndef" @keyword -"#include" @keyword -(preproc_directive) @keyword - -"--" @operator -"-" @operator -"-=" @operator -"->" @operator -"=" @operator -"!=" @operator -"*" @operator -"&" @operator -"&&" @operator -"+" @operator -"++" @operator -"+=" @operator -"<" @operator -"==" @operator -">" @operator -"||" @operator - -"." @delimiter -";" @delimiter - -(string_literal) @string -(system_lib_string) @string - -(null) @constant -(number_literal) @number -(char_literal) @number - -(identifier) @variable - -(field_identifier) @property -(statement_identifier) @label -(type_identifier) @type -(primitive_type) @type -(sized_type_specifier) @type - -(call_expression - function: (identifier) @function) -(call_expression - function: (field_expression - field: (field_identifier) @function)) -(function_declarator - declarator: (identifier) @function) -(preproc_function_def - name: (identifier) @function.special) - -((identifier) @constant - (#match? @constant "^[A-Z][A-Z\\d_]*$")) - -(comment) @comment - -[ - "in" - "out" - "inout" - "uniform" - "shared" - "layout" - "attribute" - "varying" - "buffer" - "coherent" - "readonly" - "writeonly" - "precision" - "highp" - "mediump" - "lowp" - "centroid" - "sample" - "patch" - "smooth" - "flat" - "noperspective" - "invariant" - "precise" -] @type.qualifier - -"subroutine" @keyword.function - -(extension_storage_class) @storageclass - -( - (identifier) @variable.builtin - (#match? @variable.builtin "^gl_") -) diff --git a/extensions/glsl/src/glsl.rs b/extensions/glsl/src/glsl.rs deleted file mode 100644 index 77865564cc..0000000000 --- a/extensions/glsl/src/glsl.rs +++ /dev/null @@ -1,131 +0,0 @@ -use std::fs; -use zed::settings::LspSettings; -use zed_extension_api::{self as zed, LanguageServerId, Result, serde_json}; - -struct GlslExtension { - cached_binary_path: Option, -} - -impl GlslExtension { - fn language_server_binary_path( - &mut self, - language_server_id: &LanguageServerId, - worktree: &zed::Worktree, - ) -> Result { - if let Some(path) = worktree.which("glsl_analyzer") { - return Ok(path); - } - - if let Some(path) = &self.cached_binary_path - && fs::metadata(path).is_ok_and(|stat| stat.is_file()) - { - return Ok(path.clone()); - } - - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::CheckingForUpdate, - ); - let release = zed::latest_github_release( - "nolanderc/glsl_analyzer", - zed::GithubReleaseOptions { - require_assets: true, - pre_release: false, - }, - )?; - - let (platform, arch) = zed::current_platform(); - let asset_name = format!( - "{arch}-{os}.zip", - arch = match arch { - zed::Architecture::Aarch64 => "aarch64", - zed::Architecture::X86 => "x86", - zed::Architecture::X8664 => "x86_64", - }, - os = match platform { - zed::Os::Mac => "macos", - zed::Os::Linux => "linux-musl", - zed::Os::Windows => "windows", - } - ); - - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; - - let version_dir = format!("glsl_analyzer-{}", release.version); - fs::create_dir_all(&version_dir) - .map_err(|err| format!("failed to create directory '{version_dir}': {err}"))?; - let binary_path = format!("{version_dir}/bin/glsl_analyzer"); - - if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::Downloading, - ); - - zed::download_file( - &asset.download_url, - &version_dir, - match platform { - zed::Os::Mac | zed::Os::Linux => zed::DownloadedFileType::Zip, - zed::Os::Windows => zed::DownloadedFileType::Zip, - }, - ) - .map_err(|e| format!("failed to download file: {e}"))?; - - zed::make_file_executable(&binary_path)?; - - let entries = - fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; - for entry in entries { - let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; - if entry.file_name().to_str() != Some(&version_dir) { - fs::remove_dir_all(entry.path()).ok(); - } - } - } - - self.cached_binary_path = Some(binary_path.clone()); - Ok(binary_path) - } -} - -impl zed::Extension for GlslExtension { - fn new() -> Self { - Self { - cached_binary_path: None, - } - } - - fn language_server_command( - &mut self, - language_server_id: &zed::LanguageServerId, - worktree: &zed::Worktree, - ) -> Result { - Ok(zed::Command { - command: self.language_server_binary_path(language_server_id, worktree)?, - args: vec![], - env: Default::default(), - }) - } - - fn language_server_workspace_configuration( - &mut self, - _language_server_id: &zed::LanguageServerId, - worktree: &zed::Worktree, - ) -> Result> { - let settings = LspSettings::for_worktree("glsl_analyzer", worktree) - .ok() - .and_then(|lsp_settings| lsp_settings.settings) - .unwrap_or_default(); - - Ok(Some(serde_json::json!({ - "glsl_analyzer": settings - }))) - } -} - -zed::register_extension!(GlslExtension); diff --git a/extensions/html/Cargo.toml b/extensions/html/Cargo.toml deleted file mode 100644 index 22cdb401a7..0000000000 --- a/extensions/html/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "zed_html" -version = "0.2.3" -edition.workspace = true -publish.workspace = true -license = "Apache-2.0" - -[lints] -workspace = true - -[lib] -path = "src/html.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = "0.7.0" diff --git a/extensions/html/LICENSE-APACHE b/extensions/html/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/extensions/html/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/html/extension.toml b/extensions/html/extension.toml deleted file mode 100644 index 1ded7af641..0000000000 --- a/extensions/html/extension.toml +++ /dev/null @@ -1,19 +0,0 @@ -id = "html" -name = "HTML" -description = "HTML support." -version = "0.2.3" -schema_version = 1 -authors = ["Isaac Clayton "] -repository = "https://github.com/zed-industries/zed" - -[language_servers.vscode-html-language-server] -name = "vscode-html-language-server" -language = "HTML" - -[language_servers.vscode-html-language-server.language_ids] -"HTML" = "html" -"CSS" = "css" - -[grammars.html] -repository = "https://github.com/tree-sitter/tree-sitter-html" -commit = "bfa075d83c6b97cd48440b3829ab8d24a2319809" diff --git a/extensions/html/languages/html/brackets.scm b/extensions/html/languages/html/brackets.scm deleted file mode 100644 index 53d6a6bb23..0000000000 --- a/extensions/html/languages/html/brackets.scm +++ /dev/null @@ -1,5 +0,0 @@ -("<" @open "/>" @close) -("" @close) -("<" @open ">" @close) -(("\"" @open "\"" @close) (#set! rainbow.exclude)) -((element (start_tag) @open (end_tag) @close) (#set! newline.only) (#set! rainbow.exclude)) diff --git a/extensions/html/languages/html/config.toml b/extensions/html/languages/html/config.toml deleted file mode 100644 index fc7d557198..0000000000 --- a/extensions/html/languages/html/config.toml +++ /dev/null @@ -1,19 +0,0 @@ -name = "HTML" -grammar = "html" -path_suffixes = ["html", "htm", "shtml"] -autoclose_before = ">})" -block_comment = { start = "", tab_size = 0 } -wrap_characters = { start_prefix = "<", start_suffix = ">", end_prefix = "" } -brackets = [ - { start = "{", end = "}", close = true, newline = true }, - { start = "[", end = "]", close = true, newline = true }, - { start = "(", end = ")", close = true, newline = true }, - { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, - { start = "<", end = ">", close = false, newline = true, not_in = ["comment", "string"] }, - { start = "!--", end = " --", close = true, newline = false, not_in = ["comment", "string"] }, -] -completion_query_characters = ["-"] -prettier_parser_name = "html" - -[overrides.default] -linked_edit_characters = ["-"] diff --git a/extensions/html/languages/html/highlights.scm b/extensions/html/languages/html/highlights.scm deleted file mode 100644 index 1cc0601b76..0000000000 --- a/extensions/html/languages/html/highlights.scm +++ /dev/null @@ -1,19 +0,0 @@ -(tag_name) @tag -(doctype) @tag.doctype -(attribute_name) @attribute -[ - "\"" - "'" - (attribute_value) -] @string -(comment) @comment - -"=" @punctuation.delimiter.html - -[ - "<" - ">" - "" -] @punctuation.bracket.html diff --git a/extensions/html/languages/html/indents.scm b/extensions/html/languages/html/indents.scm deleted file mode 100644 index 436663dba3..0000000000 --- a/extensions/html/languages/html/indents.scm +++ /dev/null @@ -1,6 +0,0 @@ -(start_tag ">" @end) @indent -(self_closing_tag "/>" @end) @indent - -(element - (start_tag) @start - (end_tag)? @end) @indent diff --git a/extensions/html/languages/html/injections.scm b/extensions/html/languages/html/injections.scm deleted file mode 100644 index 525b3efe29..0000000000 --- a/extensions/html/languages/html/injections.scm +++ /dev/null @@ -1,21 +0,0 @@ -((comment) @injection.content - (#set! injection.language "comment") -) - -(script_element - (raw_text) @injection.content - (#set! injection.language "javascript")) - -(style_element - (raw_text) @injection.content - (#set! injection.language "css")) - -(attribute - (attribute_name) @_attribute_name (#match? @_attribute_name "^style$") - (quoted_attribute_value (attribute_value) @injection.content) - (#set! injection.language "css")) - -(attribute - (attribute_name) @_attribute_name (#match? @_attribute_name "^on[a-z]+$") - (quoted_attribute_value (attribute_value) @injection.content) - (#set! injection.language "javascript")) diff --git a/extensions/html/languages/html/outline.scm b/extensions/html/languages/html/outline.scm deleted file mode 100644 index e7f9dc4fab..0000000000 --- a/extensions/html/languages/html/outline.scm +++ /dev/null @@ -1,5 +0,0 @@ -(comment) @annotation - -(element - (start_tag - (tag_name) @name)) @item diff --git a/extensions/html/languages/html/overrides.scm b/extensions/html/languages/html/overrides.scm deleted file mode 100644 index 434f610e70..0000000000 --- a/extensions/html/languages/html/overrides.scm +++ /dev/null @@ -1,7 +0,0 @@ -(comment) @comment -(quoted_attribute_value) @string - -[ - (start_tag) - (end_tag) -] @default diff --git a/extensions/html/src/html.rs b/extensions/html/src/html.rs deleted file mode 100644 index 337689ebdd..0000000000 --- a/extensions/html/src/html.rs +++ /dev/null @@ -1,115 +0,0 @@ -use std::{env, fs}; -use zed::settings::LspSettings; -use zed_extension_api::{self as zed, LanguageServerId, Result, serde_json::json}; - -const BINARY_NAME: &str = "vscode-html-language-server"; -const SERVER_PATH: &str = - "node_modules/@zed-industries/vscode-langservers-extracted/bin/vscode-html-language-server"; -const PACKAGE_NAME: &str = "@zed-industries/vscode-langservers-extracted"; - -struct HtmlExtension { - cached_binary_path: Option, -} - -impl HtmlExtension { - fn server_exists(&self) -> bool { - fs::metadata(SERVER_PATH).is_ok_and(|stat| stat.is_file()) - } - - fn server_script_path(&mut self, language_server_id: &LanguageServerId) -> Result { - let server_exists = self.server_exists(); - if self.cached_binary_path.is_some() && server_exists { - return Ok(SERVER_PATH.to_string()); - } - - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::CheckingForUpdate, - ); - let version = zed::npm_package_latest_version(PACKAGE_NAME)?; - - if !server_exists - || zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version) - { - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::Downloading, - ); - let result = zed::npm_install_package(PACKAGE_NAME, &version); - match result { - Ok(()) => { - if !self.server_exists() { - Err(format!( - "installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'", - ))?; - } - } - Err(error) => { - if !self.server_exists() { - Err(error)?; - } - } - } - } - Ok(SERVER_PATH.to_string()) - } -} - -impl zed::Extension for HtmlExtension { - fn new() -> Self { - Self { - cached_binary_path: None, - } - } - - fn language_server_command( - &mut self, - language_server_id: &LanguageServerId, - worktree: &zed::Worktree, - ) -> Result { - let server_path = if let Some(path) = worktree.which(BINARY_NAME) { - return Ok(zed::Command { - command: path, - args: vec!["--stdio".to_string()], - env: Default::default(), - }); - } else { - let server_path = self.server_script_path(language_server_id)?; - env::current_dir() - .unwrap() - .join(&server_path) - .to_string_lossy() - .to_string() - }; - self.cached_binary_path = Some(server_path.clone()); - - Ok(zed::Command { - command: zed::node_binary_path()?, - args: vec![server_path, "--stdio".to_string()], - env: Default::default(), - }) - } - - fn language_server_workspace_configuration( - &mut self, - server_id: &LanguageServerId, - worktree: &zed::Worktree, - ) -> Result> { - let settings = LspSettings::for_worktree(server_id.as_ref(), worktree) - .ok() - .and_then(|lsp_settings| lsp_settings.settings) - .unwrap_or_default(); - Ok(Some(settings)) - } - - fn language_server_initialization_options( - &mut self, - _server_id: &LanguageServerId, - _worktree: &zed_extension_api::Worktree, - ) -> Result> { - let initialization_options = json!({"provideFormatter": true }); - Ok(Some(initialization_options)) - } -} - -zed::register_extension!(HtmlExtension); diff --git a/extensions/proto/Cargo.toml b/extensions/proto/Cargo.toml deleted file mode 100644 index 1013d62cfa..0000000000 --- a/extensions/proto/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "zed_proto" -version = "0.2.3" -edition.workspace = true -publish.workspace = true -license = "Apache-2.0" - -[lints] -workspace = true - -[lib] -path = "src/proto.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = "0.1.0" diff --git a/extensions/proto/LICENSE-APACHE b/extensions/proto/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/extensions/proto/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/proto/extension.toml b/extensions/proto/extension.toml deleted file mode 100644 index 9bb8625065..0000000000 --- a/extensions/proto/extension.toml +++ /dev/null @@ -1,15 +0,0 @@ -id = "proto" -name = "Proto" -description = "Protocol Buffers support." -version = "0.2.3" -schema_version = 1 -authors = ["Zed Industries "] -repository = "https://github.com/zed-industries/zed" - -[grammars.proto] -repository = "https://github.com/zed-industries/tree-sitter-proto" -commit = "0848bd30a64be48772e15fbb9d5ba8c0cc5772ad" - -[language_servers.protobuf-language-server] -name = "Protobuf Language Server" -languages = ["Proto"] diff --git a/extensions/proto/languages/proto/config.toml b/extensions/proto/languages/proto/config.toml deleted file mode 100644 index 6d25c23da5..0000000000 --- a/extensions/proto/languages/proto/config.toml +++ /dev/null @@ -1,13 +0,0 @@ -name = "Proto" -grammar = "proto" -path_suffixes = ["proto"] -line_comments = ["// "] -autoclose_before = ";:.,=}])>" -brackets = [ - { start = "{", end = "}", close = true, newline = true }, - { start = "[", end = "]", close = true, newline = true }, - { start = "(", end = ")", close = true, newline = true }, - { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, - { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] }, - { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] }, -] diff --git a/extensions/proto/languages/proto/highlights.scm b/extensions/proto/languages/proto/highlights.scm deleted file mode 100644 index 5d0a513bee..0000000000 --- a/extensions/proto/languages/proto/highlights.scm +++ /dev/null @@ -1,61 +0,0 @@ -[ - "syntax" - "package" - "option" - "optional" - "import" - "service" - "rpc" - "returns" - "message" - "enum" - "oneof" - "repeated" - "reserved" - "to" -] @keyword - -[ - (key_type) - (type) - (message_name) - (enum_name) - (service_name) - (rpc_name) - (message_or_enum_type) -] @type - -(enum_field - (identifier) @constant) - -[ - (string) - "\"proto3\"" -] @string - -(int_lit) @number - -[ - (true) - (false) -] @boolean - -(comment) @comment - -[ - "(" - ")" - "[" - "]" - "{" - "}" - "<" - ">" -] @punctuation.bracket - -[ - ";" - "," -] @punctuation.delimiter - -"=" @operator diff --git a/extensions/proto/languages/proto/indents.scm b/extensions/proto/languages/proto/indents.scm deleted file mode 100644 index acb44a5e1e..0000000000 --- a/extensions/proto/languages/proto/indents.scm +++ /dev/null @@ -1,3 +0,0 @@ -(_ "{" "}" @end) @indent -(_ "[" "]" @end) @indent -(_ "(" ")" @end) @indent diff --git a/extensions/proto/languages/proto/outline.scm b/extensions/proto/languages/proto/outline.scm deleted file mode 100644 index f90b1bae33..0000000000 --- a/extensions/proto/languages/proto/outline.scm +++ /dev/null @@ -1,19 +0,0 @@ -(message - "message" @context - (message_name - (identifier) @name)) @item - -(service - "service" @context - (service_name - (identifier) @name)) @item - -(rpc - "rpc" @context - (rpc_name - (identifier) @name)) @item - -(enum - "enum" @context - (enum_name - (identifier) @name)) @item diff --git a/extensions/proto/languages/proto/textobjects.scm b/extensions/proto/languages/proto/textobjects.scm deleted file mode 100644 index 90ea84282d..0000000000 --- a/extensions/proto/languages/proto/textobjects.scm +++ /dev/null @@ -1,18 +0,0 @@ -(message (message_body - "{" - (_)* @class.inside - "}")) @class.around -(enum (enum_body - "{" - (_)* @class.inside - "}")) @class.around -(service - "service" - (_) - "{" - (_)* @class.inside - "}") @class.around - -(rpc) @function.around - -(comment)+ @comment.around diff --git a/extensions/proto/src/proto.rs b/extensions/proto/src/proto.rs deleted file mode 100644 index 36ba0faf5f..0000000000 --- a/extensions/proto/src/proto.rs +++ /dev/null @@ -1,86 +0,0 @@ -use zed_extension_api::{self as zed, Result, settings::LspSettings}; - -const PROTOBUF_LANGUAGE_SERVER_NAME: &str = "protobuf-language-server"; - -struct ProtobufLanguageServerBinary { - path: String, - args: Option>, -} - -struct ProtobufExtension; - -impl ProtobufExtension { - fn language_server_binary( - &self, - _language_server_id: &zed::LanguageServerId, - worktree: &zed::Worktree, - ) -> Result { - let binary_settings = LspSettings::for_worktree("protobuf-language-server", worktree) - .ok() - .and_then(|lsp_settings| lsp_settings.binary); - let binary_args = binary_settings - .as_ref() - .and_then(|binary_settings| binary_settings.arguments.clone()); - - if let Some(path) = binary_settings.and_then(|binary_settings| binary_settings.path) { - return Ok(ProtobufLanguageServerBinary { - path, - args: binary_args, - }); - } - - if let Some(path) = worktree.which(PROTOBUF_LANGUAGE_SERVER_NAME) { - return Ok(ProtobufLanguageServerBinary { - path, - args: binary_args, - }); - } - - Err(format!("{PROTOBUF_LANGUAGE_SERVER_NAME} not found in PATH",)) - } -} - -impl zed::Extension for ProtobufExtension { - fn new() -> Self { - Self - } - - fn language_server_command( - &mut self, - language_server_id: &zed_extension_api::LanguageServerId, - worktree: &zed_extension_api::Worktree, - ) -> zed_extension_api::Result { - let binary = self.language_server_binary(language_server_id, worktree)?; - Ok(zed::Command { - command: binary.path, - args: binary - .args - .unwrap_or_else(|| vec!["-logs".into(), "".into()]), - env: Default::default(), - }) - } - - fn language_server_workspace_configuration( - &mut self, - server_id: &zed::LanguageServerId, - worktree: &zed::Worktree, - ) -> Result> { - let settings = LspSettings::for_worktree(server_id.as_ref(), worktree) - .ok() - .and_then(|lsp_settings| lsp_settings.settings); - Ok(settings) - } - - fn language_server_initialization_options( - &mut self, - server_id: &zed::LanguageServerId, - worktree: &zed::Worktree, - ) -> Result> { - let initialization_options = LspSettings::for_worktree(server_id.as_ref(), worktree) - .ok() - .and_then(|lsp_settings| lsp_settings.initialization_options); - Ok(initialization_options) - } -} - -zed::register_extension!(ProtobufExtension); diff --git a/extensions/slash-commands-example/Cargo.toml b/extensions/slash-commands-example/Cargo.toml deleted file mode 100644 index 03b22af254..0000000000 --- a/extensions/slash-commands-example/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "slash_commands_example" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "Apache-2.0" - -[lints] -workspace = true - -[lib] -path = "src/slash_commands_example.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = "0.1.0" diff --git a/extensions/slash-commands-example/LICENSE-APACHE b/extensions/slash-commands-example/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/extensions/slash-commands-example/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/slash-commands-example/README.md b/extensions/slash-commands-example/README.md deleted file mode 100644 index 8c16a4e168..0000000000 --- a/extensions/slash-commands-example/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# Slash Commands Example Extension - -This is an example extension showcasing how to write slash commands. - -See: [Extensions: Slash Commands](https://zed.dev/docs/extensions/slash-commands) in the Zed Docs. - -## Pre-requisites - -[Install Rust Toolchain](https://www.rust-lang.org/tools/install): - -```sh -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -``` - -## Setup - -```sh -git clone https://github.com/zed-industries/zed.git -cp -RL zed/extensions/slash-commands-example . - -cd slash-commands-example/ - -# Update Cargo.toml to make it standalone -cat > Cargo.toml << EOF -[package] -name = "slash_commands_example" -version = "0.1.0" -edition = "2021" -license = "Apache-2.0" - -[lib] -path = "src/slash_commands_example.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = "0.1.0" -EOF - -curl -O https://raw.githubusercontent.com/rust-lang/rust/master/LICENSE-APACHE -echo "# Zed Slash Commands Example Extension" > README.md -echo "Cargo.lock" > .gitignore -echo "target/" >> .gitignore -echo "*.wasm" >> .gitignore - -git init -git add . -git commit -m "Initial commit" - -cd .. -mv slash-commands-example MY-SUPER-COOL-ZED-EXTENSION -zed $_ -``` - -## Installation - -1. Open the command palette (`cmd-shift-p` or `ctrl-shift-p`). -2. Launch `zed: install dev extension` -3. Select the extension folder created above - -## Test - -Open the assistant and type `/echo` and `/pick-one` at the beginning of a line. - -## Customization - -Open the `extensions.toml` file and set the `id`, `name`, `description`, `authors` and `repository` fields. - -Rename `slash-commands-example.rs` you'll also have to update `Cargo.toml` - -## Rebuild - -Rebuild to see these changes reflected: - -1. Open Zed Extensions (`cmd-shift-x` or `ctrl-shift-x`). -2. Click `Rebuild` next to your Dev Extension (formerly "Slash Command Example") - -## Troubleshooting / Logs - -- [zed.dev docs: Troubleshooting](https://zed.dev/docs/troubleshooting) - -## Documentation - -- [zed.dev docs: Extensions: Developing Extensions](https://zed.dev/docs/extensions/developing-extensions) -- [zed.dev docs: Extensions: Slash Commands](https://zed.dev/docs/extensions/slash-commands) diff --git a/extensions/slash-commands-example/extension.toml b/extensions/slash-commands-example/extension.toml deleted file mode 100644 index 888c776d01..0000000000 --- a/extensions/slash-commands-example/extension.toml +++ /dev/null @@ -1,15 +0,0 @@ -id = "slash-commands-example" -name = "Slash Commands Example" -description = "An example extension showcasing slash commands." -version = "0.1.0" -schema_version = 1 -authors = ["Zed Industries "] -repository = "https://github.com/zed-industries/zed" - -[slash_commands.echo] -description = "echoes the provided input" -requires_argument = true - -[slash_commands.pick-one] -description = "pick one of three options" -requires_argument = true diff --git a/extensions/slash-commands-example/src/slash_commands_example.rs b/extensions/slash-commands-example/src/slash_commands_example.rs deleted file mode 100644 index 5b170d63ee..0000000000 --- a/extensions/slash-commands-example/src/slash_commands_example.rs +++ /dev/null @@ -1,90 +0,0 @@ -use zed_extension_api::{ - self as zed, SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput, - SlashCommandOutputSection, Worktree, -}; - -struct SlashCommandsExampleExtension; - -impl zed::Extension for SlashCommandsExampleExtension { - fn new() -> Self { - SlashCommandsExampleExtension - } - - fn complete_slash_command_argument( - &self, - command: SlashCommand, - _args: Vec, - ) -> Result, String> { - match command.name.as_str() { - "echo" => Ok(vec![]), - "pick-one" => Ok(vec![ - SlashCommandArgumentCompletion { - label: "Option One".to_string(), - new_text: "option-1".to_string(), - run_command: true, - }, - SlashCommandArgumentCompletion { - label: "Option Two".to_string(), - new_text: "option-2".to_string(), - run_command: true, - }, - SlashCommandArgumentCompletion { - label: "Option Three".to_string(), - new_text: "option-3".to_string(), - run_command: true, - }, - ]), - command => Err(format!("unknown slash command: \"{command}\"")), - } - } - - fn run_slash_command( - &self, - command: SlashCommand, - args: Vec, - _worktree: Option<&Worktree>, - ) -> Result { - match command.name.as_str() { - "echo" => { - if args.is_empty() { - return Err("nothing to echo".to_string()); - } - - let text = args.join(" "); - - Ok(SlashCommandOutput { - sections: vec![SlashCommandOutputSection { - range: (0..text.len()).into(), - label: "Echo".to_string(), - }], - text, - }) - } - "pick-one" => { - let Some(selection) = args.first() else { - return Err("no option selected".to_string()); - }; - - match selection.as_str() { - "option-1" | "option-2" | "option-3" => {} - invalid_option => { - return Err(format!("{invalid_option} is not a valid option")); - } - } - - let text = format!("You chose {selection}."); - - Ok(SlashCommandOutput { - sections: vec![SlashCommandOutputSection { - range: (0..text.len()).into(), - label: format!("Pick One: {selection}"), - }], - text, - }) - } - command => Err(format!("unknown slash command: \"{command}\"")), - } - } -} - -zed::register_extension!(SlashCommandsExampleExtension); diff --git a/extensions/test-extension/Cargo.toml b/extensions/test-extension/Cargo.toml deleted file mode 100644 index 7d2412b98f..0000000000 --- a/extensions/test-extension/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "zed_test_extension" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "Apache-2.0" - -[lints] -workspace = true - -[lib] -path = "src/test_extension.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = { path = "../../crates/extension_api" } diff --git a/extensions/test-extension/LICENSE-APACHE b/extensions/test-extension/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/extensions/test-extension/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/test-extension/README.md b/extensions/test-extension/README.md deleted file mode 100644 index 5941f23ec4..0000000000 --- a/extensions/test-extension/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Test Extension - -This is a test extension that we use in the tests for the `extension` crate. - -Originally based off the Gleam extension. diff --git a/extensions/test-extension/extension.toml b/extensions/test-extension/extension.toml deleted file mode 100644 index 0cb5afac7f..0000000000 --- a/extensions/test-extension/extension.toml +++ /dev/null @@ -1,25 +0,0 @@ -id = "test-extension" -name = "Test Extension" -description = "An extension for use in tests." -version = "0.1.0" -schema_version = 1 -authors = ["Marshall Bowers "] -repository = "https://github.com/zed-industries/zed" - -[language_servers.gleam] -name = "Gleam LSP" -language = "Gleam" - -[grammars.gleam] -repository = "https://github.com/gleam-lang/tree-sitter-gleam" -commit = "8432ffe32ccd360534837256747beb5b1c82fca1" - -[[capabilities]] -kind = "process:exec" -command = "echo" -args = ["hello from a child process!"] - -[[capabilities]] -kind = "process:exec" -command = "cmd" -args = ["/C", "echo", "hello from a child process!"] diff --git a/extensions/test-extension/languages/gleam/config.toml b/extensions/test-extension/languages/gleam/config.toml deleted file mode 100644 index 51874945e2..0000000000 --- a/extensions/test-extension/languages/gleam/config.toml +++ /dev/null @@ -1,12 +0,0 @@ -name = "Gleam" -grammar = "gleam" -path_suffixes = ["gleam"] -line_comments = ["// ", "/// "] -autoclose_before = ";:.,=}])>" -brackets = [ - { start = "{", end = "}", close = true, newline = true }, - { start = "[", end = "]", close = true, newline = true }, - { start = "(", end = ")", close = true, newline = true }, - { start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] }, -] -tab_size = 2 diff --git a/extensions/test-extension/languages/gleam/highlights.scm b/extensions/test-extension/languages/gleam/highlights.scm deleted file mode 100644 index 4b85b88d01..0000000000 --- a/extensions/test-extension/languages/gleam/highlights.scm +++ /dev/null @@ -1,130 +0,0 @@ -; Comments -(module_comment) @comment -(statement_comment) @comment -(comment) @comment - -; Constants -(constant - name: (identifier) @constant) - -; Variables -(identifier) @variable -(discard) @comment.unused - -; Modules -(module) @module -(import alias: (identifier) @module) -(remote_type_identifier - module: (identifier) @module) -(remote_constructor_name - module: (identifier) @module) -((field_access - record: (identifier) @module - field: (label) @function) - (#is-not? local)) - -; Functions -(unqualified_import (identifier) @function) -(unqualified_import "type" (type_identifier) @type) -(unqualified_import (type_identifier) @constructor) -(function - name: (identifier) @function) -(external_function - name: (identifier) @function) -(function_parameter - name: (identifier) @variable.parameter) -((function_call - function: (identifier) @function) - (#is-not? local)) -((binary_expression - operator: "|>" - right: (identifier) @function) - (#is-not? local)) - -; "Properties" -; Assumed to be intended to refer to a name for a field; something that comes -; before ":" or after "." -; e.g. record field names, tuple indices, names for named arguments, etc -(label) @property -(tuple_access - index: (integer) @property) - -; Attributes -(attribute - "@" @attribute - name: (identifier) @attribute) - -(attribute_value (identifier) @constant) - -; Type names -(remote_type_identifier) @type -(type_identifier) @type - -; Data constructors -(constructor_name) @constructor - -; Literals -(string) @string -((escape_sequence) @warning - ; Deprecated in v0.33.0-rc2: - (#eq? @warning "\\e")) -(escape_sequence) @string.escape -(bit_string_segment_option) @function.builtin -(integer) @number -(float) @number - -; Reserved identifiers -; TODO: when tree-sitter supports `#any-of?` in the Rust bindings, -; refactor this to use `#any-of?` rather than `#match?` -((identifier) @warning - (#match? @warning "^(auto|delegate|derive|else|implement|macro|test|echo)$")) - -; Keywords -[ - (visibility_modifier) ; "pub" - (opacity_modifier) ; "opaque" - "as" - "assert" - "case" - "const" - ; DEPRECATED: 'external' was removed in v0.30. - "external" - "fn" - "if" - "import" - "let" - "panic" - "todo" - "type" - "use" -] @keyword - -; Operators -(binary_expression - operator: _ @operator) -(boolean_negation "!" @operator) -(integer_negation "-" @operator) - -; Punctuation -[ - "(" - ")" - "[" - "]" - "{" - "}" - "<<" - ">>" -] @punctuation.bracket -[ - "." - "," - ;; Controversial -- maybe some are operators? - ":" - "#" - "=" - "->" - ".." - "-" - "<-" -] @punctuation.delimiter diff --git a/extensions/test-extension/languages/gleam/indents.scm b/extensions/test-extension/languages/gleam/indents.scm deleted file mode 100644 index 112b414aa4..0000000000 --- a/extensions/test-extension/languages/gleam/indents.scm +++ /dev/null @@ -1,3 +0,0 @@ -(_ "[" "]" @end) @indent -(_ "{" "}" @end) @indent -(_ "(" ")" @end) @indent diff --git a/extensions/test-extension/languages/gleam/outline.scm b/extensions/test-extension/languages/gleam/outline.scm deleted file mode 100644 index 5df7a6af80..0000000000 --- a/extensions/test-extension/languages/gleam/outline.scm +++ /dev/null @@ -1,31 +0,0 @@ -(external_type - (visibility_modifier)? @context - "type" @context - (type_name) @name) @item - -(type_definition - (visibility_modifier)? @context - (opacity_modifier)? @context - "type" @context - (type_name) @name) @item - -(data_constructor - (constructor_name) @name) @item - -(data_constructor_argument - (label) @name) @item - -(type_alias - (visibility_modifier)? @context - "type" @context - (type_name) @name) @item - -(function - (visibility_modifier)? @context - "fn" @context - name: (_) @name) @item - -(constant - (visibility_modifier)? @context - "const" @context - name: (_) @name) @item diff --git a/extensions/test-extension/src/test_extension.rs b/extensions/test-extension/src/test_extension.rs deleted file mode 100644 index 0b96f47038..0000000000 --- a/extensions/test-extension/src/test_extension.rs +++ /dev/null @@ -1,209 +0,0 @@ -use std::fs; -use zed::lsp::CompletionKind; -use zed::{CodeLabel, CodeLabelSpan, LanguageServerId}; -use zed_extension_api::process::Command; -use zed_extension_api::{self as zed, Result}; - -struct TestExtension { - cached_binary_path: Option, -} - -impl TestExtension { - fn language_server_binary_path( - &mut self, - language_server_id: &LanguageServerId, - _worktree: &zed::Worktree, - ) -> Result { - let (platform, arch) = zed::current_platform(); - - let current_dir = std::env::current_dir().unwrap(); - println!("current_dir: {}", current_dir.display()); - assert_eq!( - current_dir.file_name().unwrap().to_str().unwrap(), - "test-extension" - ); - - fs::create_dir_all(current_dir.join("dir-created-with-abs-path")).unwrap(); - fs::create_dir_all("./dir-created-with-rel-path").unwrap(); - fs::write("file-created-with-rel-path", b"contents 1").unwrap(); - fs::write( - current_dir.join("file-created-with-abs-path"), - b"contents 2", - ) - .unwrap(); - assert_eq!( - fs::read("file-created-with-rel-path").unwrap(), - b"contents 1" - ); - assert_eq!( - fs::read("file-created-with-abs-path").unwrap(), - b"contents 2" - ); - - let command = match platform { - zed::Os::Linux | zed::Os::Mac => Command::new("echo"), - zed::Os::Windows => Command::new("cmd").args(["/C", "echo"]), - }; - let output = command.arg("hello from a child process!").output()?; - println!( - "command output: {}", - String::from_utf8_lossy(&output.stdout).trim() - ); - - if let Some(path) = &self.cached_binary_path - && fs::metadata(path).is_ok_and(|stat| stat.is_file()) - { - return Ok(path.clone()); - } - - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::CheckingForUpdate, - ); - let release = zed::latest_github_release( - "gleam-lang/gleam", - zed::GithubReleaseOptions { - require_assets: true, - pre_release: false, - }, - )?; - - let ext = "tar.gz"; - let download_type = zed::DownloadedFileType::GzipTar; - - // Do this if you want to actually run this extension - - // the actual asset is a .zip. But the integration test is simpler - // if every platform uses .tar.gz. - // - // ext = "zip"; - // download_type = zed::DownloadedFileType::Zip; - - let asset_name = format!( - "gleam-{version}-{arch}-{os}.{ext}", - version = release.version, - arch = match arch { - zed::Architecture::Aarch64 => "aarch64", - zed::Architecture::X86 => "x86", - zed::Architecture::X8664 => "x86_64", - }, - os = match platform { - zed::Os::Mac => "apple-darwin", - zed::Os::Linux => "unknown-linux-musl", - zed::Os::Windows => "pc-windows-msvc", - }, - ); - - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; - - let version_dir = format!("gleam-{}", release.version); - let binary_path = format!("{version_dir}/gleam"); - - if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::Downloading, - ); - - zed::download_file(&asset.download_url, &version_dir, download_type) - .map_err(|e| format!("failed to download file: {e}"))?; - - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::None, - ); - - let entries = - fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; - for entry in entries { - let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; - let filename = entry.file_name(); - let filename = filename.to_str().unwrap(); - if filename.starts_with("gleam-") && filename != version_dir { - fs::remove_dir_all(entry.path()).ok(); - } - } - } - - self.cached_binary_path = Some(binary_path.clone()); - Ok(binary_path) - } -} - -impl zed::Extension for TestExtension { - fn new() -> Self { - Self { - cached_binary_path: None, - } - } - - fn language_server_command( - &mut self, - language_server_id: &LanguageServerId, - worktree: &zed::Worktree, - ) -> Result { - Ok(zed::Command { - command: self.language_server_binary_path(language_server_id, worktree)?, - args: vec!["lsp".to_string()], - env: Default::default(), - }) - } - - fn label_for_completion( - &self, - _language_server_id: &LanguageServerId, - completion: zed::lsp::Completion, - ) -> Option { - let name = &completion.label; - let ty = strip_newlines_from_detail(&completion.detail?); - let let_binding = "let a"; - let colon = ": "; - let assignment = " = "; - let call = match completion.kind? { - CompletionKind::Function | CompletionKind::Constructor => "()", - _ => "", - }; - let code = format!("{let_binding}{colon}{ty}{assignment}{name}{call}"); - - Some(CodeLabel { - spans: vec![ - CodeLabelSpan::code_range({ - let start = let_binding.len() + colon.len() + ty.len() + assignment.len(); - start..start + name.len() - }), - CodeLabelSpan::code_range({ - let start = let_binding.len(); - start..start + colon.len() - }), - CodeLabelSpan::code_range({ - let start = let_binding.len() + colon.len(); - start..start + ty.len() - }), - ], - filter_range: (0..name.len()).into(), - code, - }) - } -} - -zed::register_extension!(TestExtension); - -/// Removes newlines from the completion detail. -/// -/// The Gleam LSP can return types containing newlines, which causes formatting -/// issues within the Zed completions menu. -fn strip_newlines_from_detail(detail: &str) -> String { - let without_newlines = detail - .replace("->\n ", "-> ") - .replace("\n ", "") - .replace(",\n", ""); - - let comma_delimited_parts = without_newlines.split(','); - comma_delimited_parts - .map(|part| part.trim()) - .collect::>() - .join(", ") -} diff --git a/extensions/workflows/bump_version.yml b/extensions/workflows/bump_version.yml deleted file mode 100644 index 7f4318dcf5..0000000000 --- a/extensions/workflows/bump_version.yml +++ /dev/null @@ -1,52 +0,0 @@ -# Generated from xtask::workflows::extensions::bump_version within the Zed repository. -# Rebuild with `cargo xtask workflows`. -name: extensions::bump_version -on: - pull_request: - types: - - labeled - push: - branches: - - main - paths-ignore: - - .github/** - workflow_dispatch: {} -jobs: - determine_bump_type: - runs-on: namespace-profile-16x32-ubuntu-2204 - steps: - - id: get-bump-type - name: extensions::bump_version::get_bump_type - run: | - if [ "$HAS_MAJOR_LABEL" = "true" ]; then - bump_type="major" - elif [ "$HAS_MINOR_LABEL" = "true" ]; then - bump_type="minor" - else - bump_type="patch" - fi - echo "bump_type=$bump_type" >> $GITHUB_OUTPUT - shell: bash -euxo pipefail {0} - env: - HAS_MAJOR_LABEL: |- - ${{ (github.event.action == 'labeled' && github.event.label.name == 'major') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'major')) }} - HAS_MINOR_LABEL: |- - ${{ (github.event.action == 'labeled' && github.event.label.name == 'minor') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'minor')) }} - outputs: - bump_type: ${{ steps.get-bump-type.outputs.bump_type }} - call_bump_version: - needs: - - determine_bump_type - if: github.event.action != 'labeled' || needs.determine_bump_type.outputs.bump_type != 'patch' - uses: zed-industries/zed/.github/workflows/extension_bump.yml@main - secrets: - app-id: ${{ secrets.ZED_ZIPPY_APP_ID }} - app-secret: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }} - with: - bump-type: ${{ needs.determine_bump_type.outputs.bump_type }} - force-bump: true -concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}labels - cancel-in-progress: true diff --git a/extensions/workflows/release_version.yml b/extensions/workflows/release_version.yml deleted file mode 100644 index f752931917..0000000000 --- a/extensions/workflows/release_version.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Generated from xtask::workflows::extensions::release_version within the Zed repository. -# Rebuild with `cargo xtask workflows`. -name: extensions::release_version -on: - push: - tags: - - v** -jobs: - call_release_version: - uses: zed-industries/zed/.github/workflows/extension_release.yml@main - secrets: - app-id: ${{ secrets.ZED_ZIPPY_APP_ID }} - app-secret: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }} diff --git a/extensions/workflows/run_tests.yml b/extensions/workflows/run_tests.yml deleted file mode 100644 index 81ba76c483..0000000000 --- a/extensions/workflows/run_tests.yml +++ /dev/null @@ -1,16 +0,0 @@ -# Generated from xtask::workflows::extensions::run_tests within the Zed repository. -# Rebuild with `cargo xtask workflows`. -name: extensions::run_tests -on: - pull_request: - branches: - - '**' - push: - branches: - - main -jobs: - call_extension_tests: - uses: zed-industries/zed/.github/workflows/extension_tests.yml@main -concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}pr - cancel-in-progress: true diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 3074b947ef..0000000000 --- a/flake.lock +++ /dev/null @@ -1,77 +0,0 @@ -{ - "nodes": { - "crane": { - "locked": { - "lastModified": 1762538466, - "narHash": "sha256-8zrIPl6J+wLm9MH5ksHcW7BUHo7jSNOu0/hA0ohOOaM=", - "owner": "ipetkov", - "repo": "crane", - "rev": "0cea393fffb39575c46b7a0318386467272182fe", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, - "flake-compat": { - "locked": { - "lastModified": 1761588595, - "narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 315532800, - "narHash": "sha256-5CwQ80ucRHiqVbMEEbTFnjz70/axSJ0aliyzSaFSkmY=", - "rev": "f6b44b2401525650256b977063dbcf830f762369", - "type": "tarball", - "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre891648.f6b44b240152/nixexprs.tar.xz" - }, - "original": { - "type": "tarball", - "url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz" - } - }, - "root": { - "inputs": { - "crane": "crane", - "flake-compat": "flake-compat", - "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay" - } - }, - "rust-overlay": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1762915112, - "narHash": "sha256-d9j1g8nKmYDHy+/bIOPQTh9IwjRliqaTM0QLHMV92Ic=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "aa1e85921cfa04de7b6914982a94621fbec5cc02", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index fe7a09701b..0000000000 --- a/flake.nix +++ /dev/null @@ -1,66 +0,0 @@ -{ - description = "High-performance, multiplayer code editor from the creators of Atom and Tree-sitter"; - - inputs = { - nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"; - rust-overlay = { - url = "github:oxalica/rust-overlay"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - crane.url = "github:ipetkov/crane"; - flake-compat.url = "github:edolstra/flake-compat"; - }; - - outputs = - { - nixpkgs, - rust-overlay, - crane, - ... - }: - let - systems = [ - "x86_64-linux" - "x86_64-darwin" - "aarch64-linux" - "aarch64-darwin" - ]; - - forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system}); - mkZed = - pkgs: - let - rustBin = rust-overlay.lib.mkRustBin { } pkgs; - in - pkgs.callPackage ./nix/build.nix { - crane = crane.mkLib pkgs; - rustToolchain = rustBin.fromRustupToolchainFile ./rust-toolchain.toml; - }; - in - rec { - packages = forAllSystems (pkgs: rec { - default = mkZed pkgs; - debug = default.override { profile = "dev"; }; - }); - devShells = forAllSystems (pkgs: { - default = pkgs.callPackage ./nix/shell.nix { - zed-editor = packages.${pkgs.hostPlatform.system}.default; - }; - }); - formatter = forAllSystems (pkgs: pkgs.nixfmt-rfc-style); - overlays.default = final: _: { - zed-editor = mkZed final; - }; - }; - - nixConfig = { - extra-substituters = [ - "https://zed.cachix.org" - "https://cache.garnix.io" - ]; - extra-trusted-public-keys = [ - "zed.cachix.org-1:/pHQ6dpMsAZk2DiP4WCL0p9YDNKWj2Q5FL20bNmw1cU=" - "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" - ]; - }; -} diff --git a/legal/privacy-policy.md b/legal/privacy-policy.md deleted file mode 100644 index eaf8ece781..0000000000 --- a/legal/privacy-policy.md +++ /dev/null @@ -1,268 +0,0 @@ ---- -title: Privacy Policy -slug: privacy-policy ---- - -At Zed Industries, Inc. ("Zed", "Company" or "we"), we take privacy and the security of data seriously. This Privacy Policy is established to help advise you about how we treat your personal data. By using or accessing our website located at zed.dev, or the Solution or services available pursuant the Zed End User Terms located at [https://zed.dev/terms](https://zed.dev/terms) (collectively, the "Services"), you acknowledge awareness of the practices and policies outlined below, and hereby consent that we will collect, use, and share your personal data as described in this Privacy Policy. - -As we grow and expand our Services, we may modify this Privacy Policy from time to time. When material modifications are made, we will alert you to any such changes by placing a notice on the Company website, by sending you an email and/or by some other means. Please note that if you've opted not to receive legal notice emails from us (or haven't provided us with a valid email address), those legal notices will still govern your use of the Services. If you use the Services after any changes to the Privacy Policy have been published on our website, you consent and agree to all of the changes. - -## What this Privacy Policy Covers - -Our Privacy Policy covers how we treat Personal Data that we gather when you access or use our Services. "Personal Data" means information that identifies or relates to a particular individual and includes information referred to as "personally identifiable information" or "personal information" under applicable data privacy laws, rules or regulations. Our Privacy Policy does not cover the practices of companies we don't own or control or people we don't manage. - -## Personal Data - -### Categories of Personal Data We Collect - -This chart details the categories of Personal Data that we collect and have collected over the past 12 months: - -| Category of personal data | Examples of data we collect | Categories of third parties with whom we share this data | -| ----------------------------- | ------------------------------------------------------------- | -------------------------------------------------------- | -| Profile or contact data | First and last name
Email address
GitHub username | Cloud infrastructure providers
Analytics providers | -| IP data | IP address & derived geolocation data | Cloud infrastructure providers
Analytics providers | -| Web analytics | Interactions
Referrer
Request IDs
Statistics | Cloud infrastructure providers
Analytics providers | -| Photos, videos and recordings | Screenshots
Videos and video recordings you share with us | Cloud infrastructure providers | -| Audio, screenshare data | Audio and screen sharing during collaboration calls | Cloud infrastructure providers | - -Note that "collection" does not necessarily imply long-term storage. - -### Categories of Sources of Personal Data - -We collect Personal Data about you from the following categories of sources: - -#### You - -- When you provide such information directly to us. Examples include: - - When you create an account - - When you voluntarily provide information through our Services or through responses to surveys or questionnaires. - - When you send us an email or otherwise contact us. - - When you sign up to our mailing list. -- When you use our hosted Services and such information is collected automatically. Examples include: - - Cookies (defined in the "Tracking Tools and Opt-Out" section below). -- When you use the client software we provide on your machine. Examples include: - - Authentication information when you sign in. - - Version and system metadata when the software checks for updates. - - Usage data, unless you opt out. - - Crash reports, unless you opt out. - - When you make requests to language models we host for you. - - Zed does not store or train on your requests without consent. - - Other relevant data necessary to provide you with our Services. - -#### Third Parties - -- When you login to the service using a third-party service like GitHub. -- Information collected by content delivery networks or similar service providers -- We may use analytics providers to analyze how you interact and engage with the Services, or third parties may help us provide you with customer support. - -## Our Business Purposes for Collecting or Disclosing Personal Data - -- Providing, Customizing and Improving the Services - - Creating and managing your account or other user profiles. - - Processing orders or other fee-based transactions; billing. - - Providing you with the products, services or information you request. - - Meeting or fulfilling the reason you provided the information to us. - - Providing support and assistance for the Services. - - Improving the Services, including testing, research, internal analytics and product development. - - Doing fraud protection, security and debugging. - - Carrying out other business purposes stated when collecting your Personal Data or as otherwise set forth in applicable data privacy laws. -- Marketing the Services - - Marketing and selling the Services. -- Corresponding with You - - Responding to correspondence that we receive from you, contacting you when necessary or requested, and sending you information about Zed or our Services. - - Sending emails and other communications according to your preferences or that display content that we think will interest you. -- Meeting Legal Requirements and Enforcing Legal Terms - - Fulfilling our legal obligations under applicable law, regulation, court order or other legal process, such as preventing, detecting and investigating security incidents and potentially illegal or prohibited activities. - - Protecting the rights, property or safety of you, Zed or another party. - - Enforcing any agreements with you. - - Responding to claims that any posting or other content violates third-party rights. - - Resolving disputes. - -We will not collect additional categories of Personal Data or use the Personal Data we collected for materially different, unrelated or incompatible purposes without providing you notice as is described above. - -## How We Disclose Your Personal Data - -We disclose your Personal Data to categories of service providers and other parties listed in this section. Some of these disclosures may constitute a "sale" of your Personal Data as defined under applicable laws. For more information, please refer to the state-specific sections below. - -- Service Providers. These parties help us provide the Services or perform business functions on our behalf. They include: - - Hosting, technology and communication providers. - - Providers of artificial intelligence or machine learning models - - Payment processors. - - If you are using our Services on a fee-basis, our payment processing partner Stripe, Inc. ("Stripe") collects your voluntarily-provided payment card information necessary to process your payment. - - Please see Stripe Terms of Service and Stripe Privacy Policy for information on its use and storage of your Personal Data. -- Analytics Partners. These parties provide analytics on web traffic or usage of the Services. They include: - - Companies that track how users found or were referred to the Services. - - Companies that track how users interact with the Services. -- Authorized authentication providers (e.g. GitHub OAuth) - -### Fulfilling Legal Obligations - -We may share any Personal Data that we collect with third parties in relation to the activities set forth under "Meeting Legal Requirements and Enforcing Legal Terms" in the "Our Business Purposes for Collecting Personal Data" section above. - -### Business Transfers - -Personal Data collected may be transferred to a third party if we undergo a merger, acquisition, bankruptcy or other transaction in which such third party assumes control of our business (in whole or in part). In such an event, we will make reasonable efforts to notify you before your information becomes subject to different privacy and security policies and practices as authorized or mandated by applicable law. - -## Data that is Not Personal Data - -We may create aggregated, de-identified or anonymized data from the Personal Data we collect, including by removing information that makes the data personally identifiable to a particular user. We may use such aggregated, de-identified or anonymized data and share it with third parties for our lawful business purposes, including to analyze, build and improve the Services and promote our business, provided that we will not share such data in a manner that could identify you. - -## Tracking Tools and Opt-Out - -The Services use cookies and similar technologies such as pixel tags, web beacons, clear GIFs and JavaScript (collectively, "Cookies") to enable our servers to recognize your web browser, tell us how and when you visit and use our Services, analyze trends, learn about our user base and operate and improve our Services. Cookies are small pieces of data– usually text files – placed on your computer, tablet, phone or similar device when you use that device to access our Services. We may also supplement the information we collect from you with information received from third parties, including third parties that have placed their own Cookies on your device(s). - -### We use the following types of Cookies: - -- Essential Cookies. Essential Cookies are required for providing you with features or services that you have requested. For example, certain Cookies enable you to log into secure areas of our Services. Disabling these Cookies may make certain features and services unavailable. -- Functional Cookies. Functional Cookies are used to record your choices and settings regarding our Services, maintain your preferences over time and recognize you when you return to our Services. These Cookies help us to personalize our content for you, greet you by name and remember your preferences (for example, your choice of language or region). -- Performance/Analytical Cookies. Performance/Analytical Cookies allow us to understand how visitors use our Services. They do this by collecting information about the number of visitors to the Services, what pages visitors view on our Services and how long visitors are viewing pages on the Services. Performance/Analytical Cookies also help us measure the performance of our advertising campaigns to help us improve our campaigns and Services' content for those who engage with our advertising. - -You can decide whether or not to accept Cookies through your internet browser's settings. Most browsers have an option for turning off the Cookie feature, which will prevent your browser from accepting new Cookies, as well as (depending on the sophistication of your browser software) allow you to decide on acceptance of each new Cookie in a variety of ways. You can also delete all Cookies that are already on your device. If you do this, however, you may have to manually adjust some preferences every time you visit our website and some of the Services and functionalities may not work. - -To find out more information about Cookies generally, including information about how to manage and delete Cookies, please visit [https://allaboutcookies.org/](https://allaboutcookies.org/) or [https://ico.org.uk/for-the-public/online/cookies/](https://ico.org.uk/for-the-public/online/cookies/) if you are located in the European Union. - -## Data Security - -We endeavor to protect your Personal Data from unauthorized access, use and disclosure using appropriate physical, technical, organizational and administrative security measures based on our Services,the type of Personal Data being collected and how we are processing that data. You should also help protect your data by selecting and protecting your password and/or other sign-on mechanism(s) with care; limiting access to your computer or device and browser; and signing off after you have finished accessing your account. Although we work to protect the security of your account and other data that we hold in our records, be aware that no method of transmitting data over the internet or storing data is completely secure. - -## Data Retention - -We retain Personal Data about you for as long as reasonably necessary to provide you with our Services or otherwise in support of our business or commercial purposes for utilization of your Personal Data, as expressed. When establishing a retention period for particular categories of data, we consider who we collected the data from, our need for the Personal Data, why we collected the Personal Data, and the sensitivity of the Personal Data. In some cases we retain Personal Data for a longer period, if doing so is necessary to comply with our legal obligations, resolve disputes or collect fees owed, or as is otherwise permitted or required by applicable law, rule or regulation. We may further retain information in an anonymous or aggregated form where such information would not identify you personally. - -For example: - -- We retain your profile information and credentials for as long as you have an account with us. -- We retain your payment data for as long as we need to process your purchase or subscription. -- We retain your device/IP data for as long as we need it to ensure that our systems are working appropriately, effectively and efficiently. - -It's worth noting that we avoid retaining data unless necessary to provide our Service. For example: - -- We do not currently store source code that we proxy during collaboration sessions. -- We do not currently store audio or video recordings of Collaboration calls handled by LiveKit. - -## Personal Data of Children - -We do not knowingly collect or solicit Personal Data from children under 13 years of age; if you are a child under the age of 13, please do not attempt to register for or otherwise use the Services or send us any Personal Data. If we learn we have collected Personal Data from a child under 13 years of age, we will delete that information as quickly as possible. If you believe that a child under 13 years of age may have provided Personal Data to us, please contact us at hi@zed.dev. - -## California Resident Rights - -If you are a California resident, you have the rights set forth in this section. Please see the "Exercising Your Rights" section below for instructions regarding how to exercise these rights. Please note that we may process Personal Data of our customers' end users or employees in connection with our provision of certain services to our customers. If we are processing your Personal Data as a service provider, you may contact the entity that collected your Personal Data in the first instance to address your rights with respect to such data as desired. - -If there are any conflicts between this section and any other provision of this Privacy Policy and you are a California resident, the portion that is more protective of Personal Data shall control to the extent of such conflict. If you have any questions about this section or whether any of the following rights apply to you, please contact us at hi@zed.dev. - -### Access - -You have the right to request certain information about our collection and use of your Personal Data over the past 12 months. In response, we will provide you with the following information: - -- The categories of Personal Data that we have collected about you. -- The categories of sources from which that Personal Data was collected. -- The business or commercial purpose for collecting or selling your Personal Data. -- The categories of third parties with whom we have shared your Personal Data. -- The specific pieces of Personal Data that we have collected about you. -- If we have disclosed your Personal Data to any third parties for a business purpose over the past 12 months, we will identify the categories of Personal Data shared with each category of third party recipient. If we have sold your Personal Data over the past 12 months, we will identify the categories of Personal Data sold to each category of third party recipient. - -### Deletion - -You have the right to request that we delete the Personal Data that we have collected about you. Under the CCPA, this right is subject to certain exceptions: for e.g., we may need to retain your Personal Data to provide you with the Services or complete a transaction or other action you may have requested, or if deletion of your Personal Data involves disproportionate effort to achieve. If your deletion request is subject to one of these exceptions, we may deny your deletion request to such data. - -### Correction - -You have the right to request that we correct any inaccurate Personal Data we have collected about you. Under the CCPA, this right is subject to certain exceptions: for example, if we reasonably decide, based on the totality of circumstances related to your Personal Data, that such data is correct. If your correction request is subject to one of these CCPA exceptions, we may deny your request to correct such data. - -### Processing of Sensitive Personal Information Opt-Out - -Consumers have certain rights over the processing of their sensitive information. However, we do not intentionally collect sensitive categories of personal information, but it is possible to share sensitive information with us through your use of the Services. It is your responsibility not to share any such sensitive information when you use the Services. - -### Personal Data Sales Opt-Out and Opt-In - -We will not sell your Personal Data, and have not done so over the last 12 months. To our knowledge, we do not sell the Personal Data of minors under 16 years of age. Under the CCPA, California residents have certain rights when a business "shares" Personal Data with third parties for purposes of cross-contextual behavioral advertising. We have shared the foregoing categories of Personal Data for the purposes of cross-contextual behavioral advertising, as applicable. - -Under California Civil Code Sections 1798.83-1798.84, California residents are entitled to contact us to prevent disclosure of Personal Data to third parties for such third parties' direct marketing purposes; in order to submit such a request, please contact us at hi@zed.dev. - -Your browser may offer you a "Do Not Track" option, which allows you to signal to operators of websites and web applications and services that you do not wish such operators to track certain of your online activities over time and across different websites. Our Services do not support Do Not Track requests at this time. To find out more about "Do Not Track," you can visit [www.allaboutdnt.com](https://www.allaboutdnt.com). - -### Exercising Your Rights under CCPA - -To exercise the rights described in this Privacy Policy, you or, if you are a California resident, your Authorized Agent (as defined below) can send us a request that (1) provides sufficient information to allow us to adequately verify that you are the person about whom we have collected Personal Data, and (2) describes your request in sufficient detail to allow us to understand, evaluate and respond ( a "Valid Request"). We are not obligated to respond to requests that do not meet these criteria. We will only use Personal Data provided in a Valid Request to verify your identity and complete your request. - -We are committed to respond to Valid Requests within the time frame required by applicable law. We will not charge you a fee for making a Valid Request unless your Valid Request(s) is excessive, repetitive or manifestly unfounded. If we determine that your Valid Request warrants a fee, we will notify you of the fee and explain that decision before completing your request. - -You may submit a Valid Request using the following methods: - -- Email us at: hi@zed.dev - -If you are a California resident, you may also authorize an agent (an "Authorized Agent") to exercise your rights on your behalf. - -### We Will Not Discriminate Against You for Exercising Your Rights - -We will not discriminate against you for exercising your rights under applicable data protection laws. We will not deny you our goods or services, charge you different prices or rates, or provide you a lower quality of goods and services if you exercise your rights under applicable law. However, we may offer different tiers of our Services, as allowed by applicable law, with varying prices, rates or levels of quality of the goods or services you receive related to the value of Personal Data that we receive from you. - -# European Union and United Kingdom Data Subject Rights - -## EU and UK Residents - -If you are a resident of the European Union ("EU"), United Kingdom ("UK"), Lichtenstein, Norway or Iceland, you may have additional rights under the EU or UK General Data Protection Regulation (the "GDPR") with respect to your Personal Data, as outlined below. -We use the terms "Personal Data" and "processing" as they are defined in the GDPR in this section, but "Personal Data" generally means information that can be used to individually identify a person, and "processing" generally covers actions that can be performed in connection with data such as collection, use, storage and disclosure. Company will be the controller of your Personal Data processed in connection with the Services. -If there are any conflicts between this section and any other provision of this Privacy Policy, the policy or portion that is more protective of Personal Data shall control to the extent of such conflict. If you have any questions about this section or whether any of the following applies to you, please contact us at hi@zed.dev. Note that we may also process Personal Data of our customers' end users or employees in connection with our provision of certain services to you, in which case we are the processor of Personal Data. If we are the processor of your Personal Data, please contact the controller party in the first instance to address your rights with respect to such data. - -## Personal Data We Collect - -The "Categories of Personal Data We Collect" section above details the Personal Data that we collect from you. - -## Personal Data Use and Processing Grounds - -The "Our Commercial or Business Purposes for Collecting Personal Data" section above explains how we use your Personal Data. - -We will only process your Personal Data if we have a lawful basis for doing so. Lawful bases for processing include consent, contractual necessity and our "legitimate interests" or the legitimate interest of others, as further described below. - -- Contractual Necessity: We process the following categories of Personal Data as a matter of "contractual necessity", meaning that we need to process the data to perform under our End User Terms with you, which enables us to provide you with the Services. When we process data due to contractual necessity, failure to provide such Personal Data will result in your inability to use some or all portions of the Services that require such data. - - Profile or Contact Data - - Payment Data -- Legitimate Interest: We process the following categories of Personal Data when we believe it furthers the legitimate interest of us or third parties: - - Device/IP Data - - Web Analytics - - We may also de-identify or anonymize Personal Data to further our legitimate interests. -- Examples of these legitimate interests include (as described in more detail above): - - Providing, customizing and improving the Services. - - Marketing the Services. - - Corresponding with you. - - Meeting legal requirements and enforcing legal terms. - - Completing corporate transactions. -- Consent: In some cases, we process Personal Data based on the consent you expressly grant to us at the time we collect such data. - - Other Processing Grounds: From time to time we may also need to process Personal Data to comply with a legal obligation, if it is necessary to protect the interests of you or other data subjects, or if it is necessary in the public interest. - -## Sharing Personal Data - -The "How We Share Your Personal Data" section above details how we share your Personal Data with third parties. - -## EU Data Subject Rights - -For more information about these EU or UK personal data terms and your rights related thereto, or to submit a request for information, please email us at hi@zed.dev. Please note that in some circumstances, we may not be able to fully comply with your request, such as if it is frivolous or impractical, if it jeopardizes the rights of others, or if it is not required by law, but, in those circumstances, we are committed to respond to notify you of such a decision regardless. In some cases, we may also need you to provide us with additional information, which may include Personal Data, if necessary to verify your identity and the nature of your request. - -- Access: You can request more information about the Personal Data we hold about you and request a copy of such Personal Data. You can also access certain of your Personal Data by logging on to your account. -- Rectification: If you believe that any Personal Data we are holding about you is incorrect or incomplete, you can request that we correct or supplement such data. You can also correct some of this information directly by logging on to your account. -- Erasure: You can request that we erase some or all of your Personal Data from our systems. -- Withdrawal of Consent: If we are processing your Personal Data based on your consent, you have the right to withdraw your consent at any time. Please note, however, that if you exercise this right, you may have to then provide express consent on a case-by-case basis for the use or disclosure of certain of your Personal Data, if such use or disclosure is necessary to enable you to utilize some or all of our Services. -- Portability: You can ask for a copy of your Personal Data in a machine-readable format. You can also request that we transmit the data to another controller where technically feasible. -- Objection: You can contact us to let us know that you object to the further use or disclosure of your Personal Data for certain purposes, such as for direct marketing purposes. -- Restriction of Processing: You can ask us to restrict further processing of your Personal Data. -- Right to File Complaint: You have the right to lodge a complaint about Company's practices with respect to your Personal Data with the supervisory authority of your country or EU Member State. A list of Supervisory Authorities is available here: [https://edpb.europa.eu/about-edpb/board/members_en](https://edpb.europa.eu/about-edpb/board/members_en) - -## Transfers of Personal Data - -The Services are hosted and operated in the United States ("U.S.") through Company and its service providers. By using the Services, you acknowledge that any Personal Data about you is being provided to Company in the U.S. and will be hosted on U.S. servers, and you authorize Company to transfer, store and process your information to and in the U.S., and possibly other countries. In some circumstances, your Personal Data may be transferred to the U.S. pursuant to a data processing agreement incorporating legally required data protection clauses. - -# Contact Information: - -If you have additional questions about this Privacy Policy, the methods in which we collect and use your Personal Data or your choices and rights regarding such collection and use, please do not hesitate to contact us at: - -- Website: zed.dev -- Email Address: hi@zed.dev -- Corporate Address: - Zed Industries, Inc. - 2590 Welton St - Suite 200 - PO Box 1916 - Denver CO 80205 - -**DATE: May 6, 2025** diff --git a/legal/subprocessors.md b/legal/subprocessors.md deleted file mode 100644 index df3a5f7c9f..0000000000 --- a/legal/subprocessors.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: Subprocessor List -slug: subprocessors ---- - -This page provides information about the Subprocessors Zed has engaged to provide processing activities on Customer Data as defined in the [Zed End User Terms](https://zed.dev/terms). - -| Subprocessor | Purpose | Location | -| ------------------- | ------------------------ | ------------- | -| Cloudflare | Cloud Infrastructure | Worldwide | -| Amazon Web Services | Cloud Infrastructure | United States | -| DigitalOcean | Cloud Infrastructure | United States | -| Vercel | Cloud Infrastructure | United States | -| ConvertKit | Email Marketing | United States | -| Axiom | Analytics | United States | -| Hex Technologies | Analytics | United States | -| Snowflake | Analytics | United States | -| LiveKit | Audio/Video Conferencing | United States | -| GitHub | Authentication | United States | -| Anthropic | AI Services | United States | -| BaseTen | AI Services | United States | -| Exa Labs | AI Services | United States | -| Google | AI Services | United States | -| OpenAI | AI Services | United States | - -**DATE: May 6th, 2025** diff --git a/legal/terms.md b/legal/terms.md deleted file mode 100644 index 88afa36aa9..0000000000 --- a/legal/terms.md +++ /dev/null @@ -1,197 +0,0 @@ ---- -title: Zed End User Terms -slug: terms ---- - -PLEASE READ THESE TERMS AND CONDITIONS CAREFULLY BEFORE USING THE SERVICE OR SOFTWARE OFFERED BY ZED INDUSTRIES, INC. ("ZED", OR "WE"). BY ACCESSING OR USING THE SOLUTION (AS DEFINED BELOW) IN ANY MANNER, YOU ("YOU" OR "CUSTOMER") AGREE TO BE BOUND BY THESE TERMS (THE "AGREEMENT") TO THE EXCLUSION OF ALL OTHER TERMS. YOU REPRESENT AND WARRANT THAT YOU HAVE THE AUTHORITY TO ENTER INTO THIS AGREEMENT; IF YOU ARE ENTERING INTO THIS AGREEMENT ON BEHALF OF AN ORGANIZATION OR ENTITY, REFERENCES TO "CUSTOMER" AND "YOU" IN THIS AGREEMENT, REFER TO THAT ORGANIZATION OR ENTITY. IF YOU DO NOT AGREE TO ALL OF THE FOLLOWING, YOU MAY NOT USE OR ACCESS THE SOLUTION IN ANY MANNER. IF THE TERMS OF THIS AGREEMENT ARE CONSIDERED AN OFFER, ACCEPTANCE IS EXPRESSLY LIMITED TO SUCH TERMS. - -## 1. ACCESS TO AND USE OF THE SOLUTION - -Subject to the terms and conditions of this Agreement, Zed hereby grants to You, and You hereby accept from Zed, a term-limited, non-exclusive, non-transferable, non-assignable and non-sublicensable license to make use of the Editor for Your internal use only, and subject to the use limitations in Section 2.2. - -## 2. TERMS APPLICABLE TO THE EDITOR - -### 2.1. License Grant - -Subject to the terms and conditions of this Agreement, Zed hereby grants to You, and You hereby accept from Zed, a term-limited, non-exclusive, non-transferable, non-assignable and non-sublicensable license to make use of the Editor for Your internal use only, and subject to the use limitations in Section 2.2. - -### 2.2. License Limitations - -You agree that You shall not: (a) exceed the scope of the licenses granted in Section 2.1; (b) make copies of the Editor; (c) distribute, sublicense, assign, delegate, rent, lease, sell, time-share or otherwise transfer the benefits of, use under, or rights to, the license granted in Section 2.1; (d) reverse engineer, decompile, disassemble or otherwise attempt to learn the source code, structure or algorithms underlying the Editor, except to the extent required to be permitted under applicable law; (e) modify, translate or create derivative works of the Editor; or (f) remove any copyright, trademark, patent or other proprietary notice that appears on the Editor or copies thereof. - -### 2.3. Open Source Software - -Zed makes certain versions of the Editor and related software available at the Zed GitHub Repository: [https://github.com/zed-industries/zed](https://github.com/zed-industries/zed) (the "Repo"). Your use of such software is subject to the open source software licenses declared in the Repo. - -## 3. TERMS APPLICABLE TO THE ZED SERVICE - -### 3.1. Access to and Scope of Zed Service - -If you have elected to use the Zed Service by enabling or activating the Zed Service, Zed will use commercially reasonable efforts to make the Zed Service available to You as set forth in this Agreement. Once you elected to use the Zed Service, You may access and use the Zed Service during the Term, subject to Your compliance with the terms and conditions of the Agreement. - -### 3.2. Restrictions - -You will use the Zed Service only in accordance with all applicable laws, including, but not limited to, laws related to data (whether applicable within the United States, the European Union, or otherwise). You agree not to (and will not allow any third party to): (i) remove or otherwise alter any proprietary notices or labels from the Zed Service or any portion thereof; (ii) reverse engineer, decompile, disassemble, or otherwise attempt to discover the underlying structure, ideas, or algorithms of the Zed Service or any software used to provide or make the Zed Service available; or (iii) rent, resell or otherwise allow any third party access to or use of the Zed Service. Zed may suspend Your access to or use of the Zed Service as follows: (a) immediately if Zed reasonably believes Your use of the Zed Service may pose a security risk to or may adversely impact the Zed Service; or (b) if You are in breach of this Agreement. - -### 3.3. Customer Data - -You are solely responsible for Customer Data including, but not limited to: (a) compliance with all applicable laws and this Agreement; (b) any claims relating to Customer Data; and (c) any claims that Customer Data infringes, misappropriates, or otherwise violates the rights of any third party. You agree and acknowledge that Customer Data may be irretrievably deleted if Your account is terminated. For purposes of this Agreement, "Customer Data" shall mean any data, information or other material provided, uploaded, or submitted by You to the Zed Service in the course of using the Zed Service. Notwithstanding anything to the contrary, You represent and warrant that You will not transfer or make available to Zed any personally identifiable information or related information subject to applicable data privacy laws or regulations, unless otherwise agreed to in writing by Zed. - -#### 3.3.1. Customer Data Made Available to Zed - -To the extent You elect to make Customer Data available to Zed, the same may only be used by Zed according to the Customer Data type and the use rights regarding the same as described herein: - -#### 3.3.2. Usage Data - -To improve the Editor and understand how You use it, Zed optionally collects the following usage data: - -- (a) file extensions of opened files; -- (b) features and tools You use within the Editor; -- (c) project statistics (e.g., number of files); and -- (d) frameworks detected in Your projects - -(a-d collectively, "Usage Data"). Usage Data does not include any of Your software code or sensitive project details. You may change Your preferences disabling the collection of Usage Data and You can audit Usage Data collected by the Editor at any time. See [https://zed.dev/docs/telemetry](https://zed.dev/docs/telemetry) for more. - -Usage Data is associated with a secure random telemetry ID which may be linked to Your email address. This linkage currently serves two purposes: (1) it allows Zed to analyze usage patterns over time while maintaining Your privacy; and (2) it enables Zed to reach out to specific user groups for feedback and improvement suggestions. Zed may contact You based on Your usage patterns to better understand your needs and improve the Solution. If You delete Your account, the link between Your telemetry ID and Your email address will be permanently removed. By continuing to use Editor or Solution with this feature enabled You agree to this Usage Data collection. - -#### 3.3.3. Crash Reports - -Customer Data consisting of data related to the behavior of the Solution prior to a crash or failure, such as stack traces are collected and classified as "Crash Reports". Zed will use commercially reasonable efforts to exclude any personally identifiable information from Crash Reports, but due to the nature of a crash, Zed does not ensure that information such as paths will be excluded from Crash Reports. Crash Reports will be used solely for Zed's internal purposes in connection with diagnosing defects in the Solution that led to the crash. You may grant us permission to capture Crash Reports when installing or activating the Solution, and You may change Your preferences at any time in the settings feature of the Solution. Once You grant us this permission, Zed will retain the Crash Reports indefinitely. - -#### 3.3.4. User Content - -• You may access, modify or create certain data or information in connection with your access or use of the Zed Editor or the Solution. Such data and information may include, but is not limited to any of the following: - -- (a) file contents and associated metadata (e.g., filename, paths, size, timestamps); -- (b) source control history, comments and metadata (e.g., git history, commit messages); -- (c) configuration data (e.g., settings, keymaps); -- (d) anything typed, pasted and/or displayed on screen while using the Editor; -- (e) derivative works of the above generated by the Editor (e.g., format conversions, summaries, indexes, caches); -- (f) metadata, code and other derivative works of the above returned by language servers and other local tooling; and -- (g) metadata, code and other derivative works of the above returned by services integrated with the Zed Editor - -(a-g collectively, "User Content"). - -#### 3.3.5. Handling of User Content - -Zed will make use of or transfer User Content only as specified in this Agreement, or as necessary to comply with applicable law. - -#### 3.3.5.1. Zed Collaboration Services - -When using Zed Collaboration Services, User Content is transmitted from Your environment only if You collaborate with other Zed users by electing to share a project in the Editor. Once You share a project, Zed may transmit User Content consisting of file paths, file contents, and metadata regarding the code returned by language servers. Currently, Zed does not persist any User Content beyond the Your collaboration session. If You unshare a project or disconnect from the Solution, all information associated with such project will be deleted from Zed servers. In the future, Zed may save User Content regarding projects beyond the scope of a single collaboration session. We may share such User Content with those users You elected to grant access to. Zed's access to such User Content is limited to debugging and making improvements to the Solution. - -#### 3.3.5.2. Other Services - -The Zed Editor supports integration with API-based services maintained and not operated by Zed (the "Other Services"). By way of example, Other Services includes those made available by GitHub, Anthropic, OpenAI, and similar providers, or those You host or manage directly. You may configure the Zed Editor to interoperate, communicate with, and exchange data (including User Content) directly with the Other Services. Zed is not responsible or otherwise liable with respect to Your use of any Other Service, including but not limited to the exchange of data between the Other Service and the Zed Editor. The terms and conditions, including the applicable privacy policy, with respect to the Other Service are those made available by the applicable Other Service, not these Terms. - -#### 3.3.5.3. Zed AI Services - -The Zed Editor supports integration with API-based services maintained and operated by Zed (the "Zed AI Services"). You may elect to use Zed AI Services as the provider for various Zed Editor features (e.g., Agent Panel, Inline Assistant, Edit Predictions, and similar features). In connection with Your use of these features, the Zed Editor and Zed AI Services may make use of User Content to generate contextually relevant responses (the “Output”). Other than as specified in Section 3.3.5.4 of these Terms, Zed will not use User Content for training of its models, or disclose User Content. - -Output is provided "as is" without any warranties or guarantees of functionality, security, or fitness for a particular purpose. While efforts are made to ensure the accuracy and reliability, Output may include errors, vulnerabilities, and defects. You are responsible for reviewing, testing, and validating Output before use in any production or critical environment. Zed assumes no liability for any damages, losses, or liability arising from the use, modification, reliance on, or deployment of Output. Any such use is at Your own risk. - -#### 3.3.5.4. Improvement Feedback - -When using Zed AI Services to provide Edit Predictions in connection with certain open source software projects, You may elect to share requests, responses and feedback comments (collectively "Model Improvement Feedback") with Zed, and Zed may use the same to improve Zed Edit Predictions models. You may opt-out of sharing Model Improvement Feedback at any time. - -For more information on Zed Edit Predictions please see: [https://zed.dev/docs/ai/ai-improvement](https://zed.dev/docs/ai/ai-improvement) - -When using Zed AI Services in connection with the Agent Panel, You may elect to share with Zed requests, responses and feedback regarding the Agent Panel and related Output (the “Agent Improvement Feedback”) with Zed, and Zed may use the same to improve the Agent Panel and related Output. Zed will only collect Agent Improvement Feedback when You elect to share the same. - -For more information regarding the Agent Panel please see: [https://zed.dev/docs/ai/ai-improvement](https://zed.dev/docs/ai/ai-improvement) - -#### 3.4. Privacy Policy - -You and Zed are bound by the terms and conditions contained in the Zed Privacy Policy which is incorporated by reference hereto. The Zed Privacy Policy is available at the following URL: [https://zed.dev/privacy-policy](https://zed.dev/privacy-policy). - -## 4. FEE BASED SERVICES, FEES AND PAYMENT TERMS - -### 4.1. Fee Based Services - -The Zed AI Services is made available with additional usage benefits (the “Enhanced Use ”) as described in the table published at [zed.dev/pricing](https://zed.dev/pricing) (the “Pricing Table”), subject to the requirements and limitations set forth in the Pricing Table and these Terms. In order to make use of the Enhanced Use, Customer must access the Zed AI Services through a Zed registered account. - -### 4.2. Fees - -Customer shall pay to Zed the applicable fees set forth in Pricing Table, together with any applicable taxes and shipping and handling (collectively, the “Fees”). Customer shall have no right of return, and all Fees shall be non-refundable. - -### 4.3. Payment Terms - -All amounts payable to Zed under this Agreement shall be paid in United States dollars and paid Zed according to the method of payment, frequency and calculated as set forth in the Pricing Table. - -### 4.4. Taxes; Set-offs - -Any and all payments made by Customer in accordance with this Agreement are exclusive of any taxes that might be assessed by any jurisdiction. Customer shall pay or reimburse Zed for all sales, use, property and similar taxes; all customs duties, import fees, stamp duties, license fees and similar charges; and all other mandatory payments to government agencies of whatever kind, except taxes imposed on the net or gross income of Zed. All amounts payable to Zed under this Agreement shall be without set-off and without deduction of any taxes, levies, imposts, charges, withholdings and/or duties of any nature which may be levied or imposed, including without limitation, value added tax, customs duty and withholding tax. - -## 5. TERM AND TERMINATION - -### 5.1. Term - -The term of this Agreement shall commence on the date You first download the Editor or use the Zed Service (the "Effective Date"), and unless terminated earlier according to this Section 3, will end pursuant to this Section 5 (the "Term"). - -### 5.2. Termination - -This Agreement may be terminated: (a) by either party if the other has materially breached this Agreement; or (b) by Zed at any time and for any reason upon notice to Customer. You acknowledge that Zed is under no obligation to continue to operate the Zed Service or make the Editor available, and We may end any programs in connection with the same at any time. - -### 5.3. Effect of Termination and Survival - -Upon any expiration or termination of this Agreement, Customer shall (i) immediately cease use of the Zed Service, and (ii) return all Zed Confidential Information and other materials provided by Zed. The following provisions will survive termination of this Agreement: Sections 3.3 (Customer Data), Section 3.4 (Privacy Policy), Section 5.3 (Effect of Termination and Survival), Section 6 (Ownership), Section 7 (Indemnification), Section 9 (Limitation of Liability), Section 10 (Third Party Services), and Section 11 (Miscellaneous). - -## 6. OWNERSHIP - -Zed retains all right, title, and interest in and to the Zed Service, Editor, and any software, products, works or other intellectual property created, used, provided, or made available by Zed under or in connection with the Zed Service or Editor. Customer may from time to time provide suggestions, comments, or other feedback to Zed with respect to the Zed Service or Editor ("Feedback"). Customer shall, and hereby does, grant to Zed a nonexclusive, worldwide, perpetual, irrevocable, transferable, sublicensable, royalty-free, fully paid-up license to use and exploit the Feedback for any purpose. You retain all right, title and interest in and to the Customer Data, including all intellectual property rights therein. No intellectual property rights with respect to any software code you develop or modify with the Editor or Zed Service (collectively, the “Output”) are transferred or assigned to Zed hereunder. - -## 7. INDEMNIFICATION - -Customer will defend, indemnify, and hold Zed, its affiliates, suppliers and licensors harmless and each of their respective officers, directors, employees and representatives from and against any claims, damages, losses, liabilities, costs, and expenses (including reasonable attorneys' fees) arising out of or relating to any third party claim with respect to: (a) Customer Data; (b) breach of this Agreement or violation of applicable law by Customer; or (c) alleged infringement or misappropriation of third-party's intellectual property rights resulting from Customer Data. - -## 8. WARRANTY - -Zed does not represent or warrant that the operation of the Zed Service or Editor (or any portion thereof) will be uninterrupted or error free, or that the Zed Service or Editor (or any portion thereof) will operate in combination with other hardware, software, systems or data not provided by Zed. CUSTOMER ACKNOWLEDGES THAT, ZED MAKES NO EXPRESS OR IMPLIED REPRESENTATIONS OR WARRANTIES OF ANY KIND WITH RESPECT TO THE SERVICE OR SOFTWARE, OR THEIR CONDITION. ZED HEREBY EXPRESSLY EXCLUDES, ANY AND ALL OTHER EXPRESS OR IMPLIED REPRESENTATIONS OR WARRANTIES, WHETHER UNDER COMMON LAW, STATUTE OR OTHERWISE, INCLUDING WITHOUT LIMITATION ANY AND ALL WARRANTIES AS TO MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, SATISFACTORY QUALITY OR NON-INFRINGEMENT OF THIRD-PARTY RIGHTS. - -## 9. LIMITATIONS OF LIABILITY - -IN NO EVENT SHALL ZED BE LIABLE FOR ANY LOST DATA, LOST PROFITS, BUSINESS INTERRUPTION, REPLACEMENT SERVICE OR OTHER SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR INDIRECT DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THEORY OF LIABILITY. ZED'S LIABILITY FOR ALL CLAIMS ARISING UNDER THIS AGREEMENT, WHETHER IN CONTRACT, TORT OR OTHERWISE, SHALL NOT EXCEED THE GREATER OF: THE FEES PAID TO ZED BY CUSTOMER DURING THE TWELVE (12) MONTH PERIOD PRECEDING THE DATE OF THE CLAIM, OR ONE THOUSAND US DOLLARS ($1,000). - -## 10. Third Party Services - -Zed may make certain third party services available to You within the Editor or the Zed Service (each a "Third Party Service"). You acknowledge and agree that (a) use of each Third Party Service is subject to the corresponding terms and conditions available at the following URL: [https://zed.dev/third-party-terms](https://zed.dev/third-party-terms) and/or presented in connection with Your use of such Third Party Service; (b) the terms and conditions of this Agreement do not apply with respect to Your use of any Third Party Service; and (c) Zed is not liable in any way regarding Your use of any Third Party Service. - -## 11. MISCELLANEOUS - -### 11.1. Export Control - -You hereby certify that You will comply with all current US Export Control laws. You agree to defend, indemnify and hold Zed harmless from any liability for Your violation of U.S. Export Control laws. - -### 11.2. Compliance with Laws - -You shall comply with all applicable laws and regulations in its use of the Solution, including without limitation the unlawful gathering or collecting, or assisting in the gathering or collecting of information in violation of any privacy laws or regulations. You shall, at its own expense, defend, indemnify and hold harmless Zed from and against any and all claims, losses, liabilities, damages, judgments, government or federal sanctions, costs and expenses (including attorneys' fees) incurred by Zed arising from any claim or assertion by any third party of violation of privacy laws or regulations by You or any of its agents, officers, directors or employees. - -### 11.3. Assignment - -Neither party may transfer and assign its rights and obligations under this Agreement without the prior written consent of the other party. Notwithstanding the foregoing, Zed may transfer and assign its rights under this Agreement without consent from the other party in connection with a change in control, acquisition or sale of all or substantially all of its assets. - -### 11.4. Force Majeure - -Neither party shall be responsible for failure or delay in performance by events out of their reasonable control, including but not limited to, acts of God, Internet outage, terrorism, war, fires, earthquakes and other disasters (each a "Force Majeure"). Notwithstanding the foregoing: if a Force Majeure continues for more than thirty (30) days, either party may to terminate this agreement by written notice to the other party. - -### 11.5. Notice - -All notices between the parties shall be in writing and shall be deemed to have been given if personally delivered or sent by registered or certified mail (return receipt), or by recognized courier service. - -### 11.6. No Agency - -Both parties agree that no agency, partnership, joint venture, or employment is created as a result of this Agreement. You do not have any authority of any kind to bind Zed. - -### 11.7. Governing Law - -This Agreement shall be governed exclusively by, and construed exclusively in accordance with, the laws of the United States and the State of California, without regard to its conflict of laws provisions. The federal courts of the United States in the Northern District of California and the state courts of the State of California shall have exclusive jurisdiction to adjudicate any dispute arising out of or relating to this Agreement. Each party hereby consents to the jurisdiction of such courts and waives any right it may otherwise have to challenge the appropriateness of such forums, whether on the basis of the doctrine of forum non conveniens or otherwise. The United Nations Convention on Contracts for the International Sale of Goods shall not apply to this Agreement or any Purchase Order issued under this Agreement. - -### 11.8. Updated Agreement - -Zed reserves the right to update this Agreement at any time. The terms and conditions of the updated version of the Agreement shall apply to the Zed Service and Editor downloaded, or accessed following the date of publication of the updated version. If You do not agree with any terms of the updated Agreement, You may not use or access the Zed Service or Editor in any manner. Zed may from time-to-time provide release notes applicable to the Editor or Zed Service, and such release notes may contain additional use restrictions or terms applicable to Customer Data. Your use of the Editor or Zed Service after the applicable release notes are made available shall be subject to the additional use restrictions or terms applicable to Customer Data. - -### 11.9. Entire Agreement - -This Agreement is the complete and exclusive statement of the mutual understanding of the parties and supersedes and cancels all previous written and oral agreements, communications, and other understandings relating to the subject matter of this Agreement, and all waivers and modifications must be in a writing signed by both parties, except as otherwise provided herein. Any term or provision of this Agreement held to be illegal or unenforceable shall be, to the fullest extent possible, interpreted so as to be construed as valid, but in any event the validity or enforceability of the remainder hereof shall not be affected. - -**DATE: May 6, 2025** diff --git a/legal/third-party-terms.md b/legal/third-party-terms.md deleted file mode 100644 index 4c4a0f6cce..0000000000 --- a/legal/third-party-terms.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: 3rd Party Terms -slug: third-party-terms ---- - -In addition to the [Zed End User Terms](/terms) and [Zed Privacy Policy](/privacy-policy) usage of certain Zed features may also subject you to additional 3rd party terms and conditions. These terms and conditions may include, but are not limited to, the following: - -## Anthropic - -- [Anthropic Usage Policy](https://www.anthropic.com/legal/aup) -- [Anthropic Privacy Policy](https://www.anthropic.com/legal/privacy) -- [Anthropic Commercial Terms of Service](https://www.anthropic.com/legal/commercial-terms) - -## Baseten - -- [BaseTen Terms and Conditions](https://www.baseten.co/terms-and-conditions/) - -### Exa.ai - -- [Exa Labs Terms and Conditions](https://exa.ai/assets/Exa_Labs_Terms_of_Service.pdf) -- [Exa Labs Privacy Policy](https://exa.ai/privacy-policy) - -## GitHub - -- [GitHub Terms of Service](https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) -- [GitHub Privacy Statement](https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement) -- [GitHub Acceptable Use Policies](https://docs.github.com/en/site-policy/acceptable-use-policies/github-acceptable-use-policies) -- [GitHub Copilot Product Specific Terms](https://github.com/customer-terms/github-copilot-product-specific-terms) - -## Google - -- [Google APIs Terms of Service](https://developers.google.com/terms) -- [Google Gemini API Additional Terms of Service](https://ai.google.dev/gemini-api/terms) -- [Google Generative AI Prohibited Use Policy](https://policies.google.com/terms/generative-ai/use-policy) - -## LiveKit - -- [LiveKit Terms of Service](https://livekit.io/legal/terms-of-service) -- [LiveKit Privacy Policy](https://livekit.io/legal/privacy-policy) - -## OpenAI - -- [OpenAI Terms of Use](https://openai.com/policies/terms-of-use/) -- [OpenAI Privacy Policy](https://openai.com/policies/privacy-policy/) -- [OpenAI Business terms](https://openai.com/policies/business-terms/) -- [OpenAI Service terms](https://openai.com/policies/service-terms/) - -## SuperMaven - -- [SuperMaven Terms of Service](https://supermaven.com/terms-of-service) -- [SuperMaven Privacy Policy](https://supermaven.com/privacy-policy) - -**DATE: May 6, 2025** diff --git a/livekit.yaml b/livekit.yaml deleted file mode 100644 index f2d1946721..0000000000 --- a/livekit.yaml +++ /dev/null @@ -1,10 +0,0 @@ -development: true -port: 7880 -rtc: - udp_port: 7882 - tcp_port: 7881 - use_external_ip: false -keys: - devkey: secret -logging: - level: debug diff --git a/lychee.toml b/lychee.toml deleted file mode 100644 index a769ae3b20..0000000000 --- a/lychee.toml +++ /dev/null @@ -1,29 +0,0 @@ -retry_wait_time = 5 - -accept = ["200..=204", "429"] - -max_retries = 5 - -timeout = 45 - -exclude = [ - # Don't fail CI check if collab is down - 'https://staging-collab.zed.dev/', - "https://collab.zed.dev", - - # Slow and unreliable server. - 'https://repology.org', - - # The following websites are rate limited or use bot detection and aren't nice enough to respond with 429: - 'https://openai.com', - 'https://claude.ai/download', - 'https://www.perplexity.ai', - 'https://platform.deepseek.com', - 'https://console.anthropic.com', - 'https://platform.openai.com', - 'https://linux.die.net/man/1/sed', - 'https://allaboutcookies.org', - 'https://www.gnu.org', - 'https://auth.mistral.ai', - 'https://console.mistral.ai', -] diff --git a/nix/build.nix b/nix/build.nix deleted file mode 100644 index 484049a421..0000000000 --- a/nix/build.nix +++ /dev/null @@ -1,321 +0,0 @@ -{ - lib, - stdenv, - - apple-sdk_15, - darwin, - darwinMinVersionHook, - - cargo-about, - cargo-bundle, - crane, - rustPlatform, - rustToolchain, - - copyDesktopItems, - envsubst, - fetchFromGitHub, - makeFontsConf, - makeWrapper, - - alsa-lib, - cmake, - curl, - fontconfig, - freetype, - git, - libgit2, - libglvnd, - libxkbcommon, - livekit-libwebrtc, - nodejs_22, - openssl, - perl, - pkg-config, - protobuf, - sqlite, - vulkan-loader, - wayland, - xorg, - zlib, - zstd, - - withGLES ? false, - profile ? "release", -}: -assert withGLES -> stdenv.hostPlatform.isLinux; -let - mkIncludeFilter = - root': path: type: - let - # note: under lazy-trees this introduces an extra copy - root = toString root' + "/"; - relPath = lib.removePrefix root path; - topLevelIncludes = [ - "crates" - "assets" - "extensions" - "script" - "tooling" - "Cargo.toml" - ".config" # nextest? - ".cargo" - ]; - firstComp = builtins.head (lib.path.subpath.components relPath); - in - builtins.elem firstComp topLevelIncludes; - - craneLib = crane.overrideToolchain rustToolchain; - gpu-lib = if withGLES then libglvnd else vulkan-loader; - commonArgs = - let - zedCargoLock = builtins.fromTOML (builtins.readFile ../crates/zed/Cargo.toml); - stdenv' = stdenv; - in - rec { - pname = "zed-editor"; - version = zedCargoLock.package.version + "-nightly"; - src = builtins.path { - path = ../.; - filter = mkIncludeFilter ../.; - name = "source"; - }; - - cargoLock = ../Cargo.lock; - - nativeBuildInputs = - [ - cmake - copyDesktopItems - curl - perl - pkg-config - protobuf - cargo-about - rustPlatform.bindgenHook - ] - ++ lib.optionals stdenv'.hostPlatform.isLinux [ makeWrapper ] - ++ lib.optionals stdenv'.hostPlatform.isDarwin [ - (cargo-bundle.overrideAttrs ( - new: old: { - version = "0.6.1-zed"; - src = fetchFromGitHub { - owner = "zed-industries"; - repo = "cargo-bundle"; - rev = "2be2669972dff3ddd4daf89a2cb29d2d06cad7c7"; - hash = "sha256-cSvW0ND148AGdIGWg/ku0yIacVgW+9f1Nsi+kAQxVrI="; - }; - cargoHash = "sha256-urn+A3yuw2uAO4HGmvQnKvWtHqvG9KHxNCCWTiytE4k="; - - # NOTE: can drop once upstream uses `finalAttrs` here: - # https://github.com/NixOS/nixpkgs/blob/10214747f5e6e7cb5b9bdf9e018a3c7b3032f5af/pkgs/build-support/rust/build-rust-package/default.nix#L104 - # - # See (for context): https://github.com/NixOS/nixpkgs/pull/382550 - cargoDeps = rustPlatform.fetchCargoVendor { - inherit (new) src; - hash = new.cargoHash; - patches = new.cargoPatches or []; - name = new.cargoDepsName or new.finalPackage.name; - }; - } - )) - ]; - - buildInputs = - [ - curl - fontconfig - freetype - # TODO: need staticlib of this for linking the musl remote server. - # should make it a separate derivation/flake output - # see https://crane.dev/examples/cross-musl.html - libgit2 - openssl - sqlite - zlib - zstd - ] - ++ lib.optionals stdenv'.hostPlatform.isLinux [ - alsa-lib - libxkbcommon - wayland - gpu-lib - xorg.libX11 - xorg.libxcb - ] - ++ lib.optionals stdenv'.hostPlatform.isDarwin [ - apple-sdk_15 - (darwinMinVersionHook "10.15") - ]; - - cargoExtraArgs = "-p zed -p cli --locked --features=gpui/runtime_shaders"; - - stdenv = - pkgs: - let - base = pkgs.llvmPackages.stdenv; - addBinTools = old: { - cc = old.cc.override { - inherit (pkgs.llvmPackages) bintools; - }; - }; - custom = lib.pipe base [ - (stdenv: stdenv.override addBinTools) - pkgs.stdenvAdapters.useMoldLinker - ]; - in - if stdenv'.hostPlatform.isLinux then custom else base; - - env = { - ZSTD_SYS_USE_PKG_CONFIG = true; - FONTCONFIG_FILE = makeFontsConf { - fontDirectories = [ - ../assets/fonts/lilex - ../assets/fonts/ibm-plex-sans - ]; - }; - ZED_UPDATE_EXPLANATION = "Zed has been installed using Nix. Auto-updates have thus been disabled."; - RELEASE_VERSION = version; - LK_CUSTOM_WEBRTC = livekit-libwebrtc; - PROTOC="${protobuf}/bin/protoc"; - - CARGO_PROFILE = profile; - # need to handle some profiles specially https://github.com/rust-lang/cargo/issues/11053 - TARGET_DIR = "target/" + (if profile == "dev" then "debug" else profile); - - # for some reason these deps being in buildInputs isn't enough, the only thing - # about them that's special is that they're manually dlopened at runtime - NIX_LDFLAGS = lib.optionalString stdenv'.hostPlatform.isLinux "-rpath ${ - lib.makeLibraryPath [ - gpu-lib - wayland - ] - }"; - - NIX_OUTPATH_USED_AS_RANDOM_SEED = "norebuilds"; - }; - - # prevent nix from removing the "unused" wayland/gpu-lib rpaths - dontPatchELF = stdenv'.hostPlatform.isLinux; - - # TODO: try craneLib.cargoNextest separate output - # for now we're not worried about running our test suite (or tests for deps) in the nix sandbox - doCheck = false; - - cargoVendorDir = craneLib.vendorCargoDeps { - inherit src cargoLock; - overrideVendorGitCheckout = - let - hasWebRtcSys = builtins.any (crate: crate.name == "webrtc-sys"); - # we can't set $RUSTFLAGS because that clobbers the cargo config - # see https://github.com/rust-lang/cargo/issues/5376#issuecomment-2163350032 - glesConfig = builtins.toFile "config.toml" '' - [target.'cfg(all())'] - rustflags = ["--cfg", "gles"] - ''; - - # `webrtc-sys` expects a staticlib; nixpkgs' `livekit-webrtc` has been patched to - # produce a `dylib`... patching `webrtc-sys`'s build script is the easier option - # TODO: send livekit sdk a PR to make this configurable - postPatch = - '' - substituteInPlace webrtc-sys/build.rs --replace-fail \ - "cargo:rustc-link-lib=static=webrtc" "cargo:rustc-link-lib=dylib=webrtc" - '' - + lib.optionalString withGLES '' - cat ${glesConfig} >> .cargo/config/config.toml - ''; - in - crates: drv: - if hasWebRtcSys crates then - drv.overrideAttrs (o: { - postPatch = (o.postPatch or "") + postPatch; - }) - else - drv; - }; - }; - cargoArtifacts = craneLib.buildDepsOnly commonArgs; -in -craneLib.buildPackage ( - lib.recursiveUpdate commonArgs { - inherit cargoArtifacts; - - dontUseCmakeConfigure = true; - - # without the env var generate-licenses fails due to crane's fetchCargoVendor, see: - # https://github.com/zed-industries/zed/issues/19971#issuecomment-2688455390 - # TODO: put this in a separate derivation that depends on src to avoid running it on every build - preBuild = '' - ALLOW_MISSING_LICENSES=yes bash script/generate-licenses - echo nightly > crates/zed/RELEASE_CHANNEL - ''; - - installPhase = - if stdenv.hostPlatform.isDarwin then - '' - runHook preInstall - - pushd crates/zed - sed -i "s/package.metadata.bundle-nightly/package.metadata.bundle/" Cargo.toml - export CARGO_BUNDLE_SKIP_BUILD=true - app_path="$(cargo bundle --profile $CARGO_PROFILE | xargs)" - popd - - mkdir -p $out/Applications $out/bin - # Zed expects git next to its own binary - ln -s ${git}/bin/git "$app_path/Contents/MacOS/git" - mv $TARGET_DIR/cli "$app_path/Contents/MacOS/cli" - mv "$app_path" $out/Applications/ - - # Physical location of the CLI must be inside the app bundle as this is used - # to determine which app to start - ln -s "$out/Applications/Zed Nightly.app/Contents/MacOS/cli" $out/bin/zed - - runHook postInstall - '' - else - '' - runHook preInstall - - mkdir -p $out/bin $out/libexec - cp $TARGET_DIR/zed $out/libexec/zed-editor - cp $TARGET_DIR/cli $out/bin/zed - ln -s $out/bin/zed $out/bin/zeditor # home-manager expects the CLI binary to be here - - - install -D "crates/zed/resources/app-icon-nightly@2x.png" \ - "$out/share/icons/hicolor/1024x1024@2x/apps/zed.png" - install -D crates/zed/resources/app-icon-nightly.png \ - $out/share/icons/hicolor/512x512/apps/zed.png - - # TODO: icons should probably be named "zed-nightly" - ( - export DO_STARTUP_NOTIFY="true" - export APP_CLI="zed" - export APP_ICON="zed" - export APP_NAME="Zed Nightly" - export APP_ARGS="%U" - mkdir -p "$out/share/applications" - ${lib.getExe envsubst} < "crates/zed/resources/zed.desktop.in" > "$out/share/applications/dev.zed.Zed-Nightly.desktop" - chmod +x "$out/share/applications/dev.zed.Zed-Nightly.desktop" - ) - - runHook postInstall - ''; - - # TODO: why isn't this also done on macOS? - postFixup = lib.optionalString stdenv.hostPlatform.isLinux '' - wrapProgram $out/libexec/zed-editor --suffix PATH : ${lib.makeBinPath [ nodejs_22 ]} - ''; - - meta = { - description = "High-performance, multiplayer code editor from the creators of Atom and Tree-sitter"; - homepage = "https://zed.dev"; - changelog = "https://zed.dev/releases/preview"; - license = lib.licenses.gpl3Only; - mainProgram = "zed"; - platforms = lib.platforms.linux ++ lib.platforms.darwin; - }; - } -) diff --git a/nix/shell.nix b/nix/shell.nix deleted file mode 100644 index 6956de8e8a..0000000000 --- a/nix/shell.nix +++ /dev/null @@ -1,63 +0,0 @@ -{ - mkShell, - makeFontsConf, - pkgsCross, - - zed-editor, - - rust-analyzer, - rustup, - cargo-nextest, - cargo-hakari, - cargo-machete, - cargo-zigbuild, - nixfmt-rfc-style, - protobuf, - nodejs_22, - zig, -}: -(mkShell.override { inherit (zed-editor) stdenv; }) { - inputsFrom = [ zed-editor ]; - packages = [ - rust-analyzer - rustup - cargo-nextest - cargo-hakari - cargo-machete - cargo-zigbuild - nixfmt-rfc-style - # TODO: package protobuf-language-server for editing zed.proto - # TODO: add other tools used in our scripts - - # `build.nix` adds this to the `zed-editor` wrapper (see `postFixup`) - # we'll just put it on `$PATH`: - nodejs_22 - zig - ]; - - env = - let - baseEnvs = - (zed-editor.overrideAttrs (attrs: { - passthru = { inherit (attrs) env; }; - })).env; # exfil `env`; it's not in drvAttrs - in - (removeAttrs baseEnvs [ - "LK_CUSTOM_WEBRTC" # download the staticlib during the build as usual - "ZED_UPDATE_EXPLANATION" # allow auto-updates - "CARGO_PROFILE" # let you specify the profile - "TARGET_DIR" - ]) - // { - # note: different than `$FONTCONFIG_FILE` in `build.nix` – this refers to relative paths - # outside the nix store instead of to `$src` - FONTCONFIG_FILE = makeFontsConf { - fontDirectories = [ - "./assets/fonts/lilex" - "./assets/fonts/ibm-plex-sans" - ]; - }; - PROTOC = "${protobuf}/bin/protoc"; - ZED_ZSTD_MUSL_LIB = "${pkgsCross.musl64.pkgsStatic.zstd.out}/lib"; - }; -} diff --git a/renovate.json b/renovate.json deleted file mode 100644 index 01ca7a46a1..0000000000 --- a/renovate.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:recommended", - ":semanticCommitsDisabled", - ":separateMultipleMajorReleases", - ":dependencyDashboardApproval", - "helpers:pinGitHubActionDigests", - "group:serdeMonorepo" - ], - "dependencyDashboard": true, - "timezone": "America/New_York", - "schedule": ["after 3pm on Wednesday"], - "prFooter": "Release Notes:\n\n- N/A", - "ignorePaths": ["**/node_modules/**"], - "packageRules": [ - { - "description": "Group wasmtime crates together.", - "groupName": "wasmtime", - "matchPackageNames": ["wasmtime{/,}**"] - } - ] -} diff --git a/script/analyze_highlights.py b/script/analyze_highlights.py deleted file mode 100644 index aaf7386be6..0000000000 --- a/script/analyze_highlights.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python3 -""" -This script analyzes all the highlights.scm files in our embedded languages and extensions. -It counts the number of unique instances of @{name} and the languages in which they are used. - -This is useful to help avoid accidentally introducing new tags when appropriate ones already exist when adding new languages. - -Flags: --v, --verbose: Include a detailed list of languages for each tag found in the highlights.scm files. -""" - -import argparse -import re -from collections import defaultdict -from pathlib import Path -from typing import Any - -pattern = re.compile(r'@(?!_)[a-zA-Z_.]+') - -def parse_arguments(): - parser = argparse.ArgumentParser(description='Analyze highlights.scm files for unique instances and their languages.') - parser.add_argument('-v', '--verbose', action='store_true', help='Include a list of languages for each tag.') - return parser.parse_args() - -def find_highlight_files(root_dir): - for path in Path(root_dir).rglob('highlights.scm'): - yield path - -def count_instances(files): - instances: defaultdict[list[Any], dict[str, Any]] = defaultdict(lambda: {'count': 0, 'languages': set()}) - for file_path in files: - language = file_path.parent.name - with open(file_path, "r") as file: - text = file.read() - matches = pattern.findall(text) - for match in matches: - instances[match]['count'] += 1 - instances[match]['languages'].add(language) - return instances - -def print_instances(instances, verbose=False): - for item, details in sorted(instances.items(), key=lambda x: x[0]): - languages = ', '.join(sorted(details['languages'])) - if verbose: - print(f"{item} ({details['count']}) - [{languages}]") - else: - print(f"{item} ({details['count']})") - -def main(): - args = parse_arguments() - - base_dir = Path(__file__).parent.parent - core_path = base_dir / 'crates/languages/src' - extension_path = base_dir / 'extensions/' - - core_instances = count_instances(find_highlight_files(core_path)) - extension_instances = count_instances(find_highlight_files(extension_path)) - - unique_extension_instances = {k: v for k, v in extension_instances.items() if k not in core_instances} - - print('Shared:\n') - print_instances(core_instances, args.verbose) - - if unique_extension_instances: - print('\nExtension-only:\n') - print_instances(unique_extension_instances, args.verbose) - -if __name__ == '__main__': - main() diff --git a/script/bootstrap b/script/bootstrap deleted file mode 100755 index 68888e04c1..0000000000 --- a/script/bootstrap +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -e - -# if root or if sudo/unavailable, define an empty variable -if [ "$(id -u)" -eq 0 ] -then maysudo='' -else maysudo="$(command -v sudo || command -v doas || true)" -fi - -if [[ "$OSTYPE" == "linux-gnu"* ]]; then - echo "Linux dependencies..." - script/linux -else - echo "installing foreman..." - which foreman > /dev/null || brew install foreman -fi - -# Install minio if needed -if ! which minio > /dev/null; then - if command -v brew > /dev/null; then - echo "minio not found. Installing via brew" - brew install minio/stable/minio - elif command -v apt > /dev/null; then - echo "minio not found. Installing via apt from https://dl.min.io/server/minio/release/linux-amd64/minio.deb" - wget -q https://dl.min.io/server/minio/release/linux-amd64/minio.deb -O /tmp/minio.deb - $maysudo apt install /tmp/minio.deb - rm -f /tmp/minio.deb - elif command -v dnf > /dev/null; then - echo "minio not found. Installing via dnf from https://dl.min.io/server/minio/release/linux-amd64/minio.rpm" - wget -q https://dl.min.io/server/minio/release/linux-amd64/minio.rpm -O /tmp/minio.rpm - $maysudo dnf install /tmp/minio.rpm - rm -f /tmp/minio.rpm - else - echo "No supported package manager found (brew, apt, or dnf)" - exit 1 - fi -fi - -# Install sqlx-cli if needed -if ! [[ "$(command -v sqlx)" && "$(sqlx --version)" == "sqlx-cli 0.7.2" ]]; then - echo "sqlx-cli not found or not the required version, installing version 0.7.2..." - cargo install sqlx-cli --version 0.7.2 -fi - -cd crates/collab - -# Export contents of .env.toml -eval "$(cargo run --bin dotenv)" - -echo "creating databases..." -sqlx database create --database-url "$DATABASE_URL" -sqlx database create --database-url "$LLM_DATABASE_URL" diff --git a/script/bootstrap.ps1 b/script/bootstrap.ps1 deleted file mode 100644 index 55d306d48a..0000000000 --- a/script/bootstrap.ps1 +++ /dev/null @@ -1,21 +0,0 @@ -$ErrorActionPreference = 'Stop' -$PSNativeCommandUseErrorActionPreference = $true - -$env:POWERSHELL = $true - -if (!(Get-Command sqlx -ErrorAction SilentlyContinue) -or (sqlx --version) -notlike "sqlx-cli 0.7.2") { - Write-Output "sqlx-cli not found or not the required version, installing version 0.7.2..." - cargo install sqlx-cli --version 0.7.2 -} - -Set-Location .\crates\collab - -# Export contents of .env.toml -$env = (cargo run --bin dotenv) -join "`n"; -Invoke-Expression $env - -Set-Location ../.. - -Write-Output "creating databases..." -sqlx database create --database-url "$env:DATABASE_URL" -sqlx database create --database-url "$env:LLM_DATABASE_URL" diff --git a/script/build-docker b/script/build-docker deleted file mode 100755 index c5ea294c73..0000000000 --- a/script/build-docker +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash - -# Use a docker BASE_IMAGE to test building Zed. -# e.g: ./script/bundle-docker ubuntu:20.04 -# -# Increasing resources available to podman may speed this up: -# podman machine stop -# podman machine set --memory 16384 --cpus 8 --disk-size 200 -# podman machine start - -set -euo pipefail - -BASE_IMAGE=${BASE_IMAGE:-${1:-}} -if [ -z "$BASE_IMAGE" ]; then - echo "Usage: $0 BASE_IMAGE" >&2 - exit 1 -fi - -export DOCKER_BUILDKIT=1 -cd "$(dirname "$0")/.." - -podman build . \ - -f Dockerfile-distros \ - -t many \ - --build-arg BASE_IMAGE="$BASE_IMAGE" diff --git a/script/bump-extension-cli b/script/bump-extension-cli deleted file mode 100755 index ee7ea6f8c4..0000000000 --- a/script/bump-extension-cli +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -set -e - -git pull --ff-only origin main -git tag -f extension-cli -git push -f origin extension-cli diff --git a/script/bump-gpui-version b/script/bump-gpui-version deleted file mode 100755 index 5112bde450..0000000000 --- a/script/bump-gpui-version +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash - -# Parse arguments -bump_type=${1:-minor} - -if [[ "$bump_type" != "minor" && "$bump_type" != "patch" ]]; then - echo "Usage: $0 [minor|patch]" - echo " minor (default): bumps the minor version (e.g., 0.1.0 -> 0.2.0)" - echo " patch: bumps the patch version (e.g., 0.1.0 -> 0.1.1)" - exit 1 -fi - -# Ensure we're in a clean state on an up-to-date `main` branch. -if [[ -n $(git status --short --untracked-files=no) ]]; then - echo "can't bump versions with uncommitted changes" - exit 1 -fi -if [[ $(git rev-parse --abbrev-ref HEAD) != "main" ]]; then - echo "this command must be run on main" - exit 1 -fi -git pull -q --ff-only origin main - - -# Parse the current version -version=$(script/get-crate-version gpui) -major=$(echo $version | cut -d. -f1) -minor=$(echo $version | cut -d. -f2) -patch=$(echo $version | cut -d. -f3) - -if [[ "$bump_type" == "minor" ]]; then - next_minor=$(expr $minor + 1) - next_version="${major}.${next_minor}.0" -else - next_patch=$(expr $patch + 1) - next_version="${major}.${minor}.${next_patch}" -fi - -branch_name="bump-gpui-to-v${next_version}" - -git checkout -b ${branch_name} - -script/lib/bump-version.sh gpui gpui-v "" $bump_type true - -git checkout -q main diff --git a/script/bump-nightly b/script/bump-nightly deleted file mode 100755 index 2419962387..0000000000 --- a/script/bump-nightly +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -set -e - -git fetch origin main:tags/nightly -f -git log --oneline -1 nightly -git push -f origin nightly diff --git a/script/bump-zed-minor-versions b/script/bump-zed-minor-versions deleted file mode 100755 index 536dbb6244..0000000000 --- a/script/bump-zed-minor-versions +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env bash - -set -eu - -# Ensure cargo-edit is installed -which cargo-set-version > /dev/null || cargo install cargo-edit - -# Ensure we're in a clean state on an up-to-date `main` branch. -if [[ -n $(git status --short --untracked-files=no) ]]; then - echo "can't bump versions with uncommitted changes" - exit 1 -fi -if [[ $(git rev-parse --abbrev-ref HEAD) != "main" ]]; then - echo "this command must be run on main" - exit 1 -fi -git pull -q --ff-only origin main - -# Parse the current version -version=$(script/get-crate-version zed) -major=$(echo $version | cut -d. -f1) -minor=$(echo $version | cut -d. -f2) -patch=$(echo $version | cut -d. -f3) -prev_minor=$(expr $minor - 1) -next_minor=$(expr $minor + 1) - -minor_branch_name="v${major}.${minor}.x" -prev_minor_branch_name="v${major}.${prev_minor}.x" -next_minor_branch_name="v${major}.${next_minor}.x" -preview_tag_name="v${major}.${minor}.${patch}-pre" -bump_main_branch_name="set-minor-version-to-${major}.${next_minor}" - -git fetch origin ${prev_minor_branch_name}:${prev_minor_branch_name} -git fetch origin --tags -cargo check -q - -function cleanup { - git checkout -q main -} -trap cleanup EXIT - -echo "Checking invariants before taking any actions..." -if [[ $(cat crates/zed/RELEASE_CHANNEL) != dev && $(cat crates/zed/RELEASE_CHANNEL) != nightly ]]; then - echo "release channel on main should be dev or nightly" - exit 1 -fi -if git show-ref --quiet refs/tags/${preview_tag_name}; then - echo "tag ${preview_tag_name} already exists" - exit 1 -fi -if git show-ref --quiet refs/heads/${minor_branch_name}; then - echo "branch ${minor_branch_name} already exists" - exit 1 -fi -if ! git show-ref --quiet refs/heads/${prev_minor_branch_name}; then - echo "previous branch ${minor_branch_name} doesn't exist" - exit 1 -fi -if [[ $(git show ${prev_minor_branch_name}:crates/zed/RELEASE_CHANNEL) != preview ]]; then - echo "release channel on branch ${prev_minor_branch_name} should be preview" - exit 1 -fi - -echo "Promoting existing branch ${prev_minor_branch_name} to stable..." -git checkout -q ${prev_minor_branch_name} -git clean -q -dff -stable_tag_name="v$(script/get-crate-version zed)" -if git show-ref --quiet refs/tags/${stable_tag_name}; then - echo "tag ${stable_tag_name} already exists" - exit 1 -fi -old_prev_minor_sha=$(git rev-parse HEAD) -echo -n stable > crates/zed/RELEASE_CHANNEL -git commit -q --all --message "${prev_minor_branch_name} stable" -git tag ${stable_tag_name} - -echo "Creating new preview branch ${minor_branch_name}..." -git checkout -q main -git checkout -q -b ${minor_branch_name} -echo -n preview > crates/zed/RELEASE_CHANNEL -git commit -q --all --message "${minor_branch_name} preview" -git tag ${preview_tag_name} - -echo "Preparing main for version ${next_minor_branch_name}..." -git checkout -q main -git clean -q -dff -git checkout -q -b ${bump_main_branch_name} -cargo set-version --package zed --bump minor -cargo check -q - -git commit -q --all --message "${next_minor_branch_name} dev" - -git checkout -q main - -cat <&2 - exit 1 - ;; -esac - -exec script/lib/bump-version.sh zed v "$tag_suffix" patch diff --git a/script/bundle-freebsd b/script/bundle-freebsd deleted file mode 100755 index 87c9459ffb..0000000000 --- a/script/bundle-freebsd +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env bash - -set -euxo pipefail -source script/lib/blob-store.sh - -# Function for displaying help info -help_info() { - echo " -Usage: ${0##*/} [options] -Build a release .tar.gz for FreeBSD. - -Options: - -h Display this help and exit. - " -} - -while getopts 'h' flag; do - case "${flag}" in - h) - help_info - exit 0 - ;; - esac -done - -export ZED_BUNDLE=true - -channel=$(/dev/null 2>&1; then -# rustup_installed=true -# fi -# Generate the licenses first, so they can be baked into the binaries -# script/generate-licenses -# if "$rustup_installed"; then -# rustup target add "$remote_server_triple" -# fi - -# export CC=$(which clang) - -# Build binary in release mode -export RUSTFLAGS="${RUSTFLAGS:-} -C link-args=-Wl,--disable-new-dtags,-rpath,\$ORIGIN/../lib" -# cargo build --release --target "${target_triple}" --package zed --package cli - -# Build remote_server in separate invocation to prevent feature unification from other crates -# from influencing dynamic libraries required by it. -# if [[ "$remote_server_triple" == "$musl_triple" ]]; then -# export RUSTFLAGS="${RUSTFLAGS:-} -C target-feature=+crt-static" -# fi -cargo build --release --target "${remote_server_triple}" --package remote_server - -# Strip debug symbols and save them for upload to DigitalOcean -# objcopy --only-keep-debug "${target_dir}/${target_triple}/release/zed" "${target_dir}/${target_triple}/release/zed.dbg" -# objcopy --only-keep-debug "${target_dir}/${remote_server_triple}/release/remote_server" "${target_dir}/${remote_server_triple}/release/remote_server.dbg" -# objcopy --strip-debug "${target_dir}/${target_triple}/release/zed" -# objcopy --strip-debug "${target_dir}/${target_triple}/release/cli" -# objcopy --strip-debug "${target_dir}/${remote_server_triple}/release/remote_server" - -# gzip -f "${target_dir}/${target_triple}/release/zed.dbg" -# gzip -f "${target_dir}/${remote_server_triple}/release/remote_server.dbg" - -# if [[ -n "${DIGITALOCEAN_SPACES_SECRET_KEY:-}" && -n "${DIGITALOCEAN_SPACES_ACCESS_KEY:-}" ]]; then -# upload_to_blob_store_public \ -# "zed-debug-symbols" \ -# "${target_dir}/${target_triple}/release/zed.dbg.gz" \ -# "$channel/zed-$version-${target_triple}.dbg.gz" -# upload_to_blob_store_public \ -# "zed-debug-symbols" \ -# "${target_dir}/${remote_server_triple}/release/remote_server.dbg.gz" \ -# "$channel/remote_server-$version-${remote_server_triple}.dbg.gz" -# fi - -# Ensure that remote_server does not depend on libssl nor libcrypto, as we got rid of these deps. -if ldd "${target_dir}/${remote_server_triple}/release/remote_server" | grep -q 'libcrypto\|libssl'; then - echo "Error: remote_server still depends on libssl or libcrypto" && exit 1 -fi - -suffix="" -if [ "$channel" != "stable" ]; then - suffix="-$channel" -fi - -# Move everything that should end up in the final package -# into a temp directory. -# temp_dir=$(mktemp -d) -# zed_dir="${temp_dir}/zed$suffix.app" - -# Binary -# mkdir -p "${zed_dir}/bin" "${zed_dir}/libexec" -# cp "${target_dir}/${target_triple}/release/zed" "${zed_dir}/libexec/zed-editor" -# cp "${target_dir}/${target_triple}/release/cli" "${zed_dir}/bin/zed" - -# Libs -# find_libs() { -# ldd ${target_dir}/${target_triple}/release/zed | -# cut -d' ' -f3 | -# grep -v '\<\(libstdc++.so\|libc.so\|libgcc_s.so\|libm.so\|libpthread.so\|libdl.so\|libasound.so\)' -# } - -# mkdir -p "${zed_dir}/lib" -# rm -rf "${zed_dir}/lib/*" -# cp $(find_libs) "${zed_dir}/lib" - -# Icons -# mkdir -p "${zed_dir}/share/icons/hicolor/512x512/apps" -# cp "crates/zed/resources/app-icon$suffix.png" "${zed_dir}/share/icons/hicolor/512x512/apps/zed.png" -# mkdir -p "${zed_dir}/share/icons/hicolor/1024x1024/apps" -# cp "crates/zed/resources/app-icon$suffix@2x.png" "${zed_dir}/share/icons/hicolor/1024x1024/apps/zed.png" - -# .desktop -# export DO_STARTUP_NOTIFY="true" -# export APP_CLI="zed" -# export APP_ICON="zed" -# export APP_ARGS="%U" -# if [[ "$channel" == "preview" ]]; then -# export APP_NAME="Zed Preview" -# elif [[ "$channel" == "nightly" ]]; then -# export APP_NAME="Zed Nightly" -# elif [[ "$channel" == "dev" ]]; then -# export APP_NAME="Zed Devel" -# else -# export APP_NAME="Zed" -# fi - -# mkdir -p "${zed_dir}/share/applications" -# envsubst <"crates/zed/resources/zed.desktop.in" >"${zed_dir}/share/applications/zed$suffix.desktop" -# chmod +x "${zed_dir}/share/applications/zed$suffix.desktop" - -# Copy generated licenses so they'll end up in archive too -# cp "assets/licenses.md" "${zed_dir}/licenses.md" - -# Create archive out of everything that's in the temp directory -arch=$(uname -m) -# target="freebsd-${arch}" -# if [[ "$channel" == "dev" ]]; then -# archive="zed-${commit}-${target}.tar.gz" -# else -# archive="zed-${target}.tar.gz" -# fi - -# rm -rf "${archive}" -# remove_match="zed(-[a-zA-Z0-9]+)?-linux-$(uname -m)\.tar\.gz" -# ls "${target_dir}/release" | grep -E ${remove_match} | xargs -d "\n" -I {} rm -f "${target_dir}/release/{}" || true -# tar -czvf "${target_dir}/release/$archive" -C ${temp_dir} "zed$suffix.app" - -gzip -f --stdout --best "${target_dir}/${remote_server_triple}/release/remote_server" \ - > "${target_dir}/zed-remote-server-freebsd-x86_64.gz" diff --git a/script/bundle-linux b/script/bundle-linux deleted file mode 100755 index 4f5c9f6e7e..0000000000 --- a/script/bundle-linux +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env bash - -set -euxo pipefail -source script/lib/blob-store.sh - -# Function for displaying help info -help_info() { - echo " -Usage: ${0##*/} [options] -Build a release .tar.gz for Linux. - -Options: - -h, --help Display this help and exit. - --flatpak Set ZED_BUNDLE_TYPE=flatpak so that this can be included in system info - " -} - -# Parse all arguments manually -while [[ $# -gt 0 ]]; do - case $1 in - -h|--help) - help_info - exit 0 - ;; - --flatpak) - export ZED_BUNDLE_TYPE=flatpak - shift - ;; - --) - shift - break - ;; - -*) - echo "Unknown option: $1" >&2 - help_info - exit 1 - ;; - *) - echo "Error: Unexpected argument: $1" >&2 - help_info - exit 1 - ;; - esac -done - -export ZED_BUNDLE=true - -channel=$(/dev/null 2>&1; then - rustup_installed=true -fi - -# Generate the licenses first, so they can be baked into the binaries -script/generate-licenses - -if "$rustup_installed"; then - rustup target add "$remote_server_triple" -fi - -export CC=$(which clang) - -# Build binary in release mode -export RUSTFLAGS="${RUSTFLAGS:-} -C link-args=-Wl,--disable-new-dtags,-rpath,\$ORIGIN/../lib" -cargo build --release --target "${target_triple}" --package zed --package cli -# Build remote_server in separate invocation to prevent feature unification from other crates -# from influencing dynamic libraries required by it. -if [[ "$remote_server_triple" == "$musl_triple" ]]; then - export RUSTFLAGS="${RUSTFLAGS:-} -C target-feature=+crt-static" -fi -cargo build --release --target "${remote_server_triple}" --package remote_server - -# Upload debug info to sentry.io -if ! command -v sentry-cli >/dev/null 2>&1; then - echo "sentry-cli not found. skipping sentry upload." - echo "install with: 'curl -sL https://sentry.io/get-cli | bash'" -else - if [[ -n "${SENTRY_AUTH_TOKEN:-}" ]]; then - echo "Uploading zed debug symbols to sentry..." - # note: this uploads the unstripped binary which is needed because it contains - # .eh_frame data for stack unwinding. see https://github.com/getsentry/symbolic/issues/783 - for attempt in 1 2 3; do - echo "Attempting sentry upload (attempt $attempt/3)..." - if sentry-cli debug-files upload --include-sources --wait -p zed -o zed-dev \ - "${target_dir}/${target_triple}"/release/zed \ - "${target_dir}/${remote_server_triple}"/release/remote_server; then - echo "Sentry upload successful on attempt $attempt" - break - else - echo "Sentry upload failed on attempt $attempt" - if [ $attempt -eq 3 ]; then - echo "All sentry upload attempts failed" - fi - fi - done - else - echo "missing SENTRY_AUTH_TOKEN. skipping sentry upload." - fi -fi - -# Strip debug symbols and save them for upload to DigitalOcean -objcopy --strip-debug "${target_dir}/${target_triple}/release/zed" -objcopy --strip-debug "${target_dir}/${target_triple}/release/cli" -objcopy --strip-debug "${target_dir}/${remote_server_triple}/release/remote_server" - -# Ensure that remote_server does not depend on libssl nor libcrypto, as we got rid of these deps. -if ldd "${target_dir}/${remote_server_triple}/release/remote_server" | grep -q 'libcrypto\|libssl'; then - if [[ "$remote_server_triple" == *-musl ]]; then - echo "Error: remote_server still depends on libssl or libcrypto" && exit 1 - else - echo "Info: Using non-musl remote-server build." - fi -fi - -suffix="" -if [ "$channel" != "stable" ]; then - suffix="-$channel" -fi - -# Move everything that should end up in the final package -# into a temp directory. -temp_dir=$(mktemp -d) -zed_dir="${temp_dir}/zed$suffix.app" - -# Binary -mkdir -p "${zed_dir}/bin" "${zed_dir}/libexec" -cp "${target_dir}/${target_triple}/release/zed" "${zed_dir}/libexec/zed-editor" -cp "${target_dir}/${target_triple}/release/cli" "${zed_dir}/bin/zed" - -# Libs -find_libs() { - ldd ${target_dir}/${target_triple}/release/zed |\ - cut -d' ' -f3 |\ - grep -v '\<\(libstdc++.so\|libc.so\|libgcc_s.so\|libm.so\|libpthread.so\|libdl.so\|libasound.so\)' -} - -mkdir -p "${zed_dir}/lib" -rm -rf "${zed_dir}/lib/*" -cp $(find_libs) "${zed_dir}/lib" - -# Icons -mkdir -p "${zed_dir}/share/icons/hicolor/512x512/apps" -cp "crates/zed/resources/app-icon$suffix.png" "${zed_dir}/share/icons/hicolor/512x512/apps/zed.png" -mkdir -p "${zed_dir}/share/icons/hicolor/1024x1024/apps" -cp "crates/zed/resources/app-icon$suffix@2x.png" "${zed_dir}/share/icons/hicolor/1024x1024/apps/zed.png" - -# .desktop -export DO_STARTUP_NOTIFY="true" -export APP_CLI="zed" -export APP_ICON="zed" -export APP_ARGS="%U" -if [[ "$channel" == "preview" ]]; then - export APP_NAME="Zed Preview" -elif [[ "$channel" == "nightly" ]]; then - export APP_NAME="Zed Nightly" -elif [[ "$channel" == "dev" ]]; then - export APP_NAME="Zed Devel" -else - export APP_NAME="Zed" -fi - -mkdir -p "${zed_dir}/share/applications" -envsubst < "crates/zed/resources/zed.desktop.in" > "${zed_dir}/share/applications/zed$suffix.desktop" -chmod +x "${zed_dir}/share/applications/zed$suffix.desktop" - -# Copy generated licenses so they'll end up in archive too -cp "assets/licenses.md" "${zed_dir}/licenses.md" - -# Create archive out of everything that's in the temp directory -arch=$(uname -m) -archive="zed-linux-${arch}.tar.gz" - -rm -rf "${archive}" -remove_match="zed(-[a-zA-Z0-9]+)?-linux-$(uname -m)\.tar\.gz" -ls "${target_dir}/release" | grep -E ${remove_match} | xargs -d "\n" -I {} rm -f "${target_dir}/release/{}" || true -tar -czvf "${target_dir}/release/$archive" -C ${temp_dir} "zed$suffix.app" - -gzip -f --stdout --best "${target_dir}/${remote_server_triple}/release/remote_server" > "${target_dir}/zed-remote-server-linux-${arch}.gz" diff --git a/script/bundle-mac b/script/bundle-mac deleted file mode 100755 index c6c925f073..0000000000 --- a/script/bundle-mac +++ /dev/null @@ -1,330 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail -source script/lib/blob-store.sh - -build_flag="--release" -target_dir="release" -open_result=false -local_install=false -can_code_sign=false - -# This must match the team in the provisioning profile. -IDENTITY="Zed Industries, Inc." -APPLE_NOTARIZATION_TEAM="MQ55VZLNZQ" - -# Function for displaying help info -help_info() { - echo " -Usage: ${0##*/} [options] [architecture=host] -Build the application bundle for macOS. - -Options: - -d Compile in debug mode - -o Open dir with the resulting DMG or launch the app itself in local mode. - -i Install the resulting DMG into /Applications. - -h Display this help and exit. - " -} - -while getopts 'dloih' flag -do - case "${flag}" in - o) open_result=true;; - d) - export CARGO_INCREMENTAL=true - export CARGO_BUNDLE_SKIP_BUILD=true - build_flag=""; - target_dir="debug" - ;; - i) local_install=true;; - h) - help_info - exit 0 - ;; - esac -done - -shift $((OPTIND-1)) - - -# Get release channel -pushd crates/zed -channel=$(&1 | head -n 1 || echo "") -if [ "$cargo_bundle_version" != "cargo-bundle v0.6.1-zed" ]; then - cargo install cargo-bundle --git https://github.com/zed-industries/cargo-bundle.git --branch zed-deploy -fi - -# Deal with versions of macOS that don't include libstdc++ headers -export CXXFLAGS="-stdlib=libc++" - -version_info=$(rustc --version --verbose) -host_line=$(echo "$version_info" | grep host) -target_triple=${host_line#*: } -if [[ $# -gt 0 && -n "$1" ]]; then - target_triple="$1" -fi -arch_suffix="" - -if [[ "$target_triple" = "x86_64-apple-darwin" ]]; then - arch_suffix="x86_64" -elif [[ "$target_triple" = "aarch64-apple-darwin" ]]; then - arch_suffix="aarch64" -else - echo "Unsupported architecture $target_triple" - exit 1 -fi - -# Generate the licenses first, so they can be baked into the binaries -script/generate-licenses - -rustup target add $target_triple - -echo "Compiling zed binaries" -cargo build ${build_flag} --package zed --package cli --target $target_triple -# Build remote_server in separate invocation to prevent feature unification from other crates -# from influencing dynamic libraries required by it. -cargo build ${build_flag} --package remote_server --target $target_triple - -echo "Creating application bundle" -pushd crates/zed -cp Cargo.toml Cargo.toml.backup -sed \ - -i.backup \ - "s/package.metadata.bundle-${channel}/package.metadata.bundle/" \ - Cargo.toml - -app_path=$(cargo bundle ${build_flag} --target $target_triple --select-workspace-root | xargs) - -mv Cargo.toml.backup Cargo.toml -popd -echo "Bundled ${app_path}" - -if [[ -n "${MACOS_CERTIFICATE:-}" && -n "${MACOS_CERTIFICATE_PASSWORD:-}" && -n "${APPLE_NOTARIZATION_KEY:-}" && -n "${APPLE_NOTARIZATION_KEY_ID:-}" && -n "${APPLE_NOTARIZATION_ISSUER_ID:-}" ]]; then - can_code_sign=true - - echo "Setting up keychain for code signing..." - security create-keychain -p "$MACOS_CERTIFICATE_PASSWORD" zed.keychain || echo "" - security default-keychain -s zed.keychain - security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" zed.keychain - # Calling set-keychain-settings without `-t` disables the auto-lock timeout - security set-keychain-settings zed.keychain - echo "$MACOS_CERTIFICATE" | base64 --decode > /tmp/zed-certificate.p12 - security import /tmp/zed-certificate.p12 -k zed.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign - rm /tmp/zed-certificate.p12 - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CERTIFICATE_PASSWORD" zed.keychain - - function cleanup() { - echo "Cleaning up keychain" - security default-keychain -s login.keychain - security delete-keychain zed.keychain - } - - trap cleanup EXIT -fi - -GIT_VERSION="v2.43.3" -GIT_VERSION_SHA="fa29823" - -function download_and_unpack() { - local url=$1 - local path_to_unpack=$2 - local target_path=$3 - - temp_dir=$(mktemp -d) - - if ! command -v curl &> /dev/null; then - echo "curl is not installed. Please install curl to continue." - exit 1 - fi - - curl --silent --fail --location "$url" | tar -xvz -C "$temp_dir" -f - $path_to_unpack - - mv "$temp_dir/$path_to_unpack" "$target_path" - - rm -rf "$temp_dir" -} - -function download_git() { - local architecture=$1 - local target_binary=$2 - - tmp_dir=$(mktemp -d) - pushd "$tmp_dir" - - case "$architecture" in - aarch64-apple-darwin) - download_and_unpack "https://github.com/desktop/dugite-native/releases/download/${GIT_VERSION}/dugite-native-${GIT_VERSION}-${GIT_VERSION_SHA}-macOS-arm64.tar.gz" bin/git ./git - ;; - x86_64-apple-darwin) - download_and_unpack "https://github.com/desktop/dugite-native/releases/download/${GIT_VERSION}/dugite-native-${GIT_VERSION}-${GIT_VERSION_SHA}-macOS-x64.tar.gz" bin/git ./git - ;; - *) - echo "Unsupported architecture: $architecture" - exit 1 - ;; - esac - - popd - - mv "${tmp_dir}/git" "${target_binary}" - rm -rf "$tmp_dir" -} - -function sign_app_binaries() { - rm -rf "${app_path}/Contents/Frameworks" - mkdir -p "${app_path}/Contents/Frameworks" - - echo "Downloading git binary" - download_git "${target_triple}" "${app_path}/Contents/MacOS/git" - - # Note: The app identifier for our development builds is the same as the app identifier for nightly. - cp crates/zed/contents/$channel/embedded.provisionprofile "${app_path}/Contents/" - - if [[ $can_code_sign = true ]]; then - echo "Code signing binaries" - # sequence of codesign commands modeled after this example: https://developer.apple.com/forums/thread/701514 - /usr/bin/codesign --deep --force --timestamp --options runtime --sign "$IDENTITY" "${app_path}/Contents/MacOS/cli" -v - /usr/bin/codesign --deep --force --timestamp --options runtime --sign "$IDENTITY" "${app_path}/Contents/MacOS/git" -v - /usr/bin/codesign --deep --force --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "$IDENTITY" "${app_path}/Contents/MacOS/zed" -v - /usr/bin/codesign --force --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "$IDENTITY" "${app_path}" -v - else - echo "One or more of the following variables are missing: MACOS_CERTIFICATE, MACOS_CERTIFICATE_PASSWORD, APPLE_NOTARIZATION_KEY, APPLE_NOTARIZATION_KEY_ID, APPLE_NOTARIZATION_ISSUER_ID" - - echo "====== WARNING ======" - echo "This bundle is being signed without all entitlements, some features (e.g. universal links) will not work" - echo "====== WARNING ======" - - # NOTE: if you need to test universal links you have a few paths forward: - # - create a PR and tag it with the `run-bundling` label, and download the .dmg file from there. - # - get a signing key for the MQ55VZLNZQ team from Nathan. - # - create your own signing key, and update references to MQ55VZLNZQ to your own team ID - # then comment out this line. - cat crates/zed/resources/zed.entitlements | sed '/com.apple.developer.associated-domains/,+1d' > "${app_path}/Contents/Resources/zed.entitlements" - - codesign --force --deep --entitlements "${app_path}/Contents/Resources/zed.entitlements" --sign ${MACOS_SIGNING_KEY:- -} "${app_path}" -v - fi - - bundle_name=$(basename "$app_path") - - if [ "$local_install" = true ]; then - rm -rf "/Applications/$bundle_name" - mv "$app_path" "/Applications/$bundle_name" - echo "Installed application bundle: /Applications/$bundle_name" - if [ "$open_result" = true ]; then - echo "Opening /Applications/$bundle_name" - open "/Applications/$bundle_name" - fi - elif [ "$open_result" = true ]; then - open "$app_path" - fi - - if [[ "$target_dir" = "debug" ]]; then - echo "Debug build detected - skipping DMG creation and signing" - if [ "$local_install" = false ]; then - echo "Created application bundle:" - echo "$app_path" - fi - else - dmg_target_directory="target/${target_triple}/${target_dir}" - dmg_source_directory="${dmg_target_directory}/dmg" - dmg_file_path="${dmg_target_directory}/Zed-${arch_suffix}.dmg" - xcode_bin_dir_path="$(xcode-select -p)/usr/bin" - - rm -rf ${dmg_source_directory} - mkdir -p ${dmg_source_directory} - mv "${app_path}" "${dmg_source_directory}" - notarization_key_file=$(mktemp) - - echo "Adding symlink to /Applications to ${dmg_source_directory}" - ln -s /Applications ${dmg_source_directory} - - echo "Creating final DMG at ${dmg_file_path} using ${dmg_source_directory}" - hdiutil create -volname Zed -srcfolder "${dmg_source_directory}" -ov -format UDZO "${dmg_file_path}" - - # If someone runs this bundle script locally, a symlink will be placed in `dmg_source_directory`. - # This symlink causes CPU issues with Zed if the Zed codebase is the project being worked on, so we simply remove it for now. - echo "Removing symlink to /Applications from ${dmg_source_directory}" - rm ${dmg_source_directory}/Applications - - echo "Adding license agreement to DMG" - npm install --global dmg-license minimist - dmg-license script/terms/terms.json "${dmg_file_path}" - - if [[ $can_code_sign = true ]]; then - echo "Notarizing DMG with Apple" - /usr/bin/codesign --deep --force --timestamp --options runtime --sign "$IDENTITY" "$(pwd)/${dmg_file_path}" -v - echo "$APPLE_NOTARIZATION_KEY" > "$notarization_key_file" - "${xcode_bin_dir_path}/notarytool" submit --wait --key "$notarization_key_file" --key-id "$APPLE_NOTARIZATION_KEY_ID" --issuer "$APPLE_NOTARIZATION_ISSUER_ID" "${dmg_file_path}" - rm "$notarization_key_file" - "${xcode_bin_dir_path}/stapler" staple "${dmg_file_path}" - fi - - if [ "$open_result" = true ]; then - open $dmg_target_directory - fi - fi -} - -function sign_binary() { - local binary_path=$1 - - if [[ $can_code_sign = true ]]; then - echo "Code signing executable $binary_path" - /usr/bin/codesign --deep --force --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "$IDENTITY" "${binary_path}" -v - fi -} - -function upload_debug_symbols() { - if [ "$local_install" = true ]; then - echo "local install; skipping sentry upload." - elif [[ -n "${SENTRY_AUTH_TOKEN:-}" ]]; then - echo "Uploading zed debug symbols to sentry..." - exe_path="target/${target_triple}/release/Zed" - if ! dsymutil --flat "target/${target_triple}/${target_dir}/zed" 2> target/dsymutil.log; then - echo "dsymutil failed" - cat target/dsymutil.log - exit 1 - fi - if ! dsymutil --flat "target/${target_triple}/${target_dir}/remote_server" 2> target/dsymutil.log; then - echo "dsymutil failed" - cat target/dsymutil.log - exit 1 - fi - # note: this uploads the unstripped binary which is needed because it contains - # .eh_frame data for stack unwinding. see https://github.com/getsentry/symbolic/issues/783 - sentry-cli debug-files upload --include-sources --wait -p zed -o zed-dev \ - # Try uploading up to 3 times - for attempt in 1 2 3; do - echo "Sentry upload attempt $attempt..." - if sentry-cli debug-files upload --include-sources --wait -p zed -o zed-dev \ - "target/${target_triple}/${target_dir}/zed.dwarf" \ - "target/${target_triple}/${target_dir}/remote_server.dwarf"; then - break - else - echo "Sentry upload failed on attempt $attempt" - if [ $attempt -eq 3 ]; then - echo "All sentry upload attempts failed" - exit 1 - fi - fi - done - else - echo "missing SENTRY_AUTH_TOKEN. skipping sentry upload." - fi -} - -upload_debug_symbols - -cp target/${target_triple}/${target_dir}/zed "${app_path}/Contents/MacOS/zed" -cp target/${target_triple}/${target_dir}/cli "${app_path}/Contents/MacOS/cli" -sign_app_binaries - -sign_binary "target/$target_triple/release/remote_server" -gzip -f --stdout --best target/$target_triple/release/remote_server > target/zed-remote-server-macos-$arch_suffix.gz diff --git a/script/bundle-windows.ps1 b/script/bundle-windows.ps1 deleted file mode 100644 index 48114a970f..0000000000 --- a/script/bundle-windows.ps1 +++ /dev/null @@ -1,378 +0,0 @@ -[CmdletBinding()] -Param( - [Parameter()][Alias('i')][switch]$Install, - [Parameter()][Alias('h')][switch]$Help, - [Parameter()][Alias('a')][string]$Architecture, - [Parameter()][string]$Name -) - -. "$PSScriptRoot/lib/workspace.ps1" - -# https://stackoverflow.com/questions/57949031/powershell-script-stops-if-program-fails-like-bash-set-o-errexit -$ErrorActionPreference = 'Stop' -$PSNativeCommandUseErrorActionPreference = $true - -$buildSuccess = $false - -$OSArchitecture = switch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) { - "X64" { "x86_64" } - "Arm64" { "aarch64" } - default { throw "Unsupported architecture" } -} - -$Architecture = if ($Architecture) { - $Architecture -} else { - $OSArchitecture -} - -$CargoOutDir = "./target/$Architecture-pc-windows-msvc/release" - -function Get-VSArch { - param( - [string]$Arch - ) - - switch ($Arch) { - "x86_64" { "amd64" } - "aarch64" { "arm64" } - } -} - -Push-Location -& "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\Launch-VsDevShell.ps1" -Arch (Get-VSArch -Arch $Architecture) -HostArch (Get-VSArch -Arch $OSArchitecture) -Pop-Location - -$target = "$Architecture-pc-windows-msvc" - -if ($Help) { - Write-Output "Usage: test.ps1 [-Install] [-Help]" - Write-Output "Build the installer for Windows.\n" - Write-Output "Options:" - Write-Output " -Architecture, -a Which architecture to build (x86_64 or aarch64)" - Write-Output " -Install, -i Run the installer after building." - Write-Output " -Help, -h Show this help message." - exit 0 -} - -Push-Location -Path crates/zed -$channel = Get-Content "RELEASE_CHANNEL" -$env:ZED_RELEASE_CHANNEL = $channel -$env:RELEASE_CHANNEL = $channel -Pop-Location - -function CheckEnvironmentVariables { - if(-not $env:CI) { - return - } - - $requiredVars = @( - 'ZED_WORKSPACE', 'RELEASE_VERSION', 'ZED_RELEASE_CHANNEL', - 'AZURE_TENANT_ID', 'AZURE_CLIENT_ID', 'AZURE_CLIENT_SECRET', - 'ACCOUNT_NAME', 'CERT_PROFILE_NAME', 'ENDPOINT', - 'FILE_DIGEST', 'TIMESTAMP_DIGEST', 'TIMESTAMP_SERVER' - ) - - foreach ($var in $requiredVars) { - if (-not (Test-Path "env:$var")) { - Write-Error "$var is not set" - exit 1 - } - } -} - -function PrepareForBundle { - if (Test-Path "$innoDir") { - Remove-Item -Path "$innoDir" -Recurse -Force - } - New-Item -Path "$innoDir" -ItemType Directory -Force - Copy-Item -Path "$env:ZED_WORKSPACE\crates\zed\resources\windows\*" -Destination "$innoDir" -Recurse -Force - New-Item -Path "$innoDir\make_appx" -ItemType Directory -Force - New-Item -Path "$innoDir\appx" -ItemType Directory -Force - New-Item -Path "$innoDir\bin" -ItemType Directory -Force - New-Item -Path "$innoDir\tools" -ItemType Directory -Force - - rustup target add $target -} - -function GenerateLicenses { - $oldErrorActionPreference = $ErrorActionPreference - $ErrorActionPreference = 'Continue' - . $PSScriptRoot/generate-licenses.ps1 - $ErrorActionPreference = $oldErrorActionPreference -} - -function BuildZedAndItsFriends { - Write-Output "Building Zed and its friends, for channel: $channel" - # Build zed.exe, cli.exe and auto_update_helper.exe - cargo build --release --package zed --package cli --package auto_update_helper --target $target - Copy-Item -Path ".\$CargoOutDir\zed.exe" -Destination "$innoDir\Zed.exe" -Force - Copy-Item -Path ".\$CargoOutDir\cli.exe" -Destination "$innoDir\cli.exe" -Force - Copy-Item -Path ".\$CargoOutDir\auto_update_helper.exe" -Destination "$innoDir\auto_update_helper.exe" -Force - # Build explorer_command_injector.dll - switch ($channel) { - "stable" { - cargo build --release --features stable --no-default-features --package explorer_command_injector --target $target - } - "preview" { - cargo build --release --features preview --no-default-features --package explorer_command_injector --target $target - } - default { - cargo build --release --package explorer_command_injector --target $target - } - } - Copy-Item -Path ".\$CargoOutDir\explorer_command_injector.dll" -Destination "$innoDir\zed_explorer_command_injector.dll" -Force -} - -function ZipZedAndItsFriendsDebug { - $items = @( - ".\$CargoOutDir\zed.pdb", - ".\$CargoOutDir\cli.pdb", - ".\$CargoOutDir\auto_update_helper.pdb", - ".\$CargoOutDir\explorer_command_injector.pdb" - ) - - Compress-Archive -Path $items -DestinationPath ".\$CargoOutDir\zed-$env:RELEASE_VERSION-$env:ZED_RELEASE_CHANNEL.dbg.zip" -Force -} - - -function UploadToSentry { - if (-not (Get-Command "sentry-cli" -ErrorAction SilentlyContinue)) { - Write-Output "sentry-cli not found. skipping sentry upload." - Write-Output "install with: 'winget install -e --id=Sentry.sentry-cli'" - return - } - if (-not (Test-Path "env:SENTRY_AUTH_TOKEN")) { - Write-Output "missing SENTRY_AUTH_TOKEN. skipping sentry upload." - return - } - Write-Output "Uploading zed debug symbols to sentry..." - for ($i = 1; $i -le 3; $i++) { - try { - sentry-cli debug-files upload --include-sources --wait -p zed -o zed-dev $CargoOutDir - break - } - catch { - Write-Output "Sentry upload attempt $i failed: $_" - if ($i -eq 3) { - Write-Output "All sentry upload attempts failed" - throw - } - Start-Sleep -Seconds 2 - } - } -} - -function MakeAppx { - switch ($channel) { - "stable" { - $manifestFile = "$env:ZED_WORKSPACE\crates\explorer_command_injector\AppxManifest.xml" - } - "preview" { - $manifestFile = "$env:ZED_WORKSPACE\crates\explorer_command_injector\AppxManifest-Preview.xml" - } - default { - $manifestFile = "$env:ZED_WORKSPACE\crates\explorer_command_injector\AppxManifest-Nightly.xml" - } - } - Copy-Item -Path "$manifestFile" -Destination "$innoDir\make_appx\AppxManifest.xml" - # Add makeAppx.exe to Path - $sdk = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64" - $env:Path += ';' + $sdk - makeAppx.exe pack /d "$innoDir\make_appx" /p "$innoDir\zed_explorer_command_injector.appx" /nv -} - -function SignZedAndItsFriends { - if (-not $env:CI) { - return - } - - $files = "$innoDir\Zed.exe,$innoDir\cli.exe,$innoDir\auto_update_helper.exe,$innoDir\zed_explorer_command_injector.dll,$innoDir\zed_explorer_command_injector.appx" - & "$innoDir\sign.ps1" $files -} - -function DownloadAMDGpuServices { - # If you update the AGS SDK version, please also update the version in `crates/gpui/src/platform/windows/directx_renderer.rs` - $url = "https://codeload.github.com/GPUOpen-LibrariesAndSDKs/AGS_SDK/zip/refs/tags/v6.3.0" - $zipPath = ".\AGS_SDK_v6.3.0.zip" - # Download the AGS SDK zip file - Invoke-WebRequest -Uri $url -OutFile $zipPath - # Extract the AGS SDK zip file - Expand-Archive -Path $zipPath -DestinationPath "." -Force -} - -function DownloadConpty { - $url = "https://github.com/microsoft/terminal/releases/download/v1.23.12811.0/Microsoft.Windows.Console.ConPTY.1.23.251008001.nupkg" - $zipPath = ".\Microsoft.Windows.Console.ConPTY.1.23.251008001.nupkg" - Invoke-WebRequest -Uri $url -OutFile $zipPath - Expand-Archive -Path $zipPath -DestinationPath ".\conpty" -Force -} - -function CollectFiles { - Move-Item -Path "$innoDir\zed_explorer_command_injector.appx" -Destination "$innoDir\appx\zed_explorer_command_injector.appx" -Force - Move-Item -Path "$innoDir\zed_explorer_command_injector.dll" -Destination "$innoDir\appx\zed_explorer_command_injector.dll" -Force - Move-Item -Path "$innoDir\cli.exe" -Destination "$innoDir\bin\zed.exe" -Force - Move-Item -Path "$innoDir\zed.sh" -Destination "$innoDir\bin\zed" -Force - Move-Item -Path "$innoDir\auto_update_helper.exe" -Destination "$innoDir\tools\auto_update_helper.exe" -Force - if($Architecture -eq "aarch64") { - New-Item -Type Directory -Path "$innoDir\arm64" -Force - Move-Item -Path ".\conpty\build\native\runtimes\arm64\OpenConsole.exe" -Destination "$innoDir\arm64\OpenConsole.exe" -Force - Move-Item -Path ".\conpty\runtimes\win-arm64\native\conpty.dll" -Destination "$innoDir\conpty.dll" -Force - } - else { - New-Item -Type Directory -Path "$innoDir\x64" -Force - New-Item -Type Directory -Path "$innoDir\arm64" -Force - Move-Item -Path ".\AGS_SDK-6.3.0\ags_lib\lib\amd_ags_x64.dll" -Destination "$innoDir\amd_ags_x64.dll" -Force - Move-Item -Path ".\conpty\build\native\runtimes\x64\OpenConsole.exe" -Destination "$innoDir\x64\OpenConsole.exe" -Force - Move-Item -Path ".\conpty\build\native\runtimes\arm64\OpenConsole.exe" -Destination "$innoDir\arm64\OpenConsole.exe" -Force - Move-Item -Path ".\conpty\runtimes\win-x64\native\conpty.dll" -Destination "$innoDir\conpty.dll" -Force - } -} - -function BuildInstaller { - $issFilePath = "$innoDir\zed.iss" - switch ($channel) { - "stable" { - $appId = "{{2DB0DA96-CA55-49BB-AF4F-64AF36A86712}" - $appIconName = "app-icon" - $appName = "Zed" - $appDisplayName = "Zed" - $appSetupName = "Zed-$Architecture" - # The mutex name here should match the mutex name in crates\zed\src\zed\windows_only_instance.rs - $appMutex = "Zed-Stable-Instance-Mutex" - $appExeName = "Zed" - $regValueName = "Zed" - $appUserId = "ZedIndustries.Zed" - $appShellNameShort = "Z&ed" - $appAppxFullName = "ZedIndustries.Zed_1.0.0.0_neutral__japxn1gcva8rg" - } - "preview" { - $appId = "{{F70E4811-D0E2-4D88-AC99-D63752799F95}" - $appIconName = "app-icon-preview" - $appName = "Zed Preview" - $appDisplayName = "Zed Preview" - $appSetupName = "Zed-$Architecture" - # The mutex name here should match the mutex name in crates\zed\src\zed\windows_only_instance.rs - $appMutex = "Zed-Preview-Instance-Mutex" - $appExeName = "Zed" - $regValueName = "ZedPreview" - $appUserId = "ZedIndustries.Zed.Preview" - $appShellNameShort = "Z&ed Preview" - $appAppxFullName = "ZedIndustries.Zed.Preview_1.0.0.0_neutral__japxn1gcva8rg" - } - "nightly" { - $appId = "{{1BDB21D3-14E7-433C-843C-9C97382B2FE0}" - $appIconName = "app-icon-nightly" - $appName = "Zed Nightly" - $appDisplayName = "Zed Nightly" - $appSetupName = "Zed-$Architecture" - # The mutex name here should match the mutex name in crates\zed\src\zed\windows_only_instance.rs - $appMutex = "Zed-Nightly-Instance-Mutex" - $appExeName = "Zed" - $regValueName = "ZedNightly" - $appUserId = "ZedIndustries.Zed.Nightly" - $appShellNameShort = "Z&ed Editor Nightly" - $appAppxFullName = "ZedIndustries.Zed.Nightly_1.0.0.0_neutral__japxn1gcva8rg" - } - "dev" { - $appId = "{{8357632E-24A4-4F32-BA97-E575B4D1FE5D}" - $appIconName = "app-icon-dev" - $appName = "Zed Dev" - $appDisplayName = "Zed Dev" - $appSetupName = "Zed-$Architecture" - # The mutex name here should match the mutex name in crates\zed\src\zed\windows_only_instance.rs - $appMutex = "Zed-Dev-Instance-Mutex" - $appExeName = "Zed" - $regValueName = "ZedDev" - $appUserId = "ZedIndustries.Zed.Dev" - $appShellNameShort = "Z&ed Dev" - $appAppxFullName = "ZedIndustries.Zed.Dev_1.0.0.0_neutral__japxn1gcva8rg" - } - default { - Write-Error "can't bundle installer for $channel." - exit 1 - } - } - - # Windows runner 2022 default has iscc in PATH, https://github.com/actions/runner-images/blob/main/images/windows/Windows2022-Readme.md - # Currently, we are using Windows 2022 runner. - # Windows runner 2025 doesn't have iscc in PATH for now, https://github.com/actions/runner-images/issues/11228 - $innoSetupPath = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" - - $definitions = @{ - "AppId" = $appId - "AppIconName" = $appIconName - "OutputDir" = "$env:ZED_WORKSPACE\target" - "AppSetupName" = $appSetupName - "AppName" = $appName - "AppDisplayName" = $appDisplayName - "RegValueName" = $regValueName - "AppMutex" = $appMutex - "AppExeName" = $appExeName - "ResourcesDir" = "$innoDir" - "ShellNameShort" = $appShellNameShort - "AppUserId" = $appUserId - "Version" = "$env:RELEASE_VERSION" - "SourceDir" = "$env:ZED_WORKSPACE" - "AppxFullName" = $appAppxFullName - } - - $defs = @() - foreach ($key in $definitions.Keys) { - $defs += "/d$key=`"$($definitions[$key])`"" - } - - $innoArgs = @($issFilePath) + $defs - if($env:CI) { - $signTool = "powershell.exe -ExecutionPolicy Bypass -File $innoDir\sign.ps1 `$f" - $innoArgs += "/sDefaultsign=`"$signTool`"" - } - - # Execute Inno Setup - Write-Host "🚀 Running Inno Setup: $innoSetupPath $innoArgs" - $process = Start-Process -FilePath $innoSetupPath -ArgumentList $innoArgs -NoNewWindow -Wait -PassThru - - if ($process.ExitCode -eq 0) { - Write-Host "✅ Inno Setup successfully compiled the installer" - Write-Output "SETUP_PATH=target/$appSetupName.exe" >> $env:GITHUB_ENV - $script:buildSuccess = $true - } - else { - Write-Host "❌ Inno Setup failed: $($process.ExitCode)" - $script:buildSuccess = $false - } -} - -ParseZedWorkspace -$innoDir = "$env:ZED_WORKSPACE\inno\$Architecture" -$debugArchive = "$CargoOutDir\zed-$env:RELEASE_VERSION-$env:ZED_RELEASE_CHANNEL.dbg.zip" -$debugStoreKey = "$env:ZED_RELEASE_CHANNEL/zed-$env:RELEASE_VERSION-$env:ZED_RELEASE_CHANNEL.dbg.zip" - -CheckEnvironmentVariables -PrepareForBundle -GenerateLicenses -BuildZedAndItsFriends -MakeAppx -SignZedAndItsFriends -ZipZedAndItsFriendsDebug -DownloadAMDGpuServices -DownloadConpty -CollectFiles -BuildInstaller - -if($env:CI) { - UploadToSentry -} - -if ($buildSuccess) { - Write-Output "Build successful" - if ($Install) { - Write-Output "Installing Zed..." - Start-Process -FilePath "$env:ZED_WORKSPACE/target/ZedEditorUserSetup-x64-$env:RELEASE_VERSION.exe" - } - exit 0 -} -else { - Write-Output "Build failed" - exit 1 -} diff --git a/script/check-keymaps b/script/check-keymaps deleted file mode 100755 index 44745fa8b8..0000000000 --- a/script/check-keymaps +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -pattern='cmd-' -result=$(git grep --no-color --line-number --fixed-strings -e "$pattern" -- \ - 'assets/keymaps/' \ - ':(exclude)assets/keymaps/storybook.json' \ - ':(exclude)assets/keymaps/default-macos.json' \ - ':(exclude)assets/keymaps/macos/*.json' || true) - -if [[ -n "${result}" ]]; then - echo "${result}" - echo "Error: Found 'cmd-' in non-macOS keymap files." - exit 1 -fi - -pattern='super-|win-|fn-' -result=$(git grep --no-color --line-number --fixed-strings -e "$pattern" -- \ - 'assets/keymaps/' || true) - -if [[ -n "${result}" ]]; then - echo "${result}" - echo "Error: Found 'super-', 'win-', or 'fn-' in keymap files. Currently these aren't used." - exit 1 -fi diff --git a/script/check-licenses b/script/check-licenses deleted file mode 100755 index 0363f31970..0000000000 --- a/script/check-licenses +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -AGPL_CRATES=("collab") -RELEASE_CRATES=("cli" "remote_server" "zed") - -check_license () { - local dir="$1" - local allowed_licenses=() - - local is_agpl=false - for agpl_crate in "${AGPL_CRATES[@]}"; do - if [[ "$dir" == "crates/$agpl_crate" ]]; then - is_agpl=true - break - fi - done - - if [[ "$is_agpl" == true ]]; then - allowed_licenses=("LICENSE-AGPL") - else - allowed_licenses=("LICENSE-GPL" "LICENSE-APACHE") - fi - - for license in "${allowed_licenses[@]}"; do - if [[ -L "$dir/$license" ]]; then - return 0 - elif [[ -e "$dir/$license" ]]; then - echo "Error: $dir/$license exists but is not a symlink." - exit 1 - fi - done - - if [[ "$is_agpl" == true ]]; then - echo "Error: $dir does not contain a LICENSE-AGPL symlink" - else - echo "Error: $dir does not contain a LICENSE-GPL or LICENSE-APACHE symlink" - fi - exit 1 -} - -git ls-files "**/*/Cargo.toml" | while read -r cargo_toml; do - check_license "$(dirname "$cargo_toml")" -done - - -# Make sure the AGPL server crates are included in the release tarball. -for release_crate in "${RELEASE_CRATES[@]}"; do - tree_output=$(cargo tree --package "$release_crate") - for agpl_crate in "${AGPL_CRATES[@]}"; do - # Look for lines that contain the crate name followed by " v" (version) - # This matches patterns like "├── collab v0.44.0" - if echo "$tree_output" | grep -E "(^|[^a-zA-Z_])${agpl_crate} v" > /dev/null; then - echo "Error: crate '${agpl_crate}' is AGPL and is a dependency of crate '${release_crate}'." >&2 - echo "AGPL licensed code should not be used in the release distribution, only in servers." >&2 - exit 1 - fi - done -done - -echo "check-licenses succeeded" diff --git a/script/check-links b/script/check-links deleted file mode 100755 index 259ce1d1b9..0000000000 --- a/script/check-links +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -usage() { - echo "Usage: $0 [local|all] [--help]" - echo " local Only check local links (default)" - echo " all Check all links including remote ones" - exit 1 -} - -check_mode="local" -if [ $# -eq 1 ]; then - case "$1" in - "local") check_mode="local" ;; - "all") check_mode="all" ;; - "--help") usage ;; - *) echo "Invalid argument: $1" && usage ;; - esac -else - usage -fi - -cargo install lychee -cd "$(dirname "$0")/.." - -if [ "$check_mode" = "all" ]; then - lychee --no-progress './docs/src/**/*' -else - lychee --exclude '^http' './docs/src/**/*' -fi -# diff --git a/script/check-todos b/script/check-todos deleted file mode 100755 index 4bdf328329..0000000000 --- a/script/check-todos +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# Brackets are used around characters so these don't show up in normal search. -pattern='tod[o]!|FIXM[E]' -result=$(git grep --no-color --ignore-case --line-number --extended-regexp -e $pattern -- \ - ':(exclude).github/workflows/ci.yml' \ - ':(exclude)*criteria.md' \ - ':(exclude)*prompt.md' || true) -if [[ -n "${result}" ]]; then - echo "${result}" - exit 1 -fi diff --git a/script/cherry-pick b/script/cherry-pick deleted file mode 100755 index 37106943f4..0000000000 --- a/script/cherry-pick +++ /dev/null @@ -1,33 +0,0 @@ -# #!/bin/bash -set -euxo pipefail - -if [ "$#" -ne 3 ]; then - echo "Usage: $0 " - exit 1 -fi - -BRANCH_NAME="$1" -COMMIT_SHA="$2" -CHANNEL="$3" - -SHORT_SHA="${COMMIT_SHA:0:8}" -NEW_BRANCH="cherry-pick-${BRANCH_NAME}-${SHORT_SHA}" -git fetch --depth 2 origin +${COMMIT_SHA} ${BRANCH_NAME} -git checkout --force "origin/$BRANCH_NAME" -B "$NEW_BRANCH" - -git cherry-pick "$COMMIT_SHA" - -git push origin -f "$NEW_BRANCH" -COMMIT_TITLE=$(git log -1 --pretty=format:"%s" "$COMMIT_SHA") -COMMIT_BODY=$(git log -1 --pretty=format:"%b" "$COMMIT_SHA") - -# Check if commit title ends with (#number) -if [[ "$COMMIT_TITLE" =~ \(#([0-9]+)\)$ ]]; then - PR_NUMBER="${BASH_REMATCH[1]}" - PR_BODY="Cherry-pick of #${PR_NUMBER} to ${CHANNEL}"$'\n'$'\n'"----"$'\n'"${COMMIT_BODY}" -else - PR_BODY="Cherry-pick of ${COMMIT_SHA} to ${CHANNEL}"$'\n'$'\n'"----"$'\n'"${COMMIT_BODY}" -fi - -# Create a pull request -gh pr create --base "$BRANCH_NAME" --head "$NEW_BRANCH" --title "$COMMIT_TITLE (cherry-pick to $CHANNEL)" --body "$PR_BODY" diff --git a/script/clear-target-dir-if-larger-than b/script/clear-target-dir-if-larger-than deleted file mode 100755 index 46256159a8..0000000000 --- a/script/clear-target-dir-if-larger-than +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -set -euxo pipefail - -if [[ $# -ne 1 ]]; then - echo "usage: $0 " - exit 1 -fi - -if ! [[ -d target ]]; then - echo "target directory does not exist yet" - exit 0 -fi - -max_size_gb=$1 - -current_size=$(du -s target | cut -f1) -current_size_gb=$(expr ${current_size} / 1024 / 1024) - -echo "target directory size: ${current_size_gb}gb. max size: ${max_size_gb}gb" - -if [[ ${current_size_gb} -gt ${max_size_gb} ]]; then - echo "clearing target directory" - shopt -s dotglob - rm -rf target/* -fi diff --git a/script/clear-target-dir-if-larger-than.ps1 b/script/clear-target-dir-if-larger-than.ps1 deleted file mode 100644 index c18c308624..0000000000 --- a/script/clear-target-dir-if-larger-than.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -param ( - [Parameter(Mandatory = $true)] - [int]$MAX_SIZE_IN_GB -) - -$ErrorActionPreference = "Stop" -$PSNativeCommandUseErrorActionPreference = $true -$ProgressPreference = "SilentlyContinue" - -if (-Not (Test-Path -Path "target")) { - Write-Host "target directory does not exist yet" - exit 0 -} - -$current_size_gb = (Get-ChildItem -Recurse -Force -File -Path "target" | Measure-Object -Property Length -Sum).Sum / 1GB - -Write-Host "target directory size: ${current_size_gb}GB. max size: ${MAX_SIZE_IN_GB}GB" - -if ($current_size_gb -gt $MAX_SIZE_IN_GB) { - Write-Host "clearing target directory" - Remove-Item -Recurse -Force -Path "target\*" -ErrorAction SilentlyContinue -} diff --git a/script/collab-flamegraph b/script/collab-flamegraph deleted file mode 100755 index 058c3a9f44..0000000000 --- a/script/collab-flamegraph +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash - -# Notes for fixing this script if it's broken: -# - if you see an error about "can't find perf_6.1" you need to install `linux-perf` from the -# version of Debian that matches the host (e.g. apt-get -t bookworm-backports install linux-perf) -# - if you see an error about `addr2line` you may need to install binutils - -set -euo pipefail - -source script/lib/deploy-helpers.sh - -if [[ $# != 1 ]]; then - echo "Usage: $0 " - exit 1 -fi -environment=$1 - -target_zed_kube_cluster - -# 5s in production is ~200Mb..., in staging you probably want to bump this up. -echo "Running perf on collab, collecting 5s of data..." - -kubectl -n $environment exec -it deployments/collab -- perf record -p 1 -g -m 64 --call-graph dwarf -- sleep 5 - -run="collab-$environment-$(date -Iseconds)" -echo "Processing data and downloading to '$run.perf'..." - -kubectl -n $environment exec -it deployments/collab -- perf --no-pager script > "$run.perf" - -which inferno-flamegraph 2>/dev/null || (echo "installing inferno..."; cargo install inferno) - -inferno-collapse-perf "$run.perf" | inferno-flamegraph > "$run.svg" -open "./$run.svg" diff --git a/script/crate-dep-graph b/script/crate-dep-graph deleted file mode 100755 index 54170a9986..0000000000 --- a/script/crate-dep-graph +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -set -e - -if [[ -x cargo-depgraph ]]; then - cargo install cargo-depgraph -fi - -graph_file=target/crate-graph.html - -cargo depgraph \ - --workspace-only \ - --offline \ - --root=zed,cli,collab \ - --dedup-transitive-deps \ - | dot -Tsvg > $graph_file - -echo "open $graph_file" -open $graph_file diff --git a/script/create-draft-release b/script/create-draft-release deleted file mode 100755 index d50ebf2e5a..0000000000 --- a/script/create-draft-release +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -preview="" -if [[ "$GITHUB_REF_NAME" == *"-pre" ]]; then - preview="-p" -fi - -gh release view "$GITHUB_REF_NAME" ||\ - gh release create -t "$GITHUB_REF_NAME" -d "$GITHUB_REF_NAME" -F "$1" $preview diff --git a/script/danger/.gitignore b/script/danger/.gitignore deleted file mode 100644 index c2658d7d1b..0000000000 --- a/script/danger/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules/ diff --git a/script/danger/dangerfile.ts b/script/danger/dangerfile.ts deleted file mode 100644 index 88dc5c5e71..0000000000 --- a/script/danger/dangerfile.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { danger, message, warn, fail } from "danger"; -const { prHygiene } = require("danger-plugin-pr-hygiene"); - -prHygiene({ - prefixPattern: /^([a-z\d\(\)_\s]+):(.*)/g, - rules: { - // Don't enable this rule just yet, as it can have false positives. - useImperativeMood: "off", - }, -}); - -const RELEASE_NOTES_PATTERN = /Release Notes:\r?\n\s+-/gm; -const body = danger.github.pr.body; - -const hasReleaseNotes = RELEASE_NOTES_PATTERN.test(body); - -if (!hasReleaseNotes) { - warn( - [ - "This PR is missing release notes.", - "", - 'Please add a "Release Notes" section that describes the change:', - "", - "```", - "Release Notes:", - "", - "- Added/Fixed/Improved ...", - "```", - "", - 'If your change is not user-facing, you can use "N/A" for the entry:', - "```", - "Release Notes:", - "", - "- N/A", - "```", - ].join("\n"), - ); -} - -const ISSUE_LINK_PATTERN = - /(?:- )?(? match.replace(/^#/, "").replace(/https:\/\/github\.com\/zed-industries\/zed\/issues\//, "")) - .filter((issue, index, self) => self.indexOf(issue) === index); - - const issuesToReport = issues.map((issue) => `#${issue}`).join(", "); - message( - [ - `This PR includes links to the following GitHub Issues: ${issuesToReport}`, - "If this PR aims to close an issue, please include a `Closes #ISSUE` line at the top of the PR body.", - ].join("\n"), - ); -} - -const PROMPT_PATHS = [ - "assets/prompts/content_prompt.hbs", - "assets/prompts/terminal_assistant_prompt.hbs", - "crates/agent_settings/src/prompts/summarize_thread_detailed_prompt.txt", - "crates/agent_settings/src/prompts/summarize_thread_prompt.txt", - "crates/agent/src/templates/create_file_prompt.hbs", - "crates/agent/src/templates/edit_file_prompt_xml.hbs", - "crates/agent/src/templates/edit_file_prompt_diff_fenced.hbs", - "crates/git_ui/src/commit_message_prompt.txt", -]; - -const PROMPT_CHANGE_ATTESTATION = "I have ensured the LLM Worker works with these prompt changes."; - -const modifiedPrompts = danger.git.modified_files.filter((file) => - PROMPT_PATHS.some((promptPath) => file.includes(promptPath)), -); - -for (const promptPath of modifiedPrompts) { - if (body.includes(PROMPT_CHANGE_ATTESTATION)) { - message( - [ - `This PR contains changes to "${promptPath}".`, - "The author has attested the LLM Worker works with the changes to this prompt.", - ].join("\n"), - ); - } else { - fail( - [ - `Modifying the "${promptPath}" prompt may require corresponding changes in the LLM Worker.`, - "If you are ensure what this entails, talk to @maxdeviant or another AI team member.", - `Once you have made the changes—or determined that none are necessary—add "${PROMPT_CHANGE_ATTESTATION}" to the PR description.`, - ].join("\n"), - ); - } -} - -const FIXTURE_CHANGE_ATTESTATION = "Changes to test fixtures are intentional and necessary."; - -const FIXTURES_PATHS = ["crates/assistant_tools/src/edit_agent/evals/fixtures"]; - -const modifiedFixtures = danger.git.modified_files.filter((file) => - FIXTURES_PATHS.some((fixturePath) => file.includes(fixturePath)), -); - -if (modifiedFixtures.length > 0) { - if (!body.includes(FIXTURE_CHANGE_ATTESTATION)) { - const modifiedFixturesStr = modifiedFixtures.map((path) => "`" + path + "`").join(", "); - fail( - [ - `This PR modifies eval or test fixtures (${modifiedFixturesStr}), which are typically expected to remain unchanged.`, - "If these changes are intentional and required, please add the following attestation to your PR description: ", - `"${FIXTURE_CHANGE_ATTESTATION}"`, - ].join("\n\n"), - ); - } -} diff --git a/script/danger/package.json b/script/danger/package.json deleted file mode 100644 index eaa1035e89..0000000000 --- a/script/danger/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "danger", - "version": "1.0.0", - "private": true, - "main": "index.js", - "scripts": { - "danger": "danger" - }, - "devDependencies": { - "danger": "13.0.4", - "danger-plugin-pr-hygiene": "0.6.1" - } -} diff --git a/script/danger/pnpm-lock.yaml b/script/danger/pnpm-lock.yaml deleted file mode 100644 index fd6b3f66ac..0000000000 --- a/script/danger/pnpm-lock.yaml +++ /dev/null @@ -1,884 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - devDependencies: - danger: - specifier: 13.0.4 - version: 13.0.4 - danger-plugin-pr-hygiene: - specifier: 0.6.1 - version: 0.6.1 - -packages: - - '@gitbeaker/core@38.12.1': - resolution: {integrity: sha512-8XMVcBIdVAAoxn7JtqmZ2Ee8f+AZLcCPmqEmPFOXY2jPS84y/DERISg/+sbhhb18iRy+ZsZhpWgQ/r3CkYNJOQ==} - engines: {node: '>=18.0.0'} - - '@gitbeaker/requester-utils@38.12.1': - resolution: {integrity: sha512-Rc/DgngS0YPN+AY1s9UnexKSy4Lh0bkQVAq9p7PRbRpXb33SlTeCg8eg/8+A/mrMcHgYmP0XhH8lkizyA5tBUQ==} - engines: {node: '>=18.0.0'} - - '@gitbeaker/rest@38.12.1': - resolution: {integrity: sha512-9KMSDtJ/sIov+5pcH+CAfiJXSiuYgN0KLKQFg0HHWR2DwcjGYkcbmhoZcWsaOWOqq4kihN1l7wX91UoRxxKKTQ==} - engines: {node: '>=18.0.0'} - - '@octokit/auth-token@4.0.0': - resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} - engines: {node: '>= 18'} - - '@octokit/core@5.2.2': - resolution: {integrity: sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==} - engines: {node: '>= 18'} - - '@octokit/endpoint@9.0.6': - resolution: {integrity: sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==} - engines: {node: '>= 18'} - - '@octokit/graphql@7.1.1': - resolution: {integrity: sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==} - engines: {node: '>= 18'} - - '@octokit/openapi-types@24.2.0': - resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==} - - '@octokit/plugin-paginate-rest@11.4.4-cjs.2': - resolution: {integrity: sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==} - engines: {node: '>= 18'} - peerDependencies: - '@octokit/core': '5' - - '@octokit/plugin-request-log@4.0.1': - resolution: {integrity: sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==} - engines: {node: '>= 18'} - peerDependencies: - '@octokit/core': '5' - - '@octokit/plugin-rest-endpoint-methods@13.3.2-cjs.1': - resolution: {integrity: sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==} - engines: {node: '>= 18'} - peerDependencies: - '@octokit/core': ^5 - - '@octokit/request-error@5.1.1': - resolution: {integrity: sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==} - engines: {node: '>= 18'} - - '@octokit/request@8.4.1': - resolution: {integrity: sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==} - engines: {node: '>= 18'} - - '@octokit/rest@20.1.2': - resolution: {integrity: sha512-GmYiltypkHHtihFwPRxlaorG5R9VAHuk/vbszVoRTGXnAsY60wYLkh/E2XiFmdZmqrisw+9FaazS1i5SbdWYgA==} - engines: {node: '>= 18'} - - '@octokit/types@13.10.0': - resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} - - '@tootallnate/once@2.0.0': - resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} - engines: {node: '>= 10'} - - agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} - - ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - - async-retry@1.2.3: - resolution: {integrity: sha512-tfDb02Th6CE6pJUF2gjW5ZVjsgwlucVXOEQMvEX9JgSJMs9gAX+Nz3xRuJBKuUYjTSYORqvDBORdAQ3LU59g7Q==} - - before-after-hook@2.2.3: - resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - buffer-equal-constant-time@1.0.1: - resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - - chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - - colors@1.4.0: - resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} - engines: {node: '>=0.1.90'} - - commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - - core-js@3.45.1: - resolution: {integrity: sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==} - - danger-plugin-pr-hygiene@0.6.1: - resolution: {integrity: sha512-nb+iUQvirE3BlKXI1WoOND6sujyGzHar590mJm5tt4RLi65HXFaU5hqONxgDoWFujJNHYnXse9yaZdxnxEi4QA==} - - danger@13.0.4: - resolution: {integrity: sha512-IAdQ5nSJyIs4zKj6AN35ixt2B0Ce3WZUm3IFe/CMnL/Op7wV7IGg4D348U0EKNaNPP58QgXbdSk9pM+IXP1QXg==} - engines: {node: '>=18'} - hasBin: true - - debug@4.4.1: - resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deprecation@2.3.1: - resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - ecdsa-sig-formatter@1.0.11: - resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - - fast-json-patch@3.1.1: - resolution: {integrity: sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - - get-stdin@6.0.0: - resolution: {integrity: sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==} - engines: {node: '>=4'} - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - - has-flag@2.0.0: - resolution: {integrity: sha512-P+1n3MnwjR/Epg9BBo1KT8qbye2g2Ou4sFumihwt6I4tsUX7jnLcX4BTOSKg/B1ZrIYMN9FcEnG4x5a7NB8Eng==} - engines: {node: '>=0.10.0'} - - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - http-proxy-agent@5.0.0: - resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} - engines: {node: '>= 6'} - - https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} - - hyperlinker@1.0.0: - resolution: {integrity: sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ==} - engines: {node: '>=4'} - - ini@5.0.0: - resolution: {integrity: sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==} - engines: {node: ^18.17.0 || >=20.5.0} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - jsonpointer@5.0.1: - resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} - engines: {node: '>=0.10.0'} - - jsonwebtoken@9.0.2: - resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} - engines: {node: '>=12', npm: '>=6'} - - jwa@1.4.2: - resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} - - jws@3.2.2: - resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} - - lodash.find@4.6.0: - resolution: {integrity: sha512-yaRZoAV3Xq28F1iafWN1+a0rflOej93l1DQUejs3SZ41h2O9UJBoS9aueGjPDgAl4B6tPC0NuuchLKaDQQ3Isg==} - - lodash.includes@4.3.0: - resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} - - lodash.isboolean@3.0.3: - resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} - - lodash.isinteger@4.0.4: - resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} - - lodash.isnumber@3.0.3: - resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} - - lodash.isobject@3.0.2: - resolution: {integrity: sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA==} - - lodash.isplainobject@4.0.6: - resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} - - lodash.isstring@4.0.1: - resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} - - lodash.keys@4.2.0: - resolution: {integrity: sha512-J79MkJcp7Df5mizHiVNpjoHXLi4HLjh9VLS/M7lQSGoQ+0oQ+lWEigREkqKyizPB1IawvQLLKY8mzEcm1tkyxQ==} - - lodash.mapvalues@4.6.0: - resolution: {integrity: sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==} - - lodash.memoize@4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - - lodash.once@4.1.1: - resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - memfs-or-file-map-to-github-branch@1.3.0: - resolution: {integrity: sha512-AzgIEodmt51dgwB3TmihTf1Fh2SmszdZskC6trFHy4v71R5shLmdjJSYI7ocVfFa7C/TE6ncb0OZ9eBg2rmkBQ==} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - node-cleanup@2.1.2: - resolution: {integrity: sha512-qN8v/s2PAJwGUtr1/hYTpNKlD6Y9rc4p8KSmJXyGdYGZsDGKXrGThikLFP9OCHFeLeEpQzPwiAtdIvBLqm//Hw==} - - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - override-require@1.1.1: - resolution: {integrity: sha512-eoJ9YWxFcXbrn2U8FKT6RV+/Kj7fiGAB1VvHzbYKt8xM5ZuKZgCGvnHzDxmreEjcBH28ejg5MiOH4iyY1mQnkg==} - - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - - parse-diff@0.7.1: - resolution: {integrity: sha512-1j3l8IKcy4yRK2W4o9EYvJLSzpAVwz4DXqCewYyx2vEwk2gcf3DBPqc8Fj4XV3K33OYJ08A8fWwyu/ykD/HUSg==} - - parse-github-url@1.0.3: - resolution: {integrity: sha512-tfalY5/4SqGaV/GIGzWyHnFjlpTPTNpENR9Ea2lLldSJ8EWXMsvacWucqY3m3I4YPtas15IxTLQVQ5NSYXPrww==} - engines: {node: '>= 0.10'} - hasBin: true - - parse-link-header@2.0.0: - resolution: {integrity: sha512-xjU87V0VyHZybn2RrCX5TIFGxTVZE6zqqZWMPlIKiSKuWh/X5WZdt+w1Ki1nXB+8L/KtL+nZ4iq+sfI6MrhhMw==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - pinpoint@1.1.0: - resolution: {integrity: sha512-+04FTD9x7Cls2rihLlo57QDCcHoLBGn5Dk51SwtFBWkUWLxZaBXyNVpCw1S+atvE7GmnFjeaRZ0WLq3UYuqAdg==} - - prettyjson@1.2.5: - resolution: {integrity: sha512-rksPWtoZb2ZpT5OVgtmy0KHVM+Dca3iVwWY9ifwhcexfjebtgjg3wmrUt9PvJ59XIYBcknQeYHD8IAnVlh9lAw==} - hasBin: true - - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} - engines: {node: '>=0.6'} - - readline-sync@1.4.10: - resolution: {integrity: sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==} - engines: {node: '>= 0.8.0'} - - regenerator-runtime@0.13.11: - resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} - - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - - retry@0.12.0: - resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} - engines: {node: '>= 4'} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} - engines: {node: '>=10'} - hasBin: true - - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - - supports-hyperlinks@1.0.1: - resolution: {integrity: sha512-HHi5kVSefKaJkGYXbDuKbUGRVxqnWGn3J2e39CYcNJEfWciGq2zYtOhXLTlvrOZW1QU7VX67w7fMmWafHX9Pfw==} - engines: {node: '>=4'} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - - universal-user-agent@6.0.1: - resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} - - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - xcase@2.0.1: - resolution: {integrity: sha512-UmFXIPU+9Eg3E9m/728Bii0lAIuoc+6nbrNUKaRPJOFp91ih44qqGlWtxMB6kXFrRD6po+86ksHM5XHCfk6iPw==} - - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - -snapshots: - - '@gitbeaker/core@38.12.1': - dependencies: - '@gitbeaker/requester-utils': 38.12.1 - qs: 6.14.0 - xcase: 2.0.1 - - '@gitbeaker/requester-utils@38.12.1': - dependencies: - qs: 6.14.0 - xcase: 2.0.1 - - '@gitbeaker/rest@38.12.1': - dependencies: - '@gitbeaker/core': 38.12.1 - '@gitbeaker/requester-utils': 38.12.1 - - '@octokit/auth-token@4.0.0': {} - - '@octokit/core@5.2.2': - dependencies: - '@octokit/auth-token': 4.0.0 - '@octokit/graphql': 7.1.1 - '@octokit/request': 8.4.1 - '@octokit/request-error': 5.1.1 - '@octokit/types': 13.10.0 - before-after-hook: 2.2.3 - universal-user-agent: 6.0.1 - - '@octokit/endpoint@9.0.6': - dependencies: - '@octokit/types': 13.10.0 - universal-user-agent: 6.0.1 - - '@octokit/graphql@7.1.1': - dependencies: - '@octokit/request': 8.4.1 - '@octokit/types': 13.10.0 - universal-user-agent: 6.0.1 - - '@octokit/openapi-types@24.2.0': {} - - '@octokit/plugin-paginate-rest@11.4.4-cjs.2(@octokit/core@5.2.2)': - dependencies: - '@octokit/core': 5.2.2 - '@octokit/types': 13.10.0 - - '@octokit/plugin-request-log@4.0.1(@octokit/core@5.2.2)': - dependencies: - '@octokit/core': 5.2.2 - - '@octokit/plugin-rest-endpoint-methods@13.3.2-cjs.1(@octokit/core@5.2.2)': - dependencies: - '@octokit/core': 5.2.2 - '@octokit/types': 13.10.0 - - '@octokit/request-error@5.1.1': - dependencies: - '@octokit/types': 13.10.0 - deprecation: 2.3.1 - once: 1.4.0 - - '@octokit/request@8.4.1': - dependencies: - '@octokit/endpoint': 9.0.6 - '@octokit/request-error': 5.1.1 - '@octokit/types': 13.10.0 - universal-user-agent: 6.0.1 - - '@octokit/rest@20.1.2': - dependencies: - '@octokit/core': 5.2.2 - '@octokit/plugin-paginate-rest': 11.4.4-cjs.2(@octokit/core@5.2.2) - '@octokit/plugin-request-log': 4.0.1(@octokit/core@5.2.2) - '@octokit/plugin-rest-endpoint-methods': 13.3.2-cjs.1(@octokit/core@5.2.2) - - '@octokit/types@13.10.0': - dependencies: - '@octokit/openapi-types': 24.2.0 - - '@tootallnate/once@2.0.0': {} - - agent-base@6.0.2: - dependencies: - debug: 4.4.1 - transitivePeerDependencies: - - supports-color - - ansi-styles@3.2.1: - dependencies: - color-convert: 1.9.3 - - async-retry@1.2.3: - dependencies: - retry: 0.12.0 - - before-after-hook@2.2.3: {} - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - buffer-equal-constant-time@1.0.1: {} - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - - chalk@2.4.2: - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - - color-convert@1.9.3: - dependencies: - color-name: 1.1.3 - - color-name@1.1.3: {} - - colors@1.4.0: {} - - commander@2.20.3: {} - - core-js@3.45.1: {} - - danger-plugin-pr-hygiene@0.6.1: {} - - danger@13.0.4: - dependencies: - '@gitbeaker/rest': 38.12.1 - '@octokit/rest': 20.1.2 - async-retry: 1.2.3 - chalk: 2.4.2 - commander: 2.20.3 - core-js: 3.45.1 - debug: 4.4.1 - fast-json-patch: 3.1.1 - get-stdin: 6.0.0 - http-proxy-agent: 5.0.0 - https-proxy-agent: 5.0.1 - hyperlinker: 1.0.0 - ini: 5.0.0 - json5: 2.2.3 - jsonpointer: 5.0.1 - jsonwebtoken: 9.0.2 - lodash.find: 4.6.0 - lodash.includes: 4.3.0 - lodash.isobject: 3.0.2 - lodash.keys: 4.2.0 - lodash.mapvalues: 4.6.0 - lodash.memoize: 4.1.2 - memfs-or-file-map-to-github-branch: 1.3.0 - micromatch: 4.0.8 - node-cleanup: 2.1.2 - node-fetch: 2.7.0 - override-require: 1.1.1 - p-limit: 2.3.0 - parse-diff: 0.7.1 - parse-github-url: 1.0.3 - parse-link-header: 2.0.0 - pinpoint: 1.1.0 - prettyjson: 1.2.5 - readline-sync: 1.4.10 - regenerator-runtime: 0.13.11 - require-from-string: 2.0.2 - supports-hyperlinks: 1.0.1 - transitivePeerDependencies: - - encoding - - supports-color - - debug@4.4.1: - dependencies: - ms: 2.1.3 - - deprecation@2.3.1: {} - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - ecdsa-sig-formatter@1.0.11: - dependencies: - safe-buffer: 5.2.1 - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - escape-string-regexp@1.0.5: {} - - fast-json-patch@3.1.1: {} - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - function-bind@1.1.2: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - - get-stdin@6.0.0: {} - - gopd@1.2.0: {} - - has-flag@2.0.0: {} - - has-flag@3.0.0: {} - - has-symbols@1.1.0: {} - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - http-proxy-agent@5.0.0: - dependencies: - '@tootallnate/once': 2.0.0 - agent-base: 6.0.2 - debug: 4.4.1 - transitivePeerDependencies: - - supports-color - - https-proxy-agent@5.0.1: - dependencies: - agent-base: 6.0.2 - debug: 4.4.1 - transitivePeerDependencies: - - supports-color - - hyperlinker@1.0.0: {} - - ini@5.0.0: {} - - is-number@7.0.0: {} - - json5@2.2.3: {} - - jsonpointer@5.0.1: {} - - jsonwebtoken@9.0.2: - dependencies: - jws: 3.2.2 - lodash.includes: 4.3.0 - lodash.isboolean: 3.0.3 - lodash.isinteger: 4.0.4 - lodash.isnumber: 3.0.3 - lodash.isplainobject: 4.0.6 - lodash.isstring: 4.0.1 - lodash.once: 4.1.1 - ms: 2.1.3 - semver: 7.7.2 - - jwa@1.4.2: - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 - - jws@3.2.2: - dependencies: - jwa: 1.4.2 - safe-buffer: 5.2.1 - - lodash.find@4.6.0: {} - - lodash.includes@4.3.0: {} - - lodash.isboolean@3.0.3: {} - - lodash.isinteger@4.0.4: {} - - lodash.isnumber@3.0.3: {} - - lodash.isobject@3.0.2: {} - - lodash.isplainobject@4.0.6: {} - - lodash.isstring@4.0.1: {} - - lodash.keys@4.2.0: {} - - lodash.mapvalues@4.6.0: {} - - lodash.memoize@4.1.2: {} - - lodash.once@4.1.1: {} - - math-intrinsics@1.1.0: {} - - memfs-or-file-map-to-github-branch@1.3.0: - dependencies: - '@octokit/rest': 20.1.2 - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - minimist@1.2.8: {} - - ms@2.1.3: {} - - node-cleanup@2.1.2: {} - - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - - object-inspect@1.13.4: {} - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - override-require@1.1.1: {} - - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 - - p-try@2.2.0: {} - - parse-diff@0.7.1: {} - - parse-github-url@1.0.3: {} - - parse-link-header@2.0.0: - dependencies: - xtend: 4.0.2 - - picomatch@2.3.1: {} - - pinpoint@1.1.0: {} - - prettyjson@1.2.5: - dependencies: - colors: 1.4.0 - minimist: 1.2.8 - - qs@6.14.0: - dependencies: - side-channel: 1.1.0 - - readline-sync@1.4.10: {} - - regenerator-runtime@0.13.11: {} - - require-from-string@2.0.2: {} - - retry@0.12.0: {} - - safe-buffer@5.2.1: {} - - semver@7.7.2: {} - - side-channel-list@1.0.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.0 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - - supports-color@5.5.0: - dependencies: - has-flag: 3.0.0 - - supports-hyperlinks@1.0.1: - dependencies: - has-flag: 2.0.0 - supports-color: 5.5.0 - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - tr46@0.0.3: {} - - universal-user-agent@6.0.1: {} - - webidl-conversions@3.0.1: {} - - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - - wrappy@1.0.2: {} - - xcase@2.0.1: {} - - xtend@4.0.2: {} diff --git a/script/debug-cli b/script/debug-cli deleted file mode 100755 index 65017cd456..0000000000 --- a/script/debug-cli +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -cargo build -p zed && cargo run -p cli -- --foreground --zed=${CARGO_TARGET_DIR:-target}/debug/zed "$@" diff --git a/script/deploy-collab b/script/deploy-collab deleted file mode 100755 index f9006fd700..0000000000 --- a/script/deploy-collab +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -set -eu -source script/lib/deploy-helpers.sh - -if [[ $# != 1 ]]; then - echo "Usage: $0 " - exit 1 -fi -environment=$1 -tag="$(tag_for_environment $environment)" - -branch=$(git rev-parse --abbrev-ref HEAD) -if [ "$branch" != "main" ]; then - echo "You must be on main to run this script" - exit 1 -fi - -git pull --ff-only origin main -git tag -f $tag -git push -f origin $tag diff --git a/script/determine-release-channel b/script/determine-release-channel deleted file mode 100755 index 5545cdd909..0000000000 --- a/script/determine-release-channel +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -if [ -z "${GITHUB_ACTIONS-}" ]; then - echo "Error: This script must be run in a GitHub Actions environment" - exit 1 -elif [ -z "${GITHUB_REF-}" ]; then - # This should be the release tag 'v0.x.x' - echo "Error: GITHUB_REF is not set" - exit 1 -fi - -version=$(script/get-crate-version zed) -channel=$(cat crates/zed/RELEASE_CHANNEL) -echo "Publishing version: ${version} on release channel ${channel}" -echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV -echo "RELEASE_VERSION=${version}" >> $GITHUB_ENV - -expected_tag_name="" -case ${channel} in -stable) - expected_tag_name="v${version}";; -preview) - expected_tag_name="v${version}-pre";; -*) - echo "can't publish a release on channel ${channel}" - exit 1;; -esac -if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then - echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}" - exit 1 -fi diff --git a/script/determine-release-channel.ps1 b/script/determine-release-channel.ps1 deleted file mode 100644 index eb3ad9c005..0000000000 --- a/script/determine-release-channel.ps1 +++ /dev/null @@ -1,37 +0,0 @@ -$ErrorActionPreference = "Stop" - -if (-not $env:GITHUB_ACTIONS) { - Write-Error "Error: This script must be run in a GitHub Actions environment" - exit 1 -} - -if (-not $env:GITHUB_REF) { - Write-Error "Error: GITHUB_REF is not set" - exit 1 -} - -$version = & "script/get-crate-version.ps1" "zed" -$channel = Get-Content "crates/zed/RELEASE_CHANNEL" - -Write-Host "Publishing version: $version on release channel $channel" -Write-Output "RELEASE_CHANNEL=$channel" >> $env:GITHUB_ENV -Write-Output "RELEASE_VERSION=$version" >> $env:GITHUB_ENV - -$expectedTagName = "" -switch ($channel) { - "stable" { - $expectedTagName = "v$version" - } - "preview" { - $expectedTagName = "v$version-pre" - } - default { - Write-Error "can't publish a release on channel $channel" - exit 1 - } -} - -if ($env:GITHUB_REF_NAME -ne $expectedTagName) { - Write-Error "invalid release tag $($env:GITHUB_REF_NAME). expected $expectedTagName" - exit 1 -} diff --git a/script/digital-ocean-db.sh b/script/digital-ocean-db.sh deleted file mode 100755 index fd441e593e..0000000000 --- a/script/digital-ocean-db.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -set -e - -# Check if database name is provided -if [ $# -eq 0 ]; then - echo "Usage: $0 " - doctl databases list - exit 1 -fi - -DATABASE_NAME="$1" -DATABASE_ID=$(doctl databases list --format ID,Name --no-header | grep "$DATABASE_NAME" | awk '{print $1}') - -if [ -z "$DATABASE_ID" ]; then - echo "Error: Database '$DATABASE_NAME' not found" - exit 1 -fi -CURRENT_IP=$(curl -s https://api.ipify.org) -if [ -z "$CURRENT_IP" ]; then - echo "Error: Failed to get current IP address" - exit 1 -fi - -EXISTING_RULE=$(doctl databases firewalls list "$DATABASE_ID" | grep "ip_addr" | grep "$CURRENT_IP") - -if [ -z "$EXISTING_RULE" ]; then - echo "IP not found in whitelist. Adding $CURRENT_IP to database firewall..." - doctl databases firewalls append "$DATABASE_ID" --rule ip_addr:"$CURRENT_IP" -fi - -CONNECTION_URL=$(doctl databases connection "$DATABASE_ID" --format URI --no-header) - -if [ -z "$CONNECTION_URL" ]; then - echo "Error: Failed to get database connection details" - exit 1 -fi - -psql "$CONNECTION_URL" diff --git a/script/download-wasi-sdk b/script/download-wasi-sdk deleted file mode 100755 index 8cf36ffda1..0000000000 --- a/script/download-wasi-sdk +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -# Check if ./target/wasi-sdk exists -if [ ! -d "./target/wasi-sdk" ]; then - echo "WASI SDK not found, downloading v25..." - - # Determine OS and architecture - OS=$(uname -s | tr '[:upper:]' '[:lower:]') - ARCH=$(uname -m) - - # Map architecture names to WASI SDK format - case $ARCH in - x86_64) - ARCH="x86_64" - ;; - arm64|aarch64) - ARCH="arm64" - ;; - *) - echo "Unsupported architecture: $ARCH" - exit 1 - ;; - esac - - # Map OS names to WASI SDK format - case $OS in - darwin) - OS="macos" - ;; - linux) - OS="linux" - ;; - mingw*|msys*|cygwin*) - OS="mingw" - ;; - *) - echo "Unsupported OS: $OS" - exit 1 - ;; - esac - - # Construct download URL - WASI_SDK_VERSION="25" - WASI_SDK_URL="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_VERSION}.0-${ARCH}-${OS}.tar.gz" - - echo "Downloading from: $WASI_SDK_URL" - - # Create target directory if it doesn't exist - mkdir -p ./target - - # Download and extract - curl -L "$WASI_SDK_URL" | tar -xz -C ./target - - # Rename the extracted directory to wasi-sdk - mv "./target/wasi-sdk-${WASI_SDK_VERSION}.0-${ARCH}-${OS}" "./target/wasi-sdk" - - echo "WASI SDK v25 installed successfully" -else - echo "WASI SDK already exists at ./target/wasi-sdk" -fi diff --git a/script/draft-release-notes b/script/draft-release-notes deleted file mode 100755 index 2436aa6617..0000000000 --- a/script/draft-release-notes +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env node --redirect-warnings=/dev/null - -const { execFileSync } = require("child_process"); - -main(); - -async function main() { - let version = process.argv[2]; - let channel = process.argv[3]; - let parts = version.split("."); - - if ( - process.argv.length != 4 || - parts.length != 3 || - parts.find((part) => isNaN(part)) != null || - (channel != "stable" && channel != "preview") - ) { - console.log("Usage: draft-release-notes {stable|preview}"); - process.exit(1); - } - - // currently we can only draft notes for patch releases. - if (parts[2] === 0) { - process.exit(0); - } - - let priorVersion = [parts[0], parts[1], parts[2] - 1].join("."); - let suffix = channel == "preview" ? "-pre" : ""; - let [tag, priorTag] = [`v${version}${suffix}`, `v${priorVersion}${suffix}`]; - - try { - execFileSync("rm", ["-rf", "target/shallow_clone"]); - execFileSync("git", [ - "clone", - "https://github.com/zed-industries/zed", - "target/shallow_clone", - "--filter=tree:0", - "--no-checkout", - "--branch", - tag, - "--depth", - 100, - ]); - execFileSync("git", ["-C", "target/shallow_clone", "rev-parse", "--verify", tag]); - try { - execFileSync("git", ["-C", "target/shallow_clone", "rev-parse", "--verify", priorTag]); - } catch (e) { - console.error(`Prior tag ${priorTag} not found`); - process.exit(0); - } - } catch (e) { - console.error(e.stderr.toString()); - process.exit(1); - } - - const newCommits = getCommits(priorTag, tag); - - let releaseNotes = []; - let missing = []; - let skipped = []; - - for (const commit of newCommits) { - let link = "https://github.com/zed-industries/zed/pull/" + commit.pr; - let notes = commit.releaseNotes; - if (commit.pr == "") { - link = "https://github.com/zed-industries/zed/commits/" + commit.hash; - } else if (!notes.includes("zed-industries/zed/issues")) { - notes = notes + " ([#" + commit.pr + "](" + link + "))"; - } - - if (commit.releaseNotes == "") { - missing.push("- MISSING " + commit.firstLine + " " + link); - } else if (commit.releaseNotes.startsWith("- N/A")) { - skipped.push("- N/A " + commit.firstLine + " " + link); - } else { - releaseNotes.push(notes); - } - } - - console.log(releaseNotes.join("\n") + "\n"); -} - -function getCommits(oldTag, newTag) { - const pullRequestNumbers = execFileSync( - "git", - ["-C", "target/shallow_clone", "log", `${oldTag}..${newTag}`, "--format=DIVIDER\n%H|||%B"], - { encoding: "utf8" }, - ) - .replace(/\r\n/g, "\n") - .split("DIVIDER\n") - .filter((commit) => commit.length > 0) - .map((commit) => { - let [hash, firstLine] = commit.split("\n")[0].split("|||"); - let cherryPick = firstLine.match(/\(cherry-pick #([0-9]+)\)/)?.[1] || ""; - let pr = firstLine.match(/\(#(\d+)\)$/)?.[1] || ""; - let releaseNotes = (commit.split(/Release notes:.*\n/i)[1] || "") - .split("\n\n")[0] - .trim() - .replace(/\n(?![\n-])/g, " "); - - if (releaseNotes.includes("")) { - releaseNotes = ""; - } - - return { - hash, - pr, - cherryPick, - releaseNotes, - firstLine, - }; - }); - - return pullRequestNumbers; -} diff --git a/script/drop-test-dbs b/script/drop-test-dbs deleted file mode 100755 index d96f1bd1f0..0000000000 --- a/script/drop-test-dbs +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -databases=$(psql --tuples-only --command " - SELECT - datname - FROM - pg_database - WHERE - datistemplate = false - AND datname like 'zed-test-%' -") - -for database in $databases; do - echo $database - dropdb $database -done \ No newline at end of file diff --git a/script/exit-ci-if-dev-drive-is-full.ps1 b/script/exit-ci-if-dev-drive-is-full.ps1 deleted file mode 100644 index 98684d58ee..0000000000 --- a/script/exit-ci-if-dev-drive-is-full.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -param ( - [Parameter(Mandatory = $true)] - [int]$MAX_SIZE_IN_GB -) - -$ErrorActionPreference = "Stop" -$PSNativeCommandUseErrorActionPreference = $true -$ProgressPreference = "SilentlyContinue" - -if (-Not (Test-Path -Path "target")) { - Write-Host "target directory does not exist yet" - exit 0 -} - -$current_size_gb = (Get-ChildItem -Recurse -Force -File -Path "target" | Measure-Object -Property Length -Sum).Sum / 1GB - -Write-Host "target directory size: ${current_size_gb}GB. max size: ${MAX_SIZE_IN_GB}GB" - -if ($current_size_gb -gt $MAX_SIZE_IN_GB) { - Write-Host "Dev drive is almost full, increase the size first!" - exit 1 -} diff --git a/script/flatpak/bundle-flatpak b/script/flatpak/bundle-flatpak deleted file mode 100755 index c9196ed4b8..0000000000 --- a/script/flatpak/bundle-flatpak +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail -cd "$(dirname "$0")/../.." -shopt -s extglob - -script/bundle-linux --flatpak -archive_match="zed(-[a-zA-Z0-9]+)?-linux-$(uname -m)\.tar\.gz" -archive=$(ls "target/release" | grep -E ${archive_match}) -channel=$( "$APP_ID.json" -flatpak-builder --user --install --force-clean build "$APP_ID.json" -flatpak build-bundle ~/.local/share/flatpak/repo "target/release/$APP_ID.flatpak" "$APP_ID" -echo "Created 'target/release/$APP_ID.flatpak'" diff --git a/script/flatpak/convert-release-notes.py b/script/flatpak/convert-release-notes.py deleted file mode 100644 index 2c29cbacbf..0000000000 --- a/script/flatpak/convert-release-notes.py +++ /dev/null @@ -1,93 +0,0 @@ -import os -import re -import requests -import sys -import textwrap - -from html import escape - -def clean_line(line: str, in_code_fence: bool) -> str: - line = re.sub(r"\(\[(#\d+)\]\([\w|\d\:|\/|\.|\-|_]*\)\)", lambda match: f"[{match.group(1)}]", line) - line = re.sub(r"\[(#\d+)\]\([\w|\d\:|\/|\.|\-|_]*\)", lambda match: f"[{match.group(1)}]", line) - if not in_code_fence: - line = line.strip() - - return escape(line) - - -def convert_body(body: str) -> str: - formatted = "" - - in_code_fence = False - in_list = False - for line in body.splitlines(): - line = clean_line(line, in_code_fence) - if not line: - continue - if re.search(r'\[[\w|\d|:|\/|\.|\-|_]*\]\([\w|\d|:|\/|\.|\-|_]*\)', line): - continue - line = re.sub(r"(?{match.group(1)}", line) - - contains_code_fence = bool(re.search(r"```", line)) - is_list = bool(re.search(r"^-\s*", line)) - - if in_list and not is_list: - formatted += "\n" - if (not in_code_fence and contains_code_fence) or (not in_list and is_list): - formatted += "
    \n" - in_list = is_list - in_code_fence = contains_code_fence != in_code_fence - - if is_list: - line = re.sub(r"^-\s*", "", line) - line = f"
  • {line}
  • " - elif in_code_fence or contains_code_fence: - line = f"
  • {line}
  • " - else: - line = f"

    {line}

    " - formatted += f"{line}\n" - - if (not in_code_fence and contains_code_fence): - formatted += "
\n" - if in_code_fence or in_list: - formatted += "\n" - return formatted - -def get_release_info(tag: str): - url = f"https://api.github.com/repos/zed-industries/zed/releases/tags/{tag}" - response = requests.get(url) - if response.status_code == 200: - return response.json() - else: - print(f"Failed to fetch release info for tag '{tag}'. Status code: {response.status_code}") - quit() - - -if __name__ == "__main__": - os.chdir(sys.path[0]) - - if len(sys.argv) != 3: - print("Usage: python convert-release-notes.py ") - sys.exit(1) - - tag = sys.argv[1] - channel = sys.argv[2] - - release_info = get_release_info(tag) - body = convert_body(release_info["body"]) - version = tag.removeprefix("v").removesuffix("-pre") - date = release_info["published_at"] - - release_info_str = f"\n" - release_info_str += f" \n" - release_info_str += textwrap.indent(body, " " * 8) - release_info_str += f" \n" - release_info_str += f" https://github.com/zed-industries/zed/releases/tag/{tag}\n" - release_info_str += "\n" - - channel_releases_file = f"../../crates/zed/resources/flatpak/release-info/{channel}" - with open(channel_releases_file) as f: - old_release_info = f.read() - with open(channel_releases_file, "w") as f: - f.write(textwrap.indent(release_info_str, " " * 8) + old_release_info) - print(f"Added release notes from {tag} to '{channel_releases_file}'") diff --git a/script/flatpak/deps b/script/flatpak/deps deleted file mode 100755 index dec24dfc87..0000000000 --- a/script/flatpak/deps +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo - -arch=$(arch) -fd_version=23.08 -flatpak install -y --user org.freedesktop.Platform/${arch}/${fd_version} -flatpak install -y --user org.freedesktop.Sdk/${arch}/${fd_version} -flatpak install -y --user org.freedesktop.Sdk.Extension.rust-stable/${arch}/${fd_version} diff --git a/script/freebsd b/script/freebsd deleted file mode 100755 index 58579d8ac9..0000000000 --- a/script/freebsd +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash - -set -xeuo pipefail - -# if root or if sudo/unavailable, define an empty variable -if [ "$(id -u)" -eq 0 ]; then - maysudo='' -else - maysudo="$(command -v sudo || command -v doas || true)" -fi - -function finalize { - # after packages install (curl, etc), get the rust toolchain - which rustup >/dev/null 2>&1 || curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - echo "Finished installing FreeBSD dependencies with script/freebsd" -} - -# FreeBSD -# https://www.freebsd.org/ports/ -pkg=$(command -v pkg || true) -if [[ -n $pkg ]]; then - deps=( - cmake - gcc - git - llvm - protobuf - rustup-init - libx11 - alsa-lib - ) - $maysudo "$pkg" install "${deps[@]}" - finalize - exit 0 -fi diff --git a/script/generate-licenses b/script/generate-licenses deleted file mode 100755 index 6a833acd20..0000000000 --- a/script/generate-licenses +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -CARGO_ABOUT_VERSION="0.8.2" -OUTPUT_FILE="${1:-$(pwd)/assets/licenses.md}" -TEMPLATE_FILE="script/licenses/template.md.hbs" - -fail_on_stderr() { - local tmpfile=$(mktemp) - "$@" 2> >(tee "$tmpfile" >&2) - local rc=$? - [ -s "$tmpfile" ] && rc=1 - rm "$tmpfile" - return $rc -} - -echo -n "" >"$OUTPUT_FILE" - -{ - echo -e "# ###### THEME LICENSES ######\n" - cat assets/themes/LICENSES - - echo -e "\n# ###### ICON LICENSES ######\n" - cat assets/icons/LICENSES - - echo -e "\n# ###### CODE LICENSES ######\n" -} >>"$OUTPUT_FILE" - -if ! cargo about --version | grep "cargo-about $CARGO_ABOUT_VERSION" &>/dev/null; then - echo "Installing cargo-about@$CARGO_ABOUT_VERSION..." - cargo install "cargo-about@$CARGO_ABOUT_VERSION" -else - echo "cargo-about@$CARGO_ABOUT_VERSION is already installed." -fi - -echo "Generating cargo licenses" -if [ -z "${ALLOW_MISSING_LICENSES-}" ]; then FAIL_FLAG=--fail; else FAIL_FLAG=""; fi -if [ -z "${ALLOW_MISSING_LICENSES-}" ]; then WRAPPER=fail_on_stderr; else WRAPPER=""; fi -set -x -$WRAPPER cargo about generate \ - $FAIL_FLAG \ - -c script/licenses/zed-licenses.toml \ - "$TEMPLATE_FILE" >>"$OUTPUT_FILE" -set +x - -sed -i.bak 's/"/"/g' "$OUTPUT_FILE" -sed -i.bak 's/'/'\''/g' "$OUTPUT_FILE" # The ` '\'' ` thing ends the string, appends a single quote, and re-opens the string -sed -i.bak 's/=/=/g' "$OUTPUT_FILE" -sed -i.bak 's/`/`/g' "$OUTPUT_FILE" -sed -i.bak 's/<//g' "$OUTPUT_FILE" - -rm -rf "${OUTPUT_FILE}.bak" - -echo "generate-licenses completed. See $OUTPUT_FILE" diff --git a/script/generate-licenses-csv b/script/generate-licenses-csv deleted file mode 100755 index dd86f872d0..0000000000 --- a/script/generate-licenses-csv +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -CARGO_ABOUT_VERSION="0.8.2" -OUTPUT_FILE="${1:-$(pwd)/assets/licenses.csv}" -TEMPLATE_FILE="script/licenses/template.csv.hbs" - -if ! cargo about --version | grep "cargo-about $CARGO_ABOUT_VERSION" 2>&1 > /dev/null; then - echo "Installing cargo-about@$CARGO_ABOUT_VERSION..." - cargo install "cargo-about@$CARGO_ABOUT_VERSION" -else - echo "cargo-about@$CARGO_ABOUT_VERSION is already installed." -fi - -echo "Generating cargo licenses" -set -x -cargo about generate \ - --fail \ - -c script/licenses/zed-licenses.toml \ - $TEMPLATE_FILE \ - | awk 'NR==1{print;next} NF{print | "sort"}' \ - > "$OUTPUT_FILE" -set +x - -echo "generate-licenses-csv completed. See $OUTPUT_FILE" diff --git a/script/generate-licenses.ps1 b/script/generate-licenses.ps1 deleted file mode 100644 index 80cd249a46..0000000000 --- a/script/generate-licenses.ps1 +++ /dev/null @@ -1,44 +0,0 @@ -$CARGO_ABOUT_VERSION="0.8.2" -$outputFile=$args[0] ? $args[0] : "$(Get-Location)/assets/licenses.md" -$templateFile="script/licenses/template.md.hbs" - -New-Item -Path "$outputFile" -ItemType File -Value "" -Force - -@( - "# ###### THEME LICENSES ######\n" - Get-Content assets/themes/LICENSES - "\n# ###### ICON LICENSES ######\n" - Get-Content assets/icons/LICENSES - "\n# ###### CODE LICENSES ######\n" -) | Add-Content -Path $outputFile - -$versionOutput = cargo about --version -if (-not ($versionOutput -match "cargo-about $CARGO_ABOUT_VERSION")) { - Write-Host "Installing cargo-about@$CARGO_ABOUT_VERSION..." - cargo install "cargo-about@$CARGO_ABOUT_VERSION" -} else { - Write-Host "cargo-about@$CARGO_ABOUT_VERSION" is already installed -} - -Write-Host "Generating cargo licenses" - -$failFlag = $env:ALLOW_MISSING_LICENSES ? "--fail" : "" -$args = @('about', 'generate', $failFlag, '-c', 'script/licenses/zed-licenses.toml', $templateFile, '-o', $outputFile) | Where-Object { $_ } -cargo @args - -Write-Host "Applying replacements" -$replacements = @{ - '"' = '"' - ''' = "'" - '=' = '=' - '`' = '`' - '<' = '<' - '>' = '>' -} -$content = Get-Content $outputFile -foreach ($find in $replacements.keys) { - $content = $content -replace $find, $replacements[$find] -} -$content | Set-Content $outputFile - -Write-Host "generate-licenses completed. See $outputFile" diff --git a/script/generate-terms-rtf b/script/generate-terms-rtf deleted file mode 100755 index 654972931f..0000000000 --- a/script/generate-terms-rtf +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -set -e - -if ! command -v pandoc &> /dev/null -then - brew install pandoc # Install pandoc using Homebrew -fi - -pandoc ./legal/terms.md -f markdown-smart -t rtf -o ./script/terms/terms.rtf --standalone diff --git a/script/get-crate-version b/script/get-crate-version deleted file mode 100755 index d642eb0867..0000000000 --- a/script/get-crate-version +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -set -eu - -if [[ $# -ne 1 ]]; then - echo "Usage: $0 " >&2 - exit 1 -fi - -CRATE_NAME=$1 - -cargo metadata \ - --no-deps \ - --format-version=1 \ - | jq \ - --raw-output \ - ".packages[] | select(.name == \"${CRATE_NAME}\") | .version" diff --git a/script/get-crate-version.ps1 b/script/get-crate-version.ps1 deleted file mode 100644 index d86c971e32..0000000000 --- a/script/get-crate-version.ps1 +++ /dev/null @@ -1,16 +0,0 @@ -if ($args.Length -ne 1) { - Write-Error "Usage: $($MyInvocation.MyCommand.Name) " - exit 1 -} - -$crateName = $args[0] - -$metadata = cargo metadata --no-deps --format-version=1 | ConvertFrom-Json - -$package = $metadata.packages | Where-Object { $_.name -eq $crateName } -if ($package) { - $package.version -} -else { - Write-Error "Crate '$crateName' not found." -} diff --git a/script/get-pull-requests-since b/script/get-pull-requests-since deleted file mode 100755 index c8509480a6..0000000000 --- a/script/get-pull-requests-since +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env node --redirect-warnings=/dev/null - -const { execFileSync } = require("child_process"); -let { GITHUB_ACCESS_TOKEN } = process.env; -const PR_REGEX = /#\d+/; // Ex: matches on #4241 -const FIXES_REGEX = /(fixes|closes|completes) (.+[/#]\d+.*)$/im; - -main(); - -async function main() { - if (!GITHUB_ACCESS_TOKEN) { - try { - GITHUB_ACCESS_TOKEN = execFileSync("gh", ["auth", "token"]).toString(); - } catch (error) { - console.log(error); - console.log("No GITHUB_ACCESS_TOKEN, and no `gh auth token`"); - process.exit(1); - } - } - - // Use form of: YYYY-MM-DD - 2023-01-09 - const startDate = new Date(process.argv[2]); - const today = new Date(); - - console.log(`Pull requests from ${startDate} to ${today}\n`); - - let pullRequestNumbers = getPullRequestNumbers(startDate, today); - - // Fetch the pull requests from the GitHub API. - console.log("Merged pull requests:"); - for (const pullRequestNumber of pullRequestNumbers) { - const webURL = `https://github.com/zed-industries/zed/pull/${pullRequestNumber}`; - const apiURL = `https://api.github.com/repos/zed-industries/zed/pulls/${pullRequestNumber}`; - - const response = await fetch(apiURL, { - headers: { - Authorization: `token ${GITHUB_ACCESS_TOKEN}`, - }, - }); - - const pullRequest = await response.json(); - console.log("*", pullRequest.title); - console.log(" PR URL: ", webURL); - console.log(" Merged: ", pullRequest.merged_at); - console.log(); - } -} - -function getPullRequestNumbers(startDate, endDate) { - const sinceDate = startDate.toISOString(); - const untilDate = endDate.toISOString(); - - const pullRequestNumbers = execFileSync( - "git", - ["log", `--since=${sinceDate}`, `--until=${untilDate}`, "--oneline"], - { encoding: "utf8" }, - ) - .split("\n") - .filter((line) => line.length > 0) - .map((line) => { - const match = line.match(/#(\d+)/); - return match ? match[1] : null; - }) - .filter((line) => line); - - return pullRequestNumbers; -} diff --git a/script/get-release-notes-since b/script/get-release-notes-since deleted file mode 100755 index 20a6fc18de..0000000000 --- a/script/get-release-notes-since +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env node --redirect-warnings=/dev/null - -const { execFileSync } = require("child_process"); -const { GITHUB_ACCESS_TOKEN } = process.env; - -main(); - -async function main() { - const startDate = new Date(process.argv[2]); - const today = new Date(); - - console.log(`Release notes from ${startDate} to ${today}\n`); - - const releases = await getReleases(startDate, today); - const previewReleases = releases.filter((release) => - release.tagName.includes("-pre"), - ); - - const stableReleases = releases.filter( - (release) => !release.tagName.includes("-pre"), - ); - - // Filter out all preview release, as all of those changes have made it to the stable release, except for the latest preview release - const aggregatedReleases = stableReleases - .concat(previewReleases[0]) - .reverse(); - - const aggregatedReleaseTitles = aggregatedReleases - .map((release) => release.name) - .join(", "); - - console.log(); - console.log(`Release titles: ${aggregatedReleaseTitles}`); - - console.log("Release notes:"); - console.log(); - - for (const release of aggregatedReleases) { - const publishedDate = release.publishedAt.split("T")[0]; - console.log(`${release.name}: ${publishedDate}`); - console.log(); - console.log(release.description); - console.log(); - } -} - -async function getReleases(startDate, endDate) { - const query = ` - query ($owner: String!, $repo: String!, $cursor: String) { - repository(owner: $owner, name: $repo) { - releases(first: 100, orderBy: {field: CREATED_AT, direction: DESC}, after: $cursor) { - nodes { - tagName - name - createdAt - publishedAt - description - url - author { - login - } - } - pageInfo { - hasNextPage - endCursor - } - } - } - } - `; - - let allReleases = []; - let hasNextPage = true; - let cursor = null; - - while (hasNextPage) { - const response = await fetch("https://api.github.com/graphql", { - method: "POST", - headers: { - Authorization: `Bearer ${GITHUB_ACCESS_TOKEN}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query, - variables: { owner: "zed-industries", repo: "zed", cursor }, - }), - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - - if (data.errors) { - throw new Error(`GraphQL error: ${JSON.stringify(data.errors)}`); - } - - if (!data.data || !data.data.repository || !data.data.repository.releases) { - throw new Error(`Unexpected response structure: ${JSON.stringify(data)}`); - } - - const releases = data.data.repository.releases.nodes; - allReleases = allReleases.concat(releases); - - hasNextPage = data.data.repository.releases.pageInfo.hasNextPage; - cursor = data.data.repository.releases.pageInfo.endCursor; - - lastReleaseOnPage = releases[releases.length - 1]; - - if ( - releases.length > 0 && - new Date(lastReleaseOnPage.createdAt) < startDate - ) { - break; - } - } - - const filteredReleases = allReleases.filter((release) => { - const releaseDate = new Date(release.createdAt); - return releaseDate >= startDate && releaseDate <= endDate; - }); - - return filteredReleases; -} diff --git a/script/get-released-version b/script/get-released-version deleted file mode 100755 index 0fbb2e1757..0000000000 --- a/script/get-released-version +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -channel="$1" - -query="" -case $channel in - stable) - ;; - preview) - query="&preview=1" - ;; - nightly) - query="&nightly=1" - ;; - *) - echo "this must be run on either of stable|preview|nightly release branches" >&2 - exit 1 - ;; -esac - -curl -s "https://cloud.zed.dev/releases/$channel/latest/asset?asset=zed&os=macos&arch=aarch64" | jq -r .version diff --git a/script/github-clean-issue-types.py b/script/github-clean-issue-types.py deleted file mode 100755 index dfd573628b..0000000000 --- a/script/github-clean-issue-types.py +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env python3 -"""Replace 'bug/feature/crash' labels with 'Bug/Feature/Crash' types on open -GitHub issues. - -Requires `requests` library and a GitHub access token with "Issues (write)" -permission passed as an environment variable. -Was used as a quick-and-dirty one-off-bulk-operation script to clean up issue -types in the `zed` repository. Leaving it here for reference only; there's no -error handling, you've been warned. -""" - - -import logging -import os - -import requests - -logging.basicConfig(level=logging.INFO) -log = logging.getLogger(__name__) - -GITHUB_API_BASE_URL = "https://api.github.com" -REPO_OWNER = "zed-industries" -REPO_NAME = "zed" -GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") -HEADERS = { - "Authorization": f"token {GITHUB_TOKEN}", - "Accept": "application/vnd.github+json" -} -LABELS_TO_TYPES = { - 'bug': 'Bug', - 'feature': 'Feature', - 'crash': 'Crash', - } - - -def get_open_issues_without_type(repo): - """Get open issues without type via GitHub's REST API.""" - issues = [] - issues_url = f"{GITHUB_API_BASE_URL}/repos/{REPO_OWNER}/{repo}/issues" - - log.info("Start fetching issues from the GitHub API.") - params = { - "state": "open", - "type": "none", - "page": 1, - "per_page": 100, # worked fine despite the docs saying 30 - } - while True: - response = requests.get(issues_url, headers=HEADERS, params=params) - response.raise_for_status() - issues.extend(response.json()) - log.info(f"Fetched the next page, total issues so far: {len(issues)}.") - - # is there a next page? - link_header = response.headers.get('Link', '') - if 'rel="next"' not in link_header: - break - params['page'] += 1 - - log.info("Done fetching issues.") - return issues - - -def replace_labels_with_types(issues, labels_to_types): - """Replace labels with types, a new attribute of issues. - - Only changes the issues with one type-sounding label, leaving those with - two labels (e.g. `bug` *and* `crash`) alone, logging a warning. - """ - for issue in issues: - log.debug(f"Processing issue {issue['number']}.") - # for GitHub, all PRs are issues but not all issues are PRs; skip PRs - if 'pull_request' in issue: - continue - issue_labels = (label['name'] for label in issue['labels']) - matching_labels = labels_to_types.keys() & set(issue_labels) - if len(matching_labels) != 1: - log.warning( - f"Issue {issue['url']} has either no or multiple type-sounding " - "labels, won't be processed.") - continue - label_to_replace = matching_labels.pop() - issue_type = labels_to_types[label_to_replace] - log.debug( - f"Replacing label {label_to_replace} with type {issue_type} " - f"for issue {issue['title']}.") - - # add the type - api_url_issue = f"{GITHUB_API_BASE_URL}/repos/{REPO_OWNER}/{REPO_NAME}/issues/{issue['number']}" - add_type_response = requests.patch( - api_url_issue, headers=HEADERS, json={"type": issue_type}) - add_type_response.raise_for_status() - log.debug(f"Added type {issue_type} to issue {issue['title']}.") - - # delete the label - api_url_delete_label = f"{GITHUB_API_BASE_URL}/repos/{REPO_OWNER}/{REPO_NAME}/issues/{issue['number']}/labels/{label_to_replace}" - delete_response = requests.delete(api_url_delete_label, headers=HEADERS) - delete_response.raise_for_status() - log.info( - f"Deleted label {label_to_replace} from issue {issue['title']}.") - - -if __name__ == "__main__": - open_issues_without_type = get_open_issues_without_type(REPO_NAME) - replace_labels_with_types(open_issues_without_type, LABELS_TO_TYPES) diff --git a/script/github-label-issues-to-triage.py b/script/github-label-issues-to-triage.py deleted file mode 100755 index 9a7274a4aa..0000000000 --- a/script/github-label-issues-to-triage.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python3 -"""Add `state:needs triage` label to open GitHub issues of types Bug and Crash -if they're missing area, priority, or frequency labels. Don't touch issues -with an assignee or another `state:` label. - -Requires `requests` library and a GitHub access token with "Issues (write)" -permission passed as an environment variable. Was used as a quick-and-dirty -one-off-bulk-operation script to surface older untriaged issues in the `zed` -repository. Leaving it here for reference only; there's no error handling or -guardrails, you've been warned. -""" - -import itertools -import logging -import os - -import requests - - -logging.basicConfig(level=logging.INFO) -log = logging.getLogger(__name__) - -GITHUB_API_BASE_URL = "https://api.github.com" -REPO_OWNER = "zed-industries" -REPO_NAME = "zed" -GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") -HEADERS = { - "Authorization": f"token {GITHUB_TOKEN}", - "Accept": "application/vnd.github+json" -} -REQUIRED_LABELS_PREFIXES = ["area:", "priority:", "frequency:"] -NEEDS_TRIAGE_LABEL = "state:needs triage" - - -def get_open_issues(repo, issue_type): - """Get open issues of certain type(s) via GitHub's REST API.""" - issues = [] - issues_url = f"{GITHUB_API_BASE_URL}/repos/{REPO_OWNER}/{repo}/issues" - - log.info("Start fetching open issues from the GitHub API.") - params = { - "state": "open", - "type": issue_type, - "page": 1, - "per_page": 100, # worked fine despite the docs saying 30 - } - while True: - response = requests.get(issues_url, headers=HEADERS, params=params) - response.raise_for_status() - issues.extend(response.json()) - log.info(f"Fetched the next page, total issues so far: {len(issues)}.") - - # is there a next page? - link_header = response.headers.get('Link', '') - if 'rel="next"' not in link_header: - break - params['page'] += 1 - - log.info("Done fetching issues.") - return issues - - -def is_untriaged(issue): - issue_labels = [label['name'] for label in issue['labels']] - # don't want to overwrite existing state labels - no_state_label = not any(label.startswith('state:') for label in issue_labels) - # we want at least one label for each of the required prefixes - has_all_required_labels = all( - any(label.startswith(prefix) for label in issue_labels) - for prefix in REQUIRED_LABELS_PREFIXES - ) - # let's also assume if we managed to assign an issue it's triaged enough - no_assignee = not issue['assignee'] - return no_state_label and no_assignee and not has_all_required_labels - - -def label_issues(issues, label): - for issue in issues: - log.debug(f"Processing issue {issue['number']}.") - api_url_add_label = f"{GITHUB_API_BASE_URL}/repos/{REPO_OWNER}/{REPO_NAME}/issues/{issue['number']}/labels" - add_response = requests.post( - api_url_add_label, headers=HEADERS, json={"labels": [label]} - ) - add_response.raise_for_status() - log.info(f"Added label '{label}' to issue {issue['title']}.") - - -if __name__ == "__main__": - open_bugs = get_open_issues(REPO_NAME, "Bug") - open_crashes = get_open_issues(REPO_NAME, "Crash") - untriaged_issues = filter( - is_untriaged, itertools.chain(open_bugs, open_crashes)) - label_issues(untriaged_issues, label=NEEDS_TRIAGE_LABEL) diff --git a/script/github-pr-status b/script/github-pr-status deleted file mode 100755 index b3b0463165..0000000000 --- a/script/github-pr-status +++ /dev/null @@ -1,210 +0,0 @@ -#!/usr/bin/env python3 -""" -GitHub PR Analyzer for zed-industries/zed repository -Downloads all PRs and groups them by first assignee with status, open date, and last updated date. -""" - -import urllib.request -import urllib.parse -import urllib.error -import json -from datetime import datetime -from collections import defaultdict -import sys -import os - -# GitHub API configuration -GITHUB_API_BASE = "https://api.github.com" -REPO_OWNER = "zed-industries" -REPO_NAME = "zed" -GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") - -def make_github_request(url, params=None): - """Make a request to GitHub API with proper headers and pagination support.""" - if params: - url_parts = list(urllib.parse.urlparse(url)) - query = dict(urllib.parse.parse_qsl(url_parts[4])) - query.update(params) - url_parts[4] = urllib.parse.urlencode(query) - url = urllib.parse.urlunparse(url_parts) - - req = urllib.request.Request(url) - req.add_header("Accept", "application/vnd.github.v3+json") - req.add_header("User-Agent", "GitHub-PR-Analyzer") - - if GITHUB_TOKEN: - req.add_header("Authorization", f"token {GITHUB_TOKEN}") - - try: - response = urllib.request.urlopen(req) - return response - except urllib.error.URLError as e: - print(f"Error making request to {url}: {e}") - return None - except urllib.error.HTTPError as e: - print(f"HTTP error {e.code} for {url}: {e.reason}") - return None - -def fetch_all_prs(): - """Fetch all PRs from the repository using pagination.""" - prs = [] - page = 1 - per_page = 100 - - print("Fetching PRs from GitHub API...") - - while True: - url = f"{GITHUB_API_BASE}/repos/{REPO_OWNER}/{REPO_NAME}/pulls" - params = { - "state": "open", - "sort": "updated", - "direction": "desc", - "per_page": per_page, - "page": page - } - - response = make_github_request(url, params) - if not response: - break - - try: - data = response.read().decode('utf-8') - page_prs = json.loads(data) - except (json.JSONDecodeError, UnicodeDecodeError) as e: - print(f"Error parsing response: {e}") - break - - if not page_prs: - break - - prs.extend(page_prs) - print(f"Fetched page {page}: {len(page_prs)} PRs (Total: {len(prs)})") - - # Check if we have more pages - link_header = response.getheader('Link', '') - if 'rel="next"' not in link_header: - break - - page += 1 - - print(f"Total PRs fetched: {len(prs)}") - return prs - -def format_date_as_days_ago(date_string): - """Format ISO date string as 'X days ago'.""" - if not date_string: - return "N/A days ago" - - try: - dt = datetime.fromisoformat(date_string.replace('Z', '+00:00')) - now = datetime.now(dt.tzinfo) - days_diff = (now - dt).days - - if days_diff == 0: - return "today" - elif days_diff == 1: - return "1 day ago" - else: - return f"{days_diff} days ago" - except: - return "N/A days ago" - -def get_first_assignee(pr): - """Get the first assignee from a PR, or return 'Unassigned' if none.""" - assignees = pr.get('assignees', []) - if assignees: - return assignees[0].get('login', 'Unknown') - return 'Unassigned' - -def get_pr_status(pr): - """Determine if PR is draft or ready for review.""" - if pr.get('draft', False): - return "Draft" - return "Ready" - -def analyze_prs(prs): - """Group PRs by first assignee and organize the data.""" - grouped_prs = defaultdict(list) - - for pr in prs: - assignee = get_first_assignee(pr) - - pr_info = { - 'number': pr['number'], - 'title': pr['title'], - 'status': get_pr_status(pr), - 'state': pr['state'], - 'created_at': format_date_as_days_ago(pr['created_at']), - 'updated_at': format_date_as_days_ago(pr['updated_at']), - 'updated_at_raw': pr['updated_at'], - 'url': pr['html_url'], - 'author': pr['user']['login'] - } - - grouped_prs[assignee].append(pr_info) - - # Sort PRs within each group by update date (newest first) - for assignee in grouped_prs: - grouped_prs[assignee].sort(key=lambda x: x['updated_at_raw'], reverse=True) - - return dict(grouped_prs) - -def print_pr_report(grouped_prs): - """Print formatted report of PRs grouped by assignee.""" - print(f"OPEN PR REPORT FOR {REPO_OWNER}/{REPO_NAME}") - print() - - # Sort assignees alphabetically, but put 'Unassigned' last - assignees = sorted(grouped_prs.keys()) - if 'Unassigned' in assignees: - assignees.remove('Unassigned') - assignees.append('Unassigned') - - total_prs = sum(len(prs) for prs in grouped_prs.values()) - print(f"Total Open PRs: {total_prs}") - print() - - for assignee in assignees: - prs = grouped_prs[assignee] - assignee_display = f"@{assignee}" if assignee != 'Unassigned' else assignee - print(f"assigned to {assignee_display} ({len(prs)} PRs):") - - for pr in prs: - print(f"- {pr['author']}: [{pr['title']}]({pr['url']}) opened:{pr['created_at']} updated:{pr['updated_at']}") - - print() - -def save_json_report(grouped_prs, filename="pr_report.json"): - """Save the PR data to a JSON file.""" - try: - with open(filename, 'w') as f: - json.dump(grouped_prs, f, indent=2) - print(f"📄 Report saved to {filename}") - except Exception as e: - print(f"Error saving JSON report: {e}") - -def main(): - """Main function to orchestrate the PR analysis.""" - print("GitHub PR Analyzer") - print("==================") - - if not GITHUB_TOKEN: - print("⚠️ Warning: GITHUB_TOKEN not set. You may hit rate limits.") - print(" Set GITHUB_TOKEN environment variable for authenticated requests.") - print() - - # Fetch all PRs - prs = fetch_all_prs() - - if not prs: - print("❌ Failed to fetch PRs. Please check your connection and try again.") - sys.exit(1) - - # Analyze and group PRs - grouped_prs = analyze_prs(prs) - - # Print report - print_pr_report(grouped_prs) - -if __name__ == "__main__": - main() diff --git a/script/histogram b/script/histogram deleted file mode 100755 index 32db95134e..0000000000 --- a/script/histogram +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python3 - -# Required dependencies for this script: -# -# pandas: For data manipulation and analysis. -# matplotlib: For creating static, interactive, and animated visualizations in Python. -# seaborn: For making statistical graphics in Python, based on matplotlib. - -# To install these dependencies, use the following pip command: -# pip install pandas matplotlib seaborn - -# This script is designed to parse log files for performance measurements and create histograms of these measurements. -# It expects log files to contain lines with measurements in the format "measurement: timeunit" where timeunit can be in milliseconds (ms) or microseconds (µs). -# Lines that do not contain a colon ':' are skipped. -# The script takes one or more file paths as command-line arguments, parses each log file, and then combines the data into a single DataFrame. -# It then converts all time measurements into milliseconds, discards the original time and unit columns, and creates histograms for each unique measurement type. -# The histograms display the distribution of times for each measurement, separated by log file, and normalized to show density rather than count. -# To use this script, run it from the command line with the log file paths as arguments, like so: -# python this_script.py log1.txt log2.txt ... -# The script will then parse the provided log files and display the histograms for each type of measurement found. - -import pandas as pd -import matplotlib.pyplot as plt -import seaborn as sns -import sys - -def parse_log_file(file_path): - data = {'measurement': [], 'time': [], 'unit': [], 'log_file': []} - with open(file_path, 'r') as file: - for line in file: - if ':' not in line: - continue - - parts = line.strip().split(': ') - if len(parts) != 2: - continue - - measurement, time_with_unit = parts[0], parts[1] - if 'ms' in time_with_unit: - time, unit = time_with_unit[:-2], 'ms' - elif 'µs' in time_with_unit: - time, unit = time_with_unit[:-2], 'µs' - else: - # Print an error message if we can't parse the line and then continue with rest. - print(f'Error: Invalid time unit in line "{line.strip()}". Skipping.', file=sys.stderr) - continue - - data['measurement'].append(measurement) - data['time'].append(float(time)) - data['unit'].append(unit) - data['log_file'].append(file_path.split('/')[-1]) - return pd.DataFrame(data) - -def create_histograms(df, measurement): - filtered_df = df[df['measurement'] == measurement] - plt.figure(figsize=(12, 6)) - sns.histplot(data=filtered_df, x='time_ms', hue='log_file', element='step', stat='density', common_norm=False, palette='bright') - plt.title(f'Histogram of {measurement}') - plt.xlabel('Time (ms)') - plt.ylabel('Density') - plt.grid(True) - plt.xlim(filtered_df['time_ms'].quantile(0.01), filtered_df['time_ms'].quantile(0.99)) - plt.show() - - -file_paths = sys.argv[1:] -dfs = [parse_log_file(path) for path in file_paths] -combined_df = pd.concat(dfs, ignore_index=True) -combined_df['time_ms'] = combined_df.apply(lambda row: row['time'] if row['unit'] == 'ms' else row['time'] / 1000, axis=1) -combined_df.drop(['time', 'unit'], axis=1, inplace=True) - -measurement_types = combined_df['measurement'].unique() -for measurement in measurement_types: - create_histograms(combined_df, measurement) diff --git a/script/import-themes b/script/import-themes deleted file mode 100755 index 14b844abc8..0000000000 --- a/script/import-themes +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -cargo run -p theme_importer -- "$@" diff --git a/script/install-cmake b/script/install-cmake deleted file mode 100755 index 3a28aae1b8..0000000000 --- a/script/install-cmake +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash -# -# This script installs an up-to-date version of CMake. -# -# For MacOS use Homebrew to install the latest version. -# -# For Ubuntu use the official KitWare Apt repository with backports. -# See: https://apt.kitware.com/ -# -# For other systems (RHEL 8.x, 9.x, AmazonLinux, SUSE, Fedora, Arch, etc) -# use the official CMake installer script from KitWare. -# -# Note this is similar to how GitHub Actions runners install cmake: -# https://github.com/actions/runner-images/blob/main/images/ubuntu/scripts/build/install-cmake.sh -# -# Upstream: 3.30.4 (2024-09-27) - -set -euo pipefail - - -if [[ "$(uname -s)" == "darwin" ]]; then - brew --version >/dev/null \ - || echo "Error: Homebrew is required to install cmake on MacOS." && exit 1 - echo "Installing cmake via Homebrew (can't pin to old versions)." - brew install cmake - exit 0 -elif [ "$(uname -s)" != "Linux" ]; then - echo "Error: This script is intended for MacOS/Linux systems only." - exit 1 -elif [ -z "${1:-}" ]; then - echo "Usage: $0 [3.30.4]" - exit 1 -fi -CMAKE_VERSION="${CMAKE_VERSION:-${1:-3.30.4}}" - -if [ "$(whoami)" = root ]; then SUDO=; else SUDO="$(command -v sudo || command -v doas || true)"; fi - -if cmake --version 2>/dev/null | grep -q "$CMAKE_VERSION"; then - echo "CMake $CMAKE_VERSION is already installed." - exit 0 -elif [ -e /usr/local/bin/cmake ]; then - echo "Warning: existing cmake found at /usr/local/bin/cmake. Skipping installation." - exit 0 -elif [ -e /etc/apt/sources.list.d/kitware.list ]; then - echo "Warning: existing KitWare repository found. Skipping installation." - exit 0 -elif [ -e /etc/lsb-release ] && grep -qP 'DISTRIB_ID=Ubuntu' /etc/lsb-release; then - curl -fsSL https://apt.kitware.com/keys/kitware-archive-latest.asc \ - | $SUDO gpg --dearmor - \ - | $SUDO tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null - echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $(lsb_release -cs) main" \ - | $SUDO tee /etc/apt/sources.list.d/kitware.list >/dev/null - $SUDO apt-get update - $SUDO apt-get install -y kitware-archive-keyring cmake -else - arch="$(uname -m)" - if [ "$arch" != "x86_64" ] && [ "$arch" != "aarch64" ]; then - echo "Error. Only x86_64 and aarch64 are supported." - exit 1 - fi - tempdir=$(mktemp -d) - pushd "$tempdir" - CMAKE_REPO="https://github.com/Kitware/CMake" - CMAKE_INSTALLER="cmake-$CMAKE_VERSION-linux-$arch.sh" - curl -fsSL --output cmake-$CMAKE_VERSION-SHA-256.txt \ - "$CMAKE_REPO/releases/download/v$CMAKE_VERSION/cmake-$CMAKE_VERSION-SHA-256.txt" - curl -fsSL --output $CMAKE_INSTALLER \ - "$CMAKE_REPO/releases/download/v$CMAKE_VERSION/cmake-$CMAKE_VERSION-linux-$arch.sh" - # workaround for old versions of sha256sum not having --ignore-missing - grep -F "cmake-$CMAKE_VERSION-linux-$arch.sh" "cmake-$CMAKE_VERSION-SHA-256.txt" \ - | sha256sum -c \ - | grep -qP "^${CMAKE_INSTALLER}: OK" - chmod +x cmake-$CMAKE_VERSION-linux-$arch.sh - $SUDO ./cmake-$CMAKE_VERSION-linux-$arch.sh --prefix=/usr/local --skip-license - popd - rm -rf "$tempdir" -fi diff --git a/script/install-linux b/script/install-linux deleted file mode 100755 index a642ed2e0e..0000000000 --- a/script/install-linux +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash - -set -euxo pipefail - -if [[ $# -gt 0 ]]; then - echo " - Usage: ${0##*/} - Builds and installs zed onto your system into ~/.local, making it available as ~/.local/bin/zed. - - Before running this you should ensure you have all the build dependencies installed with `./script/linux`. - " - exit 1 -fi -export ZED_CHANNEL=$(/dev/null 2>&1 && wild --version | grep -Fq "$WILD_VERSION" ; then - echo "Warning: existing wild $WILD_VERSION found at $(command -v wild). Skipping installation." - exit 0 -fi - -if [ "$(whoami)" = root ]; then SUDO=; else SUDO="$(command -v sudo || command -v doas || true)"; fi - -ARCH="$(uname -m)" -WILD_REPO="${WILD_REPO:-https://github.com/davidlattimore/wild}" -WILD_PACKAGE="wild-linker-${WILD_VERSION}-${ARCH}-unknown-linux-gnu" -WILD_URL="${WILD_URL:-$WILD_REPO}/releases/download/$WILD_VERSION/${WILD_PACKAGE}.tar.gz" -DEST_DIR=/usr/local/bin - -echo "Downloading from $WILD_URL" -curl -fsSL --output - "$WILD_URL" \ - | $SUDO tar -C ${DEST_DIR} --strip-components=1 --no-overwrite-dir -xzf - \ - "${WILD_PACKAGE}/wild" - -cat </dev/null 2>&1; then - curl () { - command curl -fL "$@" - } - elif command -v wget >/dev/null 2>&1; then - curl () { - wget -O- "$@" - } - else - echo "Could not find 'curl' or 'wget' in your path" - exit 1 - fi - - "$platform" "$@" - - if [ "$(command -v zed)" = "$HOME/.local/bin/zed" ]; then - echo "Zed has been installed. Run with 'zed'" - else - echo "To run Zed from your terminal, you must add ~/.local/bin to your PATH" - echo "Run:" - - case "$SHELL" in - *zsh) - echo " echo 'export PATH=\$HOME/.local/bin:\$PATH' >> ~/.zshrc" - echo " source ~/.zshrc" - ;; - *fish) - echo " fish_add_path -U $HOME/.local/bin" - ;; - *) - echo " echo 'export PATH=\$HOME/.local/bin:\$PATH' >> ~/.bashrc" - echo " source ~/.bashrc" - ;; - esac - - echo "To run Zed now, '~/.local/bin/zed'" - fi -} - -linux() { - if [ -n "${ZED_BUNDLE_PATH:-}" ]; then - cp "$ZED_BUNDLE_PATH" "$temp/zed-linux-$arch.tar.gz" - else - echo "Downloading Zed" - curl "https://cloud.zed.dev/releases/$channel/latest/download?asset=zed&arch=$arch&os=linux&source=install.sh" > "$temp/zed-linux-$arch.tar.gz" - fi - - suffix="" - if [ "$channel" != "stable" ]; then - suffix="-$channel" - fi - - appid="" - case "$channel" in - stable) - appid="dev.zed.Zed" - ;; - nightly) - appid="dev.zed.Zed-Nightly" - ;; - preview) - appid="dev.zed.Zed-Preview" - ;; - dev) - appid="dev.zed.Zed-Dev" - ;; - *) - echo "Unknown release channel: ${channel}. Using stable app ID." - appid="dev.zed.Zed" - ;; - esac - - # Unpack - rm -rf "$HOME/.local/zed$suffix.app" - mkdir -p "$HOME/.local/zed$suffix.app" - tar -xzf "$temp/zed-linux-$arch.tar.gz" -C "$HOME/.local/" - - # Setup ~/.local directories - mkdir -p "$HOME/.local/bin" "$HOME/.local/share/applications" - - # Link the binary - if [ -f "$HOME/.local/zed$suffix.app/bin/zed" ]; then - ln -sf "$HOME/.local/zed$suffix.app/bin/zed" "$HOME/.local/bin/zed" - else - # support for versions before 0.139.x. - ln -sf "$HOME/.local/zed$suffix.app/bin/cli" "$HOME/.local/bin/zed" - fi - - # Copy .desktop file - desktop_file_path="$HOME/.local/share/applications/${appid}.desktop" - cp "$HOME/.local/zed$suffix.app/share/applications/zed$suffix.desktop" "${desktop_file_path}" - sed -i "s|Icon=zed|Icon=$HOME/.local/zed$suffix.app/share/icons/hicolor/512x512/apps/zed.png|g" "${desktop_file_path}" - sed -i "s|Exec=zed|Exec=$HOME/.local/zed$suffix.app/bin/zed|g" "${desktop_file_path}" -} - -macos() { - echo "Downloading Zed" - curl "https://cloud.zed.dev/releases/$channel/latest/download?asset=zed&os=macos&arch=$arch&source=install.sh" > "$temp/Zed-$arch.dmg" - hdiutil attach -quiet "$temp/Zed-$arch.dmg" -mountpoint "$temp/mount" - app="$(cd "$temp/mount/"; echo *.app)" - echo "Installing $app" - if [ -d "/Applications/$app" ]; then - echo "Removing existing $app" - rm -rf "/Applications/$app" - fi - ditto "$temp/mount/$app" "/Applications/$app" - hdiutil detach -quiet "$temp/mount" - - mkdir -p "$HOME/.local/bin" - # Link the binary - ln -sf "/Applications/$app/Contents/MacOS/cli" "$HOME/.local/bin/zed" -} - -main "$@" diff --git a/script/kube-shell b/script/kube-shell deleted file mode 100755 index 67f9fc2a6b..0000000000 --- a/script/kube-shell +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -if [[ $# -ne 1 ]]; then - echo "Usage: $0 [production|staging|...]" - exit 1 -fi - -export ZED_KUBE_NAMESPACE=$1 - -pod=$(kubectl --namespace=${ZED_KUBE_NAMESPACE} get pods --selector=app=zed --output=jsonpath='{.items[*].metadata.name}') -exec kubectl --namespace $ZED_KUBE_NAMESPACE exec --tty --stdin $pod -- /bin/bash diff --git a/script/language-extension-version b/script/language-extension-version deleted file mode 100755 index 119021e566..0000000000 --- a/script/language-extension-version +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash - -set -euox pipefail - -if [ "$#" -lt 1 ]; then - echo "Usage: $0 [version]" - exit 1 -fi - -LANGUAGE=$1 -VERSION=${2:-} - -EXTENSION_DIR="extensions/$LANGUAGE" -EXTENSION_TOML="$EXTENSION_DIR/extension.toml" -CARGO_TOML="$EXTENSION_DIR/Cargo.toml" - -if [ ! -d "$EXTENSION_DIR" ]; then - echo "Directory $EXTENSION_DIR does not exist." - exit 1 -fi - -if [ -z "$VERSION" ]; then - grep -m 1 'version =' "$EXTENSION_TOML" | awk -F\" '{print $2}' - exit 0 -fi - -sed -i '' -e "s/^version = \".*\"/version = \"$VERSION\"/" "$EXTENSION_TOML" -sed -i '' -e "s/^version = \".*\"/version = \"$VERSION\"/" "$CARGO_TOML" -cargo update --workspace diff --git a/script/lib/blob-store.ps1 b/script/lib/blob-store.ps1 deleted file mode 100644 index 38bf1682d2..0000000000 --- a/script/lib/blob-store.ps1 +++ /dev/null @@ -1,68 +0,0 @@ -function UploadToBlobStoreWithACL { - param ( - [string]$BucketName, - [string]$FileToUpload, - [string]$BlobStoreKey, - [string]$ACL - ) - - # Format date to match AWS requirements - $Date = (Get-Date).ToUniversalTime().ToString("r") - # Note: Original script had a bug where it overrode the ACL parameter - # I'm keeping the same behavior for compatibility - $ACL = "public-read" - $ContentType = "application/octet-stream" - $StorageClass = "STANDARD" - - # Create string to sign (AWS S3 compatible format) - $StringToSign = "PUT`n`n${ContentType}`n${Date}`nx-amz-acl:${ACL}`nx-amz-storage-class:${StorageClass}`n/${BucketName}/${BlobStoreKey}" - - # Generate HMAC-SHA1 signature - $HMACSHA1 = New-Object System.Security.Cryptography.HMACSHA1 - $HMACSHA1.Key = [System.Text.Encoding]::UTF8.GetBytes($env:DIGITALOCEAN_SPACES_SECRET_KEY) - $Signature = [System.Convert]::ToBase64String($HMACSHA1.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($StringToSign))) - - # Upload file using Invoke-WebRequest (equivalent to curl) - $Headers = @{ - "Host" = "${BucketName}.nyc3.digitaloceanspaces.com" - "Date" = $Date - "Content-Type" = $ContentType - "x-amz-storage-class" = $StorageClass - "x-amz-acl" = $ACL - "Authorization" = "AWS ${env:DIGITALOCEAN_SPACES_ACCESS_KEY}:$Signature" - } - - $Uri = "https://${BucketName}.nyc3.digitaloceanspaces.com/${BlobStoreKey}" - - # Read file content - $FileContent = Get-Content $FileToUpload -Raw -AsByteStream - - try { - Invoke-WebRequest -Uri $Uri -Method PUT -Headers $Headers -Body $FileContent -ContentType $ContentType -Verbose - Write-Host "Successfully uploaded $FileToUpload to $Uri" -ForegroundColor Green - } - catch { - Write-Error "Failed to upload file: $_" - throw $_ - } -} - -function UploadToBlobStorePublic { - param ( - [string]$BucketName, - [string]$FileToUpload, - [string]$BlobStoreKey - ) - - UploadToBlobStoreWithACL -BucketName $BucketName -FileToUpload $FileToUpload -BlobStoreKey $BlobStoreKey -ACL "public-read" -} - -function UploadToBlobStore { - param ( - [string]$BucketName, - [string]$FileToUpload, - [string]$BlobStoreKey - ) - - UploadToBlobStoreWithACL -BucketName $BucketName -FileToUpload $FileToUpload -BlobStoreKey $BlobStoreKey -ACL "private" -} diff --git a/script/lib/blob-store.sh b/script/lib/blob-store.sh deleted file mode 100644 index 8a119bc826..0000000000 --- a/script/lib/blob-store.sh +++ /dev/null @@ -1,32 +0,0 @@ -function upload_to_blob_store_with_acl -{ - bucket_name="$1" - file_to_upload="$2" - blob_store_key="$3" - acl="$4" - - date=$(date +"%a, %d %b %Y %T %z") - content_type="application/octet-stream" - storage_type="x-amz-storage-class:STANDARD" - string="PUT\n\n${content_type}\n${date}\n${acl}\n${storage_type}\n/${bucket_name}/${blob_store_key}" - signature=$(echo -en "${string}" | openssl sha1 -hmac "${DIGITALOCEAN_SPACES_SECRET_KEY}" -binary | base64) - - curl --fail -vv -s -X PUT -T "$file_to_upload" \ - -H "Host: ${bucket_name}.nyc3.digitaloceanspaces.com" \ - -H "Date: $date" \ - -H "Content-Type: $content_type" \ - -H "$storage_type" \ - -H "$acl" \ - -H "Authorization: AWS ${DIGITALOCEAN_SPACES_ACCESS_KEY}:$signature" \ - "https://${bucket_name}.nyc3.digitaloceanspaces.com/${blob_store_key}" -} - -function upload_to_blob_store_public -{ - upload_to_blob_store_with_acl "$1" "$2" "$3" "x-amz-acl:public-read" -} - -function upload_to_blob_store -{ - upload_to_blob_store_with_acl "$1" "$2" "$3" "x-amz-acl:private" -} diff --git a/script/lib/bump-version.sh b/script/lib/bump-version.sh deleted file mode 100755 index bfe3e29202..0000000000 --- a/script/lib/bump-version.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash - -set -eu - -package=$1 -tag_prefix=$2 -tag_suffix=$3 -version_increment=$4 -gpui_release=${5:-false} - -if [[ -n $(git status --short --untracked-files=no) ]]; then - echo "can't bump version with uncommitted changes" - exit 1 -fi - -which cargo-set-version > /dev/null || cargo install cargo-edit -which jq > /dev/null || brew install jq -cargo set-version --package $package --bump $version_increment -cargo check --quiet - -new_version=$(script/get-crate-version $package) -branch_name=$(git rev-parse --abbrev-ref HEAD) -old_sha=$(git rev-parse HEAD) -tag_name=${tag_prefix}${new_version}${tag_suffix} - -git commit --quiet --all --message "${package} ${new_version}" -git tag ${tag_name} - -if [[ "$gpui_release" == "true" ]]; then -cat <&2 - exit 1 - fi - export $(grep -v '^#' $env_file | grep -v '^[[:space:]]*$') -} - -function target_zed_kube_cluster { - if [[ $(kubectl config current-context 2> /dev/null) != do-nyc1-zed-1 ]]; then - doctl kubernetes cluster kubeconfig save zed-1 - fi -} - -function tag_for_environment { - if [[ "$1" == "production" ]]; then - echo "collab-production" - elif [[ "$1" == "staging" ]]; then - echo "collab-staging" - else - echo "Invalid environment name '${environment}'" >&2 - exit 1 - fi -} - -function url_for_environment { - if [[ "$1" == "production" ]]; then - echo "https://collab.zed.dev" - elif [[ "$1" == "staging" ]]; then - echo "https://collab-staging.zed.dev" - else - echo "Invalid environment name '${environment}'" >&2 - exit 1 - fi -} diff --git a/script/lib/squawk.toml b/script/lib/squawk.toml deleted file mode 100644 index 83a238c4ff..0000000000 --- a/script/lib/squawk.toml +++ /dev/null @@ -1,11 +0,0 @@ -excluded_rules = [ - # We use `serial` already, no point changing now. - "prefer-identity", - - # We store timestamps in UTC, so we don't care about the timezone. - "prefer-timestamptz", - - "prefer-big-int", - "prefer-bigint-over-int", -] -pg_version = "15.0" diff --git a/script/lib/workspace.ps1 b/script/lib/workspace.ps1 deleted file mode 100644 index c6fdc274c1..0000000000 --- a/script/lib/workspace.ps1 +++ /dev/null @@ -1,6 +0,0 @@ - -function ParseZedWorkspace { - $metadata = cargo metadata --no-deps --offline | ConvertFrom-Json - $env:ZED_WORKSPACE = $metadata.workspace_root - $env:RELEASE_VERSION = $metadata.packages | Where-Object { $_.name -eq "zed" } | Select-Object -ExpandProperty version -} diff --git a/script/licenses/template.csv.hbs b/script/licenses/template.csv.hbs deleted file mode 100644 index 1459aa648d..0000000000 --- a/script/licenses/template.csv.hbs +++ /dev/null @@ -1,6 +0,0 @@ -Crate Name,Crate Version,License,Url -{{#each licenses}} -{{#each used_by}} -{{crate.name}},{{crate.version}},{{../name}},{{#if crate.repository}}{{crate.repository}}{{else}}https://crates.io/crates/{{crate.name}}{{/if}} -{{/each}} -{{/each}} diff --git a/script/licenses/template.md.hbs b/script/licenses/template.md.hbs deleted file mode 100644 index f37761f2c5..0000000000 --- a/script/licenses/template.md.hbs +++ /dev/null @@ -1,51 +0,0 @@ -## Overview of licenses: - -{{#each overview}} -* {{name}} ({{count}}) -{{/each}} - -### All license texts: -{{#each licenses}} - -#### {{name}} - -##### Used by: - -{{#each used_by}} -* [{{crate.name}} {{crate.version}}]({{#if crate.repository}} {{crate.repository}} {{else}} https://crates.io/crates/{{crate.name}} {{/if}}) -{{/each}} - -{{text}} - --------------------------------------------------------------------------------- -{{/each}} - -#### MIT License - -##### Used by: - -* [Windows Terminal]( https://github.com/microsoft/terminal ) - -Copyright (c) Microsoft Corporation. All rights reserved. - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - --------------------------------------------------------------------------------- diff --git a/script/licenses/zed-licenses.toml b/script/licenses/zed-licenses.toml deleted file mode 100644 index 572dd5c14a..0000000000 --- a/script/licenses/zed-licenses.toml +++ /dev/null @@ -1,47 +0,0 @@ -no-clearly-defined = true -private = { ignore = true } -# Licenses allowed in Zed's dependencies. AGPL should not be added to -# this list as use of AGPL software is sometimes disallowed. When -# adding to this list, please check the following open source license -# policies: -# -# * https://opensource.google/documentation/reference/thirdparty/licenses -# -# The Zed project does have AGPL crates, but these are only involved -# in servers and are not built into the binaries in the release -# tarball. `script/check-licenses` checks that AGPL crates are not -# involved in release binaries. -accepted = [ - "Apache-2.0", - "MIT", - "MIT-0", - "Apache-2.0 WITH LLVM-exception", - "MPL-2.0", - "BSD-3-Clause", - "BSD-2-Clause", - "ISC", - "CC0-1.0", - "NCSA", - "Unicode-3.0", - "OpenSSL", - "Zlib", - "BSL-1.0", -] - -[procinfo.clarify] -license = "MIT" -[[procinfo.clarify.files]] -path = 'LICENSE.md' -checksum = '37db33bbbd7348969eda397b89a16f252d56c1ca7481b6ccaf56ccdcbab5dcca' - -[webpki.clarify] -license = "ISC" # It actually says 'ISC-style' but I don't know the SPDX expression for that. -[[webpki.clarify.files]] -path = 'LICENSE' -checksum = '5b698ca13897be3afdb7174256fa1574f8c6892b8bea1a66dd6469d3fe27885a' - -[fuchsia-cprng.clarify] -license = "BSD-3-Clause" -[[fuchsia-cprng.clarify.files]] -path = 'LICENSE' -checksum = '03b114f53e6587a398931762ee11e2395bfdba252a329940e2c8c9e81813845b' diff --git a/script/mitm-proxy.sh b/script/mitm-proxy.sh deleted file mode 100755 index 86b151125d..0000000000 --- a/script/mitm-proxy.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -if command -v docker >/dev/null 2>&1; then - ENGINE="docker" -elif command -v podman >/dev/null 2>&1; then - ENGINE="podman" -else - echo "Neither Docker nor Podman found. Please install one of them." - exit 1 -fi -if [ ! -d ~/.mitmproxy ]; then - mkdir -p ~/.mitmproxy -fi - -CONTAINER_ID="$(${ENGINE} run -d --rm -it -v ~/.mitmproxy:/home/mitmproxy/.mitmproxy -p 9876:8080 mitmproxy/mitmproxy mitmdump)" - -trap "${ENGINE} stop \"$CONTAINER_ID\" 1> /dev/null || true; exit 1" SIGINT - -echo "Add the root certificate created in ~/.mitmproxy to your certificate chain for HTTP" -echo "on macOS:" -echo "sudo security add-trusted-cert -d -p ssl -p basic -k /Library/Keychains/System.keychain ~/.mitmproxy/mitmproxy-ca-cert.pem" -echo "Press enter to continue" -read - -http_proxy=http://localhost:9876 cargo run - -# Clean up detached proxy after running -${ENGINE} stop "${CONTAINER_ID}" 2>/dev/null || true diff --git a/script/new-crate b/script/new-crate deleted file mode 100755 index 52ee900b30..0000000000 --- a/script/new-crate +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env bash - -# Try to make sure we are in the zed repo root -if [ ! -d "crates" ] || [ ! -d "script" ]; then - echo "Error: Run from the \`zed\` repo root" - exit 1 -fi - -if [ ! -f "Cargo.toml" ]; then - echo "Error: Run from the \`zed\` repo root" - exit 1 -fi - -if [ $# -eq 0 ]; then - echo "Usage: $0 [optional_license_flag]" - exit 1 -fi - -CRATE_NAME="$1" - -LICENSE_FLAG=$(echo "${2}" | tr '[:upper:]' '[:lower:]') -if [[ "$LICENSE_FLAG" == *"apache"* ]]; then - LICENSE_MODE="Apache-2.0" - LICENSE_FILE="LICENSE-APACHE" -elif [[ "$LICENSE_FLAG" == *"agpl"* ]]; then - LICENSE_MODE="AGPL-3.0-or-later" - LICENSE_FILE="LICENSE-AGPL" -else - LICENSE_MODE="GPL-3.0-or-later" - LICENSE_FILE="LICENSE-GPL" -fi - -if [[ ! "$CRATE_NAME" =~ ^[a-z0-9_]+$ ]]; then - echo "Error: Crate name must be lowercase and contain only alphanumeric characters and underscores" - exit 1 -fi - -CRATE_PATH="crates/$CRATE_NAME" -mkdir -p "$CRATE_PATH/src" - -# Symlink the license -ln -sf "../../$LICENSE_FILE" "$CRATE_PATH/$LICENSE_FILE" - -CARGO_TOML_TEMPLATE=$(cat << 'EOF' -[package] -name = "$CRATE_NAME" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "$LICENSE_MODE" - -[lints] -workspace = true - -[lib] -path = "src/$CRATE_NAME.rs" - -[features] -default = [] - -[dependencies] -anyhow.workspace = true -gpui.workspace = true -ui.workspace = true -util.workspace = true - -# Uncomment other workspace dependencies as needed -# assistant.workspace = true -# client.workspace = true -# project.workspace = true -# settings.workspace = true -EOF -) - -# Populate template -CARGO_TOML_CONTENT=$(echo "$CARGO_TOML_TEMPLATE" | sed \ - -e "s/\$CRATE_NAME/$CRATE_NAME/g" \ - -e "s/\$LICENSE_MODE/$LICENSE_MODE/g") - -echo "$CARGO_TOML_CONTENT" > "$CRATE_PATH/Cargo.toml" - -echo "//! # $CRATE_NAME" > "$CRATE_PATH/src/$CRATE_NAME.rs" - -echo "Created new crate: $CRATE_NAME in $CRATE_PATH" -echo "License: $LICENSE_MODE (symlinked from $LICENSE_FILE)" -echo "Don't forget to add the new crate to the workspace!" diff --git a/script/prettier b/script/prettier deleted file mode 100755 index 5ad5d15cf0..0000000000 --- a/script/prettier +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -set -euxo pipefail - -PRETTIER_VERSION=3.5.0 - -pnpm dlx "prettier@${PRETTIER_VERSION}" assets/settings/default.json --parser=jsonc --check || { - echo "To fix, run from the root of the Zed repo:" - echo " pnpm dlx prettier@${PRETTIER_VERSION} assets/settings/default.json --parser=jsonc --write" - false -} - -cd docs -pnpm dlx "prettier@${PRETTIER_VERSION}" . --check || { - echo "To fix, run from the root of the Zed repo:" - echo " cd docs && pnpm dlx prettier@${PRETTIER_VERSION} . --write && cd .." - false -} diff --git a/script/prompts b/script/prompts deleted file mode 100755 index 5486600c1e..0000000000 --- a/script/prompts +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash - -# This script manages prompt overrides for the Zed editor. -# -# It provides functionality to: -# 1. Link the current repository's prompt templates to Zed's configuration. -# 2. Create and link a separate Git worktree for prompt management. -# 3. Unlink previously linked prompt overrides. -# -# Usage: -# ./script_name.sh link # Link current repo's prompts -# ./script_name.sh link --worktree # Create and link a separate worktree -# ./script_name.sh unlink # Remove existing prompt override link -# -# The script ensures proper Git branch and worktree setup when using the -# --worktree option. It also provides informative output and error handling. - -if [ "$1" = "link" ]; then - # Remove existing link (or directory) - rm -rf ~/.config/zed/prompt_overrides - if [ "$2" = "--worktree" ]; then - # Check if 'prompts' branch exists, create if not - if ! git show-ref --quiet refs/heads/prompts; then - git branch prompts - fi - # Check if 'prompts' worktree exists - if git worktree list | grep -q "../zed_prompts"; then - echo "Worktree already exists at ../zed_prompts." - else - # Create worktree if it doesn't exist - git worktree add ../zed_prompts prompts || git worktree add ../zed_prompts -b prompts - fi - ln -sf "$(realpath "$(pwd)/../zed_prompts/assets/prompts")" ~/.config/zed/prompt_overrides - echo "Linked $(realpath "$(pwd)/../zed_prompts/assets/prompts") to ~/.config/zed/prompt_overrides" - echo -e "\033[0;33mDon't forget you have it linked, or your prompts will go stale\033[0m" - else - ln -sf "$(pwd)/assets/prompts" ~/.config/zed/prompt_overrides - echo "Linked $(pwd)/assets/prompts to ~/.config/zed/prompt_overrides" - fi -elif [ "$1" = "unlink" ]; then - if [ -e ~/.config/zed/prompt_overrides ]; then - # Remove symbolic link - rm -rf ~/.config/zed/prompt_overrides - echo "Unlinked ~/.config/zed/prompt_overrides" - else - echo -e "\033[33mWarning: No file exists at ~/.config/zed/prompt_overrides\033[0m" - fi -else - echo "This script helps you manage prompt overrides for Zed." - echo "You can link this directory to have Zed use the contents of your current repo templates as your active prompts," - echo "or store your modifications in a separate Git worktree." - echo - echo "Usage: $0 [link [--worktree]|unlink]" - echo - echo "Options:" - echo " link Create a symbolic link from ./assets/prompts to ~/.config/zed/prompt_overrides" - echo " link --worktree Create a 'prompts' Git worktree in ../prompts, then link ../prompts/assets/prompts" - echo " to ~/.config/zed/prompt_overrides" - echo " unlink Remove the symbolic link at ~/.config/zed/prompt_overrides" - exit 1 -fi diff --git a/script/randomized-test-ci b/script/randomized-test-ci deleted file mode 100755 index 36eab40a7a..0000000000 --- a/script/randomized-test-ci +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env node --redirect-warnings=/dev/null - -const fs = require("fs"); -const { randomBytes } = require("crypto"); -const { execFileSync } = require("child_process"); -const { - minimizeTestPlan, - buildTests, - runTests, -} = require("./randomized-test-minimize"); - -const { ZED_SERVER_URL } = process.env; -if (!ZED_SERVER_URL) throw new Error("Missing env var `ZED_SERVER_URL`"); - -main(); - -async function main() { - buildTests(); - - const seed = randomU64(); - const commit = execFileSync("git", ["rev-parse", "HEAD"], { - encoding: "utf8", - }).trim(); - - console.log("commit:", commit); - console.log("starting seed:", seed); - - const planPath = "target/test-plan.json"; - const minPlanPath = "target/test-plan.min.json"; - const failingSeed = runTests({ - SEED: seed, - SAVE_PLAN: planPath, - ITERATIONS: 50000, - OPERATIONS: 200, - }); - - if (!failingSeed) { - console.log("tests passed"); - return; - } - - console.log("found failure at seed", failingSeed); - const minimizedSeed = minimizeTestPlan(planPath, minPlanPath); - const minimizedPlan = fs.readFileSync(minPlanPath, "utf8"); - - console.log("minimized plan:\n", minimizedPlan); - - const url = `${ZED_SERVER_URL}/api/randomized_test_failure`; - const body = { - seed: minimizedSeed, - plan: JSON.parse(minimizedPlan), - commit: commit, - }; - await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); - - process.exit(1); -} - -function randomU64() { - const bytes = randomBytes(8); - const hexString = bytes.reduce( - (string, byte) => string + byte.toString(16), - "", - ); - return BigInt("0x" + hexString).toString(10); -} diff --git a/script/randomized-test-minimize b/script/randomized-test-minimize deleted file mode 100755 index efed3ee501..0000000000 --- a/script/randomized-test-minimize +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env node --redirect-warnings=/dev/null - -const fs = require("fs"); -const path = require("path"); -const { spawnSync } = require("child_process"); - -const FAILING_SEED_REGEX = /failing seed: (\d+)/gi; -const CARGO_TEST_ARGS = ["--release", "--lib", "--package", "collab"]; - -if (require.main === module) { - if (process.argv.length < 4) { - process.stderr.write( - "usage: script/randomized-test-minimize [start-index]\n", - ); - process.exit(1); - } - - minimizeTestPlan( - process.argv[2], - process.argv[3], - parseInt(process.argv[4]) || 0, - ); -} - -function minimizeTestPlan(inputPlanPath, outputPlanPath, startIndex = 0) { - const tempPlanPath = inputPlanPath + ".try"; - - fs.copyFileSync(inputPlanPath, outputPlanPath); - let testPlan = JSON.parse(fs.readFileSync(outputPlanPath, "utf8")); - - process.stderr.write("minimizing failing test plan...\n"); - for (let ix = startIndex; ix < testPlan.length; ix++) { - // Skip 'MutateClients' entries, since they themselves are not single operations. - if (testPlan[ix].MutateClients) { - continue; - } - - // Remove a row from the test plan - const newTestPlan = testPlan.slice(); - newTestPlan.splice(ix, 1); - fs.writeFileSync(tempPlanPath, serializeTestPlan(newTestPlan), "utf8"); - - process.stderr.write( - `${ix}/${testPlan.length}: ${JSON.stringify(testPlan[ix])}`, - ); - const failingSeed = runTests({ - SEED: "0", - LOAD_PLAN: tempPlanPath, - SAVE_PLAN: tempPlanPath, - ITERATIONS: "500", - }); - - // If the test failed, keep the test plan with the removed row. Reload the test - // plan from the JSON file, since the test itself will remove any operations - // which are no longer valid before saving the test plan. - if (failingSeed != null) { - process.stderr.write(` - remove. failing seed: ${failingSeed}.\n`); - fs.copyFileSync(tempPlanPath, outputPlanPath); - testPlan = JSON.parse(fs.readFileSync(outputPlanPath, "utf8")); - ix--; - } else { - process.stderr.write(` - keep.\n`); - } - } - - fs.unlinkSync(tempPlanPath); - - // Re-run the final minimized plan to get the correct failing seed. - // This is a workaround for the fact that the execution order can - // slightly change when replaying a test plan after it has been - // saved and loaded. - const failingSeed = runTests({ - SEED: "0", - ITERATIONS: "5000", - LOAD_PLAN: outputPlanPath, - }); - - process.stderr.write(`final test plan: ${outputPlanPath}\n`); - process.stderr.write(`final seed: ${failingSeed}\n`); - return failingSeed; -} - -function buildTests() { - const { status } = spawnSync( - "cargo", - ["test", "--no-run", ...CARGO_TEST_ARGS], - { - stdio: "inherit", - encoding: "utf8", - env: { - ...process.env, - }, - }, - ); - if (status !== 0) { - throw new Error("build failed"); - } -} - -function runTests(env) { - const { status, stdout } = spawnSync( - "cargo", - ["test", ...CARGO_TEST_ARGS, "random_project_collaboration"], - { - stdio: "pipe", - encoding: "utf8", - env: { - ...process.env, - ...env, - }, - }, - ); - - if (status !== 0) { - FAILING_SEED_REGEX.lastIndex = 0; - const match = FAILING_SEED_REGEX.exec(stdout); - if (!match) { - process.stderr.write("test failed, but no failing seed found:\n"); - process.stderr.write(stdout); - process.stderr.write("\n"); - process.exit(1); - } - return match[1]; - } else { - return null; - } -} - -function serializeTestPlan(plan) { - return "[\n" + plan.map((row) => JSON.stringify(row)).join(",\n") + "\n]\n"; -} - -exports.buildTests = buildTests; -exports.runTests = runTests; -exports.minimizeTestPlan = minimizeTestPlan; diff --git a/script/remote-server b/script/remote-server deleted file mode 100755 index 1ab2d943e1..0000000000 --- a/script/remote-server +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -set -xeuo pipefail - -# if root or if sudo/unavailable, define an empty variable -if [ "$(id -u)" -eq 0 ] -then maysudo='' -else maysudo="$(command -v sudo || command -v doas || true)" -fi - -deps=( - clang -) - -$maysudo apt-get update -$maysudo apt-get install -y "${deps[@]}" -exit 0 diff --git a/script/reset_db b/script/reset_db deleted file mode 100755 index 8b04cd6cd2..0000000000 --- a/script/reset_db +++ /dev/null @@ -1,3 +0,0 @@ -psql postgres -c "DROP DATABASE zed WITH (FORCE);" -psql postgres -c "DROP DATABASE zed_llm WITH (FORCE);" -script/bootstrap diff --git a/script/run-local-minio b/script/run-local-minio deleted file mode 100755 index 61266abb21..0000000000 --- a/script/run-local-minio +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -set -e - -if ! which minio > /dev/null; then - echo "minio not found - run script/bootstrap to install it and do other setup" - exit 1 -fi - -mkdir -p .blob_store/the-extensions-bucket -mkdir -p .blob_store/zed-crash-reports - -export MINIO_ROOT_USER=the-blob-store-access-key -export MINIO_ROOT_PASSWORD=the-blob-store-secret-key -exec minio server --quiet .blob_store diff --git a/script/run-unit-evals b/script/run-unit-evals deleted file mode 100755 index adbf965536..0000000000 --- a/script/run-unit-evals +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -set -euxo pipefail - -if [ -n "${UNIT_EVAL_COMMIT:-}" ]; then - git fetch origin "$UNIT_EVAL_COMMIT" && git checkout "$UNIT_EVAL_COMMIT" -fi - -GPUI_TEST_TIMEOUT=1500 cargo nextest run --workspace --no-fail-fast --features unit-eval --no-capture -E 'test(::eval_)' diff --git a/script/seed-db b/script/seed-db deleted file mode 100755 index 4ae8977d14..0000000000 --- a/script/seed-db +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash -set -e - -cargo run -p collab migrate diff --git a/script/setup-dev-driver.ps1 b/script/setup-dev-driver.ps1 deleted file mode 100644 index c5d39c5929..0000000000 --- a/script/setup-dev-driver.ps1 +++ /dev/null @@ -1,39 +0,0 @@ -# Configures a drive for testing in CI. - -# Currently, total CI requires almost 45GB of space, here we are creating a 100GB drive. -$Volume = New-VHD -Path C:/zed_dev_drive.vhdx -SizeBytes 100GB | - Mount-VHD -Passthru | - Initialize-Disk -Passthru | - New-Partition -AssignDriveLetter -UseMaximumSize | - Format-Volume -DevDrive -Confirm:$false -Force - -$Drive = "$($Volume.DriveLetter):" - -# Designate the Dev Drive as trusted -# See https://learn.microsoft.com/en-us/windows/dev-drive/#how-do-i-designate-a-dev-drive-as-trusted -fsutil devdrv trust $Drive - -# There is no virus on the Dev Drive! -# Windows Defender is the wolf in antivirus wool, slowing your PC like a digital fool! -# See https://learn.microsoft.com/en-us/windows/dev-drive/#how-do-i-configure-additional-filters-on-dev-drive -fsutil devdrv enable /disallowAv - -# Remount so the changes take effect -Dismount-VHD -Path C:/zed_dev_drive.vhdx -Mount-VHD -Path C:/zed_dev_drive.vhdx - -# Show some debug information -Write-Output $Volume -Write-Output "Using Dev Drive at $Drive" - -# Move Cargo to the dev drive -New-Item -Path "$($Drive)/.cargo/bin" -ItemType Directory -Force -Copy-Item -Path "C:/Users/runneradmin/.cargo/*" -Destination "$($Drive)/.cargo/" -Recurse -Force - -Write-Output ` - "DEV_DRIVE=$($Drive)" ` - "RUSTUP_HOME=$($Drive)/.rustup" ` - "CARGO_HOME=$($Drive)/.cargo" ` - "ZED_WORKSPACE=$($Drive)/zed" ` - "PATH=$($Drive)/.cargo/bin;$env:PATH" ` - >> $env:GITHUB_ENV diff --git a/script/shellcheck-scripts b/script/shellcheck-scripts deleted file mode 100755 index d42b31d02f..0000000000 --- a/script/shellcheck-scripts +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -mode=${1:-error} -[[ "$mode" =~ ^(error|warning)$ ]] || { echo "Usage: $0 [error|warning]"; exit 1; } - -cd "$(dirname "$0")/.." || exit 1 - -find script -maxdepth 1 -type f -print0 | - xargs -0 grep -l -E '^#!(/bin/|/usr/bin/env )(sh|bash|dash)' | - xargs -r shellcheck -x -S "$mode" -C diff --git a/script/snap-build b/script/snap-build deleted file mode 100755 index 62eae6f9de..0000000000 --- a/script/snap-build +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -set -euxo pipefail - -if [ "$#" -ne 1 ]; then - echo "Usage: $0 " - exit 1 -fi - -mkdir -p snap/gui - -export DO_STARTUP_NOTIFY="true" -export APP_NAME="Zed" -export APP_CLI="zed" -export APP_ICON="\${SNAP}/meta/gui/zed.png" -export APP_ARGS="%U" -envsubst < "crates/zed/resources/zed.desktop.in" > "snap/gui/zed.desktop" -cp "crates/zed/resources/app-icon.png" "snap/gui/zed.png" - -RELEASE_VERSION="$1" envsubst < crates/zed/resources/snap/snapcraft.yaml.in > snap/snapcraft.yaml - -# Clean seems to be needed to actually check that the snapcraft.yaml -# works. For example, when a `stage-package` is removed, it will -# still remain on rebuild. -snapcraft clean - -snapcraft diff --git a/script/snap-try b/script/snap-try deleted file mode 100755 index a262c0139a..0000000000 --- a/script/snap-try +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash - -# This script is intended to be run after `snap-build`. -# -# It expects a version to be passed as the first argument, and expects -# the built `.snap` for that version to be in the current directory. -# -# This will uninstall the current `zed` snap, replacing it with a snap -# that directly uses the `snap/unpacked` directory. - -set -euxo pipefail - -if [ "$#" -ne 1 ]; then - echo "Usage: $0 " - exit 1 -fi - -# Rerun as root -[ "$UID" -eq 0 ] || exec sudo bash -e "$0" "$@" - -snap remove zed || true -mkdir -p snap -rm -rf snap/unpacked -unsquashfs -dest snap/unpacked "zed_$1_amd64.snap" -snap try --classic snap/unpacked diff --git a/script/squawk b/script/squawk deleted file mode 100755 index 497fcff089..0000000000 --- a/script/squawk +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash - -# Squawk is a linter for database migrations. It helps identify dangerous patterns, and suggests alternatives. -# Squawk flagging an error does not mean that you need to take a different approach, but it does indicate you need to think about what you're doing. -# See also: https://squawkhq.com - -set -e - -if [ -z "$GITHUB_BASE_REF" ]; then - echo 'Not a pull request, skipping squawk modified migrations linting' - exit -fi - -SQUAWK_VERSION=0.26.0 -SQUAWK_BIN="./target/squawk-$SQUAWK_VERSION" -SQUAWK_ARGS="--assume-in-transaction --config script/lib/squawk.toml" - -pkgutil --pkg-info com.apple.pkg.RosettaUpdateAuto || /usr/sbin/softwareupdate --install-rosetta --agree-to-license -# When bootstrapping a brand new CI machine, the `target` directory may not exist yet. -mkdir -p "./target" -curl -L -o "$SQUAWK_BIN" "https://github.com/sbdchd/squawk/releases/download/v$SQUAWK_VERSION/squawk-darwin-x86_64" -chmod +x "$SQUAWK_BIN" - -if [ -n "$SQUAWK_GITHUB_TOKEN" ]; then - export SQUAWK_GITHUB_REPO_OWNER=$(echo $GITHUB_REPOSITORY | awk -F/ '{print $1}') - export SQUAWK_GITHUB_REPO_NAME=$(echo $GITHUB_REPOSITORY | awk -F/ '{print $2}') - export SQUAWK_GITHUB_PR_NUMBER=$(echo $GITHUB_REF | awk 'BEGIN { FS = "/" } ; { print $3 }') - - $SQUAWK_BIN $SQUAWK_ARGS upload-to-github $(git diff --name-only origin/$GITHUB_BASE_REF...origin/$GITHUB_HEAD_REF 'crates/collab/migrations/*.sql' 'crates/collab/migrations_llm/*.sql') -else - $SQUAWK_BIN $SQUAWK_ARGS $(git ls-files --others crates/collab/migrations/*.sql crates/collab/migrations_llm/*.sql) $(git diff --name-only main crates/collab/migrations/*.sql crates/collab/migrations_llm/*.sql) -fi diff --git a/script/storybook b/script/storybook deleted file mode 100755 index 20a81008d1..0000000000 --- a/script/storybook +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -if [ -z "$1" ]; then - cargo run -p storybook -else - cargo run -p storybook -- "components/$1" -fi diff --git a/script/terms/terms.json b/script/terms/terms.json deleted file mode 100644 index 9be983ba82..0000000000 --- a/script/terms/terms.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "body": [ - { - "lang": "en-US", - "type": "rtf", - "file": "terms.rtf" - } - ] -} diff --git a/script/terms/terms.rtf b/script/terms/terms.rtf deleted file mode 100644 index f5fab23f45..0000000000 --- a/script/terms/terms.rtf +++ /dev/null @@ -1,129 +0,0 @@ -{\rtf1\ansi\deff0{\fonttbl{\f0 \fswiss Helvetica;}{\f1 \fmodern Courier;}} -{\colortbl;\red255\green0\blue0;\red0\green0\blue255;} -\widowctrl\hyphauto - -{\pard \qc \f0 \sa180 \li0 \fi0 \b \fs36 Zed End User Terms\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \par} -{\pard \ql \f0 \sa180 \li0 \fi0 PLEASE READ THESE TERMS AND CONDITIONS CAREFULLY BEFORE USING THE SERVICE OR SOFTWARE OFFERED BY ZED INDUSTRIES, INC. ("ZED", OR "WE"). BY ACCESSING OR USING THE SOLUTION (AS DEFINED BELOW) IN ANY MANNER, YOU ("YOU" OR "CUSTOMER") AGREE TO BE BOUND BY THESE TERMS (THE "AGREEMENT") TO THE EXCLUSION OF ALL OTHER TERMS. YOU REPRESENT AND WARRANT THAT YOU HAVE THE AUTHORITY TO ENTER INTO THIS AGREEMENT; IF YOU ARE ENTERING INTO THIS AGREEMENT ON BEHALF OF AN ORGANIZATION OR ENTITY, REFERENCES TO "CUSTOMER" AND "YOU" IN THIS AGREEMENT, REFER TO THAT ORGANIZATION OR ENTITY. IF YOU DO NOT AGREE TO ALL OF THE FOLLOWING, YOU MAY NOT USE OR ACCESS THE SOLUTION IN ANY MANNER. IF THE TERMS OF THIS AGREEMENT ARE CONSIDERED AN OFFER, ACCEPTANCE IS EXPRESSLY LIMITED TO SUCH TERMS.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel1 \b \fs32 1. ACCESS TO AND USE OF THE SOLUTION\par} -{\pard \ql \f0 \sa180 \li0 \fi0 Subject to the terms and conditions of this Agreement, Zed hereby grants to You, and You hereby accept from Zed, a term-limited, non-exclusive, non-transferable, non-assignable and non-sublicensable license to make use of the Editor for Your internal use only, and subject to the use limitations in Section 2.2.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel1 \b \fs32 2. TERMS APPLICABLE TO THE EDITOR\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 2.1. License Grant\par} -{\pard \ql \f0 \sa180 \li0 \fi0 Subject to the terms and conditions of this Agreement, Zed hereby grants to You, and You hereby accept from Zed, a term-limited, non-exclusive, non-transferable, non-assignable and non-sublicensable license to make use of the Editor for Your internal use only, and subject to the use limitations in Section 2.2.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 2.2. License Limitations\par} -{\pard \ql \f0 \sa180 \li0 \fi0 You agree that You shall not: (a) exceed the scope of the licenses granted in Section 2.1; (b) make copies of the Editor; (c) distribute, sublicense, assign, delegate, rent, lease, sell, time-share or otherwise transfer the benefits of, use under, or rights to, the license granted in Section 2.1; (d) reverse engineer, decompile, disassemble or otherwise attempt to learn the source code, structure or algorithms underlying the Editor, except to the extent required to be permitted under applicable law; (e) modify, translate or create derivative works of the Editor; or (f) remove any copyright, trademark, patent or other proprietary notice that appears on the Editor or copies thereof.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 2.3. Open Source Software\par} -{\pard \ql \f0 \sa180 \li0 \fi0 Zed makes certain versions of the Editor and related software available at the Zed GitHub Repository: {\field{\*\fldinst{HYPERLINK "https://github.com/zed-industries/zed"}}{\fldrslt{\ul -https://github.com/zed-industries/zed -}}} - (the "Repo"). Your use of such software is subject to the open source software licenses declared in the Repo.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel1 \b \fs32 3. TERMS APPLICABLE TO THE ZED SERVICE\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 3.1. Access to and Scope of Zed Service\par} -{\pard \ql \f0 \sa180 \li0 \fi0 If you have elected to use the Zed Service by enabling or activating the Zed Service, Zed will use commercially reasonable efforts to make the Zed Service available to You as set forth in this Agreement. Once you elected to use the Zed Service, You may access and use the Zed Service during the Term, subject to Your compliance with the terms and conditions of the Agreement.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 3.2. Restrictions\par} -{\pard \ql \f0 \sa180 \li0 \fi0 You will use the Zed Service only in accordance with all applicable laws, including, but not limited to, laws related to data (whether applicable within the United States, the European Union, or otherwise). You agree not to (and will not allow any third party to): (i) remove or otherwise alter any proprietary notices or labels from the Zed Service or any portion thereof; (ii) reverse engineer, decompile, disassemble, or otherwise attempt to discover the underlying structure, ideas, or algorithms of the Zed Service or any software used to provide or make the Zed Service available; or (iii) rent, resell or otherwise allow any third party access to or use of the Zed Service. Zed may suspend Your access to or use of the Zed Service as follows: (a) immediately if Zed reasonably believes Your use of the Zed Service may pose a security risk to or may adversely impact the Zed Service; or (b) if You are in breach of this Agreement.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 3.3. Customer Data\par} -{\pard \ql \f0 \sa180 \li0 \fi0 You are solely responsible for Customer Data including, but not limited to: (a) compliance with all applicable laws and this Agreement; (b) any claims relating to Customer Data; and (c) any claims that Customer Data infringes, misappropriates, or otherwise violates the rights of any third party. You agree and acknowledge that Customer Data may be irretrievably deleted if Your account is terminated. For purposes of this Agreement, "Customer Data" shall mean any data, information or other material provided, uploaded, or submitted by You to the Zed Service in the course of using the Zed Service. Notwithstanding anything to the contrary, You represent and warrant that You will not transfer or make available to Zed any personally identifiable information or related information subject to applicable data privacy laws or regulations, unless otherwise agreed to in writing by Zed.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel3 \b \fs24 3.3.1. Customer Data Made Available to Zed\par} -{\pard \ql \f0 \sa180 \li0 \fi0 To the extent You elect to make Customer Data available to Zed, the same may only be used by Zed according to the Customer Data type and the use rights regarding the same as described herein:\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel3 \b \fs24 3.3.2. Usage Data\par} -{\pard \ql \f0 \sa180 \li0 \fi0 To improve the Editor and understand how You use it, Zed optionally collects the following usage data:\par} -{\pard \ql \f0 \sa0 \li720 \fi-360 \bullet \tx360\tab (a)\tx360\tab file extensions of opened files;\sa180\par} -{\pard \ql \f0 \sa0 \li720 \fi-360 \bullet \tx360\tab (b)\tx360\tab features and tools You use within the Editor;\sa180\par} -{\pard \ql \f0 \sa0 \li720 \fi-360 \bullet \tx360\tab (c)\tx360\tab project statistics (e.g., number of files); and\sa180\par} -{\pard \ql \f0 \sa0 \li720 \fi-360 \bullet \tx360\tab (d)\tx360\tab frameworks detected in Your projects\sa180\sa180\par} -{\pard \ql \f0 \sa180 \li0 \fi0 (a-d collectively, "Usage Data"). Usage Data does not include any of Your software code or sensitive project details. You may change Your preferences disabling the collection of Usage Data and You can audit Usage Data collected by the Editor at any time. See {\field{\*\fldinst{HYPERLINK "https://zed.dev/docs/telemetry"}}{\fldrslt{\ul -https://zed.dev/docs/telemetry -}}} - for more.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 Usage Data is associated with a secure random telemetry ID which may be linked to Your email address. This linkage currently serves two purposes: (1) it allows Zed to analyze usage patterns over time while maintaining Your privacy; and (2) it enables Zed to reach out to specific user groups for feedback and improvement suggestions. Zed may contact You based on Your usage patterns to better understand your needs and improve the Solution. If You delete Your account, the link between Your telemetry ID and Your email address will be permanently removed. By continuing to use Editor or Solution with this feature enabled You agree to this Usage Data collection.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel3 \b \fs24 3.3.3. Crash Reports\par} -{\pard \ql \f0 \sa180 \li0 \fi0 Customer Data consisting of data related to the behavior of the Solution prior to a crash or failure, such as stack traces are collected and classified as "Crash Reports". Zed will use commercially reasonable efforts to exclude any personally identifiable information from Crash Reports, but due to the nature of a crash, Zed does not ensure that information such as paths will be excluded from Crash Reports. Crash Reports will be used solely for Zed's internal purposes in connection with diagnosing defects in the Solution that led to the crash. You may grant us permission to capture Crash Reports when installing or activating the Solution, and You may change Your preferences at any time in the settings feature of the Solution. Once You grant us this permission, Zed will retain the Crash Reports indefinitely.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel3 \b \fs24 3.3.4. User Content\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \u8226 ? You may access, modify or create certain data or information in connection with your access or use of the Zed Editor or the Solution. Such data and information may include, but is not limited to any of the following:\par} -{\pard \ql \f0 \sa0 \li720 \fi-360 \bullet \tx360\tab (a)\tx360\tab file contents and associated metadata (e.g., filename, paths, size, timestamps);\sa180\par} -{\pard \ql \f0 \sa0 \li720 \fi-360 \bullet \tx360\tab (b)\tx360\tab source control history, comments and metadata (e.g., git history, commit messages);\sa180\par} -{\pard \ql \f0 \sa0 \li720 \fi-360 \bullet \tx360\tab (c)\tx360\tab configuration data (e.g., settings, keymaps);\sa180\par} -{\pard \ql \f0 \sa0 \li720 \fi-360 \bullet \tx360\tab (d)\tx360\tab anything typed, pasted and/or displayed on screen while using the Editor;\sa180\par} -{\pard \ql \f0 \sa0 \li720 \fi-360 \bullet \tx360\tab (e)\tx360\tab derivative works of the above generated by the Editor (e.g., format conversions, summaries, indexes, caches);\sa180\par} -{\pard \ql \f0 \sa0 \li720 \fi-360 \bullet \tx360\tab (f)\tx360\tab metadata, code and other derivative works of the above returned by language servers and other local tooling; and\sa180\par} -{\pard \ql \f0 \sa0 \li720 \fi-360 \bullet \tx360\tab (g)\tx360\tab metadata, code and other derivative works of the above returned by services integrated with the Zed Editor\sa180\sa180\par} -{\pard \ql \f0 \sa180 \li0 \fi0 (a-g collectively, "User Content").\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel3 \b \fs24 3.3.5. Handling of User Content\par} -{\pard \ql \f0 \sa180 \li0 \fi0 Zed will make use of or transfer User Content only as specified in this Agreement, or as necessary to comply with applicable law.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel3 \b \fs24 3.3.5.1. Zed Collaboration Services\par} -{\pard \ql \f0 \sa180 \li0 \fi0 When using Zed Collaboration Services, User Content is transmitted from Your environment only if You collaborate with other Zed users by electing to share a project in the Editor. Once You share a project, Zed may transmit User Content consisting of file paths, file contents, and metadata regarding the code returned by language servers. Currently, Zed does not persist any User Content beyond the Your collaboration session. If You unshare a project or disconnect from the Solution, all information associated with such project will be deleted from Zed servers. In the future, Zed may save User Content regarding projects beyond the scope of a single collaboration session. We may share such User Content with those users You elected to grant access to. Zed's access to such User Content is limited to debugging and making improvements to the Solution.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel3 \b \fs24 3.3.5.2. Other Services\par} -{\pard \ql \f0 \sa180 \li0 \fi0 The Zed Editor supports integration with API-based services maintained and not operated by Zed (the "Other Services"). By way of example, Other Services includes those made available by GitHub, Anthropic, OpenAI, and similar providers, or those You host or manage directly. You may configure the Zed Editor to interoperate, communicate with, and exchange data (including User Content) directly with the Other Services. Zed is not responsible or otherwise liable with respect to Your use of any Other Service, including but not limited to the exchange of data between the Other Service and the Zed Editor. The terms and conditions, including the applicable privacy policy, with respect to the Other Service are those made available by the applicable Other Service, not these Terms.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel3 \b \fs24 3.3.5.3. Zed AI Services\par} -{\pard \ql \f0 \sa180 \li0 \fi0 The Zed Editor supports integration with API-based services maintained and operated by Zed (the "Zed AI Services"). You may elect to use Zed AI Services as the provider for various Zed Editor features (e.g., Agent Panel, Inline Assistant, Edit Predictions, and similar features). In connection with Your use of these features, the Zed Editor and Zed AI Services may make use of User Content to generate contextually relevant responses (the \u8220"Output\u8221"). Other than as specified in Section 3.3.5.4 of these Terms, Zed will not use User Content for training of its models, or disclose User Content.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 Output is provided "as is" without any warranties or guarantees of functionality, security, or fitness for a particular purpose. While efforts are made to ensure the accuracy and reliability, Output may include errors, vulnerabilities, and defects. You are responsible for reviewing, testing, and validating Output before use in any production or critical environment. Zed assumes no liability for any damages, losses, or liability arising from the use, modification, reliance on, or deployment of Output. Any such use is at Your own risk.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel3 \b \fs24 3.3.5.4. Improvement Feedback\par} -{\pard \ql \f0 \sa180 \li0 \fi0 When using Zed AI Services to provide Edit Predictions in connection with certain open source software projects, You may elect to share requests, responses and feedback comments (collectively "Model Improvement Feedback") with Zed, and Zed may use the same to improve Zed Edit Predictions models. You may opt-out of sharing Model Improvement Feedback at any time.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 For more information on Zed Edit Predictions please see: {\field{\*\fldinst{HYPERLINK "https://zed.dev/docs/ai/ai-improvement"}}{\fldrslt{\ul -https://zed.dev/docs/ai/ai-improvement -}}} -\par} -{\pard \ql \f0 \sa180 \li0 \fi0 When using Zed AI Services in connection with the Agent Panel, You may elect to share with Zed requests, responses and feedback regarding the Agent Panel and related Output (the \u8220"Agent Improvement Feedback\u8221") with Zed, and Zed may use the same to improve the Agent Panel and related Output. Zed will only collect Agent Improvement Feedback when You elect to share the same.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 For more information regarding the Agent Panel please see: {\field{\*\fldinst{HYPERLINK "https://zed.dev/docs/ai/ai-improvement"}}{\fldrslt{\ul -https://zed.dev/docs/ai/ai-improvement -}}} -\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel3 \b \fs24 3.4. Privacy Policy\par} -{\pard \ql \f0 \sa180 \li0 \fi0 You and Zed are bound by the terms and conditions contained in the Zed Privacy Policy which is incorporated by reference hereto. The Zed Privacy Policy is available at the following URL: {\field{\*\fldinst{HYPERLINK "https://zed.dev/privacy-policy"}}{\fldrslt{\ul -https://zed.dev/privacy-policy -}}} -.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel1 \b \fs32 4. FEE BASED SERVICES, FEES AND PAYMENT TERMS\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 4.1. Fee Based Services\par} -{\pard \ql \f0 \sa180 \li0 \fi0 The Zed AI Services is made available with additional usage benefits (the \u8220"Enhanced Use \u8221") as described in the table published at {\field{\*\fldinst{HYPERLINK "https://zed.dev/pricing"}}{\fldrslt{\ul -zed.dev/pricing -}}} - (the \u8220"Pricing Table\u8221"), subject to the requirements and limitations set forth in the Pricing Table and these Terms. In order to make use of the Enhanced Use, Customer must access the Zed AI Services through a Zed registered account.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 4.2. Fees\par} -{\pard \ql \f0 \sa180 \li0 \fi0 Customer shall pay to Zed the applicable fees set forth in Pricing Table, together with any applicable taxes and shipping and handling (collectively, the \u8220"Fees\u8221"). Customer shall have no right of return, and all Fees shall be non-refundable.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 4.3. Payment Terms\par} -{\pard \ql \f0 \sa180 \li0 \fi0 All amounts payable to Zed under this Agreement shall be paid in United States dollars and paid Zed according to the method of payment, frequency and calculated as set forth in the Pricing Table.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 4.4. Taxes; Set-offs\par} -{\pard \ql \f0 \sa180 \li0 \fi0 Any and all payments made by Customer in accordance with this Agreement are exclusive of any taxes that might be assessed by any jurisdiction. Customer shall pay or reimburse Zed for all sales, use, property and similar taxes; all customs duties, import fees, stamp duties, license fees and similar charges; and all other mandatory payments to government agencies of whatever kind, except taxes imposed on the net or gross income of Zed. All amounts payable to Zed under this Agreement shall be without set-off and without deduction of any taxes, levies, imposts, charges, withholdings and/or duties of any nature which may be levied or imposed, including without limitation, value added tax, customs duty and withholding tax.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel1 \b \fs32 5. TERM AND TERMINATION\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 5.1. Term\par} -{\pard \ql \f0 \sa180 \li0 \fi0 The term of this Agreement shall commence on the date You first download the Editor or use the Zed Service (the "Effective Date"), and unless terminated earlier according to this Section 3, will end pursuant to this Section 5 (the "Term").\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 5.2. Termination\par} -{\pard \ql \f0 \sa180 \li0 \fi0 This Agreement may be terminated: (a) by either party if the other has materially breached this Agreement; or (b) by Zed at any time and for any reason upon notice to Customer. You acknowledge that Zed is under no obligation to continue to operate the Zed Service or make the Editor available, and We may end any programs in connection with the same at any time.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 5.3. Effect of Termination and Survival\par} -{\pard \ql \f0 \sa180 \li0 \fi0 Upon any expiration or termination of this Agreement, Customer shall (i) immediately cease use of the Zed Service, and (ii) return all Zed Confidential Information and other materials provided by Zed. The following provisions will survive termination of this Agreement: Sections 3.3 (Customer Data), Section 3.4 (Privacy Policy), Section 5.3 (Effect of Termination and Survival), Section 6 (Ownership), Section 7 (Indemnification), Section 9 (Limitation of Liability), Section 10 (Third Party Services), and Section 11 (Miscellaneous).\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel1 \b \fs32 6. OWNERSHIP\par} -{\pard \ql \f0 \sa180 \li0 \fi0 Zed retains all right, title, and interest in and to the Zed Service, Editor, and any software, products, works or other intellectual property created, used, provided, or made available by Zed under or in connection with the Zed Service or Editor. Customer may from time to time provide suggestions, comments, or other feedback to Zed with respect to the Zed Service or Editor ("Feedback"). Customer shall, and hereby does, grant to Zed a nonexclusive, worldwide, perpetual, irrevocable, transferable, sublicensable, royalty-free, fully paid-up license to use and exploit the Feedback for any purpose. You retain all right, title and interest in and to the Customer Data, including all intellectual property rights therein. No intellectual property rights with respect to any software code you develop or modify with the Editor or Zed Service (collectively, the \u8220"Output\u8221") are transferred or assigned to Zed hereunder.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel1 \b \fs32 7. INDEMNIFICATION\par} -{\pard \ql \f0 \sa180 \li0 \fi0 Customer will defend, indemnify, and hold Zed, its affiliates, suppliers and licensors harmless and each of their respective officers, directors, employees and representatives from and against any claims, damages, losses, liabilities, costs, and expenses (including reasonable attorneys' fees) arising out of or relating to any third party claim with respect to: (a) Customer Data; (b) breach of this Agreement or violation of applicable law by Customer; or (c) alleged infringement or misappropriation of third-party's intellectual property rights resulting from Customer Data.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel1 \b \fs32 8. WARRANTY\par} -{\pard \ql \f0 \sa180 \li0 \fi0 Zed does not represent or warrant that the operation of the Zed Service or Editor (or any portion thereof) will be uninterrupted or error free, or that the Zed Service or Editor (or any portion thereof) will operate in combination with other hardware, software, systems or data not provided by Zed. CUSTOMER ACKNOWLEDGES THAT, ZED MAKES NO EXPRESS OR IMPLIED REPRESENTATIONS OR WARRANTIES OF ANY KIND WITH RESPECT TO THE SERVICE OR SOFTWARE, OR THEIR CONDITION. ZED HEREBY EXPRESSLY EXCLUDES, ANY AND ALL OTHER EXPRESS OR IMPLIED REPRESENTATIONS OR WARRANTIES, WHETHER UNDER COMMON LAW, STATUTE OR OTHERWISE, INCLUDING WITHOUT LIMITATION ANY AND ALL WARRANTIES AS TO MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, SATISFACTORY QUALITY OR NON-INFRINGEMENT OF THIRD-PARTY RIGHTS.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel1 \b \fs32 9. LIMITATIONS OF LIABILITY\par} -{\pard \ql \f0 \sa180 \li0 \fi0 IN NO EVENT SHALL ZED BE LIABLE FOR ANY LOST DATA, LOST PROFITS, BUSINESS INTERRUPTION, REPLACEMENT SERVICE OR OTHER SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR INDIRECT DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THEORY OF LIABILITY. ZED'S LIABILITY FOR ALL CLAIMS ARISING UNDER THIS AGREEMENT, WHETHER IN CONTRACT, TORT OR OTHERWISE, SHALL NOT EXCEED THE GREATER OF: THE FEES PAID TO ZED BY CUSTOMER DURING THE TWELVE (12) MONTH PERIOD PRECEDING THE DATE OF THE CLAIM, OR ONE THOUSAND US DOLLARS ($1,000).\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel1 \b \fs32 10. Third Party Services\par} -{\pard \ql \f0 \sa180 \li0 \fi0 Zed may make certain third party services available to You within the Editor or the Zed Service (each a "Third Party Service"). You acknowledge and agree that (a) use of each Third Party Service is subject to the corresponding terms and conditions available at the following URL: {\field{\*\fldinst{HYPERLINK "https://zed.dev/third-party-terms"}}{\fldrslt{\ul -https://zed.dev/third-party-terms -}}} - and/or presented in connection with Your use of such Third Party Service; (b) the terms and conditions of this Agreement do not apply with respect to Your use of any Third Party Service; and (c) Zed is not liable in any way regarding Your use of any Third Party Service.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel1 \b \fs32 11. MISCELLANEOUS\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 11.1. Export Control\par} -{\pard \ql \f0 \sa180 \li0 \fi0 You hereby certify that You will comply with all current US Export Control laws. You agree to defend, indemnify and hold Zed harmless from any liability for Your violation of U.S. Export Control laws.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 11.2. Compliance with Laws\par} -{\pard \ql \f0 \sa180 \li0 \fi0 You shall comply with all applicable laws and regulations in its use of the Solution, including without limitation the unlawful gathering or collecting, or assisting in the gathering or collecting of information in violation of any privacy laws or regulations. You shall, at its own expense, defend, indemnify and hold harmless Zed from and against any and all claims, losses, liabilities, damages, judgments, government or federal sanctions, costs and expenses (including attorneys' fees) incurred by Zed arising from any claim or assertion by any third party of violation of privacy laws or regulations by You or any of its agents, officers, directors or employees.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 11.3. Assignment\par} -{\pard \ql \f0 \sa180 \li0 \fi0 Neither party may transfer and assign its rights and obligations under this Agreement without the prior written consent of the other party. Notwithstanding the foregoing, Zed may transfer and assign its rights under this Agreement without consent from the other party in connection with a change in control, acquisition or sale of all or substantially all of its assets.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 11.4. Force Majeure\par} -{\pard \ql \f0 \sa180 \li0 \fi0 Neither party shall be responsible for failure or delay in performance by events out of their reasonable control, including but not limited to, acts of God, Internet outage, terrorism, war, fires, earthquakes and other disasters (each a "Force Majeure"). Notwithstanding the foregoing: if a Force Majeure continues for more than thirty (30) days, either party may to terminate this agreement by written notice to the other party.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 11.5. Notice\par} -{\pard \ql \f0 \sa180 \li0 \fi0 All notices between the parties shall be in writing and shall be deemed to have been given if personally delivered or sent by registered or certified mail (return receipt), or by recognized courier service.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 11.6. No Agency\par} -{\pard \ql \f0 \sa180 \li0 \fi0 Both parties agree that no agency, partnership, joint venture, or employment is created as a result of this Agreement. You do not have any authority of any kind to bind Zed.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 11.7. Governing Law\par} -{\pard \ql \f0 \sa180 \li0 \fi0 This Agreement shall be governed exclusively by, and construed exclusively in accordance with, the laws of the United States and the State of California, without regard to its conflict of laws provisions. The federal courts of the United States in the Northern District of California and the state courts of the State of California shall have exclusive jurisdiction to adjudicate any dispute arising out of or relating to this Agreement. Each party hereby consents to the jurisdiction of such courts and waives any right it may otherwise have to challenge the appropriateness of such forums, whether on the basis of the doctrine of forum non conveniens or otherwise. The United Nations Convention on Contracts for the International Sale of Goods shall not apply to this Agreement or any Purchase Order issued under this Agreement.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 11.8. Updated Agreement\par} -{\pard \ql \f0 \sa180 \li0 \fi0 Zed reserves the right to update this Agreement at any time. The terms and conditions of the updated version of the Agreement shall apply to the Zed Service and Editor downloaded, or accessed following the date of publication of the updated version. If You do not agree with any terms of the updated Agreement, You may not use or access the Zed Service or Editor in any manner. Zed may from time-to-time provide release notes applicable to the Editor or Zed Service, and such release notes may contain additional use restrictions or terms applicable to Customer Data. Your use of the Editor or Zed Service after the applicable release notes are made available shall be subject to the additional use restrictions or terms applicable to Customer Data.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel2 \b \fs28 11.9. Entire Agreement\par} -{\pard \ql \f0 \sa180 \li0 \fi0 This Agreement is the complete and exclusive statement of the mutual understanding of the parties and supersedes and cancels all previous written and oral agreements, communications, and other understandings relating to the subject matter of this Agreement, and all waivers and modifications must be in a writing signed by both parties, except as otherwise provided herein. Any term or provision of this Agreement held to be illegal or unenforceable shall be, to the fullest extent possible, interpreted so as to be construed as valid, but in any event the validity or enforceability of the remainder hereof shall not be affected.\par} -{\pard \ql \f0 \sa180 \li0 \fi0 {\b DATE: May 6, 2025}\par} -} diff --git a/script/trigger-release b/script/trigger-release deleted file mode 100755 index 457a1f29f7..0000000000 --- a/script/trigger-release +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -which gh >/dev/null || brew install gh - -if [ "$1" == "nightly" ]; then - ./script/bump-nightly - exit -fi - -version=$(./script/get-released-version "$1" | sed 's/\.[^\.]*$/.x/') -echo "Bumping $1 (v$version)" - -gh workflow run "bump_patch_version.yml" -f branch="v$version" -echo "Follow along at: https://github.com/zed-industries/zed/actions" diff --git a/script/uninstall.sh b/script/uninstall.sh deleted file mode 100644 index 3e460b8186..0000000000 --- a/script/uninstall.sh +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env sh -set -eu - -# Uninstalls Zed that was installed using the install.sh script - -check_remaining_installations() { - platform="$(uname -s)" - if [ "$platform" = "Darwin" ]; then - # Check for any Zed variants in /Applications - remaining=$(ls -d /Applications/Zed*.app 2>/dev/null | wc -l) - [ "$remaining" -eq 0 ] - else - # Check for any Zed variants in ~/.local - remaining=$(ls -d "$HOME/.local/zed"*.app 2>/dev/null | wc -l) - [ "$remaining" -eq 0 ] - fi -} - -prompt_remove_preferences() { - printf "Do you want to keep your Zed preferences? [Y/n] " - read -r response - case "$response" in - [nN]|[nN][oO]) - rm -rf "$HOME/.config/zed" - echo "Preferences removed." - ;; - *) - echo "Preferences kept." - ;; - esac -} - -main() { - platform="$(uname -s)" - channel="${ZED_CHANNEL:-stable}" - - if [ "$platform" = "Darwin" ]; then - platform="macos" - elif [ "$platform" = "Linux" ]; then - platform="linux" - else - echo "Unsupported platform $platform" - exit 1 - fi - - "$platform" - - echo "Zed has been uninstalled" -} - -linux() { - suffix="" - if [ "$channel" != "stable" ]; then - suffix="-$channel" - fi - - appid="" - db_suffix="stable" - case "$channel" in - stable) - appid="dev.zed.Zed" - db_suffix="stable" - ;; - nightly) - appid="dev.zed.Zed-Nightly" - db_suffix="nightly" - ;; - preview) - appid="dev.zed.Zed-Preview" - db_suffix="preview" - ;; - dev) - appid="dev.zed.Zed-Dev" - db_suffix="dev" - ;; - *) - echo "Unknown release channel: ${channel}. Using stable app ID." - appid="dev.zed.Zed" - db_suffix="stable" - ;; - esac - - # Remove the app directory - rm -rf "$HOME/.local/zed$suffix.app" - - # Remove the binary symlink - rm -f "$HOME/.local/bin/zed" - - # Remove the .desktop file - rm -f "$HOME/.local/share/applications/${appid}.desktop" - - # Remove the database directory for this channel - rm -rf "$HOME/.local/share/zed/db/0-$db_suffix" - - # Remove socket file - rm -f "$HOME/.local/share/zed/zed-$db_suffix.sock" - - # Remove the entire Zed directory if no installations remain - if check_remaining_installations; then - rm -rf "$HOME/.local/share/zed" - prompt_remove_preferences - fi - - rm -rf $HOME/.zed_server -} - -macos() { - app="Zed.app" - db_suffix="stable" - app_id="dev.zed.Zed" - case "$channel" in - nightly) - app="Zed Nightly.app" - db_suffix="nightly" - app_id="dev.zed.Zed-Nightly" - ;; - preview) - app="Zed Preview.app" - db_suffix="preview" - app_id="dev.zed.Zed-Preview" - ;; - dev) - app="Zed Dev.app" - db_suffix="dev" - app_id="dev.zed.Zed-Dev" - ;; - esac - - # Remove the app bundle - if [ -d "/Applications/$app" ]; then - rm -rf "/Applications/$app" - fi - - # Remove the binary symlink - rm -f "$HOME/.local/bin/zed" - - # Remove the database directory for this channel - rm -rf "$HOME/Library/Application Support/Zed/db/0-$db_suffix" - - # Remove app-specific files and directories - rm -rf "$HOME/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/$app_id.sfl"* - rm -rf "$HOME/Library/Caches/$app_id" - rm -rf "$HOME/Library/HTTPStorages/$app_id" - rm -rf "$HOME/Library/Preferences/$app_id.plist" - rm -rf "$HOME/Library/Saved Application State/$app_id.savedState" - - # Remove the entire Zed directory if no installations remain - if check_remaining_installations; then - rm -rf "$HOME/Library/Application Support/Zed" - rm -rf "$HOME/Library/Logs/Zed" - - prompt_remove_preferences - fi - - rm -rf $HOME/.zed_server -} - -main "$@" diff --git a/script/update-json-schemas b/script/update-json-schemas deleted file mode 100755 index 182e0ff03b..0000000000 --- a/script/update-json-schemas +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -cd "$(dirname "$0")/.." || exit 1 -cd crates/languages/src/json/schemas -files=( - "tsconfig.json" - "package.json" -) -for file in "${files[@]}"; do - curl -sL -o "$file" "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/$file" -done - -HASH="$(curl -s 'https://api.github.com/repos/SchemaStore/schemastore/commits/HEAD' | jq -r '.sha')" -SHORT_HASH="${HASH:0:7}" -DATE="$(curl -s 'https://api.github.com/repos/SchemaStore/schemastore/commits/HEAD' |jq -r .commit.author.date | cut -c1-10)" -echo -echo "Updated JSON schemas to [SchemaStore/schemastore@$SHORT_HASH](https://github.com/SchemaStore/schemastore/tree/$HASH) ($DATE)" -echo -for file in "${files[@]}"; do - echo "- [$file](https://github.com/SchemaStore/schemastore/commits/master/src/schemas/json/$file)" \ - "@ [$SHORT_HASH](https://raw.githubusercontent.com/SchemaStore/schemastore/$HASH/src/schemas/json/$file)" -done -echo diff --git a/script/update_top_ranking_issues/.python-version b/script/update_top_ranking_issues/.python-version deleted file mode 100644 index 24ee5b1be9..0000000000 --- a/script/update_top_ranking_issues/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.13 diff --git a/script/update_top_ranking_issues/main.py b/script/update_top_ranking_issues/main.py deleted file mode 100644 index 336c00497a..0000000000 --- a/script/update_top_ranking_issues/main.py +++ /dev/null @@ -1,161 +0,0 @@ -import os -from datetime import date, datetime, timedelta -from typing import Any, Optional - -import requests -import typer -from pytz import timezone -from typer import Typer - -app: Typer = typer.Typer() - -AMERICA_NEW_YORK_TIMEZONE = "America/New_York" -DATETIME_FORMAT: str = "%B %d, %Y %I:%M %p" -ISSUES_PER_SECTION: int = 50 -ISSUES_TO_FETCH: int = 100 - -REPO_OWNER = "zed-industries" -REPO_NAME = "zed" -GITHUB_API_BASE_URL = "https://api.github.com" - -EXCLUDE_LABEL = "ignore top-ranking issues" - - -@app.command() -def main( - github_token: Optional[str] = None, - issue_reference_number: Optional[int] = None, - query_day_interval: Optional[int] = None, -) -> None: - script_start_time: datetime = datetime.now() - start_date: date | None = None - - if query_day_interval: - tz = timezone(AMERICA_NEW_YORK_TIMEZONE) - today = datetime.now(tz).date() - start_date = today - timedelta(days=query_day_interval) - - # GitHub Workflow will pass in the token as an argument, - # but we can place it in our env when running the script locally, for convenience - token = github_token or os.getenv("GITHUB_ACCESS_TOKEN") - if not token: - raise typer.BadParameter( - "GitHub token is required. Pass --github-token or set GITHUB_ACCESS_TOKEN env var." - ) - - headers = { - "Authorization": f"token {token}", - "Accept": "application/vnd.github+json", - } - - section_to_issues = get_section_to_issues(headers, start_date) - issue_text: str = create_issue_text(section_to_issues) - - if issue_reference_number: - update_reference_issue(headers, issue_reference_number, issue_text) - else: - print(issue_text) - - run_duration: timedelta = datetime.now() - script_start_time - print(f"Ran for {run_duration}") - - -def get_section_to_issues( - headers: dict[str, str], start_date: date | None = None -) -> dict[str, list[dict[str, Any]]]: - """Fetch top-ranked issues for each section from GitHub.""" - - section_filters = { - "Bugs": "type:Bug", - "Crashes": "type:Crash", - "Features": "type:Feature", - "Tracking issues": "type:Tracking", - "Meta issues": "type:Meta", - "Windows": 'label:"platform:windows"', - } - - section_to_issues: dict[str, list[dict[str, Any]]] = {} - for section, search_qualifier in section_filters.items(): - query_parts = [ - f"repo:{REPO_OWNER}/{REPO_NAME}", - "is:issue", - "is:open", - f'-label:"{EXCLUDE_LABEL}"', - search_qualifier, - ] - - if start_date: - query_parts.append(f"created:>={start_date.strftime('%Y-%m-%d')}") - - query = " ".join(query_parts) - url = f"{GITHUB_API_BASE_URL}/search/issues" - params = { - "q": query, - "sort": "reactions-+1", - "order": "desc", - "per_page": ISSUES_TO_FETCH, # this will work as long as it's ≤ 100 - } - - # we are only fetching one page on purpose - response = requests.get(url, headers=headers, params=params) - response.raise_for_status() - items = response.json()["items"] - - issues: list[dict[str, Any]] = [] - for item in items: - reactions = item["reactions"] - score = reactions["+1"] - reactions["-1"] - if score > 0: - issues.append({ - "url": item["html_url"], - "score": score, - "created_at": item["created_at"], - }) - - if not issues: - continue - - issues.sort(key=lambda x: (-x["score"], x["created_at"])) - section_to_issues[section] = issues[:ISSUES_PER_SECTION] - - # Sort sections by total score (highest total first) - section_to_issues = dict( - sorted( - section_to_issues.items(), - key=lambda item: sum(issue["score"] for issue in item[1]), - reverse=True, - ) - ) - return section_to_issues - - -def update_reference_issue( - headers: dict[str, str], issue_number: int, body: str -) -> None: - url = f"{GITHUB_API_BASE_URL}/repos/{REPO_OWNER}/{REPO_NAME}/issues/{issue_number}" - response = requests.patch(url, headers=headers, json={"body": body}) - response.raise_for_status() - - -def create_issue_text(section_to_issues: dict[str, list[dict[str, Any]]]) -> str: - tz = timezone(AMERICA_NEW_YORK_TIMEZONE) - current_datetime: str = datetime.now(tz).strftime(f"{DATETIME_FORMAT} (%Z)") - - lines: list[str] = [f"*Updated on {current_datetime}*"] - - for section, issues in section_to_issues.items(): - lines.append(f"\n## {section}\n") - for i, issue in enumerate(issues): - lines.append(f"{i + 1}. {issue['url']} ({issue['score']} :thumbsup:)") - - lines.append("\n---\n") - lines.append( - "*For details on how this issue is generated, " - "[see the script](https://github.com/zed-industries/zed/blob/main/script/update_top_ranking_issues/main.py)*" - ) - - return "\n".join(lines) - - -if __name__ == "__main__": - app() diff --git a/script/update_top_ranking_issues/pyproject.toml b/script/update_top_ranking_issues/pyproject.toml deleted file mode 100644 index aa3f8cc7ff..0000000000 --- a/script/update_top_ranking_issues/pyproject.toml +++ /dev/null @@ -1,14 +0,0 @@ -[project] -name = "update-top-ranking-issues" -version = "0.1.0" -readme = "README.md" -requires-python = ">=3.13" -dependencies = [ - "mypy>=1.15.0", - "pytz>=2025.1", - "requests>=2.32.0", - "ruff>=0.9.7", - "typer>=0.15.1", - "types-pytz>=2025.1.0.20250204", - "types-requests>=2.32.0", -] diff --git a/script/update_top_ranking_issues/pyrightconfig.json b/script/update_top_ranking_issues/pyrightconfig.json deleted file mode 100644 index 8fd86437bc..0000000000 --- a/script/update_top_ranking_issues/pyrightconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "venvPath": ".", - "venv": ".venv" -} diff --git a/script/update_top_ranking_issues/uv.lock b/script/update_top_ranking_issues/uv.lock deleted file mode 100644 index 174f4e677f..0000000000 --- a/script/update_top_ranking_issues/uv.lock +++ /dev/null @@ -1,274 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.13" - -[[package]] -name = "certifi" -version = "2024.8.30" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507, upload-time = "2024-08-30T01:55:04.365Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321, upload-time = "2024-08-30T01:55:02.591Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620, upload-time = "2024-10-09T07:40:20.413Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617, upload-time = "2024-10-09T07:39:07.317Z" }, - { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310, upload-time = "2024-10-09T07:39:08.353Z" }, - { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126, upload-time = "2024-10-09T07:39:09.327Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342, upload-time = "2024-10-09T07:39:10.322Z" }, - { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383, upload-time = "2024-10-09T07:39:12.042Z" }, - { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214, upload-time = "2024-10-09T07:39:13.059Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104, upload-time = "2024-10-09T07:39:14.815Z" }, - { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255, upload-time = "2024-10-09T07:39:15.868Z" }, - { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251, upload-time = "2024-10-09T07:39:16.995Z" }, - { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474, upload-time = "2024-10-09T07:39:18.021Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849, upload-time = "2024-10-09T07:39:19.243Z" }, - { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781, upload-time = "2024-10-09T07:39:20.397Z" }, - { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970, upload-time = "2024-10-09T07:39:21.452Z" }, - { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973, upload-time = "2024-10-09T07:39:22.509Z" }, - { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308, upload-time = "2024-10-09T07:39:23.524Z" }, - { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446, upload-time = "2024-10-09T07:40:19.383Z" }, -] - -[[package]] -name = "click" -version = "8.1.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121, upload-time = "2023-08-17T17:29:11.868Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941, upload-time = "2023-08-17T17:29:10.08Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, -] - -[[package]] -name = "mypy" -version = "1.15.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717, upload-time = "2025-02-05T03:50:34.655Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592, upload-time = "2025-02-05T03:48:55.789Z" }, - { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611, upload-time = "2025-02-05T03:48:44.581Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443, upload-time = "2025-02-05T03:49:25.514Z" }, - { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541, upload-time = "2025-02-05T03:49:57.623Z" }, - { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348, upload-time = "2025-02-05T03:48:52.361Z" }, - { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648, upload-time = "2025-02-05T03:49:11.395Z" }, - { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777, upload-time = "2025-02-05T03:50:08.348Z" }, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433, upload-time = "2023-02-04T12:11:27.157Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695, upload-time = "2023-02-04T12:11:25.002Z" }, -] - -[[package]] -name = "pygments" -version = "2.18.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905, upload-time = "2024-05-04T13:42:02.013Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513, upload-time = "2024-05-04T13:41:57.345Z" }, -] - -[[package]] -name = "pytz" -version = "2025.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5f/57/df1c9157c8d5a05117e455d66fd7cf6dbc46974f832b1058ed4856785d8a/pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e", size = 319617, upload-time = "2025-01-31T01:54:48.615Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930, upload-time = "2025-01-31T01:54:45.634Z" }, -] - -[[package]] -name = "requests" -version = "2.32.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, -] - -[[package]] -name = "rich" -version = "13.9.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/9e/1784d15b057b0075e5136445aaea92d23955aad2c93eaede673718a40d95/rich-13.9.2.tar.gz", hash = "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c", size = 222843, upload-time = "2024-10-04T11:50:31.453Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/91/5474b84e505a6ccc295b2d322d90ff6aa0746745717839ee0c5fb4fdcceb/rich-13.9.2-py3-none-any.whl", hash = "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1", size = 242117, upload-time = "2024-10-04T11:50:29.123Z" }, -] - -[[package]] -name = "ruff" -version = "0.9.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/39/8b/a86c300359861b186f18359adf4437ac8e4c52e42daa9eedc731ef9d5b53/ruff-0.9.7.tar.gz", hash = "sha256:643757633417907510157b206e490c3aa11cab0c087c912f60e07fbafa87a4c6", size = 3669813, upload-time = "2025-02-20T13:26:52.111Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/f3/3a1d22973291226df4b4e2ff70196b926b6f910c488479adb0eeb42a0d7f/ruff-0.9.7-py3-none-linux_armv6l.whl", hash = "sha256:99d50def47305fe6f233eb8dabfd60047578ca87c9dcb235c9723ab1175180f4", size = 11774588, upload-time = "2025-02-20T13:25:52.253Z" }, - { url = "https://files.pythonhosted.org/packages/8e/c9/b881f4157b9b884f2994fd08ee92ae3663fb24e34b0372ac3af999aa7fc6/ruff-0.9.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d59105ae9c44152c3d40a9c40d6331a7acd1cdf5ef404fbe31178a77b174ea66", size = 11746848, upload-time = "2025-02-20T13:25:57.279Z" }, - { url = "https://files.pythonhosted.org/packages/14/89/2f546c133f73886ed50a3d449e6bf4af27d92d2f960a43a93d89353f0945/ruff-0.9.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f313b5800483770bd540cddac7c90fc46f895f427b7820f18fe1822697f1fec9", size = 11177525, upload-time = "2025-02-20T13:26:00.007Z" }, - { url = "https://files.pythonhosted.org/packages/d7/93/6b98f2c12bf28ab9def59c50c9c49508519c5b5cfecca6de871cf01237f6/ruff-0.9.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:042ae32b41343888f59c0a4148f103208bf6b21c90118d51dc93a68366f4e903", size = 11996580, upload-time = "2025-02-20T13:26:03.274Z" }, - { url = "https://files.pythonhosted.org/packages/8e/3f/b3fcaf4f6d875e679ac2b71a72f6691a8128ea3cb7be07cbb249f477c061/ruff-0.9.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87862589373b33cc484b10831004e5e5ec47dc10d2b41ba770e837d4f429d721", size = 11525674, upload-time = "2025-02-20T13:26:06.073Z" }, - { url = "https://files.pythonhosted.org/packages/f0/48/33fbf18defb74d624535d5d22adcb09a64c9bbabfa755bc666189a6b2210/ruff-0.9.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a17e1e01bee0926d351a1ee9bc15c445beae888f90069a6192a07a84af544b6b", size = 12739151, upload-time = "2025-02-20T13:26:08.964Z" }, - { url = "https://files.pythonhosted.org/packages/63/b5/7e161080c5e19fa69495cbab7c00975ef8a90f3679caa6164921d7f52f4a/ruff-0.9.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7c1f880ac5b2cbebd58b8ebde57069a374865c73f3bf41f05fe7a179c1c8ef22", size = 13416128, upload-time = "2025-02-20T13:26:12.54Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c8/b5e7d61fb1c1b26f271ac301ff6d9de5e4d9a9a63f67d732fa8f200f0c88/ruff-0.9.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e63fc20143c291cab2841dbb8260e96bafbe1ba13fd3d60d28be2c71e312da49", size = 12870858, upload-time = "2025-02-20T13:26:16.794Z" }, - { url = "https://files.pythonhosted.org/packages/da/cb/2a1a8e4e291a54d28259f8fc6a674cd5b8833e93852c7ef5de436d6ed729/ruff-0.9.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91ff963baed3e9a6a4eba2a02f4ca8eaa6eba1cc0521aec0987da8d62f53cbef", size = 14786046, upload-time = "2025-02-20T13:26:19.85Z" }, - { url = "https://files.pythonhosted.org/packages/ca/6c/c8f8a313be1943f333f376d79724260da5701426c0905762e3ddb389e3f4/ruff-0.9.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88362e3227c82f63eaebf0b2eff5b88990280fb1ecf7105523883ba8c3aaf6fb", size = 12550834, upload-time = "2025-02-20T13:26:23.082Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ad/f70cf5e8e7c52a25e166bdc84c082163c9c6f82a073f654c321b4dff9660/ruff-0.9.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0372c5a90349f00212270421fe91874b866fd3626eb3b397ede06cd385f6f7e0", size = 11961307, upload-time = "2025-02-20T13:26:26.738Z" }, - { url = "https://files.pythonhosted.org/packages/52/d5/4f303ea94a5f4f454daf4d02671b1fbfe2a318b5fcd009f957466f936c50/ruff-0.9.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d76b8ab60e99e6424cd9d3d923274a1324aefce04f8ea537136b8398bbae0a62", size = 11612039, upload-time = "2025-02-20T13:26:30.26Z" }, - { url = "https://files.pythonhosted.org/packages/eb/c8/bd12a23a75603c704ce86723be0648ba3d4ecc2af07eecd2e9fa112f7e19/ruff-0.9.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0c439bdfc8983e1336577f00e09a4e7a78944fe01e4ea7fe616d00c3ec69a3d0", size = 12168177, upload-time = "2025-02-20T13:26:33.452Z" }, - { url = "https://files.pythonhosted.org/packages/cc/57/d648d4f73400fef047d62d464d1a14591f2e6b3d4a15e93e23a53c20705d/ruff-0.9.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:115d1f15e8fdd445a7b4dc9a30abae22de3f6bcabeb503964904471691ef7606", size = 12610122, upload-time = "2025-02-20T13:26:37.365Z" }, - { url = "https://files.pythonhosted.org/packages/49/79/acbc1edd03ac0e2a04ae2593555dbc9990b34090a9729a0c4c0cf20fb595/ruff-0.9.7-py3-none-win32.whl", hash = "sha256:e9ece95b7de5923cbf38893f066ed2872be2f2f477ba94f826c8defdd6ec6b7d", size = 9988751, upload-time = "2025-02-20T13:26:40.366Z" }, - { url = "https://files.pythonhosted.org/packages/6d/95/67153a838c6b6ba7a2401241fd8a00cd8c627a8e4a0491b8d853dedeffe0/ruff-0.9.7-py3-none-win_amd64.whl", hash = "sha256:3770fe52b9d691a15f0b87ada29c45324b2ace8f01200fb0c14845e499eb0c2c", size = 11002987, upload-time = "2025-02-20T13:26:43.762Z" }, - { url = "https://files.pythonhosted.org/packages/63/6a/aca01554949f3a401991dc32fe22837baeaccb8a0d868256cbb26a029778/ruff-0.9.7-py3-none-win_arm64.whl", hash = "sha256:b075a700b2533feb7a01130ff656a4ec0d5f340bb540ad98759b8401c32c2037", size = 10177763, upload-time = "2025-02-20T13:26:48.92Z" }, -] - -[[package]] -name = "shellingham" -version = "1.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, -] - -[[package]] -name = "typer" -version = "0.15.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/dca7b219718afd37a0068f4f2530a727c2b74a8b6e8e0c0080a4c0de4fcd/typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a", size = 99789, upload-time = "2024-12-04T17:44:58.956Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908, upload-time = "2024-12-04T17:44:57.291Z" }, -] - -[[package]] -name = "types-pytz" -version = "2025.1.0.20250204" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/d2/2190c54d53c04491ad72a1df019c5dfa692e6ab6c2dba1be7b6c9d530e30/types_pytz-2025.1.0.20250204.tar.gz", hash = "sha256:00f750132769f1c65a4f7240bc84f13985b4da774bd17dfbe5d9cd442746bd49", size = 10352, upload-time = "2025-02-04T02:39:05.553Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/50/65ffad73746f1d8b15992c030e0fd22965fd5ae2c0206dc28873343b3230/types_pytz-2025.1.0.20250204-py3-none-any.whl", hash = "sha256:32ca4a35430e8b94f6603b35beb7f56c32260ddddd4f4bb305fdf8f92358b87e", size = 10059, upload-time = "2025-02-04T02:39:03.899Z" }, -] - -[[package]] -name = "types-requests" -version = "2.32.4.20250913" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113, upload-time = "2025-09-13T02:40:02.309Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658, upload-time = "2025-09-13T02:40:01.115Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321, upload-time = "2024-06-07T18:52:15.995Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438, upload-time = "2024-06-07T18:52:13.582Z" }, -] - -[[package]] -name = "update-top-ranking-issues" -version = "0.1.0" -source = { virtual = "." } -dependencies = [ - { name = "mypy" }, - { name = "pytz" }, - { name = "requests" }, - { name = "ruff" }, - { name = "typer" }, - { name = "types-pytz" }, - { name = "types-requests" }, -] - -[package.metadata] -requires-dist = [ - { name = "mypy", specifier = ">=1.15.0" }, - { name = "pytz", specifier = ">=2025.1" }, - { name = "requests", specifier = ">=2.32.0" }, - { name = "ruff", specifier = ">=0.9.7" }, - { name = "typer", specifier = ">=0.15.1" }, - { name = "types-pytz", specifier = ">=2025.1.0.20250204" }, - { name = "types-requests", specifier = ">=2.32.0" }, -] - -[[package]] -name = "urllib3" -version = "2.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677, upload-time = "2024-09-12T10:52:18.401Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338, upload-time = "2024-09-12T10:52:16.589Z" }, -] diff --git a/script/upload-extension-cli b/script/upload-extension-cli deleted file mode 100755 index 3af9c3251f..0000000000 --- a/script/upload-extension-cli +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -bash -euo pipefail -source script/lib/blob-store.sh - -commit=$1 -if [ "$#" -ne 1 ] || ! [[ $commit =~ ^[0-9a-f]{40}$ ]]; then - echo "Usage: $0 " - exit 1 -fi - -bucket_name="zed-extension-cli" -target_triple=$(rustc -vV | sed -n 's|host: ||p') - -upload_to_blob_store_public $bucket_name "target/release/zed-extension" "${commit}/${target_triple}/zed-extension" diff --git a/script/upload-nightly b/script/upload-nightly deleted file mode 100755 index 12e7c16ef7..0000000000 --- a/script/upload-nightly +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -bash -euo pipefail -source script/lib/blob-store.sh - -bucket_name="zed-nightly-host" -version=$(./script/get-crate-version zed)+nightly."${GITHUB_RUN_NUMBER}.${GITHUB_SHA}" - -for file_to_upload in ./release-artifacts/*; do - [ -f "$file_to_upload" ] || continue - upload_to_blob_store_public $bucket_name "$file_to_upload" "nightly/$(basename "$file_to_upload")" - upload_to_blob_store_public $bucket_name "$file_to_upload" "${version}/$(basename "$file_to_upload")" - rm -f "$file_to_upload" -done - -echo -n ${version} > ./release-artifacts/latest-sha -upload_to_blob_store_public $bucket_name "release-artifacts/latest-sha" "nightly/latest-sha" diff --git a/script/upload-nightly.ps1 b/script/upload-nightly.ps1 deleted file mode 100644 index 4400c4291b..0000000000 --- a/script/upload-nightly.ps1 +++ /dev/null @@ -1,33 +0,0 @@ -[CmdletBinding()] -Param( - [Parameter()][string]$Architecture -) - -# Based on the template in: https://docs.digitalocean.com/reference/api/spaces-api/ -$ErrorActionPreference = "Stop" -. "$PSScriptRoot\lib\blob-store.ps1" -. "$PSScriptRoot\lib\workspace.ps1" - -ParseZedWorkspace -Write-Host "Uploading nightly for target: $target" - -$bucketName = "zed-nightly-host" -$releaseVersion = & "$PSScriptRoot\get-crate-version.ps1" zed -$version = "$releaseVersion+nightly.$env:GITHUB_RUN_NUMBER.$env:GITHUB_SHA" - -# TODO: -# Upload remote server files -# $remoteServerFiles = Get-ChildItem -Path "target" -Filter "zed-remote-server-*.gz" -Recurse -File -# foreach ($file in $remoteServerFiles) { -# Upload-ToBlobStore -BucketName $bucketName -FileToUpload $file.FullName -BlobStoreKey "nightly/$($file.Name)" -# Remove-Item -Path $file.FullName -# } - -UploadToBlobStore -BucketName $bucketName -FileToUpload "target/Zed-$Architecture.exe" -BlobStoreKey "nightly/Zed-$Architecture.exe" -UploadToBlobStore -BucketName $bucketName -FileToUpload "target/Zed-$Architecture.exe" -BlobStoreKey "$version/Zed-$Architecture.exe" - -Remove-Item -Path "target/Zed-$Architecture.exe" -ErrorAction SilentlyContinue - -$version | Out-File -FilePath "target/latest-sha" -NoNewline -UploadToBlobStore -BucketName $bucketName -FileToUpload "target/latest-sha" -BlobStoreKey "nightly/latest-sha-windows" -Remove-Item -Path "target/latest-sha" -ErrorAction SilentlyContinue diff --git a/script/what-is-deployed b/script/what-is-deployed deleted file mode 100755 index c7c4b3ad63..0000000000 --- a/script/what-is-deployed +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash - -set -eu -source script/lib/deploy-helpers.sh - -if [[ $# != 1 ]]; then - echo "Usage: $0 " - exit 1 -fi - -environment=$1 -url="$(url_for_environment $environment)" -tag="$(tag_for_environment $environment)" - -target_zed_kube_cluster - -deployed_image_id=$( - kubectl \ - --namespace=${environment} \ - get deployment collab \ - -o 'jsonpath={.spec.template.spec.containers[0].image}' \ - | cut -d: -f2 -) - -echo "Deployed image version: $deployed_image_id" - -git fetch >/dev/null -if [[ "$(git rev-parse tags/$tag)" != $deployed_image_id ]]; then - echo "NOTE: tags/$tag $(git rev-parse tags/$tag) is not yet deployed" -fi; diff --git a/script/zed-local b/script/zed-local deleted file mode 100755 index d07eb21cba..0000000000 --- a/script/zed-local +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/env node - -const HELP = ` -USAGE - zed-local [options] [zed args] - -SUMMARY - Runs 1-6 instances of Zed using a locally-running collaboration server. - Each instance of Zed will be signed in as a different user specified in - either \`.admins.json\` or \`.admins.default.json\`. - - All arguments after the initial options will be passed through to the first - instance of Zed. This can be used to test SSH remoting along with collab, like - so: - - $ script/zed-local -2 ssh://your-ssh-uri-here - -OPTIONS - --help Print this help message - --release Build Zed in release mode - -2, -3, -4, ... Spawn multiple Zed instances, with their windows tiled. - --top Arrange the Zed windows so they take up the top half of the screen. - --stable Use stable Zed release installed on local machine for all instances (except for the first one). - --preview Like --stable, but uses the locally-installed preview release instead. -`.trim(); - -const { spawn, execSync, execFileSync } = require("child_process"); -const assert = require("assert"); - -let users; -if (process.env.SEED_PATH) { - users = require(process.env.SEED_PATH).admins; -} else { - users = require("../crates/collab/seed.default.json").admins; - try { - const defaultUsers = users; - const customUsers = require("../crates/collab/seed.json").admins; - assert(customUsers.length > 0); - users = customUsers.concat( - defaultUsers.filter((user) => !customUsers.includes(user)), - ); - } catch (_) {} -} - -const RESOLUTION_REGEX = /(\d+) x (\d+)/; -const DIGIT_FLAG_REGEX = /^--?(\d+)$/; - -let instanceCount = 1; -let isReleaseMode = false; -let isTop = false; -let othersOnStable = false; -let othersOnPreview = false; -let isStateful = false; - -const args = process.argv.slice(2); -while (args.length > 0) { - const arg = args[0]; - - const digitMatch = arg.match(DIGIT_FLAG_REGEX); - if (digitMatch) { - instanceCount = parseInt(digitMatch[1]); - } else if (arg === "--release") { - isReleaseMode = true; - } else if (arg == "--stateful") { - isStateful = true; - } else if (arg === "--top") { - isTop = true; - } else if (arg === "--help") { - console.log(HELP); - process.exit(0); - } else if (arg === "--stable") { - othersOnStable = true; - } else if (arg === "--preview") { - othersOnPreview = true; - } else { - break; - } - - args.shift(); -} -const os = require("os"); -const platform = os.platform(); - -let screenWidth, screenHeight; -const titleBarHeight = 24; - -if (platform === "darwin") { - // macOS - const displayInfo = JSON.parse( - execFileSync("system_profiler", ["SPDisplaysDataType", "-json"], { - encoding: "utf8", - }), - ); - const mainDisplayResolution = displayInfo?.SPDisplaysDataType?.flatMap( - (display) => display?.spdisplays_ndrvs, - ) - ?.find((entry) => entry?.spdisplays_main === "spdisplays_yes") - ?._spdisplays_resolution?.match(RESOLUTION_REGEX); - if (!mainDisplayResolution) { - throw new Error("Could not parse screen resolution"); - } - screenWidth = parseInt(mainDisplayResolution[1]); - screenHeight = parseInt(mainDisplayResolution[2]) - titleBarHeight; -} else if (platform === "linux") { - // Linux - try { - const xrandrOutput = execSync('xrandr | grep "\\*" | cut -d" " -f4', { - encoding: "utf8", - }).trim(); - [screenWidth, screenHeight] = xrandrOutput.split("x").map(Number); - } catch (err) { - console.log(err); - throw new Error("Could not get screen resolution"); - } -} else if (platform === "win32") { - // windows - try { - const resolutionOutput = execSync( - `powershell -Command "& {Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea.Size}"`, - { encoding: "utf8" }, - ).trim(); - [screenWidth, screenHeight] = resolutionOutput.match(/\d+/g).map(Number); - } catch (err) { - console.log(err); - throw new Error("Could not get screen resolution on Windows"); - } -} - -if (platform !== "win32") { - screenHeight -= titleBarHeight; -} - -if (isTop) { - screenHeight = Math.floor(screenHeight / 2); -} - -// Determine the window size for each instance -let rows; -let columns; -switch (instanceCount) { - case 1: - [rows, columns] = [1, 1]; - break; - case 2: - [rows, columns] = [1, 2]; - break; - case 3: - case 4: - [rows, columns] = [2, 2]; - break; - case 5: - case 6: - [rows, columns] = [2, 3]; - break; -} - -const instanceWidth = Math.floor(screenWidth / columns); -const instanceHeight = Math.floor(screenHeight / rows); - -// If a user is specified, make sure it's first in the list -const user = process.env.ZED_IMPERSONATE; -if (user) { - users = [user].concat(users.filter((u) => u !== user)); -} - -let buildArgs = ["build"]; -let zedBinary = "target/debug/zed"; -if (isReleaseMode) { - buildArgs.push("--release"); - zedBinary = "target/release/zed"; -} - -try { - execFileSync("cargo", buildArgs, { - stdio: "inherit", - }); -} catch (e) { - process.exit(0); -} - -setTimeout(() => { - for (let i = 0; i < instanceCount; i++) { - const row = Math.floor(i / columns); - const column = i % columns; - let position; - if (platform == "win32") { - position = [column * instanceWidth, row * instanceHeight].join(","); - } else { - position = [ - column * instanceWidth, - row * instanceHeight + titleBarHeight, - ].join(","); - } - const size = [instanceWidth, instanceHeight].join(","); - let binaryPath = zedBinary; - if (i != 0 && othersOnStable) { - binaryPath = "/Applications/Zed.app/Contents/MacOS/zed"; - } else if (i != 0 && othersOnPreview) { - binaryPath = "/Applications/Zed Preview.app/Contents/MacOS/zed"; - } - spawn(binaryPath, i == 0 ? args : [], { - stdio: "inherit", - env: Object.assign({}, process.env, { - ZED_IMPERSONATE: users[i], - ZED_WINDOW_POSITION: position, - ZED_STATELESS: isStateful && i == 0 ? "" : "1", - ZED_ALWAYS_ACTIVE: "1", - ZED_SERVER_URL: "http://localhost:3000", - ZED_RPC_URL: "http://localhost:8080/rpc", - ZED_ADMIN_API_TOKEN: "internal-api-key-secret", - ZED_WINDOW_SIZE: size, - ZED_CLIENT_CHECKSUM_SEED: "development-checksum-seed", - RUST_LOG: process.env.RUST_LOG || "info", - }), - }); - } -}, 0.1); diff --git a/shell.nix b/shell.nix deleted file mode 100644 index d1783071f9..0000000000 --- a/shell.nix +++ /dev/null @@ -1,11 +0,0 @@ -(import ( - let - lock = builtins.fromJSON (builtins.readFile ./flake.lock); - in - fetchTarball { - url = - lock.nodes.flake-compat.locked.url - or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; - sha256 = lock.nodes.flake-compat.locked.narHash; - } -) { src = ./.; }).shellNix diff --git a/tooling/perf/Cargo.toml b/tooling/perf/Cargo.toml index d4acad1fdb..c9018de010 100644 --- a/tooling/perf/Cargo.toml +++ b/tooling/perf/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" publish = false edition.workspace = true license = "Apache-2.0" -description = "A tool for measuring Zed test performance, with too many Clippy lints" +description = "A tool for measuring GPUI test performance" [lib] diff --git a/tooling/xtask/Cargo.toml b/tooling/xtask/Cargo.toml deleted file mode 100644 index 13179b2eb6..0000000000 --- a/tooling/xtask/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "xtask" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[dependencies] -anyhow.workspace = true -backtrace.workspace = true -cargo_metadata.workspace = true -cargo_toml.workspace = true -clap = { workspace = true, features = ["derive"] } -toml.workspace = true -indoc.workspace = true -indexmap.workspace = true -serde.workspace = true -serde_json.workspace = true -toml_edit.workspace = true -gh-workflow.workspace = true diff --git a/tooling/xtask/LICENSE-GPL b/tooling/xtask/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/tooling/xtask/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/tooling/xtask/src/main.rs b/tooling/xtask/src/main.rs deleted file mode 100644 index 6f83927d67..0000000000 --- a/tooling/xtask/src/main.rs +++ /dev/null @@ -1,38 +0,0 @@ -mod tasks; -mod workspace; - -use anyhow::Result; -use clap::{Parser, Subcommand}; - -#[derive(Parser)] -#[command(name = "cargo xtask")] -struct Args { - #[command(subcommand)] - command: CliCommand, -} - -#[derive(Subcommand)] -enum CliCommand { - /// Runs `cargo clippy`. - Clippy(tasks::clippy::ClippyArgs), - Licenses(tasks::licenses::LicensesArgs), - /// Checks that packages conform to a set of standards. - PackageConformity(tasks::package_conformity::PackageConformityArgs), - /// Publishes GPUI and its dependencies to crates.io. - PublishGpui(tasks::publish_gpui::PublishGpuiArgs), - Workflows(tasks::workflows::GenerateWorkflowArgs), -} - -fn main() -> Result<()> { - let args = Args::parse(); - - match args.command { - CliCommand::Clippy(args) => tasks::clippy::run_clippy(args), - CliCommand::Licenses(args) => tasks::licenses::run_licenses(args), - CliCommand::PackageConformity(args) => { - tasks::package_conformity::run_package_conformity(args) - } - CliCommand::PublishGpui(args) => tasks::publish_gpui::run_publish_gpui(args), - CliCommand::Workflows(args) => tasks::workflows::run_workflows(args), - } -} diff --git a/tooling/xtask/src/tasks.rs b/tooling/xtask/src/tasks.rs deleted file mode 100644 index 01b3907f04..0000000000 --- a/tooling/xtask/src/tasks.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod clippy; -pub mod licenses; -pub mod package_conformity; -pub mod publish_gpui; -pub mod workflows; diff --git a/tooling/xtask/src/tasks/clippy.rs b/tooling/xtask/src/tasks/clippy.rs deleted file mode 100644 index 517223ceb8..0000000000 --- a/tooling/xtask/src/tasks/clippy.rs +++ /dev/null @@ -1,64 +0,0 @@ -#![allow(clippy::disallowed_methods, reason = "tooling is exempt")] -use std::process::Command; - -use anyhow::{Context as _, Result, bail}; -use clap::Parser; - -#[derive(Parser)] -pub struct ClippyArgs { - /// Automatically apply lint suggestions (`clippy --fix`). - #[arg(long)] - fix: bool, - - /// The package to run Clippy against (`cargo -p clippy`). - #[arg(long, short)] - package: Option, -} - -pub fn run_clippy(args: ClippyArgs) -> Result<()> { - let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); - - let mut clippy_command = Command::new(&cargo); - clippy_command.arg("clippy"); - - if let Some(package) = args.package.as_ref() { - clippy_command.args(["--package", package]); - } else { - clippy_command.arg("--workspace"); - } - - clippy_command - .arg("--release") - .arg("--all-targets") - .arg("--all-features"); - - if args.fix { - clippy_command.arg("--fix"); - } - - clippy_command.arg("--"); - - // Deny all warnings. - clippy_command.args(["--deny", "warnings"]); - - eprintln!( - "running: {cargo} {}", - clippy_command - .get_args() - .map(|arg| arg.to_str().unwrap()) - .collect::>() - .join(" ") - ); - - let exit_status = clippy_command - .spawn() - .context("failed to spawn child process")? - .wait() - .context("failed to wait for child process")?; - - if !exit_status.success() { - bail!("clippy failed: {}", exit_status); - } - - Ok(()) -} diff --git a/tooling/xtask/src/tasks/licenses.rs b/tooling/xtask/src/tasks/licenses.rs deleted file mode 100644 index 449c774d45..0000000000 --- a/tooling/xtask/src/tasks/licenses.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::path::{Path, PathBuf}; - -use anyhow::{Context as _, Result}; -use clap::Parser; - -use crate::workspace::load_workspace; - -#[derive(Parser)] -pub struct LicensesArgs {} - -pub fn run_licenses(_args: LicensesArgs) -> Result<()> { - const LICENSE_FILES: &[&str] = &["LICENSE-APACHE", "LICENSE-GPL", "LICENSE-AGPL"]; - - let workspace = load_workspace()?; - - for package in workspace.workspace_packages() { - let crate_dir = package - .manifest_path - .parent() - .with_context(|| format!("no crate directory for {}", package.name))?; - - if let Some(license_file) = first_license_file(crate_dir, LICENSE_FILES) { - if !license_file.is_symlink() { - println!("{} is not a symlink", license_file.display()); - } - - continue; - } - - println!("Missing license: {}", package.name); - } - - Ok(()) -} - -fn first_license_file(path: impl AsRef, license_files: &[&str]) -> Option { - for license_file in license_files { - let path_to_license = path.as_ref().join(license_file); - if path_to_license.exists() { - return Some(path_to_license); - } - } - - None -} diff --git a/tooling/xtask/src/tasks/package_conformity.rs b/tooling/xtask/src/tasks/package_conformity.rs deleted file mode 100644 index e1fd15112f..0000000000 --- a/tooling/xtask/src/tasks/package_conformity.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::collections::BTreeMap; -use std::fs; -use std::path::Path; - -use anyhow::{Context as _, Result}; -use cargo_toml::{Dependency, Manifest}; -use clap::Parser; - -use crate::workspace::load_workspace; - -#[derive(Parser)] -pub struct PackageConformityArgs {} - -pub fn run_package_conformity(_args: PackageConformityArgs) -> Result<()> { - let workspace = load_workspace()?; - - let mut non_workspace_dependencies = BTreeMap::new(); - - for package in workspace.workspace_packages() { - let is_extension = package - .manifest_path - .parent() - .and_then(|parent| parent.parent()) - .is_some_and(|grandparent_dir| grandparent_dir.ends_with("extensions")); - - let cargo_toml = read_cargo_toml(&package.manifest_path)?; - - let is_using_workspace_lints = cargo_toml.lints.is_some_and(|lints| lints.workspace); - if !is_using_workspace_lints { - eprintln!( - "{package:?} is not using workspace lints", - package = package.name - ); - } - - // Extensions should not use workspace dependencies. - if is_extension || package.name == "zed_extension_api" { - continue; - } - - for dependencies in [ - &cargo_toml.dependencies, - &cargo_toml.dev_dependencies, - &cargo_toml.build_dependencies, - ] { - for (name, dependency) in dependencies { - if let Dependency::Inherited(_) = dependency { - continue; - } - - non_workspace_dependencies - .entry(name.to_owned()) - .or_insert_with(Vec::new) - .push(package.name.clone()); - } - } - } - - for (dependency, packages) in non_workspace_dependencies { - eprintln!( - "{dependency} is being used as a non-workspace dependency: {}", - packages.join(", ") - ); - } - - Ok(()) -} - -/// Returns the contents of the `Cargo.toml` file at the given path. -fn read_cargo_toml(path: impl AsRef) -> Result { - let path = path.as_ref(); - let cargo_toml_bytes = fs::read(path)?; - Manifest::from_slice(&cargo_toml_bytes) - .with_context(|| format!("reading Cargo.toml at {path:?}")) -} diff --git a/tooling/xtask/src/tasks/publish_gpui.rs b/tooling/xtask/src/tasks/publish_gpui.rs deleted file mode 100644 index 2740f75a48..0000000000 --- a/tooling/xtask/src/tasks/publish_gpui.rs +++ /dev/null @@ -1,442 +0,0 @@ -#![allow(clippy::disallowed_methods, reason = "tooling is exempt")] -use std::io::{self, Write}; -use std::process::{Command, Output, Stdio}; - -use anyhow::{Context as _, Result, bail}; -use clap::Parser; - -#[derive(Parser)] -pub struct PublishGpuiArgs { - /// Perform a dry-run and wait for user confirmation before each publish - #[arg(long)] - dry_run: bool, - - /// Skip to a specific package (by package name or crate name) and start from there - #[arg(long)] - skip_to: Option, -} - -pub fn run_publish_gpui(args: PublishGpuiArgs) -> Result<()> { - println!( - "Starting GPUI publish process{}...", - if args.dry_run { " (with dry-run)" } else { "" } - ); - - let start_time = std::time::Instant::now(); - check_workspace_root()?; - - if args.skip_to.is_none() { - check_git_clean()?; - } else { - println!("Skipping git clean check due to --skip-to flag"); - } - - let version = read_gpui_version()?; - println!("Updating GPUI to version: {}", version); - publish_dependencies(&version, args.dry_run, args.skip_to.as_deref())?; - publish_gpui(&version, args.dry_run)?; - println!("GPUI published in {}s", start_time.elapsed().as_secs_f32()); - Ok(()) -} - -fn read_gpui_version() -> Result { - let gpui_cargo_toml_path = "crates/gpui/Cargo.toml"; - let contents = std::fs::read_to_string(gpui_cargo_toml_path) - .context("Failed to read crates/gpui/Cargo.toml")?; - - let cargo_toml: toml::Value = - toml::from_str(&contents).context("Failed to parse crates/gpui/Cargo.toml")?; - - let version = cargo_toml - .get("package") - .and_then(|p| p.get("version")) - .and_then(|v| v.as_str()) - .context("Failed to find version in crates/gpui/Cargo.toml")?; - - Ok(version.to_string()) -} - -fn publish_dependencies(new_version: &str, dry_run: bool, skip_to: Option<&str>) -> Result<()> { - let gpui_dependencies = vec![ - ("collections", "gpui_collections", "crates"), - ("perf", "gpui_perf", "tooling"), - ("util_macros", "gpui_util_macros", "crates"), - ("util", "gpui_util", "crates"), - ("gpui_macros", "gpui-macros", "crates"), - ("http_client", "gpui_http_client", "crates"), - ( - "derive_refineable", - "gpui_derive_refineable", - "crates/refineable", - ), - ("refineable", "gpui_refineable", "crates"), - ("semantic_version", "gpui_semantic_version", "crates"), - ("sum_tree", "gpui_sum_tree", "crates"), - ("media", "gpui_media", "crates"), - ]; - - let mut should_skip = skip_to.is_some(); - let skip_target = skip_to.unwrap_or(""); - - for (package_name, crate_name, package_dir) in gpui_dependencies { - if should_skip { - if package_name == skip_target || crate_name == skip_target { - println!("Found skip target: {} ({})", crate_name, package_name); - should_skip = false; - } else { - println!("Skipping: {} ({})", crate_name, package_name); - continue; - } - } - - println!( - "Publishing dependency: {} (package: {})", - crate_name, package_name - ); - - update_crate_cargo_toml(package_name, crate_name, package_dir, new_version)?; - update_workspace_dependency_version(package_name, crate_name, new_version)?; - publish_crate(crate_name, dry_run)?; - } - - if should_skip { - bail!( - "Could not find package or crate named '{}' to skip to", - skip_target - ); - } - - Ok(()) -} - -fn publish_gpui(new_version: &str, dry_run: bool) -> Result<()> { - update_crate_cargo_toml("gpui", "gpui", "crates", new_version)?; - - publish_crate("gpui", dry_run)?; - - Ok(()) -} - -fn update_crate_cargo_toml( - package_name: &str, - crate_name: &str, - package_dir: &str, - new_version: &str, -) -> Result<()> { - let cargo_toml_path = format!("{}/{}/Cargo.toml", package_dir, package_name); - let contents = std::fs::read_to_string(&cargo_toml_path) - .context(format!("Failed to read {}", cargo_toml_path))?; - - let updated = update_crate_package_fields(&contents, crate_name, new_version)?; - - std::fs::write(&cargo_toml_path, updated) - .context(format!("Failed to write {}", cargo_toml_path))?; - - Ok(()) -} - -fn update_crate_package_fields( - toml_contents: &str, - crate_name: &str, - new_version: &str, -) -> Result { - let mut doc = toml_contents - .parse::() - .context("Failed to parse TOML")?; - - let package = doc - .get_mut("package") - .and_then(|p| p.as_table_like_mut()) - .context("Failed to find [package] section")?; - - package.insert("name", toml_edit::value(crate_name)); - package.insert("version", toml_edit::value(new_version)); - package.insert("publish", toml_edit::value(true)); - - Ok(doc.to_string()) -} - -fn publish_crate(crate_name: &str, dry_run: bool) -> Result<()> { - let publish_crate_impl = |crate_name, dry_run| { - let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); - - let mut command = Command::new(&cargo); - command - .arg("publish") - .arg("--allow-dirty") - .args(["-p", crate_name]); - - if dry_run { - command.arg("--dry-run"); - } - - run_command(&mut command)?; - - anyhow::Ok(()) - }; - - if dry_run { - publish_crate_impl(crate_name, true)?; - - print!("Press Enter to publish for real (or ctrl-c to abort)..."); - io::stdout().flush()?; - - let mut input = String::new(); - io::stdin().read_line(&mut input)?; - } - - publish_crate_impl(crate_name, false)?; - - Ok(()) -} - -fn update_workspace_dependency_version( - package_name: &str, - crate_name: &str, - new_version: &str, -) -> Result<()> { - let workspace_cargo_toml_path = "Cargo.toml"; - let contents = std::fs::read_to_string(workspace_cargo_toml_path) - .context("Failed to read workspace Cargo.toml")?; - - let mut doc = contents - .parse::() - .context("Failed to parse TOML")?; - - update_dependency_version_in_doc(&mut doc, package_name, crate_name, new_version)?; - update_profile_override_in_doc(&mut doc, package_name, crate_name)?; - - std::fs::write(workspace_cargo_toml_path, doc.to_string()) - .context("Failed to write workspace Cargo.toml")?; - - Ok(()) -} - -fn update_dependency_version_in_doc( - doc: &mut toml_edit::DocumentMut, - package_name: &str, - crate_name: &str, - new_version: &str, -) -> Result<()> { - let dependency = doc - .get_mut("workspace") - .and_then(|w| w.get_mut("dependencies")) - .and_then(|d| d.get_mut(package_name)) - .context(format!( - "Failed to find {} in workspace dependencies", - package_name - ))?; - - if let Some(dep_table) = dependency.as_table_like_mut() { - dep_table.insert("version", toml_edit::value(new_version)); - dep_table.insert("package", toml_edit::value(crate_name)); - } else { - bail!("{} is not a table in workspace dependencies", package_name); - } - - Ok(()) -} - -fn update_profile_override_in_doc( - doc: &mut toml_edit::DocumentMut, - package_name: &str, - crate_name: &str, -) -> Result<()> { - if let Some(profile_dev_package) = doc - .get_mut("profile") - .and_then(|p| p.get_mut("dev")) - .and_then(|d| d.get_mut("package")) - .and_then(|p| p.as_table_like_mut()) - { - if let Some(old_entry) = profile_dev_package.get(package_name) { - let old_entry_clone = old_entry.clone(); - profile_dev_package.remove(package_name); - profile_dev_package.insert(crate_name, old_entry_clone); - } - } - - Ok(()) -} - -fn check_workspace_root() -> Result<()> { - let cwd = std::env::current_dir().context("Failed to get current directory")?; - - // Check if Cargo.toml exists in the current directory - let cargo_toml_path = cwd.join("Cargo.toml"); - if !cargo_toml_path.exists() { - bail!( - "Cargo.toml not found in current directory. Please run this command from the workspace root." - ); - } - - // Check if it's a workspace by looking for [workspace] section - let contents = - std::fs::read_to_string(&cargo_toml_path).context("Failed to read Cargo.toml")?; - - if !contents.contains("[workspace]") { - bail!( - "Current directory does not appear to be a workspace root. Please run this command from the workspace root." - ); - } - - Ok(()) -} - -fn check_git_clean() -> Result<()> { - let output = run_command( - Command::new("git") - .args(["status", "--porcelain"]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()), - )?; - - if !output.status.success() { - bail!("git status command failed"); - } - - let stdout = String::from_utf8_lossy(&output.stdout); - if !stdout.trim().is_empty() { - bail!( - "Working directory is not clean. Please commit or stash your changes before publishing." - ); - } - - Ok(()) -} - -fn run_command(command: &mut Command) -> Result { - let command_str = { - let program = command.get_program().to_string_lossy(); - let args = command - .get_args() - .map(|arg| arg.to_string_lossy()) - .collect::>() - .join(" "); - - if args.is_empty() { - program.to_string() - } else { - format!("{} {}", program, args) - } - }; - eprintln!("+ {}", command_str); - - let output = command - .spawn() - .context("failed to spawn child process")? - .wait_with_output() - .context("failed to wait for child process")?; - - if !output.status.success() { - bail!("Command failed with status {}", output.status); - } - - Ok(output) -} - -#[cfg(test)] -mod tests { - use indoc::indoc; - - use super::*; - - #[test] - fn test_update_dependency_version_in_toml() { - let input = indoc! {r#" - [workspace] - resolver = "2" - - [workspace.dependencies] - # here's a comment - collections = { path = "crates/collections" } - - util = { path = "crates/util", package = "zed-util", version = "0.1.0" } - "#}; - - let mut doc = input.parse::().unwrap(); - - update_dependency_version_in_doc(&mut doc, "collections", "gpui_collections", "0.2.0") - .unwrap(); - - let result = doc.to_string(); - - let output = indoc! {r#" - [workspace] - resolver = "2" - - [workspace.dependencies] - # here's a comment - collections = { path = "crates/collections" , version = "0.2.0", package = "gpui_collections" } - - util = { path = "crates/util", package = "zed-util", version = "0.1.0" } - "#}; - - assert_eq!(result, output); - } - - #[test] - fn test_update_crate_package_fields() { - let input = indoc! {r#" - [package] - name = "collections" - version = "0.1.0" - edition = "2021" - publish = false - # some comment about the license - license = "GPL-3.0-or-later" - - [dependencies] - serde = "1.0" - "#}; - - let result = update_crate_package_fields(input, "gpui_collections", "0.2.0").unwrap(); - - let output = indoc! {r#" - [package] - name = "gpui_collections" - version = "0.2.0" - edition = "2021" - publish = true - # some comment about the license - license = "GPL-3.0-or-later" - - [dependencies] - serde = "1.0" - "#}; - - assert_eq!(result, output); - } - - #[test] - fn test_update_profile_override_in_toml() { - let input = indoc! {r#" - [profile.dev] - split-debuginfo = "unpacked" - - [profile.dev.package] - taffy = { opt-level = 3 } - collections = { codegen-units = 256 } - refineable = { codegen-units = 256 } - util = { codegen-units = 256 } - "#}; - - let mut doc = input.parse::().unwrap(); - - update_profile_override_in_doc(&mut doc, "collections", "gpui_collections").unwrap(); - - let result = doc.to_string(); - - let output = indoc! {r#" - [profile.dev] - split-debuginfo = "unpacked" - - [profile.dev.package] - taffy = { opt-level = 3 } - refineable = { codegen-units = 256 } - util = { codegen-units = 256 } - gpui_collections = { codegen-units = 256 } - "#}; - - assert_eq!(result, output); - } -} diff --git a/tooling/xtask/src/tasks/workflows.rs b/tooling/xtask/src/tasks/workflows.rs deleted file mode 100644 index 717517402d..0000000000 --- a/tooling/xtask/src/tasks/workflows.rs +++ /dev/null @@ -1,138 +0,0 @@ -use anyhow::{Context, Result}; -use clap::Parser; -use gh_workflow::Workflow; -use std::fs; -use std::path::{Path, PathBuf}; - -mod after_release; -mod cherry_pick; -mod compare_perf; -mod danger; -mod extension_bump; -mod extension_release; -mod extension_tests; -mod extensions; -mod nix_build; -mod release_nightly; -mod run_bundling; - -mod release; -mod run_agent_evals; -mod run_tests; -mod runners; -mod steps; -mod vars; - -#[derive(Parser)] -pub struct GenerateWorkflowArgs {} - -struct WorkflowFile { - source: fn() -> Workflow, - r#type: WorkflowType, -} - -impl WorkflowFile { - fn zed(f: fn() -> Workflow) -> WorkflowFile { - WorkflowFile { - source: f, - r#type: WorkflowType::Zed, - } - } - fn extension(f: fn() -> Workflow) -> WorkflowFile { - WorkflowFile { - source: f, - r#type: WorkflowType::Extensions, - } - } - - fn generate_file(&self) -> Result<()> { - let workflow = (self.source)(); - let workflow_folder = self.r#type.folder_path(); - let workflow_name = workflow - .name - .as_ref() - .expect("Workflow must have a name at this point"); - let filename = format!( - "{}.yml", - workflow_name.rsplit("::").next().unwrap_or(workflow_name) - ); - - let workflow_path = workflow_folder.join(filename); - - let content = workflow - .to_string() - .map_err(|e| anyhow::anyhow!("{:?}: {:?}", workflow_path, e))?; - - let disclaimer = self.r#type.disclaimer(workflow_name); - - let content = [disclaimer, content].join("\n"); - fs::write(&workflow_path, content).map_err(Into::into) - } -} - -enum WorkflowType { - Zed, - Extensions, -} - -impl WorkflowType { - fn disclaimer(&self, workflow_name: &str) -> String { - format!( - concat!( - "# Generated from xtask::workflows::{}{}\n", - "# Rebuild with `cargo xtask workflows`.", - ), - workflow_name, - matches!(self, WorkflowType::Extensions) - .then_some(" within the Zed repository.") - .unwrap_or_default(), - ) - } - - fn folder_path(&self) -> PathBuf { - match self { - WorkflowType::Zed => PathBuf::from(".github/workflows"), - WorkflowType::Extensions => PathBuf::from("extensions/workflows"), - } - } -} - -pub fn run_workflows(_: GenerateWorkflowArgs) -> Result<()> { - if !Path::new("crates/zed/").is_dir() { - anyhow::bail!("xtask workflows must be ran from the project root"); - } - let workflow_dir = Path::new(".github/workflows"); - let extension_workflow_dir = Path::new("extensions/workflows"); - - let workflows = [ - WorkflowFile::zed(danger::danger), - WorkflowFile::zed(run_bundling::run_bundling), - WorkflowFile::zed(release_nightly::release_nightly), - WorkflowFile::zed(run_tests::run_tests), - WorkflowFile::zed(release::release), - WorkflowFile::zed(cherry_pick::cherry_pick), - WorkflowFile::zed(compare_perf::compare_perf), - WorkflowFile::zed(run_agent_evals::run_unit_evals), - WorkflowFile::zed(run_agent_evals::run_cron_unit_evals), - WorkflowFile::zed(run_agent_evals::run_agent_evals), - WorkflowFile::zed(after_release::after_release), - WorkflowFile::zed(extension_tests::extension_tests), - WorkflowFile::zed(extension_bump::extension_bump), - WorkflowFile::zed(extension_release::extension_release), - /* workflows used for CI/CD in extension repositories */ - WorkflowFile::extension(extensions::run_tests::run_tests), - WorkflowFile::extension(extensions::bump_version::bump_version), - WorkflowFile::extension(extensions::release_version::release_version), - ]; - - for directory in [&workflow_dir, &extension_workflow_dir] { - fs::create_dir_all(directory) - .with_context(|| format!("Failed to create directory: {}", directory.display()))?; - } - - for workflow_file in workflows { - workflow_file.generate_file()?; - } - - Ok(()) -} diff --git a/tooling/xtask/src/tasks/workflows/after_release.rs b/tooling/xtask/src/tasks/workflows/after_release.rs deleted file mode 100644 index c475617197..0000000000 --- a/tooling/xtask/src/tasks/workflows/after_release.rs +++ /dev/null @@ -1,164 +0,0 @@ -use gh_workflow::*; - -use crate::tasks::workflows::{ - release::{self, notify_on_failure}, - runners, - steps::{CommonJobConditions, NamedJob, checkout_repo, dependant_job, named}, - vars::{self, StepOutput, WorkflowInput}, -}; - -const TAG_NAME: &str = "${{ github.event.release.tag_name || inputs.tag_name }}"; -const IS_PRERELEASE: &str = "${{ github.event.release.prerelease || inputs.prerelease }}"; -const RELEASE_BODY: &str = "${{ github.event.release.body || inputs.body }}"; - -pub fn after_release() -> Workflow { - let tag_name = WorkflowInput::string("tag_name", None); - let prerelease = WorkflowInput::bool("prerelease", None); - let body = WorkflowInput::string("body", Some(String::new())); - - let refresh_zed_dev = rebuild_releases_page(); - let post_to_discord = post_to_discord(&[&refresh_zed_dev]); - let publish_winget = publish_winget(); - let create_sentry_release = create_sentry_release(); - let notify_on_failure = notify_on_failure(&[ - &refresh_zed_dev, - &post_to_discord, - &publish_winget, - &create_sentry_release, - ]); - - named::workflow() - .on(Event::default() - .release(Release::default().types(vec![ReleaseType::Published])) - .workflow_dispatch( - WorkflowDispatch::default() - .add_input(tag_name.name, tag_name.input()) - .add_input(prerelease.name, prerelease.input()) - .add_input(body.name, body.input()), - )) - .add_job(refresh_zed_dev.name, refresh_zed_dev.job) - .add_job(post_to_discord.name, post_to_discord.job) - .add_job(publish_winget.name, publish_winget.job) - .add_job(create_sentry_release.name, create_sentry_release.job) - .add_job(notify_on_failure.name, notify_on_failure.job) -} - -fn rebuild_releases_page() -> NamedJob { - fn refresh_cloud_releases() -> Step { - named::bash(format!( - "curl -fX POST https://cloud.zed.dev/releases/refresh?expect_tag={TAG_NAME}" - )) - } - - fn redeploy_zed_dev() -> Step { - named::bash("npm exec --yes -- vercel@37 --token=\"$VERCEL_TOKEN\" --scope zed-industries redeploy https://zed.dev") - .add_env(("VERCEL_TOKEN", vars::VERCEL_TOKEN)) - } - - named::job( - Job::default() - .runs_on(runners::LINUX_SMALL) - .with_repository_owner_guard() - .add_step(refresh_cloud_releases()) - .add_step(redeploy_zed_dev()), - ) -} - -fn post_to_discord(deps: &[&NamedJob]) -> NamedJob { - fn get_release_url() -> Step { - named::bash(format!( - r#"if [ "{IS_PRERELEASE}" == "true" ]; then - URL="https://zed.dev/releases/preview" -else - URL="https://zed.dev/releases/stable" -fi - -echo "URL=$URL" >> "$GITHUB_OUTPUT" -"# - )) - .id("get-release-url") - } - - fn get_content() -> Step { - named::uses( - "2428392", - "gh-truncate-string-action", - "b3ff790d21cf42af3ca7579146eedb93c8fb0757", // v1.4.1 - ) - .id("get-content") - .add_with(( - "stringToTruncate", - format!( - "📣 Zed [{TAG_NAME}](<${{{{ steps.get-release-url.outputs.URL }}}}>) was just released!\n\n{RELEASE_BODY}\n" - ), - )) - .add_with(("maxLength", 2000)) - .add_with(("truncationSymbol", "...")) - } - - fn discord_webhook_action() -> Step { - named::uses( - "tsickert", - "discord-webhook", - "c840d45a03a323fbc3f7507ac7769dbd91bfb164", // v5.3.0 - ) - .add_with(("webhook-url", vars::DISCORD_WEBHOOK_RELEASE_NOTES)) - .add_with(("content", "${{ steps.get-content.outputs.string }}")) - } - let job = dependant_job(deps) - .runs_on(runners::LINUX_SMALL) - .with_repository_owner_guard() - .add_step(get_release_url()) - .add_step(get_content()) - .add_step(discord_webhook_action()); - named::job(job) -} - -fn publish_winget() -> NamedJob { - fn set_package_name() -> (Step, StepOutput) { - let script = format!( - r#"if ("{IS_PRERELEASE}" -eq "true") {{ - $PACKAGE_NAME = "ZedIndustries.Zed.Preview" -}} else {{ - $PACKAGE_NAME = "ZedIndustries.Zed" -}} - -echo "PACKAGE_NAME=$PACKAGE_NAME" >> $env:GITHUB_OUTPUT -"# - ); - let step = named::pwsh(&script).id("set-package-name"); - - let output = StepOutput::new(&step, "PACKAGE_NAME"); - (step, output) - } - - fn winget_releaser(package_name: &StepOutput) -> Step { - named::uses( - "vedantmgoyal9", - "winget-releaser", - "19e706d4c9121098010096f9c495a70a7518b30f", // v2 - ) - .add_with(("identifier", package_name.to_string())) - .add_with(("release-tag", TAG_NAME)) - .add_with(("max-versions-to-keep", 5)) - .add_with(("token", vars::WINGET_TOKEN)) - } - - let (set_package_name, package_name) = set_package_name(); - - named::job( - Job::default() - .runs_on(runners::WINDOWS_DEFAULT) - .add_step(set_package_name) - .add_step(winget_releaser(&package_name)), - ) -} - -fn create_sentry_release() -> NamedJob { - let job = Job::default() - .runs_on(runners::LINUX_SMALL) - .with_repository_owner_guard() - .add_step(checkout_repo()) - .add_step(release::create_sentry_release()); - named::job(job) -} diff --git a/tooling/xtask/src/tasks/workflows/cherry_pick.rs b/tooling/xtask/src/tasks/workflows/cherry_pick.rs deleted file mode 100644 index 105bf74c41..0000000000 --- a/tooling/xtask/src/tasks/workflows/cherry_pick.rs +++ /dev/null @@ -1,66 +0,0 @@ -use gh_workflow::*; - -use crate::tasks::workflows::{ - runners, - steps::{self, NamedJob, named}, - vars::{self, StepOutput, WorkflowInput}, -}; - -pub fn cherry_pick() -> Workflow { - let branch = WorkflowInput::string("branch", None); - let commit = WorkflowInput::string("commit", None); - let channel = WorkflowInput::string("channel", None); - let pr_number = WorkflowInput::string("pr_number", None); - let cherry_pick = run_cherry_pick(&branch, &commit, &channel); - named::workflow() - .run_name(format!("cherry_pick to {channel} #{pr_number}")) - .on(Event::default().workflow_dispatch( - WorkflowDispatch::default() - .add_input(commit.name, commit.input()) - .add_input(branch.name, branch.input()) - .add_input(channel.name, channel.input()) - .add_input(pr_number.name, pr_number.input()), - )) - .add_job(cherry_pick.name, cherry_pick.job) -} - -fn run_cherry_pick( - branch: &WorkflowInput, - commit: &WorkflowInput, - channel: &WorkflowInput, -) -> NamedJob { - fn authenticate_as_zippy() -> (Step, StepOutput) { - let step = named::uses( - "actions", - "create-github-app-token", - "bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1", - ) // v2 - .add_with(("app-id", vars::ZED_ZIPPY_APP_ID)) - .add_with(("private-key", vars::ZED_ZIPPY_APP_PRIVATE_KEY)) - .id("get-app-token"); - let output = StepOutput::new(&step, "token"); - (step, output) - } - - fn cherry_pick( - branch: &WorkflowInput, - commit: &WorkflowInput, - channel: &WorkflowInput, - token: &StepOutput, - ) -> Step { - named::bash(&format!("./script/cherry-pick {branch} {commit} {channel}")) - .add_env(("GIT_COMMITTER_NAME", "Zed Zippy")) - .add_env(("GIT_COMMITTER_EMAIL", "hi@zed.dev")) - .add_env(("GITHUB_TOKEN", token)) - } - - let (authenticate, token) = authenticate_as_zippy(); - - named::job( - Job::default() - .runs_on(runners::LINUX_SMALL) - .add_step(steps::checkout_repo()) - .add_step(authenticate) - .add_step(cherry_pick(branch, commit, channel, &token)), - ) -} diff --git a/tooling/xtask/src/tasks/workflows/compare_perf.rs b/tooling/xtask/src/tasks/workflows/compare_perf.rs deleted file mode 100644 index 1d111acc4f..0000000000 --- a/tooling/xtask/src/tasks/workflows/compare_perf.rs +++ /dev/null @@ -1,67 +0,0 @@ -use gh_workflow::*; - -use crate::tasks::workflows::run_bundling::upload_artifact; -use crate::tasks::workflows::steps::FluentBuilder; -use crate::tasks::workflows::{ - runners, - steps::{self, NamedJob, named}, - vars::WorkflowInput, -}; - -pub fn compare_perf() -> Workflow { - let head = WorkflowInput::string("head", None); - let base = WorkflowInput::string("base", None); - let crate_name = WorkflowInput::string("crate_name", Some("".to_owned())); - let run_perf = run_perf(&base, &head, &crate_name); - named::workflow() - .on(Event::default().workflow_dispatch( - WorkflowDispatch::default() - .add_input(head.name, head.input()) - .add_input(base.name, base.input()) - .add_input(crate_name.name, crate_name.input()), - )) - .add_job(run_perf.name, run_perf.job) -} - -pub fn run_perf( - base: &WorkflowInput, - head: &WorkflowInput, - crate_name: &WorkflowInput, -) -> NamedJob { - fn cargo_perf_test(ref_name: &WorkflowInput, crate_name: &WorkflowInput) -> Step { - named::bash(&format!( - " - if [ -n \"{crate_name}\" ]; then - cargo perf-test -p {crate_name} -- --json={ref_name}; - else - cargo perf-test -p vim -- --json={ref_name}; - fi" - )) - } - - fn install_hyperfine() -> Step { - named::uses("taiki-e", "install-action", "hyperfine") - } - - fn compare_runs(head: &WorkflowInput, base: &WorkflowInput) -> Step { - named::bash(&format!( - "cargo perf-compare --save=results.md {base} {head}" - )) - } - - named::job( - Job::default() - .runs_on(runners::LINUX_DEFAULT) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(runners::Platform::Linux)) - .map(steps::install_linux_dependencies) - .add_step(install_hyperfine()) - .add_step(steps::git_checkout(base)) - .add_step(cargo_perf_test(base, crate_name)) - .add_step(steps::git_checkout(head)) - .add_step(cargo_perf_test(head, crate_name)) - .add_step(compare_runs(head, base)) - .add_step(upload_artifact("results.md")) - .add_step(steps::cleanup_cargo_config(runners::Platform::Linux)), - ) -} diff --git a/tooling/xtask/src/tasks/workflows/danger.rs b/tooling/xtask/src/tasks/workflows/danger.rs deleted file mode 100644 index 8b3bf0ac3a..0000000000 --- a/tooling/xtask/src/tasks/workflows/danger.rs +++ /dev/null @@ -1,57 +0,0 @@ -use gh_workflow::*; - -use crate::tasks::workflows::steps::{CommonJobConditions, NamedJob, named}; - -use super::{runners, steps}; - -/// Generates the danger.yml workflow -pub fn danger() -> Workflow { - let danger = danger_job(); - - named::workflow() - .on( - Event::default().pull_request(PullRequest::default().add_branch("main").types([ - PullRequestType::Opened, - PullRequestType::Synchronize, - PullRequestType::Reopened, - PullRequestType::Edited, - ])), - ) - .add_job(danger.name, danger.job) -} - -fn danger_job() -> NamedJob { - pub fn install_deps() -> Step { - named::bash("pnpm install --dir script/danger") - } - - pub fn run() -> Step { - named::bash("pnpm run --dir script/danger danger ci") - // This GitHub token is not used, but the value needs to be here to prevent - // Danger from throwing an error. - .add_env(("GITHUB_TOKEN", "not_a_real_token")) - // All requests are instead proxied through an instance of - // https://github.com/maxdeviant/danger-proxy that allows Danger to securely - // authenticate with GitHub while still being able to run on PRs from forks. - .add_env(( - "DANGER_GITHUB_API_BASE_URL", - "https://danger-proxy.fly.dev/github", - )) - } - - NamedJob { - name: "danger".to_string(), - job: Job::default() - .with_repository_owner_guard() - .runs_on(runners::LINUX_SMALL) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_pnpm()) - .add_step( - steps::setup_node() - .add_with(("cache", "pnpm")) - .add_with(("cache-dependency-path", "script/danger/pnpm-lock.yaml")), - ) - .add_step(install_deps()) - .add_step(run()), - } -} diff --git a/tooling/xtask/src/tasks/workflows/extension_bump.rs b/tooling/xtask/src/tasks/workflows/extension_bump.rs deleted file mode 100644 index 34fcf80990..0000000000 --- a/tooling/xtask/src/tasks/workflows/extension_bump.rs +++ /dev/null @@ -1,306 +0,0 @@ -use gh_workflow::*; -use indoc::indoc; - -use crate::tasks::workflows::{ - extension_release::extension_workflow_secrets, - extension_tests::{self}, - runners, - steps::{ - self, CommonJobConditions, DEFAULT_REPOSITORY_OWNER_GUARD, FluentBuilder, NamedJob, named, - }, - vars::{ - JobOutput, StepOutput, WorkflowInput, WorkflowSecret, one_workflow_per_non_main_branch, - }, -}; - -const VERSION_CHECK: &str = r#"sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml"#; - -// This is used by various extensions repos in the zed-extensions org to bump extension versions. -pub(crate) fn extension_bump() -> Workflow { - let bump_type = WorkflowInput::string("bump-type", Some("patch".to_owned())); - // TODO: Ideally, this would have a default of `false`, but this is currently not - // supported in gh-workflows - let force_bump = WorkflowInput::bool("force-bump", None); - - let (app_id, app_secret) = extension_workflow_secrets(); - let (check_bump_needed, needs_bump, current_version) = check_bump_needed(); - - let needs_bump = needs_bump.as_job_output(&check_bump_needed); - let current_version = current_version.as_job_output(&check_bump_needed); - - let dependencies = [&check_bump_needed]; - let bump_version = bump_extension_version( - &dependencies, - ¤t_version, - &bump_type, - &needs_bump, - &force_bump, - &app_id, - &app_secret, - ); - let create_label = create_version_label( - &dependencies, - &needs_bump, - ¤t_version, - &app_id, - &app_secret, - ); - - named::workflow() - .add_event( - Event::default().workflow_call( - WorkflowCall::default() - .add_input(bump_type.name, bump_type.call_input()) - .add_input(force_bump.name, force_bump.call_input()) - .secrets([ - (app_id.name.to_owned(), app_id.secret_configuration()), - ( - app_secret.name.to_owned(), - app_secret.secret_configuration(), - ), - ]), - ), - ) - .concurrency(one_workflow_per_non_main_branch()) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("RUST_BACKTRACE", 1)) - .add_env(("CARGO_INCREMENTAL", 0)) - .add_env(( - "ZED_EXTENSION_CLI_SHA", - extension_tests::ZED_EXTENSION_CLI_SHA, - )) - .add_job(check_bump_needed.name, check_bump_needed.job) - .add_job(bump_version.name, bump_version.job) - .add_job(create_label.name, create_label.job) -} - -fn check_bump_needed() -> (NamedJob, StepOutput, StepOutput) { - let (compare_versions, version_changed, current_version) = compare_versions(); - - let job = Job::default() - .with_repository_owner_guard() - .outputs([ - (version_changed.name.to_owned(), version_changed.to_string()), - ( - current_version.name.to_string(), - current_version.to_string(), - ), - ]) - .runs_on(runners::LINUX_SMALL) - .timeout_minutes(1u32) - .add_step(steps::checkout_repo().add_with(("fetch-depth", 0))) - .add_step(compare_versions); - - (named::job(job), version_changed, current_version) -} - -fn create_version_label( - dependencies: &[&NamedJob], - needs_bump: &JobOutput, - current_version: &JobOutput, - app_id: &WorkflowSecret, - app_secret: &WorkflowSecret, -) -> NamedJob { - let (generate_token, generated_token) = generate_token(app_id, app_secret, None); - let job = steps::dependant_job(dependencies) - .cond(Expression::new(format!( - "{DEFAULT_REPOSITORY_OWNER_GUARD} && github.event_name == 'push' && github.ref == 'refs/heads/main' && {} == 'false'", - needs_bump.expr(), - ))) - .runs_on(runners::LINUX_LARGE) - .timeout_minutes(1u32) - .add_step(generate_token) - .add_step(steps::checkout_repo()) - .add_step(create_version_tag(current_version, generated_token)); - - named::job(job) -} - -fn create_version_tag(current_version: &JobOutput, generated_token: StepOutput) -> Step { - named::uses("actions", "github-script", "v7").with( - Input::default() - .add( - "script", - format!( - indoc! {r#" - github.rest.git.createRef({{ - owner: context.repo.owner, - repo: context.repo.repo, - ref: 'refs/tags/v{}', - sha: context.sha - }})"# - }, - current_version - ), - ) - .add("github-token", generated_token.to_string()), - ) -} - -/// Compares the current and previous commit and checks whether versions changed inbetween. -fn compare_versions() -> (Step, StepOutput, StepOutput) { - let check_needs_bump = named::bash(format!( - indoc! { - r#" - CURRENT_VERSION="$({})" - PR_PARENT_SHA="${{{{ github.event.pull_request.head.sha }}}}" - - if [[ -n "$PR_PARENT_SHA" ]]; then - git checkout "$PR_PARENT_SHA" - elif BRANCH_PARENT_SHA="$(git merge-base origin/main origin/zed-zippy-autobump)"; then - git checkout "$BRANCH_PARENT_SHA" - else - git checkout "$(git log -1 --format=%H)"~1 - fi - - PARENT_COMMIT_VERSION="$({})" - - [[ "$CURRENT_VERSION" == "$PARENT_COMMIT_VERSION" ]] && \ - echo "needs_bump=true" >> "$GITHUB_OUTPUT" || \ - echo "needs_bump=false" >> "$GITHUB_OUTPUT" - - echo "current_version=${{CURRENT_VERSION}}" >> "$GITHUB_OUTPUT" - "# - }, - VERSION_CHECK, VERSION_CHECK - )) - .id("compare-versions-check"); - - let needs_bump = StepOutput::new(&check_needs_bump, "needs_bump"); - let current_version = StepOutput::new(&check_needs_bump, "current_version"); - - (check_needs_bump, needs_bump, current_version) -} - -fn bump_extension_version( - dependencies: &[&NamedJob], - current_version: &JobOutput, - bump_type: &WorkflowInput, - needs_bump: &JobOutput, - force_bump: &WorkflowInput, - app_id: &WorkflowSecret, - app_secret: &WorkflowSecret, -) -> NamedJob { - let (generate_token, generated_token) = generate_token(app_id, app_secret, None); - let (bump_version, new_version) = bump_version(current_version, bump_type); - - let job = steps::dependant_job(dependencies) - .cond(Expression::new(format!( - "{DEFAULT_REPOSITORY_OWNER_GUARD} &&\n({} == 'true' || {} == 'true')", - force_bump.expr(), - needs_bump.expr(), - ))) - .runs_on(runners::LINUX_LARGE) - .timeout_minutes(1u32) - .add_step(generate_token) - .add_step(steps::checkout_repo()) - .add_step(install_bump_2_version()) - .add_step(bump_version) - .add_step(create_pull_request(new_version, generated_token)); - - named::job(job) -} - -pub(crate) fn generate_token( - app_id: &WorkflowSecret, - app_secret: &WorkflowSecret, - repository_target: Option, -) -> (Step, StepOutput) { - let step = named::uses("actions", "create-github-app-token", "v2") - .id("generate-token") - .add_with( - Input::default() - .add("app-id", app_id.to_string()) - .add("private-key", app_secret.to_string()) - .when_some( - repository_target, - |input, - RepositoryTarget { - owner, - repositories, - }| { - input.add("owner", owner).add("repositories", repositories) - }, - ), - ); - - let generated_token = StepOutput::new(&step, "token"); - - (step, generated_token) -} - -fn install_bump_2_version() -> Step { - named::run(runners::Platform::Linux, "pip install bump2version") -} - -fn bump_version(current_version: &JobOutput, bump_type: &WorkflowInput) -> (Step, StepOutput) { - let step = named::bash(format!( - indoc! {r#" - OLD_VERSION="{}" - - BUMP_FILES=("extension.toml") - if [[ -f "Cargo.toml" ]]; then - BUMP_FILES+=("Cargo.toml") - fi - - bump2version --verbose --current-version "$OLD_VERSION" --no-configured-files {} "${{BUMP_FILES[@]}}" - - if [[ -f "Cargo.toml" ]]; then - cargo update --workspace - fi - - NEW_VERSION="$({})" - - echo "new_version=${{NEW_VERSION}}" >> "$GITHUB_OUTPUT" - "# - }, - current_version, bump_type, VERSION_CHECK - )) - .id("bump-version"); - - let new_version = StepOutput::new(&step, "new_version"); - (step, new_version) -} - -fn create_pull_request(new_version: StepOutput, generated_token: StepOutput) -> Step { - let formatted_version = format!("v{}", new_version); - - named::uses("peter-evans", "create-pull-request", "v7").with( - Input::default() - .add("title", format!("Bump version to {}", new_version)) - .add( - "body", - format!( - "This PR bumps the version of this extension to {}", - formatted_version - ), - ) - .add( - "commit-message", - format!("Bump version to {}", formatted_version), - ) - .add("branch", "zed-zippy-autobump") - .add( - "committer", - "zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>", - ) - .add("base", "main") - .add("delete-branch", true) - .add("token", generated_token.to_string()) - .add("sign-commits", true), - ) -} - -pub(crate) struct RepositoryTarget { - owner: String, - repositories: String, -} - -impl RepositoryTarget { - pub fn new(owner: T, repositories: &[&str]) -> Self { - Self { - owner: owner.to_string(), - repositories: repositories.join("\n"), - } - } -} diff --git a/tooling/xtask/src/tasks/workflows/extension_release.rs b/tooling/xtask/src/tasks/workflows/extension_release.rs deleted file mode 100644 index c55fed0cb8..0000000000 --- a/tooling/xtask/src/tasks/workflows/extension_release.rs +++ /dev/null @@ -1,72 +0,0 @@ -use gh_workflow::{Event, Job, Run, Step, Use, Workflow, WorkflowCall}; -use indoc::indoc; - -use crate::tasks::workflows::{ - extension_bump::{RepositoryTarget, generate_token}, - runners, - steps::{CommonJobConditions, NamedJob, checkout_repo, named}, - vars::{StepOutput, WorkflowSecret}, -}; - -pub(crate) fn extension_release() -> Workflow { - let (app_id, app_secret) = extension_workflow_secrets(); - - let create_release = create_release(&app_id, &app_secret); - named::workflow() - .on( - Event::default().workflow_call(WorkflowCall::default().secrets([ - (app_id.name.to_owned(), app_id.secret_configuration()), - ( - app_secret.name.to_owned(), - app_secret.secret_configuration(), - ), - ])), - ) - .add_job(create_release.name, create_release.job) -} - -fn create_release(app_id: &WorkflowSecret, app_secret: &WorkflowSecret) -> NamedJob { - let extension_registry = RepositoryTarget::new("zed-industries", &["extensions"]); - let (generate_token, generated_token) = - generate_token(&app_id, &app_secret, Some(extension_registry)); - let (get_extension_id, extension_id) = get_extension_id(); - - let job = Job::default() - .with_repository_owner_guard() - .runs_on(runners::LINUX_LARGE) - .add_step(generate_token) - .add_step(checkout_repo()) - .add_step(get_extension_id) - .add_step(release_action(extension_id, generated_token)); - - named::job(job) -} - -fn get_extension_id() -> (Step, StepOutput) { - let step = named::bash(indoc! { - r#" - EXTENSION_ID="$(sed -n 's/id = \"\(.*\)\"/\1/p' < extension.toml)" - - echo "extension_id=${EXTENSION_ID}" >> "$GITHUB_OUTPUT" - "#}) - .id("get-extension-id"); - - let extension_id = StepOutput::new(&step, "extension_id"); - - (step, extension_id) -} - -fn release_action(extension_id: StepOutput, generated_token: StepOutput) -> Step { - named::uses("huacnlee", "zed-extension-action", "v2") - .add_with(("extension-name", extension_id.to_string())) - .add_with(("push-to", "zed-industries/extensions")) - .add_env(("COMMITTER_TOKEN", generated_token.to_string())) -} - -pub(crate) fn extension_workflow_secrets() -> (WorkflowSecret, WorkflowSecret) { - let app_id = WorkflowSecret::new("app-id", "The app ID used to create the PR"); - let app_secret = - WorkflowSecret::new("app-secret", "The app secret for the corresponding app ID"); - - (app_id, app_secret) -} diff --git a/tooling/xtask/src/tasks/workflows/extension_tests.rs b/tooling/xtask/src/tasks/workflows/extension_tests.rs deleted file mode 100644 index 4805591214..0000000000 --- a/tooling/xtask/src/tasks/workflows/extension_tests.rs +++ /dev/null @@ -1,114 +0,0 @@ -use gh_workflow::*; -use indoc::indoc; - -use crate::tasks::workflows::{ - run_tests::{orchestrate, tests_pass}, - runners, - steps::{self, CommonJobConditions, FluentBuilder, NamedJob, named}, - vars::{PathCondition, StepOutput, one_workflow_per_non_main_branch}, -}; - -pub(crate) const ZED_EXTENSION_CLI_SHA: &str = "7cfce605704d41ca247e3f84804bf323f6c6caaf"; - -// This is used by various extensions repos in the zed-extensions org to run automated tests. -pub(crate) fn extension_tests() -> Workflow { - let should_check_rust = PathCondition::new("check_rust", r"^(Cargo.lock|Cargo.toml|.*\.rs)$"); - let should_check_extension = PathCondition::new("check_extension", r"^.*\.scm$"); - - let orchestrate = orchestrate(&[&should_check_rust, &should_check_extension]); - - let jobs = [ - orchestrate, - should_check_rust.guard(check_rust()), - should_check_extension.guard(check_extension()), - ]; - - let tests_pass = tests_pass(&jobs); - - named::workflow() - .add_event(Event::default().workflow_call(WorkflowCall::default())) - .concurrency(one_workflow_per_non_main_branch()) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("RUST_BACKTRACE", 1)) - .add_env(("CARGO_INCREMENTAL", 0)) - .add_env(("ZED_EXTENSION_CLI_SHA", ZED_EXTENSION_CLI_SHA)) - .map(|workflow| { - jobs.into_iter() - .chain([tests_pass]) - .fold(workflow, |workflow, job| { - workflow.add_job(job.name, job.job) - }) - }) -} - -fn run_clippy() -> Step { - named::bash("cargo clippy --release --all-targets --all-features -- --deny warnings") -} - -fn check_rust() -> NamedJob { - let job = Job::default() - .with_repository_owner_guard() - .runs_on(runners::LINUX_DEFAULT) - .timeout_minutes(3u32) - .add_step(steps::checkout_repo()) - .add_step(steps::cache_rust_dependencies_namespace()) - .add_step(steps::cargo_fmt()) - .add_step(run_clippy()) - .add_step(steps::cargo_install_nextest()) - .add_step( - steps::cargo_nextest(runners::Platform::Linux).add_env(("NEXTEST_NO_TESTS", "warn")), - ); - - named::job(job) -} - -pub(crate) fn check_extension() -> NamedJob { - let (cache_download, cache_hit) = cache_zed_extension_cli(); - let job = Job::default() - .with_repository_owner_guard() - .runs_on(runners::LINUX_SMALL) - .timeout_minutes(2u32) - .add_step(steps::checkout_repo()) - .add_step(cache_download) - .add_step(download_zed_extension_cli(cache_hit)) - .add_step(check()); - - named::job(job) -} - -pub fn cache_zed_extension_cli() -> (Step, StepOutput) { - let step = named::uses( - "actions", - "cache", - "0057852bfaa89a56745cba8c7296529d2fc39830", - ) - .id("cache-zed-extension-cli") - .with( - Input::default() - .add("path", "zed-extension") - .add("key", "zed-extension-${{ env.ZED_EXTENSION_CLI_SHA }}"), - ); - let output = StepOutput::new(&step, "cache-hit"); - (step, output) -} - -pub fn download_zed_extension_cli(cache_hit: StepOutput) -> Step { - named::bash( - indoc! { - r#" - wget --quiet "https://zed-extension-cli.nyc3.digitaloceanspaces.com/$ZED_EXTENSION_CLI_SHA/x86_64-unknown-linux-gnu/zed-extension" - chmod +x zed-extension - "#, - } - ).if_condition(Expression::new(format!("{} != 'true'", cache_hit.expr()))) -} - -pub fn check() -> Step { - named::bash(indoc! { - r#" - mkdir -p /tmp/ext-scratch - mkdir -p /tmp/ext-output - ./zed-extension --source-dir . --scratch-dir /tmp/ext-scratch --output-dir /tmp/ext-output - "# - }) -} diff --git a/tooling/xtask/src/tasks/workflows/extensions/bump_version.rs b/tooling/xtask/src/tasks/workflows/extensions/bump_version.rs deleted file mode 100644 index 1564fef448..0000000000 --- a/tooling/xtask/src/tasks/workflows/extensions/bump_version.rs +++ /dev/null @@ -1,103 +0,0 @@ -use gh_workflow::{ - Event, Expression, Input, Job, PullRequest, PullRequestType, Push, Run, Step, UsesJob, - Workflow, WorkflowDispatch, -}; -use indexmap::IndexMap; -use indoc::indoc; - -use crate::tasks::workflows::{ - runners, - steps::{NamedJob, named}, - vars::{self, JobOutput, StepOutput, one_workflow_per_non_main_branch_and_token}, -}; - -pub(crate) fn bump_version() -> Workflow { - let (determine_bump_type, bump_type) = determine_bump_type(); - let bump_type = bump_type.as_job_output(&determine_bump_type); - - let call_bump_version = call_bump_version(&determine_bump_type, bump_type); - - named::workflow() - .on(Event::default() - .push( - Push::default() - .add_branch("main") - .add_ignored_path(".github/**"), - ) - .pull_request(PullRequest::default().add_type(PullRequestType::Labeled)) - .workflow_dispatch(WorkflowDispatch::default())) - .concurrency(one_workflow_per_non_main_branch_and_token("labels")) - .add_job(determine_bump_type.name, determine_bump_type.job) - .add_job(call_bump_version.name, call_bump_version.job) -} - -pub(crate) fn call_bump_version( - depending_job: &NamedJob, - bump_type: JobOutput, -) -> NamedJob { - let job = Job::default() - .cond(Expression::new(format!( - "github.event.action != 'labeled' || {} != 'patch'", - bump_type.expr() - ))) - .uses( - "zed-industries", - "zed", - ".github/workflows/extension_bump.yml", - "main", - ) - .add_need(depending_job.name.clone()) - .with( - Input::default() - .add("bump-type", bump_type.to_string()) - .add("force-bump", true), - ) - .secrets(IndexMap::from([ - ("app-id".to_owned(), vars::ZED_ZIPPY_APP_ID.to_owned()), - ( - "app-secret".to_owned(), - vars::ZED_ZIPPY_APP_PRIVATE_KEY.to_owned(), - ), - ])); - - named::job(job) -} - -fn determine_bump_type() -> (NamedJob, StepOutput) { - let (get_bump_type, output) = get_bump_type(); - let job = Job::default() - .runs_on(runners::LINUX_DEFAULT) - .add_step(get_bump_type) - .outputs([(output.name.to_owned(), output.to_string())]); - (named::job(job), output) -} - -fn get_bump_type() -> (Step, StepOutput) { - let step = named::bash( - indoc! {r#" - if [ "$HAS_MAJOR_LABEL" = "true" ]; then - bump_type="major" - elif [ "$HAS_MINOR_LABEL" = "true" ]; then - bump_type="minor" - else - bump_type="patch" - fi - echo "bump_type=$bump_type" >> $GITHUB_OUTPUT - "#}, - ) - .add_env(("HAS_MAJOR_LABEL", - indoc!{ - "${{ (github.event.action == 'labeled' && github.event.label.name == 'major') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'major')) }}" - })) - .add_env(("HAS_MINOR_LABEL", - indoc!{ - "${{ (github.event.action == 'labeled' && github.event.label.name == 'minor') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'minor')) }}" - })) - .id("get-bump-type"); - - let step_output = StepOutput::new(&step, "bump_type"); - - (step, step_output) -} diff --git a/tooling/xtask/src/tasks/workflows/extensions/mod.rs b/tooling/xtask/src/tasks/workflows/extensions/mod.rs deleted file mode 100644 index d26100f09d..0000000000 --- a/tooling/xtask/src/tasks/workflows/extensions/mod.rs +++ /dev/null @@ -1,24 +0,0 @@ -use gh_workflow::{Job, UsesJob}; -use indexmap::IndexMap; - -use crate::tasks::workflows::vars; - -pub(crate) mod bump_version; -pub(crate) mod release_version; -pub(crate) mod run_tests; - -pub(crate) trait WithAppSecrets: Sized { - fn with_app_secrets(self) -> Self; -} - -impl WithAppSecrets for Job { - fn with_app_secrets(self) -> Self { - self.secrets(IndexMap::from([ - ("app-id".to_owned(), vars::ZED_ZIPPY_APP_ID.to_owned()), - ( - "app-secret".to_owned(), - vars::ZED_ZIPPY_APP_PRIVATE_KEY.to_owned(), - ), - ])) - } -} diff --git a/tooling/xtask/src/tasks/workflows/extensions/release_version.rs b/tooling/xtask/src/tasks/workflows/extensions/release_version.rs deleted file mode 100644 index ebeb6959a9..0000000000 --- a/tooling/xtask/src/tasks/workflows/extensions/release_version.rs +++ /dev/null @@ -1,26 +0,0 @@ -use gh_workflow::{Event, Job, Push, UsesJob, Workflow}; - -use crate::tasks::workflows::{ - extensions::WithAppSecrets, - steps::{NamedJob, named}, -}; - -pub(crate) fn release_version() -> Workflow { - let create_release = call_release_version(); - named::workflow() - .on(Event::default().push(Push::default().add_tag("v**"))) - .add_job(create_release.name, create_release.job) -} - -pub(crate) fn call_release_version() -> NamedJob { - let job = Job::default() - .uses( - "zed-industries", - "zed", - ".github/workflows/extension_release.yml", - "main", - ) - .with_app_secrets(); - - named::job(job) -} diff --git a/tooling/xtask/src/tasks/workflows/extensions/run_tests.rs b/tooling/xtask/src/tasks/workflows/extensions/run_tests.rs deleted file mode 100644 index 885a8fd09f..0000000000 --- a/tooling/xtask/src/tasks/workflows/extensions/run_tests.rs +++ /dev/null @@ -1,27 +0,0 @@ -use gh_workflow::{Event, Job, PullRequest, Push, UsesJob, Workflow}; - -use crate::tasks::workflows::{ - steps::{NamedJob, named}, - vars::one_workflow_per_non_main_branch_and_token, -}; - -pub(crate) fn run_tests() -> Workflow { - let call_extension_tests = call_extension_tests(); - named::workflow() - .on(Event::default() - .pull_request(PullRequest::default().add_branch("**")) - .push(Push::default().add_branch("main"))) - .concurrency(one_workflow_per_non_main_branch_and_token("pr")) - .add_job(call_extension_tests.name, call_extension_tests.job) -} - -pub(crate) fn call_extension_tests() -> NamedJob { - let job = Job::default().uses( - "zed-industries", - "zed", - ".github/workflows/extension_tests.yml", - "main", - ); - - named::job(job) -} diff --git a/tooling/xtask/src/tasks/workflows/nix_build.rs b/tooling/xtask/src/tasks/workflows/nix_build.rs deleted file mode 100644 index ff98852d19..0000000000 --- a/tooling/xtask/src/tasks/workflows/nix_build.rs +++ /dev/null @@ -1,104 +0,0 @@ -use crate::tasks::workflows::{ - runners::{Arch, Platform}, - steps::{CommonJobConditions, NamedJob}, -}; - -use super::{runners, steps, steps::named, vars}; -use gh_workflow::*; -use indoc::indoc; - -pub(crate) fn build_nix( - platform: Platform, - arch: Arch, - flake_output: &str, - cachix_filter: Option<&str>, - deps: &[&NamedJob], -) -> NamedJob { - // on our macs we manually install nix. for some reason the cachix action is running - // under a non-login /bin/bash shell which doesn't source the proper script to add the - // nix profile to PATH, so we manually add them here - pub fn set_path() -> Step { - named::bash(indoc! {r#" - echo "/nix/var/nix/profiles/default/bin" >> "$GITHUB_PATH" - echo "/Users/administrator/.nix-profile/bin" >> "$GITHUB_PATH" - "#}) - } - - pub fn install_nix() -> Step { - named::uses( - "cachix", - "install-nix-action", - "02a151ada4993995686f9ed4f1be7cfbb229e56f", // v31 - ) - .add_with(("github_access_token", vars::GITHUB_TOKEN)) - } - - pub fn cachix_action(cachix_filter: Option<&str>) -> Step { - let mut step = named::uses( - "cachix", - "cachix-action", - "0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad", // v16 - ) - .add_with(("name", "zed")) - .add_with(("authToken", vars::CACHIX_AUTH_TOKEN)) - .add_with(("cachixArgs", "-v")); - if let Some(cachix_filter) = cachix_filter { - step = step.add_with(("pushFilter", cachix_filter)); - } - step - } - - pub fn build(flake_output: &str) -> Step { - named::bash(&format!( - "nix build .#{} -L --accept-flake-config", - flake_output - )) - } - - pub fn limit_store() -> Step { - named::bash(indoc! {r#" - if [ "$(du -sm /nix/store | cut -f1)" -gt 50000 ]; then - nix-collect-garbage -d || true - fi"# - }) - } - - let runner = match platform { - Platform::Windows => unimplemented!(), - Platform::Linux => runners::LINUX_X86_BUNDLER, - Platform::Mac => runners::MAC_DEFAULT, - }; - let mut job = Job::default() - .timeout_minutes(60u32) - .continue_on_error(true) - .with_repository_owner_guard() - .runs_on(runner) - .add_env(("ZED_CLIENT_CHECKSUM_SEED", vars::ZED_CLIENT_CHECKSUM_SEED)) - .add_env(("ZED_MINIDUMP_ENDPOINT", vars::ZED_SENTRY_MINIDUMP_ENDPOINT)) - .add_env(( - "ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON", - vars::ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON, - )) - .add_env(("GIT_LFS_SKIP_SMUDGE", "1")) // breaks the livekit rust sdk examples which we don't actually depend on - .add_step(steps::checkout_repo()); - - if deps.len() > 0 { - job = job.needs(deps.iter().map(|d| d.name.clone()).collect::>()); - } - - job = if platform == Platform::Linux { - job.add_step(install_nix()) - .add_step(cachix_action(cachix_filter)) - .add_step(build(&flake_output)) - } else { - job.add_step(set_path()) - .add_step(cachix_action(cachix_filter)) - .add_step(build(&flake_output)) - .add_step(limit_store()) - }; - - NamedJob { - name: format!("build_nix_{platform}_{arch}"), - job, - } -} diff --git a/tooling/xtask/src/tasks/workflows/release.rs b/tooling/xtask/src/tasks/workflows/release.rs deleted file mode 100644 index e06a713401..0000000000 --- a/tooling/xtask/src/tasks/workflows/release.rs +++ /dev/null @@ -1,195 +0,0 @@ -use gh_workflow::{Event, Expression, Push, Run, Step, Use, Workflow}; - -use crate::tasks::workflows::{ - run_bundling::{bundle_linux, bundle_mac, bundle_windows}, - run_tests, - runners::{self, Arch}, - steps::{self, FluentBuilder, NamedJob, dependant_job, named, release_job}, - vars::{self, assets}, -}; - -pub(crate) fn release() -> Workflow { - let macos_tests = run_tests::run_platform_tests(runners::Platform::Mac); - let linux_tests = run_tests::run_platform_tests(runners::Platform::Linux); - let windows_tests = run_tests::run_platform_tests(runners::Platform::Windows); - let check_scripts = run_tests::check_scripts(); - - let create_draft_release = create_draft_release(); - - let bundle = ReleaseBundleJobs { - linux_aarch64: bundle_linux(Arch::AARCH64, None, &[&linux_tests, &check_scripts]), - linux_x86_64: bundle_linux(Arch::X86_64, None, &[&linux_tests, &check_scripts]), - mac_aarch64: bundle_mac(Arch::AARCH64, None, &[&macos_tests, &check_scripts]), - mac_x86_64: bundle_mac(Arch::X86_64, None, &[&macos_tests, &check_scripts]), - windows_aarch64: bundle_windows(Arch::AARCH64, None, &[&windows_tests, &check_scripts]), - windows_x86_64: bundle_windows(Arch::X86_64, None, &[&windows_tests, &check_scripts]), - }; - - let upload_release_assets = upload_release_assets(&[&create_draft_release], &bundle); - - let auto_release_preview = auto_release_preview(&[&upload_release_assets]); - let notify_on_failure = notify_on_failure(&[&upload_release_assets, &auto_release_preview]); - - named::workflow() - .on(Event::default().push(Push::default().tags(vec!["v*".to_string()]))) - .concurrency(vars::one_workflow_per_non_main_branch()) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("RUST_BACKTRACE", "1")) - .add_job(macos_tests.name, macos_tests.job) - .add_job(linux_tests.name, linux_tests.job) - .add_job(windows_tests.name, windows_tests.job) - .add_job(check_scripts.name, check_scripts.job) - .add_job(create_draft_release.name, create_draft_release.job) - .map(|mut workflow| { - for job in bundle.into_jobs() { - workflow = workflow.add_job(job.name, job.job); - } - workflow - }) - .add_job(upload_release_assets.name, upload_release_assets.job) - .add_job(auto_release_preview.name, auto_release_preview.job) - .add_job(notify_on_failure.name, notify_on_failure.job) -} - -pub(crate) struct ReleaseBundleJobs { - pub linux_aarch64: NamedJob, - pub linux_x86_64: NamedJob, - pub mac_aarch64: NamedJob, - pub mac_x86_64: NamedJob, - pub windows_aarch64: NamedJob, - pub windows_x86_64: NamedJob, -} - -impl ReleaseBundleJobs { - pub fn jobs(&self) -> Vec<&NamedJob> { - vec![ - &self.linux_aarch64, - &self.linux_x86_64, - &self.mac_aarch64, - &self.mac_x86_64, - &self.windows_aarch64, - &self.windows_x86_64, - ] - } - - pub fn into_jobs(self) -> Vec { - vec![ - self.linux_aarch64, - self.linux_x86_64, - self.mac_aarch64, - self.mac_x86_64, - self.windows_aarch64, - self.windows_x86_64, - ] - } -} - -pub(crate) fn create_sentry_release() -> Step { - named::uses( - "getsentry", - "action-release", - "526942b68292201ac6bbb99b9a0747d4abee354c", // v3 - ) - .add_env(("SENTRY_ORG", "zed-dev")) - .add_env(("SENTRY_PROJECT", "zed")) - .add_env(("SENTRY_AUTH_TOKEN", vars::SENTRY_AUTH_TOKEN)) - .add_with(("environment", "production")) -} - -fn auto_release_preview(deps: &[&NamedJob; 1]) -> NamedJob { - named::job( - dependant_job(deps) - .runs_on(runners::LINUX_SMALL) - .cond(Expression::new(indoc::indoc!( - r#"startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')"# - ))) - .add_step( - steps::script( - r#"gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false"#, - ) - .add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN)), - ) - ) -} - -pub(crate) fn download_workflow_artifacts() -> Step { - named::uses( - "actions", - "download-artifact", - "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53", // v6.0.0 - ) - .add_with(("path", "./artifacts/")) -} - -pub(crate) fn prep_release_artifacts() -> Step { - let mut script_lines = vec!["mkdir -p release-artifacts/\n".to_string()]; - for asset in assets::all() { - let mv_command = format!("mv ./artifacts/{asset}/{asset} release-artifacts/{asset}"); - script_lines.push(mv_command) - } - - named::bash(&script_lines.join("\n")) -} - -fn upload_release_assets(deps: &[&NamedJob], bundle: &ReleaseBundleJobs) -> NamedJob { - let mut deps = deps.to_vec(); - deps.extend(bundle.jobs()); - - named::job( - dependant_job(&deps) - .runs_on(runners::LINUX_MEDIUM) - .add_step(download_workflow_artifacts()) - .add_step(steps::script("ls -lR ./artifacts")) - .add_step(prep_release_artifacts()) - .add_step( - steps::script("gh release upload \"$GITHUB_REF_NAME\" --repo=zed-industries/zed release-artifacts/*") - .add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN)), - ), - ) -} - -fn create_draft_release() -> NamedJob { - fn generate_release_notes() -> Step { - named::bash( - r#"node --redirect-warnings=/dev/null ./script/draft-release-notes "$RELEASE_VERSION" "$RELEASE_CHANNEL" > target/release-notes.md"#, - ) - } - - fn create_release() -> Step { - named::bash("script/create-draft-release target/release-notes.md") - .add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN)) - } - - named::job( - release_job(&[]) - .runs_on(runners::LINUX_SMALL) - // We need to fetch more than one commit so that `script/draft-release-notes` - // is able to diff between the current and previous tag. - // - // 25 was chosen arbitrarily. - .add_step( - steps::checkout_repo() - .add_with(("fetch-depth", 25)) - .add_with(("clean", false)) - .add_with(("ref", "${{ github.ref }}")), - ) - .add_step(steps::script("script/determine-release-channel")) - .add_step(steps::script("mkdir -p target/")) - .add_step(generate_release_notes()) - .add_step(create_release()), - ) -} - -pub(crate) fn notify_on_failure(deps: &[&NamedJob]) -> NamedJob { - fn notify_slack() -> Step { - named::bash( - "curl -X POST -H 'Content-type: application/json'\\\n --data '{\"text\":\"${{ github.workflow }} failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}' \"$SLACK_WEBHOOK\"" - ).add_env(("SLACK_WEBHOOK", vars::SLACK_WEBHOOK_WORKFLOW_FAILURES)) - } - - let job = dependant_job(deps) - .runs_on(runners::LINUX_SMALL) - .cond(Expression::new("failure()")) - .add_step(notify_slack()); - named::job(job) -} diff --git a/tooling/xtask/src/tasks/workflows/release_nightly.rs b/tooling/xtask/src/tasks/workflows/release_nightly.rs deleted file mode 100644 index 73cdbe3f3e..0000000000 --- a/tooling/xtask/src/tasks/workflows/release_nightly.rs +++ /dev/null @@ -1,131 +0,0 @@ -use crate::tasks::workflows::{ - nix_build::build_nix, - release::{ - ReleaseBundleJobs, create_sentry_release, download_workflow_artifacts, notify_on_failure, - prep_release_artifacts, - }, - run_bundling::{bundle_linux, bundle_mac, bundle_windows}, - run_tests::run_platform_tests, - runners::{Arch, Platform, ReleaseChannel}, - steps::{CommonJobConditions, FluentBuilder, NamedJob}, -}; - -use super::{runners, steps, steps::named, vars}; -use gh_workflow::*; - -/// Generates the release_nightly.yml workflow -pub fn release_nightly() -> Workflow { - let style = check_style(); - // run only on windows as that's our fastest platform right now. - let tests = run_platform_tests(Platform::Windows); - let nightly = Some(ReleaseChannel::Nightly); - - let bundle = ReleaseBundleJobs { - linux_aarch64: bundle_linux(Arch::AARCH64, nightly, &[&style, &tests]), - linux_x86_64: bundle_linux(Arch::X86_64, nightly, &[&style, &tests]), - mac_aarch64: bundle_mac(Arch::AARCH64, nightly, &[&style, &tests]), - mac_x86_64: bundle_mac(Arch::X86_64, nightly, &[&style, &tests]), - windows_aarch64: bundle_windows(Arch::AARCH64, nightly, &[&style, &tests]), - windows_x86_64: bundle_windows(Arch::X86_64, nightly, &[&style, &tests]), - }; - - let nix_linux_x86 = build_nix( - Platform::Linux, - Arch::X86_64, - "default", - None, - &[&style, &tests], - ); - let nix_mac_arm = build_nix( - Platform::Mac, - Arch::AARCH64, - "default", - None, - &[&style, &tests], - ); - let update_nightly_tag = update_nightly_tag_job(&bundle); - let notify_on_failure = notify_on_failure(&bundle.jobs()); - - named::workflow() - .on(Event::default() - // Fire every day at 7:00am UTC (Roughly before EU workday and after US workday) - .schedule([Schedule::new("0 7 * * *")]) - .push(Push::default().add_tag("nightly"))) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("RUST_BACKTRACE", "1")) - .add_job(style.name, style.job) - .add_job(tests.name, tests.job) - .map(|mut workflow| { - for job in bundle.into_jobs() { - workflow = workflow.add_job(job.name, job.job); - } - workflow - }) - .add_job(nix_linux_x86.name, nix_linux_x86.job) - .add_job(nix_mac_arm.name, nix_mac_arm.job) - .add_job(update_nightly_tag.name, update_nightly_tag.job) - .add_job(notify_on_failure.name, notify_on_failure.job) -} - -fn check_style() -> NamedJob { - let job = release_job(&[]) - .runs_on(runners::MAC_DEFAULT) - .add_step( - steps::checkout_repo() - .add_with(("clean", false)) - .add_with(("fetch-depth", 0)), - ) - .add_step(steps::cargo_fmt()) - .add_step(steps::script("./script/clippy")); - - named::job(job) -} - -fn release_job(deps: &[&NamedJob]) -> Job { - let job = Job::default() - .with_repository_owner_guard() - .timeout_minutes(60u32); - if deps.len() > 0 { - job.needs(deps.iter().map(|j| j.name.clone()).collect::>()) - } else { - job - } -} - -fn update_nightly_tag_job(bundle: &ReleaseBundleJobs) -> NamedJob { - fn update_nightly_tag() -> Step { - named::bash(indoc::indoc! {r#" - if [ "$(git rev-parse nightly)" = "$(git rev-parse HEAD)" ]; then - echo "Nightly tag already points to current commit. Skipping tagging." - exit 0 - fi - git config user.name github-actions - git config user.email github-actions@github.com - git tag -f nightly - git push origin nightly --force - "#}) - } - - NamedJob { - name: "update_nightly_tag".to_owned(), - job: steps::release_job(&bundle.jobs()) - .runs_on(runners::LINUX_MEDIUM) - .add_step(steps::checkout_repo().add_with(("fetch-depth", 0))) - .add_step(download_workflow_artifacts()) - .add_step(steps::script("ls -lR ./artifacts")) - .add_step(prep_release_artifacts()) - .add_step( - steps::script("./script/upload-nightly") - .add_env(( - "DIGITALOCEAN_SPACES_ACCESS_KEY", - vars::DIGITALOCEAN_SPACES_ACCESS_KEY, - )) - .add_env(( - "DIGITALOCEAN_SPACES_SECRET_KEY", - vars::DIGITALOCEAN_SPACES_SECRET_KEY, - )), - ) - .add_step(update_nightly_tag()) - .add_step(create_sentry_release()), - } -} diff --git a/tooling/xtask/src/tasks/workflows/run_agent_evals.rs b/tooling/xtask/src/tasks/workflows/run_agent_evals.rs deleted file mode 100644 index 667ea6a90b..0000000000 --- a/tooling/xtask/src/tasks/workflows/run_agent_evals.rs +++ /dev/null @@ -1,165 +0,0 @@ -use gh_workflow::{ - Event, Expression, Job, Run, Schedule, Step, Strategy, Use, Workflow, WorkflowDispatch, -}; -use serde_json::json; - -use crate::tasks::workflows::{ - runners::{self, Platform}, - steps::{self, FluentBuilder as _, NamedJob, named, setup_cargo_config}, - vars::{self, WorkflowInput}, -}; - -pub(crate) fn run_agent_evals() -> Workflow { - let agent_evals = agent_evals(); - let model_name = WorkflowInput::string("model_name", None); - - named::workflow() - .on(Event::default().workflow_dispatch( - WorkflowDispatch::default().add_input(model_name.name, model_name.input()), - )) - .concurrency(vars::one_workflow_per_non_main_branch()) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("CARGO_INCREMENTAL", 0)) - .add_env(("RUST_BACKTRACE", 1)) - .add_env(("ANTHROPIC_API_KEY", vars::ANTHROPIC_API_KEY)) - .add_env(("OPENAI_API_KEY", vars::OPENAI_API_KEY)) - .add_env(("GOOGLE_AI_API_KEY", vars::GOOGLE_AI_API_KEY)) - .add_env(("GOOGLE_CLOUD_PROJECT", vars::GOOGLE_CLOUD_PROJECT)) - .add_env(("ZED_CLIENT_CHECKSUM_SEED", vars::ZED_CLIENT_CHECKSUM_SEED)) - .add_env(("ZED_EVAL_TELEMETRY", 1)) - .add_env(("MODEL_NAME", model_name.to_string())) - .add_job(agent_evals.name, agent_evals.job) -} - -pub(crate) fn run_unit_evals() -> Workflow { - let model_name = WorkflowInput::string("model_name", None); - let commit_sha = WorkflowInput::string("commit_sha", None); - - let unit_evals = named::job(unit_evals(Some(&commit_sha))); - - named::workflow() - .name("run_unit_evals") - .on(Event::default().workflow_dispatch( - WorkflowDispatch::default() - .add_input(model_name.name, model_name.input()) - .add_input(commit_sha.name, commit_sha.input()), - )) - .concurrency(vars::allow_concurrent_runs()) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("CARGO_INCREMENTAL", 0)) - .add_env(("RUST_BACKTRACE", 1)) - .add_env(("ZED_CLIENT_CHECKSUM_SEED", vars::ZED_CLIENT_CHECKSUM_SEED)) - .add_env(("ZED_EVAL_TELEMETRY", 1)) - .add_env(("MODEL_NAME", model_name.to_string())) - .add_job(unit_evals.name, unit_evals.job) -} - -fn add_api_keys(step: Step) -> Step { - step.add_env(("ANTHROPIC_API_KEY", vars::ANTHROPIC_API_KEY)) - .add_env(("OPENAI_API_KEY", vars::OPENAI_API_KEY)) - .add_env(("GOOGLE_AI_API_KEY", vars::GOOGLE_AI_API_KEY)) - .add_env(("GOOGLE_CLOUD_PROJECT", vars::GOOGLE_CLOUD_PROJECT)) -} - -fn agent_evals() -> NamedJob { - fn run_eval() -> Step { - named::bash( - "cargo run --package=eval -- --repetitions=8 --concurrency=1 --model \"${MODEL_NAME}\"", - ) - } - - named::job( - Job::default() - .runs_on(runners::LINUX_DEFAULT) - .timeout_minutes(60_u32 * 10) - .add_step(steps::checkout_repo()) - .add_step(steps::cache_rust_dependencies_namespace()) - .map(steps::install_linux_dependencies) - .add_step(setup_cargo_config(Platform::Linux)) - .add_step(steps::script("cargo build --package=eval")) - .add_step(add_api_keys(run_eval())) - .add_step(steps::cleanup_cargo_config(Platform::Linux)), - ) -} - -pub(crate) fn run_cron_unit_evals() -> Workflow { - let unit_evals = cron_unit_evals(); - - named::workflow() - .name("run_cron_unit_evals") - .on(Event::default() - .schedule([ - // GitHub might drop jobs at busy times, so we choose a random time in the middle of the night. - Schedule::default().cron("47 1 * * 2"), - ]) - .workflow_dispatch(WorkflowDispatch::default())) - .concurrency(vars::one_workflow_per_non_main_branch()) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("CARGO_INCREMENTAL", 0)) - .add_env(("RUST_BACKTRACE", 1)) - .add_env(("ZED_CLIENT_CHECKSUM_SEED", vars::ZED_CLIENT_CHECKSUM_SEED)) - .add_job(unit_evals.name, unit_evals.job) -} - -fn cron_unit_evals() -> NamedJob { - fn send_failure_to_slack() -> Step { - named::uses( - "slackapi", - "slack-github-action", - "b0fa283ad8fea605de13dc3f449259339835fc52", - ) - .if_condition(Expression::new("${{ failure() }}")) - .add_with(("method", "chat.postMessage")) - .add_with(("token", vars::SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN)) - .add_with(("payload", indoc::indoc!{r#" - channel: C04UDRNNJFQ - text: "Unit Evals Failed: https://github.com/zed-industries/zed/actions/runs/${{ github.run_id }}" - "#})) - } - - named::job(cron_unit_evals_job().add_step(send_failure_to_slack())) -} - -const UNIT_EVAL_MODELS: &[&str] = &[ - "anthropic/claude-sonnet-4-5-latest", - "anthropic/claude-opus-4-5-latest", - "google/gemini-3-pro", - "openai/gpt-5", -]; - -fn cron_unit_evals_job() -> Job { - let script_step = add_api_keys(steps::script("./script/run-unit-evals")) - .add_env(("ZED_AGENT_MODEL", "${{ matrix.model }}")); - - Job::default() - .runs_on(runners::LINUX_DEFAULT) - .strategy(Strategy::default().fail_fast(false).matrix(json!({ - "model": UNIT_EVAL_MODELS - }))) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(Platform::Linux)) - .add_step(steps::cache_rust_dependencies_namespace()) - .map(steps::install_linux_dependencies) - .add_step(steps::cargo_install_nextest()) - .add_step(steps::clear_target_dir_if_large(Platform::Linux)) - .add_step(script_step) - .add_step(steps::cleanup_cargo_config(Platform::Linux)) -} - -fn unit_evals(commit: Option<&WorkflowInput>) -> Job { - let script_step = add_api_keys(steps::script("./script/run-unit-evals")); - - Job::default() - .runs_on(runners::LINUX_DEFAULT) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(Platform::Linux)) - .add_step(steps::cache_rust_dependencies_namespace()) - .map(steps::install_linux_dependencies) - .add_step(steps::cargo_install_nextest()) - .add_step(steps::clear_target_dir_if_large(Platform::Linux)) - .add_step(match commit { - Some(commit) => script_step.add_env(("UNIT_EVAL_COMMIT", commit)), - None => script_step, - }) - .add_step(steps::cleanup_cargo_config(Platform::Linux)) -} diff --git a/tooling/xtask/src/tasks/workflows/run_bundling.rs b/tooling/xtask/src/tasks/workflows/run_bundling.rs deleted file mode 100644 index a0793ffb68..0000000000 --- a/tooling/xtask/src/tasks/workflows/run_bundling.rs +++ /dev/null @@ -1,195 +0,0 @@ -use std::path::Path; - -use crate::tasks::workflows::{ - release::ReleaseBundleJobs, - runners::{Arch, Platform, ReleaseChannel}, - steps::{FluentBuilder, NamedJob, dependant_job, named}, - vars::{assets, bundle_envs}, -}; - -use super::{runners, steps}; -use gh_workflow::*; -use indoc::indoc; - -pub fn run_bundling() -> Workflow { - let bundle = ReleaseBundleJobs { - linux_aarch64: bundle_linux(Arch::AARCH64, None, &[]), - linux_x86_64: bundle_linux(Arch::X86_64, None, &[]), - mac_aarch64: bundle_mac(Arch::AARCH64, None, &[]), - mac_x86_64: bundle_mac(Arch::X86_64, None, &[]), - windows_aarch64: bundle_windows(Arch::AARCH64, None, &[]), - windows_x86_64: bundle_windows(Arch::X86_64, None, &[]), - }; - named::workflow() - .on(Event::default().pull_request( - PullRequest::default().types([PullRequestType::Labeled, PullRequestType::Synchronize]), - )) - .concurrency( - Concurrency::new(Expression::new( - "${{ github.workflow }}-${{ github.head_ref || github.ref }}", - )) - .cancel_in_progress(true), - ) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("RUST_BACKTRACE", "1")) - .map(|mut workflow| { - for job in bundle.into_jobs() { - workflow = workflow.add_job(job.name, job.job); - } - workflow - }) -} - -fn bundle_job(deps: &[&NamedJob]) -> Job { - dependant_job(deps) - .when(deps.len() == 0, |job| - job.cond(Expression::new( - indoc! { - r#"(github.event.action == 'labeled' && github.event.label.name == 'run-bundling') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))"#, - }))) - .timeout_minutes(60u32) -} - -pub(crate) fn bundle_mac( - arch: Arch, - release_channel: Option, - deps: &[&NamedJob], -) -> NamedJob { - pub fn bundle_mac(arch: Arch) -> Step { - named::bash(&format!("./script/bundle-mac {arch}-apple-darwin")) - } - let platform = Platform::Mac; - let artifact_name = match arch { - Arch::X86_64 => assets::MAC_X86_64, - Arch::AARCH64 => assets::MAC_AARCH64, - }; - let remote_server_artifact_name = match arch { - Arch::X86_64 => assets::REMOTE_SERVER_MAC_X86_64, - Arch::AARCH64 => assets::REMOTE_SERVER_MAC_AARCH64, - }; - NamedJob { - name: format!("bundle_mac_{arch}"), - job: bundle_job(deps) - .runs_on(runners::MAC_DEFAULT) - .envs(bundle_envs(platform)) - .add_step(steps::checkout_repo()) - .when_some(release_channel, |job, release_channel| { - job.add_step(set_release_channel(platform, release_channel)) - }) - .add_step(steps::setup_node()) - .add_step(steps::setup_sentry()) - .add_step(steps::clear_target_dir_if_large(runners::Platform::Mac)) - .add_step(bundle_mac(arch)) - .add_step(upload_artifact(&format!( - "target/{arch}-apple-darwin/release/{artifact_name}" - ))) - .add_step(upload_artifact(&format!( - "target/{remote_server_artifact_name}" - ))), - } -} - -pub fn upload_artifact(path: &str) -> Step { - let name = Path::new(path).file_name().unwrap().to_str().unwrap(); - Step::new(format!("@actions/upload-artifact {}", name)) - .uses( - "actions", - "upload-artifact", - "330a01c490aca151604b8cf639adc76d48f6c5d4", // v5 - ) - // N.B. "name" is the name for the asset. The uploaded - // file retains its filename. - .add_with(("name", name)) - .add_with(("path", path)) - .add_with(("if-no-files-found", "error")) -} - -pub(crate) fn bundle_linux( - arch: Arch, - release_channel: Option, - deps: &[&NamedJob], -) -> NamedJob { - let platform = Platform::Linux; - let artifact_name = match arch { - Arch::X86_64 => assets::LINUX_X86_64, - Arch::AARCH64 => assets::LINUX_AARCH64, - }; - let remote_server_artifact_name = match arch { - Arch::X86_64 => assets::REMOTE_SERVER_LINUX_X86_64, - Arch::AARCH64 => assets::REMOTE_SERVER_LINUX_AARCH64, - }; - NamedJob { - name: format!("bundle_linux_{arch}"), - job: bundle_job(deps) - .runs_on(arch.linux_bundler()) - .envs(bundle_envs(platform)) - .add_step(steps::checkout_repo()) - .when_some(release_channel, |job, release_channel| { - job.add_step(set_release_channel(platform, release_channel)) - }) - .add_step(steps::setup_sentry()) - .map(steps::install_linux_dependencies) - .add_step(steps::script("./script/bundle-linux")) - .add_step(upload_artifact(&format!("target/release/{artifact_name}"))) - .add_step(upload_artifact(&format!( - "target/{remote_server_artifact_name}" - ))), - } -} - -pub(crate) fn bundle_windows( - arch: Arch, - release_channel: Option, - deps: &[&NamedJob], -) -> NamedJob { - let platform = Platform::Windows; - pub fn bundle_windows(arch: Arch) -> Step { - let step = match arch { - Arch::X86_64 => named::pwsh("script/bundle-windows.ps1 -Architecture x86_64"), - Arch::AARCH64 => named::pwsh("script/bundle-windows.ps1 -Architecture aarch64"), - }; - step.working_directory("${{ env.ZED_WORKSPACE }}") - } - let artifact_name = match arch { - Arch::X86_64 => assets::WINDOWS_X86_64, - Arch::AARCH64 => assets::WINDOWS_AARCH64, - }; - NamedJob { - name: format!("bundle_windows_{arch}"), - job: bundle_job(deps) - .runs_on(runners::WINDOWS_DEFAULT) - .envs(bundle_envs(platform)) - .add_step(steps::checkout_repo()) - .when_some(release_channel, |job, release_channel| { - job.add_step(set_release_channel(platform, release_channel)) - }) - .add_step(steps::setup_sentry()) - .add_step(bundle_windows(arch)) - .add_step(upload_artifact(&format!("target/{artifact_name}"))), - } -} - -fn set_release_channel(platform: Platform, release_channel: ReleaseChannel) -> Step { - match release_channel { - ReleaseChannel::Nightly => set_release_channel_to_nightly(platform), - } -} - -fn set_release_channel_to_nightly(platform: Platform) -> Step { - match platform { - Platform::Linux | Platform::Mac => named::bash(indoc::indoc! {r#" - set -eu - version=$(git rev-parse --short HEAD) - echo "Publishing version: ${version} on release channel nightly" - echo "nightly" > crates/zed/RELEASE_CHANNEL - "#}), - Platform::Windows => named::pwsh(indoc::indoc! {r#" - $ErrorActionPreference = "Stop" - $version = git rev-parse --short HEAD - Write-Host "Publishing version: $version on release channel nightly" - "nightly" | Set-Content -Path "crates/zed/RELEASE_CHANNEL" - "#}) - .working_directory("${{ env.ZED_WORKSPACE }}"), - } -} diff --git a/tooling/xtask/src/tasks/workflows/run_tests.rs b/tooling/xtask/src/tasks/workflows/run_tests.rs deleted file mode 100644 index 0bb3e152fb..0000000000 --- a/tooling/xtask/src/tasks/workflows/run_tests.rs +++ /dev/null @@ -1,496 +0,0 @@ -use gh_workflow::{ - Concurrency, Event, Expression, Job, PullRequest, Push, Run, Step, Use, Workflow, -}; -use indexmap::IndexMap; - -use crate::tasks::workflows::{ - nix_build::build_nix, - runners::Arch, - steps::{BASH_SHELL, CommonJobConditions, repository_owner_guard_expression}, - vars::{self, PathCondition}, -}; - -use super::{ - runners::{self, Platform}, - steps::{self, FluentBuilder, NamedJob, named, release_job}, -}; - -pub(crate) fn run_tests() -> Workflow { - // Specify anything which should potentially skip full test suite in this regex: - // - docs/ - // - script/update_top_ranking_issues/ - // - .github/ISSUE_TEMPLATE/ - // - .github/workflows/ (except .github/workflows/ci.yml) - let should_run_tests = PathCondition::inverted( - "run_tests", - r"^(docs/|script/update_top_ranking_issues/|\.github/(ISSUE_TEMPLATE|workflows/(?!run_tests)))", - ); - let should_check_docs = PathCondition::new("run_docs", r"^(docs/|crates/.*\.rs)"); - let should_check_scripts = PathCondition::new( - "run_action_checks", - r"^\.github/(workflows/|actions/|actionlint.yml)|tooling/xtask|script/", - ); - let should_check_licences = - PathCondition::new("run_licenses", r"^(Cargo.lock|script/.*licenses)"); - let should_build_nix = PathCondition::new( - "run_nix", - r"^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)", - ); - - let orchestrate = orchestrate(&[ - &should_check_scripts, - &should_check_docs, - &should_check_licences, - &should_build_nix, - &should_run_tests, - ]); - - let mut jobs = vec![ - orchestrate, - check_style(), - should_run_tests.guard(run_platform_tests(Platform::Windows)), - should_run_tests.guard(run_platform_tests(Platform::Linux)), - should_run_tests.guard(run_platform_tests(Platform::Mac)), - should_run_tests.guard(doctests()), - should_run_tests.guard(check_workspace_binaries()), - should_run_tests.guard(check_dependencies()), // could be more specific here? - should_check_docs.guard(check_docs()), - should_check_licences.guard(check_licenses()), - should_check_scripts.guard(check_scripts()), - should_build_nix.guard(build_nix( - Platform::Linux, - Arch::X86_64, - "debug", - // *don't* cache the built output - Some("-zed-editor-[0-9.]*-nightly"), - &[], - )), - should_build_nix.guard(build_nix( - Platform::Mac, - Arch::AARCH64, - "debug", - // *don't* cache the built output - Some("-zed-editor-[0-9.]*-nightly"), - &[], - )), - ]; - let tests_pass = tests_pass(&jobs); - - jobs.push(should_run_tests.guard(check_postgres_and_protobuf_migrations())); // could be more specific here? - - named::workflow() - .add_event( - Event::default() - .push( - Push::default() - .add_branch("main") - .add_branch("v[0-9]+.[0-9]+.x"), - ) - .pull_request(PullRequest::default().add_branch("**")), - ) - .concurrency( - Concurrency::default() - .group(concat!( - "${{ github.workflow }}-${{ github.ref_name }}-", - "${{ github.ref_name == 'main' && github.sha || 'anysha' }}" - )) - .cancel_in_progress(true), - ) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("RUST_BACKTRACE", 1)) - .add_env(("CARGO_INCREMENTAL", 0)) - .map(|mut workflow| { - for job in jobs { - workflow = workflow.add_job(job.name, job.job) - } - workflow - }) - .add_job(tests_pass.name, tests_pass.job) -} - -// Generates a bash script that checks changed files against regex patterns -// and sets GitHub output variables accordingly -pub fn orchestrate(rules: &[&PathCondition]) -> NamedJob { - let name = "orchestrate".to_owned(); - let step_name = "filter".to_owned(); - let mut script = String::new(); - - script.push_str(indoc::indoc! {r#" - if [ -z "$GITHUB_BASE_REF" ]; then - echo "Not in a PR context (i.e., push to main/stable/preview)" - COMPARE_REV="$(git rev-parse HEAD~1)" - else - echo "In a PR context comparing to pull_request.base.ref" - git fetch origin "$GITHUB_BASE_REF" --depth=350 - COMPARE_REV="$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)" - fi - CHANGED_FILES="$(git diff --name-only "$COMPARE_REV" ${{ github.sha }})" - - check_pattern() { - local output_name="$1" - local pattern="$2" - local grep_arg="$3" - - echo "$CHANGED_FILES" | grep "$grep_arg" "$pattern" && \ - echo "${output_name}=true" >> "$GITHUB_OUTPUT" || \ - echo "${output_name}=false" >> "$GITHUB_OUTPUT" - } - - "#}); - - let mut outputs = IndexMap::new(); - - for rule in rules { - assert!( - rule.set_by_step - .borrow_mut() - .replace(name.clone()) - .is_none() - ); - assert!( - outputs - .insert( - rule.name.to_owned(), - format!("${{{{ steps.{}.outputs.{} }}}}", step_name, rule.name) - ) - .is_none() - ); - - let grep_arg = if rule.invert { "-qvP" } else { "-qP" }; - script.push_str(&format!( - "check_pattern \"{}\" '{}' {}\n", - rule.name, rule.pattern, grep_arg - )); - } - - let job = Job::default() - .runs_on(runners::LINUX_SMALL) - .with_repository_owner_guard() - .outputs(outputs) - .add_step(steps::checkout_repo().add_with(( - "fetch-depth", - "${{ github.ref == 'refs/heads/main' && 2 || 350 }}", - ))) - .add_step( - Step::new(step_name.clone()) - .run(script) - .id(step_name) - .shell(BASH_SHELL), - ); - - NamedJob { name, job } -} - -pub fn tests_pass(jobs: &[NamedJob]) -> NamedJob { - let mut script = String::from(indoc::indoc! {r#" - set +x - EXIT_CODE=0 - - check_result() { - echo "* $1: $2" - if [[ "$2" != "skipped" && "$2" != "success" ]]; then EXIT_CODE=1; fi - } - - "#}); - - script.push_str( - &jobs - .iter() - .map(|job| { - format!( - "check_result \"{}\" \"${{{{ needs.{}.result }}}}\"", - job.name, job.name - ) - }) - .collect::>() - .join("\n"), - ); - - script.push_str("\n\nexit $EXIT_CODE\n"); - - let job = Job::default() - .runs_on(runners::LINUX_SMALL) - .needs( - jobs.iter() - .map(|j| j.name.to_string()) - .collect::>(), - ) - .cond(repository_owner_guard_expression(true)) - .add_step(named::bash(&script)); - - named::job(job) -} - -fn check_style() -> NamedJob { - fn check_for_typos() -> Step { - named::uses( - "crate-ci", - "typos", - "2d0ce569feab1f8752f1dde43cc2f2aa53236e06", - ) // v1.40.0 - .with(("config", "./typos.toml")) - } - named::job( - release_job(&[]) - .runs_on(runners::LINUX_MEDIUM) - .add_step(steps::checkout_repo()) - .add_step(steps::cache_rust_dependencies_namespace()) - .add_step(steps::setup_pnpm()) - .add_step(steps::script("./script/prettier")) - .add_step(steps::script("./script/check-todos")) - .add_step(steps::script("./script/check-keymaps")) - .add_step(check_for_typos()) - .add_step(steps::cargo_fmt()), - ) -} - -fn check_dependencies() -> NamedJob { - fn install_cargo_machete() -> Step { - named::uses( - "clechasseur", - "rs-cargo", - "8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386", // v2 - ) - .add_with(("command", "install")) - .add_with(("args", "cargo-machete@0.7.0")) - } - - fn run_cargo_machete() -> Step { - named::uses( - "clechasseur", - "rs-cargo", - "8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386", // v2 - ) - .add_with(("command", "machete")) - } - - fn check_cargo_lock() -> Step { - named::bash("cargo update --locked --workspace") - } - - fn check_vulnerable_dependencies() -> Step { - named::uses( - "actions", - "dependency-review-action", - "67d4f4bd7a9b17a0db54d2a7519187c65e339de8", // v4 - ) - .if_condition(Expression::new("github.event_name == 'pull_request'")) - .with(("license-check", false)) - } - - named::job( - release_job(&[]) - .runs_on(runners::LINUX_SMALL) - .add_step(steps::checkout_repo()) - .add_step(steps::cache_rust_dependencies_namespace()) - .add_step(install_cargo_machete()) - .add_step(run_cargo_machete()) - .add_step(check_cargo_lock()) - .add_step(check_vulnerable_dependencies()), - ) -} - -fn check_workspace_binaries() -> NamedJob { - named::job( - release_job(&[]) - .runs_on(runners::LINUX_LARGE) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(Platform::Linux)) - .add_step(steps::cache_rust_dependencies_namespace()) - .map(steps::install_linux_dependencies) - .add_step(steps::script("cargo build -p collab")) - .add_step(steps::script("cargo build --workspace --bins --examples")) - .add_step(steps::cleanup_cargo_config(Platform::Linux)), - ) -} - -pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob { - let runner = match platform { - Platform::Windows => runners::WINDOWS_DEFAULT, - Platform::Linux => runners::LINUX_DEFAULT, - Platform::Mac => runners::MAC_DEFAULT, - }; - NamedJob { - name: format!("run_tests_{platform}"), - job: release_job(&[]) - .runs_on(runner) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(platform)) - .when(platform == Platform::Linux, |this| { - this.add_step(steps::cache_rust_dependencies_namespace()) - }) - .when( - platform == Platform::Linux, - steps::install_linux_dependencies, - ) - .add_step(steps::setup_node()) - .add_step(steps::clippy(platform)) - .when(platform == Platform::Linux, |job| { - job.add_step(steps::cargo_install_nextest()) - }) - .add_step(steps::clear_target_dir_if_large(platform)) - .add_step(steps::cargo_nextest(platform)) - .add_step(steps::cleanup_cargo_config(platform)), - } -} - -pub(crate) fn check_postgres_and_protobuf_migrations() -> NamedJob { - fn remove_untracked_files() -> Step { - named::bash("git clean -df") - } - - fn ensure_fresh_merge() -> Step { - named::bash(indoc::indoc! {r#" - if [ -z "$GITHUB_BASE_REF" ]; - then - echo "BUF_BASE_BRANCH=$(git merge-base origin/main HEAD)" >> "$GITHUB_ENV" - else - git checkout -B temp - git merge -q "origin/$GITHUB_BASE_REF" -m "merge main into temp" - echo "BUF_BASE_BRANCH=$GITHUB_BASE_REF" >> "$GITHUB_ENV" - fi - "#}) - } - - fn bufbuild_setup_action() -> Step { - named::uses("bufbuild", "buf-setup-action", "v1") - .add_with(("version", "v1.29.0")) - .add_with(("github_token", vars::GITHUB_TOKEN)) - } - - fn bufbuild_breaking_action() -> Step { - named::uses("bufbuild", "buf-breaking-action", "v1").add_with(("input", "crates/proto/proto/")) - .add_with(("against", "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/")) - } - - named::job( - release_job(&[]) - .runs_on(runners::LINUX_DEFAULT) - .add_env(("GIT_AUTHOR_NAME", "Protobuf Action")) - .add_env(("GIT_AUTHOR_EMAIL", "ci@zed.dev")) - .add_env(("GIT_COMMITTER_NAME", "Protobuf Action")) - .add_env(("GIT_COMMITTER_EMAIL", "ci@zed.dev")) - .add_step(steps::checkout_repo().with(("fetch-depth", 0))) // fetch full history - .add_step(remove_untracked_files()) - .add_step(ensure_fresh_merge()) - .add_step(bufbuild_setup_action()) - .add_step(bufbuild_breaking_action()), - ) -} - -fn doctests() -> NamedJob { - fn run_doctests() -> Step { - named::bash(indoc::indoc! {r#" - cargo test --workspace --doc --no-fail-fast - "#}) - .id("run_doctests") - } - - named::job( - release_job(&[]) - .runs_on(runners::LINUX_DEFAULT) - .add_step(steps::checkout_repo()) - .add_step(steps::cache_rust_dependencies_namespace()) - .map(steps::install_linux_dependencies) - .add_step(steps::setup_cargo_config(Platform::Linux)) - .add_step(run_doctests()) - .add_step(steps::cleanup_cargo_config(Platform::Linux)), - ) -} - -fn check_licenses() -> NamedJob { - named::job( - Job::default() - .runs_on(runners::LINUX_SMALL) - .add_step(steps::checkout_repo()) - .add_step(steps::cache_rust_dependencies_namespace()) - .add_step(steps::script("./script/check-licenses")) - .add_step(steps::script("./script/generate-licenses")), - ) -} - -fn check_docs() -> NamedJob { - fn lychee_link_check(dir: &str) -> Step { - named::uses( - "lycheeverse", - "lychee-action", - "82202e5e9c2f4ef1a55a3d02563e1cb6041e5332", - ) // v2.4.1 - .add_with(("args", format!("--no-progress --exclude '^http' '{dir}'"))) - .add_with(("fail", true)) - .add_with(("jobSummary", false)) - } - - fn install_mdbook() -> Step { - named::uses( - "peaceiris", - "actions-mdbook", - "ee69d230fe19748b7abf22df32acaa93833fad08", // v2 - ) - .with(("mdbook-version", "0.4.37")) - } - - fn build_docs() -> Step { - named::bash(indoc::indoc! {r#" - mkdir -p target/deploy - mdbook build ./docs --dest-dir=../target/deploy/docs/ - "#}) - } - - named::job( - release_job(&[]) - .runs_on(runners::LINUX_LARGE) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(Platform::Linux)) - // todo(ci): un-inline build_docs/action.yml here - .add_step(steps::cache_rust_dependencies_namespace()) - .add_step( - lychee_link_check("./docs/src/**/*"), // check markdown links - ) - .map(steps::install_linux_dependencies) - .add_step(install_mdbook()) - .add_step(build_docs()) - .add_step( - lychee_link_check("target/deploy/docs"), // check links in generated html - ), - ) -} - -pub(crate) fn check_scripts() -> NamedJob { - fn download_actionlint() -> Step { - named::bash( - "bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)", - ) - } - - fn run_actionlint() -> Step { - named::bash(indoc::indoc! {r#" - ${{ steps.get_actionlint.outputs.executable }} -color - "#}) - } - - fn run_shellcheck() -> Step { - named::bash("./script/shellcheck-scripts error") - } - - fn check_xtask_workflows() -> Step { - named::bash(indoc::indoc! {r#" - cargo xtask workflows - if ! git diff --exit-code .github; then - echo "Error: .github directory has uncommitted changes after running 'cargo xtask workflows'" - echo "Please run 'cargo xtask workflows' locally and commit the changes" - exit 1 - fi - "#}) - } - - named::job( - release_job(&[]) - .runs_on(runners::LINUX_SMALL) - .add_step(steps::checkout_repo()) - .add_step(run_shellcheck()) - .add_step(download_actionlint().id("get_actionlint")) - .add_step(run_actionlint()) - .add_step(check_xtask_workflows()), - ) -} diff --git a/tooling/xtask/src/tasks/workflows/runners.rs b/tooling/xtask/src/tasks/workflows/runners.rs deleted file mode 100644 index df98826f8a..0000000000 --- a/tooling/xtask/src/tasks/workflows/runners.rs +++ /dev/null @@ -1,66 +0,0 @@ -pub const LINUX_SMALL: Runner = Runner("namespace-profile-2x4-ubuntu-2404"); -pub const LINUX_DEFAULT: Runner = LINUX_XL; -pub const LINUX_XL: Runner = Runner("namespace-profile-16x32-ubuntu-2204"); -pub const LINUX_LARGE: Runner = Runner("namespace-profile-8x16-ubuntu-2204"); -pub const LINUX_MEDIUM: Runner = Runner("namespace-profile-4x8-ubuntu-2204"); - -// Using Ubuntu 20.04 for minimal glibc version -pub const LINUX_X86_BUNDLER: Runner = Runner("namespace-profile-32x64-ubuntu-2004"); -pub const LINUX_ARM_BUNDLER: Runner = Runner("namespace-profile-8x32-ubuntu-2004-arm-m4"); - -pub const MAC_DEFAULT: Runner = Runner("self-mini-macos"); -pub const WINDOWS_DEFAULT: Runner = Runner("self-32vcpu-windows-2022"); - -pub struct Runner(&'static str); - -impl Into for Runner { - fn into(self) -> gh_workflow::RunsOn { - self.0.into() - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Arch { - X86_64, - AARCH64, -} - -impl std::fmt::Display for Arch { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Arch::X86_64 => write!(f, "x86_64"), - Arch::AARCH64 => write!(f, "aarch64"), - } - } -} - -impl Arch { - pub fn linux_bundler(&self) -> Runner { - match self { - Arch::X86_64 => LINUX_X86_BUNDLER, - Arch::AARCH64 => LINUX_ARM_BUNDLER, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Platform { - Windows, - Linux, - Mac, -} - -impl std::fmt::Display for Platform { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Platform::Windows => write!(f, "windows"), - Platform::Linux => write!(f, "linux"), - Platform::Mac => write!(f, "mac"), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ReleaseChannel { - Nightly, -} diff --git a/tooling/xtask/src/tasks/workflows/steps.rs b/tooling/xtask/src/tasks/workflows/steps.rs deleted file mode 100644 index 722a5f0704..0000000000 --- a/tooling/xtask/src/tasks/workflows/steps.rs +++ /dev/null @@ -1,336 +0,0 @@ -use gh_workflow::*; - -use crate::tasks::workflows::{runners::Platform, vars}; - -pub const BASH_SHELL: &str = "bash -euxo pipefail {0}"; -// https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idstepsshell -pub const PWSH_SHELL: &str = "pwsh"; - -pub fn checkout_repo() -> Step { - named::uses( - "actions", - "checkout", - "11bd71901bbe5b1630ceea73d27597364c9af683", // v4 - ) - // prevent checkout action from running `git clean -ffdx` which - // would delete the target directory - .add_with(("clean", false)) -} - -pub fn setup_pnpm() -> Step { - named::uses( - "pnpm", - "action-setup", - "fe02b34f77f8bc703788d5817da081398fad5dd2", // v4.0.0 - ) - .add_with(("version", "9")) -} - -pub fn setup_node() -> Step { - named::uses( - "actions", - "setup-node", - "49933ea5288caeca8642d1e84afbd3f7d6820020", // v4 - ) - .add_with(("node-version", "20")) -} - -pub fn setup_sentry() -> Step { - named::uses( - "matbour", - "setup-sentry-cli", - "3e938c54b3018bdd019973689ef984e033b0454b", - ) - .add_with(("token", vars::SENTRY_AUTH_TOKEN)) -} - -pub fn cargo_fmt() -> Step { - named::bash("cargo fmt --all -- --check") -} - -pub fn cargo_install_nextest() -> Step { - named::uses("taiki-e", "install-action", "nextest") -} - -pub fn cargo_nextest(platform: Platform) -> Step { - named::run(platform, "cargo nextest run --workspace --no-fail-fast") -} - -pub fn setup_cargo_config(platform: Platform) -> Step { - match platform { - Platform::Windows => named::pwsh(indoc::indoc! {r#" - New-Item -ItemType Directory -Path "./../.cargo" -Force - Copy-Item -Path "./.cargo/ci-config.toml" -Destination "./../.cargo/config.toml" - "#}), - - Platform::Linux | Platform::Mac => named::bash(indoc::indoc! {r#" - mkdir -p ./../.cargo - cp ./.cargo/ci-config.toml ./../.cargo/config.toml - "#}), - } -} - -pub fn cleanup_cargo_config(platform: Platform) -> Step { - let step = match platform { - Platform::Windows => named::pwsh(indoc::indoc! {r#" - Remove-Item -Recurse -Path "./../.cargo" -Force -ErrorAction SilentlyContinue - "#}), - Platform::Linux | Platform::Mac => named::bash(indoc::indoc! {r#" - rm -rf ./../.cargo - "#}), - }; - - step.if_condition(Expression::new("always()")) -} - -pub fn clear_target_dir_if_large(platform: Platform) -> Step { - match platform { - Platform::Windows => named::pwsh("./script/clear-target-dir-if-larger-than.ps1 250"), - Platform::Linux => named::bash("./script/clear-target-dir-if-larger-than 250"), - Platform::Mac => named::bash("./script/clear-target-dir-if-larger-than 300"), - } -} - -pub fn clippy(platform: Platform) -> Step { - match platform { - Platform::Windows => named::pwsh("./script/clippy.ps1"), - _ => named::bash("./script/clippy"), - } -} - -pub fn cache_rust_dependencies_namespace() -> Step { - named::uses("namespacelabs", "nscloud-cache-action", "v1").add_with(("cache", "rust")) -} - -pub fn setup_linux() -> Step { - named::bash("./script/linux") -} - -fn install_mold() -> Step { - named::bash("./script/install-mold") -} - -fn download_wasi_sdk() -> Step { - named::bash("./script/download-wasi-sdk") -} - -pub(crate) fn install_linux_dependencies(job: Job) -> Job { - job.add_step(setup_linux()) - .add_step(install_mold()) - .add_step(download_wasi_sdk()) -} - -pub fn script(name: &str) -> Step { - if name.ends_with(".ps1") { - Step::new(name).run(name).shell(PWSH_SHELL) - } else { - Step::new(name).run(name).shell(BASH_SHELL) - } -} - -pub struct NamedJob { - pub name: String, - pub job: Job, -} - -// impl NamedJob { -// pub fn map(self, f: impl FnOnce(Job) -> Job) -> Self { -// NamedJob { -// name: self.name, -// job: f(self.job), -// } -// } -// } - -pub(crate) const DEFAULT_REPOSITORY_OWNER_GUARD: &str = - "(github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')"; - -pub fn repository_owner_guard_expression(trigger_always: bool) -> Expression { - Expression::new(format!( - "{}{}", - DEFAULT_REPOSITORY_OWNER_GUARD, - trigger_always.then_some(" && always()").unwrap_or_default() - )) -} - -pub trait CommonJobConditions: Sized { - fn with_repository_owner_guard(self) -> Self; -} - -impl CommonJobConditions for Job { - fn with_repository_owner_guard(self) -> Self { - self.cond(repository_owner_guard_expression(false)) - } -} - -pub(crate) fn release_job(deps: &[&NamedJob]) -> Job { - dependant_job(deps) - .with_repository_owner_guard() - .timeout_minutes(60u32) -} - -pub(crate) fn dependant_job(deps: &[&NamedJob]) -> Job { - let job = Job::default(); - if deps.len() > 0 { - job.needs(deps.iter().map(|j| j.name.clone()).collect::>()) - } else { - job - } -} - -impl FluentBuilder for Job {} -impl FluentBuilder for Workflow {} -impl FluentBuilder for Input {} - -/// A helper trait for building complex objects with imperative conditionals in a fluent style. -/// Copied from GPUI to avoid adding GPUI as dependency -/// todo(ci) just put this in gh-workflow -#[allow(unused)] -pub trait FluentBuilder { - /// Imperatively modify self with the given closure. - fn map(self, f: impl FnOnce(Self) -> U) -> U - where - Self: Sized, - { - f(self) - } - - /// Conditionally modify self with the given closure. - fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self - where - Self: Sized, - { - self.map(|this| if condition { then(this) } else { this }) - } - - /// Conditionally modify self with the given closure. - fn when_else( - self, - condition: bool, - then: impl FnOnce(Self) -> Self, - else_fn: impl FnOnce(Self) -> Self, - ) -> Self - where - Self: Sized, - { - self.map(|this| if condition { then(this) } else { else_fn(this) }) - } - - /// Conditionally unwrap and modify self with the given closure, if the given option is Some. - fn when_some(self, option: Option, then: impl FnOnce(Self, T) -> Self) -> Self - where - Self: Sized, - { - self.map(|this| { - if let Some(value) = option { - then(this, value) - } else { - this - } - }) - } - /// Conditionally unwrap and modify self with the given closure, if the given option is None. - fn when_none(self, option: &Option, then: impl FnOnce(Self) -> Self) -> Self - where - Self: Sized, - { - self.map(|this| if option.is_some() { this } else { then(this) }) - } -} - -// (janky) helper to generate steps with a name that corresponds -// to the name of the calling function. -pub mod named { - use super::*; - - /// Returns a uses step with the same name as the enclosing function. - /// (You shouldn't inline this function into the workflow definition, you must - /// wrap it in a new function.) - pub fn uses(owner: &str, repo: &str, ref_: &str) -> Step { - Step::new(function_name(1)).uses(owner, repo, ref_) - } - - /// Returns a bash-script step with the same name as the enclosing function. - /// (You shouldn't inline this function into the workflow definition, you must - /// wrap it in a new function.) - pub fn bash(script: impl AsRef) -> Step { - Step::new(function_name(1)) - .run(script.as_ref()) - .shell(BASH_SHELL) - } - - /// Returns a pwsh-script step with the same name as the enclosing function. - /// (You shouldn't inline this function into the workflow definition, you must - /// wrap it in a new function.) - pub fn pwsh(script: &str) -> Step { - Step::new(function_name(1)).run(script).shell(PWSH_SHELL) - } - - /// Runs the command in either powershell or bash, depending on platform. - /// (You shouldn't inline this function into the workflow definition, you must - /// wrap it in a new function.) - pub fn run(platform: Platform, script: &str) -> Step { - match platform { - Platform::Windows => Step::new(function_name(1)).run(script).shell(PWSH_SHELL), - Platform::Linux | Platform::Mac => { - Step::new(function_name(1)).run(script).shell(BASH_SHELL) - } - } - } - - /// Returns a Workflow with the same name as the enclosing module. - pub fn workflow() -> Workflow { - Workflow::default().name( - named::function_name(1) - .split("::") - .collect::>() - .into_iter() - .rev() - .skip(1) - .rev() - .collect::>() - .join("::"), - ) - } - - /// Returns a Job with the same name as the enclosing function. - /// (note job names may not contain `::`) - pub fn job(job: Job) -> NamedJob { - NamedJob { - name: function_name(1).split("::").last().unwrap().to_owned(), - job, - } - } - - /// Returns the function name N callers above in the stack - /// (typically 1). - /// This only works because xtask always runs debug builds. - pub fn function_name(i: usize) -> String { - let mut name = "".to_string(); - let mut count = 0; - backtrace::trace(|frame| { - if count < i + 3 { - count += 1; - return true; - } - backtrace::resolve_frame(frame, |cb| { - if let Some(s) = cb.name() { - name = s.to_string() - } - }); - false - }); - - name.split("::") - .skip_while(|s| s != &"workflows") - .skip(1) - .collect::>() - .join("::") - } -} - -pub fn git_checkout(ref_name: &dyn std::fmt::Display) -> Step { - named::bash(&format!( - "git fetch origin {ref_name} && git checkout {ref_name}" - )) -} diff --git a/tooling/xtask/src/tasks/workflows/vars.rs b/tooling/xtask/src/tasks/workflows/vars.rs deleted file mode 100644 index adcd252465..0000000000 --- a/tooling/xtask/src/tasks/workflows/vars.rs +++ /dev/null @@ -1,344 +0,0 @@ -use std::cell::RefCell; - -use gh_workflow::{ - Concurrency, Env, Expression, Step, WorkflowCallInput, WorkflowCallSecret, - WorkflowDispatchInput, -}; - -use crate::tasks::workflows::{runners::Platform, steps::NamedJob}; - -macro_rules! secret { - ($secret_name:ident) => { - pub const $secret_name: &str = concat!("${{ secrets.", stringify!($secret_name), " }}"); - }; -} - -macro_rules! var { - ($var_name:ident) => { - pub const $var_name: &str = concat!("${{ vars.", stringify!($var_name), " }}"); - }; -} - -secret!(ANTHROPIC_API_KEY); -secret!(OPENAI_API_KEY); -secret!(GOOGLE_AI_API_KEY); -secret!(GOOGLE_CLOUD_PROJECT); -secret!(APPLE_NOTARIZATION_ISSUER_ID); -secret!(APPLE_NOTARIZATION_KEY); -secret!(APPLE_NOTARIZATION_KEY_ID); -secret!(AZURE_SIGNING_CLIENT_ID); -secret!(AZURE_SIGNING_CLIENT_SECRET); -secret!(AZURE_SIGNING_TENANT_ID); -secret!(CACHIX_AUTH_TOKEN); -secret!(DIGITALOCEAN_SPACES_ACCESS_KEY); -secret!(DIGITALOCEAN_SPACES_SECRET_KEY); -secret!(GITHUB_TOKEN); -secret!(MACOS_CERTIFICATE); -secret!(MACOS_CERTIFICATE_PASSWORD); -secret!(SENTRY_AUTH_TOKEN); -secret!(ZED_CLIENT_CHECKSUM_SEED); -secret!(ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON); -secret!(ZED_SENTRY_MINIDUMP_ENDPOINT); -secret!(SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN); -secret!(ZED_ZIPPY_APP_ID); -secret!(ZED_ZIPPY_APP_PRIVATE_KEY); -secret!(DISCORD_WEBHOOK_RELEASE_NOTES); -secret!(WINGET_TOKEN); -secret!(VERCEL_TOKEN); -secret!(SLACK_WEBHOOK_WORKFLOW_FAILURES); - -// todo(ci) make these secrets too... -var!(AZURE_SIGNING_ACCOUNT_NAME); -var!(AZURE_SIGNING_CERT_PROFILE_NAME); -var!(AZURE_SIGNING_ENDPOINT); - -pub fn bundle_envs(platform: Platform) -> Env { - let env = Env::default() - .add("CARGO_INCREMENTAL", 0) - .add("ZED_CLIENT_CHECKSUM_SEED", ZED_CLIENT_CHECKSUM_SEED) - .add("ZED_MINIDUMP_ENDPOINT", ZED_SENTRY_MINIDUMP_ENDPOINT); - - match platform { - Platform::Linux => env, - Platform::Mac => env - .add("MACOS_CERTIFICATE", MACOS_CERTIFICATE) - .add("MACOS_CERTIFICATE_PASSWORD", MACOS_CERTIFICATE_PASSWORD) - .add("APPLE_NOTARIZATION_KEY", APPLE_NOTARIZATION_KEY) - .add("APPLE_NOTARIZATION_KEY_ID", APPLE_NOTARIZATION_KEY_ID) - .add("APPLE_NOTARIZATION_ISSUER_ID", APPLE_NOTARIZATION_ISSUER_ID), - Platform::Windows => env - .add("AZURE_TENANT_ID", AZURE_SIGNING_TENANT_ID) - .add("AZURE_CLIENT_ID", AZURE_SIGNING_CLIENT_ID) - .add("AZURE_CLIENT_SECRET", AZURE_SIGNING_CLIENT_SECRET) - .add("ACCOUNT_NAME", AZURE_SIGNING_ACCOUNT_NAME) - .add("CERT_PROFILE_NAME", AZURE_SIGNING_CERT_PROFILE_NAME) - .add("ENDPOINT", AZURE_SIGNING_ENDPOINT) - .add("FILE_DIGEST", "SHA256") - .add("TIMESTAMP_DIGEST", "SHA256") - .add("TIMESTAMP_SERVER", "http://timestamp.acs.microsoft.com"), - } -} - -pub fn one_workflow_per_non_main_branch() -> Concurrency { - one_workflow_per_non_main_branch_and_token("") -} - -pub fn one_workflow_per_non_main_branch_and_token>(token: T) -> Concurrency { - Concurrency::default() - .group(format!( - concat!( - "${{{{ github.workflow }}}}-${{{{ github.ref_name }}}}-", - "${{{{ github.ref_name == 'main' && github.sha || 'anysha' }}}}{}" - ), - token.as_ref() - )) - .cancel_in_progress(true) -} - -pub(crate) fn allow_concurrent_runs() -> Concurrency { - Concurrency::default() - .group("${{ github.workflow }}-${{ github.ref_name }}-${{ github.run_id }}") - .cancel_in_progress(true) -} - -// Represents a pattern to check for changed files and corresponding output variable -pub struct PathCondition { - pub name: &'static str, - pub pattern: &'static str, - pub invert: bool, - pub set_by_step: RefCell>, -} -impl PathCondition { - pub fn new(name: &'static str, pattern: &'static str) -> Self { - Self { - name, - pattern, - invert: false, - set_by_step: Default::default(), - } - } - pub fn inverted(name: &'static str, pattern: &'static str) -> Self { - Self { - name, - pattern, - invert: true, - set_by_step: Default::default(), - } - } - pub fn guard(&self, job: NamedJob) -> NamedJob { - let set_by_step = self - .set_by_step - .borrow() - .clone() - .unwrap_or_else(|| panic!("condition {},is never set", self.name)); - NamedJob { - name: job.name, - job: job - .job - .add_need(set_by_step.clone()) - .cond(Expression::new(format!( - "needs.{}.outputs.{} == 'true'", - &set_by_step, self.name - ))), - } - } -} - -pub(crate) struct StepOutput { - pub name: &'static str, - step_id: String, -} - -impl StepOutput { - pub fn new(step: &Step, name: &'static str) -> Self { - Self { - name, - step_id: step - .value - .id - .clone() - .expect("Steps that produce outputs must have an ID"), - } - } - - pub fn expr(&self) -> String { - format!("steps.{}.outputs.{}", self.step_id, self.name) - } - - pub fn as_job_output(self, job: &NamedJob) -> JobOutput { - JobOutput { - job_name: job.name.clone(), - name: self.name, - } - } -} - -impl serde::Serialize for StepOutput { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl std::fmt::Display for StepOutput { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "${{{{ {} }}}}", self.expr()) - } -} - -pub(crate) struct JobOutput { - job_name: String, - name: &'static str, -} - -impl JobOutput { - pub fn expr(&self) -> String { - format!("needs.{}.outputs.{}", self.job_name, self.name) - } -} - -impl serde::Serialize for JobOutput { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl std::fmt::Display for JobOutput { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "${{{{ {} }}}}", self.expr()) - } -} - -pub struct WorkflowInput { - pub input_type: &'static str, - pub name: &'static str, - pub default: Option, -} - -impl WorkflowInput { - pub fn string(name: &'static str, default: Option) -> Self { - Self { - input_type: "string", - name, - default, - } - } - - pub fn bool(name: &'static str, default: Option) -> Self { - Self { - input_type: "boolean", - name, - default: default.as_ref().map(ToString::to_string), - } - } - - pub fn input(&self) -> WorkflowDispatchInput { - WorkflowDispatchInput { - description: self.name.to_owned(), - required: self.default.is_none(), - input_type: self.input_type.to_owned(), - default: self.default.clone(), - } - } - - pub fn call_input(&self) -> WorkflowCallInput { - WorkflowCallInput { - description: self.name.to_owned(), - required: self.default.is_none(), - input_type: self.input_type.to_owned(), - default: self.default.clone(), - } - } - - pub(crate) fn expr(&self) -> String { - format!("inputs.{}", self.name) - } -} - -impl std::fmt::Display for WorkflowInput { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "${{{{ {} }}}}", self.expr()) - } -} - -impl serde::Serialize for WorkflowInput { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -pub(crate) struct WorkflowSecret { - pub name: &'static str, - description: String, - required: bool, -} - -impl WorkflowSecret { - pub fn new(name: &'static str, description: impl ToString) -> Self { - Self { - name, - description: description.to_string(), - required: true, - } - } - - pub fn secret_configuration(&self) -> WorkflowCallSecret { - WorkflowCallSecret { - description: self.description.clone(), - required: self.required, - } - } -} - -impl std::fmt::Display for WorkflowSecret { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "${{{{ secrets.{} }}}}", self.name) - } -} - -impl serde::Serialize for WorkflowSecret { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -pub mod assets { - // NOTE: these asset names also exist in the zed.dev codebase. - pub const MAC_AARCH64: &str = "Zed-aarch64.dmg"; - pub const MAC_X86_64: &str = "Zed-x86_64.dmg"; - pub const LINUX_AARCH64: &str = "zed-linux-aarch64.tar.gz"; - pub const LINUX_X86_64: &str = "zed-linux-x86_64.tar.gz"; - pub const WINDOWS_X86_64: &str = "Zed-x86_64.exe"; - pub const WINDOWS_AARCH64: &str = "Zed-aarch64.exe"; - - pub const REMOTE_SERVER_MAC_AARCH64: &str = "zed-remote-server-macos-aarch64.gz"; - pub const REMOTE_SERVER_MAC_X86_64: &str = "zed-remote-server-macos-x86_64.gz"; - pub const REMOTE_SERVER_LINUX_AARCH64: &str = "zed-remote-server-linux-aarch64.gz"; - pub const REMOTE_SERVER_LINUX_X86_64: &str = "zed-remote-server-linux-x86_64.gz"; - - pub fn all() -> Vec<&'static str> { - vec![ - MAC_AARCH64, - MAC_X86_64, - LINUX_AARCH64, - LINUX_X86_64, - WINDOWS_X86_64, - WINDOWS_AARCH64, - REMOTE_SERVER_MAC_AARCH64, - REMOTE_SERVER_MAC_X86_64, - REMOTE_SERVER_LINUX_AARCH64, - REMOTE_SERVER_LINUX_X86_64, - ] - } -} diff --git a/tooling/xtask/src/workspace.rs b/tooling/xtask/src/workspace.rs deleted file mode 100644 index fd71aa6bbd..0000000000 --- a/tooling/xtask/src/workspace.rs +++ /dev/null @@ -1,9 +0,0 @@ -use anyhow::{Context as _, Result}; -use cargo_metadata::{Metadata, MetadataCommand}; - -/// Returns the Cargo workspace. -pub fn load_workspace() -> Result { - MetadataCommand::new() - .exec() - .context("failed to load cargo metadata") -} diff --git a/typos.toml b/typos.toml index 20a7b511a8..a367402ae6 100644 --- a/typos.toml +++ b/typos.toml @@ -4,77 +4,21 @@ ignore-hidden = false extend-exclude = [ ".git/", - # Contributor names aren't typos. - ".mailmap", - - # File suffixes aren't typos. - "crates/theme/src/icon_theme.rs", - "crates/extensions_ui/src/extension_suggest.rs", - - # Some mock data is flagged as typos. - "crates/assistant_tools/src/web_search_tool.rs", - - # Suppress false positives in database schema. - "crates/collab/migrations/20251208000000_test_schema.sql", - - # Not our typos. - "crates/livekit_api/", - # Vim makes heavy use of partial typing tables. - "crates/vim/", - # Editor and file finder rely on partial typing and custom in-string syntax. - "crates/file_finder/src/file_finder_tests.rs", - "crates/editor/src/editor_tests.rs", - # There are some names in the test data that are incorrectly flagged as typos. - "crates/git/test_data/blame_incremental_complex", - "crates/git/test_data/golden/blame_incremental_complex.json", - # We have some base64-encoded data that is incorrectly being flagged. - "crates/rpc/src/auth.rs", - # glsl isn't recognized by this tool. - "extensions/glsl/languages/glsl/", # Windows likes its abbreviations. "crates/gpui/src/platform/windows/directx_renderer.rs", "crates/gpui/src/platform/windows/events.rs", "crates/gpui/src/platform/windows/direct_write.rs", "crates/gpui/src/platform/windows/window.rs", - # Some typos in the base mdBook CSS. - "docs/theme/css/", - # Spellcheck triggers on `|Fixe[sd]|` regex part. - "script/danger/dangerfile.ts", - # Eval examples for prompts and criteria - "crates/eval/src/examples/", - # File type extensions are not typos - "crates/zed/resources/windows/zed.iss", - # typos-cli doesn't understand our `vˇariable` markup - "crates/editor/src/hover_links.rs", - # typos-cli doesn't understand `setis` is intentional test case - "crates/editor/src/code_completion_tests.rs", - # Linux repository structure is not a valid text, hence we should not check it for typos - "crates/project_panel/benches/linux_repo_snapshot.txt", - # Some multibuffer test cases have word fragments that register as typos - "crates/multi_buffer/src/multi_buffer_tests.rs", - # Macos apis + + # macOS APIs "crates/gpui/src/platform/mac/dispatcher.rs", ] [default] extend-ignore-re = [ - 'cl\[ist]', - '\[lan\]guage', - '"ba"', - "doas", - # ProtoLS crate with tree-sitter Protobuf grammar. - "protols", - # x11rb SelectionNotifyEvent struct field - "requestor", # macOS version "Big Sur", - # Not an actual typo but an intentionally invalid color, in `color_extractor` - "#fof", # Stripped version of reserved keyword `type` "typ", - # AMD GPU Services - "ags", - # AMD GPU Services - "AGS" ] -check-filename = true +check-filename = true \ No newline at end of file